@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.
- package/index.js +306 -0
- package/openclaw.plugin.json +42 -12
- package/package.json +8 -35
- package/README.md +0 -167
- package/index.ts +0 -35
- package/src/agent.ts +0 -568
- package/src/bot.ts +0 -646
- package/src/channel/index.ts +0 -329
- package/src/channel/outboundMedia.ts +0 -144
- package/src/channel/outboundTarget.ts +0 -62
- package/src/channel/uploadMediaUrl.ts +0 -76
- package/src/cron/message.ts +0 -114
- package/src/cron/params.ts +0 -164
- package/src/cron/toolGuard.ts +0 -466
- package/src/cron/types.ts +0 -15
- package/src/gateway/index.ts +0 -430
- package/src/gateway/security.ts +0 -95
- package/src/gateway/socket.ts +0 -256
- 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 -269
- package/src/request/api.ts +0 -80
- package/src/request/oss.ts +0 -200
- package/src/request/request.ts +0 -191
- package/src/request/userInfo.ts +0 -44
- package/src/session.ts +0 -28
- package/src/skill.ts +0 -114
- package/src/tool.ts +0 -603
- package/src/tools/messageTool.ts +0 -334
- package/src/tools/toolCallGuard.ts +0 -327
- package/src/transport.ts +0 -217
- package/src/types.ts +0 -139
- package/src/utils/agentErrors.ts +0 -116
- package/src/utils/constant.ts +0 -60
- package/src/utils/env-config.ts +0 -19
- package/src/utils/formatLlmInputEvent.ts +0 -48
- package/src/utils/gatewayMsgHandler.ts +0 -147
- package/src/utils/global.ts +0 -309
- package/src/utils/inboundTurnState.ts +0 -66
- package/src/utils/log.ts +0 -77
- package/src/utils/mediaAttached.ts +0 -244
- package/src/utils/mediaEmitter.ts +0 -54
- package/src/utils/outboundAssistantText.ts +0 -117
- package/src/utils/params.ts +0 -104
- package/src/utils/passTxt.ts +0 -4
- package/src/utils/resolveRegisterConfig.ts +0 -33
- package/src/utils/searchFile.ts +0 -228
- package/src/utils/sessionState.ts +0 -137
- package/src/utils/sessionTermination.ts +0 -228
- package/src/utils/streamMerge.ts +0 -150
- package/src/utils/subagentRunMap.ts +0 -71
- package/src/utils/undiciFetchInterceptor.ts +0 -346
- package/src/utils/workspaceFilePaths.ts +0 -18
- package/src/utils/wsMessageHandler.ts +0 -124
- package/src/utils/zipExtract.ts +0 -97
- package/src/utils/zipPath.ts +0 -24
package/src/channel/index.ts
DELETED
|
@@ -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
|
-
}
|