u1s1-cli-cn 0.0.0-stage → 1.15.2
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/README.md +71 -2
- package/bench/suite.json +193 -0
- package/dist/agent-setup.d.ts +169 -0
- package/dist/agent-setup.js +724 -0
- package/dist/agents/agent-config.d.ts +83 -0
- package/dist/agents/agent-config.js +154 -0
- package/dist/agents/claude.d.ts +113 -0
- package/dist/agents/claude.js +411 -0
- package/dist/agents/codex.d.ts +111 -0
- package/dist/agents/codex.js +392 -0
- package/dist/agents/launch.d.ts +109 -0
- package/dist/agents/launch.js +444 -0
- package/dist/agents/pi.d.ts +9 -0
- package/dist/agents/pi.js +560 -0
- package/dist/agents/prefs.d.ts +35 -0
- package/dist/agents/prefs.js +96 -0
- package/dist/agents/runtime.d.ts +354 -0
- package/dist/agents/runtime.js +884 -0
- package/dist/agents/status.d.ts +20 -0
- package/dist/agents/status.js +241 -0
- package/dist/announcements-poll.d.ts +22 -0
- package/dist/announcements-poll.js +59 -0
- package/dist/api.d.ts +147 -0
- package/dist/api.js +366 -0
- package/dist/bench-report.d.ts +22 -0
- package/dist/bench-report.js +161 -0
- package/dist/bench-scoring.d.ts +8 -0
- package/dist/bench-scoring.js +101 -0
- package/dist/bench-types.d.ts +50 -0
- package/dist/bench-types.js +1 -0
- package/dist/bench.d.ts +1 -0
- package/dist/bench.js +470 -0
- package/dist/brand.d.ts +42 -0
- package/dist/brand.js +165 -0
- package/dist/config-command.d.ts +18 -0
- package/dist/config-command.js +335 -0
- package/dist/config.d.ts +284 -0
- package/dist/config.js +526 -0
- package/dist/context-usage.d.ts +18 -0
- package/dist/context-usage.js +50 -0
- package/dist/deploy.d.ts +22 -0
- package/dist/deploy.js +447 -0
- package/dist/device-auth.d.ts +101 -0
- package/dist/device-auth.js +363 -0
- package/dist/embed.d.ts +26 -0
- package/dist/embed.js +28 -0
- package/dist/error-humanize.d.ts +24 -0
- package/dist/error-humanize.js +115 -0
- package/dist/feedback.d.ts +68 -0
- package/dist/feedback.js +229 -0
- package/dist/gateway-route.d.ts +57 -0
- package/dist/gateway-route.js +157 -0
- package/dist/i18n.d.ts +21 -0
- package/dist/i18n.js +46 -0
- package/dist/import/claude.d.ts +4 -0
- package/dist/import/claude.js +405 -0
- package/dist/import/codex.d.ts +4 -0
- package/dist/import/codex.js +389 -0
- package/dist/import/index.d.ts +1 -0
- package/dist/import/index.js +397 -0
- package/dist/import/skills.d.ts +89 -0
- package/dist/import/skills.js +509 -0
- package/dist/import/types.d.ts +104 -0
- package/dist/import/types.js +1 -0
- package/dist/import/util.d.ts +27 -0
- package/dist/import/util.js +241 -0
- package/dist/import/write.d.ts +4 -0
- package/dist/import/write.js +98 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +342 -0
- package/dist/launcher.d.ts +48 -0
- package/dist/launcher.js +235 -0
- package/dist/login.d.ts +46 -0
- package/dist/login.js +300 -0
- package/dist/loop.d.ts +2 -0
- package/dist/loop.js +147 -0
- package/dist/mcp/client.d.ts +53 -0
- package/dist/mcp/client.js +450 -0
- package/dist/mcp/command.d.ts +25 -0
- package/dist/mcp/command.js +397 -0
- package/dist/mcp/config.d.ts +78 -0
- package/dist/mcp/config.js +224 -0
- package/dist/mcp/extension.d.ts +14 -0
- package/dist/mcp/extension.js +130 -0
- package/dist/mcp/tools.d.ts +24 -0
- package/dist/mcp/tools.js +101 -0
- package/dist/mcp-server.d.ts +105 -0
- package/dist/mcp-server.js +449 -0
- package/dist/model.d.ts +7 -0
- package/dist/model.js +76 -0
- package/dist/nudges.d.ts +27 -0
- package/dist/nudges.js +59 -0
- package/dist/pi-ui-text.d.ts +57 -0
- package/dist/pi-ui-text.js +148 -0
- package/dist/prompt.d.ts +5 -0
- package/dist/prompt.js +17 -0
- package/dist/request-trace.d.ts +12 -0
- package/dist/request-trace.js +45 -0
- package/dist/search-tools.d.ts +3 -0
- package/dist/search-tools.js +165 -0
- package/dist/secret-env.d.ts +14 -0
- package/dist/secret-env.js +90 -0
- package/dist/shell-doctor.d.ts +29 -0
- package/dist/shell-doctor.js +209 -0
- package/dist/site.d.ts +78 -0
- package/dist/site.js +142 -0
- package/dist/style.d.ts +11 -0
- package/dist/style.js +75 -0
- package/dist/subagent.d.ts +79 -0
- package/dist/subagent.js +189 -0
- package/dist/telemetry.d.ts +56 -0
- package/dist/telemetry.js +146 -0
- package/dist/tools.d.ts +90 -0
- package/dist/tools.js +679 -0
- package/dist/update.d.ts +40 -0
- package/dist/update.js +299 -0
- package/dist/usage.d.ts +21 -0
- package/dist/usage.js +238 -0
- package/dist/web.d.ts +13 -0
- package/dist/web.js +123 -0
- package/dist/webui-brand.d.ts +2 -0
- package/dist/webui-brand.js +169 -0
- package/package.json +60 -4
- package/scripts/p0-claude-code/README.md +39 -0
- package/scripts/p0-claude-code/launch-claude.ts +94 -0
- package/scripts/p0-claude-code/replay.ts +74 -0
- package/scripts/p0-claude-code/seed-local.mjs +145 -0
- package/scripts/p0-claude-code/tap.mjs +105 -0
- package/scripts/patch-pi.js +556 -0
- package/scripts/pi-patch-report.mjs +224 -0
- package/scripts/pi-upgrade.mjs +829 -0
- package/scripts/render-regression.mjs +244 -0
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { activeSite, agentDir, CUSTOM_ENDPOINTS, ENDPOINT_ID_RE, effectiveContextWindow, endpointKeyEnvName, MODELS, readCompactionBudgetSetting, PROVIDER_ID, UNKNOWN_MODEL_COST, VERSION, } from "./config.js";
|
|
5
|
+
import { tr } from "./i18n.js";
|
|
6
|
+
import { migrateLegacyMcpConfig } from "./mcp/config.js";
|
|
7
|
+
import { LEGACY_UI_TEXT_EXTENSION_FILE } from "./pi-ui-text.js";
|
|
8
|
+
/**
|
|
9
|
+
* 系统提示里的品牌人设。中国站面向编程新手、兜底语言中文(保持原文一字不差);
|
|
10
|
+
* 国际站面向开发者、兜底语言英文。按站点而不是界面语言判定,用时求值(配置可能晚于 import 才读)。
|
|
11
|
+
*/
|
|
12
|
+
export function brandAppend(site = activeSite()) {
|
|
13
|
+
if (site.id === "intl") {
|
|
14
|
+
return `## u1s1
|
|
15
|
+
|
|
16
|
+
You are u1s1 — a plain-spoken AI coding assistant:
|
|
17
|
+
- When introducing yourself or asked "who are you", say you are u1s1, never "pi"; only mention the underlying pi engine if the user explicitly asks about internals.
|
|
18
|
+
- Reply in the language of the user's latest request by default. If that is ambiguous, follow the conversation language, then the UI locale when available, and otherwise use English. Never infer language from IP or location. Keep code, commands, and verbatim error messages in English.
|
|
19
|
+
- Explain problems in plain, direct language and skip needless jargon; use a one-sentence analogy when helpful.
|
|
20
|
+
- Before making changes, briefly state what you plan to do; afterwards summarize what changed in one or two sentences.
|
|
21
|
+
- If the user's request is vague, assume the most likely intent and confirm it instead of asking a long list of questions.
|
|
22
|
+
`;
|
|
23
|
+
}
|
|
24
|
+
return `## u1s1
|
|
25
|
+
|
|
26
|
+
You are u1s1 — a plain-spoken AI coding buddy for programming beginners. Most users are unfamiliar with jargon:
|
|
27
|
+
- When introducing yourself or asked "who are you", say you are u1s1, never "pi"; only mention the underlying pi engine if the user explicitly asks about internals.
|
|
28
|
+
- Reply in the language of the user's latest request by default. If that is ambiguous, follow the conversation language, then the UI locale when available, and otherwise use Chinese. Never infer language from IP or location. Keep code, commands, and verbatim error messages in English.
|
|
29
|
+
- Explain problems in plain language without jargon; use a one-sentence analogy when helpful.
|
|
30
|
+
- Before making changes, briefly state what you plan to do; afterwards summarize what changed in one or two sentences.
|
|
31
|
+
- If the user's request is vague, assume the most likely intent and confirm it instead of asking a long list of questions.
|
|
32
|
+
`;
|
|
33
|
+
}
|
|
34
|
+
const EXEC_RULES = `
|
|
35
|
+
## Command execution
|
|
36
|
+
|
|
37
|
+
- Run any command yourself with the bash tool and show its output; never ask the user to run it manually.
|
|
38
|
+
- Only hand a command to the user when it needs admin rights, interactive input (logins, installing system components), or a new terminal window — and always give the full command ready to paste.
|
|
39
|
+
`;
|
|
40
|
+
/**
|
|
41
|
+
* pi 的系统提示不含任何 OS/平台信息,模型只能拿 uname 之类盲探,Windows 上
|
|
42
|
+
* 第一步就撞墙。这里按 shell 体检结果告诉模型:系统是什么、命令实际跑在哪、
|
|
43
|
+
* 坏了该怎么引导用户修(而不是反复重试)。
|
|
44
|
+
*/
|
|
45
|
+
function envSection(shell) {
|
|
46
|
+
if (shell.status === "posix") {
|
|
47
|
+
return `\n## Environment\n\n- Operating system: ${process.platform === "darwin" ? "macOS" : "Linux"}.\n`;
|
|
48
|
+
}
|
|
49
|
+
const lines = ["", "## Environment", "", "- Operating system: Windows. Trust this section over probing; do not detect the OS with uname."];
|
|
50
|
+
switch (shell.status) {
|
|
51
|
+
case "bash":
|
|
52
|
+
lines.push(`- Commands run through bash (${shell.shellPath}); POSIX tools like ls/grep/curl are available.`, 'To inspect native Windows state or launch Windows programs, use cmd.exe /c "..." or powershell.exe -NoProfile -Command "...".', "- Work on Windows itself by default; only consider WSL when the user asks or the task truly requires Linux.");
|
|
53
|
+
break;
|
|
54
|
+
case "wsl":
|
|
55
|
+
lines.push("- Note: commands run through WSL's bash, so they execute inside a Linux distro, not on Windows itself.", "- Access Windows files via /mnt/c/... paths; invoke Windows programs by their .exe names (cmd.exe, powershell.exe).", '- When appropriate, suggest the user install Git for Windows (run winget install --id Git.Git in PowerShell) and restart u1s1 so commands run natively on Windows again.');
|
|
56
|
+
break;
|
|
57
|
+
case "none":
|
|
58
|
+
lines.push("- Critical: no usable bash exists — every bash tool call will fail immediately, and retrying is pointless.", '- Top priority is guiding the user to fix it: have them run winget install --id Git.Git in PowerShell to install Git for Windows, then restart u1s1.', "- Until then, only help the user with other tools such as file read/write.");
|
|
59
|
+
break;
|
|
60
|
+
case "unknown":
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
return lines.join("\n") + "\n";
|
|
64
|
+
}
|
|
65
|
+
/** pi auto-appends <agentDir>/APPEND_SYSTEM.md to its system prompt — our branding hook. */
|
|
66
|
+
export function ensureBrandPrompt(shell) {
|
|
67
|
+
mkdirSync(agentDir, { recursive: true });
|
|
68
|
+
const content = brandAppend() + envSection(shell) + EXEC_RULES;
|
|
69
|
+
const p = join(agentDir, "APPEND_SYSTEM.md");
|
|
70
|
+
if (!existsSync(p) || readFileSync(p, "utf8") !== content) {
|
|
71
|
+
writeFileSync(p, content);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 当前捆绑的 pi 引擎版本号。pi 的包 exports 只有 "import" 条目,
|
|
76
|
+
* require.resolve 会报 ERR_PACKAGE_PATH_NOT_EXPORTED,必须走
|
|
77
|
+
* import.meta.resolve;拿到入口文件(dist/index.js)后向上一级找包根。
|
|
78
|
+
*/
|
|
79
|
+
function bundledPiVersion() {
|
|
80
|
+
try {
|
|
81
|
+
const entry = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
82
|
+
const pkg = JSON.parse(readFileSync(join(dirname(entry), "..", "package.json"), "utf8"));
|
|
83
|
+
return typeof pkg.version === "string" ? pkg.version : undefined;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const THINKING_DEFAULTS_MARKER = "u1s1ModelThinkingDefaults";
|
|
90
|
+
const CODEMODE_DEFAULT_MARKER = "u1s1CodemodeDefault";
|
|
91
|
+
const LLAMA_DISABLED_MARKER = "u1s1LlamaDisabledDefault";
|
|
92
|
+
const NATIVE_UI_MARKER = "u1s1NativeUiRestored";
|
|
93
|
+
function recordValue(value) {
|
|
94
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
95
|
+
? value
|
|
96
|
+
: undefined;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Seed per-model defaults without overwriting a user's Ctrl+S choice. The
|
|
100
|
+
* marker records values managed by u1s1, so a later Gateway default can update
|
|
101
|
+
* them while a diverged user value becomes user-owned.
|
|
102
|
+
*/
|
|
103
|
+
export function applyModelThinkingDefaults(settings, models) {
|
|
104
|
+
const defaults = models.filter((model) => model.thinking !== undefined);
|
|
105
|
+
if (defaults.length === 0)
|
|
106
|
+
return false;
|
|
107
|
+
const levels = { ...recordValue(settings["modelThinkingLevels"]) };
|
|
108
|
+
const managed = { ...recordValue(settings[THINKING_DEFAULTS_MARKER]) };
|
|
109
|
+
let changed = false;
|
|
110
|
+
for (const model of defaults) {
|
|
111
|
+
const key = `${PROVIDER_ID}/${model.id}`;
|
|
112
|
+
const next = model.thinking.defaultLevel;
|
|
113
|
+
const current = levels[key];
|
|
114
|
+
const previousManaged = managed[key];
|
|
115
|
+
if (current === undefined || current === previousManaged) {
|
|
116
|
+
if (current !== next) {
|
|
117
|
+
levels[key] = next;
|
|
118
|
+
changed = true;
|
|
119
|
+
}
|
|
120
|
+
if (previousManaged !== next) {
|
|
121
|
+
managed[key] = next;
|
|
122
|
+
changed = true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else if (previousManaged !== undefined) {
|
|
126
|
+
delete managed[key];
|
|
127
|
+
changed = true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (changed) {
|
|
131
|
+
settings["modelThinkingLevels"] = levels;
|
|
132
|
+
settings[THINKING_DEFAULTS_MARKER] = managed;
|
|
133
|
+
}
|
|
134
|
+
return changed;
|
|
135
|
+
}
|
|
136
|
+
/** pi 1.0 的 DEFAULT_TOOL_NAMES(未导出,测试里对过 pi 源码)。 */
|
|
137
|
+
export const PI_DEFAULT_TOOL_NAMES = ["read", "bash", "edit", "write"];
|
|
138
|
+
function isToolModifier(entry) {
|
|
139
|
+
return entry.startsWith("+") || entry.startsWith("-");
|
|
140
|
+
}
|
|
141
|
+
/** 与 pi 1.0 resolveDefaultTools 同算法:纯名字替换默认集,再按顺序应用 +name / -name。 */
|
|
142
|
+
function resolveDefaultTools(entries) {
|
|
143
|
+
const plain = entries.filter((entry) => !isToolModifier(entry));
|
|
144
|
+
const tools = plain.length > 0 || entries.length === 0 ? plain : [...PI_DEFAULT_TOOL_NAMES];
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
if (!isToolModifier(entry))
|
|
147
|
+
continue;
|
|
148
|
+
const name = entry.slice(1);
|
|
149
|
+
const index = tools.indexOf(name);
|
|
150
|
+
if (entry.startsWith("+") && index === -1 && name)
|
|
151
|
+
tools.push(name);
|
|
152
|
+
else if (entry.startsWith("-") && index !== -1)
|
|
153
|
+
tools.splice(index, 1);
|
|
154
|
+
}
|
|
155
|
+
return tools;
|
|
156
|
+
}
|
|
157
|
+
function stringList(value) {
|
|
158
|
+
return Array.isArray(value) ? value.filter((t) => typeof t === "string") : undefined;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* pi 内置 codemode 默认不开,u1s1 给每个用户开一次。标记写下后不再碰:用户删掉
|
|
162
|
+
* codemode 都算用户的选择。写成完整工具名列表而不是 `+codemode`,原因见
|
|
163
|
+
* normalizeDefaultTools。
|
|
164
|
+
*/
|
|
165
|
+
export function applyCodemodeDefault(settings) {
|
|
166
|
+
if (settings[CODEMODE_DEFAULT_MARKER] === true)
|
|
167
|
+
return false;
|
|
168
|
+
const tools = stringList(settings["defaultTools"]);
|
|
169
|
+
if (!tools?.some((t) => t.replace(/^[+-]/, "") === "codemode")) {
|
|
170
|
+
// 没写过 defaultTools 是 pi 默认集;空列表是用户明确不开工具,两者不同
|
|
171
|
+
const current = tools === undefined ? [...PI_DEFAULT_TOOL_NAMES] : resolveDefaultTools(tools);
|
|
172
|
+
settings["defaultTools"] = [...current, "codemode"];
|
|
173
|
+
}
|
|
174
|
+
settings[CODEMODE_DEFAULT_MARKER] = true;
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* 每次启动把 defaultTools 里的 `+name` / `-name` 按 pi 1.0 的规则展开成完整工具名列表。
|
|
179
|
+
* Desktop App ≤0.1.23 内置的 pi 0.87.1 和 CLI 共用这份 settings.json,它不认增减写法,
|
|
180
|
+
* 会把 `["+codemode"]` 当成「只开一个叫 +codemode 的工具」,结果旧对话一个工具都没有。
|
|
181
|
+
* 展开后 pi 1.0 解析结果不变(项目级的 +/- 照样叠在上面),旧版也能读懂;
|
|
182
|
+
* 1.13.0 写下的 `["+codemode"]` 由此修复。
|
|
183
|
+
*/
|
|
184
|
+
export function normalizeDefaultTools(settings) {
|
|
185
|
+
const tools = stringList(settings["defaultTools"]);
|
|
186
|
+
if (!tools?.some(isToolModifier))
|
|
187
|
+
return false;
|
|
188
|
+
settings["defaultTools"] = resolveDefaultTools(tools);
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* pi 1.0 内置 llama.cpp 扩展(本地模型 provider + /llama),u1s1 走网关用不上,默认关一次。
|
|
193
|
+
* 用户在 extensions 里自己写过 builtin:llama.cpp 的任何开关就不动;标记写下后同样不再碰。
|
|
194
|
+
*/
|
|
195
|
+
export function applyLlamaDisabledDefault(settings) {
|
|
196
|
+
if (settings[LLAMA_DISABLED_MARKER] === true)
|
|
197
|
+
return false;
|
|
198
|
+
const entries = Array.isArray(settings["extensions"])
|
|
199
|
+
? settings["extensions"].filter((e) => typeof e === "string")
|
|
200
|
+
: [];
|
|
201
|
+
if (!entries.some((e) => e.replace(/^[+!-]/, "") === "builtin:llama.cpp")) {
|
|
202
|
+
settings["extensions"] = [...entries, "-builtin:llama.cpp"];
|
|
203
|
+
}
|
|
204
|
+
settings[LLAMA_DISABLED_MARKER] = true;
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* 恢复 Pi 默认界面:早先版本替用户写下 hideThinkingBlock=true(隐藏思考块)和
|
|
209
|
+
* clearOnShrink=true(精简 UI 配套),并用扩展把内置工具行渲染成 0 行。现在不再覆盖,
|
|
210
|
+
* 老用户 settings.json 里残留的这两个值要清一次,否则升级后仍看不到思考块。
|
|
211
|
+
* 无法区分「u1s1 写的」和「用户后来自己选的」,所以全员只重置这一次;标记写下后
|
|
212
|
+
* 用户再在 /settings 里选的值就不会被动了。
|
|
213
|
+
*/
|
|
214
|
+
export function applyNativeUiRestore(settings) {
|
|
215
|
+
if (settings[NATIVE_UI_MARKER] === true)
|
|
216
|
+
return false;
|
|
217
|
+
delete settings["hideThinkingBlock"];
|
|
218
|
+
delete settings["clearOnShrink"];
|
|
219
|
+
const terminal = recordValue(settings["terminal"]);
|
|
220
|
+
if (terminal && "clearOnShrink" in terminal) {
|
|
221
|
+
delete terminal["clearOnShrink"];
|
|
222
|
+
if (Object.keys(terminal).length === 0)
|
|
223
|
+
delete settings["terminal"];
|
|
224
|
+
}
|
|
225
|
+
settings[NATIVE_UI_MARKER] = true;
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
/** Defaults that don't overwrite values the user already set. */
|
|
229
|
+
export function ensureDefaultSettings(models = []) {
|
|
230
|
+
mkdirSync(agentDir, { recursive: true });
|
|
231
|
+
const p = join(agentDir, "settings.json");
|
|
232
|
+
let settings = {};
|
|
233
|
+
if (existsSync(p)) {
|
|
234
|
+
try {
|
|
235
|
+
settings = JSON.parse(readFileSync(p, "utf8"));
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
let changed = false;
|
|
242
|
+
// Pi default is one-at-a-time (one queued message per turn). u1s1 delivers
|
|
243
|
+
// all queued messages together so burst typing isn't split across turns.
|
|
244
|
+
// Only fill in missing keys so an explicit /settings choice still sticks.
|
|
245
|
+
for (const key of ["steeringMode", "followUpMode"]) {
|
|
246
|
+
if (!(key in settings)) {
|
|
247
|
+
settings[key] = "all";
|
|
248
|
+
changed = true;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// 启动时是否显示品牌横幅(logo + 版本号/目录/提示)
|
|
252
|
+
if (!("showStartupBanner" in settings)) {
|
|
253
|
+
settings["showStartupBanner"] = true;
|
|
254
|
+
changed = true;
|
|
255
|
+
}
|
|
256
|
+
// 静默启动:不打印 [Context]/[Skills]/[Extensions] 等资源加载信息
|
|
257
|
+
if (!("quietStartup" in settings)) {
|
|
258
|
+
settings["quietStartup"] = true;
|
|
259
|
+
changed = true;
|
|
260
|
+
}
|
|
261
|
+
// 启动时自动检查并更新新版
|
|
262
|
+
if (!("autoUpdate" in settings)) {
|
|
263
|
+
settings["autoUpdate"] = true;
|
|
264
|
+
changed = true;
|
|
265
|
+
}
|
|
266
|
+
// pi 1.0 起默认全屏;u1s1 的精简 UI 与滚动习惯按常规模式调的,未选过的用户保持常规
|
|
267
|
+
if (!("tuiMode" in settings)) {
|
|
268
|
+
settings["tuiMode"] = "regular";
|
|
269
|
+
changed = true;
|
|
270
|
+
}
|
|
271
|
+
if (applyNativeUiRestore(settings))
|
|
272
|
+
changed = true;
|
|
273
|
+
if (applyCodemodeDefault(settings))
|
|
274
|
+
changed = true;
|
|
275
|
+
if (normalizeDefaultTools(settings))
|
|
276
|
+
changed = true;
|
|
277
|
+
if (applyLlamaDisabledDefault(settings))
|
|
278
|
+
changed = true;
|
|
279
|
+
if (applyModelThinkingDefaults(settings, models))
|
|
280
|
+
changed = true;
|
|
281
|
+
// ≤0.4.0 shipped branded themes and forced them as default; the files are
|
|
282
|
+
// gone now, so a settings.json still pointing at them must fall back to
|
|
283
|
+
// pi's default theme.
|
|
284
|
+
if (typeof settings.theme === "string" && settings.theme.includes("u1s1-")) {
|
|
285
|
+
delete settings.theme;
|
|
286
|
+
changed = true;
|
|
287
|
+
}
|
|
288
|
+
// 屏蔽 pi 引擎的启动更新日志(What's New):那是上游英文 changelog,且 pi 的
|
|
289
|
+
// 版本号体系(0.84.x)和 u1s1(0.19.x)完全对不上——settings 里「上次看到的
|
|
290
|
+
// 版本」一旦缺失或偏旧,升级后启动会一次性灌出几十条英文更新说明。
|
|
291
|
+
// u1s1 的版本动态走公告横幅和 u1s1 update,这里每次启动都把该记录钉死在
|
|
292
|
+
// 当前捆绑的引擎版本上,保证启动时永不弹 changelog。
|
|
293
|
+
const piVersion = bundledPiVersion();
|
|
294
|
+
if (piVersion && settings["lastChangelogVersion"] !== piVersion) {
|
|
295
|
+
settings["lastChangelogVersion"] = piVersion;
|
|
296
|
+
changed = true;
|
|
297
|
+
}
|
|
298
|
+
if (changed)
|
|
299
|
+
writeFileSync(p, JSON.stringify(settings, null, 2) + "\n");
|
|
300
|
+
}
|
|
301
|
+
/** ≤0.4.0 wrote u1s1-dark/u1s1-light into the pi themes dir; remove them. */
|
|
302
|
+
export function cleanupBrandThemes() {
|
|
303
|
+
for (const name of ["u1s1-dark", "u1s1-light"]) {
|
|
304
|
+
rmSync(join(agentDir, "themes", `${name}.json`), { force: true });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* u1s1 provider for SDK-based frontends (Desktop App): pi composes
|
|
309
|
+
* <agentDir>/models.json above registered providers, so this exposes the same
|
|
310
|
+
* models as the TUI's in-process extension without patching the frontend.
|
|
311
|
+
* apiKey stays an env reference — the launcher sets U1S1_API_KEY, the file
|
|
312
|
+
* itself holds no secret. Rewritten on every launch so baseUrl/model changes
|
|
313
|
+
* propagate; other providers a user may have added are preserved.
|
|
314
|
+
*/
|
|
315
|
+
/**
|
|
316
|
+
* 联网工具经 <agentDir>/extensions 投影:pi 的资源加载器对 TUI 和 Desktop App
|
|
317
|
+
* 都会扫描这个目录,两个前端从此共用同一份工具注册,不用各接一遍。
|
|
318
|
+
* 文件每次启动重写(baseUrl/服务端开关变化随之生效);apiKey 走 U1S1_API_KEY
|
|
319
|
+
* 环境变量,文件里不落密钥。U1S1_TOOLS_VIA_EXTENSION 守卫:旧版 CLI 仍在
|
|
320
|
+
* 进程内注册工具且不设该变量,残留的本文件在旧版下自动空转,避免双重注册。
|
|
321
|
+
*/
|
|
322
|
+
export function writeWebToolsExtension(cfg, features) {
|
|
323
|
+
const dir = join(agentDir, "extensions");
|
|
324
|
+
mkdirSync(dir, { recursive: true });
|
|
325
|
+
const toolsUrl = new URL("./tools.js", import.meta.url).href;
|
|
326
|
+
const searchLine = features.webSearch
|
|
327
|
+
? ` pi.registerTool(tools.createSearchTool({ baseUrl, apiKey: process.env.U1S1_API_KEY }));\n`
|
|
328
|
+
: "";
|
|
329
|
+
const imageLine = features.imageGen
|
|
330
|
+
? ` pi.registerTool(tools.createImageTool({ baseUrl, apiKey: process.env.U1S1_API_KEY }));\n`
|
|
331
|
+
: "";
|
|
332
|
+
// spawn_subagent 只给主会话注册;子 agent 环境里跳过,杜绝孙 agent 递归 spawn
|
|
333
|
+
const subagentBlock = ` if (process.env.U1S1_IN_SUBAGENT !== "1") {\n` +
|
|
334
|
+
` let getParentModel = () => undefined;\n` +
|
|
335
|
+
` pi.on("session_start", (_event, ctx) => {\n` +
|
|
336
|
+
` const m = ctx.model;\n` +
|
|
337
|
+
` if (m && typeof m.provider === "string" && typeof m.id === "string") {\n` +
|
|
338
|
+
` getParentModel = () => ({ provider: m.provider, id: m.id });\n` +
|
|
339
|
+
` }\n` +
|
|
340
|
+
` });\n` +
|
|
341
|
+
` pi.registerTool(tools.createSubagentTool(getParentModel));\n` +
|
|
342
|
+
` }\n`;
|
|
343
|
+
// 密钥剥离守卫也从这里装:Desktop App 的 pi 跑在 pi-web-ui 子进程里,没有
|
|
344
|
+
// 启动器代码,只有扩展能替它把 U1S1_API_KEY / U1S1_EP_KEY_* 挡在子进程之外
|
|
345
|
+
const secretEnvUrl = new URL("./secret-env.js", import.meta.url).href;
|
|
346
|
+
// 提示文案在生成时按当前界面语言定好,扩展文件里只是字面量
|
|
347
|
+
const toolsLoadFailed = tr("联网工具本次未能加载(程序文件可能正在更新),重启 u1s1 即可恢复", "Web tools couldn't load this time (program files may be updating). Restart u1s1 to restore them.");
|
|
348
|
+
writeFileSync(join(dir, "u1s1-tools.js"), `// 由 u1s1 每次启动自动生成,请勿手改\n` +
|
|
349
|
+
`export default async function (pi) {\n` +
|
|
350
|
+
` (await import(${JSON.stringify(secretEnvUrl)})).installChildEnvGuard();\n` +
|
|
351
|
+
` if (process.env.U1S1_TOOLS_VIA_EXTENSION !== "1") return;\n` +
|
|
352
|
+
` const baseUrl = process.env.U1S1_SIGNING_PROXY_URL || ${JSON.stringify(cfg.baseUrl)};\n` +
|
|
353
|
+
// 升级半途 / 权限问题导致 CLI dist 不可读时,宁可这一会话没有联网工具,
|
|
354
|
+
// 也不能让扩展加载失败刷英文堆栈(审计 B4);只在 UI 里提示一句人话
|
|
355
|
+
` let tools;\n` +
|
|
356
|
+
` try {\n` +
|
|
357
|
+
` tools = await import(${JSON.stringify(toolsUrl)});\n` +
|
|
358
|
+
` } catch {\n` +
|
|
359
|
+
` pi.on("session_start", (_event, ctx) => {\n` +
|
|
360
|
+
` try { if (ctx.hasUI) ctx.ui.notify(${JSON.stringify(toolsLoadFailed)}, "warning"); } catch {}\n` +
|
|
361
|
+
` });\n` +
|
|
362
|
+
` return;\n` +
|
|
363
|
+
` }\n` +
|
|
364
|
+
searchLine +
|
|
365
|
+
` pi.registerTool(tools.createFetchTool({ baseUrl, apiKey: process.env.U1S1_API_KEY, renderFallback: ${features.webFetchRender} }));\n` +
|
|
366
|
+
imageLine +
|
|
367
|
+
subagentBlock +
|
|
368
|
+
`}\n`);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* 生成「错误信息人话化」扩展到 <agentDir>/extensions/u1s1-error-humanize.js:
|
|
372
|
+
* 把「429: {json}」形态的模型调用错误翻成中文正文 + 状态码尾巴,TUI 和
|
|
373
|
+
* Desktop App 共用(同 compact-ui 的投影机制)。实现见 error-humanize.ts,
|
|
374
|
+
* 分类兼容性(重试/配额/上下文超长)的约束也写在那里。
|
|
375
|
+
* import 失败时静默跳过——Desktop 独立运行时若指向的 CLI dist 已不存在,
|
|
376
|
+
* 宁可回到原始错误文本,也不能让扩展报错刷英文堆栈。
|
|
377
|
+
*/
|
|
378
|
+
export function writeErrorHumanizeExtension() {
|
|
379
|
+
const dir = join(agentDir, "extensions");
|
|
380
|
+
mkdirSync(dir, { recursive: true });
|
|
381
|
+
const url = new URL("./error-humanize.js", import.meta.url).href;
|
|
382
|
+
const traceUrl = new URL("./request-trace.js", import.meta.url).href;
|
|
383
|
+
writeFileSync(join(dir, "u1s1-error-humanize.js"), `// 由 u1s1 每次启动自动生成,请勿手改\n` +
|
|
384
|
+
`export default async function (pi) {\n` +
|
|
385
|
+
` let humanize, extractRequestId, rememberRequestId;\n` +
|
|
386
|
+
` try {\n` +
|
|
387
|
+
` ({ humanizeModelError: humanize, extractRequestId } = await import(${JSON.stringify(url)}));\n` +
|
|
388
|
+
` ({ rememberRequestId } = await import(${JSON.stringify(traceUrl)}));\n` +
|
|
389
|
+
` } catch {\n` +
|
|
390
|
+
` return;\n` +
|
|
391
|
+
` }\n` +
|
|
392
|
+
` pi.on("message_end", (event) => {\n` +
|
|
393
|
+
` const msg = event.message;\n` +
|
|
394
|
+
` if (msg.role !== "assistant" || msg.stopReason !== "error" || !msg.errorMessage) return;\n` +
|
|
395
|
+
` // 记下最近一次报错的请求编号,u1s1 feedback / /feedback 建工单时附上\n` +
|
|
396
|
+
` rememberRequestId(extractRequestId(msg.errorMessage));\n` +
|
|
397
|
+
` const friendly = humanize(msg.errorMessage);\n` +
|
|
398
|
+
` if (!friendly) return;\n` +
|
|
399
|
+
` return { message: { ...msg, errorMessage: friendly } };\n` +
|
|
400
|
+
` });\n` +
|
|
401
|
+
`}\n`);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* 生成「会话内公告触达」扩展到 <agentDir>/extensions/u1s1-announcements.js:
|
|
405
|
+
* 启动横幅只渲染一次公告,维护窗口里挂着会话的用户只会撞裸错误(审计 F14)。
|
|
406
|
+
* 轮询逻辑在 announcements-poll.ts(5 分钟一次、首拉只建基线不打扰);
|
|
407
|
+
* 公告变化时 appendEntry 进会话 + 尽力弹 notify。TUI 和 Desktop App 共用。
|
|
408
|
+
* print/json 一次性模式不起轮询;import 失败静默跳过(同 error-humanize)。
|
|
409
|
+
*/
|
|
410
|
+
export function writeAnnouncementsExtension() {
|
|
411
|
+
const dir = join(agentDir, "extensions");
|
|
412
|
+
mkdirSync(dir, { recursive: true });
|
|
413
|
+
const url = new URL("./announcements-poll.js", import.meta.url).href;
|
|
414
|
+
writeFileSync(join(dir, "u1s1-announcements.js"), `// 由 u1s1 每次启动自动生成,请勿手改\n` +
|
|
415
|
+
`export default async function (pi) {\n` +
|
|
416
|
+
` let startAnnouncementPoll;\n` +
|
|
417
|
+
` try {\n` +
|
|
418
|
+
` startAnnouncementPoll = (await import(${JSON.stringify(url)})).startAnnouncementPoll;\n` +
|
|
419
|
+
` } catch {\n` +
|
|
420
|
+
` return;\n` +
|
|
421
|
+
` }\n` +
|
|
422
|
+
` let Text;\n` +
|
|
423
|
+
` try {\n` +
|
|
424
|
+
` Text = (await import("@earendil-works/pi-tui")).Text;\n` +
|
|
425
|
+
` } catch {\n` +
|
|
426
|
+
` return;\n` +
|
|
427
|
+
` }\n` +
|
|
428
|
+
` pi.registerEntryRenderer("u1s1-announcement", (entry, _opts, theme) => {\n` +
|
|
429
|
+
` const d = entry.data ?? {};\n` +
|
|
430
|
+
` let text = theme.fg("text", theme.bold("📢 " + String(d.text ?? "")));\n` +
|
|
431
|
+
` if (d.url) text += "\\n" + theme.fg("dim", String(d.url));\n` +
|
|
432
|
+
` return new Text(text, 1, 0);\n` +
|
|
433
|
+
` });\n` +
|
|
434
|
+
` let ui;\n` +
|
|
435
|
+
` let started = false;\n` +
|
|
436
|
+
` pi.on("session_start", (_event, ctx) => {\n` +
|
|
437
|
+
` if (ctx.hasUI) ui = ctx.ui;\n` +
|
|
438
|
+
` if (started || ctx.mode === "print" || ctx.mode === "json") return;\n` +
|
|
439
|
+
` started = true;\n` +
|
|
440
|
+
` startAnnouncementPoll({\n` +
|
|
441
|
+
` onNew: (a) => {\n` +
|
|
442
|
+
` pi.appendEntry("u1s1-announcement", { text: a.text, url: a.url });\n` +
|
|
443
|
+
` try {\n` +
|
|
444
|
+
` ui?.notify("📢 " + a.text, "info");\n` +
|
|
445
|
+
` } catch {}\n` +
|
|
446
|
+
` },\n` +
|
|
447
|
+
` });\n` +
|
|
448
|
+
` });\n` +
|
|
449
|
+
`}\n`);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* 生成「自定义 MCP 服务」扩展到 <agentDir>/extensions/u1s1-mcp.js,只给没有内置 MCP 的
|
|
453
|
+
* 旧引擎用(Desktop App 的 pi-web-ui 还是 0.83):读 agent/mcp.json,把工具注册成
|
|
454
|
+
* mcp_<服务>_<工具>,逻辑在 mcp/extension.ts。pi ≥0.99 有 pi.registerMcpServer,说明内置
|
|
455
|
+
* MCP 会读同一份配置,扩展直接让位。投影前先把 ≤1.12 的 ~/.u1s1/mcp.json 迁过去。
|
|
456
|
+
* 子会话(spawn_subagent)里不挂 MCP 工具:避免每个子进程都拉起一份服务。
|
|
457
|
+
*/
|
|
458
|
+
export function writeMcpExtension() {
|
|
459
|
+
try {
|
|
460
|
+
migrateLegacyMcpConfig();
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
// 迁移失败不挡启动:旧文件还在,下次再试
|
|
464
|
+
}
|
|
465
|
+
const dir = join(agentDir, "extensions");
|
|
466
|
+
mkdirSync(dir, { recursive: true });
|
|
467
|
+
const url = new URL("./mcp/extension.js", import.meta.url).href;
|
|
468
|
+
const mcpLoadFailed = tr("MCP 服务加载失败:", "Failed to load MCP servers: ");
|
|
469
|
+
writeFileSync(join(dir, "u1s1-mcp.js"), `// 由 u1s1 每次启动自动生成,请勿手改\n` +
|
|
470
|
+
`export default async function (pi) {\n` +
|
|
471
|
+
` if (process.env.U1S1_IN_SUBAGENT === "1") return;\n` +
|
|
472
|
+
` if (typeof pi.registerMcpServer === "function") return;\n` +
|
|
473
|
+
` let registerMcpServers;\n` +
|
|
474
|
+
` try {\n` +
|
|
475
|
+
` registerMcpServers = (await import(${JSON.stringify(url)})).registerMcpServers;\n` +
|
|
476
|
+
` } catch {\n` +
|
|
477
|
+
` return;\n` +
|
|
478
|
+
` }\n` +
|
|
479
|
+
` try {\n` +
|
|
480
|
+
` await registerMcpServers(pi);\n` +
|
|
481
|
+
` } catch (err) {\n` +
|
|
482
|
+
` const msg = err instanceof Error ? err.message : String(err);\n` +
|
|
483
|
+
` pi.on("session_start", (_event, ctx) => {\n` +
|
|
484
|
+
` if (ctx.hasUI) ctx.ui.notify(${JSON.stringify(mcpLoadFailed)} + msg, "warning");\n` +
|
|
485
|
+
` });\n` +
|
|
486
|
+
` }\n` +
|
|
487
|
+
`}\n`);
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* 生成「OpenRouter 应用归因」扩展到 <agentDir>/extensions/u1s1-attribution.js。
|
|
491
|
+
* 用户自己配 OPENROUTER_API_KEY 直连时,pi 内置的归因头归到 pi.dev 名下(且受遥测
|
|
492
|
+
* 开关控制);这里用 before_provider_headers 钩子覆盖为 u1s1,让这部分流量也计入
|
|
493
|
+
* u1s1 的 OpenRouter 公开排行与数据分析(openrouter.ai/docs/app-attribution)。
|
|
494
|
+
* 只在请求目标确认为 OpenRouter 时才改头,其它供应商不加多余头。
|
|
495
|
+
*/
|
|
496
|
+
export function writeAttributionExtension(site = activeSite()) {
|
|
497
|
+
const dir = join(agentDir, "extensions");
|
|
498
|
+
mkdirSync(dir, { recursive: true });
|
|
499
|
+
writeFileSync(join(dir, "u1s1-attribution.js"), `// 由 u1s1 每次启动自动生成,请勿手改\n` +
|
|
500
|
+
`export default async function (pi) {\n` +
|
|
501
|
+
` pi.on("before_provider_headers", (event, ctx) => {\n` +
|
|
502
|
+
` const m = ctx.model;\n` +
|
|
503
|
+
` if (!m || !(m.provider === "openrouter" || String(m.baseUrl ?? "").includes("openrouter.ai"))) return;\n` +
|
|
504
|
+
` event.headers["HTTP-Referer"] = ${JSON.stringify(site.origin)};\n` +
|
|
505
|
+
` event.headers["X-OpenRouter-Title"] = "u1s1";\n` +
|
|
506
|
+
` event.headers["X-OpenRouter-Categories"] = "cli-agent";\n` +
|
|
507
|
+
` });\n` +
|
|
508
|
+
`}\n`);
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* 「精简 UI」扩展(u1s1-compact-ui.js)已下线:界面回到 Pi 默认——内置工具调用、思考块都按原样显示。
|
|
512
|
+
* 旧版本每次启动都把它写进 <agentDir>/extensions,Pi 会照单加载并继续隐藏工具行,
|
|
513
|
+
* 所以启动时要把残留文件删掉(Desktop App 共用同一个 agentDir,同样受益)。
|
|
514
|
+
*/
|
|
515
|
+
export function removeLegacyCompactUiExtension(outputDir) {
|
|
516
|
+
const dir = outputDir ?? join(agentDir, "extensions");
|
|
517
|
+
try {
|
|
518
|
+
rmSync(join(dir, "u1s1-compact-ui.js"), { force: true });
|
|
519
|
+
}
|
|
520
|
+
catch { }
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* 「界面文案」(斜杠命令补全描述、思考中提示)上一版投影成 <agentDir>/extensions/u1s1-ui-text.js,
|
|
524
|
+
* 用户带 --no-extensions 或在设置里禁用它时就失效;现在改由 pi.ts 的进程内扩展调用
|
|
525
|
+
* pi-ui-text.ts 的 registerUiText。启动时删掉残留文件,免得和进程内扩展各包一层
|
|
526
|
+
* (两层同时生效结果也正确,只是多余);Desktop App 共用 agentDir,它那里本来就是空转。
|
|
527
|
+
*/
|
|
528
|
+
export function removeLegacyUiTextExtension(outputDir) {
|
|
529
|
+
const dir = outputDir ?? join(agentDir, "extensions");
|
|
530
|
+
try {
|
|
531
|
+
rmSync(join(dir, LEGACY_UI_TEXT_EXTENSION_FILE), { force: true });
|
|
532
|
+
}
|
|
533
|
+
catch { }
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* pi-web-ui 的就绪检测只认 <agentDir>/auth.json 里有没有凭据条目(models.json
|
|
537
|
+
* 的 apiKey 它不看),空 {} 会在网页里弹「未检测到 pi agent 配置」引导装 pi。
|
|
538
|
+
* 这里补一条 u1s1 的 api_key 记录 —— key 用 $U1S1_API_KEY 环境变量引用,
|
|
539
|
+
* pi 的 resolveConfigValue 读取时才解析,文件本身不落密钥,和 models.json 同款。
|
|
540
|
+
* 其他 provider 的凭据原样保留;文件损坏时不动它,交给 SDK 自己报错。
|
|
541
|
+
*/
|
|
542
|
+
export function ensureAuthCredential() {
|
|
543
|
+
mkdirSync(agentDir, { recursive: true });
|
|
544
|
+
const p = join(agentDir, "auth.json");
|
|
545
|
+
let root = {};
|
|
546
|
+
if (existsSync(p)) {
|
|
547
|
+
try {
|
|
548
|
+
root = JSON.parse(readFileSync(p, "utf8"));
|
|
549
|
+
}
|
|
550
|
+
catch {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (typeof root !== "object" || root === null || Array.isArray(root))
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const entry = root[PROVIDER_ID];
|
|
557
|
+
if (entry?.["type"] === "api_key" && entry["key"] === "$U1S1_API_KEY")
|
|
558
|
+
return;
|
|
559
|
+
root[PROVIDER_ID] = { type: "api_key", key: "$U1S1_API_KEY" };
|
|
560
|
+
writeFileSync(p, JSON.stringify(root, null, 2) + "\n", { mode: 0o600 });
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* pi provider 条目里的模型形状(models.json 与 registerProvider 共用)。
|
|
564
|
+
* contextWindow 不是模型真实窗口而是自动压缩预算(见 effectiveContextWindow):
|
|
565
|
+
* options.maxContextTokens 显式传 undefined 表示「用户没设」(测试用),不传则读 settings.json。
|
|
566
|
+
*/
|
|
567
|
+
export function toProviderModels(models, options = { maxContextTokens: readCompactionBudgetSetting() }) {
|
|
568
|
+
return models.map((m) => {
|
|
569
|
+
let thinkingLevelMap;
|
|
570
|
+
let compat;
|
|
571
|
+
if (m.thinking) {
|
|
572
|
+
const supported = new Set(m.thinking.levels);
|
|
573
|
+
thinkingLevelMap = {};
|
|
574
|
+
for (const level of ["off", "minimal", "low", "medium", "high", "xhigh", "max"]) {
|
|
575
|
+
thinkingLevelMap[level] = supported.has(level) ? (m.thinking.levelMap[level] ?? level) : null;
|
|
576
|
+
}
|
|
577
|
+
compat = {
|
|
578
|
+
supportsReasoningEffort: true,
|
|
579
|
+
thinkingFormat: m.thinking.requestFormat,
|
|
580
|
+
...(m.thinking.requestFormat === "deepseek"
|
|
581
|
+
? {
|
|
582
|
+
supportsStore: false,
|
|
583
|
+
supportsDeveloperRole: false,
|
|
584
|
+
maxTokensField: "max_tokens",
|
|
585
|
+
requiresReasoningContentOnAssistantMessages: true,
|
|
586
|
+
}
|
|
587
|
+
: {}),
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
id: m.id,
|
|
592
|
+
// 选择器只渲染 name 一行,把「免费/几倍」提示拼进去,切模型前就能看到
|
|
593
|
+
name: m.note ? `${m.name} · ${m.note}` : m.name,
|
|
594
|
+
reasoning: m.reasoning,
|
|
595
|
+
thinkingLevelMap,
|
|
596
|
+
input: m.vision ? ["text", "image"] : ["text"],
|
|
597
|
+
cost: m.cost ?? UNKNOWN_MODEL_COST,
|
|
598
|
+
contextWindow: effectiveContextWindow(m, options.maxContextTokens),
|
|
599
|
+
maxTokens: m.maxTokens,
|
|
600
|
+
compat,
|
|
601
|
+
};
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* 自定义端点 → pi provider 条目。密钥走环境变量引用(启动器已 set,见
|
|
606
|
+
* endpointKeyEnv),文件里不落明文;没配密钥的端点(本机 Ollama 等)写
|
|
607
|
+
* 字面量 "local" —— pi 没有凭据会直接拒启("No API key found"),
|
|
608
|
+
* 这是 pi 自带 llama provider 的同款兜底,本地服务会忽略 Authorization 头。
|
|
609
|
+
*/
|
|
610
|
+
export function endpointProviderEntry(ep, options = {}) {
|
|
611
|
+
// 进程内 registerProvider 可以直接给字面量(不落文件);写 models.json 时仍走 $VAR 引用
|
|
612
|
+
const envRef = `$${endpointKeyEnvName(ep.id)}`;
|
|
613
|
+
return {
|
|
614
|
+
name: ep.name,
|
|
615
|
+
baseUrl: ep.baseUrl,
|
|
616
|
+
api: ep.api,
|
|
617
|
+
apiKey: ep.apiKey ? (options.literalKey ? ep.apiKey : envRef) : "local",
|
|
618
|
+
models: toProviderModels(ep.models),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
/** run_workflow 已下线:清掉旧版本每次启动写入的 /workflow 提示词模板,免得它引导模型调用不存在的工具。 */
|
|
622
|
+
export function removeLegacyWorkflowPromptTemplate() {
|
|
623
|
+
try {
|
|
624
|
+
rmSync(join(agentDir, "prompts", "workflow.md"), { force: true });
|
|
625
|
+
}
|
|
626
|
+
catch { }
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* pi 会把环境里的厂商密钥(ANTHROPIC_API_KEY 等)当作「已配置的 provider」,
|
|
630
|
+
* /model 里冒出一大串没在用的模型(u1s1 又拦了 /login,配了也没法管理)。
|
|
631
|
+
* u1s1 的口径是:官方模型 + 自定义端点,所以启动 pi 前把这批**纯 LLM 厂商**
|
|
632
|
+
* 密钥从 pi 进程环境里剔掉。名单来自 pi-ai env-api-keys.ts;刻意保留
|
|
633
|
+
* AWS_WEB_IDENTITY_TOKEN_FILE / HF_TOKEN / GOOGLE_CLOUD_API_KEY /
|
|
634
|
+
* CLOUDFLARE_API_KEY 这类通用凭据 —— agent 终端里的其他工具还要用。
|
|
635
|
+
* 注意 agent 的 Bash 子进程也继承不到被剔的变量;项目 .env 文件不受影响。
|
|
636
|
+
*/
|
|
637
|
+
const FOREIGN_PROVIDER_ENV_KEYS = [
|
|
638
|
+
"AI_GATEWAY_API_KEY",
|
|
639
|
+
"ANTHROPIC_API_KEY",
|
|
640
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
641
|
+
"ANTHROPIC_OAUTH_TOKEN",
|
|
642
|
+
"ANT_LING_API_KEY",
|
|
643
|
+
"AWS_BEARER_TOKEN_BEDROCK",
|
|
644
|
+
"AZURE_OPENAI_API_KEY",
|
|
645
|
+
"BASETEN_API_KEY",
|
|
646
|
+
"CEREBRAS_API_KEY",
|
|
647
|
+
"COPILOT_GITHUB_TOKEN",
|
|
648
|
+
"DEEPSEEK_API_KEY",
|
|
649
|
+
"FIREWORKS_API_KEY",
|
|
650
|
+
"GEMINI_API_KEY",
|
|
651
|
+
"GROQ_API_KEY",
|
|
652
|
+
"KIMI_API_KEY",
|
|
653
|
+
"MINIMAX_API_KEY",
|
|
654
|
+
"MINIMAX_CN_API_KEY",
|
|
655
|
+
"MISTRAL_API_KEY",
|
|
656
|
+
"MOONSHOT_API_KEY",
|
|
657
|
+
"NVIDIA_API_KEY",
|
|
658
|
+
"OPENAI_API_KEY",
|
|
659
|
+
"OPENCODE_API_KEY",
|
|
660
|
+
"OPENROUTER_API_KEY",
|
|
661
|
+
"QWEN_TOKEN_PLAN_API_KEY",
|
|
662
|
+
"QWEN_TOKEN_PLAN_CN_API_KEY",
|
|
663
|
+
"RADIUS_API_KEY",
|
|
664
|
+
"TOGETHER_API_KEY",
|
|
665
|
+
"XAI_API_KEY",
|
|
666
|
+
"XIAOMI_API_KEY",
|
|
667
|
+
"XIAOMI_TOKEN_PLAN_AMS_API_KEY",
|
|
668
|
+
"XIAOMI_TOKEN_PLAN_CN_API_KEY",
|
|
669
|
+
"XIAOMI_TOKEN_PLAN_SGP_API_KEY",
|
|
670
|
+
"ZAI_API_KEY",
|
|
671
|
+
"ZAI_CODING_CN_API_KEY",
|
|
672
|
+
];
|
|
673
|
+
export function scrubForeignProviderEnv() {
|
|
674
|
+
for (const key of FOREIGN_PROVIDER_ENV_KEYS) {
|
|
675
|
+
delete process.env[key];
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/** 把各端点密钥放进环境(TUI 直接 process.env;web 传给子进程)。 */
|
|
679
|
+
export function endpointKeyEnv() {
|
|
680
|
+
const env = {};
|
|
681
|
+
for (const ep of CUSTOM_ENDPOINTS) {
|
|
682
|
+
if (ep.apiKey)
|
|
683
|
+
env[endpointKeyEnvName(ep.id)] = ep.apiKey;
|
|
684
|
+
}
|
|
685
|
+
return env;
|
|
686
|
+
}
|
|
687
|
+
export function ensureProviderModels(cfg, options = {}) {
|
|
688
|
+
const outputPath = options.outputPath ?? join(agentDir, "models.json");
|
|
689
|
+
const sourcePath = options.sourcePath ?? outputPath;
|
|
690
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
691
|
+
let root = {};
|
|
692
|
+
if (existsSync(sourcePath)) {
|
|
693
|
+
try {
|
|
694
|
+
root = JSON.parse(readFileSync(sourcePath, "utf8"));
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
root = {};
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
const providers = typeof root["providers"] === "object" && root["providers"] !== null
|
|
701
|
+
? root["providers"]
|
|
702
|
+
: {};
|
|
703
|
+
providers[PROVIDER_ID] = {
|
|
704
|
+
name: "u1s1",
|
|
705
|
+
baseUrl: cfg.baseUrl,
|
|
706
|
+
api: "openai-completions",
|
|
707
|
+
apiKey: "$U1S1_API_KEY",
|
|
708
|
+
// 网关按这个头识别客户端版本;不带头的旧版会在会话首轮被追加升级提示
|
|
709
|
+
headers: { "x-u1s1-version": VERSION },
|
|
710
|
+
models: toProviderModels(MODELS),
|
|
711
|
+
};
|
|
712
|
+
// 自定义端点:先清掉我们此前写入、如今已删除的条目(键形状 ep+32hex,
|
|
713
|
+
// 用户手工加的其他 provider 不受影响),再写当前列表
|
|
714
|
+
for (const key of Object.keys(providers)) {
|
|
715
|
+
if (ENDPOINT_ID_RE.test(key) && !CUSTOM_ENDPOINTS.some((e) => e.id === key)) {
|
|
716
|
+
delete providers[key];
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
for (const ep of CUSTOM_ENDPOINTS) {
|
|
720
|
+
providers[ep.id] = endpointProviderEntry(ep);
|
|
721
|
+
}
|
|
722
|
+
root["providers"] = providers;
|
|
723
|
+
writeFileSync(outputPath, JSON.stringify(root, null, 2) + "\n", { mode: 0o600 });
|
|
724
|
+
}
|