fix: harden host language switching

This commit is contained in:
xmanrui 2026-08-24 03:55:55 +08:00
parent 3741dd8fd9
commit d4b8666495
23 changed files with 189 additions and 90 deletions

File diff suppressed because one or more lines are too long

View file

@ -1,10 +1,9 @@
import { TextHarnessBridge, createTextBridgeStatus } from '../shared/text-harness-bridge.mjs'; import { TextHarnessBridge, createTextBridgeStatus } from '../shared/text-harness-bridge.mjs';
import { t } from '../shared/i18n.mjs';
export const DISCORD_DESCRIPTOR = Object.freeze({ export const DISCORD_DESCRIPTOR = Object.freeze({
key: 'discord', key: 'discord',
label: 'Discord', label: 'Discord',
connectionLabel: t(' Gateway 长连接'), connectionLabel: ' Gateway 长连接',
}); });
export class DiscordHarnessBridge extends TextHarnessBridge { export class DiscordHarnessBridge extends TextHarnessBridge {

View file

@ -50,4 +50,3 @@ export default {
'钉钉 Stream 消息连接运行正常': 'The DingTalk Stream message connection is running normally', '钉钉 Stream 消息连接运行正常': 'The DingTalk Stream message connection is running normally',
'钉钉消息连接当前离线': 'The DingTalk message connection is currently offline', '钉钉消息连接当前离线': 'The DingTalk message connection is currently offline',
}; };

View file

@ -6,4 +6,3 @@ export default {
'Discord机器人': 'Discord Bot', 'Discord机器人': 'Discord Bot',
' Gateway 长连接': ' Gateway long-lived connection', ' Gateway 长连接': ' Gateway long-lived connection',
}; };

View file

@ -21,4 +21,3 @@ export default {
'Harness Session 已创建。': 'Harness Session created.', 'Harness Session 已创建。': 'Harness Session created.',
'正在使用 {tool}…': 'Using {tool}…', '正在使用 {tool}…': 'Using {tool}…',
}; };

View file

@ -9,12 +9,6 @@ export default {
结果文件: 'Result file',
'结果文件「{name}」已生成,但 QQ 今日文件上传额度已用完,请稍后重试。': '结果文件「{name}」已生成,但 QQ 今日文件上传额度已用完,请稍后重试。':
'The result file "{name}" was generated, but QQ\'s daily file upload quota is used up. Please try again later.', 'The result file "{name}" was generated, but QQ\'s daily file upload quota is used up. Please try again later.',
@ -58,4 +52,3 @@ export default {
'QQ 已授权,但无法安全保存接入配置。': 'QQ 已授权,但无法安全保存接入配置。':
'QQ authorization succeeded, but the connection configuration could not be saved safely.', 'QQ authorization succeeded, but the connection configuration could not be saved safely.',
}; };

View file

@ -64,6 +64,20 @@ export default {
'/help 显示本帮助': '/help Show this help', '/help 显示本帮助': '/help Show this help',
'{label}机器人与 DeepSeek Harness 连接正常。': '{label}机器人与 DeepSeek Harness 连接正常。':
'The {label} bot is connected to DeepSeek Harness and working normally.', 'The {label} bot is connected to DeepSeek Harness and working normally.',
'{label}机器人凭据缺失,请移除后重新接入。':
'{label} bot credentials are missing. Remove the bot and connect it again.',
'{label}连接未就绪,插件会自动重试。':
'The {label} connection is not ready yet. The plugin will retry automatically.',
'{label}机器人已接入,消息连接暂未就绪。':
'The {label} bot is connected, but its message connection is not ready yet.',
'{label}连接仍未就绪,请稍后重试。':
'The {label} connection is still not ready. Please try again later.',
'{label}机器人尚未连接': 'The {label} bot is not connected yet',
'{name}({id})': '{name} ({id})',
'{label}{connectionLabel}运行正常': 'The {label}{connectionLabel} is running normally',
'{label}连接未就绪,插件会自动重试':
'The {label} connection is not ready. The plugin will retry automatically',
'{label}连接当前离线': 'The {label} connection is currently offline',
'已开启新会话。请发送你的问题。': 'A new session has started. Please send your question.', '已开启新会话。请发送你的问题。': 'A new session has started. Please send your question.',
'正在使用{name}…': 'Using {name}…', '正在使用{name}…': 'Using {name}…',
'已停止。': 'Stopped.', '已停止。': 'Stopped.',
@ -75,4 +89,3 @@ export default {
'检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。': '检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。':
'A pending question left over in this Session was detected. It has been safely cancelled, and your latest message is being processed.', 'A pending question left over in this Session was detected. It has been safely cancelled, and your latest message is being processed.',
}; };

View file

@ -213,4 +213,3 @@ export default {
'已提交补充指令,Agent 会在下一步读取。': '已提交补充指令,Agent 会在下一步读取。':
'Additional instruction submitted; the Agent will read it at the next step.', 'Additional instruction submitted; the Agent will read it at the next step.',
}; };

View file

@ -97,4 +97,3 @@ export default {
// agent-preset.mjs // agent-preset.mjs
'Agent Preset 无效。': 'Invalid Agent Preset.', 'Agent Preset 无效。': 'Invalid Agent Preset.',
}; };

View file

@ -25,4 +25,3 @@ export default {
'Slack App Token 无法创建 Socket Mode 连接,请确认已启用 Socket Mode 和 connections:write。': 'Slack App Token 无法创建 Socket Mode 连接,请确认已启用 Socket Mode 和 connections:write。':
'The Slack App Token could not create a Socket Mode connection. Make sure Socket Mode and connections:write are enabled.', 'The Slack App Token could not create a Socket Mode connection. Make sure Socket Mode and connections:write are enabled.',
}; };

View file

@ -19,4 +19,3 @@ export default {
'Telegram机器人': 'Telegram Bot', 'Telegram机器人': 'Telegram Bot',
' Bot API 长轮询': ' Bot API long polling', ' Bot API 长轮询': ' Bot API long polling',
}; };

View file

@ -36,4 +36,3 @@ export default {
'企业微信连接未就绪,插件会自动重试': 'The Enterprise WeChat connection is not ready. The plugin will retry automatically', '企业微信连接未就绪,插件会自动重试': 'The Enterprise WeChat connection is not ready. The plugin will retry automatically',
'企业微信连接当前离线': 'The Enterprise WeChat connection is currently offline', '企业微信连接当前离线': 'The Enterprise WeChat connection is currently offline',
}; };

View file

@ -78,4 +78,3 @@ export default {
'微信服务拒绝了文件消息。': 'The WeChat service rejected the file message.', '微信服务拒绝了文件消息。': 'The WeChat service rejected the file message.',
'微信账号连接启动失败。': 'Failed to start the WeChat account connection.', '微信账号连接启动失败。': 'Failed to start the WeChat account connection.',
}; };

View file

@ -15,4 +15,3 @@ export default {
'WhatsApp账号': 'WhatsApp account', 'WhatsApp账号': 'WhatsApp account',
' Web 关联设备': ' Web linked device', ' Web 关联设备': ' Web linked device',
}; };

View file

@ -14,7 +14,7 @@ let language = 'zh';
// (including undefined and unrecognized values) selects Chinese. // (including undefined and unrecognized values) selects Chinese.
export function setImHostLanguage(lang) { export function setImHostLanguage(lang) {
const normalized = typeof lang === 'string' ? lang.trim().toLowerCase() : ''; const normalized = typeof lang === 'string' ? lang.trim().toLowerCase() : '';
language = normalized === 'english' || normalized.startsWith('en') ? 'en' : 'zh'; language = normalized === 'english' || /^en(?:[-_].*)?$/u.test(normalized) ? 'en' : 'zh';
} }
export function getImHostLanguage() { export function getImHostLanguage() {

View file

@ -1,4 +1,5 @@
import { connectionTestMessage } from './connection-test.mjs'; import { connectionTestMessage } from './connection-test.mjs';
import { t } from './i18n.mjs';
function cleanString(value) { function cleanString(value) {
return typeof value === 'string' && value.trim() ? value.trim() : null; return typeof value === 'string' && value.trim() ? value.trim() : null;
@ -70,7 +71,9 @@ export class TokenBotController {
if (!token) { if (!token) {
this.#errors.set(config.botId, safeError( this.#errors.set(config.botId, safeError(
'missing-token', 'missing-token',
`${this.#descriptor.label}机器人凭据缺失,请移除后重新接入。`, t('{label}机器人凭据缺失,请移除后重新接入。', {
label: this.#descriptor.label,
}),
)); ));
return; return;
} }
@ -80,7 +83,9 @@ export class TokenBotController {
} catch (error) { } catch (error) {
this.#errors.set(config.botId, safeError( this.#errors.set(config.botId, safeError(
'connection-failed', 'connection-failed',
`${this.#descriptor.label}连接未就绪,插件会自动重试。`, t('{label}连接未就绪,插件会自动重试。', {
label: this.#descriptor.label,
}),
)); ));
this.#logger.warn?.( this.#logger.warn?.(
`[dsh-im:${this.#descriptor.key}] bot ${config.botId} failed to initialize:`, `[dsh-im:${this.#descriptor.key}] bot ${config.botId} failed to initialize:`,
@ -129,7 +134,9 @@ export class TokenBotController {
} catch (error) { } catch (error) {
this.#errors.set(identity.botId, safeError( this.#errors.set(identity.botId, safeError(
'connection-failed', 'connection-failed',
`${this.#descriptor.label}机器人已接入,消息连接暂未就绪。`, t('{label}机器人已接入,消息连接暂未就绪。', {
label: this.#descriptor.label,
}),
)); ));
this.#logger.warn?.( this.#logger.warn?.(
`[dsh-im:${this.#descriptor.key}] bot ${identity.botId} credential connection failed:`, `[dsh-im:${this.#descriptor.key}] bot ${identity.botId} credential connection failed:`,
@ -153,7 +160,9 @@ export class TokenBotController {
} catch (error) { } catch (error) {
this.#errors.set(botId, safeError( this.#errors.set(botId, safeError(
'connection-failed', 'connection-failed',
`${this.#descriptor.label}连接仍未就绪,请稍后重试。`, t('{label}连接仍未就绪,请稍后重试。', {
label: this.#descriptor.label,
}),
)); ));
throw error; throw error;
} finally { } finally {
@ -181,7 +190,9 @@ export class TokenBotController {
} catch (error) { } catch (error) {
this.#errors.set(botId, safeError( this.#errors.set(botId, safeError(
'connection-failed', 'connection-failed',
`${this.#descriptor.label}连接仍未就绪,请稍后重试。`, t('{label}连接仍未就绪,请稍后重试。', {
label: this.#descriptor.label,
}),
)); ));
throw error; throw error;
} finally { } finally {
@ -197,14 +208,19 @@ export class TokenBotController {
return this.#withBotTransition(botId, async () => { return this.#withBotTransition(botId, async () => {
const runtime = this.#runtimes.get(botId); const runtime = this.#runtimes.get(botId);
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') { if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
const error = new Error(`${this.#descriptor.label}机器人尚未连接`); const error = new Error(t('{label}机器人尚未连接', {
label: this.#descriptor.label,
}));
error.code = 'test-target-unavailable'; error.code = 'test-target-unavailable';
throw error; throw error;
} }
const cardLabel = `${config.name}(${this.#maskPlatformId(config.platformId)})`; const cardLabel = t('{name}({id})', {
name: config.name,
id: this.#maskPlatformId(config.platformId),
});
await runtime.sendConnectionTest(connectionTestMessage( await runtime.sendConnectionTest(connectionTestMessage(
cardLabel, cardLabel,
`${this.#descriptor.label}机器人`, t('{label}机器人', { label: this.#descriptor.label }),
)); ));
return { sent: true }; return { sent: true };
}); });
@ -260,9 +276,14 @@ export class TokenBotController {
}, },
health: { health: {
status: connected ? 'healthy' : state === 'error' ? 'error' : 'offline', status: connected ? 'healthy' : state === 'error' ? 'error' : 'offline',
summary: connected ? `${this.#descriptor.label}${this.#descriptor.connectionLabel}运行正常` summary: connected ? t('{label}{connectionLabel}运行正常', {
: state === 'error' ? `${this.#descriptor.label}连接未就绪,插件会自动重试` label: this.#descriptor.label,
: `${this.#descriptor.label}连接当前离线`, connectionLabel: t(this.#descriptor.connectionLabel),
})
: state === 'error' ? t('{label}连接未就绪,插件会自动重试', {
label: this.#descriptor.label,
})
: t('{label}连接当前离线', { label: this.#descriptor.label }),
lastCheckedAt: runtimeStatus?.lastCheckedAt ?? null, lastCheckedAt: runtimeStatus?.lastCheckedAt ?? null,
lastConnectedAt: runtimeStatus?.lastConnectedAt ?? null, lastConnectedAt: runtimeStatus?.lastConnectedAt ?? null,
}, },

View file

@ -136,8 +136,18 @@ export function normalizeSlackEvent(payload, botUserId, {
}; };
} }
function isToolProgress(text) { export function isSlackToolProgress(text) {
return typeof text === 'string' && /^正在使用.+…$/.test(text.trim()); if (typeof text !== 'string') return false;
const marker = '__DSH_IM_TOOL_NAME__';
const template = t('正在使用{name}…', { name: marker });
const markerIndex = template.indexOf(marker);
if (markerIndex < 0) return false;
const prefix = template.slice(0, markerIndex);
const suffix = template.slice(markerIndex + marker.length);
const source = text.trim();
return source.startsWith(prefix)
&& source.endsWith(suffix)
&& source.length > prefix.length + suffix.length;
} }
async function appendInChunks(api, target, ts, text, signal) { async function appendInChunks(api, target, ts, text, signal) {
@ -205,7 +215,8 @@ async function createSlackMessageStream({ api, target, signal, logger }) {
return [...providerMessageIds]; return [...providerMessageIds];
}, },
update(text) { update(text) {
if (closed || broken || typeof text !== 'string' || !text.trim() || isToolProgress(text)) return; if (closed || broken || typeof text !== 'string' || !text.trim()
|| isSlackToolProgress(text)) return;
pending = text; pending = text;
schedule(); schedule();
}, },

View file

@ -16,22 +16,29 @@ import {
} from './config-store.mjs'; } from './config-store.mjs';
export const TELEGRAM_COMMAND_MENU = Object.freeze([ export const TELEGRAM_COMMAND_MENU = Object.freeze([
{ command: 'new', description: t('开启一个全新会话') }, { command: 'new', description: '开启一个全新会话' },
{ command: 'compact', description: t('压缩当前会话的较早上下文') }, { command: 'compact', description: '压缩当前会话的较早上下文' },
{ command: 'workspace', description: t('切换工作区') }, { command: 'workspace', description: '切换工作区' },
{ command: 'workspacelist', description: t('列出工作区绝对路径') }, { command: 'workspacelist', description: '列出工作区绝对路径' },
{ command: 'sessionlist', description: t('列出会话 ID 和标题') }, { command: 'sessionlist', description: '列出会话 ID 和标题' },
{ command: 'session', description: t('将当前聊天绑定到指定会话') }, { command: 'session', description: '将当前聊天绑定到指定会话' },
{ command: 'models', description: t('按序号列出所有可用模型') }, { command: 'models', description: '按序号列出所有可用模型' },
{ command: 'model', description: t('查看或切换当前会话模型') }, { command: 'model', description: '查看或切换当前会话模型' },
{ command: 'presetlist', description: t('列出可用 Agent Preset') }, { command: 'presetlist', description: '列出可用 Agent Preset' },
{ command: 'preset', description: t('查看或设置新会话 Agent Preset') }, { command: 'preset', description: '查看或设置新会话 Agent Preset' },
{ command: 'stop', description: t('停止当前任务') }, { command: 'stop', description: '停止当前任务' },
{ command: 'steer', description: t('纠偏当前任务') }, { command: 'steer', description: '纠偏当前任务' },
{ command: 'status', description: t('检查连接状态') }, { command: 'status', description: '检查连接状态' },
{ command: 'help', description: t('显示帮助') }, { command: 'help', description: '显示帮助' },
]); ]);
export function telegramCommandMenu() {
return TELEGRAM_COMMAND_MENU.map((item) => ({
...item,
description: t(item.description),
}));
}
function escaped(value) { function escaped(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
} }
@ -709,7 +716,7 @@ export class TelegramRuntime {
throw error; throw error;
} }
try { try {
await api.setMyCommands({ commands: TELEGRAM_COMMAND_MENU, signal: controller.signal }); await api.setMyCommands({ commands: telegramCommandMenu(), signal: controller.signal });
await api.setChatMenuButton({ menuButton: COMMANDS_MENU_BUTTON, signal: controller.signal }); await api.setChatMenuButton({ menuButton: COMMANDS_MENU_BUTTON, signal: controller.signal });
} catch (error) { } catch (error) {
this.#logger.warn?.( this.#logger.warn?.(

View file

@ -280,7 +280,8 @@ function progressText(update) {
} }
function artifactFailureText(fileName, error) { 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) { switch (error?.code) {
case 'artifact-delivery-uncertain': case 'artifact-delivery-uncertain':
return t('结果文件「{name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name }); return t('结果文件「{name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });

View file

@ -20,6 +20,7 @@ import {
normalizeDiscordMessage, normalizeDiscordMessage,
resolveDiscordMessageRoute, resolveDiscordMessageRoute,
} from '../../../src/channels/discord/discord-runtime.mjs'; } from '../../../src/channels/discord/discord-runtime.mjs';
import { setImHostLanguage } from '../../../src/channels/shared/i18n.mjs';
import { import {
DISCORD_ENDPOINTS, DISCORD_ENDPOINTS,
createDiscordRpcHandler, createDiscordRpcHandler,
@ -387,6 +388,15 @@ test('Discord controller persists a credential reference and exposes only masked
const status = await controller.bindCredentials({ token: TOKEN }); const status = await controller.bindCredentials({ token: TOKEN });
assert.equal(status.totals.connected, 1); assert.equal(status.totals.connected, 1);
assert.equal(status.bots[0].bot.name, 'Harness Discord'); assert.equal(status.bots[0].bot.name, 'Harness Discord');
setImHostLanguage('en');
try {
assert.equal(
controller.status().bots[0].health.summary,
'The Discord Gateway long-lived connection is running normally',
);
} finally {
setImHostLanguage('zh');
}
const identity = deriveDiscordBotIdentity('1234567890123456789'); const identity = deriveDiscordBotIdentity('1234567890123456789');
assert.equal(credentialStore.values.get(identity.tokenRef), TOKEN); assert.equal(credentialStore.values.get(identity.tokenRef), TOKEN);
assert.doesNotMatch(await readFile(configPath, 'utf8'), new RegExp(TOKEN.replaceAll('.', '\\.'))); assert.doesNotMatch(await readFile(configPath, 'utf8'), new RegExp(TOKEN.replaceAll('.', '\\.')));

View file

@ -1,7 +1,7 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { readdirSync, readFileSync, statSync } from 'node:fs'; import { readdirSync, readFileSync, statSync } from 'node:fs';
import { dirname, join } from 'node:path'; import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath, pathToFileURL } from 'node:url';
import { after, test } from 'node:test'; import { after, test } from 'node:test';
import { import {
@ -30,7 +30,7 @@ test('setImHostLanguage accepts English spellings and falls back to Chinese', ()
setImHostLanguage(value); setImHostLanguage(value);
assert.equal(getImHostLanguage(), 'en', `expected ${value} to select English`); assert.equal(getImHostLanguage(), 'en', `expected ${value} to select English`);
} }
for (const value of [undefined, null, '', 'zh', 'zh-CN', 'fr', 42]) { for (const value of [undefined, null, '', 'zh', 'zh-CN', 'fr', 'enabled', 42]) {
setImHostLanguage(value); setImHostLanguage(value);
assert.equal(getImHostLanguage(), 'zh', `expected ${value} to select Chinese`); assert.equal(getImHostLanguage(), 'zh', `expected ${value} to select Chinese`);
} }
@ -49,11 +49,36 @@ test('t() translates known keys and fills placeholders in English mode', () => {
test('every English dictionary entry is a non-empty translation of a Chinese key', () => { test('every English dictionary entry is a non-empty translation of a Chinese key', () => {
for (const [key, value] of Object.entries(EN)) { for (const [key, value] of Object.entries(EN)) {
assert.ok(/[一-鿿]/.test(key), `dictionary key is not Chinese: ${key}`); assert.ok(/[一-鿿()]/.test(key), `dictionary key is not a Chinese source string: ${key}`);
assert.ok(typeof value === 'string' && value.trim().length > 0, `empty translation for: ${key}`); assert.ok(typeof value === 'string' && value.trim().length > 0, `empty translation for: ${key}`);
const placeholders = (text) => [...new Set(
[...text.matchAll(/\{([a-zA-Z][a-zA-Z0-9_]*)\}/g)].map((match) => match[1]),
)].sort();
assert.deepEqual(
placeholders(value),
placeholders(key),
`placeholder mismatch for: ${key}`,
);
} }
}); });
test('English dictionary source files do not define duplicate keys', async () => {
const dictionaryDir = join(srcChannelsDir, 'shared', 'i18n-en');
const seen = new Map();
const duplicates = [];
const files = readdirSync(dictionaryDir)
.filter((file) => file.endsWith('.mjs'))
.sort();
for (const file of files) {
const { default: entries } = await import(pathToFileURL(join(dictionaryDir, file)).href);
for (const key of Object.keys(entries)) {
if (seen.has(key)) duplicates.push(`${key}: ${seen.get(key)}, ${file}`);
else seen.set(key, file);
}
}
assert.deepEqual(duplicates, []);
});
test('every literal t() key in src/channels has an English dictionary entry', () => { test('every literal t() key in src/channels has an English dictionary entry', () => {
const files = []; const files = [];
const walk = (dir) => { const walk = (dir) => {

View file

@ -17,8 +17,10 @@ import {
} from '../../../src/channels/slack/slack-api.mjs'; } from '../../../src/channels/slack/slack-api.mjs';
import { import {
SlackRuntime, SlackRuntime,
isSlackToolProgress,
normalizeSlackEvent, normalizeSlackEvent,
} from '../../../src/channels/slack/slack-runtime.mjs'; } from '../../../src/channels/slack/slack-runtime.mjs';
import { setImHostLanguage } from '../../../src/channels/shared/i18n.mjs';
import { SLACK_APP_MANIFEST_YAML } from '../../../src/channels/slack/manifest.mjs'; import { SLACK_APP_MANIFEST_YAML } from '../../../src/channels/slack/manifest.mjs';
import { import {
SLACK_ENDPOINTS, SLACK_ENDPOINTS,
@ -57,6 +59,20 @@ function deferred() {
return { promise, resolve, reject }; return { promise, resolve, reject };
} }
test('Slack recognizes localized tool progress without hiding ordinary text', () => {
assert.equal(isSlackToolProgress('正在使用搜索…'), true);
assert.equal(isSlackToolProgress('正在使用…'), false);
assert.equal(isSlackToolProgress('普通回复'), false);
setImHostLanguage('en');
try {
assert.equal(isSlackToolProgress('Using search…'), true);
assert.equal(isSlackToolProgress('Using …'), false);
assert.equal(isSlackToolProgress('Ordinary reply'), false);
} finally {
setImHostLanguage('zh');
}
});
async function eventually(predicate, timeoutMs = 1_000) { async function eventually(predicate, timeoutMs = 1_000) {
const deadline = Date.now() + timeoutMs; const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) { while (Date.now() < deadline) {

View file

@ -9,6 +9,7 @@ import {
OutboundArtifactRegistry, OutboundArtifactRegistry,
createOutboundArtifactTool, createOutboundArtifactTool,
} from '../../../src/channels/shared/semantic/artifact.mjs'; } from '../../../src/channels/shared/semantic/artifact.mjs';
import { setImHostLanguage } from '../../../src/channels/shared/i18n.mjs';
import { import {
TELEGRAM_ACCESS_MODES, TELEGRAM_ACCESS_MODES,
TelegramConfigStore, TelegramConfigStore,
@ -28,6 +29,7 @@ import {
TelegramRuntime, TelegramRuntime,
TELEGRAM_COMMAND_MENU, TELEGRAM_COMMAND_MENU,
normalizeTelegramUpdate, normalizeTelegramUpdate,
telegramCommandMenu,
telegramInboundAllowed, telegramInboundAllowed,
} from '../../../src/channels/telegram/telegram-runtime.mjs'; } from '../../../src/channels/telegram/telegram-runtime.mjs';
import { TelegramStateStore } from '../../../src/channels/telegram/state-store.mjs'; import { TelegramStateStore } from '../../../src/channels/telegram/state-store.mjs';
@ -247,6 +249,17 @@ test('Telegram API registers the command menu and commands-type menu button', as
await assert.rejects(() => api.setChatMenuButton({ menuButton: 'commands' }), /menu button is invalid/); await assert.rejects(() => api.setChatMenuButton({ menuButton: 'commands' }), /menu button is invalid/);
}); });
test('Telegram command menu follows the host language at runtime', () => {
assert.equal(TELEGRAM_COMMAND_MENU[0].description, '开启一个全新会话');
setImHostLanguage('en');
try {
assert.equal(telegramCommandMenu()[0].description, 'Start a brand-new Session');
} finally {
setImHostLanguage('zh');
}
assert.deepEqual(telegramCommandMenu(), TELEGRAM_COMMAND_MENU);
});
test('Telegram API preserves the legacy plain send and edit payloads', async () => { test('Telegram API preserves the legacy plain send and edit payloads', async () => {
const calls = []; const calls = [];
const api = new TelegramApi({ const api = new TelegramApi({