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,133 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Internal permission-update flow for an existing App-ID bot.
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { spawnSync as systemSpawnSync } from "node:child_process";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
import { registerApp as channelRegisterApp } from "@larksuite/channel";
|
|
8
|
+
import { markSetupCapabilitiesRequested } from "../platform/callback-capability.mjs";
|
|
9
|
+
import * as larkinConfig from "../platform/config.mjs";
|
|
10
|
+
import { managedOfficialLarkCli } from "../app/agent-lark-cli-workspace.mjs";
|
|
11
|
+
// qrcode-terminal does not publish TypeScript declarations.
|
|
12
|
+
// @ts-expect-error bundled CommonJS dependency
|
|
13
|
+
import qrcodePackage from "qrcode-terminal";
|
|
14
|
+
const testFixture = process.env.LARKIN_TEST_GRANT_SCOPES_MODULE
|
|
15
|
+
? await import(pathToFileURL(path.resolve(process.env.LARKIN_TEST_GRANT_SCOPES_MODULE)).href)
|
|
16
|
+
: null;
|
|
17
|
+
const registerApp = testFixture?.registerApp ?? channelRegisterApp;
|
|
18
|
+
const qrcode = testFixture?.qrcode ?? qrcodePackage;
|
|
19
|
+
const spawnSync = testFixture?.spawnSync ?? systemSpawnSync;
|
|
20
|
+
const resolveManagedOfficialCli = testFixture?.managedOfficialCli ?? managedOfficialLarkCli;
|
|
21
|
+
const argv = process.argv.slice(2);
|
|
22
|
+
const flag = (name, fallback) => {
|
|
23
|
+
const index = argv.indexOf(name);
|
|
24
|
+
if (index < 0)
|
|
25
|
+
return fallback;
|
|
26
|
+
const value = argv[index + 1];
|
|
27
|
+
if (!value || value.startsWith("--"))
|
|
28
|
+
throw new Error(`${name} 缺少参数值`);
|
|
29
|
+
return value;
|
|
30
|
+
};
|
|
31
|
+
const { config } = larkinConfig.loadConfig(process.env);
|
|
32
|
+
const explicitAppId = flag("--app-id", "");
|
|
33
|
+
const explicitAgent = flag("--agent", "");
|
|
34
|
+
if (explicitAppId && explicitAgent && explicitAppId !== explicitAgent)
|
|
35
|
+
throw new Error("--app-id 必须与 --agent 指向同一 Agent");
|
|
36
|
+
const selected = larkinConfig.selectAgent(config, (explicitAgent || explicitAppId)
|
|
37
|
+
? { LARKIN_AGENT_ID: explicitAgent || explicitAppId } : process.env);
|
|
38
|
+
const APP_ID = selected.agentId;
|
|
39
|
+
if (explicitAppId && explicitAppId !== selected.feishuAppId)
|
|
40
|
+
throw new Error("--app-id 必须等于所选 Agent 的 App ID");
|
|
41
|
+
if (!/^cli_[A-Za-z0-9]+$/.test(APP_ID))
|
|
42
|
+
throw new Error(`无效飞书 App ID:${APP_ID}`);
|
|
43
|
+
const SEND_TO = flag("--send-to", "");
|
|
44
|
+
const TENANT = flag("--tenant", "feishu");
|
|
45
|
+
const WAIT_MIN = Number(flag("--wait-min", "9"));
|
|
46
|
+
const URL_FILE = flag("--url-file", "");
|
|
47
|
+
const TENANT_SCOPES = [
|
|
48
|
+
"im:message:readonly",
|
|
49
|
+
"im:chat:readonly",
|
|
50
|
+
"im:chat",
|
|
51
|
+
"im:chat.group_info:readonly",
|
|
52
|
+
"im:chat.members:read",
|
|
53
|
+
"drive:drive",
|
|
54
|
+
"docs:document.comment:read",
|
|
55
|
+
"docs:document.comment:create",
|
|
56
|
+
];
|
|
57
|
+
const TENANT_EVENTS = ["drive.notice.comment_add_v1"];
|
|
58
|
+
const log = (...args) => { process.stderr.write(`${args.join(" ")}\n`); };
|
|
59
|
+
function sendUrlToChat(url, minutes) {
|
|
60
|
+
if (!SEND_TO)
|
|
61
|
+
return;
|
|
62
|
+
const text = `🔐 larkin 给机器人【${APP_ID}】增补读取权限。\n请【应用 owner 本人】打开下面链接确认(约 ${minutes} 分钟内有效),勾选并确认这些权限:\n${TENANT_SCOPES.join("、")}\n\n${url}\n\n确认后我会自动接上并重试读取。`;
|
|
63
|
+
const managed = resolveManagedOfficialCli(selected, process.env);
|
|
64
|
+
const result = spawnSync(managed.command.command, [...managed.command.argsPrefix, "im", "+messages-send", "--chat-id", SEND_TO, "--text", text, "--json"], {
|
|
65
|
+
encoding: "utf8", env: managed.env,
|
|
66
|
+
});
|
|
67
|
+
log(result.status === 0 ? "[grant] 更新链接已发到飞书群" : `[grant] 发链接失败: ${(result.stderr || "").trim().split("\n")[0]}`);
|
|
68
|
+
}
|
|
69
|
+
export async function main() {
|
|
70
|
+
log(`=== 给 bot ${APP_ID} 增补读取权限(registerApp 更新流) ===`);
|
|
71
|
+
const timeout = setTimeout(() => {
|
|
72
|
+
log(`[grant] ${WAIT_MIN} 分钟内未完成,退出。可重跑。`);
|
|
73
|
+
process.exit(2);
|
|
74
|
+
}, WAIT_MIN * 60 * 1000);
|
|
75
|
+
const options = {
|
|
76
|
+
source: "larkin",
|
|
77
|
+
appId: APP_ID,
|
|
78
|
+
addons: {
|
|
79
|
+
scopes: { tenant: TENANT_SCOPES },
|
|
80
|
+
events: { items: { tenant: TENANT_EVENTS } },
|
|
81
|
+
callbacks: { items: ["card.action.trigger"] },
|
|
82
|
+
},
|
|
83
|
+
onQRCodeReady: (info) => {
|
|
84
|
+
const minutes = Math.max(1, Math.round(info.expireIn / 60));
|
|
85
|
+
log("\n请用飞书扫码或打开链接,确认给应用增补权限:\n");
|
|
86
|
+
qrcode.generate(info.url, { small: true });
|
|
87
|
+
log(`\n链接: ${info.url}\n有效期约 ${minutes} 分钟(⚠️ 需本进程保持轮询,否则 user_code 立即失效)\n`);
|
|
88
|
+
if (URL_FILE) {
|
|
89
|
+
try {
|
|
90
|
+
fs.writeFileSync(URL_FILE, info.url);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
log(`[grant] 写 url-file 失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
sendUrlToChat(info.url, minutes);
|
|
97
|
+
},
|
|
98
|
+
onStatusChange: (info) => log(`[grant] 状态: ${info.status}`),
|
|
99
|
+
};
|
|
100
|
+
if (TENANT === "lark")
|
|
101
|
+
options.domain = "lark";
|
|
102
|
+
let result;
|
|
103
|
+
try {
|
|
104
|
+
result = await registerApp(options);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
clearTimeout(timeout);
|
|
108
|
+
log(`[grant] 失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
clearTimeout(timeout);
|
|
112
|
+
log(`\n✓ 权限增补已确认,appId=${result?.client_id || APP_ID}`);
|
|
113
|
+
log(` 已请求增补: ${TENANT_SCOPES.join(", ")}`);
|
|
114
|
+
log(` 已请求事件: ${TENANT_EVENTS.join(", ")}(仍需发布并用真实文档 @Bot 验证)`);
|
|
115
|
+
try {
|
|
116
|
+
markSetupCapabilitiesRequested(config.larkinHome, result?.client_id || APP_ID);
|
|
117
|
+
log(" card.action.trigger 仍是 requested-unverified;请确认发布后执行 interaction callback-probe 并真实点击,验证前不会创建业务交互卡片。");
|
|
118
|
+
log(" document comment capability=publish_or_event_unverified reason=publication_and_real_event_unverified;配置发布与真实事件到达均未验证,不声明生效。");
|
|
119
|
+
log(" document comment reply scope=docs:document.comment:create requested-unverified;同时覆盖 in-thread reply 与 whole-document create_v2 fallback。");
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (error.code !== "ENOENT")
|
|
123
|
+
throw error;
|
|
124
|
+
log(" 本机没有该 App 的 bot credential,无法记录 callback readiness;能力保持 missing,请先运行 larkin setup 后再执行 callback-probe。");
|
|
125
|
+
}
|
|
126
|
+
console.log(`GRANTED_APP_ID=${result?.client_id || APP_ID}`);
|
|
127
|
+
}
|
|
128
|
+
if (path.resolve(process.argv[1] || "") === path.resolve(fileURLToPath(import.meta.url))) {
|
|
129
|
+
main().catch((error) => {
|
|
130
|
+
log(`[grant] 异常: ${error instanceof Error ? error.message : String(error)}`);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { callbackCapability, documentCommentReplyCapability, documentCommentSubscriptionCapability, } from "../platform/callback-capability.mjs";
|
|
4
|
+
const BOT_CREDENTIAL_FIELDS = new Set(["appId", "appSecret", "tenant", "ownerOpenId", "createdAt", "updatedAt", "capabilities"]);
|
|
5
|
+
function validDocumentCommentCapability(value) {
|
|
6
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype)
|
|
7
|
+
return false;
|
|
8
|
+
const capability = value;
|
|
9
|
+
return Object.keys(capability).every((key) => ["status", "event", "scope", "requestedAt"].includes(key))
|
|
10
|
+
&& Object.keys(capability).length === 4
|
|
11
|
+
&& capability.status === "requested-unverified"
|
|
12
|
+
&& capability.event === "drive.notice.comment_add_v1"
|
|
13
|
+
&& capability.scope === "drive:drive"
|
|
14
|
+
&& typeof capability.requestedAt === "string"
|
|
15
|
+
&& Number.isFinite(Date.parse(capability.requestedAt));
|
|
16
|
+
}
|
|
17
|
+
export function validCredentialRecord(value, appId) {
|
|
18
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype)
|
|
19
|
+
return false;
|
|
20
|
+
const record = value;
|
|
21
|
+
if (Object.keys(record).some((field) => !BOT_CREDENTIAL_FIELDS.has(field)))
|
|
22
|
+
return false;
|
|
23
|
+
if (record.appId !== appId || typeof record.appSecret !== "string" || !record.appSecret.trim()
|
|
24
|
+
|| !["feishu", "lark"].includes(record.tenant))
|
|
25
|
+
return false;
|
|
26
|
+
if (Object.hasOwn(record, "ownerOpenId") && record.ownerOpenId !== null && typeof record.ownerOpenId !== "string")
|
|
27
|
+
return false;
|
|
28
|
+
for (const field of ["createdAt", "updatedAt"]) {
|
|
29
|
+
if (Object.hasOwn(record, field)
|
|
30
|
+
&& (typeof record[field] !== "string" || !record[field] || !Number.isFinite(Date.parse(record[field]))))
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (Object.hasOwn(record, "capabilities")) {
|
|
34
|
+
if (!record.capabilities || typeof record.capabilities !== "object" || Array.isArray(record.capabilities))
|
|
35
|
+
return false;
|
|
36
|
+
const capabilities = record.capabilities;
|
|
37
|
+
if (Object.keys(capabilities).some((key) => !["cardActionCallback", "documentCommentEvent", "documentCommentReply", "documentCommentSubscription"].includes(key)))
|
|
38
|
+
return false;
|
|
39
|
+
if (Object.hasOwn(capabilities, "cardActionCallback") && !callbackCapability(record))
|
|
40
|
+
return false;
|
|
41
|
+
if (Object.hasOwn(capabilities, "documentCommentEvent") && !validDocumentCommentCapability(capabilities.documentCommentEvent))
|
|
42
|
+
return false;
|
|
43
|
+
if (Object.hasOwn(capabilities, "documentCommentReply") && !documentCommentReplyCapability(record))
|
|
44
|
+
return false;
|
|
45
|
+
if (Object.hasOwn(capabilities, "documentCommentSubscription") && !documentCommentSubscriptionCapability(record))
|
|
46
|
+
return false;
|
|
47
|
+
if (!Object.keys(capabilities).length)
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
function isOwnedWithExactMode(stat, mode) {
|
|
53
|
+
return (typeof process.getuid !== "function" || stat.uid === process.getuid()) && (stat.mode & 0o777) === mode;
|
|
54
|
+
}
|
|
55
|
+
export function assertSecureBotsDirectory(botsDir) {
|
|
56
|
+
const stat = fs.lstatSync(botsDir);
|
|
57
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || !isOwnedWithExactMode(stat, 0o700)) {
|
|
58
|
+
throw new Error("unsafe bots directory");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function readSecureBotCredential(botsDir, appId) {
|
|
62
|
+
const resolvedBotsDir = path.resolve(botsDir);
|
|
63
|
+
const credentialFile = path.resolve(resolvedBotsDir, `${appId}.json`);
|
|
64
|
+
if (path.dirname(credentialFile) !== resolvedBotsDir)
|
|
65
|
+
throw new Error("credential path escaped bots directory");
|
|
66
|
+
let credentialFd;
|
|
67
|
+
try {
|
|
68
|
+
credentialFd = fs.openSync(credentialFile, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
69
|
+
const stat = fs.fstatSync(credentialFd);
|
|
70
|
+
if (!stat.isFile() || stat.isSymbolicLink() || !isOwnedWithExactMode(stat, 0o600)) {
|
|
71
|
+
throw new Error("credential file security attributes are invalid");
|
|
72
|
+
}
|
|
73
|
+
return JSON.parse(fs.readFileSync(credentialFd, "utf8"));
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
if (credentialFd !== undefined) {
|
|
77
|
+
try {
|
|
78
|
+
fs.closeSync(credentialFd);
|
|
79
|
+
}
|
|
80
|
+
catch { /* best-effort close */ }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function loadValidatedBotCredential(botsDir, appId) {
|
|
85
|
+
const value = readSecureBotCredential(botsDir, appId);
|
|
86
|
+
if (!validCredentialRecord(value, appId))
|
|
87
|
+
throw new Error("invalid bot credential record");
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin, stdout } from "node:process";
|
|
3
|
+
import { PI_PROVIDER_PRESETS, resolveBuiltinPiProviderSetupSelection, } from "../runtime/pi-provider-config.mjs";
|
|
4
|
+
function number(value, fallback, max) {
|
|
5
|
+
const parsed = value.trim() ? Number(value.trim()) : fallback;
|
|
6
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > max)
|
|
7
|
+
throw new Error(`请选择 1-${max}`);
|
|
8
|
+
return parsed;
|
|
9
|
+
}
|
|
10
|
+
export async function collectBuiltinPiChoice(questioner, services) {
|
|
11
|
+
const lines = PI_PROVIDER_PRESETS.map((preset, index) => ` ${index + 1}. ${preset.name} — ${preset.baseUrl}`).join("\n");
|
|
12
|
+
const providerChoice = number(await questioner.ask(`选择模型服务 / 认证:\n${lines}\n 5. Custom OpenAI-compatible\n 6. 官方 Pi provider 登录(API Key / OAuth / subscription)\n 7. 查看或退出已有官方 Pi 认证\n> `), 1, 7);
|
|
13
|
+
if (providerChoice === 7) {
|
|
14
|
+
if (!services)
|
|
15
|
+
throw new Error("当前 setup 上下文无法管理官方 Pi 认证");
|
|
16
|
+
const status = await services.status();
|
|
17
|
+
if (status.length === 0)
|
|
18
|
+
services.report("尚无已配置的官方 Pi credential");
|
|
19
|
+
else
|
|
20
|
+
status.forEach((entry, index) => services.report(` ${index + 1}. ${entry.providerName} (${entry.providerId}) — ${entry.credentialType}/${entry.source}${entry.stored ? ",已存储" : ",ambient"}`));
|
|
21
|
+
const answer = (await questioner.ask("输入要 logout 的序号;直接回车返回:")).trim();
|
|
22
|
+
if (answer) {
|
|
23
|
+
const selected = number(answer, 0, status.length);
|
|
24
|
+
await services.logout(status[selected - 1].providerId);
|
|
25
|
+
services.report(`已退出 ${status[selected - 1].providerName};未修改其他 provider`);
|
|
26
|
+
}
|
|
27
|
+
return collectBuiltinPiChoice(questioner, services);
|
|
28
|
+
}
|
|
29
|
+
if (providerChoice === 6) {
|
|
30
|
+
if (!services)
|
|
31
|
+
throw new Error("当前 setup 上下文无法读取官方 Pi auth registry");
|
|
32
|
+
const providers = await services.providers();
|
|
33
|
+
const methods = providers.flatMap((provider) => provider.methods.map((method) => ({ provider, method })));
|
|
34
|
+
if (methods.length === 0)
|
|
35
|
+
throw new Error("捆绑官方 Pi auth registry 没有可交互登录方式");
|
|
36
|
+
const methodLines = methods.map(({ provider, method }, index) => ` ${index + 1}. ${provider.name} — ${method.name} [${method.type}]`).join("\n");
|
|
37
|
+
const methodChoice = number(await questioner.ask(`选择官方 Pi 登录方式:\n${methodLines}\n> `), 1, methods.length);
|
|
38
|
+
const selected = methods[methodChoice - 1];
|
|
39
|
+
const defaultModel = selected.provider.models[0] || "";
|
|
40
|
+
const model = (await questioner.ask(`模型 ID${defaultModel ? `(默认 ${defaultModel})` : "(provider/model)"}:`)).trim() || defaultModel;
|
|
41
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}\/[A-Za-z0-9][A-Za-z0-9._:@+\/-]{0,255}$/.test(model))
|
|
42
|
+
throw new Error("模型 ID 必须是安全的 provider/model");
|
|
43
|
+
if (!model.startsWith(`${selected.provider.id}/`))
|
|
44
|
+
throw new Error(`模型 ID 必须属于 ${selected.provider.id}`);
|
|
45
|
+
return { runtime: "pi", distribution: "builtin", preset: "official", providerId: selected.provider.id,
|
|
46
|
+
authType: selected.method.type, model };
|
|
47
|
+
}
|
|
48
|
+
const preset = providerChoice === 5 ? "custom" : PI_PROVIDER_PRESETS[providerChoice - 1].id;
|
|
49
|
+
const selected = preset === "custom" ? null : PI_PROVIDER_PRESETS.find((candidate) => candidate.id === preset);
|
|
50
|
+
const baseUrl = preset === "custom" ? await questioner.ask("Base URL(https;localhost 可用 http):") : selected.baseUrl;
|
|
51
|
+
const model = (await questioner.ask(`模型 ID${selected ? `(默认 ${selected.defaultModel})` : ""}:`)).trim() || selected?.defaultModel || "";
|
|
52
|
+
const validated = resolveBuiltinPiProviderSetupSelection({ distribution: "builtin", preset, model, ...(baseUrl ? { baseUrl } : {}) });
|
|
53
|
+
return { runtime: "pi", distribution: "builtin", preset, model: validated.model, ...(preset === "custom" ? { baseUrl: validated.baseUrl } : {}) };
|
|
54
|
+
}
|
|
55
|
+
export async function collectSetupAgentChoice(questioner, prior, services) {
|
|
56
|
+
if (prior) {
|
|
57
|
+
const keep = (await questioner.ask(`已有 Agent:${prior.runtime}/${prior.model}。直接回车保留;输入 c 才修改:`)).trim().toLowerCase();
|
|
58
|
+
if (!keep)
|
|
59
|
+
return null;
|
|
60
|
+
if (keep !== "c")
|
|
61
|
+
throw new Error("请输入 c 修改,或直接回车保留现有配置");
|
|
62
|
+
}
|
|
63
|
+
const runtimeChoice = number(await questioner.ask("选择 Agent:\n 1. Pi(推荐)\n 2. Codex\n 3. Claude Code\n> "), 1, 3);
|
|
64
|
+
if (runtimeChoice === 2)
|
|
65
|
+
return { runtime: "codex" };
|
|
66
|
+
if (runtimeChoice === 3)
|
|
67
|
+
return { runtime: "claude" };
|
|
68
|
+
const sourceChoice = number(await questioner.ask("选择 Pi:\n 1. 外置 Pi(使用本机官方 pi)\n 2. 内置 Pi(无需安装 Agent CLI,推荐)\n> "), 2, 2);
|
|
69
|
+
const distribution = sourceChoice === 1 ? "external" : "builtin";
|
|
70
|
+
return distribution === "external" ? { runtime: "pi", distribution } : collectBuiltinPiChoice(questioner, services);
|
|
71
|
+
}
|
|
72
|
+
export async function recoverUnavailableExternalPi(initial, questioner, probe, report = () => { }, services) {
|
|
73
|
+
let choice = initial;
|
|
74
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
75
|
+
if (choice?.runtime !== "pi" || choice.distribution !== "external")
|
|
76
|
+
return choice;
|
|
77
|
+
const readiness = await probe();
|
|
78
|
+
if (readiness.state === "ready")
|
|
79
|
+
return choice;
|
|
80
|
+
report(`外置 Pi ${readiness.state}:${readiness.reason || "prerequisite unavailable"};${readiness.nextAction || "可切换到内置 Pi"}`);
|
|
81
|
+
const action = number(await questioner.ask("外置 Pi 当前不可用:\n 1. 切换到内置 Pi(推荐)\n 2. 重新选择 Agent\n 3. 取消 setup(保留原配置)\n> "), 1, 3);
|
|
82
|
+
if (action === 1)
|
|
83
|
+
return collectBuiltinPiChoice(questioner, services);
|
|
84
|
+
if (action === 3)
|
|
85
|
+
throw new Error("setup 已取消;未修改 Agent/config");
|
|
86
|
+
choice = await collectSetupAgentChoice(questioner, undefined, services);
|
|
87
|
+
}
|
|
88
|
+
throw new Error("外置 Pi 恢复选择已达到 3 次;未修改 Agent/config,请修复后重试");
|
|
89
|
+
}
|
|
90
|
+
export function terminalSetupQuestioner() {
|
|
91
|
+
let rl = null;
|
|
92
|
+
let lines = null;
|
|
93
|
+
const ensureInput = () => {
|
|
94
|
+
if (!rl) {
|
|
95
|
+
rl = createInterface({ input: stdin, output: stdout, terminal: Boolean(stdin.isTTY) });
|
|
96
|
+
lines = rl[Symbol.asyncIterator]();
|
|
97
|
+
}
|
|
98
|
+
return lines;
|
|
99
|
+
};
|
|
100
|
+
const close = () => { rl?.close(); rl = null; lines = null; };
|
|
101
|
+
const nextLine = async (prompt, signal) => {
|
|
102
|
+
if (signal?.aborted)
|
|
103
|
+
throw new Error("setup auth prompt 已取消");
|
|
104
|
+
const inputLines = ensureInput();
|
|
105
|
+
stdout.write(prompt);
|
|
106
|
+
let onAbort;
|
|
107
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
108
|
+
if (!signal)
|
|
109
|
+
return;
|
|
110
|
+
onAbort = () => { close(); reject(new Error("setup auth prompt 已取消")); };
|
|
111
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
112
|
+
});
|
|
113
|
+
let next;
|
|
114
|
+
try {
|
|
115
|
+
next = await Promise.race([inputLines.next(), aborted]);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
if (signal && onAbort)
|
|
119
|
+
signal.removeEventListener("abort", onAbort);
|
|
120
|
+
}
|
|
121
|
+
if (next.done)
|
|
122
|
+
throw new Error("setup 输入已结束;未保存配置");
|
|
123
|
+
return next.value;
|
|
124
|
+
};
|
|
125
|
+
return {
|
|
126
|
+
ask: nextLine,
|
|
127
|
+
secret: async (prompt, signal) => {
|
|
128
|
+
if (!stdin.isTTY || typeof stdin.setRawMode !== "function") {
|
|
129
|
+
return nextLine(prompt, signal);
|
|
130
|
+
}
|
|
131
|
+
close();
|
|
132
|
+
stdout.write(prompt);
|
|
133
|
+
const wasRaw = stdin.isRaw;
|
|
134
|
+
stdin.setRawMode(true);
|
|
135
|
+
stdin.resume();
|
|
136
|
+
return await new Promise((resolve, reject) => {
|
|
137
|
+
let value = "";
|
|
138
|
+
const finish = (error) => {
|
|
139
|
+
stdin.off("data", onData);
|
|
140
|
+
signal?.removeEventListener("abort", onAbort);
|
|
141
|
+
stdin.setRawMode(Boolean(wasRaw));
|
|
142
|
+
stdin.pause();
|
|
143
|
+
stdout.write("\n");
|
|
144
|
+
if (error)
|
|
145
|
+
reject(error);
|
|
146
|
+
else
|
|
147
|
+
resolve(value);
|
|
148
|
+
};
|
|
149
|
+
const onData = (chunk) => {
|
|
150
|
+
for (const byte of chunk) {
|
|
151
|
+
if (byte === 3)
|
|
152
|
+
return finish(new Error("setup 已取消;未保存 API Key"));
|
|
153
|
+
if (byte === 10 || byte === 13)
|
|
154
|
+
return finish();
|
|
155
|
+
if (byte === 8 || byte === 127) {
|
|
156
|
+
value = value.slice(0, -1);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (byte >= 32 && byte <= 126)
|
|
160
|
+
value += String.fromCharCode(byte);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const onAbort = () => finish(new Error("setup auth prompt 已取消"));
|
|
164
|
+
if (signal?.aborted)
|
|
165
|
+
return onAbort();
|
|
166
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
167
|
+
stdin.on("data", onData);
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
close,
|
|
171
|
+
};
|
|
172
|
+
}
|