larkway 0.3.8 → 0.3.10
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 +179 -45
- 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 +14 -16
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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "larkway",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"description": "Thin bridge: Feishu thread to local Claude Code CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Chuck Wu (chuckwu0)",
|
|
@@ -19,18 +19,6 @@
|
|
|
19
19
|
"bin": {
|
|
20
20
|
"larkway": "bin/larkway"
|
|
21
21
|
},
|
|
22
|
-
"scripts": {
|
|
23
|
-
"start": "tsx src/main.ts",
|
|
24
|
-
"cli": "tsx src/cli/index.ts",
|
|
25
|
-
"start:supervised": "bin/start-bridge.sh",
|
|
26
|
-
"dev": "tsx watch src/main.ts",
|
|
27
|
-
"build": "node scripts/build.mjs",
|
|
28
|
-
"prepack": "node scripts/build.mjs",
|
|
29
|
-
"typecheck": "tsc --noEmit",
|
|
30
|
-
"check:links": "bash bin/check-doc-links.sh",
|
|
31
|
-
"test": "vitest run",
|
|
32
|
-
"test:v0.3": "bash bin/v0.3-local-acceptance.sh"
|
|
33
|
-
},
|
|
34
22
|
"dependencies": {},
|
|
35
23
|
"devDependencies": {
|
|
36
24
|
"@types/js-yaml": "^4.0.9",
|
|
@@ -48,7 +36,6 @@
|
|
|
48
36
|
"qrcode-terminal": "^0.12.0",
|
|
49
37
|
"zod": "^3.22.0"
|
|
50
38
|
},
|
|
51
|
-
"packageManager": "pnpm@9.0.0",
|
|
52
39
|
"engines": {
|
|
53
40
|
"node": ">=20"
|
|
54
41
|
},
|
|
@@ -56,5 +43,16 @@
|
|
|
56
43
|
"dist/",
|
|
57
44
|
"bin/larkway",
|
|
58
45
|
"README.md"
|
|
59
|
-
]
|
|
60
|
-
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"start": "tsx src/main.ts",
|
|
49
|
+
"cli": "tsx src/cli/index.ts",
|
|
50
|
+
"start:supervised": "bin/start-bridge.sh",
|
|
51
|
+
"dev": "tsx watch src/main.ts",
|
|
52
|
+
"build": "node scripts/build.mjs",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"check:links": "bash bin/check-doc-links.sh",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"test:v0.3": "bash bin/v0.3-local-acceptance.sh"
|
|
57
|
+
}
|
|
58
|
+
}
|