@xmanrui/dsh-im 2.2.1 → 2.4.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 +12 -3
- package/README.md +12 -3
- package/lib/client.js +95 -52
- package/lib/index.js +220 -191
- package/package.json +1 -1
- package/plugin-src/client/channels/feishu/index.js +37 -27
- package/plugin-src/client/channels/feishu/styles.js +26 -1
- package/plugin-src/client/i18n.js +27 -25
- package/src/channels/dingtalk/dingtalk-bridge.mjs +96 -3
- package/src/channels/discord/discord-runtime.mjs +3 -0
- package/src/channels/feishu/bridge.mjs +176 -31
- package/src/channels/feishu/feishu-cards.mjs +18 -6
- package/src/channels/feishu/feishu-runtime.mjs +0 -1
- package/src/channels/feishu/message-utils.mjs +1 -1
- package/src/channels/feishu/multi-bot-controller.mjs +1 -17
- package/src/channels/feishu/repair-manager.mjs +7 -2
- package/src/channels/qq/qq-bridge.mjs +99 -8
- package/src/channels/shared/batch-input.mjs +209 -0
- package/src/channels/shared/control-command.mjs +11 -1
- package/src/channels/shared/i18n-en/feishu.mjs +28 -25
- package/src/channels/shared/i18n-en/shared-a.mjs +1 -0
- package/src/channels/shared/i18n-en/shared-b.mjs +1 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +49 -0
- package/src/channels/shared/i18n-en/telegram.mjs +1 -0
- package/src/channels/shared/text-harness-bridge.mjs +112 -2
- package/src/channels/slack/slack-runtime.mjs +1 -0
- package/src/channels/telegram/telegram-runtime.mjs +5 -0
- package/src/channels/wecom/wecom-bridge.mjs +102 -4
- package/src/channels/weixin/weixin-bridge.mjs +101 -3
- package/src/channels/weixin/weixin-runtime.mjs +18 -1
- package/src/channels/whatsapp/whatsapp-runtime.mjs +2 -0
|
@@ -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,7 +86,11 @@ 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 检查连接状态'),
|
|
93
|
+
t('/version 查看插件版本'),
|
|
84
94
|
t('/help 显示本帮助'),
|
|
85
95
|
].join('\n');
|
|
86
96
|
}
|
|
@@ -352,6 +362,7 @@ export class QqHarnessBridge {
|
|
|
352
362
|
#approvalTasks = new Set();
|
|
353
363
|
#commandTasks = new Set();
|
|
354
364
|
#approvals;
|
|
365
|
+
#batchInputs = new BatchInputManager();
|
|
355
366
|
|
|
356
367
|
constructor({
|
|
357
368
|
bot,
|
|
@@ -407,14 +418,47 @@ export class QqHarnessBridge {
|
|
|
407
418
|
}
|
|
408
419
|
const pending = this.#pendingInteractions.get(key);
|
|
409
420
|
const commandText = safeText(message);
|
|
421
|
+
const allowed = this.#ownerUserOpenid === '*' || sender === this.#ownerUserOpenid;
|
|
422
|
+
const addressed = message.kind !== 'group'
|
|
423
|
+
|| message.rawEventType === 'GROUP_AT_MESSAGE_CREATE';
|
|
424
|
+
const batchCommand = isBatchInputCommand(commandText);
|
|
425
|
+
const batchStatus = this.#batchInputs.status(key);
|
|
426
|
+
if (batchCommand && allowed && addressed && message.kind === 'group') {
|
|
427
|
+
return this.#finishBatchResult(
|
|
428
|
+
message,
|
|
429
|
+
messageId,
|
|
430
|
+
{ message: batchInputGroupUnsupportedMessage() },
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
if (allowed && message.kind === 'c2c'
|
|
434
|
+
&& (batchCommand || batchStatus.phase === 'collecting')) {
|
|
435
|
+
const exactBatchStart = /^\/batch$/iu.test(commandText);
|
|
436
|
+
const result = exactBatchStart
|
|
437
|
+
&& batchStatus.phase === 'idle'
|
|
438
|
+
&& (this.#queues.has(key) || pending || this.#approvals.hasPending(key))
|
|
439
|
+
? { handled: true, kind: 'busy', message: batchInputBusyMessage() }
|
|
440
|
+
: this.#batchInputs.handle(key, commandText, {
|
|
441
|
+
plainText: Boolean(commandText)
|
|
442
|
+
&& !hasQqImageAttachments(message)
|
|
443
|
+
&& !hasQqFileAttachments(message),
|
|
444
|
+
});
|
|
445
|
+
if (result.handled) {
|
|
446
|
+
if (result.kind === 'submit') {
|
|
447
|
+
return this.#enqueueMessage(
|
|
448
|
+
{ ...message, content: result.prompt, attachments: [] },
|
|
449
|
+
messageId,
|
|
450
|
+
key,
|
|
451
|
+
{ batchSubmission: result },
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
return this.#finishBatchResult(message, messageId, result);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
410
457
|
const commandRunner = hasQqFileAttachments(message) ? null : isControlCommand(commandText)
|
|
411
458
|
? runControlCommand
|
|
412
459
|
: (isModelCommand(commandText)
|
|
413
460
|
? runModelCommand
|
|
414
461
|
: (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
462
|
if (commandRunner && allowed && addressed) {
|
|
419
463
|
let task;
|
|
420
464
|
task = this.#processFastCommand(
|
|
@@ -494,6 +538,7 @@ export class QqHarnessBridge {
|
|
|
494
538
|
#enqueueMessage(message, messageId, key, {
|
|
495
539
|
releaseMessageId = true,
|
|
496
540
|
alreadyRecorded = false,
|
|
541
|
+
batchSubmission = null,
|
|
497
542
|
} = {}) {
|
|
498
543
|
const allowed = this.#ownerUserOpenid === '*' || message.senderId === this.#ownerUserOpenid;
|
|
499
544
|
const addressed = message.kind !== 'group'
|
|
@@ -507,7 +552,11 @@ export class QqHarnessBridge {
|
|
|
507
552
|
const previous = this.#queues.get(key) ?? Promise.resolve();
|
|
508
553
|
const current = previous
|
|
509
554
|
.catch(() => undefined)
|
|
510
|
-
.then(() => this.#process(message, key, {
|
|
555
|
+
.then(() => this.#process(message, key, {
|
|
556
|
+
alreadyRecorded,
|
|
557
|
+
preparedMessage,
|
|
558
|
+
batchSubmission,
|
|
559
|
+
}))
|
|
511
560
|
.finally(() => {
|
|
512
561
|
if (releaseMessageId) this.#acceptedMessageIds.delete(messageId);
|
|
513
562
|
if (this.#queues.get(key) === current) this.#queues.delete(key);
|
|
@@ -553,6 +602,29 @@ export class QqHarnessBridge {
|
|
|
553
602
|
this.#status.lastError = null;
|
|
554
603
|
}
|
|
555
604
|
|
|
605
|
+
#finishBatchResult(message, messageId, result) {
|
|
606
|
+
let task;
|
|
607
|
+
task = Promise.resolve().then(async () => {
|
|
608
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
609
|
+
await this.#state.markSeen(messageId);
|
|
610
|
+
this.#status.messagesReceived += 1;
|
|
611
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
612
|
+
if (result.message) await this.#bot.sendText(message.replyTarget, result.message);
|
|
613
|
+
this.#status.lastError = null;
|
|
614
|
+
}).catch(async (error) => {
|
|
615
|
+
if (this.#signal?.aborted) return;
|
|
616
|
+
this.#status.lastError = error?.message ?? String(error);
|
|
617
|
+
this.#logger.error?.('[dsh-im:qq] failed to process a batch input message:', error);
|
|
618
|
+
await this.#bot.sendText(message.replyTarget, t('消息处理失败,请稍后重试。'))
|
|
619
|
+
.catch(() => undefined);
|
|
620
|
+
}).finally(() => {
|
|
621
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
622
|
+
this.#commandTasks.delete(task);
|
|
623
|
+
});
|
|
624
|
+
this.#commandTasks.add(task);
|
|
625
|
+
return task;
|
|
626
|
+
}
|
|
627
|
+
|
|
556
628
|
async #deliverArtifacts(target, replyTo, artifacts = [], baseReceipt = null) {
|
|
557
629
|
if (artifacts.length === 0) {
|
|
558
630
|
return { receipt: baseReceipt, failureNoticeVisible: false };
|
|
@@ -589,7 +661,11 @@ export class QqHarnessBridge {
|
|
|
589
661
|
};
|
|
590
662
|
}
|
|
591
663
|
|
|
592
|
-
async #process(message, key, {
|
|
664
|
+
async #process(message, key, {
|
|
665
|
+
alreadyRecorded = false,
|
|
666
|
+
preparedMessage,
|
|
667
|
+
batchSubmission = null,
|
|
668
|
+
} = {}) {
|
|
593
669
|
if (this.#signal?.aborted) return;
|
|
594
670
|
const messageId = nonEmptyString(message?.messageId);
|
|
595
671
|
const sender = nonEmptyString(message?.senderId);
|
|
@@ -614,6 +690,7 @@ export class QqHarnessBridge {
|
|
|
614
690
|
const hasImages = hasInboundImages(promptMessage);
|
|
615
691
|
const hasFiles = hasInboundFiles(promptMessage);
|
|
616
692
|
let stream = null;
|
|
693
|
+
let batchSettled = batchSubmission === null;
|
|
617
694
|
try {
|
|
618
695
|
if (!text && !hasImages && !hasFiles) {
|
|
619
696
|
await this.#bot.sendText(target, t('目前支持文字、图片和文件消息。'));
|
|
@@ -710,6 +787,10 @@ export class QqHarnessBridge {
|
|
|
710
787
|
files: promptMessage.files,
|
|
711
788
|
},
|
|
712
789
|
}));
|
|
790
|
+
if (batchSubmission) {
|
|
791
|
+
this.#batchInputs.complete(key, batchSubmission.token);
|
|
792
|
+
batchSettled = true;
|
|
793
|
+
}
|
|
713
794
|
} finally {
|
|
714
795
|
await Promise.allSettled([
|
|
715
796
|
this.#cancelPendingInteraction(key),
|
|
@@ -771,6 +852,15 @@ export class QqHarnessBridge {
|
|
|
771
852
|
this.#status.lastError = null;
|
|
772
853
|
return delivery.receipt;
|
|
773
854
|
} catch (error) {
|
|
855
|
+
let batchFailureMessage = null;
|
|
856
|
+
if (!batchSettled && batchSubmission) {
|
|
857
|
+
if (error?.code === 'turn-stopped') {
|
|
858
|
+
this.#batchInputs.complete(key, batchSubmission.token);
|
|
859
|
+
} else {
|
|
860
|
+
batchFailureMessage = this.#batchInputs.fail(key, batchSubmission.token).message ?? null;
|
|
861
|
+
}
|
|
862
|
+
batchSettled = true;
|
|
863
|
+
}
|
|
774
864
|
if (error?.code === 'turn-stopped') {
|
|
775
865
|
try {
|
|
776
866
|
stream?.cancel?.();
|
|
@@ -794,11 +884,12 @@ export class QqHarnessBridge {
|
|
|
794
884
|
this.#status.lastError = error?.message ?? String(error);
|
|
795
885
|
this.#logger.error?.('[dsh-im:qq] failed to process an inbound message:', error);
|
|
796
886
|
try {
|
|
887
|
+
const errorMessage = inboundFileUserMessage(error)
|
|
888
|
+
?? imagePromptUserMessage(error)
|
|
889
|
+
?? t('消息处理失败,请稍后重试。');
|
|
797
890
|
await this.#bot.sendText(
|
|
798
891
|
target,
|
|
799
|
-
|
|
800
|
-
?? imagePromptUserMessage(error)
|
|
801
|
-
?? t('消息处理失败,请稍后重试。'),
|
|
892
|
+
batchFailureMessage ? `${errorMessage}\n\n${batchFailureMessage}` : errorMessage,
|
|
802
893
|
);
|
|
803
894
|
await this.#state.markSeen(messageId);
|
|
804
895
|
} catch (sendError) {
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { t } from './i18n.mjs';
|
|
2
|
+
|
|
3
|
+
export const BATCH_INPUT_LIMIT = 10;
|
|
4
|
+
|
|
5
|
+
const BATCH_COMMAND = /^\/(batch|send|cancel)(?=$|\s)/iu;
|
|
6
|
+
const EXACT_BATCH_COMMAND = /^\/(batch|send|cancel)$/iu;
|
|
7
|
+
|
|
8
|
+
function commandName(text) {
|
|
9
|
+
if (typeof text !== 'string') return null;
|
|
10
|
+
return BATCH_COMMAND.exec(text.trim())?.[1]?.toLowerCase() ?? null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function result(kind, message, extra = {}) {
|
|
14
|
+
return { handled: true, kind, ...(message ? { message } : {}), ...extra };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function progressMessage(count) {
|
|
18
|
+
if (count === BATCH_INPUT_LIMIT) {
|
|
19
|
+
return t(`当前已处于批量输入模式,已收集 {count}/{limit} 条。
|
|
20
|
+
请发送 /send 提交或 /cancel 取消。`, { count, limit: BATCH_INPUT_LIMIT });
|
|
21
|
+
}
|
|
22
|
+
return t(`当前已处于批量输入模式,已收集 {count}/{limit} 条。
|
|
23
|
+
完成后发送 /send,取消请发送 /cancel。`, { count, limit: BATCH_INPUT_LIMIT });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function submissionPrompt(messages) {
|
|
27
|
+
const sections = messages.map((message, index) => (
|
|
28
|
+
`${t('[消息 {index}]', { index: index + 1 })}\n${message}`
|
|
29
|
+
));
|
|
30
|
+
return [
|
|
31
|
+
t('以下是用户通过批量输入模式发送的多条内容,请按顺序作为同一次输入统一处理。'),
|
|
32
|
+
...sections,
|
|
33
|
+
].join('\n\n');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isBatchInputCommand(text) {
|
|
37
|
+
return commandName(text) !== null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function batchInputGroupUnsupportedMessage() {
|
|
41
|
+
return t('批量输入模式仅支持私聊,请在与机器人的私聊中使用。');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function batchInputBusyMessage() {
|
|
45
|
+
return t(`当前聊天有正在运行的任务、待回答问题或待审批请求。
|
|
46
|
+
请先完成当前交互或发送 /stop,再使用 /batch。`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class BatchInputManager {
|
|
50
|
+
#batches = new Map();
|
|
51
|
+
|
|
52
|
+
status(key) {
|
|
53
|
+
const batch = this.#batches.get(key);
|
|
54
|
+
if (!batch) {
|
|
55
|
+
return Object.freeze({ phase: 'idle', count: 0, limit: BATCH_INPUT_LIMIT, full: false });
|
|
56
|
+
}
|
|
57
|
+
return Object.freeze({
|
|
58
|
+
phase: batch.phase,
|
|
59
|
+
count: batch.messages.length,
|
|
60
|
+
limit: BATCH_INPUT_LIMIT,
|
|
61
|
+
full: batch.messages.length === BATCH_INPUT_LIMIT,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
handle(key, text, { plainText = true } = {}) {
|
|
66
|
+
const batch = this.#batches.get(key);
|
|
67
|
+
const name = commandName(text);
|
|
68
|
+
const exact = typeof text === 'string' ? EXACT_BATCH_COMMAND.exec(text.trim()) : null;
|
|
69
|
+
|
|
70
|
+
if (name && !exact) {
|
|
71
|
+
return result('invalid-command', t('用法:/{command}(不带参数)', { command: name }));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!batch) {
|
|
75
|
+
if (!name) return { handled: false };
|
|
76
|
+
if (!plainText) {
|
|
77
|
+
return result('unsupported-content', t('批量输入命令仅支持纯文字,请移除图片或文件后重试。'));
|
|
78
|
+
}
|
|
79
|
+
if (name === 'send') {
|
|
80
|
+
return result('no-batch', t('当前没有待提交的批量内容,请先发送 /batch。'));
|
|
81
|
+
}
|
|
82
|
+
if (name === 'cancel') {
|
|
83
|
+
return result('no-batch', t('当前没有正在进行的批量输入。'));
|
|
84
|
+
}
|
|
85
|
+
this.#batches.set(key, { phase: 'collecting', messages: [], token: null });
|
|
86
|
+
return result('started', t(`已进入批量输入模式,最多可发送 {limit} 条文字。
|
|
87
|
+
完成后发送 /send,取消请发送 /cancel。`, { limit: BATCH_INPUT_LIMIT }), {
|
|
88
|
+
count: 0,
|
|
89
|
+
limit: BATCH_INPUT_LIMIT,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!plainText && (batch.phase === 'collecting' || name)) {
|
|
94
|
+
return result('unsupported-content', t(`批量输入模式目前仅支持文字,这条消息未收录。
|
|
95
|
+
请继续发送文字,或使用 /send、/cancel。`), {
|
|
96
|
+
count: batch.messages.length,
|
|
97
|
+
limit: BATCH_INPUT_LIMIT,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (batch.phase === 'submitting') {
|
|
102
|
+
if (name === 'send') {
|
|
103
|
+
return result('submitting', t('当前批次正在提交,请勿重复发送 /send。'));
|
|
104
|
+
}
|
|
105
|
+
if (name === 'cancel') {
|
|
106
|
+
return result('submitting', t(`批量内容已经提交,无法取消。
|
|
107
|
+
如需停止当前任务,请发送 /stop。`));
|
|
108
|
+
}
|
|
109
|
+
if (name === 'batch') {
|
|
110
|
+
return result('submitting', t('当前批次正在提交,请等待处理完成后再开启新批次。'));
|
|
111
|
+
}
|
|
112
|
+
return { handled: false };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (name === 'batch') {
|
|
116
|
+
return result('status', progressMessage(batch.messages.length), {
|
|
117
|
+
count: batch.messages.length,
|
|
118
|
+
limit: BATCH_INPUT_LIMIT,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (name === 'cancel') {
|
|
123
|
+
const count = batch.messages.length;
|
|
124
|
+
this.#batches.delete(key);
|
|
125
|
+
return result('cancelled', count === 0
|
|
126
|
+
? t('已取消批量输入。')
|
|
127
|
+
: t('已取消批量输入,共丢弃 {count} 条消息。', { count }), { count });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (name === 'send') {
|
|
131
|
+
if (batch.messages.length === 0) {
|
|
132
|
+
return result('empty', t('当前批次还没有内容,请先发送文字,或使用 /cancel 取消。'), {
|
|
133
|
+
count: 0,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
const messages = Object.freeze([...batch.messages]);
|
|
137
|
+
const token = Object.freeze({});
|
|
138
|
+
batch.phase = 'submitting';
|
|
139
|
+
batch.token = token;
|
|
140
|
+
return result('submit', null, {
|
|
141
|
+
token,
|
|
142
|
+
messages,
|
|
143
|
+
prompt: submissionPrompt(messages),
|
|
144
|
+
count: messages.length,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (typeof text !== 'string') {
|
|
149
|
+
return result('unsupported-content', t(`批量输入模式目前仅支持文字,这条消息未收录。
|
|
150
|
+
请继续发送文字,或使用 /send、/cancel。`), {
|
|
151
|
+
count: batch.messages.length,
|
|
152
|
+
limit: BATCH_INPUT_LIMIT,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (text.trim().startsWith('/')) {
|
|
157
|
+
return result('blocked-command', t('当前正在批量输入,请先发送 /send 提交或 /cancel 取消。'), {
|
|
158
|
+
count: batch.messages.length,
|
|
159
|
+
limit: BATCH_INPUT_LIMIT,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (batch.messages.length === BATCH_INPUT_LIMIT) {
|
|
164
|
+
return result('full', t(`当前批次已满,这条消息未收录。
|
|
165
|
+
请先发送 /send 提交或 /cancel 取消,然后重新发送这条消息。`), {
|
|
166
|
+
count: BATCH_INPUT_LIMIT,
|
|
167
|
+
limit: BATCH_INPUT_LIMIT,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
batch.messages.push(text);
|
|
172
|
+
const count = batch.messages.length;
|
|
173
|
+
return result('collected', count === BATCH_INPUT_LIMIT
|
|
174
|
+
? t('已收集 {count}/{limit} 条,当前批次已满,请发送 /send 提交或 /cancel 取消。', {
|
|
175
|
+
count,
|
|
176
|
+
limit: BATCH_INPUT_LIMIT,
|
|
177
|
+
})
|
|
178
|
+
: null, {
|
|
179
|
+
count,
|
|
180
|
+
limit: BATCH_INPUT_LIMIT,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
complete(key, token) {
|
|
185
|
+
const batch = this.#batches.get(key);
|
|
186
|
+
if (!batch || batch.phase !== 'submitting' || batch.token !== token) {
|
|
187
|
+
return Object.freeze({ completed: false });
|
|
188
|
+
}
|
|
189
|
+
const count = batch.messages.length;
|
|
190
|
+
this.#batches.delete(key);
|
|
191
|
+
return Object.freeze({ completed: true, count });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fail(key, token) {
|
|
195
|
+
const batch = this.#batches.get(key);
|
|
196
|
+
if (!batch || batch.phase !== 'submitting' || batch.token !== token) {
|
|
197
|
+
return Object.freeze({ retained: false });
|
|
198
|
+
}
|
|
199
|
+
batch.phase = 'collecting';
|
|
200
|
+
batch.token = null;
|
|
201
|
+
const count = batch.messages.length;
|
|
202
|
+
return Object.freeze({
|
|
203
|
+
retained: true,
|
|
204
|
+
count,
|
|
205
|
+
message: t(`批量内容提交失败,已保留 {count} 条消息。
|
|
206
|
+
请再次发送 /send 重试或 /cancel 取消。`, { count }),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { t } from './i18n.mjs';
|
|
2
|
+
import manifest from '../../../package.json' with { type: 'json' };
|
|
2
3
|
|
|
3
|
-
const CONTROL_COMMAND = /^\/(?:stop|steer)(?=$|\s)/iu;
|
|
4
|
+
const CONTROL_COMMAND = /^\/(?:stop|steer|version)(?=$|\s)/iu;
|
|
4
5
|
const STOP_COMMAND = /^\/stop(?=$|\s)/iu;
|
|
6
|
+
const VERSION_COMMAND = /^\/version(?=$|\s)/iu;
|
|
5
7
|
const STOP_USAGE = '用法:/stop(不带参数)';
|
|
8
|
+
const VERSION_USAGE = '用法:/version(不带参数)';
|
|
6
9
|
const STEER_USAGE = '用法:/steer <补充指令>';
|
|
7
10
|
const TEXT_ONLY = '控制命令仅支持纯文字,请移除图片后重试。';
|
|
8
11
|
|
|
@@ -41,9 +44,16 @@ export async function runControlCommand(text, harness, state, key, {
|
|
|
41
44
|
if (!isControlCommand(text)) return null;
|
|
42
45
|
const command = text.trim();
|
|
43
46
|
const stop = STOP_COMMAND.test(command);
|
|
47
|
+
const version = VERSION_COMMAND.test(command);
|
|
44
48
|
|
|
45
49
|
if (hasImages) return commandResult(t(TEXT_ONLY));
|
|
46
50
|
|
|
51
|
+
if (version) {
|
|
52
|
+
return /^\/version$/iu.test(command)
|
|
53
|
+
? commandResult(`dsh-im v${manifest.version}`)
|
|
54
|
+
: commandResult(t(VERSION_USAGE));
|
|
55
|
+
}
|
|
56
|
+
|
|
47
57
|
if (stop) {
|
|
48
58
|
if (!/^\/stop$/iu.test(command)) return commandResult(t(STOP_USAGE));
|
|
49
59
|
const session = boundSession(harness, state, key);
|
|
@@ -3,7 +3,7 @@ export default {
|
|
|
3
3
|
// feishu/bridge.mjs — welcome / help
|
|
4
4
|
'北汇星河 AIOS 已连接 DeepSeek Harness。':
|
|
5
5
|
'BeiHui XingHe AIOS is connected to DeepSeek Harness.',
|
|
6
|
-
'/repair
|
|
6
|
+
'/repair 补全飞书权限与卡片回调': '/repair Complete Feishu permissions and the card callback',
|
|
7
7
|
'/m(或 /menu) 打开交互卡片菜单': '/m (or /menu) Open the interactive card menu',
|
|
8
8
|
'/watch [Session ID 或序号] 关注会话,任务完成自动推送':
|
|
9
9
|
'/watch [Session ID or index] Watch a session; completion is pushed automatically',
|
|
@@ -18,8 +18,8 @@ export default {
|
|
|
18
18
|
'机器人正在移除或已重新接入,无法操作原会话的工作区。':
|
|
19
19
|
'The bot is being removed or has been reconnected; the original session’s workspace cannot be changed.',
|
|
20
20
|
'操作失败,请稍后重试。': 'The operation failed. Please try again later.',
|
|
21
|
-
'结果文件「{name}
|
|
22
|
-
'The result file "{name}" was generated, but the bot lacks Feishu file-upload
|
|
21
|
+
'结果文件「{name}」已生成,但机器人缺少飞书文件上传权限 im:resource。请私聊机器人执行 /repair 命令,或者在插件页面点击“补全权限”按钮并扫码。完成飞书要求的发布审批后重试。':
|
|
22
|
+
'The result file "{name}" was generated, but the bot lacks the Feishu file-upload scope im:resource. Run /repair in a direct chat with the bot, or click the “Complete permissions” button on the plugin page and scan the QR code. Complete any publishing approval Feishu requires, then try again.',
|
|
23
23
|
'结果文件「{name}」超过飞书 30 MB 上限,未发送。':
|
|
24
24
|
'The result file "{name}" exceeds the Feishu 30 MB limit and was not sent.',
|
|
25
25
|
'结果文件「{name}」为空,飞书不允许发送空文件。':
|
|
@@ -41,20 +41,20 @@ export default {
|
|
|
41
41
|
// feishu/bridge.mjs — repair flow
|
|
42
42
|
'为避免授权链接暴露,请私聊机器人发送 /repair。':
|
|
43
43
|
'To keep the authorization link private, send /repair to the bot in a direct message.',
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
'此操作只能由机器人接入者在私聊中发起,未进行任何修改。':
|
|
47
|
-
'This action can only be started by the bot owner in a direct message. Nothing was changed.',
|
|
44
|
+
'无法识别当前发送者,未发起修复。':
|
|
45
|
+
'The current sender could not be identified, so repair was not started.',
|
|
48
46
|
'当前 Host 版本暂不支持聊天内修复,请先更新插件。':
|
|
49
47
|
'The current Host version does not support in-chat repair. Update the plugin first.',
|
|
50
48
|
'用法:/repair、/repair qr、/repair status、/repair cancel 或 /repair verify':
|
|
51
49
|
'Usage: /repair, /repair qr, /repair status, /repair cancel, or /repair verify',
|
|
52
50
|
'当前 Runtime 没有可恢复的修复任务记录(机器人可能刚完成密钥更新并重启)。本命令不会启动新的授权;请查看机器人发送的验证结果,确认上一次任务已结束后再发送 /repair。':
|
|
53
51
|
'The current Runtime has no recoverable repair attempt (the bot may have just rotated its secret and restarted). This command starts no new authorization. Check the verification result the bot sent, then send /repair after the previous attempt has finished.',
|
|
54
|
-
'
|
|
55
|
-
'Another
|
|
52
|
+
'另一位用户正在修复该机器人,本次不会显示其授权信息。':
|
|
53
|
+
'Another user is repairing this bot; its authorization info will not be shown this time.',
|
|
56
54
|
'暂时无法取消修复任务,请稍后重试。':
|
|
57
55
|
'Could not cancel the repair task right now. Please try again later.',
|
|
56
|
+
'暂时无法取消旧修复任务,未生成新链接;请稍后重试。':
|
|
57
|
+
'Could not cancel the previous repair attempt, so no new link was generated. Please try again later.',
|
|
58
58
|
'暂时无法查询修复状态,请稍后重试。':
|
|
59
59
|
'Could not check the repair status right now. Please try again later.',
|
|
60
60
|
'修复流程暂时失败,现有机器人连接不受影响;请稍后发送 /repair 重试。':
|
|
@@ -69,10 +69,11 @@ export default {
|
|
|
69
69
|
'The repair status query was interrupted; the existing bot connection is unaffected. Send /repair status to retry.',
|
|
70
70
|
'链接为短期有效': 'The link is valid for a short time.',
|
|
71
71
|
'链接约 {minutes} 分钟后过期': 'The link expires in about {minutes} minutes',
|
|
72
|
-
'
|
|
73
|
-
|
|
74
|
-
'
|
|
75
|
-
|
|
72
|
+
'旧授权链接已作废,已生成新的修复链接。':
|
|
73
|
+
'The previous authorization link was invalidated and a new repair link was generated.',
|
|
74
|
+
'🔧 准备补全权限与回调。': '🔧 Preparing to complete permissions and the callback.',
|
|
75
|
+
'本次最多增量添加三项:卡片回调 card.action.trigger;飞书显示为“获取单聊、群组消息”的租户权限 im:message:readonly(用于读取用户消息中的图片或文件);以及 im:resource(用于上传机器人发送的图片或文件)。确认页只会显示当前缺少的项;若出现上述范围之外的配置,请取消。':
|
|
76
|
+
'This may incrementally add up to three items: the card callback card.action.trigger; the tenant scope im:message:readonly, shown by Feishu as “Read direct and group messages” and used to read images or files in user messages; and im:resource, used to upload images or files sent by the bot. The confirmation page shows only items the app is currently missing; cancel if anything outside this scope appears.',
|
|
76
77
|
'当前设备直接打开:': 'Open directly on this device:',
|
|
77
78
|
'若要用另一台设备扫码,发送 /repair qr。{expiry}。':
|
|
78
79
|
'To scan with another device, send /repair qr. {expiry}.',
|
|
@@ -199,8 +200,8 @@ export default {
|
|
|
199
200
|
'切换归档显示': 'Toggle archived sessions',
|
|
200
201
|
'📊 状态': '📊 Status',
|
|
201
202
|
'📖 帮助': '📖 Help',
|
|
202
|
-
'**数字兜底**\n**1**工作区列表 · **2**新会话 · **3**会话列表 · **4**状态\n**5
|
|
203
|
-
'**Number fallback**\n**1** Workspace list · **2** New session · **3** Session list · **4** Status\n**5** 🔧
|
|
203
|
+
'**数字兜底**\n**1**工作区列表 · **2**新会话 · **3**会话列表 · **4**状态\n**5**🔧补全权限 · **6**帮助':
|
|
204
|
+
'**Number fallback**\n**1** Workspace list · **2** New session · **3** Session list · **4** Status\n**5** 🔧 Complete permissions · **6** Help',
|
|
204
205
|
'跟随 Host 默认{default}': 'Follow Host default{default}',
|
|
205
206
|
'**当前**:{value}': '**Current**: {value}',
|
|
206
207
|
'**Host 默认**:{value}': '**Host default**: {value}',
|
|
@@ -228,22 +229,24 @@ export default {
|
|
|
228
229
|
'/new 开启全新会话': '/new Start a new session',
|
|
229
230
|
'📊 状态 / 压缩': '📊 Status / compact',
|
|
230
231
|
'/status 连接状态': '/status Connection status',
|
|
232
|
+
'`/version` — 查看插件版本': '`/version` — show the plugin version',
|
|
231
233
|
'/compact 压缩当前会话上下文': '/compact Compact the current session context',
|
|
232
234
|
'/archived on/off 会话列表显示/隐藏归档': '/archived on/off Show/hide archived sessions',
|
|
233
235
|
'👁 关注': '👁 Watches',
|
|
234
236
|
'/watch ID 关注会话(完成后推送)': '/watch ID Watch a session (push on completion)',
|
|
235
237
|
'/watchlist 关注列表': '/watchlist List watched sessions',
|
|
236
238
|
'/unwatch ID 取消关注': '/unwatch ID Stop watching a session',
|
|
239
|
+
'📦 批量输入(仅私聊)': '📦 Batch input (direct messages only)',
|
|
237
240
|
'🤖 预设 / 模型': '🤖 Presets / models',
|
|
238
241
|
'/models 列出模型': '/models List models',
|
|
239
242
|
'🎮 任务控制': '🎮 Task controls',
|
|
240
243
|
'/steer 指令 给 Agent 补充指令': '/steer INSTRUCTION Steer the Agent',
|
|
241
244
|
'**📋 卡片功能**\n\n1. 会话下拉 — 切换当前绑定会话\n2. 工作区下拉 — 切换工作区\n3. 🤖 预设下拉 — 切换 Agent 预设\n4. 🧠 模型下拉 — 切换模型\n5. 🆕 新会话 — 开启全新会话\n6. 📋 会话/关注 — 查看/绑定会话,管理关注\n7. ⏹ 停止 — 停止当前任务\n8. 📐 压缩 — 压缩当前会话上下文\n9. 补充指令 — 给 Agent 发送指令\n10. 🗄 归档切换 — 显示/隐藏归档会话\n11. 📊 状态 — 查看系统连接状态\n12. 📖 帮助 — 查看本帮助':
|
|
242
245
|
'**📋 Card features**\n\n1. Session dropdown — switch the bound session\n2. Workspace dropdown — switch workspace\n3. 🤖 Preset dropdown — switch Agent Preset\n4. 🧠 Model dropdown — switch model\n5. 🆕 New session — start fresh\n6. 📋 Sessions/watches — view or bind sessions and manage watches\n7. ⏹ Stop — stop the current task\n8. 📐 Compact — compact the current session context\n9. Steer task — send an instruction to the Agent\n10. 🗄 Archived toggle — show or hide archived sessions\n11. 📊 Status — view connection status\n12. 📖 Help — view this help',
|
|
243
|
-
'**⌨️ 文本命令**\n\n`/m` — 打开菜单卡片\n`/new` — 开启全新会话\n`/session ID` — 绑定已有会话\n`/sessionlist [工作区]` — 列出会话\n`/workspace 路径` — 切换工作区\n`/workspacelist` — 列出工作区\n`/status` — 查看连接状态\n`/compact` — 压缩上下文\n`/stop` — 停止当前任务\n`/steer 指令` — 补充指令\n`/watch ID` — 关注会话\n`/watchlist` — 关注列表\n`/unwatch ID` — 取消关注\n`/archived on/off` — 归档显隐\n`/presetlist` — 列出预设\n`/preset [序号/ID]` — 切换预设\n`/preset --default` — 跟随默认\n`/models` — 列出模型\n`/reasoninglist` 或 `/reasonings` — 按序号列出当前模型可用推理等级\n`/reasoning [序号、等级ID或 --default]` — 查看或切换当前推理等级\n`/model [序号或完整模型ID] [推理等级ID]` — 查看或切换当前会话模型\n`/repair` —
|
|
244
|
-
'**⌨️ Text commands**\n\n`/m` — open the menu card\n`/new` — start a new session\n`/session ID` — bind an existing session\n`/sessionlist [workspace]` — list sessions\n`/workspace PATH` — switch workspace\n`/workspacelist` — list workspaces\n`/status` — view connection status\n`/compact` — compact context\n`/stop` — stop the current task\n`/steer INSTRUCTION` — steer the task\n`/watch ID` — watch a session\n`/watchlist` — list watched sessions\n`/unwatch ID` — stop watching\n`/archived on/off` — show or hide archived sessions\n`/presetlist` — list presets\n`/preset [index/ID]` — switch preset\n`/preset --default` — follow default\n`/models` — list models\n`/reasoninglist` or `/reasonings` — list reasoning efforts for the current model\n`/reasoning [index, effort ID, or --default]` — show or switch reasoning effort\n`/model [index or full model ID] [reasoning effort ID]` — show or switch the current Session model\n`/
|
|
245
|
-
'**💡 数字兜底**\n回复数字快速操作:\n**1**工作区列表 · **2**新会话 · **3**会话/关注\n**4**状态 · **5
|
|
246
|
-
'**💡 Number fallback**\nReply with a number for a quick action:\n**1** Workspace list · **2** New session · **3** Sessions/watches\n**4** Status · **5**
|
|
246
|
+
'**⌨️ 文本命令**\n\n`/m` — 打开菜单卡片\n`/new` — 开启全新会话\n`/session ID` — 绑定已有会话\n`/sessionlist [工作区]` — 列出会话\n`/workspace 路径` — 切换工作区\n`/workspacelist` — 列出工作区\n`/status` — 查看连接状态\n`/compact` — 压缩上下文\n`/stop` — 停止当前任务\n`/steer 指令` — 补充指令\n`/watch ID` — 关注会话\n`/watchlist` — 关注列表\n`/unwatch ID` — 取消关注\n`/archived on/off` — 归档显隐\n`/presetlist` — 列出预设\n`/preset [序号/ID]` — 切换预设\n`/preset --default` — 跟随默认\n`/models` — 列出模型\n`/reasoninglist` 或 `/reasonings` — 按序号列出当前模型可用推理等级\n`/reasoning [序号、等级ID或 --default]` — 查看或切换当前推理等级\n`/model [序号或完整模型ID] [推理等级ID]` — 查看或切换当前会话模型\n`/batch` — 开启批量输入(仅私聊,最多 10 条文字)\n`/send` — 提交当前批次\n`/cancel` — 取消当前批次\n`/repair` — 补全飞书权限与卡片回调':
|
|
247
|
+
'**⌨️ Text commands**\n\n`/m` — open the menu card\n`/new` — start a new session\n`/session ID` — bind an existing session\n`/sessionlist [workspace]` — list sessions\n`/workspace PATH` — switch workspace\n`/workspacelist` — list workspaces\n`/status` — view connection status\n`/compact` — compact context\n`/stop` — stop the current task\n`/steer INSTRUCTION` — steer the task\n`/watch ID` — watch a session\n`/watchlist` — list watched sessions\n`/unwatch ID` — stop watching\n`/archived on/off` — show or hide archived sessions\n`/presetlist` — list presets\n`/preset [index/ID]` — switch preset\n`/preset --default` — follow default\n`/models` — list models\n`/reasoninglist` or `/reasonings` — list reasoning efforts for the current model\n`/reasoning [index, effort ID, or --default]` — show or switch reasoning effort\n`/model [index or full model ID] [reasoning effort ID]` — show or switch the current Session model\n`/batch` — start batch input (direct messages only, up to 10 text messages)\n`/send` — submit the current batch\n`/cancel` — cancel the current batch\n`/repair` — complete Feishu permissions and the card callback',
|
|
248
|
+
'**💡 数字兜底**\n回复数字快速操作:\n**1**工作区列表 · **2**新会话 · **3**会话/关注\n**4**状态 · **5**补全权限 · **6**帮助':
|
|
249
|
+
'**💡 Number fallback**\nReply with a number for a quick action:\n**1** Workspace list · **2** New session · **3** Sessions/watches\n**4** Status · **5** Complete permissions · **6** Help',
|
|
247
250
|
'从下方下拉选择补充指令;最后一项可自定义输入。':
|
|
248
251
|
'Choose an instruction below; the last option lets you enter a custom one.',
|
|
249
252
|
'当前没有绑定会话,请先绑定会话再补充指令。':
|
|
@@ -261,8 +264,8 @@ export default {
|
|
|
261
264
|
'3 · 新会话': '3 · New session',
|
|
262
265
|
'4 · 状态': '4 · Status',
|
|
263
266
|
'5 · 帮助': '5 · Help',
|
|
264
|
-
'**6 ·
|
|
265
|
-
'**6 ·
|
|
267
|
+
'**6 · 补全权限**(请直接回复数字 **6**)':
|
|
268
|
+
'**6 · Complete permissions** (reply with the number **6**)',
|
|
266
269
|
'7 · 关注列表': '7 · Watch list',
|
|
267
270
|
'🧪 验证卡片按钮': '🧪 Verify card buttons',
|
|
268
271
|
'授权已提交。请点击下方按钮;机器人真实收到回调后才会判定修复成功。':
|
|
@@ -289,8 +292,8 @@ export default {
|
|
|
289
292
|
'3 · /new 开启新会话': '3 · /new Start a new session',
|
|
290
293
|
'4 · /status 连接状态': '4 · /status Connection status',
|
|
291
294
|
'5 · /help 本帮助': '5 · /help This help',
|
|
292
|
-
'6 · /repair
|
|
293
|
-
'6 · /repair
|
|
295
|
+
'6 · /repair 补全权限与回调(请回复数字 6)':
|
|
296
|
+
'6 · /repair Complete permissions and callback (reply with the number 6)',
|
|
294
297
|
'7 · /watchlist 关注列表': '7 · /watchlist Watch list',
|
|
295
298
|
'直接发送文字/图片即继续当前会话。':
|
|
296
299
|
'Send text or an image directly to continue the current session.',
|
|
@@ -335,8 +338,8 @@ export default {
|
|
|
335
338
|
'飞书机器人': 'Feishu bot',
|
|
336
339
|
|
|
337
340
|
// feishu/message-utils.mjs
|
|
338
|
-
'
|
|
339
|
-
'The Feishu bot is missing image-read
|
|
341
|
+
'飞书机器人缺少图片读取权限 im:message:readonly(飞书显示为“获取单聊、群组消息”)。请私聊机器人执行 /repair 命令,或者在插件页面点击“补全权限”按钮并扫码。按飞书提示发布新版本、完成必要审批后,再重新发送图片。':
|
|
342
|
+
'The Feishu bot is missing the image-read scope im:message:readonly, shown by Feishu as “Read direct and group messages.” Run /repair in a direct chat with the bot, or click the “Complete permissions” button on the plugin page and scan the QR code. Publish a new version and complete any approval requested by Feishu, then resend the image.',
|
|
340
343
|
|
|
341
344
|
// feishu/feishu-runtime.mjs — callback probe notices
|
|
342
345
|
'✅ 修复完成:已实测收到 card.action.trigger,菜单按钮现在可用。':
|
|
@@ -69,6 +69,7 @@ export default {
|
|
|
69
69
|
'/stop 停止当前任务': '/stop Stop the current task',
|
|
70
70
|
'/steer 补充指令 纠偏当前任务': '/steer <additional instruction> Steer the current task',
|
|
71
71
|
'/status 检查连接状态': '/status Check the connection status',
|
|
72
|
+
'/version 查看插件版本': '/version Show the plugin version',
|
|
72
73
|
'/help 显示本帮助': '/help Show this help',
|
|
73
74
|
'{label}机器人与 DeepSeek Harness 连接正常。':
|
|
74
75
|
'The {label} bot is connected to DeepSeek Harness and working normally.',
|
|
@@ -242,6 +242,7 @@ export default {
|
|
|
242
242
|
|
|
243
243
|
// control-command.mjs
|
|
244
244
|
'用法:/stop(不带参数)': 'Usage: /stop (no arguments)',
|
|
245
|
+
'用法:/version(不带参数)': 'Usage: /version (no arguments)',
|
|
245
246
|
'用法:/steer <补充指令>': 'Usage: /steer <additional instruction>',
|
|
246
247
|
'控制命令仅支持纯文字,请移除图片后重试。':
|
|
247
248
|
'Control commands support text only; please remove images and try again.',
|