@jesonliu/lark-claudecode-bridge 1.4.3 → 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.
Files changed (42) hide show
  1. package/assets/web/css/main.css +766 -574
  2. package/assets/web/index.html +37 -17
  3. package/assets/web/js/auth.js +217 -0
  4. package/assets/web/js/bootstrap.js +36 -34
  5. package/assets/web/js/core.js +4 -3
  6. package/assets/web/js/main.js +79 -20
  7. package/assets/web/js/markdown.js +64 -0
  8. package/assets/web/js/pages/apps.js +338 -251
  9. package/assets/web/js/pages/chats.js +446 -0
  10. package/assets/web/js/pages/claude.js +243 -180
  11. package/assets/web/js/pages/mcp.js +59 -43
  12. package/assets/web/js/pages/memory.js +39 -43
  13. package/assets/web/js/pages/overview.js +344 -76
  14. package/assets/web/js/pages/permissions.js +40 -32
  15. package/assets/web/js/pages/plugins.js +51 -37
  16. package/assets/web/js/pages/skills.js +68 -92
  17. package/assets/web/js/pages/slash.js +31 -23
  18. package/assets/web/js/pages/workspaces.js +114 -73
  19. package/assets/web/js/theme.js +21 -18
  20. package/assets/web/js/ui.js +75 -32
  21. package/dist/bin/lcb.js +15 -0
  22. package/dist/bin/lcb.js.map +1 -1
  23. package/dist/gateway/feishu-gateway.d.ts +11 -0
  24. package/dist/gateway/feishu-gateway.js +5 -0
  25. package/dist/gateway/feishu-gateway.js.map +1 -1
  26. package/dist/gateway/gateway-registry.d.ts +16 -0
  27. package/dist/gateway/gateway-registry.js +10 -0
  28. package/dist/gateway/gateway-registry.js.map +1 -0
  29. package/dist/index.js +3 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/web/chats-api.d.ts +77 -0
  32. package/dist/web/chats-api.js +475 -0
  33. package/dist/web/chats-api.js.map +1 -0
  34. package/dist/web/logs-api.d.ts +37 -0
  35. package/dist/web/logs-api.js +141 -0
  36. package/dist/web/logs-api.js.map +1 -0
  37. package/dist/web/server.js +125 -6
  38. package/dist/web/server.js.map +1 -1
  39. package/dist/web/webauth.d.ts +44 -0
  40. package/dist/web/webauth.js +177 -0
  41. package/dist/web/webauth.js.map +1 -0
  42. package/package.json +1 -1
@@ -1,7 +1,9 @@
1
1
  // ============ 概览 ============
2
- // 桥接器启停 / 版本更新(宿主进程管理:embedded 页面随进程生死,独立页面经 PID 跨进程操作)
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, stepsDialog } 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,15 +11,27 @@ 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 = 'br-mask';
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="br-mask-card">${inner}</div>`;
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
37
 
@@ -163,7 +177,7 @@ function renderUpdateResult() {
163
177
  const area = $('#updArea');
164
178
  if (!area) return;
165
179
  if (r.hasUpdate && r.mode === 'global') {
166
- area.innerHTML = '<button class="btn primary" id="btnUpdRun">一键更新</button>';
180
+ area.innerHTML = `<button class="btn sm primary" id="btnUpdRun">更新到 v${esc(r.latest)}</button>`;
167
181
  $('#btnUpdRun').onclick = runUpdateFlow;
168
182
  } else if (r.hasUpdate) {
169
183
  area.innerHTML = '<span class="hint">当前非 npm 全局安装运行,请手动更新</span>';
@@ -189,7 +203,7 @@ async function runUpdateFlow() {
189
203
  const btn = $('#btnUpdRun');
190
204
  if (btn) { btn.disabled = true; btn.textContent = '更新中…'; }
191
205
  try {
192
- // 只更新桥接器自身:飞书 CLI 的更新在它自己那一行点「更新」,两条更新各自独立
206
+ // 只更新桥接器自身:飞书 CLI 的更新在它自己那张卡点「更新」,两条更新各自独立
193
207
  await api('POST', '/api/update/run');
194
208
  UPD.checkedAt = 0; LARKCLI.checkedAt = 0;
195
209
  if (running) await restartFlow();
@@ -201,7 +215,7 @@ async function runUpdateFlow() {
201
215
  }
202
216
  }
203
217
 
204
- /** 纯函数(可测):概览表格行里「飞书 CLI」的状态 HTML(不含操作按钮,按钮由 render 按需挂) */
218
+ /** 纯函数(可测):飞书 CLI 状态 HTML(不含操作按钮,按钮由 renderLarkCliResult 按需挂) */
205
219
  export function larkCliStatHtml(r) {
206
220
  if (!r) return '<span class="hint">未检查</span>';
207
221
  if (!r.installed) return '<span class="tag off">未安装</span>';
@@ -223,7 +237,7 @@ export function larkCliStatHtml(r) {
223
237
  }
224
238
 
225
239
  /**
226
- * 纯函数(可测):概览行主入口按钮的文案——按「点开后会先走哪一步」命名,
240
+ * 纯函数(可测):飞书 CLI 主入口按钮的文案——按「点开后会先走哪一步」命名,
227
241
  * 不出现「按钮写扫码授权、点开弹窗却是配置二维码」的错位。
228
242
  * 未配置应用时弹窗从 config 阶段起步(见 openDeviceDialog 与 larkcli-device.js),故叫「配置应用」。
229
243
  * 探测不出(unknown)**不**算未配置:误判会把已配好应用的用户反复推去重配,按「扫码授权」处理。
@@ -249,11 +263,12 @@ function openDeviceDialog() {
249
263
  });
250
264
  }
251
265
 
266
+ /** 飞书 CLI 卡内的动作按钮区(安装 / 更新 / 授权 / SKILL) */
252
267
  function renderLarkCliResult() {
253
268
  const el = $('#larkCliStat');
254
269
  const r = LARKCLI.data;
255
270
  if (!el) return;
256
- 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>`;
257
272
  const area = $('#larkCliArea');
258
273
  if (!area || !r) return;
259
274
  const add = (label, op) => {
@@ -262,29 +277,49 @@ function renderLarkCliResult() {
262
277
  b.textContent = label;
263
278
  b.onclick = () => (op === 'auth-device' ? openDeviceDialog() : larkCliActionFlow(op));
264
279
  area.appendChild(b);
265
- area.appendChild(document.createTextNode(' '));
266
280
  };
267
281
  if (!r.installed) { add('安装', 'install'); return; }
268
282
  if (r.hasUpdate) add('更新', 'update');
269
283
  // 入口常驻,且已授权也给(token 会过期,随时可重走一遍 device flow 刷新)。
270
284
  // 走**页面内二维码**(主路径,无桌面环境同样可用);终端窗口降级为弹窗里的备选。
271
- // 文案跟着「弹窗会先走哪一步」走(见 larkCliPrimaryLabel):未配置应用时弹窗从配置阶段
272
- // 起步、先出配置二维码,按钮就叫「配置应用」;已配置才是「扫码授权」。
273
- // **入口数量不变**——三种状态都是同一个弹窗(op 一律 auth-device),只是弹窗按前置条件
274
- // 决定先出哪种二维码(见 openDeviceDialog 与 larkcli-device.js)。
275
285
  add(larkCliPrimaryLabel(r), 'auth-device');
276
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' ? '重新授权' : '去授权') : '安装';
277
311
  }
278
312
 
279
313
  async function doCheckLarkCli() {
280
314
  const el = $('#larkCliStat'); if (!el) return;
281
- el.innerHTML = '<span class="hint">检测中…</span>';
282
315
  try {
283
316
  LARKCLI.data = await api('GET', '/api/lark-cli?check=1');
284
317
  LARKCLI.checkedAt = Date.now();
285
318
  renderLarkCliResult();
319
+ paintCliTile(LARKCLI.data);
286
320
  } catch (e) {
287
321
  el.innerHTML = `<span class="hint">检测失败:${esc(e.message)}</span>`;
322
+ paintCliTile(null, true);
288
323
  }
289
324
  }
290
325
 
@@ -408,7 +443,7 @@ async function larkCliActionFlow(op = 'install') {
408
443
  },
409
444
  config: {
410
445
  what: '配置应用',
411
- // 这是**备选**路径:主路径已改成弹窗里的配置二维码(见 larkcli-device.js 的 config 阶段),
446
+ // 这是**备选**路径:主路径是弹窗里的配置二维码(见 larkcli-device.js 的 config 阶段),
412
447
  // 只有用户在弹窗里显式点「改用终端窗口」才会走到这里
413
448
  msg: '将打开一个终端窗口执行 <code>lark-cli config init --new</code>:按窗口打印的链接在浏览器完成飞书应用创建,'
414
449
  + '完成后会自动进入授权登录。'
@@ -466,9 +501,9 @@ function paintAutostart(el, r) {
466
501
  const tags = r.enabled
467
502
  ? '<span class="tag ok">已开启</span>'
468
503
  : '<span class="tag off">未开启</span>';
504
+ // 只留开关 + 状态 tag + 过期警示;各平台的「登录启动管理方式」说明文案一律不显示(主人拍板)
469
505
  const stale = r.stale ? ' <span class="tag warn">启动命令已过期,关闭再开启可更新</span>' : '';
470
- const hint = r.detail ? ` <span class="hint">${esc(r.detail)}</span>` : '';
471
- 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>`;
472
507
  $('#asToggle').onchange = async (e) => {
473
508
  const on = e.target.checked;
474
509
  try {
@@ -482,7 +517,7 @@ function paintAutostart(el, r) {
482
517
  };
483
518
  }
484
519
 
485
- /** 概览页「开机自启」行:首次渲染拉状态;开关点击即生效(低风险,与权限页即时保存一致) */
520
+ /** LCB 更新卡内「开机自启」行:首次渲染拉状态;开关点击即生效(低风险,即时保存) */
486
521
  async function renderAutostart() {
487
522
  const el = $('#asStat');
488
523
  if (!el) return;
@@ -500,86 +535,319 @@ async function renderAutostart() {
500
535
  paintAutostart(el, r);
501
536
  }
502
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
+ // ============ 页面 ============
503
706
  export const page = {
504
707
  id: 'overview',
505
708
  title: '概览',
709
+ desc: '桥接器运行状态、版本与快捷入口',
506
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"/>',
507
711
  async render(el) {
508
712
  const st = S.status || {};
509
- const claudeTag = st.claude?.hasAuth === null ? '<span class="tag off">未知</span>'
510
- : st.claude?.hasAuth ? '<span class="tag ok">已就绪</span>' : '<span class="tag err">未认证</span>';
511
713
  const stale = !st.bridge; // 旧版本进程(API 无 bridge 字段):进程托管功能整体不可用
512
714
  const br = stale ? { running: st.embedded === true, pid: null, spawnable: false } : st.bridge;
513
- const ctl = stale
514
- ? '<div class="hint" style="margin-top:10px">⚠️ 配置页服务进程为旧版本,页面启停 / 重启 / 更新不可用:请重启桥接器(lcb start)后刷新本页。</div>'
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>'
515
725
  : br.spawnable === false
516
- ? '<div class="hint" style="margin-top:10px">当前运行方式不支持页面托管启停(如源码运行),请在服务器上手动运行 lcb start。</div>'
517
- : `<div style="display:flex; gap:10px; margin-top:12px">${
518
- br.running
519
- ? '<button class="btn" id="btnBrRestart">重启</button><button class="btn danger" id="btnBrStop">停止</button>'
520
- : '<button class="btn primary" id="btnBrStart">启动</button>'
521
- }</div>
522
- <div class="hint" style="margin-top:8px">${st.embedded
523
- ? '本页随桥接器进程运行:停止后页面将离线(可运行 lcb ui 重开);重启会短暂失联后自动恢复。'
524
- : '本页独立运行(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>';
525
730
  el.innerHTML = `
526
- <div class="card">
527
- <h3>运行状态</h3>
528
- <table>
529
- <tr><td>版本</td><td>v${esc(st.version)}</td></tr>
530
- <tr><td>模式</td><td>${st.embedded ? '随 lcb start 常驻' : 'lcb ui 独立(桥接器未随本页启动)'}</td></tr>
531
- <tr><td>桥接器进程</td><td>${stale
532
- ? (st.embedded ? '<span class="tag warn">运行中(旧版本进程)</span>' : '<span class="tag off">状态未知(旧版本进程)</span>')
533
- : br.running
534
- ? `<span class="tag ok">运行中</span> <span class="hint">PID ${esc(br.pid)}</span>`
535
- : '<span class="tag off">未运行</span>'}</td></tr>
536
- <tr><td>配置文件</td><td><code>${esc(st.configPath)}</code></td></tr>
537
- <tr><td>配置页</td><td><code>${esc(location.host)}</code></td></tr>
538
- <tr><td>Claude 认证</td><td>${claudeTag}(模式:${esc(st.claude?.mode || 'inherit')})</td></tr>
539
- <tr><td>开机自启</td><td id="asStat"><span class="hint">检测中…</span></td></tr>
540
- </table>
541
- ${ctl}
542
- <div class="desc" style="margin-top:10px">${st.claude?.hasAuth === false ? '⚠️ 未检测到认证:到「Claude 认证」页填写,或在配置页所在机器执行 claude login。' : ''}</div>
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
+ })}
543
765
  </div>
544
- <div class="card">
545
- <h3>版本与更新</h3>
546
- <div class="card-sub">
547
- <h4 class="card-sub-title">▍LCB 桥接器</h4>
548
- <table>
549
- <tr><td>当前版本</td><td><span class="chip">v${esc(st.version)}</span></td></tr>
550
- <tr><td>最新版本</td><td id="updLatest"><span class="hint">未检查</span></td></tr>
551
- </table>
552
- <div style="display:flex; gap:10px; margin-top:12px; align-items:center">
553
- <button class="btn" id="btnUpdCheck">检查更新</button>
554
- <span id="updArea"></span>
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>
555
783
  </div>
556
784
  </div>
557
- <div class="card-sub">
558
- <h4 class="card-sub-title">▍飞书 CLI(lark-cli)</h4>
559
- <table>
560
- <tr><td>状态</td><td id="larkCliStat"><span class="hint">检测中…</span></td></tr>
561
- </table>
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>
562
795
  </div>
563
796
  </div>
564
- <div class="card">
565
- <h3>常用命令</h3>
566
- <div class="desc">在飞书聊天框输入 <code>/</code> 触发;其它 / 开头消息原文透传给 Claude Code。斜杠面板需先到「斜杠命令」页注册(补齐内置命令)。</div>
567
- <table><thead><tr><th style="width:150px">命令</th><th>说明</th></tr></thead><tbody>
568
- ${(st.commands || []).map((c) => `<tr><td><code>/${esc(c.command)}</code></td><td>${esc(c.description)}</td></tr>`).join('') || '<tr><td colspan="2" class="hint">无</td></tr>'}
569
- </tbody></table>
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>
570
808
  </div>
571
- <div class="card">
572
- <h3>常用指引</h3>
573
- <div class="desc">飞书端与机器人对话即可使用;<code>/help</code> 查看全部命令。配置修改后:<b>应用 / 认证 / server 段需重启 lcb start</b>,工作区 / 权限 / 触发词下一条消息自动生效。</div>
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>
574
815
  </div>`;
575
- // 启停按钮 + 版本更新 + 飞书 CLI 状态;自动检查 30s 节流(切回概览 tab 不重复打 npm)
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; });
576
822
  const bind = (id, fn) => { const b = $(id); if (b) b.onclick = fn; };
577
823
  bind('#btnBrStart', startFlow); bind('#btnBrStop', stopFlow); bind('#btnBrRestart', restartFlow);
578
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)
579
830
  if (UPD.data && Date.now() - UPD.checkedAt < 30000) renderUpdateResult();
580
831
  else doCheckUpdate();
581
832
  if (LARKCLI.data && Date.now() - LARKCLI.checkedAt < 30000) renderLarkCliResult();
582
- else doCheckLarkCli();
833
+ else void doCheckLarkCli();
583
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
+ })();
584
852
  },
585
853
  };