made-bridge 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +45 -2
- package/builtin/plugins/README.md +22 -0
- package/builtin/themes/README.md +64 -0
- package/builtin/themes/sunset/theme.json +19 -0
- package/config.example.json +20 -0
- package/demo-project/README.md +5 -0
- package/demo-project/index.mjs +14 -0
- package/package.json +48 -4
- package/public/app.js +1028 -0
- package/public/assistants.html +67 -0
- package/public/bean-icon.png +0 -0
- package/public/i18n-messages.js +967 -0
- package/public/i18n-static.js +615 -0
- package/public/i18n.js +50 -0
- package/public/index.html +149 -0
- package/public/made-icon.png +0 -0
- package/public/plugins.html +64 -0
- package/public/plugins.js +335 -0
- package/public/provider-icon.js +77 -0
- package/public/styles.css +445 -0
- package/public/tasks.html +85 -0
- package/src/agent-icons.mjs +138 -0
- package/src/agents/codex.mjs +94 -0
- package/src/agents/cursor.mjs +31 -0
- package/src/agents/events.mjs +92 -0
- package/src/agents/index.mjs +7 -0
- package/src/agents/qoder.mjs +34 -0
- package/src/agents/shared.mjs +191 -0
- package/src/agents/workbuddy.mjs +121 -0
- package/src/board-link.mjs +266 -0
- package/src/config.mjs +196 -0
- package/src/doctor.mjs +146 -0
- package/src/i18n.mjs +257 -0
- package/src/installed.mjs +291 -0
- package/src/jobs.mjs +369 -0
- package/src/language-settings.mjs +38 -0
- package/src/model-settings.mjs +63 -0
- package/src/openai-key.mjs +47 -0
- package/src/oray.mjs +243 -0
- package/src/pairing.mjs +203 -0
- package/src/plugin-package.mjs +132 -0
- package/src/plugin-runtime.mjs +169 -0
- package/src/plugins/contracts.d.ts +184 -0
- package/src/plugins/enhance.mjs +156 -0
- package/src/plugins/registry.mjs +146 -0
- package/src/providers.mjs +75 -0
- package/src/server.mjs +939 -0
- package/src/sessions.mjs +134 -0
- package/src/speech/index.mjs +10 -0
- package/src/speech/openai.mjs +48 -0
- package/src/speech/whisper.mjs +59 -0
- package/src/speech-settings.mjs +209 -0
- package/src/transcribe.mjs +85 -0
- package/src/tunnel.mjs +531 -0
- package/src/usb-receiver.mjs +664 -0
- package/src/voice-settings.mjs +69 -0
package/src/config.mjs
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { loadLanguageSettings } from './language-settings.mjs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { loadOpenAIKey } from './openai-key.mjs';
|
|
7
|
+
import { pluginRuntime } from './plugin-runtime.mjs';
|
|
8
|
+
import { DEFAULT_CODEX_MODEL, loadModelSettings, validateModelSettings, validateStoredModels } from './model-settings.mjs';
|
|
9
|
+
import { DEFAULT_TRANSCRIBE_MODEL, DEFAULT_TRANSCRIBE_URL, loadVoiceSettings, validateTranscribeModel, validateTranscribeUrl } from './voice-settings.mjs';
|
|
10
|
+
|
|
11
|
+
export const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
|
+
const PLUGIN_ID = /^[a-z0-9][a-z0-9_-]{0,39}$/;
|
|
13
|
+
|
|
14
|
+
/** 随代码发布的内置包目录:bridge/builtin/{plugins,themes}。升级代码即升级内置包。 */
|
|
15
|
+
export const BUILTIN_ROOT = path.join(ROOT, 'builtin');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 用户数据目录(配置、状态、工作区):~/.made-bridge。
|
|
19
|
+
*
|
|
20
|
+
* 为什么不放在包目录里:源码仓库运行时 ROOT 可写没问题;但 `npx made-bridge`
|
|
21
|
+
* 把包装在 npm 缓存里运行,缓存随时可能被清——配置、配对、会话写在那里等于
|
|
22
|
+
* 随时丢。所以可写数据一律落到用户家目录下的固定位置,代码目录保持只读。
|
|
23
|
+
*/
|
|
24
|
+
export function dataHome(env = process.env) {
|
|
25
|
+
return path.resolve(env.VIBE_DATA_DIR || path.join(os.homedir(), '.made-bridge'));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 用户配置文件路径解析:
|
|
30
|
+
* ① 显式 VIBE_CONFIG;② 源码仓库里的 ROOT/config.local.json(老用法,向后兼容);
|
|
31
|
+
* ③ ~/.made-bridge/config.local.json(npx / 全局安装的默认位置)。
|
|
32
|
+
*/
|
|
33
|
+
export function resolveConfigPath(env = process.env) {
|
|
34
|
+
if (env.VIBE_CONFIG) return path.resolve(env.VIBE_CONFIG);
|
|
35
|
+
const repoConfig = path.join(ROOT, 'config.local.json');
|
|
36
|
+
if (fs.existsSync(repoConfig)) return repoConfig;
|
|
37
|
+
return path.join(dataHome(env), 'config.local.json');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 首次运行引导:用户目录里还没有配置时,把包内示例复制过去,让 `npx made-bridge`
|
|
42
|
+
* 开箱即得一份**可编辑且持久**的配置(直接读包内示例的话,编辑会随缓存蒸发)。
|
|
43
|
+
* 返回 { created, path };created 为 true 时调用方应提示用户去编辑。
|
|
44
|
+
*/
|
|
45
|
+
export function ensureUserConfig(env = process.env) {
|
|
46
|
+
if (env.VIBE_CONFIG) return { created: false, path: path.resolve(env.VIBE_CONFIG) };
|
|
47
|
+
const repoConfig = path.join(ROOT, 'config.local.json');
|
|
48
|
+
if (fs.existsSync(repoConfig)) return { created: false, path: repoConfig };
|
|
49
|
+
const target = path.join(dataHome(env), 'config.local.json');
|
|
50
|
+
if (fs.existsSync(target)) return { created: false, path: target };
|
|
51
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
52
|
+
fs.copyFileSync(path.join(ROOT, 'config.example.json'), target);
|
|
53
|
+
// 示例里的相对路径(./demo-project)是相对包目录的;落到用户目录后要改写成
|
|
54
|
+
// 绝对路径指向包内的演示项目,否则 npx 首次运行的默认项目找不到目录。
|
|
55
|
+
try {
|
|
56
|
+
const seeded = JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
57
|
+
if (Array.isArray(seeded.projects)) {
|
|
58
|
+
for (const project of seeded.projects) {
|
|
59
|
+
if (typeof project.path === 'string' && !path.isAbsolute(project.path)) {
|
|
60
|
+
project.path = path.join(ROOT, project.path);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
fs.writeFileSync(target, `${JSON.stringify(seeded, null, 2)}\n`, { mode: 0o600 });
|
|
64
|
+
}
|
|
65
|
+
} catch { /* 改写失败不致命:读不到就按原样用,任务报错时用户自然会去编辑配置 */ }
|
|
66
|
+
return { created: true, path: target };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 用户包目录根:默认 ~/made,可用 MADE_HOME 环境变量或配置里的 madeHome 覆盖。
|
|
71
|
+
*
|
|
72
|
+
* 这是一个固定位置,不跟着配置文件走:市场把包装到 <root>/plugins 与 <root>/themes,
|
|
73
|
+
* 桥接器每次启动自动扫描,换一个工作目录也不需要重新安装。
|
|
74
|
+
*/
|
|
75
|
+
export function packagesHome(config = {}) {
|
|
76
|
+
return path.resolve(config.madeHome || process.env.MADE_HOME || path.join(os.homedir(), 'made'));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 包目录的查找顺序:内置在前、用户目录在后,同名时用户目录覆盖内置。
|
|
81
|
+
*
|
|
82
|
+
* 内置目录只读,移除与改配置只作用于用户目录。
|
|
83
|
+
*/
|
|
84
|
+
export function packageRoots(config = {}) {
|
|
85
|
+
return [BUILTIN_ROOT, packagesHome(config)];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 校验 VIBE_USB_PORT。允许两类写法:绝对路径(/dev/cu.usbmodem1101、
|
|
90
|
+
* \\.\COM10),以及 Windows 上用户真正会写的 COM 端口名(COM3)。
|
|
91
|
+
*
|
|
92
|
+
* 为什么要特判 COM3:Windows 上「COM3」既没有盘符也没有 UNC 根,
|
|
93
|
+
* path.isAbsolute 判 false,只用绝对路径规则会把 Windows 唯一的手动兜底
|
|
94
|
+
* 直接拦掉,而那个兜底正是 USB 在这套系统上唯一能走的路。
|
|
95
|
+
*/
|
|
96
|
+
export function validateUsbPort(value, platform = process.platform) {
|
|
97
|
+
const port = String(value || '');
|
|
98
|
+
if (!port) return '';
|
|
99
|
+
if (/[\r\n\x00]/.test(port)) throw new Error('VIBE_USB_PORT 不能包含控制字符');
|
|
100
|
+
if (path.isAbsolute(port)) return port;
|
|
101
|
+
if (platform === 'win32' && /^\\{0,2}(?:\.\\)?COM\d+$/i.test(port)) return port;
|
|
102
|
+
throw new Error('VIBE_USB_PORT 必须是 USB 串口的绝对路径,或 Windows 上的 COM 端口名');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function loadConfig(env = process.env) {
|
|
106
|
+
const configPath = resolveConfigPath(env);
|
|
107
|
+
// 有用户配置就用用户配置(projects/workspaces 相对它解析);完全没有时回落包内
|
|
108
|
+
// 示例,让首次 npx 也能起服务——但状态目录仍走数据目录,绝不写进包内。
|
|
109
|
+
const hasUserConfig = fs.existsSync(configPath);
|
|
110
|
+
const selected = hasUserConfig ? configPath : path.join(ROOT, 'config.example.json');
|
|
111
|
+
const fallback = path.join(ROOT, 'config.example.json');
|
|
112
|
+
const raw = JSON.parse(fs.readFileSync(selected, 'utf8'));
|
|
113
|
+
const base = path.dirname(selected);
|
|
114
|
+
if (!Array.isArray(raw.projects) || raw.projects.length === 0) {
|
|
115
|
+
throw new Error('配置需要至少一个 projects 项目');
|
|
116
|
+
}
|
|
117
|
+
const seen = new Set();
|
|
118
|
+
const projects = raw.projects.map((project) => {
|
|
119
|
+
if (!/^[a-z0-9_-]{1,40}$/.test(project.id) || seen.has(project.id)) {
|
|
120
|
+
throw new Error(`项目 ID 无效或重复:${project.id}`);
|
|
121
|
+
}
|
|
122
|
+
seen.add(project.id);
|
|
123
|
+
return {
|
|
124
|
+
id: project.id,
|
|
125
|
+
label: String(project.label || project.id),
|
|
126
|
+
path: path.resolve(base, project.path),
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
const defaultProject = raw.defaultProject || projects[0].id;
|
|
130
|
+
const defaultProvider = raw.defaultProvider || 'codex';
|
|
131
|
+
if (!seen.has(defaultProject) || !PLUGIN_ID.test(defaultProvider)) {
|
|
132
|
+
throw new Error('默认项目或默认编程工具不在配置中');
|
|
133
|
+
}
|
|
134
|
+
const port = Number(raw.port || 8787);
|
|
135
|
+
const devicePort = Number(raw.devicePort || 8788);
|
|
136
|
+
if (![port, devicePort].every((p) => Number.isInteger(p) && p > 0 && p < 65536)) {
|
|
137
|
+
throw new Error('端口号必须在 1 到 65535 之间');
|
|
138
|
+
}
|
|
139
|
+
// 同时执行的会话数。同一会话恒为串行(编程工具按会话线程加写锁,并行必然冲突),
|
|
140
|
+
// 所以这里限制的是"能同时跑几个会话",不是"一个会话里能同时跑几条指令"。
|
|
141
|
+
const maxParallelJobs = Number(env.VIBE_MAX_PARALLEL || raw.maxParallelJobs || 2);
|
|
142
|
+
if (!Number.isInteger(maxParallelJobs) || maxParallelJobs < 1 || maxParallelJobs > 4) {
|
|
143
|
+
throw new Error('并行任务数必须是 1 到 4 之间的整数');
|
|
144
|
+
}
|
|
145
|
+
const stateDir = path.resolve(env.VIBE_STATE_DIR
|
|
146
|
+
|| (hasUserConfig ? path.join(path.dirname(configPath), 'state') : dataHome(env)));
|
|
147
|
+
const usbPort = validateUsbPort(env.VIBE_USB_PORT || '');
|
|
148
|
+
const models = { ...validateModelSettings({
|
|
149
|
+
codex: env.VIBE_CODEX_MODEL || raw.codexModel || DEFAULT_CODEX_MODEL,
|
|
150
|
+
cursor: env.VIBE_CURSOR_MODEL || raw.cursorModel || '',
|
|
151
|
+
qoder: env.VIBE_QODER_MODEL || raw.qoderModel || '',
|
|
152
|
+
}), ...validateStoredModels(raw.models || {}), ...(loadModelSettings(stateDir) || {}) };
|
|
153
|
+
const asrMode = env.VIBE_ASR_MODE || 'openai';
|
|
154
|
+
if (!PLUGIN_ID.test(asrMode)) throw new Error('VIBE_ASR_MODE 必须是语音插件 ID');
|
|
155
|
+
if (raw.plugins !== undefined && (!Array.isArray(raw.plugins) || raw.plugins.some(p => typeof p !== 'string'))) throw new Error('plugins 必须是本地模块路径数组');
|
|
156
|
+
if (raw.madeHome !== undefined && (typeof raw.madeHome !== 'string' || !raw.madeHome.trim())) throw new Error('madeHome 必须是非空字符串');
|
|
157
|
+
if (raw.disabledPlugins !== undefined && (!Array.isArray(raw.disabledPlugins) || raw.disabledPlugins.some(p => !PLUGIN_ID.test(p)))) throw new Error('disabledPlugins 必须是插件 ID 数组');
|
|
158
|
+
if (raw.pluginSettings !== undefined && (!raw.pluginSettings || typeof raw.pluginSettings !== 'object' || Array.isArray(raw.pluginSettings))) throw new Error('pluginSettings 必须是对象');
|
|
159
|
+
const savedVoiceSettings = loadVoiceSettings(stateDir);
|
|
160
|
+
const environmentOpenaiKey = env.OPENAI_API_KEY || '';
|
|
161
|
+
const savedOpenaiKey = loadOpenAIKey(stateDir);
|
|
162
|
+
return {
|
|
163
|
+
configDirectory: base,
|
|
164
|
+
madeHome: packagesHome({ madeHome: env.MADE_HOME || raw.madeHome }),
|
|
165
|
+
locale: loadLanguageSettings(stateDir, raw.locale ?? 'zh-CN'),
|
|
166
|
+
plugins: raw.plugins || [],
|
|
167
|
+
disabledPlugins: raw.disabledPlugins || [],
|
|
168
|
+
pluginSettings: raw.pluginSettings || {},
|
|
169
|
+
projects,
|
|
170
|
+
workspaceRoot: path.resolve(base, raw.workspaceRoot || './workspaces'),
|
|
171
|
+
defaultProject,
|
|
172
|
+
defaultProvider,
|
|
173
|
+
port,
|
|
174
|
+
devicePort,
|
|
175
|
+
maxParallelJobs,
|
|
176
|
+
codexModel: models.codex,
|
|
177
|
+
models,
|
|
178
|
+
asrMode,
|
|
179
|
+
whisperModel: env.VIBE_WHISPER_MODEL || '',
|
|
180
|
+
whisperCli: env.VIBE_WHISPER_CLI || 'whisper-cli',
|
|
181
|
+
openaiKey: savedOpenaiKey || environmentOpenaiKey,
|
|
182
|
+
openaiKeySource: savedOpenaiKey ? 'saved' : environmentOpenaiKey ? 'environment' : 'none',
|
|
183
|
+
environmentOpenaiKey,
|
|
184
|
+
transcribeUrl: savedVoiceSettings?.url || validateTranscribeUrl(env.VIBE_TRANSCRIBE_URL || DEFAULT_TRANSCRIBE_URL),
|
|
185
|
+
transcribeModel: savedVoiceSettings?.model || validateTranscribeModel(env.VIBE_TRANSCRIBE_MODEL || DEFAULT_TRANSCRIBE_MODEL),
|
|
186
|
+
workbuddyToken: env.WORKBUDDY_ACCESS_TOKEN || '',
|
|
187
|
+
workbuddy: {
|
|
188
|
+
allowUnscopedDispatch: raw.workbuddy?.allowUnscopedDispatch === true,
|
|
189
|
+
},
|
|
190
|
+
stateDir,
|
|
191
|
+
usbPort,
|
|
192
|
+
configFile: selected,
|
|
193
|
+
// 插件可用的跨平台助手(命令查找、子进程、npm)。见 plugin-runtime.mjs。
|
|
194
|
+
runtime: pluginRuntime,
|
|
195
|
+
};
|
|
196
|
+
}
|
package/src/doctor.mjs
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 桥接器自检:node src/server.mjs --doctor
|
|
3
|
+
*
|
|
4
|
+
* 为什么要有它:平台差异不该让用户去读代码。这里在真机上逐项检查哪些能力可用、
|
|
5
|
+
* 缺什么、缺了怎么办,并把与平台相关的限制明说——与 AGENTS.md 的约定一致:
|
|
6
|
+
* 功能不可用时必须给出原因,不能静默失败。模块化拆分是为了能直接测,
|
|
7
|
+
* 不用把桥接器真的跑起来。
|
|
8
|
+
*/
|
|
9
|
+
import { ROOT, loadConfig } from './config.mjs';
|
|
10
|
+
import { findCommand, isWindows, platform } from './plugin-runtime.mjs';
|
|
11
|
+
import { discoverUsbSerialPorts } from './usb-receiver.mjs';
|
|
12
|
+
import { loadConfiguredPlugins } from './plugins/registry.mjs';
|
|
13
|
+
import { providerStates } from './providers.mjs';
|
|
14
|
+
|
|
15
|
+
const LEVEL = { ok: '✓', warn: '!', fail: '✗' };
|
|
16
|
+
const PLATFORM_LABEL = { darwin: 'macOS', linux: 'Linux', win32: 'Windows' };
|
|
17
|
+
const TUNNEL_TOOLS = ['tailscale', 'ngrok', 'cloudflared'];
|
|
18
|
+
|
|
19
|
+
const platformLabel = () => `${PLATFORM_LABEL[platform] || platform}(${platform})`;
|
|
20
|
+
|
|
21
|
+
/** 串口发现失败不该让自检整体失败:拿不到就当没发现。 */
|
|
22
|
+
async function safePorts() {
|
|
23
|
+
try {
|
|
24
|
+
return await discoverUsbSerialPorts();
|
|
25
|
+
} catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 收集自检项。返回 { checks, code }:
|
|
32
|
+
* checks —— { level, label, detail } 或 { raw } 的纯数据,便于测试与二次渲染
|
|
33
|
+
* code —— 进程退出码,1 表示有阻断性缺项
|
|
34
|
+
*/
|
|
35
|
+
export async function collectDoctor({ config: provided } = {}) {
|
|
36
|
+
const checks = [];
|
|
37
|
+
const add = (level, label, detail = '') => checks.push({ level, label, detail });
|
|
38
|
+
const raw = (text = '') => checks.push({ raw: text });
|
|
39
|
+
|
|
40
|
+
raw('码得桥接器自检');
|
|
41
|
+
raw();
|
|
42
|
+
|
|
43
|
+
raw('运行环境');
|
|
44
|
+
add('ok', `${platformLabel()} · Node ${process.version}`);
|
|
45
|
+
add('ok', `桥接器目录 ${ROOT}`);
|
|
46
|
+
|
|
47
|
+
let config = provided;
|
|
48
|
+
if (!config) {
|
|
49
|
+
try {
|
|
50
|
+
config = loadConfig();
|
|
51
|
+
} catch (error) {
|
|
52
|
+
add('fail', '配置无法加载', error.message);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (config) {
|
|
56
|
+
add('ok', `配置文件 ${config.configFile}`);
|
|
57
|
+
add('ok', `包目录 ${config.madeHome}(plugins/、themes/)`);
|
|
58
|
+
add('ok', `状态目录 ${config.stateDir}`);
|
|
59
|
+
add('ok', `端口:电脑面板 ${config.port},设备接口 ${config.devicePort}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
raw();
|
|
63
|
+
raw('依赖命令');
|
|
64
|
+
const git = findCommand('git');
|
|
65
|
+
if (git) add('ok', `git ${git}`, '任务前后改动对比依赖它');
|
|
66
|
+
else add('fail', '未找到 git', '任务的前后改动对比依赖它,请先安装 Git 并加入 PATH');
|
|
67
|
+
|
|
68
|
+
const npm = findCommand('npm');
|
|
69
|
+
if (npm) add('ok', `npm ${npm}`, '需要自装依赖的插件(如 Pi 智能体)首次运行时用它');
|
|
70
|
+
else add('warn', '未找到 npm', '安装 Node.js 时自带 npm;缺了它,首次运行需要自动装依赖的插件会起不来');
|
|
71
|
+
|
|
72
|
+
const esptool = findCommand('esptool') || findCommand('esptool.py');
|
|
73
|
+
if (esptool) add('ok', `esptool ${esptool}`, '只给刷机工具用,桥接器运行不需要');
|
|
74
|
+
else add('warn', '未找到 esptool', '只影响 npm run flash 刷机,不影响桥接器运行');
|
|
75
|
+
|
|
76
|
+
raw();
|
|
77
|
+
raw('接入方式');
|
|
78
|
+
const ports = config ? await safePorts() : [];
|
|
79
|
+
if (isWindows) {
|
|
80
|
+
// Windows 侧没有 /dev,也没有 VID/PID 枚举,实话实说。
|
|
81
|
+
add('warn', 'USB 线直连 / 接收器转发:不能自动发现串口',
|
|
82
|
+
'需手动设 VIBE_USB_PORT=<COM 口>(设备管理器里可查);接收器只支持原生 USB 变体,'
|
|
83
|
+
+ 'WCH 转串口变体在 Windows 上不支持(不设波特率),请用 macOS / Linux');
|
|
84
|
+
} else if (ports.length) {
|
|
85
|
+
add('ok', `USB 线直连 / 接收器转发:已发现串口 ${ports.join('、')}`);
|
|
86
|
+
} else {
|
|
87
|
+
add('warn', 'USB 线直连 / 接收器转发:没有发现串口',
|
|
88
|
+
'插上码得或接收器后重试;也可以直接用 VIBE_USB_PORT 指定端口');
|
|
89
|
+
}
|
|
90
|
+
if (config?.usbPort) add('ok', `已用 VIBE_USB_PORT 指定 ${config.usbPort}`);
|
|
91
|
+
|
|
92
|
+
add('ok', '局域网 Wi-Fi 直连:设备与电脑同网即可,无需额外配置');
|
|
93
|
+
const tunnels = TUNNEL_TOOLS.filter((name) => findCommand(name));
|
|
94
|
+
if (tunnels.length) add('ok', `公网地址:本机有 ${tunnels.join('、')}`);
|
|
95
|
+
else add('warn', '公网地址:未检测到内网穿透工具',
|
|
96
|
+
`装了 ${TUNNEL_TOOLS.join(' / ')} 之一才能在桥接页配置公网地址`);
|
|
97
|
+
|
|
98
|
+
if (esptool) {
|
|
99
|
+
add('ok', '烧录:npm run flash 可用(先停掉占用串口的桥接器)');
|
|
100
|
+
} else {
|
|
101
|
+
add('warn', '烧录:npm run flash 需要 esptool,当前不可用');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (config) {
|
|
105
|
+
raw();
|
|
106
|
+
raw('插件');
|
|
107
|
+
try {
|
|
108
|
+
const registry = await loadConfiguredPlugins(config);
|
|
109
|
+
const states = providerStates({ ...config, pluginsRuntime: registry });
|
|
110
|
+
if (!states.length) add('warn', '没有加载到任何助手插件', '插件目录见上方的包目录');
|
|
111
|
+
for (const state of states) {
|
|
112
|
+
add(state.available ? 'ok' : 'warn', `${state.label}(${state.id})`, state.reason || '可用');
|
|
113
|
+
}
|
|
114
|
+
} catch (error) {
|
|
115
|
+
add('fail', '插件加载失败', error.message);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const fails = checks.filter((item) => item.level === 'fail').length;
|
|
120
|
+
const warns = checks.filter((item) => item.level === 'warn').length;
|
|
121
|
+
raw();
|
|
122
|
+
raw(fails
|
|
123
|
+
? `自检未通过:${fails} 项需要处理${warns ? `,另有 ${warns} 项受限` : ''}。详见上方 ✗ 项。`
|
|
124
|
+
: warns
|
|
125
|
+
? `自检通过:${warns} 项受限(上方 ! 项),不影响其余功能。`
|
|
126
|
+
: '自检通过:本机没有发现问题。');
|
|
127
|
+
|
|
128
|
+
return { checks, code: fails ? 1 : 0 };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 把自检项渲染成给人看的文本。 */
|
|
132
|
+
export function formatDoctor(checks) {
|
|
133
|
+
return checks
|
|
134
|
+
.map((item) => (item.raw !== undefined
|
|
135
|
+
? item.raw
|
|
136
|
+
: `${LEVEL[item.level]} ${item.label}${item.detail ? `\n ${item.detail}` : ''}`))
|
|
137
|
+
.join('\n');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 命令行入口:打印自检结果并返回进程退出码。 */
|
|
141
|
+
export async function runDoctor(options = {}) {
|
|
142
|
+
const { out = console.log, ...rest } = options;
|
|
143
|
+
const { checks, code } = await collectDoctor(rest);
|
|
144
|
+
out(formatDoctor(checks));
|
|
145
|
+
return code;
|
|
146
|
+
}
|
package/src/i18n.mjs
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/** Translate bridge-owned messages only, at explicit output boundaries. Never
|
|
2
|
+
* walk arbitrary JSON or translate instructions, titles, paths, credentials,
|
|
3
|
+
* model names, plugin-defined metadata, or assistant replies. Unknown messages
|
|
4
|
+
* remain verbatim; anchored templates preserve their dynamic parameters. */
|
|
5
|
+
const messages = new Map([
|
|
6
|
+
['语言仅支持 zh-CN 或 en', 'Language must be zh-CN or en'],
|
|
7
|
+
['语言设置文件无效', 'Invalid language settings file'],
|
|
8
|
+
['语言设置格式无效', 'Invalid language settings'],
|
|
9
|
+
['请求过大', 'Request is too large'],
|
|
10
|
+
['录音超过 30 秒上限', 'Recording exceeds the 30-second limit'],
|
|
11
|
+
['只允许从本机访问电脑面板', 'The dashboard is only accessible from this computer'],
|
|
12
|
+
['缺少本机操作令牌', 'Local operation token is missing'],
|
|
13
|
+
['密钥请求格式无效', 'Invalid API key request'],
|
|
14
|
+
['clearKey 必须为布尔值', 'clearKey must be a boolean'],
|
|
15
|
+
['不能同时设置和清除语音 API Key', 'Cannot set and clear the speech API key together'],
|
|
16
|
+
['未找到接口', 'Endpoint not found'],
|
|
17
|
+
['插件设置无效', 'Invalid plugin setting'],
|
|
18
|
+
['插件配置无效', 'Invalid plugin settings'],
|
|
19
|
+
['插件配置规范无效', 'Invalid plugin settings spec'],
|
|
20
|
+
['插件配置过大', 'Plugin settings are too large'],
|
|
21
|
+
['插件配置规范与插件 ID 不一致', 'The settings spec does not match the plugin ID'],
|
|
22
|
+
['插件没有可保存的配置', 'This plugin has no settings to save'],
|
|
23
|
+
['插件不存在', 'Plugin not found'],
|
|
24
|
+
['主题不存在', 'Theme not found'],
|
|
25
|
+
['没有已配对的码得', 'No made device is paired'],
|
|
26
|
+
['主题未能下发', 'Could not send the theme'],
|
|
27
|
+
['请先复制 config.example.json 为 config.local.json', 'Copy config.example.json to config.local.json first'],
|
|
28
|
+
['USB 接入仅允许本机串口转发', 'USB access requires the local serial relay'],
|
|
29
|
+
['探针格式无效', 'Invalid probe request'],
|
|
30
|
+
['校验请求格式无效', 'Invalid verification request'],
|
|
31
|
+
['接入地址未获桥接器授权', 'The bridge has not authorized this address'],
|
|
32
|
+
['设备未配对', 'Device is not paired'],
|
|
33
|
+
['设备未授权', 'Device is not authorized'],
|
|
34
|
+
['未知编程工具', 'Unknown coding assistant'],
|
|
35
|
+
['任务不存在', 'Task not found'],
|
|
36
|
+
['码得配置超过 32 KB,请检查助手元数据', 'Device configuration exceeds 32 KB; check assistant metadata'],
|
|
37
|
+
['另一段录音正在识别,请稍后再试', 'Another recording is being transcribed; try again shortly'],
|
|
38
|
+
['请上传 WAV 录音', 'Upload a WAV recording'],
|
|
39
|
+
['语音不存在', 'Voice clip is missing'],
|
|
40
|
+
['语音超过 256KB', 'Voice clip exceeds 256 KB'],
|
|
41
|
+
['请先新建任务', 'Create a task first'],
|
|
42
|
+
['项目不在本机允许列表中', 'Project is not in the local allowlist'],
|
|
43
|
+
['任务与编程工具或项目不匹配', 'Task does not match the assistant or project'],
|
|
44
|
+
['任务内容需在 3 到 2000 字之间', 'Instructions must contain 3 to 2000 characters'],
|
|
45
|
+
['不支持的编程工具', 'Unsupported coding assistant'],
|
|
46
|
+
['请先完成或取消当前任务的上一条指令', 'Complete or cancel the previous instruction first'],
|
|
47
|
+
['编程工具未返回可恢复的会话 ID,无法继续此任务;请新建任务', 'The assistant did not return a resumable session ID; create a new task'],
|
|
48
|
+
['当前任务不能确认', 'This task cannot be confirmed now'],
|
|
49
|
+
['编程工具尚未就绪', 'The coding assistant is not ready'],
|
|
50
|
+
['当前任务不能取消', 'This task cannot be cancelled now'],
|
|
51
|
+
['这个编程工具不支持撤回正在执行的任务,请在工具中处理', 'This assistant cannot cancel a running task; handle it in the assistant'],
|
|
52
|
+
['这个编程工具不支持撤回正在执行的任务,请先在工具中结束执行', 'This assistant cannot cancel a running task; finish it in the assistant before deleting this task'],
|
|
53
|
+
['编程工具插件返回了无效的任务状态', 'The assistant plugin returned an invalid task status'],
|
|
54
|
+
['桥接客户端重启,任务执行已中断。请检查代码目录后重新提交。', 'The bridge restarted and interrupted this task. Check the project before submitting again.'],
|
|
55
|
+
['任务标题必须是文字', 'Task title must be text'],
|
|
56
|
+
['任务标题需在 1 到 60 字之间', 'Task title must contain 1 to 60 characters'],
|
|
57
|
+
['新任务', 'New task'],
|
|
58
|
+
['编程工具插件未加载,请先安装或启用对应插件', 'The assistant plugin is not loaded; install or enable it first'],
|
|
59
|
+
['插件状态检查必须同步返回', 'The plugin status check must return synchronously'],
|
|
60
|
+
['任务已取消', 'Task cancelled'],
|
|
61
|
+
['默认编程助手未安装或已禁用', 'The default assistant is not installed or is disabled'],
|
|
62
|
+
['配对请求格式无效', 'Invalid pairing request'],
|
|
63
|
+
['设备名称无效', 'Invalid device name'],
|
|
64
|
+
['电脑配对窗口未开启', 'Pairing is not open on the computer'],
|
|
65
|
+
['待配对设备过多', 'Too many pending devices'],
|
|
66
|
+
['配对尝试过于频繁', 'Too many pairing attempts'],
|
|
67
|
+
['配对请求已失效', 'Pairing request has expired'],
|
|
68
|
+
['校验请求过于频繁', 'Too many verification requests'],
|
|
69
|
+
['语音 API Key 格式无效', 'Invalid speech API key'],
|
|
70
|
+
['密钥存储文件无效', 'Invalid API key storage file'],
|
|
71
|
+
['本机保存的 OpenAI 密钥格式无效', 'The locally saved OpenAI API key is invalid'],
|
|
72
|
+
['请填写当前已安装助手的模型设置', 'Enter model settings for installed assistants'],
|
|
73
|
+
['模型设置格式无效', 'Invalid model settings'],
|
|
74
|
+
['模型设置文件无效', 'Invalid model settings file'],
|
|
75
|
+
['录音大小不符合要求(最多约 30 秒)', 'Invalid recording size (up to approximately 30 seconds)'],
|
|
76
|
+
['录音必须是 16 kHz、16 bit、单声道 PCM WAV', 'Recording must be 16 kHz, 16-bit, mono PCM WAV'],
|
|
77
|
+
['录音内容长度无效(需 0.5 到 30 秒)', 'Recording must be 0.5 to 30 seconds long'],
|
|
78
|
+
['所选语音识别插件尚未安装', 'The selected speech recognizer is not installed'],
|
|
79
|
+
['语音识别超时,请重试', 'Speech recognition timed out; try again'],
|
|
80
|
+
['语音识别插件必须返回文字', 'The speech recognizer must return text'],
|
|
81
|
+
['识别文字过长或过短,请重试', 'The transcript is too long or too short; try again'],
|
|
82
|
+
['语音识别尚未配置或未听清内容', 'Speech recognition is not configured or no clear speech was detected'],
|
|
83
|
+
['语音接口 URL 无效', 'Invalid transcription URL'],
|
|
84
|
+
['语音接口必须使用 HTTPS;仅本机代理允许 HTTP', 'The speech endpoint must use HTTPS; HTTP is allowed only for a local proxy'],
|
|
85
|
+
['语音接口 URL 不能包含账号、密码或片段,且须包含接口路径', 'The speech URL must include an API path and no credentials or fragment'],
|
|
86
|
+
['语音模型名称无效', 'Invalid speech model name'],
|
|
87
|
+
['语音设置文件无效', 'Invalid speech settings file'],
|
|
88
|
+
['语音插件设置格式无效', 'Invalid speech settings'],
|
|
89
|
+
['语音插件设置文件无效', 'Invalid speech settings file'],
|
|
90
|
+
['语音插件设置文件格式无效', 'Invalid speech settings file format'],
|
|
91
|
+
['请选择语音识别插件', 'Select a speech recognizer'],
|
|
92
|
+
['未安装所选语音识别插件', 'The selected speech recognizer is not installed'],
|
|
93
|
+
['要清除的语音密钥字段无效', 'Invalid speech secret field to clear'],
|
|
94
|
+
['不能同时设置和清除同一语音密钥', 'Cannot set and clear the same speech secret together'],
|
|
95
|
+
['语音识别插件执行失败', 'Speech recognition failed'],
|
|
96
|
+
['语音插件 probe 必须同步返回 available', 'The speech status check must synchronously return available'],
|
|
97
|
+
['所选语音插件尚未安装', 'The selected speech recognizer is not installed'],
|
|
98
|
+
['OpenAI 兼容语音接口', 'OpenAI-compatible speech API'],
|
|
99
|
+
['转写接口 URL', 'Transcription URL'],
|
|
100
|
+
['模型名', 'Model'],
|
|
101
|
+
['硅基流动', 'SiliconFlow'],
|
|
102
|
+
['请在电脑页面配置语音 API Key', 'Configure the speech API key on the computer'],
|
|
103
|
+
['无法连接语音识别服务,请检查电脑侧接口 URL 和网络', 'Cannot reach the speech service; check the endpoint URL and computer network'],
|
|
104
|
+
['语音识别服务未返回 JSON 结果', 'The speech service did not return JSON'],
|
|
105
|
+
['关闭语音识别', 'Speech recognition off'],
|
|
106
|
+
['语音识别已关闭', 'Speech recognition is off'],
|
|
107
|
+
['本机 Whisper', 'Local Whisper'],
|
|
108
|
+
['Whisper 程序', 'Whisper executable'],
|
|
109
|
+
['本机模型路径', 'Local model path'],
|
|
110
|
+
['请先选择已下载到电脑的 Whisper 模型', 'Select a Whisper model downloaded to this computer'],
|
|
111
|
+
['本地识别未生成文字', 'Local speech recognition returned no transcript'],
|
|
112
|
+
['本地识别文字过长,请重试', 'The local transcript is too long; try again'],
|
|
113
|
+
['无法配置 USB 串口,请检查端口权限', 'Cannot configure USB serial; check port permissions'],
|
|
114
|
+
['设备接口响应过大', 'Device API response is too large'],
|
|
115
|
+
['USB 帧过长', 'USB frame is too long'],
|
|
116
|
+
['USB 帧格式无效', 'Invalid USB frame format'],
|
|
117
|
+
['USB 请求重叠', 'Overlapping USB requests'],
|
|
118
|
+
['USB 请求格式无效', 'Invalid USB request'],
|
|
119
|
+
['USB 数据帧无效', 'Invalid USB data frame'],
|
|
120
|
+
['USB 数据长度无效', 'Invalid USB data length'],
|
|
121
|
+
['USB 请求长度不符', 'USB request length does not match'],
|
|
122
|
+
['USB 请求超时', 'USB request timed out'],
|
|
123
|
+
['电脑设备接口暂不可用', 'The computer device API is temporarily unavailable'],
|
|
124
|
+
['USB 帧类型无效', 'Invalid USB frame type'],
|
|
125
|
+
['此系统需手动配置 VIBE_USB_PORT', 'Set VIBE_USB_PORT manually on this system'],
|
|
126
|
+
['USB 握手心跳超时', 'USB handshake heartbeat timed out'],
|
|
127
|
+
['USB 设备未连接', 'USB device is not connected'],
|
|
128
|
+
['未发现受支持的 USB 设备;可设置 VIBE_USB_PORT', 'No supported USB device found; you can set VIBE_USB_PORT'],
|
|
129
|
+
['USB 响应帧过长', 'USB response frame is too long'],
|
|
130
|
+
['USB 设备已断开', 'USB device disconnected'],
|
|
131
|
+
['USB 写入超时', 'USB write timed out'],
|
|
132
|
+
['USB 已连接,等待打开码得应用的 USB 直连', 'USB connected; open made and enable USB direct mode'],
|
|
133
|
+
['无法打开 USB 串口,请检查端口权限或占用情况', 'Cannot open USB serial; check permissions and whether the port is in use'],
|
|
134
|
+
['ngrok Authtoken 格式无效', 'Invalid ngrok Authtoken'],
|
|
135
|
+
['固定地址必须是 HTTPS 公网域名', 'The fixed address must be a public HTTPS domain'],
|
|
136
|
+
['固定地址须为 HTTPS 域名,不含路径、端口或参数', 'Use an HTTPS domain without a path, port, or query'],
|
|
137
|
+
['Cloudflare Tunnel 令牌格式无效', 'Invalid Cloudflare Tunnel token'],
|
|
138
|
+
['请先关闭当前穿透,再修改设置', 'Stop the current tunnel before changing settings'],
|
|
139
|
+
['请选择有效的公网接入方式', 'Select a valid public connection method'],
|
|
140
|
+
['清除令牌参数无效', 'Invalid clear-token parameter'],
|
|
141
|
+
['不能同时设置和清除令牌', 'Cannot set and clear a token together'],
|
|
142
|
+
['固定地址模式需要 HTTPS 域名和 Cloudflare Tunnel 令牌', 'Fixed-address mode requires an HTTPS domain and Cloudflare Tunnel token'],
|
|
143
|
+
['ngrok 需要先填写 Authtoken', 'Enter an ngrok Authtoken first'],
|
|
144
|
+
['请填写花生壳 HTTPS 映射域名', 'Enter the Oray HTTPS mapping domain'],
|
|
145
|
+
['请填写花生壳 API Key', 'Enter the Oray API key'],
|
|
146
|
+
['桥接器未配置状态目录', 'The bridge state directory is not configured'],
|
|
147
|
+
['请先保存固定域名和 Cloudflare Tunnel 令牌。', 'Save the fixed domain and Cloudflare Tunnel token first.'],
|
|
148
|
+
['请先在电脑端保存 ngrok Authtoken。', 'Save the ngrok Authtoken on the computer first.'],
|
|
149
|
+
['检测到指向本机码得接口的 Tailscale 公网路由。请点击“清理残留路由”后重试。', 'A Tailscale public route points to this made API. Clear the remaining route and try again.'],
|
|
150
|
+
['Tailscale 的 443 端口已有服务;请检查 tailscale funnel status,勿覆盖原有 Serve/Funnel。', 'Tailscale port 443 already has a service. Check tailscale funnel status without overwriting existing Serve/Funnel settings.'],
|
|
151
|
+
['未安装 tailscale,请先在电脑上安装并登录。', 'Install Tailscale on the computer and sign in first.'],
|
|
152
|
+
['无法读取 Tailscale 状态,请检查登录、运行状态与 Funnel 权限。', 'Cannot read Tailscale status; check sign-in, running state, and Funnel permissions.'],
|
|
153
|
+
['无法准备 ngrok 本机配置,请检查状态目录。', 'Cannot prepare ngrok configuration; check the state directory.'],
|
|
154
|
+
['无法启动内网穿透。', 'Cannot start the tunnel.'],
|
|
155
|
+
['内网穿透启动失败。', 'Tunnel startup failed.'],
|
|
156
|
+
['内网穿透连接已断开,请重新开启。', 'The tunnel disconnected; start it again.'],
|
|
157
|
+
['Tailscale Funnel 未能建立,请检查登录和 Funnel 权限。', 'Tailscale Funnel failed; check sign-in and Funnel permissions.'],
|
|
158
|
+
['ngrok 未能建立,请检查 Authtoken、域名与网络连接。', 'ngrok failed; check the Authtoken, domain, and network.'],
|
|
159
|
+
['未能建立内网穿透,请检查 cloudflared 与网络连接。', 'The tunnel failed; check cloudflared and the network.'],
|
|
160
|
+
['Tailscale 公网路由已失效,请关闭后重新开启。', 'The Tailscale public route is no longer valid; stop and restart it.'],
|
|
161
|
+
['无法核实 Tailscale 公网路由,请检查客户端状态。', 'Cannot verify the Tailscale public route; check the client status.'],
|
|
162
|
+
['ngrok 公网路由已失效或转发目标不符。', 'The ngrok public route is invalid or has an unexpected target.'],
|
|
163
|
+
['无法核实 ngrok 公网路由,已关闭穿透。', 'Cannot verify the ngrok public route; the tunnel was stopped.'],
|
|
164
|
+
['ngrok 启动超时,请检查 Authtoken、域名与网络连接。', 'ngrok startup timed out; check the Authtoken, domain, and network.'],
|
|
165
|
+
['Tailscale Funnel 启动超时,请检查登录和 Funnel 权限。', 'Tailscale Funnel startup timed out; check sign-in and Funnel permissions.'],
|
|
166
|
+
['内网穿透连接超时,请检查 cloudflared、令牌与网络连接。', 'Tunnel connection timed out; check cloudflared, the token, and network.'],
|
|
167
|
+
['无法确认 Tailscale 公网路由已关闭,请在电脑上运行 tailscale funnel status 检查。', 'Cannot confirm that the Tailscale public route is closed; run tailscale funnel status on the computer.'],
|
|
168
|
+
['花生壳 API Key 格式无效', 'Invalid Oray API key'],
|
|
169
|
+
['请输入花生壳 HTTPS 域名', 'Enter an Oray HTTPS domain'],
|
|
170
|
+
['花生壳地址须为 HTTPS 域名,不含端口、路径或参数', 'Use an Oray HTTPS domain without a port, path, or query'],
|
|
171
|
+
['设备接口端口无效', 'Invalid device API port'],
|
|
172
|
+
['请先停用已选花生壳映射,再修改设置', 'Disable the selected Oray mapping before changing settings'],
|
|
173
|
+
['API Key 设置冲突', 'Conflicting API key settings'],
|
|
174
|
+
['请先保存花生壳 API Key', 'Save the Oray API key first'],
|
|
175
|
+
['无法连接花生壳开放 API,请检查网络', 'Cannot reach the Oray API; check the network'],
|
|
176
|
+
['花生壳 API Key 无效或无权限', 'The Oray API key is invalid or lacks permission'],
|
|
177
|
+
['花生壳开放 API 返回的数据无效', 'The Oray API returned invalid data'],
|
|
178
|
+
['花生壳映射列表格式无效', 'Invalid Oray mapping list'],
|
|
179
|
+
['请先选择花生壳 HTTPS 域名', 'Select an Oray HTTPS domain first'],
|
|
180
|
+
['未找到唯一的 HTTPS/443 映射,请先在花生壳创建并选择该域名', 'No unique HTTPS/443 mapping found; create it in Oray and select its domain'],
|
|
181
|
+
['无法读取花生壳账号 ID', 'Cannot read the Oray account ID'],
|
|
182
|
+
['花生壳映射状态尚未更新,请稍后刷新状态', 'The Oray mapping state has not updated; refresh shortly'],
|
|
183
|
+
['桥接器身份尚未准备好', 'The bridge identity is not ready'],
|
|
184
|
+
['映射已启用,但无法验证公网地址指向本桥接器;请检查花生壳客户端已登录且映射保留公网 Host', 'Mapping enabled, but its public address could not be verified; check Oray sign-in and preserve the public Host header'],
|
|
185
|
+
['编程工具报告任务失败', 'The coding assistant reported a task failure'],
|
|
186
|
+
['正在分析任务', 'Analyzing task'],
|
|
187
|
+
['正在整理结果', 'Preparing results'],
|
|
188
|
+
['资料检索完成', 'Research complete'],
|
|
189
|
+
['正在查找资料', 'Researching'],
|
|
190
|
+
['已更新任务步骤', 'Task steps updated'],
|
|
191
|
+
['正在整理答复', 'Preparing reply'],
|
|
192
|
+
['文件读取完成', 'File read complete'],
|
|
193
|
+
['正在读取文件', 'Reading files'],
|
|
194
|
+
['文件修改完成', 'File changes complete'],
|
|
195
|
+
['正在修改文件', 'Editing files'],
|
|
196
|
+
['命令执行完成', 'Command complete'],
|
|
197
|
+
['正在运行命令', 'Running command'],
|
|
198
|
+
['代码检索完成', 'Code search complete'],
|
|
199
|
+
['正在检索代码', 'Searching code'],
|
|
200
|
+
['工具操作完成', 'Tool operation complete'],
|
|
201
|
+
['正在使用工具', 'Using tool'],
|
|
202
|
+
['已连接编程工具', 'Coding assistant connected'],
|
|
203
|
+
['Codex 对话正被使用,项目文件也发生变化。请先检查改动。', 'This Codex session is in use and the project has changed. Review the changes first.'],
|
|
204
|
+
['此 Codex 对话正在另一窗口或进程使用;结束后再次确认即可继续', 'This Codex session is in use in another window or process; confirm again after it finishes'],
|
|
205
|
+
['Codex 当前模型不能用于这个登录账号。请在电脑桥接页面的“编程助手模型”中选择该账号可用的模型。', 'This Codex model is unavailable for the signed-in account. Choose an available model in the bridge assistant settings.'],
|
|
206
|
+
['WorkBuddy 开放平台尚未授权或未开启转交模式', 'WorkBuddy authorization or dispatch mode is not enabled'],
|
|
207
|
+
['WorkBuddy 消息历史格式无效', 'Invalid WorkBuddy message history'],
|
|
208
|
+
['可转交任务;如获消息读取授权可回显回复,无法保证指定代码目录或撤回', 'Dispatch available; reply access requires permission. Project targeting and cancellation are not guaranteed'],
|
|
209
|
+
['需 WorkBuddy 开放平台授权,并显式允许无项目目录保证的转交模式', 'WorkBuddy authorization and explicit permission for dispatch without project targeting are required'],
|
|
210
|
+
['无法检查项目改动状态', 'Cannot check project changes'],
|
|
211
|
+
['目标项目必须是 Git 仓库,以便查看和恢复改动', 'The project must be a Git repository so changes can be reviewed and restored'],
|
|
212
|
+
['请把项目路径设为 Git 仓库根目录', 'Set the project path to the Git repository root'],
|
|
213
|
+
['项目文件自上一轮会话后发生变化,请先检查改动再继续', 'Project files changed after the previous turn; review them before continuing'],
|
|
214
|
+
['项目已有未提交改动。请先保存或提交,再确认语音任务', 'The project has uncommitted changes. Save or commit them before confirming the voice task'],
|
|
215
|
+
['任务运行超过 20 分钟,已停止', 'Task exceeded 20 minutes and was stopped'],
|
|
216
|
+
['请在电脑端检查 CLI 登录和权限设置', 'Check CLI sign-in and permissions on the computer'],
|
|
217
|
+
['请只修改当前仓库中的文件;不要提交、推送、发布或删除仓库。完成后说明改动与已运行的验证。', 'Only modify files in the current repository. Do not commit, push, publish, or delete the repository. When finished, describe the changes and the checks you ran.'],
|
|
218
|
+
['改动文件:', 'Changed files:'],
|
|
219
|
+
['没有检测到代码改动。', 'No code changes detected.'],
|
|
220
|
+
['任务已执行,未返回文字摘要。', 'Task executed without a text summary.'],
|
|
221
|
+
['当前授权不能读取 WorkBuddy 消息;需 user.localassistant.readable 权限。请在 WorkBuddy 中查看回复。', 'Current authorization cannot read WorkBuddy messages; user.localassistant.readable is required. View replies in WorkBuddy.'],
|
|
222
|
+
['出现其他 WorkBuddy 用户消息;后续回复无法可靠归属此任务,请在 WorkBuddy 中查看。', 'Another WorkBuddy user message appeared. Further replies cannot be reliably assigned to this task; view them in WorkBuddy.'],
|
|
223
|
+
['助理请求进一步确认,请在 WorkBuddy 中处理。', 'The assistant requests confirmation; handle it in WorkBuddy.'],
|
|
224
|
+
['助理有一条新消息,请在 WorkBuddy 中查看详情。', 'The assistant has a new message; view it in WorkBuddy.'],
|
|
225
|
+
['WorkBuddy 后续助理消息(任务关联未验证):', 'Further WorkBuddy assistant message (task association unverified):'],
|
|
226
|
+
]);
|
|
227
|
+
const templates = [
|
|
228
|
+
[/^(Codex|Cursor|Qoder):编程工具报告任务失败$/, (_, label) => `${label}: The coding assistant reported a task failure`],
|
|
229
|
+
[/^请先在本机登录 (.+)$/s, (_, command) => `Sign in to ${command} on this computer first`],
|
|
230
|
+
[/^本机未找到 (.+) 命令$/s, (_, command) => `${command} was not found on this computer`],
|
|
231
|
+
[/^(.+) 状态检查失败$/s, (_, label) => `${label} status check failed`],
|
|
232
|
+
[/^(.+) 模型名称无效$/s, (_, provider) => `Invalid model name for ${provider}`],
|
|
233
|
+
[/^语音识别服务返回 (\d+);请检查电脑侧 URL、API Key 和模型$/, (_, status) => `Speech service returned ${status}; check the URL, API key, and model on the computer`],
|
|
234
|
+
[/^花生壳开放 API 返回 (\d+),请检查账号服务及映射配置$/, (_, status) => `Oray API returned ${status}; check the account service and mapping settings`],
|
|
235
|
+
[/^花生壳映射须指向本机 127\.0\.0\.1:(\d+),已拒绝操作其他服务$/, (_, port) => `The Oray mapping must target 127.0.0.1:${port}; other services will not be changed`],
|
|
236
|
+
[/^未安装 (tailscale|ngrok|cloudflared),请先在电脑上安装。$/, (_, command) => `Install ${command} on this computer first.`],
|
|
237
|
+
[/^代码目录不存在:([\s\S]*)$/, (_, directory) => `Project directory does not exist: ${directory}`],
|
|
238
|
+
[/^(.+) 退出码 (null|-?\d+):([\s\S]*)$/, (_, label, code, detail) => `${label} exited with code ${code}: ${detail}`],
|
|
239
|
+
[/^本地语音识别失败(退出码 (null|-?\d+)):([\s\S]*)$/, (_, code, detail) => `Local speech recognition failed (exit code ${code}): ${detail}`],
|
|
240
|
+
[/^WorkBuddy 转交失败:([\s\S]*)$/, (_, detail) => `WorkBuddy dispatch failed: ${detail}`],
|
|
241
|
+
[/^WorkBuddy 消息读取失败:([\s\S]*)$/, (_, detail) => `WorkBuddy message read failed: ${detail}`],
|
|
242
|
+
[/^语音插件不支持设置项:(.+)$/, (_, field) => `Unsupported speech setting: ${field}`],
|
|
243
|
+
[/^(转写接口 URL|API Key|模型名|Whisper 程序|本机模型路径) 格式无效$/, (_, label) => `Invalid ${translateKnownMessage(label, 'en')}`],
|
|
244
|
+
[/^请先填写(转写接口 URL|API Key|模型名|Whisper 程序|本机模型路径)$/, (_, label) => `Enter ${translateKnownMessage(label, 'en')} first`],
|
|
245
|
+
];
|
|
246
|
+
export function translateKnownMessage(message, locale = 'zh-CN') {
|
|
247
|
+
if (locale !== 'en' || typeof message !== 'string' || !message) return message;
|
|
248
|
+
if (messages.has(message)) return messages.get(message);
|
|
249
|
+
for (const [pattern, render] of templates) {
|
|
250
|
+
const match = pattern.exec(message);
|
|
251
|
+
if (match) return render(...match);
|
|
252
|
+
}
|
|
253
|
+
return message;
|
|
254
|
+
}
|
|
255
|
+
export function localizeStatus(value, locale) {
|
|
256
|
+
return { ...value, ...(typeof value?.error === 'string' ? { error: translateKnownMessage(value.error, locale) } : {}) };
|
|
257
|
+
}
|