@dcrays/dcgchat-test 0.3.35 → 0.3.37
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 +1 -1
- package/src/agent.ts +204 -0
- package/src/bot.ts +130 -126
- package/src/channel.ts +15 -6
- package/src/gateway/index.ts +2 -26
- package/src/monitor.ts +3 -51
- package/src/skill.ts +2 -7
- package/src/tool.ts +236 -2
- package/src/types.ts +1 -0
- package/src/utils/constant.ts +2 -2
- package/src/utils/gatewayMsgHanlder.ts +43 -0
- package/src/utils/global.ts +20 -26
- package/src/utils/params.ts +18 -0
- package/src/utils/wsMessageHandler.ts +64 -0
- package/src/utils/zipPath.ts +24 -0
package/package.json
CHANGED
package/src/agent.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
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 IAgentParams = {
|
|
15
|
+
url: string
|
|
16
|
+
agent_code: string
|
|
17
|
+
agent_name: string
|
|
18
|
+
agent_description: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sendEvent(msgContent: Record<string, any>) {
|
|
22
|
+
const ws = getWsConnection()
|
|
23
|
+
if (isWsOpen()) {
|
|
24
|
+
ws?.send(
|
|
25
|
+
JSON.stringify({
|
|
26
|
+
messageType: 'openclaw_bot_event',
|
|
27
|
+
source: 'client',
|
|
28
|
+
content: msgContent
|
|
29
|
+
})
|
|
30
|
+
)
|
|
31
|
+
dcgLogger(`agent安装: ${JSON.stringify(msgContent)}`)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ICreateAgentParams {
|
|
36
|
+
code: string
|
|
37
|
+
workspace: string
|
|
38
|
+
name?: string
|
|
39
|
+
description?: string
|
|
40
|
+
msgContent?: Record<string, any>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 若 workspace-${code}/agent 存在,则复制到 agents/${code}/agent */
|
|
44
|
+
function copyAgentsFiles(code: string) {
|
|
45
|
+
const workspacePath = getWorkspaceDir()
|
|
46
|
+
if (!workspacePath) return
|
|
47
|
+
const workspaceDir = path.join(workspacePath, '../', `workspace-${code}`)
|
|
48
|
+
const agentDir = path.join(workspacePath, '../', `agents/${code}`)
|
|
49
|
+
const sourceAgent = path.join(workspaceDir, 'agent')
|
|
50
|
+
try {
|
|
51
|
+
if (!fs.existsSync(sourceAgent)) return
|
|
52
|
+
if (!fs.statSync(sourceAgent).isDirectory()) return
|
|
53
|
+
fs.mkdirSync(agentDir, { recursive: true })
|
|
54
|
+
const dest = path.join(agentDir, 'agent')
|
|
55
|
+
if (fs.existsSync(dest)) {
|
|
56
|
+
fs.rmSync(dest, { recursive: true, force: true })
|
|
57
|
+
}
|
|
58
|
+
fs.cpSync(sourceAgent, dest, { recursive: true })
|
|
59
|
+
} catch (err: unknown) {
|
|
60
|
+
dcgLogger(`copyAgentsFiles failed: ${String(err)}`, 'error')
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function onCreateAgent(params: Record<string, any>) {
|
|
65
|
+
const { code, name, description } = params
|
|
66
|
+
try {
|
|
67
|
+
await sendMessageToGateway(JSON.stringify({ method: 'agents.create', params: { name: code, workspace: code } }))
|
|
68
|
+
} catch (err: unknown) {
|
|
69
|
+
dcgLogger(`agents.create failed: ${String(err)}`, 'error')
|
|
70
|
+
}
|
|
71
|
+
// Update config.name to the user-supplied display name (may contain CJK, spaces, etc.)
|
|
72
|
+
try {
|
|
73
|
+
await sendMessageToGateway(JSON.stringify({ method: 'agents.update', params: { name: name, agentId: code } }))
|
|
74
|
+
} catch (err: unknown) {
|
|
75
|
+
dcgLogger(`agents.update failed: ${String(err)}`, 'error')
|
|
76
|
+
}
|
|
77
|
+
if (description?.trim()) {
|
|
78
|
+
try {
|
|
79
|
+
await sendMessageToGateway(
|
|
80
|
+
JSON.stringify({
|
|
81
|
+
method: 'agents.files.set',
|
|
82
|
+
params: { agentId: code, name: 'IDENTITY.md', content: description.trim() }
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
} catch {
|
|
86
|
+
// Non-fatal
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (name?.trim()) {
|
|
90
|
+
try {
|
|
91
|
+
await sendMessageToGateway(
|
|
92
|
+
JSON.stringify({
|
|
93
|
+
method: 'agents.files.set',
|
|
94
|
+
params: { agentId: code, name: 'USER.md', content: name.trim() }
|
|
95
|
+
})
|
|
96
|
+
)
|
|
97
|
+
} catch {
|
|
98
|
+
// Non-fatal
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
copyAgentsFiles(code)
|
|
102
|
+
sendEvent({ ...params, status: 'ok' })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function createAgent(msgContent: Record<string, any>) {
|
|
106
|
+
const { url, code } = msgContent
|
|
107
|
+
if (!url || !code) {
|
|
108
|
+
dcgLogger(`createAgent failed empty url&code: ${JSON.stringify(msgContent)}`, 'error')
|
|
109
|
+
sendEvent({ ...msgContent, status: 'fail' })
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
const workspacePath = getWorkspaceDir()
|
|
113
|
+
const workspaceDir = path.join(workspacePath, '../', `workspace-${code}`)
|
|
114
|
+
|
|
115
|
+
// 如果目标目录已存在,先删除
|
|
116
|
+
if (fs.existsSync(workspaceDir)) {
|
|
117
|
+
fs.rmSync(workspaceDir, { recursive: true, force: true })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
// 下载 zip 文件
|
|
122
|
+
const response = await axios({
|
|
123
|
+
method: 'get',
|
|
124
|
+
url,
|
|
125
|
+
responseType: 'stream'
|
|
126
|
+
})
|
|
127
|
+
// 创建目标目录
|
|
128
|
+
fs.mkdirSync(workspaceDir, { recursive: true })
|
|
129
|
+
// 解压文件到目标目录,跳过顶层文件夹
|
|
130
|
+
await new Promise((resolve, reject) => {
|
|
131
|
+
const tasks: Promise<void>[] = []
|
|
132
|
+
let rootDir: string | null = null
|
|
133
|
+
let hasError = false
|
|
134
|
+
|
|
135
|
+
response.data
|
|
136
|
+
.pipe(unzipper.Parse())
|
|
137
|
+
.on('entry', (entry: any) => {
|
|
138
|
+
if (hasError) {
|
|
139
|
+
entry.autodrain()
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const flags = entry.props?.flags ?? 0
|
|
144
|
+
const entryPath = decodeZipEntryPath(entry.props?.pathBuffer, flags, entry.path)
|
|
145
|
+
const pathParts = entryPath.split('/')
|
|
146
|
+
|
|
147
|
+
// 检测根目录
|
|
148
|
+
if (!rootDir && pathParts.length > 1) {
|
|
149
|
+
rootDir = pathParts[0]
|
|
150
|
+
}
|
|
151
|
+
let newPath = entryPath
|
|
152
|
+
// 移除顶层文件夹
|
|
153
|
+
if (rootDir && entryPath.startsWith(rootDir + '/')) {
|
|
154
|
+
newPath = entryPath.slice(rootDir.length + 1)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!newPath) {
|
|
158
|
+
entry.autodrain()
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const targetPath = path.join(workspacePath, newPath)
|
|
163
|
+
|
|
164
|
+
if (entry.type === 'Directory') {
|
|
165
|
+
fs.mkdirSync(targetPath, { recursive: true })
|
|
166
|
+
entry.autodrain()
|
|
167
|
+
} else {
|
|
168
|
+
const parentDir = path.dirname(targetPath)
|
|
169
|
+
fs.mkdirSync(parentDir, { recursive: true })
|
|
170
|
+
const writeStream = fs.createWriteStream(targetPath)
|
|
171
|
+
const task = pipeline(entry, writeStream).catch((err) => {
|
|
172
|
+
hasError = true
|
|
173
|
+
throw new Error(`解压文件失败 ${entryPath}: ${err.message}`)
|
|
174
|
+
})
|
|
175
|
+
tasks.push(task)
|
|
176
|
+
}
|
|
177
|
+
} catch (err) {
|
|
178
|
+
hasError = true
|
|
179
|
+
entry.autodrain()
|
|
180
|
+
reject(new Error(`处理entry失败: ${err}`))
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
.on('close', async () => {
|
|
184
|
+
try {
|
|
185
|
+
await Promise.all(tasks)
|
|
186
|
+
resolve(null)
|
|
187
|
+
} catch (err) {
|
|
188
|
+
reject(err)
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
.on('error', (err: { message: any }) => {
|
|
192
|
+
hasError = true
|
|
193
|
+
reject(new Error(`解压流错误: ${err.message}`))
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
await onCreateAgent(msgContent)
|
|
197
|
+
} catch (error) {
|
|
198
|
+
// 如果安装失败,清理目录
|
|
199
|
+
if (fs.existsSync(workspaceDir)) {
|
|
200
|
+
fs.rmSync(workspaceDir, { recursive: true, force: true })
|
|
201
|
+
}
|
|
202
|
+
sendEvent({ ...msgContent, status: 'fail' })
|
|
203
|
+
}
|
|
204
|
+
}
|
package/src/bot.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import os from 'node:os'
|
|
4
|
-
import type { ReplyPayload } from 'openclaw/plugin-sdk'
|
|
5
|
-
import { createReplyPrefixContext } from 'openclaw/plugin-sdk'
|
|
4
|
+
import type { PluginRuntime, ReplyPayload } from 'openclaw/plugin-sdk'
|
|
5
|
+
import { createPluginRuntimeStore, createReplyPrefixContext, createTypingCallbacks } from 'openclaw/plugin-sdk'
|
|
6
6
|
import type { InboundMessage } from './types.js'
|
|
7
7
|
import {
|
|
8
8
|
clearSentMediaKeys,
|
|
@@ -18,8 +18,9 @@ import { extractMobookFiles } from './utils/searchFile.js'
|
|
|
18
18
|
import { sendChunk, sendFinal, sendText as sendTextMsg, sendError, wsSendRaw, sendText } from './transport.js'
|
|
19
19
|
import { dcgLogger } from './utils/log.js'
|
|
20
20
|
import { channelInfo, systemCommand, interruptCommand, ENV, ignoreToolCommand } from './utils/constant.js'
|
|
21
|
-
import {
|
|
22
|
-
import { getEffectiveMsgParams, setParamsMessage } from './utils/params.js'
|
|
21
|
+
import { sendGatewayRpc } from './gateway/socket.js'
|
|
22
|
+
import { clearParamsMessage, getEffectiveMsgParams, setParamsMessage } from './utils/params.js'
|
|
23
|
+
import { getChildSessionKeysTrackedForRequester, resetSubagentStateForRequesterSession, waitUntilSubagentsIdle } from './tool.js'
|
|
23
24
|
|
|
24
25
|
type MediaInfo = {
|
|
25
26
|
path: string
|
|
@@ -44,13 +45,6 @@ const sessionStreamSuppressed = new Set<string>()
|
|
|
44
45
|
/** 各 sessionKey 当前轮回复的流式分片序号(仅统计实际下发的文本 chunk);每轮非打断消息开始时置 0 */
|
|
45
46
|
const streamChunkIdxBySessionKey = new Map<string, number>()
|
|
46
47
|
|
|
47
|
-
/** Active LLM generation abort controllers, keyed by conversationId */
|
|
48
|
-
// const activeGenerations = new Map<string, AbortController>()
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Extract agentId from conversation_id formatted as "agentId::suffix".
|
|
52
|
-
* Returns null if the conversation_id does not contain the "::" separator.
|
|
53
|
-
*/
|
|
54
48
|
export function extractAgentIdFromConversationId(conversationId: string): string | null {
|
|
55
49
|
const idx = conversationId.indexOf('::')
|
|
56
50
|
if (idx <= 0) return null
|
|
@@ -132,7 +126,14 @@ function resolveReplyMediaList(payload: ReplyPayload): string[] {
|
|
|
132
126
|
if (payload.mediaUrls?.length) return payload.mediaUrls.filter(Boolean)
|
|
133
127
|
return payload.mediaUrl ? [payload.mediaUrl] : []
|
|
134
128
|
}
|
|
135
|
-
|
|
129
|
+
const typingCallbacks = createTypingCallbacks({
|
|
130
|
+
start: async () => {
|
|
131
|
+
console.log('typing start')
|
|
132
|
+
},
|
|
133
|
+
onStartError: (err) => {
|
|
134
|
+
console.log('typing start error', err)
|
|
135
|
+
}
|
|
136
|
+
})
|
|
136
137
|
/**
|
|
137
138
|
* 处理一条用户消息,调用 Agent 并返回回复
|
|
138
139
|
*/
|
|
@@ -151,7 +152,6 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
151
152
|
const core = getDcgchatRuntime()
|
|
152
153
|
|
|
153
154
|
const conversationId = msg.content.session_id?.trim()
|
|
154
|
-
const agentId = msg.content.agent_id?.trim()
|
|
155
155
|
const real_mobook = msg.content.real_mobook?.toString().trim()
|
|
156
156
|
|
|
157
157
|
const route = core.channel.routing.resolveAgentRoute({
|
|
@@ -179,10 +179,6 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
179
179
|
real_mobook
|
|
180
180
|
}
|
|
181
181
|
setParamsMessage(effectiveSessionKey, mergedParams)
|
|
182
|
-
// 与 OpenClaw 会话投递里仍可能出现的 ctx.to=SenderId(userId)对齐,便于 getOutboundMsgParams 命中
|
|
183
|
-
dcgLogger(
|
|
184
|
-
`target normalize: rawTarget=${userId}, normalizedTarget=${effectiveSessionKey}, conversationId=${conversationId ?? ''}, messageId=${msg.content.message_id}`
|
|
185
|
-
)
|
|
186
182
|
setParamsMessage(userId, mergedParams)
|
|
187
183
|
dcgLogger(`target alias bound: aliasTarget=${userId} -> sessionKey=${effectiveSessionKey}`)
|
|
188
184
|
const outboundCtx = getEffectiveMsgParams(effectiveSessionKey)
|
|
@@ -190,29 +186,15 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
190
186
|
effectiveAgentId && effectiveAgentId !== 'main' ? config.agents?.list?.find((a) => a.id === effectiveAgentId) : undefined
|
|
191
187
|
const agentDisplayName = agentEntry?.name ?? (effectiveAgentId && effectiveAgentId !== 'main' ? effectiveAgentId : undefined)
|
|
192
188
|
|
|
193
|
-
const safeSendFinal = (tag: string) => {
|
|
194
|
-
if (finalSent) return
|
|
195
|
-
finalSent = true
|
|
196
|
-
sendFinal(outboundCtx, tag)
|
|
197
|
-
setMsgStatus(effectiveSessionKey, 'finished')
|
|
198
|
-
}
|
|
199
|
-
|
|
200
189
|
const text = msg.content.text?.trim()
|
|
201
190
|
|
|
202
191
|
if (!text) {
|
|
203
192
|
sendTextMsg('你需要我帮你做什么呢?', outboundCtx)
|
|
204
|
-
|
|
193
|
+
sendFinal(outboundCtx, 'not text')
|
|
205
194
|
return
|
|
206
195
|
}
|
|
207
196
|
|
|
208
197
|
try {
|
|
209
|
-
// Abort any existing generation for this conversation, then start a new one
|
|
210
|
-
// const existingCtrl = activeGenerations.get(conversationId)
|
|
211
|
-
// if (existingCtrl) existingCtrl.abort()
|
|
212
|
-
// const genCtrl = new AbortController()
|
|
213
|
-
// const genSignal = genCtrl.signal
|
|
214
|
-
// activeGenerations.set(conversationId, genCtrl)
|
|
215
|
-
|
|
216
198
|
// 处理用户上传的文件
|
|
217
199
|
const files = msg.content.files ?? []
|
|
218
200
|
let mediaPayload: Record<string, unknown> = {}
|
|
@@ -250,11 +232,10 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
250
232
|
CommandAuthorized: true,
|
|
251
233
|
OriginatingChannel: "dcgchat-test",
|
|
252
234
|
OriginatingTo: effectiveSessionKey,
|
|
235
|
+
Target: effectiveSessionKey,
|
|
236
|
+
SourceTarget: effectiveSessionKey,
|
|
253
237
|
...mediaPayload
|
|
254
238
|
})
|
|
255
|
-
dcgLogger(
|
|
256
|
-
`inbound context target: rawTarget=${userId}, normalizedTarget=${effectiveSessionKey}, ctx.To=${String(ctxPayload.To ?? '')}, ctx.SessionKey=${String(ctxPayload.SessionKey ?? '')}, ctx.OriginatingTo=${String(ctxPayload.OriginatingTo ?? '')}`
|
|
257
|
-
)
|
|
258
239
|
|
|
259
240
|
const sentMediaKeys = new Set<string>()
|
|
260
241
|
const getMediaKey = (url: string) => url.split(/[\\/]/).pop() ?? url
|
|
@@ -267,7 +248,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
267
248
|
accountId: account.accountId
|
|
268
249
|
})
|
|
269
250
|
|
|
270
|
-
const { dispatcher, replyOptions, markDispatchIdle
|
|
251
|
+
const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
|
|
271
252
|
responsePrefix: prefixContext.responsePrefix,
|
|
272
253
|
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
273
254
|
humanDelay: core.channel.reply.resolveHumanDelayConfig(config, route.agentId),
|
|
@@ -283,7 +264,8 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
283
264
|
}
|
|
284
265
|
},
|
|
285
266
|
onError: (err: unknown, info: { kind: string }) => {
|
|
286
|
-
|
|
267
|
+
setMsgStatus(effectiveSessionKey, 'finished')
|
|
268
|
+
sendFinal(outboundCtx, 'error')
|
|
287
269
|
dcgLogger(`${info.kind} reply failed: ${String(err)}`, 'error')
|
|
288
270
|
activeRunIdBySessionKey.delete(effectiveSessionKey)
|
|
289
271
|
streamChunkIdxBySessionKey.delete(effectiveSessionKey)
|
|
@@ -293,7 +275,9 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
293
275
|
}
|
|
294
276
|
dcgLogger(`${info.kind} reply failed: ${String(err)}`, 'error')
|
|
295
277
|
},
|
|
296
|
-
onIdle: () => {
|
|
278
|
+
onIdle: () => {
|
|
279
|
+
typingCallbacks.onIdle?.()
|
|
280
|
+
}
|
|
297
281
|
})
|
|
298
282
|
|
|
299
283
|
try {
|
|
@@ -303,42 +287,78 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
303
287
|
}
|
|
304
288
|
|
|
305
289
|
if (systemCommand.includes(text?.trim())) {
|
|
306
|
-
dcgLogger(`dispatching
|
|
307
|
-
await core.channel.reply.
|
|
308
|
-
ctx: ctxPayload,
|
|
309
|
-
cfg: config,
|
|
290
|
+
dcgLogger(`dispatching ${text?.trim()}`)
|
|
291
|
+
await core.channel.reply.withReplyDispatcher({
|
|
310
292
|
dispatcher,
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
293
|
+
onSettled: () => markDispatchIdle(),
|
|
294
|
+
run: () =>
|
|
295
|
+
core.channel.reply.dispatchReplyFromConfig({
|
|
296
|
+
ctx: ctxPayload,
|
|
297
|
+
cfg: config,
|
|
298
|
+
dispatcher,
|
|
299
|
+
replyOptions: {
|
|
300
|
+
...replyOptions,
|
|
301
|
+
onModelSelected: prefixContext.onModelSelected,
|
|
302
|
+
onAgentRunStart: (runId) => {
|
|
303
|
+
activeRunIdBySessionKey.set(effectiveSessionKey, runId)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
})
|
|
318
307
|
})
|
|
319
308
|
} else if (interruptCommand.includes(text?.trim())) {
|
|
320
309
|
dcgLogger(`interrupt command: ${text}`)
|
|
321
|
-
|
|
310
|
+
sendFinal(outboundCtx, 'abort')
|
|
322
311
|
sendText('会话已终止', outboundCtx)
|
|
323
312
|
sessionStreamSuppressed.add(effectiveSessionKey)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
method: 'chat.abort',
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
313
|
+
|
|
314
|
+
const abortOneSession = async (sessionKey: string) => {
|
|
315
|
+
try {
|
|
316
|
+
await sendGatewayRpc({ method: 'chat.abort', params: { sessionKey } })
|
|
317
|
+
} catch (e) {
|
|
318
|
+
dcgLogger(`chat.abort ${sessionKey}: ${String(e)}`, 'error')
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const keysToAbort = new Set<string>(getChildSessionKeysTrackedForRequester(effectiveSessionKey))
|
|
323
|
+
try {
|
|
324
|
+
const listed = await sendGatewayRpc<{ sessions?: Array<{ key?: string }> }>({
|
|
325
|
+
method: 'sessions.list',
|
|
326
|
+
params: { spawnedBy: effectiveSessionKey, limit: 256 }
|
|
332
327
|
})
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
328
|
+
for (const s of listed?.sessions ?? []) {
|
|
329
|
+
const k = typeof s?.key === 'string' ? s.key.trim() : ''
|
|
330
|
+
if (k) keysToAbort.add(k)
|
|
331
|
+
}
|
|
332
|
+
} catch (e) {
|
|
333
|
+
dcgLogger(`sessions.list spawnedBy: ${String(e)}`, 'error')
|
|
334
|
+
}
|
|
335
|
+
for (const sk of keysToAbort) {
|
|
336
|
+
await abortOneSession(sk)
|
|
337
|
+
}
|
|
338
|
+
await abortOneSession(effectiveSessionKey)
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
await sendGatewayRpc({
|
|
342
|
+
method: 'sessions.reset',
|
|
343
|
+
params: { key: effectiveSessionKey, reason: 'reset' }
|
|
344
|
+
})
|
|
345
|
+
} catch (e) {
|
|
346
|
+
dcgLogger(`sessions.reset: ${String(e)}`, 'error')
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
activeRunIdBySessionKey.delete(effectiveSessionKey)
|
|
350
|
+
streamChunkIdxBySessionKey.delete(effectiveSessionKey)
|
|
351
|
+
resetSubagentStateForRequesterSession(effectiveSessionKey)
|
|
352
|
+
setMsgStatus(effectiveSessionKey, 'finished')
|
|
353
|
+
clearSentMediaKeys(msg.content.message_id)
|
|
354
|
+
clearParamsMessage(effectiveSessionKey)
|
|
355
|
+
clearParamsMessage(userId)
|
|
356
|
+
|
|
357
|
+
sendFinal(outboundCtx, 'stop')
|
|
336
358
|
return
|
|
337
359
|
} else {
|
|
338
|
-
dcgLogger(`dispatching to agent (session=${route.sessionKey})`)
|
|
339
360
|
const params = getEffectiveMsgParams(effectiveSessionKey)
|
|
340
361
|
if (!ignoreToolCommand.includes(text?.trim())) {
|
|
341
|
-
// message_received 没有 sessionKey 前置到bot中执行
|
|
342
362
|
wsSendRaw(params, {
|
|
343
363
|
is_finish: -1,
|
|
344
364
|
tool_call_id: Date.now().toString(),
|
|
@@ -353,70 +373,58 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
353
373
|
response: ''
|
|
354
374
|
})
|
|
355
375
|
}
|
|
356
|
-
await core.channel.reply.
|
|
357
|
-
ctx: ctxPayload,
|
|
358
|
-
cfg: config,
|
|
376
|
+
await core.channel.reply.withReplyDispatcher({
|
|
359
377
|
dispatcher,
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
378
|
+
onSettled: () => markDispatchIdle(),
|
|
379
|
+
run: () =>
|
|
380
|
+
core.channel.reply.dispatchReplyFromConfig({
|
|
381
|
+
ctx: ctxPayload,
|
|
382
|
+
cfg: config,
|
|
383
|
+
dispatcher,
|
|
384
|
+
replyOptions: {
|
|
385
|
+
...replyOptions,
|
|
386
|
+
onModelSelected: prefixContext.onModelSelected,
|
|
387
|
+
onAgentRunStart: (runId) => {
|
|
388
|
+
activeRunIdBySessionKey.set(effectiveSessionKey, runId)
|
|
389
|
+
},
|
|
390
|
+
onPartialReply: async (payload: ReplyPayload) => {
|
|
391
|
+
if (sessionStreamSuppressed.has(effectiveSessionKey)) return
|
|
392
|
+
|
|
393
|
+
if (payload.text) {
|
|
394
|
+
completeText = payload.text
|
|
395
|
+
}
|
|
396
|
+
// --- Streaming text chunks ---
|
|
397
|
+
if (payload.text) {
|
|
398
|
+
const delta = payload.text.startsWith(completeText.slice(0, streamedTextLen))
|
|
399
|
+
? payload.text.slice(streamedTextLen)
|
|
400
|
+
: payload.text
|
|
401
|
+
if (delta.trim()) {
|
|
402
|
+
const prev = streamChunkIdxBySessionKey.get(effectiveSessionKey) ?? 0
|
|
403
|
+
streamChunkIdxBySessionKey.set(effectiveSessionKey, prev + 1)
|
|
404
|
+
sendChunk(delta, outboundCtx, prev)
|
|
405
|
+
dcgLogger(`[stream]: chunkIdx=${prev} len=${delta.length} user=${msg._userId} ${delta.slice(0, 100)}`)
|
|
406
|
+
}
|
|
407
|
+
streamedTextLen = payload.text.length
|
|
408
|
+
} else {
|
|
409
|
+
dcgLogger(`onPartialReply no text: ${JSON.stringify(payload)}`, 'error')
|
|
410
|
+
}
|
|
411
|
+
// --- Media from payload ---
|
|
412
|
+
const mediaList = resolveReplyMediaList(payload)
|
|
413
|
+
for (const mediaUrl of mediaList) {
|
|
414
|
+
const key = getMediaKey(mediaUrl)
|
|
415
|
+
if (sentMediaKeys.has(key)) continue
|
|
416
|
+
sentMediaKeys.add(key)
|
|
417
|
+
await sendDcgchatMedia({ sessionKey: effectiveSessionKey, mediaUrl, text: '' })
|
|
418
|
+
}
|
|
384
419
|
}
|
|
385
|
-
streamedTextLen = payload.text.length
|
|
386
|
-
}
|
|
387
|
-
// --- Media from payload ---
|
|
388
|
-
const mediaList = resolveReplyMediaList(payload)
|
|
389
|
-
for (const mediaUrl of mediaList) {
|
|
390
|
-
const key = getMediaKey(mediaUrl)
|
|
391
|
-
if (sentMediaKeys.has(key)) continue
|
|
392
|
-
sentMediaKeys.add(key)
|
|
393
|
-
await sendDcgchatMedia({ sessionKey: effectiveSessionKey, mediaUrl, text: '' })
|
|
394
420
|
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
421
|
+
})
|
|
397
422
|
})
|
|
398
423
|
}
|
|
399
424
|
} catch (err: unknown) {
|
|
400
|
-
|
|
401
|
-
// wasAborted = true
|
|
402
|
-
// dcgLogger(` generation aborted for conversationId=${conversationId}`)
|
|
403
|
-
// } else if (err instanceof Error && err.name === 'AbortError') {
|
|
404
|
-
// wasAborted = true
|
|
405
|
-
// dcgLogger(` generation aborted for conversationId=${conversationId}`)
|
|
406
|
-
// } else {
|
|
407
|
-
// dcgLogger(` dispatchReplyFromConfig error: ${String(err)}`, 'error')
|
|
408
|
-
// }
|
|
409
|
-
} finally {
|
|
410
|
-
// if (activeGenerations.get(conversationId) === genCtrl) {
|
|
411
|
-
// activeGenerations.delete(conversationId)
|
|
412
|
-
// }
|
|
413
|
-
}
|
|
414
|
-
try {
|
|
415
|
-
markRunComplete()
|
|
416
|
-
markDispatchIdle()
|
|
417
|
-
} catch (err) {
|
|
418
|
-
dcgLogger(` markRunComplete||markRunComplete error: ${String(err)}`, 'error')
|
|
425
|
+
dcgLogger(` dispatchReplyFromConfig error: ${String(err)}`, 'error')
|
|
419
426
|
}
|
|
427
|
+
|
|
420
428
|
if (![...systemCommand, ...interruptCommand].includes(text?.trim())) {
|
|
421
429
|
if (sessionStreamSuppressed.has(effectiveSessionKey)) {
|
|
422
430
|
sessionStreamSuppressed.delete(effectiveSessionKey)
|
|
@@ -443,11 +451,10 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
443
451
|
}
|
|
444
452
|
}
|
|
445
453
|
}
|
|
446
|
-
safeSendFinal('end')
|
|
447
454
|
clearSentMediaKeys(msg.content.message_id)
|
|
448
|
-
|
|
449
|
-
// Record session metadata
|
|
450
455
|
const storePath = core.channel.session.resolveStorePath(config.session?.store)
|
|
456
|
+
await waitUntilSubagentsIdle(effectiveSessionKey, { timeoutMs: 600_000 })
|
|
457
|
+
sendFinal(outboundCtx, 'end')
|
|
451
458
|
dcgLogger(
|
|
452
459
|
`record session route: rawTarget=${userId}, normalizedTarget=${effectiveSessionKey}, updateLastRoute.to=${effectiveSessionKey}, accountId=${route.accountId}`
|
|
453
460
|
)
|
|
@@ -456,7 +463,6 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
456
463
|
storePath,
|
|
457
464
|
sessionKey: effectiveSessionKey,
|
|
458
465
|
ctx: ctxPayload,
|
|
459
|
-
// 与 Telegram/Discord 等一致:写入 deliveryContext.to,否则投递可能回退为 From(userId),channel sendMedia 里 ctx.to 会变成数字 userId
|
|
460
466
|
updateLastRoute: {
|
|
461
467
|
sessionKey: effectiveSessionKey,
|
|
462
468
|
channel: "dcgchat-test",
|
|
@@ -473,7 +479,5 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
473
479
|
} catch (err) {
|
|
474
480
|
dcgLogger(` handle message failed: ${String(err)}`, 'error')
|
|
475
481
|
sendError(err instanceof Error ? err.message : String(err), outboundCtx)
|
|
476
|
-
} finally {
|
|
477
|
-
safeSendFinal('finally')
|
|
478
482
|
}
|
|
479
483
|
}
|
package/src/channel.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import type { ChannelPlugin, OpenClawConfig } from 'openclaw/plugin-sdk'
|
|
2
|
-
import { DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk'
|
|
1
|
+
import type { ChannelPlugin, OpenClawConfig, PluginRuntime } from 'openclaw/plugin-sdk'
|
|
2
|
+
import { createPluginRuntimeStore, DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk'
|
|
3
3
|
import type { ResolvedDcgchatAccount, DcgchatConfig } from './types.js'
|
|
4
4
|
import { ossUpload } from './request/oss.js'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
addSentMediaKey,
|
|
7
|
+
getCronMessageId,
|
|
8
|
+
getDcgchatRuntime,
|
|
9
|
+
getInfoBySessionKey,
|
|
10
|
+
getOpenClawConfig,
|
|
11
|
+
hasSentMediaKey
|
|
12
|
+
} from './utils/global.js'
|
|
6
13
|
import { isWsOpen, mergeDefaultParams, mergeSessionParams, sendFinal, wsSendRaw } from './transport.js'
|
|
7
14
|
import { dcgLogger, setLogger } from './utils/log.js'
|
|
8
15
|
import { getOutboundMsgParams, getParamsMessage } from './utils/params.js'
|
|
@@ -116,7 +123,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
116
123
|
uiHints: {
|
|
117
124
|
userId: {
|
|
118
125
|
label: 'WS 连接 _userId',
|
|
119
|
-
help: '仅用于拼接网关 WebSocket URL 的查询参数,不是 Agent 发消息时的 target。发消息请使用 effectiveSessionKey(与入站上下文 SessionKey 相同,格式如 agent:main:mobook:direct
|
|
126
|
+
help: '仅用于拼接网关 WebSocket URL 的查询参数,不是 Agent 发消息时的 target。发消息请使用 effectiveSessionKey(与入站上下文 SessionKey 相同,格式如 agent:main:mobook:direct:<agent_id>:<session_id>)。'
|
|
120
127
|
}
|
|
121
128
|
}
|
|
122
129
|
},
|
|
@@ -155,8 +162,9 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
155
162
|
},
|
|
156
163
|
agentPrompt: {
|
|
157
164
|
messageToolHints: () => [
|
|
158
|
-
'
|
|
159
|
-
'
|
|
165
|
+
'生成文件后,**尽可能不要**把文件路径、地址直接告诉用户。',
|
|
166
|
+
'生成文件后,把文件名告诉用户。',
|
|
167
|
+
'生成文件后,必须调用 message 工具发送文件,不可以直接在文本回复里包含文件路径、文件名、地址。'
|
|
160
168
|
]
|
|
161
169
|
},
|
|
162
170
|
outbound: {
|
|
@@ -167,6 +175,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
167
175
|
}
|
|
168
176
|
return { ok: true, to: to }
|
|
169
177
|
},
|
|
178
|
+
chunker: (text, limit) => getDcgchatRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
170
179
|
textChunkLimit: 4000,
|
|
171
180
|
sendText: async (ctx) => {
|
|
172
181
|
dcgLogger(`channel sendText to ${ctx.to} `)
|
package/src/gateway/index.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { WebSocket } from 'ws'
|
|
|
3
3
|
import crypto from 'crypto'
|
|
4
4
|
import { deriveDeviceIdFromPublicKey, publicKeyRawBase64UrlFromPem, buildDeviceAuthPayloadV3, signDevicePayload } from './security.js'
|
|
5
5
|
import { dcgLogger } from '../utils/log.js'
|
|
6
|
-
import { finishedDcgchatCron, sendDcgchatCron } from '../cron.js'
|
|
7
6
|
import { getWorkspaceDir } from '../utils/global.js'
|
|
7
|
+
import { handleGatewayEventMessage } from '../utils/gatewayMsgHanlder.js'
|
|
8
8
|
|
|
9
9
|
export interface GatewayEvent {
|
|
10
10
|
type: string
|
|
@@ -358,31 +358,7 @@ export class GatewayConnection {
|
|
|
358
358
|
}
|
|
359
359
|
|
|
360
360
|
if (msg.type === 'event') {
|
|
361
|
-
|
|
362
|
-
// 定时任务相关事件
|
|
363
|
-
if (msg.event === 'cron') {
|
|
364
|
-
dcgLogger(`[Gateway] 收到事件: ${JSON.stringify(msg)}`)
|
|
365
|
-
if (msg.payload?.action === 'added') {
|
|
366
|
-
sendDcgchatCron(msg.payload?.jobId)
|
|
367
|
-
}
|
|
368
|
-
if (msg.payload?.action === 'updated') {
|
|
369
|
-
sendDcgchatCron(msg.payload?.jobId as string)
|
|
370
|
-
}
|
|
371
|
-
if (msg.payload?.action === 'removed') {
|
|
372
|
-
sendDcgchatCron(msg.payload?.jobId as string)
|
|
373
|
-
}
|
|
374
|
-
if (msg.payload?.action === 'finished') {
|
|
375
|
-
finishedDcgchatCron(msg.payload?.jobId as string)
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
} catch (error) {
|
|
379
|
-
dcgLogger(`[Gateway] 处理事件失败: ${error}`, 'error')
|
|
380
|
-
}
|
|
381
|
-
const event: GatewayEvent = {
|
|
382
|
-
type: msg.event as string,
|
|
383
|
-
payload: msg.payload as Record<string, unknown> | undefined,
|
|
384
|
-
seq: msg.seq as number | undefined
|
|
385
|
-
}
|
|
361
|
+
const event = handleGatewayEventMessage(msg)
|
|
386
362
|
this.eventHandlers.forEach((h) => h(event))
|
|
387
363
|
}
|
|
388
364
|
}
|
package/src/monitor.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import type { ClawdbotConfig, RuntimeEnv } from 'openclaw/plugin-sdk'
|
|
2
2
|
import WebSocket from 'ws'
|
|
3
|
-
import { handleDcgchatMessage } from './bot.js'
|
|
4
3
|
import { resolveAccount } from './channel.js'
|
|
5
|
-
import { setWsConnection, getOpenClawConfig
|
|
6
|
-
import type { InboundMessage } from './types.js'
|
|
7
|
-
import { installSkill, uninstallSkill } from './skill.js'
|
|
4
|
+
import { setWsConnection, getOpenClawConfig } from './utils/global.js'
|
|
8
5
|
import { dcgLogger } from './utils/log.js'
|
|
9
|
-
import { onDisabledCronJob, onEnabledCronJob, onRemoveCronJob, onRunCronJob } from './cron.js'
|
|
10
|
-
import { ignoreToolCommand } from './utils/constant.js'
|
|
11
6
|
import { isWsOpen } from './transport.js'
|
|
12
|
-
import {
|
|
7
|
+
import { handleParsedWsMessage } from './utils/wsMessageHandler.js'
|
|
13
8
|
|
|
14
9
|
export type MonitorDcgchatOpts = {
|
|
15
10
|
config?: ClawdbotConfig
|
|
@@ -129,50 +124,7 @@ export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<
|
|
|
129
124
|
return
|
|
130
125
|
}
|
|
131
126
|
|
|
132
|
-
|
|
133
|
-
const msg = parsed as unknown as InboundMessage
|
|
134
|
-
// 与 monitor 原逻辑一致:工具类指令不进入 running,避免误触工具链监控
|
|
135
|
-
const effectiveSessionKey = getSessionKey(msg.content, account.accountId)
|
|
136
|
-
if (!ignoreToolCommand.includes(msg.content.text?.trim() ?? '')) {
|
|
137
|
-
setMsgStatus(effectiveSessionKey, 'running')
|
|
138
|
-
} else {
|
|
139
|
-
setMsgStatus(effectiveSessionKey, 'finished')
|
|
140
|
-
}
|
|
141
|
-
await handleDcgchatMessage(msg, account.accountId)
|
|
142
|
-
} else if (parsed.messageType == 'openclaw_bot_event') {
|
|
143
|
-
const { event_type, operation_type } = parsed.content ? parsed.content : ({} as Record<string, any>)
|
|
144
|
-
if (event_type === 'skill') {
|
|
145
|
-
const { skill_url, skill_code, skill_id, bot_token, websocket_trace_id } = parsed.content
|
|
146
|
-
const content = { event_type, operation_type, skill_url, skill_code, skill_id, bot_token, websocket_trace_id }
|
|
147
|
-
if (operation_type === 'install' || operation_type === 'enable' || operation_type === 'update') {
|
|
148
|
-
installSkill({ path: skill_url, code: skill_code }, content)
|
|
149
|
-
} else if (operation_type === 'remove' || operation_type === 'disable') {
|
|
150
|
-
uninstallSkill({ code: skill_code }, content)
|
|
151
|
-
} else {
|
|
152
|
-
dcgLogger(`openclaw_bot_event unknown event_type: ${event_type}, ${data.toString()}`)
|
|
153
|
-
}
|
|
154
|
-
} else if (event_type === 'cron') {
|
|
155
|
-
const { job_id, message_id } = parsed.content
|
|
156
|
-
if (operation_type === 'remove') {
|
|
157
|
-
await onRemoveCronJob(job_id)
|
|
158
|
-
} else if (operation_type === 'enable') {
|
|
159
|
-
await onEnabledCronJob(job_id)
|
|
160
|
-
} else if (operation_type === 'disable') {
|
|
161
|
-
await onDisabledCronJob(job_id)
|
|
162
|
-
} else if (operation_type === 'run') {
|
|
163
|
-
await onRunCronJob(job_id, message_id)
|
|
164
|
-
}
|
|
165
|
-
} else if (event_type === 'session') {
|
|
166
|
-
const { agent_id, session_id, agent_clone_code } = parsed.content
|
|
167
|
-
if (operation_type === 'remove') {
|
|
168
|
-
await onRemoveSession({ agent_id, session_id, agent_clone_code, account_id: accountId })
|
|
169
|
-
}
|
|
170
|
-
} else {
|
|
171
|
-
dcgLogger(`openclaw_bot_event unknown operation_type: ${operation_type}, ${data.toString()}`)
|
|
172
|
-
}
|
|
173
|
-
} else {
|
|
174
|
-
dcgLogger(`ignoring unknown messageType: ${parsed.messageType}`, 'error')
|
|
175
|
-
}
|
|
127
|
+
await handleParsedWsMessage(parsed, payloadStr, account.accountId)
|
|
176
128
|
})
|
|
177
129
|
|
|
178
130
|
ws.on('close', (code, reason) => {
|
package/src/skill.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { getWsConnection } from './utils/global.js'
|
|
|
9
9
|
import { dcgLogger } from './utils/log.js'
|
|
10
10
|
import { isWsOpen } from './transport.js'
|
|
11
11
|
import { sendMessageToGateway } from './gateway/socket.js'
|
|
12
|
+
import { decodeZipEntryPath } from './utils/zipPath.js'
|
|
12
13
|
|
|
13
14
|
type ISkillParams = {
|
|
14
15
|
path: string
|
|
@@ -69,13 +70,7 @@ export async function installSkill(params: ISkillParams, msgContent: Record<stri
|
|
|
69
70
|
}
|
|
70
71
|
try {
|
|
71
72
|
const flags = entry.props?.flags ?? 0
|
|
72
|
-
const
|
|
73
|
-
let entryPath: string
|
|
74
|
-
if (!isUtf8 && entry.props?.pathBuffer) {
|
|
75
|
-
entryPath = new TextDecoder('gbk').decode(entry.props.pathBuffer)
|
|
76
|
-
} else {
|
|
77
|
-
entryPath = entry.path
|
|
78
|
-
}
|
|
73
|
+
const entryPath = decodeZipEntryPath(entry.props?.pathBuffer, flags, entry.path)
|
|
79
74
|
const pathParts = entryPath.split('/')
|
|
80
75
|
|
|
81
76
|
// 检测根目录
|
package/src/tool.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk'
|
|
|
2
2
|
import { getMsgStatus } from './utils/global.js'
|
|
3
3
|
import { dcgLogger } from './utils/log.js'
|
|
4
4
|
import { sendFinal, sendText, wsSendRaw } from './transport.js'
|
|
5
|
-
import { getEffectiveMsgParams } from './utils/params.js'
|
|
5
|
+
import { getEffectiveMsgParams, deleteSessionKeyBySubAgentRunId, setSessionKeyBySubAgentRunId } from './utils/params.js'
|
|
6
6
|
import { cronToolCall } from './cronToolCall.js'
|
|
7
7
|
|
|
8
8
|
type PluginHookName =
|
|
@@ -68,10 +68,244 @@ interface CronDelivery {
|
|
|
68
68
|
[key: string]: unknown
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// --- Subagent 活跃跟踪(按主会话 requesterSessionKey)---
|
|
72
|
+
|
|
73
|
+
/** 主会话 sessionKey -> 仍活跃的子 agent runId */
|
|
74
|
+
const activeSubagentRunIdsByRequester = new Map<string, Set<string>>()
|
|
75
|
+
/** 子会话 childSessionKey -> 主会话 requesterSessionKey */
|
|
76
|
+
const requesterByChildSessionKey = new Map<string, string>()
|
|
77
|
+
/** 子会话 childSessionKey -> spawn 时的 runId(ended 事件可能不带 runId) */
|
|
78
|
+
const runIdByChildSessionKey = new Map<string, string>()
|
|
79
|
+
/** 主会话 -> 等待「子 agent 全部结束」的回调 */
|
|
80
|
+
const subagentIdleWaiters = new Map<string, Set<() => void>>()
|
|
81
|
+
|
|
82
|
+
function getOrCreateRunIdSet(requesterSessionKey: string): Set<string> {
|
|
83
|
+
let set = activeSubagentRunIdsByRequester.get(requesterSessionKey)
|
|
84
|
+
if (!set) {
|
|
85
|
+
set = new Set()
|
|
86
|
+
activeSubagentRunIdsByRequester.set(requesterSessionKey, set)
|
|
87
|
+
}
|
|
88
|
+
return set
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function flushSubagentIdleWaiters(requesterSessionKey: string): void {
|
|
92
|
+
const set = activeSubagentRunIdsByRequester.get(requesterSessionKey)
|
|
93
|
+
if (set && set.size > 0) return
|
|
94
|
+
activeSubagentRunIdsByRequester.delete(requesterSessionKey)
|
|
95
|
+
const waiters = subagentIdleWaiters.get(requesterSessionKey)
|
|
96
|
+
if (!waiters?.size) return
|
|
97
|
+
subagentIdleWaiters.delete(requesterSessionKey)
|
|
98
|
+
for (const w of waiters) {
|
|
99
|
+
try {
|
|
100
|
+
w()
|
|
101
|
+
} catch (e) {
|
|
102
|
+
dcgLogger(`subagent idle waiter error: ${String(e)}`, 'error')
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function registerSubagentSpawn(requesterSessionKey: string, runId: string, childSessionKey: string): void {
|
|
108
|
+
const req = requesterSessionKey.trim()
|
|
109
|
+
const rid = runId.trim()
|
|
110
|
+
const child = childSessionKey.trim()
|
|
111
|
+
if (!req || !rid || !child) {
|
|
112
|
+
dcgLogger(`subagent track spawn skipped: missing key req=${req} runId=${rid} child=${child}`)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
getOrCreateRunIdSet(req).add(rid)
|
|
116
|
+
requesterByChildSessionKey.set(child, req)
|
|
117
|
+
runIdByChildSessionKey.set(child, rid)
|
|
118
|
+
dcgLogger(`subagent track spawn: requester=${req} runId=${rid} child=${child} active=${getOrCreateRunIdSet(req).size}`)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function registerSubagentEnd(
|
|
122
|
+
ctx: { requesterSessionKey?: string; sessionKey?: string },
|
|
123
|
+
targetSessionKey: string,
|
|
124
|
+
runId?: string
|
|
125
|
+
): void {
|
|
126
|
+
const child = targetSessionKey.trim()
|
|
127
|
+
if (!child) return
|
|
128
|
+
const req = ctx.requesterSessionKey?.trim() || requesterByChildSessionKey.get(child) || ctx.sessionKey?.trim() || ''
|
|
129
|
+
const resolvedRunId = (runId?.trim() || runIdByChildSessionKey.get(child) || '').trim()
|
|
130
|
+
deleteSessionKeyBySubAgentRunId(resolvedRunId)
|
|
131
|
+
if (!req) {
|
|
132
|
+
dcgLogger(`subagent track end: no requester for child=${child} runId=${resolvedRunId}`)
|
|
133
|
+
requesterByChildSessionKey.delete(child)
|
|
134
|
+
runIdByChildSessionKey.delete(child)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
const set = activeSubagentRunIdsByRequester.get(req)
|
|
138
|
+
if (set && resolvedRunId) {
|
|
139
|
+
set.delete(resolvedRunId)
|
|
140
|
+
}
|
|
141
|
+
requesterByChildSessionKey.delete(child)
|
|
142
|
+
runIdByChildSessionKey.delete(child)
|
|
143
|
+
dcgLogger(`subagent track end: requester=${req} runId=${resolvedRunId || 'n/a'} remaining=${set?.size ?? 0}`)
|
|
144
|
+
if (set && set.size === 0) {
|
|
145
|
+
activeSubagentRunIdsByRequester.delete(req)
|
|
146
|
+
}
|
|
147
|
+
flushSubagentIdleWaiters(req)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 当前跟踪到的、挂在该主会话下的子会话 sessionKey(供 /stop 时逐个 chat.abort) */
|
|
151
|
+
export function getChildSessionKeysTrackedForRequester(requesterSessionKey: string): string[] {
|
|
152
|
+
const req = requesterSessionKey.trim()
|
|
153
|
+
if (!req) return []
|
|
154
|
+
const out: string[] = []
|
|
155
|
+
for (const [child, parent] of requesterByChildSessionKey.entries()) {
|
|
156
|
+
if (parent === req) out.push(child)
|
|
157
|
+
}
|
|
158
|
+
return out
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 打断后清空本地子 agent 跟踪(runId、父子映射、子 runId→sessionKey),并唤醒 waitUntilSubagentsIdle,避免永久挂起。
|
|
163
|
+
*/
|
|
164
|
+
export function resetSubagentStateForRequesterSession(requesterSessionKey: string): void {
|
|
165
|
+
const req = requesterSessionKey.trim()
|
|
166
|
+
if (!req) return
|
|
167
|
+
|
|
168
|
+
const runIdSet = activeSubagentRunIdsByRequester.get(req)
|
|
169
|
+
if (runIdSet) {
|
|
170
|
+
for (const rid of runIdSet) {
|
|
171
|
+
deleteSessionKeyBySubAgentRunId(rid)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const [child, parent] of [...requesterByChildSessionKey.entries()]) {
|
|
176
|
+
if (parent === req) {
|
|
177
|
+
requesterByChildSessionKey.delete(child)
|
|
178
|
+
runIdByChildSessionKey.delete(child)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
activeSubagentRunIdsByRequester.delete(req)
|
|
183
|
+
|
|
184
|
+
const waiters = subagentIdleWaiters.get(req)
|
|
185
|
+
if (!waiters?.size) return
|
|
186
|
+
subagentIdleWaiters.delete(req)
|
|
187
|
+
for (const w of waiters) {
|
|
188
|
+
try {
|
|
189
|
+
w()
|
|
190
|
+
} catch (e) {
|
|
191
|
+
dcgLogger(`subagent idle waiter error: ${String(e)}`, 'error')
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** 当前主会话下仍在跑的子 agent 数量(按 spawn 时 runId 去重) */
|
|
197
|
+
export function getActiveSubagentCount(sessionKey: string): number {
|
|
198
|
+
const sk = sessionKey?.trim()
|
|
199
|
+
if (!sk) return 0
|
|
200
|
+
return activeSubagentRunIdsByRequester.get(sk)?.size ?? 0
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 等到指定主会话下已跟踪的子 agent 全部结束(spawn 失败等路径也会触发 subagent_ended,一般会配对清理)。
|
|
205
|
+
* 注意:须在收到 `subagent_spawned` 之后才会计入;仅 spawning 未 spawned 的不会阻塞。
|
|
206
|
+
*/
|
|
207
|
+
export function waitUntilSubagentsIdle(sessionKey: string, opts?: { timeoutMs?: number; signal?: AbortSignal }): Promise<void> {
|
|
208
|
+
console.log('🚀 ~ waitUntilSubagentsIdle ~ sessionKey:', sessionKey)
|
|
209
|
+
const sk = sessionKey?.trim()
|
|
210
|
+
if (!sk) return Promise.resolve()
|
|
211
|
+
|
|
212
|
+
if (getActiveSubagentCount(sk) === 0) return Promise.resolve()
|
|
213
|
+
|
|
214
|
+
return new Promise<void>((resolve, reject) => {
|
|
215
|
+
let settled = false
|
|
216
|
+
const finish = (fn: () => void) => {
|
|
217
|
+
if (settled) return
|
|
218
|
+
settled = true
|
|
219
|
+
if (timeoutId) clearTimeout(timeoutId)
|
|
220
|
+
opts?.signal?.removeEventListener('abort', onAbort)
|
|
221
|
+
removeWaiter()
|
|
222
|
+
fn()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const removeWaiter = () => {
|
|
226
|
+
const bucket = subagentIdleWaiters.get(sk)
|
|
227
|
+
if (!bucket) return
|
|
228
|
+
bucket.delete(onIdle)
|
|
229
|
+
if (bucket.size === 0) subagentIdleWaiters.delete(sk)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const onIdle = () => finish(() => resolve())
|
|
233
|
+
|
|
234
|
+
const onAbort = () => {
|
|
235
|
+
const reason = opts?.signal?.reason
|
|
236
|
+
finish(() => reject(reason instanceof Error ? reason : new Error(String(reason ?? 'Aborted'))))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
|
240
|
+
if (opts?.timeoutMs != null && opts.timeoutMs > 0) {
|
|
241
|
+
timeoutId = setTimeout(
|
|
242
|
+
() => finish(() => reject(new Error(`waitUntilSubagentsIdle timeout ${opts.timeoutMs}ms`))),
|
|
243
|
+
opts.timeoutMs
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (opts?.signal) {
|
|
248
|
+
if (opts.signal.aborted) {
|
|
249
|
+
onAbort()
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
opts.signal.addEventListener('abort', onAbort, { once: true })
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let set = subagentIdleWaiters.get(sk)
|
|
256
|
+
if (!set) {
|
|
257
|
+
set = new Set()
|
|
258
|
+
subagentIdleWaiters.set(sk, set)
|
|
259
|
+
}
|
|
260
|
+
set.add(onIdle)
|
|
261
|
+
|
|
262
|
+
if (getActiveSubagentCount(sk) === 0) {
|
|
263
|
+
onIdle()
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function resolveHookSessionKey(
|
|
269
|
+
eventName: string,
|
|
270
|
+
args: { sessionKey?: string; requesterSessionKey?: string; runId?: string }
|
|
271
|
+
): string {
|
|
272
|
+
if (
|
|
273
|
+
eventName === 'subagent_spawned' ||
|
|
274
|
+
eventName === 'subagent_ended' ||
|
|
275
|
+
eventName === 'subagent_spawning' ||
|
|
276
|
+
eventName === 'subagent_delivery_target'
|
|
277
|
+
) {
|
|
278
|
+
if (args?.runId && args?.requesterSessionKey) setSessionKeyBySubAgentRunId(args?.runId, args?.requesterSessionKey)
|
|
279
|
+
return (args?.requesterSessionKey || args?.sessionKey || '').trim()
|
|
280
|
+
}
|
|
281
|
+
return (args?.sessionKey || '').trim()
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function trackSubagentLifecycle(eventName: string, event: any, args: any): void {
|
|
285
|
+
if (eventName === 'subagent_spawned') {
|
|
286
|
+
const runId = typeof event?.runId === 'string' ? event.runId : ''
|
|
287
|
+
const childSessionKey = typeof event?.childSessionKey === 'string' ? event.childSessionKey : ''
|
|
288
|
+
const requester =
|
|
289
|
+
typeof args?.requesterSessionKey === 'string'
|
|
290
|
+
? args.requesterSessionKey
|
|
291
|
+
: typeof args?.sessionKey === 'string'
|
|
292
|
+
? args.sessionKey
|
|
293
|
+
: ''
|
|
294
|
+
registerSubagentSpawn(requester, runId, childSessionKey)
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
if (eventName === 'subagent_ended') {
|
|
298
|
+
const targetSessionKey = typeof event?.targetSessionKey === 'string' ? event.targetSessionKey : ''
|
|
299
|
+
const runId = typeof event?.runId === 'string' ? event.runId : undefined
|
|
300
|
+
registerSubagentEnd(args ?? {}, targetSessionKey, runId)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
71
304
|
export function monitoringToolMessage(api: OpenClawPluginApi) {
|
|
72
305
|
for (const item of eventList) {
|
|
73
306
|
api.on(item.event as PluginHookName, (event: any, args: any) => {
|
|
74
|
-
|
|
307
|
+
trackSubagentLifecycle(item.event, event, args)
|
|
308
|
+
const sk = resolveHookSessionKey(item.event, args ?? {})
|
|
75
309
|
if (sk) {
|
|
76
310
|
const status = getMsgStatus(sk)
|
|
77
311
|
if (status === 'running') {
|
package/src/types.ts
CHANGED
package/src/utils/constant.ts
CHANGED
|
@@ -2,6 +2,6 @@ export const ENV: 'production' | 'test' | 'develop' = 'test'
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
export const systemCommand = ['/new', '/status']
|
|
5
|
-
export const interruptCommand = ['
|
|
5
|
+
export const interruptCommand = ['/stop']
|
|
6
6
|
|
|
7
|
-
export const ignoreToolCommand = ['/search', '/abort', '/
|
|
7
|
+
export const ignoreToolCommand = ['/search', '/abort', '/queue interrupt', ...systemCommand, ...interruptCommand]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { GatewayEvent } from '../gateway/index.js'
|
|
2
|
+
import { finishedDcgchatCron, sendDcgchatCron } from '../cron.js'
|
|
3
|
+
import { dcgLogger } from './log.js'
|
|
4
|
+
import { getEffectiveMsgParams, getSessionKeyBySubAgentRunId } from './params.js'
|
|
5
|
+
import { sendChunk } from '../transport.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 处理网关 event 帧的副作用(agent 流式输出、cron 同步),并构造供上层分发的 GatewayEvent。
|
|
9
|
+
*/
|
|
10
|
+
export function handleGatewayEventMessage(msg: { event?: string; payload?: Record<string, unknown>; seq?: number }): GatewayEvent {
|
|
11
|
+
try {
|
|
12
|
+
if (msg.event === 'agent') {
|
|
13
|
+
dcgLogger(`[Gateway] 收到agent事件: ${JSON.stringify(msg).slice(0, 100)}`)
|
|
14
|
+
const pl = msg.payload as { runId: string; data?: { delta?: unknown } }
|
|
15
|
+
const sessionKey = getSessionKeyBySubAgentRunId(pl.runId)
|
|
16
|
+
const outboundCtx = getEffectiveMsgParams(sessionKey)
|
|
17
|
+
if (outboundCtx.sessionId && pl.data?.delta) sendChunk(pl.data.delta as string, outboundCtx, 0)
|
|
18
|
+
}
|
|
19
|
+
if (msg.event === 'cron') {
|
|
20
|
+
const p = msg.payload
|
|
21
|
+
dcgLogger(`[Gateway] 收到定时任务事件: ${JSON.stringify(p)}`)
|
|
22
|
+
if (p?.action === 'added') {
|
|
23
|
+
sendDcgchatCron(p?.jobId as string)
|
|
24
|
+
}
|
|
25
|
+
if (p?.action === 'updated') {
|
|
26
|
+
sendDcgchatCron(p?.jobId as string)
|
|
27
|
+
}
|
|
28
|
+
if (p?.action === 'removed') {
|
|
29
|
+
sendDcgchatCron(p?.jobId as string)
|
|
30
|
+
}
|
|
31
|
+
if (p?.action === 'finished') {
|
|
32
|
+
finishedDcgchatCron(p?.jobId as string)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
dcgLogger(`[Gateway] 处理事件失败: ${error}`, 'error')
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
type: msg.event as string,
|
|
40
|
+
payload: msg.payload,
|
|
41
|
+
seq: msg.seq as number | undefined
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/utils/global.ts
CHANGED
|
@@ -22,7 +22,7 @@ export function getOpenClawConfig(): OpenClawConfig | null {
|
|
|
22
22
|
return config
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
import type
|
|
25
|
+
import { createPluginRuntimeStore, type OpenClawConfig, type PluginRuntime } from 'openclaw/plugin-sdk'
|
|
26
26
|
import { dcgLogger } from './log.js'
|
|
27
27
|
import { channelInfo, ENV } from './constant.js'
|
|
28
28
|
|
|
@@ -42,7 +42,6 @@ function getWorkspacePath() {
|
|
|
42
42
|
return null
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
let runtime: PluginRuntime | null = null
|
|
46
45
|
let workspaceDir: string = getWorkspacePath()
|
|
47
46
|
|
|
48
47
|
export function setWorkspaceDir(dir?: string) {
|
|
@@ -56,17 +55,10 @@ export function getWorkspaceDir(): string {
|
|
|
56
55
|
}
|
|
57
56
|
return workspaceDir
|
|
58
57
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function getDcgchatRuntime(): PluginRuntime {
|
|
65
|
-
if (!runtime) {
|
|
66
|
-
dcgLogger?.('runtime not initialized', 'error')
|
|
67
|
-
}
|
|
68
|
-
return runtime as PluginRuntime
|
|
69
|
-
}
|
|
58
|
+
const { setRuntime: setDcgchatRuntime, getRuntime: getDcgchatRuntime } = createPluginRuntimeStore<PluginRuntime>(
|
|
59
|
+
`${"dcgchat-test"} runtime not initialized`
|
|
60
|
+
)
|
|
61
|
+
export { setDcgchatRuntime, getDcgchatRuntime }
|
|
70
62
|
|
|
71
63
|
export type MsgSessionStatus = 'running' | 'finished' | ''
|
|
72
64
|
|
|
@@ -117,19 +109,6 @@ export function clearSentMediaKeys(messageId?: string) {
|
|
|
117
109
|
}
|
|
118
110
|
}
|
|
119
111
|
|
|
120
|
-
export const getSessionKey = (content: any, accountId?: string) => {
|
|
121
|
-
const { real_mobook, agent_id, conversation_id, session_id } = content
|
|
122
|
-
const core = getDcgchatRuntime()
|
|
123
|
-
|
|
124
|
-
const route = core.channel.routing.resolveAgentRoute({
|
|
125
|
-
cfg: getOpenClawConfig() as OpenClawConfig,
|
|
126
|
-
channel: "dcgchat-test",
|
|
127
|
-
accountId: accountId || 'default',
|
|
128
|
-
peer: { kind: 'direct', id: session_id }
|
|
129
|
-
})
|
|
130
|
-
return real_mobook == '1' ? route.sessionKey : `agent:main:mobook:direct:${agent_id}:${session_id}`.toLowerCase()
|
|
131
|
-
}
|
|
132
|
-
|
|
133
112
|
const cronMessageIdMap = new Map<string, string>()
|
|
134
113
|
|
|
135
114
|
export function setCronMessageId(sk: string, messageId: string) {
|
|
@@ -144,6 +123,21 @@ export function removeCronMessageId(sk: string) {
|
|
|
144
123
|
cronMessageIdMap.delete(sk)
|
|
145
124
|
}
|
|
146
125
|
|
|
126
|
+
export const getSessionKey = (content: any, accountId: string) => {
|
|
127
|
+
const { real_mobook, agent_id, agent_clone_code, session_id } = content
|
|
128
|
+
const core = getDcgchatRuntime()
|
|
129
|
+
|
|
130
|
+
const anentCode = agent_clone_code || 'main'
|
|
131
|
+
|
|
132
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
133
|
+
cfg: getOpenClawConfig() as OpenClawConfig,
|
|
134
|
+
channel: "dcgchat-test",
|
|
135
|
+
accountId: accountId || 'default',
|
|
136
|
+
peer: { kind: 'direct', id: session_id }
|
|
137
|
+
})
|
|
138
|
+
return real_mobook == '1' ? route.sessionKey : `agent:${anentCode}:mobook:direct:${agent_id}:${session_id}`.toLowerCase()
|
|
139
|
+
}
|
|
140
|
+
|
|
147
141
|
export function getInfoBySessionKey(sk: string): { sessionId: string; agentId: string } {
|
|
148
142
|
const sessionInfo = sk.split(':')
|
|
149
143
|
return { sessionId: sessionInfo.at(-1) ?? '', agentId: sessionInfo.at(-2) ?? '' }
|
package/src/utils/params.ts
CHANGED
|
@@ -69,3 +69,21 @@ export function setParamsMessage(sessionKey: string, params: Partial<IMsgParams>
|
|
|
69
69
|
export function getParamsMessage(sessionKey: string): IMsgParams | undefined {
|
|
70
70
|
return paramsMessageMap.get(sessionKey)
|
|
71
71
|
}
|
|
72
|
+
|
|
73
|
+
export function clearParamsMessage(sessionKey: string): void {
|
|
74
|
+
const k = sessionKey?.trim()
|
|
75
|
+
if (!k) return
|
|
76
|
+
paramsMessageMap.delete(k)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// sessionKey 对应的 子agent的runId
|
|
80
|
+
const subagentRunIdMap = new Map<string, string>()
|
|
81
|
+
export function getSessionKeyBySubAgentRunId(runId: string): string | undefined {
|
|
82
|
+
return subagentRunIdMap.get(runId)
|
|
83
|
+
}
|
|
84
|
+
export function setSessionKeyBySubAgentRunId(runId: string, sessionKey: string) {
|
|
85
|
+
subagentRunIdMap.set(runId, sessionKey)
|
|
86
|
+
}
|
|
87
|
+
export function deleteSessionKeyBySubAgentRunId(runId: string) {
|
|
88
|
+
subagentRunIdMap.delete(runId)
|
|
89
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { handleDcgchatMessage } from '../bot.js'
|
|
2
|
+
import { setMsgStatus, getSessionKey } from './global.js'
|
|
3
|
+
import type { InboundMessage } from '../types.js'
|
|
4
|
+
import { installSkill, uninstallSkill } from '../skill.js'
|
|
5
|
+
import { dcgLogger } from './log.js'
|
|
6
|
+
import { onDisabledCronJob, onEnabledCronJob, onRemoveCronJob, onRunCronJob } from '../cron.js'
|
|
7
|
+
import { ignoreToolCommand } from './constant.js'
|
|
8
|
+
import { createAgent } from '../agent.js'
|
|
9
|
+
|
|
10
|
+
export type ParsedWsPayload = {
|
|
11
|
+
messageType?: string
|
|
12
|
+
content: any
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 处理 WebSocket 已解析 JSON 且 content 已二次 parse 后的业务消息(openclaw_bot_chat / openclaw_bot_event)。
|
|
17
|
+
*/
|
|
18
|
+
export async function handleParsedWsMessage(parsed: ParsedWsPayload, rawPayload: string, accountId: string): Promise<void> {
|
|
19
|
+
if (parsed.messageType == 'openclaw_bot_chat') {
|
|
20
|
+
const msg = parsed as unknown as InboundMessage
|
|
21
|
+
// 与 monitor 原逻辑一致:工具类指令不进入 running,避免误触工具链监控
|
|
22
|
+
const effectiveSessionKey = getSessionKey(msg.content, accountId)
|
|
23
|
+
if (!ignoreToolCommand.includes(msg.content.text?.trim() ?? '')) {
|
|
24
|
+
setMsgStatus(effectiveSessionKey, 'running')
|
|
25
|
+
} else {
|
|
26
|
+
setMsgStatus(effectiveSessionKey, 'finished')
|
|
27
|
+
}
|
|
28
|
+
await handleDcgchatMessage(msg, accountId)
|
|
29
|
+
} else if (parsed.messageType == 'openclaw_bot_event') {
|
|
30
|
+
const { event_type, operation_type } = parsed.content ? parsed.content : ({} as Record<string, any>)
|
|
31
|
+
if (event_type === 'skill') {
|
|
32
|
+
const { skill_url, skill_code, skill_id, bot_token, websocket_trace_id } = parsed.content
|
|
33
|
+
const content = { event_type, operation_type, skill_url, skill_code, skill_id, bot_token, websocket_trace_id }
|
|
34
|
+
if (operation_type === 'install' || operation_type === 'enable' || operation_type === 'update') {
|
|
35
|
+
installSkill({ path: skill_url, code: skill_code }, content)
|
|
36
|
+
} else if (operation_type === 'remove' || operation_type === 'disable') {
|
|
37
|
+
uninstallSkill({ code: skill_code }, content)
|
|
38
|
+
} else {
|
|
39
|
+
dcgLogger(`openclaw_bot_event unknown event_type: ${event_type}, ${rawPayload}`)
|
|
40
|
+
}
|
|
41
|
+
} else if (event_type === 'agent') {
|
|
42
|
+
if (operation_type === 'install') {
|
|
43
|
+
createAgent(parsed.content)
|
|
44
|
+
} else {
|
|
45
|
+
dcgLogger(`openclaw_bot_event unknown event_type: ${event_type}, ${rawPayload}`)
|
|
46
|
+
}
|
|
47
|
+
} else if (event_type === 'cron') {
|
|
48
|
+
const { job_id, message_id } = parsed.content
|
|
49
|
+
if (operation_type === 'remove') {
|
|
50
|
+
await onRemoveCronJob(job_id)
|
|
51
|
+
} else if (operation_type === 'enable') {
|
|
52
|
+
await onEnabledCronJob(job_id)
|
|
53
|
+
} else if (operation_type === 'disable') {
|
|
54
|
+
await onDisabledCronJob(job_id)
|
|
55
|
+
} else if (operation_type === 'run') {
|
|
56
|
+
await onRunCronJob(job_id, message_id)
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
dcgLogger(`openclaw_bot_event unknown operation_type: ${operation_type}, ${rawPayload}`)
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
dcgLogger(`ignoring unknown messageType: ${parsed.messageType}`, 'error')
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZIP 文件名编码:规范要求 UTF-8 时设置 0x800;很多工具未设标志但仍写 UTF-8 字节。
|
|
3
|
+
* 无标志时若一律按 GBK 解码,会得到「鍥句功…」类乱码。先严格 UTF-8,失败再 GBK(兼容 Windows 中文 ZIP)。
|
|
4
|
+
*/
|
|
5
|
+
export function decodeZipEntryPath(
|
|
6
|
+
pathBuffer: Buffer | Uint8Array | undefined,
|
|
7
|
+
flags: number,
|
|
8
|
+
fallbackPath: string
|
|
9
|
+
): string {
|
|
10
|
+
if ((flags & 0x800) !== 0) {
|
|
11
|
+
if (pathBuffer) {
|
|
12
|
+
return new TextDecoder('utf-8').decode(pathBuffer)
|
|
13
|
+
}
|
|
14
|
+
return fallbackPath
|
|
15
|
+
}
|
|
16
|
+
if (pathBuffer && pathBuffer.length > 0) {
|
|
17
|
+
try {
|
|
18
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(pathBuffer)
|
|
19
|
+
} catch {
|
|
20
|
+
return new TextDecoder('gbk').decode(pathBuffer)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return fallbackPath
|
|
24
|
+
}
|