@dcrays/dcgchat 0.2.19 → 0.2.32
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.ts +20 -17
- package/openclaw.plugin.json +4 -2
- package/package.json +3 -6
- package/src/bot.ts +280 -514
- package/src/channel.ts +111 -179
- package/src/monitor.ts +130 -130
- package/src/{api.ts → request/api.ts} +34 -35
- package/src/request/oss.ts +58 -0
- package/src/request/request.ts +198 -0
- package/src/{userInfo.ts → request/userInfo.ts} +36 -34
- package/src/skill.ts +110 -194
- package/src/tool.ts +116 -82
- package/src/transport.ts +108 -0
- package/src/types.ts +75 -64
- package/src/utils/constant.ts +7 -0
- package/src/utils/global.ts +117 -0
- package/src/utils/log.ts +15 -0
- package/src/utils/searchFile.ts +212 -0
- package/README.md +0 -83
- package/src/connection.ts +0 -11
- package/src/log.ts +0 -46
- package/src/oss.ts +0 -72
- package/src/request.ts +0 -201
- package/src/runtime.ts +0 -40
package/src/bot.ts
CHANGED
|
@@ -1,87 +1,115 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
import type {
|
|
4
|
-
import { createReplyPrefixContext } from
|
|
5
|
-
import type { InboundMessage
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import type { ReplyPayload } from 'openclaw/plugin-sdk'
|
|
4
|
+
import { createReplyPrefixContext } from 'openclaw/plugin-sdk'
|
|
5
|
+
import type { InboundMessage } from './types.js'
|
|
6
|
+
import {
|
|
7
|
+
clearSentMediaKeys,
|
|
8
|
+
getDcgchatRuntime,
|
|
9
|
+
getOpenClawConfig,
|
|
10
|
+
getWorkspaceDir,
|
|
11
|
+
getWsConnection,
|
|
12
|
+
setMsgStatus
|
|
13
|
+
} from './utils/global.js'
|
|
14
|
+
import { resolveAccount, sendDcgchatMedia } from './channel.js'
|
|
15
|
+
import { generateSignUrl } from './request/api.js'
|
|
16
|
+
import { extractMobookFiles } from './utils/searchFile.js'
|
|
17
|
+
import { createMsgContext, sendChunk, sendFinal, sendText as sendTextMsg, sendError, sendText } from './transport.js'
|
|
18
|
+
import { dcgLogger } from './utils/log.js'
|
|
19
|
+
import { channelInfo, systemCommand, interruptCommand, ENV } from './utils/constant.js'
|
|
10
20
|
|
|
11
21
|
type MediaInfo = {
|
|
12
|
-
path: string
|
|
13
|
-
fileName: string
|
|
14
|
-
contentType: string
|
|
15
|
-
placeholder: string
|
|
16
|
-
}
|
|
22
|
+
path: string
|
|
23
|
+
fileName: string
|
|
24
|
+
contentType: string
|
|
25
|
+
placeholder: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type TFileInfo = { name: string; url: string }
|
|
29
|
+
|
|
30
|
+
const mediaMaxBytes = 300 * 1024 * 1024
|
|
31
|
+
|
|
32
|
+
/** Active LLM generation abort controllers, keyed by conversationId */
|
|
33
|
+
const activeGenerations = new Map<string, AbortController>()
|
|
17
34
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
35
|
+
/** Abort an in-progress LLM generation for a given conversationId */
|
|
36
|
+
export function abortMobookappGeneration(conversationId: string): void {
|
|
37
|
+
const ctrl = activeGenerations.get(conversationId)
|
|
38
|
+
if (ctrl) {
|
|
39
|
+
ctrl.abort()
|
|
40
|
+
activeGenerations.delete(conversationId)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extract agentId from conversation_id formatted as "agentId::suffix".
|
|
46
|
+
* Returns null if the conversation_id does not contain the "::" separator.
|
|
47
|
+
*/
|
|
48
|
+
export function extractAgentIdFromConversationId(conversationId: string): string | null {
|
|
49
|
+
const idx = conversationId.indexOf('::')
|
|
50
|
+
if (idx <= 0) return null
|
|
51
|
+
return conversationId.slice(0, idx)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function resolveMediaFromUrls(files: TFileInfo[], botToken: string): Promise<MediaInfo[]> {
|
|
55
|
+
const core = getDcgchatRuntime()
|
|
56
|
+
const out: MediaInfo[] = []
|
|
57
|
+
dcgLogger(`media: user upload files: ${JSON.stringify(files)}`)
|
|
23
58
|
|
|
24
59
|
for (let i = 0; i < files.length; i++) {
|
|
25
|
-
const file = files[i]
|
|
60
|
+
const file = files[i]
|
|
26
61
|
try {
|
|
27
62
|
let data = ''
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const response = await fetch(data)
|
|
63
|
+
if (/^https?:\/\//i.test(file.url)) {
|
|
64
|
+
data = file.url
|
|
65
|
+
} else {
|
|
66
|
+
data = await generateSignUrl(file.url, botToken)
|
|
67
|
+
}
|
|
68
|
+
dcgLogger(`media: generateSignUrl: ${data}`)
|
|
69
|
+
const response = await fetch(data)
|
|
35
70
|
if (!response.ok) {
|
|
36
|
-
|
|
37
|
-
continue
|
|
71
|
+
dcgLogger?.(`media: ${file.url} fetch failed with HTTP ${response.status}`, 'error')
|
|
72
|
+
continue
|
|
38
73
|
}
|
|
39
|
-
const buffer = Buffer.from(await response.arrayBuffer())
|
|
74
|
+
const buffer = Buffer.from(await response.arrayBuffer())
|
|
40
75
|
|
|
41
|
-
let contentType = response.headers.get(
|
|
76
|
+
let contentType = response.headers.get('content-type') || ''
|
|
42
77
|
if (!contentType) {
|
|
43
|
-
contentType = await core.media.detectMime({ buffer }) ||
|
|
78
|
+
contentType = (await core.media.detectMime({ buffer })) || ''
|
|
44
79
|
}
|
|
45
|
-
const fileName = file.name || path.basename(new URL(file.url).pathname) ||
|
|
46
|
-
const saved = await core.channel.media.saveMediaBuffer(
|
|
47
|
-
|
|
48
|
-
contentType,
|
|
49
|
-
"inbound",
|
|
50
|
-
mediaMaxBytes,
|
|
51
|
-
fileName,
|
|
52
|
-
);
|
|
53
|
-
const isImage = contentType.startsWith("image/");
|
|
80
|
+
const fileName = file.name || path.basename(new URL(file.url).pathname) || 'file'
|
|
81
|
+
const saved = await core.channel.media.saveMediaBuffer(buffer, contentType, 'inbound', mediaMaxBytes, fileName)
|
|
82
|
+
const isImage = contentType.startsWith('image/')
|
|
54
83
|
out.push({
|
|
55
84
|
path: saved.path,
|
|
56
85
|
fileName,
|
|
57
|
-
contentType: saved.contentType ||
|
|
58
|
-
placeholder: isImage ?
|
|
59
|
-
})
|
|
60
|
-
|
|
86
|
+
contentType: saved.contentType || '',
|
|
87
|
+
placeholder: isImage ? '<media:image>' : '<media:file>'
|
|
88
|
+
})
|
|
61
89
|
} catch (err) {
|
|
62
|
-
|
|
90
|
+
dcgLogger(`media: ${file.url} FAILED to process: ${String(err)}`, 'error')
|
|
63
91
|
}
|
|
64
92
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return out;
|
|
93
|
+
dcgLogger(`media: resolve complete, ${out.length}/${files.length} file(s) succeeded`)
|
|
94
|
+
return out
|
|
68
95
|
}
|
|
69
96
|
|
|
70
|
-
|
|
71
|
-
MediaPath?: string
|
|
72
|
-
MediaFileName?: string
|
|
73
|
-
MediaType?: string
|
|
74
|
-
MediaUrl?: string
|
|
75
|
-
MediaFileNames?: string[]
|
|
76
|
-
MediaPaths?: string[]
|
|
77
|
-
MediaUrls?: string[]
|
|
78
|
-
MediaTypes?: string[]
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const
|
|
97
|
+
type MediaPayload = {
|
|
98
|
+
MediaPath?: string
|
|
99
|
+
MediaFileName?: string
|
|
100
|
+
MediaType?: string
|
|
101
|
+
MediaUrl?: string
|
|
102
|
+
MediaFileNames?: string[]
|
|
103
|
+
MediaPaths?: string[]
|
|
104
|
+
MediaUrls?: string[]
|
|
105
|
+
MediaTypes?: string[]
|
|
106
|
+
}
|
|
107
|
+
function buildMediaPayload(mediaList: MediaInfo[]): MediaPayload {
|
|
108
|
+
if (mediaList.length === 0) return {}
|
|
109
|
+
const first = mediaList[0]
|
|
110
|
+
const mediaFileNames = mediaList.map((m) => m.fileName).filter(Boolean)
|
|
111
|
+
const mediaPaths = mediaList.map((m) => m.path)
|
|
112
|
+
const mediaTypes = mediaList.map((m) => m.contentType).filter(Boolean)
|
|
85
113
|
return {
|
|
86
114
|
MediaPath: first?.path,
|
|
87
115
|
MediaFileName: first?.fileName,
|
|
@@ -90,509 +118,247 @@ function buildMediaPayload(mediaList: MediaInfo[]): {
|
|
|
90
118
|
MediaFileNames: mediaFileNames.length > 0 ? mediaFileNames : undefined,
|
|
91
119
|
MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
92
120
|
MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
93
|
-
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function resolveReplyMediaList(payload: ReplyPayload): string[] {
|
|
98
|
-
if (payload.mediaUrls?.length) return payload.mediaUrls.filter(Boolean);
|
|
99
|
-
return payload.mediaUrl ? [payload.mediaUrl] : [];
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function createFileExtractor() {
|
|
103
|
-
const globalSet = new Set()
|
|
104
|
-
|
|
105
|
-
function getNewFiles(text: string) {
|
|
106
|
-
if (!text) return []
|
|
107
|
-
|
|
108
|
-
const currentSet = new Set()
|
|
109
|
-
const lines = text.split(/\n+/)
|
|
110
|
-
|
|
111
|
-
for (const line of lines) {
|
|
112
|
-
const cleanLine = line.trim()
|
|
113
|
-
if (!cleanLine) continue
|
|
114
|
-
|
|
115
|
-
const matches = cleanLine.matchAll(/`([^`]+)`/g)
|
|
116
|
-
for (const m of matches) {
|
|
117
|
-
handlePath(m[1])
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const rawMatches = cleanLine.match(/\/[^\s))]+/g) || []
|
|
121
|
-
for (const p of rawMatches) {
|
|
122
|
-
handlePath(p)
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function handlePath(p: string) {
|
|
127
|
-
const filePath = p.trim()
|
|
128
|
-
if (filePath.includes('\n')) return
|
|
129
|
-
if (isValidFile(filePath)) {
|
|
130
|
-
currentSet.add(filePath)
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const newFiles = []
|
|
135
|
-
for (const file of currentSet) {
|
|
136
|
-
if (!globalSet.has(file)) {
|
|
137
|
-
globalSet.add(file)
|
|
138
|
-
newFiles.push(file)
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return newFiles
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function isValidFile(p: string) {
|
|
146
|
-
return (
|
|
147
|
-
/\/(upload|mobook)\//i.test(p) &&
|
|
148
|
-
/\.[a-zA-Z0-9]+$/.test(p)
|
|
149
|
-
)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return { getNewFiles }
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* 从文本中提取 /mobook 目录下的文件
|
|
157
|
-
* @param {string} text
|
|
158
|
-
* @returns {string[]}
|
|
159
|
-
*/
|
|
160
|
-
const EXT_LIST = [
|
|
161
|
-
// 文档类
|
|
162
|
-
'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'pdf', 'txt', 'rtf', 'odt',
|
|
163
|
-
|
|
164
|
-
// 数据/开发
|
|
165
|
-
'json', 'xml', 'csv', 'yaml', 'yml',
|
|
166
|
-
|
|
167
|
-
// 前端/文本
|
|
168
|
-
'html', 'htm', 'md', 'markdown', 'css', 'js', 'ts',
|
|
169
|
-
|
|
170
|
-
// 图片
|
|
171
|
-
'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg', 'ico', 'tiff',
|
|
172
|
-
|
|
173
|
-
// 音频
|
|
174
|
-
'mp3', 'wav', 'ogg', 'aac', 'flac', 'm4a',
|
|
175
|
-
|
|
176
|
-
// 视频
|
|
177
|
-
'mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm',
|
|
178
|
-
|
|
179
|
-
// 压缩包
|
|
180
|
-
'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz',
|
|
181
|
-
|
|
182
|
-
// 可执行/程序
|
|
183
|
-
'exe', 'dmg', 'pkg', 'apk', 'ipa',
|
|
184
|
-
|
|
185
|
-
// 其他常见
|
|
186
|
-
'log', 'dat', 'bin'
|
|
187
|
-
];
|
|
188
|
-
|
|
189
|
-
function extractMobookFiles(text = '') {
|
|
190
|
-
if (typeof text !== 'string' || !text.trim()) return [];
|
|
191
|
-
|
|
192
|
-
const result = new Set();
|
|
193
|
-
|
|
194
|
-
// ✅ 扩展名
|
|
195
|
-
const EXT = `(${EXT_LIST.join('|')})`;
|
|
196
|
-
|
|
197
|
-
// ✅ 文件名字符(增强:支持中文、符号)
|
|
198
|
-
const FILE_NAME = `[\\w\\u4e00-\\u9fa5::《》()()\\-\\s]+?`;
|
|
199
|
-
|
|
200
|
-
try {
|
|
201
|
-
// 1️⃣ `xxx.xxx`
|
|
202
|
-
const backtickReg = new RegExp(`\`([^\\\`]+?\\.${EXT})\``, 'gi');
|
|
203
|
-
(text.match(backtickReg) || []).forEach(item => {
|
|
204
|
-
const name = item.replace(/`/g, '').trim();
|
|
205
|
-
if (isValidFileName(name)) {
|
|
206
|
-
result.add(`/mobook/${name}`);
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
// 2️⃣ /mobook/xxx.xxx
|
|
211
|
-
const fullPathReg = new RegExp(`/mobook/${FILE_NAME}\\.${EXT}`, 'gi');
|
|
212
|
-
(text.match(fullPathReg) || []).forEach(p => {
|
|
213
|
-
result.add(normalizePath(p));
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// 3️⃣ mobook下的 xxx.xxx
|
|
217
|
-
const inlineReg = new RegExp(`mobook下的\\s*(${FILE_NAME}\\.${EXT})`, 'gi');
|
|
218
|
-
(text.match(inlineReg) || []).forEach(item => {
|
|
219
|
-
const match = item.match(new RegExp(`${FILE_NAME}\\.${EXT}`, 'i'));
|
|
220
|
-
if (match && isValidFileName(match[0])) {
|
|
221
|
-
result.add(`/mobook/${match[0].trim()}`);
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// 🆕 4️⃣ **xxx.xxx**
|
|
226
|
-
const boldReg = new RegExp(`\\*\\*(${FILE_NAME}\\.${EXT})\\*\\*`, 'gi');
|
|
227
|
-
(text.match(boldReg) || []).forEach(item => {
|
|
228
|
-
const name = item.replace(/\*\*/g, '').trim();
|
|
229
|
-
if (isValidFileName(name)) {
|
|
230
|
-
result.add(`/mobook/${name}`);
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
// 🆕 5️⃣ xxx.xxx (123字节)
|
|
235
|
-
const looseReg = new RegExp(`(${FILE_NAME}\\.${EXT})\\s*\\(`, 'gi');
|
|
236
|
-
(text.match(looseReg) || []).forEach(item => {
|
|
237
|
-
const name = item.replace(/\s*\(.+$/, '').trim();
|
|
238
|
-
if (isValidFileName(name)) {
|
|
239
|
-
result.add(`/mobook/${name}`);
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
} catch (e) {
|
|
244
|
-
console.warn('extractMobookFiles error:', e);
|
|
121
|
+
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined
|
|
245
122
|
}
|
|
246
|
-
|
|
247
|
-
return [...result];
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* 校验文件名是否合法(避免脏数据)
|
|
252
|
-
*/
|
|
253
|
-
function isValidFileName(name: string) {
|
|
254
|
-
if (!name) return false;
|
|
255
|
-
|
|
256
|
-
// 过滤异常字符
|
|
257
|
-
if (/[\/\\<>:"|?*]/.test(name)) return false;
|
|
258
|
-
|
|
259
|
-
// 长度限制(防止异常长字符串)
|
|
260
|
-
if (name.length > 200) return false;
|
|
261
|
-
|
|
262
|
-
return true;
|
|
263
123
|
}
|
|
264
124
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
function normalizePath(path: string) {
|
|
269
|
-
return path
|
|
270
|
-
.replace(/\/+/g, '/') // 多斜杠 → 单斜杠
|
|
271
|
-
.replace(/\/$/, ''); // 去掉结尾 /
|
|
125
|
+
function resolveReplyMediaList(payload: ReplyPayload): string[] {
|
|
126
|
+
if (payload.mediaUrls?.length) return payload.mediaUrls.filter(Boolean)
|
|
127
|
+
return payload.mediaUrl ? [payload.mediaUrl] : []
|
|
272
128
|
}
|
|
273
129
|
|
|
274
130
|
/**
|
|
275
131
|
* 处理一条用户消息,调用 Agent 并返回回复
|
|
276
132
|
*/
|
|
277
|
-
export async function handleDcgchatMessage(
|
|
278
|
-
|
|
279
|
-
msg: InboundMessage;
|
|
280
|
-
accountId: string;
|
|
281
|
-
runtime?: RuntimeEnv;
|
|
282
|
-
onChunk: (reply: OutboundReply) => void;
|
|
283
|
-
}): Promise<void> {
|
|
284
|
-
const { cfg, msg, accountId, runtime } = params;
|
|
285
|
-
const log = runtime?.log ?? console.log;
|
|
286
|
-
const error = runtime?.error ?? console.error;
|
|
287
|
-
// 完整的文本
|
|
288
|
-
let completeText = ''
|
|
133
|
+
export async function handleDcgchatMessage(msg: InboundMessage, accountId: string): Promise<void> {
|
|
134
|
+
const msgCtx = createMsgContext(msg)
|
|
289
135
|
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
|
|
136
|
+
let completeText = ''
|
|
137
|
+
const config = getOpenClawConfig()
|
|
138
|
+
if (!config) {
|
|
139
|
+
dcgLogger('no config available', 'error')
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
const account = resolveAccount(config, accountId)
|
|
143
|
+
const userId = msg._userId.toString()
|
|
144
|
+
const text = msg.content.text?.trim()
|
|
293
145
|
|
|
294
146
|
if (!text) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
source: "client",
|
|
299
|
-
// @ts-ignore
|
|
300
|
-
content: {
|
|
301
|
-
bot_token: msg.content.bot_token,
|
|
302
|
-
domain_id: msg.content.domain_id,
|
|
303
|
-
app_id: msg.content.app_id,
|
|
304
|
-
bot_id: msg.content.bot_id,
|
|
305
|
-
agent_id: msg.content.agent_id,
|
|
306
|
-
session_id: msg.content.session_id,
|
|
307
|
-
message_id: msg.content.message_id,
|
|
308
|
-
response: "你需要我帮你做什么呢?",
|
|
309
|
-
},
|
|
310
|
-
});
|
|
311
|
-
return;
|
|
147
|
+
sendTextMsg(msgCtx, '你需要我帮你做什么呢?')
|
|
148
|
+
sendFinal(msgCtx)
|
|
149
|
+
return
|
|
312
150
|
}
|
|
313
151
|
|
|
314
152
|
try {
|
|
315
|
-
const core = getDcgchatRuntime()
|
|
153
|
+
const core = getDcgchatRuntime()
|
|
154
|
+
|
|
155
|
+
const conversationId = msg.content.session_id?.trim()
|
|
316
156
|
|
|
317
157
|
const route = core.channel.routing.resolveAgentRoute({
|
|
318
|
-
cfg,
|
|
158
|
+
cfg: config,
|
|
319
159
|
channel: "dcgchat",
|
|
320
160
|
accountId: account.accountId,
|
|
321
|
-
peer: { kind:
|
|
322
|
-
})
|
|
161
|
+
peer: { kind: 'direct', id: conversationId }
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// If conversation_id encodes an agentId prefix ("agentId::suffix"), override the route.
|
|
165
|
+
const embeddedAgentId = extractAgentIdFromConversationId(conversationId)
|
|
166
|
+
const effectiveAgentId = embeddedAgentId ?? route.agentId
|
|
167
|
+
const effectiveSessionKey = embeddedAgentId
|
|
168
|
+
? `agent:${embeddedAgentId}:mobook:direct:${conversationId}`.toLowerCase()
|
|
169
|
+
: route.sessionKey
|
|
170
|
+
|
|
171
|
+
const agentEntry =
|
|
172
|
+
effectiveAgentId && effectiveAgentId !== 'main' ? config.agents?.list?.find((a) => a.id === effectiveAgentId) : undefined
|
|
173
|
+
const agentDisplayName = agentEntry?.name ?? (effectiveAgentId && effectiveAgentId !== 'main' ? effectiveAgentId : undefined)
|
|
174
|
+
|
|
175
|
+
// Abort any existing generation for this conversation, then start a new one
|
|
176
|
+
const existingCtrl = activeGenerations.get(conversationId)
|
|
177
|
+
if (existingCtrl) existingCtrl.abort()
|
|
178
|
+
const genCtrl = new AbortController()
|
|
179
|
+
const genSignal = genCtrl.signal
|
|
180
|
+
activeGenerations.set(conversationId, genCtrl)
|
|
323
181
|
|
|
324
182
|
// 处理用户上传的文件
|
|
325
|
-
const files = msg.content.files ?? []
|
|
326
|
-
let mediaPayload: Record<string, unknown> = {}
|
|
183
|
+
const files = msg.content.files ?? []
|
|
184
|
+
let mediaPayload: Record<string, unknown> = {}
|
|
327
185
|
if (files.length > 0) {
|
|
328
|
-
const mediaList = await resolveMediaFromUrls(files, msg.content.bot_token
|
|
329
|
-
mediaPayload = buildMediaPayload(mediaList)
|
|
330
|
-
log(`dcgchat[${accountId}]: media resolved ${mediaList.length}/${files.length} file(s), payload=${JSON.stringify(mediaList)}`);
|
|
186
|
+
const mediaList = await resolveMediaFromUrls(files, msg.content.bot_token)
|
|
187
|
+
mediaPayload = buildMediaPayload(mediaList)
|
|
331
188
|
}
|
|
332
189
|
|
|
333
|
-
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(
|
|
334
|
-
|
|
335
|
-
// 补充消息
|
|
336
|
-
const messageBody = text;
|
|
190
|
+
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config)
|
|
191
|
+
const messageBody = text
|
|
337
192
|
const bodyFormatted = core.channel.reply.formatAgentEnvelope({
|
|
338
|
-
channel:
|
|
193
|
+
channel: '书灵墨宝',
|
|
339
194
|
from: userId,
|
|
340
195
|
timestamp: new Date(),
|
|
341
196
|
envelope: envelopeOptions,
|
|
342
|
-
body: messageBody
|
|
343
|
-
})
|
|
197
|
+
body: messageBody
|
|
198
|
+
})
|
|
344
199
|
|
|
345
200
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
346
201
|
Body: bodyFormatted,
|
|
347
202
|
RawBody: text,
|
|
348
203
|
CommandBody: text,
|
|
349
204
|
From: userId,
|
|
350
|
-
To:
|
|
351
|
-
SessionKey:
|
|
352
|
-
AccountId:
|
|
353
|
-
ChatType:
|
|
354
|
-
SenderName:
|
|
205
|
+
To: conversationId,
|
|
206
|
+
SessionKey: effectiveSessionKey,
|
|
207
|
+
AccountId: route.accountId,
|
|
208
|
+
ChatType: 'direct',
|
|
209
|
+
SenderName: agentDisplayName,
|
|
355
210
|
SenderId: userId,
|
|
356
|
-
Provider: "dcgchat"
|
|
357
|
-
Surface: "dcgchat"
|
|
211
|
+
Provider: "dcgchat",
|
|
212
|
+
Surface: "dcgchat",
|
|
358
213
|
MessageSid: msg.content.message_id,
|
|
359
214
|
Timestamp: Date.now(),
|
|
360
215
|
WasMentioned: true,
|
|
361
216
|
CommandAuthorized: true,
|
|
362
|
-
OriginatingChannel: "dcgchat"
|
|
217
|
+
OriginatingChannel: "dcgchat",
|
|
363
218
|
OriginatingTo: `user:${userId}`,
|
|
364
|
-
...mediaPayload
|
|
365
|
-
})
|
|
366
|
-
|
|
367
|
-
log(`dcgchat[${accountId}]: ctxPayload=${JSON.stringify(ctxPayload)}`);
|
|
219
|
+
...mediaPayload
|
|
220
|
+
})
|
|
368
221
|
|
|
369
222
|
const sentMediaKeys = new Set<string>()
|
|
370
223
|
const getMediaKey = (url: string) => url.split(/[\\/]/).pop() ?? url
|
|
371
|
-
let
|
|
372
|
-
|
|
373
|
-
const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId });
|
|
224
|
+
let streamedTextLen = 0
|
|
374
225
|
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
226
|
+
const prefixContext = createReplyPrefixContext({
|
|
227
|
+
cfg: config,
|
|
228
|
+
agentId: effectiveAgentId ?? '',
|
|
229
|
+
channel: "dcgchat",
|
|
230
|
+
accountId: account.accountId
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
const { dispatcher, replyOptions, markDispatchIdle, markRunComplete } = core.channel.reply.createReplyDispatcherWithTyping({
|
|
234
|
+
responsePrefix: prefixContext.responsePrefix,
|
|
235
|
+
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
236
|
+
humanDelay: core.channel.reply.resolveHumanDelayConfig(config, route.agentId),
|
|
237
|
+
onReplyStart: async () => {},
|
|
238
|
+
deliver: async (payload: ReplyPayload, info) => {
|
|
239
|
+
const mediaList = resolveReplyMediaList(payload)
|
|
240
|
+
for (const mediaUrl of mediaList) {
|
|
241
|
+
const key = getMediaKey(mediaUrl)
|
|
242
|
+
if (sentMediaKeys.has(key)) continue
|
|
243
|
+
sentMediaKeys.add(key)
|
|
244
|
+
await sendDcgchatMedia({ msgCtx, mediaUrl, text: '' })
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
onError: (err: unknown, info: { kind: string }) => {
|
|
248
|
+
dcgLogger(`${info.kind} reply failed: ${String(err)}`, 'error')
|
|
249
|
+
},
|
|
250
|
+
onIdle: () => {
|
|
251
|
+
sendFinal(msgCtx)
|
|
252
|
+
}
|
|
253
|
+
})
|
|
389
254
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
},
|
|
432
|
-
});
|
|
433
|
-
} else {
|
|
434
|
-
log(`dcgchat[${accountId}]: dispatching to agent (session=${route.sessionKey})`);
|
|
435
|
-
await core.channel.reply.dispatchReplyFromConfig({
|
|
436
|
-
ctx: ctxPayload,
|
|
437
|
-
cfg,
|
|
438
|
-
dispatcher,
|
|
439
|
-
replyOptions: {
|
|
440
|
-
...replyOptions,
|
|
441
|
-
onModelSelected: prefixContext.onModelSelected,
|
|
442
|
-
onPartialReply: async (payload: ReplyPayload) => {
|
|
443
|
-
log(`dcgchat[${accountId}][deliver]: received chunk, text length=${payload.text?.length || 0}`);
|
|
444
|
-
if (payload.text) {
|
|
445
|
-
completeText = payload.text
|
|
446
|
-
}
|
|
447
|
-
const mediaList = resolveReplyMediaList(payload);
|
|
448
|
-
if (mediaList.length > 0) {
|
|
449
|
-
for (let i = 0; i < mediaList.length; i++) {
|
|
450
|
-
const mediaUrl = mediaList[i];
|
|
451
|
-
const key = getMediaKey(mediaUrl);
|
|
452
|
-
if (sentMediaKeys.has(key)) continue;
|
|
453
|
-
if (!/^https?:\/\//i.test(mediaUrl) && !fs.existsSync(mediaUrl)) {
|
|
454
|
-
log(`dcgchat[${accountId}][deliver]: media file not found, skipping: ${mediaUrl}`);
|
|
455
|
-
continue;
|
|
255
|
+
let wasAborted = false
|
|
256
|
+
try {
|
|
257
|
+
if (systemCommand.includes(text?.trim())) {
|
|
258
|
+
dcgLogger(`dispatching /new`)
|
|
259
|
+
await core.channel.reply.dispatchReplyFromConfig({
|
|
260
|
+
ctx: ctxPayload,
|
|
261
|
+
cfg: config,
|
|
262
|
+
dispatcher,
|
|
263
|
+
replyOptions: {
|
|
264
|
+
...replyOptions,
|
|
265
|
+
onModelSelected: prefixContext.onModelSelected
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
} else if (interruptCommand.includes(text?.trim())) {
|
|
269
|
+
dcgLogger(`interrupt command: ${text}`)
|
|
270
|
+
abortMobookappGeneration(conversationId)
|
|
271
|
+
sendFinal(msgCtx)
|
|
272
|
+
return
|
|
273
|
+
} else {
|
|
274
|
+
dcgLogger(`dispatching to agent (session=${route.sessionKey})`)
|
|
275
|
+
await core.channel.reply.dispatchReplyFromConfig({
|
|
276
|
+
ctx: ctxPayload,
|
|
277
|
+
cfg: config,
|
|
278
|
+
dispatcher,
|
|
279
|
+
replyOptions: {
|
|
280
|
+
...replyOptions,
|
|
281
|
+
abortSignal: genSignal,
|
|
282
|
+
onModelSelected: prefixContext.onModelSelected,
|
|
283
|
+
onPartialReply: async (payload: ReplyPayload) => {
|
|
284
|
+
// Accumulate full text
|
|
285
|
+
if (payload.text) {
|
|
286
|
+
completeText = payload.text
|
|
287
|
+
}
|
|
288
|
+
// --- Streaming text chunks ---
|
|
289
|
+
if (payload.text) {
|
|
290
|
+
const delta = payload.text.startsWith(completeText.slice(0, streamedTextLen))
|
|
291
|
+
? payload.text.slice(streamedTextLen)
|
|
292
|
+
: payload.text
|
|
293
|
+
if (delta.trim()) {
|
|
294
|
+
sendChunk(msgCtx, delta)
|
|
295
|
+
dcgLogger(`[stream]: chunk ${delta.length} chars to user ${msg._userId} ${delta.slice(0, 100)}`)
|
|
456
296
|
}
|
|
457
|
-
|
|
458
|
-
await sendDcgchatMedia({
|
|
459
|
-
cfg,
|
|
460
|
-
accountId,
|
|
461
|
-
log,
|
|
462
|
-
mediaUrl,
|
|
463
|
-
text: "",
|
|
464
|
-
});
|
|
297
|
+
streamedTextLen = payload.text.length
|
|
465
298
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
messageType: "openclaw_bot_chat",
|
|
474
|
-
_userId: msg._userId,
|
|
475
|
-
source: "client",
|
|
476
|
-
content: {
|
|
477
|
-
bot_token: msg.content.bot_token,
|
|
478
|
-
domain_id: msg.content.domain_id,
|
|
479
|
-
app_id: msg.content.app_id,
|
|
480
|
-
bot_id: msg.content.bot_id,
|
|
481
|
-
agent_id: msg.content.agent_id,
|
|
482
|
-
session_id: msg.content.session_id,
|
|
483
|
-
message_id: msg.content.message_id,
|
|
484
|
-
response: nextTextChunk,
|
|
485
|
-
state: 'chunk',
|
|
486
|
-
},
|
|
487
|
-
});
|
|
488
|
-
log(`dcgchat[${accountId}][deliver]: chunk sent successfully`);
|
|
299
|
+
// --- Media from payload ---
|
|
300
|
+
const mediaList = resolveReplyMediaList(payload)
|
|
301
|
+
for (const mediaUrl of mediaList) {
|
|
302
|
+
const key = getMediaKey(mediaUrl)
|
|
303
|
+
if (sentMediaKeys.has(key)) continue
|
|
304
|
+
sentMediaKeys.add(key)
|
|
305
|
+
await sendDcgchatMedia({ msgCtx, mediaUrl, text: '' })
|
|
489
306
|
}
|
|
490
|
-
textChunk = payload.text
|
|
491
|
-
} else {
|
|
492
|
-
log(`dcgchat[${accountId}][deliver]: skipping empty chunk`);
|
|
493
307
|
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
continue;
|
|
511
|
-
}
|
|
512
|
-
if (!fs.existsSync(url)) {
|
|
513
|
-
log(`dcgchat[${accountId}]: completeFiles file not found, skipping: ${url}`);
|
|
514
|
-
continue;
|
|
515
|
-
}
|
|
516
|
-
sentMediaKeys.add(key);
|
|
517
|
-
await sendDcgchatMedia({
|
|
518
|
-
cfg,
|
|
519
|
-
accountId,
|
|
520
|
-
log,
|
|
521
|
-
mediaUrl: url,
|
|
522
|
-
text: "",
|
|
523
|
-
});
|
|
308
|
+
}
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
} catch (err: unknown) {
|
|
312
|
+
if (genSignal.aborted) {
|
|
313
|
+
wasAborted = true
|
|
314
|
+
dcgLogger(` generation aborted for conversationId=${conversationId}`)
|
|
315
|
+
} else if (err instanceof Error && err.name === 'AbortError') {
|
|
316
|
+
wasAborted = true
|
|
317
|
+
dcgLogger(` generation aborted for conversationId=${conversationId}`)
|
|
318
|
+
} else {
|
|
319
|
+
dcgLogger(` dispatchReplyFromConfig error: ${String(err)}`, 'error')
|
|
320
|
+
}
|
|
321
|
+
} finally {
|
|
322
|
+
if (activeGenerations.get(conversationId) === genCtrl) {
|
|
323
|
+
activeGenerations.delete(conversationId)
|
|
524
324
|
}
|
|
525
|
-
log(`dcgchat[${accountId}][deliver]: sent ${completeFiles.length} media file(s) through channel adapter`);
|
|
526
325
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
if (!fs.existsSync(
|
|
537
|
-
|
|
538
|
-
if (!fs.existsSync(
|
|
539
|
-
log(`dcgchat[${accountId}]: mobookFiles file not found, skipping: ${url}`);
|
|
540
|
-
continue;
|
|
541
|
-
}
|
|
326
|
+
try {
|
|
327
|
+
markRunComplete()
|
|
328
|
+
} catch (err) {
|
|
329
|
+
dcgLogger(` markRunComplete error: ${String(err)}`, 'error')
|
|
330
|
+
}
|
|
331
|
+
markDispatchIdle()
|
|
332
|
+
if (![...systemCommand, ...interruptCommand].includes(text?.trim())) {
|
|
333
|
+
for (const file of extractMobookFiles(completeText)) {
|
|
334
|
+
let resolved = file
|
|
335
|
+
if (!fs.existsSync(resolved)) {
|
|
336
|
+
resolved = path.join(getWorkspaceDir(), file)
|
|
337
|
+
if (!fs.existsSync(resolved)) return
|
|
542
338
|
}
|
|
543
|
-
|
|
544
|
-
await sendDcgchatMedia({
|
|
545
|
-
cfg,
|
|
546
|
-
accountId,
|
|
547
|
-
log,
|
|
548
|
-
mediaUrl: url,
|
|
549
|
-
text: "",
|
|
550
|
-
});
|
|
339
|
+
await sendDcgchatMedia({ msgCtx, mediaUrl: resolved, text: '' })
|
|
551
340
|
}
|
|
552
|
-
log(`dcgchat[${accountId}][deliver]: sent ${mobookFiles.length} media file(s) through channel adapter`);
|
|
553
341
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
setMsgStatus('finished');
|
|
573
|
-
textChunk = ''
|
|
574
|
-
log(`dcgchat[${accountId}]: final state sent`);
|
|
575
|
-
|
|
576
|
-
markDispatchIdle();
|
|
577
|
-
log(`dcgchat[${accountId}]: message handling complete`);
|
|
578
|
-
|
|
342
|
+
sendFinal(msgCtx)
|
|
343
|
+
clearSentMediaKeys(msg.content.message_id)
|
|
344
|
+
setMsgStatus('finished')
|
|
345
|
+
|
|
346
|
+
// Record session metadata
|
|
347
|
+
const storePath = core.channel.session.resolveStorePath(config.session?.store)
|
|
348
|
+
core.channel.session
|
|
349
|
+
.recordInboundSession({
|
|
350
|
+
storePath,
|
|
351
|
+
sessionKey: effectiveSessionKey,
|
|
352
|
+
ctx: ctxPayload,
|
|
353
|
+
onRecordError: (err) => {
|
|
354
|
+
dcgLogger(` session record error: ${String(err)}`, 'error')
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
.catch((err: unknown) => {
|
|
358
|
+
dcgLogger(` recordInboundSession failed: ${String(err)}`, 'error')
|
|
359
|
+
})
|
|
579
360
|
} catch (err) {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
messageType: "openclaw_bot_chat",
|
|
583
|
-
_userId: msg._userId,
|
|
584
|
-
source: "client",
|
|
585
|
-
content: {
|
|
586
|
-
bot_token: msg.content.bot_token,
|
|
587
|
-
domain_id: msg.content.domain_id,
|
|
588
|
-
app_id: msg.content.app_id,
|
|
589
|
-
bot_id: msg.content.bot_id,
|
|
590
|
-
agent_id: msg.content.agent_id,
|
|
591
|
-
session_id: msg.content.session_id,
|
|
592
|
-
message_id: msg.content.message_id,
|
|
593
|
-
response: `[错误] ${err instanceof Error ? err.message : String(err)}`,
|
|
594
|
-
state: 'final',
|
|
595
|
-
},
|
|
596
|
-
});
|
|
361
|
+
dcgLogger(` handle message failed: ${String(err)}`, 'error')
|
|
362
|
+
sendError(msgCtx, err instanceof Error ? err.message : String(err))
|
|
597
363
|
}
|
|
598
364
|
}
|