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.
@@ -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 result = larkJson(["profile", "list"]);
77
- if (!result.ok || !Array.isArray(result.json)) {
78
- die("读取 lark-cli profile 失败;未修改 Agent 配置,请检查隔离 profile 后重试 setup");
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 result.json;
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
- return profiles[selected];
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?.appId !== "string" || !APP_ID.test(profile.appId) || profile.name !== profile.appId) {
214
- die("profile name 与合法 App ID 必须完全一致;未修改 Agent 配置,请重新运行 larkin setup 刷新 profile");
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" && piDistribution === "builtin") {
230
- runtimeModels = [{ id: targetModelId, supportedReasoningEfforts: [] }];
231
- }
232
- else if (runtime === "pi") {
233
- const piCatalog = await discoverPiModelCatalog({
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
- ...(selection?.runtime === "pi" ? { piDistribution: selection.distribution } : {}),
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "larkin",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
4
4
  "private": false,
5
5
  "larkinPackageRole": "npm-published",
6
6
  "files": [