@spzhongwin/skill-logger-plugin 1.0.17 → 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +959 -1595
- package/package.json +1 -1
- package/src/ws-client.test.ts +16 -0
- package/src/ws-client.ts +108 -89
package/package.json
CHANGED
package/src/ws-client.test.ts
CHANGED
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
readCronJobsByAgentId,
|
|
14
14
|
findInstalledExpertSkillsRoot,
|
|
15
15
|
shouldSyncBuiltInTemplate,
|
|
16
|
+
resolveGatewaySkillTarget,
|
|
17
|
+
isGatewaySkillCommand,
|
|
16
18
|
} from "./ws-client.ts";
|
|
17
19
|
|
|
18
20
|
describe("command code boundary", () => {
|
|
@@ -156,6 +158,20 @@ describe("built-in template update boundary", () => {
|
|
|
156
158
|
});
|
|
157
159
|
});
|
|
158
160
|
|
|
161
|
+
describe("Gateway 公共 Skill 安装边界", () => {
|
|
162
|
+
it("固定安装到 OpenClaw 顶层 skills,不接受指令传入任意目录", () => {
|
|
163
|
+
assert.equal(resolveGatewaySkillTarget("/var/openclaw"), "/var/openclaw/skills");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("仅允许 Skill 安装、更新和卸载使用 gateway 作用域", () => {
|
|
167
|
+
assert.equal(isGatewaySkillCommand("INSTALL_SKILL", "gateway"), true);
|
|
168
|
+
assert.equal(isGatewaySkillCommand("UPDATE_SKILL", "gateway"), true);
|
|
169
|
+
assert.equal(isGatewaySkillCommand("UNINSTALL_SKILL", "gateway"), true);
|
|
170
|
+
assert.equal(isGatewaySkillCommand("LIST_SKILLS", "gateway"), false);
|
|
171
|
+
assert.equal(isGatewaySkillCommand("UPDATE_SKILL", "agent"), false);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
159
175
|
describe("expert skill update target", () => {
|
|
160
176
|
it("返回 .user/skills 父目录,由安装器统一追加 skill code", async () => {
|
|
161
177
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-expert-skill-"));
|
package/src/ws-client.ts
CHANGED
|
@@ -140,6 +140,15 @@ export function shouldSyncBuiltInTemplate(action: string, isBuiltIn: unknown): b
|
|
|
140
140
|
return action === "UPDATE_SKILL" && isBuiltIn === true;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
export function resolveGatewaySkillTarget(home = openclawHome()): string {
|
|
144
|
+
return path.join(home, "skills");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function isGatewaySkillCommand(action: unknown, installScope: unknown): boolean {
|
|
148
|
+
return installScope === "gateway"
|
|
149
|
+
&& ["INSTALL_SKILL", "UPDATE_SKILL", "UNINSTALL_SKILL"].includes(String(action || ""));
|
|
150
|
+
}
|
|
151
|
+
|
|
143
152
|
export async function findInstalledExpertSkillsRoot(
|
|
144
153
|
rootPath: string,
|
|
145
154
|
pureId: string,
|
|
@@ -505,6 +514,7 @@ export class GatewayWsClient {
|
|
|
505
514
|
agentIds: Array.from(this.currentAgentIds),
|
|
506
515
|
clientTime: Date.now(),
|
|
507
516
|
supportsBatch: true,
|
|
517
|
+
supportsGatewaySkillScope: true,
|
|
508
518
|
}, "Heartbeat");
|
|
509
519
|
}
|
|
510
520
|
|
|
@@ -537,7 +547,7 @@ export class GatewayWsClient {
|
|
|
537
547
|
* 核心指令分发中心:完全跳过沙盒,基于 userId 直接进行底层物理文件操作
|
|
538
548
|
*/
|
|
539
549
|
private async handleMessage(msg: any) {
|
|
540
|
-
const { action, userId, code, url, force, version, replyId, isBuiltIn } = msg;
|
|
550
|
+
const { action, userId, code, url, force, version, replyId, isBuiltIn, installScope } = msg;
|
|
541
551
|
this.appendLogToFile("INFO", "Command", `Received WS message`, {
|
|
542
552
|
action,
|
|
543
553
|
userId,
|
|
@@ -545,6 +555,7 @@ export class GatewayWsClient {
|
|
|
545
555
|
version,
|
|
546
556
|
replyId,
|
|
547
557
|
isBuiltIn,
|
|
558
|
+
installScope,
|
|
548
559
|
hasDirectUrl: Boolean(url),
|
|
549
560
|
});
|
|
550
561
|
|
|
@@ -587,16 +598,19 @@ export class GatewayWsClient {
|
|
|
587
598
|
return;
|
|
588
599
|
}
|
|
589
600
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
601
|
+
const gatewaySkillCommand = isGatewaySkillCommand(action, installScope);
|
|
602
|
+
// Gateway 公共 Skill 命令不依赖 Agent。其余命令继续执行严格的用户目录寻址。
|
|
603
|
+
const pureId = gatewaySkillCommand ? undefined : normalizeAssistantUserId(userId);
|
|
604
|
+
if (!gatewaySkillCommand && !pureId) {
|
|
593
605
|
this.reply(replyId, { success: false, message: `Invalid userId: ${userId}`, action });
|
|
594
606
|
return;
|
|
595
607
|
}
|
|
596
608
|
try {
|
|
597
609
|
if (action === "INSTALL_SKILL") {
|
|
598
610
|
if (!safeCode) throw new Error("Missing code parameter");
|
|
599
|
-
const targetDir =
|
|
611
|
+
const targetDir = gatewaySkillCommand
|
|
612
|
+
? resolveGatewaySkillTarget()
|
|
613
|
+
: await this.resolveRegularSkillTarget(userId);
|
|
600
614
|
console.log(`[skill-logger-plugin][WS] Executing INSTALL for user ${userId}, code: ${safeCode}`);
|
|
601
615
|
this.appendLogToFile("INFO", "Command", `INSTALL_SKILL received`, { userId, code: safeCode, version });
|
|
602
616
|
const result = await this.options.updater.manualInstall({
|
|
@@ -607,113 +621,111 @@ export class GatewayWsClient {
|
|
|
607
621
|
targetDir,
|
|
608
622
|
trace: this.createInstallTrace({ action, replyId, userId, code: safeCode }),
|
|
609
623
|
});
|
|
610
|
-
this.reply(replyId, {
|
|
624
|
+
this.reply(replyId, {
|
|
625
|
+
success: result.success, message: result.message, action,
|
|
626
|
+
data: {code: safeCode, version, installScope: gatewaySkillCommand ? "gateway" : "agent"},
|
|
627
|
+
});
|
|
611
628
|
|
|
612
629
|
} else if (action === "UNINSTALL_SKILL") {
|
|
613
630
|
if (!safeCode) throw new Error("Missing code parameter");
|
|
614
|
-
const targetDir =
|
|
631
|
+
const targetDir = gatewaySkillCommand
|
|
632
|
+
? resolveGatewaySkillTarget()
|
|
633
|
+
: await this.resolveRegularSkillTarget(userId);
|
|
615
634
|
console.log(`[skill-logger-plugin][WS] Executing UNINSTALL for user ${userId}, code: ${safeCode}`);
|
|
616
635
|
this.appendLogToFile("INFO", "Command", `UNINSTALL_SKILL received`, { userId, code: safeCode });
|
|
617
636
|
const skillPath = path.join(targetDir, safeCode);
|
|
618
637
|
await fs.rm(skillPath, { recursive: true, force: true });
|
|
619
|
-
this.reply(replyId, {
|
|
638
|
+
this.reply(replyId, {
|
|
639
|
+
success: true, message: `Skill ${safeCode} removed`, action,
|
|
640
|
+
data: {code: safeCode, installScope: gatewaySkillCommand ? "gateway" : "agent", removed: true},
|
|
641
|
+
});
|
|
620
642
|
|
|
621
643
|
} else if (action === "LIST_SKILLS") {
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
const entries = await fs.readdir(targetDir, { withFileTypes: true });
|
|
636
|
-
|
|
637
|
-
const dirs = entries.filter(e => (e.isDirectory() || e.isSymbolicLink()) && !e.name.startsWith("."));
|
|
638
|
-
|
|
639
|
-
for (const e of dirs) {
|
|
640
|
-
const skillDir = path.join(targetDir, e.name);
|
|
641
|
-
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
642
|
-
|
|
644
|
+
const regularTargetDir = await this.resolveRegularSkillTarget(userId);
|
|
645
|
+
|
|
646
|
+
// 用户视角的有效 Skill = Agent workspace Skill + Gateway 顶层公共 Skill。
|
|
647
|
+
// 同 code 时公共内置版本后写覆盖,确保不会被快照差分误判为缺失。
|
|
648
|
+
const skillsByCode = new Map<string, any>();
|
|
649
|
+
const sources = [
|
|
650
|
+
{dir: regularTargetDir, gatewayBuiltIn: false},
|
|
651
|
+
{dir: resolveGatewaySkillTarget(), gatewayBuiltIn: true},
|
|
652
|
+
];
|
|
653
|
+
for (const source of sources) {
|
|
654
|
+
let entries;
|
|
643
655
|
try {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
continue;
|
|
656
|
+
entries = await fs.readdir(source.dir, {withFileTypes: true});
|
|
657
|
+
} catch (err: any) {
|
|
658
|
+
// 新用户的 Agent 私有目录尚未创建时,仍必须返回 Gateway 公共内置 Skill。
|
|
659
|
+
if (err?.code === "ENOENT") continue;
|
|
660
|
+
throw err;
|
|
648
661
|
}
|
|
649
|
-
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
try {
|
|
659
|
-
const mdContent = await fs.readFile(skillMdPath, 'utf8');
|
|
660
|
-
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(mdContent)?.[1] ?? "";
|
|
661
|
-
const parsedName = /(^|\n)name:\s*(.+)/.exec(fm)?.[2]?.trim();
|
|
662
|
-
if (parsedName) name = parsedName;
|
|
663
|
-
|
|
664
|
-
const descMatch = /(^|\n)description:\s*(?:>\s*\n\s*)?(.*?)(?=\n[a-z]+:|\n---|$)/is.exec(fm);
|
|
665
|
-
if (descMatch && descMatch[2]) {
|
|
666
|
-
description = descMatch[2].replace(/\n\s+/g, ' ').trim();
|
|
662
|
+
const dirs = entries.filter(e => (e.isDirectory() || e.isSymbolicLink()) && !e.name.startsWith("."));
|
|
663
|
+
for (const e of dirs) {
|
|
664
|
+
const skillDir = path.join(source.dir, e.name);
|
|
665
|
+
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
666
|
+
try {
|
|
667
|
+
const stat = await fs.stat(skillMdPath);
|
|
668
|
+
if (!stat.isFile()) continue;
|
|
669
|
+
} catch {
|
|
670
|
+
continue;
|
|
667
671
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
672
|
+
|
|
673
|
+
const metaPath = path.join(skillDir, ".meta.json");
|
|
674
|
+
let isPlatform = source.gatewayBuiltIn;
|
|
675
|
+
let isBuiltIn = source.gatewayBuiltIn || e.isSymbolicLink();
|
|
676
|
+
let metaData: any = null;
|
|
677
|
+
let name = e.name;
|
|
678
|
+
let description = "";
|
|
679
|
+
let skillVersion = "";
|
|
680
|
+
|
|
681
|
+
try {
|
|
682
|
+
const mdContent = await fs.readFile(skillMdPath, "utf8");
|
|
683
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(mdContent)?.[1] ?? "";
|
|
684
|
+
const parsedName = /(^|\n)name:\s*(.+)/.exec(fm)?.[2]?.trim();
|
|
685
|
+
if (parsedName) name = parsedName;
|
|
686
|
+
const descMatch = /(^|\n)description:\s*(?:>\s*\n\s*)?(.*?)(?=\n[a-z]+:|\n---|$)/is.exec(fm);
|
|
687
|
+
if (descMatch?.[2]) description = descMatch[2].replace(/\n\s+/g, " ").trim();
|
|
688
|
+
} catch {}
|
|
689
|
+
|
|
690
|
+
try {
|
|
691
|
+
const parsed = JSON.parse(await fs.readFile(metaPath, "utf8"));
|
|
692
|
+
if (parsed) {
|
|
693
|
+
if (parsed.ownerId === "CMS" || parsed.ownerId === "CMS_COMPAT") isPlatform = true;
|
|
694
|
+
if (parsed.isBuiltIn === true || parsed.ownerId === "built-in") isBuiltIn = true;
|
|
695
|
+
metaData = parsed;
|
|
696
|
+
}
|
|
697
|
+
} catch {}
|
|
698
|
+
|
|
699
|
+
const resolvedVersion = await readSkillVersion(skillDir);
|
|
700
|
+
if (resolvedVersion) skillVersion = resolvedVersion;
|
|
701
|
+
|
|
702
|
+
if (isPlatform) {
|
|
703
|
+
skillsByCode.set(e.name, {
|
|
704
|
+
code: e.name, isPlatform: true, isBuiltIn, version: skillVersion,
|
|
705
|
+
name, description, publishedAt: metaData?.publishedAt,
|
|
706
|
+
});
|
|
707
|
+
} else {
|
|
708
|
+
skillsByCode.set(e.name, {
|
|
709
|
+
code: e.name, isPlatform: false, isBuiltIn, version: skillVersion,
|
|
710
|
+
name, description,
|
|
711
|
+
});
|
|
677
712
|
}
|
|
678
|
-
} catch (err) {}
|
|
679
|
-
|
|
680
|
-
const resolvedVersion = await readSkillVersion(skillDir);
|
|
681
|
-
if (resolvedVersion) skillVersion = resolvedVersion;
|
|
682
|
-
|
|
683
|
-
if (isPlatform) {
|
|
684
|
-
list.push({
|
|
685
|
-
code: e.name,
|
|
686
|
-
isPlatform: true,
|
|
687
|
-
isBuiltIn: isBuiltIn,
|
|
688
|
-
version: skillVersion,
|
|
689
|
-
name,
|
|
690
|
-
description,
|
|
691
|
-
publishedAt: metaData?.publishedAt
|
|
692
|
-
});
|
|
693
|
-
} else {
|
|
694
|
-
list.push({
|
|
695
|
-
code: e.name,
|
|
696
|
-
isPlatform: false,
|
|
697
|
-
isBuiltIn: isBuiltIn,
|
|
698
|
-
version: skillVersion,
|
|
699
|
-
name: name,
|
|
700
|
-
description: description
|
|
701
|
-
});
|
|
702
713
|
}
|
|
703
714
|
}
|
|
704
|
-
this.reply(replyId, { success: true, data:
|
|
715
|
+
this.reply(replyId, { success: true, data: [...skillsByCode.values()], action });
|
|
705
716
|
|
|
706
717
|
} else if (action === "UPDATE_SKILL") {
|
|
707
718
|
if (!safeCode) throw new Error("Missing code parameter");
|
|
708
719
|
// 更新可能是大批量广播,增加防阻塞的随机延时 (0~5秒)
|
|
709
720
|
const delayMs = Math.random() * 5000;
|
|
710
|
-
const syncBuiltInTemplate = shouldSyncBuiltInTemplate(action, isBuiltIn);
|
|
721
|
+
const syncBuiltInTemplate = !gatewaySkillCommand && shouldSyncBuiltInTemplate(action, isBuiltIn);
|
|
711
722
|
console.log(`[skill-logger-plugin][WS] Scheduled UPDATE for user ${userId}, code: ${safeCode} in ${Math.round(delayMs)}ms`);
|
|
712
723
|
this.appendLogToFile("INFO", "Command", `Scheduled UPDATE_SKILL`, {
|
|
713
724
|
userId,
|
|
714
725
|
code: safeCode,
|
|
715
726
|
version,
|
|
716
727
|
isBuiltIn,
|
|
728
|
+
installScope,
|
|
717
729
|
syncBuiltInTemplate,
|
|
718
730
|
delayMs: Math.round(delayMs),
|
|
719
731
|
});
|
|
@@ -721,13 +733,17 @@ export class GatewayWsClient {
|
|
|
721
733
|
setTimeout(async () => {
|
|
722
734
|
try {
|
|
723
735
|
// 延时期间配置可能变化;在真正写盘前重新校验 enabled 与 workspace。
|
|
724
|
-
const targetDir =
|
|
736
|
+
const targetDir = gatewaySkillCommand
|
|
737
|
+
? resolveGatewaySkillTarget()
|
|
738
|
+
: await this.resolveRegularSkillTarget(userId);
|
|
725
739
|
const additionalTargetDirs: string[] = syncBuiltInTemplate
|
|
726
740
|
? [path.join(openclawHome(), "workspace-xgjk-assistant-template", "skills")]
|
|
727
741
|
: [];
|
|
728
742
|
|
|
729
743
|
// 同步更新用户的 expert skill 目录
|
|
730
|
-
const userSkillRoot =
|
|
744
|
+
const userSkillRoot = pureId
|
|
745
|
+
? await findInstalledExpertSkillsRoot(openclawHome(), pureId, safeCode)
|
|
746
|
+
: undefined;
|
|
731
747
|
if (userSkillRoot) additionalTargetDirs.push(userSkillRoot);
|
|
732
748
|
const result = await this.options.updater.manualInstall({
|
|
733
749
|
code: safeCode,
|
|
@@ -754,7 +770,10 @@ export class GatewayWsClient {
|
|
|
754
770
|
syncBuiltInTemplate,
|
|
755
771
|
});
|
|
756
772
|
if (replyId) {
|
|
757
|
-
this.reply(replyId, {
|
|
773
|
+
this.reply(replyId, {
|
|
774
|
+
success: result.success, message: result.message, action,
|
|
775
|
+
data: {code: safeCode, version, installScope: gatewaySkillCommand ? "gateway" : "agent"},
|
|
776
|
+
});
|
|
758
777
|
}
|
|
759
778
|
} catch (e: any) {
|
|
760
779
|
this.appendLogToFile("ERROR", "Command", `UPDATE_SKILL threw`, {
|