mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-11 09:20:52 +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
|
|
@ -1,5 +1,6 @@
|
|||
import { setTimeout as sleep } from 'node:timers/promises';
|
||||
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
import { harnessApprovalText } from '../shared/harness-approval.mjs';
|
||||
import {
|
||||
harnessAnswerForQuestion,
|
||||
|
|
@ -21,23 +22,23 @@ function clean(value, max = 8_000) {
|
|||
|
||||
function safeFailure(error) {
|
||||
if (error?.code === 'office-job-alias-invalid') return error.message;
|
||||
if (error?.code === 'turn-stopped') return '本机 Harness 已停止这次执行。';
|
||||
if (error?.code === 'office-job-conflict') return 'Office Job 已被领取、取消或结束。';
|
||||
return '本机 Harness 未能完成任务;请检查 Harness 会话后重试。';
|
||||
if (error?.code === 'turn-stopped') return t('本机 Harness 已停止这次执行。');
|
||||
if (error?.code === 'office-job-conflict') return t('Office Job 已被领取、取消或结束。');
|
||||
return t('本机 Harness 未能完成任务;请检查 Harness 会话后重试。');
|
||||
}
|
||||
|
||||
function renderPrompt(job, preset) {
|
||||
return [
|
||||
'# AI Office Handoff',
|
||||
'',
|
||||
'你正在本机 DeepSeek Harness 中继续一个来自 AI Office 的任务。',
|
||||
'只在当前 Workspace 内行动。完成后必须返回:结果摘要、改动文件、验证证据、未解决风险。',
|
||||
t('你正在本机 DeepSeek Harness 中继续一个来自 AI Office 的任务。'),
|
||||
t('只在当前 Workspace 内行动。完成后必须返回:结果摘要、改动文件、验证证据、未解决风险。'),
|
||||
'',
|
||||
'## 本机 Instruction Preset',
|
||||
t('## 本机 Instruction Preset'),
|
||||
preset,
|
||||
'',
|
||||
...(clean(job.instruction) ? ['## 本轮补充指令', clean(job.instruction), ''] : []),
|
||||
'## Office 时间线',
|
||||
...(clean(job.instruction) ? [t('## 本轮补充指令'), clean(job.instruction), ''] : []),
|
||||
t('## Office 时间线'),
|
||||
clean(job.markdown, 200_000),
|
||||
].join('\n');
|
||||
}
|
||||
|
|
@ -54,7 +55,7 @@ function approvalRequest(interaction) {
|
|||
return {
|
||||
id,
|
||||
kind: 'approval',
|
||||
title: `批准 ${clean(interaction.payload?.toolName, 120) || 'Harness 工具'}`,
|
||||
title: t('批准 {tool}', { tool: clean(interaction.payload?.toolName, 120) || t('Harness 工具') }),
|
||||
prompt,
|
||||
toolName: clean(interaction.payload?.toolName, 120),
|
||||
};
|
||||
|
|
@ -66,7 +67,7 @@ function approvalRequest(interaction) {
|
|||
return {
|
||||
id,
|
||||
kind: 'question',
|
||||
title: questions.length > 1 ? `Harness 需要 ${questions.length} 项补充信息` : clean(questions[0].header || questions[0].question, 160),
|
||||
title: questions.length > 1 ? t('Harness 需要 {count} 项补充信息', { count: questions.length }) : clean(questions[0].header || questions[0].question, 160),
|
||||
prompt: questions.map((question, index) => harnessQuestionText(question, index, questions.length)).join('\n\n---\n\n'),
|
||||
questions,
|
||||
};
|
||||
|
|
@ -216,14 +217,14 @@ export class OfficeJobExecutor {
|
|||
const workspace = this.#config.workspaces[job.workspaceAlias];
|
||||
const preset = this.#config.instructionPresets[job.instructionPreset];
|
||||
if (!workspace || !preset) {
|
||||
const error = new Error('Office Job 引用了本机未配置的 Workspace/Preset alias。');
|
||||
const error = new Error(t('Office Job 引用了本机未配置的 Workspace/Preset alias。'));
|
||||
error.code = 'office-job-alias-invalid';
|
||||
throw error;
|
||||
}
|
||||
entry.harness = this.#createHarness({ workspace });
|
||||
await this.#progress(jobId, entry, { kind: 'status', message: `已领取 Job,准备 Workspace alias:${job.workspaceAlias}` }, true);
|
||||
await this.#progress(jobId, entry, { kind: 'status', message: t('已领取 Job,准备 Workspace alias:{alias}', { alias: job.workspaceAlias }) }, true);
|
||||
entry.sessionId = await entry.harness.createSession({ signal, workspace });
|
||||
await this.#progress(jobId, entry, { kind: 'status', message: 'Harness Session 已创建。', sessionId: entry.sessionId }, true);
|
||||
await this.#progress(jobId, entry, { kind: 'status', message: t('Harness Session 已创建。'), sessionId: entry.sessionId }, true);
|
||||
const answer = await entry.harness.ask(entry.sessionId, renderPrompt(job, preset), {
|
||||
timeoutMs: 30 * 60_000,
|
||||
signal,
|
||||
|
|
@ -300,7 +301,7 @@ export class OfficeJobExecutor {
|
|||
}
|
||||
|
||||
async #handleUpdate(jobId, entry, update) {
|
||||
const message = update?.type === 'tool' ? `正在使用 ${clean(update.name, 160) || 'Harness 工具'}…`
|
||||
const message = update?.type === 'tool' ? t('正在使用 {tool}…', { tool: clean(update.name, 160) || t('Harness 工具') })
|
||||
: clean(update?.text, 4_000);
|
||||
if (!message) return;
|
||||
const key = `${update.type}:${message}`;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { setTimeout as sleep } from 'node:timers/promises';
|
||||
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
import { OfficeTransport } from './office-transport.mjs';
|
||||
import { OFFICE_PROTOCOL_VERSION } from './protocol.mjs';
|
||||
import { OfficeJobExecutor } from './office-job-executor.mjs';
|
||||
|
|
@ -9,12 +10,12 @@ const RETRY_DELAYS = Object.freeze([1_000, 3_000, 10_000, 30_000]);
|
|||
function safeConnectionError(error) {
|
||||
const code = typeof error?.code === 'string' ? error.code : 'office-connection-failed';
|
||||
const messages = {
|
||||
'invalid-device-token': 'AI Office 拒绝了 Device Token。',
|
||||
'office-hook-unavailable': 'AI Office Connector Hook 尚未就绪。',
|
||||
'office-protocol-mismatch': 'AI Office Connector 协议版本不兼容。',
|
||||
'office-transport-failed': '本机暂时无法访问 AI Office。',
|
||||
'invalid-device-token': t('AI Office 拒绝了 Device Token。'),
|
||||
'office-hook-unavailable': t('AI Office Connector Hook 尚未就绪。'),
|
||||
'office-protocol-mismatch': t('AI Office Connector 协议版本不兼容。'),
|
||||
'office-transport-failed': t('本机暂时无法访问 AI Office。'),
|
||||
};
|
||||
return { code, message: messages[code] ?? 'AI Office 连接已中断。' };
|
||||
return { code, message: messages[code] ?? t('AI Office 连接已中断。') };
|
||||
}
|
||||
|
||||
export class OfficeRuntime {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue