@jesonliu/lark-claudecode-bridge 0.11.0 → 0.14.2

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 (82) hide show
  1. package/README.md +89 -49
  2. package/assets/web/index.html +1769 -0
  3. package/config.example.yaml +22 -2
  4. package/dist/auth-precheck.d.ts +5 -2
  5. package/dist/auth-precheck.js +8 -2
  6. package/dist/auth-precheck.js.map +1 -1
  7. package/dist/bin/lcb.js +32 -3
  8. package/dist/bin/lcb.js.map +1 -1
  9. package/dist/claude-config.d.ts +24 -0
  10. package/dist/claude-config.js +143 -0
  11. package/dist/claude-config.js.map +1 -0
  12. package/dist/cli/setup-wizard.d.ts +1 -0
  13. package/dist/cli/setup-wizard.js +4 -0
  14. package/dist/cli/setup-wizard.js.map +1 -1
  15. package/dist/config-defaults.d.ts +4 -0
  16. package/dist/config-defaults.js +15 -0
  17. package/dist/config-defaults.js.map +1 -0
  18. package/dist/config.d.ts +7 -0
  19. package/dist/config.js +207 -6
  20. package/dist/config.js.map +1 -1
  21. package/dist/executor/claude-executor.d.ts +7 -2
  22. package/dist/executor/claude-executor.js +62 -17
  23. package/dist/executor/claude-executor.js.map +1 -1
  24. package/dist/executor/output-collector.d.ts +2 -0
  25. package/dist/executor/output-collector.js +15 -3
  26. package/dist/executor/output-collector.js.map +1 -1
  27. package/dist/executor/permission-gate.d.ts +37 -3
  28. package/dist/executor/permission-gate.js +86 -16
  29. package/dist/executor/permission-gate.js.map +1 -1
  30. package/dist/executor/plugin-discovery.d.ts +30 -0
  31. package/dist/executor/plugin-discovery.js +68 -1
  32. package/dist/executor/plugin-discovery.js.map +1 -1
  33. package/dist/executor/plugin-manager.d.ts +35 -0
  34. package/dist/executor/plugin-manager.js +109 -0
  35. package/dist/executor/plugin-manager.js.map +1 -0
  36. package/dist/feishu/slash-commands.d.ts +77 -0
  37. package/dist/feishu/slash-commands.js +188 -0
  38. package/dist/feishu/slash-commands.js.map +1 -0
  39. package/dist/gateway/card-builder.d.ts +18 -0
  40. package/dist/gateway/card-builder.js +61 -1
  41. package/dist/gateway/card-builder.js.map +1 -1
  42. package/dist/gateway/feishu-gateway.d.ts +10 -0
  43. package/dist/gateway/feishu-gateway.js +4 -0
  44. package/dist/gateway/feishu-gateway.js.map +1 -1
  45. package/dist/gateway/progress-card.d.ts +8 -0
  46. package/dist/gateway/progress-card.js +16 -0
  47. package/dist/gateway/progress-card.js.map +1 -1
  48. package/dist/index.d.ts +2 -0
  49. package/dist/index.js +210 -28
  50. package/dist/index.js.map +1 -1
  51. package/dist/session/commands.d.ts +16 -1
  52. package/dist/session/commands.js +77 -17
  53. package/dist/session/commands.js.map +1 -1
  54. package/dist/session/plugin-command.d.ts +14 -0
  55. package/dist/session/plugin-command.js +110 -0
  56. package/dist/session/plugin-command.js.map +1 -0
  57. package/dist/session/session-store.d.ts +14 -1
  58. package/dist/session/session-store.js +15 -0
  59. package/dist/session/session-store.js.map +1 -1
  60. package/dist/types.d.ts +69 -1
  61. package/dist/util/open-browser.d.ts +1 -0
  62. package/dist/util/open-browser.js +20 -0
  63. package/dist/util/open-browser.js.map +1 -0
  64. package/dist/util/server-url.d.ts +3 -0
  65. package/dist/util/server-url.js +10 -0
  66. package/dist/util/server-url.js.map +1 -0
  67. package/dist/web/config-api.d.ts +52 -0
  68. package/dist/web/config-api.js +186 -0
  69. package/dist/web/config-api.js.map +1 -0
  70. package/dist/web/lifecycle.d.ts +52 -0
  71. package/dist/web/lifecycle.js +167 -0
  72. package/dist/web/lifecycle.js.map +1 -0
  73. package/dist/web/model-list.d.ts +36 -0
  74. package/dist/web/model-list.js +101 -0
  75. package/dist/web/model-list.js.map +1 -0
  76. package/dist/web/server.d.ts +22 -0
  77. package/dist/web/server.js +618 -0
  78. package/dist/web/server.js.map +1 -0
  79. package/dist/web/update.d.ts +26 -0
  80. package/dist/web/update.js +64 -0
  81. package/dist/web/update.js.map +1 -0
  82. package/package.json +2 -1
@@ -0,0 +1,618 @@
1
+ // Web 配置页 server:node:http 零新依赖,随 lcb start 常驻 / lcb ui 独立启动。
2
+ // 安全基线:默认只绑 127.0.0.1;Host/Origin 校验防 DNS rebinding;body ≤1MB;
3
+ // secret 永不出进程(GET 脱敏回显,PUT 空值=不修改)。改动写盘后由现有热重载器/重启消费
4
+ import { createServer } from 'node:http';
5
+ import { existsSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from 'node:fs';
6
+ import { dirname, join, parse, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { parseDocument, stringify } from 'yaml';
9
+ import { CONFIG_PATH, SLASH_COMMAND_RE, loadConfig, parseConfigText } from '../config.js';
10
+ import { DEFAULT_CLAUDE_DIR, initManagedClaudeDir, resolveClaudeDir } from '../claude-config.js';
11
+ import { hasClaudeAuth } from '../auth-precheck.js';
12
+ import { defaultPermissionsDoc, defaultServerDoc } from '../config-defaults.js';
13
+ import { VERSION } from '../version.js';
14
+ import { appStatusSummary, applyPermissionDisplayDefaults, applySecrets, claudeSettingsSummary, computeRestartRequired, docForClient, isAllowedHost, isAllowedOrigin } from './config-api.js';
15
+ import { fetchModelList, resolveModelFetchParams } from './model-list.js';
16
+ import { SLASH_COMMAND_META } from '../session/commands.js';
17
+ import { DEFAULT_ALLOW_TOOLS_LIST, DEFAULT_DANGEROUS_COMMAND_SOURCES } from '../executor/permission-gate.js';
18
+ import { openBrowser } from '../util/open-browser.js';
19
+ import { builtinCommands, createSlashApiClient, ensureBuiltins, expectedCommands, syncSlashCommands } from '../feishu/slash-commands.js';
20
+ import { runPluginCli, updateAllPlugins } from '../executor/plugin-manager.js';
21
+ import { invalidatePluginCache, listAvailablePlugins, listInstalledPlugins } from '../executor/plugin-discovery.js';
22
+ import { bridgeStatus, resolveLcbEntry, restartBridgeWithHelper, spawnBridgeDetached, stopBridgeByPid } from './lifecycle.js';
23
+ import { checkUpdate, installMode, runUpdate } from './update.js';
24
+ /** PUT /api/config 参与整段替换的顶级键;body 未携带的键保持磁盘原文(含注释) */
25
+ const PUT_SECTIONS = ['apps', 'workspaces', 'defaults', 'concurrency', 'permissions', 'server', 'claude', 'slash_commands', 'transcripts'];
26
+ const INDEX_HTML_PATH = fileURLToPath(new URL('../../assets/web/index.html', import.meta.url));
27
+ const BODY_LIMIT = 1024 * 1024;
28
+ const startedAt = Date.now();
29
+ function json(res, status, body) {
30
+ const text = JSON.stringify(body);
31
+ res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
32
+ res.end(text);
33
+ }
34
+ function readBody(req) {
35
+ return new Promise((resolve, reject) => {
36
+ let size = 0;
37
+ const chunks = [];
38
+ req.on('data', (c) => {
39
+ size += c.length;
40
+ if (size > BODY_LIMIT) {
41
+ reject(new Error('请求体超过 1MB 上限'));
42
+ req.destroy();
43
+ return;
44
+ }
45
+ chunks.push(c);
46
+ });
47
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
48
+ req.on('error', reject);
49
+ });
50
+ }
51
+ function readJsonBody(req) {
52
+ return readBody(req).then((text) => {
53
+ if (!text.trim())
54
+ return {};
55
+ const v = JSON.parse(text);
56
+ if (v === null || typeof v !== 'object' || Array.isArray(v))
57
+ throw new Error('请求体必须为 JSON 对象');
58
+ return v;
59
+ });
60
+ }
61
+ /** 原子写盘(tmp + rename),与 claude-config 同款防半截文件 */
62
+ function writeAtomic(path, text) {
63
+ const tmp = `${path}.tmp`;
64
+ writeFileSync(tmp, text, 'utf8');
65
+ renameSync(tmp, path);
66
+ }
67
+ /** 盘符根(C:\)/ posix 根(/)无上级 → null(前端据此回到根/盘符列表层) */
68
+ function parentOf(abs) {
69
+ return parse(abs).root === abs ? null : dirname(abs);
70
+ }
71
+ /**
72
+ * 目录浏览(工作区路径选择弹层数据源):列出某路径下的子目录与上级导航。
73
+ * 只返回目录名不返回文件。手输路径跳转是浏览常态操作,出错(不存在/不是目录/
74
+ * 无权限)不打断弹层——200 + error 字段内联展示;盘符根之上的「此电脑」层用
75
+ * drives(Windows 专属,逐盘符 existsSync 探测)。
76
+ */
77
+ function listSubdirs(input) {
78
+ if (!input.trim()) {
79
+ if (process.platform === 'win32') {
80
+ const drives = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map((d) => `${d}:\\`).filter((d) => existsSync(d));
81
+ return { path: '', parent: null, dirs: [], drives };
82
+ }
83
+ return listSubdirs('/');
84
+ }
85
+ const abs = resolve(input);
86
+ let st;
87
+ try {
88
+ st = statSync(abs);
89
+ }
90
+ catch {
91
+ return { path: abs, parent: parentOf(abs), dirs: [], error: '路径不存在' };
92
+ }
93
+ if (!st.isDirectory()) {
94
+ return { path: abs, parent: parentOf(abs), dirs: [], error: '该路径不是目录' };
95
+ }
96
+ try {
97
+ const dirs = readdirSync(abs, { withFileTypes: true })
98
+ .filter((e) => e.isDirectory())
99
+ .map((e) => e.name)
100
+ .sort((a, b) => a.localeCompare(b));
101
+ return { path: abs, parent: parentOf(abs), dirs };
102
+ }
103
+ catch (e) {
104
+ return { path: abs, parent: parentOf(abs), dirs: [], error: `无法读取:${e instanceof Error ? e.message : String(e)}` };
105
+ }
106
+ }
107
+ /** 读 Claude 配置目录下 settings.json(不存在/损坏 → undefined;内容仅供脱敏摘要) */
108
+ function readSettingsJson(dir) {
109
+ try {
110
+ const p = join(dir, 'settings.json');
111
+ if (!existsSync(p))
112
+ return undefined;
113
+ return JSON.parse(readFileSync(p, 'utf8'));
114
+ }
115
+ catch {
116
+ return undefined;
117
+ }
118
+ }
119
+ /** managed 模式时把 claude 段最新认证立即同步进自管目录 settings.json(后续任务即生效,无需等重启) */
120
+ function syncManagedClaude(config) {
121
+ if (config.claude?.mode === 'managed')
122
+ initManagedClaudeDir(config);
123
+ }
124
+ /** 从磁盘原文读 snake_case doc(PUT 的 secret 回填基准) */
125
+ function readRawDoc(configPath) {
126
+ if (!existsSync(configPath))
127
+ return null;
128
+ try {
129
+ return parseDocument(readFileSync(configPath, 'utf8')).toJS();
130
+ }
131
+ catch {
132
+ return null; // 磁盘文件损坏:PUT 走全量重建路径(parseDocument 抛错由调用方处理)
133
+ }
134
+ }
135
+ export async function startWebServer(opts = {}) {
136
+ const serverCfg = opts.server ?? defaultServerDoc();
137
+ if (serverCfg.enabled === false)
138
+ return null;
139
+ const configPath = opts.configPath ?? CONFIG_PATH;
140
+ const host = serverCfg.host ?? '127.0.0.1';
141
+ const port = serverCfg.port ?? 17317;
142
+ const server = createServer((req, res) => {
143
+ void handle(req, res, { configPath, host, port, embedded: opts.embedded ?? false, appsStarted: opts.appsStarted, selfStop: opts.selfStop }).catch((e) => {
144
+ console.error('[配置页] 请求处理异常:', e);
145
+ if (!res.headersSent)
146
+ json(res, 500, { error: e instanceof Error ? e.message : String(e) });
147
+ });
148
+ });
149
+ await new Promise((resolve, reject) => {
150
+ server.once('error', reject);
151
+ server.listen(port, host, () => resolve());
152
+ });
153
+ const actualPort = server.address().port;
154
+ const url = `http://${host === '::1' ? '[::1]' : host}:${actualPort}`;
155
+ console.log(`🧭 配置页已就绪:${url}${opts.embedded ? '' : '(lcb ui 独立模式,桥接器未启动)'}`);
156
+ if (opts.autoOpen)
157
+ openBrowser(url);
158
+ return {
159
+ url,
160
+ port: actualPort,
161
+ close: () => server.close(),
162
+ };
163
+ }
164
+ async function handle(req, res, ctx) {
165
+ const url = new URL(req.url ?? '/', 'http://localhost');
166
+ const path = url.pathname;
167
+ // 每请求现查(不能用启动快照):bootstrap 写盘成功后,后续请求立即按「已有配置」处理
168
+ const firstRun = !existsSync(ctx.configPath);
169
+ // 安全闸:Host / Origin 校验(防 DNS rebinding 拿本机页面读写配置)
170
+ if (!isAllowedHost(req.headers.host, ctx.host) || !isAllowedOrigin(req.headers.origin, ctx.host, ctx.port)) {
171
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
172
+ res.end('Forbidden');
173
+ return;
174
+ }
175
+ // 静态页(单文件,无目录遍历面)
176
+ if (req.method === 'GET' && (path === '/' || path === '/index.html')) {
177
+ try {
178
+ const html = readFileSync(INDEX_HTML_PATH, 'utf8');
179
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
180
+ res.end(html);
181
+ }
182
+ catch {
183
+ res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
184
+ res.end('配置页资源缺失(assets/web/index.html)');
185
+ }
186
+ return;
187
+ }
188
+ if (path === '/api/status' && req.method === 'GET') {
189
+ let claude = { mode: 'inherit', hasAuth: null };
190
+ let apps = [];
191
+ if (!firstRun) {
192
+ try {
193
+ const config = loadConfig(ctx.configPath);
194
+ const dir = resolveClaudeDir(config);
195
+ claude = {
196
+ mode: config.claude?.mode ?? 'inherit',
197
+ hasAuth: hasClaudeAuth(process.env, dir),
198
+ // 当前实际生效配置(inherit=~/.claude 状态 / managed=自管目录),凭证脱敏不出进程
199
+ current: claudeSettingsSummary(readSettingsJson(dir)),
200
+ settingsPath: join(dir, 'settings.json'),
201
+ };
202
+ apps = appStatusSummary(config.apps, ctx.appsStarted);
203
+ }
204
+ catch { /* 磁盘配置损坏:status 仍可用(页面提示重配) */ }
205
+ }
206
+ return json(res, 200, {
207
+ version: VERSION, uptimeMs: Date.now() - startedAt, configPath: ctx.configPath,
208
+ firstRun, embedded: ctx.embedded,
209
+ bridge: { ...bridgeStatus(), spawnable: resolveLcbEntry().ok },
210
+ server: { host: ctx.host, port: ctx.port },
211
+ claude, apps,
212
+ // 内置命令清单(概览「常用命令」卡)与权限运行时默认(权限页「恢复默认」)——均与配置无关的静态单源
213
+ commands: Object.entries(SLASH_COMMAND_META).map(([command, meta]) => ({ command, description: meta.description })),
214
+ permissionDefaults: {
215
+ allowTools: [...DEFAULT_ALLOW_TOOLS_LIST],
216
+ dangerousCommands: [...DEFAULT_DANGEROUS_COMMAND_SOURCES],
217
+ },
218
+ });
219
+ }
220
+ // ---- 目录浏览(工作区路径选择弹层;不依赖配置文件,首装向导同样可用) ----
221
+ if (path === '/api/fs/dirs' && req.method === 'GET') {
222
+ return json(res, 200, listSubdirs(url.searchParams.get('p') ?? ''));
223
+ }
224
+ if (path === '/api/config' && req.method === 'GET') {
225
+ if (firstRun)
226
+ return json(res, 404, { error: '尚无配置文件(首次安装),请提交 bootstrap 向导' });
227
+ const rawDoc = readRawDoc(ctx.configPath);
228
+ if (rawDoc === null)
229
+ return json(res, 500, { error: 'config.yaml 读取失败(语法错误?),lcb setup 或手工修复后重试' });
230
+ return json(res, 200, applyPermissionDisplayDefaults(docForClient(rawDoc)));
231
+ }
232
+ if (path === '/api/config' && req.method === 'PUT') {
233
+ if (firstRun)
234
+ return json(res, 404, { error: '尚无配置文件,请先提交 bootstrap 向导' });
235
+ const body = await readJsonBody(req);
236
+ // secret 回填:以磁盘原文为基准,客户端未改的 secret 字段(空/缺省/仍是脱敏对象)回填现值
237
+ const rawText = readFileSync(ctx.configPath, 'utf8');
238
+ const oldJs = parseDocument(rawText).toJS();
239
+ const merged = applySecrets(body, oldJs);
240
+ const doc = parseDocument(rawText);
241
+ for (const key of PUT_SECTIONS) {
242
+ if (merged[key] === undefined)
243
+ continue;
244
+ // 值未变的段落跳过重写:整段 set 会丢掉段内手写注释(ws-manager 同款折损),「打开页面直接保存」应无损
245
+ if (JSON.stringify(merged[key]) === JSON.stringify(oldJs[key] ?? null))
246
+ continue;
247
+ doc.set(key, merged[key]);
248
+ }
249
+ const text = doc.toString();
250
+ let after;
251
+ try {
252
+ after = parseConfigText(text, ctx.configPath); // 写盘前内存校验:拒绝坏配置落盘
253
+ }
254
+ catch (e) {
255
+ return json(res, 400, { error: e instanceof Error ? e.message : String(e) });
256
+ }
257
+ let restartRequired = [];
258
+ try {
259
+ restartRequired = computeRestartRequired(loadConfig(ctx.configPath), after);
260
+ }
261
+ catch { /* 磁盘原配置损坏:全部视为需重启 */
262
+ restartRequired = ['apps', 'concurrency', 'claude', 'server'];
263
+ }
264
+ writeAtomic(ctx.configPath, text);
265
+ syncManagedClaude(after);
266
+ return json(res, 200, { ok: true, restartRequired });
267
+ }
268
+ // ---- 模型列表拉取(厂商无关;凭证用磁盘现值或本次表单输入,响应只含模型 id,绝不回传凭证) ----
269
+ if (path === '/api/models/fetch' && req.method === 'POST') {
270
+ if (firstRun)
271
+ return json(res, 404, { error: '尚无配置文件,请先完成 bootstrap' });
272
+ const body = await readJsonBody(req);
273
+ let config;
274
+ try {
275
+ config = loadConfig(ctx.configPath);
276
+ }
277
+ catch (e) {
278
+ return json(res, 500, { error: `配置加载失败:${e instanceof Error ? e.message : String(e)}` });
279
+ }
280
+ const resolved = resolveModelFetchParams(body, config.claude);
281
+ if ('error' in resolved)
282
+ return json(res, 400, { error: resolved.error });
283
+ try {
284
+ const models = await fetchModelList(resolved);
285
+ return json(res, 200, { models });
286
+ }
287
+ catch (e) {
288
+ return json(res, 502, { error: e instanceof Error ? e.message : String(e) });
289
+ }
290
+ }
291
+ // ---- 切换当前生效厂商档案:前端拿不到档案凭证明文(脱敏模型),切换必须由后端完成并即时落盘 ----
292
+ if (path === '/api/claude/use-profile' && req.method === 'POST') {
293
+ if (firstRun)
294
+ return json(res, 404, { error: '尚无配置文件,请先完成 bootstrap' });
295
+ const body = await readJsonBody(req);
296
+ const name = String(body.name ?? '').trim();
297
+ if (!name)
298
+ return json(res, 400, { error: '缺少档案名 name' });
299
+ let config;
300
+ try {
301
+ config = loadConfig(ctx.configPath);
302
+ }
303
+ catch (e) {
304
+ return json(res, 500, { error: `配置加载失败:${e instanceof Error ? e.message : String(e)}` });
305
+ }
306
+ const prof = config.claude?.profiles?.find((p) => p.name === name);
307
+ if (!prof)
308
+ return json(res, 404, { error: `档案 "${name}" 不存在(新增/修改档案后须先保存再切换)` });
309
+ // 可选 model 覆盖(档案候选模型点选切换):须属于该档案 models 候选集或其默认模型,防手滑串档案
310
+ const modelOverride = String(body.model ?? '').trim();
311
+ if (modelOverride) {
312
+ const candidates = new Set([...(prof.models ?? []), ...(prof.model ? [prof.model] : [])]);
313
+ if (!candidates.has(modelOverride)) {
314
+ return json(res, 400, { error: `模型 "${modelOverride}" 不在档案 "${name}" 的候选模型中(${[...candidates].join('、') || '空'})` });
315
+ }
316
+ }
317
+ // 顶层四字段整体替换为档案值(档案未配置的字段清除,保证切换干净;mode 与 profiles 原样保留)
318
+ const rawText = readFileSync(ctx.configPath, 'utf8');
319
+ const doc = parseDocument(rawText);
320
+ const oldJs = doc.toJS();
321
+ const oldClaude = (oldJs.claude && typeof oldJs.claude === 'object' && !Array.isArray(oldJs.claude) ? oldJs.claude : {});
322
+ const newClaude = { ...oldClaude };
323
+ if (prof.authToken) {
324
+ newClaude.auth_token = prof.authToken;
325
+ delete newClaude.api_key;
326
+ }
327
+ else if (prof.apiKey) {
328
+ newClaude.api_key = prof.apiKey;
329
+ delete newClaude.auth_token;
330
+ }
331
+ else {
332
+ delete newClaude.auth_token;
333
+ delete newClaude.api_key;
334
+ }
335
+ if (prof.baseUrl)
336
+ newClaude.base_url = prof.baseUrl;
337
+ else
338
+ delete newClaude.base_url;
339
+ const effModel = modelOverride || prof.model;
340
+ if (effModel)
341
+ newClaude.model = effModel;
342
+ else
343
+ delete newClaude.model;
344
+ doc.set('claude', newClaude);
345
+ const text = doc.toString();
346
+ let after;
347
+ try {
348
+ after = parseConfigText(text, ctx.configPath);
349
+ }
350
+ catch (e) {
351
+ return json(res, 400, { error: e instanceof Error ? e.message : String(e) });
352
+ }
353
+ writeAtomic(ctx.configPath, text);
354
+ syncManagedClaude(after);
355
+ return json(res, 200, { ok: true, message: `已切换到档案「${name}」${effModel ? `(模型 ${effModel})` : ''}${after.claude?.mode === 'managed' ? ',managed 模式下对后续任务即生效' : ',当前 inherit 模式:顶层值已更新,切到 managed 后生效'}` });
356
+ }
357
+ if (path === '/api/bootstrap' && req.method === 'POST') {
358
+ if (!firstRun)
359
+ return json(res, 409, { error: '配置文件已存在(bootstrap 仅用于首次安装),请改用配置页编辑' });
360
+ const body = await readJsonBody(req);
361
+ const apps = Array.isArray(body.apps) ? body.apps : [];
362
+ const ws = body.workspace && typeof body.workspace === 'object' ? body.workspace : null;
363
+ if (apps.length === 0 || !ws?.name || !ws?.path) {
364
+ return json(res, 400, { error: 'bootstrap 需要 apps(≥1 个应用凭证)与 workspace(name/path)' });
365
+ }
366
+ const doc = {
367
+ apps,
368
+ workspaces: [{ name: String(ws.name), path: String(ws.path) }],
369
+ defaults: { workspace: String(ws.name) },
370
+ concurrency: 3,
371
+ permissions: defaultPermissionsDoc(),
372
+ server: defaultServerDoc(),
373
+ ...(body.claude && typeof body.claude === 'object' ? { claude: body.claude } : {}),
374
+ };
375
+ const text = stringify(doc);
376
+ let config;
377
+ try {
378
+ config = parseConfigText(text, ctx.configPath);
379
+ }
380
+ catch (e) {
381
+ return json(res, 400, { error: e instanceof Error ? e.message : String(e) });
382
+ }
383
+ writeAtomic(ctx.configPath, text);
384
+ syncManagedClaude(config);
385
+ return json(res, 200, { ok: true, message: '配置已写入。请启动/重启 lcb start 开始使用' });
386
+ }
387
+ // ---- 斜杠命令(需已配置 app 凭证;lcb ui 独立进程同样可用) ----
388
+ if (path.startsWith('/api/slash-commands')) {
389
+ if (firstRun)
390
+ return json(res, 404, { error: '尚无配置文件,请先完成 bootstrap' });
391
+ let config;
392
+ try {
393
+ config = loadConfig(ctx.configPath);
394
+ }
395
+ catch (e) {
396
+ return json(res, 500, { error: `配置加载失败:${e instanceof Error ? e.message : String(e)}` });
397
+ }
398
+ if (path === '/api/slash-commands/expected' && req.method === 'GET') {
399
+ return json(res, 200, { expected: expectedCommands(config) });
400
+ }
401
+ const body = path === '/api/slash-commands/remote' && req.method === 'GET'
402
+ ? {}
403
+ : await readJsonBody(req).catch(() => ({}));
404
+ const appName = path === '/api/slash-commands/remote'
405
+ ? url.searchParams.get('app')
406
+ : String(body.app ?? '');
407
+ const app = config.apps.find((a) => a.name === appName || a.appId === appName);
408
+ if (!app)
409
+ return json(res, 400, { error: `应用 "${String(appName)}" 不存在,可用:${config.apps.map((a) => a.name).join('、')}` });
410
+ const client = createSlashApiClient(app);
411
+ try {
412
+ if (path === '/api/slash-commands/remote' && req.method === 'GET') {
413
+ return json(res, 200, { remote: await client.list() });
414
+ }
415
+ if (path === '/api/slash-commands/sync' && req.method === 'POST') {
416
+ // mode=builtins-only 只补齐缺失的内置命令(绝不删除远端已有命令);缺省全量对齐(兼容既有 API/脚本)
417
+ const report = body.mode === 'builtins-only'
418
+ ? await ensureBuiltins(client, builtinCommands())
419
+ : await syncSlashCommands(client, expectedCommands(config));
420
+ return json(res, 200, report);
421
+ }
422
+ if (path === '/api/slash-commands/action' && req.method === 'POST') {
423
+ // 远端单条 CRUD(0.13 页面直操作飞书 API;与 config.yaml 的 slash_commands.extra 无关)
424
+ const op = String(body.op ?? '');
425
+ const command = String(body.command ?? '').trim();
426
+ const commandId = String(body.commandId ?? '').trim();
427
+ const description = String(body.description ?? '').trim();
428
+ const icon = String(body.icon ?? '').trim();
429
+ if (op === 'create') {
430
+ if (!command)
431
+ return json(res, 400, { error: '缺少 command(命令名,不带 /)' });
432
+ if (!SLASH_COMMAND_RE.test(command))
433
+ return json(res, 400, { error: `command "${command}" 须为 1-32 位字母/数字/下划线/连字符` });
434
+ if (!description)
435
+ return json(res, 400, { error: '缺少 description(飞书指令面板展示用)' });
436
+ const id = await client.create({ command, description, ...(icon ? { icon } : {}) });
437
+ return json(res, 200, { ok: true, commandId: id });
438
+ }
439
+ if (op === 'update') {
440
+ if (!commandId)
441
+ return json(res, 400, { error: 'update 需要 commandId(列表行的飞书侧命令 ID)' });
442
+ if (!description)
443
+ return json(res, 400, { error: '缺少 description(飞书 PATCH 仅支持描述与图标,命令名不可改)' });
444
+ await client.update(commandId, { command: command || '(unchanged)', description, ...(icon ? { icon } : {}) });
445
+ return json(res, 200, { ok: true });
446
+ }
447
+ if (op === 'delete') {
448
+ if (!commandId)
449
+ return json(res, 400, { error: 'delete 需要 commandId(列表行的飞书侧命令 ID)' });
450
+ await client.remove(commandId);
451
+ return json(res, 200, { ok: true });
452
+ }
453
+ return json(res, 400, { error: `未知 op "${op}"(支持 create / update / delete)` });
454
+ }
455
+ }
456
+ catch (e) {
457
+ // API 原文透传(缺 scope 等场景页面直接展示开放平台错误)
458
+ return json(res, 502, { error: `飞书 API 调用失败:${e instanceof Error ? e.message : String(e)}(请确认已开通 application:app_slash_command 读写权限并发布版本)` });
459
+ }
460
+ }
461
+ // ---- 插件管理(本机页面即可操作;与飞书端 /plugin 命令同一执行器) ----
462
+ // managed 模式双目录:自管目录(bridge)+ 本机 ~/.claude(user)。会话加载合并两处
463
+ //(index.ts discoverPlugins 合并);安装默认装 ~/.claude(与本机 claude CLI 共用一份)
464
+ if (path.startsWith('/api/plugins')) {
465
+ if (firstRun)
466
+ return json(res, 404, { error: '尚无配置文件,请先完成 bootstrap' });
467
+ let config;
468
+ try {
469
+ config = loadConfig(ctx.configPath);
470
+ }
471
+ catch (e) {
472
+ return json(res, 500, { error: `配置加载失败:${e instanceof Error ? e.message : String(e)}` });
473
+ }
474
+ const bridgeDir = resolveClaudeDir(config);
475
+ const userDir = DEFAULT_CLAUDE_DIR;
476
+ const sameDir = bridgeDir === userDir;
477
+ if (path === '/api/plugins' && req.method === 'GET') {
478
+ // 市场清单里的最新可装版本,附到已装插件上(前端「0.7.0 → 0.7.1」有新版提示);
479
+ // 优先按 name@marketplace 精确匹配,市场名对不上时退化按插件名(提示性质,宁多勿漏)
480
+ const from = (dir, source) => {
481
+ const latestByKey = new Map();
482
+ const latestByName = new Map();
483
+ for (const m of listAvailablePlugins(dir)) {
484
+ for (const p of m.plugins) {
485
+ if (!p.version)
486
+ continue;
487
+ latestByKey.set(`${p.name}@${m.name}`, p.version);
488
+ latestByName.set(p.name, p.version);
489
+ }
490
+ }
491
+ return listInstalledPlugins(dir).map((p) => {
492
+ const latest = (p.marketplace && latestByKey.get(`${p.name}@${p.marketplace}`)) ?? latestByName.get(p.name);
493
+ return { ...p, source, ...(latest && latest !== p.version ? { latestVersion: latest } : {}) };
494
+ });
495
+ };
496
+ return json(res, 200, {
497
+ configDir: bridgeDir,
498
+ userDir,
499
+ // inherit 模式两目录相同 → 单份(source=user);managed 合并两处各自可启停
500
+ plugins: sameDir ? from(bridgeDir, 'user') : [...from(bridgeDir, 'bridge'), ...from(userDir, 'user')],
501
+ });
502
+ }
503
+ if (path === '/api/plugins/available' && req.method === 'GET') {
504
+ // 安装下拉数据源:该目录已添加市场声明的可安装插件(CLI 语义:install 只能装已添加市场里的)
505
+ const dir = url.searchParams.get('dir') === 'bridge' ? bridgeDir : userDir;
506
+ return json(res, 200, { marketplaces: listAvailablePlugins(dir) });
507
+ }
508
+ if (path === '/api/plugins/action' && req.method === 'POST') {
509
+ const body = await readJsonBody(req);
510
+ const arg = String(body.arg ?? '').trim();
511
+ const op = String(body.op ?? '');
512
+ // 目标目录:enable/disable/uninstall 前端按插件来源传 dir;install/marketplace 缺省装本机 ~/.claude
513
+ const dir = body.dir === 'bridge' ? bridgeDir : userDir;
514
+ const argsTable = {
515
+ install: arg ? ['install', arg] : [],
516
+ uninstall: arg ? ['uninstall', arg] : [],
517
+ enable: arg ? ['enable', arg] : [],
518
+ disable: arg ? ['disable', arg] : [],
519
+ update: arg ? ['update', arg] : [],
520
+ 'marketplace-add': arg ? ['marketplace', 'add', arg] : [],
521
+ 'marketplace-remove': arg ? ['marketplace', 'remove', arg] : [],
522
+ 'marketplace-update': ['marketplace', 'update', ...(arg ? [arg] : [])],
523
+ };
524
+ // 全部更新 = 刷新市场索引 + 逐个更新已装插件(marketplace update 不会升级已装插件本体)
525
+ if (op === 'update-all') {
526
+ const r = await updateAllPlugins(dir);
527
+ invalidatePluginCache(bridgeDir);
528
+ if (!sameDir)
529
+ invalidatePluginCache(userDir);
530
+ return json(res, 200, r);
531
+ }
532
+ const args = argsTable[op];
533
+ if (!args || args.length === 0)
534
+ return json(res, 400, { error: `操作 ${op || '(空)'} 需要参数或未知` });
535
+ const r = await runPluginCli(args, { claudeConfigDir: dir });
536
+ invalidatePluginCache(bridgeDir);
537
+ if (!sameDir)
538
+ invalidatePluginCache(userDir);
539
+ return json(res, 200, r);
540
+ }
541
+ }
542
+ // ---- 桥接器进程启停(页面托管)。embedded 与 lcb ui 独立模式语义不同:embedded 停止/重启
543
+ // 会连带本页所在进程,先回响应再走 selfStop 优雅关闭;独立模式经 PID 文件跨进程操作 ----
544
+ if (path === '/api/bridge/action' && req.method === 'POST') {
545
+ if (firstRun)
546
+ return json(res, 400, { error: '尚无配置文件(首次安装),请先完成 bootstrap 向导' });
547
+ const body = await readJsonBody(req);
548
+ const op = String(body.op ?? '');
549
+ const st = bridgeStatus();
550
+ if (op === 'start') {
551
+ if (ctx.embedded)
552
+ return json(res, 409, { error: '桥接器已随本页运行,无需启动' });
553
+ if (st.running)
554
+ return json(res, 409, { error: `桥接器已在运行(PID ${st.pid})` });
555
+ const r = spawnBridgeDetached();
556
+ if (!r.ok)
557
+ return json(res, 400, { error: r.error });
558
+ return json(res, 200, { ok: true, pid: r.pid, message: '桥接器已在后台启动(运行日志见 ~/.lark-claudecode-bridge/bridge.log)' });
559
+ }
560
+ if (op === 'stop' || op === 'restart') {
561
+ if (ctx.embedded && !ctx.selfStop)
562
+ return json(res, 500, { error: '内部错误:embedded 模式未注入 selfStop' });
563
+ if (ctx.embedded && ctx.selfStop) {
564
+ if (op === 'restart') {
565
+ // 旧进程退出后由 detached helper 拉起新进程(含等待端口释放),页面轮询 /api/status 自动恢复
566
+ const r = restartBridgeWithHelper(process.pid);
567
+ if (!r.ok)
568
+ return json(res, 400, { error: r.error });
569
+ json(res, 200, { ok: true, message: '正在重启,页面将短暂失联后自动恢复' });
570
+ }
571
+ else {
572
+ json(res, 200, { ok: true, message: '正在停止,本配置页将随桥接器进程一同关闭' });
573
+ }
574
+ setTimeout(ctx.selfStop, 300); // 先让响应 flush 出去再退出进程
575
+ return;
576
+ }
577
+ if (op === 'stop') {
578
+ if (!st.running)
579
+ return json(res, 409, { error: '桥接器未在运行' });
580
+ return json(res, 200, { ok: stopBridgeByPid(st.pid), message: '已发送停止信号' });
581
+ }
582
+ if (st.running) {
583
+ const r = restartBridgeWithHelper(st.pid);
584
+ if (!r.ok)
585
+ return json(res, 400, { error: r.error });
586
+ return json(res, 200, { ok: true, message: '正在重启桥接器(本页不受影响,稍后状态自动更新)' });
587
+ }
588
+ const r = spawnBridgeDetached();
589
+ if (!r.ok)
590
+ return json(res, 400, { error: r.error });
591
+ return json(res, 200, { ok: true, pid: r.pid, message: '桥接器未在运行,已在后台启动' });
592
+ }
593
+ return json(res, 400, { error: `未知 op "${op}"(支持 start / stop / restart)` });
594
+ }
595
+ // ---- 版本检查与一键更新(npm view / npm install -g,registry 镜像跟随用户 .npmrc) ----
596
+ if (path === '/api/update/check' && req.method === 'GET') {
597
+ try {
598
+ return json(res, 200, { ...await checkUpdate(), mode: installMode() });
599
+ }
600
+ catch (e) {
601
+ return json(res, 502, { error: `检查更新失败:${e instanceof Error ? e.message : String(e)}(网络离线或 registry 不可达?可稍后重试)` });
602
+ }
603
+ }
604
+ if (path === '/api/update/run' && req.method === 'POST') {
605
+ if (installMode() !== 'global') {
606
+ return json(res, 400, { error: '检测到当前非 npm 安装目录运行(如源码运行),一键更新会装出另一份全局副本而非更新当前实例,请手动更新' });
607
+ }
608
+ try {
609
+ const output = await runUpdate();
610
+ return json(res, 200, { ok: true, output });
611
+ }
612
+ catch (e) {
613
+ return json(res, 502, { error: `更新失败:${e instanceof Error ? e.message : String(e)}` });
614
+ }
615
+ }
616
+ return json(res, 404, { error: `未知端点 ${req.method} ${path}` });
617
+ }
618
+ //# sourceMappingURL=server.js.map