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,655 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Internal setup stage: browser-select a bot, verify credentials, publish them, then bind its App-ID Agent.
|
|
3
|
+
import { spawn as systemSpawn, spawnSync as systemSpawnSync } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
import { registerApp as channelRegisterApp } from "@larksuite/channel";
|
|
8
|
+
import { internalCommandSpec } from "../app/internal-command.mjs";
|
|
9
|
+
import * as larkinConfig from "../platform/config.mjs";
|
|
10
|
+
import { hydrateRuntimeAgent, syncAgentProfile, syncAgentProfileAsync } from "../app/runtime-agent-config.mjs";
|
|
11
|
+
import { managedLarkCliEnv } from "../app/agent-lark-cli-workspace.mjs";
|
|
12
|
+
import { resolveOfficialLarkCli } from "../app/official-lark-cli.mjs";
|
|
13
|
+
import { collectSetupAgentChoice, recoverUnavailableExternalPi, terminalSetupQuestioner } from "./setup-agent-choice.mjs";
|
|
14
|
+
import { probeNativeRuntimeReadiness } from "../runtime/runtime-readiness.mjs";
|
|
15
|
+
import { configureBuiltinPiProviderModel } from "../runtime/pi-provider-config.mjs";
|
|
16
|
+
import { beginBuiltinPiCredentialTransaction, createOfficialPiCredentialRuntime, createOfficialPiAuthInteraction, createOfficialPiModelRuntime, createOfficialPiLogoutRuntime, createOfficialPiRegistryRuntime, listOfficialPiAuthProviders, logoutOfficialPiProvider, officialPiAuthStatus, runOfficialPiLogin, verifyOfficialPiProviderTurn, } from "../runtime/pi-official-auth.mjs";
|
|
17
|
+
import { documentCommentSubscriptionCapability, markDocumentCommentSubscriptionVerified, } from "../platform/callback-capability.mjs";
|
|
18
|
+
// qrcode-terminal does not publish TypeScript declarations.
|
|
19
|
+
// @ts-expect-error bundled CommonJS dependency
|
|
20
|
+
import qrcodePackage from "qrcode-terminal";
|
|
21
|
+
const testFixture = process.env.LARKIN_TEST_BOT_REGISTER_MODULE
|
|
22
|
+
? await import(pathToFileURL(path.resolve(process.env.LARKIN_TEST_BOT_REGISTER_MODULE)).href)
|
|
23
|
+
: null;
|
|
24
|
+
const registerApp = testFixture?.registerApp ?? channelRegisterApp;
|
|
25
|
+
const qrcode = testFixture?.qrcode ?? qrcodePackage;
|
|
26
|
+
const spawnSync = testFixture?.spawnSync ?? systemSpawnSync;
|
|
27
|
+
const synchronizeAgentProfile = testFixture?.syncAgentProfile ?? syncAgentProfile;
|
|
28
|
+
const resolveOfficialCli = testFixture?.resolveOfficialLarkCli ?? resolveOfficialLarkCli;
|
|
29
|
+
const wait = testFixture?.wait ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
|
|
30
|
+
const CFG_DIR = larkinConfig.resolveConfigDir(process.env);
|
|
31
|
+
let temporaryAgentChoiceFile = null;
|
|
32
|
+
let pendingPiAuthTransaction = null;
|
|
33
|
+
let pendingBindChild = null;
|
|
34
|
+
let pendingChildSettled = null;
|
|
35
|
+
let settlePendingChild = null;
|
|
36
|
+
let requestedShutdown = null;
|
|
37
|
+
const shutdownController = new AbortController();
|
|
38
|
+
let resolvedSetupOfficialCli = null;
|
|
39
|
+
function trackPendingChild(child) {
|
|
40
|
+
if (child) {
|
|
41
|
+
pendingBindChild = child;
|
|
42
|
+
pendingChildSettled = new Promise((resolve) => { settlePendingChild = resolve; });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
pendingBindChild = null;
|
|
46
|
+
settlePendingChild?.();
|
|
47
|
+
settlePendingChild = null;
|
|
48
|
+
pendingChildSettled = null;
|
|
49
|
+
}
|
|
50
|
+
process.on("exit", () => {
|
|
51
|
+
pendingPiAuthTransaction?.rollback();
|
|
52
|
+
if (!temporaryAgentChoiceFile)
|
|
53
|
+
return;
|
|
54
|
+
try {
|
|
55
|
+
fs.unlinkSync(temporaryAgentChoiceFile);
|
|
56
|
+
}
|
|
57
|
+
catch { /* consumed or best effort */ }
|
|
58
|
+
});
|
|
59
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
60
|
+
process.once(signal, () => {
|
|
61
|
+
if (requestedShutdown)
|
|
62
|
+
return;
|
|
63
|
+
requestedShutdown = signal;
|
|
64
|
+
const settling = pendingChildSettled;
|
|
65
|
+
shutdownController.abort(new Error(`setup interrupted by ${signal}`));
|
|
66
|
+
void (settling ?? Promise.resolve()).finally(() => {
|
|
67
|
+
pendingPiAuthTransaction?.rollback();
|
|
68
|
+
pendingPiAuthTransaction = null;
|
|
69
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const argv = process.argv.slice(2);
|
|
74
|
+
const flag = (name) => {
|
|
75
|
+
const index = argv.indexOf(name);
|
|
76
|
+
return index >= 0 ? argv[index + 1] : undefined;
|
|
77
|
+
};
|
|
78
|
+
const has = (name) => argv.includes(name);
|
|
79
|
+
const say = (...args) => console.error(...args);
|
|
80
|
+
const die = (message) => {
|
|
81
|
+
if (requestedShutdown)
|
|
82
|
+
throw new Error(`setup shutdown pending (${requestedShutdown})`);
|
|
83
|
+
console.error(`✗ ${message}`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
};
|
|
86
|
+
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
87
|
+
const APP_ID = /^cli_[A-Za-z0-9]+$/;
|
|
88
|
+
const BOT_VERIFY_BACKOFF_MS = [500, 1_000, 2_000, 4_000, 8_000, 15_000];
|
|
89
|
+
const testChildTimeout = process.env.LARKIN_TEST_BOT_REGISTER_MODULE ? Number(process.env.LARKIN_TEST_CHILD_TIMEOUT_MS) : NaN;
|
|
90
|
+
const testChildMaxOutput = process.env.LARKIN_TEST_BOT_REGISTER_MODULE ? Number(process.env.LARKIN_TEST_CHILD_MAX_OUTPUT_BYTES) : NaN;
|
|
91
|
+
const CHILD_TIMEOUT_MS = Number.isFinite(testChildTimeout) && testChildTimeout >= 50 ? testChildTimeout : 60_000;
|
|
92
|
+
const CHILD_MAX_OUTPUT_BYTES = Number.isFinite(testChildMaxOutput) && testChildMaxOutput >= 256 ? testChildMaxOutput : 64 * 1024;
|
|
93
|
+
function officialCliForProfile(env) {
|
|
94
|
+
if (resolvedSetupOfficialCli)
|
|
95
|
+
return resolvedSetupOfficialCli;
|
|
96
|
+
if (pendingPiAuthTransaction)
|
|
97
|
+
throw new Error("official lark-cli must be resolved before the Pi credential transaction starts");
|
|
98
|
+
resolvedSetupOfficialCli = resolveOfficialCli({ env });
|
|
99
|
+
return resolvedSetupOfficialCli;
|
|
100
|
+
}
|
|
101
|
+
function subscriptionStatus(payload) {
|
|
102
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
103
|
+
return null;
|
|
104
|
+
const root = payload;
|
|
105
|
+
const data = root.data && typeof root.data === "object" && !Array.isArray(root.data) ? root.data : null;
|
|
106
|
+
const nested = data?.data && typeof data.data === "object" && !Array.isArray(data.data) ? data.data : null;
|
|
107
|
+
if (root.ok !== true)
|
|
108
|
+
return null;
|
|
109
|
+
const value = data?.is_subscribe ?? nested?.is_subscribe;
|
|
110
|
+
return typeof value === "boolean" ? value : null;
|
|
111
|
+
}
|
|
112
|
+
export async function reconcileDocumentCommentSubscription(input) {
|
|
113
|
+
if (input.mode === "preserve")
|
|
114
|
+
return { changed: false, subscribed: false, dimension: null };
|
|
115
|
+
const run = input.runProcessImpl ?? ((command, args, env) => runBoundedCliProcess(command, args, env, "Document comment subscription operation"));
|
|
116
|
+
const eventType = "drive.notice.comment_add_v1";
|
|
117
|
+
const dimension = "application";
|
|
118
|
+
const identity = "bot";
|
|
119
|
+
const isShutdownAborted = input.isShutdownAborted ?? (() => shutdownController.signal.aborted);
|
|
120
|
+
const readStatus = async () => {
|
|
121
|
+
try {
|
|
122
|
+
const status = await run(input.command, [
|
|
123
|
+
...input.argsPrefix,
|
|
124
|
+
"drive", "user", "subscription_status",
|
|
125
|
+
"--params", JSON.stringify({ event_type: eventType }),
|
|
126
|
+
"--as", identity,
|
|
127
|
+
"--json",
|
|
128
|
+
], input.env);
|
|
129
|
+
let payload = null;
|
|
130
|
+
try {
|
|
131
|
+
payload = JSON.parse(String(status.stdout || "null"));
|
|
132
|
+
}
|
|
133
|
+
catch { /* fail closed */ }
|
|
134
|
+
return status.status === 0 ? subscriptionStatus(payload) : null;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const expected = input.mode !== "none";
|
|
141
|
+
const before = await readStatus();
|
|
142
|
+
if (before === null)
|
|
143
|
+
throw new Error(`document comment ${dimension} subscription preflight status was unreadable; no write attempted`);
|
|
144
|
+
if (before === expected)
|
|
145
|
+
return { changed: false, subscribed: expected, dimension };
|
|
146
|
+
const subscribeArgs = ["drive", "user", "subscription", "--data", JSON.stringify({ event_type: eventType }), "--as", identity, "--json"];
|
|
147
|
+
const removeArgs = ["drive", "user", "remove_subscription", "--event-type", eventType, "--as", identity, "--json"];
|
|
148
|
+
const mutationArgs = expected ? subscribeArgs : removeArgs;
|
|
149
|
+
try {
|
|
150
|
+
await run(input.command, [...input.argsPrefix, ...mutationArgs], input.env);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
if (isShutdownAborted()) {
|
|
154
|
+
throw new Error(`document comment ${dimension} subscription mutation was interrupted by shutdown; external state is uncertain`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (isShutdownAborted()) {
|
|
158
|
+
throw new Error(`document comment ${dimension} subscription mutation was interrupted by shutdown; external state is uncertain`);
|
|
159
|
+
}
|
|
160
|
+
const after = await readStatus();
|
|
161
|
+
if (after === expected)
|
|
162
|
+
return { changed: true, subscribed: expected, dimension };
|
|
163
|
+
if (!expected) {
|
|
164
|
+
if (after === true) {
|
|
165
|
+
throw new Error(`document comment ${dimension} subscription removal failed; platform status verified unchanged`);
|
|
166
|
+
}
|
|
167
|
+
if (isShutdownAborted()) {
|
|
168
|
+
throw new Error(`document comment ${dimension} subscription post-mutation status was interrupted by shutdown; external state is uncertain`);
|
|
169
|
+
}
|
|
170
|
+
throw new Error(`document comment ${dimension} subscription removal external state is uncertain`);
|
|
171
|
+
}
|
|
172
|
+
if (after === false) {
|
|
173
|
+
throw new Error(`document comment ${dimension} subscription activation failed; platform status verified unchanged`);
|
|
174
|
+
}
|
|
175
|
+
if (isShutdownAborted()) {
|
|
176
|
+
throw new Error(`document comment ${dimension} subscription post-mutation status was interrupted by shutdown; external state is uncertain`);
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
await run(input.command, [...input.argsPrefix, ...removeArgs], input.env);
|
|
180
|
+
}
|
|
181
|
+
catch { /* A rejected rollback is still ambiguous; final status remains authoritative. */ }
|
|
182
|
+
if (isShutdownAborted()) {
|
|
183
|
+
throw new Error(`document comment ${dimension} subscription rollback was interrupted by shutdown; external state is uncertain`);
|
|
184
|
+
}
|
|
185
|
+
const rolledBack = await readStatus();
|
|
186
|
+
if (rolledBack === false) {
|
|
187
|
+
throw new Error(`document comment ${dimension} subscription write was not verified; rollback was verified`);
|
|
188
|
+
}
|
|
189
|
+
if (rolledBack === true) {
|
|
190
|
+
throw new Error(`document comment ${dimension} subscription write was not verified; rollback failed and platform status verified the subscription remains active; run larkin setup --comment-subscription none to remove it`);
|
|
191
|
+
}
|
|
192
|
+
throw new Error(`document comment ${dimension} subscription write was not verified; rollback status is uncertain`);
|
|
193
|
+
}
|
|
194
|
+
export async function applyDocumentCommentSubscription(input) {
|
|
195
|
+
const reconciled = await reconcileDocumentCommentSubscription(input);
|
|
196
|
+
if (!reconciled.subscribed)
|
|
197
|
+
return reconciled;
|
|
198
|
+
try {
|
|
199
|
+
input.markVerified();
|
|
200
|
+
return reconciled;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
if (!reconciled.changed) {
|
|
204
|
+
throw new Error("document comment local verified-state persistence failed; pre-existing external subscription was left unchanged");
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
await reconcileDocumentCommentSubscription({ ...input, mode: "none" });
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (errorMessage(error).includes("removal failed; platform status verified unchanged")) {
|
|
211
|
+
throw new Error("document comment local verified-state persistence failed; external subscription rollback failed and platform status verified the subscription remains active; run larkin setup --comment-subscription none to remove it");
|
|
212
|
+
}
|
|
213
|
+
throw new Error("document comment local verified-state persistence failed; external subscription rollback status is uncertain");
|
|
214
|
+
}
|
|
215
|
+
throw new Error("document comment local verified-state persistence failed; external subscription rollback was verified");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function botVerificationRetryable(result) {
|
|
219
|
+
const text = `${typeof result.stdout === "string" ? result.stdout : ""}\n${typeof result.stderr === "string" ? result.stderr : ""}`;
|
|
220
|
+
return /\binvalid_client\b|specified app does not exist|EAI_AGAIN|ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|fetch failed|temporary network|too many requests|\b(?:429|502|503|504)\b/i.test(text);
|
|
221
|
+
}
|
|
222
|
+
function ensureSecureBotsDir() {
|
|
223
|
+
fs.mkdirSync(CFG_DIR, { recursive: true, mode: 0o700 });
|
|
224
|
+
const directory = path.join(CFG_DIR, "bots");
|
|
225
|
+
try {
|
|
226
|
+
fs.mkdirSync(directory, { mode: 0o700 });
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
if (error?.code !== "EEXIST")
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
const stat = fs.lstatSync(directory);
|
|
233
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
234
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
235
|
+
|| (stat.mode & 0o777) !== 0o700) {
|
|
236
|
+
die("bots 凭证目录必须是当前用户拥有、非 symlink、权限精确 0700 的真实目录;未写入新凭证");
|
|
237
|
+
}
|
|
238
|
+
return directory;
|
|
239
|
+
}
|
|
240
|
+
function openBrowser(url) {
|
|
241
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32.exe" : "xdg-open";
|
|
242
|
+
const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
243
|
+
try {
|
|
244
|
+
const child = systemSpawn(command, args, { stdio: "ignore", shell: false });
|
|
245
|
+
child.once("error", () => say(`[setup] 浏览器启动失败,请手动打开完整地址:${url}`));
|
|
246
|
+
child.unref?.();
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
say(`[setup] 浏览器启动失败,请手动打开完整地址:${url}`);
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async function runBindProcess(command, args) {
|
|
255
|
+
if (testFixture?.spawnSync && !pendingPiAuthTransaction) {
|
|
256
|
+
return spawnSync(command, [...args], { env: process.env, stdio: "inherit" }).status;
|
|
257
|
+
}
|
|
258
|
+
return await new Promise((resolve, reject) => {
|
|
259
|
+
const child = systemSpawn(command, [...args], { env: process.env, stdio: "inherit" });
|
|
260
|
+
trackPendingChild(child);
|
|
261
|
+
let killTimer = null;
|
|
262
|
+
const abort = () => {
|
|
263
|
+
child.kill("SIGTERM");
|
|
264
|
+
killTimer = setTimeout(() => { if (child.exitCode === null && child.signalCode === null)
|
|
265
|
+
child.kill("SIGKILL"); }, 1_000);
|
|
266
|
+
killTimer.unref?.();
|
|
267
|
+
};
|
|
268
|
+
if (shutdownController.signal.aborted)
|
|
269
|
+
abort();
|
|
270
|
+
else
|
|
271
|
+
shutdownController.signal.addEventListener("abort", abort, { once: true });
|
|
272
|
+
child.once("error", (error) => {
|
|
273
|
+
if (killTimer)
|
|
274
|
+
clearTimeout(killTimer);
|
|
275
|
+
shutdownController.signal.removeEventListener("abort", abort);
|
|
276
|
+
reject(error);
|
|
277
|
+
});
|
|
278
|
+
child.once("exit", (code) => {
|
|
279
|
+
if (killTimer)
|
|
280
|
+
clearTimeout(killTimer);
|
|
281
|
+
shutdownController.signal.removeEventListener("abort", abort);
|
|
282
|
+
resolve(code);
|
|
283
|
+
});
|
|
284
|
+
}).finally(() => { trackPendingChild(null); });
|
|
285
|
+
}
|
|
286
|
+
async function runBoundedCliProcess(command, args, env, label) {
|
|
287
|
+
if (testFixture?.spawnSync && !pendingPiAuthTransaction && process.env.LARKIN_TEST_ASYNC_IDENTITY !== "1") {
|
|
288
|
+
const result = spawnSync(command, [...args], { encoding: "utf8", env });
|
|
289
|
+
return { status: result.status, stdout: result.stdout || "", stderr: result.stderr || "" };
|
|
290
|
+
}
|
|
291
|
+
return await new Promise((resolve, reject) => {
|
|
292
|
+
const child = systemSpawn(command, [...args], { env, stdio: ["ignore", "pipe", "pipe"] });
|
|
293
|
+
trackPendingChild(child);
|
|
294
|
+
let stdout = "";
|
|
295
|
+
let stderr = "";
|
|
296
|
+
let outputBytes = 0;
|
|
297
|
+
let failure = null;
|
|
298
|
+
let settled = false;
|
|
299
|
+
let killTimer = null;
|
|
300
|
+
const terminate = (error) => {
|
|
301
|
+
if (failure)
|
|
302
|
+
return;
|
|
303
|
+
failure = error;
|
|
304
|
+
child.kill("SIGTERM");
|
|
305
|
+
killTimer = setTimeout(() => { if (child.exitCode === null && child.signalCode === null)
|
|
306
|
+
child.kill("SIGKILL"); }, 1_000);
|
|
307
|
+
killTimer.unref?.();
|
|
308
|
+
};
|
|
309
|
+
const collect = (target, chunk) => {
|
|
310
|
+
outputBytes += chunk.length;
|
|
311
|
+
if (outputBytes > CHILD_MAX_OUTPUT_BYTES) {
|
|
312
|
+
terminate(new Error(`${label} output exceeded the bounded limit`));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (target === "stdout")
|
|
316
|
+
stdout += chunk.toString("utf8");
|
|
317
|
+
else
|
|
318
|
+
stderr += chunk.toString("utf8");
|
|
319
|
+
};
|
|
320
|
+
child.stdout?.on("data", (chunk) => collect("stdout", chunk));
|
|
321
|
+
child.stderr?.on("data", (chunk) => collect("stderr", chunk));
|
|
322
|
+
const timeout = setTimeout(() => terminate(new Error(`${label} timed out`)), CHILD_TIMEOUT_MS);
|
|
323
|
+
timeout.unref?.();
|
|
324
|
+
const abort = () => terminate(new Error(`${label} cancelled`));
|
|
325
|
+
if (shutdownController.signal.aborted)
|
|
326
|
+
abort();
|
|
327
|
+
else
|
|
328
|
+
shutdownController.signal.addEventListener("abort", abort, { once: true });
|
|
329
|
+
const cleanup = () => {
|
|
330
|
+
clearTimeout(timeout);
|
|
331
|
+
if (killTimer)
|
|
332
|
+
clearTimeout(killTimer);
|
|
333
|
+
shutdownController.signal.removeEventListener("abort", abort);
|
|
334
|
+
};
|
|
335
|
+
child.once("error", (error) => {
|
|
336
|
+
if (settled)
|
|
337
|
+
return;
|
|
338
|
+
settled = true;
|
|
339
|
+
cleanup();
|
|
340
|
+
reject(error);
|
|
341
|
+
});
|
|
342
|
+
child.once("exit", (status) => {
|
|
343
|
+
if (settled)
|
|
344
|
+
return;
|
|
345
|
+
settled = true;
|
|
346
|
+
cleanup();
|
|
347
|
+
if (failure) {
|
|
348
|
+
reject(failure);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
resolve({ status, stdout, stderr });
|
|
352
|
+
});
|
|
353
|
+
}).finally(() => { trackPendingChild(null); });
|
|
354
|
+
}
|
|
355
|
+
async function runIdentityProcess(command, args, env) {
|
|
356
|
+
return runBoundedCliProcess(command, args, env, "Bot identity verification");
|
|
357
|
+
}
|
|
358
|
+
export async function main() {
|
|
359
|
+
if (has("--help") || has("-h")) {
|
|
360
|
+
say(`setup 内部机器人注册阶段
|
|
361
|
+
|
|
362
|
+
用法:
|
|
363
|
+
bun bot-register.mjs --auto
|
|
364
|
+
|
|
365
|
+
完成后自动:按返回 appId 判定新旧 → 同步并校验 lark-cli bot 凭证 →
|
|
366
|
+
原子写 bots/<appId>.json(0600)→ 绑定 Agent。`);
|
|
367
|
+
process.exit(0);
|
|
368
|
+
}
|
|
369
|
+
const autoSelect = has("--auto");
|
|
370
|
+
const commentSubscription = flag("--comment-subscription") || "preserve";
|
|
371
|
+
if (has("--comment-subscription") && commentSubscription === "preserve")
|
|
372
|
+
die("--comment-subscription 缺少参数值");
|
|
373
|
+
if (!["preserve", "none", "application"].includes(commentSubscription)) {
|
|
374
|
+
die("--comment-subscription 只支持 none 或 application");
|
|
375
|
+
}
|
|
376
|
+
const resultFileArg = flag("--result-file") || null;
|
|
377
|
+
const resultFile = resultFileArg ? path.resolve(resultFileArg) : null;
|
|
378
|
+
if (resultFile && (path.dirname(resultFile) !== path.resolve(CFG_DIR) || !/^\.setup-result-\d+\.json$/.test(path.basename(resultFile)))) {
|
|
379
|
+
die("--result-file 必须是配置根目录内的 .setup-result-<pid>.json");
|
|
380
|
+
}
|
|
381
|
+
if (argv.some((arg) => arg === "--app-id" || arg.startsWith("--app-id="))) {
|
|
382
|
+
die("不支持 --app-id;机器人必须在飞书网页中选择");
|
|
383
|
+
}
|
|
384
|
+
if (!autoSelect)
|
|
385
|
+
die("setup 注册阶段必须由交互式选择流程启动");
|
|
386
|
+
say("[setup 1/5] 在网页选择已有机器人或创建新机器人");
|
|
387
|
+
if (commentSubscription === "application") {
|
|
388
|
+
say(`! 已显式选择 ${commentSubscription} 维度评论订阅:一旦平台状态验证为已订阅,Bot 可见文档中实际送达的每条支持评论都会进入 Inbox 并唤醒 Agent,不要求 @Bot。`);
|
|
389
|
+
say("! setup 将通过官方 lark-cli 的结构化 API 请求创建该订阅,并以只读 subscription_status 二次核验;不会从意图或事件配置推断订阅已生效。");
|
|
390
|
+
}
|
|
391
|
+
else if (commentSubscription === "none") {
|
|
392
|
+
say("! 已显式选择 none:setup 将取消 application/Bot 维度的评论订阅,并用 subscription_status 核验;之后仅 @Bot 评论进入 Inbox。");
|
|
393
|
+
}
|
|
394
|
+
const TENANT_SCOPES = [
|
|
395
|
+
"im:message",
|
|
396
|
+
"im:message.p2p_msg:readonly",
|
|
397
|
+
"im:message.group_at_msg:readonly",
|
|
398
|
+
"im:message.group_msg",
|
|
399
|
+
"im:chat:readonly",
|
|
400
|
+
"im:chat:create",
|
|
401
|
+
"im:chat:update",
|
|
402
|
+
"im:chat.members:read",
|
|
403
|
+
"im:chat.members:write_only",
|
|
404
|
+
"im:resource",
|
|
405
|
+
"application:application:self_manage",
|
|
406
|
+
"contact:user.employee_id:readonly",
|
|
407
|
+
// drive.notice.comment_add_v1 and the stable comment read/reply APIs are
|
|
408
|
+
// currently delivered by Feishu under the aggregate drive tenant scope.
|
|
409
|
+
"drive:drive",
|
|
410
|
+
"docs:document.comment:read",
|
|
411
|
+
"docs:document.comment:create",
|
|
412
|
+
];
|
|
413
|
+
const TENANT_EVENTS = ["im.message.receive_v1", "im.message.message_read_v1", "drive.notice.comment_add_v1"];
|
|
414
|
+
let pollingCount = 0;
|
|
415
|
+
const result = await registerApp({
|
|
416
|
+
source: "larkin",
|
|
417
|
+
addons: {
|
|
418
|
+
scopes: { tenant: TENANT_SCOPES },
|
|
419
|
+
events: { items: { tenant: TENANT_EVENTS } },
|
|
420
|
+
callbacks: { items: ["card.action.trigger"] },
|
|
421
|
+
},
|
|
422
|
+
onQRCodeReady: ({ url, expireIn }) => {
|
|
423
|
+
qrcode.generate(url, { small: true }, (code) => say(code));
|
|
424
|
+
say(`\n打开以下链接(或扫码),${Math.round(expireIn / 60)} 分钟内有效:\n\n ${url}\n\n[setup 2/5] 等待飞书网页完成授权并回传凭证…`);
|
|
425
|
+
say(openBrowser(url) ? "[setup] 已在默认浏览器打开授权页" : "[setup] 未能自动打开浏览器,请手动打开上面的链接");
|
|
426
|
+
},
|
|
427
|
+
onStatusChange: ({ status, interval }) => {
|
|
428
|
+
if (status === "domain_switched")
|
|
429
|
+
say("[setup] 已切换到 Lark 域名");
|
|
430
|
+
if (status === "slow_down")
|
|
431
|
+
say(`[setup] 飞书要求降低轮询频率,${interval || "稍后"}秒后继续`);
|
|
432
|
+
if (status === "polling" && ++pollingCount % 12 === 0)
|
|
433
|
+
say(`[setup] 仍在等待飞书回传凭证(约 ${pollingCount / 12} 分钟)…`);
|
|
434
|
+
},
|
|
435
|
+
}).catch(() => die("网页授权失败;未执行凭证同步、文件写入或 Agent 绑定,请重试 setup"));
|
|
436
|
+
const { client_id: rawId, client_secret: rawSecret, user_info: userInfo } = result || {};
|
|
437
|
+
if (typeof rawId !== "string" || !APP_ID.test(rawId))
|
|
438
|
+
die("授权返回的 App ID 格式非法;未执行凭证同步、文件写入或 Agent 绑定");
|
|
439
|
+
if (typeof rawSecret !== "string" || rawSecret.length === 0)
|
|
440
|
+
die("授权完成但未取得有效凭证;未执行凭证同步、文件写入或 Agent 绑定");
|
|
441
|
+
const id = rawId;
|
|
442
|
+
const secret = rawSecret;
|
|
443
|
+
const botsDir = ensureSecureBotsDir();
|
|
444
|
+
say(`[setup 3/5] 正在按 App ID ${id} 刷新凭证并配置 Agent`);
|
|
445
|
+
const tenant = userInfo?.tenant_brand === "lark" ? "lark" : "feishu";
|
|
446
|
+
const botFile = path.join(botsDir, `${id}.json`);
|
|
447
|
+
const prior = (() => {
|
|
448
|
+
try {
|
|
449
|
+
return JSON.parse(fs.readFileSync(botFile, "utf8"));
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
return {};
|
|
453
|
+
}
|
|
454
|
+
})();
|
|
455
|
+
const requestedAt = new Date().toISOString();
|
|
456
|
+
const priorSubscription = documentCommentSubscriptionCapability(prior);
|
|
457
|
+
const documentCommentSubscription = commentSubscription === "application"
|
|
458
|
+
? { mode: "none", status: "requested-unverified", source: "setup-opt-in", dimension: commentSubscription, requestedAt }
|
|
459
|
+
: commentSubscription === "none" || !priorSubscription
|
|
460
|
+
? { mode: "none", status: "safe-default", source: "setup-default", updatedAt: requestedAt }
|
|
461
|
+
: priorSubscription;
|
|
462
|
+
if (!flag("--runtime") && (!testFixture || process.env.LARKIN_TEST_ENABLE_AGENT_CHOICE === "1")) {
|
|
463
|
+
const existing = larkinConfig.loadConfig(process.env).config.agents[id];
|
|
464
|
+
// The official resolver performs a synchronous login-shell probe. Resolve it
|
|
465
|
+
// exactly once before the credential heartbeat lock becomes active.
|
|
466
|
+
resolvedSetupOfficialCli = resolveOfficialCli({ env: process.env });
|
|
467
|
+
const questioner = terminalSetupQuestioner();
|
|
468
|
+
// One setup-owned transaction starts before status/logout and remains active
|
|
469
|
+
// through selection, login, bind, readiness, and the final setup commit.
|
|
470
|
+
pendingPiAuthTransaction = beginBuiltinPiCredentialTransaction(CFG_DIR, id);
|
|
471
|
+
const authServices = {
|
|
472
|
+
providers: async () => listOfficialPiAuthProviders(await createOfficialPiRegistryRuntime()),
|
|
473
|
+
status: async () => officialPiAuthStatus(await createOfficialPiCredentialRuntime(CFG_DIR, id)),
|
|
474
|
+
logout: async (providerId) => logoutOfficialPiProvider(await createOfficialPiLogoutRuntime(CFG_DIR, id), providerId),
|
|
475
|
+
report: (message) => say(message),
|
|
476
|
+
};
|
|
477
|
+
try {
|
|
478
|
+
const requested = await collectSetupAgentChoice(questioner, existing, authServices);
|
|
479
|
+
const choice = await recoverUnavailableExternalPi(requested, questioner, () => probeNativeRuntimeReadiness({
|
|
480
|
+
runtime: "pi", agentId: id, cwd: path.join(CFG_DIR, "agents", id), env: process.env,
|
|
481
|
+
}), (message) => say(`! ${message}`), authServices);
|
|
482
|
+
if (choice) {
|
|
483
|
+
let serializedChoice = choice;
|
|
484
|
+
if (choice.runtime === "pi" && choice.distribution === "builtin") {
|
|
485
|
+
const official = choice.preset === "official" ? choice : null;
|
|
486
|
+
const configured = official ? null : configureBuiltinPiProviderModel(CFG_DIR, id, choice);
|
|
487
|
+
const providerId = official?.providerId || configured.provider;
|
|
488
|
+
const authType = official?.authType || "api_key";
|
|
489
|
+
let piRuntime;
|
|
490
|
+
try {
|
|
491
|
+
say(`正在通过捆绑官方 Pi 登录 ${providerId}(${authType})…`);
|
|
492
|
+
piRuntime = await createOfficialPiModelRuntime(CFG_DIR, id);
|
|
493
|
+
await runOfficialPiLogin(piRuntime, providerId, authType, createOfficialPiAuthInteraction({ questioner, report: (message) => say(message), openUrl: openBrowser }));
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
pendingPiAuthTransaction.rollback();
|
|
497
|
+
pendingPiAuthTransaction = null;
|
|
498
|
+
throw new Error(`官方 Pi ${providerId} 登录失败或已取消;credential/config 未修改`);
|
|
499
|
+
}
|
|
500
|
+
if (process.env.LARKIN_TEST_SKIP_BUILTIN_PI_PROVIDER_TURN !== "1") {
|
|
501
|
+
say("正在验证内置 Pi provider(受控单轮,不发送飞书消息)…");
|
|
502
|
+
try {
|
|
503
|
+
await verifyOfficialPiProviderTurn(piRuntime, choice.model);
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
pendingPiAuthTransaction.rollback();
|
|
507
|
+
pendingPiAuthTransaction = null;
|
|
508
|
+
throw new Error(`官方 Pi ${providerId} readiness 失败;credential/config 未修改`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
serializedChoice = { ...choice, authCompleted: true, readinessCompleted: true };
|
|
512
|
+
}
|
|
513
|
+
temporaryAgentChoiceFile = path.join(CFG_DIR, `.setup-agent-choice-${process.pid}-${Date.now()}.json`);
|
|
514
|
+
fs.writeFileSync(temporaryAgentChoiceFile, `${JSON.stringify(serializedChoice)}\n`, { mode: 0o600, flag: "wx" });
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
finally {
|
|
518
|
+
questioner.close?.();
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const stagedBotFile = path.join(botsDir, `.${id}.${process.pid}.tmp`);
|
|
522
|
+
try {
|
|
523
|
+
fs.writeFileSync(stagedBotFile, `${JSON.stringify({
|
|
524
|
+
appId: id,
|
|
525
|
+
appSecret: secret,
|
|
526
|
+
tenant,
|
|
527
|
+
ownerOpenId: userInfo?.open_id || prior.ownerOpenId || null,
|
|
528
|
+
createdAt: prior.createdAt || new Date().toISOString(),
|
|
529
|
+
updatedAt: new Date().toISOString(),
|
|
530
|
+
capabilities: {
|
|
531
|
+
cardActionCallback: { status: "requested-unverified", requestedAt: new Date().toISOString() },
|
|
532
|
+
documentCommentEvent: {
|
|
533
|
+
status: "requested-unverified",
|
|
534
|
+
event: "drive.notice.comment_add_v1",
|
|
535
|
+
scope: "drive:drive",
|
|
536
|
+
requestedAt: new Date().toISOString(),
|
|
537
|
+
},
|
|
538
|
+
documentCommentReply: {
|
|
539
|
+
status: "requested-unverified",
|
|
540
|
+
scope: "docs:document.comment:create",
|
|
541
|
+
requestedAt: new Date().toISOString(),
|
|
542
|
+
},
|
|
543
|
+
documentCommentSubscription,
|
|
544
|
+
},
|
|
545
|
+
}, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
546
|
+
fs.renameSync(stagedBotFile, botFile);
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
try {
|
|
550
|
+
fs.unlinkSync(stagedBotFile);
|
|
551
|
+
}
|
|
552
|
+
catch { /* best effort */ }
|
|
553
|
+
die(`bot 凭证原子写入失败:${errorMessage(error)}`);
|
|
554
|
+
}
|
|
555
|
+
say(`✓ 凭证已写入 ${botFile}(0600,Secret 不回显)`);
|
|
556
|
+
say("! card.action.trigger 已请求但尚未证明生效;启动后运行 Agent CLI 的 interaction callback-probe,发送并点击验证卡,状态变为 verified-effective 后才能创建业务交互卡片。");
|
|
557
|
+
say("! drive.notice.comment_add_v1 + drive:drive 已请求但尚未真实验证;发布配置、授予 Bot 文档访问权并在测试文档评论中 @Bot 后,larkin agents 会显示事件是否到达及读取失败诊断。");
|
|
558
|
+
say("! docs:document.comment:create 已请求但尚未真实验证;它覆盖 in-thread reply 与 whole-document create_v2 fallback,真实回复成功前不声明生效。");
|
|
559
|
+
say(documentCommentSubscription.status === "platform-verified"
|
|
560
|
+
? `✓ 文档评论订阅已保留:${documentCommentSubscription.dimension} / platform-verified`
|
|
561
|
+
: `! 文档评论订阅=${documentCommentSubscription.status};未验证前仅 @Bot 评论进入 Inbox。`);
|
|
562
|
+
const targetAgent = flag("--agent") || id;
|
|
563
|
+
const bindArgs = ["--profile", id, "--yes", "--agent", targetAgent];
|
|
564
|
+
const runtime = flag("--runtime");
|
|
565
|
+
if (runtime)
|
|
566
|
+
bindArgs.push("--runtime", runtime);
|
|
567
|
+
if (temporaryAgentChoiceFile)
|
|
568
|
+
bindArgs.push("--selection-file", temporaryAgentChoiceFile);
|
|
569
|
+
const bindSpec = internalCommandSpec("setup-bind", bindArgs, process.env);
|
|
570
|
+
const bindStatus = await runBindProcess(bindSpec.command, bindSpec.args);
|
|
571
|
+
if (bindStatus !== 0) {
|
|
572
|
+
pendingPiAuthTransaction?.rollback();
|
|
573
|
+
pendingPiAuthTransaction = null;
|
|
574
|
+
die("新 bot 凭证已发布但 Agent 绑定失败;权威凭证状态已保留,请重跑 larkin setup 并在网页中重新选择机器人");
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
const loaded = larkinConfig.loadConfig(process.env);
|
|
578
|
+
const stored = loaded.config.agents[targetAgent];
|
|
579
|
+
if (!stored)
|
|
580
|
+
throw new Error(`Agent ${targetAgent} 不存在于 canonical config`);
|
|
581
|
+
const agent = hydrateRuntimeAgent(loaded.configDir, stored);
|
|
582
|
+
const profileEnv = { ...process.env, LARKIN_CONFIG_DIR: loaded.configDir };
|
|
583
|
+
const official = officialCliForProfile(profileEnv);
|
|
584
|
+
if (testFixture?.syncAgentProfile)
|
|
585
|
+
synchronizeAgentProfile(agent, profileEnv, { forceRebind: true });
|
|
586
|
+
else
|
|
587
|
+
await syncAgentProfileAsync(agent, profileEnv, {
|
|
588
|
+
forceRebind: true,
|
|
589
|
+
timeoutMs: CHILD_TIMEOUT_MS,
|
|
590
|
+
maxOutputBytes: CHILD_MAX_OUTPUT_BYTES,
|
|
591
|
+
signal: shutdownController.signal,
|
|
592
|
+
resolveOfficialCli: () => official,
|
|
593
|
+
onChild(child) { trackPendingChild(child); },
|
|
594
|
+
});
|
|
595
|
+
const cliEnv = managedLarkCliEnv(agent, process.env);
|
|
596
|
+
let verified = false;
|
|
597
|
+
for (let index = 0; index <= BOT_VERIFY_BACKOFF_MS.length; index += 1) {
|
|
598
|
+
const result = await runIdentityProcess(official.command, [...official.argsPrefix, "im", "+chat-list", "--as", "bot"], cliEnv);
|
|
599
|
+
let envelope;
|
|
600
|
+
try {
|
|
601
|
+
envelope = JSON.parse(result.stdout || "");
|
|
602
|
+
}
|
|
603
|
+
catch { /* fail closed below */ }
|
|
604
|
+
if (result.status === 0 && envelope?.ok === true && envelope.identity === "bot") {
|
|
605
|
+
verified = true;
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
if (!botVerificationRetryable(result) || index === BOT_VERIFY_BACKOFF_MS.length)
|
|
609
|
+
break;
|
|
610
|
+
const delay = BOT_VERIFY_BACKOFF_MS[index];
|
|
611
|
+
say(`! 新应用 Bot identity 尚未就绪,${delay}ms 后重试 ${index + 2}/${BOT_VERIFY_BACKOFF_MS.length + 1}`);
|
|
612
|
+
await wait(delay);
|
|
613
|
+
}
|
|
614
|
+
if (!verified)
|
|
615
|
+
throw new Error("Bot identity verification failed");
|
|
616
|
+
if (commentSubscription !== "preserve") {
|
|
617
|
+
const reconciled = await applyDocumentCommentSubscription({
|
|
618
|
+
mode: commentSubscription,
|
|
619
|
+
command: official.command,
|
|
620
|
+
argsPrefix: official.argsPrefix,
|
|
621
|
+
env: cliEnv,
|
|
622
|
+
markVerified: () => { markDocumentCommentSubscriptionVerified(CFG_DIR, id, "application"); },
|
|
623
|
+
});
|
|
624
|
+
if (reconciled.subscribed && reconciled.dimension) {
|
|
625
|
+
say(`✓ 文档评论订阅已由 subscription_status 验证:${reconciled.dimension}`);
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
say(`✓ ${reconciled.dimension} 维度文档评论订阅已取消并由 subscription_status 验证;仅 @Bot 评论进入 Inbox。`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
catch (error) {
|
|
633
|
+
const diagnostic = errorMessage(error);
|
|
634
|
+
if (diagnostic.startsWith("document comment application subscription")
|
|
635
|
+
|| diagnostic.startsWith("document comment local verified-state persistence"))
|
|
636
|
+
say(`! ${diagnostic}`);
|
|
637
|
+
die("Agent lark-channel binding/凭证校验失败或评论订阅核验失败;安全本地状态与权威 bot 凭证已保留,请检查身份授权后重跑 larkin setup");
|
|
638
|
+
}
|
|
639
|
+
say("✓ 官方 lark-channel workspace 已绑定(identity=bot-only),Bot 凭证校验通过");
|
|
640
|
+
say(`\n[setup 4/5] ✓ Agent ${id} 已配置`);
|
|
641
|
+
if (resultFile) {
|
|
642
|
+
try {
|
|
643
|
+
fs.writeFileSync(resultFile, `${JSON.stringify({ agentId: id })}\n`, { mode: 0o600, flag: "wx" });
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
die("Agent 绑定与新 bot 凭证已保留,但 setup 结果写入失败;请重跑 larkin setup 并在网页中重新选择机器人");
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
pendingPiAuthTransaction?.commit();
|
|
650
|
+
pendingPiAuthTransaction = null;
|
|
651
|
+
}
|
|
652
|
+
if (path.resolve(process.argv[1] || "") === path.resolve(fileURLToPath(import.meta.url))) {
|
|
653
|
+
main().catch((error) => { if (!requestedShutdown)
|
|
654
|
+
die(errorMessage(error)); });
|
|
655
|
+
}
|