u1s1-cli 0.14.0 → 0.15.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.
@@ -0,0 +1,84 @@
1
+ [
2
+ {
3
+ "name": "quick",
4
+ "description": "快速烟雾测试 — 几秒钟测完,日常切模型后随手验证",
5
+ "questions": [
6
+ {
7
+ "id": "hello",
8
+ "category": "基础",
9
+ "prompt": "只说一句「你好,我是 u1s1」就行,不要多余的话。"
10
+ },
11
+ {
12
+ "id": "reverse",
13
+ "category": "编码",
14
+ "prompt": "写一个 TypeScript 函数,把字符串里的单词顺序反转(不是字符反转)。示例: 'hello world' → 'world hello'。只给代码,不要解释。"
15
+ }
16
+ ]
17
+ },
18
+ {
19
+ "name": "full",
20
+ "description": "全面评测 — 覆盖编程、推理、中文理解、指令遵循",
21
+ "questions": [
22
+ {
23
+ "id": "fizzbuzz",
24
+ "category": "编码",
25
+ "prompt": "用 TypeScript 写 fizzbuzz,从 1 到 100,3 的倍数打印 fizz,5 的倍数打印 buzz,同时是 3 和 5 的倍数打印 fizzbuzz。只给代码,不要解释。"
26
+ },
27
+ {
28
+ "id": "sort-algo",
29
+ "category": "编码",
30
+ "prompt": "用 TypeScript 实现一个快速排序(原地排序,不创建新数组)。只给代码,不要解释。"
31
+ },
32
+ {
33
+ "id": "react-component",
34
+ "category": "编码",
35
+ "prompt": "写一个 React 计数器组件(TypeScript),包含:+ 按钮、- 按钮、重置按钮,显示当前数值。用 useState。只给代码,不要解释。"
36
+ },
37
+ {
38
+ "id": "regex",
39
+ "category": "编码",
40
+ "prompt": "写一个 JavaScript 正则表达式,匹配中国大陆手机号(11 位,1 开头,第二位 3-9)。只给正则,不要解释。"
41
+ },
42
+ {
43
+ "id": "reasoning-1",
44
+ "category": "推理",
45
+ "prompt": "有三个箱子:一个只装苹果,一个只装橘子,一个混装。所有标签都贴错了。你只能从一个箱子里拿一个水果看,就能推断出所有箱子的正确内容。请问该从哪个箱子拿?为什么?"
46
+ },
47
+ {
48
+ "id": "reasoning-2",
49
+ "category": "推理",
50
+ "prompt": "一个人花 8 元买了一只鸡,9 元卖出,10 元买回,11 元卖出。他赚了多少钱?一步步算。"
51
+ },
52
+ {
53
+ "id": "zh-explain",
54
+ "category": "中文",
55
+ "prompt": "用一句话给完全不懂编程的人解释什么是「递归」。说得通俗一点。"
56
+ },
57
+ {
58
+ "id": "zh-poem",
59
+ "category": "中文",
60
+ "prompt": "以「AI」为主题写一首五言绝句(每句五个字,共四句)。"
61
+ },
62
+ {
63
+ "id": "instruction-follow",
64
+ "category": "指令遵循",
65
+ "prompt": "你的回答里只能包含以下三个词(可以重复):好、的、行。其他任何词都不许出现。开始:今天天气怎么样?"
66
+ },
67
+ {
68
+ "id": "format-json",
69
+ "category": "指令遵循",
70
+ "prompt": "输出一个 JSON 对象,包含 name(你的名字)、version(当前日期)、models(数组,随便列三个模型名)。只输出 JSON,不要 markdown 代码块标记。"
71
+ },
72
+ {
73
+ "id": "context-length",
74
+ "category": "长上下文",
75
+ "prompt": "请重复以下句子 50 遍:「u1s1 有一说一。」然后告诉我你一共输出了多少遍。"
76
+ },
77
+ {
78
+ "id": "translation",
79
+ "category": "翻译",
80
+ "prompt": "把这句话翻译成地道的中文: 'One should always code as if the person who ends up maintaining your code is a violent psychopath who knows where you live.'"
81
+ }
82
+ ]
83
+ }
84
+ ]
@@ -0,0 +1 @@
1
+ export declare function benchCommand(args: string[]): Promise<void>;
package/dist/bench.js ADDED
@@ -0,0 +1,520 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { homedir } from "node:os";
5
+ import { CUSTOM_ENDPOINTS, loadConfig, MODELS, PROVIDER_ID, } from "./config.js";
6
+ import { loadCustomEndpoints } from "./api.js";
7
+ // ─── 工具 ───
8
+ const BENCH_DIR = join(homedir(), ".u1s1", "bench");
9
+ /** 项目 bench 数据目录 */
10
+ function benchDataDir() {
11
+ const distDir = dirname(fileURLToPath(import.meta.url));
12
+ return join(distDir, "..", "bench");
13
+ }
14
+ /** 从 suites 数组里按 name 找一个 suite */
15
+ function findSuite(suites, name) {
16
+ return suites.find((s) => s.name === name);
17
+ }
18
+ /** 从 JSON 文件读到 suites 数组 */
19
+ function readSuitesFile(filePath) {
20
+ return JSON.parse(readFileSync(filePath, "utf8"));
21
+ }
22
+ /** 读内置或用户自定义的 suite */
23
+ function loadSuite(name) {
24
+ // 先找内置的(suite.json 含多个 suite,按 name 匹配)
25
+ const builtinPath = join(benchDataDir(), "suite.json");
26
+ if (existsSync(builtinPath)) {
27
+ const found = findSuite(readSuitesFile(builtinPath), name);
28
+ if (found)
29
+ return found;
30
+ }
31
+ // 再找用户的 ~/.u1s1/bench/<name>.json(单文件单 suite,沿用 suite.json 数组格式)
32
+ const userPath = join(BENCH_DIR, `${name}.json`);
33
+ if (existsSync(userPath)) {
34
+ const found = findSuite(readSuitesFile(userPath), name);
35
+ if (found)
36
+ return found;
37
+ }
38
+ throw new Error(`找不到 bench suite「${name}」`);
39
+ }
40
+ /** 列出可用的 suite */
41
+ function listSuites() {
42
+ const suites = [];
43
+ // 内置
44
+ const builtinPath = join(benchDataDir(), "suite.json");
45
+ if (existsSync(builtinPath)) {
46
+ const raw = JSON.parse(readFileSync(builtinPath, "utf8"));
47
+ for (const s of raw) {
48
+ suites.push({ name: s.name, description: s.description, builtin: true });
49
+ }
50
+ }
51
+ // 用户自定义
52
+ if (existsSync(BENCH_DIR)) {
53
+ try {
54
+ const files = readdirSync(BENCH_DIR).filter((f) => f.endsWith(".json"));
55
+ for (const f of files) {
56
+ try {
57
+ const raw = JSON.parse(readFileSync(join(BENCH_DIR, f), "utf8"));
58
+ for (const s of raw) {
59
+ if (!suites.find((x) => x.name === s.name)) {
60
+ suites.push({ name: s.name, description: s.description, builtin: false });
61
+ }
62
+ }
63
+ }
64
+ catch { /* 格式不对跳过 */ }
65
+ }
66
+ }
67
+ catch { /* 忽略 */ }
68
+ }
69
+ return suites;
70
+ }
71
+ function formatDuration(ms) {
72
+ if (ms < 1000)
73
+ return `${ms.toFixed(0)}ms`;
74
+ return `${(ms / 1000).toFixed(1)}s`;
75
+ }
76
+ function costStr(usd) {
77
+ if (usd === 0)
78
+ return "—";
79
+ if (usd < 0.001)
80
+ return "<$0.001";
81
+ return `$${usd.toFixed(4)}`;
82
+ }
83
+ /** 排列表格:每列对齐 */
84
+ function renderTable(header, rows) {
85
+ const colW = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i]?.length ?? 0)));
86
+ const sep = "─".repeat(colW.reduce((a, b) => a + b + 3, 1));
87
+ const line = (cells) => " " + cells.map((c, i) => c.padEnd(colW[i])).join(" │ ") + " ";
88
+ return [
89
+ line(header),
90
+ sep,
91
+ ...rows.map((r) => line(r)),
92
+ ].join("\n");
93
+ }
94
+ // ─── 模型调用 ───
95
+ async function callModel(baseUrl, apiKey, modelId, prompt) {
96
+ const start = performance.now();
97
+ // 简单判断是不是推理模型 — deepseek-reasoner、grok 带 reasoning
98
+ const isReasoning = /reasoner|grok/i.test(modelId);
99
+ const body = {
100
+ model: modelId,
101
+ messages: [{ role: "user", content: prompt }],
102
+ max_tokens: 4096,
103
+ // 推理模型不需要 temperature,传了可能报错
104
+ ...(isReasoning ? {} : { temperature: 0.3 }),
105
+ stream: false,
106
+ };
107
+ const headers = {
108
+ "content-type": "application/json",
109
+ };
110
+ if (apiKey) {
111
+ headers["authorization"] = `Bearer ${apiKey}`;
112
+ }
113
+ try {
114
+ const res = await fetch(`${baseUrl}/chat/completions`, {
115
+ method: "POST",
116
+ headers,
117
+ body: JSON.stringify(body),
118
+ signal: AbortSignal.timeout(120_000), // 2 分钟超时
119
+ });
120
+ const latencyMs = Math.round(performance.now() - start);
121
+ if (!res.ok) {
122
+ const errBody = await res.text().catch(() => "未知错误");
123
+ return {
124
+ response: "",
125
+ latencyMs,
126
+ tokensIn: 0,
127
+ tokensOut: 0,
128
+ error: `HTTP ${res.status}: ${errBody.slice(0, 200)}`,
129
+ };
130
+ }
131
+ const data = (await res.json());
132
+ const content = data.choices?.[0]?.message?.content ?? "";
133
+ const tokensIn = data.usage?.prompt_tokens ?? 0;
134
+ const tokensOut = data.usage?.completion_tokens ?? 0;
135
+ return { response: content, latencyMs, tokensIn, tokensOut };
136
+ }
137
+ catch (e) {
138
+ const latencyMs = Math.round(performance.now() - start);
139
+ return {
140
+ response: "",
141
+ latencyMs,
142
+ tokensIn: 0,
143
+ tokensOut: 0,
144
+ error: e instanceof Error ? e.message : String(e),
145
+ };
146
+ }
147
+ }
148
+ /** 找一个模型的 cost 信息(用于计费估算) */
149
+ function findModelCost(modelId) {
150
+ for (const m of MODELS) {
151
+ if (m.id === modelId)
152
+ return m.cost;
153
+ }
154
+ for (const ep of CUSTOM_ENDPOINTS) {
155
+ for (const m of ep.models) {
156
+ if (m.id === modelId)
157
+ return m.cost;
158
+ }
159
+ }
160
+ return null;
161
+ }
162
+ /** 估算花费 */
163
+ function estimateCost(modelId, tokensIn, tokensOut) {
164
+ const cost = findModelCost(modelId);
165
+ if (!cost)
166
+ return 0;
167
+ // cost 是每百万 token 的美元价格
168
+ return (tokensIn * cost.input + tokensOut * cost.output) / 1_000_000;
169
+ }
170
+ // ─── 保存 & 报告 ───
171
+ function saveRun(run) {
172
+ mkdirSync(BENCH_DIR, { recursive: true });
173
+ const file = join(BENCH_DIR, `${run.id}.json`);
174
+ writeFileSync(file, JSON.stringify(run, null, 2) + "\n");
175
+ console.log(` 结果已保存:${file}`);
176
+ }
177
+ function loadRun(id) {
178
+ const file = join(BENCH_DIR, `${id}.json`);
179
+ if (!existsSync(file))
180
+ throw new Error(`找不到运行记录「${id}」`);
181
+ return JSON.parse(readFileSync(file, "utf8"));
182
+ }
183
+ /** 列出最近的运行记录 */
184
+ function listRuns() {
185
+ if (!existsSync(BENCH_DIR))
186
+ return [];
187
+ const files = readdirSync(BENCH_DIR).filter((f) => f.endsWith(".json") && f.startsWith("bench-"));
188
+ const runs = [];
189
+ for (const f of files.sort().reverse().slice(0, 20)) {
190
+ try {
191
+ const run = JSON.parse(readFileSync(join(BENCH_DIR, f), "utf8"));
192
+ runs.push({ id: run.id, suiteName: run.suiteName, timestamp: run.timestamp, models: run.models });
193
+ }
194
+ catch { /* 跳过损坏记录 */ }
195
+ }
196
+ return runs;
197
+ }
198
+ // ─── 报告生成 ───
199
+ function generateReport(run) {
200
+ const lines = [];
201
+ lines.push(`# Bench 报告: ${run.suiteName}`);
202
+ lines.push(`运行时间: ${run.timestamp}`);
203
+ lines.push(`模型: ${run.models.join(", ")}`);
204
+ lines.push("");
205
+ // 按 category 分组
206
+ const byCategory = new Map();
207
+ for (const r of run.results) {
208
+ const cat = r.category || "未分类";
209
+ if (!byCategory.has(cat))
210
+ byCategory.set(cat, []);
211
+ byCategory.get(cat).push(r);
212
+ }
213
+ for (const [cat, results] of byCategory) {
214
+ lines.push(`## ${cat}`);
215
+ lines.push("");
216
+ // 按 question 分组
217
+ const byQuestion = new Map();
218
+ for (const r of results) {
219
+ if (!byQuestion.has(r.questionId))
220
+ byQuestion.set(r.questionId, []);
221
+ byQuestion.get(r.questionId).push(r);
222
+ }
223
+ for (const [qId, qResults] of byQuestion) {
224
+ const prompt = qResults[0].prompt;
225
+ lines.push(`### ${qId}`);
226
+ lines.push(`> ${prompt}`);
227
+ lines.push("");
228
+ // 表格:模型 | 延迟 | 输入 | 输出 | 花费 | 错误
229
+ const header = ["模型", "延迟", "输入token", "输出token", "花费", "状态"];
230
+ const rows = [];
231
+ for (const r of qResults) {
232
+ const modelLabel = r.providerName === PROVIDER_ID ? r.modelId : `${r.providerName}:${r.modelId}`;
233
+ rows.push([
234
+ modelLabel,
235
+ formatDuration(r.latencyMs),
236
+ String(r.tokensIn),
237
+ String(r.tokensOut),
238
+ costStr(r.costUsd),
239
+ r.error ? `❌ ${r.error}` : "✅",
240
+ ]);
241
+ }
242
+ lines.push("```");
243
+ lines.push(renderTable(header, rows));
244
+ lines.push("```");
245
+ lines.push("");
246
+ // 各模型的回答
247
+ for (const r of qResults) {
248
+ const modelLabel = r.providerName === PROVIDER_ID ? r.modelId : `${r.providerName}:${r.modelId}`;
249
+ lines.push(`<details><summary>${modelLabel} 的回答</summary>`);
250
+ lines.push("");
251
+ lines.push("```");
252
+ lines.push(r.response.slice(0, 2000)); // 截断避免报告过大
253
+ if (r.response.length > 2000)
254
+ lines.push("...(截断)");
255
+ lines.push("```");
256
+ lines.push("</details>");
257
+ lines.push("");
258
+ }
259
+ }
260
+ }
261
+ // 汇总
262
+ lines.push("## 汇总");
263
+ lines.push("");
264
+ const modelSummary = new Map();
265
+ for (const r of run.results) {
266
+ const key = r.providerName === PROVIDER_ID ? r.modelId : `${r.providerName}:${r.modelId}`;
267
+ if (!modelSummary.has(key))
268
+ modelSummary.set(key, { totalLatency: 0, totalCost: 0, errors: 0, total: 0 });
269
+ const s = modelSummary.get(key);
270
+ s.totalLatency += r.latencyMs;
271
+ s.totalCost += r.costUsd;
272
+ if (r.error)
273
+ s.errors++;
274
+ s.total++;
275
+ }
276
+ const header = ["模型", "总延迟", "平均延迟", "总花费", "错误率"];
277
+ const rows = [];
278
+ for (const [model, s] of modelSummary) {
279
+ rows.push([
280
+ model,
281
+ formatDuration(s.totalLatency),
282
+ formatDuration(s.totalLatency / s.total),
283
+ costStr(s.totalCost),
284
+ s.errors > 0 ? `${((s.errors / s.total) * 100).toFixed(0)}%` : "0%",
285
+ ]);
286
+ }
287
+ lines.push("```");
288
+ lines.push(renderTable(header, rows));
289
+ lines.push("```");
290
+ return lines.join("\n");
291
+ }
292
+ // ─── 主命令 ───
293
+ export async function benchCommand(args) {
294
+ const sub = args[0];
295
+ if (!sub || sub === "help" || sub === "--help") {
296
+ console.log("");
297
+ console.log(" u1s1 bench — 模型质量基准测试");
298
+ console.log("");
299
+ console.log(" 用法:");
300
+ console.log(" u1s1 bench list 列出可用的测试套件");
301
+ console.log(" u1s1 bench run <suite> [models..] 跑基准测试");
302
+ console.log(" u1s1 bench report <run-id> 查看报告");
303
+ console.log(" u1s1 bench compare <run-id1> <run-id2> 对比两次运行");
304
+ console.log("");
305
+ console.log(" 示例:");
306
+ console.log(" u1s1 bench run quick 用当前默认模型跑快速测试");
307
+ console.log(" u1s1 bench run full deepseek grok 用两个模型跑全面测试");
308
+ console.log(" u1s1 bench report latest 看最近一次报告");
309
+ console.log("");
310
+ return;
311
+ }
312
+ if (sub === "list") {
313
+ const suites = listSuites();
314
+ console.log("");
315
+ console.log(" Bench 套件:");
316
+ for (const s of suites) {
317
+ const tag = s.builtin ? "(内置)" : "(自定义)";
318
+ console.log(` ${s.name.padEnd(12)} ${s.description} ${tag}`);
319
+ }
320
+ console.log("");
321
+ console.log(" 自定义套件放 ~/.u1s1/bench/<name>.json,格式同内置 suite.json");
322
+ console.log("");
323
+ return;
324
+ }
325
+ if (sub === "run") {
326
+ const suiteName = args[1] ?? "quick";
327
+ const modelQueries = args.slice(2);
328
+ const cfg = loadConfig();
329
+ if (!cfg.apiKey) {
330
+ console.error(" 还没有登录,先 u1s1 login");
331
+ process.exit(1);
332
+ }
333
+ // 加载端点配置
334
+ await loadCustomEndpoints(cfg);
335
+ // 加载测试套件
336
+ let suite;
337
+ try {
338
+ suite = loadSuite(suiteName);
339
+ }
340
+ catch (e) {
341
+ console.error(` ${e instanceof Error ? e.message : e}`);
342
+ process.exit(1);
343
+ }
344
+ let targets;
345
+ function resolveModelTarget(query) {
346
+ const lower = query.toLowerCase();
347
+ // u1s1 官方模型
348
+ const m = MODELS.find((x) => x.id.toLowerCase() === lower || x.aliases.includes(lower));
349
+ if (m) {
350
+ return { id: m.id, label: m.id, baseUrl: cfg.baseUrl, apiKey: cfg.apiKey, source: "u1s1" };
351
+ }
352
+ // 自定义端点
353
+ for (const ep of CUSTOM_ENDPOINTS) {
354
+ const em = ep.models.find((x) => x.id.toLowerCase() === lower || x.aliases.includes(lower));
355
+ if (em) {
356
+ // 自定义端点传模型短 ID(本地 Ollama 等不认带前缀的 huihui_ai/xxx)
357
+ const shortId = em.id.includes("/") ? em.id.split("/")[1] : em.id;
358
+ return { id: shortId, label: `${ep.name}:${em.id}`, baseUrl: ep.baseUrl, apiKey: ep.apiKey ?? "", source: "custom" };
359
+ }
360
+ }
361
+ // 直接当 ID 用
362
+ return { id: query, label: query, baseUrl: cfg.baseUrl, apiKey: cfg.apiKey, source: "u1s1" };
363
+ }
364
+ if (modelQueries.length > 0) {
365
+ targets = modelQueries.map((q) => resolveModelTarget(q)).filter((t) => t !== null);
366
+ }
367
+ else {
368
+ // 默认:用当前首选模型
369
+ const { resolvePreferredModel } = await import("./config.js");
370
+ const pref = resolvePreferredModel(cfg);
371
+ if (pref.provider === PROVIDER_ID) {
372
+ targets = [{ id: pref.id, label: pref.id, baseUrl: cfg.baseUrl, apiKey: cfg.apiKey, source: "u1s1" }];
373
+ }
374
+ else {
375
+ const ep = CUSTOM_ENDPOINTS.find((e) => e.id === pref.provider);
376
+ if (ep) {
377
+ const shortId = pref.id.includes("/") ? pref.id.split("/")[1] : pref.id;
378
+ targets = [{ id: shortId, label: `${ep.name}:${pref.id}`, baseUrl: ep.baseUrl, apiKey: ep.apiKey ?? "", source: "custom" }];
379
+ }
380
+ else {
381
+ targets = [{ id: pref.id, label: pref.id, baseUrl: cfg.baseUrl, apiKey: cfg.apiKey, source: "u1s1" }];
382
+ }
383
+ }
384
+ }
385
+ // 确认
386
+ console.log("");
387
+ console.log(` 📊 Bench: ${suite.name}`);
388
+ console.log(` ${suite.description}`);
389
+ console.log(` 题目数: ${suite.questions.length}`);
390
+ console.log(` 模型: ${targets.map((t) => t.label).join(", ")}`);
391
+ console.log("");
392
+ // 跑
393
+ const results = [];
394
+ const total = suite.questions.length * targets.length;
395
+ let done = 0;
396
+ for (const target of targets) {
397
+ for (const q of suite.questions) {
398
+ const label = `${target.label} / ${q.id}`;
399
+ process.stdout.write(` [${++done}/${total}] ${label.padEnd(40)} `);
400
+ const result = await callModel(target.baseUrl, target.apiKey, target.id, q.prompt);
401
+ const costUsd = estimateCost(target.id, result.tokensIn, result.tokensOut);
402
+ results.push({
403
+ questionId: q.id,
404
+ category: q.category,
405
+ prompt: q.prompt,
406
+ modelId: target.label,
407
+ providerName: target.source === "u1s1" ? PROVIDER_ID : target.label,
408
+ response: result.response,
409
+ latencyMs: result.latencyMs,
410
+ tokensIn: result.tokensIn,
411
+ tokensOut: result.tokensOut,
412
+ costUsd,
413
+ error: result.error,
414
+ thinking: result.thinking,
415
+ });
416
+ if (result.error) {
417
+ console.log(`❌ ${result.error.slice(0, 60)}`);
418
+ }
419
+ else {
420
+ console.log(`✅ ${formatDuration(result.latencyMs)} · ${result.tokensIn}→${result.tokensOut} tok · ${costStr(costUsd)}`);
421
+ }
422
+ }
423
+ }
424
+ // 保存
425
+ const runId = `bench-${suite.name}-${Date.now()}`;
426
+ const run = {
427
+ id: runId,
428
+ timestamp: new Date().toISOString(),
429
+ suiteName: suite.name,
430
+ models: [...new Set(results.map((r) => r.modelId))],
431
+ results,
432
+ };
433
+ saveRun(run);
434
+ // 打印简易报告
435
+ console.log("");
436
+ console.log(" 📋 简易汇总:");
437
+ const byModel = new Map();
438
+ for (const r of results) {
439
+ const key = r.modelId;
440
+ if (!byModel.has(key))
441
+ byModel.set(key, { ok: 0, fail: 0, totalLatency: 0, totalCost: 0 });
442
+ const s = byModel.get(key);
443
+ if (r.error)
444
+ s.fail++;
445
+ else
446
+ s.ok++;
447
+ s.totalLatency += r.latencyMs;
448
+ s.totalCost += r.costUsd;
449
+ }
450
+ for (const [model, s] of byModel) {
451
+ const passRate = s.ok / (s.ok + s.fail);
452
+ console.log(` ${model.padEnd(30)} ${(passRate * 100).toFixed(0)}%通过 · 平均 ${formatDuration(s.totalLatency / (s.ok + s.fail))} · 花费 ${costStr(s.totalCost)}`);
453
+ }
454
+ console.log("");
455
+ console.log(` 查看完整报告: u1s1 bench report ${runId}`);
456
+ console.log("");
457
+ return;
458
+ }
459
+ if (sub === "report") {
460
+ const runId = args[1] ?? "latest";
461
+ let run;
462
+ try {
463
+ const id = runId === "latest" ? findLatestRun() : runId;
464
+ if (!id)
465
+ throw new Error("还没有运行记录");
466
+ run = loadRun(id);
467
+ }
468
+ catch (e) {
469
+ console.error(` ${e instanceof Error ? e.message : e}`);
470
+ process.exit(1);
471
+ }
472
+ const report = generateReport(run);
473
+ // 写入可读文件
474
+ const reportFile = join(BENCH_DIR, `${run.id}.md`);
475
+ writeFileSync(reportFile, report);
476
+ console.log(report);
477
+ console.log(` 报告已保存: ${reportFile}`);
478
+ return;
479
+ }
480
+ if (sub === "compare" && args[1] && args[2]) {
481
+ try {
482
+ const run1 = loadRun(args[1]);
483
+ const run2 = loadRun(args[2]);
484
+ console.log("");
485
+ console.log(` 对比 ${run1.suiteName}(${run1.timestamp}) vs ${run2.suiteName}(${run2.timestamp})`);
486
+ console.log("");
487
+ // 找共同题目,并排展示
488
+ const qIds1 = new Set(run1.results.map((r) => r.questionId));
489
+ const qIds2 = new Set(run2.results.map((r) => r.questionId));
490
+ const common = [...qIds1].filter((q) => qIds2.has(q));
491
+ for (const qId of common) {
492
+ console.log(` ── ${qId} ──`);
493
+ const r1 = run1.results.find((r) => r.questionId === qId);
494
+ const r2 = run2.results.find((r) => r.questionId === qId);
495
+ if (r1 && r2) {
496
+ console.log(` [run1] ${r1.modelId}: ${r1.response.slice(0, 200)}`);
497
+ console.log(` [run2] ${r2.modelId}: ${r2.response.slice(0, 200)}`);
498
+ console.log("");
499
+ }
500
+ }
501
+ }
502
+ catch (e) {
503
+ console.error(` ${e instanceof Error ? e.message : e}`);
504
+ process.exit(1);
505
+ }
506
+ return;
507
+ }
508
+ if (sub === "compare") {
509
+ console.log(" 用法: u1s1 bench compare <run-id1> <run-id2>");
510
+ return;
511
+ }
512
+ console.error(` 未知子命令: ${sub}`);
513
+ console.log(" u1s1 bench help 查看用法");
514
+ process.exit(1);
515
+ }
516
+ /** 找最近一次运行的 id */
517
+ function findLatestRun() {
518
+ const runs = listRuns();
519
+ return runs[0]?.id ?? null;
520
+ }
package/dist/index.js CHANGED
@@ -213,6 +213,24 @@ async function runAgent(cfg, args) {
213
213
  factory: (pi) => {
214
214
  applyBrandUi(pi, VERSION);
215
215
  offerStarterTemplates(pi);
216
+ // 会话标题:拿用户第一条正经输入的前半截当名字(像 Claude Code 那样),
217
+ // /resume 的会话列表里就能一眼认出每个会话。/clear 后 session_start
218
+ // 会重置标记,新会话重新起标题。
219
+ let sessionTitled = false;
220
+ pi.on("session_start", () => {
221
+ sessionTitled = false;
222
+ });
223
+ pi.on("input", async (event) => {
224
+ if (sessionTitled || event.source === "extension")
225
+ return;
226
+ const text = event.text.trim().replace(/\s+/g, " ");
227
+ // 空输入(纯图片粘贴)和斜杠命令不当标题
228
+ if (!text || text.startsWith("/"))
229
+ return;
230
+ sessionTitled = true;
231
+ const title = text.length > 30 ? `${text.slice(0, 30)}…` : text;
232
+ pi.setSessionName(title);
233
+ });
216
234
  // pi 内置只有 /quit;补一个更常见的 /exit,方便新手退出
217
235
  pi.registerCommand("exit", {
218
236
  description: "退出 u1s1",
@@ -283,7 +301,7 @@ async function run() {
283
301
  }
284
302
  if (cmd === "--help" || cmd === "-h") {
285
303
  printConsoleBanner(VERSION);
286
- console.log(" u1s1 命令:web(浏览器网页版)· web shortcut(桌面图标)· deploy(发布网页)· login / logout · model · usage · update · import");
304
+ console.log(" u1s1 命令:web(浏览器网页版)· web shortcut(桌面图标)· deploy(发布网页)· login / logout · model · usage · update · import · bench");
287
305
  console.log("");
288
306
  }
289
307
  if (cmd === "web") {
@@ -337,6 +355,11 @@ async function run() {
337
355
  await importCommand(args.slice(1));
338
356
  return;
339
357
  }
358
+ if (cmd === "bench") {
359
+ const { benchCommand } = await import("./bench.js");
360
+ await benchCommand(args.slice(1));
361
+ return;
362
+ }
340
363
  const { ensureAuth } = await import("./login.js");
341
364
  const cfg = await ensureAuth();
342
365
  // 在后台检查更新(非阻塞,不影响启动速度)
package/dist/tools.js CHANGED
@@ -174,7 +174,8 @@ export function createImageTool(cfg) {
174
174
  description: "Reference images to edit or draw from: local file paths or http(s) URLs, up to 10. Omit for pure text-to-image.",
175
175
  })),
176
176
  size: Type.Optional(Type.String({
177
- description: 'Output resolution: "2K" (default, aspect ratio auto-adapts to the prompt), "4K", or explicit "WIDTHxHEIGHT" like "2048x2048" / "1664x2496".',
177
+ description: 'Output resolution: "2K" (default, aspect ratio auto-adapts to the prompt), "4K", or explicit "WIDTHxHEIGHT" like "2048x2048" / "1664x2496". ' +
178
+ "The model requires ≥3.7M total pixels; smaller explicit sizes are auto-scaled up keeping the aspect ratio, so prefer WIDTHxHEIGHT only to control the ratio.",
178
179
  })),
179
180
  save_path: Type.Optional(Type.String({
180
181
  description: "Where to save the image (relative to cwd). Defaults to image-<timestamp>.<ext> in the current directory.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u1s1-cli",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "u1s1 — 有一说一,最省心的 AI 编程搭子。终端里用中文说需求,AI 帮你读文件、改代码、跑命令。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,8 @@
15
15
  },
16
16
  "files": [
17
17
  "dist",
18
- "webui-dist"
18
+ "webui-dist",
19
+ "bench"
19
20
  ],
20
21
  "engines": {
21
22
  "node": ">=22.19.0"