@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.
Files changed (60) hide show
  1. package/index.js +306 -0
  2. package/openclaw.plugin.json +42 -12
  3. package/package.json +8 -35
  4. package/README.md +0 -167
  5. package/index.ts +0 -35
  6. package/src/agent.ts +0 -568
  7. package/src/bot.ts +0 -646
  8. package/src/channel/index.ts +0 -329
  9. package/src/channel/outboundMedia.ts +0 -144
  10. package/src/channel/outboundTarget.ts +0 -62
  11. package/src/channel/uploadMediaUrl.ts +0 -76
  12. package/src/cron/message.ts +0 -114
  13. package/src/cron/params.ts +0 -164
  14. package/src/cron/toolGuard.ts +0 -466
  15. package/src/cron/types.ts +0 -15
  16. package/src/gateway/index.ts +0 -430
  17. package/src/gateway/security.ts +0 -95
  18. package/src/gateway/socket.ts +0 -256
  19. package/src/libs/ali-oss-6.23.0.tgz +0 -0
  20. package/src/libs/axios-1.13.6.tgz +0 -0
  21. package/src/libs/md5-2.3.0.tgz +0 -0
  22. package/src/libs/mime-types-3.0.2.tgz +0 -0
  23. package/src/libs/unzipper-0.12.3.tgz +0 -0
  24. package/src/libs/ws-8.19.0.tgz +0 -0
  25. package/src/monitor.ts +0 -269
  26. package/src/request/api.ts +0 -80
  27. package/src/request/oss.ts +0 -200
  28. package/src/request/request.ts +0 -191
  29. package/src/request/userInfo.ts +0 -44
  30. package/src/session.ts +0 -28
  31. package/src/skill.ts +0 -114
  32. package/src/tool.ts +0 -603
  33. package/src/tools/messageTool.ts +0 -334
  34. package/src/tools/toolCallGuard.ts +0 -327
  35. package/src/transport.ts +0 -217
  36. package/src/types.ts +0 -139
  37. package/src/utils/agentErrors.ts +0 -116
  38. package/src/utils/constant.ts +0 -60
  39. package/src/utils/env-config.ts +0 -19
  40. package/src/utils/formatLlmInputEvent.ts +0 -48
  41. package/src/utils/gatewayMsgHandler.ts +0 -147
  42. package/src/utils/global.ts +0 -309
  43. package/src/utils/inboundTurnState.ts +0 -66
  44. package/src/utils/log.ts +0 -77
  45. package/src/utils/mediaAttached.ts +0 -244
  46. package/src/utils/mediaEmitter.ts +0 -54
  47. package/src/utils/outboundAssistantText.ts +0 -117
  48. package/src/utils/params.ts +0 -104
  49. package/src/utils/passTxt.ts +0 -4
  50. package/src/utils/resolveRegisterConfig.ts +0 -33
  51. package/src/utils/searchFile.ts +0 -228
  52. package/src/utils/sessionState.ts +0 -137
  53. package/src/utils/sessionTermination.ts +0 -228
  54. package/src/utils/streamMerge.ts +0 -150
  55. package/src/utils/subagentRunMap.ts +0 -71
  56. package/src/utils/undiciFetchInterceptor.ts +0 -346
  57. package/src/utils/workspaceFilePaths.ts +0 -18
  58. package/src/utils/wsMessageHandler.ts +0 -124
  59. package/src/utils/zipExtract.ts +0 -97
  60. package/src/utils/zipPath.ts +0 -24
@@ -1,147 +0,0 @@
1
- import type { GatewayEvent } from '../gateway/index.js'
2
- import { finishedDcgchatCron } from '../cron/message.js'
3
- import { cronAddHandler } from '../cron/params.js'
4
- import { sendGatewayRpc } from '../gateway/socket.js'
5
- import { dcgLogger } from './log.js'
6
- import { getEffectiveMsgParams } from './params.js'
7
- import { sendChunk, sendEventMessage } from '../transport.js'
8
- import { setCronSessionKeyMapsForJob } from '../channel/outboundTarget.js'
9
- import { resolveRequesterSessionKeyBySubagentRunId } from './subagentRunMap.js'
10
- import { isSessionActiveForTool } from '../tool.js'
11
- import { isSessionStreamSuppressed } from './sessionTermination.js'
12
- import { takeStreamChunkIdxAndAdvance } from './inboundTurnState.js'
13
- import { agentErrorUserHint } from './agentErrors.js'
14
-
15
- function collectCronSessionKeys(payload: Record<string, any>): unknown[] {
16
- const job = payload.job ?? {}
17
- const delivery = payload.delivery ?? {}
18
- const jobDelivery = job.delivery ?? {}
19
- return [
20
- payload.sessionKey,
21
- job.sessionKey,
22
- delivery.to,
23
- delivery.intended?.to,
24
- delivery.resolved?.to,
25
- jobDelivery.to,
26
- jobDelivery.intended?.to,
27
- jobDelivery.resolved?.to
28
- ]
29
- }
30
-
31
- function getCronDeliveryTo(job: Record<string, any>): string {
32
- return typeof job.delivery?.to === 'string' ? job.delivery.to.replace('dcg-cron:', '').trim() : ''
33
- }
34
-
35
- function getCronSessionKey(job: Record<string, any>): string {
36
- return typeof job.sessionKey === 'string' ? job.sessionKey.trim() : ''
37
- }
38
-
39
- function readableErrorText(value: unknown): string {
40
- if (typeof value === 'string') return value.trim()
41
- if (value instanceof Error) return value.message.trim()
42
- if (value && typeof value === 'object' && 'message' in value && typeof value.message === 'string') {
43
- return value.message.trim()
44
- }
45
- return ''
46
- }
47
-
48
- function canSendCronInstallFromAddedEvent(payload: Record<string, any>): boolean {
49
- const job = payload.job
50
- if (!job || typeof job !== 'object' || Array.isArray(job)) return false
51
- return Boolean(getCronSessionKey(job) && getCronDeliveryTo(job))
52
- }
53
-
54
- function sendCronInstallFromAddedEvent(payload: Record<string, any>): void {
55
- const job = payload.job as Record<string, any>
56
- sendEventMessage({
57
- event_type: 'cron',
58
- operation_type: 'install',
59
- data: {
60
- ...job,
61
- id: job.id ?? payload.jobId
62
- }
63
- })
64
- }
65
-
66
- /**
67
- * 处理网关 event 帧的副作用(agent 流式输出、cron 同步),并构造供上层分发的 GatewayEvent。
68
- */
69
- export async function handleGatewayEventMessage(msg: { event?: string; payload?: Record<string, unknown>; seq?: number }): Promise<GatewayEvent> {
70
- try {
71
- // 子agent消息输出
72
- if (msg.event === 'agent') {
73
- const pl = msg.payload as { runId: string; data?: { delta?: unknown } }
74
- const sessionKey = resolveRequesterSessionKeyBySubagentRunId(pl.runId)
75
- if (!sessionKey) return { type: msg.event as string, payload: msg.payload, seq: msg.seq as number | undefined }
76
- if (!isSessionActiveForTool(sessionKey)) {
77
- dcgLogger(`[Gateway] drop subagent delta: session not active, runId=${pl.runId}, sessionKey=${sessionKey}`)
78
- return { type: msg.event as string, payload: msg.payload, seq: msg.seq as number | undefined }
79
- }
80
- if (isSessionStreamSuppressed(sessionKey)) {
81
- dcgLogger(`[Gateway] drop subagent delta: stream suppressed, runId=${pl.runId}, sessionKey=${sessionKey}`)
82
- return { type: msg.event as string, payload: msg.payload, seq: msg.seq as number | undefined }
83
- }
84
- if (typeof pl.data?.delta !== 'string' || !pl.data.delta) {
85
- return { type: msg.event as string, payload: msg.payload, seq: msg.seq as number | undefined }
86
- }
87
- const outboundCtx = getEffectiveMsgParams(sessionKey)
88
- if (outboundCtx.sessionId) {
89
- const chunkIdx = takeStreamChunkIdxAndAdvance(sessionKey)
90
- sendChunk(pl.data.delta, outboundCtx, chunkIdx)
91
- }
92
- }
93
- // 定时任务
94
- if (msg.event === 'cron') {
95
- const p = msg.payload as Record<string, any>
96
- if (p?.action === 'started') {
97
- const registeredKeys = setCronSessionKeyMapsForJob(p?.jobId ?? p?.job?.id, collectCronSessionKeys(p))
98
- dcgLogger(`[Gateway] 定时任务活跃会话: jobId=${String(p?.jobId ?? p?.job?.id ?? '')} keys=${registeredKeys.join(',')}`)
99
- dcgLogger(`[Gateway] 定时任务执行开始: ${JSON.stringify(p)}`)
100
- } else {
101
- dcgLogger(`[Gateway] 收到定时任务事件: ${JSON.stringify(p)}`)
102
- }
103
- if (p?.action === 'added') {
104
- if (canSendCronInstallFromAddedEvent(p)) {
105
- sendCronInstallFromAddedEvent(p)
106
- } else {
107
- await cronAddHandler(p?.jobId as string)
108
- }
109
- }
110
- if (p?.action === 'updated') {
111
- const data = (await sendGatewayRpc({ method: 'cron.get', params: { id: p?.jobId as string } })) as Record<string, any>
112
- const params = {
113
- event_type: 'cron',
114
- operation_type: 'install',
115
- data
116
- }
117
- sendEventMessage(params)
118
- }
119
- if (p?.action === 'removed') {
120
- const params = {
121
- event_type: 'cron',
122
- operation_type: 'remove',
123
- data: { ...p }
124
- }
125
- sendEventMessage(params)
126
- }
127
- if (p?.action === 'finished') {
128
- const jobId = typeof p?.jobId === 'string' ? p.jobId.trim() : ''
129
- const summary = typeof p?.summary === 'string' ? p.summary : ''
130
- const failureDetail = summary.trim() || readableErrorText(p?.error) || readableErrorText(p?.diagnostics?.summary)
131
- const userHint = agentErrorUserHint(failureDetail)
132
- let text = ''
133
- if (p?.status !== 'ok') {
134
- text = '定时任务执行失败' + (userHint || failureDetail ? `\n ${userHint ?? failureDetail}` : '')
135
- }
136
- finishedDcgchatCron(jobId, text)
137
- }
138
- }
139
- } catch (error) {
140
- dcgLogger(`[Gateway] 处理事件失败: ${error}`, 'error')
141
- }
142
- return {
143
- type: msg.event as string,
144
- payload: msg.payload,
145
- seq: msg.seq as number | undefined
146
- }
147
- }
@@ -1,309 +0,0 @@
1
- import type WebSocket from 'ws'
2
- import fs from 'node:fs'
3
- import os from 'node:os'
4
- import path from 'node:path'
5
- import type { OpenClawConfig, PluginRuntime } from 'openclaw/plugin-sdk'
6
- import { createPluginRuntimeStore } from 'openclaw/plugin-sdk/runtime-store'
7
- import { CHANNEL_ID } from './constant.js'
8
- import { dcgLogger } from './log.js'
9
-
10
- /** 配置中 `channels.*.appId` 为该值时,默认工作区落在 `~/.mobook/workspace`(否则 `~/.openclaw/workspace`)。 */
11
- export const MOBOOK_WORKSPACE_APP_ID = 110
12
-
13
- /** socket connection */
14
- let ws: WebSocket | null = null
15
-
16
- export function setWsConnection(next: WebSocket | null) {
17
- ws = next
18
- }
19
-
20
- export function getWsConnection(): WebSocket | null {
21
- return ws
22
- }
23
-
24
- let config: OpenClawConfig | null = null
25
-
26
- export function setOpenClawConfig(next: OpenClawConfig | null) {
27
- config = next
28
- }
29
-
30
- export function getOpenClawConfig(): OpenClawConfig | null {
31
- return config
32
- }
33
-
34
- function getWorkspacePath(): string | null {
35
- const chAppId = config?.channels?.[CHANNEL_ID]?.appId
36
- const workspacePath = path.join(os.homedir(), Number(chAppId) === MOBOOK_WORKSPACE_APP_ID ? '.mobook' : '.openclaw', 'workspace')
37
- if (fs.existsSync(workspacePath)) {
38
- return workspacePath
39
- }
40
- return null
41
- }
42
-
43
- let workspaceDir: string = getWorkspacePath() ?? ''
44
-
45
- export function setWorkspaceDir(dir?: string) {
46
- if (dir && dir !== workspaceDir) {
47
- workspaceDir = dir
48
- dcgLogger(`setWorkspaceDir ==> ${dir}`)
49
- }
50
- }
51
-
52
- export function getWorkspaceDir(): string {
53
- if (!workspaceDir) {
54
- dcgLogger?.('Workspace directory not initialized', 'error')
55
- }
56
- return workspaceDir
57
- }
58
-
59
- const { setRuntime: setDcgchatRuntime, getRuntime: getDcgchatRuntime } = createPluginRuntimeStore<PluginRuntime>(
60
- `${CHANNEL_ID} runtime not initialized`
61
- )
62
- export { setDcgchatRuntime, getDcgchatRuntime }
63
-
64
- export type MsgSessionStatus = 'running' | 'finished' | ''
65
-
66
- const msgStatusBySessionKey = new Map<string, MsgSessionStatus>()
67
-
68
- export function setMsgStatus(sessionKey: string, status: MsgSessionStatus) {
69
- if (!sessionKey?.trim()) return
70
- if (status === '') {
71
- msgStatusBySessionKey.delete(sessionKey)
72
- } else {
73
- msgStatusBySessionKey.set(sessionKey, status)
74
- }
75
- }
76
-
77
- export function getMsgStatus(sessionKey: string): MsgSessionStatus {
78
- if (!sessionKey?.trim()) return ''
79
- return msgStatusBySessionKey.get(sessionKey) ?? ''
80
- }
81
-
82
- /**
83
- * 出站媒体去重 bucket:优先本轮用户 `messageId`(与 bot / sendFinal 清理一致),否则书灵 `sessionId`,最后可回落 `sessionKey`。
84
- */
85
- export function resolveOutboundMediaDedupeKey(messageId?: string, sessionId?: string, sessionKeyFallback?: string): string {
86
- const mid = messageId?.trim()
87
- if (mid) return mid
88
- const sid = sessionId?.trim()
89
- if (sid) return sid
90
- return sessionKeyFallback?.trim() ?? ''
91
- }
92
-
93
- /**
94
- * 本地路径用 `path.resolve`、http(s) 用「origin+pathname」并去掉 query,避免仅用 basename 导致跨目录误去重。
95
- */
96
- export function mediaPathDedupeKey(mediaUrl: string): string {
97
- const t = mediaUrl.trim()
98
- if (!t) return ''
99
- if (/^https?:\/\//i.test(t)) {
100
- try {
101
- const u = new URL(t)
102
- return `${u.origin}${u.pathname}`
103
- } catch {
104
- return t.split('?')[0] ?? t
105
- }
106
- }
107
- try {
108
- const r = path.resolve(t)
109
- return process.platform === 'win32' ? r.toLowerCase() : r
110
- } catch {
111
- const n = path.normalize(t)
112
- return process.platform === 'win32' ? n.toLowerCase() : n
113
- }
114
- }
115
-
116
- /** 已发送媒体去重:外层 dedupe bucket(见 resolveOutboundMediaDedupeKey)→ 内层规范化路径/url key */
117
- const sentMediaKeysBySession = new Map<string, Set<string>>()
118
-
119
- function getSessionMediaSet(dedupeBucketKey: string): Set<string> {
120
- let set = sentMediaKeysBySession.get(dedupeBucketKey)
121
- if (!set) {
122
- set = new Set<string>()
123
- sentMediaKeysBySession.set(dedupeBucketKey, set)
124
- }
125
- return set
126
- }
127
-
128
- export function addSentMediaKey(dedupeBucketKey: string, url: string) {
129
- const k = dedupeBucketKey.trim()
130
- if (!k) return
131
- getSessionMediaSet(k).add(mediaPathDedupeKey(url))
132
- }
133
-
134
- export function hasSentMediaKey(dedupeBucketKey: string, url: string): boolean {
135
- const k = dedupeBucketKey.trim()
136
- if (!k) return false
137
- return sentMediaKeysBySession.get(k)?.has(mediaPathDedupeKey(url)) ?? false
138
- }
139
-
140
- /** 仅删除指定 bucket;`dedupeBucketKey` 为空则 no-op(不做「清空全部」以免误伤)。 */
141
- export function clearSentMediaKeys(dedupeBucketKey?: string) {
142
- const k = dedupeBucketKey?.trim()
143
- if (!k) return
144
- sentMediaKeysBySession.delete(k)
145
- }
146
-
147
- /** 每个 sessionKey 下多个定时任务 messageId,入队在尾、出队在头(FIFO) */
148
- const cronMessageIdMap = new Map<string, string[]>()
149
-
150
- export function setCronMessageId(sk: string, messageId: string | number | null | undefined) {
151
- const mid = messageId != null && messageId !== '' ? String(messageId).trim() : ''
152
- if (!sk?.trim() || !mid) return
153
- let q = cronMessageIdMap.get(sk)
154
- if (!q) {
155
- q = []
156
- cronMessageIdMap.set(sk, q)
157
- }
158
- q.push(mid)
159
- }
160
-
161
- /** 窥视队首 messageId(不移除),供发送中与 finished 配对使用 */
162
- export function getCronMessageId(sk: string): string {
163
- if (!sk?.trim()) return ''
164
- return cronMessageIdMap.get(sk)?.[0] ?? ''
165
- }
166
-
167
- /** 弹出队首一条,与一次 finished 对应;队列为空时移除 key */
168
- export function removeCronMessageId(sk: string) {
169
- if (!sk?.trim()) return
170
- const q = cronMessageIdMap.get(sk)
171
- if (!q?.length) return
172
- q.shift()
173
- if (q.length === 0) {
174
- cronMessageIdMap.delete(sk)
175
- }
176
- }
177
-
178
- /**
179
- * turn 已正常收尾时清空该 sessionKey 的整条 cron 队列。
180
- *
181
- * 背景:
182
- * - `setCronMessageId` 同时服务于两类调用方:
183
- * 1. `cron/message.ts onRunCronJob`:每条 cron.run 写入队首,与 `finishedDcgchatCron` 1.2s 后 `removeCronMessageId` 配对消费(队首一条)。
184
- * 2. `outboundMedia.ts sendDcgchatMedia` 兜底:未传 `opts.messageId` 且队列为空时写入 `${Date.now()}`,本意是给后续 cron 气泡配 messageId。
185
- * 这条 entry 永远不会被 `finishedDcgchatCron` 消费(其队首始终是 onRunCronJob 写入的),且每次 `deliver` 回调都会触发一条 → 队列无限增长。
186
- * - turn 收尾时该 sessionKey 下不会再有 in-flight finished 配对,可安全清空。
187
- * - 若 1.2s 内 `finishedDcgchatCron` 才被调用,`q.shift()` 在空队列上为 no-op(`removeCronMessageId` 已判空),不报错。
188
- */
189
- export function removeAllCronMessageIdsForSession(sk: string): void {
190
- if (!sk?.trim()) return
191
- cronMessageIdMap.delete(sk)
192
- }
193
-
194
- /** 用于网关重启时持久化/恢复的全局会话状态快照 */
195
- export type GlobalSessionStateSnapshot = {
196
- msgStatusBySessionKey: Array<[string, MsgSessionStatus]>
197
- /** 媒体已发送去重:bucket 为 `resolveOutboundMediaDedupeKey`(优先 messageId) */
198
- sentMediaKeysBySession: Array<[string, string[]]>
199
- cronMessageIdMap: Array<[string, string[]]>
200
- }
201
-
202
- export function snapshotGlobalSessionState(): GlobalSessionStateSnapshot {
203
- const runningEntries = [...msgStatusBySessionKey.entries()].filter(([, status]) => status === 'running')
204
- const runningSessionKeys = new Set(runningEntries.map(([k]) => k))
205
- return {
206
- msgStatusBySessionKey: runningEntries,
207
- sentMediaKeysBySession: [],
208
- cronMessageIdMap: [...cronMessageIdMap.entries()].filter(([k]) => runningSessionKeys.has(k)).map(([k, q]) => [k, [...q]])
209
- }
210
- }
211
-
212
- export function restoreGlobalSessionState(snap: GlobalSessionStateSnapshot | null | undefined): void {
213
- if (!snap) return
214
- msgStatusBySessionKey.clear()
215
- sentMediaKeysBySession.clear()
216
- cronMessageIdMap.clear()
217
- for (const [k, v] of snap.msgStatusBySessionKey ?? []) {
218
- if (typeof k === 'string' && k && v === 'running') {
219
- msgStatusBySessionKey.set(k, v)
220
- }
221
- }
222
- for (const [k, arr] of snap.cronMessageIdMap ?? []) {
223
- if (typeof k !== 'string' || !k || !Array.isArray(arr)) continue
224
- const q = arr.filter((x): x is string => typeof x === 'string' && !!x)
225
- if (q.length > 0) cronMessageIdMap.set(k, q)
226
- }
227
- }
228
-
229
- /** 与 bot 侧一致:session_id 为 `agentId::对话标识` 时提取 agentId(专家会话可仅依赖此前缀)。 */
230
- export function extractAgentIdFromConversationId(conversationId: string): string | null {
231
- const idx = conversationId.indexOf('::')
232
- if (idx <= 0) return null
233
- return conversationId.slice(0, idx)
234
- }
235
-
236
- export const getSessionKey = (content: any, accountId: string) => {
237
- const { real_mobook, agent_id, agent_clone_code, session_id } = content
238
- const core = getDcgchatRuntime()
239
-
240
- const sid = typeof session_id === 'string' ? session_id.trim() : ''
241
- const embeddedAgent = sid ? extractAgentIdFromConversationId(sid) : null
242
- const agentCode = (typeof agent_clone_code === 'string' && agent_clone_code.trim()) || embeddedAgent || 'main'
243
-
244
- const route = core.channel.routing.resolveAgentRoute({
245
- cfg: getOpenClawConfig() as OpenClawConfig,
246
- channel: CHANNEL_ID,
247
- accountId: accountId || 'default',
248
- peer: { kind: 'direct', id: session_id }
249
- })
250
- return real_mobook == '1' ? route.sessionKey : `agent:${agentCode}:mobook:direct:${agent_id}:${session_id}`.toLowerCase()
251
- }
252
-
253
- interface TakeApartSessionKeyResult {
254
- sessionId: string
255
- agentId: string
256
- }
257
- export function takeApartSessionKey(sk: string): TakeApartSessionKeyResult {
258
- const sessionInfo = sk?.split(':')
259
- const sessionId = sessionInfo?.at(-1) ?? ''
260
- const agentId = sessionInfo?.at(-2) ?? ''
261
- return { sessionId, agentId }
262
- }
263
-
264
- export function compressFields(obj: Record<string, unknown>, fields: string[] = []) {
265
- const start = 8
266
- const end = 8
267
- const newObj = { ...obj }
268
-
269
- const compressText = (str: unknown) => {
270
- if (typeof str !== 'string') return str
271
-
272
- if (str.length <= start + end) {
273
- return str
274
- }
275
-
276
- const omittedCount = str.length - start - end
277
-
278
- return str.slice(0, start) + `...省略${omittedCount}个字...` + str.slice(-end)
279
- }
280
-
281
- fields.forEach((field) => {
282
- if (field in newObj) {
283
- newObj[field] = compressText(newObj[field])
284
- }
285
- })
286
-
287
- return newObj
288
- }
289
-
290
- export function formatText(text: string): string {
291
- if (!text) return ''
292
- const str = String(text).replace(/\s/g, '')
293
- if (!str) return ''
294
- if (str.length <= 50) {
295
- return str
296
- }
297
- return str.slice(0, 25) + `...[此处省略${str.length - 50}字]....` + str.slice(-25)
298
- }
299
-
300
- export function isReviserExpert(sessionKey?: string): boolean {
301
- if (!sessionKey) return false
302
- const segment = sessionKey.split(':')[1]
303
- return segment != null && segment.includes('reviser-expert')
304
- }
305
- export function isCollectionReviewer(sessionKey?: string): boolean {
306
- if (!sessionKey) return false
307
- const segment = sessionKey.split(':')[1]
308
- return segment != null && segment.includes('collection-reviewer')
309
- }
@@ -1,66 +0,0 @@
1
- /**
2
- * 入站 generation 与流式分片序号(bot 与会话清理共用)。
3
- * 独立模块以避免 bot 与 sessionTermination 之间的循环依赖。
4
- */
5
-
6
- const inboundGenerationBySessionKey = new Map<string, number>()
7
- const streamChunkIdxBySessionKey = new Map<string, number>()
8
-
9
- /**
10
- * stop 序列号:每条 /stop 在 turn 开始时递增,用于 stale handler 判定。
11
- * 与 generation 独立:generation 区分不同用户消息,stopSeq 区分 /stop 与普通消息。
12
- */
13
- const stopSeqBySessionKey = new Map<string, number>()
14
-
15
- /** 每条入站消息在 turn 开始时单调递增;用于 stale handler 判定 */
16
- export function bumpInboundGeneration(sessionKey: string): number {
17
- const n = (inboundGenerationBySessionKey.get(sessionKey) ?? 0) + 1
18
- inboundGenerationBySessionKey.set(sessionKey, n)
19
- return n
20
- }
21
-
22
- export function getInboundGeneration(sessionKey: string): number {
23
- return inboundGenerationBySessionKey.get(sessionKey) ?? 0
24
- }
25
-
26
- /**
27
- * 每条 /stop 在 turn 开始时单调递增;用于 stale handler 判定(与 generation 独立检查)。
28
- * 普通消息 bumpGeneration 时不 bump stopSeq,只有 /stop 才 bump。
29
- */
30
- export function bumpStopSeq(sessionKey: string): number {
31
- const n = (stopSeqBySessionKey.get(sessionKey) ?? 0) + 1
32
- stopSeqBySessionKey.set(sessionKey, n)
33
- return n
34
- }
35
-
36
- export function getStopSeq(sessionKey: string): number {
37
- return stopSeqBySessionKey.get(sessionKey) ?? 0
38
- }
39
-
40
- /** 第一条 /stop 后的新用户消息已接管会话时,消费 stop 标记,避免后续消息一直被当作 after-stop。 */
41
- export function clearStopSeqForSession(sessionKey: string): void {
42
- stopSeqBySessionKey.delete(sessionKey)
43
- }
44
-
45
- /** 会话完全终止时清理 generation 与 streamChunkIdx,防止 Map 泄漏 */
46
- export function clearInboundGenerationForSession(sessionKey: string): void {
47
- inboundGenerationBySessionKey.delete(sessionKey)
48
- streamChunkIdxBySessionKey.delete(sessionKey)
49
- stopSeqBySessionKey.delete(sessionKey)
50
- }
51
-
52
- /** 本轮正常收尾时仅删除分片序号(generation 保留) */
53
- export function deleteStreamChunkIdxForSession(sessionKey: string): void {
54
- streamChunkIdxBySessionKey.delete(sessionKey)
55
- }
56
-
57
- export function resetStreamChunkIdxForSession(sessionKey: string, value: number): void {
58
- streamChunkIdxBySessionKey.set(sessionKey, value)
59
- }
60
-
61
- /** 返回当前序号并将计数 +1,供 sendChunk(..., prev) 使用 */
62
- export function takeStreamChunkIdxAndAdvance(sessionKey: string): number {
63
- const prev = streamChunkIdxBySessionKey.get(sessionKey) ?? 0
64
- streamChunkIdxBySessionKey.set(sessionKey, prev + 1)
65
- return prev
66
- }
package/src/utils/log.ts DELETED
@@ -1,77 +0,0 @@
1
- import { RuntimeEnv } from 'openclaw/plugin-sdk'
2
- import { CHANNEL_ID } from './constant.js'
3
-
4
- let logger: RuntimeEnv | null = null
5
- const STACK_ROOT = 'openclaw-dcgchat'
6
- /** 发布包 scope,与 @dcrays/dcgchat-test 前段一致,用于匹配已安装到 node_modules 下的路径 */
7
- const SCOPE = '@dcrays/dcgchat'
8
-
9
- function isOurCodePath(line: string): boolean {
10
- if (line.includes(STACK_ROOT)) return true
11
- if (line.includes(SCOPE)) return true
12
- // 无 node_modules 的本地单文件 /dist/index.js
13
- if (line.replaceAll('\\', '/').includes('dist/index.js') && !line.includes('node_modules')) {
14
- return true
15
- }
16
- return false
17
- }
18
-
19
- function isOtherNodeModuleLine(line: string): boolean {
20
- if (!line.includes('node_modules/')) return false
21
- // 本扩展装在 host 的 node_modules 下时,路径仍要保留
22
- if (line.includes('node_modules/@dcrays/')) return false
23
- if (line.includes(`node_modules/${STACK_ROOT}/`)) return false
24
- return true
25
- }
26
-
27
- export function setLogger(next: RuntimeEnv | null) {
28
- logger = next
29
- }
30
-
31
- function buildFilteredStack(): string {
32
- const stack = new Error().stack
33
- if (!stack) return ''
34
- const frames = stack
35
- .split('\n')
36
- .slice(1)
37
- .map((line) => line.trim())
38
- .filter(
39
- (line) =>
40
- isOurCodePath(line) &&
41
- !isOtherNodeModuleLine(line) &&
42
- !line.includes('dcgLogger') &&
43
- !line.includes('buildFilteredStack')
44
- )
45
- .map((line) => {
46
- const normalized = line.replaceAll('\\', '/')
47
- const fromRoot = normalized.indexOf(`${STACK_ROOT}/`)
48
- if (fromRoot >= 0) {
49
- const trimmed = normalized.slice(fromRoot + STACK_ROOT.length + 1)
50
- return normalized.startsWith('at ') ? `at ${trimmed}` : trimmed
51
- }
52
- const scopeIdx = normalized.indexOf(`${SCOPE}`)
53
- if (scopeIdx >= 0) {
54
- const tail = normalized.slice(scopeIdx)
55
- return normalized.startsWith('at ') ? `at ${tail}` : tail
56
- }
57
- return normalized.replace(/^at\s+/, '')
58
- })
59
-
60
- if (!frames.length) return ''
61
- return `\nstack:\n${frames.join('\n')}`
62
- }
63
-
64
- export function dcgLogger(
65
- message: string,
66
- type: 'log' | 'error' = 'log',
67
- options?: { includeStack?: boolean }
68
- ): void {
69
- const includeStack = options?.includeStack ?? false
70
- const stack = includeStack ? buildFilteredStack() : ''
71
- const fullMessage = `${message}${stack}`
72
- if (logger) {
73
- logger[type](`书灵墨宝🚀 ~ [${new Date().toISOString()}] ${fullMessage}`)
74
- } else {
75
- console[type](`书灵墨宝🚀 ~ ${new Date().toISOString()} [${CHANNEL_ID}]: ${fullMessage}`)
76
- }
77
- }