@workclaw/openclaw-workclaw 1.0.20 → 1.0.25

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.
Files changed (153) hide show
  1. package/README.md +0 -1
  2. package/api.ts +3 -0
  3. package/index.ts +325 -0
  4. package/package.json +3 -11
  5. package/setup-entry.ts +13 -0
  6. package/src/accounts.ts +360 -0
  7. package/src/api/accounts-api.ts +156 -0
  8. package/src/api/prompts-api.ts +122 -0
  9. package/src/api/session-api.ts +246 -0
  10. package/src/api/skills-api.ts +74 -0
  11. package/src/api/workspace.ts +45 -0
  12. package/src/channel.ts +226 -0
  13. package/{dist/src/config-schema.js → src/config-schema.ts} +60 -56
  14. package/src/connection/workclaw-client.ts +618 -0
  15. package/src/gateway/agent-handlers.ts +551 -0
  16. package/src/gateway/config-writer.ts +378 -0
  17. package/src/gateway/cron-tasks-handler.ts +230 -0
  18. package/src/gateway/message-context.ts +645 -0
  19. package/src/gateway/message-dispatcher.ts +688 -0
  20. package/src/gateway/reconnect.ts +260 -0
  21. package/src/gateway/skills-handler.ts +805 -0
  22. package/src/gateway/skills-list-handler.ts +332 -0
  23. package/src/gateway/tools-list-handler.ts +161 -0
  24. package/src/gateway/workclaw-gateway.ts +304 -0
  25. package/src/media/upload.ts +168 -0
  26. package/src/outbound/index.ts +191 -0
  27. package/src/outbound/workclaw-sender.ts +161 -0
  28. package/src/runtime.ts +520 -0
  29. package/src/secret-contract-api.ts +4 -0
  30. package/src/send.ts +1 -0
  31. package/src/setup-api.ts +3 -0
  32. package/src/setup-core.ts +25 -0
  33. package/src/setup-surface.ts +498 -0
  34. package/src/tools/openclaw-workclaw-cron/api/index.ts +326 -0
  35. package/{dist/src/tools/openclaw-workclaw-cron/index.js → src/tools/openclaw-workclaw-cron/index.ts} +39 -33
  36. package/src/tools/openclaw-workclaw-cron/src/add/params.ts +177 -0
  37. package/src/tools/openclaw-workclaw-cron/src/add/sync.ts +188 -0
  38. package/src/tools/openclaw-workclaw-cron/src/disable/params.ts +100 -0
  39. package/src/tools/openclaw-workclaw-cron/src/disable/sync.ts +127 -0
  40. package/src/tools/openclaw-workclaw-cron/src/enable/params.ts +100 -0
  41. package/src/tools/openclaw-workclaw-cron/src/enable/sync.ts +127 -0
  42. package/src/tools/openclaw-workclaw-cron/src/notify/sync.ts +148 -0
  43. package/src/tools/openclaw-workclaw-cron/src/remove/params.ts +109 -0
  44. package/src/tools/openclaw-workclaw-cron/src/remove/sync.ts +127 -0
  45. package/src/tools/openclaw-workclaw-cron/src/update/params.ts +195 -0
  46. package/src/tools/openclaw-workclaw-cron/src/update/sync.ts +161 -0
  47. package/src/tools/openclaw-workclaw-cron/types/index.ts +55 -0
  48. package/src/tools/openclaw-workclaw-cron/utils/index.ts +141 -0
  49. package/{dist/src/tools/openclaw-workclaw-system/index.js → src/tools/openclaw-workclaw-system/index.ts} +17 -15
  50. package/src/tools/openclaw-workclaw-system/src/get/index.ts +77 -0
  51. package/src/tools/openclaw-workclaw-system/src/token/index.ts +93 -0
  52. package/src/types.ts +50 -0
  53. package/src/utils/content.ts +40 -0
  54. package/tsconfig.json +34 -0
  55. package/dist/api.d.ts +0 -3
  56. package/dist/api.js +0 -3
  57. package/dist/index.d.ts +0 -11
  58. package/dist/index.js +0 -264
  59. package/dist/setup-entry.d.ts +0 -2
  60. package/dist/setup-entry.js +0 -12
  61. package/dist/src/accounts.d.ts +0 -66
  62. package/dist/src/accounts.js +0 -279
  63. package/dist/src/api/accounts-api.d.ts +0 -2
  64. package/dist/src/api/accounts-api.js +0 -130
  65. package/dist/src/api/prompts-api.d.ts +0 -2
  66. package/dist/src/api/prompts-api.js +0 -103
  67. package/dist/src/api/session-api.d.ts +0 -2
  68. package/dist/src/api/session-api.js +0 -207
  69. package/dist/src/api/skills-api.d.ts +0 -2
  70. package/dist/src/api/skills-api.js +0 -64
  71. package/dist/src/api/workspace.d.ts +0 -3
  72. package/dist/src/api/workspace.js +0 -30
  73. package/dist/src/channel.d.ts +0 -11
  74. package/dist/src/channel.js +0 -199
  75. package/dist/src/config-schema.d.ts +0 -93
  76. package/dist/src/connection/workclaw-client.d.ts +0 -147
  77. package/dist/src/connection/workclaw-client.js +0 -351
  78. package/dist/src/gateway/agent-handlers.d.ts +0 -12
  79. package/dist/src/gateway/agent-handlers.js +0 -433
  80. package/dist/src/gateway/config-writer.d.ts +0 -71
  81. package/dist/src/gateway/config-writer.js +0 -302
  82. package/dist/src/gateway/cron-tasks-handler.d.ts +0 -19
  83. package/dist/src/gateway/cron-tasks-handler.js +0 -188
  84. package/dist/src/gateway/message-context.d.ts +0 -80
  85. package/dist/src/gateway/message-context.js +0 -509
  86. package/dist/src/gateway/message-dispatcher.d.ts +0 -18
  87. package/dist/src/gateway/message-dispatcher.js +0 -572
  88. package/dist/src/gateway/reconnect.d.ts +0 -27
  89. package/dist/src/gateway/reconnect.js +0 -210
  90. package/dist/src/gateway/skills-handler.d.ts +0 -29
  91. package/dist/src/gateway/skills-handler.js +0 -615
  92. package/dist/src/gateway/skills-list-handler.d.ts +0 -27
  93. package/dist/src/gateway/skills-list-handler.js +0 -233
  94. package/dist/src/gateway/tools-list-handler.d.ts +0 -30
  95. package/dist/src/gateway/tools-list-handler.js +0 -101
  96. package/dist/src/gateway/workclaw-gateway.d.ts +0 -14
  97. package/dist/src/gateway/workclaw-gateway.js +0 -237
  98. package/dist/src/media/upload.d.ts +0 -13
  99. package/dist/src/media/upload.js +0 -125
  100. package/dist/src/outbound/index.d.ts +0 -36
  101. package/dist/src/outbound/index.js +0 -123
  102. package/dist/src/outbound/workclaw-sender.d.ts +0 -36
  103. package/dist/src/outbound/workclaw-sender.js +0 -95
  104. package/dist/src/runtime.d.ts +0 -116
  105. package/dist/src/runtime.js +0 -374
  106. package/dist/src/secret-contract-api.d.ts +0 -4
  107. package/dist/src/secret-contract-api.js +0 -4
  108. package/dist/src/send.d.ts +0 -1
  109. package/dist/src/send.js +0 -1
  110. package/dist/src/setup-api.d.ts +0 -3
  111. package/dist/src/setup-api.js +0 -3
  112. package/dist/src/setup-core.d.ts +0 -3
  113. package/dist/src/setup-core.js +0 -13
  114. package/dist/src/setup-surface.d.ts +0 -7
  115. package/dist/src/setup-surface.js +0 -363
  116. package/dist/src/tools/openclaw-workclaw-cron/api/index.d.ts +0 -93
  117. package/dist/src/tools/openclaw-workclaw-cron/api/index.js +0 -209
  118. package/dist/src/tools/openclaw-workclaw-cron/index.d.ts +0 -10
  119. package/dist/src/tools/openclaw-workclaw-cron/src/add/params.d.ts +0 -16
  120. package/dist/src/tools/openclaw-workclaw-cron/src/add/params.js +0 -152
  121. package/dist/src/tools/openclaw-workclaw-cron/src/add/sync.d.ts +0 -18
  122. package/dist/src/tools/openclaw-workclaw-cron/src/add/sync.js +0 -162
  123. package/dist/src/tools/openclaw-workclaw-cron/src/disable/params.d.ts +0 -14
  124. package/dist/src/tools/openclaw-workclaw-cron/src/disable/params.js +0 -80
  125. package/dist/src/tools/openclaw-workclaw-cron/src/disable/sync.d.ts +0 -14
  126. package/dist/src/tools/openclaw-workclaw-cron/src/disable/sync.js +0 -107
  127. package/dist/src/tools/openclaw-workclaw-cron/src/enable/params.d.ts +0 -14
  128. package/dist/src/tools/openclaw-workclaw-cron/src/enable/params.js +0 -80
  129. package/dist/src/tools/openclaw-workclaw-cron/src/enable/sync.d.ts +0 -14
  130. package/dist/src/tools/openclaw-workclaw-cron/src/enable/sync.js +0 -107
  131. package/dist/src/tools/openclaw-workclaw-cron/src/notify/sync.d.ts +0 -17
  132. package/dist/src/tools/openclaw-workclaw-cron/src/notify/sync.js +0 -127
  133. package/dist/src/tools/openclaw-workclaw-cron/src/remove/params.d.ts +0 -14
  134. package/dist/src/tools/openclaw-workclaw-cron/src/remove/params.js +0 -87
  135. package/dist/src/tools/openclaw-workclaw-cron/src/remove/sync.d.ts +0 -14
  136. package/dist/src/tools/openclaw-workclaw-cron/src/remove/sync.js +0 -107
  137. package/dist/src/tools/openclaw-workclaw-cron/src/update/params.d.ts +0 -17
  138. package/dist/src/tools/openclaw-workclaw-cron/src/update/params.js +0 -164
  139. package/dist/src/tools/openclaw-workclaw-cron/src/update/sync.d.ts +0 -18
  140. package/dist/src/tools/openclaw-workclaw-cron/src/update/sync.js +0 -135
  141. package/dist/src/tools/openclaw-workclaw-cron/types/index.d.ts +0 -52
  142. package/dist/src/tools/openclaw-workclaw-cron/types/index.js +0 -4
  143. package/dist/src/tools/openclaw-workclaw-cron/utils/index.d.ts +0 -40
  144. package/dist/src/tools/openclaw-workclaw-cron/utils/index.js +0 -122
  145. package/dist/src/tools/openclaw-workclaw-system/index.d.ts +0 -10
  146. package/dist/src/tools/openclaw-workclaw-system/src/get/index.d.ts +0 -8
  147. package/dist/src/tools/openclaw-workclaw-system/src/get/index.js +0 -64
  148. package/dist/src/tools/openclaw-workclaw-system/src/token/index.d.ts +0 -8
  149. package/dist/src/tools/openclaw-workclaw-system/src/token/index.js +0 -78
  150. package/dist/src/types.d.ts +0 -39
  151. package/dist/src/types.js +0 -1
  152. package/dist/src/utils/content.d.ts +0 -15
  153. package/dist/src/utils/content.js +0 -38
@@ -0,0 +1,805 @@
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, 'workspace', 'skills')
279
+
280
+ // 如果有 agentId,添加到对应目录
281
+ if (agentId && agentId.trim()) {
282
+ skillsDir = path.join(openclawDir, `workspace-${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 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
+ async function handleCreateSkill(
430
+ data: any,
431
+ log?: {
432
+ info?: (msg: string) => void
433
+ error?: (msg: string) => void
434
+ },
435
+ ): Promise<any> {
436
+ const { userId, skillName, unloadUrl, mainId } = data
437
+ const agentIds = Array.isArray(data?.agentIds) ? data.agentIds : (data?.agentIds ? [data.agentIds] : [])
438
+
439
+ if (!userId || !skillName) {
440
+ const error: any = new Error('Missing required fields: userId, skillName')
441
+ error.code = 'MISSING_FIELDS'
442
+ throw error
443
+ }
444
+
445
+ log?.info?.(`[SkillsHandler] Creating skill: ${skillName}`)
446
+ log?.info?.(`[SkillsHandler] mainId: ${mainId}`)
447
+ if (agentIds.length > 0) {
448
+ log?.info?.(`[SkillsHandler] Target agents: ${agentIds.join(', ')}`)
449
+ }
450
+
451
+ // 如果提供了 URL,从 URL 下载 skill 定义
452
+ if (unloadUrl) {
453
+ log?.info?.(`[SkillsHandler] Downloading skill from URL: ${unloadUrl}`)
454
+
455
+ try {
456
+ // 下载 skill 定义
457
+ const skillContent = await downloadFromUrl(unloadUrl, log)
458
+ log?.info?.(`[SkillsHandler] Downloaded skill content, length: ${skillContent.length} bytes`)
459
+
460
+ // 确定安装位置
461
+ const installLocations: (string | undefined)[] = []
462
+
463
+ // 检查 agentIds 是否只包含 mainId
464
+ const hasOnlyMainId = agentIds.length === 1 && agentIds[0] === mainId
465
+ // 检查 agentIds 是否包含 mainId 和其他 id
466
+ const hasMainIdAndOthers = agentIds.includes(mainId) && agentIds.length > 1
467
+
468
+ if (hasOnlyMainId) {
469
+ // 如果只有 mainId,安装到默认位置
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 => 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)
489
+ }
490
+
491
+ // 安装技能到所有位置
492
+ const installResults = await installSkillToLocations(skillName, skillContent, installLocations, log)
493
+
494
+ log?.info?.(`[SkillsHandler] Skill installed to ${installResults.length} locations`)
495
+
496
+ return {
497
+ userId,
498
+ skillName,
499
+ agentId: '',
500
+ agentIds: agentIds.length > 0 ? agentIds : undefined,
501
+ status: 'add',
502
+ installedAt: new Date().toISOString(),
503
+ }
504
+ }
505
+ catch (err: any) {
506
+ log?.error?.(`[SkillsHandler] Failed to create skill from URL: ${err.message}`)
507
+ throw err
508
+ }
509
+ }
510
+
511
+ return {
512
+ userId,
513
+ skillName,
514
+ agentId: '',
515
+ agentIds: agentIds.length > 0 ? agentIds : undefined,
516
+ status: 'add',
517
+ createdAt: new Date().toISOString(),
518
+ }
519
+ }
520
+
521
+ /**
522
+ * 更新 Skill
523
+ */
524
+ async function handleUpdateSkill(
525
+ data: any,
526
+ log?: {
527
+ info?: (msg: string) => void
528
+ error?: (msg: string) => void
529
+ },
530
+ ): Promise<any> {
531
+ const { id, ...updates } = data
532
+
533
+ if (!id) {
534
+ const error: any = new Error('Missing required field: id')
535
+ error.code = 'MISSING_FIELDS'
536
+ throw error
537
+ }
538
+
539
+ log?.info?.(`[SkillsHandler] Updating skill: ${id}`)
540
+
541
+ return {
542
+ id,
543
+ ...updates,
544
+ status: 'updated',
545
+ updatedAt: new Date().toISOString(),
546
+ }
547
+ }
548
+
549
+ /**
550
+ * 删除 Skill
551
+ */
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 (err: any) {
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 (err: any) {
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
+ async function handleDeleteSkill(
590
+ data: any,
591
+ log?: {
592
+ info?: (msg: string) => void
593
+ error?: (msg: string) => void
594
+ },
595
+ ): Promise<any> {
596
+ const { userId, skillName, agentIds, mainId } = data
597
+ const agentIdsList = Array.isArray(agentIds) ? agentIds : (agentIds ? [agentIds] : [])
598
+
599
+ if (!skillName) {
600
+ const error: any = new Error('Missing required field: skillName')
601
+ error.code = 'MISSING_FIELDS'
602
+ throw error
603
+ }
604
+
605
+ log?.info?.(`[SkillsHandler] Deleting skill: ${skillName}`)
606
+ log?.info?.(`[SkillsHandler] mainId: ${mainId}`)
607
+ if (agentIdsList.length > 0) {
608
+ log?.info?.(`[SkillsHandler] Target agents: ${agentIdsList.join(', ')}`)
609
+ }
610
+
611
+ try {
612
+ let deleted = false
613
+
614
+ // 检查 agentIds 是否只包含 mainId
615
+ const hasOnlyMainId = agentIdsList.length === 1 && agentIdsList[0] === mainId
616
+ // 检查 agentIds 是否包含 mainId 和其他 id
617
+ const hasMainIdAndOthers = agentIdsList.includes(mainId) && agentIdsList.length > 1
618
+
619
+ if (hasOnlyMainId) {
620
+ // 如果只有 mainId,从默认位置删除
621
+ log?.info?.(`[SkillsHandler] Only mainId found, deleting from default location`)
622
+ deleted = await deleteSkillAtLocation(skillName, undefined, log)
623
+ }
624
+ else if (hasMainIdAndOthers) {
625
+ // 如果有 mainId 和其他 id,从默认位置和其他 id 位置删除
626
+ log?.info?.(`[SkillsHandler] mainId and other agents found, deleting from default location and other agents`)
627
+
628
+ // 从默认位置删除
629
+ const defaultDeleted = await deleteSkillAtLocation(skillName, undefined, log)
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
+ }
638
+ }
639
+ }
640
+ else if (agentIdsList.length > 0) {
641
+ // 如果没有 mainId,从所有指定的 agentId 位置删除
642
+ log?.info?.(`[SkillsHandler] No mainId found, deleting from all specified agents`)
643
+ for (const agentId of agentIdsList) {
644
+ const agentDeleted = await deleteSkillAtLocation(skillName, agentId, log)
645
+ deleted = deleted || agentDeleted
646
+ }
647
+ }
648
+ else {
649
+ // 如果没有指定智能体,从默认位置删除
650
+ log?.info?.(`[SkillsHandler] No agents specified, deleting from default location`)
651
+ deleted = await deleteSkillAtLocation(skillName, undefined, log)
652
+ }
653
+
654
+ if (!deleted) {
655
+ const error: any = new Error(`Skill not found: ${skillName}`)
656
+ error.code = 'SKILL_NOT_FOUND'
657
+ throw error
658
+ }
659
+
660
+ return {
661
+ userId,
662
+ skillName,
663
+ agentId: '',
664
+ agentIds: agentIdsList.length > 0 ? agentIdsList : undefined,
665
+ status: 'remove',
666
+ deletedAt: new Date().toISOString(),
667
+ }
668
+ }
669
+ catch (err: any) {
670
+ log?.error?.(`[SkillsHandler] Failed to delete skill: ${err.message}`)
671
+ throw err
672
+ }
673
+ }
674
+
675
+ /**
676
+ * 查询 Skills 列表
677
+ */
678
+ async function handleListSkills(
679
+ data: any,
680
+ log?: {
681
+ info?: (msg: string) => void
682
+ error?: (msg: string) => void
683
+ },
684
+ ): Promise<any> {
685
+ const { filter, limit = 100 } = data
686
+
687
+ log?.info?.(`[SkillsHandler] Listing skills, filter: ${filter}, limit: ${limit}`)
688
+
689
+ try {
690
+ // 调用 openclaw skills list 命令
691
+ const { stdout } = await execAsync('openclaw skills list --json', {
692
+ timeout: 10000,
693
+ })
694
+
695
+ let skills: any[] = []
696
+ try {
697
+ const parsed = JSON.parse(stdout)
698
+ skills = parsed.skills || parsed
699
+ }
700
+ catch {
701
+ // 解析文本输出
702
+ const lines = stdout.split('\n')
703
+ skills = lines
704
+ .filter(line => line.trim() && !line.includes('Skills'))
705
+ .map((line) => {
706
+ const parts = line.split(/\s{2,}/).map(p => p.trim())
707
+ if (parts.length >= 3) {
708
+ return {
709
+ status: parts[0]?.includes('✓') ? 'ready' : 'missing',
710
+ name: parts[1],
711
+ description: parts[2],
712
+ source: parts[3] || 'openclaw-bundled',
713
+ }
714
+ }
715
+ return null
716
+ })
717
+ .filter((s): s is any => s !== null)
718
+ }
719
+
720
+ // 应用过滤
721
+ if (filter) {
722
+ skills = skills.filter(s => s.status === filter)
723
+ }
724
+
725
+ // 应用 limit
726
+ skills = skills.slice(0, limit)
727
+
728
+ return {
729
+ total: skills.length,
730
+ skills,
731
+ }
732
+ }
733
+ catch (err: any) {
734
+ log?.error?.(`[SkillsHandler] Failed to list skills: ${err.message}`)
735
+ // 返回空列表而不是报错
736
+ return {
737
+ total: 0,
738
+ skills: [],
739
+ }
740
+ }
741
+ }
742
+
743
+ /**
744
+ * 查询单个 Skill
745
+ */
746
+ async function handleGetSkill(
747
+ data: any,
748
+ log?: {
749
+ info?: (msg: string) => void
750
+ error?: (msg: string) => void
751
+ },
752
+ ): Promise<any> {
753
+ const { id } = data
754
+
755
+ if (!id) {
756
+ const error: any = new Error('Missing required field: id')
757
+ error.code = 'MISSING_FIELDS'
758
+ throw error
759
+ }
760
+
761
+ log?.info?.(`[SkillsHandler] Getting skill: ${id}`)
762
+
763
+ // TODO: 实现实际的 skill 查询逻辑
764
+
765
+ return {
766
+ id,
767
+ name: `Skill ${id}`,
768
+ description: 'Skill description',
769
+ status: 'ready',
770
+ }
771
+ }
772
+
773
+ /**
774
+ * 执行 Skill
775
+ */
776
+ async function handleInvokeSkill(
777
+ data: any,
778
+ log?: {
779
+ info?: (msg: string) => void
780
+ error?: (msg: string) => void
781
+ },
782
+ ): Promise<any> {
783
+ const { id, input, context } = data
784
+
785
+ if (!id) {
786
+ const error: any = new Error('Missing required field: id')
787
+ error.code = 'MISSING_FIELDS'
788
+ throw error
789
+ }
790
+
791
+ log?.info?.(`[SkillsHandler] Invoking skill: ${id}`)
792
+
793
+ // TODO: 实现实际的 skill 执行逻辑
794
+
795
+ return {
796
+ id,
797
+ status: 'completed',
798
+ output: {
799
+ result: `Skill ${id} executed successfully`,
800
+ input,
801
+ context,
802
+ },
803
+ executedAt: new Date().toISOString(),
804
+ }
805
+ }