@workclaw/openclaw-workclaw 1.0.13 → 1.0.15
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/package.json +3 -4
- package/src/gateway/message-context.ts +422 -422
- package/src/gateway/message-dispatcher.ts +2 -2
- package/src/gateway/skills-handler.ts +126 -80
- package/src/gateway/skills-list-handler.ts +332 -332
- package/src/gateway/workclaw-gateway.ts +5 -1
- package/src/tools/openclaw-workclaw-cron/api/index.ts +326 -326
- package/src/tools/openclaw-workclaw-system/index.ts +17 -17
- package/src/tools/openclaw-workclaw-system/src/get/index.ts +77 -77
- package/src/tools/openclaw-workclaw-system/src/token/index.ts +93 -93
|
@@ -1,332 +1,332 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Skills list handler - 处理 SKILLS_LIST 事件
|
|
3
|
-
* 从 OpenClaw 获取 skills 列表并通过回调返回
|
|
4
|
-
*/
|
|
5
|
-
import { readFileSync } from 'node:fs'
|
|
6
|
-
|
|
7
|
-
export interface SkillsListEvent {
|
|
8
|
-
agentId: string | number
|
|
9
|
-
userId: string | number
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface SkillItem {
|
|
13
|
-
typeStr: string
|
|
14
|
-
name: string
|
|
15
|
-
description: string
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface SkillsListCallbackPayload {
|
|
19
|
-
userId: number
|
|
20
|
-
agentId: number
|
|
21
|
-
dataList: SkillItem[]
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* 处理 SKILLS_LIST 事件
|
|
26
|
-
* @param event - 事件数据
|
|
27
|
-
* @param baseUrl - 回调基础 URL
|
|
28
|
-
* @param token - 鉴权 token
|
|
29
|
-
* @param log 日志对象(可选)
|
|
30
|
-
* @param {Function} [log.info] - 信息日志函数
|
|
31
|
-
* @param {Function} [log.error] - 错误日志函数
|
|
32
|
-
*/
|
|
33
|
-
export async function handleSkillsListEvent(
|
|
34
|
-
event: SkillsListEvent,
|
|
35
|
-
baseUrl: string,
|
|
36
|
-
token: string,
|
|
37
|
-
log?: {
|
|
38
|
-
info?: (msg: string) => void
|
|
39
|
-
error?: (msg: string) => void
|
|
40
|
-
},
|
|
41
|
-
): Promise<void> {
|
|
42
|
-
const agentId = Number(event.agentId)
|
|
43
|
-
const userId = Number(event.userId)
|
|
44
|
-
|
|
45
|
-
log?.info?.(`SkillsList: Handling SKILLS_LIST event for agentId: ${agentId}, userId: ${userId}`)
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
// 从 OpenClaw 获取 skills 列表
|
|
49
|
-
const skills = await fetchSkillsFromOpenClaw(agentId, userId, log)
|
|
50
|
-
|
|
51
|
-
// 构建回调 payload
|
|
52
|
-
const callbackPayload: SkillsListCallbackPayload = {
|
|
53
|
-
userId,
|
|
54
|
-
agentId,
|
|
55
|
-
dataList: skills,
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// 发送回调
|
|
59
|
-
const callbackUrl = `${baseUrl.replace(/\/$/, '')}/open-apis/v1/claw/push/skills`
|
|
60
|
-
await sendSkillsListCallback(callbackUrl, callbackPayload, token, log)
|
|
61
|
-
|
|
62
|
-
log?.info?.(`SkillsList: Successfully processed SKILLS_LIST event, sent ${skills.length} skills`)
|
|
63
|
-
}
|
|
64
|
-
catch (error) {
|
|
65
|
-
log?.error?.(`SkillsList: Failed to handle SKILLS_LIST event: ${String(error)}`)
|
|
66
|
-
throw error
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* 从 OpenClaw 获取 skills 列表
|
|
72
|
-
* 包括全局技能和智能体工作区的专有技能
|
|
73
|
-
*/
|
|
74
|
-
async function fetchSkillsFromOpenClaw(
|
|
75
|
-
agentId: number,
|
|
76
|
-
_userId: number,
|
|
77
|
-
log?: {
|
|
78
|
-
info?: (msg: string) => void
|
|
79
|
-
error?: (msg: string) => void
|
|
80
|
-
},
|
|
81
|
-
): Promise<SkillItem[]> {
|
|
82
|
-
log?.info?.(`SkillsList: Fetching skills from OpenClaw for agentId: ${agentId}`)
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
// 使用 OpenClaw CLI 获取 skills 列表(全局技能)
|
|
86
|
-
const { execSync } = await import('node:child_process')
|
|
87
|
-
|
|
88
|
-
const result = execSync('openclaw skills list', {
|
|
89
|
-
encoding: 'utf-8',
|
|
90
|
-
timeout: 30000, // 30秒超时
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
// 解析输出
|
|
94
|
-
const globalSkills = parseSkillsListOutput(result)
|
|
95
|
-
|
|
96
|
-
// 检查智能体工作区中的专有技能
|
|
97
|
-
const agentWorkspaceSkills = await fetchAgentWorkspaceSkills(agentId, log)
|
|
98
|
-
|
|
99
|
-
// 合并技能列表,去重
|
|
100
|
-
const allSkills = mergeSkills(globalSkills, agentWorkspaceSkills)
|
|
101
|
-
|
|
102
|
-
log?.info?.(`SkillsList: Found ${allSkills.length} skills (${globalSkills.length} global, ${agentWorkspaceSkills.length} agent-specific)`)
|
|
103
|
-
return allSkills
|
|
104
|
-
}
|
|
105
|
-
catch (error) {
|
|
106
|
-
log?.error?.(`SkillsList: Error fetching skills: ${String(error)}`)
|
|
107
|
-
// 如果获取失败,返回空列表
|
|
108
|
-
return []
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* 获取智能体工作区中的专有技能
|
|
114
|
-
*/
|
|
115
|
-
async function fetchAgentWorkspaceSkills(
|
|
116
|
-
agentId: number,
|
|
117
|
-
log?: {
|
|
118
|
-
info?: (msg: string) => void
|
|
119
|
-
error?: (msg: string) => void
|
|
120
|
-
},
|
|
121
|
-
): Promise<SkillItem[]> {
|
|
122
|
-
const skills: SkillItem[] = []
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
// 智能体工作区路径
|
|
126
|
-
const { homedir } = await import('node:os')
|
|
127
|
-
const { join } = await import('node:path')
|
|
128
|
-
const agentWorkspace = join(homedir(), '.openclaw', 'agents', `openclaw-workclaw-${agentId}`)
|
|
129
|
-
const skillsDir = join(agentWorkspace, 'skills')
|
|
130
|
-
|
|
131
|
-
// 检查技能目录是否存在
|
|
132
|
-
const { existsSync, readdirSync, statSync } = await import('node:fs')
|
|
133
|
-
if (!existsSync(skillsDir)) {
|
|
134
|
-
log?.info?.(`SkillsList: No skills directory found in agent workspace: ${skillsDir}`)
|
|
135
|
-
return skills
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// 读取技能目录中的子目录(每个子目录是一个技能)
|
|
139
|
-
const items = readdirSync(skillsDir)
|
|
140
|
-
for (const item of items) {
|
|
141
|
-
const itemPath = join(skillsDir, item)
|
|
142
|
-
const stat = statSync(itemPath)
|
|
143
|
-
|
|
144
|
-
// 如果是目录,检查里面的技能文件
|
|
145
|
-
if (stat.isDirectory()) {
|
|
146
|
-
const skillFiles = readdirSync(itemPath)
|
|
147
|
-
for (const skillFile of skillFiles) {
|
|
148
|
-
if (skillFile.endsWith('.md') || skillFile.endsWith('.prose')) {
|
|
149
|
-
const skillFilePath = join(itemPath, skillFile)
|
|
150
|
-
const skill = parseSkillFile(skillFilePath, item, log)
|
|
151
|
-
if (skill) {
|
|
152
|
-
skills.push(skill)
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
log?.info?.(`SkillsList: Found ${skills.length} agent-specific skills in ${skillsDir}`)
|
|
160
|
-
}
|
|
161
|
-
catch (error) {
|
|
162
|
-
log?.error?.(`SkillsList: Error reading agent workspace skills: ${String(error)}`)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return skills
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* 解析技能文件
|
|
170
|
-
*/
|
|
171
|
-
function parseSkillFile(
|
|
172
|
-
filePath: string,
|
|
173
|
-
skillName: string,
|
|
174
|
-
log?: {
|
|
175
|
-
info?: (msg: string) => void
|
|
176
|
-
error?: (msg: string) => void
|
|
177
|
-
},
|
|
178
|
-
): SkillItem | null {
|
|
179
|
-
try {
|
|
180
|
-
const content = readFileSync(filePath, 'utf-8')
|
|
181
|
-
|
|
182
|
-
// 提取描述(从文件内容中)
|
|
183
|
-
let description = 'Agent-specific skill'
|
|
184
|
-
const lines = content.split('\n')
|
|
185
|
-
for (const line of lines) {
|
|
186
|
-
if (line.startsWith('# ')) {
|
|
187
|
-
description = line.substring(2).trim()
|
|
188
|
-
break
|
|
189
|
-
}
|
|
190
|
-
else if (line.startsWith('summary:') || line.startsWith('Summary:')) {
|
|
191
|
-
description = line.substring(line.indexOf(':') + 1).trim()
|
|
192
|
-
break
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return {
|
|
197
|
-
typeStr: 'skill',
|
|
198
|
-
name: skillName,
|
|
199
|
-
description,
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
catch (error) {
|
|
203
|
-
log?.error?.(`SkillsList: Error parsing skill file ${filePath}: ${String(error)}`)
|
|
204
|
-
return null
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* 合并技能列表,去重
|
|
210
|
-
*/
|
|
211
|
-
function mergeSkills(globalSkills: SkillItem[], agentSkills: SkillItem[]): SkillItem[] {
|
|
212
|
-
const skillMap = new Map<string, SkillItem>()
|
|
213
|
-
|
|
214
|
-
// 先添加全局技能
|
|
215
|
-
for (const skill of globalSkills) {
|
|
216
|
-
skillMap.set(skill.name, skill)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// 再添加智能体专有技能(会覆盖同名的全局技能)
|
|
220
|
-
for (const skill of agentSkills) {
|
|
221
|
-
skillMap.set(skill.name, skill)
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return Array.from(skillMap.values())
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* 解析 openclaw skills list 的输出
|
|
229
|
-
*/
|
|
230
|
-
function parseSkillsListOutput(output: string): SkillItem[] {
|
|
231
|
-
const skills: SkillItem[] = []
|
|
232
|
-
const lines = output.split('\n')
|
|
233
|
-
|
|
234
|
-
// 查找表格内容(从表头后开始)
|
|
235
|
-
let inTable = false
|
|
236
|
-
let headerLine = -1
|
|
237
|
-
|
|
238
|
-
for (let i = 0; i < lines.length; i++) {
|
|
239
|
-
const line = lines[i]
|
|
240
|
-
|
|
241
|
-
// 找到表头行(包含 Status | Skill | Description | Source)
|
|
242
|
-
if (line.includes('Status') && line.includes('Skill') && line.includes('Description')) {
|
|
243
|
-
headerLine = i
|
|
244
|
-
inTable = true
|
|
245
|
-
continue
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// 跳过分隔线
|
|
249
|
-
if (inTable && (line.startsWith('├') || line.startsWith('┌') || line.startsWith('└'))) {
|
|
250
|
-
continue
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// 解析表格行
|
|
254
|
-
if (inTable && line.startsWith('│') && i > headerLine + 1) {
|
|
255
|
-
const parsed = parseSkillsTableRow(line)
|
|
256
|
-
if (parsed) {
|
|
257
|
-
skills.push(parsed)
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// 表格结束
|
|
262
|
-
if (inTable && line.startsWith('└')) {
|
|
263
|
-
break
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return skills
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* 解析单行表格数据
|
|
272
|
-
*/
|
|
273
|
-
function parseSkillsTableRow(line: string): SkillItem | null {
|
|
274
|
-
// 移除开头的 │ 和结尾的 │
|
|
275
|
-
const content = line.replace(/^│\s*/, '').replace(/\s*│$/, '')
|
|
276
|
-
|
|
277
|
-
// 按 │ 分割列
|
|
278
|
-
const columns = content.split('│').map(col => col.trim())
|
|
279
|
-
|
|
280
|
-
if (columns.length < 4) {
|
|
281
|
-
return null
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const status = columns[0]
|
|
285
|
-
const skillName = columns[1]
|
|
286
|
-
const description = columns[2]
|
|
287
|
-
|
|
288
|
-
// 只返回 ready 状态的 skills
|
|
289
|
-
if (!status.includes('✓') && !status.includes('ready')) {
|
|
290
|
-
return null
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// 提取 skill 名称(移除 emoji)
|
|
294
|
-
const cleanName = skillName.replace(/[\u{1F300}-\u{1F9FF}]/gu, '').trim()
|
|
295
|
-
|
|
296
|
-
return {
|
|
297
|
-
typeStr: 'skill',
|
|
298
|
-
name: cleanName,
|
|
299
|
-
description,
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* 发送 skills 列表回调
|
|
305
|
-
*/
|
|
306
|
-
async function sendSkillsListCallback(
|
|
307
|
-
callbackUrl: string,
|
|
308
|
-
payload: SkillsListCallbackPayload,
|
|
309
|
-
token: string,
|
|
310
|
-
log?: {
|
|
311
|
-
info?: (msg: string) => void
|
|
312
|
-
error?: (msg: string) => void
|
|
313
|
-
},
|
|
314
|
-
): Promise<void> {
|
|
315
|
-
log?.info?.(`SkillsList: Sending callback to ${callbackUrl}`)
|
|
316
|
-
|
|
317
|
-
const response = await fetch(callbackUrl, {
|
|
318
|
-
method: 'POST',
|
|
319
|
-
headers: {
|
|
320
|
-
'Content-Type': 'application/json',
|
|
321
|
-
'Authorization': `Bearer ${token}`,
|
|
322
|
-
},
|
|
323
|
-
body: JSON.stringify(payload),
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
if (!response.ok) {
|
|
327
|
-
const errorText = await response.text().catch(() => 'Unknown error')
|
|
328
|
-
throw new Error(`Callback failed: ${response.status} ${errorText}`)
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
log?.info?.(`SkillsList: Callback sent successfully`)
|
|
332
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Skills list handler - 处理 SKILLS_LIST 事件
|
|
3
|
+
* 从 OpenClaw 获取 skills 列表并通过回调返回
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from 'node:fs'
|
|
6
|
+
|
|
7
|
+
export interface SkillsListEvent {
|
|
8
|
+
agentId: string | number
|
|
9
|
+
userId: string | number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SkillItem {
|
|
13
|
+
typeStr: string
|
|
14
|
+
name: string
|
|
15
|
+
description: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SkillsListCallbackPayload {
|
|
19
|
+
userId: number
|
|
20
|
+
agentId: number
|
|
21
|
+
dataList: SkillItem[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 处理 SKILLS_LIST 事件
|
|
26
|
+
* @param event - 事件数据
|
|
27
|
+
* @param baseUrl - 回调基础 URL
|
|
28
|
+
* @param token - 鉴权 token
|
|
29
|
+
* @param log 日志对象(可选)
|
|
30
|
+
* @param {Function} [log.info] - 信息日志函数
|
|
31
|
+
* @param {Function} [log.error] - 错误日志函数
|
|
32
|
+
*/
|
|
33
|
+
export async function handleSkillsListEvent(
|
|
34
|
+
event: SkillsListEvent,
|
|
35
|
+
baseUrl: string,
|
|
36
|
+
token: string,
|
|
37
|
+
log?: {
|
|
38
|
+
info?: (msg: string) => void
|
|
39
|
+
error?: (msg: string) => void
|
|
40
|
+
},
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
const agentId = Number(event.agentId)
|
|
43
|
+
const userId = Number(event.userId)
|
|
44
|
+
|
|
45
|
+
log?.info?.(`SkillsList: Handling SKILLS_LIST event for agentId: ${agentId}, userId: ${userId}`)
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// 从 OpenClaw 获取 skills 列表
|
|
49
|
+
const skills = await fetchSkillsFromOpenClaw(agentId, userId, log)
|
|
50
|
+
|
|
51
|
+
// 构建回调 payload
|
|
52
|
+
const callbackPayload: SkillsListCallbackPayload = {
|
|
53
|
+
userId,
|
|
54
|
+
agentId,
|
|
55
|
+
dataList: skills,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 发送回调
|
|
59
|
+
const callbackUrl = `${baseUrl.replace(/\/$/, '')}/open-apis/v1/claw/push/skills`
|
|
60
|
+
await sendSkillsListCallback(callbackUrl, callbackPayload, token, log)
|
|
61
|
+
|
|
62
|
+
log?.info?.(`SkillsList: Successfully processed SKILLS_LIST event, sent ${skills.length} skills`)
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
log?.error?.(`SkillsList: Failed to handle SKILLS_LIST event: ${String(error)}`)
|
|
66
|
+
throw error
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 从 OpenClaw 获取 skills 列表
|
|
72
|
+
* 包括全局技能和智能体工作区的专有技能
|
|
73
|
+
*/
|
|
74
|
+
async function fetchSkillsFromOpenClaw(
|
|
75
|
+
agentId: number,
|
|
76
|
+
_userId: number,
|
|
77
|
+
log?: {
|
|
78
|
+
info?: (msg: string) => void
|
|
79
|
+
error?: (msg: string) => void
|
|
80
|
+
},
|
|
81
|
+
): Promise<SkillItem[]> {
|
|
82
|
+
log?.info?.(`SkillsList: Fetching skills from OpenClaw for agentId: ${agentId}`)
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
// 使用 OpenClaw CLI 获取 skills 列表(全局技能)
|
|
86
|
+
const { execSync } = await import('node:child_process')
|
|
87
|
+
|
|
88
|
+
const result = execSync('openclaw skills list', {
|
|
89
|
+
encoding: 'utf-8',
|
|
90
|
+
timeout: 30000, // 30秒超时
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// 解析输出
|
|
94
|
+
const globalSkills = parseSkillsListOutput(result)
|
|
95
|
+
|
|
96
|
+
// 检查智能体工作区中的专有技能
|
|
97
|
+
const agentWorkspaceSkills = await fetchAgentWorkspaceSkills(agentId, log)
|
|
98
|
+
|
|
99
|
+
// 合并技能列表,去重
|
|
100
|
+
const allSkills = mergeSkills(globalSkills, agentWorkspaceSkills)
|
|
101
|
+
|
|
102
|
+
log?.info?.(`SkillsList: Found ${allSkills.length} skills (${globalSkills.length} global, ${agentWorkspaceSkills.length} agent-specific)`)
|
|
103
|
+
return allSkills
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
log?.error?.(`SkillsList: Error fetching skills: ${String(error)}`)
|
|
107
|
+
// 如果获取失败,返回空列表
|
|
108
|
+
return []
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 获取智能体工作区中的专有技能
|
|
114
|
+
*/
|
|
115
|
+
async function fetchAgentWorkspaceSkills(
|
|
116
|
+
agentId: number,
|
|
117
|
+
log?: {
|
|
118
|
+
info?: (msg: string) => void
|
|
119
|
+
error?: (msg: string) => void
|
|
120
|
+
},
|
|
121
|
+
): Promise<SkillItem[]> {
|
|
122
|
+
const skills: SkillItem[] = []
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
// 智能体工作区路径
|
|
126
|
+
const { homedir } = await import('node:os')
|
|
127
|
+
const { join } = await import('node:path')
|
|
128
|
+
const agentWorkspace = join(homedir(), '.openclaw', 'agents', `openclaw-workclaw-${agentId}`)
|
|
129
|
+
const skillsDir = join(agentWorkspace, 'skills')
|
|
130
|
+
|
|
131
|
+
// 检查技能目录是否存在
|
|
132
|
+
const { existsSync, readdirSync, statSync } = await import('node:fs')
|
|
133
|
+
if (!existsSync(skillsDir)) {
|
|
134
|
+
log?.info?.(`SkillsList: No skills directory found in agent workspace: ${skillsDir}`)
|
|
135
|
+
return skills
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 读取技能目录中的子目录(每个子目录是一个技能)
|
|
139
|
+
const items = readdirSync(skillsDir)
|
|
140
|
+
for (const item of items) {
|
|
141
|
+
const itemPath = join(skillsDir, item)
|
|
142
|
+
const stat = statSync(itemPath)
|
|
143
|
+
|
|
144
|
+
// 如果是目录,检查里面的技能文件
|
|
145
|
+
if (stat.isDirectory()) {
|
|
146
|
+
const skillFiles = readdirSync(itemPath)
|
|
147
|
+
for (const skillFile of skillFiles) {
|
|
148
|
+
if (skillFile.endsWith('.md') || skillFile.endsWith('.prose')) {
|
|
149
|
+
const skillFilePath = join(itemPath, skillFile)
|
|
150
|
+
const skill = parseSkillFile(skillFilePath, item, log)
|
|
151
|
+
if (skill) {
|
|
152
|
+
skills.push(skill)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
log?.info?.(`SkillsList: Found ${skills.length} agent-specific skills in ${skillsDir}`)
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
log?.error?.(`SkillsList: Error reading agent workspace skills: ${String(error)}`)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return skills
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 解析技能文件
|
|
170
|
+
*/
|
|
171
|
+
function parseSkillFile(
|
|
172
|
+
filePath: string,
|
|
173
|
+
skillName: string,
|
|
174
|
+
log?: {
|
|
175
|
+
info?: (msg: string) => void
|
|
176
|
+
error?: (msg: string) => void
|
|
177
|
+
},
|
|
178
|
+
): SkillItem | null {
|
|
179
|
+
try {
|
|
180
|
+
const content = readFileSync(filePath, 'utf-8')
|
|
181
|
+
|
|
182
|
+
// 提取描述(从文件内容中)
|
|
183
|
+
let description = 'Agent-specific skill'
|
|
184
|
+
const lines = content.split('\n')
|
|
185
|
+
for (const line of lines) {
|
|
186
|
+
if (line.startsWith('# ')) {
|
|
187
|
+
description = line.substring(2).trim()
|
|
188
|
+
break
|
|
189
|
+
}
|
|
190
|
+
else if (line.startsWith('summary:') || line.startsWith('Summary:')) {
|
|
191
|
+
description = line.substring(line.indexOf(':') + 1).trim()
|
|
192
|
+
break
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
typeStr: 'skill',
|
|
198
|
+
name: skillName,
|
|
199
|
+
description,
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
log?.error?.(`SkillsList: Error parsing skill file ${filePath}: ${String(error)}`)
|
|
204
|
+
return null
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 合并技能列表,去重
|
|
210
|
+
*/
|
|
211
|
+
function mergeSkills(globalSkills: SkillItem[], agentSkills: SkillItem[]): SkillItem[] {
|
|
212
|
+
const skillMap = new Map<string, SkillItem>()
|
|
213
|
+
|
|
214
|
+
// 先添加全局技能
|
|
215
|
+
for (const skill of globalSkills) {
|
|
216
|
+
skillMap.set(skill.name, skill)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 再添加智能体专有技能(会覆盖同名的全局技能)
|
|
220
|
+
for (const skill of agentSkills) {
|
|
221
|
+
skillMap.set(skill.name, skill)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return Array.from(skillMap.values())
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 解析 openclaw skills list 的输出
|
|
229
|
+
*/
|
|
230
|
+
function parseSkillsListOutput(output: string): SkillItem[] {
|
|
231
|
+
const skills: SkillItem[] = []
|
|
232
|
+
const lines = output.split('\n')
|
|
233
|
+
|
|
234
|
+
// 查找表格内容(从表头后开始)
|
|
235
|
+
let inTable = false
|
|
236
|
+
let headerLine = -1
|
|
237
|
+
|
|
238
|
+
for (let i = 0; i < lines.length; i++) {
|
|
239
|
+
const line = lines[i]
|
|
240
|
+
|
|
241
|
+
// 找到表头行(包含 Status | Skill | Description | Source)
|
|
242
|
+
if (line.includes('Status') && line.includes('Skill') && line.includes('Description')) {
|
|
243
|
+
headerLine = i
|
|
244
|
+
inTable = true
|
|
245
|
+
continue
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 跳过分隔线
|
|
249
|
+
if (inTable && (line.startsWith('├') || line.startsWith('┌') || line.startsWith('└'))) {
|
|
250
|
+
continue
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 解析表格行
|
|
254
|
+
if (inTable && line.startsWith('│') && i > headerLine + 1) {
|
|
255
|
+
const parsed = parseSkillsTableRow(line)
|
|
256
|
+
if (parsed) {
|
|
257
|
+
skills.push(parsed)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 表格结束
|
|
262
|
+
if (inTable && line.startsWith('└')) {
|
|
263
|
+
break
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return skills
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 解析单行表格数据
|
|
272
|
+
*/
|
|
273
|
+
function parseSkillsTableRow(line: string): SkillItem | null {
|
|
274
|
+
// 移除开头的 │ 和结尾的 │
|
|
275
|
+
const content = line.replace(/^│\s*/, '').replace(/\s*│$/, '')
|
|
276
|
+
|
|
277
|
+
// 按 │ 分割列
|
|
278
|
+
const columns = content.split('│').map(col => col.trim())
|
|
279
|
+
|
|
280
|
+
if (columns.length < 4) {
|
|
281
|
+
return null
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const status = columns[0]
|
|
285
|
+
const skillName = columns[1]
|
|
286
|
+
const description = columns[2]
|
|
287
|
+
|
|
288
|
+
// 只返回 ready 状态的 skills
|
|
289
|
+
if (!status.includes('✓') && !status.includes('ready')) {
|
|
290
|
+
return null
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 提取 skill 名称(移除 emoji)
|
|
294
|
+
const cleanName = skillName.replace(/[\u{1F300}-\u{1F9FF}]/gu, '').trim()
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
typeStr: 'skill',
|
|
298
|
+
name: cleanName,
|
|
299
|
+
description,
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* 发送 skills 列表回调
|
|
305
|
+
*/
|
|
306
|
+
async function sendSkillsListCallback(
|
|
307
|
+
callbackUrl: string,
|
|
308
|
+
payload: SkillsListCallbackPayload,
|
|
309
|
+
token: string,
|
|
310
|
+
log?: {
|
|
311
|
+
info?: (msg: string) => void
|
|
312
|
+
error?: (msg: string) => void
|
|
313
|
+
},
|
|
314
|
+
): Promise<void> {
|
|
315
|
+
log?.info?.(`SkillsList: Sending callback to ${callbackUrl}`)
|
|
316
|
+
|
|
317
|
+
const response = await fetch(callbackUrl, {
|
|
318
|
+
method: 'POST',
|
|
319
|
+
headers: {
|
|
320
|
+
'Content-Type': 'application/json',
|
|
321
|
+
'Authorization': `Bearer ${token}`,
|
|
322
|
+
},
|
|
323
|
+
body: JSON.stringify(payload),
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
if (!response.ok) {
|
|
327
|
+
const errorText = await response.text().catch(() => 'Unknown error')
|
|
328
|
+
throw new Error(`Callback failed: ${response.status} ${errorText}`)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
log?.info?.(`SkillsList: Callback sent successfully`)
|
|
332
|
+
}
|
|
@@ -448,10 +448,14 @@ function handleSharedMessage(appKey: string, data: string, cfg: any, logger: any
|
|
|
448
448
|
userId = String(parsed.eventData?.futureId || parsed.eventData?.userId || '')
|
|
449
449
|
agentId = String(parsed.eventData?.id || parsed.eventData?.agentId || '')
|
|
450
450
|
}
|
|
451
|
-
else if (parsed.type === 'tools_list' || parsed.type === 'skills_list'
|
|
451
|
+
else if (parsed.type === 'tools_list' || parsed.type === 'skills_list') {
|
|
452
452
|
userId = String(parsed.eventData?.userId || '')
|
|
453
453
|
agentId = String(parsed.eventData?.agentId || '')
|
|
454
454
|
}
|
|
455
|
+
else if(parsed.type === "skills_event") {
|
|
456
|
+
userId = String(parsed.eventData?.data?.userId || "");
|
|
457
|
+
agentId = String(parsed.eventData?.data?.mainId || "");
|
|
458
|
+
}
|
|
455
459
|
else if (parsed.type === 'init_agent') {
|
|
456
460
|
userId = String(parsed.eventData?.futureId || parsed.eventData?.userId || '')
|
|
457
461
|
agentId = String(parsed.eventData?.id || parsed.eventData?.agentId || '')
|