@xmanrui/dsh-im 1.1.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/README.en.md +5 -3
- package/README.md +5 -3
- package/lib/client.js +212 -0
- package/lib/index.js +166 -163
- package/package.json +1 -1
- package/plugin-src/client/channels/whatsapp/api.js +11 -0
- package/plugin-src/client/channels/whatsapp/index.js +125 -0
- package/plugin-src/client/channels/whatsapp/styles.js +25 -0
- package/plugin-src/client/i18n.js +18 -0
- 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/channels/whatsapp/rpc.mjs +19 -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/config-store.mjs +43 -0
- package/src/channels/whatsapp/whatsapp-controller.mjs +22 -1
- package/src/channels/whatsapp/whatsapp-runtime.mjs +83 -2
- package/src/channels/whatsapp/whatsapp-web-session.mjs +1 -1
|
@@ -435,6 +435,48 @@ export function createDingtalkApi({
|
|
|
435
435
|
return request;
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
+
async function messageFileDownloadUrl({
|
|
439
|
+
clientId,
|
|
440
|
+
clientSecret,
|
|
441
|
+
robotCode,
|
|
442
|
+
downloadCode,
|
|
443
|
+
signal,
|
|
444
|
+
kind,
|
|
445
|
+
}) {
|
|
446
|
+
const botCode = nonEmptyString(robotCode);
|
|
447
|
+
const fileCode = nonEmptyString(downloadCode);
|
|
448
|
+
if (!botCode || !fileCode) throw new TypeError('robotCode and downloadCode are required');
|
|
449
|
+
const token = await accessToken({ clientId, clientSecret, signal });
|
|
450
|
+
let response;
|
|
451
|
+
try {
|
|
452
|
+
response = await requestJson(
|
|
453
|
+
fetchImpl,
|
|
454
|
+
endpoint(apiBase, 'v1.0/robot/messageFiles/download'),
|
|
455
|
+
{
|
|
456
|
+
body: { downloadCode: fileCode, robotCode: botCode },
|
|
457
|
+
headers: { 'x-acs-dingtalk-access-token': token },
|
|
458
|
+
signal,
|
|
459
|
+
action: `${kind === 'image' ? '图片' : '文件'}下载地址`,
|
|
460
|
+
},
|
|
461
|
+
);
|
|
462
|
+
} catch (error) {
|
|
463
|
+
if (signal?.aborted) throw error;
|
|
464
|
+
throw new DingtalkApiError(
|
|
465
|
+
`${kind}-download-address-failed`,
|
|
466
|
+
`钉钉${kind === 'image' ? '图片' : '文件'}下载地址获取失败。`,
|
|
467
|
+
{ cause: error, status: error?.status, providerCode: error?.providerCode },
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
const downloadUrl = nonEmptyString(response?.downloadUrl ?? response?.download_url);
|
|
471
|
+
if (!downloadUrl) {
|
|
472
|
+
throw new DingtalkApiError(
|
|
473
|
+
`invalid-${kind}-download`,
|
|
474
|
+
`钉钉服务没有返回${kind === 'image' ? '图片' : '文件'}下载地址。`,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
return secureDingtalkDownloadUrl(downloadUrl);
|
|
478
|
+
}
|
|
479
|
+
|
|
438
480
|
function acquireCardRequestSlot(signal) {
|
|
439
481
|
const acquire = async () => {
|
|
440
482
|
const waitMs = Math.max(0, nextCardRequestAt - now());
|
|
@@ -568,45 +610,69 @@ export function createDingtalkApi({
|
|
|
568
610
|
signal,
|
|
569
611
|
maxBytes,
|
|
570
612
|
}) {
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
613
|
+
const downloadUrl = await messageFileDownloadUrl({
|
|
614
|
+
clientId,
|
|
615
|
+
clientSecret,
|
|
616
|
+
robotCode,
|
|
617
|
+
downloadCode,
|
|
618
|
+
signal,
|
|
619
|
+
kind: 'image',
|
|
620
|
+
});
|
|
576
621
|
try {
|
|
577
|
-
|
|
622
|
+
return await fetchImageBuffer(downloadUrl, {
|
|
578
623
|
fetchImpl,
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
headers: { 'x-acs-dingtalk-access-token': token },
|
|
583
|
-
signal,
|
|
584
|
-
action: '图片下载地址',
|
|
585
|
-
},
|
|
586
|
-
);
|
|
624
|
+
signal,
|
|
625
|
+
maxBytes,
|
|
626
|
+
});
|
|
587
627
|
} catch (error) {
|
|
588
|
-
if (signal?.aborted) throw error;
|
|
628
|
+
if (signal?.aborted || error instanceof ImagePromptError) throw error;
|
|
589
629
|
throw new DingtalkApiError(
|
|
590
|
-
'image-download-
|
|
591
|
-
'
|
|
592
|
-
{ cause: error
|
|
630
|
+
'image-content-download-failed',
|
|
631
|
+
'钉钉图片内容下载失败。',
|
|
632
|
+
{ cause: error },
|
|
593
633
|
);
|
|
594
634
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
635
|
+
},
|
|
636
|
+
|
|
637
|
+
async downloadFile({
|
|
638
|
+
clientId,
|
|
639
|
+
clientSecret,
|
|
640
|
+
robotCode,
|
|
641
|
+
downloadCode,
|
|
642
|
+
signal,
|
|
643
|
+
}) {
|
|
644
|
+
const downloadUrl = await messageFileDownloadUrl({
|
|
645
|
+
clientId,
|
|
646
|
+
clientSecret,
|
|
647
|
+
robotCode,
|
|
648
|
+
downloadCode,
|
|
649
|
+
signal,
|
|
650
|
+
kind: 'file',
|
|
651
|
+
});
|
|
652
|
+
if (downloadUrl.protocol !== 'https:') {
|
|
653
|
+
throw new DingtalkApiError('invalid-file-download', '钉钉服务返回了无效的文件下载地址。');
|
|
598
654
|
}
|
|
599
655
|
try {
|
|
600
|
-
|
|
601
|
-
|
|
656
|
+
const response = await fetchImpl(downloadUrl, {
|
|
657
|
+
method: 'GET',
|
|
658
|
+
redirect: 'follow',
|
|
602
659
|
signal,
|
|
603
|
-
maxBytes,
|
|
604
660
|
});
|
|
661
|
+
if (!response?.ok) {
|
|
662
|
+
await response?.body?.cancel?.().catch?.(() => undefined);
|
|
663
|
+
throw new DingtalkApiError(
|
|
664
|
+
'file-content-download-failed',
|
|
665
|
+
`钉钉文件内容下载失败(HTTP ${response?.status ?? 'unknown'})。`,
|
|
666
|
+
{ status: response?.status },
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
return Buffer.from(await response.arrayBuffer());
|
|
605
670
|
} catch (error) {
|
|
606
|
-
if (signal?.aborted
|
|
671
|
+
if (signal?.aborted) throw abortError(signal);
|
|
672
|
+
if (error instanceof DingtalkApiError) throw error;
|
|
607
673
|
throw new DingtalkApiError(
|
|
608
|
-
'
|
|
609
|
-
'
|
|
674
|
+
'file-content-download-failed',
|
|
675
|
+
'钉钉文件内容下载失败。',
|
|
610
676
|
{ cause: error },
|
|
611
677
|
);
|
|
612
678
|
}
|
|
@@ -29,6 +29,11 @@ import {
|
|
|
29
29
|
imagePromptUserMessage,
|
|
30
30
|
promptContentForMessage,
|
|
31
31
|
} from '../shared/image-prompt.mjs';
|
|
32
|
+
import {
|
|
33
|
+
hasInboundFiles,
|
|
34
|
+
inboundFileUserMessage,
|
|
35
|
+
prefetchInboundFiles,
|
|
36
|
+
} from '../shared/inbound-file.mjs';
|
|
32
37
|
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
|
33
38
|
import {
|
|
34
39
|
materializeOutboundArtifact,
|
|
@@ -48,7 +53,7 @@ const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无
|
|
|
48
53
|
const HELP_TEXT = [
|
|
49
54
|
'钉钉机器人已连接 DeepSeek Harness。',
|
|
50
55
|
'',
|
|
51
|
-
'
|
|
56
|
+
'直接发送文字、图片或文件即可继续当前会话。',
|
|
52
57
|
'/new 开启一个全新会话',
|
|
53
58
|
'/compact 压缩当前会话的较早上下文',
|
|
54
59
|
'/workspace 工作区绝对路径 切换工作区',
|
|
@@ -175,6 +180,7 @@ export function dingtalkInboundMessage(message, {
|
|
|
175
180
|
if (code) imageCodes.push(code);
|
|
176
181
|
}
|
|
177
182
|
}
|
|
183
|
+
const fileCode = msgtype === 'file' ? downloadCodeFor(content) : null;
|
|
178
184
|
return {
|
|
179
185
|
content: text,
|
|
180
186
|
images: imageCodes.map((downloadCode, index) => ({
|
|
@@ -193,6 +199,21 @@ export function dingtalkInboundMessage(message, {
|
|
|
193
199
|
});
|
|
194
200
|
},
|
|
195
201
|
})),
|
|
202
|
+
files: fileCode ? [{
|
|
203
|
+
name: nonEmptyString(content?.fileName ?? content?.file_name) ?? 'file',
|
|
204
|
+
load: ({ signal } = {}) => {
|
|
205
|
+
if (typeof api?.downloadFile !== 'function') {
|
|
206
|
+
throw new Error('DingTalk API does not support file downloads');
|
|
207
|
+
}
|
|
208
|
+
return api.downloadFile({
|
|
209
|
+
clientId,
|
|
210
|
+
clientSecret,
|
|
211
|
+
robotCode: message?.robotCode,
|
|
212
|
+
downloadCode: fileCode,
|
|
213
|
+
signal,
|
|
214
|
+
});
|
|
215
|
+
},
|
|
216
|
+
}] : [],
|
|
196
217
|
};
|
|
197
218
|
}
|
|
198
219
|
|
|
@@ -393,7 +414,7 @@ export class DingtalkHarnessBridge {
|
|
|
393
414
|
clientSecret: this.#clientSecret,
|
|
394
415
|
});
|
|
395
416
|
const commandText = nonEmptyString(promptMessage.content) ?? '';
|
|
396
|
-
const commandRunner = isControlCommand(commandText)
|
|
417
|
+
const commandRunner = hasInboundFiles(promptMessage) ? null : isControlCommand(commandText)
|
|
397
418
|
? runControlCommand
|
|
398
419
|
: (isModelCommand(commandText)
|
|
399
420
|
? runModelCommand
|
|
@@ -499,10 +520,28 @@ export class DingtalkHarnessBridge {
|
|
|
499
520
|
releaseMessageId = true,
|
|
500
521
|
alreadyRecorded = false,
|
|
501
522
|
} = {}) {
|
|
523
|
+
let hasSafeReplyRoute = false;
|
|
524
|
+
try {
|
|
525
|
+
normalizeDingtalkSessionWebhook(message.sessionWebhook);
|
|
526
|
+
hasSafeReplyRoute = true;
|
|
527
|
+
} catch {
|
|
528
|
+
// Keep the existing rejection path without downloading an unusable file.
|
|
529
|
+
}
|
|
530
|
+
const addressed = String(message.conversationType) !== '2' || message.isInAtList === true;
|
|
531
|
+
const preparedMessage = hasSafeReplyRoute && addressed
|
|
532
|
+
? prefetchInboundFiles(dingtalkInboundMessage(message, {
|
|
533
|
+
api: this.#api,
|
|
534
|
+
clientId: this.#clientId,
|
|
535
|
+
clientSecret: this.#clientSecret,
|
|
536
|
+
}), { signal: this.#signal })
|
|
537
|
+
: undefined;
|
|
502
538
|
const previous = this.#queues.get(key) ?? Promise.resolve();
|
|
503
539
|
const current = previous
|
|
504
540
|
.catch(() => undefined)
|
|
505
|
-
.then(() => this.#process(message, messageId, sender, key, {
|
|
541
|
+
.then(() => this.#process(message, messageId, sender, key, {
|
|
542
|
+
alreadyRecorded,
|
|
543
|
+
preparedMessage,
|
|
544
|
+
}))
|
|
506
545
|
.finally(() => {
|
|
507
546
|
if (releaseMessageId) this.#acceptedMessageIds.delete(messageId);
|
|
508
547
|
if (this.#queues.get(key) === current) this.#queues.delete(key);
|
|
@@ -536,6 +575,7 @@ export class DingtalkHarnessBridge {
|
|
|
536
575
|
{
|
|
537
576
|
signal: this.#signal,
|
|
538
577
|
hasImages: hasInboundImages(prompt),
|
|
578
|
+
hasFiles: hasInboundFiles(prompt),
|
|
539
579
|
pendingInteraction: this.#pendingInteractions.has(key)
|
|
540
580
|
|| this.#approvals.hasPending(key),
|
|
541
581
|
control: { owner: this, key },
|
|
@@ -553,7 +593,10 @@ export class DingtalkHarnessBridge {
|
|
|
553
593
|
this.#status.lastError = null;
|
|
554
594
|
}
|
|
555
595
|
|
|
556
|
-
async #process(message, messageId, sender, key, {
|
|
596
|
+
async #process(message, messageId, sender, key, {
|
|
597
|
+
alreadyRecorded = false,
|
|
598
|
+
preparedMessage,
|
|
599
|
+
} = {}) {
|
|
557
600
|
this.#signal?.throwIfAborted();
|
|
558
601
|
if (!alreadyRecorded) {
|
|
559
602
|
if (this.#state.hasSeen(messageId)) return;
|
|
@@ -577,38 +620,39 @@ export class DingtalkHarnessBridge {
|
|
|
577
620
|
return;
|
|
578
621
|
}
|
|
579
622
|
|
|
580
|
-
const promptMessage = dingtalkInboundMessage(message, {
|
|
623
|
+
const promptMessage = preparedMessage ?? dingtalkInboundMessage(message, {
|
|
581
624
|
api: this.#api,
|
|
582
625
|
clientId: this.#clientId,
|
|
583
626
|
clientSecret: this.#clientSecret,
|
|
584
627
|
});
|
|
585
628
|
const text = promptMessage.content;
|
|
586
629
|
const hasImages = hasInboundImages(promptMessage);
|
|
630
|
+
const hasFiles = hasInboundFiles(promptMessage);
|
|
587
631
|
const isPlainText = String(message?.msgtype).toLowerCase() === 'text';
|
|
588
632
|
let cardStream = null;
|
|
589
633
|
let cardStarted = false;
|
|
590
634
|
try {
|
|
591
|
-
if (!text && !hasImages) {
|
|
592
|
-
await this.#send(sessionWebhook, '
|
|
635
|
+
if (!text && !hasImages && !hasFiles) {
|
|
636
|
+
await this.#send(sessionWebhook, '目前支持文字、图片和文件消息。');
|
|
593
637
|
return;
|
|
594
638
|
}
|
|
595
639
|
|
|
596
640
|
const command = text.toLowerCase();
|
|
597
|
-
if (isPlainText && !hasImages && command === '/help') {
|
|
641
|
+
if (isPlainText && !hasImages && !hasFiles && command === '/help') {
|
|
598
642
|
await this.#send(sessionWebhook, HELP_TEXT);
|
|
599
643
|
return;
|
|
600
644
|
}
|
|
601
|
-
if (isPlainText && !hasImages && command === '/status') {
|
|
645
|
+
if (isPlainText && !hasImages && !hasFiles && command === '/status') {
|
|
602
646
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
603
647
|
await this.#send(sessionWebhook, '钉钉机器人与 DeepSeek Harness 连接正常。');
|
|
604
648
|
return;
|
|
605
649
|
}
|
|
606
|
-
if (isPlainText && !hasImages && command === '/new') {
|
|
650
|
+
if (isPlainText && !hasImages && !hasFiles && command === '/new') {
|
|
607
651
|
await this.#state.clearSession(key);
|
|
608
652
|
await this.#send(sessionWebhook, '已开启新会话。请发送你的问题。');
|
|
609
653
|
return;
|
|
610
654
|
}
|
|
611
|
-
const workspaceCommand = isPlainText && !hasImages
|
|
655
|
+
const workspaceCommand = isPlainText && !hasImages && !hasFiles
|
|
612
656
|
? await runWorkspaceCommand(text, this.#harness, key)
|
|
613
657
|
: null;
|
|
614
658
|
if (workspaceCommand) {
|
|
@@ -617,7 +661,7 @@ export class DingtalkHarnessBridge {
|
|
|
617
661
|
}
|
|
618
662
|
return;
|
|
619
663
|
}
|
|
620
|
-
const compactCommand = isPlainText && !hasImages
|
|
664
|
+
const compactCommand = isPlainText && !hasImages && !hasFiles
|
|
621
665
|
? await runCompactCommand(
|
|
622
666
|
text,
|
|
623
667
|
this.#harness,
|
|
@@ -668,6 +712,7 @@ export class DingtalkHarnessBridge {
|
|
|
668
712
|
requiresMention: String(message.conversationType) === '2',
|
|
669
713
|
}),
|
|
670
714
|
onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
|
|
715
|
+
files: promptMessage.files,
|
|
671
716
|
},
|
|
672
717
|
});
|
|
673
718
|
const answerText = typeof answer === 'string' && answer.trim()
|
|
@@ -717,7 +762,9 @@ export class DingtalkHarnessBridge {
|
|
|
717
762
|
safeErrorDiagnostic(error),
|
|
718
763
|
);
|
|
719
764
|
try {
|
|
720
|
-
const errorText =
|
|
765
|
+
const errorText = inboundFileUserMessage(error)
|
|
766
|
+
?? dingtalkImageErrorUserMessage(error)
|
|
767
|
+
?? CARD_ERROR_TEXT;
|
|
721
768
|
const streamed = cardStarted && await cardStream.finish(errorText);
|
|
722
769
|
if (!streamed) await this.#send(sessionWebhook, errorText);
|
|
723
770
|
} catch {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createEditableMessageStream, splitMessageText } from '../shared/editable-message-stream.mjs';
|
|
2
|
+
import { fetchFileStream } from '../shared/file-download.mjs';
|
|
2
3
|
import { fetchImageBuffer } from '../shared/image-prompt.mjs';
|
|
3
4
|
import { DiscordApi } from './discord-api.mjs';
|
|
4
5
|
import { createDiscordBridgeStatus, DiscordHarnessBridge } from './discord-bridge.mjs';
|
|
@@ -88,6 +89,25 @@ function discordImageSource(attachment, fetchImpl) {
|
|
|
88
89
|
};
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
function discordFileSource(attachment, fetchImpl) {
|
|
93
|
+
if (attachmentMediaType(attachment) || typeof attachment?.url !== 'string' || !attachment.url) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const mediaType = typeof attachment.content_type === 'string' && attachment.content_type
|
|
97
|
+
? attachment.content_type.split(';', 1)[0].trim().toLowerCase() : undefined;
|
|
98
|
+
return {
|
|
99
|
+
name: typeof attachment.filename === 'string' && attachment.filename
|
|
100
|
+
? attachment.filename : String(attachment.id ?? 'discord-file'),
|
|
101
|
+
...(mediaType ? { mediaType } : {}),
|
|
102
|
+
size: attachmentSize(attachment.size),
|
|
103
|
+
load: ({ signal } = {}) => fetchFileStream(attachment.url, {
|
|
104
|
+
fetchImpl,
|
|
105
|
+
signal,
|
|
106
|
+
allowedHosts: DISCORD_IMAGE_HOSTS,
|
|
107
|
+
}),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
91
111
|
export function normalizeDiscordMessage(message, botId, { fetchImpl = fetch } = {}) {
|
|
92
112
|
if (!message?.id || !message?.channel_id || !message?.author?.id) return null;
|
|
93
113
|
const direct = !message.guild_id;
|
|
@@ -103,6 +123,9 @@ export function normalizeDiscordMessage(message, botId, { fetchImpl = fetch } =
|
|
|
103
123
|
images: Array.isArray(message.attachments)
|
|
104
124
|
? message.attachments.map((attachment) => discordImageSource(attachment, fetchImpl)).filter(Boolean)
|
|
105
125
|
: [],
|
|
126
|
+
files: Array.isArray(message.attachments)
|
|
127
|
+
? message.attachments.map((attachment) => discordFileSource(attachment, fetchImpl)).filter(Boolean)
|
|
128
|
+
: [],
|
|
106
129
|
addressed,
|
|
107
130
|
replyTarget: {
|
|
108
131
|
channelId: String(message.channel_id),
|
|
@@ -12,6 +12,10 @@ import {
|
|
|
12
12
|
imagePromptUserMessage,
|
|
13
13
|
promptContentForMessage,
|
|
14
14
|
} from '../shared/image-prompt.mjs';
|
|
15
|
+
import {
|
|
16
|
+
hasInboundFiles,
|
|
17
|
+
inboundFileUserMessage,
|
|
18
|
+
} from '../shared/inbound-file.mjs';
|
|
15
19
|
import {
|
|
16
20
|
harnessAnswerForQuestion,
|
|
17
21
|
harnessQuestionText,
|
|
@@ -91,7 +95,7 @@ const REPAIR_URL_HOSTS = new Set([
|
|
|
91
95
|
const HELP_TEXT = [
|
|
92
96
|
'北汇星河 AIOS 已连接 DeepSeek Harness。',
|
|
93
97
|
'',
|
|
94
|
-
'
|
|
98
|
+
'直接发送文字、图片或文件即可继续当前会话。',
|
|
95
99
|
'/new 开启一个全新会话',
|
|
96
100
|
'/compact 压缩当前会话的较早上下文',
|
|
97
101
|
'/workspace 工作区绝对路径 切换工作区',
|
|
@@ -431,7 +435,7 @@ export class FeishuHarnessBridge {
|
|
|
431
435
|
const processingReaction = this.#addReaction(messageId, 'OnIt');
|
|
432
436
|
const commandMessage = extractInboundMessage(event, this.#client);
|
|
433
437
|
const commandText = nonEmptyString(commandMessage.content) ?? '';
|
|
434
|
-
const commandRunner = isControlCommand(commandText)
|
|
438
|
+
const commandRunner = hasInboundFiles(commandMessage) ? null : isControlCommand(commandText)
|
|
435
439
|
? runControlCommand
|
|
436
440
|
: (isModelCommand(commandText)
|
|
437
441
|
? runModelCommand
|
|
@@ -610,7 +614,8 @@ export class FeishuHarnessBridge {
|
|
|
610
614
|
await this.#finishReaction(messageId, processingReaction, 'ERROR');
|
|
611
615
|
await this.#send(
|
|
612
616
|
event.message.chat_id,
|
|
613
|
-
|
|
617
|
+
inboundFileUserMessage(error)
|
|
618
|
+
?? imagePromptUserMessage(error)
|
|
614
619
|
?? '处理失败,请稍后重试。如果问题持续,请在 DeepSeek Harness 的飞书插件页面检查连接状态。',
|
|
615
620
|
).catch(() => undefined);
|
|
616
621
|
}
|
|
@@ -641,6 +646,7 @@ export class FeishuHarnessBridge {
|
|
|
641
646
|
{
|
|
642
647
|
signal: this.#signal,
|
|
643
648
|
hasImages: hasInboundImages(message),
|
|
649
|
+
hasFiles: hasInboundFiles(message),
|
|
644
650
|
pendingInteraction: this.#pendingInteractions.has(key)
|
|
645
651
|
|| this.#approvals.hasPending(key),
|
|
646
652
|
control: { owner: this, key },
|
|
@@ -671,9 +677,10 @@ export class FeishuHarnessBridge {
|
|
|
671
677
|
const message = extractInboundMessage(event, this.#client);
|
|
672
678
|
const text = message.content;
|
|
673
679
|
const hasImages = hasInboundImages(message);
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
680
|
+
const hasFiles = hasInboundFiles(message);
|
|
681
|
+
const commandText = event.message.message_type === 'text' && !hasImages && !hasFiles ? text : null;
|
|
682
|
+
if (!text && !hasImages && !hasFiles) {
|
|
683
|
+
await this.#send(event.message.chat_id, '目前支持文字、图片和文件消息。');
|
|
677
684
|
return;
|
|
678
685
|
}
|
|
679
686
|
|
|
@@ -1603,7 +1610,7 @@ export class FeishuHarnessBridge {
|
|
|
1603
1610
|
}
|
|
1604
1611
|
}
|
|
1605
1612
|
|
|
1606
|
-
#interactionAskOptions(event, key) {
|
|
1613
|
+
#interactionAskOptions(event, key, files) {
|
|
1607
1614
|
return {
|
|
1608
1615
|
timeoutMs: this.#replyTimeoutMs,
|
|
1609
1616
|
signal: this.#signal,
|
|
@@ -1615,6 +1622,7 @@ export class FeishuHarnessBridge {
|
|
|
1615
1622
|
requiresMention: event.message.chat_type !== 'p2p',
|
|
1616
1623
|
}),
|
|
1617
1624
|
onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
|
|
1625
|
+
files,
|
|
1618
1626
|
};
|
|
1619
1627
|
}
|
|
1620
1628
|
|
|
@@ -1709,7 +1717,7 @@ export class FeishuHarnessBridge {
|
|
|
1709
1717
|
content,
|
|
1710
1718
|
createOptions: { signal: this.#signal },
|
|
1711
1719
|
existsOptions: { signal: this.#signal },
|
|
1712
|
-
askOptions: this.#interactionAskOptions(event, key),
|
|
1720
|
+
askOptions: this.#interactionAskOptions(event, key, message.files),
|
|
1713
1721
|
});
|
|
1714
1722
|
let textReceipt;
|
|
1715
1723
|
let textSendError = null;
|
|
@@ -1749,7 +1757,7 @@ export class FeishuHarnessBridge {
|
|
|
1749
1757
|
markdown: async (controller) => {
|
|
1750
1758
|
promptStarted = true;
|
|
1751
1759
|
const askOptions = {
|
|
1752
|
-
...this.#interactionAskOptions(event, key),
|
|
1760
|
+
...this.#interactionAskOptions(event, key, message.files),
|
|
1753
1761
|
onUpdate: async (update) => {
|
|
1754
1762
|
await controller.setContent(this.#progressText(update));
|
|
1755
1763
|
this.#status.streamUpdates = (this.#status.streamUpdates ?? 0) + 1;
|
|
@@ -1821,7 +1829,7 @@ export class FeishuHarnessBridge {
|
|
|
1821
1829
|
content,
|
|
1822
1830
|
createOptions: { signal: this.#signal },
|
|
1823
1831
|
existsOptions: { signal: this.#signal },
|
|
1824
|
-
askOptions: this.#interactionAskOptions(event, key),
|
|
1832
|
+
askOptions: this.#interactionAskOptions(event, key, message.files),
|
|
1825
1833
|
});
|
|
1826
1834
|
let textReceipt;
|
|
1827
1835
|
let textSendError = null;
|
|
@@ -125,6 +125,31 @@ async function readBoundedStream(stream, { signal, maxBytes }) {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
async function readStream(stream, { signal }) {
|
|
129
|
+
if (!stream || typeof stream[Symbol.asyncIterator] !== 'function') {
|
|
130
|
+
throw new Error('Feishu file download returned no readable stream');
|
|
131
|
+
}
|
|
132
|
+
signal?.throwIfAborted();
|
|
133
|
+
const abort = () => stream.destroy?.(
|
|
134
|
+
signal.reason ?? new DOMException('Feishu file download aborted', 'AbortError'),
|
|
135
|
+
);
|
|
136
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
137
|
+
const chunks = [];
|
|
138
|
+
let size = 0;
|
|
139
|
+
try {
|
|
140
|
+
for await (const chunk of stream) {
|
|
141
|
+
signal?.throwIfAborted();
|
|
142
|
+
const data = Buffer.from(chunk);
|
|
143
|
+
size += data.length;
|
|
144
|
+
chunks.push(data);
|
|
145
|
+
}
|
|
146
|
+
signal?.throwIfAborted();
|
|
147
|
+
return Buffer.concat(chunks, size);
|
|
148
|
+
} finally {
|
|
149
|
+
signal?.removeEventListener('abort', abort);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
128
153
|
function providerCode(value) {
|
|
129
154
|
if (!value || typeof value !== 'object') return null;
|
|
130
155
|
const code = value.code ?? value.error?.code;
|
|
@@ -232,6 +257,26 @@ function feishuImageSource(event, client, key) {
|
|
|
232
257
|
};
|
|
233
258
|
}
|
|
234
259
|
|
|
260
|
+
function feishuFileSource(event, client, file) {
|
|
261
|
+
const key = nonEmptyString(file?.file_key);
|
|
262
|
+
if (!key) return null;
|
|
263
|
+
return {
|
|
264
|
+
name: nonEmptyString(file?.file_name) ?? 'file',
|
|
265
|
+
async load({ signal } = {}) {
|
|
266
|
+
signal?.throwIfAborted();
|
|
267
|
+
const resource = await client?.im?.v1?.messageResource?.get?.({
|
|
268
|
+
path: {
|
|
269
|
+
message_id: event.message.message_id,
|
|
270
|
+
file_key: key,
|
|
271
|
+
},
|
|
272
|
+
params: { type: 'file' },
|
|
273
|
+
});
|
|
274
|
+
signal?.throwIfAborted();
|
|
275
|
+
return readStream(resource?.getReadableStream?.(), { signal });
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
235
280
|
export function extractInboundMessage(event, client) {
|
|
236
281
|
const messageType = event?.message?.message_type;
|
|
237
282
|
const parsed = parsedMessageContent(event);
|
|
@@ -240,9 +285,11 @@ export function extractInboundMessage(event, client) {
|
|
|
240
285
|
? nonEmptyString(parsed?.image_key)
|
|
241
286
|
: null;
|
|
242
287
|
const imageKeys = standaloneImageKey ? [standaloneImageKey] : post?.imageKeys ?? [];
|
|
288
|
+
const file = messageType === 'file' ? feishuFileSource(event, client, parsed) : null;
|
|
243
289
|
return {
|
|
244
290
|
content: messageType === 'text' ? extractText(event) ?? '' : post?.text ?? '',
|
|
245
291
|
images: imageKeys.map((key) => feishuImageSource(event, client, key)),
|
|
292
|
+
files: file ? [file] : [],
|
|
246
293
|
};
|
|
247
294
|
}
|
|
248
295
|
|