@xmanrui/dsh-im 0.9.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 +5 -3
- package/README.md +5 -3
- package/lib/client.js +1 -0
- package/lib/index.js +121 -119
- package/package.json +1 -1
- package/src/channels/dingtalk/dingtalk-api.mjs +82 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +156 -20
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/discord/discord-runtime.mjs +44 -1
- package/src/channels/feishu/bridge.mjs +39 -18
- 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 +100 -20
- package/src/channels/shared/harness-client.mjs +8 -2
- package/src/channels/shared/image-prompt.mjs +268 -0
- package/src/channels/shared/text-harness-bridge.mjs +43 -16
- 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 +156 -23
- package/src/channels/weixin/weixin-api.mjs +98 -2
- package/src/channels/weixin/weixin-bridge.mjs +48 -19
- 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,
|
|
@@ -12,13 +13,18 @@ import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
|
12
13
|
import { runCompactCommand } from '../shared/compact-command.mjs';
|
|
13
14
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
14
15
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
16
|
+
import {
|
|
17
|
+
hasInboundImages,
|
|
18
|
+
imagePromptUserMessage,
|
|
19
|
+
promptContentForMessage,
|
|
20
|
+
} from '../shared/image-prompt.mjs';
|
|
15
21
|
|
|
16
22
|
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
|
17
23
|
|
|
18
24
|
const HELP_TEXT = [
|
|
19
25
|
'微信已连接 DeepSeek Harness。',
|
|
20
26
|
'',
|
|
21
|
-
'
|
|
27
|
+
'直接发送文字、图片或带文字识别结果的语音即可继续当前会话。',
|
|
22
28
|
'/new 开启一个全新会话',
|
|
23
29
|
'/compact 压缩当前会话的较早上下文',
|
|
24
30
|
'/workspace 工作区绝对路径 切换工作区',
|
|
@@ -37,9 +43,15 @@ function nonEmptyString(value) {
|
|
|
37
43
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
38
44
|
}
|
|
39
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
|
+
|
|
40
51
|
function canClaimInteractionReply(message, pending) {
|
|
41
52
|
return pending.questions[pending.index]
|
|
42
53
|
&& nonEmptyString(message?.from_user_id) === pending.actor
|
|
54
|
+
&& !hasWeixinImageItems(message)
|
|
43
55
|
&& nonEmptyString(extractWeixinText(message));
|
|
44
56
|
}
|
|
45
57
|
|
|
@@ -124,7 +136,7 @@ export class WeixinHarnessBridge {
|
|
|
124
136
|
key,
|
|
125
137
|
actor: sender,
|
|
126
138
|
messageId,
|
|
127
|
-
text: extractWeixinText(message),
|
|
139
|
+
text: hasWeixinImageItems(message) ? '' : extractWeixinText(message),
|
|
128
140
|
addressed: true,
|
|
129
141
|
hasPendingQuestion: Boolean(pending),
|
|
130
142
|
questionCompletion: pending?.submitting || pending?.claimedReplyMessageId
|
|
@@ -216,33 +228,40 @@ export class WeixinHarnessBridge {
|
|
|
216
228
|
|
|
217
229
|
const contextToken = typeof message.context_token === 'string' ? message.context_token : undefined;
|
|
218
230
|
const runId = typeof message.run_id === 'string' ? message.run_id : undefined;
|
|
219
|
-
const text = extractWeixinText(message);
|
|
231
|
+
const text = extractWeixinText(message) ?? '';
|
|
220
232
|
try {
|
|
221
|
-
|
|
222
|
-
|
|
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);
|
|
223
240
|
await this.#state.markSeen(messageId);
|
|
224
241
|
return;
|
|
225
242
|
}
|
|
226
243
|
|
|
227
244
|
const command = text.trim().toLowerCase();
|
|
228
|
-
if (command === '/help') {
|
|
245
|
+
if (!hasImages && command === '/help') {
|
|
229
246
|
await this.#send(sender, HELP_TEXT, contextToken, runId);
|
|
230
247
|
await this.#state.markSeen(messageId);
|
|
231
248
|
return;
|
|
232
249
|
}
|
|
233
|
-
if (command === '/status') {
|
|
250
|
+
if (!hasImages && command === '/status') {
|
|
234
251
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
235
252
|
await this.#send(sender, '微信与 DeepSeek Harness 连接正常。', contextToken, runId);
|
|
236
253
|
await this.#state.markSeen(messageId);
|
|
237
254
|
return;
|
|
238
255
|
}
|
|
239
|
-
if (command === '/new') {
|
|
256
|
+
if (!hasImages && command === '/new') {
|
|
240
257
|
await this.#state.clearSession(key);
|
|
241
258
|
await this.#send(sender, '已开启新会话。请发送你的问题。', contextToken, runId);
|
|
242
259
|
await this.#state.markSeen(messageId);
|
|
243
260
|
return;
|
|
244
261
|
}
|
|
245
|
-
const workspaceCommand =
|
|
262
|
+
const workspaceCommand = hasImages
|
|
263
|
+
? null
|
|
264
|
+
: await runWorkspaceCommand(text, this.#harness, key);
|
|
246
265
|
if (workspaceCommand) {
|
|
247
266
|
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
|
248
267
|
await this.#send(sender, reply, contextToken, runId);
|
|
@@ -250,26 +269,31 @@ export class WeixinHarnessBridge {
|
|
|
250
269
|
await this.#state.markSeen(messageId);
|
|
251
270
|
return;
|
|
252
271
|
}
|
|
253
|
-
const compactCommand =
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
272
|
+
const compactCommand = hasImages
|
|
273
|
+
? null
|
|
274
|
+
: await runCompactCommand(
|
|
275
|
+
text,
|
|
276
|
+
this.#harness,
|
|
277
|
+
this.#state,
|
|
278
|
+
key,
|
|
279
|
+
{ signal: this.#signal },
|
|
280
|
+
);
|
|
260
281
|
if (compactCommand) {
|
|
261
282
|
await this.#send(sender, compactCommand.message, contextToken, runId);
|
|
262
283
|
await this.#state.markSeen(messageId);
|
|
263
284
|
return;
|
|
264
285
|
}
|
|
265
286
|
|
|
287
|
+
const content = hasImages
|
|
288
|
+
? await promptContentForMessage(promptMessage, { signal: this.#signal })
|
|
289
|
+
: undefined;
|
|
266
290
|
let answer;
|
|
267
291
|
try {
|
|
268
292
|
({ answer } = await askInWorkspaceSession({
|
|
269
293
|
harness: this.#harness,
|
|
270
294
|
state: this.#state,
|
|
271
295
|
key,
|
|
272
|
-
text,
|
|
296
|
+
...(hasImages ? { content } : { text }),
|
|
273
297
|
createOptions: { signal: this.#signal },
|
|
274
298
|
existsOptions: { signal: this.#signal },
|
|
275
299
|
askOptions: {
|
|
@@ -300,7 +324,12 @@ export class WeixinHarnessBridge {
|
|
|
300
324
|
this.#status.lastError = error?.message ?? String(error);
|
|
301
325
|
this.#logger.error?.('[dsh-weixin] failed to process an inbound message:', error);
|
|
302
326
|
try {
|
|
303
|
-
await this.#send(
|
|
327
|
+
await this.#send(
|
|
328
|
+
sender,
|
|
329
|
+
imagePromptUserMessage(error) ?? '消息处理失败,请稍后重试。',
|
|
330
|
+
contextToken,
|
|
331
|
+
runId,
|
|
332
|
+
);
|
|
304
333
|
await this.#state.markSeen(messageId);
|
|
305
334
|
} catch (sendError) {
|
|
306
335
|
this.#logger.error?.('[dsh-weixin] failed to send the safe error reply:', sendError);
|
|
@@ -326,7 +355,7 @@ export class WeixinHarnessBridge {
|
|
|
326
355
|
const text = nonEmptyString(extractWeixinText(message));
|
|
327
356
|
const contextToken = nonEmptyString(message?.context_token) ?? undefined;
|
|
328
357
|
const runId = nonEmptyString(message?.run_id) ?? undefined;
|
|
329
|
-
if (!text) {
|
|
358
|
+
if (!text || hasWeixinImageItems(message)) {
|
|
330
359
|
await this.#send(
|
|
331
360
|
expected.actor,
|
|
332
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 },
|