u1s1-cli 1.11.7 → 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/README.md +5 -5
- package/dist/agent-setup.d.ts +7 -1
- package/dist/agent-setup.js +34 -10
- package/dist/api.js +42 -34
- package/dist/bench-report.js +12 -9
- package/dist/bench.js +34 -31
- package/dist/brand.d.ts +7 -2
- package/dist/brand.js +44 -13
- package/dist/config.d.ts +28 -2
- package/dist/config.js +52 -14
- package/dist/context-usage.js +12 -9
- package/dist/deploy.js +53 -47
- package/dist/device-auth.js +7 -5
- package/dist/embed.d.ts +2 -1
- package/dist/embed.js +2 -1
- package/dist/error-humanize.d.ts +3 -1
- package/dist/error-humanize.js +24 -9
- package/dist/feedback.d.ts +4 -2
- package/dist/feedback.js +60 -39
- 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 +153 -54
- package/dist/login.d.ts +13 -2
- package/dist/login.js +84 -27
- 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 +12 -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/site.d.ts +71 -0
- package/dist/site.js +123 -0
- package/dist/subagent.js +5 -4
- package/dist/tools.js +53 -37
- package/dist/update.js +47 -39
- package/dist/usage.d.ts +6 -4
- package/dist/usage.js +111 -53
- 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 +3 -3
- package/scripts/patch-pi.js +243 -278
- package/scripts/render-regression.mjs +55 -4
package/dist/site.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 双站点表:中国站(cn)与国际站(intl)。见 docs/region-split-architecture-2026-09-26.md 第 3 节。
|
|
3
|
+
*
|
|
4
|
+
* 站点在分发时就定好(从哪个站下载就连哪个站),界面里不放选站入口;运行时一律按
|
|
5
|
+
* 当前 baseUrl 所在站点取域名/链接,CLI 里不再散落 u1s1.io 字面量。
|
|
6
|
+
*
|
|
7
|
+
* 本模块不 import 任何本地模块:config.ts 依赖它(DEFAULT_BASE_URL、老配置迁移),
|
|
8
|
+
* 反向依赖会成环。「当前生效站点」要读配置,放在 config.ts 的 activeSite()。
|
|
9
|
+
*/
|
|
10
|
+
export type SiteId = "cn" | "intl";
|
|
11
|
+
export interface Site {
|
|
12
|
+
id: SiteId;
|
|
13
|
+
/** 主站 origin,站内页面链接都挂在这里 */
|
|
14
|
+
origin: string;
|
|
15
|
+
/** API 根,形如 https://api.<host>/v1(由 origin 派生) */
|
|
16
|
+
apiBase: string;
|
|
17
|
+
/** 发布件(安装脚本/SHA256SUMS/便携包)所在 origin */
|
|
18
|
+
releaseOrigin: string;
|
|
19
|
+
/** 申诉/联系邮箱 */
|
|
20
|
+
contactEmail: string;
|
|
21
|
+
/** 认作本站的主机名(baseUrl 落在这些主机上即判定为本站) */
|
|
22
|
+
hosts: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 中国站当前主域(P1 起为备案域 u1s1app.com,原主域 u1s1.io 的页面 308 过来、API/下载原地服务)。
|
|
26
|
+
* apiBase / releaseOrigin / 安装命令 / 站内链接全部随之派生,老配置里的
|
|
27
|
+
* api.u1s1.io 由 migrateStoredBaseUrl 在下次保存时改写到新 apiBase。
|
|
28
|
+
*/
|
|
29
|
+
export declare const CN_ORIGIN = "https://u1s1app.com";
|
|
30
|
+
/** origin → https://api.<host>/v1 */
|
|
31
|
+
export declare function apiBaseForOrigin(origin: string): string;
|
|
32
|
+
export declare const SITES: Readonly<Record<SiteId, Site>>;
|
|
33
|
+
/**
|
|
34
|
+
* 历代中国站 API 根。配置文件里存的是其中之一、但不是当前中国站 apiBase 时,
|
|
35
|
+
* 读配置时换成当前值(见 migrateStoredBaseUrl),把流量逐步挪到新主域。
|
|
36
|
+
*/
|
|
37
|
+
export declare const LEGACY_CN_API_BASES: readonly string[];
|
|
38
|
+
/** baseUrl 属于哪个站;自定义网关/localhost/解析不了的一律按中国站处理(与现状一致)。 */
|
|
39
|
+
export declare function siteForBaseUrl(baseUrl: string | undefined): SiteId;
|
|
40
|
+
/** baseUrl 是否落在该站(含历史主域)的主机上;自定义网关/localhost 不算,不能假定账号相通。 */
|
|
41
|
+
export declare function isSiteUrl(baseUrl: string | undefined, site: SiteId): boolean;
|
|
42
|
+
/** 有 cfg 在手时直接用它的 baseUrl 定站(结构类型,免得 import config.ts)。 */
|
|
43
|
+
export declare function siteOf(cfg: {
|
|
44
|
+
baseUrl?: string;
|
|
45
|
+
}): Site;
|
|
46
|
+
/**
|
|
47
|
+
* 老配置迁移:存的是历代中国站 API 根、且与当前中国站 apiBase 不同 → 换成当前值;
|
|
48
|
+
* 其余(国际站、自定义网关、当前值)原样返回。currentCnApiBase 可注入,便于测试
|
|
49
|
+
* P1 之后的行为(P0 两者相等,这里是 no-op)。
|
|
50
|
+
*/
|
|
51
|
+
export declare function migrateStoredBaseUrl(stored: string | undefined, currentCnApiBase?: string): string | undefined;
|
|
52
|
+
/** `u1s1 login --site <值>`:cn/china → 中国站;global/intl → 国际站;其它返回 undefined。 */
|
|
53
|
+
export declare function parseSiteArg(value: string | undefined): SiteId | undefined;
|
|
54
|
+
export interface SiteLinks {
|
|
55
|
+
home: string;
|
|
56
|
+
dashboard: string;
|
|
57
|
+
/** 仪表盘充值卡片深链 */
|
|
58
|
+
topup: string;
|
|
59
|
+
/** 仪表盘工单/留言区 */
|
|
60
|
+
support: string;
|
|
61
|
+
/** 仪表盘「我的站点」 */
|
|
62
|
+
sites: string;
|
|
63
|
+
guides: string;
|
|
64
|
+
status: string;
|
|
65
|
+
community: string;
|
|
66
|
+
/** 官网下载区(Desktop App) */
|
|
67
|
+
download: string;
|
|
68
|
+
}
|
|
69
|
+
export declare function siteLinks(site: Site | SiteId): SiteLinks;
|
|
70
|
+
/** 本站官网安装命令:sh = curl … | bash(Linux/macOS),ps1 = irm … | iex(PowerShell)。 */
|
|
71
|
+
export declare function installCommand(site: Site | SiteId, shell: "sh" | "ps1"): string;
|
package/dist/site.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 双站点表:中国站(cn)与国际站(intl)。见 docs/region-split-architecture-2026-09-26.md 第 3 节。
|
|
3
|
+
*
|
|
4
|
+
* 站点在分发时就定好(从哪个站下载就连哪个站),界面里不放选站入口;运行时一律按
|
|
5
|
+
* 当前 baseUrl 所在站点取域名/链接,CLI 里不再散落 u1s1.io 字面量。
|
|
6
|
+
*
|
|
7
|
+
* 本模块不 import 任何本地模块:config.ts 依赖它(DEFAULT_BASE_URL、老配置迁移),
|
|
8
|
+
* 反向依赖会成环。「当前生效站点」要读配置,放在 config.ts 的 activeSite()。
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* 中国站当前主域(P1 起为备案域 u1s1app.com,原主域 u1s1.io 的页面 308 过来、API/下载原地服务)。
|
|
12
|
+
* apiBase / releaseOrigin / 安装命令 / 站内链接全部随之派生,老配置里的
|
|
13
|
+
* api.u1s1.io 由 migrateStoredBaseUrl 在下次保存时改写到新 apiBase。
|
|
14
|
+
*/
|
|
15
|
+
export const CN_ORIGIN = "https://u1s1app.com";
|
|
16
|
+
const INTL_ORIGIN = "https://u1s1.dev";
|
|
17
|
+
/** origin → https://api.<host>/v1 */
|
|
18
|
+
export function apiBaseForOrigin(origin) {
|
|
19
|
+
const url = new URL(origin);
|
|
20
|
+
return `${url.protocol}//api.${url.host}/v1`;
|
|
21
|
+
}
|
|
22
|
+
export const SITES = {
|
|
23
|
+
cn: {
|
|
24
|
+
id: "cn",
|
|
25
|
+
origin: CN_ORIGIN,
|
|
26
|
+
apiBase: apiBaseForOrigin(CN_ORIGIN),
|
|
27
|
+
releaseOrigin: CN_ORIGIN,
|
|
28
|
+
// 邮件域已切到 u1s1app.com(2026-09-30);contact@u1s1.io 仍能收信,老版本照旧可用
|
|
29
|
+
contactEmail: "contact@u1s1app.com",
|
|
30
|
+
hosts: ["u1s1.io", "www.u1s1.io", "api.u1s1.io", "u1s1app.com", "www.u1s1app.com", "api.u1s1app.com"],
|
|
31
|
+
},
|
|
32
|
+
intl: {
|
|
33
|
+
id: "intl",
|
|
34
|
+
origin: INTL_ORIGIN,
|
|
35
|
+
apiBase: apiBaseForOrigin(INTL_ORIGIN),
|
|
36
|
+
releaseOrigin: INTL_ORIGIN,
|
|
37
|
+
contactEmail: "contact@u1s1.dev",
|
|
38
|
+
hosts: ["u1s1.dev", "www.u1s1.dev", "api.u1s1.dev"],
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* 历代中国站 API 根。配置文件里存的是其中之一、但不是当前中国站 apiBase 时,
|
|
43
|
+
* 读配置时换成当前值(见 migrateStoredBaseUrl),把流量逐步挪到新主域。
|
|
44
|
+
*/
|
|
45
|
+
export const LEGACY_CN_API_BASES = ["https://api.u1s1.io/v1", "https://api.u1s1app.com/v1"];
|
|
46
|
+
/** baseUrl 属于哪个站;自定义网关/localhost/解析不了的一律按中国站处理(与现状一致)。 */
|
|
47
|
+
export function siteForBaseUrl(baseUrl) {
|
|
48
|
+
if (!baseUrl)
|
|
49
|
+
return "cn";
|
|
50
|
+
let host;
|
|
51
|
+
try {
|
|
52
|
+
host = new URL(baseUrl).hostname.toLowerCase();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return "cn";
|
|
56
|
+
}
|
|
57
|
+
return SITES.intl.hosts.includes(host) ? "intl" : "cn";
|
|
58
|
+
}
|
|
59
|
+
/** baseUrl 是否落在该站(含历史主域)的主机上;自定义网关/localhost 不算,不能假定账号相通。 */
|
|
60
|
+
export function isSiteUrl(baseUrl, site) {
|
|
61
|
+
if (!baseUrl)
|
|
62
|
+
return false;
|
|
63
|
+
try {
|
|
64
|
+
return SITES[site].hosts.includes(new URL(baseUrl).hostname.toLowerCase());
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** 有 cfg 在手时直接用它的 baseUrl 定站(结构类型,免得 import config.ts)。 */
|
|
71
|
+
export function siteOf(cfg) {
|
|
72
|
+
return SITES[siteForBaseUrl(cfg.baseUrl)];
|
|
73
|
+
}
|
|
74
|
+
function resolveSite(site) {
|
|
75
|
+
return typeof site === "string" ? SITES[site] : site;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 老配置迁移:存的是历代中国站 API 根、且与当前中国站 apiBase 不同 → 换成当前值;
|
|
79
|
+
* 其余(国际站、自定义网关、当前值)原样返回。currentCnApiBase 可注入,便于测试
|
|
80
|
+
* P1 之后的行为(P0 两者相等,这里是 no-op)。
|
|
81
|
+
*/
|
|
82
|
+
export function migrateStoredBaseUrl(stored, currentCnApiBase = SITES.cn.apiBase) {
|
|
83
|
+
if (!stored)
|
|
84
|
+
return stored;
|
|
85
|
+
const normalized = stored.replace(/\/+$/, "");
|
|
86
|
+
if (LEGACY_CN_API_BASES.includes(normalized) && normalized !== currentCnApiBase)
|
|
87
|
+
return currentCnApiBase;
|
|
88
|
+
return stored;
|
|
89
|
+
}
|
|
90
|
+
/** `u1s1 login --site <值>`:cn/china → 中国站;global/intl → 国际站;其它返回 undefined。 */
|
|
91
|
+
export function parseSiteArg(value) {
|
|
92
|
+
switch (value?.trim().toLowerCase()) {
|
|
93
|
+
case "cn":
|
|
94
|
+
case "china":
|
|
95
|
+
return "cn";
|
|
96
|
+
case "global":
|
|
97
|
+
case "intl":
|
|
98
|
+
return "intl";
|
|
99
|
+
default:
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export function siteLinks(site) {
|
|
104
|
+
const { origin } = resolveSite(site);
|
|
105
|
+
return {
|
|
106
|
+
home: origin,
|
|
107
|
+
dashboard: `${origin}/dashboard`,
|
|
108
|
+
topup: `${origin}/dashboard#usage-topup-card`,
|
|
109
|
+
support: `${origin}/dashboard#sec-support`,
|
|
110
|
+
sites: `${origin}/dashboard#sec-sites`,
|
|
111
|
+
guides: `${origin}/guides`,
|
|
112
|
+
status: `${origin}/status`,
|
|
113
|
+
community: `${origin}/community`,
|
|
114
|
+
download: `${origin}/#download`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** 本站官网安装命令:sh = curl … | bash(Linux/macOS),ps1 = irm … | iex(PowerShell)。 */
|
|
118
|
+
export function installCommand(site, shell) {
|
|
119
|
+
const { releaseOrigin } = resolveSite(site);
|
|
120
|
+
return shell === "ps1"
|
|
121
|
+
? `irm ${releaseOrigin}/releases/install.ps1 | iex`
|
|
122
|
+
: `curl -fsSL ${releaseOrigin}/releases/install.sh | bash`;
|
|
123
|
+
}
|
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,换浏览器再试;渲染失败就退回空壳
|