u1s1-cli 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-setup.d.ts +9 -0
- package/dist/agent-setup.js +31 -1
- package/dist/api.d.ts +2 -0
- package/dist/api.js +2 -0
- package/dist/brand.d.ts +2 -0
- package/dist/brand.js +16 -0
- package/dist/config.d.ts +8 -3
- package/dist/config.js +26 -0
- package/dist/deploy.d.ts +1 -0
- package/dist/deploy.js +14 -0
- package/dist/device-auth.d.ts +29 -2
- package/dist/device-auth.js +82 -6
- package/dist/error-humanize.d.ts +15 -0
- package/dist/error-humanize.js +47 -0
- package/dist/index.js +94 -10
- package/dist/login.d.ts +9 -2
- package/dist/login.js +29 -6
- package/dist/model.js +3 -0
- package/dist/style.js +11 -0
- package/dist/tools.d.ts +4 -0
- package/dist/tools.js +12 -3
- package/dist/usage.d.ts +8 -0
- package/dist/usage.js +55 -34
- package/dist/web.js +14 -2
- package/package.json +1 -1
- package/scripts/patch-pi.js +53 -0
package/dist/agent-setup.d.ts
CHANGED
|
@@ -32,6 +32,15 @@ export declare function writeWebToolsExtension(cfg: CliConfig, features: {
|
|
|
32
32
|
webFetchRender: boolean;
|
|
33
33
|
imageGen: boolean;
|
|
34
34
|
}): void;
|
|
35
|
+
/**
|
|
36
|
+
* 生成「错误信息人话化」扩展到 <agentDir>/extensions/u1s1-error-humanize.js:
|
|
37
|
+
* 把「429: {json}」形态的模型调用错误翻成中文正文 + 状态码尾巴,TUI 和
|
|
38
|
+
* Desktop App 共用(同 compact-ui 的投影机制)。实现见 error-humanize.ts,
|
|
39
|
+
* 分类兼容性(重试/配额/上下文超长)的约束也写在那里。
|
|
40
|
+
* import 失败时静默跳过——Desktop 独立运行时若指向的 CLI dist 已不存在,
|
|
41
|
+
* 宁可回到原始错误文本,也不能让扩展报错刷英文堆栈。
|
|
42
|
+
*/
|
|
43
|
+
export declare function writeErrorHumanizeExtension(): void;
|
|
35
44
|
/**
|
|
36
45
|
* 生成「OpenRouter 应用归因」扩展到 <agentDir>/extensions/u1s1-attribution.js。
|
|
37
46
|
* 用户自己配 OPENROUTER_API_KEY 直连时,pi 内置的归因头归到 pi.dev 名下(且受遥测
|
package/dist/agent-setup.js
CHANGED
|
@@ -235,6 +235,35 @@ export function writeWebToolsExtension(cfg, features) {
|
|
|
235
235
|
subagentBlock +
|
|
236
236
|
`}\n`);
|
|
237
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* 生成「错误信息人话化」扩展到 <agentDir>/extensions/u1s1-error-humanize.js:
|
|
240
|
+
* 把「429: {json}」形态的模型调用错误翻成中文正文 + 状态码尾巴,TUI 和
|
|
241
|
+
* Desktop App 共用(同 compact-ui 的投影机制)。实现见 error-humanize.ts,
|
|
242
|
+
* 分类兼容性(重试/配额/上下文超长)的约束也写在那里。
|
|
243
|
+
* import 失败时静默跳过——Desktop 独立运行时若指向的 CLI dist 已不存在,
|
|
244
|
+
* 宁可回到原始错误文本,也不能让扩展报错刷英文堆栈。
|
|
245
|
+
*/
|
|
246
|
+
export function writeErrorHumanizeExtension() {
|
|
247
|
+
const dir = join(agentDir, "extensions");
|
|
248
|
+
mkdirSync(dir, { recursive: true });
|
|
249
|
+
const url = new URL("./error-humanize.js", import.meta.url).href;
|
|
250
|
+
writeFileSync(join(dir, "u1s1-error-humanize.js"), `// 由 u1s1 每次启动自动生成,请勿手改\n` +
|
|
251
|
+
`export default async function (pi) {\n` +
|
|
252
|
+
` let humanize;\n` +
|
|
253
|
+
` try {\n` +
|
|
254
|
+
` humanize = (await import(${JSON.stringify(url)})).humanizeModelError;\n` +
|
|
255
|
+
` } catch {\n` +
|
|
256
|
+
` return;\n` +
|
|
257
|
+
` }\n` +
|
|
258
|
+
` pi.on("message_end", (event) => {\n` +
|
|
259
|
+
` const msg = event.message;\n` +
|
|
260
|
+
` if (msg.role !== "assistant" || msg.stopReason !== "error" || !msg.errorMessage) return;\n` +
|
|
261
|
+
` const friendly = humanize(msg.errorMessage);\n` +
|
|
262
|
+
` if (!friendly) return;\n` +
|
|
263
|
+
` return { message: { ...msg, errorMessage: friendly } };\n` +
|
|
264
|
+
` });\n` +
|
|
265
|
+
`}\n`);
|
|
266
|
+
}
|
|
238
267
|
/**
|
|
239
268
|
* 生成「OpenRouter 应用归因」扩展到 <agentDir>/extensions/u1s1-attribution.js。
|
|
240
269
|
* 用户自己配 OPENROUTER_API_KEY 直连时,pi 内置的归因头归到 pi.dev 名下(且受遥测
|
|
@@ -404,7 +433,8 @@ export function toProviderModels(models) {
|
|
|
404
433
|
}
|
|
405
434
|
return {
|
|
406
435
|
id: m.id,
|
|
407
|
-
name
|
|
436
|
+
// 选择器只渲染 name 一行,把「免费/几倍」提示拼进去,切模型前就能看到
|
|
437
|
+
name: m.note ? `${m.name} · ${m.note}` : m.name,
|
|
408
438
|
reasoning: m.reasoning,
|
|
409
439
|
thinkingLevelMap,
|
|
410
440
|
input: m.vision ? ["text", "image"] : ["text"],
|
package/dist/api.d.ts
CHANGED
|
@@ -72,6 +72,8 @@ export interface ModelsResponse {
|
|
|
72
72
|
announcement?: ApiAnnouncement | null;
|
|
73
73
|
/** Opaque, device-bound canary echoed only by the official signing proxy. */
|
|
74
74
|
clientAttestation?: string;
|
|
75
|
+
/** Remaining lifetime of the canary, so the proxy can refresh it before it expires. */
|
|
76
|
+
clientAttestationExpiresInSeconds?: number;
|
|
75
77
|
}
|
|
76
78
|
/** 401:凭证失效(设备被移除/换过钥匙),调用方应引导重新登录而不是继续硬跑。 */
|
|
77
79
|
export declare class AuthError extends Error {
|
package/dist/api.js
CHANGED
|
@@ -82,11 +82,13 @@ export async function fetchModels(cfg) {
|
|
|
82
82
|
if (!body || !Array.isArray(body.data))
|
|
83
83
|
throw new Error("服务端模型列表格式不正确");
|
|
84
84
|
const token = body.client_attestation?.token;
|
|
85
|
+
const expiresIn = body.client_attestation?.expires_in;
|
|
85
86
|
return {
|
|
86
87
|
models: body.data,
|
|
87
88
|
features: body.features ?? {},
|
|
88
89
|
announcement: body.announcement,
|
|
89
90
|
clientAttestation: typeof token === "string" && token.length <= 1024 ? token : undefined,
|
|
91
|
+
clientAttestationExpiresInSeconds: typeof expiresIn === "number" && Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : undefined,
|
|
90
92
|
};
|
|
91
93
|
}
|
|
92
94
|
/**
|
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,7 @@ interface ApiModelShape {
|
|
|
43
47
|
* Convert an API model response to our internal ModelDef.
|
|
44
48
|
* Aliases are derived from the short id; note is left empty (filled by /model command).
|
|
45
49
|
*/
|
|
46
|
-
export declare function apiModelToDef(m:
|
|
50
|
+
export declare function apiModelToDef(m: ApiModel): ModelDef;
|
|
47
51
|
export interface ModelDef {
|
|
48
52
|
id: string;
|
|
49
53
|
name: string;
|
|
@@ -62,6 +66,8 @@ export interface ModelDef {
|
|
|
62
66
|
cacheRead: number;
|
|
63
67
|
cacheWrite: number;
|
|
64
68
|
};
|
|
69
|
+
/** 网关下发的免费用量包覆盖标记;老网关缺失时为 undefined(不派生 note)。 */
|
|
70
|
+
freeEligible?: boolean;
|
|
65
71
|
note: string;
|
|
66
72
|
}
|
|
67
73
|
export declare const MODELS: ModelDef[];
|
|
@@ -158,4 +164,3 @@ export declare function saveConfig(cfg: CliConfig): void;
|
|
|
158
164
|
export declare function persistPreferredModel(cfg: CliConfig, provider: string, modelId: string): CliConfig;
|
|
159
165
|
export declare function saveEndpointsCache(endpoints: CustomEndpoint[]): void;
|
|
160
166
|
export declare function loadEndpointsCache(): CustomEndpoint[];
|
|
161
|
-
export {};
|
package/dist/config.js
CHANGED
|
@@ -28,6 +28,7 @@ function defaultAliases(id) {
|
|
|
28
28
|
const aliases = [short, ...parts.filter((p) => p.length > 1)];
|
|
29
29
|
return [...new Set(aliases)];
|
|
30
30
|
}
|
|
31
|
+
// 与 workers/gateway/src/model-thinking.ts 的 THINKING_LEVELS 是同一契约,改动必须双侧同步(gateway test/thinking-levels-contract.test.ts 钉死)。
|
|
31
32
|
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
32
33
|
const THINKING_LEVEL_SET = new Set(THINKING_LEVELS);
|
|
33
34
|
function parseThinkingCapabilities(value) {
|
|
@@ -75,6 +76,7 @@ export function apiModelToDef(m) {
|
|
|
75
76
|
cacheRead: m.price.cache_read ?? 0,
|
|
76
77
|
cacheWrite: 0,
|
|
77
78
|
},
|
|
79
|
+
freeEligible: typeof m.free_package_eligible === "boolean" ? m.free_package_eligible : undefined,
|
|
78
80
|
note: "",
|
|
79
81
|
};
|
|
80
82
|
}
|
|
@@ -109,8 +111,32 @@ export const MODELS = [
|
|
|
109
111
|
note: "更强 · 但烧额度快约 20 倍,难题再用",
|
|
110
112
|
},
|
|
111
113
|
];
|
|
114
|
+
/**
|
|
115
|
+
* 按免费包覆盖标记 + 相对默认免费模型的价格倍数派生一句人话提示。
|
|
116
|
+
* TUI 的模型选择器只渲染 name 一行,note 会被拼进去(见 toProviderModels),
|
|
117
|
+
* 让人切模型前就知道「这个不走免费包 / 大概贵多少」。
|
|
118
|
+
*/
|
|
119
|
+
function deriveModelNote(m, base) {
|
|
120
|
+
if (m.freeEligible === true)
|
|
121
|
+
return "免费用量包可抵扣";
|
|
122
|
+
if (m.freeEligible === false) {
|
|
123
|
+
const blended = (m.cost.input + m.cost.output) / 2;
|
|
124
|
+
const baseBlended = base ? (base.cost.input + base.cost.output) / 2 : 0;
|
|
125
|
+
const mult = baseBlended > 0 ? Math.round(blended / baseBlended) : 0;
|
|
126
|
+
return mult >= 2
|
|
127
|
+
? `不走免费包 · 费用约为默认模型 ${mult} 倍`
|
|
128
|
+
: "不走免费包,用余额或全模型包";
|
|
129
|
+
}
|
|
130
|
+
return "";
|
|
131
|
+
}
|
|
112
132
|
/** Replace MODELS with a fresh list fetched from server (e.g. at startup). */
|
|
113
133
|
export function setModelsFromApi(apiModels) {
|
|
134
|
+
const base = apiModels.find((m) => m.id === DEFAULT_MODEL_ID)
|
|
135
|
+
?? apiModels.find((m) => m.freeEligible === true);
|
|
136
|
+
for (const m of apiModels) {
|
|
137
|
+
if (!m.note)
|
|
138
|
+
m.note = deriveModelNote(m, base);
|
|
139
|
+
}
|
|
114
140
|
MODELS.length = 0;
|
|
115
141
|
MODELS.push(...apiModels);
|
|
116
142
|
}
|
package/dist/deploy.d.ts
CHANGED
|
@@ -12,5 +12,6 @@ interface DeployArgs {
|
|
|
12
12
|
visibility?: SiteVisibility;
|
|
13
13
|
}
|
|
14
14
|
export declare function parseDeployArgs(args: string[]): DeployArgs;
|
|
15
|
+
export declare function printDeployHelp(): void;
|
|
15
16
|
export declare function deployCommand(cfg: CliConfig, args: string[]): Promise<void>;
|
|
16
17
|
export {};
|
package/dist/deploy.js
CHANGED
|
@@ -342,8 +342,22 @@ function printDeploymentResult(result, start) {
|
|
|
342
342
|
console.log(" 改完代码再跑一次 u1s1 deploy 即可更新。");
|
|
343
343
|
console.log("");
|
|
344
344
|
}
|
|
345
|
+
export function printDeployHelp() {
|
|
346
|
+
console.log("");
|
|
347
|
+
console.log(" u1s1 deploy [目录] [--name 站点名] [--public|--private]");
|
|
348
|
+
console.log("");
|
|
349
|
+
console.log(" 不带参数时自动找构建产物目录(dist/build/out 等),否则用当前目录。");
|
|
350
|
+
console.log(" 首次发布会问项目名和公开/私密,之后记住;再跑一次即为更新。");
|
|
351
|
+
console.log("");
|
|
352
|
+
console.log(" u1s1 deploy list 查看已发布的站点");
|
|
353
|
+
console.log("");
|
|
354
|
+
}
|
|
345
355
|
export async function deployCommand(cfg, args) {
|
|
346
356
|
// 参数:[dir] [--name xxx] [--public|--private];u1s1 deploy list 列出已有站点
|
|
357
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
358
|
+
printDeployHelp();
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
347
361
|
if (args[0] === "list") {
|
|
348
362
|
await listDeployments(cfg);
|
|
349
363
|
return;
|
package/dist/device-auth.d.ts
CHANGED
|
@@ -11,17 +11,44 @@ export declare function dpopHeaders(cfg: CliConfig, method: string, url: string)
|
|
|
11
11
|
export declare function authorizedFetch(cfg: CliConfig, input: string | URL, init?: RequestInit): Promise<Response>;
|
|
12
12
|
/** Buffer a loopback request only up to the same JSON limit enforced by Gateway. */
|
|
13
13
|
export declare function readSigningProxyRequestBody(request: IncomingMessage, maxBytes?: number): Promise<Buffer | undefined>;
|
|
14
|
+
/**
|
|
15
|
+
* How the proxy obtains and refreshes the device attestation canary. The token
|
|
16
|
+
* is device-bound and expires (7 days server-side); the proxy keeps it fresh so
|
|
17
|
+
* a healthy Gateway always sees a valid attestation instead of falling back to
|
|
18
|
+
* the weaker "legacy official" verdict when a transient /v1/models fetch missed.
|
|
19
|
+
*/
|
|
20
|
+
export interface ClientAttestationSource {
|
|
21
|
+
token?: string;
|
|
22
|
+
expiresInSeconds?: number;
|
|
23
|
+
/** Fetch a fresh token (e.g. via GET /v1/models). Single-flighted by the proxy. */
|
|
24
|
+
refresh?: () => Promise<{
|
|
25
|
+
token?: string;
|
|
26
|
+
expiresInSeconds?: number;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
interface AttestationHolder {
|
|
30
|
+
token?: string;
|
|
31
|
+
expiresAtMs?: number;
|
|
32
|
+
refresh?: () => Promise<{
|
|
33
|
+
token?: string;
|
|
34
|
+
expiresInSeconds?: number;
|
|
35
|
+
}>;
|
|
36
|
+
refreshing?: Promise<void>;
|
|
37
|
+
lastFailureMs?: number;
|
|
38
|
+
}
|
|
14
39
|
interface SigningProxy {
|
|
15
40
|
baseUrl: string;
|
|
16
41
|
localKey: string;
|
|
17
42
|
token: string;
|
|
18
43
|
client: ClientSurface;
|
|
19
|
-
|
|
44
|
+
attestation: AttestationHolder;
|
|
20
45
|
}
|
|
46
|
+
/** Attach a fresh (self-healing) attestation header to an outbound proxy request. */
|
|
47
|
+
export declare function attachAttestationHeader(holder: AttestationHolder, headers: Headers): Promise<void>;
|
|
21
48
|
export type ClientSurface = "terminal" | "web" | "desktop" | "cloud";
|
|
22
49
|
/**
|
|
23
50
|
* pi accepts static provider headers only. Keep it behind a loopback proxy that
|
|
24
51
|
* replaces the local bearer credential with a fresh DPoP proof per request.
|
|
25
52
|
*/
|
|
26
|
-
export declare function ensureSigningProxy(cfg: CliConfig, fallbackClient?: ClientSurface,
|
|
53
|
+
export declare function ensureSigningProxy(cfg: CliConfig, fallbackClient?: ClientSurface, attestationSource?: ClientAttestationSource): Promise<SigningProxy>;
|
|
27
54
|
export {};
|
package/dist/device-auth.js
CHANGED
|
@@ -110,7 +110,81 @@ export async function readSigningProxyRequestBody(request, maxBytes = SIGNING_PR
|
|
|
110
110
|
}
|
|
111
111
|
return chunks.length ? Buffer.concat(chunks, total) : undefined;
|
|
112
112
|
}
|
|
113
|
+
// Refresh once we are within a day of the server-side 7-day expiry; back off for
|
|
114
|
+
// a while after a failed refresh so a flaky Gateway is not hammered per request.
|
|
115
|
+
const ATTESTATION_REFRESH_MARGIN_MS = 24 * 60 * 60 * 1000;
|
|
116
|
+
const ATTESTATION_REFRESH_COOLDOWN_MS = 30_000;
|
|
117
|
+
// Only ever block a request while we have no token at all, and even then only
|
|
118
|
+
// briefly — a Gateway too slow to answer would fail the real request anyway.
|
|
119
|
+
const ATTESTATION_BLOCK_TIMEOUT_MS = 4_000;
|
|
113
120
|
let signingProxy;
|
|
121
|
+
function unrefDelay(ms) {
|
|
122
|
+
return new Promise((resolve) => {
|
|
123
|
+
const timer = setTimeout(resolve, ms);
|
|
124
|
+
if (typeof timer.unref === "function")
|
|
125
|
+
timer.unref();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function applyAttestation(holder, result) {
|
|
129
|
+
if (typeof result.token !== "string" || result.token.length === 0 || result.token.length > 1024)
|
|
130
|
+
return;
|
|
131
|
+
holder.token = result.token;
|
|
132
|
+
holder.expiresAtMs = typeof result.expiresInSeconds === "number" && result.expiresInSeconds > 0
|
|
133
|
+
? Date.now() + result.expiresInSeconds * 1000
|
|
134
|
+
: undefined;
|
|
135
|
+
}
|
|
136
|
+
/** Seed the mutable holder from a new ensureSigningProxy call without dropping a fresher token. */
|
|
137
|
+
function updateAttestationHolder(holder, source) {
|
|
138
|
+
if (!source)
|
|
139
|
+
return;
|
|
140
|
+
if (source.refresh)
|
|
141
|
+
holder.refresh = source.refresh;
|
|
142
|
+
if (source.token && !holder.token) {
|
|
143
|
+
applyAttestation(holder, { token: source.token, expiresInSeconds: source.expiresInSeconds });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Kick off a single-flight refresh when the token is missing or nearing expiry.
|
|
148
|
+
* Returns the in-flight refresh promise (or undefined when no refresh is due),
|
|
149
|
+
* so the caller can decide whether to wait for it.
|
|
150
|
+
*/
|
|
151
|
+
function ensureFreshAttestation(holder) {
|
|
152
|
+
if (!holder.refresh)
|
|
153
|
+
return undefined;
|
|
154
|
+
const now = Date.now();
|
|
155
|
+
const stale = !holder.token
|
|
156
|
+
|| (holder.expiresAtMs !== undefined && now >= holder.expiresAtMs - ATTESTATION_REFRESH_MARGIN_MS);
|
|
157
|
+
if (!stale)
|
|
158
|
+
return undefined;
|
|
159
|
+
if (holder.refreshing)
|
|
160
|
+
return holder.refreshing;
|
|
161
|
+
if (holder.lastFailureMs !== undefined && now - holder.lastFailureMs < ATTESTATION_REFRESH_COOLDOWN_MS) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
const run = (async () => {
|
|
165
|
+
try {
|
|
166
|
+
applyAttestation(holder, await holder.refresh());
|
|
167
|
+
holder.lastFailureMs = undefined;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
holder.lastFailureMs = Date.now();
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
holder.refreshing = undefined;
|
|
174
|
+
}
|
|
175
|
+
})();
|
|
176
|
+
holder.refreshing = run;
|
|
177
|
+
return run;
|
|
178
|
+
}
|
|
179
|
+
/** Attach a fresh (self-healing) attestation header to an outbound proxy request. */
|
|
180
|
+
export async function attachAttestationHeader(holder, headers) {
|
|
181
|
+
const refreshing = ensureFreshAttestation(holder);
|
|
182
|
+
if (refreshing && !holder.token) {
|
|
183
|
+
await Promise.race([refreshing, unrefDelay(ATTESTATION_BLOCK_TIMEOUT_MS)]);
|
|
184
|
+
}
|
|
185
|
+
if (holder.token)
|
|
186
|
+
headers.set("x-u1s1-attestation", holder.token);
|
|
187
|
+
}
|
|
114
188
|
function clientSurface(fallback) {
|
|
115
189
|
const explicit = process.env["U1S1_CLIENT"];
|
|
116
190
|
if (explicit === "terminal" || explicit === "web" || explicit === "desktop" || explicit === "cloud") {
|
|
@@ -122,14 +196,17 @@ function clientSurface(fallback) {
|
|
|
122
196
|
* pi accepts static provider headers only. Keep it behind a loopback proxy that
|
|
123
197
|
* replaces the local bearer credential with a fresh DPoP proof per request.
|
|
124
198
|
*/
|
|
125
|
-
export async function ensureSigningProxy(cfg, fallbackClient = "terminal",
|
|
199
|
+
export async function ensureSigningProxy(cfg, fallbackClient = "terminal", attestationSource) {
|
|
126
200
|
if (!hasDeviceCredential(cfg))
|
|
127
201
|
throw new Error("当前安装需要重新登录,以创建设备凭证");
|
|
128
202
|
const client = clientSurface(fallbackClient);
|
|
129
203
|
const current = signingProxy;
|
|
130
|
-
if (current && current.token === cfg.deviceToken && current.client === client
|
|
131
|
-
|
|
204
|
+
if (current && current.token === cfg.deviceToken && current.client === client) {
|
|
205
|
+
updateAttestationHolder(current.attestation, attestationSource);
|
|
132
206
|
return current;
|
|
207
|
+
}
|
|
208
|
+
const attestation = {};
|
|
209
|
+
updateAttestationHolder(attestation, attestationSource);
|
|
133
210
|
const localKey = `local-${randomBytes(32).toString("hex")}`;
|
|
134
211
|
const upstreamOrigin = new URL(cfg.baseUrl).origin;
|
|
135
212
|
const server = createServer(async (req, res) => {
|
|
@@ -163,8 +240,7 @@ export async function ensureSigningProxy(cfg, fallbackClient = "terminal", clien
|
|
|
163
240
|
outboundHeaders.set("x-u1s1-client", client);
|
|
164
241
|
outboundHeaders.set("x-u1s1-version", VERSION);
|
|
165
242
|
outboundHeaders.set("x-u1s1-platform", `${process.platform}-${process.arch}`);
|
|
166
|
-
|
|
167
|
-
outboundHeaders.set("x-u1s1-attestation", clientAttestation);
|
|
243
|
+
await attachAttestationHeader(attestation, outboundHeaders);
|
|
168
244
|
const upstream = await authorizedFetch(cfg, target, {
|
|
169
245
|
method: req.method ?? "GET",
|
|
170
246
|
headers: outboundHeaders,
|
|
@@ -216,7 +292,7 @@ export async function ensureSigningProxy(cfg, fallbackClient = "terminal", clien
|
|
|
216
292
|
localKey,
|
|
217
293
|
token: cfg.deviceToken,
|
|
218
294
|
client,
|
|
219
|
-
|
|
295
|
+
attestation,
|
|
220
296
|
};
|
|
221
297
|
return signingProxy;
|
|
222
298
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模型调用失败时,网关的中文说明会被 OpenAI SDK + pi-ai 包成
|
|
3
|
+
* `429: {"message":"…","type":"…","code":"…"}` 直出到聊天区。
|
|
4
|
+
* 这里把 JSON 里的 message 提出来当正文,状态码与错误代号收进尾巴。
|
|
5
|
+
*
|
|
6
|
+
* 尾巴不是装饰,必须保留:pi 按 errorMessage 正则做三类分类——
|
|
7
|
+
* - 重试:429/5xx 数字、rate_limit、fetch failed 等(retry.js RETRYABLE)
|
|
8
|
+
* - 不重试:insufficient_quota(额度用尽时快速失败,不做无谓退避)
|
|
9
|
+
* - 上下文超长:context_length_exceeded(触发自动压缩而非重试)
|
|
10
|
+
* 改写后的文本必须与原文落进同一分类,否则会破坏重试/压缩行为。
|
|
11
|
+
*
|
|
12
|
+
* 网关会在错误 JSON 里注入 error.request_id(见 gateway request-id.ts),
|
|
13
|
+
* 拼进尾巴让用户报障时有编号可给,客服凭它直查日志和 usage 记录。
|
|
14
|
+
*/
|
|
15
|
+
export declare function humanizeModelError(raw: string): string | undefined;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模型调用失败时,网关的中文说明会被 OpenAI SDK + pi-ai 包成
|
|
3
|
+
* `429: {"message":"…","type":"…","code":"…"}` 直出到聊天区。
|
|
4
|
+
* 这里把 JSON 里的 message 提出来当正文,状态码与错误代号收进尾巴。
|
|
5
|
+
*
|
|
6
|
+
* 尾巴不是装饰,必须保留:pi 按 errorMessage 正则做三类分类——
|
|
7
|
+
* - 重试:429/5xx 数字、rate_limit、fetch failed 等(retry.js RETRYABLE)
|
|
8
|
+
* - 不重试:insufficient_quota(额度用尽时快速失败,不做无谓退避)
|
|
9
|
+
* - 上下文超长:context_length_exceeded(触发自动压缩而非重试)
|
|
10
|
+
* 改写后的文本必须与原文落进同一分类,否则会破坏重试/压缩行为。
|
|
11
|
+
*
|
|
12
|
+
* 网关会在错误 JSON 里注入 error.request_id(见 gateway request-id.ts),
|
|
13
|
+
* 拼进尾巴让用户报障时有编号可给,客服凭它直查日志和 usage 记录。
|
|
14
|
+
*/
|
|
15
|
+
export function humanizeModelError(raw) {
|
|
16
|
+
const jsonStart = raw.indexOf("{");
|
|
17
|
+
const jsonEnd = raw.lastIndexOf("}");
|
|
18
|
+
if (jsonStart === -1 || jsonEnd <= jsonStart)
|
|
19
|
+
return undefined;
|
|
20
|
+
let parsed;
|
|
21
|
+
try {
|
|
22
|
+
parsed = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
if (parsed === null || typeof parsed !== "object")
|
|
28
|
+
return undefined;
|
|
29
|
+
const root = parsed;
|
|
30
|
+
const err = root["error"] !== null && typeof root["error"] === "object"
|
|
31
|
+
? root["error"]
|
|
32
|
+
: root;
|
|
33
|
+
const message = typeof err["message"] === "string" ? err["message"].trim() : "";
|
|
34
|
+
if (!message)
|
|
35
|
+
return undefined;
|
|
36
|
+
const code = typeof err["code"] === "string" ? err["code"] : "";
|
|
37
|
+
const type = typeof err["type"] === "string" ? err["type"] : "";
|
|
38
|
+
const status = /(?:^|\s|\()(\d{3})\b/.exec(raw.slice(0, jsonStart))?.[1];
|
|
39
|
+
const requestId = typeof err["request_id"] === "string" ? err["request_id"] : "";
|
|
40
|
+
const tags = [
|
|
41
|
+
status ? `HTTP ${status}` : "",
|
|
42
|
+
type === "insufficient_quota" || code === "quota_exceeded" ? "insufficient_quota" : code,
|
|
43
|
+
requestId ? `请求编号 ${requestId}` : "",
|
|
44
|
+
].filter(Boolean);
|
|
45
|
+
const friendly = tags.length ? `${message} (${tags.join(" · ")})` : message;
|
|
46
|
+
return friendly === raw ? undefined : friendly;
|
|
47
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execSync, spawnSync } from "node:child_process";
|
|
3
3
|
import { writeFileSync } from "node:fs";
|
|
4
|
-
import { cleanupBrandThemes, endpointKeyEnv, endpointProviderEntry, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, ensureWorkflowPromptTemplate, scrubForeignProviderEnv, toProviderModels, writeAttributionExtension, writeCompactUiExtension, writeWebToolsExtension, } from "./agent-setup.js";
|
|
4
|
+
import { cleanupBrandThemes, endpointKeyEnv, endpointProviderEntry, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, ensureWorkflowPromptTemplate, scrubForeignProviderEnv, toProviderModels, writeAttributionExtension, writeCompactUiExtension, writeErrorHumanizeExtension, writeWebToolsExtension, } from "./agent-setup.js";
|
|
5
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
5
6
|
import { printConsoleBanner } from "./brand.js";
|
|
6
7
|
import { agentDir, apiModelToDef, CUSTOM_ENDPOINTS, isPortableInstall, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, readSettings, refValid, resolvePreferredModel, setModelsFromApi, VERSION, } from "./config.js";
|
|
7
8
|
import { registerLoopCommand } from "./loop.js";
|
|
@@ -197,7 +198,17 @@ async function runAgent(cfg, args) {
|
|
|
197
198
|
// it after the live model list arrives; explicit user choices remain intact.
|
|
198
199
|
ensureDefaultSettings(MODELS);
|
|
199
200
|
// pi provider 只支持静态 header;指向本机 signing proxy,由它逐请求附 DPoP proof。
|
|
200
|
-
const signing = await ensureSigningProxy(cfg, "terminal",
|
|
201
|
+
const signing = await ensureSigningProxy(cfg, "terminal", {
|
|
202
|
+
token: modelsResp?.clientAttestation,
|
|
203
|
+
expiresInSeconds: modelsResp?.clientAttestationExpiresInSeconds,
|
|
204
|
+
refresh: async () => {
|
|
205
|
+
const refreshed = await fetchModels(cfg);
|
|
206
|
+
return {
|
|
207
|
+
token: refreshed.clientAttestation,
|
|
208
|
+
expiresInSeconds: refreshed.clientAttestationExpiresInSeconds,
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
});
|
|
201
212
|
const officialCfg = { ...cfg, baseUrl: signing.baseUrl, apiKey: signing.localKey };
|
|
202
213
|
ensureBrandPrompt(await shellReady);
|
|
203
214
|
ensureProviderModels(officialCfg);
|
|
@@ -210,6 +221,8 @@ async function runAgent(cfg, args) {
|
|
|
210
221
|
});
|
|
211
222
|
// 精简 UI:隐藏工具调用 + 汇总行 + 隐藏思考标签(同样投影到 agentDir/extensions)
|
|
212
223
|
writeCompactUiExtension();
|
|
224
|
+
// 模型调用错误人话化(同样投影,Desktop App 共享)
|
|
225
|
+
writeErrorHumanizeExtension();
|
|
213
226
|
// OpenRouter 应用归因:直连流量计入 u1s1 的公开排行
|
|
214
227
|
writeAttributionExtension();
|
|
215
228
|
ensureTmuxKeyboardProtocol();
|
|
@@ -286,6 +299,41 @@ async function runAgent(cfg, args) {
|
|
|
286
299
|
ctx.shutdown();
|
|
287
300
|
},
|
|
288
301
|
});
|
|
302
|
+
// /help:启动横幅和官网教程都在引导用户输入它,必须真实存在
|
|
303
|
+
pi.registerEntryRenderer("u1s1-help", (_entry, _opts, theme) => {
|
|
304
|
+
const text = [
|
|
305
|
+
theme.bold(theme.fg("text", "u1s1 常用操作")),
|
|
306
|
+
theme.fg("text", "直接打字说需求,回车发送 · Shift+Enter 换行 · Ctrl+V 贴图 · Esc 中断"),
|
|
307
|
+
theme.fg("text", "/model 切换模型(留意免费/价格提示) · /clear 清空上下文开新会话"),
|
|
308
|
+
theme.fg("text", "/usage 查剩余额度 · /resume 恢复历史会话 · /settings 设置"),
|
|
309
|
+
theme.fg("dim", "/compact 压缩上下文 · /hotkeys 全部快捷键(英文) · /exit 退出"),
|
|
310
|
+
theme.fg("dim", "退出后在终端:u1s1 deploy 发布网页 · u1s1 update 升级"),
|
|
311
|
+
theme.fg("dim", "新手教程 → https://u1s1.io/guides"),
|
|
312
|
+
].join("\n");
|
|
313
|
+
return new Text(text, 1, 0);
|
|
314
|
+
});
|
|
315
|
+
pi.registerCommand("help", {
|
|
316
|
+
description: "查看常用操作和命令",
|
|
317
|
+
handler: async () => {
|
|
318
|
+
pi.appendEntry("u1s1-help");
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
// /usage:会话内查额度。此前额度只有用完撞 429 才可见(审计 G1)
|
|
322
|
+
pi.registerEntryRenderer("u1s1-usage", (entry, _opts, theme) => {
|
|
323
|
+
const data = entry.data ?? {};
|
|
324
|
+
if (data.error)
|
|
325
|
+
return new Text(theme.fg("dim", data.error), 1, 0);
|
|
326
|
+
return new Text((data.lines ?? []).map((l) => theme.fg("text", l)).join("\n"), 1, 0);
|
|
327
|
+
});
|
|
328
|
+
pi.registerCommand("usage", {
|
|
329
|
+
description: "查看剩余额度和本月用量",
|
|
330
|
+
handler: async () => {
|
|
331
|
+
const { usageEntryData } = await import("./usage.js");
|
|
332
|
+
pi.appendEntry("u1s1-usage", await usageEntryData());
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
// 模型调用错误人话化不在这里注册:走 writeErrorHumanizeExtension 投影,
|
|
336
|
+
// TUI 和 Desktop App 共用一份(见 agent-setup.ts)
|
|
289
337
|
// /clear: 清空当前对话上下文,开始新会话
|
|
290
338
|
pi.registerCommand("clear", {
|
|
291
339
|
description: "清除当前对话上下文,开始新会话",
|
|
@@ -348,21 +396,23 @@ async function run() {
|
|
|
348
396
|
console.log(`u1s1 v${VERSION}`);
|
|
349
397
|
return;
|
|
350
398
|
}
|
|
351
|
-
if (cmd === "--help" || cmd === "-h") {
|
|
399
|
+
if (cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
352
400
|
printConsoleBanner(VERSION);
|
|
353
|
-
console.log(" u1s1 命令:deploy(发布网页,可选 --public / --private)· login / logout · model · usage · update · import · bench");
|
|
354
|
-
console.log("");
|
|
355
401
|
console.log(" 命令:");
|
|
402
|
+
console.log(" u1s1 进入对话界面(最常用,不带参数)");
|
|
403
|
+
console.log(" u1s1 -p \"一句话\" 不进界面直接回答,适合脚本里用");
|
|
356
404
|
console.log(" u1s1 login / logout 登录 / 退出登录");
|
|
357
405
|
console.log(" u1s1 model 查看或切换默认模型");
|
|
358
|
-
console.log(" u1s1 usage
|
|
406
|
+
console.log(" u1s1 usage 查看剩余额度");
|
|
359
407
|
console.log(" u1s1 update 升级到最新版");
|
|
360
|
-
console.log(" u1s1 deploy
|
|
408
|
+
console.log(" u1s1 deploy 发布网页(--public / --private)");
|
|
409
|
+
console.log(" u1s1 deploy list 查看已发布的站点");
|
|
361
410
|
console.log(" u1s1 import 导入历史会话");
|
|
411
|
+
console.log(" u1s1 bench 模型编码能力评测");
|
|
362
412
|
console.log(" u1s1 --version 查看版本");
|
|
363
413
|
console.log("");
|
|
364
|
-
console.log(" 对话里输入 /
|
|
365
|
-
console.log(" 更多帮助 → https://u1s1.io/
|
|
414
|
+
console.log(" 对话里输入 /help 看会话内命令(/model /clear /exit …)");
|
|
415
|
+
console.log(" 更多帮助 → https://u1s1.io/guides");
|
|
366
416
|
return;
|
|
367
417
|
}
|
|
368
418
|
if (cmd === "web") {
|
|
@@ -371,9 +421,14 @@ async function run() {
|
|
|
371
421
|
return;
|
|
372
422
|
}
|
|
373
423
|
if (cmd === "deploy") {
|
|
424
|
+
const { deployCommand, printDeployHelp } = await import("./deploy.js");
|
|
425
|
+
// --help 不该先被拽去登录
|
|
426
|
+
if (args.slice(1).includes("--help") || args.slice(1).includes("-h")) {
|
|
427
|
+
printDeployHelp();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
374
430
|
const { ensureAuth } = await import("./login.js");
|
|
375
431
|
const cfg = await ensureAuth();
|
|
376
|
-
const { deployCommand } = await import("./deploy.js");
|
|
377
432
|
await deployCommand(cfg, args.slice(1));
|
|
378
433
|
return;
|
|
379
434
|
}
|
|
@@ -437,6 +492,20 @@ async function run() {
|
|
|
437
492
|
await benchCommand(args.slice(1));
|
|
438
493
|
return;
|
|
439
494
|
}
|
|
495
|
+
// 拼错的子命令(如 `u1s1 depoly`)不该被静默当成第一条消息发给模型白烧 token。
|
|
496
|
+
// 只拦「单个短英文词 + 与已知命令编辑距离很近」的情况,正常 prompt 不受影响。
|
|
497
|
+
if (cmd
|
|
498
|
+
&& args.length === 1
|
|
499
|
+
&& /^[a-z][a-z0-9-]{1,15}$/.test(cmd)
|
|
500
|
+
&& !KNOWN_COMMANDS.includes(cmd)) {
|
|
501
|
+
const near = KNOWN_COMMANDS.find((k) => editDistance(k, cmd) <= (k.length <= 4 ? 1 : 2));
|
|
502
|
+
if (near) {
|
|
503
|
+
console.error(`不认识的命令「${cmd}」,你是不是想运行:u1s1 ${near}`);
|
|
504
|
+
console.error("查看全部命令:u1s1 --help;直接开始对话:运行 u1s1(不带参数)");
|
|
505
|
+
process.exitCode = 1;
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
440
509
|
const { ensureAuth } = await import("./login.js");
|
|
441
510
|
const cfg = await ensureAuth();
|
|
442
511
|
// 在后台检查更新(非阻塞,不影响启动速度)
|
|
@@ -445,6 +514,21 @@ async function run() {
|
|
|
445
514
|
// autoUpdate 开着且启动时发现了新版:现在装(TUI 已退出,原地重铺安全)
|
|
446
515
|
installPendingUpdate();
|
|
447
516
|
}
|
|
517
|
+
const KNOWN_COMMANDS = ["deploy", "login", "logout", "usage", "model", "update", "import", "bench", "help", "web"];
|
|
518
|
+
/** 经典 Levenshtein,命令名都很短,O(nm) 足够。 */
|
|
519
|
+
function editDistance(a, b) {
|
|
520
|
+
const dp = Array.from({ length: a.length + 1 }, (_, i) => i);
|
|
521
|
+
for (let j = 1; j <= b.length; j++) {
|
|
522
|
+
let prev = dp[0];
|
|
523
|
+
dp[0] = j;
|
|
524
|
+
for (let i = 1; i <= a.length; i++) {
|
|
525
|
+
const cur = dp[i];
|
|
526
|
+
dp[i] = Math.min(dp[i] + 1, dp[i - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
527
|
+
prev = cur;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return dp[a.length];
|
|
531
|
+
}
|
|
448
532
|
run().catch((e) => {
|
|
449
533
|
console.error(e instanceof Error ? e.message : e);
|
|
450
534
|
process.exit(1);
|
package/dist/login.d.ts
CHANGED
|
@@ -10,8 +10,15 @@ export interface DeviceStart {
|
|
|
10
10
|
private_jwk: webcrypto.JsonWebKey;
|
|
11
11
|
public_jwk: webcrypto.JsonWebKey;
|
|
12
12
|
}
|
|
13
|
-
/**
|
|
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([
|
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/tools.d.ts
CHANGED
|
@@ -58,6 +58,10 @@ export declare function createImageTool(cfg: Pick<CliConfig, "baseUrl" | "apiKey
|
|
|
58
58
|
path: string;
|
|
59
59
|
size: string | null;
|
|
60
60
|
bytes: number;
|
|
61
|
+
displayImage: {
|
|
62
|
+
data: string;
|
|
63
|
+
mimeType: string;
|
|
64
|
+
};
|
|
61
65
|
}, any> & import("@earendil-works/pi-coding-agent").ToolDefinition<any, any, any>;
|
|
62
66
|
export interface FetchToolConfig {
|
|
63
67
|
baseUrl: string;
|
package/dist/tools.js
CHANGED
|
@@ -406,7 +406,7 @@ export function createImageTool(cfg) {
|
|
|
406
406
|
name: "generate_image",
|
|
407
407
|
label: "生成图片",
|
|
408
408
|
description: "Generate an image from a text prompt, or edit/compose existing images, using the Seedream image model. " +
|
|
409
|
-
"Saves the result as a local image file and
|
|
409
|
+
"Saves the result as a local image file and displays it directly in supported chat UIs. " +
|
|
410
410
|
"Pass local file paths or http(s) URLs in `images` to edit an image or use references (style transfer, adding elements, combining up to 10 images). " +
|
|
411
411
|
"Prompts work in Chinese or English; describe content, style, composition, and any text to render.",
|
|
412
412
|
promptSnippet: "AI image generation/editing (text-to-image, editing, multi-image composition)",
|
|
@@ -415,6 +415,7 @@ export function createImageTool(cfg) {
|
|
|
415
415
|
"Each call generates one image and costs the user credits; refine the prompt first instead of regenerating repeatedly.",
|
|
416
416
|
"If an error says the image may/already has been generated or says not to call generate_image again, stop immediately; never retry with a new generation.",
|
|
417
417
|
"To edit an existing image, pass its path in `images` and describe only the change in `prompt`.",
|
|
418
|
+
"A successful result is already displayed in supported chat UIs; never open an OS image viewer or claim that you opened one when the user asks to see it.",
|
|
418
419
|
],
|
|
419
420
|
parameters: Type.Object({
|
|
420
421
|
prompt: Type.String({
|
|
@@ -442,9 +443,17 @@ export function createImageTool(cfg) {
|
|
|
442
443
|
mkdirSync(dirname(path), { recursive: true });
|
|
443
444
|
writeFileSync(path, bytes);
|
|
444
445
|
const sizeNote = result.size ? ` (${result.size})` : "";
|
|
446
|
+
const mimeType = REF_IMAGE_MIME[urlExt] ?? "image/jpeg";
|
|
445
447
|
return {
|
|
446
|
-
content: [{ type: "text", text:
|
|
447
|
-
|
|
448
|
+
content: [{ type: "text", text: `图片已保存并在对话中展示: ${path}${sizeNote}` }],
|
|
449
|
+
// displayImage 只给 UI 预览,不放进 content:避免每轮都把整张 2K/4K
|
|
450
|
+
// 图片送回模型占用 context。pi-web-ui 会从 tool details 序列化它。
|
|
451
|
+
details: {
|
|
452
|
+
path,
|
|
453
|
+
size: result.size,
|
|
454
|
+
bytes: bytes.byteLength,
|
|
455
|
+
displayImage: { data: Buffer.from(bytes).toString("base64"), mimeType },
|
|
456
|
+
},
|
|
448
457
|
};
|
|
449
458
|
},
|
|
450
459
|
});
|
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/dist/web.js
CHANGED
|
@@ -55,21 +55,33 @@ export async function prepareWebEnv(cfg) {
|
|
|
55
55
|
// 老网关没有 /v1/image,image_gen 缺失时按关闭处理,不注册生图工具
|
|
56
56
|
let imageGenEnabled = false;
|
|
57
57
|
let clientAttestation;
|
|
58
|
+
let clientAttestationExpiresInSeconds;
|
|
58
59
|
const endpointsReady = loadCustomEndpoints(cfg);
|
|
59
60
|
try {
|
|
60
|
-
const { models, features, clientAttestation: attestation } = await fetchModels(cfg);
|
|
61
|
+
const { models, features, clientAttestation: attestation, clientAttestationExpiresInSeconds: attestationTtl } = await fetchModels(cfg);
|
|
61
62
|
setModelsFromApi(models.map(apiModelToDef));
|
|
62
63
|
webSearchEnabled = features.web_search !== false;
|
|
63
64
|
webFetchRenderEnabled = features.web_fetch_render === true;
|
|
64
65
|
imageGenEnabled = features.image_gen === true;
|
|
65
66
|
clientAttestation = attestation;
|
|
67
|
+
clientAttestationExpiresInSeconds = attestationTtl;
|
|
66
68
|
}
|
|
67
69
|
catch (e) {
|
|
68
70
|
console.error(" 获取模型列表失败,使用内置列表:", e.message);
|
|
69
71
|
}
|
|
70
72
|
await endpointsReady;
|
|
71
73
|
ensureDefaultSettings(MODELS);
|
|
72
|
-
const signing = await ensureSigningProxy(cfg, "desktop",
|
|
74
|
+
const signing = await ensureSigningProxy(cfg, "desktop", {
|
|
75
|
+
token: clientAttestation,
|
|
76
|
+
expiresInSeconds: clientAttestationExpiresInSeconds,
|
|
77
|
+
refresh: async () => {
|
|
78
|
+
const refreshed = await fetchModels(cfg);
|
|
79
|
+
return {
|
|
80
|
+
token: refreshed.clientAttestation,
|
|
81
|
+
expiresInSeconds: refreshed.clientAttestationExpiresInSeconds,
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
});
|
|
73
85
|
const officialCfg = { ...cfg, baseUrl: signing.baseUrl, apiKey: signing.localKey };
|
|
74
86
|
webOfficialCfg = officialCfg;
|
|
75
87
|
const modelsPath = refreshWebModels();
|
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}),不影响安装`);
|