u1s1-cli 1.3.2 → 1.4.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.
@@ -32,6 +32,15 @@ export declare function writeWebToolsExtension(cfg: CliConfig, features: {
32
32
  webFetchRender: boolean;
33
33
  imageGen: boolean;
34
34
  }): void;
35
+ /**
36
+ * 生成「错误信息人话化」扩展到 <agentDir>/extensions/u1s1-error-humanize.js:
37
+ * 把「429: {json}」形态的模型调用错误翻成中文正文 + 状态码尾巴,TUI 和
38
+ * Desktop App 共用(同 compact-ui 的投影机制)。实现见 error-humanize.ts,
39
+ * 分类兼容性(重试/配额/上下文超长)的约束也写在那里。
40
+ * import 失败时静默跳过——Desktop 独立运行时若指向的 CLI dist 已不存在,
41
+ * 宁可回到原始错误文本,也不能让扩展报错刷英文堆栈。
42
+ */
43
+ export declare function writeErrorHumanizeExtension(): void;
35
44
  /**
36
45
  * 生成「OpenRouter 应用归因」扩展到 <agentDir>/extensions/u1s1-attribution.js。
37
46
  * 用户自己配 OPENROUTER_API_KEY 直连时,pi 内置的归因头归到 pi.dev 名下(且受遥测
@@ -235,6 +235,35 @@ export function writeWebToolsExtension(cfg, features) {
235
235
  subagentBlock +
236
236
  `}\n`);
237
237
  }
238
+ /**
239
+ * 生成「错误信息人话化」扩展到 <agentDir>/extensions/u1s1-error-humanize.js:
240
+ * 把「429: {json}」形态的模型调用错误翻成中文正文 + 状态码尾巴,TUI 和
241
+ * Desktop App 共用(同 compact-ui 的投影机制)。实现见 error-humanize.ts,
242
+ * 分类兼容性(重试/配额/上下文超长)的约束也写在那里。
243
+ * import 失败时静默跳过——Desktop 独立运行时若指向的 CLI dist 已不存在,
244
+ * 宁可回到原始错误文本,也不能让扩展报错刷英文堆栈。
245
+ */
246
+ export function writeErrorHumanizeExtension() {
247
+ const dir = join(agentDir, "extensions");
248
+ mkdirSync(dir, { recursive: true });
249
+ const url = new URL("./error-humanize.js", import.meta.url).href;
250
+ writeFileSync(join(dir, "u1s1-error-humanize.js"), `// 由 u1s1 每次启动自动生成,请勿手改\n` +
251
+ `export default async function (pi) {\n` +
252
+ ` let humanize;\n` +
253
+ ` try {\n` +
254
+ ` humanize = (await import(${JSON.stringify(url)})).humanizeModelError;\n` +
255
+ ` } catch {\n` +
256
+ ` return;\n` +
257
+ ` }\n` +
258
+ ` pi.on("message_end", (event) => {\n` +
259
+ ` const msg = event.message;\n` +
260
+ ` if (msg.role !== "assistant" || msg.stopReason !== "error" || !msg.errorMessage) return;\n` +
261
+ ` const friendly = humanize(msg.errorMessage);\n` +
262
+ ` if (!friendly) return;\n` +
263
+ ` return { message: { ...msg, errorMessage: friendly } };\n` +
264
+ ` });\n` +
265
+ `}\n`);
266
+ }
238
267
  /**
239
268
  * 生成「OpenRouter 应用归因」扩展到 <agentDir>/extensions/u1s1-attribution.js。
240
269
  * 用户自己配 OPENROUTER_API_KEY 直连时,pi 内置的归因头归到 pi.dev 名下(且受遥测
@@ -404,7 +433,8 @@ export function toProviderModels(models) {
404
433
  }
405
434
  return {
406
435
  id: m.id,
407
- name: m.name,
436
+ // 选择器只渲染 name 一行,把「免费/几倍」提示拼进去,切模型前就能看到
437
+ name: m.note ? `${m.name} · ${m.note}` : m.name,
408
438
  reasoning: m.reasoning,
409
439
  thinkingLevelMap,
410
440
  input: m.vision ? ["text", "image"] : ["text"],
package/dist/brand.d.ts CHANGED
@@ -18,5 +18,7 @@ export declare function renderBrandHeader(theme: Theme, input: {
18
18
  text: string;
19
19
  url?: string;
20
20
  };
21
+ /** 当前目录为空时给可照抄的开场 prompt(见 starterLines)。 */
22
+ starterTips?: boolean;
21
23
  }): string[];
22
24
  export declare function printConsoleBanner(version: string): void;
package/dist/brand.js CHANGED
@@ -47,6 +47,15 @@ function announcementLine(theme, a) {
47
47
  const link = a.url ? ` ${theme.fg("dim", a.url)}` : "";
48
48
  return `${theme.fg("accent", "📢")} ${theme.fg("accent", a.text)}${link}`;
49
49
  }
50
+ /** 空目录首跑引导:不让新手面对空白输入框想 prompt,给几句能照抄的开场白。 */
51
+ function starterLines(theme) {
52
+ return [
53
+ ` ${theme.fg("muted", "这个文件夹还是空的?把想做的直接说出来就行,比如:")}`,
54
+ ` ${theme.fg("text", "「做一个自我介绍网页,做完帮我发布出去」")}`,
55
+ ` ${theme.fg("text", "「做一个给朋友的生日祝福页面,要有点小动画」")}`,
56
+ ` ${theme.fg("text", "「写一个把文件夹里照片按日期重命名的小工具」")}`,
57
+ ];
58
+ }
50
59
  /**
51
60
  * Startup hero, responsive to terminal width:
52
61
  * wide → wordmark with info column beside it; medium → stacked; narrow → one-liner.
@@ -57,10 +66,13 @@ export function renderBrandHeader(theme, input) {
57
66
  const brand = theme.fg("muted", `${BRAND_CN} · ${BRAND_TAGLINE}`);
58
67
  const dir = theme.fg("dim", `cwd: ${formatHomePath(input.cwd)}`);
59
68
  const hints = theme.fg("dim", "/help · Ctrl+V 图片 · Shift+Enter 换行 · Esc 中断");
69
+ const starter = input.starterTips ? starterLines(theme) : [];
60
70
  if (input.width < ART_WIDTH + 4) {
61
71
  const lines = ["", ` ${theme.fg("accent", "✻")} ${name} ${theme.fg("muted", BRAND_CN)}`, ` ${dir}`, ` ${theme.fg("dim", "Ctrl+V 粘贴图片")}`];
62
72
  if (input.announcement)
63
73
  lines.push(` ${announcementLine(theme, input.announcement)}`);
74
+ if (starter.length)
75
+ lines.push("", ...starter);
64
76
  return [...lines, ""];
65
77
  }
66
78
  const art = HERO_ART.map((line) => ` ${paintArt(theme, line)}`);
@@ -69,6 +81,8 @@ export function renderBrandHeader(theme, input) {
69
81
  const lines = ["", ...art, "", ` ${name} ${brand}`, ` ${dir}`, ` ${hints}`];
70
82
  if (input.announcement)
71
83
  lines.push(` ${announcementLine(theme, input.announcement)}`);
84
+ if (starter.length)
85
+ lines.push("", ...starter);
72
86
  return [...lines, ""];
73
87
  }
74
88
  const rows = [...art];
@@ -80,6 +94,8 @@ export function renderBrandHeader(theme, input) {
80
94
  // 公告放信息列末尾(最后一行字模旁),够醒目又不挤掉常规信息
81
95
  if (input.announcement)
82
96
  rows[5] += `${gap}${announcementLine(theme, input.announcement)}`;
97
+ if (starter.length)
98
+ rows.push("", ...starter);
83
99
  return ["", ...rows, ""];
84
100
  }
85
101
  export function printConsoleBanner(version) {
package/dist/config.d.ts CHANGED
@@ -25,14 +25,18 @@ export interface ApiThinkingCapabilities {
25
25
  level_map: Record<string, string>;
26
26
  request_format: string;
27
27
  }
28
- interface ApiModelShape {
28
+ export interface ApiModel {
29
29
  id: string;
30
30
  name: string;
31
31
  reasoning: boolean;
32
+ /** Older gateways omit model-specific thinking metadata. */
32
33
  thinking?: ApiThinkingCapabilities | null;
34
+ /** Older gateways omit this field; missing means text-only. */
33
35
  vision?: boolean;
34
36
  context_length: number;
35
37
  max_tokens: number;
38
+ /** Older gateways omit this field; missing means unknown coverage. */
39
+ free_package_eligible?: boolean;
36
40
  price: {
37
41
  input: number;
38
42
  output: number;
@@ -43,7 +47,7 @@ interface ApiModelShape {
43
47
  * Convert an API model response to our internal ModelDef.
44
48
  * Aliases are derived from the short id; note is left empty (filled by /model command).
45
49
  */
46
- export declare function apiModelToDef(m: ApiModelShape): ModelDef;
50
+ export declare function apiModelToDef(m: ApiModel): ModelDef;
47
51
  export interface ModelDef {
48
52
  id: string;
49
53
  name: string;
@@ -62,6 +66,8 @@ export interface ModelDef {
62
66
  cacheRead: number;
63
67
  cacheWrite: number;
64
68
  };
69
+ /** 网关下发的免费用量包覆盖标记;老网关缺失时为 undefined(不派生 note)。 */
70
+ freeEligible?: boolean;
65
71
  note: string;
66
72
  }
67
73
  export declare const MODELS: ModelDef[];
@@ -158,4 +164,3 @@ export declare function saveConfig(cfg: CliConfig): void;
158
164
  export declare function persistPreferredModel(cfg: CliConfig, provider: string, modelId: string): CliConfig;
159
165
  export declare function saveEndpointsCache(endpoints: CustomEndpoint[]): void;
160
166
  export declare function loadEndpointsCache(): CustomEndpoint[];
161
- export {};
package/dist/config.js CHANGED
@@ -28,6 +28,7 @@ function defaultAliases(id) {
28
28
  const aliases = [short, ...parts.filter((p) => p.length > 1)];
29
29
  return [...new Set(aliases)];
30
30
  }
31
+ // 与 workers/gateway/src/model-thinking.ts 的 THINKING_LEVELS 是同一契约,改动必须双侧同步(gateway test/thinking-levels-contract.test.ts 钉死)。
31
32
  export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
32
33
  const THINKING_LEVEL_SET = new Set(THINKING_LEVELS);
33
34
  function parseThinkingCapabilities(value) {
@@ -75,6 +76,7 @@ export function apiModelToDef(m) {
75
76
  cacheRead: m.price.cache_read ?? 0,
76
77
  cacheWrite: 0,
77
78
  },
79
+ freeEligible: typeof m.free_package_eligible === "boolean" ? m.free_package_eligible : undefined,
78
80
  note: "",
79
81
  };
80
82
  }
@@ -109,8 +111,32 @@ export const MODELS = [
109
111
  note: "更强 · 但烧额度快约 20 倍,难题再用",
110
112
  },
111
113
  ];
114
+ /**
115
+ * 按免费包覆盖标记 + 相对默认免费模型的价格倍数派生一句人话提示。
116
+ * TUI 的模型选择器只渲染 name 一行,note 会被拼进去(见 toProviderModels),
117
+ * 让人切模型前就知道「这个不走免费包 / 大概贵多少」。
118
+ */
119
+ function deriveModelNote(m, base) {
120
+ if (m.freeEligible === true)
121
+ return "免费用量包可抵扣";
122
+ if (m.freeEligible === false) {
123
+ const blended = (m.cost.input + m.cost.output) / 2;
124
+ const baseBlended = base ? (base.cost.input + base.cost.output) / 2 : 0;
125
+ const mult = baseBlended > 0 ? Math.round(blended / baseBlended) : 0;
126
+ return mult >= 2
127
+ ? `不走免费包 · 费用约为默认模型 ${mult} 倍`
128
+ : "不走免费包,用余额或全模型包";
129
+ }
130
+ return "";
131
+ }
112
132
  /** Replace MODELS with a fresh list fetched from server (e.g. at startup). */
113
133
  export function setModelsFromApi(apiModels) {
134
+ const base = apiModels.find((m) => m.id === DEFAULT_MODEL_ID)
135
+ ?? apiModels.find((m) => m.freeEligible === true);
136
+ for (const m of apiModels) {
137
+ if (!m.note)
138
+ m.note = deriveModelNote(m, base);
139
+ }
114
140
  MODELS.length = 0;
115
141
  MODELS.push(...apiModels);
116
142
  }
package/dist/deploy.d.ts CHANGED
@@ -12,5 +12,6 @@ interface DeployArgs {
12
12
  visibility?: SiteVisibility;
13
13
  }
14
14
  export declare function parseDeployArgs(args: string[]): DeployArgs;
15
+ export declare function printDeployHelp(): void;
15
16
  export declare function deployCommand(cfg: CliConfig, args: string[]): Promise<void>;
16
17
  export {};
package/dist/deploy.js CHANGED
@@ -342,8 +342,22 @@ function printDeploymentResult(result, start) {
342
342
  console.log(" 改完代码再跑一次 u1s1 deploy 即可更新。");
343
343
  console.log("");
344
344
  }
345
+ export function printDeployHelp() {
346
+ console.log("");
347
+ console.log(" u1s1 deploy [目录] [--name 站点名] [--public|--private]");
348
+ console.log("");
349
+ console.log(" 不带参数时自动找构建产物目录(dist/build/out 等),否则用当前目录。");
350
+ console.log(" 首次发布会问项目名和公开/私密,之后记住;再跑一次即为更新。");
351
+ console.log("");
352
+ console.log(" u1s1 deploy list 查看已发布的站点");
353
+ console.log("");
354
+ }
345
355
  export async function deployCommand(cfg, args) {
346
356
  // 参数:[dir] [--name xxx] [--public|--private];u1s1 deploy list 列出已有站点
357
+ if (args.includes("--help") || args.includes("-h")) {
358
+ printDeployHelp();
359
+ return;
360
+ }
347
361
  if (args[0] === "list") {
348
362
  await listDeployments(cfg);
349
363
  return;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 模型调用失败时,网关的中文说明会被 OpenAI SDK + pi-ai 包成
3
+ * `429: {"message":"…","type":"…","code":"…"}` 直出到聊天区。
4
+ * 这里把 JSON 里的 message 提出来当正文,状态码与错误代号收进尾巴。
5
+ *
6
+ * 尾巴不是装饰,必须保留:pi 按 errorMessage 正则做三类分类——
7
+ * - 重试:429/5xx 数字、rate_limit、fetch failed 等(retry.js RETRYABLE)
8
+ * - 不重试:insufficient_quota(额度用尽时快速失败,不做无谓退避)
9
+ * - 上下文超长:context_length_exceeded(触发自动压缩而非重试)
10
+ * 改写后的文本必须与原文落进同一分类,否则会破坏重试/压缩行为。
11
+ *
12
+ * 网关会在错误 JSON 里注入 error.request_id(见 gateway request-id.ts),
13
+ * 拼进尾巴让用户报障时有编号可给,客服凭它直查日志和 usage 记录。
14
+ */
15
+ export declare function humanizeModelError(raw: string): string | undefined;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * 模型调用失败时,网关的中文说明会被 OpenAI SDK + pi-ai 包成
3
+ * `429: {"message":"…","type":"…","code":"…"}` 直出到聊天区。
4
+ * 这里把 JSON 里的 message 提出来当正文,状态码与错误代号收进尾巴。
5
+ *
6
+ * 尾巴不是装饰,必须保留:pi 按 errorMessage 正则做三类分类——
7
+ * - 重试:429/5xx 数字、rate_limit、fetch failed 等(retry.js RETRYABLE)
8
+ * - 不重试:insufficient_quota(额度用尽时快速失败,不做无谓退避)
9
+ * - 上下文超长:context_length_exceeded(触发自动压缩而非重试)
10
+ * 改写后的文本必须与原文落进同一分类,否则会破坏重试/压缩行为。
11
+ *
12
+ * 网关会在错误 JSON 里注入 error.request_id(见 gateway request-id.ts),
13
+ * 拼进尾巴让用户报障时有编号可给,客服凭它直查日志和 usage 记录。
14
+ */
15
+ export function humanizeModelError(raw) {
16
+ const jsonStart = raw.indexOf("{");
17
+ const jsonEnd = raw.lastIndexOf("}");
18
+ if (jsonStart === -1 || jsonEnd <= jsonStart)
19
+ return undefined;
20
+ let parsed;
21
+ try {
22
+ parsed = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));
23
+ }
24
+ catch {
25
+ return undefined;
26
+ }
27
+ if (parsed === null || typeof parsed !== "object")
28
+ return undefined;
29
+ const root = parsed;
30
+ const err = root["error"] !== null && typeof root["error"] === "object"
31
+ ? root["error"]
32
+ : root;
33
+ const message = typeof err["message"] === "string" ? err["message"].trim() : "";
34
+ if (!message)
35
+ return undefined;
36
+ const code = typeof err["code"] === "string" ? err["code"] : "";
37
+ const type = typeof err["type"] === "string" ? err["type"] : "";
38
+ const status = /(?:^|\s|\()(\d{3})\b/.exec(raw.slice(0, jsonStart))?.[1];
39
+ const requestId = typeof err["request_id"] === "string" ? err["request_id"] : "";
40
+ const tags = [
41
+ status ? `HTTP ${status}` : "",
42
+ type === "insufficient_quota" || code === "quota_exceeded" ? "insufficient_quota" : code,
43
+ requestId ? `请求编号 ${requestId}` : "",
44
+ ].filter(Boolean);
45
+ const friendly = tags.length ? `${message} (${tags.join(" · ")})` : message;
46
+ return friendly === raw ? undefined : friendly;
47
+ }
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync, spawnSync } from "node:child_process";
3
3
  import { writeFileSync } from "node:fs";
4
- import { cleanupBrandThemes, endpointKeyEnv, endpointProviderEntry, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, ensureWorkflowPromptTemplate, scrubForeignProviderEnv, toProviderModels, writeAttributionExtension, writeCompactUiExtension, writeWebToolsExtension, } from "./agent-setup.js";
4
+ import { cleanupBrandThemes, endpointKeyEnv, endpointProviderEntry, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, ensureWorkflowPromptTemplate, scrubForeignProviderEnv, toProviderModels, writeAttributionExtension, writeCompactUiExtension, writeErrorHumanizeExtension, writeWebToolsExtension, } from "./agent-setup.js";
5
+ import { Text } from "@earendil-works/pi-tui";
5
6
  import { printConsoleBanner } from "./brand.js";
6
7
  import { agentDir, apiModelToDef, CUSTOM_ENDPOINTS, isPortableInstall, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, readSettings, refValid, resolvePreferredModel, setModelsFromApi, VERSION, } from "./config.js";
7
8
  import { registerLoopCommand } from "./loop.js";
@@ -220,6 +221,8 @@ async function runAgent(cfg, args) {
220
221
  });
221
222
  // 精简 UI:隐藏工具调用 + 汇总行 + 隐藏思考标签(同样投影到 agentDir/extensions)
222
223
  writeCompactUiExtension();
224
+ // 模型调用错误人话化(同样投影,Desktop App 共享)
225
+ writeErrorHumanizeExtension();
223
226
  // OpenRouter 应用归因:直连流量计入 u1s1 的公开排行
224
227
  writeAttributionExtension();
225
228
  ensureTmuxKeyboardProtocol();
@@ -296,6 +299,41 @@ async function runAgent(cfg, args) {
296
299
  ctx.shutdown();
297
300
  },
298
301
  });
302
+ // /help:启动横幅和官网教程都在引导用户输入它,必须真实存在
303
+ pi.registerEntryRenderer("u1s1-help", (_entry, _opts, theme) => {
304
+ const text = [
305
+ theme.bold(theme.fg("text", "u1s1 常用操作")),
306
+ theme.fg("text", "直接打字说需求,回车发送 · Shift+Enter 换行 · Ctrl+V 贴图 · Esc 中断"),
307
+ theme.fg("text", "/model 切换模型(留意免费/价格提示) · /clear 清空上下文开新会话"),
308
+ theme.fg("text", "/usage 查剩余额度 · /resume 恢复历史会话 · /settings 设置"),
309
+ theme.fg("dim", "/compact 压缩上下文 · /hotkeys 全部快捷键(英文) · /exit 退出"),
310
+ theme.fg("dim", "退出后在终端:u1s1 deploy 发布网页 · u1s1 update 升级"),
311
+ theme.fg("dim", "新手教程 → https://u1s1.io/guides"),
312
+ ].join("\n");
313
+ return new Text(text, 1, 0);
314
+ });
315
+ pi.registerCommand("help", {
316
+ description: "查看常用操作和命令",
317
+ handler: async () => {
318
+ pi.appendEntry("u1s1-help");
319
+ },
320
+ });
321
+ // /usage:会话内查额度。此前额度只有用完撞 429 才可见(审计 G1)
322
+ pi.registerEntryRenderer("u1s1-usage", (entry, _opts, theme) => {
323
+ const data = entry.data ?? {};
324
+ if (data.error)
325
+ return new Text(theme.fg("dim", data.error), 1, 0);
326
+ return new Text((data.lines ?? []).map((l) => theme.fg("text", l)).join("\n"), 1, 0);
327
+ });
328
+ pi.registerCommand("usage", {
329
+ description: "查看剩余额度和本月用量",
330
+ handler: async () => {
331
+ const { usageEntryData } = await import("./usage.js");
332
+ pi.appendEntry("u1s1-usage", await usageEntryData());
333
+ },
334
+ });
335
+ // 模型调用错误人话化不在这里注册:走 writeErrorHumanizeExtension 投影,
336
+ // TUI 和 Desktop App 共用一份(见 agent-setup.ts)
299
337
  // /clear: 清空当前对话上下文,开始新会话
300
338
  pi.registerCommand("clear", {
301
339
  description: "清除当前对话上下文,开始新会话",
@@ -358,21 +396,23 @@ async function run() {
358
396
  console.log(`u1s1 v${VERSION}`);
359
397
  return;
360
398
  }
361
- if (cmd === "--help" || cmd === "-h") {
399
+ if (cmd === "--help" || cmd === "-h" || cmd === "help") {
362
400
  printConsoleBanner(VERSION);
363
- console.log(" u1s1 命令:deploy(发布网页,可选 --public / --private)· login / logout · model · usage · update · import · bench");
364
- console.log("");
365
401
  console.log(" 命令:");
402
+ console.log(" u1s1 进入对话界面(最常用,不带参数)");
403
+ console.log(" u1s1 -p \"一句话\" 不进界面直接回答,适合脚本里用");
366
404
  console.log(" u1s1 login / logout 登录 / 退出登录");
367
405
  console.log(" u1s1 model 查看或切换默认模型");
368
- console.log(" u1s1 usage 查看免费额度和余额");
406
+ console.log(" u1s1 usage 查看剩余额度");
369
407
  console.log(" u1s1 update 升级到最新版");
370
- console.log(" u1s1 deploy 发布网页,可选 --public / --private");
408
+ console.log(" u1s1 deploy 发布网页(--public / --private)");
409
+ console.log(" u1s1 deploy list 查看已发布的站点");
371
410
  console.log(" u1s1 import 导入历史会话");
411
+ console.log(" u1s1 bench 模型编码能力评测");
372
412
  console.log(" u1s1 --version 查看版本");
373
413
  console.log("");
374
- console.log(" 对话里输入 / 可以看会话内命令(/model /clear /exit …)");
375
- console.log(" 更多帮助 → https://u1s1.io/guide");
414
+ console.log(" 对话里输入 /help 看会话内命令(/model /clear /exit …)");
415
+ console.log(" 更多帮助 → https://u1s1.io/guides");
376
416
  return;
377
417
  }
378
418
  if (cmd === "web") {
@@ -381,9 +421,14 @@ async function run() {
381
421
  return;
382
422
  }
383
423
  if (cmd === "deploy") {
424
+ const { deployCommand, printDeployHelp } = await import("./deploy.js");
425
+ // --help 不该先被拽去登录
426
+ if (args.slice(1).includes("--help") || args.slice(1).includes("-h")) {
427
+ printDeployHelp();
428
+ return;
429
+ }
384
430
  const { ensureAuth } = await import("./login.js");
385
431
  const cfg = await ensureAuth();
386
- const { deployCommand } = await import("./deploy.js");
387
432
  await deployCommand(cfg, args.slice(1));
388
433
  return;
389
434
  }
@@ -447,6 +492,20 @@ async function run() {
447
492
  await benchCommand(args.slice(1));
448
493
  return;
449
494
  }
495
+ // 拼错的子命令(如 `u1s1 depoly`)不该被静默当成第一条消息发给模型白烧 token。
496
+ // 只拦「单个短英文词 + 与已知命令编辑距离很近」的情况,正常 prompt 不受影响。
497
+ if (cmd
498
+ && args.length === 1
499
+ && /^[a-z][a-z0-9-]{1,15}$/.test(cmd)
500
+ && !KNOWN_COMMANDS.includes(cmd)) {
501
+ const near = KNOWN_COMMANDS.find((k) => editDistance(k, cmd) <= (k.length <= 4 ? 1 : 2));
502
+ if (near) {
503
+ console.error(`不认识的命令「${cmd}」,你是不是想运行:u1s1 ${near}`);
504
+ console.error("查看全部命令:u1s1 --help;直接开始对话:运行 u1s1(不带参数)");
505
+ process.exitCode = 1;
506
+ return;
507
+ }
508
+ }
450
509
  const { ensureAuth } = await import("./login.js");
451
510
  const cfg = await ensureAuth();
452
511
  // 在后台检查更新(非阻塞,不影响启动速度)
@@ -455,6 +514,21 @@ async function run() {
455
514
  // autoUpdate 开着且启动时发现了新版:现在装(TUI 已退出,原地重铺安全)
456
515
  installPendingUpdate();
457
516
  }
517
+ const KNOWN_COMMANDS = ["deploy", "login", "logout", "usage", "model", "update", "import", "bench", "help", "web"];
518
+ /** 经典 Levenshtein,命令名都很短,O(nm) 足够。 */
519
+ function editDistance(a, b) {
520
+ const dp = Array.from({ length: a.length + 1 }, (_, i) => i);
521
+ for (let j = 1; j <= b.length; j++) {
522
+ let prev = dp[0];
523
+ dp[0] = j;
524
+ for (let i = 1; i <= a.length; i++) {
525
+ const cur = dp[i];
526
+ dp[i] = Math.min(dp[i] + 1, dp[i - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
527
+ prev = cur;
528
+ }
529
+ }
530
+ return dp[a.length];
531
+ }
458
532
  run().catch((e) => {
459
533
  console.error(e instanceof Error ? e.message : e);
460
534
  process.exit(1);
package/dist/login.d.ts CHANGED
@@ -10,8 +10,15 @@ export interface DeviceStart {
10
10
  private_jwk: webcrypto.JsonWebKey;
11
11
  public_jwk: webcrypto.JsonWebKey;
12
12
  }
13
- /** 发起浏览器设备登录;网关不支持或网络不可用时返回 null。 */
14
- export declare function startDeviceLogin(origin: string): Promise<DeviceStart | null>;
13
+ /**
14
+ * 发起浏览器设备登录;网关不支持或网络不可用时返回 null
15
+ * 服务器主动拒绝(限流/风控)时把原因带出到 failure —— 否则「请求太频繁」
16
+ * 会被误报成「检查网络」,用户怎么查网络都查不出问题。
17
+ */
18
+ export declare function startDeviceLogin(origin: string, failure?: {
19
+ status?: number;
20
+ message?: string;
21
+ }): Promise<DeviceStart | null>;
15
22
  export interface DeviceLoginResult {
16
23
  apiKey: string;
17
24
  deviceToken: string;
package/dist/login.js CHANGED
@@ -36,8 +36,12 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
36
36
  export function apiOrigin(cfg) {
37
37
  return cfg.baseUrl.replace(/\/v1\/?$/, "");
38
38
  }
39
- /** 发起浏览器设备登录;网关不支持或网络不可用时返回 null。 */
40
- export async function startDeviceLogin(origin) {
39
+ /**
40
+ * 发起浏览器设备登录;网关不支持或网络不可用时返回 null。
41
+ * 服务器主动拒绝(限流/风控)时把原因带出到 failure —— 否则「请求太频繁」
42
+ * 会被误报成「检查网络」,用户怎么查网络都查不出问题。
43
+ */
44
+ export async function startDeviceLogin(origin, failure) {
41
45
  try {
42
46
  const pair = await generateDeviceKeyPair();
43
47
  const resp = await fetch(`${origin}/auth/device/start`, {
@@ -51,7 +55,17 @@ export async function startDeviceLogin(origin) {
51
55
  signal: AbortSignal.timeout(15_000),
52
56
  });
53
57
  if (!resp.ok) {
54
- void resp.body?.cancel("device login start rejected").catch(() => { });
58
+ if (failure) {
59
+ failure.status = resp.status;
60
+ const body = await readJsonResponseCapped(resp, MAX_DEVICE_AUTH_RESPONSE_BYTES).catch(() => null);
61
+ const message = body?.error?.message;
62
+ if (typeof message === "string" && message && message.length <= 500) {
63
+ failure.message = message;
64
+ }
65
+ }
66
+ else {
67
+ void resp.body?.cancel("device login start rejected").catch(() => { });
68
+ }
55
69
  return null;
56
70
  }
57
71
  const data = await readJsonResponseCapped(resp, MAX_DEVICE_AUTH_RESPONSE_BYTES);
@@ -134,10 +148,19 @@ export async function login(keyArg) {
134
148
  }
135
149
  printConsoleBanner(VERSION);
136
150
  const origin = apiOrigin(cfg);
137
- const start = await startDeviceLogin(origin);
151
+ const failure = {};
152
+ const start = await startDeviceLogin(origin, failure);
138
153
  if (!start) {
139
- console.error(" 连不上 u1s1 服务器,请检查网络后重试。");
140
- console.error(" 如果网络正常,可能是 u1s1 版本太旧,运行 u1s1 update 升级后再试。");
154
+ if (failure.message) {
155
+ // 服务器有明确说法(限流/风控等),原样转达,别伪装成网络问题
156
+ console.error(` 服务器没有接受登录请求:${failure.message}`);
157
+ if (failure.status === 429)
158
+ console.error(" 稍等几分钟再运行 u1s1 login 重试即可。");
159
+ }
160
+ else {
161
+ console.error(" 连不上 u1s1 服务器,请检查网络后重试。");
162
+ console.error(" 如果网络正常,可能是 u1s1 版本太旧,运行 u1s1 update 升级后再试。");
163
+ }
141
164
  process.exit(1);
142
165
  }
143
166
  console.log(" 用浏览器登录并批准这台设备:");
package/dist/model.js CHANGED
@@ -14,6 +14,9 @@ export async function refreshOfficialModels(cfg, fetcher = fetchModels) {
14
14
  }
15
15
  }
16
16
  export async function modelCommand(nameOrAlias) {
17
+ // `u1s1 model --help` 不该被当成模型名报错;直接展示列表(自带切换说明)
18
+ if (nameOrAlias === "--help" || nameOrAlias === "-h")
19
+ nameOrAlias = undefined;
17
20
  const cfg = loadConfig();
18
21
  // Fetch both live catalogues before resolving the persisted default.
19
22
  const [officialModelsError] = await Promise.all([
package/dist/style.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { readdirSync } from "node:fs";
1
2
  import { basename } from "node:path";
2
3
  import { truncateToWidth } from "@earendil-works/pi-tui";
3
4
  import { readSettings } from "./config.js";
@@ -26,6 +27,15 @@ export function applyBrandUi(pi, version) {
26
27
  pi.on("session_start", async (_event, ctx) => {
27
28
  if (ctx.mode !== "tui")
28
29
  return;
30
+ // 空目录首跑:横幅下给几句可照抄的开场 prompt(dotfile 不算「有内容」)
31
+ const starterTips = (() => {
32
+ try {
33
+ return readdirSync(process.cwd()).filter((n) => !n.startsWith(".")).length === 0;
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ })();
29
39
  // 检查设置:关掉就不显示启动横幅
30
40
  const settings = readSettings();
31
41
  if (settings.showStartupBanner !== false) {
@@ -40,6 +50,7 @@ export function applyBrandUi(pi, version) {
40
50
  width,
41
51
  notice: updateNotice,
42
52
  announcement,
53
+ starterTips,
43
54
  }).map((line) => truncateToWidth(line, width));
44
55
  },
45
56
  invalidate() { },
package/dist/usage.d.ts CHANGED
@@ -1 +1,9 @@
1
+ import { type MeResponse } from "./api.js";
2
+ /** 额度报告正文(不含首尾空行),终端 `u1s1 usage` 和会话内 /usage 共用。 */
3
+ export declare function usageReportLines(me: MeResponse): string[];
4
+ /** 会话内 /usage 用:拉取额度并打包成 entry data,错误转成中文提示而不是抛出。 */
5
+ export declare function usageEntryData(): Promise<{
6
+ lines?: string[];
7
+ error?: string;
8
+ }>;
1
9
  export declare function usage(): Promise<void>;
package/dist/usage.js CHANGED
@@ -26,6 +26,7 @@ const PACKAGE_LABELS = {
26
26
  invite: "邀请赠送",
27
27
  new_user: "新用户赠送",
28
28
  login_checkin: "登录打卡",
29
+ login_checkin_bonus: "打卡加成",
29
30
  payment_delay_gift: "临时加量包",
30
31
  topup_daily: "每日加量包",
31
32
  admin_grant: "官方赠送",
@@ -61,58 +62,82 @@ function packageLabel(pkg) {
61
62
  const count = pkg.count > 1 ? ` ×${pkg.count}` : "";
62
63
  return `${PACKAGE_LABELS[pkg.kind] ?? pkg.kind}${count}`.padEnd(5, " ");
63
64
  }
64
- function printPackage(pkg) {
65
+ function packageLines(pkg) {
65
66
  const isDaily = pkg.daily_tokens != null;
66
67
  const total = isDaily ? (pkg.daily_tokens ?? 0) : (pkg.total_tokens ?? 0);
67
68
  const ratio = total > 0 ? pkg.remaining / total : 0;
68
- console.log(` ${packageLabel(pkg)} 还剩 ${fmtTokensCn(pkg.remaining)} / ${fmtTokensCn(total)}${isDaily ? "/天" : ""} ${bar(ratio)}`);
69
69
  const expiry = pkg.expires_at ? `${pkg.expires_at.slice(0, 10)} 到期` : "永不过期";
70
- console.log(` ${packageScopeNote(pkg)} · ${expiry}`);
70
+ return [
71
+ ` ${packageLabel(pkg)} 还剩 ${fmtTokensCn(pkg.remaining)} / ${fmtTokensCn(total)}${isDaily ? "/天" : ""} ${bar(ratio)}`,
72
+ ` ${packageScopeNote(pkg)} · ${expiry}`,
73
+ ];
71
74
  }
72
- function printPackageUsage(me, tokensPerUsd) {
75
+ function packageUsageLines(me, tokensPerUsd) {
73
76
  const packages = me.packages ?? [];
77
+ const lines = [];
74
78
  if (packages.length === 0)
75
- console.log(" 用量包 (无生效中的用量包)");
79
+ lines.push(" 用量包 (无生效中的用量包)");
76
80
  for (const pkg of groupPackages(packages))
77
- printPackage(pkg);
81
+ lines.push(...packageLines(pkg));
78
82
  if (me.bonus_balance_usd > 0) {
79
83
  const balance = tokensPerUsd > 0
80
84
  ? `${fmtTokensCn(me.bonus_balance_usd * tokensPerUsd)} Token`
81
85
  : `$${me.bonus_balance_usd.toFixed(2)}`;
82
- console.log(` 余额(按量) ${balance}`);
86
+ lines.push(` 余额(按量) ${balance}`);
83
87
  }
84
88
  const monthlyUsage = tokensPerUsd > 0
85
89
  ? `约 ${fmtTokensCn(me.mtd_usd * tokensPerUsd)} Token($${me.mtd_usd.toFixed(2)})`
86
90
  : `$${me.mtd_usd.toFixed(2)}`;
87
- console.log(` 本月已用 ${monthlyUsage}`);
88
- console.log("");
89
- console.log(" 免费用量包覆盖 DeepSeek V4 Flash / Vision、GLM-5.3 Flash、Qwen3.8 Flash 和联网搜索。");
90
- console.log(" DeepSeek V4 Pro 仅可使用余额 → https://u1s1.io/dashboard");
91
- console.log(" Token 数按默认模型单价折算,为约数;用更贵的模型时消耗更快。");
92
- console.log("");
91
+ lines.push(` 本月已用 ${monthlyUsage}`);
92
+ lines.push("");
93
+ lines.push(" 免费用量包覆盖 DeepSeek V4 Flash / Vision、GLM-5.3 Flash、Qwen3.8 Flash 和联网搜索。");
94
+ lines.push(" 其他模型(如 DeepSeek V4 Pro)不走免费包,可用余额或全模型包 → https://u1s1.io/dashboard");
95
+ lines.push(" Token 数按默认模型单价折算,为约数;用更贵的模型时消耗更快。");
96
+ return lines;
93
97
  }
94
- function printLegacyUsage(me, tokensPerUsd) {
98
+ function legacyUsageLines(me, tokensPerUsd) {
95
99
  const freeRemain = me.daily_free_remaining_usd;
96
100
  const freeTotal = me.daily_free_usd;
97
101
  const freeRatio = freeTotal > 0 ? freeRemain / freeTotal : 0;
102
+ const lines = [];
98
103
  if (tokensPerUsd > 0) {
99
104
  const tokens = (usd) => `${fmtTokensCn(usd * tokensPerUsd)} Token`;
100
- console.log(` 今日免费 还剩 ${fmtTokensCn(freeRemain * tokensPerUsd)} / ${tokens(freeTotal)} ${bar(freeRatio)}`);
101
- console.log(" DeepSeek V4 Flash · 北京时间 0 点恢复");
102
- console.log(` 永久余额 ${tokens(me.remaining_usd)}`);
103
- console.log(` 本月已用 约 ${tokens(me.mtd_usd)}($${me.mtd_usd.toFixed(2)})`);
104
- console.log("");
105
- console.log(" Token 数按默认模型单价折算,为约数;用更贵的模型时消耗更快。");
105
+ lines.push(` 今日免费 还剩 ${fmtTokensCn(freeRemain * tokensPerUsd)} / ${tokens(freeTotal)} ${bar(freeRatio)}`);
106
+ lines.push(" DeepSeek V4 Flash · 北京时间 0 点恢复");
107
+ lines.push(` 永久余额 ${tokens(me.remaining_usd)}`);
108
+ lines.push(` 本月已用 约 ${tokens(me.mtd_usd)}($${me.mtd_usd.toFixed(2)})`);
109
+ lines.push("");
110
+ lines.push(" Token 数按默认模型单价折算,为约数;用更贵的模型时消耗更快。");
106
111
  }
107
112
  else {
108
- console.log(` 今日免费 $${freeRemain.toFixed(2)} / $${freeTotal.toFixed(2)} ${bar(freeRatio)}`);
109
- console.log(" DeepSeek V4 Flash · 北京时间 0 点恢复");
110
- console.log(` 永久余额 $${me.remaining_usd.toFixed(2)}`);
111
- console.log(` 本月成本 $${me.mtd_usd.toFixed(2)}`);
113
+ lines.push(` 今日免费 $${freeRemain.toFixed(2)} / $${freeTotal.toFixed(2)} ${bar(freeRatio)}`);
114
+ lines.push(" DeepSeek V4 Flash · 北京时间 0 点恢复");
115
+ lines.push(` 永久余额 $${me.remaining_usd.toFixed(2)}`);
116
+ lines.push(` 本月成本 $${me.mtd_usd.toFixed(2)}`);
117
+ }
118
+ lines.push("");
119
+ lines.push(" 免费额度每天自动恢复;邀请朋友双方各得永久加量 → https://u1s1.io/dashboard");
120
+ return lines;
121
+ }
122
+ /** 额度报告正文(不含首尾空行),终端 `u1s1 usage` 和会话内 /usage 共用。 */
123
+ export function usageReportLines(me) {
124
+ const tokensPerUsd = me.tokens_per_usd ?? 0;
125
+ return [
126
+ ` 账号 ${me.email ?? "(未绑定邮箱)"}`,
127
+ ...(me.packages ? packageUsageLines(me, tokensPerUsd) : legacyUsageLines(me, tokensPerUsd)),
128
+ ];
129
+ }
130
+ /** 会话内 /usage 用:拉取额度并打包成 entry data,错误转成中文提示而不是抛出。 */
131
+ export async function usageEntryData() {
132
+ const cfg = loadConfig();
133
+ if (!hasDeviceCredential(cfg))
134
+ return { error: "还没登录,先在终端运行 u1s1 login" };
135
+ try {
136
+ return { lines: usageReportLines(await fetchMe(cfg)) };
137
+ }
138
+ catch (e) {
139
+ return { error: e instanceof Error ? e.message : String(e) };
112
140
  }
113
- console.log("");
114
- console.log(" 免费额度每天自动恢复;邀请朋友双方各得永久加量 → https://u1s1.io/dashboard");
115
- console.log("");
116
141
  }
117
142
  export async function usage() {
118
143
  const cfg = loadConfig();
@@ -124,12 +149,8 @@ export async function usage() {
124
149
  console.error(e.message);
125
150
  process.exit(1);
126
151
  });
127
- const tokensPerUsd = me.tokens_per_usd ?? 0;
128
152
  console.log("");
129
- console.log(` 账号 ${me.email ?? "(未绑定邮箱)"}`);
130
- if (me.packages) {
131
- printPackageUsage(me, tokensPerUsd);
132
- return;
133
- }
134
- printLegacyUsage(me, tokensPerUsd);
153
+ for (const line of usageReportLines(me))
154
+ console.log(line);
155
+ console.log("");
135
156
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u1s1-cli",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "u1s1 — 有一说一,最省心的 AI 编程搭子。终端里用中文说需求,AI 帮你读文件、改代码、跑命令。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -76,6 +76,58 @@ const COMPACT_UI_REPLACEMENTS = [
76
76
  ],
77
77
  ];
78
78
 
79
+ // 内置斜杠命令描述汉化(dist/core/slash-commands.js),与 pnpm coding-agent patch 同步。
80
+ const SLASH_COMMANDS_REPLACEMENTS = [
81
+ ['{ name: "settings", description: "Open settings menu" },', '{ name: "settings", description: "打开设置菜单" },'],
82
+ ['{ name: "model", description: "Select model (opens selector UI)", argumentHint: "<provider/model>" },', '{ name: "model", description: "切换模型(列表里有免费/价格提示)", argumentHint: "<provider/model>" },'],
83
+ ['{ name: "tree", description: "Navigate session tree (switch branches)" },', '{ name: "tree", description: "查看会话分支树,切换分支" },'],
84
+ ['{ name: "thinking", description: "Set thinking level", argumentHint: "<level>" },', '{ name: "thinking", description: "调整思考强度(想得越深越费额度)", argumentHint: "<level>" },'],
85
+ ['{ name: "scoped-models", description: "Enable/disable models for Ctrl+P cycling" },', '{ name: "scoped-models", description: "设置 Ctrl+P 轮换的模型范围" },'],
86
+ ['{ name: "export", description: "Export session (HTML default, or specify path: .html/.jsonl)" },', '{ name: "export", description: "导出会话(默认 HTML,也可指定 .html/.jsonl 路径)" },'],
87
+ ['{ name: "import", description: "Import and resume a session from a JSONL file" },', '{ name: "import", description: "从 JSONL 文件导入并继续会话" },'],
88
+ ['{ name: "share", description: "Share session as a secret GitHub gist" },', '{ name: "share", description: "把会话分享为 GitHub 私密 gist" },'],
89
+ ['{ name: "copy", description: "Copy last agent message to clipboard" },', '{ name: "copy", description: "复制上一条回复到剪贴板" },'],
90
+ ['{ name: "name", description: "Set session display name" },', '{ name: "name", description: "给当前会话起个名字" },'],
91
+ ['{ name: "session", description: "Show session info and stats" },', '{ name: "session", description: "查看会话信息与统计" },'],
92
+ ['{ name: "changelog", description: "Show changelog entries" },', '{ name: "changelog", description: "查看引擎更新日志(英文)" },'],
93
+ ['{ name: "hotkeys", description: "Show all keyboard shortcuts" },', '{ name: "hotkeys", description: "查看全部快捷键(英文)" },'],
94
+ ['{ name: "fork", description: "Create a new fork from a previous user message" },', '{ name: "fork", description: "从之前某条消息分叉出一个新会话" },'],
95
+ ['{ name: "clone", description: "Duplicate the current session at the current position" },', '{ name: "clone", description: "复制当前会话再继续" },'],
96
+ ['{ name: "trust", description: "Save project trust decision for future sessions" },', '{ name: "trust", description: "记住对这个项目目录的信任选择" },'],
97
+ ['{ name: "login", description: "Configure provider authentication", argumentHint: "<provider>" },', '{ name: "login", description: "换 u1s1 账号请先 /exit,再运行 u1s1 login", argumentHint: "<provider>" },'],
98
+ ['{ name: "logout", description: "Remove provider authentication" },', '{ name: "logout", description: "退出登录请先 /exit,再运行 u1s1 logout" },'],
99
+ ['{ name: "new", description: "Start a new session" },', '{ name: "new", description: "开始新会话" },'],
100
+ ['{ name: "compact", description: "Manually compact the session context" },', '{ name: "compact", description: "压缩会话上下文,腾出空间继续聊" },'],
101
+ ['{ name: "resume", description: "Resume a different session" },', '{ name: "resume", description: "恢复或切换到其他会话" },'],
102
+ ['{ name: "reload", description: "Reload keybindings, extensions, skills, prompts, themes, and context files" },', '{ name: "reload", description: "重新加载扩展、技能、主题等资源" },'],
103
+ ['{ name: "quit", description: `Quit ${APP_NAME}` },', '{ name: "quit", description: `退出 ${APP_NAME}` },'],
104
+ ];
105
+
106
+ function patchSlashCommands() {
107
+ for (const piDir of findPackageDirs("@earendil-works/pi-coding-agent")) {
108
+ const target = join(piDir, "dist", "core", "slash-commands.js");
109
+ try {
110
+ let text = readFileSync(target, "utf8");
111
+ if (text.includes("打开设置菜单")) continue; // pnpm patch 或此前运行已应用
112
+
113
+ let applied = 0;
114
+ for (const [from, to] of SLASH_COMMANDS_REPLACEMENTS) {
115
+ if (!text.includes(from)) continue;
116
+ text = text.split(from).join(to);
117
+ applied++;
118
+ }
119
+ if (applied < SLASH_COMMANDS_REPLACEMENTS.length) {
120
+ console.log(
121
+ `[u1s1] 提示: pi 版本可能已更新,命令描述汉化只应用了 ${applied}/${SLASH_COMMANDS_REPLACEMENTS.length} 处(不影响使用)`,
122
+ );
123
+ }
124
+ writeFileSync(target, text);
125
+ } catch {
126
+ continue;
127
+ }
128
+ }
129
+ }
130
+
79
131
  function patchCompactUi() {
80
132
  const piDirs = findPackageDirs("@earendil-works/pi-coding-agent");
81
133
  if (piDirs.length === 0) {
@@ -166,6 +218,7 @@ const ENABLE_AUTOWRAP = "\\x1b[?7h";`,
166
218
 
167
219
  try {
168
220
  patchCompactUi();
221
+ patchSlashCommands();
169
222
  patchMainScreenAutowrap();
170
223
  } catch (err) {
171
224
  console.log(`[u1s1] 提示: pi 运行时补丁未生效(${err?.message ?? err}),不影响安装`);