@xmanrui/dsh-im 3.1.1 → 4.0.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 +16 -1
- package/README.md +16 -1
- package/lib/client.js +1653 -333
- package/lib/index.js +235 -226
- 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/index.mjs +1 -1
- 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/discord/index.mjs +1 -1
- package/plugin-src/host/channels/feishu/index.mjs +1 -1
- 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/index.mjs +1 -1
- 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/index.mjs +1 -1
- 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/telegram/index.mjs +1 -1
- package/plugin-src/host/channels/wecom/index.mjs +1 -1
- 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/index.mjs +1 -1
- 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/index.mjs +1 -1
- package/plugin-src/host/channels/whatsapp/production.mjs +1 -0
- package/plugin-src/host/channels/whatsapp/rpc.mjs +11 -0
- package/plugin-src/host/harness-connection.mjs +9 -4
- package/plugin-src/host/index.mjs +43 -29
- package/plugin-src/host/modern-harness-api.mjs +577 -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 +18 -3
- 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/harness-client.mjs +18 -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
|
@@ -316,16 +316,30 @@ function normalizeCardTarget(target) {
|
|
|
316
316
|
}
|
|
317
317
|
if (target?.type === 'group') {
|
|
318
318
|
const openConversationId = nonEmptyString(target.openConversationId);
|
|
319
|
-
if (openConversationId)
|
|
319
|
+
if (openConversationId) {
|
|
320
|
+
return { type: 'group', openConversationId, atUserIds: target.atUserIds };
|
|
321
|
+
}
|
|
320
322
|
}
|
|
321
323
|
throw new TypeError('DingTalk AI Card target is invalid');
|
|
322
324
|
}
|
|
323
325
|
|
|
324
|
-
function
|
|
326
|
+
function cardMarkdown(text, target) {
|
|
327
|
+
const escape = (value) => String(value).replace(/[&<>"']/g, (char) => ({
|
|
328
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
329
|
+
})[char]);
|
|
330
|
+
const mentions = target?.type === 'group'
|
|
331
|
+
? Object.entries(target.atUserIds ?? {}).map(([userId, name]) => (
|
|
332
|
+
`<a atId="${escape(userId)}">${escape(name)}</a>`
|
|
333
|
+
)).join(' ')
|
|
334
|
+
: '';
|
|
335
|
+
return normalizeDingtalkCardMarkdown(mentions ? `${mentions}\n\n${text}` : text);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function cardData(text, flowStatus, target) {
|
|
325
339
|
return {
|
|
326
340
|
cardParamMap: {
|
|
327
341
|
flowStatus,
|
|
328
|
-
msgContent:
|
|
342
|
+
msgContent: cardMarkdown(text, target),
|
|
329
343
|
staticMsgContent: '',
|
|
330
344
|
sys_full_json_obj: JSON.stringify({ order: ['msgContent'] }),
|
|
331
345
|
config: JSON.stringify({ autoLayout: true }),
|
|
@@ -339,7 +353,7 @@ function cardDeliverBody(cardInstanceId, target, robotCode) {
|
|
|
339
353
|
return {
|
|
340
354
|
...base,
|
|
341
355
|
openSpaceId: `dtv1.card//IM_GROUP.${target.openConversationId}`,
|
|
342
|
-
imGroupOpenDeliverModel: { robotCode },
|
|
356
|
+
imGroupOpenDeliverModel: { robotCode, atUserIds: target.atUserIds },
|
|
343
357
|
};
|
|
344
358
|
}
|
|
345
359
|
return {
|
|
@@ -575,7 +589,7 @@ export function createDingtalkApi({
|
|
|
575
589
|
}
|
|
576
590
|
}
|
|
577
591
|
|
|
578
|
-
async function failCard({ clientId, clientSecret, cardInstanceId, text, signal }) {
|
|
592
|
+
async function failCard({ clientId, clientSecret, cardInstanceId, text, target, signal }) {
|
|
579
593
|
const instanceId = nonEmptyString(cardInstanceId);
|
|
580
594
|
const content = nonEmptyString(text);
|
|
581
595
|
if (!instanceId) throw new TypeError('cardInstanceId is required');
|
|
@@ -589,7 +603,7 @@ export function createDingtalkApi({
|
|
|
589
603
|
outTrackId: instanceId,
|
|
590
604
|
guid: randomUUID(),
|
|
591
605
|
key: 'msgContent',
|
|
592
|
-
content:
|
|
606
|
+
content: cardMarkdown(content, target),
|
|
593
607
|
isFull: true,
|
|
594
608
|
isFinalize: false,
|
|
595
609
|
isError: true,
|
|
@@ -602,7 +616,7 @@ export function createDingtalkApi({
|
|
|
602
616
|
method: 'PUT',
|
|
603
617
|
body: {
|
|
604
618
|
outTrackId: instanceId,
|
|
605
|
-
cardData: cardData(content, '5'),
|
|
619
|
+
cardData: cardData(content, '5', target),
|
|
606
620
|
cardUpdateOptions: { updateCardDataByKey: true },
|
|
607
621
|
},
|
|
608
622
|
headers,
|
|
@@ -883,6 +897,9 @@ export function createDingtalkApi({
|
|
|
883
897
|
cardParamMap: { config: JSON.stringify({ autoLayout: true }) },
|
|
884
898
|
},
|
|
885
899
|
callbackType: 'STREAM',
|
|
900
|
+
cardAtUserIds: normalizedTarget.atUserIds
|
|
901
|
+
? Object.keys(normalizedTarget.atUserIds)
|
|
902
|
+
: undefined,
|
|
886
903
|
imGroupOpenSpaceModel: { supportForward: true },
|
|
887
904
|
imRobotOpenSpaceModel: { supportForward: true },
|
|
888
905
|
},
|
|
@@ -899,7 +916,7 @@ export function createDingtalkApi({
|
|
|
899
916
|
delivered = true;
|
|
900
917
|
await cardRequest('v1.0/card/instances', {
|
|
901
918
|
method: 'PUT',
|
|
902
|
-
body: { outTrackId: cardInstanceId, cardData: cardData(content, '2') },
|
|
919
|
+
body: { outTrackId: cardInstanceId, cardData: cardData(content, '2', normalizedTarget) },
|
|
903
920
|
headers,
|
|
904
921
|
signal,
|
|
905
922
|
action: 'AI Card 启动',
|
|
@@ -910,7 +927,7 @@ export function createDingtalkApi({
|
|
|
910
927
|
outTrackId: cardInstanceId,
|
|
911
928
|
guid: randomUUID(),
|
|
912
929
|
key: 'msgContent',
|
|
913
|
-
content:
|
|
930
|
+
content: cardMarkdown(content, normalizedTarget).replace(/\n+$/, ''),
|
|
914
931
|
isFull: true,
|
|
915
932
|
isFinalize: false,
|
|
916
933
|
isError: false,
|
|
@@ -927,6 +944,7 @@ export function createDingtalkApi({
|
|
|
927
944
|
clientSecret: appSecret,
|
|
928
945
|
cardInstanceId,
|
|
929
946
|
text: t('卡片已结束,请查看后续消息。'),
|
|
947
|
+
target: normalizedTarget,
|
|
930
948
|
signal: cleanupSignal,
|
|
931
949
|
}).catch(() => undefined);
|
|
932
950
|
}
|
|
@@ -935,7 +953,7 @@ export function createDingtalkApi({
|
|
|
935
953
|
return { cardInstanceId };
|
|
936
954
|
},
|
|
937
955
|
|
|
938
|
-
async updateAiCard({ clientId, clientSecret, cardInstanceId, text, signal }) {
|
|
956
|
+
async updateAiCard({ clientId, clientSecret, cardInstanceId, text, target, signal }) {
|
|
939
957
|
const instanceId = nonEmptyString(cardInstanceId);
|
|
940
958
|
const content = nonEmptyString(text);
|
|
941
959
|
if (!instanceId) throw new TypeError('cardInstanceId is required');
|
|
@@ -947,7 +965,7 @@ export function createDingtalkApi({
|
|
|
947
965
|
outTrackId: instanceId,
|
|
948
966
|
guid: randomUUID(),
|
|
949
967
|
key: 'msgContent',
|
|
950
|
-
content:
|
|
968
|
+
content: cardMarkdown(content, target).replace(/\n+$/, ''),
|
|
951
969
|
isFull: true,
|
|
952
970
|
isFinalize: false,
|
|
953
971
|
isError: false,
|
|
@@ -959,14 +977,14 @@ export function createDingtalkApi({
|
|
|
959
977
|
return true;
|
|
960
978
|
},
|
|
961
979
|
|
|
962
|
-
async finishAiCard({ clientId, clientSecret, cardInstanceId, text, signal }) {
|
|
980
|
+
async finishAiCard({ clientId, clientSecret, cardInstanceId, text, target, signal }) {
|
|
963
981
|
const instanceId = nonEmptyString(cardInstanceId);
|
|
964
982
|
const content = nonEmptyString(text);
|
|
965
983
|
if (!instanceId) throw new TypeError('cardInstanceId is required');
|
|
966
984
|
if (!content) throw new TypeError('text is required');
|
|
967
985
|
const token = await accessToken({ clientId, clientSecret, signal });
|
|
968
986
|
const headers = { 'x-acs-dingtalk-access-token': token };
|
|
969
|
-
const normalizedContent =
|
|
987
|
+
const normalizedContent = cardMarkdown(content, target);
|
|
970
988
|
await cardRequest('v1.0/card/streaming', {
|
|
971
989
|
method: 'PUT',
|
|
972
990
|
body: {
|
|
@@ -987,7 +1005,7 @@ export function createDingtalkApi({
|
|
|
987
1005
|
method: 'PUT',
|
|
988
1006
|
body: {
|
|
989
1007
|
outTrackId: instanceId,
|
|
990
|
-
cardData: cardData(content, '3'),
|
|
1008
|
+
cardData: cardData(content, '3', target),
|
|
991
1009
|
cardUpdateOptions: { updateCardDataByKey: true },
|
|
992
1010
|
},
|
|
993
1011
|
headers,
|
|
@@ -1008,13 +1026,18 @@ export function createDingtalkApi({
|
|
|
1008
1026
|
|
|
1009
1027
|
failAiCard: failCard,
|
|
1010
1028
|
|
|
1011
|
-
async sendText({ clientId, clientSecret, sessionWebhook, text, signal }) {
|
|
1029
|
+
async sendText({ clientId, clientSecret, sessionWebhook, text, at, signal }) {
|
|
1012
1030
|
const content = nonEmptyString(text);
|
|
1013
1031
|
if (!content) throw new TypeError('text is required');
|
|
1014
1032
|
const webhook = normalizeDingtalkSessionWebhook(sessionWebhook);
|
|
1015
1033
|
const token = await accessToken({ clientId, clientSecret, signal });
|
|
1034
|
+
const replyAt = at && typeof at === 'object'
|
|
1035
|
+
&& ((Array.isArray(at.atUserIds) && at.atUserIds.length > 0)
|
|
1036
|
+
|| (Array.isArray(at.atMobiles) && at.atMobiles.length > 0)
|
|
1037
|
+
|| at.isAtAll === true)
|
|
1038
|
+
? { at } : {};
|
|
1016
1039
|
const response = await requestJson(fetchImpl, webhook, {
|
|
1017
|
-
body: { msgtype: 'text', text: { content } },
|
|
1040
|
+
body: { msgtype: 'text', text: { content }, ...replyAt },
|
|
1018
1041
|
headers: { 'x-acs-dingtalk-access-token': token },
|
|
1019
1042
|
signal,
|
|
1020
1043
|
action: '消息回复',
|
|
@@ -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,6 +46,7 @@ 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,
|
|
@@ -383,12 +384,14 @@ export class FeishuHarnessBridge {
|
|
|
383
384
|
#channel;
|
|
384
385
|
#harness;
|
|
385
386
|
#state;
|
|
387
|
+
#contextEnhancement;
|
|
386
388
|
#queues = new Map();
|
|
387
389
|
#batchInputs = new BatchInputManager();
|
|
388
390
|
#pendingInteractions = new Map();
|
|
389
391
|
#interactionKeys = new Map();
|
|
390
392
|
#resolvedQuestionReplies = new Map();
|
|
391
|
-
|
|
393
|
+
// Keep the accepted configuration through the existing queue/reply lifecycle.
|
|
394
|
+
#acceptedMessageIds = new Map();
|
|
392
395
|
#interactionTasks = new Set();
|
|
393
396
|
#commandTasks = new Set();
|
|
394
397
|
/** All accepted card work, including tasks waiting behind an earlier click. */
|
|
@@ -447,6 +450,7 @@ export class FeishuHarnessBridge {
|
|
|
447
450
|
channel,
|
|
448
451
|
harness,
|
|
449
452
|
state,
|
|
453
|
+
contextEnhancement,
|
|
450
454
|
status,
|
|
451
455
|
allowedSenderOpenIds = new Set(),
|
|
452
456
|
botId,
|
|
@@ -483,6 +487,7 @@ export class FeishuHarnessBridge {
|
|
|
483
487
|
this.#channel = channel;
|
|
484
488
|
this.#harness = harness;
|
|
485
489
|
this.#state = state;
|
|
490
|
+
this.#contextEnhancement = contextEnhancement;
|
|
486
491
|
this.#status = status;
|
|
487
492
|
this.#allowedSenderOpenIds = allowedSenderOpenIds;
|
|
488
493
|
this.#botId = nonEmptyString(botId);
|
|
@@ -551,7 +556,10 @@ export class FeishuHarnessBridge {
|
|
|
551
556
|
if (chatId) rememberConnectionTestTarget(this.#state, { chatId });
|
|
552
557
|
}
|
|
553
558
|
|
|
554
|
-
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
|
+
));
|
|
555
563
|
const processingReaction = this.#beginReaction(messageId);
|
|
556
564
|
const commandMessage = extractInboundMessage(event, this.#client);
|
|
557
565
|
const commandText = nonEmptyString(commandMessage.content) ?? '';
|
|
@@ -3143,9 +3151,16 @@ export class FeishuHarnessBridge {
|
|
|
3143
3151
|
askCompleted = true;
|
|
3144
3152
|
onAskComplete?.();
|
|
3145
3153
|
};
|
|
3146
|
-
|
|
3154
|
+
let content = hasInboundImages(message)
|
|
3147
3155
|
? await promptContentForMessage(message, { signal: this.#signal })
|
|
3148
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
|
+
}
|
|
3149
3164
|
if (!this.#channel?.stream) {
|
|
3150
3165
|
const { answer, artifacts = [] } = await askInWorkspaceSession({
|
|
3151
3166
|
harness: this.#harness,
|
|
@@ -104,6 +104,7 @@ export class FeishuRuntime {
|
|
|
104
104
|
#ownerOpenIds;
|
|
105
105
|
#harness;
|
|
106
106
|
#state;
|
|
107
|
+
#contextEnhancement;
|
|
107
108
|
#replyTimeoutMs;
|
|
108
109
|
#connectTimeoutMs;
|
|
109
110
|
#requestTimeoutMs;
|
|
@@ -131,6 +132,7 @@ export class FeishuRuntime {
|
|
|
131
132
|
ownerOpenIds,
|
|
132
133
|
harness,
|
|
133
134
|
state,
|
|
135
|
+
contextEnhancement,
|
|
134
136
|
repair,
|
|
135
137
|
replyTimeoutMs = 600000,
|
|
136
138
|
connectTimeoutMs = 15000,
|
|
@@ -162,6 +164,7 @@ export class FeishuRuntime {
|
|
|
162
164
|
this.#ownerOpenIds = normalizedOwners;
|
|
163
165
|
this.#harness = harness;
|
|
164
166
|
this.#state = state;
|
|
167
|
+
this.#contextEnhancement = contextEnhancement;
|
|
165
168
|
this.#repair = repair ?? null;
|
|
166
169
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
167
170
|
this.#connectTimeoutMs = connectTimeoutMs;
|
|
@@ -260,6 +263,7 @@ export class FeishuRuntime {
|
|
|
260
263
|
channel,
|
|
261
264
|
harness: this.#harness,
|
|
262
265
|
state: this.#state,
|
|
266
|
+
contextEnhancement: this.#contextEnhancement,
|
|
263
267
|
status: this.#status,
|
|
264
268
|
allowedSenderOpenIds: new Set(this.#ownerOpenIds),
|
|
265
269
|
botId: this.#botId,
|