larkin 0.4.15 → 0.4.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app/setup.mjs
CHANGED
|
@@ -190,8 +190,9 @@ export async function main() {
|
|
|
190
190
|
die(`setup 完成但 Agent ${selectedAgentId} 配置不可读`);
|
|
191
191
|
if (!["codex", "claude", "pi"].includes(configuredAgent.runtime))
|
|
192
192
|
die(`Runtime ${configuredAgent.runtime} 不受支持`);
|
|
193
|
-
// 非交互(Agent
|
|
194
|
-
//
|
|
193
|
+
// 非交互(Agent 驱动)时仍跳过 probeNativeRuntimeReadiness:该探测保持 interactive-only。
|
|
194
|
+
// external-pi 绑定已在 setup-bind 内做非交互 Pi RPC catalog discovery(RPC over pipes 不需要 TTY);
|
|
195
|
+
// daemon attach 探测仍是权威校验。其余真实阻塞(缺 runtime 可执行文件 / 缺 key / 缺 lark-cli)一律 fast-fail。
|
|
195
196
|
if (!OPT.nonInteractive) {
|
|
196
197
|
const runtimeReadiness = await probeNativeRuntimeReadiness({ runtime: configuredAgent.runtime,
|
|
197
198
|
agentId: selectedAgentId, cwd: configuredAgent.workspaceDir,
|
|
@@ -11,7 +11,9 @@ export function isAllowedDashboardAvatarUrl(value) {
|
|
|
11
11
|
&& url.username === ""
|
|
12
12
|
&& url.password === ""
|
|
13
13
|
&& (url.port === "" || url.port === "443")
|
|
14
|
-
|
|
14
|
+
// Feishu: s1-imfile.feishucdn.com
|
|
15
|
+
// Lark regional: s16-imfile-sg.feishucdn.com
|
|
16
|
+
&& /^s\d+-imfile(?:-[a-z]{2,3})?\.feishucdn\.com$/i.test(url.hostname)
|
|
15
17
|
&& url.pathname.startsWith("/static-resource/v1/");
|
|
16
18
|
}
|
|
17
19
|
catch {
|
|
@@ -32,8 +32,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
35
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
39
|
exports.releasePiProfileMigrationLock = releasePiProfileMigrationLock;
|
|
40
|
+
exports.clearStalePiProfileMigrationLock = clearStalePiProfileMigrationLock;
|
|
37
41
|
exports.preparePiProfileMigration = preparePiProfileMigration;
|
|
38
42
|
exports.validatePiProfileMigrationState = validatePiProfileMigrationState;
|
|
39
43
|
exports.assertPiProfileMigrationAfterState = assertPiProfileMigrationAfterState;
|
|
@@ -44,6 +48,7 @@ const fs = __importStar(require("node:fs"));
|
|
|
44
48
|
const os = __importStar(require("node:os"));
|
|
45
49
|
const path = __importStar(require("node:path"));
|
|
46
50
|
const node_child_process_1 = require("node:child_process");
|
|
51
|
+
const process_inspect_cjs_1 = __importDefault(require("../platform/process-inspect.cjs"));
|
|
47
52
|
const runtime_readiness_js_1 = require("./runtime-readiness.cjs");
|
|
48
53
|
const pi_compaction_recovery_js_1 = require("./pi-compaction-recovery.cjs");
|
|
49
54
|
const pi_provider_config_js_1 = require("./pi-provider-config.cjs");
|
|
@@ -87,6 +92,109 @@ function releasePiProfileMigrationLock(state) {
|
|
|
87
92
|
throw error;
|
|
88
93
|
}
|
|
89
94
|
}
|
|
95
|
+
const TARGET_BUSY = "Pi provider target is busy";
|
|
96
|
+
function migrationLockPath(configDir, agentId) {
|
|
97
|
+
if (!AGENT_ID.test(agentId))
|
|
98
|
+
throw new Error("Pi Agent ID 格式无效");
|
|
99
|
+
return path.join(path.resolve(configDir), "providers", "pi", `${agentId}${TARGET_LOCK_SUFFIX}`);
|
|
100
|
+
}
|
|
101
|
+
function lockPidFromBytes(bytes) {
|
|
102
|
+
const text = bytes.toString("utf8");
|
|
103
|
+
if (text.endsWith("\n") ? text.slice(0, -1).includes("\n") : text.includes("\n"))
|
|
104
|
+
return null;
|
|
105
|
+
const line = text.endsWith("\n") ? text.slice(0, -1) : text;
|
|
106
|
+
if (!/^[1-9][0-9]*$/.test(line))
|
|
107
|
+
return null;
|
|
108
|
+
const pid = Number(line);
|
|
109
|
+
return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
|
|
110
|
+
}
|
|
111
|
+
function inspectOwnedLock(file) {
|
|
112
|
+
let fd;
|
|
113
|
+
try {
|
|
114
|
+
const initial = fs.lstatSync(file);
|
|
115
|
+
if (initial.isSymbolicLink() || !initial.isFile() || initial.nlink !== 1 || (initial.mode & 0o777) !== 0o600) {
|
|
116
|
+
throw new Error(TARGET_BUSY);
|
|
117
|
+
}
|
|
118
|
+
assertOwner(initial, "Pi provider lock");
|
|
119
|
+
const noFollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
120
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
|
|
121
|
+
const before = fs.fstatSync(fd);
|
|
122
|
+
if (!before.isFile() || before.nlink !== 1 || (before.mode & 0o777) !== 0o600)
|
|
123
|
+
throw new Error(TARGET_BUSY);
|
|
124
|
+
assertOwner(before, "Pi provider lock");
|
|
125
|
+
const bytes = Buffer.allocUnsafe(before.size);
|
|
126
|
+
let offset = 0;
|
|
127
|
+
while (offset < before.size) {
|
|
128
|
+
const read = fs.readSync(fd, bytes, offset, before.size - offset, null);
|
|
129
|
+
if (read === 0)
|
|
130
|
+
throw new Error(TARGET_BUSY);
|
|
131
|
+
offset += read;
|
|
132
|
+
}
|
|
133
|
+
const after = fs.fstatSync(fd);
|
|
134
|
+
if (after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size || after.nlink !== before.nlink
|
|
135
|
+
|| after.uid !== before.uid || (after.mode & 0o777) !== 0o600)
|
|
136
|
+
throw new Error(TARGET_BUSY);
|
|
137
|
+
const pid = lockPidFromBytes(bytes.subarray(0, after.size));
|
|
138
|
+
if (pid === null)
|
|
139
|
+
throw new Error(TARGET_BUSY);
|
|
140
|
+
return { pid, dev: after.dev, ino: after.ino };
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (error.code === "ENOENT")
|
|
144
|
+
return null;
|
|
145
|
+
if (error.code === "ELOOP")
|
|
146
|
+
throw new Error(TARGET_BUSY);
|
|
147
|
+
if (error instanceof Error && error.message === TARGET_BUSY)
|
|
148
|
+
throw error;
|
|
149
|
+
if (error instanceof Error && /unsafe/.test(error.message))
|
|
150
|
+
throw new Error(TARGET_BUSY);
|
|
151
|
+
throw new Error(TARGET_BUSY);
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
if (fd !== undefined)
|
|
155
|
+
fs.closeSync(fd);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Reclaim a credential-adjacent import lock only when it is a proven-dead owned regular file. */
|
|
159
|
+
function clearStalePiProfileMigrationLock(configDir, agentId, options = {}) {
|
|
160
|
+
const file = migrationLockPath(configDir, agentId);
|
|
161
|
+
assertNoSymlinkAncestors(path.dirname(file));
|
|
162
|
+
const inspected = inspectOwnedLock(file);
|
|
163
|
+
if (!inspected)
|
|
164
|
+
return;
|
|
165
|
+
const kill = options.kill ?? process.kill;
|
|
166
|
+
if (process_inspect_cjs_1.default.pidAlive(inspected.pid, kill))
|
|
167
|
+
throw new Error(TARGET_BUSY);
|
|
168
|
+
const quarantine = `${file}.stale-${process.pid}-${crypto.randomUUID()}`;
|
|
169
|
+
try {
|
|
170
|
+
fs.renameSync(file, quarantine);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (error.code === "ENOENT")
|
|
174
|
+
return;
|
|
175
|
+
throw new Error(TARGET_BUSY);
|
|
176
|
+
}
|
|
177
|
+
fsyncDirectory(path.dirname(file));
|
|
178
|
+
try {
|
|
179
|
+
const quarantined = fs.lstatSync(quarantine);
|
|
180
|
+
if (quarantined.isSymbolicLink() || !quarantined.isFile() || quarantined.nlink !== 1
|
|
181
|
+
|| (quarantined.mode & 0o777) !== 0o600 || quarantined.dev !== inspected.dev || quarantined.ino !== inspected.ino) {
|
|
182
|
+
throw new Error(TARGET_BUSY);
|
|
183
|
+
}
|
|
184
|
+
assertOwner(quarantined, "Pi provider lock");
|
|
185
|
+
const bytes = fs.readFileSync(quarantine);
|
|
186
|
+
if (lockPidFromBytes(bytes) !== inspected.pid || process_inspect_cjs_1.default.pidAlive(inspected.pid, kill)) {
|
|
187
|
+
throw new Error(TARGET_BUSY);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
try {
|
|
192
|
+
fs.unlinkSync(quarantine);
|
|
193
|
+
}
|
|
194
|
+
catch { /* quarantine is not the live lock path */ }
|
|
195
|
+
fsyncDirectory(path.dirname(file));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
90
198
|
function isKnownSystemSymlink(value) {
|
|
91
199
|
return value === "/tmp" || value === "/private/tmp" || value === "/var" || value === "/private/var"
|
|
92
200
|
|| value === "/var/folders" || /^\/var\/folders\/[^/]+$/.test(value)
|
|
@@ -263,7 +371,7 @@ function preparePiProfileMigration(env, configDir, agentId, distribution = "buil
|
|
|
263
371
|
const sourceDirectoryState = assertDirectory(sourceDir, "external Pi profile");
|
|
264
372
|
const sourceExecutable = sourceVersion(env, sourceDir);
|
|
265
373
|
const sourceFiles = Object.fromEntries(FILES.map((name) => {
|
|
266
|
-
const file = readRegular(path.join(sourceDir, name), `external Pi ${name}
|
|
374
|
+
const file = readRegular(path.join(sourceDir, name), `external Pi ${name}`, false);
|
|
267
375
|
if (!file)
|
|
268
376
|
throw new Error(`external Pi ${name} is required`);
|
|
269
377
|
if (name === "auth.json" && file.mode !== 0o600)
|
|
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
|
+
import processInspect from "../platform/process-inspect.cjs";
|
|
6
7
|
import { resolveRuntimeExecutable } from "./runtime-readiness.mjs";
|
|
7
8
|
import { mergeOwnedPiSettings, parsePiExecutableVersion } from "./pi-compaction-recovery.mjs";
|
|
8
9
|
import { BUNDLED_PI_VERSION } from "./pi-provider-config.mjs";
|
|
@@ -46,6 +47,109 @@ export function releasePiProfileMigrationLock(state) {
|
|
|
46
47
|
throw error;
|
|
47
48
|
}
|
|
48
49
|
}
|
|
50
|
+
const TARGET_BUSY = "Pi provider target is busy";
|
|
51
|
+
function migrationLockPath(configDir, agentId) {
|
|
52
|
+
if (!AGENT_ID.test(agentId))
|
|
53
|
+
throw new Error("Pi Agent ID 格式无效");
|
|
54
|
+
return path.join(path.resolve(configDir), "providers", "pi", `${agentId}${TARGET_LOCK_SUFFIX}`);
|
|
55
|
+
}
|
|
56
|
+
function lockPidFromBytes(bytes) {
|
|
57
|
+
const text = bytes.toString("utf8");
|
|
58
|
+
if (text.endsWith("\n") ? text.slice(0, -1).includes("\n") : text.includes("\n"))
|
|
59
|
+
return null;
|
|
60
|
+
const line = text.endsWith("\n") ? text.slice(0, -1) : text;
|
|
61
|
+
if (!/^[1-9][0-9]*$/.test(line))
|
|
62
|
+
return null;
|
|
63
|
+
const pid = Number(line);
|
|
64
|
+
return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
|
|
65
|
+
}
|
|
66
|
+
function inspectOwnedLock(file) {
|
|
67
|
+
let fd;
|
|
68
|
+
try {
|
|
69
|
+
const initial = fs.lstatSync(file);
|
|
70
|
+
if (initial.isSymbolicLink() || !initial.isFile() || initial.nlink !== 1 || (initial.mode & 0o777) !== 0o600) {
|
|
71
|
+
throw new Error(TARGET_BUSY);
|
|
72
|
+
}
|
|
73
|
+
assertOwner(initial, "Pi provider lock");
|
|
74
|
+
const noFollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
75
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
|
|
76
|
+
const before = fs.fstatSync(fd);
|
|
77
|
+
if (!before.isFile() || before.nlink !== 1 || (before.mode & 0o777) !== 0o600)
|
|
78
|
+
throw new Error(TARGET_BUSY);
|
|
79
|
+
assertOwner(before, "Pi provider lock");
|
|
80
|
+
const bytes = Buffer.allocUnsafe(before.size);
|
|
81
|
+
let offset = 0;
|
|
82
|
+
while (offset < before.size) {
|
|
83
|
+
const read = fs.readSync(fd, bytes, offset, before.size - offset, null);
|
|
84
|
+
if (read === 0)
|
|
85
|
+
throw new Error(TARGET_BUSY);
|
|
86
|
+
offset += read;
|
|
87
|
+
}
|
|
88
|
+
const after = fs.fstatSync(fd);
|
|
89
|
+
if (after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size || after.nlink !== before.nlink
|
|
90
|
+
|| after.uid !== before.uid || (after.mode & 0o777) !== 0o600)
|
|
91
|
+
throw new Error(TARGET_BUSY);
|
|
92
|
+
const pid = lockPidFromBytes(bytes.subarray(0, after.size));
|
|
93
|
+
if (pid === null)
|
|
94
|
+
throw new Error(TARGET_BUSY);
|
|
95
|
+
return { pid, dev: after.dev, ino: after.ino };
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error.code === "ENOENT")
|
|
99
|
+
return null;
|
|
100
|
+
if (error.code === "ELOOP")
|
|
101
|
+
throw new Error(TARGET_BUSY);
|
|
102
|
+
if (error instanceof Error && error.message === TARGET_BUSY)
|
|
103
|
+
throw error;
|
|
104
|
+
if (error instanceof Error && /unsafe/.test(error.message))
|
|
105
|
+
throw new Error(TARGET_BUSY);
|
|
106
|
+
throw new Error(TARGET_BUSY);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
if (fd !== undefined)
|
|
110
|
+
fs.closeSync(fd);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Reclaim a credential-adjacent import lock only when it is a proven-dead owned regular file. */
|
|
114
|
+
export function clearStalePiProfileMigrationLock(configDir, agentId, options = {}) {
|
|
115
|
+
const file = migrationLockPath(configDir, agentId);
|
|
116
|
+
assertNoSymlinkAncestors(path.dirname(file));
|
|
117
|
+
const inspected = inspectOwnedLock(file);
|
|
118
|
+
if (!inspected)
|
|
119
|
+
return;
|
|
120
|
+
const kill = options.kill ?? process.kill;
|
|
121
|
+
if (processInspect.pidAlive(inspected.pid, kill))
|
|
122
|
+
throw new Error(TARGET_BUSY);
|
|
123
|
+
const quarantine = `${file}.stale-${process.pid}-${crypto.randomUUID()}`;
|
|
124
|
+
try {
|
|
125
|
+
fs.renameSync(file, quarantine);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (error.code === "ENOENT")
|
|
129
|
+
return;
|
|
130
|
+
throw new Error(TARGET_BUSY);
|
|
131
|
+
}
|
|
132
|
+
fsyncDirectory(path.dirname(file));
|
|
133
|
+
try {
|
|
134
|
+
const quarantined = fs.lstatSync(quarantine);
|
|
135
|
+
if (quarantined.isSymbolicLink() || !quarantined.isFile() || quarantined.nlink !== 1
|
|
136
|
+
|| (quarantined.mode & 0o777) !== 0o600 || quarantined.dev !== inspected.dev || quarantined.ino !== inspected.ino) {
|
|
137
|
+
throw new Error(TARGET_BUSY);
|
|
138
|
+
}
|
|
139
|
+
assertOwner(quarantined, "Pi provider lock");
|
|
140
|
+
const bytes = fs.readFileSync(quarantine);
|
|
141
|
+
if (lockPidFromBytes(bytes) !== inspected.pid || processInspect.pidAlive(inspected.pid, kill)) {
|
|
142
|
+
throw new Error(TARGET_BUSY);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
try {
|
|
147
|
+
fs.unlinkSync(quarantine);
|
|
148
|
+
}
|
|
149
|
+
catch { /* quarantine is not the live lock path */ }
|
|
150
|
+
fsyncDirectory(path.dirname(file));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
49
153
|
function isKnownSystemSymlink(value) {
|
|
50
154
|
return value === "/tmp" || value === "/private/tmp" || value === "/var" || value === "/private/var"
|
|
51
155
|
|| value === "/var/folders" || /^\/var\/folders\/[^/]+$/.test(value)
|
|
@@ -222,7 +326,7 @@ export function preparePiProfileMigration(env, configDir, agentId, distribution
|
|
|
222
326
|
const sourceDirectoryState = assertDirectory(sourceDir, "external Pi profile");
|
|
223
327
|
const sourceExecutable = sourceVersion(env, sourceDir);
|
|
224
328
|
const sourceFiles = Object.fromEntries(FILES.map((name) => {
|
|
225
|
-
const file = readRegular(path.join(sourceDir, name), `external Pi ${name}
|
|
329
|
+
const file = readRegular(path.join(sourceDir, name), `external Pi ${name}`, false);
|
|
226
330
|
if (!file)
|
|
227
331
|
throw new Error(`external Pi ${name} is required`);
|
|
228
332
|
if (name === "auth.json" && file.mode !== 0o600)
|
|
@@ -9,7 +9,10 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
import { isWindows } from "../platform/secure-metadata.mjs";
|
|
10
10
|
import { TargetRootLayout } from "../platform/root-layout.mjs";
|
|
11
11
|
import { planSingleRootBinding } from "./setup-binding.mjs";
|
|
12
|
-
import { configureBuiltinPiProviderModel } from "../runtime/pi-provider-config.mjs";
|
|
12
|
+
import { configureBuiltinPiProviderModel, piAgentDirectory } from "../runtime/pi-provider-config.mjs";
|
|
13
|
+
import { applyPiProfileMigration, clearStalePiProfileMigrationLock, preparePiProfileMigration, releasePiProfileMigrationLock, rollbackPiProfileMigration, } from "../runtime/pi-profile-migration.mjs";
|
|
14
|
+
import { discoverPiModelCatalog } from "../runtime/pi-model-catalog.mjs";
|
|
15
|
+
import { calculatePiCompactionSettings } from "../runtime/pi-compaction-recovery.mjs";
|
|
13
16
|
import { terminalSetupQuestioner } from "./setup-agent-choice.mjs";
|
|
14
17
|
import { beginBuiltinPiCredentialTransaction, createOfficialPiAuthInteraction, createOfficialPiModelRuntime, runOfficialPiLogin, verifyOfficialPiProviderTurn, } from "../runtime/pi-official-auth.mjs";
|
|
15
18
|
import * as larkinConfigImport from "../platform/config.mjs";
|
|
@@ -195,6 +198,30 @@ function fabricateAttachment(root, serverId) {
|
|
|
195
198
|
};
|
|
196
199
|
fs.writeFileSync(file, JSON.stringify(attachment, null, 2), { mode: 0o600 });
|
|
197
200
|
}
|
|
201
|
+
function catalogModelsFromError(error) {
|
|
202
|
+
if (!error || typeof error !== "object" || !("catalogModels" in error))
|
|
203
|
+
return [];
|
|
204
|
+
const models = error.catalogModels;
|
|
205
|
+
return Array.isArray(models) ? models.filter((id) => typeof id === "string" && id.length > 0) : [];
|
|
206
|
+
}
|
|
207
|
+
function formatExternalPiSetupFailure(error, options) {
|
|
208
|
+
const original = (error instanceof Error ? error.message : String(error)).replace(/[\r\n]+/g, " ").trim();
|
|
209
|
+
const alternatives = catalogModelsFromError(error);
|
|
210
|
+
const parts = [];
|
|
211
|
+
if (/external Pi (?:auth\.json|models\.json|settings\.json) is required|external Pi profile|external Pi auth\.json has an unsafe mode|external Pi models\.json has an unsafe mode|external Pi settings\.json has an unsafe mode/.test(original)) {
|
|
212
|
+
parts.push("external-pi requires the official file-backed Pi profile containing auth.json (0600), models.json, and settings.json, normally after `pi` login and default-model selection. Environment-variable-only credentials are unsupported.");
|
|
213
|
+
}
|
|
214
|
+
if (original)
|
|
215
|
+
parts.push(original);
|
|
216
|
+
parts.push("Fix the official `pi` login and default model, then rerun `larkin setup --runtime external-pi`.");
|
|
217
|
+
if (options.agentExisted) {
|
|
218
|
+
parts.push("This Agent already exists; after a successful setup you can switch models with `larkin model <provider>/<model>`.");
|
|
219
|
+
}
|
|
220
|
+
if (alternatives.length) {
|
|
221
|
+
parts.push(`Alternative models that report a context window: ${alternatives.join(", ")}.`);
|
|
222
|
+
}
|
|
223
|
+
return parts.join(" ");
|
|
224
|
+
}
|
|
198
225
|
function safeProviderFailure(error) {
|
|
199
226
|
const message = error instanceof Error ? error.message : String(error);
|
|
200
227
|
if (/\b(?:401|403)\b|unauth|invalid.*(?:key|token)|credential/i.test(message)) {
|
|
@@ -267,49 +294,90 @@ export async function main() {
|
|
|
267
294
|
validateRuntime(runtime);
|
|
268
295
|
const defaultModel = larkinConfig.defaultModelFor(runtime);
|
|
269
296
|
const selectedModel = selection?.runtime === "pi" && selection.distribution === "builtin" ? selection.model : undefined;
|
|
270
|
-
|
|
271
|
-
let
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
? [prior.effort]
|
|
275
|
-
: [];
|
|
276
|
-
runtimeModels = [{ id: targetModelId, supportedReasoningEfforts: preservedEffort }];
|
|
277
|
-
}
|
|
278
|
-
const targetModel = runtimeModels.find((model) => model.id === targetModelId);
|
|
279
|
-
if (!targetModel)
|
|
280
|
-
die(`目标模型 ${runtime}/${targetModelId} 不在 runtime 模型目录中;未修改 Agent 配置`);
|
|
281
|
-
const stored = planSingleRootBinding({
|
|
282
|
-
config: larkinConfig.toStored(config),
|
|
283
|
-
profile,
|
|
284
|
-
requestedAgent,
|
|
285
|
-
runtime: selection?.runtime || options.runtime,
|
|
286
|
-
...(runtime === "pi" && piDistribution ? { piDistribution } : {}),
|
|
287
|
-
...(selectedModel ? { model: selectedModel } : {}),
|
|
288
|
-
defaultModel,
|
|
289
|
-
supportedReasoningEfforts: targetModel.supportedReasoningEfforts || [],
|
|
290
|
-
now: new Date().toISOString(),
|
|
291
|
-
});
|
|
292
|
-
const bound = stored.agents[profile.appId];
|
|
293
|
-
fs.mkdirSync(CFG_DIR, { recursive: true });
|
|
297
|
+
let targetModelId = selectedModel || (prior?.runtime === runtime ? prior.model : defaultModel);
|
|
298
|
+
let resolvedExternalModel;
|
|
299
|
+
let pendingMigration = null;
|
|
300
|
+
let setupConfigCommitted = false;
|
|
294
301
|
let providerTransaction = null;
|
|
295
302
|
let providerPublished = false;
|
|
296
|
-
const authAlreadyCompleted = selection?.runtime === "pi" && selection.distribution === "builtin"
|
|
297
|
-
&& selection.authCompleted === true;
|
|
298
|
-
const readinessAlreadyCompleted = authAlreadyCompleted
|
|
299
|
-
&& selection.readinessCompleted === true;
|
|
300
303
|
const authAbort = new AbortController();
|
|
301
|
-
const
|
|
304
|
+
const cancelOnSignal = (signal) => {
|
|
302
305
|
authAbort.abort();
|
|
306
|
+
if (!setupConfigCommitted && pendingMigration) {
|
|
307
|
+
try {
|
|
308
|
+
rollbackPiProfileMigration(pendingMigration.state);
|
|
309
|
+
}
|
|
310
|
+
catch { /* best effort */ }
|
|
311
|
+
}
|
|
303
312
|
if (!providerPublished)
|
|
304
313
|
providerTransaction?.rollback();
|
|
305
314
|
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
306
315
|
};
|
|
307
|
-
const onSigint = () =>
|
|
308
|
-
const onSigterm = () =>
|
|
316
|
+
const onSigint = () => cancelOnSignal("SIGINT");
|
|
317
|
+
const onSigterm = () => cancelOnSignal("SIGTERM");
|
|
309
318
|
process.once("SIGINT", onSigint);
|
|
310
319
|
process.once("SIGTERM", onSigterm);
|
|
320
|
+
let runtimeModels = larkinConfig.loadRuntimeModels()[runtime];
|
|
311
321
|
let authQuestioner = null;
|
|
312
322
|
try {
|
|
323
|
+
if (runtime === "pi" && piDistribution === "external" && targetModelId === "default") {
|
|
324
|
+
fs.mkdirSync(CFG_DIR, { recursive: true, mode: 0o700 });
|
|
325
|
+
try {
|
|
326
|
+
clearStalePiProfileMigrationLock(CFG_DIR, profile.appId);
|
|
327
|
+
const plan = preparePiProfileMigration(process.env, CFG_DIR, profile.appId, "external");
|
|
328
|
+
pendingMigration = plan;
|
|
329
|
+
applyPiProfileMigration(plan);
|
|
330
|
+
const catalog = await discoverPiModelCatalog({
|
|
331
|
+
cwd: CFG_DIR,
|
|
332
|
+
agentDir: piAgentDirectory(CFG_DIR, profile.appId),
|
|
333
|
+
env: process.env,
|
|
334
|
+
});
|
|
335
|
+
const effective = catalog.effectiveModel;
|
|
336
|
+
const entry = effective ? catalog.models.find((model) => model.id === effective) : undefined;
|
|
337
|
+
if (!effective || !entry) {
|
|
338
|
+
throw new Error("Pi official default resolution returned an unavailable model");
|
|
339
|
+
}
|
|
340
|
+
if (!entry.contextWindow) {
|
|
341
|
+
const alternatives = catalog.models
|
|
342
|
+
.filter((model) => Number.isFinite(model.contextWindow) && Number(model.contextWindow) > 0)
|
|
343
|
+
.map((model) => model.id);
|
|
344
|
+
throw Object.assign(new Error("Pi effective model is missing a context window"), { catalogModels: alternatives });
|
|
345
|
+
}
|
|
346
|
+
calculatePiCompactionSettings(entry.contextWindow);
|
|
347
|
+
targetModelId = effective;
|
|
348
|
+
resolvedExternalModel = effective;
|
|
349
|
+
runtimeModels = [{ id: effective, supportedReasoningEfforts: entry.supportedReasoningEfforts }];
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
throw new Error(formatExternalPiSetupFailure(error, { agentExisted: Boolean(prior) }));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
else if (runtime === "pi") {
|
|
356
|
+
const preservedEffort = prior?.runtime === "pi" && typeof prior.effort === "string" && prior.effort.trim()
|
|
357
|
+
? [prior.effort]
|
|
358
|
+
: [];
|
|
359
|
+
runtimeModels = [{ id: targetModelId, supportedReasoningEfforts: preservedEffort }];
|
|
360
|
+
}
|
|
361
|
+
const targetModel = runtimeModels.find((model) => model.id === targetModelId);
|
|
362
|
+
if (!targetModel)
|
|
363
|
+
throw new Error(`目标模型 ${runtime}/${targetModelId} 不在 runtime 模型目录中;未修改 Agent 配置`);
|
|
364
|
+
const stored = planSingleRootBinding({
|
|
365
|
+
config: larkinConfig.toStored(config),
|
|
366
|
+
profile,
|
|
367
|
+
requestedAgent,
|
|
368
|
+
runtime: selection?.runtime || options.runtime,
|
|
369
|
+
...(runtime === "pi" && piDistribution ? { piDistribution } : {}),
|
|
370
|
+
...(selectedModel || resolvedExternalModel ? { model: selectedModel || resolvedExternalModel } : {}),
|
|
371
|
+
defaultModel,
|
|
372
|
+
supportedReasoningEfforts: targetModel.supportedReasoningEfforts || [],
|
|
373
|
+
now: new Date().toISOString(),
|
|
374
|
+
});
|
|
375
|
+
const bound = stored.agents[profile.appId];
|
|
376
|
+
fs.mkdirSync(CFG_DIR, { recursive: true });
|
|
377
|
+
const authAlreadyCompleted = selection?.runtime === "pi" && selection.distribution === "builtin"
|
|
378
|
+
&& selection.authCompleted === true;
|
|
379
|
+
const readinessAlreadyCompleted = authAlreadyCompleted
|
|
380
|
+
&& selection.readinessCompleted === true;
|
|
313
381
|
providerTransaction = selection?.runtime === "pi" && selection.distribution === "builtin"
|
|
314
382
|
&& !authAlreadyCompleted ? beginBuiltinPiCredentialTransaction(CFG_DIR, profile.appId)
|
|
315
383
|
: null;
|
|
@@ -349,10 +417,34 @@ export async function main() {
|
|
|
349
417
|
}
|
|
350
418
|
}
|
|
351
419
|
larkinConfig.commitSetupConfig(process.env, loaded.revision, stored);
|
|
420
|
+
setupConfigCommitted = true;
|
|
352
421
|
providerTransaction?.commit();
|
|
353
422
|
providerPublished = true;
|
|
423
|
+
if (pendingMigration) {
|
|
424
|
+
try {
|
|
425
|
+
releasePiProfileMigrationLock(pendingMigration.state);
|
|
426
|
+
}
|
|
427
|
+
catch { /* post-commit lock release is best-effort; do not roll back credentials */ }
|
|
428
|
+
pendingMigration = null;
|
|
429
|
+
}
|
|
430
|
+
fabricateAttachment(layout.root, stored.serverId);
|
|
431
|
+
say(`\n✓ 已写配置: ${CFG_FILE}`);
|
|
432
|
+
say(` agent=${profile.appId} runtime=${bound.runtime} model=${bound.model}`);
|
|
433
|
+
say(` 单服务 serverId=${stored.serverId} larkinHome=${layout.root}`);
|
|
434
|
+
say("\n启动这个 Agent:");
|
|
435
|
+
say(" larkin start # 单服务启动全部已配置 Agent");
|
|
436
|
+
say(` larkin start --agent ${profile.appId} # 仅调试这个 Agent`);
|
|
437
|
+
say("创建独立飞书(Lark)机器人 + Agent:");
|
|
438
|
+
say(" larkin setup");
|
|
354
439
|
}
|
|
355
440
|
catch (error) {
|
|
441
|
+
if (!setupConfigCommitted && pendingMigration) {
|
|
442
|
+
try {
|
|
443
|
+
rollbackPiProfileMigration(pendingMigration.state);
|
|
444
|
+
}
|
|
445
|
+
catch { /* best effort */ }
|
|
446
|
+
pendingMigration = null;
|
|
447
|
+
}
|
|
356
448
|
providerTransaction?.rollback();
|
|
357
449
|
throw error;
|
|
358
450
|
}
|
|
@@ -361,15 +453,6 @@ export async function main() {
|
|
|
361
453
|
process.off("SIGINT", onSigint);
|
|
362
454
|
process.off("SIGTERM", onSigterm);
|
|
363
455
|
}
|
|
364
|
-
fabricateAttachment(layout.root, stored.serverId);
|
|
365
|
-
say(`\n✓ 已写配置: ${CFG_FILE}`);
|
|
366
|
-
say(` agent=${profile.appId} runtime=${bound.runtime} model=${bound.model}`);
|
|
367
|
-
say(` 单服务 serverId=${stored.serverId} larkinHome=${layout.root}`);
|
|
368
|
-
say("\n启动这个 Agent:");
|
|
369
|
-
say(" larkin start # 单服务启动全部已配置 Agent");
|
|
370
|
-
say(` larkin start --agent ${profile.appId} # 仅调试这个 Agent`);
|
|
371
|
-
say("创建独立飞书(Lark)机器人 + Agent:");
|
|
372
|
-
say(" larkin setup");
|
|
373
456
|
}
|
|
374
457
|
if (path.resolve(process.argv[1] || "") === path.resolve(fileURLToPath(import.meta.url))) {
|
|
375
458
|
main().catch((error) => die(error instanceof Error ? error.message : String(error)));
|