lark-kiro-bridge 0.3.1
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.en.md +293 -0
- package/README.md +291 -0
- package/bin/lark-kiro-bridge.mjs +5 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +4602 -0
- package/dist/index.d.ts +592 -0
- package/dist/index.js +4252 -0
- package/package.json +95 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
import * as lark from '@larksuiteoapi/node-sdk';
|
|
2
|
+
import { Logger } from 'pino';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 业务层使用的精简飞书事件类型。
|
|
7
|
+
* 把飞书 SDK 的复杂结构压平成 bridge 关心的字段。
|
|
8
|
+
*/
|
|
9
|
+
type ChatType = 'p2p' | 'group' | 'topic_group' | 'unknown';
|
|
10
|
+
interface IncomingMessage {
|
|
11
|
+
/** 事件 id,用于去重和日志关联 */
|
|
12
|
+
eventId: string;
|
|
13
|
+
/** 飞书 message id */
|
|
14
|
+
messageId: string;
|
|
15
|
+
/** 飞书 chat id(DM 或群) */
|
|
16
|
+
chatId: string;
|
|
17
|
+
/** 主题群里的 thread id,普通群/DM 为 undefined */
|
|
18
|
+
threadId?: string;
|
|
19
|
+
/** 单聊 / 群聊 / 主题群 */
|
|
20
|
+
chatType: ChatType;
|
|
21
|
+
/** 发送者的 open_id(飞书租户内稳定 id) */
|
|
22
|
+
senderOpenId: string;
|
|
23
|
+
/** 消息类型:text、post、image、file、... */
|
|
24
|
+
messageType: string;
|
|
25
|
+
/** 原始 content(JSON 字符串,由飞书发来),具体结构因 messageType 而异 */
|
|
26
|
+
rawContent: string;
|
|
27
|
+
/** 已抽取的纯文本(仅 text/post 类型;其他类型为空字符串) */
|
|
28
|
+
text: string;
|
|
29
|
+
/** 是否 @ 了某个用户/机器人 */
|
|
30
|
+
mentions: Array<{
|
|
31
|
+
key: string;
|
|
32
|
+
openId?: string;
|
|
33
|
+
name: string;
|
|
34
|
+
}>;
|
|
35
|
+
/** 收到事件的时间戳(毫秒) */
|
|
36
|
+
receivedAt: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 卡片按钮回调事件(业务层格式)
|
|
40
|
+
*
|
|
41
|
+
* 用户点击卡片上的 button(behaviors:[{type:'callback'}]) 时,飞书会推一个
|
|
42
|
+
* card.action.trigger 事件,dispatch 到这里。
|
|
43
|
+
*/
|
|
44
|
+
interface CardActionEvent {
|
|
45
|
+
/** 触发事件的 message_id(即卡片所在那条消息的 id) */
|
|
46
|
+
messageId: string;
|
|
47
|
+
/** chat id */
|
|
48
|
+
chatId: string;
|
|
49
|
+
/** 操作者的 open_id */
|
|
50
|
+
senderOpenId: string;
|
|
51
|
+
/**
|
|
52
|
+
* 按钮 value 字段——业务层自定义结构。
|
|
53
|
+
* 我们约定每个按钮都带 { action: 'model.set' | 'ws.use' | ..., ... }。
|
|
54
|
+
*/
|
|
55
|
+
value: Record<string, unknown>;
|
|
56
|
+
/**
|
|
57
|
+
* 表单提交字段(仅 button 在 form 里点提交时有值)。
|
|
58
|
+
* 飞书 v2 表单:每个 input 的 name 属性 → 用户输入的 value。
|
|
59
|
+
*/
|
|
60
|
+
formValue?: Record<string, unknown>;
|
|
61
|
+
/** 飞书发的 token(可用于在 30 分钟内更新原卡片,最多 2 次。当前未用,留给未来) */
|
|
62
|
+
token?: string;
|
|
63
|
+
/** 触发时间戳 */
|
|
64
|
+
receivedAt: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 飞书 SDK 封装
|
|
69
|
+
*
|
|
70
|
+
* 提供两个能力:
|
|
71
|
+
* 1. WebSocket 长连接事件监听(订阅 im.message.receive_v1 + card.action.trigger)
|
|
72
|
+
* 2. 飞书 OpenAPI 调用:发消息、发卡片、更新卡片、查机器人 open_id
|
|
73
|
+
*
|
|
74
|
+
* 与业务层之间通过 onMessage / onCardAction 回调解耦。
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
interface LarkClientOptions {
|
|
78
|
+
appId: string;
|
|
79
|
+
appSecret: string;
|
|
80
|
+
logger: Logger;
|
|
81
|
+
}
|
|
82
|
+
declare class LarkClient {
|
|
83
|
+
readonly appId: string;
|
|
84
|
+
private readonly appSecret;
|
|
85
|
+
private readonly log;
|
|
86
|
+
readonly api: lark.Client;
|
|
87
|
+
private wsClient;
|
|
88
|
+
private botOpenIdCache;
|
|
89
|
+
constructor(opts: LarkClientOptions);
|
|
90
|
+
/**
|
|
91
|
+
* 启动 WebSocket 长连接,注册事件 handler:
|
|
92
|
+
* - im.message.receive_v1:用户发的消息
|
|
93
|
+
* - card.action.trigger:用户点了卡片上的按钮
|
|
94
|
+
*/
|
|
95
|
+
startEventLoop(handlers: {
|
|
96
|
+
onMessage: (msg: IncomingMessage) => void | Promise<void>;
|
|
97
|
+
onCardAction?: (evt: CardActionEvent) => void | Promise<void>;
|
|
98
|
+
onReady?: () => void;
|
|
99
|
+
onReconnected?: () => void;
|
|
100
|
+
}): Promise<void>;
|
|
101
|
+
close(): void;
|
|
102
|
+
/**
|
|
103
|
+
* 查询当前机器人在租户里的 open_id(用来识别 @bot)。
|
|
104
|
+
* 应用启动时调用一次后缓存。
|
|
105
|
+
*/
|
|
106
|
+
getBotOpenId(): Promise<string>;
|
|
107
|
+
/** 业务侧主动设置 botOpenId(一般从配置或第一次 @bot 学习而来)。 */
|
|
108
|
+
setBotOpenId(openId: string): void;
|
|
109
|
+
/**
|
|
110
|
+
* 发送一张飞书卡片(v2 协议)。
|
|
111
|
+
* 返回卡片所在消息的 message_id,后续用 patchCard 更新。
|
|
112
|
+
*/
|
|
113
|
+
sendCard(chatId: string, cardJson: object): Promise<string>;
|
|
114
|
+
/**
|
|
115
|
+
* 回复一条消息(reply 卡片,让卡片挂在用户消息下面)。
|
|
116
|
+
*/
|
|
117
|
+
replyCard(messageId: string, cardJson: object): Promise<string>;
|
|
118
|
+
/**
|
|
119
|
+
* 用 patch 接口整体替换卡片内容。
|
|
120
|
+
* 飞书消息卡片支持通过 `im/v1/messages/:message_id` PATCH 来更新内容,
|
|
121
|
+
* 内容必须是合法的卡片 JSON 字符串。
|
|
122
|
+
*/
|
|
123
|
+
patchCard(messageId: string, cardJson: object): Promise<void>;
|
|
124
|
+
/** 发送纯文本消息(错误兜底用) */
|
|
125
|
+
sendText(chatId: string, text: string): Promise<string>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
declare const ChatSessionSchema: z.ZodObject<{
|
|
129
|
+
currentCwd: z.ZodString;
|
|
130
|
+
sessionsByCwd: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
131
|
+
lastActiveAt: z.ZodDefault<z.ZodNumber>;
|
|
132
|
+
/**
|
|
133
|
+
* per-chat idle watchdog 分钟数覆盖:
|
|
134
|
+
* undefined → 用全局默认(preferences/kiro.idleTimeoutMinutes)
|
|
135
|
+
* 0 → 显式关闭
|
|
136
|
+
* N>0 → 用 N 分钟
|
|
137
|
+
*/
|
|
138
|
+
idleTimeoutMinutes: z.ZodOptional<z.ZodNumber>;
|
|
139
|
+
}, "strip", z.ZodTypeAny, {
|
|
140
|
+
currentCwd: string;
|
|
141
|
+
sessionsByCwd: Record<string, string>;
|
|
142
|
+
lastActiveAt: number;
|
|
143
|
+
idleTimeoutMinutes?: number | undefined;
|
|
144
|
+
}, {
|
|
145
|
+
currentCwd: string;
|
|
146
|
+
idleTimeoutMinutes?: number | undefined;
|
|
147
|
+
sessionsByCwd?: Record<string, string> | undefined;
|
|
148
|
+
lastActiveAt?: number | undefined;
|
|
149
|
+
}>;
|
|
150
|
+
type ChatSession = z.infer<typeof ChatSessionSchema>;
|
|
151
|
+
declare class SessionStore {
|
|
152
|
+
/**
|
|
153
|
+
* 获取一个 chat 的会话状态;不存在则用 defaultCwd 初始化。
|
|
154
|
+
*/
|
|
155
|
+
get(chatId: string, defaultCwd: string): Promise<ChatSession>;
|
|
156
|
+
/**
|
|
157
|
+
* 切换 chat 的当前 cwd。该 cwd 下若已有 kiro session 会被自动延用。
|
|
158
|
+
*/
|
|
159
|
+
setCwd(chatId: string, cwd: string, defaultCwd: string): Promise<ChatSession>;
|
|
160
|
+
/**
|
|
161
|
+
* 关联当前 (chatId, cwd) 到一个 kiroSessionId(Kiro CLI 跑完返回的 sid)。
|
|
162
|
+
*/
|
|
163
|
+
setKiroSession(chatId: string, cwd: string, kiroSessionId: string): Promise<void>;
|
|
164
|
+
/**
|
|
165
|
+
* 清空当前 cwd 下的 kiro session(用于 /new 命令)。
|
|
166
|
+
*/
|
|
167
|
+
clearKiroSession(chatId: string, cwd: string): Promise<void>;
|
|
168
|
+
/**
|
|
169
|
+
* 获取当前 (chatId, cwd) 对应的 kiroSessionId(可能不存在)。
|
|
170
|
+
*/
|
|
171
|
+
getKiroSession(chatId: string, cwd: string): Promise<string | undefined>;
|
|
172
|
+
/**
|
|
173
|
+
* 设置 chat 的 idle watchdog 阈值(分钟)。
|
|
174
|
+
* - undefined:清除覆盖,回归全局默认
|
|
175
|
+
* - 0:关闭
|
|
176
|
+
* - N>0:用 N 分钟
|
|
177
|
+
*/
|
|
178
|
+
setIdleTimeout(chatId: string, minutes: number | undefined, defaultCwd: string): Promise<void>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
declare class WorkspaceStore {
|
|
182
|
+
list(): Promise<Record<string, string>>;
|
|
183
|
+
get(name: string): Promise<string | undefined>;
|
|
184
|
+
save(name: string, absPath: string): Promise<void>;
|
|
185
|
+
remove(name: string): Promise<boolean>;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
declare const ConfigSchema: z.ZodObject<{
|
|
189
|
+
lark: z.ZodObject<{
|
|
190
|
+
appId: z.ZodString;
|
|
191
|
+
appSecret: z.ZodString;
|
|
192
|
+
}, "strip", z.ZodTypeAny, {
|
|
193
|
+
appId: string;
|
|
194
|
+
appSecret: string;
|
|
195
|
+
}, {
|
|
196
|
+
appId: string;
|
|
197
|
+
appSecret: string;
|
|
198
|
+
}>;
|
|
199
|
+
kiro: z.ZodDefault<z.ZodObject<{
|
|
200
|
+
binPath: z.ZodDefault<z.ZodString>;
|
|
201
|
+
trustedTools: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
202
|
+
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
203
|
+
/**
|
|
204
|
+
* 默认 idle watchdog(分钟)。0 = 关闭。
|
|
205
|
+
* stdout 连续这么久没新输出就当作 kiro-cli 假死,killTree。
|
|
206
|
+
* /timeout N 可以临时为某个 chat 覆盖。
|
|
207
|
+
*/
|
|
208
|
+
idleTimeoutMinutes: z.ZodDefault<z.ZodNumber>;
|
|
209
|
+
model: z.ZodOptional<z.ZodString>;
|
|
210
|
+
agent: z.ZodOptional<z.ZodString>;
|
|
211
|
+
}, "strip", z.ZodTypeAny, {
|
|
212
|
+
binPath: string;
|
|
213
|
+
trustedTools: string[];
|
|
214
|
+
timeoutMs: number;
|
|
215
|
+
idleTimeoutMinutes: number;
|
|
216
|
+
model?: string | undefined;
|
|
217
|
+
agent?: string | undefined;
|
|
218
|
+
}, {
|
|
219
|
+
binPath?: string | undefined;
|
|
220
|
+
trustedTools?: string[] | undefined;
|
|
221
|
+
timeoutMs?: number | undefined;
|
|
222
|
+
idleTimeoutMinutes?: number | undefined;
|
|
223
|
+
model?: string | undefined;
|
|
224
|
+
agent?: string | undefined;
|
|
225
|
+
}>>;
|
|
226
|
+
workspace: z.ZodDefault<z.ZodObject<{
|
|
227
|
+
defaultCwd: z.ZodDefault<z.ZodString>;
|
|
228
|
+
allowedRoots: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
229
|
+
}, "strip", z.ZodTypeAny, {
|
|
230
|
+
defaultCwd: string;
|
|
231
|
+
allowedRoots: string[];
|
|
232
|
+
}, {
|
|
233
|
+
defaultCwd?: string | undefined;
|
|
234
|
+
allowedRoots?: string[] | undefined;
|
|
235
|
+
}>>;
|
|
236
|
+
access: z.ZodDefault<z.ZodObject<{
|
|
237
|
+
allowedUsers: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
238
|
+
allowedChats: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
239
|
+
admins: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
240
|
+
}, "strip", z.ZodTypeAny, {
|
|
241
|
+
allowedUsers: string[];
|
|
242
|
+
allowedChats: string[];
|
|
243
|
+
admins: string[];
|
|
244
|
+
}, {
|
|
245
|
+
allowedUsers?: string[] | undefined;
|
|
246
|
+
allowedChats?: string[] | undefined;
|
|
247
|
+
admins?: string[] | undefined;
|
|
248
|
+
}>>;
|
|
249
|
+
preferences: z.ZodDefault<z.ZodObject<{
|
|
250
|
+
requireMentionInGroup: z.ZodDefault<z.ZodBoolean>;
|
|
251
|
+
cardUpdateIntervalMs: z.ZodDefault<z.ZodNumber>;
|
|
252
|
+
/** 启动时清理多少天之前的日志文件 */
|
|
253
|
+
logRetentionDays: z.ZodDefault<z.ZodNumber>;
|
|
254
|
+
}, "strip", z.ZodTypeAny, {
|
|
255
|
+
requireMentionInGroup: boolean;
|
|
256
|
+
cardUpdateIntervalMs: number;
|
|
257
|
+
logRetentionDays: number;
|
|
258
|
+
}, {
|
|
259
|
+
requireMentionInGroup?: boolean | undefined;
|
|
260
|
+
cardUpdateIntervalMs?: number | undefined;
|
|
261
|
+
logRetentionDays?: number | undefined;
|
|
262
|
+
}>>;
|
|
263
|
+
}, "strip", z.ZodTypeAny, {
|
|
264
|
+
lark: {
|
|
265
|
+
appId: string;
|
|
266
|
+
appSecret: string;
|
|
267
|
+
};
|
|
268
|
+
kiro: {
|
|
269
|
+
binPath: string;
|
|
270
|
+
trustedTools: string[];
|
|
271
|
+
timeoutMs: number;
|
|
272
|
+
idleTimeoutMinutes: number;
|
|
273
|
+
model?: string | undefined;
|
|
274
|
+
agent?: string | undefined;
|
|
275
|
+
};
|
|
276
|
+
workspace: {
|
|
277
|
+
defaultCwd: string;
|
|
278
|
+
allowedRoots: string[];
|
|
279
|
+
};
|
|
280
|
+
access: {
|
|
281
|
+
allowedUsers: string[];
|
|
282
|
+
allowedChats: string[];
|
|
283
|
+
admins: string[];
|
|
284
|
+
};
|
|
285
|
+
preferences: {
|
|
286
|
+
requireMentionInGroup: boolean;
|
|
287
|
+
cardUpdateIntervalMs: number;
|
|
288
|
+
logRetentionDays: number;
|
|
289
|
+
};
|
|
290
|
+
}, {
|
|
291
|
+
lark: {
|
|
292
|
+
appId: string;
|
|
293
|
+
appSecret: string;
|
|
294
|
+
};
|
|
295
|
+
kiro?: {
|
|
296
|
+
binPath?: string | undefined;
|
|
297
|
+
trustedTools?: string[] | undefined;
|
|
298
|
+
timeoutMs?: number | undefined;
|
|
299
|
+
idleTimeoutMinutes?: number | undefined;
|
|
300
|
+
model?: string | undefined;
|
|
301
|
+
agent?: string | undefined;
|
|
302
|
+
} | undefined;
|
|
303
|
+
workspace?: {
|
|
304
|
+
defaultCwd?: string | undefined;
|
|
305
|
+
allowedRoots?: string[] | undefined;
|
|
306
|
+
} | undefined;
|
|
307
|
+
access?: {
|
|
308
|
+
allowedUsers?: string[] | undefined;
|
|
309
|
+
allowedChats?: string[] | undefined;
|
|
310
|
+
admins?: string[] | undefined;
|
|
311
|
+
} | undefined;
|
|
312
|
+
preferences?: {
|
|
313
|
+
requireMentionInGroup?: boolean | undefined;
|
|
314
|
+
cardUpdateIntervalMs?: number | undefined;
|
|
315
|
+
logRetentionDays?: number | undefined;
|
|
316
|
+
} | undefined;
|
|
317
|
+
}>;
|
|
318
|
+
type Config = z.infer<typeof ConfigSchema>;
|
|
319
|
+
/**
|
|
320
|
+
* 从磁盘加载配置;不存在则抛错(让 CLI 引导用户跑 init/wizard)。
|
|
321
|
+
*/
|
|
322
|
+
declare function loadConfig(): Config;
|
|
323
|
+
/**
|
|
324
|
+
* 写入配置文件,权限 0600。
|
|
325
|
+
*/
|
|
326
|
+
declare function saveConfig(cfg: Config): void;
|
|
327
|
+
/**
|
|
328
|
+
* 生成最小可用的配置模板,供 init 命令使用。
|
|
329
|
+
*/
|
|
330
|
+
declare function defaultConfig(appId: string, appSecret: string): Config;
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* 消息总分发器
|
|
334
|
+
*
|
|
335
|
+
* 把一条飞书消息变成一个动作:
|
|
336
|
+
* 1. 访问控制校验(用户/群白名单、@bot 检测)
|
|
337
|
+
* 2. 下载图片/文件资源(如果有)
|
|
338
|
+
* 3. 解析斜杠命令
|
|
339
|
+
* 4. 路由到 commandHandler 或 kiroHandler
|
|
340
|
+
* 5. 更新卡片
|
|
341
|
+
*
|
|
342
|
+
* 跨 chat 并发不限(每个 chat 自己内部串行)。
|
|
343
|
+
*/
|
|
344
|
+
|
|
345
|
+
interface DispatcherOptions {
|
|
346
|
+
config: Config;
|
|
347
|
+
lark: LarkClient;
|
|
348
|
+
sessions: SessionStore;
|
|
349
|
+
workspaces: WorkspaceStore;
|
|
350
|
+
logger: Logger;
|
|
351
|
+
/** 当 /reconnect 命令触发时调用,由 bootstrap 注入 */
|
|
352
|
+
onReconnect?: () => Promise<void>;
|
|
353
|
+
}
|
|
354
|
+
declare class Dispatcher {
|
|
355
|
+
private config;
|
|
356
|
+
private readonly lark;
|
|
357
|
+
private readonly sessions;
|
|
358
|
+
private readonly workspaces;
|
|
359
|
+
private readonly log;
|
|
360
|
+
private readonly pipelines;
|
|
361
|
+
private readonly onReconnect?;
|
|
362
|
+
constructor(opts: DispatcherOptions);
|
|
363
|
+
private getPipeline;
|
|
364
|
+
/** eventId 去重缓存:飞书 at-least-once 投递保险 */
|
|
365
|
+
private readonly seenEventIds;
|
|
366
|
+
private readonly EVENT_TTL_MS;
|
|
367
|
+
/**
|
|
368
|
+
* rapid-fire 消息合并:同 chat 短时间连发时把多条消息拼成一次 Kiro 调用。
|
|
369
|
+
*
|
|
370
|
+
* 价值:用户在飞书 IM 里习惯连发短消息("等等"、"还有"、"刚才那个改一下"),
|
|
371
|
+
* 不合并的话每条都跑一次 Kiro,浪费 token 还会被前面的 abort 打断;合并后
|
|
372
|
+
* 一次性给 Kiro 完整意图。
|
|
373
|
+
*
|
|
374
|
+
* 实现:每条消息进来先放 buffer,200ms 内有新消息就追加;到时一次性 submit。
|
|
375
|
+
* - 第一条触发计时器、注册 buffer
|
|
376
|
+
* - 后续消息往 buffer 里追加文本(+ 媒体路径)
|
|
377
|
+
* - 200ms 静默期到 → flush,用第一条的 msg/eventId 作为 reply 锚点
|
|
378
|
+
*/
|
|
379
|
+
private readonly mergeBuffers;
|
|
380
|
+
private readonly MERGE_WINDOW_MS;
|
|
381
|
+
private isDuplicate;
|
|
382
|
+
/**
|
|
383
|
+
* 主入口:处理一条飞书消息。
|
|
384
|
+
*/
|
|
385
|
+
handle(msg: IncomingMessage): Promise<void>;
|
|
386
|
+
private workspaceNameOf;
|
|
387
|
+
/**
|
|
388
|
+
* 计算某个 chat 实际生效的 idle watchdog 分钟数。
|
|
389
|
+
* - per-chat override 存在(包括 0)→ 优先用它
|
|
390
|
+
* - 否则用 config.kiro.idleTimeoutMinutes
|
|
391
|
+
*/
|
|
392
|
+
private effectiveIdleMinutes;
|
|
393
|
+
private handleTimeoutCmd;
|
|
394
|
+
/**
|
|
395
|
+
* /doctor [描述]
|
|
396
|
+
* 把最近 200 行结构化日志 + 用户描述拼成 prompt 喂给 Kiro 自诊断。
|
|
397
|
+
* 走标准 runKiroTask 流程,享受所有流式卡片和 watchdog。
|
|
398
|
+
*/
|
|
399
|
+
private handleDoctorCmd;
|
|
400
|
+
/**
|
|
401
|
+
* /model → 列出所有模型 + 当前选中(漂亮按钮卡片)
|
|
402
|
+
* /model <name> → 切换模型,写入 config.json,立即生效
|
|
403
|
+
* /model auto → 清除模型覆盖,回归 kiro-cli 默认(auto)
|
|
404
|
+
*
|
|
405
|
+
* 短名容错:/model sonnet-4.6 → 自动补 claude- 前缀
|
|
406
|
+
*
|
|
407
|
+
* 设计取舍:
|
|
408
|
+
* - 切模型只改全局 config.json,不做 per-chat 覆盖(先做最少必要)
|
|
409
|
+
* - 切完不需要 reconnect,下一条消息直接生效(spawn kiro-cli 时读最新 config)
|
|
410
|
+
*/
|
|
411
|
+
private handleModelCmd;
|
|
412
|
+
/**
|
|
413
|
+
* 模型名解析:
|
|
414
|
+
* - 列表里精确匹配 → 直接返回
|
|
415
|
+
* - 列表里有 "claude-<name>" → 返回带前缀的全名(短名容错)
|
|
416
|
+
* - 都不匹配 → 返回 undefined(让上层报错)
|
|
417
|
+
* 列表为空(fetch 失败)时直接返回原名,不阻塞。
|
|
418
|
+
*/
|
|
419
|
+
private resolveModelName;
|
|
420
|
+
/**
|
|
421
|
+
* 发送一张飞书 v2 交互卡片(带按钮的那种)作为对原消息的回复。
|
|
422
|
+
* 跟 replyDoneCard 不同:不走 CardRenderer 的状态机,发一次就完事,
|
|
423
|
+
* 不会再 patch;按钮回调走 onCardAction 流程。
|
|
424
|
+
*/
|
|
425
|
+
private sendInteractiveCard;
|
|
426
|
+
/**
|
|
427
|
+
* 命令型卡片的"先占位再 patch"模式。
|
|
428
|
+
*
|
|
429
|
+
* 用于命令本身需要做异步工作(spawn kiro-cli 等)才能算出最终卡片内容的场景:
|
|
430
|
+
* 1. 先用 placeholderCard 立刻 reply 出去,让用户看到反馈
|
|
431
|
+
* 2. 异步跑 buildFinalCard()
|
|
432
|
+
* 3. 用 patchCard 替换成最终卡片
|
|
433
|
+
*
|
|
434
|
+
* 失败时直接发 fallback 错误卡片,不让用户卡在 placeholder。
|
|
435
|
+
*/
|
|
436
|
+
private sendInteractiveCardAsync;
|
|
437
|
+
/**
|
|
438
|
+
* 直接发卡片到 chatId(不 reply 任何特定消息)。
|
|
439
|
+
* cardAction handler 用这个——按钮触发时不 reply 那条卡片本身(嵌套体验差),
|
|
440
|
+
* 而是发新消息。
|
|
441
|
+
*/
|
|
442
|
+
private sendCardToChat;
|
|
443
|
+
/**
|
|
444
|
+
* 处理用户点了卡片按钮的事件。
|
|
445
|
+
* value 约定字段:{ action: 'xxx.yyy', ...payload }
|
|
446
|
+
*
|
|
447
|
+
* 安全:button 触发的命令也会经过 admin 校验(调用 needAdminForAction)。
|
|
448
|
+
*/
|
|
449
|
+
handleCardAction(evt: CardActionEvent): Promise<void>;
|
|
450
|
+
/** 哪些 action 是写操作,需要 admin */
|
|
451
|
+
private actionNeedsAdmin;
|
|
452
|
+
/**
|
|
453
|
+
* /config 命令:展示当前配置(只读卡片,admin 可见编辑按钮)
|
|
454
|
+
*/
|
|
455
|
+
private handleConfigCmd;
|
|
456
|
+
/**
|
|
457
|
+
* 处理 config 表单提交。
|
|
458
|
+
*
|
|
459
|
+
* 流程:
|
|
460
|
+
* 1. 解析 form_value(用户输入的逗号分隔列表 / 整数 / yes/no)
|
|
461
|
+
* 2. 用 validateAccessChange 校验,防止把自己锁出去
|
|
462
|
+
* 3. patchAndSaveConfig 落盘 + 立即生效
|
|
463
|
+
* 4. 回一张确认卡片(同时显示新的 config view)
|
|
464
|
+
*/
|
|
465
|
+
private handleConfigSubmit;
|
|
466
|
+
private replyErrorCard;
|
|
467
|
+
/**
|
|
468
|
+
* 把一条用户消息丢给 Kiro 处理。
|
|
469
|
+
*
|
|
470
|
+
* **包了一层 rapid-fire 合并**:先放 buffer,等 200ms 静默期再真正 submit;
|
|
471
|
+
* 期间若同 chat 又来新消息,就追加到同一个 buffer,不会触发 abort+rerun。
|
|
472
|
+
*
|
|
473
|
+
* 真正的 Kiro 调用在 executeKiroTask 里。
|
|
474
|
+
*/
|
|
475
|
+
private runKiroTask;
|
|
476
|
+
/**
|
|
477
|
+
* flush 当前 chat 的合并 buffer:把累计的文本拼一起,调 executeKiroTask。
|
|
478
|
+
*/
|
|
479
|
+
private flushMergeBuffer;
|
|
480
|
+
/**
|
|
481
|
+
* 真正把任务丢到 ChatPipeline 跑 Kiro 的实现(不含 rapid-fire 合并)。
|
|
482
|
+
* - 在 ChatPipeline 里跑(自动 preempt)
|
|
483
|
+
* - 用 RunCardController 流式刷新卡片(每个工具独立 panel)
|
|
484
|
+
* - mediaPaths 非空时,把绝对路径作为前缀加到 prompt 前面
|
|
485
|
+
* - perChatIdleMin 控制本次 idle watchdog 阈值
|
|
486
|
+
*/
|
|
487
|
+
private executeKiroTask;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* 单 chat 任务管线
|
|
492
|
+
*
|
|
493
|
+
* 一个飞书 chat 一个 ChatPipeline 实例。职责:
|
|
494
|
+
* - 对该 chat 串行执行任务(同一时刻最多一个 Kiro 在跑)
|
|
495
|
+
* - 新任务到来时打断旧任务("preempt")
|
|
496
|
+
* - 提供 stop() 主动中止当前任务
|
|
497
|
+
*/
|
|
498
|
+
|
|
499
|
+
interface PipelineTask {
|
|
500
|
+
/** 任务 id(用于日志) */
|
|
501
|
+
id: string;
|
|
502
|
+
/** 真正干活的函数;接收 AbortSignal */
|
|
503
|
+
run: (signal: AbortSignal) => Promise<void>;
|
|
504
|
+
}
|
|
505
|
+
declare class ChatPipeline {
|
|
506
|
+
readonly chatId: string;
|
|
507
|
+
private readonly log;
|
|
508
|
+
private currentAbort;
|
|
509
|
+
private currentTaskId;
|
|
510
|
+
private currentPromise;
|
|
511
|
+
constructor(chatId: string, logger: Logger);
|
|
512
|
+
/**
|
|
513
|
+
* 提交新任务。
|
|
514
|
+
* 如果当前有任务在跑:
|
|
515
|
+
* - 先发 abort 信号
|
|
516
|
+
* - 等旧任务收尾(旧 run() 应该 catch AbortError 并 finalize 卡片)
|
|
517
|
+
* - 再启动新任务
|
|
518
|
+
*/
|
|
519
|
+
submit(task: PipelineTask): Promise<void>;
|
|
520
|
+
/** 主动中止当前任务(/stop 命令)。返回是否真的中止了某任务。 */
|
|
521
|
+
abortCurrent(): boolean;
|
|
522
|
+
hasActiveTask(): boolean;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
interface RunOptions {
|
|
526
|
+
/** 用户消息(即喂给 kiro-cli 的 INPUT 参数) */
|
|
527
|
+
prompt: string;
|
|
528
|
+
/** 工作目录 */
|
|
529
|
+
cwd: string;
|
|
530
|
+
/** 续接的 session id;不传则新建会话 */
|
|
531
|
+
resumeId?: string | undefined;
|
|
532
|
+
/** kiro-cli 可执行文件,默认 'kiro-cli' */
|
|
533
|
+
binPath?: string;
|
|
534
|
+
/** 信任的工具集合 */
|
|
535
|
+
trustedTools?: string[];
|
|
536
|
+
/** 模型名(可选) */
|
|
537
|
+
model?: string | undefined;
|
|
538
|
+
/** Agent 名(可选) */
|
|
539
|
+
agent?: string | undefined;
|
|
540
|
+
/** 总超时毫秒 */
|
|
541
|
+
timeoutMs?: number;
|
|
542
|
+
/**
|
|
543
|
+
* 空闲 watchdog 阈值(毫秒)。
|
|
544
|
+
* 若 stdout 连续这么久没新输出就认为假死,杀掉子进程。
|
|
545
|
+
* 0 或不传 = 关闭 watchdog(仅依赖 timeoutMs)。
|
|
546
|
+
*/
|
|
547
|
+
idleTimeoutMs?: number;
|
|
548
|
+
/**
|
|
549
|
+
* 流式文本回调;text 是已剥离 ANSI 的纯文本片段。
|
|
550
|
+
* 调用方拿到原始流后自行解析(用 createRunStreamParser),
|
|
551
|
+
* 不再由 runner 内部做 trace/正文分离。
|
|
552
|
+
*/
|
|
553
|
+
onChunk?: (text: string) => void;
|
|
554
|
+
/** AbortSignal 用于外部打断 */
|
|
555
|
+
signal?: AbortSignal;
|
|
556
|
+
}
|
|
557
|
+
interface RunResult {
|
|
558
|
+
/** 完整回复(已剥离 ANSI) */
|
|
559
|
+
text: string;
|
|
560
|
+
/** kiro-cli 退出码 */
|
|
561
|
+
exitCode: number | null;
|
|
562
|
+
/** 跑完之后从 list-sessions 拿到的最新 session id(可能为 undefined) */
|
|
563
|
+
newSessionId?: string;
|
|
564
|
+
/** 是否被外部信号中止 */
|
|
565
|
+
aborted: boolean;
|
|
566
|
+
/** 是否因总超时被强杀 */
|
|
567
|
+
timedOut: boolean;
|
|
568
|
+
/** 是否因 idle watchdog 被强杀(超过 idleTimeoutMs 没新输出) */
|
|
569
|
+
idleTimedOut: boolean;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* 跑一次 kiro-cli。流式回调和超时/打断都安全终止。
|
|
573
|
+
*/
|
|
574
|
+
declare function runKiro(opts: RunOptions): Promise<RunResult>;
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* 全局日志器
|
|
578
|
+
* - 开发模式(TTY)用 pino-pretty 输出彩色易读日志
|
|
579
|
+
* - 生产模式输出 NDJSON,写入 ~/.lark-kiro-bridge/logs/YYYY-MM-DD.log
|
|
580
|
+
* - 启动时按 logRetentionDays(默认 7 天)清理旧日志
|
|
581
|
+
* ENV LARK_KIRO_LOG_DAYS 可覆盖,便于不读 config 的场景
|
|
582
|
+
*/
|
|
583
|
+
|
|
584
|
+
declare function getLogger(): Logger;
|
|
585
|
+
|
|
586
|
+
interface RunBridgeHandle {
|
|
587
|
+
/** 主动停止;返回 promise 在所有清理完成后 resolve */
|
|
588
|
+
stop: () => Promise<void>;
|
|
589
|
+
}
|
|
590
|
+
declare function runBridge(): Promise<RunBridgeHandle>;
|
|
591
|
+
|
|
592
|
+
export { ChatPipeline, type Config, Dispatcher, LarkClient, SessionStore, WorkspaceStore, defaultConfig, getLogger, loadConfig, runBridge, runKiro, saveConfig };
|