@spzhongwin/skill-logger-plugin 1.0.18 → 1.0.20
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 +2435 -916
- package/openclaw.plugin.json +6 -0
- package/package.json +22 -5
- package/src/free-skill-directory.test.ts +109 -0
- package/src/free-skill-directory.ts +188 -0
- package/src/free-skill-tool.test.ts +68 -0
- package/src/free-skill-tool.ts +99 -0
- package/src/free-skill-workspace.test.ts +109 -0
- package/src/free-skill-workspace.ts +223 -0
- package/src/free-skill-writer.test.ts +105 -0
- package/src/free-skill-writer.ts +591 -0
- package/src/index.ts +5 -0
- package/src/ws-client.test.ts +146 -0
- package/src/ws-client.ts +119 -6
package/src/ws-client.test.ts
CHANGED
|
@@ -5,6 +5,7 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { DatabaseSync } from "node:sqlite";
|
|
7
7
|
import {
|
|
8
|
+
GatewayWsClient,
|
|
8
9
|
normalizeAssistantUserId,
|
|
9
10
|
normalizeCommandCode,
|
|
10
11
|
enabledAgentIdsFromAccounts,
|
|
@@ -17,6 +18,151 @@ import {
|
|
|
17
18
|
isGatewaySkillCommand,
|
|
18
19
|
} from "./ws-client.ts";
|
|
19
20
|
|
|
21
|
+
async function handleClientMessage(
|
|
22
|
+
client: GatewayWsClient,
|
|
23
|
+
message: Record<string, unknown>,
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
await (client as unknown as { handleMessage(value: Record<string, unknown>): Promise<void> }).handleMessage(message);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createReplyingClient(gatewayId: string): { client: GatewayWsClient; replies: Record<string, unknown>[] } {
|
|
29
|
+
const replies: Record<string, unknown>[] = [];
|
|
30
|
+
const client = new GatewayWsClient({
|
|
31
|
+
serverUrl: "ws://localhost:1",
|
|
32
|
+
gatewayId,
|
|
33
|
+
updater: {} as never,
|
|
34
|
+
});
|
|
35
|
+
const socket = {
|
|
36
|
+
readyState: 1,
|
|
37
|
+
send(payload: string, callback?: (error?: Error) => void) {
|
|
38
|
+
replies.push(JSON.parse(payload) as Record<string, unknown>);
|
|
39
|
+
callback?.();
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
Object.assign(client as object, { ws: socket });
|
|
43
|
+
return { client, replies };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("LIST_FREE_SKILLS", () => {
|
|
47
|
+
it("通过 action 分发扫描当前 workspace,并返回来源明确的路径字段", async () => {
|
|
48
|
+
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "free-skill-ws-client-"));
|
|
49
|
+
try {
|
|
50
|
+
const skillPath = path.join(workspace, ".xg-platform", "demo");
|
|
51
|
+
fs.mkdirSync(skillPath, { recursive: true });
|
|
52
|
+
fs.writeFileSync(
|
|
53
|
+
path.join(skillPath, "SKILL.md"),
|
|
54
|
+
"---\nname: Demo\ndescription: local demo\nversion: 1.2.3\n---\n",
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const { client, replies } = createReplyingClient("gateway-local-1");
|
|
58
|
+
Object.assign(client as object, {
|
|
59
|
+
resolveRegularSkillContext: async () => ({
|
|
60
|
+
skillsDir: path.join(workspace, "skills"),
|
|
61
|
+
localAgentId: "sales-agent",
|
|
62
|
+
workspace,
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
await handleClientMessage(client, {
|
|
66
|
+
action: "LIST_FREE_SKILLS",
|
|
67
|
+
userId: "12345",
|
|
68
|
+
agentId: "injected-agent-must-be-ignored",
|
|
69
|
+
workspace: path.join(workspace, "..", "external-workspace"),
|
|
70
|
+
replyId: "free-1",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const hostSkillFilePath = fs.realpathSync(path.join(skillPath, "SKILL.md"));
|
|
74
|
+
assert.deepEqual(replies, [{
|
|
75
|
+
type: "REPLY",
|
|
76
|
+
replyId: "free-1",
|
|
77
|
+
success: true,
|
|
78
|
+
action: "LIST_FREE_SKILLS",
|
|
79
|
+
data: {
|
|
80
|
+
gatewayId: "gateway-local-1",
|
|
81
|
+
agentId: "sales-agent",
|
|
82
|
+
directoryPath: fs.realpathSync(path.join(workspace, ".xg-platform")),
|
|
83
|
+
skills: [{
|
|
84
|
+
code: "demo",
|
|
85
|
+
name: "Demo",
|
|
86
|
+
description: "local demo",
|
|
87
|
+
version: "1.2.3",
|
|
88
|
+
workspaceRelativePath: path.join(".xg-platform", "demo"),
|
|
89
|
+
skillFilePath: path.join(".xg-platform", "demo", "SKILL.md"),
|
|
90
|
+
hostSkillFilePath,
|
|
91
|
+
}],
|
|
92
|
+
},
|
|
93
|
+
}]);
|
|
94
|
+
} finally {
|
|
95
|
+
fs.rmSync(workspace, { recursive: true, force: true });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("忽略请求注入的外部 workspace 和 agentId,只使用解析出的 Agent workspace", async () => {
|
|
100
|
+
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "free-skill-resolved-ws-"));
|
|
101
|
+
const externalWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), "free-skill-injected-ws-"));
|
|
102
|
+
try {
|
|
103
|
+
const safeSkillPath = path.join(workspace, ".xg-platform", "safe");
|
|
104
|
+
fs.mkdirSync(safeSkillPath, { recursive: true });
|
|
105
|
+
fs.writeFileSync(path.join(safeSkillPath, "SKILL.md"), "---\nname: Safe\n---\n");
|
|
106
|
+
|
|
107
|
+
const externalSkillPath = path.join(externalWorkspace, ".xg-platform", "outside");
|
|
108
|
+
fs.mkdirSync(externalSkillPath, { recursive: true });
|
|
109
|
+
fs.writeFileSync(path.join(externalSkillPath, "SKILL.md"), "---\nname: Outside\n---\n");
|
|
110
|
+
|
|
111
|
+
const { client, replies } = createReplyingClient("gateway-local-2");
|
|
112
|
+
Object.assign(client as object, {
|
|
113
|
+
resolveRegularSkillContext: async () => ({
|
|
114
|
+
skillsDir: path.join(workspace, "skills"),
|
|
115
|
+
localAgentId: "resolved-agent",
|
|
116
|
+
workspace,
|
|
117
|
+
}),
|
|
118
|
+
});
|
|
119
|
+
await handleClientMessage(client, {
|
|
120
|
+
action: "LIST_FREE_SKILLS",
|
|
121
|
+
userId: "12345",
|
|
122
|
+
agentId: "injected-agent",
|
|
123
|
+
workspace: externalWorkspace,
|
|
124
|
+
workspacePath: externalWorkspace,
|
|
125
|
+
agentWorkspace: externalWorkspace,
|
|
126
|
+
replyId: "free-isolated",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const response = replies[0] as { data: Record<string, unknown> };
|
|
130
|
+
assert.equal(response.data.agentId, "resolved-agent");
|
|
131
|
+
assert.equal(response.data.directoryPath, fs.realpathSync(path.join(workspace, ".xg-platform")));
|
|
132
|
+
assert.deepEqual(response.data.skills, [{
|
|
133
|
+
code: "safe",
|
|
134
|
+
name: "Safe",
|
|
135
|
+
description: "",
|
|
136
|
+
version: "",
|
|
137
|
+
workspaceRelativePath: path.join(".xg-platform", "safe"),
|
|
138
|
+
skillFilePath: path.join(".xg-platform", "safe", "SKILL.md"),
|
|
139
|
+
hostSkillFilePath: fs.realpathSync(path.join(safeSkillPath, "SKILL.md")),
|
|
140
|
+
}]);
|
|
141
|
+
} finally {
|
|
142
|
+
fs.rmSync(workspace, { recursive: true, force: true });
|
|
143
|
+
fs.rmSync(externalWorkspace, { recursive: true, force: true });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("缺少 userId 时返回明确错误,不静默丢弃新 action", async () => {
|
|
148
|
+
const { client, replies } = createReplyingClient("gateway-local-1");
|
|
149
|
+
|
|
150
|
+
await handleClientMessage(client, {
|
|
151
|
+
action: "LIST_FREE_SKILLS",
|
|
152
|
+
replyId: "free-missing-user",
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
assert.deepEqual(replies, [{
|
|
156
|
+
type: "REPLY",
|
|
157
|
+
replyId: "free-missing-user",
|
|
158
|
+
success: false,
|
|
159
|
+
error: "INVALID_REQUEST",
|
|
160
|
+
message: "Missing userId parameter",
|
|
161
|
+
action: "LIST_FREE_SKILLS",
|
|
162
|
+
}]);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
20
166
|
describe("command code boundary", () => {
|
|
21
167
|
it("只接受单一路径段,不静默改写路径穿越输入", () => {
|
|
22
168
|
assert.equal(normalizeCommandCode("demo-skill"), "demo-skill");
|
package/src/ws-client.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
5
5
|
import { SkillUpdater } from "./updater.ts";
|
|
6
6
|
import { openclawHome } from "./paths.ts";
|
|
7
7
|
import { readSkillVersion } from "./skill-version.ts";
|
|
8
|
+
import { scanFreeSkillDirectory, type FreeSkill } from "./free-skill-directory.ts";
|
|
8
9
|
|
|
9
10
|
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
10
11
|
const HEARTBEAT_ACK_TIMEOUT_MS = 75_000;
|
|
@@ -184,6 +185,73 @@ export function readCronJobsByAgentId(
|
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
187
|
|
|
188
|
+
export type FreeSkillDirectorySkill = Pick<FreeSkill, "code" | "name" | "description" | "version"> & {
|
|
189
|
+
workspaceRelativePath: string;
|
|
190
|
+
skillFilePath: string;
|
|
191
|
+
hostSkillFilePath: string;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export type FreeSkillDirectoryResponse = {
|
|
195
|
+
gatewayId: string;
|
|
196
|
+
agentId: string;
|
|
197
|
+
directoryPath: string;
|
|
198
|
+
skills: FreeSkillDirectorySkill[];
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
function isWithinPath(parent: string, candidate: string): boolean {
|
|
202
|
+
const relative = path.relative(parent, candidate);
|
|
203
|
+
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function relativeWorkspacePath(workspace: string, target: string): string {
|
|
207
|
+
const relative = path.relative(workspace, target);
|
|
208
|
+
return isWithinPath(workspace, target) ? relative : "";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function resolveRealWorkspace(workspace: string): Promise<string> {
|
|
212
|
+
const resolved = path.resolve(workspace);
|
|
213
|
+
try {
|
|
214
|
+
return await fs.realpath(resolved);
|
|
215
|
+
} catch {
|
|
216
|
+
return resolved;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function resolveFreeSkillDirectoryPath(workspace: string): Promise<string> {
|
|
221
|
+
const workspacePath = await resolveRealWorkspace(workspace);
|
|
222
|
+
const candidate = path.resolve(workspacePath, ".xg-platform");
|
|
223
|
+
try {
|
|
224
|
+
const directoryPath = await fs.realpath(candidate);
|
|
225
|
+
const stat = await fs.stat(directoryPath);
|
|
226
|
+
return stat.isDirectory() && isWithinPath(workspacePath, directoryPath) ? directoryPath : "";
|
|
227
|
+
} catch {
|
|
228
|
+
return "";
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function buildFreeSkillDirectoryResponse(
|
|
233
|
+
gatewayId: string,
|
|
234
|
+
agentId: string,
|
|
235
|
+
workspacePath: string,
|
|
236
|
+
directoryPath: string,
|
|
237
|
+
skills: FreeSkill[],
|
|
238
|
+
): FreeSkillDirectoryResponse {
|
|
239
|
+
return {
|
|
240
|
+
gatewayId,
|
|
241
|
+
agentId,
|
|
242
|
+
directoryPath,
|
|
243
|
+
skills: skills.map((skill) => ({
|
|
244
|
+
code: skill.code,
|
|
245
|
+
name: skill.name,
|
|
246
|
+
description: skill.description,
|
|
247
|
+
version: skill.version,
|
|
248
|
+
workspaceRelativePath: relativeWorkspacePath(workspacePath, skill.skillPath),
|
|
249
|
+
skillFilePath: relativeWorkspacePath(workspacePath, skill.skillFilePath),
|
|
250
|
+
hostSkillFilePath: skill.skillFilePath,
|
|
251
|
+
})),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
187
255
|
export interface WsClientOptions {
|
|
188
256
|
serverUrl: string; // 例如: wss://api.aishuo.co/gateway/ws
|
|
189
257
|
authToken?: string; // 用于网关鉴权
|
|
@@ -239,16 +307,28 @@ export class GatewayWsClient {
|
|
|
239
307
|
};
|
|
240
308
|
}
|
|
241
309
|
|
|
242
|
-
|
|
243
|
-
private async resolveRegularSkillTarget(userId: string): Promise<string> {
|
|
310
|
+
private async readOpenclawConfig(): Promise<unknown> {
|
|
244
311
|
const configPath = path.join(openclawHome(), "openclaw.json");
|
|
245
|
-
let config: unknown;
|
|
246
312
|
try {
|
|
247
|
-
|
|
313
|
+
return JSON.parse(await fs.readFile(configPath, "utf-8"));
|
|
248
314
|
} catch (err: any) {
|
|
249
315
|
throw new Error(`无法读取 openclaw.json: ${err?.message || String(err)}`);
|
|
250
316
|
}
|
|
251
|
-
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private async resolveRegularSkillContext(userId: string): Promise<SkillInstallTarget & { workspace: string }> {
|
|
320
|
+
const target = resolveSkillInstallTarget(await this.readOpenclawConfig(), userId);
|
|
321
|
+
return { ...target, workspace: path.dirname(target.skillsDir) };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** 普通 Skill 的所有读写操作共用这一个配置驱动的寻址入口。 */
|
|
325
|
+
private async resolveRegularSkillTarget(userId: string): Promise<string> {
|
|
326
|
+
return (await this.resolveRegularSkillContext(userId)).skillsDir;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private async resolveFreeSkillRequestContext(userId: string): Promise<{ workspace: string; agentId: string }> {
|
|
330
|
+
const target = await this.resolveRegularSkillContext(userId);
|
|
331
|
+
return { workspace: target.workspace, agentId: target.localAgentId };
|
|
252
332
|
}
|
|
253
333
|
|
|
254
334
|
constructor(options: WsClientOptions) {
|
|
@@ -588,6 +668,14 @@ export class GatewayWsClient {
|
|
|
588
668
|
|
|
589
669
|
if (!userId) {
|
|
590
670
|
this.appendLogToFile("WARN", "Command", `Message dropped: missing userId`, msg);
|
|
671
|
+
if (action === "LIST_FREE_SKILLS") {
|
|
672
|
+
this.reply(replyId, {
|
|
673
|
+
success: false,
|
|
674
|
+
error: "INVALID_REQUEST",
|
|
675
|
+
message: "Missing userId parameter",
|
|
676
|
+
action,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
591
679
|
return;
|
|
592
680
|
}
|
|
593
681
|
|
|
@@ -640,6 +728,21 @@ export class GatewayWsClient {
|
|
|
640
728
|
data: {code: safeCode, installScope: gatewaySkillCommand ? "gateway" : "agent", removed: true},
|
|
641
729
|
});
|
|
642
730
|
|
|
731
|
+
} else if (action === "LIST_FREE_SKILLS") {
|
|
732
|
+
const context = await this.resolveFreeSkillRequestContext(userId);
|
|
733
|
+
const workspacePath = await resolveRealWorkspace(context.workspace);
|
|
734
|
+
const scannedSkills = await scanFreeSkillDirectory(context.workspace);
|
|
735
|
+
const directoryPath = scannedSkills[0]?.directoryPath
|
|
736
|
+
|| await resolveFreeSkillDirectoryPath(context.workspace);
|
|
737
|
+
const data = buildFreeSkillDirectoryResponse(
|
|
738
|
+
this.options.gatewayId,
|
|
739
|
+
context.agentId,
|
|
740
|
+
workspacePath,
|
|
741
|
+
directoryPath,
|
|
742
|
+
scannedSkills,
|
|
743
|
+
);
|
|
744
|
+
this.reply(replyId, { success: true, data, action });
|
|
745
|
+
|
|
643
746
|
} else if (action === "LIST_SKILLS") {
|
|
644
747
|
const regularTargetDir = await this.resolveRegularSkillTarget(userId);
|
|
645
748
|
|
|
@@ -899,7 +1002,17 @@ export class GatewayWsClient {
|
|
|
899
1002
|
} else {
|
|
900
1003
|
console.warn(`[skill-logger-plugin][WS] Unknown action: ${action}`);
|
|
901
1004
|
this.appendLogToFile("WARN", "Command", `Unknown action: ${action}`);
|
|
902
|
-
|
|
1005
|
+
if (action === "LIST_FREE_SKILLS") {
|
|
1006
|
+
this.reply(replyId, {
|
|
1007
|
+
success: false,
|
|
1008
|
+
unsupported: true,
|
|
1009
|
+
error: "UNSUPPORTED_ACTION",
|
|
1010
|
+
message: "LIST_FREE_SKILLS unsupported by this plugin",
|
|
1011
|
+
action,
|
|
1012
|
+
});
|
|
1013
|
+
} else {
|
|
1014
|
+
this.reply(replyId, { success: false, message: `Unknown action: ${action}`, action });
|
|
1015
|
+
}
|
|
903
1016
|
}
|
|
904
1017
|
} catch (err: any) {
|
|
905
1018
|
this.appendLogToFile("ERROR", "Command", `Error executing action ${action}`, err);
|