u1s1-cli 1.4.0 → 1.4.1

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.
@@ -41,6 +41,14 @@ export declare function writeWebToolsExtension(cfg: CliConfig, features: {
41
41
  * 宁可回到原始错误文本,也不能让扩展报错刷英文堆栈。
42
42
  */
43
43
  export declare function writeErrorHumanizeExtension(): void;
44
+ /**
45
+ * 生成「会话内公告触达」扩展到 <agentDir>/extensions/u1s1-announcements.js:
46
+ * 启动横幅只渲染一次公告,维护窗口里挂着会话的用户只会撞裸错误(审计 F14)。
47
+ * 轮询逻辑在 announcements-poll.ts(5 分钟一次、首拉只建基线不打扰);
48
+ * 公告变化时 appendEntry 进会话 + 尽力弹 notify。TUI 和 Desktop App 共用。
49
+ * print/json 一次性模式不起轮询;import 失败静默跳过(同 error-humanize)。
50
+ */
51
+ export declare function writeAnnouncementsExtension(): void;
44
52
  /**
45
53
  * 生成「OpenRouter 应用归因」扩展到 <agentDir>/extensions/u1s1-attribution.js。
46
54
  * 用户自己配 OPENROUTER_API_KEY 直连时,pi 内置的归因头归到 pi.dev 名下(且受遥测
@@ -71,7 +79,7 @@ export declare function toProviderModels(models: ModelDef[]): {
71
79
  reasoning: boolean;
72
80
  thinkingLevelMap?: Partial<Record<ThinkingLevel, string | null>>;
73
81
  input: ("text" | "image")[];
74
- cost: ModelDef["cost"];
82
+ cost: NonNullable<ModelDef["cost"]>;
75
83
  contextWindow: number;
76
84
  maxTokens: number;
77
85
  compat?: {
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { dirname, join } from "node:path";
4
- import { agentDir, CUSTOM_ENDPOINTS, ENDPOINT_ID_RE, endpointKeyEnvName, MODELS, PROVIDER_ID, VERSION, } from "./config.js";
4
+ import { agentDir, CUSTOM_ENDPOINTS, ENDPOINT_ID_RE, endpointKeyEnvName, MODELS, PROVIDER_ID, UNKNOWN_MODEL_COST, VERSION, } from "./config.js";
5
5
  const BRAND_APPEND = `## u1s1
6
6
 
7
7
  You are u1s1 — a plain-spoken AI coding buddy for programming beginners. Most users are unfamiliar with jargon:
@@ -264,6 +264,49 @@ export function writeErrorHumanizeExtension() {
264
264
  ` });\n` +
265
265
  `}\n`);
266
266
  }
267
+ /**
268
+ * 生成「会话内公告触达」扩展到 <agentDir>/extensions/u1s1-announcements.js:
269
+ * 启动横幅只渲染一次公告,维护窗口里挂着会话的用户只会撞裸错误(审计 F14)。
270
+ * 轮询逻辑在 announcements-poll.ts(5 分钟一次、首拉只建基线不打扰);
271
+ * 公告变化时 appendEntry 进会话 + 尽力弹 notify。TUI 和 Desktop App 共用。
272
+ * print/json 一次性模式不起轮询;import 失败静默跳过(同 error-humanize)。
273
+ */
274
+ export function writeAnnouncementsExtension() {
275
+ const dir = join(agentDir, "extensions");
276
+ mkdirSync(dir, { recursive: true });
277
+ const url = new URL("./announcements-poll.js", import.meta.url).href;
278
+ writeFileSync(join(dir, "u1s1-announcements.js"), `// 由 u1s1 每次启动自动生成,请勿手改\n` +
279
+ `export default async function (pi) {\n` +
280
+ ` let startAnnouncementPoll;\n` +
281
+ ` try {\n` +
282
+ ` startAnnouncementPoll = (await import(${JSON.stringify(url)})).startAnnouncementPoll;\n` +
283
+ ` } catch {\n` +
284
+ ` return;\n` +
285
+ ` }\n` +
286
+ ` const { Text } = await import("@earendil-works/pi-tui");\n` +
287
+ ` pi.registerEntryRenderer("u1s1-announcement", (entry, _opts, theme) => {\n` +
288
+ ` const d = entry.data ?? {};\n` +
289
+ ` let text = theme.fg("text", theme.bold("📢 " + String(d.text ?? "")));\n` +
290
+ ` if (d.url) text += "\\n" + theme.fg("dim", String(d.url));\n` +
291
+ ` return new Text(text, 1, 0);\n` +
292
+ ` });\n` +
293
+ ` let ui;\n` +
294
+ ` let started = false;\n` +
295
+ ` pi.on("session_start", (_event, ctx) => {\n` +
296
+ ` if (ctx.hasUI) ui = ctx.ui;\n` +
297
+ ` if (started || ctx.mode === "print" || ctx.mode === "json") return;\n` +
298
+ ` started = true;\n` +
299
+ ` startAnnouncementPoll({\n` +
300
+ ` onNew: (a) => {\n` +
301
+ ` pi.appendEntry("u1s1-announcement", { text: a.text, url: a.url });\n` +
302
+ ` try {\n` +
303
+ ` ui?.notify("📢 " + a.text, "info");\n` +
304
+ ` } catch {}\n` +
305
+ ` },\n` +
306
+ ` });\n` +
307
+ ` });\n` +
308
+ `}\n`);
309
+ }
267
310
  /**
268
311
  * 生成「OpenRouter 应用归因」扩展到 <agentDir>/extensions/u1s1-attribution.js。
269
312
  * 用户自己配 OPENROUTER_API_KEY 直连时,pi 内置的归因头归到 pi.dev 名下(且受遥测
@@ -438,7 +481,7 @@ export function toProviderModels(models) {
438
481
  reasoning: m.reasoning,
439
482
  thinkingLevelMap,
440
483
  input: m.vision ? ["text", "image"] : ["text"],
441
- cost: m.cost,
484
+ cost: m.cost ?? UNKNOWN_MODEL_COST,
442
485
  contextWindow: m.contextWindow,
443
486
  maxTokens: m.maxTokens,
444
487
  compat,
@@ -0,0 +1,22 @@
1
+ import { type ApiAnnouncement } from "./api.js";
2
+ /**
3
+ * 会话内公告轮询(审计 F14):启动横幅只渲染一次公告,维护窗口里挂着
4
+ * 会话的用户只会撞裸错误。这里每 5 分钟带凭证拉一次模型列表(与启动同一
5
+ * 接口,公告随响应捎回,服务端零新增开销),公告发生变化时推进会话。
6
+ *
7
+ * 首次成功拉取只做基线、不打扰——启动横幅已经展示过当前公告;之后
8
+ * 「出现新公告」或「公告内容变化」才触发 onNew。公告被清空不提示。
9
+ */
10
+ export declare const ANNOUNCEMENT_POLL_INTERVAL_MS: number;
11
+ export declare function announcementKey(a?: ApiAnnouncement | null): string;
12
+ /** 基线跟踪器:observe 返回「需要通知的新公告」,否则 undefined。 */
13
+ export declare function createAnnouncementTracker(): {
14
+ observe(a: ApiAnnouncement | null | undefined): ApiAnnouncement | undefined;
15
+ };
16
+ export declare function startAnnouncementPoll(deps: {
17
+ onNew: (a: ApiAnnouncement) => void;
18
+ fetch?: () => Promise<{
19
+ announcement?: ApiAnnouncement | null;
20
+ }>;
21
+ intervalMs?: number;
22
+ }): () => void;
@@ -0,0 +1,58 @@
1
+ import { fetchModels } from "./api.js";
2
+ import { loadConfig } from "./config.js";
3
+ import { hasDeviceCredential } from "./device-auth.js";
4
+ /**
5
+ * 会话内公告轮询(审计 F14):启动横幅只渲染一次公告,维护窗口里挂着
6
+ * 会话的用户只会撞裸错误。这里每 5 分钟带凭证拉一次模型列表(与启动同一
7
+ * 接口,公告随响应捎回,服务端零新增开销),公告发生变化时推进会话。
8
+ *
9
+ * 首次成功拉取只做基线、不打扰——启动横幅已经展示过当前公告;之后
10
+ * 「出现新公告」或「公告内容变化」才触发 onNew。公告被清空不提示。
11
+ */
12
+ export const ANNOUNCEMENT_POLL_INTERVAL_MS = 5 * 60_000;
13
+ export function announcementKey(a) {
14
+ return a?.text ? `${a.text}\u0000${a.url ?? ""}` : "";
15
+ }
16
+ /** 基线跟踪器:observe 返回「需要通知的新公告」,否则 undefined。 */
17
+ export function createAnnouncementTracker() {
18
+ let baseline;
19
+ return {
20
+ observe(a) {
21
+ const key = announcementKey(a);
22
+ if (baseline === undefined) {
23
+ baseline = key;
24
+ return undefined;
25
+ }
26
+ const changed = key !== "" && key !== baseline;
27
+ baseline = key;
28
+ return changed ? a : undefined;
29
+ },
30
+ };
31
+ }
32
+ export function startAnnouncementPoll(deps) {
33
+ const doFetch = deps.fetch ?? (async () => {
34
+ const cfg = loadConfig();
35
+ if (!hasDeviceCredential(cfg))
36
+ throw new Error("not logged in");
37
+ return fetchModels(cfg);
38
+ });
39
+ const tracker = createAnnouncementTracker();
40
+ const tick = async () => {
41
+ let announcement;
42
+ try {
43
+ announcement = (await doFetch()).announcement;
44
+ }
45
+ catch {
46
+ return; // 网络抖动/未登录:静默,下一轮再试
47
+ }
48
+ const fresh = tracker.observe(announcement ?? null);
49
+ if (fresh)
50
+ deps.onNew(fresh);
51
+ };
52
+ void tick(); // 立刻建基线,避免把启动前就存在的公告当成"新公告"
53
+ const timer = setInterval(() => {
54
+ void tick();
55
+ }, deps.intervalMs ?? ANNOUNCEMENT_POLL_INTERVAL_MS);
56
+ timer.unref?.();
57
+ return () => clearInterval(timer);
58
+ }
package/dist/api.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ApiThinkingCapabilities, type CliConfig } from "./config.js";
1
+ import { type ApiModel, type CliConfig } from "./config.js";
2
2
  /** Preserve caller cancellation while always enforcing the operation deadline. */
3
3
  export declare function signalWithTimeout(signal: AbortSignal | undefined, timeoutMs: number): AbortSignal;
4
4
  /** Read a complete response without allowing a custom or faulty server to exhaust CLI memory. */
@@ -37,22 +37,7 @@ export interface MePackage {
37
37
  expires_at: string | null;
38
38
  note: string | null;
39
39
  }
40
- export interface ApiModel {
41
- id: string;
42
- name: string;
43
- reasoning: boolean;
44
- /** Older gateways omit model-specific thinking metadata. */
45
- thinking?: ApiThinkingCapabilities | null;
46
- /** Older gateways omit this field; missing means text-only. */
47
- vision?: boolean;
48
- context_length: number;
49
- max_tokens: number;
50
- price: {
51
- input: number;
52
- output: number;
53
- cache_read: number | null;
54
- };
55
- }
40
+ export type { ApiModel };
56
41
  /** 启动横幅公告(后台「全局设置」下发);url 可选。老网关没有该字段,undefined = 不显示。 */
57
42
  export interface ApiAnnouncement {
58
43
  text: string;
package/dist/config.d.ts CHANGED
@@ -48,6 +48,14 @@ export interface ApiModel {
48
48
  * Aliases are derived from the short id; note is left empty (filled by /model command).
49
49
  */
50
50
  export declare function apiModelToDef(m: ApiModel): ModelDef;
51
+ export interface ModelCost {
52
+ input: number;
53
+ output: number;
54
+ cacheRead: number;
55
+ cacheWrite: number;
56
+ }
57
+ /** pi 的 Model 类型要求 cost 非空;价格未知时按 0 交给 pi(与自有端点同一约定)。 */
58
+ export declare const UNKNOWN_MODEL_COST: ModelCost;
51
59
  export interface ModelDef {
52
60
  id: string;
53
61
  name: string;
@@ -59,13 +67,11 @@ export interface ModelDef {
59
67
  vision: boolean;
60
68
  contextWindow: number;
61
69
  maxTokens: number;
62
- /** USD per million tokens — display/estimation only; billing is server-side */
63
- cost: {
64
- input: number;
65
- output: number;
66
- cacheRead: number;
67
- cacheWrite: number;
68
- };
70
+ /**
71
+ * USD per million tokens — display/estimation only; billing is server-side.
72
+ * null = 价格未知(内置兜底目录不再硬编码价格,权威在网关 D1;09-01 评审 D5 拍板删)。
73
+ */
74
+ cost: ModelCost | null;
69
75
  /** 网关下发的免费用量包覆盖标记;老网关缺失时为 undefined(不派生 note)。 */
70
76
  freeEligible?: boolean;
71
77
  note: string;
package/dist/config.js CHANGED
@@ -80,6 +80,8 @@ export function apiModelToDef(m) {
80
80
  note: "",
81
81
  };
82
82
  }
83
+ /** pi 的 Model 类型要求 cost 非空;价格未知时按 0 交给 pi(与自有端点同一约定)。 */
84
+ export const UNKNOWN_MODEL_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
83
85
  export const MODELS = [
84
86
  {
85
87
  id: "deepseek-v4-flash",
@@ -96,7 +98,7 @@ export const MODELS = [
96
98
  vision: false,
97
99
  contextWindow: 1_048_576,
98
100
  maxTokens: 384_000,
99
- cost: { input: 0.14, output: 0.28, cacheRead: 0, cacheWrite: 0 },
101
+ cost: null,
100
102
  note: "默认 · 免费用量包额度内免费,日常写代码首选",
101
103
  },
102
104
  {
@@ -107,7 +109,7 @@ export const MODELS = [
107
109
  vision: false,
108
110
  contextWindow: 500_000,
109
111
  maxTokens: 65_536,
110
- cost: { input: 2, output: 6, cacheRead: 0, cacheWrite: 0 },
112
+ cost: null,
111
113
  note: "更强 · 但烧额度快约 20 倍,难题再用",
112
114
  },
113
115
  ];
@@ -120,8 +122,8 @@ function deriveModelNote(m, base) {
120
122
  if (m.freeEligible === true)
121
123
  return "免费用量包可抵扣";
122
124
  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 blended = m.cost ? (m.cost.input + m.cost.output) / 2 : 0;
126
+ const baseBlended = base?.cost ? (base.cost.input + base.cost.output) / 2 : 0;
125
127
  const mult = baseBlended > 0 ? Math.round(blended / baseBlended) : 0;
126
128
  return mult >= 2
127
129
  ? `不走免费包 · 费用约为默认模型 ${mult} 倍`
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
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, writeErrorHumanizeExtension, writeWebToolsExtension, } from "./agent-setup.js";
4
+ import { cleanupBrandThemes, endpointKeyEnv, endpointProviderEntry, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, ensureWorkflowPromptTemplate, scrubForeignProviderEnv, toProviderModels, writeAnnouncementsExtension, writeAttributionExtension, writeCompactUiExtension, writeErrorHumanizeExtension, writeWebToolsExtension, } from "./agent-setup.js";
5
5
  import { Text } from "@earendil-works/pi-tui";
6
6
  import { printConsoleBanner } from "./brand.js";
7
7
  import { agentDir, apiModelToDef, CUSTOM_ENDPOINTS, isPortableInstall, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, readSettings, refValid, resolvePreferredModel, setModelsFromApi, VERSION, } from "./config.js";
@@ -223,6 +223,8 @@ async function runAgent(cfg, args) {
223
223
  writeCompactUiExtension();
224
224
  // 模型调用错误人话化(同样投影,Desktop App 共享)
225
225
  writeErrorHumanizeExtension();
226
+ // 会话内公告触达:5 分钟轮询,公告变化时推进会话(同样投影,Desktop 共享)
227
+ writeAnnouncementsExtension();
226
228
  // OpenRouter 应用归因:直连流量计入 u1s1 的公开排行
227
229
  writeAttributionExtension();
228
230
  ensureTmuxKeyboardProtocol();
package/dist/model.js CHANGED
@@ -32,12 +32,11 @@ export async function modelCommand(nameOrAlias) {
32
32
  for (const m of MODELS) {
33
33
  const mark = current.provider === PROVIDER_ID && m.id === current.id ? "●" : " ";
34
34
  console.log(` ${mark} ${m.aliases[0].padEnd(10)} ${m.name}`);
35
- if (m.note) {
36
- console.log(` ${m.note} · $${m.cost.input}/$${m.cost.output} 每百万 token`);
37
- }
38
- else {
39
- console.log(` $${m.cost.input}/$${m.cost.output} 每百万 token`);
40
- }
35
+ // 价格来自网关目录;离线兜底目录不带价格,只列名字与提示
36
+ const price = m.cost ? `$${m.cost.input}/$${m.cost.output} 每百万 token` : "";
37
+ const detail = [m.note, price].filter(Boolean).join(" · ");
38
+ if (detail)
39
+ console.log(` ${detail}`);
41
40
  }
42
41
  for (const ep of CUSTOM_ENDPOINTS) {
43
42
  console.log("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u1s1-cli",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "u1s1 — 有一说一,最省心的 AI 编程搭子。终端里用中文说需求,AI 帮你读文件、改代码、跑命令。",
5
5
  "type": "module",
6
6
  "bin": {