@dcrays/dcgchat-test 0.6.12 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +306 -0
- package/openclaw.plugin.json +42 -12
- package/package.json +8 -35
- package/README.md +0 -167
- package/index.ts +0 -35
- package/src/agent.ts +0 -568
- package/src/bot.ts +0 -646
- package/src/channel/index.ts +0 -329
- package/src/channel/outboundMedia.ts +0 -144
- package/src/channel/outboundTarget.ts +0 -62
- package/src/channel/uploadMediaUrl.ts +0 -76
- package/src/cron/message.ts +0 -114
- package/src/cron/params.ts +0 -164
- package/src/cron/toolGuard.ts +0 -466
- package/src/cron/types.ts +0 -15
- package/src/gateway/index.ts +0 -430
- package/src/gateway/security.ts +0 -95
- package/src/gateway/socket.ts +0 -256
- 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 -269
- package/src/request/api.ts +0 -80
- package/src/request/oss.ts +0 -200
- package/src/request/request.ts +0 -191
- package/src/request/userInfo.ts +0 -44
- package/src/session.ts +0 -28
- package/src/skill.ts +0 -114
- package/src/tool.ts +0 -603
- package/src/tools/messageTool.ts +0 -334
- package/src/tools/toolCallGuard.ts +0 -327
- package/src/transport.ts +0 -217
- package/src/types.ts +0 -139
- package/src/utils/agentErrors.ts +0 -116
- package/src/utils/constant.ts +0 -60
- package/src/utils/env-config.ts +0 -19
- package/src/utils/formatLlmInputEvent.ts +0 -48
- package/src/utils/gatewayMsgHandler.ts +0 -147
- package/src/utils/global.ts +0 -309
- package/src/utils/inboundTurnState.ts +0 -66
- package/src/utils/log.ts +0 -77
- package/src/utils/mediaAttached.ts +0 -244
- package/src/utils/mediaEmitter.ts +0 -54
- package/src/utils/outboundAssistantText.ts +0 -117
- package/src/utils/params.ts +0 -104
- package/src/utils/passTxt.ts +0 -4
- package/src/utils/resolveRegisterConfig.ts +0 -33
- package/src/utils/searchFile.ts +0 -228
- package/src/utils/sessionState.ts +0 -137
- package/src/utils/sessionTermination.ts +0 -228
- package/src/utils/streamMerge.ts +0 -150
- package/src/utils/subagentRunMap.ts +0 -71
- package/src/utils/undiciFetchInterceptor.ts +0 -346
- package/src/utils/workspaceFilePaths.ts +0 -18
- package/src/utils/wsMessageHandler.ts +0 -124
- package/src/utils/zipExtract.ts +0 -97
- package/src/utils/zipPath.ts +0 -24
package/src/bot.ts
DELETED
|
@@ -1,646 +0,0 @@
|
|
|
1
|
-
import path from 'node:path'
|
|
2
|
-
import type { OpenClawConfig, ReplyPayload } from 'openclaw/plugin-sdk'
|
|
3
|
-
import { createReplyPrefixContext, createTypingCallbacks } from 'openclaw/plugin-sdk/channel-reply-pipeline'
|
|
4
|
-
import type { IMsgParams, InboundMessage } from './types.js'
|
|
5
|
-
import {
|
|
6
|
-
formatText,
|
|
7
|
-
getSessionKey,
|
|
8
|
-
getWorkspaceDir,
|
|
9
|
-
isReviserExpert,
|
|
10
|
-
setMsgStatus,
|
|
11
|
-
clearSentMediaKeys,
|
|
12
|
-
removeAllCronMessageIdsForSession,
|
|
13
|
-
takeApartSessionKey,
|
|
14
|
-
getDcgchatRuntime,
|
|
15
|
-
getOpenClawConfig,
|
|
16
|
-
isCollectionReviewer
|
|
17
|
-
} from './utils/global.js'
|
|
18
|
-
import { resolveAccount } from './channel/index.js'
|
|
19
|
-
import { ensureExpertAgentWorkspaceBinding, isAgentInstalled, isAgentInstalling, retryDownloadAgent, waitForAgentInstall } from './agent.js'
|
|
20
|
-
import { sendFinal, sendText as sendTextMsg, sendError, sendText } from './transport.js'
|
|
21
|
-
import { dcgLogger } from './utils/log.js'
|
|
22
|
-
import { agentErrorUserHint } from './utils/agentErrors.js'
|
|
23
|
-
import { isInsufficientBalanceAssistantText } from './utils/passTxt.js'
|
|
24
|
-
import { systemCommand, stopCommand, ignoreToolCommand, CHANNEL_ID, AFTER_STOP_SETTLE_MS, SUBAGENT_IDLE_TIMEOUT_MS } from './utils/constant.js'
|
|
25
|
-
import { clearParamsMessage, getEffectiveMsgParams, setParamsMessage } from './utils/params.js'
|
|
26
|
-
import { resetSubagentStateForRequesterSession, waitUntilSubagentsIdle, stringifyToolHookPayload, sendToolCallMessage } from './tool.js'
|
|
27
|
-
import { createStreamMergeController } from './utils/streamMerge.js'
|
|
28
|
-
import { createMediaEmitter } from './utils/mediaEmitter.js'
|
|
29
|
-
import {
|
|
30
|
-
beginDispatchAbortForInboundTurn,
|
|
31
|
-
clearActiveRunIdForSession,
|
|
32
|
-
clearSessionStreamSuppression,
|
|
33
|
-
isSessionStreamSuppressed,
|
|
34
|
-
releaseDispatchAbortIfCurrent,
|
|
35
|
-
requestStopInterruption,
|
|
36
|
-
runInboundTurnSequenced,
|
|
37
|
-
setActiveRunIdForSession,
|
|
38
|
-
waitForStopInterruption
|
|
39
|
-
} from './utils/sessionTermination.js'
|
|
40
|
-
import {
|
|
41
|
-
buildAgentInboundBodySuffix,
|
|
42
|
-
buildDcgchatInboundMediaPayload,
|
|
43
|
-
normalizeInboundFileList,
|
|
44
|
-
resolveMediaFromUrls,
|
|
45
|
-
type DcgchatInboundMediaInfo
|
|
46
|
-
} from './utils/mediaAttached.js'
|
|
47
|
-
import {
|
|
48
|
-
bumpInboundGeneration,
|
|
49
|
-
clearStopSeqForSession,
|
|
50
|
-
deleteStreamChunkIdxForSession,
|
|
51
|
-
getInboundGeneration,
|
|
52
|
-
getStopSeq,
|
|
53
|
-
resetStreamChunkIdxForSession
|
|
54
|
-
} from './utils/inboundTurnState.js'
|
|
55
|
-
import {
|
|
56
|
-
clearOutboundTextState,
|
|
57
|
-
emitOutboundAssistantText,
|
|
58
|
-
outboundHasEmittedAssistantText,
|
|
59
|
-
outboundHasStreamedAssistantText,
|
|
60
|
-
resetOutboundTextState,
|
|
61
|
-
resetOutboundTextSnapshot
|
|
62
|
-
} from './utils/outboundAssistantText.js'
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* 本轮入站**已正常收尾**时清理该 sessionKey 的内存态(params、去重、分片、msgStatus、子 agent 本地跟踪等)。
|
|
66
|
-
* 不可在「stale」或已有新入站占用的场景调用,否则会删掉新一轮写入的 `paramsMessageMap`。
|
|
67
|
-
* 保留入站 generation(单调计数,见 inboundTurnState);下一轮 turn 开始时会 bump,旧 handler 用 gen 不等判定 stale。
|
|
68
|
-
*/
|
|
69
|
-
function clearDcgSessionKeyCachesForCompletedTurn(dcgSessionKey: string, messageId: string | undefined) {
|
|
70
|
-
clearParamsMessage(dcgSessionKey)
|
|
71
|
-
deleteStreamChunkIdxForSession(dcgSessionKey)
|
|
72
|
-
clearSessionStreamSuppression(dcgSessionKey)
|
|
73
|
-
clearActiveRunIdForSession(dcgSessionKey)
|
|
74
|
-
setMsgStatus(dcgSessionKey, '')
|
|
75
|
-
// 出站媒体去重 bucket:写入时为 `resolveOutboundMediaDedupeKey(messageId, sessionId, sessionKey)` 的结果,
|
|
76
|
-
// 可能是 messageId / sessionId / sessionKey 之一。turn 收尾时按这三者都清一次,覆盖 messageId 为空的兜底场景。
|
|
77
|
-
if (messageId) clearSentMediaKeys(messageId)
|
|
78
|
-
const { sessionId: tookApartSessionId } = takeApartSessionKey(dcgSessionKey)
|
|
79
|
-
if (tookApartSessionId) clearSentMediaKeys(tookApartSessionId)
|
|
80
|
-
clearSentMediaKeys(dcgSessionKey)
|
|
81
|
-
// cronMessageIdMap:sendDcgchatMedia 兜底路径会写一条永不消费的 entry(见 global.removeAllCronMessageIdsForSession 注释),
|
|
82
|
-
// turn 收尾时整条队列即可视为失效。
|
|
83
|
-
removeAllCronMessageIdsForSession(dcgSessionKey)
|
|
84
|
-
resetSubagentStateForRequesterSession(dcgSessionKey)
|
|
85
|
-
clearOutboundTextState(dcgSessionKey)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const DEFAULT_AGENT_ID = 'main'
|
|
89
|
-
const VALID_AGENT_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i
|
|
90
|
-
const INVALID_AGENT_ID_CHARS_RE = /[^a-z0-9_-]+/g
|
|
91
|
-
const LEADING_AGENT_ID_DASH_RE = /^-+/
|
|
92
|
-
const TRAILING_AGENT_ID_DASH_RE = /-+$/
|
|
93
|
-
|
|
94
|
-
type HumanDelayConfig = NonNullable<NonNullable<NonNullable<OpenClawConfig['agents']>['defaults']>['humanDelay']>
|
|
95
|
-
|
|
96
|
-
function normalizeDcgAgentId(value: string | undefined): string {
|
|
97
|
-
const trimmed = (value ?? '').trim()
|
|
98
|
-
if (!trimmed) return DEFAULT_AGENT_ID
|
|
99
|
-
const normalized = trimmed.toLowerCase()
|
|
100
|
-
if (VALID_AGENT_ID_RE.test(trimmed)) return normalized
|
|
101
|
-
return (
|
|
102
|
-
normalized.replace(INVALID_AGENT_ID_CHARS_RE, '-').replace(LEADING_AGENT_ID_DASH_RE, '').replace(TRAILING_AGENT_ID_DASH_RE, '').slice(0, 64) ||
|
|
103
|
-
DEFAULT_AGENT_ID
|
|
104
|
-
)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function resolveDcgHumanDelayConfig(cfg: OpenClawConfig, agentId: string | undefined): HumanDelayConfig | undefined {
|
|
108
|
-
const defaults = cfg.agents?.defaults?.humanDelay
|
|
109
|
-
const normalizedAgentId = normalizeDcgAgentId(agentId)
|
|
110
|
-
const overrides = cfg.agents?.list?.find((entry) => normalizeDcgAgentId(entry.id) === normalizedAgentId)?.humanDelay
|
|
111
|
-
if (!defaults && !overrides) return undefined
|
|
112
|
-
return {
|
|
113
|
-
mode: overrides?.mode ?? defaults?.mode,
|
|
114
|
-
minMs: overrides?.minMs ?? defaults?.minMs,
|
|
115
|
-
maxMs: overrides?.maxMs ?? defaults?.maxMs
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const typingCallbacks = createTypingCallbacks({
|
|
120
|
-
start: async () => {},
|
|
121
|
-
onStartError: (err) => {
|
|
122
|
-
dcgLogger(`typing start error: ${String(err)}`, 'error')
|
|
123
|
-
}
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
function sendAbortFinalForInterruptedTurn(ctx: IMsgParams, stopMessageId: string | undefined, sessionKey: string): void {
|
|
127
|
-
const interruptedMessageId = ctx.messageId?.trim()
|
|
128
|
-
if (!interruptedMessageId || interruptedMessageId === stopMessageId?.trim()) return
|
|
129
|
-
sendFinal(ctx, 'abort')
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** `/stop` 是 control message:立即 ack 当前消息,再用 stop barrier 终止旧 run 并阻塞后续普通消息。 */
|
|
133
|
-
async function handleStopControlMessage(msg: InboundMessage, accountId: string, interruptedOutboundCtx: IMsgParams | undefined): Promise<void> {
|
|
134
|
-
const config = getOpenClawConfig()
|
|
135
|
-
if (!config) {
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
const account = resolveAccount(config, accountId)
|
|
139
|
-
const dcgSessionKey = getSessionKey(msg.content, account.accountId)
|
|
140
|
-
const conversationId = msg.content.session_id?.trim()
|
|
141
|
-
const mergedParams = {
|
|
142
|
-
userId: msg._userId,
|
|
143
|
-
botToken: msg.content.bot_token,
|
|
144
|
-
sessionId: conversationId,
|
|
145
|
-
messageId: msg.content.message_id,
|
|
146
|
-
domainId: msg.content.domain_id,
|
|
147
|
-
appId: config.channels?.[CHANNEL_ID]?.appId || 100,
|
|
148
|
-
botId: msg.content.bot_id ?? '',
|
|
149
|
-
agentId: msg.content.agent_id ?? '',
|
|
150
|
-
sessionKey: dcgSessionKey,
|
|
151
|
-
real_mobook: msg.content.real_mobook?.toString().trim()
|
|
152
|
-
}
|
|
153
|
-
setParamsMessage(dcgSessionKey, mergedParams)
|
|
154
|
-
const outboundCtx = getEffectiveMsgParams(dcgSessionKey)
|
|
155
|
-
sendFinal(outboundCtx, 'stop')
|
|
156
|
-
const { started, barrier } = requestStopInterruption(dcgSessionKey)
|
|
157
|
-
if (started && interruptedOutboundCtx) {
|
|
158
|
-
sendAbortFinalForInterruptedTurn(interruptedOutboundCtx, msg.content.message_id, dcgSessionKey)
|
|
159
|
-
}
|
|
160
|
-
dcgLogger(`stop control ack: sessionKey=${dcgSessionKey} messageId=${msg.content.message_id} started=${started}`)
|
|
161
|
-
await barrier
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* 处理一条用户消息,调用 Agent 并返回回复(同会话串行见 sessionTermination.runInboundTurnSequenced)。
|
|
166
|
-
*/
|
|
167
|
-
export async function handleDcgchatMessage(msg: InboundMessage, accountId: string): Promise<void> {
|
|
168
|
-
const config = getOpenClawConfig()
|
|
169
|
-
if (!config) {
|
|
170
|
-
dcgLogger('no OpenClaw config available', 'error')
|
|
171
|
-
return
|
|
172
|
-
}
|
|
173
|
-
const account = resolveAccount(config, accountId)
|
|
174
|
-
const queueSessionKey = getSessionKey(msg.content, account.accountId)
|
|
175
|
-
const queueText = (msg.content.text ?? '').trim()
|
|
176
|
-
const isStopInbound = stopCommand.includes(queueText)
|
|
177
|
-
|
|
178
|
-
const interruptedOutboundCtx = isStopInbound ? getEffectiveMsgParams(queueSessionKey) : undefined
|
|
179
|
-
if (isStopInbound) {
|
|
180
|
-
await handleStopControlMessage(msg, accountId, interruptedOutboundCtx)
|
|
181
|
-
return
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
await waitForStopInterruption(queueSessionKey)
|
|
185
|
-
await runInboundTurnSequenced(queueSessionKey, () => handleDcgchatMessageInboundTurn(msg, accountId))
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async function handleDcgchatMessageInboundTurn(msg: InboundMessage, accountId: string): Promise<void> {
|
|
189
|
-
let config = getOpenClawConfig()
|
|
190
|
-
if (!config) {
|
|
191
|
-
return
|
|
192
|
-
}
|
|
193
|
-
const account = resolveAccount(config, accountId)
|
|
194
|
-
|
|
195
|
-
const core = getDcgchatRuntime()
|
|
196
|
-
|
|
197
|
-
const conversationId = msg.content.session_id?.trim()
|
|
198
|
-
const real_mobook = msg.content.real_mobook?.toString().trim()
|
|
199
|
-
|
|
200
|
-
const agentCloneCode = msg.content?.agent_clone_code?.trim() || ''
|
|
201
|
-
|
|
202
|
-
if (agentCloneCode) {
|
|
203
|
-
await ensureExpertAgentWorkspaceBinding(agentCloneCode)
|
|
204
|
-
const latest = getOpenClawConfig()
|
|
205
|
-
if (latest) config = latest
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
let route = core.channel.routing.resolveAgentRoute({
|
|
209
|
-
cfg: config,
|
|
210
|
-
channel: CHANNEL_ID,
|
|
211
|
-
accountId: account.accountId,
|
|
212
|
-
peer: { kind: 'direct', id: conversationId }
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
let effectiveAgentId = route.agentId
|
|
216
|
-
const dcgSessionKey = getSessionKey(msg.content, account.accountId)
|
|
217
|
-
|
|
218
|
-
const previousStopSeq = getStopSeq(dcgSessionKey)
|
|
219
|
-
|
|
220
|
-
// 每轮入站先卸掉可能残留的流抑制(如 /stop 与 dispatch 收尾竞态),避免 deliver 全程静默 return
|
|
221
|
-
clearSessionStreamSuppression(dcgSessionKey)
|
|
222
|
-
const inboundGenAtStart = bumpInboundGeneration(dcgSessionKey)
|
|
223
|
-
|
|
224
|
-
const isAfterStop = previousStopSeq > 0 && !stopCommand.includes((msg.content.text ?? '').trim())
|
|
225
|
-
|
|
226
|
-
const mergedParams = {
|
|
227
|
-
userId: msg._userId,
|
|
228
|
-
botToken: msg.content.bot_token,
|
|
229
|
-
sessionId: conversationId,
|
|
230
|
-
messageId: msg.content.message_id,
|
|
231
|
-
domainId: msg.content.domain_id,
|
|
232
|
-
appId: config.channels?.[CHANNEL_ID]?.appId || 100,
|
|
233
|
-
botId: msg.content.bot_id ?? '',
|
|
234
|
-
agentId: msg.content.agent_id ?? '',
|
|
235
|
-
sessionKey: dcgSessionKey,
|
|
236
|
-
real_mobook
|
|
237
|
-
}
|
|
238
|
-
setParamsMessage(dcgSessionKey, mergedParams)
|
|
239
|
-
const outboundCtx = getEffectiveMsgParams(dcgSessionKey)
|
|
240
|
-
let agentDisplayName = null
|
|
241
|
-
|
|
242
|
-
let text = msg.content.text?.trim()
|
|
243
|
-
|
|
244
|
-
if (!text) {
|
|
245
|
-
sendTextMsg('你需要我帮你做什么呢?', outboundCtx)
|
|
246
|
-
sendFinal(outboundCtx, 'not text')
|
|
247
|
-
clearDcgSessionKeyCachesForCompletedTurn(dcgSessionKey, msg.content.message_id)
|
|
248
|
-
return
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (stopCommand.includes(text)) {
|
|
252
|
-
sendFinal(outboundCtx, 'stop')
|
|
253
|
-
clearDcgSessionKeyCachesForCompletedTurn(dcgSessionKey, msg.content.message_id)
|
|
254
|
-
return
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// 校验 agent 是否安装。gateway 重连等场景下可能漏掉安装事件,导致
|
|
258
|
-
// agent 文件不存在。若正在安装则等待结果;若未安装或安装失败,利用
|
|
259
|
-
// 缓存的 URL 尝试一次重新下载(只试一次),仍失败则提示网络异常。
|
|
260
|
-
if (agentCloneCode && dcgSessionKey.includes(agentCloneCode) && !isAgentInstalled(agentCloneCode)) {
|
|
261
|
-
let agentReady = false
|
|
262
|
-
|
|
263
|
-
if (isAgentInstalling(agentCloneCode)) {
|
|
264
|
-
const installOk = await waitForAgentInstall(agentCloneCode)
|
|
265
|
-
if (getInboundGeneration(dcgSessionKey) !== inboundGenAtStart) {
|
|
266
|
-
dcgLogger(
|
|
267
|
-
`skip stale install result: sessionKey=${dcgSessionKey} inboundGen=${inboundGenAtStart} current=${getInboundGeneration(dcgSessionKey)}`
|
|
268
|
-
)
|
|
269
|
-
return
|
|
270
|
-
}
|
|
271
|
-
const isInstalled = isAgentInstalled(agentCloneCode)
|
|
272
|
-
if (!installOk || !isInstalled) {
|
|
273
|
-
agentReady = await retryDownloadAgent(agentCloneCode)
|
|
274
|
-
} else {
|
|
275
|
-
agentReady = true
|
|
276
|
-
}
|
|
277
|
-
} else {
|
|
278
|
-
dcgLogger(`retry install because the agent not installed, ${agentCloneCode}`)
|
|
279
|
-
agentReady = await retryDownloadAgent(agentCloneCode)
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (!agentReady || !isAgentInstalled(agentCloneCode)) {
|
|
283
|
-
dcgLogger(`receive agentCloneCode, but agent not ready: ${agentCloneCode}`)
|
|
284
|
-
sendTextMsg('抱歉,网络异常,请稍后重试', outboundCtx, { message_tags: { source: 'agent_not_found' }, is_finish: -1 })
|
|
285
|
-
sendFinal(outboundCtx, 'agent_not_installed')
|
|
286
|
-
clearDcgSessionKeyCachesForCompletedTurn(dcgSessionKey, msg.content.message_id)
|
|
287
|
-
return
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
await ensureExpertAgentWorkspaceBinding(agentCloneCode)
|
|
291
|
-
const latestAfterInstall = getOpenClawConfig()
|
|
292
|
-
if (latestAfterInstall) config = latestAfterInstall
|
|
293
|
-
route = core.channel.routing.resolveAgentRoute({
|
|
294
|
-
cfg: config,
|
|
295
|
-
channel: CHANNEL_ID,
|
|
296
|
-
accountId: account.accountId,
|
|
297
|
-
peer: { kind: 'direct', id: conversationId }
|
|
298
|
-
})
|
|
299
|
-
effectiveAgentId = route.agentId
|
|
300
|
-
agentDisplayName = config.agents?.list?.find((a) => a.id === effectiveAgentId)?.name || effectiveAgentId || null
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
try {
|
|
304
|
-
let dispatchReplyErrorHandledByOnError = false
|
|
305
|
-
if ((msg.content.skills_scope?.length ?? 0) > 0 && !ignoreToolCommand.includes(text?.trim()) && !agentCloneCode) {
|
|
306
|
-
const workspaceDir = getWorkspaceDir()
|
|
307
|
-
const skill = msg.content.skills_scope?.[0]
|
|
308
|
-
if (skill?.skill_code) {
|
|
309
|
-
const skillDir = path.join(workspaceDir, 'skills', skill.skill_code)
|
|
310
|
-
const skillText = `用户选择使用此技能:"${skill.skill_code}",技能路径是:"${skillDir}",在目录下查找并`
|
|
311
|
-
text = `${skillText} ${text}`
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
// 处理用户上传的文件(兼容多种下行字段名与单文件对象)
|
|
315
|
-
const files = normalizeInboundFileList(msg.content as Record<string, unknown>)
|
|
316
|
-
let mediaListResolved: DcgchatInboundMediaInfo[] = []
|
|
317
|
-
let mediaPayload: Record<string, unknown> = {}
|
|
318
|
-
if (files.length > 0) {
|
|
319
|
-
// 从 OSS 下载 附件内容
|
|
320
|
-
mediaListResolved = await resolveMediaFromUrls(files, msg.content.bot_token)
|
|
321
|
-
mediaPayload = buildDcgchatInboundMediaPayload(mediaListResolved)
|
|
322
|
-
if (isCollectionReviewer(dcgSessionKey)) {
|
|
323
|
-
text += `\n files=${JSON.stringify(files)}`
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const agentInboundText = buildAgentInboundBodySuffix(text, mediaListResolved)
|
|
328
|
-
|
|
329
|
-
const ctxPayload = core.channel.inbound.buildContext({
|
|
330
|
-
channel: CHANNEL_ID,
|
|
331
|
-
accountId: route.accountId,
|
|
332
|
-
provider: CHANNEL_ID,
|
|
333
|
-
surface: CHANNEL_ID,
|
|
334
|
-
messageId: msg.content.message_id,
|
|
335
|
-
timestamp: Date.now(),
|
|
336
|
-
from: dcgSessionKey,
|
|
337
|
-
sender: {
|
|
338
|
-
id: dcgSessionKey,
|
|
339
|
-
name: agentDisplayName ?? undefined
|
|
340
|
-
},
|
|
341
|
-
conversation: {
|
|
342
|
-
kind: 'direct',
|
|
343
|
-
id: conversationId ?? dcgSessionKey
|
|
344
|
-
},
|
|
345
|
-
route: {
|
|
346
|
-
agentId: effectiveAgentId ?? '',
|
|
347
|
-
accountId: route.accountId,
|
|
348
|
-
routeSessionKey: dcgSessionKey,
|
|
349
|
-
dispatchSessionKey: dcgSessionKey
|
|
350
|
-
},
|
|
351
|
-
reply: {
|
|
352
|
-
to: dcgSessionKey,
|
|
353
|
-
originatingTo: dcgSessionKey
|
|
354
|
-
},
|
|
355
|
-
message: {
|
|
356
|
-
body: agentInboundText,
|
|
357
|
-
bodyForAgent: agentInboundText,
|
|
358
|
-
rawBody: text,
|
|
359
|
-
commandBody: text
|
|
360
|
-
},
|
|
361
|
-
access: {
|
|
362
|
-
mentions: {
|
|
363
|
-
canDetectMention: true,
|
|
364
|
-
wasMentioned: true
|
|
365
|
-
}
|
|
366
|
-
},
|
|
367
|
-
channelContext: {
|
|
368
|
-
sender: {
|
|
369
|
-
id: dcgSessionKey
|
|
370
|
-
},
|
|
371
|
-
chat: {
|
|
372
|
-
id: conversationId ?? dcgSessionKey,
|
|
373
|
-
sessionId: conversationId,
|
|
374
|
-
domainId: msg.content.domain_id,
|
|
375
|
-
appId: msg.content.app_id,
|
|
376
|
-
botId: msg.content.bot_id,
|
|
377
|
-
agentId: msg.content.agent_id
|
|
378
|
-
}
|
|
379
|
-
},
|
|
380
|
-
media: mediaListResolved.map((m) => ({
|
|
381
|
-
path: m.path,
|
|
382
|
-
contentType: m.contentType || undefined,
|
|
383
|
-
messageId: msg.content.message_id
|
|
384
|
-
})),
|
|
385
|
-
extra: {
|
|
386
|
-
Target: dcgSessionKey,
|
|
387
|
-
SourceTarget: dcgSessionKey,
|
|
388
|
-
SuppressMessageReceivedHooks: true,
|
|
389
|
-
...mediaPayload
|
|
390
|
-
}
|
|
391
|
-
})
|
|
392
|
-
const storePath = core.channel.session.resolveStorePath(config.session?.store, { agentId: effectiveAgentId })
|
|
393
|
-
|
|
394
|
-
const mediaEmitter = createMediaEmitter({
|
|
395
|
-
sessionKey: dcgSessionKey,
|
|
396
|
-
onEmit: () => {
|
|
397
|
-
hasEmittedAssistantMedia = true
|
|
398
|
-
}
|
|
399
|
-
})
|
|
400
|
-
let hasEmittedAssistantMedia = false
|
|
401
|
-
// bot 流式/deliver 与 dcgchat_message 共用 outboundAssistantText 快照,避免双发或漏发
|
|
402
|
-
resetOutboundTextState(dcgSessionKey, inboundGenAtStart)
|
|
403
|
-
let partialTextSnapshot = ''
|
|
404
|
-
const streamMerge = createStreamMergeController({
|
|
405
|
-
sessionKey: dcgSessionKey,
|
|
406
|
-
inboundGenAtStart,
|
|
407
|
-
outboundCtx
|
|
408
|
-
})
|
|
409
|
-
|
|
410
|
-
const prefixContext = createReplyPrefixContext({
|
|
411
|
-
cfg: config,
|
|
412
|
-
agentId: effectiveAgentId ?? '',
|
|
413
|
-
channel: CHANNEL_ID,
|
|
414
|
-
accountId: account.accountId
|
|
415
|
-
})
|
|
416
|
-
|
|
417
|
-
const createDcgDeliveryAdapter = () => ({
|
|
418
|
-
deliver: async (payload: ReplyPayload, info: { kind: string }) => {
|
|
419
|
-
if (getInboundGeneration(dcgSessionKey) !== inboundGenAtStart) return
|
|
420
|
-
if (isSessionStreamSuppressed(dcgSessionKey)) return
|
|
421
|
-
if (isInsufficientBalanceAssistantText(payload?.text)) {
|
|
422
|
-
dcgLogger(`[deliver]: pass text — skip raw insufficient-balance assistant line (plugin llm_output sends user copy)`)
|
|
423
|
-
return
|
|
424
|
-
}
|
|
425
|
-
await mediaEmitter.emitFromPayload(payload)
|
|
426
|
-
// 未真正发出过流式文本时,deliver/final 快照仍应作为正文兜底发送。
|
|
427
|
-
if (payload?.text?.trim() && !outboundHasStreamedAssistantText(dcgSessionKey)) {
|
|
428
|
-
emitOutboundAssistantText(dcgSessionKey, payload.text, outboundCtx, {
|
|
429
|
-
isError: payload.isError === true,
|
|
430
|
-
emitDelta: (delta) => streamMerge.queueDelta(delta)
|
|
431
|
-
})
|
|
432
|
-
}
|
|
433
|
-
const p = payload as Record<string, unknown>
|
|
434
|
-
const mediaCount = Array.isArray(p.mediaUrls) ? p.mediaUrls.length : p.mediaUrl ? 1 : 0
|
|
435
|
-
dcgLogger(
|
|
436
|
-
`[deliver]: kind=${info.kind}, text.length=${payload?.text?.length}, media.length=${mediaCount}, sessionId=${outboundCtx.sessionId}, ${formatText(payload?.text ?? '')}`
|
|
437
|
-
)
|
|
438
|
-
},
|
|
439
|
-
onError: (err: unknown, info: { kind: string }) => {
|
|
440
|
-
if (getInboundGeneration(dcgSessionKey) !== inboundGenAtStart) {
|
|
441
|
-
dcgLogger(`${info.kind} reply failed (stale handler, ignored): ${String(err)}`)
|
|
442
|
-
return
|
|
443
|
-
}
|
|
444
|
-
dispatchReplyErrorHandledByOnError = true
|
|
445
|
-
const suppressed = isSessionStreamSuppressed(dcgSessionKey)
|
|
446
|
-
streamMerge.flush(true)
|
|
447
|
-
const userHint = agentErrorUserHint(err)
|
|
448
|
-
if (!suppressed && userHint) {
|
|
449
|
-
sendText(userHint, outboundCtx)
|
|
450
|
-
}
|
|
451
|
-
sendFinal(outboundCtx, 'error')
|
|
452
|
-
dcgLogger(`${info.kind} reply failed${suppressed ? ' (stream suppressed)' : ''}: ${String(err)}`, 'error')
|
|
453
|
-
clearDcgSessionKeyCachesForCompletedTurn(dcgSessionKey, msg.content.message_id)
|
|
454
|
-
}
|
|
455
|
-
})
|
|
456
|
-
|
|
457
|
-
const createDcgReplyDispatcherOptions = () => ({
|
|
458
|
-
responsePrefix: prefixContext.responsePrefix,
|
|
459
|
-
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
460
|
-
humanDelay: resolveDcgHumanDelayConfig(config, effectiveAgentId),
|
|
461
|
-
onReplyStart: async () => {},
|
|
462
|
-
onIdle: () => {
|
|
463
|
-
typingCallbacks.onIdle?.()
|
|
464
|
-
}
|
|
465
|
-
})
|
|
466
|
-
|
|
467
|
-
// /stop 后立即发下一条时,宿主 reply run 可能尚未从 registry 释放 → 无 llm_input、秒 end。短延迟缓解竞态。
|
|
468
|
-
if (isAfterStop) {
|
|
469
|
-
if (AFTER_STOP_SETTLE_MS > 0) {
|
|
470
|
-
dcgLogger(`after-stop settle: waiting ${AFTER_STOP_SETTLE_MS}ms before dispatch sessionKey=${dcgSessionKey}`)
|
|
471
|
-
await new Promise<void>((r) => setTimeout(r, AFTER_STOP_SETTLE_MS))
|
|
472
|
-
}
|
|
473
|
-
if (getInboundGeneration(dcgSessionKey) !== inboundGenAtStart) return
|
|
474
|
-
clearStopSeqForSession(dcgSessionKey)
|
|
475
|
-
clearSessionStreamSuppression(dcgSessionKey)
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
let dispatchAbort: AbortController | undefined
|
|
479
|
-
try {
|
|
480
|
-
resetStreamChunkIdxForSession(dcgSessionKey, 0)
|
|
481
|
-
dispatchAbort = beginDispatchAbortForInboundTurn(dcgSessionKey)
|
|
482
|
-
if (!ignoreToolCommand.includes(text?.trim())) {
|
|
483
|
-
const { text: thinkingContent, callId } = stringifyToolHookPayload('message_received', {}, {}, 'lifecycle')
|
|
484
|
-
sendToolCallMessage(dcgSessionKey, thinkingContent, callId, 0)
|
|
485
|
-
}
|
|
486
|
-
const dispatchReplyAttempt = async () => {
|
|
487
|
-
const dispatcherOptions = createDcgReplyDispatcherOptions()
|
|
488
|
-
const delivery = createDcgDeliveryAdapter()
|
|
489
|
-
await core.channel.inbound.dispatchReply({
|
|
490
|
-
cfg: config,
|
|
491
|
-
channel: CHANNEL_ID,
|
|
492
|
-
accountId: route.accountId,
|
|
493
|
-
agentId: effectiveAgentId ?? '',
|
|
494
|
-
routeSessionKey: dcgSessionKey,
|
|
495
|
-
storePath,
|
|
496
|
-
ctxPayload,
|
|
497
|
-
recordInboundSession: core.channel.session.recordInboundSession,
|
|
498
|
-
dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
|
|
499
|
-
delivery,
|
|
500
|
-
dispatcherOptions,
|
|
501
|
-
record: {
|
|
502
|
-
updateLastRoute: {
|
|
503
|
-
sessionKey: dcgSessionKey,
|
|
504
|
-
channel: CHANNEL_ID,
|
|
505
|
-
to: dcgSessionKey,
|
|
506
|
-
accountId: route.accountId
|
|
507
|
-
},
|
|
508
|
-
onRecordError: (err) => {
|
|
509
|
-
dcgLogger(` session record error: ${String(err)}`, 'error')
|
|
510
|
-
}
|
|
511
|
-
},
|
|
512
|
-
messageId: msg.content.message_id,
|
|
513
|
-
replyOptions: {
|
|
514
|
-
abortSignal: dispatchAbort!.signal,
|
|
515
|
-
onModelSelected: prefixContext.onModelSelected,
|
|
516
|
-
onAgentRunStart: (runId) => {
|
|
517
|
-
setActiveRunIdForSession(dcgSessionKey, runId)
|
|
518
|
-
},
|
|
519
|
-
onPartialReply: async (payload: ReplyPayload) => {
|
|
520
|
-
if (getInboundGeneration(dcgSessionKey) !== inboundGenAtStart) return
|
|
521
|
-
if (isSessionStreamSuppressed(dcgSessionKey)) return
|
|
522
|
-
if (isInsufficientBalanceAssistantText(payload.text)) {
|
|
523
|
-
return
|
|
524
|
-
}
|
|
525
|
-
const p = payload as Record<string, unknown>
|
|
526
|
-
// --- Streaming text chunks / deltas ---
|
|
527
|
-
if (p.replace === true) {
|
|
528
|
-
resetOutboundTextSnapshot(dcgSessionKey)
|
|
529
|
-
streamMerge.flush(true)
|
|
530
|
-
if (typeof p.text === 'string' && p.text.trim()) {
|
|
531
|
-
partialTextSnapshot = p.text
|
|
532
|
-
emitOutboundAssistantText(dcgSessionKey, p.text, outboundCtx, {
|
|
533
|
-
markStreamed: true,
|
|
534
|
-
isError: payload.isError === true,
|
|
535
|
-
emitDelta: (delta) => streamMerge.queueDelta(delta)
|
|
536
|
-
})
|
|
537
|
-
}
|
|
538
|
-
} else if (typeof p.delta === 'string' && p.delta) {
|
|
539
|
-
partialTextSnapshot += p.delta
|
|
540
|
-
emitOutboundAssistantText(dcgSessionKey, partialTextSnapshot, outboundCtx, {
|
|
541
|
-
markStreamed: true,
|
|
542
|
-
isError: payload.isError === true,
|
|
543
|
-
emitDelta: (delta) => streamMerge.queueDelta(delta)
|
|
544
|
-
})
|
|
545
|
-
} else if (payload.text) {
|
|
546
|
-
partialTextSnapshot = payload.text
|
|
547
|
-
emitOutboundAssistantText(dcgSessionKey, payload.text, outboundCtx, {
|
|
548
|
-
markStreamed: true,
|
|
549
|
-
isError: payload.isError === true,
|
|
550
|
-
emitDelta: (delta) => streamMerge.queueDelta(delta)
|
|
551
|
-
})
|
|
552
|
-
} else {
|
|
553
|
-
dcgLogger(`[onPartialReply]: no text (media/tool metadata) keys=${Object.keys(p).join(',')}`)
|
|
554
|
-
}
|
|
555
|
-
// --- Media from payload ---
|
|
556
|
-
await mediaEmitter.emitFromPayload(payload)
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
})
|
|
560
|
-
}
|
|
561
|
-
await dispatchReplyAttempt()
|
|
562
|
-
streamMerge.flush(true)
|
|
563
|
-
} catch (err: unknown) {
|
|
564
|
-
dcgLogger(`handleDcgchatMessage error: ${String(err)}`, 'error')
|
|
565
|
-
if (getInboundGeneration(dcgSessionKey) === inboundGenAtStart && !dispatchReplyErrorHandledByOnError) {
|
|
566
|
-
streamMerge.flush(true)
|
|
567
|
-
if (!isSessionStreamSuppressed(dcgSessionKey)) {
|
|
568
|
-
sendText(agentErrorUserHint(err) ?? '抱歉,这次对话执行时发生异常,请稍后重试。', outboundCtx)
|
|
569
|
-
}
|
|
570
|
-
sendFinal(outboundCtx, 'error')
|
|
571
|
-
releaseDispatchAbortIfCurrent(dcgSessionKey, dispatchAbort)
|
|
572
|
-
streamMerge.dispose()
|
|
573
|
-
clearDcgSessionKeyCachesForCompletedTurn(dcgSessionKey, msg.content.message_id)
|
|
574
|
-
return
|
|
575
|
-
}
|
|
576
|
-
if (getInboundGeneration(dcgSessionKey) === inboundGenAtStart && dispatchReplyErrorHandledByOnError) {
|
|
577
|
-
releaseDispatchAbortIfCurrent(dcgSessionKey, dispatchAbort)
|
|
578
|
-
streamMerge.dispose()
|
|
579
|
-
return
|
|
580
|
-
}
|
|
581
|
-
} finally {
|
|
582
|
-
if (getInboundGeneration(dcgSessionKey) !== inboundGenAtStart) {
|
|
583
|
-
releaseDispatchAbortIfCurrent(dcgSessionKey, dispatchAbort)
|
|
584
|
-
streamMerge.dispose()
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
const inboundGenNow = getInboundGeneration(dcgSessionKey)
|
|
589
|
-
if (inboundGenNow !== inboundGenAtStart) {
|
|
590
|
-
streamMerge.dispose()
|
|
591
|
-
dcgLogger(`skip post-reply tail: sessionKey=${dcgSessionKey} (stale handler: inbound gen ${inboundGenAtStart}→${inboundGenNow})`)
|
|
592
|
-
return
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
if (![...systemCommand, ...stopCommand].includes(text?.trim())) {
|
|
596
|
-
if (isSessionStreamSuppressed(dcgSessionKey)) {
|
|
597
|
-
clearSessionStreamSuppression(dcgSessionKey)
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
clearSentMediaKeys(msg.content.message_id)
|
|
601
|
-
try {
|
|
602
|
-
try {
|
|
603
|
-
await waitUntilSubagentsIdle(dcgSessionKey, { timeoutMs: SUBAGENT_IDLE_TIMEOUT_MS, signal: dispatchAbort?.signal })
|
|
604
|
-
} catch (err) {
|
|
605
|
-
if (dispatchAbort?.signal.aborted || getInboundGeneration(dcgSessionKey) !== inboundGenAtStart) {
|
|
606
|
-
throw err
|
|
607
|
-
}
|
|
608
|
-
const message = err instanceof Error ? err.message : String(err)
|
|
609
|
-
if (/^waitUntilSubagentsIdle timeout /u.test(message)) {
|
|
610
|
-
dcgLogger(`subagent idle wait timeout, force cleanup: sessionKey=${dcgSessionKey}, err=${message}`, 'error')
|
|
611
|
-
resetSubagentStateForRequesterSession(dcgSessionKey)
|
|
612
|
-
} else {
|
|
613
|
-
throw err
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
// 等待子 agent 期间可能已有新入站消息(generation 已变),不能再 end/finished,否则会误结束新轮或放行错挂的正文。
|
|
617
|
-
if (getInboundGeneration(dcgSessionKey) !== inboundGenAtStart) {
|
|
618
|
-
streamMerge.dispose()
|
|
619
|
-
dcgLogger(
|
|
620
|
-
`skip post-wait tail: sessionKey=${dcgSessionKey} (stale handler after subagent wait: inbound gen ${inboundGenAtStart}→${getInboundGeneration(dcgSessionKey)})`
|
|
621
|
-
)
|
|
622
|
-
return
|
|
623
|
-
}
|
|
624
|
-
} finally {
|
|
625
|
-
releaseDispatchAbortIfCurrent(dcgSessionKey, dispatchAbort)
|
|
626
|
-
}
|
|
627
|
-
streamMerge.flush(true)
|
|
628
|
-
if (!outboundHasEmittedAssistantText(dcgSessionKey) && !hasEmittedAssistantMedia) {
|
|
629
|
-
dcgLogger(`empty assistant reply: sessionKey=${dcgSessionKey}, messageId=${msg.content.message_id}`, 'error')
|
|
630
|
-
}
|
|
631
|
-
if (!isReviserExpert(dcgSessionKey)) {
|
|
632
|
-
sendFinal(outboundCtx, 'end')
|
|
633
|
-
} else {
|
|
634
|
-
dcgLogger(`审校专家跳过终止 sessionKey=${dcgSessionKey}, messageId=${msg.content.message_id}`)
|
|
635
|
-
}
|
|
636
|
-
streamMerge.dispose()
|
|
637
|
-
clearDcgSessionKeyCachesForCompletedTurn(dcgSessionKey, msg.content.message_id)
|
|
638
|
-
} catch (err) {
|
|
639
|
-
dcgLogger(` handle message failed: ${String(err)}`, 'error')
|
|
640
|
-
if (getInboundGeneration(dcgSessionKey) === inboundGenAtStart) {
|
|
641
|
-
const rawErr = err instanceof Error ? err.message : String(err)
|
|
642
|
-
sendError(agentErrorUserHint(err) ?? rawErr, outboundCtx)
|
|
643
|
-
clearDcgSessionKeyCachesForCompletedTurn(dcgSessionKey, msg.content.message_id)
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
}
|