@dcrays/dcgchat-test 0.5.0-alpha.2 → 0.5.0-alpha.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 (42) hide show
  1. package/index.js +292 -0
  2. package/package.json +7 -15
  3. package/schemas/gateway-cron-finished.payload.json +39 -0
  4. package/index.ts +0 -24
  5. package/src/agent.ts +0 -128
  6. package/src/bot.ts +0 -515
  7. package/src/channel.ts +0 -474
  8. package/src/cron.ts +0 -199
  9. package/src/cronToolCall.ts +0 -202
  10. package/src/gateway/cronFinishedPayload.ts +0 -118
  11. package/src/gateway/index.ts +0 -452
  12. package/src/gateway/security.ts +0 -95
  13. package/src/gateway/socket.ts +0 -285
  14. package/src/libs/ali-oss-6.23.0.tgz +0 -0
  15. package/src/libs/axios-1.13.6.tgz +0 -0
  16. package/src/libs/md5-2.3.0.tgz +0 -0
  17. package/src/libs/mime-types-3.0.2.tgz +0 -0
  18. package/src/libs/unzipper-0.12.3.tgz +0 -0
  19. package/src/libs/ws-8.19.0.tgz +0 -0
  20. package/src/monitor.ts +0 -165
  21. package/src/request/api.ts +0 -70
  22. package/src/request/oss.ts +0 -212
  23. package/src/request/request.ts +0 -192
  24. package/src/request/userInfo.ts +0 -93
  25. package/src/session.ts +0 -19
  26. package/src/sessionTermination.ts +0 -168
  27. package/src/skill.ts +0 -146
  28. package/src/tool.ts +0 -403
  29. package/src/tools/messageTool.ts +0 -273
  30. package/src/transport.ts +0 -206
  31. package/src/types.ts +0 -139
  32. package/src/utils/agentErrors.ts +0 -23
  33. package/src/utils/constant.ts +0 -7
  34. package/src/utils/gatewayMsgHanlder.ts +0 -84
  35. package/src/utils/global.ts +0 -161
  36. package/src/utils/log.ts +0 -15
  37. package/src/utils/params.ts +0 -88
  38. package/src/utils/searchFile.ts +0 -228
  39. package/src/utils/workspaceFilePaths.ts +0 -89
  40. package/src/utils/wsMessageHandler.ts +0 -64
  41. package/src/utils/zipExtract.ts +0 -97
  42. package/src/utils/zipPath.ts +0 -24
@@ -1,84 +0,0 @@
1
- import type { GatewayEvent } from '../gateway/index.js'
2
- import { finishedDcgchatCron, getCronJobsPath, readCronJob, sendDcgchatCron } from '../cron.js'
3
- import { sendDcgchatMedia } from '../channel.js'
4
- import { resolveCronFinishedLocalPaths } from '../gateway/cronFinishedPayload.js'
5
- import { channelInfo, ENV } from './constant.js'
6
- import { dcgLogger } from './log.js'
7
- import { getEffectiveMsgParams, getSessionKeyBySubAgentRunId } from './params.js'
8
- import { sendChunk } from '../transport.js'
9
- import { getCronMessageId, getOpenClawConfig, getWorkspaceDir, setCronMessageId } from './global.js'
10
-
11
- /**
12
- * 处理网关 event 帧的副作用(agent 流式输出、cron 同步),并构造供上层分发的 GatewayEvent。
13
- */
14
- export async function handleGatewayEventMessage(msg: {
15
- event?: string
16
- payload?: Record<string, unknown>
17
- seq?: number
18
- }): Promise<GatewayEvent> {
19
- try {
20
- // 子agent消息输出
21
- if (msg.event === 'agent') {
22
- const pl = msg.payload as { runId: string; data?: { delta?: unknown } }
23
- const sessionKey = getSessionKeyBySubAgentRunId(pl.runId)
24
- const outboundCtx = getEffectiveMsgParams(sessionKey)
25
- if (pl.data?.delta) {
26
- if (outboundCtx.sessionId) {
27
- sendChunk(pl.data.delta as string, outboundCtx, 0)
28
- }
29
- }
30
- }
31
- // 定时任务
32
- if (msg.event === 'cron') {
33
- const p = msg.payload
34
- dcgLogger(`[Gateway] 收到定时任务事件: ${JSON.stringify(p)}`)
35
- if (p?.action === 'added') {
36
- sendDcgchatCron(p?.jobId as string)
37
- }
38
- if (p?.action === 'updated') {
39
- sendDcgchatCron(p?.jobId as string)
40
- }
41
- if (p?.action === 'removed') {
42
- sendDcgchatCron(p?.jobId as string)
43
- }
44
- if (p?.action === 'finished' && p?.status === 'ok') {
45
- let summary = typeof p?.summary === 'string' ? p.summary : ''
46
- if (summary.indexOf('HEARTBEAT_OK') >= 0 && summary !== 'HEARTBEAT_OK') {
47
- summary = summary.replace('HEARTBEAT_OK', '')
48
- }
49
-
50
- const jobIdStr = typeof p?.jobId === 'string' ? p.jobId.trim() : ''
51
- const cfg = getOpenClawConfig()
52
- const chCfg = cfg?.channels?.["dcgchat-test" as keyof NonNullable<typeof cfg.channels>] as { allowedPaths?: string[] } | undefined
53
- const attachmentPaths = resolveCronFinishedLocalPaths(p, summary, getWorkspaceDir(), chCfg?.allowedPaths)
54
- const jobPath = getCronJobsPath()
55
- const job = jobIdStr ? readCronJob(jobPath, jobIdStr) : null
56
- const sessionKey = job && typeof job.sessionKey === 'string' ? job.sessionKey.trim() : ''
57
-
58
- if (sessionKey && attachmentPaths.length > 0) {
59
- const messageId = getCronMessageId(sessionKey) || `${Date.now()}`
60
- setCronMessageId(sessionKey, messageId)
61
- for (const mediaPath of attachmentPaths) {
62
- try {
63
- await sendDcgchatMedia({ sessionKey, mediaUrl: mediaPath, messageId })
64
- } catch (err) {
65
- dcgLogger(`[Gateway] cron 附件经通道发送失败: ${mediaPath} ${String(err)}`, 'error')
66
- }
67
- }
68
- } else if (attachmentPaths.length > 0 && !sessionKey) {
69
- dcgLogger(`[Gateway] cron finished 有可发送附件路径但 jobs.json 无 sessionKey: jobId=${jobIdStr}`, 'error')
70
- }
71
-
72
- const hasAttachments = attachmentPaths.length > 0
73
- finishedDcgchatCron(jobIdStr, summary, hasAttachments)
74
- }
75
- }
76
- } catch (error) {
77
- dcgLogger(`[Gateway] 处理事件失败: ${error}`, 'error')
78
- }
79
- return {
80
- type: msg.event as string,
81
- payload: msg.payload,
82
- seq: msg.seq as number | undefined
83
- }
84
- }
@@ -1,161 +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 { channelInfo, ENV } from './constant.js'
8
- import { dcgLogger } from './log.js'
9
-
10
- /** socket connection */
11
- let ws: WebSocket | null = null
12
-
13
- export function setWsConnection(next: WebSocket | null) {
14
- ws = next
15
- }
16
-
17
- export function getWsConnection(): WebSocket | null {
18
- return ws
19
- }
20
-
21
- let config: OpenClawConfig | null = null
22
-
23
- export function setOpenClawConfig(next: OpenClawConfig | null) {
24
- config = next
25
- }
26
-
27
- export function getOpenClawConfig(): OpenClawConfig | null {
28
- return config
29
- }
30
-
31
- function getWorkspacePath(): string | null {
32
- const workspacePath = path.join(
33
- os.homedir(),
34
- config?.channels?.["dcgchat-test"]?.appId == 110 ? '.mobook' : '.openclaw',
35
- 'workspace'
36
- )
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) {
47
- workspaceDir = dir
48
- }
49
- }
50
-
51
- export function getWorkspaceDir(): string {
52
- if (!workspaceDir) {
53
- dcgLogger?.('Workspace directory not initialized', 'error')
54
- }
55
- return workspaceDir
56
- }
57
-
58
- const { setRuntime: setDcgchatRuntime, getRuntime: getDcgchatRuntime } = createPluginRuntimeStore<PluginRuntime>(
59
- `${"dcgchat-test"} runtime not initialized`
60
- )
61
- export { setDcgchatRuntime, getDcgchatRuntime }
62
-
63
- export type MsgSessionStatus = 'running' | 'finished' | ''
64
-
65
- const msgStatusBySessionKey = new Map<string, MsgSessionStatus>()
66
-
67
- export function setMsgStatus(sessionKey: string, status: MsgSessionStatus) {
68
- if (!sessionKey?.trim()) return
69
- if (status === '') {
70
- msgStatusBySessionKey.delete(sessionKey)
71
- } else {
72
- msgStatusBySessionKey.set(sessionKey, status)
73
- }
74
- }
75
-
76
- export function getMsgStatus(sessionKey: string): MsgSessionStatus {
77
- if (!sessionKey?.trim()) return ''
78
- return msgStatusBySessionKey.get(sessionKey) ?? ''
79
- }
80
-
81
- const getMediaKey = (url: string) => url.split(/[\\/]/).pop() ?? url
82
-
83
- /** 已发送媒体去重:外层 messageId → 内层该会话下已发送的媒体 key(文件名) */
84
- const sentMediaKeysBySession = new Map<string, Set<string>>()
85
-
86
- function getSessionMediaSet(sessionId: string): Set<string> {
87
- let set = sentMediaKeysBySession.get(sessionId)
88
- if (!set) {
89
- set = new Set<string>()
90
- sentMediaKeysBySession.set(sessionId, set)
91
- }
92
- return set
93
- }
94
-
95
- export function addSentMediaKey(messageId: string, url: string) {
96
- getSessionMediaSet(messageId).add(getMediaKey(url))
97
- }
98
-
99
- export function hasSentMediaKey(sessionId: string, url: string): boolean {
100
- return sentMediaKeysBySession.get(sessionId)?.has(getMediaKey(url)) ?? false
101
- }
102
-
103
- /** 不传 messageId 时清空全部会话;传入则只清空该会话 */
104
- export function clearSentMediaKeys(sessionId?: string) {
105
- if (sessionId) {
106
- sentMediaKeysBySession.delete(sessionId)
107
- } else {
108
- sentMediaKeysBySession.clear()
109
- }
110
- }
111
-
112
- /** 每个 sessionKey 下多个定时任务 messageId,入队在尾、出队在头(FIFO) */
113
- const cronMessageIdMap = new Map<string, string[]>()
114
-
115
- export function setCronMessageId(sk: string, messageId: string | number | null | undefined) {
116
- const mid = messageId != null && messageId !== '' ? String(messageId).trim() : ''
117
- if (!sk?.trim() || !mid) return
118
- let q = cronMessageIdMap.get(sk)
119
- if (!q) {
120
- q = []
121
- cronMessageIdMap.set(sk, q)
122
- }
123
- q.push(mid)
124
- }
125
-
126
- /** 窥视队首 messageId(不移除),供发送中与 finished 配对使用 */
127
- export function getCronMessageId(sk: string): string {
128
- if (!sk?.trim()) return ''
129
- return cronMessageIdMap.get(sk)?.[0] ?? ''
130
- }
131
-
132
- /** 弹出队首一条,与一次 finished 对应;队列为空时移除 key */
133
- export function removeCronMessageId(sk: string) {
134
- if (!sk?.trim()) return
135
- const q = cronMessageIdMap.get(sk)
136
- if (!q?.length) return
137
- q.shift()
138
- if (q.length === 0) {
139
- cronMessageIdMap.delete(sk)
140
- }
141
- }
142
-
143
- export const getSessionKey = (content: any, accountId: string) => {
144
- const { real_mobook, agent_id, agent_clone_code, session_id } = content
145
- const core = getDcgchatRuntime()
146
-
147
- const agentCode = agent_clone_code || 'main'
148
-
149
- const route = core.channel.routing.resolveAgentRoute({
150
- cfg: getOpenClawConfig() as OpenClawConfig,
151
- channel: "dcgchat-test",
152
- accountId: accountId || 'default',
153
- peer: { kind: 'direct', id: session_id }
154
- })
155
- return real_mobook == '1' ? route.sessionKey : `agent:${agentCode}:mobook:direct:${agent_id}:${session_id}`.toLowerCase()
156
- }
157
-
158
- export function getInfoBySessionKey(sk: string): { sessionId: string; agentId: string } {
159
- const sessionInfo = sk.split(':')
160
- return { sessionId: sessionInfo.at(-1) ?? '', agentId: sessionInfo.at(-2) ?? '' }
161
- }
package/src/utils/log.ts DELETED
@@ -1,15 +0,0 @@
1
- import { RuntimeEnv } from 'openclaw/plugin-sdk'
2
-
3
- let logger: RuntimeEnv | null = null
4
-
5
- export function setLogger(next: RuntimeEnv | null) {
6
- logger = next
7
- }
8
-
9
- export function dcgLogger(message: string, type: 'log' | 'error' = 'log'): void {
10
- if (logger) {
11
- logger[type](`书灵墨宝🚀 ~ [${new Date().toISOString()}] ${message}`)
12
- } else {
13
- console[type](`书灵墨宝🚀 ~ ${new Date().toISOString()} [${"dcgchat-test"}]: ${message}`)
14
- }
15
- }
@@ -1,88 +0,0 @@
1
- import { channelInfo, ENV } 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?.["dcgchat-test"] 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
- // sessionKey 对应的 子agent的runId
79
- const subagentRunIdMap = new Map<string, string>()
80
- export function getSessionKeyBySubAgentRunId(runId: string): string | undefined {
81
- return subagentRunIdMap.get(runId)
82
- }
83
- export function setSessionKeyBySubAgentRunId(runId: string, sessionKey: string) {
84
- subagentRunIdMap.set(runId, sessionKey)
85
- }
86
- export function deleteSessionKeyBySubAgentRunId(runId: string) {
87
- subagentRunIdMap.delete(runId)
88
- }
@@ -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}`)
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,89 +0,0 @@
1
- import os from 'node:os'
2
- import path from 'node:path'
3
-
4
- /** 统一为 POSIX 风格斜杠,便于跨平台判断。 */
5
- export function toPosixPath(p: string): string {
6
- return path.normalize(p.trim()).replace(/\\/g, '/')
7
- }
8
-
9
- /** `filepath` 解析后在 `rootDir` 内或等于 `rootDir`(防 `..` 逃逸)。 */
10
- export function isPathInsideDir(filepath: string, rootDir: string): boolean {
11
- const root = path.resolve(rootDir)
12
- const resolved = path.resolve(filepath)
13
- const rel = path.relative(root, resolved)
14
- if (rel.startsWith('..') || path.isAbsolute(rel)) return false
15
- return true
16
- }
17
-
18
- /**
19
- * 与 dcgchat_message 一致:允许的路径为当前工作区根、`allowedPaths`、或兼容挂载 /workspace、/mobook。
20
- */
21
- export function isAllowedSendPath(filepath: string, workspaceDir?: string, allowedPaths?: string[]): boolean {
22
- const ws = workspaceDir?.trim()
23
- if (ws && isPathInsideDir(filepath, ws)) return true
24
- if (allowedPaths?.length) {
25
- for (const allowed of allowedPaths) {
26
- if (isPathInsideDir(filepath, allowed)) return true
27
- }
28
- }
29
- const p = toPosixPath(filepath)
30
- if (p.startsWith('/workspace/') || p === '/workspace') return true
31
- if (p.startsWith('/mobook/') || p === '/mobook') return true
32
- return /^[A-Za-z]:\/(workspace|mobook)(\/|$)/.test(p)
33
- }
34
-
35
- /** 去掉 Markdown/标点误粘在路径末尾的字符(如 `**`、反引号、右括号)。 */
36
- export function trimArtifactPathCandidate(raw: string): string {
37
- return raw.replace(/[`'",,*_)\]]+$/u, '').trimEnd()
38
- }
39
-
40
- /**
41
- * 将正文里的 `~/...` / `~\...` 展开为绝对路径(`path.resolve` 不会处理 `~`)。
42
- */
43
- export function expandTildePath(input: string): string {
44
- const s = input.trim()
45
- if (!s) return s
46
- if (s === '~') return os.homedir()
47
- if (s.startsWith('~/')) {
48
- return path.resolve(os.homedir(), s.slice(2))
49
- }
50
- if (s.startsWith('~\\')) {
51
- return path.resolve(os.homedir(), s.slice(2))
52
- }
53
- return s
54
- }
55
-
56
- /**
57
- * 从正文中提取可作为附件发送的本地路径(与 messageTool 规则一致:工作区前缀、`/workspace`、`~` 等)。
58
- */
59
- export function extractWorkspaceFilePathsFromText(text: string | undefined, workspaceDir?: string): string[] {
60
- if (!text) return []
61
- const unix = text.match(/\/workspace\/[^\s]+|\/mobook\/[^\s]+/g) ?? []
62
- const win = text.match(/[A-Za-z]:[/\\](?:workspace|mobook)[/\\][^\s]+/g) ?? []
63
- const tildePaths = text.match(/~[/\\][^\s`'")\]]+/g) ?? []
64
- const underWs: string[] = []
65
- const ws = workspaceDir?.trim()
66
- if (ws) {
67
- const variants = new Set<string>()
68
- variants.add(ws)
69
- variants.add(toPosixPath(ws))
70
- if (path.sep === '\\') variants.add(ws.replace(/\//g, '\\'))
71
- for (const prefix of variants) {
72
- if (!prefix) continue
73
- let from = 0
74
- while (from < text.length) {
75
- const i = text.indexOf(prefix, from)
76
- if (i === -1) break
77
- let end = i + prefix.length
78
- while (end < text.length && !/\s/.test(text[end])) end++
79
- underWs.push(text.slice(i, end))
80
- from = i + 1
81
- }
82
- }
83
- }
84
- const cleaned = [...unix, ...win, ...tildePaths, ...underWs]
85
- .map((s) => trimArtifactPathCandidate(s))
86
- .filter(Boolean)
87
- const resolved = cleaned.map((s) => (s.startsWith('~') ? expandTildePath(s) : s))
88
- return [...new Set(resolved)]
89
- }