@dcrays/dcgchat 0.4.29 → 0.5.1
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.js +292 -0
- package/openclaw.plugin.json +17 -1
- package/package.json +18 -13
- package/schemas/gateway-cron-finished.payload.json +39 -0
- package/index.ts +0 -26
- package/src/agent.ts +0 -128
- package/src/bot.ts +0 -500
- package/src/channel.ts +0 -470
- package/src/cron.ts +0 -194
- package/src/cronToolCall.ts +0 -202
- package/src/gateway/index.ts +0 -447
- package/src/gateway/security.ts +0 -95
- package/src/gateway/socket.ts +0 -285
- package/src/libs/ali-oss-6.23.0.tgz +0 -0
- package/src/libs/axios-1.13.6.tgz +0 -0
- package/src/libs/md5-2.3.0.tgz +0 -0
- package/src/libs/mime-types-3.0.2.tgz +0 -0
- package/src/libs/unzipper-0.12.3.tgz +0 -0
- package/src/libs/ws-8.19.0.tgz +0 -0
- package/src/monitor.ts +0 -165
- package/src/request/api.ts +0 -70
- package/src/request/oss.ts +0 -212
- package/src/request/request.ts +0 -192
- package/src/request/userInfo.ts +0 -99
- package/src/session.ts +0 -19
- package/src/sessionTermination.ts +0 -154
- package/src/skill.ts +0 -151
- package/src/tool.ts +0 -422
- package/src/tools/messageTool.ts +0 -224
- package/src/transport.ts +0 -203
- package/src/types.ts +0 -139
- package/src/utils/constant.ts +0 -7
- package/src/utils/gatewayMsgHanlder.ts +0 -55
- package/src/utils/global.ts +0 -160
- package/src/utils/log.ts +0 -15
- package/src/utils/params.ts +0 -88
- package/src/utils/searchFile.ts +0 -228
- package/src/utils/wsMessageHandler.ts +0 -64
- package/src/utils/zipExtract.ts +0 -97
- package/src/utils/zipPath.ts +0 -24
package/src/bot.ts
DELETED
|
@@ -1,500 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import type { ReplyPayload } from 'openclaw/plugin-sdk'
|
|
4
|
-
import { createReplyPrefixContext, createTypingCallbacks } from 'openclaw/plugin-sdk'
|
|
5
|
-
import type { InboundMessage } from './types.js'
|
|
6
|
-
import {
|
|
7
|
-
clearSentMediaKeys,
|
|
8
|
-
getDcgchatRuntime,
|
|
9
|
-
getOpenClawConfig,
|
|
10
|
-
getSessionKey,
|
|
11
|
-
getWorkspaceDir,
|
|
12
|
-
setMsgStatus
|
|
13
|
-
} from './utils/global.js'
|
|
14
|
-
import { normalizeOutboundMediaPaths, resolveAccount, sendDcgchatMedia } from './channel.js'
|
|
15
|
-
import { generateSignUrl } from './request/api.js'
|
|
16
|
-
import { sendChunk, sendFinal, sendText as sendTextMsg, sendError, wsSendRaw, sendText } from './transport.js'
|
|
17
|
-
import { dcgLogger } from './utils/log.js'
|
|
18
|
-
import { channelInfo, systemCommand, stopCommand, ENV, ignoreToolCommand } from './utils/constant.js'
|
|
19
|
-
import { clearParamsMessage, getEffectiveMsgParams, setParamsMessage } from './utils/params.js'
|
|
20
|
-
import { waitUntilSubagentsIdle } from './tool.js'
|
|
21
|
-
import {
|
|
22
|
-
beginSupersedingUserTurn,
|
|
23
|
-
clearActiveRunIdForSession,
|
|
24
|
-
clearSessionStreamSuppression,
|
|
25
|
-
interruptLocalDispatchAndGateway,
|
|
26
|
-
isSessionStreamSuppressed,
|
|
27
|
-
preemptInboundQueueForStop,
|
|
28
|
-
releaseDispatchAbortIfCurrent,
|
|
29
|
-
runInboundTurnSequenced,
|
|
30
|
-
setActiveRunIdForSession
|
|
31
|
-
} from './sessionTermination.js'
|
|
32
|
-
|
|
33
|
-
type MediaInfo = {
|
|
34
|
-
path: string
|
|
35
|
-
fileName: string
|
|
36
|
-
contentType: string
|
|
37
|
-
placeholder: string
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
type TFileInfo = { name: string; url: string }
|
|
41
|
-
|
|
42
|
-
const mediaMaxBytes = 300 * 1024 * 1024
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* 每条入站消息(含 /stop)递增。用于丢弃「已被后续入站抢占」的处理在 dispatch 结束后的尾部逻辑。
|
|
46
|
-
* 仅 /stop 时的 epoch 无法覆盖「新用户消息抢占上一轮」的并发 WS 场景,会导致旧 handler 仍执行 sendFinal(end)、
|
|
47
|
-
* 而新消息被误判为 stale;且上一轮若未对网关 chat.abort,服务端 run 会继续,表现为「新消息秒结束、旧回复复活」。
|
|
48
|
-
*/
|
|
49
|
-
const inboundGenerationBySessionKey = new Map<string, number>()
|
|
50
|
-
|
|
51
|
-
function bumpInboundGeneration(sessionKey: string): number {
|
|
52
|
-
const n = (inboundGenerationBySessionKey.get(sessionKey) ?? 0) + 1
|
|
53
|
-
inboundGenerationBySessionKey.set(sessionKey, n)
|
|
54
|
-
return n
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** 各 sessionKey 当前轮回复的流式分片序号(仅统计实际下发的文本 chunk);每轮非打断消息开始时置 0 */
|
|
58
|
-
const streamChunkIdxBySessionKey = new Map<string, number>()
|
|
59
|
-
|
|
60
|
-
export function extractAgentIdFromConversationId(conversationId: string): string | null {
|
|
61
|
-
const idx = conversationId.indexOf('::')
|
|
62
|
-
if (idx <= 0) return null
|
|
63
|
-
return conversationId.slice(0, idx)
|
|
64
|
-
}
|
|
65
|
-
function formatText(text: string): string {
|
|
66
|
-
if (!text) return ''
|
|
67
|
-
const str = String(text).replace(/\s/g, '')
|
|
68
|
-
if (!str) return ''
|
|
69
|
-
if (str.length <= 50) {
|
|
70
|
-
return str
|
|
71
|
-
}
|
|
72
|
-
return str.slice(0, 25) + `...[此处省略${str.length - 50}字]....` + str.slice(-25)
|
|
73
|
-
}
|
|
74
|
-
async function resolveMediaFromUrls(files: TFileInfo[], botToken: string): Promise<MediaInfo[]> {
|
|
75
|
-
const core = getDcgchatRuntime()
|
|
76
|
-
const out: MediaInfo[] = []
|
|
77
|
-
dcgLogger(`media: user upload files: ${JSON.stringify(files)}`)
|
|
78
|
-
|
|
79
|
-
for (let i = 0; i < files.length; i++) {
|
|
80
|
-
const file = files[i]
|
|
81
|
-
try {
|
|
82
|
-
let data = ''
|
|
83
|
-
if (/^https?:\/\//i.test(file.url)) {
|
|
84
|
-
data = file.url
|
|
85
|
-
} else {
|
|
86
|
-
data = await generateSignUrl(file.url, botToken)
|
|
87
|
-
}
|
|
88
|
-
dcgLogger(`media: generateSignUrl: ${data}`)
|
|
89
|
-
const response = await fetch(data)
|
|
90
|
-
if (!response.ok) {
|
|
91
|
-
dcgLogger?.(`media: ${file.url} fetch failed with HTTP ${response.status}`, 'error')
|
|
92
|
-
continue
|
|
93
|
-
}
|
|
94
|
-
const buffer = Buffer.from(await response.arrayBuffer())
|
|
95
|
-
|
|
96
|
-
let contentType = response.headers.get('content-type') || ''
|
|
97
|
-
if (!contentType) {
|
|
98
|
-
contentType = (await core.media.detectMime({ buffer })) || ''
|
|
99
|
-
}
|
|
100
|
-
const fileName = file.name || path.basename(new URL(file.url).pathname) || 'file'
|
|
101
|
-
const saved = await core.channel.media.saveMediaBuffer(buffer, contentType, 'inbound', mediaMaxBytes, fileName)
|
|
102
|
-
const isImage = contentType.startsWith('image/')
|
|
103
|
-
out.push({
|
|
104
|
-
path: saved.path,
|
|
105
|
-
fileName,
|
|
106
|
-
contentType: saved.contentType || '',
|
|
107
|
-
placeholder: isImage ? '<media:image>' : '<media:file>'
|
|
108
|
-
})
|
|
109
|
-
} catch (err) {
|
|
110
|
-
dcgLogger(`media: ${file.url} FAILED to process: ${String(err)}`, 'error')
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
dcgLogger(`media: resolve complete, ${out.length}/${files.length} file(s) succeeded`)
|
|
114
|
-
return out
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
type MediaPayload = {
|
|
118
|
-
MediaPath?: string
|
|
119
|
-
MediaFileName?: string
|
|
120
|
-
MediaType?: string
|
|
121
|
-
MediaUrl?: string
|
|
122
|
-
MediaFileNames?: string[]
|
|
123
|
-
MediaPaths?: string[]
|
|
124
|
-
MediaUrls?: string[]
|
|
125
|
-
MediaTypes?: string[]
|
|
126
|
-
}
|
|
127
|
-
function buildMediaPayload(mediaList: MediaInfo[]): MediaPayload {
|
|
128
|
-
if (mediaList.length === 0) return {}
|
|
129
|
-
const first = mediaList[0]
|
|
130
|
-
const mediaFileNames = mediaList.map((m) => m.fileName).filter(Boolean)
|
|
131
|
-
const mediaPaths = mediaList.map((m) => m.path)
|
|
132
|
-
const mediaTypes = mediaList.map((m) => m.contentType).filter(Boolean)
|
|
133
|
-
return {
|
|
134
|
-
MediaPath: first?.path,
|
|
135
|
-
MediaFileName: first?.fileName,
|
|
136
|
-
MediaType: first?.contentType,
|
|
137
|
-
MediaUrl: first?.path,
|
|
138
|
-
MediaFileNames: mediaFileNames.length > 0 ? mediaFileNames : undefined,
|
|
139
|
-
MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
140
|
-
MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
141
|
-
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function resolveReplyMediaList(payload: ReplyPayload): string[] {
|
|
146
|
-
const p = payload as { mediaUrls?: unknown[]; mediaUrl?: unknown }
|
|
147
|
-
if (p.mediaUrls != null && Array.isArray(p.mediaUrls) && p.mediaUrls.length > 0) {
|
|
148
|
-
return normalizeOutboundMediaPaths(p.mediaUrls)
|
|
149
|
-
}
|
|
150
|
-
return normalizeOutboundMediaPaths(p.mediaUrl ?? null)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const typingCallbacks = createTypingCallbacks({
|
|
154
|
-
start: async () => {},
|
|
155
|
-
onStartError: (err) => {
|
|
156
|
-
dcgLogger(`typing start error: ${String(err)}`, 'error')
|
|
157
|
-
}
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* 处理一条用户消息,调用 Agent 并返回回复(同会话串行见 sessionTermination.runInboundTurnSequenced)。
|
|
162
|
-
*/
|
|
163
|
-
export async function handleDcgchatMessage(msg: InboundMessage, accountId: string): Promise<void> {
|
|
164
|
-
const config = getOpenClawConfig()
|
|
165
|
-
if (!config) {
|
|
166
|
-
dcgLogger('no config available', 'error')
|
|
167
|
-
return
|
|
168
|
-
}
|
|
169
|
-
const account = resolveAccount(config, accountId)
|
|
170
|
-
const queueSessionKey = getSessionKey(msg.content, account.accountId)
|
|
171
|
-
const queueText = (msg.content.text ?? '').trim()
|
|
172
|
-
if (stopCommand.includes(queueText)) {
|
|
173
|
-
preemptInboundQueueForStop(queueSessionKey)
|
|
174
|
-
}
|
|
175
|
-
await runInboundTurnSequenced(queueSessionKey, () => handleDcgchatMessageInboundTurn(msg, accountId))
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async function handleDcgchatMessageInboundTurn(msg: InboundMessage, accountId: string): Promise<void> {
|
|
179
|
-
const config = getOpenClawConfig()
|
|
180
|
-
if (!config) {
|
|
181
|
-
return
|
|
182
|
-
}
|
|
183
|
-
const account = resolveAccount(config, accountId)
|
|
184
|
-
const userId = msg._userId.toString()
|
|
185
|
-
|
|
186
|
-
const core = getDcgchatRuntime()
|
|
187
|
-
|
|
188
|
-
const conversationId = msg.content.session_id?.trim()
|
|
189
|
-
const real_mobook = msg.content.real_mobook?.toString().trim()
|
|
190
|
-
|
|
191
|
-
const route = core.channel.routing.resolveAgentRoute({
|
|
192
|
-
cfg: config,
|
|
193
|
-
channel: "dcgchat",
|
|
194
|
-
accountId: account.accountId,
|
|
195
|
-
peer: { kind: 'direct', id: conversationId }
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
const embeddedAgentId = extractAgentIdFromConversationId(conversationId)
|
|
199
|
-
|
|
200
|
-
const effectiveAgentId = embeddedAgentId ?? route.agentId
|
|
201
|
-
const dcgSessionKey = getSessionKey(msg.content, account.accountId)
|
|
202
|
-
const inboundGenAtStart = bumpInboundGeneration(dcgSessionKey)
|
|
203
|
-
|
|
204
|
-
const mergedParams = {
|
|
205
|
-
userId: msg._userId,
|
|
206
|
-
botToken: msg.content.bot_token,
|
|
207
|
-
sessionId: conversationId,
|
|
208
|
-
messageId: msg.content.message_id,
|
|
209
|
-
domainId: msg.content.domain_id,
|
|
210
|
-
appId: config.channels?.["dcgchat"]?.appId || 100,
|
|
211
|
-
botId: msg.content.bot_id ?? '',
|
|
212
|
-
agentId: msg.content.agent_id ?? '',
|
|
213
|
-
sessionKey: dcgSessionKey,
|
|
214
|
-
real_mobook
|
|
215
|
-
}
|
|
216
|
-
/** 写入本条消息参数前快照:流式/abort 的 final 须对齐「上一轮」触发的对话 messageId,而非打断指令本身 */
|
|
217
|
-
const priorOutboundCtx = getEffectiveMsgParams(dcgSessionKey)
|
|
218
|
-
setParamsMessage(dcgSessionKey, mergedParams)
|
|
219
|
-
dcgLogger(`target alias bound: aliasTarget=${userId} -> sessionKey=${dcgSessionKey}`)
|
|
220
|
-
const outboundCtx = getEffectiveMsgParams(dcgSessionKey)
|
|
221
|
-
const agentEntry =
|
|
222
|
-
effectiveAgentId && effectiveAgentId !== 'main' ? config.agents?.list?.find((a) => a.id === effectiveAgentId) : undefined
|
|
223
|
-
const agentDisplayName = agentEntry?.name ?? (effectiveAgentId && effectiveAgentId !== 'main' ? effectiveAgentId : undefined)
|
|
224
|
-
|
|
225
|
-
let text = msg.content.text?.trim()
|
|
226
|
-
|
|
227
|
-
if (!text) {
|
|
228
|
-
sendTextMsg('你需要我帮你做什么呢?', outboundCtx)
|
|
229
|
-
sendFinal(outboundCtx, 'not text')
|
|
230
|
-
setMsgStatus(dcgSessionKey, 'finished')
|
|
231
|
-
return
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
try {
|
|
235
|
-
if (msg.content.skills_scope.length > 0 && !msg.content?.agent_clone_code && !ignoreToolCommand.includes(text?.trim())) {
|
|
236
|
-
const workspaceDir = getWorkspaceDir()
|
|
237
|
-
const skill = msg.content.skills_scope[0]
|
|
238
|
-
const skillDir = `${workspaceDir}/skills/${skill.skill_code}`
|
|
239
|
-
const skillText = `用户选择使用此技能:"${skill.skill_code}",技能路径是:"${skillDir}",在目录下查找并`
|
|
240
|
-
text = skill.skill_code ? `${skillText} ${text}` : text
|
|
241
|
-
dcgLogger(`skill: text: ${text}`)
|
|
242
|
-
}
|
|
243
|
-
// 处理用户上传的文件
|
|
244
|
-
const files = msg.content.files ?? []
|
|
245
|
-
let mediaPayload: Record<string, unknown> = {}
|
|
246
|
-
if (files.length > 0) {
|
|
247
|
-
const mediaList = await resolveMediaFromUrls(files, msg.content.bot_token)
|
|
248
|
-
mediaPayload = buildMediaPayload(mediaList)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config)
|
|
252
|
-
const messageBody = text
|
|
253
|
-
const bodyFormatted = core.channel.reply.formatAgentEnvelope({
|
|
254
|
-
channel: '书灵墨宝',
|
|
255
|
-
from: userId,
|
|
256
|
-
timestamp: new Date(),
|
|
257
|
-
envelope: envelopeOptions,
|
|
258
|
-
body: messageBody
|
|
259
|
-
})
|
|
260
|
-
|
|
261
|
-
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
262
|
-
Body: bodyFormatted,
|
|
263
|
-
RawBody: text,
|
|
264
|
-
CommandBody: text,
|
|
265
|
-
From: userId,
|
|
266
|
-
To: dcgSessionKey,
|
|
267
|
-
SessionKey: dcgSessionKey,
|
|
268
|
-
AccountId: route.accountId,
|
|
269
|
-
ChatType: 'direct',
|
|
270
|
-
SenderName: agentDisplayName,
|
|
271
|
-
SenderId: userId,
|
|
272
|
-
Provider: "dcgchat",
|
|
273
|
-
Surface: "dcgchat",
|
|
274
|
-
MessageSid: msg.content.message_id,
|
|
275
|
-
Timestamp: Date.now(),
|
|
276
|
-
WasMentioned: true,
|
|
277
|
-
CommandAuthorized: true,
|
|
278
|
-
OriginatingChannel: "dcgchat",
|
|
279
|
-
OriginatingTo: dcgSessionKey,
|
|
280
|
-
Target: dcgSessionKey,
|
|
281
|
-
SourceTarget: dcgSessionKey,
|
|
282
|
-
...mediaPayload
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
const sentMediaKeys = new Set<string>()
|
|
286
|
-
const getMediaKey = (url: string) => url.split(/[\\/]/).pop() ?? url
|
|
287
|
-
/** 与 Feishu snapshot 模式一致:payload.text 为当前轮助手全文快照,据此算增量,避免工具前后快照变短或非单调时丢字 */
|
|
288
|
-
let lastStreamSnapshot = ''
|
|
289
|
-
|
|
290
|
-
if (msg.content.skills_scope.length > 0 && !msg.content?.agent_clone_code && !ignoreToolCommand.includes(text?.trim())) {
|
|
291
|
-
const workspaceDir = getWorkspaceDir()
|
|
292
|
-
const skill = msg.content.skills_scope[0]
|
|
293
|
-
const skillDir = `${workspaceDir}/skills/${skill.skill_code}`
|
|
294
|
-
const skillText = `用户选择使用此技能:"${skill.skill_code}",技能路径是:"${skillDir}",在目录下查找并`
|
|
295
|
-
text = skill.skill_code ? `${skillText} ${text}` : text
|
|
296
|
-
dcgLogger(`skill: text: ${text}`)
|
|
297
|
-
}
|
|
298
|
-
const prefixContext = createReplyPrefixContext({
|
|
299
|
-
cfg: config,
|
|
300
|
-
agentId: effectiveAgentId ?? '',
|
|
301
|
-
channel: "dcgchat",
|
|
302
|
-
accountId: account.accountId
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
|
|
306
|
-
responsePrefix: prefixContext.responsePrefix,
|
|
307
|
-
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
308
|
-
humanDelay: core.channel.reply.resolveHumanDelayConfig(config, route.agentId),
|
|
309
|
-
onReplyStart: async () => {},
|
|
310
|
-
deliver: async (payload: ReplyPayload, info) => {
|
|
311
|
-
if (isSessionStreamSuppressed(dcgSessionKey)) return
|
|
312
|
-
const mediaList = resolveReplyMediaList(payload)
|
|
313
|
-
for (const mediaUrl of mediaList) {
|
|
314
|
-
const key = getMediaKey(mediaUrl)
|
|
315
|
-
if (sentMediaKeys.has(key)) continue
|
|
316
|
-
sentMediaKeys.add(key)
|
|
317
|
-
await sendDcgchatMedia({ sessionKey: dcgSessionKey, mediaUrl, text: '' })
|
|
318
|
-
}
|
|
319
|
-
dcgLogger(`[deliver]: len=${payload?.text?.length} sessionId=${outboundCtx.sessionId} ${formatText(payload?.text ?? '')}`)
|
|
320
|
-
},
|
|
321
|
-
onError: (err: unknown, info: { kind: string }) => {
|
|
322
|
-
setMsgStatus(dcgSessionKey, 'finished')
|
|
323
|
-
sendFinal(outboundCtx, 'error')
|
|
324
|
-
clearActiveRunIdForSession(dcgSessionKey)
|
|
325
|
-
streamChunkIdxBySessionKey.delete(dcgSessionKey)
|
|
326
|
-
const suppressed = isSessionStreamSuppressed(dcgSessionKey)
|
|
327
|
-
dcgLogger(`${info.kind} reply failed${suppressed ? ' (stream suppressed)' : ''}: ${String(err)}`, 'error')
|
|
328
|
-
},
|
|
329
|
-
onIdle: () => {
|
|
330
|
-
typingCallbacks.onIdle?.()
|
|
331
|
-
}
|
|
332
|
-
})
|
|
333
|
-
|
|
334
|
-
let dispatchAbort: AbortController | undefined
|
|
335
|
-
try {
|
|
336
|
-
if (!stopCommand.includes(text?.trim())) {
|
|
337
|
-
streamChunkIdxBySessionKey.set(dcgSessionKey, 0)
|
|
338
|
-
dispatchAbort = await beginSupersedingUserTurn(dcgSessionKey)
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (systemCommand.includes(text?.trim())) {
|
|
342
|
-
dcgLogger(`dispatching ${text?.trim()}`)
|
|
343
|
-
await core.channel.reply.withReplyDispatcher({
|
|
344
|
-
dispatcher,
|
|
345
|
-
onSettled: () => markDispatchIdle(),
|
|
346
|
-
run: () =>
|
|
347
|
-
core.channel.reply.dispatchReplyFromConfig({
|
|
348
|
-
ctx: ctxPayload,
|
|
349
|
-
cfg: config,
|
|
350
|
-
dispatcher,
|
|
351
|
-
replyOptions: {
|
|
352
|
-
...replyOptions,
|
|
353
|
-
abortSignal: dispatchAbort!.signal,
|
|
354
|
-
onModelSelected: prefixContext.onModelSelected,
|
|
355
|
-
onAgentRunStart: (runId) => {
|
|
356
|
-
setActiveRunIdForSession(dcgSessionKey, runId)
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
})
|
|
360
|
-
})
|
|
361
|
-
} else if (stopCommand.includes(text?.trim())) {
|
|
362
|
-
const ctxForAbort = priorOutboundCtx.messageId?.trim() || priorOutboundCtx.sessionId?.trim() ? priorOutboundCtx : outboundCtx
|
|
363
|
-
await interruptLocalDispatchAndGateway(dcgSessionKey, ctxForAbort)
|
|
364
|
-
streamChunkIdxBySessionKey.delete(dcgSessionKey)
|
|
365
|
-
clearSentMediaKeys(msg.content.message_id)
|
|
366
|
-
setMsgStatus(dcgSessionKey, 'finished')
|
|
367
|
-
clearParamsMessage(dcgSessionKey)
|
|
368
|
-
sendText('会话已终止', outboundCtx)
|
|
369
|
-
sendFinal(outboundCtx, 'stop')
|
|
370
|
-
return
|
|
371
|
-
} else {
|
|
372
|
-
const params = getEffectiveMsgParams(dcgSessionKey)
|
|
373
|
-
if (!ignoreToolCommand.includes(text?.trim())) {
|
|
374
|
-
wsSendRaw(params, {
|
|
375
|
-
is_finish: -1,
|
|
376
|
-
tool_call_id: Date.now().toString(),
|
|
377
|
-
is_cover: 0,
|
|
378
|
-
thinking_content: JSON.stringify({
|
|
379
|
-
type: 'message_received',
|
|
380
|
-
specialIdentification: 'dcgchat_tool_call_special_identification',
|
|
381
|
-
toolName: '',
|
|
382
|
-
callId: Date.now().toString(),
|
|
383
|
-
params: ''
|
|
384
|
-
}),
|
|
385
|
-
response: ''
|
|
386
|
-
})
|
|
387
|
-
}
|
|
388
|
-
await core.channel.reply.withReplyDispatcher({
|
|
389
|
-
dispatcher,
|
|
390
|
-
onSettled: () => markDispatchIdle(),
|
|
391
|
-
run: () =>
|
|
392
|
-
core.channel.reply.dispatchReplyFromConfig({
|
|
393
|
-
ctx: ctxPayload,
|
|
394
|
-
cfg: config,
|
|
395
|
-
dispatcher,
|
|
396
|
-
replyOptions: {
|
|
397
|
-
...replyOptions,
|
|
398
|
-
abortSignal: dispatchAbort!.signal,
|
|
399
|
-
onModelSelected: prefixContext.onModelSelected,
|
|
400
|
-
onAgentRunStart: (runId) => {
|
|
401
|
-
setActiveRunIdForSession(dcgSessionKey, runId)
|
|
402
|
-
},
|
|
403
|
-
onPartialReply: async (payload: ReplyPayload) => {
|
|
404
|
-
if (isSessionStreamSuppressed(dcgSessionKey)) return
|
|
405
|
-
|
|
406
|
-
// --- Streaming text chunks ---
|
|
407
|
-
if (payload.text) {
|
|
408
|
-
const t = payload.text
|
|
409
|
-
let delta = ''
|
|
410
|
-
if (t.startsWith(lastStreamSnapshot)) {
|
|
411
|
-
delta = t.slice(lastStreamSnapshot.length)
|
|
412
|
-
lastStreamSnapshot = t
|
|
413
|
-
} else if (lastStreamSnapshot.startsWith(t)) {
|
|
414
|
-
// 快照缩短(模型修订等):不重复下发
|
|
415
|
-
} else {
|
|
416
|
-
// 与上一轮快照不衔接(常见于工具后快照从新的助手片段重新开始):整段下发
|
|
417
|
-
delta = t
|
|
418
|
-
lastStreamSnapshot = t
|
|
419
|
-
}
|
|
420
|
-
if (delta.trim()) {
|
|
421
|
-
const prev = streamChunkIdxBySessionKey.get(dcgSessionKey) ?? 0
|
|
422
|
-
streamChunkIdxBySessionKey.set(dcgSessionKey, prev + 1)
|
|
423
|
-
sendChunk(delta, outboundCtx, prev)
|
|
424
|
-
}
|
|
425
|
-
} else {
|
|
426
|
-
dcgLogger(`onPartialReply no text (media/tool metadata): ${JSON.stringify(payload)}`)
|
|
427
|
-
}
|
|
428
|
-
// --- Media from payload ---
|
|
429
|
-
const mediaList = resolveReplyMediaList(payload)
|
|
430
|
-
for (const mediaUrl of mediaList) {
|
|
431
|
-
const key = getMediaKey(mediaUrl)
|
|
432
|
-
if (sentMediaKeys.has(key)) continue
|
|
433
|
-
sentMediaKeys.add(key)
|
|
434
|
-
await sendDcgchatMedia({ sessionKey: dcgSessionKey, mediaUrl, text: '' })
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
})
|
|
439
|
-
})
|
|
440
|
-
}
|
|
441
|
-
} catch (err: unknown) {
|
|
442
|
-
dcgLogger(`handleDcgchatMessage error: ${String(err)}`, 'error')
|
|
443
|
-
if ((inboundGenerationBySessionKey.get(dcgSessionKey) ?? 0) === inboundGenAtStart) {
|
|
444
|
-
setMsgStatus(dcgSessionKey, 'finished')
|
|
445
|
-
}
|
|
446
|
-
} finally {
|
|
447
|
-
releaseDispatchAbortIfCurrent(dcgSessionKey, dispatchAbort)
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
const inboundGenNow = inboundGenerationBySessionKey.get(dcgSessionKey) ?? 0
|
|
451
|
-
if (inboundGenNow !== inboundGenAtStart) {
|
|
452
|
-
dcgLogger(`skip post-reply tail: sessionKey=${dcgSessionKey} (stale handler: inbound gen ${inboundGenAtStart}→${inboundGenNow})`)
|
|
453
|
-
return
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (![...systemCommand, ...stopCommand].includes(text?.trim())) {
|
|
457
|
-
if (isSessionStreamSuppressed(dcgSessionKey)) {
|
|
458
|
-
clearSessionStreamSuppression(dcgSessionKey)
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
clearSentMediaKeys(msg.content.message_id)
|
|
462
|
-
const storePath = core.channel.session.resolveStorePath(config.session?.store)
|
|
463
|
-
await waitUntilSubagentsIdle(dcgSessionKey, { timeoutMs: 600_000 })
|
|
464
|
-
// 等待子 agent 期间可能已有新入站消息(generation 已变),不能再 end/finished,否则会误结束新轮或放行错挂的正文。
|
|
465
|
-
if ((inboundGenerationBySessionKey.get(dcgSessionKey) ?? 0) !== inboundGenAtStart) {
|
|
466
|
-
dcgLogger(
|
|
467
|
-
`skip post-wait tail: sessionKey=${dcgSessionKey} (stale handler after subagent wait: inbound gen ${inboundGenAtStart}→${inboundGenerationBySessionKey.get(dcgSessionKey)})`
|
|
468
|
-
)
|
|
469
|
-
return
|
|
470
|
-
}
|
|
471
|
-
clearActiveRunIdForSession(dcgSessionKey)
|
|
472
|
-
setMsgStatus(dcgSessionKey, 'finished')
|
|
473
|
-
sendFinal(outboundCtx, 'end')
|
|
474
|
-
dcgLogger(`record session route: updateLastRoute.to=${dcgSessionKey}, accountId=${route.accountId}`)
|
|
475
|
-
core.channel.session
|
|
476
|
-
.recordInboundSession({
|
|
477
|
-
storePath,
|
|
478
|
-
sessionKey: dcgSessionKey,
|
|
479
|
-
ctx: ctxPayload,
|
|
480
|
-
updateLastRoute: {
|
|
481
|
-
sessionKey: dcgSessionKey,
|
|
482
|
-
channel: "dcgchat",
|
|
483
|
-
to: dcgSessionKey,
|
|
484
|
-
accountId: route.accountId
|
|
485
|
-
},
|
|
486
|
-
onRecordError: (err) => {
|
|
487
|
-
dcgLogger(` session record error: ${String(err)}`, 'error')
|
|
488
|
-
}
|
|
489
|
-
})
|
|
490
|
-
.catch((err: unknown) => {
|
|
491
|
-
dcgLogger(` recordInboundSession failed: ${String(err)}`, 'error')
|
|
492
|
-
})
|
|
493
|
-
} catch (err) {
|
|
494
|
-
dcgLogger(` handle message failed: ${String(err)}`, 'error')
|
|
495
|
-
if ((inboundGenerationBySessionKey.get(dcgSessionKey) ?? 0) === inboundGenAtStart) {
|
|
496
|
-
setMsgStatus(dcgSessionKey, 'finished')
|
|
497
|
-
}
|
|
498
|
-
sendError(err instanceof Error ? err.message : String(err), outboundCtx)
|
|
499
|
-
}
|
|
500
|
-
}
|