@xmanrui/dsh-im 3.1.0 → 3.2.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 +15 -0
- package/README.md +15 -0
- package/lib/client.js +1653 -333
- package/lib/index.js +235 -223
- package/package.json +5 -1
- package/plugin-src/client/channels/dingtalk/api.js +3 -0
- package/plugin-src/client/channels/dingtalk/index.js +18 -5
- package/plugin-src/client/channels/feishu/api.js +3 -0
- package/plugin-src/client/channels/feishu/index.js +18 -5
- package/plugin-src/client/channels/qq/api.js +3 -0
- package/plugin-src/client/channels/qq/index.js +13 -0
- package/plugin-src/client/channels/shared/token-api.js +3 -0
- package/plugin-src/client/channels/shared/token-channel.js +13 -1
- package/plugin-src/client/channels/wecom/api.js +3 -0
- package/plugin-src/client/channels/wecom/index.js +13 -0
- package/plugin-src/client/channels/weixin/api.js +3 -0
- package/plugin-src/client/channels/weixin/index.js +19 -5
- package/plugin-src/client/channels/whatsapp/api.js +3 -0
- package/plugin-src/client/channels/whatsapp/index.js +13 -0
- package/plugin-src/client/context-enhancement.js +275 -0
- package/plugin-src/client/i18n.js +54 -3
- package/plugin-src/client/styles.js +78 -0
- package/plugin-src/client/update-panel.js +108 -7
- package/plugin-src/host/channels/dingtalk/production.mjs +1 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
- package/plugin-src/host/channels/feishu/production.mjs +3 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +13 -0
- package/plugin-src/host/channels/qq/production.mjs +1 -0
- package/plugin-src/host/channels/qq/rpc.mjs +11 -0
- package/plugin-src/host/channels/shared/context-enhancement-rpc.mjs +17 -0
- package/plugin-src/host/channels/shared/production.mjs +1 -0
- package/plugin-src/host/channels/shared/rpc.mjs +9 -0
- package/plugin-src/host/channels/shared/workspace-rpc.mjs +1 -0
- package/plugin-src/host/channels/slack/production.mjs +1 -0
- package/plugin-src/host/channels/slack/rpc.mjs +10 -0
- package/plugin-src/host/channels/wecom/production.mjs +1 -0
- package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
- package/plugin-src/host/channels/weixin/production.mjs +1 -0
- package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +1 -0
- package/plugin-src/host/channels/whatsapp/rpc.mjs +11 -0
- package/plugin-src/host/update-runtime.mjs +3 -2
- package/plugin-src/host/update-service.mjs +2 -0
- package/scripts/verify-package.mjs +15 -2
- package/src/channels/dingtalk/dingtalk-api.mjs +39 -16
- package/src/channels/dingtalk/dingtalk-bridge.mjs +52 -23
- package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -1
- package/src/channels/discord/discord-runtime.mjs +18 -4
- package/src/channels/feishu/bridge.mjs +20 -4
- package/src/channels/feishu/feishu-channel.mjs +100 -44
- package/src/channels/feishu/feishu-runtime.mjs +4 -0
- package/src/channels/qq/qq-bridge.mjs +20 -4
- package/src/channels/qq/qq-runtime.mjs +4 -0
- package/src/channels/shared/bot-workspace-store.mjs +103 -6
- package/src/channels/shared/context-enhancement.mjs +135 -0
- package/src/channels/shared/i18n-en/feishu.mjs +2 -0
- package/src/channels/shared/text-harness-bridge.mjs +19 -4
- package/src/channels/slack/slack-runtime.mjs +4 -0
- package/src/channels/telegram/telegram-runtime.mjs +24 -2
- package/src/channels/wecom/wecom-bridge.mjs +18 -3
- package/src/channels/wecom/wecom-runtime.mjs +4 -0
- package/src/channels/weixin/weixin-bridge.mjs +19 -4
- package/src/channels/weixin/weixin-runtime.mjs +4 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +5 -0
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from '../shared/preset-command.mjs';
|
|
29
29
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
30
30
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
31
|
+
import { captureContextEnhancement, enhanceContextContent } from '../shared/context-enhancement.mjs';
|
|
31
32
|
import {
|
|
32
33
|
BatchInputManager,
|
|
33
34
|
batchInputBusyMessage,
|
|
@@ -251,7 +252,11 @@ function conversationKey(message, sender) {
|
|
|
251
252
|
|
|
252
253
|
function cardTarget(message, sender) {
|
|
253
254
|
if (String(message?.conversationType) === '2') {
|
|
254
|
-
return {
|
|
255
|
+
return {
|
|
256
|
+
type: 'group',
|
|
257
|
+
openConversationId: nonEmptyString(message?.conversationId),
|
|
258
|
+
atUserIds: { [sender]: nonEmptyString(message?.senderNick) ?? sender },
|
|
259
|
+
};
|
|
255
260
|
}
|
|
256
261
|
return { type: 'user', userId: sender };
|
|
257
262
|
}
|
|
@@ -368,6 +373,7 @@ export class DingtalkHarnessBridge {
|
|
|
368
373
|
#clientSecret;
|
|
369
374
|
#harness;
|
|
370
375
|
#state;
|
|
376
|
+
#contextEnhancement;
|
|
371
377
|
#status;
|
|
372
378
|
#logger;
|
|
373
379
|
#replyTimeoutMs;
|
|
@@ -379,7 +385,8 @@ export class DingtalkHarnessBridge {
|
|
|
379
385
|
#interactionKeys = new Map();
|
|
380
386
|
#interactionTasks = new Set();
|
|
381
387
|
#commandTasks = new Set();
|
|
382
|
-
|
|
388
|
+
// Keep the accepted configuration through the existing queue/reply lifecycle.
|
|
389
|
+
#acceptedMessageIds = new Map();
|
|
383
390
|
#approvals;
|
|
384
391
|
#batchInputs = new BatchInputManager();
|
|
385
392
|
|
|
@@ -389,6 +396,7 @@ export class DingtalkHarnessBridge {
|
|
|
389
396
|
clientSecret,
|
|
390
397
|
harness,
|
|
391
398
|
state,
|
|
399
|
+
contextEnhancement,
|
|
392
400
|
status = createDingtalkBridgeStatus(),
|
|
393
401
|
logger = console,
|
|
394
402
|
replyTimeoutMs = 600_000,
|
|
@@ -406,6 +414,7 @@ export class DingtalkHarnessBridge {
|
|
|
406
414
|
this.#clientSecret = clientSecret.trim();
|
|
407
415
|
this.#harness = harness;
|
|
408
416
|
this.#state = state;
|
|
417
|
+
this.#contextEnhancement = contextEnhancement;
|
|
409
418
|
this.#status = status;
|
|
410
419
|
this.#logger = logger;
|
|
411
420
|
this.#approvals = new HarnessApprovalQueue({ label: 'DingTalk', logger });
|
|
@@ -424,13 +433,17 @@ export class DingtalkHarnessBridge {
|
|
|
424
433
|
return structuredClone(this.#status);
|
|
425
434
|
}
|
|
426
435
|
|
|
427
|
-
accept(message) {
|
|
436
|
+
accept(message, { contextSnapshot } = {}) {
|
|
428
437
|
if (this.#signal?.aborted) return Promise.resolve();
|
|
429
438
|
const messageId = nonEmptyString(message?.msgId);
|
|
430
439
|
const sender = senderStaffId(message);
|
|
431
440
|
if (!messageId || !sender || this.#state.hasSeen(messageId)
|
|
432
441
|
|| this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
|
|
433
|
-
this.#acceptedMessageIds.
|
|
442
|
+
this.#acceptedMessageIds.set(messageId, contextSnapshot === undefined ? captureContextEnhancement(
|
|
443
|
+
this.#contextEnhancement,
|
|
444
|
+
message.conversationType === '1' || message.conversationType === 1 ? 'direct'
|
|
445
|
+
: message.conversationType === '2' || message.conversationType === 2 ? 'group' : null,
|
|
446
|
+
) : contextSnapshot);
|
|
434
447
|
|
|
435
448
|
let key;
|
|
436
449
|
try {
|
|
@@ -551,7 +564,7 @@ export class DingtalkHarnessBridge {
|
|
|
551
564
|
`[dsh-dingtalk] failed to process a command [${failure.referenceId}]`,
|
|
552
565
|
safeErrorDiagnostic(error),
|
|
553
566
|
);
|
|
554
|
-
return this.#send(sessionWebhook, messageFailureText(failure)).catch(() => undefined);
|
|
567
|
+
return this.#send(sessionWebhook, messageFailureText(failure), this.#atUsersFor(message)).catch(() => undefined);
|
|
555
568
|
}).finally(() => {
|
|
556
569
|
this.#acceptedMessageIds.delete(messageId);
|
|
557
570
|
this.#commandTasks.delete(task);
|
|
@@ -573,7 +586,7 @@ export class DingtalkHarnessBridge {
|
|
|
573
586
|
: null,
|
|
574
587
|
isQuestionPending: () => this.#pendingInteractions.has(key),
|
|
575
588
|
send: sessionWebhook
|
|
576
|
-
? (reply) => this.#send(sessionWebhook, reply)
|
|
589
|
+
? (reply) => this.#send(sessionWebhook, reply, this.#atUsersFor(message))
|
|
577
590
|
: async () => undefined,
|
|
578
591
|
});
|
|
579
592
|
if (approvalReply) {
|
|
@@ -807,7 +820,7 @@ export class DingtalkHarnessBridge {
|
|
|
807
820
|
]);
|
|
808
821
|
}
|
|
809
822
|
for (const reply of result?.messages ?? [result?.message]) {
|
|
810
|
-
if (reply) await this.#send(sessionWebhook, reply);
|
|
823
|
+
if (reply) await this.#send(sessionWebhook, reply, this.#atUsersFor(message));
|
|
811
824
|
}
|
|
812
825
|
this.#status.lastError = null;
|
|
813
826
|
}
|
|
@@ -885,23 +898,23 @@ export class DingtalkHarnessBridge {
|
|
|
885
898
|
let batchSettled = batchSubmission === null;
|
|
886
899
|
try {
|
|
887
900
|
if (!text && !hasImages && !hasFiles) {
|
|
888
|
-
await this.#send(sessionWebhook, t('目前支持文字、图片和文件消息。'));
|
|
901
|
+
await this.#send(sessionWebhook, t('目前支持文字、图片和文件消息。'), this.#atUsersFor(message));
|
|
889
902
|
return;
|
|
890
903
|
}
|
|
891
904
|
|
|
892
905
|
const command = text.toLowerCase();
|
|
893
906
|
if (isPlainText && !hasImages && !hasFiles && command === '/help') {
|
|
894
|
-
await this.#send(sessionWebhook, helpText());
|
|
907
|
+
await this.#send(sessionWebhook, helpText(), this.#atUsersFor(message));
|
|
895
908
|
return;
|
|
896
909
|
}
|
|
897
910
|
if (isPlainText && !hasImages && !hasFiles && command === '/status') {
|
|
898
911
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
899
|
-
await this.#send(sessionWebhook, t('钉钉机器人与 DeepSeek Harness 连接正常。'));
|
|
912
|
+
await this.#send(sessionWebhook, t('钉钉机器人与 DeepSeek Harness 连接正常。'), this.#atUsersFor(message));
|
|
900
913
|
return;
|
|
901
914
|
}
|
|
902
915
|
if (isPlainText && !hasImages && !hasFiles && command === '/new') {
|
|
903
916
|
await this.#state.clearSession(key);
|
|
904
|
-
await this.#send(sessionWebhook, t('已开启新会话。请发送你的问题。'));
|
|
917
|
+
await this.#send(sessionWebhook, t('已开启新会话。请发送你的问题。'), this.#atUsersFor(message));
|
|
905
918
|
return;
|
|
906
919
|
}
|
|
907
920
|
const workspaceCommand = isPlainText && !hasImages && !hasFiles
|
|
@@ -909,7 +922,7 @@ export class DingtalkHarnessBridge {
|
|
|
909
922
|
: null;
|
|
910
923
|
if (workspaceCommand) {
|
|
911
924
|
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
|
912
|
-
await this.#send(sessionWebhook, reply);
|
|
925
|
+
await this.#send(sessionWebhook, reply, this.#atUsersFor(message));
|
|
913
926
|
}
|
|
914
927
|
return;
|
|
915
928
|
}
|
|
@@ -923,13 +936,21 @@ export class DingtalkHarnessBridge {
|
|
|
923
936
|
)
|
|
924
937
|
: null;
|
|
925
938
|
if (compactCommand) {
|
|
926
|
-
await this.#send(sessionWebhook, compactCommand.message);
|
|
939
|
+
await this.#send(sessionWebhook, compactCommand.message, this.#atUsersFor(message));
|
|
927
940
|
return;
|
|
928
941
|
}
|
|
929
942
|
|
|
930
|
-
|
|
943
|
+
let content = hasImages
|
|
931
944
|
? await promptContentForMessage(promptMessage, { signal: this.#signal })
|
|
932
945
|
: undefined;
|
|
946
|
+
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
947
|
+
if (snapshot) {
|
|
948
|
+
content = enhanceContextContent(content ?? text, snapshot, () => ({
|
|
949
|
+
channel: 'dingtalk',
|
|
950
|
+
senderId: sender,
|
|
951
|
+
senderName: message.senderNick,
|
|
952
|
+
}));
|
|
953
|
+
}
|
|
933
954
|
if (typeof this.#api.createAiCard === 'function'
|
|
934
955
|
&& typeof this.#api.updateAiCard === 'function'
|
|
935
956
|
&& typeof this.#api.finishAiCard === 'function') {
|
|
@@ -947,7 +968,7 @@ export class DingtalkHarnessBridge {
|
|
|
947
968
|
harness: this.#harness,
|
|
948
969
|
state: this.#state,
|
|
949
970
|
key,
|
|
950
|
-
...(
|
|
971
|
+
...(content !== undefined ? { content } : { text }),
|
|
951
972
|
createOptions: { signal: this.#signal },
|
|
952
973
|
existsOptions: { signal: this.#signal },
|
|
953
974
|
askOptions: {
|
|
@@ -988,7 +1009,7 @@ export class DingtalkHarnessBridge {
|
|
|
988
1009
|
textReceipt = createDeliveryReceipt({
|
|
989
1010
|
deliveryId: messageId,
|
|
990
1011
|
presentation: 'dingtalk-text',
|
|
991
|
-
providerMessageIds: await this.#send(sessionWebhook, answerText),
|
|
1012
|
+
providerMessageIds: await this.#send(sessionWebhook, answerText, this.#atUsersFor(message)),
|
|
992
1013
|
});
|
|
993
1014
|
}
|
|
994
1015
|
} catch (error) {
|
|
@@ -1049,7 +1070,7 @@ export class DingtalkHarnessBridge {
|
|
|
1049
1070
|
? `${errorText}\n\n${batchFailureMessage}`
|
|
1050
1071
|
: errorText;
|
|
1051
1072
|
const streamed = cardStarted && await cardStream.finish(visibleError);
|
|
1052
|
-
if (!streamed) await this.#send(sessionWebhook, visibleError);
|
|
1073
|
+
if (!streamed) await this.#send(sessionWebhook, visibleError, this.#atUsersFor(message));
|
|
1053
1074
|
} catch {
|
|
1054
1075
|
this.#logger.error?.('[dsh-dingtalk] failed to send the safe error reply');
|
|
1055
1076
|
}
|
|
@@ -1102,7 +1123,7 @@ export class DingtalkHarnessBridge {
|
|
|
1102
1123
|
const text = message?.msgtype === 'text' ? nonEmptyString(message?.text?.content) : null;
|
|
1103
1124
|
if (!text) {
|
|
1104
1125
|
try {
|
|
1105
|
-
await this.#send(sessionWebhook, t('请用文字回答当前问题。'));
|
|
1126
|
+
await this.#send(sessionWebhook, t('请用文字回答当前问题。'), this.#atUsersFor(message));
|
|
1106
1127
|
} catch {
|
|
1107
1128
|
this.#logger.error?.('[dsh-dingtalk] failed to reject a non-text interaction reply');
|
|
1108
1129
|
}
|
|
@@ -1113,7 +1134,7 @@ export class DingtalkHarnessBridge {
|
|
|
1113
1134
|
if (!pending || pending !== expected || pending.submitting) {
|
|
1114
1135
|
if (claimed && (!pending || pending !== expected)) {
|
|
1115
1136
|
try {
|
|
1116
|
-
await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT));
|
|
1137
|
+
await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT), this.#atUsersFor(message));
|
|
1117
1138
|
} catch {
|
|
1118
1139
|
this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice');
|
|
1119
1140
|
}
|
|
@@ -1178,7 +1199,7 @@ export class DingtalkHarnessBridge {
|
|
|
1178
1199
|
if (error?.code === 'interaction-not-pending') {
|
|
1179
1200
|
this.#clearPendingInteraction(key, pending.interactionId);
|
|
1180
1201
|
try {
|
|
1181
|
-
await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT));
|
|
1202
|
+
await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT), this.#atUsersFor(message));
|
|
1182
1203
|
} catch {
|
|
1183
1204
|
this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice');
|
|
1184
1205
|
}
|
|
@@ -1191,7 +1212,7 @@ export class DingtalkHarnessBridge {
|
|
|
1191
1212
|
this.#status.lastError = t('回答提交失败。');
|
|
1192
1213
|
this.#logger.error?.('[dsh-dingtalk] failed to answer a Harness interaction');
|
|
1193
1214
|
try {
|
|
1194
|
-
await this.#send(sessionWebhook, t('回答提交失败,请重新发送当前问题的答案。'));
|
|
1215
|
+
await this.#send(sessionWebhook, t('回答提交失败,请重新发送当前问题的答案。'), this.#atUsersFor(message));
|
|
1195
1216
|
} catch {
|
|
1196
1217
|
this.#logger.error?.('[dsh-dingtalk] failed to send an interaction retry notice');
|
|
1197
1218
|
}
|
|
@@ -1323,7 +1344,7 @@ export class DingtalkHarnessBridge {
|
|
|
1323
1344
|
return;
|
|
1324
1345
|
}
|
|
1325
1346
|
try {
|
|
1326
|
-
await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT));
|
|
1347
|
+
await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT), this.#atUsersFor(message));
|
|
1327
1348
|
} catch {
|
|
1328
1349
|
this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice');
|
|
1329
1350
|
}
|
|
@@ -1367,7 +1388,14 @@ export class DingtalkHarnessBridge {
|
|
|
1367
1388
|
}
|
|
1368
1389
|
}
|
|
1369
1390
|
|
|
1370
|
-
|
|
1391
|
+
#atUsersFor(message) {
|
|
1392
|
+
const sender = senderStaffId(message);
|
|
1393
|
+
return String(message?.conversationType) === '2' && sender
|
|
1394
|
+
? { atUserIds: [sender] }
|
|
1395
|
+
: undefined;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
async #send(sessionWebhook, text, at) {
|
|
1371
1399
|
const providerMessageIds = [];
|
|
1372
1400
|
for (const chunk of splitDingtalkText(text, this.#maxMessageChars)) {
|
|
1373
1401
|
this.#signal?.throwIfAborted();
|
|
@@ -1376,6 +1404,7 @@ export class DingtalkHarnessBridge {
|
|
|
1376
1404
|
clientSecret: this.#clientSecret,
|
|
1377
1405
|
sessionWebhook,
|
|
1378
1406
|
text: chunk,
|
|
1407
|
+
at,
|
|
1379
1408
|
signal: this.#signal,
|
|
1380
1409
|
});
|
|
1381
1410
|
providerMessageIds.push(...providerMessageIdsFor(result));
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from './dingtalk-bridge.mjs';
|
|
6
6
|
import { sendRememberedConnectionTest } from '../shared/connection-test.mjs';
|
|
7
7
|
import { t } from '../shared/i18n.mjs';
|
|
8
|
+
import { captureContextEnhancement } from '../shared/context-enhancement.mjs';
|
|
8
9
|
|
|
9
10
|
function nonEmptyString(value) {
|
|
10
11
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
@@ -128,6 +129,7 @@ export class DingtalkRuntime {
|
|
|
128
129
|
#clientSecret;
|
|
129
130
|
#harness;
|
|
130
131
|
#state;
|
|
132
|
+
#contextEnhancement;
|
|
131
133
|
#logger;
|
|
132
134
|
#replyTimeoutMs;
|
|
133
135
|
#maxMessageChars;
|
|
@@ -149,6 +151,7 @@ export class DingtalkRuntime {
|
|
|
149
151
|
clientSecret,
|
|
150
152
|
harness,
|
|
151
153
|
state,
|
|
154
|
+
contextEnhancement,
|
|
152
155
|
logger = console,
|
|
153
156
|
replyTimeoutMs = 600_000,
|
|
154
157
|
maxMessageChars = 4_000,
|
|
@@ -166,6 +169,7 @@ export class DingtalkRuntime {
|
|
|
166
169
|
this.#clientSecret = clientSecret.trim();
|
|
167
170
|
this.#harness = harness;
|
|
168
171
|
this.#state = state;
|
|
172
|
+
this.#contextEnhancement = contextEnhancement;
|
|
169
173
|
this.#logger = logger;
|
|
170
174
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
171
175
|
this.#maxMessageChars = maxMessageChars;
|
|
@@ -233,6 +237,7 @@ export class DingtalkRuntime {
|
|
|
233
237
|
approvedSenders: this.#config.approvedSenders,
|
|
234
238
|
harness: this.#harness,
|
|
235
239
|
state: this.#state,
|
|
240
|
+
contextEnhancement: this.#contextEnhancement,
|
|
236
241
|
status: this.#status,
|
|
237
242
|
logger: this.#logger,
|
|
238
243
|
replyTimeoutMs: this.#replyTimeoutMs,
|
|
@@ -268,6 +273,15 @@ export class DingtalkRuntime {
|
|
|
268
273
|
}
|
|
269
274
|
}
|
|
270
275
|
|
|
276
|
+
// Retain the committed, read-only settings at receipt without moving
|
|
277
|
+
// JSON parsing out of the existing asynchronous callback path.
|
|
278
|
+
const contextEnhancement = this.#contextEnhancement;
|
|
279
|
+
let receivedSettings;
|
|
280
|
+
try {
|
|
281
|
+
receivedSettings = contextEnhancement?.getSettings?.();
|
|
282
|
+
} catch {
|
|
283
|
+
// Optional settings failures leave the original message path active.
|
|
284
|
+
}
|
|
271
285
|
const task = Promise.resolve().then(async () => {
|
|
272
286
|
if (this.#bridge !== bridge) return;
|
|
273
287
|
let message;
|
|
@@ -282,7 +296,12 @@ export class DingtalkRuntime {
|
|
|
282
296
|
}
|
|
283
297
|
if (!message || typeof message !== 'object') return;
|
|
284
298
|
this.#status.lastCallbackAt = Date.now();
|
|
285
|
-
|
|
299
|
+
const contextSnapshot = captureContextEnhancement({
|
|
300
|
+
getSettings: () => receivedSettings,
|
|
301
|
+
get botId() { return contextEnhancement?.botId; },
|
|
302
|
+
}, message.conversationType === '1' || message.conversationType === 1 ? 'direct'
|
|
303
|
+
: message.conversationType === '2' || message.conversationType === 2 ? 'group' : null);
|
|
304
|
+
await bridge.accept(message, { contextSnapshot });
|
|
286
305
|
}).catch(() => {
|
|
287
306
|
if (signal.aborted || this.#bridge !== bridge) return;
|
|
288
307
|
this.#status.lastError = t('钉钉消息处理失败。');
|
|
@@ -2,6 +2,7 @@ import { createEditableMessageStream, splitMessageText } from '../shared/editabl
|
|
|
2
2
|
import { fetchFileStream } from '../shared/file-download.mjs';
|
|
3
3
|
import { fetchImageBuffer } from '../shared/image-prompt.mjs';
|
|
4
4
|
import { t } from '../shared/i18n.mjs';
|
|
5
|
+
import { captureContextEnhancement } from '../shared/context-enhancement.mjs';
|
|
5
6
|
import { DiscordApi } from './discord-api.mjs';
|
|
6
7
|
import { createDiscordBridgeStatus, DiscordHarnessBridge } from './discord-bridge.mjs';
|
|
7
8
|
|
|
@@ -213,6 +214,10 @@ export function normalizeDiscordMessage(message, botId, { fetchImpl = fetch } =
|
|
|
213
214
|
return {
|
|
214
215
|
messageId: String(message.id),
|
|
215
216
|
senderId: String(message.author.id),
|
|
217
|
+
contextSource: () => ({
|
|
218
|
+
senderName: [message.member?.nick, message.author.global_name, message.author.username]
|
|
219
|
+
.find((value) => typeof value === 'string' && value.trim()),
|
|
220
|
+
}),
|
|
216
221
|
senderIsBot: message.author.bot === true,
|
|
217
222
|
kind: direct ? 'direct' : 'group',
|
|
218
223
|
conversationId: String(message.channel_id),
|
|
@@ -427,6 +432,7 @@ export class DiscordRuntime {
|
|
|
427
432
|
#token;
|
|
428
433
|
#harness;
|
|
429
434
|
#state;
|
|
435
|
+
#contextEnhancement;
|
|
430
436
|
#logger;
|
|
431
437
|
#replyTimeoutMs;
|
|
432
438
|
#connectTimeoutMs;
|
|
@@ -457,6 +463,7 @@ export class DiscordRuntime {
|
|
|
457
463
|
token,
|
|
458
464
|
harness,
|
|
459
465
|
state,
|
|
466
|
+
contextEnhancement,
|
|
460
467
|
logger = console,
|
|
461
468
|
replyTimeoutMs = 600_000,
|
|
462
469
|
connectTimeoutMs = 20_000,
|
|
@@ -472,6 +479,7 @@ export class DiscordRuntime {
|
|
|
472
479
|
this.#token = token;
|
|
473
480
|
this.#harness = harness;
|
|
474
481
|
this.#state = state;
|
|
482
|
+
this.#contextEnhancement = contextEnhancement;
|
|
475
483
|
this.#logger = logger;
|
|
476
484
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
477
485
|
this.#connectTimeoutMs = connectTimeoutMs;
|
|
@@ -534,6 +542,7 @@ export class DiscordRuntime {
|
|
|
534
542
|
bot: client,
|
|
535
543
|
harness: this.#harness,
|
|
536
544
|
state: this.#state,
|
|
545
|
+
contextEnhancement: this.#contextEnhancement,
|
|
537
546
|
status: this.#status,
|
|
538
547
|
logger: this.#logger,
|
|
539
548
|
replyTimeoutMs: this.#replyTimeoutMs,
|
|
@@ -707,20 +716,25 @@ export class DiscordRuntime {
|
|
|
707
716
|
if (!messageId || this.#state.hasSeen(messageId)) return;
|
|
708
717
|
let route = this.#routing.get(messageId);
|
|
709
718
|
if (!route) {
|
|
710
|
-
|
|
719
|
+
const contextSnapshot = captureContextEnhancement(
|
|
720
|
+
this.#contextEnhancement,
|
|
721
|
+
message.guild_id ? 'group' : 'direct',
|
|
722
|
+
);
|
|
723
|
+
const pendingRoute = resolveDiscordMessageRoute(message, this.#config.platformId, {
|
|
711
724
|
api: this.#api,
|
|
712
725
|
channel: this.#channels.get(String(message.channel_id)),
|
|
713
726
|
signal: this.#abortController?.signal,
|
|
714
727
|
onChannel: (resolved) => this.#rememberChannel(resolved),
|
|
715
728
|
});
|
|
729
|
+
route = { pendingRoute, contextSnapshot };
|
|
716
730
|
this.#routing.set(messageId, route);
|
|
717
|
-
void
|
|
731
|
+
void pendingRoute.finally(() => {
|
|
718
732
|
if (this.#routing.get(messageId) === route) this.#routing.delete(messageId);
|
|
719
733
|
}).catch(() => undefined);
|
|
720
734
|
}
|
|
721
735
|
try {
|
|
722
|
-
const normalized = await route;
|
|
723
|
-
if (normalized) await bridge.accept(normalized);
|
|
736
|
+
const normalized = await route.pendingRoute;
|
|
737
|
+
if (normalized) await bridge.accept(normalized, { contextSnapshot: route.contextSnapshot });
|
|
724
738
|
} catch (error) {
|
|
725
739
|
if (error?.code === 'discord-thread-create-uncertain') {
|
|
726
740
|
await this.#state.markSeen(messageId);
|
|
@@ -46,9 +46,11 @@ import {
|
|
|
46
46
|
} from '../shared/preset-command.mjs';
|
|
47
47
|
import { runWorkspaceCommand, resolveSessionListWorkspace, workspacePathSnapshot } from '../shared/workspace-command.mjs';
|
|
48
48
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
49
|
+
import { captureContextEnhancement, enhanceContextContent } from '../shared/context-enhancement.mjs';
|
|
49
50
|
import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
|
|
50
51
|
import {
|
|
51
52
|
createDeliveryReceipt,
|
|
53
|
+
providerMessageIdsFor,
|
|
52
54
|
} from '../shared/semantic/delivery.mjs';
|
|
53
55
|
import {
|
|
54
56
|
channelDeliveryFailure,
|
|
@@ -382,12 +384,14 @@ export class FeishuHarnessBridge {
|
|
|
382
384
|
#channel;
|
|
383
385
|
#harness;
|
|
384
386
|
#state;
|
|
387
|
+
#contextEnhancement;
|
|
385
388
|
#queues = new Map();
|
|
386
389
|
#batchInputs = new BatchInputManager();
|
|
387
390
|
#pendingInteractions = new Map();
|
|
388
391
|
#interactionKeys = new Map();
|
|
389
392
|
#resolvedQuestionReplies = new Map();
|
|
390
|
-
|
|
393
|
+
// Keep the accepted configuration through the existing queue/reply lifecycle.
|
|
394
|
+
#acceptedMessageIds = new Map();
|
|
391
395
|
#interactionTasks = new Set();
|
|
392
396
|
#commandTasks = new Set();
|
|
393
397
|
/** All accepted card work, including tasks waiting behind an earlier click. */
|
|
@@ -446,6 +450,7 @@ export class FeishuHarnessBridge {
|
|
|
446
450
|
channel,
|
|
447
451
|
harness,
|
|
448
452
|
state,
|
|
453
|
+
contextEnhancement,
|
|
449
454
|
status,
|
|
450
455
|
allowedSenderOpenIds = new Set(),
|
|
451
456
|
botId,
|
|
@@ -482,6 +487,7 @@ export class FeishuHarnessBridge {
|
|
|
482
487
|
this.#channel = channel;
|
|
483
488
|
this.#harness = harness;
|
|
484
489
|
this.#state = state;
|
|
490
|
+
this.#contextEnhancement = contextEnhancement;
|
|
485
491
|
this.#status = status;
|
|
486
492
|
this.#allowedSenderOpenIds = allowedSenderOpenIds;
|
|
487
493
|
this.#botId = nonEmptyString(botId);
|
|
@@ -550,7 +556,10 @@ export class FeishuHarnessBridge {
|
|
|
550
556
|
if (chatId) rememberConnectionTestTarget(this.#state, { chatId });
|
|
551
557
|
}
|
|
552
558
|
|
|
553
|
-
this.#acceptedMessageIds.
|
|
559
|
+
this.#acceptedMessageIds.set(messageId, captureContextEnhancement(
|
|
560
|
+
this.#contextEnhancement,
|
|
561
|
+
event.message.chat_type === 'p2p' ? 'direct' : event.message.chat_type === 'group' ? 'group' : null,
|
|
562
|
+
));
|
|
554
563
|
const processingReaction = this.#beginReaction(messageId);
|
|
555
564
|
const commandMessage = extractInboundMessage(event, this.#client);
|
|
556
565
|
const commandText = nonEmptyString(commandMessage.content) ?? '';
|
|
@@ -3142,9 +3151,16 @@ export class FeishuHarnessBridge {
|
|
|
3142
3151
|
askCompleted = true;
|
|
3143
3152
|
onAskComplete?.();
|
|
3144
3153
|
};
|
|
3145
|
-
|
|
3154
|
+
let content = hasInboundImages(message)
|
|
3146
3155
|
? await promptContentForMessage(message, { signal: this.#signal })
|
|
3147
3156
|
: undefined;
|
|
3157
|
+
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
3158
|
+
if (snapshot) {
|
|
3159
|
+
content = enhanceContextContent(content ?? text, snapshot, () => ({
|
|
3160
|
+
channel: 'feishu',
|
|
3161
|
+
senderId: senderOpenId(event),
|
|
3162
|
+
}));
|
|
3163
|
+
}
|
|
3148
3164
|
if (!this.#channel?.stream) {
|
|
3149
3165
|
const { answer, artifacts = [] } = await askInWorkspaceSession({
|
|
3150
3166
|
harness: this.#harness,
|
|
@@ -3315,7 +3331,7 @@ export class FeishuHarnessBridge {
|
|
|
3315
3331
|
createDeliveryReceipt({
|
|
3316
3332
|
deliveryId: messageId,
|
|
3317
3333
|
presentation: 'feishu-cardkit',
|
|
3318
|
-
providerMessageIds: stream
|
|
3334
|
+
providerMessageIds: providerMessageIdsFor(stream),
|
|
3319
3335
|
}),
|
|
3320
3336
|
);
|
|
3321
3337
|
this.#status.streamResponses = (this.#status.streamResponses ?? 0) + 1;
|
|
@@ -126,7 +126,37 @@ function deliveryUuid(file, chatId, messageType) {
|
|
|
126
126
|
|
|
127
127
|
function summaryOf(text) {
|
|
128
128
|
const summary = String(text ?? '').replace(/\s+/g, ' ').trim();
|
|
129
|
-
return summary.length <= 50 ? summary : `${summary
|
|
129
|
+
return summary.length <= 50 ? summary : `${streamTextPrefix(summary, 49)}…`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function streamTextPrefix(text, maxChars) {
|
|
133
|
+
let end = Math.min(text.length, maxChars);
|
|
134
|
+
// Keep a UTF-16 surrogate pair together when the limit lands inside an emoji.
|
|
135
|
+
const before = text.charCodeAt(end - 1);
|
|
136
|
+
const after = text.charCodeAt(end);
|
|
137
|
+
if (before >= 0xd800 && before <= 0xdbff && after >= 0xdc00 && after <= 0xdfff) end -= 1;
|
|
138
|
+
return text.slice(0, end);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function streamPreview(text) {
|
|
142
|
+
if (text.length <= MAX_STREAM_CHARS) return text;
|
|
143
|
+
const notice = `\n\n${t('内容较长,生成完成后将分段发送完整回答。')}`;
|
|
144
|
+
return streamTextPrefix(text, MAX_STREAM_CHARS - notice.length) + notice;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function splitStreamContent(text) {
|
|
148
|
+
const chunks = [];
|
|
149
|
+
let remaining = text;
|
|
150
|
+
while (remaining.length > MAX_STREAM_CHARS) {
|
|
151
|
+
const prefix = streamTextPrefix(remaining, MAX_STREAM_CHARS);
|
|
152
|
+
let end = prefix.lastIndexOf('\n') + 1;
|
|
153
|
+
if (end < MAX_STREAM_CHARS * 0.6) end = prefix.length;
|
|
154
|
+
chunks.push(remaining.slice(0, end));
|
|
155
|
+
// Unlike plain-text fallback splitting, keep all whitespace in the answer.
|
|
156
|
+
remaining = remaining.slice(end);
|
|
157
|
+
}
|
|
158
|
+
if (remaining) chunks.push(remaining);
|
|
159
|
+
return chunks;
|
|
130
160
|
}
|
|
131
161
|
|
|
132
162
|
function streamingCard(initialText) {
|
|
@@ -164,7 +194,7 @@ export class VerifiedFeishuChannel {
|
|
|
164
194
|
fileMessageTimeoutMs = MAX_FILE_OPERATION_TIMEOUT_MS,
|
|
165
195
|
}) {
|
|
166
196
|
this.#client = client;
|
|
167
|
-
this.#initialText = initialText ?? t(DEFAULT_INITIAL_TEXT);
|
|
197
|
+
this.#initialText = String(initialText ?? t(DEFAULT_INITIAL_TEXT)) || '…';
|
|
168
198
|
this.#fileUploadTimeoutMs = boundedFileTimeout(fileUploadTimeoutMs, 'fileUploadTimeoutMs');
|
|
169
199
|
this.#fileMessageTimeoutMs = boundedFileTimeout(fileMessageTimeoutMs, 'fileMessageTimeoutMs');
|
|
170
200
|
}
|
|
@@ -174,59 +204,40 @@ export class VerifiedFeishuChannel {
|
|
|
174
204
|
throw new Error('Feishu stream requires a markdown producer');
|
|
175
205
|
}
|
|
176
206
|
|
|
177
|
-
|
|
178
|
-
const cardResponse = assertApiSuccess('Feishu card.create', await this.#client.cardkit.v1.card.create({
|
|
179
|
-
data: {
|
|
180
|
-
type: 'card_json',
|
|
181
|
-
data: JSON.stringify(streamingCard(this.#initialText)),
|
|
182
|
-
},
|
|
183
|
-
}));
|
|
184
|
-
const cardId = cardResponse?.data?.card_id;
|
|
185
|
-
if (!cardId) throw new Error('Feishu card.create returned no card_id');
|
|
186
|
-
|
|
207
|
+
const cards = [];
|
|
187
208
|
try {
|
|
188
|
-
|
|
189
|
-
|
|
209
|
+
const firstCard = await this.#createStreamCard(chatId, options.replyTo);
|
|
210
|
+
cards.push(firstCard);
|
|
190
211
|
let lastContent = this.#initialText;
|
|
191
212
|
const controller = {
|
|
192
|
-
messageId,
|
|
213
|
+
messageId: firstCard.messageId,
|
|
193
214
|
setContent: async (content) => {
|
|
194
215
|
const next = String(content ?? '') || '…';
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
const response = await this.#client.cardkit.v1.cardElement.content({
|
|
200
|
-
path: { card_id: cardId, element_id: STREAM_ELEMENT_ID },
|
|
201
|
-
data: {
|
|
202
|
-
content: next,
|
|
203
|
-
sequence: ++sequence,
|
|
204
|
-
uuid: `content_${cardId}_${sequence}`,
|
|
205
|
-
},
|
|
206
|
-
});
|
|
207
|
-
assertApiSuccess('Feishu cardElement.content', response);
|
|
216
|
+
await this.#updateStreamCard(firstCard, streamPreview(next));
|
|
217
|
+
// Updates are replaceable snapshots, including progress/tool text.
|
|
218
|
+
// Retain the full latest snapshot even when its preview is unchanged.
|
|
208
219
|
lastContent = next;
|
|
209
220
|
},
|
|
210
221
|
};
|
|
211
222
|
|
|
212
223
|
await input.markdown(controller);
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
assertApiSuccess('Feishu card.settings', finishResponse);
|
|
227
|
-
return { messageId };
|
|
224
|
+
const chunks = splitStreamContent(lastContent);
|
|
225
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
226
|
+
const card = index === 0
|
|
227
|
+
? firstCard
|
|
228
|
+
: await this.#createStreamCard(chatId, options.replyTo);
|
|
229
|
+
if (index > 0) cards.push(card);
|
|
230
|
+
await this.#updateStreamCard(card, chunk);
|
|
231
|
+
await this.#finishStreamCard(card);
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
messageId: firstCard.messageId,
|
|
235
|
+
providerMessageIds: cards.map((card) => card.messageId),
|
|
236
|
+
};
|
|
228
237
|
} catch (error) {
|
|
229
|
-
|
|
238
|
+
// Preserve the existing provider-error fallback contract: the bridge
|
|
239
|
+
// resends the completed answer, so remove any cards it would duplicate.
|
|
240
|
+
for (const card of cards) await this.#recall(card.messageId);
|
|
230
241
|
throw error;
|
|
231
242
|
}
|
|
232
243
|
}
|
|
@@ -369,6 +380,51 @@ export class VerifiedFeishuChannel {
|
|
|
369
380
|
});
|
|
370
381
|
}
|
|
371
382
|
|
|
383
|
+
async #createStreamCard(chatId, replyTo) {
|
|
384
|
+
const content = streamPreview(this.#initialText);
|
|
385
|
+
const response = assertApiSuccess('Feishu card.create', await this.#client.cardkit.v1.card.create({
|
|
386
|
+
data: {
|
|
387
|
+
type: 'card_json',
|
|
388
|
+
data: JSON.stringify(streamingCard(content)),
|
|
389
|
+
},
|
|
390
|
+
}));
|
|
391
|
+
const cardId = response?.data?.card_id;
|
|
392
|
+
if (!cardId) throw new Error('Feishu card.create returned no card_id');
|
|
393
|
+
const messageId = await this.#sendCard(chatId, cardId, replyTo);
|
|
394
|
+
return { cardId, messageId, content, sequence: 0 };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async #updateStreamCard(card, content) {
|
|
398
|
+
if (content === card.content) return;
|
|
399
|
+
const response = await this.#client.cardkit.v1.cardElement.content({
|
|
400
|
+
path: { card_id: card.cardId, element_id: STREAM_ELEMENT_ID },
|
|
401
|
+
data: {
|
|
402
|
+
content,
|
|
403
|
+
sequence: ++card.sequence,
|
|
404
|
+
uuid: `content_${card.cardId}_${card.sequence}`,
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
assertApiSuccess('Feishu cardElement.content', response);
|
|
408
|
+
card.content = content;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async #finishStreamCard(card) {
|
|
412
|
+
const response = await this.#client.cardkit.v1.card.settings({
|
|
413
|
+
path: { card_id: card.cardId },
|
|
414
|
+
data: {
|
|
415
|
+
settings: JSON.stringify({
|
|
416
|
+
config: {
|
|
417
|
+
streaming_mode: false,
|
|
418
|
+
summary: { content: summaryOf(card.content) || t('回答完成') },
|
|
419
|
+
},
|
|
420
|
+
}),
|
|
421
|
+
sequence: ++card.sequence,
|
|
422
|
+
uuid: `settings_${card.cardId}_${card.sequence}`,
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
assertApiSuccess('Feishu card.settings', response);
|
|
426
|
+
}
|
|
427
|
+
|
|
372
428
|
async #sendCard(chatId, cardId, replyTo) {
|
|
373
429
|
const content = JSON.stringify({ type: 'card', data: { card_id: cardId } });
|
|
374
430
|
const response = replyTo
|