@dcrays/dcgchat-test 0.6.2 → 0.6.3

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 (59) hide show
  1. package/index.js +304 -0
  2. package/openclaw.plugin.json +41 -12
  3. package/package.json +9 -36
  4. package/README.md +0 -167
  5. package/index.ts +0 -31
  6. package/src/agent.ts +0 -592
  7. package/src/bot.ts +0 -676
  8. package/src/channel/index.ts +0 -329
  9. package/src/channel/outboundMedia.ts +0 -144
  10. package/src/channel/outboundTarget.ts +0 -24
  11. package/src/channel/uploadMediaUrl.ts +0 -76
  12. package/src/cron/message.ts +0 -109
  13. package/src/cron/params.ts +0 -166
  14. package/src/cron/toolGuard.ts +0 -285
  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 -625
  33. package/src/tools/messageTool.ts +0 -334
  34. package/src/tools/toolCallGuard.ts +0 -323
  35. package/src/transport.ts +0 -217
  36. package/src/types.ts +0 -139
  37. package/src/utils/agentErrors.ts +0 -23
  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 -89
  42. package/src/utils/global.ts +0 -306
  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 -110
  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/workspaceFilePaths.ts +0 -18
  57. package/src/utils/wsMessageHandler.ts +0 -124
  58. package/src/utils/zipExtract.ts +0 -97
  59. package/src/utils/zipPath.ts +0 -24
@@ -1,54 +0,0 @@
1
- import type { ReplyPayload } from 'openclaw/plugin-sdk'
2
- import { mediaPathDedupeKey } from './global.js'
3
- import { normalizeOutboundMediaPaths, sendDcgchatMedia } from '../channel/index.js'
4
-
5
- export interface MediaEmitter {
6
- emitFromPayload(payload: Partial<ReplyPayload>): Promise<boolean>
7
- readonly sentKeys: ReadonlySet<string>
8
- }
9
-
10
- export interface CreateMediaEmitterOptions {
11
- sessionKey: string
12
- onEmit?: () => void
13
- }
14
-
15
- function resolveReplyMediaList(payload: Partial<ReplyPayload>): string[] {
16
- if ('mediaUrls' in payload && Array.isArray(payload.mediaUrls) && payload.mediaUrls.length > 0) {
17
- return normalizeOutboundMediaPaths(payload.mediaUrls)
18
- }
19
- const single = 'mediaUrl' in payload ? payload.mediaUrl : null
20
- return normalizeOutboundMediaPaths(single ?? null)
21
- }
22
-
23
- export function createMediaEmitter(options: CreateMediaEmitterOptions): MediaEmitter {
24
- const { sessionKey, onEmit } = options
25
- const sentKeys = new Set<string>()
26
-
27
- const emit = async (mediaUrls: string[]): Promise<boolean> => {
28
- if (mediaUrls.length === 0) return false
29
- await sendDcgchatMedia({ sessionKey, mediaUrls, text: '' })
30
- onEmit?.()
31
- return true
32
- }
33
-
34
- const emitFromPayload = async (payload: Partial<ReplyPayload>): Promise<boolean> => {
35
- const list = resolveReplyMediaList(payload)
36
- const urls: string[] = []
37
- for (const url of list) {
38
- const key = mediaPathDedupeKey(url)
39
- if (sentKeys.has(key)) continue
40
- sentKeys.add(key)
41
- urls.push(url)
42
- }
43
- if (urls.length === 0) return false
44
- await emit(urls)
45
- return true
46
- }
47
-
48
- return {
49
- emitFromPayload,
50
- get sentKeys(): ReadonlySet<string> {
51
- return new Set(sentKeys)
52
- }
53
- }
54
- }
@@ -1,110 +0,0 @@
1
- import type { IMsgParams } from '../types.js'
2
- import { sendChunk } from '../transport.js'
3
- import { takeStreamChunkIdxAndAdvance } from './inboundTurnState.js'
4
-
5
- type OutboundTextState = {
6
- inboundGen: number
7
- lastStreamSnapshot: string
8
- /** 仅 onPartialReply 成功下发过正文时为 true,用于 deliver 兜底判断 */
9
- hasStreamedAssistantText: boolean
10
- hasEmittedAssistantText: boolean
11
- }
12
-
13
- const stateBySessionKey = new Map<string, OutboundTextState>()
14
-
15
- /** 每轮入站 dispatch 开始前初始化,与 inboundGen 绑定供调试对齐 */
16
- export function resetOutboundTextState(sessionKey: string, inboundGen: number): void {
17
- const k = sessionKey.trim()
18
- if (!k) return
19
- stateBySessionKey.set(k, {
20
- inboundGen,
21
- lastStreamSnapshot: '',
22
- hasStreamedAssistantText: false,
23
- hasEmittedAssistantText: false
24
- })
25
- }
26
-
27
- export function clearOutboundTextState(sessionKey: string): void {
28
- const k = sessionKey.trim()
29
- if (!k) return
30
- stateBySessionKey.delete(k)
31
- }
32
-
33
- function getState(sessionKey: string): OutboundTextState | undefined {
34
- const k = sessionKey.trim()
35
- if (!k) return undefined
36
- return stateBySessionKey.get(k)
37
- }
38
-
39
- /** 子 agent 等路径可能未走 bot reset;懒初始化保证 dcgchat_message 仍可出站且参与快照去重 */
40
- function ensureState(sessionKey: string): OutboundTextState | undefined {
41
- const k = sessionKey.trim()
42
- if (!k) return undefined
43
- let state = stateBySessionKey.get(k)
44
- if (!state) {
45
- state = {
46
- inboundGen: 0,
47
- lastStreamSnapshot: '',
48
- hasStreamedAssistantText: false,
49
- hasEmittedAssistantText: false
50
- }
51
- stateBySessionKey.set(k, state)
52
- }
53
- return state
54
- }
55
-
56
- export function outboundHasStreamedAssistantText(sessionKey: string): boolean {
57
- return getState(sessionKey)?.hasStreamedAssistantText ?? false
58
- }
59
-
60
- export function outboundHasEmittedAssistantText(sessionKey: string): boolean {
61
- return getState(sessionKey)?.hasEmittedAssistantText ?? false
62
- }
63
-
64
- /** 仅重置快照(用于 `replace` 流式片段),保留 generation 与已下发标记 */
65
- export function resetOutboundTextSnapshot(sessionKey: string): void {
66
- const k = sessionKey.trim()
67
- if (!k) return
68
- const state = ensureState(k)
69
- if (state) {
70
- state.lastStreamSnapshot = ''
71
- }
72
- }
73
-
74
- /**
75
- * 统一助手正文出站:bot 流式/deliver 与 dcgchat_message 共用快照增量,避免双发或漏发。
76
- * @param markStreamed 为 true 时表示来自 onPartialReply,会置 hasStreamedAssistantText
77
- */
78
- export function emitOutboundAssistantText(
79
- sessionKey: string,
80
- raw: string | undefined,
81
- outboundCtx: IMsgParams,
82
- options?: { markStreamed?: boolean; emitDelta?: (delta: string) => boolean | void }
83
- ): boolean {
84
- if (!raw) return false
85
- const state = ensureState(sessionKey)
86
- if (!state) return false
87
-
88
- const t = raw
89
- let delta = ''
90
- if (t.startsWith(state.lastStreamSnapshot)) {
91
- delta = t.slice(state.lastStreamSnapshot.length)
92
- state.lastStreamSnapshot = t
93
- } else if (state.lastStreamSnapshot.startsWith(t)) {
94
- // 快照缩短(模型修订等):不重复下发
95
- } else {
96
- delta = t
97
- state.lastStreamSnapshot = t
98
- }
99
-
100
- if (!delta.trim()) return false
101
-
102
- const emitted =
103
- options?.emitDelta != null ? options.emitDelta(delta) !== false : sendChunk(delta, outboundCtx, takeStreamChunkIdxAndAdvance(sessionKey))
104
- if (!emitted) return false
105
- state.hasEmittedAssistantText = true
106
- if (options?.markStreamed) {
107
- state.hasStreamedAssistantText = true
108
- }
109
- return true
110
- }
@@ -1,104 +0,0 @@
1
- import { CHANNEL_ID } from './constant.js'
2
- import { getOpenClawConfig } from './global.js'
3
- import type { DcgchatConfig, IMsgParams } from '../types.js'
4
-
5
- /**
6
- * map key 是 session_key,value 为该会话下已 merge 后的消息参数。
7
- */
8
- const paramsMessageMap = new Map<string, IMsgParams>()
9
-
10
- /** 从 OpenClaw 配置读取当前 channel 的基础参数(唯一来源,供 transport / resolve 等复用) */
11
- export function getParamsDefaults(): IMsgParams {
12
- const ch = (getOpenClawConfig()?.channels?.[CHANNEL_ID] as DcgchatConfig | undefined) ?? {}
13
- return {
14
- userId: Number(ch.userId ?? 0),
15
- botToken: ch.botToken ?? '',
16
- sessionId: '',
17
- messageId: '',
18
- domainId: String(ch.domainId ?? '1000'),
19
- appId: String(ch.appId ?? '100'),
20
- botId: '',
21
- agentId: '',
22
- sessionKey: ''
23
- }
24
- }
25
-
26
- export function resolveParamsMessage(params: Partial<IMsgParams>): IMsgParams {
27
- const defaults = getParamsDefaults()
28
- return {
29
- userId: Number(params.userId ?? defaults.userId),
30
- botToken: params.botToken ?? defaults.botToken,
31
- sessionId: params.sessionId ?? defaults.sessionId,
32
- messageId: params.messageId ?? defaults.messageId,
33
- domainId: String(params.domainId ?? defaults.domainId),
34
- appId: String(params.appId ?? defaults.appId),
35
- botId: params.botId ?? defaults.botId,
36
- agentId: params.agentId ?? defaults.agentId,
37
- sessionKey: params.sessionKey ?? defaults.sessionKey
38
- }
39
- }
40
-
41
- /**
42
- * 统一取值入口:显式 sessionKey,或回落到当前会话;再与配置缺省 merge,保证字段完整。
43
- */
44
- export function getEffectiveMsgParams(sessionKey?: string): IMsgParams {
45
- const stored = sessionKey ? paramsMessageMap.get(sessionKey) : undefined
46
- return stored ? resolveParamsMessage(stored) : getParamsDefaults()
47
- }
48
-
49
- /**
50
- * Agent `dcgchat_message` / 出站 `target` 应为 `dcgSessionKey`(如 `agent:main:mobook:direct:...`)。
51
- * `setParamsMessage` 的 key 与此一致;查不到 map 时回落到配置缺省(无会话级 messageId/sessionId)。
52
- */
53
- export function getOutboundMsgParams(preferredKey: string): IMsgParams {
54
- const k = preferredKey?.trim()
55
- if (k && paramsMessageMap.has(k)) {
56
- return getEffectiveMsgParams(k)
57
- }
58
- return getEffectiveMsgParams()
59
- }
60
-
61
- export function setParamsMessage(sessionKey: string, params: Partial<IMsgParams>) {
62
- if (!sessionKey) return
63
- const previous = paramsMessageMap.get(sessionKey)
64
- const base = previous ? resolveParamsMessage(previous) : getParamsDefaults()
65
- paramsMessageMap.set(sessionKey, resolveParamsMessage({ ...base, ...params, sessionKey }))
66
- }
67
-
68
- export function getParamsMessage(sessionKey: string): IMsgParams | undefined {
69
- return paramsMessageMap.get(sessionKey)
70
- }
71
-
72
- export function clearParamsMessage(sessionKey: string): void {
73
- const k = sessionKey?.trim()
74
- if (!k) return
75
- paramsMessageMap.delete(k)
76
- }
77
-
78
- /** 用于网关重启时持久化/恢复的参数模块状态快照 */
79
- export type ParamsStateSnapshot = {
80
- paramsMessageMap: Array<[string, Partial<IMsgParams>]>
81
- }
82
-
83
- function snapshotMsgParams(params: IMsgParams): Partial<IMsgParams> {
84
- const { botToken: _botToken, ...rest } = params
85
- return rest
86
- }
87
-
88
- export function snapshotParamsState(sessionKeys?: ReadonlySet<string>): ParamsStateSnapshot {
89
- return {
90
- paramsMessageMap: [...paramsMessageMap.entries()]
91
- .filter(([k]) => !sessionKeys || sessionKeys.has(k))
92
- .map(([k, v]) => [k, snapshotMsgParams(v)])
93
- }
94
- }
95
-
96
- export function restoreParamsState(snap: ParamsStateSnapshot | null | undefined): void {
97
- if (!snap) return
98
- paramsMessageMap.clear()
99
- for (const [k, v] of snap.paramsMessageMap ?? []) {
100
- if (typeof k === 'string' && k && v && typeof v === 'object') {
101
- paramsMessageMap.set(k, resolveParamsMessage(v))
102
- }
103
- }
104
- }
@@ -1,4 +0,0 @@
1
- export function isInsufficientBalanceAssistantText(text: string | undefined): boolean {
2
- const t = text?.trim() ?? ''
3
- return t === '1003-额度不足请充值' || /1003[-–]?额度不足请充值/.test(t)
4
- }
@@ -1,33 +0,0 @@
1
- import type { OpenClawConfig } from 'openclaw/plugin-sdk'
2
- import type { OpenClawPluginApi } from 'openclaw/plugin-sdk'
3
- import { CHANNEL_ID } from './constant.js'
4
-
5
- function isEmptyObject(value: unknown): boolean {
6
- return value !== null && typeof value === 'object' && Object.keys(value as object).length === 0
7
- }
8
-
9
- export function openclawConfigLooksUsable(cfg: OpenClawConfig | undefined | null): cfg is OpenClawConfig {
10
- if (!cfg || typeof cfg !== 'object' || isEmptyObject(cfg)) return false
11
- const ch = cfg.channels
12
- if (!ch || typeof ch !== 'object' || Object.keys(ch).length === 0) return false
13
- return ch[CHANNEL_ID as keyof typeof ch] != null
14
- }
15
-
16
- /**
17
- * `registerFull` 里 OpenClaw 传入的 `api.config` 在部分加载路径下会是 `{}` 或缺本通道段落,
18
- * 应改用 `api.runtime.config.loadConfig()` 读盘上完整配置,避免 `getOpenClawConfig()` 不可靠。
19
- *
20
- * (`setRuntime` 在 `registerFull` 之前已执行,故此处可安全使用 `api.runtime`。)
21
- */
22
- export function resolveOpenClawConfigAtRegister(api: OpenClawPluginApi): OpenClawConfig {
23
- const snap = api.config as OpenClawConfig | undefined
24
- if (openclawConfigLooksUsable(snap)) {
25
- return snap
26
- }
27
- try {
28
- const full = api.runtime.config.loadConfig() as OpenClawConfig
29
- return structuredClone(full)
30
- } catch {
31
- return (snap ?? {}) as OpenClawConfig
32
- }
33
- }
@@ -1,228 +0,0 @@
1
- import { dcgLogger } from './log.js'
2
-
3
- /**
4
- * 从文本中提取 /mobook 目录下的文件
5
- * @param {string} text
6
- * @returns {string[]}
7
- */
8
- const EXT_LIST = [
9
- // 文档类
10
- 'doc',
11
- 'docx',
12
- 'xls',
13
- 'xlsx',
14
- 'ppt',
15
- 'pptx',
16
- 'pdf',
17
- 'txt',
18
- 'rtf',
19
- 'odt',
20
-
21
- // 数据/开发
22
- 'json',
23
- 'xml',
24
- 'csv',
25
- 'yaml',
26
- 'yml',
27
-
28
- // 前端/文本
29
- 'html',
30
- 'htm',
31
- 'md',
32
- 'markdown',
33
- 'css',
34
- 'js',
35
- 'ts',
36
-
37
- // 图片
38
- 'png',
39
- 'jpg',
40
- 'jpeg',
41
- 'gif',
42
- 'bmp',
43
- 'webp',
44
- 'svg',
45
- 'ico',
46
- 'tiff',
47
-
48
- // 音频
49
- 'mp3',
50
- 'wav',
51
- 'ogg',
52
- 'aac',
53
- 'flac',
54
- 'm4a',
55
-
56
- // 视频
57
- 'mp4',
58
- 'avi',
59
- 'mov',
60
- 'wmv',
61
- 'flv',
62
- 'mkv',
63
- 'webm',
64
-
65
- // 压缩包
66
- 'zip',
67
- 'rar',
68
- '7z',
69
- 'tar',
70
- 'gz',
71
- 'bz2',
72
- 'xz',
73
-
74
- // 可执行/程序
75
- 'exe',
76
- 'dmg',
77
- 'pkg',
78
- 'apk',
79
- 'ipa',
80
-
81
- // 其他常见
82
- 'log',
83
- 'dat',
84
- 'bin'
85
- ]
86
- /**
87
- * 扩展名按长度降序,用于正则交替,避免 xls 抢先匹配 xlsx、htm 抢先匹配 html 等
88
- */
89
- const EXT_SORTED_FOR_REGEX = [...EXT_LIST].sort((a, b) => b.length - a.length)
90
-
91
- /** 去除控制符、零宽字符等常见脏值 */
92
- function stripMobookNoise(s: string) {
93
- return s.replace(/[\u0000-\u001F\u007F\u200B-\u200D\u200E\u200F\uFEFF]/g, '')
94
- }
95
-
96
- /**
97
- * 从文本中扫描 `.../mobook/...` 或 `...\mobook\...` 片段,按最长后缀匹配合法扩展名(兜底)
98
- */
99
- function collectMobookPathsAfterNeedle(text: string, lower: string, needle: string, result: Set<string>): void {
100
- let from = 0
101
- while (from < text.length) {
102
- const i = lower.indexOf(needle, from)
103
- if (i < 0) break
104
- const start = i + needle.length
105
- const tail = text.slice(start)
106
- const seg = tail.match(/^([^\s\]\)'"}\u3002,,]+)/)
107
- if (!seg) {
108
- from = start + 1
109
- continue
110
- }
111
- let raw = stripMobookNoise(seg[1]).trim()
112
- if (!raw || raw.includes('\uFFFD')) {
113
- from = start + 1
114
- continue
115
- }
116
- const low = raw.toLowerCase()
117
- let matchedExt: string | undefined
118
- for (const ext of EXT_SORTED_FOR_REGEX) {
119
- if (low.endsWith(`.${ext}`)) {
120
- matchedExt = ext
121
- break
122
- }
123
- }
124
- if (!matchedExt) {
125
- from = start + 1
126
- continue
127
- }
128
- const base = raw.slice(0, -(matchedExt.length + 1))
129
- const fileName = `${base}.${matchedExt}`
130
- if (isValidFileName(fileName)) {
131
- result.add(normalizePath(`/mobook/${fileName}`))
132
- }
133
- from = start + 1
134
- }
135
- }
136
-
137
- function collectMobookPathsByScan(text: string, result: Set<string>): void {
138
- const lower = text.toLowerCase()
139
- collectMobookPathsAfterNeedle(text, lower, '/mobook/', result)
140
- collectMobookPathsAfterNeedle(text, lower, '\\mobook\\', result)
141
- }
142
-
143
- export function extractMobookFiles(text = '') {
144
- if (typeof text !== 'string' || !text.trim()) return []
145
- // 全角冒号(中文输入常见)→ 半角,便于匹配 c:\mobook\
146
- text = text.replace(/\uFF1A/g, ':')
147
- const result = new Set<string>()
148
- // ✅ 扩展名(必须长扩展名优先,见 EXT_SORTED_FOR_REGEX)
149
- const EXT = `(${EXT_SORTED_FOR_REGEX.join('|')})`
150
- // ✅ 文件名字符(增强:支持中文、符号)
151
- const FILE_NAME = `[\\w\\u4e00-\\u9fa5::《》()()\\-\\s]+?`
152
- try {
153
- // 1️⃣ `xxx.xxx`
154
- const backtickReg = new RegExp(`\`([^\\\`]+?\\.${EXT})\``, 'gi')
155
- ;(text.match(backtickReg) || []).forEach((item) => {
156
- const name = item.replace(/`/g, '').trim()
157
- if (isValidFileName(name)) {
158
- result.add(`/mobook/${name}`)
159
- }
160
- })
161
- // 2️⃣ /mobook/xxx.xxx
162
- const fullPathReg = new RegExp(`/mobook/${FILE_NAME}\\.${EXT}`, 'gi')
163
- ;(text.match(fullPathReg) || []).forEach((p) => {
164
- result.add(normalizePath(p))
165
- })
166
- // 2️⃣b Windows 实际保存路径:C:\mobook\xxx、c:/mobook/xxx、\mobook\xxx(模型常写反斜杠,原先无法识别)
167
- const winMobookReg = new RegExp(`(?:[a-zA-Z]:)?[/\\\\]mobook[/\\\\]${FILE_NAME}\\.${EXT}`, 'gi')
168
- ;(text.match(winMobookReg) || []).forEach((full) => {
169
- const name = full.replace(/^(?:[a-zA-Z]:)?[/\\\\]mobook[/\\\\]/i, '').trim()
170
- if (isValidFileName(name)) {
171
- result.add(normalizePath(`/mobook/${name}`))
172
- }
173
- })
174
- // 3️⃣ mobook下的 xxx.xxx
175
- const inlineReg = new RegExp(`mobook下的\\s*(${FILE_NAME}\\.${EXT})`, 'gi')
176
- ;(text.match(inlineReg) || []).forEach((item) => {
177
- const match = item.match(new RegExp(`${FILE_NAME}\\.${EXT}`, 'i'))
178
- if (match && isValidFileName(match[0])) {
179
- result.add(`/mobook/${match[0].trim()}`)
180
- }
181
- })
182
- // 🆕 4️⃣ **xxx.xxx**
183
- const boldReg = new RegExp(`\\*\\*(${FILE_NAME}\\.${EXT})\\*\\*`, 'gi')
184
- ;(text.match(boldReg) || []).forEach((item) => {
185
- const name = item.replace(/\*\*/g, '').trim()
186
- if (isValidFileName(name)) {
187
- result.add(`/mobook/${name}`)
188
- }
189
- })
190
- // 🆕 5️⃣ xxx.xxx (123字节)
191
- const looseReg = new RegExp(`(${FILE_NAME}\\.${EXT})\\s*\\(`, 'gi')
192
- ;(text.match(looseReg) || []).forEach((item) => {
193
- const name = item.replace(/\s*\(.+$/, '').trim()
194
- if (isValidFileName(name)) {
195
- result.add(`/mobook/${name}`)
196
- }
197
- })
198
- // 6️⃣ 兜底:绝对路径等 `.../mobook/<文件名>.<扩展名>` + 最长后缀匹配 + 去脏字符
199
- collectMobookPathsByScan(text, result)
200
- } catch (e) {
201
- dcgLogger(`extractMobookFiles error:${e}`, 'error')
202
- }
203
- return [...result]
204
- }
205
-
206
- /**
207
- * 校验文件名是否合法(避免脏数据)
208
- */
209
- function isValidFileName(name: string) {
210
- if (!name) return false
211
- const cleaned = stripMobookNoise(name).trim()
212
- if (!cleaned) return false
213
- if (cleaned.includes('\uFFFD')) return false
214
- // 过滤异常字符
215
- if (/[\/\\<>:"|?*]/.test(cleaned)) return false
216
- // 长度限制(防止异常长字符串)
217
- if (cleaned.length > 200) return false
218
- return true
219
- }
220
-
221
- /**
222
- * 规范路径(去重用)
223
- */
224
- function normalizePath(path: string) {
225
- return path
226
- .replace(/\/+/g, '/') // 多斜杠 → 单斜杠
227
- .replace(/\/$/, '') // 去掉结尾 /
228
- }
@@ -1,137 +0,0 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
- import { getWorkspaceDir, snapshotGlobalSessionState, restoreGlobalSessionState, type GlobalSessionStateSnapshot } from './global.js'
4
- import { snapshotParamsState, restoreParamsState, type ParamsStateSnapshot } from './params.js'
5
- import { dcgLogger } from './log.js'
6
-
7
- /** 子 agent 跟踪状态(由 tool.ts 侧维护)的可序列化快照 */
8
- export type SubagentTrackingSnapshot = {
9
- activeSubagentRunIdsByRequester: Array<[string, string[]]>
10
- requesterByChildSessionKey: Array<[string, string]>
11
- runIdByChildSessionKey: Array<[string, string]>
12
- requesterSessionKeyByRunId?: Array<[string, string]>
13
- childSessionKeyByRunId?: Array<[string, string]>
14
- }
15
-
16
- /** 完整的会话相关内存状态快照 */
17
- export type SessionStateFile = {
18
- version: number
19
- savedAt: number
20
- params: ParamsStateSnapshot
21
- global: GlobalSessionStateSnapshot
22
- subagent: SubagentTrackingSnapshot
23
- }
24
-
25
- const SESSION_STATE_FILE_VERSION = 1
26
- const SESSION_STATE_FILE_NAME = 'session-state.json'
27
-
28
- /**
29
- * 将快照落地到 `${workspace}/../session-state.json`,与 cron 走同级约定,
30
- * 避免运行时污染 workspace 文件树。workspace 未初始化时直接跳过。
31
- */
32
- function getSessionStateFilePath(): string | null {
33
- const ws = getWorkspaceDir()
34
- if (!ws) return null
35
- return path.join(path.dirname(ws), SESSION_STATE_FILE_NAME)
36
- }
37
-
38
- export function writeSessionStateFile(subagent: SubagentTrackingSnapshot): void {
39
- const filePath = getSessionStateFilePath()
40
- if (!filePath) {
41
- dcgLogger('writeSessionStateFile: workspace dir not initialized, skip', 'error')
42
- return
43
- }
44
- const global = snapshotGlobalSessionState()
45
- const runningSessionKeys = new Set(global.msgStatusBySessionKey.map(([k]) => k))
46
- if (runningSessionKeys.size === 0) {
47
- removeSessionStateFile()
48
- dcgLogger('session state skipped: no running sessions')
49
- return
50
- }
51
- const payload: SessionStateFile = {
52
- version: SESSION_STATE_FILE_VERSION,
53
- savedAt: Date.now(),
54
- params: snapshotParamsState(runningSessionKeys),
55
- global,
56
- subagent
57
- }
58
- try {
59
- const dir = path.dirname(filePath)
60
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
61
- const tmp = `${filePath}.tmp`
62
- fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8')
63
- fs.renameSync(tmp, filePath)
64
- dcgLogger(`session state persisted: ${filePath}`)
65
- } catch (e) {
66
- dcgLogger(`writeSessionStateFile failed: ${String(e)}`, 'error')
67
- }
68
- }
69
-
70
- export function readSessionStateFile(): SessionStateFile | null {
71
- const filePath = getSessionStateFilePath()
72
- if (!filePath || !fs.existsSync(filePath)) return null
73
- try {
74
- const raw = fs.readFileSync(filePath, 'utf8')
75
- const data = JSON.parse(raw) as SessionStateFile
76
- if (!data || typeof data !== 'object') return null
77
- if (data.version !== SESSION_STATE_FILE_VERSION) {
78
- dcgLogger(`readSessionStateFile: version mismatch ${data.version} vs ${SESSION_STATE_FILE_VERSION}`, 'error')
79
- return null
80
- }
81
- return data
82
- } catch (e) {
83
- dcgLogger(`readSessionStateFile failed: ${String(e)}`, 'error')
84
- return null
85
- }
86
- }
87
-
88
- /**
89
- * 恢复 params/global 两个模块的内存状态。subagent 跟踪由 tool.ts 侧自行 restore(避免循环依赖)。
90
- */
91
- export function restoreParamsAndGlobal(snap: SessionStateFile | null): void {
92
- if (!snap) return
93
- restoreParamsState(snap.params)
94
- restoreGlobalSessionState(snap.global)
95
- }
96
-
97
- export function removeSessionStateFile(): void {
98
- const filePath = getSessionStateFilePath()
99
- if (!filePath) return
100
- try {
101
- if (fs.existsSync(filePath)) fs.unlinkSync(filePath)
102
- } catch (e) {
103
- dcgLogger(`removeSessionStateFile failed: ${String(e)}`, 'error')
104
- }
105
- }
106
-
107
- function addSessionKeyToSet(out: Set<string>, k: string | undefined) {
108
- const t = k?.trim()
109
- if (t) out.add(t)
110
- }
111
-
112
- /**
113
- * 从持久化快照中收集所有与「会话」相关的 dcg sessionKey,供网关恢复后 `chat.send` 等用。
114
- * (不含 `sentMediaKeys` 外层 key:那是 messageId,不是 sessionKey。)
115
- */
116
- export function collectSessionKeysFromSnapshot(snap: SessionStateFile | null | undefined): string[] {
117
- if (!snap) return []
118
- const set = new Set<string>()
119
- for (const [k, status] of snap.global?.msgStatusBySessionKey ?? []) {
120
- if (status === 'running') addSessionKeyToSet(set, k)
121
- }
122
- return [...set]
123
- }
124
-
125
- /**
126
- * 网关恢复时只恢复顶层会话,避免对旧子会话发送「继续执行」但本地不恢复 runId 映射,
127
- * 导致后续子 agent 流式输出无法回到主消息。
128
- */
129
- export function collectRootSessionKeysFromSnapshot(snap: SessionStateFile | null | undefined): string[] {
130
- if (!snap) return []
131
- const childKeys = new Set<string>()
132
- for (const [child] of snap.subagent?.requesterByChildSessionKey ?? []) {
133
- const k = child?.trim()
134
- if (k) childKeys.add(k)
135
- }
136
- return collectSessionKeysFromSnapshot(snap).filter((k) => !childKeys.has(k))
137
- }