@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.
- package/index.js +292 -0
- package/package.json +7 -15
- package/schemas/gateway-cron-finished.payload.json +39 -0
- package/index.ts +0 -24
- package/src/agent.ts +0 -128
- package/src/bot.ts +0 -515
- package/src/channel.ts +0 -474
- package/src/cron.ts +0 -199
- package/src/cronToolCall.ts +0 -202
- package/src/gateway/cronFinishedPayload.ts +0 -118
- package/src/gateway/index.ts +0 -452
- package/src/gateway/security.ts +0 -95
- package/src/gateway/socket.ts +0 -285
- package/src/libs/ali-oss-6.23.0.tgz +0 -0
- package/src/libs/axios-1.13.6.tgz +0 -0
- package/src/libs/md5-2.3.0.tgz +0 -0
- package/src/libs/mime-types-3.0.2.tgz +0 -0
- package/src/libs/unzipper-0.12.3.tgz +0 -0
- package/src/libs/ws-8.19.0.tgz +0 -0
- package/src/monitor.ts +0 -165
- package/src/request/api.ts +0 -70
- package/src/request/oss.ts +0 -212
- package/src/request/request.ts +0 -192
- package/src/request/userInfo.ts +0 -93
- package/src/session.ts +0 -19
- package/src/sessionTermination.ts +0 -168
- package/src/skill.ts +0 -146
- package/src/tool.ts +0 -403
- package/src/tools/messageTool.ts +0 -273
- package/src/transport.ts +0 -206
- package/src/types.ts +0 -139
- package/src/utils/agentErrors.ts +0 -23
- package/src/utils/constant.ts +0 -7
- package/src/utils/gatewayMsgHanlder.ts +0 -84
- package/src/utils/global.ts +0 -161
- package/src/utils/log.ts +0 -15
- package/src/utils/params.ts +0 -88
- package/src/utils/searchFile.ts +0 -228
- package/src/utils/workspaceFilePaths.ts +0 -89
- package/src/utils/wsMessageHandler.ts +0 -64
- package/src/utils/zipExtract.ts +0 -97
- 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
|
-
}
|
package/src/utils/global.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/params.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/searchFile.ts
DELETED
|
@@ -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
|
-
}
|