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.
Files changed (52) hide show
  1. package/README.md +5 -5
  2. package/dist/agent-setup.d.ts +7 -1
  3. package/dist/agent-setup.js +34 -10
  4. package/dist/api.js +42 -34
  5. package/dist/bench-report.js +12 -9
  6. package/dist/bench.js +34 -31
  7. package/dist/brand.d.ts +7 -2
  8. package/dist/brand.js +44 -13
  9. package/dist/config.d.ts +28 -2
  10. package/dist/config.js +52 -14
  11. package/dist/context-usage.js +12 -9
  12. package/dist/deploy.js +53 -47
  13. package/dist/device-auth.js +7 -5
  14. package/dist/embed.d.ts +2 -1
  15. package/dist/embed.js +2 -1
  16. package/dist/error-humanize.d.ts +3 -1
  17. package/dist/error-humanize.js +24 -9
  18. package/dist/feedback.d.ts +4 -2
  19. package/dist/feedback.js +60 -39
  20. package/dist/i18n.d.ts +21 -0
  21. package/dist/i18n.js +46 -0
  22. package/dist/import/index.js +64 -28
  23. package/dist/import/skills.js +72 -41
  24. package/dist/import/util.js +3 -2
  25. package/dist/import/write.js +2 -1
  26. package/dist/index.js +153 -54
  27. package/dist/login.d.ts +13 -2
  28. package/dist/login.js +84 -27
  29. package/dist/loop.js +18 -12
  30. package/dist/mcp/client.js +15 -14
  31. package/dist/mcp/command.js +76 -48
  32. package/dist/mcp/extension.js +5 -3
  33. package/dist/mcp/tools.js +6 -5
  34. package/dist/model.js +12 -10
  35. package/dist/nudges.d.ts +8 -2
  36. package/dist/nudges.js +22 -3
  37. package/dist/search-tools.js +7 -6
  38. package/dist/shell-doctor.js +9 -8
  39. package/dist/site.d.ts +71 -0
  40. package/dist/site.js +123 -0
  41. package/dist/subagent.js +5 -4
  42. package/dist/tools.js +53 -37
  43. package/dist/update.js +47 -39
  44. package/dist/usage.d.ts +6 -4
  45. package/dist/usage.js +111 -53
  46. package/dist/web.js +2 -1
  47. package/dist/workflow/runner.js +33 -29
  48. package/dist/workflow/templates.js +70 -45
  49. package/dist/workflow/tool.js +15 -12
  50. package/package.json +3 -3
  51. package/scripts/patch-pi.js +243 -278
  52. package/scripts/render-regression.mjs +55 -4
package/dist/deploy.js CHANGED
@@ -4,8 +4,10 @@ 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";
10
+ import { siteLinks, siteOf } from "./site.js";
9
11
  /**
10
12
  * u1s1 deploy publishes a static site to <project>.<account-code>.u1s1.app.
11
13
  * Detect the site root, ask for a project slug, and remember it in ~/.u1s1/deploys.json.
@@ -67,10 +69,10 @@ function resolveSiteDir(explicit) {
67
69
  if (explicit) {
68
70
  const dir = resolve(explicit);
69
71
  if (!existsSync(dir) || !statSync(dir).isDirectory()) {
70
- throw new Error(`目录不存在:${dir}`);
72
+ throw new Error(tr(`目录不存在:${dir}`, `Directory not found: ${dir}`));
71
73
  }
72
74
  if (!existsSync(join(dir, "index.html"))) {
73
- 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`));
74
76
  }
75
77
  return dir;
76
78
  }
@@ -81,9 +83,11 @@ function resolveSiteDir(explicit) {
81
83
  }
82
84
  if (existsSync(join(cwd, "index.html")))
83
85
  return cwd;
84
- throw new Error("这里找不到能发布的网页(index.html)。\n" +
86
+ throw new Error(tr("这里找不到能发布的网页(index.html)。\n" +
85
87
  " 在网站目录里运行 u1s1 deploy,或指定目录:u1s1 deploy <目录>\n" +
86
- " 如果项目需要构建(如 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"));
87
91
  }
88
92
  export function collectFiles(root) {
89
93
  const files = [];
@@ -132,23 +136,23 @@ export function parseDeployArgs(args) {
132
136
  if (arg === "--name" || arg === "-n") {
133
137
  const value = args[++i];
134
138
  if (!value || value.startsWith("-"))
135
- throw new Error(`${arg} 后面需要站点名`);
139
+ throw new Error(tr(`${arg} 后面需要站点名`, `${arg} needs a site name`));
136
140
  parsed.name = value.toLowerCase();
137
141
  }
138
142
  else if (arg.startsWith("--name=")) {
139
143
  parsed.name = arg.slice(7).toLowerCase();
140
144
  if (!parsed.name)
141
- throw new Error("--name 后面需要站点名");
145
+ throw new Error(tr("--name 后面需要站点名", "--name needs a site name"));
142
146
  }
143
147
  else if (arg === "--public" || arg === "--private") {
144
148
  const visibility = arg.slice(2);
145
149
  if (parsed.visibility && parsed.visibility !== visibility) {
146
- throw new Error("--public 和 --private 不能同时使用");
150
+ throw new Error(tr("--public 和 --private 不能同时使用", "--public and --private can't be used together"));
147
151
  }
148
152
  parsed.visibility = visibility;
149
153
  }
150
154
  else if (arg.startsWith("-")) {
151
- throw new Error(`不认识的 deploy 参数:${arg}`);
155
+ throw new Error(tr(`不认识的 deploy 参数:${arg}`, `Unknown deploy option: ${arg}`));
152
156
  }
153
157
  else {
154
158
  parsed.dirArg = arg;
@@ -167,7 +171,7 @@ async function api(cfg, request) {
167
171
  });
168
172
  }
169
173
  catch {
170
- throw new Error(`连不上 ${cfg.baseUrl},检查一下网络?`);
174
+ throw new Error(tr(`连不上 ${cfg.baseUrl},检查一下网络?`, `Can't reach ${cfg.baseUrl}. Check your network?`));
171
175
  }
172
176
  const value = await readJsonResponseCapped(resp, MAX_DEPLOY_API_RESPONSE_BYTES).catch(() => null);
173
177
  const data = value && typeof value === "object" && !Array.isArray(value)
@@ -179,7 +183,7 @@ async function api(cfg, request) {
179
183
  : undefined;
180
184
  const message = typeof detail?.message === "string"
181
185
  ? detail.message.slice(0, 2_000)
182
- : `服务端返回 ${resp.status},稍后再试`;
186
+ : tr(`服务端返回 ${resp.status},稍后再试`, `Server returned ${resp.status}. Please try again later.`);
183
187
  const e = new Error(message);
184
188
  if (typeof detail?.code === "string" && detail.code.length <= 100) {
185
189
  e.code = detail.code;
@@ -187,7 +191,7 @@ async function api(cfg, request) {
187
191
  throw e;
188
192
  }
189
193
  if (!data)
190
- throw new Error("服务端部署响应格式不正确");
194
+ throw new Error(tr("服务端部署响应格式不正确", "Unexpected deploy response from the server"));
191
195
  return data;
192
196
  }
193
197
  /** 逐个上传,失败重试一次;并发数保守取 6。 */
@@ -217,12 +221,12 @@ async function uploadAll(cfg, start, files) {
217
221
  const body = value && typeof value === "object" && !Array.isArray(value) ? value : null;
218
222
  const message = body?.error && typeof body.error === "object" && typeof body.error.message === "string"
219
223
  ? body.error.message.slice(0, 2_000)
220
- : "网络错误";
221
- throw new Error(`上传 ${f.path} 失败:${message}`);
224
+ : tr("网络错误", "network error");
225
+ throw new Error(tr(`上传 ${f.path} 失败:${message}`, `Failed to upload ${f.path}: ${message}`));
222
226
  }
223
227
  void resp.body?.cancel("deploy upload completed").catch(() => { });
224
228
  done++;
225
- 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)}`));
226
230
  };
227
231
  const workers = Array.from({ length: Math.min(6, queue.length) }, async () => {
228
232
  for (let f = queue.shift(); f; f = queue.shift())
@@ -237,60 +241,60 @@ async function uploadAll(cfg, start, files) {
237
241
  }
238
242
  }
239
243
  async function promptSiteName(def) {
240
- 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();
241
245
  }
242
246
  async function promptVisibility() {
243
247
  if (!process.stdin.isTTY)
244
248
  return "private";
245
249
  while (true) {
246
- 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();
247
251
  if (!answer || answer === "2" || answer === "private" || answer === "私密")
248
252
  return "private";
249
253
  if (answer === "1" || answer === "public" || answer === "公开")
250
254
  return "public";
251
- console.log(" 请输入 1 或 2");
255
+ console.log(tr(" 请输入 1 或 2", " Please enter 1 or 2"));
252
256
  }
253
257
  }
254
258
  async function listDeployments(cfg) {
255
259
  const { sites } = await api(cfg, { method: "GET", path: "/deploy/sites" });
256
260
  if (!sites.length) {
257
- console.log(" 还没有部署过站点。在网页目录里跑 u1s1 deploy 试试。");
261
+ console.log(tr(" 还没有部署过站点。在网页目录里跑 u1s1 deploy 试试。", " No sites deployed yet. Try u1s1 deploy in your site directory."));
258
262
  return;
259
263
  }
260
264
  console.log("");
261
265
  for (const site of sites) {
262
266
  const visibility = site.visibility === "private"
263
- ? "私密"
264
- : site.community_listed ? "公开(社区)" : "公开(未展示)";
267
+ ? tr("私密", "private")
268
+ : site.community_listed ? tr("公开(社区)", "public (community)") : tr("公开(未展示)", "public (unlisted)");
265
269
  console.log(` ${site.deployed ? "●" : "○"} ${site.url} ${visibility} · ${fmtBytes(site.total_bytes)} · ${site.updated_at} UTC`);
266
270
  }
267
271
  console.log("");
268
272
  }
269
273
  async function confirmRemoval(url) {
270
- const answer = (await askLine(` 确定删除 ${url} 吗?网址会立即失效,站点文件会被清除,无法恢复。输入 y 确认:`)).toLowerCase();
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();
271
275
  return answer === "y" || answer === "yes";
272
276
  }
273
277
  async function removeDeployment(cfg, args) {
274
278
  const yes = args.some((arg) => arg === "--yes" || arg === "-y");
275
279
  const positional = args.filter((arg) => !arg.startsWith("-"));
276
280
  if (positional.length !== 1) {
277
- 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]"));
278
282
  }
279
283
  const reference = siteReferenceFromInput(positional[0]);
280
284
  if (!reference)
281
- throw new Error("站点名不能为空");
285
+ throw new Error(tr("站点名不能为空", "Site name can't be empty"));
282
286
  // 先按列表核对,拿到完整网址给确认提示;名字打错在这里就能提前发现
283
287
  const { sites } = await api(cfg, { method: "GET", path: "/deploy/sites" });
284
288
  const site = sites.find((s) => (s.slug || s.name) === reference || s.name === reference);
285
289
  if (!site) {
286
- 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`));
287
291
  }
288
292
  if (!yes) {
289
293
  if (!process.stdin.isTTY) {
290
- throw new Error("非交互环境下删除站点需要加 --yes 明确确认");
294
+ throw new Error(tr("非交互环境下删除站点需要加 --yes 明确确认", "Deleting a site non-interactively requires --yes"));
291
295
  }
292
296
  if (!(await confirmRemoval(site.url))) {
293
- console.log(" 已取消,站点保留。");
297
+ console.log(tr(" 已取消,站点保留。", " Cancelled; the site was kept."));
294
298
  return;
295
299
  }
296
300
  }
@@ -300,8 +304,8 @@ async function removeDeployment(cfg, args) {
300
304
  });
301
305
  forgetSite(site.slug || site.name);
302
306
  console.log("");
303
- console.log(` ✓ 已删除 ${site.url}`);
304
- 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."));
305
309
  console.log("");
306
310
  }
307
311
  async function selectDeployment(args) {
@@ -310,12 +314,12 @@ async function selectDeployment(args) {
310
314
  const dir = resolveSiteDir(dirArg);
311
315
  const files = collectFiles(dir);
312
316
  if (!files.some((file) => file.path === "index.html")) {
313
- 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`));
314
318
  }
315
319
  const totalBytes = files.reduce((sum, file) => sum + file.bytes, 0);
316
320
  console.log("");
317
- console.log(` 部署目录 ${dir}`);
318
- 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`));
319
323
  // 站点名:--name > 上次用过的 > 交互询问(默认目录名;dist 等产物目录用项目名)
320
324
  const remembered = readDeploys()[dir];
321
325
  if (!name && remembered)
@@ -363,43 +367,45 @@ async function startDeployment(cfg, initialName) {
363
367
  function validateDeploymentLimits(start, files, totalBytes) {
364
368
  const tooBig = files.filter((file) => file.bytes > start.limits.max_file_bytes);
365
369
  if (tooBig.length) {
366
- 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`) +
367
371
  tooBig.map((file) => ` ${file.path}(${fmtBytes(file.bytes)})`).join("\n"));
368
372
  }
369
373
  if (files.length > start.limits.max_files || totalBytes > start.limits.max_total_bytes) {
370
- throw new Error(`超出配额:最多 ${start.limits.max_files} 个文件 / ${fmtBytes(start.limits.max_total_bytes)}。` +
371
- `当前 ${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)}`));
372
377
  }
373
378
  }
374
- function printDeploymentResult(result, start) {
375
- console.log(` ✅ 部署完成,${result.file_count} 个文件已上线`);
379
+ function printDeploymentResult(result, start, site) {
380
+ const links = siteLinks(site);
381
+ console.log(tr(` ✅ 部署完成,${result.file_count} 个文件已上线`, ` ✅ Deployed, ${result.file_count} files live`));
376
382
  console.log("");
377
383
  console.log(` ${result.visibility === "private" ? "🔒" : "🌐"} ${result.url}`);
378
384
  console.log("");
379
385
  if ((result.hostname || start.hostname || "").split(".").length > 3) {
380
- 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."));
381
387
  }
382
388
  if (result.visibility === "private") {
383
- console.log(" 私密站点仅你可见,请从 https://u1s1.io/dashboard#sec-sites 打开。");
389
+ console.log(tr(` 私密站点仅你可见,请从 ${links.sites} 打开。`, ` Private sites are only visible to you; open them from ${links.sites}`));
384
390
  }
385
391
  else if (result.community_listed) {
386
- console.log(" 已进入 https://u1s1.io/community ,把网址发给朋友就能看。");
392
+ console.log(tr(` 已进入 ${links.community} ,把网址发给朋友就能看。`, ` Listed on ${links.community} — share the URL with friends.`));
387
393
  }
388
394
  else {
389
- 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."));
390
396
  }
391
- console.log(" 改完代码再跑一次 u1s1 deploy 即可更新。");
397
+ console.log(tr(" 改完代码再跑一次 u1s1 deploy 即可更新。", " Run u1s1 deploy again after changes to update."));
392
398
  console.log("");
393
399
  }
394
400
  export function printDeployHelp() {
395
401
  console.log("");
396
- console.log(" u1s1 deploy [目录] [--name 站点名] [--public|--private]");
402
+ console.log(tr(" u1s1 deploy [目录] [--name 站点名] [--public|--private]", " u1s1 deploy [dir] [--name site-name] [--public|--private]"));
397
403
  console.log("");
398
- console.log(" 不带参数时自动找构建产物目录(dist/build/out 等),否则用当前目录。");
399
- 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."));
400
406
  console.log("");
401
- console.log(" u1s1 deploy list 查看已发布的站点");
402
- 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)"));
403
409
  console.log("");
404
410
  }
405
411
  export async function deployCommand(cfg, args) {
@@ -433,7 +439,7 @@ export async function deployCommand(cfg, args) {
433
439
  rememberSite(selection.dir, start.slug || name || start.site);
434
440
  // 部署过一次之后,会话里不再推「u1s1 deploy 一键上线」
435
441
  markNudge("deploy_done");
436
- printDeploymentResult(result, start);
442
+ printDeploymentResult(result, start, siteOf(cfg));
437
443
  await sendTelemetryEvent(cfg, "deploy_done", {
438
444
  public: result.visibility === "public",
439
445
  file_count: result.file_count,
@@ -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
- // 中文说明放前面给用户看;原始英文错误保留在括号里,pi 的重试分类正则
290
+ // 说明放前面给用户看;原始英文错误保留在括号里,pi 的重试分类正则
290
291
  // (fetch failed / timeout 等)和排查都还认得
291
292
  const raw = error instanceof Error ? error.message : "signing proxy failed";
292
- res.end(JSON.stringify({ error: { message: `连不上 u1s1 服务器,检查一下网络?(${raw})` } }));
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/embed.d.ts CHANGED
@@ -3,10 +3,11 @@
3
3
  * package.json exports)。App 与 CLI 共享 ~/.u1s1 的登录态和 agentDir 配置,
4
4
  * 这里只做转发,不放实现 —— 保证两个入口永远走同一份逻辑。
5
5
  */
6
- export { agentDir, loadConfig, saveConfig, u1s1Dir, VERSION, type CliConfig, } from "./config.js";
6
+ export { activeSite, agentDir, loadConfig, saveConfig, u1s1Dir, VERSION, type CliConfig, } from "./config.js";
7
7
  export { fetchMe, fetchModels } from "./api.js";
8
8
  export { apiOrigin, pollDeviceLogin, startDeviceLogin, type DeviceStart } from "./login.js";
9
9
  export { prepareWebEnv, refreshWebModels } from "./web.js";
10
10
  export { applyWebUiBranding } from "./webui-brand.js";
11
11
  export { DASHBOARD_URL } from "./brand.js";
12
+ export { SITES, siteForBaseUrl, siteLinks, type Site, type SiteId, type SiteLinks } from "./site.js";
12
13
  export { hasNudge, markNudge, readNudges, type NudgeKey } from "./nudges.js";
package/dist/embed.js CHANGED
@@ -3,10 +3,11 @@
3
3
  * package.json exports)。App 与 CLI 共享 ~/.u1s1 的登录态和 agentDir 配置,
4
4
  * 这里只做转发,不放实现 —— 保证两个入口永远走同一份逻辑。
5
5
  */
6
- export { agentDir, loadConfig, saveConfig, u1s1Dir, VERSION, } from "./config.js";
6
+ export { activeSite, agentDir, loadConfig, saveConfig, u1s1Dir, VERSION, } from "./config.js";
7
7
  export { fetchMe, fetchModels } from "./api.js";
8
8
  export { apiOrigin, pollDeviceLogin, startDeviceLogin } from "./login.js";
9
9
  export { prepareWebEnv, refreshWebModels } from "./web.js";
10
10
  export { applyWebUiBranding } from "./webui-brand.js";
11
11
  export { DASHBOARD_URL } from "./brand.js";
12
+ export { SITES, siteForBaseUrl, siteLinks } from "./site.js";
12
13
  export { hasNudge, markNudge, readNudges } from "./nudges.js";
@@ -1,3 +1,4 @@
1
+ import { type Site } from "./site.js";
1
2
  /**
2
3
  * 模型调用失败时,网关的中文说明会被 OpenAI SDK + pi-ai 包成
3
4
  * `429: {"message":"…","type":"…","code":"…"}` 直出到聊天区。
@@ -15,8 +16,9 @@
15
16
  * 额度用尽(reason=exhausted)且今天已打卡时网关还带 error.resets_at(下次可打卡时刻,
16
17
  * 即北京时间 0 点,ISO),这里换算成「距下次可打卡还有 N 小时 M 分」接在正文后:比
17
18
  * 「北京时间 0 点」更省用户脑子。今天没打卡时网关不带 resets_at(现在就能领)。
19
+ * 国际站没有打卡/免费包(网关也不带 resets_at),英文界面一律不给这句。
18
20
  */
19
21
  export declare function quotaResetHint(resetsAt: unknown, now?: number): string;
20
22
  /** 只取错误里的 request_id(纯函数,给 request-trace 记「最近一次请求编号」用)。 */
21
23
  export declare function extractRequestId(raw: string): string | undefined;
22
- export declare function humanizeModelError(raw: string, now?: number): string | undefined;
24
+ export declare function humanizeModelError(raw: string, now?: number, site?: Site): string | undefined;
@@ -1,3 +1,6 @@
1
+ import { activeSite } from "./config.js";
2
+ import { isEnglish, tr } from "./i18n.js";
3
+ import { siteLinks } from "./site.js";
1
4
  /**
2
5
  * 模型调用失败时,网关的中文说明会被 OpenAI SDK + pi-ai 包成
3
6
  * `429: {"message":"…","type":"…","code":"…"}` 直出到聊天区。
@@ -15,8 +18,11 @@
15
18
  * 额度用尽(reason=exhausted)且今天已打卡时网关还带 error.resets_at(下次可打卡时刻,
16
19
  * 即北京时间 0 点,ISO),这里换算成「距下次可打卡还有 N 小时 M 分」接在正文后:比
17
20
  * 「北京时间 0 点」更省用户脑子。今天没打卡时网关不带 resets_at(现在就能领)。
21
+ * 国际站没有打卡/免费包(网关也不带 resets_at),英文界面一律不给这句。
18
22
  */
19
23
  export function quotaResetHint(resetsAt, now = Date.now()) {
24
+ if (isEnglish())
25
+ return "";
20
26
  if (typeof resetsAt !== "string")
21
27
  return "";
22
28
  const at = Date.parse(resetsAt);
@@ -61,7 +67,7 @@ export function extractRequestId(raw) {
61
67
  * `Error: 503 <!DOCTYPE html>…` 一大坨。只认得出状态码就给一句人话,
62
68
  * 状态码数字保留在尾巴里,pi 的重试分类(5xx/429 可重试)不受影响。
63
69
  */
64
- function humanizeOpaqueError(raw) {
70
+ function humanizeOpaqueError(raw, site) {
65
71
  const status = /^\s*(\d{3})\b/.exec(raw)?.[1];
66
72
  if (!status)
67
73
  return undefined;
@@ -70,16 +76,17 @@ function humanizeOpaqueError(raw) {
70
76
  return undefined;
71
77
  const n = Number(status);
72
78
  const body = n >= 500
73
- ? "服务暂时不可用,稍等再试;持续出现可看状态页 https://u1s1.io/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}`)
74
80
  : n === 429
75
- ? "请求太频繁,稍等再试"
76
- : "请求被拒绝,重试仍失败可运行 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.");
77
83
  return `${body} (HTTP ${status})`;
78
84
  }
79
- export function humanizeModelError(raw, now = Date.now()) {
85
+ export function humanizeModelError(raw, now = Date.now(), site) {
80
86
  const payload = parseErrorPayload(raw);
87
+ // 站点只有状态页链接要用,按需现读配置,正常 JSON 错误路径不碰磁盘
81
88
  if (!payload)
82
- return humanizeOpaqueError(raw);
89
+ return humanizeOpaqueError(raw, site ?? activeSite());
83
90
  const { err, jsonStart } = payload;
84
91
  const message = typeof err["message"] === "string" ? err["message"].trim() : "";
85
92
  if (!message)
@@ -88,13 +95,21 @@ export function humanizeModelError(raw, now = Date.now()) {
88
95
  const type = typeof err["type"] === "string" ? err["type"] : "";
89
96
  const status = /(?:^|\s|\()(\d{3})\b/.exec(raw.slice(0, jsonStart))?.[1];
90
97
  const requestId = typeof err["request_id"] === "string" ? err["request_id"] : "";
98
+ const insufficient = type === "insufficient_quota" || code === "quota_exceeded";
91
99
  const tags = [
92
100
  status ? `HTTP ${status}` : "",
93
- type === "insufficient_quota" || code === "quota_exceeded" ? "insufficient_quota" : code,
94
- requestId ? `请求编号 ${requestId}` : "",
101
+ insufficient ? "insufficient_quota" : code,
102
+ requestId ? tr(`请求编号 ${requestId}`, `request ID ${requestId}`) : "",
95
103
  ].filter(Boolean);
96
104
  const resetHint = err["reason"] === "exhausted" ? quotaResetHint(err["resets_at"], now) : "";
97
- const body = resetHint ? `${message}〔${resetHint}〕` : message;
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
+ }
98
113
  const friendly = tags.length ? `${body} (${tags.join(" · ")})` : body;
99
114
  return friendly === raw ? undefined : friendly;
100
115
  }
@@ -1,4 +1,5 @@
1
1
  import { type CliConfig } from "./config.js";
2
+ import { type Site } from "./site.js";
2
3
  /**
3
4
  * `u1s1 feedback [text]` / 会话内 `/feedback`:一条命令直接建工单,并附上
4
5
  * 版本、平台、最近一次请求编号等诊断快照(运营清单 E4),不用再手写邮件。
@@ -8,7 +9,8 @@ export declare const FEEDBACK_CATEGORIES: readonly ["question", "bug", "billing"
8
9
  export type FeedbackCategory = (typeof FEEDBACK_CATEGORIES)[number];
9
10
  export declare const FEEDBACK_SUBJECT_MAX = 50;
10
11
  export declare const FEEDBACK_MESSAGE_MAX = 4000;
11
- export declare const SUPPORT_URL = "https://u1s1.io/dashboard#sec-support";
12
+ /** 当前站点仪表盘的工单/留言区,提交失败或服务端没回链接时的兜底入口。 */
13
+ export declare function supportUrl(site?: Site): string;
12
14
  export interface FeedbackArgs {
13
15
  category: FeedbackCategory;
14
16
  text: string;
@@ -55,7 +57,7 @@ export type FeedbackOutcome = {
55
57
  /** 提交工单;网络错误直接抛,HTTP 错误(含 429)转成结构化结果由调用方措辞。 */
56
58
  export declare function submitFeedback(cfg: CliConfig, body: FeedbackRequest): Promise<FeedbackOutcome>;
57
59
  /** 结果措辞,终端与会话内共用(每项一行)。 */
58
- export declare function feedbackOutcomeLines(outcome: FeedbackOutcome): string[];
60
+ export declare function feedbackOutcomeLines(outcome: FeedbackOutcome, site?: Site): string[];
59
61
  export declare function printFeedbackHelp(): void;
60
62
  /** 终端命令入口。 */
61
63
  export declare function feedbackCommand(args: string[]): Promise<void>;