u1s1-cli 1.3.2 → 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.
- package/dist/agent-setup.d.ts +18 -1
- package/dist/agent-setup.js +76 -3
- package/dist/announcements-poll.d.ts +22 -0
- package/dist/announcements-poll.js +58 -0
- package/dist/api.d.ts +2 -17
- package/dist/brand.d.ts +2 -0
- package/dist/brand.js +16 -0
- package/dist/config.d.ts +21 -10
- package/dist/config.js +30 -2
- package/dist/deploy.d.ts +1 -0
- package/dist/deploy.js +14 -0
- package/dist/error-humanize.d.ts +15 -0
- package/dist/error-humanize.js +47 -0
- package/dist/index.js +85 -9
- package/dist/login.d.ts +9 -2
- package/dist/login.js +29 -6
- package/dist/model.js +8 -6
- package/dist/style.js +11 -0
- package/dist/usage.d.ts +8 -0
- package/dist/usage.js +55 -34
- package/package.json +1 -1
- package/scripts/patch-pi.js +53 -0
package/dist/agent-setup.d.ts
CHANGED
|
@@ -32,6 +32,23 @@ 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;
|
|
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;
|
|
35
52
|
/**
|
|
36
53
|
* 生成「OpenRouter 应用归因」扩展到 <agentDir>/extensions/u1s1-attribution.js。
|
|
37
54
|
* 用户自己配 OPENROUTER_API_KEY 直连时,pi 内置的归因头归到 pi.dev 名下(且受遥测
|
|
@@ -62,7 +79,7 @@ export declare function toProviderModels(models: ModelDef[]): {
|
|
|
62
79
|
reasoning: boolean;
|
|
63
80
|
thinkingLevelMap?: Partial<Record<ThinkingLevel, string | null>>;
|
|
64
81
|
input: ("text" | "image")[];
|
|
65
|
-
cost: ModelDef["cost"]
|
|
82
|
+
cost: NonNullable<ModelDef["cost"]>;
|
|
66
83
|
contextWindow: number;
|
|
67
84
|
maxTokens: number;
|
|
68
85
|
compat?: {
|
package/dist/agent-setup.js
CHANGED
|
@@ -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:
|
|
@@ -235,6 +235,78 @@ 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
|
+
}
|
|
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
|
+
}
|
|
238
310
|
/**
|
|
239
311
|
* 生成「OpenRouter 应用归因」扩展到 <agentDir>/extensions/u1s1-attribution.js。
|
|
240
312
|
* 用户自己配 OPENROUTER_API_KEY 直连时,pi 内置的归因头归到 pi.dev 名下(且受遥测
|
|
@@ -404,11 +476,12 @@ export function toProviderModels(models) {
|
|
|
404
476
|
}
|
|
405
477
|
return {
|
|
406
478
|
id: m.id,
|
|
407
|
-
name
|
|
479
|
+
// 选择器只渲染 name 一行,把「免费/几倍」提示拼进去,切模型前就能看到
|
|
480
|
+
name: m.note ? `${m.name} · ${m.note}` : m.name,
|
|
408
481
|
reasoning: m.reasoning,
|
|
409
482
|
thinkingLevelMap,
|
|
410
483
|
input: m.vision ? ["text", "image"] : ["text"],
|
|
411
|
-
cost: m.cost,
|
|
484
|
+
cost: m.cost ?? UNKNOWN_MODEL_COST,
|
|
412
485
|
contextWindow: m.contextWindow,
|
|
413
486
|
maxTokens: m.maxTokens,
|
|
414
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
|
|
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
|
|
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/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
|
|
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,15 @@ 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:
|
|
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;
|
|
47
59
|
export interface ModelDef {
|
|
48
60
|
id: string;
|
|
49
61
|
name: string;
|
|
@@ -55,13 +67,13 @@ export interface ModelDef {
|
|
|
55
67
|
vision: boolean;
|
|
56
68
|
contextWindow: number;
|
|
57
69
|
maxTokens: number;
|
|
58
|
-
/**
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
/**
|
|
71
|
+
* USD per million tokens — display/estimation only; billing is server-side.
|
|
72
|
+
* null = 价格未知(内置兜底目录不再硬编码价格,权威在网关 D1;09-01 评审 D5 拍板删)。
|
|
73
|
+
*/
|
|
74
|
+
cost: ModelCost | null;
|
|
75
|
+
/** 网关下发的免费用量包覆盖标记;老网关缺失时为 undefined(不派生 note)。 */
|
|
76
|
+
freeEligible?: boolean;
|
|
65
77
|
note: string;
|
|
66
78
|
}
|
|
67
79
|
export declare const MODELS: ModelDef[];
|
|
@@ -158,4 +170,3 @@ export declare function saveConfig(cfg: CliConfig): void;
|
|
|
158
170
|
export declare function persistPreferredModel(cfg: CliConfig, provider: string, modelId: string): CliConfig;
|
|
159
171
|
export declare function saveEndpointsCache(endpoints: CustomEndpoint[]): void;
|
|
160
172
|
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,9 +76,12 @@ 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
|
}
|
|
83
|
+
/** pi 的 Model 类型要求 cost 非空;价格未知时按 0 交给 pi(与自有端点同一约定)。 */
|
|
84
|
+
export const UNKNOWN_MODEL_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
81
85
|
export const MODELS = [
|
|
82
86
|
{
|
|
83
87
|
id: "deepseek-v4-flash",
|
|
@@ -94,7 +98,7 @@ export const MODELS = [
|
|
|
94
98
|
vision: false,
|
|
95
99
|
contextWindow: 1_048_576,
|
|
96
100
|
maxTokens: 384_000,
|
|
97
|
-
cost:
|
|
101
|
+
cost: null,
|
|
98
102
|
note: "默认 · 免费用量包额度内免费,日常写代码首选",
|
|
99
103
|
},
|
|
100
104
|
{
|
|
@@ -105,12 +109,36 @@ export const MODELS = [
|
|
|
105
109
|
vision: false,
|
|
106
110
|
contextWindow: 500_000,
|
|
107
111
|
maxTokens: 65_536,
|
|
108
|
-
cost:
|
|
112
|
+
cost: null,
|
|
109
113
|
note: "更强 · 但烧额度快约 20 倍,难题再用",
|
|
110
114
|
},
|
|
111
115
|
];
|
|
116
|
+
/**
|
|
117
|
+
* 按免费包覆盖标记 + 相对默认免费模型的价格倍数派生一句人话提示。
|
|
118
|
+
* TUI 的模型选择器只渲染 name 一行,note 会被拼进去(见 toProviderModels),
|
|
119
|
+
* 让人切模型前就知道「这个不走免费包 / 大概贵多少」。
|
|
120
|
+
*/
|
|
121
|
+
function deriveModelNote(m, base) {
|
|
122
|
+
if (m.freeEligible === true)
|
|
123
|
+
return "免费用量包可抵扣";
|
|
124
|
+
if (m.freeEligible === false) {
|
|
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;
|
|
127
|
+
const mult = baseBlended > 0 ? Math.round(blended / baseBlended) : 0;
|
|
128
|
+
return mult >= 2
|
|
129
|
+
? `不走免费包 · 费用约为默认模型 ${mult} 倍`
|
|
130
|
+
: "不走免费包,用余额或全模型包";
|
|
131
|
+
}
|
|
132
|
+
return "";
|
|
133
|
+
}
|
|
112
134
|
/** Replace MODELS with a fresh list fetched from server (e.g. at startup). */
|
|
113
135
|
export function setModelsFromApi(apiModels) {
|
|
136
|
+
const base = apiModels.find((m) => m.id === DEFAULT_MODEL_ID)
|
|
137
|
+
?? apiModels.find((m) => m.freeEligible === true);
|
|
138
|
+
for (const m of apiModels) {
|
|
139
|
+
if (!m.note)
|
|
140
|
+
m.note = deriveModelNote(m, base);
|
|
141
|
+
}
|
|
114
142
|
MODELS.length = 0;
|
|
115
143
|
MODELS.push(...apiModels);
|
|
116
144
|
}
|
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, writeAnnouncementsExtension, 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,10 @@ async function runAgent(cfg, args) {
|
|
|
220
221
|
});
|
|
221
222
|
// 精简 UI:隐藏工具调用 + 汇总行 + 隐藏思考标签(同样投影到 agentDir/extensions)
|
|
222
223
|
writeCompactUiExtension();
|
|
224
|
+
// 模型调用错误人话化(同样投影,Desktop App 共享)
|
|
225
|
+
writeErrorHumanizeExtension();
|
|
226
|
+
// 会话内公告触达:5 分钟轮询,公告变化时推进会话(同样投影,Desktop 共享)
|
|
227
|
+
writeAnnouncementsExtension();
|
|
223
228
|
// OpenRouter 应用归因:直连流量计入 u1s1 的公开排行
|
|
224
229
|
writeAttributionExtension();
|
|
225
230
|
ensureTmuxKeyboardProtocol();
|
|
@@ -296,6 +301,41 @@ async function runAgent(cfg, args) {
|
|
|
296
301
|
ctx.shutdown();
|
|
297
302
|
},
|
|
298
303
|
});
|
|
304
|
+
// /help:启动横幅和官网教程都在引导用户输入它,必须真实存在
|
|
305
|
+
pi.registerEntryRenderer("u1s1-help", (_entry, _opts, theme) => {
|
|
306
|
+
const text = [
|
|
307
|
+
theme.bold(theme.fg("text", "u1s1 常用操作")),
|
|
308
|
+
theme.fg("text", "直接打字说需求,回车发送 · Shift+Enter 换行 · Ctrl+V 贴图 · Esc 中断"),
|
|
309
|
+
theme.fg("text", "/model 切换模型(留意免费/价格提示) · /clear 清空上下文开新会话"),
|
|
310
|
+
theme.fg("text", "/usage 查剩余额度 · /resume 恢复历史会话 · /settings 设置"),
|
|
311
|
+
theme.fg("dim", "/compact 压缩上下文 · /hotkeys 全部快捷键(英文) · /exit 退出"),
|
|
312
|
+
theme.fg("dim", "退出后在终端:u1s1 deploy 发布网页 · u1s1 update 升级"),
|
|
313
|
+
theme.fg("dim", "新手教程 → https://u1s1.io/guides"),
|
|
314
|
+
].join("\n");
|
|
315
|
+
return new Text(text, 1, 0);
|
|
316
|
+
});
|
|
317
|
+
pi.registerCommand("help", {
|
|
318
|
+
description: "查看常用操作和命令",
|
|
319
|
+
handler: async () => {
|
|
320
|
+
pi.appendEntry("u1s1-help");
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
// /usage:会话内查额度。此前额度只有用完撞 429 才可见(审计 G1)
|
|
324
|
+
pi.registerEntryRenderer("u1s1-usage", (entry, _opts, theme) => {
|
|
325
|
+
const data = entry.data ?? {};
|
|
326
|
+
if (data.error)
|
|
327
|
+
return new Text(theme.fg("dim", data.error), 1, 0);
|
|
328
|
+
return new Text((data.lines ?? []).map((l) => theme.fg("text", l)).join("\n"), 1, 0);
|
|
329
|
+
});
|
|
330
|
+
pi.registerCommand("usage", {
|
|
331
|
+
description: "查看剩余额度和本月用量",
|
|
332
|
+
handler: async () => {
|
|
333
|
+
const { usageEntryData } = await import("./usage.js");
|
|
334
|
+
pi.appendEntry("u1s1-usage", await usageEntryData());
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
// 模型调用错误人话化不在这里注册:走 writeErrorHumanizeExtension 投影,
|
|
338
|
+
// TUI 和 Desktop App 共用一份(见 agent-setup.ts)
|
|
299
339
|
// /clear: 清空当前对话上下文,开始新会话
|
|
300
340
|
pi.registerCommand("clear", {
|
|
301
341
|
description: "清除当前对话上下文,开始新会话",
|
|
@@ -358,21 +398,23 @@ async function run() {
|
|
|
358
398
|
console.log(`u1s1 v${VERSION}`);
|
|
359
399
|
return;
|
|
360
400
|
}
|
|
361
|
-
if (cmd === "--help" || cmd === "-h") {
|
|
401
|
+
if (cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
362
402
|
printConsoleBanner(VERSION);
|
|
363
|
-
console.log(" u1s1 命令:deploy(发布网页,可选 --public / --private)· login / logout · model · usage · update · import · bench");
|
|
364
|
-
console.log("");
|
|
365
403
|
console.log(" 命令:");
|
|
404
|
+
console.log(" u1s1 进入对话界面(最常用,不带参数)");
|
|
405
|
+
console.log(" u1s1 -p \"一句话\" 不进界面直接回答,适合脚本里用");
|
|
366
406
|
console.log(" u1s1 login / logout 登录 / 退出登录");
|
|
367
407
|
console.log(" u1s1 model 查看或切换默认模型");
|
|
368
|
-
console.log(" u1s1 usage
|
|
408
|
+
console.log(" u1s1 usage 查看剩余额度");
|
|
369
409
|
console.log(" u1s1 update 升级到最新版");
|
|
370
|
-
console.log(" u1s1 deploy
|
|
410
|
+
console.log(" u1s1 deploy 发布网页(--public / --private)");
|
|
411
|
+
console.log(" u1s1 deploy list 查看已发布的站点");
|
|
371
412
|
console.log(" u1s1 import 导入历史会话");
|
|
413
|
+
console.log(" u1s1 bench 模型编码能力评测");
|
|
372
414
|
console.log(" u1s1 --version 查看版本");
|
|
373
415
|
console.log("");
|
|
374
|
-
console.log(" 对话里输入 /
|
|
375
|
-
console.log(" 更多帮助 → https://u1s1.io/
|
|
416
|
+
console.log(" 对话里输入 /help 看会话内命令(/model /clear /exit …)");
|
|
417
|
+
console.log(" 更多帮助 → https://u1s1.io/guides");
|
|
376
418
|
return;
|
|
377
419
|
}
|
|
378
420
|
if (cmd === "web") {
|
|
@@ -381,9 +423,14 @@ async function run() {
|
|
|
381
423
|
return;
|
|
382
424
|
}
|
|
383
425
|
if (cmd === "deploy") {
|
|
426
|
+
const { deployCommand, printDeployHelp } = await import("./deploy.js");
|
|
427
|
+
// --help 不该先被拽去登录
|
|
428
|
+
if (args.slice(1).includes("--help") || args.slice(1).includes("-h")) {
|
|
429
|
+
printDeployHelp();
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
384
432
|
const { ensureAuth } = await import("./login.js");
|
|
385
433
|
const cfg = await ensureAuth();
|
|
386
|
-
const { deployCommand } = await import("./deploy.js");
|
|
387
434
|
await deployCommand(cfg, args.slice(1));
|
|
388
435
|
return;
|
|
389
436
|
}
|
|
@@ -447,6 +494,20 @@ async function run() {
|
|
|
447
494
|
await benchCommand(args.slice(1));
|
|
448
495
|
return;
|
|
449
496
|
}
|
|
497
|
+
// 拼错的子命令(如 `u1s1 depoly`)不该被静默当成第一条消息发给模型白烧 token。
|
|
498
|
+
// 只拦「单个短英文词 + 与已知命令编辑距离很近」的情况,正常 prompt 不受影响。
|
|
499
|
+
if (cmd
|
|
500
|
+
&& args.length === 1
|
|
501
|
+
&& /^[a-z][a-z0-9-]{1,15}$/.test(cmd)
|
|
502
|
+
&& !KNOWN_COMMANDS.includes(cmd)) {
|
|
503
|
+
const near = KNOWN_COMMANDS.find((k) => editDistance(k, cmd) <= (k.length <= 4 ? 1 : 2));
|
|
504
|
+
if (near) {
|
|
505
|
+
console.error(`不认识的命令「${cmd}」,你是不是想运行:u1s1 ${near}`);
|
|
506
|
+
console.error("查看全部命令:u1s1 --help;直接开始对话:运行 u1s1(不带参数)");
|
|
507
|
+
process.exitCode = 1;
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
450
511
|
const { ensureAuth } = await import("./login.js");
|
|
451
512
|
const cfg = await ensureAuth();
|
|
452
513
|
// 在后台检查更新(非阻塞,不影响启动速度)
|
|
@@ -455,6 +516,21 @@ async function run() {
|
|
|
455
516
|
// autoUpdate 开着且启动时发现了新版:现在装(TUI 已退出,原地重铺安全)
|
|
456
517
|
installPendingUpdate();
|
|
457
518
|
}
|
|
519
|
+
const KNOWN_COMMANDS = ["deploy", "login", "logout", "usage", "model", "update", "import", "bench", "help", "web"];
|
|
520
|
+
/** 经典 Levenshtein,命令名都很短,O(nm) 足够。 */
|
|
521
|
+
function editDistance(a, b) {
|
|
522
|
+
const dp = Array.from({ length: a.length + 1 }, (_, i) => i);
|
|
523
|
+
for (let j = 1; j <= b.length; j++) {
|
|
524
|
+
let prev = dp[0];
|
|
525
|
+
dp[0] = j;
|
|
526
|
+
for (let i = 1; i <= a.length; i++) {
|
|
527
|
+
const cur = dp[i];
|
|
528
|
+
dp[i] = Math.min(dp[i] + 1, dp[i - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
529
|
+
prev = cur;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return dp[a.length];
|
|
533
|
+
}
|
|
458
534
|
run().catch((e) => {
|
|
459
535
|
console.error(e instanceof Error ? e.message : e);
|
|
460
536
|
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
|
-
/**
|
|
14
|
-
|
|
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
|
-
/**
|
|
40
|
-
|
|
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
|
-
|
|
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
|
|
151
|
+
const failure = {};
|
|
152
|
+
const start = await startDeviceLogin(origin, failure);
|
|
138
153
|
if (!start) {
|
|
139
|
-
|
|
140
|
-
|
|
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([
|
|
@@ -29,12 +32,11 @@ export async function modelCommand(nameOrAlias) {
|
|
|
29
32
|
for (const m of MODELS) {
|
|
30
33
|
const mark = current.provider === PROVIDER_ID && m.id === current.id ? "●" : " ";
|
|
31
34
|
console.log(` ${mark} ${m.aliases[0].padEnd(10)} ${m.name}`);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
console.log(`
|
|
37
|
-
}
|
|
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}`);
|
|
38
40
|
}
|
|
39
41
|
for (const ep of CUSTOM_ENDPOINTS) {
|
|
40
42
|
console.log("");
|
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
|
|
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
|
-
|
|
70
|
+
return [
|
|
71
|
+
` ${packageLabel(pkg)} 还剩 ${fmtTokensCn(pkg.remaining)} / ${fmtTokensCn(total)}${isDaily ? "/天" : ""} ${bar(ratio)}`,
|
|
72
|
+
` ${packageScopeNote(pkg)} · ${expiry}`,
|
|
73
|
+
];
|
|
71
74
|
}
|
|
72
|
-
function
|
|
75
|
+
function packageUsageLines(me, tokensPerUsd) {
|
|
73
76
|
const packages = me.packages ?? [];
|
|
77
|
+
const lines = [];
|
|
74
78
|
if (packages.length === 0)
|
|
75
|
-
|
|
79
|
+
lines.push(" 用量包 (无生效中的用量包)");
|
|
76
80
|
for (const pkg of groupPackages(packages))
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
package/scripts/patch-pi.js
CHANGED
|
@@ -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}),不影响安装`);
|