mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 01:53:21 +08:00
Merge pull request #46 from zaakirio/feat/host-i18n-en
feat(i18n): add English support for host-side chat messages
This commit is contained in:
commit
3741dd8fd9
78 changed files with 2290 additions and 1000 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -6,6 +6,16 @@ This file records the notable changes in each dsh-im release. Its format follows
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added / 新增
|
||||
|
||||
- 新增机器人聊天消息的英文支持:Host 配置 `language: en`(或环境变量 `DSH_IM_LANGUAGE=en`)后,各渠道发送给用户的提示、命令帮助和交互消息会切换为英文;未设置或未收录的文案仍以中文原样输出,不影响现有中文用户。
|
||||
Added English support for bot chat messages: with `language: en` in the Host config (or the `DSH_IM_LANGUAGE=en` environment variable), prompts, command help, and interaction messages sent by every channel switch to English; unset or untranslated text is still sent verbatim in Chinese, so existing Chinese users are unaffected.
|
||||
|
||||
### Changed / 变更
|
||||
|
||||
- 统一各渠道共用的英文文案到共享词典,消除同名键在不同渠道词典中的重复定义。
|
||||
Consolidated English copy shared across channels into the shared dictionaries, removing duplicate keys that were defined in multiple channel dictionaries.
|
||||
|
||||
## [1.5.0] - 2026-08-24
|
||||
|
||||
### Added / 新增
|
||||
|
|
|
|||
14
README.en.md
14
README.en.md
|
|
@ -190,7 +190,7 @@ If the Slack desktop app has no native Slash Command registered with the same na
|
|||
|
||||
- Registers one **IM Bot** settings page containing nine IM channels and one AI Office Connector.
|
||||
- Maintains the Host, client, and runtime sources for all nine channels and the Office Connector in this repository without external standalone plugins.
|
||||
- Follows the DeepSeek Harness language preference and switches the settings UI live between Chinese and English.
|
||||
- Follows the DeepSeek Harness language preference and switches the settings UI live between Chinese and English. Bot chat messages follow the Host's `language` config (Chinese by default; `en` switches them to English), with Chinese always as the fallback so untranslated text is sent verbatim.
|
||||
- Uses logos for WeChat, Feishu, DingTalk, WeCom, QQ, Slack, Telegram, Discord, WhatsApp, and AI Office navigation without enable/disable switches.
|
||||
- Keeps RPC endpoints, credentials, connection supervision, and session mappings isolated by IM channel; the Office Connector separately owns Device credentials, Job leases, approval waits, and concurrency limits.
|
||||
- Returns only QR codes, the public Slack Manifest, redacted status data, and access modes or allowlist identifiers explicitly saved for the current Telegram or WhatsApp bot. Manually entered secrets and Tokens travel one way to the local Host; no RPC response returns App Secrets, `bot_token`, DingTalk `client_secret`, WeCom Secrets, QQ `app_secret`, Slack Bot/App Tokens, Telegram/Discord Bot Tokens, WhatsApp linked-device keys, AI Office Device Tokens, or other raw user identifiers observed from platform messages.
|
||||
|
|
@ -215,6 +215,18 @@ IM management RPCs accept loopback browsers by default. When a Web profile is de
|
|||
|
||||
`trusted-host` reuses Harness's Host/Origin fence; it is not user authentication. Anyone who can reach that LAN authority can inspect bot status, scan or submit application credentials, reconnect bots, and remove bots. Enable it only on a trusted network.
|
||||
|
||||
### Bot chat message language
|
||||
|
||||
Bot chat messages are in Chinese by default. To switch them to English, set `language: en` in the plugin config (also accepts `en-US` or `english`), or set the `DSH_IM_LANGUAGE=en` environment variable:
|
||||
|
||||
```yaml
|
||||
- id: xmanrui-dsh-im
|
||||
config:
|
||||
language: en
|
||||
```
|
||||
|
||||
Without a setting, Chinese is used. Chinese is always the fallback language — any text missing from the English dictionary is sent verbatim in Chinese, so this feature never changes the behavior of existing Chinese users.
|
||||
|
||||
---
|
||||
|
||||
## Contact
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -193,7 +193,7 @@ Slack 桌面端若未注册同名的原生 Slash Command,会拦截直接以 `/
|
|||
|
||||
- Harness 中只注册一个「IM机器人」设置页,其中包含九个 IM 渠道和一个 AI Office Connector;
|
||||
- 九个渠道及 Office Connector 的 Host、客户端与运行时源码都在本仓库维护,不依赖外部独立插件;
|
||||
- 设置页跟随 DeepSeek Harness 的语言选择,在中文和 English 之间即时切换;
|
||||
- 设置页跟随 DeepSeek Harness 的语言选择,在中文和 English 之间即时切换;机器人发出的聊天消息跟随 Host 的 `language` 配置(默认中文;设为 `en` 即为英文),中文始终为兜底,未收录的文案原样输出;
|
||||
- 左侧使用 Logo 切换微信、飞书、钉钉、企业微信、QQ、Slack、Telegram、Discord、WhatsApp 和 AI Office,不使用启用/停用开关;
|
||||
- 九个 IM 渠道保持独立的 RPC、凭据、连接监督和会话映射;Office Connector 另行维护设备凭据、Job 租约、审批等待与并发上限;
|
||||
- 浏览器只获得二维码、Manifest、脱敏状态,以及用户为当前 Telegram 或 WhatsApp 机器人主动保存的访问模式和白名单标识;手动输入的 Secret 或 Token 仅单向提交给本机 Host,任何 RPC 响应都不会返回 App Secret、`bot_token`、钉钉 `client_secret`、企业微信 Secret、QQ `app_secret`、Slack Bot/App Token、Telegram/Discord Bot Token、WhatsApp 关联设备密钥、AI Office Device Token,或从平台消息中观察到的其他原始用户标识。
|
||||
|
|
@ -218,6 +218,18 @@ IM 管理 RPC 默认仅接受回环浏览器。如果 Web profile 在受信任
|
|||
|
||||
`trusted-host` 只复用 Harness 的 Host/Origin 防护,不是用户认证。启用后,能访问该局域网地址的人也能查看机器人状态、扫码或提交应用凭据、重连和删除机器人;只应在可信网络中使用。
|
||||
|
||||
### 聊天消息语言
|
||||
|
||||
机器人发出的聊天消息默认使用中文。要切换为英文,在插件配置中设置 `language: en`(也接受 `en-US`、`english`),或设置环境变量 `DSH_IM_LANGUAGE=en`:
|
||||
|
||||
```yaml
|
||||
- id: xmanrui-dsh-im
|
||||
config:
|
||||
language: en
|
||||
```
|
||||
|
||||
未设置时保持中文;中文始终是兜底语言,任何未收录到英文词典的文案都会原样以中文输出,因此该功能不会改变现有中文用户的行为。
|
||||
|
||||
---
|
||||
|
||||
## 联系方式
|
||||
|
|
|
|||
373
lib/index.js
373
lib/index.js
File diff suppressed because one or more lines are too long
|
|
@ -9,6 +9,7 @@ import { apply as applyWecom } from './channels/wecom/index.mjs';
|
|||
import { apply as applyWeixin } from './channels/weixin/index.mjs';
|
||||
import { apply as applyWhatsapp } from './channels/whatsapp/index.mjs';
|
||||
import { installOutboundArtifactTool } from '../../src/channels/shared/semantic/artifact.mjs';
|
||||
import { setImHostLanguage } from '../../src/channels/shared/i18n.mjs';
|
||||
|
||||
export const name = 'dsh-im-host';
|
||||
export const inject = [
|
||||
|
|
@ -40,6 +41,7 @@ export function createImHostPlugin(internals = {}) {
|
|||
name,
|
||||
inject,
|
||||
async apply(ctx, config = {}) {
|
||||
setImHostLanguage(config.language ?? process.env.DSH_IM_LANGUAGE);
|
||||
if (typeof ctx?.inject === 'function') {
|
||||
ctx.inject(['tools', 'systemPrompt'], (artifactCtx) => {
|
||||
installOutboundArtifactTool(artifactCtx);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
maskPlatformId,
|
||||
TokenBotConfigStore,
|
||||
} from '../shared/token-config-store.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const IDENTITY_OPTIONS = Object.freeze({
|
||||
botPrefix: 'discord',
|
||||
|
|
@ -14,7 +15,7 @@ export function deriveDiscordBotIdentity(platformId) {
|
|||
}
|
||||
|
||||
export function maskDiscordBotId(platformId) {
|
||||
return maskPlatformId(platformId, 'Discord机器人');
|
||||
return maskPlatformId(platformId, t('Discord机器人'));
|
||||
}
|
||||
|
||||
export class DiscordConfigStore extends TokenBotConfigStore {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { createHash } from 'node:crypto';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const DEFAULT_BASE_URL = 'https://discord.com/api/v10/';
|
||||
const DEFAULT_FILE_UPLOAD_TIMEOUT_MS = 120_000;
|
||||
|
|
@ -298,7 +299,7 @@ export async function inspectDiscordToken(token, options = {}) {
|
|||
if (!bot?.id || bot?.bot !== true) throw new Error('Discord token does not belong to a bot');
|
||||
return {
|
||||
platformId: String(bot.id),
|
||||
name: cleanString(bot.global_name) ?? cleanString(bot.username) ?? 'Discord机器人',
|
||||
name: cleanString(bot.global_name) ?? cleanString(bot.username) ?? t('Discord机器人'),
|
||||
username: cleanString(bot.username),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { TextHarnessBridge, createTextBridgeStatus } from '../shared/text-harness-bridge.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
export const DISCORD_DESCRIPTOR = Object.freeze({
|
||||
key: 'discord',
|
||||
label: 'Discord',
|
||||
connectionLabel: ' Gateway 长连接',
|
||||
connectionLabel: t(' Gateway 长连接'),
|
||||
});
|
||||
|
||||
export class DiscordHarnessBridge extends TextHarnessBridge {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { createEditableMessageStream, splitMessageText } from '../shared/editable-message-stream.mjs';
|
||||
import { fetchFileStream } from '../shared/file-download.mjs';
|
||||
import { fetchImageBuffer } from '../shared/image-prompt.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
import { DiscordApi } from './discord-api.mjs';
|
||||
import { createDiscordBridgeStatus, DiscordHarnessBridge } from './discord-bridge.mjs';
|
||||
|
||||
|
|
@ -47,12 +48,12 @@ function eventData(event) {
|
|||
|
||||
function gatewayCloseError(code) {
|
||||
if (code === 4004) {
|
||||
const error = new Error('Discord Bot Token 无效,请重新填写。');
|
||||
const error = new Error(t('Discord Bot Token 无效,请重新填写。'));
|
||||
error.code = 'discord-401';
|
||||
return error;
|
||||
}
|
||||
if (code === 4013 || code === 4014) {
|
||||
const error = new Error('Discord Gateway Intents 配置不正确,请检查 Developer Portal 的 Bot 设置。');
|
||||
const error = new Error(t('Discord Gateway Intents 配置不正确,请检查 Developer Portal 的 Bot 设置。'));
|
||||
error.code = 'discord-intents';
|
||||
return error;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,13 +51,15 @@ import {
|
|||
watchListCard,
|
||||
workspaceListCard,
|
||||
} from './feishu-cards.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
import { MAX_WATCHES_PER_KEY } from './state-store.mjs';
|
||||
import {
|
||||
FEISHU_GROUP_RESPONSE_MODES,
|
||||
normalizeFeishuGroupResponseMode,
|
||||
} from './group-response-mode.mjs';
|
||||
|
||||
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
||||
// Lazily evaluated: t() must run after setImHostLanguage, not at import time.
|
||||
const INTERACTION_RESOLVED_TEXT = () => t('这个问题已在其他客户端处理,无需再次回答。');
|
||||
const RESOLVED_REPLY_TTL_MS = 30 * 60_000;
|
||||
|
||||
const MENU_COMMAND = /^\/m(?:enu)?$/i;
|
||||
|
|
@ -87,34 +89,37 @@ const REPAIR_URL_HOSTS = new Set([
|
|||
'open.larksuite.com',
|
||||
]);
|
||||
|
||||
const HELP_TEXT = [
|
||||
'北汇星河 AIOS 已连接 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 检查连接状态',
|
||||
'/repair 修复卡片按钮回调',
|
||||
'/m(或 /menu) 打开交互卡片菜单',
|
||||
'/watch [Session ID 或序号] 关注会话,任务完成自动推送',
|
||||
'/unwatch [Session ID 或序号] 取消关注',
|
||||
'/watchlist 查看关注列表',
|
||||
'/archived on|off 会话列表是否包含归档会话',
|
||||
'/help 显示本帮助',
|
||||
].join('\n');
|
||||
// Built lazily: t() must run after setImHostLanguage, not at import time.
|
||||
function helpText() {
|
||||
return [
|
||||
t('北汇星河 AIOS 已连接 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('/repair 修复卡片按钮回调'),
|
||||
t('/m(或 /menu) 打开交互卡片菜单'),
|
||||
t('/watch [Session ID 或序号] 关注会话,任务完成自动推送'),
|
||||
t('/unwatch [Session ID 或序号] 取消关注'),
|
||||
t('/watchlist 查看关注列表'),
|
||||
t('/archived on|off 会话列表是否包含归档会话'),
|
||||
t('/help 显示本帮助'),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
const ARCHIVED_COMMAND = /^\/archived(?:\s+(on|off))?$/i;
|
||||
|
||||
|
|
@ -122,43 +127,43 @@ const ARCHIVED_COMMAND = /^\/archived(?:\s+(on|off))?$/i;
|
|||
function safeErrorText(error) {
|
||||
switch (error?.code) {
|
||||
case 'workspace-not-absolute':
|
||||
return '工作区必须是绝对路径。';
|
||||
return t('工作区必须是绝对路径。');
|
||||
case 'workspace-not-found':
|
||||
return '工作区路径不存在。';
|
||||
return t('工作区路径不存在。');
|
||||
case 'workspace-not-directory':
|
||||
return '工作区路径必须指向一个目录。';
|
||||
return t('工作区路径必须指向一个目录。');
|
||||
case 'workspace-bot-not-found':
|
||||
return '机器人正在移除或已重新接入,无法操作原会话的工作区。';
|
||||
return t('机器人正在移除或已重新接入,无法操作原会话的工作区。');
|
||||
default:
|
||||
return '操作失败,请稍后重试。';
|
||||
return t('操作失败,请稍后重试。');
|
||||
}
|
||||
}
|
||||
|
||||
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-permission-required':
|
||||
return `结果文件「${name}」已生成,但机器人缺少飞书文件上传权限。请为应用添加 im:resource 并完成必要审批后重试。`;
|
||||
return t('结果文件「{name}」已生成,但机器人缺少飞书文件上传权限。请为应用添加 im:resource 并完成必要审批后重试。', { name });
|
||||
case 'artifact-too-large':
|
||||
return `结果文件「${name}」超过飞书 30 MB 上限,未发送。`;
|
||||
return t('结果文件「{name}」超过飞书 30 MB 上限,未发送。', { 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-delivery-uncertain':
|
||||
return `结果文件「${name}」发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。`;
|
||||
return t('结果文件「{name}」发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
|
||||
default:
|
||||
return `结果文件「${name}」已生成,但暂时未能发送,请稍后重试。`;
|
||||
return t('结果文件「{name}」已生成,但暂时未能发送,请稍后重试。', { name });
|
||||
}
|
||||
}
|
||||
|
||||
function answerTextForDelivery(answer, artifacts) {
|
||||
if (typeof answer === 'string' && answer.trim()) return answer;
|
||||
return artifacts.length > 0 ? '结果文件已生成。' : answer;
|
||||
return artifacts.length > 0 ? t('结果文件已生成。') : answer;
|
||||
}
|
||||
|
||||
function nonEmptyString(value) {
|
||||
|
|
@ -611,7 +616,7 @@ export class FeishuHarnessBridge {
|
|||
event.message.chat_id,
|
||||
inboundFileUserMessage(error)
|
||||
?? imagePromptUserMessage(error)
|
||||
?? '处理失败,请稍后重试。如果问题持续,请在 DeepSeek Harness 的飞书插件页面检查连接状态。',
|
||||
?? t('处理失败,请稍后重试。如果问题持续,请在 DeepSeek Harness 的飞书插件页面检查连接状态。'),
|
||||
).catch(() => undefined);
|
||||
}
|
||||
|
||||
|
|
@ -675,7 +680,7 @@ export class FeishuHarnessBridge {
|
|||
const hasFiles = hasInboundFiles(message);
|
||||
const commandText = event.message.message_type === 'text' && !hasImages && !hasFiles ? text : null;
|
||||
if (!text && !hasImages && !hasFiles) {
|
||||
await this.#send(event.message.chat_id, '目前支持文字、图片和文件消息。');
|
||||
await this.#send(event.message.chat_id, t('目前支持文字、图片和文件消息。'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -684,7 +689,7 @@ export class FeishuHarnessBridge {
|
|||
return;
|
||||
}
|
||||
if (commandText === '/help') {
|
||||
await this.#send(event.message.chat_id, HELP_TEXT);
|
||||
await this.#send(event.message.chat_id, helpText());
|
||||
return;
|
||||
}
|
||||
if (MENU_COMMAND.test(commandText)) {
|
||||
|
|
@ -694,12 +699,12 @@ export class FeishuHarnessBridge {
|
|||
}
|
||||
if (commandText === '/new') {
|
||||
await this.#state.clearSession(key);
|
||||
await this.#send(event.message.chat_id, '已开启全新 Harness 会话。');
|
||||
await this.#send(event.message.chat_id, t('已开启全新 Harness 会话。'));
|
||||
return;
|
||||
}
|
||||
if (commandText === '/status') {
|
||||
await this.#harness.ensureRunning({ signal: this.#signal });
|
||||
await this.#send(event.message.chat_id, '飞书机器人与 DeepSeek Harness 连接正常。');
|
||||
await this.#send(event.message.chat_id, t('飞书机器人与 DeepSeek Harness 连接正常。'));
|
||||
return;
|
||||
}
|
||||
if (SESSION_LIST_PREFIX.test(commandText)) {
|
||||
|
|
@ -729,7 +734,7 @@ export class FeishuHarnessBridge {
|
|||
const match = ARCHIVED_COMMAND.exec(commandText);
|
||||
const value = match[1]?.toLowerCase();
|
||||
if (value !== 'on' && value !== 'off') {
|
||||
await this.#send(event.message.chat_id, '用法:/archived on(包含归档会话)或 /archived off(隐藏归档会话)');
|
||||
await this.#send(event.message.chat_id, t('用法:/archived on(包含归档会话)或 /archived off(隐藏归档会话)'));
|
||||
return;
|
||||
}
|
||||
if (typeof this.#state?.setIncludeArchivedSessions === 'function') {
|
||||
|
|
@ -737,7 +742,7 @@ export class FeishuHarnessBridge {
|
|||
}
|
||||
await this.#send(
|
||||
event.message.chat_id,
|
||||
value === 'on' ? '已开启:会话列表包含归档会话。' : '已关闭:会话列表隐藏归档会话。',
|
||||
value === 'on' ? t('已开启:会话列表包含归档会话。') : t('已关闭:会话列表隐藏归档会话。'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -794,7 +799,7 @@ export class FeishuHarnessBridge {
|
|||
// and number replies because callback buttons are the capability being fixed.
|
||||
async #handleRepairCommand(event, commandText) {
|
||||
if (event?.message?.chat_type !== 'p2p') {
|
||||
await this.#send(event.message.chat_id, '为避免授权链接暴露,请私聊机器人发送 /repair。');
|
||||
await this.#send(event.message.chat_id, t('为避免授权链接暴露,请私聊机器人发送 /repair。'));
|
||||
return;
|
||||
}
|
||||
const actorOpenId = strictSenderOpenId(event);
|
||||
|
|
@ -802,19 +807,19 @@ export class FeishuHarnessBridge {
|
|||
await this.#send(
|
||||
event.message.chat_id,
|
||||
this.#repairOwnerOpenIds.size === 0
|
||||
? '当前机器人没有可验证的接入者身份,不能从聊天发起修复;请先在插件页设置管理员。'
|
||||
: '此操作只能由机器人接入者在私聊中发起,未进行任何修改。',
|
||||
? t('当前机器人没有可验证的接入者身份,不能从聊天发起修复;请先在插件页设置管理员。')
|
||||
: t('此操作只能由机器人接入者在私聊中发起,未进行任何修改。'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!this.#repair) {
|
||||
await this.#send(event.message.chat_id, '当前 Host 版本暂不支持聊天内修复,请先更新插件。');
|
||||
await this.#send(event.message.chat_id, t('当前 Host 版本暂不支持聊天内修复,请先更新插件。'));
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = REPAIR_COMMAND.exec(commandText);
|
||||
if (!parsed) {
|
||||
await this.#send(event.message.chat_id, '用法:/repair、/repair qr、/repair status、/repair cancel 或 /repair verify');
|
||||
await this.#send(event.message.chat_id, t('用法:/repair、/repair qr、/repair status、/repair cancel 或 /repair verify'));
|
||||
return;
|
||||
}
|
||||
const operation = parsed[1]?.toLowerCase() ?? 'start';
|
||||
|
|
@ -828,12 +833,12 @@ export class FeishuHarnessBridge {
|
|||
if (!attempt) {
|
||||
await this.#send(
|
||||
chatId,
|
||||
'当前 Runtime 没有可恢复的修复任务记录(机器人可能刚完成密钥更新并重启)。本命令不会启动新的授权;请查看机器人发送的验证结果,确认上一次任务已结束后再发送 /repair。',
|
||||
t('当前 Runtime 没有可恢复的修复任务记录(机器人可能刚完成密钥更新并重启)。本命令不会启动新的授权;请查看机器人发送的验证结果,确认上一次任务已结束后再发送 /repair。'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (attempt.actorOpenId !== actorOpenId) {
|
||||
await this.#send(chatId, '另一位管理员正在修复该机器人,本次不会显示其授权信息。');
|
||||
await this.#send(chatId, t('另一位管理员正在修复该机器人,本次不会显示其授权信息。'));
|
||||
return;
|
||||
}
|
||||
if (operation === 'cancel') {
|
||||
|
|
@ -843,7 +848,7 @@ export class FeishuHarnessBridge {
|
|||
snapshot = repairSnapshot(result, { botId: this.#botId });
|
||||
attempt.snapshot = snapshot;
|
||||
} catch {
|
||||
await this.#send(chatId, '暂时无法取消修复任务,请稍后重试。');
|
||||
await this.#send(chatId, t('暂时无法取消修复任务,请稍后重试。'));
|
||||
return;
|
||||
}
|
||||
if (snapshot.state === 'cancelled') {
|
||||
|
|
@ -858,7 +863,7 @@ export class FeishuHarnessBridge {
|
|||
try {
|
||||
snapshot = await this.#refreshRepairAttempt(attempt);
|
||||
} catch {
|
||||
await this.#send(chatId, '暂时无法查询修复状态,请稍后重试。');
|
||||
await this.#send(chatId, t('暂时无法查询修复状态,请稍后重试。'));
|
||||
return;
|
||||
}
|
||||
if (operation === 'qr') {
|
||||
|
|
@ -887,7 +892,7 @@ export class FeishuHarnessBridge {
|
|||
const previous = this.#repairAttempt;
|
||||
if (previous && REPAIR_ACTIVE_STATES.has(previous.snapshot.state)) {
|
||||
if (previous.actorOpenId !== actorOpenId) {
|
||||
await this.#send(chatId, '另一位管理员正在修复该机器人,本次不会显示其授权信息。');
|
||||
await this.#send(chatId, t('另一位管理员正在修复该机器人,本次不会显示其授权信息。'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
|
@ -897,7 +902,7 @@ export class FeishuHarnessBridge {
|
|||
return;
|
||||
}
|
||||
} catch {
|
||||
await this.#send(chatId, '暂时无法查询修复状态,请稍后重试。');
|
||||
await this.#send(chatId, t('暂时无法查询修复状态,请稍后重试。'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -911,7 +916,7 @@ export class FeishuHarnessBridge {
|
|||
}), { botId: this.#botId });
|
||||
snapshot = await this.#waitForRepairLink(snapshot, { actorOpenId, chatId });
|
||||
} catch {
|
||||
await this.#send(chatId, '修复流程暂时失败,现有机器人连接不受影响;请稍后发送 /repair 重试。');
|
||||
await this.#send(chatId, t('修复流程暂时失败,现有机器人连接不受影响;请稍后发送 /repair 重试。'));
|
||||
return;
|
||||
}
|
||||
const attempt = {
|
||||
|
|
@ -932,7 +937,7 @@ export class FeishuHarnessBridge {
|
|||
} catch {
|
||||
attempt.stopped = true;
|
||||
await this.#repair.cancel(this.#repairArgs(attempt)).catch(() => undefined);
|
||||
await this.#send(chatId, '飞书返回了无法安全验证的授权链接,已中止本次修复。');
|
||||
await this.#send(chatId, t('飞书返回了无法安全验证的授权链接,已中止本次修复。'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -945,7 +950,7 @@ export class FeishuHarnessBridge {
|
|||
}
|
||||
if (!attempt.verificationUrl) {
|
||||
attempt.stopped = true;
|
||||
await this.#send(chatId, '飞书未返回授权链接,已中止本次修复。');
|
||||
await this.#send(chatId, t('飞书未返回授权链接,已中止本次修复。'));
|
||||
return;
|
||||
}
|
||||
await this.#sendRepairLink(chatId, attempt.verificationUrl, snapshot);
|
||||
|
|
@ -1000,7 +1005,7 @@ export class FeishuHarnessBridge {
|
|||
attempt.announcedSaving = true;
|
||||
await this.#send(
|
||||
attempt.chatId,
|
||||
'授权已确认,正在发送并等待测试按钮回调;收到真实回调后才会完成。',
|
||||
t('授权已确认,正在发送并等待测试按钮回调;收到真实回调后才会完成。'),
|
||||
);
|
||||
}
|
||||
if (REPAIR_TERMINAL_STATES.has(snapshot.state)) {
|
||||
|
|
@ -1021,7 +1026,7 @@ export class FeishuHarnessBridge {
|
|||
this.#logger.warn?.('[dsh-feishu] callback repair status monitoring failed');
|
||||
await this.#send(
|
||||
attempt.chatId,
|
||||
'修复状态查询中断,现有机器人连接不受影响;发送 /repair status 重试查询。',
|
||||
t('修复状态查询中断,现有机器人连接不受影响;发送 /repair status 重试查询。'),
|
||||
).catch(() => undefined);
|
||||
});
|
||||
}
|
||||
|
|
@ -1030,16 +1035,16 @@ export class FeishuHarnessBridge {
|
|||
const remaining = snapshot.remainingSeconds
|
||||
?? (snapshot.expiresAt ? Math.max(0, Math.ceil((snapshot.expiresAt - Date.now()) / 1000)) : null);
|
||||
const expiry = remaining === null
|
||||
? '链接为短期有效'
|
||||
: `链接约 ${Math.max(1, Math.ceil(remaining / 60))} 分钟后过期`;
|
||||
? t('链接为短期有效')
|
||||
: t('链接约 {minutes} 分钟后过期', { minutes: Math.max(1, Math.ceil(remaining / 60)) });
|
||||
await this.#send(chatId, [
|
||||
existing ? '已有一个修复任务在等待授权。' : '🔧 准备修复卡片按钮。',
|
||||
'本次只会增量添加 card.action.trigger。请核对确认页只显示这一项;若出现其他权限或事件,请取消。',
|
||||
existing ? t('已有一个修复任务在等待授权。') : t('🔧 准备修复卡片按钮。'),
|
||||
t('本次只会增量添加 card.action.trigger。请核对确认页只显示这一项;若出现其他权限或事件,请取消。'),
|
||||
'',
|
||||
'当前设备直接打开:',
|
||||
t('当前设备直接打开:'),
|
||||
url,
|
||||
'',
|
||||
`若要用另一台设备扫码,发送 /repair qr。${expiry}。`,
|
||||
t('若要用另一台设备扫码,发送 /repair qr。{expiry}。', { expiry }),
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
|
|
@ -1055,9 +1060,12 @@ export class FeishuHarnessBridge {
|
|||
if (!imageKey) throw new Error('Feishu QR upload returned no image key');
|
||||
const remaining = snapshot.remainingSeconds
|
||||
?? (snapshot.expiresAt ? Math.max(0, Math.ceil((snapshot.expiresAt - Date.now()) / 1000)) : null);
|
||||
const remainingText = remaining === null
|
||||
? ''
|
||||
: t('(剩余约 {minutes} 分钟)', { minutes: Math.max(1, Math.ceil(remaining / 60)) });
|
||||
await this.#send(
|
||||
chatId,
|
||||
`请用另一台设备扫码完成授权${remaining === null ? '' : `(剩余约 ${Math.max(1, Math.ceil(remaining / 60))} 分钟)`}。`,
|
||||
t('请用另一台设备扫码完成授权{remaining}。', { remaining: remainingText }),
|
||||
);
|
||||
const response = await this.#client.im.v1.message.create({
|
||||
params: { receive_id_type: 'chat_id' },
|
||||
|
|
@ -1069,40 +1077,40 @@ export class FeishuHarnessBridge {
|
|||
});
|
||||
if (response?.code && response.code !== 0) throw new Error('Feishu QR message send failed');
|
||||
} catch {
|
||||
await this.#send(chatId, `二维码暂时无法发送,请直接打开授权链接:\n${url}`);
|
||||
await this.#send(chatId, t('二维码暂时无法发送,请直接打开授权链接:\n{url}', { url }));
|
||||
}
|
||||
}
|
||||
|
||||
#repairStatusText(snapshot, { verificationFocused = false } = {}) {
|
||||
if (snapshot.state === 'succeeded') {
|
||||
return '✅ 修复完成:已实测收到 card.action.trigger,菜单按钮现在可用。';
|
||||
return t('✅ 修复完成:已实测收到 card.action.trigger,菜单按钮现在可用。');
|
||||
}
|
||||
if (snapshot.state === 'expired' || snapshot.error?.code === 'expired_token') {
|
||||
return '授权链接已过期;平台未返回成功结果,无法确认已修复。发送 /repair 生成新链接。';
|
||||
return t('授权链接已过期;平台未返回成功结果,无法确认已修复。发送 /repair 生成新链接。');
|
||||
}
|
||||
if (snapshot.state === 'cancelled' || snapshot.error?.code === 'abort') {
|
||||
return '已取消本次修复授权,未确认完成修复。';
|
||||
return t('已取消本次修复授权,未确认完成修复。');
|
||||
}
|
||||
if (snapshot.error?.code === 'access_denied') {
|
||||
return '你已取消或拒绝授权,没有确认修复;发送 /repair 可重试。';
|
||||
return t('你已取消或拒绝授权,没有确认修复;发送 /repair 可重试。');
|
||||
}
|
||||
if (snapshot.error?.code === 'card_action_probe_timeout'
|
||||
|| snapshot.error?.code === 'card-action-probe-timeout') {
|
||||
return '授权已提交,但未收到测试按钮回调。可能尚未点击或配置仍在传播;稍后发送 /repair verify 查询,不要盲目重复授权。';
|
||||
return t('授权已提交,但未收到测试按钮回调。可能尚未点击或配置仍在传播;稍后发送 /repair verify 查询,不要盲目重复授权。');
|
||||
}
|
||||
if (snapshot.state === 'error') {
|
||||
return '修复流程暂时失败,现有机器人连接不受影响;发送 /repair 可重试。';
|
||||
return t('修复流程暂时失败,现有机器人连接不受影响;发送 /repair 可重试。');
|
||||
}
|
||||
if (snapshot.state === 'saving') {
|
||||
return '授权已确认,正在等待专用测试按钮的真实回调;回调到达前不会宣告成功。';
|
||||
return t('授权已确认,正在等待专用测试按钮的真实回调;回调到达前不会宣告成功。');
|
||||
}
|
||||
if (verificationFocused) {
|
||||
return '授权尚未完成,暂时不能验证卡片按钮。请先打开授权链接并确认。';
|
||||
return t('授权尚未完成,暂时不能验证卡片按钮。请先打开授权链接并确认。');
|
||||
}
|
||||
const remaining = snapshot.remainingSeconds === null
|
||||
? ''
|
||||
: `,剩余约 ${Math.max(1, Math.ceil(snapshot.remainingSeconds / 60))} 分钟`;
|
||||
return `修复任务正在等待授权${remaining}。发送 /repair qr 可获取二维码,/repair cancel 可取消。`;
|
||||
: t(',剩余约 {minutes} 分钟', { minutes: Math.max(1, Math.ceil(snapshot.remainingSeconds / 60)) });
|
||||
return t('修复任务正在等待授权{remaining}。发送 /repair qr 可获取二维码,/repair cancel 可取消。', { remaining });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1134,7 +1142,7 @@ export class FeishuHarnessBridge {
|
|||
// restart) or never came from us: nudge instead of staying silent.
|
||||
const chatId = nonEmptyString(event?.context?.open_chat_id);
|
||||
if (chatId) {
|
||||
this.#send(chatId, '这个菜单已过期,请回复 /m 重新打开。').catch(() => undefined);
|
||||
this.#send(chatId, t('这个菜单已过期,请回复 /m 重新打开。')).catch(() => undefined);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
|
@ -1161,12 +1169,12 @@ export class FeishuHarnessBridge {
|
|||
}
|
||||
if (action === 'new') {
|
||||
await this.#state.clearSession(key);
|
||||
await this.#send(chatId, '已开启全新 Harness 会话。');
|
||||
await this.#send(chatId, t('已开启全新 Harness 会话。'));
|
||||
return;
|
||||
}
|
||||
if (action === 'status') {
|
||||
await this.#harness.ensureRunning({ signal: this.#signal });
|
||||
await this.#send(chatId, '飞书机器人与 DeepSeek Harness 连接正常。');
|
||||
await this.#send(chatId, t('飞书机器人与 DeepSeek Harness 连接正常。'));
|
||||
return;
|
||||
}
|
||||
if (action === 'help') {
|
||||
|
|
@ -1213,7 +1221,7 @@ export class FeishuHarnessBridge {
|
|||
if (menu.kind === 'menu') {
|
||||
const action = ['sessions', 'workspaces', 'new', 'status', 'help', 'repair', 'watchlist'][number - 1];
|
||||
if (!action) {
|
||||
await this.#send(chatId, '菜单没有这个编号,回复 /m 重新打开。');
|
||||
await this.#send(chatId, t('菜单没有这个编号,回复 /m 重新打开。'));
|
||||
return;
|
||||
}
|
||||
if (action === 'repair') {
|
||||
|
|
@ -1226,7 +1234,7 @@ export class FeishuHarnessBridge {
|
|||
if (menu.kind === 'sessions') {
|
||||
const session = menu.sessions[number - 1];
|
||||
if (!session?.sessionId) {
|
||||
await this.#send(chatId, `本页只有 ${menu.sessions.length} 个会话,回复 /sessionlist 重新查看。`);
|
||||
await this.#send(chatId, t('本页只有 {count} 个会话,回复 /sessionlist 重新查看。', { count: menu.sessions.length }));
|
||||
return;
|
||||
}
|
||||
// The number label sits on the session (bind) button of the row.
|
||||
|
|
@ -1236,7 +1244,7 @@ export class FeishuHarnessBridge {
|
|||
if (menu.kind === 'workspaces') {
|
||||
const workspace = menu.paths[number - 1];
|
||||
if (!workspace) {
|
||||
await this.#send(chatId, `只有 ${menu.paths.length} 个工作区,回复 /workspacelist 重新查看。`);
|
||||
await this.#send(chatId, t('只有 {count} 个工作区,回复 /workspacelist 重新查看。', { count: menu.paths.length }));
|
||||
return;
|
||||
}
|
||||
await this.#handleCardAction(`workspace:${workspace}`, { chatId, key });
|
||||
|
|
@ -1245,7 +1253,7 @@ export class FeishuHarnessBridge {
|
|||
if (menu.kind === 'watches') {
|
||||
const entry = menu.entries[number - 1];
|
||||
if (!entry?.sessionId) {
|
||||
await this.#send(chatId, `关注列表只有 ${menu.entries.length} 个会话。`);
|
||||
await this.#send(chatId, t('关注列表只有 {count} 个会话。', { count: menu.entries.length }));
|
||||
return;
|
||||
}
|
||||
await this.#handleCardAction(`unwatch:${entry.sessionId}`, { chatId, key });
|
||||
|
|
@ -1271,7 +1279,7 @@ export class FeishuHarnessBridge {
|
|||
const sessions = this.#visibleSessions(Array.isArray(listed?.sessions) ? listed.sessions : []);
|
||||
const workspace = listed?.workspace ?? resolved.workspace;
|
||||
if (sessions.length === 0) {
|
||||
await this.#send(chatId, `工作区:${workspace}\n该工作区暂无会话。`);
|
||||
await this.#send(chatId, t('工作区:{workspace}\n该工作区暂无会话。', { workspace }));
|
||||
return;
|
||||
}
|
||||
const pageCount = Math.ceil(sessions.length / MENU_PAGE_SIZE);
|
||||
|
|
@ -1296,7 +1304,7 @@ export class FeishuHarnessBridge {
|
|||
);
|
||||
} catch (error) {
|
||||
this.#logger.warn?.('[dsh-feishu] session list failed:', error.message);
|
||||
await this.#send(chatId, '暂时无法获取会话列表,请稍后重试。');
|
||||
await this.#send(chatId, t('暂时无法获取会话列表,请稍后重试。'));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1307,26 +1315,26 @@ export class FeishuHarnessBridge {
|
|||
await this.#sendCard(chatId, workspaceListCard(paths, current), { key });
|
||||
} catch (error) {
|
||||
this.#logger.warn?.('[dsh-feishu] workspace list failed:', error.message);
|
||||
await this.#send(chatId, '暂时无法获取工作区列表,请稍后重试。');
|
||||
await this.#send(chatId, t('暂时无法获取工作区列表,请稍后重试。'));
|
||||
}
|
||||
}
|
||||
|
||||
async #bindSession(key, chatId, sessionId) {
|
||||
try {
|
||||
const bound = await this.#harness.bindWorkspaceSession(key, sessionId);
|
||||
const title = String(bound?.title ?? '').replace(/\s+/gu, ' ').trim() || '暂无标题';
|
||||
await this.#send(chatId, `已绑定会话「${title}」\nID:${bound?.sessionId ?? sessionId}`);
|
||||
const title = String(bound?.title ?? '').replace(/\s+/gu, ' ').trim() || t('暂无标题');
|
||||
await this.#send(chatId, t('已绑定会话「{title}」\nID:{id}', { title, id: bound?.sessionId ?? sessionId }));
|
||||
} catch (error) {
|
||||
await this.#send(chatId, `绑定失败:${safeErrorText(error)}`);
|
||||
await this.#send(chatId, t('绑定失败:{message}', { message: safeErrorText(error) }));
|
||||
}
|
||||
}
|
||||
|
||||
async #switchWorkspace(key, chatId, workspace) {
|
||||
try {
|
||||
const current = await this.#harness.switchWorkspace(workspace);
|
||||
await this.#send(chatId, `工作区已切换为:${current}`);
|
||||
await this.#send(chatId, t('工作区已切换为:{workspace}', { workspace: current }));
|
||||
} catch (error) {
|
||||
await this.#send(chatId, `切换失败:${safeErrorText(error)}`);
|
||||
await this.#send(chatId, t('切换失败:{message}', { message: safeErrorText(error) }));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1398,7 +1406,7 @@ export class FeishuHarnessBridge {
|
|||
*/
|
||||
async #resolveWatchTarget(target) {
|
||||
if (typeof target !== 'string' || target === '') {
|
||||
return { error: '用法:/watch <Session ID 或当前工作区序号>' };
|
||||
return { error: t('用法:/watch <Session ID 或当前工作区序号>') };
|
||||
}
|
||||
const numeric = /^\d{1,4}$/.test(target) ? Number(target) : null;
|
||||
const currentPath = typeof this.#harness?.currentWorkspace === 'function'
|
||||
|
|
@ -1409,13 +1417,13 @@ export class FeishuHarnessBridge {
|
|||
return Array.isArray(listed?.sessions) ? listed.sessions : [];
|
||||
};
|
||||
if (numeric !== null) {
|
||||
if (!currentPath) return { error: '当前机器人没有可用的工作区,无法按序号解析会话。' };
|
||||
if (!currentPath) return { error: t('当前机器人没有可用的工作区,无法按序号解析会话。') };
|
||||
const sessions = this.#visibleSessions(await listSessions(currentPath));
|
||||
const session = sessions[numeric - 1];
|
||||
if (!session?.sessionId) {
|
||||
return { error: `当前工作区只有 ${sessions.length} 个会话。` };
|
||||
return { error: t('当前工作区只有 {count} 个会话。', { count: sessions.length }) };
|
||||
}
|
||||
return { sessionId: session.sessionId, title: session.title ?? '暂无标题' };
|
||||
return { sessionId: session.sessionId, title: session.title ?? t('暂无标题') };
|
||||
}
|
||||
const extraPaths = typeof this.#harness?.listWorkspaces === 'function'
|
||||
? (await this.#harness.listWorkspaces()).filter((path) => path !== currentPath)
|
||||
|
|
@ -1424,9 +1432,9 @@ export class FeishuHarnessBridge {
|
|||
for (const workspace of paths) {
|
||||
const sessions = await listSessions(workspace);
|
||||
const session = sessions.find((candidate) => candidate.sessionId === target);
|
||||
if (session) return { sessionId: target, title: session.title ?? '暂无标题' };
|
||||
if (session) return { sessionId: target, title: session.title ?? t('暂无标题') };
|
||||
}
|
||||
return { error: '没有找到这个会话,请用 /sessionlist 查看可用会话。' };
|
||||
return { error: t('没有找到这个会话,请用 /sessionlist 查看可用会话。') };
|
||||
}
|
||||
|
||||
async #latestSessionSeq(sessionId) {
|
||||
|
|
@ -1443,14 +1451,14 @@ export class FeishuHarnessBridge {
|
|||
async #runWatch(key, chatId, target) {
|
||||
this.#ensureEventWatcher();
|
||||
if (typeof this.#state?.setWatch !== 'function') {
|
||||
await this.#send(chatId, '当前状态存储不支持关注。');
|
||||
await this.#send(chatId, t('当前状态存储不支持关注。'));
|
||||
return;
|
||||
}
|
||||
let resolved;
|
||||
try {
|
||||
resolved = await this.#resolveWatchTarget(target);
|
||||
} catch (error) {
|
||||
await this.#send(chatId, `无法解析会话:${safeErrorText(error)}`);
|
||||
await this.#send(chatId, t('无法解析会话:{message}', { message: safeErrorText(error) }));
|
||||
return;
|
||||
}
|
||||
if (resolved.error) {
|
||||
|
|
@ -1460,7 +1468,7 @@ export class FeishuHarnessBridge {
|
|||
const existing = this.#state.watchEntries?.(key) ?? [];
|
||||
const existingEntry = existing.find((entry) => entry.sessionId === resolved.sessionId);
|
||||
if (!existingEntry && existing.length >= MAX_WATCHES_PER_KEY) {
|
||||
await this.#send(chatId, `每个聊天最多关注 ${MAX_WATCHES_PER_KEY} 个会话。`);
|
||||
await this.#send(chatId, t('每个聊天最多关注 {count} 个会话。', { count: MAX_WATCHES_PER_KEY }));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
|
@ -1473,10 +1481,10 @@ export class FeishuHarnessBridge {
|
|||
chatId,
|
||||
lastSeq,
|
||||
});
|
||||
await this.#send(chatId, `已关注会话「${String(resolved.title).replace(/\s+/gu, ' ')}」,任务完成会推送结果。`);
|
||||
await this.#send(chatId, t('已关注会话「{title}」,任务完成会推送结果。', { title: String(resolved.title).replace(/\s+/gu, ' ') }));
|
||||
await this.#queueEventTask(() => this.#compensateSession(resolved.sessionId));
|
||||
} catch (error) {
|
||||
await this.#send(chatId, `关注失败:${safeErrorText(error)}`);
|
||||
await this.#send(chatId, t('关注失败:{message}', { message: safeErrorText(error) }));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1487,15 +1495,15 @@ export class FeishuHarnessBridge {
|
|||
? entries[Number(target) - 1]
|
||||
: entries.find((candidate) => candidate.sessionId === target);
|
||||
if (!entry) {
|
||||
await this.#send(chatId, '关注列表里没有这个会话,回复 /watchlist 查看。');
|
||||
await this.#send(chatId, t('关注列表里没有这个会话,回复 /watchlist 查看。'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.#state.removeWatch(key, entry.sessionId);
|
||||
this.#failedWatchSeqs.delete(`${key}\0${entry.sessionId}`);
|
||||
await this.#send(chatId, `已取消关注「${String(entry.title ?? '').replace(/\s+/gu, ' ')}」。`);
|
||||
await this.#send(chatId, t('已取消关注「{title}」。', { title: String(entry.title ?? '').replace(/\s+/gu, ' ') }));
|
||||
} catch (error) {
|
||||
await this.#send(chatId, `取消失败:${safeErrorText(error)}`);
|
||||
await this.#send(chatId, t('取消失败:{message}', { message: safeErrorText(error) }));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1877,18 +1885,18 @@ export class FeishuHarnessBridge {
|
|||
|
||||
const text = extractText(event);
|
||||
if (!text) {
|
||||
await this.#send(event.message.chat_id, '请用文字回答当前问题。');
|
||||
await this.#send(event.message.chat_id, t('请用文字回答当前问题。'));
|
||||
return;
|
||||
}
|
||||
|
||||
const pending = this.#pendingInteractions.get(key);
|
||||
if (!pending || pending !== expected || pending.submitting) {
|
||||
if (this.#isResolvedQuestionReply(event, key)) {
|
||||
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT).catch(() => undefined);
|
||||
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT()).catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
if (claimed && (!pending || pending !== expected)) {
|
||||
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT);
|
||||
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT());
|
||||
return;
|
||||
}
|
||||
return this.#enqueueMessage(event, messageId, key, processingReaction, {
|
||||
|
|
@ -1946,7 +1954,7 @@ export class FeishuHarnessBridge {
|
|||
if (error?.code === 'interaction-not-pending') {
|
||||
this.#rememberResolvedInteraction(key, pending);
|
||||
this.#clearPendingInteraction(key, pending.interactionId);
|
||||
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT).catch(() => undefined);
|
||||
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT()).catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
pending.submitting = false;
|
||||
|
|
@ -1954,7 +1962,7 @@ export class FeishuHarnessBridge {
|
|||
pending.index -= 1;
|
||||
this.#status.lastError = '回答提交失败。';
|
||||
this.#logger.error?.('[dsh-feishu] failed to answer a Harness interaction');
|
||||
await this.#send(event.message.chat_id, '回答提交失败,请重新发送当前问题的答案。')
|
||||
await this.#send(event.message.chat_id, t('回答提交失败,请重新发送当前问题的答案。'))
|
||||
.catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
|
@ -1999,7 +2007,7 @@ export class FeishuHarnessBridge {
|
|||
});
|
||||
await this.#send(
|
||||
chatId,
|
||||
'检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
|
||||
t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'),
|
||||
).catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
|
|
@ -2101,7 +2109,7 @@ export class FeishuHarnessBridge {
|
|||
await this.#state.markSeen(messageId);
|
||||
this.#status.lastMessageAt = new Date().toISOString();
|
||||
this.#status.messagesReceived += 1;
|
||||
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT).catch(() => undefined);
|
||||
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT()).catch(() => undefined);
|
||||
}
|
||||
|
||||
#takePendingInteraction(key, interactionId) {
|
||||
|
|
@ -2141,10 +2149,10 @@ export class FeishuHarnessBridge {
|
|||
#progressText(update) {
|
||||
if (update.type === 'text' && update.text) return update.text;
|
||||
if (update.type === 'tool') {
|
||||
if (update.name === 'web_search') return '_正在搜索网络并整理信息…_';
|
||||
return `_正在使用 ${update.name || '工具'}…_`;
|
||||
if (update.name === 'web_search') return t('_正在搜索网络并整理信息…_');
|
||||
return t('_正在使用 {name}…_', { name: update.name || t('工具') });
|
||||
}
|
||||
return `_${update.text || '正在处理…'}_`;
|
||||
return t('_{text}_', { text: update.text || t('正在处理…') });
|
||||
}
|
||||
|
||||
async #addReaction(messageId, emojiType) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
* without button callbacks.
|
||||
*/
|
||||
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
export const MENU_PAGE_SIZE = 10;
|
||||
|
||||
function plainText(content) {
|
||||
|
|
@ -56,7 +58,7 @@ function buttonElement(content, actionValue) {
|
|||
|
||||
function safeTitle(value) {
|
||||
const title = String(value ?? '').replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+/gu, ' ').replace(/\s+/gu, ' ').trim();
|
||||
return title || '暂无标题';
|
||||
return title || t('暂无标题');
|
||||
}
|
||||
|
||||
function cardWith(headerText, elements) {
|
||||
|
|
@ -69,18 +71,18 @@ function cardWith(headerText, elements) {
|
|||
|
||||
/** The main command menu (buttons + number-reply fallback). */
|
||||
export function menuCard() {
|
||||
return cardWith('🤖 助手菜单', [
|
||||
{ tag: 'div', text: markdown('**点击按钮或直接回复数字**') },
|
||||
button('1 · 会话列表', 'sessions'),
|
||||
button('2 · 工作区', 'workspaces'),
|
||||
button('3 · 新会话', 'new'),
|
||||
button('4 · 状态', 'status'),
|
||||
button('5 · 帮助', 'help'),
|
||||
return cardWith(t('🤖 助手菜单'), [
|
||||
{ tag: 'div', text: markdown(t('**点击按钮或直接回复数字**')) },
|
||||
button(t('1 · 会话列表'), 'sessions'),
|
||||
button(t('2 · 工作区'), 'workspaces'),
|
||||
button(t('3 · 新会话'), 'new'),
|
||||
button(t('4 · 状态'), 'status'),
|
||||
button(t('5 · 帮助'), 'help'),
|
||||
// Repair must remain number-driven. Apps that need this command do not
|
||||
// have card.action.trigger yet, so rendering it as a callback button would
|
||||
// send the user straight back to Feishu's broken callback setup popup.
|
||||
{ tag: 'div', text: markdown('**6 · 修复卡片按钮**(请直接回复数字 **6**)') },
|
||||
button('7 · 关注列表', 'watchlist'),
|
||||
{ tag: 'div', text: markdown(t('**6 · 修复卡片按钮**(请直接回复数字 **6**)')) },
|
||||
button(t('7 · 关注列表'), 'watchlist'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -89,10 +91,10 @@ export function cardActionProbeCard(nonce) {
|
|||
if (typeof nonce !== 'string' || !/^[A-Za-z0-9_-]{16,128}$/.test(nonce)) {
|
||||
throw new TypeError('A safe card-action probe nonce is required');
|
||||
}
|
||||
return cardWith('🧪 验证卡片按钮', [
|
||||
return cardWith(t('🧪 验证卡片按钮'), [
|
||||
{
|
||||
tag: 'div',
|
||||
text: markdown('授权已提交。请点击下方按钮;机器人真实收到回调后才会判定修复成功。'),
|
||||
text: markdown(t('授权已提交。请点击下方按钮;机器人真实收到回调后才会判定修复成功。')),
|
||||
},
|
||||
{
|
||||
tag: 'column_set',
|
||||
|
|
@ -103,7 +105,7 @@ export function cardActionProbeCard(nonce) {
|
|||
weight: 1,
|
||||
elements: [{
|
||||
tag: 'button',
|
||||
text: plainText('完成验证'),
|
||||
text: plainText(t('完成验证')),
|
||||
type: 'primary',
|
||||
width: 'fill',
|
||||
behaviors: [{
|
||||
|
|
@ -140,74 +142,80 @@ export function sessionListCard(workspace, sessions, page, total, watchedSession
|
|||
],
|
||||
});
|
||||
const elements = [
|
||||
{ tag: 'div', text: markdown(`**工作区**:\`${workspace}\`\n共 **${total}** 个会话${total > MENU_PAGE_SIZE ? `(第 ${page + 1}/${pageCount} 页)` : ''}`) },
|
||||
{ tag: 'div', text: markdown(t('**工作区**:{workspace}\n共 **{total}** 个会话{paging}', {
|
||||
workspace: `\`${workspace}\``,
|
||||
total,
|
||||
paging: total > MENU_PAGE_SIZE
|
||||
? t('(第 {page}/{pageCount} 页)', { page: page + 1, pageCount })
|
||||
: '',
|
||||
})) },
|
||||
...slice.map((session, offset) => {
|
||||
// Page-local numbering: number replies resolve against this page.
|
||||
const label = `${offset + 1}. ${safeTitle(session.title)}${session.archived === true ? '(已归档)' : ''}`;
|
||||
const label = `${offset + 1}. ${safeTitle(session.title)}${session.archived === true ? t('(已归档)') : ''}`;
|
||||
const watching = watched(session.sessionId);
|
||||
return row(
|
||||
buttonElement(watching ? '⭐取关' : '⭐关注', watching ? `unwatch:${session.sessionId}` : `watch:${session.sessionId}`),
|
||||
buttonElement(watching ? t('⭐取关') : t('⭐关注'), watching ? `unwatch:${session.sessionId}` : `watch:${session.sessionId}`),
|
||||
buttonElement(label, `use:${session.sessionId}`),
|
||||
);
|
||||
}),
|
||||
];
|
||||
if (page > 0) elements.push(button('◀ 上一页', `sessions:${page - 1}`));
|
||||
if (page + 1 < pageCount) elements.push(button('下一页 ▶', `sessions:${page + 1}`));
|
||||
elements.push({ tag: 'div', text: markdown('回复数字(1~N)绑定本页会话。') });
|
||||
return cardWith('📂 会话列表', elements);
|
||||
if (page > 0) elements.push(button(t('◀ 上一页'), `sessions:${page - 1}`));
|
||||
if (page + 1 < pageCount) elements.push(button(t('下一页 ▶'), `sessions:${page + 1}`));
|
||||
elements.push({ tag: 'div', text: markdown(t('回复数字(1~N)绑定本页会话。')) });
|
||||
return cardWith(t('📂 会话列表'), elements);
|
||||
}
|
||||
|
||||
/** The workspace list card (switch-workspace buttons + reply fallback). */
|
||||
export function workspaceListCard(paths, current) {
|
||||
const elements = paths.length === 0
|
||||
? [{ tag: 'div', text: markdown('当前 Host 上没有已登记的工作区。') }]
|
||||
? [{ tag: 'div', text: markdown(t('当前 Host 上没有已登记的工作区。')) }]
|
||||
: [
|
||||
{ tag: 'div', text: markdown(`回复数字切换工作区,或点击按钮:`) },
|
||||
{ tag: 'div', text: markdown(t('回复数字切换工作区,或点击按钮:')) },
|
||||
...paths.map((path, index) => button(
|
||||
`${index + 1}. ${path}${path === current ? '(当前)' : ''}`,
|
||||
`${index + 1}. ${path}${path === current ? t('(当前)') : ''}`,
|
||||
`workspace:${path}`,
|
||||
)),
|
||||
];
|
||||
return cardWith('🗂 工作区', elements);
|
||||
return cardWith(t('🗂 工作区'), elements);
|
||||
}
|
||||
|
||||
/** The card-menu help text (number-driven, no command memorization). */
|
||||
export function menuHelpText() {
|
||||
return [
|
||||
'🤖 助手菜单(回复数字即可,无需记命令)',
|
||||
t('🤖 助手菜单(回复数字即可,无需记命令)'),
|
||||
'',
|
||||
'1 · /sessionlist 列出会话(回复数字绑定)',
|
||||
'2 · /workspacelist 列出工作区(回复数字切换)',
|
||||
'3 · /new 开启新会话',
|
||||
'4 · /status 连接状态',
|
||||
'5 · /help 本帮助',
|
||||
'6 · /repair 修复卡片按钮(请回复数字 6)',
|
||||
'7 · /watchlist 关注列表',
|
||||
t('1 · /sessionlist 列出会话(回复数字绑定)'),
|
||||
t('2 · /workspacelist 列出工作区(回复数字切换)'),
|
||||
t('3 · /new 开启新会话'),
|
||||
t('4 · /status 连接状态'),
|
||||
t('5 · /help 本帮助'),
|
||||
t('6 · /repair 修复卡片按钮(请回复数字 6)'),
|
||||
t('7 · /watchlist 关注列表'),
|
||||
'',
|
||||
'直接发送文字/图片即继续当前会话。',
|
||||
'/session ID 或序号 绑定已有会话',
|
||||
'/watch ID 或序号 关注会话(完成后推送)',
|
||||
'/compact 压缩上下文',
|
||||
'/workspace 绝对路径 切换工作区',
|
||||
'/presetlist 列出可用 Agent Preset',
|
||||
'/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
|
||||
'纯数字 ID:/preset id:<ID>',
|
||||
'/preset --default 跟随 Host 默认',
|
||||
t('直接发送文字/图片即继续当前会话。'),
|
||||
t('/session ID 或序号 绑定已有会话'),
|
||||
t('/watch ID 或序号 关注会话(完成后推送)'),
|
||||
t('/compact 压缩上下文'),
|
||||
t('/workspace 绝对路径 切换工作区'),
|
||||
t('/presetlist 列出可用 Agent Preset'),
|
||||
t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
|
||||
t('纯数字 ID:/preset id:<ID>'),
|
||||
t('/preset --default 跟随 Host 默认'),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
/** The watch list for one conversation (unwatch buttons + reply fallback). */
|
||||
export function watchListCard(entries) {
|
||||
const elements = entries.length === 0
|
||||
? [{ tag: 'div', text: markdown('当前没有关注的会话。\n`/watch <ID|序号>` 关注后,任务完成会自动推送。') }]
|
||||
? [{ tag: 'div', text: markdown(t('当前没有关注的会话。\n`/watch <ID|序号>` 关注后,任务完成会自动推送。')) }]
|
||||
: [
|
||||
{ tag: 'div', text: markdown('任务完成会自动推送,回复数字或点按钮取消关注:') },
|
||||
{ tag: 'div', text: markdown(t('任务完成会自动推送,回复数字或点按钮取消关注:')) },
|
||||
...entries.map((entry, index) => button(
|
||||
`${index + 1}. ${safeTitle(entry.title)}`,
|
||||
`unwatch:${entry.sessionId}`,
|
||||
)),
|
||||
];
|
||||
return cardWith('👁 关注列表', elements);
|
||||
return cardWith(t('👁 关注列表'), elements);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -216,19 +224,19 @@ export function watchListCard(entries) {
|
|||
*/
|
||||
export function completionCard(sessionId, title, reason) {
|
||||
const reasonText = reason === 'completed'
|
||||
? '已完成'
|
||||
? t('已完成')
|
||||
: reason === 'stopped'
|
||||
? '已停止'
|
||||
? t('已停止')
|
||||
: reason === 'aborted'
|
||||
? '已中止'
|
||||
? t('已中止')
|
||||
: reason === 'cancelled'
|
||||
? '已取消'
|
||||
: '已结束';
|
||||
return cardWith('✅ 任务完成', [
|
||||
? t('已取消')
|
||||
: t('已结束');
|
||||
return cardWith(t('✅ 任务完成'), [
|
||||
{ tag: 'div', text: markdown(`**${safeTitle(title)}**\n\`${sessionId}\``) },
|
||||
{ tag: 'div', text: markdown(`**状态**:${reasonText}`) },
|
||||
button('打开会话列表', 'sessions'),
|
||||
button('工作区', 'workspaces'),
|
||||
{ tag: 'div', text: markdown('绑定该会话后可继续追问,输入文字即可。') },
|
||||
{ tag: 'div', text: markdown(t('**状态**:{reason}', { reason: reasonText })) },
|
||||
button(t('打开会话列表'), 'sessions'),
|
||||
button(t('工作区'), 'workspaces'),
|
||||
{ tag: 'div', text: markdown(t('绑定该会话后可继续追问,输入文字即可。')) },
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
|
|||
|
||||
import { trackOutboundArtifactProviderPromise } from '../shared/semantic/artifact.mjs';
|
||||
import { createDeliveryReceipt } from '../shared/semantic/delivery.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const STREAM_ELEMENT_ID = 'stream_md';
|
||||
const DEFAULT_INITIAL_TEXT = '已连接 DeepSeek Harness,正在思考…';
|
||||
|
|
@ -133,7 +134,7 @@ function streamingCard(initialText) {
|
|||
schema: '2.0',
|
||||
config: {
|
||||
streaming_mode: true,
|
||||
summary: { content: '正在生成…' },
|
||||
summary: { content: t('正在生成…') },
|
||||
streaming_config: {
|
||||
print_frequency_ms: { default: 70 },
|
||||
print_step: { default: 1 },
|
||||
|
|
@ -158,12 +159,12 @@ export class VerifiedFeishuChannel {
|
|||
|
||||
constructor({
|
||||
client,
|
||||
initialText = DEFAULT_INITIAL_TEXT,
|
||||
initialText,
|
||||
fileUploadTimeoutMs = MAX_FILE_OPERATION_TIMEOUT_MS,
|
||||
fileMessageTimeoutMs = MAX_FILE_OPERATION_TIMEOUT_MS,
|
||||
}) {
|
||||
this.#client = client;
|
||||
this.#initialText = initialText;
|
||||
this.#initialText = initialText ?? t(DEFAULT_INITIAL_TEXT);
|
||||
this.#fileUploadTimeoutMs = boundedFileTimeout(fileUploadTimeoutMs, 'fileUploadTimeoutMs');
|
||||
this.#fileMessageTimeoutMs = boundedFileTimeout(fileMessageTimeoutMs, 'fileMessageTimeoutMs');
|
||||
}
|
||||
|
|
@ -215,7 +216,7 @@ export class VerifiedFeishuChannel {
|
|||
settings: JSON.stringify({
|
||||
config: {
|
||||
streaming_mode: false,
|
||||
summary: { content: summaryOf(lastContent) || '回答完成' },
|
||||
summary: { content: summaryOf(lastContent) || t('回答完成') },
|
||||
},
|
||||
}),
|
||||
sequence: ++sequence,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
connectionTestTargetUnavailable,
|
||||
sendRememberedConnectionTest,
|
||||
} from '../shared/connection-test.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
||||
const CALLBACK_PROBE_SUCCESS_NOTICE = '✅ 修复完成:已实测收到 card.action.trigger,菜单按钮现在可用。';
|
||||
|
|
@ -206,7 +207,7 @@ export class FeishuRuntime {
|
|||
this.#client = new this.#lark.Client(larkConfig);
|
||||
const channel = new VerifiedFeishuChannel({
|
||||
client: this.#client,
|
||||
initialText: '已连接 DeepSeek Harness,正在思考…',
|
||||
initialText: t('已连接 DeepSeek Harness,正在思考…'),
|
||||
});
|
||||
this.#bridge = new FeishuHarnessBridge({
|
||||
client: this.#client,
|
||||
|
|
@ -340,7 +341,7 @@ export class FeishuRuntime {
|
|||
} catch {
|
||||
void this.#sendCardActionProbeNotice(
|
||||
operatorOpenId,
|
||||
CALLBACK_PROBE_SEND_FAILURE_NOTICE,
|
||||
t(CALLBACK_PROBE_SEND_FAILURE_NOTICE),
|
||||
'failure',
|
||||
);
|
||||
throw probeError('card_action_probe_send_failed', '无法发送飞书卡片回调测试');
|
||||
|
|
@ -348,7 +349,7 @@ export class FeishuRuntime {
|
|||
if (response?.code && response.code !== 0) {
|
||||
void this.#sendCardActionProbeNotice(
|
||||
operatorOpenId,
|
||||
CALLBACK_PROBE_SEND_FAILURE_NOTICE,
|
||||
t(CALLBACK_PROBE_SEND_FAILURE_NOTICE),
|
||||
'failure',
|
||||
);
|
||||
throw probeError('card_action_probe_send_failed', '无法发送飞书卡片回调测试');
|
||||
|
|
@ -358,7 +359,7 @@ export class FeishuRuntime {
|
|||
if (!messageId) {
|
||||
void this.#sendCardActionProbeNotice(
|
||||
operatorOpenId,
|
||||
CALLBACK_PROBE_SEND_FAILURE_NOTICE,
|
||||
t(CALLBACK_PROBE_SEND_FAILURE_NOTICE),
|
||||
'failure',
|
||||
);
|
||||
throw probeError('card_action_probe_send_failed', '飞书未返回测试卡片的消息 ID');
|
||||
|
|
@ -371,7 +372,7 @@ export class FeishuRuntime {
|
|||
this.#pendingCardActionProbes.delete(messageId);
|
||||
void this.#sendCardActionProbeNotice(
|
||||
operatorOpenId,
|
||||
CALLBACK_PROBE_TIMEOUT_NOTICE,
|
||||
t(CALLBACK_PROBE_TIMEOUT_NOTICE),
|
||||
'timeout',
|
||||
);
|
||||
reject(probeError(
|
||||
|
|
@ -413,7 +414,7 @@ export class FeishuRuntime {
|
|||
// the callback proof itself.
|
||||
void this.#sendCardActionProbeNotice(
|
||||
operatorOpenId,
|
||||
CALLBACK_PROBE_SUCCESS_NOTICE,
|
||||
t(CALLBACK_PROBE_SUCCESS_NOTICE),
|
||||
'success',
|
||||
).finally(() => {
|
||||
probe.resolve({
|
||||
|
|
@ -482,10 +483,10 @@ export class FeishuRuntime {
|
|||
return sendRememberedConnectionTest({
|
||||
state: this.#state,
|
||||
text,
|
||||
channelLabel: '飞书机器人',
|
||||
channelLabel: t('飞书机器人'),
|
||||
send: async (target, content) => {
|
||||
const chatId = typeof target?.chatId === 'string' ? target.chatId.trim() : '';
|
||||
if (!chatId) throw connectionTestTargetUnavailable('飞书机器人');
|
||||
if (!chatId) throw connectionTestTargetUnavailable(t('飞书机器人'));
|
||||
await send('chat_id', chatId, content);
|
||||
},
|
||||
});
|
||||
|
|
@ -500,7 +501,7 @@ export class FeishuRuntime {
|
|||
clearTimeout(probe.timeout);
|
||||
void this.#sendCardActionProbeNotice(
|
||||
probe.expectedOperatorOpenId,
|
||||
CALLBACK_PROBE_ABORT_NOTICE,
|
||||
t(CALLBACK_PROBE_ABORT_NOTICE),
|
||||
'abort',
|
||||
);
|
||||
probe.reject(probeError('abort', '飞书运行时已停止'));
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { ImagePromptError } from '../shared/image-prompt.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const FEISHU_MISSING_MESSAGE_SCOPE_CODE = 99991672;
|
||||
const FEISHU_ERROR_BODY_LIMIT = 64 * 1024;
|
||||
|
|
@ -113,7 +114,7 @@ async function readBoundedStream(stream, { signal, maxBytes }) {
|
|||
throw new ImagePromptError(
|
||||
'image-too-large',
|
||||
`Feishu image exceeds ${maxBytes} bytes`,
|
||||
'图片超过 5 MB,请压缩后重试。',
|
||||
t('图片超过 5 MB,请压缩后重试。'),
|
||||
);
|
||||
}
|
||||
chunks.push(data);
|
||||
|
|
@ -221,7 +222,7 @@ async function feishuImageDownloadError(error, signal) {
|
|||
return new ImagePromptError(
|
||||
'feishu-image-permission-required',
|
||||
'Feishu image download requires the im:message:readonly tenant scope',
|
||||
FEISHU_IMAGE_PERMISSION_MESSAGE,
|
||||
t(FEISHU_IMAGE_PERMISSION_MESSAGE),
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
|
|
@ -249,7 +250,7 @@ function feishuImageSource(event, client, key) {
|
|||
throw new ImagePromptError(
|
||||
'image-too-large',
|
||||
`Feishu image declares ${size} bytes; the limit is ${maxBytes}`,
|
||||
'图片超过 5 MB,请压缩后重试。',
|
||||
t('图片超过 5 MB,请压缩后重试。'),
|
||||
);
|
||||
}
|
||||
return readBoundedStream(resource?.getReadableStream?.(), { signal, maxBytes });
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 deriveQqBotIdentity(appId) {
|
|||
|
||||
export function maskQqAppId(appId) {
|
||||
const value = cleanString(appId) ?? '';
|
||||
if (value.length <= 10) return value ? `${value.slice(0, 3)}•••` : 'QQ机器人';
|
||||
if (value.length <= 10) return value ? `${value.slice(0, 3)}•••` : t('QQ机器人');
|
||||
return `${value.slice(0, 6)}••••${value.slice(-4)}`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
// QQ markdown 回复投递:长文尽量按结构边界切分,以 msg_type=2 发送,
|
||||
// 平台拒绝 markdown 时逐条回退纯文本。
|
||||
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const DEFAULT_CHUNK_LIMIT = 4_500;
|
||||
const CODE_FENCE_OPEN = /^```/;
|
||||
const GFM_TABLE_LINE = /^\|.+\|$/;
|
||||
const PASSIVE_REPLY_LIMIT = Object.freeze({ c2c: 4, group: 5 });
|
||||
const PARTIAL_REPLY_NOTICE = '回答较长,后续内容未能通过 QQ 完整发送,请回复“继续”。';
|
||||
const PARTIAL_REPLY_NOTICE = () => t('回答较长,后续内容未能通过 QQ 完整发送,请回复“继续”。');
|
||||
|
||||
function safeSliceIndex(value, limit) {
|
||||
let index = Math.min(limit, value.length);
|
||||
|
|
@ -141,7 +143,7 @@ export async function sendMarkdownReply(bot, target, text, { logger } = {}) {
|
|||
if (partialNoticeSent || !target?.msgId) return;
|
||||
partialNoticeSent = true;
|
||||
try {
|
||||
results.push(await bot.sendText(target, PARTIAL_REPLY_NOTICE));
|
||||
results.push(await bot.sendText(target, PARTIAL_REPLY_NOTICE()));
|
||||
} catch (error) {
|
||||
logger?.warn?.('[dsh-im:qq] unable to send partial reply notice:', error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,8 +40,11 @@ import {
|
|||
providerMessageIdsFor,
|
||||
} from '../shared/semantic/delivery.mjs';
|
||||
import { sendMarkdownReply } from './markdown-reply.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
||||
function interactionResolvedText() {
|
||||
return t('这个问题已在其他客户端处理,无需再次回答。');
|
||||
}
|
||||
const DEFAULT_FILE_UPLOAD_TIMEOUT_MS = 120_000;
|
||||
|
||||
export const QQ_IMAGE_HOSTS = Object.freeze([
|
||||
|
|
@ -55,28 +58,30 @@ export const QQ_IMAGE_HOSTS = Object.freeze([
|
|||
|
||||
const QQ_IMAGE_FILENAME = /\.(?:gif|jpe?g|png|webp)$/i;
|
||||
|
||||
const HELP_TEXT = [
|
||||
'QQ 机器人已连接 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');
|
||||
function helpText() {
|
||||
return [
|
||||
t('QQ 机器人已连接 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');
|
||||
}
|
||||
|
||||
function conversationKey(message) {
|
||||
return `${message.kind}:${message.kind === 'group' ? message.groupOpenid : message.senderId}`;
|
||||
|
|
@ -167,35 +172,35 @@ function nonEmptyString(value) {
|
|||
}
|
||||
|
||||
function artifactFailureText(fileName, error) {
|
||||
const name = String(fileName ?? '结果文件').replace(/[\r\n]+/g, ' ').trim() || '结果文件';
|
||||
const name = String(fileName ?? t('结果文件')).replace(/[\r\n]+/g, ' ').trim() || t('结果文件');
|
||||
if (error?.name === 'UploadDailyLimitExceededError') {
|
||||
return `结果文件「${name}」已生成,但 QQ 今日文件上传额度已用完,请稍后重试。`;
|
||||
return t('结果文件「{name}」已生成,但 QQ 今日文件上传额度已用完,请稍后重试。', { name });
|
||||
}
|
||||
switch (error?.code) {
|
||||
case 'artifact-delivery-uncertain':
|
||||
return `结果文件「${name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。`;
|
||||
return t('结果文件「{name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
|
||||
case 'artifact-permission-required':
|
||||
return `结果文件「${name}」已生成,但当前 QQ 机器人没有文件消息权限。`;
|
||||
return t('结果文件「{name}」已生成,但当前 QQ 机器人没有文件消息权限。', { name });
|
||||
case 'artifact-too-large':
|
||||
return `结果文件「${name}」超过当前 QQ 机器人可发送的文件大小,未发送。`;
|
||||
return t('结果文件「{name}」超过当前 QQ 机器人可发送的文件大小,未发送。', { name });
|
||||
case 'artifact-empty':
|
||||
return `结果文件「${name}」为空,QQ 不允许发送空文件。`;
|
||||
return t('结果文件「{name}」为空,QQ 不允许发送空文件。', { name });
|
||||
case 'artifact-changed':
|
||||
case 'artifact-invalid':
|
||||
case 'artifact-unavailable':
|
||||
return `结果文件「${name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。`;
|
||||
return t('结果文件「{name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。', { name });
|
||||
case 'artifact-rate-limited':
|
||||
return `结果文件「${name}」暂时被 QQ 限流,未能发送,请稍后重试。`;
|
||||
return t('结果文件「{name}」暂时被 QQ 限流,未能发送,请稍后重试。', { name });
|
||||
case 'artifact-provider-rejected':
|
||||
return `结果文件「${name}」已生成,但 QQ 拒绝了该文件或文件消息。`;
|
||||
return t('结果文件「{name}」已生成,但 QQ 拒绝了该文件或文件消息。', { name });
|
||||
default:
|
||||
return `结果文件「${name}」已生成,但暂时未能通过 QQ 发送,请稍后重试。`;
|
||||
return t('结果文件「{name}」已生成,但暂时未能通过 QQ 发送,请稍后重试。', { name });
|
||||
}
|
||||
}
|
||||
|
||||
function answerTextForDelivery(answer, artifacts) {
|
||||
if (typeof answer === 'string' && answer.trim()) return answer;
|
||||
return artifacts.length > 0 ? '结果文件已生成。' : answer;
|
||||
return artifacts.length > 0 ? t('结果文件已生成。') : answer;
|
||||
}
|
||||
|
||||
function qqArtifactError(error, { dispatched = false } = {}) {
|
||||
|
|
@ -420,7 +425,7 @@ export class QqHarnessBridge {
|
|||
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
|
||||
this.#status.lastError = error?.message ?? String(error);
|
||||
this.#logger.error?.('[dsh-im:qq] failed to process a command:', error);
|
||||
return this.#bot.sendText(message.replyTarget, '消息处理失败,请稍后重试。')
|
||||
return this.#bot.sendText(message.replyTarget, t('消息处理失败,请稍后重试。'))
|
||||
.catch(() => undefined);
|
||||
}).finally(() => {
|
||||
this.#acceptedMessageIds.delete(messageId);
|
||||
|
|
@ -609,25 +614,25 @@ export class QqHarnessBridge {
|
|||
let stream = null;
|
||||
try {
|
||||
if (!text && !hasImages && !hasFiles) {
|
||||
await this.#bot.sendText(target, '目前支持文字、图片和文件消息。');
|
||||
await this.#bot.sendText(target, t('目前支持文字、图片和文件消息。'));
|
||||
await this.#state.markSeen(messageId);
|
||||
return;
|
||||
}
|
||||
const command = text.toLowerCase();
|
||||
if (!hasImages && !hasFiles && command === '/help') {
|
||||
await this.#bot.sendText(target, HELP_TEXT);
|
||||
await this.#bot.sendText(target, helpText());
|
||||
await this.#state.markSeen(messageId);
|
||||
return;
|
||||
}
|
||||
if (!hasImages && !hasFiles && command === '/status') {
|
||||
await this.#harness.ensureRunning({ signal: this.#signal });
|
||||
await this.#bot.sendText(target, 'QQ 机器人与 DeepSeek Harness 连接正常。');
|
||||
await this.#bot.sendText(target, t('QQ 机器人与 DeepSeek Harness 连接正常。'));
|
||||
await this.#state.markSeen(messageId);
|
||||
return;
|
||||
}
|
||||
if (!hasImages && !hasFiles && command === '/new') {
|
||||
await this.#state.clearSession(key);
|
||||
await this.#bot.sendText(target, '已开启新会话。请发送你的问题。');
|
||||
await this.#bot.sendText(target, t('已开启新会话。请发送你的问题。'));
|
||||
await this.#state.markSeen(messageId);
|
||||
return;
|
||||
}
|
||||
|
|
@ -771,7 +776,7 @@ export class QqHarnessBridge {
|
|||
this.#logger.warn?.('[dsh-im:qq] unable to cancel a stopped QQ stream:', streamError);
|
||||
}
|
||||
try {
|
||||
await this.#bot.sendText(target, '已停止。');
|
||||
await this.#bot.sendText(target, t('已停止。'));
|
||||
} catch (sendError) {
|
||||
this.#logger.warn?.('[dsh-im:qq] unable to announce a stopped QQ turn:', sendError);
|
||||
}
|
||||
|
|
@ -791,7 +796,7 @@ export class QqHarnessBridge {
|
|||
target,
|
||||
inboundFileUserMessage(error)
|
||||
?? imagePromptUserMessage(error)
|
||||
?? '消息处理失败,请稍后重试。',
|
||||
?? t('消息处理失败,请稍后重试。'),
|
||||
);
|
||||
await this.#state.markSeen(messageId);
|
||||
} catch (sendError) {
|
||||
|
|
@ -818,14 +823,14 @@ export class QqHarnessBridge {
|
|||
if (message.kind === 'group' && message.rawEventType !== 'GROUP_AT_MESSAGE_CREATE') return;
|
||||
const text = nonEmptyString(safeText(message));
|
||||
if (!text || hasQqImageAttachments(message) || hasQqFileAttachments(message)) {
|
||||
await this.#bot.sendText(message.replyTarget, '请用文字回答当前问题。');
|
||||
await this.#bot.sendText(message.replyTarget, t('请用文字回答当前问题。'));
|
||||
return;
|
||||
}
|
||||
|
||||
const pending = this.#pendingInteractions.get(key);
|
||||
if (!pending || pending !== expected || pending.submitting) {
|
||||
if (claimed && (!pending || pending !== expected)) {
|
||||
await this.#bot.sendText(message.replyTarget, INTERACTION_RESOLVED_TEXT);
|
||||
await this.#bot.sendText(message.replyTarget, interactionResolvedText());
|
||||
return;
|
||||
}
|
||||
return this.#enqueueMessage(message, messageId, key, {
|
||||
|
|
@ -838,7 +843,7 @@ export class QqHarnessBridge {
|
|||
try {
|
||||
await this.#presentInteraction(pending);
|
||||
} catch {
|
||||
this.#status.lastError = 'QQ 交互问题发送失败。';
|
||||
this.#status.lastError = t('QQ 交互问题发送失败。');
|
||||
this.#logger.error?.('[dsh-im:qq] failed to retry an interaction question');
|
||||
pending.interaction.reconnect?.();
|
||||
return;
|
||||
|
|
@ -846,7 +851,7 @@ export class QqHarnessBridge {
|
|||
const presentedPending = this.#pendingInteractions.get(key);
|
||||
if (!presentedPending || presentedPending !== expected || presentedPending.submitting) {
|
||||
if (claimed && (!presentedPending || presentedPending !== expected)) {
|
||||
await this.#bot.sendText(message.replyTarget, INTERACTION_RESOLVED_TEXT)
|
||||
await this.#bot.sendText(message.replyTarget, interactionResolvedText())
|
||||
.catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
|
|
@ -869,7 +874,7 @@ export class QqHarnessBridge {
|
|||
try {
|
||||
await this.#presentInteraction(pending);
|
||||
} catch {
|
||||
this.#status.lastError = 'QQ 交互问题发送失败。';
|
||||
this.#status.lastError = t('QQ 交互问题发送失败。');
|
||||
this.#logger.error?.('[dsh-im:qq] failed to send the next interaction question');
|
||||
pending.interaction.reconnect?.();
|
||||
}
|
||||
|
|
@ -891,16 +896,16 @@ export class QqHarnessBridge {
|
|||
if (this.#signal?.aborted) return;
|
||||
if (error?.code === 'interaction-not-pending') {
|
||||
this.#clearPendingInteraction(key, pending.interactionId);
|
||||
await this.#bot.sendText(pending.target, INTERACTION_RESOLVED_TEXT).catch(() => undefined);
|
||||
await this.#bot.sendText(pending.target, interactionResolvedText()).catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
if (this.#pendingInteractions.get(key) !== pending) return;
|
||||
pending.submitting = false;
|
||||
pending.answers.pop();
|
||||
pending.index -= 1;
|
||||
this.#status.lastError = '回答提交失败。';
|
||||
this.#status.lastError = t('回答提交失败。');
|
||||
this.#logger.error?.('[dsh-im:qq] failed to answer a Harness interaction');
|
||||
await this.#bot.sendText(pending.target, '回答提交失败,请重新发送当前问题的答案。')
|
||||
await this.#bot.sendText(pending.target, t('回答提交失败,请重新发送当前问题的答案。'))
|
||||
.catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
|
@ -945,7 +950,7 @@ export class QqHarnessBridge {
|
|||
});
|
||||
await this.#bot.sendText(
|
||||
target,
|
||||
'检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
|
||||
t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'),
|
||||
).catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
|
|
@ -1030,7 +1035,7 @@ export class QqHarnessBridge {
|
|||
await this.#state.markSeen(messageId);
|
||||
this.#status.messagesReceived += 1;
|
||||
this.#status.lastMessageAt = new Date().toISOString();
|
||||
await this.#bot.sendText(message.replyTarget, INTERACTION_RESOLVED_TEXT).catch(() => undefined);
|
||||
await this.#bot.sendText(message.replyTarget, interactionResolvedText()).catch(() => undefined);
|
||||
}
|
||||
|
||||
#takePendingInteraction(key, interactionId) {
|
||||
|
|
@ -1072,7 +1077,7 @@ export class QqHarnessBridge {
|
|||
if (!this.#state.hasSeen(messageId)) {
|
||||
await this.#state.markSeen(messageId).catch(() => undefined);
|
||||
}
|
||||
await this.#bot.sendText(message.replyTarget, '消息处理失败,请稍后重试。')
|
||||
await this.#bot.sendText(message.replyTarget, t('消息处理失败,请稍后重试。'))
|
||||
.catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { connectionTestMessage } from '../shared/connection-test.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
import { deriveQqBotIdentity, maskQqAppId } from './config-store.mjs';
|
||||
|
||||
const ACTIVE_ATTEMPT_STATES = new Set(['starting', 'pending', 'refreshing', 'connecting']);
|
||||
|
|
@ -77,14 +78,14 @@ export class QqController {
|
|||
if (this.#closed || this.#runtimes.get(config.botId)?.status?.ready) return;
|
||||
const appSecret = await this.#resolveSecret(config.secretRef);
|
||||
if (!appSecret) {
|
||||
this.#errors.set(config.botId, safeError('missing-secret', 'QQ 机器人凭据缺失,请移除后重新扫码。'));
|
||||
this.#errors.set(config.botId, safeError('missing-secret', t('QQ 机器人凭据缺失,请移除后重新扫码。')));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.#startRuntime(config, appSecret);
|
||||
this.#errors.delete(config.botId);
|
||||
} catch (error) {
|
||||
this.#errors.set(config.botId, safeError('connection-failed', 'QQ 连接未就绪,插件会自动重试。'));
|
||||
this.#errors.set(config.botId, safeError('connection-failed', t('QQ 连接未就绪,插件会自动重试。')));
|
||||
this.#logger.warn?.(`[dsh-im:qq] bot ${config.botId} failed to initialize:`, error);
|
||||
} finally {
|
||||
this.#touch();
|
||||
|
|
@ -147,7 +148,7 @@ export class QqController {
|
|||
onFailure: (error) => {
|
||||
if (record.controller.signal.aborted || TERMINAL_ATTEMPT_STATES.has(record.state)) return;
|
||||
record.state = 'failed';
|
||||
record.error = safeError('qr-connect-failed', 'QQ 扫码服务暂时不可用,请重新生成二维码。');
|
||||
record.error = safeError('qr-connect-failed', t('QQ 扫码服务暂时不可用,请重新生成二维码。'));
|
||||
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
||||
this.#touch();
|
||||
firstQrReject(error);
|
||||
|
|
@ -158,10 +159,10 @@ export class QqController {
|
|||
} catch (error) {
|
||||
if (record.controller.signal.aborted) {
|
||||
record.state = 'cancelled';
|
||||
record.error = safeError('cancelled', '扫码绑定已取消。');
|
||||
record.error = safeError('cancelled', t('扫码绑定已取消。'));
|
||||
} else if (!TERMINAL_ATTEMPT_STATES.has(record.state)) {
|
||||
record.state = 'failed';
|
||||
record.error = safeError('qr-start-failed', '无法生成 QQ 二维码,请稍后重试。');
|
||||
record.error = safeError('qr-start-failed', t('无法生成 QQ 二维码,请稍后重试。'));
|
||||
}
|
||||
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
||||
this.#touch();
|
||||
|
|
@ -207,7 +208,7 @@ export class QqController {
|
|||
await this.#startRuntime(config, normalizedSecret);
|
||||
this.#errors.delete(identity.botId);
|
||||
} catch (error) {
|
||||
this.#errors.set(identity.botId, safeError('connection-failed', 'QQ 机器人已绑定,消息连接暂未就绪。'));
|
||||
this.#errors.set(identity.botId, safeError('connection-failed', t('QQ 机器人已绑定,消息连接暂未就绪。')));
|
||||
this.#logger.warn?.(`[dsh-im:qq] bot ${identity.botId} credential connection failed:`, error);
|
||||
}
|
||||
this.#touch();
|
||||
|
|
@ -223,7 +224,7 @@ export class QqController {
|
|||
record.dispose?.();
|
||||
await record.task?.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();
|
||||
|
|
@ -240,7 +241,7 @@ export class QqController {
|
|||
await this.#startRuntime(config, secret);
|
||||
this.#errors.delete(botId);
|
||||
} catch (error) {
|
||||
this.#errors.set(botId, safeError('connection-failed', 'QQ 连接仍未就绪,请稍后重试。'));
|
||||
this.#errors.set(botId, safeError('connection-failed', t('QQ 连接仍未就绪,请稍后重试。')));
|
||||
throw error;
|
||||
} finally {
|
||||
this.#touch();
|
||||
|
|
@ -255,12 +256,12 @@ export class QqController {
|
|||
return this.#withBotTransition(botId, async () => {
|
||||
const runtime = this.#runtimes.get(botId);
|
||||
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
||||
const error = new Error('QQ机器人尚未连接');
|
||||
const error = new Error(t('QQ机器人尚未连接'));
|
||||
error.code = 'test-target-unavailable';
|
||||
throw error;
|
||||
}
|
||||
await runtime.sendConnectionTest(connectionTestMessage(
|
||||
`QQ 机器人(${maskQqAppId(config.appId)})`,
|
||||
t('QQ 机器人({appId})', { appId: maskQqAppId(config.appId) }),
|
||||
));
|
||||
return { sent: true };
|
||||
});
|
||||
|
|
@ -306,11 +307,11 @@ export class QqController {
|
|||
state,
|
||||
connected,
|
||||
configured: true,
|
||||
bot: { name: 'QQ机器人', appIdMasked: maskQqAppId(config.appId) },
|
||||
bot: { name: t('QQ机器人'), appIdMasked: maskQqAppId(config.appId) },
|
||||
health: {
|
||||
status: connected ? 'healthy' : state === 'error' ? 'error' : 'offline',
|
||||
summary: connected ? 'QQ WebSocket 长连接运行正常'
|
||||
: state === 'error' ? 'QQ 连接未就绪,插件会自动重试' : 'QQ 连接当前离线',
|
||||
summary: connected ? t('QQ WebSocket 长连接运行正常')
|
||||
: state === 'error' ? t('QQ 连接未就绪,插件会自动重试') : t('QQ 连接当前离线'),
|
||||
lastCheckedAt: runtimeStatus?.lastCheckedAt ?? null,
|
||||
lastConnectedAt: runtimeStatus?.lastConnectedAt ?? null,
|
||||
},
|
||||
|
|
@ -364,10 +365,10 @@ export class QqController {
|
|||
} 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', 'QQ 已授权,但无法安全保存接入配置。');
|
||||
record.error = safeError('activation-failed', t('QQ 已授权,但无法安全保存接入配置。'));
|
||||
this.#logger.error?.('[dsh-im:qq] provisioning failed:', error);
|
||||
}
|
||||
} finally {
|
||||
|
|
@ -417,7 +418,7 @@ export class QqController {
|
|||
await this.#restoreCredential(identity.secretRef, previousSecret);
|
||||
throw error;
|
||||
}
|
||||
this.#errors.set(identity.botId, safeError('connection-failed', 'QQ 机器人已绑定,消息连接暂未就绪。'));
|
||||
this.#errors.set(identity.botId, safeError('connection-failed', t('QQ 机器人已绑定,消息连接暂未就绪。')));
|
||||
this.#logger.warn?.(`[dsh-im:qq] bot ${identity.botId} activation connection failed:`, error);
|
||||
}
|
||||
this.#touch();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
connectionTestTarget,
|
||||
connectionTestTargetUnavailable,
|
||||
} from '../shared/connection-test.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
import { createQqBridgeStatus, QqHarnessBridge } from './qq-bridge.mjs';
|
||||
|
||||
function timeoutError() {
|
||||
|
|
@ -73,7 +74,7 @@ export class QqRuntime {
|
|||
|
||||
async sendConnectionTest(text) {
|
||||
if (!this.#status.ready || !this.#bot) {
|
||||
throw connectionTestTargetUnavailable('QQ机器人');
|
||||
throw connectionTestTargetUnavailable(t('QQ机器人'));
|
||||
}
|
||||
const ownerUserOpenid = typeof this.#config.ownerUserOpenid === 'string'
|
||||
? this.#config.ownerUserOpenid.trim()
|
||||
|
|
@ -88,7 +89,7 @@ export class QqRuntime {
|
|||
: (ownerUserOpenid && ownerUserOpenid !== '*'
|
||||
? { scope: 'c2c', targetId: ownerUserOpenid }
|
||||
: null);
|
||||
if (!target) throw connectionTestTargetUnavailable('QQ机器人');
|
||||
if (!target) throw connectionTestTargetUnavailable(t('QQ机器人'));
|
||||
await this.#bot.sendText(target, text);
|
||||
return { sent: true };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { t } from './i18n.mjs';
|
||||
|
||||
/** Matches DeepSeek Harness agent-preset directory ids. */
|
||||
export const AGENT_PRESET_ID = /^[a-z0-9][a-z0-9-]*$/;
|
||||
|
||||
|
|
@ -17,7 +19,7 @@ export function validateAgentPresetId(value) {
|
|||
if (value == null || value === '') return null;
|
||||
const id = normalizeAgentPresetId(value);
|
||||
if (!id) {
|
||||
const error = new Error('Agent Preset 无效。');
|
||||
const error = new Error(t('Agent Preset 无效。'));
|
||||
error.code = 'agent-preset-invalid';
|
||||
throw error;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { t } from './i18n.mjs';
|
||||
import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
|
||||
|
||||
const COMPACT_COMMAND = /^\/compact(?=$|\s)([\s\S]*)$/i;
|
||||
|
|
@ -41,27 +42,27 @@ function compactResultText(result) {
|
|||
const text = result.text?.trim() ?? '';
|
||||
const compacted = /^Compacted (\d+) history items \(~(\d+) tokens\)\.$/u.exec(text);
|
||||
if (compacted) {
|
||||
return `已压缩 ${compacted[1]} 条历史记录(约 ${compacted[2]} 个 token)。`;
|
||||
return t('已压缩 {count} 条历史记录(约 {tokens} 个 token)。', { count: compacted[1], tokens: compacted[2] });
|
||||
}
|
||||
if (COMPACT_RESULT_TEXT.has(text)) return COMPACT_RESULT_TEXT.get(text);
|
||||
if (COMPACT_RESULT_TEXT.has(text)) return t(COMPACT_RESULT_TEXT.get(text));
|
||||
if (text) return text;
|
||||
return result.kind === 'success' ? '上下文压缩完成。' : '上下文压缩失败。';
|
||||
return result.kind === 'success' ? t('上下文压缩完成。') : t('上下文压缩失败。');
|
||||
}
|
||||
|
||||
function compactErrorMessage(error) {
|
||||
const code = error?.code ?? error?.failure?.code;
|
||||
if (code === 'session-not-found') {
|
||||
return '当前聊天绑定的会话已不存在,请发送新消息开启会话。';
|
||||
return t('当前聊天绑定的会话已不存在,请发送新消息开启会话。');
|
||||
}
|
||||
if (code === 'agent-busy') return '当前会话正在生成回复,请稍后重试。';
|
||||
if (code === 'cancelled' || error?.name === 'AbortError') return '上下文压缩已取消。';
|
||||
if (code === 'agent-busy') return t('当前会话正在生成回复,请稍后重试。');
|
||||
if (code === 'cancelled' || error?.name === 'AbortError') return t('上下文压缩已取消。');
|
||||
if (code === WORKSPACE_SESSION_STALE || code === 'workspace-bot-not-found') {
|
||||
return '工作区或机器人状态已发生变化,请重试。';
|
||||
return t('工作区或机器人状态已发生变化,请重试。');
|
||||
}
|
||||
if (code === 'commands-unavailable') {
|
||||
return '当前 Harness 暂不支持从机器人执行上下文压缩。';
|
||||
return t('当前 Harness 暂不支持从机器人执行上下文压缩。');
|
||||
}
|
||||
return '上下文压缩失败,请稍后重试。';
|
||||
return t('上下文压缩失败,请稍后重试。');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -72,21 +73,21 @@ export async function runCompactCommand(text, harness, state, conversationKey, o
|
|||
if (typeof text !== 'string') return null;
|
||||
const match = COMPACT_COMMAND.exec(text.trim());
|
||||
if (!match) return null;
|
||||
if (match[1].trim()) return commandResult(COMPACT_USAGE);
|
||||
if (match[1].trim()) return commandResult(t(COMPACT_USAGE));
|
||||
if (typeof state?.sessionFor !== 'function') {
|
||||
return commandResult('当前机器人没有可用的会话状态。');
|
||||
return commandResult(t('当前机器人没有可用的会话状态。'));
|
||||
}
|
||||
const sessionId = state.sessionFor(conversationKey);
|
||||
if (typeof sessionId !== 'string' || !sessionId) {
|
||||
return commandResult('当前聊天还没有可压缩的会话,请先发送一条消息。');
|
||||
return commandResult(t('当前聊天还没有可压缩的会话,请先发送一条消息。'));
|
||||
}
|
||||
if (typeof harness?.executeCommand !== 'function') {
|
||||
return commandResult('当前机器人暂不支持上下文压缩。');
|
||||
return commandResult(t('当前机器人暂不支持上下文压缩。'));
|
||||
}
|
||||
try {
|
||||
const execution = await harness.executeCommand(sessionId, '/compact', options);
|
||||
if (execution === undefined) {
|
||||
return commandResult('当前 Harness 未注册 /compact 命令,请确认上下文压缩组件已启用。');
|
||||
return commandResult(t('当前 Harness 未注册 /compact 命令,请确认上下文压缩组件已启用。'));
|
||||
}
|
||||
return commandResult(compactResultText(execution?.result));
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { t } from './i18n.mjs';
|
||||
|
||||
const targets = new WeakMap();
|
||||
|
||||
export const CONNECTION_TEST_STATE_IDENTITY = Symbol('dsh-im.connection-test-state-identity');
|
||||
|
|
@ -25,13 +27,13 @@ export function connectionTestTarget(state) {
|
|||
return target ? structuredClone(target) : null;
|
||||
}
|
||||
|
||||
export function connectionTestMessage(botName, channelLabel = '机器人') {
|
||||
export function connectionTestMessage(botName, channelLabel = t('机器人')) {
|
||||
const name = cleanText(botName) ?? channelLabel;
|
||||
return `✅ DeepSeek Harness 连接测试成功\n这条消息由插件页面中的“${name}”机器人卡片发出。`;
|
||||
return `${t('✅ DeepSeek Harness 连接测试成功')}\n${t('这条消息由插件页面中的“{name}”机器人卡片发出。', { name })}`;
|
||||
}
|
||||
|
||||
export function connectionTestTargetUnavailable(channelLabel = '机器人') {
|
||||
const error = new Error(`${channelLabel}尚未收到可用于测试的私聊消息。`);
|
||||
export function connectionTestTargetUnavailable(channelLabel = t('机器人')) {
|
||||
const error = new Error(t('{channelLabel}尚未收到可用于测试的私聊消息。', { channelLabel }));
|
||||
error.code = 'test-target-unavailable';
|
||||
return error;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { t } from './i18n.mjs';
|
||||
|
||||
const CONTROL_COMMAND = /^\/(?:stop|steer)(?=$|\s)/iu;
|
||||
const STOP_COMMAND = /^\/stop(?=$|\s)/iu;
|
||||
const STOP_USAGE = '用法:/stop(不带参数)';
|
||||
|
|
@ -40,35 +42,35 @@ export async function runControlCommand(text, harness, state, key, {
|
|||
const command = text.trim();
|
||||
const stop = STOP_COMMAND.test(command);
|
||||
|
||||
if (hasImages) return commandResult(TEXT_ONLY);
|
||||
if (hasImages) return commandResult(t(TEXT_ONLY));
|
||||
|
||||
if (stop) {
|
||||
if (!/^\/stop$/iu.test(command)) return commandResult(STOP_USAGE);
|
||||
if (!/^\/stop$/iu.test(command)) return commandResult(t(STOP_USAGE));
|
||||
const session = boundSession(harness, state, key);
|
||||
if (!session) return commandResult('当前聊天没有正在运行的任务。');
|
||||
if (!session) return commandResult(t('当前聊天没有正在运行的任务。'));
|
||||
if (typeof session.stopActiveTurn !== 'function') {
|
||||
throw new TypeError('Harness session does not support stopping active turns');
|
||||
}
|
||||
const stopped = await session.stopActiveTurn(control, requestOptions(signal));
|
||||
return stopped
|
||||
? commandResult('已请求停止当前任务。', { stopped: true })
|
||||
: commandResult('当前聊天没有正在运行的任务。');
|
||||
? commandResult(t('已请求停止当前任务。'), { stopped: true })
|
||||
: commandResult(t('当前聊天没有正在运行的任务。'));
|
||||
}
|
||||
|
||||
const match = /^\/steer(?:\s+([\s\S]*))?$/iu.exec(command);
|
||||
const instruction = match?.[1]?.trim() ?? '';
|
||||
if (!instruction) return commandResult(STEER_USAGE);
|
||||
if (!instruction) return commandResult(t(STEER_USAGE));
|
||||
if (pendingInteraction) {
|
||||
return commandResult([
|
||||
'当前任务正在等待你的回答或审批。',
|
||||
t('当前任务正在等待你的回答或审批。'),
|
||||
'',
|
||||
'请先处理当前请求,或者发送 /stop 停止任务。',
|
||||
t('请先处理当前请求,或者发送 /stop 停止任务。'),
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
const session = boundSession(harness, state, key);
|
||||
if (!session) {
|
||||
return commandResult('当前聊天没有正在运行的任务,请直接发送普通消息。');
|
||||
return commandResult(t('当前聊天没有正在运行的任务,请直接发送普通消息。'));
|
||||
}
|
||||
if (typeof session.steerActiveTurn !== 'function') {
|
||||
throw new TypeError('Harness session does not support steering active turns');
|
||||
|
|
@ -79,6 +81,6 @@ export async function runControlCommand(text, harness, state, key, {
|
|||
requestOptions(signal),
|
||||
);
|
||||
return steered
|
||||
? commandResult('已提交补充指令,Agent 会在下一步读取。')
|
||||
: commandResult('当前聊天没有正在运行的任务,请直接发送普通消息。');
|
||||
? commandResult(t('已提交补充指令,Agent 会在下一步读取。'))
|
||||
: commandResult(t('当前聊天没有正在运行的任务,请直接发送普通消息。'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { t } from './i18n.mjs';
|
||||
|
||||
export function splitMessageText(value, limit) {
|
||||
const text = typeof value === 'string' ? value.trim() : '';
|
||||
if (!text) return [];
|
||||
|
|
@ -15,7 +17,7 @@ export function splitMessageText(value, limit) {
|
|||
}
|
||||
|
||||
export function createEditableMessageStream({
|
||||
initialText = '正在处理…',
|
||||
initialText = t('正在处理…'),
|
||||
limit,
|
||||
updateIntervalMs = 800,
|
||||
create,
|
||||
|
|
@ -77,7 +79,7 @@ export function createEditableMessageStream({
|
|||
pending = null;
|
||||
await inFlight?.catch(() => undefined);
|
||||
const chunks = splitMessageText(text, limit);
|
||||
const first = chunks[0] ?? '处理完成。';
|
||||
const first = chunks[0] ?? t('处理完成。');
|
||||
if (first !== lastSent) await edit(messageId, first);
|
||||
lastSent = first;
|
||||
for (const chunk of chunks.slice(1)) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { t } from './i18n.mjs';
|
||||
|
||||
const APPROVAL_REPLIES = new Map([
|
||||
['批准', 'allowed-once'],
|
||||
['同意', 'allowed-once'],
|
||||
|
|
@ -68,16 +70,16 @@ export function harnessApprovalText(payload, {
|
|||
if (!operation || operation.length > maxArgumentsLength) return null;
|
||||
|
||||
const lines = [
|
||||
'DeepSeek Harness 需要你的审批:',
|
||||
t('DeepSeek Harness 需要你的审批:'),
|
||||
'',
|
||||
`工具:${printableText(payload.toolName)}`,
|
||||
'操作参数:',
|
||||
t('工具:{tool}', { tool: printableText(payload.toolName) }),
|
||||
t('操作参数:'),
|
||||
operation,
|
||||
];
|
||||
const reason = printableText(payload.reason);
|
||||
if (reason) lines.push(`原因:${reason}`);
|
||||
lines.push('', APPROVAL_PROMPT);
|
||||
if (requiresMention) lines.push('', '群聊中请 @机器人 后发送审批决定。');
|
||||
if (reason) lines.push(t('原因:{reason}', { reason }));
|
||||
lines.push('', t(APPROVAL_PROMPT));
|
||||
if (requiresMention) lines.push('', t('群聊中请 @机器人 后发送审批决定。'));
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
|
|
@ -93,9 +95,9 @@ function approvalResult(pending, outcome) {
|
|||
}
|
||||
|
||||
function approvalOutcomeText(outcome) {
|
||||
if (outcome === 'allowed-once') return '已批准,仅对本次操作有效。';
|
||||
if (outcome === 'rejected') return '已拒绝此次操作。';
|
||||
return APPROVAL_RESOLVED_TEXT;
|
||||
if (outcome === 'allowed-once') return t('已批准,仅对本次操作有效。');
|
||||
if (outcome === 'rejected') return t('已拒绝此次操作。');
|
||||
return t(APPROVAL_RESOLVED_TEXT);
|
||||
}
|
||||
|
||||
export class HarnessApprovalQueue {
|
||||
|
|
@ -149,11 +151,11 @@ export class HarnessApprovalQueue {
|
|||
return null;
|
||||
}
|
||||
if (!decision) return null;
|
||||
return notice(APPROVAL_RESOLVED_TEXT, true);
|
||||
return notice(t(APPROVAL_RESOLVED_TEXT), true);
|
||||
}
|
||||
if (pending.actor !== actor || (pending.requiresMention && addressed !== true)) {
|
||||
if (!decision) return null;
|
||||
return notice('只有发起当前任务的用户可以处理这条审批。');
|
||||
return notice(t('只有发起当前任务的用户可以处理这条审批。'));
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -168,14 +170,14 @@ export class HarnessApprovalQueue {
|
|||
await questionCompletion.catch(() => undefined);
|
||||
if (pending.inactive || pending.resolving) return;
|
||||
if (typeof isQuestionPending === 'function' && isQuestionPending()) {
|
||||
await send(APPROVAL_AFTER_QUESTION_PROMPT);
|
||||
await send(t(APPROVAL_AFTER_QUESTION_PROMPT));
|
||||
return;
|
||||
}
|
||||
}
|
||||
await pending.activationTask?.catch(() => undefined);
|
||||
await pending.presentationTask?.catch(() => undefined);
|
||||
if (pending.inactive || pending.resolving) {
|
||||
await send(APPROVAL_RESOLVED_TEXT);
|
||||
await send(t(APPROVAL_RESOLVED_TEXT));
|
||||
return;
|
||||
}
|
||||
pending.send = send;
|
||||
|
|
@ -185,15 +187,15 @@ export class HarnessApprovalQueue {
|
|||
if (!presentedWhenClaimed || !pending.presented) {
|
||||
if (!pending.presented) await this.#present(pending);
|
||||
if (pending.inactive || pending.resolving) return;
|
||||
await send(APPROVAL_PROMPT);
|
||||
await send(t(APPROVAL_PROMPT));
|
||||
return;
|
||||
}
|
||||
if (pending.submitting) {
|
||||
await send('审批决定正在提交,请稍候。');
|
||||
await send(t('审批决定正在提交,请稍候。'));
|
||||
return;
|
||||
}
|
||||
if (!decision) {
|
||||
await send(APPROVAL_PROMPT);
|
||||
await send(t(APPROVAL_PROMPT));
|
||||
return;
|
||||
}
|
||||
await this.#submit(pending, decision);
|
||||
|
|
@ -251,8 +253,8 @@ export class HarnessApprovalQueue {
|
|||
if (!text) {
|
||||
const rejected = await this.#rejectInteraction(interaction, payload);
|
||||
await send(rejected
|
||||
? '无法完整展示这次操作,已安全拒绝此次审批。'
|
||||
: APPROVAL_RESOLVED_TEXT);
|
||||
? t('无法完整展示这次操作,已安全拒绝此次审批。')
|
||||
: t(APPROVAL_RESOLVED_TEXT));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -335,7 +337,7 @@ export class HarnessApprovalQueue {
|
|||
pending.closedOutcome = 'resolved';
|
||||
if ((pending.presented || pending.deliveryCompleted) && !pending.resolutionNotified) {
|
||||
pending.resolutionNotified = true;
|
||||
await pending.send(APPROVAL_RESOLVED_TEXT).catch(() => undefined);
|
||||
await pending.send(t(APPROVAL_RESOLVED_TEXT)).catch(() => undefined);
|
||||
}
|
||||
} else {
|
||||
this.#logger.warn?.(`[dsh-im:${this.#label}] failed to reject a closing approval:`, error);
|
||||
|
|
@ -377,7 +379,7 @@ export class HarnessApprovalQueue {
|
|||
const next = this.#remove(pending);
|
||||
await this.#transition(next, async () => {
|
||||
if (!pending.resolutionNotified) {
|
||||
await send(APPROVAL_RESOLVED_TEXT).catch(() => undefined);
|
||||
await send(t(APPROVAL_RESOLVED_TEXT)).catch(() => undefined);
|
||||
}
|
||||
});
|
||||
return;
|
||||
|
|
@ -385,7 +387,7 @@ export class HarnessApprovalQueue {
|
|||
if (pending.inactive) return;
|
||||
pending.submitting = false;
|
||||
this.#logger.error?.(`[dsh-im:${this.#label}] failed to submit an approval:`, error);
|
||||
await pending.send('审批提交失败,请重新回复「批准」或「拒绝」。').catch(() => undefined);
|
||||
await pending.send(t('审批提交失败,请重新回复「批准」或「拒绝」。')).catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
InboundFileError,
|
||||
} from './inbound-file.mjs';
|
||||
import { outboundArtifactRegistry } from './semantic/artifact.mjs';
|
||||
import { t } from './i18n.mjs';
|
||||
|
||||
// Every channel plugin runs in the same Host process. Sharing ownership by
|
||||
// Harness origin prevents two channel-specific clients bound to one Session
|
||||
|
|
@ -438,7 +439,7 @@ export class HarnessReplyTracker {
|
|||
}
|
||||
|
||||
if (event.type === 'tool/call') {
|
||||
const name = nonEmptyText(event.data?.name) ?? '工具';
|
||||
const name = nonEmptyText(event.data?.name) ?? t('工具');
|
||||
const callId = nonEmptyText(event.data?.callId)
|
||||
?? nonEmptyText(event.data?.subCallId);
|
||||
if (callId) this.#toolNames.set(callId, name);
|
||||
|
|
@ -453,7 +454,7 @@ export class HarnessReplyTracker {
|
|||
const error = toolResultErrorText(event.data?.error);
|
||||
pushUpdate({
|
||||
type: 'status',
|
||||
text: '正在整理结果…',
|
||||
text: t('正在整理结果…'),
|
||||
...(toolName ? { toolName } : {}),
|
||||
...(error ? { error } : {}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { t } from './i18n.mjs';
|
||||
|
||||
function nonEmptyString(value) {
|
||||
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
||||
}
|
||||
|
|
@ -17,9 +19,9 @@ export function validHarnessQuestion(question) {
|
|||
export function harnessQuestionText(question, index, total, { requiresMention = false } = {}) {
|
||||
const lines = [];
|
||||
const progress = total > 1 ? `(${index + 1}/${total})` : '';
|
||||
lines.push(`DeepSeek Harness 需要你补充信息${progress}:`);
|
||||
lines.push(t('DeepSeek Harness 需要你补充信息{progress}:', { progress }));
|
||||
if (nonEmptyString(question.header)) lines.push('', question.header.trim());
|
||||
lines.push('', nonEmptyString(question.question) ?? '请输入你的回答。');
|
||||
lines.push('', nonEmptyString(question.question) ?? t('请输入你的回答。'));
|
||||
if (nonEmptyString(question.detail)) lines.push('', question.detail.trim());
|
||||
|
||||
const options = Array.isArray(question.options) ? question.options : [];
|
||||
|
|
@ -31,12 +33,12 @@ export function harnessQuestionText(question, index, total, { requiresMention =
|
|||
lines.push(`${optionIndex + 1}. ${label}${description ? ` — ${description}` : ''}`);
|
||||
});
|
||||
lines.push('', question.multiSelect === true
|
||||
? '请回复选项序号或文字;多选用逗号分隔,也可补充其他内容。'
|
||||
: '请回复一个选项序号或文字,也可直接输入其他答案。');
|
||||
? t('请回复选项序号或文字;多选用逗号分隔,也可补充其他内容。')
|
||||
: t('请回复一个选项序号或文字,也可直接输入其他答案。'));
|
||||
} else {
|
||||
lines.push('', '请直接回复你的答案。');
|
||||
lines.push('', t('请直接回复你的答案。'));
|
||||
}
|
||||
if (requiresMention) lines.push('', '群聊中请 @机器人 后发送答案。');
|
||||
if (requiresMention) lines.push('', t('群聊中请 @机器人 后发送答案。'));
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
|
|
|
|||
35
src/channels/shared/i18n-en.mjs
Normal file
35
src/channels/shared/i18n-en.mjs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// English translations for host-side user-facing text.
|
||||
// Keys are the exact Chinese literals passed to t() in src/channels/**.
|
||||
// Chinese output is the identity default and needs no entries here.
|
||||
// Entries are maintained per area in ./i18n-en/*.mjs and merged here.
|
||||
|
||||
import sharedA from './i18n-en/shared-a.mjs';
|
||||
import sharedB from './i18n-en/shared-b.mjs';
|
||||
import sharedC from './i18n-en/shared-c.mjs';
|
||||
import feishu from './i18n-en/feishu.mjs';
|
||||
import dingtalk from './i18n-en/dingtalk.mjs';
|
||||
import wecom from './i18n-en/wecom.mjs';
|
||||
import qq from './i18n-en/qq.mjs';
|
||||
import weixin from './i18n-en/weixin.mjs';
|
||||
import slack from './i18n-en/slack.mjs';
|
||||
import telegram from './i18n-en/telegram.mjs';
|
||||
import discord from './i18n-en/discord.mjs';
|
||||
import whatsapp from './i18n-en/whatsapp.mjs';
|
||||
import office from './i18n-en/office.mjs';
|
||||
|
||||
export const EN = Object.freeze(Object.assign(
|
||||
{},
|
||||
sharedA,
|
||||
sharedB,
|
||||
sharedC,
|
||||
feishu,
|
||||
dingtalk,
|
||||
wecom,
|
||||
qq,
|
||||
weixin,
|
||||
slack,
|
||||
telegram,
|
||||
discord,
|
||||
whatsapp,
|
||||
office,
|
||||
));
|
||||
53
src/channels/shared/i18n-en/dingtalk.mjs
Normal file
53
src/channels/shared/i18n-en/dingtalk.mjs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// English translations (dingtalk area). Keys are exact Chinese literals passed to t().
|
||||
export default {
|
||||
// Bot chat replies and card text (dingtalk-bridge.mjs, dingtalk-card-stream.mjs,
|
||||
// dingtalk-api.mjs)
|
||||
'已连接 DeepSeek Harness,正在思考…': 'Connected to DeepSeek Harness, thinking…',
|
||||
|
||||
'钉钉机器人已连接 DeepSeek Harness。': 'The DingTalk bot is connected to DeepSeek Harness.',
|
||||
|
||||
|
||||
|
||||
|
||||
'钉钉未能换取图片下载地址,请重新发送;若持续失败,请检查机器人的“企业内机器人发送消息权限”。': 'DingTalk could not provide the image download address. Please resend; if it keeps failing, check the bot\'s "Send messages as an internal robot" permission.',
|
||||
'钉钉没有返回图片下载地址,请重新发送。': 'DingTalk did not return an image download address. Please resend.',
|
||||
'钉钉返回的图片临时地址无法读取,请重新发送。': 'The temporary image address returned by DingTalk could not be read. Please resend.',
|
||||
'结果文件「{name}」发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。': 'Delivery of the result file "{name}" could not be confirmed. Please check whether it already arrived in the chat before retrying.',
|
||||
'结果文件「{name}」已生成,但钉钉应用或机器人缺少文件消息权限。请开通应用 qyapi_base 权限,并确认机器人具备文件消息发送能力。': 'Result file "{name}" was generated, but the DingTalk app or bot lacks file message permission. Enable the app\'s qyapi_base permission and make sure the bot can send file messages.',
|
||||
'结果文件「{name}」超过当前钉钉机器人可发送的文件大小,未发送。': 'Result file "{name}" exceeds the file size limit of the current DingTalk bot and was not sent.',
|
||||
'结果文件「{name}」暂时被钉钉限流,未能发送,请稍后重试。': 'Result file "{name}" was rate-limited by DingTalk and could not be sent. Please try again later.',
|
||||
'结果文件「{name}」已生成,但钉钉拒绝了该文件消息,请检查文件类型和机器人文件消息配置。': 'Result file "{name}" was generated, but DingTalk rejected the file message. Check the file type and the bot\'s file message configuration.',
|
||||
|
||||
'结果文件「{name}」已生成,但暂时未能通过钉钉发送,请稍后重试。': 'Result file "{name}" was generated but could not be sent via DingTalk right now. Please try again later.',
|
||||
'结果文件已生成。': 'The result file has been generated.',
|
||||
'钉钉机器人与 DeepSeek Harness 连接正常。': 'The DingTalk bot is connected to DeepSeek Harness normally.',
|
||||
|
||||
|
||||
|
||||
// Host-side status and error text (dingtalk-bridge.mjs, dingtalk-runtime.mjs,
|
||||
// dingtalk-controller.mjs, config-store.mjs, state-store.mjs)
|
||||
'钉钉命令处理失败。': 'Failed to process the DingTalk command.',
|
||||
'钉钉消息没有安全的回复地址。': 'The DingTalk message has no safe reply address.',
|
||||
'钉钉审批处理失败。': 'Failed to process the DingTalk approval.',
|
||||
'钉钉消息处理失败。': 'Failed to process the DingTalk message.',
|
||||
'钉钉消息格式无效。': 'Invalid DingTalk message format.',
|
||||
'钉钉交互问题发送失败。': 'Failed to send the DingTalk interaction question.',
|
||||
'钉钉用户': 'DingTalk user',
|
||||
'钉钉机器人': 'DingTalk bot',
|
||||
'身份已隐藏': 'Identity hidden',
|
||||
'钉钉机器人凭据缺失,请移除后重新扫码。': 'The DingTalk bot credentials are missing. Remove the bot and scan the QR code again.',
|
||||
'钉钉连接未就绪,请稍后重试。': 'The DingTalk connection is not ready. Please try again later.',
|
||||
'钉钉连接仍未就绪,请稍后重试。': 'The DingTalk connection is still not ready. Please try again later.',
|
||||
'扫码接入已取消。': 'QR code setup has been cancelled.',
|
||||
'无法生成钉钉二维码,请稍后重试。': 'Could not generate the DingTalk QR code. Please try again later.',
|
||||
'二维码已过期,请重新生成。': 'The QR code has expired. Generate a new one.',
|
||||
'钉钉已接入,但消息连接暂未就绪,请稍后重试。': 'DingTalk is connected, but the message connection is not ready yet. Please try again later.',
|
||||
'钉钉未完成机器人授权,请重新扫码。': 'DingTalk did not complete the bot authorization. Please scan the QR code again.',
|
||||
'钉钉授权状态暂时不可用,正在重试。': 'The DingTalk authorization status is temporarily unavailable. Retrying.',
|
||||
'钉钉已授权,但无法安全保存接入配置。': 'DingTalk authorization succeeded, but the configuration could not be saved safely.',
|
||||
'钉钉授权查询暂时失败,正在重试。': 'The DingTalk authorization query failed temporarily. Retrying.',
|
||||
'钉钉机器人({clientId})': 'DingTalk bot ({clientId})',
|
||||
'钉钉 Stream 消息连接运行正常': 'The DingTalk Stream message connection is running normally',
|
||||
'钉钉消息连接当前离线': 'The DingTalk message connection is currently offline',
|
||||
};
|
||||
|
||||
9
src/channels/shared/i18n-en/discord.mjs
Normal file
9
src/channels/shared/i18n-en/discord.mjs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// English translations (discord area). Keys are exact Chinese literals passed to t().
|
||||
export default {
|
||||
'Discord Bot Token 无效,请重新填写。': 'The Discord Bot Token is invalid. Please enter it again.',
|
||||
'Discord Gateway Intents 配置不正确,请检查 Developer Portal 的 Bot 设置。':
|
||||
'The Discord Gateway Intents are misconfigured. Please check the Bot settings in the Developer Portal.',
|
||||
'Discord机器人': 'Discord Bot',
|
||||
' Gateway 长连接': ' Gateway long-lived connection',
|
||||
};
|
||||
|
||||
217
src/channels/shared/i18n-en/feishu.mjs
Normal file
217
src/channels/shared/i18n-en/feishu.mjs
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
// English translations (feishu area). Keys are exact Chinese literals passed to t().
|
||||
export default {
|
||||
// feishu/bridge.mjs — welcome / help
|
||||
'北汇星河 AIOS 已连接 DeepSeek Harness。':
|
||||
'BeiHui XingHe AIOS is connected to DeepSeek Harness.',
|
||||
'/repair 修复卡片按钮回调': '/repair Repair the card-button callback',
|
||||
'/m(或 /menu) 打开交互卡片菜单': '/m (or /menu) Open the interactive card menu',
|
||||
'/watch [Session ID 或序号] 关注会话,任务完成自动推送':
|
||||
'/watch [Session ID or index] Watch a session; completion is pushed automatically',
|
||||
'/unwatch [Session ID 或序号] 取消关注':
|
||||
'/unwatch [Session ID or index] Stop watching',
|
||||
'/watchlist 查看关注列表': '/watchlist View the watch list',
|
||||
'/archived on|off 会话列表是否包含归档会话':
|
||||
'/archived on|off Whether the session list includes archived sessions',
|
||||
'工作区必须是绝对路径。': 'The workspace must be an absolute path.',
|
||||
'工作区路径不存在。': 'The workspace path does not exist.',
|
||||
'工作区路径必须指向一个目录。': 'The workspace path must point to a directory.',
|
||||
'机器人正在移除或已重新接入,无法操作原会话的工作区。':
|
||||
'The bot is being removed or has been reconnected; the original session’s workspace cannot be changed.',
|
||||
'操作失败,请稍后重试。': 'The operation failed. Please try again later.',
|
||||
'结果文件「{name}」已生成,但机器人缺少飞书文件上传权限。请为应用添加 im:resource 并完成必要审批后重试。':
|
||||
'The result file "{name}" was generated, but the bot lacks Feishu file-upload permission. Add the im:resource scope to the app, complete the required approval, and try again.',
|
||||
'结果文件「{name}」超过飞书 30 MB 上限,未发送。':
|
||||
'The result file "{name}" exceeds the Feishu 30 MB limit and was not sent.',
|
||||
'结果文件「{name}」为空,飞书不允许发送空文件。':
|
||||
'The result file "{name}" is empty; Feishu does not allow sending empty files.',
|
||||
'结果文件「{name}」暂时被飞书限流,未能发送,请稍后重试。':
|
||||
'The result file "{name}" was temporarily rate-limited by Feishu and could not be sent. Please try again later.',
|
||||
'结果文件「{name}」已生成,但暂时未能发送,请稍后重试。':
|
||||
'The result file "{name}" was generated but could not be sent right now. Please try again later.',
|
||||
'处理失败,请稍后重试。如果问题持续,请在 DeepSeek Harness 的飞书插件页面检查连接状态。':
|
||||
'Message processing failed. Please try again later. If the problem persists, check the connection status on the Feishu plugin page in DeepSeek Harness.',
|
||||
'已开启全新 Harness 会话。': 'A brand-new Harness session has started.',
|
||||
'飞书机器人与 DeepSeek Harness 连接正常。':
|
||||
'The Feishu bot is connected to DeepSeek Harness and working normally.',
|
||||
'用法:/archived on(包含归档会话)或 /archived off(隐藏归档会话)':
|
||||
'Usage: /archived on (include archived sessions) or /archived off (hide archived sessions)',
|
||||
'已开启:会话列表包含归档会话。': 'On: the session list includes archived sessions.',
|
||||
'已关闭:会话列表隐藏归档会话。': 'Off: the session list hides archived sessions.',
|
||||
|
||||
// feishu/bridge.mjs — repair flow
|
||||
'为避免授权链接暴露,请私聊机器人发送 /repair。':
|
||||
'To keep the authorization link private, send /repair to the bot in a direct message.',
|
||||
'当前机器人没有可验证的接入者身份,不能从聊天发起修复;请先在插件页设置管理员。':
|
||||
'This bot has no verifiable owner identity, so repair cannot be started from chat. Set an administrator on the plugin page first.',
|
||||
'此操作只能由机器人接入者在私聊中发起,未进行任何修改。':
|
||||
'This action can only be started by the bot owner in a direct message. Nothing was changed.',
|
||||
'当前 Host 版本暂不支持聊天内修复,请先更新插件。':
|
||||
'The current Host version does not support in-chat repair. Update the plugin first.',
|
||||
'用法:/repair、/repair qr、/repair status、/repair cancel 或 /repair verify':
|
||||
'Usage: /repair, /repair qr, /repair status, /repair cancel, or /repair verify',
|
||||
'当前 Runtime 没有可恢复的修复任务记录(机器人可能刚完成密钥更新并重启)。本命令不会启动新的授权;请查看机器人发送的验证结果,确认上一次任务已结束后再发送 /repair。':
|
||||
'The current Runtime has no recoverable repair attempt (the bot may have just rotated its secret and restarted). This command starts no new authorization. Check the verification result the bot sent, then send /repair after the previous attempt has finished.',
|
||||
'另一位管理员正在修复该机器人,本次不会显示其授权信息。':
|
||||
'Another administrator is repairing this bot; its authorization info will not be shown this time.',
|
||||
'暂时无法取消修复任务,请稍后重试。':
|
||||
'Could not cancel the repair task right now. Please try again later.',
|
||||
'暂时无法查询修复状态,请稍后重试。':
|
||||
'Could not check the repair status right now. Please try again later.',
|
||||
'修复流程暂时失败,现有机器人连接不受影响;请稍后发送 /repair 重试。':
|
||||
'The repair flow failed for now; the existing bot connection is unaffected. Send /repair again later.',
|
||||
'飞书返回了无法安全验证的授权链接,已中止本次修复。':
|
||||
'Feishu returned an authorization link that cannot be verified safely. This repair was aborted.',
|
||||
'飞书未返回授权链接,已中止本次修复。':
|
||||
'Feishu did not return an authorization link. This repair was aborted.',
|
||||
'授权已确认,正在发送并等待测试按钮回调;收到真实回调后才会完成。':
|
||||
'Authorization confirmed. Sending a test button and waiting for its callback; this completes only after a real callback arrives.',
|
||||
'修复状态查询中断,现有机器人连接不受影响;发送 /repair status 重试查询。':
|
||||
'The repair status query was interrupted; the existing bot connection is unaffected. Send /repair status to retry.',
|
||||
'链接为短期有效': 'The link is valid for a short time.',
|
||||
'链接约 {minutes} 分钟后过期': 'The link expires in about {minutes} minutes',
|
||||
'已有一个修复任务在等待授权。': 'A repair task is already waiting for authorization.',
|
||||
'🔧 准备修复卡片按钮。': '🔧 Preparing to repair the card buttons.',
|
||||
'本次只会增量添加 card.action.trigger。请核对确认页只显示这一项;若出现其他权限或事件,请取消。':
|
||||
'Only card.action.trigger will be added. Check that the confirmation page shows only this item; cancel if any other permission or event appears.',
|
||||
'当前设备直接打开:': 'Open directly on this device:',
|
||||
'若要用另一台设备扫码,发送 /repair qr。{expiry}。':
|
||||
'To scan with another device, send /repair qr. {expiry}.',
|
||||
'请用另一台设备扫码完成授权{remaining}。':
|
||||
'Complete authorization by scanning with another device{remaining}.',
|
||||
'(剩余约 {minutes} 分钟)': ' (about {minutes} minutes left)',
|
||||
'二维码暂时无法发送,请直接打开授权链接:\n{url}':
|
||||
'The QR code could not be sent. Open the authorization link directly:\n{url}',
|
||||
'授权链接已过期;平台未返回成功结果,无法确认已修复。发送 /repair 生成新链接。':
|
||||
'The authorization link expired; the platform returned no success, so the repair cannot be confirmed. Send /repair for a new link.',
|
||||
'已取消本次修复授权,未确认完成修复。':
|
||||
'Repair authorization was cancelled; the repair was not confirmed.',
|
||||
'你已取消或拒绝授权,没有确认修复;发送 /repair 可重试。':
|
||||
'You cancelled or declined authorization, so the repair was not confirmed. Send /repair to retry.',
|
||||
'授权已提交,但未收到测试按钮回调。可能尚未点击或配置仍在传播;稍后发送 /repair verify 查询,不要盲目重复授权。':
|
||||
'Authorization was submitted, but no test-button callback arrived. It may not have been tapped yet, or the config is still propagating. Send /repair verify later to check; do not authorize blindly again.',
|
||||
'修复流程暂时失败,现有机器人连接不受影响;发送 /repair 可重试。':
|
||||
'The repair flow failed for now; the existing bot connection is unaffected. Send /repair to retry.',
|
||||
'授权已确认,正在等待专用测试按钮的真实回调;回调到达前不会宣告成功。':
|
||||
'Authorization confirmed. Waiting for the real callback from the dedicated test button; success is not declared until it arrives.',
|
||||
'授权尚未完成,暂时不能验证卡片按钮。请先打开授权链接并确认。':
|
||||
'Authorization is not complete, so the card buttons cannot be verified yet. Open the authorization link and confirm first.',
|
||||
'修复任务正在等待授权{remaining}。发送 /repair qr 可获取二维码,/repair cancel 可取消。':
|
||||
'The repair task is waiting for authorization{remaining}. Send /repair qr for the QR code, or /repair cancel to cancel.',
|
||||
',剩余约 {minutes} 分钟': ', about {minutes} minutes left',
|
||||
'这个菜单已过期,请回复 /m 重新打开。': 'This menu has expired. Send /m to reopen it.',
|
||||
'菜单没有这个编号,回复 /m 重新打开。': 'This menu has no such number. Send /m to reopen it.',
|
||||
'本页只有 {count} 个会话,回复 /sessionlist 重新查看。':
|
||||
'This page has only {count} sessions. Send /sessionlist to view them again.',
|
||||
'只有 {count} 个工作区,回复 /workspacelist 重新查看。':
|
||||
'There are only {count} workspaces. Send /workspacelist to view them again.',
|
||||
'关注列表只有 {count} 个会话。': 'The watch list has only {count} sessions.',
|
||||
|
||||
|
||||
|
||||
|
||||
'已绑定会话「{title}」\nID:{id}': 'Session bound: "{title}"\nID: {id}',
|
||||
'绑定失败:{message}': 'Binding failed: {message}',
|
||||
|
||||
'切换失败:{message}': 'Switch failed: {message}',
|
||||
'用法:/watch <Session ID 或当前工作区序号>':
|
||||
'Usage: /watch <Session ID or the current workspace index>',
|
||||
'当前机器人没有可用的工作区,无法按序号解析会话。':
|
||||
'This bot has no available workspace, so sessions cannot be resolved by index.',
|
||||
'当前工作区只有 {count} 个会话。': 'The current workspace has only {count} sessions.',
|
||||
'没有找到这个会话,请用 /sessionlist 查看可用会话。':
|
||||
'Session not found. Use /sessionlist to see the available sessions.',
|
||||
'当前状态存储不支持关注。': 'The current state store does not support watching.',
|
||||
'无法解析会话:{message}': 'Could not resolve the session: {message}',
|
||||
'每个聊天最多关注 {count} 个会话。': 'Each chat can watch at most {count} sessions.',
|
||||
'关注列表里没有这个会话,回复 /watchlist 查看。':
|
||||
'This session is not in the watch list. Send /watchlist to view it.',
|
||||
'已关注会话「{title}」,任务完成会推送结果。':
|
||||
'Watching session "{title}"; results are pushed when the task completes.',
|
||||
'关注失败:{message}': 'Could not watch: {message}',
|
||||
'已取消关注「{title}」。': 'Unwatched "{title}".',
|
||||
'取消失败:{message}': 'Could not unwatch: {message}',
|
||||
'飞书交互问题发送失败。': 'Failed to send the Feishu interaction question.',
|
||||
|
||||
// feishu/feishu-cards.mjs — interactive cards
|
||||
'🤖 助手菜单': '🤖 Assistant menu',
|
||||
'**点击按钮或直接回复数字**': '**Tap a button or reply with a number**',
|
||||
'1 · 会话列表': '1 · Sessions',
|
||||
'2 · 工作区': '2 · Workspace',
|
||||
'3 · 新会话': '3 · New session',
|
||||
'4 · 状态': '4 · Status',
|
||||
'5 · 帮助': '5 · Help',
|
||||
'**6 · 修复卡片按钮**(请直接回复数字 **6**)':
|
||||
'**6 · Repair card buttons** (reply with the number **6**)',
|
||||
'7 · 关注列表': '7 · Watch list',
|
||||
'🧪 验证卡片按钮': '🧪 Verify card buttons',
|
||||
'授权已提交。请点击下方按钮;机器人真实收到回调后才会判定修复成功。':
|
||||
'Authorization submitted. Tap the button below; the repair is confirmed only after the bot receives the real callback.',
|
||||
'完成验证': 'Finish verification',
|
||||
'**工作区**:{workspace}\n共 **{total}** 个会话{paging}':
|
||||
'**Workspace**: {workspace}\n**{total}** sessions in total{paging}',
|
||||
'(第 {page}/{pageCount} 页)': ' (page {page}/{pageCount})',
|
||||
'⭐取关': '⭐ Unwatch',
|
||||
'⭐关注': '⭐ Watch',
|
||||
'◀ 上一页': '◀ Previous',
|
||||
'下一页 ▶': 'Next ▶',
|
||||
'回复数字(1~N)绑定本页会话。': 'Reply with a number (1–N) to bind a session on this page.',
|
||||
'📂 会话列表': '📂 Session list',
|
||||
'当前 Host 上没有已登记的工作区。': 'No workspaces are registered on the current Host.',
|
||||
'回复数字切换工作区,或点击按钮:': 'Reply with a number to switch workspaces, or tap a button:',
|
||||
'🗂 工作区': '🗂 Workspace',
|
||||
'🤖 助手菜单(回复数字即可,无需记命令)':
|
||||
'🤖 Assistant menu (reply with a number — no need to memorize commands)',
|
||||
'1 · /sessionlist 列出会话(回复数字绑定)':
|
||||
'1 · /sessionlist List sessions (reply with a number to bind)',
|
||||
'2 · /workspacelist 列出工作区(回复数字切换)':
|
||||
'2 · /workspacelist List workspaces (reply with a number to switch)',
|
||||
'3 · /new 开启新会话': '3 · /new Start a new session',
|
||||
'4 · /status 连接状态': '4 · /status Connection status',
|
||||
'5 · /help 本帮助': '5 · /help This help',
|
||||
'6 · /repair 修复卡片按钮(请回复数字 6)':
|
||||
'6 · /repair Repair card buttons (reply with the number 6)',
|
||||
'7 · /watchlist 关注列表': '7 · /watchlist Watch list',
|
||||
'直接发送文字/图片即继续当前会话。':
|
||||
'Send text or an image directly to continue the current session.',
|
||||
'/session ID 或序号 绑定已有会话': '/session ID or index Bind an existing session',
|
||||
'/watch ID 或序号 关注会话(完成后推送)':
|
||||
'/watch ID or index Watch a session (push when done)',
|
||||
'/compact 压缩上下文': '/compact Compact the context',
|
||||
'/workspace 绝对路径 切换工作区': '/workspace <absolute path> Switch workspace',
|
||||
'/presetlist 列出可用 Agent Preset': '/presetlist List available Agent Presets',
|
||||
'任务完成会自动推送,回复数字或点按钮取消关注:':
|
||||
'Completion is pushed automatically. Reply with a number or tap a button to unwatch:',
|
||||
'👁 关注列表': '👁 Watch list',
|
||||
'已完成': 'Completed',
|
||||
'已停止': 'Stopped',
|
||||
'已中止': 'Aborted',
|
||||
'已取消': 'Cancelled',
|
||||
'已结束': 'Ended',
|
||||
'✅ 任务完成': '✅ Task complete',
|
||||
'**状态**:{reason}': '**Status**: {reason}',
|
||||
'打开会话列表': 'Open session list',
|
||||
'工作区': 'Workspace',
|
||||
'绑定该会话后可继续追问,输入文字即可。':
|
||||
'After binding this session you can keep asking; just type a message.',
|
||||
'当前没有关注的会话。\n`/watch <ID|序号>` 关注后,任务完成会自动推送。':
|
||||
'No sessions are being watched.\nWatch one with `/watch <ID|index>` and completion is pushed automatically.',
|
||||
|
||||
// feishu/feishu-channel.mjs
|
||||
'正在生成…': 'Generating…',
|
||||
'回答完成': 'Answer complete',
|
||||
'飞书机器人': 'Feishu bot',
|
||||
|
||||
// feishu/message-utils.mjs
|
||||
'飞书机器人缺少图片读取权限。请在飞书开放平台为该应用添加 im:message:readonly,发布新版本并完成必要的管理员审批后,再重新发送图片。':
|
||||
'The Feishu bot is missing image-read permission. Add im:message:readonly to the app in Feishu Open Platform, publish a new version, complete the required admin approval, then resend the image.',
|
||||
|
||||
// feishu/feishu-runtime.mjs — callback probe notices
|
||||
'✅ 修复完成:已实测收到 card.action.trigger,菜单按钮现在可用。':
|
||||
'✅ Repair complete: card.action.trigger was received in a real test; the menu buttons now work.',
|
||||
'⚠️ 修复验证超时:未收到测试卡按钮的 card.action.trigger,不能确认按钮已修复。请不要重复授权;先检查飞书开放平台的卡片回调配置,确认后再发送 /repair。':
|
||||
'⚠️ Repair verification timed out: no card.action.trigger from the test card button was received, so the buttons cannot be confirmed repaired. Do not authorize again; check the card-callback configuration in Feishu Open Platform first, then send /repair.',
|
||||
'⚠️ 修复验证失败:无法发送专用测试卡,不能确认 card.action.trigger 已恢复。请不要重复授权;先检查机器人消息权限和连接状态。':
|
||||
'⚠️ Repair verification failed: the dedicated test card could not be sent, so card.action.trigger cannot be confirmed restored. Do not authorize again; check the bot message permission and connection status first.',
|
||||
'⚠️ 修复验证中断:Runtime 已停止,未完成 card.action.trigger 实测,不能确认修复成功。请不要重复授权;先等待机器人恢复连接。':
|
||||
'⚠️ Repair verification interrupted: the Runtime stopped before the card.action.trigger test completed, so the repair cannot be confirmed. Do not authorize again; wait for the bot to reconnect.',
|
||||
};
|
||||
24
src/channels/shared/i18n-en/office.mjs
Normal file
24
src/channels/shared/i18n-en/office.mjs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// English translations (office area). Keys are exact Chinese literals passed to t().
|
||||
export default {
|
||||
'AI Office 拒绝了 Device Token。': 'AI Office rejected the Device Token.',
|
||||
'AI Office Connector Hook 尚未就绪。': 'The AI Office Connector Hook is not ready yet.',
|
||||
'AI Office Connector 协议版本不兼容。': 'The AI Office Connector protocol version is incompatible.',
|
||||
'本机暂时无法访问 AI Office。': 'This machine cannot reach AI Office right now.',
|
||||
'AI Office 连接已中断。': 'The AI Office connection was interrupted.',
|
||||
'本机 Harness 已停止这次执行。': 'The local Harness stopped this run.',
|
||||
'Office Job 已被领取、取消或结束。': 'The Office Job has already been claimed, cancelled, or finished.',
|
||||
'本机 Harness 未能完成任务;请检查 Harness 会话后重试。': 'The local Harness could not complete the task; check the Harness Session and try again.',
|
||||
'Office Job 引用了本机未配置的 Workspace/Preset alias。': 'The Office Job references a Workspace/Preset alias that is not configured locally.',
|
||||
'你正在本机 DeepSeek Harness 中继续一个来自 AI Office 的任务。': 'You are continuing an AI Office task inside the local DeepSeek Harness.',
|
||||
'只在当前 Workspace 内行动。完成后必须返回:结果摘要、改动文件、验证证据、未解决风险。': 'Act only within the current Workspace. When finished you must return: a result summary, changed files, verification evidence, and unresolved risks.',
|
||||
'## 本机 Instruction Preset': '## Local Instruction Preset',
|
||||
'## 本轮补充指令': '## Additional Instructions for This Round',
|
||||
'## Office 时间线': '## Office Timeline',
|
||||
'批准 {tool}': 'Approve {tool}',
|
||||
'Harness 工具': 'Harness tool',
|
||||
'Harness 需要 {count} 项补充信息': 'Harness needs {count} pieces of additional information',
|
||||
'已领取 Job,准备 Workspace alias:{alias}': 'Job claimed; preparing Workspace alias: {alias}',
|
||||
'Harness Session 已创建。': 'Harness Session created.',
|
||||
'正在使用 {tool}…': 'Using {tool}…',
|
||||
};
|
||||
|
||||
61
src/channels/shared/i18n-en/qq.mjs
Normal file
61
src/channels/shared/i18n-en/qq.mjs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// English translations (qq area). Keys are exact Chinese literals passed to t().
|
||||
export default {
|
||||
|
||||
'QQ 机器人已连接 DeepSeek Harness。':
|
||||
'The QQ bot is connected to DeepSeek Harness.',
|
||||
'回答较长,后续内容未能通过 QQ 完整发送,请回复“继续”。':
|
||||
'The answer is long and the rest could not be sent fully through QQ. Reply “继续” (continue) to receive it.',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
结果文件: 'Result file',
|
||||
'结果文件「{name}」已生成,但 QQ 今日文件上传额度已用完,请稍后重试。':
|
||||
'The result file "{name}" was generated, but QQ\'s daily file upload quota is used up. Please try again later.',
|
||||
|
||||
'结果文件「{name}」已生成,但当前 QQ 机器人没有文件消息权限。':
|
||||
'The result file "{name}" was generated, but the current QQ bot does not have file message permission.',
|
||||
'结果文件「{name}」超过当前 QQ 机器人可发送的文件大小,未发送。':
|
||||
'The result file "{name}" exceeds the file size the current QQ bot can send and was not sent.',
|
||||
'结果文件「{name}」为空,QQ 不允许发送空文件。':
|
||||
'The result file "{name}" is empty, and QQ does not allow sending empty files.',
|
||||
|
||||
'结果文件「{name}」暂时被 QQ 限流,未能发送,请稍后重试。':
|
||||
'The result file "{name}" was temporarily rate-limited by QQ and was not sent. Please try again later.',
|
||||
'结果文件「{name}」已生成,但 QQ 拒绝了该文件或文件消息。':
|
||||
'The result file "{name}" was generated, but QQ rejected the file or file message.',
|
||||
'结果文件「{name}」已生成,但暂时未能通过 QQ 发送,请稍后重试。':
|
||||
'The result file "{name}" was generated but could not be sent via QQ right now. Please try again later.',
|
||||
'QQ 机器人与 DeepSeek Harness 连接正常。':
|
||||
'The QQ bot is connected to DeepSeek Harness normally.',
|
||||
'QQ 交互问题发送失败。': 'Failed to send the QQ interaction question.',
|
||||
|
||||
|
||||
'QQ 机器人凭据缺失,请移除后重新扫码。':
|
||||
'The QQ bot credentials are missing. Remove the bot and scan the QR code again.',
|
||||
'QQ 连接未就绪,插件会自动重试。':
|
||||
'The QQ connection is not ready; the plugin will retry automatically.',
|
||||
'QQ 扫码服务暂时不可用,请重新生成二维码。':
|
||||
'The QQ QR service is temporarily unavailable. Please generate a new QR code.',
|
||||
'无法生成 QQ 二维码,请稍后重试。':
|
||||
'Unable to generate the QQ QR code. Please try again later.',
|
||||
'QQ 机器人已绑定,消息连接暂未就绪。':
|
||||
'The QQ bot is bound, but the message connection is not ready yet.',
|
||||
'QQ 连接仍未就绪,请稍后重试。':
|
||||
'The QQ connection is still not ready. Please try again later.',
|
||||
QQ机器人尚未连接: 'The QQ bot is not connected yet',
|
||||
'QQ 机器人({appId})': 'QQ bot ({appId})',
|
||||
QQ机器人: 'QQ bot',
|
||||
'QQ WebSocket 长连接运行正常': 'The QQ WebSocket long connection is running normally',
|
||||
'QQ 连接未就绪,插件会自动重试':
|
||||
'The QQ connection is not ready; the plugin will retry automatically',
|
||||
'QQ 连接当前离线': 'The QQ connection is currently offline',
|
||||
'QQ 已授权,但无法安全保存接入配置。':
|
||||
'QQ authorization succeeded, but the connection configuration could not be saved safely.',
|
||||
};
|
||||
|
||||
78
src/channels/shared/i18n-en/shared-a.mjs
Normal file
78
src/channels/shared/i18n-en/shared-a.mjs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// English translations (shared-a area). Keys are exact Chinese literals passed to t().
|
||||
export default {
|
||||
'这个问题已在其他客户端处理,无需再次回答。':
|
||||
'This question has already been answered from another client; no further reply is needed.',
|
||||
'任务已完成。': 'Task completed.',
|
||||
'结果文件': 'result file',
|
||||
'结果文件「{name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。':
|
||||
'The delivery result of the result file "{name}" could not be confirmed. Please check whether it already arrived in the chat before retrying.',
|
||||
'结果文件「{name}」已生成,但 Slack 应用缺少 files:write 权限。请更新 Manifest、重新安装应用并重新连接机器人后重试。':
|
||||
'The result file "{name}" was generated, but the Slack app is missing the files:write permission. Please update the Manifest, reinstall the app, reconnect the bot, and try again.',
|
||||
'结果文件「{name}」已生成,但机器人缺少 Discord 的 Send Messages、Attach Files 或 Read Message History 权限。':
|
||||
'The result file "{name}" was generated, but the bot is missing the Discord Send Messages, Attach Files, or Read Message History permission.',
|
||||
'结果文件「{name}」已生成,但 Telegram 不允许机器人在当前聊天发送文档,请检查聊天权限。':
|
||||
'The result file "{name}" was generated, but Telegram does not allow the bot to send documents in this chat. Please check the chat permissions.',
|
||||
'结果文件「{name}」已生成,但当前机器人没有文件发送权限,请检查渠道权限。':
|
||||
'The result file "{name}" was generated, but the bot does not have permission to send files. Please check the channel permissions.',
|
||||
'结果文件「{name}」超过当前渠道大小上限,未发送。':
|
||||
'The result file "{name}" exceeds the size limit of this channel and was not sent.',
|
||||
'结果文件「{name}」为空,未发送。':
|
||||
'The result file "{name}" is empty and was not sent.',
|
||||
'结果文件「{name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。':
|
||||
'The result file "{name}" cannot be read or prepared for sending right now. Please make sure the file is still accessible and try again.',
|
||||
'结果文件「{name}」暂时被当前渠道限流,未能发送,请稍后重试。':
|
||||
'The result file "{name}" was temporarily rate-limited by this channel and could not be sent. Please try again later.',
|
||||
'结果文件「{name}」已生成,但当前渠道拒绝了该文件或文件消息。':
|
||||
'The result file "{name}" was generated, but this channel rejected the file or file message.',
|
||||
'结果文件「{name}」已生成,但当前渠道暂时未能发送,请稍后重试。':
|
||||
'The result file "{name}" was generated, but this channel could not send it right now. Please try again later.',
|
||||
'消息处理失败,请稍后重试。': 'Failed to process the message. Please try again later.',
|
||||
'{label}机器人': '{label} bot',
|
||||
'目前支持文字和图片消息。': 'Only text and image messages are supported at the moment.',
|
||||
'目前支持文字、图片和文件消息。':
|
||||
'Only text, image, and file messages are supported at the moment.',
|
||||
'目前支持文字、图片、文件和语音转写消息。':
|
||||
'Only text, image, file, and voice-transcription messages are supported at the moment.',
|
||||
'目前支持文字、图片、文件,以及微信已转成文字的语音消息。':
|
||||
'Only text, image, file, and voice messages already transcribed to text by WeChat are supported at the moment.',
|
||||
'直接发送文字或图片即可继续当前会话。': 'Send text or an image directly to continue the current session.',
|
||||
'直接发送文字、图片或文件即可继续当前会话。':
|
||||
'Send text, an image, or a file directly to continue the current session.',
|
||||
'直接发送文字、图片、文件或带文字识别结果的语音即可继续当前会话。':
|
||||
'Send text, an image, a file, or a voice message already transcribed to text to continue the current session.',
|
||||
'{label}机器人已连接 DeepSeek Harness。': 'The {label} bot is connected to DeepSeek Harness.',
|
||||
'/new 开启一个全新会话': '/new Start a brand-new session',
|
||||
'/compact 压缩当前会话的较早上下文': '/compact Compact the earlier context of the current session',
|
||||
'/workspace 工作区绝对路径 切换工作区': '/workspace <absolute workspace path> Switch workspace',
|
||||
'/workspacelist 列出工作区绝对路径': '/workspacelist List absolute workspace paths',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题':
|
||||
'/sessionlist [workspace index or absolute path] List session IDs and titles',
|
||||
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话':
|
||||
'/session <Session ID or workspace index> Bind this chat to the specified session',
|
||||
'/models 按序号列出所有可用模型': '/models List all available models by index',
|
||||
'/model [序号或完整模型ID] 查看或切换当前会话模型':
|
||||
'/model [index or full model ID] Show or switch the model of the current session',
|
||||
'示例:先发 /models,再发 /model 2': 'Example: send /models first, then /model 2',
|
||||
'/presetlist 按序号列出可用 Agent Preset': '/presetlist List available Agent Presets by index',
|
||||
'/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset':
|
||||
'/preset [index or full ID] Show or set the Agent Preset of this bot',
|
||||
'纯数字 ID:/preset id:<ID>': 'Numeric-only ID: /preset id:<ID>',
|
||||
'/preset --default 跟随 Host 默认': '/preset --default Follow the Host default',
|
||||
'/stop 停止当前任务': '/stop Stop the current task',
|
||||
'/steer 补充指令 纠偏当前任务': '/steer <additional instruction> Steer the current task',
|
||||
'/status 检查连接状态': '/status Check the connection status',
|
||||
'/help 显示本帮助': '/help Show this help',
|
||||
'{label}机器人与 DeepSeek Harness 连接正常。':
|
||||
'The {label} bot is connected to DeepSeek Harness and working normally.',
|
||||
'已开启新会话。请发送你的问题。': 'A new session has started. Please send your question.',
|
||||
'正在使用{name}…': 'Using {name}…',
|
||||
'已停止。': 'Stopped.',
|
||||
'请用文字回答当前问题。': 'Please answer the current question with text.',
|
||||
'{label}交互问题发送失败。': 'Failed to send the {label} interaction question.',
|
||||
'回答提交失败。': 'Failed to submit the answer.',
|
||||
'回答提交失败,请重新发送当前问题的答案。':
|
||||
'Failed to submit the answer. Please resend your answer to the current question.',
|
||||
'检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。':
|
||||
'A pending question left over in this Session was detected. It has been safely cancelled, and your latest message is being processed.',
|
||||
};
|
||||
|
||||
216
src/channels/shared/i18n-en/shared-b.mjs
Normal file
216
src/channels/shared/i18n-en/shared-b.mjs
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
// English translations (shared-b area). Keys are exact Chinese literals passed to t().
|
||||
export default {
|
||||
// workspace-command.mjs
|
||||
'用法:/session Session ID 或当前工作区序号(/session N)':
|
||||
'Usage: /session Session ID or the session index in the current Workspace (/session N)',
|
||||
'用法:\n/sessionlist 列出当前工作区会话\n/sessionlist 工作区序号 按 /workspacelist 序号列出会话\n/sessionlist 工作区绝对路径 列出指定工作区会话':
|
||||
'Usage:\n/sessionlist List Sessions in the current Workspace\n/sessionlist Workspace index List Sessions by /workspacelist index\n/sessionlist Workspace absolute path List Sessions in the given Workspace',
|
||||
'工作区必须是绝对路径。\n{usage}': 'The Workspace must be an absolute path.\n{usage}',
|
||||
'工作区路径包含不支持的字符或长度超过限制。\n{usage}':
|
||||
'The Workspace path contains unsupported characters or exceeds the length limit.\n{usage}',
|
||||
'工作区路径不存在。\n{usage}': 'The Workspace path does not exist.\n{usage}',
|
||||
'工作区路径必须指向一个目录。\n{usage}': 'The Workspace path must point to a directory.\n{usage}',
|
||||
'用法:/workspacelist': 'Usage: /workspacelist',
|
||||
'当前机器人暂不支持列出工作区。': 'This bot does not support listing Workspaces yet.',
|
||||
'当前 Harness Host 上没有仍然存在的已登记工作区。':
|
||||
'No registered Workspaces still exist on the current Harness Host.',
|
||||
'当前 Harness Host 上存在的工作区({count}):':
|
||||
'Workspaces on the current Harness Host ({count}):',
|
||||
'(当前)': '(current)',
|
||||
'切换用法:/workspace 工作区绝对路径': 'To switch: /workspace Workspace absolute path',
|
||||
'查看会话:/sessionlist 工作区序号或绝对路径':
|
||||
'To view Sessions: /sessionlist Workspace index or absolute path',
|
||||
'机器人正在移除或已重新接入,无法列出原会话的工作区。':
|
||||
'The bot is being removed or has been reconnected; cannot list Workspaces of the original Session.',
|
||||
'暂时无法获取工作区列表,请稍后重试。':
|
||||
'Unable to get the Workspace list right now; please try again later.',
|
||||
'当前机器人没有可用的工作区。': 'This bot has no available Workspace.',
|
||||
'当前机器人暂不支持按序号选择工作区。':
|
||||
'This bot does not support selecting a Workspace by index yet.',
|
||||
'工作区序号不存在,请先执行 /workspacelist。':
|
||||
'The Workspace index does not exist; please run /workspacelist first.',
|
||||
'今天 {time}': 'Today {time}',
|
||||
'昨天 {time}': 'Yesterday {time}',
|
||||
'前天 {time}': 'The day before yesterday {time}',
|
||||
'{month}月{day}日 {time}': '{month}/{day} {time}',
|
||||
'{year}年{month}月{day}日': '{year}-{month}-{day}',
|
||||
'标题暂不可用': 'Title unavailable',
|
||||
'暂无标题': 'No title',
|
||||
'(已归档)': '(archived)',
|
||||
'工作区:{workspace}\n该工作区暂无会话。': 'Workspace: {workspace}\nThis Workspace has no Sessions yet.',
|
||||
'工作区:{workspace}': 'Workspace: {workspace}',
|
||||
'会话({count}):': 'Sessions ({count}):',
|
||||
'绑定用法:/session Session ID 或当前工作区序号(/session N)':
|
||||
'To bind: /session Session ID or the Session index in the current Workspace (/session N)',
|
||||
'绑定用法:/session Session ID\n提示:/session N 只按机器人当前工作区的序号绑定。':
|
||||
"To bind: /session Session ID\nNote: /session N binds by index within the bot's current Workspace only.",
|
||||
'当前机器人暂不支持列出工作区会话。':
|
||||
'This bot does not support listing Workspace Sessions yet.',
|
||||
'机器人正在移除或已重新接入,无法列出原会话的工作区会话。':
|
||||
'The bot is being removed or has been reconnected; cannot list Workspace Sessions of the original Session.',
|
||||
'暂时无法获取工作区会话列表,请稍后重试。':
|
||||
'Unable to get the Workspace Session list right now; please try again later.',
|
||||
'Session ID 格式无效。\n{usage}': 'Invalid Session ID format.\n{usage}',
|
||||
'未找到该会话,请先执行 /sessionlist 确认 Session ID。':
|
||||
'Session not found; please run /sessionlist to confirm the Session ID.',
|
||||
'子代理会话不能绑定到机器人对话,请选择普通会话。':
|
||||
'Sub-agent Sessions cannot be bound to a bot conversation; please choose a regular Session.',
|
||||
'该会话的工作区归属不明确,暂时无法绑定。':
|
||||
"The Session's Workspace ownership is ambiguous; cannot bind it right now.",
|
||||
'暂时无法读取该会话的信息,请稍后重试。':
|
||||
'Unable to read the Session info right now; please try again later.',
|
||||
'机器人正在移除或已重新接入,无法绑定原对话的会话。':
|
||||
'The bot is being removed or has been reconnected; cannot bind the Session of the original conversation.',
|
||||
'工作区或会话状态已发生变化,请重试。':
|
||||
'The Workspace or Session state has changed; please try again.',
|
||||
'暂时无法绑定会话,请稍后重试。': 'Unable to bind the Session right now; please try again later.',
|
||||
'当前机器人暂不支持按序号绑定,请使用 /session Session ID。':
|
||||
'This bot does not support binding by index yet; please use /session Session ID.',
|
||||
'会话序号不存在,请先执行 /sessionlist 查看序号。':
|
||||
'The Session index does not exist; please run /sessionlist to see the indexes.',
|
||||
'暂时无法获取会话列表,请稍后重试。':
|
||||
'Unable to get the Session list right now; please try again later.',
|
||||
'当前机器人暂不支持绑定已有会话。':
|
||||
'This bot does not support binding existing Sessions yet.',
|
||||
'当前消息缺少可绑定的会话上下文。':
|
||||
'The current message lacks a conversation context to bind to.',
|
||||
'当前聊天已绑定会话:': 'This chat is now bound to the Session:',
|
||||
'标题:{title}': 'Title: {title}',
|
||||
'归档:{archived}': 'Archived: {archived}',
|
||||
'是': 'Yes',
|
||||
'否': 'No',
|
||||
'用法:/workspace 工作区绝对路径': 'Usage: /workspace Workspace absolute path',
|
||||
'当前机器人暂不支持切换工作区。': 'This bot does not support switching Workspaces yet.',
|
||||
'工作区已切换为:{workspace}': 'Workspace switched to: {workspace}',
|
||||
'{message}\n用法:/workspace 工作区绝对路径': '{message}\nUsage: /workspace Workspace absolute path',
|
||||
'机器人正在移除或已重新接入,无法切换原会话的工作区。':
|
||||
'The bot is being removed or has been reconnected; cannot switch the Workspace of the original Session.',
|
||||
|
||||
// preset-command.mjs
|
||||
'用法:/presetlist(不带参数)': 'Usage: /presetlist (no arguments)',
|
||||
'用法:\n/preset 查看当前设置\n/preset <序号> 按最近一次 /presetlist 的序号选择\n/preset <ID> 按 Agent Preset ID 选择\n/preset id:<纯数字 ID> 选择纯数字 ID\n/preset --default 跟随 Host 默认':
|
||||
'Usage:\n/preset Show current settings\n/preset <index> Select by index from the latest /presetlist\n/preset <ID> Select by Agent Preset ID\n/preset id:<numeric ID> Select a purely numeric ID\n/preset --default Follow the Host default',
|
||||
'未设置或当前不可用': 'Not set or currently unavailable',
|
||||
'{id}(当前不可用)': '{id} (currently unavailable)',
|
||||
'跟随 Host 默认:{preset}': 'Following Host default: {preset}',
|
||||
'跟随 Host 默认(Host 默认当前不可用)': 'Following Host default (Host default currently unavailable)',
|
||||
'{id}(已不可用)': '{id} (no longer available)',
|
||||
'当前机器人用于新会话的 Agent Preset:': 'Agent Preset this bot uses for new Sessions:',
|
||||
'已有会话不会受此设置影响。': 'Existing Sessions are not affected by this setting.',
|
||||
'查看可用项:/presetlist': 'See available options: /presetlist',
|
||||
'恢复跟随 Host 默认:/preset --default': 'Restore following Host default: /preset --default',
|
||||
'Host 默认:{preset}': 'Host default: {preset}',
|
||||
'可用 Agent Preset({count}):': 'Available Agent Presets ({count}):',
|
||||
'当前没有可用 Agent Preset。': 'No Agent Presets are currently available.',
|
||||
'Host 默认': 'Host default',
|
||||
'当前选择': 'current selection',
|
||||
'当前生效': 'currently in effect',
|
||||
'选择:/preset <序号或 ID>': 'To select: /preset <index or ID>',
|
||||
'当前机器人用于新会话的 Agent Preset 已设置为:':
|
||||
'The Agent Preset this bot uses for new Sessions is now set to:',
|
||||
'已有会话不变。若当前聊天已有会话,请先发送 /new,再发送普通消息,才会使用新设置创建会话。':
|
||||
'Existing Sessions are unchanged. If this chat already has a Session, send /new first, then a regular message, to create a Session with the new setting.',
|
||||
'Agent Preset 序号无效,请先执行 /presetlist。':
|
||||
'Invalid Agent Preset index; please run /presetlist first.',
|
||||
'请先执行 /presetlist,再按列表序号选择 Agent Preset。':
|
||||
'Please run /presetlist first, then select an Agent Preset by list index.',
|
||||
'Agent Preset 序号不存在,请重新执行 /presetlist。':
|
||||
'The Agent Preset index does not exist; please run /presetlist again.',
|
||||
'Agent Preset ID 格式无效。\n{usage}': 'Invalid Agent Preset ID format.\n{usage}',
|
||||
'Agent Preset 不存在或当前不可用,请重新执行 /presetlist。':
|
||||
'The Agent Preset does not exist or is currently unavailable; please run /presetlist again.',
|
||||
'工作区或机器人状态已发生变化,请重试。':
|
||||
'The Workspace or bot state has changed; please try again.',
|
||||
'获取 Agent Preset 列表已取消。': 'Fetching the Agent Preset list was cancelled.',
|
||||
'获取 Agent Preset 设置已取消。': 'Fetching the Agent Preset settings was cancelled.',
|
||||
'Agent Preset 修改已取消。': 'The Agent Preset change was cancelled.',
|
||||
'暂时无法获取 Agent Preset 列表,请稍后重试。':
|
||||
'Unable to get the Agent Preset list right now; please try again later.',
|
||||
'暂时无法获取 Agent Preset 设置,请稍后重试。':
|
||||
'Unable to get the Agent Preset settings right now; please try again later.',
|
||||
'Agent Preset 修改失败,请稍后重试。': 'Failed to change the Agent Preset; please try again later.',
|
||||
'Agent Preset 命令仅支持纯文字,请移除图片后重试。':
|
||||
'Agent Preset commands support text only; please remove images and try again.',
|
||||
|
||||
// model-command.mjs
|
||||
'用法:/model <序号> 或 /model <provider>/<model>': 'Usage: /model <index> or /model <provider>/<model>',
|
||||
'用法:/models(不带参数)': 'Usage: /models (no arguments)',
|
||||
'模型序号无效:{input}': 'Invalid model index: {input}',
|
||||
'请发送 /models 查看并输入有效的正整数序号。':
|
||||
'Please send /models to view the list and enter a valid positive integer index.',
|
||||
'可用模型:': 'Available models:',
|
||||
'当前没有可用模型。': 'No models are currently available.',
|
||||
'以下模型提供方暂时不可用:': 'The following model providers are temporarily unavailable:',
|
||||
'切换模型:/model <序号>': 'To switch models: /model <index>',
|
||||
'当前模型:': 'Current model:',
|
||||
'查看全部模型:/models': 'See all models: /models',
|
||||
'当前聊天还没有会话。': 'This chat has no Session yet.',
|
||||
'查看模型:/models': 'View models: /models',
|
||||
'选择模型:/model <序号>': 'Select a model: /model <index>',
|
||||
'当前任务正在运行,请等待完成或先发送 /stop。':
|
||||
'A task is currently running; please wait for it to finish or send /stop first.',
|
||||
'当前聊天绑定的会话已不存在,请重试。':
|
||||
'The Session bound to this chat no longer exists; please try again.',
|
||||
'无法切换到该模型。模型当前不可用,或不支持当前会话中的图片。':
|
||||
'Cannot switch to that model. It is currently unavailable, or does not support the images in this Session.',
|
||||
'当前聊天绑定的会话已发生变化,请重试。':
|
||||
'The Session bound to this chat has changed; please try again.',
|
||||
'获取模型列表已取消。': 'Fetching the model list was cancelled.',
|
||||
'模型切换已取消。': 'The model switch was cancelled.',
|
||||
'暂时无法获取模型列表,请稍后重试。':
|
||||
'Unable to get the model list right now; please try again later.',
|
||||
'模型切换失败,请稍后重试。': 'Failed to switch the model; please try again later.',
|
||||
'模型命令仅支持纯文字,请移除图片后重试。':
|
||||
'Model commands support text only; please remove images and try again.',
|
||||
'当前任务正在等待你的回答或审批。': 'The current task is waiting for your answer or approval.',
|
||||
'请先处理当前请求,或者发送 /stop 停止任务。':
|
||||
'Please handle the current request first, or send /stop to stop the task.',
|
||||
'没有找到模型:{model}': 'Model not found: {model}',
|
||||
'请发送 /models 查看可用模型。': 'Please send /models to view available models.',
|
||||
'模型已切换为:\n{model}\n\n后续消息将使用该模型。':
|
||||
'Model switched to:\n{model}\n\nSubsequent messages will use this model.',
|
||||
|
||||
// compact-command.mjs
|
||||
'用法:/compact(不带参数)': 'Usage: /compact (no arguments)',
|
||||
'暂无可压缩的历史记录。': 'No compactable history yet.',
|
||||
'当前会话正在生成回复或执行压缩,请稍后重试。':
|
||||
'The current Session is generating a reply or compacting; please try again later.',
|
||||
'上下文压缩已取消。': 'Context compaction was cancelled.',
|
||||
'压缩期间会话历史发生变化,本次未修改会话,请重试。':
|
||||
'The Session history changed during compaction; the Session was not modified this time, please try again.',
|
||||
'未能生成有效的压缩摘要,本次未修改会话。':
|
||||
'Could not produce a useful compaction summary; the Session was not modified this time.',
|
||||
'上下文压缩未正常完成,部分会话历史可能已变化,请检查会话后再重试。':
|
||||
'Context compaction did not finish cleanly; some Session history may have changed. Please inspect the Session before retrying.',
|
||||
'上下文已压缩,但会话保存失败。': 'Context was compacted, but saving the Session failed.',
|
||||
'已压缩 {count} 条历史记录(约 {tokens} 个 token)。':
|
||||
'Compacted {count} history items (~{tokens} tokens).',
|
||||
'上下文压缩完成。': 'Context compaction completed.',
|
||||
'上下文压缩失败。': 'Context compaction failed.',
|
||||
'当前聊天绑定的会话已不存在,请发送新消息开启会话。':
|
||||
'The Session bound to this chat no longer exists; please send a new message to start a Session.',
|
||||
'当前会话正在生成回复,请稍后重试。':
|
||||
'The current Session is generating a reply; please try again later.',
|
||||
'当前 Harness 暂不支持从机器人执行上下文压缩。':
|
||||
'The current Harness does not support running context compaction from a bot yet.',
|
||||
'上下文压缩失败,请稍后重试。': 'Context compaction failed; please try again later.',
|
||||
'当前机器人没有可用的会话状态。': 'This bot has no available Session state.',
|
||||
'当前聊天还没有可压缩的会话,请先发送一条消息。':
|
||||
'This chat has no Session to compact yet; please send a message first.',
|
||||
'当前机器人暂不支持上下文压缩。': 'This bot does not support context compaction yet.',
|
||||
'当前 Harness 未注册 /compact 命令,请确认上下文压缩组件已启用。':
|
||||
'The current Harness has not registered the /compact command; please make sure the context compaction component is enabled.',
|
||||
|
||||
// control-command.mjs
|
||||
'用法:/stop(不带参数)': 'Usage: /stop (no arguments)',
|
||||
'用法:/steer <补充指令>': 'Usage: /steer <additional instruction>',
|
||||
'控制命令仅支持纯文字,请移除图片后重试。':
|
||||
'Control commands support text only; please remove images and try again.',
|
||||
'当前聊天没有正在运行的任务。': 'This chat has no running task.',
|
||||
'已请求停止当前任务。': 'Stop requested for the current task.',
|
||||
'当前聊天没有正在运行的任务,请直接发送普通消息。':
|
||||
'This chat has no running task; just send a regular message.',
|
||||
'已提交补充指令,Agent 会在下一步读取。':
|
||||
'Additional instruction submitted; the Agent will read it at the next step.',
|
||||
};
|
||||
|
||||
100
src/channels/shared/i18n-en/shared-c.mjs
Normal file
100
src/channels/shared/i18n-en/shared-c.mjs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// English translations (shared-c area). Keys are exact Chinese literals passed to t().
|
||||
export default {
|
||||
// harness-approval.mjs
|
||||
'请精准回复「批准」或「拒绝」(也支持:同意 / 不同意 / yes / no)。':
|
||||
'Please reply exactly with 「批准」 (approve) or 「拒绝」 (reject). Also accepted: 同意 / 不同意 / yes / no.',
|
||||
'请先完成当前问题,再精准回复「批准」或「拒绝」。':
|
||||
'Please finish the current question first, then reply exactly with 「批准」 (approve) or 「拒绝」 (reject).',
|
||||
'该审批已处理,无需再次回复。':
|
||||
'This approval has already been handled; no need to reply again.',
|
||||
'DeepSeek Harness 需要你的审批:':
|
||||
'DeepSeek Harness needs your approval:',
|
||||
'工具:{tool}': 'Tool: {tool}',
|
||||
'操作参数:': 'Operation parameters:',
|
||||
'原因:{reason}': 'Reason: {reason}',
|
||||
'群聊中请 @机器人 后发送审批决定。':
|
||||
'In group chats, please @ the bot before sending your approval decision.',
|
||||
'只有发起当前任务的用户可以处理这条审批。':
|
||||
'Only the user who started the current task can handle this approval.',
|
||||
'审批决定正在提交,请稍候。':
|
||||
'Your approval decision is being submitted; please wait.',
|
||||
'已批准,仅对本次操作有效。':
|
||||
'Approved — valid for this operation only.',
|
||||
'已拒绝此次操作。': 'This operation was rejected.',
|
||||
'无法完整展示这次操作,已安全拒绝此次审批。':
|
||||
'The operation could not be displayed in full, so this approval was safely rejected.',
|
||||
'审批提交失败,请重新回复「批准」或「拒绝」。':
|
||||
'Failed to submit the approval; please reply with 「批准」 (approve) or 「拒绝」 (reject) again.',
|
||||
|
||||
// harness-question.mjs
|
||||
'DeepSeek Harness 需要你补充信息{progress}:':
|
||||
'DeepSeek Harness needs more information{progress}:',
|
||||
'请输入你的回答。': 'Please enter your answer.',
|
||||
'请回复选项序号或文字;多选用逗号分隔,也可补充其他内容。':
|
||||
'Reply with option numbers or text; separate multiple choices with commas, or add anything else.',
|
||||
'请回复一个选项序号或文字,也可直接输入其他答案。':
|
||||
'Reply with an option number or its text, or type your own answer directly.',
|
||||
'请直接回复你的答案。': 'Please reply with your answer directly.',
|
||||
'群聊中请 @机器人 后发送答案。':
|
||||
'In group chats, please @ the bot before sending your answer.',
|
||||
|
||||
// image-prompt.mjs
|
||||
'当前模型不支持图片,请用 /models 查看可用模型,再用 /model <序号> 切换后重发。':
|
||||
'The current model does not support images. Use /models to list available models, switch with /model <number>, then resend.',
|
||||
'图片超过宿主允许的大小,请压缩后重试。':
|
||||
'The image exceeds the size allowed by the host; compress it and try again.',
|
||||
'图片分辨率过高,请压缩后重试。':
|
||||
'The image resolution is too high; compress it and try again.',
|
||||
'图片内容无效或格式不受支持,请重新发送。':
|
||||
'The image content is invalid or its format is unsupported; please resend it.',
|
||||
'未能读取图片内容,请重新发送。':
|
||||
'Could not read the image content; please resend it.',
|
||||
'图片格式与实际内容不一致,请重新发送。':
|
||||
'The image format does not match its actual content; please resend it.',
|
||||
'一次发送的图片数量超过宿主限制,请减少后重试。':
|
||||
'The number of images sent at once exceeds the host limit; send fewer and try again.',
|
||||
'图片总大小超过宿主限制,请减少图片或压缩后重试。':
|
||||
'The total image size exceeds the host limit; send fewer images or compress them and try again.',
|
||||
'图片下载地址发生了重定向,暂时无法读取。':
|
||||
'The image download URL redirected and cannot be read right now.',
|
||||
'图片下载失败(HTTP {status}),请重新发送后再试。':
|
||||
'Image download failed (HTTP {status}); please resend it and try again.',
|
||||
'图片超过 5 MB,请压缩后重试。':
|
||||
'The image is larger than 5 MB; compress it and try again.',
|
||||
'一次最多只能处理 {maxImages} 张图片。':
|
||||
'At most {maxImages} images can be processed at once.',
|
||||
'一次发送的图片总大小过大,请减少图片数量或压缩后重试。':
|
||||
'The total size of the images sent is too large; send fewer images or compress them and try again.',
|
||||
'图片下载失败,请重新发送后再试。':
|
||||
'Image download failed; please resend it and try again.',
|
||||
'暂不支持该图片格式,请发送 JPEG、PNG、WebP 或 GIF 图片。':
|
||||
'This image format is not supported yet; please send a JPEG, PNG, WebP, or GIF image.',
|
||||
|
||||
// connection-test.mjs
|
||||
'✅ DeepSeek Harness 连接测试成功':
|
||||
'✅ DeepSeek Harness connection test succeeded',
|
||||
'这条消息由插件页面中的“{name}”机器人卡片发出。':
|
||||
'This message was sent from the "{name}" bot card on the plugin page.',
|
||||
'{channelLabel}尚未收到可用于测试的私聊消息。':
|
||||
'The {channelLabel} has not received a direct message that can be used for testing yet.',
|
||||
'机器人': 'bot',
|
||||
|
||||
// editable-message-stream.mjs / harness-client.mjs / progress text
|
||||
'正在处理…': 'Processing…',
|
||||
'处理完成。': 'Processing complete.',
|
||||
'工具': 'tool',
|
||||
'正在整理结果…': 'Gathering results…',
|
||||
'_正在搜索网络并整理信息…_': '_Searching the web and gathering information…_',
|
||||
'_正在使用 {name}…_': '_Using {name}…_',
|
||||
|
||||
// image-prompt.mjs
|
||||
'请分析这张图片。': 'Analyze this image.',
|
||||
|
||||
// inbound-file.mjs
|
||||
'文件接收失败,请重新发送后再试。': 'File reception failed. Please resend it.',
|
||||
'文件下载失败,请重新发送后再试。': 'File download failed. Please resend it.',
|
||||
|
||||
// agent-preset.mjs
|
||||
'Agent Preset 无效。': 'Invalid Agent Preset.',
|
||||
};
|
||||
|
||||
28
src/channels/shared/i18n-en/slack.mjs
Normal file
28
src/channels/shared/i18n-en/slack.mjs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// English translations (slack area). Keys are exact Chinese literals passed to t().
|
||||
export default {
|
||||
'Slack机器人': 'Slack bot',
|
||||
'Slack机器人凭据缺失,请移除后重新接入。':
|
||||
'Slack bot credentials are missing. Remove the bot and connect it again.',
|
||||
'Slack Socket Mode 连接未就绪,插件会自动重试。':
|
||||
'The Slack Socket Mode connection is not ready yet. The plugin will retry automatically.',
|
||||
'Slack机器人已接入,Socket Mode 连接暂未就绪。':
|
||||
'The Slack bot is connected, but the Socket Mode connection is not ready yet.',
|
||||
'Slack Socket Mode 连接仍未就绪,请检查两个 Token。':
|
||||
'The Slack Socket Mode connection is still not ready. Check both tokens.',
|
||||
'Slack机器人尚未连接': 'The Slack bot is not connected yet',
|
||||
'Slack Socket Mode 长连接运行正常':
|
||||
'The Slack Socket Mode long-lived connection is running normally',
|
||||
'Slack连接未就绪,插件会自动重试':
|
||||
'The Slack connection is not ready. The plugin will retry automatically',
|
||||
'Slack连接当前离线': 'The Slack connection is currently offline',
|
||||
|
||||
'Slack 未授权机器人读取该文件。请为应用添加 files:read 后重新安装,再重新发送图片。':
|
||||
'Slack has not granted the bot access to this file. Add the files:read scope to the app, reinstall it, then send the image again.',
|
||||
'Slack Bot Token 必须以 xoxb- 开头。': 'The Slack Bot Token must start with xoxb-.',
|
||||
'Slack App Token 必须以 xapp- 开头。': 'The Slack App Token must start with xapp-.',
|
||||
'Slack Bot Token 没有返回完整的机器人身份。':
|
||||
'The Slack Bot Token did not return a complete bot identity.',
|
||||
'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.',
|
||||
};
|
||||
|
||||
22
src/channels/shared/i18n-en/telegram.mjs
Normal file
22
src/channels/shared/i18n-en/telegram.mjs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// English translations (telegram area). Keys are exact Chinese literals passed to t().
|
||||
export default {
|
||||
'开启一个全新会话': 'Start a brand-new Session',
|
||||
'压缩当前会话的较早上下文': 'Compact the earlier context of the current Session',
|
||||
'切换工作区': 'Switch Workspace',
|
||||
'列出工作区绝对路径': 'List Workspace absolute paths',
|
||||
'列出会话 ID 和标题': 'List Session IDs and titles',
|
||||
'将当前聊天绑定到指定会话': 'Bind the current chat to a specific Session',
|
||||
'按序号列出所有可用模型': 'List all available models by number',
|
||||
'查看或切换当前会话模型': 'View or switch the model of the current Session',
|
||||
'列出可用 Agent Preset': 'List available Agent Presets',
|
||||
'查看或设置新会话 Agent Preset': 'View or set the Agent Preset for new Sessions',
|
||||
'停止当前任务': 'Stop the current task',
|
||||
'纠偏当前任务': 'Steer the current task',
|
||||
'检查连接状态': 'Check connection status',
|
||||
'显示帮助': 'Show help',
|
||||
'该 Telegram 机器人已配置 Webhook,请先在原服务中移除 Webhook 后重试。':
|
||||
'This Telegram bot already has a Webhook configured. Remove the Webhook from the original service and try again.',
|
||||
'Telegram机器人': 'Telegram Bot',
|
||||
' Bot API 长轮询': ' Bot API long polling',
|
||||
};
|
||||
|
||||
39
src/channels/shared/i18n-en/wecom.mjs
Normal file
39
src/channels/shared/i18n-en/wecom.mjs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// English translations (wecom area). Keys are exact Chinese literals passed to t().
|
||||
export default {
|
||||
// Help text (wecom-bridge.mjs)
|
||||
'企业微信机器人已连接 DeepSeek Harness。': 'The Enterprise WeChat bot is connected to DeepSeek Harness.',
|
||||
|
||||
// Bot replies and status text (wecom-bridge.mjs)
|
||||
|
||||
'当前待处理图片较多,请稍后重新发送。': 'Too many images are pending. Please send them again later.',
|
||||
|
||||
'结果文件「{name}」已生成,但企业微信智能机器人缺少素材上传或文件消息能力,请检查机器人权限。': 'The result file "{name}" was generated, but the Enterprise WeChat bot lacks media upload or file message capability. Please check the bot permissions.',
|
||||
'结果文件「{name}」超过当前企业微信机器人可发送的文件大小,未发送。': 'The result file "{name}" exceeds the file size this Enterprise WeChat bot can send and was not sent.',
|
||||
'结果文件「{name}」为空,企业微信不允许发送空文件。': 'The result file "{name}" is empty. Enterprise WeChat does not allow sending empty files.',
|
||||
'结果文件「{name}」暂时被企业微信限流,未能发送,请稍后重试。': 'The result file "{name}" was rate-limited by Enterprise WeChat and could not be sent. Please try again later.',
|
||||
'结果文件「{name}」已生成,但企业微信拒绝了该文件或文件消息。': 'The result file "{name}" was generated, but Enterprise WeChat rejected the file or file message.',
|
||||
'结果文件「{name}」已生成,但暂时未能通过企业微信发送,请稍后重试。': 'The result file "{name}" was generated but could not be sent via Enterprise WeChat right now. Please try again later.',
|
||||
'任务已完成,但没有生成可显示的文本。': 'The task completed, but no displayable text was generated.',
|
||||
'目前支持文字、图片和语音转写消息。': 'Currently only text, images, and voice transcription messages are supported.',
|
||||
'企业微信机器人与 DeepSeek Harness 连接正常。': 'The connection between the Enterprise WeChat bot and DeepSeek Harness is working.',
|
||||
'正在思考中…': 'Thinking…',
|
||||
'企业微信交互问题发送失败。': 'Failed to send the Enterprise WeChat interaction question.',
|
||||
|
||||
// Provisioning, status, and errors (wecom-controller.mjs)
|
||||
'企业微信机器人凭据缺失,请移除后重新扫码。': 'The Enterprise WeChat bot credentials are missing. Please remove the bot and scan the QR code again.',
|
||||
'企业微信连接未就绪,插件会自动重试。': 'The Enterprise WeChat connection is not ready. The plugin will retry automatically.',
|
||||
'扫码绑定已取消。': 'QR code binding has been cancelled.',
|
||||
'无法生成企业微信二维码,请稍后重试。': 'Unable to generate the Enterprise WeChat QR code. Please try again later.',
|
||||
'企业微信二维码已过期,请重新生成。': 'The Enterprise WeChat QR code has expired. Please generate a new one.',
|
||||
'企业微信机器人已绑定,消息连接暂未就绪。': 'The Enterprise WeChat bot is bound, but the message connection is not ready yet.',
|
||||
'企业微信连接仍未就绪,请稍后重试。': 'The Enterprise WeChat connection is still not ready. Please try again later.',
|
||||
'企业微信扫码没有完成,请重新生成二维码。': 'The Enterprise WeChat QR scan was not completed. Please generate a new QR code.',
|
||||
'企业微信扫码服务暂时不可用,请重新生成二维码。': 'The Enterprise WeChat QR service is temporarily unavailable. Please generate a new QR code.',
|
||||
'企业微信已授权,但无法安全保存接入配置。': 'Enterprise WeChat authorization succeeded, but the connection configuration could not be saved safely.',
|
||||
'企业微信机器人': 'Enterprise WeChat bot',
|
||||
'企业微信机器人({botId})': 'Enterprise WeChat bot ({botId})',
|
||||
'企业微信 WebSocket 长连接运行正常': 'The Enterprise WeChat WebSocket long connection is running normally',
|
||||
'企业微信连接未就绪,插件会自动重试': 'The Enterprise WeChat connection is not ready. The plugin will retry automatically',
|
||||
'企业微信连接当前离线': 'The Enterprise WeChat connection is currently offline',
|
||||
};
|
||||
|
||||
81
src/channels/shared/i18n-en/weixin.mjs
Normal file
81
src/channels/shared/i18n-en/weixin.mjs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// English translations (weixin area). Keys are exact Chinese literals passed to t().
|
||||
export default {
|
||||
// weixin-bridge.mjs
|
||||
'微信已连接 DeepSeek Harness。': 'WeChat is connected to DeepSeek Harness.',
|
||||
'结果文件「{name}」已生成,但微信机器人当前没有文件消息发送权限,请检查机器人文件消息能力。': 'The result file "{name}" was generated, but the WeChat bot currently has no permission to send file messages. Please check the bot\'s file messaging capability.',
|
||||
'结果文件「{name}」超过当前微信会话可发送的文件大小,未发送。': 'The result file "{name}" exceeds the file size limit of the current WeChat conversation and was not sent.',
|
||||
'结果文件「{name}」暂时被微信限流,未能发送,请稍后重试。': 'The result file "{name}" was temporarily rate-limited by WeChat and was not sent. Please try again later.',
|
||||
'结果文件「{name}」已生成,但微信拒绝了该文件消息。': 'The result file "{name}" was generated, but WeChat rejected the file message.',
|
||||
'结果文件「{name}」已生成,但暂时未能通过微信发送,请稍后重试。': 'The result file "{name}" was generated but could not be sent via WeChat right now. Please try again later.',
|
||||
'目前支持文字、图片,以及微信已转成文字的语音消息。': 'Currently supports text, images, and voice messages that WeChat has converted to text.',
|
||||
'微信与 DeepSeek Harness 连接正常。': 'WeChat is connected to DeepSeek Harness normally.',
|
||||
'微信交互问题发送失败。': 'Failed to send the WeChat interaction question.',
|
||||
|
||||
// weixin-controller.mjs
|
||||
'微信已授权,但无法读取现有登录凭据。请检查 DSH 凭据存储。': 'WeChat was authorized, but the existing login credential could not be read. Please check the DSH credential store.',
|
||||
'微信已授权,但登录凭据无法写入 DSH 凭据存储。请检查凭据存储是否可写。': 'WeChat was authorized, but the login credential could not be written to the DSH credential store. Please check that the credential store is writable.',
|
||||
'微信已授权,但账号配置无法写入本机。请检查 DSH_HOME 目录权限。': 'WeChat was authorized, but the account configuration could not be written locally. Please check the DSH_HOME directory permissions.',
|
||||
'微信已授权,但无法初始化账号状态或工作区。请检查 DSH_HOME 和工作区目录。': 'WeChat was authorized, but the account state or workspace could not be initialized. Please check DSH_HOME and the workspace directories.',
|
||||
'微信已授权,但插件无法连接本机 Harness。请检查 dsh web 地址和端口。': 'WeChat was authorized, but the plugin could not connect to the local Harness. Please check the dsh web address and port.',
|
||||
'微信已授权,但 Harness 健康检查超时。请确认 dsh web 未阻塞。': 'WeChat was authorized, but the Harness health check timed out. Please confirm dsh web is not blocked.',
|
||||
'微信已授权,但 Harness 健康检查需要身份认证。请检查代理、网关或自定义鉴权配置。': 'WeChat was authorized, but the Harness health check requires authentication. Please check proxy, gateway, or custom auth configuration.',
|
||||
'微信已授权,但本机 Harness 请求被代理要求认证。请让回环地址绕过代理,并检查 NO_PROXY 配置。': 'WeChat was authorized, but the local Harness request was challenged for proxy authentication. Please bypass the proxy for loopback addresses and check the NO_PROXY configuration.',
|
||||
'微信已授权,但 Harness 异常拒绝了回环地址的健康检查。请检查 HTTP 代理、Harness 源码版本和构建产物。': 'WeChat was authorized, but Harness unexpectedly rejected the health check from a loopback address. Please check the HTTP proxy, Harness source version, and build artifacts.',
|
||||
'微信已授权,但 Harness 的 Host 信任检查拒绝了非回环地址请求。请检查 harnessBaseUrl 与 trustedHosts 配置。': 'WeChat was authorized, but the Harness Host trust check rejected a non-loopback request. Please check the harnessBaseUrl and trustedHosts configuration.',
|
||||
'微信已授权,但健康检查收到了非 Harness 标准的 403 拒绝响应。请检查代理或网关配置。': 'WeChat was authorized, but the health check received a non-standard Harness 403 rejection. Please check the proxy or gateway configuration.',
|
||||
'微信已授权,但找不到 Harness 健康检查接口。请确认 Harness 与插件版本兼容。': 'WeChat was authorized, but the Harness health check endpoint was not found. Please confirm the Harness and plugin versions are compatible.',
|
||||
'微信已授权,但 Harness 健康检查返回服务错误。请查看 dsh web 日志。': 'WeChat was authorized, but the Harness health check returned a server error. Please check the dsh web logs.',
|
||||
'微信已授权,但 Harness 返回了无法识别的响应。请确认 Harness 与插件版本兼容。': 'WeChat was authorized, but Harness returned an unrecognized response. Please confirm the Harness and plugin versions are compatible.',
|
||||
'微信已授权,但 Harness 拒绝了健康检查请求。请查看 dsh web 日志。': 'WeChat was authorized, but Harness rejected the health check request. Please check the dsh web logs.',
|
||||
'微信已授权,但 Harness 健康检查发生未知错误。请查看 dsh web 日志。': 'WeChat was authorized, but the Harness health check failed with an unknown error. Please check the dsh web logs.',
|
||||
'微信已授权,但消息连接初始化失败。请查看 dsh web 日志后重试。': 'WeChat was authorized, but the message connection failed to initialize. Please check the dsh web logs and try again.',
|
||||
'微信已授权,但激活过程中发生未知错误。请查看 dsh web 日志。': 'WeChat was authorized, but an unknown error occurred during activation. Please check the dsh web logs.',
|
||||
'登录凭据缺失,请移除账号后重新扫码。': 'The login credential is missing. Please remove the account and scan the QR code again.',
|
||||
'微信连接未就绪,插件会自动重试。': 'The WeChat connection is not ready; the plugin will retry automatically.',
|
||||
'微信连接未就绪,插件会自动重试': 'The WeChat connection is not ready; the plugin will retry automatically',
|
||||
'无法生成微信二维码,请稍后重试。': 'Unable to generate the WeChat QR code. Please try again later.',
|
||||
'微信连接仍未就绪,请稍后重试。': 'The WeChat connection is still not ready. Please try again later.',
|
||||
'微信机器人': 'WeChat Bot',
|
||||
'微信机器人({name})': 'WeChat Bot ({name})',
|
||||
'微信消息长轮询运行正常': 'WeChat message long-polling is running normally',
|
||||
'微信连接当前离线': 'The WeChat connection is currently offline',
|
||||
'配对码多次错误,请重新生成二维码。': 'The pairing code was entered incorrectly too many times. Please generate a new QR code.',
|
||||
'该微信账号已绑定,但本机没有可恢复的凭据。': 'This WeChat account is already bound, but no recoverable credential exists on this machine.',
|
||||
'微信授权成功,但返回的账号凭据不完整。': 'WeChat authorization succeeded, but the returned account credential is incomplete.',
|
||||
|
||||
// weixin-runtime.mjs
|
||||
'微信登录凭据已失效,请移除账号后重新扫码。': 'The WeChat login credential has expired. Please remove the account and scan the QR code again.',
|
||||
'微信消息同步请求被拒绝。': 'The WeChat message sync request was rejected.',
|
||||
|
||||
// weixin-api.mjs
|
||||
'微信图片的加密密钥无效。': 'The encryption key of the WeChat image is invalid.',
|
||||
'微信图片的加密数据无效。': 'The encrypted data of the WeChat image is invalid.',
|
||||
'微信图片解密失败。': 'Failed to decrypt the WeChat image.',
|
||||
'微信图片没有可用的下载地址。': 'The WeChat image has no usable download URL.',
|
||||
'微信图片的下载地址无效。': 'The download URL of the WeChat image is invalid.',
|
||||
'微信图片的下载地址不受信任。': 'The download URL of the WeChat image is not trusted.',
|
||||
'微信服务返回了无效的连接地址。': 'The WeChat service returned an invalid connection URL.',
|
||||
'微信服务返回了不受信任的连接地址。': 'The WeChat service returned an untrusted connection URL.',
|
||||
'微信服务没有返回扫码地址。': 'The WeChat service did not return a QR-code URL.',
|
||||
'微信服务返回了无效的扫码地址。': 'The WeChat service returned an invalid QR-code URL.',
|
||||
'微信服务返回了不受信任的扫码地址。': 'The WeChat service returned an untrusted QR-code URL.',
|
||||
'微信服务返回了无效的文件上传地址。': 'The WeChat service returned an invalid file upload URL.',
|
||||
'微信服务返回了不受信任的文件上传地址。': 'The WeChat service returned an untrusted file upload URL.',
|
||||
'微信服务没有返回文件上传地址。': 'The WeChat service did not return a file upload URL.',
|
||||
'微信文件上传被拒绝(HTTP {status})。': 'The WeChat file upload was rejected (HTTP {status}).',
|
||||
'微信文件上传失败(HTTP {status})。': 'The WeChat file upload failed (HTTP {status}).',
|
||||
'微信文件上传响应缺少下载参数。': 'The WeChat file upload response is missing the download parameter.',
|
||||
'微信文件上传失败。': 'The WeChat file upload failed.',
|
||||
'拒绝访问不受信任的微信服务地址。': 'Refusing to access an untrusted WeChat service URL.',
|
||||
'微信服务请求失败(HTTP {status})。': 'The WeChat service request failed (HTTP {status}).',
|
||||
'微信服务返回了无法解析的响应。': 'The WeChat service returned an unparseable response.',
|
||||
'微信服务请求超时。': 'The WeChat service request timed out.',
|
||||
'暂时无法访问微信服务。': 'The WeChat service is temporarily unreachable.',
|
||||
'微信服务返回了无法识别的扫码状态。': 'The WeChat service returned an unrecognized QR-code status.',
|
||||
'微信服务没有返回二维码令牌。': 'The WeChat service did not return a QR-code token.',
|
||||
'微信服务拒绝了回复消息。': 'The WeChat service rejected the reply message.',
|
||||
'微信服务拒绝了文件上传请求。': 'The WeChat service rejected the file upload request.',
|
||||
'微信服务拒绝了文件消息。': 'The WeChat service rejected the file message.',
|
||||
'微信账号连接启动失败。': 'Failed to start the WeChat account connection.',
|
||||
};
|
||||
|
||||
18
src/channels/shared/i18n-en/whatsapp.mjs
Normal file
18
src/channels/shared/i18n-en/whatsapp.mjs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// English translations (whatsapp area). Keys are exact Chinese literals passed to t().
|
||||
export default {
|
||||
'WhatsApp 关联设备已失效,请移除后重新扫码。': 'The WhatsApp linked device is no longer valid. Remove it and scan the QR code again.',
|
||||
'WhatsApp 连接未就绪,插件会自动重试。': 'The WhatsApp connection is not ready yet; the plugin will retry automatically.',
|
||||
'WhatsApp 连接仍未就绪,请稍后重试。': 'The WhatsApp connection is still not ready. Please try again later.',
|
||||
'WhatsApp机器人尚未连接': 'The WhatsApp bot is not connected yet',
|
||||
'WhatsApp机器人': 'WhatsApp bot',
|
||||
'WhatsApp Web 关联设备运行正常': 'The WhatsApp Web linked device is working normally',
|
||||
'WhatsApp 连接未就绪': 'The WhatsApp connection is not ready',
|
||||
'WhatsApp 连接当前离线': 'The WhatsApp connection is currently offline',
|
||||
'WhatsApp 已绑定,消息连接暂未就绪。': 'WhatsApp is linked, but the messaging connection is not ready yet.',
|
||||
'WhatsApp 已扫码,但无法保存关联设备。': 'The QR code was scanned, but the linked device could not be saved.',
|
||||
'无法连接 WhatsApp,请重新生成二维码。': 'Unable to connect to WhatsApp. Please generate a new QR code.',
|
||||
|
||||
'WhatsApp账号': 'WhatsApp account',
|
||||
' Web 关联设备': ' Web linked device',
|
||||
};
|
||||
|
||||
35
src/channels/shared/i18n.mjs
Normal file
35
src/channels/shared/i18n.mjs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// Host-side i18n for dsh-im. Mirrors the conventions of the settings-UI
|
||||
// translator in plugin-src/client/i18n.js: dictionary keys are the exact
|
||||
// Chinese source literals, and Chinese (zh) is the identity default, so
|
||||
// untranslated or unrecognized text always falls back to the original
|
||||
// Chinese output unchanged.
|
||||
//
|
||||
// The English dictionary lives in ./i18n-en.mjs to keep this module small.
|
||||
|
||||
import { EN } from './i18n-en.mjs';
|
||||
|
||||
let language = 'zh';
|
||||
|
||||
// Accepts 'en', 'en-US', 'english' (any case) as English; anything else
|
||||
// (including undefined and unrecognized values) selects Chinese.
|
||||
export function setImHostLanguage(lang) {
|
||||
const normalized = typeof lang === 'string' ? lang.trim().toLowerCase() : '';
|
||||
language = normalized === 'english' || normalized.startsWith('en') ? 'en' : 'zh';
|
||||
}
|
||||
|
||||
export function getImHostLanguage() {
|
||||
return language;
|
||||
}
|
||||
|
||||
// Translate a user-facing Chinese literal. In zh mode (the default) this is
|
||||
// the identity function. Optional `params` fills `{name}` placeholders in
|
||||
// both the Chinese key and its translation, e.g.
|
||||
// t('共 {count} 个机器人', { count: 3 })
|
||||
export function t(text, params) {
|
||||
if (typeof text !== 'string') return text;
|
||||
const translated = language === 'en' ? EN[text] ?? text : text;
|
||||
if (params == null) return translated;
|
||||
return translated.replace(/\{(\w+)\}/g, (match, name) =>
|
||||
Object.hasOwn(params, name) ? String(params[name]) : match,
|
||||
);
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import { t } from './i18n.mjs';
|
||||
|
||||
const DEFAULT_MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
||||
const DEFAULT_MAX_IMAGES = 20;
|
||||
const DEFAULT_MAX_TOTAL_IMAGE_BYTES = 20 * 1024 * 1024;
|
||||
|
|
@ -67,7 +69,7 @@ export async function fetchImageBuffer(url, {
|
|||
throw new ImagePromptError(
|
||||
'image-redirect-blocked',
|
||||
`Image download redirect was blocked (HTTP ${response.status})`,
|
||||
'图片下载地址发生了重定向,暂时无法读取。',
|
||||
t('图片下载地址发生了重定向,暂时无法读取。'),
|
||||
);
|
||||
}
|
||||
if (!response?.ok) {
|
||||
|
|
@ -75,7 +77,7 @@ export async function fetchImageBuffer(url, {
|
|||
throw new ImagePromptError(
|
||||
'image-http-error',
|
||||
`Image download failed with HTTP ${response?.status ?? 'unknown'}`,
|
||||
`图片下载失败(HTTP ${response?.status ?? 'unknown'}),请重新发送后再试。`,
|
||||
t('图片下载失败(HTTP {status}),请重新发送后再试。', { status: response?.status ?? 'unknown' }),
|
||||
);
|
||||
}
|
||||
const declaredLength = Number(response.headers?.get?.('content-length'));
|
||||
|
|
@ -84,7 +86,7 @@ export async function fetchImageBuffer(url, {
|
|||
throw new ImagePromptError(
|
||||
'image-too-large',
|
||||
`Image response declares ${declaredLength} bytes; the limit is ${maxBytes}`,
|
||||
'图片超过 5 MB,请压缩后重试。',
|
||||
t('图片超过 5 MB,请压缩后重试。'),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -99,7 +101,7 @@ export async function fetchImageBuffer(url, {
|
|||
throw new ImagePromptError(
|
||||
'image-too-large',
|
||||
`Image response exceeded ${maxBytes} bytes`,
|
||||
'图片超过 5 MB,请压缩后重试。',
|
||||
t('图片超过 5 MB,请压缩后重试。'),
|
||||
);
|
||||
}
|
||||
chunks.push(data);
|
||||
|
|
@ -112,7 +114,7 @@ export async function fetchImageBuffer(url, {
|
|||
throw new ImagePromptError(
|
||||
'image-too-large',
|
||||
`Image response contains ${data.length} bytes; the limit is ${maxBytes}`,
|
||||
'图片超过 5 MB,请压缩后重试。',
|
||||
t('图片超过 5 MB,请压缩后重试。'),
|
||||
);
|
||||
}
|
||||
return data;
|
||||
|
|
@ -192,7 +194,7 @@ export async function promptContentForMessage(message, {
|
|||
throw new ImagePromptError(
|
||||
'too-many-images',
|
||||
`Image message contains ${sources.length} images; the limit is ${maxImages}`,
|
||||
`一次最多只能处理 ${maxImages} 张图片。`,
|
||||
t('一次最多只能处理 {maxImages} 张图片。', { maxImages }),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -200,7 +202,7 @@ export async function promptContentForMessage(message, {
|
|||
const content = [];
|
||||
let totalImageBytes = 0;
|
||||
if (text) content.push({ type: 'text', text });
|
||||
else if (sources.length > 0) content.push({ type: 'text', text: DEFAULT_IMAGE_PROMPT });
|
||||
else if (sources.length > 0) content.push({ type: 'text', text: t(DEFAULT_IMAGE_PROMPT) });
|
||||
|
||||
for (const [index, source] of sources.entries()) {
|
||||
signal?.throwIfAborted();
|
||||
|
|
@ -208,14 +210,14 @@ export async function promptContentForMessage(message, {
|
|||
throw new ImagePromptError(
|
||||
'image-too-large',
|
||||
`Image ${index + 1} declares ${source.size} bytes; the limit is ${maxImageBytes}`,
|
||||
'图片超过 5 MB,请压缩后重试。',
|
||||
t('图片超过 5 MB,请压缩后重试。'),
|
||||
);
|
||||
}
|
||||
if (Number.isFinite(source?.size) && totalImageBytes + source.size > maxTotalImageBytes) {
|
||||
throw new ImagePromptError(
|
||||
'images-too-large',
|
||||
`Images declare more than ${maxTotalImageBytes} bytes in total`,
|
||||
'一次发送的图片总大小过大,请减少图片数量或压缩后重试。',
|
||||
t('一次发送的图片总大小过大,请减少图片数量或压缩后重试。'),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -230,7 +232,7 @@ export async function promptContentForMessage(message, {
|
|||
throw new ImagePromptError(
|
||||
'image-download-failed',
|
||||
`Unable to download image ${index + 1}: ${error?.message ?? String(error)}`,
|
||||
'图片下载失败,请重新发送后再试。',
|
||||
t('图片下载失败,请重新发送后再试。'),
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
|
|
@ -239,21 +241,21 @@ export async function promptContentForMessage(message, {
|
|||
throw new ImagePromptError(
|
||||
'invalid-image-data',
|
||||
`Image ${index + 1} returned no data`,
|
||||
'未能读取图片内容,请重新发送。',
|
||||
t('未能读取图片内容,请重新发送。'),
|
||||
);
|
||||
}
|
||||
if (loaded.data.length > maxImageBytes) {
|
||||
throw new ImagePromptError(
|
||||
'image-too-large',
|
||||
`Image ${index + 1} contains ${loaded.data.length} bytes; the limit is ${maxImageBytes}`,
|
||||
'图片超过 5 MB,请压缩后重试。',
|
||||
t('图片超过 5 MB,请压缩后重试。'),
|
||||
);
|
||||
}
|
||||
if (totalImageBytes + loaded.data.length > maxTotalImageBytes) {
|
||||
throw new ImagePromptError(
|
||||
'images-too-large',
|
||||
`Images contain more than ${maxTotalImageBytes} bytes in total`,
|
||||
'一次发送的图片总大小过大,请减少图片数量或压缩后重试。',
|
||||
t('一次发送的图片总大小过大,请减少图片数量或压缩后重试。'),
|
||||
);
|
||||
}
|
||||
totalImageBytes += loaded.data.length;
|
||||
|
|
@ -262,7 +264,7 @@ export async function promptContentForMessage(message, {
|
|||
throw new ImagePromptError(
|
||||
'unsupported-image-type',
|
||||
`Image ${index + 1} is not JPEG, PNG, GIF, or WebP`,
|
||||
'暂不支持该图片格式,请发送 JPEG、PNG、WebP 或 GIF 图片。',
|
||||
t('暂不支持该图片格式,请发送 JPEG、PNG、WebP 或 GIF 图片。'),
|
||||
);
|
||||
}
|
||||
content.push({
|
||||
|
|
@ -289,7 +291,7 @@ export function imagePromptDiagnostic(error) {
|
|||
}
|
||||
const reason = error.details.reason;
|
||||
const userMessage = Object.hasOwn(HOST_ATTACHMENT_USER_MESSAGES, reason)
|
||||
? HOST_ATTACHMENT_USER_MESSAGES[reason]
|
||||
? t(HOST_ATTACHMENT_USER_MESSAGES[reason])
|
||||
: null;
|
||||
return userMessage ? { code: 'attachment-error', reason, userMessage } : null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|||
import { isAbsolute, join, relative, resolve } from 'node:path';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
|
||||
import { t } from './i18n.mjs';
|
||||
|
||||
const FILES_DIRECTORY = join('.dsh-im', 'inbound');
|
||||
|
||||
export class InboundFileError extends Error {
|
||||
constructor(code, message, userMessage = '文件接收失败,请重新发送后再试。', options = {}) {
|
||||
constructor(code, message, userMessage = t('文件接收失败,请重新发送后再试。'), options = {}) {
|
||||
super(message, options);
|
||||
this.name = 'InboundFileError';
|
||||
this.code = code;
|
||||
|
|
@ -124,7 +126,7 @@ export async function stageInboundFiles(message, {
|
|||
throw new InboundFileError(
|
||||
'inbound-file-download-failed',
|
||||
`Unable to download inbound file ${index + 1}: ${error?.message ?? String(error)}`,
|
||||
'文件下载失败,请重新发送后再试。',
|
||||
t('文件下载失败,请重新发送后再试。'),
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
|
|
@ -152,7 +154,7 @@ export async function stageInboundFiles(message, {
|
|||
throw new InboundFileError(
|
||||
'inbound-file-download-failed',
|
||||
`Unable to stream inbound file ${index + 1}: ${error?.message ?? String(error)}`,
|
||||
'文件下载失败,请重新发送后再试。',
|
||||
t('文件下载失败,请重新发送后再试。'),
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { splitWorkspaceCommandMessage } from './workspace-command.mjs';
|
||||
import { t } from './i18n.mjs';
|
||||
import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
|
||||
import { withSessionBindingLock } from './session-binding-lock.mjs';
|
||||
|
||||
|
|
@ -109,9 +110,9 @@ function modelNumberRequest(requested) {
|
|||
|
||||
function invalidModelNumberMessage(requested) {
|
||||
return [
|
||||
`模型序号无效:${safeDisplayText(requested)}`,
|
||||
t('模型序号无效:{input}', { input: safeDisplayText(requested) }),
|
||||
'',
|
||||
'请发送 /models 查看并输入有效的正整数序号。',
|
||||
t('请发送 /models 查看并输入有效的正整数序号。'),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
|
|
@ -119,43 +120,43 @@ function formatCatalog(catalog) {
|
|||
const currentId = catalog.current
|
||||
? modelId(catalog.current.provider, catalog.current.model)
|
||||
: null;
|
||||
const lines = ['可用模型:'];
|
||||
const lines = [t('可用模型:')];
|
||||
let index = 0;
|
||||
if (catalog.groups.length === 0) lines.push('', '当前没有可用模型。');
|
||||
if (catalog.groups.length === 0) lines.push('', t('当前没有可用模型。'));
|
||||
for (const group of catalog.groups) {
|
||||
lines.push('', safeDisplayText(group.name) || safeDisplayText(group.id));
|
||||
for (const model of group.models) {
|
||||
index += 1;
|
||||
const id = modelId(group.id, model.id);
|
||||
lines.push(`${index}. ${safeDisplayText(id)}${id === currentId ? '(当前)' : ''}`);
|
||||
lines.push(`${index}. ${safeDisplayText(id)}${id === currentId ? t('(当前)') : ''}`);
|
||||
}
|
||||
}
|
||||
if (catalog.failures.length > 0) {
|
||||
lines.push('', '以下模型提供方暂时不可用:');
|
||||
lines.push('', t('以下模型提供方暂时不可用:'));
|
||||
for (const failure of catalog.failures) {
|
||||
lines.push(`- ${safeDisplayText(failure.name) || safeDisplayText(failure.id)}`);
|
||||
}
|
||||
}
|
||||
if (index > 0) lines.push('', '切换模型:/model <序号>');
|
||||
if (index > 0) lines.push('', t('切换模型:/model <序号>'));
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function currentModelMessage(current) {
|
||||
return [
|
||||
'当前模型:',
|
||||
t('当前模型:'),
|
||||
modelId(current.provider, current.model),
|
||||
'',
|
||||
'查看全部模型:/models',
|
||||
'切换模型:/model <序号>',
|
||||
t('查看全部模型:/models'),
|
||||
t('切换模型:/model <序号>'),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function noSessionMessage() {
|
||||
return [
|
||||
'当前聊天还没有会话。',
|
||||
t('当前聊天还没有会话。'),
|
||||
'',
|
||||
'查看模型:/models',
|
||||
'选择模型:/model <序号>',
|
||||
t('查看模型:/models'),
|
||||
t('选择模型:/model <序号>'),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
|
|
@ -166,26 +167,26 @@ function errorCode(error) {
|
|||
function modelErrorMessage(error, action) {
|
||||
const code = errorCode(error);
|
||||
if (code === 'agent-busy') {
|
||||
return '当前任务正在运行,请等待完成或先发送 /stop。';
|
||||
return t('当前任务正在运行,请等待完成或先发送 /stop。');
|
||||
}
|
||||
if (code === 'session-not-found') {
|
||||
return '当前聊天绑定的会话已不存在,请重试。';
|
||||
return t('当前聊天绑定的会话已不存在,请重试。');
|
||||
}
|
||||
if (code === 'model-unavailable') {
|
||||
return '无法切换到该模型。模型当前不可用,或不支持当前会话中的图片。';
|
||||
return t('无法切换到该模型。模型当前不可用,或不支持当前会话中的图片。');
|
||||
}
|
||||
if (code === WORKSPACE_SESSION_STALE || code === 'workspace-bot-not-found') {
|
||||
return '工作区或机器人状态已发生变化,请重试。';
|
||||
return t('工作区或机器人状态已发生变化,请重试。');
|
||||
}
|
||||
if (code === SESSION_BINDING_CHANGED) {
|
||||
return '当前聊天绑定的会话已发生变化,请重试。';
|
||||
return t('当前聊天绑定的会话已发生变化,请重试。');
|
||||
}
|
||||
if (code === 'cancelled' || error?.name === 'AbortError') {
|
||||
return action === 'list' ? '获取模型列表已取消。' : '模型切换已取消。';
|
||||
return action === 'list' ? t('获取模型列表已取消。') : t('模型切换已取消。');
|
||||
}
|
||||
return action === 'list'
|
||||
? '暂时无法获取模型列表,请稍后重试。'
|
||||
: '模型切换失败,请稍后重试。';
|
||||
? t('暂时无法获取模型列表,请稍后重试。')
|
||||
: t('模型切换失败,请稍后重试。');
|
||||
}
|
||||
|
||||
async function boundSession(harness, state, key, options) {
|
||||
|
|
@ -243,12 +244,12 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
|
|||
if (!isModelCommand(text)) return null;
|
||||
const command = text.trim();
|
||||
if (options.hasImages) {
|
||||
return commandResult('模型命令仅支持纯文字,请移除图片后重试。');
|
||||
return commandResult(t('模型命令仅支持纯文字,请移除图片后重试。'));
|
||||
}
|
||||
const requestOptions = rpcOptions(options.signal);
|
||||
|
||||
if (isModelsCommand(command)) {
|
||||
if (!/^\/models[ \t]*$/iu.test(command)) return commandResult(MODELS_USAGE);
|
||||
if (!/^\/models[ \t]*$/iu.test(command)) return commandResult(t(MODELS_USAGE));
|
||||
try {
|
||||
const bound = await boundSession(harness, state, key, requestOptions);
|
||||
const catalog = bound
|
||||
|
|
@ -261,7 +262,7 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
|
|||
}
|
||||
|
||||
const match = /^\/model(?:[ \t]+([^\s]+))?[ \t]*$/iu.exec(command);
|
||||
if (!match) return commandResult(MODEL_USAGE);
|
||||
if (!match) return commandResult(t(MODEL_USAGE));
|
||||
const requested = match[1];
|
||||
if (!requested) {
|
||||
try {
|
||||
|
|
@ -279,13 +280,13 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
|
|||
}
|
||||
if (!numberRequest
|
||||
&& (!requested.includes('/') || requested.startsWith('/') || requested.endsWith('/'))) {
|
||||
return commandResult(MODEL_USAGE);
|
||||
return commandResult(t(MODEL_USAGE));
|
||||
}
|
||||
if (options.pendingInteraction) {
|
||||
return commandResult([
|
||||
'当前任务正在等待你的回答或审批。',
|
||||
t('当前任务正在等待你的回答或审批。'),
|
||||
'',
|
||||
'请先处理当前请求,或者发送 /stop 停止任务。',
|
||||
t('请先处理当前请求,或者发送 /stop 停止任务。'),
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
|
|
@ -293,7 +294,7 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
|
|||
return await withSessionBindingLock(state, key, async () => {
|
||||
const bound = await boundSession(harness, state, key, requestOptions);
|
||||
if (bound && await sessionIsBusy(bound.session, options.control, requestOptions)) {
|
||||
return commandResult('当前任务正在运行,请等待完成或先发送 /stop。');
|
||||
return commandResult(t('当前任务正在运行,请等待完成或先发送 /stop。'));
|
||||
}
|
||||
|
||||
const catalog = bound
|
||||
|
|
@ -305,9 +306,9 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
|
|||
if (!selection) {
|
||||
if (numberRequest) return commandResult(invalidModelNumberMessage(requested));
|
||||
return commandResult([
|
||||
`没有找到模型:${safeDisplayText(requested)}`,
|
||||
t('没有找到模型:{model}', { model: safeDisplayText(requested) }),
|
||||
'',
|
||||
'请发送 /models 查看可用模型。',
|
||||
t('请发送 /models 查看可用模型。'),
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
|
|
@ -344,7 +345,10 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
|
|||
throw stale;
|
||||
}
|
||||
}
|
||||
return commandResult(`模型已切换为:\n${modelId(selection.provider, selection.model)}\n\n后续消息将使用该模型。`);
|
||||
return commandResult(t(`模型已切换为:
|
||||
{model}
|
||||
|
||||
后续消息将使用该模型。`, { model: modelId(selection.provider, selection.model) }));
|
||||
});
|
||||
} catch (error) {
|
||||
return commandResult(modelErrorMessage(error, 'select'));
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import {
|
|||
normalizeAgentPresetCatalog,
|
||||
normalizeAgentPresetId,
|
||||
} from './agent-preset.mjs';
|
||||
import { t } from './i18n.mjs';
|
||||
import { withSessionBindingLock } from './session-binding-lock.mjs';
|
||||
import { splitWorkspaceCommandMessage } from './workspace-command.mjs';
|
||||
import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
|
||||
|
|
@ -59,7 +60,7 @@ function normalizeSettings(value) {
|
|||
|
||||
function presetItemText(item) {
|
||||
const label = safeDisplayText(item.label) || item.id;
|
||||
return `${label}(${item.id})`;
|
||||
return t('{label}({id})', { label, id: item.id });
|
||||
}
|
||||
|
||||
function itemFor(catalog, id) {
|
||||
|
|
@ -67,11 +68,11 @@ function itemFor(catalog, id) {
|
|||
}
|
||||
|
||||
function defaultDescription(catalog) {
|
||||
if (!catalog.defaultId) return '未设置或当前不可用';
|
||||
if (!catalog.defaultId) return t('未设置或当前不可用');
|
||||
const item = itemFor(catalog, catalog.defaultId);
|
||||
return item
|
||||
? presetItemText(item)
|
||||
: `${catalog.defaultId}(当前不可用)`;
|
||||
: t('{id}(当前不可用)', { id: catalog.defaultId });
|
||||
}
|
||||
|
||||
function currentDescription(settings) {
|
||||
|
|
@ -79,63 +80,63 @@ function currentDescription(settings) {
|
|||
if (agentPreset === null) {
|
||||
const item = itemFor(catalog, catalog.defaultId);
|
||||
return item
|
||||
? `跟随 Host 默认:${presetItemText(item)}`
|
||||
: '跟随 Host 默认(Host 默认当前不可用)';
|
||||
? t('跟随 Host 默认:{preset}', { preset: presetItemText(item) })
|
||||
: t('跟随 Host 默认(Host 默认当前不可用)');
|
||||
}
|
||||
const item = itemFor(catalog, agentPreset);
|
||||
return item
|
||||
? presetItemText(item)
|
||||
: `${agentPreset}(已不可用)`;
|
||||
: t('{id}(已不可用)', { id: agentPreset });
|
||||
}
|
||||
|
||||
function formatCurrent(settings) {
|
||||
return [
|
||||
'当前机器人用于新会话的 Agent Preset:',
|
||||
t('当前机器人用于新会话的 Agent Preset:'),
|
||||
currentDescription(settings),
|
||||
'',
|
||||
'已有会话不会受此设置影响。',
|
||||
'查看可用项:/presetlist',
|
||||
'恢复跟随 Host 默认:/preset --default',
|
||||
t('已有会话不会受此设置影响。'),
|
||||
t('查看可用项:/presetlist'),
|
||||
t('恢复跟随 Host 默认:/preset --default'),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function formatList(settings) {
|
||||
const { agentPreset, agentPresetCatalog: catalog } = settings;
|
||||
const lines = [
|
||||
'当前机器人用于新会话的 Agent Preset:',
|
||||
t('当前机器人用于新会话的 Agent Preset:'),
|
||||
currentDescription(settings),
|
||||
'',
|
||||
`Host 默认:${defaultDescription(catalog)}`,
|
||||
t('Host 默认:{preset}', { preset: defaultDescription(catalog) }),
|
||||
'',
|
||||
`可用 Agent Preset(${catalog.items.length}):`,
|
||||
t('可用 Agent Preset({count}):', { count: catalog.items.length }),
|
||||
];
|
||||
if (catalog.items.length === 0) {
|
||||
lines.push('当前没有可用 Agent Preset。');
|
||||
lines.push(t('当前没有可用 Agent Preset。'));
|
||||
} else {
|
||||
catalog.items.forEach((item, index) => {
|
||||
const markers = [];
|
||||
if (item.id === catalog.defaultId) markers.push('Host 默认');
|
||||
if (item.id === agentPreset) markers.push('当前选择');
|
||||
if (agentPreset === null && item.id === catalog.defaultId) markers.push('当前生效');
|
||||
if (item.id === catalog.defaultId) markers.push(t('Host 默认'));
|
||||
if (item.id === agentPreset) markers.push(t('当前选择'));
|
||||
if (agentPreset === null && item.id === catalog.defaultId) markers.push(t('当前生效'));
|
||||
const annotation = markers.length > 0 ? `(${markers.join(',')})` : '';
|
||||
lines.push(`${index + 1}. ${presetItemText(item)}${annotation}`);
|
||||
});
|
||||
}
|
||||
lines.push(
|
||||
'',
|
||||
'选择:/preset <序号或 ID>',
|
||||
'纯数字 ID:/preset id:<ID>',
|
||||
'恢复跟随 Host 默认:/preset --default',
|
||||
t('选择:/preset <序号或 ID>'),
|
||||
t('纯数字 ID:/preset id:<ID>'),
|
||||
t('恢复跟随 Host 默认:/preset --default'),
|
||||
);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function formatUpdated(settings) {
|
||||
return [
|
||||
'当前机器人用于新会话的 Agent Preset 已设置为:',
|
||||
t('当前机器人用于新会话的 Agent Preset 已设置为:'),
|
||||
currentDescription(settings),
|
||||
'',
|
||||
'已有会话不变。若当前聊天已有会话,请先发送 /new,再发送普通消息,才会使用新设置创建会话。',
|
||||
t('已有会话不变。若当前聊天已有会话,请先发送 /new,再发送普通消息,才会使用新设置创建会话。'),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
|
|
@ -189,16 +190,16 @@ function presetFromSnapshot(state, key, requested) {
|
|||
if (!/^\d+$/u.test(requested)) return { numeric: false, id: null };
|
||||
const index = Number(requested);
|
||||
if (!Number.isSafeInteger(index) || index < 1) {
|
||||
return { numeric: true, error: 'Agent Preset 序号无效,请先执行 /presetlist。' };
|
||||
return { numeric: true, error: t('Agent Preset 序号无效,请先执行 /presetlist。') };
|
||||
}
|
||||
const snapshot = loadSnapshot(state, key);
|
||||
if (!snapshot) {
|
||||
return { numeric: true, error: '请先执行 /presetlist,再按列表序号选择 Agent Preset。' };
|
||||
return { numeric: true, error: t('请先执行 /presetlist,再按列表序号选择 Agent Preset。') };
|
||||
}
|
||||
const id = snapshot[index - 1];
|
||||
return id
|
||||
? { numeric: true, id }
|
||||
: { numeric: true, error: 'Agent Preset 序号不存在,请重新执行 /presetlist。' };
|
||||
: { numeric: true, error: t('Agent Preset 序号不存在,请重新执行 /presetlist。') };
|
||||
}
|
||||
|
||||
function errorCode(error) {
|
||||
|
|
@ -208,22 +209,23 @@ function errorCode(error) {
|
|||
function presetErrorMessage(error, action) {
|
||||
const code = errorCode(error);
|
||||
if (code === 'agent-preset-invalid') {
|
||||
return `Agent Preset ID 格式无效。\n${PRESET_USAGE}`;
|
||||
return t(`Agent Preset ID 格式无效。
|
||||
{usage}`, { usage: t(PRESET_USAGE) });
|
||||
}
|
||||
if (code === 'agent-preset-unavailable') {
|
||||
return 'Agent Preset 不存在或当前不可用,请重新执行 /presetlist。';
|
||||
return t('Agent Preset 不存在或当前不可用,请重新执行 /presetlist。');
|
||||
}
|
||||
if (code === WORKSPACE_SESSION_STALE || code === 'workspace-bot-not-found') {
|
||||
return '工作区或机器人状态已发生变化,请重试。';
|
||||
return t('工作区或机器人状态已发生变化,请重试。');
|
||||
}
|
||||
if (code === 'cancelled' || error?.name === 'AbortError') {
|
||||
if (action === 'list') return '获取 Agent Preset 列表已取消。';
|
||||
if (action === 'current') return '获取 Agent Preset 设置已取消。';
|
||||
return 'Agent Preset 修改已取消。';
|
||||
if (action === 'list') return t('获取 Agent Preset 列表已取消。');
|
||||
if (action === 'current') return t('获取 Agent Preset 设置已取消。');
|
||||
return t('Agent Preset 修改已取消。');
|
||||
}
|
||||
if (action === 'list') return '暂时无法获取 Agent Preset 列表,请稍后重试。';
|
||||
if (action === 'current') return '暂时无法获取 Agent Preset 设置,请稍后重试。';
|
||||
return 'Agent Preset 修改失败,请稍后重试。';
|
||||
if (action === 'list') return t('暂时无法获取 Agent Preset 列表,请稍后重试。');
|
||||
if (action === 'current') return t('暂时无法获取 Agent Preset 设置,请稍后重试。');
|
||||
return t('Agent Preset 修改失败,请稍后重试。');
|
||||
}
|
||||
|
||||
async function settings(harness, options) {
|
||||
|
|
@ -250,12 +252,12 @@ export async function runPresetCommand(text, harness, state, key, options = {})
|
|||
if (!isPresetCommand(text)) return null;
|
||||
const command = text.trim();
|
||||
if (options.hasImages) {
|
||||
return commandResult('Agent Preset 命令仅支持纯文字,请移除图片后重试。');
|
||||
return commandResult(t('Agent Preset 命令仅支持纯文字,请移除图片后重试。'));
|
||||
}
|
||||
const requestOptions = rpcOptions(options.signal);
|
||||
|
||||
if (PRESET_LIST_COMMAND.test(command)) {
|
||||
if (!/^\/presetlist[ \t]*$/iu.test(command)) return commandResult(PRESET_LIST_USAGE);
|
||||
if (!/^\/presetlist[ \t]*$/iu.test(command)) return commandResult(t(PRESET_LIST_USAGE));
|
||||
try {
|
||||
const current = await settings(harness, requestOptions);
|
||||
saveSnapshot(state, key, current.agentPresetCatalog.items);
|
||||
|
|
@ -266,7 +268,7 @@ export async function runPresetCommand(text, harness, state, key, options = {})
|
|||
}
|
||||
|
||||
const match = /^\/preset(?:[ \t]+([^\s]+))?[ \t]*$/iu.exec(command);
|
||||
if (!match) return commandResult(PRESET_USAGE);
|
||||
if (!match) return commandResult(t(PRESET_USAGE));
|
||||
const requested = match[1];
|
||||
if (!requested) {
|
||||
try {
|
||||
|
|
@ -290,7 +292,8 @@ export async function runPresetCommand(text, harness, state, key, options = {})
|
|||
selected = fromSnapshot.id;
|
||||
} else {
|
||||
selected = normalizeAgentPresetId(requested);
|
||||
if (!selected) return commandResult(`Agent Preset ID 格式无效。\n${PRESET_USAGE}`);
|
||||
if (!selected) return commandResult(t(`Agent Preset ID 格式无效。
|
||||
{usage}`, { usage: t(PRESET_USAGE) }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { t } from './i18n.mjs';
|
||||
import { runWorkspaceCommand } from './workspace-command.mjs';
|
||||
import { runCompactCommand } from './compact-command.mjs';
|
||||
import {
|
||||
|
|
@ -55,38 +56,38 @@ function canClaimInteractionReply(message, pending, senderId) {
|
|||
}
|
||||
|
||||
function artifactFailureText(fileName, error, descriptor) {
|
||||
const name = String(fileName ?? '结果文件')
|
||||
const name = String(fileName ?? t('结果文件'))
|
||||
.replace(/[\r\n]+/g, ' ')
|
||||
.trim()
|
||||
.slice(0, 255) || '结果文件';
|
||||
.slice(0, 255) || t('结果文件');
|
||||
switch (error?.code) {
|
||||
case 'artifact-delivery-uncertain':
|
||||
return `结果文件「${name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。`;
|
||||
return t('结果文件「{name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
|
||||
case 'artifact-permission-required':
|
||||
if (descriptor?.key === 'slack') {
|
||||
return `结果文件「${name}」已生成,但 Slack 应用缺少 files:write 权限。请更新 Manifest、重新安装应用并重新连接机器人后重试。`;
|
||||
return t('结果文件「{name}」已生成,但 Slack 应用缺少 files:write 权限。请更新 Manifest、重新安装应用并重新连接机器人后重试。', { name });
|
||||
}
|
||||
if (descriptor?.key === 'discord') {
|
||||
return `结果文件「${name}」已生成,但机器人缺少 Discord 的 Send Messages、Attach Files 或 Read Message History 权限。`;
|
||||
return t('结果文件「{name}」已生成,但机器人缺少 Discord 的 Send Messages、Attach Files 或 Read Message History 权限。', { name });
|
||||
}
|
||||
if (descriptor?.key === 'telegram') {
|
||||
return `结果文件「${name}」已生成,但 Telegram 不允许机器人在当前聊天发送文档,请检查聊天权限。`;
|
||||
return t('结果文件「{name}」已生成,但 Telegram 不允许机器人在当前聊天发送文档,请检查聊天权限。', { name });
|
||||
}
|
||||
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-invalid':
|
||||
case 'artifact-changed':
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -321,7 +322,7 @@ export class TextHarnessBridge {
|
|||
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
|
||||
this.#status.lastError = error?.message ?? String(error);
|
||||
this.#logger.error?.(`[dsh-im:${this.#descriptor.key}] failed to process a command:`, error);
|
||||
await this.#bot.sendText(target, '消息处理失败,请稍后重试。').catch(() => undefined);
|
||||
await this.#bot.sendText(target, t('消息处理失败,请稍后重试。')).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -329,7 +330,7 @@ export class TextHarnessBridge {
|
|||
return sendRememberedConnectionTest({
|
||||
state: this.#state,
|
||||
text,
|
||||
channelLabel: `${this.#descriptor.label}机器人`,
|
||||
channelLabel: t('{label}机器人', { label: this.#descriptor.label }),
|
||||
send: (target, message) => this.#bot.sendText(target, message),
|
||||
});
|
||||
}
|
||||
|
|
@ -384,38 +385,38 @@ export class TextHarnessBridge {
|
|||
const hasImages = hasInboundImages(message);
|
||||
const hasFiles = hasInboundFiles(message);
|
||||
if (!text && !hasImages && !hasFiles) {
|
||||
await this.#bot.sendText(target, '目前支持文字、图片和文件消息。');
|
||||
await this.#bot.sendText(target, t('目前支持文字、图片和文件消息。'));
|
||||
return;
|
||||
}
|
||||
const command = text.toLowerCase();
|
||||
if (!hasImages && !hasFiles && command === '/help') {
|
||||
await this.#bot.sendText(target, [
|
||||
`${this.#descriptor.label}机器人已连接 DeepSeek Harness。`,
|
||||
t('{label}机器人已连接 DeepSeek Harness。', { label: this.#descriptor.label }),
|
||||
'',
|
||||
'直接发送文字、图片或文件即可继续当前会话。',
|
||||
'/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 显示本帮助',
|
||||
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'));
|
||||
return;
|
||||
}
|
||||
if (!hasImages && !hasFiles && command === '/status') {
|
||||
await this.#harness.ensureRunning({ signal: this.#signal });
|
||||
await this.#bot.sendText(target, `${this.#descriptor.label}机器人与 DeepSeek Harness 连接正常。`);
|
||||
await this.#bot.sendText(target, t('{label}机器人与 DeepSeek Harness 连接正常。', { label: this.#descriptor.label }));
|
||||
return;
|
||||
}
|
||||
const workspaceCommand = !hasImages && !hasFiles
|
||||
|
|
@ -429,7 +430,7 @@ export class TextHarnessBridge {
|
|||
}
|
||||
if (!hasImages && !hasFiles && command === '/new') {
|
||||
await this.#state.clearSession(conversationKey);
|
||||
await this.#bot.sendText(target, '已开启新会话。请发送你的问题。');
|
||||
await this.#bot.sendText(target, t('已开启新会话。请发送你的问题。'));
|
||||
return;
|
||||
}
|
||||
const compactCommand = !hasImages && !hasFiles
|
||||
|
|
@ -487,7 +488,7 @@ export class TextHarnessBridge {
|
|||
control: { owner: this, key: conversationKey },
|
||||
onUpdate: stream ? async (update) => {
|
||||
const progress = update.type === 'text' ? update.text
|
||||
: update.type === 'tool' ? `正在使用${update.name}…` : update.text;
|
||||
: update.type === 'tool' ? t('正在使用{name}…', { name: update.name }) : update.text;
|
||||
if (progress) {
|
||||
const format = update.type === 'text' ? 'markdown' : 'plain';
|
||||
await stream.update(semanticStream
|
||||
|
|
@ -505,12 +506,11 @@ export class TextHarnessBridge {
|
|||
files: message.files,
|
||||
},
|
||||
});
|
||||
const visibleAnswer = !cleanText(answer) && artifacts.length > 0
|
||||
? FILE_ONLY_COMPLETION_TEXT
|
||||
const fileOnlyCompletion = !cleanText(answer) && artifacts.length > 0;
|
||||
const visibleAnswer = fileOnlyCompletion
|
||||
? t(FILE_ONLY_COMPLETION_TEXT)
|
||||
: answer;
|
||||
const answerFormat = visibleAnswer === FILE_ONLY_COMPLETION_TEXT && !cleanText(answer)
|
||||
? 'plain'
|
||||
: 'markdown';
|
||||
const answerFormat = fileOnlyCompletion ? 'plain' : 'markdown';
|
||||
let textDeliveryError = null;
|
||||
let textReceipt = null;
|
||||
if (stream) {
|
||||
|
|
@ -580,7 +580,7 @@ export class TextHarnessBridge {
|
|||
if (error?.code === 'turn-stopped') {
|
||||
if (stream) {
|
||||
try {
|
||||
await stream.finish('已停止。');
|
||||
await stream.finish(t('已停止。'));
|
||||
} catch {
|
||||
stream.cancel?.();
|
||||
}
|
||||
|
|
@ -639,7 +639,7 @@ export class TextHarnessBridge {
|
|||
}
|
||||
stream?.cancel?.();
|
||||
try {
|
||||
await this.#bot.sendText(target, '消息处理失败,请稍后重试。');
|
||||
await this.#bot.sendText(target, t('消息处理失败,请稍后重试。'));
|
||||
} catch (sendError) {
|
||||
this.#logger.error?.(
|
||||
`[dsh-im:${this.#descriptor.key}] failed to send the safe error reply:`,
|
||||
|
|
@ -682,7 +682,7 @@ export class TextHarnessBridge {
|
|||
const text = cleanText(message.content);
|
||||
if (!text || hasInboundImages(message) || hasInboundFiles(message)) {
|
||||
try {
|
||||
await this.#bot.sendText(target, '请用文字回答当前问题。');
|
||||
await this.#bot.sendText(target, t('请用文字回答当前问题。'));
|
||||
} catch (error) {
|
||||
this.#logger.error?.(
|
||||
`[dsh-im:${this.#descriptor.key}] failed to reject a non-text interaction reply:`,
|
||||
|
|
@ -710,7 +710,7 @@ export class TextHarnessBridge {
|
|||
try {
|
||||
await this.#presentInteraction(pending);
|
||||
} catch (error) {
|
||||
this.#status.lastError = `${this.#descriptor.label}交互问题发送失败。`;
|
||||
this.#status.lastError = t('{label}交互问题发送失败。', { label: this.#descriptor.label });
|
||||
this.#logger.error?.(
|
||||
`[dsh-im:${this.#descriptor.key}] failed to retry an interaction question:`,
|
||||
error,
|
||||
|
|
@ -748,7 +748,7 @@ export class TextHarnessBridge {
|
|||
try {
|
||||
await this.#presentInteraction(pending);
|
||||
} catch (error) {
|
||||
this.#status.lastError = `${this.#descriptor.label}交互问题发送失败。`;
|
||||
this.#status.lastError = t('{label}交互问题发送失败。', { label: this.#descriptor.label });
|
||||
this.#logger.error?.(
|
||||
`[dsh-im:${this.#descriptor.key}] failed to send the next interaction question:`,
|
||||
error,
|
||||
|
|
@ -774,7 +774,7 @@ export class TextHarnessBridge {
|
|||
this.#clearPendingInteraction(key, pending.interactionId);
|
||||
if (this.#signal?.aborted) return;
|
||||
try {
|
||||
await this.#bot.sendText(target, INTERACTION_RESOLVED_TEXT);
|
||||
await this.#bot.sendText(target, t(INTERACTION_RESOLVED_TEXT));
|
||||
} catch (sendError) {
|
||||
this.#logger.error?.(
|
||||
`[dsh-im:${this.#descriptor.key}] failed to send an expired interaction notice:`,
|
||||
|
|
@ -787,13 +787,13 @@ export class TextHarnessBridge {
|
|||
pending.submitting = false;
|
||||
pending.answers.pop();
|
||||
pending.index -= 1;
|
||||
this.#status.lastError = '回答提交失败。';
|
||||
this.#status.lastError = t('回答提交失败。');
|
||||
this.#logger.error?.(
|
||||
`[dsh-im:${this.#descriptor.key}] failed to answer a Harness interaction:`,
|
||||
error,
|
||||
);
|
||||
try {
|
||||
await this.#bot.sendText(target, '回答提交失败,请重新发送当前问题的答案。');
|
||||
await this.#bot.sendText(target, t('回答提交失败,请重新发送当前问题的答案。'));
|
||||
} catch (sendError) {
|
||||
this.#logger.error?.(
|
||||
`[dsh-im:${this.#descriptor.key}] failed to send an interaction retry notice:`,
|
||||
|
|
@ -840,7 +840,7 @@ export class TextHarnessBridge {
|
|||
try {
|
||||
await this.#bot.sendText(
|
||||
target,
|
||||
'检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
|
||||
t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'),
|
||||
);
|
||||
} catch (error) {
|
||||
this.#logger.error?.(
|
||||
|
|
@ -941,7 +941,7 @@ export class TextHarnessBridge {
|
|||
this.#status.lastMessageAt = new Date().toISOString();
|
||||
}
|
||||
try {
|
||||
await this.#bot.sendText(message.replyTarget, INTERACTION_RESOLVED_TEXT);
|
||||
await this.#bot.sendText(message.replyTarget, t(INTERACTION_RESOLVED_TEXT));
|
||||
} catch (error) {
|
||||
this.#logger.error?.(
|
||||
`[dsh-im:${this.#descriptor.key}] failed to send an expired interaction notice:`,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { realpath, stat } from 'node:fs/promises';
|
||||
import { isAbsolute, resolve } from 'node:path';
|
||||
|
||||
import { t } from './i18n.mjs';
|
||||
import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
|
||||
|
||||
const WORKSPACE_COMMAND = /^\/workspace(?:\s+([\s\S]+))?$/i;
|
||||
|
|
@ -60,28 +61,34 @@ async function existingWorkspacePaths(values) {
|
|||
|
||||
async function selectedWorkspacePath(value) {
|
||||
if (typeof value !== 'string' || !isAbsolute(value.trim())) {
|
||||
return { error: `工作区必须是绝对路径。\n${SESSION_LIST_USAGE}` };
|
||||
return { error: t(`工作区必须是绝对路径。
|
||||
{usage}`, { usage: t(SESSION_LIST_USAGE) }) };
|
||||
}
|
||||
const workspace = normalizedWorkspacePath(value.trim());
|
||||
if (!workspace) {
|
||||
return { error: `工作区路径包含不支持的字符或长度超过限制。\n${SESSION_LIST_USAGE}` };
|
||||
return { error: t(`工作区路径包含不支持的字符或长度超过限制。
|
||||
{usage}`, { usage: t(SESSION_LIST_USAGE) }) };
|
||||
}
|
||||
let info;
|
||||
try {
|
||||
info = await stat(workspace);
|
||||
} catch {
|
||||
return { error: `工作区路径不存在。\n${SESSION_LIST_USAGE}` };
|
||||
return { error: t(`工作区路径不存在。
|
||||
{usage}`, { usage: t(SESSION_LIST_USAGE) }) };
|
||||
}
|
||||
if (!info.isDirectory()) {
|
||||
return { error: `工作区路径必须指向一个目录。\n${SESSION_LIST_USAGE}` };
|
||||
return { error: t(`工作区路径必须指向一个目录。
|
||||
{usage}`, { usage: t(SESSION_LIST_USAGE) }) };
|
||||
}
|
||||
try {
|
||||
const canonical = normalizedWorkspacePath(await realpath(workspace));
|
||||
return canonical
|
||||
? { workspace: canonical }
|
||||
: { error: `工作区路径包含不支持的字符或长度超过限制。\n${SESSION_LIST_USAGE}` };
|
||||
: { error: t(`工作区路径包含不支持的字符或长度超过限制。
|
||||
{usage}`, { usage: t(SESSION_LIST_USAGE) }) };
|
||||
} catch {
|
||||
return { error: `工作区路径不存在。\n${SESSION_LIST_USAGE}` };
|
||||
return { error: t(`工作区路径不存在。
|
||||
{usage}`, { usage: t(SESSION_LIST_USAGE) }) };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -120,38 +127,38 @@ export function splitWorkspaceCommandMessage(message) {
|
|||
}
|
||||
|
||||
async function runWorkspaceListCommand(match, harness) {
|
||||
if (match[1]?.trim()) return commandResult('用法:/workspacelist');
|
||||
if (match[1]?.trim()) return commandResult(t('用法:/workspacelist'));
|
||||
if (typeof harness?.listWorkspaces !== 'function') {
|
||||
return commandResult('当前机器人暂不支持列出工作区。');
|
||||
return commandResult(t('当前机器人暂不支持列出工作区。'));
|
||||
}
|
||||
try {
|
||||
const { current, paths } = await workspacePathSnapshot(harness);
|
||||
if (paths.length === 0) {
|
||||
return commandResult('当前 Harness Host 上没有仍然存在的已登记工作区。');
|
||||
return commandResult(t('当前 Harness Host 上没有仍然存在的已登记工作区。'));
|
||||
}
|
||||
const lines = [
|
||||
`当前 Harness Host 上存在的工作区(${paths.length}):`,
|
||||
t('当前 Harness Host 上存在的工作区({count}):', { count: paths.length }),
|
||||
...paths.map((workspace, index) => (
|
||||
`${index + 1}. ${workspace}${workspace === current ? '(当前)' : ''}`
|
||||
`${index + 1}. ${workspace}${workspace === current ? t('(当前)') : ''}`
|
||||
)),
|
||||
'',
|
||||
'切换用法:/workspace 工作区绝对路径',
|
||||
'查看会话:/sessionlist 工作区序号或绝对路径',
|
||||
t('切换用法:/workspace 工作区绝对路径'),
|
||||
t('查看会话:/sessionlist 工作区序号或绝对路径'),
|
||||
];
|
||||
const message = lines.join('\n');
|
||||
return commandResult(message, splitWorkspaceCommandMessage(message));
|
||||
} catch (error) {
|
||||
if (error?.code === 'workspace-bot-not-found') {
|
||||
return commandResult('机器人正在移除或已重新接入,无法列出原会话的工作区。');
|
||||
return commandResult(t('机器人正在移除或已重新接入,无法列出原会话的工作区。'));
|
||||
}
|
||||
return commandResult('暂时无法获取工作区列表,请稍后重试。');
|
||||
return commandResult(t('暂时无法获取工作区列表,请稍后重试。'));
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveSessionListWorkspace(selector, harness) {
|
||||
if (!selector) {
|
||||
if (typeof harness?.currentWorkspace !== 'function') {
|
||||
return { error: '当前机器人没有可用的工作区。' };
|
||||
return { error: t('当前机器人没有可用的工作区。') };
|
||||
}
|
||||
const selected = await selectedWorkspacePath(harness.currentWorkspace());
|
||||
harness.assertWorkspaceScope?.();
|
||||
|
|
@ -160,12 +167,12 @@ export async function resolveSessionListWorkspace(selector, harness) {
|
|||
|
||||
if (/^\d+$/u.test(selector)) {
|
||||
if (typeof harness?.listWorkspaces !== 'function') {
|
||||
return { error: '当前机器人暂不支持按序号选择工作区。' };
|
||||
return { error: t('当前机器人暂不支持按序号选择工作区。') };
|
||||
}
|
||||
const { paths } = await workspacePathSnapshot(harness);
|
||||
const position = Number(selector);
|
||||
if (!Number.isSafeInteger(position) || position < 1 || position > paths.length) {
|
||||
return { error: '工作区序号不存在,请先执行 /workspacelist。' };
|
||||
return { error: t('工作区序号不存在,请先执行 /workspacelist。') };
|
||||
}
|
||||
return { workspace: paths[position - 1] };
|
||||
}
|
||||
|
|
@ -185,13 +192,13 @@ function formatSessionRelativeTime(value) {
|
|||
const startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
||||
const now = new Date();
|
||||
const dayDiff = Math.round((startOfDay(now) - startOfDay(date)) / 86_400_000);
|
||||
if (dayDiff === 0) return `今天 ${hm}`;
|
||||
if (dayDiff === 1) return `昨天 ${hm}`;
|
||||
if (dayDiff === 2) return `前天 ${hm}`;
|
||||
if (dayDiff === 0) return t('今天 {time}', { time: hm });
|
||||
if (dayDiff === 1) return t('昨天 {time}', { time: hm });
|
||||
if (dayDiff === 2) return t('前天 {time}', { time: hm });
|
||||
if (date.getFullYear() === now.getFullYear()) {
|
||||
return `${date.getMonth() + 1}月${date.getDate()}日 ${hm}`;
|
||||
return t('{month}月{day}日 {time}', { month: date.getMonth() + 1, day: date.getDate(), time: hm });
|
||||
}
|
||||
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`;
|
||||
return t('{year}年{month}月{day}日', { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() });
|
||||
}
|
||||
|
||||
function sessionListMessage(workspace, sessions, { currentWorkspace = false } = {}) {
|
||||
|
|
@ -199,22 +206,24 @@ function sessionListMessage(workspace, sessions, { currentWorkspace = false } =
|
|||
const sessionId = safeDisplayText(session?.sessionId);
|
||||
if (!sessionId) throw new TypeError('Harness returned an invalid session id');
|
||||
const title = session?.summaryAvailable === false
|
||||
? '标题暂不可用'
|
||||
: safeDisplayText(session?.title) || '暂无标题';
|
||||
? t('标题暂不可用')
|
||||
: safeDisplayText(session?.title) || t('暂无标题');
|
||||
const timeText = formatSessionRelativeTime(session?.time);
|
||||
const annotation = `${timeText ? ` · ${timeText}` : ''}${session?.archived === true ? '(已归档)' : ''}`;
|
||||
const annotation = `${timeText ? ` · ${timeText}` : ''}${session?.archived === true ? t('(已归档)') : ''}`;
|
||||
return `${title}${annotation}\n ID: ${sessionId}`;
|
||||
});
|
||||
if (rows.length === 0) return `工作区:${workspace}\n该工作区暂无会话。`;
|
||||
if (rows.length === 0) return t(`工作区:{workspace}
|
||||
该工作区暂无会话。`, { workspace });
|
||||
return [
|
||||
`工作区:${workspace}`,
|
||||
`会话(${rows.length}):`,
|
||||
t('工作区:{workspace}', { workspace }),
|
||||
t('会话({count}):', { count: rows.length }),
|
||||
'',
|
||||
...rows.map((row, index) => `${index + 1}. ${row}`),
|
||||
'',
|
||||
currentWorkspace
|
||||
? '绑定用法:/session Session ID 或当前工作区序号(/session N)'
|
||||
: '绑定用法:/session Session ID\n提示:/session N 只按机器人当前工作区的序号绑定。',
|
||||
? t('绑定用法:/session Session ID 或当前工作区序号(/session N)')
|
||||
: t(`绑定用法:/session Session ID
|
||||
提示:/session N 只按机器人当前工作区的序号绑定。`),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
|
|
@ -227,7 +236,7 @@ async function currentSessionListWorkspace(harness) {
|
|||
|
||||
async function runSessionListCommand(match, harness) {
|
||||
if (typeof harness?.listWorkspaceSessions !== 'function') {
|
||||
return commandResult('当前机器人暂不支持列出工作区会话。');
|
||||
return commandResult(t('当前机器人暂不支持列出工作区会话。'));
|
||||
}
|
||||
const selector = match[1]?.trim() ?? '';
|
||||
try {
|
||||
|
|
@ -246,36 +255,37 @@ async function runSessionListCommand(match, harness) {
|
|||
return commandResult(message, splitWorkspaceCommandMessage(message));
|
||||
} catch (error) {
|
||||
if (error?.code === 'workspace-bot-not-found') {
|
||||
return commandResult('机器人正在移除或已重新接入,无法列出原会话的工作区会话。');
|
||||
return commandResult(t('机器人正在移除或已重新接入,无法列出原会话的工作区会话。'));
|
||||
}
|
||||
return commandResult('暂时无法获取工作区会话列表,请稍后重试。');
|
||||
return commandResult(t('暂时无法获取工作区会话列表,请稍后重试。'));
|
||||
}
|
||||
}
|
||||
|
||||
function sessionBindErrorMessage(error) {
|
||||
if (error?.code === 'session-id-invalid') {
|
||||
return `Session ID 格式无效。\n${SESSION_BIND_USAGE}`;
|
||||
return t(`Session ID 格式无效。
|
||||
{usage}`, { usage: t(SESSION_BIND_USAGE) });
|
||||
}
|
||||
if (['session-not-registered', 'session-not-found'].includes(error?.code)) {
|
||||
return '未找到该会话,请先执行 /sessionlist 确认 Session ID。';
|
||||
return t('未找到该会话,请先执行 /sessionlist 确认 Session ID。');
|
||||
}
|
||||
if (error?.code === 'session-subagent-unsupported') {
|
||||
return '子代理会话不能绑定到机器人对话,请选择普通会话。';
|
||||
return t('子代理会话不能绑定到机器人对话,请选择普通会话。');
|
||||
}
|
||||
if (error?.code === 'session-workspace-ambiguous') {
|
||||
return '该会话的工作区归属不明确,暂时无法绑定。';
|
||||
return t('该会话的工作区归属不明确,暂时无法绑定。');
|
||||
}
|
||||
if (error?.code === 'session-summary-unavailable') {
|
||||
return '暂时无法读取该会话的信息,请稍后重试。';
|
||||
return t('暂时无法读取该会话的信息,请稍后重试。');
|
||||
}
|
||||
if (error?.code === 'workspace-bot-not-found') {
|
||||
return '机器人正在移除或已重新接入,无法绑定原对话的会话。';
|
||||
return t('机器人正在移除或已重新接入,无法绑定原对话的会话。');
|
||||
}
|
||||
if ([WORKSPACE_SESSION_STALE, 'agent-busy', 'session-conflict', 'workspace-conflict']
|
||||
.includes(error?.code)) {
|
||||
return '工作区或会话状态已发生变化,请重试。';
|
||||
return t('工作区或会话状态已发生变化,请重试。');
|
||||
}
|
||||
return '暂时无法绑定会话,请稍后重试。';
|
||||
return t('暂时无法绑定会话,请稍后重试。');
|
||||
}
|
||||
|
||||
async function runSessionBindCommand(command, harness, conversationKey) {
|
||||
|
|
@ -285,7 +295,7 @@ async function runSessionBindCommand(command, harness, conversationKey) {
|
|||
// 序号模式:把 /session N 解析成当前工作区会话列表中的第 N 个会话
|
||||
if (typeof harness?.listWorkspaceSessions !== 'function'
|
||||
|| typeof harness?.currentWorkspace !== 'function') {
|
||||
return commandResult('当前机器人暂不支持按序号绑定,请使用 /session Session ID。');
|
||||
return commandResult(t('当前机器人暂不支持按序号绑定,请使用 /session Session ID。'));
|
||||
}
|
||||
try {
|
||||
const selected = await selectedWorkspacePath(harness.currentWorkspace());
|
||||
|
|
@ -298,7 +308,7 @@ async function runSessionBindCommand(command, harness, conversationKey) {
|
|||
const position = Number(sessionId);
|
||||
if (!Number.isSafeInteger(position) || position < 1
|
||||
|| position > listed.sessions.length) {
|
||||
return commandResult('会话序号不存在,请先执行 /sessionlist 查看序号。');
|
||||
return commandResult(t('会话序号不存在,请先执行 /sessionlist 查看序号。'));
|
||||
}
|
||||
const selectedSessionId = listed.sessions[position - 1]?.sessionId;
|
||||
if (!validSessionId(selectedSessionId)) {
|
||||
|
|
@ -309,15 +319,15 @@ async function runSessionBindCommand(command, harness, conversationKey) {
|
|||
if (error?.code === 'workspace-bot-not-found') {
|
||||
return commandResult(sessionBindErrorMessage(error));
|
||||
}
|
||||
return commandResult('暂时无法获取会话列表,请稍后重试。');
|
||||
return commandResult(t('暂时无法获取会话列表,请稍后重试。'));
|
||||
}
|
||||
}
|
||||
if (!validSessionId(sessionId)) return commandResult(SESSION_BIND_USAGE);
|
||||
if (!validSessionId(sessionId)) return commandResult(t(SESSION_BIND_USAGE));
|
||||
if (typeof harness?.bindWorkspaceSession !== 'function') {
|
||||
return commandResult('当前机器人暂不支持绑定已有会话。');
|
||||
return commandResult(t('当前机器人暂不支持绑定已有会话。'));
|
||||
}
|
||||
if (typeof conversationKey !== 'string' || !conversationKey) {
|
||||
return commandResult('当前消息缺少可绑定的会话上下文。');
|
||||
return commandResult(t('当前消息缺少可绑定的会话上下文。'));
|
||||
}
|
||||
try {
|
||||
const bound = await harness.bindWorkspaceSession(conversationKey, sessionId);
|
||||
|
|
@ -327,13 +337,13 @@ async function runSessionBindCommand(command, harness, conversationKey) {
|
|||
if (!workspace || !boundSessionId) {
|
||||
throw new TypeError('Harness returned an invalid bound session');
|
||||
}
|
||||
const title = safeDisplayText(bound?.title) || '暂无标题';
|
||||
const title = safeDisplayText(bound?.title) || t('暂无标题');
|
||||
const message = [
|
||||
'当前聊天已绑定会话:',
|
||||
`工作区:${workspace}`,
|
||||
`标题:${title}`,
|
||||
t('当前聊天已绑定会话:'),
|
||||
t('工作区:{workspace}', { workspace }),
|
||||
t('标题:{title}', { title }),
|
||||
`ID:${boundSessionId}`,
|
||||
`归档:${bound?.archived === true ? '是' : '否'}`,
|
||||
t('归档:{archived}', { archived: bound?.archived === true ? t('是') : t('否') }),
|
||||
].join('\n');
|
||||
return commandResult(message, splitWorkspaceCommandMessage(message));
|
||||
} catch (error) {
|
||||
|
|
@ -355,20 +365,21 @@ export async function runWorkspaceCommand(text, harness, conversationKey) {
|
|||
if (!match) return null;
|
||||
const workspace = match[1]?.trim();
|
||||
if (!workspace) {
|
||||
return commandResult('用法:/workspace 工作区绝对路径');
|
||||
return commandResult(t('用法:/workspace 工作区绝对路径'));
|
||||
}
|
||||
if (typeof harness?.switchWorkspace !== 'function') {
|
||||
return commandResult('当前机器人暂不支持切换工作区。');
|
||||
return commandResult(t('当前机器人暂不支持切换工作区。'));
|
||||
}
|
||||
try {
|
||||
const current = await harness.switchWorkspace(workspace);
|
||||
return commandResult(`工作区已切换为:${current}`);
|
||||
return commandResult(t('工作区已切换为:{workspace}', { workspace: current }));
|
||||
} catch (error) {
|
||||
if (['workspace-not-absolute', 'workspace-not-found', 'workspace-not-directory'].includes(error?.code)) {
|
||||
return commandResult(`${error.message}\n用法:/workspace 工作区绝对路径`);
|
||||
return commandResult(t(`{message}
|
||||
用法:/workspace 工作区绝对路径`, { message: error.message }));
|
||||
}
|
||||
if (error?.code === 'workspace-bot-not-found') {
|
||||
return commandResult('机器人正在移除或已重新接入,无法切换原会话的工作区。');
|
||||
return commandResult(t('机器人正在移除或已重新接入,无法切换原会话的工作区。'));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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([]) });
|
||||
const BOT_ID_PATTERN = /^slack_[a-f0-9]{24}$/;
|
||||
const BOT_TOKEN_REF_PATTERN = /^DSH_SLACK_BOT_TOKEN_[A-F0-9]{24}$/;
|
||||
|
|
@ -27,7 +29,7 @@ export function maskSlackBotId(platformId) {
|
|||
const value = cleanString(platformId) ?? '';
|
||||
const [teamId, userId] = value.split(':');
|
||||
if (teamId && userId) return `${teamId.slice(0, 5)}••• · ${userId.slice(0, 5)}•••`;
|
||||
return value ? `${value.slice(0, 6)}••••` : 'Slack机器人';
|
||||
return value ? `${value.slice(0, 6)}••••` : t('Slack机器人');
|
||||
}
|
||||
|
||||
export class SlackConfigStore {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { fetchFileStream } from '../shared/file-download.mjs';
|
||||
import { fetchImageBuffer, ImagePromptError } from '../shared/image-prompt.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const DEFAULT_BASE_URL = 'https://slack.com/api/';
|
||||
const SLACK_FILE_HOST = 'files.slack.com';
|
||||
|
|
@ -430,7 +431,7 @@ export class SlackApi {
|
|||
throw new ImagePromptError(
|
||||
'slack-file-access-required',
|
||||
'Slack redirected a private file request to the workspace because file access was not granted',
|
||||
'Slack 未授权机器人读取该文件。请为应用添加 files:read 后重新安装,再重新发送图片。',
|
||||
t('Slack 未授权机器人读取该文件。请为应用添加 files:read 后重新安装,再重新发送图片。'),
|
||||
);
|
||||
}
|
||||
return response;
|
||||
|
|
@ -505,19 +506,19 @@ export class SlackApi {
|
|||
|
||||
export async function inspectSlackCredentials({ botToken, appToken }, options = {}) {
|
||||
if (!validSlackBotToken(botToken)) {
|
||||
const error = new TypeError('Slack Bot Token 必须以 xoxb- 开头。');
|
||||
const error = new TypeError(t('Slack Bot Token 必须以 xoxb- 开头。'));
|
||||
error.code = 'slack-invalid-bot-token';
|
||||
throw error;
|
||||
}
|
||||
if (!validSlackAppToken(appToken)) {
|
||||
const error = new TypeError('Slack App Token 必须以 xapp- 开头。');
|
||||
const error = new TypeError(t('Slack App Token 必须以 xapp- 开头。'));
|
||||
error.code = 'slack-invalid-app-token';
|
||||
throw error;
|
||||
}
|
||||
const api = new SlackApi({ botToken, appToken, ...options });
|
||||
const [identity, connection] = await Promise.all([api.authTest(), api.openConnection()]);
|
||||
if (!identity?.team_id || !identity?.user_id || !identity?.bot_id) {
|
||||
throw new Error('Slack Bot Token 没有返回完整的机器人身份。');
|
||||
throw new Error(t('Slack Bot Token 没有返回完整的机器人身份。'));
|
||||
}
|
||||
let socketUrl;
|
||||
try {
|
||||
|
|
@ -526,7 +527,7 @@ export async function inspectSlackCredentials({ botToken, appToken }, options =
|
|||
socketUrl = null;
|
||||
}
|
||||
if (!socketUrl || socketUrl.protocol !== 'wss:') {
|
||||
const error = new Error('Slack App Token 无法创建 Socket Mode 连接,请确认已启用 Socket Mode 和 connections:write。');
|
||||
const error = new Error(t('Slack App Token 无法创建 Socket Mode 连接,请确认已启用 Socket Mode 和 connections:write。'));
|
||||
error.code = 'slack-socket-mode';
|
||||
throw error;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { connectionTestMessage } from '../shared/connection-test.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
import { deriveSlackBotIdentity, maskSlackBotId } from './config-store.mjs';
|
||||
import { inspectSlackCredentials } from './slack-api.mjs';
|
||||
import { SLACK_DESCRIPTOR } from './slack-bridge.mjs';
|
||||
|
|
@ -60,7 +61,7 @@ export class SlackController {
|
|||
if (!resolved) {
|
||||
this.#errors.set(config.botId, safeError(
|
||||
'missing-token',
|
||||
'Slack机器人凭据缺失,请移除后重新接入。',
|
||||
t('Slack机器人凭据缺失,请移除后重新接入。'),
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
|
@ -70,7 +71,7 @@ export class SlackController {
|
|||
} catch (error) {
|
||||
this.#errors.set(config.botId, safeError(
|
||||
'connection-failed',
|
||||
'Slack Socket Mode 连接未就绪,插件会自动重试。',
|
||||
t('Slack Socket Mode 连接未就绪,插件会自动重试。'),
|
||||
));
|
||||
this.#logger.warn?.(
|
||||
`[dsh-im:slack] bot ${config.botId} failed to initialize:`,
|
||||
|
|
@ -135,7 +136,7 @@ export class SlackController {
|
|||
} catch (error) {
|
||||
this.#errors.set(identity.botId, safeError(
|
||||
'connection-failed',
|
||||
'Slack机器人已接入,Socket Mode 连接暂未就绪。',
|
||||
t('Slack机器人已接入,Socket Mode 连接暂未就绪。'),
|
||||
));
|
||||
this.#logger.warn?.(
|
||||
`[dsh-im:slack] bot ${identity.botId} credential connection failed:`,
|
||||
|
|
@ -159,7 +160,7 @@ export class SlackController {
|
|||
} catch (error) {
|
||||
this.#errors.set(botId, safeError(
|
||||
'connection-failed',
|
||||
'Slack Socket Mode 连接仍未就绪,请检查两个 Token。',
|
||||
t('Slack Socket Mode 连接仍未就绪,请检查两个 Token。'),
|
||||
));
|
||||
throw error;
|
||||
} finally {
|
||||
|
|
@ -175,13 +176,13 @@ export class SlackController {
|
|||
return this.#withBotTransition(botId, async () => {
|
||||
const runtime = this.#runtimes.get(botId);
|
||||
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
||||
const error = new Error('Slack机器人尚未连接');
|
||||
const error = new Error(t('Slack机器人尚未连接'));
|
||||
error.code = 'test-target-unavailable';
|
||||
throw error;
|
||||
}
|
||||
await runtime.sendConnectionTest(connectionTestMessage(
|
||||
`${config.name}(${maskSlackBotId(config.platformId)})`,
|
||||
'Slack机器人',
|
||||
t('Slack机器人'),
|
||||
));
|
||||
return { sent: true };
|
||||
});
|
||||
|
|
@ -243,9 +244,9 @@ export class SlackController {
|
|||
},
|
||||
health: {
|
||||
status: connected ? 'healthy' : state === 'error' ? 'error' : 'offline',
|
||||
summary: connected ? `Slack${SLACK_DESCRIPTOR.connectionLabel}运行正常`
|
||||
: state === 'error' ? 'Slack连接未就绪,插件会自动重试'
|
||||
: 'Slack连接当前离线',
|
||||
summary: connected ? t(`Slack${SLACK_DESCRIPTOR.connectionLabel}运行正常`)
|
||||
: state === 'error' ? t('Slack连接未就绪,插件会自动重试')
|
||||
: t('Slack连接当前离线'),
|
||||
lastCheckedAt: runtimeStatus?.lastCheckedAt ?? null,
|
||||
lastConnectedAt: runtimeStatus?.lastConnectedAt ?? null,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { splitMessageText } from '../shared/editable-message-stream.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
import { SlackApi } from './slack-api.mjs';
|
||||
import { createSlackBridgeStatus, SlackHarnessBridge } from './slack-bridge.mjs';
|
||||
|
||||
|
|
@ -217,7 +218,7 @@ async function createSlackMessageStream({ api, target, signal, logger }) {
|
|||
await inFlight?.catch(() => undefined);
|
||||
|
||||
const chunks = splitMessageText(text, SLACK_MESSAGE_LIMIT);
|
||||
const first = chunks[0] ?? '处理完成。';
|
||||
const first = chunks[0] ?? t('处理完成。');
|
||||
if (!broken && first.startsWith(appended)) {
|
||||
await appendInChunks(api, target, ts, first.slice(appended.length), signal);
|
||||
await api.stopStream({ channelId: target.channelId, ts, signal });
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { t } from '../shared/i18n.mjs';
|
||||
import {
|
||||
deriveTokenBotIdentity,
|
||||
maskPlatformId,
|
||||
|
|
@ -66,7 +67,7 @@ export function deriveTelegramBotIdentity(platformId) {
|
|||
}
|
||||
|
||||
export function maskTelegramBotId(platformId) {
|
||||
return maskPlatformId(platformId, 'Telegram机器人');
|
||||
return maskPlatformId(platformId, t('Telegram机器人'));
|
||||
}
|
||||
|
||||
export class TelegramConfigStore extends TokenBotConfigStore {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { fetchFileStream } from '../shared/file-download.mjs';
|
||||
import { fetchImageBuffer } from '../shared/image-prompt.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const DEFAULT_BASE_URL = 'https://api.telegram.org/';
|
||||
const TELEGRAM_FILE_HOSTS = Object.freeze(['api.telegram.org']);
|
||||
|
|
@ -394,7 +395,7 @@ export async function inspectTelegramToken(token, options = {}) {
|
|||
if (!bot?.id || bot?.is_bot !== true) throw new Error('Telegram token does not belong to a bot');
|
||||
return {
|
||||
platformId: String(bot.id),
|
||||
name: cleanString([bot.first_name, bot.last_name].filter(Boolean).join(' ')) ?? 'Telegram机器人',
|
||||
name: cleanString([bot.first_name, bot.last_name].filter(Boolean).join(' ')) ?? t('Telegram机器人'),
|
||||
username: cleanString(bot.username),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { randomInt } from 'node:crypto';
|
|||
|
||||
import { createEditableMessageStream, splitMessageText } from '../shared/editable-message-stream.mjs';
|
||||
import { createTextDeliveryBlock } from '../shared/semantic/delivery.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
import { COMMANDS_MENU_BUTTON, TelegramApi } from './telegram-api.mjs';
|
||||
import { createTelegramBridgeStatus, TelegramHarnessBridge } from './telegram-bridge.mjs';
|
||||
import {
|
||||
|
|
@ -15,20 +16,20 @@ import {
|
|||
} from './config-store.mjs';
|
||||
|
||||
export const TELEGRAM_COMMAND_MENU = Object.freeze([
|
||||
{ command: 'new', description: '开启一个全新会话' },
|
||||
{ command: 'compact', description: '压缩当前会话的较早上下文' },
|
||||
{ command: 'workspace', description: '切换工作区' },
|
||||
{ command: 'workspacelist', description: '列出工作区绝对路径' },
|
||||
{ command: 'sessionlist', description: '列出会话 ID 和标题' },
|
||||
{ command: 'session', description: '将当前聊天绑定到指定会话' },
|
||||
{ command: 'models', description: '按序号列出所有可用模型' },
|
||||
{ command: 'model', description: '查看或切换当前会话模型' },
|
||||
{ command: 'presetlist', description: '列出可用 Agent Preset' },
|
||||
{ command: 'preset', description: '查看或设置新会话 Agent Preset' },
|
||||
{ command: 'stop', description: '停止当前任务' },
|
||||
{ command: 'steer', description: '纠偏当前任务' },
|
||||
{ command: 'status', description: '检查连接状态' },
|
||||
{ command: 'help', description: '显示帮助' },
|
||||
{ command: 'new', description: t('开启一个全新会话') },
|
||||
{ command: 'compact', description: t('压缩当前会话的较早上下文') },
|
||||
{ command: 'workspace', description: t('切换工作区') },
|
||||
{ command: 'workspacelist', description: t('列出工作区绝对路径') },
|
||||
{ command: 'sessionlist', description: t('列出会话 ID 和标题') },
|
||||
{ command: 'session', description: t('将当前聊天绑定到指定会话') },
|
||||
{ command: 'models', description: t('按序号列出所有可用模型') },
|
||||
{ command: 'model', description: t('查看或切换当前会话模型') },
|
||||
{ command: 'presetlist', description: t('列出可用 Agent Preset') },
|
||||
{ command: 'preset', description: t('查看或设置新会话 Agent Preset') },
|
||||
{ command: 'stop', description: t('停止当前任务') },
|
||||
{ command: 'steer', description: t('纠偏当前任务') },
|
||||
{ command: 'status', description: t('检查连接状态') },
|
||||
{ command: 'help', description: t('显示帮助') },
|
||||
]);
|
||||
|
||||
function escaped(value) {
|
||||
|
|
@ -703,7 +704,7 @@ export class TelegramRuntime {
|
|||
}
|
||||
const webhook = await api.getWebhookInfo({ signal: controller.signal });
|
||||
if (typeof webhook?.url === 'string' && webhook.url) {
|
||||
const error = new Error('该 Telegram 机器人已配置 Webhook,请先在原服务中移除 Webhook 后重试。');
|
||||
const error = new Error(t('该 Telegram 机器人已配置 Webhook,请先在原服务中移除 Webhook 后重试。'));
|
||||
error.code = 'webhook-configured';
|
||||
throw error;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|||
import { dirname } from 'node:path';
|
||||
|
||||
import { normalizeWeixinApiBaseUrl } from './weixin-api.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const EMPTY_DOCUMENT = Object.freeze({ version: 1, accounts: Object.freeze([]) });
|
||||
|
||||
|
|
@ -32,7 +33,7 @@ export function deriveWeixinBotIdentity(accountId) {
|
|||
|
||||
export function maskWeixinAccountId(accountId) {
|
||||
const value = cleanString(accountId) ?? '';
|
||||
if (value.length <= 10) return value ? `${value.slice(0, 3)}•••` : '微信机器人';
|
||||
if (value.length <= 10) return value ? `${value.slice(0, 3)}•••` : t('微信机器人');
|
||||
return `${value.slice(0, 6)}••••${value.slice(-4)}`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,31 +43,32 @@ import {
|
|||
createDeliveryReceipt,
|
||||
providerMessageIdsFor,
|
||||
} from '../shared/semantic/delivery.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
||||
const GENERIC_PROCESSING_ERROR = '消息处理失败,请稍后重试。';
|
||||
const INTERACTION_RESOLVED_TEXT = () => t('这个问题已在其他客户端处理,无需再次回答。');
|
||||
const GENERIC_PROCESSING_ERROR = () => t('消息处理失败,请稍后重试。');
|
||||
|
||||
const HELP_TEXT = [
|
||||
'微信已连接 DeepSeek Harness。',
|
||||
const HELP_TEXT = () => [
|
||||
t('微信已连接 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 显示本帮助',
|
||||
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');
|
||||
|
||||
function conversationKey(userId) {
|
||||
|
|
@ -108,7 +109,7 @@ function canClaimInteractionReply(message, pending) {
|
|||
&& nonEmptyString(extractWeixinText(message));
|
||||
}
|
||||
|
||||
function safeMessageError(error, userMessage = GENERIC_PROCESSING_ERROR) {
|
||||
function safeMessageError(error, userMessage = GENERIC_PROCESSING_ERROR()) {
|
||||
const diagnostic = imagePromptDiagnostic(error);
|
||||
return {
|
||||
code: diagnostic?.code ?? 'message-processing-failed',
|
||||
|
|
@ -119,24 +120,24 @@ function safeMessageError(error, userMessage = GENERIC_PROCESSING_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) {
|
||||
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-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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -244,7 +245,7 @@ export class WeixinHarnessBridge {
|
|||
this.#status.lastError = error?.message ?? String(error);
|
||||
this.#status.lastMessageError = safeMessageError(error);
|
||||
this.#logger.error?.('[dsh-weixin] failed to process a command:', error);
|
||||
return this.#send(sender, GENERIC_PROCESSING_ERROR, contextToken, runId)
|
||||
return this.#send(sender, GENERIC_PROCESSING_ERROR(), contextToken, runId)
|
||||
.catch(() => undefined);
|
||||
}).finally(() => {
|
||||
this.#acceptedMessageIds.delete(messageId);
|
||||
|
|
@ -400,26 +401,26 @@ export class WeixinHarnessBridge {
|
|||
const hasImages = hasInboundImages(promptMessage);
|
||||
const hasFiles = hasInboundFiles(promptMessage);
|
||||
if (!text && !hasImages && !hasFiles) {
|
||||
await this.#send(sender, '目前支持文字、图片、文件,以及微信已转成文字的语音消息。', contextToken, runId);
|
||||
await this.#send(sender, t('目前支持文字、图片、文件,以及微信已转成文字的语音消息。'), contextToken, runId);
|
||||
await this.#state.markSeen(messageId);
|
||||
return;
|
||||
}
|
||||
|
||||
const command = text.trim().toLowerCase();
|
||||
if (!hasImages && !hasFiles && command === '/help') {
|
||||
await this.#send(sender, HELP_TEXT, contextToken, runId);
|
||||
await this.#send(sender, HELP_TEXT(), contextToken, runId);
|
||||
await this.#state.markSeen(messageId);
|
||||
return;
|
||||
}
|
||||
if (!hasImages && !hasFiles && command === '/status') {
|
||||
await this.#harness.ensureRunning({ signal: this.#signal });
|
||||
await this.#send(sender, '微信与 DeepSeek Harness 连接正常。', contextToken, runId);
|
||||
await this.#send(sender, t('微信与 DeepSeek Harness 连接正常。'), contextToken, runId);
|
||||
await this.#state.markSeen(messageId);
|
||||
return;
|
||||
}
|
||||
if (!hasImages && !hasFiles && command === '/new') {
|
||||
await this.#state.clearSession(key);
|
||||
await this.#send(sender, '已开启新会话。请发送你的问题。', contextToken, runId);
|
||||
await this.#send(sender, t('已开启新会话。请发送你的问题。'), contextToken, runId);
|
||||
await this.#state.markSeen(messageId);
|
||||
return;
|
||||
}
|
||||
|
|
@ -483,7 +484,7 @@ export class WeixinHarnessBridge {
|
|||
}
|
||||
const answerText = typeof answer === 'string' && answer.trim()
|
||||
? answer
|
||||
: artifacts.length > 0 ? '结果文件已生成。' : answer;
|
||||
: artifacts.length > 0 ? t('结果文件已生成。') : answer;
|
||||
let textDeliveryError = null;
|
||||
let textReceipt = null;
|
||||
try {
|
||||
|
|
@ -519,7 +520,7 @@ export class WeixinHarnessBridge {
|
|||
this.#status.lastError = error?.message ?? String(error);
|
||||
const userMessage = inboundFileUserMessage(error)
|
||||
?? imagePromptUserMessage(error)
|
||||
?? GENERIC_PROCESSING_ERROR;
|
||||
?? GENERIC_PROCESSING_ERROR();
|
||||
this.#status.lastMessageError = safeMessageError(error, userMessage);
|
||||
this.#logger.error?.('[dsh-weixin] failed to process an inbound message:', error);
|
||||
try {
|
||||
|
|
@ -557,7 +558,7 @@ export class WeixinHarnessBridge {
|
|||
if (!text || hasWeixinImageItems(message)) {
|
||||
await this.#send(
|
||||
expected.actor,
|
||||
'请用文字回答当前问题。',
|
||||
t('请用文字回答当前问题。'),
|
||||
contextToken,
|
||||
runId,
|
||||
);
|
||||
|
|
@ -569,7 +570,7 @@ export class WeixinHarnessBridge {
|
|||
if (claimed && (!pending || pending !== expected)) {
|
||||
await this.#send(
|
||||
expected.actor,
|
||||
INTERACTION_RESOLVED_TEXT,
|
||||
INTERACTION_RESOLVED_TEXT(),
|
||||
contextToken,
|
||||
runId,
|
||||
);
|
||||
|
|
@ -586,7 +587,7 @@ export class WeixinHarnessBridge {
|
|||
try {
|
||||
await this.#presentInteraction(pending);
|
||||
} catch {
|
||||
this.#status.lastError = '微信交互问题发送失败。';
|
||||
this.#status.lastError = t('微信交互问题发送失败。');
|
||||
this.#logger.error?.('[dsh-weixin] failed to retry an interaction question');
|
||||
pending.interaction.reconnect?.();
|
||||
return;
|
||||
|
|
@ -596,7 +597,7 @@ export class WeixinHarnessBridge {
|
|||
if (claimed && (!presentedPending || presentedPending !== expected)) {
|
||||
await this.#send(
|
||||
expected.actor,
|
||||
INTERACTION_RESOLVED_TEXT,
|
||||
INTERACTION_RESOLVED_TEXT(),
|
||||
contextToken,
|
||||
runId,
|
||||
).catch(() => undefined);
|
||||
|
|
@ -621,7 +622,7 @@ export class WeixinHarnessBridge {
|
|||
try {
|
||||
await this.#presentInteraction(pending);
|
||||
} catch {
|
||||
this.#status.lastError = '微信交互问题发送失败。';
|
||||
this.#status.lastError = t('微信交互问题发送失败。');
|
||||
this.#logger.error?.('[dsh-weixin] failed to send the next interaction question');
|
||||
pending.interaction.reconnect?.();
|
||||
}
|
||||
|
|
@ -646,7 +647,7 @@ export class WeixinHarnessBridge {
|
|||
this.#clearPendingInteraction(key, pending.interactionId);
|
||||
await this.#send(
|
||||
pending.actor,
|
||||
INTERACTION_RESOLVED_TEXT,
|
||||
INTERACTION_RESOLVED_TEXT(),
|
||||
pending.contextToken,
|
||||
pending.runId,
|
||||
).catch(() => undefined);
|
||||
|
|
@ -656,11 +657,11 @@ export class WeixinHarnessBridge {
|
|||
pending.submitting = false;
|
||||
pending.answers.pop();
|
||||
pending.index -= 1;
|
||||
this.#status.lastError = '回答提交失败。';
|
||||
this.#status.lastError = t('回答提交失败。');
|
||||
this.#logger.error?.('[dsh-weixin] failed to answer a Harness interaction');
|
||||
await this.#send(
|
||||
pending.actor,
|
||||
'回答提交失败,请重新发送当前问题的答案。',
|
||||
t('回答提交失败,请重新发送当前问题的答案。'),
|
||||
pending.contextToken,
|
||||
pending.runId,
|
||||
).catch(() => undefined);
|
||||
|
|
@ -706,7 +707,7 @@ export class WeixinHarnessBridge {
|
|||
});
|
||||
await this.#send(
|
||||
actor,
|
||||
'检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
|
||||
t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'),
|
||||
contextToken,
|
||||
runId,
|
||||
).catch(() => undefined);
|
||||
|
|
@ -792,7 +793,7 @@ export class WeixinHarnessBridge {
|
|||
this.#status.lastMessageAt = new Date().toISOString();
|
||||
await this.#send(
|
||||
nonEmptyString(message?.from_user_id),
|
||||
INTERACTION_RESOLVED_TEXT,
|
||||
INTERACTION_RESOLVED_TEXT(),
|
||||
nonEmptyString(message?.context_token) ?? undefined,
|
||||
nonEmptyString(message?.run_id) ?? undefined,
|
||||
).catch(() => undefined);
|
||||
|
|
@ -840,7 +841,7 @@ export class WeixinHarnessBridge {
|
|||
}
|
||||
await this.#send(
|
||||
nonEmptyString(message?.from_user_id),
|
||||
GENERIC_PROCESSING_ERROR,
|
||||
GENERIC_PROCESSING_ERROR(),
|
||||
nonEmptyString(message?.context_token) ?? undefined,
|
||||
nonEmptyString(message?.run_id) ?? undefined,
|
||||
).catch(() => undefined);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
@ -98,8 +99,8 @@ function publicProvisioningError(error) {
|
|||
if (error instanceof WeixinApiError) return safeAccountError(error.code, error.message);
|
||||
const message = ACTIVATION_ERROR_MESSAGES[error?.code];
|
||||
return message
|
||||
? safeAccountError(error.code, message)
|
||||
: safeAccountError('activation-unknown-failed', '微信已授权,但激活过程中发生未知错误。请查看 dsh web 日志。');
|
||||
? safeAccountError(error.code, t(message))
|
||||
: safeAccountError('activation-unknown-failed', t('微信已授权,但激活过程中发生未知错误。请查看 dsh web 日志。'));
|
||||
}
|
||||
|
||||
function preserveActivationError(error, fallbackCode) {
|
||||
|
|
@ -167,7 +168,7 @@ export class WeixinController {
|
|||
if (!token) {
|
||||
this.#errors.set(
|
||||
latest.botId,
|
||||
safeAccountError('missing-token', '登录凭据缺失,请移除账号后重新扫码。'),
|
||||
safeAccountError('missing-token', t('登录凭据缺失,请移除账号后重新扫码。')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -176,7 +177,7 @@ export class WeixinController {
|
|||
} catch (error) {
|
||||
this.#errors.set(
|
||||
latest.botId,
|
||||
safeAccountError('connection-failed', '微信连接未就绪,插件会自动重试。'),
|
||||
safeAccountError('connection-failed', t('微信连接未就绪,插件会自动重试。')),
|
||||
);
|
||||
this.#logger.warn?.(`[dsh-weixin] account ${latest.botId} failed to initialize:`, error);
|
||||
} finally {
|
||||
|
|
@ -227,12 +228,12 @@ export class WeixinController {
|
|||
} catch (error) {
|
||||
if (record.controller.signal.aborted) {
|
||||
record.state = 'cancelled';
|
||||
record.error = safeAccountError('cancelled', '扫码绑定已取消。');
|
||||
record.error = safeAccountError('cancelled', t('扫码绑定已取消。'));
|
||||
} else {
|
||||
record.state = 'failed';
|
||||
record.error = safeAccountError(
|
||||
error instanceof WeixinApiError ? error.code : 'qr-start-failed',
|
||||
error instanceof WeixinApiError ? error.message : '无法生成微信二维码,请稍后重试。',
|
||||
error instanceof WeixinApiError ? error.message : t('无法生成微信二维码,请稍后重试。'),
|
||||
);
|
||||
}
|
||||
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
||||
|
|
@ -271,7 +272,7 @@ export class WeixinController {
|
|||
record.verifyResolve = null;
|
||||
await record.task?.catch(() => undefined);
|
||||
if (!TERMINAL_ATTEMPT_STATES.has(record.state)) record.state = 'cancelled';
|
||||
record.error ??= safeAccountError('cancelled', '扫码绑定已取消。');
|
||||
record.error ??= safeAccountError('cancelled', t('扫码绑定已取消。'));
|
||||
}
|
||||
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
||||
this.#touch();
|
||||
|
|
@ -288,7 +289,7 @@ export class WeixinController {
|
|||
await this.#startRuntime(config, token);
|
||||
this.#errors.delete(botId);
|
||||
} catch (error) {
|
||||
this.#errors.set(botId, safeAccountError('connection-failed', '微信连接仍未就绪,请稍后重试。'));
|
||||
this.#errors.set(botId, safeAccountError('connection-failed', t('微信连接仍未就绪,请稍后重试。')));
|
||||
throw error;
|
||||
} finally {
|
||||
this.#touch();
|
||||
|
|
@ -303,10 +304,10 @@ export class WeixinController {
|
|||
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(
|
||||
`微信机器人(${maskWeixinAccountId(config.accountId)})`,
|
||||
t('微信机器人({name})', { name: maskWeixinAccountId(config.accountId) }),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
|
@ -352,7 +353,7 @@ export class WeixinController {
|
|||
? 'error'
|
||||
: 'offline';
|
||||
const error = this.#errors.get(config.botId) ?? (state === 'error'
|
||||
? safeAccountError('connection-failed', '微信连接未就绪,插件会自动重试。')
|
||||
? safeAccountError('connection-failed', t('微信连接未就绪,插件会自动重试。'))
|
||||
: null);
|
||||
return {
|
||||
botId: config.botId,
|
||||
|
|
@ -360,16 +361,16 @@ export class WeixinController {
|
|||
connected,
|
||||
configured: true,
|
||||
bot: {
|
||||
name: '微信机器人',
|
||||
name: t('微信机器人'),
|
||||
accountIdMasked: maskWeixinAccountId(config.accountId),
|
||||
},
|
||||
health: {
|
||||
status: connected ? 'healthy' : state === 'error' ? 'error' : 'offline',
|
||||
summary: connected
|
||||
? '微信消息长轮询运行正常'
|
||||
? t('微信消息长轮询运行正常')
|
||||
: state === 'error'
|
||||
? '微信连接未就绪,插件会自动重试'
|
||||
: '微信连接当前离线',
|
||||
? t('微信连接未就绪,插件会自动重试')
|
||||
: t('微信连接当前离线'),
|
||||
lastCheckedAt: runtimeStatus?.lastCheckedAt ?? null,
|
||||
},
|
||||
stats: {
|
||||
|
|
@ -438,11 +439,11 @@ export class WeixinController {
|
|||
record.state = 'needs_verification';
|
||||
} else if (response.status === 'verify_code_blocked') {
|
||||
record.state = 'failed';
|
||||
record.error = safeAccountError('verification-blocked', '配对码多次错误,请重新生成二维码。');
|
||||
record.error = safeAccountError('verification-blocked', t('配对码多次错误,请重新生成二维码。'));
|
||||
break;
|
||||
} else if (response.status === 'expired') {
|
||||
record.state = 'expired';
|
||||
record.error = safeAccountError('expired', '二维码已过期,请重新生成。');
|
||||
record.error = safeAccountError('expired', t('二维码已过期,请重新生成。'));
|
||||
break;
|
||||
} else if (response.status === 'scaned_but_redirect') {
|
||||
record.currentBaseUrl = apiBaseFromServer(response.redirect_host, record.currentBaseUrl);
|
||||
|
|
@ -453,7 +454,7 @@ export class WeixinController {
|
|||
) ?? this.#configStore.list()[0];
|
||||
if (!existing) {
|
||||
record.state = 'failed';
|
||||
record.error = safeAccountError('already-bound', '该微信账号已绑定,但本机没有可恢复的凭据。');
|
||||
record.error = safeAccountError('already-bound', t('该微信账号已绑定,但本机没有可恢复的凭据。'));
|
||||
} else {
|
||||
record.state = 'connected';
|
||||
record.botId = existing.botId;
|
||||
|
|
@ -465,7 +466,7 @@ export class WeixinController {
|
|||
const accountId = cleanString(response.ilink_bot_id);
|
||||
const ownerUserId = cleanString(response.ilink_user_id);
|
||||
if (!token || !accountId || !ownerUserId) {
|
||||
throw new WeixinApiError('incomplete-login', '微信授权成功,但返回的账号凭据不完整。');
|
||||
throw new WeixinApiError('incomplete-login', t('微信授权成功,但返回的账号凭据不完整。'));
|
||||
}
|
||||
record.state = 'connecting';
|
||||
this.#touch();
|
||||
|
|
@ -485,12 +486,12 @@ export class WeixinController {
|
|||
if (!record.controller.signal.aborted && Date.now() >= record.expiresAt
|
||||
&& !TERMINAL_ATTEMPT_STATES.has(record.state)) {
|
||||
record.state = 'expired';
|
||||
record.error = safeAccountError('expired', '二维码已过期,请重新生成。');
|
||||
record.error = safeAccountError('expired', t('二维码已过期,请重新生成。'));
|
||||
}
|
||||
} catch (error) {
|
||||
if (record.controller.signal.aborted || error?.name === 'AbortError') {
|
||||
record.state = 'cancelled';
|
||||
record.error = safeAccountError('cancelled', '扫码绑定已取消。');
|
||||
record.error = safeAccountError('cancelled', t('扫码绑定已取消。'));
|
||||
} else {
|
||||
record.state = 'failed';
|
||||
record.error = publicProvisioningError(error);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
connectionTestTarget,
|
||||
connectionTestTargetUnavailable,
|
||||
} from '../shared/connection-test.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const DEFAULT_START_RETRY_DELAYS_MS = Object.freeze([250, 1_000, 3_000]);
|
||||
const HARNESS_HEALTH_ERROR_CODES = new Set([
|
||||
|
|
@ -224,7 +225,7 @@ export class WeixinRuntime {
|
|||
const code = response.errcode ?? response.ret;
|
||||
throw new WeixinApiError(
|
||||
code === -14 ? 'stale-token' : 'updates-rejected',
|
||||
code === -14 ? '微信登录凭据已失效,请移除账号后重新扫码。' : '微信消息同步请求被拒绝。',
|
||||
code === -14 ? t('微信登录凭据已失效,请移除账号后重新扫码。') : t('微信消息同步请求被拒绝。'),
|
||||
);
|
||||
}
|
||||
consecutiveFailures = 0;
|
||||
|
|
@ -293,7 +294,7 @@ export class WeixinRuntime {
|
|||
: typeof this.#config.ownerUserId === 'string' && this.#config.ownerUserId.trim()
|
||||
? this.#config.ownerUserId.trim()
|
||||
: null;
|
||||
if (!toUserId) throw connectionTestTargetUnavailable('微信机器人');
|
||||
if (!toUserId) throw connectionTestTargetUnavailable(t('微信机器人'));
|
||||
if (!this.#status.ready || !this.#abortController) {
|
||||
throw new Error('Weixin runtime is not connected');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: 2, bots: Object.freeze([]) });
|
||||
const BOT_ID_PATTERN = /^whatsapp_[a-f0-9]{24}$/;
|
||||
const AUTH_DIRECTORY_PATTERN = /^[a-f0-9-]{36}$/;
|
||||
|
|
@ -30,7 +32,7 @@ export function deriveWhatsappBotId(accountJid) {
|
|||
|
||||
export function maskWhatsappAccount(accountJid) {
|
||||
const digits = normalizeWhatsappAccountJid(accountJid)?.split('@')[0] ?? '';
|
||||
if (!digits) return 'WhatsApp账号';
|
||||
if (!digits) return t('WhatsApp账号');
|
||||
if (digits.length <= 7) return `${digits.slice(0, 2)}•••${digits.slice(-2)}`;
|
||||
return `${digits.slice(0, 4)}••••${digits.slice(-4)}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { t } from '../shared/i18n.mjs';
|
||||
import { createTextBridgeStatus, TextHarnessBridge } from '../shared/text-harness-bridge.mjs';
|
||||
|
||||
export const WHATSAPP_DESCRIPTOR = Object.freeze({
|
||||
key: 'whatsapp',
|
||||
label: 'WhatsApp',
|
||||
connectionLabel: ' Web 关联设备',
|
||||
// Translated lazily: t() must run after setImHostLanguage, not at import time.
|
||||
get connectionLabel() { return t(' Web 关联设备'); },
|
||||
});
|
||||
|
||||
export class WhatsappHarnessBridge extends TextHarnessBridge {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { connectionTestMessage } from '../shared/connection-test.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
import {
|
||||
deriveWhatsappBotId,
|
||||
maskWhatsappAccount,
|
||||
|
|
@ -83,8 +84,8 @@ export class WhatsappController {
|
|||
this.#errors.set(config.botId, safeError(
|
||||
error?.code === 'relink-required' ? 'relink-required' : 'connection-failed',
|
||||
error?.code === 'relink-required'
|
||||
? 'WhatsApp 关联设备已失效,请移除后重新扫码。'
|
||||
: 'WhatsApp 连接未就绪,插件会自动重试。',
|
||||
? t('WhatsApp 关联设备已失效,请移除后重新扫码。')
|
||||
: t('WhatsApp 连接未就绪,插件会自动重试。'),
|
||||
));
|
||||
this.#logger.warn?.(`[dsh-im:whatsapp] bot ${config.botId} failed to initialize`);
|
||||
} finally {
|
||||
|
|
@ -172,7 +173,7 @@ export class WhatsappController {
|
|||
await record.task?.catch(() => undefined);
|
||||
if (!TERMINAL_ATTEMPT_STATES.has(record.state)) {
|
||||
record.state = 'cancelled';
|
||||
record.error = safeError('cancelled', '扫码接入已取消。');
|
||||
record.error = safeError('cancelled', t('扫码接入已取消。'));
|
||||
}
|
||||
await this.#deleteAuth(record.authDirectory).catch(() => undefined);
|
||||
}
|
||||
|
|
@ -192,8 +193,8 @@ export class WhatsappController {
|
|||
this.#errors.set(botId, safeError(
|
||||
error?.code === 'relink-required' ? 'relink-required' : 'connection-failed',
|
||||
error?.code === 'relink-required'
|
||||
? 'WhatsApp 关联设备已失效,请移除后重新扫码。'
|
||||
: 'WhatsApp 连接仍未就绪,请稍后重试。',
|
||||
? t('WhatsApp 关联设备已失效,请移除后重新扫码。')
|
||||
: t('WhatsApp 连接仍未就绪,请稍后重试。'),
|
||||
));
|
||||
throw error;
|
||||
} finally {
|
||||
|
|
@ -209,14 +210,14 @@ export class WhatsappController {
|
|||
return this.#withBotTransition(botId, async () => {
|
||||
const runtime = this.#runtimes.get(botId);
|
||||
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
||||
const error = new Error('WhatsApp机器人尚未连接');
|
||||
const error = new Error(t('WhatsApp机器人尚未连接'));
|
||||
error.code = 'test-target-unavailable';
|
||||
throw error;
|
||||
}
|
||||
return runtime.sendConnectionTest(
|
||||
connectionTestMessage(
|
||||
`${config.name}(${maskWhatsappAccount(config.accountJid)})`,
|
||||
'WhatsApp机器人',
|
||||
t('WhatsApp机器人'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
|
@ -275,8 +276,8 @@ export class WhatsappController {
|
|||
bot: { name: config.name, idMasked: maskWhatsappAccount(config.accountJid) },
|
||||
health: {
|
||||
status: connected ? 'healthy' : state === 'error' ? 'error' : 'offline',
|
||||
summary: connected ? 'WhatsApp Web 关联设备运行正常'
|
||||
: state === 'error' ? 'WhatsApp 连接未就绪' : 'WhatsApp 连接当前离线',
|
||||
summary: connected ? t('WhatsApp Web 关联设备运行正常')
|
||||
: state === 'error' ? t('WhatsApp 连接未就绪') : t('WhatsApp 连接当前离线'),
|
||||
lastCheckedAt: runtimeStatus?.lastCheckedAt ?? null,
|
||||
lastConnectedAt: runtimeStatus?.lastConnectedAt ?? null,
|
||||
},
|
||||
|
|
@ -340,7 +341,7 @@ export class WhatsappController {
|
|||
await this.#startRuntime(config);
|
||||
this.#errors.delete(botId);
|
||||
} catch (error) {
|
||||
this.#errors.set(botId, safeError('connection-failed', 'WhatsApp 已绑定,消息连接暂未就绪。'));
|
||||
this.#errors.set(botId, safeError('connection-failed', t('WhatsApp 已绑定,消息连接暂未就绪。')));
|
||||
this.#logger.warn?.(`[dsh-im:whatsapp] bot ${botId} did not reconnect after QR binding`);
|
||||
}
|
||||
if (previous?.authDirectory && previous.authDirectory !== config.authDirectory) {
|
||||
|
|
@ -363,11 +364,11 @@ export class WhatsappController {
|
|||
await this.#deleteAuth(record.authDirectory).catch(() => undefined);
|
||||
if (previous) await this.#startRuntime(previous).catch(() => undefined);
|
||||
record.state = 'cancelled';
|
||||
record.error = safeError('cancelled', '扫码接入已取消。');
|
||||
record.error = safeError('cancelled', t('扫码接入已取消。'));
|
||||
} else {
|
||||
await this.#deleteAuth(record.authDirectory).catch(() => undefined);
|
||||
record.state = 'failed';
|
||||
record.error = safeError('activation-failed', 'WhatsApp 已扫码,但无法保存关联设备。');
|
||||
record.error = safeError('activation-failed', t('WhatsApp 已扫码,但无法保存关联设备。'));
|
||||
this.#logger.error?.('[dsh-im:whatsapp] unable to persist linked-device session');
|
||||
}
|
||||
} finally {
|
||||
|
|
@ -380,10 +381,10 @@ export class WhatsappController {
|
|||
if (TERMINAL_ATTEMPT_STATES.has(record.state)) return;
|
||||
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-connect-failed', '无法连接 WhatsApp,请重新生成二维码。');
|
||||
record.error = safeError('qr-connect-failed', t('无法连接 WhatsApp,请重新生成二维码。'));
|
||||
}
|
||||
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
||||
await record.session?.close().catch(() => undefined);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
} from '@whiskeysockets/baileys';
|
||||
|
||||
import { splitMessageText } from '../shared/editable-message-stream.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
import { ImagePromptError } from '../shared/image-prompt.mjs';
|
||||
import { trackOutboundArtifactProviderPromise } from '../shared/semantic/artifact.mjs';
|
||||
import { createWhatsappBridgeStatus, WhatsappHarnessBridge } from './whatsapp-bridge.mjs';
|
||||
|
|
@ -107,7 +108,7 @@ async function downloadWhatsappImage(message, download, {
|
|||
throw new ImagePromptError(
|
||||
'image-download-failed',
|
||||
`WhatsApp image download timed out after ${IMAGE_DOWNLOAD_TIMEOUT_MS} ms`,
|
||||
'图片下载失败,请重新发送后再试。',
|
||||
t('图片下载失败,请重新发送后再试。'),
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
|
|
@ -126,7 +127,7 @@ async function downloadWhatsappImage(message, download, {
|
|||
throw new ImagePromptError(
|
||||
'image-too-large',
|
||||
`WhatsApp image exceeded ${maxBytes} bytes`,
|
||||
'图片超过 5 MB,请压缩后重试。',
|
||||
t('图片超过 5 MB,请压缩后重试。'),
|
||||
);
|
||||
}
|
||||
chunks.push(data);
|
||||
|
|
@ -137,7 +138,7 @@ async function downloadWhatsappImage(message, download, {
|
|||
throw new ImagePromptError(
|
||||
'image-download-failed',
|
||||
`WhatsApp image stream timed out after ${IMAGE_DOWNLOAD_TIMEOUT_MS} ms`,
|
||||
'图片下载失败,请重新发送后再试。',
|
||||
t('图片下载失败,请重新发送后再试。'),
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
|
|
@ -640,7 +641,7 @@ export class WhatsappRuntime {
|
|||
|
||||
async sendConnectionTest(text) {
|
||||
if (!this.#status.ready || !this.#client) {
|
||||
const error = new Error('WhatsApp机器人尚未连接');
|
||||
const error = new Error(t('WhatsApp机器人尚未连接'));
|
||||
error.code = 'test-target-unavailable';
|
||||
throw error;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import makeWASocket, {
|
|||
useMultiFileAuthState,
|
||||
} from '@whiskeysockets/baileys';
|
||||
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const SILENT_LOGGER = Object.freeze({
|
||||
level: 'silent',
|
||||
trace() {},
|
||||
|
|
@ -57,7 +59,7 @@ function normalizeIdentity(socket, authState) {
|
|||
return {
|
||||
accountJid,
|
||||
name: typeof source?.name === 'string' && source.name.trim()
|
||||
? source.name.trim().slice(0, 100) : 'WhatsApp机器人',
|
||||
? source.name.trim().slice(0, 100) : t('WhatsApp机器人'),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
102
test/channels/shared/i18n.test.mjs
Normal file
102
test/channels/shared/i18n.test.mjs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { after, test } from 'node:test';
|
||||
|
||||
import {
|
||||
getImHostLanguage,
|
||||
setImHostLanguage,
|
||||
t,
|
||||
} from '../../../src/channels/shared/i18n.mjs';
|
||||
import { EN } from '../../../src/channels/shared/i18n-en.mjs';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const srcChannelsDir = join(here, '..', '..', '..', 'src', 'channels');
|
||||
|
||||
after(() => {
|
||||
setImHostLanguage('zh');
|
||||
});
|
||||
|
||||
test('host language defaults to Chinese and t() is the identity function', () => {
|
||||
setImHostLanguage(undefined);
|
||||
assert.equal(getImHostLanguage(), 'zh');
|
||||
assert.equal(t('连接成功'), '连接成功');
|
||||
assert.equal(t('共 {count} 个机器人', { count: 3 }), '共 3 个机器人');
|
||||
});
|
||||
|
||||
test('setImHostLanguage accepts English spellings and falls back to Chinese', () => {
|
||||
for (const value of ['en', 'EN', 'en-US', ' english ']) {
|
||||
setImHostLanguage(value);
|
||||
assert.equal(getImHostLanguage(), 'en', `expected ${value} to select English`);
|
||||
}
|
||||
for (const value of [undefined, null, '', 'zh', 'zh-CN', 'fr', 42]) {
|
||||
setImHostLanguage(value);
|
||||
assert.equal(getImHostLanguage(), 'zh', `expected ${value} to select Chinese`);
|
||||
}
|
||||
});
|
||||
|
||||
test('t() translates known keys and fills placeholders in English mode', () => {
|
||||
setImHostLanguage('en');
|
||||
const [key] = Object.keys(EN);
|
||||
if (key) {
|
||||
assert.equal(t(key), EN[key]);
|
||||
}
|
||||
assert.equal(t('未收录的中文'), '未收录的中文');
|
||||
assert.equal(t('{value} 测试', { value: 'x' }), EN['{value} 测试'] ?? 'x 测试');
|
||||
assert.equal(t(42), 42);
|
||||
});
|
||||
|
||||
test('every English dictionary entry is a non-empty translation of a Chinese key', () => {
|
||||
for (const [key, value] of Object.entries(EN)) {
|
||||
assert.ok(/[一-鿿]/.test(key), `dictionary key is not Chinese: ${key}`);
|
||||
assert.ok(typeof value === 'string' && value.trim().length > 0, `empty translation for: ${key}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('every literal t() key in src/channels has an English dictionary entry', () => {
|
||||
const files = [];
|
||||
const walk = (dir) => {
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const path = join(dir, entry);
|
||||
if (statSync(path).isDirectory()) {
|
||||
if (entry !== 'i18n-en') walk(path);
|
||||
} else if (entry.endsWith('.mjs') && entry !== 'i18n.mjs' && entry !== 'i18n-en.mjs') {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(srcChannelsDir);
|
||||
|
||||
const keyPattern = /\bt\(\s*(['`])((?:[^\\`'"$]|\$(?!\{)|\\.)*)\1/g;
|
||||
// Decode the escapes a real string literal would apply, so keys written as
|
||||
// `...\n...` in source compare against the runtime dictionary key (newline).
|
||||
const decodeLiteral = (raw) => {
|
||||
let out = '';
|
||||
for (let i = 0; i < raw.length; i += 1) {
|
||||
if (raw[i] !== '\\' || i + 1 >= raw.length) {
|
||||
out += raw[i];
|
||||
continue;
|
||||
}
|
||||
const next = raw[i + 1];
|
||||
if (next === 'n') out += '\n';
|
||||
else if (next === 't') out += '\t';
|
||||
else if (next === 'r') out += '\r';
|
||||
else out += next; // \', \", \\, \` and anything else keep the char
|
||||
i += 1;
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const missing = [];
|
||||
for (const file of files) {
|
||||
const source = readFileSync(file, 'utf8');
|
||||
for (const match of source.matchAll(keyPattern)) {
|
||||
const key = decodeLiteral(match[2]);
|
||||
if (!/[一-鿿]/.test(key)) continue;
|
||||
if (!Object.hasOwn(EN, key)) {
|
||||
missing.push(`${file}: ${match[2]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.deepEqual(missing, [], 't() keys missing from the English dictionary');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue