u1s1-cli 1.11.8 → 1.13.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 +19 -3
- package/dist/agent-setup.js +85 -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 +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 +140 -49
- package/dist/login.js +39 -28
- package/dist/loop.js +18 -12
- package/dist/mcp/client.js +15 -14
- package/dist/mcp/command.d.ts +2 -2
- package/dist/mcp/command.js +109 -69
- package/dist/mcp/config.d.ts +22 -6
- package/dist/mcp/config.js +0 -0
- package/dist/mcp/extension.js +5 -3
- package/dist/mcp/tools.d.ts +2 -0
- package/dist/mcp/tools.js +10 -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 +4 -4
- package/scripts/patch-pi.js +347 -278
- package/scripts/render-regression.mjs +55 -4
package/dist/deploy.js
CHANGED
|
@@ -4,6 +4,7 @@ import { VERSION, u1s1Dir } from "./config.js";
|
|
|
4
4
|
import { authorizedFetch } from "./device-auth.js";
|
|
5
5
|
import { readJsonResponseCapped } from "./api.js";
|
|
6
6
|
import { markNudge } from "./nudges.js";
|
|
7
|
+
import { tr } from "./i18n.js";
|
|
7
8
|
import { askLine } from "./prompt.js";
|
|
8
9
|
import { sendTelemetryEvent } from "./telemetry.js";
|
|
9
10
|
import { siteLinks, siteOf } from "./site.js";
|
|
@@ -68,10 +69,10 @@ function resolveSiteDir(explicit) {
|
|
|
68
69
|
if (explicit) {
|
|
69
70
|
const dir = resolve(explicit);
|
|
70
71
|
if (!existsSync(dir) || !statSync(dir).isDirectory()) {
|
|
71
|
-
throw new Error(`目录不存在:${dir}`);
|
|
72
|
+
throw new Error(tr(`目录不存在:${dir}`, `Directory not found: ${dir}`));
|
|
72
73
|
}
|
|
73
74
|
if (!existsSync(join(dir, "index.html"))) {
|
|
74
|
-
throw new Error(`${dir} 里没有 index.html
|
|
75
|
+
throw new Error(tr(`${dir} 里没有 index.html,网站需要一个首页`, `${dir} has no index.html; a site needs a home page`));
|
|
75
76
|
}
|
|
76
77
|
return dir;
|
|
77
78
|
}
|
|
@@ -82,9 +83,11 @@ function resolveSiteDir(explicit) {
|
|
|
82
83
|
}
|
|
83
84
|
if (existsSync(join(cwd, "index.html")))
|
|
84
85
|
return cwd;
|
|
85
|
-
throw new Error("这里找不到能发布的网页(index.html)。\n" +
|
|
86
|
+
throw new Error(tr("这里找不到能发布的网页(index.html)。\n" +
|
|
86
87
|
" 在网站目录里运行 u1s1 deploy,或指定目录:u1s1 deploy <目录>\n" +
|
|
87
|
-
" 如果项目需要构建(如 Vite/Next),先跑构建再部署 dist/ 等产物目录")
|
|
88
|
+
" 如果项目需要构建(如 Vite/Next),先跑构建再部署 dist/ 等产物目录", "No publishable page (index.html) found here.\n" +
|
|
89
|
+
" Run u1s1 deploy in your site directory, or pass one: u1s1 deploy <dir>\n" +
|
|
90
|
+
" If the project needs a build step (Vite/Next etc.), build first and deploy dist/ or similar"));
|
|
88
91
|
}
|
|
89
92
|
export function collectFiles(root) {
|
|
90
93
|
const files = [];
|
|
@@ -133,23 +136,23 @@ export function parseDeployArgs(args) {
|
|
|
133
136
|
if (arg === "--name" || arg === "-n") {
|
|
134
137
|
const value = args[++i];
|
|
135
138
|
if (!value || value.startsWith("-"))
|
|
136
|
-
throw new Error(`${arg}
|
|
139
|
+
throw new Error(tr(`${arg} 后面需要站点名`, `${arg} needs a site name`));
|
|
137
140
|
parsed.name = value.toLowerCase();
|
|
138
141
|
}
|
|
139
142
|
else if (arg.startsWith("--name=")) {
|
|
140
143
|
parsed.name = arg.slice(7).toLowerCase();
|
|
141
144
|
if (!parsed.name)
|
|
142
|
-
throw new Error("--name 后面需要站点名");
|
|
145
|
+
throw new Error(tr("--name 后面需要站点名", "--name needs a site name"));
|
|
143
146
|
}
|
|
144
147
|
else if (arg === "--public" || arg === "--private") {
|
|
145
148
|
const visibility = arg.slice(2);
|
|
146
149
|
if (parsed.visibility && parsed.visibility !== visibility) {
|
|
147
|
-
throw new Error("--public 和 --private 不能同时使用");
|
|
150
|
+
throw new Error(tr("--public 和 --private 不能同时使用", "--public and --private can't be used together"));
|
|
148
151
|
}
|
|
149
152
|
parsed.visibility = visibility;
|
|
150
153
|
}
|
|
151
154
|
else if (arg.startsWith("-")) {
|
|
152
|
-
throw new Error(`不认识的 deploy 参数:${arg}`);
|
|
155
|
+
throw new Error(tr(`不认识的 deploy 参数:${arg}`, `Unknown deploy option: ${arg}`));
|
|
153
156
|
}
|
|
154
157
|
else {
|
|
155
158
|
parsed.dirArg = arg;
|
|
@@ -168,7 +171,7 @@ async function api(cfg, request) {
|
|
|
168
171
|
});
|
|
169
172
|
}
|
|
170
173
|
catch {
|
|
171
|
-
throw new Error(`连不上 ${cfg.baseUrl}
|
|
174
|
+
throw new Error(tr(`连不上 ${cfg.baseUrl},检查一下网络?`, `Can't reach ${cfg.baseUrl}. Check your network?`));
|
|
172
175
|
}
|
|
173
176
|
const value = await readJsonResponseCapped(resp, MAX_DEPLOY_API_RESPONSE_BYTES).catch(() => null);
|
|
174
177
|
const data = value && typeof value === "object" && !Array.isArray(value)
|
|
@@ -180,7 +183,7 @@ async function api(cfg, request) {
|
|
|
180
183
|
: undefined;
|
|
181
184
|
const message = typeof detail?.message === "string"
|
|
182
185
|
? detail.message.slice(0, 2_000)
|
|
183
|
-
: `服务端返回 ${resp.status}
|
|
186
|
+
: tr(`服务端返回 ${resp.status},稍后再试`, `Server returned ${resp.status}. Please try again later.`);
|
|
184
187
|
const e = new Error(message);
|
|
185
188
|
if (typeof detail?.code === "string" && detail.code.length <= 100) {
|
|
186
189
|
e.code = detail.code;
|
|
@@ -188,7 +191,7 @@ async function api(cfg, request) {
|
|
|
188
191
|
throw e;
|
|
189
192
|
}
|
|
190
193
|
if (!data)
|
|
191
|
-
throw new Error("服务端部署响应格式不正确");
|
|
194
|
+
throw new Error(tr("服务端部署响应格式不正确", "Unexpected deploy response from the server"));
|
|
192
195
|
return data;
|
|
193
196
|
}
|
|
194
197
|
/** 逐个上传,失败重试一次;并发数保守取 6。 */
|
|
@@ -218,12 +221,12 @@ async function uploadAll(cfg, start, files) {
|
|
|
218
221
|
const body = value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
219
222
|
const message = body?.error && typeof body.error === "object" && typeof body.error.message === "string"
|
|
220
223
|
? body.error.message.slice(0, 2_000)
|
|
221
|
-
: "网络错误";
|
|
222
|
-
throw new Error(`上传 ${f.path} 失败:${message}`);
|
|
224
|
+
: tr("网络错误", "network error");
|
|
225
|
+
throw new Error(tr(`上传 ${f.path} 失败:${message}`, `Failed to upload ${f.path}: ${message}`));
|
|
223
226
|
}
|
|
224
227
|
void resp.body?.cancel("deploy upload completed").catch(() => { });
|
|
225
228
|
done++;
|
|
226
|
-
process.stdout.write(`\r 上传中 ${done}/${files.length} ${f.path.slice(0, 48).padEnd(48)}`);
|
|
229
|
+
process.stdout.write(tr(`\r 上传中 ${done}/${files.length} ${f.path.slice(0, 48).padEnd(48)}`, `\r Uploading ${done}/${files.length} ${f.path.slice(0, 48).padEnd(48)}`));
|
|
227
230
|
};
|
|
228
231
|
const workers = Array.from({ length: Math.min(6, queue.length) }, async () => {
|
|
229
232
|
for (let f = queue.shift(); f; f = queue.shift())
|
|
@@ -238,60 +241,60 @@ async function uploadAll(cfg, start, files) {
|
|
|
238
241
|
}
|
|
239
242
|
}
|
|
240
243
|
async function promptSiteName(def) {
|
|
241
|
-
return (await askLine(` 项目名(回车用 ${def},只需在你的账号内唯一):`, def)).toLowerCase();
|
|
244
|
+
return (await askLine(tr(` 项目名(回车用 ${def},只需在你的账号内唯一):`, ` Project name (Enter for ${def}; only needs to be unique in your account): `), def)).toLowerCase();
|
|
242
245
|
}
|
|
243
246
|
async function promptVisibility() {
|
|
244
247
|
if (!process.stdin.isTTY)
|
|
245
248
|
return "private";
|
|
246
249
|
while (true) {
|
|
247
|
-
const answer = (await askLine(" 发布方式 [1] 公开(进入作品社区) [2] 私密(仅自己登录后可看,默认):")).toLowerCase();
|
|
250
|
+
const answer = (await askLine(tr(" 发布方式 [1] 公开(进入作品社区) [2] 私密(仅自己登录后可看,默认):", " Visibility [1] Public (listed in the community) [2] Private (only you, when signed in; default): "))).toLowerCase();
|
|
248
251
|
if (!answer || answer === "2" || answer === "private" || answer === "私密")
|
|
249
252
|
return "private";
|
|
250
253
|
if (answer === "1" || answer === "public" || answer === "公开")
|
|
251
254
|
return "public";
|
|
252
|
-
console.log(" 请输入 1 或 2");
|
|
255
|
+
console.log(tr(" 请输入 1 或 2", " Please enter 1 or 2"));
|
|
253
256
|
}
|
|
254
257
|
}
|
|
255
258
|
async function listDeployments(cfg) {
|
|
256
259
|
const { sites } = await api(cfg, { method: "GET", path: "/deploy/sites" });
|
|
257
260
|
if (!sites.length) {
|
|
258
|
-
console.log(" 还没有部署过站点。在网页目录里跑 u1s1 deploy 试试。");
|
|
261
|
+
console.log(tr(" 还没有部署过站点。在网页目录里跑 u1s1 deploy 试试。", " No sites deployed yet. Try u1s1 deploy in your site directory."));
|
|
259
262
|
return;
|
|
260
263
|
}
|
|
261
264
|
console.log("");
|
|
262
265
|
for (const site of sites) {
|
|
263
266
|
const visibility = site.visibility === "private"
|
|
264
|
-
? "私密"
|
|
265
|
-
: site.community_listed ? "公开(社区)" : "公开(未展示)";
|
|
267
|
+
? tr("私密", "private")
|
|
268
|
+
: site.community_listed ? tr("公开(社区)", "public (community)") : tr("公开(未展示)", "public (unlisted)");
|
|
266
269
|
console.log(` ${site.deployed ? "●" : "○"} ${site.url} ${visibility} · ${fmtBytes(site.total_bytes)} · ${site.updated_at} UTC`);
|
|
267
270
|
}
|
|
268
271
|
console.log("");
|
|
269
272
|
}
|
|
270
273
|
async function confirmRemoval(url) {
|
|
271
|
-
const answer = (await askLine(` 确定删除 ${url} 吗?网址会立即失效,站点文件会被清除,无法恢复。输入 y
|
|
274
|
+
const answer = (await askLine(tr(` 确定删除 ${url} 吗?网址会立即失效,站点文件会被清除,无法恢复。输入 y 确认:`, ` Delete ${url}? The URL stops working immediately and the files are erased for good. Type y to confirm: `))).toLowerCase();
|
|
272
275
|
return answer === "y" || answer === "yes";
|
|
273
276
|
}
|
|
274
277
|
async function removeDeployment(cfg, args) {
|
|
275
278
|
const yes = args.some((arg) => arg === "--yes" || arg === "-y");
|
|
276
279
|
const positional = args.filter((arg) => !arg.startsWith("-"));
|
|
277
280
|
if (positional.length !== 1) {
|
|
278
|
-
throw new Error("用法:u1s1 deploy remove <站点名或网址> [--yes]");
|
|
281
|
+
throw new Error(tr("用法:u1s1 deploy remove <站点名或网址> [--yes]", "Usage: u1s1 deploy remove <site name or URL> [--yes]"));
|
|
279
282
|
}
|
|
280
283
|
const reference = siteReferenceFromInput(positional[0]);
|
|
281
284
|
if (!reference)
|
|
282
|
-
throw new Error("站点名不能为空");
|
|
285
|
+
throw new Error(tr("站点名不能为空", "Site name can't be empty"));
|
|
283
286
|
// 先按列表核对,拿到完整网址给确认提示;名字打错在这里就能提前发现
|
|
284
287
|
const { sites } = await api(cfg, { method: "GET", path: "/deploy/sites" });
|
|
285
288
|
const site = sites.find((s) => (s.slug || s.name) === reference || s.name === reference);
|
|
286
289
|
if (!site) {
|
|
287
|
-
throw new Error(`没有找到站点「${reference}」。运行 u1s1 deploy list
|
|
290
|
+
throw new Error(tr(`没有找到站点「${reference}」。运行 u1s1 deploy list 查看已有站点`, `Site "${reference}" not found. Run u1s1 deploy list to see your sites`));
|
|
288
291
|
}
|
|
289
292
|
if (!yes) {
|
|
290
293
|
if (!process.stdin.isTTY) {
|
|
291
|
-
throw new Error("非交互环境下删除站点需要加 --yes 明确确认");
|
|
294
|
+
throw new Error(tr("非交互环境下删除站点需要加 --yes 明确确认", "Deleting a site non-interactively requires --yes"));
|
|
292
295
|
}
|
|
293
296
|
if (!(await confirmRemoval(site.url))) {
|
|
294
|
-
console.log(" 已取消,站点保留。");
|
|
297
|
+
console.log(tr(" 已取消,站点保留。", " Cancelled; the site was kept."));
|
|
295
298
|
return;
|
|
296
299
|
}
|
|
297
300
|
}
|
|
@@ -301,8 +304,8 @@ async function removeDeployment(cfg, args) {
|
|
|
301
304
|
});
|
|
302
305
|
forgetSite(site.slug || site.name);
|
|
303
306
|
console.log("");
|
|
304
|
-
console.log(` ✓ 已删除 ${site.url}`);
|
|
305
|
-
console.log(" 同一个名字可以再次 u1s1 deploy 重新发布。");
|
|
307
|
+
console.log(tr(` ✓ 已删除 ${site.url}`, ` ✓ Deleted ${site.url}`));
|
|
308
|
+
console.log(tr(" 同一个名字可以再次 u1s1 deploy 重新发布。", " You can publish under the same name again with u1s1 deploy."));
|
|
306
309
|
console.log("");
|
|
307
310
|
}
|
|
308
311
|
async function selectDeployment(args) {
|
|
@@ -311,12 +314,12 @@ async function selectDeployment(args) {
|
|
|
311
314
|
const dir = resolveSiteDir(dirArg);
|
|
312
315
|
const files = collectFiles(dir);
|
|
313
316
|
if (!files.some((file) => file.path === "index.html")) {
|
|
314
|
-
throw new Error(`${dir} 里没有 index.html
|
|
317
|
+
throw new Error(tr(`${dir} 里没有 index.html,网站需要一个首页`, `${dir} has no index.html; a site needs a home page`));
|
|
315
318
|
}
|
|
316
319
|
const totalBytes = files.reduce((sum, file) => sum + file.bytes, 0);
|
|
317
320
|
console.log("");
|
|
318
|
-
console.log(` 部署目录 ${dir}`);
|
|
319
|
-
console.log(` 文件 ${files.length} 个,共 ${fmtBytes(totalBytes)}`);
|
|
321
|
+
console.log(tr(` 部署目录 ${dir}`, ` Directory ${dir}`));
|
|
322
|
+
console.log(tr(` 文件 ${files.length} 个,共 ${fmtBytes(totalBytes)}`, ` Files ${files.length}, ${fmtBytes(totalBytes)} total`));
|
|
320
323
|
// 站点名:--name > 上次用过的 > 交互询问(默认目录名;dist 等产物目录用项目名)
|
|
321
324
|
const remembered = readDeploys()[dir];
|
|
322
325
|
if (!name && remembered)
|
|
@@ -364,44 +367,45 @@ async function startDeployment(cfg, initialName) {
|
|
|
364
367
|
function validateDeploymentLimits(start, files, totalBytes) {
|
|
365
368
|
const tooBig = files.filter((file) => file.bytes > start.limits.max_file_bytes);
|
|
366
369
|
if (tooBig.length) {
|
|
367
|
-
throw new Error(`这些文件超过单文件上限 ${fmtBytes(start.limits.max_file_bytes)}:\n` +
|
|
370
|
+
throw new Error(tr(`这些文件超过单文件上限 ${fmtBytes(start.limits.max_file_bytes)}:\n`, `These files exceed the per-file limit of ${fmtBytes(start.limits.max_file_bytes)}:\n`) +
|
|
368
371
|
tooBig.map((file) => ` ${file.path}(${fmtBytes(file.bytes)})`).join("\n"));
|
|
369
372
|
}
|
|
370
373
|
if (files.length > start.limits.max_files || totalBytes > start.limits.max_total_bytes) {
|
|
371
|
-
throw new Error(`超出配额:最多 ${start.limits.max_files} 个文件 / ${fmtBytes(start.limits.max_total_bytes)}。` +
|
|
372
|
-
`当前 ${files.length} 个 / ${fmtBytes(totalBytes)}`)
|
|
374
|
+
throw new Error(tr(`超出配额:最多 ${start.limits.max_files} 个文件 / ${fmtBytes(start.limits.max_total_bytes)}。` +
|
|
375
|
+
`当前 ${files.length} 个 / ${fmtBytes(totalBytes)}`, `Over quota: at most ${start.limits.max_files} files / ${fmtBytes(start.limits.max_total_bytes)}. ` +
|
|
376
|
+
`This deploy has ${files.length} files / ${fmtBytes(totalBytes)}`));
|
|
373
377
|
}
|
|
374
378
|
}
|
|
375
379
|
function printDeploymentResult(result, start, site) {
|
|
376
380
|
const links = siteLinks(site);
|
|
377
|
-
console.log(` ✅ 部署完成,${result.file_count}
|
|
381
|
+
console.log(tr(` ✅ 部署完成,${result.file_count} 个文件已上线`, ` ✅ Deployed, ${result.file_count} files live`));
|
|
378
382
|
console.log("");
|
|
379
383
|
console.log(` ${result.visibility === "private" ? "🔒" : "🌐"} ${result.url}`);
|
|
380
384
|
console.log("");
|
|
381
385
|
if ((result.hostname || start.hostname || "").split(".").length > 3) {
|
|
382
|
-
console.log(" 新 hostname 的 HTTPS 证书会由 Total TLS 自动签发,首次访问可能需要等待几分钟。");
|
|
386
|
+
console.log(tr(" 新 hostname 的 HTTPS 证书会由 Total TLS 自动签发,首次访问可能需要等待几分钟。", " The HTTPS certificate for a new hostname is issued automatically; the first visit may take a few minutes."));
|
|
383
387
|
}
|
|
384
388
|
if (result.visibility === "private") {
|
|
385
|
-
console.log(` 私密站点仅你可见,请从 ${links.sites}
|
|
389
|
+
console.log(tr(` 私密站点仅你可见,请从 ${links.sites} 打开。`, ` Private sites are only visible to you; open them from ${links.sites}`));
|
|
386
390
|
}
|
|
387
391
|
else if (result.community_listed) {
|
|
388
|
-
console.log(` 已进入 ${links.community}
|
|
392
|
+
console.log(tr(` 已进入 ${links.community} ,把网址发给朋友就能看。`, ` Listed on ${links.community} — share the URL with friends.`));
|
|
389
393
|
}
|
|
390
394
|
else {
|
|
391
|
-
console.log(" 公开链接可直接访问;运行 u1s1 deploy --public 可明确加入作品社区。");
|
|
395
|
+
console.log(tr(" 公开链接可直接访问;运行 u1s1 deploy --public 可明确加入作品社区。", " The public link works for anyone; run u1s1 deploy --public to list it in the community."));
|
|
392
396
|
}
|
|
393
|
-
console.log(" 改完代码再跑一次 u1s1 deploy 即可更新。");
|
|
397
|
+
console.log(tr(" 改完代码再跑一次 u1s1 deploy 即可更新。", " Run u1s1 deploy again after changes to update."));
|
|
394
398
|
console.log("");
|
|
395
399
|
}
|
|
396
400
|
export function printDeployHelp() {
|
|
397
401
|
console.log("");
|
|
398
|
-
console.log(" u1s1 deploy [目录] [--name 站点名] [--public|--private]");
|
|
402
|
+
console.log(tr(" u1s1 deploy [目录] [--name 站点名] [--public|--private]", " u1s1 deploy [dir] [--name site-name] [--public|--private]"));
|
|
399
403
|
console.log("");
|
|
400
|
-
console.log(" 不带参数时自动找构建产物目录(dist/build/out 等),否则用当前目录。");
|
|
401
|
-
console.log(" 首次发布会问项目名和公开/私密,之后记住;再跑一次即为更新。");
|
|
404
|
+
console.log(tr(" 不带参数时自动找构建产物目录(dist/build/out 等),否则用当前目录。", " With no directory, uses a build output dir (dist/build/out etc.) if found, else the current dir."));
|
|
405
|
+
console.log(tr(" 首次发布会问项目名和公开/私密,之后记住;再跑一次即为更新。", " The first deploy asks for a name and visibility and remembers them; run it again to update."));
|
|
402
406
|
console.log("");
|
|
403
|
-
console.log(" u1s1 deploy list 查看已发布的站点");
|
|
404
|
-
console.log(" u1s1 deploy remove <站点名或网址> 删除站点(会二次确认,--yes 跳过)");
|
|
407
|
+
console.log(tr(" u1s1 deploy list 查看已发布的站点", " u1s1 deploy list List your sites"));
|
|
408
|
+
console.log(tr(" u1s1 deploy remove <站点名或网址> 删除站点(会二次确认,--yes 跳过)", " u1s1 deploy remove <name-or-url> Delete a site (asks to confirm; --yes skips)"));
|
|
405
409
|
console.log("");
|
|
406
410
|
}
|
|
407
411
|
export async function deployCommand(cfg, args) {
|
package/dist/device-auth.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID, webcrypto } from "node:crypto";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
import { VERSION } from "./config.js";
|
|
4
|
+
import { tr } from "./i18n.js";
|
|
4
5
|
const enc = new TextEncoder();
|
|
5
6
|
const SIGNING_PROXY_REQUEST_LIMIT_BYTES = 32 * 1024 * 1024;
|
|
6
7
|
let cachedPrivate;
|
|
@@ -42,7 +43,7 @@ async function privateKey(jwk) {
|
|
|
42
43
|
}
|
|
43
44
|
export async function dpopHeaders(cfg, method, url) {
|
|
44
45
|
if (!hasDeviceCredential(cfg))
|
|
45
|
-
throw new Error("当前安装还没有设备凭证,请重新运行 u1s1 login");
|
|
46
|
+
throw new Error(tr("当前安装还没有设备凭证,请重新运行 u1s1 login", "This install has no device credential yet. Run u1s1 login again."));
|
|
46
47
|
const token = cfg.deviceToken;
|
|
47
48
|
const header = b64url(JSON.stringify({
|
|
48
49
|
typ: "dpop+jwt",
|
|
@@ -76,7 +77,7 @@ export async function authorizedFetch(cfg, input, init = {}) {
|
|
|
76
77
|
headers.set("authorization", `Bearer ${cfg.apiKey}`);
|
|
77
78
|
}
|
|
78
79
|
else {
|
|
79
|
-
throw new Error("还没有登录,请运行 u1s1 login");
|
|
80
|
+
throw new Error(tr("还没有登录,请运行 u1s1 login", "Not signed in. Run u1s1 login."));
|
|
80
81
|
}
|
|
81
82
|
return fetch(url, { ...init, headers });
|
|
82
83
|
}
|
|
@@ -214,7 +215,7 @@ function clientSurface(fallback) {
|
|
|
214
215
|
*/
|
|
215
216
|
export async function ensureSigningProxy(cfg, fallbackClient = "terminal", attestationSource) {
|
|
216
217
|
if (!hasDeviceCredential(cfg))
|
|
217
|
-
throw new Error("当前安装需要重新登录,以创建设备凭证");
|
|
218
|
+
throw new Error(tr("当前安装需要重新登录,以创建设备凭证", "Please sign in again to create a device credential for this install."));
|
|
218
219
|
const client = clientSurface(fallbackClient);
|
|
219
220
|
const current = signingProxy;
|
|
220
221
|
if (current && current.token === cfg.deviceToken && current.client === client) {
|
|
@@ -286,10 +287,11 @@ export async function ensureSigningProxy(cfg, fallbackClient = "terminal", attes
|
|
|
286
287
|
if (res.headersSent)
|
|
287
288
|
return void res.destroy(error);
|
|
288
289
|
res.writeHead(502, { "content-type": "application/json" });
|
|
289
|
-
//
|
|
290
|
+
// 说明放前面给用户看;原始英文错误保留在括号里,pi 的重试分类正则
|
|
290
291
|
// (fetch failed / timeout 等)和排查都还认得
|
|
291
292
|
const raw = error instanceof Error ? error.message : "signing proxy failed";
|
|
292
|
-
|
|
293
|
+
const message = tr(`连不上 u1s1 服务器,检查一下网络?(${raw})`, `Can't reach the u1s1 server. Check your network? (${raw})`);
|
|
294
|
+
res.end(JSON.stringify({ error: { message } }));
|
|
293
295
|
}
|
|
294
296
|
});
|
|
295
297
|
await new Promise((resolve, reject) => {
|
package/dist/error-humanize.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { type Site } from "./site.js";
|
|
|
16
16
|
* 额度用尽(reason=exhausted)且今天已打卡时网关还带 error.resets_at(下次可打卡时刻,
|
|
17
17
|
* 即北京时间 0 点,ISO),这里换算成「距下次可打卡还有 N 小时 M 分」接在正文后:比
|
|
18
18
|
* 「北京时间 0 点」更省用户脑子。今天没打卡时网关不带 resets_at(现在就能领)。
|
|
19
|
+
* 国际站没有打卡/免费包(网关也不带 resets_at),英文界面一律不给这句。
|
|
19
20
|
*/
|
|
20
21
|
export declare function quotaResetHint(resetsAt: unknown, now?: number): string;
|
|
21
22
|
/** 只取错误里的 request_id(纯函数,给 request-trace 记「最近一次请求编号」用)。 */
|
package/dist/error-humanize.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { activeSite } from "./config.js";
|
|
2
|
+
import { isEnglish, tr } from "./i18n.js";
|
|
2
3
|
import { siteLinks } from "./site.js";
|
|
3
4
|
/**
|
|
4
5
|
* 模型调用失败时,网关的中文说明会被 OpenAI SDK + pi-ai 包成
|
|
@@ -17,8 +18,11 @@ import { siteLinks } from "./site.js";
|
|
|
17
18
|
* 额度用尽(reason=exhausted)且今天已打卡时网关还带 error.resets_at(下次可打卡时刻,
|
|
18
19
|
* 即北京时间 0 点,ISO),这里换算成「距下次可打卡还有 N 小时 M 分」接在正文后:比
|
|
19
20
|
* 「北京时间 0 点」更省用户脑子。今天没打卡时网关不带 resets_at(现在就能领)。
|
|
21
|
+
* 国际站没有打卡/免费包(网关也不带 resets_at),英文界面一律不给这句。
|
|
20
22
|
*/
|
|
21
23
|
export function quotaResetHint(resetsAt, now = Date.now()) {
|
|
24
|
+
if (isEnglish())
|
|
25
|
+
return "";
|
|
22
26
|
if (typeof resetsAt !== "string")
|
|
23
27
|
return "";
|
|
24
28
|
const at = Date.parse(resetsAt);
|
|
@@ -72,10 +76,10 @@ function humanizeOpaqueError(raw, site) {
|
|
|
72
76
|
return undefined;
|
|
73
77
|
const n = Number(status);
|
|
74
78
|
const body = n >= 500
|
|
75
|
-
? `服务暂时不可用,稍等再试;持续出现可看状态页 ${siteLinks(site).status}`
|
|
79
|
+
? tr(`服务暂时不可用,稍等再试;持续出现可看状态页 ${siteLinks(site).status}`, `The service is temporarily unavailable. Please try again shortly; if it keeps happening, check the status page: ${siteLinks(site).status}`)
|
|
76
80
|
: n === 429
|
|
77
|
-
? "请求太频繁,稍等再试"
|
|
78
|
-
: "请求被拒绝,重试仍失败可运行 u1s1 feedback 反馈";
|
|
81
|
+
? tr("请求太频繁,稍等再试", "Too many requests. Please wait a moment and try again.")
|
|
82
|
+
: tr("请求被拒绝,重试仍失败可运行 u1s1 feedback 反馈", "The request was rejected. If retrying doesn't help, run u1s1 feedback to report it.");
|
|
79
83
|
return `${body} (HTTP ${status})`;
|
|
80
84
|
}
|
|
81
85
|
export function humanizeModelError(raw, now = Date.now(), site) {
|
|
@@ -91,13 +95,21 @@ export function humanizeModelError(raw, now = Date.now(), site) {
|
|
|
91
95
|
const type = typeof err["type"] === "string" ? err["type"] : "";
|
|
92
96
|
const status = /(?:^|\s|\()(\d{3})\b/.exec(raw.slice(0, jsonStart))?.[1];
|
|
93
97
|
const requestId = typeof err["request_id"] === "string" ? err["request_id"] : "";
|
|
98
|
+
const insufficient = type === "insufficient_quota" || code === "quota_exceeded";
|
|
94
99
|
const tags = [
|
|
95
100
|
status ? `HTTP ${status}` : "",
|
|
96
|
-
|
|
97
|
-
requestId ? `请求编号 ${requestId}` : "",
|
|
101
|
+
insufficient ? "insufficient_quota" : code,
|
|
102
|
+
requestId ? tr(`请求编号 ${requestId}`, `request ID ${requestId}`) : "",
|
|
98
103
|
].filter(Boolean);
|
|
99
104
|
const resetHint = err["reason"] === "exhausted" ? quotaResetHint(err["resets_at"], now) : "";
|
|
100
|
-
|
|
105
|
+
let body = resetHint ? `${message}〔${resetHint}〕` : message;
|
|
106
|
+
// 国际站(英文界面)余额不足:网关正文本该带充值链接;老网关/个别路由没带时补一句,指向本站充值卡片。
|
|
107
|
+
// 中文界面不动(中国站网关文案自带仪表盘链接,另有免费包等分流说明)
|
|
108
|
+
if (insufficient && isEnglish()) {
|
|
109
|
+
const links = siteLinks(site ?? activeSite());
|
|
110
|
+
if (!message.includes(links.dashboard))
|
|
111
|
+
body = `${body} Top up to continue → ${links.topup}`;
|
|
112
|
+
}
|
|
101
113
|
const friendly = tags.length ? `${body} (${tags.join(" · ")})` : body;
|
|
102
114
|
return friendly === raw ? undefined : friendly;
|
|
103
115
|
}
|
package/dist/feedback.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readJsonResponseCapped } from "./api.js";
|
|
2
2
|
import { activeSite, loadConfig, PROVIDER_ID, resolvePreferredModel, VERSION } from "./config.js";
|
|
3
3
|
import { authorizedFetch, hasDeviceCredential } from "./device-auth.js";
|
|
4
|
+
import { tr } from "./i18n.js";
|
|
4
5
|
import { askLine } from "./prompt.js";
|
|
5
6
|
import { lastRequestId } from "./request-trace.js";
|
|
6
7
|
import { siteLinks, siteOf } from "./site.js";
|
|
@@ -10,13 +11,14 @@ import { siteLinks, siteOf } from "./site.js";
|
|
|
10
11
|
* 走 POST /v1/support/tickets(设备签名),201 返回 {id, url}。
|
|
11
12
|
*/
|
|
12
13
|
export const FEEDBACK_CATEGORIES = ["question", "bug", "billing", "suggestion", "other"];
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
// 用时求值:模块加载时还不知道站点/语言
|
|
15
|
+
const categoryLabels = () => ({
|
|
16
|
+
question: tr("提问", "question"),
|
|
17
|
+
bug: tr("故障", "bug"),
|
|
18
|
+
billing: tr("计费", "billing"),
|
|
19
|
+
suggestion: tr("建议", "suggestion"),
|
|
20
|
+
other: tr("其他", "other"),
|
|
21
|
+
});
|
|
20
22
|
export const FEEDBACK_SUBJECT_MAX = 50;
|
|
21
23
|
export const FEEDBACK_MESSAGE_MAX = 4_000;
|
|
22
24
|
/** 当前站点仪表盘的工单/留言区,提交失败或服务端没回链接时的兜底入口。 */
|
|
@@ -36,7 +38,7 @@ export function parseFeedbackArgs(args) {
|
|
|
36
38
|
continue;
|
|
37
39
|
}
|
|
38
40
|
if (arg.startsWith("--"))
|
|
39
|
-
throw new Error(`不认识的 feedback 参数:${arg}(可用 --bug --question --suggestion --billing)`);
|
|
41
|
+
throw new Error(tr(`不认识的 feedback 参数:${arg}(可用 --bug --question --suggestion --billing)`, `Unknown feedback option: ${arg} (use --bug --question --suggestion --billing)`));
|
|
40
42
|
words.push(arg);
|
|
41
43
|
}
|
|
42
44
|
return { category, text: words.join(" ").trim() };
|
|
@@ -45,7 +47,7 @@ export function parseFeedbackArgs(args) {
|
|
|
45
47
|
export function buildFeedbackRequest(input) {
|
|
46
48
|
const message = input.text.replace(/\r\n?/g, "\n").trim();
|
|
47
49
|
if (!message)
|
|
48
|
-
throw new Error("反馈内容不能为空");
|
|
50
|
+
throw new Error(tr("反馈内容不能为空", "Feedback can't be empty"));
|
|
49
51
|
const firstLine = message.split("\n").find((l) => l.trim())?.replace(/\s+/g, " ").trim() ?? message;
|
|
50
52
|
const subject = firstLine.length > FEEDBACK_SUBJECT_MAX
|
|
51
53
|
? `${firstLine.slice(0, FEEDBACK_SUBJECT_MAX - 1)}…`
|
|
@@ -90,7 +92,7 @@ export async function submitFeedback(cfg, body) {
|
|
|
90
92
|
});
|
|
91
93
|
}
|
|
92
94
|
catch {
|
|
93
|
-
throw new Error(`连不上 ${cfg.baseUrl}
|
|
95
|
+
throw new Error(tr(`连不上 ${cfg.baseUrl},检查一下网络?`, `Can't reach ${cfg.baseUrl}. Check your network?`));
|
|
94
96
|
}
|
|
95
97
|
const value = await readJsonResponseCapped(resp, MAX_TICKET_RESPONSE_BYTES).catch(() => null);
|
|
96
98
|
const data = value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -106,7 +108,7 @@ export async function submitFeedback(cfg, body) {
|
|
|
106
108
|
function createdOutcome(status, data, site) {
|
|
107
109
|
const id = typeof data?.id === "string" || typeof data?.id === "number" ? String(data.id) : "";
|
|
108
110
|
if (!id)
|
|
109
|
-
return { ok: false, status, message: "服务端工单响应格式不正确" };
|
|
111
|
+
return { ok: false, status, message: tr("服务端工单响应格式不正确", "Unexpected support ticket response from the server") };
|
|
110
112
|
const url = typeof data?.url === "string" && data.url.startsWith("https://") ? data.url : supportUrl(site);
|
|
111
113
|
return { ok: true, id, url };
|
|
112
114
|
}
|
|
@@ -121,32 +123,40 @@ function retryAfterSeconds(resp, data) {
|
|
|
121
123
|
function failureMessage(status, data) {
|
|
122
124
|
if (typeof data?.error?.message === "string")
|
|
123
125
|
return data.error.message.slice(0, 500);
|
|
124
|
-
return status === 401
|
|
126
|
+
return status === 401
|
|
127
|
+
? tr("登录已失效,请重新运行 u1s1 login", "Your session has expired. Run u1s1 login to sign in again.")
|
|
128
|
+
: tr(`服务端返回 ${status},稍后再试`, `Server returned ${status}. Please try again later.`);
|
|
125
129
|
}
|
|
126
130
|
/** 结果措辞,终端与会话内共用(每项一行)。 */
|
|
127
131
|
export function feedbackOutcomeLines(outcome, site = activeSite()) {
|
|
128
132
|
if (outcome.ok) {
|
|
129
133
|
return [
|
|
130
|
-
`✅ 反馈已提交,工单编号 ${outcome.id}。`,
|
|
131
|
-
`进度与回复在这里看:${outcome.url}`,
|
|
132
|
-
"我们通常会在 24 小时内回复到你的注册邮箱。",
|
|
134
|
+
tr(`✅ 反馈已提交,工单编号 ${outcome.id}。`, `✅ Feedback sent. Ticket #${outcome.id}.`),
|
|
135
|
+
tr(`进度与回复在这里看:${outcome.url}`, `Track progress and replies here: ${outcome.url}`),
|
|
136
|
+
tr("我们通常会在 24 小时内回复到你的注册邮箱。", "We usually reply to your account email within 24 hours."),
|
|
133
137
|
];
|
|
134
138
|
}
|
|
135
139
|
if (outcome.status === 429) {
|
|
136
140
|
const wait = outcome.retryAfterSeconds
|
|
137
|
-
? `请等 ${Math.ceil(outcome.retryAfterSeconds / 60) || 1}
|
|
138
|
-
: "请稍等几分钟再试";
|
|
139
|
-
return [
|
|
141
|
+
? tr(`请等 ${Math.ceil(outcome.retryAfterSeconds / 60) || 1} 分钟后再试`, `please wait ${Math.ceil(outcome.retryAfterSeconds / 60) || 1} min and try again`)
|
|
142
|
+
: tr("请稍等几分钟再试", "please wait a few minutes and try again");
|
|
143
|
+
return [
|
|
144
|
+
tr(`提交太频繁了,${wait}。`, `Too many submissions; ${wait}.`),
|
|
145
|
+
tr(`等不及的话直接到后台留言:${supportUrl(site)}`, `Or leave a message on the dashboard: ${supportUrl(site)}`),
|
|
146
|
+
];
|
|
140
147
|
}
|
|
141
|
-
return [
|
|
148
|
+
return [
|
|
149
|
+
tr(`提交失败:${outcome.message}`, `Submission failed: ${outcome.message}`),
|
|
150
|
+
tr(`可以直接到后台留言:${supportUrl(site)}`, `You can leave a message on the dashboard instead: ${supportUrl(site)}`),
|
|
151
|
+
];
|
|
142
152
|
}
|
|
143
153
|
export function printFeedbackHelp() {
|
|
144
154
|
console.log("");
|
|
145
|
-
console.log(" u1s1 feedback [--bug|--question|--suggestion|--billing] [一句话描述]");
|
|
155
|
+
console.log(tr(" u1s1 feedback [--bug|--question|--suggestion|--billing] [一句话描述]", " u1s1 feedback [--bug|--question|--suggestion|--billing] [short description]"));
|
|
146
156
|
console.log("");
|
|
147
|
-
console.log(" 不带描述时会问你一行;分类不写默认「其他」。");
|
|
148
|
-
console.log(" 会自动附上版本、平台、最近一次报错的请求编号,方便我们定位。");
|
|
149
|
-
console.log(` 也可以在后台留言:${supportUrl()}`);
|
|
157
|
+
console.log(tr(" 不带描述时会问你一行;分类不写默认「其他」。", " Without a description you'll be asked for one line; the category defaults to \"other\"."));
|
|
158
|
+
console.log(tr(" 会自动附上版本、平台、最近一次报错的请求编号,方便我们定位。", " Your version, platform and the request ID of the last error are attached automatically to help us debug."));
|
|
159
|
+
console.log(tr(` 也可以在后台留言:${supportUrl()}`, ` You can also leave a message on the dashboard: ${supportUrl()}`));
|
|
150
160
|
console.log("");
|
|
151
161
|
}
|
|
152
162
|
/** 终端命令入口。 */
|
|
@@ -158,16 +168,16 @@ export async function feedbackCommand(args) {
|
|
|
158
168
|
}
|
|
159
169
|
const cfg = loadConfig();
|
|
160
170
|
if (!hasDeviceCredential(cfg)) {
|
|
161
|
-
console.error("还没登录,先跑 u1s1 login");
|
|
171
|
+
console.error(tr("还没登录,先跑 u1s1 login", "Not signed in. Run u1s1 login first."));
|
|
162
172
|
process.exitCode = 1;
|
|
163
173
|
return;
|
|
164
174
|
}
|
|
165
175
|
let text = parsed.text;
|
|
166
176
|
if (!text) {
|
|
167
|
-
text = await askLine(` 想反馈什么?(${
|
|
177
|
+
text = await askLine(tr(` 想反馈什么?(${categoryLabels()[parsed.category]},一行说清即可):`, ` What's your feedback? (${categoryLabels()[parsed.category]}, one line is enough): `));
|
|
168
178
|
}
|
|
169
179
|
if (!text) {
|
|
170
|
-
console.error("反馈内容不能为空。用法:u1s1 feedback \"一句话描述\"");
|
|
180
|
+
console.error(tr("反馈内容不能为空。用法:u1s1 feedback \"一句话描述\"", "Feedback can't be empty. Usage: u1s1 feedback \"short description\""));
|
|
171
181
|
process.exitCode = 1;
|
|
172
182
|
return;
|
|
173
183
|
}
|
|
@@ -191,13 +201,17 @@ export async function feedbackCommand(args) {
|
|
|
191
201
|
export async function feedbackInSession(rawArgs, model) {
|
|
192
202
|
const parsed = parseFeedbackArgs(rawArgs.split(/\s+/).filter(Boolean));
|
|
193
203
|
if (parsed.help) {
|
|
194
|
-
return [
|
|
204
|
+
return [
|
|
205
|
+
tr("/feedback [--bug|--question|--suggestion|--billing] 一句话描述", "/feedback [--bug|--question|--suggestion|--billing] short description"),
|
|
206
|
+
tr(`或到后台留言:${supportUrl()}`, `Or leave a message on the dashboard: ${supportUrl()}`),
|
|
207
|
+
];
|
|
195
208
|
}
|
|
196
209
|
const cfg = loadConfig();
|
|
197
210
|
if (!hasDeviceCredential(cfg))
|
|
198
|
-
return ["还没登录,先在终端运行 u1s1 login"];
|
|
199
|
-
if (!parsed.text)
|
|
200
|
-
return ["反馈内容不能为空,例如:/feedback --bug 生成的网页打不开"];
|
|
211
|
+
return [tr("还没登录,先在终端运行 u1s1 login", "Not signed in. Run u1s1 login in your terminal first.")];
|
|
212
|
+
if (!parsed.text) {
|
|
213
|
+
return [tr("反馈内容不能为空,例如:/feedback --bug 生成的网页打不开", "Feedback can't be empty, e.g. /feedback --bug the generated page won't open")];
|
|
214
|
+
}
|
|
201
215
|
const body = buildFeedbackRequest({
|
|
202
216
|
text: parsed.text,
|
|
203
217
|
category: parsed.category,
|
|
@@ -207,6 +221,9 @@ export async function feedbackInSession(rawArgs, model) {
|
|
|
207
221
|
return feedbackOutcomeLines(await submitFeedback(cfg, body), siteOf(cfg));
|
|
208
222
|
}
|
|
209
223
|
catch (e) {
|
|
210
|
-
return [
|
|
224
|
+
return [
|
|
225
|
+
e instanceof Error ? e.message : String(e),
|
|
226
|
+
tr(`可以直接到后台留言:${supportUrl(siteOf(cfg))}`, `You can leave a message on the dashboard instead: ${supportUrl(siteOf(cfg))}`),
|
|
227
|
+
];
|
|
211
228
|
}
|
|
212
229
|
}
|
package/dist/i18n.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 界面语言跟站点走:国际站(u1s1.dev)英文,中国站中文;不提供语言切换
|
|
3
|
+
* (运营者 2026-10-01 定「国际版只要英文」)。站点按下载渠道定,见 site.ts。
|
|
4
|
+
*
|
|
5
|
+
* 文案就地写成 tr("中文", "English"),不做 key 表:两种语言挨着,改一处顺手改另一处。
|
|
6
|
+
* 模块顶层常量不能直接调 tr —— 那时配置可能还没读,要写成函数在用时求值。
|
|
7
|
+
*
|
|
8
|
+
* 本模块不 import 任何本地模块:config.ts 在加载时用 setLangResolver 注册「按当前站点
|
|
9
|
+
* 判定」,saveConfig 后 resetLang(),`u1s1 login --site` 切站后同进程立即换语言。
|
|
10
|
+
*/
|
|
11
|
+
export type Lang = "zh" | "en";
|
|
12
|
+
export declare function setLangResolver(fn: () => Lang): void;
|
|
13
|
+
/** 配置变了(换站)后调用,下次 lang() 重新判定。 */
|
|
14
|
+
export declare function resetLang(): void;
|
|
15
|
+
/**
|
|
16
|
+
* 当前界面语言。U1S1_LANG=en|zh 可强制(测试用;CLI 拉起 pi 前也写它,
|
|
17
|
+
* 让 pi 补丁里的文案跟着走,见 patches/ 与 scripts/patch-pi.js)。
|
|
18
|
+
*/
|
|
19
|
+
export declare function lang(): Lang;
|
|
20
|
+
export declare function isEnglish(): boolean;
|
|
21
|
+
export declare function tr(zh: string, en: string): string;
|
package/dist/i18n.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 界面语言跟站点走:国际站(u1s1.dev)英文,中国站中文;不提供语言切换
|
|
3
|
+
* (运营者 2026-10-01 定「国际版只要英文」)。站点按下载渠道定,见 site.ts。
|
|
4
|
+
*
|
|
5
|
+
* 文案就地写成 tr("中文", "English"),不做 key 表:两种语言挨着,改一处顺手改另一处。
|
|
6
|
+
* 模块顶层常量不能直接调 tr —— 那时配置可能还没读,要写成函数在用时求值。
|
|
7
|
+
*
|
|
8
|
+
* 本模块不 import 任何本地模块:config.ts 在加载时用 setLangResolver 注册「按当前站点
|
|
9
|
+
* 判定」,saveConfig 后 resetLang(),`u1s1 login --site` 切站后同进程立即换语言。
|
|
10
|
+
*/
|
|
11
|
+
let resolver;
|
|
12
|
+
let cached;
|
|
13
|
+
export function setLangResolver(fn) {
|
|
14
|
+
resolver = fn;
|
|
15
|
+
cached = undefined;
|
|
16
|
+
}
|
|
17
|
+
/** 配置变了(换站)后调用,下次 lang() 重新判定。 */
|
|
18
|
+
export function resetLang() {
|
|
19
|
+
cached = undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 当前界面语言。U1S1_LANG=en|zh 可强制(测试用;CLI 拉起 pi 前也写它,
|
|
23
|
+
* 让 pi 补丁里的文案跟着走,见 patches/ 与 scripts/patch-pi.js)。
|
|
24
|
+
*/
|
|
25
|
+
export function lang() {
|
|
26
|
+
const forced = process.env["U1S1_LANG"];
|
|
27
|
+
if (forced === "en" || forced === "zh")
|
|
28
|
+
return forced;
|
|
29
|
+
if (cached)
|
|
30
|
+
return cached;
|
|
31
|
+
if (!resolver)
|
|
32
|
+
return "zh"; // config.ts 还没加载:不缓存,免得把默认值钉死
|
|
33
|
+
try {
|
|
34
|
+
cached = resolver();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
cached = "zh";
|
|
38
|
+
}
|
|
39
|
+
return cached;
|
|
40
|
+
}
|
|
41
|
+
export function isEnglish() {
|
|
42
|
+
return lang() === "en";
|
|
43
|
+
}
|
|
44
|
+
export function tr(zh, en) {
|
|
45
|
+
return lang() === "en" ? en : zh;
|
|
46
|
+
}
|