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/bench.js
CHANGED
|
@@ -8,6 +8,7 @@ import { authorizedFetch, hasDeviceCredential } from "./device-auth.js";
|
|
|
8
8
|
import { costStr, errorRate, formatDuration, generateReport, renderTable, scoreBar, scoreRate, sortByScore, summarizeByModel, tokPerSec, } from "./bench-report.js";
|
|
9
9
|
import { scoreResponse } from "./bench-scoring.js";
|
|
10
10
|
import { DEFAULT_MAX_SCORE, } from "./bench-types.js";
|
|
11
|
+
import { isEnglish, tr } from "./i18n.js";
|
|
11
12
|
// ─── 工具 ───
|
|
12
13
|
const BENCH_DIR = join(homedir(), ".u1s1", "bench");
|
|
13
14
|
/** compare 子命令里单条回答的截断长度 */
|
|
@@ -43,7 +44,7 @@ function loadSuite(name) {
|
|
|
43
44
|
if (found)
|
|
44
45
|
return found;
|
|
45
46
|
}
|
|
46
|
-
throw new Error(`找不到 bench suite「${name}
|
|
47
|
+
throw new Error(tr(`找不到 bench suite「${name}」`, `Bench suite "${name}" not found`));
|
|
47
48
|
}
|
|
48
49
|
/** 列出可用的 suite */
|
|
49
50
|
function suiteSummaries(filePath, builtin) {
|
|
@@ -126,7 +127,7 @@ async function callModel(input) {
|
|
|
126
127
|
: await fetch(`${baseUrl}/chat/completions`, request);
|
|
127
128
|
const latencyMs = Math.round(performance.now() - start);
|
|
128
129
|
if (!res.ok) {
|
|
129
|
-
const errBody = await readResponseTextCapped(res, 64 * 1024).catch(() => "未知错误");
|
|
130
|
+
const errBody = await readResponseTextCapped(res, 64 * 1024).catch(() => tr("未知错误", "unknown error"));
|
|
130
131
|
return { response: "", latencyMs, tokensIn: 0, tokensOut: 0, error: `HTTP ${res.status}: ${errBody.slice(0, 200)}` };
|
|
131
132
|
}
|
|
132
133
|
return { ...parseChatCompletion(await readJsonResponseCapped(res)), latencyMs };
|
|
@@ -160,12 +161,12 @@ function saveRun(run) {
|
|
|
160
161
|
mkdirSync(BENCH_DIR, { recursive: true });
|
|
161
162
|
const file = join(BENCH_DIR, `${run.id}.json`);
|
|
162
163
|
writeFileSync(file, JSON.stringify(run, null, 2) + "\n");
|
|
163
|
-
console.log(` 结果已保存:${file}`);
|
|
164
|
+
console.log(tr(` 结果已保存:${file}`, ` Results saved: ${file}`));
|
|
164
165
|
}
|
|
165
166
|
function loadRun(id) {
|
|
166
167
|
const file = join(BENCH_DIR, `${id}.json`);
|
|
167
168
|
if (!existsSync(file))
|
|
168
|
-
throw new Error(`找不到运行记录「${id}
|
|
169
|
+
throw new Error(tr(`找不到运行记录「${id}」`, `Run "${id}" not found`));
|
|
169
170
|
return JSON.parse(readFileSync(file, "utf8"));
|
|
170
171
|
}
|
|
171
172
|
function listRuns() {
|
|
@@ -190,30 +191,30 @@ function failWithError(e) {
|
|
|
190
191
|
}
|
|
191
192
|
function printHelp() {
|
|
192
193
|
console.log("");
|
|
193
|
-
console.log(" u1s1 bench — 模型质量基准测试(带自动评分)");
|
|
194
|
+
console.log(tr(" u1s1 bench — 模型质量基准测试(带自动评分)", " u1s1 bench — model quality benchmark (auto-scored)"));
|
|
194
195
|
console.log("");
|
|
195
|
-
console.log(" 用法:");
|
|
196
|
-
console.log(" u1s1 bench list 列出可用的测试套件");
|
|
197
|
-
console.log(" u1s1 bench run <suite> [models..] 跑基准测试(--delay 秒数:题目间隔,限流渠道用)");
|
|
198
|
-
console.log(" u1s1 bench report <run-id> 查看报告");
|
|
199
|
-
console.log(" u1s1 bench compare <run-id1> <run-id2> 对比两次运行");
|
|
196
|
+
console.log(tr(" 用法:", " Usage:"));
|
|
197
|
+
console.log(tr(" u1s1 bench list 列出可用的测试套件", " u1s1 bench list List available suites"));
|
|
198
|
+
console.log(tr(" u1s1 bench run <suite> [models..] 跑基准测试(--delay 秒数:题目间隔,限流渠道用)", " u1s1 bench run <suite> [models..] Run a benchmark (--delay <seconds> between questions, for rate-limited providers)"));
|
|
199
|
+
console.log(tr(" u1s1 bench report <run-id> 查看报告", " u1s1 bench report <run-id> Show a report"));
|
|
200
|
+
console.log(tr(" u1s1 bench compare <run-id1> <run-id2> 对比两次运行", " u1s1 bench compare <run-id1> <run-id2> Compare two runs"));
|
|
200
201
|
console.log("");
|
|
201
|
-
console.log(" 示例:");
|
|
202
|
-
console.log(" u1s1 bench run quick 用当前默认模型跑快速测试");
|
|
203
|
-
console.log(" u1s1 bench run full deepseek pro 用多个模型跑全面测试");
|
|
204
|
-
console.log(" u1s1 bench report latest 看最近一次报告");
|
|
202
|
+
console.log(tr(" 示例:", " Examples:"));
|
|
203
|
+
console.log(tr(" u1s1 bench run quick 用当前默认模型跑快速测试", " u1s1 bench run quick Quick suite with your default model"));
|
|
204
|
+
console.log(tr(" u1s1 bench run full deepseek pro 用多个模型跑全面测试", " u1s1 bench run full deepseek pro Full suite across several models"));
|
|
205
|
+
console.log(tr(" u1s1 bench report latest 看最近一次报告", " u1s1 bench report latest Show the latest report"));
|
|
205
206
|
console.log("");
|
|
206
207
|
}
|
|
207
208
|
function listSuitesCommand() {
|
|
208
209
|
const suites = listSuites();
|
|
209
210
|
console.log("");
|
|
210
|
-
console.log(" Bench 套件:");
|
|
211
|
+
console.log(tr(" Bench 套件:", " Bench suites:"));
|
|
211
212
|
for (const s of suites) {
|
|
212
|
-
const tag = s.builtin ? "(内置)" : "(自定义)";
|
|
213
|
+
const tag = s.builtin ? tr("(内置)", "(built-in)") : tr("(自定义)", "(custom)");
|
|
213
214
|
console.log(` ${s.name.padEnd(12)} ${s.description} ${tag}`);
|
|
214
215
|
}
|
|
215
216
|
console.log("");
|
|
216
|
-
console.log(" 自定义套件放 ~/.u1s1/bench/<name>.json,格式同内置 suite.json");
|
|
217
|
+
console.log(tr(" 自定义套件放 ~/.u1s1/bench/<name>.json,格式同内置 suite.json", " Put custom suites in ~/.u1s1/bench/<name>.json (same format as the built-in suite.json)"));
|
|
217
218
|
console.log("");
|
|
218
219
|
}
|
|
219
220
|
/** 把模型名解析成走 u1s1 默认端点的调用目标 */
|
|
@@ -329,10 +330,10 @@ async function runBenchQuestion(input) {
|
|
|
329
330
|
async function runBench(args) {
|
|
330
331
|
const { suiteName, modelQueries, delayMs } = parseBenchRunArgs(args);
|
|
331
332
|
if (delayMs > 0)
|
|
332
|
-
console.log(` ⏳ 题目间隔:${(delayMs / 1000).toFixed(0)}s(限流渠道用)`);
|
|
333
|
+
console.log(tr(` ⏳ 题目间隔:${(delayMs / 1000).toFixed(0)}s(限流渠道用)`, ` ⏳ Delay between questions: ${(delayMs / 1000).toFixed(0)}s (for rate-limited providers)`));
|
|
333
334
|
const cfg = loadConfig();
|
|
334
335
|
if (!hasDeviceCredential(cfg)) {
|
|
335
|
-
console.error(" 还没有登录,先 u1s1 login");
|
|
336
|
+
console.error(tr(" 还没有登录,先 u1s1 login", " Not signed in. Run u1s1 login first."));
|
|
336
337
|
process.exit(1);
|
|
337
338
|
}
|
|
338
339
|
await loadCustomEndpoints(cfg);
|
|
@@ -343,8 +344,8 @@ async function runBench(args) {
|
|
|
343
344
|
console.log("");
|
|
344
345
|
console.log(` 📊 Bench: ${suite.name}`);
|
|
345
346
|
console.log(` ${suite.description}`);
|
|
346
|
-
console.log(` 题目数: ${suite.questions.length}`);
|
|
347
|
-
console.log(` 模型: ${targets.map((t) => t.label).join(", ")}`);
|
|
347
|
+
console.log(tr(` 题目数: ${suite.questions.length}`, ` Questions: ${suite.questions.length}`));
|
|
348
|
+
console.log(tr(` 模型: ${targets.map((t) => t.label).join(", ")}`, ` Models: ${targets.map((t) => t.label).join(", ")}`));
|
|
348
349
|
console.log("");
|
|
349
350
|
const results = [];
|
|
350
351
|
const total = suite.questions.length * targets.length;
|
|
@@ -368,14 +369,16 @@ async function runBench(args) {
|
|
|
368
369
|
results,
|
|
369
370
|
});
|
|
370
371
|
printConsoleSummary(results);
|
|
371
|
-
console.log(` 查看完整报告: u1s1 bench report ${runId}`);
|
|
372
|
+
console.log(tr(` 查看完整报告: u1s1 bench report ${runId}`, ` Full report: u1s1 bench report ${runId}`));
|
|
372
373
|
console.log("");
|
|
373
374
|
}
|
|
374
375
|
/** 控制台里的简易评分汇总表 */
|
|
375
376
|
function printConsoleSummary(results) {
|
|
376
377
|
console.log("");
|
|
377
|
-
console.log(" 📋 评分汇总:");
|
|
378
|
-
const header =
|
|
378
|
+
console.log(tr(" 📋 评分汇总:", " 📋 Score summary:"));
|
|
379
|
+
const header = isEnglish()
|
|
380
|
+
? ["Model", "Score", "Rate", "Avg latency", "Speed", "Cost", "Status"]
|
|
381
|
+
: ["模型", "总分", "得分率", "平均延迟", "速度", "花费", "状态"];
|
|
379
382
|
const rows = sortByScore([...summarizeByModel(results, (r) => r.modelId)]).map(([model, s]) => [
|
|
380
383
|
model,
|
|
381
384
|
`${s.totalScore}/${s.totalMax}`,
|
|
@@ -383,7 +386,7 @@ function printConsoleSummary(results) {
|
|
|
383
386
|
formatDuration(s.totalLatency / s.total),
|
|
384
387
|
tokPerSec(s),
|
|
385
388
|
costStr(s.totalCost),
|
|
386
|
-
s.errors > 0 ? `${errorRate(s)}
|
|
389
|
+
s.errors > 0 ? tr(`${errorRate(s)}%失败`, `${errorRate(s)}% failed`) : tr("✅全部通过", "✅ all passed"),
|
|
387
390
|
]);
|
|
388
391
|
console.log(" " + renderTable(header, rows));
|
|
389
392
|
console.log("");
|
|
@@ -395,7 +398,7 @@ function showReport(args) {
|
|
|
395
398
|
try {
|
|
396
399
|
const id = runIdArg === "latest" ? findLatestRun() : runIdArg;
|
|
397
400
|
if (!id)
|
|
398
|
-
throw new Error("还没有运行记录");
|
|
401
|
+
throw new Error(tr("还没有运行记录", "No runs yet"));
|
|
399
402
|
run = loadRun(id);
|
|
400
403
|
}
|
|
401
404
|
catch (e) {
|
|
@@ -405,7 +408,7 @@ function showReport(args) {
|
|
|
405
408
|
const reportFile = join(BENCH_DIR, `${run.id}.md`);
|
|
406
409
|
writeFileSync(reportFile, report);
|
|
407
410
|
console.log(report);
|
|
408
|
-
console.log(` 报告已保存: ${reportFile}`);
|
|
411
|
+
console.log(tr(` 报告已保存: ${reportFile}`, ` Report saved: ${reportFile}`));
|
|
409
412
|
}
|
|
410
413
|
/** 两次运行共同拥有的题目 id */
|
|
411
414
|
function commonQuestionIds(a, b) {
|
|
@@ -428,7 +431,7 @@ function compareRuns(runId1, runId2) {
|
|
|
428
431
|
failWithError(e);
|
|
429
432
|
}
|
|
430
433
|
console.log("");
|
|
431
|
-
console.log(` 对比 ${run1.suiteName}(${run1.timestamp}) vs ${run2.suiteName}(${run2.timestamp})`);
|
|
434
|
+
console.log(tr(` 对比 ${run1.suiteName}(${run1.timestamp}) vs ${run2.suiteName}(${run2.timestamp})`, ` Comparing ${run1.suiteName} (${run1.timestamp}) vs ${run2.suiteName} (${run2.timestamp})`));
|
|
432
435
|
console.log("");
|
|
433
436
|
for (const qId of commonQuestionIds(run1, run2)) {
|
|
434
437
|
console.log(` ── ${qId} ──`);
|
|
@@ -452,13 +455,13 @@ export async function benchCommand(args) {
|
|
|
452
455
|
return showReport(args.slice(1));
|
|
453
456
|
if (sub === "compare") {
|
|
454
457
|
if (!args[1] || !args[2]) {
|
|
455
|
-
console.log(" 用法: u1s1 bench compare <run-id1> <run-id2>");
|
|
458
|
+
console.log(tr(" 用法: u1s1 bench compare <run-id1> <run-id2>", " Usage: u1s1 bench compare <run-id1> <run-id2>"));
|
|
456
459
|
return;
|
|
457
460
|
}
|
|
458
461
|
return compareRuns(args[1], args[2]);
|
|
459
462
|
}
|
|
460
|
-
console.error(` 未知子命令: ${sub}`);
|
|
461
|
-
console.log(" u1s1 bench help 查看用法");
|
|
463
|
+
console.error(tr(` 未知子命令: ${sub}`, ` Unknown subcommand: ${sub}`));
|
|
464
|
+
console.log(tr(" u1s1 bench help 查看用法", " Run u1s1 bench help for usage"));
|
|
462
465
|
process.exit(1);
|
|
463
466
|
}
|
|
464
467
|
function findLatestRun() {
|
package/dist/brand.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
export declare const BRAND_NAME = "u1s1";
|
|
3
3
|
export declare const BRAND_CN = "\u6709\u4E00\u8BF4\u4E00";
|
|
4
|
-
|
|
5
|
-
export declare
|
|
4
|
+
/** 中文品牌名只在中国站界面出现;国际站只叫 u1s1。 */
|
|
5
|
+
export declare function brandTagline(): string;
|
|
6
|
+
/**
|
|
7
|
+
* 中国站仪表盘。只为 embed 导出保留(Desktop App 现在只有中国站包,P3 出 intl 包时
|
|
8
|
+
* 改用自带的站点配置);CLI 内部一律按当前站点取 siteLinks(activeSite()).dashboard。
|
|
9
|
+
*/
|
|
10
|
+
export declare const DASHBOARD_URL: string;
|
|
6
11
|
export declare function formatHomePath(path: string): string;
|
|
7
12
|
/**
|
|
8
13
|
* 启动目录不对时的一句提醒(工单 b70c901e:用户在 C:\\Users\\1\\.u1s1 里跑 u1s1)。
|
package/dist/brand.js
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
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";
|
|
5
|
+
import { siteLinks } from "./site.js";
|
|
3
6
|
export const BRAND_NAME = "u1s1";
|
|
4
7
|
export const BRAND_CN = "有一说一";
|
|
5
|
-
|
|
6
|
-
export
|
|
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
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 中国站仪表盘。只为 embed 导出保留(Desktop App 现在只有中国站包,P3 出 intl 包时
|
|
18
|
+
* 改用自带的站点配置);CLI 内部一律按当前站点取 siteLinks(activeSite()).dashboard。
|
|
19
|
+
*/
|
|
20
|
+
export const DASHBOARD_URL = siteLinks("cn").dashboard;
|
|
7
21
|
export function formatHomePath(path) {
|
|
8
22
|
const home = homedir();
|
|
9
23
|
const resolved = resolve(path);
|
|
@@ -25,10 +39,10 @@ export function cwdAdvice(cwd, dirs = {}) {
|
|
|
25
39
|
const here = resolve(cwd);
|
|
26
40
|
const within = (dir) => here === dir || here.startsWith(dir + sep);
|
|
27
41
|
if (within(configDir)) {
|
|
28
|
-
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.");
|
|
29
43
|
}
|
|
30
44
|
if (here === home) {
|
|
31
|
-
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.");
|
|
32
46
|
}
|
|
33
47
|
return undefined;
|
|
34
48
|
}
|
|
@@ -67,13 +81,25 @@ function announcementLine(theme, a) {
|
|
|
67
81
|
}
|
|
68
82
|
/** 空目录首跑引导:不让新手面对空白输入框想 prompt,给几句能照抄的开场白。 */
|
|
69
83
|
function starterLines(theme) {
|
|
70
|
-
|
|
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 = [
|
|
71
93
|
` ${theme.fg("muted", "这个文件夹还是空的?把想做的直接说出来就行,比如:")}`,
|
|
72
94
|
` ${theme.fg("text", "「做一个自我介绍网页,做完帮我发布出去」")}`,
|
|
73
95
|
` ${theme.fg("text", "「做一个给朋友的生日祝福页面,要有点小动画」")}`,
|
|
74
96
|
` ${theme.fg("text", "「写一个把文件夹里照片按日期重命名的小工具」")}`,
|
|
75
|
-
` ${theme.fg("muted", "网页做好后,输入 /exit 回到终端,运行 u1s1 deploy --public 一键上线,拿到可分享的链接发给朋友")}`,
|
|
76
97
|
];
|
|
98
|
+
// 国际站没有网页托管(u1s1 deploy),不提发布
|
|
99
|
+
if (activeSite().id !== "intl") {
|
|
100
|
+
lines.push(` ${theme.fg("muted", "网页做好后,输入 /exit 回到终端,运行 u1s1 deploy --public 一键上线,拿到可分享的链接发给朋友")}`);
|
|
101
|
+
}
|
|
102
|
+
return lines;
|
|
77
103
|
}
|
|
78
104
|
/**
|
|
79
105
|
* Startup hero, responsive to terminal width:
|
|
@@ -82,13 +108,18 @@ function starterLines(theme) {
|
|
|
82
108
|
export function renderBrandHeader(theme, input) {
|
|
83
109
|
const name = theme.bold(theme.fg("text", `${BRAND_NAME} v${input.version}`)) +
|
|
84
110
|
(input.notice ? ` ${theme.fg("accent", input.notice)}` : "");
|
|
85
|
-
const brand = theme.fg("muted",
|
|
86
|
-
const dir = theme.fg("dim", `cwd: ${formatHomePath(input.cwd)}${input.account ? ` · 账号 ${input.account}` : ""}`);
|
|
87
|
-
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"));
|
|
88
114
|
const starter = input.starterTips ? starterLines(theme) : [];
|
|
89
115
|
const warning = input.cwdWarning ? [` ${theme.fg("warning", "⚠")} ${theme.fg("warning", input.cwdWarning)}`] : [];
|
|
90
116
|
if (input.width < ART_WIDTH + 4) {
|
|
91
|
-
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
|
+
];
|
|
92
123
|
if (input.announcement)
|
|
93
124
|
lines.push(` ${announcementLine(theme, input.announcement)}`);
|
|
94
125
|
if (warning.length)
|
|
@@ -98,8 +129,8 @@ export function renderBrandHeader(theme, input) {
|
|
|
98
129
|
return [...lines, ""];
|
|
99
130
|
}
|
|
100
131
|
const art = HERO_ART.map((line) => ` ${paintArt(theme, line)}`);
|
|
101
|
-
// widest info row (hints) needs 46 cols beside the 28-col wordmark
|
|
102
|
-
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)) {
|
|
103
134
|
const lines = ["", ...art, "", ` ${name} ${brand}`, ` ${dir}`, ` ${hints}`];
|
|
104
135
|
if (input.announcement)
|
|
105
136
|
lines.push(` ${announcementLine(theme, input.announcement)}`);
|
|
@@ -129,6 +160,6 @@ export function printConsoleBanner(version) {
|
|
|
129
160
|
for (const line of HERO_ART)
|
|
130
161
|
console.log(` ${line}`);
|
|
131
162
|
console.log("");
|
|
132
|
-
console.log(` ${BRAND_NAME} v${version} — ${BRAND_CN},${
|
|
163
|
+
console.log(tr(` ${BRAND_NAME} v${version} — ${BRAND_CN},${brandTagline()}`, ` ${BRAND_NAME} v${version} — ${brandTagline()}`));
|
|
133
164
|
console.log("");
|
|
134
165
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type webcrypto } from "node:crypto";
|
|
2
|
+
import { type Site } from "./site.js";
|
|
2
3
|
export declare const VERSION: string;
|
|
3
4
|
/**
|
|
4
5
|
* 便携包安装(install.sh / install.ps1):包根旁边带自己的 node 运行时,
|
|
@@ -6,7 +7,8 @@ export declare const VERSION: string;
|
|
|
6
7
|
* 下,按父目录名先排除,避免撞上恰好叫 node 的目录误判。
|
|
7
8
|
*/
|
|
8
9
|
export declare function isPortableInstall(): boolean;
|
|
9
|
-
|
|
10
|
+
/** 默认连中国站;国际站由安装脚本写入 baseUrl 或 `u1s1 login --site global` 切换。 */
|
|
11
|
+
export declare const DEFAULT_BASE_URL: string;
|
|
10
12
|
export declare const PROVIDER_ID = "u1s1";
|
|
11
13
|
export declare const THINKING_LEVELS: readonly ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
12
14
|
export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
@@ -37,6 +39,8 @@ export interface ApiModel {
|
|
|
37
39
|
max_tokens: number;
|
|
38
40
|
/** Older gateways omit this field; missing means unknown coverage. */
|
|
39
41
|
free_package_eligible?: boolean;
|
|
42
|
+
/** 本站默认模型(网关按站点定:中国站 deepseek-flash,国际站另配);老网关也会发。 */
|
|
43
|
+
is_default?: boolean;
|
|
40
44
|
/**
|
|
41
45
|
* 网关建议的自动压缩预算(token);缺失/null 时 CLI 用内置默认 200K。
|
|
42
46
|
* 用户设置 compaction.maxContextTokens 优先级更高(见 effectiveContextWindow)。
|
|
@@ -79,6 +83,8 @@ export interface ModelDef {
|
|
|
79
83
|
cost: ModelCost | null;
|
|
80
84
|
/** 网关下发的免费用量包覆盖标记;老网关缺失时为 undefined(不派生 note)。 */
|
|
81
85
|
freeEligible?: boolean;
|
|
86
|
+
/** 网关标的本站默认模型(is_default)。 */
|
|
87
|
+
isDefault?: boolean;
|
|
82
88
|
/** 网关按模型下发的自动压缩预算(token);缺失时用 DEFAULT_COMPACTION_BUDGET_TOKENS。 */
|
|
83
89
|
compactAtTokens?: number;
|
|
84
90
|
note: string;
|
|
@@ -99,7 +105,13 @@ export declare function effectiveContextWindow(m: Pick<ModelDef, "contextWindow"
|
|
|
99
105
|
export declare const MODELS: ModelDef[];
|
|
100
106
|
/** Replace MODELS with a fresh list fetched from server (e.g. at startup). */
|
|
101
107
|
export declare function setModelsFromApi(apiModels: ModelDef[]): void;
|
|
102
|
-
|
|
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;
|
|
103
115
|
export declare function resolveModel(nameOrAlias: string): ModelDef | undefined;
|
|
104
116
|
/** provider 注册键就是云端的端点 id(ep+uuid);models.json 清理旧条目按此形状识别。 */
|
|
105
117
|
export declare const ENDPOINT_ID_RE: RegExp;
|
|
@@ -206,6 +218,20 @@ export declare function writeAgentDefaultModel(provider: string, modelId: string
|
|
|
206
218
|
*/
|
|
207
219
|
export declare function resolvePreferredModel(cfg: Pick<CliConfig, "model" | "modelProvider">): ModelRef;
|
|
208
220
|
export declare function loadConfig(): CliConfig;
|
|
221
|
+
/** 只看配置文件、不叠加环境变量覆盖的配置(判断「文件里存的是哪个站」用)。 */
|
|
222
|
+
export declare function loadFileConfig(): CliConfig;
|
|
223
|
+
/**
|
|
224
|
+
* 配置文件内容 + 环境变量 → 生效配置。环境变量优先于文件;文件里的历代中国站
|
|
225
|
+
* API 根换成当前中国站 apiBase(migrateStoredBaseUrl),下次 saveConfig 时落盘。
|
|
226
|
+
* env / currentCnApiBase 可注入,供测试模拟 P1 换主域后的迁移。
|
|
227
|
+
*/
|
|
228
|
+
export declare function configFromFile(file: Partial<CliConfig>, env?: NodeJS.ProcessEnv, currentCnApiBase?: string): CliConfig;
|
|
229
|
+
/**
|
|
230
|
+
* 当前生效站点(按 loadConfig().baseUrl 判定,含 U1S1_BASE_URL 覆盖)。手里没有 cfg 的
|
|
231
|
+
* 文案/链接(模块级提示、生成的扩展、错误人话化)用它;有 cfg 的地方直接 siteOf(cfg)。
|
|
232
|
+
* 每次现读配置不缓存:`u1s1 login --site` 切站后同进程里的链接立即跟着变。
|
|
233
|
+
*/
|
|
234
|
+
export declare function activeSite(): Site;
|
|
209
235
|
/**
|
|
210
236
|
* 凭据文件落盘:先以 0600 写临时文件再 rename 覆盖,文件从不以默认权限
|
|
211
237
|
* (umask 下常是 0644)存在过哪怕一瞬;rename 在 POSIX 上原子替换。
|
package/dist/config.js
CHANGED
|
@@ -4,6 +4,8 @@ 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";
|
|
8
|
+
import { migrateStoredBaseUrl, SITES, siteOf } from "./site.js";
|
|
7
9
|
const require = createRequire(import.meta.url);
|
|
8
10
|
export const VERSION = require("../package.json").version;
|
|
9
11
|
/**
|
|
@@ -20,7 +22,8 @@ export function isPortableInstall() {
|
|
|
20
22
|
: join(pkgRoot, "..", "node", "bin", "node");
|
|
21
23
|
return existsSync(portableNode);
|
|
22
24
|
}
|
|
23
|
-
|
|
25
|
+
/** 默认连中国站;国际站由安装脚本写入 baseUrl 或 `u1s1 login --site global` 切换。 */
|
|
26
|
+
export const DEFAULT_BASE_URL = SITES.cn.apiBase;
|
|
24
27
|
export const PROVIDER_ID = "u1s1";
|
|
25
28
|
/** Make a short alias from a model id by stripping common prefixes. */
|
|
26
29
|
function defaultAliases(id) {
|
|
@@ -78,6 +81,7 @@ export function apiModelToDef(m) {
|
|
|
78
81
|
cacheWrite: 0,
|
|
79
82
|
},
|
|
80
83
|
freeEligible: typeof m.free_package_eligible === "boolean" ? m.free_package_eligible : undefined,
|
|
84
|
+
isDefault: m.is_default === true ? true : undefined,
|
|
81
85
|
compactAtTokens: positiveInteger(m.compact_at_tokens),
|
|
82
86
|
note: "",
|
|
83
87
|
};
|
|
@@ -124,7 +128,8 @@ export const MODELS = [
|
|
|
124
128
|
contextWindow: 1_048_576,
|
|
125
129
|
maxTokens: 384_000,
|
|
126
130
|
cost: null,
|
|
127
|
-
|
|
131
|
+
// 离线兜底列表的说明按语言现取(模块加载时还不知道站点)
|
|
132
|
+
get note() { return tr("默认 · 免费用量包额度内免费,日常写代码首选,支持看图", "Default · everyday coding, supports images"); },
|
|
128
133
|
},
|
|
129
134
|
{
|
|
130
135
|
id: "x-ai/grok-4.6",
|
|
@@ -135,7 +140,7 @@ export const MODELS = [
|
|
|
135
140
|
contextWindow: 500_000,
|
|
136
141
|
maxTokens: 65_536,
|
|
137
142
|
cost: null,
|
|
138
|
-
note
|
|
143
|
+
get note() { return tr("更强 · 但烧额度快约 20 倍,难题再用", "Stronger · about 20× the cost, for hard problems"); },
|
|
139
144
|
},
|
|
140
145
|
];
|
|
141
146
|
/**
|
|
@@ -144,21 +149,23 @@ export const MODELS = [
|
|
|
144
149
|
* 让人切模型前就知道「这个不走免费包 / 大概贵多少」。
|
|
145
150
|
*/
|
|
146
151
|
function deriveModelNote(m, base) {
|
|
152
|
+
// 国际站没有免费包:不提免费包,只给价格倍数
|
|
147
153
|
if (m.freeEligible === true)
|
|
148
|
-
return "免费用量包可抵扣";
|
|
154
|
+
return tr("免费用量包可抵扣", "");
|
|
149
155
|
if (m.freeEligible === false) {
|
|
150
156
|
const blended = m.cost ? (m.cost.input + m.cost.output) / 2 : 0;
|
|
151
157
|
const baseBlended = base?.cost ? (base.cost.input + base.cost.output) / 2 : 0;
|
|
152
158
|
const mult = baseBlended > 0 ? Math.round(blended / baseBlended) : 0;
|
|
153
159
|
return mult >= 2
|
|
154
|
-
? `不走免费包 · 费用约为默认模型 ${mult}
|
|
155
|
-
: "不走免费包,用余额或全模型包";
|
|
160
|
+
? tr(`不走免费包 · 费用约为默认模型 ${mult} 倍`, `about ${mult}× the default model's price`)
|
|
161
|
+
: tr("不走免费包,用余额或全模型包", "");
|
|
156
162
|
}
|
|
157
163
|
return "";
|
|
158
164
|
}
|
|
159
165
|
/** Replace MODELS with a fresh list fetched from server (e.g. at startup). */
|
|
160
166
|
export function setModelsFromApi(apiModels) {
|
|
161
|
-
const base = apiModels.find((m) => m.
|
|
167
|
+
const base = apiModels.find((m) => m.isDefault)
|
|
168
|
+
?? apiModels.find((m) => m.id === FALLBACK_DEFAULT_MODEL_ID)
|
|
162
169
|
?? apiModels.find((m) => m.freeEligible === true);
|
|
163
170
|
for (const m of apiModels) {
|
|
164
171
|
if (!m.note)
|
|
@@ -167,7 +174,17 @@ export function setModelsFromApi(apiModels) {
|
|
|
167
174
|
MODELS.length = 0;
|
|
168
175
|
MODELS.push(...apiModels);
|
|
169
176
|
}
|
|
170
|
-
|
|
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
|
+
}
|
|
171
188
|
export function resolveModel(nameOrAlias) {
|
|
172
189
|
const q = nameOrAlias.trim().toLowerCase();
|
|
173
190
|
return MODELS.find((m) => m.id.toLowerCase() === q || m.aliases.includes(q));
|
|
@@ -310,12 +327,23 @@ export function resolvePreferredModel(cfg) {
|
|
|
310
327
|
if (refValid(ref))
|
|
311
328
|
return ref;
|
|
312
329
|
}
|
|
313
|
-
return { provider: PROVIDER_ID, id:
|
|
330
|
+
return { provider: PROVIDER_ID, id: defaultModelId() };
|
|
314
331
|
}
|
|
315
332
|
export function loadConfig() {
|
|
316
|
-
|
|
333
|
+
return configFromFile((readJsonFile(configFile) ?? {}));
|
|
334
|
+
}
|
|
335
|
+
/** 只看配置文件、不叠加环境变量覆盖的配置(判断「文件里存的是哪个站」用)。 */
|
|
336
|
+
export function loadFileConfig() {
|
|
337
|
+
return configFromFile((readJsonFile(configFile) ?? {}), {});
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* 配置文件内容 + 环境变量 → 生效配置。环境变量优先于文件;文件里的历代中国站
|
|
341
|
+
* API 根换成当前中国站 apiBase(migrateStoredBaseUrl),下次 saveConfig 时落盘。
|
|
342
|
+
* env / currentCnApiBase 可注入,供测试模拟 P1 换主域后的迁移。
|
|
343
|
+
*/
|
|
344
|
+
export function configFromFile(file, env = process.env, currentCnApiBase = SITES.cn.apiBase) {
|
|
317
345
|
const envJwk = (name) => {
|
|
318
|
-
const raw =
|
|
346
|
+
const raw = env[name];
|
|
319
347
|
if (!raw)
|
|
320
348
|
return undefined;
|
|
321
349
|
try {
|
|
@@ -326,18 +354,27 @@ export function loadConfig() {
|
|
|
326
354
|
}
|
|
327
355
|
};
|
|
328
356
|
return {
|
|
329
|
-
apiKey:
|
|
330
|
-
deviceToken:
|
|
357
|
+
apiKey: env["U1S1_API_KEY"] || file.apiKey,
|
|
358
|
+
deviceToken: env["U1S1_DEVICE_TOKEN"] || file.deviceToken,
|
|
331
359
|
deviceId: file.deviceId,
|
|
332
360
|
devicePrivateJwk: envJwk("U1S1_DEVICE_PRIVATE_JWK") || file.devicePrivateJwk,
|
|
333
361
|
devicePublicJwk: envJwk("U1S1_DEVICE_PUBLIC_JWK") || file.devicePublicJwk,
|
|
334
|
-
baseUrl:
|
|
362
|
+
baseUrl: env["U1S1_BASE_URL"] || migrateStoredBaseUrl(file.baseUrl, currentCnApiBase) || currentCnApiBase,
|
|
335
363
|
// 有效性不在这里裁决:端点列表可能还没拉回来,交给 resolvePreferredModel
|
|
336
364
|
model: file.model,
|
|
337
365
|
modelProvider: typeof file.modelProvider === "string" ? file.modelProvider : undefined,
|
|
338
366
|
email: typeof file.email === "string" && file.email ? file.email : undefined,
|
|
339
367
|
};
|
|
340
368
|
}
|
|
369
|
+
/**
|
|
370
|
+
* 当前生效站点(按 loadConfig().baseUrl 判定,含 U1S1_BASE_URL 覆盖)。手里没有 cfg 的
|
|
371
|
+
* 文案/链接(模块级提示、生成的扩展、错误人话化)用它;有 cfg 的地方直接 siteOf(cfg)。
|
|
372
|
+
* 每次现读配置不缓存:`u1s1 login --site` 切站后同进程里的链接立即跟着变。
|
|
373
|
+
*/
|
|
374
|
+
export function activeSite() {
|
|
375
|
+
return siteOf(loadConfig());
|
|
376
|
+
}
|
|
377
|
+
setLangResolver(() => (activeSite().id === "intl" ? "en" : "zh"));
|
|
341
378
|
/**
|
|
342
379
|
* 凭据文件落盘:先以 0600 写临时文件再 rename 覆盖,文件从不以默认权限
|
|
343
380
|
* (umask 下常是 0644)存在过哪怕一瞬;rename 在 POSIX 上原子替换。
|
|
@@ -398,6 +435,7 @@ export function stripEnvOverrides(cfg, file, env = process.env) {
|
|
|
398
435
|
export function saveConfig(cfg) {
|
|
399
436
|
const file = (readJsonFile(configFile) ?? {});
|
|
400
437
|
writePrivateFile(configFile, JSON.stringify(stripEnvOverrides(cfg, file), null, 2) + "\n");
|
|
438
|
+
resetLang();
|
|
401
439
|
}
|
|
402
440
|
/** Persist the user's preferred model to both stores. */
|
|
403
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
|
}
|