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