@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,329 +0,0 @@
1
- import type { ChannelPlugin, OpenClawConfig } from 'openclaw/plugin-sdk'
2
- import { DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk/account-id'
3
- import type { ResolvedDcgchatAccount, DcgchatConfig } from '../types.js'
4
- import { getCronMessageId, getDcgchatRuntime, getOpenClawConfig, takeApartSessionKey } from '../utils/global.js'
5
- import { mergeDefaultParams, wsSendRaw } from '../transport.js'
6
- import { CHANNEL_ID } from '../utils/constant.js'
7
- import { dcgLogger, setLogger } from '../utils/log.js'
8
- import { getOutboundMsgParams } from '../utils/params.js'
9
- import { isSessionActiveForTool } from '../tool.js'
10
- import { startDcgchatGatewaySocket } from '../gateway/socket.js'
11
- import { dispatchChannelOutboundSendMedia } from './outboundMedia.js'
12
- import { outboundChatId, normalizeSessionTarget } from './outboundTarget.js'
13
- import { ChannelOutboundContext } from 'openclaw/plugin-sdk/channel-runtime'
14
-
15
- export type { DcgchatMediaSendOptions } from './outboundMedia.js'
16
- export { normalizeOutboundMediaPaths, sendDcgchatMedia } from './outboundMedia.js'
17
-
18
- function dcgchatChannelCfg(): DcgchatConfig {
19
- return (getOpenClawConfig()?.channels?.[CHANNEL_ID] as DcgchatConfig | undefined) ?? {}
20
- }
21
-
22
- /** `agent:<code>:mobook:direct:<agentId>:<sessionId>`(与 getSessionKey 非 real_mobook 分支一致) */
23
- function isMobookDirectSessionKey(s: string): boolean {
24
- const parts = s.split(':').filter((p) => p.length > 0)
25
- const low = parts.map((p) => p.toLowerCase())
26
- return parts.length >= 6 && low[0] === 'agent' && low[2] === 'mobook' && low[3] === 'direct'
27
- }
28
-
29
- /** real_mobook 等线路下 Core 分配的 `agent:<agentId>:…` sessionKey */
30
- function isAgentPrefixedSessionKey(s: string): boolean {
31
- const parts = s.split(':').filter((p) => p.length > 0)
32
- return parts.length >= 3 && parts[0].toLowerCase() === 'agent'
33
- }
34
-
35
- /**
36
- * 供 `messaging.targetResolver.looksLikeId` 使用:与 OpenClaw `resolveMessagingTarget` 对齐,
37
- * 仅当 target「像合法会话路由键」时才走 id 类解析;纯数字不会命中,从而在系统层拒绝误填 userId。
38
- */
39
- function looksLikeDcgchatMessageToolTarget(raw: string): boolean {
40
- let s = raw.trim()
41
- if (!s) return false
42
- const prefix = 'dcg-cron:'
43
- if (s.startsWith(prefix)) {
44
- s = s.slice(prefix.length).trim()
45
- if (!s) return false
46
- }
47
- if (isMobookDirectSessionKey(s)) return true
48
- if (isAgentPrefixedSessionKey(s)) return true
49
- return false
50
- }
51
-
52
- function dcgchatMessageTargetLooksLikeId(raw: string, _normalized?: string): boolean {
53
- if (dcgchatChannelCfg().strictMessageToolTarget === false) {
54
- return Boolean(raw?.trim())
55
- }
56
- return looksLikeDcgchatMessageToolTarget(raw)
57
- }
58
-
59
- export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null): ResolvedDcgchatAccount {
60
- const id = accountId ?? DEFAULT_ACCOUNT_ID
61
- const raw = (cfg.channels?.[CHANNEL_ID] as DcgchatConfig | undefined) ?? {}
62
- return {
63
- accountId: id,
64
- enabled: raw.enabled !== false,
65
- configured: Boolean(raw.wsUrl),
66
- wsUrl: raw.wsUrl ?? '',
67
- botToken: raw.botToken ?? '',
68
- userId: raw.userId ?? '',
69
- domainId: raw.domainId ?? '',
70
- appId: raw.appId ?? ''
71
- }
72
- }
73
-
74
- export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
75
- id: CHANNEL_ID,
76
- meta: {
77
- id: CHANNEL_ID,
78
- label: '书灵墨宝',
79
- selectionLabel: '书灵墨宝',
80
- docsPath: '/channels/' + CHANNEL_ID,
81
- docsLabel: CHANNEL_ID,
82
- blurb: '连接 OpenClaw 与 书灵墨宝 产品',
83
- order: 80
84
- },
85
- capabilities: {
86
- chatTypes: ['direct'],
87
- polls: false,
88
- threads: true,
89
- media: true,
90
- nativeCommands: true,
91
- reactions: true,
92
- edit: false,
93
- reply: true,
94
- effects: true
95
- // blockStreaming: true,
96
- },
97
- /** 当前构建的 channel id + 兼容旧配置键 `channels.dcgchat` */
98
- reload: { configPrefixes: [`channels.${CHANNEL_ID}`, 'channels.dcgchat'] },
99
- configSchema: {
100
- schema: {
101
- type: 'object',
102
- additionalProperties: false,
103
- properties: {
104
- enabled: { type: 'boolean' },
105
- wsUrl: { type: 'string' },
106
- botToken: { type: 'string' },
107
- userId: { type: 'string', description: 'WebSocket 连接参数 _userId,与 dcgchat_message 工具的 target(dcgSessionKey)无关' },
108
- appId: { type: 'string' },
109
- domainId: { type: 'string' },
110
- capabilities: { type: 'array', items: { type: 'string' } },
111
- strictMessageToolTarget: {
112
- type: 'boolean',
113
- description:
114
- '默认 true:内置 message 工具的 target 须为 sessionKey 形态(如 agent:…:mobook:direct:… 或 agent: 前缀多段),禁止纯数字 WS userId。设为 false 关闭此校验。'
115
- }
116
- }
117
- },
118
- uiHints: {
119
- userId: {
120
- label: 'WS 连接 _userId',
121
- help: '仅用于拼接网关 WebSocket URL 的查询参数,不是 Agent 发消息时的 target。发消息请使用 dcgSessionKey(与入站上下文 SessionKey 相同)。'
122
- },
123
- strictMessageToolTarget: {
124
- label: '严格 message.target',
125
- help: '开启后由通道目标解析层拒绝纯数字等非 sessionKey 的 target(推荐开启);关闭则与旧版行为一致。'
126
- }
127
- }
128
- },
129
- config: {
130
- listAccountIds: () => [DEFAULT_ACCOUNT_ID],
131
- resolveAccount: (cfg, accountId) => resolveAccount(cfg, accountId),
132
- defaultAccountId: () => DEFAULT_ACCOUNT_ID,
133
- setAccountEnabled: ({ cfg, enabled }) => {
134
- const channelKey = CHANNEL_ID
135
- const prev = (cfg.channels?.[channelKey as keyof NonNullable<typeof cfg.channels>] as Record<string, unknown> | undefined) ?? {}
136
- return {
137
- ...cfg,
138
- channels: {
139
- ...cfg.channels,
140
- [channelKey]: { ...prev, enabled }
141
- }
142
- }
143
- },
144
- isConfigured: (account) => account.configured,
145
- describeAccount: (account) => ({
146
- accountId: account.accountId,
147
- enabled: account.enabled,
148
- configured: account.configured,
149
- wsUrl: account.wsUrl,
150
- botToken: account.botToken ? '***' : '',
151
- userId: account.userId,
152
- domainId: account.domainId,
153
- appId: account.appId
154
- })
155
- },
156
- status: {
157
- defaultRuntime: {
158
- accountId: DEFAULT_ACCOUNT_ID,
159
- running: false,
160
- connected: false,
161
- lastConnectedAt: null,
162
- lastDisconnect: null,
163
- lastError: null
164
- },
165
- buildAccountSnapshot: ({ account, runtime }) => ({
166
- accountId: account.accountId,
167
- name: account.accountId,
168
- enabled: account.enabled,
169
- configured: account.configured,
170
- running: runtime?.running ?? false,
171
- connected: runtime?.connected ?? false,
172
- lastStartAt: runtime?.lastStartAt ?? null,
173
- lastStopAt: runtime?.lastStopAt ?? null,
174
- lastError: runtime?.lastError ?? null,
175
- reconnectAttempts: runtime?.reconnectAttempts ?? 0,
176
- lastConnectedAt: runtime?.lastConnectedAt ?? null,
177
- lastDisconnect: runtime?.lastDisconnect ?? null,
178
- lastInboundAt: runtime?.lastInboundAt ?? null,
179
- lastOutboundAt: runtime?.lastOutboundAt ?? null,
180
- healthState: runtime?.healthState ?? undefined
181
- }),
182
- buildChannelSummary: ({ snapshot }) => ({
183
- configured: snapshot.configured ?? false,
184
- running: snapshot.running ?? false,
185
- connected: snapshot.connected ?? false,
186
- lastConnectedAt: snapshot.lastConnectedAt ?? null,
187
- lastDisconnect: snapshot.lastDisconnect ?? null,
188
- lastError: snapshot.lastError ?? null
189
- }),
190
- resolveAccountState: ({ enabled, configured }) => {
191
- if (!enabled) return 'disabled'
192
- return configured ? 'linked' : 'not linked'
193
- }
194
- },
195
- messaging: {
196
- normalizeTarget: (raw) => raw || undefined,
197
- targetResolver: {
198
- looksLikeId: dcgchatMessageTargetLooksLikeId,
199
- hint: '须为完整 dcgSessionKey(与 SessionKey 一致,形如 agent:…:mobook:direct:… 或 agent: 前缀路由键);禁止填 WS userId 等纯数字。可在通道配置 strictMessageToolTarget=false 关闭校验。'
200
- }
201
- },
202
- /**
203
- * 与 Telegram 等通道一致:用入站路由键 `To`(即 SessionKey / OriginatingTo)作为 message 工具默认 `currentChannelId`。
204
- * 显式 target 由 `messaging.targetResolver.looksLikeId` + OpenClaw `resolveMessagingTarget` 校验(见 strictMessageToolTarget)。
205
- */
206
- threading: {
207
- buildToolContext: ({ context, hasRepliedRef }) => ({
208
- currentChannelId: context.To?.trim() || undefined,
209
- hasRepliedRef
210
- })
211
- },
212
- agentPrompt: {
213
- messageToolHints: () => {
214
- const isApp110 = Number(dcgchatChannelCfg().appId || 100) === 110
215
- const filePathHint = isApp110
216
- ? '生成文件后,**需要**把文件路径、地址直接告诉用户;仍须通过工具发文件完成交付。'
217
- : '生成文件后,**不要**把文件路径、地址直接告诉用户;把文件名告诉用户;须通过工具发文件,勿在正文里直接输出可访问路径。'
218
- return [
219
- '书灵墨宝 / 内置 `message`:回复当前会话时 **不要传 `target`**,由 OpenClaw 使用工具上下文里的 `currentChannelId`(来自入站 `To`,即当前 SessionKey)。',
220
- '必须指定会话时:`target` 须为上下文中出现的 **整段 SessionKey,逐字一致**(如 `agent:…:mobook:direct:…` 或以 `agent:` 开头的路由键);**禁止**使用 `userId`、`From`、纯数字会话号等代替。',
221
- filePathHint,
222
- '用户待发送文件若**不在**兼容挂载 **`/workspace/`**、**`/mobook/`**(及 Windows 下 `workspace`、`mobook` 盘符路径),且也不在 **当前 Agent 工作区** 或通道 **`allowedPaths`** 所列目录内:**建议**用户「先复制进工作区再发」作为首选。',
223
- '使用 `dcgchat_message` 时同样遵守上述 SessionKey 规则(该工具通常由插件注入当前会话,一般无需自造 target)。',
224
- '**正文出站二选一(禁止双写)**:(A) 普通回复——把要对用户说的正文写在**助手消息**里,由通道流式下发;此模式下 `dcgchat_message` **仅用于附件**(`media`),`content` 留空或只写极短说明(如文件名),**不要**把同一段长正文再塞进工具。(B) 工具投递——必须把完整正文放进 `dcgchat_message` 的 `content` 时,助手收尾须输出 **`NO_REPLY`**,**禁止**在助手消息里重复相同或摘要版正文。',
225
- '优先 (A):除非报告/清单极长或须与附件强绑定一次性交付,否则不要走 (B)。同一轮内**禁止**既流式输出助手正文、又用 `dcgchat_message.content` 发送相同或高度重叠的文本。',
226
- '**发送附件不需要、也不依赖 inline buttons**(inline buttons 仅用于 Telegram 等通道的交互回调按钮)。发文件请用 `dcgchat_message` 的 `media: [{ file: \"绝对路径\" }]`,或内置 `message` 的 `filePath`/`media`;**禁止**以「未开启 inline buttons」为由拒绝发文件或让用户去开该配置。',
227
- '所有对话、思考、输出:**必须使用中文**;不允许直接输出英文原文;用户输入英文,你必须翻译成中文回答。'
228
- ]
229
- }
230
- },
231
- outbound: {
232
- deliveryMode: 'direct',
233
- resolveTarget: ({ to }) => {
234
- if (!to) {
235
- return { ok: false, error: new Error('target is empty') }
236
- }
237
- return { ok: true, to: to }
238
- },
239
- chunker: (text, limit) => getDcgchatRuntime().channel.text.chunkMarkdownText(text, limit),
240
- textChunkLimit: 4000,
241
- sendText: async (ctx) => {
242
- let messageId = ''
243
- const sessionKey = normalizeSessionTarget(ctx.to)
244
- dcgLogger(`dcgchat: channel sendText to=${ctx.text} sessionKey=${sessionKey}`)
245
- const isCron = ctx.to.indexOf('dcg-cron:') >= 0
246
- const content: Record<string, unknown> = { response: ctx.text }
247
- // 入站 handler 已将本轮标为 running;若已 sendFinal(end) 则应为 finished。
248
- // 否则网关/Core 晚到的正文会仍用「当前 params map」里的 messageId,错挂到后一条用户消息上。
249
- if (isSessionActiveForTool(sessionKey)) {
250
- const outboundCtx = getOutboundMsgParams(sessionKey)
251
- messageId = outboundCtx?.messageId || `${Date.now()}`
252
- wsSendRaw({ ...outboundCtx, messageId }, content)
253
- } else if (isCron) {
254
- const { sessionId, agentId } = takeApartSessionKey(sessionKey)
255
- messageId = getCronMessageId(sessionKey) || `${Date.now()}`
256
- const baseCtx = mergeDefaultParams({
257
- sessionId: `${sessionId}`,
258
- agentId: agentId,
259
- messageId: messageId
260
- })
261
- content.is_finish = -1
262
- wsSendRaw(baseCtx, content)
263
- } else {
264
- dcgLogger(`dcgchat: channel sendText dropped (session not active): to=${sessionKey}`)
265
- }
266
- return {
267
- channel: CHANNEL_ID,
268
- messageId: `${messageId}`,
269
- chatId: outboundChatId(ctx.to, sessionKey)
270
- }
271
- },
272
- sendMedia: async (ctx: ChannelOutboundContext) => {
273
- const meta = await dispatchChannelOutboundSendMedia(ctx)
274
- return {
275
- channel: CHANNEL_ID,
276
- messageId: meta.messageId,
277
- chatId: meta.chatId
278
- }
279
- }
280
- },
281
- gateway: {
282
- startAccount: async (ctx) => {
283
- const { monitorDcgchatProvider } = await import('../monitor.js')
284
- const account = resolveAccount(ctx.cfg, ctx.accountId)
285
- setLogger(ctx.runtime)
286
- if (!account.wsUrl) {
287
- dcgLogger(`dcgchat[${account.accountId}]: wsUrl not configured, skipping`, 'error')
288
- return
289
- }
290
- startDcgchatGatewaySocket()
291
- const statusSink = (
292
- patch: Partial<{
293
- connected: boolean
294
- running: boolean
295
- lastConnectedAt: number | null
296
- lastDisconnect:
297
- | string
298
- | {
299
- at: number
300
- status?: number
301
- error?: string
302
- loggedOut?: boolean
303
- }
304
- | null
305
- lastError: string | null
306
- lastStartAt: number | null
307
- lastStopAt: number | null
308
- lastInboundAt: number | null
309
- lastOutboundAt: number | null
310
- reconnectAttempts: number
311
- healthState: string
312
- }>
313
- ) => {
314
- ctx.setStatus({
315
- ...ctx.getStatus(),
316
- accountId: ctx.accountId,
317
- ...patch
318
- })
319
- }
320
- return monitorDcgchatProvider({
321
- config: ctx.cfg,
322
- runtime: ctx.runtime,
323
- abortSignal: ctx.abortSignal,
324
- accountId: ctx.accountId,
325
- statusSink
326
- })
327
- }
328
- }
329
- }
@@ -1,144 +0,0 @@
1
- import { getCronMessageId, takeApartSessionKey, setCronMessageId } from '../utils/global.js'
2
- import { dcgLogger } from '../utils/log.js'
3
- import { getOutboundMsgParams, getParamsMessage } from '../utils/params.js'
4
- import { isWsOpen, wsSendRaw } from '../transport.js'
5
- import { normalizeSessionTarget, outboundChatId } from './outboundTarget.js'
6
- import { uploadMediaUrls } from './uploadMediaUrl.js'
7
-
8
- /**
9
- * 仅从 JSON / `{ file | path | url }` 等结构里取出路径字符串,不做改写(不拼 workspace、不 normalize)。
10
- */
11
- function collectOutboundMediaPaths(item: unknown, out: string[]): void {
12
- if (item == null) return
13
- if (typeof item === 'string') {
14
- const t = item.trim()
15
- if (!t) return
16
- if (t.startsWith('[') && t.endsWith(']')) {
17
- try {
18
- const parsed = JSON.parse(t) as unknown
19
- collectOutboundMediaPaths(parsed, out)
20
- return
21
- } catch {
22
- /* 非 JSON,按普通路径处理 */
23
- }
24
- }
25
- out.push(t)
26
- return
27
- }
28
- if (Array.isArray(item)) {
29
- for (const el of item) collectOutboundMediaPaths(el, out)
30
- return
31
- }
32
- if (typeof item === 'object') {
33
- const o = item as Record<string, unknown>
34
- const raw = o.file ?? o.path ?? o.url
35
- if (typeof raw === 'string' && raw.trim()) {
36
- out.push(raw.trim())
37
- }
38
- }
39
- }
40
-
41
- /** 将出站 media 展平为路径字符串列表(去重保序;路径保持 Core 原样) */
42
- export function normalizeOutboundMediaPaths(raw: unknown): string[] {
43
- const acc: string[] = []
44
- collectOutboundMediaPaths(raw, acc)
45
- return [...new Set(acc.filter(Boolean))]
46
- }
47
-
48
- export type DcgchatMediaSendOptions = {
49
- /** 与 setParamsMessage / map 一致,用于 getOutboundMsgParams */
50
- sessionKey: string
51
- mediaUrls?: string[]
52
- text?: string
53
- /** 定时任务等场景须与 `getCronMessageId` 一致,避免沿用 map 里上一条用户消息的 messageId */
54
- messageId?: string
55
- }
56
-
57
- const SHULING_MSG_ID_LENGTH = 13
58
-
59
- export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions, tags?: Record<string, string | boolean>): Promise<void> {
60
- const sessionKey = opts.sessionKey?.trim()
61
- /** 定时自动执行未走 onRunCronJob,须与 finishedDcgchatCron 共用同一 messageId,否则附件与气泡错位 */
62
- if (!opts.messageId?.trim() && !getCronMessageId(sessionKey)) {
63
- setCronMessageId(sessionKey, `${Date.now()}`)
64
- }
65
-
66
- const messageId = (opts.messageId || getCronMessageId(sessionKey))?.trim()
67
- const baseCtx = getOutboundMsgParams(sessionKey)
68
- const msgCtx = { ...baseCtx, messageId }
69
- if (!isWsOpen()) {
70
- dcgLogger(`dcgchat: outbound media skipped -> ws not isWsOpen failed open: ${opts.mediaUrls ?? ''}`)
71
- return
72
- }
73
-
74
- const { sessionId, agentId } = takeApartSessionKey(sessionKey)
75
- if (!msgCtx.sessionId) {
76
- msgCtx.sessionId = sessionId
77
- }
78
- if (!msgCtx.agentId) {
79
- msgCtx.agentId = agentId
80
- }
81
-
82
- const expanded = normalizeOutboundMediaPaths(opts.mediaUrls)
83
- if (expanded.length === 0) {
84
- dcgLogger(`dcgchat: sendMedia skipped (no resolvable path): ${JSON.stringify(opts.mediaUrls)} sessionKey=${sessionKey}`, 'error')
85
- return
86
- }
87
- try {
88
- const urls = await uploadMediaUrls(expanded, msgCtx, sessionKey)
89
- const notMessageId = !msgCtx?.messageId || String(msgCtx?.messageId).length === SHULING_MSG_ID_LENGTH
90
- wsSendRaw(msgCtx, {
91
- response: opts.text ?? '',
92
- message_tags: notMessageId ? { mark: 'empty_text_file', ...tags } : tags,
93
- is_finish: notMessageId ? -1 : 0,
94
- files: urls.map((url) => ({ url: url.url ?? '', name: url.fileName }))
95
- })
96
- dcgLogger(`dcgchat: sendMedia session=${sessionKey}, file=${urls.map((url) => url.fileName).join(', ')}`)
97
- } catch (error) {
98
- dcgLogger(`dcgchat: error sendMedia session=${sessionKey}: ${String(error)}`, 'error')
99
- }
100
- }
101
-
102
- export type ChannelOutboundSendMediaCtx = {
103
- to: string
104
- mediaUrls?: unknown
105
- mediaUrl?: string
106
- text?: string
107
- }
108
-
109
- /**
110
- * Plugin `outbound.sendMedia`:解析 cron / 会话、归一化路径并逐条投递。
111
- */
112
- interface ChannelOutboundSendMediaResult {
113
- messageId: string
114
- chatId: string
115
- }
116
- export async function dispatchChannelOutboundSendMedia(ctx: ChannelOutboundSendMediaCtx): Promise<ChannelOutboundSendMediaResult> {
117
- const sessionKey = normalizeSessionTarget(ctx.to)
118
- const isCron = ctx.to.indexOf('dcg-cron:') >= 0
119
- const outboundCtx = getOutboundMsgParams(sessionKey)
120
- const msgCtx = getParamsMessage(sessionKey) ?? outboundCtx
121
- if (isCron && !getCronMessageId(sessionKey)) {
122
- setCronMessageId(sessionKey, `${Date.now()}`)
123
- }
124
- const cronMsgId = getCronMessageId(sessionKey)
125
- const messageId = (isCron ? cronMsgId : msgCtx?.messageId) || `${Date.now()}`
126
- const { sessionId } = takeApartSessionKey(sessionKey)
127
- if (!sessionId) {
128
- dcgLogger(`dcgchat: channel sendMedia to ${ctx.to} -> sessionId not found`, 'error')
129
- return {
130
- messageId,
131
- chatId: outboundChatId(ctx.to, sessionKey || '')
132
- }
133
- }
134
-
135
- dcgLogger(`dcgchat: channel sendMedia to ${ctx.to}`)
136
-
137
- const rawMedia = ctx.mediaUrls ?? ctx.mediaUrl
138
- const mediaUrls = normalizeOutboundMediaPaths(rawMedia)
139
- await sendDcgchatMedia({ sessionKey, mediaUrls, messageId, text: ctx.text })
140
- return {
141
- messageId,
142
- chatId: outboundChatId(ctx.to, sessionKey || '')
143
- }
144
- }
@@ -1,62 +0,0 @@
1
- import { getParamsMessage } from '../utils/params.js'
2
-
3
- export function normalizeSessionTarget(rawTo: string): string {
4
- const cleaned = rawTo.replace('dcg-cron:', '').trim()
5
- if (!cleaned) return ''
6
- return getParamsMessage(cleaned)?.sessionKey?.trim() || cleaned
7
- }
8
-
9
- const cronSessionKeyMap = new Set<string>()
10
- const cronSessionKeysByJobId = new Map<string, Set<string>>()
11
-
12
- function normalizeCronSessionKey(value: unknown): string {
13
- if (typeof value !== 'string') return ''
14
- return value.replace('dcg-cron:', '').trim()
15
- }
16
-
17
- export function setCronSessionKeyMap(sessionKey: string) {
18
- const key = normalizeCronSessionKey(sessionKey)
19
- if (key) cronSessionKeyMap.add(key)
20
- }
21
-
22
- export function setCronSessionKeyMapsForJob(jobId: unknown, sessionKeys: unknown[]): string[] {
23
- const keys = [...new Set(sessionKeys.map(normalizeCronSessionKey).filter(Boolean))]
24
- for (const key of keys) {
25
- cronSessionKeyMap.add(key)
26
- }
27
- const id = typeof jobId === 'string' ? jobId.trim() : ''
28
- if (id && keys.length > 0) {
29
- const bucket = cronSessionKeysByJobId.get(id) ?? new Set<string>()
30
- for (const key of keys) bucket.add(key)
31
- cronSessionKeysByJobId.set(id, bucket)
32
- }
33
- return keys
34
- }
35
- export function hasCronSessionKeyMap(sessionKey: string): boolean {
36
- const key = normalizeCronSessionKey(sessionKey)
37
- return !!key && cronSessionKeyMap.has(key)
38
- }
39
- export function deleteCronSessionKeyMap(sessionKey: string) {
40
- const key = normalizeCronSessionKey(sessionKey)
41
- if (!key) return
42
- cronSessionKeyMap.delete(key)
43
- for (const [jobId, keys] of cronSessionKeysByJobId.entries()) {
44
- keys.delete(key)
45
- if (keys.size === 0) cronSessionKeysByJobId.delete(jobId)
46
- }
47
- }
48
-
49
- export function deleteCronSessionKeyMapsForJob(jobId: unknown): string[] {
50
- const id = typeof jobId === 'string' ? jobId.trim() : ''
51
- if (!id) return []
52
- const keys = [...(cronSessionKeysByJobId.get(id) ?? [])]
53
- for (const key of keys) cronSessionKeyMap.delete(key)
54
- cronSessionKeysByJobId.delete(id)
55
- return keys
56
- }
57
-
58
- /** 出站返回的 chatId:含 `dcg-cron:` 时保留原始 `to`,便于下游识别定时投递 */
59
- export function outboundChatId(rawTo: string | undefined, normalizedTo: string): string {
60
- const raw = rawTo?.trim() ?? ''
61
- return raw.indexOf('dcg-cron:') >= 0 ? raw : normalizedTo
62
- }
@@ -1,76 +0,0 @@
1
- import fs from 'node:fs'
2
- import { basename } from 'node:path'
3
- import { ossUpload } from '../request/oss.js'
4
- import { CHANNEL_ID } from '../utils/constant.js'
5
- import { addSentMediaKey, getOpenClawConfig, hasSentMediaKey, resolveOutboundMediaDedupeKey } from '../utils/global.js'
6
- import { dcgLogger } from '../utils/log.js'
7
- import { IMsgParams } from '../types.js'
8
-
9
- export type UploadedMediaUrl = {
10
- url: string
11
- fileName: string
12
- }
13
-
14
- /**
15
- * 上传本地文件到 OSS,返回上传成功的文件访问地址与原始文件名。
16
- */
17
- export async function uploadMediaUrls(
18
- mediaUrls: string[] | undefined,
19
- msgCtx: IMsgParams,
20
- sessionKey: string
21
- ): Promise<UploadedMediaUrl[]> {
22
- if (!mediaUrls || mediaUrls.length === 0) {
23
- return []
24
- }
25
- const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.[CHANNEL_ID]?.botToken ?? ''
26
- const urls: UploadedMediaUrl[] = []
27
-
28
- for (const mediaUrl of mediaUrls) {
29
- const filePath = mediaUrl?.trim()
30
- if (!filePath) {
31
- dcgLogger('dcgchat: uploadMediaUrls skipped (empty path)', 'error')
32
- continue
33
- }
34
-
35
- let mediaStat: fs.Stats
36
- try {
37
- mediaStat = fs.statSync(filePath)
38
- } catch (err) {
39
- dcgLogger(`dcgchat: uploadMediaUrls skipped (cannot stat path): ${filePath} ${String(err)}`, 'error')
40
- continue
41
- }
42
-
43
- if (!mediaStat.isFile()) {
44
- const kind = mediaStat.isDirectory() ? 'directory' : 'non-regular'
45
- dcgLogger(`dcgchat: uploadMediaUrls skipped (not a regular file, ${kind}): ${filePath}`, 'error')
46
- continue
47
- }
48
- if (mediaUrl && msgCtx.sessionId) {
49
- const mediaDedupeBucket = resolveOutboundMediaDedupeKey(msgCtx.messageId, msgCtx.sessionId, sessionKey)
50
- if (!mediaDedupeBucket) {
51
- dcgLogger(`dcgchat: sendMedia skipped (no media dedupe bucket): ${mediaUrl} sessionKey=${sessionKey}`, 'error')
52
- continue
53
- }
54
- if (hasSentMediaKey(mediaDedupeBucket, mediaUrl)) {
55
- dcgLogger(
56
- `dcgchat: sendMedia skipped (hasSentMediaKey): ${mediaUrl} dedupeBucket=${mediaDedupeBucket} sessionKey=${sessionKey}`
57
- )
58
- continue
59
- }
60
- addSentMediaKey(mediaDedupeBucket, mediaUrl)
61
- }
62
-
63
- try {
64
- const url = await ossUpload(filePath, botToken, 1)
65
- if (!url) {
66
- dcgLogger(`dcgchat: uploadMediaUrls failed (empty OSS url): ${filePath}`, 'error')
67
- continue
68
- }
69
- urls.push({ url, fileName: basename(filePath) })
70
- } catch (err) {
71
- dcgLogger(`dcgchat: uploadMediaUrls failed: ${filePath} ${String(err)}`, 'error')
72
- }
73
- }
74
-
75
- return urls
76
- }