u1s1-cli 1.2.3 → 1.2.5

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.
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import { dirname, join } from "node:path";
3
+ import { join, win32 } from "node:path";
4
4
  import { agentDir, agentSettingsFile } from "./config.js";
5
5
  /** System32/Sysnative 的 bash.exe 是 WSL 转发器,不是真 bash(与 pi 同款判断)。 */
6
6
  function isWslRelayBash(p) {
@@ -91,21 +91,38 @@ function knownGitBashPaths() {
91
91
  }
92
92
  return out;
93
93
  }
94
- /** 慢路径候选:where git.exe 反推安装根,兜住 portable 等非常规安装。 */
95
- async function discoveredGitBashPaths(skip) {
94
+ /** git.exe 路径反推 Git Bash 候选;用 win32 保证非 Windows CI 也能覆盖路径规则。 */
95
+ export function gitBashPathsFromGitExecutables(gitPaths, skip, pathExists = existsSync) {
96
96
  const out = [];
97
97
  // git.exe 在 <root>\cmd、<root>\bin 或 <root>\mingw64\bin 下,向上找 root
98
- for (const gitPath of await runWhere("git.exe")) {
99
- const dir = dirname(gitPath);
100
- for (const root of [dirname(dir), dirname(dirname(dir))]) {
101
- for (const p of [join(root, "bin", "bash.exe"), join(root, "usr", "bin", "bash.exe")]) {
102
- if (!out.includes(p) && !skip.includes(p) && existsSync(p))
98
+ for (const gitPath of gitPaths) {
99
+ const dir = win32.dirname(gitPath);
100
+ for (const root of [win32.dirname(dir), win32.dirname(win32.dirname(dir))]) {
101
+ for (const p of [win32.join(root, "bin", "bash.exe"), win32.join(root, "usr", "bin", "bash.exe")]) {
102
+ if (!out.includes(p) && !skip.includes(p) && pathExists(p))
103
103
  out.push(p);
104
104
  }
105
105
  }
106
106
  }
107
107
  return out;
108
108
  }
109
+ /** 慢路径候选:where git.exe 反推安装根,兜住 portable 等非常规安装。 */
110
+ async function discoveredGitBashPaths(skip) {
111
+ return gitBashPathsFromGitExecutables(await runWhere("git.exe"), skip);
112
+ }
113
+ /** 固定位置与 PATH 反推共用同一验活流程,缓存 WSL 和首次探测都走这里。 */
114
+ async function findUsableGitBash() {
115
+ const known = knownGitBashPaths();
116
+ for (const candidate of known) {
117
+ if (await bashWorks(candidate))
118
+ return candidate;
119
+ }
120
+ for (const candidate of await discoveredGitBashPaths(known)) {
121
+ if (await bashWorks(candidate))
122
+ return candidate;
123
+ }
124
+ return undefined;
125
+ }
109
126
  /** 持久化写 settings.json:写入时现读现改,避免覆盖并行流程刚写的其他键。 */
110
127
  function persistShellPath(p) {
111
128
  let settings = {};
@@ -139,17 +156,16 @@ export async function ensureUsableShell() {
139
156
  }
140
157
  }
141
158
  // 已配置且文件还在:信它,不每次启动都探活(坏了 pi 会在会话里报错)。
142
- // 配置的是 WSL 转发器时,零成本看一眼 Git Bash 是否新装了,装了就换轨。
159
+ // 配置的是 WSL 转发器时,同时检查固定位置与 PATH 里的 Git 安装,找到就换轨。
143
160
  const configured = settings["shellPath"];
144
161
  if (typeof configured === "string" && configured && existsSync(configured)) {
145
162
  if (!isWslRelayBash(configured))
146
163
  return { status: "bash", shellPath: configured };
147
- for (const candidate of knownGitBashPaths()) {
148
- if (await bashWorks(candidate)) {
149
- persistShellPath(candidate);
150
- console.error(` 检测到 Git Bash,命令改回 Windows 本机执行: ${candidate}`);
151
- return { status: "bash", shellPath: candidate };
152
- }
164
+ const candidate = await findUsableGitBash();
165
+ if (candidate) {
166
+ persistShellPath(candidate);
167
+ console.error(` 检测到 Git Bash,命令改回 Windows 本机执行: ${candidate}`);
168
+ return { status: "bash", shellPath: candidate };
153
169
  }
154
170
  adviseInstallGitBash();
155
171
  return { status: "wsl", shellPath: configured };
@@ -163,20 +179,14 @@ export async function ensureUsableShell() {
163
179
  }
164
180
  return { status: "bash", shellPath: candidate };
165
181
  };
166
- const known = knownGitBashPaths();
167
- for (const candidate of known) {
168
- if (await bashWorks(candidate))
169
- return adopt(candidate);
170
- }
171
- for (const candidate of await discoveredGitBashPaths(known)) {
172
- if (await bashWorks(candidate))
173
- return adopt(candidate);
174
- }
182
+ const gitBash = await findUsableGitBash();
183
+ if (gitBash)
184
+ return adopt(gitBash);
175
185
  // 没有 Git Bash:看 PATH 上还有什么(与 pi 的兜底一致,但这里要验活)
176
186
  const fallback = (await runWhere("bash.exe")).find((p) => existsSync(p));
177
187
  if (fallback && (await bashWorks(fallback))) {
178
188
  // WSL 转发器也持久化:冷启 WSL 验活要好几秒,不能每次启动都付。装了
179
- // Git Bash 后的换轨由上面的 configured-WSL 分支负责(零成本 existsSync)。
189
+ // Git Bash 后的换轨由上面的 configured-WSL 分支负责。
180
190
  persistShellPath(fallback);
181
191
  if (isWslRelayBash(fallback)) {
182
192
  adviseInstallGitBash();
package/dist/style.js CHANGED
@@ -34,7 +34,13 @@ export function applyBrandUi(pi, version) {
34
34
  return {
35
35
  render(width) {
36
36
  // pi-tui crashes on lines wider than the terminal, so truncate defensively.
37
- return renderBrandHeader(theme, version, process.cwd(), width, updateNotice, announcement).map((line) => truncateToWidth(line, width));
37
+ return renderBrandHeader(theme, {
38
+ version,
39
+ cwd: process.cwd(),
40
+ width,
41
+ notice: updateNotice,
42
+ announcement,
43
+ }).map((line) => truncateToWidth(line, width));
38
44
  },
39
45
  invalidate() { },
40
46
  };
@@ -41,4 +41,9 @@ export declare function runSubagent(opts: SubagentOptions): Promise<SubagentOutc
41
41
  * 固定并发的工作池:limit 个工人依次领任务跑完一个补位一个。
42
42
  * worker 抛错由调用方在 fn 内部兜住;signal 中止后不再领新任务。
43
43
  */
44
- export declare function runPool<T>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<void>, signal?: AbortSignal): Promise<void>;
44
+ export declare function runPool<T>(input: {
45
+ items: readonly T[];
46
+ limit: number;
47
+ run: (item: T, index: number) => Promise<void>;
48
+ signal?: AbortSignal;
49
+ }): Promise<void>;
package/dist/subagent.js CHANGED
@@ -119,15 +119,15 @@ export async function runSubagent(opts) {
119
119
  * 固定并发的工作池:limit 个工人依次领任务跑完一个补位一个。
120
120
  * worker 抛错由调用方在 fn 内部兜住;signal 中止后不再领新任务。
121
121
  */
122
- export async function runPool(items, limit, fn, signal) {
122
+ export async function runPool(input) {
123
123
  let next = 0;
124
124
  const worker = async () => {
125
125
  for (;;) {
126
126
  const i = next++;
127
- if (i >= items.length || signal?.aborted)
127
+ if (i >= input.items.length || input.signal?.aborted)
128
128
  return;
129
- await fn(items[i], i);
129
+ await input.run(input.items[i], i);
130
130
  }
131
131
  };
132
- await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, worker));
132
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(input.limit, input.items.length)) }, worker));
133
133
  }
package/dist/tools.d.ts CHANGED
@@ -4,18 +4,24 @@ import { Type } from "typebox";
4
4
  import type { CliConfig } from "./config.js";
5
5
  export declare function truncate(text: string): string;
6
6
  /** 联网工具通用渲染:调用行不占位,收起时只显一行摘要,ctrl+o 展开全文,出错显一行 ✗ */
7
- export declare function compactResultRender(result: {
8
- content: Array<{
9
- type: string;
10
- text?: string;
11
- }>;
12
- }, options: {
13
- expanded: boolean;
14
- }, theme: {
15
- fg: (color: any, text: string) => string;
16
- }, context: {
17
- isError: boolean;
18
- }, summaryLine: string): Text;
7
+ export declare function compactResultRender(input: {
8
+ result: {
9
+ content: Array<{
10
+ type: string;
11
+ text?: string;
12
+ }>;
13
+ };
14
+ options: {
15
+ expanded: boolean;
16
+ };
17
+ theme: {
18
+ fg: (color: any, text: string) => string;
19
+ };
20
+ context: {
21
+ isError: boolean;
22
+ };
23
+ summaryLine: string;
24
+ }): Text;
19
25
  /** 联网搜索工具:走 u1s1 网关代理,上游 key 不落到用户机器上。 */
20
26
  export declare function createSearchTool(cfg: Pick<CliConfig, "baseUrl" | "apiKey">): import("@earendil-works/pi-coding-agent").ToolDefinition<Type.TObject<{
21
27
  query: Type.TString;
package/dist/tools.js CHANGED
@@ -88,10 +88,8 @@ function resultText(result) {
88
88
  return c && c.type === "text" ? (c.text ?? "") : "";
89
89
  }
90
90
  /** 联网工具通用渲染:调用行不占位,收起时只显一行摘要,ctrl+o 展开全文,出错显一行 ✗ */
91
- export function compactResultRender(result, options,
92
- // 放宽成 any 只为避开 pi ThemeColor 联合类型在结构化匹配时的逆变报错;
93
- // 实际传入的就是 pi 的 Theme,取值只用 "warning"/"muted" 两色
94
- theme, context, summaryLine) {
91
+ export function compactResultRender(input) {
92
+ const { result, options, theme, context, summaryLine } = input;
95
93
  if (context.isError) {
96
94
  // 低调处理:✗ 用警示色,内容用灰色弱化(与内置工具的错误展示一致)
97
95
  const msg = previewLine(resultText(result) || "error", 160);
@@ -123,14 +121,15 @@ export function createSearchTool(cfg) {
123
121
  renderCall() {
124
122
  return new Text("", 0, 0);
125
123
  },
126
- renderResult(result, options, theme, context) {
124
+ renderResult(...args) {
125
+ const [result, options, theme, context] = args;
127
126
  const d = result.details;
128
127
  const q = d?.query ? `"${previewLine(d.query, 80)}"` : "…";
129
128
  const n = typeof d?.count === "number" ? ` · ${d.count} 条结果` : "";
130
- return compactResultRender(result, options, theme, context, `🔍 搜索 ${q}${n}`);
129
+ return compactResultRender({ result, options, theme, context, summaryLine: `🔍 搜索 ${q}${n}` });
131
130
  },
132
131
  async execute(_toolCallId, params, signal) {
133
- const data = await searchWeb(cfg, params.query, params.maxResults, signal);
132
+ const data = await searchWeb(cfg, { query: params.query, maxResults: params.maxResults, signal });
134
133
  const lines = [];
135
134
  if (data.answer)
136
135
  lines.push(`Answer: ${data.answer}`, "");
@@ -187,14 +186,15 @@ export function createSubagentTool(getParentModel) {
187
186
  renderCall() {
188
187
  return new Text("", 0, 0);
189
188
  },
190
- renderResult(result, options, theme, context) {
189
+ renderResult(...args) {
190
+ const [result, options, theme, context] = args;
191
191
  const d = result.details;
192
192
  const summary = d && typeof d.count === "number"
193
193
  ? `🤖 子任务 ×${d.count}` +
194
194
  (typeof d.ok === "number" && d.failed ? ` · ✓${d.ok} ✗${d.failed}` : "") +
195
195
  (typeof d.seconds === "number" ? ` · ${d.seconds}s` : "")
196
196
  : "🤖 子任务";
197
- return compactResultRender(result, options, theme, context, summary);
197
+ return compactResultRender({ result, options, theme, context, summaryLine: summary });
198
198
  },
199
199
  async execute(_toolCallId, params, signal) {
200
200
  const list = (params.tasks?.length ? params.tasks : params.task ? [params.task] : [])
@@ -210,11 +210,16 @@ export function createSubagentTool(getParentModel) {
210
210
  const prevFlag = process.env.U1S1_IN_SUBAGENT;
211
211
  process.env.U1S1_IN_SUBAGENT = "1";
212
212
  const started = Date.now();
213
- const outcomes = new Array(list.length);
213
+ const outcomes = Array.from({ length: list.length });
214
214
  try {
215
- await runPool(list, SUBAGENT_CONCURRENCY, async (task, i) => {
216
- outcomes[i] = await runSubagent({ task, parentModel, model: params.model, timeoutMs, signal }).catch((e) => ({ ok: false, text: e.message, usage: { totalTokens: 0, costUsd: 0 } }));
217
- }, signal);
215
+ await runPool({
216
+ items: list,
217
+ limit: SUBAGENT_CONCURRENCY,
218
+ run: async (task, i) => {
219
+ outcomes[i] = await runSubagent({ task, parentModel, model: params.model, timeoutMs, signal }).catch((e) => ({ ok: false, text: e.message, usage: { totalTokens: 0, costUsd: 0 } }));
220
+ },
221
+ signal,
222
+ });
218
223
  }
219
224
  finally {
220
225
  if (prevFlag === undefined)
@@ -458,12 +463,13 @@ export function createFetchTool(cfg) {
458
463
  renderCall() {
459
464
  return new Text("", 0, 0);
460
465
  },
461
- renderResult(result, options, theme, context) {
466
+ renderResult(...args) {
467
+ const [result, options, theme, context] = args;
462
468
  const d = result.details;
463
469
  const u = previewLine(d?.url ?? "…", 90);
464
470
  const n = typeof d?.chars === "number" ? ` · ${d.chars} 字符` : "";
465
471
  const cloud = d?.via === "render" ? " · ☁ 浏览器渲染" : "";
466
- return compactResultRender(result, options, theme, context, `🌐 ${u}${n}${cloud}`);
472
+ return compactResultRender({ result, options, theme, context, summaryLine: `🌐 ${u}${n}${cloud}` });
467
473
  },
468
474
  async execute(_toolCallId, params, signal) {
469
475
  let url;
@@ -492,7 +498,7 @@ export function createFetchTool(cfg) {
492
498
  return ok(await renderViaGateway(url, signal), "render", "text/markdown");
493
499
  }
494
500
  catch (re) {
495
- throw new Error(`${e.message};云端浏览器渲染也失败: ${re.message}`);
501
+ throw new Error(`${e.message};云端浏览器渲染也失败: ${re.message}`, { cause: re });
496
502
  }
497
503
  }
498
504
  // 直连 200 但正文近乎空:大概率是 JS 渲染的 SPA,换浏览器再试;渲染失败就退回空壳
package/dist/usage.js CHANGED
@@ -20,89 +20,91 @@ function fmtTokensCn(tokens) {
20
20
  return sig2(t / 1e4).toLocaleString("en-US") + " 万";
21
21
  return sig2(t).toLocaleString("en-US");
22
22
  }
23
- export async function usage() {
24
- const cfg = loadConfig();
25
- if (!hasDeviceCredential(cfg)) {
26
- console.error("还没登录,先跑 u1s1 login");
27
- process.exit(1);
23
+ const PACKAGE_LABELS = {
24
+ free_first: "首月免费包",
25
+ free_yearly: "年度免费包",
26
+ invite: "邀请赠送",
27
+ new_user: "新用户赠送",
28
+ login_checkin: "登录打卡",
29
+ payment_delay_gift: "临时加量包",
30
+ topup_daily: "每日加量包",
31
+ admin_grant: "官方赠送",
32
+ };
33
+ function sumOptional(a, b) {
34
+ return a == null && b == null ? null : (a ?? 0) + (b ?? 0);
35
+ }
36
+ function groupPackages(packages) {
37
+ const groups = new Map();
38
+ for (const pkg of packages) {
39
+ const key = `${pkg.kind}|${pkg.scope}|${pkg.daily_tokens != null}|${pkg.expires_at || ""}`;
40
+ const group = groups.get(key);
41
+ if (!group) {
42
+ groups.set(key, { ...pkg, count: 1 });
43
+ continue;
44
+ }
45
+ group.count += 1;
46
+ group.daily_tokens = sumOptional(group.daily_tokens, pkg.daily_tokens);
47
+ group.total_tokens = sumOptional(group.total_tokens, pkg.total_tokens);
48
+ group.remaining += pkg.remaining;
28
49
  }
29
- const me = await fetchMe(cfg).catch((e) => {
30
- console.error(e.message);
31
- process.exit(1);
32
- });
50
+ return [...groups.values()];
51
+ }
52
+ function packageScopeNote(pkg) {
53
+ if (pkg.kind === "login_checkin") {
54
+ return "仅限 u1s1 客户端使用 · 全模型可用";
55
+ }
56
+ return pkg.scope === "free"
57
+ ? "免费包适用模型 · 0 点恢复"
58
+ : "仅限 u1s1 客户端使用 · 免费包适用模型";
59
+ }
60
+ function packageLabel(pkg) {
61
+ const count = pkg.count > 1 ? ` ×${pkg.count}` : "";
62
+ return `${PACKAGE_LABELS[pkg.kind] ?? pkg.kind}${count}`.padEnd(5, " ");
63
+ }
64
+ function printPackage(pkg) {
65
+ const isDaily = pkg.daily_tokens != null;
66
+ const total = isDaily ? (pkg.daily_tokens ?? 0) : (pkg.total_tokens ?? 0);
67
+ const ratio = total > 0 ? pkg.remaining / total : 0;
68
+ console.log(` ${packageLabel(pkg)} 还剩 ${fmtTokensCn(pkg.remaining)} / ${fmtTokensCn(total)}${isDaily ? "/天" : ""} ${bar(ratio)}`);
69
+ const expiry = pkg.expires_at ? `${pkg.expires_at.slice(0, 10)} 到期` : "永不过期";
70
+ console.log(` ${packageScopeNote(pkg)} · ${expiry}`);
71
+ }
72
+ function printPackageUsage(me, tokensPerUsd) {
73
+ const packages = me.packages ?? [];
74
+ if (packages.length === 0)
75
+ console.log(" 用量包 (无生效中的用量包)");
76
+ for (const pkg of groupPackages(packages))
77
+ printPackage(pkg);
78
+ if (me.bonus_balance_usd > 0) {
79
+ const balance = tokensPerUsd > 0
80
+ ? `${fmtTokensCn(me.bonus_balance_usd * tokensPerUsd)} Token`
81
+ : `$${me.bonus_balance_usd.toFixed(2)}`;
82
+ console.log(` 余额(按量) ${balance}`);
83
+ }
84
+ const monthlyUsage = tokensPerUsd > 0
85
+ ? `约 ${fmtTokensCn(me.mtd_usd * tokensPerUsd)} Token($${me.mtd_usd.toFixed(2)})`
86
+ : `$${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("");
93
+ }
94
+ function printLegacyUsage(me, tokensPerUsd) {
33
95
  const freeRemain = me.daily_free_remaining_usd;
34
96
  const freeTotal = me.daily_free_usd;
35
97
  const freeRatio = freeTotal > 0 ? freeRemain / freeTotal : 0;
36
- const tpu = me.tokens_per_usd ?? 0;
37
- console.log("");
38
- console.log(` 账号 ${me.email ?? "(未绑定邮箱)"}`);
39
- if (me.packages) {
40
- // 新网关:按用量包逐个展示,token 原生数字
41
- const PKG_LABEL = {
42
- free_first: "首月免费包",
43
- free_yearly: "年度免费包",
44
- invite: "邀请赠送",
45
- new_user: "新用户赠送",
46
- payment_delay_gift: "临时加量包",
47
- topup_daily: "每日加量包",
48
- admin_grant: "官方赠送",
49
- };
50
- if (me.packages.length === 0)
51
- console.log(" 用量包 (无生效中的用量包)");
52
- // 同种类、同范围、同有效期的包合并成一条展示(比如邀请多个好友拿到的多份邀请赠送)
53
- const groups = new Map();
54
- for (const p of me.packages) {
55
- const key = `${p.kind}|${p.scope}|${p.daily_tokens != null}|${p.expires_at || ""}`;
56
- const g = groups.get(key);
57
- // 可空字段求和:两边都是 null 就保持 null,别把「一次性包没有日额度」污染成 0
58
- const sumOpt = (a, b) => (a == null && b == null ? null : (a ?? 0) + (b ?? 0));
59
- if (g) {
60
- g.count += 1;
61
- g.daily_tokens = sumOpt(g.daily_tokens, p.daily_tokens);
62
- g.total_tokens = sumOpt(g.total_tokens, p.total_tokens);
63
- g.remaining += p.remaining;
64
- }
65
- else {
66
- groups.set(key, { ...p, count: 1 });
67
- }
68
- }
69
- for (const p of groups.values()) {
70
- const daily = p.daily_tokens != null;
71
- const total = daily ? (p.daily_tokens ?? 0) : (p.total_tokens ?? 0);
72
- const ratio = total > 0 ? p.remaining / total : 0;
73
- const label = ((PKG_LABEL[p.kind] ?? p.kind) + (p.count > 1 ? ` ×${p.count}` : "")).padEnd(5, " ");
74
- const per = daily ? "/天" : "";
75
- console.log(` ${label} 还剩 ${fmtTokensCn(p.remaining)} / ${fmtTokensCn(total)}${per} ${bar(ratio)}`);
76
- const scopeNote = p.kind === "login_checkin"
77
- ? "仅限 u1s1 客户端使用 · 全模型可用"
78
- : p.scope === "free"
79
- ? "免费包适用模型 · 0 点恢复"
80
- : "仅限 u1s1 客户端使用 · 免费包适用模型";
81
- console.log(` ${scopeNote} · ${p.expires_at ? `${p.expires_at.slice(0, 10)} 到期` : "永不过期"}`);
82
- }
83
- if (me.bonus_balance_usd > 0) {
84
- const balText = tpu > 0 ? `${fmtTokensCn(me.bonus_balance_usd * tpu)} Token` : `$${me.bonus_balance_usd.toFixed(2)}`;
85
- console.log(` 余额(按量) ${balText}`);
86
- }
87
- console.log(` 本月已用 ${tpu > 0 ? `约 ${fmtTokensCn(me.mtd_usd * tpu)} Token($${me.mtd_usd.toFixed(2)})` : `$${me.mtd_usd.toFixed(2)}`}`);
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("");
93
- return;
94
- }
95
- if (tpu > 0) {
96
- const tok = (usd) => `${fmtTokensCn(usd * tpu)} Token`;
97
- console.log(` 今日免费 还剩 ${fmtTokensCn(freeRemain * tpu)} / ${tok(freeTotal)} ${bar(freeRatio)}`);
98
+ if (tokensPerUsd > 0) {
99
+ const tokens = (usd) => `${fmtTokensCn(usd * tokensPerUsd)} Token`;
100
+ console.log(` 今日免费 还剩 ${fmtTokensCn(freeRemain * tokensPerUsd)} / ${tokens(freeTotal)} ${bar(freeRatio)}`);
98
101
  console.log(" DeepSeek V4 Flash · 北京时间 0 点恢复");
99
- console.log(` 永久余额 ${tok(me.remaining_usd)}`);
100
- console.log(` 本月已用 约 ${tok(me.mtd_usd)}($${me.mtd_usd.toFixed(2)})`);
102
+ console.log(` 永久余额 ${tokens(me.remaining_usd)}`);
103
+ console.log(` 本月已用 约 ${tokens(me.mtd_usd)}($${me.mtd_usd.toFixed(2)})`);
101
104
  console.log("");
102
105
  console.log(" Token 数按默认模型单价折算,为约数;用更贵的模型时消耗更快。");
103
106
  }
104
107
  else {
105
- // 老网关没下发 tokens_per_usd,退回金额显示
106
108
  console.log(` 今日免费 $${freeRemain.toFixed(2)} / $${freeTotal.toFixed(2)} ${bar(freeRatio)}`);
107
109
  console.log(" DeepSeek V4 Flash · 北京时间 0 点恢复");
108
110
  console.log(` 永久余额 $${me.remaining_usd.toFixed(2)}`);
@@ -112,3 +114,22 @@ export async function usage() {
112
114
  console.log(" 免费额度每天自动恢复;邀请朋友双方各得永久加量 → https://u1s1.io/dashboard");
113
115
  console.log("");
114
116
  }
117
+ export async function usage() {
118
+ const cfg = loadConfig();
119
+ if (!hasDeviceCredential(cfg)) {
120
+ console.error("还没登录,先跑 u1s1 login");
121
+ process.exit(1);
122
+ }
123
+ const me = await fetchMe(cfg).catch((e) => {
124
+ console.error(e.message);
125
+ process.exit(1);
126
+ });
127
+ const tokensPerUsd = me.tokens_per_usd ?? 0;
128
+ console.log("");
129
+ console.log(` 账号 ${me.email ?? "(未绑定邮箱)"}`);
130
+ if (me.packages) {
131
+ printPackageUsage(me, tokensPerUsd);
132
+ return;
133
+ }
134
+ printLegacyUsage(me, tokensPerUsd);
135
+ }
@@ -1,9 +1,2 @@
1
- /**
2
- * u1s1 自有前端(packages/webui,Codex 布局复刻)整体替换 pi-web-ui 的
3
- * web/dist。产物随 CLI 包发布在 <cliPkg>/webui-dist;index.html 内容一致
4
- * 说明已同步过,跳过。目标目录先整体删除再拷贝 —— 断开 pnpm 硬链接,
5
- * 也顺带清掉上游的旧 assets。返回 false 表示产物缺失(回退到品牌补丁)。
6
- */
7
- export declare function applyWebUiFrontend(binPath: string): boolean;
8
1
  /** binPath = <pkg>/bin/pi-web-ui.mjs → 前端产物在 <pkg>/web/dist。 */
9
2
  export declare function applyWebUiBranding(binPath: string): void;
@@ -1,33 +1,5 @@
1
- import { cpSync, existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- /**
5
- * u1s1 自有前端(packages/webui,Codex 布局复刻)整体替换 pi-web-ui 的
6
- * web/dist。产物随 CLI 包发布在 <cliPkg>/webui-dist;index.html 内容一致
7
- * 说明已同步过,跳过。目标目录先整体删除再拷贝 —— 断开 pnpm 硬链接,
8
- * 也顺带清掉上游的旧 assets。返回 false 表示产物缺失(回退到品牌补丁)。
9
- */
10
- export function applyWebUiFrontend(binPath) {
11
- const src = fileURLToPath(new URL("../webui-dist/", import.meta.url));
12
- const srcIndex = join(src, "index.html");
13
- if (!existsSync(srcIndex))
14
- return false;
15
- const dist = join(dirname(binPath), "..", "web", "dist");
16
- const distIndex = join(dist, "index.html");
17
- try {
18
- if (existsSync(distIndex) &&
19
- readFileSync(distIndex, "utf8") === readFileSync(srcIndex, "utf8")) {
20
- return true;
21
- }
22
- rmSync(dist, { recursive: true, force: true });
23
- cpSync(src, dist, { recursive: true });
24
- return true;
25
- }
26
- catch (e) {
27
- console.error(" 前端替换失败,退回默认界面:", e.message);
28
- return false;
29
- }
30
- }
31
3
  /**
32
4
  * pi-web-ui 前端产物的启动时品牌补丁。不 fork 上游,只在 Desktop App
33
5
  * 启动前改它 web/dist 里的静态文件:重写 <title>、换 favicon、注入
@@ -41,9 +13,9 @@ export function applyWebUiFrontend(binPath) {
41
13
  */
42
14
  const PATCH_MARK = "u1s1-brand-v2";
43
15
  const PAGE_TITLE = "u1s1 Desktop App — 有一说一";
44
- const FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
45
- <rect width="64" height="64" rx="14" fill="#101418"/>
46
- <text x="32" y="42" font-family="ui-monospace,Menlo,Consolas,monospace" font-size="26" font-weight="700" fill="#4ea1ff" text-anchor="middle">u1</text>
16
+ const FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
17
+ <rect width="100" height="100" rx="18" fill="#e8442e"/>
18
+ <text x="50" y="68" font-family="ui-monospace,Menlo,Consolas,monospace" font-size="52" font-weight="700" fill="#faf5ec" text-anchor="middle">u1</text>
47
19
  </svg>
48
20
  `;
49
21
  /** 页面 chrome 品牌替换:改 .brand-logo/.brand-name,盯住 title(React 可能改回去)。 */
@@ -189,12 +189,12 @@ export async function runWorkflow(opts) {
189
189
  store.append({ key, task: task.slice(0, 120), ok: true, text: out.text, ms: Date.now() - t0 });
190
190
  return out.text;
191
191
  }
192
- catch (e) {
192
+ catch (error) {
193
193
  stats.failed++;
194
194
  progress.failed = stats.failed;
195
- const msg = e.message || "未知错误";
195
+ const msg = error.message || "未知错误";
196
196
  store.append({ key, task: task.slice(0, 120), ok: false, text: msg, ms: Date.now() - t0 });
197
- throw new Error(msg);
197
+ throw new Error(msg, { cause: error });
198
198
  }
199
199
  finally {
200
200
  if (worktreeDir)
@@ -221,15 +221,20 @@ export async function runWorkflow(opts) {
221
221
  async function parallel(thunks) {
222
222
  if (!Array.isArray(thunks))
223
223
  throw new Error("parallel() 需要一个 thunk 数组");
224
- const results = new Array(thunks.length);
225
- await runPool(thunks, SUBAGENT_CONCURRENCY, async (thunk, i) => {
226
- results[i] = await Promise.resolve()
227
- .then(thunk)
228
- .catch((e) => {
229
- logs.push(`✗ 任务 ${i + 1} 失败:${e.message}`);
230
- return null;
231
- });
232
- }, internal.signal);
224
+ const results = [];
225
+ await runPool({
226
+ items: thunks,
227
+ limit: SUBAGENT_CONCURRENCY,
228
+ run: async (thunk, i) => {
229
+ results[i] = await Promise.resolve()
230
+ .then(thunk)
231
+ .catch((e) => {
232
+ logs.push(`✗ 任务 ${i + 1} 失败:${e.message}`);
233
+ return null;
234
+ });
235
+ },
236
+ signal: internal.signal,
237
+ });
233
238
  return results;
234
239
  }
235
240
  /** 无屏障流水线:每个 item 独立穿过所有 stage,item 失败只废自己。 */
@@ -238,21 +243,26 @@ export async function runWorkflow(opts) {
238
243
  throw new Error("pipeline() 第一个参数需要数组");
239
244
  if (!Array.isArray(stages) || stages.length === 0)
240
245
  throw new Error("pipeline() 第二个参数需要非空 stage 数组");
241
- const results = new Array(items.length);
242
- await runPool(items, SUBAGENT_CONCURRENCY, async (item, i) => {
243
- let current = item;
244
- for (let s = 0; s < stages.length; s++) {
245
- try {
246
- current = await stages[s](current);
247
- }
248
- catch (e) {
249
- logs.push(`✗ item ${i + 1} 在第 ${s + 1} 阶段失败:${e.message}`);
250
- current = null;
251
- break;
246
+ const results = [];
247
+ await runPool({
248
+ items: items,
249
+ limit: SUBAGENT_CONCURRENCY,
250
+ run: async (item, i) => {
251
+ let current = item;
252
+ for (let s = 0; s < stages.length; s++) {
253
+ try {
254
+ current = await stages[s](current);
255
+ }
256
+ catch (e) {
257
+ logs.push(`✗ item ${i + 1} 在第 ${s + 1} 阶段失败:${e.message}`);
258
+ current = null;
259
+ break;
260
+ }
252
261
  }
253
- }
254
- results[i] = current;
255
- }, internal.signal);
262
+ results[i] = current;
263
+ },
264
+ signal: internal.signal,
265
+ });
256
266
  return results;
257
267
  }
258
268
  // 沙箱:只有编排原语 + 受限 log/timer,标准 JS 内建(Promise/JSON/Math…)天然可用