@xmanrui/dsh-im 2.2.0 → 2.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 +6 -2
- package/README.md +6 -2
- package/lib/client.js +2 -2
- package/lib/index.js +217 -189
- package/package.json +1 -1
- package/plugin-src/client/channels/whatsapp/index.js +1 -1
- package/plugin-src/client/i18n.js +1 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +95 -3
- package/src/channels/discord/discord-runtime.mjs +3 -0
- package/src/channels/feishu/bridge.mjs +127 -4
- package/src/channels/feishu/feishu-cards.mjs +8 -0
- package/src/channels/qq/qq-bridge.mjs +98 -8
- package/src/channels/shared/batch-input.mjs +209 -0
- package/src/channels/shared/i18n-en/feishu.mjs +3 -2
- package/src/channels/shared/i18n-en/shared-c.mjs +49 -0
- package/src/channels/shared/text-harness-bridge.mjs +111 -2
- package/src/channels/slack/slack-runtime.mjs +1 -0
- package/src/channels/telegram/telegram-runtime.mjs +4 -0
- package/src/channels/wecom/wecom-bridge.mjs +101 -4
- package/src/channels/weixin/weixin-bridge.mjs +100 -3
- package/src/channels/weixin/weixin-runtime.mjs +18 -1
- package/src/channels/whatsapp/whatsapp-runtime.mjs +29 -6
package/package.json
CHANGED
|
@@ -99,7 +99,7 @@ export function WhatsappAccessSettings({ account, busy = false, onSave }) {
|
|
|
99
99
|
h('span', null, '响应自聊和白名单联系人的私聊,忽略群聊。')),
|
|
100
100
|
h('span', { className: 'dwa-accessTooltipItem' },
|
|
101
101
|
h('strong', null, '开放响应模式'),
|
|
102
|
-
h('span', null, '
|
|
102
|
+
h('span', null, '响应所有私聊、已绑定账号自己发出的群聊消息,以及其他群成员的提及或回复。')))))),
|
|
103
103
|
h('label', { className: 'dwa-accessField' },
|
|
104
104
|
h('span', null, '模式'),
|
|
105
105
|
h('select', {
|
|
@@ -373,7 +373,7 @@ const EN = Object.freeze({
|
|
|
373
373
|
'已生效:': 'Active: ',
|
|
374
374
|
'只响应已绑定 WhatsApp 账号的自聊消息。': 'Only respond to self-chat messages from the linked WhatsApp account.',
|
|
375
375
|
'响应自聊和白名单联系人的私聊,忽略群聊。': 'Respond to self-chat and allowlisted direct messages; ignore group messages.',
|
|
376
|
-
'
|
|
376
|
+
'响应所有私聊、已绑定账号自己发出的群聊消息,以及其他群成员的提及或回复。': 'Respond to all direct messages, group messages sent by the linked account, and mentions or replies from other group members.',
|
|
377
377
|
'允许私聊的 WhatsApp 电话号码': 'WhatsApp phone numbers allowed to send direct messages',
|
|
378
378
|
'每行一个含国家或地区代码的号码': 'One number with country or region code per line',
|
|
379
379
|
'可以包含开头的 +,保存时会自动移除。': 'A leading + is allowed and removed when saved.',
|
|
@@ -24,6 +24,12 @@ import {
|
|
|
24
24
|
} from '../shared/preset-command.mjs';
|
|
25
25
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
26
26
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
27
|
+
import {
|
|
28
|
+
BatchInputManager,
|
|
29
|
+
batchInputBusyMessage,
|
|
30
|
+
batchInputGroupUnsupportedMessage,
|
|
31
|
+
isBatchInputCommand,
|
|
32
|
+
} from '../shared/batch-input.mjs';
|
|
27
33
|
import {
|
|
28
34
|
hasInboundImages,
|
|
29
35
|
imagePromptUserMessage,
|
|
@@ -67,6 +73,9 @@ const HELP_TEXT_LINES = [
|
|
|
67
73
|
'/preset --default 跟随 Host 默认',
|
|
68
74
|
'/stop 停止当前任务',
|
|
69
75
|
'/steer 补充指令 纠偏当前任务',
|
|
76
|
+
'/batch 开始批量输入(仅私聊,最多 10 条文字)',
|
|
77
|
+
'/send 提交当前批次',
|
|
78
|
+
'/cancel 取消当前批次',
|
|
70
79
|
'/status 检查连接状态',
|
|
71
80
|
'/help 显示本帮助',
|
|
72
81
|
];
|
|
@@ -344,6 +353,7 @@ export class DingtalkHarnessBridge {
|
|
|
344
353
|
#commandTasks = new Set();
|
|
345
354
|
#acceptedMessageIds = new Set();
|
|
346
355
|
#approvals;
|
|
356
|
+
#batchInputs = new BatchInputManager();
|
|
347
357
|
|
|
348
358
|
constructor({
|
|
349
359
|
api,
|
|
@@ -416,12 +426,52 @@ export class DingtalkHarnessBridge {
|
|
|
416
426
|
clientSecret: this.#clientSecret,
|
|
417
427
|
});
|
|
418
428
|
const commandText = nonEmptyString(promptMessage.content) ?? '';
|
|
429
|
+
const addressed = String(message.conversationType) !== '2' || message?.isInAtList === true;
|
|
430
|
+
const direct = String(message.conversationType) !== '2';
|
|
431
|
+
const batchCommand = String(message?.msgtype).toLowerCase() === 'text'
|
|
432
|
+
&& isBatchInputCommand(commandText);
|
|
433
|
+
const batchStatus = this.#batchInputs.status(key);
|
|
434
|
+
if (batchCommand && !direct && sessionWebhook && addressed) {
|
|
435
|
+
return this.#finishBatchResult(
|
|
436
|
+
messageId,
|
|
437
|
+
sessionWebhook,
|
|
438
|
+
{ message: batchInputGroupUnsupportedMessage() },
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
if (direct && sessionWebhook && (batchCommand || batchStatus.phase === 'collecting')) {
|
|
442
|
+
const exactBatchStart = /^\/batch$/iu.test(commandText);
|
|
443
|
+
const result = exactBatchStart
|
|
444
|
+
&& batchStatus.phase === 'idle'
|
|
445
|
+
&& (this.#queues.has(key) || pending || this.#approvals.hasPending(key))
|
|
446
|
+
? { handled: true, kind: 'busy', message: batchInputBusyMessage() }
|
|
447
|
+
: this.#batchInputs.handle(key, commandText, {
|
|
448
|
+
plainText: Boolean(commandText)
|
|
449
|
+
&& String(message?.msgtype).toLowerCase() === 'text'
|
|
450
|
+
&& !hasInboundFiles(promptMessage)
|
|
451
|
+
&& !hasInboundImages(promptMessage),
|
|
452
|
+
});
|
|
453
|
+
if (result.handled) {
|
|
454
|
+
if (result.kind === 'submit') {
|
|
455
|
+
return this.#enqueueMessage(
|
|
456
|
+
{
|
|
457
|
+
...message,
|
|
458
|
+
msgtype: 'text',
|
|
459
|
+
text: { content: result.prompt },
|
|
460
|
+
},
|
|
461
|
+
messageId,
|
|
462
|
+
sender,
|
|
463
|
+
key,
|
|
464
|
+
{ batchSubmission: result },
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
return this.#finishBatchResult(messageId, sessionWebhook, result);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
419
470
|
const commandRunner = hasInboundFiles(promptMessage) ? null : isControlCommand(commandText)
|
|
420
471
|
? runControlCommand
|
|
421
472
|
: (isModelCommand(commandText)
|
|
422
473
|
? runModelCommand
|
|
423
474
|
: (isPresetCommand(commandText) ? runPresetCommand : null));
|
|
424
|
-
const addressed = String(message.conversationType) !== '2' || message?.isInAtList === true;
|
|
425
475
|
if (commandRunner && sessionWebhook && addressed) {
|
|
426
476
|
let task;
|
|
427
477
|
task = this.#processFastCommand(
|
|
@@ -521,6 +571,7 @@ export class DingtalkHarnessBridge {
|
|
|
521
571
|
#enqueueMessage(message, messageId, sender, key, {
|
|
522
572
|
releaseMessageId = true,
|
|
523
573
|
alreadyRecorded = false,
|
|
574
|
+
batchSubmission = null,
|
|
524
575
|
} = {}) {
|
|
525
576
|
let hasSafeReplyRoute = false;
|
|
526
577
|
try {
|
|
@@ -543,6 +594,7 @@ export class DingtalkHarnessBridge {
|
|
|
543
594
|
.then(() => this.#process(message, messageId, sender, key, {
|
|
544
595
|
alreadyRecorded,
|
|
545
596
|
preparedMessage,
|
|
597
|
+
batchSubmission,
|
|
546
598
|
}))
|
|
547
599
|
.finally(() => {
|
|
548
600
|
if (releaseMessageId) this.#acceptedMessageIds.delete(messageId);
|
|
@@ -595,9 +647,32 @@ export class DingtalkHarnessBridge {
|
|
|
595
647
|
this.#status.lastError = null;
|
|
596
648
|
}
|
|
597
649
|
|
|
650
|
+
#finishBatchResult(messageId, sessionWebhook, result) {
|
|
651
|
+
let task;
|
|
652
|
+
task = Promise.resolve().then(async () => {
|
|
653
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
654
|
+
await this.#state.markSeen(messageId);
|
|
655
|
+
increment(this.#status, 'messagesReceived');
|
|
656
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
657
|
+
if (result.message) await this.#send(sessionWebhook, result.message);
|
|
658
|
+
this.#status.lastError = null;
|
|
659
|
+
}).catch(async (error) => {
|
|
660
|
+
if (this.#signal?.aborted) return;
|
|
661
|
+
this.#status.lastError = t('钉钉命令处理失败。');
|
|
662
|
+
this.#logger.error?.('[dsh-dingtalk] failed to process a batch input message', safeErrorDiagnostic(error));
|
|
663
|
+
await this.#send(sessionWebhook, t(CARD_ERROR_TEXT)).catch(() => undefined);
|
|
664
|
+
}).finally(() => {
|
|
665
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
666
|
+
this.#commandTasks.delete(task);
|
|
667
|
+
});
|
|
668
|
+
this.#commandTasks.add(task);
|
|
669
|
+
return task;
|
|
670
|
+
}
|
|
671
|
+
|
|
598
672
|
async #process(message, messageId, sender, key, {
|
|
599
673
|
alreadyRecorded = false,
|
|
600
674
|
preparedMessage,
|
|
675
|
+
batchSubmission = null,
|
|
601
676
|
} = {}) {
|
|
602
677
|
this.#signal?.throwIfAborted();
|
|
603
678
|
if (!alreadyRecorded) {
|
|
@@ -633,6 +708,7 @@ export class DingtalkHarnessBridge {
|
|
|
633
708
|
const isPlainText = String(message?.msgtype).toLowerCase() === 'text';
|
|
634
709
|
let cardStream = null;
|
|
635
710
|
let cardStarted = false;
|
|
711
|
+
let batchSettled = batchSubmission === null;
|
|
636
712
|
try {
|
|
637
713
|
if (!text && !hasImages && !hasFiles) {
|
|
638
714
|
await this.#send(sessionWebhook, t('目前支持文字、图片和文件消息。'));
|
|
@@ -717,6 +793,10 @@ export class DingtalkHarnessBridge {
|
|
|
717
793
|
files: promptMessage.files,
|
|
718
794
|
},
|
|
719
795
|
});
|
|
796
|
+
if (batchSubmission) {
|
|
797
|
+
this.#batchInputs.complete(key, batchSubmission.token);
|
|
798
|
+
batchSettled = true;
|
|
799
|
+
}
|
|
720
800
|
const answerText = typeof answer === 'string' && answer.trim()
|
|
721
801
|
? answer
|
|
722
802
|
: artifacts.length > 0 ? t('结果文件已生成。') : answer;
|
|
@@ -753,6 +833,15 @@ export class DingtalkHarnessBridge {
|
|
|
753
833
|
this.#status.lastError = null;
|
|
754
834
|
return delivery.receipt;
|
|
755
835
|
} catch (error) {
|
|
836
|
+
let batchFailureMessage = null;
|
|
837
|
+
if (!batchSettled && batchSubmission) {
|
|
838
|
+
if (error?.code === 'turn-stopped') {
|
|
839
|
+
this.#batchInputs.complete(key, batchSubmission.token);
|
|
840
|
+
} else {
|
|
841
|
+
batchFailureMessage = this.#batchInputs.fail(key, batchSubmission.token).message ?? null;
|
|
842
|
+
}
|
|
843
|
+
batchSettled = true;
|
|
844
|
+
}
|
|
756
845
|
if (error?.code === 'turn-stopped') {
|
|
757
846
|
if (cardStarted) await cardStream.finish(t('已停止。')).catch(() => undefined);
|
|
758
847
|
return;
|
|
@@ -767,8 +856,11 @@ export class DingtalkHarnessBridge {
|
|
|
767
856
|
const errorText = inboundFileUserMessage(error)
|
|
768
857
|
?? dingtalkImageErrorUserMessage(error)
|
|
769
858
|
?? t(CARD_ERROR_TEXT);
|
|
770
|
-
const
|
|
771
|
-
|
|
859
|
+
const visibleError = batchFailureMessage
|
|
860
|
+
? `${errorText}\n\n${batchFailureMessage}`
|
|
861
|
+
: errorText;
|
|
862
|
+
const streamed = cardStarted && await cardStream.finish(visibleError);
|
|
863
|
+
if (!streamed) await this.#send(sessionWebhook, visibleError);
|
|
772
864
|
} catch {
|
|
773
865
|
this.#logger.error?.('[dsh-dingtalk] failed to send the safe error reply');
|
|
774
866
|
}
|
|
@@ -217,6 +217,9 @@ export function normalizeDiscordMessage(message, botId, { fetchImpl = fetch } =
|
|
|
217
217
|
kind: direct ? 'direct' : 'group',
|
|
218
218
|
conversationId: String(message.channel_id),
|
|
219
219
|
content: stripBotMention(message.content ?? '', botId),
|
|
220
|
+
plainText: (!Array.isArray(message.attachments) || message.attachments.length === 0)
|
|
221
|
+
&& (!Array.isArray(message.sticker_items) || message.sticker_items.length === 0)
|
|
222
|
+
&& !message.poll,
|
|
220
223
|
images: Array.isArray(message.attachments)
|
|
221
224
|
? message.attachments.map((attachment) => discordImageSource(attachment, fetchImpl)).filter(Boolean)
|
|
222
225
|
: [],
|
|
@@ -22,6 +22,12 @@ import {
|
|
|
22
22
|
validHarnessQuestion,
|
|
23
23
|
} from '../shared/harness-question.mjs';
|
|
24
24
|
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
25
|
+
import {
|
|
26
|
+
BatchInputManager,
|
|
27
|
+
batchInputBusyMessage,
|
|
28
|
+
batchInputGroupUnsupportedMessage,
|
|
29
|
+
isBatchInputCommand,
|
|
30
|
+
} from '../shared/batch-input.mjs';
|
|
25
31
|
import { runCompactCommand } from '../shared/compact-command.mjs';
|
|
26
32
|
import {
|
|
27
33
|
isControlCommand,
|
|
@@ -366,6 +372,7 @@ export class FeishuHarnessBridge {
|
|
|
366
372
|
#harness;
|
|
367
373
|
#state;
|
|
368
374
|
#queues = new Map();
|
|
375
|
+
#batchInputs = new BatchInputManager();
|
|
369
376
|
#pendingInteractions = new Map();
|
|
370
377
|
#interactionKeys = new Map();
|
|
371
378
|
#resolvedQuestionReplies = new Map();
|
|
@@ -542,6 +549,58 @@ export class FeishuHarnessBridge {
|
|
|
542
549
|
const processingReaction = this.#addReaction(messageId, 'OnIt');
|
|
543
550
|
const commandMessage = extractInboundMessage(event, this.#client);
|
|
544
551
|
const commandText = nonEmptyString(commandMessage.content) ?? '';
|
|
552
|
+
const batchText = event.message.message_type === 'text'
|
|
553
|
+
? nonEmptyString(extractText(event)) ?? ''
|
|
554
|
+
: '';
|
|
555
|
+
const batchCommand = event.message.message_type === 'text'
|
|
556
|
+
&& isBatchInputCommand(batchText);
|
|
557
|
+
const pending = this.#pendingInteractions.get(key);
|
|
558
|
+
const batchStatus = this.#batchInputs.status(key);
|
|
559
|
+
if (batchCommand && event.message.chat_type !== 'p2p') {
|
|
560
|
+
return this.#finishBatchResult(
|
|
561
|
+
event,
|
|
562
|
+
messageId,
|
|
563
|
+
processingReaction,
|
|
564
|
+
{ message: batchInputGroupUnsupportedMessage() },
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
if (event.message.chat_type === 'p2p'
|
|
568
|
+
&& (batchCommand || batchStatus.phase === 'collecting')) {
|
|
569
|
+
const exactBatchStart = /^\/batch$/iu.test(batchText);
|
|
570
|
+
const result = exactBatchStart
|
|
571
|
+
&& batchStatus.phase === 'idle'
|
|
572
|
+
&& (this.#queues.has(key) || pending || this.#approvals.hasPending(key))
|
|
573
|
+
? { handled: true, kind: 'busy', message: batchInputBusyMessage() }
|
|
574
|
+
: this.#batchInputs.handle(key, batchText, {
|
|
575
|
+
plainText: event.message.message_type === 'text' && Boolean(batchText),
|
|
576
|
+
});
|
|
577
|
+
if (result.handled) {
|
|
578
|
+
if (result.kind === 'submit') {
|
|
579
|
+
const submissionEvent = {
|
|
580
|
+
...event,
|
|
581
|
+
batchSubmission: { token: result.token },
|
|
582
|
+
message: {
|
|
583
|
+
...event.message,
|
|
584
|
+
message_type: 'text',
|
|
585
|
+
content: JSON.stringify({ text: result.prompt }),
|
|
586
|
+
mentions: [],
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
return this.#enqueueMessage(
|
|
590
|
+
submissionEvent,
|
|
591
|
+
messageId,
|
|
592
|
+
key,
|
|
593
|
+
processingReaction,
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
return this.#finishBatchResult(
|
|
597
|
+
event,
|
|
598
|
+
messageId,
|
|
599
|
+
processingReaction,
|
|
600
|
+
result,
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
545
604
|
// Card commands (/m, /help, /status, etc.) bypass the queue so they
|
|
546
605
|
// respond immediately even when a harness task is still streaming.
|
|
547
606
|
if (CARD_COMMAND.test(commandText)) {
|
|
@@ -599,7 +658,6 @@ export class FeishuHarnessBridge {
|
|
|
599
658
|
.finally(() => this.#acceptedMessageIds.delete(messageId));
|
|
600
659
|
return current;
|
|
601
660
|
}
|
|
602
|
-
const pending = this.#pendingInteractions.get(key);
|
|
603
661
|
const approvalReply = this.#approvals.claimReply({
|
|
604
662
|
key,
|
|
605
663
|
actor: senderOpenId(event),
|
|
@@ -691,6 +749,32 @@ export class FeishuHarnessBridge {
|
|
|
691
749
|
return this.#enqueueMessage(event, messageId, key, processingReaction);
|
|
692
750
|
}
|
|
693
751
|
|
|
752
|
+
#finishBatchResult(event, messageId, processingReaction, result) {
|
|
753
|
+
let current;
|
|
754
|
+
current = Promise.resolve()
|
|
755
|
+
.then(async () => {
|
|
756
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
757
|
+
await this.#state.markSeen(messageId);
|
|
758
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
759
|
+
this.#status.messagesReceived += 1;
|
|
760
|
+
if (result?.message) await this.#send(event.message.chat_id, result.message);
|
|
761
|
+
this.#status.lastError = null;
|
|
762
|
+
})
|
|
763
|
+
.then(() => this.#finishReaction(messageId, processingReaction, 'DONE'))
|
|
764
|
+
.catch((error) => this.#handleMessageFailure(
|
|
765
|
+
event,
|
|
766
|
+
messageId,
|
|
767
|
+
processingReaction,
|
|
768
|
+
error,
|
|
769
|
+
))
|
|
770
|
+
.finally(() => {
|
|
771
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
772
|
+
this.#commandTasks.delete(current);
|
|
773
|
+
});
|
|
774
|
+
this.#commandTasks.add(current);
|
|
775
|
+
return current;
|
|
776
|
+
}
|
|
777
|
+
|
|
694
778
|
#enqueueMessage(event, messageId, key, processingReaction, {
|
|
695
779
|
releaseMessageId = true,
|
|
696
780
|
alreadyRecorded = false,
|
|
@@ -725,6 +809,9 @@ export class FeishuHarnessBridge {
|
|
|
725
809
|
async #handleMessageFailure(event, messageId, processingReaction, error) {
|
|
726
810
|
if (error?.code === 'turn-stopped') {
|
|
727
811
|
await this.#removeProcessingReaction(messageId, processingReaction);
|
|
812
|
+
if (error?.batchInputMessage) {
|
|
813
|
+
await this.#send(event.message.chat_id, error.batchInputMessage).catch(() => undefined);
|
|
814
|
+
}
|
|
728
815
|
return;
|
|
729
816
|
}
|
|
730
817
|
if (this.#signal?.aborted) {
|
|
@@ -736,7 +823,8 @@ export class FeishuHarnessBridge {
|
|
|
736
823
|
await this.#finishReaction(messageId, processingReaction, 'ERROR');
|
|
737
824
|
await this.#send(
|
|
738
825
|
event.message.chat_id,
|
|
739
|
-
|
|
826
|
+
error?.batchInputMessage
|
|
827
|
+
?? inboundFileUserMessage(error)
|
|
740
828
|
?? imagePromptUserMessage(error)
|
|
741
829
|
?? t('处理失败,请稍后重试。如果问题持续,请在 DeepSeek Harness 的飞书插件页面检查连接状态。'),
|
|
742
830
|
).catch(() => undefined);
|
|
@@ -939,12 +1027,38 @@ export class FeishuHarnessBridge {
|
|
|
939
1027
|
}
|
|
940
1028
|
|
|
941
1029
|
this.#logger.info?.(`[dsh-feishu] processing ${event.message.chat_type} message ${messageId}`);
|
|
1030
|
+
const batchSubmission = event.batchSubmission ?? null;
|
|
1031
|
+
let batchAskCompleted = false;
|
|
942
1032
|
try {
|
|
943
|
-
const receipt = await this.#answerWithStream(event, key, message
|
|
1033
|
+
const receipt = await this.#answerWithStream(event, key, message, {
|
|
1034
|
+
onAskComplete: batchSubmission
|
|
1035
|
+
? () => {
|
|
1036
|
+
batchAskCompleted = this.#batchInputs.complete(
|
|
1037
|
+
key,
|
|
1038
|
+
batchSubmission.token,
|
|
1039
|
+
).completed;
|
|
1040
|
+
}
|
|
1041
|
+
: undefined,
|
|
1042
|
+
});
|
|
944
1043
|
this.#status.messagesReplied += 1;
|
|
945
1044
|
this.#status.lastReplyAt = new Date().toISOString();
|
|
946
1045
|
this.#status.lastError = null;
|
|
947
1046
|
return receipt;
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
if (batchSubmission && !batchAskCompleted) {
|
|
1049
|
+
if (error?.code === 'turn-stopped') {
|
|
1050
|
+
this.#batchInputs.complete(key, batchSubmission.token);
|
|
1051
|
+
throw error;
|
|
1052
|
+
}
|
|
1053
|
+
const failed = this.#batchInputs.fail(key, batchSubmission.token);
|
|
1054
|
+
if (failed.retained) {
|
|
1055
|
+
const batchError = new Error(error?.message ?? String(error), { cause: error });
|
|
1056
|
+
batchError.code = error?.code;
|
|
1057
|
+
batchError.batchInputMessage = failed.message;
|
|
1058
|
+
throw batchError;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
throw error;
|
|
948
1062
|
} finally {
|
|
949
1063
|
await this.#cancelPendingInteraction(key);
|
|
950
1064
|
await this.#approvals.closeRoute(key);
|
|
@@ -2937,10 +3051,16 @@ export class FeishuHarnessBridge {
|
|
|
2937
3051
|
};
|
|
2938
3052
|
}
|
|
2939
3053
|
|
|
2940
|
-
async #answerWithStream(event, key, message) {
|
|
3054
|
+
async #answerWithStream(event, key, message, { onAskComplete } = {}) {
|
|
2941
3055
|
const chatId = event.message.chat_id;
|
|
2942
3056
|
const messageId = event.message.message_id;
|
|
2943
3057
|
const text = message.content;
|
|
3058
|
+
let askCompleted = false;
|
|
3059
|
+
const markAskComplete = () => {
|
|
3060
|
+
if (askCompleted) return;
|
|
3061
|
+
askCompleted = true;
|
|
3062
|
+
onAskComplete?.();
|
|
3063
|
+
};
|
|
2944
3064
|
const content = hasInboundImages(message)
|
|
2945
3065
|
? await promptContentForMessage(message, { signal: this.#signal })
|
|
2946
3066
|
: undefined;
|
|
@@ -2955,6 +3075,7 @@ export class FeishuHarnessBridge {
|
|
|
2955
3075
|
existsOptions: { signal: this.#signal },
|
|
2956
3076
|
askOptions: this.#interactionAskOptions(event, key, message.files),
|
|
2957
3077
|
});
|
|
3078
|
+
markAskComplete();
|
|
2958
3079
|
let textReceipt;
|
|
2959
3080
|
let textSendError = null;
|
|
2960
3081
|
try {
|
|
@@ -3009,6 +3130,7 @@ export class FeishuHarnessBridge {
|
|
|
3009
3130
|
existsOptions: { signal: this.#signal },
|
|
3010
3131
|
askOptions,
|
|
3011
3132
|
});
|
|
3133
|
+
markAskComplete();
|
|
3012
3134
|
completedAnswer = completed.answer;
|
|
3013
3135
|
completedArtifacts = completed.artifacts ?? [];
|
|
3014
3136
|
await controller.setContent(answerTextForDelivery(completedAnswer, completedArtifacts));
|
|
@@ -3067,6 +3189,7 @@ export class FeishuHarnessBridge {
|
|
|
3067
3189
|
existsOptions: { signal: this.#signal },
|
|
3068
3190
|
askOptions: this.#interactionAskOptions(event, key, message.files),
|
|
3069
3191
|
});
|
|
3192
|
+
markAskComplete();
|
|
3070
3193
|
let textReceipt;
|
|
3071
3194
|
let textSendError = null;
|
|
3072
3195
|
try {
|
|
@@ -513,6 +513,11 @@ export function menuHelpText() {
|
|
|
513
513
|
'/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级',
|
|
514
514
|
'/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型',
|
|
515
515
|
'',
|
|
516
|
+
'📦 批量输入(仅私聊)',
|
|
517
|
+
'/batch 开始批量输入(仅私聊,最多 10 条文字)',
|
|
518
|
+
'/send 提交当前批次',
|
|
519
|
+
'/cancel 取消当前批次',
|
|
520
|
+
'',
|
|
516
521
|
'🎮 任务控制',
|
|
517
522
|
'/stop 停止当前任务',
|
|
518
523
|
'/steer 指令 给 Agent 补充指令',
|
|
@@ -564,6 +569,9 @@ const HELP_TEXT_COMMANDS = [
|
|
|
564
569
|
'`/reasoninglist` 或 `/reasonings` — 按序号列出当前模型可用推理等级',
|
|
565
570
|
'`/reasoning [序号、等级ID或 --default]` — 查看或切换当前推理等级',
|
|
566
571
|
'`/model [序号或完整模型ID] [推理等级ID]` — 查看或切换当前会话模型',
|
|
572
|
+
'`/batch` — 开启批量输入(仅私聊,最多 10 条文字)',
|
|
573
|
+
'`/send` — 提交当前批次',
|
|
574
|
+
'`/cancel` — 取消当前批次',
|
|
567
575
|
'`/repair` — 修复卡片按钮',
|
|
568
576
|
].join('\n');
|
|
569
577
|
|
|
@@ -20,6 +20,12 @@ import {
|
|
|
20
20
|
runPresetCommand,
|
|
21
21
|
} from '../shared/preset-command.mjs';
|
|
22
22
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
23
|
+
import {
|
|
24
|
+
BatchInputManager,
|
|
25
|
+
batchInputBusyMessage,
|
|
26
|
+
batchInputGroupUnsupportedMessage,
|
|
27
|
+
isBatchInputCommand,
|
|
28
|
+
} from '../shared/batch-input.mjs';
|
|
23
29
|
import {
|
|
24
30
|
fetchImageBuffer,
|
|
25
31
|
hasInboundImages,
|
|
@@ -80,6 +86,9 @@ function helpText() {
|
|
|
80
86
|
t('/preset --default 跟随 Host 默认'),
|
|
81
87
|
t('/stop 停止当前任务'),
|
|
82
88
|
t('/steer 补充指令 纠偏当前任务'),
|
|
89
|
+
t('/batch 开始批量输入(仅私聊,最多 10 条文字)'),
|
|
90
|
+
t('/send 提交当前批次'),
|
|
91
|
+
t('/cancel 取消当前批次'),
|
|
83
92
|
t('/status 检查连接状态'),
|
|
84
93
|
t('/help 显示本帮助'),
|
|
85
94
|
].join('\n');
|
|
@@ -352,6 +361,7 @@ export class QqHarnessBridge {
|
|
|
352
361
|
#approvalTasks = new Set();
|
|
353
362
|
#commandTasks = new Set();
|
|
354
363
|
#approvals;
|
|
364
|
+
#batchInputs = new BatchInputManager();
|
|
355
365
|
|
|
356
366
|
constructor({
|
|
357
367
|
bot,
|
|
@@ -407,14 +417,47 @@ export class QqHarnessBridge {
|
|
|
407
417
|
}
|
|
408
418
|
const pending = this.#pendingInteractions.get(key);
|
|
409
419
|
const commandText = safeText(message);
|
|
420
|
+
const allowed = this.#ownerUserOpenid === '*' || sender === this.#ownerUserOpenid;
|
|
421
|
+
const addressed = message.kind !== 'group'
|
|
422
|
+
|| message.rawEventType === 'GROUP_AT_MESSAGE_CREATE';
|
|
423
|
+
const batchCommand = isBatchInputCommand(commandText);
|
|
424
|
+
const batchStatus = this.#batchInputs.status(key);
|
|
425
|
+
if (batchCommand && allowed && addressed && message.kind === 'group') {
|
|
426
|
+
return this.#finishBatchResult(
|
|
427
|
+
message,
|
|
428
|
+
messageId,
|
|
429
|
+
{ message: batchInputGroupUnsupportedMessage() },
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
if (allowed && message.kind === 'c2c'
|
|
433
|
+
&& (batchCommand || batchStatus.phase === 'collecting')) {
|
|
434
|
+
const exactBatchStart = /^\/batch$/iu.test(commandText);
|
|
435
|
+
const result = exactBatchStart
|
|
436
|
+
&& batchStatus.phase === 'idle'
|
|
437
|
+
&& (this.#queues.has(key) || pending || this.#approvals.hasPending(key))
|
|
438
|
+
? { handled: true, kind: 'busy', message: batchInputBusyMessage() }
|
|
439
|
+
: this.#batchInputs.handle(key, commandText, {
|
|
440
|
+
plainText: Boolean(commandText)
|
|
441
|
+
&& !hasQqImageAttachments(message)
|
|
442
|
+
&& !hasQqFileAttachments(message),
|
|
443
|
+
});
|
|
444
|
+
if (result.handled) {
|
|
445
|
+
if (result.kind === 'submit') {
|
|
446
|
+
return this.#enqueueMessage(
|
|
447
|
+
{ ...message, content: result.prompt, attachments: [] },
|
|
448
|
+
messageId,
|
|
449
|
+
key,
|
|
450
|
+
{ batchSubmission: result },
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
return this.#finishBatchResult(message, messageId, result);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
410
456
|
const commandRunner = hasQqFileAttachments(message) ? null : isControlCommand(commandText)
|
|
411
457
|
? runControlCommand
|
|
412
458
|
: (isModelCommand(commandText)
|
|
413
459
|
? runModelCommand
|
|
414
460
|
: (isPresetCommand(commandText) ? runPresetCommand : null));
|
|
415
|
-
const allowed = this.#ownerUserOpenid === '*' || sender === this.#ownerUserOpenid;
|
|
416
|
-
const addressed = message.kind !== 'group'
|
|
417
|
-
|| message.rawEventType === 'GROUP_AT_MESSAGE_CREATE';
|
|
418
461
|
if (commandRunner && allowed && addressed) {
|
|
419
462
|
let task;
|
|
420
463
|
task = this.#processFastCommand(
|
|
@@ -494,6 +537,7 @@ export class QqHarnessBridge {
|
|
|
494
537
|
#enqueueMessage(message, messageId, key, {
|
|
495
538
|
releaseMessageId = true,
|
|
496
539
|
alreadyRecorded = false,
|
|
540
|
+
batchSubmission = null,
|
|
497
541
|
} = {}) {
|
|
498
542
|
const allowed = this.#ownerUserOpenid === '*' || message.senderId === this.#ownerUserOpenid;
|
|
499
543
|
const addressed = message.kind !== 'group'
|
|
@@ -507,7 +551,11 @@ export class QqHarnessBridge {
|
|
|
507
551
|
const previous = this.#queues.get(key) ?? Promise.resolve();
|
|
508
552
|
const current = previous
|
|
509
553
|
.catch(() => undefined)
|
|
510
|
-
.then(() => this.#process(message, key, {
|
|
554
|
+
.then(() => this.#process(message, key, {
|
|
555
|
+
alreadyRecorded,
|
|
556
|
+
preparedMessage,
|
|
557
|
+
batchSubmission,
|
|
558
|
+
}))
|
|
511
559
|
.finally(() => {
|
|
512
560
|
if (releaseMessageId) this.#acceptedMessageIds.delete(messageId);
|
|
513
561
|
if (this.#queues.get(key) === current) this.#queues.delete(key);
|
|
@@ -553,6 +601,29 @@ export class QqHarnessBridge {
|
|
|
553
601
|
this.#status.lastError = null;
|
|
554
602
|
}
|
|
555
603
|
|
|
604
|
+
#finishBatchResult(message, messageId, result) {
|
|
605
|
+
let task;
|
|
606
|
+
task = Promise.resolve().then(async () => {
|
|
607
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
608
|
+
await this.#state.markSeen(messageId);
|
|
609
|
+
this.#status.messagesReceived += 1;
|
|
610
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
611
|
+
if (result.message) await this.#bot.sendText(message.replyTarget, result.message);
|
|
612
|
+
this.#status.lastError = null;
|
|
613
|
+
}).catch(async (error) => {
|
|
614
|
+
if (this.#signal?.aborted) return;
|
|
615
|
+
this.#status.lastError = error?.message ?? String(error);
|
|
616
|
+
this.#logger.error?.('[dsh-im:qq] failed to process a batch input message:', error);
|
|
617
|
+
await this.#bot.sendText(message.replyTarget, t('消息处理失败,请稍后重试。'))
|
|
618
|
+
.catch(() => undefined);
|
|
619
|
+
}).finally(() => {
|
|
620
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
621
|
+
this.#commandTasks.delete(task);
|
|
622
|
+
});
|
|
623
|
+
this.#commandTasks.add(task);
|
|
624
|
+
return task;
|
|
625
|
+
}
|
|
626
|
+
|
|
556
627
|
async #deliverArtifacts(target, replyTo, artifacts = [], baseReceipt = null) {
|
|
557
628
|
if (artifacts.length === 0) {
|
|
558
629
|
return { receipt: baseReceipt, failureNoticeVisible: false };
|
|
@@ -589,7 +660,11 @@ export class QqHarnessBridge {
|
|
|
589
660
|
};
|
|
590
661
|
}
|
|
591
662
|
|
|
592
|
-
async #process(message, key, {
|
|
663
|
+
async #process(message, key, {
|
|
664
|
+
alreadyRecorded = false,
|
|
665
|
+
preparedMessage,
|
|
666
|
+
batchSubmission = null,
|
|
667
|
+
} = {}) {
|
|
593
668
|
if (this.#signal?.aborted) return;
|
|
594
669
|
const messageId = nonEmptyString(message?.messageId);
|
|
595
670
|
const sender = nonEmptyString(message?.senderId);
|
|
@@ -614,6 +689,7 @@ export class QqHarnessBridge {
|
|
|
614
689
|
const hasImages = hasInboundImages(promptMessage);
|
|
615
690
|
const hasFiles = hasInboundFiles(promptMessage);
|
|
616
691
|
let stream = null;
|
|
692
|
+
let batchSettled = batchSubmission === null;
|
|
617
693
|
try {
|
|
618
694
|
if (!text && !hasImages && !hasFiles) {
|
|
619
695
|
await this.#bot.sendText(target, t('目前支持文字、图片和文件消息。'));
|
|
@@ -710,6 +786,10 @@ export class QqHarnessBridge {
|
|
|
710
786
|
files: promptMessage.files,
|
|
711
787
|
},
|
|
712
788
|
}));
|
|
789
|
+
if (batchSubmission) {
|
|
790
|
+
this.#batchInputs.complete(key, batchSubmission.token);
|
|
791
|
+
batchSettled = true;
|
|
792
|
+
}
|
|
713
793
|
} finally {
|
|
714
794
|
await Promise.allSettled([
|
|
715
795
|
this.#cancelPendingInteraction(key),
|
|
@@ -771,6 +851,15 @@ export class QqHarnessBridge {
|
|
|
771
851
|
this.#status.lastError = null;
|
|
772
852
|
return delivery.receipt;
|
|
773
853
|
} catch (error) {
|
|
854
|
+
let batchFailureMessage = null;
|
|
855
|
+
if (!batchSettled && batchSubmission) {
|
|
856
|
+
if (error?.code === 'turn-stopped') {
|
|
857
|
+
this.#batchInputs.complete(key, batchSubmission.token);
|
|
858
|
+
} else {
|
|
859
|
+
batchFailureMessage = this.#batchInputs.fail(key, batchSubmission.token).message ?? null;
|
|
860
|
+
}
|
|
861
|
+
batchSettled = true;
|
|
862
|
+
}
|
|
774
863
|
if (error?.code === 'turn-stopped') {
|
|
775
864
|
try {
|
|
776
865
|
stream?.cancel?.();
|
|
@@ -794,11 +883,12 @@ export class QqHarnessBridge {
|
|
|
794
883
|
this.#status.lastError = error?.message ?? String(error);
|
|
795
884
|
this.#logger.error?.('[dsh-im:qq] failed to process an inbound message:', error);
|
|
796
885
|
try {
|
|
886
|
+
const errorMessage = inboundFileUserMessage(error)
|
|
887
|
+
?? imagePromptUserMessage(error)
|
|
888
|
+
?? t('消息处理失败,请稍后重试。');
|
|
797
889
|
await this.#bot.sendText(
|
|
798
890
|
target,
|
|
799
|
-
|
|
800
|
-
?? imagePromptUserMessage(error)
|
|
801
|
-
?? t('消息处理失败,请稍后重试。'),
|
|
891
|
+
batchFailureMessage ? `${errorMessage}\n\n${batchFailureMessage}` : errorMessage,
|
|
802
892
|
);
|
|
803
893
|
await this.#state.markSeen(messageId);
|
|
804
894
|
} catch (sendError) {
|