@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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +183 -0
  3. package/config.example.yaml +42 -0
  4. package/deploy/com.lark-claudecode-bridge.plist +9 -0
  5. package/deploy/lark-claudecode-bridge.service +11 -0
  6. package/deploy/windows-start.bat +2 -0
  7. package/dist/access/access-control.d.ts +43 -0
  8. package/dist/access/access-control.js +88 -0
  9. package/dist/access/access-control.js.map +1 -0
  10. package/dist/bin/lcb.d.ts +2 -0
  11. package/dist/bin/lcb.js +191 -0
  12. package/dist/bin/lcb.js.map +1 -0
  13. package/dist/cli/app-manager.d.ts +28 -0
  14. package/dist/cli/app-manager.js +99 -0
  15. package/dist/cli/app-manager.js.map +1 -0
  16. package/dist/cli/asker.d.ts +7 -0
  17. package/dist/cli/asker.js +41 -0
  18. package/dist/cli/asker.js.map +1 -0
  19. package/dist/cli/setup-wizard.d.ts +28 -0
  20. package/dist/cli/setup-wizard.js +114 -0
  21. package/dist/cli/setup-wizard.js.map +1 -0
  22. package/dist/cli/stdin-pairing.d.ts +7 -0
  23. package/dist/cli/stdin-pairing.js +19 -0
  24. package/dist/cli/stdin-pairing.js.map +1 -0
  25. package/dist/cli/ws-manager.d.ts +11 -0
  26. package/dist/cli/ws-manager.js +67 -0
  27. package/dist/cli/ws-manager.js.map +1 -0
  28. package/dist/config.d.ts +7 -0
  29. package/dist/config.js +169 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/executor/claude-executor.d.ts +28 -0
  32. package/dist/executor/claude-executor.js +84 -0
  33. package/dist/executor/claude-executor.js.map +1 -0
  34. package/dist/executor/notify-server.d.ts +34 -0
  35. package/dist/executor/notify-server.js +193 -0
  36. package/dist/executor/notify-server.js.map +1 -0
  37. package/dist/executor/output-collector.d.ts +5 -0
  38. package/dist/executor/output-collector.js +17 -0
  39. package/dist/executor/output-collector.js.map +1 -0
  40. package/dist/executor/permission-gate.d.ts +20 -0
  41. package/dist/executor/permission-gate.js +66 -0
  42. package/dist/executor/permission-gate.js.map +1 -0
  43. package/dist/gateway/card-builder.d.ts +15 -0
  44. package/dist/gateway/card-builder.js +60 -0
  45. package/dist/gateway/card-builder.js.map +1 -0
  46. package/dist/gateway/feishu-gateway.d.ts +164 -0
  47. package/dist/gateway/feishu-gateway.js +249 -0
  48. package/dist/gateway/feishu-gateway.js.map +1 -0
  49. package/dist/gateway/progress-card.d.ts +34 -0
  50. package/dist/gateway/progress-card.js +106 -0
  51. package/dist/gateway/progress-card.js.map +1 -0
  52. package/dist/index.d.ts +38 -0
  53. package/dist/index.js +433 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/session/channel.d.ts +18 -0
  56. package/dist/session/channel.js +44 -0
  57. package/dist/session/channel.js.map +1 -0
  58. package/dist/session/commands.d.ts +24 -0
  59. package/dist/session/commands.js +87 -0
  60. package/dist/session/commands.js.map +1 -0
  61. package/dist/session/session-store.d.ts +42 -0
  62. package/dist/session/session-store.js +85 -0
  63. package/dist/session/session-store.js.map +1 -0
  64. package/dist/session/triggers.d.ts +12 -0
  65. package/dist/session/triggers.js +34 -0
  66. package/dist/session/triggers.js.map +1 -0
  67. package/dist/transcript/transcript-writer.d.ts +85 -0
  68. package/dist/transcript/transcript-writer.js +64 -0
  69. package/dist/transcript/transcript-writer.js.map +1 -0
  70. package/dist/types.d.ts +105 -0
  71. package/dist/types.js +2 -0
  72. package/dist/types.js.map +1 -0
  73. package/dist/version.d.ts +1 -0
  74. package/dist/version.js +3 -0
  75. package/dist/version.js.map +1 -0
  76. package/package.json +41 -0
@@ -0,0 +1,66 @@
1
+ // 权限闸:只读工具直通,写操作经 ask 回调询问(飞书卡片确认),带超时自动拒绝与会话级记忆
2
+ import { randomUUID } from 'node:crypto';
3
+ import { NOTIFY_TOOL_PREFIX } from './notify-server.js';
4
+ // 会话内免询问的只读工具白名单(不含任何写文件/执行类工具)
5
+ export const READ_ONLY_TOOLS = new Set([
6
+ 'Read', 'Glob', 'Grep', 'LS', 'TodoRead', 'TodoWrite', 'WebFetch', 'WebSearch',
7
+ ]);
8
+ // 超时默认 10 分钟:飞书端可能长时间无人点击卡片
9
+ const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
10
+ // 内部哨兵:区分「超时拒绝」与「用户主动拒绝」,便于给出准确原因
11
+ const TIMED_OUT = Symbol('timed-out');
12
+ export class PermissionGate {
13
+ opts;
14
+ remembered = new Set();
15
+ constructor(opts) {
16
+ this.opts = opts;
17
+ }
18
+ async decide(toolName, input, workspaceName) {
19
+ if (READ_ONLY_TOOLS.has(toolName))
20
+ return { behavior: 'allow' };
21
+ // lcb-notify 通知工具直通:server 实例由 executeTask 闭包构造,chatId 硬绑定,
22
+ // 模型无法选择接收者(只能发到当前任务发起的聊天),无破坏性,逐张发图不应逐次弹确认卡。
23
+ // 用前缀匹配:后续往 notify server 新增工具自动直通。
24
+ if (toolName.startsWith(NOTIFY_TOOL_PREFIX))
25
+ return { behavior: 'allow' };
26
+ if (this.remembered.has(toolName))
27
+ return { behavior: 'allow' };
28
+ const req = {
29
+ requestId: randomUUID(),
30
+ toolName,
31
+ summary: typeof input.command === 'string' ? input.command
32
+ : typeof input.file_path === 'string' ? input.file_path
33
+ : JSON.stringify(input).slice(0, 800),
34
+ workspaceName,
35
+ };
36
+ const timeoutMs = this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
37
+ // 持 timer handle:ask 及时答复后主动清理,避免常驻进程中每个已答复 decide 留一个最长 10 分钟的存活 timer
38
+ let timer;
39
+ const timeoutPromise = new Promise((resolve) => {
40
+ timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
41
+ timer.unref();
42
+ });
43
+ let decision;
44
+ try {
45
+ decision = await Promise.race([this.opts.ask(req), timeoutPromise]);
46
+ }
47
+ finally {
48
+ clearTimeout(timer);
49
+ }
50
+ if (decision === 'allow-session')
51
+ this.rememberSession(toolName);
52
+ if (decision === TIMED_OUT) {
53
+ return { behavior: 'deny', message: `确认超时(${Math.round(timeoutMs / 60000)} 分钟未响应),已自动拒绝 ${toolName}` };
54
+ }
55
+ if (decision === 'deny')
56
+ return { behavior: 'deny', message: `用户在飞书端拒绝了本次操作(${toolName})` };
57
+ return { behavior: 'allow' };
58
+ }
59
+ rememberSession(toolName) {
60
+ this.remembered.add(toolName);
61
+ }
62
+ reset() {
63
+ this.remembered.clear();
64
+ }
65
+ }
66
+ //# sourceMappingURL=permission-gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-gate.js","sourceRoot":"","sources":["../../src/executor/permission-gate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { ConfirmationRequest, PermissionDecision } from '../types.js';
2
+ export interface ProgressState {
3
+ title: string;
4
+ status: string;
5
+ textTail: string;
6
+ toolLine: string;
7
+ startedAt: number;
8
+ }
9
+ export declare function buildTextCard(markdown: string): unknown;
10
+ /** 图片卡片:caption(可选)显示在图片上方——逐张发图时带编号说明用 */
11
+ export declare function buildImageCard(caption: string | undefined, imgKey: string): unknown;
12
+ export declare function buildProgressCard(state: ProgressState): unknown;
13
+ export declare function buildConfirmCard(req: ConfirmationRequest): unknown;
14
+ export declare const DECISION_TEXT: Record<PermissionDecision, string>;
15
+ export declare function buildConfirmResultCard(req: ConfirmationRequest, decision: PermissionDecision, byName: string): unknown;
@@ -0,0 +1,60 @@
1
+ function card(elements) {
2
+ return { schema: '2.0', config: { update_multi: true }, body: { elements } };
3
+ }
4
+ function md(content) {
5
+ return { tag: 'markdown', content };
6
+ }
7
+ export function buildTextCard(markdown) {
8
+ return card([md(markdown)]);
9
+ }
10
+ /** 图片卡片:caption(可选)显示在图片上方——逐张发图时带编号说明用 */
11
+ export function buildImageCard(caption, imgKey) {
12
+ const elements = [];
13
+ if (caption)
14
+ elements.push(md(caption));
15
+ elements.push({ tag: 'img', img_key: imgKey, alt: { tag: 'plain_text', content: caption ?? '图片' } });
16
+ return card(elements);
17
+ }
18
+ export function buildProgressCard(state) {
19
+ const elapsed = Math.max(0, Math.floor((Date.now() - state.startedAt) / 1000));
20
+ const lines = [`**${state.title}**`, ``, state.status, ``];
21
+ if (state.toolLine)
22
+ lines.push(`🔧 ${state.toolLine}`, ``);
23
+ if (state.textTail)
24
+ lines.push('---', state.textTail.slice(-1200)); // 只保留尾部,防爆卡片
25
+ lines.push(``, `<font color='grey'>⏱ 已运行 ${Math.floor(elapsed / 60)} 分 ${elapsed % 60} 秒</font>`);
26
+ return card([md(lines.join('\n'))]);
27
+ }
28
+ export function buildConfirmCard(req) {
29
+ const body = req.diff
30
+ ? `\`\`\`diff\n${req.diff.slice(0, 1500)}\n\`\`\``
31
+ : `\`\`\`\n${req.summary.slice(0, 1500)}\n\`\`\``;
32
+ const button = (decision) => ({
33
+ tag: 'button',
34
+ text: { tag: 'plain_text', content: decision === 'allow' ? '✅ 允许' : decision === 'deny' ? '❌ 拒绝' : '⏭ 本次会话不再询问' },
35
+ type: decision === 'allow' ? 'primary' : decision === 'deny' ? 'danger' : 'default',
36
+ behaviors: [{ type: 'callback', value: { requestId: req.requestId, decision } }],
37
+ });
38
+ return card([
39
+ md(`**🔐 Claude 请求执行操作**\n\n工具: \`${req.toolName}\` 工作区: \`${req.workspaceName}\`\n${body}`),
40
+ {
41
+ // 卡片 V2 已废弃 tag:'action' 交互模块,改用 column_set 分栏实现按钮并排
42
+ tag: 'column_set',
43
+ flex_mode: 'flow',
44
+ columns: ['allow', 'deny', 'allow-session'].map((decision) => ({
45
+ tag: 'column', width: 'auto', weight: 1, vertical_align: 'top',
46
+ elements: [button(decision)],
47
+ })),
48
+ },
49
+ ]);
50
+ }
51
+ export const DECISION_TEXT = {
52
+ allow: '✅ 已允许',
53
+ deny: '❌ 已拒绝',
54
+ 'allow-session': '⏭ 本次会话不再询问',
55
+ };
56
+ export function buildConfirmResultCard(req, decision, byName) {
57
+ // 摘要截断与确认卡一致(1500):点击瞬间回调响应内联换卡,摘要突然缩水会被用户看到
58
+ return card([md(`**🔐 Claude 请求执行操作**\n\n工具: \`${req.toolName}\` 工作区: \`${req.workspaceName}\`\n\`\`\`\n${req.summary.slice(0, 1500)}\n\`\`\`\n\n${DECISION_TEXT[decision]}(由 ${byName} 操作)`)]);
59
+ }
60
+ //# sourceMappingURL=card-builder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"card-builder.js","sourceRoot":"","sources":["../../src/gateway/card-builder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,164 @@
1
+ import type { FeishuAppConfig, GatewayHandlers, IncomingMessage } from '../types.js';
2
+ /** SDK 形状:仅用到 WSClient/EventDispatcher/Client/Domain 四个导出,测试注入假对象时按此约束 */
3
+ export interface FeishuSdk {
4
+ WSClient: new (params: {
5
+ appId: string;
6
+ appSecret?: string;
7
+ domain?: unknown;
8
+ loggerLevel?: number;
9
+ }) => {
10
+ start(params: {
11
+ eventDispatcher: unknown;
12
+ }): Promise<void>;
13
+ /** 优雅关闭长连接(node-sdk 1.73.0 已实现:终止重连循环并关 socket) */
14
+ close(params?: {
15
+ force?: boolean;
16
+ }): void;
17
+ };
18
+ EventDispatcher: new (params: Record<string, never>) => {
19
+ register<T extends Record<string, (data: never) => unknown>>(handles: T): unknown;
20
+ };
21
+ Client: new (params: {
22
+ appId: string;
23
+ appSecret?: string;
24
+ domain?: unknown;
25
+ loggerLevel?: number;
26
+ }) => {
27
+ im: {
28
+ message: {
29
+ create(payload: {
30
+ params: {
31
+ receive_id_type: 'chat_id';
32
+ };
33
+ data: {
34
+ receive_id: string;
35
+ msg_type: string;
36
+ content: string;
37
+ };
38
+ }): Promise<{
39
+ code?: number;
40
+ msg?: string;
41
+ data?: {
42
+ message_id?: string;
43
+ };
44
+ }>;
45
+ patch(payload: {
46
+ path: {
47
+ message_id: string;
48
+ };
49
+ data: {
50
+ content: string;
51
+ };
52
+ }): Promise<{
53
+ code?: number;
54
+ msg?: string;
55
+ }>;
56
+ };
57
+ image: {
58
+ create(payload: {
59
+ data: {
60
+ image_type: 'message';
61
+ image: NodeJS.ReadableStream;
62
+ };
63
+ }): Promise<{
64
+ image_key?: string;
65
+ } | null>;
66
+ };
67
+ file: {
68
+ create(payload: {
69
+ data: {
70
+ file_type: 'opus' | 'mp4' | 'pdf' | 'doc' | 'xls' | 'ppt' | 'stream';
71
+ file_name: string;
72
+ file: NodeJS.ReadableStream;
73
+ };
74
+ }): Promise<{
75
+ file_key?: string;
76
+ } | null>;
77
+ };
78
+ };
79
+ request(payload: {
80
+ method: string;
81
+ url: string;
82
+ }): Promise<{
83
+ code?: number;
84
+ msg?: string;
85
+ bot?: {
86
+ open_id?: string;
87
+ };
88
+ }>;
89
+ };
90
+ Domain: {
91
+ Feishu: unknown;
92
+ Lark: unknown;
93
+ };
94
+ }
95
+ /** 去 @机器人 前缀(飞书群聊 @ 在文本中渲染为 @_user_N 占位) */
96
+ export declare function stripMention(text: string): string;
97
+ /**
98
+ * 解析 im.message.receive_v1 事件为 IncomingMessage。
99
+ *
100
+ * 群聊 @检测:botOpenId(机器人 open_id)可用时按 mentions 数组精确匹配——
101
+ * 占位符/mentions 非空无法区分 @机器人 vs @普通人(群里 @任何人都命中,会被误当任务执行);
102
+ * botOpenId 缺失(获取失败/未提供)时退化为旧的占位符 + mentions 非空判定,
103
+ * 但 strictGroupMention=true(多机器人部署)时宁丢不猜——同群多机器人下占位符退化判定
104
+ * 会让每个机器人都被触发(重复执行),丢弃比误执行安全;p2p 不依赖 @ 检测不受影响。
105
+ *
106
+ * ⚠️ 数据结构说明(与 brief 参考实现不同):node-sdk 的 EventDispatcher 回调收到的是
107
+ * RequestHandle.parse() 展平后的数据——v2 事件 {schema, header, event} 被解包为
108
+ * 顶层 {...header, ...event},即顶层 {sender, message};而非 {event: {message, sender}}。
109
+ * 本函数以展平结构为主,同时兼容 {event: {...}} 包裹形态(原始事件体直投)。
110
+ * 结构不合法一律返回 null,绝不抛异常。
111
+ */
112
+ export declare function parseIncomingMessage(event: unknown, botOpenId?: string, opts?: {
113
+ strictGroupMention?: boolean;
114
+ }): IncomingMessage | null;
115
+ export declare class FeishuGateway {
116
+ private client;
117
+ private sdk;
118
+ private cfg;
119
+ private ws?;
120
+ private log;
121
+ private strictGroupMention;
122
+ constructor(cfg: FeishuAppConfig, deps?: {
123
+ sdk?: FeishuSdk;
124
+ /** 日志出口(多 app 部署时注入带 [app:name] 前缀的 logger 以区分机器人) */
125
+ log?: {
126
+ warn: (...a: unknown[]) => void;
127
+ error: (...a: unknown[]) => void;
128
+ };
129
+ /** 严格群聊 @ 模式:botOpenId 不可用时丢弃群消息而非退化猜测(多机器人同群防双触发) */
130
+ strictGroupMention?: boolean;
131
+ });
132
+ /**
133
+ * 拉取机器人 open_id:群聊 @检测的精确匹配依据。
134
+ * SDK 1.73.0 未封装「获取机器人信息」接口(bot 命名空间仅 v4.bot.search),
135
+ * 经通用 request() 调 GET /open-apis/bot/v3/info。失败不阻断启动,
136
+ * 仅 warn 一次并让群聊 @检测退化为占位符匹配(严格模式下群消息将被丢弃)。
137
+ */
138
+ private fetchBotOpenId;
139
+ /** 建立 WS 长连接并注册事件分发(im.message.receive_v1 / card.action.trigger) */
140
+ start(handlers: GatewayHandlers): Promise<void>;
141
+ /** 优雅关闭长连接(SIGINT/SIGTERM 时逐个 app 调用);未 start 时为 no-op */
142
+ close(): void;
143
+ /** 发送卡片消息,返回 message_id */
144
+ sendCard(chatId: string, card: unknown): Promise<string>;
145
+ /** 以卡片形态发送 markdown 文本 */
146
+ sendText(chatId: string, markdown: string): Promise<string>;
147
+ /** 更新已发送卡片(流式进度刷新) */
148
+ updateCard(messageId: string, card: unknown): Promise<void>;
149
+ /**
150
+ * 上传图片并发送带 caption 的卡片(caption 显示在图片上方,逐张发图带编号说明用)。
151
+ * 卡片形态发送失败时降级为「caption 文本卡 + 纯图片消息」两条,保证图片必达
152
+ * (img 卡片元素对 message 型 image_key 的兼容性异常兜底)。
153
+ */
154
+ sendImage(chatId: string, filePath: string, caption?: string): Promise<string>;
155
+ /**
156
+ * 上传并发送文件:图片扩展名走 im.image.create(返回顶层 image_key),
157
+ * 其余走 im.file.create(返回顶层 file_key)。
158
+ * ⚠️ 与 brief 参考实现不同:这两个上传接口的 SDK 封装比普通接口多拆一层,
159
+ * 返回 {image_key} / {file_key} 顶层对象(或 null),不存在 res.data 包装。
160
+ * 上传与发消息两步都校验关键返回值——SDK 业务级失败(HTTP 200 + code!=0)不抛错,
161
+ * 不校验会静默 resolve,用户收不到文件且无任何报错。
162
+ */
163
+ uploadAndSendFile(chatId: string, filePath: string): Promise<void>;
164
+ }
@@ -0,0 +1,249 @@
1
+ import * as realSdk from '@larksuiteoapi/node-sdk';
2
+ import { createReadStream } from 'node:fs';
3
+ import { extname, basename } from 'node:path';
4
+ import { buildImageCard, buildTextCard } from './card-builder.js';
5
+ const IMAGE_EXT = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
6
+ /** 去 @机器人 前缀(飞书群聊 @ 在文本中渲染为 @_user_N 占位) */
7
+ export function stripMention(text) {
8
+ return text.replace(/@_user_\d+\s*/g, '').trim();
9
+ }
10
+ /**
11
+ * 解析 im.message.receive_v1 事件为 IncomingMessage。
12
+ *
13
+ * 群聊 @检测:botOpenId(机器人 open_id)可用时按 mentions 数组精确匹配——
14
+ * 占位符/mentions 非空无法区分 @机器人 vs @普通人(群里 @任何人都命中,会被误当任务执行);
15
+ * botOpenId 缺失(获取失败/未提供)时退化为旧的占位符 + mentions 非空判定,
16
+ * 但 strictGroupMention=true(多机器人部署)时宁丢不猜——同群多机器人下占位符退化判定
17
+ * 会让每个机器人都被触发(重复执行),丢弃比误执行安全;p2p 不依赖 @ 检测不受影响。
18
+ *
19
+ * ⚠️ 数据结构说明(与 brief 参考实现不同):node-sdk 的 EventDispatcher 回调收到的是
20
+ * RequestHandle.parse() 展平后的数据——v2 事件 {schema, header, event} 被解包为
21
+ * 顶层 {...header, ...event},即顶层 {sender, message};而非 {event: {message, sender}}。
22
+ * 本函数以展平结构为主,同时兼容 {event: {...}} 包裹形态(原始事件体直投)。
23
+ * 结构不合法一律返回 null,绝不抛异常。
24
+ */
25
+ export function parseIncomingMessage(event, botOpenId, opts = {}) {
26
+ try {
27
+ if (event === null || typeof event !== 'object')
28
+ return null;
29
+ const raw = event;
30
+ const payload = raw.message || raw.sender ? raw : raw.event;
31
+ const m = payload?.message;
32
+ if (!m?.chat_id || !m.message_id || m.message_type !== 'text')
33
+ return null;
34
+ const text = JSON.parse(m.content ?? '{}').text ?? '';
35
+ if (!text.trim())
36
+ return null;
37
+ const isGroup = m.chat_type !== 'p2p';
38
+ if (isGroup) {
39
+ if (botOpenId) {
40
+ // 精确判定:mentions 中存在 open_id === 机器人 open_id 的条目才算 @机器人
41
+ const mentionsBot = (m.mentions ?? []).some((t) => t.id?.open_id === botOpenId);
42
+ if (!mentionsBot)
43
+ return null;
44
+ }
45
+ else if (opts.strictGroupMention) {
46
+ // 严格模式:无法精确判定 @ 目标时直接丢弃(多机器人同群防双触发)
47
+ return null;
48
+ }
49
+ else {
50
+ // 退化判定:文本里渲染为 @_user_N 占位;mentions 数组兜底
51
+ //(部分客户端形态下占位符缺失但 mentions 存在)
52
+ const mentionedInText = /@_user_\d+/.test(text);
53
+ const hasMention = mentionedInText || (m.mentions?.length ?? 0) > 0;
54
+ if (!hasMention)
55
+ return null;
56
+ }
57
+ }
58
+ const userId = payload?.sender?.sender_id?.open_id;
59
+ if (!userId)
60
+ return null;
61
+ return {
62
+ chatId: m.chat_id,
63
+ chatType: isGroup ? 'group' : 'p2p',
64
+ userId,
65
+ text: stripMention(text),
66
+ messageId: m.message_id,
67
+ };
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
73
+ const VALID_DECISIONS = new Set(['allow', 'deny', 'allow-session']);
74
+ /** 解析 card.action.trigger 回调;不完整或 decision 不在合法枚举内返回 null */
75
+ function parseCardAction(data) {
76
+ try {
77
+ if (data === null || typeof data !== 'object')
78
+ return null;
79
+ const d = data;
80
+ const value = d.action?.value;
81
+ // decision 必须严格匹配枚举:畸形字符串(如 'ALLOW')不能流入 CardActionValue.decision
82
+ if (!value?.requestId || typeof value.decision !== 'string' || !VALID_DECISIONS.has(value.decision) || !d.operator?.open_id)
83
+ return null;
84
+ return {
85
+ value: { requestId: value.requestId, decision: value.decision },
86
+ operatorId: d.operator.open_id,
87
+ openMessageId: d.context?.open_message_id ?? d.open_message_id ?? '',
88
+ };
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ }
94
+ export class FeishuGateway {
95
+ client;
96
+ sdk;
97
+ cfg;
98
+ ws;
99
+ log;
100
+ strictGroupMention;
101
+ constructor(cfg, deps = {}) {
102
+ this.cfg = cfg;
103
+ this.sdk = deps.sdk ?? realSdk;
104
+ const tag = `[gateway:${cfg.name ?? cfg.appId}]`;
105
+ this.log = deps.log ?? {
106
+ warn: (...a) => console.warn(tag, ...a),
107
+ error: (...a) => console.error(tag, ...a),
108
+ };
109
+ this.strictGroupMention = deps.strictGroupMention ?? false;
110
+ const domain = cfg.domain === 'lark' ? this.sdk.Domain.Lark : this.sdk.Domain.Feishu;
111
+ this.client = new this.sdk.Client({ appId: cfg.appId, appSecret: cfg.appSecret, domain });
112
+ }
113
+ /**
114
+ * 拉取机器人 open_id:群聊 @检测的精确匹配依据。
115
+ * SDK 1.73.0 未封装「获取机器人信息」接口(bot 命名空间仅 v4.bot.search),
116
+ * 经通用 request() 调 GET /open-apis/bot/v3/info。失败不阻断启动,
117
+ * 仅 warn 一次并让群聊 @检测退化为占位符匹配(严格模式下群消息将被丢弃)。
118
+ */
119
+ async fetchBotOpenId() {
120
+ try {
121
+ const res = await this.client.request({ method: 'GET', url: '/open-apis/bot/v3/info' });
122
+ const openId = res?.bot?.open_id;
123
+ if (openId)
124
+ return openId;
125
+ this.log.warn(`机器人信息未返回 open_id:${JSON.stringify(res)},群聊 @检测${this.strictGroupMention ? '严格模式:群消息将被丢弃' : '退化为占位符匹配'}`);
126
+ }
127
+ catch (e) {
128
+ this.log.warn('获取机器人 open_id 失败(不影响启动),群聊 @检测' + (this.strictGroupMention ? '严格模式:群消息将被丢弃' : '退化为占位符匹配') + ':', e);
129
+ }
130
+ return undefined;
131
+ }
132
+ /** 建立 WS 长连接并注册事件分发(im.message.receive_v1 / card.action.trigger) */
133
+ async start(handlers) {
134
+ const botOpenId = await this.fetchBotOpenId();
135
+ const ws = new this.sdk.WSClient({ appId: this.cfg.appId, appSecret: this.cfg.appSecret });
136
+ this.ws = ws;
137
+ const dispatcher = new this.sdk.EventDispatcher({}).register({
138
+ 'im.message.receive_v1': async (data) => {
139
+ const msg = parseIncomingMessage(data, botOpenId, { strictGroupMention: this.strictGroupMention });
140
+ if (msg)
141
+ await handlers.onMessage(msg);
142
+ },
143
+ // 卡片回调必须返回对象(飞书 SDK 契约:返回值经 WS 回传,undefined 会被当异常);
144
+ // handler 返回了响应体(如 toast / 内联换卡 card)则透传,否则返回 {}。
145
+ // 畸形回调(parseCardAction 为 null)有意静默返回 {}:无有效操作对象,弹 toast 无服务意义
146
+ 'card.action.trigger': async (data) => {
147
+ const action = parseCardAction(data);
148
+ const ret = action ? await handlers.onCardAction(action) : undefined;
149
+ return ret ?? {};
150
+ },
151
+ });
152
+ await ws.start({ eventDispatcher: dispatcher });
153
+ }
154
+ /** 优雅关闭长连接(SIGINT/SIGTERM 时逐个 app 调用);未 start 时为 no-op */
155
+ close() {
156
+ try {
157
+ this.ws?.close();
158
+ }
159
+ catch {
160
+ // best effort:关闭失败不阻断其余连接的关闭
161
+ }
162
+ }
163
+ /** 发送卡片消息,返回 message_id */
164
+ async sendCard(chatId, card) {
165
+ const res = await this.client.im.message.create({
166
+ params: { receive_id_type: 'chat_id' },
167
+ data: { receive_id: chatId, msg_type: 'interactive', content: JSON.stringify(card) },
168
+ });
169
+ if (!res.data?.message_id)
170
+ throw new Error(`发送卡片失败: ${JSON.stringify(res)}`);
171
+ return res.data.message_id;
172
+ }
173
+ /** 以卡片形态发送 markdown 文本 */
174
+ async sendText(chatId, markdown) {
175
+ return this.sendCard(chatId, buildTextCard(markdown));
176
+ }
177
+ /** 更新已发送卡片(流式进度刷新) */
178
+ async updateCard(messageId, card) {
179
+ await this.client.im.message.patch({ path: { message_id: messageId }, data: { content: JSON.stringify(card) } });
180
+ }
181
+ /**
182
+ * 上传图片并发送带 caption 的卡片(caption 显示在图片上方,逐张发图带编号说明用)。
183
+ * 卡片形态发送失败时降级为「caption 文本卡 + 纯图片消息」两条,保证图片必达
184
+ * (img 卡片元素对 message 型 image_key 的兼容性异常兜底)。
185
+ */
186
+ async sendImage(chatId, filePath, caption) {
187
+ const up = await this.client.im.image.create({
188
+ data: { image_type: 'message', image: createReadStream(filePath) },
189
+ });
190
+ const imageKey = up?.image_key;
191
+ if (!imageKey)
192
+ throw new Error(`上传图片失败: ${JSON.stringify(up)}`);
193
+ try {
194
+ return await this.sendCard(chatId, buildImageCard(caption, imageKey));
195
+ }
196
+ catch (e) {
197
+ this.log.warn('图片卡片发送失败,降级为说明文本 + 图片消息两条:', e);
198
+ if (caption)
199
+ await this.sendText(chatId, caption).catch(() => { });
200
+ const res = await this.client.im.message.create({
201
+ params: { receive_id_type: 'chat_id' },
202
+ data: { receive_id: chatId, msg_type: 'image', content: JSON.stringify({ image_key: imageKey }) },
203
+ });
204
+ if (!res.data?.message_id)
205
+ throw new Error(`发送图片消息失败: ${JSON.stringify(res)}`);
206
+ return res.data.message_id;
207
+ }
208
+ }
209
+ /**
210
+ * 上传并发送文件:图片扩展名走 im.image.create(返回顶层 image_key),
211
+ * 其余走 im.file.create(返回顶层 file_key)。
212
+ * ⚠️ 与 brief 参考实现不同:这两个上传接口的 SDK 封装比普通接口多拆一层,
213
+ * 返回 {image_key} / {file_key} 顶层对象(或 null),不存在 res.data 包装。
214
+ * 上传与发消息两步都校验关键返回值——SDK 业务级失败(HTTP 200 + code!=0)不抛错,
215
+ * 不校验会静默 resolve,用户收不到文件且无任何报错。
216
+ */
217
+ async uploadAndSendFile(chatId, filePath) {
218
+ const ext = extname(filePath).toLowerCase();
219
+ if (IMAGE_EXT.has(ext)) {
220
+ const up = await this.client.im.image.create({
221
+ data: { image_type: 'message', image: createReadStream(filePath) },
222
+ });
223
+ const imageKey = up?.image_key;
224
+ if (!imageKey)
225
+ throw new Error(`上传图片失败: ${JSON.stringify(up)}`);
226
+ const res = await this.client.im.message.create({
227
+ params: { receive_id_type: 'chat_id' },
228
+ data: { receive_id: chatId, msg_type: 'image', content: JSON.stringify({ image_key: imageKey }) },
229
+ });
230
+ if (!res.data?.message_id)
231
+ throw new Error(`发送图片消息失败: ${JSON.stringify(res)}`);
232
+ }
233
+ else {
234
+ const up = await this.client.im.file.create({
235
+ data: { file_type: 'stream', file_name: basename(filePath), file: createReadStream(filePath) },
236
+ });
237
+ const fileKey = up?.file_key;
238
+ if (!fileKey)
239
+ throw new Error(`上传文件失败: ${JSON.stringify(up)}`);
240
+ const res = await this.client.im.message.create({
241
+ params: { receive_id_type: 'chat_id' },
242
+ data: { receive_id: chatId, msg_type: 'file', content: JSON.stringify({ file_key: fileKey }) },
243
+ });
244
+ if (!res.data?.message_id)
245
+ throw new Error(`发送文件消息失败: ${JSON.stringify(res)}`);
246
+ }
247
+ }
248
+ }
249
+ //# sourceMappingURL=feishu-gateway.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feishu-gateway.js","sourceRoot":"","sources":["../../src/gateway/feishu-gateway.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ export interface CardSender {
2
+ sendCard(card: unknown): Promise<string>;
3
+ updateCard(messageId: string, card: unknown): Promise<void>;
4
+ }
5
+ export declare class ProgressCard {
6
+ private sender;
7
+ private opts;
8
+ private messageId?;
9
+ private buffer;
10
+ private state;
11
+ private flushTimer?;
12
+ private heartbeatTimer?;
13
+ private lastActivityAt;
14
+ private done;
15
+ private flushChain;
16
+ private dirty;
17
+ private flushing;
18
+ constructor(sender: CardSender, title: string, opts?: {
19
+ flushIntervalMs?: number;
20
+ idleHeartbeatMs?: number;
21
+ });
22
+ start(): Promise<void>;
23
+ appendText(delta: string): void;
24
+ toolStart(name: string, summary: string): void;
25
+ toolResult(name: string, ok: boolean): void;
26
+ setStatus(status: string): void;
27
+ finish(summary: string): Promise<void>;
28
+ /**
29
+ * 串行化 flush:所有更新走同一条 promise 链按发起顺序落地。
30
+ * - in-flight 中再来请求 → 标记 dirty,本轮落地后自动补刷一次(带上最新 state);
31
+ * - finish 的终态 flush 也入链,天然排在先前挂起的更新之后。
32
+ */
33
+ private flush;
34
+ }