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,83 @@
|
|
|
1
|
+
import { loadConfig } from "../platform/config.mjs";
|
|
2
|
+
import { requestSessionReset } from "./local-control.mjs";
|
|
3
|
+
function parseResetArguments(args) {
|
|
4
|
+
if (args[0] !== "reset")
|
|
5
|
+
throw new Error("unsupported session subcommand");
|
|
6
|
+
const values = new Map();
|
|
7
|
+
let json = false;
|
|
8
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
9
|
+
const token = args[index];
|
|
10
|
+
if (token === "--json") {
|
|
11
|
+
if (json)
|
|
12
|
+
throw new Error("duplicate flag: --json");
|
|
13
|
+
json = true;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (!["--agent", "--wait-ready"].includes(token)) {
|
|
17
|
+
throw new Error(token.startsWith("-") ? `unknown flag: ${token}` : `unexpected positional: ${token}`);
|
|
18
|
+
}
|
|
19
|
+
if (values.has(token))
|
|
20
|
+
throw new Error(`duplicate flag: ${token}`);
|
|
21
|
+
const value = args[index + 1];
|
|
22
|
+
if (!value || value.startsWith("-"))
|
|
23
|
+
throw new Error(`missing value: ${token}`);
|
|
24
|
+
values.set(token, value);
|
|
25
|
+
index += 1;
|
|
26
|
+
}
|
|
27
|
+
if (!json)
|
|
28
|
+
throw new Error("--json is required");
|
|
29
|
+
const agentId = values.get("--agent");
|
|
30
|
+
if (!agentId || !/^cli_[A-Za-z0-9]+$/.test(agentId))
|
|
31
|
+
throw new Error("--agent requires an exact App ID");
|
|
32
|
+
const waitSeconds = Number(values.get("--wait-ready") ?? "30");
|
|
33
|
+
if (!Number.isFinite(waitSeconds) || waitSeconds < 0 || waitSeconds > 300)
|
|
34
|
+
throw new Error("--wait-ready must be 0..300 seconds");
|
|
35
|
+
return { agentId, waitReadyMs: Math.round(waitSeconds * 1000) };
|
|
36
|
+
}
|
|
37
|
+
export async function runSessionCli(args, env = process.env, dependencies = {}) {
|
|
38
|
+
const io = dependencies.io ?? { stdout: (value) => process.stdout.write(value), stderr: (value) => process.stderr.write(value) };
|
|
39
|
+
const fail = (code, message, agentId = "invalid") => {
|
|
40
|
+
io.stdout(`${JSON.stringify({ ok: false, agent_id: agentId, code, error: message }, null, 2)}\n`);
|
|
41
|
+
return 1;
|
|
42
|
+
};
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = parseResetArguments(args);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return fail("invalid_arguments", error instanceof Error ? error.message : String(error));
|
|
49
|
+
}
|
|
50
|
+
const agentId = parsed.agentId;
|
|
51
|
+
if (typeof env.LARKIN_AGENT_ID === "string" && env.LARKIN_AGENT_ID.trim()) {
|
|
52
|
+
return fail("user_authority_required", "session reset is available only from a user terminal", agentId);
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const loaded = loadConfig(env);
|
|
56
|
+
const result = await (dependencies.request ?? requestSessionReset)({
|
|
57
|
+
larkinHome: loaded.config.larkinHome, agentId, waitReadyMs: parsed.waitReadyMs,
|
|
58
|
+
});
|
|
59
|
+
const output = {
|
|
60
|
+
ok: result.ok, agent_id: result.agentId,
|
|
61
|
+
reset_committed: result.resetCommitted, generation_changed: result.generationChanged,
|
|
62
|
+
session_changed: result.sessionChanged, turns: result.turns, runtime_ready: result.runtimeReady,
|
|
63
|
+
channel_connected: result.channelConnected, reconnecting: result.reconnecting,
|
|
64
|
+
pending_count: result.pendingCount, ready_for_fresh_scenario: result.readyForFreshScenario,
|
|
65
|
+
inbound_observed: result.inboundObserved, ...(result.code ? { code: result.code } : {}),
|
|
66
|
+
...(result.error ? { error: result.error } : {}), ...(result.readiness ? { readiness: result.readiness } : {}),
|
|
67
|
+
};
|
|
68
|
+
io.stdout(`${JSON.stringify(output, null, 2)}\n`);
|
|
69
|
+
return result.ok ? 0 : 1;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
73
|
+
const output = {
|
|
74
|
+
ok: false, agent_id: agentId,
|
|
75
|
+
code: /timeout/i.test(message) ? "control_timeout" : "control_unavailable", error: message,
|
|
76
|
+
};
|
|
77
|
+
io.stdout(`${JSON.stringify(output, null, 2)}\n`);
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export async function main(args = process.argv.slice(2), env = process.env) {
|
|
82
|
+
process.exitCode = await runSessionCli(args, env);
|
|
83
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readProcessState } from "../platform/process-state.mjs";
|
|
3
|
+
export async function waitForOwnedDashboard(configDir, { timeoutMs = 15_000, pollMs = 100, readDashboard = (root) => readProcessState(root).dashboard, } = {}) {
|
|
4
|
+
const deadline = Date.now() + timeoutMs;
|
|
5
|
+
do {
|
|
6
|
+
const state = readDashboard(configDir);
|
|
7
|
+
if (state.state === "owned" && typeof state.url === "string" && state.url) {
|
|
8
|
+
return { state: "owned", url: state.url };
|
|
9
|
+
}
|
|
10
|
+
if (Date.now() >= deadline)
|
|
11
|
+
break;
|
|
12
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
13
|
+
} while (true);
|
|
14
|
+
return { state: "timeout" };
|
|
15
|
+
}
|
|
16
|
+
export function openBrowser(url) {
|
|
17
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
18
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
19
|
+
return spawnSync(command, args, { stdio: "ignore" }).status === 0;
|
|
20
|
+
}
|
|
21
|
+
export async function openOwnedDashboardWhenReady(configDir, { opener = openBrowser, ...readinessOptions } = {}) {
|
|
22
|
+
const readiness = await waitForOwnedDashboard(configDir, readinessOptions);
|
|
23
|
+
if (readiness.state !== "owned" || !readiness.url)
|
|
24
|
+
return { readiness, opened: false };
|
|
25
|
+
return { readiness, opened: opener(readiness.url) };
|
|
26
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import "../platform/check-bun-version.cjs";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import readline from "node:readline/promises";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import * as larkinConfig from "../platform/config.mjs";
|
|
10
|
+
import { acquireProcessLock, readProcessState } from "../platform/process-state.mjs";
|
|
11
|
+
import { requestAgentUpsert } from "./local-control.mjs";
|
|
12
|
+
import { openOwnedDashboardWhenReady } from "./setup-dashboard.mjs";
|
|
13
|
+
import { probeNativeRuntimeReadiness } from "../runtime/runtime-readiness.mjs";
|
|
14
|
+
import { internalCommandSpec, processCommandToken } from "./internal-command.mjs";
|
|
15
|
+
import { ensureOfficialLarkCliForSetup, } from "./official-lark-cli.mjs";
|
|
16
|
+
const CFG_DIR = process.env.LARKIN_CONFIG_DIR || path.join(os.homedir(), ".larkin");
|
|
17
|
+
const argv = process.argv.slice(2);
|
|
18
|
+
const has = (name) => argv.includes(name);
|
|
19
|
+
const flag = (name) => { const index = argv.indexOf(name); return index >= 0 ? argv[index + 1] : undefined; };
|
|
20
|
+
const say = (...args) => console.log(...args);
|
|
21
|
+
const die = (message) => { console.error(`✗ ${message}`); process.exit(1); };
|
|
22
|
+
if (argv.some((arg) => arg === "--app-id" || arg.startsWith("--app-id="))) {
|
|
23
|
+
die("setup 不支持 --app-id;请运行 larkin setup 并在飞书网页中选择机器人");
|
|
24
|
+
}
|
|
25
|
+
if (argv.some((arg) => arg === "--no-dashboard" || arg.startsWith("--no-dashboard="))) {
|
|
26
|
+
die("--no-dashboard 已移除;dashboard 由 larkin start 统一管理");
|
|
27
|
+
}
|
|
28
|
+
if (has("--start") && has("--no-start"))
|
|
29
|
+
die("--start 与 --no-start 不能同时使用");
|
|
30
|
+
const commentSubscription = flag("--comment-subscription") || null;
|
|
31
|
+
if (has("--comment-subscription") && !commentSubscription)
|
|
32
|
+
die("--comment-subscription 缺少参数值");
|
|
33
|
+
if (commentSubscription && !["none", "application"].includes(commentSubscription)) {
|
|
34
|
+
die("--comment-subscription 只支持 none 或 application");
|
|
35
|
+
}
|
|
36
|
+
const OPT = { runtime: flag("--runtime") || null, commentSubscription, start: !has("--no-start"), help: has("--help") || has("-h") };
|
|
37
|
+
if (OPT.help) {
|
|
38
|
+
say(`larkin setup — Create or connect a Feishu bot, then configure and attach its Agent
|
|
39
|
+
|
|
40
|
+
Usage:
|
|
41
|
+
larkin setup Select a bot in the browser and run setup
|
|
42
|
+
|
|
43
|
+
Options:
|
|
44
|
+
--runtime <runtime> Select the Agent runtime
|
|
45
|
+
--comment-subscription <mode> none (safe default) or application
|
|
46
|
+
--no-start Configure the Agent without starting or attaching it
|
|
47
|
+
|
|
48
|
+
setup handles browser authorization, permission grants, credential storage, Agent configuration,
|
|
49
|
+
and target-only hot attach. Each Agent is identified by its bot App ID: selecting the same bot reuses
|
|
50
|
+
its existing Agent, memory, and state; creating a new bot creates a new Agent.
|
|
51
|
+
|
|
52
|
+
If larkin is running, only the selected Agent is added or updated. Otherwise setup starts the same
|
|
53
|
+
daemon + dashboard supervisor used by larkin start.`);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
function runForeground(mode, args) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const childSpec = internalCommandSpec(mode, args);
|
|
59
|
+
const child = spawn(childSpec.command, childSpec.args, { env: process.env, stdio: "inherit" });
|
|
60
|
+
const forward = (signal) => { if (child.exitCode === null && child.signalCode === null)
|
|
61
|
+
child.kill(signal); };
|
|
62
|
+
const onInt = () => forward("SIGINT");
|
|
63
|
+
const onTerm = () => forward("SIGTERM");
|
|
64
|
+
process.on("SIGINT", onInt);
|
|
65
|
+
process.on("SIGTERM", onTerm);
|
|
66
|
+
const cleanup = () => { process.off("SIGINT", onInt); process.off("SIGTERM", onTerm); };
|
|
67
|
+
child.once("error", (error) => { cleanup(); reject(error); });
|
|
68
|
+
child.once("exit", (code, signal) => { cleanup(); resolve({ code, signal }); });
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async function openOwnedDashboard() {
|
|
72
|
+
const result = await openOwnedDashboardWhenReady(CFG_DIR);
|
|
73
|
+
if (result.readiness.state !== "owned" || !result.readiness.url) {
|
|
74
|
+
say("! dashboard 尚未在等待期限内就绪;稍后可运行 larkin status 查看 URL");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
say(`✓ dashboard:${result.readiness.url}`);
|
|
78
|
+
if (!result.opened)
|
|
79
|
+
say(`! 未能自动打开浏览器,请手动打开 ${result.readiness.url}`);
|
|
80
|
+
}
|
|
81
|
+
export async function main() {
|
|
82
|
+
const mutationLock = acquireProcessLock(path.join(CFG_DIR, "setup.lock.json"), processCommandToken("setup", "app/setup.mjs"));
|
|
83
|
+
const releaseMutationLock = mutationLock.release;
|
|
84
|
+
process.on("exit", releaseMutationLock);
|
|
85
|
+
const official = await ensureOfficialLarkCliForSetup({
|
|
86
|
+
env: process.env,
|
|
87
|
+
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
88
|
+
async confirmInstall(command) {
|
|
89
|
+
say("[setup 0/5] Larkin 需要未修改的官方 lark-cli 作为 Feishu 命令下游。");
|
|
90
|
+
say(`将执行:${command}`);
|
|
91
|
+
const input = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
92
|
+
const answer = (await input.question("是否安装?[y/N] ")).trim().toLowerCase();
|
|
93
|
+
input.close();
|
|
94
|
+
return answer === "y" || answer === "yes";
|
|
95
|
+
},
|
|
96
|
+
}).catch((error) => die(error instanceof Error ? error.message : String(error)));
|
|
97
|
+
say(`[setup 0/5] ✓ 官方 lark-cli ${official.command.version}: ${official.command.command}${official.installed ? "(刚刚安装)" : ""}`);
|
|
98
|
+
say("\nAgent 与飞书机器人按 App ID 一一对应:");
|
|
99
|
+
say(" • 网页选择同一个机器人 → 热更新该 Agent,不重启其他 Agent");
|
|
100
|
+
say(" • 网页创建新机器人 → 热挂载新 Agent,状态彼此独立\n");
|
|
101
|
+
fs.mkdirSync(CFG_DIR, { recursive: true, mode: 0o700 });
|
|
102
|
+
const resultFile = path.join(CFG_DIR, `.setup-result-${process.pid}.json`);
|
|
103
|
+
const registerArgs = ["--auto", "--result-file", resultFile];
|
|
104
|
+
if (OPT.runtime)
|
|
105
|
+
registerArgs.push("--runtime", OPT.runtime);
|
|
106
|
+
if (OPT.commentSubscription)
|
|
107
|
+
registerArgs.push("--comment-subscription", OPT.commentSubscription);
|
|
108
|
+
const result = await runForeground("bot-register", registerArgs);
|
|
109
|
+
if (result.code !== 0)
|
|
110
|
+
die("机器人授权或 Agent 配置未完成");
|
|
111
|
+
let agentId;
|
|
112
|
+
try {
|
|
113
|
+
({ agentId } = JSON.parse(fs.readFileSync(resultFile, "utf8")));
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
try {
|
|
117
|
+
fs.unlinkSync(resultFile);
|
|
118
|
+
}
|
|
119
|
+
catch { /* best effort */ }
|
|
120
|
+
}
|
|
121
|
+
if (!agentId)
|
|
122
|
+
die("setup 完成但没有取得 Agent ID");
|
|
123
|
+
const selectedAgentId = agentId;
|
|
124
|
+
const { config: configured } = larkinConfig.loadConfig(process.env);
|
|
125
|
+
const configuredAgent = configured.agents[selectedAgentId];
|
|
126
|
+
if (!configuredAgent)
|
|
127
|
+
die(`setup 完成但 Agent ${selectedAgentId} 配置不可读`);
|
|
128
|
+
if (!["codex", "claude", "pi"].includes(configuredAgent.runtime))
|
|
129
|
+
die(`Runtime ${configuredAgent.runtime} 不受支持`);
|
|
130
|
+
const runtimeReadiness = await probeNativeRuntimeReadiness({ runtime: configuredAgent.runtime,
|
|
131
|
+
agentId: selectedAgentId, cwd: configuredAgent.workspaceDir,
|
|
132
|
+
env: { ...process.env, LARKIN_CONFIG_DIR: CFG_DIR,
|
|
133
|
+
...(configuredAgent.piDistribution ? { LARKIN_PI_DISTRIBUTION: configuredAgent.piDistribution } : {}) } });
|
|
134
|
+
if (runtimeReadiness.state !== "ready") {
|
|
135
|
+
die(`Runtime ${configuredAgent.runtime} ${runtimeReadiness.state}:${runtimeReadiness.reason || "prerequisite unavailable"};${runtimeReadiness.nextAction || "修复后重试"}`);
|
|
136
|
+
}
|
|
137
|
+
if (!OPT.start) {
|
|
138
|
+
say(`\n[setup 5/5] ✓ 配置完成。运行 larkin start 启动。`);
|
|
139
|
+
releaseMutationLock();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const state = readProcessState(CFG_DIR);
|
|
143
|
+
if (state.daemon.state === "unknown" || state.supervisor.state === "unknown") {
|
|
144
|
+
throw new Error("现有 Larkin 进程身份无法确认;拒绝热挂载或并行启动");
|
|
145
|
+
}
|
|
146
|
+
if (state.daemon.state === "owned") {
|
|
147
|
+
say(`\n[setup 5/5] 正在向 daemon PID ${state.daemon.pid} 热挂载 Agent ${agentId}…`);
|
|
148
|
+
const response = await requestAgentUpsert({ larkinHome: CFG_DIR, agentId: selectedAgentId });
|
|
149
|
+
if (!response.ok)
|
|
150
|
+
die(`Agent ${agentId} 热挂载失败:${response.error || "unknown error"}`);
|
|
151
|
+
releaseMutationLock();
|
|
152
|
+
say(`[setup 5/5] ✓ Agent ${agentId} 已热挂载/更新;其他 Agent、session、连接和任务未重启`);
|
|
153
|
+
await openOwnedDashboard();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
say(`\n[setup 5/5] 正在通过统一 supervisor 启动 daemon + dashboard(Agent ${agentId})…`);
|
|
157
|
+
releaseMutationLock();
|
|
158
|
+
void openOwnedDashboard().catch((error) => say(`! dashboard 自动打开失败:${error instanceof Error ? error.message : String(error)}`));
|
|
159
|
+
const started = await runForeground("run", []);
|
|
160
|
+
if (started.code !== 0 && started.signal !== "SIGINT" && started.signal !== "SIGTERM") {
|
|
161
|
+
die(`Larkin 启动失败(exit=${started.code ?? started.signal}),请运行 larkin status 查看详情`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (path.resolve(process.argv[1] || "") === path.resolve(fileURLToPath(import.meta.url))) {
|
|
165
|
+
main().catch((error) => { console.error(`✗ ${error instanceof Error ? error.message : String(error)}`); process.exit(1); });
|
|
166
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { loadTelemetryConfig, safeEndpointLabel, validateOtlpEndpoint } from "../platform/telemetry-config.mjs";
|
|
4
|
+
import { TelemetrySpool } from "../platform/telemetry-spool.mjs";
|
|
5
|
+
import { flushTelemetry } from "../platform/telemetry-uploader.mjs";
|
|
6
|
+
function option(argv, name) {
|
|
7
|
+
const index = argv.indexOf(name);
|
|
8
|
+
if (index < 0)
|
|
9
|
+
return undefined;
|
|
10
|
+
const value = argv[index + 1];
|
|
11
|
+
if (!value || value.startsWith("--"))
|
|
12
|
+
throw new Error(`${name} requires a value`);
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function headers(argv) {
|
|
16
|
+
const output = {};
|
|
17
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
18
|
+
if (argv[index] !== "--header")
|
|
19
|
+
continue;
|
|
20
|
+
const value = argv[++index];
|
|
21
|
+
const separator = value?.indexOf("=") ?? -1;
|
|
22
|
+
if (!value || separator <= 0 || /[\r\n]/.test(value))
|
|
23
|
+
throw new Error("--header must use name=value");
|
|
24
|
+
output[value.slice(0, separator)] = value.slice(separator + 1);
|
|
25
|
+
}
|
|
26
|
+
return output;
|
|
27
|
+
}
|
|
28
|
+
const print = (value) => { process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); };
|
|
29
|
+
async function run(argv, env) {
|
|
30
|
+
const [operation = "status"] = argv;
|
|
31
|
+
const config = loadTelemetryConfig(env);
|
|
32
|
+
const spool = new TelemetrySpool(config);
|
|
33
|
+
if (operation === "status") {
|
|
34
|
+
print({ endpoint: safeEndpointLabel(config.endpoint), autoUpload: Boolean(config.endpoint), ...spool.status() });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (operation === "export") {
|
|
38
|
+
const destination = path.resolve(option(argv, "--output") || `larkin-telemetry-${Date.now()}.json.gz`);
|
|
39
|
+
const result = spool.exportBundle(destination);
|
|
40
|
+
print({ operation, output: path.basename(destination), ...result, sourceQueueRetained: true });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (operation === "import") {
|
|
44
|
+
const source = option(argv, "--input") || argv[1];
|
|
45
|
+
if (!source)
|
|
46
|
+
throw new Error("telemetry import requires --input <bundle>");
|
|
47
|
+
print({ operation, ...spool.importBundle(path.resolve(source)) });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (operation === "flush") {
|
|
51
|
+
const endpoint = option(argv, "--endpoint") ? validateOtlpEndpoint(option(argv, "--endpoint")) : config.endpoint;
|
|
52
|
+
if (!endpoint)
|
|
53
|
+
throw new Error("telemetry flush requires --endpoint or LARKIN_TELEMETRY_OTLP_ENDPOINT");
|
|
54
|
+
const result = await flushTelemetry(spool, { endpoint, headers: { ...config.headers, ...headers(argv) }, timeoutMs: config.requestTimeoutMs });
|
|
55
|
+
print({ operation, endpoint: safeEndpointLabel(endpoint), ...result, ...spool.status() });
|
|
56
|
+
if (result.status === "retained")
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (operation === "help" || operation === "--help" || operation === "-h") {
|
|
61
|
+
process.stdout.write(`Usage:
|
|
62
|
+
larkin telemetry status
|
|
63
|
+
larkin telemetry export --output <bundle.json.gz>
|
|
64
|
+
larkin telemetry import --input <bundle.json.gz>
|
|
65
|
+
larkin telemetry flush [--endpoint <http(s)://host:port/v1/traces>] [--header name=value]\n`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
throw new Error("unknown telemetry operation");
|
|
69
|
+
}
|
|
70
|
+
export async function main(argv = process.argv.slice(2), env = process.env) {
|
|
71
|
+
try {
|
|
72
|
+
await run(argv, env);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const message = error instanceof Error ? error.message : "telemetry operation failed";
|
|
76
|
+
if (/^(?:unknown telemetry operation|telemetry (?:import|flush) requires|--(?:input|output|endpoint|header) |OTLP (?:endpoint|header))/.test(message))
|
|
77
|
+
throw new Error(message);
|
|
78
|
+
throw new Error("telemetry operation failed");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (path.resolve(process.argv[1] || "") === path.resolve(import.meta.filename)) {
|
|
82
|
+
main().catch((error) => { process.stderr.write(`larkin telemetry: ${error instanceof Error ? error.message : String(error)}\n`); process.exitCode = 1; });
|
|
83
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const MIME_TYPES = {
|
|
4
|
+
"larkin-mark.svg": "image/svg+xml",
|
|
5
|
+
"dashboard.css": "text/css; charset=utf-8",
|
|
6
|
+
"dashboard.js": "text/javascript; charset=utf-8",
|
|
7
|
+
};
|
|
8
|
+
export function dashboardAsset(name, root) {
|
|
9
|
+
const embedded = globalThis.__LARKIN_EMBEDDED_DASHBOARD_ASSETS__?.[name];
|
|
10
|
+
if (embedded)
|
|
11
|
+
return { body: embedded, contentType: MIME_TYPES[name], embedded: true };
|
|
12
|
+
const file = name === "larkin-mark.svg"
|
|
13
|
+
? path.join(root, "assets", name)
|
|
14
|
+
: path.join(root, "dist", "dashboard", "web", "assets", name);
|
|
15
|
+
try {
|
|
16
|
+
return { body: fs.readFileSync(file), contentType: MIME_TYPES[name], embedded: false };
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
const MAX_AVATAR_BYTES = 2_000_000;
|
|
3
|
+
const DEFAULT_TTL_MS = 5 * 60 * 1_000;
|
|
4
|
+
const ALLOWED_CONTENT_TYPES = new Set(["image/avif", "image/gif", "image/jpeg", "image/png", "image/webp"]);
|
|
5
|
+
export function isAllowedDashboardAvatarUrl(value) {
|
|
6
|
+
if (typeof value !== "string")
|
|
7
|
+
return false;
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(value);
|
|
10
|
+
return url.protocol === "https:"
|
|
11
|
+
&& url.username === ""
|
|
12
|
+
&& url.password === ""
|
|
13
|
+
&& (url.port === "" || url.port === "443")
|
|
14
|
+
&& /^s\d+-imfile\.feishucdn\.com$/i.test(url.hostname)
|
|
15
|
+
&& url.pathname.startsWith("/static-resource/v1/");
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function readBoundedBody(response) {
|
|
22
|
+
const declared = Number(response.headers.get("content-length"));
|
|
23
|
+
if (Number.isFinite(declared) && declared > MAX_AVATAR_BYTES) {
|
|
24
|
+
throw new Error(`avatar exceeds ${MAX_AVATAR_BYTES} bytes`);
|
|
25
|
+
}
|
|
26
|
+
if (!response.body)
|
|
27
|
+
throw new Error("avatar response has no body");
|
|
28
|
+
const chunks = [];
|
|
29
|
+
let total = 0;
|
|
30
|
+
const reader = response.body.getReader();
|
|
31
|
+
while (true) {
|
|
32
|
+
const { done, value } = await reader.read();
|
|
33
|
+
if (done)
|
|
34
|
+
break;
|
|
35
|
+
total += value.byteLength;
|
|
36
|
+
if (total > MAX_AVATAR_BYTES) {
|
|
37
|
+
await reader.cancel();
|
|
38
|
+
throw new Error(`avatar exceeds ${MAX_AVATAR_BYTES} bytes`);
|
|
39
|
+
}
|
|
40
|
+
chunks.push(Buffer.from(value));
|
|
41
|
+
}
|
|
42
|
+
return Buffer.concat(chunks, total);
|
|
43
|
+
}
|
|
44
|
+
async function fetchDashboardAvatar(sourceUrl, fetchImpl) {
|
|
45
|
+
if (!isAllowedDashboardAvatarUrl(sourceUrl))
|
|
46
|
+
throw new Error("avatar source is not allowed");
|
|
47
|
+
const response = await fetchImpl(sourceUrl, {
|
|
48
|
+
headers: { Accept: "image/avif,image/webp,image/png,image/jpeg,image/gif" },
|
|
49
|
+
redirect: "error",
|
|
50
|
+
signal: AbortSignal.timeout(5_000),
|
|
51
|
+
});
|
|
52
|
+
if (!response.ok)
|
|
53
|
+
throw new Error(`avatar upstream returned ${response.status}`);
|
|
54
|
+
const contentType = (response.headers.get("content-type") || "").split(";", 1)[0].trim().toLowerCase();
|
|
55
|
+
if (!ALLOWED_CONTENT_TYPES.has(contentType))
|
|
56
|
+
throw new Error("unsupported avatar content type");
|
|
57
|
+
const body = await readBoundedBody(response);
|
|
58
|
+
const digest = crypto.createHash("sha256").update(body).digest("hex");
|
|
59
|
+
return { body, contentType, etag: `"sha256-${digest}"` };
|
|
60
|
+
}
|
|
61
|
+
export function createDashboardAvatarCache(options = {}) {
|
|
62
|
+
const fetchImpl = options.fetchImpl || fetch;
|
|
63
|
+
const now = options.now || Date.now;
|
|
64
|
+
const ttlMs = options.ttlMs || DEFAULT_TTL_MS;
|
|
65
|
+
const entries = new Map();
|
|
66
|
+
return {
|
|
67
|
+
async get(agentId, sourceUrl) {
|
|
68
|
+
const cached = entries.get(agentId);
|
|
69
|
+
if (cached && cached.sourceUrl === sourceUrl && cached.expiresAt > now())
|
|
70
|
+
return cached.payload;
|
|
71
|
+
const payload = await fetchDashboardAvatar(sourceUrl, fetchImpl);
|
|
72
|
+
entries.set(agentId, { sourceUrl, expiresAt: now() + ttlMs, payload });
|
|
73
|
+
return payload;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function createDashboardAvatarController(options) {
|
|
78
|
+
const cache = createDashboardAvatarCache({ fetchImpl: options.fetchImpl });
|
|
79
|
+
return {
|
|
80
|
+
async handle(req, res, requestUrl) {
|
|
81
|
+
const match = /^\/api\/avatar\/([^/]+)$/.exec(requestUrl.pathname);
|
|
82
|
+
if (!match)
|
|
83
|
+
return false;
|
|
84
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
85
|
+
res.writeHead(405, { Allow: "GET, HEAD", "Cache-Control": "no-store" });
|
|
86
|
+
res.end();
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
let agentId;
|
|
90
|
+
try {
|
|
91
|
+
agentId = decodeURIComponent(match[1]);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
res.writeHead(400, { "Cache-Control": "no-store" });
|
|
95
|
+
res.end("bad request");
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const sourceUrl = options.resolveSource(agentId);
|
|
100
|
+
if (!sourceUrl) {
|
|
101
|
+
res.writeHead(404, { "Cache-Control": "no-store" });
|
|
102
|
+
res.end("not found");
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
const payload = await cache.get(agentId, sourceUrl);
|
|
106
|
+
if (req.headers["if-none-match"] === payload.etag) {
|
|
107
|
+
res.writeHead(304, { ETag: payload.etag });
|
|
108
|
+
res.end();
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
res.writeHead(200, {
|
|
112
|
+
"Content-Type": payload.contentType,
|
|
113
|
+
"Content-Length": payload.body.byteLength,
|
|
114
|
+
"Cache-Control": "private, max-age=60, must-revalidate",
|
|
115
|
+
"X-Content-Type-Options": "nosniff",
|
|
116
|
+
"Content-Security-Policy": "default-src 'none'; sandbox",
|
|
117
|
+
ETag: payload.etag,
|
|
118
|
+
});
|
|
119
|
+
res.end(req.method === "HEAD" ? undefined : payload.body);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
res.writeHead(502, { "Cache-Control": "no-store" });
|
|
123
|
+
res.end("avatar unavailable");
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|