@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,44 @@
|
|
|
1
|
+
/** 通道键:chatId 与 userId 拼接,同 chat + 同 user 视为同一会话通道 */
|
|
2
|
+
export function channelKey(chatId, userId) {
|
|
3
|
+
return `${chatId}:${userId}`;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* 异步信号量:限制同时在跑的 Claude 任务数。
|
|
7
|
+
* acquire() 返回一个 release 函数,调用后唤醒最早排队者(FIFO)。
|
|
8
|
+
*
|
|
9
|
+
* - 许可转移式 release:有等待者时不减 active,直接把许可转移给队首等待者,
|
|
10
|
+
* 消除「release 先减、waiter 恢复后加」的 microtask 窗口内被新 acquire 插队超发。
|
|
11
|
+
* - 每个 release 带 once 保护:重复调用为 no-op,防止 active 递减为负。
|
|
12
|
+
*/
|
|
13
|
+
export class Semaphore {
|
|
14
|
+
n;
|
|
15
|
+
active = 0;
|
|
16
|
+
waiters = [];
|
|
17
|
+
constructor(n) {
|
|
18
|
+
this.n = n;
|
|
19
|
+
}
|
|
20
|
+
async acquire() {
|
|
21
|
+
if (this.active < this.n) {
|
|
22
|
+
this.active++;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
// 排队等待;许可由 release() 原地转移(active 保持),唤醒后不再自增
|
|
26
|
+
await new Promise((resolve) => this.waiters.push(resolve));
|
|
27
|
+
}
|
|
28
|
+
return this.makeRelease();
|
|
29
|
+
}
|
|
30
|
+
makeRelease() {
|
|
31
|
+
let released = false;
|
|
32
|
+
return () => {
|
|
33
|
+
if (released)
|
|
34
|
+
return; // 重复释放为 no-op
|
|
35
|
+
released = true;
|
|
36
|
+
const next = this.waiters.shift();
|
|
37
|
+
if (next)
|
|
38
|
+
next(); // 许可直接转移给队首等待者,active 不变
|
|
39
|
+
else
|
|
40
|
+
this.active--;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=channel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel.js","sourceRoot":"","sources":["../../src/session/channel.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,MAAM,UAAU,UAAU,CAAC,MAAc,EAAE,MAAc;IACvD,OAAO,GAAG,MAAM,IAAI,MAAM,EAAE,CAAC;AAC/B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,SAAS;IAIA,CAAC;IAHb,MAAM,GAAG,CAAC,CAAC;IACX,OAAO,GAAsB,EAAE,CAAC;IAExC,YAAoB,CAAS;iBAAT,CAAC;IAAW,CAAC;IAEjC,KAAK,CAAC,OAAO;QACX,IAAI,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC;YACzB,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,CAAC;aAAM,CAAC;YACN,6CAA6C;YAC7C,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;QACnE,CAAC;QACD,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC;IAC5B,CAAC;IAEO,WAAW;QACjB,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,OAAO,GAAG,EAAE;YACV,IAAI,QAAQ;gBAAE,OAAO,CAAC,cAAc;YACpC,QAAQ,GAAG,IAAI,CAAC;YAChB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YAClC,IAAI,IAAI;gBAAE,IAAI,EAAE,CAAC,CAAC,yBAAyB;;gBACtC,IAAI,CAAC,MAAM,EAAE,CAAC;QACrB,CAAC,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BridgeConfig } from '../types.js';
|
|
2
|
+
import type { SessionStore } from './session-store.js';
|
|
3
|
+
export interface CommandContext {
|
|
4
|
+
channelKey: string;
|
|
5
|
+
store: SessionStore;
|
|
6
|
+
config: BridgeConfig;
|
|
7
|
+
/** 本机器人显示名:多机器人同群/多会话时 /status 需要辨认对谁说话 */
|
|
8
|
+
appName: string;
|
|
9
|
+
/** member 不能 /ws use 切换工作区(spec 权限分级) */
|
|
10
|
+
isAdmin: boolean;
|
|
11
|
+
currentWorkspace(): string;
|
|
12
|
+
stopCurrentTask(): boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface CommandResult {
|
|
15
|
+
handled: boolean;
|
|
16
|
+
reply?: string;
|
|
17
|
+
taskText?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 斜杠命令处理。非 / 开头的文本返回 { handled: false, taskText } 交给任务链路。
|
|
21
|
+
* 会话编号约定:列表按时间正序展示(1 = 最早归档);
|
|
22
|
+
* 内部存储为新→旧(sessions[0] 为当前),故编号 n 对应内部下标 length - n。
|
|
23
|
+
*/
|
|
24
|
+
export declare function handleCommand(text: string, ctx: CommandContext): Promise<CommandResult>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const HELP = `**可用命令**
|
|
2
|
+
/new — 开启新会话
|
|
3
|
+
/resume — 列出历史会话
|
|
4
|
+
/resume <编号> — 恢复指定会话
|
|
5
|
+
/stop — 停止当前任务
|
|
6
|
+
/status — 查看当前状态
|
|
7
|
+
/ws list — 列出工作区
|
|
8
|
+
/ws use <名字> — 切换工作区
|
|
9
|
+
/help — 帮助`;
|
|
10
|
+
/**
|
|
11
|
+
* 斜杠命令处理。非 / 开头的文本返回 { handled: false, taskText } 交给任务链路。
|
|
12
|
+
* 会话编号约定:列表按时间正序展示(1 = 最早归档);
|
|
13
|
+
* 内部存储为新→旧(sessions[0] 为当前),故编号 n 对应内部下标 length - n。
|
|
14
|
+
*/
|
|
15
|
+
export async function handleCommand(text, ctx) {
|
|
16
|
+
if (!text.startsWith('/'))
|
|
17
|
+
return { handled: false, taskText: text };
|
|
18
|
+
const [name, ...args] = text.slice(1).trim().split(/\s+/);
|
|
19
|
+
const { store, channelKey: key } = ctx;
|
|
20
|
+
switch (name) {
|
|
21
|
+
case 'help':
|
|
22
|
+
return { handled: true, reply: HELP };
|
|
23
|
+
case 'new': {
|
|
24
|
+
const ws = ctx.currentWorkspace();
|
|
25
|
+
const st = store.getChannelState(key);
|
|
26
|
+
// 显式清空历史(保留工作区归属),会话指针重置为空
|
|
27
|
+
store.setChannelState(key, { workspaceName: st?.workspaceName ?? ws, sessions: [] });
|
|
28
|
+
return { handled: true, reply: `✅ 已开启新会话(工作区:${ws})` };
|
|
29
|
+
}
|
|
30
|
+
case 'resume': {
|
|
31
|
+
const sessions = store.listSessions(key);
|
|
32
|
+
if (args.length === 0) {
|
|
33
|
+
if (sessions.length === 0)
|
|
34
|
+
return { handled: true, reply: '暂无历史会话' };
|
|
35
|
+
// 按时间正序展示:最早的排最前
|
|
36
|
+
const chrono = [...sessions].reverse();
|
|
37
|
+
const list = chrono
|
|
38
|
+
.map((s, i) => `${i + 1}. ${s.summary || '(无摘要)'} <font color='grey'>${s.updatedAt.slice(0, 16)}</font>`)
|
|
39
|
+
.join('\n');
|
|
40
|
+
return { handled: true, reply: `**历史会话**(回复 /resume 编号 恢复)\n${list}` };
|
|
41
|
+
}
|
|
42
|
+
const n = Number(args[0]);
|
|
43
|
+
if (!Number.isInteger(n) || n < 1 || n > sessions.length) {
|
|
44
|
+
return { handled: true, reply: `编号无效,范围 1-${sessions.length}` };
|
|
45
|
+
}
|
|
46
|
+
const target = sessions[sessions.length - n]; // 编号 n → 时间正序第 n 个
|
|
47
|
+
const st = store.getChannelState(key);
|
|
48
|
+
if (st) {
|
|
49
|
+
// 恢复 = 将选中会话移到头部(当前)
|
|
50
|
+
st.sessions = [target, ...st.sessions.filter((s) => s.sessionId !== target.sessionId)];
|
|
51
|
+
store.setChannelState(key, st);
|
|
52
|
+
}
|
|
53
|
+
return { handled: true, reply: `↩️ 已恢复会话:${target.summary || target.sessionId}` };
|
|
54
|
+
}
|
|
55
|
+
case 'stop':
|
|
56
|
+
return { handled: true, reply: ctx.stopCurrentTask() ? '🛑 已发送停止信号' : '当前没有运行中的任务' };
|
|
57
|
+
case 'status': {
|
|
58
|
+
const st = store.getChannelState(key);
|
|
59
|
+
return {
|
|
60
|
+
handled: true,
|
|
61
|
+
reply: `机器人:**${ctx.appName}**\n工作区:**${st?.workspaceName ?? ctx.currentWorkspace()}**\n历史会话:${store.listSessions(key).length} 个`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
case 'ws': {
|
|
65
|
+
const sub = args[0];
|
|
66
|
+
if (sub === 'list') {
|
|
67
|
+
const list = ctx.config.workspaces.map((w) => `- **${w.name}** \`${w.path}\``).join('\n');
|
|
68
|
+
return { handled: true, reply: `**工作区列表**\n${list}` };
|
|
69
|
+
}
|
|
70
|
+
if (sub === 'use') {
|
|
71
|
+
if (!ctx.isAdmin)
|
|
72
|
+
return { handled: true, reply: '⛔ 切换工作区仅管理员可用' };
|
|
73
|
+
const target = ctx.config.workspaces.find((w) => w.name === args[1]);
|
|
74
|
+
if (!target)
|
|
75
|
+
return { handled: true, reply: `工作区 "${args[1] ?? ''}" 不存在,/ws list 查看` };
|
|
76
|
+
const st = store.getChannelState(key);
|
|
77
|
+
// 切换工作区保留历史会话记录
|
|
78
|
+
store.setChannelState(key, { workspaceName: target.name, sessions: st?.sessions ?? [] });
|
|
79
|
+
return { handled: true, reply: `✅ 已切换工作区:**${target.name}**(${target.path})` };
|
|
80
|
+
}
|
|
81
|
+
return { handled: true, reply: '用法:/ws list | /ws use <名字>' };
|
|
82
|
+
}
|
|
83
|
+
default:
|
|
84
|
+
return { handled: true, reply: `未知命令 /${name},/help 查看帮助` };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=commands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../../src/session/commands.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface SessionMeta {
|
|
2
|
+
sessionId: string;
|
|
3
|
+
summary: string;
|
|
4
|
+
updatedAt: string;
|
|
5
|
+
}
|
|
6
|
+
/** 通道状态:sessions[0] 为当前会话,其余按新→旧排列 */
|
|
7
|
+
export interface ChannelState {
|
|
8
|
+
workspaceName: string;
|
|
9
|
+
sessions: SessionMeta[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 通道会话存储:JSON 文件持久化,每个 channelKey 一份 ChannelState。
|
|
13
|
+
* 约定:sessions 数组按新→旧排列,sessions[0] 即当前会话;
|
|
14
|
+
* /resume 列表展示时按时间正序编号(1 = 最早归档),恢复后移回数组头部。
|
|
15
|
+
*/
|
|
16
|
+
export declare class SessionStore {
|
|
17
|
+
private path;
|
|
18
|
+
private data;
|
|
19
|
+
private constructor();
|
|
20
|
+
static load(path: string): SessionStore;
|
|
21
|
+
private save;
|
|
22
|
+
getChannelState(key: string): ChannelState | undefined;
|
|
23
|
+
setChannelState(key: string, state: ChannelState): void;
|
|
24
|
+
/** 归档会话并置为当前(头插,去重;summary 截断到 60 字) */
|
|
25
|
+
archiveSession(key: string, sessionId: string, summary: string): void;
|
|
26
|
+
/**
|
|
27
|
+
* 开启新会话:channel 不存在时以 defaultWorkspace 初始化;
|
|
28
|
+
* 已存在时保持工作区与历史不变(历史由 /new 命令显式清空)。
|
|
29
|
+
*/
|
|
30
|
+
newSession(key: string, defaultWorkspace: string): void;
|
|
31
|
+
listSessions(key: string): SessionMeta[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 一次性迁移:旧单应用的 sessions.json → sessions.<appId>.json。
|
|
35
|
+
* 归一化保证旧 feishu 应用恒为 apps[0],与「旧数据属于原单应用」语义吻合;
|
|
36
|
+
* channelKey 格式未变,纯文件改名即完成迁移(历史会话立即可 /resume)。
|
|
37
|
+
* - 旧文件不存在 → 'noop'
|
|
38
|
+
* - 旧文件存在且目标分片不存在 → 改名 → 'migrated'
|
|
39
|
+
* - 两者都存在 → 旧文件改名 .bak 保全(不覆盖新分片);.bak 已存在则完全不动 → 'conflict-kept'
|
|
40
|
+
* 只应在 startBridge 装配前调用一次(单进程无竞态)。
|
|
41
|
+
*/
|
|
42
|
+
export declare function migrateLegacySessions(configDir: string, firstAppId: string): 'noop' | 'migrated' | 'conflict-kept';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* 通道会话存储:JSON 文件持久化,每个 channelKey 一份 ChannelState。
|
|
5
|
+
* 约定:sessions 数组按新→旧排列,sessions[0] 即当前会话;
|
|
6
|
+
* /resume 列表展示时按时间正序编号(1 = 最早归档),恢复后移回数组头部。
|
|
7
|
+
*/
|
|
8
|
+
export class SessionStore {
|
|
9
|
+
path;
|
|
10
|
+
data = {};
|
|
11
|
+
constructor(path) {
|
|
12
|
+
this.path = path;
|
|
13
|
+
if (existsSync(path)) {
|
|
14
|
+
try {
|
|
15
|
+
this.data = JSON.parse(readFileSync(path, 'utf8'));
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
this.data = {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
static load(path) {
|
|
23
|
+
return new SessionStore(path);
|
|
24
|
+
}
|
|
25
|
+
save() {
|
|
26
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
27
|
+
writeFileSync(this.path, JSON.stringify(this.data, null, 2), 'utf8');
|
|
28
|
+
}
|
|
29
|
+
getChannelState(key) {
|
|
30
|
+
return this.data[key];
|
|
31
|
+
}
|
|
32
|
+
setChannelState(key, state) {
|
|
33
|
+
this.data[key] = state;
|
|
34
|
+
this.save();
|
|
35
|
+
}
|
|
36
|
+
/** 归档会话并置为当前(头插,去重;summary 截断到 60 字) */
|
|
37
|
+
archiveSession(key, sessionId, summary) {
|
|
38
|
+
const st = this.data[key] ?? { workspaceName: '', sessions: [] };
|
|
39
|
+
const rest = st.sessions.filter((s) => s.sessionId !== sessionId);
|
|
40
|
+
st.sessions = [{ sessionId, summary: summary.slice(0, 60), updatedAt: new Date().toISOString() }, ...rest];
|
|
41
|
+
this.data[key] = st;
|
|
42
|
+
this.save();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 开启新会话:channel 不存在时以 defaultWorkspace 初始化;
|
|
46
|
+
* 已存在时保持工作区与历史不变(历史由 /new 命令显式清空)。
|
|
47
|
+
*/
|
|
48
|
+
newSession(key, defaultWorkspace) {
|
|
49
|
+
if (!this.data[key]) {
|
|
50
|
+
this.setChannelState(key, { workspaceName: defaultWorkspace, sessions: [] });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
listSessions(key) {
|
|
54
|
+
return this.data[key]?.sessions ?? [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 一次性迁移:旧单应用的 sessions.json → sessions.<appId>.json。
|
|
59
|
+
* 归一化保证旧 feishu 应用恒为 apps[0],与「旧数据属于原单应用」语义吻合;
|
|
60
|
+
* channelKey 格式未变,纯文件改名即完成迁移(历史会话立即可 /resume)。
|
|
61
|
+
* - 旧文件不存在 → 'noop'
|
|
62
|
+
* - 旧文件存在且目标分片不存在 → 改名 → 'migrated'
|
|
63
|
+
* - 两者都存在 → 旧文件改名 .bak 保全(不覆盖新分片);.bak 已存在则完全不动 → 'conflict-kept'
|
|
64
|
+
* 只应在 startBridge 装配前调用一次(单进程无竞态)。
|
|
65
|
+
*/
|
|
66
|
+
export function migrateLegacySessions(configDir, firstAppId) {
|
|
67
|
+
const legacyPath = join(configDir, 'sessions.json');
|
|
68
|
+
if (!existsSync(legacyPath))
|
|
69
|
+
return 'noop';
|
|
70
|
+
const shardedPath = join(configDir, `sessions.${firstAppId}.json`);
|
|
71
|
+
if (!existsSync(shardedPath)) {
|
|
72
|
+
renameSync(legacyPath, shardedPath);
|
|
73
|
+
return 'migrated';
|
|
74
|
+
}
|
|
75
|
+
const bakPath = join(configDir, 'sessions.json.bak');
|
|
76
|
+
if (!existsSync(bakPath)) {
|
|
77
|
+
renameSync(legacyPath, bakPath);
|
|
78
|
+
console.warn(`[迁移] sessions.json 与 sessions.${firstAppId}.json 并存,旧文件已保全为 sessions.json.bak(请人工确认归属)`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
console.warn('[迁移] sessions.json / 分片文件 / .bak 三者并存,保持原状不动(请人工处理)');
|
|
82
|
+
}
|
|
83
|
+
return 'conflict-kept';
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=session-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-store.js","sourceRoot":"","sources":["../../src/session/session-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TriggerRule } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* 命中触发词则返回改写后的 prompt,未命中返回 null(走原链路)。
|
|
4
|
+
*
|
|
5
|
+
* 匹配语义:
|
|
6
|
+
* - 斜杠形态(match 以 / 开头):消息首 token 精确相等才命中(/produce 命中,/produced 不命中);
|
|
7
|
+
* 本地命令(RESERVED)最高优先级直接放行;斜杠消息未命中任何规则也返回 null——
|
|
8
|
+
* 保持 handleCommand 的「未知命令」防呆 UX,不被关键词规则误捞。
|
|
9
|
+
* - 关键词形态(match 不带 /):整条消息包含该关键词即命中,按数组顺序首个生效。
|
|
10
|
+
* 占位符:{text}=原始全文,{args}=去掉首 token 的剩余参数(斜杠形态)/ 全文(关键词形态)。
|
|
11
|
+
*/
|
|
12
|
+
export declare function rewriteByTrigger(text: string, rules: TriggerRule[] | undefined): string | null;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// bridge 本地命令首 token(去 / 后比对):触发词不得劫持本地命令——/stop 永远是停止任务
|
|
2
|
+
const RESERVED_COMMANDS = new Set(['help', 'new', 'resume', 'stop', 'status', 'ws']);
|
|
3
|
+
function applyRewrite(template, text, args) {
|
|
4
|
+
return template.replaceAll('{text}', text).replaceAll('{args}', args);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* 命中触发词则返回改写后的 prompt,未命中返回 null(走原链路)。
|
|
8
|
+
*
|
|
9
|
+
* 匹配语义:
|
|
10
|
+
* - 斜杠形态(match 以 / 开头):消息首 token 精确相等才命中(/produce 命中,/produced 不命中);
|
|
11
|
+
* 本地命令(RESERVED)最高优先级直接放行;斜杠消息未命中任何规则也返回 null——
|
|
12
|
+
* 保持 handleCommand 的「未知命令」防呆 UX,不被关键词规则误捞。
|
|
13
|
+
* - 关键词形态(match 不带 /):整条消息包含该关键词即命中,按数组顺序首个生效。
|
|
14
|
+
* 占位符:{text}=原始全文,{args}=去掉首 token 的剩余参数(斜杠形态)/ 全文(关键词形态)。
|
|
15
|
+
*/
|
|
16
|
+
export function rewriteByTrigger(text, rules) {
|
|
17
|
+
const t = text.trim();
|
|
18
|
+
if (!t || !rules?.length)
|
|
19
|
+
return null;
|
|
20
|
+
const firstToken = t.split(/\s+/)[0] ?? '';
|
|
21
|
+
if (firstToken.startsWith('/')) {
|
|
22
|
+
if (RESERVED_COMMANDS.has(firstToken.slice(1).toLowerCase()))
|
|
23
|
+
return null;
|
|
24
|
+
const rule = rules.find((r) => r.match.startsWith('/') && r.match === firstToken);
|
|
25
|
+
if (!rule)
|
|
26
|
+
return null;
|
|
27
|
+
return applyRewrite(rule.rewrite, t, t.slice(firstToken.length).trim());
|
|
28
|
+
}
|
|
29
|
+
const rule = rules.find((r) => !r.match.startsWith('/') && t.includes(r.match));
|
|
30
|
+
if (!rule)
|
|
31
|
+
return null;
|
|
32
|
+
return applyRewrite(rule.rewrite, t, t);
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=triggers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"triggers.js","sourceRoot":"","sources":["../../src/session/triggers.ts"],"names":[],"mappings":"AAIA,yDAAyD;AACzD,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;AAErF,SAAS,YAAY,CAAC,QAAgB,EAAE,IAAY,EAAE,IAAY;IAChE,OAAO,QAAQ,CAAC,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AACxE,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,KAAgC;IAC7E,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IACtB,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM;QAAE,OAAO,IAAI,CAAC;IACtC,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC3C,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/B,IAAI,iBAAiB,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;YAAE,OAAO,IAAI,CAAC;QAC1E,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC;QAClF,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC;QACvB,OAAO,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAC1E,CAAC;IACD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;IAChF,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,OAAO,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAC1C,CAAC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export type TranscriptEvent = {
|
|
2
|
+
v: 1;
|
|
3
|
+
ts: string;
|
|
4
|
+
kind: 'user';
|
|
5
|
+
app: string;
|
|
6
|
+
chatId: string;
|
|
7
|
+
userId: string;
|
|
8
|
+
workspace?: string;
|
|
9
|
+
sessionId?: string;
|
|
10
|
+
text: string;
|
|
11
|
+
} | {
|
|
12
|
+
v: 1;
|
|
13
|
+
ts: string;
|
|
14
|
+
kind: 'assistant';
|
|
15
|
+
app: string;
|
|
16
|
+
chatId: string;
|
|
17
|
+
userId: string;
|
|
18
|
+
sessionId?: string;
|
|
19
|
+
text: string;
|
|
20
|
+
} | {
|
|
21
|
+
v: 1;
|
|
22
|
+
ts: string;
|
|
23
|
+
kind: 'tool';
|
|
24
|
+
app: string;
|
|
25
|
+
chatId: string;
|
|
26
|
+
userId: string;
|
|
27
|
+
sessionId?: string;
|
|
28
|
+
phase: 'start' | 'result';
|
|
29
|
+
tool: string;
|
|
30
|
+
summary: string;
|
|
31
|
+
ok: boolean;
|
|
32
|
+
} | {
|
|
33
|
+
v: 1;
|
|
34
|
+
ts: string;
|
|
35
|
+
kind: 'result';
|
|
36
|
+
app: string;
|
|
37
|
+
chatId: string;
|
|
38
|
+
userId: string;
|
|
39
|
+
workspace?: string;
|
|
40
|
+
sessionId: string;
|
|
41
|
+
subtype: 'success' | 'stopped' | 'error';
|
|
42
|
+
text: string;
|
|
43
|
+
producedFiles?: string[];
|
|
44
|
+
turns?: number;
|
|
45
|
+
};
|
|
46
|
+
/** 落盘记录器 DI 接口:wiring 层依赖此抽象,测试可注入 mock */
|
|
47
|
+
export interface TranscriptRecorder {
|
|
48
|
+
user(ev: TranscriptEvent & {
|
|
49
|
+
kind: 'user';
|
|
50
|
+
}): void;
|
|
51
|
+
assistant(ev: TranscriptEvent & {
|
|
52
|
+
kind: 'assistant';
|
|
53
|
+
}): void;
|
|
54
|
+
tool(ev: TranscriptEvent & {
|
|
55
|
+
kind: 'tool';
|
|
56
|
+
}): void;
|
|
57
|
+
result(ev: TranscriptEvent & {
|
|
58
|
+
kind: 'result';
|
|
59
|
+
}): void;
|
|
60
|
+
}
|
|
61
|
+
/** 本地时区日期(避免 toISOString 的 UTC 偏移导致跨天分文件错位) */
|
|
62
|
+
export declare function localDate(d?: Date): string;
|
|
63
|
+
export declare class TranscriptWriter implements TranscriptRecorder {
|
|
64
|
+
private baseDir;
|
|
65
|
+
/** 已确认存在的会话目录缓存:避免每行一次 existsSync/mkdirSync syscall */
|
|
66
|
+
private madeDirs;
|
|
67
|
+
/** 告警节流:同一分钟内最多告警一次(失败风暴不刷屏) */
|
|
68
|
+
private lastWarnMin;
|
|
69
|
+
constructor(baseDir: string);
|
|
70
|
+
user(ev: TranscriptEvent & {
|
|
71
|
+
kind: 'user';
|
|
72
|
+
}): void;
|
|
73
|
+
assistant(ev: TranscriptEvent & {
|
|
74
|
+
kind: 'assistant';
|
|
75
|
+
}): void;
|
|
76
|
+
tool(ev: TranscriptEvent & {
|
|
77
|
+
kind: 'tool';
|
|
78
|
+
}): void;
|
|
79
|
+
result(ev: TranscriptEvent & {
|
|
80
|
+
kind: 'result';
|
|
81
|
+
}): void;
|
|
82
|
+
private append;
|
|
83
|
+
}
|
|
84
|
+
/** 清理过期落盘:递归删除 baseDir 下 mtime 早于 retentionDays 的 .jsonl,返回删除文件数 */
|
|
85
|
+
export declare function sweepTranscripts(baseDir: string, retentionDays: number): number;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// 对话内容落盘:用户消息 / Claude 回复 / 工具调用 / 最终结果 全文持久化为 JSONL。
|
|
2
|
+
// 布局 <CONFIG_DIR>/transcripts/<appId>/<chatId>/<本地日期>.jsonl,按天分文件即天然轮转。
|
|
3
|
+
// 为后续「对话挖掘写 Notion」打底(v:1 版本号为挖掘管道的格式演化留空间)。
|
|
4
|
+
// 设计约束:任何写失败只节流告警,绝不向调用方抛出、绝不阻断任务主流程。
|
|
5
|
+
import { appendFileSync, mkdirSync, readdirSync, statSync, unlinkSync, existsSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
/** 本地时区日期(避免 toISOString 的 UTC 偏移导致跨天分文件错位) */
|
|
8
|
+
export function localDate(d = new Date()) {
|
|
9
|
+
const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
10
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
11
|
+
return `${d.getFullYear()}-${m}-${day}`;
|
|
12
|
+
}
|
|
13
|
+
export class TranscriptWriter {
|
|
14
|
+
baseDir;
|
|
15
|
+
/** 已确认存在的会话目录缓存:避免每行一次 existsSync/mkdirSync syscall */
|
|
16
|
+
madeDirs = new Set();
|
|
17
|
+
/** 告警节流:同一分钟内最多告警一次(失败风暴不刷屏) */
|
|
18
|
+
lastWarnMin = 0;
|
|
19
|
+
constructor(baseDir) {
|
|
20
|
+
this.baseDir = baseDir;
|
|
21
|
+
}
|
|
22
|
+
user(ev) { this.append(ev); }
|
|
23
|
+
assistant(ev) { this.append(ev); }
|
|
24
|
+
tool(ev) { this.append(ev); }
|
|
25
|
+
result(ev) { this.append(ev); }
|
|
26
|
+
append(ev) {
|
|
27
|
+
try {
|
|
28
|
+
const dir = join(this.baseDir, ev.chatId);
|
|
29
|
+
if (!this.madeDirs.has(dir)) {
|
|
30
|
+
mkdirSync(dir, { recursive: true });
|
|
31
|
+
this.madeDirs.add(dir);
|
|
32
|
+
}
|
|
33
|
+
appendFileSync(join(dir, `${localDate()}.jsonl`), JSON.stringify(ev) + '\n', 'utf8');
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
const min = Math.floor(Date.now() / 60_000);
|
|
37
|
+
if (min !== this.lastWarnMin) {
|
|
38
|
+
this.lastWarnMin = min;
|
|
39
|
+
console.warn('[transcripts] 对话落盘失败(不影响任务执行):', e instanceof Error ? e.message : e);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** 清理过期落盘:递归删除 baseDir 下 mtime 早于 retentionDays 的 .jsonl,返回删除文件数 */
|
|
45
|
+
export function sweepTranscripts(baseDir, retentionDays) {
|
|
46
|
+
if (!existsSync(baseDir))
|
|
47
|
+
return 0;
|
|
48
|
+
const cutoff = Date.now() - retentionDays * 86_400_000;
|
|
49
|
+
let removed = 0;
|
|
50
|
+
const walk = (dir) => {
|
|
51
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
52
|
+
const p = join(dir, entry.name);
|
|
53
|
+
if (entry.isDirectory())
|
|
54
|
+
walk(p);
|
|
55
|
+
else if (entry.name.endsWith('.jsonl') && statSync(p).mtimeMs < cutoff) {
|
|
56
|
+
unlinkSync(p);
|
|
57
|
+
removed++;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
walk(baseDir);
|
|
62
|
+
return removed;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=transcript-writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcript-writer.js","sourceRoot":"","sources":["../../src/transcript/transcript-writer.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** 触发词规则:match 命中后把消息改写为 rewrite 再入队({text}=原文全文,{args}=去首 token 的剩余参数) */
|
|
2
|
+
export interface TriggerRule {
|
|
3
|
+
match: string;
|
|
4
|
+
rewrite: string;
|
|
5
|
+
}
|
|
6
|
+
/** 显式加载的本地插件(path 指含 .claude-plugin/plugin.json 的插件目录) */
|
|
7
|
+
export interface PluginRef {
|
|
8
|
+
name: string;
|
|
9
|
+
path: string;
|
|
10
|
+
}
|
|
11
|
+
export interface FeishuAppConfig {
|
|
12
|
+
/** 显示名(日志前缀 / /status / lcb app list),缺省 = appId */
|
|
13
|
+
name: string;
|
|
14
|
+
appId: string;
|
|
15
|
+
appSecret: string;
|
|
16
|
+
domain?: 'feishu' | 'lark';
|
|
17
|
+
/** 该 app 的默认工作区(须在 workspaces 列表内),缺省回退 defaults.workspace */
|
|
18
|
+
defaultWorkspace?: string;
|
|
19
|
+
/** 该 app 的并发上限(1-100),缺省用全局 concurrency */
|
|
20
|
+
concurrency?: number;
|
|
21
|
+
/** 可选人格补充,直通 SDK options.appendSystemPrompt */
|
|
22
|
+
appendSystemPrompt?: string;
|
|
23
|
+
/** Claude Code 数据目录(会话存储/用户设置/插件,每个机器人一套):
|
|
24
|
+
* apps 里新配置的 app 缺省 <CONFIG_DIR>/claude/<appId>;旧 feishu 迁移的 app 沿用系统默认 ~/.claude(保存量会话 resume) */
|
|
25
|
+
claudeConfigDir?: string;
|
|
26
|
+
/** per-app 环境变量(如 ANTHROPIC_BASE_URL/ANTHROPIC_MODEL 凭证差异化),合并进 Claude Code 子进程环境 */
|
|
27
|
+
env?: Record<string, string>;
|
|
28
|
+
/** 触发词映射:按数组顺序首个命中生效;不配置 = 行为零变化。match 以 / 开头=首 token 精确匹配,否则=关键词包含 */
|
|
29
|
+
triggers?: TriggerRule[];
|
|
30
|
+
/** 显式加载的本地插件目录列表(SDK 单次 query 不会展开斜杠命令,技能须经改写指令触发;插件目录须含 .claude-plugin/plugin.json) */
|
|
31
|
+
plugins?: PluginRef[];
|
|
32
|
+
}
|
|
33
|
+
export interface Workspace {
|
|
34
|
+
name: string;
|
|
35
|
+
path: string;
|
|
36
|
+
}
|
|
37
|
+
export interface BridgeConfig {
|
|
38
|
+
/** 多飞书应用(多机器人):每 app 一条独立 WS 长连接 + 独立会话池 + 独立 Claude Code 实例 */
|
|
39
|
+
apps: FeishuAppConfig[];
|
|
40
|
+
workspaces: Workspace[];
|
|
41
|
+
defaults: {
|
|
42
|
+
workspace: string;
|
|
43
|
+
};
|
|
44
|
+
concurrency: number;
|
|
45
|
+
/** 对话落盘清理策略;缺省/0 = 永久保留 */
|
|
46
|
+
transcripts?: {
|
|
47
|
+
retentionDays?: number;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export interface IncomingMessage {
|
|
51
|
+
chatId: string;
|
|
52
|
+
chatType: 'p2p' | 'group';
|
|
53
|
+
userId: string;
|
|
54
|
+
text: string;
|
|
55
|
+
messageId: string;
|
|
56
|
+
}
|
|
57
|
+
export interface CardActionValue {
|
|
58
|
+
requestId: string;
|
|
59
|
+
decision: 'allow' | 'deny' | 'allow-session';
|
|
60
|
+
}
|
|
61
|
+
export interface CardActionEvent {
|
|
62
|
+
value: CardActionValue;
|
|
63
|
+
operatorId: string;
|
|
64
|
+
openMessageId: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* card.action.trigger 回调的返回体:非空时由 gateway 透传给飞书。
|
|
68
|
+
* toast 为客户端弹窗提示;card 为回调响应内联新卡片,飞书收到响应后同步替换
|
|
69
|
+
* 被点击的卡片(3 秒内响应有效)——仅发起人有效点击路径使用,
|
|
70
|
+
* !pending 等分支严禁携带(此时卡片可能已是结果卡,带卡会把结果卡换回带按钮状态,
|
|
71
|
+
* 等同剥夺发起人操作权,参照 I2 教训)。
|
|
72
|
+
*/
|
|
73
|
+
export interface CardActionResponse {
|
|
74
|
+
toast?: {
|
|
75
|
+
type: 'info' | 'success' | 'error' | 'warning';
|
|
76
|
+
content: string;
|
|
77
|
+
};
|
|
78
|
+
card?: {
|
|
79
|
+
type: 'raw';
|
|
80
|
+
data: unknown;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export interface GatewayHandlers {
|
|
84
|
+
onMessage(msg: IncomingMessage): Promise<void>;
|
|
85
|
+
onCardAction(action: CardActionEvent): Promise<CardActionResponse | void>;
|
|
86
|
+
}
|
|
87
|
+
export interface ConfirmationRequest {
|
|
88
|
+
requestId: string;
|
|
89
|
+
toolName: string;
|
|
90
|
+
summary: string;
|
|
91
|
+
diff?: string;
|
|
92
|
+
workspaceName: string;
|
|
93
|
+
}
|
|
94
|
+
export type PermissionDecision = 'allow' | 'deny' | 'allow-session';
|
|
95
|
+
export interface ProgressEvent {
|
|
96
|
+
kind: 'text' | 'tool-start' | 'tool-result' | 'status';
|
|
97
|
+
content: string;
|
|
98
|
+
ok?: boolean;
|
|
99
|
+
}
|
|
100
|
+
export interface TaskOutcome {
|
|
101
|
+
sessionId: string;
|
|
102
|
+
finalText: string;
|
|
103
|
+
producedFiles: string[];
|
|
104
|
+
turns: number;
|
|
105
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const VERSION = "0.2.0";
|
package/dist/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC"}
|