@dcrays/dcgchat 0.4.29 → 0.5.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/src/skill.ts DELETED
@@ -1,151 +0,0 @@
1
- import axios from 'axios'
2
- /** @ts-ignore */
3
- import unzipper from 'unzipper'
4
- import { pipeline } from 'stream/promises'
5
- import fs from 'fs'
6
- import path from 'path'
7
- import { getWorkspaceDir } from './utils/global.js'
8
- import { getWsConnection } from './utils/global.js'
9
- import { dcgLogger } from './utils/log.js'
10
- import { isWsOpen } from './transport.js'
11
- import { sendMessageToGateway } from './gateway/socket.js'
12
- import { decodeZipEntryPath } from './utils/zipPath.js'
13
-
14
- type ISkillParams = {
15
- path: string
16
- code: string
17
- }
18
-
19
- function sendEvent(msgContent: Record<string, any>) {
20
- const ws = getWsConnection()
21
- if (isWsOpen()) {
22
- ws?.send(
23
- JSON.stringify({
24
- messageType: 'openclaw_bot_event',
25
- source: 'client',
26
- content: msgContent
27
- })
28
- )
29
- dcgLogger(`技能安装: ${JSON.stringify(msgContent)}`)
30
- }
31
- }
32
-
33
- export async function installSkill(params: ISkillParams, msgContent: Record<string, any>) {
34
- const { path: cdnUrl, code } = params
35
- const workspacePath = getWorkspaceDir()
36
-
37
- // 确保 skills 目录存在
38
- const skillsDir = path.join(workspacePath, 'skills')
39
- if (!fs.existsSync(skillsDir)) {
40
- fs.mkdirSync(skillsDir, { recursive: true })
41
- }
42
- // 如果目标目录已存在,先删除
43
- const skillDir = path.join(workspacePath, 'skills', code)
44
- if (fs.existsSync(skillDir)) {
45
- fs.rmSync(skillDir, { recursive: true, force: true })
46
- }
47
-
48
- try {
49
- // 下载 zip 文件
50
- const response = await axios({
51
- method: 'get',
52
- url: cdnUrl,
53
- responseType: 'stream'
54
- })
55
- // 创建目标目录
56
- fs.mkdirSync(skillDir, { recursive: true })
57
- // 解压文件到目标目录,跳过顶层文件夹
58
- await new Promise((resolve, reject) => {
59
- const tasks: Promise<void>[] = []
60
- let rootDir: string | null = null
61
- let hasError = false
62
-
63
- response.data
64
- .pipe(unzipper.Parse())
65
- .on('entry', (entry: any) => {
66
- if (hasError) {
67
- entry.autodrain()
68
- return
69
- }
70
- try {
71
- const flags = entry.props?.flags ?? 0
72
- const entryPath = decodeZipEntryPath(entry.props?.pathBuffer, flags, entry.path)
73
- const pathParts = entryPath.split('/')
74
-
75
- // 检测根目录
76
- if (!rootDir && pathParts.length > 1) {
77
- rootDir = pathParts[0]
78
- }
79
- let newPath = entryPath
80
- // 移除顶层文件夹
81
- if (rootDir && entryPath.startsWith(rootDir + '/')) {
82
- newPath = entryPath.slice(rootDir.length + 1)
83
- }
84
-
85
- if (!newPath) {
86
- entry.autodrain()
87
- return
88
- }
89
-
90
- const targetPath = path.join(skillDir, newPath)
91
-
92
- if (entry.type === 'Directory') {
93
- fs.mkdirSync(targetPath, { recursive: true })
94
- entry.autodrain()
95
- } else {
96
- const parentDir = path.dirname(targetPath)
97
- fs.mkdirSync(parentDir, { recursive: true })
98
- const writeStream = fs.createWriteStream(targetPath)
99
- const task = pipeline(entry, writeStream).catch((err) => {
100
- hasError = true
101
- throw new Error(`解压文件失败 ${entryPath}: ${err.message}`)
102
- })
103
- tasks.push(task)
104
- }
105
- } catch (err) {
106
- hasError = true
107
- entry.autodrain()
108
- reject(new Error(`处理entry失败: ${err}`))
109
- }
110
- })
111
- .on('close', async () => {
112
- try {
113
- await Promise.all(tasks)
114
- resolve(null)
115
- } catch (err) {
116
- reject(err)
117
- }
118
- })
119
- .on('error', (err: { message: any }) => {
120
- hasError = true
121
- reject(new Error(`解压流错误: ${err.message}`))
122
- })
123
- })
124
- sendEvent({ ...msgContent, status: 'ok' })
125
- sendMessageToGateway(JSON.stringify({ method: 'skills.status', params: {} }))
126
- } catch (error) {
127
- // 如果安装失败,清理目录
128
- if (fs.existsSync(skillDir)) {
129
- fs.rmSync(skillDir, { recursive: true, force: true })
130
- }
131
- sendEvent({ ...msgContent, status: 'fail' })
132
- }
133
- }
134
-
135
- export function uninstallSkill(params: Omit<ISkillParams, 'path'>, msgContent: Record<string, any>) {
136
- const { code } = params
137
-
138
- const workspacePath = getWorkspaceDir()
139
- if (!workspacePath) {
140
- sendEvent({ ...msgContent, status: 'ok' })
141
- }
142
-
143
- const skillDir = path.join(workspacePath, 'skills', code)
144
-
145
- if (fs.existsSync(skillDir)) {
146
- fs.rmSync(skillDir, { recursive: true, force: true })
147
- sendEvent({ ...msgContent, status: 'ok' })
148
- } else {
149
- sendEvent({ ...msgContent, status: 'ok' })
150
- }
151
- }
package/src/tool.ts DELETED
@@ -1,422 +0,0 @@
1
- import type { OpenClawPluginApi } from 'openclaw/plugin-sdk'
2
- import { getMsgStatus } from './utils/global.js'
3
- import { dcgLogger } from './utils/log.js'
4
- import { sendFinal, sendText, wsSendRaw } from './transport.js'
5
- import { getEffectiveMsgParams, deleteSessionKeyBySubAgentRunId, setSessionKeyBySubAgentRunId } from './utils/params.js'
6
- import { cronToolCall } from './cronToolCall.js'
7
-
8
- type PluginHookName =
9
- | 'before_model_resolve'
10
- | 'before_prompt_build'
11
- | 'before_agent_start'
12
- | 'llm_input'
13
- | 'llm_output'
14
- | 'agent_end'
15
- | 'before_compaction'
16
- | 'after_compaction'
17
- | 'before_reset'
18
- | 'message_received'
19
- | 'message_sending'
20
- | 'message_sent'
21
- | 'before_tool_call'
22
- | 'after_tool_call'
23
- | 'tool_result_persist'
24
- | 'before_message_write'
25
- | 'session_start'
26
- | 'session_end'
27
- | 'subagent_spawning'
28
- | 'subagent_delivery_target'
29
- | 'subagent_spawned'
30
- | 'subagent_ended'
31
- | 'gateway_start'
32
- | 'gateway_stop'
33
-
34
- // message_received 没有 sessionKey 前置到bot中执行
35
- const eventList = [
36
- // { event: 'message_received', message: '' },
37
- // {event: 'before_model_resolve', message: ''},
38
- // {event: 'before_prompt_build', message: '正在查阅背景资料,构建思考逻辑'},
39
- // {event: 'before_agent_start', message: '书灵墨宝已就位,准备开始执行任务'},
40
- { event: 'subagent_spawning', message: '' },
41
- { event: 'subagent_spawned', message: '' },
42
- { event: 'subagent_delivery_target', message: '' },
43
- // {event: 'llm_input', message: ''},
44
- { event: 'llm_output', message: '' },
45
- // {event: 'agent_end', message: '核心任务已处理完毕...'},
46
- { event: 'subagent_ended', message: '' },
47
- // {event: 'before_message_write', message: '正在将本次对话存入记忆库...'},
48
- // {event: 'message_sending', message: ''},
49
- // {event: 'message_send', message: ''},
50
- { event: 'before_tool_call', message: '' },
51
- { event: 'after_tool_call', message: '' }
52
- ]
53
-
54
- /** 子 agent 的 sessionKey 往往未写入 params map,回落到主会话 outbound 参数避免 messageId 缺失 */
55
- function resolveOutboundParamsForSession(sk: string) {
56
- const k = sk.trim()
57
- let params = getEffectiveMsgParams(k)
58
- if (params.messageId?.trim() || params.sessionId?.trim()) return params
59
- const parent = requesterByChildSessionKey.get(k)
60
- if (parent) {
61
- const parentParams = getEffectiveMsgParams(parent)
62
- if (parentParams.messageId?.trim() || parentParams.sessionId?.trim()) return parentParams
63
- }
64
- return params
65
- }
66
-
67
- /** 主会话已 running 时,子会话上的工具/事件也应下发(否则子 key 无 running 状态会整段丢消息) */
68
- export function isSessionActiveForTool(sk: string): boolean {
69
- const k = sk.trim()
70
- if (!k) return false
71
- if (getMsgStatus(k) === 'running') return true
72
- const parent = requesterByChildSessionKey.get(k)
73
- return parent ? getMsgStatus(parent) === 'running' : false
74
- }
75
-
76
- function sendToolCallMessage(sk: string, text: string, toolCallId: string, isCover: number) {
77
- const params = resolveOutboundParamsForSession(sk)
78
- const content = { is_finish: -1, tool_call_id: toolCallId, is_cover: isCover, thinking_content: text, response: '' }
79
- wsSendRaw(params, content, false)
80
- }
81
-
82
- /**
83
- * 深拷贝 params 并注入 bestEffort: true
84
- */
85
- interface CronDelivery {
86
- mode?: string
87
- channel?: string
88
- to?: string
89
- bestEffort?: boolean
90
- [key: string]: unknown
91
- }
92
-
93
- // --- Subagent 活跃跟踪(按主会话 requesterSessionKey)---
94
-
95
- /** 主会话 sessionKey -> 仍活跃的子 agent runId */
96
- const activeSubagentRunIdsByRequester = new Map<string, Set<string>>()
97
- /** 子会话 childSessionKey -> 主会话 requesterSessionKey */
98
- const requesterByChildSessionKey = new Map<string, string>()
99
- /** 子会话 childSessionKey -> spawn 时的 runId(ended 事件可能不带 runId) */
100
- const runIdByChildSessionKey = new Map<string, string>()
101
- /** 主会话 -> 等待「子 agent 全部结束」的回调 */
102
- const subagentIdleWaiters = new Map<string, Set<() => void>>()
103
-
104
- function getOrCreateRunIdSet(requesterSessionKey: string): Set<string> {
105
- let set = activeSubagentRunIdsByRequester.get(requesterSessionKey)
106
- if (!set) {
107
- set = new Set()
108
- activeSubagentRunIdsByRequester.set(requesterSessionKey, set)
109
- }
110
- return set
111
- }
112
-
113
- function flushSubagentIdleWaiters(requesterSessionKey: string): void {
114
- const set = activeSubagentRunIdsByRequester.get(requesterSessionKey)
115
- if (set && set.size > 0) return
116
- activeSubagentRunIdsByRequester.delete(requesterSessionKey)
117
- const waiters = subagentIdleWaiters.get(requesterSessionKey)
118
- if (!waiters?.size) return
119
- subagentIdleWaiters.delete(requesterSessionKey)
120
- for (const w of waiters) {
121
- try {
122
- w()
123
- } catch (e) {
124
- dcgLogger(`subagent idle waiter error: ${String(e)}`, 'error')
125
- }
126
- }
127
- }
128
-
129
- function registerSubagentSpawn(requesterSessionKey: string, runId: string, childSessionKey: string): void {
130
- const req = requesterSessionKey.trim()
131
- const rid = runId.trim()
132
- const child = childSessionKey.trim()
133
- if (!req || !rid || !child) {
134
- dcgLogger(`subagent track spawn skipped: missing key req=${req} runId=${rid} child=${child}`)
135
- return
136
- }
137
- getOrCreateRunIdSet(req).add(rid)
138
- requesterByChildSessionKey.set(child, req)
139
- runIdByChildSessionKey.set(child, rid)
140
- dcgLogger(`subagent track spawn: requester=${req} runId=${rid} child=${child} active=${getOrCreateRunIdSet(req).size}`)
141
- }
142
-
143
- function registerSubagentEnd(
144
- ctx: { requesterSessionKey?: string; sessionKey?: string },
145
- targetSessionKey: string,
146
- runId?: string
147
- ): void {
148
- const child = targetSessionKey.trim()
149
- if (!child) return
150
- const req = ctx.requesterSessionKey?.trim() || requesterByChildSessionKey.get(child) || ctx.sessionKey?.trim() || ''
151
- const resolvedRunId = (runId?.trim() || runIdByChildSessionKey.get(child) || '').trim()
152
- deleteSessionKeyBySubAgentRunId(resolvedRunId)
153
- if (!req) {
154
- dcgLogger(`subagent track end: no requester for child=${child} runId=${resolvedRunId}`)
155
- requesterByChildSessionKey.delete(child)
156
- runIdByChildSessionKey.delete(child)
157
- return
158
- }
159
- const set = activeSubagentRunIdsByRequester.get(req)
160
- if (set && resolvedRunId) {
161
- set.delete(resolvedRunId)
162
- }
163
- requesterByChildSessionKey.delete(child)
164
- runIdByChildSessionKey.delete(child)
165
- dcgLogger(`subagent track end: requester=${req} runId=${resolvedRunId || 'n/a'} remaining=${set?.size ?? 0}`)
166
- if (set && set.size === 0) {
167
- activeSubagentRunIdsByRequester.delete(req)
168
- }
169
- flushSubagentIdleWaiters(req)
170
- }
171
-
172
- /** 当前跟踪到的、挂在该主会话下的子会话 sessionKey(供 /stop 时逐个 chat.abort) */
173
- export function getChildSessionKeysTrackedForRequester(requesterSessionKey: string): string[] {
174
- const req = requesterSessionKey.trim()
175
- if (!req) return []
176
- const out: string[] = []
177
- for (const [child, parent] of requesterByChildSessionKey.entries()) {
178
- if (parent === req) out.push(child)
179
- }
180
- return out
181
- }
182
-
183
- /**
184
- * 自根 requester 起 BFS 收集所有已跟踪后代子会话(含嵌套)。网关 abort 时宜自深到浅,调用方对结果 `.reverse()` 后再逐个 chat.abort。
185
- */
186
- export function getDescendantSessionKeysForRequester(rootRequesterSessionKey: string): string[] {
187
- const root = rootRequesterSessionKey.trim()
188
- if (!root) return []
189
- const ordered: string[] = []
190
- const seen = new Set<string>()
191
- let frontier = getChildSessionKeysTrackedForRequester(root)
192
- while (frontier.length > 0) {
193
- const next: string[] = []
194
- for (const sk of frontier) {
195
- const k = sk.trim()
196
- if (!k || seen.has(k)) continue
197
- seen.add(k)
198
- ordered.push(k)
199
- next.push(...getChildSessionKeysTrackedForRequester(k))
200
- }
201
- frontier = next
202
- }
203
- return ordered
204
- }
205
-
206
- /**
207
- * 打断后清空本地子 agent 跟踪(runId、父子映射、子 runId→sessionKey),并唤醒 waitUntilSubagentsIdle,避免永久挂起。
208
- */
209
- export function resetSubagentStateForRequesterSession(requesterSessionKey: string): void {
210
- const req = requesterSessionKey.trim()
211
- if (!req) return
212
-
213
- const runIdSet = activeSubagentRunIdsByRequester.get(req)
214
- if (runIdSet) {
215
- for (const rid of runIdSet) {
216
- deleteSessionKeyBySubAgentRunId(rid)
217
- }
218
- }
219
-
220
- for (const [child, parent] of [...requesterByChildSessionKey.entries()]) {
221
- if (parent === req) {
222
- requesterByChildSessionKey.delete(child)
223
- runIdByChildSessionKey.delete(child)
224
- }
225
- }
226
-
227
- activeSubagentRunIdsByRequester.delete(req)
228
-
229
- const waiters = subagentIdleWaiters.get(req)
230
- if (!waiters?.size) return
231
- subagentIdleWaiters.delete(req)
232
- for (const w of waiters) {
233
- try {
234
- w()
235
- } catch (e) {
236
- dcgLogger(`subagent idle waiter error: ${String(e)}`, 'error')
237
- }
238
- }
239
- }
240
-
241
- /** 当前主会话下仍在跑的子 agent 数量(按 spawn 时 runId 去重) */
242
- export function getActiveSubagentCount(sessionKey: string): number {
243
- const sk = sessionKey?.trim()
244
- if (!sk) return 0
245
- return activeSubagentRunIdsByRequester.get(sk)?.size ?? 0
246
- }
247
-
248
- /**
249
- * 等到指定主会话下已跟踪的子 agent 全部结束(spawn 失败等路径也会触发 subagent_ended,一般会配对清理)。
250
- * 注意:须在收到 `subagent_spawned` 之后才会计入;仅 spawning 未 spawned 的不会阻塞。
251
- */
252
- export function waitUntilSubagentsIdle(sessionKey: string, opts?: { timeoutMs?: number; signal?: AbortSignal }): Promise<void> {
253
- const sk = sessionKey?.trim()
254
- if (!sk) return Promise.resolve()
255
-
256
- if (getActiveSubagentCount(sk) === 0) return Promise.resolve()
257
-
258
- return new Promise<void>((resolve, reject) => {
259
- let settled = false
260
- const finish = (fn: () => void) => {
261
- if (settled) return
262
- settled = true
263
- if (timeoutId) clearTimeout(timeoutId)
264
- opts?.signal?.removeEventListener('abort', onAbort)
265
- removeWaiter()
266
- fn()
267
- }
268
-
269
- const removeWaiter = () => {
270
- const bucket = subagentIdleWaiters.get(sk)
271
- if (!bucket) return
272
- bucket.delete(onIdle)
273
- if (bucket.size === 0) subagentIdleWaiters.delete(sk)
274
- }
275
-
276
- const onIdle = () => finish(() => resolve())
277
-
278
- const onAbort = () => {
279
- const reason = opts?.signal?.reason
280
- finish(() => reject(reason instanceof Error ? reason : new Error(String(reason ?? 'Aborted'))))
281
- }
282
-
283
- let timeoutId: ReturnType<typeof setTimeout> | undefined
284
- if (opts?.timeoutMs != null && opts.timeoutMs > 0) {
285
- timeoutId = setTimeout(
286
- () => finish(() => reject(new Error(`waitUntilSubagentsIdle timeout ${opts.timeoutMs}ms`))),
287
- opts.timeoutMs
288
- )
289
- }
290
-
291
- if (opts?.signal) {
292
- if (opts.signal.aborted) {
293
- onAbort()
294
- return
295
- }
296
- opts.signal.addEventListener('abort', onAbort, { once: true })
297
- }
298
-
299
- let set = subagentIdleWaiters.get(sk)
300
- if (!set) {
301
- set = new Set()
302
- subagentIdleWaiters.set(sk, set)
303
- }
304
- set.add(onIdle)
305
-
306
- if (getActiveSubagentCount(sk) === 0) {
307
- onIdle()
308
- }
309
- })
310
- }
311
-
312
- function resolveHookSessionKey(
313
- eventName: string,
314
- args: { sessionKey?: string; requesterSessionKey?: string; runId?: string }
315
- ): string {
316
- if (
317
- eventName === 'subagent_spawned' ||
318
- eventName === 'subagent_ended' ||
319
- eventName === 'subagent_spawning' ||
320
- eventName === 'subagent_delivery_target'
321
- ) {
322
- if (args?.runId && args?.requesterSessionKey) setSessionKeyBySubAgentRunId(args?.runId, args?.requesterSessionKey)
323
- return (args?.requesterSessionKey || args?.sessionKey || '').trim()
324
- }
325
- return (args?.sessionKey || '').trim()
326
- }
327
-
328
- /** 定时触发时会话往往非 running,但仍需跑 before_tool_call 以注入 sessionKey / delivery(见 cronToolCall) */
329
- function shouldRunBeforeToolCallWithoutRunningSession(event: { toolName?: string; params?: { command?: string } }): boolean {
330
- if (event?.toolName === 'cron') return true
331
- const cmd = event?.params?.command
332
- if (event?.toolName === 'exec' && typeof cmd === 'string') {
333
- return cmd.includes('cron create') || cmd.includes('cron add')
334
- }
335
- return false
336
- }
337
-
338
- function trackSubagentLifecycle(eventName: string, event: any, args: any): void {
339
- if (eventName === 'subagent_spawned') {
340
- const runId = typeof event?.runId === 'string' ? event.runId : ''
341
- const childSessionKey = typeof event?.childSessionKey === 'string' ? event.childSessionKey : ''
342
- const requester =
343
- typeof args?.requesterSessionKey === 'string'
344
- ? args.requesterSessionKey
345
- : typeof args?.sessionKey === 'string'
346
- ? args.sessionKey
347
- : ''
348
- registerSubagentSpawn(requester, runId, childSessionKey)
349
- return
350
- }
351
- if (eventName === 'subagent_ended') {
352
- const targetSessionKey = typeof event?.targetSessionKey === 'string' ? event.targetSessionKey : ''
353
- const runId = typeof event?.runId === 'string' ? event.runId : undefined
354
- registerSubagentEnd(args ?? {}, targetSessionKey, runId)
355
- }
356
- }
357
-
358
- export function monitoringToolMessage(api: OpenClawPluginApi) {
359
- for (const item of eventList) {
360
- api.on(item.event as PluginHookName, (event: any, args: any) => {
361
- trackSubagentLifecycle(item.event, event, args)
362
- const sk = resolveHookSessionKey(item.event, args ?? {})
363
- if (sk) {
364
- const toolHooksOk =
365
- isSessionActiveForTool(sk) || (item.event === 'before_tool_call' && shouldRunBeforeToolCallWithoutRunningSession(event))
366
- if (toolHooksOk) {
367
- if (['after_tool_call', 'before_tool_call'].includes(item.event)) {
368
- const { result: _result, ...rest } = event
369
- dcgLogger(`工具调用结果: ~ event:${item.event} ~ params:${JSON.stringify(rest)}`)
370
-
371
- if (item.event === 'before_tool_call') {
372
- const hookResult = cronToolCall(rest, sk)
373
- const text = JSON.stringify({
374
- type: item.event,
375
- specialIdentification: 'dcgchat_tool_call_special_identification',
376
- callId: event.toolCallId || event.runId || Date.now().toString(),
377
- ...rest,
378
- status: 'running'
379
- })
380
- sendToolCallMessage(sk, text, event.toolCallId || event.runId || Date.now().toString(), 0)
381
- return hookResult
382
- }
383
- const text = JSON.stringify({
384
- type: item.event,
385
- specialIdentification: 'dcgchat_tool_call_special_identification',
386
- callId: event.toolCallId || event.runId || Date.now().toString(),
387
- ...rest,
388
- status: item.event === 'after_tool_call' ? 'finished' : 'running'
389
- })
390
- sendToolCallMessage(
391
- sk,
392
- text,
393
- event.toolCallId || event.runId || Date.now().toString(),
394
- item.event === 'after_tool_call' ? 1 : 0
395
- )
396
- } else if (item.event) {
397
- const msgCtx = resolveOutboundParamsForSession(sk)
398
- if (item.event === 'llm_output') {
399
- if (event.lastAssistant?.errorMessage === '1003-额度不足请充值') {
400
- const message = '您的积分已消耗完,您可以通过充值积分来继续使用'
401
- sendText(message, msgCtx, { message_tags: { insufficient_balance: 1 }, is_finish: -1 })
402
- sendFinal(msgCtx, '积分不足')
403
- return
404
- }
405
- }
406
- const text = JSON.stringify({
407
- type: item.event,
408
- specialIdentification: 'dcgchat_tool_call_special_identification',
409
- toolName: '',
410
- callId: event.runId || Date.now().toString(),
411
- params: item.message
412
- })
413
- sendToolCallMessage(sk, text, event.runId || Date.now().toString(), 0)
414
- dcgLogger(`工具调用结果: ~ event:${item.event}`)
415
- }
416
- }
417
- } else if (item.event !== 'before_tool_call') {
418
- dcgLogger(`工具调用结果: ~ event:${item.event} ~ 没有sessionKey 为执行`)
419
- }
420
- })
421
- }
422
- }