@xmanrui/dsh-im 1.4.0 → 2.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 +18 -6
- package/README.md +18 -6
- package/lib/index.js +206 -176
- package/package.json +1 -1
- package/plugin-src/host/index.mjs +2 -0
- package/src/channels/dingtalk/config-store.mjs +5 -3
- package/src/channels/dingtalk/dingtalk-api.mjs +105 -82
- package/src/channels/dingtalk/dingtalk-bridge.mjs +79 -110
- package/src/channels/dingtalk/dingtalk-card-stream.mjs +3 -1
- package/src/channels/dingtalk/dingtalk-controller.mjs +24 -23
- package/src/channels/dingtalk/dingtalk-runtime.mjs +4 -3
- package/src/channels/dingtalk/state-store.mjs +3 -1
- package/src/channels/discord/config-store.mjs +2 -1
- package/src/channels/discord/discord-api.mjs +25 -1
- package/src/channels/discord/discord-runtime.mjs +237 -11
- package/src/channels/feishu/bridge.mjs +169 -180
- package/src/channels/feishu/feishu-cards.mjs +62 -54
- package/src/channels/feishu/feishu-channel.mjs +57 -21
- package/src/channels/feishu/feishu-runtime.mjs +10 -9
- package/src/channels/feishu/message-utils.mjs +4 -3
- package/src/channels/office/office-job-executor.mjs +15 -14
- package/src/channels/office/office-runtime.mjs +6 -5
- package/src/channels/qq/config-store.mjs +3 -1
- package/src/channels/qq/markdown-reply.mjs +4 -2
- package/src/channels/qq/qq-bridge.mjs +149 -130
- package/src/channels/qq/qq-controller.mjs +17 -16
- package/src/channels/qq/qq-runtime.mjs +3 -2
- package/src/channels/shared/agent-preset.mjs +3 -1
- package/src/channels/shared/compact-command.mjs +15 -14
- package/src/channels/shared/connection-test.mjs +6 -4
- package/src/channels/shared/control-command.mjs +13 -11
- package/src/channels/shared/editable-message-stream.mjs +4 -2
- package/src/channels/shared/harness-approval.mjs +23 -21
- package/src/channels/shared/harness-client.mjs +3 -2
- package/src/channels/shared/harness-question.mjs +8 -6
- package/src/channels/shared/i18n-en/dingtalk.mjs +52 -0
- package/src/channels/shared/i18n-en/discord.mjs +8 -0
- package/src/channels/shared/i18n-en/feishu.mjs +217 -0
- package/src/channels/shared/i18n-en/office.mjs +23 -0
- package/src/channels/shared/i18n-en/qq.mjs +54 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +91 -0
- package/src/channels/shared/i18n-en/shared-b.mjs +215 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +99 -0
- package/src/channels/shared/i18n-en/slack.mjs +27 -0
- package/src/channels/shared/i18n-en/telegram.mjs +21 -0
- package/src/channels/shared/i18n-en/wecom.mjs +38 -0
- package/src/channels/shared/i18n-en/weixin.mjs +80 -0
- package/src/channels/shared/i18n-en/whatsapp.mjs +17 -0
- package/src/channels/shared/i18n-en.mjs +35 -0
- package/src/channels/shared/i18n.mjs +35 -0
- package/src/channels/shared/image-prompt.mjs +17 -15
- package/src/channels/shared/inbound-file.mjs +5 -3
- package/src/channels/shared/model-command.mjs +35 -31
- package/src/channels/shared/preset-command.mjs +41 -38
- package/src/channels/shared/semantic/artifact-delivery.mjs +170 -0
- package/src/channels/shared/semantic/artifact.mjs +2 -2
- package/src/channels/shared/semantic/delivery.mjs +40 -0
- package/src/channels/shared/text-harness-bridge.mjs +154 -136
- package/src/channels/shared/token-bot-controller.mjs +32 -11
- package/src/channels/shared/workspace-command.mjs +69 -58
- package/src/channels/slack/config-store.mjs +3 -1
- package/src/channels/slack/slack-api.mjs +6 -5
- package/src/channels/slack/slack-controller.mjs +10 -9
- package/src/channels/slack/slack-runtime.mjs +16 -4
- package/src/channels/telegram/config-store.mjs +2 -1
- package/src/channels/telegram/telegram-api.mjs +111 -19
- package/src/channels/telegram/telegram-rich-message.mjs +147 -0
- package/src/channels/telegram/telegram-runtime.mjs +383 -5
- package/src/channels/wecom/config-store.mjs +3 -1
- package/src/channels/wecom/wecom-bridge.mjs +154 -153
- package/src/channels/wecom/wecom-controller.mjs +20 -19
- package/src/channels/wecom/wecom-runtime.mjs +2 -1
- package/src/channels/weixin/config-store.mjs +2 -1
- package/src/channels/weixin/weixin-api.mjs +140 -107
- package/src/channels/weixin/weixin-bridge.mjs +86 -123
- package/src/channels/weixin/weixin-controller.mjs +22 -21
- package/src/channels/weixin/weixin-runtime.mjs +3 -2
- package/src/channels/whatsapp/config-store.mjs +3 -1
- package/src/channels/whatsapp/whatsapp-bridge.mjs +3 -1
- package/src/channels/whatsapp/whatsapp-controller.mjs +15 -14
- package/src/channels/whatsapp/whatsapp-runtime.mjs +47 -13
- package/src/channels/whatsapp/whatsapp-web-session.mjs +3 -1
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ import { apply as applyWecom } from './channels/wecom/index.mjs';
|
|
|
9
9
|
import { apply as applyWeixin } from './channels/weixin/index.mjs';
|
|
10
10
|
import { apply as applyWhatsapp } from './channels/whatsapp/index.mjs';
|
|
11
11
|
import { installOutboundArtifactTool } from '../../src/channels/shared/semantic/artifact.mjs';
|
|
12
|
+
import { setImHostLanguage } from '../../src/channels/shared/i18n.mjs';
|
|
12
13
|
|
|
13
14
|
export const name = 'dsh-im-host';
|
|
14
15
|
export const inject = [
|
|
@@ -40,6 +41,7 @@ export function createImHostPlugin(internals = {}) {
|
|
|
40
41
|
name,
|
|
41
42
|
inject,
|
|
42
43
|
async apply(ctx, config = {}) {
|
|
44
|
+
setImHostLanguage(config.language ?? process.env.DSH_IM_LANGUAGE);
|
|
43
45
|
if (typeof ctx?.inject === 'function') {
|
|
44
46
|
ctx.inject(['tools', 'systemPrompt'], (artifactCtx) => {
|
|
45
47
|
installOutboundArtifactTool(artifactCtx);
|
|
@@ -2,6 +2,8 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
4
|
|
|
5
|
+
import { t } from '../shared/i18n.mjs';
|
|
6
|
+
|
|
5
7
|
const EMPTY_DOCUMENT = Object.freeze({ version: 1, bots: Object.freeze([]) });
|
|
6
8
|
const STORED_BOT_KEYS = new Set(['clientId', 'secretRef', 'approvedSenders']);
|
|
7
9
|
|
|
@@ -86,8 +88,8 @@ export function deriveDingtalkSenderKey() {
|
|
|
86
88
|
*/
|
|
87
89
|
export function maskDingtalkSenderId(staffId) {
|
|
88
90
|
const value = cleanString(staffId);
|
|
89
|
-
if (!value) return '钉钉用户';
|
|
90
|
-
return '身份已隐藏';
|
|
91
|
+
if (!value) return t('钉钉用户');
|
|
92
|
+
return t('身份已隐藏');
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
/**
|
|
@@ -97,7 +99,7 @@ export function maskDingtalkSenderId(staffId) {
|
|
|
97
99
|
*/
|
|
98
100
|
export function maskDingtalkClientId(clientId) {
|
|
99
101
|
const value = cleanString(clientId);
|
|
100
|
-
if (!value) return '钉钉机器人';
|
|
102
|
+
if (!value) return t('钉钉机器人');
|
|
101
103
|
if (value.length <= 8) return `${value.slice(0, 2)}••••`;
|
|
102
104
|
return `${value.slice(0, 4)}••••${value.slice(-4)}`;
|
|
103
105
|
}
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { extname } from 'node:path';
|
|
3
3
|
|
|
4
4
|
import { fetchImageBuffer, ImagePromptError } from '../shared/image-prompt.mjs';
|
|
5
|
+
import { t } from '../shared/i18n.mjs';
|
|
5
6
|
|
|
6
7
|
export const DINGTALK_REGISTRATION_BASE_URL = 'https://oapi.dingtalk.com/';
|
|
7
8
|
export const DINGTALK_API_BASE_URL = 'https://api.dingtalk.com/';
|
|
@@ -540,6 +541,93 @@ export function createDingtalkApi({
|
|
|
540
541
|
return true;
|
|
541
542
|
}
|
|
542
543
|
|
|
544
|
+
async function sendArtifact({
|
|
545
|
+
clientId,
|
|
546
|
+
clientSecret,
|
|
547
|
+
target,
|
|
548
|
+
file,
|
|
549
|
+
signal,
|
|
550
|
+
}, { uploadType, messageKey, createMessageParams }) {
|
|
551
|
+
if (!file || typeof file !== 'object'
|
|
552
|
+
|| typeof file.fileName !== 'string' || !file.fileName
|
|
553
|
+
|| !Buffer.isBuffer(file.bytes)) {
|
|
554
|
+
throw new TypeError('A DingTalk file is required');
|
|
555
|
+
}
|
|
556
|
+
const normalizedTarget = normalizeFileTarget(target);
|
|
557
|
+
let token;
|
|
558
|
+
try {
|
|
559
|
+
token = await accessToken({ clientId, clientSecret, signal });
|
|
560
|
+
} catch (error) {
|
|
561
|
+
if (signal?.aborted) throw abortError(signal);
|
|
562
|
+
const status = Number(error?.status);
|
|
563
|
+
const fallback = error?.code === 'http-error' && status >= 400 && status < 500
|
|
564
|
+
? 'artifact-provider-rejected'
|
|
565
|
+
: 'artifact-provider-failed';
|
|
566
|
+
throw dingtalkArtifactError(error, { fallback });
|
|
567
|
+
}
|
|
568
|
+
const uploadUrl = new URL('media/upload', DINGTALK_REGISTRATION_BASE_URL);
|
|
569
|
+
uploadUrl.searchParams.set('access_token', token);
|
|
570
|
+
uploadUrl.searchParams.set('type', uploadType);
|
|
571
|
+
const form = new FormData();
|
|
572
|
+
form.append(
|
|
573
|
+
'media',
|
|
574
|
+
new Blob([file.bytes], { type: file.mediaType ?? 'application/octet-stream' }),
|
|
575
|
+
file.fileName,
|
|
576
|
+
);
|
|
577
|
+
let uploaded;
|
|
578
|
+
try {
|
|
579
|
+
uploaded = await requestMultipart(fetchImpl, uploadUrl, { body: form, signal });
|
|
580
|
+
} catch (error) {
|
|
581
|
+
if (signal?.aborted) throw abortError(signal);
|
|
582
|
+
const status = Number(error?.status);
|
|
583
|
+
const fallback = error?.code === 'http-error' && status >= 400 && status < 500
|
|
584
|
+
? 'artifact-provider-rejected'
|
|
585
|
+
: 'artifact-provider-failed';
|
|
586
|
+
throw dingtalkArtifactError(error, { fallback });
|
|
587
|
+
}
|
|
588
|
+
const uploadRejection = rejectedProviderResponse(uploaded);
|
|
589
|
+
const mediaId = nonEmptyString(uploaded?.media_id);
|
|
590
|
+
if (uploadRejection || !mediaId) {
|
|
591
|
+
throw dingtalkArtifactError(new DingtalkApiError(
|
|
592
|
+
'upload-rejected',
|
|
593
|
+
'钉钉服务拒绝了文件上传。',
|
|
594
|
+
{ providerCode: uploadRejection ?? 'missing-media-id' },
|
|
595
|
+
));
|
|
596
|
+
}
|
|
597
|
+
signal?.throwIfAborted();
|
|
598
|
+
const messageBody = {
|
|
599
|
+
robotCode: normalizedTarget.robotCode,
|
|
600
|
+
msgKey: messageKey,
|
|
601
|
+
msgParam: JSON.stringify(createMessageParams(mediaId, file)),
|
|
602
|
+
...(normalizedTarget.type === 'group'
|
|
603
|
+
? { openConversationId: normalizedTarget.openConversationId }
|
|
604
|
+
: { userIds: [normalizedTarget.userId] }),
|
|
605
|
+
};
|
|
606
|
+
const pathname = normalizedTarget.type === 'group'
|
|
607
|
+
? 'v1.0/robot/groupMessages/send'
|
|
608
|
+
: 'v1.0/robot/oToMessages/batchSend';
|
|
609
|
+
let response;
|
|
610
|
+
try {
|
|
611
|
+
response = await requestJson(fetchImpl, endpoint(apiBase, pathname), {
|
|
612
|
+
body: messageBody,
|
|
613
|
+
headers: { 'x-acs-dingtalk-access-token': token },
|
|
614
|
+
signal,
|
|
615
|
+
action: '文件消息发送',
|
|
616
|
+
});
|
|
617
|
+
} catch (error) {
|
|
618
|
+
throw classifyDingtalkFinalDeliveryError(error, signal);
|
|
619
|
+
}
|
|
620
|
+
const sendRejection = rejectedProviderResponse(response);
|
|
621
|
+
if (sendRejection) {
|
|
622
|
+
throw dingtalkArtifactError(new DingtalkApiError(
|
|
623
|
+
'send-rejected',
|
|
624
|
+
'钉钉服务拒绝了文件消息。',
|
|
625
|
+
{ providerCode: sendRejection },
|
|
626
|
+
));
|
|
627
|
+
}
|
|
628
|
+
return response;
|
|
629
|
+
}
|
|
630
|
+
|
|
543
631
|
return Object.freeze({
|
|
544
632
|
async beginRegistration({ signal } = {}) {
|
|
545
633
|
const initialized = assertRegistrationOk(await requestJson(
|
|
@@ -742,7 +830,7 @@ export function createDingtalkApi({
|
|
|
742
830
|
clientId: appKey,
|
|
743
831
|
clientSecret: appSecret,
|
|
744
832
|
cardInstanceId,
|
|
745
|
-
text: '消息处理失败,请稍后重试。',
|
|
833
|
+
text: t('消息处理失败,请稍后重试。'),
|
|
746
834
|
signal: cleanupSignal,
|
|
747
835
|
}).catch(() => undefined);
|
|
748
836
|
}
|
|
@@ -842,89 +930,24 @@ export function createDingtalkApi({
|
|
|
842
930
|
return true;
|
|
843
931
|
},
|
|
844
932
|
|
|
845
|
-
async sendFile(
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
const normalizedTarget = normalizeFileTarget(target);
|
|
852
|
-
const fileType = dingtalkFileType(file.fileName);
|
|
853
|
-
let token;
|
|
854
|
-
try {
|
|
855
|
-
token = await accessToken({ clientId, clientSecret, signal });
|
|
856
|
-
} catch (error) {
|
|
857
|
-
if (signal?.aborted) throw abortError(signal);
|
|
858
|
-
const status = Number(error?.status);
|
|
859
|
-
const fallback = error?.code === 'http-error' && status >= 400 && status < 500
|
|
860
|
-
? 'artifact-provider-rejected'
|
|
861
|
-
: 'artifact-provider-failed';
|
|
862
|
-
throw dingtalkArtifactError(error, { fallback });
|
|
863
|
-
}
|
|
864
|
-
const uploadUrl = new URL('media/upload', DINGTALK_REGISTRATION_BASE_URL);
|
|
865
|
-
uploadUrl.searchParams.set('access_token', token);
|
|
866
|
-
uploadUrl.searchParams.set('type', 'file');
|
|
867
|
-
const form = new FormData();
|
|
868
|
-
form.append(
|
|
869
|
-
'media',
|
|
870
|
-
new Blob([file.bytes], { type: file.mediaType ?? 'application/octet-stream' }),
|
|
871
|
-
file.fileName,
|
|
872
|
-
);
|
|
873
|
-
let uploaded;
|
|
874
|
-
try {
|
|
875
|
-
uploaded = await requestMultipart(fetchImpl, uploadUrl, { body: form, signal });
|
|
876
|
-
} catch (error) {
|
|
877
|
-
if (signal?.aborted) throw abortError(signal);
|
|
878
|
-
const status = Number(error?.status);
|
|
879
|
-
const fallback = error?.code === 'http-error' && status >= 400 && status < 500
|
|
880
|
-
? 'artifact-provider-rejected'
|
|
881
|
-
: 'artifact-provider-failed';
|
|
882
|
-
throw dingtalkArtifactError(error, { fallback });
|
|
883
|
-
}
|
|
884
|
-
const uploadRejection = rejectedProviderResponse(uploaded);
|
|
885
|
-
if (uploadRejection || !nonEmptyString(uploaded?.media_id)) {
|
|
886
|
-
throw dingtalkArtifactError(new DingtalkApiError(
|
|
887
|
-
'upload-rejected',
|
|
888
|
-
'钉钉服务拒绝了文件上传。',
|
|
889
|
-
{ providerCode: uploadRejection ?? 'missing-media-id' },
|
|
890
|
-
));
|
|
891
|
-
}
|
|
892
|
-
signal?.throwIfAborted();
|
|
893
|
-
const messageBody = {
|
|
894
|
-
robotCode: normalizedTarget.robotCode,
|
|
895
|
-
msgKey: 'sampleFile',
|
|
896
|
-
msgParam: JSON.stringify({
|
|
897
|
-
mediaId: uploaded.media_id,
|
|
933
|
+
async sendFile(request) {
|
|
934
|
+
return sendArtifact(request, {
|
|
935
|
+
uploadType: 'file',
|
|
936
|
+
messageKey: 'sampleFile',
|
|
937
|
+
createMessageParams: (mediaId, file) => ({
|
|
938
|
+
mediaId,
|
|
898
939
|
fileName: file.fileName,
|
|
899
|
-
fileType,
|
|
940
|
+
fileType: dingtalkFileType(file.fileName),
|
|
900
941
|
}),
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
: '
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
response = await requestJson(fetchImpl, endpoint(apiBase, pathname), {
|
|
911
|
-
body: messageBody,
|
|
912
|
-
headers: { 'x-acs-dingtalk-access-token': token },
|
|
913
|
-
signal,
|
|
914
|
-
action: '文件消息发送',
|
|
915
|
-
});
|
|
916
|
-
} catch (error) {
|
|
917
|
-
throw classifyDingtalkFinalDeliveryError(error, signal);
|
|
918
|
-
}
|
|
919
|
-
const sendRejection = rejectedProviderResponse(response);
|
|
920
|
-
if (sendRejection) {
|
|
921
|
-
throw dingtalkArtifactError(new DingtalkApiError(
|
|
922
|
-
'send-rejected',
|
|
923
|
-
'钉钉服务拒绝了文件消息。',
|
|
924
|
-
{ providerCode: sendRejection },
|
|
925
|
-
));
|
|
926
|
-
}
|
|
927
|
-
return response;
|
|
942
|
+
});
|
|
943
|
+
},
|
|
944
|
+
|
|
945
|
+
async sendImage(request) {
|
|
946
|
+
return sendArtifact(request, {
|
|
947
|
+
uploadType: 'image',
|
|
948
|
+
messageKey: 'sampleImageMsg',
|
|
949
|
+
createMessageParams: (mediaId) => ({ photoURL: mediaId }),
|
|
950
|
+
});
|
|
928
951
|
},
|
|
929
952
|
|
|
930
953
|
clearAccessToken(clientId) {
|
|
@@ -35,22 +35,18 @@ import {
|
|
|
35
35
|
prefetchInboundFiles,
|
|
36
36
|
} from '../shared/inbound-file.mjs';
|
|
37
37
|
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
|
38
|
+
import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
|
|
38
39
|
import {
|
|
39
|
-
materializeOutboundArtifact,
|
|
40
|
-
releaseOutboundArtifact,
|
|
41
|
-
} from '../shared/semantic/artifact.mjs';
|
|
42
|
-
import {
|
|
43
|
-
createArtifactFailureReceipt,
|
|
44
40
|
createDeliveryReceipt,
|
|
45
|
-
mergeDeliveryReceipts,
|
|
46
41
|
providerMessageIdsFor,
|
|
47
42
|
} from '../shared/semantic/delivery.mjs';
|
|
43
|
+
import { t } from '../shared/i18n.mjs';
|
|
48
44
|
|
|
49
45
|
const CARD_INITIAL_TEXT = '已连接 DeepSeek Harness,正在思考…';
|
|
50
46
|
const CARD_ERROR_TEXT = '消息处理失败,请稍后重试。';
|
|
51
47
|
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
|
52
48
|
|
|
53
|
-
const
|
|
49
|
+
const HELP_TEXT_LINES = [
|
|
54
50
|
'钉钉机器人已连接 DeepSeek Harness。',
|
|
55
51
|
'',
|
|
56
52
|
'直接发送文字、图片或文件即可继续当前会话。',
|
|
@@ -71,7 +67,11 @@ const HELP_TEXT = [
|
|
|
71
67
|
'/steer 补充指令 纠偏当前任务',
|
|
72
68
|
'/status 检查连接状态',
|
|
73
69
|
'/help 显示本帮助',
|
|
74
|
-
]
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
function helpText() {
|
|
73
|
+
return HELP_TEXT_LINES.map((line) => t(line)).join('\n');
|
|
74
|
+
}
|
|
75
75
|
|
|
76
76
|
function nonEmptyString(value) {
|
|
77
77
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
@@ -111,13 +111,13 @@ function dingtalkImageErrorUserMessage(error) {
|
|
|
111
111
|
while (current && typeof current === 'object' && !seen.has(current)) {
|
|
112
112
|
seen.add(current);
|
|
113
113
|
if (current.code === 'image-download-address-failed') {
|
|
114
|
-
return '钉钉未能换取图片下载地址,请重新发送;若持续失败,请检查机器人的“企业内机器人发送消息权限”。';
|
|
114
|
+
return t('钉钉未能换取图片下载地址,请重新发送;若持续失败,请检查机器人的“企业内机器人发送消息权限”。');
|
|
115
115
|
}
|
|
116
116
|
if (current.code === 'invalid-image-download') {
|
|
117
|
-
return '钉钉没有返回图片下载地址,请重新发送。';
|
|
117
|
+
return t('钉钉没有返回图片下载地址,请重新发送。');
|
|
118
118
|
}
|
|
119
119
|
if (current.code === 'image-content-download-failed') {
|
|
120
|
-
return '钉钉返回的图片临时地址无法读取,请重新发送。';
|
|
120
|
+
return t('钉钉返回的图片临时地址无法读取,请重新发送。');
|
|
121
121
|
}
|
|
122
122
|
current = current.cause;
|
|
123
123
|
}
|
|
@@ -246,34 +246,34 @@ function fileTarget(message, sender, clientId) {
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
function artifactFailureText(fileName, error) {
|
|
249
|
-
const name = String(fileName ?? '结果文件').replace(/[\r\n]+/g, ' ').trim() || '结果文件';
|
|
249
|
+
const name = String(fileName ?? t('结果文件')).replace(/[\r\n]+/g, ' ').trim() || t('结果文件');
|
|
250
250
|
switch (error?.code) {
|
|
251
251
|
case 'artifact-delivery-uncertain':
|
|
252
|
-
return
|
|
252
|
+
return t('结果文件「{name}」发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
|
|
253
253
|
case 'artifact-permission-required':
|
|
254
|
-
return
|
|
254
|
+
return t('结果文件「{name}」已生成,但钉钉应用或机器人缺少文件消息权限。请开通应用 qyapi_base 权限,并确认机器人具备文件消息发送能力。', { name });
|
|
255
255
|
case 'artifact-too-large':
|
|
256
|
-
return
|
|
256
|
+
return t('结果文件「{name}」超过当前钉钉机器人可发送的文件大小,未发送。', { name });
|
|
257
257
|
case 'artifact-rate-limited':
|
|
258
|
-
return
|
|
258
|
+
return t('结果文件「{name}」暂时被钉钉限流,未能发送,请稍后重试。', { name });
|
|
259
259
|
case 'artifact-provider-rejected':
|
|
260
|
-
return
|
|
260
|
+
return t('结果文件「{name}」已生成,但钉钉拒绝了该文件消息,请检查文件类型和机器人文件消息配置。', { name });
|
|
261
261
|
case 'artifact-invalid':
|
|
262
262
|
case 'artifact-changed':
|
|
263
263
|
case 'artifact-unavailable':
|
|
264
|
-
return
|
|
264
|
+
return t('结果文件「{name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。', { name });
|
|
265
265
|
default:
|
|
266
|
-
return
|
|
266
|
+
return t('结果文件「{name}」已生成,但暂时未能通过钉钉发送,请稍后重试。', { name });
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
function progressText(update) {
|
|
271
271
|
if (update?.type === 'text' && nonEmptyString(update.text)) return update.text;
|
|
272
272
|
if (update?.type === 'tool') {
|
|
273
|
-
if (update.name === 'web_search') return '_正在搜索网络并整理信息…_';
|
|
274
|
-
return
|
|
273
|
+
if (update.name === 'web_search') return t('_正在搜索网络并整理信息…_');
|
|
274
|
+
return t('_正在使用 {name}…_', { name: nonEmptyString(update.name) ?? t('工具') });
|
|
275
275
|
}
|
|
276
|
-
return
|
|
276
|
+
return t('_{text}_', { text: nonEmptyString(update?.text) ?? t('正在处理…') });
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
function canClaimInteractionReply(message, pending, sender) {
|
|
@@ -431,9 +431,9 @@ export class DingtalkHarnessBridge {
|
|
|
431
431
|
commandRunner,
|
|
432
432
|
).catch((error) => {
|
|
433
433
|
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
|
|
434
|
-
this.#status.lastError = '钉钉命令处理失败。';
|
|
434
|
+
this.#status.lastError = t('钉钉命令处理失败。');
|
|
435
435
|
this.#logger.error?.('[dsh-dingtalk] failed to process a command', safeErrorDiagnostic(error));
|
|
436
|
-
return this.#send(sessionWebhook, CARD_ERROR_TEXT).catch(() => undefined);
|
|
436
|
+
return this.#send(sessionWebhook, t(CARD_ERROR_TEXT)).catch(() => undefined);
|
|
437
437
|
}).finally(() => {
|
|
438
438
|
this.#acceptedMessageIds.delete(messageId);
|
|
439
439
|
this.#commandTasks.delete(task);
|
|
@@ -468,13 +468,13 @@ export class DingtalkHarnessBridge {
|
|
|
468
468
|
if (!sessionWebhook) {
|
|
469
469
|
increment(this.#status, 'messagesRejected');
|
|
470
470
|
this.#status.lastRejectedAt = new Date().toISOString();
|
|
471
|
-
this.#status.lastError = '钉钉消息没有安全的回复地址。';
|
|
471
|
+
this.#status.lastError = t('钉钉消息没有安全的回复地址。');
|
|
472
472
|
}
|
|
473
473
|
return true;
|
|
474
474
|
})
|
|
475
475
|
.catch((error) => {
|
|
476
476
|
if (this.#signal?.aborted) return;
|
|
477
|
-
this.#status.lastError = '钉钉审批处理失败。';
|
|
477
|
+
this.#status.lastError = t('钉钉审批处理失败。');
|
|
478
478
|
this.#logger.error?.('[dsh-dingtalk] failed to process an approval reply', error);
|
|
479
479
|
})
|
|
480
480
|
.finally(() => {
|
|
@@ -616,7 +616,7 @@ export class DingtalkHarnessBridge {
|
|
|
616
616
|
} catch {
|
|
617
617
|
increment(this.#status, 'messagesRejected');
|
|
618
618
|
this.#status.lastRejectedAt = new Date().toISOString();
|
|
619
|
-
this.#status.lastError = '钉钉消息没有安全的回复地址。';
|
|
619
|
+
this.#status.lastError = t('钉钉消息没有安全的回复地址。');
|
|
620
620
|
return;
|
|
621
621
|
}
|
|
622
622
|
|
|
@@ -633,23 +633,23 @@ export class DingtalkHarnessBridge {
|
|
|
633
633
|
let cardStarted = false;
|
|
634
634
|
try {
|
|
635
635
|
if (!text && !hasImages && !hasFiles) {
|
|
636
|
-
await this.#send(sessionWebhook, '目前支持文字、图片和文件消息。');
|
|
636
|
+
await this.#send(sessionWebhook, t('目前支持文字、图片和文件消息。'));
|
|
637
637
|
return;
|
|
638
638
|
}
|
|
639
639
|
|
|
640
640
|
const command = text.toLowerCase();
|
|
641
641
|
if (isPlainText && !hasImages && !hasFiles && command === '/help') {
|
|
642
|
-
await this.#send(sessionWebhook,
|
|
642
|
+
await this.#send(sessionWebhook, helpText());
|
|
643
643
|
return;
|
|
644
644
|
}
|
|
645
645
|
if (isPlainText && !hasImages && !hasFiles && command === '/status') {
|
|
646
646
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
647
|
-
await this.#send(sessionWebhook, '钉钉机器人与 DeepSeek Harness 连接正常。');
|
|
647
|
+
await this.#send(sessionWebhook, t('钉钉机器人与 DeepSeek Harness 连接正常。'));
|
|
648
648
|
return;
|
|
649
649
|
}
|
|
650
650
|
if (isPlainText && !hasImages && !hasFiles && command === '/new') {
|
|
651
651
|
await this.#state.clearSession(key);
|
|
652
|
-
await this.#send(sessionWebhook, '已开启新会话。请发送你的问题。');
|
|
652
|
+
await this.#send(sessionWebhook, t('已开启新会话。请发送你的问题。'));
|
|
653
653
|
return;
|
|
654
654
|
}
|
|
655
655
|
const workspaceCommand = isPlainText && !hasImages && !hasFiles
|
|
@@ -689,7 +689,7 @@ export class DingtalkHarnessBridge {
|
|
|
689
689
|
signal: this.#signal,
|
|
690
690
|
logger: this.#logger,
|
|
691
691
|
});
|
|
692
|
-
cardStarted = await cardStream.start(CARD_INITIAL_TEXT);
|
|
692
|
+
cardStarted = await cardStream.start(t(CARD_INITIAL_TEXT));
|
|
693
693
|
}
|
|
694
694
|
const { answer, artifacts = [] } = await askInWorkspaceSession({
|
|
695
695
|
harness: this.#harness,
|
|
@@ -717,7 +717,7 @@ export class DingtalkHarnessBridge {
|
|
|
717
717
|
});
|
|
718
718
|
const answerText = typeof answer === 'string' && answer.trim()
|
|
719
719
|
? answer
|
|
720
|
-
: artifacts.length > 0 ? '结果文件已生成。' : answer;
|
|
720
|
+
: artifacts.length > 0 ? t('结果文件已生成。') : answer;
|
|
721
721
|
let textDeliveryError = null;
|
|
722
722
|
let textReceipt = null;
|
|
723
723
|
let streamed = false;
|
|
@@ -752,11 +752,11 @@ export class DingtalkHarnessBridge {
|
|
|
752
752
|
return delivery.receipt;
|
|
753
753
|
} catch (error) {
|
|
754
754
|
if (error?.code === 'turn-stopped') {
|
|
755
|
-
if (cardStarted) await cardStream.finish('已停止。').catch(() => undefined);
|
|
755
|
+
if (cardStarted) await cardStream.finish(t('已停止。')).catch(() => undefined);
|
|
756
756
|
return;
|
|
757
757
|
}
|
|
758
758
|
if (this.#signal?.aborted) return;
|
|
759
|
-
this.#status.lastError = '钉钉消息处理失败。';
|
|
759
|
+
this.#status.lastError = t('钉钉消息处理失败。');
|
|
760
760
|
this.#logger.error?.(
|
|
761
761
|
'[dsh-dingtalk] failed to process an inbound message',
|
|
762
762
|
safeErrorDiagnostic(error),
|
|
@@ -764,7 +764,7 @@ export class DingtalkHarnessBridge {
|
|
|
764
764
|
try {
|
|
765
765
|
const errorText = inboundFileUserMessage(error)
|
|
766
766
|
?? dingtalkImageErrorUserMessage(error)
|
|
767
|
-
?? CARD_ERROR_TEXT;
|
|
767
|
+
?? t(CARD_ERROR_TEXT);
|
|
768
768
|
const streamed = cardStarted && await cardStream.finish(errorText);
|
|
769
769
|
if (!streamed) await this.#send(sessionWebhook, errorText);
|
|
770
770
|
} catch {
|
|
@@ -802,14 +802,14 @@ export class DingtalkHarnessBridge {
|
|
|
802
802
|
} catch {
|
|
803
803
|
increment(this.#status, 'messagesRejected');
|
|
804
804
|
this.#status.lastRejectedAt = new Date().toISOString();
|
|
805
|
-
this.#status.lastError = '钉钉消息没有安全的回复地址。';
|
|
805
|
+
this.#status.lastError = t('钉钉消息没有安全的回复地址。');
|
|
806
806
|
return;
|
|
807
807
|
}
|
|
808
808
|
|
|
809
809
|
const text = message?.msgtype === 'text' ? nonEmptyString(message?.text?.content) : null;
|
|
810
810
|
if (!text) {
|
|
811
811
|
try {
|
|
812
|
-
await this.#send(sessionWebhook, '请用文字回答当前问题。');
|
|
812
|
+
await this.#send(sessionWebhook, t('请用文字回答当前问题。'));
|
|
813
813
|
} catch {
|
|
814
814
|
this.#logger.error?.('[dsh-dingtalk] failed to reject a non-text interaction reply');
|
|
815
815
|
}
|
|
@@ -820,7 +820,7 @@ export class DingtalkHarnessBridge {
|
|
|
820
820
|
if (!pending || pending !== expected || pending.submitting) {
|
|
821
821
|
if (claimed && (!pending || pending !== expected)) {
|
|
822
822
|
try {
|
|
823
|
-
await this.#send(sessionWebhook, INTERACTION_RESOLVED_TEXT);
|
|
823
|
+
await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT));
|
|
824
824
|
} catch {
|
|
825
825
|
this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice');
|
|
826
826
|
}
|
|
@@ -836,7 +836,7 @@ export class DingtalkHarnessBridge {
|
|
|
836
836
|
try {
|
|
837
837
|
await this.#presentInteraction(pending);
|
|
838
838
|
} catch {
|
|
839
|
-
this.#status.lastError = '钉钉交互问题发送失败。';
|
|
839
|
+
this.#status.lastError = t('钉钉交互问题发送失败。');
|
|
840
840
|
this.#logger.error?.('[dsh-dingtalk] failed to retry an interaction question');
|
|
841
841
|
pending.interaction.reconnect?.();
|
|
842
842
|
}
|
|
@@ -855,7 +855,7 @@ export class DingtalkHarnessBridge {
|
|
|
855
855
|
try {
|
|
856
856
|
await this.#presentInteraction(pending);
|
|
857
857
|
} catch {
|
|
858
|
-
this.#status.lastError = '钉钉交互问题发送失败。';
|
|
858
|
+
this.#status.lastError = t('钉钉交互问题发送失败。');
|
|
859
859
|
this.#logger.error?.('[dsh-dingtalk] failed to send the next interaction question');
|
|
860
860
|
pending.interaction.reconnect?.();
|
|
861
861
|
}
|
|
@@ -879,7 +879,7 @@ export class DingtalkHarnessBridge {
|
|
|
879
879
|
if (error?.code === 'interaction-not-pending') {
|
|
880
880
|
this.#clearPendingInteraction(key, pending.interactionId);
|
|
881
881
|
try {
|
|
882
|
-
await this.#send(sessionWebhook, INTERACTION_RESOLVED_TEXT);
|
|
882
|
+
await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT));
|
|
883
883
|
} catch {
|
|
884
884
|
this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice');
|
|
885
885
|
}
|
|
@@ -888,10 +888,10 @@ export class DingtalkHarnessBridge {
|
|
|
888
888
|
pending.submitting = false;
|
|
889
889
|
pending.answers.pop();
|
|
890
890
|
pending.index -= 1;
|
|
891
|
-
this.#status.lastError = '回答提交失败。';
|
|
891
|
+
this.#status.lastError = t('回答提交失败。');
|
|
892
892
|
this.#logger.error?.('[dsh-dingtalk] failed to answer a Harness interaction');
|
|
893
893
|
try {
|
|
894
|
-
await this.#send(sessionWebhook, '回答提交失败,请重新发送当前问题的答案。');
|
|
894
|
+
await this.#send(sessionWebhook, t('回答提交失败,请重新发送当前问题的答案。'));
|
|
895
895
|
} catch {
|
|
896
896
|
this.#logger.error?.('[dsh-dingtalk] failed to send an interaction retry notice');
|
|
897
897
|
}
|
|
@@ -937,7 +937,7 @@ export class DingtalkHarnessBridge {
|
|
|
937
937
|
},
|
|
938
938
|
});
|
|
939
939
|
try {
|
|
940
|
-
await this.#send(sessionWebhook, '检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。');
|
|
940
|
+
await this.#send(sessionWebhook, t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'));
|
|
941
941
|
} catch {
|
|
942
942
|
this.#logger.error?.('[dsh-dingtalk] failed to send an interaction recovery notice');
|
|
943
943
|
}
|
|
@@ -1023,7 +1023,7 @@ export class DingtalkHarnessBridge {
|
|
|
1023
1023
|
return;
|
|
1024
1024
|
}
|
|
1025
1025
|
try {
|
|
1026
|
-
await this.#send(sessionWebhook, INTERACTION_RESOLVED_TEXT);
|
|
1026
|
+
await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT));
|
|
1027
1027
|
} catch {
|
|
1028
1028
|
this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice');
|
|
1029
1029
|
}
|
|
@@ -1084,70 +1084,39 @@ export class DingtalkHarnessBridge {
|
|
|
1084
1084
|
}
|
|
1085
1085
|
|
|
1086
1086
|
async #deliverArtifacts(target, sessionWebhook, replyTo, artifacts, baseReceipt) {
|
|
1087
|
-
const
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
);
|
|
1121
|
-
let noticeSent = false;
|
|
1122
|
-
const providerMessageIds = await this.#send(
|
|
1123
|
-
sessionWebhook,
|
|
1124
|
-
artifactFailureText(artifact?.fileName, error),
|
|
1125
|
-
).then((ids) => {
|
|
1126
|
-
noticeSent = true;
|
|
1127
|
-
return ids;
|
|
1128
|
-
}).catch(() => []);
|
|
1129
|
-
const failureReceipt = createArtifactFailureReceipt({
|
|
1130
|
-
artifactId: artifact?.artifactId ?? 'unknown',
|
|
1131
|
-
deliveryId: artifact?.deliveryKey ?? artifact?.artifactId ?? 'unknown',
|
|
1132
|
-
error,
|
|
1133
|
-
providerMessageIds,
|
|
1134
|
-
});
|
|
1135
|
-
receipts.push(failureReceipt);
|
|
1136
|
-
if (noticeSent || failureReceipt.artifacts[0]?.outcome === 'unknown') userVisible = true;
|
|
1137
|
-
} finally {
|
|
1138
|
-
releaseOutboundArtifact(artifact);
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
const receipt = receipts.length === 0
|
|
1142
|
-
? null
|
|
1143
|
-
: receipts.length === 1
|
|
1144
|
-
? receipts[0]
|
|
1145
|
-
: mergeDeliveryReceipts({
|
|
1146
|
-
deliveryId: replyTo,
|
|
1147
|
-
presentation: baseReceipt ? 'dingtalk-text-and-files' : 'dingtalk-files',
|
|
1148
|
-
receipts,
|
|
1149
|
-
});
|
|
1150
|
-
return { receipt, userVisible };
|
|
1087
|
+
const sendArtifact = async (method, file) => dingtalkFileProviderIds(
|
|
1088
|
+
await this.#api[method]({
|
|
1089
|
+
clientId: this.#clientId,
|
|
1090
|
+
clientSecret: this.#clientSecret,
|
|
1091
|
+
target,
|
|
1092
|
+
file,
|
|
1093
|
+
signal: this.#signal,
|
|
1094
|
+
}),
|
|
1095
|
+
);
|
|
1096
|
+
const delivery = await deliverOutboundArtifacts({
|
|
1097
|
+
artifacts,
|
|
1098
|
+
baseReceipt,
|
|
1099
|
+
deliveryId: replyTo,
|
|
1100
|
+
aggregatePresentation: baseReceipt ? 'dingtalk-text-and-files' : 'dingtalk-files',
|
|
1101
|
+
channelKey: 'dingtalk',
|
|
1102
|
+
signal: this.#signal,
|
|
1103
|
+
sendImage: typeof this.#api.sendImage === 'function'
|
|
1104
|
+
? (file) => sendArtifact('sendImage', file)
|
|
1105
|
+
: undefined,
|
|
1106
|
+
sendFile: typeof this.#api.sendFile === 'function'
|
|
1107
|
+
? (file) => sendArtifact('sendFile', file)
|
|
1108
|
+
: undefined,
|
|
1109
|
+
sendFailureNotice: (artifact, error) => this.#send(
|
|
1110
|
+
sessionWebhook,
|
|
1111
|
+
artifactFailureText(artifact?.fileName, error),
|
|
1112
|
+
),
|
|
1113
|
+
logger: this.#logger,
|
|
1114
|
+
});
|
|
1115
|
+
this.#status.artifactsSent = (this.#status.artifactsSent ?? 0)
|
|
1116
|
+
+ delivery.artifactsSent;
|
|
1117
|
+
this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0)
|
|
1118
|
+
+ delivery.artifactSendErrors;
|
|
1119
|
+
return { receipt: delivery.receipt, userVisible: delivery.userVisible };
|
|
1151
1120
|
}
|
|
1152
1121
|
}
|
|
1153
1122
|
|