u1s1-cli 1.11.8 → 1.12.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/dist/agent-setup.d.ts +5 -0
- package/dist/agent-setup.js +31 -7
- package/dist/api.js +42 -34
- package/dist/bench-report.js +12 -9
- package/dist/bench.js +34 -31
- package/dist/brand.d.ts +2 -1
- package/dist/brand.js +38 -12
- package/dist/config.d.ts +11 -1
- package/dist/config.js +25 -8
- package/dist/context-usage.js +12 -9
- package/dist/deploy.js +49 -45
- package/dist/device-auth.js +7 -5
- package/dist/error-humanize.d.ts +1 -0
- package/dist/error-humanize.js +18 -6
- package/dist/feedback.js +48 -31
- package/dist/i18n.d.ts +21 -0
- package/dist/i18n.js +46 -0
- package/dist/import/index.js +64 -28
- package/dist/import/skills.js +72 -41
- package/dist/import/util.js +3 -2
- package/dist/import/write.js +2 -1
- package/dist/index.js +139 -48
- package/dist/login.js +39 -28
- package/dist/loop.js +18 -12
- package/dist/mcp/client.js +15 -14
- package/dist/mcp/command.js +76 -48
- package/dist/mcp/extension.js +5 -3
- package/dist/mcp/tools.js +6 -5
- package/dist/model.js +11 -10
- package/dist/nudges.d.ts +8 -2
- package/dist/nudges.js +22 -3
- package/dist/search-tools.js +7 -6
- package/dist/shell-doctor.js +9 -8
- package/dist/subagent.js +5 -4
- package/dist/tools.js +53 -37
- package/dist/update.js +29 -28
- package/dist/usage.d.ts +1 -1
- package/dist/usage.js +96 -43
- package/dist/web.js +2 -1
- package/dist/workflow/runner.js +33 -29
- package/dist/workflow/templates.js +70 -45
- package/dist/workflow/tool.js +15 -12
- package/package.json +2 -2
- package/scripts/patch-pi.js +243 -278
- package/scripts/render-regression.mjs +55 -4
package/dist/model.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { fetchModels, loadCustomEndpoints } from "./api.js";
|
|
2
2
|
import { apiModelToDef, CUSTOM_ENDPOINTS, findModel, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, resolvePreferredModel, setModelsFromApi, } from "./config.js";
|
|
3
3
|
import { siteLinks, siteOf } from "./site.js";
|
|
4
|
+
import { tr } from "./i18n.js";
|
|
4
5
|
/** Refresh official models while preserving the built-in list as an explicit offline fallback. */
|
|
5
6
|
export async function refreshOfficialModels(cfg, fetcher = fetchModels) {
|
|
6
7
|
try {
|
|
7
8
|
const { models } = await fetcher(cfg);
|
|
8
9
|
if (models.length === 0)
|
|
9
|
-
return "线上模型列表为空";
|
|
10
|
+
return tr("线上模型列表为空", "the online model list is empty");
|
|
10
11
|
setModelsFromApi(models.map(apiModelToDef));
|
|
11
12
|
return null;
|
|
12
13
|
}
|
|
@@ -25,7 +26,7 @@ export async function modelCommand(nameOrAlias) {
|
|
|
25
26
|
loadCustomEndpoints(cfg),
|
|
26
27
|
]);
|
|
27
28
|
if (officialModelsError) {
|
|
28
|
-
console.error(` 获取线上模型列表失败,使用内置列表:${officialModelsError}`);
|
|
29
|
+
console.error(tr(` 获取线上模型列表失败,使用内置列表:${officialModelsError}`, ` Couldn't fetch the online model list; using the built-in list: ${officialModelsError}`));
|
|
29
30
|
}
|
|
30
31
|
const current = resolvePreferredModel(cfg);
|
|
31
32
|
if (!nameOrAlias) {
|
|
@@ -34,26 +35,26 @@ export async function modelCommand(nameOrAlias) {
|
|
|
34
35
|
const mark = current.provider === PROVIDER_ID && m.id === current.id ? "●" : " ";
|
|
35
36
|
console.log(` ${mark} ${m.aliases[0].padEnd(10)} ${m.name}`);
|
|
36
37
|
// 价格来自网关目录;离线兜底目录不带价格,只列名字与提示
|
|
37
|
-
const price = m.cost ? `$${m.cost.input}/$${m.cost.output} 每百万 token` : "";
|
|
38
|
+
const price = m.cost ? tr(`$${m.cost.input}/$${m.cost.output} 每百万 token`, `$${m.cost.input}/$${m.cost.output} per 1M tokens`) : "";
|
|
38
39
|
const detail = [m.note, price].filter(Boolean).join(" · ");
|
|
39
40
|
if (detail)
|
|
40
41
|
console.log(` ${detail}`);
|
|
41
42
|
}
|
|
42
43
|
for (const ep of CUSTOM_ENDPOINTS) {
|
|
43
44
|
console.log("");
|
|
44
|
-
console.log(` ─ 自定义端点 ${ep.name}(${ep.baseUrl},走本地直连不计费)`);
|
|
45
|
+
console.log(tr(` ─ 自定义端点 ${ep.name}(${ep.baseUrl},走本地直连不计费)`, ` ─ Custom endpoint ${ep.name} (${ep.baseUrl}, direct connection, not billed)`));
|
|
45
46
|
for (const m of ep.models) {
|
|
46
47
|
const mark = current.provider === ep.id && m.id === current.id ? "●" : " ";
|
|
47
48
|
console.log(` ${mark} ${m.id.padEnd(10)} ${m.name === m.id ? "" : m.name}`);
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
console.log("");
|
|
51
|
-
console.log(" 切换:u1s1 model <model-id>(对话里 /model 同样会记住)");
|
|
52
|
+
console.log(tr(" 切换:u1s1 model <model-id>(对话里 /model 同样会记住)", " Switch: u1s1 model <model-id> (/model in a session is remembered too)"));
|
|
52
53
|
if (CUSTOM_ENDPOINTS.length) {
|
|
53
|
-
console.log(" 端点模型重名时可加限定:u1s1 model 端点名:模型id");
|
|
54
|
+
console.log(tr(" 端点模型重名时可加限定:u1s1 model 端点名:模型id", " If endpoint model names clash, qualify them: u1s1 model <endpoint>:<model-id>"));
|
|
54
55
|
}
|
|
55
56
|
else {
|
|
56
|
-
console.log(` 想接自己的 API(含本机 Ollama)?去 ${siteLinks(siteOf(cfg)).dashboard}
|
|
57
|
+
console.log(tr(` 想接自己的 API(含本机 Ollama)?去 ${siteLinks(siteOf(cfg)).dashboard} 配「自定义模型端点」`, ` Want to use your own API (including local Ollama)? Add a custom model endpoint at ${siteLinks(siteOf(cfg)).dashboard}`));
|
|
57
58
|
}
|
|
58
59
|
console.log("");
|
|
59
60
|
return;
|
|
@@ -62,14 +63,14 @@ export async function modelCommand(nameOrAlias) {
|
|
|
62
63
|
if (!m) {
|
|
63
64
|
const custom = CUSTOM_ENDPOINTS.flatMap((e) => e.models.map((x) => x.id));
|
|
64
65
|
const all = [...MODELS.map((x) => x.aliases[0]), ...custom];
|
|
65
|
-
console.error(` 没有叫「${nameOrAlias}」的模型,可选:${all.join(" / ")}`);
|
|
66
|
+
console.error(tr(` 没有叫「${nameOrAlias}」的模型,可选:${all.join(" / ")}`, ` No model named "${nameOrAlias}". Available: ${all.join(" / ")}`));
|
|
66
67
|
process.exit(1);
|
|
67
68
|
}
|
|
68
69
|
persistPreferredModel(cfg, m.provider, m.id);
|
|
69
70
|
if (m.provider === PROVIDER_ID) {
|
|
70
|
-
console.log(` ✓ 默认模型已切到 ${m.model.name}${m.model.note ? `(${m.model.note})` : ""}`);
|
|
71
|
+
console.log(tr(` ✓ 默认模型已切到 ${m.model.name}${m.model.note ? `(${m.model.note})` : ""}`, ` ✓ Default model set to ${m.model.name}${m.model.note ? ` (${m.model.note})` : ""}`));
|
|
71
72
|
}
|
|
72
73
|
else {
|
|
73
|
-
console.log(` ✓ 默认模型已切到 ${m.model.name}(自定义端点 ${m.providerName},本地直连不计费)`);
|
|
74
|
+
console.log(tr(` ✓ 默认模型已切到 ${m.model.name}(自定义端点 ${m.providerName},本地直连不计费)`, ` ✓ Default model set to ${m.model.name} (custom endpoint ${m.providerName}, direct connection, not billed)`));
|
|
74
75
|
}
|
|
75
76
|
}
|
package/dist/nudges.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SiteId } from "./site.js";
|
|
1
2
|
/**
|
|
2
3
|
* 「只提示一次」的机器级标记,落在 ~/.u1s1/nudges.json,CLI 与桌面版共用
|
|
3
4
|
* (桌面版经 u1s1-cli/embed 拿到同一份 helper)。值是 ISO 时间戳,便于事后
|
|
@@ -16,6 +17,11 @@ export declare const nudgesFile: string;
|
|
|
16
17
|
export declare function readNudges(file?: string): NudgeState;
|
|
17
18
|
export declare function hasNudge(key: NudgeKey, file?: string): boolean;
|
|
18
19
|
export declare function markNudge(key: NudgeKey, file?: string): void;
|
|
19
|
-
/**
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* 会话里首次写/改文件后要不要提示 deploy:没提示过、也没部署过才提示。
|
|
22
|
+
* 国际站没有站点托管(网关拒绝 deploy),一律不提示。
|
|
23
|
+
*/
|
|
24
|
+
export declare function shouldShowDeployHint(file?: string, site?: SiteId): boolean;
|
|
25
|
+
/** 老导出,固定中文;新代码用 deployHintText()(按当前语言现取)。 */
|
|
21
26
|
export declare const DEPLOY_HINT_TEXT = "\u505A\u597D\u4E86\u60F3\u7ED9\u670B\u53CB\u770B\uFF1F\u8F93\u5165 /exit \u56DE\u5230\u7EC8\u7AEF,\u8FD0\u884C u1s1 deploy \u4E00\u952E\u4E0A\u7EBF";
|
|
27
|
+
export declare function deployHintText(): string;
|
package/dist/nudges.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { u1s1Dir } from "./config.js";
|
|
3
|
+
import { activeSite, u1s1Dir } from "./config.js";
|
|
4
|
+
import { tr } from "./i18n.js";
|
|
4
5
|
export const nudgesFile = join(u1s1Dir, "nudges.json");
|
|
5
6
|
export function readNudges(file = nudgesFile) {
|
|
6
7
|
try {
|
|
@@ -32,9 +33,27 @@ export function markNudge(key, file = nudgesFile) {
|
|
|
32
33
|
// ignore
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
|
-
/**
|
|
36
|
-
|
|
36
|
+
/**
|
|
37
|
+
* 会话里首次写/改文件后要不要提示 deploy:没提示过、也没部署过才提示。
|
|
38
|
+
* 国际站没有站点托管(网关拒绝 deploy),一律不提示。
|
|
39
|
+
*/
|
|
40
|
+
export function shouldShowDeployHint(file = nudgesFile, site) {
|
|
41
|
+
let siteId = site;
|
|
42
|
+
if (!siteId) {
|
|
43
|
+
try {
|
|
44
|
+
siteId = activeSite().id;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
siteId = "cn";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (siteId === "intl")
|
|
51
|
+
return false;
|
|
37
52
|
const state = readNudges(file);
|
|
38
53
|
return state.deploy_hint_shown === undefined && state.deploy_done === undefined;
|
|
39
54
|
}
|
|
55
|
+
/** 老导出,固定中文;新代码用 deployHintText()(按当前语言现取)。 */
|
|
40
56
|
export const DEPLOY_HINT_TEXT = "做好了想给朋友看?输入 /exit 回到终端,运行 u1s1 deploy 一键上线";
|
|
57
|
+
export function deployHintText() {
|
|
58
|
+
return tr(DEPLOY_HINT_TEXT, "Want to show it off? Type /exit to return to the terminal, then run u1s1 deploy to publish it.");
|
|
59
|
+
}
|
package/dist/search-tools.js
CHANGED
|
@@ -5,6 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { Readable, Transform } from "node:stream";
|
|
6
6
|
import { pipeline } from "node:stream/promises";
|
|
7
7
|
import { agentDir } from "./config.js";
|
|
8
|
+
import { tr } from "./i18n.js";
|
|
8
9
|
/**
|
|
9
10
|
* 预 seed pi 的搜索组件(fd / ripgrep)到 ~/.u1s1/agent/bin。
|
|
10
11
|
*
|
|
@@ -104,7 +105,7 @@ function findBinary(root, name) {
|
|
|
104
105
|
async function install(tool, baseUrl, binName) {
|
|
105
106
|
const asset = tool.asset(tool.version);
|
|
106
107
|
if (!asset)
|
|
107
|
-
throw new Error(`不支持的平台 ${platform()}/${arch()}`);
|
|
108
|
+
throw new Error(tr(`不支持的平台 ${platform()}/${arch()}`, `Unsupported platform ${platform()}/${arch()}`));
|
|
108
109
|
mkdirSync(binDir, { recursive: true });
|
|
109
110
|
const res = await fetch(new URL(`/tools/${asset}`, baseUrl), {
|
|
110
111
|
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
|
|
@@ -114,7 +115,7 @@ async function install(tool, baseUrl, binName) {
|
|
|
114
115
|
const declaredLength = Number(res.headers.get("content-length"));
|
|
115
116
|
if (Number.isFinite(declaredLength) && declaredLength > MAX_TOOL_ARCHIVE_BYTES) {
|
|
116
117
|
void res.body.cancel("tool archive exceeded limit").catch(() => { });
|
|
117
|
-
throw new Error(`下载文件超过 ${MAX_TOOL_ARCHIVE_BYTES} bytes`);
|
|
118
|
+
throw new Error(tr(`下载文件超过 ${MAX_TOOL_ARCHIVE_BYTES} bytes`, `Download exceeds ${MAX_TOOL_ARCHIVE_BYTES} bytes`));
|
|
118
119
|
}
|
|
119
120
|
const archive = join(binDir, asset);
|
|
120
121
|
// fd/rg 并行安装,解压目录按工具名+pid 隔离,避免互踩
|
|
@@ -130,10 +131,10 @@ async function install(tool, baseUrl, binName) {
|
|
|
130
131
|
await pipeline(Readable.fromWeb(res.body), limiter, createWriteStream(archive));
|
|
131
132
|
mkdirSync(extractDir, { recursive: true });
|
|
132
133
|
if (!extract(archive, extractDir))
|
|
133
|
-
throw new Error(`解压失败 ${asset}`);
|
|
134
|
+
throw new Error(tr(`解压失败 ${asset}`, `Failed to extract ${asset}`));
|
|
134
135
|
const found = findBinary(extractDir, binName);
|
|
135
136
|
if (!found)
|
|
136
|
-
throw new Error(`包内没有 ${binName}`);
|
|
137
|
+
throw new Error(tr(`包内没有 ${binName}`, `${binName} not found in the archive`));
|
|
137
138
|
renameSync(found, join(binDir, binName));
|
|
138
139
|
if (platform() !== "win32")
|
|
139
140
|
chmodSync(join(binDir, binName), 0o755);
|
|
@@ -151,10 +152,10 @@ export async function ensureSearchTools(cfg) {
|
|
|
151
152
|
if (missing.length === 0)
|
|
152
153
|
return;
|
|
153
154
|
// 这是新手首启最早看到的输出之一:说清是首次准备、不用管;fd/rg 等术语不外露
|
|
154
|
-
console.log(" 首次运行,正在准备文件搜索组件(只需一次)…");
|
|
155
|
+
console.log(tr(" 首次运行,正在准备文件搜索组件(只需一次)…", " First run: setting up file search components (one-time)…"));
|
|
155
156
|
const results = await Promise.allSettled(missing.map((t) => install(t, cfg.baseUrl, t.bin + ext)));
|
|
156
157
|
if (results.some((r) => r.status === "rejected")) {
|
|
157
|
-
console.log(" 部分搜索组件暂时没装上,不影响使用,之后会自动重试。");
|
|
158
|
+
console.log(tr(" 部分搜索组件暂时没装上,不影响使用,之后会自动重试。", " Some search components couldn't be installed yet. You can keep working; we'll retry automatically later."));
|
|
158
159
|
}
|
|
159
160
|
}
|
|
160
161
|
catch {
|
package/dist/shell-doctor.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join, win32 } from "node:path";
|
|
4
4
|
import { agentDir, agentSettingsFile } from "./config.js";
|
|
5
|
+
import { tr } from "./i18n.js";
|
|
5
6
|
/** System32/Sysnative 的 bash.exe 是 WSL 转发器,不是真 bash(与 pi 同款判断)。 */
|
|
6
7
|
function isWslRelayBash(p) {
|
|
7
8
|
const normalized = p.replace(/\//g, "\\").toLowerCase();
|
|
@@ -139,8 +140,8 @@ function persistShellPath(p) {
|
|
|
139
140
|
return true;
|
|
140
141
|
}
|
|
141
142
|
function adviseInstallGitBash() {
|
|
142
|
-
console.error(" ⚠ 未找到 Git Bash,将使用 WSL 的 bash:命令会在 WSL 的 Linux 里执行,不是 Windows 本机。");
|
|
143
|
-
console.error(" 建议安装 Git for Windows(PowerShell 运行: winget install --id Git.Git)后重启 u1s1。");
|
|
143
|
+
console.error(tr(" ⚠ 未找到 Git Bash,将使用 WSL 的 bash:命令会在 WSL 的 Linux 里执行,不是 Windows 本机。", " ⚠ Git Bash not found; using WSL's bash. Commands will run inside WSL's Linux, not natively on Windows."));
|
|
144
|
+
console.error(tr(" 建议安装 Git for Windows(PowerShell 运行: winget install --id Git.Git)后重启 u1s1。", " We recommend installing Git for Windows (in PowerShell: winget install --id Git.Git), then restarting u1s1."));
|
|
144
145
|
}
|
|
145
146
|
export async function ensureUsableShell() {
|
|
146
147
|
if (process.platform !== "win32")
|
|
@@ -164,7 +165,7 @@ export async function ensureUsableShell() {
|
|
|
164
165
|
const candidate = await findUsableGitBash();
|
|
165
166
|
if (candidate) {
|
|
166
167
|
persistShellPath(candidate);
|
|
167
|
-
console.error(` 检测到 Git Bash,命令改回 Windows 本机执行: ${candidate}`);
|
|
168
|
+
console.error(tr(` 检测到 Git Bash,命令改回 Windows 本机执行: ${candidate}`, ` Found Git Bash; commands will run natively on Windows again: ${candidate}`));
|
|
168
169
|
return { status: "bash", shellPath: candidate };
|
|
169
170
|
}
|
|
170
171
|
adviseInstallGitBash();
|
|
@@ -175,7 +176,7 @@ export async function ensureUsableShell() {
|
|
|
175
176
|
const adopt = (candidate) => {
|
|
176
177
|
persistShellPath(candidate);
|
|
177
178
|
if (typeof configured === "string") {
|
|
178
|
-
console.error(` 原 shellPath 已失效,自动改用: ${candidate}`);
|
|
179
|
+
console.error(tr(` 原 shellPath 已失效,自动改用: ${candidate}`, ` The configured shellPath no longer works; switched to: ${candidate}`));
|
|
179
180
|
}
|
|
180
181
|
return { status: "bash", shellPath: candidate };
|
|
181
182
|
};
|
|
@@ -195,14 +196,14 @@ export async function ensureUsableShell() {
|
|
|
195
196
|
return { status: "bash", shellPath: fallback };
|
|
196
197
|
}
|
|
197
198
|
if (fallback) {
|
|
198
|
-
console.error(` ⚠ 检测到的 bash(${fallback})无法执行命令,u1s1
|
|
199
|
+
console.error(tr(` ⚠ 检测到的 bash(${fallback})无法执行命令,u1s1 的命令工具会全部失败。`, ` ⚠ The bash found (${fallback}) can't run commands, so all of u1s1's command tools will fail.`));
|
|
199
200
|
if (isWslRelayBash(fallback)) {
|
|
200
|
-
console.error(" 它是 WSL 转发器,当前默认发行版里没有 /bin/bash(docker-desktop 被设为默认时就会这样)。");
|
|
201
|
+
console.error(tr(" 它是 WSL 转发器,当前默认发行版里没有 /bin/bash(docker-desktop 被设为默认时就会这样)。", " It's the WSL relay, and the default distro has no /bin/bash (this happens when docker-desktop is the default)."));
|
|
201
202
|
}
|
|
202
203
|
}
|
|
203
204
|
else {
|
|
204
|
-
console.error(" ⚠ 没找到 bash,u1s1 无法执行命令。");
|
|
205
|
+
console.error(tr(" ⚠ 没找到 bash,u1s1 无法执行命令。", " ⚠ No bash found; u1s1 can't run commands."));
|
|
205
206
|
}
|
|
206
|
-
console.error(" 修复:PowerShell 里运行 winget install --id Git.Git 安装 Git for Windows,装完重启 u1s1。");
|
|
207
|
+
console.error(tr(" 修复:PowerShell 里运行 winget install --id Git.Git 安装 Git for Windows,装完重启 u1s1。", " Fix: in PowerShell, run winget install --id Git.Git to install Git for Windows, then restart u1s1."));
|
|
207
208
|
return { status: "none" };
|
|
208
209
|
}
|
package/dist/subagent.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createAgentSession, DefaultResourceLoader, getAgentDir, hasTrustRequiringProjectResources, ModelRuntime, ProjectTrustStore, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { tr } from "./i18n.js";
|
|
2
3
|
// ---- 子 agent spawn 基础设施:spawn_subagent 工具与 workflow runner 共用 ----
|
|
3
4
|
/** 同时在跑的子 agent 上限;超额排队,防止打爆网关。 */
|
|
4
5
|
export const SUBAGENT_CONCURRENCY = 4;
|
|
@@ -20,7 +21,7 @@ export function resolveStoredProjectTrust(cwd, agentDir = getAgentDir()) {
|
|
|
20
21
|
return new ProjectTrustStore(agentDir).get(cwd) === true;
|
|
21
22
|
}
|
|
22
23
|
function cancelledError(signal) {
|
|
23
|
-
return new Error("已取消", { cause: signal.reason });
|
|
24
|
+
return new Error(tr("已取消", "Cancelled"), { cause: signal.reason });
|
|
24
25
|
}
|
|
25
26
|
/**
|
|
26
27
|
* Run one prompt while preserving the reason that stopped it. AgentSession.abort()
|
|
@@ -54,7 +55,7 @@ export async function promptSubagentSession(session, task, options) {
|
|
|
54
55
|
if (options.signal?.aborted)
|
|
55
56
|
throw cancelledError(options.signal);
|
|
56
57
|
if (timedOut)
|
|
57
|
-
throw new Error(`子任务超时(${options.timeoutMs}ms)`, { cause: promptError });
|
|
58
|
+
throw new Error(tr(`子任务超时(${options.timeoutMs}ms)`, `Subtask timed out (${options.timeoutMs}ms)`), { cause: promptError });
|
|
58
59
|
if (promptError)
|
|
59
60
|
throw promptError;
|
|
60
61
|
}
|
|
@@ -72,7 +73,7 @@ function extractFinalText(messages) {
|
|
|
72
73
|
if (text)
|
|
73
74
|
return text;
|
|
74
75
|
}
|
|
75
|
-
return "(子 agent 结束但没有文本输出)";
|
|
76
|
+
return tr("(子 agent 结束但没有文本输出)", "(sub-agent finished without any text output)");
|
|
76
77
|
}
|
|
77
78
|
/**
|
|
78
79
|
* 汇总会话内全部 assistant 消息的 token 与费用。
|
|
@@ -123,7 +124,7 @@ export async function runSubagent(opts) {
|
|
|
123
124
|
sharedModelRuntime.getModel(providerId, modelId) ??
|
|
124
125
|
(slash === -1 ? sharedModelRuntime.getModel("u1s1", opts.model) : undefined);
|
|
125
126
|
if (!model)
|
|
126
|
-
throw new Error(`找不到模型 "${opts.model}"(可用 /model 查看已配置的模型)`);
|
|
127
|
+
throw new Error(tr(`找不到模型 "${opts.model}"(可用 /model 查看已配置的模型)`, `Model "${opts.model}" not found (use /model to see configured models)`));
|
|
127
128
|
}
|
|
128
129
|
else if (opts.parentModel) {
|
|
129
130
|
// 继承主会话模型;解析不到(端点被删等)就落回默认,不硬失败
|
package/dist/tools.js
CHANGED
|
@@ -5,6 +5,7 @@ import { contextProjectTrust, runPool, runSubagent, SUBAGENT_CONCURRENCY, SUBAGE
|
|
|
5
5
|
import { Text } from "@earendil-works/pi-tui";
|
|
6
6
|
import { Type } from "typebox";
|
|
7
7
|
import { generateImage, renderPage, searchWeb } from "./api.js";
|
|
8
|
+
import { tr } from "./i18n.js";
|
|
8
9
|
const FETCH_TIMEOUT_MS = 20_000;
|
|
9
10
|
const MAX_FETCH_BYTES = 2_000_000;
|
|
10
11
|
const MAX_TEXT_CHARS = 30_000;
|
|
@@ -38,7 +39,7 @@ function htmlToText(html) {
|
|
|
38
39
|
export function truncate(text) {
|
|
39
40
|
if (text.length <= MAX_TEXT_CHARS)
|
|
40
41
|
return text;
|
|
41
|
-
return `${text.slice(0, MAX_TEXT_CHARS)}\n\n
|
|
42
|
+
return `${text.slice(0, MAX_TEXT_CHARS)}\n\n${tr(`…(内容过长已截断,共 ${text.length} 字符)`, `… (truncated; ${text.length} characters total)`)}`;
|
|
42
43
|
}
|
|
43
44
|
/**
|
|
44
45
|
* 长网页按 30k 字符开窗返回:光截断而不给「怎么读下一段」,模型会拿前 30k 硬答
|
|
@@ -50,10 +51,10 @@ export function windowText(text, offset) {
|
|
|
50
51
|
const body = text.slice(start, end);
|
|
51
52
|
if (start === 0 && end === text.length)
|
|
52
53
|
return { text: body, truncated: false, nextOffset: null };
|
|
53
|
-
const head = start > 0 ? `(从第 ${start} 字符起继续)\n\n` : "";
|
|
54
|
+
const head = start > 0 ? tr(`(从第 ${start} 字符起继续)\n\n`, `(continuing from character ${start})\n\n`) : "";
|
|
54
55
|
const tail = end < text.length
|
|
55
|
-
? `\n\n…(内容过长已截断:本次为第 ${start}–${end} 字符,共 ${text.length} 字符;要继续读取,用同一 url 再调一次 web_fetch 并传 offset=${end}
|
|
56
|
-
: `\n\n(已到末尾:第 ${start}–${end} 字符,共 ${text.length}
|
|
56
|
+
? tr(`\n\n…(内容过长已截断:本次为第 ${start}–${end} 字符,共 ${text.length} 字符;要继续读取,用同一 url 再调一次 web_fetch 并传 offset=${end})`, `\n\n… (truncated: this is characters ${start}–${end} of ${text.length}; to keep reading, call web_fetch again with the same url and offset=${end})`)
|
|
57
|
+
: tr(`\n\n(已到末尾:第 ${start}–${end} 字符,共 ${text.length} 字符)`, `\n\n(end of page: characters ${start}–${end} of ${text.length})`);
|
|
57
58
|
return { text: `${head}${body}${tail}`, truncated: end < text.length, nextOffset: end < text.length ? end : null };
|
|
58
59
|
}
|
|
59
60
|
/** 同一页面带 offset 续读时直接命中,不再重新抓取(云端渲染是按次计费的)。 */
|
|
@@ -159,7 +160,7 @@ const SEARCH_QUERY_MAX_CHARS = 100;
|
|
|
159
160
|
export function createSearchTool(cfg) {
|
|
160
161
|
return defineTool({
|
|
161
162
|
name: "web_search",
|
|
162
|
-
label: "联网搜索",
|
|
163
|
+
label: tr("联网搜索", "Web search"),
|
|
163
164
|
description: "Search the web for current information. Use it for anything time-sensitive or outside the codebase: library docs, error messages, latest versions, recent events. Returns a short answer plus ranked results with URLs and snippets. Follow up with web_fetch to read a promising result in full.",
|
|
164
165
|
promptSnippet: "Search the internet for up-to-date information (docs, errors, versions, current events)",
|
|
165
166
|
promptGuidelines: [
|
|
@@ -181,8 +182,8 @@ export function createSearchTool(cfg) {
|
|
|
181
182
|
const [result, options, theme, context] = args;
|
|
182
183
|
const d = result.details;
|
|
183
184
|
const q = d?.query ? `"${previewLine(d.query, 80)}"` : "…";
|
|
184
|
-
const n = typeof d?.count === "number" ? ` · ${d.count}
|
|
185
|
-
return compactResultRender({ result, options, theme, context, summaryLine: `🔍 搜索 ${q}${n}` });
|
|
185
|
+
const n = typeof d?.count === "number" ? tr(` · ${d.count} 条结果`, ` · ${d.count} result${d.count === 1 ? "" : "s"}`) : "";
|
|
186
|
+
return compactResultRender({ result, options, theme, context, summaryLine: tr(`🔍 搜索 ${q}${n}`, `🔍 Search ${q}${n}`) });
|
|
186
187
|
},
|
|
187
188
|
async execute(_toolCallId, params, signal) {
|
|
188
189
|
const data = await searchWeb(cfg, { query: params.query, maxResults: params.maxResults, signal });
|
|
@@ -215,7 +216,7 @@ const SUBAGENT_MAX_TASKS = 10;
|
|
|
215
216
|
export function createSubagentTool(getParentModel) {
|
|
216
217
|
return defineTool({
|
|
217
218
|
name: "spawn_subagent",
|
|
218
|
-
label: "子任务",
|
|
219
|
+
label: tr("子任务", "Subtask"),
|
|
219
220
|
description: "Spawn an independent sub-agent with its own context window to complete a self-contained task, then return its final answer. " +
|
|
220
221
|
"Pass `tasks` (array) to run N sub-agents concurrently — ideal for embarrassingly parallel work like translating many locale files, " +
|
|
221
222
|
"reviewing several files, or researching multiple directions at once. Each sub-agent can read/write files and run commands. " +
|
|
@@ -250,10 +251,10 @@ export function createSubagentTool(getParentModel) {
|
|
|
250
251
|
const [result, options, theme, context] = args;
|
|
251
252
|
const d = result.details;
|
|
252
253
|
const summary = d && typeof d.count === "number"
|
|
253
|
-
? `🤖 子任务 ×${d.count}` +
|
|
254
|
+
? tr(`🤖 子任务 ×${d.count}`, `🤖 Subtasks ×${d.count}`) +
|
|
254
255
|
(typeof d.ok === "number" && d.failed ? ` · ✓${d.ok} ✗${d.failed}` : "") +
|
|
255
256
|
(typeof d.seconds === "number" ? ` · ${d.seconds}s` : "")
|
|
256
|
-
: "🤖 子任务";
|
|
257
|
+
: tr("🤖 子任务", "🤖 Subtask");
|
|
257
258
|
return compactResultRender({ result, options, theme, context, summaryLine: summary });
|
|
258
259
|
},
|
|
259
260
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
@@ -264,7 +265,7 @@ export function createSubagentTool(getParentModel) {
|
|
|
264
265
|
.filter(Boolean)
|
|
265
266
|
.slice(0, SUBAGENT_MAX_TASKS);
|
|
266
267
|
if (list.length === 0) {
|
|
267
|
-
throw new Error("task 和 tasks 至少填一个");
|
|
268
|
+
throw new Error(tr("task 和 tasks 至少填一个", "Provide at least one of task or tasks"));
|
|
268
269
|
}
|
|
269
270
|
const parentModel = getParentModel();
|
|
270
271
|
const timeoutMs = Math.min(60, params.timeout_minutes ?? SUBAGENT_TIMEOUT_MS / 60_000) * 60_000;
|
|
@@ -299,10 +300,13 @@ export function createSubagentTool(getParentModel) {
|
|
|
299
300
|
details: { ...details, seconds: undefined },
|
|
300
301
|
};
|
|
301
302
|
}
|
|
302
|
-
const lines = [
|
|
303
|
+
const lines = [
|
|
304
|
+
tr(`共 ${list.length} 个子任务,✓${details.ok} ✗${details.failed},用时 ${seconds}s`, `${list.length} subtasks, ✓${details.ok} ✗${details.failed}, took ${seconds}s`),
|
|
305
|
+
"",
|
|
306
|
+
];
|
|
303
307
|
outcomes.forEach((o, i) => {
|
|
304
|
-
lines.push(`## 任务 ${i + 1}/${list.length} ${o?.ok ? "✅" : "❌"}`, "");
|
|
305
|
-
lines.push(o?.ok ? o.text : `失败:${o?.text ?? "未知错误"}`, "", "---", "");
|
|
308
|
+
lines.push(tr(`## 任务 ${i + 1}/${list.length} ${o?.ok ? "✅" : "❌"}`, `## Task ${i + 1}/${list.length} ${o?.ok ? "✅" : "❌"}`), "");
|
|
309
|
+
lines.push(o?.ok ? o.text : tr(`失败:${o?.text ?? "未知错误"}`, `Failed: ${o?.text ?? "unknown error"}`), "", "---", "");
|
|
306
310
|
});
|
|
307
311
|
return {
|
|
308
312
|
content: [{ type: "text", text: truncate(lines.join("\n").trim()) }],
|
|
@@ -333,12 +337,13 @@ function refImageToPayload(ref) {
|
|
|
333
337
|
return s;
|
|
334
338
|
const path = resolve(s);
|
|
335
339
|
if (!existsSync(path))
|
|
336
|
-
throw new Error(`参考图不存在: ${s}`);
|
|
340
|
+
throw new Error(tr(`参考图不存在: ${s}`, `Reference image not found: ${s}`));
|
|
337
341
|
const mime = REF_IMAGE_MIME[extname(path).toLowerCase()];
|
|
338
|
-
if (!mime)
|
|
339
|
-
throw new Error(`参考图格式不支持: ${s}(支持 png/jpg/jpeg/webp/bmp/gif)`);
|
|
342
|
+
if (!mime) {
|
|
343
|
+
throw new Error(tr(`参考图格式不支持: ${s}(支持 png/jpg/jpeg/webp/bmp/gif)`, `Unsupported reference image format: ${s} (supported: png/jpg/jpeg/webp/bmp/gif)`));
|
|
344
|
+
}
|
|
340
345
|
if (statSync(path).size > MAX_REF_IMAGE_BYTES)
|
|
341
|
-
throw new Error(`参考图超过 10MB: ${s}`);
|
|
346
|
+
throw new Error(tr(`参考图超过 10MB: ${s}`, `Reference image exceeds 10MB: ${s}`));
|
|
342
347
|
return `data:${mime};base64,${readFileSync(path).toString("base64")}`;
|
|
343
348
|
}
|
|
344
349
|
/** 结果落盘路径:模型没指定时按时间戳起名,已存在就加序号,不覆盖旧图。 */
|
|
@@ -380,12 +385,16 @@ async function abortableDelay(ms, signal) {
|
|
|
380
385
|
}
|
|
381
386
|
class GeneratedImageTooLargeError extends Error {
|
|
382
387
|
}
|
|
388
|
+
function imageTooLargeMessage(maxBytes) {
|
|
389
|
+
const mib = Math.floor(maxBytes / 1024 / 1024);
|
|
390
|
+
return tr(`图片文件超过下载上限 ${mib} MiB`, `the image file exceeds the ${mib} MiB download limit`);
|
|
391
|
+
}
|
|
383
392
|
async function readGeneratedImageBody(resp, maxBytes) {
|
|
384
393
|
const declaredLength = Number(resp.headers.get("content-length"));
|
|
385
394
|
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
|
|
386
395
|
if (resp.body)
|
|
387
396
|
void resp.body.cancel("generated image body limit reached").catch(() => { });
|
|
388
|
-
throw new GeneratedImageTooLargeError(
|
|
397
|
+
throw new GeneratedImageTooLargeError(imageTooLargeMessage(maxBytes));
|
|
389
398
|
}
|
|
390
399
|
if (!resp.body)
|
|
391
400
|
return new Uint8Array();
|
|
@@ -399,7 +408,7 @@ async function readGeneratedImageBody(resp, maxBytes) {
|
|
|
399
408
|
break;
|
|
400
409
|
if (total + value.byteLength > maxBytes) {
|
|
401
410
|
void reader.cancel("generated image body limit reached").catch(() => { });
|
|
402
|
-
throw new GeneratedImageTooLargeError(
|
|
411
|
+
throw new GeneratedImageTooLargeError(imageTooLargeMessage(maxBytes));
|
|
403
412
|
}
|
|
404
413
|
chunks.push(value);
|
|
405
414
|
total += value.byteLength;
|
|
@@ -444,21 +453,22 @@ export async function downloadGeneratedImage(url, signal, options = {}) {
|
|
|
444
453
|
if (signal.aborted)
|
|
445
454
|
throw error;
|
|
446
455
|
if (error instanceof GeneratedImageTooLargeError) {
|
|
447
|
-
throw new Error(`图片已经生成,但${error.message}。不要重新调用 generate_image,可在 24 小时内手动下载: ${url}`, { cause: error });
|
|
456
|
+
throw new Error(tr(`图片已经生成,但${error.message}。不要重新调用 generate_image,可在 24 小时内手动下载: ${url}`, `The image was generated, but ${error.message}. Do not call generate_image again; it can be downloaded manually within 24 hours: ${url}`), { cause: error });
|
|
448
457
|
}
|
|
449
458
|
lastError = error;
|
|
450
459
|
if (attempt < attempts)
|
|
451
460
|
await abortableDelay(retryDelayMs * attempt, signal);
|
|
452
461
|
}
|
|
453
462
|
}
|
|
454
|
-
throw new Error(`图片已经生成,但下载失败(同一地址已重试 ${attempts} 次): ${errorDetail(lastError)}。` +
|
|
455
|
-
`不要重新调用 generate_image,可在 24 小时内手动下载: ${url}`, {
|
|
463
|
+
throw new Error(tr(`图片已经生成,但下载失败(同一地址已重试 ${attempts} 次): ${errorDetail(lastError)}。` +
|
|
464
|
+
`不要重新调用 generate_image,可在 24 小时内手动下载: ${url}`, `The image was generated, but downloading it failed (retried the same URL ${attempts} times): ${errorDetail(lastError)}. ` +
|
|
465
|
+
`Do not call generate_image again; it can be downloaded manually within 24 hours: ${url}`), { cause: lastError });
|
|
456
466
|
}
|
|
457
467
|
/** 生图工具:走 u1s1 网关代理火山方舟 Seedream,上游 key 不落到用户机器上。 */
|
|
458
468
|
export function createImageTool(cfg) {
|
|
459
469
|
return defineTool({
|
|
460
470
|
name: "generate_image",
|
|
461
|
-
label: "生成图片",
|
|
471
|
+
label: tr("生成图片", "Generate image"),
|
|
462
472
|
description: "Generate an image from a text prompt, or edit/compose existing images, using the Seedream image model. " +
|
|
463
473
|
"Saves the result as a local image file and displays it directly in supported chat UIs. " +
|
|
464
474
|
"Pass local file paths or http(s) URLs in `images` to edit an image or use references (style transfer, adding elements, combining up to 10 images). " +
|
|
@@ -499,7 +509,9 @@ export function createImageTool(cfg) {
|
|
|
499
509
|
const sizeNote = result.size ? ` (${result.size})` : "";
|
|
500
510
|
const mimeType = REF_IMAGE_MIME[urlExt] ?? "image/jpeg";
|
|
501
511
|
return {
|
|
502
|
-
content: [
|
|
512
|
+
content: [
|
|
513
|
+
{ type: "text", text: tr(`图片已保存并在对话中展示: ${path}${sizeNote}`, `Image saved and shown in the chat: ${path}${sizeNote}`) },
|
|
514
|
+
],
|
|
503
515
|
// displayImage 只给 UI 预览,不放进 content:避免每轮都把整张 2K/4K
|
|
504
516
|
// 图片送回模型占用 context。pi-web-ui 会从 tool details 序列化它。
|
|
505
517
|
details: {
|
|
@@ -541,16 +553,16 @@ async function fetchDirect(url, signal) {
|
|
|
541
553
|
// 用户主动打断不算失败,直接往外抛,不去碰云端渲染
|
|
542
554
|
if (signal?.aborted)
|
|
543
555
|
throw e;
|
|
544
|
-
throw new DirectFetchError(`打不开 ${url.href}: ${e.message}`, true);
|
|
556
|
+
throw new DirectFetchError(tr(`打不开 ${url.href}: ${e.message}`, `Couldn't open ${url.href}: ${e.message}`), true);
|
|
545
557
|
}
|
|
546
558
|
if (!resp.ok) {
|
|
547
559
|
void resp.body?.cancel("web fetch rejected HTTP response").catch(() => undefined);
|
|
548
|
-
throw new DirectFetchError(`${url.href} 返回 ${resp.status} ${resp.statusText}`, RENDER_WORTHY_STATUS.has(resp.status));
|
|
560
|
+
throw new DirectFetchError(tr(`${url.href} 返回 ${resp.status} ${resp.statusText}`, `${url.href} returned ${resp.status} ${resp.statusText}`), RENDER_WORTHY_STATUS.has(resp.status));
|
|
549
561
|
}
|
|
550
562
|
const type = resp.headers.get("content-type") ?? "";
|
|
551
563
|
if (!/text\/|json|xml|javascript/i.test(type)) {
|
|
552
564
|
void resp.body?.cancel("web fetch rejected non-text response").catch(() => undefined);
|
|
553
|
-
throw new DirectFetchError(`${url.href} 不是文本内容 (${type || "unknown"}),读不了`, false);
|
|
565
|
+
throw new DirectFetchError(tr(`${url.href} 不是文本内容 (${type || "unknown"}),读不了`, `${url.href} is not text content (${type || "unknown"}); can't read it`), false);
|
|
554
566
|
}
|
|
555
567
|
let bytes;
|
|
556
568
|
try {
|
|
@@ -560,7 +572,7 @@ async function fetchDirect(url, signal) {
|
|
|
560
572
|
if (signal?.aborted)
|
|
561
573
|
throw error;
|
|
562
574
|
const detail = error instanceof Error ? error.message : String(error);
|
|
563
|
-
throw new DirectFetchError(`${url.href} 正文读取失败: ${detail}`, true);
|
|
575
|
+
throw new DirectFetchError(tr(`${url.href} 正文读取失败: ${detail}`, `Failed to read the body of ${url.href}: ${detail}`), true);
|
|
564
576
|
}
|
|
565
577
|
const raw = decodeBody(bytes, type);
|
|
566
578
|
const isHtml = /html|xml/i.test(type);
|
|
@@ -580,7 +592,7 @@ export function createFetchTool(cfg) {
|
|
|
580
592
|
}
|
|
581
593
|
return defineTool({
|
|
582
594
|
name: "web_fetch",
|
|
583
|
-
label: "读取网页",
|
|
595
|
+
label: tr("读取网页", "Fetch page"),
|
|
584
596
|
description: "Fetch a URL and return its readable text content (HTML is stripped to text; JSON and plain text are returned as-is). Use it to read a page found via web_search, or any URL the user pasted. If the direct fetch fails or the page needs JavaScript, it automatically retries through a cloud browser that can also clear bot-check interstitials. Long pages are returned in 30,000-character windows: when the result says it was truncated, call again with the same url and the offset it gives to read the next part.",
|
|
585
597
|
promptSnippet: "Fetch a URL and convert it to readable text",
|
|
586
598
|
promptGuidelines: [
|
|
@@ -602,9 +614,13 @@ export function createFetchTool(cfg) {
|
|
|
602
614
|
const [result, options, theme, context] = args;
|
|
603
615
|
const d = result.details;
|
|
604
616
|
const u = previewLine(d?.url ?? "…", 90);
|
|
605
|
-
const n = typeof d?.chars === "number" ? ` · ${d.chars}
|
|
606
|
-
const win = d?.offset
|
|
607
|
-
|
|
617
|
+
const n = typeof d?.chars === "number" ? tr(` · ${d.chars} 字符`, ` · ${d.chars} chars`) : "";
|
|
618
|
+
const win = d?.offset
|
|
619
|
+
? tr(` · 从第 ${d.offset} 字符续读`, ` · continued from char ${d.offset}`)
|
|
620
|
+
: d?.truncated
|
|
621
|
+
? tr(" · 只读了开头", " · first part only")
|
|
622
|
+
: "";
|
|
623
|
+
const cloud = d?.via === "render" ? tr(" · ☁ 浏览器渲染", " · ☁ browser render") : "";
|
|
608
624
|
return compactResultRender({ result, options, theme, context, summaryLine: `🌐 ${u}${n}${win}${cloud}` });
|
|
609
625
|
},
|
|
610
626
|
async execute(_toolCallId, params, signal) {
|
|
@@ -614,15 +630,15 @@ export function createFetchTool(cfg) {
|
|
|
614
630
|
url = new URL(params.url.trim().replace(/^@/, ""));
|
|
615
631
|
}
|
|
616
632
|
catch {
|
|
617
|
-
throw new Error(`不是合法的网址: ${params.url}`);
|
|
633
|
+
throw new Error(tr(`不是合法的网址: ${params.url}`, `Not a valid URL: ${params.url}`));
|
|
618
634
|
}
|
|
619
635
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
620
|
-
throw new Error(`只支持 http/https,收到 ${url.protocol}`);
|
|
636
|
+
throw new Error(tr(`只支持 http/https,收到 ${url.protocol}`, `Only http/https is supported; got ${url.protocol}`));
|
|
621
637
|
}
|
|
622
638
|
const offset = Math.max(0, Math.floor(params.offset ?? 0));
|
|
623
639
|
const ok = (text, via, contentType) => {
|
|
624
640
|
rememberPage(url.href, { text, via, contentType });
|
|
625
|
-
const win = windowText(text || "(空白页面)", offset);
|
|
641
|
+
const win = windowText(text || tr("(空白页面)", "(empty page)"), offset);
|
|
626
642
|
return {
|
|
627
643
|
content: [{ type: "text", text: `# ${url.href}\n\n${win.text}` }],
|
|
628
644
|
details: { url: url.href, contentType, via, chars: text.length, offset, truncated: win.truncated, nextOffset: win.nextOffset },
|
|
@@ -643,7 +659,7 @@ export function createFetchTool(cfg) {
|
|
|
643
659
|
return ok(await renderViaGateway(url, signal), "render", "text/markdown");
|
|
644
660
|
}
|
|
645
661
|
catch (re) {
|
|
646
|
-
throw new Error(`${e.message};云端浏览器渲染也失败: ${re.message}`, { cause: re });
|
|
662
|
+
throw new Error(tr(`${e.message};云端浏览器渲染也失败: ${re.message}`, `${e.message}; cloud browser rendering also failed: ${re.message}`), { cause: re });
|
|
647
663
|
}
|
|
648
664
|
}
|
|
649
665
|
// 直连 200 但正文近乎空:大概率是 JS 渲染的 SPA,换浏览器再试;渲染失败就退回空壳
|