larkin 0.4.10 → 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
CHANGED
|
@@ -82,11 +82,10 @@ Options:
|
|
|
82
82
|
--base-url <url> custom 端点(provider=custom 时必填;也可覆盖 preset 默认端点)
|
|
83
83
|
--model <id> 模型 ID;默认取 provider 预设。不同 runtime 的模型可用
|
|
84
84
|
\`larkin model\` 查看 / 切换
|
|
85
|
-
--
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
Lark 租户不得使用 feishu.cn 主机。
|
|
85
|
+
--from-cli-profile <name> 可选:复用已有官方 lark-cli profile(需 LARKIN_SETUP_APP_SECRET)
|
|
86
|
+
--tenant feishu|lark 授权二维码之前选择品牌。默认 feishu(扫码 /page/launcher)。
|
|
87
|
+
Lark 用 --tenant lark:扫码 /page/cli,完成后凭证回传,
|
|
88
|
+
不要打开 /page/launcher。交互式未指定时会先询问。
|
|
90
89
|
|
|
91
90
|
setup handles browser authorization, permission grants, credential storage, Agent configuration,
|
|
92
91
|
and target-only hot attach. Each Agent is identified by its bot App ID: selecting the same bot reuses
|
|
@@ -163,12 +162,13 @@ export async function main() {
|
|
|
163
162
|
die("--tenant 只支持 feishu 或 lark");
|
|
164
163
|
registerArgs.push("--tenant", tenant === "lark" ? "lark" : "feishu");
|
|
165
164
|
}
|
|
166
|
-
for (const name of ["--provider", "--api-key", "--base-url", "--model"]) {
|
|
165
|
+
for (const name of ["--provider", "--api-key", "--base-url", "--model", "--from-cli-profile"]) {
|
|
167
166
|
const value = flag(name);
|
|
168
167
|
if (value)
|
|
169
168
|
registerArgs.push(name, value);
|
|
170
169
|
}
|
|
171
170
|
const result = await runForeground("bot-register", registerArgs);
|
|
171
|
+
delete process.env.LARKIN_SETUP_APP_SECRET;
|
|
172
172
|
if (result.code !== 0)
|
|
173
173
|
die("机器人授权或 Agent 配置未完成");
|
|
174
174
|
let agentId;
|
|
@@ -18,6 +18,7 @@ import { configureBuiltinPiProviderModel, stageBuiltinPiProvider, validatePiBase
|
|
|
18
18
|
import { beginBuiltinPiCredentialTransaction, createOfficialPiCredentialRuntime, createOfficialPiAuthInteraction, createOfficialPiModelRuntime, createOfficialPiLogoutRuntime, createOfficialPiRegistryRuntime, listOfficialPiAuthProviders, logoutOfficialPiProvider, officialPiAuthStatus, runOfficialPiLogin, verifyOfficialPiProviderTurn, } from "../runtime/pi-official-auth.mjs";
|
|
19
19
|
import { documentCommentSubscriptionCapability, markDocumentCommentSubscriptionVerified, } from "../platform/callback-capability.mjs";
|
|
20
20
|
import { openPlatformHost, parseLarkinTenant, registerAppAccountsHost, } from "../feishu/platform-hosts.mjs";
|
|
21
|
+
import { authorizationUrlFailureMessage, presentAuthorizationUrl } from "./setup-authorization-url.mjs";
|
|
21
22
|
// qrcode-terminal does not publish TypeScript declarations.
|
|
22
23
|
// @ts-expect-error bundled CommonJS dependency
|
|
23
24
|
import qrcodePackage from "qrcode-terminal";
|
|
@@ -466,8 +467,10 @@ export async function main() {
|
|
|
466
467
|
用法:
|
|
467
468
|
bun bot-register.mjs --auto [--tenant feishu|lark]
|
|
468
469
|
|
|
469
|
-
--tenant 在授权二维码之前选择品牌。默认 feishu(accounts.feishu.cn
|
|
470
|
-
Lark 必须用 --tenant lark
|
|
470
|
+
--tenant 在授权二维码之前选择品牌。默认 feishu(accounts.feishu.cn /page/launcher)。
|
|
471
|
+
Lark 必须用 --tenant lark:授权二维码是官方 /page/cli(begin 仍走飞书 accounts,
|
|
472
|
+
打开 open.larksuite.com),不要打开 /page/launcher(ack 10074 / 链接已失效)。
|
|
473
|
+
扫码完成后凭证回传,无需手抄 App Secret。
|
|
471
474
|
|
|
472
475
|
完成后自动:按返回 appId 判定新旧 → 同步并校验 lark-cli bot 凭证 →
|
|
473
476
|
原子写 bots/<appId>.json(0600)→ 绑定 Agent。`);
|
|
@@ -486,11 +489,14 @@ Lark 必须用 --tenant lark 或交互选择,授权主机是 accounts.larksuit
|
|
|
486
489
|
die("--result-file 必须是配置根目录内的 .setup-result-<pid>.json");
|
|
487
490
|
}
|
|
488
491
|
if (argv.some((arg) => arg === "--app-id" || arg.startsWith("--app-id="))) {
|
|
489
|
-
die("不支持 --app-id;机器人必须在飞书(Lark
|
|
492
|
+
die("不支持 --app-id;机器人必须在飞书(Lark)网页中选择。已有官方 lark-cli profile 时用 --from-cli-profile");
|
|
490
493
|
}
|
|
494
|
+
const fromCliProfile = flag("--from-cli-profile") || "";
|
|
495
|
+
if (fromCliProfile && !/^[A-Za-z0-9._-]{1,64}$/.test(fromCliProfile))
|
|
496
|
+
die("--from-cli-profile 非法");
|
|
491
497
|
if (!autoSelect)
|
|
492
498
|
die("setup 注册阶段必须由交互式选择流程启动");
|
|
493
|
-
|
|
499
|
+
let selectedTenant = await resolveSelectedTenant();
|
|
494
500
|
say(`[setup 1/5] 在${selectedTenant === "lark" ? " International Lark" : "中国飞书 Feishu"}网页选择已有机器人或创建新机器人`);
|
|
495
501
|
if (commentSubscription === "application") {
|
|
496
502
|
say(`! 已显式选择 ${commentSubscription} 维度评论订阅:一旦平台状态验证为已订阅,Bot 可见文档中实际送达的每条支持评论都会进入 Inbox 并唤醒 Agent,不要求 @Bot。`);
|
|
@@ -524,30 +530,91 @@ Lark 必须用 --tenant lark 或交互选择,授权主机是 accounts.larksuit
|
|
|
524
530
|
"docs:document.comment:create",
|
|
525
531
|
];
|
|
526
532
|
const TENANT_EVENTS = ["im.message.receive_v1", "im.message.message_read_v1", "drive.notice.comment_add_v1"];
|
|
533
|
+
const LARKIN_REGISTER_ADDONS = {
|
|
534
|
+
scopes: { tenant: TENANT_SCOPES },
|
|
535
|
+
events: { items: { tenant: TENANT_EVENTS } },
|
|
536
|
+
callbacks: { items: ["card.action.trigger"] },
|
|
537
|
+
};
|
|
527
538
|
let pollingCount = 0;
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
539
|
+
const presentUrl = (url, expireIn) => {
|
|
540
|
+
qrcode.generate(url, { small: true }, (code) => say(code));
|
|
541
|
+
say(`\n打开以下链接(或扫码),${Math.round(expireIn / 60)} 分钟内有效:\n\n ${url}\n\n[setup 2/5] 等待飞书(Lark)网页完成授权并回传凭证…`);
|
|
542
|
+
say(openBrowser(url) ? "[setup] 已在默认浏览器打开授权页" : "[setup] 未能自动打开浏览器,请手动打开上面的链接");
|
|
543
|
+
};
|
|
544
|
+
const importedSecret = typeof process.env.LARKIN_SETUP_APP_SECRET === "string"
|
|
545
|
+
? process.env.LARKIN_SETUP_APP_SECRET.trim() : "";
|
|
546
|
+
delete process.env.LARKIN_SETUP_APP_SECRET;
|
|
547
|
+
let result = null;
|
|
548
|
+
if (fromCliProfile) {
|
|
549
|
+
if (!importedSecret)
|
|
550
|
+
die("--from-cli-profile 需要环境变量 LARKIN_SETUP_APP_SECRET(App Secret 不进 argv)");
|
|
551
|
+
const official = officialCliForProfile(process.env);
|
|
552
|
+
const listed = spawnSync(official.command, [...official.argsPrefix, "profile", "list"], { encoding: "utf8", env: process.env });
|
|
553
|
+
let profiles = [];
|
|
554
|
+
try {
|
|
555
|
+
profiles = JSON.parse(listed.stdout || "[]");
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
die("读取 lark-cli profile list 失败");
|
|
559
|
+
}
|
|
560
|
+
const matched = profiles.find((profile) => profile.name === fromCliProfile);
|
|
561
|
+
if (matched == null || typeof matched.appId !== "string" || !APP_ID.test(matched.appId)) {
|
|
562
|
+
die(`找不到 lark-cli profile ${fromCliProfile}`);
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
const brand = matched.brand === "lark" || matched.brand === "feishu" ? matched.brand : selectedTenant;
|
|
566
|
+
selectedTenant = brand;
|
|
567
|
+
result = { client_id: matched.appId, client_secret: importedSecret, user_info: { tenant_brand: brand } };
|
|
568
|
+
say(`[setup 1/5] 复用官方 lark-cli profile ${fromCliProfile} → ${matched.appId}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
else if (selectedTenant === "lark") {
|
|
572
|
+
// Official lark-cli always begins on Feishu accounts, then presents
|
|
573
|
+
// open.larksuite.com/page/cli. Beginning on Lark accounts yields /page/launcher
|
|
574
|
+
// codes that open.larksuite.com ack rejects with 10074.
|
|
575
|
+
say("[setup 1/5] Lark 租户走官方 /page/cli 扫码授权;扫码完成后凭证回传,不要打开 /page/launcher");
|
|
576
|
+
const official = officialCliForProfile(process.env);
|
|
577
|
+
result = await registerApp({
|
|
578
|
+
source: "larkin",
|
|
579
|
+
domain: registerAppAccountsHost("feishu"),
|
|
580
|
+
larkDomain: registerAppAccountsHost("lark"),
|
|
581
|
+
addons: LARKIN_REGISTER_ADDONS,
|
|
582
|
+
onQRCodeReady: ({ url, expireIn }) => {
|
|
583
|
+
const presented = presentAuthorizationUrl(url, { tenant: "lark", larkCliVersion: official.version });
|
|
584
|
+
if (/\/page\/launcher(?:\?|$)/.test(presented)) {
|
|
585
|
+
throw new Error("Lark 租户拒绝把 /page/launcher 交给浏览器");
|
|
586
|
+
}
|
|
587
|
+
presentUrl(presented, expireIn);
|
|
588
|
+
},
|
|
589
|
+
onStatusChange: ({ status, interval }) => {
|
|
590
|
+
if (status === "domain_switched")
|
|
591
|
+
say("[setup] 已切换到 Lark 域名");
|
|
592
|
+
if (status === "slow_down")
|
|
593
|
+
say(`[setup] 飞书(Lark)要求降低轮询频率,${interval || "稍后"}秒后继续`);
|
|
594
|
+
if (status === "polling" && ++pollingCount % 12 === 0)
|
|
595
|
+
say(`[setup] 仍在等待飞书(Lark)回传凭证(约 ${pollingCount / 12} 分钟)…`);
|
|
596
|
+
},
|
|
597
|
+
}).catch((error) => die(authorizationUrlFailureMessage(error)));
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
result = await registerApp({
|
|
601
|
+
source: "larkin",
|
|
602
|
+
domain: registerAppAccountsHost(selectedTenant),
|
|
603
|
+
larkDomain: registerAppAccountsHost("lark"),
|
|
604
|
+
addons: LARKIN_REGISTER_ADDONS,
|
|
605
|
+
onQRCodeReady: ({ url, expireIn }) => {
|
|
606
|
+
presentUrl(url, expireIn);
|
|
607
|
+
},
|
|
608
|
+
onStatusChange: ({ status, interval }) => {
|
|
609
|
+
if (status === "domain_switched")
|
|
610
|
+
say("[setup] 已切换到 Lark 域名");
|
|
611
|
+
if (status === "slow_down")
|
|
612
|
+
say(`[setup] 飞书(Lark)要求降低轮询频率,${interval || "稍后"}秒后继续`);
|
|
613
|
+
if (status === "polling" && ++pollingCount % 12 === 0)
|
|
614
|
+
say(`[setup] 仍在等待飞书(Lark)回传凭证(约 ${pollingCount / 12} 分钟)…`);
|
|
615
|
+
},
|
|
616
|
+
}).catch((error) => die(authorizationUrlFailureMessage(error)));
|
|
617
|
+
}
|
|
551
618
|
const { client_id: rawId, client_secret: rawSecret, user_info: userInfo } = result || {};
|
|
552
619
|
if (typeof rawId !== "string" || !APP_ID.test(rawId))
|
|
553
620
|
die("授权返回的 App ID 格式非法;未执行凭证同步、文件写入或 Agent 绑定");
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { openPlatformHost } from "../feishu/platform-hosts.mjs";
|
|
2
|
+
// Official lark-cli presents /page/cli. node-sdk registerApp returns /page/launcher,
|
|
3
|
+
// which open.larksuite.com ack rejects with 10074 ("链接已失效") for Lark tenants.
|
|
4
|
+
export function officialCliSetupUrl(input) {
|
|
5
|
+
const userCode = input.userCode.trim();
|
|
6
|
+
if (!/^[A-Z0-9]{4}-[A-Z0-9]{4}$/.test(userCode))
|
|
7
|
+
throw new Error("setup user_code 格式非法");
|
|
8
|
+
const version = input.larkCliVersion.trim();
|
|
9
|
+
if (!/^\d+\.\d+\.\d+$/.test(version))
|
|
10
|
+
throw new Error("lark-cli version 格式非法");
|
|
11
|
+
const url = new URL(`${openPlatformHost(input.tenant)}/page/cli`);
|
|
12
|
+
url.searchParams.set("user_code", userCode);
|
|
13
|
+
url.searchParams.set("lpv", version);
|
|
14
|
+
url.searchParams.set("ocv", version);
|
|
15
|
+
url.searchParams.set("from", "cli");
|
|
16
|
+
return url.toString();
|
|
17
|
+
}
|
|
18
|
+
export function presentAuthorizationUrl(rawUrl, input) {
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = new URL(rawUrl);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new Error("授权 URL 非法");
|
|
25
|
+
}
|
|
26
|
+
const userCode = parsed.searchParams.get("user_code") || "";
|
|
27
|
+
// Feishu China still authorizes on /page/launcher. Only International Lark
|
|
28
|
+
// must leave launcher (open.larksuite.com ack 10074 / 链接已失效).
|
|
29
|
+
if (input.tenant === "lark" && parsed.pathname === "/page/launcher") {
|
|
30
|
+
if (!userCode)
|
|
31
|
+
throw new Error("Lark 租户拒绝把 /page/launcher 交给浏览器");
|
|
32
|
+
const presented = new URL(officialCliSetupUrl({ tenant: "lark", userCode, larkCliVersion: input.larkCliVersion }));
|
|
33
|
+
// node-sdk registerApp encodes scopes/events/callbacks only on the landing URL
|
|
34
|
+
// (`addons=`). Keep that payload when rewriting launcher → /page/cli so Lark
|
|
35
|
+
// still requests the same Larkin addons Feishu already sends.
|
|
36
|
+
const addons = parsed.searchParams.get("addons");
|
|
37
|
+
if (addons)
|
|
38
|
+
presented.searchParams.set("addons", addons);
|
|
39
|
+
return presented.toString();
|
|
40
|
+
}
|
|
41
|
+
return parsed.toString();
|
|
42
|
+
}
|
|
43
|
+
export function authorizationUrlFailureMessage(error) {
|
|
44
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
45
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
|
46
|
+
const description = error && typeof error === "object" && "description" in error
|
|
47
|
+
? String(error.description) : "";
|
|
48
|
+
const detail = [code, description, message].filter(Boolean).join(": ");
|
|
49
|
+
return `网页授权失败(${detail || "unknown"});未执行凭证同步、文件写入或 Agent 绑定。Lark 租户不要使用 /page/launcher(ack 10074 / 链接已失效),应走官方 lark-cli 的 /page/cli。`;
|
|
50
|
+
}
|
|
@@ -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
|
+
}
|