fix(skills): replace generated plugin skill directories · openclaw/openclaw@30cfcf2
2026-05-15
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
|
32 | 32 | |
33 | 33 | ### Fixes |
34 | 34 | |
| 35 | +- Plugin skills: replace generated Windows plugin-skill directories before publishing the current skill link, avoiding repeated `EINVAL` warnings from stale non-symlink entries. Fixes #81432. (#81446) Thanks @hclsys and @vincentkoc. |
35 | 36 | - Channels/config: treat channel entries with only `enabled: true` as configured state so plugin-backed channels can auto-enable from an explicit on switch. Fixes #81323. (#81331) Thanks @EvanYao826 and @vincentkoc. |
36 | 37 | - CLI/update: add an update finalization path for externally swapped core runtimes, running update-time doctor repair and plugin convergence from post-doctor config and install-record state before reporting completion. Thanks @shakkernerd. |
37 | 38 | - Agents/WebChat: stop a successful assistant turn whose stale `errorMessage` matches a billing, auth, or rate-limit pattern from rotating profiles, falling back, or surfacing a hard `FailoverError` unless the current attempt has a real failover failure. (#70900) Thanks @truffle-dev. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -473,6 +473,22 @@ describe("publishPluginSkills", () => {
|
473 | 473 | expect(fsSync.readlinkSync(path.join(managedDir, "my-skill"))).toBe(dir2); |
474 | 474 | }); |
475 | 475 | |
| 476 | +it("replaces generated Windows directory entries before publishing a current skill", async () => { |
| 477 | +const skillParent = await tempDirs.make("plugin-skills-"); |
| 478 | +const managedDir = await tempDirs.make("managed-skills-"); |
| 479 | + |
| 480 | +const dir = await writeSkillDir(skillParent, "my-skill"); |
| 481 | +const existingDir = path.join(managedDir, "my-skill"); |
| 482 | +await fs.mkdir(existingDir, { recursive: true }); |
| 483 | +await fs.writeFile(path.join(existingDir, "stale.txt"), "stale"); |
| 484 | + |
| 485 | +withPlatform("win32", () => { |
| 486 | +publishPluginSkills([dir], { pluginSkillsDir: managedDir }); |
| 487 | +}); |
| 488 | + |
| 489 | +expect(fsSync.readlinkSync(existingDir)).toBe(dir); |
| 490 | +}); |
| 491 | + |
476 | 492 | it("cleans up stale symlinks whose targets still exist", async () => { |
477 | 493 | const skillParent = await tempDirs.make("plugin-skills-"); |
478 | 494 | const managedDir = await tempDirs.make("managed-skills-"); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -220,11 +220,19 @@ function publishPluginSkills(skillDirs: string[], opts?: { pluginSkillsDir?: str
|
220 | 220 | // best-effort; symlink will fail below if dir is truly unusable |
221 | 221 | } |
222 | 222 | try { |
223 | | -const existingTarget = fs.readlinkSync(linkPath); |
224 | | -if (existingTarget === target) { |
| 223 | +const existingEntry = fs.lstatSync(linkPath); |
| 224 | +if (existingEntry.isSymbolicLink()) { |
| 225 | +const existingTarget = fs.readlinkSync(linkPath); |
| 226 | +if (existingTarget === target) { |
| 227 | +continue; |
| 228 | +} |
| 229 | +removeGeneratedPluginSkillEntry(linkPath); |
| 230 | +} else if (isGeneratedPluginSkillEntry(existingEntry)) { |
| 231 | +removeGeneratedPluginSkillEntry(linkPath); |
| 232 | +} else { |
| 233 | +log.warn(`plugin skill entry is not a generated symlink: ${linkPath}`); |
225 | 234 | continue; |
226 | 235 | } |
227 | | -removeGeneratedPluginSkillEntry(linkPath); |
228 | 236 | } catch (err) { |
229 | 237 | if (!isNotFoundError(err)) { |
230 | 238 | log.warn(`failed to inspect plugin skill symlink "${linkPath}": ${String(err)}`); |
@@ -259,7 +267,10 @@ function publishPluginSkills(skillDirs: string[], opts?: { pluginSkillsDir?: str
|
259 | 267 | } |
260 | 268 | } |
261 | 269 | |
262 | | -function isGeneratedPluginSkillEntry(entry: fs.Dirent): boolean { |
| 270 | +function isGeneratedPluginSkillEntry( |
| 271 | +entry: Pick<fs.Dirent, "isDirectory" | "isSymbolicLink">, |
| 272 | +): boolean { |
| 273 | +// Windows directory symlinks are junctions and lstat reports them as directories. |
263 | 274 | return entry.isSymbolicLink() || (process.platform === "win32" && entry.isDirectory()); |
264 | 275 | } |
265 | 276 | |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。