u1s1-cli 1.4.3 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -12,6 +12,8 @@ import { applyBrandUi, setAnnouncement, setUpdateNotice } from "./style.js";
12
12
  import { AuthError, fetchModels, loadCustomEndpoints, readJsonResponseCapped } from "./api.js";
13
13
  import { ensureSigningProxy } from "./device-auth.js";
14
14
  import { installChildEnvGuard } from "./secret-env.js";
15
+ import { DEPLOY_HINT_TEXT, markNudge, shouldShowDeployHint } from "./nudges.js";
16
+ import { createGatewayTelemetry } from "./telemetry.js";
15
17
  const PACKAGE_NAME = "u1s1-cli";
16
18
  /** 启动时检测到的可自动安装的新版;TUI 退出后才装(见 installPendingUpdate)。 */
17
19
  let pendingUpdate;
@@ -257,6 +259,26 @@ async function runAgent(cfg, args) {
257
259
  scrubForeignProviderEnv();
258
260
  // 密钥只留在本进程:模型跑的 shell 命令、子 agent、扩展派生的任何子进程都拿不到
259
261
  installChildEnvGuard();
262
+ // 客户端事件上报(见 telemetry.ts):状态放在 runAgent 作用域,/clear、/resume
263
+ // 会重建扩展运行时、重跑下面的 factory,但一个 TUI 进程只算一个会话
264
+ const telemetry = createGatewayTelemetry(cfg);
265
+ const sessionStartedAt = Date.now();
266
+ const stats = { turns: 0, toolCalls: 0, filesWritten: 0, writesThisRun: 0 };
267
+ let firstToolSuccessSent = false;
268
+ let firstTaskDoneSent = false;
269
+ let sessionEndSent = false;
270
+ telemetry.track("session_start", { surface: "terminal" });
271
+ const trackSessionEnd = () => {
272
+ if (sessionEndSent)
273
+ return;
274
+ sessionEndSent = true;
275
+ telemetry.track("session_end", {
276
+ duration_ms: Date.now() - sessionStartedAt,
277
+ turns: stats.turns,
278
+ tool_calls: stats.toolCalls,
279
+ files_written: stats.filesWritten,
280
+ });
281
+ };
260
282
  await searchToolsReady;
261
283
  const { CustomEditor, main } = await agentReady;
262
284
  // 网页终端(如 Taikula)把 Shift+Enter 发成 ESC+CR(\x1b\r)。pi 在没开 Kitty
@@ -326,8 +348,10 @@ async function runAgent(cfg, args) {
326
348
  theme.fg("text", "直接打字说需求,回车发送 · Shift+Enter 换行 · Ctrl+V 贴图 · Esc 中断"),
327
349
  theme.fg("text", "/model 切换模型(留意免费/价格提示) · /clear 清空上下文开新会话"),
328
350
  theme.fg("text", "/usage 查剩余额度 · /resume 恢复历史会话 · /settings 设置"),
329
- theme.fg("dim", "/compact 压缩上下文 · /hotkeys 全部快捷键(英文) · /exit 退出"),
351
+ theme.fg("text", "/feedback 一句话反馈问题或建议(自动附带版本与请求编号建工单)"),
352
+ theme.fg("dim", "/compact 压缩上下文 · /hotkeys 全部快捷键 · /exit 退出"),
330
353
  theme.fg("dim", "退出后在终端:u1s1 deploy 发布网页 · u1s1 update 升级"),
354
+ theme.fg("dim", "匿名使用统计只记会话开始/结束等节点,不含任何内容;U1S1_TELEMETRY=0 可关闭"),
331
355
  theme.fg("dim", "新手教程 → https://u1s1.io/guides"),
332
356
  ].join("\n");
333
357
  return new Text(text, 1, 0);
@@ -352,6 +376,75 @@ async function runAgent(cfg, args) {
352
376
  pi.appendEntry("u1s1-usage", await usageEntryData());
353
377
  },
354
378
  });
379
+ // /feedback:会话内直接建工单,附版本/平台/最近请求编号(运营清单 E4)
380
+ pi.registerEntryRenderer("u1s1-feedback", (entry, _opts, theme) => {
381
+ const data = entry.data ?? {};
382
+ return new Text((data.lines ?? []).map((l) => theme.fg("text", l)).join("\n"), 1, 0);
383
+ });
384
+ pi.registerCommand("feedback", {
385
+ description: "反馈问题或建议(自动附带诊断信息建工单)",
386
+ handler: async (cmdArgs, ctx) => {
387
+ const { feedbackInSession, parseFeedbackArgs } = await import("./feedback.js");
388
+ let raw = cmdArgs.trim();
389
+ let parsed;
390
+ try {
391
+ parsed = parseFeedbackArgs(raw.split(/\s+/).filter(Boolean));
392
+ }
393
+ catch (e) {
394
+ pi.appendEntry("u1s1-feedback", { lines: [e instanceof Error ? e.message : String(e)] });
395
+ return;
396
+ }
397
+ if (!parsed.help && !parsed.text && ctx.hasUI) {
398
+ const text = (await ctx.ui.input("反馈内容", "一句话说清楚问题或建议"))?.trim();
399
+ if (!text)
400
+ return;
401
+ raw = `--${parsed.category} ${text}`;
402
+ }
403
+ const model = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
404
+ pi.appendEntry("u1s1-feedback", { lines: await feedbackInSession(raw, model) });
405
+ },
406
+ });
407
+ // ---- 事件上报 + 首次写文件后的 deploy 提示 ----
408
+ pi.on("agent_start", () => {
409
+ stats.writesThisRun = 0;
410
+ });
411
+ pi.on("turn_end", () => {
412
+ stats.turns++;
413
+ });
414
+ pi.on("tool_result", (event) => {
415
+ stats.toolCalls++;
416
+ if (event.isError)
417
+ return;
418
+ if (!firstToolSuccessSent) {
419
+ firstToolSuccessSent = true;
420
+ telemetry.track("first_tool_success", { tool: event.toolName });
421
+ }
422
+ if (event.toolName === "write" || event.toolName === "edit") {
423
+ stats.filesWritten++;
424
+ stats.writesThisRun++;
425
+ }
426
+ });
427
+ pi.on("agent_end", (_event, ctx) => {
428
+ if (stats.writesThisRun === 0)
429
+ return;
430
+ if (!firstTaskDoneSent) {
431
+ firstTaskDoneSent = true;
432
+ telemetry.track("first_task_done", { files_written: stats.filesWritten });
433
+ }
434
+ // 每台机器只提示一次;跑过 u1s1 deploy 后永不再提(见 nudges.ts)
435
+ if (ctx.hasUI && shouldShowDeployHint()) {
436
+ markNudge("deploy_hint_shown");
437
+ ctx.ui.notify(DEPLOY_HINT_TEXT, "info");
438
+ }
439
+ });
440
+ // pi 在 session_shutdown 处理完后直接 process.exit,session_end 只能在这里
441
+ // 发并等冲刷(最多 1 秒);/clear、/resume 的 reason 不是 quit,不算会话结束
442
+ pi.on("session_shutdown", async (event) => {
443
+ if (event.reason !== "quit")
444
+ return;
445
+ trackSessionEnd();
446
+ await telemetry.shutdown();
447
+ });
355
448
  // 模型调用错误人话化不在这里注册:走 writeErrorHumanizeExtension 投影,
356
449
  // TUI 和 Desktop App 共用一份(见 agent-setup.ts)
357
450
  // /clear: 清空当前对话上下文,开始新会话
@@ -408,7 +501,13 @@ async function runAgent(cfg, args) {
408
501
  persistPreferredModel(cfg, pref.provider, pref.id);
409
502
  }
410
503
  const finalArgs = hasModelArg ? args : ["--model", `${pref.provider}/${pref.id}`, ...args];
411
- await main(finalArgs, { extensionFactories: extension });
504
+ try {
505
+ await main(finalArgs, { extensionFactories: extension });
506
+ }
507
+ finally {
508
+ trackSessionEnd();
509
+ await telemetry.shutdown();
510
+ }
412
511
  }
413
512
  async function run() {
414
513
  const args = process.argv.slice(2);
@@ -428,11 +527,14 @@ async function run() {
428
527
  console.log(" u1s1 update 升级到最新版");
429
528
  console.log(" u1s1 deploy 发布网页(--public / --private)");
430
529
  console.log(" u1s1 deploy list 查看已发布的站点");
530
+ console.log(" u1s1 deploy remove 删除已发布的站点");
531
+ console.log(" u1s1 feedback \"一句话\" 反馈问题或建议(自动建工单,--bug/--question…)");
431
532
  console.log(" u1s1 import 导入历史会话");
432
533
  console.log(" u1s1 bench 模型编码能力评测");
433
534
  console.log(" u1s1 --version 查看版本");
434
535
  console.log("");
435
- console.log(" 对话里输入 /help 看会话内命令(/model /clear /exit …)");
536
+ console.log(" 对话里输入 /help 看会话内命令(/model /clear /feedback /exit …)");
537
+ console.log(" 匿名使用统计只记会话开始/结束等节点,不含内容;设 U1S1_TELEMETRY=0 关闭");
436
538
  console.log(" 更多帮助 → https://u1s1.io/guides");
437
539
  return;
438
540
  }
@@ -463,6 +565,11 @@ async function run() {
463
565
  await usage();
464
566
  return;
465
567
  }
568
+ if (cmd === "feedback") {
569
+ const { feedbackCommand } = await import("./feedback.js");
570
+ await feedbackCommand(args.slice(1));
571
+ return;
572
+ }
466
573
  if (cmd === "model") {
467
574
  const { modelCommand } = await import("./model.js");
468
575
  await modelCommand(args[1]);
@@ -535,7 +642,7 @@ async function run() {
535
642
  // autoUpdate 开着且启动时发现了新版:现在装(TUI 已退出,原地重铺安全)
536
643
  installPendingUpdate();
537
644
  }
538
- const KNOWN_COMMANDS = ["deploy", "login", "logout", "usage", "model", "update", "import", "bench", "help", "web"];
645
+ const KNOWN_COMMANDS = ["deploy", "login", "logout", "usage", "model", "update", "import", "bench", "feedback", "help", "web"];
539
646
  /** 经典 Levenshtein,命令名都很短,O(nm) 足够。 */
540
647
  function editDistance(a, b) {
541
648
  const dp = Array.from({ length: a.length + 1 }, (_, i) => i);
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 「只提示一次」的机器级标记,落在 ~/.u1s1/nudges.json,CLI 与桌面版共用
3
+ * (桌面版经 u1s1-cli/embed 拿到同一份 helper)。值是 ISO 时间戳,便于事后
4
+ * 排查「什么时候提示过」。读写都是尽力而为:文件坏了当作没提示过,写失败
5
+ * 最多是多提示一次,绝不能因为它抛错影响主流程。
6
+ */
7
+ export type NudgeKey =
8
+ /** 首次写文件后打印过「u1s1 deploy 一键上线」提示 */
9
+ "deploy_hint_shown"
10
+ /** 用户成功跑过一次 u1s1 deploy(之后不再推 deploy) */
11
+ | "deploy_done"
12
+ /** 桌面版首启引导已看过 */
13
+ | "desktop_onboarding_done";
14
+ export type NudgeState = Partial<Record<NudgeKey, string>>;
15
+ export declare const nudgesFile: string;
16
+ export declare function readNudges(file?: string): NudgeState;
17
+ export declare function hasNudge(key: NudgeKey, file?: string): boolean;
18
+ export declare function markNudge(key: NudgeKey, file?: string): void;
19
+ /** 会话里首次写/改文件后要不要提示 deploy:没提示过、也没部署过才提示。 */
20
+ export declare function shouldShowDeployHint(file?: string): boolean;
21
+ export declare const DEPLOY_HINT_TEXT = "\u505A\u597D\u4E86\u60F3\u7ED9\u670B\u53CB\u770B\uFF1F\u8FD0\u884C u1s1 deploy \u4E00\u952E\u4E0A\u7EBF";
package/dist/nudges.js ADDED
@@ -0,0 +1,40 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { u1s1Dir } from "./config.js";
4
+ export const nudgesFile = join(u1s1Dir, "nudges.json");
5
+ export function readNudges(file = nudgesFile) {
6
+ try {
7
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
8
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
9
+ return {};
10
+ const out = {};
11
+ for (const [k, v] of Object.entries(parsed)) {
12
+ if (typeof v === "string")
13
+ out[k] = v;
14
+ }
15
+ return out;
16
+ }
17
+ catch {
18
+ return {};
19
+ }
20
+ }
21
+ export function hasNudge(key, file = nudgesFile) {
22
+ return readNudges(file)[key] !== undefined;
23
+ }
24
+ export function markNudge(key, file = nudgesFile) {
25
+ try {
26
+ mkdirSync(join(file, ".."), { recursive: true });
27
+ const all = readNudges(file);
28
+ all[key] = new Date().toISOString();
29
+ writeFileSync(file, JSON.stringify(all, null, 2) + "\n");
30
+ }
31
+ catch {
32
+ // ignore
33
+ }
34
+ }
35
+ /** 会话里首次写/改文件后要不要提示 deploy:没提示过、也没部署过才提示。 */
36
+ export function shouldShowDeployHint(file = nudgesFile) {
37
+ const state = readNudges(file);
38
+ return state.deploy_hint_shown === undefined && state.deploy_done === undefined;
39
+ }
40
+ export const DEPLOY_HINT_TEXT = "做好了想给朋友看?运行 u1s1 deploy 一键上线";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 终端里问一行:非 TTY(脚本/管道)直接返回默认值,不挂住。
3
+ * deploy 问站点名、feedback 问反馈内容都走这里。
4
+ */
5
+ export declare function askLine(question: string, def?: string): Promise<string>;
package/dist/prompt.js ADDED
@@ -0,0 +1,17 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ /**
3
+ * 终端里问一行:非 TTY(脚本/管道)直接返回默认值,不挂住。
4
+ * deploy 问站点名、feedback 问反馈内容都走这里。
5
+ */
6
+ export async function askLine(question, def = "") {
7
+ if (!process.stdin.isTTY)
8
+ return def;
9
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
10
+ try {
11
+ const answer = (await rl.question(question)).trim();
12
+ return answer || def;
13
+ }
14
+ finally {
15
+ rl.close();
16
+ }
17
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 记住最近一次模型调用报错带回的 request_id(网关在错误 JSON 里注入,见
3
+ * error-humanize.ts),`u1s1 feedback` / 会话内 /feedback 建工单时附进诊断
4
+ * 快照,客服凭它直查日志。进程内先记内存;同时尽力落到 ~/.u1s1/last-request.json,
5
+ * 让退出会话后再跑 `u1s1 feedback` 的用户也能带上编号。超过 24 小时的记录
6
+ * 不再当「最近」。
7
+ */
8
+ export declare const lastRequestFile: string;
9
+ export declare function rememberRequestId(id: string | undefined, file?: string): void;
10
+ export declare function lastRequestId(file?: string, now?: number): string | undefined;
11
+ /** 测试用:清掉进程内缓存。 */
12
+ export declare function resetRequestTraceForTests(): void;
@@ -0,0 +1,45 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { u1s1Dir } from "./config.js";
4
+ /**
5
+ * 记住最近一次模型调用报错带回的 request_id(网关在错误 JSON 里注入,见
6
+ * error-humanize.ts),`u1s1 feedback` / 会话内 /feedback 建工单时附进诊断
7
+ * 快照,客服凭它直查日志。进程内先记内存;同时尽力落到 ~/.u1s1/last-request.json,
8
+ * 让退出会话后再跑 `u1s1 feedback` 的用户也能带上编号。超过 24 小时的记录
9
+ * 不再当「最近」。
10
+ */
11
+ export const lastRequestFile = join(u1s1Dir, "last-request.json");
12
+ const MAX_AGE_MS = 24 * 60 * 60 * 1000;
13
+ let inMemory;
14
+ export function rememberRequestId(id, file = lastRequestFile) {
15
+ if (!id || id.length > 128)
16
+ return;
17
+ const at = Date.now();
18
+ inMemory = { request_id: id, at };
19
+ try {
20
+ writeFileSync(file, JSON.stringify({ request_id: id, at: new Date(at).toISOString() }) + "\n");
21
+ }
22
+ catch {
23
+ // ~/.u1s1 不存在(纯环境变量登录)等情况:只记内存
24
+ }
25
+ }
26
+ export function lastRequestId(file = lastRequestFile, now = Date.now()) {
27
+ if (inMemory && now - inMemory.at <= MAX_AGE_MS)
28
+ return inMemory.request_id;
29
+ try {
30
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
31
+ if (typeof parsed.request_id !== "string" || typeof parsed.at !== "string")
32
+ return undefined;
33
+ const at = Date.parse(parsed.at);
34
+ if (!Number.isFinite(at) || now - at > MAX_AGE_MS)
35
+ return undefined;
36
+ return parsed.request_id;
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ }
42
+ /** 测试用:清掉进程内缓存。 */
43
+ export function resetRequestTraceForTests() {
44
+ inMemory = undefined;
45
+ }
@@ -0,0 +1,56 @@
1
+ import { type CliConfig } from "./config.js";
2
+ /**
3
+ * 客户端事件上报(运营清单 C1):只报「会话开始/结束、首次工具成功、首次任务
4
+ * 完成、部署完成」几个节点,不采任何内容。事件先在内存排队,每 30 秒或退出时
5
+ * 批量 POST /v1/telemetry(每批最多 20 条),失败静默丢弃,绝不阻塞界面、
6
+ * 绝不拖慢退出(退出冲刷最多等 1 秒)。
7
+ *
8
+ * 关闭方式:环境变量 U1S1_TELEMETRY=0,或 ~/.u1s1/agent/settings.json 里
9
+ * "telemetry": false。关闭后 track 直接丢,不打印任何东西。
10
+ */
11
+ export type TelemetryEventName = "session_start" | "session_end" | "first_tool_success" | "first_task_done" | "deploy_done";
12
+ export interface TelemetryEvent {
13
+ name: TelemetryEventName;
14
+ /** epoch ms */
15
+ ts: number;
16
+ props?: Record<string, unknown>;
17
+ }
18
+ export type TelemetryTransport = (events: TelemetryEvent[]) => Promise<void>;
19
+ export interface TelemetryOptions {
20
+ transport: TelemetryTransport;
21
+ /** 默认 true;false 时所有 track 都是空操作 */
22
+ enabled?: boolean;
23
+ /** 定时冲刷间隔,默认 30 秒 */
24
+ flushIntervalMs?: number;
25
+ /** 单批上限,默认 20(与网关契约一致) */
26
+ maxBatch?: number;
27
+ /** 退出冲刷的等待上限,默认 1 秒 */
28
+ shutdownTimeoutMs?: number;
29
+ now?: () => number;
30
+ }
31
+ export interface Telemetry {
32
+ track(name: TelemetryEventName, props?: Record<string, unknown>): void;
33
+ /** 立即把队列里的事件按批发出;传输失败静默丢弃 */
34
+ flush(): Promise<void>;
35
+ /** 停掉定时器并做最后一次冲刷(最多等 shutdownTimeoutMs) */
36
+ shutdown(): Promise<void>;
37
+ /** 当前排队条数(测试用) */
38
+ pending(): number;
39
+ readonly enabled: boolean;
40
+ }
41
+ export declare const TELEMETRY_FLUSH_INTERVAL_MS = 30000;
42
+ export declare const TELEMETRY_MAX_BATCH = 20;
43
+ export declare const TELEMETRY_SHUTDOWN_TIMEOUT_MS = 1000;
44
+ /** 环境变量优先于设置文件;两者都没说就默认开启。 */
45
+ export declare function telemetryEnabled(env?: NodeJS.ProcessEnv, settings?: {
46
+ telemetry?: unknown;
47
+ }): boolean;
48
+ export declare function createTelemetry(options: TelemetryOptions): Telemetry;
49
+ /** 真正的传输:设备签名 POST /v1/telemetry,3 秒超时,响应体直接丢。 */
50
+ export declare function gatewayTransport(cfg: CliConfig, timeoutMs?: number): TelemetryTransport;
51
+ /** 按当前环境/设置组装一个连着网关的 Telemetry。 */
52
+ export declare function createGatewayTelemetry(cfg: CliConfig, overrides?: Partial<TelemetryOptions>): Telemetry;
53
+ /**
54
+ * 一次性命令(如 deploy)用:发一个事件就冲刷,最多等 1 秒;任何失败都吞掉。
55
+ */
56
+ export declare function sendTelemetryEvent(cfg: CliConfig, name: TelemetryEventName, props?: Record<string, unknown>): Promise<void>;
@@ -0,0 +1,146 @@
1
+ import { readSettings, VERSION } from "./config.js";
2
+ import { authorizedFetch } from "./device-auth.js";
3
+ export const TELEMETRY_FLUSH_INTERVAL_MS = 30_000;
4
+ export const TELEMETRY_MAX_BATCH = 20;
5
+ export const TELEMETRY_SHUTDOWN_TIMEOUT_MS = 1_000;
6
+ /** 内存里最多攒这么多条;再多就丢最旧的,防止网络长期不通时无限增长 */
7
+ const MAX_QUEUE = 200;
8
+ /** 环境变量优先于设置文件;两者都没说就默认开启。 */
9
+ export function telemetryEnabled(env = process.env, settings = readSettings()) {
10
+ const raw = env["U1S1_TELEMETRY"];
11
+ if (raw !== undefined && raw !== "") {
12
+ const v = raw.trim().toLowerCase();
13
+ if (v === "0" || v === "false" || v === "off" || v === "no")
14
+ return false;
15
+ if (v === "1" || v === "true" || v === "on" || v === "yes")
16
+ return true;
17
+ }
18
+ return settings.telemetry !== false;
19
+ }
20
+ export function createTelemetry(options) {
21
+ const enabled = options.enabled ?? true;
22
+ const flushIntervalMs = options.flushIntervalMs ?? TELEMETRY_FLUSH_INTERVAL_MS;
23
+ const maxBatch = options.maxBatch ?? TELEMETRY_MAX_BATCH;
24
+ const shutdownTimeoutMs = options.shutdownTimeoutMs ?? TELEMETRY_SHUTDOWN_TIMEOUT_MS;
25
+ const now = options.now ?? Date.now;
26
+ const queue = [];
27
+ let timer;
28
+ let inflight;
29
+ let stopped = false;
30
+ const clearTimer = () => {
31
+ if (timer)
32
+ clearTimeout(timer);
33
+ timer = undefined;
34
+ };
35
+ const flushNow = async () => {
36
+ clearTimer();
37
+ // 串行发送:上一批还在路上就等它,避免乱序与并发放大
38
+ if (inflight)
39
+ await inflight;
40
+ inflight = (async () => {
41
+ // 一次 flush 最多发几批,剩下的留给下一轮,别把退出拖住
42
+ for (let round = 0; round < 3 && queue.length > 0; round++) {
43
+ const batch = queue.splice(0, maxBatch);
44
+ try {
45
+ await options.transport(batch);
46
+ }
47
+ catch {
48
+ // 静默丢弃:遥测不值得重试,更不值得打扰用户
49
+ }
50
+ }
51
+ })();
52
+ try {
53
+ await inflight;
54
+ }
55
+ finally {
56
+ inflight = undefined;
57
+ }
58
+ };
59
+ const schedule = () => {
60
+ if (timer || stopped)
61
+ return;
62
+ timer = setTimeout(() => {
63
+ timer = undefined;
64
+ void flushNow();
65
+ }, flushIntervalMs);
66
+ // 定时器不能拖住进程退出
67
+ timer.unref?.();
68
+ };
69
+ return {
70
+ enabled,
71
+ track(name, props) {
72
+ if (!enabled || stopped)
73
+ return;
74
+ queue.push(props ? { name, ts: now(), props } : { name, ts: now() });
75
+ if (queue.length > MAX_QUEUE)
76
+ queue.splice(0, queue.length - MAX_QUEUE);
77
+ if (queue.length >= maxBatch)
78
+ void flushNow();
79
+ else
80
+ schedule();
81
+ },
82
+ async flush() {
83
+ if (!enabled)
84
+ return;
85
+ await flushNow();
86
+ },
87
+ async shutdown() {
88
+ if (!enabled || stopped)
89
+ return;
90
+ stopped = true;
91
+ clearTimer();
92
+ if (queue.length === 0 && !inflight)
93
+ return;
94
+ let cap;
95
+ const deadline = new Promise((resolve) => {
96
+ cap = setTimeout(resolve, shutdownTimeoutMs);
97
+ cap.unref?.();
98
+ });
99
+ try {
100
+ await Promise.race([flushNow(), deadline]);
101
+ }
102
+ finally {
103
+ if (cap)
104
+ clearTimeout(cap);
105
+ }
106
+ },
107
+ pending() {
108
+ return queue.length;
109
+ },
110
+ };
111
+ }
112
+ /** 真正的传输:设备签名 POST /v1/telemetry,3 秒超时,响应体直接丢。 */
113
+ export function gatewayTransport(cfg, timeoutMs = 3_000) {
114
+ return async (events) => {
115
+ const resp = await authorizedFetch(cfg, `${cfg.baseUrl}/telemetry`, {
116
+ method: "POST",
117
+ headers: { "x-u1s1-version": VERSION, "content-type": "application/json" },
118
+ body: JSON.stringify({ events }),
119
+ signal: AbortSignal.timeout(timeoutMs),
120
+ });
121
+ void resp.body?.cancel("telemetry response ignored").catch(() => { });
122
+ };
123
+ }
124
+ /** 按当前环境/设置组装一个连着网关的 Telemetry。 */
125
+ export function createGatewayTelemetry(cfg, overrides = {}) {
126
+ return createTelemetry({
127
+ transport: gatewayTransport(cfg),
128
+ enabled: telemetryEnabled(),
129
+ ...overrides,
130
+ });
131
+ }
132
+ /**
133
+ * 一次性命令(如 deploy)用:发一个事件就冲刷,最多等 1 秒;任何失败都吞掉。
134
+ */
135
+ export async function sendTelemetryEvent(cfg, name, props) {
136
+ try {
137
+ const t = createGatewayTelemetry(cfg);
138
+ if (!t.enabled)
139
+ return;
140
+ t.track(name, props);
141
+ await t.shutdown();
142
+ }
143
+ catch {
144
+ // 遥测永远不影响命令本身
145
+ }
146
+ }
package/dist/update.js CHANGED
@@ -166,8 +166,8 @@ async function portableSelfUpdate(latest) {
166
166
  execFileSync("bash", [tmp], { stdio: "inherit" });
167
167
  }
168
168
  catch {
169
- console.error("\n自动升级失败。请手动运行:");
170
- console.error(" curl -fsSL https://u1s1.io/releases/install.sh | bash");
169
+ // 安装脚本已经打印了失败原因与重试提示(多半是到 u1s1.io 的链路慢),这里不再重复一整段
170
+ console.error("\n自动升级没有完成,稍后重试 u1s1 update 即可;已安装版本不受影响。");
171
171
  process.exit(1);
172
172
  }
173
173
  // 脚本退出码 0 ≠ 升级成功:安装脚本认的是官网 LATEST 版本源,它若认为
package/dist/usage.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import { type MeResponse } from "./api.js";
2
+ export declare const TOPUP_URL = "https://u1s1.io/dashboard#usage-topup-card";
3
+ /** 报告尾巴的两条行动入口:邀请(永久加量)与充值(撞线用户的直接出口,运营清单 A2)。 */
4
+ export declare function usageCtaLines(): string[];
2
5
  /** 额度报告正文(不含首尾空行),终端 `u1s1 usage` 和会话内 /usage 共用。 */
3
6
  export declare function usageReportLines(me: MeResponse): string[];
4
7
  /** 会话内 /usage 用:拉取额度并打包成 entry data,错误转成中文提示而不是抛出。 */
package/dist/usage.js CHANGED
@@ -93,6 +93,8 @@ function packageUsageLines(me, tokensPerUsd) {
93
93
  lines.push(" 免费用量包覆盖 DeepSeek V4 Flash / Vision、GLM-5.3 Flash、Qwen3.8 Flash 和联网搜索。");
94
94
  lines.push(" 其他模型(如 DeepSeek V4 Pro)不走免费包,可用余额或全模型包 → https://u1s1.io/dashboard");
95
95
  lines.push(" Token 数按默认模型单价折算,为约数;用更贵的模型时消耗更快。");
96
+ lines.push("");
97
+ lines.push(...usageCtaLines());
96
98
  return lines;
97
99
  }
98
100
  function legacyUsageLines(me, tokensPerUsd) {
@@ -116,9 +118,17 @@ function legacyUsageLines(me, tokensPerUsd) {
116
118
  lines.push(` 本月成本 $${me.mtd_usd.toFixed(2)}`);
117
119
  }
118
120
  lines.push("");
119
- lines.push(" 免费额度每天自动恢复;邀请朋友双方各得永久加量 → https://u1s1.io/dashboard");
121
+ lines.push(...usageCtaLines());
120
122
  return lines;
121
123
  }
124
+ export const TOPUP_URL = "https://u1s1.io/dashboard#usage-topup-card";
125
+ /** 报告尾巴的两条行动入口:邀请(永久加量)与充值(撞线用户的直接出口,运营清单 A2)。 */
126
+ export function usageCtaLines() {
127
+ return [
128
+ " 免费额度每天自动恢复;邀请朋友双方各得永久加量 → https://u1s1.io/dashboard",
129
+ ` 额度不够用?充值 → ${TOPUP_URL}`,
130
+ ];
131
+ }
122
132
  /** 额度报告正文(不含首尾空行),终端 `u1s1 usage` 和会话内 /usage 共用。 */
123
133
  export function usageReportLines(me) {
124
134
  const tokensPerUsd = me.tokens_per_usd ?? 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u1s1-cli",
3
- "version": "1.4.3",
3
+ "version": "1.6.0",
4
4
  "description": "u1s1 — 有一说一,最省心的 AI 编程搭子。终端里用中文说需求,AI 帮你读文件、改代码、跑命令。",
5
5
  "type": "module",
6
6
  "bin": {