@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,244 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path'
3
- import { buildAgentMediaPayload } from 'openclaw/plugin-sdk/agent-media-payload'
4
- import { getDcgchatRuntime } from './global.js'
5
- import { dcgLogger } from './log.js'
6
- import { generateSignUrl } from '../request/api.js'
7
-
8
- export type DcgchatInboundMediaInfo = {
9
- path: string
10
- fileName: string
11
- contentType: string
12
- placeholder: string
13
- }
14
-
15
- export type DcgchatInboundFileRef = { name: string; url: string }
16
-
17
- export const DCGCHAT_INBOUND_MEDIA_MAX_BYTES = 500 * 1024 * 1024
18
-
19
- const INBOUND_FILE_URL_KEYS = ['url', 'fileUrl', 'file_url', 'link', 'href', 'path', 'ossKey', 'oss_key', 'key'] as const
20
- const INBOUND_FILE_NAME_KEYS = ['name', 'fileName', 'file_name', 'title', 'originalName', 'original_filename'] as const
21
- const INBOUND_FILE_ARRAY_KEYS = [
22
- 'files',
23
- 'file_list',
24
- 'fileList',
25
- 'attachments',
26
- 'media_files',
27
- 'mediaFiles',
28
- 'resources',
29
- 'uploads'
30
- ] as const
31
-
32
- function firstNonEmptyString(o: Record<string, unknown>, keys: readonly string[]): string | undefined {
33
- for (const k of keys) {
34
- const v = o[k]
35
- if (typeof v === 'string' && v.trim()) return v.trim()
36
- }
37
- return undefined
38
- }
39
-
40
- function fallbackFileNameFromUrl(url: string): string {
41
- try {
42
- return path.basename(new URL(url).pathname) || 'file'
43
- } catch {
44
- const base = url.split(/[\\/]/).pop() ?? url
45
- return path.basename(base.split('?')[0] || base) || 'file'
46
- }
47
- }
48
-
49
- function coerceInboundFileItem(item: unknown): DcgchatInboundFileRef | null {
50
- if (item == null) return null
51
- if (typeof item === 'string') {
52
- const t = item.trim()
53
- if (!t) return null
54
- return { url: t, name: fallbackFileNameFromUrl(t) }
55
- }
56
- if (typeof item !== 'object') return null
57
- const o = item as Record<string, unknown>
58
- const url = firstNonEmptyString(o, INBOUND_FILE_URL_KEYS)
59
- if (!url) return null
60
- const name = firstNonEmptyString(o, INBOUND_FILE_NAME_KEYS) ?? ''
61
- return { url, name: name || fallbackFileNameFromUrl(url) }
62
- }
63
-
64
- function asInboundFileArray(raw: unknown): unknown[] {
65
- if (Array.isArray(raw)) return raw
66
- if (typeof raw === 'string') {
67
- const t = raw.trim()
68
- if (!t) return []
69
- try {
70
- const p = JSON.parse(t) as unknown
71
- return Array.isArray(p) ? p : []
72
- } catch {
73
- return []
74
- }
75
- }
76
- return []
77
- }
78
-
79
- /** 合并服务端可能使用的多种附件字段与对象形状,避免仅认 `files`+`url` 时视频未入队 */
80
- export function normalizeInboundFileList(content: Record<string, unknown>): DcgchatInboundFileRef[] {
81
- const seen = new Set<string>()
82
- const out: DcgchatInboundFileRef[] = []
83
- for (const key of INBOUND_FILE_ARRAY_KEYS) {
84
- for (const item of asInboundFileArray(content[key])) {
85
- const f = coerceInboundFileItem(item)
86
- if (f && !seen.has(f.url)) {
87
- seen.add(f.url)
88
- out.push(f)
89
- }
90
- }
91
- }
92
- for (const key of ['file', 'attachment', 'media'] as const) {
93
- const f = coerceInboundFileItem(content[key])
94
- if (f && !seen.has(f.url)) {
95
- seen.add(f.url)
96
- out.push(f)
97
- }
98
- }
99
- return out
100
- }
101
-
102
- /** 供模型侧识别媒体槽位类型 */
103
- export function inferPlaceholderFromContentType(contentType: string): string {
104
- const ct = (contentType || '').toLowerCase()
105
- if (ct.startsWith('image/')) return '<media:image>'
106
- if (ct.startsWith('video/')) return '<media:video>'
107
- if (ct.startsWith('audio/')) return '<media:audio>'
108
- if (
109
- ct === 'application/pdf' ||
110
- ct.includes('word') ||
111
- ct.includes('document') ||
112
- ct.includes('msword') ||
113
- ct.includes('officedocument') ||
114
- ct.includes('spreadsheet') ||
115
- ct.includes('presentation')
116
- ) {
117
- return '<media:document>'
118
- }
119
- return '<media:document>'
120
- }
121
-
122
- /**
123
- * 使用 SDK 与飞书通道一致的 Media* 布局,并保留 dcgchat 原有的文件名字段供工具/模板使用。
124
- */
125
- export function buildDcgchatInboundMediaPayload(mediaList: DcgchatInboundMediaInfo[]): Record<string, unknown> {
126
- if (mediaList.length === 0) return {}
127
- const base = buildAgentMediaPayload(mediaList.map((m) => ({ path: m.path, contentType: m.contentType || null })))
128
- const mediaFileNames = mediaList.map((m) => m.fileName).filter(Boolean)
129
- if (mediaFileNames.length === 0) return { ...base }
130
- return {
131
- ...base,
132
- MediaFileName: mediaFileNames[0],
133
- MediaFileNames: mediaFileNames
134
- }
135
- }
136
-
137
- /** BodyForAgent:含附件说明与槽位行;与 RawBody/CommandBody(纯用户侧文本)分离 */
138
- export function buildAgentInboundBodySuffix(userText: string, mediaList: DcgchatInboundMediaInfo[]): string {
139
- if (mediaList.length === 0) return userText
140
- const brief = mediaList.map((m, i) => `${i + 1}) ${m.fileName} [${m.contentType || 'unknown'}]`).join('; ')
141
- const slots = mediaList.map((m) => m.placeholder).join('\n')
142
- return `${userText}\n\n[系统: 用户已随本消息上传 ${mediaList.length} 个附件: ${brief}。请勿要求用户再次上传;下列槽位与 MediaPaths/MediaTypes 顺序一致。]\n${slots}`
143
- }
144
-
145
- const DEFAULT_INBOUND_FETCH_TIMEOUT_MS = 180_000
146
- const DEFAULT_INBOUND_DOWNLOAD_CONCURRENCY = 3
147
-
148
- async function fetchWithTimeout(url: string, timeoutMs: number): Promise<Response> {
149
- const ctrl = new AbortController()
150
- const t = setTimeout(() => ctrl.abort(new Error(`fetch timeout ${timeoutMs}ms`)), timeoutMs)
151
- try {
152
- return await fetch(url, { signal: ctrl.signal })
153
- } finally {
154
- clearTimeout(t)
155
- }
156
- }
157
-
158
- function resolveFilepath(savedFilePath: string, filename: string) {
159
- if (path.extname(savedFilePath)) return savedFilePath
160
-
161
- const ext = path.extname(filename)
162
- if (!ext) return savedFilePath
163
-
164
- const newPath = `${savedFilePath}${ext}`
165
- try {
166
- fs.renameSync(savedFilePath, newPath);
167
- return newPath
168
- } catch (err) {
169
- dcgLogger(`media: failed to append extension to ${savedFilePath}: ${String(err)}`, 'error')
170
- return savedFilePath
171
- }
172
- }
173
-
174
- async function resolveOneInboundFile(
175
- file: DcgchatInboundFileRef,
176
- botToken: string,
177
- maxBytes: number,
178
- fetchTimeoutMs: number
179
- ): Promise<DcgchatInboundMediaInfo | null> {
180
- const core = getDcgchatRuntime()
181
- try {
182
- let data = ''
183
- if (/^https?:\/\//i.test(file.url)) {
184
- data = file.url
185
- } else {
186
- data = await generateSignUrl(file.url, botToken)
187
- }
188
- const response = await fetchWithTimeout(data, fetchTimeoutMs)
189
- if (!response.ok) {
190
- dcgLogger?.(`media: ${file.url} fetch failed with HTTP ${response.status}`, 'error')
191
- return null
192
- }
193
- const buffer = Buffer.from(await response.arrayBuffer())
194
-
195
- let contentType = response.headers.get('content-type') || ''
196
- if (!contentType) {
197
- contentType = (await core.media.detectMime({ buffer })) || ''
198
- }
199
- const fileName = file.name || fallbackFileNameFromUrl(file.url)
200
- const saved = await core.channel.media.saveMediaBuffer(buffer, contentType, 'inbound', maxBytes, fileName)
201
- const effectiveType = saved.contentType || contentType
202
-
203
- const finalPath = resolveFilepath(saved.path, fileName)
204
-
205
- return {
206
- path: finalPath,
207
- fileName,
208
- contentType: effectiveType,
209
- placeholder: inferPlaceholderFromContentType(effectiveType)
210
- }
211
- } catch (err) {
212
- dcgLogger(`media: ${file.url} FAILED to process: ${String(err)}`, 'error')
213
- return null
214
- }
215
- }
216
-
217
- export async function resolveMediaFromUrls(
218
- files: DcgchatInboundFileRef[],
219
- botToken: string,
220
- opts?: { maxBytes?: number; fetchTimeoutMs?: number; concurrency?: number }
221
- ): Promise<DcgchatInboundMediaInfo[]> {
222
- const maxBytes = opts?.maxBytes ?? DCGCHAT_INBOUND_MEDIA_MAX_BYTES
223
- const fetchTimeoutMs = opts?.fetchTimeoutMs ?? DEFAULT_INBOUND_FETCH_TIMEOUT_MS
224
- const conc = Math.max(1, opts?.concurrency ?? DEFAULT_INBOUND_DOWNLOAD_CONCURRENCY)
225
- const concurrency = files.length === 0 ? 1 : Math.min(conc, files.length)
226
-
227
- dcgLogger(`media: inbound files count=${files.length}, concurrency=${concurrency}, fetchTimeoutMs=${fetchTimeoutMs}`)
228
- const results: (DcgchatInboundMediaInfo | null)[] = new Array(files.length).fill(null)
229
- let nextIndex = 0
230
-
231
- async function worker() {
232
- while (true) {
233
- const i = nextIndex++
234
- if (i >= files.length) break
235
- results[i] = await resolveOneInboundFile(files[i], botToken, maxBytes, fetchTimeoutMs)
236
- }
237
- }
238
-
239
- await Promise.all(Array.from({ length: concurrency }, () => worker()))
240
-
241
- const out = results.filter((x): x is DcgchatInboundMediaInfo => x != null)
242
- dcgLogger(`media: resolve complete, ${out.length}/${files.length} file(s) succeeded`)
243
- return out
244
- }
@@ -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,117 +0,0 @@
1
- import type { IMsgParams } from '../types.js'
2
- import { sendChunk } from '../transport.js'
3
- import { agentErrorPayloadText } from './agentErrors.js'
4
- import { takeStreamChunkIdxAndAdvance } from './inboundTurnState.js'
5
- import { isReviserExpert } from './global.js'
6
-
7
- type OutboundTextState = {
8
- inboundGen: number
9
- lastStreamSnapshot: string
10
- /** 仅 onPartialReply 成功下发过正文时为 true,用于 deliver 兜底判断 */
11
- hasStreamedAssistantText: boolean
12
- hasEmittedAssistantText: boolean
13
- }
14
-
15
- const stateBySessionKey = new Map<string, OutboundTextState>()
16
-
17
- /** 每轮入站 dispatch 开始前初始化,与 inboundGen 绑定供调试对齐 */
18
- export function resetOutboundTextState(sessionKey: string, inboundGen: number): void {
19
- const k = sessionKey.trim()
20
- if (!k) return
21
- stateBySessionKey.set(k, {
22
- inboundGen,
23
- lastStreamSnapshot: '',
24
- hasStreamedAssistantText: false,
25
- hasEmittedAssistantText: false
26
- })
27
- }
28
-
29
- export function clearOutboundTextState(sessionKey: string): void {
30
- const k = sessionKey.trim()
31
- if (!k) return
32
- stateBySessionKey.delete(k)
33
- }
34
-
35
- function getState(sessionKey: string): OutboundTextState | undefined {
36
- const k = sessionKey.trim()
37
- if (!k) return undefined
38
- return stateBySessionKey.get(k)
39
- }
40
-
41
- /** 子 agent 等路径可能未走 bot reset;懒初始化保证 dcgchat_message 仍可出站且参与快照去重 */
42
- function ensureState(sessionKey: string): OutboundTextState | undefined {
43
- const k = sessionKey.trim()
44
- if (!k) return undefined
45
- let state = stateBySessionKey.get(k)
46
- if (!state) {
47
- state = {
48
- inboundGen: 0,
49
- lastStreamSnapshot: '',
50
- hasStreamedAssistantText: false,
51
- hasEmittedAssistantText: false
52
- }
53
- stateBySessionKey.set(k, state)
54
- }
55
- return state
56
- }
57
-
58
- export function outboundHasStreamedAssistantText(sessionKey: string): boolean {
59
- return getState(sessionKey)?.hasStreamedAssistantText ?? false
60
- }
61
-
62
- export function outboundHasEmittedAssistantText(sessionKey: string): boolean {
63
- return getState(sessionKey)?.hasEmittedAssistantText ?? false
64
- }
65
-
66
- /** 仅重置快照(用于 `replace` 流式片段),保留 generation 与已下发标记 */
67
- export function resetOutboundTextSnapshot(sessionKey: string): void {
68
- const k = sessionKey.trim()
69
- if (!k) return
70
- const state = ensureState(k)
71
- if (state) {
72
- state.lastStreamSnapshot = ''
73
- }
74
- }
75
-
76
- /**
77
- * 统一助手正文出站:bot 流式/deliver 与 dcgchat_message 共用快照增量,避免双发或漏发。
78
- * @param markStreamed 为 true 时表示来自 onPartialReply,会置 hasStreamedAssistantText
79
- * @param isError 仅对 OpenClaw 明确标记的错误载荷做用户可见翻译,避免普通正文中的错误短语被误判
80
- */
81
- export function emitOutboundAssistantText(
82
- sessionKey: string,
83
- raw: string | undefined,
84
- outboundCtx: IMsgParams,
85
- options?: { markStreamed?: boolean; isError?: boolean; emitDelta?: (delta: string) => boolean | void }
86
- ): boolean {
87
- if (!raw || isReviserExpert(sessionKey)) return false
88
- const state = ensureState(sessionKey)
89
- if (!state) return false
90
-
91
- const t = raw
92
- let delta = ''
93
- if (t.startsWith(state.lastStreamSnapshot)) {
94
- delta = t.slice(state.lastStreamSnapshot.length)
95
- state.lastStreamSnapshot = t
96
- } else if (state.lastStreamSnapshot.startsWith(t)) {
97
- // 快照缩短(模型修订等):不重复下发
98
- } else {
99
- delta = t
100
- state.lastStreamSnapshot = t
101
- }
102
-
103
- if (!delta.trim()) return false
104
-
105
- delta = agentErrorPayloadText(delta, options?.isError === true)
106
-
107
- const emitted =
108
- options?.emitDelta != null
109
- ? options.emitDelta(delta) !== false
110
- : sendChunk(delta, outboundCtx, takeStreamChunkIdxAndAdvance(sessionKey))
111
- if (!emitted) return false
112
- state.hasEmittedAssistantText = true
113
- if (options?.markStreamed) {
114
- state.hasStreamedAssistantText = true
115
- }
116
- return true
117
- }
@@ -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
- }