@jesonliu/lark-claudecode-bridge 0.10.0 → 0.13.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/README.md +84 -46
- package/assets/web/index.html +1416 -0
- package/config.example.yaml +22 -2
- package/dist/auth-precheck.d.ts +5 -2
- package/dist/auth-precheck.js +8 -2
- package/dist/auth-precheck.js.map +1 -1
- package/dist/bin/lcb.js +34 -3
- package/dist/bin/lcb.js.map +1 -1
- package/dist/claude-config.d.ts +19 -0
- package/dist/claude-config.js +76 -0
- package/dist/claude-config.js.map +1 -0
- package/dist/cli/setup-wizard.d.ts +1 -0
- package/dist/cli/setup-wizard.js +4 -0
- package/dist/cli/setup-wizard.js.map +1 -1
- package/dist/config-defaults.d.ts +4 -0
- package/dist/config-defaults.js +15 -0
- package/dist/config-defaults.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +190 -6
- package/dist/config.js.map +1 -1
- package/dist/executor/permission-gate.d.ts +8 -1
- package/dist/executor/permission-gate.js +21 -14
- package/dist/executor/permission-gate.js.map +1 -1
- package/dist/executor/plugin-discovery.d.ts +11 -0
- package/dist/executor/plugin-discovery.js +28 -0
- package/dist/executor/plugin-discovery.js.map +1 -1
- package/dist/executor/plugin-manager.d.ts +19 -0
- package/dist/executor/plugin-manager.js +84 -0
- package/dist/executor/plugin-manager.js.map +1 -0
- package/dist/feishu/slash-commands.d.ts +77 -0
- package/dist/feishu/slash-commands.js +188 -0
- package/dist/feishu/slash-commands.js.map +1 -0
- package/dist/gateway/card-builder.d.ts +2 -0
- package/dist/gateway/card-builder.js +3 -1
- package/dist/gateway/card-builder.js.map +1 -1
- package/dist/gateway/feishu-gateway.d.ts +37 -2
- package/dist/gateway/feishu-gateway.js +153 -8
- package/dist/gateway/feishu-gateway.js.map +1 -1
- package/dist/index.js +87 -20
- package/dist/index.js.map +1 -1
- package/dist/session/commands.d.ts +16 -1
- package/dist/session/commands.js +33 -1
- package/dist/session/commands.js.map +1 -1
- package/dist/session/plugin-command.d.ts +14 -0
- package/dist/session/plugin-command.js +110 -0
- package/dist/session/plugin-command.js.map +1 -0
- package/dist/types.d.ts +69 -0
- package/dist/util/open-browser.d.ts +1 -0
- package/dist/util/open-browser.js +20 -0
- package/dist/util/open-browser.js.map +1 -0
- package/dist/util/server-url.d.ts +3 -0
- package/dist/util/server-url.js +10 -0
- package/dist/util/server-url.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +6 -2
- package/dist/version.js.map +1 -1
- package/dist/web/config-api.d.ts +50 -0
- package/dist/web/config-api.js +184 -0
- package/dist/web/config-api.js.map +1 -0
- package/dist/web/lifecycle.d.ts +52 -0
- package/dist/web/lifecycle.js +167 -0
- package/dist/web/lifecycle.js.map +1 -0
- package/dist/web/model-list.d.ts +36 -0
- package/dist/web/model-list.js +101 -0
- package/dist/web/model-list.js.map +1 -0
- package/dist/web/server.d.ts +22 -0
- package/dist/web/server.js +543 -0
- package/dist/web/server.js.map +1 -0
- package/dist/web/update.d.ts +26 -0
- package/dist/web/update.js +64 -0
- package/dist/web/update.js.map +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { BridgeConfig, FeishuAppConfig, SlashCommandDef } from '../types.js';
|
|
2
|
+
/** 远端(飞书侧)已注册的斜杠命令(GET 列表条目) */
|
|
3
|
+
export interface RemoteSlashCommand {
|
|
4
|
+
command_id: string;
|
|
5
|
+
command: string;
|
|
6
|
+
description: {
|
|
7
|
+
default_value?: string;
|
|
8
|
+
i18n?: Record<string, string>;
|
|
9
|
+
icon?: {
|
|
10
|
+
icon_key?: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** 最小 API 客户端(执行器依赖此接口,测试注入 mock) */
|
|
15
|
+
export interface SlashApiClient {
|
|
16
|
+
list(): Promise<RemoteSlashCommand[]>;
|
|
17
|
+
create(def: SlashCommandDef): Promise<string>;
|
|
18
|
+
update(commandId: string, def: SlashCommandDef): Promise<void>;
|
|
19
|
+
remove(commandId: string): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
/** 内置命令集(SLASH_COMMAND_META 全量映射为注册定义)——expectedCommands 与 ensureBuiltins 共用 */
|
|
22
|
+
export declare function builtinCommands(): SlashCommandDef[];
|
|
23
|
+
/** 期望集 = 内置命令(SLASH_COMMAND_META 全量)+ config extras;与内置重名的 extra 跳过(内置优先) */
|
|
24
|
+
export declare function expectedCommands(config: BridgeConfig): SlashCommandDef[];
|
|
25
|
+
/** 全量对齐 diff:远端按 command 名匹配期望集;描述/icon 变化进 toUpdate,远端多余进 toDelete */
|
|
26
|
+
export declare function diffSlashCommands(remote: RemoteSlashCommand[], expected: SlashCommandDef[]): {
|
|
27
|
+
toCreate: SlashCommandDef[];
|
|
28
|
+
toUpdate: Array<{
|
|
29
|
+
commandId: string;
|
|
30
|
+
def: SlashCommandDef;
|
|
31
|
+
}>;
|
|
32
|
+
toDelete: Array<{
|
|
33
|
+
commandId: string;
|
|
34
|
+
command: string;
|
|
35
|
+
}>;
|
|
36
|
+
unchanged: string[];
|
|
37
|
+
};
|
|
38
|
+
export interface SyncReport {
|
|
39
|
+
created: string[];
|
|
40
|
+
updated: string[];
|
|
41
|
+
deleted: string[];
|
|
42
|
+
unchanged: string[];
|
|
43
|
+
errors: Array<{
|
|
44
|
+
command: string;
|
|
45
|
+
error: string;
|
|
46
|
+
}>;
|
|
47
|
+
}
|
|
48
|
+
/** 执行全量对齐同步:list → diff → 逐条 create/update/delete,每条独立容错汇总报告 */
|
|
49
|
+
export declare function syncSlashCommands(client: SlashApiClient, expected: SlashCommandDef[]): Promise<SyncReport>;
|
|
50
|
+
/**
|
|
51
|
+
* 只补齐远端缺失的内置命令(0.13 Web 页「补齐内置命令」按钮):
|
|
52
|
+
* list → 内置差集 → 逐条 create,**绝不 delete/update 远端已有命令**(用户手动注册的命令不受影响)。
|
|
53
|
+
* deleted/updated 恒空;already-exists(并发残留)计 unchanged。
|
|
54
|
+
*/
|
|
55
|
+
export declare function ensureBuiltins(client: SlashApiClient, builtins: SlashCommandDef[]): Promise<SyncReport>;
|
|
56
|
+
/** node-sdk 通用 request 的最小类型(官方 Client.request 支持 data/params,SDK 类型面未导出) */
|
|
57
|
+
interface SdkClient {
|
|
58
|
+
request(payload: {
|
|
59
|
+
method: string;
|
|
60
|
+
url: string;
|
|
61
|
+
data?: unknown;
|
|
62
|
+
}): Promise<unknown>;
|
|
63
|
+
}
|
|
64
|
+
interface SdkModule {
|
|
65
|
+
Client: new (opts: {
|
|
66
|
+
appId: string;
|
|
67
|
+
appSecret: string;
|
|
68
|
+
domain?: unknown;
|
|
69
|
+
}) => SdkClient;
|
|
70
|
+
Domain: {
|
|
71
|
+
Feishu: unknown;
|
|
72
|
+
Lark: unknown;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** 独立 API 客户端:不依赖运行中的 gateway(lcb ui 独立进程同样可用);token 由 SDK 自管 */
|
|
76
|
+
export declare function createSlashApiClient(app: FeishuAppConfig, sdk?: SdkModule): SlashApiClient;
|
|
77
|
+
export {};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// 飞书 Slash Command 同步:把 bridge 命令一键注册为飞书输入框斜杠指令
|
|
2
|
+
// (用户输入 / 弹面板 → 选中后命令留在输入框可继续输入描述 → 发送后走 bridge 命令/透传链路)。
|
|
3
|
+
// API:application/v7/app_slash_commands CRUD,scope application:app_slash_command:write/read,
|
|
4
|
+
// tenant_access_token 认证;上限 100 条;客户端生效约 5 分钟(PC 端 7.70+)。
|
|
5
|
+
// 纯函数(expectedCommands / diffSlashCommands)与执行(syncSlashCommands)分离,测试免网络
|
|
6
|
+
import * as lark from '@larksuiteoapi/node-sdk';
|
|
7
|
+
import { SLASH_COMMAND_META } from '../session/commands.js';
|
|
8
|
+
/** 内置命令集(SLASH_COMMAND_META 全量映射为注册定义)——expectedCommands 与 ensureBuiltins 共用 */
|
|
9
|
+
export function builtinCommands() {
|
|
10
|
+
return Object.entries(SLASH_COMMAND_META).map(([command, meta]) => ({
|
|
11
|
+
command, description: meta.description, icon: meta.icon,
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
/** 期望集 = 内置命令(SLASH_COMMAND_META 全量)+ config extras;与内置重名的 extra 跳过(内置优先) */
|
|
15
|
+
export function expectedCommands(config) {
|
|
16
|
+
const out = builtinCommands();
|
|
17
|
+
const seen = new Set(out.map((d) => d.command));
|
|
18
|
+
for (const extra of config.slashCommands?.extra ?? []) {
|
|
19
|
+
if (seen.has(extra.command)) {
|
|
20
|
+
console.warn(`[斜杠命令] extra "${extra.command}" 与内置命令重名,已跳过(内置优先)`);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
seen.add(extra.command);
|
|
24
|
+
out.push(extra);
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
function sameCommand(remote, def) {
|
|
29
|
+
const desc = remote.description ?? {};
|
|
30
|
+
return desc.default_value === def.description
|
|
31
|
+
&& desc.i18n?.zh_cn === def.description
|
|
32
|
+
&& desc.i18n?.en_us === def.description
|
|
33
|
+
&& (desc.icon?.icon_key ?? 'skill_outlined') === (def.icon ?? 'skill_outlined');
|
|
34
|
+
}
|
|
35
|
+
/** 全量对齐 diff:远端按 command 名匹配期望集;描述/icon 变化进 toUpdate,远端多余进 toDelete */
|
|
36
|
+
export function diffSlashCommands(remote, expected) {
|
|
37
|
+
const byName = new Map(remote.map((r) => [r.command, r]));
|
|
38
|
+
const toCreate = [];
|
|
39
|
+
const toUpdate = [];
|
|
40
|
+
const unchanged = [];
|
|
41
|
+
for (const def of expected) {
|
|
42
|
+
const r = byName.get(def.command);
|
|
43
|
+
if (!r) {
|
|
44
|
+
toCreate.push(def);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (sameCommand(r, def))
|
|
48
|
+
unchanged.push(def.command);
|
|
49
|
+
else
|
|
50
|
+
toUpdate.push({ commandId: r.command_id, def });
|
|
51
|
+
}
|
|
52
|
+
const expectedNames = new Set(expected.map((d) => d.command));
|
|
53
|
+
const toDelete = remote.filter((r) => !expectedNames.has(r.command))
|
|
54
|
+
.map((r) => ({ commandId: r.command_id, command: r.command }));
|
|
55
|
+
return { toCreate, toUpdate, toDelete, unchanged };
|
|
56
|
+
}
|
|
57
|
+
function isAlreadyExists(err) {
|
|
58
|
+
const e = err;
|
|
59
|
+
return e?.code === 40000000 || /already exists/i.test(String(e?.msg ?? e?.message ?? ''));
|
|
60
|
+
}
|
|
61
|
+
/** 执行全量对齐同步:list → diff → 逐条 create/update/delete,每条独立容错汇总报告 */
|
|
62
|
+
export async function syncSlashCommands(client, expected) {
|
|
63
|
+
const report = { created: [], updated: [], deleted: [], unchanged: [], errors: [] };
|
|
64
|
+
let remote;
|
|
65
|
+
try {
|
|
66
|
+
remote = await client.list();
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
report.errors.push({ command: '(list)', error: e instanceof Error ? e.message : String(e) });
|
|
70
|
+
return report;
|
|
71
|
+
}
|
|
72
|
+
const { toCreate, toUpdate, toDelete, unchanged } = diffSlashCommands(remote, expected);
|
|
73
|
+
report.unchanged = unchanged;
|
|
74
|
+
for (const def of toCreate) {
|
|
75
|
+
try {
|
|
76
|
+
await client.create(def);
|
|
77
|
+
report.created.push(def.command);
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
if (isAlreadyExists(e)) {
|
|
81
|
+
// 并发/历史残留导致重名:转为对现有条目的更新,保证对齐
|
|
82
|
+
try {
|
|
83
|
+
const existing = (await client.list()).find((r) => r.command === def.command);
|
|
84
|
+
if (existing) {
|
|
85
|
+
await client.update(existing.command_id, def);
|
|
86
|
+
report.updated.push(def.command);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch { /* 落入 errors */ }
|
|
91
|
+
}
|
|
92
|
+
report.errors.push({ command: def.command, error: e instanceof Error ? e.message : String(e) });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const { commandId, def } of toUpdate) {
|
|
96
|
+
try {
|
|
97
|
+
await client.update(commandId, def);
|
|
98
|
+
report.updated.push(def.command);
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
report.errors.push({ command: def.command, error: e instanceof Error ? e.message : String(e) });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
for (const { commandId, command } of toDelete) {
|
|
105
|
+
try {
|
|
106
|
+
await client.remove(commandId);
|
|
107
|
+
report.deleted.push(command);
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
report.errors.push({ command, error: e instanceof Error ? e.message : String(e) });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return report;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* 只补齐远端缺失的内置命令(0.13 Web 页「补齐内置命令」按钮):
|
|
117
|
+
* list → 内置差集 → 逐条 create,**绝不 delete/update 远端已有命令**(用户手动注册的命令不受影响)。
|
|
118
|
+
* deleted/updated 恒空;already-exists(并发残留)计 unchanged。
|
|
119
|
+
*/
|
|
120
|
+
export async function ensureBuiltins(client, builtins) {
|
|
121
|
+
const report = { created: [], updated: [], deleted: [], unchanged: [], errors: [] };
|
|
122
|
+
let remote;
|
|
123
|
+
try {
|
|
124
|
+
remote = await client.list();
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
report.errors.push({ command: '(list)', error: e instanceof Error ? e.message : String(e) });
|
|
128
|
+
return report;
|
|
129
|
+
}
|
|
130
|
+
const names = new Set(remote.map((r) => r.command));
|
|
131
|
+
for (const def of builtins) {
|
|
132
|
+
if (names.has(def.command)) {
|
|
133
|
+
report.unchanged.push(def.command);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
await client.create(def);
|
|
138
|
+
report.created.push(def.command);
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
if (isAlreadyExists(e)) {
|
|
142
|
+
report.unchanged.push(def.command);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
report.errors.push({ command: def.command, error: e instanceof Error ? e.message : String(e) });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return report;
|
|
149
|
+
}
|
|
150
|
+
/** 独立 API 客户端:不依赖运行中的 gateway(lcb ui 独立进程同样可用);token 由 SDK 自管 */
|
|
151
|
+
export function createSlashApiClient(app, sdk = lark) {
|
|
152
|
+
const client = new sdk.Client({
|
|
153
|
+
appId: app.appId,
|
|
154
|
+
appSecret: app.appSecret,
|
|
155
|
+
domain: app.domain === 'lark' ? sdk.Domain.Lark : sdk.Domain.Feishu,
|
|
156
|
+
});
|
|
157
|
+
// 业务失败 SDK 不抛错而是 code!=0:统一在此转 throw,调用方单一错误通道
|
|
158
|
+
const call = async (payload) => {
|
|
159
|
+
const res = (await client.request(payload));
|
|
160
|
+
if (res && typeof res.code === 'number' && res.code !== 0) {
|
|
161
|
+
const err = new Error(`${res.msg ?? 'unknown'}(code ${res.code})`);
|
|
162
|
+
err.code = res.code;
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
165
|
+
return (res?.data ?? {});
|
|
166
|
+
};
|
|
167
|
+
const body = (def) => ({
|
|
168
|
+
description: {
|
|
169
|
+
default_value: def.description,
|
|
170
|
+
i18n: { zh_cn: def.description, en_us: def.description },
|
|
171
|
+
...(def.icon ? { icon: { icon_key: def.icon } } : {}),
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
return {
|
|
175
|
+
list: async () => (await call({ method: 'GET', url: '/open-apis/application/v7/app_slash_commands' })).items ?? [],
|
|
176
|
+
create: async (def) => (await call({
|
|
177
|
+
method: 'POST', url: '/open-apis/application/v7/app_slash_commands',
|
|
178
|
+
data: { command: def.command, ...body(def) },
|
|
179
|
+
})).command_id,
|
|
180
|
+
update: async (commandId, def) => {
|
|
181
|
+
await call({ method: 'PATCH', url: `/open-apis/application/v7/app_slash_commands/${commandId}`, data: body(def) });
|
|
182
|
+
},
|
|
183
|
+
remove: async (commandId) => {
|
|
184
|
+
await call({ method: 'DELETE', url: `/open-apis/application/v7/app_slash_commands/${commandId}` });
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=slash-commands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slash-commands.js","sourceRoot":"","sources":["../../src/feishu/slash-commands.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,6 +7,8 @@ export interface ProgressState {
|
|
|
7
7
|
startedAt: number;
|
|
8
8
|
}
|
|
9
9
|
export declare function buildTextCard(markdown: string): unknown;
|
|
10
|
+
/** 进度卡正文尾部的字符上限(防爆卡片);任务收尾据此判断短回复是否需要独立结果消息 */
|
|
11
|
+
export declare const PROGRESS_TAIL_CHARS = 1200;
|
|
10
12
|
/** 图片卡片:caption(可选)显示在图片上方——逐张发图时带编号说明用 */
|
|
11
13
|
export declare function buildImageCard(caption: string | undefined, imgKey: string): unknown;
|
|
12
14
|
export declare function buildProgressCard(state: ProgressState): unknown;
|
|
@@ -8,6 +8,8 @@ function md(content) {
|
|
|
8
8
|
export function buildTextCard(markdown) {
|
|
9
9
|
return card([md(markdown)]);
|
|
10
10
|
}
|
|
11
|
+
/** 进度卡正文尾部的字符上限(防爆卡片);任务收尾据此判断短回复是否需要独立结果消息 */
|
|
12
|
+
export const PROGRESS_TAIL_CHARS = 1200;
|
|
11
13
|
/** 图片卡片:caption(可选)显示在图片上方——逐张发图时带编号说明用 */
|
|
12
14
|
export function buildImageCard(caption, imgKey) {
|
|
13
15
|
const elements = [];
|
|
@@ -22,7 +24,7 @@ export function buildProgressCard(state) {
|
|
|
22
24
|
if (state.toolLine)
|
|
23
25
|
lines.push(`🔧 ${state.toolLine}`, ``);
|
|
24
26
|
if (state.textTail)
|
|
25
|
-
lines.push('---', state.textTail.slice(-
|
|
27
|
+
lines.push('---', state.textTail.slice(-PROGRESS_TAIL_CHARS)); // 只保留尾部,防爆卡片
|
|
26
28
|
lines.push(``, `<font color='grey'>⏱ 已运行 ${Math.floor(elapsed / 60)} 分 ${elapsed % 60} 秒</font>`);
|
|
27
29
|
return card([md(lines.join('\n'))]);
|
|
28
30
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"card-builder.js","sourceRoot":"","sources":["../../src/gateway/card-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAIlD,SAAS,IAAI,CAAC,QAAmB;IAC/B,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC;AAC/E,CAAC;AACD,SAAS,EAAE,CAAC,OAAe;IACzB,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC;AACtC,CAAC;AACD,MAAM,UAAU,aAAa,CAAC,QAAgB;IAC5C,OAAO,IAAI,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;AAC9B,CAAC;AACD,2CAA2C;AAC3C,MAAM,UAAU,cAAc,CAAC,OAA2B,EAAE,MAAc;IACxE,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,IAAI,OAAO;QAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IACxC,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC;IACrG,OAAO,IAAI,CAAC,QAAQ,CAAC,CAAC;AACxB,CAAC;AACD,MAAM,UAAU,iBAAiB,CAAC,KAAoB;IACpD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IAC/E,MAAM,KAAK,GAAG,CAAC,KAAK,KAAK,CAAC,KAAK,IAAI,EAAE,EAAE,EAAE,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC3D,IAAI,KAAK,CAAC,QAAQ;QAAE,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;IAC3D,IAAI,KAAK,CAAC,QAAQ;QAAE,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"card-builder.js","sourceRoot":"","sources":["../../src/gateway/card-builder.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FeishuAppConfig, GatewayHandlers, IncomingMessage } from '../types.js';
|
|
1
|
+
import type { FeishuAppConfig, GatewayHandlers, IncomingMessage, RejectedMessage } from '../types.js';
|
|
2
2
|
/** SDK 形状:仅用到 WSClient/EventDispatcher/Client/Domain 四个导出,测试注入假对象时按此约束 */
|
|
3
3
|
export interface FeishuSdk {
|
|
4
4
|
WSClient: new (params: {
|
|
@@ -75,6 +75,21 @@ export interface FeishuSdk {
|
|
|
75
75
|
file_key?: string;
|
|
76
76
|
} | null>;
|
|
77
77
|
};
|
|
78
|
+
messageResource: {
|
|
79
|
+
get(payload: {
|
|
80
|
+
params: {
|
|
81
|
+
type: string;
|
|
82
|
+
};
|
|
83
|
+
path: {
|
|
84
|
+
message_id: string;
|
|
85
|
+
file_key: string;
|
|
86
|
+
};
|
|
87
|
+
}): Promise<{
|
|
88
|
+
writeFile(filePath: string): Promise<unknown>;
|
|
89
|
+
getReadableStream(): NodeJS.ReadableStream;
|
|
90
|
+
headers: unknown;
|
|
91
|
+
}>;
|
|
92
|
+
};
|
|
78
93
|
};
|
|
79
94
|
request(payload: {
|
|
80
95
|
method: string;
|
|
@@ -97,6 +112,11 @@ export declare function stripMention(text: string): string;
|
|
|
97
112
|
/**
|
|
98
113
|
* 解析 im.message.receive_v1 事件为 IncomingMessage。
|
|
99
114
|
*
|
|
115
|
+
* 支持的消息类型:text(纯文本,含手打多行)、post(富文本——粘贴带格式内容会被客户端编码为
|
|
116
|
+
* 此类型,拍平为多行纯文本并提取内嵌图片)、image(纯图片,提取 image_key 供 gateway 下载)。
|
|
117
|
+
* 其余类型(audio/media/file 等):p2p 返回 RejectedMessage 供上层回提示(不再静默蒸发),
|
|
118
|
+
* 群聊保持 null 静默(@ 判定对非常规类型无法可靠进行)。
|
|
119
|
+
*
|
|
100
120
|
* 群聊 @检测:botOpenId(机器人 open_id)可用时按 mentions 数组精确匹配——
|
|
101
121
|
* 占位符/mentions 非空无法区分 @机器人 vs @普通人(群里 @任何人都命中,会被误当任务执行);
|
|
102
122
|
* botOpenId 缺失(获取失败/未提供)时退化为旧的占位符 + mentions 非空判定,
|
|
@@ -111,7 +131,7 @@ export declare function stripMention(text: string): string;
|
|
|
111
131
|
*/
|
|
112
132
|
export declare function parseIncomingMessage(event: unknown, botOpenId?: string, opts?: {
|
|
113
133
|
strictGroupMention?: boolean;
|
|
114
|
-
}): IncomingMessage | null;
|
|
134
|
+
}): IncomingMessage | RejectedMessage | null;
|
|
115
135
|
export declare class FeishuGateway {
|
|
116
136
|
private client;
|
|
117
137
|
private sdk;
|
|
@@ -119,6 +139,10 @@ export declare class FeishuGateway {
|
|
|
119
139
|
private ws?;
|
|
120
140
|
private log;
|
|
121
141
|
private strictGroupMention;
|
|
142
|
+
/** 入站图片落盘目录(默认 ~/.lark-claudecode-bridge/inbox/,测试可注入临时目录) */
|
|
143
|
+
private inboxDir;
|
|
144
|
+
private seenMessageIds;
|
|
145
|
+
private static readonly SEEN_MAX;
|
|
122
146
|
constructor(cfg: FeishuAppConfig, deps?: {
|
|
123
147
|
sdk?: FeishuSdk;
|
|
124
148
|
/** 日志出口(多 app 部署时注入带 [app:name] 前缀的 logger 以区分机器人) */
|
|
@@ -128,6 +152,8 @@ export declare class FeishuGateway {
|
|
|
128
152
|
};
|
|
129
153
|
/** 严格群聊 @ 模式:botOpenId 不可用时丢弃群消息而非退化猜测(多机器人同群防双触发) */
|
|
130
154
|
strictGroupMention?: boolean;
|
|
155
|
+
/** 入站图片下载目录,缺省 CONFIG_DIR/inbox */
|
|
156
|
+
inboxDir?: string;
|
|
131
157
|
});
|
|
132
158
|
/**
|
|
133
159
|
* 拉取机器人 open_id:群聊 @检测的精确匹配依据。
|
|
@@ -136,6 +162,15 @@ export declare class FeishuGateway {
|
|
|
136
162
|
* 仅 warn 一次并让群聊 @检测退化为占位符匹配(严格模式下群消息将被丢弃)。
|
|
137
163
|
*/
|
|
138
164
|
private fetchBotOpenId;
|
|
165
|
+
/** 记录 messageId 并返回是否首次出现(true=新消息,false=重复投递应忽略) */
|
|
166
|
+
private markSeen;
|
|
167
|
+
/** 响应头 content-type → 图片扩展名(未知兜底 .png) */
|
|
168
|
+
private static extFromHeaders;
|
|
169
|
+
/**
|
|
170
|
+
* 下载消息内嵌图片到 inbox 目录(文件名 messageId_序号.扩展名)。
|
|
171
|
+
* 单张失败仅 warn 记录、不阻塞其余图片与消息转发(im:resource 权限缺失等场景文字任务照常)。
|
|
172
|
+
*/
|
|
173
|
+
private downloadImages;
|
|
139
174
|
/** 建立 WS 长连接并注册事件分发(im.message.receive_v1 / card.action.trigger) */
|
|
140
175
|
start(handlers: GatewayHandlers): Promise<void>;
|
|
141
176
|
/** 优雅关闭长连接(SIGINT/SIGTERM 时逐个 app 调用);未 start 时为 no-op */
|
|
@@ -1,15 +1,55 @@
|
|
|
1
1
|
import * as realSdk from '@larksuiteoapi/node-sdk';
|
|
2
|
-
import { createReadStream } from 'node:fs';
|
|
3
|
-
import { basename } from 'node:path';
|
|
2
|
+
import { createReadStream, mkdirSync } from 'node:fs';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
import { CONFIG_DIR } from '../config.js';
|
|
4
5
|
import { buildImageCard, buildTextCard } from './card-builder.js';
|
|
5
6
|
import { isImageFile } from '../util/file-types.js';
|
|
6
7
|
/** 去 @机器人 前缀(飞书群聊 @ 在文本中渲染为 @_user_N 占位) */
|
|
7
8
|
export function stripMention(text) {
|
|
8
9
|
return text.replace(/@_user_\d+\s*/g, '').trim();
|
|
9
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* post 富文本拍平为纯文本 + 图片 key 列表:
|
|
13
|
+
* text→text 字段、a→[text](href)、img→收集 image_key、at 及其他 tag→跳过;
|
|
14
|
+
* title 非空作首行,行间 \n 连接(保留多行结构)。content 非法返回 null。
|
|
15
|
+
*/
|
|
16
|
+
function flattenPost(rawContent) {
|
|
17
|
+
try {
|
|
18
|
+
const post = JSON.parse(rawContent);
|
|
19
|
+
if (!Array.isArray(post.content))
|
|
20
|
+
return null;
|
|
21
|
+
const imageKeys = [];
|
|
22
|
+
const lines = [];
|
|
23
|
+
if (typeof post.title === 'string' && post.title.trim())
|
|
24
|
+
lines.push(post.title);
|
|
25
|
+
for (const line of post.content) {
|
|
26
|
+
if (!Array.isArray(line))
|
|
27
|
+
continue;
|
|
28
|
+
const parts = [];
|
|
29
|
+
for (const node of line) {
|
|
30
|
+
if (node?.tag === 'text' && node.text)
|
|
31
|
+
parts.push(node.text);
|
|
32
|
+
else if (node?.tag === 'a' && node.text)
|
|
33
|
+
parts.push(node.href ? `[${node.text}](${node.href})` : node.text);
|
|
34
|
+
else if (node?.tag === 'img' && node.image_key)
|
|
35
|
+
imageKeys.push(node.image_key);
|
|
36
|
+
}
|
|
37
|
+
lines.push(parts.join(''));
|
|
38
|
+
}
|
|
39
|
+
return { text: lines.join('\n'), imageKeys };
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
10
45
|
/**
|
|
11
46
|
* 解析 im.message.receive_v1 事件为 IncomingMessage。
|
|
12
47
|
*
|
|
48
|
+
* 支持的消息类型:text(纯文本,含手打多行)、post(富文本——粘贴带格式内容会被客户端编码为
|
|
49
|
+
* 此类型,拍平为多行纯文本并提取内嵌图片)、image(纯图片,提取 image_key 供 gateway 下载)。
|
|
50
|
+
* 其余类型(audio/media/file 等):p2p 返回 RejectedMessage 供上层回提示(不再静默蒸发),
|
|
51
|
+
* 群聊保持 null 静默(@ 判定对非常规类型无法可靠进行)。
|
|
52
|
+
*
|
|
13
53
|
* 群聊 @检测:botOpenId(机器人 open_id)可用时按 mentions 数组精确匹配——
|
|
14
54
|
* 占位符/mentions 非空无法区分 @机器人 vs @普通人(群里 @任何人都命中,会被误当任务执行);
|
|
15
55
|
* botOpenId 缺失(获取失败/未提供)时退化为旧的占位符 + mentions 非空判定,
|
|
@@ -29,10 +69,31 @@ export function parseIncomingMessage(event, botOpenId, opts = {}) {
|
|
|
29
69
|
const raw = event;
|
|
30
70
|
const payload = raw.message || raw.sender ? raw : raw.event;
|
|
31
71
|
const m = payload?.message;
|
|
32
|
-
if (!m?.chat_id || !m.message_id || m.message_type
|
|
72
|
+
if (!m?.chat_id || !m.message_id || !m.message_type)
|
|
33
73
|
return null;
|
|
34
|
-
|
|
35
|
-
|
|
74
|
+
let text = '';
|
|
75
|
+
let imageKeys = [];
|
|
76
|
+
if (m.message_type === 'text') {
|
|
77
|
+
text = JSON.parse(m.content ?? '{}').text ?? '';
|
|
78
|
+
}
|
|
79
|
+
else if (m.message_type === 'post') {
|
|
80
|
+
const flat = flattenPost(m.content ?? '{}');
|
|
81
|
+
if (!flat)
|
|
82
|
+
return null;
|
|
83
|
+
text = flat.text;
|
|
84
|
+
imageKeys = flat.imageKeys;
|
|
85
|
+
}
|
|
86
|
+
else if (m.message_type === 'image') {
|
|
87
|
+
const key = JSON.parse(m.content ?? '{}').image_key;
|
|
88
|
+
if (key)
|
|
89
|
+
imageKeys.push(key);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
return m.chat_type === 'p2p'
|
|
93
|
+
? { rejected: { kind: 'unsupported-type', chatId: m.chat_id, chatType: 'p2p', messageType: m.message_type } }
|
|
94
|
+
: null;
|
|
95
|
+
}
|
|
96
|
+
if (!text.trim() && imageKeys.length === 0)
|
|
36
97
|
return null;
|
|
37
98
|
const isGroup = m.chat_type !== 'p2p';
|
|
38
99
|
if (isGroup) {
|
|
@@ -64,6 +125,7 @@ export function parseIncomingMessage(event, botOpenId, opts = {}) {
|
|
|
64
125
|
userId,
|
|
65
126
|
text: stripMention(text),
|
|
66
127
|
messageId: m.message_id,
|
|
128
|
+
...(imageKeys.length > 0 ? { imageKeys } : {}),
|
|
67
129
|
};
|
|
68
130
|
}
|
|
69
131
|
catch {
|
|
@@ -105,6 +167,12 @@ export class FeishuGateway {
|
|
|
105
167
|
ws;
|
|
106
168
|
log;
|
|
107
169
|
strictGroupMention;
|
|
170
|
+
/** 入站图片落盘目录(默认 ~/.lark-claudecode-bridge/inbox/,测试可注入临时目录) */
|
|
171
|
+
inboxDir;
|
|
172
|
+
// 入站消息去重:飞书 WS 长连接 at-least-once 投递,重连窗口内同一消息可能重投——
|
|
173
|
+
// 不去重会导致整个任务跑两遍(两张进度卡 + 两条结果)。Map 迭代序即插入序,容量超限删最旧。
|
|
174
|
+
seenMessageIds = new Map();
|
|
175
|
+
static SEEN_MAX = 1000;
|
|
108
176
|
constructor(cfg, deps = {}) {
|
|
109
177
|
this.cfg = cfg;
|
|
110
178
|
this.sdk = deps.sdk ?? realSdk;
|
|
@@ -114,6 +182,7 @@ export class FeishuGateway {
|
|
|
114
182
|
error: (...a) => console.error(tag, ...a),
|
|
115
183
|
};
|
|
116
184
|
this.strictGroupMention = deps.strictGroupMention ?? false;
|
|
185
|
+
this.inboxDir = deps.inboxDir ?? join(CONFIG_DIR, 'inbox');
|
|
117
186
|
const domain = cfg.domain === 'lark' ? this.sdk.Domain.Lark : this.sdk.Domain.Feishu;
|
|
118
187
|
this.client = new this.sdk.Client({ appId: cfg.appId, appSecret: cfg.appSecret, domain });
|
|
119
188
|
}
|
|
@@ -136,6 +205,55 @@ export class FeishuGateway {
|
|
|
136
205
|
}
|
|
137
206
|
return undefined;
|
|
138
207
|
}
|
|
208
|
+
/** 记录 messageId 并返回是否首次出现(true=新消息,false=重复投递应忽略) */
|
|
209
|
+
markSeen(messageId) {
|
|
210
|
+
if (this.seenMessageIds.has(messageId))
|
|
211
|
+
return false;
|
|
212
|
+
this.seenMessageIds.set(messageId, Date.now());
|
|
213
|
+
if (this.seenMessageIds.size > FeishuGateway.SEEN_MAX) {
|
|
214
|
+
const oldest = this.seenMessageIds.keys().next().value;
|
|
215
|
+
if (oldest !== undefined)
|
|
216
|
+
this.seenMessageIds.delete(oldest);
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
/** 响应头 content-type → 图片扩展名(未知兜底 .png) */
|
|
221
|
+
static extFromHeaders(headers) {
|
|
222
|
+
const ct = String(headers?.['content-type'] ?? '');
|
|
223
|
+
if (ct.includes('jpeg') || ct.includes('jpg'))
|
|
224
|
+
return '.jpg';
|
|
225
|
+
if (ct.includes('gif'))
|
|
226
|
+
return '.gif';
|
|
227
|
+
if (ct.includes('webp'))
|
|
228
|
+
return '.webp';
|
|
229
|
+
return '.png';
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* 下载消息内嵌图片到 inbox 目录(文件名 messageId_序号.扩展名)。
|
|
233
|
+
* 单张失败仅 warn 记录、不阻塞其余图片与消息转发(im:resource 权限缺失等场景文字任务照常)。
|
|
234
|
+
*/
|
|
235
|
+
async downloadImages(msg) {
|
|
236
|
+
const paths = [];
|
|
237
|
+
const failures = [];
|
|
238
|
+
mkdirSync(this.inboxDir, { recursive: true });
|
|
239
|
+
for (let i = 0; i < (msg.imageKeys ?? []).length; i++) {
|
|
240
|
+
const key = msg.imageKeys[i];
|
|
241
|
+
try {
|
|
242
|
+
const res = await this.client.im.messageResource.get({
|
|
243
|
+
path: { message_id: msg.messageId, file_key: key },
|
|
244
|
+
params: { type: 'image' },
|
|
245
|
+
});
|
|
246
|
+
const filePath = join(this.inboxDir, `${msg.messageId}_${i + 1}${FeishuGateway.extFromHeaders(res?.headers)}`);
|
|
247
|
+
await res.writeFile(filePath);
|
|
248
|
+
paths.push(filePath);
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
this.log.warn(`[图片] 下载失败(${key}):`, e);
|
|
252
|
+
failures.push(key);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return { paths, failures };
|
|
256
|
+
}
|
|
139
257
|
/** 建立 WS 长连接并注册事件分发(im.message.receive_v1 / card.action.trigger) */
|
|
140
258
|
async start(handlers) {
|
|
141
259
|
const botOpenId = await this.fetchBotOpenId();
|
|
@@ -143,9 +261,36 @@ export class FeishuGateway {
|
|
|
143
261
|
this.ws = ws;
|
|
144
262
|
const dispatcher = new this.sdk.EventDispatcher({}).register({
|
|
145
263
|
'im.message.receive_v1': async (data) => {
|
|
146
|
-
const
|
|
147
|
-
if (
|
|
148
|
-
|
|
264
|
+
const parsed = parseIncomingMessage(data, botOpenId, { strictGroupMention: this.strictGroupMention });
|
|
265
|
+
if (parsed && 'rejected' in parsed) {
|
|
266
|
+
// 不支持的消息类型(p2p):明确反馈而非静默蒸发;提示发送失败不影响主流程
|
|
267
|
+
if (parsed.rejected.chatType === 'p2p') {
|
|
268
|
+
await this.sendText(parsed.rejected.chatId, `🤖 暂不支持该消息类型(${parsed.rejected.messageType}),当前支持:文字、富文本(多行/粘贴)、图片`)
|
|
269
|
+
.catch((e) => this.log.warn('[类型提示] 发送失败:', e));
|
|
270
|
+
}
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (parsed && !this.markSeen(parsed.messageId)) {
|
|
274
|
+
this.log.warn('[去重] 忽略重复投递的消息', parsed.messageId);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (parsed) {
|
|
278
|
+
// 图片消息:先下载落盘,再把本地路径注记拼进 text——下游(触发词/命令/执行器/transcript)
|
|
279
|
+
// 对注记形态无感知,Claude Code 侧用 Read 工具读取路径即可看到图片
|
|
280
|
+
if ((parsed.imageKeys?.length ?? 0) > 0) {
|
|
281
|
+
const { paths, failures } = await this.downloadImages(parsed);
|
|
282
|
+
const hasText = parsed.text.trim().length > 0;
|
|
283
|
+
const notes = [];
|
|
284
|
+
if (paths.length > 0) {
|
|
285
|
+
notes.push(`[用户${hasText ? '随消息' : ''}发送了 ${paths.length} 张图片${hasText ? '' : '(无文字说明)'},已保存到本地:${paths.join('、')}。需要查看图片内容时用 Read 工具读取这些路径。]`);
|
|
286
|
+
}
|
|
287
|
+
if (failures.length > 0) {
|
|
288
|
+
notes.push(`[另有 ${failures.length} 张图片下载失败(${failures.join('、')})——如需查看请让用户重发,或检查应用 im:resource 权限。]`);
|
|
289
|
+
}
|
|
290
|
+
parsed.text = hasText ? `${parsed.text}\n\n${notes.join('\n')}` : notes.join('\n');
|
|
291
|
+
}
|
|
292
|
+
await handlers.onMessage(parsed);
|
|
293
|
+
}
|
|
149
294
|
},
|
|
150
295
|
// 卡片回调必须返回对象(飞书 SDK 契约:返回值经 WS 回传,undefined 会被当异常);
|
|
151
296
|
// handler 返回了响应体(如 toast / 内联换卡 card)则透传,否则返回 {}。
|