larkin 0.4.9 → 0.4.11
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 +6 -6
- package/dist/runtime/pi-subagents-notification.mjs +161 -0
- package/dist/runtime/runtime-adapters.mjs +32 -1
- package/dist/runtime/runtime-host.cjs +250 -1
- package/dist/runtime/runtime-host.mjs +250 -1
- package/dist/setup/bot-register.mjs +94 -27
- package/dist/setup/setup-authorization-url.mjs +50 -0
- package/dist/setup/setup-bind.mjs +60 -22
- package/dist/setup/setup-official-cli-register.mjs +74 -0
- package/package.json +1 -1
|
@@ -9,12 +9,12 @@ 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 { discoverPiModelCatalog } from "../runtime/pi-model-catalog.mjs";
|
|
13
12
|
import { configureBuiltinPiProviderModel } from "../runtime/pi-provider-config.mjs";
|
|
14
13
|
import { terminalSetupQuestioner } from "./setup-agent-choice.mjs";
|
|
15
14
|
import { beginBuiltinPiCredentialTransaction, createOfficialPiAuthInteraction, createOfficialPiModelRuntime, runOfficialPiLogin, verifyOfficialPiProviderTurn, } from "../runtime/pi-official-auth.mjs";
|
|
16
15
|
import * as larkinConfigImport from "../platform/config.mjs";
|
|
17
16
|
import { resolveOfficialLarkCli } from "../app/official-lark-cli.mjs";
|
|
17
|
+
import { loadValidatedBotCredential } from "./run-credential-preflight.mjs";
|
|
18
18
|
const larkinConfig = larkinConfigImport;
|
|
19
19
|
const layout = TargetRootLayout.fromConfigDir(larkinConfig.resolveConfigDir(process.env));
|
|
20
20
|
const CFG_DIR = layout.root;
|
|
@@ -72,20 +72,49 @@ function openAuthUrl(url) {
|
|
|
72
72
|
const result = spawnSync(command, args, { stdio: "ignore", timeout: 5_000 });
|
|
73
73
|
return result.status === 0;
|
|
74
74
|
}
|
|
75
|
+
function listProfilesFromEnv(env) {
|
|
76
|
+
const official = resolveOfficialLarkCli({ env });
|
|
77
|
+
const result = spawnSync(official.command, [...official.argsPrefix, "profile", "list"], { encoding: "utf8", env });
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(result.stdout || "[]");
|
|
80
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
75
86
|
function listProfiles() {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
const isolated = listProfilesFromEnv(larkEnv());
|
|
88
|
+
const globalEnv = { ...process.env };
|
|
89
|
+
delete globalEnv.LARKSUITE_CLI_CONFIG_DIR;
|
|
90
|
+
const global = listProfilesFromEnv(globalEnv);
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
const merged = [];
|
|
93
|
+
for (const profile of [...isolated, ...global]) {
|
|
94
|
+
const key = `${String(profile.name || "")}\0${String(profile.appId || "")}`;
|
|
95
|
+
if (seen.has(key))
|
|
96
|
+
continue;
|
|
97
|
+
seen.add(key);
|
|
98
|
+
merged.push(profile);
|
|
79
99
|
}
|
|
80
|
-
return
|
|
100
|
+
return merged;
|
|
101
|
+
}
|
|
102
|
+
function resolveProfile(requested, profiles) {
|
|
103
|
+
return profiles.find((profile) => profile.name === requested)
|
|
104
|
+
|| profiles.find((profile) => profile.appId === requested);
|
|
81
105
|
}
|
|
82
106
|
async function pickProfile() {
|
|
83
|
-
if (options.profile) {
|
|
84
|
-
if (!APP_ID.test(options.profile))
|
|
85
|
-
die(`profile ${options.profile} 不是合法 App ID`);
|
|
107
|
+
if (options.profile && APP_ID.test(options.profile)) {
|
|
86
108
|
return { name: options.profile, appId: options.profile, active: true };
|
|
87
109
|
}
|
|
88
110
|
const profiles = listProfiles();
|
|
111
|
+
if (options.profile) {
|
|
112
|
+
const matched = resolveProfile(options.profile, profiles);
|
|
113
|
+
if (matched && typeof matched.appId === "string" && APP_ID.test(matched.appId)) {
|
|
114
|
+
return { name: String(matched.name || matched.appId), appId: matched.appId, active: matched.active };
|
|
115
|
+
}
|
|
116
|
+
die(`找不到 lark-cli profile ${options.profile}(可用名字或 App ID,不要求名字等于 App ID)`);
|
|
117
|
+
}
|
|
89
118
|
if (profiles.length === 0)
|
|
90
119
|
die("lark-cli 里还没有 bot profile。请运行 larkin setup 创建或连接机器人");
|
|
91
120
|
say("\n可复用的飞书(Lark)机器人(lark-cli profiles):");
|
|
@@ -96,7 +125,19 @@ async function pickProfile() {
|
|
|
96
125
|
const selected = answer === "" ? activeIndex : Number(answer) - 1;
|
|
97
126
|
if (!(selected >= 0 && selected < profiles.length))
|
|
98
127
|
die("无效的 profile 选择");
|
|
99
|
-
|
|
128
|
+
const picked = profiles[selected];
|
|
129
|
+
if (typeof picked.appId !== "string" || !APP_ID.test(picked.appId)) {
|
|
130
|
+
die(`profile ${picked.name || "?"} 没有合法 App ID`);
|
|
131
|
+
}
|
|
132
|
+
return picked;
|
|
133
|
+
}
|
|
134
|
+
function requireUsableBotCredential(appId) {
|
|
135
|
+
try {
|
|
136
|
+
loadValidatedBotCredential(path.join(CFG_DIR, "bots"), appId);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
die(`没有可用的 bot 凭证 ${appId}(bots/${appId}.json);未修改 Agent 配置,请先运行 larkin setup 完成扫码授权`);
|
|
140
|
+
}
|
|
100
141
|
}
|
|
101
142
|
function verifyBot(profile) {
|
|
102
143
|
const result = larkJson(["--profile", profile.name, "im", "+chat-list", "--as", "bot", "--json"]);
|
|
@@ -210,9 +251,11 @@ export async function main() {
|
|
|
210
251
|
}
|
|
211
252
|
say("=== larkin 多 Agent onboarding ===");
|
|
212
253
|
const profile = await pickProfile();
|
|
213
|
-
if (typeof profile
|
|
214
|
-
die(
|
|
254
|
+
if (typeof profile.appId !== "string" || !APP_ID.test(profile.appId)) {
|
|
255
|
+
die(`profile ${profile.name || "?"} 没有合法 App ID;未修改 Agent 配置`);
|
|
215
256
|
}
|
|
257
|
+
say(`使用 profile ${profile.name} → App ID ${profile.appId}`);
|
|
258
|
+
requireUsableBotCredential(profile.appId);
|
|
216
259
|
if (!options.profile)
|
|
217
260
|
verifyBot(profile);
|
|
218
261
|
const requestedAgent = options.agent || profile.appId;
|
|
@@ -226,16 +269,11 @@ export async function main() {
|
|
|
226
269
|
const selectedModel = selection?.runtime === "pi" && selection.distribution === "builtin" ? selection.model : undefined;
|
|
227
270
|
const targetModelId = selectedModel || (prior?.runtime === runtime ? prior.model : defaultModel);
|
|
228
271
|
let runtimeModels = larkinConfig.loadRuntimeModels()[runtime];
|
|
229
|
-
if (runtime === "pi"
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
cwd: layout.workspaceDir(profile.appId),
|
|
235
|
-
...(process.env.PI_CODING_AGENT_DIR ? { agentDir: process.env.PI_CODING_AGENT_DIR } : {}),
|
|
236
|
-
});
|
|
237
|
-
const effective = piCatalog.models.find((model) => model.id === piCatalog.effectiveModel);
|
|
238
|
-
runtimeModels = [{ id: "default", supportedReasoningEfforts: effective?.supportedReasoningEfforts || [] }, ...piCatalog.models];
|
|
272
|
+
if (runtime === "pi") {
|
|
273
|
+
const preservedEffort = prior?.runtime === "pi" && typeof prior.effort === "string" && prior.effort.trim()
|
|
274
|
+
? [prior.effort]
|
|
275
|
+
: [];
|
|
276
|
+
runtimeModels = [{ id: targetModelId, supportedReasoningEfforts: preservedEffort }];
|
|
239
277
|
}
|
|
240
278
|
const targetModel = runtimeModels.find((model) => model.id === targetModelId);
|
|
241
279
|
if (!targetModel)
|
|
@@ -245,7 +283,7 @@ export async function main() {
|
|
|
245
283
|
profile,
|
|
246
284
|
requestedAgent,
|
|
247
285
|
runtime: selection?.runtime || options.runtime,
|
|
248
|
-
...(
|
|
286
|
+
...(runtime === "pi" && piDistribution ? { piDistribution } : {}),
|
|
249
287
|
...(selectedModel ? { model: selectedModel } : {}),
|
|
250
288
|
defaultModel,
|
|
251
289
|
supportedReasoningEfforts: targetModel.supportedReasoningEfforts || [],
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { spawn as systemSpawn } from "node:child_process";
|
|
2
|
+
const APP_ID = /^cli_[A-Za-z0-9]+$/;
|
|
3
|
+
const SETUP_URL = /https:\/\/open\.(?:larksuite\.com|feishu\.cn)\/page\/cli\?[^\s]+/;
|
|
4
|
+
export function parseOfficialCliAuthorizationUrl(text) {
|
|
5
|
+
return text.match(SETUP_URL)?.[0] ?? null;
|
|
6
|
+
}
|
|
7
|
+
export function parseOfficialCliInitOutput(text) {
|
|
8
|
+
const authorizationUrl = parseOfficialCliAuthorizationUrl(text);
|
|
9
|
+
const appId = text.match(/App ID:\s*(cli_[A-Za-z0-9]+)/)?.[1]
|
|
10
|
+
|| text.match(/"appId"\s*:\s*"(cli_[A-Za-z0-9]+)"/)?.[1];
|
|
11
|
+
const brand = text.match(/"brand"\s*:\s*"(lark|feishu)"/)?.[1];
|
|
12
|
+
if (!authorizationUrl)
|
|
13
|
+
throw new Error("官方 lark-cli init 未返回 /page/cli 授权链接");
|
|
14
|
+
if (!appId || !APP_ID.test(appId))
|
|
15
|
+
throw new Error("官方 lark-cli init 未返回合法 App ID");
|
|
16
|
+
if (brand !== "lark" && brand !== "feishu")
|
|
17
|
+
throw new Error("官方 lark-cli init 未返回合法 brand");
|
|
18
|
+
return { appId, authorizationUrl, brand };
|
|
19
|
+
}
|
|
20
|
+
export function officialCliInitArgs(tenant, profileName) {
|
|
21
|
+
if (!/^[A-Za-z0-9._-]{1,64}$/.test(profileName))
|
|
22
|
+
throw new Error("lark-cli profile name 非法");
|
|
23
|
+
return ["config", "init", "--new", "--brand", tenant, "--name", profileName];
|
|
24
|
+
}
|
|
25
|
+
export async function runOfficialCliAppInit(input) {
|
|
26
|
+
const args = [...input.official.argsPrefix, ...officialCliInitArgs(input.tenant, input.profileName)];
|
|
27
|
+
const child = (input.spawnImpl ?? systemSpawn)(input.official.command, args, {
|
|
28
|
+
env: input.env,
|
|
29
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
30
|
+
});
|
|
31
|
+
input.onChild?.(child);
|
|
32
|
+
let output = "";
|
|
33
|
+
let presented = false;
|
|
34
|
+
const consume = (chunk) => {
|
|
35
|
+
output += String(chunk);
|
|
36
|
+
if (presented)
|
|
37
|
+
return;
|
|
38
|
+
const url = parseOfficialCliAuthorizationUrl(output);
|
|
39
|
+
if (!url)
|
|
40
|
+
return;
|
|
41
|
+
presented = true;
|
|
42
|
+
input.onAuthorizationUrl(url);
|
|
43
|
+
};
|
|
44
|
+
child.stdout?.on("data", consume);
|
|
45
|
+
child.stderr?.on("data", consume);
|
|
46
|
+
const abort = () => {
|
|
47
|
+
child.kill("SIGTERM");
|
|
48
|
+
const killTimer = setTimeout(() => {
|
|
49
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
50
|
+
child.kill("SIGKILL");
|
|
51
|
+
}, 1_000);
|
|
52
|
+
killTimer.unref?.();
|
|
53
|
+
};
|
|
54
|
+
if (input.signal?.aborted)
|
|
55
|
+
abort();
|
|
56
|
+
else
|
|
57
|
+
input.signal?.addEventListener("abort", abort, { once: true });
|
|
58
|
+
try {
|
|
59
|
+
const status = await new Promise((resolve, reject) => {
|
|
60
|
+
child.once("error", reject);
|
|
61
|
+
child.once("exit", (code) => resolve(code));
|
|
62
|
+
});
|
|
63
|
+
if (input.signal?.aborted)
|
|
64
|
+
throw new Error("官方 lark-cli init 已取消");
|
|
65
|
+
const parsed = parseOfficialCliInitOutput(output);
|
|
66
|
+
if (status !== 0)
|
|
67
|
+
throw new Error(`官方 lark-cli init 失败(exit=${status ?? "none"})`);
|
|
68
|
+
return parsed;
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
input.signal?.removeEventListener("abort", abort);
|
|
72
|
+
input.onChild?.(null);
|
|
73
|
+
}
|
|
74
|
+
}
|