larkin 0.2.70
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/CONTRIBUTING.md +48 -0
- package/LICENSE +201 -0
- package/README.md +122 -0
- package/SECURITY.md +14 -0
- package/artifacts/release/THIRD_PARTY_NOTICES.txt +5488 -0
- package/dist/agent/agent-cli-capabilities.cjs +43 -0
- package/dist/agent/agent-cli-capabilities.mjs +35 -0
- package/dist/agent/agent-state-store.cjs +961 -0
- package/dist/agent/agent-state-store.mjs +923 -0
- package/dist/agent/agent-transport.cjs +201 -0
- package/dist/agent/agent-transport.mjs +194 -0
- package/dist/agent/config-cli-contract.cjs +19 -0
- package/dist/agent/config-cli-contract.mjs +16 -0
- package/dist/agent/context-prompt.cjs +159 -0
- package/dist/agent/context-prompt.mjs +155 -0
- package/dist/agent/freshness-gate.mjs +24 -0
- package/dist/agent/history-projection.cjs +72 -0
- package/dist/agent/history-projection.mjs +65 -0
- package/dist/agent/host-reminder-orchestrator.cjs +289 -0
- package/dist/agent/host-reminder-orchestrator.mjs +252 -0
- package/dist/agent/inbox-projection.cjs +92 -0
- package/dist/agent/inbox-projection.mjs +85 -0
- package/dist/agent/interaction-state-machine.cjs +710 -0
- package/dist/agent/interaction-state-machine.mjs +705 -0
- package/dist/agent/reminder-routes.cjs +192 -0
- package/dist/agent/reminder-routes.mjs +156 -0
- package/dist/agent/reminder-store.cjs +220 -0
- package/dist/agent/reminder-store.mjs +205 -0
- package/dist/agent/transport-business-context.cjs +486 -0
- package/dist/agent/transport-business-context.mjs +446 -0
- package/dist/agent/transport-shell.cjs +45 -0
- package/dist/agent/transport-shell.mjs +39 -0
- package/dist/app/agent-cli.mjs +596 -0
- package/dist/app/agent-config.mjs +633 -0
- package/dist/app/agent-lark-cli-workspace.cjs +95 -0
- package/dist/app/agent-lark-cli-workspace.mjs +85 -0
- package/dist/app/agent-readiness.cjs +42 -0
- package/dist/app/agent-readiness.mjs +38 -0
- package/dist/app/binary-entry.mjs +86 -0
- package/dist/app/cli.mjs +176 -0
- package/dist/app/dashboard.mjs +205 -0
- package/dist/app/internal-command.cjs +57 -0
- package/dist/app/internal-command.mjs +48 -0
- package/dist/app/lark-channel-secret.mjs +22 -0
- package/dist/app/lark-cli.mjs +887 -0
- package/dist/app/lark.mjs +45 -0
- package/dist/app/local-control.mjs +728 -0
- package/dist/app/official-lark-cli.cjs +138 -0
- package/dist/app/official-lark-cli.mjs +128 -0
- package/dist/app/pi-auth-cli.mjs +67 -0
- package/dist/app/run.mjs +364 -0
- package/dist/app/runtime-agent-config.mjs +416 -0
- package/dist/app/runtime-model-directory.mjs +78 -0
- package/dist/app/runtime-process.mjs +113 -0
- package/dist/app/session-cli.mjs +83 -0
- package/dist/app/setup-dashboard.mjs +26 -0
- package/dist/app/setup.mjs +166 -0
- package/dist/app/telemetry.mjs +83 -0
- package/dist/dashboard/dashboard-assets.mjs +21 -0
- package/dist/dashboard/dashboard-avatar.mjs +128 -0
- package/dist/dashboard/dashboard-config-controller.mjs +525 -0
- package/dist/dashboard/dashboard-lifecycle.cjs +97 -0
- package/dist/dashboard/dashboard-lifecycle.mjs +88 -0
- package/dist/dashboard/dashboard-template.mjs +29 -0
- package/dist/dashboard/dashboard-view-model.mjs +556 -0
- package/dist/dashboard/dashboard-workspace.mjs +104 -0
- package/dist/dashboard/web/assets/dashboard.css +3 -0
- package/dist/dashboard/web/assets/dashboard.js +49 -0
- package/dist/feishu/channel-transport.cjs +151 -0
- package/dist/feishu/channel-transport.mjs +148 -0
- package/dist/feishu/document-comment.cjs +120 -0
- package/dist/feishu/document-comment.mjs +109 -0
- package/dist/feishu/host-business-state.cjs +477 -0
- package/dist/feishu/host-business-state.mjs +462 -0
- package/dist/feishu/host-channel-business.cjs +161 -0
- package/dist/feishu/host-channel-business.mjs +157 -0
- package/dist/feishu/host-processing-eye.cjs +185 -0
- package/dist/feishu/host-processing-eye.mjs +181 -0
- package/dist/feishu/host-shell.cjs +1425 -0
- package/dist/feishu/host-shell.mjs +1385 -0
- package/dist/feishu/im-freshness-adapter.mjs +89 -0
- package/dist/feishu/interaction-orchestrator.cjs +279 -0
- package/dist/feishu/interaction-orchestrator.mjs +242 -0
- package/dist/feishu/lark-passthrough.mjs +70 -0
- package/dist/feishu/message-policy.cjs +111 -0
- package/dist/feishu/message-policy.mjs +98 -0
- package/dist/feishu/outbound-transport.cjs +221 -0
- package/dist/feishu/outbound-transport.mjs +215 -0
- package/dist/platform/build-info.cjs +25 -0
- package/dist/platform/build-info.mjs +18 -0
- package/dist/platform/callback-capability.cjs +301 -0
- package/dist/platform/callback-capability.mjs +252 -0
- package/dist/platform/check-bun-version.cjs +12 -0
- package/dist/platform/config.cjs +751 -0
- package/dist/platform/config.mjs +694 -0
- package/dist/platform/process-inspect.cjs +89 -0
- package/dist/platform/process-state.cjs +237 -0
- package/dist/platform/process-state.mjs +223 -0
- package/dist/platform/release-artifacts.mjs +61 -0
- package/dist/platform/root-layout.cjs +111 -0
- package/dist/platform/root-layout.mjs +72 -0
- package/dist/platform/telemetry-config.cjs +64 -0
- package/dist/platform/telemetry-config.mjs +56 -0
- package/dist/platform/telemetry-spool.cjs +661 -0
- package/dist/platform/telemetry-spool.mjs +653 -0
- package/dist/platform/telemetry-tracing.cjs +602 -0
- package/dist/platform/telemetry-tracing.mjs +596 -0
- package/dist/platform/telemetry-uploader.cjs +150 -0
- package/dist/platform/telemetry-uploader.mjs +146 -0
- package/dist/platform/workspace-service.cjs +469 -0
- package/dist/platform/workspace-service.mjs +430 -0
- package/dist/runtime/builtin-pi-assets.mjs +33 -0
- package/dist/runtime/claude-model-catalog.cjs +102 -0
- package/dist/runtime/claude-model-catalog.mjs +99 -0
- package/dist/runtime/codex-model-catalog.cjs +108 -0
- package/dist/runtime/codex-model-catalog.mjs +105 -0
- package/dist/runtime/pi-bash-timeout-extension.mjs +60 -0
- package/dist/runtime/pi-bash-timeout-injection.mjs +60 -0
- package/dist/runtime/pi-bash-timeout.bundle.js +43 -0
- package/dist/runtime/pi-model-catalog.cjs +116 -0
- package/dist/runtime/pi-model-catalog.mjs +75 -0
- package/dist/runtime/pi-official-auth.mjs +480 -0
- package/dist/runtime/pi-provider-config.cjs +252 -0
- package/dist/runtime/pi-provider-config.mjs +239 -0
- package/dist/runtime/pi-rpc-client.cjs +255 -0
- package/dist/runtime/pi-rpc-client.mjs +251 -0
- package/dist/runtime/pi-subagent-injection.mjs +129 -0
- package/dist/runtime/pi-subagents.bundle.js +8990 -0
- package/dist/runtime/runtime-adapters.mjs +1019 -0
- package/dist/runtime/runtime-contracts.cjs +2 -0
- package/dist/runtime/runtime-contracts.mjs +1 -0
- package/dist/runtime/runtime-host.cjs +930 -0
- package/dist/runtime/runtime-host.mjs +922 -0
- package/dist/runtime/runtime-model-catalog.cjs +43 -0
- package/dist/runtime/runtime-model-catalog.mjs +40 -0
- package/dist/runtime/runtime-readiness.cjs +177 -0
- package/dist/runtime/runtime-readiness.mjs +133 -0
- package/dist/setup/bot-register.mjs +655 -0
- package/dist/setup/grant-scopes.mjs +133 -0
- package/dist/setup/run-credential-preflight.mjs +89 -0
- package/dist/setup/setup-agent-choice.mjs +172 -0
- package/dist/setup/setup-bind.mjs +337 -0
- package/dist/setup/setup-binding.mjs +52 -0
- package/package.json +112 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PI_PROVIDER_PRESETS = exports.BUNDLED_PI_VERSION = void 0;
|
|
7
|
+
exports.validatePiBaseUrl = validatePiBaseUrl;
|
|
8
|
+
exports.resolveBuiltinPiProviderSetupSelection = resolveBuiltinPiProviderSetupSelection;
|
|
9
|
+
exports.validateBuiltinPiProviderSelection = validateBuiltinPiProviderSelection;
|
|
10
|
+
exports.piAgentDirectory = piAgentDirectory;
|
|
11
|
+
exports.assertBuiltinPiAgentDirectory = assertBuiltinPiAgentDirectory;
|
|
12
|
+
exports.configureBuiltinPiProviderModel = configureBuiltinPiProviderModel;
|
|
13
|
+
exports.stageBuiltinPiProvider = stageBuiltinPiProvider;
|
|
14
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
15
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
16
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
17
|
+
exports.BUNDLED_PI_VERSION = "0.83.0";
|
|
18
|
+
// These values mirror the catalog shipped by the pinned official Pi distribution.
|
|
19
|
+
// Keeping the user-facing defaults here makes endpoint changes reviewable in Larkin.
|
|
20
|
+
exports.PI_PROVIDER_PRESETS = Object.freeze([
|
|
21
|
+
{ id: "deepseek", name: "DeepSeek(推荐)", provider: "deepseek", baseUrl: "https://api.deepseek.com", defaultModel: "deepseek-v4-pro", api: "openai-completions" },
|
|
22
|
+
{ id: "kimi", name: "Kimi / Moonshot(中国)", provider: "moonshotai-cn", baseUrl: "https://api.moonshot.cn/v1", defaultModel: "kimi-k2.7-code", api: "openai-completions" },
|
|
23
|
+
{ id: "minimax", name: "MiniMax(中国)", provider: "minimax-cn", baseUrl: "https://api.minimaxi.com/anthropic", defaultModel: "MiniMax-M2.7", api: "anthropic-messages" },
|
|
24
|
+
{ id: "zhipu", name: "智谱 / BigModel", provider: "zai-coding-cn", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", defaultModel: "glm-5.2", api: "openai-completions" },
|
|
25
|
+
]);
|
|
26
|
+
const APP_ID = /^cli_[A-Za-z0-9]+$/;
|
|
27
|
+
const MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._:@+\/-]{0,255}$/;
|
|
28
|
+
function validatePiBaseUrl(raw) {
|
|
29
|
+
const input = raw.trim();
|
|
30
|
+
if (!input || /[\0\r\n\t]/.test(input))
|
|
31
|
+
throw new Error("Base URL 不能为空或包含控制字符");
|
|
32
|
+
let url;
|
|
33
|
+
try {
|
|
34
|
+
url = new URL(input);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new Error("Base URL 不是合法 URL");
|
|
38
|
+
}
|
|
39
|
+
if (url.username || url.password)
|
|
40
|
+
throw new Error("Base URL 不允许包含用户名或凭证");
|
|
41
|
+
if (url.search || url.hash)
|
|
42
|
+
throw new Error("Base URL 不允许 query 或 fragment");
|
|
43
|
+
const local = ["localhost", "127.0.0.1", "::1"].includes(url.hostname);
|
|
44
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
|
|
45
|
+
throw new Error("Base URL 必须使用 https;仅 localhost/loopback 开发端点允许 http");
|
|
46
|
+
}
|
|
47
|
+
if (/\/(?:chat\/completions|responses|models)\/?$/i.test(url.pathname)) {
|
|
48
|
+
throw new Error("Base URL 应填写 API 根路径,不要包含 chat/completions、responses 或 models");
|
|
49
|
+
}
|
|
50
|
+
return url.toString().replace(/\/$/, "");
|
|
51
|
+
}
|
|
52
|
+
function resolveBuiltinPiProviderSetupSelection(input) {
|
|
53
|
+
if (input.distribution !== "builtin")
|
|
54
|
+
throw new Error("provider 凭证只能用于内置 Pi");
|
|
55
|
+
const model = input.model.trim();
|
|
56
|
+
if (!MODEL_ID.test(model))
|
|
57
|
+
throw new Error("模型 ID 格式不安全");
|
|
58
|
+
if (input.preset === "custom") {
|
|
59
|
+
const provider = "larkin-custom";
|
|
60
|
+
return {
|
|
61
|
+
...input,
|
|
62
|
+
model: model.startsWith(`${provider}/`) ? model : `${provider}/${model}`,
|
|
63
|
+
provider,
|
|
64
|
+
baseUrl: validatePiBaseUrl(input.baseUrl || ""),
|
|
65
|
+
api: "openai-completions",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const preset = exports.PI_PROVIDER_PRESETS.find((candidate) => candidate.id === input.preset);
|
|
69
|
+
if (!preset)
|
|
70
|
+
throw new Error(`未知 Pi provider preset: ${String(input.preset)}`);
|
|
71
|
+
return { ...input, model: model.startsWith(`${preset.provider}/`) ? model : `${preset.provider}/${model}`,
|
|
72
|
+
provider: preset.provider, baseUrl: preset.baseUrl, api: preset.api };
|
|
73
|
+
}
|
|
74
|
+
function validateBuiltinPiProviderSelection(input) {
|
|
75
|
+
const key = input.apiKey.trim();
|
|
76
|
+
if (!key || key.length > 16_384 || /[\0\r\n]/.test(key))
|
|
77
|
+
throw new Error("API Key 不能为空或包含控制字符");
|
|
78
|
+
return { ...resolveBuiltinPiProviderSetupSelection(input), apiKey: key };
|
|
79
|
+
}
|
|
80
|
+
function piAgentDirectory(configDir, agentId) {
|
|
81
|
+
if (!APP_ID.test(agentId))
|
|
82
|
+
throw new Error("Pi Agent ID 格式无效");
|
|
83
|
+
return node_path_1.default.join(node_path_1.default.resolve(configDir), "providers", "pi", agentId);
|
|
84
|
+
}
|
|
85
|
+
function assertPrivateDirectory(directory) {
|
|
86
|
+
const stat = node_fs_1.default.lstatSync(directory);
|
|
87
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
88
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
89
|
+
|| (stat.mode & 0o777) !== 0o700) {
|
|
90
|
+
throw new Error("Pi provider 凭证目录必须是当前用户拥有、非 symlink、权限精确 0700 的目录");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function assertBuiltinPiAgentDirectory(directory) {
|
|
94
|
+
assertPrivateDirectory(directory);
|
|
95
|
+
const authFile = node_path_1.default.join(directory, "auth.json");
|
|
96
|
+
const auth = readSnapshot(authFile);
|
|
97
|
+
if (!auth)
|
|
98
|
+
throw new Error("内置 Pi 尚未配置 API Key");
|
|
99
|
+
try {
|
|
100
|
+
const value = JSON.parse(auth.toString("utf8"));
|
|
101
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).length === 0)
|
|
102
|
+
throw new Error("empty");
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
throw new Error("内置 Pi auth.json 格式无效");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function ensurePrivateDirectory(directory) {
|
|
109
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(directory), { recursive: true, mode: 0o700 });
|
|
110
|
+
node_fs_1.default.chmodSync(node_path_1.default.dirname(directory), 0o700);
|
|
111
|
+
node_fs_1.default.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
112
|
+
node_fs_1.default.chmodSync(directory, 0o700);
|
|
113
|
+
assertPrivateDirectory(directory);
|
|
114
|
+
}
|
|
115
|
+
function readSnapshot(file) {
|
|
116
|
+
try {
|
|
117
|
+
const stat = node_fs_1.default.lstatSync(file);
|
|
118
|
+
if (!stat.isFile() || stat.isSymbolicLink()
|
|
119
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
120
|
+
|| (stat.mode & 0o777) !== 0o600)
|
|
121
|
+
throw new Error(`Pi provider 文件不安全: ${node_path_1.default.basename(file)}`);
|
|
122
|
+
return node_fs_1.default.readFileSync(file);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (error.code === "ENOENT")
|
|
126
|
+
return null;
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function atomicPrivateWrite(file, content) {
|
|
131
|
+
const directory = node_path_1.default.dirname(file);
|
|
132
|
+
assertPrivateDirectory(directory);
|
|
133
|
+
const temporary = node_path_1.default.join(directory, `.${node_path_1.default.basename(file)}.${process.pid}.${node_crypto_1.default.randomUUID()}.tmp`);
|
|
134
|
+
try {
|
|
135
|
+
node_fs_1.default.writeFileSync(temporary, content, { mode: 0o600, flag: "wx" });
|
|
136
|
+
node_fs_1.default.renameSync(temporary, file);
|
|
137
|
+
node_fs_1.default.chmodSync(file, 0o600);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
try {
|
|
141
|
+
node_fs_1.default.unlinkSync(temporary);
|
|
142
|
+
}
|
|
143
|
+
catch { /* renamed or absent */ }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function restore(file, snapshot) {
|
|
147
|
+
if (snapshot === null) {
|
|
148
|
+
try {
|
|
149
|
+
node_fs_1.default.unlinkSync(file);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (error.code !== "ENOENT")
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else
|
|
157
|
+
atomicPrivateWrite(file, snapshot.toString("utf8"));
|
|
158
|
+
}
|
|
159
|
+
function configureBuiltinPiProviderModel(configDir, agentId, raw) {
|
|
160
|
+
const selection = resolveBuiltinPiProviderSetupSelection(raw);
|
|
161
|
+
const directory = piAgentDirectory(configDir, agentId);
|
|
162
|
+
ensurePrivateDirectory(directory);
|
|
163
|
+
if (selection.preset !== "custom")
|
|
164
|
+
return selection;
|
|
165
|
+
const modelsFile = node_path_1.default.join(directory, "models.json");
|
|
166
|
+
const beforeModels = readSnapshot(modelsFile);
|
|
167
|
+
const currentModels = beforeModels ? JSON.parse(beforeModels.toString("utf8")) : {};
|
|
168
|
+
if (!currentModels || typeof currentModels !== "object" || Array.isArray(currentModels))
|
|
169
|
+
throw new Error("现有 Pi models.json 格式无效");
|
|
170
|
+
const currentProviders = currentModels.providers === undefined ? {} : currentModels.providers;
|
|
171
|
+
if (!currentProviders || typeof currentProviders !== "object" || Array.isArray(currentProviders))
|
|
172
|
+
throw new Error("现有 Pi models.json providers 格式无效");
|
|
173
|
+
const modelId = selection.model.slice(`${selection.provider}/`.length);
|
|
174
|
+
atomicPrivateWrite(modelsFile, `${JSON.stringify({
|
|
175
|
+
...currentModels,
|
|
176
|
+
providers: {
|
|
177
|
+
...currentProviders,
|
|
178
|
+
[selection.provider]: {
|
|
179
|
+
baseUrl: selection.baseUrl,
|
|
180
|
+
api: selection.api,
|
|
181
|
+
models: [{
|
|
182
|
+
id: modelId, name: modelId, reasoning: false, input: ["text"], contextWindow: 131_072, maxTokens: 16_384,
|
|
183
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
184
|
+
}],
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
}, null, 2)}\n`);
|
|
188
|
+
return selection;
|
|
189
|
+
}
|
|
190
|
+
function stageBuiltinPiProvider(configDir, agentId, raw) {
|
|
191
|
+
const selection = validateBuiltinPiProviderSelection(raw);
|
|
192
|
+
const directory = piAgentDirectory(configDir, agentId);
|
|
193
|
+
ensurePrivateDirectory(directory);
|
|
194
|
+
const authFile = node_path_1.default.join(directory, "auth.json");
|
|
195
|
+
const modelsFile = node_path_1.default.join(directory, "models.json");
|
|
196
|
+
const beforeAuth = readSnapshot(authFile);
|
|
197
|
+
const beforeModels = readSnapshot(modelsFile);
|
|
198
|
+
const currentAuth = beforeAuth ? JSON.parse(beforeAuth.toString("utf8")) : {};
|
|
199
|
+
if (!currentAuth || typeof currentAuth !== "object" || Array.isArray(currentAuth))
|
|
200
|
+
throw new Error("现有 Pi auth.json 格式无效");
|
|
201
|
+
const auth = { ...currentAuth, [selection.provider]: { type: "api_key", key: selection.apiKey } };
|
|
202
|
+
const currentModels = beforeModels ? JSON.parse(beforeModels.toString("utf8")) : {};
|
|
203
|
+
if (!currentModels || typeof currentModels !== "object" || Array.isArray(currentModels))
|
|
204
|
+
throw new Error("现有 Pi models.json 格式无效");
|
|
205
|
+
const currentProviders = currentModels.providers === undefined ? {} : currentModels.providers;
|
|
206
|
+
if (!currentProviders || typeof currentProviders !== "object" || Array.isArray(currentProviders))
|
|
207
|
+
throw new Error("现有 Pi models.json providers 格式无效");
|
|
208
|
+
const customModels = selection.preset === "custom" ? {
|
|
209
|
+
...currentModels,
|
|
210
|
+
providers: {
|
|
211
|
+
...currentProviders,
|
|
212
|
+
[selection.provider]: {
|
|
213
|
+
baseUrl: selection.baseUrl,
|
|
214
|
+
api: selection.api,
|
|
215
|
+
models: [{
|
|
216
|
+
id: selection.model.slice(`${selection.provider}/`.length),
|
|
217
|
+
name: selection.model.slice(`${selection.provider}/`.length),
|
|
218
|
+
reasoning: false,
|
|
219
|
+
input: ["text"],
|
|
220
|
+
contextWindow: 131_072,
|
|
221
|
+
maxTokens: 16_384,
|
|
222
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
223
|
+
}],
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
} : null;
|
|
227
|
+
let active = true;
|
|
228
|
+
try {
|
|
229
|
+
atomicPrivateWrite(authFile, `${JSON.stringify(auth, null, 2)}\n`);
|
|
230
|
+
if (customModels)
|
|
231
|
+
atomicPrivateWrite(modelsFile, `${JSON.stringify(customModels, null, 2)}\n`);
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
try {
|
|
235
|
+
restore(authFile, beforeAuth);
|
|
236
|
+
restore(modelsFile, beforeModels);
|
|
237
|
+
}
|
|
238
|
+
catch { /* original error remains primary */ }
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
directory,
|
|
243
|
+
commit() { active = false; },
|
|
244
|
+
rollback() {
|
|
245
|
+
if (!active)
|
|
246
|
+
return;
|
|
247
|
+
restore(authFile, beforeAuth);
|
|
248
|
+
restore(modelsFile, beforeModels);
|
|
249
|
+
active = false;
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export const BUNDLED_PI_VERSION = "0.83.0";
|
|
5
|
+
// These values mirror the catalog shipped by the pinned official Pi distribution.
|
|
6
|
+
// Keeping the user-facing defaults here makes endpoint changes reviewable in Larkin.
|
|
7
|
+
export const PI_PROVIDER_PRESETS = Object.freeze([
|
|
8
|
+
{ id: "deepseek", name: "DeepSeek(推荐)", provider: "deepseek", baseUrl: "https://api.deepseek.com", defaultModel: "deepseek-v4-pro", api: "openai-completions" },
|
|
9
|
+
{ id: "kimi", name: "Kimi / Moonshot(中国)", provider: "moonshotai-cn", baseUrl: "https://api.moonshot.cn/v1", defaultModel: "kimi-k2.7-code", api: "openai-completions" },
|
|
10
|
+
{ id: "minimax", name: "MiniMax(中国)", provider: "minimax-cn", baseUrl: "https://api.minimaxi.com/anthropic", defaultModel: "MiniMax-M2.7", api: "anthropic-messages" },
|
|
11
|
+
{ id: "zhipu", name: "智谱 / BigModel", provider: "zai-coding-cn", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", defaultModel: "glm-5.2", api: "openai-completions" },
|
|
12
|
+
]);
|
|
13
|
+
const APP_ID = /^cli_[A-Za-z0-9]+$/;
|
|
14
|
+
const MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._:@+\/-]{0,255}$/;
|
|
15
|
+
export function validatePiBaseUrl(raw) {
|
|
16
|
+
const input = raw.trim();
|
|
17
|
+
if (!input || /[\0\r\n\t]/.test(input))
|
|
18
|
+
throw new Error("Base URL 不能为空或包含控制字符");
|
|
19
|
+
let url;
|
|
20
|
+
try {
|
|
21
|
+
url = new URL(input);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new Error("Base URL 不是合法 URL");
|
|
25
|
+
}
|
|
26
|
+
if (url.username || url.password)
|
|
27
|
+
throw new Error("Base URL 不允许包含用户名或凭证");
|
|
28
|
+
if (url.search || url.hash)
|
|
29
|
+
throw new Error("Base URL 不允许 query 或 fragment");
|
|
30
|
+
const local = ["localhost", "127.0.0.1", "::1"].includes(url.hostname);
|
|
31
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
|
|
32
|
+
throw new Error("Base URL 必须使用 https;仅 localhost/loopback 开发端点允许 http");
|
|
33
|
+
}
|
|
34
|
+
if (/\/(?:chat\/completions|responses|models)\/?$/i.test(url.pathname)) {
|
|
35
|
+
throw new Error("Base URL 应填写 API 根路径,不要包含 chat/completions、responses 或 models");
|
|
36
|
+
}
|
|
37
|
+
return url.toString().replace(/\/$/, "");
|
|
38
|
+
}
|
|
39
|
+
export function resolveBuiltinPiProviderSetupSelection(input) {
|
|
40
|
+
if (input.distribution !== "builtin")
|
|
41
|
+
throw new Error("provider 凭证只能用于内置 Pi");
|
|
42
|
+
const model = input.model.trim();
|
|
43
|
+
if (!MODEL_ID.test(model))
|
|
44
|
+
throw new Error("模型 ID 格式不安全");
|
|
45
|
+
if (input.preset === "custom") {
|
|
46
|
+
const provider = "larkin-custom";
|
|
47
|
+
return {
|
|
48
|
+
...input,
|
|
49
|
+
model: model.startsWith(`${provider}/`) ? model : `${provider}/${model}`,
|
|
50
|
+
provider,
|
|
51
|
+
baseUrl: validatePiBaseUrl(input.baseUrl || ""),
|
|
52
|
+
api: "openai-completions",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const preset = PI_PROVIDER_PRESETS.find((candidate) => candidate.id === input.preset);
|
|
56
|
+
if (!preset)
|
|
57
|
+
throw new Error(`未知 Pi provider preset: ${String(input.preset)}`);
|
|
58
|
+
return { ...input, model: model.startsWith(`${preset.provider}/`) ? model : `${preset.provider}/${model}`,
|
|
59
|
+
provider: preset.provider, baseUrl: preset.baseUrl, api: preset.api };
|
|
60
|
+
}
|
|
61
|
+
export function validateBuiltinPiProviderSelection(input) {
|
|
62
|
+
const key = input.apiKey.trim();
|
|
63
|
+
if (!key || key.length > 16_384 || /[\0\r\n]/.test(key))
|
|
64
|
+
throw new Error("API Key 不能为空或包含控制字符");
|
|
65
|
+
return { ...resolveBuiltinPiProviderSetupSelection(input), apiKey: key };
|
|
66
|
+
}
|
|
67
|
+
export function piAgentDirectory(configDir, agentId) {
|
|
68
|
+
if (!APP_ID.test(agentId))
|
|
69
|
+
throw new Error("Pi Agent ID 格式无效");
|
|
70
|
+
return path.join(path.resolve(configDir), "providers", "pi", agentId);
|
|
71
|
+
}
|
|
72
|
+
function assertPrivateDirectory(directory) {
|
|
73
|
+
const stat = fs.lstatSync(directory);
|
|
74
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
75
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
76
|
+
|| (stat.mode & 0o777) !== 0o700) {
|
|
77
|
+
throw new Error("Pi provider 凭证目录必须是当前用户拥有、非 symlink、权限精确 0700 的目录");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function assertBuiltinPiAgentDirectory(directory) {
|
|
81
|
+
assertPrivateDirectory(directory);
|
|
82
|
+
const authFile = path.join(directory, "auth.json");
|
|
83
|
+
const auth = readSnapshot(authFile);
|
|
84
|
+
if (!auth)
|
|
85
|
+
throw new Error("内置 Pi 尚未配置 API Key");
|
|
86
|
+
try {
|
|
87
|
+
const value = JSON.parse(auth.toString("utf8"));
|
|
88
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).length === 0)
|
|
89
|
+
throw new Error("empty");
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
throw new Error("内置 Pi auth.json 格式无效");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function ensurePrivateDirectory(directory) {
|
|
96
|
+
fs.mkdirSync(path.dirname(directory), { recursive: true, mode: 0o700 });
|
|
97
|
+
fs.chmodSync(path.dirname(directory), 0o700);
|
|
98
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
99
|
+
fs.chmodSync(directory, 0o700);
|
|
100
|
+
assertPrivateDirectory(directory);
|
|
101
|
+
}
|
|
102
|
+
function readSnapshot(file) {
|
|
103
|
+
try {
|
|
104
|
+
const stat = fs.lstatSync(file);
|
|
105
|
+
if (!stat.isFile() || stat.isSymbolicLink()
|
|
106
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
107
|
+
|| (stat.mode & 0o777) !== 0o600)
|
|
108
|
+
throw new Error(`Pi provider 文件不安全: ${path.basename(file)}`);
|
|
109
|
+
return fs.readFileSync(file);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (error.code === "ENOENT")
|
|
113
|
+
return null;
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function atomicPrivateWrite(file, content) {
|
|
118
|
+
const directory = path.dirname(file);
|
|
119
|
+
assertPrivateDirectory(directory);
|
|
120
|
+
const temporary = path.join(directory, `.${path.basename(file)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
121
|
+
try {
|
|
122
|
+
fs.writeFileSync(temporary, content, { mode: 0o600, flag: "wx" });
|
|
123
|
+
fs.renameSync(temporary, file);
|
|
124
|
+
fs.chmodSync(file, 0o600);
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
try {
|
|
128
|
+
fs.unlinkSync(temporary);
|
|
129
|
+
}
|
|
130
|
+
catch { /* renamed or absent */ }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function restore(file, snapshot) {
|
|
134
|
+
if (snapshot === null) {
|
|
135
|
+
try {
|
|
136
|
+
fs.unlinkSync(file);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (error.code !== "ENOENT")
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else
|
|
144
|
+
atomicPrivateWrite(file, snapshot.toString("utf8"));
|
|
145
|
+
}
|
|
146
|
+
export function configureBuiltinPiProviderModel(configDir, agentId, raw) {
|
|
147
|
+
const selection = resolveBuiltinPiProviderSetupSelection(raw);
|
|
148
|
+
const directory = piAgentDirectory(configDir, agentId);
|
|
149
|
+
ensurePrivateDirectory(directory);
|
|
150
|
+
if (selection.preset !== "custom")
|
|
151
|
+
return selection;
|
|
152
|
+
const modelsFile = path.join(directory, "models.json");
|
|
153
|
+
const beforeModels = readSnapshot(modelsFile);
|
|
154
|
+
const currentModels = beforeModels ? JSON.parse(beforeModels.toString("utf8")) : {};
|
|
155
|
+
if (!currentModels || typeof currentModels !== "object" || Array.isArray(currentModels))
|
|
156
|
+
throw new Error("现有 Pi models.json 格式无效");
|
|
157
|
+
const currentProviders = currentModels.providers === undefined ? {} : currentModels.providers;
|
|
158
|
+
if (!currentProviders || typeof currentProviders !== "object" || Array.isArray(currentProviders))
|
|
159
|
+
throw new Error("现有 Pi models.json providers 格式无效");
|
|
160
|
+
const modelId = selection.model.slice(`${selection.provider}/`.length);
|
|
161
|
+
atomicPrivateWrite(modelsFile, `${JSON.stringify({
|
|
162
|
+
...currentModels,
|
|
163
|
+
providers: {
|
|
164
|
+
...currentProviders,
|
|
165
|
+
[selection.provider]: {
|
|
166
|
+
baseUrl: selection.baseUrl,
|
|
167
|
+
api: selection.api,
|
|
168
|
+
models: [{
|
|
169
|
+
id: modelId, name: modelId, reasoning: false, input: ["text"], contextWindow: 131_072, maxTokens: 16_384,
|
|
170
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
171
|
+
}],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
}, null, 2)}\n`);
|
|
175
|
+
return selection;
|
|
176
|
+
}
|
|
177
|
+
export function stageBuiltinPiProvider(configDir, agentId, raw) {
|
|
178
|
+
const selection = validateBuiltinPiProviderSelection(raw);
|
|
179
|
+
const directory = piAgentDirectory(configDir, agentId);
|
|
180
|
+
ensurePrivateDirectory(directory);
|
|
181
|
+
const authFile = path.join(directory, "auth.json");
|
|
182
|
+
const modelsFile = path.join(directory, "models.json");
|
|
183
|
+
const beforeAuth = readSnapshot(authFile);
|
|
184
|
+
const beforeModels = readSnapshot(modelsFile);
|
|
185
|
+
const currentAuth = beforeAuth ? JSON.parse(beforeAuth.toString("utf8")) : {};
|
|
186
|
+
if (!currentAuth || typeof currentAuth !== "object" || Array.isArray(currentAuth))
|
|
187
|
+
throw new Error("现有 Pi auth.json 格式无效");
|
|
188
|
+
const auth = { ...currentAuth, [selection.provider]: { type: "api_key", key: selection.apiKey } };
|
|
189
|
+
const currentModels = beforeModels ? JSON.parse(beforeModels.toString("utf8")) : {};
|
|
190
|
+
if (!currentModels || typeof currentModels !== "object" || Array.isArray(currentModels))
|
|
191
|
+
throw new Error("现有 Pi models.json 格式无效");
|
|
192
|
+
const currentProviders = currentModels.providers === undefined ? {} : currentModels.providers;
|
|
193
|
+
if (!currentProviders || typeof currentProviders !== "object" || Array.isArray(currentProviders))
|
|
194
|
+
throw new Error("现有 Pi models.json providers 格式无效");
|
|
195
|
+
const customModels = selection.preset === "custom" ? {
|
|
196
|
+
...currentModels,
|
|
197
|
+
providers: {
|
|
198
|
+
...currentProviders,
|
|
199
|
+
[selection.provider]: {
|
|
200
|
+
baseUrl: selection.baseUrl,
|
|
201
|
+
api: selection.api,
|
|
202
|
+
models: [{
|
|
203
|
+
id: selection.model.slice(`${selection.provider}/`.length),
|
|
204
|
+
name: selection.model.slice(`${selection.provider}/`.length),
|
|
205
|
+
reasoning: false,
|
|
206
|
+
input: ["text"],
|
|
207
|
+
contextWindow: 131_072,
|
|
208
|
+
maxTokens: 16_384,
|
|
209
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
210
|
+
}],
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
} : null;
|
|
214
|
+
let active = true;
|
|
215
|
+
try {
|
|
216
|
+
atomicPrivateWrite(authFile, `${JSON.stringify(auth, null, 2)}\n`);
|
|
217
|
+
if (customModels)
|
|
218
|
+
atomicPrivateWrite(modelsFile, `${JSON.stringify(customModels, null, 2)}\n`);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
try {
|
|
222
|
+
restore(authFile, beforeAuth);
|
|
223
|
+
restore(modelsFile, beforeModels);
|
|
224
|
+
}
|
|
225
|
+
catch { /* original error remains primary */ }
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
directory,
|
|
230
|
+
commit() { active = false; },
|
|
231
|
+
rollback() {
|
|
232
|
+
if (!active)
|
|
233
|
+
return;
|
|
234
|
+
restore(authFile, beforeAuth);
|
|
235
|
+
restore(modelsFile, beforeModels);
|
|
236
|
+
active = false;
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|