@jesonliu/lark-claudecode-bridge 0.3.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/LICENSE +21 -0
- package/README.md +183 -0
- package/config.example.yaml +42 -0
- package/deploy/com.lark-claudecode-bridge.plist +9 -0
- package/deploy/lark-claudecode-bridge.service +11 -0
- package/deploy/windows-start.bat +2 -0
- package/dist/access/access-control.d.ts +43 -0
- package/dist/access/access-control.js +88 -0
- package/dist/access/access-control.js.map +1 -0
- package/dist/bin/lcb.d.ts +2 -0
- package/dist/bin/lcb.js +191 -0
- package/dist/bin/lcb.js.map +1 -0
- package/dist/cli/app-manager.d.ts +28 -0
- package/dist/cli/app-manager.js +99 -0
- package/dist/cli/app-manager.js.map +1 -0
- package/dist/cli/asker.d.ts +7 -0
- package/dist/cli/asker.js +41 -0
- package/dist/cli/asker.js.map +1 -0
- package/dist/cli/setup-wizard.d.ts +28 -0
- package/dist/cli/setup-wizard.js +114 -0
- package/dist/cli/setup-wizard.js.map +1 -0
- package/dist/cli/stdin-pairing.d.ts +7 -0
- package/dist/cli/stdin-pairing.js +19 -0
- package/dist/cli/stdin-pairing.js.map +1 -0
- package/dist/cli/ws-manager.d.ts +11 -0
- package/dist/cli/ws-manager.js +67 -0
- package/dist/cli/ws-manager.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +169 -0
- package/dist/config.js.map +1 -0
- package/dist/executor/claude-executor.d.ts +28 -0
- package/dist/executor/claude-executor.js +84 -0
- package/dist/executor/claude-executor.js.map +1 -0
- package/dist/executor/notify-server.d.ts +34 -0
- package/dist/executor/notify-server.js +193 -0
- package/dist/executor/notify-server.js.map +1 -0
- package/dist/executor/output-collector.d.ts +5 -0
- package/dist/executor/output-collector.js +17 -0
- package/dist/executor/output-collector.js.map +1 -0
- package/dist/executor/permission-gate.d.ts +20 -0
- package/dist/executor/permission-gate.js +66 -0
- package/dist/executor/permission-gate.js.map +1 -0
- package/dist/gateway/card-builder.d.ts +15 -0
- package/dist/gateway/card-builder.js +60 -0
- package/dist/gateway/card-builder.js.map +1 -0
- package/dist/gateway/feishu-gateway.d.ts +164 -0
- package/dist/gateway/feishu-gateway.js +249 -0
- package/dist/gateway/feishu-gateway.js.map +1 -0
- package/dist/gateway/progress-card.d.ts +34 -0
- package/dist/gateway/progress-card.js +106 -0
- package/dist/gateway/progress-card.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +433 -0
- package/dist/index.js.map +1 -0
- package/dist/session/channel.d.ts +18 -0
- package/dist/session/channel.js +44 -0
- package/dist/session/channel.js.map +1 -0
- package/dist/session/commands.d.ts +24 -0
- package/dist/session/commands.js +87 -0
- package/dist/session/commands.js.map +1 -0
- package/dist/session/session-store.d.ts +42 -0
- package/dist/session/session-store.js +85 -0
- package/dist/session/session-store.js.map +1 -0
- package/dist/session/triggers.d.ts +12 -0
- package/dist/session/triggers.js +34 -0
- package/dist/session/triggers.js.map +1 -0
- package/dist/transcript/transcript-writer.d.ts +85 -0
- package/dist/transcript/transcript-writer.js +64 -0
- package/dist/transcript/transcript-writer.js.map +1 -0
- package/dist/types.d.ts +105 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type AppResult = {
|
|
2
|
+
ok: true;
|
|
3
|
+
yaml: string;
|
|
4
|
+
} | {
|
|
5
|
+
ok: false;
|
|
6
|
+
error: string;
|
|
7
|
+
};
|
|
8
|
+
export interface AppInput {
|
|
9
|
+
name?: string;
|
|
10
|
+
appId: string;
|
|
11
|
+
appSecret: string;
|
|
12
|
+
domain?: 'feishu' | 'lark';
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 添加应用。配置为旧 feishu 单应用格式时先原地转 apps 数组(首次 app 操作即完成格式升级):
|
|
16
|
+
* 旧 app 显式写 claude_config_dir=~/.claude——物理转成 apps 格式后 loadConfig 会按新规则
|
|
17
|
+
* 推导独立目录,不显式钉住系统默认目录会丢存量会话的 resume 链。
|
|
18
|
+
*/
|
|
19
|
+
export declare function addApp(raw: string, input: AppInput): AppResult;
|
|
20
|
+
/** 删除应用(按 name 或 app_id 匹配);最后一个不可删;遗留的会话分片与落盘目录不动(人工清理) */
|
|
21
|
+
export declare function removeApp(raw: string, nameOrAppId: string): AppResult;
|
|
22
|
+
/** 列出应用(旧 feishu 格式也能列:归一化为单元素);纯 yaml 解析,不做完整配置校验 */
|
|
23
|
+
export declare function listApps(raw: string): Array<{
|
|
24
|
+
name: string;
|
|
25
|
+
appId: string;
|
|
26
|
+
domain?: string;
|
|
27
|
+
defaultWorkspace?: string;
|
|
28
|
+
}>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// lcb app add/remove/list 的核心逻辑:对已有 config.yaml 做增量改写(保注释),模式复刻 ws-manager。
|
|
2
|
+
// 纯逻辑,写盘由调用方(bin)负责,tests/app-manager.test.ts 直接单测。
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { parseDocument } from 'yaml';
|
|
6
|
+
function appsSeq(raw) {
|
|
7
|
+
const doc = parseDocument(raw);
|
|
8
|
+
return doc.get('apps');
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 添加应用。配置为旧 feishu 单应用格式时先原地转 apps 数组(首次 app 操作即完成格式升级):
|
|
12
|
+
* 旧 app 显式写 claude_config_dir=~/.claude——物理转成 apps 格式后 loadConfig 会按新规则
|
|
13
|
+
* 推导独立目录,不显式钉住系统默认目录会丢存量会话的 resume 链。
|
|
14
|
+
*/
|
|
15
|
+
export function addApp(raw, input) {
|
|
16
|
+
const appId = input.appId.trim();
|
|
17
|
+
const appSecret = input.appSecret.trim();
|
|
18
|
+
if (!appId)
|
|
19
|
+
return { ok: false, error: 'app_id 不能为空' };
|
|
20
|
+
if (!appSecret)
|
|
21
|
+
return { ok: false, error: 'app_secret 不能为空' };
|
|
22
|
+
const doc = parseDocument(raw);
|
|
23
|
+
if (!doc.has('apps')) {
|
|
24
|
+
const feishu = doc.get('feishu');
|
|
25
|
+
if (feishu && typeof feishu === 'object') {
|
|
26
|
+
const oldAppId = String(feishu.get('app_id') ?? '');
|
|
27
|
+
const oldSecret = String(feishu.get('app_secret') ?? '');
|
|
28
|
+
const oldDomain = feishu.get('domain');
|
|
29
|
+
if (!oldAppId || !oldSecret)
|
|
30
|
+
return { ok: false, error: '旧 feishu 配置缺少 app_id/app_secret,请先运行 lcb setup 重建' };
|
|
31
|
+
// doc.createNode 把 plain 对象转成 YAML 节点(set plain 值后 get 拿不到 Node,无法继续 items 操作)
|
|
32
|
+
doc.set('apps', doc.createNode([{
|
|
33
|
+
app_id: oldAppId,
|
|
34
|
+
app_secret: oldSecret,
|
|
35
|
+
...(oldDomain ? { domain: String(oldDomain) } : {}),
|
|
36
|
+
// 存量会话在系统默认目录,显式钉住防止归一化按新 app 规则推导独立目录
|
|
37
|
+
claude_config_dir: join(homedir(), '.claude'),
|
|
38
|
+
}]));
|
|
39
|
+
doc.delete('feishu');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
return { ok: false, error: '配置缺少 apps(或旧版 feishu),请先运行 lcb setup' };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const seq = doc.get('apps');
|
|
46
|
+
const dup = seq.items.some((m) => String(m.get('app_id')) === appId);
|
|
47
|
+
if (dup)
|
|
48
|
+
return { ok: false, error: `app_id "${appId}" 重复:同一应用建两条长连接会导致事件错乱` };
|
|
49
|
+
const name = input.name?.trim();
|
|
50
|
+
doc.addIn(['apps'], {
|
|
51
|
+
...(name ? { name } : {}), // 不写 name:loadConfig 归一化时缺省取 appId
|
|
52
|
+
app_id: appId,
|
|
53
|
+
app_secret: appSecret,
|
|
54
|
+
...(input.domain === 'lark' ? { domain: 'lark' } : {}),
|
|
55
|
+
});
|
|
56
|
+
return { ok: true, yaml: doc.toString() };
|
|
57
|
+
}
|
|
58
|
+
/** 删除应用(按 name 或 app_id 匹配);最后一个不可删;遗留的会话分片与落盘目录不动(人工清理) */
|
|
59
|
+
export function removeApp(raw, nameOrAppId) {
|
|
60
|
+
const doc = parseDocument(raw);
|
|
61
|
+
const seq = doc.get('apps');
|
|
62
|
+
if (!seq) {
|
|
63
|
+
const feishu = doc.get('feishu');
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
error: feishu ? '当前为旧 feishu 单应用配置(唯一应用不可删);如需更换请重跑 lcb setup' : '配置缺少 apps,请先运行 lcb setup',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const idx = seq.items.findIndex((m) => String(m.get('name')) === nameOrAppId || String(m.get('app_id')) === nameOrAppId);
|
|
70
|
+
if (idx < 0)
|
|
71
|
+
return { ok: false, error: `应用 "${nameOrAppId}" 不存在,lcb app list 查看` };
|
|
72
|
+
if (seq.items.length === 1)
|
|
73
|
+
return { ok: false, error: '不能删除最后一个应用(至少保留一个)' };
|
|
74
|
+
seq.delete(idx);
|
|
75
|
+
return { ok: true, yaml: doc.toString() };
|
|
76
|
+
}
|
|
77
|
+
/** 列出应用(旧 feishu 格式也能列:归一化为单元素);纯 yaml 解析,不做完整配置校验 */
|
|
78
|
+
export function listApps(raw) {
|
|
79
|
+
const doc = parseDocument(raw);
|
|
80
|
+
const seq = doc.get('apps');
|
|
81
|
+
if (!seq) {
|
|
82
|
+
const feishu = doc.get('feishu');
|
|
83
|
+
if (!feishu || typeof feishu !== 'object')
|
|
84
|
+
return [];
|
|
85
|
+
const appId = String(feishu.get('app_id') ?? '');
|
|
86
|
+
return [{
|
|
87
|
+
name: appId,
|
|
88
|
+
appId,
|
|
89
|
+
...(feishu.get('domain') ? { domain: String(feishu.get('domain')) } : {}),
|
|
90
|
+
}];
|
|
91
|
+
}
|
|
92
|
+
return seq.items.map((m) => ({
|
|
93
|
+
name: String(m.get('name') ?? m.get('app_id') ?? ''),
|
|
94
|
+
appId: String(m.get('app_id') ?? ''),
|
|
95
|
+
...(m.get('domain') ? { domain: String(m.get('domain')) } : {}),
|
|
96
|
+
...(m.get('default_workspace') ? { defaultWorkspace: String(m.get('default_workspace')) } : {}),
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=app-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-manager.js","sourceRoot":"","sources":["../../src/cli/app-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// 行问答器:lcb setup 向导与 lcb app add 交互共用。
|
|
2
|
+
// 不用 readline/promises 的 rl.question 链:管道输入下所有行可能同 tick 送达,
|
|
3
|
+
// 连续 await question() 会丢行挂起;改为持久 line 监听 + 行队列,TTY 与管道两用。
|
|
4
|
+
// EOF(管道读完 / Ctrl+D)时未决问题以空串放行——语义等同「直接回车取默认值」,
|
|
5
|
+
// 向导流程保证能走完,不会挂出 unsettled promise。
|
|
6
|
+
import { createInterface } from 'node:readline';
|
|
7
|
+
export function asker(input, output) {
|
|
8
|
+
const rl = createInterface({ input, output });
|
|
9
|
+
const lines = [];
|
|
10
|
+
const waiters = [];
|
|
11
|
+
let eof = false;
|
|
12
|
+
rl.on('line', (l) => {
|
|
13
|
+
const w = waiters.shift();
|
|
14
|
+
if (w)
|
|
15
|
+
w(l);
|
|
16
|
+
else
|
|
17
|
+
lines.push(l);
|
|
18
|
+
});
|
|
19
|
+
rl.on('close', () => {
|
|
20
|
+
eof = true;
|
|
21
|
+
while (waiters.length)
|
|
22
|
+
waiters.shift()('');
|
|
23
|
+
});
|
|
24
|
+
return {
|
|
25
|
+
async ask(q, def) {
|
|
26
|
+
output.write(def ? `${q}(回车默认 ${def}):` : `${q}:`);
|
|
27
|
+
const a = await new Promise((resolve) => {
|
|
28
|
+
const first = lines.shift();
|
|
29
|
+
if (first !== undefined || eof)
|
|
30
|
+
resolve(first ?? '');
|
|
31
|
+
else
|
|
32
|
+
waiters.push(resolve);
|
|
33
|
+
});
|
|
34
|
+
if (!input.isTTY)
|
|
35
|
+
output.write(`${a}\n`); // 管道模式回显答案(TTY 下终端已自行回显)
|
|
36
|
+
return a.trim() || def || '';
|
|
37
|
+
},
|
|
38
|
+
close: () => { rl.close(); },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=asker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asker.js","sourceRoot":"","sources":["../../src/cli/asker.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,4DAA4D;AAC5D,0DAA0D;AAC1D,gDAAgD;AAChD,oCAAoC;AACpC,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhD,MAAM,UAAU,KAAK,CAAC,KAAkD,EAAE,MAA6B;IACrG,MAAM,EAAE,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;IAC9C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,OAAO,GAA+B,EAAE,CAAC;IAC/C,IAAI,GAAG,GAAG,KAAK,CAAC;IAChB,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAS,EAAE,EAAE;QAC1B,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;QAC1B,IAAI,CAAC;YAAE,CAAC,CAAC,CAAC,CAAC,CAAC;;YACP,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;QAClB,GAAG,GAAG,IAAI,CAAC;QACX,OAAO,OAAO,CAAC,MAAM;YAAE,OAAO,CAAC,KAAK,EAAG,CAAC,EAAE,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IACH,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,CAAS,EAAE,GAAY;YAC/B,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACnD,MAAM,CAAC,GAAG,MAAM,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,EAAE;gBAC9C,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC;gBAC5B,IAAI,KAAK,KAAK,SAAS,IAAI,GAAG;oBAAE,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;;oBAChD,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC7B,CAAC,CAAC,CAAC;YACH,IAAI,CAAC,KAAK,CAAC,KAAK;gBAAE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,yBAAyB;YACnE,OAAO,CAAC,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,EAAE,CAAC;QAC/B,CAAC;QACD,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;KAC7B,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { BridgeConfig } from '../types.js';
|
|
2
|
+
/** 向导收集到的原始答案(apps 支持多机器人) */
|
|
3
|
+
export interface SetupAnswers {
|
|
4
|
+
apps: Array<{
|
|
5
|
+
name?: string;
|
|
6
|
+
appId: string;
|
|
7
|
+
appSecret: string;
|
|
8
|
+
}>;
|
|
9
|
+
workspaces: Array<{
|
|
10
|
+
name: string;
|
|
11
|
+
path: string;
|
|
12
|
+
}>;
|
|
13
|
+
defaultWorkspace: string;
|
|
14
|
+
}
|
|
15
|
+
/** App ID 形状(Task 6 遗留建议:不符仅警告,不阻止) */
|
|
16
|
+
export declare const APP_ID_RE: RegExp;
|
|
17
|
+
/**
|
|
18
|
+
* 答案 → 运行时配置对象。
|
|
19
|
+
* 默认工作区名不在列表时回退到第一个工作区(防手滑回车写出非法默认值)。
|
|
20
|
+
*/
|
|
21
|
+
export declare function answersToConfig(a: SetupAnswers): BridgeConfig;
|
|
22
|
+
/**
|
|
23
|
+
* 配置 → 可写盘的 YAML 文档。
|
|
24
|
+
* ⚠️ 键名必须用 snake_case(apps[].app_id / app_secret)——loadConfig 按此形状解析,
|
|
25
|
+
* 直接 stringify(camelCase 的 BridgeConfig) 会写出 loadConfig 读不回的配置。
|
|
26
|
+
*/
|
|
27
|
+
export declare function toYamlDocument(cfg: BridgeConfig): Record<string, unknown>;
|
|
28
|
+
export declare function runSetup(configPath: string): Promise<void>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// lcb setup 引导式配置向导
|
|
2
|
+
// 交互层(readline 问答)与序列化层(answersToConfig / toYamlDocument)分离:
|
|
3
|
+
// 序列化层是纯函数,tests/cli.test.ts 直接单测;交互层留给手动冒烟
|
|
4
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { dirname } from 'node:path';
|
|
6
|
+
import { stringify } from 'yaml';
|
|
7
|
+
import { asker } from './asker.js';
|
|
8
|
+
/** App ID 形状(Task 6 遗留建议:不符仅警告,不阻止) */
|
|
9
|
+
export const APP_ID_RE = /^cli_[0-9a-fA-F]{16}$/;
|
|
10
|
+
/**
|
|
11
|
+
* 答案 → 运行时配置对象。
|
|
12
|
+
* 默认工作区名不在列表时回退到第一个工作区(防手滑回车写出非法默认值)。
|
|
13
|
+
*/
|
|
14
|
+
export function answersToConfig(a) {
|
|
15
|
+
const first = a.workspaces[0]?.name ?? '';
|
|
16
|
+
const ws = a.defaultWorkspace || first;
|
|
17
|
+
return {
|
|
18
|
+
apps: a.apps.map((app) => ({
|
|
19
|
+
name: app.name?.trim() || app.appId,
|
|
20
|
+
appId: app.appId,
|
|
21
|
+
appSecret: app.appSecret,
|
|
22
|
+
})),
|
|
23
|
+
workspaces: a.workspaces,
|
|
24
|
+
defaults: { workspace: a.workspaces.some((w) => w.name === ws) ? ws : first },
|
|
25
|
+
concurrency: 3,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 配置 → 可写盘的 YAML 文档。
|
|
30
|
+
* ⚠️ 键名必须用 snake_case(apps[].app_id / app_secret)——loadConfig 按此形状解析,
|
|
31
|
+
* 直接 stringify(camelCase 的 BridgeConfig) 会写出 loadConfig 读不回的配置。
|
|
32
|
+
*/
|
|
33
|
+
export function toYamlDocument(cfg) {
|
|
34
|
+
return {
|
|
35
|
+
apps: cfg.apps.map((app) => ({
|
|
36
|
+
...(app.name !== app.appId ? { name: app.name } : {}),
|
|
37
|
+
app_id: app.appId,
|
|
38
|
+
app_secret: app.appSecret,
|
|
39
|
+
...(app.domain === 'lark' ? { domain: 'lark' } : {}),
|
|
40
|
+
})),
|
|
41
|
+
workspaces: cfg.workspaces,
|
|
42
|
+
defaults: { workspace: cfg.defaults.workspace },
|
|
43
|
+
concurrency: cfg.concurrency,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export async function runSetup(configPath) {
|
|
47
|
+
const { ask, close } = asker(process.stdin, process.stdout);
|
|
48
|
+
console.log('🛠 lcb setup — 引导式配置\n');
|
|
49
|
+
if (existsSync(configPath)) {
|
|
50
|
+
const overwrite = await ask(`配置已存在 ${configPath},覆盖?(y/N)`, 'n');
|
|
51
|
+
if (overwrite.toLowerCase() !== 'y') {
|
|
52
|
+
close();
|
|
53
|
+
console.log('已取消');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// 覆盖提示:已有多个应用时明确告知(增量管理请改用 lcb app add)
|
|
57
|
+
try {
|
|
58
|
+
const raw = readFileSync(configPath, 'utf8');
|
|
59
|
+
const count = (raw.match(/^ {2}- (?:name:|app_id:)/gm) ?? []).length || (raw.match(/^feishu:/m) ? 1 : 0);
|
|
60
|
+
if (count > 1)
|
|
61
|
+
console.log(`⚠️ 将覆盖现有配置(含 ${count} 个应用);只想增删应用请改用 lcb app add`);
|
|
62
|
+
}
|
|
63
|
+
catch { /* 读取失败不影响覆盖流程,写盘时自然暴露 */ }
|
|
64
|
+
}
|
|
65
|
+
console.log('\n请先在飞书开放平台完成(详见 README):\n'
|
|
66
|
+
+ ' 1. 创建企业自建应用并添加机器人能力\n'
|
|
67
|
+
+ ' 2. 开通权限 im:message / im:message:send_as_bot / im:resource / contact:user.base:readonly\n'
|
|
68
|
+
+ ' 3. 事件订阅选「长连接」,添加 im.message.receive_v1\n'
|
|
69
|
+
+ ' 4. 卡片交互配置选「长连接」\n');
|
|
70
|
+
const apps = [];
|
|
71
|
+
for (let i = 0;; i++) {
|
|
72
|
+
const appId = await ask(i === 0
|
|
73
|
+
? '第一个应用的 App ID (cli_ 开头,形如 cli_aabbccddeeff0011)'
|
|
74
|
+
: '再添一个应用的 App ID(直接回车结束添加)');
|
|
75
|
+
if (!appId) {
|
|
76
|
+
if (i === 0) {
|
|
77
|
+
console.log('至少需要一个应用,已退出');
|
|
78
|
+
close();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
// 空值(EOF 截断/直接回车)不警告——后续必填校验会让流程退出,此处无需噪声
|
|
84
|
+
if (!APP_ID_RE.test(appId))
|
|
85
|
+
console.warn(`⚠️ App ID ${appId} 不符合常见形状 cli_ + 16 位十六进制,请确认(不阻止继续)`);
|
|
86
|
+
const name = await ask('应用名字(显示用,如「素材收集」,回车默认取 App ID)');
|
|
87
|
+
const appSecret = await ask('App Secret');
|
|
88
|
+
apps.push(name ? { name, appId, appSecret } : { appId, appSecret });
|
|
89
|
+
}
|
|
90
|
+
const workspaces = [];
|
|
91
|
+
let defaultWorkspace = '';
|
|
92
|
+
for (let i = 0;; i++) {
|
|
93
|
+
const name = await ask(i === 0 ? '第一个工作区名字(如 demo)' : '再添一个工作区名字(直接回车结束)');
|
|
94
|
+
if (!name)
|
|
95
|
+
break;
|
|
96
|
+
const path = await ask(`工作区 ${name} 的本机路径`);
|
|
97
|
+
workspaces.push({ name, path });
|
|
98
|
+
if (!defaultWorkspace)
|
|
99
|
+
defaultWorkspace = name;
|
|
100
|
+
}
|
|
101
|
+
if (workspaces.length === 0) {
|
|
102
|
+
console.log('至少需要一个工作区,已退出');
|
|
103
|
+
close();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const pick = await ask('默认工作区', defaultWorkspace);
|
|
107
|
+
defaultWorkspace = workspaces.some((w) => w.name === pick) ? pick : defaultWorkspace;
|
|
108
|
+
const cfg = answersToConfig({ apps, workspaces, defaultWorkspace });
|
|
109
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
110
|
+
writeFileSync(configPath, stringify(toYamlDocument(cfg)), 'utf8');
|
|
111
|
+
close();
|
|
112
|
+
console.log(`\n✅ 配置已写入 ${configPath}(${apps.length} 个应用)\n下一步:lcb start 启动;lcb app add 可继续追加机器人`);
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=setup-wizard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup-wizard.js","sourceRoot":"","sources":["../../src/cli/setup-wizard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 处理 stdin 的一行输入:6 位数字按配对码现读现批,其余静默忽略。
|
|
3
|
+
* ⚠️ 必须每次现 `AccessControl.load()`:桥运行中用户触发 beginPairing 写入的新 pending
|
|
4
|
+
* 不在启动快照里——复用启动实例必然「配对码无效」,且其 save() 整盘覆写会抹掉桥内
|
|
5
|
+
* 新增的 pending。与独立进程的 `lcb pair` 走同一条现读路径(已验证安全)。
|
|
6
|
+
*/
|
|
7
|
+
export declare function approvePairingLine(line: string, storePath?: string): void;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// 「运行终端输入配对码」的行处理逻辑(lcb start 的 stdin 监听用)
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { AccessControl } from '../access/access-control.js';
|
|
4
|
+
import { CONFIG_DIR } from '../config.js';
|
|
5
|
+
/**
|
|
6
|
+
* 处理 stdin 的一行输入:6 位数字按配对码现读现批,其余静默忽略。
|
|
7
|
+
* ⚠️ 必须每次现 `AccessControl.load()`:桥运行中用户触发 beginPairing 写入的新 pending
|
|
8
|
+
* 不在启动快照里——复用启动实例必然「配对码无效」,且其 save() 整盘覆写会抹掉桥内
|
|
9
|
+
* 新增的 pending。与独立进程的 `lcb pair` 走同一条现读路径(已验证安全)。
|
|
10
|
+
*/
|
|
11
|
+
export function approvePairingLine(line, storePath = join(CONFIG_DIR, 'access.json')) {
|
|
12
|
+
const code = line.trim();
|
|
13
|
+
if (!/^\d{6}$/.test(code))
|
|
14
|
+
return;
|
|
15
|
+
const access = AccessControl.load(storePath);
|
|
16
|
+
const r = access.approvePairing(code);
|
|
17
|
+
console.log(r.ok ? `✅ 已批准用户 ${r.userId}${r.isFirstAdmin ? '(admin)' : ''}` : `❌ ${r.error}`);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=stdin-pairing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdin-pairing.js","sourceRoot":"","sources":["../../src/cli/stdin-pairing.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,SAAS,GAAW,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC;IAClG,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IACzB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO;IAClC,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC7C,MAAM,CAAC,GAAG,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;IACtC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;AAC/F,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type WsResult = {
|
|
2
|
+
ok: true;
|
|
3
|
+
yaml: string;
|
|
4
|
+
} | {
|
|
5
|
+
ok: false;
|
|
6
|
+
error: string;
|
|
7
|
+
};
|
|
8
|
+
/** 添加工作区:校验名字形状 / 重名 / 路径存在性,通过后追加到 workspaces 序列 */
|
|
9
|
+
export declare function addWorkspace(raw: string, name: string, path: string): WsResult;
|
|
10
|
+
/** 删除工作区:最后一个不可删(loadConfig 要求 ≥1);删的是默认工作区时回退到剩余第一个 */
|
|
11
|
+
export declare function removeWorkspace(raw: string, name: string): WsResult;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// lcb ws add/remove 的核心逻辑:对已有 config.yaml 做增量改写。
|
|
2
|
+
// ⚠️ 用 parseDocument 而非 stringify 全量重写——目标文件可能是用户手写的(带注释),
|
|
3
|
+
// 全量重写会抹掉注释;setup 向导可以 stringify 是因为它写的是全新文件。
|
|
4
|
+
// 纯逻辑 + statSync 存在性检查,写盘由调用方(bin)负责,tests/ws-manager.test.ts 直接单测。
|
|
5
|
+
import { statSync } from 'node:fs';
|
|
6
|
+
import { parseDocument } from 'yaml';
|
|
7
|
+
function workspacesSeq(raw) {
|
|
8
|
+
const doc = parseDocument(raw);
|
|
9
|
+
return doc.get('workspaces');
|
|
10
|
+
}
|
|
11
|
+
/** 现有工作区名列表(保持文档顺序) */
|
|
12
|
+
function existingNames(seq) {
|
|
13
|
+
return (seq?.items ?? []).map((m) => String(m.get('name')));
|
|
14
|
+
}
|
|
15
|
+
/** 添加工作区:校验名字形状 / 重名 / 路径存在性,通过后追加到 workspaces 序列 */
|
|
16
|
+
export function addWorkspace(raw, name, path) {
|
|
17
|
+
const trimmed = name.trim();
|
|
18
|
+
// /ws use 按空格分词,含空白的名字无法被切换到
|
|
19
|
+
if (!trimmed || /\s/.test(trimmed))
|
|
20
|
+
return { ok: false, error: '工作区名字不能为空、且不能包含空白字符' };
|
|
21
|
+
const seq = workspacesSeq(raw);
|
|
22
|
+
if (!seq)
|
|
23
|
+
return { ok: false, error: '配置缺少 workspaces 列表,请先运行 lcb setup' };
|
|
24
|
+
if (existingNames(seq).includes(trimmed))
|
|
25
|
+
return { ok: false, error: `工作区 "${trimmed}" 已存在` };
|
|
26
|
+
try {
|
|
27
|
+
// 白名单路径必须是真实目录:注册不存在的路径没有意义,且会把拼写错误留到任务执行时才暴露
|
|
28
|
+
if (!statSync(path).isDirectory())
|
|
29
|
+
return { ok: false, error: `路径 ${path} 不是目录` };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { ok: false, error: `路径 ${path} 不存在,请先创建目录` };
|
|
33
|
+
}
|
|
34
|
+
const doc = parseDocument(raw);
|
|
35
|
+
doc.addIn(['workspaces'], { name: trimmed, path });
|
|
36
|
+
return { ok: true, yaml: doc.toString() };
|
|
37
|
+
}
|
|
38
|
+
/** 删除工作区:最后一个不可删(loadConfig 要求 ≥1);删的是默认工作区时回退到剩余第一个 */
|
|
39
|
+
export function removeWorkspace(raw, name) {
|
|
40
|
+
const seq = workspacesSeq(raw);
|
|
41
|
+
if (!seq)
|
|
42
|
+
return { ok: false, error: '配置缺少 workspaces 列表,请先运行 lcb setup' };
|
|
43
|
+
const names = existingNames(seq);
|
|
44
|
+
if (!names.includes(name))
|
|
45
|
+
return { ok: false, error: `工作区 "${name}" 不存在,lcb ws list 查看` };
|
|
46
|
+
if (names.length === 1)
|
|
47
|
+
return { ok: false, error: '不能删除最后一个工作区(配置至少需要一个)' };
|
|
48
|
+
const doc = parseDocument(raw);
|
|
49
|
+
const idx = doc.get('workspaces').items.findIndex((m) => String(m.get('name')) === name);
|
|
50
|
+
doc.get('workspaces').delete(idx);
|
|
51
|
+
// 删默认工作区:defaults 必须指向仍存在的名字,否则下次 loadConfig 直接抛错拒启
|
|
52
|
+
if (doc.getIn(['defaults', 'workspace']) === name) {
|
|
53
|
+
const fallback = existingNames(doc.get('workspaces')).find((n) => n !== name);
|
|
54
|
+
doc.setIn(['defaults', 'workspace'], fallback);
|
|
55
|
+
}
|
|
56
|
+
// 联动清理:apps[].default_workspace 指向被删工作区时一并删除,
|
|
57
|
+
// 否则下次 loadConfig(含热重载的每次读盘)都会抛「不在 workspaces 列表」错误
|
|
58
|
+
const appsSeq = doc.get('apps');
|
|
59
|
+
if (appsSeq) {
|
|
60
|
+
for (const m of appsSeq.items) {
|
|
61
|
+
if (String(m.get('default_workspace')) === name)
|
|
62
|
+
m.delete('default_workspace');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { ok: true, yaml: doc.toString() };
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=ws-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws-manager.js","sourceRoot":"","sources":["../../src/cli/ws-manager.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BridgeConfig, FeishuAppConfig } from './types.js';
|
|
2
|
+
/** LCB_CONFIG_DIR 环境变量可覆盖配置根目录(多租户分进程场景用;正常用户无须设置) */
|
|
3
|
+
export declare const CONFIG_DIR: string;
|
|
4
|
+
export declare const CONFIG_PATH: string;
|
|
5
|
+
export declare function loadConfig(path?: string): BridgeConfig;
|
|
6
|
+
/** 热重载比较:任一 app 的凭证/名称/工作区/并发/配置目录/人格变化即视为需重启的变更(顺序敏感) */
|
|
7
|
+
export declare function sameApps(a: FeishuAppConfig[], b: FeishuAppConfig[]): boolean;
|