@xmanrui/dsh-im 1.2.0 → 1.3.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/lib/index.js +166 -163
- package/package.json +1 -1
- package/plugin-src/host/channels/dingtalk/production.mjs +3 -1
- package/plugin-src/host/channels/feishu/production.mjs +3 -1
- package/plugin-src/host/channels/qq/production.mjs +3 -1
- package/plugin-src/host/channels/shared/production.mjs +3 -1
- package/plugin-src/host/channels/slack/production.mjs +3 -1
- package/plugin-src/host/channels/wecom/production.mjs +3 -1
- package/plugin-src/host/channels/weixin/production.mjs +3 -1
- package/plugin-src/host/channels/whatsapp/production.mjs +3 -1
- package/plugin-src/host/harness-session-coordinator.mjs +32 -5
- package/src/channels/dingtalk/dingtalk-api.mjs +93 -27
- package/src/channels/dingtalk/dingtalk-bridge.mjs +60 -13
- package/src/channels/discord/discord-runtime.mjs +23 -0
- package/src/channels/feishu/bridge.mjs +18 -10
- package/src/channels/feishu/message-utils.mjs +47 -0
- package/src/channels/qq/qq-bridge.mjs +80 -28
- package/src/channels/shared/file-download.mjs +64 -0
- package/src/channels/shared/harness-client.mjs +45 -0
- package/src/channels/shared/inbound-file.mjs +206 -0
- package/src/channels/shared/text-harness-bridge.mjs +31 -11
- package/src/channels/slack/slack-api.mjs +27 -4
- package/src/channels/slack/slack-runtime.mjs +55 -5
- package/src/channels/telegram/telegram-api.mjs +21 -6
- package/src/channels/telegram/telegram-runtime.mjs +24 -3
- package/src/channels/wecom/wecom-bridge.mjs +73 -10
- package/src/channels/weixin/weixin-api.mjs +45 -0
- package/src/channels/weixin/weixin-bridge.mjs +52 -18
- package/src/channels/whatsapp/whatsapp-runtime.mjs +42 -2
- package/src/channels/whatsapp/whatsapp-web-session.mjs +1 -1
|
@@ -26,6 +26,10 @@ import {
|
|
|
26
26
|
imagePromptUserMessage,
|
|
27
27
|
promptContentForMessage,
|
|
28
28
|
} from '../shared/image-prompt.mjs';
|
|
29
|
+
import {
|
|
30
|
+
hasInboundFiles,
|
|
31
|
+
inboundFileUserMessage,
|
|
32
|
+
} from '../shared/inbound-file.mjs';
|
|
29
33
|
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
|
30
34
|
import {
|
|
31
35
|
materializeOutboundArtifact,
|
|
@@ -43,7 +47,7 @@ const DEFAULT_FILE_UPLOAD_TIMEOUT_MS = 120_000;
|
|
|
43
47
|
const HELP_TEXT = [
|
|
44
48
|
'企业微信机器人已连接 DeepSeek Harness。',
|
|
45
49
|
'',
|
|
46
|
-
'
|
|
50
|
+
'直接发送文字、图片或文件即可继续当前会话。',
|
|
47
51
|
'/new 开启一个全新会话',
|
|
48
52
|
'/compact 压缩当前会话的较早上下文',
|
|
49
53
|
'/workspace 工作区绝对路径 切换工作区',
|
|
@@ -110,6 +114,13 @@ function imageContents(frame) {
|
|
|
110
114
|
.map((item) => item.image);
|
|
111
115
|
}
|
|
112
116
|
|
|
117
|
+
function fileContents(frame) {
|
|
118
|
+
const body = bodyOf(frame);
|
|
119
|
+
return body.msgtype === 'file' && body.file && typeof body.file === 'object'
|
|
120
|
+
? [body.file]
|
|
121
|
+
: [];
|
|
122
|
+
}
|
|
123
|
+
|
|
113
124
|
function imageSource(client, image) {
|
|
114
125
|
const url = nonEmptyString(image?.url);
|
|
115
126
|
if (!url) return null;
|
|
@@ -139,10 +150,56 @@ function imageSource(client, image) {
|
|
|
139
150
|
};
|
|
140
151
|
}
|
|
141
152
|
|
|
153
|
+
function fileSource(client, file) {
|
|
154
|
+
const url = nonEmptyString(file?.url);
|
|
155
|
+
if (!url) return null;
|
|
156
|
+
const aeskey = nonEmptyString(file?.aeskey) ?? undefined;
|
|
157
|
+
return {
|
|
158
|
+
name: nonEmptyString(file?.filename ?? file?.file_name ?? file?.name) ?? 'file',
|
|
159
|
+
async load({ signal } = {}) {
|
|
160
|
+
signal?.throwIfAborted();
|
|
161
|
+
if (typeof client?.downloadFile !== 'function') {
|
|
162
|
+
throw new Error('Enterprise WeChat file download is unavailable');
|
|
163
|
+
}
|
|
164
|
+
const result = await client.downloadFile(url, aeskey);
|
|
165
|
+
signal?.throwIfAborted();
|
|
166
|
+
const raw = result?.buffer ?? result?.data;
|
|
167
|
+
if (!Buffer.isBuffer(raw) && !(raw instanceof Uint8Array)) {
|
|
168
|
+
throw new Error('Enterprise WeChat file download returned no data');
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
data: Buffer.from(raw),
|
|
172
|
+
...(nonEmptyString(result?.filename) ? { name: result.filename.trim() } : {}),
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
142
178
|
export function wecomInboundMessage(frame, client) {
|
|
143
179
|
return {
|
|
144
180
|
content: messageText(frame),
|
|
145
181
|
images: imageContents(frame).map((image) => imageSource(client, image)).filter(Boolean),
|
|
182
|
+
files: fileContents(frame).map((file) => fileSource(client, file)).filter(Boolean),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function prefetchInboundFiles(message, signal) {
|
|
187
|
+
if (!Array.isArray(message?.files) || message.files.length === 0) return message;
|
|
188
|
+
return {
|
|
189
|
+
...message,
|
|
190
|
+
files: message.files.map((source) => {
|
|
191
|
+
const download = source.load({ signal });
|
|
192
|
+
download.catch(() => undefined);
|
|
193
|
+
return {
|
|
194
|
+
...source,
|
|
195
|
+
async load({ signal: loadSignal } = {}) {
|
|
196
|
+
loadSignal?.throwIfAborted();
|
|
197
|
+
const result = await download;
|
|
198
|
+
loadSignal?.throwIfAborted();
|
|
199
|
+
return result;
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}),
|
|
146
203
|
};
|
|
147
204
|
}
|
|
148
205
|
|
|
@@ -400,7 +457,7 @@ export class WecomHarnessBridge {
|
|
|
400
457
|
const pending = this.#pendingInteractions.get(key);
|
|
401
458
|
const commandMessage = wecomInboundMessage(frame, this.#client);
|
|
402
459
|
const commandText = nonEmptyString(commandMessage.content) ?? '';
|
|
403
|
-
const commandRunner = isControlCommand(commandText)
|
|
460
|
+
const commandRunner = hasInboundFiles(commandMessage) ? null : isControlCommand(commandText)
|
|
404
461
|
? runControlCommand
|
|
405
462
|
: (isModelCommand(commandText)
|
|
406
463
|
? runModelCommand
|
|
@@ -510,6 +567,7 @@ export class WecomHarnessBridge {
|
|
|
510
567
|
preparedMessage = imageQueueFullMessage(inboundMessage);
|
|
511
568
|
}
|
|
512
569
|
}
|
|
570
|
+
preparedMessage = prefetchInboundFiles(preparedMessage, this.#signal);
|
|
513
571
|
const previous = this.#queues.get(key) ?? Promise.resolve();
|
|
514
572
|
const current = previous
|
|
515
573
|
.catch(() => undefined)
|
|
@@ -543,6 +601,7 @@ export class WecomHarnessBridge {
|
|
|
543
601
|
const result = await runner(message.content, this.#harness, this.#state, key, {
|
|
544
602
|
signal: this.#signal,
|
|
545
603
|
hasImages: hasInboundImages(message),
|
|
604
|
+
hasFiles: hasInboundFiles(message),
|
|
546
605
|
pendingInteraction: this.#pendingInteractions.has(key)
|
|
547
606
|
|| this.#approvals.hasPending(key),
|
|
548
607
|
control: { owner: this, key },
|
|
@@ -701,34 +760,35 @@ export class WecomHarnessBridge {
|
|
|
701
760
|
const message = preparedMessage ?? wecomInboundMessage(frame, this.#client);
|
|
702
761
|
const text = message.content;
|
|
703
762
|
const hasImages = hasInboundImages(message);
|
|
763
|
+
const hasFiles = hasInboundFiles(message);
|
|
704
764
|
const key = conversationKey(frame);
|
|
705
765
|
let streamId = null;
|
|
706
766
|
let streamStarted = false;
|
|
707
767
|
try {
|
|
708
|
-
if (!text && !hasImages) {
|
|
709
|
-
await this.#sendImmediate(frame, chatId, '
|
|
768
|
+
if (!text && !hasImages && !hasFiles) {
|
|
769
|
+
await this.#sendImmediate(frame, chatId, '目前支持文字、图片、文件和语音转写消息。');
|
|
710
770
|
await this.#state.markSeen(messageId);
|
|
711
771
|
return;
|
|
712
772
|
}
|
|
713
773
|
const command = text.toLowerCase();
|
|
714
|
-
if (!hasImages && command === '/help') {
|
|
774
|
+
if (!hasImages && !hasFiles && command === '/help') {
|
|
715
775
|
await this.#sendImmediate(frame, chatId, HELP_TEXT);
|
|
716
776
|
await this.#state.markSeen(messageId);
|
|
717
777
|
return;
|
|
718
778
|
}
|
|
719
|
-
if (!hasImages && command === '/status') {
|
|
779
|
+
if (!hasImages && !hasFiles && command === '/status') {
|
|
720
780
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
721
781
|
await this.#sendImmediate(frame, chatId, '企业微信机器人与 DeepSeek Harness 连接正常。');
|
|
722
782
|
await this.#state.markSeen(messageId);
|
|
723
783
|
return;
|
|
724
784
|
}
|
|
725
|
-
if (!hasImages && command === '/new') {
|
|
785
|
+
if (!hasImages && !hasFiles && command === '/new') {
|
|
726
786
|
await this.#state.clearSession(key);
|
|
727
787
|
await this.#sendImmediate(frame, chatId, '已开启新会话。请发送你的问题。');
|
|
728
788
|
await this.#state.markSeen(messageId);
|
|
729
789
|
return;
|
|
730
790
|
}
|
|
731
|
-
const workspaceCommand = hasImages
|
|
791
|
+
const workspaceCommand = hasImages || hasFiles
|
|
732
792
|
? null
|
|
733
793
|
: await runWorkspaceCommand(text, this.#harness, key);
|
|
734
794
|
if (workspaceCommand) {
|
|
@@ -738,7 +798,7 @@ export class WecomHarnessBridge {
|
|
|
738
798
|
await this.#state.markSeen(messageId);
|
|
739
799
|
return;
|
|
740
800
|
}
|
|
741
|
-
const compactCommand = hasImages
|
|
801
|
+
const compactCommand = hasImages || hasFiles
|
|
742
802
|
? null
|
|
743
803
|
: await runCompactCommand(
|
|
744
804
|
text,
|
|
@@ -789,6 +849,7 @@ export class WecomHarnessBridge {
|
|
|
789
849
|
requiresMention: body.chattype === 'group',
|
|
790
850
|
}),
|
|
791
851
|
onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
|
|
852
|
+
files: message.files,
|
|
792
853
|
},
|
|
793
854
|
});
|
|
794
855
|
|
|
@@ -862,7 +923,9 @@ export class WecomHarnessBridge {
|
|
|
862
923
|
if (this.#signal?.aborted) return;
|
|
863
924
|
this.#status.lastError = error?.message ?? String(error);
|
|
864
925
|
this.#logger.error?.('[dsh-im:wecom] failed to process an inbound message');
|
|
865
|
-
const errorText =
|
|
926
|
+
const errorText = inboundFileUserMessage(error)
|
|
927
|
+
?? imagePromptUserMessage(error)
|
|
928
|
+
?? '消息处理失败,请稍后重试。';
|
|
866
929
|
try {
|
|
867
930
|
if (streamStarted && streamId) {
|
|
868
931
|
await this.#client.replyStream(frame, streamId, errorText, true);
|
|
@@ -196,6 +196,47 @@ export function extractWeixinImages(message, { fetchImpl = fetch } = {}) {
|
|
|
196
196
|
return images;
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
async function fetchWeixinFileCiphertext(url, { fetchImpl, signal }) {
|
|
200
|
+
const response = await fetchImpl(new URL(url), {
|
|
201
|
+
method: 'GET',
|
|
202
|
+
signal,
|
|
203
|
+
redirect: 'manual',
|
|
204
|
+
});
|
|
205
|
+
if (!response?.ok) {
|
|
206
|
+
await response?.body?.cancel?.().catch?.(() => undefined);
|
|
207
|
+
throw new WeixinApiError(
|
|
208
|
+
'file-download-failed',
|
|
209
|
+
`微信文件下载失败(HTTP ${response?.status ?? 'unknown'})。`,
|
|
210
|
+
{ status: response?.status },
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
return Buffer.from(await response.arrayBuffer());
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Convert native iLink file items into lazily downloaded, decrypted file references. */
|
|
217
|
+
export function extractWeixinFiles(message, { fetchImpl = fetch } = {}) {
|
|
218
|
+
if (typeof fetchImpl !== 'function') throw new TypeError('fetchImpl must be a function');
|
|
219
|
+
const files = [];
|
|
220
|
+
for (const item of message?.item_list ?? []) {
|
|
221
|
+
const fileItem = item?.file_item;
|
|
222
|
+
if (!fileItem || typeof fileItem !== 'object') continue;
|
|
223
|
+
const declaredSize = Number(fileItem.len);
|
|
224
|
+
files.push({
|
|
225
|
+
name: nonEmptyString(fileItem.file_name) ?? (files.length === 0 ? 'file' : `file-${files.length + 1}`),
|
|
226
|
+
...(Number.isFinite(declaredSize) && declaredSize >= 0 ? { size: declaredSize } : {}),
|
|
227
|
+
load: async ({ signal } = {}) => {
|
|
228
|
+
signal?.throwIfAborted();
|
|
229
|
+
const key = parseWeixinImageAesKey(fileItem);
|
|
230
|
+
const url = weixinImageDownloadUrl(fileItem.media);
|
|
231
|
+
const ciphertext = await fetchWeixinFileCiphertext(url, { fetchImpl, signal });
|
|
232
|
+
signal?.throwIfAborted();
|
|
233
|
+
return decryptWeixinImage(ciphertext, key);
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return files;
|
|
238
|
+
}
|
|
239
|
+
|
|
199
240
|
function isWeixinHost(hostname) {
|
|
200
241
|
const normalized = hostname.toLowerCase().replace(/\.$/, '');
|
|
201
242
|
return normalized === 'weixin.qq.com' || normalized.endsWith('.weixin.qq.com');
|
|
@@ -421,6 +462,10 @@ export function createWeixinApi({ fetchImpl = fetch } = {}) {
|
|
|
421
462
|
return extractWeixinImages(message, { fetchImpl });
|
|
422
463
|
},
|
|
423
464
|
|
|
465
|
+
inboundFiles(message) {
|
|
466
|
+
return extractWeixinFiles(message, { fetchImpl });
|
|
467
|
+
},
|
|
468
|
+
|
|
424
469
|
async beginLogin({ localTokens = [], botType = DEFAULT_BOT_TYPE, signal } = {}) {
|
|
425
470
|
const tokens = [...new Set(localTokens.map(nonEmptyString).filter(Boolean))].slice(-10);
|
|
426
471
|
const response = await requestJson(fetchImpl, {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
extractWeixinFiles,
|
|
2
3
|
extractWeixinImages,
|
|
3
4
|
extractWeixinText,
|
|
4
5
|
splitWeixinText,
|
|
@@ -31,6 +32,11 @@ import {
|
|
|
31
32
|
imagePromptUserMessage,
|
|
32
33
|
promptContentForMessage,
|
|
33
34
|
} from '../shared/image-prompt.mjs';
|
|
35
|
+
import {
|
|
36
|
+
hasInboundFiles,
|
|
37
|
+
inboundFileUserMessage,
|
|
38
|
+
prefetchInboundFiles,
|
|
39
|
+
} from '../shared/inbound-file.mjs';
|
|
34
40
|
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
|
35
41
|
import {
|
|
36
42
|
materializeOutboundArtifact,
|
|
@@ -49,7 +55,7 @@ const GENERIC_PROCESSING_ERROR = '消息处理失败,请稍后重试。';
|
|
|
49
55
|
const HELP_TEXT = [
|
|
50
56
|
'微信已连接 DeepSeek Harness。',
|
|
51
57
|
'',
|
|
52
|
-
'
|
|
58
|
+
'直接发送文字、图片、文件或带文字识别结果的语音即可继续当前会话。',
|
|
53
59
|
'/new 开启一个全新会话',
|
|
54
60
|
'/compact 压缩当前会话的较早上下文',
|
|
55
61
|
'/workspace 工作区绝对路径 切换工作区',
|
|
@@ -77,15 +83,33 @@ function nonEmptyString(value) {
|
|
|
77
83
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
78
84
|
}
|
|
79
85
|
|
|
86
|
+
export function weixinInboundMessage(message, api) {
|
|
87
|
+
return {
|
|
88
|
+
content: extractWeixinText(message) ?? '',
|
|
89
|
+
images: typeof api?.inboundImages === 'function'
|
|
90
|
+
? api.inboundImages(message)
|
|
91
|
+
: extractWeixinImages(message),
|
|
92
|
+
files: typeof api?.inboundFiles === 'function'
|
|
93
|
+
? api.inboundFiles(message)
|
|
94
|
+
: extractWeixinFiles(message),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
80
98
|
function hasWeixinImageItems(message) {
|
|
81
99
|
return Array.isArray(message?.item_list)
|
|
82
100
|
&& message.item_list.some((item) => item?.image_item && typeof item.image_item === 'object');
|
|
83
101
|
}
|
|
84
102
|
|
|
103
|
+
function hasWeixinFileItems(message) {
|
|
104
|
+
return Array.isArray(message?.item_list)
|
|
105
|
+
&& message.item_list.some((item) => item?.file_item && typeof item.file_item === 'object');
|
|
106
|
+
}
|
|
107
|
+
|
|
85
108
|
function canClaimInteractionReply(message, pending) {
|
|
86
109
|
return pending.questions[pending.index]
|
|
87
110
|
&& nonEmptyString(message?.from_user_id) === pending.actor
|
|
88
111
|
&& !hasWeixinImageItems(message)
|
|
112
|
+
&& !hasWeixinFileItems(message)
|
|
89
113
|
&& nonEmptyString(extractWeixinText(message));
|
|
90
114
|
}
|
|
91
115
|
|
|
@@ -204,7 +228,7 @@ export class WeixinHarnessBridge {
|
|
|
204
228
|
const runId = nonEmptyString(message?.run_id) ?? undefined;
|
|
205
229
|
const pending = this.#pendingInteractions.get(key);
|
|
206
230
|
const commandText = nonEmptyString(extractWeixinText(message)) ?? '';
|
|
207
|
-
const commandRunner = isControlCommand(commandText)
|
|
231
|
+
const commandRunner = hasWeixinFileItems(message) ? null : isControlCommand(commandText)
|
|
208
232
|
? runControlCommand
|
|
209
233
|
: (isModelCommand(commandText)
|
|
210
234
|
? runModelCommand
|
|
@@ -238,7 +262,9 @@ export class WeixinHarnessBridge {
|
|
|
238
262
|
key,
|
|
239
263
|
actor: sender,
|
|
240
264
|
messageId,
|
|
241
|
-
text: hasWeixinImageItems(message)
|
|
265
|
+
text: hasWeixinImageItems(message) || hasWeixinFileItems(message)
|
|
266
|
+
? ''
|
|
267
|
+
: extractWeixinText(message),
|
|
242
268
|
addressed: true,
|
|
243
269
|
hasPendingQuestion: Boolean(pending),
|
|
244
270
|
questionCompletion: pending?.submitting || pending?.claimedReplyMessageId
|
|
@@ -290,10 +316,16 @@ export class WeixinHarnessBridge {
|
|
|
290
316
|
releaseMessageId = true,
|
|
291
317
|
alreadyRecorded = false,
|
|
292
318
|
} = {}) {
|
|
319
|
+
const preparedMessage = message.from_user_id === this.#ownerUserId
|
|
320
|
+
? prefetchInboundFiles(
|
|
321
|
+
weixinInboundMessage(message, this.#api),
|
|
322
|
+
{ signal: this.#signal },
|
|
323
|
+
)
|
|
324
|
+
: undefined;
|
|
293
325
|
const previous = this.#queues.get(key) ?? Promise.resolve();
|
|
294
326
|
const current = previous
|
|
295
327
|
.catch(() => undefined)
|
|
296
|
-
.then(() => this.#process(message, key, { alreadyRecorded }))
|
|
328
|
+
.then(() => this.#process(message, key, { alreadyRecorded, preparedMessage }))
|
|
297
329
|
.finally(() => {
|
|
298
330
|
if (releaseMessageId) this.#acceptedMessageIds.delete(messageId);
|
|
299
331
|
if (this.#queues.get(key) === current) this.#queues.delete(key);
|
|
@@ -331,6 +363,7 @@ export class WeixinHarnessBridge {
|
|
|
331
363
|
const result = await runner(text, this.#harness, this.#state, key, {
|
|
332
364
|
signal: this.#signal,
|
|
333
365
|
hasImages: hasWeixinImageItems(message),
|
|
366
|
+
hasFiles: hasWeixinFileItems(message),
|
|
334
367
|
pendingInteraction: this.#pendingInteractions.has(key)
|
|
335
368
|
|| this.#approvals.hasPending(key),
|
|
336
369
|
control: { owner: this, key },
|
|
@@ -348,7 +381,7 @@ export class WeixinHarnessBridge {
|
|
|
348
381
|
this.#status.lastMessageError = null;
|
|
349
382
|
}
|
|
350
383
|
|
|
351
|
-
async #process(message, key, { alreadyRecorded = false } = {}) {
|
|
384
|
+
async #process(message, key, { alreadyRecorded = false, preparedMessage } = {}) {
|
|
352
385
|
this.#signal?.throwIfAborted();
|
|
353
386
|
const messageId = weixinMessageId(message);
|
|
354
387
|
const sender = nonEmptyString(message?.from_user_id);
|
|
@@ -366,38 +399,36 @@ export class WeixinHarnessBridge {
|
|
|
366
399
|
|
|
367
400
|
const contextToken = typeof message.context_token === 'string' ? message.context_token : undefined;
|
|
368
401
|
const runId = typeof message.run_id === 'string' ? message.run_id : undefined;
|
|
369
|
-
const text = extractWeixinText(message) ?? '';
|
|
370
402
|
try {
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
: extractWeixinImages(message);
|
|
374
|
-
const promptMessage = { content: text, images };
|
|
403
|
+
const promptMessage = preparedMessage ?? weixinInboundMessage(message, this.#api);
|
|
404
|
+
const text = promptMessage.content;
|
|
375
405
|
const hasImages = hasInboundImages(promptMessage);
|
|
376
|
-
|
|
377
|
-
|
|
406
|
+
const hasFiles = hasInboundFiles(promptMessage);
|
|
407
|
+
if (!text && !hasImages && !hasFiles) {
|
|
408
|
+
await this.#send(sender, '目前支持文字、图片、文件,以及微信已转成文字的语音消息。', contextToken, runId);
|
|
378
409
|
await this.#state.markSeen(messageId);
|
|
379
410
|
return;
|
|
380
411
|
}
|
|
381
412
|
|
|
382
413
|
const command = text.trim().toLowerCase();
|
|
383
|
-
if (!hasImages && command === '/help') {
|
|
414
|
+
if (!hasImages && !hasFiles && command === '/help') {
|
|
384
415
|
await this.#send(sender, HELP_TEXT, contextToken, runId);
|
|
385
416
|
await this.#state.markSeen(messageId);
|
|
386
417
|
return;
|
|
387
418
|
}
|
|
388
|
-
if (!hasImages && command === '/status') {
|
|
419
|
+
if (!hasImages && !hasFiles && command === '/status') {
|
|
389
420
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
390
421
|
await this.#send(sender, '微信与 DeepSeek Harness 连接正常。', contextToken, runId);
|
|
391
422
|
await this.#state.markSeen(messageId);
|
|
392
423
|
return;
|
|
393
424
|
}
|
|
394
|
-
if (!hasImages && command === '/new') {
|
|
425
|
+
if (!hasImages && !hasFiles && command === '/new') {
|
|
395
426
|
await this.#state.clearSession(key);
|
|
396
427
|
await this.#send(sender, '已开启新会话。请发送你的问题。', contextToken, runId);
|
|
397
428
|
await this.#state.markSeen(messageId);
|
|
398
429
|
return;
|
|
399
430
|
}
|
|
400
|
-
const workspaceCommand = hasImages
|
|
431
|
+
const workspaceCommand = hasImages || hasFiles
|
|
401
432
|
? null
|
|
402
433
|
: await runWorkspaceCommand(text, this.#harness, key);
|
|
403
434
|
if (workspaceCommand) {
|
|
@@ -407,7 +438,7 @@ export class WeixinHarnessBridge {
|
|
|
407
438
|
await this.#state.markSeen(messageId);
|
|
408
439
|
return;
|
|
409
440
|
}
|
|
410
|
-
const compactCommand = hasImages
|
|
441
|
+
const compactCommand = hasImages || hasFiles
|
|
411
442
|
? null
|
|
412
443
|
: await runCompactCommand(
|
|
413
444
|
text,
|
|
@@ -446,6 +477,7 @@ export class WeixinHarnessBridge {
|
|
|
446
477
|
runId,
|
|
447
478
|
}),
|
|
448
479
|
onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
|
|
480
|
+
files: promptMessage.files,
|
|
449
481
|
},
|
|
450
482
|
}));
|
|
451
483
|
} finally {
|
|
@@ -490,7 +522,9 @@ export class WeixinHarnessBridge {
|
|
|
490
522
|
}
|
|
491
523
|
if (this.#signal?.aborted) return;
|
|
492
524
|
this.#status.lastError = error?.message ?? String(error);
|
|
493
|
-
const userMessage =
|
|
525
|
+
const userMessage = inboundFileUserMessage(error)
|
|
526
|
+
?? imagePromptUserMessage(error)
|
|
527
|
+
?? GENERIC_PROCESSING_ERROR;
|
|
494
528
|
this.#status.lastMessageError = safeMessageError(error, userMessage);
|
|
495
529
|
this.#logger.error?.('[dsh-weixin] failed to process an inbound message:', error);
|
|
496
530
|
try {
|
|
@@ -170,6 +170,39 @@ function whatsappImageSource(message, content, download, { viewOnce = false } =
|
|
|
170
170
|
};
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
function whatsappFileSource(message, content, download) {
|
|
174
|
+
const media = content?.documentMessage;
|
|
175
|
+
if (!media) return null;
|
|
176
|
+
const mediaType = typeof media.mimetype === 'string' && media.mimetype
|
|
177
|
+
? media.mimetype.toLowerCase() : undefined;
|
|
178
|
+
if (IMAGE_MEDIA_TYPES.has(mediaType)) return null;
|
|
179
|
+
return {
|
|
180
|
+
name: typeof media.fileName === 'string' && media.fileName
|
|
181
|
+
? media.fileName : 'whatsapp-file',
|
|
182
|
+
...(mediaType ? { mediaType } : {}),
|
|
183
|
+
size: mediaSize(media.fileLength),
|
|
184
|
+
async load({ signal } = {}) {
|
|
185
|
+
signal?.throwIfAborted();
|
|
186
|
+
const stream = await download(message, 'stream', { options: { signal } });
|
|
187
|
+
signal?.throwIfAborted();
|
|
188
|
+
return { stream };
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function createWhatsappMediaDownloader({
|
|
194
|
+
socket,
|
|
195
|
+
logger = console,
|
|
196
|
+
download = downloadMediaMessage,
|
|
197
|
+
} = {}) {
|
|
198
|
+
if (typeof download !== 'function') throw new TypeError('WhatsApp media downloader is required');
|
|
199
|
+
if (typeof socket?.updateMediaMessage !== 'function') return download;
|
|
200
|
+
return (message, type, options) => download(message, type, options, {
|
|
201
|
+
logger,
|
|
202
|
+
reuploadRequest: (candidate) => socket.updateMediaMessage(candidate),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
173
206
|
export function normalizeWhatsappMessage(message, accountJid, {
|
|
174
207
|
download = downloadMediaMessage,
|
|
175
208
|
} = {}) {
|
|
@@ -195,6 +228,7 @@ export function normalizeWhatsappMessage(message, accountJid, {
|
|
|
195
228
|
const replyToSelf = typeof context?.participant === 'string'
|
|
196
229
|
&& areJidsSameUser(context.participant, accountJid);
|
|
197
230
|
const image = whatsappImageSource(message, content, download, { viewOnce });
|
|
231
|
+
const file = whatsappFileSource(message, content, download);
|
|
198
232
|
return {
|
|
199
233
|
messageId: `${remoteJid}:${messageId}`,
|
|
200
234
|
providerMessageId: messageId,
|
|
@@ -205,6 +239,7 @@ export function normalizeWhatsappMessage(message, accountJid, {
|
|
|
205
239
|
conversationId: remoteJid,
|
|
206
240
|
content: messageText(content),
|
|
207
241
|
images: image ? [image] : [],
|
|
242
|
+
files: file ? [file] : [],
|
|
208
243
|
addressed: !group || mentioned || replyToSelf,
|
|
209
244
|
selfChat,
|
|
210
245
|
replyTarget: { jid: remoteJid, quoted: message, selfChat },
|
|
@@ -495,8 +530,13 @@ export class WhatsappRuntime {
|
|
|
495
530
|
new Error('WhatsApp linked-device session must be scanned again'),
|
|
496
531
|
{ code: 'relink-required' },
|
|
497
532
|
)),
|
|
498
|
-
onMessage: async (raw) => {
|
|
499
|
-
const message = normalizeWhatsappMessage(raw, this.#config.accountJid
|
|
533
|
+
onMessage: async (raw, context) => {
|
|
534
|
+
const message = normalizeWhatsappMessage(raw, this.#config.accountJid, {
|
|
535
|
+
download: createWhatsappMediaDownloader({
|
|
536
|
+
socket: context?.socket,
|
|
537
|
+
logger: this.#logger,
|
|
538
|
+
}),
|
|
539
|
+
});
|
|
500
540
|
if (!message || outboundIds.has(message.providerMessageId) || !this.#bridge) return;
|
|
501
541
|
this.#status.lastCheckedAt = Date.now();
|
|
502
542
|
if (!whatsappInboundAllowed(message, {
|
|
@@ -190,7 +190,7 @@ export async function createWhatsappWebSession({
|
|
|
190
190
|
const timestamp = messageTimestampMs(message?.messageTimestamp);
|
|
191
191
|
if (timestamp === null || timestamp < sessionStartedAt - APPEND_RECENT_GRACE_MS) continue;
|
|
192
192
|
}
|
|
193
|
-
Promise.resolve(onMessage(message)).catch(() => {
|
|
193
|
+
Promise.resolve(onMessage(message, { socket: nextSocket })).catch(() => {
|
|
194
194
|
logger.error?.('[dsh-im:whatsapp] failed to process an inbound WhatsApp message');
|
|
195
195
|
});
|
|
196
196
|
}
|