@jesonliu/lark-claudecode-bridge 1.4.2 → 2.0.0
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/assets/web/css/main.css +766 -574
- package/assets/web/index.html +37 -17
- package/assets/web/js/auth.js +217 -0
- package/assets/web/js/bootstrap.js +36 -34
- package/assets/web/js/core.js +4 -3
- package/assets/web/js/main.js +79 -20
- package/assets/web/js/markdown.js +64 -0
- package/assets/web/js/pages/apps.js +338 -251
- package/assets/web/js/pages/chats.js +446 -0
- package/assets/web/js/pages/claude.js +243 -180
- package/assets/web/js/pages/mcp.js +59 -43
- package/assets/web/js/pages/memory.js +39 -43
- package/assets/web/js/pages/overview.js +430 -101
- package/assets/web/js/pages/permissions.js +40 -32
- package/assets/web/js/pages/plugins.js +51 -37
- package/assets/web/js/pages/skills.js +68 -92
- package/assets/web/js/pages/slash.js +31 -23
- package/assets/web/js/pages/workspaces.js +114 -73
- package/assets/web/js/theme.js +21 -18
- package/assets/web/js/ui.js +135 -32
- package/dist/bin/lcb.js +15 -0
- package/dist/bin/lcb.js.map +1 -1
- package/dist/gateway/feishu-gateway.d.ts +11 -0
- package/dist/gateway/feishu-gateway.js +5 -0
- package/dist/gateway/feishu-gateway.js.map +1 -1
- package/dist/gateway/gateway-registry.d.ts +16 -0
- package/dist/gateway/gateway-registry.js +10 -0
- package/dist/gateway/gateway-registry.js.map +1 -0
- package/dist/index.js +54 -41
- package/dist/index.js.map +1 -1
- package/dist/memory-installer.d.ts +7 -0
- package/dist/memory-installer.js +22 -5
- package/dist/memory-installer.js.map +1 -1
- package/dist/web/chats-api.d.ts +77 -0
- package/dist/web/chats-api.js +475 -0
- package/dist/web/chats-api.js.map +1 -0
- package/dist/web/logs-api.d.ts +37 -0
- package/dist/web/logs-api.js +141 -0
- package/dist/web/logs-api.js.map +1 -0
- package/dist/web/server.js +125 -6
- package/dist/web/server.js.map +1 -1
- package/dist/web/webauth.d.ts +44 -0
- package/dist/web/webauth.js +177 -0
- package/dist/web/webauth.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// ============ 概览 ============
|
|
2
|
-
//
|
|
2
|
+
// 状态看板:4 张瓦片(桥接器进程 / Claude 认证 / 飞书 CLI / 记忆增强)+ LCB、飞书 CLI
|
|
3
|
+
// 各自独立的版本更新卡 + 机器人与快捷指令 + 日志查看器抽屉。
|
|
4
|
+
// 桥接器启停(宿主进程管理):embedded 页面随进程生死,独立页面经 PID 跨进程操作。
|
|
3
5
|
import { S, $, esc, toast, api, refresh } from '../core.js';
|
|
4
|
-
import { bindDialogKeys, confirmDialog, mountDialog, progressDialog } from '../ui.js';
|
|
6
|
+
import { openDrawer, closeDrawer, bindDialogKeys, confirmDialog, mountDialog, progressDialog, stepsDialog } from '../ui.js';
|
|
5
7
|
// larkNotConfigured 与后端 needsLarkCliConfig 同源(detail 含 not_configured 即「未配置应用」),
|
|
6
8
|
// 判定只有一份、放在弹窗模块里,避免两处漂移
|
|
7
9
|
import { larkNotConfigured, openLarkCliDeviceDialog } from './larkcli-device.js';
|
|
@@ -9,18 +11,29 @@ import { larkNotConfigured, openLarkCliDeviceDialog } from './larkcli-device.js'
|
|
|
9
11
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
10
12
|
const UPD = { checkedAt: 0, data: null };
|
|
11
13
|
const LARKCLI = { checkedAt: 0, data: null }; // 飞书官方 CLI 状态缓存(30s 节流,同 UPD)
|
|
14
|
+
const svg = (inner, s = 14) => `<svg viewBox="0 0 24 24" width="${s}" height="${s}" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${inner}</svg>`;
|
|
15
|
+
const IC = {
|
|
16
|
+
apps: '<path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/>',
|
|
17
|
+
claude: '<circle cx="7.5" cy="15.5" r="5.5"/><path d="m21 2-9.6 9.6"/><path d="m15.5 7.5 3 3L22 7l-3-3"/>',
|
|
18
|
+
zap: '<path d="M13 2 3 14h7l-1 8 10-12h-7l1-8z"/>',
|
|
19
|
+
memory: '<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14a9 3 0 0 0 18 0V5"/><path d="M3 12a9 3 0 0 0 18 0"/>',
|
|
20
|
+
file: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>',
|
|
21
|
+
chats: '<path d="M14 9a2 2 0 0 1-2 2H6l-4 4V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2z"/><path d="M18 9h2a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1"/>',
|
|
22
|
+
copy: '<rect width="13" height="13" x="9" y="9" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/>',
|
|
23
|
+
folder: '<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>',
|
|
24
|
+
};
|
|
12
25
|
|
|
13
26
|
function showMask(inner) {
|
|
14
27
|
let m = $('#brMask');
|
|
15
28
|
if (!m) {
|
|
16
29
|
m = document.createElement('div');
|
|
17
|
-
m.id = 'brMask'; m.className = '
|
|
30
|
+
m.id = 'brMask'; m.className = 'modal-mask';
|
|
31
|
+
m.style.zIndex = '150';
|
|
18
32
|
document.body.appendChild(m);
|
|
19
33
|
}
|
|
20
|
-
m.innerHTML = `<div class="
|
|
34
|
+
m.innerHTML = `<div class="modal dlg" style="text-align:center"><div class="dlg-msg" style="white-space:normal">${inner}</div></div>`;
|
|
21
35
|
m.style.display = 'flex';
|
|
22
36
|
}
|
|
23
|
-
function hideMask() { const m = $('#brMask'); if (m) m.style.display = 'none'; }
|
|
24
37
|
|
|
25
38
|
/** 启停请求的容错:embedded 停止/重启时进程退出可能先于响应到达,网络断开视为已受理 */
|
|
26
39
|
async function bridgeActionSafe(op) {
|
|
@@ -31,20 +44,96 @@ async function bridgeActionSafe(op) {
|
|
|
31
44
|
}
|
|
32
45
|
}
|
|
33
46
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
47
|
+
/**
|
|
48
|
+
* 重启进度分步弹窗(两种运行模式统一)。步骤① LCB 重启(探测新进程上线),步骤② 记忆增强
|
|
49
|
+
* OpenViking(轮询自举状态/健康探活)——两步状态源不同、节奏不同,旧版单句遮罩 + 45s 超时
|
|
50
|
+
* 报错的做法在 OpenViking 冷启动(30-60s+)时必撞「重启超时」误报,整体重做:
|
|
51
|
+
* - LCB 自身恢复只取决于 LCB 本体(服务端已把配置页启动前移、记忆自举后台化)
|
|
52
|
+
* - 记忆增强**永不导致「重启失败」**:未就绪呈现为黄色警示 + 手动开启指引,弹窗可随时关闭
|
|
53
|
+
*/
|
|
54
|
+
async function restartFlow() {
|
|
55
|
+
const embedded = !!S.status?.embedded;
|
|
56
|
+
const oldPid = S.status?.bridge?.pid ?? null;
|
|
57
|
+
const st = [
|
|
58
|
+
{ title: 'LCB 重启中', detail: '正在停止旧进程并拉起新进程…', status: 'running' },
|
|
59
|
+
{ title: '记忆增强(OpenViking)', detail: '等待 LCB 重启完成后检测…', status: 'pending' },
|
|
60
|
+
];
|
|
61
|
+
let stopped = false;
|
|
62
|
+
const dlg = stepsDialog({ title: '重启桥接器', steps: st });
|
|
63
|
+
// 关弹窗 = 后台运行:停轮询,稍后刷新概览状态(重启幂等,流程在桥接器侧继续不受影响)
|
|
64
|
+
dlg.onClose(() => { stopped = true; setTimeout(() => void refresh(), 3000); });
|
|
65
|
+
const paint = (i, patch) => { Object.assign(st[i], patch); dlg.setSteps(st.map((s) => ({ ...s }))); };
|
|
66
|
+
|
|
67
|
+
try { await bridgeActionSafe('restart'); }
|
|
68
|
+
catch (e) { paint(0, { status: 'failed', detail: e.message }); toast(e.message, true); return; }
|
|
69
|
+
|
|
70
|
+
// ---- 步骤①:等 LCB 回归。embedded:本页随进程失联,连续两次 /api/status 成功才算新进程
|
|
71
|
+
//(排除旧进程残影);独立:本页不失联,等 PID 文件换新(bridge.running 且 pid 变化)。
|
|
72
|
+
// 超时不再报「重启失败」退出——如实呈现等待中 + 给手动刷新出口,弹窗保持观测
|
|
36
73
|
const t0 = Date.now();
|
|
37
74
|
await sleep(2500); // 留给旧进程退场:立刻探测可能命中尚未退出的旧 server
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
75
|
+
let streak = false;
|
|
76
|
+
let back = false;
|
|
77
|
+
while (!stopped && Date.now() - t0 < 120_000) {
|
|
78
|
+
const sec = Math.round((Date.now() - t0) / 1000);
|
|
79
|
+
let s = null;
|
|
80
|
+
try { s = await api('GET', '/api/status'); } catch { /* 失联窗口属预期 */ }
|
|
81
|
+
if (s) {
|
|
82
|
+
if (embedded) {
|
|
83
|
+
if (streak) { back = true; break; }
|
|
84
|
+
streak = true;
|
|
85
|
+
await sleep(1000); // 快速二次确认(旧实现同口径)
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (s.bridge?.running && (oldPid == null || s.bridge?.pid !== oldPid)) { back = true; break; }
|
|
89
|
+
} else if (embedded) streak = false;
|
|
90
|
+
paint(0, { detail: `已等 ${sec} 秒…(新进程上线即自动恢复)` });
|
|
45
91
|
await sleep(1500);
|
|
46
92
|
}
|
|
47
|
-
|
|
93
|
+
if (stopped) return;
|
|
94
|
+
if (!back) {
|
|
95
|
+
paint(0, { status: 'failed', detail: '超过 120 秒未见新进程上线——可能端口占用等启动异常,请查看日志' });
|
|
96
|
+
dlg.setHint('<button class="btn sm" id="sdReload">手动刷新页面</button>');
|
|
97
|
+
document.querySelector('#sdReload').onclick = () => location.reload();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
paint(0, { status: 'done', detail: `LCB 已重启(用时 ${Math.round((Date.now() - t0) / 1000)} 秒)` });
|
|
101
|
+
toast('✅ LCB 已重启'); // 弹窗可能已被用户关掉(后台运行),toast 兜一条完成通知
|
|
102
|
+
|
|
103
|
+
// ---- 步骤②:记忆增强 OpenViking。embedded 轮询 /api/memory/status:enabled=false → 跳过;
|
|
104
|
+
// health 探活通过 → 就绪;自举 phase=starting → 实时「已等 N 秒」;phase=failed → 黄色警示
|
|
105
|
+
//(不红叉、不阻断——LCB 本体已正常,只是记忆增强没起来)。独立模式本页拿不到桥接器进程内
|
|
106
|
+
// 的自举报告,以 health 探活为准(新桥接器起来会自动拉起受管服务)。
|
|
107
|
+
paint(1, { status: 'running', detail: '正在检测记忆服务…' });
|
|
108
|
+
const MEM_POLL_MAX_MS = embedded ? 620_000 : 150_000; // embedded 与后端慢启动续等窗口(600s)对齐
|
|
109
|
+
const m0 = Date.now();
|
|
110
|
+
while (!stopped) {
|
|
111
|
+
let ms = null;
|
|
112
|
+
try { ms = await api('GET', '/api/memory/status'); } catch { /* 新进程刚回来时的空窗:静默重试 */ }
|
|
113
|
+
const sec = Math.round((Date.now() - m0) / 1000);
|
|
114
|
+
if (ms) {
|
|
115
|
+
if (ms.memory?.enabled === false) { paint(1, { status: 'skipped', detail: '未启用(config.yaml 的 memory.enabled=false)' }); break; }
|
|
116
|
+
if (ms.health?.healthy) { paint(1, { status: 'done', detail: `记忆服务已就绪:${ms.memory?.endpoint || ''}` }); break; }
|
|
117
|
+
const auto = embedded ? ms.install?.autoStart : undefined;
|
|
118
|
+
if (auto?.phase === 'failed') {
|
|
119
|
+
paint(1, { status: 'warn', detail: `${auto.detail}${auto.hint ? `。${auto.hint}` : ''}` });
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
if (auto?.phase === 'starting') {
|
|
123
|
+
const waited = auto.at ? Math.max(0, Math.round((Date.now() - new Date(auto.at).getTime()) / 1000)) : sec;
|
|
124
|
+
paint(1, { detail: `${auto.detail}(已等 ${waited} 秒)` });
|
|
125
|
+
} else {
|
|
126
|
+
paint(1, { detail: embedded ? `检测记忆服务状态…(${sec} 秒)` : `等待桥接器拉起记忆服务…(已等 ${sec} 秒)` });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (Date.now() - m0 > MEM_POLL_MAX_MS) {
|
|
130
|
+
paint(1, { status: 'warn', detail: '长时间未见就绪,请到「记忆增强」页查看服务状态或手动启动' });
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
await sleep(2000);
|
|
134
|
+
}
|
|
135
|
+
dlg.setHint('本弹窗可随时关闭(后台运行),重启与记忆启动不受影响');
|
|
136
|
+
void refresh();
|
|
48
137
|
}
|
|
49
138
|
|
|
50
139
|
async function startFlow() {
|
|
@@ -77,20 +166,6 @@ async function stopFlow() {
|
|
|
77
166
|
} catch (e) { toast(e.message, true); }
|
|
78
167
|
}
|
|
79
168
|
|
|
80
|
-
async function restartFlow() {
|
|
81
|
-
if (S.status?.embedded) {
|
|
82
|
-
showMask('<div class="br-spin"></div>正在重启桥接器,页面将自动恢复…');
|
|
83
|
-
try { await bridgeActionSafe('restart'); } catch (e) { hideMask(); toast(e.message, true); return; }
|
|
84
|
-
try { await waitServerBack(); hideMask(); toast('✅ 桥接器已重启'); await refresh(); }
|
|
85
|
-
catch (e) { hideMask(); toast(e.message, true); }
|
|
86
|
-
} else {
|
|
87
|
-
try { const r = await bridgeActionSafe('restart'); toast(r.message || '正在重启…'); }
|
|
88
|
-
catch (e) { toast(e.message, true); return; }
|
|
89
|
-
setTimeout(() => refresh(), 4000);
|
|
90
|
-
setTimeout(() => refresh(), 10000);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
169
|
function renderUpdateResult() {
|
|
95
170
|
const el = $('#updLatest');
|
|
96
171
|
const r = UPD.data;
|
|
@@ -102,7 +177,7 @@ function renderUpdateResult() {
|
|
|
102
177
|
const area = $('#updArea');
|
|
103
178
|
if (!area) return;
|
|
104
179
|
if (r.hasUpdate && r.mode === 'global') {
|
|
105
|
-
area.innerHTML =
|
|
180
|
+
area.innerHTML = `<button class="btn sm primary" id="btnUpdRun">更新到 v${esc(r.latest)}</button>`;
|
|
106
181
|
$('#btnUpdRun').onclick = runUpdateFlow;
|
|
107
182
|
} else if (r.hasUpdate) {
|
|
108
183
|
area.innerHTML = '<span class="hint">当前非 npm 全局安装运行,请手动更新</span>';
|
|
@@ -128,7 +203,7 @@ async function runUpdateFlow() {
|
|
|
128
203
|
const btn = $('#btnUpdRun');
|
|
129
204
|
if (btn) { btn.disabled = true; btn.textContent = '更新中…'; }
|
|
130
205
|
try {
|
|
131
|
-
// 只更新桥接器自身:飞书 CLI
|
|
206
|
+
// 只更新桥接器自身:飞书 CLI 的更新在它自己那张卡点「更新」,两条更新各自独立
|
|
132
207
|
await api('POST', '/api/update/run');
|
|
133
208
|
UPD.checkedAt = 0; LARKCLI.checkedAt = 0;
|
|
134
209
|
if (running) await restartFlow();
|
|
@@ -140,7 +215,7 @@ async function runUpdateFlow() {
|
|
|
140
215
|
}
|
|
141
216
|
}
|
|
142
217
|
|
|
143
|
-
/**
|
|
218
|
+
/** 纯函数(可测):飞书 CLI 状态 HTML(不含操作按钮,按钮由 renderLarkCliResult 按需挂) */
|
|
144
219
|
export function larkCliStatHtml(r) {
|
|
145
220
|
if (!r) return '<span class="hint">未检查</span>';
|
|
146
221
|
if (!r.installed) return '<span class="tag off">未安装</span>';
|
|
@@ -162,7 +237,7 @@ export function larkCliStatHtml(r) {
|
|
|
162
237
|
}
|
|
163
238
|
|
|
164
239
|
/**
|
|
165
|
-
*
|
|
240
|
+
* 纯函数(可测):飞书 CLI 主入口按钮的文案——按「点开后会先走哪一步」命名,
|
|
166
241
|
* 不出现「按钮写扫码授权、点开弹窗却是配置二维码」的错位。
|
|
167
242
|
* 未配置应用时弹窗从 config 阶段起步(见 openDeviceDialog 与 larkcli-device.js),故叫「配置应用」。
|
|
168
243
|
* 探测不出(unknown)**不**算未配置:误判会把已配好应用的用户反复推去重配,按「扫码授权」处理。
|
|
@@ -188,11 +263,12 @@ function openDeviceDialog() {
|
|
|
188
263
|
});
|
|
189
264
|
}
|
|
190
265
|
|
|
266
|
+
/** 飞书 CLI 卡内的动作按钮区(安装 / 更新 / 授权 / SKILL) */
|
|
191
267
|
function renderLarkCliResult() {
|
|
192
268
|
const el = $('#larkCliStat');
|
|
193
269
|
const r = LARKCLI.data;
|
|
194
270
|
if (!el) return;
|
|
195
|
-
el.innerHTML = `${larkCliStatHtml(r)} <span id="larkCliArea"></span>`;
|
|
271
|
+
el.innerHTML = `${larkCliStatHtml(r)} <span id="larkCliArea" style="display:inline-flex;gap:6px;margin-left:6px;vertical-align:1px"></span>`;
|
|
196
272
|
const area = $('#larkCliArea');
|
|
197
273
|
if (!area || !r) return;
|
|
198
274
|
const add = (label, op) => {
|
|
@@ -201,29 +277,49 @@ function renderLarkCliResult() {
|
|
|
201
277
|
b.textContent = label;
|
|
202
278
|
b.onclick = () => (op === 'auth-device' ? openDeviceDialog() : larkCliActionFlow(op));
|
|
203
279
|
area.appendChild(b);
|
|
204
|
-
area.appendChild(document.createTextNode(' '));
|
|
205
280
|
};
|
|
206
281
|
if (!r.installed) { add('安装', 'install'); return; }
|
|
207
282
|
if (r.hasUpdate) add('更新', 'update');
|
|
208
283
|
// 入口常驻,且已授权也给(token 会过期,随时可重走一遍 device flow 刷新)。
|
|
209
284
|
// 走**页面内二维码**(主路径,无桌面环境同样可用);终端窗口降级为弹窗里的备选。
|
|
210
|
-
// 文案跟着「弹窗会先走哪一步」走(见 larkCliPrimaryLabel):未配置应用时弹窗从配置阶段
|
|
211
|
-
// 起步、先出配置二维码,按钮就叫「配置应用」;已配置才是「扫码授权」。
|
|
212
|
-
// **入口数量不变**——三种状态都是同一个弹窗(op 一律 auth-device),只是弹窗按前置条件
|
|
213
|
-
// 决定先出哪种二维码(见 openDeviceDialog 与 larkcli-device.js)。
|
|
214
285
|
add(larkCliPrimaryLabel(r), 'auth-device');
|
|
215
286
|
if (r.skill && !r.skill.installed) add('装 SKILL', 'skill');
|
|
287
|
+
paintCliTile(r); // 30s 缓存路径(render 时直接重放)同样回填瓦片
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** 飞书 CLI 瓦片回填:检测结果同时刷新上方瓦片(此前只回填更新卡,瓦片永远停在「检测中」) */
|
|
291
|
+
function paintCliTile(r, failed = false) {
|
|
292
|
+
const top = $('#tileCliTop');
|
|
293
|
+
if (!top) return; // 页面已切走
|
|
294
|
+
let dot = 'off';
|
|
295
|
+
let dstate = '未安装';
|
|
296
|
+
let subText = '未安装——点「安装」一键装好并引导授权';
|
|
297
|
+
if (failed) {
|
|
298
|
+
dot = 'err';
|
|
299
|
+
dstate = '检测失败';
|
|
300
|
+
subText = 'CLI 状态探测异常,点「检查更新」重试';
|
|
301
|
+
} else if (r?.installed) {
|
|
302
|
+
dot = r.auth?.state === 'authorized' ? 'ok' : 'warn';
|
|
303
|
+
dstate = r.auth?.state === 'authorized' ? '已授权' : r.auth?.state === 'expired' ? '授权已过期' : '未授权';
|
|
304
|
+
subText = `v${r.version || '?'} · SKILL ${r.skill?.installed ? '已装' : '未装'}${r.hasUpdate ? ' · 可更新' : ''}`;
|
|
305
|
+
}
|
|
306
|
+
top.innerHTML = `<span class="dot ${dot}"></span><span class="tile-state" style="color:var(--${dot}-tx)">${dstate}</span><span class="tile-ico">${svg(IC.zap, 16)}</span>`;
|
|
307
|
+
const sub = $('#tileCliSub');
|
|
308
|
+
if (sub) { sub.textContent = subText; sub.title = subText; }
|
|
309
|
+
const btn = $('#tileCliAuth');
|
|
310
|
+
if (btn) btn.textContent = r?.installed ? (r.auth?.state === 'authorized' ? '重新授权' : '去授权') : '安装';
|
|
216
311
|
}
|
|
217
312
|
|
|
218
313
|
async function doCheckLarkCli() {
|
|
219
314
|
const el = $('#larkCliStat'); if (!el) return;
|
|
220
|
-
el.innerHTML = '<span class="hint">检测中…</span>';
|
|
221
315
|
try {
|
|
222
316
|
LARKCLI.data = await api('GET', '/api/lark-cli?check=1');
|
|
223
317
|
LARKCLI.checkedAt = Date.now();
|
|
224
318
|
renderLarkCliResult();
|
|
319
|
+
paintCliTile(LARKCLI.data);
|
|
225
320
|
} catch (e) {
|
|
226
321
|
el.innerHTML = `<span class="hint">检测失败:${esc(e.message)}</span>`;
|
|
322
|
+
paintCliTile(null, true);
|
|
227
323
|
}
|
|
228
324
|
}
|
|
229
325
|
|
|
@@ -347,7 +443,7 @@ async function larkCliActionFlow(op = 'install') {
|
|
|
347
443
|
},
|
|
348
444
|
config: {
|
|
349
445
|
what: '配置应用',
|
|
350
|
-
//
|
|
446
|
+
// 这是**备选**路径:主路径是弹窗里的配置二维码(见 larkcli-device.js 的 config 阶段),
|
|
351
447
|
// 只有用户在弹窗里显式点「改用终端窗口」才会走到这里
|
|
352
448
|
msg: '将打开一个终端窗口执行 <code>lark-cli config init --new</code>:按窗口打印的链接在浏览器完成飞书应用创建,'
|
|
353
449
|
+ '完成后会自动进入授权登录。'
|
|
@@ -405,9 +501,9 @@ function paintAutostart(el, r) {
|
|
|
405
501
|
const tags = r.enabled
|
|
406
502
|
? '<span class="tag ok">已开启</span>'
|
|
407
503
|
: '<span class="tag off">未开启</span>';
|
|
504
|
+
// 只留开关 + 状态 tag + 过期警示;各平台的「登录启动管理方式」说明文案一律不显示(主人拍板)
|
|
408
505
|
const stale = r.stale ? ' <span class="tag warn">启动命令已过期,关闭再开启可更新</span>' : '';
|
|
409
|
-
|
|
410
|
-
el.innerHTML = `<span class="switch-row tight"><label class="switch"><input type="checkbox" id="asToggle" ${r.enabled ? 'checked' : ''}><span class="track"></span></label>${tags}${stale}${hint}</span>`;
|
|
506
|
+
el.innerHTML = `<span class="switch-row" style="margin:0"><label class="switch"><input type="checkbox" id="asToggle" ${r.enabled ? 'checked' : ''}><span class="track"></span></label>${tags}${stale}</span>`;
|
|
411
507
|
$('#asToggle').onchange = async (e) => {
|
|
412
508
|
const on = e.target.checked;
|
|
413
509
|
try {
|
|
@@ -421,7 +517,7 @@ function paintAutostart(el, r) {
|
|
|
421
517
|
};
|
|
422
518
|
}
|
|
423
519
|
|
|
424
|
-
/**
|
|
520
|
+
/** LCB 更新卡内「开机自启」行:首次渲染拉状态;开关点击即生效(低风险,即时保存) */
|
|
425
521
|
async function renderAutostart() {
|
|
426
522
|
const el = $('#asStat');
|
|
427
523
|
if (!el) return;
|
|
@@ -439,86 +535,319 @@ async function renderAutostart() {
|
|
|
439
535
|
paintAutostart(el, r);
|
|
440
536
|
}
|
|
441
537
|
|
|
538
|
+
// ============ 运行日志查看器(快捷时间切换 + 截止天优先 + 关键字全范围检索) ============
|
|
539
|
+
async function logViewer() {
|
|
540
|
+
const pad2 = (n) => String(n).padStart(2, '0');
|
|
541
|
+
const dpart = (d) => `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
|
542
|
+
const tpart = (d) => `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
|
|
543
|
+
const at = (dayOffset, h, m) => { const d = new Date(); d.setDate(d.getDate() + dayOffset); d.setHours(h, m, 0, 0); return d; };
|
|
544
|
+
/** 快捷档 → [开始, 结束](Date) */
|
|
545
|
+
const QUICKS = {
|
|
546
|
+
today: () => [at(0, 0, 0), at(0, 23, 59)],
|
|
547
|
+
yesterday: () => [at(-1, 0, 0), at(-1, 23, 59)],
|
|
548
|
+
week: () => [at(-6, 0, 0), at(0, 23, 59)],
|
|
549
|
+
};
|
|
550
|
+
openDrawer({
|
|
551
|
+
title: '运行日志',
|
|
552
|
+
bodyHtml: `
|
|
553
|
+
<div class="subtabs" id="lgChips" style="margin-bottom:10px">
|
|
554
|
+
<button data-q="today" class="active">今天</button>
|
|
555
|
+
<button data-q="yesterday">昨天</button>
|
|
556
|
+
<button data-q="week">最近7天</button>
|
|
557
|
+
<button data-q="custom">自定义</button>
|
|
558
|
+
</div>
|
|
559
|
+
<div id="lgCustom" style="display:none;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:10px">
|
|
560
|
+
<input type="datetime-local" id="lgFrom" style="width:210px;padding:5px 10px;font-size:12.5px">
|
|
561
|
+
<span class="mut" style="font-size:12.5px">至</span>
|
|
562
|
+
<input type="datetime-local" id="lgTo" style="width:210px;padding:5px 10px;font-size:12.5px">
|
|
563
|
+
<button class="btn sm primary" id="lgQuery">查询</button>
|
|
564
|
+
</div>
|
|
565
|
+
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:10px">
|
|
566
|
+
<label style="display:inline-flex;align-items:center;gap:6px;font-size:12.5px" title="勾选后显示所选时间范围内的全部日志;默认只显示截止那天的日志"><input type="checkbox" id="lgAll"> 显示整个范围</label>
|
|
567
|
+
<select id="lgLevel" style="width:auto;padding:5px 10px;font-size:12.5px">
|
|
568
|
+
<option value="all">全部级别</option>
|
|
569
|
+
<option value="INFO">Info</option>
|
|
570
|
+
<option value="WARN">Warn</option>
|
|
571
|
+
<option value="ERROR">Error</option>
|
|
572
|
+
</select>
|
|
573
|
+
<input type="search" id="lgQ" placeholder="关键字(在时间范围内检索)…" style="width:200px;padding:5px 10px;font-size:12.5px">
|
|
574
|
+
<label style="display:inline-flex;align-items:center;gap:6px;font-size:12.5px"><input type="checkbox" id="lgAuto" checked> 自动滚动</label>
|
|
575
|
+
<span style="flex:1"></span>
|
|
576
|
+
<button class="btn sm" id="lgRefresh">↻ 刷新</button>
|
|
577
|
+
<button class="btn sm" id="lgCopy">复制</button>
|
|
578
|
+
<button class="btn sm" id="lgDownload">下载</button>
|
|
579
|
+
</div>
|
|
580
|
+
<pre id="lgView" style="background:#0d1117;color:#c9d4e6;border:1px solid var(--border);border-radius:10px;padding:14px 16px;font-family:var(--mono);font-size:12px;line-height:1.8;overflow:auto;max-height:calc(100vh - 340px);white-space:pre-wrap;word-break:break-all;margin:0">加载中…</pre>
|
|
581
|
+
<div class="mini-note" style="margin-top:8px" id="lgNote">默认只显示截止那天的日志;输入关键字或勾选「显示整个范围」时才检索所选范围(最多 7 天)。日志按天落盘于 <code>~/.lark-claudecode-bridge/logs/</code>,14 天前自动清理。</div>`,
|
|
582
|
+
footHtml: `<span class="sp"></span><button class="btn" id="lgClose">关闭</button>`,
|
|
583
|
+
});
|
|
584
|
+
$('#lgClose').onclick = closeDrawer;
|
|
585
|
+
const lvlColor = (l) => (l === 'ERROR' ? '#ff8f94' : l === 'WARN' ? '#ffb45e' : '#7d8aa5');
|
|
586
|
+
/** 行首日期(YYYY-MM-DD)/ 时刻,解析失败返回 null(该行只受关键字/级别过滤) */
|
|
587
|
+
const lineDay = (t) => t.match(/^(\d{4}-\d{2}-\d{2})/)?.[1] || null;
|
|
588
|
+
const lineTime = (t) => {
|
|
589
|
+
const m = t.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})/);
|
|
590
|
+
return m ? new Date(`${m[1]}T${m[2]}`).getTime() : null;
|
|
591
|
+
};
|
|
592
|
+
let allLines = []; // 后端返回的时间范围内的全部行(按时间序)
|
|
593
|
+
let rangeFrom = null; // Date
|
|
594
|
+
let rangeTo = null; // Date
|
|
595
|
+
|
|
596
|
+
/** 视图行 = 时间窗(默认只显截止天)∩ 级别 ∩ 关键字 */
|
|
597
|
+
const viewLines = () => {
|
|
598
|
+
const lv = $('#lgLevel').value;
|
|
599
|
+
const q = $('#lgQ').value.trim().toLowerCase();
|
|
600
|
+
const wantAll = $('#lgAll').checked || !!q;
|
|
601
|
+
const fromMs = rangeFrom?.getTime();
|
|
602
|
+
const toMs = rangeTo?.getTime();
|
|
603
|
+
const toDay = rangeTo ? dpart(rangeTo) : '';
|
|
604
|
+
const timeOk = (t) => {
|
|
605
|
+
if (wantAll) {
|
|
606
|
+
if (fromMs != null) { const ts = lineTime(t); if (ts != null && ts < fromMs) return false; }
|
|
607
|
+
if (toMs != null) { const ts = lineTime(t); if (ts != null && ts > toMs) return false; }
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
// 默认:只看截止那天(且不超过结束时刻)
|
|
611
|
+
if (toDay && lineDay(t) !== toDay) return false;
|
|
612
|
+
if (toMs != null) { const ts = lineTime(t); if (ts != null && ts > toMs) return false; }
|
|
613
|
+
return true;
|
|
614
|
+
};
|
|
615
|
+
return allLines.filter((t) => timeOk(t)
|
|
616
|
+
&& ((lv === 'all' || (t.match(/\b(ERROR|WARN|INFO|DEBUG)\b/)?.[1] || 'INFO') === lv))
|
|
617
|
+
&& (!q || t.toLowerCase().includes(q)));
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const paintView = () => {
|
|
621
|
+
const rows = viewLines();
|
|
622
|
+
$('#lgView').innerHTML = (rows.length ? rows : ['(无匹配日志行)']).map((t) => {
|
|
623
|
+
const m = t.match(/\b(ERROR|WARN|INFO|DEBUG)\b/);
|
|
624
|
+
const level = m ? m[1] : 'INFO';
|
|
625
|
+
const i = m ? t.indexOf(m[1]) : -1;
|
|
626
|
+
if (i < 0) return esc(t);
|
|
627
|
+
return `<span style="color:${lvlColor(level)};font-weight:600">${esc(t.slice(0, i + level.length))}</span><span>${esc(t.slice(i + level.length))}</span>`;
|
|
628
|
+
}).join('\n');
|
|
629
|
+
if ($('#lgAuto').checked) { const v = $('#lgView'); v.scrollTop = v.scrollHeight; }
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
const setNote = (extra) => {
|
|
633
|
+
const q = $('#lgQ').value.trim();
|
|
634
|
+
const wantAll = $('#lgAll').checked;
|
|
635
|
+
const span = rangeFrom && rangeTo ? `${dpart(rangeFrom)} ${tpart(rangeFrom)} ~ ${dpart(rangeTo)} ${tpart(rangeTo)}` : '';
|
|
636
|
+
$('#lgNote').innerHTML = `范围 ${esc(span)};当前显示:${q ? '关键字命中的整个范围' : wantAll ? '整个范围' : '截止那天'}(${allLines.length} 行已载入${extra ? `,${extra}` : ''})`;
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
const loadRange = async () => {
|
|
640
|
+
const fromV = $('#lgFrom').value;
|
|
641
|
+
const toV = $('#lgTo').value;
|
|
642
|
+
if (!fromV || !toV) { $('#lgView').textContent = '请选择开始与结束时间'; return; }
|
|
643
|
+
const from = new Date(fromV);
|
|
644
|
+
const to = new Date(toV);
|
|
645
|
+
if (to < from) { $('#lgView').textContent = '结束时间不能早于开始时间'; return; }
|
|
646
|
+
if ((dpart(to) - dpart(from)) / 86_400_000 > 6) {
|
|
647
|
+
$('#lgView').textContent = '抱歉,最多只能搜索7天范围内日志,请重新选择时间';
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
$('#lgView').textContent = '加载中…';
|
|
651
|
+
try {
|
|
652
|
+
const r = await api('GET', `/api/logs/content?from=${encodeURIComponent(dpart(from))}&to=${encodeURIComponent(dpart(to))}&lines=2000`);
|
|
653
|
+
allLines = r.lines || [];
|
|
654
|
+
rangeFrom = from;
|
|
655
|
+
rangeTo = to;
|
|
656
|
+
setNote(`${r.files.length} 个日志文件${r.truncated ? ',日志量大仅显示末尾部分' : ''}`);
|
|
657
|
+
paintView();
|
|
658
|
+
} catch (e) {
|
|
659
|
+
allLines = [];
|
|
660
|
+
$('#lgView').textContent = `加载失败:${e.message}`;
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
/** 快捷档:设置起止时间并立即查询 */
|
|
665
|
+
const applyQuick = (key) => {
|
|
666
|
+
let from; let to;
|
|
667
|
+
if (key === 'custom') {
|
|
668
|
+
$('#lgCustom').style.display = 'flex';
|
|
669
|
+
return; // 自定义等手动改完点「查询」
|
|
670
|
+
}
|
|
671
|
+
$('#lgCustom').style.display = 'none';
|
|
672
|
+
[from, to] = QUICKS[key]();
|
|
673
|
+
$('#lgFrom').value = `${dpart(from)}T${tpart(from)}`;
|
|
674
|
+
$('#lgTo').value = `${dpart(to)}T${tpart(to)}`;
|
|
675
|
+
void loadRange();
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
$('#lgChips').querySelectorAll('button').forEach((b) => {
|
|
679
|
+
b.onclick = () => {
|
|
680
|
+
$('#lgChips').querySelectorAll('button').forEach((x) => x.classList.toggle('active', x === b));
|
|
681
|
+
applyQuick(b.dataset.q);
|
|
682
|
+
};
|
|
683
|
+
});
|
|
684
|
+
$('#lgQuery').onclick = () => void loadRange();
|
|
685
|
+
$('#lgLevel').onchange = paintView;
|
|
686
|
+
$('#lgQ').oninput = paintView;
|
|
687
|
+
$('#lgAll').onchange = () => { paintView(); setNote(); };
|
|
688
|
+
$('#lgAuto').onchange = paintView;
|
|
689
|
+
$('#lgRefresh').onclick = () => void loadRange();
|
|
690
|
+
$('#lgCopy').onclick = async () => {
|
|
691
|
+
try { await navigator.clipboard.writeText(viewLines().join('\n')); toast('当前视图日志已复制到剪贴板'); }
|
|
692
|
+
catch { toast('复制失败(浏览器剪贴板权限)', true); }
|
|
693
|
+
};
|
|
694
|
+
$('#lgDownload').onclick = () => {
|
|
695
|
+
const blob = new Blob([viewLines().join('\n')], { type: 'text/plain;charset=utf-8' });
|
|
696
|
+
const a = document.createElement('a');
|
|
697
|
+
a.href = URL.createObjectURL(blob);
|
|
698
|
+
a.download = `lcb-logs-${($('#lgFrom').value || 'start').replace(/[:T]/g, '')}-${($('#lgTo').value || 'end').replace(/[:T]/g, '')}.log`;
|
|
699
|
+
a.click();
|
|
700
|
+
URL.revokeObjectURL(a.href);
|
|
701
|
+
};
|
|
702
|
+
applyQuick('today'); // 默认今天
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// ============ 页面 ============
|
|
442
706
|
export const page = {
|
|
443
707
|
id: 'overview',
|
|
444
708
|
title: '概览',
|
|
709
|
+
desc: '桥接器运行状态、版本与快捷入口',
|
|
445
710
|
icon: '<rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/>',
|
|
446
711
|
async render(el) {
|
|
447
712
|
const st = S.status || {};
|
|
448
|
-
const claudeTag = st.claude?.hasAuth === null ? '<span class="tag off">未知</span>'
|
|
449
|
-
: st.claude?.hasAuth ? '<span class="tag ok">已就绪</span>' : '<span class="tag err">未认证</span>';
|
|
450
713
|
const stale = !st.bridge; // 旧版本进程(API 无 bridge 字段):进程托管功能整体不可用
|
|
451
714
|
const br = stale ? { running: st.embedded === true, pid: null, spawnable: false } : st.bridge;
|
|
452
|
-
const
|
|
453
|
-
|
|
715
|
+
const claudeReady = st.claude?.hasAuth !== false && st.claude?.hasAuth !== null;
|
|
716
|
+
const tile = ({ dot, dstate, name, sub, icon, actions = '', id = '' }) => `
|
|
717
|
+
<div class="tile"${id ? ` id="${id}"` : ''}>
|
|
718
|
+
<div class="tile-top"${id ? ` id="${id}Top"` : ''}><span class="dot ${dot}"></span><span class="tile-state" style="color:var(--${dot}-tx)">${dstate}</span><span class="tile-ico">${svg(icon, 16)}</span></div>
|
|
719
|
+
<div class="tile-name">${name}</div>
|
|
720
|
+
<div class="tile-sub"${id ? ` id="${id}Sub"` : ''} title="${esc(sub)}">${sub}</div>
|
|
721
|
+
<div class="tile-actions">${actions}</div>
|
|
722
|
+
</div>`;
|
|
723
|
+
const brCtl = stale
|
|
724
|
+
? '<span class="hint">进程为旧版本,启停不可用</span>'
|
|
454
725
|
: br.spawnable === false
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
: '<button class="btn primary" id="btnBrStart">启动</button>'
|
|
460
|
-
}</div>
|
|
461
|
-
<div class="hint" style="margin-top:8px">${st.embedded
|
|
462
|
-
? '本页随桥接器进程运行:停止后页面将离线(可运行 lcb ui 重开);重启会短暂失联后自动恢复。'
|
|
463
|
-
: '本页独立运行(lcb ui):在此启动的桥接器为后台进程,运行日志见 ~/.lark-claudecode-bridge/bridge.log。'}</div>`;
|
|
726
|
+
? '<span class="hint">当前运行方式不支持页面托管启停</span>'
|
|
727
|
+
: br.running
|
|
728
|
+
? '<button class="btn" id="btnBrRestart">重启</button><button class="btn danger" id="btnBrStop">停止</button>'
|
|
729
|
+
: '<button class="btn primary" id="btnBrStart">启动</button>';
|
|
464
730
|
el.innerHTML = `
|
|
465
|
-
<div class="
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
731
|
+
<div class="tiles">
|
|
732
|
+
${tile({
|
|
733
|
+
dot: br.running ? 'ok' : 'off',
|
|
734
|
+
dstate: br.running ? '运行中' : (stale && st.embedded ? '运行中(旧版本)' : '未运行'),
|
|
735
|
+
name: '桥接器进程',
|
|
736
|
+
sub: br.running ? `PID ${br.pid ?? '?'} · ${st.embedded ? '随 lcb start 常驻' : '独立页面托管'}` : (st.embedded ? '随 lcb start 常驻' : '独立页面托管'),
|
|
737
|
+
icon: IC.apps,
|
|
738
|
+
actions: brCtl,
|
|
739
|
+
})}
|
|
740
|
+
${tile({
|
|
741
|
+
dot: claudeReady ? 'ok' : 'err',
|
|
742
|
+
dstate: st.claude?.hasAuth === null ? '未知' : claudeReady ? '已就绪' : '未认证',
|
|
743
|
+
name: 'Claude 认证',
|
|
744
|
+
sub: `${st.claude?.mode === 'managed' ? 'bridge 托管' : '继承本机'} · ${esc(st.claude?.current?.model || 'CLI 默认')}`,
|
|
745
|
+
icon: IC.claude,
|
|
746
|
+
actions: '<button class="btn" data-go="claude">管理</button>',
|
|
747
|
+
})}
|
|
748
|
+
${tile({
|
|
749
|
+
dot: 'off',
|
|
750
|
+
dstate: '检测中',
|
|
751
|
+
name: '飞书 CLI',
|
|
752
|
+
sub: '安装 / 授权状态检测中…',
|
|
753
|
+
icon: IC.zap,
|
|
754
|
+
actions: '<button class="btn" id="tileCliAuth">授权</button>',
|
|
755
|
+
id: 'tileCli',
|
|
756
|
+
})}
|
|
757
|
+
${tile({
|
|
758
|
+
dot: 'off',
|
|
759
|
+
dstate: '检测中',
|
|
760
|
+
name: '记忆增强',
|
|
761
|
+
sub: 'OpenViking 记忆服务检测中…',
|
|
762
|
+
icon: IC.memory,
|
|
763
|
+
actions: '<button class="btn" data-go="memory">管理</button>',
|
|
764
|
+
})}
|
|
482
765
|
</div>
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
<div class="
|
|
486
|
-
<
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
766
|
+
|
|
767
|
+
<div class="grid2" style="gap:16px;align-items:stretch;margin-bottom: 16px;">
|
|
768
|
+
<div class="panel" style="margin:0">
|
|
769
|
+
<header class="panel-head"><h3>LCB 桥接器 · 版本与更新</h3>
|
|
770
|
+
<div class="panel-actions" id="updLatest"><span class="hint">未检查</span></div>
|
|
771
|
+
</header>
|
|
772
|
+
<div class="panel-body">
|
|
773
|
+
<table class="kv">
|
|
774
|
+
<tr><td>当前版本</td><td><span class="chip">v${esc(st.version)}</span></td></tr>
|
|
775
|
+
<tr><td>运行模式</td><td>${st.embedded ? '随 lcb start 常驻' : 'lcb ui 独立页面'}</td></tr>
|
|
776
|
+
<tr><td>配置文件</td><td><code>${esc(st.configPath)}</code></td></tr>
|
|
777
|
+
<tr><td>开机自启</td><td id="asStat"><span class="hint">检测中…</span></td></tr>
|
|
778
|
+
</table>
|
|
779
|
+
<div style="display:flex;gap:8px;margin-top:12px;align-items:center">
|
|
780
|
+
<button class="btn sm" id="btnUpdCheck">检查更新</button>
|
|
781
|
+
<span id="updArea"></span>
|
|
782
|
+
</div>
|
|
494
783
|
</div>
|
|
495
784
|
</div>
|
|
496
|
-
<div class="
|
|
497
|
-
<
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
785
|
+
<div class="panel" style="margin:0">
|
|
786
|
+
<header class="panel-head"><h3>飞书 CLI · 版本与更新</h3>
|
|
787
|
+
<div class="panel-actions"><button class="btn sm" id="btnCliCheck">检查更新</button></div>
|
|
788
|
+
</header>
|
|
789
|
+
<div class="panel-body">
|
|
790
|
+
<table class="kv">
|
|
791
|
+
<tr><td>当前状态</td><td id="larkCliStat"><span class="hint">检测中…</span></td></tr>
|
|
792
|
+
</table>
|
|
793
|
+
<div class="mini-note" style="margin-top:10px">飞书官方 CLI(@larksuite/cli)负责设备授权与应用配置;与 LCB 桥接器的更新相互独立、各走各的按钮。</div>
|
|
794
|
+
</div>
|
|
501
795
|
</div>
|
|
502
796
|
</div>
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
<
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
797
|
+
|
|
798
|
+
<div class="panel">
|
|
799
|
+
<header class="panel-head"><h3>飞书快捷指令</h3>
|
|
800
|
+
<div class="panel-actions"><button class="btn sm" data-go="slash">管理命令 →</button></div>
|
|
801
|
+
</header>
|
|
802
|
+
<div class="panel-body" style="padding-top:8px">
|
|
803
|
+
<div style="display:flex;flex-wrap:wrap;gap:8px">
|
|
804
|
+
${(st.commands || []).map((c) => `<span class="chip src" style="font-size:12.5px;padding:4px 12px">/${esc(c.command)}<span class="mut" style="margin-left:4px">${esc(c.description)}</span></span>`).join('')}
|
|
805
|
+
</div>
|
|
806
|
+
<div class="mini-note" style="margin-top:10px">在飞书聊天框输入 / 触发;其它 / 开头的消息原文透传给 Claude Code。</div>
|
|
807
|
+
</div>
|
|
509
808
|
</div>
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
<
|
|
809
|
+
|
|
810
|
+
<div class="panel">
|
|
811
|
+
<header class="panel-head"><h3>常用指引</h3></header>
|
|
812
|
+
<div class="panel-body">
|
|
813
|
+
<div class="mini-note">飞书端与机器人对话即可使用;<code>/help</code> 查看全部命令。配置修改后:<b>应用 / 认证 / server 段需重启 lcb start</b>,工作区 / 权限 / 触发词下一条消息自动生效。${st.claude?.hasAuth === false ? '<br>⚠️ 未检测到 Claude 认证:到「Claude 认证」页配置,或在配置页所在机器执行 claude login。' : ''}</div>
|
|
814
|
+
</div>
|
|
513
815
|
</div>`;
|
|
514
|
-
|
|
816
|
+
$('#pageActions').innerHTML = `<button class="btn primary" id="btnOpenLog">${svg(IC.file, 14)} 查看日志</button>`;
|
|
817
|
+
// 页头按钮在 #view 之外,需单独绑定(绑在 #view 委托上会因不在容器内而永远收不到事件)
|
|
818
|
+
$('#btnOpenLog').onclick = () => void logViewer();
|
|
819
|
+
|
|
820
|
+
// ---- 瓦片与跳转 ----
|
|
821
|
+
el.querySelectorAll('[data-go]').forEach((b) => b.onclick = () => { location.hash = b.dataset.go; });
|
|
515
822
|
const bind = (id, fn) => { const b = $(id); if (b) b.onclick = fn; };
|
|
516
823
|
bind('#btnBrStart', startFlow); bind('#btnBrStop', stopFlow); bind('#btnBrRestart', restartFlow);
|
|
517
824
|
bind('#btnUpdCheck', doCheckUpdate);
|
|
825
|
+
bind('#btnCliCheck', () => void doCheckLarkCli());
|
|
826
|
+
$('#tileCliAuth').onclick = () => (
|
|
827
|
+
LARKCLI.data && !LARKCLI.data.installed ? void larkCliActionFlow('install') : openDeviceDialog()
|
|
828
|
+
);
|
|
829
|
+
// 自动检查 30s 节流(切回概览 tab 不重复打 npm)
|
|
518
830
|
if (UPD.data && Date.now() - UPD.checkedAt < 30000) renderUpdateResult();
|
|
519
831
|
else doCheckUpdate();
|
|
520
832
|
if (LARKCLI.data && Date.now() - LARKCLI.checkedAt < 30000) renderLarkCliResult();
|
|
521
|
-
else doCheckLarkCli();
|
|
833
|
+
else void doCheckLarkCli();
|
|
522
834
|
void renderAutostart();
|
|
835
|
+
|
|
836
|
+
// ---- 记忆增强瓦片:异步补状态(失败静默保留「检测中」) ----
|
|
837
|
+
void (async () => {
|
|
838
|
+
let ms = null;
|
|
839
|
+
try { ms = await api('GET', '/api/memory/status'); } catch { /* 首装/服务未装:瓦片保持未就绪 */ }
|
|
840
|
+
const tileEl = [...el.querySelectorAll('.tile')].find((t) => t.querySelector('.tile-name')?.textContent === '记忆增强');
|
|
841
|
+
if (!tileEl) return;
|
|
842
|
+
const m = ms?.memory || {};
|
|
843
|
+
const healthy = !!ms?.health?.healthy;
|
|
844
|
+
const dot = m.enabled === false ? 'off' : healthy ? 'ok' : 'warn';
|
|
845
|
+
const dstate = m.enabled === false ? '已停用' : healthy ? '运行中' : '服务异常';
|
|
846
|
+
const sub = healthy ? `OpenViking · ${m.endpoint || ''}` : (m.enabled === false ? '未启用(可在记忆增强页开启)' : '未就绪——到记忆增强页检查或启动');
|
|
847
|
+
tileEl.querySelector('.tile-top').innerHTML = `<span class="dot ${dot}"></span><span class="tile-state" style="color:var(--${dot}-tx)">${dstate}</span><span class="tile-ico">${svg(IC.memory, 16)}</span>`;
|
|
848
|
+
const subEl = tileEl.querySelector('.tile-sub');
|
|
849
|
+
subEl.textContent = sub;
|
|
850
|
+
subEl.title = sub;
|
|
851
|
+
})();
|
|
523
852
|
},
|
|
524
853
|
};
|