@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.
- 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 +344 -76
- 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 +75 -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 +3 -0
- package/dist/index.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, 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 = '
|
|
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
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 =
|
|
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
|
-
/**
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
514
|
-
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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="
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
<div class="
|
|
547
|
-
<
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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="
|
|
558
|
-
<
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
<
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
<
|
|
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
|
-
|
|
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
|
};
|