u1s1-cli 1.11.8 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-setup.d.ts +5 -0
- package/dist/agent-setup.js +31 -7
- package/dist/api.js +42 -34
- package/dist/bench-report.js +12 -9
- package/dist/bench.js +34 -31
- package/dist/brand.d.ts +2 -1
- package/dist/brand.js +38 -12
- package/dist/config.d.ts +11 -1
- package/dist/config.js +25 -8
- package/dist/context-usage.js +12 -9
- package/dist/deploy.js +49 -45
- package/dist/device-auth.js +7 -5
- package/dist/error-humanize.d.ts +1 -0
- package/dist/error-humanize.js +18 -6
- package/dist/feedback.js +48 -31
- package/dist/i18n.d.ts +21 -0
- package/dist/i18n.js +46 -0
- package/dist/import/index.js +64 -28
- package/dist/import/skills.js +72 -41
- package/dist/import/util.js +3 -2
- package/dist/import/write.js +2 -1
- package/dist/index.js +139 -48
- package/dist/login.js +39 -28
- package/dist/loop.js +18 -12
- package/dist/mcp/client.js +15 -14
- package/dist/mcp/command.js +76 -48
- package/dist/mcp/extension.js +5 -3
- package/dist/mcp/tools.js +6 -5
- package/dist/model.js +11 -10
- package/dist/nudges.d.ts +8 -2
- package/dist/nudges.js +22 -3
- package/dist/search-tools.js +7 -6
- package/dist/shell-doctor.js +9 -8
- package/dist/subagent.js +5 -4
- package/dist/tools.js +53 -37
- package/dist/update.js +29 -28
- package/dist/usage.d.ts +1 -1
- package/dist/usage.js +96 -43
- package/dist/web.js +2 -1
- package/dist/workflow/runner.js +33 -29
- package/dist/workflow/templates.js +70 -45
- package/dist/workflow/tool.js +15 -12
- package/package.json +2 -2
- package/scripts/patch-pi.js +243 -278
- package/scripts/render-regression.mjs +55 -4
package/dist/brand.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join, resolve, sep } from "node:path";
|
|
3
|
+
import { activeSite } from "./config.js";
|
|
4
|
+
import { isEnglish, tr } from "./i18n.js";
|
|
3
5
|
import { siteLinks } from "./site.js";
|
|
4
6
|
export const BRAND_NAME = "u1s1";
|
|
5
7
|
export const BRAND_CN = "有一说一";
|
|
6
|
-
|
|
8
|
+
/** 中文品牌名只在中国站界面出现;国际站只叫 u1s1。 */
|
|
9
|
+
export function brandTagline() {
|
|
10
|
+
return tr("说人话的 AI 编程搭子", "the plain-spoken AI coding assistant");
|
|
11
|
+
}
|
|
12
|
+
/** 横幅里品牌名后面那一行:中国站「有一说一 · 标语」,国际站只有英文标语。 */
|
|
13
|
+
function brandSubtitle() {
|
|
14
|
+
return isEnglish() ? "Plain-spoken AI coding assistant" : `${BRAND_CN} · ${brandTagline()}`;
|
|
15
|
+
}
|
|
7
16
|
/**
|
|
8
17
|
* 中国站仪表盘。只为 embed 导出保留(Desktop App 现在只有中国站包,P3 出 intl 包时
|
|
9
18
|
* 改用自带的站点配置);CLI 内部一律按当前站点取 siteLinks(activeSite()).dashboard。
|
|
@@ -30,10 +39,10 @@ export function cwdAdvice(cwd, dirs = {}) {
|
|
|
30
39
|
const here = resolve(cwd);
|
|
31
40
|
const within = (dir) => here === dir || here.startsWith(dir + sep);
|
|
32
41
|
if (within(configDir)) {
|
|
33
|
-
return "当前在 u1s1 的配置目录里(存着登录凭据和设置),模型的文件工具会直接看到它们;建议 /exit 后 cd 到你的项目文件夹再运行 u1s1";
|
|
42
|
+
return tr("当前在 u1s1 的配置目录里(存着登录凭据和设置),模型的文件工具会直接看到它们;建议 /exit 后 cd 到你的项目文件夹再运行 u1s1", "You're inside u1s1's config folder (it holds your sign-in credentials and settings), so the model's file tools can see them. /exit, cd to your project folder, and run u1s1 again.");
|
|
34
43
|
}
|
|
35
44
|
if (here === home) {
|
|
36
|
-
return "当前在用户主目录,模型找文件会翻遍整个主目录,又慢又容易改错地方;建议 /exit 后 cd 到项目文件夹再运行 u1s1";
|
|
45
|
+
return tr("当前在用户主目录,模型找文件会翻遍整个主目录,又慢又容易改错地方;建议 /exit 后 cd 到项目文件夹再运行 u1s1", "You're in your home folder, so the model will search all of it: slow, and easy to edit the wrong files. /exit, cd to a project folder, and run u1s1 again.");
|
|
37
46
|
}
|
|
38
47
|
return undefined;
|
|
39
48
|
}
|
|
@@ -72,13 +81,25 @@ function announcementLine(theme, a) {
|
|
|
72
81
|
}
|
|
73
82
|
/** 空目录首跑引导:不让新手面对空白输入框想 prompt,给几句能照抄的开场白。 */
|
|
74
83
|
function starterLines(theme) {
|
|
75
|
-
|
|
84
|
+
if (isEnglish()) {
|
|
85
|
+
return [
|
|
86
|
+
` ${theme.fg("muted", "Empty folder? Just describe what you want to build, for example:")}`,
|
|
87
|
+
` ${theme.fg("text", "\"Build a personal landing page with a short bio\"")}`,
|
|
88
|
+
` ${theme.fg("text", "\"Make a birthday card page for a friend with a little animation\"")}`,
|
|
89
|
+
` ${theme.fg("text", "\"Write a small tool that renames the photos in this folder by date\"")}`,
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
const lines = [
|
|
76
93
|
` ${theme.fg("muted", "这个文件夹还是空的?把想做的直接说出来就行,比如:")}`,
|
|
77
94
|
` ${theme.fg("text", "「做一个自我介绍网页,做完帮我发布出去」")}`,
|
|
78
95
|
` ${theme.fg("text", "「做一个给朋友的生日祝福页面,要有点小动画」")}`,
|
|
79
96
|
` ${theme.fg("text", "「写一个把文件夹里照片按日期重命名的小工具」")}`,
|
|
80
|
-
` ${theme.fg("muted", "网页做好后,输入 /exit 回到终端,运行 u1s1 deploy --public 一键上线,拿到可分享的链接发给朋友")}`,
|
|
81
97
|
];
|
|
98
|
+
// 国际站没有网页托管(u1s1 deploy),不提发布
|
|
99
|
+
if (activeSite().id !== "intl") {
|
|
100
|
+
lines.push(` ${theme.fg("muted", "网页做好后,输入 /exit 回到终端,运行 u1s1 deploy --public 一键上线,拿到可分享的链接发给朋友")}`);
|
|
101
|
+
}
|
|
102
|
+
return lines;
|
|
82
103
|
}
|
|
83
104
|
/**
|
|
84
105
|
* Startup hero, responsive to terminal width:
|
|
@@ -87,13 +108,18 @@ function starterLines(theme) {
|
|
|
87
108
|
export function renderBrandHeader(theme, input) {
|
|
88
109
|
const name = theme.bold(theme.fg("text", `${BRAND_NAME} v${input.version}`)) +
|
|
89
110
|
(input.notice ? ` ${theme.fg("accent", input.notice)}` : "");
|
|
90
|
-
const brand = theme.fg("muted",
|
|
91
|
-
const dir = theme.fg("dim", `cwd: ${formatHomePath(input.cwd)}${input.account ? ` · 账号 ${input.account}` : ""}`);
|
|
92
|
-
const hints = theme.fg("dim", "/help · Ctrl+V 图片 · Shift+Enter 换行 · Esc 中断");
|
|
111
|
+
const brand = theme.fg("muted", brandSubtitle());
|
|
112
|
+
const dir = theme.fg("dim", `cwd: ${formatHomePath(input.cwd)}${input.account ? tr(` · 账号 ${input.account}`, ` · account ${input.account}`) : ""}`);
|
|
113
|
+
const hints = theme.fg("dim", tr("/help · Ctrl+V 图片 · Shift+Enter 换行 · Esc 中断", "/help · Ctrl+V image · Shift+Enter newline · Esc stop"));
|
|
93
114
|
const starter = input.starterTips ? starterLines(theme) : [];
|
|
94
115
|
const warning = input.cwdWarning ? [` ${theme.fg("warning", "⚠")} ${theme.fg("warning", input.cwdWarning)}`] : [];
|
|
95
116
|
if (input.width < ART_WIDTH + 4) {
|
|
96
|
-
const lines = [
|
|
117
|
+
const lines = [
|
|
118
|
+
"",
|
|
119
|
+
` ${theme.fg("accent", "✻")} ${name}${isEnglish() ? "" : ` ${theme.fg("muted", BRAND_CN)}`}`,
|
|
120
|
+
` ${dir}`,
|
|
121
|
+
` ${theme.fg("dim", tr("Ctrl+V 粘贴图片", "Ctrl+V to paste images"))}`,
|
|
122
|
+
];
|
|
97
123
|
if (input.announcement)
|
|
98
124
|
lines.push(` ${announcementLine(theme, input.announcement)}`);
|
|
99
125
|
if (warning.length)
|
|
@@ -103,8 +129,8 @@ export function renderBrandHeader(theme, input) {
|
|
|
103
129
|
return [...lines, ""];
|
|
104
130
|
}
|
|
105
131
|
const art = HERO_ART.map((line) => ` ${paintArt(theme, line)}`);
|
|
106
|
-
// widest info row (hints) needs 46 cols beside the 28-col wordmark
|
|
107
|
-
if (input.width < ART_WIDTH + 46) {
|
|
132
|
+
// widest info row (hints) needs 46 cols beside the 28-col wordmark (English hints are wider: 56)
|
|
133
|
+
if (input.width < ART_WIDTH + (isEnglish() ? 56 : 46)) {
|
|
108
134
|
const lines = ["", ...art, "", ` ${name} ${brand}`, ` ${dir}`, ` ${hints}`];
|
|
109
135
|
if (input.announcement)
|
|
110
136
|
lines.push(` ${announcementLine(theme, input.announcement)}`);
|
|
@@ -134,6 +160,6 @@ export function printConsoleBanner(version) {
|
|
|
134
160
|
for (const line of HERO_ART)
|
|
135
161
|
console.log(` ${line}`);
|
|
136
162
|
console.log("");
|
|
137
|
-
console.log(` ${BRAND_NAME} v${version} — ${BRAND_CN},${
|
|
163
|
+
console.log(tr(` ${BRAND_NAME} v${version} — ${BRAND_CN},${brandTagline()}`, ` ${BRAND_NAME} v${version} — ${brandTagline()}`));
|
|
138
164
|
console.log("");
|
|
139
165
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface ApiModel {
|
|
|
39
39
|
max_tokens: number;
|
|
40
40
|
/** Older gateways omit this field; missing means unknown coverage. */
|
|
41
41
|
free_package_eligible?: boolean;
|
|
42
|
+
/** 本站默认模型(网关按站点定:中国站 deepseek-flash,国际站另配);老网关也会发。 */
|
|
43
|
+
is_default?: boolean;
|
|
42
44
|
/**
|
|
43
45
|
* 网关建议的自动压缩预算(token);缺失/null 时 CLI 用内置默认 200K。
|
|
44
46
|
* 用户设置 compaction.maxContextTokens 优先级更高(见 effectiveContextWindow)。
|
|
@@ -81,6 +83,8 @@ export interface ModelDef {
|
|
|
81
83
|
cost: ModelCost | null;
|
|
82
84
|
/** 网关下发的免费用量包覆盖标记;老网关缺失时为 undefined(不派生 note)。 */
|
|
83
85
|
freeEligible?: boolean;
|
|
86
|
+
/** 网关标的本站默认模型(is_default)。 */
|
|
87
|
+
isDefault?: boolean;
|
|
84
88
|
/** 网关按模型下发的自动压缩预算(token);缺失时用 DEFAULT_COMPACTION_BUDGET_TOKENS。 */
|
|
85
89
|
compactAtTokens?: number;
|
|
86
90
|
note: string;
|
|
@@ -101,7 +105,13 @@ export declare function effectiveContextWindow(m: Pick<ModelDef, "contextWindow"
|
|
|
101
105
|
export declare const MODELS: ModelDef[];
|
|
102
106
|
/** Replace MODELS with a fresh list fetched from server (e.g. at startup). */
|
|
103
107
|
export declare function setModelsFromApi(apiModels: ModelDef[]): void;
|
|
104
|
-
|
|
108
|
+
/** 离线兜底列表的首个模型(中国站默认);没拉到目录时用它。 */
|
|
109
|
+
export declare const FALLBACK_DEFAULT_MODEL_ID: string;
|
|
110
|
+
/**
|
|
111
|
+
* 当前默认模型:优先网关标的 is_default(两站默认模型不同,国际站目录里没有 deepseek-flash),
|
|
112
|
+
* 老网关没标时退回兜底 id(在目录里的话),再退回目录第一个。
|
|
113
|
+
*/
|
|
114
|
+
export declare function defaultModelId(): string;
|
|
105
115
|
export declare function resolveModel(nameOrAlias: string): ModelDef | undefined;
|
|
106
116
|
/** provider 注册键就是云端的端点 id(ep+uuid);models.json 清理旧条目按此形状识别。 */
|
|
107
117
|
export declare const ENDPOINT_ID_RE: RegExp;
|
package/dist/config.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createRequire } from "node:module";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { basename, dirname, join } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { resetLang, setLangResolver, tr } from "./i18n.js";
|
|
7
8
|
import { migrateStoredBaseUrl, SITES, siteOf } from "./site.js";
|
|
8
9
|
const require = createRequire(import.meta.url);
|
|
9
10
|
export const VERSION = require("../package.json").version;
|
|
@@ -80,6 +81,7 @@ export function apiModelToDef(m) {
|
|
|
80
81
|
cacheWrite: 0,
|
|
81
82
|
},
|
|
82
83
|
freeEligible: typeof m.free_package_eligible === "boolean" ? m.free_package_eligible : undefined,
|
|
84
|
+
isDefault: m.is_default === true ? true : undefined,
|
|
83
85
|
compactAtTokens: positiveInteger(m.compact_at_tokens),
|
|
84
86
|
note: "",
|
|
85
87
|
};
|
|
@@ -126,7 +128,8 @@ export const MODELS = [
|
|
|
126
128
|
contextWindow: 1_048_576,
|
|
127
129
|
maxTokens: 384_000,
|
|
128
130
|
cost: null,
|
|
129
|
-
|
|
131
|
+
// 离线兜底列表的说明按语言现取(模块加载时还不知道站点)
|
|
132
|
+
get note() { return tr("默认 · 免费用量包额度内免费,日常写代码首选,支持看图", "Default · everyday coding, supports images"); },
|
|
130
133
|
},
|
|
131
134
|
{
|
|
132
135
|
id: "x-ai/grok-4.6",
|
|
@@ -137,7 +140,7 @@ export const MODELS = [
|
|
|
137
140
|
contextWindow: 500_000,
|
|
138
141
|
maxTokens: 65_536,
|
|
139
142
|
cost: null,
|
|
140
|
-
note
|
|
143
|
+
get note() { return tr("更强 · 但烧额度快约 20 倍,难题再用", "Stronger · about 20× the cost, for hard problems"); },
|
|
141
144
|
},
|
|
142
145
|
];
|
|
143
146
|
/**
|
|
@@ -146,21 +149,23 @@ export const MODELS = [
|
|
|
146
149
|
* 让人切模型前就知道「这个不走免费包 / 大概贵多少」。
|
|
147
150
|
*/
|
|
148
151
|
function deriveModelNote(m, base) {
|
|
152
|
+
// 国际站没有免费包:不提免费包,只给价格倍数
|
|
149
153
|
if (m.freeEligible === true)
|
|
150
|
-
return "免费用量包可抵扣";
|
|
154
|
+
return tr("免费用量包可抵扣", "");
|
|
151
155
|
if (m.freeEligible === false) {
|
|
152
156
|
const blended = m.cost ? (m.cost.input + m.cost.output) / 2 : 0;
|
|
153
157
|
const baseBlended = base?.cost ? (base.cost.input + base.cost.output) / 2 : 0;
|
|
154
158
|
const mult = baseBlended > 0 ? Math.round(blended / baseBlended) : 0;
|
|
155
159
|
return mult >= 2
|
|
156
|
-
? `不走免费包 · 费用约为默认模型 ${mult}
|
|
157
|
-
: "不走免费包,用余额或全模型包";
|
|
160
|
+
? tr(`不走免费包 · 费用约为默认模型 ${mult} 倍`, `about ${mult}× the default model's price`)
|
|
161
|
+
: tr("不走免费包,用余额或全模型包", "");
|
|
158
162
|
}
|
|
159
163
|
return "";
|
|
160
164
|
}
|
|
161
165
|
/** Replace MODELS with a fresh list fetched from server (e.g. at startup). */
|
|
162
166
|
export function setModelsFromApi(apiModels) {
|
|
163
|
-
const base = apiModels.find((m) => m.
|
|
167
|
+
const base = apiModels.find((m) => m.isDefault)
|
|
168
|
+
?? apiModels.find((m) => m.id === FALLBACK_DEFAULT_MODEL_ID)
|
|
164
169
|
?? apiModels.find((m) => m.freeEligible === true);
|
|
165
170
|
for (const m of apiModels) {
|
|
166
171
|
if (!m.note)
|
|
@@ -169,7 +174,17 @@ export function setModelsFromApi(apiModels) {
|
|
|
169
174
|
MODELS.length = 0;
|
|
170
175
|
MODELS.push(...apiModels);
|
|
171
176
|
}
|
|
172
|
-
|
|
177
|
+
/** 离线兜底列表的首个模型(中国站默认);没拉到目录时用它。 */
|
|
178
|
+
export const FALLBACK_DEFAULT_MODEL_ID = MODELS[0].id;
|
|
179
|
+
/**
|
|
180
|
+
* 当前默认模型:优先网关标的 is_default(两站默认模型不同,国际站目录里没有 deepseek-flash),
|
|
181
|
+
* 老网关没标时退回兜底 id(在目录里的话),再退回目录第一个。
|
|
182
|
+
*/
|
|
183
|
+
export function defaultModelId() {
|
|
184
|
+
return MODELS.find((m) => m.isDefault)?.id
|
|
185
|
+
?? (MODELS.some((m) => m.id === FALLBACK_DEFAULT_MODEL_ID) ? FALLBACK_DEFAULT_MODEL_ID : MODELS[0]?.id)
|
|
186
|
+
?? FALLBACK_DEFAULT_MODEL_ID;
|
|
187
|
+
}
|
|
173
188
|
export function resolveModel(nameOrAlias) {
|
|
174
189
|
const q = nameOrAlias.trim().toLowerCase();
|
|
175
190
|
return MODELS.find((m) => m.id.toLowerCase() === q || m.aliases.includes(q));
|
|
@@ -312,7 +327,7 @@ export function resolvePreferredModel(cfg) {
|
|
|
312
327
|
if (refValid(ref))
|
|
313
328
|
return ref;
|
|
314
329
|
}
|
|
315
|
-
return { provider: PROVIDER_ID, id:
|
|
330
|
+
return { provider: PROVIDER_ID, id: defaultModelId() };
|
|
316
331
|
}
|
|
317
332
|
export function loadConfig() {
|
|
318
333
|
return configFromFile((readJsonFile(configFile) ?? {}));
|
|
@@ -359,6 +374,7 @@ export function configFromFile(file, env = process.env, currentCnApiBase = SITES
|
|
|
359
374
|
export function activeSite() {
|
|
360
375
|
return siteOf(loadConfig());
|
|
361
376
|
}
|
|
377
|
+
setLangResolver(() => (activeSite().id === "intl" ? "en" : "zh"));
|
|
362
378
|
/**
|
|
363
379
|
* 凭据文件落盘:先以 0600 写临时文件再 rename 覆盖,文件从不以默认权限
|
|
364
380
|
* (umask 下常是 0644)存在过哪怕一瞬;rename 在 POSIX 上原子替换。
|
|
@@ -419,6 +435,7 @@ export function stripEnvOverrides(cfg, file, env = process.env) {
|
|
|
419
435
|
export function saveConfig(cfg) {
|
|
420
436
|
const file = (readJsonFile(configFile) ?? {});
|
|
421
437
|
writePrivateFile(configFile, JSON.stringify(stripEnvOverrides(cfg, file), null, 2) + "\n");
|
|
438
|
+
resetLang();
|
|
422
439
|
}
|
|
423
440
|
/** Persist the user's preferred model to both stores. */
|
|
424
441
|
export function persistPreferredModel(cfg, provider, modelId) {
|
package/dist/context-usage.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CUSTOM_ENDPOINTS, DEFAULT_COMPACTION_BUDGET_TOKENS, MODELS, PROVIDER_ID, } from "./config.js";
|
|
2
|
+
import { tr } from "./i18n.js";
|
|
2
3
|
/** 1_048_576 → "1M",200_000 → "200K",184_000 → "184K",12_345 → "12.3K"。 */
|
|
3
4
|
export function formatTokens(n) {
|
|
4
5
|
if (n >= 1_000_000) {
|
|
@@ -23,25 +24,27 @@ export function contextEntryLines(input) {
|
|
|
23
24
|
const def = findModelDef(input.model);
|
|
24
25
|
const usage = input.usage;
|
|
25
26
|
if (!usage || usage.contextWindow <= 0) {
|
|
26
|
-
return ["当前模型没有上下文窗口信息,无法统计。"];
|
|
27
|
+
return [tr("当前模型没有上下文窗口信息,无法统计。", "The current model has no context window info, so usage can't be measured.")];
|
|
27
28
|
}
|
|
28
29
|
const budget = usage.contextWindow;
|
|
29
30
|
const modelWindow = def?.contextWindow ?? 0;
|
|
30
31
|
const lines = [];
|
|
31
32
|
if (usage.tokens === null || usage.percent === null) {
|
|
32
|
-
lines.push(`当前上下文:未知(刚压缩过,等下一次回复后才能统计) · 压缩预算 ${formatTokens(budget)}`);
|
|
33
|
+
lines.push(tr(`当前上下文:未知(刚压缩过,等下一次回复后才能统计) · 压缩预算 ${formatTokens(budget)}`, `Current context: unknown (just compacted; available after the next reply) · compaction budget ${formatTokens(budget)}`));
|
|
33
34
|
}
|
|
34
35
|
else {
|
|
35
|
-
lines.push(`当前上下文:约 ${formatTokens(usage.tokens)} tokens,占压缩预算 ${formatTokens(budget)} 的 ${usage.percent.toFixed(1)}
|
|
36
|
+
lines.push(tr(`当前上下文:约 ${formatTokens(usage.tokens)} tokens,占压缩预算 ${formatTokens(budget)} 的 ${usage.percent.toFixed(1)}%`, `Current context: ~${formatTokens(usage.tokens)} tokens, ${usage.percent.toFixed(1)}% of the ${formatTokens(budget)} compaction budget`));
|
|
36
37
|
}
|
|
37
38
|
const trigger = Math.max(0, budget - input.reserveTokens);
|
|
38
39
|
const budgetOrigin = input.budgetSetting !== undefined
|
|
39
|
-
? "来自你的设置"
|
|
40
|
+
? tr("来自你的设置", "from your settings")
|
|
40
41
|
: def?.compactAtTokens
|
|
41
|
-
? "由 u1s1 按模型下发"
|
|
42
|
-
: `默认 ${formatTokens(DEFAULT_COMPACTION_BUDGET_TOKENS)}
|
|
43
|
-
const windowNote = modelWindow > budget
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
? tr("由 u1s1 按模型下发", "set by u1s1 for this model")
|
|
43
|
+
: tr(`默认 ${formatTokens(DEFAULT_COMPACTION_BUDGET_TOKENS)}`, `default ${formatTokens(DEFAULT_COMPACTION_BUDGET_TOKENS)}`);
|
|
44
|
+
const windowNote = modelWindow > budget
|
|
45
|
+
? tr(`(模型窗口 ${formatTokens(modelWindow)},预算${budgetOrigin})`, `(model window ${formatTokens(modelWindow)}; budget ${budgetOrigin})`)
|
|
46
|
+
: `(${budgetOrigin})`;
|
|
47
|
+
lines.push(tr(`超过约 ${formatTokens(trigger)} tokens 会自动压缩 ${windowNote};/compact 可随时手动压缩`, `Auto-compacts above ~${formatTokens(trigger)} tokens ${windowNote}; run /compact to compact any time`));
|
|
48
|
+
lines.push(tr("改预算:~/.u1s1/agent/settings.json 里 compaction.maxContextTokens(0 = 不限,按模型窗口);只影响自动压缩时机,不改模型能力", "To change the budget, set compaction.maxContextTokens in ~/.u1s1/agent/settings.json (0 = no limit, use the model window). It only affects when auto-compaction kicks in, not what the model can do."));
|
|
46
49
|
return lines;
|
|
47
50
|
}
|
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 记「最近一次请求编号」用)。 */
|