mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-11 05:23:18 +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 } 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([]) });
|
||||
|
||||
function cleanString(value) {
|
||||
|
|
@ -30,7 +32,7 @@ export function deriveWecomBotIdentity(remoteBotId) {
|
|||
|
||||
export function maskWecomBotId(remoteBotId) {
|
||||
const value = cleanString(remoteBotId) ?? '';
|
||||
if (!value) return '企业微信机器人';
|
||||
if (!value) return t('企业微信机器人');
|
||||
if (value.length <= 10) return `${value.slice(0, 3)}•••`;
|
||||
return `${value.slice(0, 6)}••••${value.slice(-4)}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,35 +36,38 @@ import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.m
|
|||
import {
|
||||
createDeliveryReceipt,
|
||||
} from '../shared/semantic/delivery.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const DEFAULT_FILE_UPLOAD_TIMEOUT_MS = 120_000;
|
||||
|
||||
const HELP_TEXT = [
|
||||
'企业微信机器人已连接 DeepSeek Harness。',
|
||||
'',
|
||||
'直接发送文字、图片或文件即可继续当前会话。',
|
||||
'/new 开启一个全新会话',
|
||||
'/compact 压缩当前会话的较早上下文',
|
||||
'/workspace 工作区绝对路径 切换工作区',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
||||
'/models 按序号列出所有可用模型',
|
||||
'/model [序号或完整模型ID] 查看或切换当前会话模型',
|
||||
'示例:先发 /models,再发 /model 2',
|
||||
'/presetlist 按序号列出可用 Agent Preset',
|
||||
'/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
|
||||
'纯数字 ID:/preset id:<ID>',
|
||||
'/preset --default 跟随 Host 默认',
|
||||
'/stop 停止当前任务',
|
||||
'/steer 补充指令 纠偏当前任务',
|
||||
'/status 检查连接状态',
|
||||
'/help 显示本帮助',
|
||||
].join('\n');
|
||||
// Built lazily: t() must run after setImHostLanguage, not at import time.
|
||||
function helpText() {
|
||||
return [
|
||||
t('企业微信机器人已连接 DeepSeek Harness。'),
|
||||
'',
|
||||
t('直接发送文字、图片或文件即可继续当前会话。'),
|
||||
t('/new 开启一个全新会话'),
|
||||
t('/compact 压缩当前会话的较早上下文'),
|
||||
t('/workspace 工作区绝对路径 切换工作区'),
|
||||
t('/workspacelist 列出工作区绝对路径'),
|
||||
t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
||||
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
||||
t('/models 按序号列出所有可用模型'),
|
||||
t('/model [序号或完整模型ID] 查看或切换当前会话模型'),
|
||||
t('示例:先发 /models,再发 /model 2'),
|
||||
t('/presetlist 按序号列出可用 Agent Preset'),
|
||||
t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
|
||||
t('纯数字 ID:/preset id:<ID>'),
|
||||
t('/preset --default 跟随 Host 默认'),
|
||||
t('/stop 停止当前任务'),
|
||||
t('/steer 补充指令 纠偏当前任务'),
|
||||
t('/status 检查连接状态'),
|
||||
t('/help 显示本帮助'),
|
||||
].join('\n');
|
||||
}
|
||||
const MAX_REPLY_BYTES = 18_000;
|
||||
const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
||||
const MAX_PREFETCHED_IMAGES = 4;
|
||||
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
||||
|
||||
function nonEmptyString(value) {
|
||||
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
||||
|
|
@ -137,7 +140,7 @@ function imageSource(client, image) {
|
|||
throw new ImagePromptError(
|
||||
'image-too-large',
|
||||
`Enterprise WeChat image exceeds ${maxBytes} bytes`,
|
||||
'图片超过 5 MB,请压缩后重试。',
|
||||
t('图片超过 5 MB,请压缩后重试。'),
|
||||
);
|
||||
}
|
||||
return { data, name: result?.filename };
|
||||
|
|
@ -219,7 +222,7 @@ function prefetchInboundImages(message, signal) {
|
|||
throw new ImagePromptError(
|
||||
'image-too-large',
|
||||
`Enterprise WeChat image exceeds ${maxBytes} bytes`,
|
||||
'图片超过 5 MB,请压缩后重试。',
|
||||
t('图片超过 5 MB,请压缩后重试。'),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
|
|
@ -238,7 +241,7 @@ function imageQueueFullMessage(message) {
|
|||
throw new ImagePromptError(
|
||||
'image-queue-full',
|
||||
`Enterprise WeChat already has ${MAX_PREFETCHED_IMAGES} prefetched images`,
|
||||
'当前待处理图片较多,请稍后重新发送。',
|
||||
t('当前待处理图片较多,请稍后重新发送。'),
|
||||
);
|
||||
},
|
||||
})),
|
||||
|
|
@ -272,7 +275,7 @@ function splitUtf8(text, maxBytes = MAX_REPLY_BYTES) {
|
|||
|
||||
function progressText(update) {
|
||||
if (update?.type === 'text') return update.text;
|
||||
if (update?.type === 'tool') return `正在使用${update.name}…`;
|
||||
if (update?.type === 'tool') return t('正在使用{name}…', { name: update.name });
|
||||
return update?.text;
|
||||
}
|
||||
|
||||
|
|
@ -280,23 +283,23 @@ function artifactFailureText(fileName, error) {
|
|||
const name = String(fileName ?? '结果文件').replace(/[\r\n]+/g, ' ').trim() || '结果文件';
|
||||
switch (error?.code) {
|
||||
case 'artifact-delivery-uncertain':
|
||||
return `结果文件「${name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。`;
|
||||
return t('结果文件「{name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
|
||||
case 'artifact-permission-required':
|
||||
return `结果文件「${name}」已生成,但企业微信智能机器人缺少素材上传或文件消息能力,请检查机器人权限。`;
|
||||
return t('结果文件「{name}」已生成,但企业微信智能机器人缺少素材上传或文件消息能力,请检查机器人权限。', { name });
|
||||
case 'artifact-too-large':
|
||||
return `结果文件「${name}」超过当前企业微信机器人可发送的文件大小,未发送。`;
|
||||
return t('结果文件「{name}」超过当前企业微信机器人可发送的文件大小,未发送。', { name });
|
||||
case 'artifact-empty':
|
||||
return `结果文件「${name}」为空,企业微信不允许发送空文件。`;
|
||||
return t('结果文件「{name}」为空,企业微信不允许发送空文件。', { name });
|
||||
case 'artifact-changed':
|
||||
case 'artifact-invalid':
|
||||
case 'artifact-unavailable':
|
||||
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 });
|
||||
default:
|
||||
return `结果文件「${name}」已生成,但暂时未能通过企业微信发送,请稍后重试。`;
|
||||
return t('结果文件「{name}」已生成,但暂时未能通过企业微信发送,请稍后重试。', { name });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -415,7 +418,7 @@ function sendWecomFile(client, chatId, file, options) {
|
|||
|
||||
function answerTextForDelivery(answer, artifacts) {
|
||||
if (typeof answer === 'string' && answer.trim()) return answer;
|
||||
return artifacts.length > 0 ? '结果文件已生成。' : '任务已完成,但没有生成可显示的文本。';
|
||||
return artifacts.length > 0 ? t('结果文件已生成。') : t('任务已完成,但没有生成可显示的文本。');
|
||||
}
|
||||
|
||||
function providerMessageId(result) {
|
||||
|
|
@ -535,7 +538,7 @@ export class WecomHarnessBridge {
|
|||
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
|
||||
this.#status.lastError = error?.message ?? String(error);
|
||||
this.#logger.error?.('[dsh-im:wecom] failed to process a command');
|
||||
return this.#sendImmediate(frame, chatId, '消息处理失败,请稍后重试。')
|
||||
return this.#sendImmediate(frame, chatId, t('消息处理失败,请稍后重试。'))
|
||||
.catch(() => undefined);
|
||||
}).finally(() => {
|
||||
this.#acceptedMessageIds.delete(messageId);
|
||||
|
|
@ -763,25 +766,25 @@ export class WecomHarnessBridge {
|
|||
let streamStarted = false;
|
||||
try {
|
||||
if (!text && !hasImages && !hasFiles) {
|
||||
await this.#sendImmediate(frame, chatId, '目前支持文字、图片、文件和语音转写消息。');
|
||||
await this.#sendImmediate(frame, chatId, t('目前支持文字、图片、文件和语音转写消息。'));
|
||||
await this.#state.markSeen(messageId);
|
||||
return;
|
||||
}
|
||||
const command = text.toLowerCase();
|
||||
if (!hasImages && !hasFiles && command === '/help') {
|
||||
await this.#sendImmediate(frame, chatId, HELP_TEXT);
|
||||
await this.#sendImmediate(frame, chatId, helpText());
|
||||
await this.#state.markSeen(messageId);
|
||||
return;
|
||||
}
|
||||
if (!hasImages && !hasFiles && command === '/status') {
|
||||
await this.#harness.ensureRunning({ signal: this.#signal });
|
||||
await this.#sendImmediate(frame, chatId, '企业微信机器人与 DeepSeek Harness 连接正常。');
|
||||
await this.#sendImmediate(frame, chatId, t('企业微信机器人与 DeepSeek Harness 连接正常。'));
|
||||
await this.#state.markSeen(messageId);
|
||||
return;
|
||||
}
|
||||
if (!hasImages && !hasFiles && command === '/new') {
|
||||
await this.#state.clearSession(key);
|
||||
await this.#sendImmediate(frame, chatId, '已开启新会话。请发送你的问题。');
|
||||
await this.#sendImmediate(frame, chatId, t('已开启新会话。请发送你的问题。'));
|
||||
await this.#state.markSeen(messageId);
|
||||
return;
|
||||
}
|
||||
|
|
@ -812,7 +815,7 @@ export class WecomHarnessBridge {
|
|||
|
||||
streamId = this.#generateReqId('stream');
|
||||
try {
|
||||
await this.#client.replyStream(frame, streamId, '正在思考中…', false);
|
||||
await this.#client.replyStream(frame, streamId, t('正在思考中…'), false);
|
||||
streamStarted = true;
|
||||
} catch (error) {
|
||||
this.#logger.warn?.('[dsh-im:wecom] unable to start a stream; using an active reply:', error);
|
||||
|
|
@ -911,7 +914,7 @@ export class WecomHarnessBridge {
|
|||
} catch (error) {
|
||||
if (error?.code === 'turn-stopped') {
|
||||
if (streamStarted && streamId) {
|
||||
await this.#client.replyStream(frame, streamId, '已停止。', true)
|
||||
await this.#client.replyStream(frame, streamId, t('已停止。'), true)
|
||||
.catch(() => undefined);
|
||||
}
|
||||
await this.#state.markSeen(messageId);
|
||||
|
|
@ -922,7 +925,7 @@ export class WecomHarnessBridge {
|
|||
this.#logger.error?.('[dsh-im:wecom] failed to process an inbound message');
|
||||
const errorText = inboundFileUserMessage(error)
|
||||
?? imagePromptUserMessage(error)
|
||||
?? '消息处理失败,请稍后重试。';
|
||||
?? t('消息处理失败,请稍后重试。');
|
||||
try {
|
||||
if (streamStarted && streamId) {
|
||||
await this.#client.replyStream(frame, streamId, errorText, true);
|
||||
|
|
@ -958,7 +961,7 @@ export class WecomHarnessBridge {
|
|||
|
||||
const text = nonEmptyString(interactionReplyText(frame));
|
||||
if (!text) {
|
||||
await this.#sendImmediate(frame, chatId, '请用文字回答当前问题。')
|
||||
await this.#sendImmediate(frame, chatId, t('请用文字回答当前问题。'))
|
||||
.catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
|
|
@ -966,7 +969,7 @@ export class WecomHarnessBridge {
|
|||
const pending = this.#pendingInteractions.get(key);
|
||||
if (!pending || pending !== expected || pending.submitting) {
|
||||
if (claimed && (!pending || pending !== expected)) {
|
||||
await this.#sendImmediate(frame, chatId, INTERACTION_RESOLVED_TEXT)
|
||||
await this.#sendImmediate(frame, chatId, t('这个问题已在其他客户端处理,无需再次回答。'))
|
||||
.catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
|
|
@ -987,7 +990,7 @@ export class WecomHarnessBridge {
|
|||
try {
|
||||
await this.#presentInteraction(pending);
|
||||
} catch {
|
||||
this.#status.lastError = '企业微信交互问题发送失败。';
|
||||
this.#status.lastError = t('企业微信交互问题发送失败。');
|
||||
this.#logger.error?.('[dsh-im:wecom] failed to retry an interaction question');
|
||||
pending.interaction.reconnect?.();
|
||||
return;
|
||||
|
|
@ -995,7 +998,7 @@ export class WecomHarnessBridge {
|
|||
const presentedPending = this.#pendingInteractions.get(key);
|
||||
if (!presentedPending || presentedPending !== expected || presentedPending.submitting) {
|
||||
if (claimed && (!presentedPending || presentedPending !== expected)) {
|
||||
await this.#sendImmediate(frame, chatId, INTERACTION_RESOLVED_TEXT)
|
||||
await this.#sendImmediate(frame, chatId, t('这个问题已在其他客户端处理,无需再次回答。'))
|
||||
.catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
|
|
@ -1018,7 +1021,7 @@ export class WecomHarnessBridge {
|
|||
try {
|
||||
await this.#presentInteraction(pending);
|
||||
} catch {
|
||||
this.#status.lastError = '企业微信交互问题发送失败。';
|
||||
this.#status.lastError = t('企业微信交互问题发送失败。');
|
||||
this.#logger.error?.('[dsh-im:wecom] failed to send the next interaction question');
|
||||
pending.interaction.reconnect?.();
|
||||
}
|
||||
|
|
@ -1042,7 +1045,7 @@ export class WecomHarnessBridge {
|
|||
if (this.#pendingInteractions.get(key) === pending) {
|
||||
this.#clearPendingInteraction(key, pending.interactionId);
|
||||
}
|
||||
await this.#sendImmediate(frame, chatId, INTERACTION_RESOLVED_TEXT)
|
||||
await this.#sendImmediate(frame, chatId, t('这个问题已在其他客户端处理,无需再次回答。'))
|
||||
.catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
|
|
@ -1050,9 +1053,9 @@ export class WecomHarnessBridge {
|
|||
pending.submitting = false;
|
||||
pending.answers.pop();
|
||||
pending.index -= 1;
|
||||
this.#status.lastError = '回答提交失败。';
|
||||
this.#status.lastError = t('回答提交失败。');
|
||||
this.#logger.error?.('[dsh-im:wecom] failed to answer a Harness interaction');
|
||||
await this.#sendImmediate(frame, chatId, '回答提交失败,请重新发送当前问题的答案。')
|
||||
await this.#sendImmediate(frame, chatId, t('回答提交失败,请重新发送当前问题的答案。'))
|
||||
.catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
|
@ -1097,7 +1100,7 @@ export class WecomHarnessBridge {
|
|||
});
|
||||
await this.#sendActive(
|
||||
chatId,
|
||||
'检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
|
||||
t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'),
|
||||
).catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
|
|
@ -1185,7 +1188,7 @@ export class WecomHarnessBridge {
|
|||
await this.#state.markSeen(messageId);
|
||||
this.#status.messagesReceived += 1;
|
||||
this.#status.lastMessageAt = new Date().toISOString();
|
||||
await this.#sendImmediate(frame, chatId, INTERACTION_RESOLVED_TEXT).catch(() => undefined);
|
||||
await this.#sendImmediate(frame, chatId, t('这个问题已在其他客户端处理,无需再次回答。')).catch(() => undefined);
|
||||
}
|
||||
|
||||
#takePendingInteraction(key, interactionId) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
connectionTestMessage,
|
||||
connectionTestTargetUnavailable,
|
||||
} from '../shared/connection-test.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const ACTIVE_ATTEMPT_STATES = new Set(['pending', 'connecting']);
|
||||
const TERMINAL_ATTEMPT_STATES = new Set(['connected', 'failed', 'cancelled', 'expired']);
|
||||
|
|
@ -82,14 +83,14 @@ export class WecomController {
|
|||
if (this.#closed || existing?.ready || existing?.wecomConnectionState === 'connecting') return;
|
||||
const secret = await this.#resolveSecret(config.secretRef);
|
||||
if (!secret) {
|
||||
this.#errors.set(config.botId, safeError('missing-secret', '企业微信机器人凭据缺失,请移除后重新扫码。'));
|
||||
this.#errors.set(config.botId, safeError('missing-secret', t('企业微信机器人凭据缺失,请移除后重新扫码。')));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.#startRuntime(config, secret);
|
||||
this.#errors.delete(config.botId);
|
||||
} catch (error) {
|
||||
this.#errors.set(config.botId, safeError('connection-failed', '企业微信连接未就绪,插件会自动重试。'));
|
||||
this.#errors.set(config.botId, safeError('connection-failed', t('企业微信连接未就绪,插件会自动重试。')));
|
||||
this.#logger.warn?.(`[dsh-im:wecom] bot ${config.botId} failed to initialize`);
|
||||
} finally {
|
||||
this.#touch();
|
||||
|
|
@ -134,8 +135,8 @@ export class WecomController {
|
|||
} catch (error) {
|
||||
record.state = record.controller.signal.aborted ? 'cancelled' : 'failed';
|
||||
record.error = record.controller.signal.aborted
|
||||
? safeError('cancelled', '扫码绑定已取消。')
|
||||
: safeError('qr-start-failed', '无法生成企业微信二维码,请稍后重试。');
|
||||
? safeError('cancelled', t('扫码绑定已取消。'))
|
||||
: safeError('qr-start-failed', t('无法生成企业微信二维码,请稍后重试。'));
|
||||
this.#finishAttempt(record);
|
||||
throw error;
|
||||
}
|
||||
|
|
@ -150,7 +151,7 @@ export class WecomController {
|
|||
}
|
||||
if (Date.now() >= record.expiresAt) {
|
||||
record.state = 'expired';
|
||||
record.error = safeError('expired', '企业微信二维码已过期,请重新生成。');
|
||||
record.error = safeError('expired', t('企业微信二维码已过期,请重新生成。'));
|
||||
record.controller.abort();
|
||||
this.#finishAttempt(record);
|
||||
return publicAttempt(record);
|
||||
|
|
@ -200,7 +201,7 @@ export class WecomController {
|
|||
} catch {
|
||||
this.#errors.set(
|
||||
identity.botId,
|
||||
safeError('connection-failed', '企业微信机器人已绑定,消息连接暂未就绪。'),
|
||||
safeError('connection-failed', t('企业微信机器人已绑定,消息连接暂未就绪。')),
|
||||
);
|
||||
this.#logger.warn?.(`[dsh-im:wecom] bot ${identity.botId} credential connection failed`);
|
||||
}
|
||||
|
|
@ -216,7 +217,7 @@ export class WecomController {
|
|||
record.controller.abort();
|
||||
await Promise.allSettled([record.polling, record.transition].filter(Boolean));
|
||||
if (!TERMINAL_ATTEMPT_STATES.has(record.state)) record.state = 'cancelled';
|
||||
record.error ??= safeError('cancelled', '扫码绑定已取消。');
|
||||
record.error ??= safeError('cancelled', t('扫码绑定已取消。'));
|
||||
this.#finishAttempt(record);
|
||||
}
|
||||
return publicAttempt(record);
|
||||
|
|
@ -232,7 +233,7 @@ export class WecomController {
|
|||
await this.#startRuntime(config, secret);
|
||||
this.#errors.delete(botId);
|
||||
} catch (error) {
|
||||
this.#errors.set(botId, safeError('connection-failed', '企业微信连接仍未就绪,请稍后重试。'));
|
||||
this.#errors.set(botId, safeError('connection-failed', t('企业微信连接仍未就绪,请稍后重试。')));
|
||||
throw error;
|
||||
} finally {
|
||||
this.#touch();
|
||||
|
|
@ -247,10 +248,10 @@ export class WecomController {
|
|||
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(
|
||||
`企业微信机器人(${maskWecomBotId(config.remoteBotId)})`,
|
||||
t('企业微信机器人({botId})', { botId: maskWecomBotId(config.remoteBotId) }),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
|
@ -295,11 +296,11 @@ export class WecomController {
|
|||
state,
|
||||
connected,
|
||||
configured: true,
|
||||
bot: { name: '企业微信机器人', appIdMasked: maskWecomBotId(config.remoteBotId) },
|
||||
bot: { name: t('企业微信机器人'), appIdMasked: maskWecomBotId(config.remoteBotId) },
|
||||
health: {
|
||||
status: connected ? 'healthy' : state === 'error' ? 'error' : 'offline',
|
||||
summary: connected ? '企业微信 WebSocket 长连接运行正常'
|
||||
: state === 'error' ? '企业微信连接未就绪,插件会自动重试' : '企业微信连接当前离线',
|
||||
summary: connected ? t('企业微信 WebSocket 长连接运行正常')
|
||||
: state === 'error' ? t('企业微信连接未就绪,插件会自动重试') : t('企业微信连接当前离线'),
|
||||
lastCheckedAt: runtimeStatus?.lastCheckedAt ?? null,
|
||||
lastConnectedAt: runtimeStatus?.lastConnectedAt ?? null,
|
||||
},
|
||||
|
|
@ -341,13 +342,13 @@ export class WecomController {
|
|||
if (result.status === 'waiting') return;
|
||||
if (result.status === 'expired') {
|
||||
record.state = 'expired';
|
||||
record.error = safeError('expired', '企业微信二维码已过期,请重新生成。');
|
||||
record.error = safeError('expired', t('企业微信二维码已过期,请重新生成。'));
|
||||
this.#finishAttempt(record);
|
||||
return;
|
||||
}
|
||||
if (result.status !== 'success') {
|
||||
record.state = 'failed';
|
||||
record.error = safeError('qr-connect-failed', '企业微信扫码没有完成,请重新生成二维码。');
|
||||
record.error = safeError('qr-connect-failed', t('企业微信扫码没有完成,请重新生成二维码。'));
|
||||
this.#finishAttempt(record);
|
||||
return;
|
||||
}
|
||||
|
|
@ -362,7 +363,7 @@ export class WecomController {
|
|||
} catch (error) {
|
||||
if (record.controller.signal.aborted) return;
|
||||
record.state = 'failed';
|
||||
record.error = safeError('qr-connect-failed', '企业微信扫码服务暂时不可用,请重新生成二维码。');
|
||||
record.error = safeError('qr-connect-failed', t('企业微信扫码服务暂时不可用,请重新生成二维码。'));
|
||||
this.#logger.warn?.('[dsh-im:wecom] QR polling failed');
|
||||
this.#finishAttempt(record);
|
||||
}
|
||||
|
|
@ -379,10 +380,10 @@ export class WecomController {
|
|||
} catch (error) {
|
||||
if (record.controller.signal.aborted) {
|
||||
record.state = 'cancelled';
|
||||
record.error = safeError('cancelled', '扫码绑定已取消。');
|
||||
record.error = safeError('cancelled', t('扫码绑定已取消。'));
|
||||
} else {
|
||||
record.state = 'failed';
|
||||
record.error = safeError('activation-failed', '企业微信已授权,但无法安全保存接入配置。');
|
||||
record.error = safeError('activation-failed', t('企业微信已授权,但无法安全保存接入配置。'));
|
||||
this.#logger.error?.('[dsh-im:wecom] provisioning failed');
|
||||
}
|
||||
} finally {
|
||||
|
|
@ -429,7 +430,7 @@ export class WecomController {
|
|||
await this.#restoreCredential(identity.secretRef, previousSecret);
|
||||
throw error;
|
||||
}
|
||||
this.#errors.set(identity.botId, safeError('connection-failed', '企业微信机器人已绑定,消息连接暂未就绪。'));
|
||||
this.#errors.set(identity.botId, safeError('connection-failed', t('企业微信机器人已绑定,消息连接暂未就绪。')));
|
||||
this.#logger.warn?.(`[dsh-im:wecom] bot ${identity.botId} activation connection failed`);
|
||||
}
|
||||
this.#touch();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { WSAuthFailureError, WSClient, WSReconnectExhaustedError } from '@wecom/
|
|||
|
||||
import { createWecomBridgeStatus, WecomHarnessBridge } from './wecom-bridge.mjs';
|
||||
import { sendRememberedConnectionTest } from '../shared/connection-test.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
function timeoutError() {
|
||||
const error = new Error('Enterprise WeChat WebSocket authentication timed out');
|
||||
|
|
@ -202,7 +203,7 @@ export class WecomRuntime {
|
|||
return sendRememberedConnectionTest({
|
||||
state: this.#state,
|
||||
text,
|
||||
channelLabel: '企业微信机器人',
|
||||
channelLabel: t('企业微信机器人'),
|
||||
send: async ({ chatId }, content) => {
|
||||
if (!this.#status.ready || !this.#client) {
|
||||
throw new Error('Enterprise WeChat runtime is not connected');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue