u1s1-cli 1.2.3 → 1.2.4

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/bench.js CHANGED
@@ -5,26 +5,17 @@ import { homedir } from "node:os";
5
5
  import { CUSTOM_ENDPOINTS, loadConfig, MODELS, PROVIDER_ID, } from "./config.js";
6
6
  import { loadCustomEndpoints } from "./api.js";
7
7
  import { authorizedFetch, hasDeviceCredential } from "./device-auth.js";
8
+ import { costStr, errorRate, formatDuration, generateReport, renderTable, scoreBar, scoreRate, sortByScore, summarizeByModel, tokPerSec, } from "./bench-report.js";
9
+ import { scoreResponse } from "./bench-scoring.js";
10
+ import { DEFAULT_MAX_SCORE, } from "./bench-types.js";
8
11
  // ─── 工具 ───
9
12
  const BENCH_DIR = join(homedir(), ".u1s1", "bench");
10
- /** 题目未声明 maxScore 时的默认满分 */
11
- const DEFAULT_MAX_SCORE = 10;
12
- /** 报告里单条回答最多保留的字符数 */
13
- const MAX_ANSWER_CHARS = 2000;
14
13
  /** compare 子命令里单条回答的截断长度 */
15
14
  const MAX_COMPARE_CHARS = 200;
16
- /** 去掉回答里的 markdown 代码块围栏,便于 JSON 解析 */
17
- function stripCodeFences(text) {
18
- return text.replace(/```json\s*\n?/gi, "").replace(/\n?```/g, "").trim();
19
- }
20
15
  /** 自定义端点的模型 id 常带 org/ 前缀,取斜杠后的短 id */
21
16
  function shortModelId(id) {
22
17
  return id.includes("/") ? id.split("/")[1] : id;
23
18
  }
24
- /** 结果里模型的显示名:内置模型不带前缀,自定义端点带 provider 前缀 */
25
- function resultModelLabel(r) {
26
- return r.providerName === PROVIDER_ID ? r.modelId : `${r.providerName}:${r.modelId}`;
27
- }
28
19
  /** 项目 bench 数据目录 */
29
20
  function benchDataDir() {
30
21
  const distDir = dirname(fileURLToPath(import.meta.url));
@@ -55,184 +46,44 @@ function loadSuite(name) {
55
46
  throw new Error(`找不到 bench suite「${name}」`);
56
47
  }
57
48
  /** 列出可用的 suite */
58
- function listSuites() {
59
- const suites = [];
60
- const builtinPath = join(benchDataDir(), "suite.json");
61
- if (existsSync(builtinPath)) {
62
- const raw = JSON.parse(readFileSync(builtinPath, "utf8"));
63
- for (const s of raw) {
64
- suites.push({ name: s.name, description: s.description, builtin: true });
65
- }
49
+ function suiteSummaries(filePath, builtin) {
50
+ try {
51
+ return readSuitesFile(filePath).map((suite) => ({
52
+ name: suite.name,
53
+ description: suite.description,
54
+ builtin,
55
+ }));
66
56
  }
67
- if (existsSync(BENCH_DIR)) {
68
- try {
69
- const files = readdirSync(BENCH_DIR).filter((f) => f.endsWith(".json"));
70
- for (const f of files) {
71
- try {
72
- const raw = JSON.parse(readFileSync(join(BENCH_DIR, f), "utf8"));
73
- for (const s of raw) {
74
- if (!suites.find((x) => x.name === s.name)) {
75
- suites.push({ name: s.name, description: s.description, builtin: false });
76
- }
77
- }
78
- }
79
- catch { /* 格式不对跳过 */ }
80
- }
81
- }
82
- catch { /* 忽略 */ }
57
+ catch {
58
+ return [];
83
59
  }
84
- return suites;
85
60
  }
86
- function formatDuration(ms) {
87
- if (ms < 1000)
88
- return `${ms.toFixed(0)}ms`;
89
- return `${(ms / 1000).toFixed(1)}s`;
90
- }
91
- function costStr(usd) {
92
- if (usd === 0)
93
- return "—";
94
- if (usd < 0.001)
95
- return "<$0.001";
96
- return `$${usd.toFixed(4)}`;
97
- }
98
- /** 排列表格 */
99
- function renderTable(header, rows) {
100
- const colW = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i]?.length ?? 0)));
101
- const sep = "─".repeat(colW.reduce((a, b) => a + b + 3, 1));
102
- const line = (cells) => " " + cells.map((c, i) => c.padEnd(colW[i])).join(" │ ") + " ";
103
- return [line(header), sep, ...rows.map((r) => line(r))].join("\n");
104
- }
105
- /** 热度条:分数按比例转成视觉条 */
106
- function scoreBar(score, maxScore, width = 8) {
107
- const ratio = maxScore > 0 ? Math.min(score / maxScore, 1) : 0;
108
- const filled = Math.round(ratio * width);
109
- const empty = width - filled;
110
- const bar = "█".repeat(filled) + "░".repeat(empty);
111
- // 颜色标记
112
- if (ratio >= 0.8)
113
- return `\x1b[32m${bar}\x1b[0m`; // 绿
114
- if (ratio >= 0.5)
115
- return `\x1b[33m${bar}\x1b[0m`; // 黄
116
- return `\x1b[31m${bar}\x1b[0m`; // 红
117
- }
118
- // ─── 评分引擎 ───
119
- /** 对一条回答按 checks 逐项打分 */
120
- function scoreResponse(response, checks, maxScore) {
121
- const details = [];
122
- let total = 0;
123
- for (const check of checks) {
124
- const result = runCheck(response, check);
125
- details.push({
126
- check: check.description,
127
- passed: result,
128
- points: check.points,
129
- earned: result ? check.points : 0,
130
- });
131
- if (result)
132
- total += check.points;
61
+ function userSuitePaths() {
62
+ if (!existsSync(BENCH_DIR))
63
+ return [];
64
+ try {
65
+ return readdirSync(BENCH_DIR)
66
+ .filter((file) => file.endsWith(".json"))
67
+ .map((file) => join(BENCH_DIR, file));
133
68
  }
134
- return { score: Math.min(total, maxScore), details };
135
- }
136
- /** 执行单项检查 */
137
- function runCheck(response, check) {
138
- const val = check.value;
139
- switch (check.type) {
140
- case "contains":
141
- case "not_contains": {
142
- const needle = String(val);
143
- const text = check.ignoreCase ? response.toLowerCase() : response;
144
- const search = check.ignoreCase ? needle.toLowerCase() : needle;
145
- const found = text.includes(search);
146
- return check.type === "contains" ? found : !found;
147
- }
148
- case "contains_code_block":
149
- return /```[\s\S]*?```/.test(response) || /`[^`]+`/.test(response);
150
- case "parse_json": {
151
- try {
152
- JSON.parse(stripCodeFences(response));
153
- return true;
154
- }
155
- catch {
156
- return false;
157
- }
158
- }
159
- case "valid_regex": {
160
- try {
161
- new RegExp(String(val));
162
- return true;
163
- }
164
- catch {
165
- // 从回答里提取 regex 来验证
166
- const match = response.match(/\/(.+)\/[gimsuy]*/);
167
- if (match) {
168
- new RegExp(match[1]);
169
- return true;
170
- }
171
- // 也可能是文本形式
172
- const cleaned = response.replace(/```[\s\S]*?```/g, "").trim();
173
- if (cleaned.startsWith("/")) {
174
- const parts = cleaned.match(/^\/(.+)\/([gimsuy]*)$/);
175
- if (parts) {
176
- new RegExp(parts[1]);
177
- return true;
178
- }
179
- }
180
- return false;
181
- }
182
- }
183
- case "has_json_key": {
184
- try {
185
- const obj = JSON.parse(stripCodeFences(response));
186
- return obj[String(val)] !== undefined;
187
- }
188
- catch {
189
- return false;
190
- }
191
- }
192
- case "max_length": {
193
- const limit = Number(val);
194
- return response.length <= limit;
195
- }
196
- case "min_length": {
197
- const limit = Number(val);
198
- return response.length >= limit;
199
- }
200
- case "allowed_words_only": {
201
- const allowed = val;
202
- // 提取回答中的所有中文/英文字词
203
- const tokens = response.split(/[\s,。!?、;:""''()\[【】\]「」]+/).filter(Boolean);
204
- for (const token of tokens) {
205
- if (token.length === 0)
206
- continue;
207
- // 标点/空格不检查
208
- if (/^[,。!?、;:""''()《》\s.。,,、!?;:…\-—]+$/.test(token))
209
- continue;
210
- if (!allowed.includes(token))
211
- return false;
212
- }
213
- return true;
214
- }
215
- case "has_chinese": {
216
- return /[\u4e00-\u9fff\u3400-\u4dbf]/.test(response);
217
- }
218
- case "line_count_5chars": {
219
- // 检查有几行是恰好 5 个汉字(去除标点空格)
220
- const expected = Number(val);
221
- const lines = response.split("\n");
222
- let count5 = 0;
223
- for (const line of lines) {
224
- const chars = line.replace(/[,。!?、;:""''()《》\s,\.\-,;:!?\dA-Za-z]/g, "").trim();
225
- if (/^[\u4e00-\u9fff]{5}$/.test(chars))
226
- count5++;
227
- }
228
- return count5 >= expected;
69
+ catch {
70
+ return [];
71
+ }
72
+ }
73
+ function listSuites() {
74
+ const builtinPath = join(benchDataDir(), "suite.json");
75
+ const suites = existsSync(builtinPath) ? suiteSummaries(builtinPath, true) : [];
76
+ for (const path of userSuitePaths()) {
77
+ for (const suite of suiteSummaries(path, false)) {
78
+ if (!suites.some((current) => current.name === suite.name))
79
+ suites.push(suite);
229
80
  }
230
- default:
231
- return false;
232
81
  }
82
+ return suites;
233
83
  }
234
84
  // ─── 模型调用 ───
235
- async function callModel(baseUrl, apiKey, modelId, prompt, officialCfg) {
85
+ async function callModel(input) {
86
+ const { baseUrl, apiKey, modelId, prompt, officialCfg } = input;
236
87
  const start = performance.now();
237
88
  const isReasoning = /reasoner|grok/i.test(modelId);
238
89
  const body = {
@@ -319,146 +170,6 @@ function listRuns() {
319
170
  }
320
171
  return runs;
321
172
  }
322
- /** 把一批结果按模型聚合成分数/延迟/花费/错误数 */
323
- function summarizeByModel(results, keyOf) {
324
- const map = new Map();
325
- for (const r of results) {
326
- const key = keyOf(r);
327
- const s = map.get(key) ?? { totalScore: 0, totalMax: 0, totalLatency: 0, totalCost: 0, errors: 0, total: 0, okTokensOut: 0, okLatency: 0 };
328
- s.totalScore += r.score ?? 0;
329
- s.totalMax += r.maxScore ?? DEFAULT_MAX_SCORE;
330
- s.totalLatency += r.latencyMs;
331
- s.totalCost += r.costUsd;
332
- if (r.error) {
333
- s.errors++;
334
- }
335
- else {
336
- s.okTokensOut += r.tokensOut;
337
- s.okLatency += r.latencyMs;
338
- }
339
- s.total++;
340
- map.set(key, s);
341
- }
342
- return map;
343
- }
344
- /** 按总分从高到低排序 */
345
- function sortByScore(entries) {
346
- return entries.sort((a, b) => b[1].totalScore - a[1].totalScore);
347
- }
348
- /** 得分率百分数字符串(一位小数) */
349
- function scoreRate(s) {
350
- return s.totalMax > 0 ? ((s.totalScore / s.totalMax) * 100).toFixed(1) : "0.0";
351
- }
352
- /** 错误率百分数字符串(整数) */
353
- function errorRate(s) {
354
- return ((s.errors / s.total) * 100).toFixed(0);
355
- }
356
- /** 推理速度:成功题的输出 token / 耗时(tok/s),没有成功题返回 — */
357
- function tokPerSec(s) {
358
- return s.okLatency > 0 ? (s.okTokensOut / s.okLatency * 1000).toFixed(1) : "—";
359
- }
360
- // ─── 报告生成 ───
361
- function generateReport(run) {
362
- const lines = [];
363
- lines.push(`# Bench 报告: ${run.suiteName}`);
364
- lines.push(`运行时间: ${run.timestamp}`);
365
- lines.push(`模型: ${run.models.join(", ")}`);
366
- lines.push("");
367
- const byCategory = new Map();
368
- for (const r of run.results) {
369
- const cat = r.category || "未分类";
370
- if (!byCategory.has(cat))
371
- byCategory.set(cat, []);
372
- byCategory.get(cat).push(r);
373
- }
374
- for (const [cat, results] of byCategory) {
375
- lines.push(`## ${cat}`);
376
- lines.push("");
377
- const byQuestion = new Map();
378
- for (const r of results) {
379
- if (!byQuestion.has(r.questionId))
380
- byQuestion.set(r.questionId, []);
381
- byQuestion.get(r.questionId).push(r);
382
- }
383
- for (const [qId, qResults] of byQuestion) {
384
- const prompt = qResults[0].prompt;
385
- const maxScore = qResults[0].maxScore ?? DEFAULT_MAX_SCORE;
386
- lines.push(`### ${qId}`);
387
- lines.push(`> ${prompt}`);
388
- lines.push("");
389
- // 表格: 模型 | 分数 | 延迟 | 输出token | 状态
390
- const header = ["模型", `得分/${maxScore}`, "延迟", "输出tok", "状态"];
391
- const rows = [];
392
- for (const r of qResults) {
393
- const scoreStr = r.score !== undefined ? `${r.score}/${maxScore}` : "—";
394
- rows.push([
395
- resultModelLabel(r),
396
- scoreStr,
397
- formatDuration(r.latencyMs),
398
- String(r.tokensOut),
399
- r.error ? `❌ ${r.error.slice(0, 40)}` : "✅",
400
- ]);
401
- }
402
- lines.push("```");
403
- lines.push(renderTable(header, rows));
404
- lines.push("```");
405
- // 评分明细
406
- const scored = qResults.filter((r) => r.scoreDetails && r.scoreDetails.length > 0);
407
- if (scored.length > 0) {
408
- lines.push("");
409
- lines.push("<details><summary>评分明细</summary>");
410
- lines.push("");
411
- for (const r of scored) {
412
- lines.push(`**${resultModelLabel(r)}:** ${r.score}/${maxScore}`);
413
- for (const d of r.scoreDetails) {
414
- const icon = d.passed ? "✅" : "❌";
415
- lines.push(`- ${icon} ${d.check} (+${d.earned}/${d.points})`);
416
- }
417
- lines.push("");
418
- }
419
- lines.push("</details>");
420
- lines.push("");
421
- }
422
- // 各模型回答
423
- for (const r of qResults) {
424
- lines.push(`<details><summary>${resultModelLabel(r)} 的回答</summary>`);
425
- lines.push("");
426
- lines.push("```");
427
- lines.push(r.response.slice(0, MAX_ANSWER_CHARS));
428
- if (r.response.length > MAX_ANSWER_CHARS)
429
- lines.push("...(截断)");
430
- lines.push("");
431
- lines.push("```");
432
- lines.push(r.response.slice(0, 2000));
433
- if (r.response.length > 2000)
434
- lines.push("...(截断)");
435
- lines.push("```");
436
- lines.push("</details>");
437
- lines.push("");
438
- }
439
- }
440
- }
441
- // 汇总 — 带分数
442
- lines.push("## 汇总");
443
- lines.push("");
444
- const header = ["模型", "总分", "得分率", "平均延迟", "速度(tok/s)", "总花费", "错误率"];
445
- const rows = [];
446
- for (const [model, s] of sortByScore([...summarizeByModel(run.results, resultModelLabel)])) {
447
- rows.push([
448
- model,
449
- `${s.totalScore}/${s.totalMax}`,
450
- `${scoreRate(s)}%`,
451
- formatDuration(s.totalLatency / s.total),
452
- tokPerSec(s),
453
- costStr(s.totalCost),
454
- s.errors > 0 ? `${errorRate(s)}%` : "0%",
455
- ]);
456
- }
457
- lines.push("```");
458
- lines.push(renderTable(header, rows));
459
- lines.push("```");
460
- return lines.join("\n");
461
- }
462
173
  // ─── 主命令 ───
463
174
  /** 打印错误信息并以退出码 1 结束。永不返回。 */
464
175
  function failWithError(e) {
@@ -541,19 +252,70 @@ function printQuestionOutcome(resp, score, maxScore) {
541
252
  console.log(`✅ ${formatDuration(resp.latencyMs)} · ${resp.tokensIn}→${resp.tokensOut} tok`);
542
253
  }
543
254
  }
544
- async function runBench(args) {
255
+ function parseBenchRunArgs(args) {
545
256
  const suiteName = args[0] ?? "quick";
546
257
  const modelQueries = [];
547
258
  let delayMs = 0;
548
- for (let i = 1; i < args.length; i++) {
549
- const a = args[i];
550
- if (a === "--delay")
551
- delayMs = Number(args[++i]) || 0;
552
- else if (a.startsWith("--delay="))
553
- delayMs = Number(a.slice("--delay=".length)) || 0;
554
- else
555
- modelQueries.push(a);
259
+ for (let index = 1; index < args.length; index++) {
260
+ const argument = args[index];
261
+ if (argument === "--delay") {
262
+ delayMs = Number(args[++index]) || 0;
263
+ }
264
+ else if (argument.startsWith("--delay=")) {
265
+ delayMs = Number(argument.slice("--delay=".length)) || 0;
266
+ }
267
+ else {
268
+ modelQueries.push(argument);
269
+ }
556
270
  }
271
+ return { suiteName, modelQueries, delayMs };
272
+ }
273
+ function loadSuiteOrExit(name) {
274
+ try {
275
+ return loadSuite(name);
276
+ }
277
+ catch (error) {
278
+ return failWithError(error);
279
+ }
280
+ }
281
+ async function runBenchQuestion(input) {
282
+ const { target, question, progress, delayMs } = input;
283
+ const label = `${target.label} / ${question.id}`;
284
+ process.stdout.write(` [${progress}] ${label.padEnd(40)} `);
285
+ const response = await callModel({
286
+ baseUrl: target.baseUrl,
287
+ apiKey: target.apiKey,
288
+ modelId: target.id,
289
+ prompt: question.prompt,
290
+ officialCfg: target.officialCfg,
291
+ });
292
+ const maxScore = question.maxScore ?? DEFAULT_MAX_SCORE;
293
+ const scoring = !response.error && question.checks && question.checks.length > 0
294
+ ? scoreResponse(response.response, question.checks, maxScore)
295
+ : null;
296
+ const result = {
297
+ questionId: question.id,
298
+ category: question.category,
299
+ prompt: question.prompt,
300
+ modelId: target.label,
301
+ providerName: target.source === "u1s1" ? PROVIDER_ID : target.label,
302
+ response: response.response,
303
+ latencyMs: response.latencyMs,
304
+ tokensIn: response.tokensIn,
305
+ tokensOut: response.tokensOut,
306
+ costUsd: estimateCost(target.id, response.tokensIn, response.tokensOut),
307
+ error: response.error,
308
+ score: scoring?.score,
309
+ maxScore: scoring ? maxScore : undefined,
310
+ scoreDetails: scoring?.details,
311
+ };
312
+ printQuestionOutcome(response, scoring?.score, scoring ? maxScore : undefined);
313
+ if (delayMs > 0)
314
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, delayMs));
315
+ return result;
316
+ }
317
+ async function runBench(args) {
318
+ const { suiteName, modelQueries, delayMs } = parseBenchRunArgs(args);
557
319
  if (delayMs > 0)
558
320
  console.log(` ⏳ 题目间隔:${(delayMs / 1000).toFixed(0)}s(限流渠道用)`);
559
321
  const cfg = loadConfig();
@@ -562,13 +324,7 @@ async function runBench(args) {
562
324
  process.exit(1);
563
325
  }
564
326
  await loadCustomEndpoints(cfg);
565
- let suite;
566
- try {
567
- suite = loadSuite(suiteName);
568
- }
569
- catch (e) {
570
- failWithError(e);
571
- }
327
+ const suite = loadSuiteOrExit(suiteName);
572
328
  const targets = modelQueries.length > 0
573
329
  ? modelQueries.map((q) => resolveModelTarget(q, cfg))
574
330
  : await resolveDefaultTargets(cfg);
@@ -582,35 +338,13 @@ async function runBench(args) {
582
338
  const total = suite.questions.length * targets.length;
583
339
  let done = 0;
584
340
  for (const target of targets) {
585
- for (const q of suite.questions) {
586
- const label = `${target.label} / ${q.id}`;
587
- process.stdout.write(` [${++done}/${total}] ${label.padEnd(40)} `);
588
- const resp = await callModel(target.baseUrl, target.apiKey, target.id, q.prompt, target.officialCfg);
589
- const costUsd = estimateCost(target.id, resp.tokensIn, resp.tokensOut);
590
- // 有评分规则且调用没出错时才打分
591
- const maxScore = q.maxScore ?? DEFAULT_MAX_SCORE;
592
- const scoring = !resp.error && q.checks && q.checks.length > 0
593
- ? scoreResponse(resp.response, q.checks, maxScore)
594
- : null;
595
- results.push({
596
- questionId: q.id,
597
- category: q.category,
598
- prompt: q.prompt,
599
- modelId: target.label,
600
- providerName: target.source === "u1s1" ? PROVIDER_ID : target.label,
601
- response: resp.response,
602
- latencyMs: resp.latencyMs,
603
- tokensIn: resp.tokensIn,
604
- tokensOut: resp.tokensOut,
605
- costUsd,
606
- error: resp.error,
607
- score: scoring?.score,
608
- maxScore: scoring ? maxScore : undefined,
609
- scoreDetails: scoring?.details,
610
- });
611
- printQuestionOutcome(resp, scoring?.score, scoring ? maxScore : undefined);
612
- if (delayMs > 0)
613
- await new Promise((r) => setTimeout(r, delayMs));
341
+ for (const question of suite.questions) {
342
+ results.push(await runBenchQuestion({
343
+ target,
344
+ question,
345
+ progress: `${++done}/${total}`,
346
+ delayMs,
347
+ }));
614
348
  }
615
349
  }
616
350
  const runId = `bench-${suite.name}-${Date.now()}`;
package/dist/brand.d.ts CHANGED
@@ -9,8 +9,14 @@ export declare const HERO_ART: string[];
9
9
  * Startup hero, responsive to terminal width:
10
10
  * wide → wordmark with info column beside it; medium → stacked; narrow → one-liner.
11
11
  */
12
- export declare function renderBrandHeader(theme: Theme, version: string, cwd: string, width: number, notice?: string, announcement?: {
13
- text: string;
14
- url?: string;
12
+ export declare function renderBrandHeader(theme: Theme, input: {
13
+ version: string;
14
+ cwd: string;
15
+ width: number;
16
+ notice?: string;
17
+ announcement?: {
18
+ text: string;
19
+ url?: string;
20
+ };
15
21
  }): string[];
16
22
  export declare function printConsoleBanner(version: string): void;
package/dist/brand.js CHANGED
@@ -51,24 +51,24 @@ function announcementLine(theme, a) {
51
51
  * Startup hero, responsive to terminal width:
52
52
  * wide → wordmark with info column beside it; medium → stacked; narrow → one-liner.
53
53
  */
54
- export function renderBrandHeader(theme, version, cwd, width, notice, announcement) {
55
- const name = theme.bold(theme.fg("text", `${BRAND_NAME} v${version}`)) +
56
- (notice ? ` ${theme.fg("accent", notice)}` : "");
54
+ export function renderBrandHeader(theme, input) {
55
+ const name = theme.bold(theme.fg("text", `${BRAND_NAME} v${input.version}`)) +
56
+ (input.notice ? ` ${theme.fg("accent", input.notice)}` : "");
57
57
  const brand = theme.fg("muted", `${BRAND_CN} · ${BRAND_TAGLINE}`);
58
- const dir = theme.fg("dim", `cwd: ${formatHomePath(cwd)}`);
58
+ const dir = theme.fg("dim", `cwd: ${formatHomePath(input.cwd)}`);
59
59
  const hints = theme.fg("dim", "/help · Ctrl+V 图片 · Shift+Enter 换行 · Esc 中断");
60
- if (width < ART_WIDTH + 4) {
60
+ if (input.width < ART_WIDTH + 4) {
61
61
  const lines = ["", ` ${theme.fg("accent", "✻")} ${name} ${theme.fg("muted", BRAND_CN)}`, ` ${dir}`, ` ${theme.fg("dim", "Ctrl+V 粘贴图片")}`];
62
- if (announcement)
63
- lines.push(` ${announcementLine(theme, announcement)}`);
62
+ if (input.announcement)
63
+ lines.push(` ${announcementLine(theme, input.announcement)}`);
64
64
  return [...lines, ""];
65
65
  }
66
66
  const art = HERO_ART.map((line) => ` ${paintArt(theme, line)}`);
67
67
  // widest info row (hints) needs 46 cols beside the 28-col wordmark
68
- if (width < ART_WIDTH + 46) {
68
+ if (input.width < ART_WIDTH + 46) {
69
69
  const lines = ["", ...art, "", ` ${name} ${brand}`, ` ${dir}`, ` ${hints}`];
70
- if (announcement)
71
- lines.push(` ${announcementLine(theme, announcement)}`);
70
+ if (input.announcement)
71
+ lines.push(` ${announcementLine(theme, input.announcement)}`);
72
72
  return [...lines, ""];
73
73
  }
74
74
  const rows = [...art];
@@ -78,8 +78,8 @@ export function renderBrandHeader(theme, version, cwd, width, notice, announceme
78
78
  rows[3] += `${gap}${dir}`;
79
79
  rows[4] += `${gap}${hints}`;
80
80
  // 公告放信息列末尾(最后一行字模旁),够醒目又不挤掉常规信息
81
- if (announcement)
82
- rows[5] += `${gap}${announcementLine(theme, announcement)}`;
81
+ if (input.announcement)
82
+ rows[5] += `${gap}${announcementLine(theme, input.announcement)}`;
83
83
  return ["", ...rows, ""];
84
84
  }
85
85
  export function printConsoleBanner(version) {