@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.
Files changed (45) 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 +430 -101
  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 +135 -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 +54 -41
  30. package/dist/index.js.map +1 -1
  31. package/dist/memory-installer.d.ts +7 -0
  32. package/dist/memory-installer.js +22 -5
  33. package/dist/memory-installer.js.map +1 -1
  34. package/dist/web/chats-api.d.ts +77 -0
  35. package/dist/web/chats-api.js +475 -0
  36. package/dist/web/chats-api.js.map +1 -0
  37. package/dist/web/logs-api.d.ts +37 -0
  38. package/dist/web/logs-api.js +141 -0
  39. package/dist/web/logs-api.js.map +1 -0
  40. package/dist/web/server.js +125 -6
  41. package/dist/web/server.js.map +1 -1
  42. package/dist/web/webauth.d.ts +44 -0
  43. package/dist/web/webauth.js +177 -0
  44. package/dist/web/webauth.js.map +1 -0
  45. package/package.json +1 -1
@@ -0,0 +1,141 @@
1
+ // 运行日志查看器 API(概览页「查看日志」抽屉数据源):
2
+ // - 列表:logs/ 目录下全部 *.log(现命名 bridge-YYYY-MM-DD.log,按天滚动),按修改时间倒序
3
+ // - 单文件尾读:按文件名取尾部 N 行
4
+ // - 日期范围(会话页主路径):from~to 每天的日志依时间序合并尾读,前端不再列文件只选日期
5
+ // 安全:文件名白名单 + 解析后必须仍落在 logs 目录内(防路径穿越读到配置/凭证)
6
+ import { closeSync, openSync, readFileSync, readSync, readdirSync, statSync } from 'node:fs';
7
+ import { join, resolve, sep } from 'node:path';
8
+ const LOG_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*\.log$/;
9
+ const RANGE_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
10
+ /** 尾读切块上限:超过就只读文件末尾这一段(首行可能被截断,标记 truncated 丢弃) */
11
+ const TAIL_WINDOW_BYTES = 1_500_000;
12
+ /** 日期范围查询的防呆上限(主人拍板:最多搜 7 天,跨天全量检索代价高且很少需要) */
13
+ const RANGE_MAX_DAYS = 7;
14
+ /** 单文件在范围查询里的行数配额(避免单日巨日志吃光总量配额) */
15
+ const RANGE_PER_FILE_LINES = 1000;
16
+ const logsDirOf = (configPath) => join(resolve(configPath, '..'), 'logs');
17
+ export function listLogFiles(configPath) {
18
+ const dir = logsDirOf(configPath);
19
+ let files = [];
20
+ try {
21
+ files = readdirSync(dir)
22
+ .filter((n) => n.endsWith('.log'))
23
+ .map((name) => {
24
+ try {
25
+ const st = statSync(join(dir, name));
26
+ return { name, size: st.size, mtime: st.mtime.toISOString() };
27
+ }
28
+ catch {
29
+ return { name, size: 0, mtime: '' };
30
+ }
31
+ })
32
+ .filter((f) => f.mtime)
33
+ .sort((a, b) => b.mtime.localeCompare(a.mtime));
34
+ }
35
+ catch { /* logs 目录不存在(从未启动过桥):空列表 */ }
36
+ return { dir, files };
37
+ }
38
+ /** 读取文件文本:>TAIL_WINDOW_BYTES 时只读末尾窗口(首行可能残缺) */
39
+ function readTailText(fp, size) {
40
+ if (size > TAIL_WINDOW_BYTES) {
41
+ // 只读末尾窗口:开 fd 定位到 size-window 再读(避免整文件载入内存)
42
+ const fd = openSync(fp, 'r');
43
+ try {
44
+ const buf = Buffer.alloc(TAIL_WINDOW_BYTES);
45
+ const read = readSync(fd, buf, 0, TAIL_WINDOW_BYTES, size - TAIL_WINDOW_BYTES);
46
+ return { text: buf.subarray(0, read).toString('utf8'), truncated: true };
47
+ }
48
+ finally {
49
+ closeSync(fd);
50
+ }
51
+ }
52
+ return { text: readFileSync(fp, 'utf8'), truncated: false };
53
+ }
54
+ /** 取尾部 maxLines 行(默认 500,上限 5000) */
55
+ export function tailLogFile(configPath, name, maxLines) {
56
+ if (!LOG_NAME_RE.test(name))
57
+ return { error: '文件名不合法' };
58
+ const dir = resolve(logsDirOf(configPath));
59
+ const fp = resolve(dir, name);
60
+ if (!fp.startsWith(dir + sep))
61
+ return { error: '路径不在日志目录内' };
62
+ const limit = Number.isFinite(maxLines) ? Math.min(Math.max(Math.floor(maxLines), 1), 5000) : 500;
63
+ let st;
64
+ try {
65
+ st = statSync(fp);
66
+ }
67
+ catch {
68
+ return { error: '日志文件不存在(可能已被 14 天滚动清理)' };
69
+ }
70
+ if (!st.isFile())
71
+ return { error: '不是文件' };
72
+ const { text, truncated } = readTailText(fp, st.size);
73
+ let lines = text.split(/\r?\n/);
74
+ if (truncated && lines.length > 0)
75
+ lines.shift(); // 末尾窗口首行大概率被切断
76
+ while (lines.length && lines[lines.length - 1] === '')
77
+ lines.pop();
78
+ return { name, size: st.size, truncated, lines: lines.slice(-limit) };
79
+ }
80
+ const pad2 = (n) => String(n).padStart(2, '0');
81
+ /** 本地时区的 YYYY-MM-DD(日志文件名按本地日滚动,与 log-tee 命名同口径) */
82
+ const localDate = (t) => {
83
+ const d = new Date(t);
84
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
85
+ };
86
+ /**
87
+ * 日期范围聚合尾读:from~to(含两端)每天的 bridge-YYYY-MM-DD.log 依时间序合并。
88
+ * 每文件最多 RANGE_PER_FILE_LINES 行(防止单日巨日志吃光配额),总量再按 maxLines 截尾。
89
+ * 单日文件过大只读末尾窗口时同样标记 truncated。
90
+ */
91
+ export function tailLogRange(configPath, from, to, maxLines) {
92
+ if (!RANGE_DATE_RE.test(from) || !RANGE_DATE_RE.test(to))
93
+ return { error: '日期格式须为 YYYY-MM-DD' };
94
+ const startMs = new Date(`${from}T00:00:00`).getTime();
95
+ const endMs = new Date(`${to}T00:00:00`).getTime();
96
+ if (!Number.isFinite(startMs) || !Number.isFinite(endMs))
97
+ return { error: '日期不合法' };
98
+ if (endMs < startMs)
99
+ return { error: '结束日期不能早于开始日期' };
100
+ if ((endMs - startMs) / 86_400_000 > RANGE_MAX_DAYS - 1) {
101
+ return { error: `抱歉,最多只能搜索 ${RANGE_MAX_DAYS} 天范围内日志,请重新选择时间` };
102
+ }
103
+ const limit = Number.isFinite(maxLines) ? Math.min(Math.max(Math.floor(maxLines), 1), 5000) : 2000;
104
+ const dir = resolve(logsDirOf(configPath));
105
+ const files = [];
106
+ let truncated = false;
107
+ const lines = [];
108
+ for (let t = startMs; t <= endMs; t += 86_400_000) {
109
+ // 按 1 天步进在 DST/大小时由 localDate 归一(国内无 DST;跨月由 Date 自动进位)
110
+ const dateStr = localDate(t);
111
+ if (localDate(t + 12 * 3600_000) !== dateStr)
112
+ continue; // 极端时钟回拨防重复
113
+ const name = `bridge-${dateStr}.log`;
114
+ const fp = resolve(dir, name);
115
+ let st;
116
+ try {
117
+ st = statSync(fp);
118
+ if (!st.isFile())
119
+ continue;
120
+ }
121
+ catch {
122
+ continue; // 当天无日志(未运行/已被清理):静默跳过
123
+ }
124
+ files.push(name);
125
+ const { text, truncated: tailCut } = readTailText(fp, st.size);
126
+ if (tailCut)
127
+ truncated = true;
128
+ let dayLines = text.split(/\r?\n/);
129
+ if (tailCut && dayLines.length > 0)
130
+ dayLines.shift();
131
+ while (dayLines.length && dayLines[dayLines.length - 1] === '')
132
+ dayLines.pop();
133
+ lines.push(...dayLines.slice(-RANGE_PER_FILE_LINES));
134
+ if (lines.length > limit * 2)
135
+ break; // 已远超总量:提前收尾(最后统一截尾)
136
+ }
137
+ if (files.length === 0)
138
+ return { error: '所选日期范围内没有日志文件(未运行过或已被 14 天滚动清理)' };
139
+ return { from, to, files, truncated: truncated || lines.length > limit, lines: lines.slice(-limit) };
140
+ }
141
+ //# sourceMappingURL=logs-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logs-api.js","sourceRoot":"","sources":["../../src/web/logs-api.ts"],"names":[],"mappings":"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"}
@@ -29,6 +29,9 @@ import { bridgeStatus, resolveLcbEntry, restartBridgeWithHelper, spawnBridgeDeta
29
29
  import { getAutostartStatus, setAutostartEnabled } from './autostart.js';
30
30
  import { checkUpdate, hasNewerVersion, installMode, runUpdate } from './update.js';
31
31
  import { memoryStatus, testMemoryConnection } from './memory-api.js';
32
+ import { chatsTree, chatTopics, chatTimeline } from './chats-api.js';
33
+ import { authMeta, authThrottleState, changePassword, clearAuthFailures, clearSessionCookie, createSession, dropOtherSessions, dropSession, readSessionToken, registerAuthFailure, resetPasswordToDefault, sessionCookie, setLockMinutes, touchSession, verifyPassword, } from './webauth.js';
34
+ import { listLogFiles, tailLogFile, tailLogRange } from './logs-api.js';
32
35
  import { deleteMemoryUser, initMemoryUsers, listMemoryUsers, resetUserKey, revealRootKey, revealUserKey, } from './memory-users-api.js';
33
36
  import { MANAGED_OV_CONF, getInstallState, runInstall, runStartPipeline, stopManagedServer } from '../memory-installer.js';
34
37
  import { checkLarkCliAuth, checkLarkCliLatest, detectLarkCli, detectLarkCliSkill, getLarkCliConfigStatus, getLarkCliDeviceStatus, runLarkCliActionFlow, startLarkCliConfigFlow, startLarkCliDeviceAuth, } from '../lark-cli-manager.js';
@@ -122,7 +125,8 @@ function listSubdirs(input) {
122
125
  const dirs = readdirSync(abs, { withFileTypes: true })
123
126
  .filter((e) => e.isDirectory())
124
127
  .map((e) => e.name)
125
- .sort((a, b) => a.localeCompare(b));
128
+ // 普通目录在前、点开头隐藏目录殿后(目录选择弹层网格的浏览主路径是普通目录)
129
+ .sort((a, b) => (a.startsWith('.') ? 1 : 0) - (b.startsWith('.') ? 1 : 0) || a.localeCompare(b));
126
130
  return { path: abs, parent: parentOf(abs), dirs };
127
131
  }
128
132
  catch (e) {
@@ -175,11 +179,10 @@ async function friendlyAddrInUse(host, port) {
175
179
  let occupier = '';
176
180
  try {
177
181
  const res = await fetch(`http://${disp}:${port}/api/status`, { signal: AbortSignal.timeout(2000) });
178
- if (res.ok) {
179
- const st = await res.json().catch(() => null);
180
- if (st && typeof st.version === 'string') {
181
- occupier = `(占用者疑似旧 lcb 配置页进程 v${st.version}——npm 更新后旧进程未退出,需手动结束)`;
182
- }
182
+ // 401 也带 version(鉴权网关对 /api/status 特批附版本):加锁后的新进程仍可被识别
183
+ const st = await res.json().catch(() => null);
184
+ if (st && typeof st.version === 'string') {
185
+ occupier = `(占用者疑似旧 lcb 配置页进程 v${st.version}——npm 更新后旧进程未退出,需手动结束)`;
183
186
  }
184
187
  }
185
188
  catch { /* 探测失败/非 lcb 服务:走通用文案 */ }
@@ -238,6 +241,7 @@ export async function startWebServer(opts = {}) {
238
241
  async function handle(req, res, ctx) {
239
242
  const url = new URL(req.url ?? '/', 'http://localhost');
240
243
  const path = url.pathname;
244
+ const configPath = ctx.configPath; // 短别名:鉴权段大量引用
241
245
  // 每请求现查(不能用启动快照):bootstrap 写盘成功后,后续请求立即按「已有配置」处理
242
246
  const firstRun = !existsSync(ctx.configPath);
243
247
  // 安全闸:Host / Origin 校验(防 DNS rebinding 拿本机页面读写配置)
@@ -246,6 +250,97 @@ async function handle(req, res, ctx) {
246
250
  res.end('Forbidden');
247
251
  return;
248
252
  }
253
+ // ---- 配置页密码闸:静态资源(html/js/css)公开(页面源码不含秘密);锁屏本身在前端,
254
+ // 但数据全部挡在 API 层——所有 /api/*(除登录)必须携带有效会话 cookie,否则 401。
255
+ // 首装向导 /api/bootstrap 同样被拦:内网他人无法抢在主人之前 bootstrap 配置 ----
256
+ const sessionToken = readSessionToken(req.headers.cookie);
257
+ if (path.startsWith('/api/') && path !== '/api/auth/verify') {
258
+ // 每请求滑动续期:TTL 取当前锁屏设置(改设置即刻约束既有会话;webauth 文件随读随取,
259
+ // 顺带让 `lcb reset-password` 的改密对运行中的进程即时生效)
260
+ const okSession = !!sessionToken && touchSession(sessionToken, authMeta(configPath).lockMinutes);
261
+ if (!okSession) {
262
+ // /api/status 在 401 里特批附 version:EADDRINUSE 端口占用探测靠它识别运行中的 lcb
263
+ if (path === '/api/status')
264
+ return json(res, 401, { error: '配置页已锁定', version: VERSION });
265
+ return json(res, 401, { error: '配置页已锁定,请先解锁' });
266
+ }
267
+ }
268
+ // ---- 配置页密码:登录 / 登出 / 会话元信息 / 改密 / 恢复初始密码 / 锁屏设置 ----
269
+ if (path === '/api/auth/verify' && req.method === 'POST') {
270
+ const ip = req.socket.remoteAddress ?? 'unknown';
271
+ const th = authThrottleState(ip);
272
+ if (th.blocked)
273
+ return json(res, 429, { error: `密码尝试过于频繁,请 ${th.retryAfter} 秒后再试` });
274
+ const body = await readJsonBody(req).catch(() => ({}));
275
+ if (!verifyPassword(configPath, String(body.password ?? ''))) {
276
+ registerAuthFailure(ip);
277
+ return json(res, 401, { error: '密码错误,请重试' });
278
+ }
279
+ clearAuthFailures(ip);
280
+ const meta = authMeta(configPath);
281
+ const token = createSession(meta.lockMinutes);
282
+ res.setHeader('Set-Cookie', sessionCookie(token, meta.lockMinutes));
283
+ // isDefault:仍为默认密码 123456 时前端每次登录都弹「修改默认密码」引导
284
+ return json(res, 200, { ok: true, isDefault: meta.isDefault, lockMinutes: meta.lockMinutes });
285
+ }
286
+ if (path === '/api/auth/meta' && req.method === 'GET') {
287
+ // 能过鉴权闸才可能走到这里 ⇒ 会话有效;供设置弹窗取锁屏设置与默认密码状态
288
+ return json(res, 200, { ok: true, ...authMeta(configPath) });
289
+ }
290
+ if (path === '/api/auth/logout' && req.method === 'POST') {
291
+ dropSession(sessionToken);
292
+ res.setHeader('Set-Cookie', clearSessionCookie());
293
+ return json(res, 200, { ok: true });
294
+ }
295
+ if (path === '/api/auth/change' && req.method === 'POST') {
296
+ const body = await readJsonBody(req);
297
+ const r = changePassword(configPath, String(body.old ?? ''), String(body.next ?? ''));
298
+ if (!r.ok)
299
+ return json(res, 400, { error: r.error });
300
+ dropOtherSessions(sessionToken); // 其他标签页/旧会话全部失效,当前页保持登录
301
+ return json(res, 200, { ok: true, isDefault: false });
302
+ }
303
+ if (path === '/api/auth/reset-default' && req.method === 'POST') {
304
+ // 恢复初始密码 123456:必须验当前密码(防锁屏状态下被内网他人一键重置)
305
+ const body = await readJsonBody(req);
306
+ if (!verifyPassword(configPath, String(body.old ?? '')))
307
+ return json(res, 401, { error: '当前密码不正确' });
308
+ resetPasswordToDefault(configPath);
309
+ dropOtherSessions(sessionToken);
310
+ return json(res, 200, { ok: true, isDefault: true });
311
+ }
312
+ if (path === '/api/auth/settings' && req.method === 'PUT') {
313
+ const body = await readJsonBody(req);
314
+ const r = setLockMinutes(configPath, Number(body.lockMinutes));
315
+ if (!r.ok)
316
+ return json(res, 400, { error: r.error });
317
+ const lockMinutes = Number(body.lockMinutes);
318
+ if (sessionToken)
319
+ touchSession(sessionToken, lockMinutes); // 立即按新 TTL 续期
320
+ return json(res, 200, { ok: true, lockMinutes });
321
+ }
322
+ // ---- 运行日志(概览「查看日志」抽屉):列表 + 尾部 N 行 + 日期范围聚合 ----
323
+ if (path === '/api/logs' && req.method === 'GET') {
324
+ return json(res, 200, listLogFiles(configPath));
325
+ }
326
+ if (path === '/api/logs/content' && req.method === 'GET') {
327
+ const from = url.searchParams.get('from');
328
+ const to = url.searchParams.get('to');
329
+ if (from || to) {
330
+ // 日期范围模式(前端主路径):from~to 每天日志依时间序合并
331
+ const r = tailLogRange(configPath, from || to || '', to || from || '', Number(url.searchParams.get('lines')) || 2000);
332
+ if ('error' in r)
333
+ return json(res, 400, { error: r.error });
334
+ return json(res, 200, r);
335
+ }
336
+ const file = url.searchParams.get('file');
337
+ if (!file)
338
+ return json(res, 400, { error: '缺少查询参数:请传 from/to(日期范围)或 file(单文件名)' });
339
+ const r = tailLogFile(configPath, file, Number(url.searchParams.get('lines')) || 500);
340
+ if ('error' in r)
341
+ return json(res, 400, { error: r.error });
342
+ return json(res, 200, r);
343
+ }
249
344
  // 静态页与模块:入口单文件 + js/css 目录服务(防穿越见下)
250
345
  if (req.method === 'GET' && (path === '/' || path === '/index.html')) {
251
346
  try {
@@ -400,6 +495,30 @@ async function handle(req, res, ctx) {
400
495
  return json(res, 400, { error: e instanceof Error ? e.message : String(e) });
401
496
  }
402
497
  }
498
+ // ---- 会话记录(只读查看器):LCB 本地 transcripts 时间线 + 话题聚合 + 飞书真群名 ----
499
+ if (path === '/api/chats/tree' && req.method === 'GET') {
500
+ if (firstRun)
501
+ return json(res, 404, { error: '尚无配置文件(首次安装),请先完成 bootstrap 向导' });
502
+ try {
503
+ return json(res, 200, await chatsTree(ctx.configPath));
504
+ }
505
+ catch (e) {
506
+ return json(res, 500, { error: `会话树扫描失败:${e instanceof Error ? e.message : String(e)}` });
507
+ }
508
+ }
509
+ if (path === '/api/chats/topics' && req.method === 'GET') {
510
+ return json(res, 200, chatTopics(url.searchParams.get('app') ?? '', url.searchParams.get('chat') ?? ''));
511
+ }
512
+ if (path === '/api/chats/timeline' && req.method === 'GET') {
513
+ const q = (k) => url.searchParams.get(k) ?? undefined;
514
+ const limit = Number(q('limit'));
515
+ return json(res, 200, chatTimeline(q('app') ?? '', q('chat') ?? '', {
516
+ date: q('date'),
517
+ session: q('session'),
518
+ user: q('user'),
519
+ ...(Number.isFinite(limit) && limit > 0 ? { limit } : {}),
520
+ }));
521
+ }
403
522
  // ---- B3 一键安装(§6.7 嵌入式流水线):后台执行 + 进度轮询;失败步骤可整链重跑(幂等跳过) ----
404
523
  if (path === '/api/memory/install' && req.method === 'POST') {
405
524
  if (firstRun)