larkin 0.4.29 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +5 -9
- package/artifacts/release/THIRD_PARTY_NOTICES.txt +751 -4425
- package/dist/app/agent-cli.mjs +14 -2
- package/dist/app/agent-config.mjs +26 -86
- package/dist/app/binary-entry.mjs +0 -20
- package/dist/app/cli.mjs +6 -22
- package/dist/app/internal-command.cjs +0 -2
- package/dist/app/internal-command.mjs +0 -2
- package/dist/app/runtime-model-directory.mjs +2 -16
- package/dist/app/runtime-process.mjs +0 -1
- package/dist/app/setup.mjs +52 -57
- package/dist/dashboard/dashboard-config-controller.mjs +43 -109
- package/dist/dashboard/dashboard-view-model.mjs +4 -7
- package/dist/dashboard/web/assets/dashboard.css +1 -1
- package/dist/dashboard/web/assets/dashboard.js +15 -15
- package/dist/feishu/host-shell.cjs +1 -2
- package/dist/feishu/host-shell.mjs +1 -2
- package/dist/platform/config.cjs +168 -133
- package/dist/platform/config.mjs +168 -133
- package/dist/runtime/pi-artifact-provenance.mjs +3 -3
- package/dist/runtime/pi-bash-timeout-extension.mjs +4 -5
- package/dist/runtime/pi-compaction-recovery.cjs +45 -15
- package/dist/runtime/pi-compaction-recovery.mjs +43 -14
- package/dist/runtime/pi-extension-api.mjs +1 -0
- package/dist/runtime/pi-model-catalog.cjs +7 -8
- package/dist/runtime/pi-model-catalog.mjs +7 -8
- package/dist/runtime/pi-subagent-injection.mjs +17 -4
- package/dist/runtime/runtime-adapters.mjs +26 -43
- package/dist/runtime/runtime-host.cjs +21 -43
- package/dist/runtime/runtime-host.mjs +22 -44
- package/dist/runtime/runtime-model-catalog.cjs +1 -1
- package/dist/runtime/runtime-model-catalog.mjs +1 -1
- package/dist/runtime/runtime-readiness.cjs +62 -98
- package/dist/runtime/runtime-readiness.mjs +60 -97
- package/dist/runtime/user-runtime.cjs +7 -24
- package/dist/runtime/user-runtime.mjs +7 -23
- package/dist/setup/bot-register.mjs +15 -123
- package/dist/setup/setup-agent-choice.mjs +48 -74
- package/dist/setup/setup-bind.mjs +51 -151
- package/dist/setup/setup-binding.mjs +6 -9
- package/package.json +3 -2
- package/dist/app/pi-auth-cli.mjs +0 -277
- package/dist/platform/process-boundary-trace.cjs +0 -108
- package/dist/runtime/builtin-pi-assets.cjs +0 -117
- package/dist/runtime/builtin-pi-assets.mjs +0 -105
- package/dist/runtime/pi-artifact-provenance.cjs +0 -147
- package/dist/runtime/pi-distribution-label.cjs +0 -6
- package/dist/runtime/pi-distribution-label.mjs +0 -3
- package/dist/runtime/pi-inline-extensions.mjs +0 -78
- package/dist/runtime/pi-official-auth.cjs +0 -594
- package/dist/runtime/pi-official-auth.mjs +0 -544
- package/dist/runtime/pi-profile-migration.cjs +0 -764
- package/dist/runtime/pi-profile-migration.mjs +0 -719
- package/dist/runtime/pi-provider-config.cjs +0 -354
- package/dist/runtime/pi-provider-config.mjs +0 -334
- package/dist/runtime/pi-provider-login.mjs +0 -240
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { internalCommandSpec } from "../app/internal-command.mjs";
|
|
5
|
-
import { isWindows, secureWindowsDirectoryAcl } from "../platform/secure-metadata.mjs";
|
|
6
|
-
export { piDistributionLabel } from "./pi-distribution-label.mjs";
|
|
7
|
-
export const BUNDLED_PI_VERSION = "0.84.2";
|
|
8
|
-
/** API Key 字符上限;stdin 必须在读入任意多余输入前按同一上限截断。 */
|
|
9
|
-
export const MAX_BUILTIN_PI_API_KEY_LENGTH = 16_384;
|
|
10
|
-
// These values mirror the catalog shipped by the pinned official Pi distribution.
|
|
11
|
-
// Keeping the user-facing defaults here makes endpoint changes reviewable in Larkin.
|
|
12
|
-
export const PI_PROVIDER_PRESETS = Object.freeze([
|
|
13
|
-
{ id: "deepseek", name: "DeepSeek(推荐)", provider: "deepseek", baseUrl: "https://api.deepseek.com", defaultModel: "deepseek-v4-pro", api: "openai-completions" },
|
|
14
|
-
{ id: "kimi", name: "Kimi / Moonshot(中国)", provider: "moonshotai-cn", baseUrl: "https://api.moonshot.cn/v1", defaultModel: "kimi-k2.6", api: "openai-completions" },
|
|
15
|
-
{ id: "minimax", name: "MiniMax(中国)", provider: "minimax-cn", baseUrl: "https://api.minimaxi.com/anthropic", defaultModel: "MiniMax-M2.7", api: "anthropic-messages" },
|
|
16
|
-
{ id: "zhipu", name: "智谱 / BigModel", provider: "zai-coding-cn", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", defaultModel: "glm-5.2", api: "openai-completions" },
|
|
17
|
-
{ id: "openai", name: "OpenAI", provider: "openai", baseUrl: "https://api.openai.com/v1", defaultModel: "gpt-5.5", api: "openai-completions" },
|
|
18
|
-
{ id: "anthropic", name: "Anthropic Claude", provider: "anthropic", baseUrl: "https://api.anthropic.com", defaultModel: "claude-opus-4-8", api: "anthropic-messages" },
|
|
19
|
-
{ id: "gemini", name: "Google Gemini", provider: "google", baseUrl: "https://generativelanguage.googleapis.com/v1beta", defaultModel: "gemini-3.1-pro-preview", api: "openai-completions" },
|
|
20
|
-
{ id: "groq", name: "Groq", provider: "groq", baseUrl: "https://api.groq.com/openai/v1", defaultModel: "openai/gpt-oss-120b", api: "openai-completions" },
|
|
21
|
-
{ id: "cerebras", name: "Cerebras", provider: "cerebras", baseUrl: "https://api.cerebras.ai/v1", defaultModel: "zai-glm-4.7", api: "openai-completions" },
|
|
22
|
-
{ id: "xai", name: "xAI Grok", provider: "xai", baseUrl: "https://api.x.ai/v1", defaultModel: "grok-4.5", api: "openai-completions" },
|
|
23
|
-
{ id: "fireworks", name: "Fireworks AI", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", defaultModel: "accounts/fireworks/models/kimi-k2p6", api: "openai-completions" },
|
|
24
|
-
{ id: "together", name: "Together AI", provider: "together", baseUrl: "https://api.together.ai/v1", defaultModel: "moonshotai/Kimi-K2.6", api: "openai-completions" },
|
|
25
|
-
{ id: "mistral", name: "Mistral", provider: "mistral", baseUrl: "https://api.mistral.ai", defaultModel: "devstral-medium-latest", api: "openai-completions" },
|
|
26
|
-
{ id: "openrouter", name: "OpenRouter", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", defaultModel: "moonshotai/kimi-k2.6", api: "openai-completions" },
|
|
27
|
-
{ id: "kimi-coding", name: "Kimi For Coding", provider: "kimi-coding", baseUrl: "https://api.kimi.com/coding", defaultModel: "kimi-for-coding", api: "openai-completions" },
|
|
28
|
-
{ id: "qwen-cn", name: "通义千问 Token Plan(中国)", provider: "qwen-token-plan-cn", baseUrl: "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1", defaultModel: "qwen3.7-max", api: "openai-completions" },
|
|
29
|
-
{ id: "opencode-go", name: "OpenCode Go", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", defaultModel: "kimi-k2.6", api: "openai-completions" },
|
|
30
|
-
{ id: "ant-ling", name: "Ant Ling", provider: "ant-ling", baseUrl: "https://api.ant-ling.com/v1", defaultModel: "Ring-2.6-1T", api: "openai-completions" },
|
|
31
|
-
{ id: "nvidia", name: "NVIDIA NIM", provider: "nvidia", baseUrl: "https://integrate.api.nvidia.com/v1", defaultModel: "google/gemma-3-12b-it", api: "openai-completions" },
|
|
32
|
-
{ id: "huggingface", name: "Hugging Face", provider: "huggingface", baseUrl: "https://router.huggingface.co/v1", defaultModel: "MiniMaxAI/MiniMax-M2", api: "openai-completions" },
|
|
33
|
-
{ id: "minimax-global", name: "MiniMax(全球)", provider: "minimax", baseUrl: "https://api.minimax.io/anthropic", defaultModel: "MiniMax-M2.7", api: "anthropic-messages" },
|
|
34
|
-
{ id: "moonshotai", name: "Moonshot(全球)", provider: "moonshotai", baseUrl: "https://api.moonshot.ai/v1", defaultModel: "kimi-k2-0711-preview", api: "openai-completions" },
|
|
35
|
-
{ id: "opencode", name: "OpenCode Zen", provider: "opencode", baseUrl: "https://opencode.ai/zen", defaultModel: "claude-fable-5", api: "openai-completions" },
|
|
36
|
-
{ id: "qwen-token-plan", name: "通义千问 Token Plan(全球)", provider: "qwen-token-plan", baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1", defaultModel: "qwen3.7-max", api: "openai-completions" },
|
|
37
|
-
{ id: "vercel-ai-gateway", name: "Vercel AI Gateway", provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", defaultModel: "zai/glm-5.1", api: "anthropic-messages" },
|
|
38
|
-
{ id: "xiaomi", name: "Xiaomi MiMo", provider: "xiaomi", baseUrl: "https://api.xiaomimimo.com/v1", defaultModel: "mimo-v2-flash", api: "openai-completions" },
|
|
39
|
-
{ id: "xiaomi-token-plan-cn", name: "Xiaomi MiMo Token Plan(中国)", provider: "xiaomi-token-plan-cn", baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", defaultModel: "mimo-v2.5-pro", api: "openai-completions" },
|
|
40
|
-
{ id: "xiaomi-token-plan-ams", name: "Xiaomi MiMo Token Plan(阿姆斯特丹)", provider: "xiaomi-token-plan-ams", baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", defaultModel: "mimo-v2.5-pro", api: "openai-completions" },
|
|
41
|
-
{ id: "xiaomi-token-plan-sgp", name: "Xiaomi MiMo Token Plan(新加坡)", provider: "xiaomi-token-plan-sgp", baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", defaultModel: "mimo-v2.5-pro", api: "openai-completions" },
|
|
42
|
-
{ id: "zai", name: "ZAI Coding Plan(全球)", provider: "zai", baseUrl: "https://api.z.ai/api/coding/paas/v4", defaultModel: "glm-4.5-air", api: "openai-completions" },
|
|
43
|
-
]);
|
|
44
|
-
const APP_ID = /^cli_[A-Za-z0-9]+$/;
|
|
45
|
-
const MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._:@+\/-]{0,255}$/;
|
|
46
|
-
/** Node 的 URL.hostname 对 IPv6 是 [::1],不是 ::1。 */
|
|
47
|
-
export function isPiLoopbackHostname(hostname) {
|
|
48
|
-
const host = hostname.trim().replace(/^\[|\]$/g, "").toLowerCase();
|
|
49
|
-
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
50
|
-
}
|
|
51
|
-
/** 官方 provider ID(如 zai-coding-cn)映射到 CLI/Dashboard 接受的 preset ID(如 zhipu)。 */
|
|
52
|
-
export function presetIdForOfficialProvider(providerId) {
|
|
53
|
-
const id = providerId.trim();
|
|
54
|
-
if (id === "larkin-custom")
|
|
55
|
-
return "custom";
|
|
56
|
-
return PI_PROVIDER_PRESETS.find((preset) => preset.provider === id)?.id ?? null;
|
|
57
|
-
}
|
|
58
|
-
/** Agent 范围的缺凭证恢复路径:只用 preset,不指向 setup / import / 全局 ~/.pi。 */
|
|
59
|
-
export function builtinPiProviderRecoveryNextAction(options = {}) {
|
|
60
|
-
const preset = options.providerId ? presetIdForOfficialProvider(options.providerId) ?? "<preset|custom>" : "<preset|custom>";
|
|
61
|
-
const agent = options.agentId && /^cli_[A-Za-z0-9]+$/.test(options.agentId) ? options.agentId : "<App ID>";
|
|
62
|
-
return `在 Dashboard 的 Provider Credentials 中为该 Agent 配置 Provider 与 API Key,或运行 larkin pi-auth login ${preset} --agent ${agent}。`;
|
|
63
|
-
}
|
|
64
|
-
export function validatePiBaseUrl(raw) {
|
|
65
|
-
const input = raw.trim();
|
|
66
|
-
if (!input || /[\0\r\n\t]/.test(input))
|
|
67
|
-
throw new Error("Base URL 不能为空或包含控制字符");
|
|
68
|
-
let url;
|
|
69
|
-
try {
|
|
70
|
-
url = new URL(input);
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
throw new Error("Base URL 不是合法 URL");
|
|
74
|
-
}
|
|
75
|
-
if (url.username || url.password)
|
|
76
|
-
throw new Error("Base URL 不允许包含用户名或凭证");
|
|
77
|
-
if (url.search || url.hash)
|
|
78
|
-
throw new Error("Base URL 不允许 query 或 fragment");
|
|
79
|
-
const local = isPiLoopbackHostname(url.hostname);
|
|
80
|
-
if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
|
|
81
|
-
throw new Error("Base URL 必须使用 https;仅 localhost/loopback 开发端点允许 http");
|
|
82
|
-
}
|
|
83
|
-
if (/\/(?:chat\/completions|responses|models)\/?$/i.test(url.pathname)) {
|
|
84
|
-
throw new Error("Base URL 应填写 API 根路径,不要包含 chat/completions、responses 或 models");
|
|
85
|
-
}
|
|
86
|
-
return url.toString().replace(/\/$/, "");
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* 查询 OpenAI 兼容 provider 的 /models 列表(setup 时校验 --model 是否真实可用)。
|
|
90
|
-
* 网络超时/不可达返回 null(不阻塞 setup——运行时对账会严格兜底);
|
|
91
|
-
* 列表拿到但模型不在其中时抛错。
|
|
92
|
-
*/
|
|
93
|
-
export async function listProviderModels(baseUrl, apiKey, fetchImpl = fetch) {
|
|
94
|
-
try {
|
|
95
|
-
const url = `${validatePiBaseUrl(baseUrl)}/models`;
|
|
96
|
-
const response = await fetchImpl(url, {
|
|
97
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
98
|
-
signal: AbortSignal.timeout(30_000),
|
|
99
|
-
});
|
|
100
|
-
if (!response.ok)
|
|
101
|
-
throw new Error(`provider /models 查询失败:HTTP ${response.status}`);
|
|
102
|
-
const payload = await response.json();
|
|
103
|
-
if (!payload || !Array.isArray(payload.data))
|
|
104
|
-
throw new Error("provider /models 响应格式非法");
|
|
105
|
-
return payload.data.map((entry) => String(entry.id ?? "")).filter(Boolean);
|
|
106
|
-
}
|
|
107
|
-
catch (error) {
|
|
108
|
-
if (error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError"
|
|
109
|
-
|| /fetch failed|network|ETIMEDOUT|ECONNREFUSED|ConnectionRefused|Unable to connect/i.test(error.message))) {
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
throw error;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
export function resolveBuiltinPiProviderSetupSelection(input) {
|
|
116
|
-
if (input.distribution !== "builtin")
|
|
117
|
-
throw new Error("provider 凭证只能用于内置 Pi");
|
|
118
|
-
const model = input.model.trim();
|
|
119
|
-
if (!MODEL_ID.test(model))
|
|
120
|
-
throw new Error("模型 ID 格式不安全");
|
|
121
|
-
if (input.preset === "custom") {
|
|
122
|
-
const provider = "larkin-custom";
|
|
123
|
-
return {
|
|
124
|
-
...input,
|
|
125
|
-
model: model.startsWith(`${provider}/`) ? model : `${provider}/${model}`,
|
|
126
|
-
provider,
|
|
127
|
-
baseUrl: validatePiBaseUrl(input.baseUrl || ""),
|
|
128
|
-
api: "openai-completions",
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
const preset = PI_PROVIDER_PRESETS.find((candidate) => candidate.id === input.preset);
|
|
132
|
-
if (!preset)
|
|
133
|
-
throw new Error(`未知 Pi provider preset: ${String(input.preset)}`);
|
|
134
|
-
return { ...input, model: model.startsWith(`${preset.provider}/`) ? model : `${preset.provider}/${model}`,
|
|
135
|
-
provider: preset.provider, baseUrl: preset.baseUrl, api: preset.api };
|
|
136
|
-
}
|
|
137
|
-
export function validateBuiltinPiProviderSelection(input) {
|
|
138
|
-
const key = input.apiKey.trim();
|
|
139
|
-
if (!key || key.length > MAX_BUILTIN_PI_API_KEY_LENGTH || /[\0\r\n]/.test(key))
|
|
140
|
-
throw new Error("API Key 不能为空或包含控制字符");
|
|
141
|
-
return { ...resolveBuiltinPiProviderSetupSelection(input), apiKey: key };
|
|
142
|
-
}
|
|
143
|
-
export function piAgentDirectory(configDir, agentId) {
|
|
144
|
-
if (!APP_ID.test(agentId))
|
|
145
|
-
throw new Error("Pi Agent ID 格式无效");
|
|
146
|
-
return path.join(path.resolve(configDir), "providers", "pi", agentId);
|
|
147
|
-
}
|
|
148
|
-
/** Catalog/CLI/dashboard must use the Agent-owned provider dir, never host ~/.pi. */
|
|
149
|
-
export function ownedPiCatalogAgentDir(configDir, agentId) {
|
|
150
|
-
return piAgentDirectory(configDir, agentId);
|
|
151
|
-
}
|
|
152
|
-
export function piCatalogCommandSpec(distribution, env = process.env) {
|
|
153
|
-
if (distribution === "builtin") {
|
|
154
|
-
const spec = internalCommandSpec("pi-rpc", [], env);
|
|
155
|
-
return { command: spec.command, commandArgs: spec.args };
|
|
156
|
-
}
|
|
157
|
-
return {
|
|
158
|
-
command: env.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi",
|
|
159
|
-
commandArgs: [],
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
function assertPrivateDirectory(directory) {
|
|
163
|
-
if (isWindows) {
|
|
164
|
-
// Windows 无 POSIX 权限位:icacls 收紧为「当前用户 + SYSTEM」并回读校验。
|
|
165
|
-
secureWindowsDirectoryAcl(directory, { label: "Pi provider 凭证目录" });
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
const stat = fs.lstatSync(directory);
|
|
169
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
170
|
-
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
171
|
-
|| (stat.mode & 0o777) !== 0o700) {
|
|
172
|
-
throw new Error("Pi provider 凭证目录必须是当前用户拥有、非 symlink、权限精确 0700 的目录");
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
export function assertBuiltinPiAgentDirectory(directory) {
|
|
176
|
-
assertPrivateDirectory(directory);
|
|
177
|
-
const authFile = path.join(directory, "auth.json");
|
|
178
|
-
const auth = readSnapshot(authFile);
|
|
179
|
-
if (!auth)
|
|
180
|
-
throw new Error("内置 Pi 尚未配置 API Key");
|
|
181
|
-
try {
|
|
182
|
-
const value = JSON.parse(auth.toString("utf8"));
|
|
183
|
-
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).length === 0)
|
|
184
|
-
throw new Error("empty");
|
|
185
|
-
}
|
|
186
|
-
catch {
|
|
187
|
-
throw new Error("内置 Pi auth.json 格式无效");
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
function ensurePrivateDirectory(directory) {
|
|
191
|
-
fs.mkdirSync(path.dirname(directory), { recursive: true, mode: 0o700 });
|
|
192
|
-
fs.chmodSync(path.dirname(directory), 0o700);
|
|
193
|
-
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
194
|
-
fs.chmodSync(directory, 0o700);
|
|
195
|
-
assertPrivateDirectory(directory);
|
|
196
|
-
}
|
|
197
|
-
function readSnapshot(file) {
|
|
198
|
-
try {
|
|
199
|
-
const stat = fs.lstatSync(file);
|
|
200
|
-
if (!stat.isFile() || stat.isSymbolicLink()
|
|
201
|
-
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
202
|
-
|| (!isWindows && (stat.mode & 0o777) !== 0o600))
|
|
203
|
-
throw new Error(`Pi provider 文件不安全: ${path.basename(file)}`);
|
|
204
|
-
return fs.readFileSync(file);
|
|
205
|
-
}
|
|
206
|
-
catch (error) {
|
|
207
|
-
if (error.code === "ENOENT")
|
|
208
|
-
return null;
|
|
209
|
-
throw error;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
function atomicPrivateWrite(file, content) {
|
|
213
|
-
const directory = path.dirname(file);
|
|
214
|
-
assertPrivateDirectory(directory);
|
|
215
|
-
const temporary = path.join(directory, `.${path.basename(file)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
216
|
-
try {
|
|
217
|
-
fs.writeFileSync(temporary, content, { mode: 0o600, flag: "wx" });
|
|
218
|
-
fs.renameSync(temporary, file);
|
|
219
|
-
fs.chmodSync(file, 0o600);
|
|
220
|
-
}
|
|
221
|
-
finally {
|
|
222
|
-
try {
|
|
223
|
-
fs.unlinkSync(temporary);
|
|
224
|
-
}
|
|
225
|
-
catch { /* renamed or absent */ }
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
function restore(file, snapshot) {
|
|
229
|
-
if (snapshot === null) {
|
|
230
|
-
try {
|
|
231
|
-
fs.unlinkSync(file);
|
|
232
|
-
}
|
|
233
|
-
catch (error) {
|
|
234
|
-
if (error.code !== "ENOENT")
|
|
235
|
-
throw error;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
else
|
|
239
|
-
atomicPrivateWrite(file, snapshot.toString("utf8"));
|
|
240
|
-
}
|
|
241
|
-
export function configureBuiltinPiProviderModel(configDir, agentId, raw) {
|
|
242
|
-
const selection = resolveBuiltinPiProviderSetupSelection(raw);
|
|
243
|
-
const directory = piAgentDirectory(configDir, agentId);
|
|
244
|
-
ensurePrivateDirectory(directory);
|
|
245
|
-
if (selection.preset !== "custom")
|
|
246
|
-
return selection;
|
|
247
|
-
const modelsFile = path.join(directory, "models.json");
|
|
248
|
-
const beforeModels = readSnapshot(modelsFile);
|
|
249
|
-
const currentModels = beforeModels ? JSON.parse(beforeModels.toString("utf8")) : {};
|
|
250
|
-
if (!currentModels || typeof currentModels !== "object" || Array.isArray(currentModels))
|
|
251
|
-
throw new Error("现有 Pi models.json 格式无效");
|
|
252
|
-
const currentProviders = currentModels.providers === undefined ? {} : currentModels.providers;
|
|
253
|
-
if (!currentProviders || typeof currentProviders !== "object" || Array.isArray(currentProviders))
|
|
254
|
-
throw new Error("现有 Pi models.json providers 格式无效");
|
|
255
|
-
const modelId = selection.model.slice(`${selection.provider}/`.length);
|
|
256
|
-
atomicPrivateWrite(modelsFile, `${JSON.stringify({
|
|
257
|
-
...currentModels,
|
|
258
|
-
providers: {
|
|
259
|
-
...currentProviders,
|
|
260
|
-
[selection.provider]: {
|
|
261
|
-
baseUrl: selection.baseUrl,
|
|
262
|
-
api: selection.api,
|
|
263
|
-
models: [{
|
|
264
|
-
id: modelId, name: modelId, reasoning: false, input: ["text"], contextWindow: 272_000, maxTokens: 16_384,
|
|
265
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
266
|
-
}],
|
|
267
|
-
},
|
|
268
|
-
},
|
|
269
|
-
}, null, 2)}\n`);
|
|
270
|
-
return selection;
|
|
271
|
-
}
|
|
272
|
-
export function stageBuiltinPiProvider(configDir, agentId, raw) {
|
|
273
|
-
const selection = validateBuiltinPiProviderSelection(raw);
|
|
274
|
-
const directory = piAgentDirectory(configDir, agentId);
|
|
275
|
-
ensurePrivateDirectory(directory);
|
|
276
|
-
const authFile = path.join(directory, "auth.json");
|
|
277
|
-
const modelsFile = path.join(directory, "models.json");
|
|
278
|
-
const beforeAuth = readSnapshot(authFile);
|
|
279
|
-
const beforeModels = readSnapshot(modelsFile);
|
|
280
|
-
const currentAuth = beforeAuth ? JSON.parse(beforeAuth.toString("utf8")) : {};
|
|
281
|
-
if (!currentAuth || typeof currentAuth !== "object" || Array.isArray(currentAuth))
|
|
282
|
-
throw new Error("现有 Pi auth.json 格式无效");
|
|
283
|
-
const auth = { ...currentAuth, [selection.provider]: { type: "api_key", key: selection.apiKey } };
|
|
284
|
-
const currentModels = beforeModels ? JSON.parse(beforeModels.toString("utf8")) : {};
|
|
285
|
-
if (!currentModels || typeof currentModels !== "object" || Array.isArray(currentModels))
|
|
286
|
-
throw new Error("现有 Pi models.json 格式无效");
|
|
287
|
-
const currentProviders = currentModels.providers === undefined ? {} : currentModels.providers;
|
|
288
|
-
if (!currentProviders || typeof currentProviders !== "object" || Array.isArray(currentProviders))
|
|
289
|
-
throw new Error("现有 Pi models.json providers 格式无效");
|
|
290
|
-
const customModels = selection.preset === "custom" ? {
|
|
291
|
-
...currentModels,
|
|
292
|
-
providers: {
|
|
293
|
-
...currentProviders,
|
|
294
|
-
[selection.provider]: {
|
|
295
|
-
baseUrl: selection.baseUrl,
|
|
296
|
-
api: selection.api,
|
|
297
|
-
models: [{
|
|
298
|
-
id: selection.model.slice(`${selection.provider}/`.length),
|
|
299
|
-
name: selection.model.slice(`${selection.provider}/`.length),
|
|
300
|
-
reasoning: false,
|
|
301
|
-
input: ["text"],
|
|
302
|
-
contextWindow: 272_000,
|
|
303
|
-
maxTokens: 16_384,
|
|
304
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
305
|
-
}],
|
|
306
|
-
},
|
|
307
|
-
},
|
|
308
|
-
} : null;
|
|
309
|
-
let active = true;
|
|
310
|
-
try {
|
|
311
|
-
atomicPrivateWrite(authFile, `${JSON.stringify(auth, null, 2)}\n`);
|
|
312
|
-
if (customModels)
|
|
313
|
-
atomicPrivateWrite(modelsFile, `${JSON.stringify(customModels, null, 2)}\n`);
|
|
314
|
-
}
|
|
315
|
-
catch (error) {
|
|
316
|
-
try {
|
|
317
|
-
restore(authFile, beforeAuth);
|
|
318
|
-
restore(modelsFile, beforeModels);
|
|
319
|
-
}
|
|
320
|
-
catch { /* original error remains primary */ }
|
|
321
|
-
throw error;
|
|
322
|
-
}
|
|
323
|
-
return {
|
|
324
|
-
directory,
|
|
325
|
-
commit() { active = false; },
|
|
326
|
-
rollback() {
|
|
327
|
-
if (!active)
|
|
328
|
-
return;
|
|
329
|
-
restore(authFile, beforeAuth);
|
|
330
|
-
restore(modelsFile, beforeModels);
|
|
331
|
-
active = false;
|
|
332
|
-
},
|
|
333
|
-
};
|
|
334
|
-
}
|
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
import { PI_PROVIDER_PRESETS, configureBuiltinPiProviderModel, validateBuiltinPiProviderSelection, } from "./pi-provider-config.mjs";
|
|
2
|
-
import { beginBuiltinPiCredentialTransaction, createOfficialPiAuthInteraction, createOfficialPiLogoutRuntime, createOfficialPiModelRuntime, logoutOfficialPiProvider, runOfficialPiLogin, } from "./pi-official-auth.mjs";
|
|
3
|
-
import { loadConfig, markConfigApplied, mutateConfig, runtimeConfigSignature, } from "../platform/config.mjs";
|
|
4
|
-
import { requestAgentUpsert } from "../app/local-control.mjs";
|
|
5
|
-
import { readProcessState } from "../platform/process-state.mjs";
|
|
6
|
-
const APP_ID = /^cli_[A-Za-z0-9]+$/;
|
|
7
|
-
const PROVIDER_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
8
|
-
const PRESET_IDS = new Set([...PI_PROVIDER_PRESETS.map((preset) => preset.id), "custom"]);
|
|
9
|
-
/** 用户可见 catalog:已知 preset + 自定义 OpenAI 兼容端点,不含 Base URL / secret。 */
|
|
10
|
-
export function listBuiltinPiProviderCatalog() {
|
|
11
|
-
return [
|
|
12
|
-
...PI_PROVIDER_PRESETS.map((preset) => ({
|
|
13
|
-
id: preset.id,
|
|
14
|
-
name: preset.name,
|
|
15
|
-
provider: preset.provider,
|
|
16
|
-
defaultModel: `${preset.provider}/${preset.defaultModel}`,
|
|
17
|
-
custom: false,
|
|
18
|
-
openaiCompatible: preset.api === "openai-completions",
|
|
19
|
-
})),
|
|
20
|
-
{
|
|
21
|
-
id: "custom",
|
|
22
|
-
name: "Custom OpenAI-compatible",
|
|
23
|
-
provider: "larkin-custom",
|
|
24
|
-
defaultModel: null,
|
|
25
|
-
custom: true,
|
|
26
|
-
openaiCompatible: true,
|
|
27
|
-
},
|
|
28
|
-
];
|
|
29
|
-
}
|
|
30
|
-
export function sanitizeProviderLoginError(error, secret) {
|
|
31
|
-
let text = error instanceof Error ? error.message : String(error);
|
|
32
|
-
if (secret) {
|
|
33
|
-
let from = 0;
|
|
34
|
-
while (from < text.length) {
|
|
35
|
-
const at = text.indexOf(secret, from);
|
|
36
|
-
if (at < 0)
|
|
37
|
-
break;
|
|
38
|
-
text = `${text.slice(0, at)}[redacted]${text.slice(at + secret.length)}`;
|
|
39
|
-
from = at + 10;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return text
|
|
43
|
-
.replace(/\b(?:authorization|proxy-authorization|api[_ -]?key|access[_ -]?token|refresh[_ -]?token|secret|password)\s*[:=]\s*\S+/gi, (match) => `${match.split(/[:=]/, 1)[0]}=[redacted]`)
|
|
44
|
-
.replace(/Bearer\s+\S+/gi, "Bearer [redacted]")
|
|
45
|
-
.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "[redacted]")
|
|
46
|
-
.replace(/https?:\/\/[^\s]+/gi, (url) => {
|
|
47
|
-
try {
|
|
48
|
-
const parsed = new URL(url);
|
|
49
|
-
parsed.username = "";
|
|
50
|
-
parsed.password = "";
|
|
51
|
-
parsed.search = "";
|
|
52
|
-
parsed.hash = "";
|
|
53
|
-
return parsed.toString();
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
return "(url)";
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
.replace(/[\0\r\n\t]+/g, " ")
|
|
60
|
-
.replace(/\s+/g, " ")
|
|
61
|
-
.trim()
|
|
62
|
-
.slice(0, 240) || "builtin Pi provider configuration failed";
|
|
63
|
-
}
|
|
64
|
-
export function resolveBuiltinPiLoginSelection(input) {
|
|
65
|
-
if (!PRESET_IDS.has(input.preset))
|
|
66
|
-
throw new Error(`未知 Pi provider preset: ${input.preset}`);
|
|
67
|
-
const preset = input.preset;
|
|
68
|
-
if (preset === "custom") {
|
|
69
|
-
if (!input.baseUrl?.trim())
|
|
70
|
-
throw new Error("custom OpenAI-compatible endpoint 需要 Base URL");
|
|
71
|
-
if (!input.model?.trim())
|
|
72
|
-
throw new Error("custom OpenAI-compatible endpoint 需要 model");
|
|
73
|
-
return validateBuiltinPiProviderSelection({
|
|
74
|
-
distribution: "builtin",
|
|
75
|
-
preset,
|
|
76
|
-
apiKey: input.apiKey,
|
|
77
|
-
model: input.model,
|
|
78
|
-
baseUrl: input.baseUrl,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
if (input.baseUrl?.trim())
|
|
82
|
-
throw new Error("已知 provider 不接受 --base-url;自定义端点请使用 custom");
|
|
83
|
-
const known = PI_PROVIDER_PRESETS.find((candidate) => candidate.id === preset);
|
|
84
|
-
if (!known)
|
|
85
|
-
throw new Error(`未知 Pi provider preset: ${input.preset}`);
|
|
86
|
-
return validateBuiltinPiProviderSelection({
|
|
87
|
-
distribution: "builtin",
|
|
88
|
-
preset,
|
|
89
|
-
apiKey: input.apiKey,
|
|
90
|
-
model: input.model?.trim() || known.defaultModel,
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
export function createApiKeyOnlyInteraction(apiKey, options = {}) {
|
|
94
|
-
const questioner = {
|
|
95
|
-
ask: async () => {
|
|
96
|
-
throw new Error("API-key login does not accept interactive text prompts");
|
|
97
|
-
},
|
|
98
|
-
secret: async () => apiKey,
|
|
99
|
-
};
|
|
100
|
-
return createOfficialPiAuthInteraction({
|
|
101
|
-
questioner,
|
|
102
|
-
report: options.report ?? (() => { }),
|
|
103
|
-
signal: options.signal,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
function assertTargetBuiltinPi(env, agentId, load) {
|
|
107
|
-
if (!APP_ID.test(agentId))
|
|
108
|
-
throw new Error("请用 --agent <App ID> 选择 Agent");
|
|
109
|
-
const loaded = load(env);
|
|
110
|
-
const agent = loaded.config.agents[agentId];
|
|
111
|
-
if (!agent)
|
|
112
|
-
throw new Error(`Agent ${agentId} 不存在`);
|
|
113
|
-
if (agent.runtime !== "pi" || agent.piDistribution !== "builtin") {
|
|
114
|
-
throw new Error(`Agent ${agentId} 不是内置 Pi`);
|
|
115
|
-
}
|
|
116
|
-
return { configDir: loaded.configDir };
|
|
117
|
-
}
|
|
118
|
-
async function applyTargetModel(env, agentId, model, mutate, inspect, upsert, markApplied, secret) {
|
|
119
|
-
let mutated;
|
|
120
|
-
try {
|
|
121
|
-
mutated = mutate(env, { kind: "set-agent-model", agentId, model, requireBuiltinPi: true }, { kind: "user" });
|
|
122
|
-
}
|
|
123
|
-
catch (error) {
|
|
124
|
-
throw new Error(sanitizeProviderLoginError(error, secret));
|
|
125
|
-
}
|
|
126
|
-
const stillBuiltin = mutated.config.agents[agentId]?.runtime === "pi"
|
|
127
|
-
&& mutated.config.agents[agentId]?.piDistribution === "builtin";
|
|
128
|
-
if (!stillBuiltin) {
|
|
129
|
-
return { applyState: "pending", applyError: sanitizeProviderLoginError(new Error(`Agent ${agentId} 不是内置 Pi`), secret) };
|
|
130
|
-
}
|
|
131
|
-
const expectedSignature = runtimeConfigSignature(mutated.config, agentId);
|
|
132
|
-
const state = inspect(mutated.config.larkinHome);
|
|
133
|
-
if (state.daemon.state !== "owned" || state.supervisor.state !== "owned") {
|
|
134
|
-
return { applyState: "saved_not_applied" };
|
|
135
|
-
}
|
|
136
|
-
try {
|
|
137
|
-
const result = await upsert({
|
|
138
|
-
larkinHome: mutated.config.larkinHome,
|
|
139
|
-
agentId,
|
|
140
|
-
expectedSignature,
|
|
141
|
-
});
|
|
142
|
-
if (!result.ok) {
|
|
143
|
-
return {
|
|
144
|
-
applyState: "pending",
|
|
145
|
-
applyError: sanitizeProviderLoginError(result.error || "targeted apply failed", secret),
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
markApplied(env, agentId, expectedSignature);
|
|
149
|
-
return { applyState: "applied" };
|
|
150
|
-
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
return { applyState: "pending", applyError: sanitizeProviderLoginError(error, secret) };
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* 为已有 builtin Pi Agent 配置已知 preset 或自定义 OpenAI 兼容端点。
|
|
157
|
-
* 凭证事务覆盖 auth.json / models.json / models-store.json;模型写入走独立 CAS。
|
|
158
|
-
* 不读取 host ~/.pi,也不验证真实 provider。
|
|
159
|
-
*/
|
|
160
|
-
export async function configureBuiltinPiProvider(input, deps = {}) {
|
|
161
|
-
const env = input.env ?? process.env;
|
|
162
|
-
const load = deps.loadConfig ?? loadConfig;
|
|
163
|
-
const beginTransaction = deps.beginTransaction ?? beginBuiltinPiCredentialTransaction;
|
|
164
|
-
const configureModel = deps.configureModel ?? configureBuiltinPiProviderModel;
|
|
165
|
-
const createRuntime = deps.createRuntime ?? createOfficialPiModelRuntime;
|
|
166
|
-
const runLogin = deps.runLogin ?? runOfficialPiLogin;
|
|
167
|
-
const createInteraction = deps.createInteraction ?? createApiKeyOnlyInteraction;
|
|
168
|
-
const mutate = deps.mutateConfig ?? mutateConfig;
|
|
169
|
-
const inspect = deps.readProcessState ?? readProcessState;
|
|
170
|
-
const upsert = deps.requestUpsert ?? requestAgentUpsert;
|
|
171
|
-
const markApplied = deps.markApplied ?? markConfigApplied;
|
|
172
|
-
let selection;
|
|
173
|
-
try {
|
|
174
|
-
selection = resolveBuiltinPiLoginSelection({
|
|
175
|
-
preset: input.preset,
|
|
176
|
-
apiKey: input.apiKey,
|
|
177
|
-
model: input.model,
|
|
178
|
-
baseUrl: input.baseUrl,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
catch (error) {
|
|
182
|
-
throw new Error(sanitizeProviderLoginError(error, input.apiKey));
|
|
183
|
-
}
|
|
184
|
-
const { configDir } = assertTargetBuiltinPi(env, input.agentId, load);
|
|
185
|
-
const transaction = beginTransaction(configDir, input.agentId);
|
|
186
|
-
try {
|
|
187
|
-
if (selection.preset === "custom") {
|
|
188
|
-
configureModel(configDir, input.agentId, {
|
|
189
|
-
distribution: "builtin",
|
|
190
|
-
preset: "custom",
|
|
191
|
-
model: selection.model,
|
|
192
|
-
baseUrl: selection.baseUrl,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
const runtime = await createRuntime(configDir, input.agentId);
|
|
196
|
-
await runLogin(runtime, selection.provider, "api_key", createInteraction(selection.apiKey));
|
|
197
|
-
const applied = await applyTargetModel(env, input.agentId, selection.model, mutate, inspect, upsert, markApplied, input.apiKey);
|
|
198
|
-
transaction.commit();
|
|
199
|
-
return {
|
|
200
|
-
agentId: input.agentId,
|
|
201
|
-
provider: selection.provider,
|
|
202
|
-
preset: selection.preset,
|
|
203
|
-
model: selection.model,
|
|
204
|
-
credentialType: "api_key",
|
|
205
|
-
applyState: applied.applyState,
|
|
206
|
-
...(applied.applyError ? { applyError: applied.applyError } : {}),
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
catch (error) {
|
|
210
|
-
try {
|
|
211
|
-
transaction.rollback();
|
|
212
|
-
}
|
|
213
|
-
catch { /* 原始错误优先 */ }
|
|
214
|
-
throw new Error(sanitizeProviderLoginError(error, input.apiKey));
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
export async function logoutBuiltinPiProvider(input, deps = {}) {
|
|
218
|
-
const env = input.env ?? process.env;
|
|
219
|
-
const load = deps.loadConfig ?? loadConfig;
|
|
220
|
-
if (!PROVIDER_ID.test(input.providerId))
|
|
221
|
-
throw new Error("logout 需要安全的 provider ID");
|
|
222
|
-
const { configDir } = assertTargetBuiltinPi(env, input.agentId, load);
|
|
223
|
-
const beginTransaction = deps.beginTransaction ?? beginBuiltinPiCredentialTransaction;
|
|
224
|
-
const createLogoutRuntime = deps.createLogoutRuntime ?? createOfficialPiLogoutRuntime;
|
|
225
|
-
const logout = deps.logout ?? logoutOfficialPiProvider;
|
|
226
|
-
const transaction = beginTransaction(configDir, input.agentId);
|
|
227
|
-
try {
|
|
228
|
-
const runtime = await createLogoutRuntime(configDir, input.agentId);
|
|
229
|
-
await logout(runtime, input.providerId);
|
|
230
|
-
transaction.commit();
|
|
231
|
-
return { agentId: input.agentId, provider: input.providerId };
|
|
232
|
-
}
|
|
233
|
-
catch (error) {
|
|
234
|
-
try {
|
|
235
|
-
transaction.rollback();
|
|
236
|
-
}
|
|
237
|
-
catch { /* 原始错误优先 */ }
|
|
238
|
-
throw new Error(sanitizeProviderLoginError(error));
|
|
239
|
-
}
|
|
240
|
-
}
|