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/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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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,
|
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/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";
|
package/dist/error-humanize.d.ts
CHANGED
|
@@ -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;
|
package/dist/error-humanize.js
CHANGED
|
@@ -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
|
-
?
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/feedback.d.ts
CHANGED
|
@@ -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
|
-
|
|
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>;
|