@workclaw/openclaw-workclaw 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/README.md +21 -1
- package/index.ts +210 -210
- package/openclaw.plugin.json +1 -0
- package/package.json +11 -4
- package/setup-entry.ts +6 -0
- package/skills/openclaw-workclaw-cron/SKILL.md +45 -28
- package/src/accounts.ts +62 -37
- package/src/api/accounts-api.ts +88 -89
- package/src/api/prompts-api.ts +70 -77
- package/src/api/session-api.ts +99 -108
- package/src/api/skills-api.ts +35 -37
- package/src/api/workspace.ts +27 -29
- package/src/channel.ts +200 -202
- package/src/config-schema.ts +9 -9
- package/src/connection/workclaw-client.ts +554 -567
- package/src/gateway/agent-handlers.ts +392 -426
- package/src/gateway/config-writer.ts +228 -243
- package/src/gateway/message-context.ts +534 -362
- package/src/gateway/message-dispatcher.ts +529 -489
- package/src/gateway/reconnect.ts +217 -113
- package/src/gateway/skills-handler.ts +408 -472
- package/src/gateway/skills-list-handler.ts +9 -9
- package/src/gateway/tools-list-handler.ts +70 -72
- package/src/gateway/workclaw-gateway.ts +328 -486
- package/src/media/upload.ts +83 -94
- package/src/outbound/index.ts +57 -55
- package/src/outbound/workclaw-sender.ts +134 -133
- package/src/runtime.ts +291 -194
- package/src/send.ts +1 -1
- package/src/tools/openclaw-workclaw-cron/api/index.ts +6 -6
- package/src/tools/openclaw-workclaw-cron/src/add/params.ts +20 -19
- package/src/tools/openclaw-workclaw-cron/src/add/sync.ts +2 -2
- package/src/tools/openclaw-workclaw-cron/src/disable/params.ts +1 -1
- package/src/tools/openclaw-workclaw-cron/src/disable/sync.ts +3 -3
- package/src/tools/openclaw-workclaw-cron/src/enable/params.ts +1 -1
- package/src/tools/openclaw-workclaw-cron/src/enable/sync.ts +3 -3
- package/src/tools/openclaw-workclaw-cron/src/notify/sync.ts +2 -2
- package/src/tools/openclaw-workclaw-cron/src/remove/params.ts +1 -1
- package/src/tools/openclaw-workclaw-cron/src/remove/sync.ts +3 -3
- package/src/tools/openclaw-workclaw-cron/src/update/params.ts +195 -197
- package/src/tools/openclaw-workclaw-cron/src/update/sync.ts +4 -4
- package/src/tools/openclaw-workclaw-system/src/get/index.ts +2 -2
- package/src/tools/openclaw-workclaw-system/src/token/index.ts +4 -4
- package/src/types.ts +38 -40
- package/src/utils/content.ts +16 -21
- package/tests/accounts.test.ts +285 -0
- package/tests/message-context.test.ts +313 -0
- package/tests/reconnect.test.ts +257 -0
- package/tests/workclaw-client.test.ts +112 -0
- package/tsconfig.json +8 -5
- package/vitest.config.ts +8 -0
|
@@ -4,158 +4,156 @@
|
|
|
4
4
|
* 通过 HTTP 回调返回结果
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import fs from
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
|
|
17
|
-
const execAsync = promisify(exec)
|
|
7
|
+
import { exec } from "node:child_process";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import fs from "node:fs/promises";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import https from "node:https";
|
|
13
|
+
import http from "node:http";
|
|
14
|
+
import zlib from "node:zlib";
|
|
15
|
+
import stream from "node:stream";
|
|
16
|
+
|
|
17
|
+
const execAsync = promisify(exec);
|
|
18
18
|
|
|
19
19
|
export interface SkillsEventData {
|
|
20
|
-
topic: string // skills/create, skills/update, skills/delete, skills/list, skills/get, skills/invoke
|
|
21
|
-
requestId: string
|
|
22
|
-
callbackUrl: string
|
|
23
|
-
authToken?: string
|
|
24
|
-
data: any
|
|
20
|
+
topic: string; // skills/create, skills/update, skills/delete, skills/list, skills/get, skills/invoke
|
|
21
|
+
requestId: string;
|
|
22
|
+
callbackUrl: string;
|
|
23
|
+
authToken?: string;
|
|
24
|
+
data: any;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export interface CallbackResponse {
|
|
28
|
-
userId?: string
|
|
29
|
-
agentId?: string
|
|
30
|
-
agentIds?: string[]
|
|
31
|
-
appKey?: string
|
|
32
|
-
dotype?: string
|
|
33
|
-
doStatus?: boolean
|
|
34
|
-
doErrorMsg?: string
|
|
28
|
+
userId?: string,
|
|
29
|
+
agentId?: string,
|
|
30
|
+
agentIds?: string[],
|
|
31
|
+
appKey?: string,
|
|
32
|
+
dotype?: string,
|
|
33
|
+
doStatus?: boolean,
|
|
34
|
+
doErrorMsg?: string,
|
|
35
35
|
dataList?: any[]
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// 回调 URL 映射
|
|
39
39
|
const TOPIC_TO_CALLBACK_URL: Record<string, string> = {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
40
|
+
"skills/add": "/open-apis/v1/claw/do/skills/result",
|
|
41
|
+
"skills/update": "/open-apis/v1/claw/do/skills/result",
|
|
42
|
+
"skills/remove": "/open-apis/v1/claw/do/skills/result",
|
|
43
|
+
"skills/list": "/open-apis/v1/claw/do/skills/result",
|
|
44
|
+
"skills/get": "/open-apis/v1/claw/do/skills/result",
|
|
45
|
+
"skills/invoke": "/open-apis/v1/claw/do/skills/result",
|
|
46
|
+
};
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* 处理 skills 事件
|
|
50
50
|
*/
|
|
51
51
|
export async function handleSkillsEvent(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
eventData: SkillsEventData,
|
|
53
|
+
token: string,
|
|
54
|
+
baseUrl: string,
|
|
55
|
+
appKey: string,
|
|
56
|
+
log?: {
|
|
57
|
+
info?: (msg: string) => void;
|
|
58
|
+
error?: (msg: string) => void;
|
|
59
|
+
},
|
|
60
60
|
): Promise<void> {
|
|
61
|
-
const { topic, data } = eventData
|
|
62
|
-
log?.info?.(`[SkillsHandler] Processing ${topic}, data: ${JSON.stringify(data)}`)
|
|
61
|
+
const { topic, data } = eventData;
|
|
62
|
+
log?.info?.(`[SkillsHandler] Processing ${topic}, data: ${JSON.stringify(data)}`);
|
|
63
63
|
|
|
64
64
|
try {
|
|
65
|
-
let result: any
|
|
65
|
+
let result: any;
|
|
66
66
|
|
|
67
67
|
// 根据 topic 路由到不同的处理函数
|
|
68
68
|
switch (topic) {
|
|
69
|
-
case
|
|
70
|
-
result = await handleCreateSkill(data, log)
|
|
71
|
-
break
|
|
72
|
-
case
|
|
73
|
-
result = await handleUpdateSkill(data, log)
|
|
74
|
-
break
|
|
75
|
-
case
|
|
76
|
-
result = await handleDeleteSkill(data, log)
|
|
77
|
-
break
|
|
78
|
-
case
|
|
79
|
-
result = await handleListSkills(data, log)
|
|
80
|
-
break
|
|
81
|
-
case
|
|
82
|
-
result = await handleGetSkill(data, log)
|
|
83
|
-
break
|
|
84
|
-
case
|
|
85
|
-
result = await handleInvokeSkill(data, log)
|
|
86
|
-
break
|
|
69
|
+
case "skills/add":
|
|
70
|
+
result = await handleCreateSkill(data, log);
|
|
71
|
+
break;
|
|
72
|
+
case "skills/update":
|
|
73
|
+
result = await handleUpdateSkill(data, log);
|
|
74
|
+
break;
|
|
75
|
+
case "skills/remove":
|
|
76
|
+
result = await handleDeleteSkill(data, log);
|
|
77
|
+
break;
|
|
78
|
+
case "skills/list":
|
|
79
|
+
result = await handleListSkills(data, log);
|
|
80
|
+
break;
|
|
81
|
+
case "skills/get":
|
|
82
|
+
result = await handleGetSkill(data, log);
|
|
83
|
+
break;
|
|
84
|
+
case "skills/invoke":
|
|
85
|
+
result = await handleInvokeSkill(data, log);
|
|
86
|
+
break;
|
|
87
87
|
default:
|
|
88
|
-
throw new Error(`Unknown topic: ${topic}`)
|
|
88
|
+
throw new Error(`Unknown topic: ${topic}`);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// 发送成功回调
|
|
92
92
|
await sendCallback(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
log?.info?.(`[SkillsHandler] ${topic} completed, callback sent`)
|
|
93
|
+
baseUrl,
|
|
94
|
+
topic,
|
|
95
|
+
{
|
|
96
|
+
userId: result.userId || "",
|
|
97
|
+
agentId: "",
|
|
98
|
+
appKey,
|
|
99
|
+
agentIds: Array.isArray(result.agentIds) ? result.agentIds : (result.agentIds || 0),
|
|
100
|
+
dotype: result.status || topic.split('/')[1],
|
|
101
|
+
doStatus: true,
|
|
102
|
+
doErrorMsg: "",
|
|
103
|
+
dataList: [
|
|
104
|
+
{
|
|
105
|
+
typeStr: "",
|
|
106
|
+
name: result.skillName || "",
|
|
107
|
+
description: "",
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
token,
|
|
112
|
+
log
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
log?.info?.(`[SkillsHandler] ${topic} completed, callback sent`);
|
|
116
116
|
|
|
117
117
|
// 重启 OpenClaw 网关(仅在添加、更新、删除技能时)
|
|
118
118
|
if (['skills/add', 'skills/update', 'skills/remove'].includes(topic)) {
|
|
119
119
|
try {
|
|
120
|
-
log?.info?.(`[SkillsHandler] rescan...`)
|
|
121
|
-
await execAsync(`openclaw skills rescan`, { timeout: 2000 })
|
|
122
|
-
await execAsync(`openclaw skills reload --force`, { timeout: 2000 })
|
|
123
|
-
await execAsync(`openclaw skills list`, { timeout: 2000 })
|
|
124
|
-
log?.info?.(`[SkillsHandler] OpenClaw skills rescaned successfully`)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
log?.error?.(`[SkillsHandler] Failed to rescan OpenClaw skills: ${err.message}`)
|
|
120
|
+
log?.info?.(`[SkillsHandler] rescan...`);
|
|
121
|
+
await execAsync(`openclaw skills rescan`, { timeout: 2000 });
|
|
122
|
+
await execAsync(`openclaw skills reload --force`, { timeout: 2000 });
|
|
123
|
+
await execAsync(`openclaw skills list`, { timeout: 2000 });
|
|
124
|
+
log?.info?.(`[SkillsHandler] OpenClaw skills rescaned successfully`);
|
|
125
|
+
} catch (err: any) {
|
|
126
|
+
log?.error?.(`[SkillsHandler] Failed to rescan OpenClaw skills: ${err.message}`);
|
|
128
127
|
// 重启 OpenClaw 网关
|
|
129
|
-
await execAsync(`openclaw skills restart`, { timeout: 20000 })
|
|
128
|
+
await execAsync(`openclaw skills restart`, { timeout: 20000 });
|
|
130
129
|
}
|
|
131
130
|
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
log?.error?.(`[SkillsHandler] ${topic} failed: ${err.message}`)
|
|
131
|
+
} catch (err: any) {
|
|
132
|
+
log?.error?.(`[SkillsHandler] ${topic} failed: ${err.message}`);
|
|
135
133
|
|
|
136
134
|
// 发送失败回调
|
|
137
135
|
await sendCallback(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
)
|
|
136
|
+
baseUrl,
|
|
137
|
+
topic,
|
|
138
|
+
{
|
|
139
|
+
userId: "",
|
|
140
|
+
agentId: "",
|
|
141
|
+
agentIds: [],
|
|
142
|
+
appKey,
|
|
143
|
+
dotype: topic.split("/")[1],
|
|
144
|
+
doStatus: false,
|
|
145
|
+
doErrorMsg: err.message,
|
|
146
|
+
dataList: [
|
|
147
|
+
{
|
|
148
|
+
typeStr: "",
|
|
149
|
+
name: "",
|
|
150
|
+
description: "",
|
|
151
|
+
}
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
token,
|
|
155
|
+
log
|
|
156
|
+
);
|
|
159
157
|
}
|
|
160
158
|
}
|
|
161
159
|
|
|
@@ -163,512 +161,452 @@ export async function handleSkillsEvent(
|
|
|
163
161
|
* 发送 HTTP 回调
|
|
164
162
|
*/
|
|
165
163
|
async function sendCallback(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
164
|
+
baseUrl: string,
|
|
165
|
+
topic: string,
|
|
166
|
+
response?: CallbackResponse,
|
|
167
|
+
authToken?: string,
|
|
168
|
+
log?: {
|
|
169
|
+
info?: (msg: string) => void;
|
|
170
|
+
error?: (msg: string) => void;
|
|
171
|
+
}
|
|
174
172
|
): Promise<void> {
|
|
175
173
|
// 获取回调 URL
|
|
176
|
-
const callbackPath = TOPIC_TO_CALLBACK_URL[topic]
|
|
174
|
+
const callbackPath = TOPIC_TO_CALLBACK_URL[topic];
|
|
177
175
|
if (!callbackPath) {
|
|
178
|
-
throw new Error(`No callback URL defined for topic: ${topic}`)
|
|
176
|
+
throw new Error(`No callback URL defined for topic: ${topic}`);
|
|
179
177
|
}
|
|
180
178
|
|
|
181
|
-
const callbackUrl = `${baseUrl.replace(/\/$/,
|
|
182
|
-
const responseData = JSON.stringify(response)
|
|
179
|
+
const callbackUrl = `${baseUrl.replace(/\/$/, "")}${callbackPath}`;
|
|
180
|
+
const responseData = JSON.stringify(response);
|
|
183
181
|
|
|
184
|
-
log?.info?.(`[SkillsHandler] Sending callback to ${callbackUrl}, data: ${responseData}`)
|
|
182
|
+
log?.info?.(`[SkillsHandler] Sending callback to ${callbackUrl}, data: ${responseData}`);
|
|
185
183
|
|
|
186
184
|
// 准备请求头
|
|
187
185
|
const headers: Record<string, string> = {
|
|
188
|
-
|
|
189
|
-
}
|
|
186
|
+
"Content-Type": "application/json",
|
|
187
|
+
};
|
|
190
188
|
|
|
191
189
|
if (authToken) {
|
|
192
|
-
headers
|
|
193
|
-
|
|
194
|
-
|
|
190
|
+
headers["Authorization"] = authToken.startsWith("Bearer ")
|
|
191
|
+
? authToken
|
|
192
|
+
: `Bearer ${authToken}`;
|
|
195
193
|
}
|
|
196
194
|
|
|
197
195
|
// 使用 AbortController 实现超时
|
|
198
|
-
const controller = new AbortController()
|
|
199
|
-
const timeoutId = setTimeout(() => controller.abort(), 30000)
|
|
196
|
+
const controller = new AbortController();
|
|
197
|
+
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
200
198
|
|
|
201
199
|
try {
|
|
202
200
|
const res = await fetch(callbackUrl, {
|
|
203
|
-
method:
|
|
201
|
+
method: "POST",
|
|
204
202
|
headers,
|
|
205
203
|
body: responseData,
|
|
206
204
|
signal: controller.signal,
|
|
207
|
-
})
|
|
205
|
+
});
|
|
208
206
|
|
|
209
|
-
clearTimeout(timeoutId)
|
|
207
|
+
clearTimeout(timeoutId);
|
|
210
208
|
|
|
211
209
|
if (!res.ok) {
|
|
212
|
-
const errorText = await res.text()
|
|
213
|
-
throw new Error(`HTTP ${res.status}: ${res.statusText || errorText}`)
|
|
210
|
+
const errorText = await res.text();
|
|
211
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText || errorText}`);
|
|
214
212
|
}
|
|
215
213
|
|
|
216
|
-
log?.info?.(`[SkillsHandler] Callback sent successfully, status: ${res.status}`)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
clearTimeout(timeoutId)
|
|
214
|
+
log?.info?.(`[SkillsHandler] Callback sent successfully, status: ${res.status}`);
|
|
215
|
+
} catch (err: any) {
|
|
216
|
+
clearTimeout(timeoutId);
|
|
220
217
|
|
|
221
|
-
if (err.name ===
|
|
222
|
-
log?.error?.(`[SkillsHandler] Callback timeout after 30s: ${callbackUrl}`)
|
|
223
|
-
throw new Error(
|
|
218
|
+
if (err.name === "AbortError") {
|
|
219
|
+
log?.error?.(`[SkillsHandler] Callback timeout after 30s: ${callbackUrl}`);
|
|
220
|
+
throw new Error("Callback timeout");
|
|
224
221
|
}
|
|
225
222
|
|
|
226
|
-
log?.error?.(`[SkillsHandler] Failed to send callback: ${err.message}`)
|
|
227
|
-
throw err
|
|
223
|
+
log?.error?.(`[SkillsHandler] Failed to send callback: ${err.message}`);
|
|
224
|
+
throw err;
|
|
228
225
|
}
|
|
229
226
|
}
|
|
230
227
|
|
|
231
228
|
/**
|
|
232
229
|
* 从 URL 下载文件内容
|
|
233
230
|
*/
|
|
234
|
-
async function downloadFromUrl(url: string): Promise<Buffer> {
|
|
231
|
+
async function downloadFromUrl(url: string, log?: any): Promise<Buffer> {
|
|
235
232
|
return new Promise((resolve, reject) => {
|
|
236
|
-
const client = url.startsWith(
|
|
233
|
+
const client = url.startsWith("https:") ? https : http;
|
|
237
234
|
|
|
238
235
|
const req = client.get(url, (res) => {
|
|
239
236
|
if (res.statusCode !== 200) {
|
|
240
|
-
reject(new Error(`Failed to download: HTTP ${res.statusCode}`))
|
|
241
|
-
return
|
|
237
|
+
reject(new Error(`Failed to download: HTTP ${res.statusCode}`));
|
|
238
|
+
return;
|
|
242
239
|
}
|
|
243
240
|
|
|
244
|
-
const chunks: Buffer[] = []
|
|
245
|
-
res.on(
|
|
246
|
-
chunks.push(chunk)
|
|
247
|
-
})
|
|
248
|
-
res.on(
|
|
249
|
-
const buffer = Buffer.concat(chunks)
|
|
250
|
-
resolve(buffer)
|
|
251
|
-
})
|
|
252
|
-
})
|
|
241
|
+
const chunks: Buffer[] = [];
|
|
242
|
+
res.on("data", (chunk) => {
|
|
243
|
+
chunks.push(chunk);
|
|
244
|
+
});
|
|
245
|
+
res.on("end", () => {
|
|
246
|
+
const buffer = Buffer.concat(chunks);
|
|
247
|
+
resolve(buffer);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
253
250
|
|
|
254
|
-
req.on(
|
|
255
|
-
reject(new Error(`Download failed: ${err.message}`))
|
|
256
|
-
})
|
|
251
|
+
req.on("error", (err) => {
|
|
252
|
+
reject(new Error(`Download failed: ${err.message}`));
|
|
253
|
+
});
|
|
257
254
|
|
|
258
255
|
req.setTimeout(30000, () => {
|
|
259
|
-
req.destroy()
|
|
260
|
-
reject(new Error(
|
|
261
|
-
})
|
|
262
|
-
})
|
|
256
|
+
req.destroy();
|
|
257
|
+
reject(new Error("Download timeout"));
|
|
258
|
+
});
|
|
259
|
+
});
|
|
263
260
|
}
|
|
264
261
|
|
|
265
262
|
/**
|
|
266
263
|
* 获取 OpenClaw 配置目录
|
|
267
264
|
*/
|
|
268
265
|
function getOpenClawConfigDir(): string {
|
|
269
|
-
const homeDir = os.homedir()
|
|
270
|
-
return path.join(homeDir,
|
|
266
|
+
const homeDir = os.homedir();
|
|
267
|
+
return path.join(homeDir, ".openclaw");
|
|
271
268
|
}
|
|
272
269
|
|
|
273
270
|
/**
|
|
274
271
|
* 获取 skills 目录
|
|
275
272
|
*/
|
|
276
273
|
async function getSkillsDir(agentId?: string, log?: any): Promise<string> {
|
|
277
|
-
const openclawDir = getOpenClawConfigDir()
|
|
278
|
-
let skillsDir = path.join(openclawDir,
|
|
274
|
+
const openclawDir = getOpenClawConfigDir();
|
|
275
|
+
let skillsDir = path.join(openclawDir, "skills");
|
|
279
276
|
|
|
280
277
|
// 如果有 agentId,添加到对应目录
|
|
281
278
|
if (agentId && agentId.trim()) {
|
|
282
|
-
skillsDir = path.join(openclawDir, `
|
|
279
|
+
skillsDir = path.join(openclawDir, "agents", `openclaw-workclaw-${agentId}`, "skills");
|
|
283
280
|
}
|
|
284
281
|
|
|
285
282
|
try {
|
|
286
|
-
await fs.mkdir(skillsDir, { recursive: true })
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
throw new Error(`Failed to create skills directory: ${err.message}`)
|
|
283
|
+
await fs.mkdir(skillsDir, { recursive: true });
|
|
284
|
+
} catch (err: any) {
|
|
285
|
+
log?.error?.(`[SkillsHandler] Failed to create skills directory: ${err.message}`);
|
|
286
|
+
throw new Error(`Failed to create skills directory: ${err.message}`);
|
|
291
287
|
}
|
|
292
288
|
|
|
293
|
-
return skillsDir
|
|
289
|
+
return skillsDir;
|
|
294
290
|
}
|
|
295
291
|
|
|
296
292
|
/**
|
|
297
293
|
* 安装 skill 到 OpenClaw
|
|
298
294
|
*/
|
|
299
295
|
async function installSkillToClaw(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
): Promise<{ success: boolean
|
|
296
|
+
skillName: string,
|
|
297
|
+
skillContent: Buffer,
|
|
298
|
+
agentId?: string,
|
|
299
|
+
log?: any
|
|
300
|
+
): Promise<{ success: boolean; message: string; path?: string }> {
|
|
305
301
|
try {
|
|
306
302
|
// 检测是否为 ZIP 文件(ZIP 文件以 PK 开头)
|
|
307
|
-
const isZip = skillContent.toString('utf8', 0, 2) === 'PK'
|
|
303
|
+
const isZip = skillContent.toString('utf8', 0, 2) === 'PK';
|
|
308
304
|
|
|
309
305
|
if (isZip) {
|
|
310
|
-
log?.info?.(`[SkillsHandler] Detected ZIP file, extracting...`)
|
|
306
|
+
log?.info?.(`[SkillsHandler] Detected ZIP file, extracting...`);
|
|
311
307
|
|
|
312
308
|
// 获取 skills 目录
|
|
313
|
-
const skillsDir = await getSkillsDir(agentId, log)
|
|
309
|
+
const skillsDir = await getSkillsDir(agentId, log);
|
|
314
310
|
// 创建以 skillName 命名的目录
|
|
315
|
-
const skillDir = path.join(skillsDir, skillName)
|
|
316
|
-
const tempZipPath = path.join(skillsDir, `${skillName}.zip`)
|
|
311
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
312
|
+
const tempZipPath = path.join(skillsDir, `${skillName}.zip`);
|
|
317
313
|
|
|
318
314
|
try {
|
|
319
315
|
// 创建技能目录
|
|
320
|
-
await fs.mkdir(skillDir, { recursive: true })
|
|
321
|
-
log?.info?.(`[SkillsHandler] Skill directory created: ${skillDir}`)
|
|
316
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
317
|
+
log?.info?.(`[SkillsHandler] Skill directory created: ${skillDir}`);
|
|
322
318
|
|
|
323
319
|
// 写入临时 ZIP 文件
|
|
324
|
-
await fs.writeFile(tempZipPath, skillContent)
|
|
325
|
-
log?.info?.(`[SkillsHandler] Temporary ZIP file created: ${tempZipPath}`)
|
|
320
|
+
await fs.writeFile(tempZipPath, skillContent);
|
|
321
|
+
log?.info?.(`[SkillsHandler] Temporary ZIP file created: ${tempZipPath}`);
|
|
326
322
|
|
|
327
323
|
// 解压 ZIP 文件到技能目录
|
|
328
324
|
if (process.platform === 'win32') {
|
|
329
325
|
// Windows 系统使用 PowerShell 解压
|
|
330
|
-
await execAsync(`powershell -Command "Expand-Archive -Path '${tempZipPath}' -DestinationPath '${skillDir}' -Force"`)
|
|
331
|
-
}
|
|
332
|
-
else {
|
|
326
|
+
await execAsync(`powershell -Command "Expand-Archive -Path '${tempZipPath}' -DestinationPath '${skillDir}' -Force"`);
|
|
327
|
+
} else {
|
|
333
328
|
// Unix-like 系统使用 unzip 命令
|
|
334
|
-
await execAsync(`unzip -o '${tempZipPath}' -d '${skillDir}'`)
|
|
329
|
+
await execAsync(`unzip -o '${tempZipPath}' -d '${skillDir}'`);
|
|
335
330
|
}
|
|
336
331
|
|
|
337
|
-
log?.info?.(`[SkillsHandler] ZIP file extracted successfully to: ${skillDir}`)
|
|
332
|
+
log?.info?.(`[SkillsHandler] ZIP file extracted successfully to: ${skillDir}`);
|
|
338
333
|
|
|
339
334
|
// 删除临时 ZIP 文件
|
|
340
|
-
await fs.unlink(tempZipPath)
|
|
341
|
-
log?.info?.(`[SkillsHandler] Temporary ZIP file deleted`)
|
|
335
|
+
await fs.unlink(tempZipPath);
|
|
336
|
+
log?.info?.(`[SkillsHandler] Temporary ZIP file deleted`);
|
|
342
337
|
|
|
343
338
|
return {
|
|
344
339
|
success: true,
|
|
345
340
|
message: `ZIP skill package ${skillName} installed successfully`,
|
|
346
341
|
path: skillDir,
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
log?.error?.(`[SkillsHandler] Failed to extract ZIP file: ${err.message}`)
|
|
342
|
+
};
|
|
343
|
+
} catch (err: any) {
|
|
344
|
+
log?.error?.(`[SkillsHandler] Failed to extract ZIP file: ${err.message}`);
|
|
351
345
|
// 清理临时文件和目录
|
|
352
346
|
try {
|
|
353
|
-
await fs.unlink(tempZipPath)
|
|
354
|
-
}
|
|
355
|
-
catch {}
|
|
347
|
+
await fs.unlink(tempZipPath);
|
|
348
|
+
} catch {}
|
|
356
349
|
try {
|
|
357
|
-
await fs.rm(skillDir, { recursive: true, force: true })
|
|
358
|
-
}
|
|
359
|
-
catch {}
|
|
350
|
+
await fs.rm(skillDir, { recursive: true, force: true });
|
|
351
|
+
} catch {}
|
|
360
352
|
return {
|
|
361
353
|
success: false,
|
|
362
354
|
message: `Failed to extract ZIP file: ${err.message}`,
|
|
363
|
-
}
|
|
355
|
+
};
|
|
364
356
|
}
|
|
365
357
|
}
|
|
366
358
|
|
|
367
359
|
// 尝试解析为 JSON
|
|
368
|
-
let skillData: any
|
|
360
|
+
let skillData: any;
|
|
369
361
|
try {
|
|
370
|
-
const jsonString = skillContent.toString('utf8')
|
|
371
|
-
skillData = JSON.parse(jsonString)
|
|
372
|
-
}
|
|
373
|
-
catch (err: any) {
|
|
362
|
+
const jsonString = skillContent.toString('utf8');
|
|
363
|
+
skillData = JSON.parse(jsonString);
|
|
364
|
+
} catch (err: any) {
|
|
374
365
|
return {
|
|
375
366
|
success: false,
|
|
376
367
|
message: `Invalid skill JSON: ${err.message}`,
|
|
377
|
-
}
|
|
368
|
+
};
|
|
378
369
|
}
|
|
379
370
|
|
|
380
371
|
// 验证必要的字段
|
|
381
372
|
if (!skillData.id || !skillData.name) {
|
|
382
373
|
return {
|
|
383
374
|
success: false,
|
|
384
|
-
message:
|
|
385
|
-
}
|
|
375
|
+
message: "Missing required fields in skill: id, name",
|
|
376
|
+
};
|
|
386
377
|
}
|
|
387
378
|
|
|
388
379
|
// 获取 skills 目录
|
|
389
|
-
const skillsDir = await getSkillsDir(agentId, log)
|
|
390
|
-
const skillFilePath = path.join(skillsDir, `${skillName}.json`)
|
|
380
|
+
const skillsDir = await getSkillsDir(agentId, log);
|
|
381
|
+
const skillFilePath = path.join(skillsDir, `${skillName}.json`);
|
|
391
382
|
|
|
392
383
|
// 写入 skill 文件
|
|
393
|
-
await fs.writeFile(skillFilePath, skillContent,
|
|
394
|
-
log?.info?.(`[SkillsHandler] Skill saved to: ${skillFilePath}`)
|
|
384
|
+
await fs.writeFile(skillFilePath, skillContent, "utf-8");
|
|
385
|
+
log?.info?.(`[SkillsHandler] Skill saved to: ${skillFilePath}`);
|
|
395
386
|
|
|
396
387
|
return {
|
|
397
388
|
success: true,
|
|
398
389
|
message: `Skill ${skillName} installed successfully`,
|
|
399
390
|
path: skillFilePath,
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
catch (err: any) {
|
|
391
|
+
};
|
|
392
|
+
} catch (err: any) {
|
|
403
393
|
return {
|
|
404
394
|
success: false,
|
|
405
395
|
message: `Failed to install skill: ${err.message}`,
|
|
406
|
-
}
|
|
396
|
+
};
|
|
407
397
|
}
|
|
408
398
|
}
|
|
409
399
|
|
|
410
400
|
/**
|
|
411
401
|
* 创建 Skill
|
|
412
402
|
*/
|
|
413
|
-
async function installSkillToLocations(skillName: string, skillContent: Buffer, locations: (string | undefined)[], log?: any): Promise<any[]> {
|
|
414
|
-
const installResults = []
|
|
415
|
-
|
|
416
|
-
for (const agentId of locations) {
|
|
417
|
-
const installResult = await installSkillToClaw(skillName, skillContent, agentId, log)
|
|
418
|
-
if (!installResult.success) {
|
|
419
|
-
const error: any = new Error(`Failed to install skill to ${agentId ? `agent ${agentId}` : 'default location'}: ${installResult.message}`)
|
|
420
|
-
error.code = 'INSTALL_FAILED'
|
|
421
|
-
throw error
|
|
422
|
-
}
|
|
423
|
-
installResults.push(installResult)
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
return installResults
|
|
427
|
-
}
|
|
428
|
-
|
|
429
403
|
async function handleCreateSkill(
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
404
|
+
data: any,
|
|
405
|
+
log?: {
|
|
406
|
+
info?: (msg: string) => void;
|
|
407
|
+
error?: (msg: string) => void;
|
|
408
|
+
}
|
|
435
409
|
): Promise<any> {
|
|
436
|
-
const { userId, skillName, unloadUrl
|
|
437
|
-
const agentIds = Array.isArray(data?.agentIds) ? data.agentIds : (data?.agentIds ? [data.agentIds] : [])
|
|
410
|
+
const { userId, skillName, unloadUrl } = data;
|
|
411
|
+
const agentIds = Array.isArray(data?.agentIds) ? data.agentIds : (data?.agentIds ? [data.agentIds] : []);
|
|
438
412
|
|
|
439
413
|
if (!userId || !skillName) {
|
|
440
|
-
const error: any = new Error(
|
|
441
|
-
error.code =
|
|
442
|
-
throw error
|
|
414
|
+
const error: any = new Error("Missing required fields: userId, skillName");
|
|
415
|
+
error.code = "MISSING_FIELDS";
|
|
416
|
+
throw error;
|
|
443
417
|
}
|
|
444
418
|
|
|
445
|
-
log?.info?.(`[SkillsHandler] Creating skill: ${skillName}`)
|
|
446
|
-
log?.info?.(`[SkillsHandler] mainId: ${mainId}`)
|
|
419
|
+
log?.info?.(`[SkillsHandler] Creating skill: ${skillName}`);
|
|
447
420
|
if (agentIds.length > 0) {
|
|
448
|
-
log?.info?.(`[SkillsHandler] Target agents: ${agentIds.join(
|
|
421
|
+
log?.info?.(`[SkillsHandler] Target agents: ${agentIds.join(", ")}`);
|
|
449
422
|
}
|
|
450
423
|
|
|
451
424
|
// 如果提供了 URL,从 URL 下载 skill 定义
|
|
452
425
|
if (unloadUrl) {
|
|
453
|
-
log?.info?.(`[SkillsHandler] Downloading skill from URL: ${unloadUrl}`)
|
|
426
|
+
log?.info?.(`[SkillsHandler] Downloading skill from URL: ${unloadUrl}`);
|
|
454
427
|
|
|
455
428
|
try {
|
|
456
429
|
// 下载 skill 定义
|
|
457
|
-
const skillContent = await downloadFromUrl(unloadUrl)
|
|
458
|
-
log?.info?.(`[SkillsHandler] Downloaded skill content, length: ${skillContent.length} bytes`)
|
|
459
|
-
|
|
460
|
-
//
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
log?.info?.(`[SkillsHandler] Only mainId found, installing to default location`)
|
|
471
|
-
installLocations.push(undefined)
|
|
472
|
-
}
|
|
473
|
-
else if (hasMainIdAndOthers) {
|
|
474
|
-
// 如果有 mainId 和其他 id,安装到默认位置和其他 id 位置
|
|
475
|
-
log?.info?.(`[SkillsHandler] mainId and other agents found, installing to default location and other agents`)
|
|
476
|
-
installLocations.push(undefined)
|
|
477
|
-
// 添加其他 id 位置(排除 mainId)
|
|
478
|
-
installLocations.push(...agentIds.filter((id: any) => id !== mainId))
|
|
479
|
-
}
|
|
480
|
-
else if (agentIds.length > 0) {
|
|
481
|
-
// 如果没有 mainId,安装到所有指定的 agentId 位置
|
|
482
|
-
log?.info?.(`[SkillsHandler] No mainId found, installing to all specified agents`)
|
|
483
|
-
installLocations.push(...agentIds)
|
|
484
|
-
}
|
|
485
|
-
else {
|
|
486
|
-
// 如果没有指定智能体,安装到默认位置
|
|
487
|
-
log?.info?.(`[SkillsHandler] No agents specified, installing to default location`)
|
|
488
|
-
installLocations.push(undefined)
|
|
430
|
+
const skillContent = await downloadFromUrl(unloadUrl, log);
|
|
431
|
+
log?.info?.(`[SkillsHandler] Downloaded skill content, length: ${skillContent.length} bytes`);
|
|
432
|
+
|
|
433
|
+
// 安装 skill 到多个智能体
|
|
434
|
+
const installResults = [];
|
|
435
|
+
for (const agentId of agentIds) {
|
|
436
|
+
const installResult = await installSkillToClaw(skillName, skillContent, agentId, log);
|
|
437
|
+
if (!installResult.success) {
|
|
438
|
+
const error: any = new Error(`Failed to install skill to agent ${agentId}: ${installResult.message}`);
|
|
439
|
+
error.code = "INSTALL_FAILED";
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
installResults.push(installResult);
|
|
489
443
|
}
|
|
490
444
|
|
|
491
|
-
//
|
|
492
|
-
|
|
445
|
+
// 如果没有指定智能体,安装到默认位置
|
|
446
|
+
if (agentIds.length === 0) {
|
|
447
|
+
const installResult = await installSkillToClaw(skillName, skillContent, undefined, log);
|
|
448
|
+
if (!installResult.success) {
|
|
449
|
+
const error: any = new Error(installResult.message);
|
|
450
|
+
error.code = "INSTALL_FAILED";
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
installResults.push(installResult);
|
|
454
|
+
}
|
|
493
455
|
|
|
494
|
-
log?.info?.(`[SkillsHandler] Skill installed to ${installResults.length} locations`)
|
|
456
|
+
log?.info?.(`[SkillsHandler] Skill installed to ${installResults.length} locations`);
|
|
495
457
|
|
|
496
458
|
return {
|
|
497
459
|
userId,
|
|
498
460
|
skillName,
|
|
499
|
-
agentId:
|
|
461
|
+
agentId: "",
|
|
500
462
|
agentIds: agentIds.length > 0 ? agentIds : undefined,
|
|
501
|
-
status:
|
|
463
|
+
status: "add",
|
|
502
464
|
installedAt: new Date().toISOString(),
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
throw err
|
|
465
|
+
};
|
|
466
|
+
} catch (err: any) {
|
|
467
|
+
log?.error?.(`[SkillsHandler] Failed to create skill from URL: ${err.message}`);
|
|
468
|
+
throw err;
|
|
508
469
|
}
|
|
509
470
|
}
|
|
510
471
|
|
|
511
472
|
return {
|
|
512
473
|
userId,
|
|
513
474
|
skillName,
|
|
514
|
-
agentId:
|
|
475
|
+
agentId: "",
|
|
515
476
|
agentIds: agentIds.length > 0 ? agentIds : undefined,
|
|
516
|
-
status:
|
|
477
|
+
status: "add",
|
|
517
478
|
createdAt: new Date().toISOString(),
|
|
518
|
-
}
|
|
479
|
+
};
|
|
519
480
|
}
|
|
520
481
|
|
|
521
482
|
/**
|
|
522
483
|
* 更新 Skill
|
|
523
484
|
*/
|
|
524
485
|
async function handleUpdateSkill(
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
486
|
+
data: any,
|
|
487
|
+
log?: {
|
|
488
|
+
info?: (msg: string) => void;
|
|
489
|
+
error?: (msg: string) => void;
|
|
490
|
+
}
|
|
530
491
|
): Promise<any> {
|
|
531
|
-
const { id, ...updates } = data
|
|
492
|
+
const { id, ...updates } = data;
|
|
532
493
|
|
|
533
494
|
if (!id) {
|
|
534
|
-
const error: any = new Error(
|
|
535
|
-
error.code =
|
|
536
|
-
throw error
|
|
495
|
+
const error: any = new Error("Missing required field: id");
|
|
496
|
+
error.code = "MISSING_FIELDS";
|
|
497
|
+
throw error;
|
|
537
498
|
}
|
|
538
499
|
|
|
539
|
-
log?.info?.(`[SkillsHandler] Updating skill: ${id}`)
|
|
500
|
+
log?.info?.(`[SkillsHandler] Updating skill: ${id}`);
|
|
540
501
|
|
|
541
502
|
return {
|
|
542
503
|
id,
|
|
543
504
|
...updates,
|
|
544
|
-
status:
|
|
505
|
+
status: "updated",
|
|
545
506
|
updatedAt: new Date().toISOString(),
|
|
546
|
-
}
|
|
507
|
+
};
|
|
547
508
|
}
|
|
548
509
|
|
|
549
510
|
/**
|
|
550
511
|
* 删除 Skill
|
|
551
512
|
*/
|
|
552
|
-
async function deleteSkillAtLocation(skillName: string, agentId: string | undefined, log?: any): Promise<boolean> {
|
|
553
|
-
try {
|
|
554
|
-
const skillsDir = await getSkillsDir(agentId, log)
|
|
555
|
-
|
|
556
|
-
const skillFilePath = path.join(skillsDir, `${skillName}.json`)
|
|
557
|
-
const skillDirPath = path.join(skillsDir, skillName)
|
|
558
|
-
|
|
559
|
-
let deleted = false
|
|
560
|
-
|
|
561
|
-
// 尝试删除技能文件
|
|
562
|
-
try {
|
|
563
|
-
await fs.unlink(skillFilePath)
|
|
564
|
-
log?.info?.(`[SkillsHandler] Skill file deleted: ${skillFilePath}`)
|
|
565
|
-
deleted = true
|
|
566
|
-
}
|
|
567
|
-
catch {
|
|
568
|
-
// 文件不存在,继续尝试删除目录
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// 尝试删除技能目录(ZIP 安装的技能)
|
|
572
|
-
try {
|
|
573
|
-
await fs.rm(skillDirPath, { recursive: true, force: true })
|
|
574
|
-
log?.info?.(`[SkillsHandler] Skill directory deleted: ${skillDirPath}`)
|
|
575
|
-
deleted = true
|
|
576
|
-
}
|
|
577
|
-
catch {
|
|
578
|
-
// 目录不存在,继续
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
return deleted
|
|
582
|
-
}
|
|
583
|
-
catch (err: any) {
|
|
584
|
-
log?.error?.(`[SkillsHandler] Failed to delete skill at location: ${err.message}`)
|
|
585
|
-
return false
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
513
|
async function handleDeleteSkill(
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
514
|
+
data: any,
|
|
515
|
+
log?: {
|
|
516
|
+
info?: (msg: string) => void;
|
|
517
|
+
error?: (msg: string) => void;
|
|
518
|
+
}
|
|
595
519
|
): Promise<any> {
|
|
596
|
-
const { userId, skillName, agentIds
|
|
597
|
-
const agentIdsList = Array.isArray(agentIds) ? agentIds : (agentIds ? [agentIds] : [])
|
|
520
|
+
const { userId, skillName, agentIds } = data;
|
|
521
|
+
const agentIdsList = Array.isArray(agentIds) ? agentIds : (agentIds ? [agentIds] : []);
|
|
598
522
|
|
|
599
523
|
if (!skillName) {
|
|
600
|
-
const error: any = new Error(
|
|
601
|
-
error.code =
|
|
602
|
-
throw error
|
|
524
|
+
const error: any = new Error("Missing required field: skillName");
|
|
525
|
+
error.code = "MISSING_FIELDS";
|
|
526
|
+
throw error;
|
|
603
527
|
}
|
|
604
528
|
|
|
605
|
-
log?.info?.(`[SkillsHandler] Deleting skill: ${skillName}`)
|
|
606
|
-
log?.info?.(`[SkillsHandler] mainId: ${mainId}`)
|
|
529
|
+
log?.info?.(`[SkillsHandler] Deleting skill: ${skillName}`);
|
|
607
530
|
if (agentIdsList.length > 0) {
|
|
608
|
-
log?.info?.(`[SkillsHandler] Target agents: ${agentIdsList.join(
|
|
531
|
+
log?.info?.(`[SkillsHandler] Target agents: ${agentIdsList.join(", ")}`);
|
|
609
532
|
}
|
|
610
533
|
|
|
611
534
|
try {
|
|
612
|
-
let deleted = false
|
|
535
|
+
let deleted = false;
|
|
613
536
|
|
|
614
|
-
//
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
537
|
+
// 从多个智能体中删除技能
|
|
538
|
+
for (const agentId of agentIdsList) {
|
|
539
|
+
// 获取技能目录
|
|
540
|
+
const skillsDir = await getSkillsDir(agentId, log);
|
|
618
541
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
deleted = deleted || defaultDeleted
|
|
631
|
-
|
|
632
|
-
// 从其他 id 位置删除(排除 mainId)
|
|
633
|
-
for (const agentId of agentIdsList) {
|
|
634
|
-
if (agentId !== mainId) {
|
|
635
|
-
const agentDeleted = await deleteSkillAtLocation(skillName, agentId, log)
|
|
636
|
-
deleted = deleted || agentDeleted
|
|
637
|
-
}
|
|
542
|
+
// 检查技能文件和目录
|
|
543
|
+
const skillFilePath = path.join(skillsDir, `${skillName}.json`);
|
|
544
|
+
const skillDirPath = path.join(skillsDir, skillName);
|
|
545
|
+
|
|
546
|
+
// 尝试删除技能文件
|
|
547
|
+
try {
|
|
548
|
+
await fs.unlink(skillFilePath);
|
|
549
|
+
log?.info?.(`[SkillsHandler] Skill file deleted: ${skillFilePath}`);
|
|
550
|
+
deleted = true;
|
|
551
|
+
} catch (err: any) {
|
|
552
|
+
// 文件不存在,继续尝试删除目录
|
|
638
553
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
554
|
+
|
|
555
|
+
// 尝试删除技能目录(ZIP 安装的技能)
|
|
556
|
+
try {
|
|
557
|
+
await fs.rm(skillDirPath, { recursive: true, force: true });
|
|
558
|
+
log?.info?.(`[SkillsHandler] Skill directory deleted: ${skillDirPath}`);
|
|
559
|
+
deleted = true;
|
|
560
|
+
} catch (err: any) {
|
|
561
|
+
// 目录不存在,继续
|
|
646
562
|
}
|
|
647
563
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
564
|
+
|
|
565
|
+
// 如果没有指定智能体,从默认位置删除
|
|
566
|
+
if (agentIdsList.length === 0) {
|
|
567
|
+
// 获取默认技能目录
|
|
568
|
+
const skillsDir = await getSkillsDir(undefined, log);
|
|
569
|
+
|
|
570
|
+
// 检查技能文件和目录
|
|
571
|
+
const skillFilePath = path.join(skillsDir, `${skillName}.json`);
|
|
572
|
+
const skillDirPath = path.join(skillsDir, skillName);
|
|
573
|
+
|
|
574
|
+
// 尝试删除技能文件
|
|
575
|
+
try {
|
|
576
|
+
await fs.unlink(skillFilePath);
|
|
577
|
+
log?.info?.(`[SkillsHandler] Skill file deleted: ${skillFilePath}`);
|
|
578
|
+
deleted = true;
|
|
579
|
+
} catch (err: any) {
|
|
580
|
+
// 文件不存在,继续尝试删除目录
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// 尝试删除技能目录(ZIP 安装的技能)
|
|
584
|
+
try {
|
|
585
|
+
await fs.rm(skillDirPath, { recursive: true, force: true });
|
|
586
|
+
log?.info?.(`[SkillsHandler] Skill directory deleted: ${skillDirPath}`);
|
|
587
|
+
deleted = true;
|
|
588
|
+
} catch (err: any) {
|
|
589
|
+
// 目录不存在,继续
|
|
590
|
+
}
|
|
652
591
|
}
|
|
653
592
|
|
|
654
593
|
if (!deleted) {
|
|
655
|
-
const error: any = new Error(`Skill not found: ${skillName}`)
|
|
656
|
-
error.code =
|
|
657
|
-
throw error
|
|
594
|
+
const error: any = new Error(`Skill not found: ${skillName}`);
|
|
595
|
+
error.code = "SKILL_NOT_FOUND";
|
|
596
|
+
throw error;
|
|
658
597
|
}
|
|
659
598
|
|
|
660
599
|
return {
|
|
661
600
|
userId,
|
|
662
601
|
skillName,
|
|
663
|
-
agentId:
|
|
602
|
+
agentId: "",
|
|
664
603
|
agentIds: agentIdsList.length > 0 ? agentIdsList : undefined,
|
|
665
|
-
status:
|
|
604
|
+
status: "remove",
|
|
666
605
|
deletedAt: new Date().toISOString(),
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
throw err
|
|
606
|
+
};
|
|
607
|
+
} catch (err: any) {
|
|
608
|
+
log?.error?.(`[SkillsHandler] Failed to delete skill: ${err.message}`);
|
|
609
|
+
throw err;
|
|
672
610
|
}
|
|
673
611
|
}
|
|
674
612
|
|
|
@@ -676,67 +614,65 @@ async function handleDeleteSkill(
|
|
|
676
614
|
* 查询 Skills 列表
|
|
677
615
|
*/
|
|
678
616
|
async function handleListSkills(
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
617
|
+
data: any,
|
|
618
|
+
log?: {
|
|
619
|
+
info?: (msg: string) => void;
|
|
620
|
+
error?: (msg: string) => void;
|
|
621
|
+
}
|
|
684
622
|
): Promise<any> {
|
|
685
|
-
const { filter, limit = 100 } = data
|
|
623
|
+
const { filter, limit = 100 } = data;
|
|
686
624
|
|
|
687
|
-
log?.info?.(`[SkillsHandler] Listing skills, filter: ${filter}, limit: ${limit}`)
|
|
625
|
+
log?.info?.(`[SkillsHandler] Listing skills, filter: ${filter}, limit: ${limit}`);
|
|
688
626
|
|
|
689
627
|
try {
|
|
690
628
|
// 调用 openclaw skills list 命令
|
|
691
|
-
const { stdout } = await execAsync(
|
|
629
|
+
const { stdout } = await execAsync("openclaw skills list --json", {
|
|
692
630
|
timeout: 10000,
|
|
693
|
-
})
|
|
631
|
+
});
|
|
694
632
|
|
|
695
|
-
let skills: any[] = []
|
|
633
|
+
let skills: any[] = [];
|
|
696
634
|
try {
|
|
697
|
-
const parsed = JSON.parse(stdout)
|
|
698
|
-
skills = parsed.skills || parsed
|
|
699
|
-
}
|
|
700
|
-
catch {
|
|
635
|
+
const parsed = JSON.parse(stdout);
|
|
636
|
+
skills = parsed.skills || parsed;
|
|
637
|
+
} catch {
|
|
701
638
|
// 解析文本输出
|
|
702
|
-
const lines = stdout.split(
|
|
639
|
+
const lines = stdout.split("\n");
|
|
703
640
|
skills = lines
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
641
|
+
.filter((line) => line.trim() && !line.includes("Skills"))
|
|
642
|
+
.map((line) => {
|
|
643
|
+
const parts = line.split(/\s{2,}/).map((p) => p.trim());
|
|
644
|
+
if (parts.length >= 3) {
|
|
645
|
+
return {
|
|
646
|
+
status: parts[0]?.includes("✓") ? "ready" : "missing",
|
|
647
|
+
name: parts[1],
|
|
648
|
+
description: parts[2],
|
|
649
|
+
source: parts[3] || "openclaw-bundled",
|
|
650
|
+
};
|
|
713
651
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
.filter((s): s is any => s !== null)
|
|
652
|
+
return null;
|
|
653
|
+
})
|
|
654
|
+
.filter((s): s is any => s !== null);
|
|
718
655
|
}
|
|
719
656
|
|
|
720
657
|
// 应用过滤
|
|
721
658
|
if (filter) {
|
|
722
|
-
skills = skills.filter(s => s.status === filter)
|
|
659
|
+
skills = skills.filter((s) => s.status === filter);
|
|
723
660
|
}
|
|
724
661
|
|
|
725
662
|
// 应用 limit
|
|
726
|
-
skills = skills.slice(0, limit)
|
|
663
|
+
skills = skills.slice(0, limit);
|
|
727
664
|
|
|
728
665
|
return {
|
|
729
666
|
total: skills.length,
|
|
730
667
|
skills,
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
log?.error?.(`[SkillsHandler] Failed to list skills: ${err.message}`)
|
|
668
|
+
};
|
|
669
|
+
} catch (err: any) {
|
|
670
|
+
log?.error?.(`[SkillsHandler] Failed to list skills: ${err.message}`);
|
|
735
671
|
// 返回空列表而不是报错
|
|
736
672
|
return {
|
|
737
673
|
total: 0,
|
|
738
674
|
skills: [],
|
|
739
|
-
}
|
|
675
|
+
};
|
|
740
676
|
}
|
|
741
677
|
}
|
|
742
678
|
|
|
@@ -744,62 +680,62 @@ async function handleListSkills(
|
|
|
744
680
|
* 查询单个 Skill
|
|
745
681
|
*/
|
|
746
682
|
async function handleGetSkill(
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
683
|
+
data: any,
|
|
684
|
+
log?: {
|
|
685
|
+
info?: (msg: string) => void;
|
|
686
|
+
error?: (msg: string) => void;
|
|
687
|
+
}
|
|
752
688
|
): Promise<any> {
|
|
753
|
-
const { id } = data
|
|
689
|
+
const { id } = data;
|
|
754
690
|
|
|
755
691
|
if (!id) {
|
|
756
|
-
const error: any = new Error(
|
|
757
|
-
error.code =
|
|
758
|
-
throw error
|
|
692
|
+
const error: any = new Error("Missing required field: id");
|
|
693
|
+
error.code = "MISSING_FIELDS";
|
|
694
|
+
throw error;
|
|
759
695
|
}
|
|
760
696
|
|
|
761
|
-
log?.info?.(`[SkillsHandler] Getting skill: ${id}`)
|
|
697
|
+
log?.info?.(`[SkillsHandler] Getting skill: ${id}`);
|
|
762
698
|
|
|
763
699
|
// TODO: 实现实际的 skill 查询逻辑
|
|
764
700
|
|
|
765
701
|
return {
|
|
766
702
|
id,
|
|
767
703
|
name: `Skill ${id}`,
|
|
768
|
-
description:
|
|
769
|
-
status:
|
|
770
|
-
}
|
|
704
|
+
description: "Skill description",
|
|
705
|
+
status: "ready",
|
|
706
|
+
};
|
|
771
707
|
}
|
|
772
708
|
|
|
773
709
|
/**
|
|
774
710
|
* 执行 Skill
|
|
775
711
|
*/
|
|
776
712
|
async function handleInvokeSkill(
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
713
|
+
data: any,
|
|
714
|
+
log?: {
|
|
715
|
+
info?: (msg: string) => void;
|
|
716
|
+
error?: (msg: string) => void;
|
|
717
|
+
}
|
|
782
718
|
): Promise<any> {
|
|
783
|
-
const { id, input, context } = data
|
|
719
|
+
const { id, input, context } = data;
|
|
784
720
|
|
|
785
721
|
if (!id) {
|
|
786
|
-
const error: any = new Error(
|
|
787
|
-
error.code =
|
|
788
|
-
throw error
|
|
722
|
+
const error: any = new Error("Missing required field: id");
|
|
723
|
+
error.code = "MISSING_FIELDS";
|
|
724
|
+
throw error;
|
|
789
725
|
}
|
|
790
726
|
|
|
791
|
-
log?.info?.(`[SkillsHandler] Invoking skill: ${id}`)
|
|
727
|
+
log?.info?.(`[SkillsHandler] Invoking skill: ${id}`);
|
|
792
728
|
|
|
793
729
|
// TODO: 实现实际的 skill 执行逻辑
|
|
794
730
|
|
|
795
731
|
return {
|
|
796
732
|
id,
|
|
797
|
-
status:
|
|
733
|
+
status: "completed",
|
|
798
734
|
output: {
|
|
799
735
|
result: `Skill ${id} executed successfully`,
|
|
800
736
|
input,
|
|
801
737
|
context,
|
|
802
738
|
},
|
|
803
739
|
executedAt: new Date().toISOString(),
|
|
804
|
-
}
|
|
805
|
-
}
|
|
740
|
+
};
|
|
741
|
+
}
|