larkway 0.3.8 → 0.3.9
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/README.md +1 -1
- package/README.zh.md +1 -1
- package/dist/cli/index.js +158 -16
- package/dist/main.js +217 -46
- package/dist/web/public/app.js +192 -105
- package/dist/web/public/style.css +34 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
You @ the bot in a Feishu thread. It runs on your machine — reading your real codebase, executing commands, opening MRs — and posts the result back. You define what the agent knows and what it can do. Larkway just carries the messages.
|
|
10
10
|
|
|
11
|
-
**Current release: v0.3.
|
|
11
|
+
**Current release: v0.3.9**
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
package/README.zh.md
CHANGED
package/dist/cli/index.js
CHANGED
|
@@ -119646,6 +119646,14 @@ var GitIdentitySchema = external_exports.object({
|
|
|
119646
119646
|
name: external_exports.string().min(1),
|
|
119647
119647
|
email: external_exports.string().email()
|
|
119648
119648
|
});
|
|
119649
|
+
var GitCloneUrlSchema = external_exports.string().min(1).refine(
|
|
119650
|
+
(value) => {
|
|
119651
|
+
if (/^https?:\/\/\S+$/i.test(value)) return true;
|
|
119652
|
+
if (/^ssh:\/\/\S+$/i.test(value)) return true;
|
|
119653
|
+
return /^[A-Za-z0-9_.-]+@[^:\s]+:[^ \t\r\n]+$/.test(value);
|
|
119654
|
+
},
|
|
119655
|
+
"url must be an http(s), ssh://, or scp-like Git clone URL"
|
|
119656
|
+
);
|
|
119649
119657
|
var BotConfigSchema = external_exports.object({
|
|
119650
119658
|
/** Unique identifier, kebab-case. Used as key in sessionStore. */
|
|
119651
119659
|
id: external_exports.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "id must be kebab-case"),
|
|
@@ -119720,7 +119728,7 @@ var BotConfigSchema = external_exports.object({
|
|
|
119720
119728
|
* Agent workspace runtime: URL is a pointer only; the Agent decides clone
|
|
119721
119729
|
* timing and destination.
|
|
119722
119730
|
*/
|
|
119723
|
-
url:
|
|
119731
|
+
url: GitCloneUrlSchema.optional()
|
|
119724
119732
|
})
|
|
119725
119733
|
).default([]),
|
|
119726
119734
|
/**
|
|
@@ -120016,7 +120024,7 @@ function renderBotYaml(config) {
|
|
|
120016
120024
|
const header = [
|
|
120017
120025
|
"# Larkway bot config (L1) \u2014 generated/edited via `larkway` CLI.",
|
|
120018
120026
|
"# Credentials are env-var NAMES, never values:",
|
|
120019
|
-
"# app_secret_env /
|
|
120027
|
+
"# app_secret_env / git_token_env reference keys in ~/.larkway/.env (chmod 0600).",
|
|
120020
120028
|
"# L2 \u804C\u80FD memory lives alongside in <id>.memory.md (memory_file points at it).",
|
|
120021
120029
|
""
|
|
120022
120030
|
].join("\n");
|
|
@@ -122196,14 +122204,14 @@ async function runAdd(ctx, args) {
|
|
|
122196
122204
|
});
|
|
122197
122205
|
if (repoSlug) {
|
|
122198
122206
|
const repoBranch = await ui.prompt("\u9ED8\u8BA4\u5206\u652F:", {
|
|
122199
|
-
default: setMap.get("repo_branch") ?? "
|
|
122207
|
+
default: setMap.get("repo_branch") ?? "main",
|
|
122200
122208
|
nonInteractive: flags.nonInteractive
|
|
122201
122209
|
});
|
|
122202
|
-
const repoUrl = await ui.prompt("clone URL (\
|
|
122210
|
+
const repoUrl = await ui.prompt("clone URL (\u5EFA\u8BAE\u586B\u5199;agent \u7528\u5B83 clone):", {
|
|
122203
122211
|
default: setMap.get("repo_url") ?? "",
|
|
122204
122212
|
nonInteractive: flags.nonInteractive
|
|
122205
122213
|
});
|
|
122206
|
-
const repoEntry = { slug: repoSlug, branch: repoBranch || "
|
|
122214
|
+
const repoEntry = { slug: repoSlug, branch: repoBranch || "main" };
|
|
122207
122215
|
if (repoUrl) repoEntry.url = repoUrl;
|
|
122208
122216
|
repos.push(repoEntry);
|
|
122209
122217
|
}
|
|
@@ -122212,7 +122220,7 @@ async function runAdd(ctx, args) {
|
|
|
122212
122220
|
if (repoSlug) {
|
|
122213
122221
|
const repoEntry = {
|
|
122214
122222
|
slug: repoSlug,
|
|
122215
|
-
branch: setMap.get("repo_branch") ?? "
|
|
122223
|
+
branch: setMap.get("repo_branch") ?? "main"
|
|
122216
122224
|
};
|
|
122217
122225
|
const repoUrl = setMap.get("repo_url");
|
|
122218
122226
|
if (repoUrl) repoEntry.url = repoUrl;
|
|
@@ -122220,12 +122228,11 @@ async function runAdd(ctx, args) {
|
|
|
122220
122228
|
}
|
|
122221
122229
|
}
|
|
122222
122230
|
let gitlab_token_env;
|
|
122223
|
-
|
|
122224
|
-
if (flags.advanced || setMap.has("gitlab_token_env") || repos.length > 0) {
|
|
122231
|
+
if (flags.advanced || setMap.has("gitlab_token_env")) {
|
|
122225
122232
|
const rawGitlab = await ui.prompt(
|
|
122226
|
-
"Git access token \u73AF\u5883\u53D8\u91CF\u540D(\
|
|
122233
|
+
"Git access token \u73AF\u5883\u53D8\u91CF\u540D(\u53EF\u9009;\u79C1\u6709\u5E93/push/MR \u624D\u9700\u8981):",
|
|
122227
122234
|
{
|
|
122228
|
-
default: setMap.get("gitlab_token_env") ??
|
|
122235
|
+
default: setMap.get("gitlab_token_env") ?? "",
|
|
122229
122236
|
nonInteractive: flags.nonInteractive
|
|
122230
122237
|
}
|
|
122231
122238
|
);
|
|
@@ -122236,10 +122243,6 @@ async function runAdd(ctx, args) {
|
|
|
122236
122243
|
return 1;
|
|
122237
122244
|
}
|
|
122238
122245
|
}
|
|
122239
|
-
if (repos.length > 0 && !gitlab_token_env) {
|
|
122240
|
-
ui.failure("agent_workspace repo bot \u5FC5\u987B\u58F0\u660E git_token_env(\u6216 gitlab_token_env);v0.3 \u4E0D\u7EE7\u627F\u5168\u5C40 GITLAB_TOKEN\u3002");
|
|
122241
|
-
return 1;
|
|
122242
|
-
}
|
|
122243
122246
|
let lark_cli_profile;
|
|
122244
122247
|
if (flags.advanced || setMap.has("lark_cli_profile")) {
|
|
122245
122248
|
const rawProfile = await ui.prompt(
|
|
@@ -124556,10 +124559,10 @@ function defaultOnboardPermissionRequests(input) {
|
|
|
124556
124559
|
items.push(`Feishu chat allowlist: ${input.chats.join(", ")}`);
|
|
124557
124560
|
}
|
|
124558
124561
|
for (const repo of input.repos) {
|
|
124559
|
-
items.push(`
|
|
124562
|
+
items.push(`Git repo pointer: ${repo.slug} (${repo.branch})`);
|
|
124560
124563
|
}
|
|
124561
124564
|
if (input.gitlab_token_env) {
|
|
124562
|
-
items.push(`
|
|
124565
|
+
items.push(`Agent-level Git token env name: ${input.gitlab_token_env}`);
|
|
124563
124566
|
}
|
|
124564
124567
|
items.push("Local shell inside the Agent Workspace for task execution and verification");
|
|
124565
124568
|
return items;
|
|
@@ -124706,6 +124709,128 @@ async function cancelOnboard(sessionId) {
|
|
|
124706
124709
|
return { cancelled: true };
|
|
124707
124710
|
}
|
|
124708
124711
|
|
|
124712
|
+
// src/runtimeRequirements.ts
|
|
124713
|
+
import { execFileSync } from "node:child_process";
|
|
124714
|
+
function checkCli(command) {
|
|
124715
|
+
try {
|
|
124716
|
+
execFileSync("which", [command], { stdio: ["pipe", "pipe", "pipe"] });
|
|
124717
|
+
} catch {
|
|
124718
|
+
return { ok: false };
|
|
124719
|
+
}
|
|
124720
|
+
try {
|
|
124721
|
+
const version = execFileSync(command, ["--version"], { stdio: ["pipe", "pipe", "pipe"] }).toString().trim().split("\n")[0];
|
|
124722
|
+
return { ok: true, ...version ? { version } : {} };
|
|
124723
|
+
} catch {
|
|
124724
|
+
return { ok: true };
|
|
124725
|
+
}
|
|
124726
|
+
}
|
|
124727
|
+
function repoLooksGitLab(urlOrSlug) {
|
|
124728
|
+
return /(^|[./:@-])gitlab([./:@-]|$)/i.test(urlOrSlug);
|
|
124729
|
+
}
|
|
124730
|
+
function botUsesGitLab(bot) {
|
|
124731
|
+
return bot.repos.some((repo) => repoLooksGitLab(repo.url ?? repo.slug));
|
|
124732
|
+
}
|
|
124733
|
+
function addCliRequirement(requirements, input) {
|
|
124734
|
+
const existing = requirements.get(`cli:${input.command}`);
|
|
124735
|
+
if (existing) {
|
|
124736
|
+
existing.botIds = Array.from(/* @__PURE__ */ new Set([...existing.botIds, ...input.botIds])).sort();
|
|
124737
|
+
if (existing.severity === "optional" && input.severity === "required") {
|
|
124738
|
+
existing.severity = "required";
|
|
124739
|
+
}
|
|
124740
|
+
return;
|
|
124741
|
+
}
|
|
124742
|
+
const probe = checkCli(input.command);
|
|
124743
|
+
requirements.set(`cli:${input.command}`, {
|
|
124744
|
+
id: `cli:${input.command}`,
|
|
124745
|
+
label: input.label ?? input.command,
|
|
124746
|
+
command: input.command,
|
|
124747
|
+
kind: "cli",
|
|
124748
|
+
severity: input.severity,
|
|
124749
|
+
ok: probe.ok,
|
|
124750
|
+
...probe.version ? { version: probe.version } : {},
|
|
124751
|
+
reason: input.reason,
|
|
124752
|
+
...input.installHint ? { installHint: input.installHint } : {},
|
|
124753
|
+
botIds: Array.from(new Set(input.botIds)).sort()
|
|
124754
|
+
});
|
|
124755
|
+
}
|
|
124756
|
+
function addSecretRequirement(requirements, input) {
|
|
124757
|
+
const id = `secret:${input.envName}`;
|
|
124758
|
+
const existing = requirements.get(id);
|
|
124759
|
+
if (existing) {
|
|
124760
|
+
existing.botIds = Array.from(/* @__PURE__ */ new Set([...existing.botIds, ...input.botIds])).sort();
|
|
124761
|
+
return;
|
|
124762
|
+
}
|
|
124763
|
+
const value = process.env[input.envName];
|
|
124764
|
+
requirements.set(id, {
|
|
124765
|
+
id,
|
|
124766
|
+
label: input.envName,
|
|
124767
|
+
kind: "secret",
|
|
124768
|
+
severity: input.severity ?? "required",
|
|
124769
|
+
ok: value != null && value !== "",
|
|
124770
|
+
reason: input.reason,
|
|
124771
|
+
installHint: `Set ${input.envName} in ~/.larkway/.env and restart Larkway.`,
|
|
124772
|
+
botIds: Array.from(new Set(input.botIds)).sort()
|
|
124773
|
+
});
|
|
124774
|
+
}
|
|
124775
|
+
function runtimeRequirementsForBots(bots) {
|
|
124776
|
+
const requirements = /* @__PURE__ */ new Map();
|
|
124777
|
+
if (bots.length > 0) {
|
|
124778
|
+
addCliRequirement(requirements, {
|
|
124779
|
+
command: "lark-cli",
|
|
124780
|
+
label: "Feishu CLI",
|
|
124781
|
+
severity: "required",
|
|
124782
|
+
botIds: bots.map((bot) => bot.id),
|
|
124783
|
+
reason: "Required for agents to read Feishu topic history, attachments, docs, and other context.",
|
|
124784
|
+
installHint: "Install and configure lark-cli, then restart Larkway."
|
|
124785
|
+
});
|
|
124786
|
+
}
|
|
124787
|
+
for (const bot of bots) {
|
|
124788
|
+
addCliRequirement(requirements, {
|
|
124789
|
+
command: bot.backend ?? "claude",
|
|
124790
|
+
severity: "required",
|
|
124791
|
+
botIds: [bot.id],
|
|
124792
|
+
reason: `Required to run this bot's ${bot.backend ?? "claude"} backend.`
|
|
124793
|
+
});
|
|
124794
|
+
if (bot.runtime === "agent_workspace" && bot.repos.length > 0) {
|
|
124795
|
+
const tokenEnvName = bot.git_token_env ?? bot.gitlab_token_env;
|
|
124796
|
+
if (tokenEnvName) {
|
|
124797
|
+
addSecretRequirement(requirements, {
|
|
124798
|
+
envName: tokenEnvName,
|
|
124799
|
+
botIds: [bot.id],
|
|
124800
|
+
severity: "optional",
|
|
124801
|
+
reason: "Optional Git identity material. When absent, agents use the host's normal Git auth."
|
|
124802
|
+
});
|
|
124803
|
+
} else {
|
|
124804
|
+
requirements.set(`secret:missing-git-token:${bot.id}`, {
|
|
124805
|
+
id: `secret:missing-git-token:${bot.id}`,
|
|
124806
|
+
label: "Git access token env",
|
|
124807
|
+
kind: "secret",
|
|
124808
|
+
severity: "optional",
|
|
124809
|
+
ok: false,
|
|
124810
|
+
reason: `Bot "${bot.id}" has repo pointers but no git_token_env. It will use the host's normal Git auth.`,
|
|
124811
|
+
installHint: "Add a Git access token only when you want this agent to use a specific GitHub/GitLab identity.",
|
|
124812
|
+
botIds: [bot.id]
|
|
124813
|
+
});
|
|
124814
|
+
}
|
|
124815
|
+
}
|
|
124816
|
+
if (botUsesGitLab(bot)) {
|
|
124817
|
+
addCliRequirement(requirements, {
|
|
124818
|
+
command: "glab",
|
|
124819
|
+
label: "GitLab CLI",
|
|
124820
|
+
severity: "optional",
|
|
124821
|
+
botIds: [bot.id],
|
|
124822
|
+
reason: "Only needed when an agent uses GitLab-specific actions such as MRs, pipelines, or releases.",
|
|
124823
|
+
installHint: "Install glab only for bots that need GitLab-specific workflow commands."
|
|
124824
|
+
});
|
|
124825
|
+
}
|
|
124826
|
+
}
|
|
124827
|
+
return [...requirements.values()].sort((a, b) => {
|
|
124828
|
+
if (a.severity !== b.severity) return a.severity === "required" ? -1 : 1;
|
|
124829
|
+
if (a.kind !== b.kind) return a.kind === "cli" ? -1 : 1;
|
|
124830
|
+
return a.label.localeCompare(b.label);
|
|
124831
|
+
});
|
|
124832
|
+
}
|
|
124833
|
+
|
|
124709
124834
|
// src/web/api.ts
|
|
124710
124835
|
var execFileAsync7 = promisify8(execFileCallback2);
|
|
124711
124836
|
var CHAT_NAME_CACHE_MS = 5 * 60 * 1e3;
|
|
@@ -125395,6 +125520,22 @@ var getBridgeLogs = async (req) => {
|
|
|
125395
125520
|
json: result
|
|
125396
125521
|
};
|
|
125397
125522
|
};
|
|
125523
|
+
var getRuntimeRequirements = async (req) => {
|
|
125524
|
+
const { ctx } = req;
|
|
125525
|
+
const ids = await ctx.stores.botsStore.listBots();
|
|
125526
|
+
const bots = await Promise.all(
|
|
125527
|
+
ids.map(async (id) => ctx.stores.botsStore.readBot(id))
|
|
125528
|
+
);
|
|
125529
|
+
const requirements = runtimeRequirementsForBots(bots);
|
|
125530
|
+
return {
|
|
125531
|
+
status: 200,
|
|
125532
|
+
json: {
|
|
125533
|
+
requirements,
|
|
125534
|
+
missingRequired: requirements.filter((req2) => req2.severity === "required" && !req2.ok),
|
|
125535
|
+
missingOptional: requirements.filter((req2) => req2.severity === "optional" && !req2.ok)
|
|
125536
|
+
}
|
|
125537
|
+
};
|
|
125538
|
+
};
|
|
125398
125539
|
var BACKEND_META = {
|
|
125399
125540
|
claude: { id: "claude", name: "Claude Code", short: "Claude", vendor: "Anthropic \u8BA2\u9605", mono: "CC" },
|
|
125400
125541
|
codex: { id: "codex", name: "Codex", short: "Codex", vendor: "OpenAI \u8BA2\u9605", mono: "CX" }
|
|
@@ -125436,6 +125577,7 @@ var ROUTES = {
|
|
|
125436
125577
|
"GET /api/bridge": getBridge,
|
|
125437
125578
|
"POST /api/bridge/restart": postBridgeRestart,
|
|
125438
125579
|
"GET /api/bridge/logs": getBridgeLogs,
|
|
125580
|
+
"GET /api/runtime/requirements": getRuntimeRequirements,
|
|
125439
125581
|
"GET /api/backends": getBackends
|
|
125440
125582
|
};
|
|
125441
125583
|
function matchRoute(method, pathname, routes = ROUTES) {
|
package/dist/main.js
CHANGED
|
@@ -107360,7 +107360,6 @@ var require_main = __commonJS({
|
|
|
107360
107360
|
});
|
|
107361
107361
|
|
|
107362
107362
|
// src/main.ts
|
|
107363
|
-
import { execSync } from "node:child_process";
|
|
107364
107363
|
import { mkdirSync } from "node:fs";
|
|
107365
107364
|
import path13 from "node:path";
|
|
107366
107365
|
|
|
@@ -112529,9 +112528,11 @@ ${recent.join("\n")}` : recent.join("\n");
|
|
|
112529
112528
|
});
|
|
112530
112529
|
elements.push({ tag: "hr" });
|
|
112531
112530
|
}
|
|
112531
|
+
const mentionPrefix = atMentionMarkdown(opts.mentionOpenId);
|
|
112532
112532
|
const cleanBody = opts.bodyText ? stripLeakedToolMarkup(opts.bodyText) : "";
|
|
112533
|
-
|
|
112534
|
-
|
|
112533
|
+
const bodyWithMention = [mentionPrefix, cleanBody].filter(Boolean).join("\n\n");
|
|
112534
|
+
if (bodyWithMention) {
|
|
112535
|
+
const chunks = chunkMarkdown(bodyWithMention);
|
|
112535
112536
|
for (let i = 0; i < chunks.length; i++) {
|
|
112536
112537
|
elements.push({
|
|
112537
112538
|
tag: "markdown",
|
|
@@ -112573,6 +112574,11 @@ ${chunks[i]}`
|
|
|
112573
112574
|
};
|
|
112574
112575
|
return JSON.stringify(card);
|
|
112575
112576
|
}
|
|
112577
|
+
function atMentionMarkdown(userId) {
|
|
112578
|
+
if (!userId) return "";
|
|
112579
|
+
if (!/^[A-Za-z0-9_:-]+$/.test(userId)) return "";
|
|
112580
|
+
return `<at id=${userId}></at>`;
|
|
112581
|
+
}
|
|
112576
112582
|
var CardHandleImpl = class {
|
|
112577
112583
|
messageId;
|
|
112578
112584
|
/** Transport for the leaf PATCH network call (default = lark-cli). */
|
|
@@ -112624,6 +112630,7 @@ var CardHandleImpl = class {
|
|
|
112624
112630
|
showToolSummary: this.showToolSummary,
|
|
112625
112631
|
status: cardStatus,
|
|
112626
112632
|
failureReason: opts.failureReason,
|
|
112633
|
+
mentionOpenId: opts.mentionOpenId,
|
|
112627
112634
|
// V2 dynamic-choice buttons — agent-declared, only on finalize.
|
|
112628
112635
|
choices: opts.choices,
|
|
112629
112636
|
choicePrompt: opts.choicePrompt,
|
|
@@ -113353,6 +113360,33 @@ function renderTurnTakingHint(limit) {
|
|
|
113353
113360
|
"</turn-taking>"
|
|
113354
113361
|
];
|
|
113355
113362
|
}
|
|
113363
|
+
function renderRuntimeWarningsBlock(warnings) {
|
|
113364
|
+
if (warnings.length === 0) return [];
|
|
113365
|
+
const hasMissingLarkCli = warnings.some((warning) => warning.command === "lark-cli");
|
|
113366
|
+
return [
|
|
113367
|
+
"<runtime-warnings>",
|
|
113368
|
+
"Bridge \u68C0\u6D4B\u5230\u4EE5\u4E0B\u672C\u673A\u80FD\u529B\u6682\u4E0D\u53EF\u7528\u3002\u8FD9\u662F\u63D0\u793A,\u4E0D\u662F\u5F3A\u5236\u505C\u6B62\u6761\u4EF6:",
|
|
113369
|
+
...warnings.map((warning) => {
|
|
113370
|
+
const name = warning.command ? `${warning.label} (${warning.command})` : warning.label;
|
|
113371
|
+
const reason = warning.reason ? `: ${warning.reason}` : "";
|
|
113372
|
+
const installHint = warning.installHint ? ` Fix hint: ${warning.installHint}` : "";
|
|
113373
|
+
return `- ${name}${reason}${installHint}`;
|
|
113374
|
+
}),
|
|
113375
|
+
"",
|
|
113376
|
+
"\u5904\u7406\u539F\u5219:",
|
|
113377
|
+
"- \u80FD\u4EC5\u51ED\u5F53\u524D\u6D88\u606F\u7EE7\u7EED\u7684\u4EFB\u52A1,\u7EE7\u7EED\u5904\u7406,\u4E0D\u8981\u56E0\u4E3A warning \u76F4\u63A5\u62D2\u7EDD\u3002",
|
|
113378
|
+
"- \u53EA\u6709\u5F53\u4EFB\u52A1\u786E\u5B9E\u9700\u8981\u7F3A\u5931\u80FD\u529B\u65F6,\u518D\u5728 last_message \u91CC\u7528\u4EA7\u54C1\u5316\u8BED\u8A00\u544A\u8BC9\u7528\u6237\u7F3A\u4EC0\u4E48\u3001\u4F1A\u5F71\u54CD\u4EC0\u4E48\u3001\u5982\u4F55\u7EE7\u7EED\u3002",
|
|
113379
|
+
...hasMissingLarkCli ? [
|
|
113380
|
+
"- \u5BF9\u7F3A\u5C11 lark-cli \u7684\u60C5\u51B5:\u4E0D\u8981\u989D\u5916 @ \u7528\u6237;\u5728\u5361\u7247\u91CC\u8F7B\u91CF\u8BF4\u660E\u5F53\u524D\u65E0\u6CD5\u81EA\u52A8\u8BFB\u53D6\u98DE\u4E66\u8BDD\u9898\u5386\u53F2\u3001\u9644\u4EF6\u6216\u6587\u6863\u5373\u53EF\u3002",
|
|
113381
|
+
'- \u5982\u679C\u5F53\u524D\u4EFB\u52A1\u9700\u8981\u8FD9\u4E9B\u4E0A\u4E0B\u6587,\u7528 choices \u95EE\u662F\u5426\u5141\u8BB8\u5B89\u88C5\u6700\u65B0\u7248\u98DE\u4E66 CLI\u3002\u5EFA\u8BAE: `choice_prompt: "\u8BFB\u53D6\u98DE\u4E66\u5386\u53F2\u9700\u8981\u672C\u673A\u5B89\u88C5\u6700\u65B0\u7248\u98DE\u4E66 CLI,\u662F\u5426\u5141\u8BB8\u6211\u5C1D\u8BD5\u5B89\u88C5?"`, `choices: [{label:"\u5141\u8BB8\u5B89\u88C5", value:"\u5141\u8BB8\u5B89\u88C5 lark-cli"}, {label:"\u5148\u4E0D\u5B89\u88C5", value:"\u5148\u4E0D\u5B89\u88C5 lark-cli,\u6211\u4F1A\u628A\u8981\u5904\u7406\u7684\u5185\u5BB9\u8D34\u5230\u8BDD\u9898\u91CC"}]`\u3002',
|
|
113382
|
+
"- \u7528\u6237\u660E\u786E\u9009\u62E9/\u56DE\u590D\u5141\u8BB8\u5B89\u88C5\u540E,\u518D\u5C1D\u8BD5\u5B89\u88C5;\u4E0D\u8981\u5728\u672A\u786E\u8BA4\u524D\u6539\u5BBF\u4E3B\u673A\u5168\u5C40\u73AF\u5883\u3002",
|
|
113383
|
+
"- \u63A8\u8350\u5B89\u88C5\u547D\u4EE4: `npx -y @larksuite/cli@latest install`,\u7136\u540E\u8FD0\u884C `lark-cli --version` \u9A8C\u8BC1\u3002",
|
|
113384
|
+
'- \u5982\u9047 npm \u5168\u5C40\u76EE\u5F55\u6743\u9650\u9519\u8BEF(EACCES/permission denied),\u4F7F\u7528\u7528\u6237\u7EA7 prefix \u540E\u91CD\u8BD5: `mkdir -p ~/.npm-global && npm config set prefix "$HOME/.npm-global" && export PATH="$HOME/.npm-global/bin:$PATH" && npx -y @larksuite/cli@latest install`\u3002\u4E0D\u8981\u9ED8\u8BA4\u8981\u6C42 sudo\u3002',
|
|
113385
|
+
"- \u5B89\u88C5\u6210\u529F\u540E,\u5982\u679C\u672C\u8F6E\u9700\u8981\u7ACB\u5373\u8BFB\u53D6\u98DE\u4E66\u4E0A\u4E0B\u6587,\u53EF\u5728\u5F53\u524D shell \u4E2D\u5E26\u4E0A\u4FEE\u590D\u540E\u7684 PATH \u7EE7\u7EED\u5C1D\u8BD5;\u82E5\u9700\u8981 bridge \u540E\u7EED\u8F6E\u6B21\u7A33\u5B9A\u4F7F\u7528,\u8BF7\u63D0\u793A owner \u91CD\u542F Larkway\u3002"
|
|
113386
|
+
] : [],
|
|
113387
|
+
"</runtime-warnings>"
|
|
113388
|
+
];
|
|
113389
|
+
}
|
|
113356
113390
|
function renderWorkspaceBlock(primarySlug, primaryCachePath, defaultBranch, extraRepos) {
|
|
113357
113391
|
const lines = [
|
|
113358
113392
|
"<workspace>",
|
|
@@ -113423,7 +113457,17 @@ function sceneFacts(parsed, isNewThread) {
|
|
|
113423
113457
|
};
|
|
113424
113458
|
}
|
|
113425
113459
|
function renderPrompt(input) {
|
|
113426
|
-
const {
|
|
113460
|
+
const {
|
|
113461
|
+
parsed,
|
|
113462
|
+
isNewThread,
|
|
113463
|
+
conventions,
|
|
113464
|
+
peers,
|
|
113465
|
+
turn_taking_limit,
|
|
113466
|
+
agentMemory,
|
|
113467
|
+
extraRepoPaths,
|
|
113468
|
+
larkCliProfile,
|
|
113469
|
+
runtimeWarnings = []
|
|
113470
|
+
} = input;
|
|
113427
113471
|
const backend = input.backend ?? "claude";
|
|
113428
113472
|
const profileFlag = larkCliProfile ? ` --profile ${larkCliProfile}` : "";
|
|
113429
113473
|
const attachmentKeys = parsed.attachments.map((a) => a.fileKey);
|
|
@@ -113436,6 +113480,7 @@ function renderPrompt(input) {
|
|
|
113436
113480
|
const stateContract = renderStateContract(conventions.stateFilePath);
|
|
113437
113481
|
const peersBlock = peers && peers.length > 0 ? renderPeersBlock(peers) : [];
|
|
113438
113482
|
const turnTakingBlock = turn_taking_limit && turn_taking_limit > 0 ? renderTurnTakingHint(turn_taking_limit) : [];
|
|
113483
|
+
const runtimeWarningsBlock = renderRuntimeWarningsBlock(runtimeWarnings);
|
|
113439
113484
|
const extraRepos = extraRepoPaths ?? conventions.extraRepoPaths ?? [];
|
|
113440
113485
|
const workspaceBlock = isAgentWorkspace ? renderAgentWorkspaceBlock(conventions, extraRepos) : hasRepo ? renderWorkspaceBlock(
|
|
113441
113486
|
conventions.defaultProjectSlug ?? "repo",
|
|
@@ -113491,6 +113536,7 @@ function renderPrompt(input) {
|
|
|
113491
113536
|
"",
|
|
113492
113537
|
...agentMemoryBlock,
|
|
113493
113538
|
...skillIntroNew,
|
|
113539
|
+
...runtimeWarningsBlock.length > 0 ? [...runtimeWarningsBlock, ""] : [],
|
|
113494
113540
|
"<thread-context>",
|
|
113495
113541
|
`thread_id: ${parsed.threadId}`,
|
|
113496
113542
|
`message_id: ${parsed.messageId}`,
|
|
@@ -113558,6 +113604,7 @@ function renderPrompt(input) {
|
|
|
113558
113604
|
return [
|
|
113559
113605
|
...agentMemoryBlock,
|
|
113560
113606
|
...skillIntroCont,
|
|
113607
|
+
...runtimeWarningsBlock.length > 0 ? [...runtimeWarningsBlock, ""] : [],
|
|
113561
113608
|
"<thread-context>",
|
|
113562
113609
|
`thread_id: ${parsed.threadId}`,
|
|
113563
113610
|
`message_id: ${parsed.messageId}`,
|
|
@@ -114441,6 +114488,11 @@ var BridgeHandler = class {
|
|
|
114441
114488
|
constructor(deps) {
|
|
114442
114489
|
this.deps = deps;
|
|
114443
114490
|
}
|
|
114491
|
+
runtimeWarnings() {
|
|
114492
|
+
return (this.deps.runtimeRequirements ?? []).filter(
|
|
114493
|
+
(req) => !req.ok && (req.severity === "required" || req.kind === "secret")
|
|
114494
|
+
);
|
|
114495
|
+
}
|
|
114444
114496
|
/**
|
|
114445
114497
|
* Enter the main loop: for-await over client.events(), per-thread concurrent dispatch.
|
|
114446
114498
|
*
|
|
@@ -114742,7 +114794,8 @@ var BridgeHandler = class {
|
|
|
114742
114794
|
botName: this.deps.botConfig?.name,
|
|
114743
114795
|
backend: this.deps.botConfig?.backend,
|
|
114744
114796
|
agentMemory: this.deps.agentMemory,
|
|
114745
|
-
larkCliProfile: this.deps.larkCliProfile
|
|
114797
|
+
larkCliProfile: this.deps.larkCliProfile,
|
|
114798
|
+
runtimeWarnings: this.runtimeWarnings()
|
|
114746
114799
|
});
|
|
114747
114800
|
const backend = this.deps.botConfig?.backend ?? "claude";
|
|
114748
114801
|
const permissionMode = this.deps.permissionMode ?? (isAgentWorkspace ? "acceptEdits" : "bypassPermissions");
|
|
@@ -117852,6 +117905,14 @@ var GitIdentitySchema = external_exports.object({
|
|
|
117852
117905
|
name: external_exports.string().min(1),
|
|
117853
117906
|
email: external_exports.string().email()
|
|
117854
117907
|
});
|
|
117908
|
+
var GitCloneUrlSchema = external_exports.string().min(1).refine(
|
|
117909
|
+
(value) => {
|
|
117910
|
+
if (/^https?:\/\/\S+$/i.test(value)) return true;
|
|
117911
|
+
if (/^ssh:\/\/\S+$/i.test(value)) return true;
|
|
117912
|
+
return /^[A-Za-z0-9_.-]+@[^:\s]+:[^ \t\r\n]+$/.test(value);
|
|
117913
|
+
},
|
|
117914
|
+
"url must be an http(s), ssh://, or scp-like Git clone URL"
|
|
117915
|
+
);
|
|
117855
117916
|
var BotConfigSchema = external_exports.object({
|
|
117856
117917
|
/** Unique identifier, kebab-case. Used as key in sessionStore. */
|
|
117857
117918
|
id: external_exports.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "id must be kebab-case"),
|
|
@@ -117926,7 +117987,7 @@ var BotConfigSchema = external_exports.object({
|
|
|
117926
117987
|
* Agent workspace runtime: URL is a pointer only; the Agent decides clone
|
|
117927
117988
|
* timing and destination.
|
|
117928
117989
|
*/
|
|
117929
|
-
url:
|
|
117990
|
+
url: GitCloneUrlSchema.optional()
|
|
117930
117991
|
})
|
|
117931
117992
|
).default([]),
|
|
117932
117993
|
/**
|
|
@@ -118284,12 +118345,12 @@ var SIGKILL_GRACE_MS = 5e3;
|
|
|
118284
118345
|
function buildEnv(botGitIdentity, gitlabToken) {
|
|
118285
118346
|
const env = { ...process.env };
|
|
118286
118347
|
delete env["ANTHROPIC_API_KEY"];
|
|
118287
|
-
|
|
118288
|
-
|
|
118289
|
-
|
|
118290
|
-
|
|
118291
|
-
|
|
118292
|
-
|
|
118348
|
+
if (botGitIdentity) {
|
|
118349
|
+
env["GIT_AUTHOR_NAME"] = botGitIdentity.name;
|
|
118350
|
+
env["GIT_AUTHOR_EMAIL"] = botGitIdentity.email;
|
|
118351
|
+
env["GIT_COMMITTER_NAME"] = botGitIdentity.name;
|
|
118352
|
+
env["GIT_COMMITTER_EMAIL"] = botGitIdentity.email;
|
|
118353
|
+
}
|
|
118293
118354
|
if (gitlabToken !== void 0) {
|
|
118294
118355
|
env["GITLAB_TOKEN"] = gitlabToken;
|
|
118295
118356
|
}
|
|
@@ -118598,12 +118659,12 @@ function buildCodexEnv(botGitIdentity, gitlabToken) {
|
|
|
118598
118659
|
const env = { ...process.env };
|
|
118599
118660
|
delete env["OPENAI_API_KEY"];
|
|
118600
118661
|
delete env["ANTHROPIC_API_KEY"];
|
|
118601
|
-
|
|
118602
|
-
|
|
118603
|
-
|
|
118604
|
-
|
|
118605
|
-
|
|
118606
|
-
|
|
118662
|
+
if (botGitIdentity) {
|
|
118663
|
+
env["GIT_AUTHOR_NAME"] = botGitIdentity.name;
|
|
118664
|
+
env["GIT_AUTHOR_EMAIL"] = botGitIdentity.email;
|
|
118665
|
+
env["GIT_COMMITTER_NAME"] = botGitIdentity.name;
|
|
118666
|
+
env["GIT_COMMITTER_EMAIL"] = botGitIdentity.email;
|
|
118667
|
+
}
|
|
118607
118668
|
if (gitlabToken !== void 0) {
|
|
118608
118669
|
env["GITLAB_TOKEN"] = gitlabToken;
|
|
118609
118670
|
}
|
|
@@ -118989,33 +119050,144 @@ function checkGrantMatchesBot(normalizedGrantText, bot) {
|
|
|
118989
119050
|
return { ok: true };
|
|
118990
119051
|
}
|
|
118991
119052
|
|
|
118992
|
-
// src/
|
|
118993
|
-
|
|
118994
|
-
|
|
118995
|
-
function checkCli(name) {
|
|
119053
|
+
// src/runtimeRequirements.ts
|
|
119054
|
+
import { execFileSync } from "node:child_process";
|
|
119055
|
+
function checkCli(command) {
|
|
118996
119056
|
try {
|
|
118997
|
-
|
|
118998
|
-
if (!p) return { ok: false, version: "" };
|
|
118999
|
-
const version = execSync(`${name} --version`, {
|
|
119000
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
119001
|
-
}).toString().trim().split("\n")[0] ?? "";
|
|
119002
|
-
return { ok: true, version };
|
|
119057
|
+
execFileSync("which", [command], { stdio: ["pipe", "pipe", "pipe"] });
|
|
119003
119058
|
} catch {
|
|
119004
|
-
return { ok: false
|
|
119059
|
+
return { ok: false };
|
|
119005
119060
|
}
|
|
119061
|
+
try {
|
|
119062
|
+
const version = execFileSync(command, ["--version"], { stdio: ["pipe", "pipe", "pipe"] }).toString().trim().split("\n")[0];
|
|
119063
|
+
return { ok: true, ...version ? { version } : {} };
|
|
119064
|
+
} catch {
|
|
119065
|
+
return { ok: true };
|
|
119066
|
+
}
|
|
119067
|
+
}
|
|
119068
|
+
function repoLooksGitLab(urlOrSlug) {
|
|
119069
|
+
return /(^|[./:@-])gitlab([./:@-]|$)/i.test(urlOrSlug);
|
|
119006
119070
|
}
|
|
119007
|
-
function
|
|
119008
|
-
|
|
119009
|
-
return Array.from(/* @__PURE__ */ new Set(["lark-cli", ...backendBins, "glab"]));
|
|
119071
|
+
function botUsesGitLab(bot) {
|
|
119072
|
+
return bot.repos.some((repo) => repoLooksGitLab(repo.url ?? repo.slug));
|
|
119010
119073
|
}
|
|
119074
|
+
function addCliRequirement(requirements, input) {
|
|
119075
|
+
const existing = requirements.get(`cli:${input.command}`);
|
|
119076
|
+
if (existing) {
|
|
119077
|
+
existing.botIds = Array.from(/* @__PURE__ */ new Set([...existing.botIds, ...input.botIds])).sort();
|
|
119078
|
+
if (existing.severity === "optional" && input.severity === "required") {
|
|
119079
|
+
existing.severity = "required";
|
|
119080
|
+
}
|
|
119081
|
+
return;
|
|
119082
|
+
}
|
|
119083
|
+
const probe = checkCli(input.command);
|
|
119084
|
+
requirements.set(`cli:${input.command}`, {
|
|
119085
|
+
id: `cli:${input.command}`,
|
|
119086
|
+
label: input.label ?? input.command,
|
|
119087
|
+
command: input.command,
|
|
119088
|
+
kind: "cli",
|
|
119089
|
+
severity: input.severity,
|
|
119090
|
+
ok: probe.ok,
|
|
119091
|
+
...probe.version ? { version: probe.version } : {},
|
|
119092
|
+
reason: input.reason,
|
|
119093
|
+
...input.installHint ? { installHint: input.installHint } : {},
|
|
119094
|
+
botIds: Array.from(new Set(input.botIds)).sort()
|
|
119095
|
+
});
|
|
119096
|
+
}
|
|
119097
|
+
function addSecretRequirement(requirements, input) {
|
|
119098
|
+
const id = `secret:${input.envName}`;
|
|
119099
|
+
const existing = requirements.get(id);
|
|
119100
|
+
if (existing) {
|
|
119101
|
+
existing.botIds = Array.from(/* @__PURE__ */ new Set([...existing.botIds, ...input.botIds])).sort();
|
|
119102
|
+
return;
|
|
119103
|
+
}
|
|
119104
|
+
const value = process.env[input.envName];
|
|
119105
|
+
requirements.set(id, {
|
|
119106
|
+
id,
|
|
119107
|
+
label: input.envName,
|
|
119108
|
+
kind: "secret",
|
|
119109
|
+
severity: input.severity ?? "required",
|
|
119110
|
+
ok: value != null && value !== "",
|
|
119111
|
+
reason: input.reason,
|
|
119112
|
+
installHint: `Set ${input.envName} in ~/.larkway/.env and restart Larkway.`,
|
|
119113
|
+
botIds: Array.from(new Set(input.botIds)).sort()
|
|
119114
|
+
});
|
|
119115
|
+
}
|
|
119116
|
+
function runtimeRequirementsForBots(bots) {
|
|
119117
|
+
const requirements = /* @__PURE__ */ new Map();
|
|
119118
|
+
if (bots.length > 0) {
|
|
119119
|
+
addCliRequirement(requirements, {
|
|
119120
|
+
command: "lark-cli",
|
|
119121
|
+
label: "Feishu CLI",
|
|
119122
|
+
severity: "required",
|
|
119123
|
+
botIds: bots.map((bot) => bot.id),
|
|
119124
|
+
reason: "Required for agents to read Feishu topic history, attachments, docs, and other context.",
|
|
119125
|
+
installHint: "Install and configure lark-cli, then restart Larkway."
|
|
119126
|
+
});
|
|
119127
|
+
}
|
|
119128
|
+
for (const bot of bots) {
|
|
119129
|
+
addCliRequirement(requirements, {
|
|
119130
|
+
command: bot.backend ?? "claude",
|
|
119131
|
+
severity: "required",
|
|
119132
|
+
botIds: [bot.id],
|
|
119133
|
+
reason: `Required to run this bot's ${bot.backend ?? "claude"} backend.`
|
|
119134
|
+
});
|
|
119135
|
+
if (bot.runtime === "agent_workspace" && bot.repos.length > 0) {
|
|
119136
|
+
const tokenEnvName = bot.git_token_env ?? bot.gitlab_token_env;
|
|
119137
|
+
if (tokenEnvName) {
|
|
119138
|
+
addSecretRequirement(requirements, {
|
|
119139
|
+
envName: tokenEnvName,
|
|
119140
|
+
botIds: [bot.id],
|
|
119141
|
+
severity: "optional",
|
|
119142
|
+
reason: "Optional Git identity material. When absent, agents use the host's normal Git auth."
|
|
119143
|
+
});
|
|
119144
|
+
} else {
|
|
119145
|
+
requirements.set(`secret:missing-git-token:${bot.id}`, {
|
|
119146
|
+
id: `secret:missing-git-token:${bot.id}`,
|
|
119147
|
+
label: "Git access token env",
|
|
119148
|
+
kind: "secret",
|
|
119149
|
+
severity: "optional",
|
|
119150
|
+
ok: false,
|
|
119151
|
+
reason: `Bot "${bot.id}" has repo pointers but no git_token_env. It will use the host's normal Git auth.`,
|
|
119152
|
+
installHint: "Add a Git access token only when you want this agent to use a specific GitHub/GitLab identity.",
|
|
119153
|
+
botIds: [bot.id]
|
|
119154
|
+
});
|
|
119155
|
+
}
|
|
119156
|
+
}
|
|
119157
|
+
if (botUsesGitLab(bot)) {
|
|
119158
|
+
addCliRequirement(requirements, {
|
|
119159
|
+
command: "glab",
|
|
119160
|
+
label: "GitLab CLI",
|
|
119161
|
+
severity: "optional",
|
|
119162
|
+
botIds: [bot.id],
|
|
119163
|
+
reason: "Only needed when an agent uses GitLab-specific actions such as MRs, pipelines, or releases.",
|
|
119164
|
+
installHint: "Install glab only for bots that need GitLab-specific workflow commands."
|
|
119165
|
+
});
|
|
119166
|
+
}
|
|
119167
|
+
}
|
|
119168
|
+
return [...requirements.values()].sort((a, b) => {
|
|
119169
|
+
if (a.severity !== b.severity) return a.severity === "required" ? -1 : 1;
|
|
119170
|
+
if (a.kind !== b.kind) return a.kind === "cli" ? -1 : 1;
|
|
119171
|
+
return a.label.localeCompare(b.label);
|
|
119172
|
+
});
|
|
119173
|
+
}
|
|
119174
|
+
|
|
119175
|
+
// src/main.ts
|
|
119176
|
+
var STATUS_WRITE_INTERVAL_MS = 3e4;
|
|
119177
|
+
var VERSION = resolveLarkwayVersion(import.meta.url);
|
|
119011
119178
|
function printExternalCliProbe(bots) {
|
|
119012
|
-
console.log("
|
|
119013
|
-
for (const
|
|
119014
|
-
const
|
|
119015
|
-
|
|
119016
|
-
|
|
119179
|
+
console.log("Runtime requirements:");
|
|
119180
|
+
for (const req of runtimeRequirementsForBots(bots)) {
|
|
119181
|
+
const icon = req.ok ? "\u2713" : "\u2717";
|
|
119182
|
+
const tag = req.severity === "optional" ? "optional" : "required";
|
|
119183
|
+
const target = req.command ?? req.label;
|
|
119184
|
+
const botScope = req.botIds.length > 0 ? ` [${req.botIds.join(", ")}]` : "";
|
|
119185
|
+
if (req.ok) {
|
|
119186
|
+
console.log(` ${target.padEnd(14)} ${icon} ${req.version ?? tag}${botScope}`);
|
|
119017
119187
|
} else {
|
|
119018
|
-
|
|
119188
|
+
const message = req.kind === "secret" ? req.reason : `not found \u2014 ${req.reason}`;
|
|
119189
|
+
console.warn(` ${target.padEnd(14)} ${icon} (${tag}) ${message}${botScope}`);
|
|
119190
|
+
if (req.installHint) console.warn(` ${"".padEnd(14)} ${req.installHint}`);
|
|
119019
119191
|
}
|
|
119020
119192
|
}
|
|
119021
119193
|
}
|
|
@@ -119049,18 +119221,16 @@ async function runV2Mode({
|
|
|
119049
119221
|
if (bot.runtime === "agent_workspace") {
|
|
119050
119222
|
const _agentTokenEnvName = bot.git_token_env ?? bot.gitlab_token_env;
|
|
119051
119223
|
if (bot.repos.length > 0 && !_agentTokenEnvName) {
|
|
119052
|
-
console.
|
|
119053
|
-
`[larkway]
|
|
119224
|
+
console.warn(
|
|
119225
|
+
`[larkway] bot "${bot.id}": runtime=agent_workspace has repo pointers but no git_token_env. Starting anyway; the agent will use the host's normal Git identity/auth.`
|
|
119054
119226
|
);
|
|
119055
|
-
continue;
|
|
119056
119227
|
}
|
|
119057
119228
|
if (_agentTokenEnvName) {
|
|
119058
119229
|
const agentToken = process.env[_agentTokenEnvName];
|
|
119059
119230
|
if (agentToken == null || agentToken === "") {
|
|
119060
|
-
console.
|
|
119061
|
-
`[larkway]
|
|
119231
|
+
console.warn(
|
|
119232
|
+
`[larkway] bot "${bot.id}": runtime=agent_workspace declares token env "${_agentTokenEnvName}", but that env var is unset/empty. Starting anyway; the agent will use the host's normal Git identity/auth.`
|
|
119062
119233
|
);
|
|
119063
|
-
continue;
|
|
119064
119234
|
}
|
|
119065
119235
|
}
|
|
119066
119236
|
const permissionGate = await checkWorkspacePermissionGrant(resolveAgentWorkspacePath(bot.id), bot);
|
|
@@ -119091,7 +119261,7 @@ async function runV2Mode({
|
|
|
119091
119261
|
const gitlabToken = tokenEnvName != null ? process.env[tokenEnvName] : void 0;
|
|
119092
119262
|
if (tokenEnvName != null && (gitlabToken == null || gitlabToken === "")) {
|
|
119093
119263
|
console.warn(
|
|
119094
|
-
`[larkway] bot "${bot.id}" declares token env "${tokenEnvName}" but that env var is unset/empty \u2014
|
|
119264
|
+
`[larkway] bot "${bot.id}" declares token env "${tokenEnvName}" but that env var is unset/empty \u2014 leaving the host Git auth environment unchanged.`
|
|
119095
119265
|
);
|
|
119096
119266
|
}
|
|
119097
119267
|
const larkCliProfile = deriveLarkCliProfile(bot.lark_cli_profile, bot.app_id);
|
|
@@ -119162,7 +119332,7 @@ async function runV2Mode({
|
|
|
119162
119332
|
readOnly: bot.read_only,
|
|
119163
119333
|
gitlabTokenEnvName: tokenEnvName
|
|
119164
119334
|
};
|
|
119165
|
-
const effectiveGitlabToken =
|
|
119335
|
+
const effectiveGitlabToken = gitlabToken;
|
|
119166
119336
|
const resolvedPeers = bot.peers.flatMap((peerId) => {
|
|
119167
119337
|
const peer = bots.find((b) => b.id === peerId);
|
|
119168
119338
|
if (!peer) return [];
|
|
@@ -119189,6 +119359,7 @@ async function runV2Mode({
|
|
|
119189
119359
|
gitlabToken: effectiveGitlabToken,
|
|
119190
119360
|
agentMemory: bot.agent_memory,
|
|
119191
119361
|
larkCliProfile,
|
|
119362
|
+
runtimeRequirements: runtimeRequirementsForBots([bot]),
|
|
119192
119363
|
recordRuntimeEvent: async (patch) => {
|
|
119193
119364
|
await upsertRuntimeEvent(larkwayHome(), bot.id, patch);
|
|
119194
119365
|
}
|
package/dist/web/public/app.js
CHANGED
|
@@ -377,6 +377,8 @@ const state = {
|
|
|
377
377
|
selected: null,
|
|
378
378
|
/** Bridge 进程状态(来自 GET /api/bridge;null = 未知)。 */
|
|
379
379
|
bridge: /** @type {{running:boolean,pid:number|null,platform:string,mode:string}|null} */ (null),
|
|
380
|
+
/** Runtime requirement diagnostics from GET /api/runtime/requirements. */
|
|
381
|
+
requirements: /** @type {{requirements:Array<any>,missingRequired:Array<any>,missingOptional:Array<any>}|null} */ (null),
|
|
380
382
|
/**
|
|
381
383
|
* 每个 bot 的实时在线状态(来自 GET /api/status 的 bots[]),按 id 索引。
|
|
382
384
|
* "serving"=🟢正常服务中 / "degraded"=🟡连接异常 / "offline"=🔴未运行掉线 / null=状态未知。
|
|
@@ -867,6 +869,34 @@ function effLive(id) {
|
|
|
867
869
|
return bridgeRunning ? botLiveness(id) : "offline";
|
|
868
870
|
}
|
|
869
871
|
|
|
872
|
+
function missingRequiredForBot(id) {
|
|
873
|
+
const missing = state.requirements?.missingRequired;
|
|
874
|
+
if (!Array.isArray(missing)) return [];
|
|
875
|
+
return missing.filter((req) => Array.isArray(req.botIds) && req.botIds.includes(id));
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function missingRequiredAll() {
|
|
879
|
+
return Array.isArray(state.requirements?.missingRequired) ? state.requirements.missingRequired : [];
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function requirementShortLabel(req) {
|
|
883
|
+
if (!req) return "运行依赖";
|
|
884
|
+
if (req.command === "lark-cli") return "飞书 CLI";
|
|
885
|
+
if (req.kind === "secret") return req.label === "Git access token env" ? "Git 访问令牌" : req.label;
|
|
886
|
+
return req.label || req.command || "运行依赖";
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function requirementInstallText(req) {
|
|
890
|
+
if (!req) return "";
|
|
891
|
+
if (req.command === "lark-cli") return "安装并配置 lark-cli 后重启服务。";
|
|
892
|
+
if (req.kind === "secret") return req.installHint || "在看板里粘贴 Git access token,或补齐 bot yaml 的 git_token_env。";
|
|
893
|
+
return req.installHint || "安装缺失的 CLI 后重启服务。";
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function primaryMissingForBot(id) {
|
|
897
|
+
return missingRequiredForBot(id)[0] ?? null;
|
|
898
|
+
}
|
|
899
|
+
|
|
870
900
|
/**
|
|
871
901
|
* pendingNew:bridge 在跑且存在从未上报过 status 的 bot(unknown liveness)的数量。
|
|
872
902
|
* 对应 LkServiceIndicator pendingNew prop。
|
|
@@ -988,6 +1018,8 @@ function buildStatusActionPanel(liveKey, busyAction = null, moreOpen = false) {
|
|
|
988
1018
|
};
|
|
989
1019
|
|
|
990
1020
|
const primaryBusy = busyAction === fx.primary.action;
|
|
1021
|
+
const heading = fx.heading;
|
|
1022
|
+
const say = fx.say;
|
|
991
1023
|
|
|
992
1024
|
// Build "more" section for degraded
|
|
993
1025
|
let moreSection = "";
|
|
@@ -1045,8 +1077,8 @@ function buildStatusActionPanel(liveKey, busyAction = null, moreOpen = false) {
|
|
|
1045
1077
|
iconSvg + `</span>` +
|
|
1046
1078
|
`<div style="min-width:0;flex:1">` +
|
|
1047
1079
|
`<div style="font-size:12.5px;font-weight:700;letter-spacing:.04em;color:${sc.text};margin-bottom:4px">` +
|
|
1048
|
-
esc(
|
|
1049
|
-
`<p style="margin:0;font-size:14px;line-height:1.55;color:#334155">${esc(
|
|
1080
|
+
esc(heading) + `</div>` +
|
|
1081
|
+
`<p style="margin:0;font-size:14px;line-height:1.55;color:#334155">${esc(say)}</p>` +
|
|
1050
1082
|
`<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:13px;align-items:center">` +
|
|
1051
1083
|
fixBtnPrimary(primaryBusy) +
|
|
1052
1084
|
(fx.secondary ? fixBtnSecondary(fx.secondary) : "") +
|
|
@@ -2295,37 +2327,44 @@ function normalizeRepos(raw) {
|
|
|
2295
2327
|
if (!Array.isArray(raw)) return [];
|
|
2296
2328
|
return raw.map((r) => {
|
|
2297
2329
|
if (typeof r === "string") {
|
|
2298
|
-
const [slug, branch = "
|
|
2330
|
+
const [slug, branch = "main"] = r.split(":").map((s) => s.trim());
|
|
2299
2331
|
return { slug, branch, url: "" };
|
|
2300
2332
|
}
|
|
2301
|
-
return { slug: r.slug ?? "", branch: r.branch ?? "
|
|
2333
|
+
return { slug: r.slug ?? "", branch: r.branch ?? "main", url: r.url ?? "" };
|
|
2302
2334
|
});
|
|
2303
2335
|
}
|
|
2304
2336
|
|
|
2337
|
+
function inferRepoSlugFromUrl(url) {
|
|
2338
|
+
const text = String(url ?? "").trim();
|
|
2339
|
+
if (!text) return "";
|
|
2340
|
+
const stripGit = (s) => s.replace(/\.git$/i, "").replace(/^\/+|\/+$/g, "");
|
|
2341
|
+
|
|
2342
|
+
const scp = /^[A-Za-z0-9_.-]+@[^:\s]+:(.+)$/.exec(text);
|
|
2343
|
+
if (scp) return stripGit(scp[1] ?? "");
|
|
2344
|
+
|
|
2345
|
+
try {
|
|
2346
|
+
const parsed = new URL(text);
|
|
2347
|
+
return stripGit(parsed.pathname);
|
|
2348
|
+
} catch {
|
|
2349
|
+
return "";
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2305
2353
|
/**
|
|
2306
|
-
* 生成一个仓库行的 HTML
|
|
2354
|
+
* 生成一个仓库行的 HTML。UI 只暴露 Git 地址;slug/branch 都是内部细节。
|
|
2307
2355
|
* @param {number} idx
|
|
2308
2356
|
* @param {{slug:string,branch:string,url:string}} repo
|
|
2309
2357
|
*/
|
|
2310
2358
|
function acRepoRowHTML(idx, repo) {
|
|
2311
2359
|
const xIcon = `<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg>`;
|
|
2360
|
+
const displayUrl = repo.url || "";
|
|
2312
2361
|
return (
|
|
2313
2362
|
`<div class="ac-repo-row" data-repo-idx="${idx}">` +
|
|
2314
|
-
`<div class="ac-repo-grid">` +
|
|
2315
2363
|
`<div class="ac-field">` +
|
|
2316
|
-
`<label class="ac-label" for="ac-repo-
|
|
2317
|
-
`<input id="ac-repo-
|
|
2318
|
-
`</div>` +
|
|
2319
|
-
`<div class="ac-field">` +
|
|
2320
|
-
`<label class="ac-label" for="ac-repo-branch-${idx}">分支</label>` +
|
|
2321
|
-
`<input id="ac-repo-branch-${idx}" class="ac-input ac-mono" type="text" placeholder="main" spellcheck="false" data-repo="branch" data-repo-idx="${idx}" value="${esc(repo.branch)}" />` +
|
|
2322
|
-
`</div>` +
|
|
2323
|
-
`</div>` +
|
|
2324
|
-
`<div class="ac-field" style="margin-top:8px">` +
|
|
2325
|
-
`<label class="ac-label" for="ac-repo-url-${idx}">clone 地址 <span class="ac-optional">私有库必需</span></label>` +
|
|
2326
|
-
`<input id="ac-repo-url-${idx}" class="ac-input ac-mono" type="text" placeholder="git@github.com:org/repo.git" spellcheck="false" data-repo="url" data-repo-idx="${idx}" value="${esc(repo.url)}" />` +
|
|
2364
|
+
`<label class="ac-label" for="ac-repo-url-${idx}">仓库 Git 地址 <span class="ac-required">必填</span></label>` +
|
|
2365
|
+
`<input id="ac-repo-url-${idx}" class="ac-input ac-mono" type="text" placeholder="git@github.com:org/repo.git" spellcheck="false" data-repo="url" data-repo-idx="${idx}" value="${esc(displayUrl)}" />` +
|
|
2327
2366
|
`</div>` +
|
|
2328
|
-
`<button type="button" class="ac-repo-del" title="移除这个仓库" data-repo-idx="${idx}" aria-label="移除仓库 ${esc(repo.slug || idx + 1)}">` +
|
|
2367
|
+
`<button type="button" class="ac-repo-del" title="移除这个仓库" data-repo-idx="${idx}" aria-label="移除仓库 ${esc(repo.slug || repo.url || idx + 1)}">` +
|
|
2329
2368
|
xIcon +
|
|
2330
2369
|
`</button>` +
|
|
2331
2370
|
`</div>`
|
|
@@ -2367,7 +2406,8 @@ function buildAgentConfigHTML(bot, memContent, mode, prefill) {
|
|
|
2367
2406
|
// 层二 summary 药丸(收起态)
|
|
2368
2407
|
const permSummaryHTML = codeAccess
|
|
2369
2408
|
? acPillHTML("brand", "m16 18 6-6-6-6M8 6l-6 6 6 6", "可访问代码仓库") +
|
|
2370
|
-
acPillHTML("muted", "M21 8 12 3 3 8v8l9 5 9-5ZM3 8l9 5 9-5M12 13v8", repos.length ?
|
|
2409
|
+
acPillHTML("muted", "M21 8 12 3 3 8v8l9 5 9-5ZM3 8l9 5 9-5M12 13v8", repos.length ? `${repos.length} 个仓库` : "待添加仓库") +
|
|
2410
|
+
acPillHTML("muted", "M5 11h14a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1ZM8 11V7a4 4 0 0 1 8 0v4", gitlabConfigured ? "Agent 级 Git 身份" : "沿用本机 Git 身份")
|
|
2371
2411
|
: acPillHTML("muted", "M5 11h14a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1ZM8 11V7a4 4 0 0 1 8 0v4", "纯对话 · 不碰任何代码仓库");
|
|
2372
2412
|
|
|
2373
2413
|
// 层三 summary 药丸
|
|
@@ -2450,7 +2490,7 @@ function buildAgentConfigHTML(bot, memContent, mode, prefill) {
|
|
|
2450
2490
|
`<div class="ac-access-toggle-row${codeAccess ? " is-on" : ""}" id="ac-access-row">` +
|
|
2451
2491
|
`<div class="ac-access-info">` +
|
|
2452
2492
|
`<div class="ac-access-title">给它访问代码仓库的权限</div>` +
|
|
2453
|
-
`<p class="ac-access-desc"
|
|
2493
|
+
`<p class="ac-access-desc">打开后给这个 Agent 配一组仓库地址;它只有这些 repo 作为默认工作范围。Git 身份是 Agent 级的:不填就沿用这台机器现有的 SSH key、credential helper 或环境变量。</p>` +
|
|
2454
2494
|
`</div>` +
|
|
2455
2495
|
`<button type="button" class="ac-toggle${codeAccess ? " is-on" : ""}" id="ac-code-access-btn" role="switch" aria-checked="${codeAccess}" title="开 / 关代码访问权限">` +
|
|
2456
2496
|
`<span class="ac-toggle-thumb"></span>` +
|
|
@@ -2461,40 +2501,43 @@ function buildAgentConfigHTML(bot, memContent, mode, prefill) {
|
|
|
2461
2501
|
`<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex-shrink:0;color:var(--faint)"><path d="M5 11h14a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1ZM8 11V7a4 4 0 0 1 8 0v4"/></svg>` +
|
|
2462
2502
|
`当前这个 agent 不访问任何代码仓库。需要它读 / 改代码时,再打开上面的开关。` +
|
|
2463
2503
|
`</div>` +
|
|
2464
|
-
//
|
|
2504
|
+
// 开启态:仓库 + Agent 级 Git 身份
|
|
2465
2505
|
`<div class="ac-access-fields" id="ac-access-fields" ${codeAccess ? "" : 'style="display:none"'}>` +
|
|
2506
|
+
// 仓库列表
|
|
2507
|
+
`<div class="ac-repos-section">` +
|
|
2508
|
+
`<div class="ac-repos-header">` +
|
|
2509
|
+
`<span class="ac-repos-title">这个 Agent 可碰的仓库</span>` +
|
|
2510
|
+
`<span class="ac-optional-badge">可多个</span>` +
|
|
2511
|
+
`</div>` +
|
|
2512
|
+
`<p class="ac-hint">每行只填 clone 地址。分支默认 main;同一个 Agent 的所有仓库共用下面的 Git 身份。</p>` +
|
|
2513
|
+
`<div class="ac-repos-empty" id="ac-repos-empty" ${repos.length ? 'style="display:none"' : ""}><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex-shrink:0"><path d="M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9L12 3Z"/></svg>还没有仓库。需要访问代码时,请添加仓库 Git 地址。</div>` +
|
|
2514
|
+
`<div class="ac-repos-list" id="ac-repos-list" ${repos.length ? "" : 'style="display:none"'}>${repos.map((r, i) => acRepoRowHTML(i, r)).join("")}</div>` +
|
|
2515
|
+
`<button type="button" class="ac-add-repo-btn" id="ac-add-repo-btn">` +
|
|
2516
|
+
`<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>` +
|
|
2517
|
+
` 添加仓库` +
|
|
2518
|
+
`</button>` +
|
|
2519
|
+
`</div>` + // ac-repos-section
|
|
2520
|
+
`<details class="ac-token-advanced">` +
|
|
2521
|
+
`<summary>Git 身份 <span>可选,作用于上面所有仓库</span></summary>` +
|
|
2466
2522
|
`<div class="ac-field">` +
|
|
2467
|
-
`<label class="ac-label" for="ac-gitlab-token">Git Access Token
|
|
2468
|
-
`<p class="ac-hint"
|
|
2523
|
+
`<label class="ac-label" for="ac-gitlab-token">Agent 级 Git Access Token <span class="ac-optional">可选</span></label>` +
|
|
2524
|
+
`<p class="ac-hint">只配置一个,供这个 Agent 的所有仓库使用。GitHub fine-grained PAT 可以在 GitHub 里只勾选这些仓库;这里只保存 token 真值到本机 <code>~/.larkway/.env</code>(权限 0600),页面不回显。</p>` +
|
|
2469
2525
|
(gitlabConfigured
|
|
2470
2526
|
? `<div class="ac-token-configured" id="ac-token-configured">` +
|
|
2471
|
-
`<span class="ac-token-mask">${ICONS.lock}
|
|
2527
|
+
`<span class="ac-token-mask">${ICONS.lock} 已配置此 Agent 的 Git 身份 <span style="letter-spacing:.12em">••••••</span></span>` +
|
|
2472
2528
|
`<button type="button" class="btn btn-sm ac-token-reset-btn" id="ac-token-reset-btn">重新设置</button>` +
|
|
2473
2529
|
`</div>` +
|
|
2474
2530
|
`<div class="ac-secret-wrap" id="ac-token-input-wrap" style="display:none">` +
|
|
2475
2531
|
`<svg class="ac-secret-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 11h14a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1ZM8 11V7a4 4 0 0 1 8 0v4"/></svg>` +
|
|
2476
|
-
`<input id="ac-gitlab-token" name="gitlab_token_value" class="ac-input ac-secret-input" type="password" autocomplete="new-password" placeholder="
|
|
2532
|
+
`<input id="ac-gitlab-token" name="gitlab_token_value" class="ac-input ac-secret-input" type="password" autocomplete="new-password" placeholder="粘贴新的 Agent 级 token" value="" />` +
|
|
2477
2533
|
`</div>`
|
|
2478
2534
|
: `<div class="ac-secret-wrap" id="ac-token-input-wrap">` +
|
|
2479
2535
|
`<svg class="ac-secret-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 11h14a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1ZM8 11V7a4 4 0 0 1 8 0v4"/></svg>` +
|
|
2480
|
-
`<input id="ac-gitlab-token" name="gitlab_token_value" class="ac-input ac-secret-input" type="password" autocomplete="new-password" placeholder="
|
|
2536
|
+
`<input id="ac-gitlab-token" name="gitlab_token_value" class="ac-input ac-secret-input" type="password" autocomplete="new-password" placeholder="可留空,或粘贴 Agent 级 token" value="" />` +
|
|
2481
2537
|
`</div>`
|
|
2482
2538
|
) +
|
|
2483
2539
|
`</div>` +
|
|
2484
|
-
|
|
2485
|
-
`<div class="ac-repos-section">` +
|
|
2486
|
-
`<div class="ac-repos-header">` +
|
|
2487
|
-
`<span class="ac-repos-title">预热这些仓库</span>` +
|
|
2488
|
-
`<span class="ac-optional-badge">可选</span>` +
|
|
2489
|
-
`</div>` +
|
|
2490
|
-
`<p class="ac-hint">列出主要会用到的仓库 → 我们提前 clone 好,agent 一上来就有,提速。<b>留空也行</b> —— agent 会用上面的令牌自己判断需要哪些、自己 clone。</p>` +
|
|
2491
|
-
`<div class="ac-repos-empty" id="ac-repos-empty" ${repos.length ? 'style="display:none"' : ""}><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex-shrink:0"><path d="M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9L12 3Z"/></svg>未列仓库 —— agent 会用令牌自己理解需要哪些、自己 clone。</div>` +
|
|
2492
|
-
`<div class="ac-repos-list" id="ac-repos-list" ${repos.length ? "" : 'style="display:none"'}>${repos.map((r, i) => acRepoRowHTML(i, r)).join("")}</div>` +
|
|
2493
|
-
`<button type="button" class="ac-add-repo-btn" id="ac-add-repo-btn">` +
|
|
2494
|
-
`<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>` +
|
|
2495
|
-
` 添加仓库` +
|
|
2496
|
-
`</button>` +
|
|
2497
|
-
`</div>` + // ac-repos-section
|
|
2540
|
+
`</details>` +
|
|
2498
2541
|
`</div>` + // ac-access-fields
|
|
2499
2542
|
`</div>` + // ac-perm-body
|
|
2500
2543
|
`</section>` +
|
|
@@ -2652,17 +2695,9 @@ function wireAgentConfigEvents(panel, id, bot) {
|
|
|
2652
2695
|
if (accessRow) accessRow.classList.toggle("is-on", on);
|
|
2653
2696
|
if (noAccessHint) noAccessHint.style.display = on ? "none" : "";
|
|
2654
2697
|
if (accessFields) accessFields.style.display = on ? "" : "none";
|
|
2655
|
-
//
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
// 隐藏密码框,重置 tokenReset 标记(关闭等于「清除」)
|
|
2659
|
-
const configured = ac.querySelector("#ac-token-configured");
|
|
2660
|
-
const inputWrap = ac.querySelector("#ac-token-input-wrap");
|
|
2661
|
-
if (configured) configured.style.display = "none";
|
|
2662
|
-
if (inputWrap) { inputWrap.style.display = ""; } // 关闭态直接露空框,保存时会带 ""
|
|
2663
|
-
delete ac.dataset.tokenReset;
|
|
2664
|
-
acClearRepos(ac);
|
|
2665
|
-
}
|
|
2698
|
+
// 关掉代码访问只改变本次保存的序列化结果,不立即清空表单里的仓库行。
|
|
2699
|
+
// 这样用户误点开关后再打开,未保存的仓库地址还在。
|
|
2700
|
+
if (!on && gitlabInput) gitlabInput.value = "";
|
|
2666
2701
|
// open 徽标
|
|
2667
2702
|
if (permLayerTitleRow) {
|
|
2668
2703
|
let badge = permLayerTitleRow.querySelector(".ac-open-badge");
|
|
@@ -2770,11 +2805,10 @@ function readAgentConfigValues(panel) {
|
|
|
2770
2805
|
if (codeAccess) {
|
|
2771
2806
|
const repoRows = ac.querySelectorAll(".ac-repo-row");
|
|
2772
2807
|
for (const row of repoRows) {
|
|
2773
|
-
const
|
|
2774
|
-
const
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
const r = { slug, branch };
|
|
2808
|
+
const url = (row.querySelector("[data-repo='url']")?.value ?? "").trim();
|
|
2809
|
+
const inferredSlug = inferRepoSlugFromUrl(url);
|
|
2810
|
+
if (inferredSlug || url) {
|
|
2811
|
+
const r = { slug: inferredSlug, branch: "main" };
|
|
2778
2812
|
if (url) r.url = url;
|
|
2779
2813
|
repos.push(r);
|
|
2780
2814
|
}
|
|
@@ -2803,12 +2837,46 @@ function readAgentConfigValues(panel) {
|
|
|
2803
2837
|
|
|
2804
2838
|
function acGetRepos(ac) {
|
|
2805
2839
|
const rows = ac.querySelectorAll(".ac-repo-row");
|
|
2806
|
-
return Array.from(rows).map((row) =>
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2840
|
+
return Array.from(rows).map((row) => {
|
|
2841
|
+
const url = (row.querySelector("[data-repo='url']")?.value ?? "").trim();
|
|
2842
|
+
return {
|
|
2843
|
+
slug: inferRepoSlugFromUrl(url),
|
|
2844
|
+
branch: "main",
|
|
2845
|
+
url,
|
|
2846
|
+
_idx: parseInt(row.dataset.repoIdx ?? "-1", 10),
|
|
2847
|
+
};
|
|
2848
|
+
});
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
function validateCodeAccessConfig(panel) {
|
|
2852
|
+
const ac = panel?.querySelector?.("#ac-panel") ?? panel;
|
|
2853
|
+
if (!ac) return true;
|
|
2854
|
+
const codeAccessBtn = ac.querySelector("#ac-code-access-btn");
|
|
2855
|
+
const codeAccess = codeAccessBtn?.getAttribute("aria-checked") === "true";
|
|
2856
|
+
if (!codeAccess) return true;
|
|
2857
|
+
|
|
2858
|
+
const repos = acGetRepos(ac);
|
|
2859
|
+
if (repos.length === 0) {
|
|
2860
|
+
toast("已打开代码访问,请至少添加一个仓库。", "warn");
|
|
2861
|
+
ac.querySelector("#ac-add-repo-btn")?.focus();
|
|
2862
|
+
return false;
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
for (const repo of repos) {
|
|
2866
|
+
const row = ac.querySelector(`.ac-repo-row[data-repo-idx="${repo._idx}"]`);
|
|
2867
|
+
const urlInput = row?.querySelector("[data-repo='url']");
|
|
2868
|
+
if (!repo.url) {
|
|
2869
|
+
toast("请填写代码仓库 Git 地址。Git Access Token 是选填。", "warn");
|
|
2870
|
+
urlInput?.focus();
|
|
2871
|
+
return false;
|
|
2872
|
+
}
|
|
2873
|
+
if (!repo.slug) {
|
|
2874
|
+
toast("无法从 Git 地址识别仓库,请检查地址格式。", "warn");
|
|
2875
|
+
urlInput?.focus();
|
|
2876
|
+
return false;
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
return true;
|
|
2812
2880
|
}
|
|
2813
2881
|
|
|
2814
2882
|
function acRebuildRepoRows(ac) {
|
|
@@ -2836,8 +2904,7 @@ function acAddRepo(ac) {
|
|
|
2836
2904
|
list.appendChild(tmp.firstElementChild);
|
|
2837
2905
|
list.style.display = "";
|
|
2838
2906
|
if (empty) empty.style.display = "none";
|
|
2839
|
-
|
|
2840
|
-
list.querySelector(`#ac-repo-slug-${newIdx}`)?.focus();
|
|
2907
|
+
list.querySelector(`#ac-repo-url-${newIdx}`)?.focus();
|
|
2841
2908
|
acUpdatePermSummary(ac);
|
|
2842
2909
|
ac.dispatchEvent(new Event("ac-change", { bubbles: true }));
|
|
2843
2910
|
}
|
|
@@ -2855,14 +2922,6 @@ function acRemoveRepo(ac, idx) {
|
|
|
2855
2922
|
ac.dispatchEvent(new Event("ac-change", { bubbles: true }));
|
|
2856
2923
|
}
|
|
2857
2924
|
|
|
2858
|
-
function acClearRepos(ac) {
|
|
2859
|
-
const list = ac.querySelector("#ac-repos-list");
|
|
2860
|
-
const empty = ac.querySelector("#ac-repos-empty");
|
|
2861
|
-
if (list) { list.innerHTML = ""; list.style.display = "none"; }
|
|
2862
|
-
if (empty) empty.style.display = "";
|
|
2863
|
-
acUpdatePermSummary(ac);
|
|
2864
|
-
}
|
|
2865
|
-
|
|
2866
2925
|
// ── summary 药丸实时更新 ─────────────────────────────────────────────────────
|
|
2867
2926
|
|
|
2868
2927
|
function acUpdatePermSummary(ac) {
|
|
@@ -2874,11 +2933,19 @@ function acUpdatePermSummary(ac) {
|
|
|
2874
2933
|
const repoCount = ac.querySelectorAll(".ac-repo-row").length;
|
|
2875
2934
|
const html = codeAccess
|
|
2876
2935
|
? acPillHTML("brand", "m16 18 6-6-6-6M8 6l-6 6 6 6", "可访问代码仓库") +
|
|
2877
|
-
acPillHTML("muted", "M21 8 12 3 3 8v8l9 5 9-5ZM3 8l9 5 9-5M12 13v8", repoCount ?
|
|
2936
|
+
acPillHTML("muted", "M21 8 12 3 3 8v8l9 5 9-5ZM3 8l9 5 9-5M12 13v8", repoCount ? `${repoCount} 个仓库` : "待添加仓库") +
|
|
2937
|
+
acPillHTML("muted", "M5 11h14a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1ZM8 11V7a4 4 0 0 1 8 0v4", acHasAgentGitIdentity(ac) ? "Agent 级 Git 身份" : "沿用本机 Git 身份")
|
|
2878
2938
|
: acPillHTML("muted", "M5 11h14a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1ZM8 11V7a4 4 0 0 1 8 0v4", "纯对话 · 不碰任何代码仓库");
|
|
2879
2939
|
summaryEl.innerHTML = html;
|
|
2880
2940
|
}
|
|
2881
2941
|
|
|
2942
|
+
function acHasAgentGitIdentity(ac) {
|
|
2943
|
+
const typedValue = ac.querySelector("#ac-gitlab-token")?.value ?? "";
|
|
2944
|
+
if (typedValue.trim()) return true;
|
|
2945
|
+
const configuredEl = ac.querySelector("#ac-token-configured");
|
|
2946
|
+
return !!(configuredEl && configuredEl.style.display !== "none");
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2882
2949
|
function acUpdateAdvSummary(ac) {
|
|
2883
2950
|
const summaryEl = ac.querySelector("#ac-adv-summary");
|
|
2884
2951
|
if (!summaryEl) return;
|
|
@@ -3400,7 +3467,7 @@ function readFormValues(form) {
|
|
|
3400
3467
|
.map((s) => s.trim())
|
|
3401
3468
|
.filter(Boolean)
|
|
3402
3469
|
.map((line) => {
|
|
3403
|
-
const [slug, branch = "
|
|
3470
|
+
const [slug, branch = "main"] = line.split(":").map((p) => p.trim());
|
|
3404
3471
|
return { slug, branch };
|
|
3405
3472
|
});
|
|
3406
3473
|
const peers = peersRaw
|
|
@@ -3504,6 +3571,8 @@ async function saveMemory(panel, id) {
|
|
|
3504
3571
|
* memory 随「保存」一起提交。序列化字段见 readAgentConfigValues。
|
|
3505
3572
|
*/
|
|
3506
3573
|
async function saveAcBot(panel, id) {
|
|
3574
|
+
if (!validateCodeAccessConfig(panel)) return;
|
|
3575
|
+
|
|
3507
3576
|
const vals = readAgentConfigValues(panel);
|
|
3508
3577
|
const memContent = vals._memContent ?? "";
|
|
3509
3578
|
|
|
@@ -3602,22 +3671,10 @@ function renderServiceIndicator() {
|
|
|
3602
3671
|
icon + label +
|
|
3603
3672
|
`</button>`;
|
|
3604
3673
|
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
`<span class="lk-svc-label">` +
|
|
3610
|
-
`<span class="bridge-dot is-stopped" style="margin-right:6px"></span>` +
|
|
3611
|
-
`服务未运行` +
|
|
3612
|
-
`<span class="lk-svc-sublabel"> · 助手都不会回复</span>` +
|
|
3613
|
-
`</span>` +
|
|
3614
|
-
svcBtn("btn-svc-start", "#fecaca", ICONS.zap, "启动服务", "start") +
|
|
3615
|
-
`</div>`;
|
|
3616
|
-
container.querySelector("#btn-svc-start")?.addEventListener("click", () => doFixAction("start"));
|
|
3617
|
-
return;
|
|
3618
|
-
}
|
|
3619
|
-
|
|
3620
|
-
// ── BL-18:重启过渡态 ─────────────────────────
|
|
3674
|
+
// ── BL-18:启动/重启过渡态 ─────────────────────
|
|
3675
|
+
// Must run before the `!running` branch: after clicking "启动服务", the
|
|
3676
|
+
// backend has accepted the start but detectBridgeStatus may still report
|
|
3677
|
+
// running=false for a few seconds while bridge boots and bots reconnect.
|
|
3621
3678
|
if (!readonly) {
|
|
3622
3679
|
const rs = state.restart;
|
|
3623
3680
|
|
|
@@ -3664,6 +3721,21 @@ function renderServiceIndicator() {
|
|
|
3664
3721
|
}
|
|
3665
3722
|
}
|
|
3666
3723
|
|
|
3724
|
+
if (!running) {
|
|
3725
|
+
// running=false → 红报警条(+ indigo 启动按钮,只读时省略)
|
|
3726
|
+
container.innerHTML =
|
|
3727
|
+
`<div class="lk-svc-indicator lk-svc-indicator--offline">` +
|
|
3728
|
+
`<span class="lk-svc-label">` +
|
|
3729
|
+
`<span class="bridge-dot is-stopped" style="margin-right:6px"></span>` +
|
|
3730
|
+
`服务未运行` +
|
|
3731
|
+
`<span class="lk-svc-sublabel"> · 助手都不会回复</span>` +
|
|
3732
|
+
`</span>` +
|
|
3733
|
+
svcBtn("btn-svc-start", "#fecaca", ICONS.zap, "启动服务", "start") +
|
|
3734
|
+
`</div>`;
|
|
3735
|
+
container.querySelector("#btn-svc-start")?.addEventListener("click", () => doFixAction("start"));
|
|
3736
|
+
return;
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3667
3739
|
// 综合 pendingRestart(后端感知新增/已删)与本地 pendingNewCount(客户端推算)
|
|
3668
3740
|
// 后端字段更权威;本地计算作回退(后端未返回时)。
|
|
3669
3741
|
const pr = state.pendingRestart;
|
|
@@ -3781,29 +3853,33 @@ async function doFixAction(action, triggerEl = null) {
|
|
|
3781
3853
|
const errMsg = res.json?.message ?? res.json?.error ?? res.status;
|
|
3782
3854
|
toast(`操作失败:${errMsg}`, "error");
|
|
3783
3855
|
// Refresh status even on failure
|
|
3856
|
+
await refreshRuntimeRequirements();
|
|
3784
3857
|
await refreshBridgeStatus();
|
|
3785
3858
|
await pollStatus();
|
|
3786
3859
|
if (state.selected) rerenderStatusAction(state.selected);
|
|
3787
3860
|
return;
|
|
3788
3861
|
}
|
|
3789
3862
|
|
|
3790
|
-
// ── BL-18:POST 成功后 → 进入 restarting 过渡态(
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3863
|
+
// ── BL-18:POST 成功后 → 进入 restarting 过渡态(start/restart 都走同一套) ──
|
|
3864
|
+
// "启动服务" 和 "重启服务" 都调用同一个后端 restart endpoint。返回成功只
|
|
3865
|
+
// 代表 supervisor 已接受请求,不代表 bridge 已连回飞书或 bot 已写心跳。
|
|
3866
|
+
stopRestartTicker();
|
|
3867
|
+
state.restart = { status: "restarting", startedAt: Date.now(), elapsed: 0 };
|
|
3868
|
+
toast(
|
|
3869
|
+
action === "start"
|
|
3870
|
+
? "正在启动服务 —— 连上后会自动转回正常,不用重复点"
|
|
3871
|
+
: "正在重启服务 —— 好了会自动转回正常,不用重复点",
|
|
3872
|
+
"info",
|
|
3873
|
+
);
|
|
3874
|
+
// 全量刷新三触点(顶栏+名册+hero)
|
|
3875
|
+
renderServiceIndicator();
|
|
3876
|
+
renderBotList();
|
|
3877
|
+
if (state.selected) refreshDetailHero();
|
|
3878
|
+
// 启动 1s ticker 刷新已用时显示
|
|
3879
|
+
startRestartTicker();
|
|
3805
3880
|
|
|
3806
3881
|
// Refresh bridge + liveness
|
|
3882
|
+
await refreshRuntimeRequirements();
|
|
3807
3883
|
await refreshBridgeStatus();
|
|
3808
3884
|
await pollStatus();
|
|
3809
3885
|
if (state.selected) rerenderStatusAction(state.selected);
|
|
@@ -3819,6 +3895,7 @@ async function doFixAction(action, triggerEl = null) {
|
|
|
3819
3895
|
const BURST_INTERVAL = 2000;
|
|
3820
3896
|
_restartPollHandle = setInterval(async () => {
|
|
3821
3897
|
burstCount++;
|
|
3898
|
+
await refreshRuntimeRequirements();
|
|
3822
3899
|
await refreshBridgeStatus();
|
|
3823
3900
|
await pollStatus();
|
|
3824
3901
|
if (state.selected) rerenderStatusAction(state.selected);
|
|
@@ -4326,6 +4403,7 @@ function renderOnboardNameForm(prefill, sessionId) {
|
|
|
4326
4403
|
async function submitOnboardName(prefill, sessionId, modal) {
|
|
4327
4404
|
if (!modal) modal = document.getElementById("onboard-modal");
|
|
4328
4405
|
const sid = sessionId ?? onboard.sessionId;
|
|
4406
|
+
if (!validateCodeAccessConfig(modal)) return;
|
|
4329
4407
|
|
|
4330
4408
|
// 从 AC 面板读取表单值
|
|
4331
4409
|
const vals = readAgentConfigValues(modal);
|
|
@@ -4553,6 +4631,7 @@ async function boot() {
|
|
|
4553
4631
|
|
|
4554
4632
|
// 拉 bot 列表(先于首次状态轮询,确保左侧条目存在好让圆点落上去)
|
|
4555
4633
|
await loadBots();
|
|
4634
|
+
await refreshRuntimeRequirements();
|
|
4556
4635
|
|
|
4557
4636
|
// 拉 bridge 服务状态并渲染顶栏指示
|
|
4558
4637
|
await refreshBridgeStatus();
|
|
@@ -4560,6 +4639,7 @@ async function boot() {
|
|
|
4560
4639
|
// 首次拉状态 + 每 15s 轮询(实时在线状态可视化)
|
|
4561
4640
|
await pollStatus();
|
|
4562
4641
|
setInterval(pollStatus, 15000);
|
|
4642
|
+
setInterval(refreshRuntimeRequirements, 30000);
|
|
4563
4643
|
}
|
|
4564
4644
|
|
|
4565
4645
|
/**
|
|
@@ -4571,4 +4651,11 @@ async function pollStatus() {
|
|
|
4571
4651
|
renderStatus(res.ok ? res.json : null);
|
|
4572
4652
|
}
|
|
4573
4653
|
|
|
4654
|
+
async function refreshRuntimeRequirements() {
|
|
4655
|
+
const res = await api("GET", "/api/runtime/requirements");
|
|
4656
|
+
state.requirements = res.ok ? res.json : null;
|
|
4657
|
+
renderServiceIndicator();
|
|
4658
|
+
if (state.selected) rerenderStatusAction(state.selected);
|
|
4659
|
+
}
|
|
4660
|
+
|
|
4574
4661
|
boot();
|
|
@@ -3266,18 +3266,12 @@ textarea.input {
|
|
|
3266
3266
|
.ac-repo-row {
|
|
3267
3267
|
position: relative;
|
|
3268
3268
|
padding: 13px 14px;
|
|
3269
|
+
padding-right: 52px;
|
|
3269
3270
|
border-radius: 11px;
|
|
3270
3271
|
border: 1px solid var(--border);
|
|
3271
3272
|
background: #fff;
|
|
3272
3273
|
}
|
|
3273
3274
|
|
|
3274
|
-
.ac-repo-grid {
|
|
3275
|
-
display: grid;
|
|
3276
|
-
grid-template-columns: 1fr 120px;
|
|
3277
|
-
gap: 10px;
|
|
3278
|
-
margin-bottom: 10px;
|
|
3279
|
-
}
|
|
3280
|
-
|
|
3281
3275
|
.ac-repo-del {
|
|
3282
3276
|
position: absolute;
|
|
3283
3277
|
top: 11px;
|
|
@@ -3358,6 +3352,39 @@ textarea.input {
|
|
|
3358
3352
|
gap: 16px;
|
|
3359
3353
|
}
|
|
3360
3354
|
|
|
3355
|
+
.ac-token-advanced {
|
|
3356
|
+
border: 1px solid var(--border);
|
|
3357
|
+
border-radius: 10px;
|
|
3358
|
+
background: #fff;
|
|
3359
|
+
padding: 0;
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
.ac-token-advanced > summary {
|
|
3363
|
+
display: flex;
|
|
3364
|
+
align-items: baseline;
|
|
3365
|
+
gap: 8px;
|
|
3366
|
+
padding: 11px 13px;
|
|
3367
|
+
cursor: pointer;
|
|
3368
|
+
list-style: none;
|
|
3369
|
+
font-size: 13.5px;
|
|
3370
|
+
font-weight: 700;
|
|
3371
|
+
color: var(--text);
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
.ac-token-advanced > summary::-webkit-details-marker {
|
|
3375
|
+
display: none;
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
.ac-token-advanced > summary span {
|
|
3379
|
+
font-size: 12.5px;
|
|
3380
|
+
font-weight: 500;
|
|
3381
|
+
color: var(--muted);
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
.ac-token-advanced > .ac-field {
|
|
3385
|
+
padding: 0 13px 13px;
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3361
3388
|
/* ── 保存栏(edit 模式 sticky,复用 .form-actions-sticky 的 sticky + gradient) ── */
|
|
3362
3389
|
.ac-save-bar {
|
|
3363
3390
|
display: flex;
|