mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-10 19:30:46 +08:00
feat(i18n): add English support for host-side chat messages
Wrap all user-facing chat text in src/channels with a host-side t() translator (Chinese is the identity default, so untranslated text is always sent verbatim in Chinese). English is selected via the host plugin config `language: en` or the DSH_IM_LANGUAGE environment variable, and the settings UI keeps following the DSH web locale. - add src/channels/shared/i18n.mjs with setImHostLanguage/t() and a Chinese-key -> English dictionary split by area (shared + channels) - complete the English dictionary for every t() key in src/channels, including the feishu channel (welcome/help, repair flow, interactive cards, watch flow) and the shared command/approval/stream text - wrap previously untranslated chat-facing strings in feishu bridge, feishu runtime, editable-message-stream, harness-client, connection-test and image-prompt - dedupe dictionary entries that existed in multiple channel files and canonicalize conflicting translations into the shared dictionaries - extend the i18n unit test to decode string-literal escapes so keys containing \n are checked correctly - document the language switch in README.md, README.en.md and CHANGELOG.md, and rebuild lib/index.js Added English support for bot chat messages across all nine IM channels and AI Office without changing any Chinese output.
This commit is contained in:
parent
3ff8c2a519
commit
dcd7cd64d8
78 changed files with 2283 additions and 992 deletions
|
|
@ -2,6 +2,8 @@ import { createHash, randomUUID } from 'node:crypto';
|
|||
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
||||
import { dirname } from 'node:path';
|
||||
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const EMPTY_DOCUMENT = Object.freeze({ version: 1, bots: Object.freeze([]) });
|
||||
const STORED_BOT_KEYS = new Set(['clientId', 'secretRef', 'approvedSenders']);
|
||||
|
||||
|
|
@ -86,8 +88,8 @@ export function deriveDingtalkSenderKey() {
|
|||
*/
|
||||
export function maskDingtalkSenderId(staffId) {
|
||||
const value = cleanString(staffId);
|
||||
if (!value) return '钉钉用户';
|
||||
return '身份已隐藏';
|
||||
if (!value) return t('钉钉用户');
|
||||
return t('身份已隐藏');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -97,7 +99,7 @@ export function maskDingtalkSenderId(staffId) {
|
|||
*/
|
||||
export function maskDingtalkClientId(clientId) {
|
||||
const value = cleanString(clientId);
|
||||
if (!value) return '钉钉机器人';
|
||||
if (!value) return t('钉钉机器人');
|
||||
if (value.length <= 8) return `${value.slice(0, 2)}••••`;
|
||||
return `${value.slice(0, 4)}••••${value.slice(-4)}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|||
import { extname } from 'node:path';
|
||||
|
||||
import { fetchImageBuffer, ImagePromptError } from '../shared/image-prompt.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
export const DINGTALK_REGISTRATION_BASE_URL = 'https://oapi.dingtalk.com/';
|
||||
export const DINGTALK_API_BASE_URL = 'https://api.dingtalk.com/';
|
||||
|
|
@ -829,7 +830,7 @@ export function createDingtalkApi({
|
|||
clientId: appKey,
|
||||
clientSecret: appSecret,
|
||||
cardInstanceId,
|
||||
text: '消息处理失败,请稍后重试。',
|
||||
text: t('消息处理失败,请稍后重试。'),
|
||||
signal: cleanupSignal,
|
||||
}).catch(() => undefined);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,12 +40,13 @@ import {
|
|||
createDeliveryReceipt,
|
||||
providerMessageIdsFor,
|
||||
} from '../shared/semantic/delivery.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const CARD_INITIAL_TEXT = '已连接 DeepSeek Harness,正在思考…';
|
||||
const CARD_ERROR_TEXT = '消息处理失败,请稍后重试。';
|
||||
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
||||
|
||||
const HELP_TEXT = [
|
||||
const HELP_TEXT_LINES = [
|
||||
'钉钉机器人已连接 DeepSeek Harness。',
|
||||
'',
|
||||
'直接发送文字、图片或文件即可继续当前会话。',
|
||||
|
|
@ -66,7 +67,11 @@ const HELP_TEXT = [
|
|||
'/steer 补充指令 纠偏当前任务',
|
||||
'/status 检查连接状态',
|
||||
'/help 显示本帮助',
|
||||
].join('\n');
|
||||
];
|
||||
|
||||
function helpText() {
|
||||
return HELP_TEXT_LINES.map((line) => t(line)).join('\n');
|
||||
}
|
||||
|
||||
function nonEmptyString(value) {
|
||||
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
||||
|
|
@ -106,13 +111,13 @@ function dingtalkImageErrorUserMessage(error) {
|
|||
while (current && typeof current === 'object' && !seen.has(current)) {
|
||||
seen.add(current);
|
||||
if (current.code === 'image-download-address-failed') {
|
||||
return '钉钉未能换取图片下载地址,请重新发送;若持续失败,请检查机器人的“企业内机器人发送消息权限”。';
|
||||
return t('钉钉未能换取图片下载地址,请重新发送;若持续失败,请检查机器人的“企业内机器人发送消息权限”。');
|
||||
}
|
||||
if (current.code === 'invalid-image-download') {
|
||||
return '钉钉没有返回图片下载地址,请重新发送。';
|
||||
return t('钉钉没有返回图片下载地址,请重新发送。');
|
||||
}
|
||||
if (current.code === 'image-content-download-failed') {
|
||||
return '钉钉返回的图片临时地址无法读取,请重新发送。';
|
||||
return t('钉钉返回的图片临时地址无法读取,请重新发送。');
|
||||
}
|
||||
current = current.cause;
|
||||
}
|
||||
|
|
@ -241,34 +246,34 @@ function fileTarget(message, sender, clientId) {
|
|||
}
|
||||
|
||||
function artifactFailureText(fileName, error) {
|
||||
const name = String(fileName ?? '结果文件').replace(/[\r\n]+/g, ' ').trim() || '结果文件';
|
||||
const name = String(fileName ?? t('结果文件')).replace(/[\r\n]+/g, ' ').trim() || t('结果文件');
|
||||
switch (error?.code) {
|
||||
case 'artifact-delivery-uncertain':
|
||||
return `结果文件「${name}」发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。`;
|
||||
return t('结果文件「{name}」发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
|
||||
case 'artifact-permission-required':
|
||||
return `结果文件「${name}」已生成,但钉钉应用或机器人缺少文件消息权限。请开通应用 qyapi_base 权限,并确认机器人具备文件消息发送能力。`;
|
||||
return t('结果文件「{name}」已生成,但钉钉应用或机器人缺少文件消息权限。请开通应用 qyapi_base 权限,并确认机器人具备文件消息发送能力。', { name });
|
||||
case 'artifact-too-large':
|
||||
return `结果文件「${name}」超过当前钉钉机器人可发送的文件大小,未发送。`;
|
||||
return t('结果文件「{name}」超过当前钉钉机器人可发送的文件大小,未发送。', { name });
|
||||
case 'artifact-rate-limited':
|
||||
return `结果文件「${name}」暂时被钉钉限流,未能发送,请稍后重试。`;
|
||||
return t('结果文件「{name}」暂时被钉钉限流,未能发送,请稍后重试。', { name });
|
||||
case 'artifact-provider-rejected':
|
||||
return `结果文件「${name}」已生成,但钉钉拒绝了该文件消息,请检查文件类型和机器人文件消息配置。`;
|
||||
return t('结果文件「{name}」已生成,但钉钉拒绝了该文件消息,请检查文件类型和机器人文件消息配置。', { name });
|
||||
case 'artifact-invalid':
|
||||
case 'artifact-changed':
|
||||
case 'artifact-unavailable':
|
||||
return `结果文件「${name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。`;
|
||||
return t('结果文件「{name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。', { name });
|
||||
default:
|
||||
return `结果文件「${name}」已生成,但暂时未能通过钉钉发送,请稍后重试。`;
|
||||
return t('结果文件「{name}」已生成,但暂时未能通过钉钉发送,请稍后重试。', { name });
|
||||
}
|
||||
}
|
||||
|
||||
function progressText(update) {
|
||||
if (update?.type === 'text' && nonEmptyString(update.text)) return update.text;
|
||||
if (update?.type === 'tool') {
|
||||
if (update.name === 'web_search') return '_正在搜索网络并整理信息…_';
|
||||
return `_正在使用 ${nonEmptyString(update.name) ?? '工具'}…_`;
|
||||
if (update.name === 'web_search') return t('_正在搜索网络并整理信息…_');
|
||||
return t('_正在使用 {name}…_', { name: nonEmptyString(update.name) ?? t('工具') });
|
||||
}
|
||||
return `_${nonEmptyString(update?.text) ?? '正在处理…'}_`;
|
||||
return t('_{text}_', { text: nonEmptyString(update?.text) ?? t('正在处理…') });
|
||||
}
|
||||
|
||||
function canClaimInteractionReply(message, pending, sender) {
|
||||
|
|
@ -426,9 +431,9 @@ export class DingtalkHarnessBridge {
|
|||
commandRunner,
|
||||
).catch((error) => {
|
||||
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
|
||||
this.#status.lastError = '钉钉命令处理失败。';
|
||||
this.#status.lastError = t('钉钉命令处理失败。');
|
||||
this.#logger.error?.('[dsh-dingtalk] failed to process a command', safeErrorDiagnostic(error));
|
||||
return this.#send(sessionWebhook, CARD_ERROR_TEXT).catch(() => undefined);
|
||||
return this.#send(sessionWebhook, t(CARD_ERROR_TEXT)).catch(() => undefined);
|
||||
}).finally(() => {
|
||||
this.#acceptedMessageIds.delete(messageId);
|
||||
this.#commandTasks.delete(task);
|
||||
|
|
@ -463,13 +468,13 @@ export class DingtalkHarnessBridge {
|
|||
if (!sessionWebhook) {
|
||||
increment(this.#status, 'messagesRejected');
|
||||
this.#status.lastRejectedAt = new Date().toISOString();
|
||||
this.#status.lastError = '钉钉消息没有安全的回复地址。';
|
||||
this.#status.lastError = t('钉钉消息没有安全的回复地址。');
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.catch((error) => {
|
||||
if (this.#signal?.aborted) return;
|
||||
this.#status.lastError = '钉钉审批处理失败。';
|
||||
this.#status.lastError = t('钉钉审批处理失败。');
|
||||
this.#logger.error?.('[dsh-dingtalk] failed to process an approval reply', error);
|
||||
})
|
||||
.finally(() => {
|
||||
|
|
@ -611,7 +616,7 @@ export class DingtalkHarnessBridge {
|
|||
} catch {
|
||||
increment(this.#status, 'messagesRejected');
|
||||
this.#status.lastRejectedAt = new Date().toISOString();
|
||||
this.#status.lastError = '钉钉消息没有安全的回复地址。';
|
||||
this.#status.lastError = t('钉钉消息没有安全的回复地址。');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -628,23 +633,23 @@ export class DingtalkHarnessBridge {
|
|||
let cardStarted = false;
|
||||
try {
|
||||
if (!text && !hasImages && !hasFiles) {
|
||||
await this.#send(sessionWebhook, '目前支持文字、图片和文件消息。');
|
||||
await this.#send(sessionWebhook, t('目前支持文字、图片和文件消息。'));
|
||||
return;
|
||||
}
|
||||
|
||||
const command = text.toLowerCase();
|
||||
if (isPlainText && !hasImages && !hasFiles && command === '/help') {
|
||||
await this.#send(sessionWebhook, HELP_TEXT);
|
||||
await this.#send(sessionWebhook, helpText());
|
||||
return;
|
||||
}
|
||||
if (isPlainText && !hasImages && !hasFiles && command === '/status') {
|
||||
await this.#harness.ensureRunning({ signal: this.#signal });
|
||||
await this.#send(sessionWebhook, '钉钉机器人与 DeepSeek Harness 连接正常。');
|
||||
await this.#send(sessionWebhook, t('钉钉机器人与 DeepSeek Harness 连接正常。'));
|
||||
return;
|
||||
}
|
||||
if (isPlainText && !hasImages && !hasFiles && command === '/new') {
|
||||
await this.#state.clearSession(key);
|
||||
await this.#send(sessionWebhook, '已开启新会话。请发送你的问题。');
|
||||
await this.#send(sessionWebhook, t('已开启新会话。请发送你的问题。'));
|
||||
return;
|
||||
}
|
||||
const workspaceCommand = isPlainText && !hasImages && !hasFiles
|
||||
|
|
@ -684,7 +689,7 @@ export class DingtalkHarnessBridge {
|
|||
signal: this.#signal,
|
||||
logger: this.#logger,
|
||||
});
|
||||
cardStarted = await cardStream.start(CARD_INITIAL_TEXT);
|
||||
cardStarted = await cardStream.start(t(CARD_INITIAL_TEXT));
|
||||
}
|
||||
const { answer, artifacts = [] } = await askInWorkspaceSession({
|
||||
harness: this.#harness,
|
||||
|
|
@ -712,7 +717,7 @@ export class DingtalkHarnessBridge {
|
|||
});
|
||||
const answerText = typeof answer === 'string' && answer.trim()
|
||||
? answer
|
||||
: artifacts.length > 0 ? '结果文件已生成。' : answer;
|
||||
: artifacts.length > 0 ? t('结果文件已生成。') : answer;
|
||||
let textDeliveryError = null;
|
||||
let textReceipt = null;
|
||||
let streamed = false;
|
||||
|
|
@ -747,11 +752,11 @@ export class DingtalkHarnessBridge {
|
|||
return delivery.receipt;
|
||||
} catch (error) {
|
||||
if (error?.code === 'turn-stopped') {
|
||||
if (cardStarted) await cardStream.finish('已停止。').catch(() => undefined);
|
||||
if (cardStarted) await cardStream.finish(t('已停止。')).catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
if (this.#signal?.aborted) return;
|
||||
this.#status.lastError = '钉钉消息处理失败。';
|
||||
this.#status.lastError = t('钉钉消息处理失败。');
|
||||
this.#logger.error?.(
|
||||
'[dsh-dingtalk] failed to process an inbound message',
|
||||
safeErrorDiagnostic(error),
|
||||
|
|
@ -759,7 +764,7 @@ export class DingtalkHarnessBridge {
|
|||
try {
|
||||
const errorText = inboundFileUserMessage(error)
|
||||
?? dingtalkImageErrorUserMessage(error)
|
||||
?? CARD_ERROR_TEXT;
|
||||
?? t(CARD_ERROR_TEXT);
|
||||
const streamed = cardStarted && await cardStream.finish(errorText);
|
||||
if (!streamed) await this.#send(sessionWebhook, errorText);
|
||||
} catch {
|
||||
|
|
@ -797,14 +802,14 @@ export class DingtalkHarnessBridge {
|
|||
} catch {
|
||||
increment(this.#status, 'messagesRejected');
|
||||
this.#status.lastRejectedAt = new Date().toISOString();
|
||||
this.#status.lastError = '钉钉消息没有安全的回复地址。';
|
||||
this.#status.lastError = t('钉钉消息没有安全的回复地址。');
|
||||
return;
|
||||
}
|
||||
|
||||
const text = message?.msgtype === 'text' ? nonEmptyString(message?.text?.content) : null;
|
||||
if (!text) {
|
||||
try {
|
||||
await this.#send(sessionWebhook, '请用文字回答当前问题。');
|
||||
await this.#send(sessionWebhook, t('请用文字回答当前问题。'));
|
||||
} catch {
|
||||
this.#logger.error?.('[dsh-dingtalk] failed to reject a non-text interaction reply');
|
||||
}
|
||||
|
|
@ -815,7 +820,7 @@ export class DingtalkHarnessBridge {
|
|||
if (!pending || pending !== expected || pending.submitting) {
|
||||
if (claimed && (!pending || pending !== expected)) {
|
||||
try {
|
||||
await this.#send(sessionWebhook, INTERACTION_RESOLVED_TEXT);
|
||||
await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT));
|
||||
} catch {
|
||||
this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice');
|
||||
}
|
||||
|
|
@ -831,7 +836,7 @@ export class DingtalkHarnessBridge {
|
|||
try {
|
||||
await this.#presentInteraction(pending);
|
||||
} catch {
|
||||
this.#status.lastError = '钉钉交互问题发送失败。';
|
||||
this.#status.lastError = t('钉钉交互问题发送失败。');
|
||||
this.#logger.error?.('[dsh-dingtalk] failed to retry an interaction question');
|
||||
pending.interaction.reconnect?.();
|
||||
}
|
||||
|
|
@ -850,7 +855,7 @@ export class DingtalkHarnessBridge {
|
|||
try {
|
||||
await this.#presentInteraction(pending);
|
||||
} catch {
|
||||
this.#status.lastError = '钉钉交互问题发送失败。';
|
||||
this.#status.lastError = t('钉钉交互问题发送失败。');
|
||||
this.#logger.error?.('[dsh-dingtalk] failed to send the next interaction question');
|
||||
pending.interaction.reconnect?.();
|
||||
}
|
||||
|
|
@ -874,7 +879,7 @@ export class DingtalkHarnessBridge {
|
|||
if (error?.code === 'interaction-not-pending') {
|
||||
this.#clearPendingInteraction(key, pending.interactionId);
|
||||
try {
|
||||
await this.#send(sessionWebhook, INTERACTION_RESOLVED_TEXT);
|
||||
await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT));
|
||||
} catch {
|
||||
this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice');
|
||||
}
|
||||
|
|
@ -883,10 +888,10 @@ export class DingtalkHarnessBridge {
|
|||
pending.submitting = false;
|
||||
pending.answers.pop();
|
||||
pending.index -= 1;
|
||||
this.#status.lastError = '回答提交失败。';
|
||||
this.#status.lastError = t('回答提交失败。');
|
||||
this.#logger.error?.('[dsh-dingtalk] failed to answer a Harness interaction');
|
||||
try {
|
||||
await this.#send(sessionWebhook, '回答提交失败,请重新发送当前问题的答案。');
|
||||
await this.#send(sessionWebhook, t('回答提交失败,请重新发送当前问题的答案。'));
|
||||
} catch {
|
||||
this.#logger.error?.('[dsh-dingtalk] failed to send an interaction retry notice');
|
||||
}
|
||||
|
|
@ -932,7 +937,7 @@ export class DingtalkHarnessBridge {
|
|||
},
|
||||
});
|
||||
try {
|
||||
await this.#send(sessionWebhook, '检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。');
|
||||
await this.#send(sessionWebhook, t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'));
|
||||
} catch {
|
||||
this.#logger.error?.('[dsh-dingtalk] failed to send an interaction recovery notice');
|
||||
}
|
||||
|
|
@ -1018,7 +1023,7 @@ export class DingtalkHarnessBridge {
|
|||
return;
|
||||
}
|
||||
try {
|
||||
await this.#send(sessionWebhook, INTERACTION_RESOLVED_TEXT);
|
||||
await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT));
|
||||
} catch {
|
||||
this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const DEFAULT_UPDATE_INTERVAL_MS = 500;
|
||||
const FAILURE_TEXT = '消息处理失败,请稍后重试。';
|
||||
|
||||
|
|
@ -100,7 +102,7 @@ export function createDingTalkCardStream({
|
|||
if (!cleanupPromise) {
|
||||
cleanupPromise = api.failAiCard({
|
||||
...cardRequest,
|
||||
text: FAILURE_TEXT,
|
||||
text: t(FAILURE_TEXT),
|
||||
signal: AbortSignal.timeout(5_000),
|
||||
}).then(
|
||||
() => true,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
connectionTestMessage,
|
||||
connectionTestTargetUnavailable,
|
||||
} from '../shared/connection-test.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const ACTIVE_ATTEMPT_STATES = new Set(['starting', 'pending', 'connecting']);
|
||||
const TERMINAL_ATTEMPT_STATES = new Set(['connected', 'expired', 'failed', 'cancelled']);
|
||||
|
|
@ -84,7 +85,7 @@ function normalizePendingSender(value) {
|
|||
return {
|
||||
requestId: opaqueRequestId,
|
||||
staffId,
|
||||
displayName: cleanString(value.displayName ?? value.senderName ?? value.senderNick) ?? '钉钉用户',
|
||||
displayName: cleanString(value.displayName ?? value.senderName ?? value.senderNick) ?? t('钉钉用户'),
|
||||
requestedAt: cleanString(value.requestedAt),
|
||||
};
|
||||
}
|
||||
|
|
@ -114,7 +115,7 @@ function publicPendingSender(sender) {
|
|||
function publicApprovedSender(sender) {
|
||||
return {
|
||||
senderKey: sender.senderKey,
|
||||
displayName: cleanString(sender.displayName) ?? '钉钉用户',
|
||||
displayName: cleanString(sender.displayName) ?? t('钉钉用户'),
|
||||
senderIdMasked: maskDingtalkSenderId(sender.staffId),
|
||||
approvedAt: cleanString(sender.approvedAt),
|
||||
};
|
||||
|
|
@ -206,7 +207,7 @@ export class DingtalkController {
|
|||
if (!clientSecret) {
|
||||
this.#errors.set(
|
||||
latest.botId,
|
||||
safeError('missing-secret', '钉钉机器人凭据缺失,请移除后重新扫码。'),
|
||||
safeError('missing-secret', t('钉钉机器人凭据缺失,请移除后重新扫码。')),
|
||||
);
|
||||
this.#touch();
|
||||
return;
|
||||
|
|
@ -217,7 +218,7 @@ export class DingtalkController {
|
|||
} catch {
|
||||
this.#errors.set(
|
||||
latest.botId,
|
||||
safeError('connection-failed', '钉钉连接未就绪,请稍后重试。'),
|
||||
safeError('connection-failed', t('钉钉连接未就绪,请稍后重试。')),
|
||||
);
|
||||
this.#logger.warn?.(`[dsh-dingtalk] bot ${latest.botId} failed to initialize`);
|
||||
}
|
||||
|
|
@ -270,10 +271,10 @@ export class DingtalkController {
|
|||
} catch (error) {
|
||||
if (record.controller.signal.aborted || error?.name === 'AbortError') {
|
||||
record.state = 'cancelled';
|
||||
record.error = safeError('cancelled', '扫码接入已取消。');
|
||||
record.error = safeError('cancelled', t('扫码接入已取消。'));
|
||||
} else {
|
||||
record.state = 'failed';
|
||||
record.error = safeError('qr-start-failed', '无法生成钉钉二维码,请稍后重试。');
|
||||
record.error = safeError('qr-start-failed', t('无法生成钉钉二维码,请稍后重试。'));
|
||||
}
|
||||
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
||||
this.#touch();
|
||||
|
|
@ -319,7 +320,7 @@ export class DingtalkController {
|
|||
} catch {
|
||||
this.#errors.set(
|
||||
identity.botId,
|
||||
safeError('connection-failed', '钉钉已接入,但消息连接暂未就绪,请稍后重试。'),
|
||||
safeError('connection-failed', t('钉钉已接入,但消息连接暂未就绪,请稍后重试。')),
|
||||
);
|
||||
this.#logger.warn?.('[dsh-dingtalk] credential-bound bot saved but its connection is not ready');
|
||||
}
|
||||
|
|
@ -337,7 +338,7 @@ export class DingtalkController {
|
|||
}
|
||||
if (nowFrom(this.#clock) >= record.expiresAt) {
|
||||
record.state = 'expired';
|
||||
record.error = safeError('expired', '二维码已过期,请重新生成。');
|
||||
record.error = safeError('expired', t('二维码已过期,请重新生成。'));
|
||||
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
||||
this.#touch();
|
||||
return publicAttempt(record);
|
||||
|
|
@ -360,7 +361,7 @@ export class DingtalkController {
|
|||
record.controller.abort();
|
||||
await record.pollTask?.catch(() => undefined);
|
||||
if (!TERMINAL_ATTEMPT_STATES.has(record.state)) record.state = 'cancelled';
|
||||
record.error ??= safeError('cancelled', '扫码接入已取消。');
|
||||
record.error ??= safeError('cancelled', t('扫码接入已取消。'));
|
||||
}
|
||||
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
||||
this.#touch();
|
||||
|
|
@ -380,7 +381,7 @@ export class DingtalkController {
|
|||
} catch (error) {
|
||||
this.#errors.set(
|
||||
botId,
|
||||
safeError('connection-failed', '钉钉连接仍未就绪,请稍后重试。'),
|
||||
safeError('connection-failed', t('钉钉连接仍未就绪,请稍后重试。')),
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
|
|
@ -396,10 +397,10 @@ export class DingtalkController {
|
|||
return this.#withBotTransition(botId, async () => {
|
||||
const runtime = this.#runtimes.get(botId);
|
||||
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
||||
throw connectionTestTargetUnavailable('钉钉机器人');
|
||||
throw connectionTestTargetUnavailable(t('钉钉机器人'));
|
||||
}
|
||||
return runtime.sendConnectionTest(connectionTestMessage(
|
||||
`钉钉机器人(${maskDingtalkClientId(config.clientId)})`,
|
||||
t('钉钉机器人({clientId})', { clientId: maskDingtalkClientId(config.clientId) }),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
|
@ -501,14 +502,14 @@ export class DingtalkController {
|
|||
connected,
|
||||
configured: true,
|
||||
bot: {
|
||||
name: '钉钉机器人',
|
||||
name: t('钉钉机器人'),
|
||||
clientIdMasked: maskDingtalkClientId(config.clientId),
|
||||
},
|
||||
health: {
|
||||
status: connected ? 'healthy' : accountError ? 'error' : 'offline',
|
||||
summary: connected
|
||||
? '钉钉 Stream 消息连接运行正常'
|
||||
: accountError?.message ?? '钉钉消息连接当前离线',
|
||||
? t('钉钉 Stream 消息连接运行正常')
|
||||
: accountError?.message ?? t('钉钉消息连接当前离线'),
|
||||
lastCheckedAt: currentStatus.lastCheckedAt ?? null,
|
||||
},
|
||||
stats: {
|
||||
|
|
@ -580,32 +581,32 @@ export class DingtalkController {
|
|||
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
||||
} else if (state === 'EXPIRED') {
|
||||
record.state = 'expired';
|
||||
record.error = safeError('expired', '二维码已过期,请重新生成。');
|
||||
record.error = safeError('expired', t('二维码已过期,请重新生成。'));
|
||||
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
||||
} else if (state === 'FAIL') {
|
||||
record.state = 'failed';
|
||||
record.error = safeError('authorization-failed', '钉钉未完成机器人授权,请重新扫码。');
|
||||
record.error = safeError('authorization-failed', t('钉钉未完成机器人授权,请重新扫码。'));
|
||||
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
||||
} else {
|
||||
record.state = 'pending';
|
||||
record.error = safeError('poll-pending', '钉钉授权状态暂时不可用,正在重试。');
|
||||
record.error = safeError('poll-pending', t('钉钉授权状态暂时不可用,正在重试。'));
|
||||
}
|
||||
} catch (error) {
|
||||
if (record.controller.signal.aborted || error?.name === 'AbortError') {
|
||||
record.state = 'cancelled';
|
||||
record.error = safeError('cancelled', '扫码接入已取消。');
|
||||
record.error = safeError('cancelled', t('扫码接入已取消。'));
|
||||
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
||||
} else if (record.state === 'connecting') {
|
||||
record.state = 'failed';
|
||||
record.error = safeError(
|
||||
'activation-failed',
|
||||
'钉钉已授权,但无法安全保存接入配置。',
|
||||
t('钉钉已授权,但无法安全保存接入配置。'),
|
||||
);
|
||||
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
||||
this.#logger.error?.('[dsh-dingtalk] bot activation failed');
|
||||
} else {
|
||||
record.state = 'pending';
|
||||
record.error = safeError('poll-failed', '钉钉授权查询暂时失败,正在重试。');
|
||||
record.error = safeError('poll-failed', t('钉钉授权查询暂时失败,正在重试。'));
|
||||
}
|
||||
} finally {
|
||||
this.#touch();
|
||||
|
|
@ -660,7 +661,7 @@ export class DingtalkController {
|
|||
}
|
||||
this.#errors.set(
|
||||
identity.botId,
|
||||
safeError('connection-failed', '钉钉已接入,但消息连接暂未就绪,请稍后重试。'),
|
||||
safeError('connection-failed', t('钉钉已接入,但消息连接暂未就绪,请稍后重试。')),
|
||||
);
|
||||
this.#logger.warn?.('[dsh-dingtalk] authorized bot saved but its connection is not ready');
|
||||
}
|
||||
|
|
@ -681,7 +682,7 @@ export class DingtalkController {
|
|||
await this.#startRuntime(previousConfig, clientSecret).catch(() => undefined);
|
||||
this.#errors.set(
|
||||
previousConfig.botId,
|
||||
safeError('connection-failed', '钉钉连接未就绪,请稍后重试。'),
|
||||
safeError('connection-failed', t('钉钉连接未就绪,请稍后重试。')),
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
DingtalkHarnessBridge,
|
||||
} from './dingtalk-bridge.mjs';
|
||||
import { sendRememberedConnectionTest } from '../shared/connection-test.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
function nonEmptyString(value) {
|
||||
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
||||
|
|
@ -275,7 +276,7 @@ export class DingtalkRuntime {
|
|||
? JSON.parse(response.data)
|
||||
: response?.data;
|
||||
} catch {
|
||||
this.#status.lastError = '钉钉消息格式无效。';
|
||||
this.#status.lastError = t('钉钉消息格式无效。');
|
||||
this.#logger.warn?.('[dsh-dingtalk] ignored an invalid callback payload');
|
||||
return;
|
||||
}
|
||||
|
|
@ -284,7 +285,7 @@ export class DingtalkRuntime {
|
|||
await bridge.accept(message);
|
||||
}).catch(() => {
|
||||
if (signal.aborted || this.#bridge !== bridge) return;
|
||||
this.#status.lastError = '钉钉消息处理失败。';
|
||||
this.#status.lastError = t('钉钉消息处理失败。');
|
||||
this.#logger.error?.('[dsh-dingtalk] callback processing failed');
|
||||
}).finally(() => this.#callbackTasks.delete(task));
|
||||
this.#callbackTasks.add(task);
|
||||
|
|
@ -352,7 +353,7 @@ export class DingtalkRuntime {
|
|||
return sendRememberedConnectionTest({
|
||||
state: this.#state,
|
||||
text,
|
||||
channelLabel: '钉钉机器人',
|
||||
channelLabel: t('钉钉机器人'),
|
||||
send: async ({ sessionWebhook }, content) => {
|
||||
if (!this.#status.ready || !this.#abortController) {
|
||||
throw new Error('DingTalk runtime is not connected');
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { randomUUID } from 'node:crypto';
|
|||
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
||||
import { dirname } from 'node:path';
|
||||
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const EMPTY_STATE = Object.freeze({
|
||||
version: 1,
|
||||
sessions: {},
|
||||
|
|
@ -14,7 +16,7 @@ function nonEmptyString(value) {
|
|||
}
|
||||
|
||||
function displayName(value) {
|
||||
return (nonEmptyString(value) ?? '钉钉用户').slice(0, 100);
|
||||
return (nonEmptyString(value) ?? t('钉钉用户')).slice(0, 100);
|
||||
}
|
||||
|
||||
function normalizePendingSender(value, fallbackRequestId) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue