@xmanrui/dsh-im 1.5.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 +16 -4
- package/README.md +16 -4
- package/lib/index.js +208 -178
- 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 +2 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +45 -40
- 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 +131 -123
- package/src/channels/feishu/feishu-cards.mjs +62 -54
- package/src/channels/feishu/feishu-channel.mjs +5 -4
- 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 +57 -52
- 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 +1 -1
- package/src/channels/shared/semantic/delivery.mjs +38 -0
- package/src/channels/shared/text-harness-bridge.mjs +130 -63
- 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 +72 -6
- package/src/channels/telegram/telegram-rich-message.mjs +147 -0
- package/src/channels/telegram/telegram-runtime.mjs +373 -5
- package/src/channels/wecom/config-store.mjs +3 -1
- package/src/channels/wecom/wecom-bridge.mjs +59 -55
- 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-bridge.mjs +50 -49
- 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 +5 -4
- 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/';
|
|
@@ -829,7 +830,7 @@ export function createDingtalkApi({
|
|
|
829
830
|
clientId: appKey,
|
|
830
831
|
clientSecret: appSecret,
|
|
831
832
|
cardInstanceId,
|
|
832
|
-
text: '消息处理失败,请稍后重试。',
|
|
833
|
+
text: t('消息处理失败,请稍后重试。'),
|
|
833
834
|
signal: cleanupSignal,
|
|
834
835
|
}).catch(() => undefined);
|
|
835
836
|
}
|
|
@@ -40,12 +40,13 @@ import {
|
|
|
40
40
|
createDeliveryReceipt,
|
|
41
41
|
providerMessageIdsFor,
|
|
42
42
|
} from '../shared/semantic/delivery.mjs';
|
|
43
|
+
import { t } from '../shared/i18n.mjs';
|
|
43
44
|
|
|
44
45
|
const CARD_INITIAL_TEXT = '已连接 DeepSeek Harness,正在思考…';
|
|
45
46
|
const CARD_ERROR_TEXT = '消息处理失败,请稍后重试。';
|
|
46
47
|
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
|
47
48
|
|
|
48
|
-
const
|
|
49
|
+
const HELP_TEXT_LINES = [
|
|
49
50
|
'钉钉机器人已连接 DeepSeek Harness。',
|
|
50
51
|
'',
|
|
51
52
|
'直接发送文字、图片或文件即可继续当前会话。',
|
|
@@ -66,7 +67,11 @@ const HELP_TEXT = [
|
|
|
66
67
|
'/steer 补充指令 纠偏当前任务',
|
|
67
68
|
'/status 检查连接状态',
|
|
68
69
|
'/help 显示本帮助',
|
|
69
|
-
]
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
function helpText() {
|
|
73
|
+
return HELP_TEXT_LINES.map((line) => t(line)).join('\n');
|
|
74
|
+
}
|
|
70
75
|
|
|
71
76
|
function nonEmptyString(value) {
|
|
72
77
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
@@ -106,13 +111,13 @@ function dingtalkImageErrorUserMessage(error) {
|
|
|
106
111
|
while (current && typeof current === 'object' && !seen.has(current)) {
|
|
107
112
|
seen.add(current);
|
|
108
113
|
if (current.code === 'image-download-address-failed') {
|
|
109
|
-
return '钉钉未能换取图片下载地址,请重新发送;若持续失败,请检查机器人的“企业内机器人发送消息权限”。';
|
|
114
|
+
return t('钉钉未能换取图片下载地址,请重新发送;若持续失败,请检查机器人的“企业内机器人发送消息权限”。');
|
|
110
115
|
}
|
|
111
116
|
if (current.code === 'invalid-image-download') {
|
|
112
|
-
return '钉钉没有返回图片下载地址,请重新发送。';
|
|
117
|
+
return t('钉钉没有返回图片下载地址,请重新发送。');
|
|
113
118
|
}
|
|
114
119
|
if (current.code === 'image-content-download-failed') {
|
|
115
|
-
return '钉钉返回的图片临时地址无法读取,请重新发送。';
|
|
120
|
+
return t('钉钉返回的图片临时地址无法读取,请重新发送。');
|
|
116
121
|
}
|
|
117
122
|
current = current.cause;
|
|
118
123
|
}
|
|
@@ -241,34 +246,34 @@ function fileTarget(message, sender, clientId) {
|
|
|
241
246
|
}
|
|
242
247
|
|
|
243
248
|
function artifactFailureText(fileName, error) {
|
|
244
|
-
const name = String(fileName ?? '结果文件').replace(/[\r\n]+/g, ' ').trim() || '结果文件';
|
|
249
|
+
const name = String(fileName ?? t('结果文件')).replace(/[\r\n]+/g, ' ').trim() || t('结果文件');
|
|
245
250
|
switch (error?.code) {
|
|
246
251
|
case 'artifact-delivery-uncertain':
|
|
247
|
-
return
|
|
252
|
+
return t('结果文件「{name}」发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
|
|
248
253
|
case 'artifact-permission-required':
|
|
249
|
-
return
|
|
254
|
+
return t('结果文件「{name}」已生成,但钉钉应用或机器人缺少文件消息权限。请开通应用 qyapi_base 权限,并确认机器人具备文件消息发送能力。', { name });
|
|
250
255
|
case 'artifact-too-large':
|
|
251
|
-
return
|
|
256
|
+
return t('结果文件「{name}」超过当前钉钉机器人可发送的文件大小,未发送。', { name });
|
|
252
257
|
case 'artifact-rate-limited':
|
|
253
|
-
return
|
|
258
|
+
return t('结果文件「{name}」暂时被钉钉限流,未能发送,请稍后重试。', { name });
|
|
254
259
|
case 'artifact-provider-rejected':
|
|
255
|
-
return
|
|
260
|
+
return t('结果文件「{name}」已生成,但钉钉拒绝了该文件消息,请检查文件类型和机器人文件消息配置。', { name });
|
|
256
261
|
case 'artifact-invalid':
|
|
257
262
|
case 'artifact-changed':
|
|
258
263
|
case 'artifact-unavailable':
|
|
259
|
-
return
|
|
264
|
+
return t('结果文件「{name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。', { name });
|
|
260
265
|
default:
|
|
261
|
-
return
|
|
266
|
+
return t('结果文件「{name}」已生成,但暂时未能通过钉钉发送,请稍后重试。', { name });
|
|
262
267
|
}
|
|
263
268
|
}
|
|
264
269
|
|
|
265
270
|
function progressText(update) {
|
|
266
271
|
if (update?.type === 'text' && nonEmptyString(update.text)) return update.text;
|
|
267
272
|
if (update?.type === 'tool') {
|
|
268
|
-
if (update.name === 'web_search') return '_正在搜索网络并整理信息…_';
|
|
269
|
-
return
|
|
273
|
+
if (update.name === 'web_search') return t('_正在搜索网络并整理信息…_');
|
|
274
|
+
return t('_正在使用 {name}…_', { name: nonEmptyString(update.name) ?? t('工具') });
|
|
270
275
|
}
|
|
271
|
-
return
|
|
276
|
+
return t('_{text}_', { text: nonEmptyString(update?.text) ?? t('正在处理…') });
|
|
272
277
|
}
|
|
273
278
|
|
|
274
279
|
function canClaimInteractionReply(message, pending, sender) {
|
|
@@ -426,9 +431,9 @@ export class DingtalkHarnessBridge {
|
|
|
426
431
|
commandRunner,
|
|
427
432
|
).catch((error) => {
|
|
428
433
|
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
|
|
429
|
-
this.#status.lastError = '钉钉命令处理失败。';
|
|
434
|
+
this.#status.lastError = t('钉钉命令处理失败。');
|
|
430
435
|
this.#logger.error?.('[dsh-dingtalk] failed to process a command', safeErrorDiagnostic(error));
|
|
431
|
-
return this.#send(sessionWebhook, CARD_ERROR_TEXT).catch(() => undefined);
|
|
436
|
+
return this.#send(sessionWebhook, t(CARD_ERROR_TEXT)).catch(() => undefined);
|
|
432
437
|
}).finally(() => {
|
|
433
438
|
this.#acceptedMessageIds.delete(messageId);
|
|
434
439
|
this.#commandTasks.delete(task);
|
|
@@ -463,13 +468,13 @@ export class DingtalkHarnessBridge {
|
|
|
463
468
|
if (!sessionWebhook) {
|
|
464
469
|
increment(this.#status, 'messagesRejected');
|
|
465
470
|
this.#status.lastRejectedAt = new Date().toISOString();
|
|
466
|
-
this.#status.lastError = '钉钉消息没有安全的回复地址。';
|
|
471
|
+
this.#status.lastError = t('钉钉消息没有安全的回复地址。');
|
|
467
472
|
}
|
|
468
473
|
return true;
|
|
469
474
|
})
|
|
470
475
|
.catch((error) => {
|
|
471
476
|
if (this.#signal?.aborted) return;
|
|
472
|
-
this.#status.lastError = '钉钉审批处理失败。';
|
|
477
|
+
this.#status.lastError = t('钉钉审批处理失败。');
|
|
473
478
|
this.#logger.error?.('[dsh-dingtalk] failed to process an approval reply', error);
|
|
474
479
|
})
|
|
475
480
|
.finally(() => {
|
|
@@ -611,7 +616,7 @@ export class DingtalkHarnessBridge {
|
|
|
611
616
|
} catch {
|
|
612
617
|
increment(this.#status, 'messagesRejected');
|
|
613
618
|
this.#status.lastRejectedAt = new Date().toISOString();
|
|
614
|
-
this.#status.lastError = '钉钉消息没有安全的回复地址。';
|
|
619
|
+
this.#status.lastError = t('钉钉消息没有安全的回复地址。');
|
|
615
620
|
return;
|
|
616
621
|
}
|
|
617
622
|
|
|
@@ -628,23 +633,23 @@ export class DingtalkHarnessBridge {
|
|
|
628
633
|
let cardStarted = false;
|
|
629
634
|
try {
|
|
630
635
|
if (!text && !hasImages && !hasFiles) {
|
|
631
|
-
await this.#send(sessionWebhook, '目前支持文字、图片和文件消息。');
|
|
636
|
+
await this.#send(sessionWebhook, t('目前支持文字、图片和文件消息。'));
|
|
632
637
|
return;
|
|
633
638
|
}
|
|
634
639
|
|
|
635
640
|
const command = text.toLowerCase();
|
|
636
641
|
if (isPlainText && !hasImages && !hasFiles && command === '/help') {
|
|
637
|
-
await this.#send(sessionWebhook,
|
|
642
|
+
await this.#send(sessionWebhook, helpText());
|
|
638
643
|
return;
|
|
639
644
|
}
|
|
640
645
|
if (isPlainText && !hasImages && !hasFiles && command === '/status') {
|
|
641
646
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
642
|
-
await this.#send(sessionWebhook, '钉钉机器人与 DeepSeek Harness 连接正常。');
|
|
647
|
+
await this.#send(sessionWebhook, t('钉钉机器人与 DeepSeek Harness 连接正常。'));
|
|
643
648
|
return;
|
|
644
649
|
}
|
|
645
650
|
if (isPlainText && !hasImages && !hasFiles && command === '/new') {
|
|
646
651
|
await this.#state.clearSession(key);
|
|
647
|
-
await this.#send(sessionWebhook, '已开启新会话。请发送你的问题。');
|
|
652
|
+
await this.#send(sessionWebhook, t('已开启新会话。请发送你的问题。'));
|
|
648
653
|
return;
|
|
649
654
|
}
|
|
650
655
|
const workspaceCommand = isPlainText && !hasImages && !hasFiles
|
|
@@ -684,7 +689,7 @@ export class DingtalkHarnessBridge {
|
|
|
684
689
|
signal: this.#signal,
|
|
685
690
|
logger: this.#logger,
|
|
686
691
|
});
|
|
687
|
-
cardStarted = await cardStream.start(CARD_INITIAL_TEXT);
|
|
692
|
+
cardStarted = await cardStream.start(t(CARD_INITIAL_TEXT));
|
|
688
693
|
}
|
|
689
694
|
const { answer, artifacts = [] } = await askInWorkspaceSession({
|
|
690
695
|
harness: this.#harness,
|
|
@@ -712,7 +717,7 @@ export class DingtalkHarnessBridge {
|
|
|
712
717
|
});
|
|
713
718
|
const answerText = typeof answer === 'string' && answer.trim()
|
|
714
719
|
? answer
|
|
715
|
-
: artifacts.length > 0 ? '结果文件已生成。' : answer;
|
|
720
|
+
: artifacts.length > 0 ? t('结果文件已生成。') : answer;
|
|
716
721
|
let textDeliveryError = null;
|
|
717
722
|
let textReceipt = null;
|
|
718
723
|
let streamed = false;
|
|
@@ -747,11 +752,11 @@ export class DingtalkHarnessBridge {
|
|
|
747
752
|
return delivery.receipt;
|
|
748
753
|
} catch (error) {
|
|
749
754
|
if (error?.code === 'turn-stopped') {
|
|
750
|
-
if (cardStarted) await cardStream.finish('已停止。').catch(() => undefined);
|
|
755
|
+
if (cardStarted) await cardStream.finish(t('已停止。')).catch(() => undefined);
|
|
751
756
|
return;
|
|
752
757
|
}
|
|
753
758
|
if (this.#signal?.aborted) return;
|
|
754
|
-
this.#status.lastError = '钉钉消息处理失败。';
|
|
759
|
+
this.#status.lastError = t('钉钉消息处理失败。');
|
|
755
760
|
this.#logger.error?.(
|
|
756
761
|
'[dsh-dingtalk] failed to process an inbound message',
|
|
757
762
|
safeErrorDiagnostic(error),
|
|
@@ -759,7 +764,7 @@ export class DingtalkHarnessBridge {
|
|
|
759
764
|
try {
|
|
760
765
|
const errorText = inboundFileUserMessage(error)
|
|
761
766
|
?? dingtalkImageErrorUserMessage(error)
|
|
762
|
-
?? CARD_ERROR_TEXT;
|
|
767
|
+
?? t(CARD_ERROR_TEXT);
|
|
763
768
|
const streamed = cardStarted && await cardStream.finish(errorText);
|
|
764
769
|
if (!streamed) await this.#send(sessionWebhook, errorText);
|
|
765
770
|
} catch {
|
|
@@ -797,14 +802,14 @@ export class DingtalkHarnessBridge {
|
|
|
797
802
|
} catch {
|
|
798
803
|
increment(this.#status, 'messagesRejected');
|
|
799
804
|
this.#status.lastRejectedAt = new Date().toISOString();
|
|
800
|
-
this.#status.lastError = '钉钉消息没有安全的回复地址。';
|
|
805
|
+
this.#status.lastError = t('钉钉消息没有安全的回复地址。');
|
|
801
806
|
return;
|
|
802
807
|
}
|
|
803
808
|
|
|
804
809
|
const text = message?.msgtype === 'text' ? nonEmptyString(message?.text?.content) : null;
|
|
805
810
|
if (!text) {
|
|
806
811
|
try {
|
|
807
|
-
await this.#send(sessionWebhook, '请用文字回答当前问题。');
|
|
812
|
+
await this.#send(sessionWebhook, t('请用文字回答当前问题。'));
|
|
808
813
|
} catch {
|
|
809
814
|
this.#logger.error?.('[dsh-dingtalk] failed to reject a non-text interaction reply');
|
|
810
815
|
}
|
|
@@ -815,7 +820,7 @@ export class DingtalkHarnessBridge {
|
|
|
815
820
|
if (!pending || pending !== expected || pending.submitting) {
|
|
816
821
|
if (claimed && (!pending || pending !== expected)) {
|
|
817
822
|
try {
|
|
818
|
-
await this.#send(sessionWebhook, INTERACTION_RESOLVED_TEXT);
|
|
823
|
+
await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT));
|
|
819
824
|
} catch {
|
|
820
825
|
this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice');
|
|
821
826
|
}
|
|
@@ -831,7 +836,7 @@ export class DingtalkHarnessBridge {
|
|
|
831
836
|
try {
|
|
832
837
|
await this.#presentInteraction(pending);
|
|
833
838
|
} catch {
|
|
834
|
-
this.#status.lastError = '钉钉交互问题发送失败。';
|
|
839
|
+
this.#status.lastError = t('钉钉交互问题发送失败。');
|
|
835
840
|
this.#logger.error?.('[dsh-dingtalk] failed to retry an interaction question');
|
|
836
841
|
pending.interaction.reconnect?.();
|
|
837
842
|
}
|
|
@@ -850,7 +855,7 @@ export class DingtalkHarnessBridge {
|
|
|
850
855
|
try {
|
|
851
856
|
await this.#presentInteraction(pending);
|
|
852
857
|
} catch {
|
|
853
|
-
this.#status.lastError = '钉钉交互问题发送失败。';
|
|
858
|
+
this.#status.lastError = t('钉钉交互问题发送失败。');
|
|
854
859
|
this.#logger.error?.('[dsh-dingtalk] failed to send the next interaction question');
|
|
855
860
|
pending.interaction.reconnect?.();
|
|
856
861
|
}
|
|
@@ -874,7 +879,7 @@ export class DingtalkHarnessBridge {
|
|
|
874
879
|
if (error?.code === 'interaction-not-pending') {
|
|
875
880
|
this.#clearPendingInteraction(key, pending.interactionId);
|
|
876
881
|
try {
|
|
877
|
-
await this.#send(sessionWebhook, INTERACTION_RESOLVED_TEXT);
|
|
882
|
+
await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT));
|
|
878
883
|
} catch {
|
|
879
884
|
this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice');
|
|
880
885
|
}
|
|
@@ -883,10 +888,10 @@ export class DingtalkHarnessBridge {
|
|
|
883
888
|
pending.submitting = false;
|
|
884
889
|
pending.answers.pop();
|
|
885
890
|
pending.index -= 1;
|
|
886
|
-
this.#status.lastError = '回答提交失败。';
|
|
891
|
+
this.#status.lastError = t('回答提交失败。');
|
|
887
892
|
this.#logger.error?.('[dsh-dingtalk] failed to answer a Harness interaction');
|
|
888
893
|
try {
|
|
889
|
-
await this.#send(sessionWebhook, '回答提交失败,请重新发送当前问题的答案。');
|
|
894
|
+
await this.#send(sessionWebhook, t('回答提交失败,请重新发送当前问题的答案。'));
|
|
890
895
|
} catch {
|
|
891
896
|
this.#logger.error?.('[dsh-dingtalk] failed to send an interaction retry notice');
|
|
892
897
|
}
|
|
@@ -932,7 +937,7 @@ export class DingtalkHarnessBridge {
|
|
|
932
937
|
},
|
|
933
938
|
});
|
|
934
939
|
try {
|
|
935
|
-
await this.#send(sessionWebhook, '检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。');
|
|
940
|
+
await this.#send(sessionWebhook, t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'));
|
|
936
941
|
} catch {
|
|
937
942
|
this.#logger.error?.('[dsh-dingtalk] failed to send an interaction recovery notice');
|
|
938
943
|
}
|
|
@@ -1018,7 +1023,7 @@ export class DingtalkHarnessBridge {
|
|
|
1018
1023
|
return;
|
|
1019
1024
|
}
|
|
1020
1025
|
try {
|
|
1021
|
-
await this.#send(sessionWebhook, INTERACTION_RESOLVED_TEXT);
|
|
1026
|
+
await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT));
|
|
1022
1027
|
} catch {
|
|
1023
1028
|
this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice');
|
|
1024
1029
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { t } from '../shared/i18n.mjs';
|
|
2
|
+
|
|
1
3
|
const DEFAULT_UPDATE_INTERVAL_MS = 500;
|
|
2
4
|
const FAILURE_TEXT = '消息处理失败,请稍后重试。';
|
|
3
5
|
|
|
@@ -100,7 +102,7 @@ export function createDingTalkCardStream({
|
|
|
100
102
|
if (!cleanupPromise) {
|
|
101
103
|
cleanupPromise = api.failAiCard({
|
|
102
104
|
...cardRequest,
|
|
103
|
-
text: FAILURE_TEXT,
|
|
105
|
+
text: t(FAILURE_TEXT),
|
|
104
106
|
signal: AbortSignal.timeout(5_000),
|
|
105
107
|
}).then(
|
|
106
108
|
() => true,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
connectionTestMessage,
|
|
11
11
|
connectionTestTargetUnavailable,
|
|
12
12
|
} from '../shared/connection-test.mjs';
|
|
13
|
+
import { t } from '../shared/i18n.mjs';
|
|
13
14
|
|
|
14
15
|
const ACTIVE_ATTEMPT_STATES = new Set(['starting', 'pending', 'connecting']);
|
|
15
16
|
const TERMINAL_ATTEMPT_STATES = new Set(['connected', 'expired', 'failed', 'cancelled']);
|
|
@@ -84,7 +85,7 @@ function normalizePendingSender(value) {
|
|
|
84
85
|
return {
|
|
85
86
|
requestId: opaqueRequestId,
|
|
86
87
|
staffId,
|
|
87
|
-
displayName: cleanString(value.displayName ?? value.senderName ?? value.senderNick) ?? '钉钉用户',
|
|
88
|
+
displayName: cleanString(value.displayName ?? value.senderName ?? value.senderNick) ?? t('钉钉用户'),
|
|
88
89
|
requestedAt: cleanString(value.requestedAt),
|
|
89
90
|
};
|
|
90
91
|
}
|
|
@@ -114,7 +115,7 @@ function publicPendingSender(sender) {
|
|
|
114
115
|
function publicApprovedSender(sender) {
|
|
115
116
|
return {
|
|
116
117
|
senderKey: sender.senderKey,
|
|
117
|
-
displayName: cleanString(sender.displayName) ?? '钉钉用户',
|
|
118
|
+
displayName: cleanString(sender.displayName) ?? t('钉钉用户'),
|
|
118
119
|
senderIdMasked: maskDingtalkSenderId(sender.staffId),
|
|
119
120
|
approvedAt: cleanString(sender.approvedAt),
|
|
120
121
|
};
|
|
@@ -206,7 +207,7 @@ export class DingtalkController {
|
|
|
206
207
|
if (!clientSecret) {
|
|
207
208
|
this.#errors.set(
|
|
208
209
|
latest.botId,
|
|
209
|
-
safeError('missing-secret', '钉钉机器人凭据缺失,请移除后重新扫码。'),
|
|
210
|
+
safeError('missing-secret', t('钉钉机器人凭据缺失,请移除后重新扫码。')),
|
|
210
211
|
);
|
|
211
212
|
this.#touch();
|
|
212
213
|
return;
|
|
@@ -217,7 +218,7 @@ export class DingtalkController {
|
|
|
217
218
|
} catch {
|
|
218
219
|
this.#errors.set(
|
|
219
220
|
latest.botId,
|
|
220
|
-
safeError('connection-failed', '钉钉连接未就绪,请稍后重试。'),
|
|
221
|
+
safeError('connection-failed', t('钉钉连接未就绪,请稍后重试。')),
|
|
221
222
|
);
|
|
222
223
|
this.#logger.warn?.(`[dsh-dingtalk] bot ${latest.botId} failed to initialize`);
|
|
223
224
|
}
|
|
@@ -270,10 +271,10 @@ export class DingtalkController {
|
|
|
270
271
|
} catch (error) {
|
|
271
272
|
if (record.controller.signal.aborted || error?.name === 'AbortError') {
|
|
272
273
|
record.state = 'cancelled';
|
|
273
|
-
record.error = safeError('cancelled', '扫码接入已取消。');
|
|
274
|
+
record.error = safeError('cancelled', t('扫码接入已取消。'));
|
|
274
275
|
} else {
|
|
275
276
|
record.state = 'failed';
|
|
276
|
-
record.error = safeError('qr-start-failed', '无法生成钉钉二维码,请稍后重试。');
|
|
277
|
+
record.error = safeError('qr-start-failed', t('无法生成钉钉二维码,请稍后重试。'));
|
|
277
278
|
}
|
|
278
279
|
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
|
279
280
|
this.#touch();
|
|
@@ -319,7 +320,7 @@ export class DingtalkController {
|
|
|
319
320
|
} catch {
|
|
320
321
|
this.#errors.set(
|
|
321
322
|
identity.botId,
|
|
322
|
-
safeError('connection-failed', '钉钉已接入,但消息连接暂未就绪,请稍后重试。'),
|
|
323
|
+
safeError('connection-failed', t('钉钉已接入,但消息连接暂未就绪,请稍后重试。')),
|
|
323
324
|
);
|
|
324
325
|
this.#logger.warn?.('[dsh-dingtalk] credential-bound bot saved but its connection is not ready');
|
|
325
326
|
}
|
|
@@ -337,7 +338,7 @@ export class DingtalkController {
|
|
|
337
338
|
}
|
|
338
339
|
if (nowFrom(this.#clock) >= record.expiresAt) {
|
|
339
340
|
record.state = 'expired';
|
|
340
|
-
record.error = safeError('expired', '二维码已过期,请重新生成。');
|
|
341
|
+
record.error = safeError('expired', t('二维码已过期,请重新生成。'));
|
|
341
342
|
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
|
342
343
|
this.#touch();
|
|
343
344
|
return publicAttempt(record);
|
|
@@ -360,7 +361,7 @@ export class DingtalkController {
|
|
|
360
361
|
record.controller.abort();
|
|
361
362
|
await record.pollTask?.catch(() => undefined);
|
|
362
363
|
if (!TERMINAL_ATTEMPT_STATES.has(record.state)) record.state = 'cancelled';
|
|
363
|
-
record.error ??= safeError('cancelled', '扫码接入已取消。');
|
|
364
|
+
record.error ??= safeError('cancelled', t('扫码接入已取消。'));
|
|
364
365
|
}
|
|
365
366
|
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
|
366
367
|
this.#touch();
|
|
@@ -380,7 +381,7 @@ export class DingtalkController {
|
|
|
380
381
|
} catch (error) {
|
|
381
382
|
this.#errors.set(
|
|
382
383
|
botId,
|
|
383
|
-
safeError('connection-failed', '钉钉连接仍未就绪,请稍后重试。'),
|
|
384
|
+
safeError('connection-failed', t('钉钉连接仍未就绪,请稍后重试。')),
|
|
384
385
|
);
|
|
385
386
|
throw error;
|
|
386
387
|
} finally {
|
|
@@ -396,10 +397,10 @@ export class DingtalkController {
|
|
|
396
397
|
return this.#withBotTransition(botId, async () => {
|
|
397
398
|
const runtime = this.#runtimes.get(botId);
|
|
398
399
|
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
|
399
|
-
throw connectionTestTargetUnavailable('钉钉机器人');
|
|
400
|
+
throw connectionTestTargetUnavailable(t('钉钉机器人'));
|
|
400
401
|
}
|
|
401
402
|
return runtime.sendConnectionTest(connectionTestMessage(
|
|
402
|
-
|
|
403
|
+
t('钉钉机器人({clientId})', { clientId: maskDingtalkClientId(config.clientId) }),
|
|
403
404
|
));
|
|
404
405
|
});
|
|
405
406
|
}
|
|
@@ -501,14 +502,14 @@ export class DingtalkController {
|
|
|
501
502
|
connected,
|
|
502
503
|
configured: true,
|
|
503
504
|
bot: {
|
|
504
|
-
name: '钉钉机器人',
|
|
505
|
+
name: t('钉钉机器人'),
|
|
505
506
|
clientIdMasked: maskDingtalkClientId(config.clientId),
|
|
506
507
|
},
|
|
507
508
|
health: {
|
|
508
509
|
status: connected ? 'healthy' : accountError ? 'error' : 'offline',
|
|
509
510
|
summary: connected
|
|
510
|
-
? '钉钉 Stream 消息连接运行正常'
|
|
511
|
-
: accountError?.message ?? '钉钉消息连接当前离线',
|
|
511
|
+
? t('钉钉 Stream 消息连接运行正常')
|
|
512
|
+
: accountError?.message ?? t('钉钉消息连接当前离线'),
|
|
512
513
|
lastCheckedAt: currentStatus.lastCheckedAt ?? null,
|
|
513
514
|
},
|
|
514
515
|
stats: {
|
|
@@ -580,32 +581,32 @@ export class DingtalkController {
|
|
|
580
581
|
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
|
581
582
|
} else if (state === 'EXPIRED') {
|
|
582
583
|
record.state = 'expired';
|
|
583
|
-
record.error = safeError('expired', '二维码已过期,请重新生成。');
|
|
584
|
+
record.error = safeError('expired', t('二维码已过期,请重新生成。'));
|
|
584
585
|
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
|
585
586
|
} else if (state === 'FAIL') {
|
|
586
587
|
record.state = 'failed';
|
|
587
|
-
record.error = safeError('authorization-failed', '钉钉未完成机器人授权,请重新扫码。');
|
|
588
|
+
record.error = safeError('authorization-failed', t('钉钉未完成机器人授权,请重新扫码。'));
|
|
588
589
|
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
|
589
590
|
} else {
|
|
590
591
|
record.state = 'pending';
|
|
591
|
-
record.error = safeError('poll-pending', '钉钉授权状态暂时不可用,正在重试。');
|
|
592
|
+
record.error = safeError('poll-pending', t('钉钉授权状态暂时不可用,正在重试。'));
|
|
592
593
|
}
|
|
593
594
|
} catch (error) {
|
|
594
595
|
if (record.controller.signal.aborted || error?.name === 'AbortError') {
|
|
595
596
|
record.state = 'cancelled';
|
|
596
|
-
record.error = safeError('cancelled', '扫码接入已取消。');
|
|
597
|
+
record.error = safeError('cancelled', t('扫码接入已取消。'));
|
|
597
598
|
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
|
598
599
|
} else if (record.state === 'connecting') {
|
|
599
600
|
record.state = 'failed';
|
|
600
601
|
record.error = safeError(
|
|
601
602
|
'activation-failed',
|
|
602
|
-
'钉钉已授权,但无法安全保存接入配置。',
|
|
603
|
+
t('钉钉已授权,但无法安全保存接入配置。'),
|
|
603
604
|
);
|
|
604
605
|
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
|
605
606
|
this.#logger.error?.('[dsh-dingtalk] bot activation failed');
|
|
606
607
|
} else {
|
|
607
608
|
record.state = 'pending';
|
|
608
|
-
record.error = safeError('poll-failed', '钉钉授权查询暂时失败,正在重试。');
|
|
609
|
+
record.error = safeError('poll-failed', t('钉钉授权查询暂时失败,正在重试。'));
|
|
609
610
|
}
|
|
610
611
|
} finally {
|
|
611
612
|
this.#touch();
|
|
@@ -660,7 +661,7 @@ export class DingtalkController {
|
|
|
660
661
|
}
|
|
661
662
|
this.#errors.set(
|
|
662
663
|
identity.botId,
|
|
663
|
-
safeError('connection-failed', '钉钉已接入,但消息连接暂未就绪,请稍后重试。'),
|
|
664
|
+
safeError('connection-failed', t('钉钉已接入,但消息连接暂未就绪,请稍后重试。')),
|
|
664
665
|
);
|
|
665
666
|
this.#logger.warn?.('[dsh-dingtalk] authorized bot saved but its connection is not ready');
|
|
666
667
|
}
|
|
@@ -681,7 +682,7 @@ export class DingtalkController {
|
|
|
681
682
|
await this.#startRuntime(previousConfig, clientSecret).catch(() => undefined);
|
|
682
683
|
this.#errors.set(
|
|
683
684
|
previousConfig.botId,
|
|
684
|
-
safeError('connection-failed', '钉钉连接未就绪,请稍后重试。'),
|
|
685
|
+
safeError('connection-failed', t('钉钉连接未就绪,请稍后重试。')),
|
|
685
686
|
);
|
|
686
687
|
throw error;
|
|
687
688
|
} finally {
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
DingtalkHarnessBridge,
|
|
5
5
|
} from './dingtalk-bridge.mjs';
|
|
6
6
|
import { sendRememberedConnectionTest } from '../shared/connection-test.mjs';
|
|
7
|
+
import { t } from '../shared/i18n.mjs';
|
|
7
8
|
|
|
8
9
|
function nonEmptyString(value) {
|
|
9
10
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
@@ -275,7 +276,7 @@ export class DingtalkRuntime {
|
|
|
275
276
|
? JSON.parse(response.data)
|
|
276
277
|
: response?.data;
|
|
277
278
|
} catch {
|
|
278
|
-
this.#status.lastError = '钉钉消息格式无效。';
|
|
279
|
+
this.#status.lastError = t('钉钉消息格式无效。');
|
|
279
280
|
this.#logger.warn?.('[dsh-dingtalk] ignored an invalid callback payload');
|
|
280
281
|
return;
|
|
281
282
|
}
|
|
@@ -284,7 +285,7 @@ export class DingtalkRuntime {
|
|
|
284
285
|
await bridge.accept(message);
|
|
285
286
|
}).catch(() => {
|
|
286
287
|
if (signal.aborted || this.#bridge !== bridge) return;
|
|
287
|
-
this.#status.lastError = '钉钉消息处理失败。';
|
|
288
|
+
this.#status.lastError = t('钉钉消息处理失败。');
|
|
288
289
|
this.#logger.error?.('[dsh-dingtalk] callback processing failed');
|
|
289
290
|
}).finally(() => this.#callbackTasks.delete(task));
|
|
290
291
|
this.#callbackTasks.add(task);
|
|
@@ -352,7 +353,7 @@ export class DingtalkRuntime {
|
|
|
352
353
|
return sendRememberedConnectionTest({
|
|
353
354
|
state: this.#state,
|
|
354
355
|
text,
|
|
355
|
-
channelLabel: '钉钉机器人',
|
|
356
|
+
channelLabel: t('钉钉机器人'),
|
|
356
357
|
send: async ({ sessionWebhook }, content) => {
|
|
357
358
|
if (!this.#status.ready || !this.#abortController) {
|
|
358
359
|
throw new Error('DingTalk runtime is not connected');
|
|
@@ -2,6 +2,8 @@ import { 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_STATE = Object.freeze({
|
|
6
8
|
version: 1,
|
|
7
9
|
sessions: {},
|
|
@@ -14,7 +16,7 @@ function nonEmptyString(value) {
|
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
function displayName(value) {
|
|
17
|
-
return (nonEmptyString(value) ?? '钉钉用户').slice(0, 100);
|
|
19
|
+
return (nonEmptyString(value) ?? t('钉钉用户')).slice(0, 100);
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
function normalizePendingSender(value, fallbackRequestId) {
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
maskPlatformId,
|
|
4
4
|
TokenBotConfigStore,
|
|
5
5
|
} from '../shared/token-config-store.mjs';
|
|
6
|
+
import { t } from '../shared/i18n.mjs';
|
|
6
7
|
|
|
7
8
|
const IDENTITY_OPTIONS = Object.freeze({
|
|
8
9
|
botPrefix: 'discord',
|
|
@@ -14,7 +15,7 @@ export function deriveDiscordBotIdentity(platformId) {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export function maskDiscordBotId(platformId) {
|
|
17
|
-
return maskPlatformId(platformId, 'Discord机器人');
|
|
18
|
+
return maskPlatformId(platformId, t('Discord机器人'));
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export class DiscordConfigStore extends TokenBotConfigStore {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { t } from '../shared/i18n.mjs';
|
|
2
3
|
|
|
3
4
|
const DEFAULT_BASE_URL = 'https://discord.com/api/v10/';
|
|
4
5
|
const DEFAULT_FILE_UPLOAD_TIMEOUT_MS = 120_000;
|
|
@@ -120,6 +121,29 @@ export class DiscordApi {
|
|
|
120
121
|
return this.#request('gateway/bot', { ...options, method: 'GET' });
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
getChannel({ channelId, signal } = {}) {
|
|
125
|
+
return this.#request(`channels/${snowflake(channelId, 'channel id')}`, {
|
|
126
|
+
method: 'GET',
|
|
127
|
+
signal,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
startThreadFromMessage({ channelId, messageId, name, signal } = {}) {
|
|
132
|
+
const threadName = cleanString(name);
|
|
133
|
+
if (!threadName || [...threadName].length > 100) {
|
|
134
|
+
throw new TypeError('Discord thread name must contain 1-100 characters');
|
|
135
|
+
}
|
|
136
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
137
|
+
return this.#request(
|
|
138
|
+
`channels/${snowflake(channelId, 'channel id')}/messages/${snowflake(messageId, 'message id')}/threads`,
|
|
139
|
+
{
|
|
140
|
+
method: 'POST',
|
|
141
|
+
signal,
|
|
142
|
+
body: { name: threadName },
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
123
147
|
createMessage({ channelId, content, replyToMessageId, signal }) {
|
|
124
148
|
return this.#request(`channels/${snowflake(channelId, 'channel id')}/messages`, {
|
|
125
149
|
method: 'POST',
|
|
@@ -275,7 +299,7 @@ export async function inspectDiscordToken(token, options = {}) {
|
|
|
275
299
|
if (!bot?.id || bot?.bot !== true) throw new Error('Discord token does not belong to a bot');
|
|
276
300
|
return {
|
|
277
301
|
platformId: String(bot.id),
|
|
278
|
-
name: cleanString(bot.global_name) ?? cleanString(bot.username) ?? 'Discord机器人',
|
|
302
|
+
name: cleanString(bot.global_name) ?? cleanString(bot.username) ?? t('Discord机器人'),
|
|
279
303
|
username: cleanString(bot.username),
|
|
280
304
|
};
|
|
281
305
|
}
|