@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
|
@@ -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,
|
|
@@ -100,9 +106,10 @@ const CARD_OVERLOAD_NOTICE_COOLDOWN_MS = 5_000;
|
|
|
100
106
|
const CARD_DATA_TIMEOUT_MS = 5_000;
|
|
101
107
|
const REPAIR_LINK_WAIT_MS = 15_000;
|
|
102
108
|
const REPAIR_POLL_INTERVAL_MS = 1_000;
|
|
103
|
-
const
|
|
104
|
-
'starting', 'qr_ready', 'polling', 'slow_down', 'domain_switched',
|
|
109
|
+
const REPAIR_AUTHORIZATION_STATES = new Set([
|
|
110
|
+
'starting', 'qr_ready', 'polling', 'slow_down', 'domain_switched',
|
|
105
111
|
]);
|
|
112
|
+
const REPAIR_ACTIVE_STATES = new Set([...REPAIR_AUTHORIZATION_STATES, 'saving']);
|
|
106
113
|
const REPAIR_TERMINAL_STATES = new Set([
|
|
107
114
|
'succeeded', 'expired', 'cancelled', 'error',
|
|
108
115
|
]);
|
|
@@ -144,7 +151,7 @@ function artifactFailureText(fileName, error) {
|
|
|
144
151
|
const name = String(fileName ?? t('结果文件')).replace(/[\r\n]+/g, ' ').trim() || t('结果文件');
|
|
145
152
|
switch (error?.code) {
|
|
146
153
|
case 'artifact-permission-required':
|
|
147
|
-
return t('结果文件「{name}
|
|
154
|
+
return t('结果文件「{name}」已生成,但机器人缺少飞书文件上传权限 im:resource。请私聊机器人执行 /repair 命令,或者在插件页面点击“补全权限”按钮并扫码。完成飞书要求的发布审批后重试。', { name });
|
|
148
155
|
case 'artifact-too-large':
|
|
149
156
|
return t('结果文件「{name}」超过飞书 30 MB 上限,未发送。', { name });
|
|
150
157
|
case 'artifact-empty':
|
|
@@ -366,6 +373,7 @@ export class FeishuHarnessBridge {
|
|
|
366
373
|
#harness;
|
|
367
374
|
#state;
|
|
368
375
|
#queues = new Map();
|
|
376
|
+
#batchInputs = new BatchInputManager();
|
|
369
377
|
#pendingInteractions = new Map();
|
|
370
378
|
#interactionKeys = new Map();
|
|
371
379
|
#resolvedQuestionReplies = new Map();
|
|
@@ -403,7 +411,6 @@ export class FeishuHarnessBridge {
|
|
|
403
411
|
#botOpenId;
|
|
404
412
|
#groupResponseMode;
|
|
405
413
|
#repair;
|
|
406
|
-
#repairOwnerOpenIds;
|
|
407
414
|
#repairAttempt = null;
|
|
408
415
|
#repairMonitorVersion = 0;
|
|
409
416
|
#repairPollIntervalMs;
|
|
@@ -436,7 +443,6 @@ export class FeishuHarnessBridge {
|
|
|
436
443
|
botOpenId,
|
|
437
444
|
groupResponseMode = FEISHU_GROUP_RESPONSE_MODES.ALL,
|
|
438
445
|
repair,
|
|
439
|
-
repairOwnerOpenIds,
|
|
440
446
|
repairPollIntervalMs = REPAIR_POLL_INTERVAL_MS,
|
|
441
447
|
repairLinkWaitMs = REPAIR_LINK_WAIT_MS,
|
|
442
448
|
cardDataTimeoutMs = CARD_DATA_TIMEOUT_MS,
|
|
@@ -473,10 +479,6 @@ export class FeishuHarnessBridge {
|
|
|
473
479
|
this.#botOpenId = nonEmptyString(botOpenId);
|
|
474
480
|
this.#groupResponseMode = normalizeFeishuGroupResponseMode(groupResponseMode);
|
|
475
481
|
this.#repair = repair ?? null;
|
|
476
|
-
const repairOwners = repairOwnerOpenIds ?? allowedSenderOpenIds;
|
|
477
|
-
this.#repairOwnerOpenIds = new Set(
|
|
478
|
-
[...(repairOwners ?? [])].filter((value) => typeof value === 'string' && value && value !== '*'),
|
|
479
|
-
);
|
|
480
482
|
this.#repairPollIntervalMs = repairPollIntervalMs;
|
|
481
483
|
this.#repairLinkWaitMs = repairLinkWaitMs;
|
|
482
484
|
this.#cardDataTimeoutMs = cardDataTimeoutMs;
|
|
@@ -542,6 +544,58 @@ export class FeishuHarnessBridge {
|
|
|
542
544
|
const processingReaction = this.#addReaction(messageId, 'OnIt');
|
|
543
545
|
const commandMessage = extractInboundMessage(event, this.#client);
|
|
544
546
|
const commandText = nonEmptyString(commandMessage.content) ?? '';
|
|
547
|
+
const batchText = event.message.message_type === 'text'
|
|
548
|
+
? nonEmptyString(extractText(event)) ?? ''
|
|
549
|
+
: '';
|
|
550
|
+
const batchCommand = event.message.message_type === 'text'
|
|
551
|
+
&& isBatchInputCommand(batchText);
|
|
552
|
+
const pending = this.#pendingInteractions.get(key);
|
|
553
|
+
const batchStatus = this.#batchInputs.status(key);
|
|
554
|
+
if (batchCommand && event.message.chat_type !== 'p2p') {
|
|
555
|
+
return this.#finishBatchResult(
|
|
556
|
+
event,
|
|
557
|
+
messageId,
|
|
558
|
+
processingReaction,
|
|
559
|
+
{ message: batchInputGroupUnsupportedMessage() },
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
if (event.message.chat_type === 'p2p'
|
|
563
|
+
&& (batchCommand || batchStatus.phase === 'collecting')) {
|
|
564
|
+
const exactBatchStart = /^\/batch$/iu.test(batchText);
|
|
565
|
+
const result = exactBatchStart
|
|
566
|
+
&& batchStatus.phase === 'idle'
|
|
567
|
+
&& (this.#queues.has(key) || pending || this.#approvals.hasPending(key))
|
|
568
|
+
? { handled: true, kind: 'busy', message: batchInputBusyMessage() }
|
|
569
|
+
: this.#batchInputs.handle(key, batchText, {
|
|
570
|
+
plainText: event.message.message_type === 'text' && Boolean(batchText),
|
|
571
|
+
});
|
|
572
|
+
if (result.handled) {
|
|
573
|
+
if (result.kind === 'submit') {
|
|
574
|
+
const submissionEvent = {
|
|
575
|
+
...event,
|
|
576
|
+
batchSubmission: { token: result.token },
|
|
577
|
+
message: {
|
|
578
|
+
...event.message,
|
|
579
|
+
message_type: 'text',
|
|
580
|
+
content: JSON.stringify({ text: result.prompt }),
|
|
581
|
+
mentions: [],
|
|
582
|
+
},
|
|
583
|
+
};
|
|
584
|
+
return this.#enqueueMessage(
|
|
585
|
+
submissionEvent,
|
|
586
|
+
messageId,
|
|
587
|
+
key,
|
|
588
|
+
processingReaction,
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
return this.#finishBatchResult(
|
|
592
|
+
event,
|
|
593
|
+
messageId,
|
|
594
|
+
processingReaction,
|
|
595
|
+
result,
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
545
599
|
// Card commands (/m, /help, /status, etc.) bypass the queue so they
|
|
546
600
|
// respond immediately even when a harness task is still streaming.
|
|
547
601
|
if (CARD_COMMAND.test(commandText)) {
|
|
@@ -599,7 +653,6 @@ export class FeishuHarnessBridge {
|
|
|
599
653
|
.finally(() => this.#acceptedMessageIds.delete(messageId));
|
|
600
654
|
return current;
|
|
601
655
|
}
|
|
602
|
-
const pending = this.#pendingInteractions.get(key);
|
|
603
656
|
const approvalReply = this.#approvals.claimReply({
|
|
604
657
|
key,
|
|
605
658
|
actor: senderOpenId(event),
|
|
@@ -691,6 +744,32 @@ export class FeishuHarnessBridge {
|
|
|
691
744
|
return this.#enqueueMessage(event, messageId, key, processingReaction);
|
|
692
745
|
}
|
|
693
746
|
|
|
747
|
+
#finishBatchResult(event, messageId, processingReaction, result) {
|
|
748
|
+
let current;
|
|
749
|
+
current = Promise.resolve()
|
|
750
|
+
.then(async () => {
|
|
751
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
752
|
+
await this.#state.markSeen(messageId);
|
|
753
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
754
|
+
this.#status.messagesReceived += 1;
|
|
755
|
+
if (result?.message) await this.#send(event.message.chat_id, result.message);
|
|
756
|
+
this.#status.lastError = null;
|
|
757
|
+
})
|
|
758
|
+
.then(() => this.#finishReaction(messageId, processingReaction, 'DONE'))
|
|
759
|
+
.catch((error) => this.#handleMessageFailure(
|
|
760
|
+
event,
|
|
761
|
+
messageId,
|
|
762
|
+
processingReaction,
|
|
763
|
+
error,
|
|
764
|
+
))
|
|
765
|
+
.finally(() => {
|
|
766
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
767
|
+
this.#commandTasks.delete(current);
|
|
768
|
+
});
|
|
769
|
+
this.#commandTasks.add(current);
|
|
770
|
+
return current;
|
|
771
|
+
}
|
|
772
|
+
|
|
694
773
|
#enqueueMessage(event, messageId, key, processingReaction, {
|
|
695
774
|
releaseMessageId = true,
|
|
696
775
|
alreadyRecorded = false,
|
|
@@ -725,6 +804,9 @@ export class FeishuHarnessBridge {
|
|
|
725
804
|
async #handleMessageFailure(event, messageId, processingReaction, error) {
|
|
726
805
|
if (error?.code === 'turn-stopped') {
|
|
727
806
|
await this.#removeProcessingReaction(messageId, processingReaction);
|
|
807
|
+
if (error?.batchInputMessage) {
|
|
808
|
+
await this.#send(event.message.chat_id, error.batchInputMessage).catch(() => undefined);
|
|
809
|
+
}
|
|
728
810
|
return;
|
|
729
811
|
}
|
|
730
812
|
if (this.#signal?.aborted) {
|
|
@@ -736,7 +818,8 @@ export class FeishuHarnessBridge {
|
|
|
736
818
|
await this.#finishReaction(messageId, processingReaction, 'ERROR');
|
|
737
819
|
await this.#send(
|
|
738
820
|
event.message.chat_id,
|
|
739
|
-
|
|
821
|
+
error?.batchInputMessage
|
|
822
|
+
?? inboundFileUserMessage(error)
|
|
740
823
|
?? imagePromptUserMessage(error)
|
|
741
824
|
?? t('处理失败,请稍后重试。如果问题持续,请在 DeepSeek Harness 的飞书插件页面检查连接状态。'),
|
|
742
825
|
).catch(() => undefined);
|
|
@@ -939,12 +1022,38 @@ export class FeishuHarnessBridge {
|
|
|
939
1022
|
}
|
|
940
1023
|
|
|
941
1024
|
this.#logger.info?.(`[dsh-feishu] processing ${event.message.chat_type} message ${messageId}`);
|
|
1025
|
+
const batchSubmission = event.batchSubmission ?? null;
|
|
1026
|
+
let batchAskCompleted = false;
|
|
942
1027
|
try {
|
|
943
|
-
const receipt = await this.#answerWithStream(event, key, message
|
|
1028
|
+
const receipt = await this.#answerWithStream(event, key, message, {
|
|
1029
|
+
onAskComplete: batchSubmission
|
|
1030
|
+
? () => {
|
|
1031
|
+
batchAskCompleted = this.#batchInputs.complete(
|
|
1032
|
+
key,
|
|
1033
|
+
batchSubmission.token,
|
|
1034
|
+
).completed;
|
|
1035
|
+
}
|
|
1036
|
+
: undefined,
|
|
1037
|
+
});
|
|
944
1038
|
this.#status.messagesReplied += 1;
|
|
945
1039
|
this.#status.lastReplyAt = new Date().toISOString();
|
|
946
1040
|
this.#status.lastError = null;
|
|
947
1041
|
return receipt;
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
if (batchSubmission && !batchAskCompleted) {
|
|
1044
|
+
if (error?.code === 'turn-stopped') {
|
|
1045
|
+
this.#batchInputs.complete(key, batchSubmission.token);
|
|
1046
|
+
throw error;
|
|
1047
|
+
}
|
|
1048
|
+
const failed = this.#batchInputs.fail(key, batchSubmission.token);
|
|
1049
|
+
if (failed.retained) {
|
|
1050
|
+
const batchError = new Error(error?.message ?? String(error), { cause: error });
|
|
1051
|
+
batchError.code = error?.code;
|
|
1052
|
+
batchError.batchInputMessage = failed.message;
|
|
1053
|
+
throw batchError;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
throw error;
|
|
948
1057
|
} finally {
|
|
949
1058
|
await this.#cancelPendingInteraction(key);
|
|
950
1059
|
await this.#approvals.closeRoute(key);
|
|
@@ -961,13 +1070,8 @@ export class FeishuHarnessBridge {
|
|
|
961
1070
|
return;
|
|
962
1071
|
}
|
|
963
1072
|
const actorOpenId = strictSenderOpenId(event);
|
|
964
|
-
if (!actorOpenId
|
|
965
|
-
await this.#send(
|
|
966
|
-
event.message.chat_id,
|
|
967
|
-
this.#repairOwnerOpenIds.size === 0
|
|
968
|
-
? t('当前机器人没有可验证的接入者身份,不能从聊天发起修复;请先在插件页设置管理员。')
|
|
969
|
-
: t('此操作只能由机器人接入者在私聊中发起,未进行任何修改。'),
|
|
970
|
-
);
|
|
1073
|
+
if (!actorOpenId) {
|
|
1074
|
+
await this.#send(event.message.chat_id, t('无法识别当前发送者,未发起修复。'));
|
|
971
1075
|
return;
|
|
972
1076
|
}
|
|
973
1077
|
if (!this.#repair) {
|
|
@@ -996,7 +1100,7 @@ export class FeishuHarnessBridge {
|
|
|
996
1100
|
return;
|
|
997
1101
|
}
|
|
998
1102
|
if (attempt.actorOpenId !== actorOpenId) {
|
|
999
|
-
await this.#send(chatId, t('
|
|
1103
|
+
await this.#send(chatId, t('另一位用户正在修复该机器人,本次不会显示其授权信息。'));
|
|
1000
1104
|
return;
|
|
1001
1105
|
}
|
|
1002
1106
|
if (operation === 'cancel') {
|
|
@@ -1048,21 +1152,53 @@ export class FeishuHarnessBridge {
|
|
|
1048
1152
|
|
|
1049
1153
|
async #startRepair({ actorOpenId, chatId }) {
|
|
1050
1154
|
const previous = this.#repairAttempt;
|
|
1155
|
+
let restarted = false;
|
|
1051
1156
|
if (previous && REPAIR_ACTIVE_STATES.has(previous.snapshot.state)) {
|
|
1052
1157
|
if (previous.actorOpenId !== actorOpenId) {
|
|
1053
|
-
await this.#send(chatId, t('
|
|
1158
|
+
await this.#send(chatId, t('另一位用户正在修复该机器人,本次不会显示其授权信息。'));
|
|
1054
1159
|
return;
|
|
1055
1160
|
}
|
|
1161
|
+
let current;
|
|
1056
1162
|
try {
|
|
1057
|
-
|
|
1058
|
-
if (REPAIR_ACTIVE_STATES.has(current.state) && previous.verificationUrl) {
|
|
1059
|
-
await this.#sendRepairLink(chatId, previous.verificationUrl, current, { existing: true });
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1163
|
+
current = await this.#refreshRepairAttempt(previous);
|
|
1062
1164
|
} catch {
|
|
1063
1165
|
await this.#send(chatId, t('暂时无法查询修复状态,请稍后重试。'));
|
|
1064
1166
|
return;
|
|
1065
1167
|
}
|
|
1168
|
+
// Once Feishu has accepted the update, starting another attempt could
|
|
1169
|
+
// race the credential swap and callback probe. Status commands remain
|
|
1170
|
+
// available while that non-cancellable convergence is in progress.
|
|
1171
|
+
if (current.state === 'saving') {
|
|
1172
|
+
await this.#send(chatId, this.#repairStatusText(current));
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
// Feishu launcher links carry one-time user codes. Opening one with the
|
|
1176
|
+
// wrong Open Platform account can consume it even though authorization
|
|
1177
|
+
// did not succeed, so a fresh bare /repair must replace a still-waiting
|
|
1178
|
+
// attempt instead of redisplaying the same unusable URL.
|
|
1179
|
+
if (REPAIR_AUTHORIZATION_STATES.has(current.state)) {
|
|
1180
|
+
let cancelled;
|
|
1181
|
+
try {
|
|
1182
|
+
cancelled = repairSnapshot(
|
|
1183
|
+
await this.#repair.cancel(this.#repairArgs(previous)),
|
|
1184
|
+
{ botId: this.#botId },
|
|
1185
|
+
);
|
|
1186
|
+
previous.snapshot = cancelled;
|
|
1187
|
+
} catch {
|
|
1188
|
+
await this.#send(chatId, t('暂时无法取消旧修复任务,未生成新链接;请稍后重试。'));
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
if (!REPAIR_TERMINAL_STATES.has(cancelled.state)) {
|
|
1192
|
+
await this.#send(chatId, this.#repairStatusText(cancelled));
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
previous.stopped = true;
|
|
1196
|
+
this.#repairMonitorVersion += 1;
|
|
1197
|
+
restarted = cancelled.state === 'cancelled';
|
|
1198
|
+
} else if (REPAIR_TERMINAL_STATES.has(current.state)) {
|
|
1199
|
+
previous.stopped = true;
|
|
1200
|
+
this.#repairMonitorVersion += 1;
|
|
1201
|
+
}
|
|
1066
1202
|
}
|
|
1067
1203
|
|
|
1068
1204
|
let snapshot;
|
|
@@ -1111,7 +1247,7 @@ export class FeishuHarnessBridge {
|
|
|
1111
1247
|
await this.#send(chatId, t('飞书未返回授权链接,已中止本次修复。'));
|
|
1112
1248
|
return;
|
|
1113
1249
|
}
|
|
1114
|
-
await this.#sendRepairLink(chatId, attempt.verificationUrl, snapshot);
|
|
1250
|
+
await this.#sendRepairLink(chatId, attempt.verificationUrl, snapshot, { restarted });
|
|
1115
1251
|
this.#monitorRepair(attempt);
|
|
1116
1252
|
}
|
|
1117
1253
|
|
|
@@ -1189,15 +1325,15 @@ export class FeishuHarnessBridge {
|
|
|
1189
1325
|
});
|
|
1190
1326
|
}
|
|
1191
1327
|
|
|
1192
|
-
async #sendRepairLink(chatId, url, snapshot, {
|
|
1328
|
+
async #sendRepairLink(chatId, url, snapshot, { restarted = false } = {}) {
|
|
1193
1329
|
const remaining = snapshot.remainingSeconds
|
|
1194
1330
|
?? (snapshot.expiresAt ? Math.max(0, Math.ceil((snapshot.expiresAt - Date.now()) / 1000)) : null);
|
|
1195
1331
|
const expiry = remaining === null
|
|
1196
1332
|
? t('链接为短期有效')
|
|
1197
1333
|
: t('链接约 {minutes} 分钟后过期', { minutes: Math.max(1, Math.ceil(remaining / 60)) });
|
|
1198
1334
|
await this.#send(chatId, [
|
|
1199
|
-
|
|
1200
|
-
t('
|
|
1335
|
+
restarted ? t('旧授权链接已作废,已生成新的修复链接。') : t('🔧 准备补全权限与回调。'),
|
|
1336
|
+
t('本次最多增量添加三项:卡片回调 card.action.trigger;飞书显示为“获取单聊、群组消息”的租户权限 im:message:readonly(用于读取用户消息中的图片或文件);以及 im:resource(用于上传机器人发送的图片或文件)。确认页只会显示当前缺少的项;若出现上述范围之外的配置,请取消。'),
|
|
1201
1337
|
'',
|
|
1202
1338
|
t('当前设备直接打开:'),
|
|
1203
1339
|
url,
|
|
@@ -2937,10 +3073,16 @@ export class FeishuHarnessBridge {
|
|
|
2937
3073
|
};
|
|
2938
3074
|
}
|
|
2939
3075
|
|
|
2940
|
-
async #answerWithStream(event, key, message) {
|
|
3076
|
+
async #answerWithStream(event, key, message, { onAskComplete } = {}) {
|
|
2941
3077
|
const chatId = event.message.chat_id;
|
|
2942
3078
|
const messageId = event.message.message_id;
|
|
2943
3079
|
const text = message.content;
|
|
3080
|
+
let askCompleted = false;
|
|
3081
|
+
const markAskComplete = () => {
|
|
3082
|
+
if (askCompleted) return;
|
|
3083
|
+
askCompleted = true;
|
|
3084
|
+
onAskComplete?.();
|
|
3085
|
+
};
|
|
2944
3086
|
const content = hasInboundImages(message)
|
|
2945
3087
|
? await promptContentForMessage(message, { signal: this.#signal })
|
|
2946
3088
|
: undefined;
|
|
@@ -2955,6 +3097,7 @@ export class FeishuHarnessBridge {
|
|
|
2955
3097
|
existsOptions: { signal: this.#signal },
|
|
2956
3098
|
askOptions: this.#interactionAskOptions(event, key, message.files),
|
|
2957
3099
|
});
|
|
3100
|
+
markAskComplete();
|
|
2958
3101
|
let textReceipt;
|
|
2959
3102
|
let textSendError = null;
|
|
2960
3103
|
try {
|
|
@@ -3009,6 +3152,7 @@ export class FeishuHarnessBridge {
|
|
|
3009
3152
|
existsOptions: { signal: this.#signal },
|
|
3010
3153
|
askOptions,
|
|
3011
3154
|
});
|
|
3155
|
+
markAskComplete();
|
|
3012
3156
|
completedAnswer = completed.answer;
|
|
3013
3157
|
completedArtifacts = completed.artifacts ?? [];
|
|
3014
3158
|
await controller.setContent(answerTextForDelivery(completedAnswer, completedArtifacts));
|
|
@@ -3067,6 +3211,7 @@ export class FeishuHarnessBridge {
|
|
|
3067
3211
|
existsOptions: { signal: this.#signal },
|
|
3068
3212
|
askOptions: this.#interactionAskOptions(event, key, message.files),
|
|
3069
3213
|
});
|
|
3214
|
+
markAskComplete();
|
|
3070
3215
|
let textReceipt;
|
|
3071
3216
|
let textSendError = null;
|
|
3072
3217
|
try {
|
|
@@ -124,7 +124,7 @@ function backButton() {
|
|
|
124
124
|
* - presetCatalog: object|null (预设目录, {items,defaultId,_currentId})
|
|
125
125
|
* - modelCatalog: object|null (模型目录, {groups,current})
|
|
126
126
|
* Number fallback: 1=续写 2=新会话 3=会话列表 4=状态
|
|
127
|
-
* 5
|
|
127
|
+
* 5=补全权限 6=帮助.
|
|
128
128
|
*/
|
|
129
129
|
export function menuCard(ctx) {
|
|
130
130
|
const {
|
|
@@ -323,7 +323,7 @@ export function menuCard(ctx) {
|
|
|
323
323
|
|
|
324
324
|
// 命令与数字兜底说明
|
|
325
325
|
elements.push({ tag: 'div', text: markdown(t(
|
|
326
|
-
'**数字兜底**\n**1**工作区列表 · **2**新会话 · **3**会话列表 · **4**状态\n**5
|
|
326
|
+
'**数字兜底**\n**1**工作区列表 · **2**新会话 · **3**会话列表 · **4**状态\n**5**🔧补全权限 · **6**帮助',
|
|
327
327
|
)) });
|
|
328
328
|
return cardWith(t('🤖 助手中心'), elements);
|
|
329
329
|
}
|
|
@@ -495,6 +495,7 @@ export function menuHelpText() {
|
|
|
495
495
|
'',
|
|
496
496
|
'📊 状态 / 压缩',
|
|
497
497
|
'/status 连接状态',
|
|
498
|
+
'/version 查看插件版本',
|
|
498
499
|
'/compact 压缩当前会话上下文',
|
|
499
500
|
'/archived on/off 会话列表显示/隐藏归档',
|
|
500
501
|
'',
|
|
@@ -513,10 +514,15 @@ export function menuHelpText() {
|
|
|
513
514
|
'/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级',
|
|
514
515
|
'/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型',
|
|
515
516
|
'',
|
|
517
|
+
'📦 批量输入(仅私聊)',
|
|
518
|
+
'/batch 开始批量输入(仅私聊,最多 10 条文字)',
|
|
519
|
+
'/send 提交当前批次',
|
|
520
|
+
'/cancel 取消当前批次',
|
|
521
|
+
'',
|
|
516
522
|
'🎮 任务控制',
|
|
517
523
|
'/stop 停止当前任务',
|
|
518
524
|
'/steer 指令 给 Agent 补充指令',
|
|
519
|
-
'/repair
|
|
525
|
+
'/repair 补全飞书权限与卡片回调',
|
|
520
526
|
].map((line) => t(line)).join('\n');
|
|
521
527
|
}
|
|
522
528
|
|
|
@@ -564,14 +570,17 @@ const HELP_TEXT_COMMANDS = [
|
|
|
564
570
|
'`/reasoninglist` 或 `/reasonings` — 按序号列出当前模型可用推理等级',
|
|
565
571
|
'`/reasoning [序号、等级ID或 --default]` — 查看或切换当前推理等级',
|
|
566
572
|
'`/model [序号或完整模型ID] [推理等级ID]` — 查看或切换当前会话模型',
|
|
567
|
-
'`/
|
|
573
|
+
'`/batch` — 开启批量输入(仅私聊,最多 10 条文字)',
|
|
574
|
+
'`/send` — 提交当前批次',
|
|
575
|
+
'`/cancel` — 取消当前批次',
|
|
576
|
+
'`/repair` — 补全飞书权限与卡片回调',
|
|
568
577
|
].join('\n');
|
|
569
578
|
|
|
570
579
|
const HELP_NUMBER_FALLBACK = [
|
|
571
580
|
'**💡 数字兜底**',
|
|
572
581
|
'回复数字快速操作:',
|
|
573
582
|
'**1**工作区列表 · **2**新会话 · **3**会话/关注',
|
|
574
|
-
|
|
583
|
+
'**4**状态 · **5**补全权限 · **6**帮助',
|
|
575
584
|
].join('\n');
|
|
576
585
|
|
|
577
586
|
export function helpCard(extraTextLines = []) {
|
|
@@ -581,7 +590,10 @@ export function helpCard(extraTextLines = []) {
|
|
|
581
590
|
const elements = [
|
|
582
591
|
{ tag: 'div', text: markdown(t(HELP_CARD_FEATURES)) },
|
|
583
592
|
{ tag: 'hr' },
|
|
584
|
-
{ tag: 'div', text: markdown(
|
|
593
|
+
{ tag: 'div', text: markdown([
|
|
594
|
+
t(HELP_TEXT_COMMANDS),
|
|
595
|
+
t('`/version` — 查看插件版本'),
|
|
596
|
+
].join('\n') + extraText) },
|
|
585
597
|
{ tag: 'hr' },
|
|
586
598
|
{ tag: 'div', text: markdown(t(HELP_NUMBER_FALLBACK)) },
|
|
587
599
|
{ tag: 'hr' },
|
|
@@ -267,7 +267,6 @@ export class FeishuRuntime {
|
|
|
267
267
|
botOpenId: this.#botOpenId,
|
|
268
268
|
groupResponseMode: this.#groupResponseMode,
|
|
269
269
|
repair: this.#repair,
|
|
270
|
-
repairOwnerOpenIds: new Set(this.#ownerOpenIds.filter((value) => value !== '*')),
|
|
271
270
|
replyTimeoutMs: this.#replyTimeoutMs,
|
|
272
271
|
signal,
|
|
273
272
|
logger: this.#logger,
|
|
@@ -5,7 +5,7 @@ const FEISHU_MISSING_MESSAGE_SCOPE_CODE = 99991672;
|
|
|
5
5
|
const FEISHU_ERROR_BODY_LIMIT = 64 * 1024;
|
|
6
6
|
const FEISHU_ERROR_BODY_TIMEOUT_MS = 1_000;
|
|
7
7
|
const FEISHU_IMAGE_PERMISSION_MESSAGE =
|
|
8
|
-
'
|
|
8
|
+
'飞书机器人缺少图片读取权限 im:message:readonly(飞书显示为“获取单聊、群组消息”)。请私聊机器人执行 /repair 命令,或者在插件页面点击“补全权限”按钮并扫码。按飞书提示发布新版本、完成必要审批后,再重新发送图片。';
|
|
9
9
|
|
|
10
10
|
export function conversationKey(event) {
|
|
11
11
|
const chatType = event?.message?.chat_type;
|
|
@@ -869,22 +869,6 @@ export class MultiBotDshFeishuController {
|
|
|
869
869
|
'Feishu returned credentials for a different account domain.',
|
|
870
870
|
);
|
|
871
871
|
}
|
|
872
|
-
if (record.initiator.actorOpenId && record.initiator.actorOpenId !== ownerOpenId) {
|
|
873
|
-
throw this.#callbackRepairError(
|
|
874
|
-
record,
|
|
875
|
-
'repair_owner_mismatch',
|
|
876
|
-
'The Feishu repair was confirmed by a different operator.',
|
|
877
|
-
);
|
|
878
|
-
}
|
|
879
|
-
if (!target.ownerOpenIds.includes(ALL_VISIBLE_SENDERS)
|
|
880
|
-
&& !target.ownerOpenIds.includes(ownerOpenId)) {
|
|
881
|
-
throw this.#callbackRepairError(
|
|
882
|
-
record,
|
|
883
|
-
'repair_owner_mismatch',
|
|
884
|
-
'The Feishu repair operator is not an owner of this configured bot.',
|
|
885
|
-
);
|
|
886
|
-
}
|
|
887
|
-
|
|
888
872
|
record.stage = 'verifying_identity';
|
|
889
873
|
let verified;
|
|
890
874
|
try {
|
|
@@ -1008,7 +992,7 @@ export class MultiBotDshFeishuController {
|
|
|
1008
992
|
record.stage = 'awaiting_callback';
|
|
1009
993
|
try {
|
|
1010
994
|
const proof = await runtime.beginCardActionProbe({
|
|
1011
|
-
expectedOperatorOpenId: ownerOpenId,
|
|
995
|
+
expectedOperatorOpenId: record.initiator.actorOpenId ?? ownerOpenId,
|
|
1012
996
|
timeoutMs: this.#callbackProbeTimeoutMs,
|
|
1013
997
|
...(record.initiator.chatId ? { chatId: record.initiator.chatId } : {}),
|
|
1014
998
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { RegistrationManager } from './registration-manager.mjs';
|
|
2
2
|
|
|
3
3
|
export const CARD_ACTION_CALLBACK = 'card.action.trigger';
|
|
4
|
+
export const FEISHU_MESSAGE_READ_SCOPE = 'im:message:readonly';
|
|
5
|
+
export const FEISHU_RESOURCE_SCOPE = 'im:resource';
|
|
4
6
|
export const CALLBACK_REPAIR_OPERATION = 'callback_repair';
|
|
5
7
|
|
|
6
8
|
function accountsDomain(domain) {
|
|
@@ -71,8 +73,10 @@ export function assertCallbackRepairUrl(value, expectedAppId, domain = 'feishu')
|
|
|
71
73
|
/**
|
|
72
74
|
* One targeted update attempt for an existing Feishu app. It intentionally
|
|
73
75
|
* shares RegistrationManager's polling/state implementation while fixing the
|
|
74
|
-
* update manifest in one place so callers
|
|
75
|
-
*
|
|
76
|
+
* update manifest in one place so callers can add only the card callback, the
|
|
77
|
+
* message-read scope needed to download user-sent media, and the resource
|
|
78
|
+
* scope needed to upload bot-sent images/files, without adding unrelated
|
|
79
|
+
* scopes, events, presets, or createOnly.
|
|
76
80
|
*/
|
|
77
81
|
export class CallbackRepairManager {
|
|
78
82
|
#manager;
|
|
@@ -106,6 +110,7 @@ export class CallbackRepairManager {
|
|
|
106
110
|
appId: this.#appId,
|
|
107
111
|
addons: {
|
|
108
112
|
preset: false,
|
|
113
|
+
scopes: { tenant: [FEISHU_MESSAGE_READ_SCOPE, FEISHU_RESOURCE_SCOPE] },
|
|
109
114
|
callbacks: { items: [CARD_ACTION_CALLBACK] },
|
|
110
115
|
},
|
|
111
116
|
});
|