@dcrays/dcgchat 0.3.35 → 0.4.4
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/index.ts +3 -4
- package/package.json +1 -1
- package/src/agent.ts +125 -0
- package/src/bot.ts +161 -182
- package/src/channel.ts +91 -19
- package/src/cron.ts +8 -9
- package/src/gateway/index.ts +2 -26
- package/src/gateway/socket.ts +5 -3
- package/src/monitor.ts +3 -52
- package/src/session.ts +2 -2
- package/src/tool.ts +236 -3
- package/src/tools/messageTool.ts +215 -0
- package/src/transport.ts +10 -8
- package/src/types.ts +7 -0
- package/src/utils/constant.ts +2 -2
- package/src/utils/gatewayMsgHanlder.ts +47 -0
- package/src/utils/global.ts +29 -36
- package/src/utils/params.ts +20 -3
- package/src/utils/wsMessageHandler.ts +64 -0
- package/src/utils/zipExtract.ts +97 -0
- package/src/utils/zipPath.ts +24 -0
package/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { dcgchatPlugin } from './src/channel.js'
|
|
|
4
4
|
import { setDcgchatRuntime, setWorkspaceDir } from './src/utils/global.js'
|
|
5
5
|
import { monitoringToolMessage } from './src/tool.js'
|
|
6
6
|
import { setOpenClawConfig } from './src/utils/global.js'
|
|
7
|
-
import {
|
|
7
|
+
import { createDcgchatMessageTool } from './src/tools/messageTool.js'
|
|
8
8
|
|
|
9
9
|
const plugin = {
|
|
10
10
|
id: "dcgchat",
|
|
@@ -18,9 +18,8 @@ const plugin = {
|
|
|
18
18
|
api.registerChannel({ plugin: dcgchatPlugin })
|
|
19
19
|
setWorkspaceDir(api.config?.agents?.defaults?.workspace)
|
|
20
20
|
api.registerTool((ctx) => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return null
|
|
21
|
+
setWorkspaceDir(ctx.workspaceDir)
|
|
22
|
+
return createDcgchatMessageTool(ctx)
|
|
24
23
|
})
|
|
25
24
|
}
|
|
26
25
|
}
|
package/package.json
CHANGED
package/src/agent.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { getWorkspaceDir } from './utils/global.js'
|
|
5
|
+
import { getWsConnection } from './utils/global.js'
|
|
6
|
+
import { dcgLogger } from './utils/log.js'
|
|
7
|
+
import { isWsOpen } from './transport.js'
|
|
8
|
+
import { sendMessageToGateway } from './gateway/socket.js'
|
|
9
|
+
import { extractZipBufferToDirectory } from './utils/zipExtract.js'
|
|
10
|
+
|
|
11
|
+
type IAgentParams = {
|
|
12
|
+
url: string
|
|
13
|
+
agent_code: string
|
|
14
|
+
agent_name: string
|
|
15
|
+
agent_description: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sendEvent(msgContent: Record<string, any>) {
|
|
19
|
+
const ws = getWsConnection()
|
|
20
|
+
if (isWsOpen()) {
|
|
21
|
+
ws?.send(
|
|
22
|
+
JSON.stringify({
|
|
23
|
+
messageType: 'openclaw_bot_event',
|
|
24
|
+
source: 'client',
|
|
25
|
+
content: msgContent
|
|
26
|
+
})
|
|
27
|
+
)
|
|
28
|
+
dcgLogger(`agent安装: ${JSON.stringify(msgContent)}`)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 若 workspace-${clone_code}/agent 存在,则复制到 agents/${clone_code}/agent */
|
|
33
|
+
function copyAgentsFiles(clone_code: string) {
|
|
34
|
+
const workspacePath = getWorkspaceDir()
|
|
35
|
+
if (!workspacePath) return
|
|
36
|
+
const workspaceDir = path.join(workspacePath, '../', `workspace-${clone_code}`)
|
|
37
|
+
const agentDir = path.join(workspacePath, '../', `agents/${clone_code}`)
|
|
38
|
+
const sourceAgent = path.join(workspaceDir, 'agent')
|
|
39
|
+
try {
|
|
40
|
+
if (!fs.existsSync(sourceAgent)) return
|
|
41
|
+
if (!fs.statSync(sourceAgent).isDirectory()) return
|
|
42
|
+
fs.mkdirSync(agentDir, { recursive: true })
|
|
43
|
+
const dest = path.join(agentDir, 'agent')
|
|
44
|
+
if (fs.existsSync(dest)) {
|
|
45
|
+
fs.rmSync(dest, { recursive: true, force: true })
|
|
46
|
+
}
|
|
47
|
+
fs.cpSync(sourceAgent, dest, { recursive: true })
|
|
48
|
+
} catch (err: unknown) {
|
|
49
|
+
dcgLogger(`copyAgentsFiles failed: ${String(err)}`, 'error')
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function onCreateAgent(params: Record<string, any>) {
|
|
54
|
+
const { clone_code, name, description } = params
|
|
55
|
+
try {
|
|
56
|
+
await sendMessageToGateway(JSON.stringify({ method: 'agents.create', params: { name: clone_code, workspace: clone_code } }))
|
|
57
|
+
} catch (err: unknown) {
|
|
58
|
+
dcgLogger(`agents.create failed: ${String(err)}`, 'error')
|
|
59
|
+
}
|
|
60
|
+
// Update config.name to the user-supplied display name (may contain CJK, spaces, etc.)
|
|
61
|
+
try {
|
|
62
|
+
await sendMessageToGateway(JSON.stringify({ method: 'agents.update', params: { name: name, agentId: clone_code } }))
|
|
63
|
+
} catch (err: unknown) {
|
|
64
|
+
dcgLogger(`agents.update failed: ${String(err)}`, 'error')
|
|
65
|
+
}
|
|
66
|
+
if (description?.trim()) {
|
|
67
|
+
try {
|
|
68
|
+
await sendMessageToGateway(
|
|
69
|
+
JSON.stringify({
|
|
70
|
+
method: 'agents.files.set',
|
|
71
|
+
params: { agentId: clone_code, name: 'IDENTITY.md', content: description.trim() }
|
|
72
|
+
})
|
|
73
|
+
)
|
|
74
|
+
} catch {
|
|
75
|
+
// Non-fatal
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (name?.trim()) {
|
|
79
|
+
try {
|
|
80
|
+
await sendMessageToGateway(
|
|
81
|
+
JSON.stringify({
|
|
82
|
+
method: 'agents.files.set',
|
|
83
|
+
params: { agentId: clone_code, name: 'USER.md', content: name.trim() }
|
|
84
|
+
})
|
|
85
|
+
)
|
|
86
|
+
} catch {
|
|
87
|
+
// Non-fatal
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
copyAgentsFiles(clone_code)
|
|
91
|
+
sendEvent({ ...params, status: 'ok' })
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function createAgent(msgContent: Record<string, any>) {
|
|
95
|
+
const { url, clone_code } = msgContent
|
|
96
|
+
if (!url || !clone_code) {
|
|
97
|
+
dcgLogger(`createAgent failed empty url&clone_code: ${JSON.stringify(msgContent)}`, 'error')
|
|
98
|
+
sendEvent({ ...msgContent, status: 'fail' })
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
const workspacePath = getWorkspaceDir()
|
|
102
|
+
const workspaceDir = path.join(workspacePath, '../', `workspace-${clone_code}`)
|
|
103
|
+
|
|
104
|
+
// 如果目标目录已存在,先删除
|
|
105
|
+
if (fs.existsSync(workspaceDir)) {
|
|
106
|
+
fs.rmSync(workspaceDir, { recursive: true, force: true })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const response = await axios({
|
|
111
|
+
method: 'get',
|
|
112
|
+
url,
|
|
113
|
+
responseType: 'arraybuffer'
|
|
114
|
+
})
|
|
115
|
+
fs.mkdirSync(workspaceDir, { recursive: true })
|
|
116
|
+
await extractZipBufferToDirectory(Buffer.from(response.data), workspaceDir)
|
|
117
|
+
await onCreateAgent(msgContent)
|
|
118
|
+
} catch (error) {
|
|
119
|
+
// 如果安装失败,清理目录
|
|
120
|
+
if (fs.existsSync(workspaceDir)) {
|
|
121
|
+
fs.rmSync(workspaceDir, { recursive: true, force: true })
|
|
122
|
+
}
|
|
123
|
+
sendEvent({ ...msgContent, status: 'fail' })
|
|
124
|
+
}
|
|
125
|
+
}
|
package/src/bot.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
import os from 'node:os'
|
|
4
2
|
import type { ReplyPayload } from 'openclaw/plugin-sdk'
|
|
5
|
-
import { createReplyPrefixContext } from 'openclaw/plugin-sdk'
|
|
3
|
+
import { createReplyPrefixContext, createTypingCallbacks } from 'openclaw/plugin-sdk'
|
|
6
4
|
import type { InboundMessage } from './types.js'
|
|
7
5
|
import {
|
|
8
6
|
clearSentMediaKeys,
|
|
@@ -14,12 +12,12 @@ import {
|
|
|
14
12
|
} from './utils/global.js'
|
|
15
13
|
import { resolveAccount, sendDcgchatMedia } from './channel.js'
|
|
16
14
|
import { generateSignUrl } from './request/api.js'
|
|
17
|
-
import { extractMobookFiles } from './utils/searchFile.js'
|
|
18
15
|
import { sendChunk, sendFinal, sendText as sendTextMsg, sendError, wsSendRaw, sendText } from './transport.js'
|
|
19
16
|
import { dcgLogger } from './utils/log.js'
|
|
20
17
|
import { channelInfo, systemCommand, interruptCommand, ENV, ignoreToolCommand } from './utils/constant.js'
|
|
21
|
-
import {
|
|
22
|
-
import { getEffectiveMsgParams, setParamsMessage } from './utils/params.js'
|
|
18
|
+
import { sendGatewayRpc } from './gateway/socket.js'
|
|
19
|
+
import { clearParamsMessage, getEffectiveMsgParams, setParamsMessage } from './utils/params.js'
|
|
20
|
+
import { getChildSessionKeysTrackedForRequester, resetSubagentStateForRequesterSession, waitUntilSubagentsIdle } from './tool.js'
|
|
23
21
|
|
|
24
22
|
type MediaInfo = {
|
|
25
23
|
path: string
|
|
@@ -44,13 +42,6 @@ const sessionStreamSuppressed = new Set<string>()
|
|
|
44
42
|
/** 各 sessionKey 当前轮回复的流式分片序号(仅统计实际下发的文本 chunk);每轮非打断消息开始时置 0 */
|
|
45
43
|
const streamChunkIdxBySessionKey = new Map<string, number>()
|
|
46
44
|
|
|
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
45
|
export function extractAgentIdFromConversationId(conversationId: string): string | null {
|
|
55
46
|
const idx = conversationId.indexOf('::')
|
|
56
47
|
if (idx <= 0) return null
|
|
@@ -133,12 +124,17 @@ function resolveReplyMediaList(payload: ReplyPayload): string[] {
|
|
|
133
124
|
return payload.mediaUrl ? [payload.mediaUrl] : []
|
|
134
125
|
}
|
|
135
126
|
|
|
127
|
+
const typingCallbacks = createTypingCallbacks({
|
|
128
|
+
start: async () => {},
|
|
129
|
+
onStartError: (err) => {
|
|
130
|
+
dcgLogger(`typing start error: ${String(err)}`, 'error')
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
136
134
|
/**
|
|
137
135
|
* 处理一条用户消息,调用 Agent 并返回回复
|
|
138
136
|
*/
|
|
139
137
|
export async function handleDcgchatMessage(msg: InboundMessage, accountId: string): Promise<void> {
|
|
140
|
-
let finalSent = false
|
|
141
|
-
|
|
142
138
|
let completeText = ''
|
|
143
139
|
const config = getOpenClawConfig()
|
|
144
140
|
if (!config) {
|
|
@@ -151,7 +147,6 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
151
147
|
const core = getDcgchatRuntime()
|
|
152
148
|
|
|
153
149
|
const conversationId = msg.content.session_id?.trim()
|
|
154
|
-
const agentId = msg.content.agent_id?.trim()
|
|
155
150
|
const real_mobook = msg.content.real_mobook?.toString().trim()
|
|
156
151
|
|
|
157
152
|
const route = core.channel.routing.resolveAgentRoute({
|
|
@@ -164,7 +159,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
164
159
|
const embeddedAgentId = extractAgentIdFromConversationId(conversationId)
|
|
165
160
|
|
|
166
161
|
const effectiveAgentId = embeddedAgentId ?? route.agentId
|
|
167
|
-
const
|
|
162
|
+
const dcgSessionKey = getSessionKey(msg.content, account.accountId)
|
|
168
163
|
|
|
169
164
|
const mergedParams = {
|
|
170
165
|
userId: msg._userId,
|
|
@@ -175,44 +170,27 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
175
170
|
appId: msg.content.app_id,
|
|
176
171
|
botId: msg.content.bot_id ?? '',
|
|
177
172
|
agentId: msg.content.agent_id ?? '',
|
|
178
|
-
sessionKey:
|
|
173
|
+
sessionKey: dcgSessionKey,
|
|
179
174
|
real_mobook
|
|
180
175
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
)
|
|
186
|
-
setParamsMessage(userId, mergedParams)
|
|
187
|
-
dcgLogger(`target alias bound: aliasTarget=${userId} -> sessionKey=${effectiveSessionKey}`)
|
|
188
|
-
const outboundCtx = getEffectiveMsgParams(effectiveSessionKey)
|
|
176
|
+
/** 写入本条消息参数前快照:流式/abort 的 final 须对齐「上一轮」触发的对话 messageId,而非打断指令本身 */
|
|
177
|
+
const priorOutboundCtx = getEffectiveMsgParams(dcgSessionKey)
|
|
178
|
+
setParamsMessage(dcgSessionKey, mergedParams)
|
|
179
|
+
dcgLogger(`target alias bound: aliasTarget=${userId} -> sessionKey=${dcgSessionKey}`)
|
|
180
|
+
const outboundCtx = getEffectiveMsgParams(dcgSessionKey)
|
|
189
181
|
const agentEntry =
|
|
190
182
|
effectiveAgentId && effectiveAgentId !== 'main' ? config.agents?.list?.find((a) => a.id === effectiveAgentId) : undefined
|
|
191
183
|
const agentDisplayName = agentEntry?.name ?? (effectiveAgentId && effectiveAgentId !== 'main' ? effectiveAgentId : undefined)
|
|
192
184
|
|
|
193
|
-
|
|
194
|
-
if (finalSent) return
|
|
195
|
-
finalSent = true
|
|
196
|
-
sendFinal(outboundCtx, tag)
|
|
197
|
-
setMsgStatus(effectiveSessionKey, 'finished')
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const text = msg.content.text?.trim()
|
|
185
|
+
let text = msg.content.text?.trim()
|
|
201
186
|
|
|
202
187
|
if (!text) {
|
|
203
188
|
sendTextMsg('你需要我帮你做什么呢?', outboundCtx)
|
|
204
|
-
|
|
189
|
+
sendFinal(outboundCtx, 'not text')
|
|
205
190
|
return
|
|
206
191
|
}
|
|
207
192
|
|
|
208
193
|
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
194
|
// 处理用户上传的文件
|
|
217
195
|
const files = msg.content.files ?? []
|
|
218
196
|
let mediaPayload: Record<string, unknown> = {}
|
|
@@ -236,8 +214,8 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
236
214
|
RawBody: text,
|
|
237
215
|
CommandBody: text,
|
|
238
216
|
From: userId,
|
|
239
|
-
To:
|
|
240
|
-
SessionKey:
|
|
217
|
+
To: dcgSessionKey,
|
|
218
|
+
SessionKey: dcgSessionKey,
|
|
241
219
|
AccountId: route.accountId,
|
|
242
220
|
ChatType: 'direct',
|
|
243
221
|
SenderName: agentDisplayName,
|
|
@@ -249,17 +227,27 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
249
227
|
WasMentioned: true,
|
|
250
228
|
CommandAuthorized: true,
|
|
251
229
|
OriginatingChannel: "dcgchat",
|
|
252
|
-
OriginatingTo:
|
|
230
|
+
OriginatingTo: dcgSessionKey,
|
|
231
|
+
Target: dcgSessionKey,
|
|
232
|
+
SourceTarget: dcgSessionKey,
|
|
253
233
|
...mediaPayload
|
|
254
234
|
})
|
|
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
235
|
|
|
259
236
|
const sentMediaKeys = new Set<string>()
|
|
260
237
|
const getMediaKey = (url: string) => url.split(/[\\/]/).pop() ?? url
|
|
261
238
|
let streamedTextLen = 0
|
|
262
239
|
|
|
240
|
+
if (msg.content.skills_scope.length > 0 && !msg.content?.agent_clone_code) {
|
|
241
|
+
const workspaceDir = getWorkspaceDir()
|
|
242
|
+
const skillText = msg.content.skills_scope
|
|
243
|
+
.map((skill) => {
|
|
244
|
+
const skillDir = `${workspaceDir}/skills/${skill.skill_code}`
|
|
245
|
+
return `技能${skill.skill_code} 在目录${skillDir}下,在目录${skillDir}下读取技能 \n`
|
|
246
|
+
})
|
|
247
|
+
.join('\n')
|
|
248
|
+
text = skillText ? `${skillText} ${text}` : text
|
|
249
|
+
dcgLogger(`skill: text: ${text}`)
|
|
250
|
+
}
|
|
263
251
|
const prefixContext = createReplyPrefixContext({
|
|
264
252
|
cfg: config,
|
|
265
253
|
agentId: effectiveAgentId ?? '',
|
|
@@ -267,78 +255,106 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
267
255
|
accountId: account.accountId
|
|
268
256
|
})
|
|
269
257
|
|
|
270
|
-
const { dispatcher, replyOptions, markDispatchIdle
|
|
258
|
+
const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
|
|
271
259
|
responsePrefix: prefixContext.responsePrefix,
|
|
272
260
|
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
273
261
|
humanDelay: core.channel.reply.resolveHumanDelayConfig(config, route.agentId),
|
|
274
262
|
onReplyStart: async () => {},
|
|
275
263
|
deliver: async (payload: ReplyPayload, info) => {
|
|
276
|
-
if (sessionStreamSuppressed.has(
|
|
264
|
+
if (sessionStreamSuppressed.has(dcgSessionKey)) return
|
|
277
265
|
const mediaList = resolveReplyMediaList(payload)
|
|
278
266
|
for (const mediaUrl of mediaList) {
|
|
279
267
|
const key = getMediaKey(mediaUrl)
|
|
280
268
|
if (sentMediaKeys.has(key)) continue
|
|
281
269
|
sentMediaKeys.add(key)
|
|
282
|
-
await sendDcgchatMedia({ sessionKey:
|
|
270
|
+
await sendDcgchatMedia({ sessionKey: dcgSessionKey, mediaUrl, text: '' })
|
|
283
271
|
}
|
|
284
272
|
},
|
|
285
273
|
onError: (err: unknown, info: { kind: string }) => {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
activeRunIdBySessionKey.delete(
|
|
289
|
-
streamChunkIdxBySessionKey.delete(
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
return
|
|
293
|
-
}
|
|
294
|
-
dcgLogger(`${info.kind} reply failed: ${String(err)}`, 'error')
|
|
274
|
+
setMsgStatus(dcgSessionKey, 'finished')
|
|
275
|
+
sendFinal(outboundCtx, 'error')
|
|
276
|
+
activeRunIdBySessionKey.delete(dcgSessionKey)
|
|
277
|
+
streamChunkIdxBySessionKey.delete(dcgSessionKey)
|
|
278
|
+
const suppressed = sessionStreamSuppressed.has(dcgSessionKey)
|
|
279
|
+
dcgLogger(`${info.kind} reply failed${suppressed ? ' (stream suppressed)' : ''}: ${String(err)}`, 'error')
|
|
295
280
|
},
|
|
296
|
-
onIdle: () => {
|
|
281
|
+
onIdle: () => {
|
|
282
|
+
typingCallbacks.onIdle?.()
|
|
283
|
+
}
|
|
297
284
|
})
|
|
298
285
|
|
|
299
286
|
try {
|
|
300
287
|
if (!interruptCommand.includes(text?.trim())) {
|
|
301
|
-
sessionStreamSuppressed.delete(
|
|
302
|
-
streamChunkIdxBySessionKey.set(
|
|
288
|
+
sessionStreamSuppressed.delete(dcgSessionKey)
|
|
289
|
+
streamChunkIdxBySessionKey.set(dcgSessionKey, 0)
|
|
303
290
|
}
|
|
304
291
|
|
|
305
292
|
if (systemCommand.includes(text?.trim())) {
|
|
306
|
-
dcgLogger(`dispatching
|
|
307
|
-
await core.channel.reply.
|
|
308
|
-
ctx: ctxPayload,
|
|
309
|
-
cfg: config,
|
|
293
|
+
dcgLogger(`dispatching ${text?.trim()}`)
|
|
294
|
+
await core.channel.reply.withReplyDispatcher({
|
|
310
295
|
dispatcher,
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
296
|
+
onSettled: () => markDispatchIdle(),
|
|
297
|
+
run: () =>
|
|
298
|
+
core.channel.reply.dispatchReplyFromConfig({
|
|
299
|
+
ctx: ctxPayload,
|
|
300
|
+
cfg: config,
|
|
301
|
+
dispatcher,
|
|
302
|
+
replyOptions: {
|
|
303
|
+
...replyOptions,
|
|
304
|
+
onModelSelected: prefixContext.onModelSelected,
|
|
305
|
+
onAgentRunStart: (runId) => {
|
|
306
|
+
activeRunIdBySessionKey.set(dcgSessionKey, runId)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
})
|
|
318
310
|
})
|
|
319
311
|
} else if (interruptCommand.includes(text?.trim())) {
|
|
320
312
|
dcgLogger(`interrupt command: ${text}`)
|
|
321
|
-
|
|
313
|
+
const ctxForAbort =
|
|
314
|
+
priorOutboundCtx.messageId?.trim() || priorOutboundCtx.sessionId?.trim()
|
|
315
|
+
? priorOutboundCtx
|
|
316
|
+
: outboundCtx
|
|
317
|
+
sendFinal(
|
|
318
|
+
ctxForAbort.messageId?.trim() ? ctxForAbort : { ...ctxForAbort, messageId: `${Date.now()}` },
|
|
319
|
+
'abort'
|
|
320
|
+
)
|
|
322
321
|
sendText('会话已终止', outboundCtx)
|
|
323
|
-
sessionStreamSuppressed.add(
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
322
|
+
sessionStreamSuppressed.add(dcgSessionKey)
|
|
323
|
+
const abortOneSession = async (sessionKey: string) => {
|
|
324
|
+
try {
|
|
325
|
+
await sendGatewayRpc({ method: 'chat.abort', params: { sessionKey } })
|
|
326
|
+
} catch (e) {
|
|
327
|
+
dcgLogger(`chat.abort ${sessionKey}: ${String(e)}`, 'error')
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const keysToAbort = new Set<string>(getChildSessionKeysTrackedForRequester(dcgSessionKey))
|
|
331
|
+
try {
|
|
332
|
+
const listed = await sendGatewayRpc<{ sessions?: Array<{ key?: string }> }>({
|
|
333
|
+
method: 'sessions.list',
|
|
334
|
+
params: { spawnedBy: dcgSessionKey, limit: 256 }
|
|
332
335
|
})
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
+
for (const s of listed?.sessions ?? []) {
|
|
337
|
+
const k = typeof s?.key === 'string' ? s.key.trim() : ''
|
|
338
|
+
if (k) keysToAbort.add(k)
|
|
339
|
+
}
|
|
340
|
+
} catch (e) {
|
|
341
|
+
dcgLogger(`sessions.list spawnedBy: ${String(e)}`, 'error')
|
|
342
|
+
}
|
|
343
|
+
for (const sk of keysToAbort) {
|
|
344
|
+
await abortOneSession(sk)
|
|
345
|
+
}
|
|
346
|
+
await abortOneSession(dcgSessionKey)
|
|
347
|
+
streamChunkIdxBySessionKey.delete(dcgSessionKey)
|
|
348
|
+
resetSubagentStateForRequesterSession(dcgSessionKey)
|
|
349
|
+
setMsgStatus(dcgSessionKey, 'finished')
|
|
350
|
+
clearSentMediaKeys(msg.content.message_id)
|
|
351
|
+
clearParamsMessage(dcgSessionKey)
|
|
352
|
+
clearParamsMessage(userId)
|
|
353
|
+
sendFinal(outboundCtx, 'stop')
|
|
336
354
|
return
|
|
337
355
|
} else {
|
|
338
|
-
|
|
339
|
-
const params = getEffectiveMsgParams(effectiveSessionKey)
|
|
356
|
+
const params = getEffectiveMsgParams(dcgSessionKey)
|
|
340
357
|
if (!ignoreToolCommand.includes(text?.trim())) {
|
|
341
|
-
// message_received 没有 sessionKey 前置到bot中执行
|
|
342
358
|
wsSendRaw(params, {
|
|
343
359
|
is_finish: -1,
|
|
344
360
|
tool_call_id: Date.now().toString(),
|
|
@@ -353,114 +369,79 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
353
369
|
response: ''
|
|
354
370
|
})
|
|
355
371
|
}
|
|
356
|
-
await core.channel.reply.
|
|
357
|
-
ctx: ctxPayload,
|
|
358
|
-
cfg: config,
|
|
372
|
+
await core.channel.reply.withReplyDispatcher({
|
|
359
373
|
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
|
-
|
|
374
|
+
onSettled: () => markDispatchIdle(),
|
|
375
|
+
run: () =>
|
|
376
|
+
core.channel.reply.dispatchReplyFromConfig({
|
|
377
|
+
ctx: ctxPayload,
|
|
378
|
+
cfg: config,
|
|
379
|
+
dispatcher,
|
|
380
|
+
replyOptions: {
|
|
381
|
+
...replyOptions,
|
|
382
|
+
onModelSelected: prefixContext.onModelSelected,
|
|
383
|
+
onAgentRunStart: (runId) => {
|
|
384
|
+
activeRunIdBySessionKey.set(dcgSessionKey, runId)
|
|
385
|
+
},
|
|
386
|
+
onPartialReply: async (payload: ReplyPayload) => {
|
|
387
|
+
if (sessionStreamSuppressed.has(dcgSessionKey)) return
|
|
388
|
+
|
|
389
|
+
if (payload.text) {
|
|
390
|
+
completeText = payload.text
|
|
391
|
+
}
|
|
392
|
+
// --- Streaming text chunks ---
|
|
393
|
+
if (payload.text) {
|
|
394
|
+
const delta = payload.text.startsWith(completeText.slice(0, streamedTextLen))
|
|
395
|
+
? payload.text.slice(streamedTextLen)
|
|
396
|
+
: payload.text
|
|
397
|
+
if (delta.trim()) {
|
|
398
|
+
const prev = streamChunkIdxBySessionKey.get(dcgSessionKey) ?? 0
|
|
399
|
+
streamChunkIdxBySessionKey.set(dcgSessionKey, prev + 1)
|
|
400
|
+
sendChunk(delta, outboundCtx, prev)
|
|
401
|
+
dcgLogger(
|
|
402
|
+
`[stream]: chunkIdx=${prev} len=${delta.length} sessionId=${outboundCtx.sessionId} ${delta.slice(0, 100)}`
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
streamedTextLen = payload.text.length
|
|
406
|
+
} else {
|
|
407
|
+
dcgLogger(`onPartialReply no text: ${JSON.stringify(payload)}`, 'error')
|
|
408
|
+
}
|
|
409
|
+
// --- Media from payload ---
|
|
410
|
+
const mediaList = resolveReplyMediaList(payload)
|
|
411
|
+
for (const mediaUrl of mediaList) {
|
|
412
|
+
const key = getMediaKey(mediaUrl)
|
|
413
|
+
if (sentMediaKeys.has(key)) continue
|
|
414
|
+
sentMediaKeys.add(key)
|
|
415
|
+
await sendDcgchatMedia({ sessionKey: dcgSessionKey, mediaUrl, text: '' })
|
|
416
|
+
}
|
|
384
417
|
}
|
|
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
418
|
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
419
|
+
})
|
|
397
420
|
})
|
|
398
421
|
}
|
|
399
422
|
} 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')
|
|
423
|
+
dcgLogger(` dispatchReplyFromConfig error: ${String(err)}`, 'error')
|
|
419
424
|
}
|
|
425
|
+
|
|
420
426
|
if (![...systemCommand, ...interruptCommand].includes(text?.trim())) {
|
|
421
|
-
if (sessionStreamSuppressed.has(
|
|
422
|
-
sessionStreamSuppressed.delete(
|
|
423
|
-
} else {
|
|
424
|
-
for (const file of extractMobookFiles(completeText)) {
|
|
425
|
-
const candidates: string[] = [file]
|
|
426
|
-
candidates.push(path.join(getWorkspaceDir(), file))
|
|
427
|
-
candidates.push(path.join(getWorkspaceDir(), file.replace(/^\//, '')))
|
|
428
|
-
const underMobook = file.replace(/^\/mobook\//i, '').replace(/^mobook[\\/]/i, '')
|
|
429
|
-
if (underMobook) {
|
|
430
|
-
if (process.platform === 'win32') {
|
|
431
|
-
candidates.push(path.join('C:\\', 'mobook', underMobook))
|
|
432
|
-
} else if (process.platform === 'darwin') {
|
|
433
|
-
candidates.push(path.join(os.homedir(), 'mobook', underMobook))
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
const resolved = candidates.find((p) => fs.existsSync(p))
|
|
437
|
-
if (!resolved) continue
|
|
438
|
-
try {
|
|
439
|
-
await sendDcgchatMedia({ sessionKey: effectiveSessionKey, mediaUrl: resolved, text: '' })
|
|
440
|
-
} catch (err) {
|
|
441
|
-
dcgLogger(` sendDcgchatMedia error: ${String(err)}`, 'error')
|
|
442
|
-
}
|
|
443
|
-
}
|
|
427
|
+
if (sessionStreamSuppressed.has(dcgSessionKey)) {
|
|
428
|
+
sessionStreamSuppressed.delete(dcgSessionKey)
|
|
444
429
|
}
|
|
445
430
|
}
|
|
446
|
-
safeSendFinal('end')
|
|
447
431
|
clearSentMediaKeys(msg.content.message_id)
|
|
448
|
-
|
|
449
|
-
// Record session metadata
|
|
450
432
|
const storePath = core.channel.session.resolveStorePath(config.session?.store)
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
)
|
|
433
|
+
await waitUntilSubagentsIdle(dcgSessionKey, { timeoutMs: 600_000 })
|
|
434
|
+
sendFinal(outboundCtx, 'end')
|
|
435
|
+
dcgLogger(`record session route: updateLastRoute.to=${dcgSessionKey}, accountId=${route.accountId}`)
|
|
454
436
|
core.channel.session
|
|
455
437
|
.recordInboundSession({
|
|
456
438
|
storePath,
|
|
457
|
-
sessionKey:
|
|
439
|
+
sessionKey: dcgSessionKey,
|
|
458
440
|
ctx: ctxPayload,
|
|
459
|
-
// 与 Telegram/Discord 等一致:写入 deliveryContext.to,否则投递可能回退为 From(userId),channel sendMedia 里 ctx.to 会变成数字 userId
|
|
460
441
|
updateLastRoute: {
|
|
461
|
-
sessionKey:
|
|
442
|
+
sessionKey: dcgSessionKey,
|
|
462
443
|
channel: "dcgchat",
|
|
463
|
-
to:
|
|
444
|
+
to: dcgSessionKey,
|
|
464
445
|
accountId: route.accountId
|
|
465
446
|
},
|
|
466
447
|
onRecordError: (err) => {
|
|
@@ -473,7 +454,5 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
473
454
|
} catch (err) {
|
|
474
455
|
dcgLogger(` handle message failed: ${String(err)}`, 'error')
|
|
475
456
|
sendError(err instanceof Error ? err.message : String(err), outboundCtx)
|
|
476
|
-
} finally {
|
|
477
|
-
safeSendFinal('finally')
|
|
478
457
|
}
|
|
479
458
|
}
|