@jesonliu/lark-claudecode-bridge 1.4.3 → 2.0.1

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 (56) hide show
  1. package/assets/web/css/main.css +774 -574
  2. package/assets/web/index.html +37 -17
  3. package/assets/web/js/auth.js +218 -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 +481 -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 +43 -46
  13. package/assets/web/js/pages/overview.js +426 -88
  14. package/assets/web/js/pages/permissions.js +121 -34
  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/access/access-control.d.ts +18 -0
  22. package/dist/access/access-control.js +29 -0
  23. package/dist/access/access-control.js.map +1 -1
  24. package/dist/bin/lcb.js +15 -0
  25. package/dist/bin/lcb.js.map +1 -1
  26. package/dist/feishu/name-cache.d.ts +15 -0
  27. package/dist/feishu/name-cache.js +82 -0
  28. package/dist/feishu/name-cache.js.map +1 -0
  29. package/dist/feishu/sibling-names.d.ts +1 -0
  30. package/dist/feishu/sibling-names.js +63 -0
  31. package/dist/feishu/sibling-names.js.map +1 -0
  32. package/dist/gateway/feishu-gateway.d.ts +24 -0
  33. package/dist/gateway/feishu-gateway.js +132 -0
  34. package/dist/gateway/feishu-gateway.js.map +1 -1
  35. package/dist/gateway/gateway-registry.d.ts +18 -0
  36. package/dist/gateway/gateway-registry.js +14 -0
  37. package/dist/gateway/gateway-registry.js.map +1 -0
  38. package/dist/index.d.ts +4 -0
  39. package/dist/index.js +52 -9
  40. package/dist/index.js.map +1 -1
  41. package/dist/types.d.ts +2 -0
  42. package/dist/web/chats-api.d.ts +93 -0
  43. package/dist/web/chats-api.js +603 -0
  44. package/dist/web/chats-api.js.map +1 -0
  45. package/dist/web/lifecycle.d.ts +21 -0
  46. package/dist/web/lifecycle.js +80 -3
  47. package/dist/web/lifecycle.js.map +1 -1
  48. package/dist/web/logs-api.d.ts +37 -0
  49. package/dist/web/logs-api.js +141 -0
  50. package/dist/web/logs-api.js.map +1 -0
  51. package/dist/web/server.js +196 -13
  52. package/dist/web/server.js.map +1 -1
  53. package/dist/web/webauth.d.ts +44 -0
  54. package/dist/web/webauth.js +221 -0
  55. package/dist/web/webauth.js.map +1 -0
  56. package/package.json +1 -1
@@ -36,6 +36,27 @@ export declare function spawnBridgeDetached(): {
36
36
  * server.ts 注入的 selfStop 优雅路径,不经本函数
37
37
  */
38
38
  export declare function stopBridgeByPid(pid: number): boolean;
39
+ export interface PortOccupier {
40
+ isLcb: boolean;
41
+ pid: number | null;
42
+ version: string | null;
43
+ }
44
+ /** 反查监听指定端口的进程 PID(回退路径:占用者未提供顶层 pid 时使用)。
45
+ * win32=netstat -ano(LISTENING 行末列 PID);darwin/linux= lsof -ti(缺失则放弃返回 null) */
46
+ export declare function pidListeningOnPortAsync(port: number): Promise<number | null>;
47
+ /** 探测 127.0.0.1:port 的占用者身份。端口无人监听(连接拒绝/超时)→ null */
48
+ export declare function probePortOccupier(port: number): Promise<PortOccupier | null>;
49
+ export interface PortFreeResult {
50
+ freed: boolean;
51
+ pid: number | null;
52
+ detail: string;
53
+ }
54
+ /**
55
+ * 端口实占清场:占用者是 LCB 进程(/api/status 带 version)→ 强制结束并轮询等端口释放;
56
+ * 非 LCB 占用者(别的软件占端口)不动,交由调用方按 EADDRINUSE 提示换端口。
57
+ * 防自杀:占用者即本进程时不杀(理论上启动清场阶段本进程尚未监听,纯防御)
58
+ */
59
+ export declare function freeLcbOccupiedPort(port: number, timeoutMs?: number): Promise<PortFreeResult>;
39
60
  /** 重启桥接器:spawn detached helper 由其等待旧进程退出后拉起新进程(跨平台统一路径) */
40
61
  export declare function restartBridgeWithHelper(oldPid: number): {
41
62
  ok: true;
@@ -1,8 +1,8 @@
1
- // 桥接器进程生命周期:PID 文件探活 / 后台拉起 / 跨进程停止 / 旧进程退出后重启。
1
+ // 桥接器进程生命周期:PID 文件探活 / 后台拉起 / 跨进程停止 / 旧进程退出后重启 / 端口实占清场。
2
2
  // 消费方:Web 配置页 /api/bridge/* 端点(页面按钮启停);startBridge 写/清 PID(src/index.ts)。
3
3
  // 路径参数可注入(默认 CONFIG_DIR),便于测试用临时目录
4
4
  import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
5
- import { spawn } from 'node:child_process';
5
+ import { execFile, spawn } from 'node:child_process';
6
6
  import { join } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { CONFIG_DIR } from '../config.js';
@@ -94,16 +94,93 @@ export function stopBridgeByPid(pid) {
94
94
  return false; // 已退出或无权限
95
95
  }
96
96
  }
97
+ /** 反查监听指定端口的进程 PID(回退路径:占用者未提供顶层 pid 时使用)。
98
+ * win32=netstat -ano(LISTENING 行末列 PID);darwin/linux= lsof -ti(缺失则放弃返回 null) */
99
+ export async function pidListeningOnPortAsync(port) {
100
+ const exec = (cmd, args, timeoutMs = 5000) => new Promise((resolve, reject) => {
101
+ execFile(cmd, args, { timeout: timeoutMs, windowsHide: true, maxBuffer: 4 * 1024 * 1024 }, (e, stdout) => e ? reject(e) : resolve(String(stdout)));
102
+ });
103
+ try {
104
+ if (process.platform === 'win32') {
105
+ const out = await exec('netstat', ['-ano']);
106
+ // 行形如:TCP 127.0.0.1:17317 0.0.0.0:0 LISTENING 2708(IPv6 为 [::]:17317)
107
+ for (const line of out.split('\n')) {
108
+ const m = line.match(new RegExp(String.raw `^\s*TCP\s+\S+:${port}\s+\S+\s+LISTENING\s+(\d+)`, 'i'));
109
+ if (m)
110
+ return Number.parseInt(m[1], 10);
111
+ }
112
+ return null;
113
+ }
114
+ const out = await exec('lsof', ['-ti', `tcp:${port}`]); // darwin / 装了 lsof 的 linux:每行一个 PID
115
+ const pid = Number.parseInt(out.trim().split('\n')[0] ?? '', 10);
116
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
117
+ }
118
+ catch {
119
+ return null; // netstat/lsof 缺失或执行失败:拿不到 PID,调用方降级为仅凭 version 判断
120
+ }
121
+ }
122
+ /** 探测 127.0.0.1:port 的占用者身份。端口无人监听(连接拒绝/超时)→ null */
123
+ export async function probePortOccupier(port) {
124
+ let st = null;
125
+ try {
126
+ const res = await fetch(`http://127.0.0.1:${port}/api/status`, {
127
+ signal: AbortSignal.timeout(2000),
128
+ });
129
+ st = (await res.json().catch(() => null));
130
+ }
131
+ catch {
132
+ return null; // 端口无人监听或非 HTTP 服务
133
+ }
134
+ const isLcb = typeof st?.version === 'string';
135
+ const respPid = typeof st?.pid === 'number' && Number.isInteger(st.pid) && st.pid > 0 ? st.pid : null;
136
+ const pid = respPid ?? (isLcb ? await pidListeningOnPortAsync(port) : null);
137
+ return { isLcb, pid, version: isLcb ? st.version : null };
138
+ }
139
+ /**
140
+ * 端口实占清场:占用者是 LCB 进程(/api/status 带 version)→ 强制结束并轮询等端口释放;
141
+ * 非 LCB 占用者(别的软件占端口)不动,交由调用方按 EADDRINUSE 提示换端口。
142
+ * 防自杀:占用者即本进程时不杀(理论上启动清场阶段本进程尚未监听,纯防御)
143
+ */
144
+ export async function freeLcbOccupiedPort(port, timeoutMs = 10_000) {
145
+ const occ = await probePortOccupier(port);
146
+ if (!occ)
147
+ return { freed: true, pid: null, detail: '端口空闲' };
148
+ if (!occ.isLcb) {
149
+ return { freed: false, pid: occ.pid, detail: `端口被非 LCB 进程占用${occ.pid ? `(PID ${occ.pid})` : ''},不动` };
150
+ }
151
+ if (occ.pid === null) {
152
+ return { freed: false, pid: null, detail: `端口被 LCB 进程(v${occ.version})占用,但未能定位 PID,无法自动清场` };
153
+ }
154
+ if (occ.pid === process.pid) {
155
+ return { freed: true, pid: occ.pid, detail: '占用者为本进程,跳过' };
156
+ }
157
+ if (!stopBridgeByPid(occ.pid)) {
158
+ return { freed: false, pid: occ.pid, detail: `向占用者(PID ${occ.pid})发送停止信号失败(无权限或已退出)` };
159
+ }
160
+ const t0 = Date.now();
161
+ while (Date.now() - t0 < timeoutMs) {
162
+ if ((await probePortOccupier(port)) === null) {
163
+ return { freed: true, pid: occ.pid, detail: `已结束占用端口的旧 LCB 进程(PID ${occ.pid})并释放端口` };
164
+ }
165
+ await new Promise((r) => setTimeout(r, 300));
166
+ }
167
+ return { freed: false, pid: occ.pid, detail: `已向占用者(PID ${occ.pid})发送停止信号,但端口 ${timeoutMs / 1000}s 内未释放` };
168
+ }
97
169
  /**
98
- * 内嵌重启 helper(CJS,node -e 执行):轮询旧进程退出 → 拉起新桥接器 → 自退。
170
+ * 内嵌重启 helper(CJS,node -e 执行):补刀杀旧进程 → 轮询确认退出 → 拉起新桥接器 → 自退。
99
171
  * 不能「先拉新再杀旧」:新进程的 Web 配置页会因端口仍被旧进程占用而启动失败,
100
172
  * 导致重启后页面失联。超时 30s 放弃(保留旧进程运行态,页面可重试)。
173
+ * 补刀延迟 500ms:embedded 模式 server.ts 的 selfStop 在 300ms 后优雅退出(关长连接/停受管
174
+ * 服务/清 PID),这刀落空(catch 忽略);独立模式原本「只等不杀」——旧进程永生导致必撞
175
+ * 页面 120s 重启超时(2026-10-17 定性),这刀是关键修复。Windows 上 SIGTERM=硬杀,风险
176
+ * 与 stopBridgeByPid 同口径(状态逐消息落盘,可控)。
101
177
  * #7:stdio 走 ignore,日志由进程内 console tee 自管到 logs/bridge-YYYY-MM-DD.log
102
178
  */
103
179
  const RESTART_HELPER = `
104
180
  const { spawn } = require('node:child_process');
105
181
  const [oldPid, entry] = process.argv.slice(1);
106
182
  const t0 = Date.now();
183
+ setTimeout(function () { try { process.kill(+oldPid, 'SIGTERM'); } catch (e) {} }, 500);
107
184
  (function wait() {
108
185
  try { process.kill(+oldPid, 0); } catch {
109
186
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../../src/web/lifecycle.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../../src/web/lifecycle.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ export interface LogFileEntry {
2
+ name: string;
3
+ size: number;
4
+ mtime: string;
5
+ }
6
+ export declare function listLogFiles(configPath: string): {
7
+ dir: string;
8
+ files: LogFileEntry[];
9
+ };
10
+ export interface LogTail {
11
+ name: string;
12
+ size: number;
13
+ /** true = 文件过大只读了末尾窗口,首行可能不完整(前端丢弃首行残缺即可) */
14
+ truncated: boolean;
15
+ lines: string[];
16
+ }
17
+ /** 取尾部 maxLines 行(默认 500,上限 5000) */
18
+ export declare function tailLogFile(configPath: string, name: string, maxLines: number): LogTail | {
19
+ error: string;
20
+ };
21
+ export interface LogRangeTail {
22
+ from: string;
23
+ to: string;
24
+ /** 实际并入的日志文件(升序 = 时间序),前端可提示覆盖了哪些天 */
25
+ files: string[];
26
+ /** true = 达到总量上限或某文件只读了末尾窗口 */
27
+ truncated: boolean;
28
+ lines: string[];
29
+ }
30
+ /**
31
+ * 日期范围聚合尾读:from~to(含两端)每天的 bridge-YYYY-MM-DD.log 依时间序合并。
32
+ * 每文件最多 RANGE_PER_FILE_LINES 行(防止单日巨日志吃光配额),总量再按 maxLines 截尾。
33
+ * 单日文件过大只读末尾窗口时同样标记 truncated。
34
+ */
35
+ export declare function tailLogRange(configPath: string, from: string, to: string, maxLines: number): LogRangeTail | {
36
+ error: string;
37
+ };
@@ -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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"}
@@ -25,10 +25,14 @@ import { ensureRuntimeDirs } from '../util/runtime-dirs.js';
25
25
  import { builtinCommands, createSlashApiClient, ensureBuiltins, expectedCommands, syncSlashCommands } from '../feishu/slash-commands.js';
26
26
  import { refreshMarketplaceIndex, removeMarketplaceIfUnused, resolvePluginMarketplace, runPluginCli, updateAllPlugins } from '../executor/plugin-manager.js';
27
27
  import { invalidatePluginCache, listAvailablePlugins, listInstalledPlugins, loadEnabledPlugins } from '../executor/plugin-discovery.js';
28
- import { bridgeStatus, resolveLcbEntry, restartBridgeWithHelper, spawnBridgeDetached, stopBridgeByPid } from './lifecycle.js';
28
+ import { bridgeStatus, probePortOccupier, resolveLcbEntry, restartBridgeWithHelper, spawnBridgeDetached, stopBridgeByPid } from './lifecycle.js';
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, fetchUserNames } from './chats-api.js';
33
+ import { AccessControl } from '../access/access-control.js';
34
+ import { authMeta, authThrottleState, changePassword, clearAuthFailures, clearSessionCookie, createSession, dropOtherSessions, dropSession, readSessionToken, registerAuthFailure, resetPasswordToDefault, sessionCookie, setLockMinutes, touchSession, verifyPassword, } from './webauth.js';
35
+ import { listLogFiles, tailLogFile, tailLogRange } from './logs-api.js';
32
36
  import { deleteMemoryUser, initMemoryUsers, listMemoryUsers, resetUserKey, revealRootKey, revealUserKey, } from './memory-users-api.js';
33
37
  import { MANAGED_OV_CONF, getInstallState, runInstall, runStartPipeline, stopManagedServer } from '../memory-installer.js';
34
38
  import { checkLarkCliAuth, checkLarkCliLatest, detectLarkCli, detectLarkCliSkill, getLarkCliConfigStatus, getLarkCliDeviceStatus, runLarkCliActionFlow, startLarkCliConfigFlow, startLarkCliDeviceAuth, } from '../lark-cli-manager.js';
@@ -122,7 +126,8 @@ function listSubdirs(input) {
122
126
  const dirs = readdirSync(abs, { withFileTypes: true })
123
127
  .filter((e) => e.isDirectory())
124
128
  .map((e) => e.name)
125
- .sort((a, b) => a.localeCompare(b));
129
+ // 普通目录在前、点开头隐藏目录殿后(目录选择弹层网格的浏览主路径是普通目录)
130
+ .sort((a, b) => (a.startsWith('.') ? 1 : 0) - (b.startsWith('.') ? 1 : 0) || a.localeCompare(b));
126
131
  return { path: abs, parent: parentOf(abs), dirs };
127
132
  }
128
133
  catch (e) {
@@ -168,21 +173,17 @@ export function shouldShowTunnelHint(host) {
168
173
  return true;
169
174
  return process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
170
175
  }
171
- /** EADDRINUSE 报错文案:探测占用者身份(是否旧 lcb 配置页进程)+ 按平台给排查命令。
176
+ /** EADDRINUSE 报错文案:探测占用者身份(是否旧 lcb 进程 + PID)+ 按平台给排查命令。
172
177
  * 裸 Node 错误(listen EADDRINUSE :::17317)对首装用户完全不可操作 */
173
178
  async function friendlyAddrInUse(host, port) {
174
- const disp = host === '::1' ? '[::1]' : host;
179
+ void host; // 占用者探测固定走回环(probePortOccupier 内部 127.0.0.1),host 仅保留签名兼容
175
180
  let occupier = '';
176
- try {
177
- 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
- }
183
- }
181
+ const occ = await probePortOccupier(port).catch(() => null);
182
+ if (occ?.isLcb) {
183
+ // 401 也带 version(鉴权网关对 /api/status 特批附版本):加锁后的旧进程仍可被识别;
184
+ // PID 直取占用者自报(顶层 pid),lcb start 的启动清场(freeLcbOccupiedPort)据此自动接管
185
+ occupier = `(占用者疑似旧 lcb 进程 v${occ.version}${occ.pid ? `,PID ${occ.pid}` : ''}——npm 更新后旧进程未退出,需手动结束)`;
184
186
  }
185
- catch { /* 探测失败/非 lcb 服务:走通用文案 */ }
186
187
  const probe = process.platform === 'win32'
187
188
  ? `netstat -ano | findstr :${port} 查 PID、taskkill /F /PID <pid> 停止`
188
189
  : process.platform === 'darwin'
@@ -238,6 +239,7 @@ export async function startWebServer(opts = {}) {
238
239
  async function handle(req, res, ctx) {
239
240
  const url = new URL(req.url ?? '/', 'http://localhost');
240
241
  const path = url.pathname;
242
+ const configPath = ctx.configPath; // 短别名:鉴权段大量引用
241
243
  // 每请求现查(不能用启动快照):bootstrap 写盘成功后,后续请求立即按「已有配置」处理
242
244
  const firstRun = !existsSync(ctx.configPath);
243
245
  // 安全闸:Host / Origin 校验(防 DNS rebinding 拿本机页面读写配置)
@@ -246,6 +248,98 @@ async function handle(req, res, ctx) {
246
248
  res.end('Forbidden');
247
249
  return;
248
250
  }
251
+ // ---- 配置页密码闸:静态资源(html/js/css)公开(页面源码不含秘密);锁屏本身在前端,
252
+ // 但数据全部挡在 API 层——所有 /api/*(除登录)必须携带有效会话 cookie,否则 401。
253
+ // 首装向导 /api/bootstrap 同样被拦:内网他人无法抢在主人之前 bootstrap 配置 ----
254
+ const sessionToken = readSessionToken(req.headers.cookie);
255
+ if (path.startsWith('/api/') && path !== '/api/auth/verify') {
256
+ // 每请求滑动续期:TTL 取当前锁屏设置(改设置即刻约束既有会话;webauth 文件随读随取,
257
+ // 顺带让 `lcb reset-password` 的改密对运行中的进程即时生效)
258
+ const okSession = !!sessionToken && touchSession(sessionToken, authMeta(configPath).lockMinutes);
259
+ if (!okSession) {
260
+ // /api/status 在 401 里特批附 version+pid:EADDRINUSE 端口占用探测靠它们识别运行中的
261
+ // lcb 并定位 PID(freeLcbOccupiedPort 自动清场、friendlyAddrInUse 报文案)
262
+ if (path === '/api/status')
263
+ return json(res, 401, { error: '配置页已锁定', version: VERSION, pid: process.pid });
264
+ return json(res, 401, { error: '配置页已锁定,请先解锁' });
265
+ }
266
+ }
267
+ // ---- 配置页密码:登录 / 登出 / 会话元信息 / 改密 / 恢复初始密码 / 锁屏设置 ----
268
+ if (path === '/api/auth/verify' && req.method === 'POST') {
269
+ const ip = req.socket.remoteAddress ?? 'unknown';
270
+ const th = authThrottleState(ip);
271
+ if (th.blocked)
272
+ return json(res, 429, { error: `密码尝试过于频繁,请 ${th.retryAfter} 秒后再试` });
273
+ const body = await readJsonBody(req).catch(() => ({}));
274
+ if (!verifyPassword(configPath, String(body.password ?? ''))) {
275
+ registerAuthFailure(ip);
276
+ return json(res, 401, { error: '密码错误,请重试' });
277
+ }
278
+ clearAuthFailures(ip);
279
+ const meta = authMeta(configPath);
280
+ const token = createSession(meta.lockMinutes);
281
+ res.setHeader('Set-Cookie', sessionCookie(token, meta.lockMinutes));
282
+ // isDefault:仍为默认密码 123456 时前端每次登录都弹「修改默认密码」引导
283
+ return json(res, 200, { ok: true, isDefault: meta.isDefault, lockMinutes: meta.lockMinutes });
284
+ }
285
+ if (path === '/api/auth/meta' && req.method === 'GET') {
286
+ // 能过鉴权闸才可能走到这里 ⇒ 会话有效;供设置弹窗取锁屏设置与默认密码状态
287
+ return json(res, 200, { ok: true, ...authMeta(configPath) });
288
+ }
289
+ if (path === '/api/auth/logout' && req.method === 'POST') {
290
+ dropSession(sessionToken);
291
+ res.setHeader('Set-Cookie', clearSessionCookie());
292
+ return json(res, 200, { ok: true });
293
+ }
294
+ if (path === '/api/auth/change' && req.method === 'POST') {
295
+ const body = await readJsonBody(req);
296
+ const r = changePassword(configPath, String(body.old ?? ''), String(body.next ?? ''));
297
+ if (!r.ok)
298
+ return json(res, 400, { error: r.error });
299
+ dropOtherSessions(sessionToken); // 其他标签页/旧会话全部失效,当前页保持登录
300
+ return json(res, 200, { ok: true, isDefault: false });
301
+ }
302
+ if (path === '/api/auth/reset-default' && req.method === 'POST') {
303
+ // 恢复初始密码 123456:必须验当前密码(防锁屏状态下被内网他人一键重置)
304
+ const body = await readJsonBody(req);
305
+ if (!verifyPassword(configPath, String(body.old ?? '')))
306
+ return json(res, 401, { error: '当前密码不正确' });
307
+ resetPasswordToDefault(configPath);
308
+ dropOtherSessions(sessionToken);
309
+ return json(res, 200, { ok: true, isDefault: true });
310
+ }
311
+ if (path === '/api/auth/settings' && req.method === 'PUT') {
312
+ const body = await readJsonBody(req);
313
+ const r = setLockMinutes(configPath, Number(body.lockMinutes));
314
+ if (!r.ok)
315
+ return json(res, 400, { error: r.error });
316
+ const lockMinutes = Number(body.lockMinutes);
317
+ if (sessionToken)
318
+ touchSession(sessionToken, lockMinutes); // 立即按新 TTL 续期
319
+ return json(res, 200, { ok: true, lockMinutes });
320
+ }
321
+ // ---- 运行日志(概览「查看日志」抽屉):列表 + 尾部 N 行 + 日期范围聚合 ----
322
+ if (path === '/api/logs' && req.method === 'GET') {
323
+ return json(res, 200, listLogFiles(configPath));
324
+ }
325
+ if (path === '/api/logs/content' && req.method === 'GET') {
326
+ const from = url.searchParams.get('from');
327
+ const to = url.searchParams.get('to');
328
+ if (from || to) {
329
+ // 日期范围模式(前端主路径):from~to 每天日志依时间序合并
330
+ const r = tailLogRange(configPath, from || to || '', to || from || '', Number(url.searchParams.get('lines')) || 2000);
331
+ if ('error' in r)
332
+ return json(res, 400, { error: r.error });
333
+ return json(res, 200, r);
334
+ }
335
+ const file = url.searchParams.get('file');
336
+ if (!file)
337
+ return json(res, 400, { error: '缺少查询参数:请传 from/to(日期范围)或 file(单文件名)' });
338
+ const r = tailLogFile(configPath, file, Number(url.searchParams.get('lines')) || 500);
339
+ if ('error' in r)
340
+ return json(res, 400, { error: r.error });
341
+ return json(res, 200, r);
342
+ }
249
343
  // 静态页与模块:入口单文件 + js/css 目录服务(防穿越见下)
250
344
  if (req.method === 'GET' && (path === '/' || path === '/index.html')) {
251
345
  try {
@@ -310,6 +404,9 @@ async function handle(req, res, ctx) {
310
404
  }
311
405
  return json(res, 200, {
312
406
  version: VERSION, uptimeMs: Date.now() - startedAt, configPath: ctx.configPath,
407
+ // 顶层 pid=应答进程自身:前端重启判定/端口占用探测(freeLcbOccupiedPort)直接取用,
408
+ // 不必回退 netstat 反查。与 bridge.pid(PID 文件内容)区分:独立模式下两者可能不同
409
+ pid: process.pid,
313
410
  firstRun, embedded: ctx.embedded,
314
411
  bridge: { ...bridgeStatus(), spawnable: resolveLcbEntry().ok },
315
412
  server: { host: ctx.host, port: ctx.port },
@@ -400,6 +497,30 @@ async function handle(req, res, ctx) {
400
497
  return json(res, 400, { error: e instanceof Error ? e.message : String(e) });
401
498
  }
402
499
  }
500
+ // ---- 会话记录(只读查看器):LCB 本地 transcripts 时间线 + 话题聚合 + 飞书真群名 ----
501
+ if (path === '/api/chats/tree' && req.method === 'GET') {
502
+ if (firstRun)
503
+ return json(res, 404, { error: '尚无配置文件(首次安装),请先完成 bootstrap 向导' });
504
+ try {
505
+ return json(res, 200, await chatsTree(ctx.configPath));
506
+ }
507
+ catch (e) {
508
+ return json(res, 500, { error: `会话树扫描失败:${e instanceof Error ? e.message : String(e)}` });
509
+ }
510
+ }
511
+ if (path === '/api/chats/topics' && req.method === 'GET') {
512
+ return json(res, 200, chatTopics(url.searchParams.get('app') ?? '', url.searchParams.get('chat') ?? ''));
513
+ }
514
+ if (path === '/api/chats/timeline' && req.method === 'GET') {
515
+ const q = (k) => url.searchParams.get(k) ?? undefined;
516
+ const limit = Number(q('limit'));
517
+ return json(res, 200, chatTimeline(q('app') ?? '', q('chat') ?? '', {
518
+ date: q('date'),
519
+ session: q('session'),
520
+ user: q('user'),
521
+ ...(Number.isFinite(limit) && limit > 0 ? { limit } : {}),
522
+ }));
523
+ }
403
524
  // ---- B3 一键安装(§6.7 嵌入式流水线):后台执行 + 进度轮询;失败步骤可整链重跑(幂等跳过) ----
404
525
  if (path === '/api/memory/install' && req.method === 'POST') {
405
526
  if (firstRun)
@@ -1205,6 +1326,68 @@ async function handle(req, res, ctx) {
1205
1326
  killTree();
1206
1327
  return json(res, 200, result);
1207
1328
  }
1329
+ // ---- 接入申请(配对授权):配置页「权限 → 接入申请」。数据层直读/直写 access.json——
1330
+ // 桥内 AccessControl 每条消息前 reload,此处批准/拒绝后用户下一条消息即放行;
1331
+ // 终端 lcb pair 双通道并存。申请人真名按 appId 借网关通讯录解析(失败降级原始 id) ----
1332
+ if (path === '/api/access/pending' && req.method === 'GET') {
1333
+ const items = AccessControl.load(join(CONFIG_DIR, 'access.json')).listPendingDetailed();
1334
+ const byApp = new Map();
1335
+ for (const it of items) {
1336
+ if (!it.appId)
1337
+ continue;
1338
+ byApp.set(it.appId, [...(byApp.get(it.appId) ?? []), it.userId]);
1339
+ }
1340
+ const names = new Map();
1341
+ for (const [appId, ids] of byApp) {
1342
+ try {
1343
+ for (const [uid, n] of await fetchUserNames(appId, ids))
1344
+ names.set(uid, n);
1345
+ }
1346
+ catch { /* 解析失败降级原始 id 展示 */ }
1347
+ }
1348
+ let appNames = new Map();
1349
+ try {
1350
+ appNames = new Map((loadConfig(ctx.configPath)?.apps ?? []).map((a) => [a.appId, a.name || a.appId]));
1351
+ }
1352
+ catch { /* 配置损坏降级 appId */ }
1353
+ return json(res, 200, {
1354
+ pending: items.map((it) => {
1355
+ const n = names.get(it.userId);
1356
+ return {
1357
+ userId: it.userId,
1358
+ name: it.name,
1359
+ code: it.code,
1360
+ appId: it.appId,
1361
+ appName: it.appId ? appNames.get(it.appId) : undefined,
1362
+ displayName: n && n !== it.userId ? n : undefined,
1363
+ expiresAt: it.expiresAt,
1364
+ expiresInSec: Math.max(0, Math.round((it.expiresAt - Date.now()) / 1000)),
1365
+ };
1366
+ }),
1367
+ });
1368
+ }
1369
+ if (path === '/api/access/pending/approve' && req.method === 'POST') {
1370
+ const body = await readJsonBody(req);
1371
+ const code = String(body.code ?? '').trim();
1372
+ if (!/^\d{6}$/.test(code))
1373
+ return json(res, 400, { error: '配对码格式不正确(6 位数字)' });
1374
+ const r = AccessControl.load(join(CONFIG_DIR, 'access.json')).approvePairing(code);
1375
+ if (!r.ok)
1376
+ return json(res, 404, { error: r.error ?? '配对码无效或已过期' });
1377
+ console.log(`[配对] 配置页批准配对码 ${code}(用户 ${r.userId}${r.isFirstAdmin ? ',成为 admin' : ',member'})`);
1378
+ return json(res, 200, { ok: true, userId: r.userId, isFirstAdmin: r.isFirstAdmin });
1379
+ }
1380
+ if (path === '/api/access/pending/reject' && req.method === 'POST') {
1381
+ const body = await readJsonBody(req);
1382
+ const code = String(body.code ?? '').trim();
1383
+ if (!/^\d{6}$/.test(code))
1384
+ return json(res, 400, { error: '配对码格式不正确(6 位数字)' });
1385
+ const ok = AccessControl.load(join(CONFIG_DIR, 'access.json')).rejectPairing(code);
1386
+ if (!ok)
1387
+ return json(res, 404, { error: '配对码无效或已过期' });
1388
+ console.log(`[配对] 配置页拒绝配对码 ${code}`);
1389
+ return json(res, 200, { ok: true });
1390
+ }
1208
1391
  // ---- 桥接器进程启停(页面托管)。embedded 与 lcb ui 独立模式语义不同:embedded 停止/重启
1209
1392
  // 会连带本页所在进程,先回响应再走 selfStop 优雅关闭;独立模式经 PID 文件跨进程操作 ----
1210
1393
  if (path === '/api/bridge/action' && req.method === 'POST') {