larkway 0.3.7 → 0.3.8
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 -5
- package/README.zh.md +1 -5
- package/dist/cli/index.js +6008 -7265
- package/dist/main.js +35 -42
- package/dist/web/public/app.js +74 -1571
- package/dist/web/public/index.html +5 -37
- package/dist/web/public/style.css +29 -504
- package/package.json +1 -1
package/dist/web/public/app.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Larkway 管理面 — 原生 ES module SPA,无框架、无构建步骤。
|
|
5
5
|
*
|
|
6
|
-
* Token 解析:server 注入 window.
|
|
6
|
+
* Token 解析:server 注入 window.__LK_BOOT_TOKEN__ > ?token= 回退。
|
|
7
7
|
* 所有 /api/* 请求自动带 X-Larkway-Token header。
|
|
8
8
|
* secret 绝不显示真值(gitlab_token_env 变量名是内部细节,UI 不暴露)。
|
|
9
9
|
*
|
|
@@ -37,7 +37,6 @@ const ICONS = {
|
|
|
37
37
|
scan: `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 8V6a2 2 0 0 1 2-2h2M16 4h2a2 2 0 0 1 2 2v2M20 16v2a2 2 0 0 1-2 2h-2M8 20H6a2 2 0 0 1-2-2v-2"/></svg>`,
|
|
38
38
|
box: `<svg class="icon icon-sm" viewBox="0 0 24 24" aria-hidden="true"><path d="M21 8 12 3 3 8v8l9 5 9-5Z"/><path d="m3 8 9 5 9-5M12 13v8"/></svg>`,
|
|
39
39
|
plus: `<svg class="icon icon-sm" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>`,
|
|
40
|
-
// ── 公司中心库专用(centralData.jsx CC_ICON + LK_ICON 复刻)──
|
|
41
40
|
repo: `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 6a2 2 0 0 1 2-2h11l3 3v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2zM16 4v3h3M7 13h8M7 16h5"/></svg>`,
|
|
42
41
|
branch: `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 4v9M6 20a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM6 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM18 8a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM18 6c0 5-6 4-6 9"/></svg>`,
|
|
43
42
|
folder: `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>`,
|
|
@@ -101,7 +100,7 @@ function heartSVG(liveKey, w = 72, h = 22, sw = 1.8) {
|
|
|
101
100
|
const PLACEHOLDER = "__LARKWAY_TOKEN__";
|
|
102
101
|
|
|
103
102
|
function resolveToken() {
|
|
104
|
-
const injected = window.
|
|
103
|
+
const injected = window.__LK_BOOT_TOKEN__;
|
|
105
104
|
if (typeof injected === "string" && injected && injected !== PLACEHOLDER) {
|
|
106
105
|
return injected;
|
|
107
106
|
}
|
|
@@ -287,7 +286,7 @@ function deleteAssistantDialog(bot) {
|
|
|
287
286
|
<div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:10px 14px;display:flex;align-items:flex-start;gap:10px;margin-bottom:20px">
|
|
288
287
|
<input type="checkbox" id="del-ack-chk" style="margin-top:3px;accent-color:#dc2626;flex-shrink:0" />
|
|
289
288
|
<label for="del-ack-chk" style="font-size:13.5px;color:var(--text);cursor:pointer;line-height:1.5">
|
|
290
|
-
|
|
289
|
+
我明白删除后它会停止服务。
|
|
291
290
|
</label>
|
|
292
291
|
</div>
|
|
293
292
|
</div>
|
|
@@ -326,110 +325,6 @@ function deleteAssistantDialog(bot) {
|
|
|
326
325
|
});
|
|
327
326
|
}
|
|
328
327
|
|
|
329
|
-
// ---------------------------------------------------------------------------
|
|
330
|
-
// uploadConfirmDialog — 正式上传(晋升 push)二次确认弹窗
|
|
331
|
-
// 复刻 centralPromote.jsx LkUploadConfirm:红 upload 徽章 + 标题 + 三条「会发生
|
|
332
|
-
// 什么」+ ack 勾选 gate + 红「确认上传」。不可逆外发,样式走 destructive 红。
|
|
333
|
-
// ---------------------------------------------------------------------------
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* 弹出「正式上传到公司中心库」二次确认弹窗,返回 Promise<boolean>。
|
|
337
|
-
* 勾选「我明白这是不可逆外发」前确认按钮 disabled。点确认 → true;取消/Esc/点背景 → false。
|
|
338
|
-
*
|
|
339
|
-
* @param {{name:string}} bot 要上传的助手(显示名)
|
|
340
|
-
* @param {{name?:string,branch?:string}|null} repo 中心库信息(展示推到哪)
|
|
341
|
-
* @returns {Promise<boolean>}
|
|
342
|
-
*/
|
|
343
|
-
function uploadConfirmDialog(bot, repo) {
|
|
344
|
-
return new Promise((resolve) => {
|
|
345
|
-
let resolved = false;
|
|
346
|
-
function done(result) {
|
|
347
|
-
if (resolved) return;
|
|
348
|
-
resolved = true;
|
|
349
|
-
document.removeEventListener("keydown", onKey);
|
|
350
|
-
backdrop.remove();
|
|
351
|
-
resolve(result);
|
|
352
|
-
}
|
|
353
|
-
function onKey(e) {
|
|
354
|
-
if (e.key === "Escape") done(false);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const botName = esc(bot?.name || "这个助手");
|
|
358
|
-
const repoName = esc(repo?.name || "公司中心库");
|
|
359
|
-
const branch = esc(repo?.branch || "main");
|
|
360
|
-
|
|
361
|
-
// 三条「会发生什么」(upload / users / lock)
|
|
362
|
-
const bullets = [
|
|
363
|
-
[ICONS.upload, `推送到 ${repoName}@${branch}`],
|
|
364
|
-
[ICONS.users, "团队任何人都能同步、复用这一份"],
|
|
365
|
-
[ICONS.lock, "只上传配置 —— 密钥、本机 .env 永远不会被推上去"],
|
|
366
|
-
];
|
|
367
|
-
|
|
368
|
-
const uploadBadge = `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true" style="width:19px;height:19px"><path d="M12 15V3M8 7l4-4 4 4"/><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg>`;
|
|
369
|
-
const uploadIcon15 = `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true" style="width:15px;height:15px;flex-shrink:0"><path d="M12 15V3M8 7l4-4 4 4"/><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg>`;
|
|
370
|
-
|
|
371
|
-
const backdrop = document.createElement("div");
|
|
372
|
-
backdrop.className = "modal-backdrop";
|
|
373
|
-
backdrop.setAttribute("role", "dialog");
|
|
374
|
-
backdrop.setAttribute("aria-modal", "true");
|
|
375
|
-
|
|
376
|
-
backdrop.innerHTML = `
|
|
377
|
-
<div class="modal" style="max-width:520px">
|
|
378
|
-
<div class="modal-body" style="padding:24px 26px 0">
|
|
379
|
-
<div style="display:flex;gap:14px;align-items:flex-start">
|
|
380
|
-
<div style="flex-shrink:0;width:40px;height:40px;border-radius:11px;background:var(--destructive-weak);border:1px solid #fecaca;color:var(--destructive-strong);display:flex;align-items:center;justify-content:center">
|
|
381
|
-
${uploadBadge}
|
|
382
|
-
</div>
|
|
383
|
-
<div style="min-width:0">
|
|
384
|
-
<div style="font-size:18px;font-weight:700;color:var(--text);line-height:1.3">正式上传「${botName}」?</div>
|
|
385
|
-
<p style="margin:6px 0 0;font-size:13.5px;color:var(--muted);line-height:1.58">
|
|
386
|
-
这会把它推送到团队仓库 <b style="color:var(--text);font-family:ui-monospace,monospace">${repoName}</b>,<b style="color:var(--destructive-text)">所有人同步都能拉到</b>。推上去之后<b style="color:var(--destructive-text)">没法自动撤回</b>(要撤得让工程师改仓库)。
|
|
387
|
-
</p>
|
|
388
|
-
</div>
|
|
389
|
-
</div>
|
|
390
|
-
<ul style="list-style:none;margin:14px 0 0;padding:0;display:flex;flex-direction:column;gap:8px">
|
|
391
|
-
${bullets
|
|
392
|
-
.map(
|
|
393
|
-
([ic, tx]) =>
|
|
394
|
-
`<li style="display:flex;align-items:center;gap:9px;font-size:13px;color:#334155"><span style="color:var(--faint);flex-shrink:0;display:inline-flex">${ic}</span>${esc(tx)}</li>`,
|
|
395
|
-
)
|
|
396
|
-
.join("")}
|
|
397
|
-
</ul>
|
|
398
|
-
<label style="display:flex;align-items:flex-start;gap:9px;margin:16px 0 0;padding:12px 14px;border-radius:10px;background:var(--bg);border:1px solid var(--border);cursor:pointer">
|
|
399
|
-
<input type="checkbox" id="up-ack-chk" style="margin-top:2px;width:16px;height:16px;accent-color:var(--destructive-strong);flex-shrink:0;cursor:pointer" />
|
|
400
|
-
<span style="font-size:13px;color:var(--text);line-height:1.5">我明白这是不可逆的外发,全团队同步都会拉到这一份。</span>
|
|
401
|
-
</label>
|
|
402
|
-
</div>
|
|
403
|
-
<div class="modal-footer" style="padding:18px 26px 22px">
|
|
404
|
-
<div class="modal-btns">
|
|
405
|
-
<button class="btn" id="up-cancel" type="button">取消</button>
|
|
406
|
-
<button class="btn btn-del-confirm" id="up-confirm" type="button" disabled>
|
|
407
|
-
${uploadIcon15} 确认上传
|
|
408
|
-
</button>
|
|
409
|
-
</div>
|
|
410
|
-
</div>
|
|
411
|
-
</div>
|
|
412
|
-
`;
|
|
413
|
-
|
|
414
|
-
const ackChk = backdrop.querySelector("#up-ack-chk");
|
|
415
|
-
const confirmBtn = backdrop.querySelector("#up-confirm");
|
|
416
|
-
ackChk.addEventListener("change", () => {
|
|
417
|
-
confirmBtn.disabled = !ackChk.checked;
|
|
418
|
-
});
|
|
419
|
-
backdrop.addEventListener("click", (e) => {
|
|
420
|
-
if (e.target === backdrop) done(false);
|
|
421
|
-
});
|
|
422
|
-
backdrop.querySelector("#up-cancel").addEventListener("click", () => done(false));
|
|
423
|
-
confirmBtn.addEventListener("click", () => {
|
|
424
|
-
if (!ackChk.checked) return;
|
|
425
|
-
done(true);
|
|
426
|
-
});
|
|
427
|
-
document.addEventListener("keydown", onKey);
|
|
428
|
-
|
|
429
|
-
document.body.appendChild(backdrop);
|
|
430
|
-
backdrop.querySelector("#up-cancel").focus();
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
328
|
|
|
434
329
|
/**
|
|
435
330
|
* 删除助手流程:弹确认 → DELETE /api/bot/:id → 更新 state → toast。
|
|
@@ -474,14 +369,12 @@ async function doDeleteBot(id) {
|
|
|
474
369
|
// ---------------------------------------------------------------------------
|
|
475
370
|
|
|
476
371
|
const state = {
|
|
477
|
-
/** @type {"local"
|
|
372
|
+
/** @type {"local"} */
|
|
478
373
|
mode: "local",
|
|
479
374
|
/** @type {Array<{id:string,name:string,description:string,avatar:string|null}>} */
|
|
480
375
|
bots: [],
|
|
481
376
|
/** @type {string|null} */
|
|
482
377
|
selected: null,
|
|
483
|
-
/** 中心上下文是否可用 */
|
|
484
|
-
centralAvailable: false,
|
|
485
378
|
/** Bridge 进程状态(来自 GET /api/bridge;null = 未知)。 */
|
|
486
379
|
bridge: /** @type {{running:boolean,pid:number|null,platform:string,mode:string}|null} */ (null),
|
|
487
380
|
/**
|
|
@@ -515,21 +408,6 @@ const state = {
|
|
|
515
408
|
/** 当前详情 memory 是否有未保存改动。 */
|
|
516
409
|
memoryDirty: false,
|
|
517
410
|
|
|
518
|
-
// ── 公司中心库(connect / roster / sync)──────────────────────────────
|
|
519
|
-
/** 是否已连接中心库(config.json.centralConfig 已设)。来自 GET /api/central/status。 */
|
|
520
|
-
centralConnected: false,
|
|
521
|
-
/** 已连接时的仓库信息 { name, url, branch, path }。 */
|
|
522
|
-
centralRepo: /** @type {{name:string,url:string,branch:string,path:string}|null} */ (null),
|
|
523
|
-
/** 最近同步时间(ms epoch,来自 status.lastSyncMs);null = 未知。 */
|
|
524
|
-
centralLastSyncMs: /** @type {number|null} */ (null),
|
|
525
|
-
/** 中心库共享助手数(来自 status.sharedCount)。 */
|
|
526
|
-
centralSharedCount: 0,
|
|
527
|
-
/** 中心库只读名册 [{id,name,desc,by,updated,commit,chats,repos}]。来自 GET /api/central/bots。 */
|
|
528
|
-
centralBots: /** @type {Array<{id:string,name:string,desc:string,by:string,updated:string,commit:string,chats:number,repos:number,avatar:string|null}>} */ ([]),
|
|
529
|
-
/** 来源条同步态:'fresh'(已是最新) | 'updates'(有更新可拉) | 'syncing' | 'unknown'。 */
|
|
530
|
-
centralSyncState: /** @type {"fresh"|"updates"|"syncing"|"unknown"} */ ("unknown"),
|
|
531
|
-
/** 可更新项数(added+updated+removed),驱动来源条「N 项可更新」pill。 */
|
|
532
|
-
centralUpdateCount: 0,
|
|
533
411
|
/**
|
|
534
412
|
* 待重启提示:来自 GET /api/status 的 pendingRestart 字段。
|
|
535
413
|
* newCount = 有 yaml 无 status.json(新增 bot,待重启上线);
|
|
@@ -825,24 +703,9 @@ async function loadBackends() {
|
|
|
825
703
|
* 选中项用 indigo 交互色;未就绪提示用中性 slate(绝不染状态色)。
|
|
826
704
|
*
|
|
827
705
|
* @param {string} value 当前选中的 backend id
|
|
828
|
-
* @param {boolean} [readOnly] 只读态(中心库展示)
|
|
829
706
|
* @param {string} [containerId] 容器 id(用于 change 事件冒泡)
|
|
830
707
|
*/
|
|
831
|
-
function lkBackendSelectHTML(value,
|
|
832
|
-
if (readOnly) {
|
|
833
|
-
const b = lkBackend(value);
|
|
834
|
-
return (
|
|
835
|
-
`<div class="lk-bk-readonly">` +
|
|
836
|
-
lkBackendMonoHTML(value, "lg") +
|
|
837
|
-
`<div style="min-width:0;flex:1">` +
|
|
838
|
-
`<div class="lk-bk-readonly-name">${esc(b.name)}</div>` +
|
|
839
|
-
`<div class="lk-bk-readonly-vendor">${esc(b.vendor)}</div>` +
|
|
840
|
-
`</div>` +
|
|
841
|
-
`<span class="lk-bk-readonly-lock">${ICONS.lock} 只读</span>` +
|
|
842
|
-
`</div>`
|
|
843
|
-
);
|
|
844
|
-
}
|
|
845
|
-
|
|
708
|
+
function lkBackendSelectHTML(value, containerId = "") {
|
|
846
709
|
const items = LK_BACKEND_ORDER.map((id) => {
|
|
847
710
|
const b = lkBackend(id);
|
|
848
711
|
const sel = id === value;
|
|
@@ -1390,589 +1253,12 @@ function formatDuration(ms) {
|
|
|
1390
1253
|
// ---------------------------------------------------------------------------
|
|
1391
1254
|
|
|
1392
1255
|
function renderContextSwitch() {
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
// 「公司中心库」段永远可点 —— 未连接时进去看的是连接引导卡(这正是要进的页)。
|
|
1397
|
-
const central = document.getElementById("ctx-central");
|
|
1398
|
-
if (central) central.disabled = false;
|
|
1399
|
-
|
|
1400
|
-
// 侧栏底部按钮:本机显「添加新助手」;中心(只读)隐藏添加,改在 renderBotList 注入
|
|
1401
|
-
// 「去本机添加/晋升」。旧的 #btn-sync(从公司中心库拉取)由来源条的同步入口替代,常隐藏。
|
|
1402
|
-
const btnAdd = document.getElementById("btn-add");
|
|
1403
|
-
if (btnAdd) btnAdd.style.display = state.mode === "central" ? "none" : "";
|
|
1404
|
-
const btnSync = document.getElementById("btn-sync");
|
|
1405
|
-
if (btnSync) btnSync.style.display = "none";
|
|
1406
|
-
|
|
1407
|
-
// 中心库(只读)= 无「添加」按钮,改放「去本机添加/晋升」引导(CcRoster 底部)。
|
|
1408
|
-
const actions = document.getElementById("sidebar-actions");
|
|
1409
|
-
if (actions) {
|
|
1410
|
-
let goLocal = document.getElementById("cc-go-local-block");
|
|
1411
|
-
if (state.mode === "central") {
|
|
1412
|
-
if (!goLocal) {
|
|
1413
|
-
goLocal = document.createElement("button");
|
|
1414
|
-
goLocal.id = "cc-go-local-block";
|
|
1415
|
-
goLocal.type = "button";
|
|
1416
|
-
goLocal.className = "cc-go-local-block";
|
|
1417
|
-
goLocal.innerHTML = `${ICONS.plus} 去「本机」添加 / 晋升`;
|
|
1418
|
-
goLocal.addEventListener("click", () => switchContext("local"));
|
|
1419
|
-
actions.appendChild(goLocal);
|
|
1420
|
-
}
|
|
1421
|
-
goLocal.style.display = "";
|
|
1422
|
-
} else if (goLocal) {
|
|
1423
|
-
goLocal.style.display = "none";
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
// ---------------------------------------------------------------------------
|
|
1429
|
-
// 渲染:公司中心库来源条(LkSyncBar 复刻)
|
|
1430
|
-
// ---------------------------------------------------------------------------
|
|
1431
|
-
|
|
1432
|
-
/** 把「距上次同步的毫秒数」换算成「N 分钟前 / 刚刚 / N 小时前」。 */
|
|
1433
|
-
function formatSyncAgo(lastSyncMs) {
|
|
1434
|
-
if (typeof lastSyncMs !== "number" || !isFinite(lastSyncMs)) return null;
|
|
1435
|
-
const diff = Date.now() - lastSyncMs;
|
|
1436
|
-
if (diff < 0) return "刚刚";
|
|
1437
|
-
const min = Math.floor(diff / 60000);
|
|
1438
|
-
if (min < 1) return "刚刚";
|
|
1439
|
-
if (min < 60) return `${min} 分钟前`;
|
|
1440
|
-
const hr = Math.floor(min / 60);
|
|
1441
|
-
if (hr < 24) return `${hr} 小时前`;
|
|
1442
|
-
return `${Math.floor(hr / 24)} 天前`;
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
/**
|
|
1446
|
-
* 渲染顶部来源条:来自 <仓库>@<分支> · 最近同步 N 分钟前 + 同步入口。
|
|
1447
|
-
* 仅在 central 已连接态显示;其余态隐藏。
|
|
1448
|
-
* 状态:'fresh'(已是最新) / 'updates'(有更新可拉) / 'syncing' / 'unknown'(检查更新)。
|
|
1449
|
-
*/
|
|
1450
|
-
function renderCentralSourceBar() {
|
|
1451
|
-
const bar = document.getElementById("central-source-bar");
|
|
1452
|
-
if (!bar) return;
|
|
1453
|
-
|
|
1454
|
-
if (state.mode !== "central" || !state.centralConnected || !state.centralRepo) {
|
|
1455
|
-
bar.hidden = true;
|
|
1456
|
-
bar.innerHTML = "";
|
|
1457
|
-
return;
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
const r = state.centralRepo;
|
|
1461
|
-
const syncState = state.centralSyncState;
|
|
1462
|
-
const ago = syncState === "syncing" ? "…" : (formatSyncAgo(state.centralLastSyncMs) ?? "未知");
|
|
1463
|
-
|
|
1464
|
-
// 右侧状态指示(updates pill / fresh 平静态)
|
|
1465
|
-
let statusBadge = "";
|
|
1466
|
-
if (syncState === "updates") {
|
|
1467
|
-
statusBadge = `<span class="csb-updates">${state.centralUpdateCount} 项可更新</span>`;
|
|
1468
|
-
} else if (syncState === "fresh") {
|
|
1469
|
-
statusBadge = `<span class="csb-fresh">${ICONS.check} 已是最新</span>`;
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
const hasUpdates = syncState === "updates";
|
|
1473
|
-
const syncing = syncState === "syncing";
|
|
1474
|
-
const btnLabel = syncing ? "正在同步…" : hasUpdates ? "拉取最新" : "检查更新";
|
|
1475
|
-
const btnIcon = syncing
|
|
1476
|
-
? `<span class="spinner" style="width:14px;height:14px;border-width:2px"></span>`
|
|
1477
|
-
: ICONS.pull;
|
|
1478
|
-
|
|
1479
|
-
bar.innerHTML =
|
|
1480
|
-
`<span class="csb-from">` +
|
|
1481
|
-
ICONS.repo +
|
|
1482
|
-
`<span>来自 <b class="csb-repo">${esc(r.name)}</b></span>` +
|
|
1483
|
-
`<span class="csb-branch">${ICONS.branch}${esc(r.branch)}</span>` +
|
|
1484
|
-
`<span class="csb-sep">·</span>` +
|
|
1485
|
-
`<span class="csb-synced">最近同步 ${esc(ago)}</span>` +
|
|
1486
|
-
`</span>` +
|
|
1487
|
-
`<span class="csb-right">` +
|
|
1488
|
-
statusBadge +
|
|
1489
|
-
`<button type="button" id="btn-central-sync" class="csb-sync-btn${hasUpdates ? " has-updates" : ""}"${syncing ? " disabled" : ""}>` +
|
|
1490
|
-
btnIcon + esc(btnLabel) +
|
|
1491
|
-
`</button>` +
|
|
1492
|
-
// 已连接设置块(改配置 / 断开)—— LkConnectedBlock 复刻,放在同步入口右侧
|
|
1493
|
-
`<button type="button" id="btn-central-edit" class="csb-sync-btn" title="改中心库配置">${ICONS.gear} 改配置</button>` +
|
|
1494
|
-
`<button type="button" id="btn-central-disconnect" class="csb-sync-btn csb-disconnect-btn" title="断开公司中心库">断开</button>` +
|
|
1495
|
-
`</span>`;
|
|
1496
|
-
bar.hidden = false;
|
|
1497
|
-
|
|
1498
|
-
bar.querySelector("#btn-central-sync")?.addEventListener("click", () => doCentralSync());
|
|
1499
|
-
bar.querySelector("#btn-central-edit")?.addEventListener("click", () => openConnectFlow(true));
|
|
1500
|
-
bar.querySelector("#btn-central-disconnect")?.addEventListener("click", () => doCentralDisconnect());
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
// ---------------------------------------------------------------------------
|
|
1504
|
-
// 公司中心库:连接状态 + 名册拉取
|
|
1505
|
-
// ---------------------------------------------------------------------------
|
|
1506
|
-
|
|
1507
|
-
/**
|
|
1508
|
-
* 拉 GET /api/central/status,写入 state.central*。
|
|
1509
|
-
* connected = config.json.centralConfig 已设;best-effort 回填 repo/head/sharedCount/lastSyncMs。
|
|
1510
|
-
* 失败时降级:connected 保持已知值(不抛),让 UI 仍能渲染连接引导。
|
|
1511
|
-
*/
|
|
1512
|
-
async function loadCentralStatus() {
|
|
1513
|
-
const res = await api("GET", "/api/central/status");
|
|
1514
|
-
if (!res.ok || !res.json) {
|
|
1515
|
-
state.centralConnected = false;
|
|
1516
|
-
state.centralRepo = null;
|
|
1517
|
-
return;
|
|
1518
|
-
}
|
|
1519
|
-
const j = res.json;
|
|
1520
|
-
state.centralConnected = j.connected === true;
|
|
1521
|
-
state.centralRepo = j.repo ?? null;
|
|
1522
|
-
state.centralLastSyncMs = typeof j.lastSyncMs === "number" ? j.lastSyncMs : null;
|
|
1523
|
-
state.centralSharedCount = typeof j.sharedCount === "number" ? j.sharedCount : 0;
|
|
1524
|
-
// 与 /api/context 的 centralAvailable 对齐(连接=可用)
|
|
1525
|
-
state.centralAvailable = state.centralConnected;
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
/**
|
|
1529
|
-
* 拉 GET /api/central/bots → state.centralBots(只读名册,含 by/updated/commit)。
|
|
1530
|
-
* @returns {Promise<boolean>} 成功与否
|
|
1531
|
-
*/
|
|
1532
|
-
async function loadCentralBots() {
|
|
1533
|
-
const res = await api("GET", "/api/central/bots");
|
|
1534
|
-
if (!res.ok) {
|
|
1535
|
-
state.centralBots = [];
|
|
1536
|
-
return false;
|
|
1537
|
-
}
|
|
1538
|
-
state.centralBots = Array.isArray(res.json?.bots) ? res.json.bots : [];
|
|
1539
|
-
return true;
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
// ---------------------------------------------------------------------------
|
|
1543
|
-
// 公司中心库:连接流弹窗(form → connecting → connected/failed)
|
|
1544
|
-
// 复刻 centralConnect.jsx LkConnectFlow。connecting 调 POST /api/central/config。
|
|
1545
|
-
// ---------------------------------------------------------------------------
|
|
1546
|
-
|
|
1547
|
-
/** 连接流弹窗的瞬态(表单值 + 连上后回填的仓库)。 */
|
|
1548
|
-
const connectFlow = {
|
|
1549
|
-
/** @type {{url:string,branch:string,path:string}} */
|
|
1550
|
-
values: { url: "", branch: "main", path: "bots/" },
|
|
1551
|
-
/** @type {{name:string,url:string,branch:string,path:string}|null} */
|
|
1552
|
-
repo: null,
|
|
1553
|
-
/** @type {{kind?:string,error?:string}|null} */
|
|
1554
|
-
fail: null,
|
|
1555
|
-
};
|
|
1556
|
-
|
|
1557
|
-
/** 打开连接流弹窗(默认 form 态;edit=true 时预填现有配置)。 */
|
|
1558
|
-
function openConnectFlow(edit = false) {
|
|
1559
|
-
if (edit && state.centralRepo) {
|
|
1560
|
-
connectFlow.values = {
|
|
1561
|
-
url: state.centralRepo.url ?? "",
|
|
1562
|
-
branch: state.centralRepo.branch ?? "main",
|
|
1563
|
-
path: state.centralRepo.path ?? "bots/",
|
|
1564
|
-
};
|
|
1565
|
-
} else {
|
|
1566
|
-
connectFlow.values = { url: "", branch: "main", path: "bots/" };
|
|
1567
|
-
}
|
|
1568
|
-
connectFlow.repo = null;
|
|
1569
|
-
connectFlow.fail = null;
|
|
1570
|
-
renderConnectFlow("form");
|
|
1571
|
-
const backdrop = document.getElementById("connect-backdrop");
|
|
1572
|
-
if (backdrop) backdrop.hidden = false;
|
|
1573
|
-
document.getElementById("cc-url")?.focus();
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
function closeConnectFlow() {
|
|
1577
|
-
const backdrop = document.getElementById("connect-backdrop");
|
|
1578
|
-
if (backdrop) backdrop.hidden = true;
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
/** 连接弹窗 header(indigo link 徽章 + 标题 + 副标题 + 关闭)。 */
|
|
1582
|
-
function connectHead(title, sub) {
|
|
1583
|
-
return (
|
|
1584
|
-
`<div style="padding:22px 26px 0">` +
|
|
1585
|
-
`<div style="display:flex;align-items:center;justify-content:space-between">` +
|
|
1586
|
-
`<h3 style="margin:0;font-size:18px;font-weight:700;color:var(--text);display:flex;align-items:center;gap:9px">` +
|
|
1587
|
-
`<span style="width:30px;height:30px;border-radius:9px;background:var(--br-soft);border:1px solid var(--br-edge);color:var(--br);display:inline-flex;align-items:center;justify-content:center">${ICONS.link2}</span>` +
|
|
1588
|
-
esc(title) +
|
|
1589
|
-
`</h3>` +
|
|
1590
|
-
`<button type="button" id="cc-close" aria-label="关闭" style="display:inline-flex;padding:6px;border:none;border-radius:8px;background:transparent;color:var(--muted);cursor:pointer">${ICONS.x}</button>` +
|
|
1591
|
-
`</div>` +
|
|
1592
|
-
(sub ? `<p style="margin:8px 0 0;font-size:13.5px;color:var(--muted);line-height:1.55">${esc(sub)}</p>` : "") +
|
|
1593
|
-
`</div>`
|
|
1594
|
-
);
|
|
1256
|
+
// Static local-only mode — show hostname badge, nothing to toggle.
|
|
1257
|
+
const hostEl = document.getElementById("ctx-host");
|
|
1258
|
+
if (hostEl) hostEl.textContent = location.hostname ? " · " + location.hostname : "";
|
|
1595
1259
|
}
|
|
1596
1260
|
|
|
1597
|
-
/**
|
|
1598
|
-
* 渲染连接流弹窗的某个 phase。
|
|
1599
|
-
* @param {"form"|"connecting"|"connected"|"failed"} phase
|
|
1600
|
-
*/
|
|
1601
|
-
function renderConnectFlow(phase) {
|
|
1602
|
-
const modal = document.getElementById("connect-modal");
|
|
1603
|
-
if (!modal) return;
|
|
1604
|
-
const v = connectFlow.values;
|
|
1605
|
-
|
|
1606
|
-
if (phase === "connecting") {
|
|
1607
|
-
const steps = ["找到这个仓库", "验证这台机器有没有权限", `读取 ${v.path || "bots/"} 里的助手`];
|
|
1608
|
-
modal.innerHTML =
|
|
1609
|
-
connectHead("正在连接…", "在测试这台电脑能不能访问这个仓库。不会改动仓库里任何东西。") +
|
|
1610
|
-
`<div style="padding:20px 26px 26px">` +
|
|
1611
|
-
`<div style="display:flex;flex-direction:column;align-items:center;gap:14px;padding:18px 0 22px">` +
|
|
1612
|
-
`<span class="spinner spinner-lg"></span>` +
|
|
1613
|
-
`<code style="font-size:12.5px;color:var(--muted);font-family:ui-monospace,monospace;word-break:break-all;text-align:center;max-width:380px">${esc(v.url)}</code>` +
|
|
1614
|
-
`</div>` +
|
|
1615
|
-
`<ul style="list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px">` +
|
|
1616
|
-
steps.map((s, i) =>
|
|
1617
|
-
`<li style="display:flex;align-items:center;gap:10px;font-size:13.5px;color:${i === 0 ? "var(--text)" : "var(--faint)"}">` +
|
|
1618
|
-
(i === 0
|
|
1619
|
-
? `<span class="spinner" style="width:15px;height:15px;border-width:2px"></span>`
|
|
1620
|
-
: `<span style="width:15px;height:15px;border-radius:50%;border:1.6px solid var(--border);flex-shrink:0"></span>`) +
|
|
1621
|
-
esc(s) + `</li>`
|
|
1622
|
-
).join("") +
|
|
1623
|
-
`</ul></div>`;
|
|
1624
|
-
// connecting 态不可关闭(无 close 按钮)
|
|
1625
|
-
return;
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
if (phase === "connected") {
|
|
1629
|
-
const r = connectFlow.repo ?? state.centralRepo ?? { name: "", branch: "main", path: "bots/" };
|
|
1630
|
-
const rows = [
|
|
1631
|
-
["repo", "仓库", r.name, true],
|
|
1632
|
-
["branch", "分支", r.branch, false],
|
|
1633
|
-
["folder", "助手目录", r.path, false],
|
|
1634
|
-
];
|
|
1635
|
-
modal.innerHTML =
|
|
1636
|
-
`<div style="padding:26px 26px 0;display:flex;gap:14px">` +
|
|
1637
|
-
`<span style="flex-shrink:0;width:40px;height:40px;border-radius:11px;background:${LIVE_SOFT.serving};border:1px solid ${LIVE_EDGE.serving};color:${LIVE_COLOR.serving};display:flex;align-items:center;justify-content:center">${ICONS.check}</span>` +
|
|
1638
|
-
`<div style="min-width:0">` +
|
|
1639
|
-
`<h3 style="margin:0;font-size:18px;font-weight:700;color:var(--text)">连上了</h3>` +
|
|
1640
|
-
`<p style="margin:6px 0 0;font-size:13.5px;color:var(--muted);line-height:1.55">这台电脑已经能访问公司中心库了。现在去本机详情页,<b style="color:var(--br-text)">晋升</b>按钮就能点了。</p>` +
|
|
1641
|
-
`</div></div>` +
|
|
1642
|
-
`<div style="margin:16px 26px 0;border-radius:12px;border:1px solid var(--border);overflow:hidden">` +
|
|
1643
|
-
rows.map(([ic, k, val, mono], i) =>
|
|
1644
|
-
`<div style="display:flex;align-items:center;gap:10px;padding:11px 14px;${i ? "border-top:1px solid var(--border);" : ""}background:${i % 2 ? "var(--surface)" : "var(--bg)"}">` +
|
|
1645
|
-
ICONS[ic] +
|
|
1646
|
-
`<span style="font-size:12.5px;color:var(--faint);width:64px">${esc(k)}</span>` +
|
|
1647
|
-
`<span style="font-size:13.5px;font-weight:600;color:var(--text);${mono ? "font-family:ui-monospace,monospace" : ""}">${esc(val ?? "")}</span>` +
|
|
1648
|
-
`</div>`
|
|
1649
|
-
).join("") +
|
|
1650
|
-
`</div>` +
|
|
1651
|
-
`<p style="margin:14px 26px 0;font-size:13px;color:var(--muted);display:flex;align-items:center;gap:8px">${ICONS.users} 中心库里现在有 <b style="color:var(--text)">${state.centralSharedCount}</b> 个共享助手,去「公司中心库」看看。</p>` +
|
|
1652
|
-
`<div style="display:flex;justify-content:flex-end;gap:10px;padding:20px 26px 22px">` +
|
|
1653
|
-
`<button type="button" id="cc-done" class="btn btn-primary">${ICONS.check} 完成</button>` +
|
|
1654
|
-
`</div>`;
|
|
1655
|
-
modal.querySelector("#cc-done")?.addEventListener("click", () => {
|
|
1656
|
-
closeConnectFlow();
|
|
1657
|
-
// 连上后进入中心库视图并加载名册。
|
|
1658
|
-
// 注意:若此前已在「公司中心库」上下文(从引导卡进来),switchContext('central')
|
|
1659
|
-
// 会因 mode 未变而 no-op,所以这里直接强制刷新中心视图。
|
|
1660
|
-
void enterCentralConnected();
|
|
1661
|
-
});
|
|
1662
|
-
return;
|
|
1663
|
-
}
|
|
1664
1261
|
|
|
1665
|
-
if (phase === "failed") {
|
|
1666
|
-
const f = connectFlow.fail ?? {};
|
|
1667
|
-
const reasons = [
|
|
1668
|
-
["地址可能写错了", "核对一下仓库地址有没有漏字符、对不对。"],
|
|
1669
|
-
["这台机器没有访问权限", "让工程师把你这台电脑的 SSH key 加进仓库,或换 HTTPS + 令牌。"],
|
|
1670
|
-
];
|
|
1671
|
-
const errLine = f.error
|
|
1672
|
-
? `<div style="margin:14px 26px 0;font-size:12.5px;color:var(--destructive-text);line-height:1.5">${esc(f.error)}</div>`
|
|
1673
|
-
: "";
|
|
1674
|
-
modal.innerHTML =
|
|
1675
|
-
`<div style="padding:24px 26px 0;display:flex;gap:14px">` +
|
|
1676
|
-
`<span style="flex-shrink:0;width:40px;height:40px;border-radius:11px;background:${LIVE_SOFT.offline};border:1px solid ${LIVE_EDGE.offline};color:${LIVE_COLOR.offline};display:flex;align-items:center;justify-content:center">${ICONS.warn}</span>` +
|
|
1677
|
-
`<div style="min-width:0">` +
|
|
1678
|
-
`<h3 style="margin:0;font-size:18px;font-weight:700;color:var(--text)">仓库连不上</h3>` +
|
|
1679
|
-
`<p style="margin:6px 0 0;font-size:13.5px;color:var(--muted);line-height:1.55">没改动任何东西。多半是下面两种情况之一:</p>` +
|
|
1680
|
-
`</div></div>` +
|
|
1681
|
-
`<ul style="list-style:none;margin:14px 26px 0;padding:0;display:flex;flex-direction:column;gap:10px">` +
|
|
1682
|
-
reasons.map(([h, d], i) =>
|
|
1683
|
-
`<li style="display:flex;gap:11px;align-items:flex-start;padding:12px 14px;border-radius:10px;background:${LIVE_SOFT.offline};border:1px solid ${LIVE_EDGE.offline}">` +
|
|
1684
|
-
`<span style="flex-shrink:0;width:20px;height:20px;margin-top:1px;border-radius:999px;background:var(--surface);border:1px solid ${LIVE_EDGE.offline};color:${LIVE_TEXT.offline};font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center">${i + 1}</span>` +
|
|
1685
|
-
`<div><div style="font-size:13.5px;font-weight:600;color:#334155">${esc(h)}</div><div style="font-size:12.5px;color:var(--muted);line-height:1.5;margin-top:1px">${esc(d)}</div></div>` +
|
|
1686
|
-
`</li>`
|
|
1687
|
-
).join("") +
|
|
1688
|
-
`</ul>` +
|
|
1689
|
-
errLine +
|
|
1690
|
-
`<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;padding:20px 26px 22px">` +
|
|
1691
|
-
`<button type="button" id="cc-edit" class="btn">改一下配置</button>` +
|
|
1692
|
-
`<button type="button" id="cc-retry" class="btn btn-primary">${ICONS.refresh} 重试</button>` +
|
|
1693
|
-
`</div>`;
|
|
1694
|
-
modal.querySelector("#cc-edit")?.addEventListener("click", () => renderConnectFlow("form"));
|
|
1695
|
-
modal.querySelector("#cc-retry")?.addEventListener("click", () => submitConnect());
|
|
1696
|
-
return;
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
// ── form(默认)──
|
|
1700
|
-
modal.innerHTML =
|
|
1701
|
-
connectHead(
|
|
1702
|
-
"连接公司中心库",
|
|
1703
|
-
"把一个 git 仓库连上,团队的助手就能互相同步。地址一般让工程师帮你配一次,之后就不用管了。",
|
|
1704
|
-
) +
|
|
1705
|
-
`<div style="padding:18px 26px 0;display:flex;flex-direction:column;gap:16px">` +
|
|
1706
|
-
ccField("仓库地址", "团队放助手配置的 git 仓库。SSH 或 HTTPS 都行。不知道填啥?问下工程师。",
|
|
1707
|
-
`<input id="cc-url" class="cc-input" value="${esc(v.url)}" placeholder="git@gitlab.公司.com:team/larkway-bots.git" />`) +
|
|
1708
|
-
`<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">` +
|
|
1709
|
-
ccField("分支", "一般就是 main,别动。", `<input id="cc-branch" class="cc-input" value="${esc(v.branch)}" />`) +
|
|
1710
|
-
ccField("助手放在仓库哪个目录", "团队约定的目录,默认就行。", `<input id="cc-path" class="cc-input" value="${esc(v.path)}" />`) +
|
|
1711
|
-
`</div>` +
|
|
1712
|
-
`<div style="display:flex;align-items:center;gap:8px;padding:10px 13px;border-radius:10px;background:var(--bg);border:1px solid var(--border);font-size:12.5px;color:var(--muted)">${ICONS.lock} 只做一次只读连接测试,不会往仓库里写任何东西。</div>` +
|
|
1713
|
-
`</div>` +
|
|
1714
|
-
`<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;padding:20px 26px 22px">` +
|
|
1715
|
-
`<button type="button" id="cc-cancel" class="btn">取消</button>` +
|
|
1716
|
-
`<button type="button" id="cc-submit" class="btn btn-primary">${ICONS.link2} 连接</button>` +
|
|
1717
|
-
`</div>`;
|
|
1718
|
-
|
|
1719
|
-
const urlEl = modal.querySelector("#cc-url");
|
|
1720
|
-
const submitEl = modal.querySelector("#cc-submit");
|
|
1721
|
-
const syncCan = () => {
|
|
1722
|
-
const can = (urlEl?.value ?? "").trim().length > 6;
|
|
1723
|
-
if (submitEl) submitEl.disabled = !can;
|
|
1724
|
-
};
|
|
1725
|
-
syncCan();
|
|
1726
|
-
urlEl?.addEventListener("input", syncCan);
|
|
1727
|
-
modal.querySelector("#cc-close")?.addEventListener("click", () => closeConnectFlow());
|
|
1728
|
-
modal.querySelector("#cc-cancel")?.addEventListener("click", () => closeConnectFlow());
|
|
1729
|
-
submitEl?.addEventListener("click", () => submitConnect());
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
/** label + 说人话 help + 输入(CcField 复刻)。 */
|
|
1733
|
-
function ccField(label, help, inputHTML) {
|
|
1734
|
-
return (
|
|
1735
|
-
`<div style="display:flex;flex-direction:column;gap:6px">` +
|
|
1736
|
-
`<label style="font-size:14px;font-weight:600;color:var(--text)">${esc(label)}</label>` +
|
|
1737
|
-
(help ? `<p style="margin:0;font-size:12.5px;color:var(--muted);line-height:1.5">${esc(help)}</p>` : "") +
|
|
1738
|
-
inputHTML +
|
|
1739
|
-
`</div>`
|
|
1740
|
-
);
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
/**
|
|
1744
|
-
* 提交连接:读表单 → connecting 态 → POST /api/central/config →
|
|
1745
|
-
* ok → connected 态 + 刷新 status;否则 → failed 态(说人话,不甩堆栈)。
|
|
1746
|
-
*/
|
|
1747
|
-
async function submitConnect() {
|
|
1748
|
-
const modal = document.getElementById("connect-modal");
|
|
1749
|
-
// form 态时从输入读取最新值;retry 时沿用上次 values
|
|
1750
|
-
const urlEl = modal?.querySelector("#cc-url");
|
|
1751
|
-
if (urlEl) {
|
|
1752
|
-
connectFlow.values = {
|
|
1753
|
-
url: (modal.querySelector("#cc-url")?.value ?? "").trim(),
|
|
1754
|
-
branch: (modal.querySelector("#cc-branch")?.value ?? "").trim() || "main",
|
|
1755
|
-
path: (modal.querySelector("#cc-path")?.value ?? "").trim() || "bots/",
|
|
1756
|
-
};
|
|
1757
|
-
}
|
|
1758
|
-
const v = connectFlow.values;
|
|
1759
|
-
if (!v.url || v.url.length <= 6) {
|
|
1760
|
-
toast("请填写仓库地址。", "warn");
|
|
1761
|
-
return;
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
renderConnectFlow("connecting");
|
|
1765
|
-
const res = await api("POST", "/api/central/config", {
|
|
1766
|
-
url: v.url,
|
|
1767
|
-
branch: v.branch,
|
|
1768
|
-
path: v.path,
|
|
1769
|
-
});
|
|
1770
|
-
|
|
1771
|
-
if (res.ok && res.json?.ok === true) {
|
|
1772
|
-
connectFlow.repo = res.json.repo ?? null;
|
|
1773
|
-
// 刷新连接状态(取 sharedCount 等)
|
|
1774
|
-
await loadCentralStatus();
|
|
1775
|
-
renderConnectFlow("connected");
|
|
1776
|
-
} else {
|
|
1777
|
-
const j = res.json ?? {};
|
|
1778
|
-
connectFlow.fail = { kind: j.kind, error: j.error };
|
|
1779
|
-
renderConnectFlow("failed");
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
// ---------------------------------------------------------------------------
|
|
1784
|
-
// 公司中心库:断开连接
|
|
1785
|
-
// ---------------------------------------------------------------------------
|
|
1786
|
-
|
|
1787
|
-
async function doCentralDisconnect() {
|
|
1788
|
-
const ok = await confirmDialog({
|
|
1789
|
-
title: "断开公司中心库?",
|
|
1790
|
-
body:
|
|
1791
|
-
"断开后这台电脑不再跟团队中心库同步,「晋升」也会停用。本机已有的助手不受影响,继续在本机跑。\n\n随时可以再连回来。",
|
|
1792
|
-
confirmText: "断开",
|
|
1793
|
-
confirmDanger: true,
|
|
1794
|
-
});
|
|
1795
|
-
if (!ok) return;
|
|
1796
|
-
const res = await api("POST", "/api/central/disconnect");
|
|
1797
|
-
if (!res.ok || res.json?.ok !== true) {
|
|
1798
|
-
toast(`断开失败:${res.json?.error ?? res.status}`, "error");
|
|
1799
|
-
return;
|
|
1800
|
-
}
|
|
1801
|
-
toast("已断开公司中心库", "ok");
|
|
1802
|
-
state.centralConnected = false;
|
|
1803
|
-
state.centralRepo = null;
|
|
1804
|
-
state.centralAvailable = false;
|
|
1805
|
-
// 后端断开后会把 mode 重置为 local;前端跟随切回本机
|
|
1806
|
-
state.mode = "local";
|
|
1807
|
-
state.selected = null;
|
|
1808
|
-
renderContextSwitch();
|
|
1809
|
-
renderCentralSourceBar();
|
|
1810
|
-
renderBotDetail(null);
|
|
1811
|
-
await loadBots();
|
|
1812
|
-
renderServiceIndicator();
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
// ---------------------------------------------------------------------------
|
|
1816
|
-
// 公司中心库:同步(中心 → 本地)—— preview(dry-run)+ ack gate + apply
|
|
1817
|
-
// 复刻 centralSync.jsx LkSyncPreview。preview 走 GET /api/central/sync/preview,
|
|
1818
|
-
// apply 走 POST /api/central/sync/apply。移除类醒目(amber)+ ack 勾选。
|
|
1819
|
-
// ---------------------------------------------------------------------------
|
|
1820
|
-
|
|
1821
|
-
async function doCentralSync() {
|
|
1822
|
-
state.centralSyncState = "syncing";
|
|
1823
|
-
renderCentralSourceBar();
|
|
1824
|
-
|
|
1825
|
-
const res = await api("GET", "/api/central/sync/preview");
|
|
1826
|
-
if (!res.ok) {
|
|
1827
|
-
state.centralSyncState = "unknown";
|
|
1828
|
-
renderCentralSourceBar();
|
|
1829
|
-
toast(`同步预览失败:${res.json?.error ?? res.status}`, "error");
|
|
1830
|
-
return;
|
|
1831
|
-
}
|
|
1832
|
-
const preview = {
|
|
1833
|
-
added: Array.isArray(res.json?.added) ? res.json.added : [],
|
|
1834
|
-
updated: Array.isArray(res.json?.updated) ? res.json.updated : [],
|
|
1835
|
-
removed: Array.isArray(res.json?.removed) ? res.json.removed : [],
|
|
1836
|
-
};
|
|
1837
|
-
// 「N 项可更新」只数中心 sync 会真正应用的增量(added + updated)。removed(本机自建未晋升的
|
|
1838
|
-
// bot)本机自管、绝不删,不该算「可更新」—— 否则本地独有 bot 会让来源条永远显示「1 项可更新」,
|
|
1839
|
-
// 点进去又走删除路径(2026-06-01 误删 larkway-2 的连锁起点)。
|
|
1840
|
-
const total = preview.added.length + preview.updated.length;
|
|
1841
|
-
state.centralUpdateCount = total;
|
|
1842
|
-
state.centralSyncState = total > 0 ? "updates" : "fresh";
|
|
1843
|
-
renderCentralSourceBar();
|
|
1844
|
-
|
|
1845
|
-
if (total === 0) {
|
|
1846
|
-
toast("已是最新,没有要同步的改动。", "ok");
|
|
1847
|
-
return;
|
|
1848
|
-
}
|
|
1849
|
-
showCentralSyncModal(preview);
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
/** 一行变更项(CcChangeRow):added=绿 / updated=indigo / removed=amber。 */
|
|
1853
|
-
function ccChangeRow(kind, item) {
|
|
1854
|
-
const map = {
|
|
1855
|
-
added: { c: LIVE_COLOR.serving, soft: LIVE_SOFT.serving, edge: LIVE_EDGE.serving, text: LIVE_TEXT.serving, icon: ICONS.plus, tag: "新增" },
|
|
1856
|
-
updated: { c: BR.c, soft: BR.soft, edge: BR.edge, text: BR.text, icon: ICONS.refresh, tag: "更新" },
|
|
1857
|
-
removed: { c: LIVE_COLOR.degraded, soft: LIVE_SOFT.degraded, edge: LIVE_EDGE.degraded, text: LIVE_TEXT.degraded, icon: ICONS.trash, tag: "移除" },
|
|
1858
|
-
}[kind];
|
|
1859
|
-
const removed = kind === "removed";
|
|
1860
|
-
const by = item.by ? `<span style="font-size:11.5px;color:var(--faint)">· ${esc(item.by)}</span>` : "";
|
|
1861
|
-
const note = removed
|
|
1862
|
-
? "中心库里已被移除 —— 同步后本机这份(由中心库管理)也会删掉。"
|
|
1863
|
-
: (item.note ?? "");
|
|
1864
|
-
return (
|
|
1865
|
-
`<div class="cc-change-row${removed ? " is-removed" : ""}">` +
|
|
1866
|
-
`<span class="cc-change-badge" style="border:1px solid ${map.edge};color:${map.c}">${map.icon}</span>` +
|
|
1867
|
-
`<div style="min-width:0;flex:1">` +
|
|
1868
|
-
`<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">` +
|
|
1869
|
-
`<span style="font-size:14px;font-weight:600;color:var(--text)">${esc(item.name)}</span>` +
|
|
1870
|
-
`<span class="cc-change-tag" style="color:${map.text};background:${map.soft};border:1px solid ${map.edge}">${map.tag}</span>` +
|
|
1871
|
-
by +
|
|
1872
|
-
`</div>` +
|
|
1873
|
-
`<div style="font-size:12.5px;color:${removed ? map.text : "var(--muted)"};line-height:1.5;margin-top:2px">${esc(note)}</div>` +
|
|
1874
|
-
`</div></div>`
|
|
1875
|
-
);
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1878
|
-
/** 同步预览 modal(复用 #modal-backdrop)。移除>0 时 ack gate。 */
|
|
1879
|
-
function showCentralSyncModal(preview) {
|
|
1880
|
-
const backdrop = document.getElementById("modal-backdrop");
|
|
1881
|
-
const modal = document.getElementById("modal");
|
|
1882
|
-
if (!backdrop || !modal) return;
|
|
1883
|
-
// 同步弹窗:设计稿 520px / 圆角 16px,加 class 后关闭时清掉
|
|
1884
|
-
modal.classList.add("modal--sync");
|
|
1885
|
-
|
|
1886
|
-
const nAdd = preview.added.length, nUpd = preview.updated.length;
|
|
1887
|
-
// 中心 sync 只做增量:added(中心有本机没有)+ updated(两边都有、中心更新)。
|
|
1888
|
-
// `preview.removed` = 「本机有、中心没有」,即本机自建没晋升过的 bot —— 本机自管,中心 sync
|
|
1889
|
-
// 绝不删它(见下方 prune=false)。所以这里不计入改动、不展示为「移除」、不做 ack gate,
|
|
1890
|
-
// 避免把用户自己的本地 bot 误报成「会被移除」吓人(2026-06-01 误删 larkway-2 的教训)。
|
|
1891
|
-
const nRem = 0;
|
|
1892
|
-
const total = nAdd + nUpd;
|
|
1893
|
-
|
|
1894
|
-
const sumCell = (n, label, c) =>
|
|
1895
|
-
`<span style="display:inline-flex;align-items:baseline;gap:5px"><b style="font-size:16px;font-weight:800;color:${n ? c : "var(--faint)"}">${n}</b><span style="font-size:12.5px;color:var(--muted)">${label}</span></span>`;
|
|
1896
|
-
const sep = `<span style="width:1px;height:20px;background:var(--border)"></span>`;
|
|
1897
|
-
|
|
1898
|
-
const removeWarn = "";
|
|
1899
|
-
|
|
1900
|
-
const list =
|
|
1901
|
-
preview.added.map((it) => ccChangeRow("added", it)).join("") +
|
|
1902
|
-
preview.updated.map((it) => ccChangeRow("updated", it)).join("");
|
|
1903
|
-
|
|
1904
|
-
const ackBlock = "";
|
|
1905
|
-
|
|
1906
|
-
modal.querySelector(".modal-header")?.remove();
|
|
1907
|
-
modal.querySelector(".modal-header-sync")?.remove();
|
|
1908
|
-
modal.innerHTML =
|
|
1909
|
-
`<div class="modal-header-sync">` +
|
|
1910
|
-
`<div class="modal-header-sync-row">` +
|
|
1911
|
-
`<h3><span class="sync-icon-badge">${ICONS.pull}</span>同步前先看一眼</h3>` +
|
|
1912
|
-
`<button class="sync-close-btn" id="cc-sync-close-x" type="button" aria-label="关闭">${ICONS.x}</button>` +
|
|
1913
|
-
`</div>` +
|
|
1914
|
-
`<p>把中心库最新的拉到本机。下面是会发生的改动,确认了再应用。</p>` +
|
|
1915
|
-
`</div>` +
|
|
1916
|
-
`<div class="modal-body">` +
|
|
1917
|
-
`<div style="display:flex;align-items:center;gap:20px;padding:12px 16px;border-radius:11px;background:var(--bg);border:1px solid var(--border)">` +
|
|
1918
|
-
sumCell(nAdd, "新增", LIVE_COLOR.serving) + sep + sumCell(nUpd, "更新", BR.c) + sep + sumCell(nRem, "移除", LIVE_COLOR.degraded) +
|
|
1919
|
-
`</div>` +
|
|
1920
|
-
removeWarn +
|
|
1921
|
-
`<div style="margin:16px 0 0;display:flex;flex-direction:column;gap:8px">${list}</div>` +
|
|
1922
|
-
ackBlock +
|
|
1923
|
-
`</div>` +
|
|
1924
|
-
`<div class="modal-footer">` +
|
|
1925
|
-
`<div class="modal-btns">` +
|
|
1926
|
-
`<button class="btn" id="cc-sync-cancel" type="button">先不同步</button>` +
|
|
1927
|
-
`<button class="btn btn-primary" id="cc-sync-apply" type="button">${ICONS.pull} 应用更新(${total})</button>` +
|
|
1928
|
-
`</div></div>`;
|
|
1929
|
-
|
|
1930
|
-
const applyBtn = modal.querySelector("#cc-sync-apply");
|
|
1931
|
-
const ackChk = modal.querySelector("#cc-sync-ack");
|
|
1932
|
-
if (ackChk && applyBtn) {
|
|
1933
|
-
applyBtn.disabled = true;
|
|
1934
|
-
ackChk.addEventListener("change", () => { applyBtn.disabled = !ackChk.checked; });
|
|
1935
|
-
}
|
|
1936
|
-
|
|
1937
|
-
backdrop.hidden = false;
|
|
1938
|
-
|
|
1939
|
-
const closeSync = () => { backdrop.hidden = true; modal.classList.remove("modal--sync"); };
|
|
1940
|
-
modal.querySelector("#cc-sync-cancel")?.addEventListener("click", closeSync);
|
|
1941
|
-
modal.querySelector("#cc-sync-close-x")?.addEventListener("click", closeSync);
|
|
1942
|
-
applyBtn?.addEventListener("click", async () => {
|
|
1943
|
-
if (applyBtn.disabled) return;
|
|
1944
|
-
// 安全:中心库 sync 永不自动删本机 bot。planSync 的 `removed` = 「本机有、中心没有」,
|
|
1945
|
-
// 它把「本机自建从没晋升过的 bot」(如本地新建测试 bot)和「曾被中心管理后从中心删掉的」
|
|
1946
|
-
// 混为一谈 —— 旧代码 `nRem>0` 就 prune=true 会把前者一并硬删(2026-06-01 误删 larkway-2)。
|
|
1947
|
-
// 在 provenance-aware prune 落地前,这里强制 false:本机自管是安全默认,中心 sync 只增量
|
|
1948
|
-
// 拉 added/updated,绝不删本机。真要清理本机多余 bot 走「拉取最新」里显式勾选 prune 的流程。
|
|
1949
|
-
const prune = false;
|
|
1950
|
-
const restore = btnLoading(applyBtn, "正在应用…");
|
|
1951
|
-
const res = await api("POST", "/api/central/sync/apply", { prune });
|
|
1952
|
-
restore();
|
|
1953
|
-
backdrop.hidden = true;
|
|
1954
|
-
modal.classList.remove("modal--sync");
|
|
1955
|
-
if (!res.ok || res.json?.ok !== true) {
|
|
1956
|
-
toast(`同步失败:${res.json?.error ?? res.status}`, "error");
|
|
1957
|
-
return;
|
|
1958
|
-
}
|
|
1959
|
-
const j = res.json;
|
|
1960
|
-
const parts = [];
|
|
1961
|
-
if ((j.added ?? []).length) parts.push(`新增 ${j.added.length}`);
|
|
1962
|
-
if ((j.updated ?? []).length) parts.push(`更新 ${j.updated.length}`);
|
|
1963
|
-
if ((j.removed ?? []).length) parts.push(`移除 ${j.removed.length}`);
|
|
1964
|
-
toast(`同步完成。${parts.join("、") || "无变化"}`, "ok");
|
|
1965
|
-
if ((j.warnings ?? []).length) {
|
|
1966
|
-
for (const w of j.warnings) console.warn("[larkway central sync]", w);
|
|
1967
|
-
}
|
|
1968
|
-
// 同步后刷新中心库状态 + 名册(本机也变了,但当前在中心视图,刷中心名册)
|
|
1969
|
-
state.centralSyncState = "fresh";
|
|
1970
|
-
state.centralUpdateCount = 0;
|
|
1971
|
-
await loadCentralStatus();
|
|
1972
|
-
renderCentralSourceBar();
|
|
1973
|
-
await loadBots();
|
|
1974
|
-
});
|
|
1975
|
-
}
|
|
1976
1262
|
|
|
1977
1263
|
// ---------------------------------------------------------------------------
|
|
1978
1264
|
// 渲染:主机状态 pill
|
|
@@ -2088,12 +1374,11 @@ function renderDetailBanner() {
|
|
|
2088
1374
|
* 一行(名字下方的「● 连接异常/正常服务中 · N 秒前心跳」+ 头像光环)是 renderBotDetail
|
|
2089
1375
|
* 选中时一次性渲染的,poll 不碰它 → bot 恢复在线后 hero pill 卡在旧的「连接异常」+ 旧心跳。
|
|
2090
1376
|
* 这里在每次 /api/status 落地后,用 acRefreshHero 按 state.liveness/lastSeenMs 重建 hero body
|
|
2091
|
-
* (它读 AC 面板的实时表单值,所以不会丢用户正在编辑的输入)
|
|
2092
|
-
* 中心库(只读)hero 不显在线/心跳,且没有 AC 面板可读。
|
|
1377
|
+
* (它读 AC 面板的实时表单值,所以不会丢用户正在编辑的输入)。
|
|
2093
1378
|
*/
|
|
2094
1379
|
function refreshDetailHero() {
|
|
2095
1380
|
const id = state.selected;
|
|
2096
|
-
if (!id
|
|
1381
|
+
if (!id) return;
|
|
2097
1382
|
const panel = document.getElementById("detail");
|
|
2098
1383
|
if (!panel || !panel.querySelector("#detail-hero-body")) return;
|
|
2099
1384
|
if (!panel.querySelector("#ac-panel")) return; // 编辑式面板未挂载(加载中/占位)
|
|
@@ -2118,19 +1403,12 @@ function renderBotListLoading() {
|
|
|
2118
1403
|
}
|
|
2119
1404
|
}
|
|
2120
1405
|
|
|
2121
|
-
/** 更新名册顶部计数文案「这台电脑上的 N
|
|
1406
|
+
/** 更新名册顶部计数文案「这台电脑上的 N 个助手」。 */
|
|
2122
1407
|
function renderRosterCount() {
|
|
2123
1408
|
const el = document.getElementById("roster-count-text");
|
|
2124
1409
|
if (!el) return;
|
|
2125
1410
|
const n = state.bots.length;
|
|
2126
|
-
|
|
2127
|
-
el.textContent =
|
|
2128
|
-
state.mode === "central" ? "中心库还没有共享助手" : "这台电脑上还没有助手";
|
|
2129
|
-
} else if (state.mode === "central") {
|
|
2130
|
-
el.textContent = `公司中心库的 ${n} 个助手`;
|
|
2131
|
-
} else {
|
|
2132
|
-
el.textContent = `这台电脑上的 ${n} 个助手`;
|
|
2133
|
-
}
|
|
1411
|
+
el.textContent = n === 0 ? "这台电脑上还没有助手" : `这台电脑上的 ${n} 个助手`;
|
|
2134
1412
|
}
|
|
2135
1413
|
|
|
2136
1414
|
/**
|
|
@@ -2141,7 +1419,7 @@ function renderRosterCount() {
|
|
|
2141
1419
|
*/
|
|
2142
1420
|
function renderRosterRestartStatus() {
|
|
2143
1421
|
const el = document.getElementById("roster-restart-status");
|
|
2144
|
-
if (!el
|
|
1422
|
+
if (!el) return;
|
|
2145
1423
|
const rs = state.restart;
|
|
2146
1424
|
if (rs.status === "restarting") {
|
|
2147
1425
|
const total = state.bots.length;
|
|
@@ -2217,7 +1495,6 @@ function renderBotList() {
|
|
|
2217
1495
|
}
|
|
2218
1496
|
|
|
2219
1497
|
// 添加/更新条目(顺序也要对得上编号 01/02 → 按 state.bots 顺序 re-append)
|
|
2220
|
-
const readonly = state.mode === "central";
|
|
2221
1498
|
state.bots.forEach((bot, i) => {
|
|
2222
1499
|
let li = list.querySelector(`li[data-bot-id="${CSS.escape(bot.id)}"]`);
|
|
2223
1500
|
if (!li) {
|
|
@@ -2239,40 +1516,13 @@ function renderBotList() {
|
|
|
2239
1516
|
li.classList.toggle("is-selected", selected);
|
|
2240
1517
|
const num = String(i + 1).padStart(2, "0");
|
|
2241
1518
|
|
|
2242
|
-
// ── 中心库只读名册行(CcRosterItem):头像 + 名字 + 「<by> 晋升 · <updated>」──
|
|
2243
|
-
// 中心库存「配置」不是「进程」→ 不显在线/心跳,只显谁晋升的 + 更新时间;无删除。
|
|
2244
|
-
if (readonly) {
|
|
2245
|
-
li.classList.add("cc-roster-item");
|
|
2246
|
-
const meta = state.centralBots.find((b) => b.id === bot.id) ?? {};
|
|
2247
|
-
const by = meta.by || "";
|
|
2248
|
-
const updated = meta.updated || "";
|
|
2249
|
-
const byLine = by || updated
|
|
2250
|
-
? `<span class="cc-roster-by">${ICONS.upload} ${esc(by)}${by ? " 晋升" : ""}${updated ? ` · ${esc(updated)}` : ""}</span>`
|
|
2251
|
-
: `<span class="cc-roster-by">${ICONS.upload} 共享助手</span>`;
|
|
2252
|
-
// ⑦ 中心库名册:用 bot.avatar 飞书头像,没有才回退首字母
|
|
2253
|
-
const ccAvatar = state.avatars[bot.id] ?? bot.avatar ?? null;
|
|
2254
|
-
li.innerHTML =
|
|
2255
|
-
`<span class="roster-num">${num}</span>` +
|
|
2256
|
-
avatarHTML(bot.id, bot.name, ccAvatar, "list", "unknown").replace(
|
|
2257
|
-
/<span class="avatar-dot[^>]*><\/span>/,
|
|
2258
|
-
"", // 中心库无在线状态,去掉头像角标圆点
|
|
2259
|
-
) +
|
|
2260
|
-
`<span class="roster-meta">` +
|
|
2261
|
-
`<span class="bot-name">${esc(bot.name)}</span>` +
|
|
2262
|
-
`<span class="roster-state">${byLine}</span>` +
|
|
2263
|
-
`</span>`;
|
|
2264
|
-
return;
|
|
2265
|
-
}
|
|
2266
|
-
|
|
2267
1519
|
// ── BL-18:显示覆盖 —— restarting/timeout 时用 restartDisplayLive 替代真实 liveKey ─
|
|
2268
1520
|
const realLiveKey = botLiveness(bot.id);
|
|
2269
1521
|
const dispLiveKey = restartDisplayLive(realLiveKey, state.restart.status);
|
|
2270
1522
|
const liveKey = dispLiveKey;
|
|
2271
1523
|
const live = LIVENESS[liveKey] ?? LIVENESS.unknown;
|
|
2272
1524
|
const avatar = state.avatars[bot.id] ?? bot.avatar ?? null;
|
|
2273
|
-
const delBtn =
|
|
2274
|
-
? ""
|
|
2275
|
-
: `<button class="lk-del" type="button" aria-label="删除 ${esc(bot.name)}" title="删除助手">${ICONS.trash}</button>`;
|
|
1525
|
+
const delBtn = `<button class="lk-del" type="button" aria-label="删除 ${esc(bot.name)}" title="删除助手">${ICONS.trash}</button>`;
|
|
2276
1526
|
// 名册头部:重启中显示进度「已连回 N/total」
|
|
2277
1527
|
// (已在名册标题区渲染,不再重复到行级)
|
|
2278
1528
|
// LkAttentionPill / LkRestartPill:
|
|
@@ -2329,14 +1579,12 @@ function renderBotList() {
|
|
|
2329
1579
|
`</span>` +
|
|
2330
1580
|
delBtn;
|
|
2331
1581
|
// Wire del button after innerHTML (DOM replaced)
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
});
|
|
2339
|
-
}
|
|
1582
|
+
const delEl = li.querySelector(".lk-del");
|
|
1583
|
+
if (delEl) {
|
|
1584
|
+
delEl.addEventListener("click", (e) => {
|
|
1585
|
+
e.stopPropagation();
|
|
1586
|
+
doDeleteBot(bot.id);
|
|
1587
|
+
});
|
|
2340
1588
|
}
|
|
2341
1589
|
});
|
|
2342
1590
|
}
|
|
@@ -2474,11 +1722,9 @@ function buildStepFlowHTML() {
|
|
|
2474
1722
|
|
|
2475
1723
|
/**
|
|
2476
1724
|
* 构建 LkEmptyState 的完整 HTML。
|
|
2477
|
-
* @param {'local'|'central'} variant
|
|
2478
1725
|
* @param {boolean} serviceRunning false 时在 CTA 下加温和提示
|
|
2479
1726
|
*/
|
|
2480
|
-
function buildEmptyStateHTML(
|
|
2481
|
-
const central = variant === "central";
|
|
1727
|
+
function buildEmptyStateHTML(serviceRunning) {
|
|
2482
1728
|
const c = BR.c;
|
|
2483
1729
|
const soft = BR.soft;
|
|
2484
1730
|
const edge = BR.edge;
|
|
@@ -2495,23 +1741,8 @@ function buildEmptyStateHTML(variant, serviceRunning) {
|
|
|
2495
1741
|
const bgStyle = `radial-gradient(120% 90% at 50% 0%, ${lkHexA(c, 0.05)} 0%, ${surface} 56%)`;
|
|
2496
1742
|
|
|
2497
1743
|
let mainContent;
|
|
2498
|
-
|
|
2499
|
-
//
|
|
2500
|
-
const switchBtn = `<button class="es-central-switch-btn" type="button"
|
|
2501
|
-
style="margin-top:24px;display:inline-flex;align-items:center;gap:8px;padding:11px 20px;font-size:14.5px;font-weight:700;font-family:inherit;color:${brText};background:${soft};border:1px solid ${edge};border-radius:11px;cursor:pointer">
|
|
2502
|
-
切到「本机」去添加
|
|
2503
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
|
|
2504
|
-
</button>`;
|
|
2505
|
-
|
|
2506
|
-
mainContent = `
|
|
2507
|
-
<h1 style="margin:20px 0 0;font-size:30px;font-weight:800;letter-spacing:-.03em;line-height:1.12;color:${text}">公司中心库还没有共享助手</h1>
|
|
2508
|
-
<p style="margin:12px 0 0;font-size:16px;color:${muted};line-height:1.62;max-width:460px">
|
|
2509
|
-
中心库是<b style="color:#334155">只读</b>的展示窗。先在<b style="color:${brText}">「本机」</b>配好一个助手,再把它<b style="color:#334155">晋升到这里</b>,全组就都能看到、复用。
|
|
2510
|
-
</p>
|
|
2511
|
-
${switchBtn}
|
|
2512
|
-
`;
|
|
2513
|
-
} else {
|
|
2514
|
-
// 本机变体:欢迎 + CTA
|
|
1744
|
+
{
|
|
1745
|
+
// 本机:欢迎 + CTA
|
|
2515
1746
|
const ctaBtn = `<button id="es-add-btn" type="button"
|
|
2516
1747
|
class="es-cta-btn"
|
|
2517
1748
|
style="display:inline-flex;align-items:center;gap:9px;padding:14px 28px;font-size:16px;font-weight:700;font-family:inherit;color:#fff;cursor:pointer;background:${c};border:none;border-radius:13px;box-shadow:0 4px 14px ${lkHexA(c, 0.26)};transition:background .15s,box-shadow .18s,transform .18s">
|
|
@@ -2546,7 +1777,7 @@ function buildEmptyStateHTML(variant, serviceRunning) {
|
|
|
2546
1777
|
}
|
|
2547
1778
|
|
|
2548
1779
|
// 底部安心语(两变体共有)
|
|
2549
|
-
const shieldNote = `<div style="margin-top
|
|
1780
|
+
const shieldNote = `<div style="margin-top:26px;display:inline-flex;align-items:center;gap:7px;font-size:12.5px;color:${faint}">
|
|
2550
1781
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l7 3v6c0 4.4-3 7.6-7 9-4-1.4-7-4.6-7-9V6z"/></svg>
|
|
2551
1782
|
全程不碰终端;密钥只存在你本机,别人看不到
|
|
2552
1783
|
</div>`;
|
|
@@ -2560,56 +1791,9 @@ function buildEmptyStateHTML(variant, serviceRunning) {
|
|
|
2560
1791
|
</div>`;
|
|
2561
1792
|
}
|
|
2562
1793
|
|
|
2563
|
-
/**
|
|
2564
|
-
* 构建 LkConnectGuide(未连接引导卡)的 HTML —— 复刻 centralConnect.jsx。
|
|
2565
|
-
* 母题:本机一份 → 仓库 → 同事各自一份(共享);CTA = indigo「连接公司中心库」。
|
|
2566
|
-
*/
|
|
2567
|
-
function buildConnectGuideHTML() {
|
|
2568
|
-
const c = BR.c, soft = BR.soft, text = "#1e293b", muted = "#64748b", faint = "#94a3b8", brText = BR.text;
|
|
2569
|
-
const bgStyle = `radial-gradient(120% 90% at 50% 0%, ${lkHexA(c, 0.05)} 0%, #fff 56%)`;
|
|
2570
|
-
|
|
2571
|
-
// 连接母题插画(中央仓库 + 三个同事机器 + 双向同步弧线/流动光点)
|
|
2572
|
-
const machines = [[34, 38], [34, 94], [266, 66]]
|
|
2573
|
-
.map(([x, y]) =>
|
|
2574
|
-
`<g><rect x="${x - 20}" y="${y - 14}" width="40" height="28" rx="7" fill="#fff" stroke="#cbd5e1" stroke-width="1.5"/>` +
|
|
2575
|
-
`<circle cx="${x - 9}" cy="${y}" r="3.5" fill="#cbd5e1"/>` +
|
|
2576
|
-
`<rect x="${x - 2}" y="${y - 4}" width="16" height="3.5" rx="1.75" fill="#e2e8f0"/>` +
|
|
2577
|
-
`<rect x="${x - 2}" y="${y + 3}" width="11" height="3" rx="1.5" fill="#eef0f4"/></g>`,
|
|
2578
|
-
).join("");
|
|
2579
|
-
const arcs = [[54, 44, 123, 56], [54, 88, 123, 78], [246, 66, 177, 67]]
|
|
2580
|
-
.map((p, i) => {
|
|
2581
|
-
const d = `M${p[0]} ${p[1]} Q ${(p[0] + p[2]) / 2} ${(p[1] + p[3]) / 2 - 14}, ${p[2]} ${p[3]}`;
|
|
2582
|
-
return (
|
|
2583
|
-
`<g><path d="${d}" fill="none" stroke="${lkHexA(c, 0.4)}" stroke-width="1.8" stroke-dasharray="3 4" stroke-linecap="round"/>` +
|
|
2584
|
-
`<circle r="2.6" fill="${c}">` +
|
|
2585
|
-
`<animateMotion dur="2.6s" begin="${i * 0.5}s" repeatCount="indefinite" path="${d}"/>` +
|
|
2586
|
-
`<animate attributeName="opacity" dur="2.6s" begin="${i * 0.5}s" repeatCount="indefinite" values="0;1;1;0" keyTimes="0;0.15;0.8;1"/>` +
|
|
2587
|
-
`</circle></g>`
|
|
2588
|
-
);
|
|
2589
|
-
}).join("");
|
|
2590
|
-
const svg =
|
|
2591
|
-
`<svg viewBox="0 0 300 132" width="272" height="120" aria-hidden="true" style="display:block">` +
|
|
2592
|
-
`<ellipse cx="150" cy="66" rx="132" ry="58" fill="${lkHexA(c, 0.05)}"/>` +
|
|
2593
|
-
`<rect x="123" y="44" width="54" height="46" rx="11" fill="#fff" stroke="${c}" stroke-width="1.8"/>` +
|
|
2594
|
-
`<path d="M135 58h30M135 67h30M135 76h20" stroke="${lkHexA(c, 0.5)}" stroke-width="2" stroke-linecap="round"/>` +
|
|
2595
|
-
machines + arcs +
|
|
2596
|
-
`</svg>`;
|
|
2597
|
-
|
|
2598
|
-
return (
|
|
2599
|
-
`<div class="es-root" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;box-sizing:border-box;padding:40px 32px;background:${bgStyle};overflow-y:auto">` +
|
|
2600
|
-
`<div style="width:100%;max-width:520px;display:flex;flex-direction:column;align-items:center;text-align:center">` +
|
|
2601
|
-
svg +
|
|
2602
|
-
`<h1 style="margin:18px 0 0;font-size:27px;font-weight:800;letter-spacing:-.025em;line-height:1.14;color:${text};text-wrap:balance">连接公司中心库,和团队共享助手</h1>` +
|
|
2603
|
-
`<p style="margin:11px 0 0;font-size:15.5px;color:${muted};line-height:1.62;max-width:440px;text-wrap:pretty">把本机调好的助手<b style="color:${brText}">晋升</b>到团队的中心库,别的同事一拉就能用同一份;别人晋升的,你也<b style="color:#334155">一键同步</b>下来。</p>` +
|
|
2604
|
-
`<button id="cc-guide-connect" type="button" class="es-cta-btn" style="margin-top:24px;display:inline-flex;align-items:center;gap:9px;padding:13px 26px;font-size:15.5px;font-weight:700;font-family:inherit;color:#fff;background:${c};border:none;border-radius:12px;cursor:pointer;box-shadow:0 4px 14px ${lkHexA(c, 0.26)};transition:background .15s">${ICONS.link2} 连接公司中心库</button>` +
|
|
2605
|
-
`<div style="margin-top:18px;display:inline-flex;align-items:center;gap:7px;font-size:12.5px;color:${faint}">${ICONS.info} 第一次连可能要工程师帮忙给个仓库地址;连好之后日常同步、晋升你自己点就行</div>` +
|
|
2606
|
-
`</div></div>`
|
|
2607
|
-
);
|
|
2608
|
-
}
|
|
2609
1794
|
|
|
2610
1795
|
// ---------------------------------------------------------------------------
|
|
2611
1796
|
// 详情区「空态」(名册有助手但未选中)—— 复刻 Claude Design empty-detail 稿。
|
|
2612
|
-
// 两个变体:本机(可改,indigo)/ 公司中心库(只读,slate)。设计稿 emptyDetail.jsx。
|
|
2613
1797
|
// ---------------------------------------------------------------------------
|
|
2614
1798
|
|
|
2615
1799
|
/** 空态用的 icon path(复合 path 按 'M' 拆段,见 edIcon)。 */
|
|
@@ -2697,34 +1881,20 @@ function edRail(steps) {
|
|
|
2697
1881
|
`</div>`;
|
|
2698
1882
|
}
|
|
2699
1883
|
|
|
2700
|
-
/** 详情区空态 HTML(
|
|
2701
|
-
function buildEmptyDetailHTML(
|
|
2702
|
-
if (ro) {
|
|
2703
|
-
return `<div class="lk-ed lk-ed--ro" style="container-type:inline-size;container-name:lk-ed">` +
|
|
2704
|
-
`<div class="lk-ed__inner">${edHeroSVG(true, 348)}` +
|
|
2705
|
-
`<span class="lk-ed__eyebrow">${edIcon(ED_ICON.lock, 14, 2.1)} 公司中心库 · 只读</span>` +
|
|
2706
|
-
`<h2 class="lk-ed__title">选一个共享助手,看它怎么配的</h2>` +
|
|
2707
|
-
`<p class="lk-ed__body">这里是全组共享的助手,内容<b>只读</b> —— 能看它的配置和职责,照着用。想改?先回<b class="br">「本机」</b>改好,再<b>「晋升」</b>覆盖回来。</p>` +
|
|
2708
|
-
`<div class="lk-ed__actions"><button type="button" id="ed-go-local" class="lk-ed__btn lk-ed__btn--soft">${edIcon(ED_ICON.arrowL, 16, 2.1)} 回「本机」去改配置</button></div>` +
|
|
2709
|
-
edRail([
|
|
2710
|
-
{ d: ED_ICON.eye, t: "在这里浏览", s: "只读看配置 / 职责" },
|
|
2711
|
-
{ d: ED_ICON.edit, t: "回本机修改", s: "改动只能在本机做" },
|
|
2712
|
-
{ d: ED_ICON.upload, t: "晋升覆盖", s: "推上来,全组更新" },
|
|
2713
|
-
]) +
|
|
2714
|
-
`<span class="lk-ed__foot">${edIcon(ED_ICON.lock, 14, 1.7)} 中心库只读;任何改动都要从本机晋升上来</span></div></div>`;
|
|
2715
|
-
}
|
|
1884
|
+
/** 详情区空态 HTML(本机 · 可改)。 */
|
|
1885
|
+
function buildEmptyDetailHTML() {
|
|
2716
1886
|
return `<div class="lk-ed" style="container-type:inline-size;container-name:lk-ed">` +
|
|
2717
1887
|
`<div class="lk-ed__inner">${edHeroSVG(false, 348)}` +
|
|
2718
1888
|
`<span class="lk-ed__eyebrow">${edIcon(ED_ICON.edit, 14, 2)} 本机 · 可改</span>` +
|
|
2719
1889
|
`<h2 class="lk-ed__title">从左边选一个助手,开始配置</h2>` +
|
|
2720
|
-
`<p class="lk-ed__body"
|
|
1890
|
+
`<p class="lk-ed__body">在这里可以改它的名字、介绍、能用它的群、能改的代码仓库,还有它的职责说明。改好点<b class="br">「保存」</b>就写入本机生效,同事在群里 @ 它就能用。</p>` +
|
|
2721
1891
|
`<div class="lk-ed__actions">` +
|
|
2722
1892
|
`<button type="button" id="ed-add-bot" class="lk-ed__btn lk-ed__btn--primary">${edIcon(ED_ICON.plus, 18, 2.2)} 添加新助手</button>` +
|
|
2723
1893
|
`<button type="button" id="ed-pick" class="lk-ed__btn lk-ed__btn--ghost">${edIcon(ED_ICON.arrowL, 16, 2)} 从名册里挑一个</button></div>` +
|
|
2724
1894
|
edRail([
|
|
2725
1895
|
{ d: ED_ICON.sliders, t: "配置", s: "改名字、群、仓库、职责" },
|
|
2726
|
-
{ d: ED_ICON.
|
|
2727
|
-
{ d: ED_ICON.users, t: "
|
|
1896
|
+
{ d: ED_ICON.edit, t: "保存", s: "写入本机即生效" },
|
|
1897
|
+
{ d: ED_ICON.users, t: "群里 @", s: "同事直接用" },
|
|
2728
1898
|
]) +
|
|
2729
1899
|
`<span class="lk-ed__foot">${edIcon(ED_ICON.shield, 14, 1.7)} 全程不碰终端;密钥只存在你本机,别人看不到</span></div></div>`;
|
|
2730
1900
|
}
|
|
@@ -2733,16 +1903,11 @@ function buildEmptyDetailHTML(ro) {
|
|
|
2733
1903
|
function renderDetailPlaceholder() {
|
|
2734
1904
|
const placeholder = document.getElementById("detail-placeholder");
|
|
2735
1905
|
if (!placeholder) return;
|
|
2736
|
-
|
|
2737
|
-
placeholder.
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
}
|
|
2741
|
-
placeholder.querySelector("#ed-add-bot")?.addEventListener("click", () => openOnboardModal());
|
|
2742
|
-
placeholder.querySelector("#ed-pick")?.addEventListener("click", () => {
|
|
2743
|
-
if (state.bots[0]) selectBot(state.bots[0].id);
|
|
2744
|
-
});
|
|
2745
|
-
}
|
|
1906
|
+
placeholder.innerHTML = buildEmptyDetailHTML();
|
|
1907
|
+
placeholder.querySelector("#ed-add-bot")?.addEventListener("click", () => openOnboardModal());
|
|
1908
|
+
placeholder.querySelector("#ed-pick")?.addEventListener("click", () => {
|
|
1909
|
+
if (state.bots[0]) selectBot(state.bots[0].id);
|
|
1910
|
+
});
|
|
2746
1911
|
}
|
|
2747
1912
|
|
|
2748
1913
|
/**
|
|
@@ -2760,26 +1925,6 @@ function renderEmptyOrPlaceholder() {
|
|
|
2760
1925
|
const existingEs = detail.querySelector(".es-panel");
|
|
2761
1926
|
const panel = detail.querySelector(".detail-panel");
|
|
2762
1927
|
|
|
2763
|
-
// 中心库未连接 → 全屏连接引导卡(LkConnectGuide),优先级高于空态。
|
|
2764
|
-
const centralDisconnected = state.mode === "central" && !state.centralConnected;
|
|
2765
|
-
|
|
2766
|
-
if (centralDisconnected) {
|
|
2767
|
-
if (placeholder) placeholder.style.display = "none";
|
|
2768
|
-
if (panel) panel.style.display = "none";
|
|
2769
|
-
if (!existingEs) {
|
|
2770
|
-
const esEl = document.createElement("div");
|
|
2771
|
-
esEl.className = "es-panel";
|
|
2772
|
-
esEl.style.cssText = "flex:1;min-height:0;display:flex;flex-direction:column;height:100%";
|
|
2773
|
-
detail.appendChild(esEl);
|
|
2774
|
-
}
|
|
2775
|
-
const es = detail.querySelector(".es-panel");
|
|
2776
|
-
if (es) {
|
|
2777
|
-
es.innerHTML = buildConnectGuideHTML();
|
|
2778
|
-
es.querySelector("#cc-guide-connect")?.addEventListener("click", () => openConnectFlow());
|
|
2779
|
-
}
|
|
2780
|
-
return;
|
|
2781
|
-
}
|
|
2782
|
-
|
|
2783
1928
|
if (state.bots.length === 0 && !state.selected) {
|
|
2784
1929
|
// 零助手 → 空态全屏
|
|
2785
1930
|
if (placeholder) placeholder.style.display = "none";
|
|
@@ -2793,9 +1938,8 @@ function renderEmptyOrPlaceholder() {
|
|
|
2793
1938
|
}
|
|
2794
1939
|
const es = detail.querySelector(".es-panel");
|
|
2795
1940
|
if (es) {
|
|
2796
|
-
const variant = state.mode === "central" ? "central" : "local";
|
|
2797
1941
|
const serviceRunning = state.bridge?.running ?? false;
|
|
2798
|
-
es.innerHTML = buildEmptyStateHTML(
|
|
1942
|
+
es.innerHTML = buildEmptyStateHTML(serviceRunning);
|
|
2799
1943
|
wireEmptyStateEvents(es);
|
|
2800
1944
|
}
|
|
2801
1945
|
} else {
|
|
@@ -2803,7 +1947,7 @@ function renderEmptyOrPlaceholder() {
|
|
|
2803
1947
|
if (existingEs) existingEs.remove();
|
|
2804
1948
|
if (panel) panel.style.display = "";
|
|
2805
1949
|
if (!state.selected && placeholder) {
|
|
2806
|
-
renderDetailPlaceholder();
|
|
1950
|
+
renderDetailPlaceholder();
|
|
2807
1951
|
placeholder.style.display = "";
|
|
2808
1952
|
}
|
|
2809
1953
|
}
|
|
@@ -2811,14 +1955,10 @@ function renderEmptyOrPlaceholder() {
|
|
|
2811
1955
|
|
|
2812
1956
|
/** 接线空态内的事件。*/
|
|
2813
1957
|
function wireEmptyStateEvents(container) {
|
|
2814
|
-
//
|
|
1958
|
+
// CTA → openOnboardModal
|
|
2815
1959
|
const addBtn = container.querySelector("#es-add-btn");
|
|
2816
1960
|
addBtn?.addEventListener("click", () => openOnboardModal());
|
|
2817
1961
|
|
|
2818
|
-
// central 变体:「切到本机」→ switchContext('local')
|
|
2819
|
-
const switchBtn = container.querySelector(".es-central-switch-btn");
|
|
2820
|
-
switchBtn?.addEventListener("click", () => switchContext("local"));
|
|
2821
|
-
|
|
2822
1962
|
// CTA hover 效果
|
|
2823
1963
|
addBtn?.addEventListener("mouseenter", () => {
|
|
2824
1964
|
addBtn.style.background = "#4338ca";
|
|
@@ -2866,8 +2006,6 @@ async function renderBotDetail(id) {
|
|
|
2866
2006
|
}
|
|
2867
2007
|
panel.innerHTML = `<div class="loading-block"><span class="spinner spinner-lg"></span><span>正在打开助手…</span></div>`;
|
|
2868
2008
|
|
|
2869
|
-
const readonly = state.mode === "central";
|
|
2870
|
-
|
|
2871
2009
|
// 并行拉配置 + memory
|
|
2872
2010
|
const [botRes, memRes] = await Promise.all([
|
|
2873
2011
|
api("GET", `/api/bot/${encodeURIComponent(id)}`),
|
|
@@ -2889,17 +2027,7 @@ async function renderBotDetail(id) {
|
|
|
2889
2027
|
state.formDirty = false;
|
|
2890
2028
|
state.memoryDirty = false;
|
|
2891
2029
|
|
|
2892
|
-
|
|
2893
|
-
if (readonly) {
|
|
2894
|
-
panel.innerHTML = buildCentralDetailHTML(id, bot, memContent);
|
|
2895
|
-
panel.classList.remove("panel-enter");
|
|
2896
|
-
void panel.offsetWidth;
|
|
2897
|
-
panel.classList.add("panel-enter");
|
|
2898
|
-
panel.querySelector("#cc-detail-golocal")?.addEventListener("click", () => switchContext("local"));
|
|
2899
|
-
return;
|
|
2900
|
-
}
|
|
2901
|
-
|
|
2902
|
-
panel.innerHTML = buildDetailHTML(id, bot, memContent, readonly);
|
|
2030
|
+
panel.innerHTML = buildDetailHTML(id, bot, memContent);
|
|
2903
2031
|
// 切 bot 丝滑:详情区淡入(reduced-motion 下 transition 被禁,无白闪)
|
|
2904
2032
|
panel.classList.remove("panel-enter");
|
|
2905
2033
|
void panel.offsetWidth; // 强制 reflow 重新触发动画
|
|
@@ -2908,135 +2036,11 @@ async function renderBotDetail(id) {
|
|
|
2908
2036
|
const avatar = state.avatars[id] ?? bot.avatar ?? null;
|
|
2909
2037
|
const liveKey = botLiveness(id);
|
|
2910
2038
|
acRenderAvatar(panel, id, bot.name || id, avatar, liveKey);
|
|
2911
|
-
wireDetailEvents(panel, id, bot
|
|
2039
|
+
wireDetailEvents(panel, id, bot);
|
|
2912
2040
|
renderDetailBanner();
|
|
2913
2041
|
loadRecentEvents(id, panel);
|
|
2914
2042
|
}
|
|
2915
2043
|
|
|
2916
|
-
/**
|
|
2917
|
-
* 构建中心库只读详情(CcReadonlyDetail 复刻):
|
|
2918
|
-
* hero(标题 + 描述 + by/updated/commit + 谁能用它/能改仓库/谁晋升的) + 只读横幅(回本机改) + 只读职责说明。
|
|
2919
|
-
* 中心库不显在线/心跳;memory 即「职责说明」,只读;密钥不显示。
|
|
2920
|
-
*/
|
|
2921
|
-
function buildCentralDetailHTML(id, bot, memContent) {
|
|
2922
|
-
const meta = state.centralBots.find((b) => b.id === id) ?? {};
|
|
2923
|
-
const by = meta.by || "—";
|
|
2924
|
-
const updated = meta.updated || "—";
|
|
2925
|
-
const commit = meta.commit || "—";
|
|
2926
|
-
const name = bot.name ?? id;
|
|
2927
|
-
const desc = bot.description ?? "";
|
|
2928
|
-
const chats = Array.isArray(bot.chats) ? bot.chats.length : 0;
|
|
2929
|
-
const repos = Array.isArray(bot.repos) ? bot.repos.length : 0;
|
|
2930
|
-
|
|
2931
|
-
// L1 权限层(只读):能改仓库 / repos 明细 / 令牌变量名(脱敏,绝不显真值)+ 约束。
|
|
2932
|
-
const repoList = Array.isArray(bot.repos) ? bot.repos : [];
|
|
2933
|
-
const codeAccess = !!(bot.gitlab_token_env || repoList.length > 0);
|
|
2934
|
-
const turnLimit = bot.turn_taking_limit ?? 10;
|
|
2935
|
-
// chats 元素可能是字符串(oc_…)或 {label/chat_id} 对象;统一成可读文案。
|
|
2936
|
-
const chatLabel = (c) =>
|
|
2937
|
-
typeof c === "string" ? c : (c?.label || c?.chat_id || c?.id || JSON.stringify(c));
|
|
2938
|
-
const chatsList = Array.isArray(bot.chats) ? bot.chats.map(chatLabel) : [];
|
|
2939
|
-
|
|
2940
|
-
// 只读卡片外壳(与「它的职责说明」同款)。
|
|
2941
|
-
const roCard = (title, inner) =>
|
|
2942
|
-
`<div style="background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:22px 24px;box-shadow:var(--shadow-sm);margin-bottom:16px">` +
|
|
2943
|
-
`<h4 style="margin:0 0 14px;font-size:16px;font-weight:700;color:var(--text)">${esc(title)}</h4>` +
|
|
2944
|
-
inner +
|
|
2945
|
-
`</div>`;
|
|
2946
|
-
const roKv = (label, value) =>
|
|
2947
|
-
`<div style="display:flex;gap:12px;align-items:baseline;padding:7px 0">` +
|
|
2948
|
-
`<span style="font-size:12.5px;color:var(--faint);width:104px;flex-shrink:0">${esc(label)}</span>` +
|
|
2949
|
-
`<span style="font-size:13.5px;color:var(--text);line-height:1.5">${esc(value)}</span></div>`;
|
|
2950
|
-
|
|
2951
|
-
// 「它的权限」卡内容
|
|
2952
|
-
const permInner = codeAccess
|
|
2953
|
-
? `<div style="display:inline-flex;align-items:center;gap:7px;font-size:14px;font-weight:600;color:${LIVE_TEXT.serving}">${ICONS.check} 能改代码仓库 · 通过 GitLab 令牌</div>` +
|
|
2954
|
-
(repoList.length
|
|
2955
|
-
? `<div style="margin-top:12px;display:flex;flex-direction:column;gap:8px">` +
|
|
2956
|
-
repoList.map((r) =>
|
|
2957
|
-
`<div style="padding:10px 12px;border:1px solid var(--border);border-radius:10px;background:var(--bg)">` +
|
|
2958
|
-
`<div style="font-size:13.5px;font-weight:600;color:var(--text);font-family:ui-monospace,monospace">${esc(r.slug || "—")} <span style="color:var(--faint);font-weight:400">@ ${esc(r.branch || "main")}</span></div>` +
|
|
2959
|
-
(r.url ? `<div style="margin-top:3px;font-size:12px;color:var(--muted);font-family:ui-monospace,monospace;word-break:break-all">${esc(r.url)}</div>` : "") +
|
|
2960
|
-
`</div>`
|
|
2961
|
-
).join("") +
|
|
2962
|
-
`</div>`
|
|
2963
|
-
: `<p style="margin:10px 0 0;font-size:13px;color:var(--muted);line-height:1.55">未列具体仓库 —— agent 用令牌自己判断需要哪些、自己 clone。</p>`) +
|
|
2964
|
-
(bot.gitlab_token_env
|
|
2965
|
-
? `<p style="margin:12px 0 0;font-size:12px;color:var(--faint);display:flex;align-items:center;gap:6px">${ICONS.lock} 令牌已配置,真值只存本机 <code>.env</code>(0600),这里看不到。</p>`
|
|
2966
|
-
: "")
|
|
2967
|
-
: `<div style="display:inline-flex;align-items:center;gap:7px;font-size:14px;color:var(--muted)">${ICONS.lock} 纯答疑,不碰代码。</div>`;
|
|
2968
|
-
|
|
2969
|
-
// 「行为约束」卡内容
|
|
2970
|
-
const constraintInner =
|
|
2971
|
-
roKv("谁能 @ 它", chatsList.length ? chatsList.join("、") : "任何群都能 @(没限制)") +
|
|
2972
|
-
roKv("最多连做", `${turnLimit} 步就停下问人`);
|
|
2973
|
-
|
|
2974
|
-
const metaCell = (label, value, tone) =>
|
|
2975
|
-
`<div style="display:flex;flex-direction:column;gap:3px">` +
|
|
2976
|
-
`<span style="font-size:10.5px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--faint)">${esc(label)}</span>` +
|
|
2977
|
-
`<span style="font-size:13.5px;font-weight:600;color:${tone || "var(--text)"}">${esc(value)}</span>` +
|
|
2978
|
-
`</div>`;
|
|
2979
|
-
|
|
2980
|
-
const chatVal = chats === 0 ? "任何群都能 @" : `仅 ${chats} 个群`;
|
|
2981
|
-
const repoVal = repos === 0 ? "纯答疑" : `${repos} 个仓库`;
|
|
2982
|
-
const botBackend = bot.backend || LK_BACKEND_DEFAULT;
|
|
2983
|
-
|
|
2984
|
-
// 底座列:用 chip(sm + mono)内联显示,套进 metaCell value 位置
|
|
2985
|
-
const backendCellValue =
|
|
2986
|
-
`<span style="display:inline-flex">${lkBackendChipHTML(botBackend, { size: "sm", mono: true })}</span>`;
|
|
2987
|
-
const metaCellRaw = (label, innerHTML) =>
|
|
2988
|
-
`<div style="display:flex;flex-direction:column;gap:3px">` +
|
|
2989
|
-
`<span style="font-size:10.5px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--faint)">${esc(label)}</span>` +
|
|
2990
|
-
innerHTML +
|
|
2991
|
-
`</div>`;
|
|
2992
|
-
|
|
2993
|
-
return (
|
|
2994
|
-
// hero band
|
|
2995
|
-
`<div style="padding:30px 44px 24px;background:linear-gradient(180deg,var(--bg) 0%,var(--surface) 100%);border-bottom:1px solid var(--border)">` +
|
|
2996
|
-
`<div style="max-width:800px;display:flex;align-items:flex-start;gap:22px">` +
|
|
2997
|
-
avatarHTML(id, name, bot.avatar ?? null, "list", "unknown").replace(/avatar-list/, "avatar-list cc-detail-avatar").replace(/<span class="avatar-dot[^>]*><\/span>/, "") +
|
|
2998
|
-
`<div style="flex:1;min-width:0;padding-top:2px">` +
|
|
2999
|
-
`<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px">` +
|
|
3000
|
-
`<span style="font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--faint)">公司中心库 · 共享助手</span>` +
|
|
3001
|
-
`<span style="display:inline-flex;align-items:center;gap:5px;padding:2px 9px;border-radius:999px;font-size:11.5px;font-weight:700;color:var(--muted);background:var(--bg);border:1px solid var(--border)">${ICONS.lock} 只读</span>` +
|
|
3002
|
-
`</div>` +
|
|
3003
|
-
`<h1 style="margin:0 0 8px;font-size:31px;font-weight:800;letter-spacing:-.025em;line-height:1.06;color:var(--text)">${esc(name)}</h1>` +
|
|
3004
|
-
`<p style="margin:0 0 14px;font-size:15.5px;color:var(--muted);line-height:1.55;max-width:540px">${esc(desc || "(还没填介绍)")}</p>` +
|
|
3005
|
-
`<div style="display:flex;align-items:center;gap:14px;flex-wrap:wrap;font-size:13px;color:var(--muted)">` +
|
|
3006
|
-
`<span style="display:inline-flex;align-items:center;gap:6px">${ICONS.upload} ${esc(by)} 晋升</span>` +
|
|
3007
|
-
`<span style="color:var(--faint)">·</span><span>更新于 ${esc(updated)}</span>` +
|
|
3008
|
-
`<span style="color:var(--faint)">·</span><span style="font-family:ui-monospace,monospace">commit ${esc(commit)}</span>` +
|
|
3009
|
-
`</div></div></div>` +
|
|
3010
|
-
`<div style="max-width:800px;margin-top:22px;padding-top:18px;border-top:1px solid var(--border);display:grid;grid-template-columns:repeat(4,1fr);gap:16px">` +
|
|
3011
|
-
metaCell("谁能用它", chatVal, chats === 0 ? BR.text : null) +
|
|
3012
|
-
metaCell("能改仓库", repoVal, null) +
|
|
3013
|
-
metaCell("谁晋升的", by, null) +
|
|
3014
|
-
metaCellRaw("底座", backendCellValue) +
|
|
3015
|
-
`</div></div>` +
|
|
3016
|
-
// body
|
|
3017
|
-
`<div style="padding:24px 44px 36px"><div style="max-width:800px">` +
|
|
3018
|
-
// 只读横幅 + 回本机引导
|
|
3019
|
-
`<div class="cc-readonly-banner">${ICONS.lock}` +
|
|
3020
|
-
`<div style="min-width:0;flex:1">` +
|
|
3021
|
-
`<div class="cc-readonly-banner-title">这是中心库里的只读副本</div>` +
|
|
3022
|
-
`<div class="cc-readonly-banner-sub">中心库不能直接改。想改?回「本机」改好那一份,再<b>晋升</b>覆盖上来。</div>` +
|
|
3023
|
-
`</div>` +
|
|
3024
|
-
`<button type="button" id="cc-detail-golocal" class="cc-go-local-btn">回本机改 ${ICONS.arrowRight}</button>` +
|
|
3025
|
-
`</div>` +
|
|
3026
|
-
// 只读底座展示卡
|
|
3027
|
-
roCard("底座", lkBackendSelectHTML(botBackend, true)) +
|
|
3028
|
-
// L1 权限层(只读):它能改哪些仓库 + 令牌变量名(脱敏)
|
|
3029
|
-
roCard("它的权限", permInner) +
|
|
3030
|
-
// 约束层(只读):谁能 @ + 最多连做几步
|
|
3031
|
-
roCard("行为约束", constraintInner) +
|
|
3032
|
-
// 只读职责说明(memory)
|
|
3033
|
-
`<div style="background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:22px 24px;box-shadow:var(--shadow-sm)">` +
|
|
3034
|
-
`<h4 style="margin:0 0 14px;font-size:16px;font-weight:700;color:var(--text)">它的职责说明</h4>` +
|
|
3035
|
-
`<div style="width:100%;padding:12px 14px;border:1px solid var(--border);border-radius:10px;background:var(--bg);font-size:13.5px;line-height:1.65;color:#334155;font-family:ui-monospace,'Cascadia Code',monospace;white-space:pre-wrap">${esc(memContent || "(没有职责说明)")}</div>` +
|
|
3036
|
-
`<p style="margin:12px 0 0;font-size:12px;color:var(--faint);display:flex;align-items:center;gap:6px">${ICONS.lock} 只读 —— 在这里看不到、也改不了真密钥;每个人同步到本机后用自己的密钥跑。</p>` +
|
|
3037
|
-
`</div></div></div>`
|
|
3038
|
-
);
|
|
3039
|
-
}
|
|
3040
2044
|
|
|
3041
2045
|
/**
|
|
3042
2046
|
* 构建 hero band 内层(名字 / 介绍 / meta strip)—— 依赖 P.form 的实时值,
|
|
@@ -3046,7 +2050,7 @@ function buildCentralDetailHTML(id, bot, memContent) {
|
|
|
3046
2050
|
* @param {{name:string,description:string,chatCount:number,repoCount:number}} f 当前表单值
|
|
3047
2051
|
* @param {boolean} readonly
|
|
3048
2052
|
*/
|
|
3049
|
-
function buildHeroInner(id, bot, f
|
|
2053
|
+
function buildHeroInner(id, bot, f) {
|
|
3050
2054
|
const idx = state.bots.findIndex((b) => b.id === id);
|
|
3051
2055
|
const num = String((idx < 0 ? 0 : idx) + 1).padStart(2, "0");
|
|
3052
2056
|
// ⑤ hero 内状态条:BL-18 重启窗口内用 restartDisplayLive 覆盖;否则 effLive
|
|
@@ -3057,10 +2061,8 @@ function buildHeroInner(id, bot, f, readonly) {
|
|
|
3057
2061
|
const dispLiveKey = (isR || isTimeout) ? restartDisplayLive(realLive, rs.status) : realLive;
|
|
3058
2062
|
const liveKey = dispLiveKey;
|
|
3059
2063
|
|
|
3060
|
-
const editBadge =
|
|
3061
|
-
|
|
3062
|
-
: `<span class="hero-badge">${ICONS.check} 可改</span>`;
|
|
3063
|
-
const eyebrowLabel = readonly ? "公司中心库 · 第" : "本机助手 · 第";
|
|
2064
|
+
const editBadge = `<span class="hero-badge">${ICONS.check} 可改</span>`;
|
|
2065
|
+
const eyebrowLabel = "本机助手 · 第";
|
|
3064
2066
|
|
|
3065
2067
|
// 状态条(1.5px edge pill):重启中 → sky「重启中 · 预期内,正在恢复」;否则正常
|
|
3066
2068
|
const statusPillText = isR
|
|
@@ -3109,11 +2111,9 @@ function buildHeroInner(id, bot, f, readonly) {
|
|
|
3109
2111
|
`<span style="display:inline-flex">${lkBackendChipHTML(bot.backend || LK_BACKEND_DEFAULT, { size: "sm", mono: true })}</span>` +
|
|
3110
2112
|
`</div>`;
|
|
3111
2113
|
|
|
3112
|
-
const heroDelBtn =
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
`<svg class="icon" viewBox="0 0 24 24" aria-hidden="true" style="width:14px;height:14px;flex-shrink:0"><path d="M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6M10 11v6M14 11v6"/></svg>` +
|
|
3116
|
-
` 删除</button>`;
|
|
2114
|
+
const heroDelBtn = `<button class="btn btn-hero-del" id="btn-hero-del" type="button" title="删除助手">` +
|
|
2115
|
+
`<svg class="icon" viewBox="0 0 24 24" aria-hidden="true" style="width:14px;height:14px;flex-shrink:0"><path d="M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6M10 11v6M14 11v6"/></svg>` +
|
|
2116
|
+
` 删除</button>`;
|
|
3117
2117
|
|
|
3118
2118
|
// 触点②:hero kicker 行,紧跟眉批加 backend chip(mono + vendor)
|
|
3119
2119
|
const backendKickerChip = lkBackendChipHTML(bot.backend || LK_BACKEND_DEFAULT, { mono: true, vendor: true });
|
|
@@ -3323,7 +2323,7 @@ function acRepoRowHTML(idx, repo) {
|
|
|
3323
2323
|
`</div>` +
|
|
3324
2324
|
`<div class="ac-field" style="margin-top:8px">` +
|
|
3325
2325
|
`<label class="ac-label" for="ac-repo-url-${idx}">clone 地址 <span class="ac-optional">私有库必需</span></label>` +
|
|
3326
|
-
`<input id="ac-repo-url-${idx}" class="ac-input ac-mono" type="text" placeholder="git@
|
|
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)}" />` +
|
|
3327
2327
|
`</div>` +
|
|
3328
2328
|
`<button type="button" class="ac-repo-del" title="移除这个仓库" data-repo-idx="${idx}" aria-label="移除仓库 ${esc(repo.slug || idx + 1)}">` +
|
|
3329
2329
|
xIcon +
|
|
@@ -3355,8 +2355,8 @@ function acPillHTML(tone, iconD, text) {
|
|
|
3355
2355
|
function buildAgentConfigHTML(bot, memContent, mode, prefill) {
|
|
3356
2356
|
const isCreate = mode === "create";
|
|
3357
2357
|
const repos = normalizeRepos(bot.repos);
|
|
3358
|
-
// ① 用 gitlab_token_env
|
|
3359
|
-
const gitlabConfigured = !!(bot.gitlab_token_env);
|
|
2358
|
+
// ① 用 git_token_env(兼容旧 gitlab_token_env)非空来判断「已配置」——新保存契约:值存后端,前端只知道「有/无」
|
|
2359
|
+
const gitlabConfigured = !!(bot.git_token_env || bot.gitlab_token_env);
|
|
3360
2360
|
const codeAccess = !!(gitlabConfigured || repos.length > 0);
|
|
3361
2361
|
const chatsVal = Array.isArray(bot.chats) ? bot.chats.join("\n") : (bot.chats || "");
|
|
3362
2362
|
const turnLimit = bot.turn_taking_limit ?? 10;
|
|
@@ -3450,7 +2450,7 @@ function buildAgentConfigHTML(bot, memContent, mode, prefill) {
|
|
|
3450
2450
|
`<div class="ac-access-toggle-row${codeAccess ? " is-on" : ""}" id="ac-access-row">` +
|
|
3451
2451
|
`<div class="ac-access-info">` +
|
|
3452
2452
|
`<div class="ac-access-title">给它访问代码仓库的权限</div>` +
|
|
3453
|
-
`<p class="ac-access-desc">通过配一个 <b>
|
|
2453
|
+
`<p class="ac-access-desc">通过配一个 <b>Git 访问令牌</b> 来表达。读和改都用这一个令牌 —— <b>没有读 / 写之分</b>,agent 看任务自己定。不开 = 纯对话 / 自带知识答疑。</p>` +
|
|
3454
2454
|
`</div>` +
|
|
3455
2455
|
`<button type="button" class="ac-toggle${codeAccess ? " is-on" : ""}" id="ac-code-access-btn" role="switch" aria-checked="${codeAccess}" title="开 / 关代码访问权限">` +
|
|
3456
2456
|
`<span class="ac-toggle-thumb"></span>` +
|
|
@@ -3464,8 +2464,8 @@ function buildAgentConfigHTML(bot, memContent, mode, prefill) {
|
|
|
3464
2464
|
// 开启态:令牌 + 仓库
|
|
3465
2465
|
`<div class="ac-access-fields" id="ac-access-fields" ${codeAccess ? "" : 'style="display:none"'}>` +
|
|
3466
2466
|
`<div class="ac-field">` +
|
|
3467
|
-
`<label class="ac-label" for="ac-gitlab-token">
|
|
3468
|
-
`<p class="ac-hint">把
|
|
2467
|
+
`<label class="ac-label" for="ac-gitlab-token">Git Access Token</label>` +
|
|
2468
|
+
`<p class="ac-hint">把 Git access token 粘这里。只存本机 <code>~/.larkway/.env</code>(权限 0600),不回显、不外发。</p>` +
|
|
3469
2469
|
(gitlabConfigured
|
|
3470
2470
|
? `<div class="ac-token-configured" id="ac-token-configured">` +
|
|
3471
2471
|
`<span class="ac-token-mask">${ICONS.lock} 已配置 <span style="letter-spacing:.12em">••••••</span></span>` +
|
|
@@ -3477,7 +2477,7 @@ function buildAgentConfigHTML(bot, memContent, mode, prefill) {
|
|
|
3477
2477
|
`</div>`
|
|
3478
2478
|
: `<div class="ac-secret-wrap" id="ac-token-input-wrap">` +
|
|
3479
2479
|
`<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>` +
|
|
3480
|
-
`<input id="ac-gitlab-token" name="gitlab_token_value" class="ac-input ac-secret-input" type="password" autocomplete="new-password" placeholder="粘贴
|
|
2480
|
+
`<input id="ac-gitlab-token" name="gitlab_token_value" class="ac-input ac-secret-input" type="password" autocomplete="new-password" placeholder="粘贴 Git access token(只存本机,不回显)" value="" />` +
|
|
3481
2481
|
`</div>`
|
|
3482
2482
|
) +
|
|
3483
2483
|
`</div>` +
|
|
@@ -4116,7 +3116,7 @@ function wireRecentEvents(panel, id) {
|
|
|
4116
3116
|
* hero band + status action 保留;配置表单 + memory 由 AC 面板统一承载。
|
|
4117
3117
|
* secret 只显示变量名。
|
|
4118
3118
|
*/
|
|
4119
|
-
function buildDetailHTML(id, bot, memContent
|
|
3119
|
+
function buildDetailHTML(id, bot, memContent) {
|
|
4120
3120
|
// 详情头像(hero,右下角状态角标 + 健康脉冲环);头像 URL 复用列表/status 同源
|
|
4121
3121
|
const avatar = state.avatars[id] ?? bot.avatar ?? null;
|
|
4122
3122
|
// ⑤ hero 颜色以后端 effLive 为准:bridge 停时即使 status 说 serving,也强制 offline
|
|
@@ -4135,61 +3135,32 @@ function buildDetailHTML(id, bot, memContent, readonly) {
|
|
|
4135
3135
|
chatCount: chatLines.length,
|
|
4136
3136
|
repoCount: repos.length,
|
|
4137
3137
|
},
|
|
4138
|
-
readonly,
|
|
4139
3138
|
);
|
|
4140
3139
|
|
|
4141
3140
|
// 状态可操作化面板
|
|
4142
3141
|
const effLiveKey = effLive(id);
|
|
4143
|
-
const statusActionPanel =
|
|
3142
|
+
const statusActionPanel = effLiveKey !== "serving"
|
|
4144
3143
|
? `<div id="detail-status-action">${buildStatusActionPanel(effLiveKey, null, false)}</div>`
|
|
4145
3144
|
: `<div id="detail-status-action"></div>`;
|
|
4146
3145
|
|
|
4147
|
-
const centralReadonlyNotice = readonly
|
|
4148
|
-
? `<div class="notice notice-warn">${ICONS.lock}<div>中心库的配置是只读的,改本地后用「晋升」推上去。</div></div>`
|
|
4149
|
-
: "";
|
|
4150
|
-
|
|
4151
3146
|
// 三层 AC 配置面板(edit 态)
|
|
4152
3147
|
const acPanelHTML = buildAgentConfigHTML(bot, memContent, "edit");
|
|
4153
3148
|
|
|
4154
|
-
// 触点④(编辑表单顶部):底座选择卡
|
|
4155
|
-
// backendKickerChip 已在 kicker 展示;这里用完整 select 控件让用户改
|
|
3149
|
+
// 触点④(编辑表单顶部):底座选择卡
|
|
4156
3150
|
const _configuredBk4 = bot.backend || LK_BACKEND_DEFAULT;
|
|
4157
3151
|
const _runningBk4 = state.runningBackends[id] ?? null;
|
|
4158
|
-
// BL-17:底座不一致 badge(详情区,md)—
|
|
3152
|
+
// BL-17:底座不一致 badge(详情区,md)— 只在 bridge 在跑时显示
|
|
4159
3153
|
const bkMismatchDetail =
|
|
4160
|
-
|
|
3154
|
+
state.bridge?.running && isBackendMismatch(_runningBk4, _configuredBk4)
|
|
4161
3155
|
? `<div style="margin-top:10px">${backendMismatchBadgeHTML(_runningBk4, _configuredBk4, "md")}` +
|
|
4162
3156
|
`<span style="margin-left:8px;font-size:12px;color:var(--muted)">右上角重启服务后生效</span></div>`
|
|
4163
3157
|
: "";
|
|
4164
|
-
const backendCardHTML =
|
|
4165
|
-
`<div class="lk-bk-card" id="lk-bk-card">` +
|
|
3158
|
+
const backendCardHTML = `<div class="lk-bk-card" id="lk-bk-card">` +
|
|
4166
3159
|
`<h4 class="lk-bk-card-title">${ICONS.box} 用哪个底座驱动它</h4>` +
|
|
4167
3160
|
`<p class="lk-bk-card-desc">底座决定这个助手背后跑哪个 CLI agent。默认 Codex;切换后需重启服务生效。</p>` +
|
|
4168
|
-
lkBackendSelectHTML(_configuredBk4,
|
|
3161
|
+
lkBackendSelectHTML(_configuredBk4, `bk-edit-${id}`) +
|
|
4169
3162
|
bkMismatchDetail +
|
|
4170
|
-
`</div
|
|
4171
|
-
) : "";
|
|
4172
|
-
|
|
4173
|
-
// 晋升区(仅本机非只读时显示)
|
|
4174
|
-
const promoteSection = !readonly ? `
|
|
4175
|
-
<div class="form-section promote-section" id="promote-section">
|
|
4176
|
-
<h4 class="section-title">${ICONS.box} 交给公司统一管理(晋升)</h4>
|
|
4177
|
-
<p class="section-desc">${
|
|
4178
|
-
state.centralAvailable
|
|
4179
|
-
? `已连上 <b style="font-family:ui-monospace,monospace;color:var(--text)">${esc(state.centralRepo?.name ?? "公司中心库")}</b>。先暂存只动本机副本;正式上传会推给全团队。`
|
|
4180
|
-
: "把这个本地助手交给公司中心库,其它机器就能拉到同一份。"
|
|
4181
|
-
}</p>
|
|
4182
|
-
<div class="promote-actions">
|
|
4183
|
-
<button class="btn${state.centralAvailable ? "" : " is-disabled"}" id="btn-promote-no-push" type="button"${state.centralAvailable ? "" : " disabled"}>${ICONS.box} 先暂存到本机副本(不上传)</button>
|
|
4184
|
-
<button class="btn btn-upload${state.centralAvailable ? "" : " is-disabled"}" id="btn-promote-push" type="button"${state.centralAvailable ? "" : " disabled"}>${ICONS.upload} 正式上传到公司中心库</button>
|
|
4185
|
-
</div>
|
|
4186
|
-
${
|
|
4187
|
-
state.centralAvailable
|
|
4188
|
-
? `<p class="promote-hint">${ICONS.warn}<span><b style="color:var(--destructive-text)">正式上传</b>会推送到团队仓库,所有人都拉得到,<b style="color:var(--destructive-text)">推后无法自动撤销</b> —— 点了会再确认一次。</span></p>`
|
|
4189
|
-
: `<div class="promote-connect-hint">${ICONS.link2}<span>得先<b>连接公司中心库</b>,这两个按钮才能用。</span><button type="button" id="btn-promote-connect" class="btn btn-primary btn-sm">${ICONS.link2} 去连接</button></div>`
|
|
4190
|
-
}
|
|
4191
|
-
</div>
|
|
4192
|
-
` : "";
|
|
3163
|
+
`</div>`;
|
|
4193
3164
|
|
|
4194
3165
|
return `
|
|
4195
3166
|
<!-- 编辑式 hero band(健康=indigo soft 渐变,异常=status soft 渐变) -->
|
|
@@ -4204,7 +3175,6 @@ function buildDetailHTML(id, bot, memContent, readonly) {
|
|
|
4204
3175
|
<div class="config-area">
|
|
4205
3176
|
<div class="status-banner" id="detail-status-banner" role="status" aria-live="polite" hidden></div>
|
|
4206
3177
|
${statusActionPanel}
|
|
4207
|
-
${centralReadonlyNotice}
|
|
4208
3178
|
|
|
4209
3179
|
${buildRecentEventsPanelHTML(id)}
|
|
4210
3180
|
|
|
@@ -4215,8 +3185,6 @@ function buildDetailHTML(id, bot, memContent, readonly) {
|
|
|
4215
3185
|
<div class="ac-panel-wrap" id="ac-panel-wrap">
|
|
4216
3186
|
${acPanelHTML}
|
|
4217
3187
|
</div>
|
|
4218
|
-
|
|
4219
|
-
${promoteSection}
|
|
4220
3188
|
</div>
|
|
4221
3189
|
`;
|
|
4222
3190
|
}
|
|
@@ -4301,16 +3269,13 @@ function wireRestartPanelButtons(panel) {
|
|
|
4301
3269
|
}
|
|
4302
3270
|
|
|
4303
3271
|
/** 为详情区内的按钮绑定事件(三层 AC 面板版)。 */
|
|
4304
|
-
function wireDetailEvents(panel, id, bot
|
|
3272
|
+
function wireDetailEvents(panel, id, bot) {
|
|
4305
3273
|
wireRecentEvents(panel, id);
|
|
4306
|
-
// Wire status-action fix buttons (always, even in readonly — fix buttons only appear in non-readonly)
|
|
4307
3274
|
const statusActionContainer = panel.querySelector("#detail-status-action");
|
|
4308
3275
|
if (statusActionContainer) wireStatusActionButtons(statusActionContainer);
|
|
4309
3276
|
// Wire restart panel buttons (timeout panel: logs/restart/rescan)
|
|
4310
3277
|
wireRestartPanelButtons(panel);
|
|
4311
3278
|
|
|
4312
|
-
if (readonly) return;
|
|
4313
|
-
|
|
4314
3279
|
// ── 三层 AC 面板:交互 + 脏检测 + 保存 ──────────────────────────────────
|
|
4315
3280
|
wireAgentConfigEvents(panel, id, bot);
|
|
4316
3281
|
|
|
@@ -4372,7 +3337,6 @@ function wireDetailEvents(panel, id, bot, readonly) {
|
|
|
4372
3337
|
chatCount: chatLines.length,
|
|
4373
3338
|
repoCount: repos.length,
|
|
4374
3339
|
},
|
|
4375
|
-
false,
|
|
4376
3340
|
);
|
|
4377
3341
|
// re-wire hero del btn
|
|
4378
3342
|
heroBody.querySelector("#btn-hero-del")?.addEventListener("click", () => doDeleteBot(id));
|
|
@@ -4393,34 +3357,6 @@ function wireDetailEvents(panel, id, bot, readonly) {
|
|
|
4393
3357
|
btnHeroDel.addEventListener("click", () => doDeleteBot(id));
|
|
4394
3358
|
}
|
|
4395
3359
|
|
|
4396
|
-
// promote 不推送
|
|
4397
|
-
const btnPromoteNoP = panel.querySelector("#btn-promote-no-push");
|
|
4398
|
-
if (btnPromoteNoP) {
|
|
4399
|
-
btnPromoteNoP.addEventListener("click", async () => {
|
|
4400
|
-
if (btnPromoteNoP.disabled) return;
|
|
4401
|
-
await doPromote(btnPromoteNoP, id, false, panel);
|
|
4402
|
-
});
|
|
4403
|
-
}
|
|
4404
|
-
|
|
4405
|
-
// promote 并推送(不可逆外发 → LkUploadConfirm 样式二次确认弹窗)
|
|
4406
|
-
const btnPromoteP = panel.querySelector("#btn-promote-push");
|
|
4407
|
-
if (btnPromoteP) {
|
|
4408
|
-
btnPromoteP.addEventListener("click", async () => {
|
|
4409
|
-
if (btnPromoteP.disabled) return;
|
|
4410
|
-
const confirmed = await uploadConfirmDialog(
|
|
4411
|
-
{ name: bot.name || id },
|
|
4412
|
-
state.centralRepo,
|
|
4413
|
-
);
|
|
4414
|
-
if (!confirmed) return;
|
|
4415
|
-
await doPromote(btnPromoteP, id, true, panel);
|
|
4416
|
-
});
|
|
4417
|
-
}
|
|
4418
|
-
|
|
4419
|
-
// 未连接态:「去连接」→ 打开连接流弹窗(连上后 centralAvailable 变 true,晋升按钮启用)
|
|
4420
|
-
const btnPromoteConnect = panel.querySelector("#btn-promote-connect");
|
|
4421
|
-
if (btnPromoteConnect) {
|
|
4422
|
-
btnPromoteConnect.addEventListener("click", () => openConnectFlow());
|
|
4423
|
-
}
|
|
4424
3360
|
}
|
|
4425
3361
|
|
|
4426
3362
|
/**
|
|
@@ -4435,13 +3371,12 @@ function acRefreshHero(panel, id, bot) {
|
|
|
4435
3371
|
const vals = readAgentConfigValues(panel);
|
|
4436
3372
|
const chatCount = Array.isArray(vals.chats) ? vals.chats.length : 0;
|
|
4437
3373
|
const repoCount = Array.isArray(vals.repos) ? vals.repos.length : 0;
|
|
4438
|
-
const readonly = false;
|
|
4439
3374
|
body.innerHTML = buildHeroInner(id, { ...bot, name: vals.name, description: vals.description }, {
|
|
4440
3375
|
name: vals.name ?? "",
|
|
4441
3376
|
description: vals.description ?? "",
|
|
4442
3377
|
chatCount,
|
|
4443
3378
|
repoCount,
|
|
4444
|
-
}
|
|
3379
|
+
});
|
|
4445
3380
|
// re-wire del button(内容重建后需重绑)
|
|
4446
3381
|
const btnHeroDel = body.querySelector("#btn-hero-del");
|
|
4447
3382
|
if (btnHeroDel) btnHeroDel.addEventListener("click", () => doDeleteBot(id));
|
|
@@ -4606,190 +3541,7 @@ async function saveAcBot(panel, id) {
|
|
|
4606
3541
|
}
|
|
4607
3542
|
}
|
|
4608
3543
|
|
|
4609
|
-
/** commit hash → 短 7 位(便于运营报给工程师);取不到给原值。 */
|
|
4610
|
-
function shortHash(sha) {
|
|
4611
|
-
return typeof sha === "string" && sha.length >= 7 ? sha.slice(0, 7) : (sha ?? "?");
|
|
4612
|
-
}
|
|
4613
|
-
|
|
4614
|
-
/**
|
|
4615
|
-
* 晋升:暂存(本地 indigo)/ 正式上传(红,不可逆外发)。
|
|
4616
|
-
* 成功 → toast 给 commit 短 hash;失败按 kind 走「怎么办」:
|
|
4617
|
-
* - behind → 要先同步别人的改动,带「去同步」入口
|
|
4618
|
-
* - noperm → 没写权限,提示找工程师开权限
|
|
4619
|
-
* - 其它 → 原样人话错误
|
|
4620
|
-
* 增强后的 POST /api/promote/:id:成功 { ok:true, commit, pushed };
|
|
4621
|
-
* 失败(HTTP 200){ ok:false, kind:"behind"|"noperm"|"other", error };
|
|
4622
|
-
* 未连接(HTTP 409){ error }。
|
|
4623
|
-
* @param {HTMLElement} btn
|
|
4624
|
-
* @param {string} id
|
|
4625
|
-
* @param {boolean} push true=正式上传 / false=仅暂存本机
|
|
4626
|
-
* @param {HTMLElement|null} [panel] 详情面板根元素,有则在 promote-section 内联渲染结果
|
|
4627
|
-
*/
|
|
4628
|
-
async function doPromote(btn, id, push, panel, _extraBody) {
|
|
4629
|
-
const restore = btnLoading(btn, push ? "正在上传…" : "暂存中…");
|
|
4630
|
-
const body = { push, ...((_extraBody) ? _extraBody : {}) };
|
|
4631
|
-
const res = await api("POST", `/api/promote/${encodeURIComponent(id)}`, body);
|
|
4632
|
-
restore();
|
|
4633
3544
|
|
|
4634
|
-
// ⑧ 409 有两种情况:requiresConfirm(推 main 需要二次确认) 或 未连接中心库
|
|
4635
|
-
if (res.status === 409) {
|
|
4636
|
-
if (res.json?.requiresConfirm) {
|
|
4637
|
-
const branch = res.json.branch ?? "main";
|
|
4638
|
-
const confirmed = await confirmDialog({
|
|
4639
|
-
title: "推送到主干分支",
|
|
4640
|
-
body: `你正要晋升到公司主干 \`${branch}\`!确认?此操作会覆盖团队共享内容,推后无法自动撤销。`,
|
|
4641
|
-
confirmText: "确认晋升",
|
|
4642
|
-
confirmDanger: true,
|
|
4643
|
-
});
|
|
4644
|
-
if (confirmed) {
|
|
4645
|
-
await doPromote(btn, id, push, panel, { confirmMain: true });
|
|
4646
|
-
}
|
|
4647
|
-
return;
|
|
4648
|
-
}
|
|
4649
|
-
toast("未连接公司中心库,无法晋升。先在顶部「公司中心库」里连接一次。", "error");
|
|
4650
|
-
return;
|
|
4651
|
-
}
|
|
4652
|
-
|
|
4653
|
-
// 网络/服务器层错误(无 json.ok 字段)
|
|
4654
|
-
if (!res.ok && res.json?.ok === undefined) {
|
|
4655
|
-
toast(`失败:${res.json?.error ?? res.status}`, "error");
|
|
4656
|
-
return;
|
|
4657
|
-
}
|
|
4658
|
-
|
|
4659
|
-
const j = res.json ?? {};
|
|
4660
|
-
|
|
4661
|
-
// 业务失败(HTTP 200 + ok:false + kind)
|
|
4662
|
-
if (j.ok === false) {
|
|
4663
|
-
if (j.kind === "behind") {
|
|
4664
|
-
// 落后:内联渲染 amber 失败面板 + 「先同步再上传」按钮
|
|
4665
|
-
renderPromoteFailPanel(panel, "behind", null, async () => {
|
|
4666
|
-
if (state.mode !== "central") await switchContext("central");
|
|
4667
|
-
if (state.centralConnected) await doCentralSync();
|
|
4668
|
-
});
|
|
4669
|
-
return;
|
|
4670
|
-
}
|
|
4671
|
-
if (j.kind === "noperm") {
|
|
4672
|
-
// 无权限:内联渲染红色失败面板 + 「重试」按钮
|
|
4673
|
-
renderPromoteFailPanel(panel, "noperm", j.error ?? null, async () => {
|
|
4674
|
-
await doPromote(btn, id, push, panel);
|
|
4675
|
-
});
|
|
4676
|
-
return;
|
|
4677
|
-
}
|
|
4678
|
-
// 其它失败:内联渲染红色失败面板
|
|
4679
|
-
renderPromoteFailPanel(panel, "other", j.error ?? null, async () => {
|
|
4680
|
-
await doPromote(btn, id, push, panel);
|
|
4681
|
-
});
|
|
4682
|
-
return;
|
|
4683
|
-
}
|
|
4684
|
-
|
|
4685
|
-
// 成功:commit 短 hash + 是否真推到了中心库
|
|
4686
|
-
const commit = shortHash(j.commit ?? j.sha);
|
|
4687
|
-
if (j.pushed) {
|
|
4688
|
-
const botName = state.bots.find((b) => b.id === id)?.name ?? id;
|
|
4689
|
-
const repoPath = state.centralRepo?.path ?? "bots/";
|
|
4690
|
-
renderPromoteSuccessPanel(panel, botName, commit, repoPath);
|
|
4691
|
-
} else {
|
|
4692
|
-
toast(`已暂存到本机副本(未上传)· commit ${commit}`, "ok");
|
|
4693
|
-
}
|
|
4694
|
-
}
|
|
4695
|
-
|
|
4696
|
-
/**
|
|
4697
|
-
* 在 promote-section 内渲染上传成功内联面板(复刻 LkPromoteCard phase==='success')。
|
|
4698
|
-
* 绿色 check 徽章 + 「已上传」标题 + larkway sync 提示 + 路径/commit + 「去中心库看看」。
|
|
4699
|
-
* @param {HTMLElement|null} panel
|
|
4700
|
-
* @param {string} botName
|
|
4701
|
-
* @param {string} commit 短 7 位 hash
|
|
4702
|
-
* @param {string} repoPath "bots/" 之类
|
|
4703
|
-
*/
|
|
4704
|
-
function renderPromoteSuccessPanel(panel, botName, commit, repoPath) {
|
|
4705
|
-
const section = panel?.querySelector(".promote-section");
|
|
4706
|
-
if (!section) { toast(`已上传「${esc(botName)}」到公司中心库 · commit ${commit}`, "ok"); return; }
|
|
4707
|
-
|
|
4708
|
-
const gC = LIVE_COLOR.serving, gE = LIVE_EDGE.serving, gS = LIVE_SOFT.serving, gT = LIVE_TEXT.serving;
|
|
4709
|
-
const checkSVG = `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true" style="width:19px;height:19px;stroke-width:2.4"><path d="M20 6 9 17l-5-5"/></svg>`;
|
|
4710
|
-
const arrowSVG = `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true" style="width:15px;height:15px"><path d="M5 12h14M13 6l6 6-6 6"/></svg>`;
|
|
4711
|
-
|
|
4712
|
-
section.innerHTML =
|
|
4713
|
-
`<h4 class="section-title">${ICONS.box} 交给公司统一管理(晋升)</h4>` +
|
|
4714
|
-
`<div class="promote-result-panel" style="background:${gS};border:1px solid ${gE}">` +
|
|
4715
|
-
`<span class="prp-badge" style="border:1px solid ${gE};color:${gC}">${checkSVG}</span>` +
|
|
4716
|
-
`<div class="prp-body">` +
|
|
4717
|
-
`<div class="prp-title" style="color:${gT}">已上传「${esc(botName)}」到中心库</div>` +
|
|
4718
|
-
`<div class="prp-desc">同事 <code style="font-family:ui-monospace,monospace;background:#fff;padding:1px 5px;border-radius:5px;border:1px solid var(--border)">larkway sync</code> 一下就能拉到这一份了。</div>` +
|
|
4719
|
-
`<div class="prp-meta">` +
|
|
4720
|
-
`<span style="display:inline-flex;align-items:center;gap:5px">${ICONS.folder} ${esc(repoPath)}${esc(botName)}/</span>` +
|
|
4721
|
-
`<span>·</span>` +
|
|
4722
|
-
`<span style="font-family:ui-monospace,monospace">commit ${esc(commit)}</span>` +
|
|
4723
|
-
`</div>` +
|
|
4724
|
-
`<div class="prp-actions">` +
|
|
4725
|
-
`<button class="btn" id="prp-go-central" type="button" style="background:var(--br-soft);border:1px solid var(--br-edge);color:var(--br-text);font-size:13.5px;padding:8px 15px;border-radius:9px">去「公司中心库」看看 ${arrowSVG}</button>` +
|
|
4726
|
-
`</div>` +
|
|
4727
|
-
`</div>` +
|
|
4728
|
-
`</div>`;
|
|
4729
|
-
|
|
4730
|
-
section.querySelector("#prp-go-central")?.addEventListener("click", () => switchContext("central"));
|
|
4731
|
-
}
|
|
4732
|
-
|
|
4733
|
-
/**
|
|
4734
|
-
* 在 promote-section 内渲染上传失败内联面板(复刻 LkPromoteCard phase==='failed')。
|
|
4735
|
-
* behind=amber 「推不上去」;noperm/other=红「上传失败」。
|
|
4736
|
-
* @param {HTMLElement|null} panel
|
|
4737
|
-
* @param {"behind"|"noperm"|"other"} kind
|
|
4738
|
-
* @param {string|null} errorMsg
|
|
4739
|
-
* @param {function():Promise<void>} onAction 主 CTA 点击回调(同步 / 重试)
|
|
4740
|
-
*/
|
|
4741
|
-
function renderPromoteFailPanel(panel, kind, errorMsg, onAction) {
|
|
4742
|
-
const section = panel?.querySelector(".promote-section");
|
|
4743
|
-
const behind = kind === "behind";
|
|
4744
|
-
|
|
4745
|
-
if (!section) {
|
|
4746
|
-
// 无 panel 降级 toast
|
|
4747
|
-
if (behind) {
|
|
4748
|
-
toast("推不上去 —— 中心库有别人的新改动,得先同步再上传。", "error");
|
|
4749
|
-
} else {
|
|
4750
|
-
toast(`上传失败${errorMsg ? ":" + errorMsg : ""}`, "error");
|
|
4751
|
-
}
|
|
4752
|
-
return;
|
|
4753
|
-
}
|
|
4754
|
-
|
|
4755
|
-
// amber(behind) or red(noperm/other)
|
|
4756
|
-
const sc = behind
|
|
4757
|
-
? { c: LIVE_COLOR.degraded, soft: LIVE_SOFT.degraded, edge: LIVE_EDGE.degraded, text: LIVE_TEXT.degraded }
|
|
4758
|
-
: { c: "#dc2626", soft: "#fef2f2", edge: "#fecaca", text: "#b91c1c" };
|
|
4759
|
-
|
|
4760
|
-
const warnSVG = `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true" style="width:18px;height:18px"><path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"/><path d="M12 9v4M12 17h.01"/></svg>`;
|
|
4761
|
-
const pullSVG = `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true" style="width:15px;height:15px"><path d="M12 3v12M8 11l4 4 4-4M4 21h16"/></svg>`;
|
|
4762
|
-
const refreshSVG = `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true" style="width:15px;height:15px"><path d="M3 12a9 9 0 0 1 15-6.7L21 8M21 3v5h-5M21 12a9 9 0 0 1-15 6.7L3 16M3 21v-5h5"/></svg>`;
|
|
4763
|
-
|
|
4764
|
-
const titleText = behind ? "推不上去 —— 中心库有新改动" : "上传失败";
|
|
4765
|
-
const descText = behind
|
|
4766
|
-
? "别人在你之前也晋升了东西。你得先把那些「同步」拉下来,跟你的合到一起,再上传一次。"
|
|
4767
|
-
: (errorMsg
|
|
4768
|
-
? esc(errorMsg)
|
|
4769
|
-
: "这台机器可能没有往团队仓库写的权限。让工程师给你开一下,或换一台有权限的机器再试。");
|
|
4770
|
-
const ctaLabel = behind ? "先同步,再上传" : "重试";
|
|
4771
|
-
const ctaIcon = behind ? pullSVG : refreshSVG;
|
|
4772
|
-
|
|
4773
|
-
section.innerHTML =
|
|
4774
|
-
`<h4 class="section-title">${ICONS.box} 交给公司统一管理(晋升)</h4>` +
|
|
4775
|
-
`<div class="promote-result-panel" style="background:${sc.soft};border:1px solid ${sc.edge}">` +
|
|
4776
|
-
`<span class="prp-badge" style="border:1px solid ${sc.edge};color:${sc.c}">${warnSVG}</span>` +
|
|
4777
|
-
`<div class="prp-body">` +
|
|
4778
|
-
`<div class="prp-title" style="color:${sc.text}">${titleText}</div>` +
|
|
4779
|
-
`<p class="prp-desc">${descText}</p>` +
|
|
4780
|
-
`<div class="prp-actions">` +
|
|
4781
|
-
`<button class="btn" id="prp-cta" type="button" style="background:var(--br);border:none;color:#fff;font-size:13.5px;padding:8px 15px;border-radius:9px">${ctaIcon} ${ctaLabel}</button>` +
|
|
4782
|
-
`<button class="btn" id="prp-detail" type="button" style="border:1px solid var(--border);background:#fff;color:var(--muted);font-size:13.5px;padding:8px 15px;border-radius:9px">${ICONS.code} 查看详情</button>` +
|
|
4783
|
-
`</div>` +
|
|
4784
|
-
`</div>` +
|
|
4785
|
-
`</div>`;
|
|
4786
|
-
|
|
4787
|
-
section.querySelector("#prp-cta")?.addEventListener("click", () => { void onAction(); });
|
|
4788
|
-
section.querySelector("#prp-detail")?.addEventListener("click", () => {
|
|
4789
|
-
const detail = errorMsg ?? (behind ? "中心库有新改动,需先同步。" : "无权限写入团队仓库。");
|
|
4790
|
-
toast(detail, "error");
|
|
4791
|
-
});
|
|
4792
|
-
}
|
|
4793
3545
|
|
|
4794
3546
|
// ---------------------------------------------------------------------------
|
|
4795
3547
|
// 行为:Bridge 服务控制(B3)
|
|
@@ -4819,7 +3571,6 @@ async function refreshBridgeStatus() {
|
|
|
4819
3571
|
* - restart.status='timeout' → 红「重启异常」+「再试一次」(照 LkServiceRestartChip timeout)
|
|
4820
3572
|
* - pendingNew>0 → amber提示+重启
|
|
4821
3573
|
* - running+ok → 绿chip+重启服务幽灵按钮(照 LkServiceRestartChip serving)
|
|
4822
|
-
* central 只读模式:操作按钮隐藏(本机操作无意义)。
|
|
4823
3574
|
*/
|
|
4824
3575
|
function renderServiceIndicator() {
|
|
4825
3576
|
const container = document.getElementById("bridge-indicator");
|
|
@@ -4827,7 +3578,7 @@ function renderServiceIndicator() {
|
|
|
4827
3578
|
|
|
4828
3579
|
const b = state.bridge;
|
|
4829
3580
|
const running = b?.running ?? false;
|
|
4830
|
-
const readonly =
|
|
3581
|
+
const readonly = false;
|
|
4831
3582
|
|
|
4832
3583
|
if (!b) {
|
|
4833
3584
|
// 未知态(/api/bridge 没拉到):中性灰 chip,不报警也不显绿。
|
|
@@ -4840,7 +3591,7 @@ function renderServiceIndicator() {
|
|
|
4840
3591
|
}
|
|
4841
3592
|
|
|
4842
3593
|
// 状态(running)与按钮(readonly)是两个独立维度:服务没跑/有新助手时**始终**显示
|
|
4843
|
-
//
|
|
3594
|
+
// 报警状态条:服务异常时绝不把状态改成绿色。
|
|
4844
3595
|
const svcBtn = (id, borderColor, icon, label, action) =>
|
|
4845
3596
|
readonly
|
|
4846
3597
|
? ""
|
|
@@ -4866,7 +3617,7 @@ function renderServiceIndicator() {
|
|
|
4866
3617
|
return;
|
|
4867
3618
|
}
|
|
4868
3619
|
|
|
4869
|
-
// ── BL-18
|
|
3620
|
+
// ── BL-18:重启过渡态 ─────────────────────────
|
|
4870
3621
|
if (!readonly) {
|
|
4871
3622
|
const rs = state.restart;
|
|
4872
3623
|
|
|
@@ -4962,7 +3713,7 @@ function renderServiceIndicator() {
|
|
|
4962
3713
|
return;
|
|
4963
3714
|
}
|
|
4964
3715
|
|
|
4965
|
-
//
|
|
3716
|
+
// 绿 chip(serving 正常态)
|
|
4966
3717
|
container.innerHTML =
|
|
4967
3718
|
`<span class="lk-svc-indicator lk-svc-indicator--ok">` +
|
|
4968
3719
|
`<span class="bridge-dot is-running" style="margin-right:6px"></span>` +
|
|
@@ -5230,114 +3981,6 @@ async function doBridgeRestart(triggerBtn = null, successMsg = null) {
|
|
|
5230
3981
|
await refreshBridgeStatus();
|
|
5231
3982
|
}
|
|
5232
3983
|
|
|
5233
|
-
// ---------------------------------------------------------------------------
|
|
5234
|
-
// 行为:上下文切换
|
|
5235
|
-
// ---------------------------------------------------------------------------
|
|
5236
|
-
|
|
5237
|
-
async function switchContext(mode) {
|
|
5238
|
-
if (mode === state.mode) return;
|
|
5239
|
-
|
|
5240
|
-
// ── 切到公司中心库 ──────────────────────────────────────────────────────
|
|
5241
|
-
if (mode === "central") {
|
|
5242
|
-
// 先确认连接状态 —— 未连接也允许进入(进去看的是连接引导卡)。
|
|
5243
|
-
await loadCentralStatus();
|
|
5244
|
-
if (!state.centralConnected) {
|
|
5245
|
-
// 未连接:不调 POST /api/context(后端会 409),只在前端切视图显引导。
|
|
5246
|
-
state.mode = "central";
|
|
5247
|
-
state.selected = null;
|
|
5248
|
-
state.bots = [];
|
|
5249
|
-
renderContextSwitch();
|
|
5250
|
-
renderCentralSourceBar();
|
|
5251
|
-
renderBotList();
|
|
5252
|
-
renderBotDetail(null);
|
|
5253
|
-
renderServiceIndicator();
|
|
5254
|
-
return;
|
|
5255
|
-
}
|
|
5256
|
-
// 已连接:正常切 context
|
|
5257
|
-
const res = await api("POST", "/api/context", { mode });
|
|
5258
|
-
if (!res.ok) {
|
|
5259
|
-
toast(`切换失败:${res.json?.error ?? res.status}`, "error");
|
|
5260
|
-
return;
|
|
5261
|
-
}
|
|
5262
|
-
state.mode = res.json?.mode ?? mode;
|
|
5263
|
-
state.selected = null;
|
|
5264
|
-
renderContextSwitch();
|
|
5265
|
-
renderCentralSourceBar();
|
|
5266
|
-
renderBotDetail(null);
|
|
5267
|
-
await loadBots();
|
|
5268
|
-
// 进中心库后台算一次同步预览,驱动来源条「N 项可更新 / 已是最新」
|
|
5269
|
-
void refreshCentralSyncState();
|
|
5270
|
-
renderServiceIndicator();
|
|
5271
|
-
return;
|
|
5272
|
-
}
|
|
5273
|
-
|
|
5274
|
-
// ── 切回本机 ────────────────────────────────────────────────────────────
|
|
5275
|
-
const res = await api("POST", "/api/context", { mode });
|
|
5276
|
-
if (!res.ok) {
|
|
5277
|
-
toast(`切换失败:${res.json?.error ?? res.status}`, "error");
|
|
5278
|
-
return;
|
|
5279
|
-
}
|
|
5280
|
-
state.mode = res.json?.mode ?? mode;
|
|
5281
|
-
state.selected = null;
|
|
5282
|
-
renderContextSwitch();
|
|
5283
|
-
renderCentralSourceBar();
|
|
5284
|
-
// 清空详情
|
|
5285
|
-
renderBotDetail(null);
|
|
5286
|
-
await loadBots();
|
|
5287
|
-
// 切换上下文后刷新 bridge 指示(central 模式下按钮要隐藏)
|
|
5288
|
-
renderServiceIndicator();
|
|
5289
|
-
}
|
|
5290
|
-
|
|
5291
|
-
/**
|
|
5292
|
-
* 强制进入「已连接的公司中心库」视图并刷新名册 + 来源条。
|
|
5293
|
-
* 用于连接成功后(此时 mode 可能已是 central,switchContext 会 no-op)。
|
|
5294
|
-
*/
|
|
5295
|
-
async function enterCentralConnected() {
|
|
5296
|
-
await loadCentralStatus();
|
|
5297
|
-
if (!state.centralConnected) {
|
|
5298
|
-
// 极端情况下连接态丢失 → 退回引导
|
|
5299
|
-
state.mode = "central";
|
|
5300
|
-
state.selected = null;
|
|
5301
|
-
state.bots = [];
|
|
5302
|
-
renderContextSwitch();
|
|
5303
|
-
renderCentralSourceBar();
|
|
5304
|
-
renderBotList();
|
|
5305
|
-
renderBotDetail(null);
|
|
5306
|
-
return;
|
|
5307
|
-
}
|
|
5308
|
-
// 确保后端上下文也切到 central(幂等;失败不阻断前端展示)
|
|
5309
|
-
await api("POST", "/api/context", { mode: "central" });
|
|
5310
|
-
state.mode = "central";
|
|
5311
|
-
state.selected = null;
|
|
5312
|
-
renderContextSwitch();
|
|
5313
|
-
renderCentralSourceBar();
|
|
5314
|
-
renderBotDetail(null);
|
|
5315
|
-
await loadBots();
|
|
5316
|
-
void refreshCentralSyncState();
|
|
5317
|
-
renderServiceIndicator();
|
|
5318
|
-
}
|
|
5319
|
-
|
|
5320
|
-
/**
|
|
5321
|
-
* 后台算一次同步预览,只为驱动来源条状态(不弹 modal)。
|
|
5322
|
-
* 失败静默(来源条退回「检查更新」)。
|
|
5323
|
-
*/
|
|
5324
|
-
async function refreshCentralSyncState() {
|
|
5325
|
-
if (state.mode !== "central" || !state.centralConnected) return;
|
|
5326
|
-
const res = await api("GET", "/api/central/sync/preview");
|
|
5327
|
-
if (!res.ok) {
|
|
5328
|
-
state.centralSyncState = "unknown";
|
|
5329
|
-
renderCentralSourceBar();
|
|
5330
|
-
return;
|
|
5331
|
-
}
|
|
5332
|
-
// 只数会真正应用的增量(added + updated);removed(本机自建未晋升)本机自管不删,见上。
|
|
5333
|
-
const total =
|
|
5334
|
-
(res.json?.added?.length ?? 0) +
|
|
5335
|
-
(res.json?.updated?.length ?? 0);
|
|
5336
|
-
state.centralUpdateCount = total;
|
|
5337
|
-
state.centralSyncState = total > 0 ? "updates" : "fresh";
|
|
5338
|
-
renderCentralSourceBar();
|
|
5339
|
-
}
|
|
5340
|
-
|
|
5341
3984
|
// ---------------------------------------------------------------------------
|
|
5342
3985
|
// 行为:选中 bot
|
|
5343
3986
|
// ---------------------------------------------------------------------------
|
|
@@ -5360,30 +4003,6 @@ function selectBot(id) {
|
|
|
5360
4003
|
async function loadBots(opts = {}) {
|
|
5361
4004
|
if (!opts.silent) renderBotListLoading();
|
|
5362
4005
|
|
|
5363
|
-
// 中心库:只读名册走 GET /api/central/bots(含 by/updated/commit),不显在线/心跳。
|
|
5364
|
-
if (state.mode === "central") {
|
|
5365
|
-
const ok = await loadCentralBots();
|
|
5366
|
-
if (!ok) {
|
|
5367
|
-
if (!opts.silent) toast("加载中心库名册失败", "error");
|
|
5368
|
-
state.bots = [];
|
|
5369
|
-
} else {
|
|
5370
|
-
// 归一化成名册行需要的 {id,name,description,avatar}(中心库 yaml 现在带飞书头像,用它)
|
|
5371
|
-
state.bots = state.centralBots.map((b) => ({
|
|
5372
|
-
id: b.id,
|
|
5373
|
-
name: b.name ?? b.id,
|
|
5374
|
-
description: b.desc ?? "",
|
|
5375
|
-
avatar: b.avatar ?? null,
|
|
5376
|
-
backend: b.backend || LK_BACKEND_DEFAULT,
|
|
5377
|
-
}));
|
|
5378
|
-
// 中心库头像并入 state.avatars,名册行 + 详情区都能用(与本机分支一致)
|
|
5379
|
-
for (const b of state.centralBots) {
|
|
5380
|
-
if (b && typeof b.id === "string" && b.avatar) state.avatars[b.id] = b.avatar;
|
|
5381
|
-
}
|
|
5382
|
-
}
|
|
5383
|
-
renderBotList();
|
|
5384
|
-
return;
|
|
5385
|
-
}
|
|
5386
|
-
|
|
5387
4006
|
const res = await api("GET", "/api/bots");
|
|
5388
4007
|
if (!res.ok) {
|
|
5389
4008
|
if (!opts.silent) toast(`加载助手列表失败:${res.json?.error ?? res.status}`, "error");
|
|
@@ -5397,91 +4016,6 @@ async function loadBots(opts = {}) {
|
|
|
5397
4016
|
renderBotList();
|
|
5398
4017
|
}
|
|
5399
4018
|
|
|
5400
|
-
// ---------------------------------------------------------------------------
|
|
5401
|
-
// 行为:从中心同步(先 dryRun,再二次确认)
|
|
5402
|
-
// ---------------------------------------------------------------------------
|
|
5403
|
-
|
|
5404
|
-
async function doSync() {
|
|
5405
|
-
const btn = document.getElementById("btn-sync");
|
|
5406
|
-
const restore = btnLoading(btn, "拉取预览中…");
|
|
5407
|
-
const dryRes = await api("POST", "/api/sync", { dryRun: true });
|
|
5408
|
-
restore();
|
|
5409
|
-
if (!dryRes.ok) {
|
|
5410
|
-
toast(`同步预览失败:${dryRes.json?.error ?? dryRes.status}`, "error");
|
|
5411
|
-
return;
|
|
5412
|
-
}
|
|
5413
|
-
const plan = dryRes.json?.plan ?? { added: [], updated: [], removed: [], unchanged: [] };
|
|
5414
|
-
showSyncModal(plan);
|
|
5415
|
-
}
|
|
5416
|
-
|
|
5417
|
-
function showSyncModal(plan) {
|
|
5418
|
-
const backdrop = document.getElementById("modal-backdrop");
|
|
5419
|
-
const body = document.getElementById("modal-body");
|
|
5420
|
-
const footer = document.getElementById("modal-footer");
|
|
5421
|
-
if (!backdrop || !body || !footer) return;
|
|
5422
|
-
|
|
5423
|
-
const rows = [
|
|
5424
|
-
{ label: "新增", items: plan.added, cls: "ok" },
|
|
5425
|
-
{ label: "更新", items: plan.updated, cls: "warn" },
|
|
5426
|
-
{ label: "本机独有(中心库没有,默认保留)", items: plan.removed, cls: "dim" },
|
|
5427
|
-
{ label: "无变化", items: plan.unchanged, cls: "dim" },
|
|
5428
|
-
];
|
|
5429
|
-
|
|
5430
|
-
let html = "";
|
|
5431
|
-
for (const row of rows) {
|
|
5432
|
-
if (!Array.isArray(row.items) || row.items.length === 0) continue;
|
|
5433
|
-
html += `<div class="sync-group"><span class="sync-label sync-${row.cls}">${esc(row.label)}(${row.items.length})</span>`;
|
|
5434
|
-
html += `<ul class="sync-list">${row.items.map((id) => `<li>${esc(id)}</li>`).join("")}</ul></div>`;
|
|
5435
|
-
}
|
|
5436
|
-
if (!html) html = `<p class="dim">没有变化,本机已与公司中心库一致。</p>`;
|
|
5437
|
-
|
|
5438
|
-
body.innerHTML = html;
|
|
5439
|
-
footer.innerHTML = `
|
|
5440
|
-
<label class="prune-label">
|
|
5441
|
-
<input type="checkbox" id="chk-prune" />
|
|
5442
|
-
同时删除本机独有的助手(prune)
|
|
5443
|
-
</label>
|
|
5444
|
-
<div class="modal-btns">
|
|
5445
|
-
<button class="btn" id="modal-cancel" type="button">取消</button>
|
|
5446
|
-
<button class="btn btn-primary" id="modal-confirm" type="button">确认拉取</button>
|
|
5447
|
-
</div>
|
|
5448
|
-
`;
|
|
5449
|
-
|
|
5450
|
-
backdrop.hidden = false;
|
|
5451
|
-
|
|
5452
|
-
document.getElementById("modal-cancel")?.addEventListener("click", () => {
|
|
5453
|
-
backdrop.hidden = true;
|
|
5454
|
-
});
|
|
5455
|
-
|
|
5456
|
-
document.getElementById("modal-confirm")?.addEventListener("click", async () => {
|
|
5457
|
-
const prune = document.getElementById("chk-prune")?.checked ?? false;
|
|
5458
|
-
const confirmBtn = document.getElementById("modal-confirm");
|
|
5459
|
-
const restore = btnLoading(confirmBtn, "拉取中…");
|
|
5460
|
-
const res = await api("POST", "/api/sync", { dryRun: false, prune });
|
|
5461
|
-
restore();
|
|
5462
|
-
backdrop.hidden = true;
|
|
5463
|
-
if (!res.ok) {
|
|
5464
|
-
toast(`同步失败:${res.json?.error ?? res.status}`, "error");
|
|
5465
|
-
return;
|
|
5466
|
-
}
|
|
5467
|
-
const result = res.json?.result ?? {};
|
|
5468
|
-
const parts = [];
|
|
5469
|
-
if ((result.applied ?? []).length) parts.push(`应用 ${result.applied.length}`);
|
|
5470
|
-
if ((result.pruned ?? []).length) parts.push(`删除 ${result.pruned.length}`);
|
|
5471
|
-
if ((result.skipped ?? []).length) parts.push(`跳过 ${result.skipped.length}`);
|
|
5472
|
-
toast(`同步完成。${parts.join("、") || "无变化"}`, "ok");
|
|
5473
|
-
if ((res.json?.warnings ?? []).length) {
|
|
5474
|
-
for (const w of res.json.warnings) console.warn("[larkway sync]", w);
|
|
5475
|
-
}
|
|
5476
|
-
await loadBots();
|
|
5477
|
-
// 如果当前选中 bot 已被删除,清空详情
|
|
5478
|
-
if (state.selected && !state.bots.find((b) => b.id === state.selected)) {
|
|
5479
|
-
state.selected = null;
|
|
5480
|
-
renderBotDetail(null);
|
|
5481
|
-
}
|
|
5482
|
-
});
|
|
5483
|
-
}
|
|
5484
|
-
|
|
5485
4019
|
// ---------------------------------------------------------------------------
|
|
5486
4020
|
// 行为:添加新助手(页面内扫码开通)
|
|
5487
4021
|
//
|
|
@@ -5749,7 +4283,7 @@ function renderOnboardNameForm(prefill, sessionId) {
|
|
|
5749
4283
|
`<div class="lk-bk-card" id="ob2-bk-card" style="margin:0 0 18px">` +
|
|
5750
4284
|
`<h4 class="lk-bk-card-title" style="font-size:14.5px">${ICONS.box} 用哪个底座` +
|
|
5751
4285
|
`<span style="font-weight:400;color:var(--faint);font-size:13px;margin-left:4px">默认 Codex</span></h4>` +
|
|
5752
|
-
lkBackendSelectHTML(LK_BACKEND_DEFAULT,
|
|
4286
|
+
lkBackendSelectHTML(LK_BACKEND_DEFAULT, "ob2-bk") +
|
|
5753
4287
|
`</div>`;
|
|
5754
4288
|
|
|
5755
4289
|
modal.innerHTML =
|
|
@@ -5952,23 +4486,11 @@ function renderOnboardError(msg) {
|
|
|
5952
4486
|
// ---------------------------------------------------------------------------
|
|
5953
4487
|
|
|
5954
4488
|
function wireEvents() {
|
|
5955
|
-
// 上下文切换
|
|
5956
|
-
for (const btn of document.querySelectorAll(".ctx-btn")) {
|
|
5957
|
-
btn.addEventListener("click", () => switchContext(btn.dataset.mode));
|
|
5958
|
-
}
|
|
5959
|
-
|
|
5960
4489
|
// 刷新列表
|
|
5961
4490
|
document.getElementById("btn-refresh")?.addEventListener("click", () => loadBots());
|
|
5962
4491
|
|
|
5963
|
-
// 从中心同步
|
|
5964
|
-
document.getElementById("btn-sync")?.addEventListener("click", () => doSync());
|
|
5965
|
-
|
|
5966
4492
|
// 添加新助手:页面内扫码开通(POST /api/onboard/start → 轮询 → 落盘)
|
|
5967
4493
|
document.getElementById("btn-add")?.addEventListener("click", () => {
|
|
5968
|
-
if (state.mode === "central") {
|
|
5969
|
-
toast("「公司中心库」是只读的,新建助手请先切到「本机」。", "warn");
|
|
5970
|
-
return;
|
|
5971
|
-
}
|
|
5972
4494
|
openOnboardModal();
|
|
5973
4495
|
});
|
|
5974
4496
|
|
|
@@ -5979,13 +4501,6 @@ function wireEvents() {
|
|
|
5979
4501
|
}
|
|
5980
4502
|
});
|
|
5981
4503
|
|
|
5982
|
-
// 连接公司中心库 modal 背景点击关闭(connecting 态正在测网络,不打断)
|
|
5983
|
-
document.getElementById("connect-backdrop")?.addEventListener("click", (e) => {
|
|
5984
|
-
if (e.target !== e.currentTarget) return;
|
|
5985
|
-
if (document.getElementById("connect-modal")?.querySelector(".spinner-lg")) return;
|
|
5986
|
-
closeConnectFlow();
|
|
5987
|
-
});
|
|
5988
|
-
|
|
5989
4504
|
// 添加新助手 modal 背景点击 → 走取消(中止后端 + 清定时器)
|
|
5990
4505
|
document.getElementById("onboard-backdrop")?.addEventListener("click", (e) => {
|
|
5991
4506
|
if (e.target === e.currentTarget) {
|
|
@@ -5998,7 +4513,7 @@ function wireEvents() {
|
|
|
5998
4513
|
await doBridgeRestart(e.currentTarget);
|
|
5999
4514
|
});
|
|
6000
4515
|
|
|
6001
|
-
// Esc 关闭 modal
|
|
4516
|
+
// Esc 关闭 modal(同步预览直接关;添加新助手走取消清理)
|
|
6002
4517
|
document.addEventListener("keydown", (e) => {
|
|
6003
4518
|
if (e.key !== "Escape") return;
|
|
6004
4519
|
const ob = document.getElementById("onboard-backdrop");
|
|
@@ -6006,12 +4521,6 @@ function wireEvents() {
|
|
|
6006
4521
|
closeOnboardModal();
|
|
6007
4522
|
return;
|
|
6008
4523
|
}
|
|
6009
|
-
const cb = document.getElementById("connect-backdrop");
|
|
6010
|
-
if (cb && !cb.hidden) {
|
|
6011
|
-
if (document.getElementById("connect-modal")?.querySelector(".spinner-lg")) return;
|
|
6012
|
-
closeConnectFlow();
|
|
6013
|
-
return;
|
|
6014
|
-
}
|
|
6015
4524
|
const bd = document.getElementById("modal-backdrop");
|
|
6016
4525
|
if (bd && !bd.hidden) bd.hidden = true;
|
|
6017
4526
|
});
|
|
@@ -6024,21 +4533,20 @@ async function boot() {
|
|
|
6024
4533
|
|
|
6025
4534
|
wireEvents();
|
|
6026
4535
|
|
|
6027
|
-
// 拉初始上下文状态(
|
|
4536
|
+
// 拉初始上下文状态(version + hostname)
|
|
6028
4537
|
const ctxRes = await api("GET", "/api/context");
|
|
6029
4538
|
if (ctxRes.ok) {
|
|
6030
|
-
state.mode =
|
|
6031
|
-
state.centralAvailable = ctxRes.json?.centralAvailable ?? false;
|
|
4539
|
+
state.mode = "local";
|
|
6032
4540
|
const ver = ctxRes.json?.version;
|
|
6033
4541
|
const verEl = document.getElementById("brand-ver");
|
|
6034
4542
|
if (verEl && ver) verEl.textContent = "v" + ver;
|
|
4543
|
+
// 填充本机 hostname badge(浏览器侧地址,本机即 127.0.0.1)
|
|
4544
|
+
const hostEl = document.getElementById("ctx-host");
|
|
4545
|
+
const host = location.hostname || "";
|
|
4546
|
+
if (hostEl && host) hostEl.textContent = " · " + host;
|
|
6035
4547
|
}
|
|
6036
4548
|
|
|
6037
|
-
// 拉中心库连接状态(driver:连接引导 vs 已连接名册 + 来源条)
|
|
6038
|
-
await loadCentralStatus();
|
|
6039
|
-
|
|
6040
4549
|
renderContextSwitch();
|
|
6041
|
-
renderCentralSourceBar();
|
|
6042
4550
|
|
|
6043
4551
|
// 拉 backend 注册表(驱动底座选择就绪态;失败静默)
|
|
6044
4552
|
void loadBackends();
|
|
@@ -6046,11 +4554,6 @@ async function boot() {
|
|
|
6046
4554
|
// 拉 bot 列表(先于首次状态轮询,确保左侧条目存在好让圆点落上去)
|
|
6047
4555
|
await loadBots();
|
|
6048
4556
|
|
|
6049
|
-
// 若初始就处于已连接的中心库上下文,后台算同步预览驱动来源条
|
|
6050
|
-
if (state.mode === "central" && state.centralConnected) {
|
|
6051
|
-
void refreshCentralSyncState();
|
|
6052
|
-
}
|
|
6053
|
-
|
|
6054
4557
|
// 拉 bridge 服务状态并渲染顶栏指示
|
|
6055
4558
|
await refreshBridgeStatus();
|
|
6056
4559
|
|