@xmanrui/dsh-im 0.8.0 → 0.10.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/README.en.md +8 -4
- package/README.md +8 -4
- package/lib/client.js +16 -14
- package/lib/index.js +123 -121
- package/package.json +1 -1
- package/plugin-src/client/styles.js +15 -14
- package/plugin-src/host/channels/dingtalk/index.mjs +1 -1
- package/plugin-src/host/channels/dingtalk/production.mjs +3 -0
- package/plugin-src/host/channels/discord/index.mjs +1 -1
- package/plugin-src/host/channels/feishu/index.mjs +1 -1
- package/plugin-src/host/channels/feishu/production.mjs +3 -0
- package/plugin-src/host/channels/qq/index.mjs +1 -1
- package/plugin-src/host/channels/qq/production.mjs +3 -0
- package/plugin-src/host/channels/shared/production.mjs +3 -0
- package/plugin-src/host/channels/slack/index.mjs +1 -1
- package/plugin-src/host/channels/slack/production.mjs +3 -0
- package/plugin-src/host/channels/telegram/index.mjs +1 -1
- package/plugin-src/host/channels/wecom/index.mjs +1 -1
- package/plugin-src/host/channels/wecom/production.mjs +3 -0
- package/plugin-src/host/channels/weixin/index.mjs +1 -1
- package/plugin-src/host/channels/weixin/production.mjs +3 -0
- package/plugin-src/host/channels/whatsapp/index.mjs +1 -1
- package/plugin-src/host/channels/whatsapp/production.mjs +3 -0
- package/plugin-src/host/harness-command-executor.mjs +21 -0
- package/plugin-src/host/index.mjs +1 -1
- package/src/channels/dingtalk/dingtalk-api.mjs +82 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +162 -13
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/discord/discord-runtime.mjs +44 -1
- package/src/channels/feishu/bridge.mjs +45 -11
- package/src/channels/feishu/message-utils.mjs +142 -8
- package/src/channels/feishu/plugin-controller.mjs +1 -0
- package/src/channels/qq/qq-bridge.mjs +107 -13
- package/src/channels/shared/bot-workspace-store.mjs +13 -0
- package/src/channels/shared/compact-command.mjs +95 -0
- package/src/channels/shared/harness-client.mjs +32 -2
- package/src/channels/shared/image-prompt.mjs +268 -0
- package/src/channels/shared/text-harness-bridge.mjs +49 -9
- package/src/channels/shared/workspace-session.mjs +2 -1
- package/src/channels/slack/manifest.mjs +1 -0
- package/src/channels/slack/slack-api.mjs +95 -0
- package/src/channels/slack/slack-runtime.mjs +24 -3
- package/src/channels/telegram/telegram-api.mjs +37 -0
- package/src/channels/telegram/telegram-runtime.mjs +71 -9
- package/src/channels/wecom/wecom-bridge.mjs +163 -16
- package/src/channels/weixin/weixin-api.mjs +98 -2
- package/src/channels/weixin/weixin-bridge.mjs +55 -12
- package/src/channels/whatsapp/whatsapp-runtime.mjs +144 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
extractWeixinImages,
|
|
2
3
|
extractWeixinText,
|
|
3
4
|
splitWeixinText,
|
|
4
5
|
weixinMessageId,
|
|
@@ -9,16 +10,23 @@ import {
|
|
|
9
10
|
validHarnessQuestion,
|
|
10
11
|
} from '../shared/harness-question.mjs';
|
|
11
12
|
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
13
|
+
import { runCompactCommand } from '../shared/compact-command.mjs';
|
|
12
14
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
13
15
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
16
|
+
import {
|
|
17
|
+
hasInboundImages,
|
|
18
|
+
imagePromptUserMessage,
|
|
19
|
+
promptContentForMessage,
|
|
20
|
+
} from '../shared/image-prompt.mjs';
|
|
14
21
|
|
|
15
22
|
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
|
16
23
|
|
|
17
24
|
const HELP_TEXT = [
|
|
18
25
|
'微信已连接 DeepSeek Harness。',
|
|
19
26
|
'',
|
|
20
|
-
'
|
|
27
|
+
'直接发送文字、图片或带文字识别结果的语音即可继续当前会话。',
|
|
21
28
|
'/new 开启一个全新会话',
|
|
29
|
+
'/compact 压缩当前会话的较早上下文',
|
|
22
30
|
'/workspace 工作区绝对路径 切换工作区',
|
|
23
31
|
'/workspacelist 列出工作区绝对路径',
|
|
24
32
|
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
@@ -35,9 +43,15 @@ function nonEmptyString(value) {
|
|
|
35
43
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
36
44
|
}
|
|
37
45
|
|
|
46
|
+
function hasWeixinImageItems(message) {
|
|
47
|
+
return Array.isArray(message?.item_list)
|
|
48
|
+
&& message.item_list.some((item) => item?.image_item && typeof item.image_item === 'object');
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
function canClaimInteractionReply(message, pending) {
|
|
39
52
|
return pending.questions[pending.index]
|
|
40
53
|
&& nonEmptyString(message?.from_user_id) === pending.actor
|
|
54
|
+
&& !hasWeixinImageItems(message)
|
|
41
55
|
&& nonEmptyString(extractWeixinText(message));
|
|
42
56
|
}
|
|
43
57
|
|
|
@@ -122,7 +136,7 @@ export class WeixinHarnessBridge {
|
|
|
122
136
|
key,
|
|
123
137
|
actor: sender,
|
|
124
138
|
messageId,
|
|
125
|
-
text: extractWeixinText(message),
|
|
139
|
+
text: hasWeixinImageItems(message) ? '' : extractWeixinText(message),
|
|
126
140
|
addressed: true,
|
|
127
141
|
hasPendingQuestion: Boolean(pending),
|
|
128
142
|
questionCompletion: pending?.submitting || pending?.claimedReplyMessageId
|
|
@@ -214,33 +228,40 @@ export class WeixinHarnessBridge {
|
|
|
214
228
|
|
|
215
229
|
const contextToken = typeof message.context_token === 'string' ? message.context_token : undefined;
|
|
216
230
|
const runId = typeof message.run_id === 'string' ? message.run_id : undefined;
|
|
217
|
-
const text = extractWeixinText(message);
|
|
231
|
+
const text = extractWeixinText(message) ?? '';
|
|
218
232
|
try {
|
|
219
|
-
|
|
220
|
-
|
|
233
|
+
const images = typeof this.#api.inboundImages === 'function'
|
|
234
|
+
? this.#api.inboundImages(message)
|
|
235
|
+
: extractWeixinImages(message);
|
|
236
|
+
const promptMessage = { content: text, images };
|
|
237
|
+
const hasImages = hasInboundImages(promptMessage);
|
|
238
|
+
if (!text && !hasImages) {
|
|
239
|
+
await this.#send(sender, '目前支持文字、图片,以及微信已转成文字的语音消息。', contextToken, runId);
|
|
221
240
|
await this.#state.markSeen(messageId);
|
|
222
241
|
return;
|
|
223
242
|
}
|
|
224
243
|
|
|
225
244
|
const command = text.trim().toLowerCase();
|
|
226
|
-
if (command === '/help') {
|
|
245
|
+
if (!hasImages && command === '/help') {
|
|
227
246
|
await this.#send(sender, HELP_TEXT, contextToken, runId);
|
|
228
247
|
await this.#state.markSeen(messageId);
|
|
229
248
|
return;
|
|
230
249
|
}
|
|
231
|
-
if (command === '/status') {
|
|
250
|
+
if (!hasImages && command === '/status') {
|
|
232
251
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
233
252
|
await this.#send(sender, '微信与 DeepSeek Harness 连接正常。', contextToken, runId);
|
|
234
253
|
await this.#state.markSeen(messageId);
|
|
235
254
|
return;
|
|
236
255
|
}
|
|
237
|
-
if (command === '/new') {
|
|
256
|
+
if (!hasImages && command === '/new') {
|
|
238
257
|
await this.#state.clearSession(key);
|
|
239
258
|
await this.#send(sender, '已开启新会话。请发送你的问题。', contextToken, runId);
|
|
240
259
|
await this.#state.markSeen(messageId);
|
|
241
260
|
return;
|
|
242
261
|
}
|
|
243
|
-
const workspaceCommand =
|
|
262
|
+
const workspaceCommand = hasImages
|
|
263
|
+
? null
|
|
264
|
+
: await runWorkspaceCommand(text, this.#harness, key);
|
|
244
265
|
if (workspaceCommand) {
|
|
245
266
|
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
|
246
267
|
await this.#send(sender, reply, contextToken, runId);
|
|
@@ -248,14 +269,31 @@ export class WeixinHarnessBridge {
|
|
|
248
269
|
await this.#state.markSeen(messageId);
|
|
249
270
|
return;
|
|
250
271
|
}
|
|
272
|
+
const compactCommand = hasImages
|
|
273
|
+
? null
|
|
274
|
+
: await runCompactCommand(
|
|
275
|
+
text,
|
|
276
|
+
this.#harness,
|
|
277
|
+
this.#state,
|
|
278
|
+
key,
|
|
279
|
+
{ signal: this.#signal },
|
|
280
|
+
);
|
|
281
|
+
if (compactCommand) {
|
|
282
|
+
await this.#send(sender, compactCommand.message, contextToken, runId);
|
|
283
|
+
await this.#state.markSeen(messageId);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
251
286
|
|
|
287
|
+
const content = hasImages
|
|
288
|
+
? await promptContentForMessage(promptMessage, { signal: this.#signal })
|
|
289
|
+
: undefined;
|
|
252
290
|
let answer;
|
|
253
291
|
try {
|
|
254
292
|
({ answer } = await askInWorkspaceSession({
|
|
255
293
|
harness: this.#harness,
|
|
256
294
|
state: this.#state,
|
|
257
295
|
key,
|
|
258
|
-
text,
|
|
296
|
+
...(hasImages ? { content } : { text }),
|
|
259
297
|
createOptions: { signal: this.#signal },
|
|
260
298
|
existsOptions: { signal: this.#signal },
|
|
261
299
|
askOptions: {
|
|
@@ -286,7 +324,12 @@ export class WeixinHarnessBridge {
|
|
|
286
324
|
this.#status.lastError = error?.message ?? String(error);
|
|
287
325
|
this.#logger.error?.('[dsh-weixin] failed to process an inbound message:', error);
|
|
288
326
|
try {
|
|
289
|
-
await this.#send(
|
|
327
|
+
await this.#send(
|
|
328
|
+
sender,
|
|
329
|
+
imagePromptUserMessage(error) ?? '消息处理失败,请稍后重试。',
|
|
330
|
+
contextToken,
|
|
331
|
+
runId,
|
|
332
|
+
);
|
|
290
333
|
await this.#state.markSeen(messageId);
|
|
291
334
|
} catch (sendError) {
|
|
292
335
|
this.#logger.error?.('[dsh-weixin] failed to send the safe error reply:', sendError);
|
|
@@ -312,7 +355,7 @@ export class WeixinHarnessBridge {
|
|
|
312
355
|
const text = nonEmptyString(extractWeixinText(message));
|
|
313
356
|
const contextToken = nonEmptyString(message?.context_token) ?? undefined;
|
|
314
357
|
const runId = nonEmptyString(message?.run_id) ?? undefined;
|
|
315
|
-
if (!text) {
|
|
358
|
+
if (!text || hasWeixinImageItems(message)) {
|
|
316
359
|
await this.#send(
|
|
317
360
|
expected.actor,
|
|
318
361
|
'请用文字回答当前问题。',
|
|
@@ -1,12 +1,45 @@
|
|
|
1
1
|
import {
|
|
2
2
|
areJidsSameUser,
|
|
3
|
+
downloadMediaMessage,
|
|
3
4
|
normalizeMessageContent,
|
|
4
5
|
} from '@whiskeysockets/baileys';
|
|
5
6
|
|
|
6
7
|
import { splitMessageText } from '../shared/editable-message-stream.mjs';
|
|
8
|
+
import { ImagePromptError } from '../shared/image-prompt.mjs';
|
|
7
9
|
import { createWhatsappBridgeStatus, WhatsappHarnessBridge } from './whatsapp-bridge.mjs';
|
|
8
10
|
import { createWhatsappWebSession } from './whatsapp-web-session.mjs';
|
|
9
11
|
|
|
12
|
+
const IMAGE_MEDIA_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif']);
|
|
13
|
+
const DEFAULT_MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
14
|
+
const IMAGE_DOWNLOAD_TIMEOUT_MS = 15_000;
|
|
15
|
+
const MESSAGE_WRAPPER_KEYS = [
|
|
16
|
+
'ephemeralMessage',
|
|
17
|
+
'viewOnceMessage',
|
|
18
|
+
'documentWithCaptionMessage',
|
|
19
|
+
'viewOnceMessageV2',
|
|
20
|
+
'viewOnceMessageV2Extension',
|
|
21
|
+
'editedMessage',
|
|
22
|
+
'associatedChildMessage',
|
|
23
|
+
'groupStatusMessage',
|
|
24
|
+
'groupStatusMessageV2',
|
|
25
|
+
];
|
|
26
|
+
const VIEW_ONCE_WRAPPER_KEYS = new Set([
|
|
27
|
+
'viewOnceMessage',
|
|
28
|
+
'viewOnceMessageV2',
|
|
29
|
+
'viewOnceMessageV2Extension',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
function hasViewOnceWrapper(content) {
|
|
33
|
+
let current = content;
|
|
34
|
+
for (let depth = 0; depth < 5 && current && typeof current === 'object'; depth += 1) {
|
|
35
|
+
const wrapperKey = MESSAGE_WRAPPER_KEYS.find((key) => current[key]?.message);
|
|
36
|
+
if (!wrapperKey) return false;
|
|
37
|
+
if (VIEW_ONCE_WRAPPER_KEYS.has(wrapperKey)) return true;
|
|
38
|
+
current = current[wrapperKey].message;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
10
43
|
function messageContext(content) {
|
|
11
44
|
return content?.extendedTextMessage?.contextInfo
|
|
12
45
|
?? content?.imageMessage?.contextInfo
|
|
@@ -24,7 +57,114 @@ function messageText(content) {
|
|
|
24
57
|
?? '';
|
|
25
58
|
}
|
|
26
59
|
|
|
27
|
-
|
|
60
|
+
function mediaSize(value) {
|
|
61
|
+
if (Number.isSafeInteger(value) && value >= 0) return value;
|
|
62
|
+
let converted;
|
|
63
|
+
try {
|
|
64
|
+
converted = Number(value?.toString?.());
|
|
65
|
+
} catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
return Number.isSafeInteger(converted) && converted >= 0 ? converted : undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function downloadWhatsappImage(message, download, {
|
|
72
|
+
signal,
|
|
73
|
+
maxBytes = DEFAULT_MAX_IMAGE_BYTES,
|
|
74
|
+
} = {}) {
|
|
75
|
+
signal?.throwIfAborted();
|
|
76
|
+
const timeout = AbortSignal.timeout(IMAGE_DOWNLOAD_TIMEOUT_MS);
|
|
77
|
+
const downloadSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
78
|
+
const pendingStream = Promise.resolve().then(() => (
|
|
79
|
+
download(message, 'stream', { options: { signal: downloadSignal } })
|
|
80
|
+
));
|
|
81
|
+
const stream = await new Promise((resolve, reject) => {
|
|
82
|
+
let settled = false;
|
|
83
|
+
const finish = (callback, value) => {
|
|
84
|
+
if (settled) return false;
|
|
85
|
+
settled = true;
|
|
86
|
+
downloadSignal.removeEventListener('abort', onAbort);
|
|
87
|
+
callback(value);
|
|
88
|
+
return true;
|
|
89
|
+
};
|
|
90
|
+
const onAbort = () => finish(reject, downloadSignal.reason);
|
|
91
|
+
downloadSignal.addEventListener('abort', onAbort, { once: true });
|
|
92
|
+
pendingStream.then((value) => {
|
|
93
|
+
if (!finish(resolve, value)) value?.destroy?.();
|
|
94
|
+
}, (error) => finish(reject, error));
|
|
95
|
+
if (downloadSignal.aborted) onAbort();
|
|
96
|
+
}).catch((error) => {
|
|
97
|
+
if (signal?.aborted) throw signal.reason ?? error;
|
|
98
|
+
if (timeout.aborted) {
|
|
99
|
+
throw new ImagePromptError(
|
|
100
|
+
'image-download-failed',
|
|
101
|
+
`WhatsApp image download timed out after ${IMAGE_DOWNLOAD_TIMEOUT_MS} ms`,
|
|
102
|
+
'图片下载失败,请重新发送后再试。',
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
});
|
|
107
|
+
const chunks = [];
|
|
108
|
+
let size = 0;
|
|
109
|
+
const abortStream = () => stream?.destroy?.(downloadSignal.reason);
|
|
110
|
+
downloadSignal.addEventListener('abort', abortStream, { once: true });
|
|
111
|
+
try {
|
|
112
|
+
for await (const chunk of stream) {
|
|
113
|
+
downloadSignal.throwIfAborted();
|
|
114
|
+
const data = Buffer.from(chunk);
|
|
115
|
+
size += data.length;
|
|
116
|
+
if (size > maxBytes) {
|
|
117
|
+
stream.destroy?.();
|
|
118
|
+
throw new ImagePromptError(
|
|
119
|
+
'image-too-large',
|
|
120
|
+
`WhatsApp image exceeded ${maxBytes} bytes`,
|
|
121
|
+
'图片超过 5 MB,请压缩后重试。',
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
chunks.push(data);
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (signal?.aborted) throw signal.reason ?? error;
|
|
128
|
+
if (timeout.aborted) {
|
|
129
|
+
throw new ImagePromptError(
|
|
130
|
+
'image-download-failed',
|
|
131
|
+
`WhatsApp image stream timed out after ${IMAGE_DOWNLOAD_TIMEOUT_MS} ms`,
|
|
132
|
+
'图片下载失败,请重新发送后再试。',
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
} finally {
|
|
137
|
+
downloadSignal.removeEventListener('abort', abortStream);
|
|
138
|
+
}
|
|
139
|
+
return Buffer.concat(chunks, size);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function whatsappImageSource(message, content, download, { viewOnce = false } = {}) {
|
|
143
|
+
let media;
|
|
144
|
+
let name;
|
|
145
|
+
if (!viewOnce && content?.imageMessage && content.imageMessage.viewOnce !== true) {
|
|
146
|
+
media = content.imageMessage;
|
|
147
|
+
} else if (content?.documentMessage) {
|
|
148
|
+
const type = typeof content.documentMessage.mimetype === 'string'
|
|
149
|
+
? content.documentMessage.mimetype.toLowerCase() : '';
|
|
150
|
+
if (!IMAGE_MEDIA_TYPES.has(type)) return null;
|
|
151
|
+
media = content.documentMessage;
|
|
152
|
+
name = typeof media.fileName === 'string' ? media.fileName : undefined;
|
|
153
|
+
}
|
|
154
|
+
if (!media) return null;
|
|
155
|
+
const mediaType = typeof media.mimetype === 'string' ? media.mimetype.toLowerCase() : '';
|
|
156
|
+
if (!IMAGE_MEDIA_TYPES.has(mediaType)) return null;
|
|
157
|
+
return {
|
|
158
|
+
name,
|
|
159
|
+
mediaType,
|
|
160
|
+
size: mediaSize(media.fileLength),
|
|
161
|
+
load: (options) => downloadWhatsappImage(message, download, options),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function normalizeWhatsappMessage(message, accountJid, {
|
|
166
|
+
download = downloadMediaMessage,
|
|
167
|
+
} = {}) {
|
|
28
168
|
const remoteJid = typeof message?.key?.remoteJid === 'string' ? message.key.remoteJid : '';
|
|
29
169
|
const alternateRemoteJid = typeof message?.key?.remoteJidAlt === 'string'
|
|
30
170
|
? message.key.remoteJidAlt : '';
|
|
@@ -38,12 +178,14 @@ export function normalizeWhatsappMessage(message, accountJid) {
|
|
|
38
178
|
if (fromMe && !selfChat) return null;
|
|
39
179
|
const senderJid = selfChat ? accountJid : group ? message.key.participant : remoteJid;
|
|
40
180
|
if (typeof senderJid !== 'string' || !senderJid) return null;
|
|
181
|
+
const viewOnce = hasViewOnceWrapper(message.message);
|
|
41
182
|
const content = normalizeMessageContent(message.message);
|
|
42
183
|
const context = messageContext(content);
|
|
43
184
|
const mentioned = Array.isArray(context?.mentionedJid)
|
|
44
185
|
&& context.mentionedJid.some((jid) => areJidsSameUser(jid, accountJid));
|
|
45
186
|
const replyToSelf = typeof context?.participant === 'string'
|
|
46
187
|
&& areJidsSameUser(context.participant, accountJid);
|
|
188
|
+
const image = whatsappImageSource(message, content, download, { viewOnce });
|
|
47
189
|
return {
|
|
48
190
|
messageId: `${remoteJid}:${messageId}`,
|
|
49
191
|
providerMessageId: messageId,
|
|
@@ -52,6 +194,7 @@ export function normalizeWhatsappMessage(message, accountJid) {
|
|
|
52
194
|
kind: group ? 'group' : 'direct',
|
|
53
195
|
conversationId: remoteJid,
|
|
54
196
|
content: messageText(content),
|
|
197
|
+
images: image ? [image] : [],
|
|
55
198
|
addressed: !group || mentioned || replyToSelf,
|
|
56
199
|
selfChat,
|
|
57
200
|
replyTarget: { jid: remoteJid, quoted: message, selfChat },
|