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.js
ADDED
|
@@ -0,0 +1,4252 @@
|
|
|
1
|
+
// src/lark/client.ts
|
|
2
|
+
import * as lark from "@larksuiteoapi/node-sdk";
|
|
3
|
+
|
|
4
|
+
// src/lark/parse.ts
|
|
5
|
+
function normalizeChatType(s) {
|
|
6
|
+
switch (s) {
|
|
7
|
+
case "p2p":
|
|
8
|
+
return "p2p";
|
|
9
|
+
case "group":
|
|
10
|
+
return "group";
|
|
11
|
+
case "topic_group":
|
|
12
|
+
return "topic_group";
|
|
13
|
+
default:
|
|
14
|
+
return "unknown";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function extractText(messageType, contentJson) {
|
|
18
|
+
if (messageType !== "text" && messageType !== "post") return "";
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = JSON.parse(contentJson);
|
|
22
|
+
} catch {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
if (messageType === "text") {
|
|
26
|
+
const text = parsed.text;
|
|
27
|
+
return typeof text === "string" ? text : "";
|
|
28
|
+
}
|
|
29
|
+
const post = parsed;
|
|
30
|
+
const parts = [];
|
|
31
|
+
if (post.title) parts.push(post.title);
|
|
32
|
+
if (Array.isArray(post.content)) {
|
|
33
|
+
for (const para of post.content) {
|
|
34
|
+
if (!Array.isArray(para)) continue;
|
|
35
|
+
for (const seg of para) {
|
|
36
|
+
if (seg.tag === "text" && typeof seg.text === "string") {
|
|
37
|
+
parts.push(seg.text);
|
|
38
|
+
} else if (seg.tag === "at" && typeof seg.user_id === "string") {
|
|
39
|
+
parts.push(`@${seg.user_id}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return parts.join(" ").trim();
|
|
45
|
+
}
|
|
46
|
+
function parseIncomingMessage(ev) {
|
|
47
|
+
const m = ev.message;
|
|
48
|
+
const senderOpenId = ev.sender?.sender_id?.open_id ?? "";
|
|
49
|
+
const mentions = (m.mentions ?? []).map((x) => {
|
|
50
|
+
const mention = {
|
|
51
|
+
key: x.key,
|
|
52
|
+
name: x.name
|
|
53
|
+
};
|
|
54
|
+
if (x.id.open_id !== void 0) {
|
|
55
|
+
mention.openId = x.id.open_id;
|
|
56
|
+
}
|
|
57
|
+
return mention;
|
|
58
|
+
});
|
|
59
|
+
const result = {
|
|
60
|
+
eventId: ev.event_id ?? "",
|
|
61
|
+
messageId: m.message_id,
|
|
62
|
+
chatId: m.chat_id,
|
|
63
|
+
chatType: normalizeChatType(m.chat_type),
|
|
64
|
+
senderOpenId,
|
|
65
|
+
messageType: m.message_type,
|
|
66
|
+
rawContent: m.content,
|
|
67
|
+
text: extractText(m.message_type, m.content),
|
|
68
|
+
mentions,
|
|
69
|
+
receivedAt: Date.now()
|
|
70
|
+
};
|
|
71
|
+
if (m.thread_id !== void 0) {
|
|
72
|
+
result.threadId = m.thread_id;
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
function stripMentions(msg, botOpenId) {
|
|
77
|
+
let text = msg.text;
|
|
78
|
+
for (const m of msg.mentions) {
|
|
79
|
+
if (m.openId === botOpenId) {
|
|
80
|
+
text = text.split(m.key).join("");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return text.trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/lark/cardAction.ts
|
|
87
|
+
function parseCardAction(raw) {
|
|
88
|
+
if (!raw || typeof raw !== "object") return null;
|
|
89
|
+
const r = raw;
|
|
90
|
+
const ev = r.event ?? r;
|
|
91
|
+
const op = ev.operator ?? r.operator ?? {};
|
|
92
|
+
const action = ev.action ?? r.action ?? {};
|
|
93
|
+
const ctx = ev.context ?? {};
|
|
94
|
+
const messageId = ctx.open_message_id ?? ev.open_message_id ?? r.open_message_id ?? "";
|
|
95
|
+
const chatId = ctx.open_chat_id ?? ev.open_chat_id ?? r.open_chat_id ?? "";
|
|
96
|
+
const senderOpenId = op.open_id ?? op.openId ?? "";
|
|
97
|
+
if (!messageId || !chatId || !senderOpenId) return null;
|
|
98
|
+
let value = {};
|
|
99
|
+
if (action.value && typeof action.value === "object" && !Array.isArray(action.value)) {
|
|
100
|
+
value = action.value;
|
|
101
|
+
}
|
|
102
|
+
const result = {
|
|
103
|
+
messageId,
|
|
104
|
+
chatId,
|
|
105
|
+
senderOpenId,
|
|
106
|
+
value,
|
|
107
|
+
receivedAt: Date.now()
|
|
108
|
+
};
|
|
109
|
+
const formValue = action.form_value ?? action.input_value;
|
|
110
|
+
if (formValue && typeof formValue === "object") {
|
|
111
|
+
result.formValue = formValue;
|
|
112
|
+
}
|
|
113
|
+
const token = ev.token ?? r.token;
|
|
114
|
+
if (token !== void 0) result.token = token;
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/lark/client.ts
|
|
119
|
+
var LarkClient = class {
|
|
120
|
+
appId;
|
|
121
|
+
appSecret;
|
|
122
|
+
log;
|
|
123
|
+
api;
|
|
124
|
+
wsClient = null;
|
|
125
|
+
botOpenIdCache = null;
|
|
126
|
+
constructor(opts) {
|
|
127
|
+
this.appId = opts.appId;
|
|
128
|
+
this.appSecret = opts.appSecret;
|
|
129
|
+
this.log = opts.logger.child({ module: "lark-client" });
|
|
130
|
+
this.api = new lark.Client({
|
|
131
|
+
appId: this.appId,
|
|
132
|
+
appSecret: this.appSecret,
|
|
133
|
+
disableTokenCache: false
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* 启动 WebSocket 长连接,注册事件 handler:
|
|
138
|
+
* - im.message.receive_v1:用户发的消息
|
|
139
|
+
* - card.action.trigger:用户点了卡片上的按钮
|
|
140
|
+
*/
|
|
141
|
+
async startEventLoop(handlers) {
|
|
142
|
+
const wsClient = new lark.WSClient({
|
|
143
|
+
appId: this.appId,
|
|
144
|
+
appSecret: this.appSecret,
|
|
145
|
+
loggerLevel: lark.LoggerLevel.warn,
|
|
146
|
+
onReady: () => {
|
|
147
|
+
this.log.info("lark websocket connected");
|
|
148
|
+
handlers.onReady?.();
|
|
149
|
+
},
|
|
150
|
+
onReconnecting: () => {
|
|
151
|
+
this.log.warn("lark websocket reconnecting");
|
|
152
|
+
},
|
|
153
|
+
onReconnected: () => {
|
|
154
|
+
this.log.info("lark websocket reconnected");
|
|
155
|
+
handlers.onReconnected?.();
|
|
156
|
+
},
|
|
157
|
+
onError: (err) => {
|
|
158
|
+
this.log.error({ err }, "lark websocket error");
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
const dispatcher = new lark.EventDispatcher({}).register({
|
|
162
|
+
"im.message.receive_v1": async (data) => {
|
|
163
|
+
try {
|
|
164
|
+
const msg = parseIncomingMessage(data);
|
|
165
|
+
this.log.info(
|
|
166
|
+
{
|
|
167
|
+
event: "enter",
|
|
168
|
+
eventId: msg.eventId,
|
|
169
|
+
chatId: msg.chatId,
|
|
170
|
+
senderId: msg.senderOpenId,
|
|
171
|
+
chatType: msg.chatType,
|
|
172
|
+
messageType: msg.messageType
|
|
173
|
+
},
|
|
174
|
+
"incoming message"
|
|
175
|
+
);
|
|
176
|
+
Promise.resolve(handlers.onMessage(msg)).catch((e) => {
|
|
177
|
+
this.log.error({ err: e }, "onMessage handler threw");
|
|
178
|
+
});
|
|
179
|
+
} catch (e) {
|
|
180
|
+
this.log.error({ err: e }, "event parse failed");
|
|
181
|
+
}
|
|
182
|
+
return { code: 0 };
|
|
183
|
+
},
|
|
184
|
+
"card.action.trigger": async (data) => {
|
|
185
|
+
try {
|
|
186
|
+
const evt = parseCardAction(data);
|
|
187
|
+
if (!evt) {
|
|
188
|
+
this.log.debug({ raw: data }, "card.action.trigger: cannot parse, skip");
|
|
189
|
+
return { code: 0 };
|
|
190
|
+
}
|
|
191
|
+
this.log.info(
|
|
192
|
+
{
|
|
193
|
+
event: "card-action",
|
|
194
|
+
messageId: evt.messageId,
|
|
195
|
+
chatId: evt.chatId,
|
|
196
|
+
senderId: evt.senderOpenId,
|
|
197
|
+
valueKeys: Object.keys(evt.value)
|
|
198
|
+
},
|
|
199
|
+
"card action"
|
|
200
|
+
);
|
|
201
|
+
if (handlers.onCardAction) {
|
|
202
|
+
Promise.resolve(handlers.onCardAction(evt)).catch((e) => {
|
|
203
|
+
this.log.error({ err: e }, "onCardAction handler threw");
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
} catch (e) {
|
|
207
|
+
this.log.error({ err: e }, "card action parse failed");
|
|
208
|
+
}
|
|
209
|
+
return { code: 0 };
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
await wsClient.start({ eventDispatcher: dispatcher });
|
|
213
|
+
this.wsClient = wsClient;
|
|
214
|
+
}
|
|
215
|
+
close() {
|
|
216
|
+
this.wsClient?.close();
|
|
217
|
+
this.wsClient = null;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* 查询当前机器人在租户里的 open_id(用来识别 @bot)。
|
|
221
|
+
* 应用启动时调用一次后缓存。
|
|
222
|
+
*/
|
|
223
|
+
async getBotOpenId() {
|
|
224
|
+
if (this.botOpenIdCache) return this.botOpenIdCache;
|
|
225
|
+
return "";
|
|
226
|
+
}
|
|
227
|
+
/** 业务侧主动设置 botOpenId(一般从配置或第一次 @bot 学习而来)。 */
|
|
228
|
+
setBotOpenId(openId) {
|
|
229
|
+
this.botOpenIdCache = openId;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* 发送一张飞书卡片(v2 协议)。
|
|
233
|
+
* 返回卡片所在消息的 message_id,后续用 patchCard 更新。
|
|
234
|
+
*/
|
|
235
|
+
async sendCard(chatId, cardJson) {
|
|
236
|
+
const resp = await this.api.im.v1.message.create({
|
|
237
|
+
params: { receive_id_type: "chat_id" },
|
|
238
|
+
data: {
|
|
239
|
+
receive_id: chatId,
|
|
240
|
+
msg_type: "interactive",
|
|
241
|
+
content: JSON.stringify(cardJson)
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
const messageId = resp.data?.message_id;
|
|
245
|
+
if (!messageId) {
|
|
246
|
+
throw new Error(`sendCard failed, no message_id in response: ${JSON.stringify(resp)}`);
|
|
247
|
+
}
|
|
248
|
+
return messageId;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* 回复一条消息(reply 卡片,让卡片挂在用户消息下面)。
|
|
252
|
+
*/
|
|
253
|
+
async replyCard(messageId, cardJson) {
|
|
254
|
+
const resp = await this.api.im.v1.message.reply({
|
|
255
|
+
path: { message_id: messageId },
|
|
256
|
+
data: {
|
|
257
|
+
msg_type: "interactive",
|
|
258
|
+
content: JSON.stringify(cardJson)
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
const newMsgId = resp.data?.message_id;
|
|
262
|
+
if (!newMsgId) {
|
|
263
|
+
throw new Error(`replyCard failed, no message_id in response: ${JSON.stringify(resp)}`);
|
|
264
|
+
}
|
|
265
|
+
return newMsgId;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* 用 patch 接口整体替换卡片内容。
|
|
269
|
+
* 飞书消息卡片支持通过 `im/v1/messages/:message_id` PATCH 来更新内容,
|
|
270
|
+
* 内容必须是合法的卡片 JSON 字符串。
|
|
271
|
+
*/
|
|
272
|
+
async patchCard(messageId, cardJson) {
|
|
273
|
+
await this.api.im.v1.message.patch({
|
|
274
|
+
path: { message_id: messageId },
|
|
275
|
+
data: { content: JSON.stringify(cardJson) }
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
/** 发送纯文本消息(错误兜底用) */
|
|
279
|
+
async sendText(chatId, text) {
|
|
280
|
+
const resp = await this.api.im.v1.message.create({
|
|
281
|
+
params: { receive_id_type: "chat_id" },
|
|
282
|
+
data: {
|
|
283
|
+
receive_id: chatId,
|
|
284
|
+
msg_type: "text",
|
|
285
|
+
content: JSON.stringify({ text })
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
const messageId = resp.data?.message_id;
|
|
289
|
+
if (!messageId) {
|
|
290
|
+
throw new Error(`sendText failed, no message_id in response: ${JSON.stringify(resp)}`);
|
|
291
|
+
}
|
|
292
|
+
return messageId;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// src/store/sessions.ts
|
|
297
|
+
import { readFileSync as readFileSync2, writeFileSync, existsSync } from "fs";
|
|
298
|
+
import lockfile from "proper-lockfile";
|
|
299
|
+
import { z } from "zod";
|
|
300
|
+
|
|
301
|
+
// src/lib/paths.ts
|
|
302
|
+
import { homedir } from "os";
|
|
303
|
+
import { join } from "path";
|
|
304
|
+
import { mkdirSync } from "fs";
|
|
305
|
+
var DATA_DIR = join(homedir(), ".lark-kiro-bridge");
|
|
306
|
+
var LOGS_DIR = join(DATA_DIR, "logs");
|
|
307
|
+
var MEDIA_DIR = join(DATA_DIR, "media");
|
|
308
|
+
var CONFIG_FILE = join(DATA_DIR, "config.json");
|
|
309
|
+
var SESSIONS_FILE = join(DATA_DIR, "sessions.json");
|
|
310
|
+
var WORKSPACES_FILE = join(DATA_DIR, "workspaces.json");
|
|
311
|
+
var PROCESSES_FILE = join(DATA_DIR, "processes.json");
|
|
312
|
+
function ensureDataDirs() {
|
|
313
|
+
mkdirSync(DATA_DIR, { recursive: true, mode: 448 });
|
|
314
|
+
mkdirSync(LOGS_DIR, { recursive: true, mode: 448 });
|
|
315
|
+
mkdirSync(MEDIA_DIR, { recursive: true, mode: 448 });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/lib/logger.ts
|
|
319
|
+
import pino from "pino";
|
|
320
|
+
import { join as join2 } from "path";
|
|
321
|
+
import { readdirSync, statSync, unlinkSync, readFileSync } from "fs";
|
|
322
|
+
function todayLogFile() {
|
|
323
|
+
const d = /* @__PURE__ */ new Date();
|
|
324
|
+
const yyyy = d.getFullYear();
|
|
325
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
326
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
327
|
+
return join2(LOGS_DIR, `${yyyy}-${mm}-${dd}.log`);
|
|
328
|
+
}
|
|
329
|
+
function pruneOldLogs(retainDays) {
|
|
330
|
+
if (!Number.isFinite(retainDays) || retainDays <= 0) return;
|
|
331
|
+
const cutoff = Date.now() - retainDays * 24 * 60 * 60 * 1e3;
|
|
332
|
+
try {
|
|
333
|
+
const files = readdirSync(LOGS_DIR);
|
|
334
|
+
for (const f of files) {
|
|
335
|
+
if (!f.endsWith(".log")) continue;
|
|
336
|
+
const full = join2(LOGS_DIR, f);
|
|
337
|
+
try {
|
|
338
|
+
const st = statSync(full);
|
|
339
|
+
if (st.mtimeMs < cutoff) unlinkSync(full);
|
|
340
|
+
} catch {
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
var REDACT_PATHS = [
|
|
347
|
+
// App 凭证
|
|
348
|
+
"*.appSecret",
|
|
349
|
+
"*.app_secret",
|
|
350
|
+
"config.lark.appSecret",
|
|
351
|
+
"lark.appSecret",
|
|
352
|
+
// Tokens(access / tenant / app / refresh)
|
|
353
|
+
"*.accessToken",
|
|
354
|
+
"*.access_token",
|
|
355
|
+
"*.tenantAccessToken",
|
|
356
|
+
"*.tenant_access_token",
|
|
357
|
+
"*.appAccessToken",
|
|
358
|
+
"*.app_access_token",
|
|
359
|
+
"*.refreshToken",
|
|
360
|
+
"*.refresh_token",
|
|
361
|
+
// 通用 Auth header
|
|
362
|
+
"*.authorization",
|
|
363
|
+
"*.Authorization",
|
|
364
|
+
// 飞书卡片回调里的临时 token(30 分钟有效,泄露能伪造卡片更新)
|
|
365
|
+
"event.token",
|
|
366
|
+
"*.cardToken"
|
|
367
|
+
];
|
|
368
|
+
var cachedLogger = null;
|
|
369
|
+
function getLogger() {
|
|
370
|
+
if (cachedLogger) return cachedLogger;
|
|
371
|
+
ensureDataDirs();
|
|
372
|
+
const isTty = process.stdout.isTTY;
|
|
373
|
+
const level = process.env["LARK_KIRO_LOG_LEVEL"] ?? (isTty ? "info" : "info");
|
|
374
|
+
if (isTty) {
|
|
375
|
+
cachedLogger = pino({
|
|
376
|
+
level,
|
|
377
|
+
redact: { paths: REDACT_PATHS, censor: "[REDACTED]" },
|
|
378
|
+
transport: {
|
|
379
|
+
target: "pino-pretty",
|
|
380
|
+
options: {
|
|
381
|
+
colorize: true,
|
|
382
|
+
translateTime: "HH:MM:ss.l",
|
|
383
|
+
ignore: "pid,hostname"
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
} else {
|
|
388
|
+
cachedLogger = pino(
|
|
389
|
+
{
|
|
390
|
+
level,
|
|
391
|
+
base: { pid: process.pid },
|
|
392
|
+
redact: { paths: REDACT_PATHS, censor: "[REDACTED]" }
|
|
393
|
+
},
|
|
394
|
+
pino.destination({ dest: todayLogFile(), append: true, sync: false })
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
const envDays = Number(process.env["LARK_KIRO_LOG_DAYS"]);
|
|
398
|
+
if (Number.isFinite(envDays) && envDays > 0) {
|
|
399
|
+
pruneOldLogs(envDays);
|
|
400
|
+
} else {
|
|
401
|
+
pruneOldLogs(7);
|
|
402
|
+
}
|
|
403
|
+
return cachedLogger;
|
|
404
|
+
}
|
|
405
|
+
function readRecentLogLines(maxLines = 200) {
|
|
406
|
+
try {
|
|
407
|
+
const files = readdirSync(LOGS_DIR).filter((f) => f.endsWith(".log") && !f.startsWith("daemon-")).map((f) => ({ f, t: statSync(join2(LOGS_DIR, f)).mtimeMs })).sort((a, b) => b.t - a.t).slice(0, 3).map((x) => x.f);
|
|
408
|
+
const allLines = [];
|
|
409
|
+
for (const f of files) {
|
|
410
|
+
const full = join2(LOGS_DIR, f);
|
|
411
|
+
const raw = readFileSync(full, "utf-8");
|
|
412
|
+
for (const line of raw.split("\n")) {
|
|
413
|
+
if (!line) continue;
|
|
414
|
+
allLines.push(line.length > 1024 ? line.slice(0, 1024) + "\u2026[truncated]" : line);
|
|
415
|
+
}
|
|
416
|
+
if (allLines.length >= maxLines * 3) break;
|
|
417
|
+
}
|
|
418
|
+
return allLines.slice(-maxLines);
|
|
419
|
+
} catch {
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// src/store/sessions.ts
|
|
425
|
+
var ChatSessionSchema = z.object({
|
|
426
|
+
currentCwd: z.string(),
|
|
427
|
+
sessionsByCwd: z.record(z.string(), z.string()).default({}),
|
|
428
|
+
lastActiveAt: z.number().int().nonnegative().default(0),
|
|
429
|
+
/**
|
|
430
|
+
* per-chat idle watchdog 分钟数覆盖:
|
|
431
|
+
* undefined → 用全局默认(preferences/kiro.idleTimeoutMinutes)
|
|
432
|
+
* 0 → 显式关闭
|
|
433
|
+
* N>0 → 用 N 分钟
|
|
434
|
+
*/
|
|
435
|
+
idleTimeoutMinutes: z.number().int().nonnegative().optional()
|
|
436
|
+
});
|
|
437
|
+
var SessionsFileSchema = z.object({
|
|
438
|
+
version: z.literal(1).default(1),
|
|
439
|
+
chats: z.record(z.string(), ChatSessionSchema).default({})
|
|
440
|
+
});
|
|
441
|
+
var log = () => getLogger().child({ module: "sessions" });
|
|
442
|
+
function readFile() {
|
|
443
|
+
if (!existsSync(SESSIONS_FILE)) {
|
|
444
|
+
return SessionsFileSchema.parse({});
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
const raw = readFileSync2(SESSIONS_FILE, "utf-8");
|
|
448
|
+
const parsed = SessionsFileSchema.safeParse(JSON.parse(raw));
|
|
449
|
+
if (!parsed.success) {
|
|
450
|
+
log().warn({ err: parsed.error.issues }, "sessions.json validation failed, resetting");
|
|
451
|
+
return SessionsFileSchema.parse({});
|
|
452
|
+
}
|
|
453
|
+
return parsed.data;
|
|
454
|
+
} catch (e) {
|
|
455
|
+
log().warn({ err: e }, "sessions.json read failed, resetting");
|
|
456
|
+
return SessionsFileSchema.parse({});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
function writeFile(data) {
|
|
460
|
+
ensureDataDirs();
|
|
461
|
+
writeFileSync(SESSIONS_FILE, JSON.stringify(data, null, 2) + "\n", { mode: 384 });
|
|
462
|
+
}
|
|
463
|
+
async function withLock(fn) {
|
|
464
|
+
ensureDataDirs();
|
|
465
|
+
if (!existsSync(SESSIONS_FILE)) {
|
|
466
|
+
writeFileSync(SESSIONS_FILE, "{}\n", { mode: 384 });
|
|
467
|
+
}
|
|
468
|
+
const release = await lockfile.lock(SESSIONS_FILE, {
|
|
469
|
+
retries: { retries: 5, minTimeout: 50, maxTimeout: 200 },
|
|
470
|
+
stale: 1e4
|
|
471
|
+
});
|
|
472
|
+
try {
|
|
473
|
+
return fn();
|
|
474
|
+
} finally {
|
|
475
|
+
await release();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
var SessionStore = class {
|
|
479
|
+
/**
|
|
480
|
+
* 获取一个 chat 的会话状态;不存在则用 defaultCwd 初始化。
|
|
481
|
+
*/
|
|
482
|
+
async get(chatId, defaultCwd) {
|
|
483
|
+
return withLock(() => {
|
|
484
|
+
const data = readFile();
|
|
485
|
+
let session = data.chats[chatId];
|
|
486
|
+
if (!session) {
|
|
487
|
+
session = {
|
|
488
|
+
currentCwd: defaultCwd,
|
|
489
|
+
sessionsByCwd: {},
|
|
490
|
+
lastActiveAt: Date.now()
|
|
491
|
+
};
|
|
492
|
+
data.chats[chatId] = session;
|
|
493
|
+
writeFile(data);
|
|
494
|
+
}
|
|
495
|
+
return session;
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* 切换 chat 的当前 cwd。该 cwd 下若已有 kiro session 会被自动延用。
|
|
500
|
+
*/
|
|
501
|
+
async setCwd(chatId, cwd, defaultCwd) {
|
|
502
|
+
return withLock(() => {
|
|
503
|
+
const data = readFile();
|
|
504
|
+
const session = data.chats[chatId] ?? {
|
|
505
|
+
currentCwd: defaultCwd,
|
|
506
|
+
sessionsByCwd: {},
|
|
507
|
+
lastActiveAt: Date.now()
|
|
508
|
+
};
|
|
509
|
+
session.currentCwd = cwd;
|
|
510
|
+
session.lastActiveAt = Date.now();
|
|
511
|
+
data.chats[chatId] = session;
|
|
512
|
+
writeFile(data);
|
|
513
|
+
return session;
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* 关联当前 (chatId, cwd) 到一个 kiroSessionId(Kiro CLI 跑完返回的 sid)。
|
|
518
|
+
*/
|
|
519
|
+
async setKiroSession(chatId, cwd, kiroSessionId) {
|
|
520
|
+
await withLock(() => {
|
|
521
|
+
const data = readFile();
|
|
522
|
+
const session = data.chats[chatId];
|
|
523
|
+
if (!session) return;
|
|
524
|
+
session.sessionsByCwd[cwd] = kiroSessionId;
|
|
525
|
+
session.lastActiveAt = Date.now();
|
|
526
|
+
writeFile(data);
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* 清空当前 cwd 下的 kiro session(用于 /new 命令)。
|
|
531
|
+
*/
|
|
532
|
+
async clearKiroSession(chatId, cwd) {
|
|
533
|
+
await withLock(() => {
|
|
534
|
+
const data = readFile();
|
|
535
|
+
const session = data.chats[chatId];
|
|
536
|
+
if (!session) return;
|
|
537
|
+
delete session.sessionsByCwd[cwd];
|
|
538
|
+
session.lastActiveAt = Date.now();
|
|
539
|
+
writeFile(data);
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* 获取当前 (chatId, cwd) 对应的 kiroSessionId(可能不存在)。
|
|
544
|
+
*/
|
|
545
|
+
async getKiroSession(chatId, cwd) {
|
|
546
|
+
return withLock(() => {
|
|
547
|
+
const data = readFile();
|
|
548
|
+
return data.chats[chatId]?.sessionsByCwd[cwd];
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* 设置 chat 的 idle watchdog 阈值(分钟)。
|
|
553
|
+
* - undefined:清除覆盖,回归全局默认
|
|
554
|
+
* - 0:关闭
|
|
555
|
+
* - N>0:用 N 分钟
|
|
556
|
+
*/
|
|
557
|
+
async setIdleTimeout(chatId, minutes, defaultCwd) {
|
|
558
|
+
await withLock(() => {
|
|
559
|
+
const data = readFile();
|
|
560
|
+
const session = data.chats[chatId] ?? {
|
|
561
|
+
currentCwd: defaultCwd,
|
|
562
|
+
sessionsByCwd: {},
|
|
563
|
+
lastActiveAt: Date.now()
|
|
564
|
+
};
|
|
565
|
+
if (minutes === void 0) {
|
|
566
|
+
delete session.idleTimeoutMinutes;
|
|
567
|
+
} else {
|
|
568
|
+
session.idleTimeoutMinutes = minutes;
|
|
569
|
+
}
|
|
570
|
+
session.lastActiveAt = Date.now();
|
|
571
|
+
data.chats[chatId] = session;
|
|
572
|
+
writeFile(data);
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
// src/store/workspaces.ts
|
|
578
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
|
|
579
|
+
import lockfile2 from "proper-lockfile";
|
|
580
|
+
import { z as z2 } from "zod";
|
|
581
|
+
var WorkspacesFileSchema = z2.object({
|
|
582
|
+
version: z2.literal(1).default(1),
|
|
583
|
+
workspaces: z2.record(z2.string(), z2.string()).default({})
|
|
584
|
+
});
|
|
585
|
+
var log2 = () => getLogger().child({ module: "workspaces" });
|
|
586
|
+
function readFile2() {
|
|
587
|
+
if (!existsSync2(WORKSPACES_FILE)) {
|
|
588
|
+
return WorkspacesFileSchema.parse({});
|
|
589
|
+
}
|
|
590
|
+
try {
|
|
591
|
+
const raw = readFileSync3(WORKSPACES_FILE, "utf-8");
|
|
592
|
+
const parsed = WorkspacesFileSchema.safeParse(JSON.parse(raw));
|
|
593
|
+
if (!parsed.success) {
|
|
594
|
+
log2().warn({ err: parsed.error.issues }, "workspaces.json validation failed, resetting");
|
|
595
|
+
return WorkspacesFileSchema.parse({});
|
|
596
|
+
}
|
|
597
|
+
return parsed.data;
|
|
598
|
+
} catch (e) {
|
|
599
|
+
log2().warn({ err: e }, "workspaces.json read failed, resetting");
|
|
600
|
+
return WorkspacesFileSchema.parse({});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
function writeFile2(data) {
|
|
604
|
+
ensureDataDirs();
|
|
605
|
+
writeFileSync2(WORKSPACES_FILE, JSON.stringify(data, null, 2) + "\n", { mode: 384 });
|
|
606
|
+
}
|
|
607
|
+
async function withLock2(fn) {
|
|
608
|
+
ensureDataDirs();
|
|
609
|
+
if (!existsSync2(WORKSPACES_FILE)) {
|
|
610
|
+
writeFileSync2(WORKSPACES_FILE, "{}\n", { mode: 384 });
|
|
611
|
+
}
|
|
612
|
+
const release = await lockfile2.lock(WORKSPACES_FILE, {
|
|
613
|
+
retries: { retries: 5, minTimeout: 50, maxTimeout: 200 },
|
|
614
|
+
stale: 1e4
|
|
615
|
+
});
|
|
616
|
+
try {
|
|
617
|
+
return fn();
|
|
618
|
+
} finally {
|
|
619
|
+
await release();
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
var NAME_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
623
|
+
var WorkspaceStore = class {
|
|
624
|
+
async list() {
|
|
625
|
+
return withLock2(() => readFile2().workspaces);
|
|
626
|
+
}
|
|
627
|
+
async get(name) {
|
|
628
|
+
return withLock2(() => readFile2().workspaces[name]);
|
|
629
|
+
}
|
|
630
|
+
async save(name, absPath) {
|
|
631
|
+
if (!NAME_PATTERN.test(name)) {
|
|
632
|
+
throw new Error(
|
|
633
|
+
`Invalid workspace name "${name}". Use letters, digits, "-" or "_" only (1\u201364 chars).`
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
await withLock2(() => {
|
|
637
|
+
const data = readFile2();
|
|
638
|
+
data.workspaces[name] = absPath;
|
|
639
|
+
writeFile2(data);
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
async remove(name) {
|
|
643
|
+
return withLock2(() => {
|
|
644
|
+
const data = readFile2();
|
|
645
|
+
if (!(name in data.workspaces)) return false;
|
|
646
|
+
delete data.workspaces[name];
|
|
647
|
+
writeFile2(data);
|
|
648
|
+
return true;
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
// src/lib/config.ts
|
|
654
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync3, chmodSync } from "fs";
|
|
655
|
+
import { z as z3 } from "zod";
|
|
656
|
+
var ConfigSchema = z3.object({
|
|
657
|
+
lark: z3.object({
|
|
658
|
+
appId: z3.string().min(1),
|
|
659
|
+
appSecret: z3.string().min(1)
|
|
660
|
+
}),
|
|
661
|
+
kiro: z3.object({
|
|
662
|
+
binPath: z3.string().default("kiro-cli"),
|
|
663
|
+
trustedTools: z3.array(z3.string()).default(["fs_read", "fs_write", "grep", "glob", "code"]),
|
|
664
|
+
timeoutMs: z3.number().int().positive().default(10 * 60 * 1e3),
|
|
665
|
+
/**
|
|
666
|
+
* 默认 idle watchdog(分钟)。0 = 关闭。
|
|
667
|
+
* stdout 连续这么久没新输出就当作 kiro-cli 假死,killTree。
|
|
668
|
+
* /timeout N 可以临时为某个 chat 覆盖。
|
|
669
|
+
*/
|
|
670
|
+
idleTimeoutMinutes: z3.number().int().nonnegative().default(5),
|
|
671
|
+
model: z3.string().optional(),
|
|
672
|
+
agent: z3.string().optional()
|
|
673
|
+
}).default({}),
|
|
674
|
+
workspace: z3.object({
|
|
675
|
+
defaultCwd: z3.string().default("/Users/administrator/PycharmProjects"),
|
|
676
|
+
allowedRoots: z3.array(z3.string()).default(["/Users/administrator/PycharmProjects"])
|
|
677
|
+
}).default({}),
|
|
678
|
+
access: z3.object({
|
|
679
|
+
allowedUsers: z3.array(z3.string()).default([]),
|
|
680
|
+
allowedChats: z3.array(z3.string()).default([]),
|
|
681
|
+
admins: z3.array(z3.string()).default([])
|
|
682
|
+
}).default({}),
|
|
683
|
+
preferences: z3.object({
|
|
684
|
+
requireMentionInGroup: z3.boolean().default(true),
|
|
685
|
+
cardUpdateIntervalMs: z3.number().int().positive().default(800),
|
|
686
|
+
/** 启动时清理多少天之前的日志文件 */
|
|
687
|
+
logRetentionDays: z3.number().int().positive().default(7)
|
|
688
|
+
}).default({})
|
|
689
|
+
});
|
|
690
|
+
var ConfigError = class extends Error {
|
|
691
|
+
constructor(message) {
|
|
692
|
+
super(message);
|
|
693
|
+
this.name = "ConfigError";
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
function loadConfig() {
|
|
697
|
+
ensureDataDirs();
|
|
698
|
+
if (!existsSync3(CONFIG_FILE)) {
|
|
699
|
+
throw new ConfigError(
|
|
700
|
+
`Config file not found at ${CONFIG_FILE}. Run \`lark-kiro-bridge init\` to create one.`
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
const raw = readFileSync4(CONFIG_FILE, "utf-8");
|
|
704
|
+
let parsed;
|
|
705
|
+
try {
|
|
706
|
+
parsed = JSON.parse(raw);
|
|
707
|
+
} catch (e) {
|
|
708
|
+
throw new ConfigError(`Config file is not valid JSON: ${e.message}`);
|
|
709
|
+
}
|
|
710
|
+
const result = ConfigSchema.safeParse(parsed);
|
|
711
|
+
if (!result.success) {
|
|
712
|
+
throw new ConfigError(
|
|
713
|
+
`Config validation failed:
|
|
714
|
+
${result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}`
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
return result.data;
|
|
718
|
+
}
|
|
719
|
+
function saveConfig(cfg) {
|
|
720
|
+
ensureDataDirs();
|
|
721
|
+
writeFileSync3(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
|
|
722
|
+
chmodSync(CONFIG_FILE, 384);
|
|
723
|
+
}
|
|
724
|
+
function patchAndSaveConfig(cfg, mutator) {
|
|
725
|
+
const draft = JSON.parse(JSON.stringify(cfg));
|
|
726
|
+
mutator(draft);
|
|
727
|
+
const validated = ConfigSchema.parse(draft);
|
|
728
|
+
saveConfig(validated);
|
|
729
|
+
return validated;
|
|
730
|
+
}
|
|
731
|
+
function defaultConfig(appId, appSecret) {
|
|
732
|
+
return ConfigSchema.parse({
|
|
733
|
+
lark: { appId, appSecret }
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// src/lark/media.ts
|
|
738
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4, existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2, rmSync } from "fs";
|
|
739
|
+
import { join as join3 } from "path";
|
|
740
|
+
import "stream";
|
|
741
|
+
var log3 = () => getLogger().child({ module: "media" });
|
|
742
|
+
function sanitizeFilename(name, fallbackKey, defaultExt) {
|
|
743
|
+
let s = name.replace(/[/\\\x00-\x1f]/g, "_").trim();
|
|
744
|
+
if (!s) s = fallbackKey + defaultExt;
|
|
745
|
+
if (s.length > 80) {
|
|
746
|
+
const dotIdx = s.lastIndexOf(".");
|
|
747
|
+
if (dotIdx > 0 && dotIdx > s.length - 12) {
|
|
748
|
+
const ext = s.slice(dotIdx);
|
|
749
|
+
s = s.slice(0, 80 - ext.length) + ext;
|
|
750
|
+
} else {
|
|
751
|
+
s = s.slice(0, 80);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return s;
|
|
755
|
+
}
|
|
756
|
+
function ensureChatDir(chatId) {
|
|
757
|
+
const dir = join3(MEDIA_DIR, chatId);
|
|
758
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
759
|
+
return dir;
|
|
760
|
+
}
|
|
761
|
+
async function downloadMessageMedia(lark2, msg) {
|
|
762
|
+
const out = [];
|
|
763
|
+
const ts = Date.now();
|
|
764
|
+
let parsed;
|
|
765
|
+
try {
|
|
766
|
+
parsed = JSON.parse(msg.rawContent);
|
|
767
|
+
} catch {
|
|
768
|
+
return out;
|
|
769
|
+
}
|
|
770
|
+
if (msg.messageType === "image") {
|
|
771
|
+
const c = parsed;
|
|
772
|
+
if (c.image_key) {
|
|
773
|
+
const filename = sanitizeFilename("", `${ts}-${c.image_key.slice(0, 8)}`, ".jpg");
|
|
774
|
+
const path = await fetchResource(
|
|
775
|
+
lark2,
|
|
776
|
+
msg.messageId,
|
|
777
|
+
c.image_key,
|
|
778
|
+
"image",
|
|
779
|
+
chatPath(msg.chatId, filename)
|
|
780
|
+
);
|
|
781
|
+
if (path) out.push(path);
|
|
782
|
+
}
|
|
783
|
+
} else if (msg.messageType === "file") {
|
|
784
|
+
const c = parsed;
|
|
785
|
+
if (c.file_key) {
|
|
786
|
+
const filename = sanitizeFilename(
|
|
787
|
+
c.file_name ?? "",
|
|
788
|
+
`${ts}-${c.file_key.slice(0, 8)}`,
|
|
789
|
+
".bin"
|
|
790
|
+
);
|
|
791
|
+
const path = await fetchResource(
|
|
792
|
+
lark2,
|
|
793
|
+
msg.messageId,
|
|
794
|
+
c.file_key,
|
|
795
|
+
"file",
|
|
796
|
+
chatPath(msg.chatId, filename)
|
|
797
|
+
);
|
|
798
|
+
if (path) out.push(path);
|
|
799
|
+
}
|
|
800
|
+
} else if (msg.messageType === "audio") {
|
|
801
|
+
const c = parsed;
|
|
802
|
+
if (c.file_key) {
|
|
803
|
+
const filename = sanitizeFilename("", `${ts}-${c.file_key.slice(0, 8)}`, ".opus");
|
|
804
|
+
const path = await fetchResource(
|
|
805
|
+
lark2,
|
|
806
|
+
msg.messageId,
|
|
807
|
+
c.file_key,
|
|
808
|
+
"file",
|
|
809
|
+
chatPath(msg.chatId, filename)
|
|
810
|
+
);
|
|
811
|
+
if (path) out.push(path);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return out;
|
|
815
|
+
}
|
|
816
|
+
function chatPath(chatId, filename) {
|
|
817
|
+
const dir = ensureChatDir(chatId);
|
|
818
|
+
let name = filename;
|
|
819
|
+
let full = join3(dir, name);
|
|
820
|
+
let i = 1;
|
|
821
|
+
while (existsSync4(full)) {
|
|
822
|
+
const dotIdx = filename.lastIndexOf(".");
|
|
823
|
+
name = dotIdx > 0 ? `${filename.slice(0, dotIdx)}-${i}${filename.slice(dotIdx)}` : `${filename}-${i}`;
|
|
824
|
+
full = join3(dir, name);
|
|
825
|
+
i++;
|
|
826
|
+
if (i > 1e3) break;
|
|
827
|
+
}
|
|
828
|
+
return { dir, full, name };
|
|
829
|
+
}
|
|
830
|
+
async function fetchResource(lark2, messageId, fileKey, resourceType, target) {
|
|
831
|
+
try {
|
|
832
|
+
const resp = await lark2.api.im.v1.messageResource.get({
|
|
833
|
+
path: { message_id: messageId, file_key: fileKey },
|
|
834
|
+
params: { type: resourceType }
|
|
835
|
+
});
|
|
836
|
+
if (typeof resp.writeFile === "function") {
|
|
837
|
+
await resp.writeFile(target.full);
|
|
838
|
+
} else {
|
|
839
|
+
const stream = resp.getReadableStream();
|
|
840
|
+
const chunks = [];
|
|
841
|
+
for await (const chunk of stream) {
|
|
842
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
843
|
+
}
|
|
844
|
+
writeFileSync4(target.full, Buffer.concat(chunks), { mode: 384 });
|
|
845
|
+
}
|
|
846
|
+
log3().info({ messageId, fileKey, resourceType, path: target.full }, "media downloaded");
|
|
847
|
+
return target.full;
|
|
848
|
+
} catch (e) {
|
|
849
|
+
log3().warn({ err: e.message, messageId, fileKey }, "media download failed");
|
|
850
|
+
return void 0;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
function pruneOldMedia(retainHours = 24) {
|
|
854
|
+
if (!existsSync4(MEDIA_DIR)) return;
|
|
855
|
+
const cutoff = Date.now() - retainHours * 60 * 60 * 1e3;
|
|
856
|
+
try {
|
|
857
|
+
for (const chat of readdirSync2(MEDIA_DIR)) {
|
|
858
|
+
const chatDir = join3(MEDIA_DIR, chat);
|
|
859
|
+
let dirSt;
|
|
860
|
+
try {
|
|
861
|
+
dirSt = statSync2(chatDir);
|
|
862
|
+
} catch {
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
if (!dirSt.isDirectory()) continue;
|
|
866
|
+
let files = [];
|
|
867
|
+
try {
|
|
868
|
+
files = readdirSync2(chatDir);
|
|
869
|
+
} catch {
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
for (const f of files) {
|
|
873
|
+
const full = join3(chatDir, f);
|
|
874
|
+
try {
|
|
875
|
+
const st = statSync2(full);
|
|
876
|
+
if (st.mtimeMs < cutoff) rmSync(full, { force: true });
|
|
877
|
+
} catch {
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
try {
|
|
881
|
+
if (readdirSync2(chatDir).length === 0) rmSync(chatDir, { recursive: true, force: true });
|
|
882
|
+
} catch {
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
} catch {
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// src/lark/asr.ts
|
|
890
|
+
import { execa } from "execa";
|
|
891
|
+
import { readFileSync as readFileSync5, statSync as statSync3, unlinkSync as unlinkSync2 } from "fs";
|
|
892
|
+
import { tmpdir } from "os";
|
|
893
|
+
import { join as join4 } from "path";
|
|
894
|
+
import { randomUUID } from "crypto";
|
|
895
|
+
var log4 = () => getLogger().child({ module: "asr" });
|
|
896
|
+
var MAX_DURATION_SEC = 60;
|
|
897
|
+
var ffmpegAvailableCache;
|
|
898
|
+
async function isFfmpegAvailable() {
|
|
899
|
+
if (ffmpegAvailableCache !== void 0) return ffmpegAvailableCache;
|
|
900
|
+
try {
|
|
901
|
+
await execa("ffmpeg", ["-version"], { reject: false, timeout: 3e3 });
|
|
902
|
+
ffmpegAvailableCache = true;
|
|
903
|
+
} catch {
|
|
904
|
+
ffmpegAvailableCache = false;
|
|
905
|
+
}
|
|
906
|
+
return ffmpegAvailableCache;
|
|
907
|
+
}
|
|
908
|
+
async function probeDurationSec(audioPath) {
|
|
909
|
+
try {
|
|
910
|
+
const r = await execa("ffmpeg", ["-i", audioPath, "-f", "null", "-"], {
|
|
911
|
+
reject: false,
|
|
912
|
+
timeout: 5e3
|
|
913
|
+
});
|
|
914
|
+
const stderr = r.stderr ?? "";
|
|
915
|
+
const m = stderr.match(/Duration:\s*(\d+):(\d+):(\d+(?:\.\d+)?)/);
|
|
916
|
+
if (!m) return -1;
|
|
917
|
+
const [, h, mm, ss] = m;
|
|
918
|
+
return Number(h) * 3600 + Number(mm) * 60 + Number(ss);
|
|
919
|
+
} catch {
|
|
920
|
+
return -1;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
async function toPcm(audioPath) {
|
|
924
|
+
const out = join4(tmpdir(), `lkb-asr-${randomUUID()}.pcm`);
|
|
925
|
+
await execa(
|
|
926
|
+
"ffmpeg",
|
|
927
|
+
[
|
|
928
|
+
"-y",
|
|
929
|
+
"-i",
|
|
930
|
+
audioPath,
|
|
931
|
+
"-ar",
|
|
932
|
+
"16000",
|
|
933
|
+
// 16 kHz
|
|
934
|
+
"-ac",
|
|
935
|
+
"1",
|
|
936
|
+
// mono
|
|
937
|
+
"-f",
|
|
938
|
+
"s16le",
|
|
939
|
+
// 16-bit little-endian PCM, 飞书要求的格式
|
|
940
|
+
out
|
|
941
|
+
],
|
|
942
|
+
{ reject: true, timeout: 3e4 }
|
|
943
|
+
);
|
|
944
|
+
return out;
|
|
945
|
+
}
|
|
946
|
+
async function transcribeAudio(client, audioPath) {
|
|
947
|
+
if (!await isFfmpegAvailable()) {
|
|
948
|
+
log4().warn("ffmpeg not found in PATH; voice transcription disabled");
|
|
949
|
+
return { ok: false, reason: "ffmpeg-missing" };
|
|
950
|
+
}
|
|
951
|
+
const durSec = await probeDurationSec(audioPath);
|
|
952
|
+
if (durSec > MAX_DURATION_SEC) {
|
|
953
|
+
log4().info({ durSec, audioPath }, "audio too long for ASR API, skipping");
|
|
954
|
+
return {
|
|
955
|
+
ok: false,
|
|
956
|
+
reason: "too-long",
|
|
957
|
+
detail: `${durSec.toFixed(1)}s > ${MAX_DURATION_SEC}s`
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
let pcmPath;
|
|
961
|
+
try {
|
|
962
|
+
pcmPath = await toPcm(audioPath);
|
|
963
|
+
} catch (e) {
|
|
964
|
+
log4().warn({ err: e.message, audioPath }, "ffmpeg transcoding failed");
|
|
965
|
+
return { ok: false, reason: "ffmpeg-failed", detail: e.message };
|
|
966
|
+
}
|
|
967
|
+
try {
|
|
968
|
+
const pcm = readFileSync5(pcmPath);
|
|
969
|
+
if (pcm.length === 0) {
|
|
970
|
+
return { ok: false, reason: "empty", detail: "transcoded pcm is empty" };
|
|
971
|
+
}
|
|
972
|
+
const speech = pcm.toString("base64");
|
|
973
|
+
const fileId = randomUUID().replace(/-/g, "").slice(0, 16);
|
|
974
|
+
const resp = await client.api.speech_to_text.v1.speech.fileRecognize({
|
|
975
|
+
data: {
|
|
976
|
+
speech: { speech },
|
|
977
|
+
config: {
|
|
978
|
+
file_id: fileId,
|
|
979
|
+
format: "pcm",
|
|
980
|
+
engine_type: "16k_auto"
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
if (resp.code !== 0) {
|
|
985
|
+
log4().warn({ code: resp.code, msg: resp.msg }, "ASR API returned non-zero code");
|
|
986
|
+
return { ok: false, reason: "api-failed", detail: `code=${resp.code} msg=${resp.msg}` };
|
|
987
|
+
}
|
|
988
|
+
const text = (resp.data?.recognition_text ?? "").trim();
|
|
989
|
+
if (!text) {
|
|
990
|
+
return { ok: false, reason: "empty", detail: "recognition_text is empty" };
|
|
991
|
+
}
|
|
992
|
+
log4().info({ durSec, textLength: text.length }, "ASR success");
|
|
993
|
+
return { ok: true, text };
|
|
994
|
+
} catch (e) {
|
|
995
|
+
log4().warn({ err: e.message }, "ASR API call threw");
|
|
996
|
+
return { ok: false, reason: "api-failed", detail: e.message };
|
|
997
|
+
} finally {
|
|
998
|
+
try {
|
|
999
|
+
const st = statSync3(pcmPath);
|
|
1000
|
+
if (st.isFile()) unlinkSync2(pcmPath);
|
|
1001
|
+
} catch {
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// src/commands/parse.ts
|
|
1007
|
+
var KIRO_INTERNAL_COMMANDS = /* @__PURE__ */ new Set([
|
|
1008
|
+
"agent",
|
|
1009
|
+
"tools",
|
|
1010
|
+
"compact",
|
|
1011
|
+
"login",
|
|
1012
|
+
"logout",
|
|
1013
|
+
"session",
|
|
1014
|
+
"sessions",
|
|
1015
|
+
"clear",
|
|
1016
|
+
"usage",
|
|
1017
|
+
"cost",
|
|
1018
|
+
"profile"
|
|
1019
|
+
]);
|
|
1020
|
+
var COMMAND_ALIASES = {
|
|
1021
|
+
// /model 系列
|
|
1022
|
+
m: "model",
|
|
1023
|
+
mod: "model",
|
|
1024
|
+
mode: "model",
|
|
1025
|
+
modle: "model",
|
|
1026
|
+
models: "model",
|
|
1027
|
+
// /help 系列
|
|
1028
|
+
h: "help",
|
|
1029
|
+
"?": "help",
|
|
1030
|
+
// /status 系列
|
|
1031
|
+
s: "status",
|
|
1032
|
+
stat: "status",
|
|
1033
|
+
// /new 系列
|
|
1034
|
+
reset: "new",
|
|
1035
|
+
clear: "new",
|
|
1036
|
+
// 注意:覆盖 KIRO_INTERNAL_COMMANDS 里的 clear
|
|
1037
|
+
// /stop 系列
|
|
1038
|
+
abort: "stop",
|
|
1039
|
+
cancel: "stop",
|
|
1040
|
+
// /pwd 系列
|
|
1041
|
+
cwd: "pwd",
|
|
1042
|
+
// /reconnect
|
|
1043
|
+
reconect: "reconnect",
|
|
1044
|
+
rc: "reconnect",
|
|
1045
|
+
// /timeout
|
|
1046
|
+
to: "timeout"
|
|
1047
|
+
};
|
|
1048
|
+
function normalizeCommandHead(head) {
|
|
1049
|
+
const lower = head.toLowerCase();
|
|
1050
|
+
return COMMAND_ALIASES[lower] ?? lower;
|
|
1051
|
+
}
|
|
1052
|
+
function parseCommand(text) {
|
|
1053
|
+
const trimmed = text.trim();
|
|
1054
|
+
if (!trimmed.startsWith("/")) return null;
|
|
1055
|
+
const rest = trimmed.slice(1);
|
|
1056
|
+
const [headRaw, ...tailParts] = rest.split(/\s+/);
|
|
1057
|
+
const tail = tailParts.join(" ").trim();
|
|
1058
|
+
const head = normalizeCommandHead(headRaw ?? "");
|
|
1059
|
+
switch (head) {
|
|
1060
|
+
case "new":
|
|
1061
|
+
case "reset":
|
|
1062
|
+
return { kind: "new" };
|
|
1063
|
+
case "cd":
|
|
1064
|
+
if (!tail) return { kind: "unknown", raw: trimmed };
|
|
1065
|
+
return { kind: "cd", path: tail };
|
|
1066
|
+
case "pwd":
|
|
1067
|
+
return { kind: "pwd" };
|
|
1068
|
+
case "status":
|
|
1069
|
+
return { kind: "status" };
|
|
1070
|
+
case "stop":
|
|
1071
|
+
return { kind: "stop" };
|
|
1072
|
+
case "reconnect":
|
|
1073
|
+
return { kind: "reconnect" };
|
|
1074
|
+
case "doctor":
|
|
1075
|
+
return { kind: "doctor", description: tail };
|
|
1076
|
+
case "config":
|
|
1077
|
+
case "cfg":
|
|
1078
|
+
case "settings":
|
|
1079
|
+
return { kind: "config", mode: "show" };
|
|
1080
|
+
case "model": {
|
|
1081
|
+
if (!tail) return { kind: "model", mode: "show" };
|
|
1082
|
+
const lower = tail.toLowerCase();
|
|
1083
|
+
if (lower === "auto" || lower === "default" || lower === "reset")
|
|
1084
|
+
return { kind: "model", mode: "reset" };
|
|
1085
|
+
if (!/^[a-zA-Z0-9._-]{1,64}$/.test(tail)) {
|
|
1086
|
+
return { kind: "unknown", raw: trimmed };
|
|
1087
|
+
}
|
|
1088
|
+
return { kind: "model", mode: "set", name: tail };
|
|
1089
|
+
}
|
|
1090
|
+
case "timeout": {
|
|
1091
|
+
if (!tail) return { kind: "timeout", mode: "show" };
|
|
1092
|
+
const lower = tail.toLowerCase();
|
|
1093
|
+
if (lower === "off" || lower === "0" || lower === "disable")
|
|
1094
|
+
return { kind: "timeout", mode: "off" };
|
|
1095
|
+
if (lower === "default" || lower === "reset") return { kind: "timeout", mode: "default" };
|
|
1096
|
+
const n = Number(lower);
|
|
1097
|
+
if (Number.isFinite(n) && n > 0 && n <= 600) {
|
|
1098
|
+
return { kind: "timeout", mode: "set", minutes: Math.floor(n) };
|
|
1099
|
+
}
|
|
1100
|
+
return { kind: "unknown", raw: trimmed };
|
|
1101
|
+
}
|
|
1102
|
+
case "help":
|
|
1103
|
+
return { kind: "help" };
|
|
1104
|
+
case "ws": {
|
|
1105
|
+
const [sub, ...nameParts] = tail.split(/\s+/);
|
|
1106
|
+
const name = nameParts.join(" ").trim();
|
|
1107
|
+
if (sub === "list" || sub === "" || sub === void 0) return { kind: "ws-list" };
|
|
1108
|
+
if (sub === "save" && name) return { kind: "ws-save", name };
|
|
1109
|
+
if (sub === "use" && name) return { kind: "ws-use", name };
|
|
1110
|
+
if ((sub === "remove" || sub === "rm" || sub === "delete") && name) {
|
|
1111
|
+
return { kind: "ws-remove", name };
|
|
1112
|
+
}
|
|
1113
|
+
return { kind: "unknown", raw: trimmed };
|
|
1114
|
+
}
|
|
1115
|
+
default:
|
|
1116
|
+
if (head && KIRO_INTERNAL_COMMANDS.has(head)) {
|
|
1117
|
+
return { kind: "kiro-internal", name: head };
|
|
1118
|
+
}
|
|
1119
|
+
return { kind: "unknown", raw: trimmed };
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// src/kiro/runner.ts
|
|
1124
|
+
import { execa as execa2 } from "execa";
|
|
1125
|
+
var log5 = () => getLogger().child({ module: "kiro-runner" });
|
|
1126
|
+
var ANSI_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\)|[ -/]*[0-9?])/g;
|
|
1127
|
+
function stripAnsi(input) {
|
|
1128
|
+
return input.replace(ANSI_REGEX, "");
|
|
1129
|
+
}
|
|
1130
|
+
var PROMPT_PREFIX_REGEX = /^>\s*/;
|
|
1131
|
+
async function runKiro(opts) {
|
|
1132
|
+
const {
|
|
1133
|
+
prompt,
|
|
1134
|
+
cwd,
|
|
1135
|
+
resumeId,
|
|
1136
|
+
binPath = "kiro-cli",
|
|
1137
|
+
trustedTools = ["fs_read", "fs_write", "grep", "glob", "code"],
|
|
1138
|
+
model,
|
|
1139
|
+
agent,
|
|
1140
|
+
timeoutMs = 10 * 60 * 1e3,
|
|
1141
|
+
idleTimeoutMs = 0,
|
|
1142
|
+
onChunk,
|
|
1143
|
+
signal
|
|
1144
|
+
} = opts;
|
|
1145
|
+
const args = ["chat", "--no-interactive"];
|
|
1146
|
+
if (resumeId) args.push("--resume-id", resumeId);
|
|
1147
|
+
if (model) args.push("--model", model);
|
|
1148
|
+
if (agent) args.push("--agent", agent);
|
|
1149
|
+
if (trustedTools.length > 0) {
|
|
1150
|
+
args.push(`--trust-tools=${trustedTools.join(",")}`);
|
|
1151
|
+
} else {
|
|
1152
|
+
args.push("--trust-tools=");
|
|
1153
|
+
}
|
|
1154
|
+
args.push(prompt);
|
|
1155
|
+
log5().info({ cwd, resumeId, args: args.slice(0, 5) }, "spawning kiro-cli");
|
|
1156
|
+
let timedOut = false;
|
|
1157
|
+
let aborted = false;
|
|
1158
|
+
let idleTimedOut = false;
|
|
1159
|
+
let textBuf = "";
|
|
1160
|
+
let lastChunkAt = Date.now();
|
|
1161
|
+
const child = execa2(binPath, args, {
|
|
1162
|
+
cwd,
|
|
1163
|
+
reject: false,
|
|
1164
|
+
stripFinalNewline: false,
|
|
1165
|
+
buffer: false,
|
|
1166
|
+
stdin: "ignore",
|
|
1167
|
+
stdout: "pipe",
|
|
1168
|
+
stderr: "pipe",
|
|
1169
|
+
detached: true,
|
|
1170
|
+
env: { ...process.env, FORCE_COLOR: "0" }
|
|
1171
|
+
// 尽量减少 ANSI(虽然没完全消除)
|
|
1172
|
+
});
|
|
1173
|
+
const killTree = (signal2) => {
|
|
1174
|
+
const pid = child.pid;
|
|
1175
|
+
if (pid === void 0) return;
|
|
1176
|
+
try {
|
|
1177
|
+
process.kill(-pid, signal2);
|
|
1178
|
+
} catch (e) {
|
|
1179
|
+
const code = e.code;
|
|
1180
|
+
if (code !== "ESRCH") {
|
|
1181
|
+
try {
|
|
1182
|
+
child.kill(signal2);
|
|
1183
|
+
} catch {
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
1188
|
+
const timeoutHandle = setTimeout(() => {
|
|
1189
|
+
timedOut = true;
|
|
1190
|
+
log5().warn({ pid: child.pid, timeoutMs }, "kiro-cli timed out, sending SIGTERM");
|
|
1191
|
+
killTree("SIGTERM");
|
|
1192
|
+
setTimeout(() => {
|
|
1193
|
+
killTree("SIGKILL");
|
|
1194
|
+
child.stdout?.destroy();
|
|
1195
|
+
}, 2e3);
|
|
1196
|
+
}, timeoutMs);
|
|
1197
|
+
let idleHandle = null;
|
|
1198
|
+
if (idleTimeoutMs > 0) {
|
|
1199
|
+
const checkInterval = Math.min(3e4, Math.max(5e3, Math.floor(idleTimeoutMs / 4)));
|
|
1200
|
+
idleHandle = setInterval(() => {
|
|
1201
|
+
if (Date.now() - lastChunkAt < idleTimeoutMs) return;
|
|
1202
|
+
idleTimedOut = true;
|
|
1203
|
+
log5().warn(
|
|
1204
|
+
{ pid: child.pid, idleTimeoutMs, sinceLastChunkMs: Date.now() - lastChunkAt },
|
|
1205
|
+
"kiro-cli idle timeout, sending SIGTERM"
|
|
1206
|
+
);
|
|
1207
|
+
killTree("SIGTERM");
|
|
1208
|
+
setTimeout(() => {
|
|
1209
|
+
killTree("SIGKILL");
|
|
1210
|
+
child.stdout?.destroy();
|
|
1211
|
+
}, 2e3);
|
|
1212
|
+
if (idleHandle) {
|
|
1213
|
+
clearInterval(idleHandle);
|
|
1214
|
+
idleHandle = null;
|
|
1215
|
+
}
|
|
1216
|
+
}, checkInterval);
|
|
1217
|
+
}
|
|
1218
|
+
const onAbort = () => {
|
|
1219
|
+
aborted = true;
|
|
1220
|
+
log5().info({ pid: child.pid }, "kiro-cli abort requested");
|
|
1221
|
+
killTree("SIGTERM");
|
|
1222
|
+
setTimeout(() => {
|
|
1223
|
+
killTree("SIGKILL");
|
|
1224
|
+
child.stdout?.destroy();
|
|
1225
|
+
}, 2e3);
|
|
1226
|
+
};
|
|
1227
|
+
if (signal) {
|
|
1228
|
+
if (signal.aborted) onAbort();
|
|
1229
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
1230
|
+
}
|
|
1231
|
+
if (child.stdout) {
|
|
1232
|
+
child.stdout.setEncoding("utf-8");
|
|
1233
|
+
child.stdout.on("data", (chunk) => {
|
|
1234
|
+
lastChunkAt = Date.now();
|
|
1235
|
+
const cleanRaw = stripAnsi(chunk).replace(PROMPT_PREFIX_REGEX, "");
|
|
1236
|
+
if (!cleanRaw) return;
|
|
1237
|
+
textBuf += cleanRaw;
|
|
1238
|
+
try {
|
|
1239
|
+
onChunk?.(cleanRaw);
|
|
1240
|
+
} catch (e) {
|
|
1241
|
+
log5().error({ err: e }, "onChunk callback threw");
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
if (child.stderr) {
|
|
1246
|
+
child.stderr.setEncoding("utf-8");
|
|
1247
|
+
child.stderr.on("data", (chunk) => {
|
|
1248
|
+
log5().debug({ stderr: chunk.slice(0, 200) }, "kiro stderr");
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
const result = await child;
|
|
1252
|
+
clearTimeout(timeoutHandle);
|
|
1253
|
+
if (idleHandle) clearInterval(idleHandle);
|
|
1254
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
1255
|
+
log5().info(
|
|
1256
|
+
{ exitCode: result.exitCode, textLen: textBuf.length, aborted, timedOut, idleTimedOut },
|
|
1257
|
+
"kiro-cli finished"
|
|
1258
|
+
);
|
|
1259
|
+
let newSessionId;
|
|
1260
|
+
if (!aborted && !timedOut && !idleTimedOut && result.exitCode === 0) {
|
|
1261
|
+
try {
|
|
1262
|
+
newSessionId = await getLatestSessionId(cwd, binPath);
|
|
1263
|
+
} catch (e) {
|
|
1264
|
+
log5().warn({ err: e }, "failed to fetch latest session id");
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
return {
|
|
1268
|
+
text: textBuf.trim(),
|
|
1269
|
+
exitCode: result.exitCode ?? null,
|
|
1270
|
+
newSessionId: newSessionId ?? resumeId,
|
|
1271
|
+
aborted,
|
|
1272
|
+
timedOut,
|
|
1273
|
+
idleTimedOut
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
async function getLatestSessionId(cwd, binPath) {
|
|
1277
|
+
const result = await execa2(binPath, ["chat", "--list-sessions"], {
|
|
1278
|
+
cwd,
|
|
1279
|
+
reject: false,
|
|
1280
|
+
timeout: 1e4,
|
|
1281
|
+
all: true
|
|
1282
|
+
});
|
|
1283
|
+
if (result.exitCode !== 0) return void 0;
|
|
1284
|
+
const combined = result.all ?? result.stdout ?? "";
|
|
1285
|
+
const text = stripAnsi(combined);
|
|
1286
|
+
const m = text.match(/Chat SessionId:\s*([0-9a-f-]{36})/i);
|
|
1287
|
+
return m?.[1];
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// src/kiro/models.ts
|
|
1291
|
+
import { execa as execa3 } from "execa";
|
|
1292
|
+
var log6 = () => getLogger().child({ module: "kiro-models" });
|
|
1293
|
+
var cache = null;
|
|
1294
|
+
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
1295
|
+
async function listModels(binPath = "kiro-cli") {
|
|
1296
|
+
if (cache && Date.now() - cache.at < CACHE_TTL_MS) return cache.data;
|
|
1297
|
+
let r;
|
|
1298
|
+
try {
|
|
1299
|
+
r = await execa3(binPath, ["chat", "--list-models", "--format", "json"], {
|
|
1300
|
+
reject: false,
|
|
1301
|
+
timeout: 1e4,
|
|
1302
|
+
all: true
|
|
1303
|
+
});
|
|
1304
|
+
} catch (e) {
|
|
1305
|
+
log6().warn({ err: e.message }, "list-models spawn failed");
|
|
1306
|
+
return void 0;
|
|
1307
|
+
}
|
|
1308
|
+
if (r.exitCode !== 0) {
|
|
1309
|
+
log6().warn(
|
|
1310
|
+
{
|
|
1311
|
+
exitCode: r.exitCode,
|
|
1312
|
+
stdoutSample: (r.stdout ?? "").slice(0, 200),
|
|
1313
|
+
stderrSample: (r.stderr ?? "").slice(0, 200),
|
|
1314
|
+
allSample: (r.all ?? "").slice(0, 200)
|
|
1315
|
+
},
|
|
1316
|
+
"list-models non-zero exit"
|
|
1317
|
+
);
|
|
1318
|
+
return void 0;
|
|
1319
|
+
}
|
|
1320
|
+
const text = stripAnsi((r.all ?? r.stdout ?? "").trim());
|
|
1321
|
+
if (!text) return void 0;
|
|
1322
|
+
let parsed;
|
|
1323
|
+
try {
|
|
1324
|
+
parsed = JSON.parse(text);
|
|
1325
|
+
} catch (e) {
|
|
1326
|
+
log6().warn(
|
|
1327
|
+
{ err: e.message, sample: text.slice(0, 200) },
|
|
1328
|
+
"list-models json parse failed"
|
|
1329
|
+
);
|
|
1330
|
+
return void 0;
|
|
1331
|
+
}
|
|
1332
|
+
if (!Array.isArray(parsed.models)) return void 0;
|
|
1333
|
+
const data = {
|
|
1334
|
+
defaultModel: parsed.default_model ?? "auto",
|
|
1335
|
+
models: parsed.models.map((m) => ({
|
|
1336
|
+
name: m.model_name,
|
|
1337
|
+
description: m.description ?? "",
|
|
1338
|
+
rateMultiplier: m.rate_multiplier ?? 1,
|
|
1339
|
+
contextWindow: m.context_window_tokens ?? 0
|
|
1340
|
+
}))
|
|
1341
|
+
};
|
|
1342
|
+
cache = { at: Date.now(), data };
|
|
1343
|
+
return data;
|
|
1344
|
+
}
|
|
1345
|
+
function clearModelCache() {
|
|
1346
|
+
cache = null;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/lib/debounce.ts
|
|
1350
|
+
var Debouncer = class {
|
|
1351
|
+
constructor(delayMs) {
|
|
1352
|
+
this.delayMs = delayMs;
|
|
1353
|
+
}
|
|
1354
|
+
delayMs;
|
|
1355
|
+
timer = null;
|
|
1356
|
+
pendingFn = null;
|
|
1357
|
+
schedule(fn) {
|
|
1358
|
+
this.pendingFn = fn;
|
|
1359
|
+
if (this.timer) return;
|
|
1360
|
+
this.timer = setTimeout(() => {
|
|
1361
|
+
this.timer = null;
|
|
1362
|
+
const f = this.pendingFn;
|
|
1363
|
+
this.pendingFn = null;
|
|
1364
|
+
if (f) {
|
|
1365
|
+
Promise.resolve(f()).catch((err) => {
|
|
1366
|
+
console.error("[debouncer] task failed:", err);
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
}, this.delayMs);
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* 立刻执行最近一次 schedule 的任务(如果有),并取消计时器。
|
|
1373
|
+
*/
|
|
1374
|
+
async flush() {
|
|
1375
|
+
if (this.timer) {
|
|
1376
|
+
clearTimeout(this.timer);
|
|
1377
|
+
this.timer = null;
|
|
1378
|
+
}
|
|
1379
|
+
const f = this.pendingFn;
|
|
1380
|
+
this.pendingFn = null;
|
|
1381
|
+
if (f) await f();
|
|
1382
|
+
}
|
|
1383
|
+
cancel() {
|
|
1384
|
+
if (this.timer) {
|
|
1385
|
+
clearTimeout(this.timer);
|
|
1386
|
+
this.timer = null;
|
|
1387
|
+
}
|
|
1388
|
+
this.pendingFn = null;
|
|
1389
|
+
}
|
|
1390
|
+
};
|
|
1391
|
+
|
|
1392
|
+
// src/card/schema.ts
|
|
1393
|
+
var HEADER_TEMPLATES = {
|
|
1394
|
+
// pending/streaming 用蓝色,做出"进行中"的视觉信号
|
|
1395
|
+
pending: { title: "\u23F3 \u601D\u8003\u4E2D", template: "blue" },
|
|
1396
|
+
streaming: { title: "\u{1F4AC} \u56DE\u590D\u4E2D", template: "blue" },
|
|
1397
|
+
// 完成态:标题极简就一个 "Kiro",靠绿色模板色和 ✅ 头像传达成功
|
|
1398
|
+
done: { title: "Kiro", template: "green" },
|
|
1399
|
+
aborted: { title: "\u5DF2\u4E2D\u6B62", template: "orange" },
|
|
1400
|
+
timedout: { title: "\u8D85\u65F6", template: "red" },
|
|
1401
|
+
error: { title: "\u51FA\u9519", template: "red" }
|
|
1402
|
+
};
|
|
1403
|
+
function buildCard(state, body, ctx, traces) {
|
|
1404
|
+
const header = HEADER_TEMPLATES[state];
|
|
1405
|
+
const elements = [];
|
|
1406
|
+
if (traces && traces.length > 0) {
|
|
1407
|
+
const isProgressing = state === "pending" || state === "streaming";
|
|
1408
|
+
elements.push({
|
|
1409
|
+
tag: "collapsible_panel",
|
|
1410
|
+
expanded: isProgressing,
|
|
1411
|
+
vertical_spacing: "small",
|
|
1412
|
+
padding: "4px 8px",
|
|
1413
|
+
header: {
|
|
1414
|
+
title: {
|
|
1415
|
+
tag: "markdown",
|
|
1416
|
+
content: `<font color='grey'>${isProgressing ? `\u2699\uFE0F \u5DE5\u5177\u8C03\u7528 \xB7 ${traces.length} \u6B65` : `\u5DE5\u5177\u8C03\u7528 \xB7 ${traces.length} \u6B65\uFF08\u70B9\u51FB\u67E5\u770B\uFF09`}</font>`
|
|
1417
|
+
},
|
|
1418
|
+
vertical_align: "center",
|
|
1419
|
+
icon: {
|
|
1420
|
+
tag: "standard_icon",
|
|
1421
|
+
token: "down-small-ccm_outlined",
|
|
1422
|
+
size: "12px 12px"
|
|
1423
|
+
},
|
|
1424
|
+
icon_position: "follow_text",
|
|
1425
|
+
icon_expanded_angle: -180
|
|
1426
|
+
},
|
|
1427
|
+
elements: traces.map((t) => ({
|
|
1428
|
+
tag: "markdown",
|
|
1429
|
+
content: `<font color='grey'>${t}</font>`
|
|
1430
|
+
}))
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
const bodyText = body.trim() || (state === "pending" || state === "streaming" ? "<font color='grey'>\u7B49\u5F85\u54CD\u5E94\u2026</font>" : "<font color='grey'>\u65E0\u8F93\u51FA</font>");
|
|
1434
|
+
elements.push({
|
|
1435
|
+
tag: "markdown",
|
|
1436
|
+
content: bodyText
|
|
1437
|
+
});
|
|
1438
|
+
if (ctx.showFooter === true) {
|
|
1439
|
+
const segs = [];
|
|
1440
|
+
if (ctx.workspaceName) segs.push(`\u{1F5C2}\uFE0F ${ctx.workspaceName}`);
|
|
1441
|
+
if (ctx.sessionStatus) segs.push(ctx.sessionStatus);
|
|
1442
|
+
if (segs.length) {
|
|
1443
|
+
elements.push({
|
|
1444
|
+
tag: "markdown",
|
|
1445
|
+
content: `<font color='grey'>${segs.join(" \xB7 ")}</font>`
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
const headerObj = {
|
|
1450
|
+
title: { tag: "plain_text", content: header.title },
|
|
1451
|
+
template: header.template
|
|
1452
|
+
};
|
|
1453
|
+
if (ctx.showFooter === true) {
|
|
1454
|
+
const subtitleSegs = [];
|
|
1455
|
+
if (ctx.workspaceName) subtitleSegs.push(`\u{1F5C2}\uFE0F ${ctx.workspaceName}`);
|
|
1456
|
+
if (ctx.sessionStatus) subtitleSegs.push(ctx.sessionStatus);
|
|
1457
|
+
const subtitle = subtitleSegs.join(" \xB7 ");
|
|
1458
|
+
if (subtitle) {
|
|
1459
|
+
headerObj["subtitle"] = { tag: "plain_text", content: subtitle };
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
return {
|
|
1463
|
+
schema: "2.0",
|
|
1464
|
+
header: headerObj,
|
|
1465
|
+
body: { elements }
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
function truncateForCard(text, maxBytes = 2e4) {
|
|
1469
|
+
const buf = Buffer.from(text, "utf-8");
|
|
1470
|
+
if (buf.byteLength <= maxBytes) return text;
|
|
1471
|
+
const cut = buf.subarray(0, maxBytes).toString("utf-8");
|
|
1472
|
+
return cut + "\n\n<font color='grey'>\u2026\u5185\u5BB9\u8D85\u51FA\u5361\u7247\u4E0A\u9650\uFF0C\u5DF2\u622A\u65AD</font>";
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// src/card/renderer.ts
|
|
1476
|
+
var CardRenderer = class {
|
|
1477
|
+
lark;
|
|
1478
|
+
chatId;
|
|
1479
|
+
replyToMessageId;
|
|
1480
|
+
debouncer;
|
|
1481
|
+
log;
|
|
1482
|
+
ctx;
|
|
1483
|
+
messageId = null;
|
|
1484
|
+
/** LLM 的真正回复文本(去掉 trace) */
|
|
1485
|
+
accText = "";
|
|
1486
|
+
/** 工具调用 trace 摘要(按出现顺序) */
|
|
1487
|
+
traces = [];
|
|
1488
|
+
currentState = "pending";
|
|
1489
|
+
closed = false;
|
|
1490
|
+
constructor(opts) {
|
|
1491
|
+
this.lark = opts.lark;
|
|
1492
|
+
this.chatId = opts.chatId;
|
|
1493
|
+
if (opts.replyToMessageId !== void 0) {
|
|
1494
|
+
this.replyToMessageId = opts.replyToMessageId;
|
|
1495
|
+
}
|
|
1496
|
+
this.debouncer = new Debouncer(opts.intervalMs);
|
|
1497
|
+
this.log = opts.logger.child({ module: "card-renderer" });
|
|
1498
|
+
this.ctx = opts.ctx;
|
|
1499
|
+
}
|
|
1500
|
+
/** 发出初始卡片。必须在 appendText 之前调用一次。 */
|
|
1501
|
+
async open(initialState = "pending", initialText = "") {
|
|
1502
|
+
this.currentState = initialState;
|
|
1503
|
+
this.accText = initialText;
|
|
1504
|
+
const card = buildCard(initialState, truncateForCard(this.accText), this.ctx, this.traces);
|
|
1505
|
+
if (this.replyToMessageId) {
|
|
1506
|
+
this.messageId = await this.lark.replyCard(this.replyToMessageId, card);
|
|
1507
|
+
} else {
|
|
1508
|
+
this.messageId = await this.lark.sendCard(this.chatId, card);
|
|
1509
|
+
}
|
|
1510
|
+
this.log.debug({ messageId: this.messageId, state: initialState }, "card opened");
|
|
1511
|
+
}
|
|
1512
|
+
/** 追加正文文本(LLM 真正的回复),触发节流更新。 */
|
|
1513
|
+
appendText(chunk) {
|
|
1514
|
+
if (this.closed) return;
|
|
1515
|
+
this.accText += chunk;
|
|
1516
|
+
if (this.currentState === "pending") {
|
|
1517
|
+
this.currentState = "streaming";
|
|
1518
|
+
}
|
|
1519
|
+
this.debouncer.schedule(async () => {
|
|
1520
|
+
await this.flush();
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
/** 追加一条工具调用 trace 摘要(如 "📖 读取 SKILL.md")。 */
|
|
1524
|
+
appendTrace(line) {
|
|
1525
|
+
if (this.closed) return;
|
|
1526
|
+
const trimmed = line.trim();
|
|
1527
|
+
if (!trimmed) return;
|
|
1528
|
+
this.traces.push(trimmed);
|
|
1529
|
+
if (this.currentState === "pending") {
|
|
1530
|
+
this.currentState = "streaming";
|
|
1531
|
+
}
|
|
1532
|
+
this.debouncer.schedule(async () => {
|
|
1533
|
+
await this.flush();
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
/** 把当前累积状态立即 patch 到飞书。 */
|
|
1537
|
+
async flush() {
|
|
1538
|
+
if (!this.messageId || this.closed) return;
|
|
1539
|
+
const card = buildCard(this.currentState, truncateForCard(this.accText), this.ctx, this.traces);
|
|
1540
|
+
try {
|
|
1541
|
+
await this.lark.patchCard(this.messageId, card);
|
|
1542
|
+
} catch (e) {
|
|
1543
|
+
this.log.warn({ err: e }, "patchCard failed; will retry on next chunk");
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* 切到终态并最后更新一次。
|
|
1548
|
+
* @param state 终态:done/aborted/timedout/error
|
|
1549
|
+
* @param finalText 如果传入,覆盖现有累积文本(比如错误信息)
|
|
1550
|
+
*/
|
|
1551
|
+
async finalize(state, finalText) {
|
|
1552
|
+
if (this.closed) return;
|
|
1553
|
+
this.closed = true;
|
|
1554
|
+
this.debouncer.cancel();
|
|
1555
|
+
if (finalText !== void 0) this.accText = finalText;
|
|
1556
|
+
this.currentState = state;
|
|
1557
|
+
if (!this.messageId) {
|
|
1558
|
+
try {
|
|
1559
|
+
await this.lark.sendText(this.chatId, this.accText.slice(0, 4e3));
|
|
1560
|
+
} catch (e) {
|
|
1561
|
+
this.log.error({ err: e }, "fallback sendText failed");
|
|
1562
|
+
}
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
const card = buildCard(state, truncateForCard(this.accText), this.ctx, this.traces);
|
|
1566
|
+
try {
|
|
1567
|
+
await this.lark.patchCard(this.messageId, card);
|
|
1568
|
+
} catch (e) {
|
|
1569
|
+
this.log.error({ err: e }, "finalize patchCard failed");
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
/** 中途切换上下文(比如 /cd 之后 cwd 变了) */
|
|
1573
|
+
updateContext(ctx) {
|
|
1574
|
+
this.ctx = { ...this.ctx, ...ctx };
|
|
1575
|
+
}
|
|
1576
|
+
};
|
|
1577
|
+
|
|
1578
|
+
// src/card/toolRender.ts
|
|
1579
|
+
import { homedir as homedir2 } from "os";
|
|
1580
|
+
var HEADER_SUMMARY_MAX = 80;
|
|
1581
|
+
var BODY_FIELD_MAX = 600;
|
|
1582
|
+
var OUTPUT_MAX = 1200;
|
|
1583
|
+
var BODY_TOTAL_MAX = 2500;
|
|
1584
|
+
function statusIcon(status) {
|
|
1585
|
+
if (status === "done") return "\u2705";
|
|
1586
|
+
if (status === "error") return "\u274C";
|
|
1587
|
+
return "\u23F3";
|
|
1588
|
+
}
|
|
1589
|
+
function toolHeaderText(tool) {
|
|
1590
|
+
const icon = statusIcon(tool.status);
|
|
1591
|
+
const summary = summarizeInput(tool.name, tool.input);
|
|
1592
|
+
return summary ? `${icon} **${tool.name}** \u2014 ${summary}` : `${icon} **${tool.name}**`;
|
|
1593
|
+
}
|
|
1594
|
+
function toolBodyMd(tool) {
|
|
1595
|
+
const parts = [];
|
|
1596
|
+
const inputMd = renderInput(tool);
|
|
1597
|
+
if (inputMd) parts.push(inputMd);
|
|
1598
|
+
if (tool.output) {
|
|
1599
|
+
const truncated = truncate(tool.output, OUTPUT_MAX);
|
|
1600
|
+
if (tool.status === "error") {
|
|
1601
|
+
parts.push(`**Error**
|
|
1602
|
+
\`\`\`
|
|
1603
|
+
${truncated}
|
|
1604
|
+
\`\`\``);
|
|
1605
|
+
} else if (tool.name === "Bash") {
|
|
1606
|
+
parts.push(`**Output**
|
|
1607
|
+
\`\`\`
|
|
1608
|
+
${truncated}
|
|
1609
|
+
\`\`\``);
|
|
1610
|
+
} else {
|
|
1611
|
+
parts.push(`**Output**
|
|
1612
|
+
\`\`\`
|
|
1613
|
+
${truncated}
|
|
1614
|
+
\`\`\``);
|
|
1615
|
+
}
|
|
1616
|
+
} else if (tool.status === "running") {
|
|
1617
|
+
parts.push("<font color='grey'>\u8FD0\u884C\u4E2D\u2026</font>");
|
|
1618
|
+
}
|
|
1619
|
+
const body = parts.join("\n\n");
|
|
1620
|
+
if (body.length <= BODY_TOTAL_MAX) return body;
|
|
1621
|
+
return `${body.slice(0, BODY_TOTAL_MAX)}\u2026
|
|
1622
|
+
|
|
1623
|
+
<font color='grey'>\uFF08body \u5DF2\u622A\u65AD\uFF0C\u5B8C\u6574\u5185\u5BB9\u67E5 \`/doctor\` \u6216\u65E5\u5FD7\uFF09</font>`;
|
|
1624
|
+
}
|
|
1625
|
+
function summarizeInput(name, input) {
|
|
1626
|
+
if (!input || typeof input !== "object") return "";
|
|
1627
|
+
const rec = input;
|
|
1628
|
+
const pick = (key, max = HEADER_SUMMARY_MAX) => {
|
|
1629
|
+
const v = rec[key];
|
|
1630
|
+
if (typeof v !== "string") return "";
|
|
1631
|
+
const oneLine = v.replace(/\s+/g, " ").trim();
|
|
1632
|
+
return oneLine.length > max ? `${oneLine.slice(0, max)}\u2026` : oneLine;
|
|
1633
|
+
};
|
|
1634
|
+
switch (name) {
|
|
1635
|
+
case "Bash":
|
|
1636
|
+
return pick("command");
|
|
1637
|
+
case "Read":
|
|
1638
|
+
case "Write":
|
|
1639
|
+
return shortenPath(pick("file_path"));
|
|
1640
|
+
case "Grep":
|
|
1641
|
+
case "Glob":
|
|
1642
|
+
return pick("pattern");
|
|
1643
|
+
case "WebFetch":
|
|
1644
|
+
return pick("url");
|
|
1645
|
+
case "WebSearch":
|
|
1646
|
+
return pick("query", 60);
|
|
1647
|
+
default:
|
|
1648
|
+
return pick("command") || pick("file_path") || pick("pattern") || pick("query");
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
function renderInput(tool) {
|
|
1652
|
+
const input = tool.input;
|
|
1653
|
+
if (!input || typeof input !== "object") return "";
|
|
1654
|
+
const rec = input;
|
|
1655
|
+
const str = (k) => typeof rec[k] === "string" ? rec[k] : "";
|
|
1656
|
+
switch (tool.name) {
|
|
1657
|
+
case "Bash": {
|
|
1658
|
+
const cmd = str("command");
|
|
1659
|
+
return cmd ? `**Command**
|
|
1660
|
+
\`\`\`bash
|
|
1661
|
+
${truncate(cmd, BODY_FIELD_MAX)}
|
|
1662
|
+
\`\`\`` : "";
|
|
1663
|
+
}
|
|
1664
|
+
case "Read":
|
|
1665
|
+
case "Write": {
|
|
1666
|
+
const fp = str("file_path");
|
|
1667
|
+
const range = str("range");
|
|
1668
|
+
const lines = [];
|
|
1669
|
+
if (fp) lines.push(`**File** \`${shortenPath(fp)}\``);
|
|
1670
|
+
if (range) lines.push(`**Range** ${range}`);
|
|
1671
|
+
return lines.join("\n");
|
|
1672
|
+
}
|
|
1673
|
+
case "Grep":
|
|
1674
|
+
case "Glob": {
|
|
1675
|
+
const lines = [];
|
|
1676
|
+
if (str("pattern")) lines.push(`**Pattern** \`${str("pattern")}\``);
|
|
1677
|
+
if (str("path")) lines.push(`**Path** \`${shortenPath(str("path"))}\``);
|
|
1678
|
+
return lines.join("\n");
|
|
1679
|
+
}
|
|
1680
|
+
case "WebFetch":
|
|
1681
|
+
return str("url") ? `**URL** ${str("url")}` : "";
|
|
1682
|
+
case "WebSearch":
|
|
1683
|
+
return str("query") ? `**Query** \`${truncate(str("query"), BODY_FIELD_MAX)}\`` : "";
|
|
1684
|
+
default:
|
|
1685
|
+
return "";
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
function shortenPath(p) {
|
|
1689
|
+
if (!p) return p;
|
|
1690
|
+
const home = homedir2();
|
|
1691
|
+
if (home && p.startsWith(home)) return `~${p.slice(home.length)}`;
|
|
1692
|
+
return p;
|
|
1693
|
+
}
|
|
1694
|
+
function truncate(s, max) {
|
|
1695
|
+
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// src/card/runRenderer.ts
|
|
1699
|
+
var REASONING_MAX = 1500;
|
|
1700
|
+
var COLLAPSE_TOOL_THRESHOLD = 3;
|
|
1701
|
+
function renderRunCard(state) {
|
|
1702
|
+
const elements = [];
|
|
1703
|
+
if (state.reasoning.content) {
|
|
1704
|
+
elements.push(reasoningPanel(state.reasoning.content, state.reasoning.active));
|
|
1705
|
+
}
|
|
1706
|
+
for (const group of groupBlocks(state.blocks)) {
|
|
1707
|
+
if (group.kind === "text") {
|
|
1708
|
+
if (group.content.trim()) {
|
|
1709
|
+
elements.push(markdown(group.content));
|
|
1710
|
+
}
|
|
1711
|
+
} else {
|
|
1712
|
+
elements.push(...renderToolGroup(group.tools, state.terminal !== "running"));
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
if (state.terminal === "interrupted") {
|
|
1716
|
+
elements.push(noteMd("<font color='grey'>\u23F9 \u5DF2\u88AB\u4E2D\u65AD</font>"));
|
|
1717
|
+
} else if (state.terminal === "idle_timeout") {
|
|
1718
|
+
const mins = state.idleTimeoutMinutes ?? 0;
|
|
1719
|
+
elements.push(noteMd(`<font color='grey'>\u23F1 ${mins} \u5206\u949F\u65E0\u54CD\u5E94\uFF0C\u5DF2\u81EA\u52A8\u7EC8\u6B62</font>`));
|
|
1720
|
+
} else if (state.terminal === "error" && state.errorMsg) {
|
|
1721
|
+
elements.push(noteMd(`<font color='red'>\u26A0\uFE0F Kiro \u5931\u8D25\uFF1A${escapeMd(state.errorMsg)}</font>`));
|
|
1722
|
+
} else if (state.terminal === "done" && elements.length === 0) {
|
|
1723
|
+
elements.push(noteMd("<font color='grey'>\uFF08\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09</font>"));
|
|
1724
|
+
}
|
|
1725
|
+
if (state.terminal === "running") {
|
|
1726
|
+
if (state.footer) elements.push(footerStatus(state.footer));
|
|
1727
|
+
elements.push(stopButton());
|
|
1728
|
+
}
|
|
1729
|
+
return {
|
|
1730
|
+
schema: "2.0",
|
|
1731
|
+
config: {
|
|
1732
|
+
streaming_mode: state.terminal === "running",
|
|
1733
|
+
summary: { content: summaryText(state) }
|
|
1734
|
+
},
|
|
1735
|
+
header: cardHeader(state),
|
|
1736
|
+
body: { elements }
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
function cardHeader(state) {
|
|
1740
|
+
const { title, template } = headerOf(state.terminal);
|
|
1741
|
+
return {
|
|
1742
|
+
title: { tag: "plain_text", content: title },
|
|
1743
|
+
template
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
function headerOf(terminal) {
|
|
1747
|
+
switch (terminal) {
|
|
1748
|
+
case "running":
|
|
1749
|
+
return { title: "\u{1F4AC} Kiro", template: "blue" };
|
|
1750
|
+
case "done":
|
|
1751
|
+
return { title: "\u2705 Kiro", template: "green" };
|
|
1752
|
+
case "error":
|
|
1753
|
+
return { title: "\u274C Kiro \u51FA\u9519", template: "red" };
|
|
1754
|
+
case "interrupted":
|
|
1755
|
+
return { title: "\u23F9 \u5DF2\u4E2D\u6B62", template: "orange" };
|
|
1756
|
+
case "idle_timeout":
|
|
1757
|
+
return { title: "\u23F1 \u8D85\u65F6", template: "red" };
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
function* groupBlocks(blocks) {
|
|
1761
|
+
let toolBuf = [];
|
|
1762
|
+
for (const b of blocks) {
|
|
1763
|
+
if (b.kind === "tool") {
|
|
1764
|
+
toolBuf.push(b.tool);
|
|
1765
|
+
} else {
|
|
1766
|
+
if (toolBuf.length > 0) {
|
|
1767
|
+
yield { kind: "tools", tools: toolBuf };
|
|
1768
|
+
toolBuf = [];
|
|
1769
|
+
}
|
|
1770
|
+
yield { kind: "text", content: b.content };
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
if (toolBuf.length > 0) yield { kind: "tools", tools: toolBuf };
|
|
1774
|
+
}
|
|
1775
|
+
function renderToolGroup(tools, finalized) {
|
|
1776
|
+
if (tools.length === 0) return [];
|
|
1777
|
+
if (tools.length < COLLAPSE_TOOL_THRESHOLD) {
|
|
1778
|
+
return tools.map((t) => toolPanel(t, false));
|
|
1779
|
+
}
|
|
1780
|
+
if (finalized) {
|
|
1781
|
+
return [collapsedToolSummary(tools, true)];
|
|
1782
|
+
}
|
|
1783
|
+
const prior = tools.slice(0, -1);
|
|
1784
|
+
const latest = tools[tools.length - 1];
|
|
1785
|
+
const out = [];
|
|
1786
|
+
if (prior.length > 0) out.push(collapsedToolSummary(prior, false));
|
|
1787
|
+
if (latest) out.push(toolPanel(latest, true));
|
|
1788
|
+
return out;
|
|
1789
|
+
}
|
|
1790
|
+
function reasoningPanel(content, active) {
|
|
1791
|
+
const title = active ? "\u{1F9E0} **\u601D\u8003\u4E2D**" : "\u{1F9E0} **\u601D\u8003\u5B8C\u6210\uFF0C\u70B9\u51FB\u67E5\u770B**";
|
|
1792
|
+
return collapsiblePanel({
|
|
1793
|
+
title,
|
|
1794
|
+
expanded: active,
|
|
1795
|
+
border: "grey",
|
|
1796
|
+
body: truncate2(content, REASONING_MAX)
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
function toolPanel(tool, expanded) {
|
|
1800
|
+
return collapsiblePanel({
|
|
1801
|
+
title: toolHeaderText(tool),
|
|
1802
|
+
expanded,
|
|
1803
|
+
border: tool.status === "error" ? "red" : "grey",
|
|
1804
|
+
body: toolBodyMd(tool) || "<font color='grey'>\u65E0\u8F93\u51FA</font>"
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
function collapsedToolSummary(tools, finalized) {
|
|
1808
|
+
const suffix = finalized ? "\uFF08\u5DF2\u7ED3\u675F\uFF09" : "";
|
|
1809
|
+
const title = `\u2615 **${tools.length} \u4E2A\u5DE5\u5177\u8C03\u7528${suffix}**`;
|
|
1810
|
+
const headerList = tools.map((t) => `- ${toolHeaderText(t)}`).join("\n");
|
|
1811
|
+
return {
|
|
1812
|
+
tag: "collapsible_panel",
|
|
1813
|
+
expanded: false,
|
|
1814
|
+
header: panelHeader(title),
|
|
1815
|
+
border: { color: "blue", corner_radius: "5px" },
|
|
1816
|
+
vertical_spacing: "8px",
|
|
1817
|
+
padding: "8px 8px 8px 8px",
|
|
1818
|
+
elements: [{ tag: "markdown", content: headerList, text_size: "notation" }]
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
function collapsiblePanel(opts) {
|
|
1822
|
+
return {
|
|
1823
|
+
tag: "collapsible_panel",
|
|
1824
|
+
expanded: opts.expanded,
|
|
1825
|
+
header: panelHeader(opts.title),
|
|
1826
|
+
border: { color: opts.border, corner_radius: "5px" },
|
|
1827
|
+
vertical_spacing: "8px",
|
|
1828
|
+
padding: "8px 8px 8px 8px",
|
|
1829
|
+
elements: [{ tag: "markdown", content: opts.body, text_size: "notation" }]
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
function panelHeader(titleMd) {
|
|
1833
|
+
return {
|
|
1834
|
+
title: { tag: "markdown", content: titleMd },
|
|
1835
|
+
vertical_align: "center",
|
|
1836
|
+
icon: { tag: "standard_icon", token: "down-small-ccm_outlined", size: "16px 16px" },
|
|
1837
|
+
icon_position: "follow_text",
|
|
1838
|
+
icon_expanded_angle: -180
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
function markdown(content) {
|
|
1842
|
+
return { tag: "markdown", content };
|
|
1843
|
+
}
|
|
1844
|
+
function noteMd(content) {
|
|
1845
|
+
return { tag: "markdown", content, text_size: "notation" };
|
|
1846
|
+
}
|
|
1847
|
+
function stopButton() {
|
|
1848
|
+
return {
|
|
1849
|
+
tag: "button",
|
|
1850
|
+
text: { tag: "plain_text", content: "\u23F9 \u7EC8\u6B62" },
|
|
1851
|
+
type: "danger",
|
|
1852
|
+
size: "small",
|
|
1853
|
+
behaviors: [{ type: "callback", value: { action: "session.stop" } }]
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
function footerStatus(status) {
|
|
1857
|
+
const text = status === "thinking" ? "<font color='grey'>\u{1F9E0} \u6B63\u5728\u601D\u8003</font>" : status === "tool_running" ? "<font color='grey'>\u{1F9F0} \u6B63\u5728\u8C03\u7528\u5DE5\u5177</font>" : "<font color='grey'>\u270D\uFE0F \u6B63\u5728\u8F93\u51FA</font>";
|
|
1858
|
+
return noteMd(text);
|
|
1859
|
+
}
|
|
1860
|
+
function summaryText(state) {
|
|
1861
|
+
if (state.terminal === "interrupted") return "\u5DF2\u4E2D\u65AD";
|
|
1862
|
+
if (state.terminal === "idle_timeout") return "\u5DF2\u8D85\u65F6";
|
|
1863
|
+
if (state.terminal === "error") return "\u51FA\u9519";
|
|
1864
|
+
if (state.terminal === "done") return "\u5DF2\u5B8C\u6210";
|
|
1865
|
+
if (state.footer === "tool_running") return "\u6B63\u5728\u8C03\u7528\u5DE5\u5177";
|
|
1866
|
+
if (state.footer === "streaming") return "\u6B63\u5728\u8F93\u51FA";
|
|
1867
|
+
return "\u601D\u8003\u4E2D";
|
|
1868
|
+
}
|
|
1869
|
+
function truncate2(s, max) {
|
|
1870
|
+
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
1871
|
+
}
|
|
1872
|
+
function escapeMd(s) {
|
|
1873
|
+
return s.replace(/([*_`\\])/g, "\\$1");
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// src/kiro/runState.ts
|
|
1877
|
+
function createInitialState(idleTimeoutMinutes) {
|
|
1878
|
+
const state = {
|
|
1879
|
+
blocks: [],
|
|
1880
|
+
reasoning: { content: "", active: false },
|
|
1881
|
+
terminal: "running",
|
|
1882
|
+
footer: "thinking"
|
|
1883
|
+
};
|
|
1884
|
+
if (idleTimeoutMinutes !== void 0) {
|
|
1885
|
+
state.idleTimeoutMinutes = idleTimeoutMinutes;
|
|
1886
|
+
}
|
|
1887
|
+
return state;
|
|
1888
|
+
}
|
|
1889
|
+
function findLastRunningTool(state) {
|
|
1890
|
+
for (let i = state.blocks.length - 1; i >= 0; i--) {
|
|
1891
|
+
const b = state.blocks[i];
|
|
1892
|
+
if (b && b.kind === "tool" && b.tool.status === "running") return b.tool;
|
|
1893
|
+
}
|
|
1894
|
+
return void 0;
|
|
1895
|
+
}
|
|
1896
|
+
function pushTool(state, tool) {
|
|
1897
|
+
state.blocks.push({ kind: "tool", tool });
|
|
1898
|
+
state.footer = "tool_running";
|
|
1899
|
+
}
|
|
1900
|
+
function appendText(state, content) {
|
|
1901
|
+
if (!content) return;
|
|
1902
|
+
const last = state.blocks[state.blocks.length - 1];
|
|
1903
|
+
if (last && last.kind === "text") {
|
|
1904
|
+
last.content += content;
|
|
1905
|
+
} else {
|
|
1906
|
+
state.blocks.push({ kind: "text", content });
|
|
1907
|
+
}
|
|
1908
|
+
state.footer = "streaming";
|
|
1909
|
+
}
|
|
1910
|
+
function finishLastRunningTool(state, status, output) {
|
|
1911
|
+
const tool = findLastRunningTool(state);
|
|
1912
|
+
if (!tool) return;
|
|
1913
|
+
tool.status = status;
|
|
1914
|
+
tool.finishedAt = Date.now();
|
|
1915
|
+
if (output !== void 0) tool.output = output;
|
|
1916
|
+
}
|
|
1917
|
+
function appendToolOutput(state, chunk) {
|
|
1918
|
+
if (!chunk) return;
|
|
1919
|
+
const tool = findLastRunningTool(state);
|
|
1920
|
+
if (!tool) return;
|
|
1921
|
+
tool.output = (tool.output ?? "") + chunk;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// src/kiro/runStreamParser.ts
|
|
1925
|
+
function detectToolStart(line) {
|
|
1926
|
+
const t = line.trim();
|
|
1927
|
+
let m = t.match(/^Reading file:\s*([^,(]+?)\s*(?:,([^()]+))?\s*\(using tool:/);
|
|
1928
|
+
if (m) {
|
|
1929
|
+
const input = { file_path: (m[1] ?? "").trim() };
|
|
1930
|
+
const range = (m[2] ?? "").trim();
|
|
1931
|
+
if (range) input["range"] = range;
|
|
1932
|
+
return { name: "Read", input };
|
|
1933
|
+
}
|
|
1934
|
+
m = t.match(/^Writing file:\s*([^,(]+?)\s*\(using tool:/);
|
|
1935
|
+
if (m) {
|
|
1936
|
+
return { name: "Write", input: { file_path: (m[1] ?? "").trim() } };
|
|
1937
|
+
}
|
|
1938
|
+
m = t.match(/^I will run the following command:\s*(.+?)\s*\(using tool:/);
|
|
1939
|
+
if (m) {
|
|
1940
|
+
return { name: "Bash", input: { command: (m[1] ?? "").trim() } };
|
|
1941
|
+
}
|
|
1942
|
+
m = t.match(/^Searching for\s*[`"']?([^`"'\n]+?)[`"']?\s*\(using tool:\s*(grep|glob)/);
|
|
1943
|
+
if (m) {
|
|
1944
|
+
const tool = m[2] === "glob" ? "Glob" : "Grep";
|
|
1945
|
+
return { name: tool, input: { pattern: (m[1] ?? "").trim() } };
|
|
1946
|
+
}
|
|
1947
|
+
m = t.match(/^(?:Fetching|Browsing)\s+(.+?)\s*\(using tool:\s*web_fetch/);
|
|
1948
|
+
if (m) {
|
|
1949
|
+
return { name: "WebFetch", input: { url: (m[1] ?? "").trim() } };
|
|
1950
|
+
}
|
|
1951
|
+
m = t.match(/^Searching the web for\s+(.+?)\s*\(using tool:\s*web_search/);
|
|
1952
|
+
if (m) {
|
|
1953
|
+
return { name: "WebSearch", input: { query: (m[1] ?? "").trim() } };
|
|
1954
|
+
}
|
|
1955
|
+
m = t.match(/\(using tool:\s*([a-z_]+)\)/);
|
|
1956
|
+
if (m) {
|
|
1957
|
+
return {
|
|
1958
|
+
name: prettifyToolName(m[1] ?? "tool"),
|
|
1959
|
+
input: { raw_line: t }
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
return null;
|
|
1963
|
+
}
|
|
1964
|
+
function prettifyToolName(name) {
|
|
1965
|
+
switch (name) {
|
|
1966
|
+
case "fs_read":
|
|
1967
|
+
case "read":
|
|
1968
|
+
return "Read";
|
|
1969
|
+
case "fs_write":
|
|
1970
|
+
case "write":
|
|
1971
|
+
return "Write";
|
|
1972
|
+
case "execute_bash":
|
|
1973
|
+
case "shell":
|
|
1974
|
+
return "Bash";
|
|
1975
|
+
case "grep":
|
|
1976
|
+
return "Grep";
|
|
1977
|
+
case "glob":
|
|
1978
|
+
return "Glob";
|
|
1979
|
+
case "web_search":
|
|
1980
|
+
return "WebSearch";
|
|
1981
|
+
case "web_fetch":
|
|
1982
|
+
return "WebFetch";
|
|
1983
|
+
case "use_aws":
|
|
1984
|
+
return "AWS";
|
|
1985
|
+
case "use_subagent":
|
|
1986
|
+
return "Subagent";
|
|
1987
|
+
case "code":
|
|
1988
|
+
return "Code";
|
|
1989
|
+
default:
|
|
1990
|
+
return name.split("_").map((s) => s ? s[0].toUpperCase() + s.slice(1) : "").join("");
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
function isSilent(t) {
|
|
1994
|
+
return /^✓ Successfully /.test(t) || /^[▸▶]\s*Credits:/.test(t) || /^Purpose:/.test(t) || /^WARNING:/.test(t);
|
|
1995
|
+
}
|
|
1996
|
+
function createRunStreamParser() {
|
|
1997
|
+
let buf = "";
|
|
1998
|
+
let parserState = "normal";
|
|
1999
|
+
let toolIdCounter = 0;
|
|
2000
|
+
const newToolId = () => `t${Date.now().toString(36)}-${toolIdCounter++}`;
|
|
2001
|
+
const processLine = (rawLine, state) => {
|
|
2002
|
+
const line = rawLine;
|
|
2003
|
+
const t = line.trim();
|
|
2004
|
+
if (/^- Completed in /.test(t)) {
|
|
2005
|
+
finishLastRunningTool(state, "done");
|
|
2006
|
+
parserState = "normal";
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
if (isSilent(t)) return;
|
|
2010
|
+
if (parserState === "in-tool") {
|
|
2011
|
+
appendToolOutput(state, line + "\n");
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
const toolStart = detectToolStart(line);
|
|
2015
|
+
if (toolStart) {
|
|
2016
|
+
const tool = {
|
|
2017
|
+
id: newToolId(),
|
|
2018
|
+
name: toolStart.name,
|
|
2019
|
+
input: toolStart.input,
|
|
2020
|
+
status: "running",
|
|
2021
|
+
startedAt: Date.now()
|
|
2022
|
+
};
|
|
2023
|
+
pushTool(state, tool);
|
|
2024
|
+
parserState = "in-tool";
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
if (t.startsWith("> ")) {
|
|
2028
|
+
appendText(state, t.slice(2) + "\n");
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
if (t === ">") {
|
|
2032
|
+
appendText(state, "\n");
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
if (line.length > 0 || state.blocks[state.blocks.length - 1]?.kind === "text") {
|
|
2036
|
+
appendText(state, line + "\n");
|
|
2037
|
+
}
|
|
2038
|
+
};
|
|
2039
|
+
return {
|
|
2040
|
+
feed(chunk, state) {
|
|
2041
|
+
buf += chunk;
|
|
2042
|
+
const lines = buf.split("\n");
|
|
2043
|
+
buf = lines.pop() ?? "";
|
|
2044
|
+
for (const line of lines) {
|
|
2045
|
+
processLine(line, state);
|
|
2046
|
+
}
|
|
2047
|
+
},
|
|
2048
|
+
flush(state) {
|
|
2049
|
+
if (!buf) return;
|
|
2050
|
+
processLine(buf, state);
|
|
2051
|
+
buf = "";
|
|
2052
|
+
}
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// src/card/runCardController.ts
|
|
2057
|
+
var RunCardController = class {
|
|
2058
|
+
lark;
|
|
2059
|
+
chatId;
|
|
2060
|
+
replyToMessageId;
|
|
2061
|
+
debouncer;
|
|
2062
|
+
log;
|
|
2063
|
+
parser;
|
|
2064
|
+
messageId = null;
|
|
2065
|
+
state;
|
|
2066
|
+
closed = false;
|
|
2067
|
+
constructor(opts) {
|
|
2068
|
+
this.lark = opts.lark;
|
|
2069
|
+
this.chatId = opts.chatId;
|
|
2070
|
+
if (opts.replyToMessageId !== void 0) {
|
|
2071
|
+
this.replyToMessageId = opts.replyToMessageId;
|
|
2072
|
+
}
|
|
2073
|
+
this.debouncer = new Debouncer(opts.intervalMs);
|
|
2074
|
+
this.log = opts.logger.child({ module: "run-card" });
|
|
2075
|
+
this.parser = createRunStreamParser();
|
|
2076
|
+
this.state = createInitialState(opts.idleTimeoutMinutes);
|
|
2077
|
+
}
|
|
2078
|
+
/** 发送初始卡片。必须在 feed 之前调用一次。 */
|
|
2079
|
+
async open() {
|
|
2080
|
+
const card = renderRunCard(this.state);
|
|
2081
|
+
if (this.replyToMessageId) {
|
|
2082
|
+
this.messageId = await this.lark.replyCard(this.replyToMessageId, card);
|
|
2083
|
+
} else {
|
|
2084
|
+
this.messageId = await this.lark.sendCard(this.chatId, card);
|
|
2085
|
+
}
|
|
2086
|
+
this.log.debug({ messageId: this.messageId }, "run card opened");
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* 喂入一段 stdout chunk(已 stripAnsi)。
|
|
2090
|
+
* 内部经过 parser 更新 state,节流后 patchCard。
|
|
2091
|
+
*/
|
|
2092
|
+
feed(chunk) {
|
|
2093
|
+
if (this.closed) return;
|
|
2094
|
+
this.parser.feed(chunk, this.state);
|
|
2095
|
+
this.scheduleFlush();
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* 把卡片状态切到终态并立刻刷新。
|
|
2099
|
+
*
|
|
2100
|
+
* @param terminal 终态:done/error/interrupted/idle_timeout
|
|
2101
|
+
* @param errorMsg 仅在 terminal=error 时使用
|
|
2102
|
+
*/
|
|
2103
|
+
async finalize(terminal, errorMsg) {
|
|
2104
|
+
if (this.closed) return;
|
|
2105
|
+
this.closed = true;
|
|
2106
|
+
this.debouncer.cancel();
|
|
2107
|
+
this.parser.flush(this.state);
|
|
2108
|
+
this.state.terminal = terminal;
|
|
2109
|
+
this.state.footer = null;
|
|
2110
|
+
if (errorMsg !== void 0) this.state.errorMsg = errorMsg;
|
|
2111
|
+
if (!this.messageId) {
|
|
2112
|
+
try {
|
|
2113
|
+
const text = this.fallbackText();
|
|
2114
|
+
await this.lark.sendText(this.chatId, text.slice(0, 4e3));
|
|
2115
|
+
} catch (e) {
|
|
2116
|
+
this.log.error({ err: e }, "fallback sendText failed");
|
|
2117
|
+
}
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
2120
|
+
const card = renderRunCard(this.state);
|
|
2121
|
+
try {
|
|
2122
|
+
await this.lark.patchCard(this.messageId, card);
|
|
2123
|
+
} catch (e) {
|
|
2124
|
+
this.log.error({ err: e }, "finalize patchCard failed");
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
/** 当前是否还在跑(外部判断要不要 flush) */
|
|
2128
|
+
isClosed() {
|
|
2129
|
+
return this.closed;
|
|
2130
|
+
}
|
|
2131
|
+
// ----- 内部 -----
|
|
2132
|
+
scheduleFlush() {
|
|
2133
|
+
this.debouncer.schedule(async () => {
|
|
2134
|
+
await this.flush();
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
async flush() {
|
|
2138
|
+
if (!this.messageId || this.closed) return;
|
|
2139
|
+
const card = renderRunCard(this.state);
|
|
2140
|
+
try {
|
|
2141
|
+
await this.lark.patchCard(this.messageId, card);
|
|
2142
|
+
} catch (e) {
|
|
2143
|
+
this.log.warn({ err: e }, "patchCard failed; will retry next chunk");
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
/** finalize 但 messageId 没发出来时的兜底纯文本 */
|
|
2147
|
+
fallbackText() {
|
|
2148
|
+
const parts = [];
|
|
2149
|
+
for (const b of this.state.blocks) {
|
|
2150
|
+
if (b.kind === "text") parts.push(b.content);
|
|
2151
|
+
}
|
|
2152
|
+
if (this.state.errorMsg) parts.push(`
|
|
2153
|
+
[\u9519\u8BEF] ${this.state.errorMsg}`);
|
|
2154
|
+
return parts.join("") || "\uFF08\u65E0\u56DE\u590D\uFF09";
|
|
2155
|
+
}
|
|
2156
|
+
};
|
|
2157
|
+
|
|
2158
|
+
// src/card/builders.ts
|
|
2159
|
+
function buildHeader(h) {
|
|
2160
|
+
const out = {
|
|
2161
|
+
title: { tag: "plain_text", content: h.title },
|
|
2162
|
+
template: h.template
|
|
2163
|
+
};
|
|
2164
|
+
if (h.subtitle) out["subtitle"] = { tag: "plain_text", content: h.subtitle };
|
|
2165
|
+
return out;
|
|
2166
|
+
}
|
|
2167
|
+
function btn(opts) {
|
|
2168
|
+
const out = {
|
|
2169
|
+
tag: "button",
|
|
2170
|
+
text: { tag: "plain_text", content: opts.text },
|
|
2171
|
+
type: opts.type ?? "default",
|
|
2172
|
+
size: opts.size ?? "small",
|
|
2173
|
+
width: opts.width ?? "default",
|
|
2174
|
+
behaviors: [{ type: "callback", value: opts.value }]
|
|
2175
|
+
};
|
|
2176
|
+
if (opts.hoverTip) {
|
|
2177
|
+
out["hover_tips"] = { tag: "plain_text", content: opts.hoverTip };
|
|
2178
|
+
}
|
|
2179
|
+
if (opts.confirm) {
|
|
2180
|
+
out["confirm"] = {
|
|
2181
|
+
title: { tag: "plain_text", content: opts.confirm.title },
|
|
2182
|
+
text: { tag: "plain_text", content: opts.confirm.text }
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
return out;
|
|
2186
|
+
}
|
|
2187
|
+
function md(content, textAlign = "left") {
|
|
2188
|
+
return { tag: "markdown", content, text_align: textAlign };
|
|
2189
|
+
}
|
|
2190
|
+
function hr() {
|
|
2191
|
+
return { tag: "hr" };
|
|
2192
|
+
}
|
|
2193
|
+
function column(opts) {
|
|
2194
|
+
return {
|
|
2195
|
+
tag: "column",
|
|
2196
|
+
width: opts.width ?? "weighted",
|
|
2197
|
+
weight: opts.weight ?? 1,
|
|
2198
|
+
vertical_align: opts.vAlign ?? "center",
|
|
2199
|
+
elements: opts.elements
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
function columnSet(opts) {
|
|
2203
|
+
return {
|
|
2204
|
+
tag: "column_set",
|
|
2205
|
+
flex_mode: opts.flexMode ?? "none",
|
|
2206
|
+
background_style: opts.background ?? "default",
|
|
2207
|
+
horizontal_spacing: opts.horizontalSpacing ?? "default",
|
|
2208
|
+
columns: opts.columns
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
function buildModelPickerCard(opts) {
|
|
2212
|
+
const { current, list } = opts;
|
|
2213
|
+
const groups = groupModels(list.models);
|
|
2214
|
+
const elements = [];
|
|
2215
|
+
const renderModelRow = (m) => {
|
|
2216
|
+
const isCurrent = m.name === current;
|
|
2217
|
+
const ctxLabel = m.contextWindow >= 1e6 ? "1M" : m.contextWindow >= 1e3 ? `${Math.round(m.contextWindow / 1e3)}K` : `${m.contextWindow}`;
|
|
2218
|
+
const rateLabel = `${m.rateMultiplier}\xD7`;
|
|
2219
|
+
const shortName = m.name.replace(/^claude-/, "");
|
|
2220
|
+
const namePart = isCurrent ? `**\u2713 ${shortName}**` : `\u3000${shortName}`;
|
|
2221
|
+
return columnSet({
|
|
2222
|
+
flexMode: "none",
|
|
2223
|
+
horizontalSpacing: "small",
|
|
2224
|
+
columns: [
|
|
2225
|
+
column({ weight: 5, elements: [md(namePart)] }),
|
|
2226
|
+
column({
|
|
2227
|
+
weight: 3,
|
|
2228
|
+
elements: [md(`<font color='grey'>${rateLabel} \xB7 ${ctxLabel}</font>`, "right")]
|
|
2229
|
+
}),
|
|
2230
|
+
column({
|
|
2231
|
+
weight: 2,
|
|
2232
|
+
vAlign: "center",
|
|
2233
|
+
elements: [
|
|
2234
|
+
isCurrent ? md(`<font color='green'>**\u5F53\u524D**</font>`, "right") : btn({
|
|
2235
|
+
text: "\u9009\u7528",
|
|
2236
|
+
type: "primary",
|
|
2237
|
+
size: "tiny",
|
|
2238
|
+
value: { action: "model.set", name: m.name },
|
|
2239
|
+
hoverTip: m.description || m.name
|
|
2240
|
+
})
|
|
2241
|
+
]
|
|
2242
|
+
})
|
|
2243
|
+
]
|
|
2244
|
+
});
|
|
2245
|
+
};
|
|
2246
|
+
for (const m of groups.recommended) {
|
|
2247
|
+
elements.push(renderModelRow(m));
|
|
2248
|
+
}
|
|
2249
|
+
const otherModels = [...groups.experimental, ...groups.legacy];
|
|
2250
|
+
if (otherModels.length > 0) {
|
|
2251
|
+
elements.push({
|
|
2252
|
+
tag: "collapsible_panel",
|
|
2253
|
+
expanded: false,
|
|
2254
|
+
vertical_spacing: "small",
|
|
2255
|
+
padding: "4px 8px",
|
|
2256
|
+
header: {
|
|
2257
|
+
title: {
|
|
2258
|
+
tag: "markdown",
|
|
2259
|
+
content: `<font color='grey'>\u5C55\u5F00\u5176\u4ED6 ${otherModels.length} \u4E2A\u6A21\u578B\uFF08\u5B9E\u9A8C\u6027 / \u7B2C\u4E09\u65B9 / \u65E7\u7248\uFF09</font>`
|
|
2260
|
+
},
|
|
2261
|
+
vertical_align: "center",
|
|
2262
|
+
icon: {
|
|
2263
|
+
tag: "standard_icon",
|
|
2264
|
+
token: "down-small-ccm_outlined",
|
|
2265
|
+
size: "14px 14px"
|
|
2266
|
+
},
|
|
2267
|
+
icon_position: "follow_text",
|
|
2268
|
+
icon_expanded_angle: -180
|
|
2269
|
+
},
|
|
2270
|
+
elements: otherModels.map((m) => renderModelRow(m))
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
elements.push(
|
|
2274
|
+
columnSet({
|
|
2275
|
+
flexMode: "flow",
|
|
2276
|
+
horizontalSpacing: "small",
|
|
2277
|
+
columns: [
|
|
2278
|
+
column({
|
|
2279
|
+
width: "auto",
|
|
2280
|
+
elements: [
|
|
2281
|
+
btn({
|
|
2282
|
+
text: "\u21BA \u6062\u590D\u9ED8\u8BA4",
|
|
2283
|
+
type: "default",
|
|
2284
|
+
size: "tiny",
|
|
2285
|
+
value: { action: "model.reset" },
|
|
2286
|
+
hoverTip: "\u6E05\u9664\u6A21\u578B\u8986\u76D6\uFF0C\u56DE\u5F52 kiro-cli \u9ED8\u8BA4\uFF08auto\uFF09"
|
|
2287
|
+
})
|
|
2288
|
+
]
|
|
2289
|
+
}),
|
|
2290
|
+
column({
|
|
2291
|
+
width: "auto",
|
|
2292
|
+
elements: [
|
|
2293
|
+
btn({
|
|
2294
|
+
text: "\u{1F504} \u5237\u65B0",
|
|
2295
|
+
type: "default",
|
|
2296
|
+
size: "tiny",
|
|
2297
|
+
value: { action: "model.refresh" },
|
|
2298
|
+
hoverTip: "\u6E05\u7F13\u5B58\u91CD\u65B0\u67E5\u8BE2\u6A21\u578B\u5217\u8868"
|
|
2299
|
+
})
|
|
2300
|
+
]
|
|
2301
|
+
})
|
|
2302
|
+
]
|
|
2303
|
+
})
|
|
2304
|
+
);
|
|
2305
|
+
return {
|
|
2306
|
+
schema: "2.0",
|
|
2307
|
+
header: buildHeader({
|
|
2308
|
+
title: "\u{1F39B}\uFE0F \u9009\u62E9\u6A21\u578B",
|
|
2309
|
+
template: "blue",
|
|
2310
|
+
subtitle: `\u5F53\u524D \xB7 ${current.replace(/^claude-/, "")}`
|
|
2311
|
+
}),
|
|
2312
|
+
body: { elements }
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
function groupModels(models) {
|
|
2316
|
+
const recommended = [];
|
|
2317
|
+
const experimental = [];
|
|
2318
|
+
const legacy = [];
|
|
2319
|
+
for (const m of models) {
|
|
2320
|
+
const name = m.name.toLowerCase();
|
|
2321
|
+
const desc = (m.description || "").toLowerCase();
|
|
2322
|
+
if (name === "auto") {
|
|
2323
|
+
recommended.unshift(m);
|
|
2324
|
+
continue;
|
|
2325
|
+
}
|
|
2326
|
+
if (desc.includes("experimental") || desc.includes("preview")) {
|
|
2327
|
+
experimental.push(m);
|
|
2328
|
+
continue;
|
|
2329
|
+
}
|
|
2330
|
+
if (name.startsWith("deepseek") || name.startsWith("minimax") || name.startsWith("glm") || name.startsWith("qwen")) {
|
|
2331
|
+
experimental.push(m);
|
|
2332
|
+
continue;
|
|
2333
|
+
}
|
|
2334
|
+
if (name === "claude-sonnet-4.6" || name === "claude-opus-4.6" || name === "claude-haiku-4.5") {
|
|
2335
|
+
recommended.push(m);
|
|
2336
|
+
continue;
|
|
2337
|
+
}
|
|
2338
|
+
legacy.push(m);
|
|
2339
|
+
}
|
|
2340
|
+
return { recommended, experimental, legacy };
|
|
2341
|
+
}
|
|
2342
|
+
function buildHelpCard() {
|
|
2343
|
+
const sec = (items) => items.map(
|
|
2344
|
+
([cmd, desc]) => columnSet({
|
|
2345
|
+
flexMode: "none",
|
|
2346
|
+
horizontalSpacing: "small",
|
|
2347
|
+
columns: [
|
|
2348
|
+
column({ weight: 3, elements: [md(`\`${cmd}\``)] }),
|
|
2349
|
+
column({
|
|
2350
|
+
weight: 5,
|
|
2351
|
+
elements: [md(`<font color='grey'>${desc}</font>`)]
|
|
2352
|
+
})
|
|
2353
|
+
]
|
|
2354
|
+
})
|
|
2355
|
+
);
|
|
2356
|
+
const collapsed = (title, items) => ({
|
|
2357
|
+
tag: "collapsible_panel",
|
|
2358
|
+
expanded: false,
|
|
2359
|
+
vertical_spacing: "small",
|
|
2360
|
+
padding: "4px 8px",
|
|
2361
|
+
header: {
|
|
2362
|
+
title: {
|
|
2363
|
+
tag: "markdown",
|
|
2364
|
+
content: `<font color='grey'>${title}</font>`
|
|
2365
|
+
},
|
|
2366
|
+
vertical_align: "center",
|
|
2367
|
+
icon: {
|
|
2368
|
+
tag: "standard_icon",
|
|
2369
|
+
token: "down-small-ccm_outlined",
|
|
2370
|
+
size: "14px 14px"
|
|
2371
|
+
},
|
|
2372
|
+
icon_position: "follow_text",
|
|
2373
|
+
icon_expanded_angle: -180
|
|
2374
|
+
},
|
|
2375
|
+
elements: sec(items)
|
|
2376
|
+
});
|
|
2377
|
+
const elements = [
|
|
2378
|
+
md("\u5728\u98DE\u4E66\u91CC\u8C03\u7528\u672C\u5730 Kiro CLI\uFF0C\u6BCF\u4E2A\u5BF9\u8BDD\u72EC\u7ACB session\u3002"),
|
|
2379
|
+
...sec([
|
|
2380
|
+
["/new", "\u91CD\u7F6E\u5F53\u524D\u4F1A\u8BDD"],
|
|
2381
|
+
["/status", "\u67E5\u770B cwd / session / watchdog"],
|
|
2382
|
+
["/stop", "\u505C\u6B62\u6B63\u5728\u8DD1\u7684\u4EFB\u52A1"],
|
|
2383
|
+
["/model \xB7 /m", "\u67E5\u770B / \u5207\u6362\u6A21\u578B"]
|
|
2384
|
+
]),
|
|
2385
|
+
collapsed("\u5C55\u5F00\uFF1A\u5DE5\u4F5C\u76EE\u5F55 & \u5DE5\u4F5C\u533A", [
|
|
2386
|
+
["/pwd", "\u67E5\u770B\u5F53\u524D\u76EE\u5F55"],
|
|
2387
|
+
["/cd <path>", "\u5207\u6362\u76EE\u5F55\uFF08\u767D\u540D\u5355\u5185\uFF09"],
|
|
2388
|
+
["/ws list", "\u5217\u51FA\u547D\u540D\u5DE5\u4F5C\u533A"],
|
|
2389
|
+
["/ws save <name>", "\u628A\u5F53\u524D cwd \u5B58\u4E3A\u5DE5\u4F5C\u533A"],
|
|
2390
|
+
["/ws use <name>", "\u5207\u5230\u547D\u540D\u5DE5\u4F5C\u533A"]
|
|
2391
|
+
]),
|
|
2392
|
+
collapsed("\u5C55\u5F00\uFF1A\u8FD0\u7EF4", [
|
|
2393
|
+
["/timeout [N|off]", "idle watchdog \u9608\u503C"],
|
|
2394
|
+
["/config", "\u67E5\u770B / \u7F16\u8F91\u8BBF\u95EE\u63A7\u5236 + \u504F\u597D\uFF08\u7BA1\u7406\u5458\uFF09"],
|
|
2395
|
+
["/reconnect", "\u91CD\u8FDE\u98DE\u4E66 WebSocket"],
|
|
2396
|
+
["/doctor [\u63CF\u8FF0]", "\u770B\u65E5\u5FD7\u81EA\u8BCA\u65AD"]
|
|
2397
|
+
]),
|
|
2398
|
+
hr(),
|
|
2399
|
+
columnSet({
|
|
2400
|
+
flexMode: "flow",
|
|
2401
|
+
horizontalSpacing: "small",
|
|
2402
|
+
columns: [
|
|
2403
|
+
column({
|
|
2404
|
+
width: "auto",
|
|
2405
|
+
elements: [
|
|
2406
|
+
btn({
|
|
2407
|
+
text: "\u{1F4CA} \u72B6\u6001",
|
|
2408
|
+
type: "default",
|
|
2409
|
+
size: "tiny",
|
|
2410
|
+
value: { action: "session.status" }
|
|
2411
|
+
})
|
|
2412
|
+
]
|
|
2413
|
+
}),
|
|
2414
|
+
column({
|
|
2415
|
+
width: "auto",
|
|
2416
|
+
elements: [
|
|
2417
|
+
btn({
|
|
2418
|
+
text: "\u{1F39B}\uFE0F \u6A21\u578B",
|
|
2419
|
+
type: "default",
|
|
2420
|
+
size: "tiny",
|
|
2421
|
+
value: { action: "model.show" }
|
|
2422
|
+
})
|
|
2423
|
+
]
|
|
2424
|
+
}),
|
|
2425
|
+
column({
|
|
2426
|
+
width: "auto",
|
|
2427
|
+
elements: [
|
|
2428
|
+
btn({
|
|
2429
|
+
text: "\u{1F5C2}\uFE0F \u5DE5\u4F5C\u533A",
|
|
2430
|
+
type: "default",
|
|
2431
|
+
size: "tiny",
|
|
2432
|
+
value: { action: "ws.list" }
|
|
2433
|
+
})
|
|
2434
|
+
]
|
|
2435
|
+
}),
|
|
2436
|
+
column({
|
|
2437
|
+
width: "auto",
|
|
2438
|
+
elements: [
|
|
2439
|
+
btn({
|
|
2440
|
+
text: "\u{1F504} \u91CD\u7F6E\u4F1A\u8BDD",
|
|
2441
|
+
type: "default",
|
|
2442
|
+
size: "tiny",
|
|
2443
|
+
value: { action: "session.new" },
|
|
2444
|
+
confirm: {
|
|
2445
|
+
title: "\u91CD\u7F6E\u4F1A\u8BDD",
|
|
2446
|
+
text: "\u5C06\u6E05\u7A7A\u5F53\u524D cwd \u4E0B\u7684 Kiro \u4F1A\u8BDD\u5386\u53F2\u3002\u4E0B\u6761\u6D88\u606F\u4F1A\u65B0\u5EFA session\u3002"
|
|
2447
|
+
}
|
|
2448
|
+
})
|
|
2449
|
+
]
|
|
2450
|
+
})
|
|
2451
|
+
]
|
|
2452
|
+
})
|
|
2453
|
+
];
|
|
2454
|
+
return {
|
|
2455
|
+
schema: "2.0",
|
|
2456
|
+
header: buildHeader({ title: "\u{1F4D6} \u547D\u4EE4\u5E2E\u52A9", template: "blue" }),
|
|
2457
|
+
body: { elements }
|
|
2458
|
+
};
|
|
2459
|
+
}
|
|
2460
|
+
function buildWorkspaceListCard(opts) {
|
|
2461
|
+
const entries = Object.entries(opts.workspaces);
|
|
2462
|
+
const elements = [];
|
|
2463
|
+
if (entries.length === 0) {
|
|
2464
|
+
elements.push(
|
|
2465
|
+
md(
|
|
2466
|
+
"_\u5F53\u524D\u6CA1\u6709\u547D\u540D\u5DE5\u4F5C\u533A\u3002_\n\n\u7528 `/ws save <name>` \u628A\u5F53\u524D\u76EE\u5F55\u5B58\u4E3A\u5DE5\u4F5C\u533A\uFF0C\u65B9\u4FBF\u540E\u7EED `/ws use <name>` \u4E00\u952E\u5207\u6362\u3002"
|
|
2467
|
+
)
|
|
2468
|
+
);
|
|
2469
|
+
return {
|
|
2470
|
+
schema: "2.0",
|
|
2471
|
+
header: buildHeader({ title: "\u{1F5C2}\uFE0F \u547D\u540D\u5DE5\u4F5C\u533A", template: "blue" }),
|
|
2472
|
+
body: { elements }
|
|
2473
|
+
};
|
|
2474
|
+
}
|
|
2475
|
+
for (const [name, path] of entries) {
|
|
2476
|
+
const isCurrent = path === opts.currentCwd;
|
|
2477
|
+
const shortPath = shortenPath2(path, 40);
|
|
2478
|
+
elements.push(
|
|
2479
|
+
columnSet({
|
|
2480
|
+
flexMode: "none",
|
|
2481
|
+
horizontalSpacing: "small",
|
|
2482
|
+
columns: [
|
|
2483
|
+
column({
|
|
2484
|
+
weight: 3,
|
|
2485
|
+
elements: [md(isCurrent ? `**\u2713 ${name}**` : `\u3000${name}`)]
|
|
2486
|
+
}),
|
|
2487
|
+
column({
|
|
2488
|
+
weight: 4,
|
|
2489
|
+
elements: [md(`<font color='grey'>${shortPath}</font>`, "right")]
|
|
2490
|
+
}),
|
|
2491
|
+
column({
|
|
2492
|
+
weight: 2,
|
|
2493
|
+
vAlign: "center",
|
|
2494
|
+
elements: [
|
|
2495
|
+
isCurrent ? md(`<font color='green'>**\u5F53\u524D**</font>`, "right") : btn({
|
|
2496
|
+
text: "\u5207\u6362",
|
|
2497
|
+
type: "primary",
|
|
2498
|
+
size: "tiny",
|
|
2499
|
+
value: { action: "ws.use", name },
|
|
2500
|
+
hoverTip: path
|
|
2501
|
+
})
|
|
2502
|
+
]
|
|
2503
|
+
})
|
|
2504
|
+
]
|
|
2505
|
+
})
|
|
2506
|
+
);
|
|
2507
|
+
}
|
|
2508
|
+
elements.push(hr());
|
|
2509
|
+
elements.push(
|
|
2510
|
+
md('<font color="grey">\u{1F4A1} \u7528 `/ws save <name>` \u6DFB\u52A0\uFF0C`/ws remove <name>` \u5220\u9664\u3002</font>')
|
|
2511
|
+
);
|
|
2512
|
+
return {
|
|
2513
|
+
schema: "2.0",
|
|
2514
|
+
header: buildHeader({
|
|
2515
|
+
title: "\u{1F5C2}\uFE0F \u547D\u540D\u5DE5\u4F5C\u533A",
|
|
2516
|
+
template: "blue",
|
|
2517
|
+
subtitle: `${entries.length} \u4E2A`
|
|
2518
|
+
}),
|
|
2519
|
+
body: { elements }
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
function buildStatusCard(opts) {
|
|
2523
|
+
const elements = [];
|
|
2524
|
+
const row = (label, val) => columnSet({
|
|
2525
|
+
flexMode: "none",
|
|
2526
|
+
horizontalSpacing: "small",
|
|
2527
|
+
columns: [
|
|
2528
|
+
column({
|
|
2529
|
+
weight: 2,
|
|
2530
|
+
elements: [md(`<font color='grey'>${label}</font>`)]
|
|
2531
|
+
}),
|
|
2532
|
+
column({ weight: 5, elements: [md(val)] })
|
|
2533
|
+
]
|
|
2534
|
+
});
|
|
2535
|
+
elements.push(row("\u5F53\u524D\u76EE\u5F55", `\`${opts.cwd}\``));
|
|
2536
|
+
if (opts.workspaceName) {
|
|
2537
|
+
elements.push(row("\u5DE5\u4F5C\u533A", `\u{1F5C2}\uFE0F \`${opts.workspaceName}\``));
|
|
2538
|
+
}
|
|
2539
|
+
elements.push(
|
|
2540
|
+
row(
|
|
2541
|
+
"Kiro session",
|
|
2542
|
+
opts.kiroSessionId ? `\u21AA\uFE0F \`${opts.kiroSessionId.slice(0, 8)}\u2026\`` : "_\u672A\u5EFA\u7ACB\uFF0C\u4E0B\u6761\u6D88\u606F\u4F1A\u65B0\u5EFA_"
|
|
2543
|
+
)
|
|
2544
|
+
);
|
|
2545
|
+
elements.push(row("\u4EFB\u52A1\u72B6\u6001", opts.hasActiveTask ? "\u{1F7E2} \u8FDB\u884C\u4E2D" : "\u26AA \u7A7A\u95F2"));
|
|
2546
|
+
elements.push(
|
|
2547
|
+
row(
|
|
2548
|
+
"Idle watchdog",
|
|
2549
|
+
opts.idleMinutes > 0 ? `${opts.idleMinutes} \u5206\u949F${opts.isPerChatOverride ? "\uFF08per-chat \u8986\u76D6\uFF09" : ""}` : "\u5173\u95ED"
|
|
2550
|
+
)
|
|
2551
|
+
);
|
|
2552
|
+
elements.push(hr());
|
|
2553
|
+
elements.push(
|
|
2554
|
+
columnSet({
|
|
2555
|
+
flexMode: "flow",
|
|
2556
|
+
horizontalSpacing: "small",
|
|
2557
|
+
columns: [
|
|
2558
|
+
column({
|
|
2559
|
+
width: "auto",
|
|
2560
|
+
elements: [
|
|
2561
|
+
btn({
|
|
2562
|
+
text: "\u{1F504} \u91CD\u7F6E\u4F1A\u8BDD",
|
|
2563
|
+
type: "default",
|
|
2564
|
+
size: "tiny",
|
|
2565
|
+
value: { action: "session.new" },
|
|
2566
|
+
confirm: {
|
|
2567
|
+
title: "\u91CD\u7F6E\u4F1A\u8BDD",
|
|
2568
|
+
text: "\u5C06\u6E05\u7A7A\u5F53\u524D cwd \u4E0B\u7684 Kiro \u4F1A\u8BDD\u5386\u53F2\u3002\u4E0B\u6761\u6D88\u606F\u4F1A\u65B0\u5EFA session\u3002"
|
|
2569
|
+
}
|
|
2570
|
+
})
|
|
2571
|
+
]
|
|
2572
|
+
}),
|
|
2573
|
+
...opts.hasActiveTask ? [
|
|
2574
|
+
column({
|
|
2575
|
+
width: "auto",
|
|
2576
|
+
elements: [
|
|
2577
|
+
btn({
|
|
2578
|
+
text: "\u23F9 \u505C\u6B62\u4EFB\u52A1",
|
|
2579
|
+
type: "danger",
|
|
2580
|
+
size: "tiny",
|
|
2581
|
+
value: { action: "session.stop" }
|
|
2582
|
+
})
|
|
2583
|
+
]
|
|
2584
|
+
})
|
|
2585
|
+
] : [],
|
|
2586
|
+
column({
|
|
2587
|
+
width: "auto",
|
|
2588
|
+
elements: [
|
|
2589
|
+
btn({
|
|
2590
|
+
text: "\u{1F39B}\uFE0F \u6A21\u578B",
|
|
2591
|
+
type: "default",
|
|
2592
|
+
size: "tiny",
|
|
2593
|
+
value: { action: "model.show" }
|
|
2594
|
+
})
|
|
2595
|
+
]
|
|
2596
|
+
}),
|
|
2597
|
+
column({
|
|
2598
|
+
width: "auto",
|
|
2599
|
+
elements: [
|
|
2600
|
+
btn({
|
|
2601
|
+
text: "\u{1F5C2}\uFE0F \u5DE5\u4F5C\u533A",
|
|
2602
|
+
type: "default",
|
|
2603
|
+
size: "tiny",
|
|
2604
|
+
value: { action: "ws.list" }
|
|
2605
|
+
})
|
|
2606
|
+
]
|
|
2607
|
+
})
|
|
2608
|
+
]
|
|
2609
|
+
})
|
|
2610
|
+
);
|
|
2611
|
+
return {
|
|
2612
|
+
schema: "2.0",
|
|
2613
|
+
header: buildHeader({
|
|
2614
|
+
title: "\u{1F4CA} \u5F53\u524D\u72B6\u6001",
|
|
2615
|
+
template: "green",
|
|
2616
|
+
...opts.workspaceName ? { subtitle: `\u{1F5C2}\uFE0F ${opts.workspaceName}` } : {}
|
|
2617
|
+
}),
|
|
2618
|
+
body: { elements }
|
|
2619
|
+
};
|
|
2620
|
+
}
|
|
2621
|
+
function buildAckCard(opts) {
|
|
2622
|
+
const tmpl = {
|
|
2623
|
+
done: { title: "\u2705 \u5DF2\u5B8C\u6210", template: "green" },
|
|
2624
|
+
error: { title: "\u274C \u51FA\u9519", template: "red" },
|
|
2625
|
+
aborted: { title: "\u23F9 \u5DF2\u4E2D\u6B62", template: "orange" }
|
|
2626
|
+
};
|
|
2627
|
+
const { template } = tmpl[opts.state];
|
|
2628
|
+
const title = opts.title ?? tmpl[opts.state].title;
|
|
2629
|
+
return {
|
|
2630
|
+
schema: "2.0",
|
|
2631
|
+
header: buildHeader({ title, template }),
|
|
2632
|
+
body: { elements: [md(opts.body)] }
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
function buildLoadingCard(message = "\u5904\u7406\u4E2D\u2026", title = "\u23F3 \u5904\u7406\u4E2D") {
|
|
2636
|
+
return {
|
|
2637
|
+
schema: "2.0",
|
|
2638
|
+
header: buildHeader({ title, template: "wathet" }),
|
|
2639
|
+
body: { elements: [md(message)] }
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
function buildConfigViewCard(opts) {
|
|
2643
|
+
const elements = [];
|
|
2644
|
+
const list = (xs, emptyHint) => {
|
|
2645
|
+
if (xs.length === 0) return `<font color='grey'>${emptyHint}</font>`;
|
|
2646
|
+
return xs.map((x) => `\`${x}\``).join("\u3001");
|
|
2647
|
+
};
|
|
2648
|
+
const row = (label, value) => columnSet({
|
|
2649
|
+
flexMode: "none",
|
|
2650
|
+
horizontalSpacing: "small",
|
|
2651
|
+
columns: [
|
|
2652
|
+
column({ weight: 2, elements: [md(`<font color='grey'>${label}</font>`)] }),
|
|
2653
|
+
column({ weight: 5, elements: [md(value)] })
|
|
2654
|
+
]
|
|
2655
|
+
});
|
|
2656
|
+
elements.push(md("**\u{1F510} \u8BBF\u95EE\u63A7\u5236**"));
|
|
2657
|
+
elements.push(row("\u5141\u8BB8\u7684\u7528\u6237", list(opts.allowedUsers, "\u7A7A = \u6240\u6709\u7528\u6237")));
|
|
2658
|
+
elements.push(row("\u5141\u8BB8\u7684\u7FA4", list(opts.allowedChats, "\u7A7A = \u6240\u6709\u7FA4\uFF08DM \u6C38\u8FDC\u8C41\u514D\uFF09")));
|
|
2659
|
+
elements.push(row("\u7BA1\u7406\u5458", list(opts.admins, "\u7A7A = \u6240\u6709\u7528\u6237\u90FD\u662F admin")));
|
|
2660
|
+
elements.push(hr());
|
|
2661
|
+
elements.push(md("**\u2699\uFE0F \u504F\u597D**"));
|
|
2662
|
+
elements.push(row("\u7FA4\u91CC\u9700\u8981 @bot", opts.requireMentionInGroup ? "\u2713 \u662F" : "\xD7 \u5426"));
|
|
2663
|
+
elements.push(
|
|
2664
|
+
row(
|
|
2665
|
+
"Idle watchdog\uFF08\u9ED8\u8BA4\uFF09",
|
|
2666
|
+
opts.idleTimeoutMinutes > 0 ? `${opts.idleTimeoutMinutes} \u5206\u949F` : "\u5173\u95ED"
|
|
2667
|
+
)
|
|
2668
|
+
);
|
|
2669
|
+
elements.push(row("\u5361\u7247\u66F4\u65B0\u95F4\u9694", `${opts.cardUpdateIntervalMs}ms`));
|
|
2670
|
+
elements.push(hr());
|
|
2671
|
+
if (opts.isAdmin) {
|
|
2672
|
+
elements.push(
|
|
2673
|
+
columnSet({
|
|
2674
|
+
flexMode: "flow",
|
|
2675
|
+
horizontalSpacing: "small",
|
|
2676
|
+
columns: [
|
|
2677
|
+
column({
|
|
2678
|
+
width: "auto",
|
|
2679
|
+
elements: [
|
|
2680
|
+
btn({
|
|
2681
|
+
text: "\u270F\uFE0F \u7F16\u8F91\u914D\u7F6E",
|
|
2682
|
+
type: "primary",
|
|
2683
|
+
size: "tiny",
|
|
2684
|
+
value: { action: "config.edit" }
|
|
2685
|
+
})
|
|
2686
|
+
]
|
|
2687
|
+
}),
|
|
2688
|
+
column({
|
|
2689
|
+
width: "auto",
|
|
2690
|
+
elements: [
|
|
2691
|
+
btn({
|
|
2692
|
+
text: "\u{1F504} \u5237\u65B0",
|
|
2693
|
+
type: "default",
|
|
2694
|
+
size: "tiny",
|
|
2695
|
+
value: { action: "config.show" }
|
|
2696
|
+
})
|
|
2697
|
+
]
|
|
2698
|
+
})
|
|
2699
|
+
]
|
|
2700
|
+
})
|
|
2701
|
+
);
|
|
2702
|
+
} else {
|
|
2703
|
+
elements.push(md('<font color="grey">\u4EC5\u7BA1\u7406\u5458\u53EF\u7F16\u8F91\u914D\u7F6E</font>'));
|
|
2704
|
+
}
|
|
2705
|
+
return {
|
|
2706
|
+
schema: "2.0",
|
|
2707
|
+
header: buildHeader({ title: "\u2699\uFE0F \u5F53\u524D\u914D\u7F6E", template: "wathet" }),
|
|
2708
|
+
body: { elements }
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
function buildConfigFormCard(opts) {
|
|
2712
|
+
const csv = (xs) => xs.join(", ");
|
|
2713
|
+
const inputElement = (name, label, value, placeholder) => columnSet({
|
|
2714
|
+
flexMode: "none",
|
|
2715
|
+
horizontalSpacing: "small",
|
|
2716
|
+
columns: [
|
|
2717
|
+
column({ weight: 2, elements: [md(`<font color='grey'>${label}</font>`)] }),
|
|
2718
|
+
column({
|
|
2719
|
+
weight: 5,
|
|
2720
|
+
elements: [
|
|
2721
|
+
{
|
|
2722
|
+
tag: "input",
|
|
2723
|
+
name,
|
|
2724
|
+
default_value: value,
|
|
2725
|
+
placeholder: { tag: "plain_text", content: placeholder },
|
|
2726
|
+
width: "fill"
|
|
2727
|
+
}
|
|
2728
|
+
]
|
|
2729
|
+
})
|
|
2730
|
+
]
|
|
2731
|
+
});
|
|
2732
|
+
const radioElement = (name, label, current) => columnSet({
|
|
2733
|
+
flexMode: "none",
|
|
2734
|
+
horizontalSpacing: "small",
|
|
2735
|
+
columns: [
|
|
2736
|
+
column({ weight: 2, elements: [md(`<font color='grey'>${label}</font>`)] }),
|
|
2737
|
+
column({
|
|
2738
|
+
weight: 5,
|
|
2739
|
+
elements: [
|
|
2740
|
+
{
|
|
2741
|
+
tag: "select_static",
|
|
2742
|
+
name,
|
|
2743
|
+
initial_option: current,
|
|
2744
|
+
options: [
|
|
2745
|
+
{ text: { tag: "plain_text", content: "\u662F" }, value: "yes" },
|
|
2746
|
+
{ text: { tag: "plain_text", content: "\u5426" }, value: "no" }
|
|
2747
|
+
],
|
|
2748
|
+
width: "fill"
|
|
2749
|
+
}
|
|
2750
|
+
]
|
|
2751
|
+
})
|
|
2752
|
+
]
|
|
2753
|
+
});
|
|
2754
|
+
const elements = [
|
|
2755
|
+
md("**\u{1F510} \u8BBF\u95EE\u63A7\u5236** \u2014 \u9017\u53F7\u5206\u9694\u591A\u4E2A ID\u3002\u7A7A = \u4E0D\u9650\u5236\u3002"),
|
|
2756
|
+
inputElement("allowedUsers", "\u5141\u8BB8\u7684\u7528\u6237", csv(opts.allowedUsers), "ou_xxx, ou_yyy"),
|
|
2757
|
+
inputElement(
|
|
2758
|
+
"allowedChats",
|
|
2759
|
+
"\u5141\u8BB8\u7684\u7FA4",
|
|
2760
|
+
csv(opts.allowedChats),
|
|
2761
|
+
"oc_xxx, oc_yyy\uFF08DM \u6C38\u8FDC\u8C41\u514D\uFF09"
|
|
2762
|
+
),
|
|
2763
|
+
inputElement("admins", "\u7BA1\u7406\u5458", csv(opts.admins), "ou_xxx"),
|
|
2764
|
+
hr(),
|
|
2765
|
+
md("**\u2699\uFE0F \u504F\u597D**"),
|
|
2766
|
+
radioElement("requireMentionInGroup", "\u7FA4\u91CC\u8981 @bot", opts.requireMentionInGroup ? "yes" : "no"),
|
|
2767
|
+
inputElement(
|
|
2768
|
+
"idleTimeoutMinutes",
|
|
2769
|
+
"Idle watchdog\uFF08\u5206\u949F\uFF09",
|
|
2770
|
+
String(opts.idleTimeoutMinutes),
|
|
2771
|
+
"0 = \u5173\u95ED"
|
|
2772
|
+
),
|
|
2773
|
+
hr(),
|
|
2774
|
+
columnSet({
|
|
2775
|
+
flexMode: "flow",
|
|
2776
|
+
horizontalSpacing: "small",
|
|
2777
|
+
columns: [
|
|
2778
|
+
column({
|
|
2779
|
+
width: "auto",
|
|
2780
|
+
elements: [
|
|
2781
|
+
{
|
|
2782
|
+
tag: "button",
|
|
2783
|
+
text: { tag: "plain_text", content: "\u{1F4BE} \u4FDD\u5B58" },
|
|
2784
|
+
type: "primary_filled",
|
|
2785
|
+
size: "small",
|
|
2786
|
+
behaviors: [
|
|
2787
|
+
{
|
|
2788
|
+
type: "callback",
|
|
2789
|
+
value: { action: "config.submit" }
|
|
2790
|
+
}
|
|
2791
|
+
],
|
|
2792
|
+
form_action_type: "submit"
|
|
2793
|
+
}
|
|
2794
|
+
]
|
|
2795
|
+
}),
|
|
2796
|
+
column({
|
|
2797
|
+
width: "auto",
|
|
2798
|
+
elements: [
|
|
2799
|
+
btn({
|
|
2800
|
+
text: "\u53D6\u6D88",
|
|
2801
|
+
type: "default",
|
|
2802
|
+
size: "small",
|
|
2803
|
+
value: { action: "config.show" }
|
|
2804
|
+
})
|
|
2805
|
+
]
|
|
2806
|
+
}),
|
|
2807
|
+
column({
|
|
2808
|
+
width: "auto",
|
|
2809
|
+
elements: [
|
|
2810
|
+
md('<font color="grey">\u{1F4A1} \u627E open_id\uFF1A\u53D1\u6761\u6D88\u606F\u540E\u67E5 `~/.lark-kiro-bridge/logs/`</font>')
|
|
2811
|
+
]
|
|
2812
|
+
})
|
|
2813
|
+
]
|
|
2814
|
+
})
|
|
2815
|
+
];
|
|
2816
|
+
return {
|
|
2817
|
+
schema: "2.0",
|
|
2818
|
+
header: buildHeader({ title: "\u270F\uFE0F \u7F16\u8F91\u914D\u7F6E", template: "blue" }),
|
|
2819
|
+
body: {
|
|
2820
|
+
elements: [
|
|
2821
|
+
{
|
|
2822
|
+
tag: "form",
|
|
2823
|
+
name: "config_form",
|
|
2824
|
+
elements
|
|
2825
|
+
}
|
|
2826
|
+
]
|
|
2827
|
+
}
|
|
2828
|
+
};
|
|
2829
|
+
}
|
|
2830
|
+
function shortenPath2(p, maxLen) {
|
|
2831
|
+
if (p.length <= maxLen) return p;
|
|
2832
|
+
const segs = p.split("/").filter(Boolean);
|
|
2833
|
+
if (segs.length <= 2) return "\u2026" + p.slice(-(maxLen - 1));
|
|
2834
|
+
return "\u2026/" + segs.slice(-2).join("/");
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
// src/core/pipeline.ts
|
|
2838
|
+
var ChatPipeline = class {
|
|
2839
|
+
constructor(chatId, logger) {
|
|
2840
|
+
this.chatId = chatId;
|
|
2841
|
+
this.log = logger.child({ module: "pipeline", chatId });
|
|
2842
|
+
}
|
|
2843
|
+
chatId;
|
|
2844
|
+
log;
|
|
2845
|
+
currentAbort = null;
|
|
2846
|
+
currentTaskId = null;
|
|
2847
|
+
currentPromise = null;
|
|
2848
|
+
/**
|
|
2849
|
+
* 提交新任务。
|
|
2850
|
+
* 如果当前有任务在跑:
|
|
2851
|
+
* - 先发 abort 信号
|
|
2852
|
+
* - 等旧任务收尾(旧 run() 应该 catch AbortError 并 finalize 卡片)
|
|
2853
|
+
* - 再启动新任务
|
|
2854
|
+
*/
|
|
2855
|
+
async submit(task) {
|
|
2856
|
+
if (this.currentAbort) {
|
|
2857
|
+
this.log.info({ oldTask: this.currentTaskId, newTask: task.id }, "preempting current task");
|
|
2858
|
+
this.currentAbort.abort();
|
|
2859
|
+
try {
|
|
2860
|
+
await this.currentPromise;
|
|
2861
|
+
} catch {
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
const ctrl = new AbortController();
|
|
2865
|
+
this.currentAbort = ctrl;
|
|
2866
|
+
this.currentTaskId = task.id;
|
|
2867
|
+
const p = (async () => {
|
|
2868
|
+
try {
|
|
2869
|
+
await task.run(ctrl.signal);
|
|
2870
|
+
} catch (e) {
|
|
2871
|
+
this.log.error({ err: e, taskId: task.id }, "task threw");
|
|
2872
|
+
} finally {
|
|
2873
|
+
if (this.currentAbort === ctrl) {
|
|
2874
|
+
this.currentAbort = null;
|
|
2875
|
+
this.currentTaskId = null;
|
|
2876
|
+
this.currentPromise = null;
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
})();
|
|
2880
|
+
this.currentPromise = p;
|
|
2881
|
+
await p;
|
|
2882
|
+
}
|
|
2883
|
+
/** 主动中止当前任务(/stop 命令)。返回是否真的中止了某任务。 */
|
|
2884
|
+
abortCurrent() {
|
|
2885
|
+
if (this.currentAbort) {
|
|
2886
|
+
this.log.info({ taskId: this.currentTaskId }, "aborting current task");
|
|
2887
|
+
this.currentAbort.abort();
|
|
2888
|
+
return true;
|
|
2889
|
+
}
|
|
2890
|
+
return false;
|
|
2891
|
+
}
|
|
2892
|
+
hasActiveTask() {
|
|
2893
|
+
return this.currentAbort !== null;
|
|
2894
|
+
}
|
|
2895
|
+
};
|
|
2896
|
+
|
|
2897
|
+
// src/lib/security.ts
|
|
2898
|
+
import { resolve, normalize } from "path";
|
|
2899
|
+
import { existsSync as existsSync5, statSync as statSync4 } from "fs";
|
|
2900
|
+
import { homedir as homedir3 } from "os";
|
|
2901
|
+
var SecurityError = class extends Error {
|
|
2902
|
+
constructor(message) {
|
|
2903
|
+
super(message);
|
|
2904
|
+
this.name = "SecurityError";
|
|
2905
|
+
}
|
|
2906
|
+
};
|
|
2907
|
+
function resolvePath(p, baseCwd) {
|
|
2908
|
+
let s = p.trim();
|
|
2909
|
+
if (s.startsWith("~/") || s === "~") {
|
|
2910
|
+
s = s.replace(/^~/, homedir3());
|
|
2911
|
+
}
|
|
2912
|
+
return normalize(resolve(baseCwd, s));
|
|
2913
|
+
}
|
|
2914
|
+
function isPathAllowed(absPath, allowedRoots) {
|
|
2915
|
+
const target = normalize(absPath);
|
|
2916
|
+
for (const root of allowedRoots) {
|
|
2917
|
+
const r = normalize(resolve(root));
|
|
2918
|
+
const rWithSep = r.endsWith("/") ? r : `${r}/`;
|
|
2919
|
+
if (target === r || target.startsWith(rWithSep)) return true;
|
|
2920
|
+
}
|
|
2921
|
+
return false;
|
|
2922
|
+
}
|
|
2923
|
+
function validateCwd(targetPath, config, baseCwd) {
|
|
2924
|
+
const abs = resolvePath(targetPath, baseCwd);
|
|
2925
|
+
if (!isPathAllowed(abs, config.workspace.allowedRoots)) {
|
|
2926
|
+
throw new SecurityError(
|
|
2927
|
+
`\u8DEF\u5F84 \`${abs}\` \u4E0D\u5728\u767D\u540D\u5355\u5185\u3002
|
|
2928
|
+
\u767D\u540D\u5355\uFF1A
|
|
2929
|
+
${config.workspace.allowedRoots.map((r) => ` \u2022 \`${r}\``).join("\n")}`
|
|
2930
|
+
);
|
|
2931
|
+
}
|
|
2932
|
+
if (!existsSync5(abs)) {
|
|
2933
|
+
throw new SecurityError(`\u8DEF\u5F84\u4E0D\u5B58\u5728\uFF1A\`${abs}\``);
|
|
2934
|
+
}
|
|
2935
|
+
const stat = statSync4(abs);
|
|
2936
|
+
if (!stat.isDirectory()) {
|
|
2937
|
+
throw new SecurityError(`\u4E0D\u662F\u76EE\u5F55\uFF1A\`${abs}\``);
|
|
2938
|
+
}
|
|
2939
|
+
return abs;
|
|
2940
|
+
}
|
|
2941
|
+
function isUserAllowed(senderOpenId, chatId, chatType, config) {
|
|
2942
|
+
const { allowedUsers, allowedChats } = config.access;
|
|
2943
|
+
const userOk = allowedUsers.length === 0 || allowedUsers.includes(senderOpenId);
|
|
2944
|
+
const chatOk = chatType === "p2p" || allowedChats.length === 0 || allowedChats.includes(chatId);
|
|
2945
|
+
return userOk && chatOk;
|
|
2946
|
+
}
|
|
2947
|
+
function isAdmin(senderOpenId, config) {
|
|
2948
|
+
const { admins } = config.access;
|
|
2949
|
+
return admins.length === 0 || admins.includes(senderOpenId);
|
|
2950
|
+
}
|
|
2951
|
+
function validateAccessChange(opts) {
|
|
2952
|
+
const errors = [];
|
|
2953
|
+
const { submitterOpenId, next } = opts;
|
|
2954
|
+
if (next.admins.length > 0 && !next.admins.includes(submitterOpenId)) {
|
|
2955
|
+
errors.push(
|
|
2956
|
+
`\u274C \u4F60\u7684 open_id\uFF08\`${submitterOpenId}\`\uFF09\u4E0D\u5728 admins \u5217\u8868\u91CC\uFF0C\u63D0\u4EA4\u540E\u4F60\u5C06\u65E0\u6CD5\u518D\u6539\u914D\u7F6E\u3002\u8BF7\u5148\u628A\u81EA\u5DF1\u52A0\u8FDB\u53BB\u3002`
|
|
2957
|
+
);
|
|
2958
|
+
}
|
|
2959
|
+
if (next.allowedUsers.length > 0 && !next.allowedUsers.includes(submitterOpenId)) {
|
|
2960
|
+
errors.push(
|
|
2961
|
+
`\u274C \u4F60\u7684 open_id\uFF08\`${submitterOpenId}\`\uFF09\u4E0D\u5728 allowedUsers \u5217\u8868\u91CC\uFF0C\u63D0\u4EA4\u540E\u4F60\u7684\u4E0B\u4E00\u6761\u6D88\u606F\u4F1A\u88AB\u9759\u9ED8\u4E22\u5F03\u3002\u8BF7\u5148\u628A\u81EA\u5DF1\u52A0\u8FDB\u53BB\u3002`
|
|
2962
|
+
);
|
|
2963
|
+
}
|
|
2964
|
+
return errors;
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
// src/core/dispatcher.ts
|
|
2968
|
+
var Dispatcher = class {
|
|
2969
|
+
config;
|
|
2970
|
+
lark;
|
|
2971
|
+
sessions;
|
|
2972
|
+
workspaces;
|
|
2973
|
+
log;
|
|
2974
|
+
pipelines = /* @__PURE__ */ new Map();
|
|
2975
|
+
onReconnect;
|
|
2976
|
+
constructor(opts) {
|
|
2977
|
+
this.config = opts.config;
|
|
2978
|
+
this.lark = opts.lark;
|
|
2979
|
+
this.sessions = opts.sessions;
|
|
2980
|
+
this.workspaces = opts.workspaces;
|
|
2981
|
+
this.log = opts.logger.child({ module: "dispatcher" });
|
|
2982
|
+
if (opts.onReconnect) this.onReconnect = opts.onReconnect;
|
|
2983
|
+
}
|
|
2984
|
+
getPipeline(chatId) {
|
|
2985
|
+
let p = this.pipelines.get(chatId);
|
|
2986
|
+
if (!p) {
|
|
2987
|
+
p = new ChatPipeline(chatId, this.log);
|
|
2988
|
+
this.pipelines.set(chatId, p);
|
|
2989
|
+
}
|
|
2990
|
+
return p;
|
|
2991
|
+
}
|
|
2992
|
+
/** eventId 去重缓存:飞书 at-least-once 投递保险 */
|
|
2993
|
+
seenEventIds = /* @__PURE__ */ new Map();
|
|
2994
|
+
EVENT_TTL_MS = 5 * 60 * 1e3;
|
|
2995
|
+
/**
|
|
2996
|
+
* rapid-fire 消息合并:同 chat 短时间连发时把多条消息拼成一次 Kiro 调用。
|
|
2997
|
+
*
|
|
2998
|
+
* 价值:用户在飞书 IM 里习惯连发短消息("等等"、"还有"、"刚才那个改一下"),
|
|
2999
|
+
* 不合并的话每条都跑一次 Kiro,浪费 token 还会被前面的 abort 打断;合并后
|
|
3000
|
+
* 一次性给 Kiro 完整意图。
|
|
3001
|
+
*
|
|
3002
|
+
* 实现:每条消息进来先放 buffer,200ms 内有新消息就追加;到时一次性 submit。
|
|
3003
|
+
* - 第一条触发计时器、注册 buffer
|
|
3004
|
+
* - 后续消息往 buffer 里追加文本(+ 媒体路径)
|
|
3005
|
+
* - 200ms 静默期到 → flush,用第一条的 msg/eventId 作为 reply 锚点
|
|
3006
|
+
*/
|
|
3007
|
+
mergeBuffers = /* @__PURE__ */ new Map();
|
|
3008
|
+
MERGE_WINDOW_MS = 200;
|
|
3009
|
+
isDuplicate(eventId) {
|
|
3010
|
+
if (!eventId) return false;
|
|
3011
|
+
const now = Date.now();
|
|
3012
|
+
for (const [k, t] of this.seenEventIds) {
|
|
3013
|
+
if (now - t > this.EVENT_TTL_MS) this.seenEventIds.delete(k);
|
|
3014
|
+
}
|
|
3015
|
+
if (this.seenEventIds.has(eventId)) return true;
|
|
3016
|
+
this.seenEventIds.set(eventId, now);
|
|
3017
|
+
return false;
|
|
3018
|
+
}
|
|
3019
|
+
/**
|
|
3020
|
+
* 主入口:处理一条飞书消息。
|
|
3021
|
+
*/
|
|
3022
|
+
async handle(msg) {
|
|
3023
|
+
if (this.isDuplicate(msg.eventId)) {
|
|
3024
|
+
this.log.info({ eventId: msg.eventId }, "duplicate event, skip");
|
|
3025
|
+
return;
|
|
3026
|
+
}
|
|
3027
|
+
if (!this.lark["botOpenIdCache"]) {
|
|
3028
|
+
const guess = msg.mentions.find((m) => m.name?.toLowerCase().includes("kiro"))?.openId;
|
|
3029
|
+
if (guess) this.lark.setBotOpenId(guess);
|
|
3030
|
+
}
|
|
3031
|
+
if (!isUserAllowed(msg.senderOpenId, msg.chatId, msg.chatType, this.config)) {
|
|
3032
|
+
this.log.debug(
|
|
3033
|
+
{ user: msg.senderOpenId, chat: msg.chatId, chatType: msg.chatType },
|
|
3034
|
+
"message dropped by access control"
|
|
3035
|
+
);
|
|
3036
|
+
return;
|
|
3037
|
+
}
|
|
3038
|
+
if (msg.chatType === "group" || msg.chatType === "topic_group") {
|
|
3039
|
+
if (this.config.preferences.requireMentionInGroup) {
|
|
3040
|
+
const botMentioned = msg.mentions.some(
|
|
3041
|
+
(m) => (m.name ?? "").toLowerCase().includes("kiro") || (m.name ?? "").toLowerCase().includes("bot")
|
|
3042
|
+
);
|
|
3043
|
+
if (!botMentioned && msg.mentions.length > 0) {
|
|
3044
|
+
this.log.debug("group message without @bot, ignored");
|
|
3045
|
+
return;
|
|
3046
|
+
}
|
|
3047
|
+
if (msg.mentions.length === 0) {
|
|
3048
|
+
this.log.debug("group message has no mentions at all, ignored (requireMentionInGroup)");
|
|
3049
|
+
return;
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
const supportedMedia = msg.messageType === "image" || msg.messageType === "file" || msg.messageType === "audio";
|
|
3054
|
+
if (msg.messageType !== "text" && msg.messageType !== "post" && !supportedMedia) {
|
|
3055
|
+
await this.sendInteractiveCard(
|
|
3056
|
+
msg,
|
|
3057
|
+
buildAckCard({
|
|
3058
|
+
state: "error",
|
|
3059
|
+
title: "\u26A0\uFE0F \u4E0D\u652F\u6301\u7684\u6D88\u606F\u7C7B\u578B",
|
|
3060
|
+
body: `\u5F53\u524D\u7248\u672C\u4E0D\u652F\u6301 \`${msg.messageType}\` \u7C7B\u578B\uFF0C\u5DF2\u5FFD\u7565\u3002`
|
|
3061
|
+
})
|
|
3062
|
+
);
|
|
3063
|
+
return;
|
|
3064
|
+
}
|
|
3065
|
+
const guessedBotOpenId = msg.mentions.find((m) => (m.name ?? "").toLowerCase().includes("kiro"))?.openId ?? "";
|
|
3066
|
+
const cleanText = stripMentions(msg, guessedBotOpenId).trim();
|
|
3067
|
+
let mediaPaths = [];
|
|
3068
|
+
let asrText = "";
|
|
3069
|
+
if (supportedMedia) {
|
|
3070
|
+
try {
|
|
3071
|
+
mediaPaths = await downloadMessageMedia(this.lark, msg);
|
|
3072
|
+
} catch (e) {
|
|
3073
|
+
this.log.warn({ err: e }, "media download error, will skip");
|
|
3074
|
+
}
|
|
3075
|
+
if (msg.messageType === "audio" && mediaPaths.length > 0) {
|
|
3076
|
+
const audioPath = mediaPaths[0];
|
|
3077
|
+
const r = await transcribeAudio(this.lark, audioPath);
|
|
3078
|
+
if (r.ok) {
|
|
3079
|
+
asrText = r.text;
|
|
3080
|
+
mediaPaths = mediaPaths.filter((p) => p !== audioPath);
|
|
3081
|
+
this.log.info({ textLen: r.text.length }, "audio transcribed");
|
|
3082
|
+
} else {
|
|
3083
|
+
this.log.warn({ reason: r.reason, detail: r.detail }, "audio transcription failed");
|
|
3084
|
+
const hint = (() => {
|
|
3085
|
+
switch (r.reason) {
|
|
3086
|
+
case "ffmpeg-missing":
|
|
3087
|
+
return "\u26A0\uFE0F \u672A\u68C0\u6D4B\u5230 `ffmpeg`\uFF0C\u65E0\u6CD5\u628A\u8BED\u97F3\u8F6C\u6210\u6587\u5B57\u3002\n\u8BF7\u5B89\u88C5\uFF1A`brew install ffmpeg`\uFF08macOS\uFF09\u6216\u5305\u7BA1\u7406\u5668\u5BF9\u5E94\u547D\u4EE4\u3002";
|
|
3088
|
+
case "too-long":
|
|
3089
|
+
return `\u26A0\uFE0F \u8BED\u97F3\u592A\u957F\uFF08${r.detail ?? "> 60s"}\uFF09\uFF0C\u98DE\u4E66 ASR \u4EC5\u652F\u6301 60 \u79D2\u4EE5\u5185\u3002\u8BF7\u5206\u6BB5\u91CD\u53D1\u3002`;
|
|
3090
|
+
case "api-failed":
|
|
3091
|
+
return `\u26A0\uFE0F \u8BED\u97F3\u8BC6\u522B\u5931\u8D25\uFF1A${r.detail ?? "\u8BF7\u7A0D\u540E\u91CD\u8BD5"}`;
|
|
3092
|
+
case "ffmpeg-failed":
|
|
3093
|
+
return "\u26A0\uFE0F \u8BED\u97F3\u8F6C\u7801\u5931\u8D25\uFF0C\u53EF\u80FD\u97F3\u9891\u6587\u4EF6\u635F\u574F\u3002";
|
|
3094
|
+
case "empty":
|
|
3095
|
+
return "\u26A0\uFE0F \u8BED\u97F3\u4E2D\u6CA1\u8BC6\u522B\u5230\u6709\u6548\u5185\u5BB9\u3002";
|
|
3096
|
+
default:
|
|
3097
|
+
return "\u26A0\uFE0F \u8BED\u97F3\u8BC6\u522B\u5931\u8D25\u3002";
|
|
3098
|
+
}
|
|
3099
|
+
})();
|
|
3100
|
+
await this.sendInteractiveCard(
|
|
3101
|
+
msg,
|
|
3102
|
+
buildAckCard({ state: "error", title: "\u{1F399}\uFE0F \u8BED\u97F3\u8F6C\u5199\u5931\u8D25", body: hint })
|
|
3103
|
+
);
|
|
3104
|
+
if (!cleanText) return;
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
if (mediaPaths.length === 0 && !cleanText && !asrText) {
|
|
3108
|
+
await this.sendInteractiveCard(
|
|
3109
|
+
msg,
|
|
3110
|
+
buildAckCard({
|
|
3111
|
+
state: "error",
|
|
3112
|
+
title: "\u274C \u8D44\u6E90\u4E0B\u8F7D\u5931\u8D25",
|
|
3113
|
+
body: `\`${msg.messageType}\` \u8D44\u6E90\u4E0B\u8F7D\u5931\u8D25\uFF0C\u8BF7\u91CD\u53D1\u6216\u68C0\u67E5\u673A\u5668\u4EBA\u6743\u9650\u3002`
|
|
3114
|
+
})
|
|
3115
|
+
);
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
if (!cleanText && !asrText && mediaPaths.length === 0) {
|
|
3120
|
+
this.log.debug("empty text after strip and no media, ignored");
|
|
3121
|
+
return;
|
|
3122
|
+
}
|
|
3123
|
+
const ASR_SYSTEM_HINT = '\uFF08\u4EE5\u4E0A\u7531\u8BED\u97F3\u8F6C\u5199\u5F97\u5230\uFF0C\u53EF\u80FD\u6709\u9519\u522B\u5B57\u3002\u8BF7\u6309\u7528\u6237\u65E5\u5E38\u5BF9\u8BDD\u7684\u53E3\u8BED\u610F\u56FE\u56DE\u7B54\uFF0C\u7B80\u77ED\u81EA\u7136\uFF0C\u4E0D\u8981\u8C08\u8BBA"\u8BED\u97F3"\u6216\u8F6C\u5199\u672C\u8EAB\u3002\uFF09';
|
|
3124
|
+
const effectiveText = asrText ? cleanText ? `[\u8BED\u97F3] ${asrText}
|
|
3125
|
+
|
|
3126
|
+
${cleanText}
|
|
3127
|
+
|
|
3128
|
+
${ASR_SYSTEM_HINT}` : `[\u8BED\u97F3] ${asrText}
|
|
3129
|
+
|
|
3130
|
+
${ASR_SYSTEM_HINT}` : cleanText;
|
|
3131
|
+
const cmd = parseCommand(cleanText);
|
|
3132
|
+
const session = await this.sessions.get(msg.chatId, this.config.workspace.defaultCwd);
|
|
3133
|
+
const needAdmin = (() => {
|
|
3134
|
+
if (!cmd) return false;
|
|
3135
|
+
switch (cmd.kind) {
|
|
3136
|
+
case "cd":
|
|
3137
|
+
case "ws-save":
|
|
3138
|
+
case "ws-use":
|
|
3139
|
+
case "ws-remove":
|
|
3140
|
+
case "reconnect":
|
|
3141
|
+
case "config":
|
|
3142
|
+
return true;
|
|
3143
|
+
default:
|
|
3144
|
+
return false;
|
|
3145
|
+
}
|
|
3146
|
+
})();
|
|
3147
|
+
if (needAdmin && !isAdmin(msg.senderOpenId, this.config)) {
|
|
3148
|
+
await this.sendInteractiveCard(
|
|
3149
|
+
msg,
|
|
3150
|
+
buildAckCard({
|
|
3151
|
+
state: "error",
|
|
3152
|
+
title: "\u{1F6AB} \u6743\u9650\u4E0D\u8DB3",
|
|
3153
|
+
body: "\u6B64\u547D\u4EE4\u4EC5\u7BA1\u7406\u5458\u53EF\u7528\u3002"
|
|
3154
|
+
})
|
|
3155
|
+
);
|
|
3156
|
+
return;
|
|
3157
|
+
}
|
|
3158
|
+
if (cmd) {
|
|
3159
|
+
switch (cmd.kind) {
|
|
3160
|
+
case "help":
|
|
3161
|
+
await this.sendInteractiveCard(msg, buildHelpCard());
|
|
3162
|
+
return;
|
|
3163
|
+
case "pwd": {
|
|
3164
|
+
const wsName = await this.workspaceNameOf(session.currentCwd);
|
|
3165
|
+
const body = wsName ? `\u{1F4C1} \`${session.currentCwd}\`
|
|
3166
|
+
\u{1F5C2}\uFE0F \u5DE5\u4F5C\u533A\uFF1A\`${wsName}\`` : `\u{1F4C1} \`${session.currentCwd}\``;
|
|
3167
|
+
await this.sendInteractiveCard(
|
|
3168
|
+
msg,
|
|
3169
|
+
buildAckCard({ state: "done", title: "\u{1F4C1} \u5F53\u524D\u76EE\u5F55", body })
|
|
3170
|
+
);
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
case "status": {
|
|
3174
|
+
const kiroSid = await this.sessions.getKiroSession(msg.chatId, session.currentCwd);
|
|
3175
|
+
const wsName = await this.workspaceNameOf(session.currentCwd);
|
|
3176
|
+
const idleMin = this.effectiveIdleMinutes(session.idleTimeoutMinutes);
|
|
3177
|
+
const cardOpts = {
|
|
3178
|
+
cwd: session.currentCwd,
|
|
3179
|
+
hasActiveTask: this.getPipeline(msg.chatId).hasActiveTask(),
|
|
3180
|
+
idleMinutes: idleMin,
|
|
3181
|
+
isPerChatOverride: session.idleTimeoutMinutes !== void 0
|
|
3182
|
+
};
|
|
3183
|
+
if (wsName !== void 0) cardOpts.workspaceName = wsName;
|
|
3184
|
+
if (kiroSid !== void 0) cardOpts.kiroSessionId = kiroSid;
|
|
3185
|
+
await this.sendInteractiveCard(msg, buildStatusCard(cardOpts));
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3188
|
+
case "new": {
|
|
3189
|
+
await this.sessions.clearKiroSession(msg.chatId, session.currentCwd);
|
|
3190
|
+
await this.sendInteractiveCard(
|
|
3191
|
+
msg,
|
|
3192
|
+
buildAckCard({
|
|
3193
|
+
state: "done",
|
|
3194
|
+
title: "\u{1F504} \u4F1A\u8BDD\u5DF2\u91CD\u7F6E",
|
|
3195
|
+
body: `\u4E0B\u6B21\u63D0\u95EE\u4F1A\u5728 \`${session.currentCwd}\` \u4E0B\u65B0\u5EFA Kiro session\u3002`
|
|
3196
|
+
})
|
|
3197
|
+
);
|
|
3198
|
+
return;
|
|
3199
|
+
}
|
|
3200
|
+
case "stop": {
|
|
3201
|
+
const ok = this.getPipeline(msg.chatId).abortCurrent();
|
|
3202
|
+
await this.sendInteractiveCard(
|
|
3203
|
+
msg,
|
|
3204
|
+
buildAckCard({
|
|
3205
|
+
state: ok ? "aborted" : "done",
|
|
3206
|
+
title: ok ? "\u23F9 \u5DF2\u53D1\u51FA\u4E2D\u6B62\u4FE1\u53F7" : "\u2139\uFE0F \u6CA1\u6709\u8FDB\u884C\u4E2D\u7684\u4EFB\u52A1",
|
|
3207
|
+
body: ok ? "\u5F53\u524D\u4EFB\u52A1\u6B63\u5728\u6536\u5C3E\uFF0C\u6700\u591A 2 \u79D2\u540E\u5207\u5230\u4E2D\u6B62\u6001\u3002" : "\u5F53\u524D chat \u6CA1\u6709\u6B63\u5728\u8DD1\u7684 Kiro \u4EFB\u52A1\u3002"
|
|
3208
|
+
})
|
|
3209
|
+
);
|
|
3210
|
+
return;
|
|
3211
|
+
}
|
|
3212
|
+
case "cd": {
|
|
3213
|
+
try {
|
|
3214
|
+
const abs = validateCwd(cmd.path, this.config, session.currentCwd);
|
|
3215
|
+
await this.sessions.setCwd(msg.chatId, abs, this.config.workspace.defaultCwd);
|
|
3216
|
+
const wsName = await this.workspaceNameOf(abs);
|
|
3217
|
+
await this.sendInteractiveCard(
|
|
3218
|
+
msg,
|
|
3219
|
+
buildAckCard({
|
|
3220
|
+
state: "done",
|
|
3221
|
+
title: "\u{1F4C1} \u76EE\u5F55\u5DF2\u5207\u6362",
|
|
3222
|
+
body: wsName ? `\`${abs}\`
|
|
3223
|
+
\u{1F5C2}\uFE0F \u5DE5\u4F5C\u533A\uFF1A\`${wsName}\`` : `\`${abs}\``
|
|
3224
|
+
})
|
|
3225
|
+
);
|
|
3226
|
+
} catch (e) {
|
|
3227
|
+
const m = e instanceof SecurityError ? e.message : String(e.message);
|
|
3228
|
+
await this.sendInteractiveCard(
|
|
3229
|
+
msg,
|
|
3230
|
+
buildAckCard({ state: "error", title: "\u274C \u5207\u6362\u5931\u8D25", body: m })
|
|
3231
|
+
);
|
|
3232
|
+
}
|
|
3233
|
+
return;
|
|
3234
|
+
}
|
|
3235
|
+
case "ws-list": {
|
|
3236
|
+
const all = await this.workspaces.list();
|
|
3237
|
+
await this.sendInteractiveCard(
|
|
3238
|
+
msg,
|
|
3239
|
+
buildWorkspaceListCard({ workspaces: all, currentCwd: session.currentCwd })
|
|
3240
|
+
);
|
|
3241
|
+
return;
|
|
3242
|
+
}
|
|
3243
|
+
case "ws-save": {
|
|
3244
|
+
await this.workspaces.save(cmd.name, session.currentCwd);
|
|
3245
|
+
await this.sendInteractiveCard(
|
|
3246
|
+
msg,
|
|
3247
|
+
buildAckCard({
|
|
3248
|
+
state: "done",
|
|
3249
|
+
title: "\u{1F5C2}\uFE0F \u5DE5\u4F5C\u533A\u5DF2\u4FDD\u5B58",
|
|
3250
|
+
body: `\`${cmd.name}\` \u2192 \`${session.currentCwd}\``
|
|
3251
|
+
})
|
|
3252
|
+
);
|
|
3253
|
+
return;
|
|
3254
|
+
}
|
|
3255
|
+
case "ws-use": {
|
|
3256
|
+
const target = await this.workspaces.get(cmd.name);
|
|
3257
|
+
if (!target) {
|
|
3258
|
+
await this.sendInteractiveCard(
|
|
3259
|
+
msg,
|
|
3260
|
+
buildAckCard({
|
|
3261
|
+
state: "error",
|
|
3262
|
+
title: "\u274C \u5DE5\u4F5C\u533A\u4E0D\u5B58\u5728",
|
|
3263
|
+
body: `\u6CA1\u6709\u540D\u4E3A \`${cmd.name}\` \u7684\u5DE5\u4F5C\u533A\u3002\u7528 \`/ws list\` \u67E5\u770B\u5168\u90E8\u3002`
|
|
3264
|
+
})
|
|
3265
|
+
);
|
|
3266
|
+
return;
|
|
3267
|
+
}
|
|
3268
|
+
try {
|
|
3269
|
+
const abs = validateCwd(target, this.config, session.currentCwd);
|
|
3270
|
+
await this.sessions.setCwd(msg.chatId, abs, this.config.workspace.defaultCwd);
|
|
3271
|
+
await this.sendInteractiveCard(
|
|
3272
|
+
msg,
|
|
3273
|
+
buildAckCard({
|
|
3274
|
+
state: "done",
|
|
3275
|
+
title: "\u{1F5C2}\uFE0F \u5DE5\u4F5C\u533A\u5DF2\u5207\u6362",
|
|
3276
|
+
body: `\`${cmd.name}\` \u2192 \`${abs}\``
|
|
3277
|
+
})
|
|
3278
|
+
);
|
|
3279
|
+
} catch (e) {
|
|
3280
|
+
const m = e instanceof SecurityError ? e.message : String(e.message);
|
|
3281
|
+
await this.sendInteractiveCard(
|
|
3282
|
+
msg,
|
|
3283
|
+
buildAckCard({ state: "error", title: "\u274C \u5207\u6362\u5931\u8D25", body: m })
|
|
3284
|
+
);
|
|
3285
|
+
}
|
|
3286
|
+
return;
|
|
3287
|
+
}
|
|
3288
|
+
case "ws-remove": {
|
|
3289
|
+
const ok = await this.workspaces.remove(cmd.name);
|
|
3290
|
+
await this.sendInteractiveCard(
|
|
3291
|
+
msg,
|
|
3292
|
+
buildAckCard({
|
|
3293
|
+
state: ok ? "done" : "error",
|
|
3294
|
+
title: ok ? "\u{1F5D1}\uFE0F \u5DE5\u4F5C\u533A\u5DF2\u5220\u9664" : "\u274C \u5DE5\u4F5C\u533A\u4E0D\u5B58\u5728",
|
|
3295
|
+
body: ok ? `\u5DF2\u5220\u9664 \`${cmd.name}\`` : `\u6CA1\u6709\u540D\u4E3A \`${cmd.name}\` \u7684\u5DE5\u4F5C\u533A`
|
|
3296
|
+
})
|
|
3297
|
+
);
|
|
3298
|
+
return;
|
|
3299
|
+
}
|
|
3300
|
+
case "timeout": {
|
|
3301
|
+
await this.handleTimeoutCmd(msg, session, cmd);
|
|
3302
|
+
return;
|
|
3303
|
+
}
|
|
3304
|
+
case "reconnect": {
|
|
3305
|
+
await this.sendInteractiveCard(
|
|
3306
|
+
msg,
|
|
3307
|
+
buildAckCard({
|
|
3308
|
+
state: "done",
|
|
3309
|
+
title: "\u{1F504} \u6B63\u5728\u91CD\u8FDE",
|
|
3310
|
+
body: "\u6B63\u5728\u91CD\u65B0\u5EFA\u7ACB\u98DE\u4E66 WebSocket \u8FDE\u63A5\u2026"
|
|
3311
|
+
})
|
|
3312
|
+
);
|
|
3313
|
+
if (this.onReconnect) {
|
|
3314
|
+
try {
|
|
3315
|
+
await this.onReconnect();
|
|
3316
|
+
} catch (e) {
|
|
3317
|
+
this.log.warn({ err: e }, "reconnect failed");
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
return;
|
|
3321
|
+
}
|
|
3322
|
+
case "doctor": {
|
|
3323
|
+
await this.handleDoctorCmd(msg, cmd.description, session.currentCwd);
|
|
3324
|
+
return;
|
|
3325
|
+
}
|
|
3326
|
+
case "model": {
|
|
3327
|
+
await this.handleModelCmd(msg, cmd, session.currentCwd);
|
|
3328
|
+
return;
|
|
3329
|
+
}
|
|
3330
|
+
case "config": {
|
|
3331
|
+
await this.handleConfigCmd(msg);
|
|
3332
|
+
return;
|
|
3333
|
+
}
|
|
3334
|
+
case "kiro-internal": {
|
|
3335
|
+
const body = [
|
|
3336
|
+
`\u2753 \`/${cmd.name}\` \u662F kiro-cli \u7684**\u4EA4\u4E92\u5F0F TUI** \u547D\u4EE4\uFF0C\u6865\u63A5\u5668\u8DD1\u7684\u662F\u975E\u4EA4\u4E92\u6A21\u5F0F\uFF08\`--no-interactive\`\uFF09\uFF0C\u65E0\u6CD5\u6267\u884C\u3002`,
|
|
3337
|
+
"",
|
|
3338
|
+
"**\u600E\u4E48\u529E**",
|
|
3339
|
+
cmd.name === "model" || cmd.name === "agent" ? `\u8981\u5207\u6362 ${cmd.name === "model" ? "\u6A21\u578B" : "agent"}\uFF0C\u8BF7\u7F16\u8F91 \`~/.lark-kiro-bridge/config.json\` \u91CC\u7684 \`kiro.${cmd.name}\` \u5B57\u6BB5\uFF0C\u7136\u540E \`/reconnect\` \u751F\u6548\u3002` : `\u8FD9\u6761\u547D\u4EE4\u53EA\u5728\u7EC8\u7AEF\u8DD1 \`kiro-cli\` \u65F6\u53EF\u7528\uFF0C\u6865\u63A5\u5668\u65E0\u6CD5\u4EE3\u7406\u3002`,
|
|
3340
|
+
"",
|
|
3341
|
+
"**\u6865\u63A5\u5668\u81EA\u8EAB\u7684\u547D\u4EE4** \u7528 `/help` \u67E5\u770B\u3002"
|
|
3342
|
+
].join("\n");
|
|
3343
|
+
await this.replyErrorCard(msg, body, session.currentCwd);
|
|
3344
|
+
return;
|
|
3345
|
+
}
|
|
3346
|
+
case "unknown":
|
|
3347
|
+
break;
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
await this.runKiroTask(
|
|
3351
|
+
msg,
|
|
3352
|
+
effectiveText,
|
|
3353
|
+
session.currentCwd,
|
|
3354
|
+
mediaPaths,
|
|
3355
|
+
session.idleTimeoutMinutes
|
|
3356
|
+
);
|
|
3357
|
+
}
|
|
3358
|
+
async workspaceNameOf(cwd) {
|
|
3359
|
+
const all = await this.workspaces.list();
|
|
3360
|
+
for (const [name, p] of Object.entries(all)) {
|
|
3361
|
+
if (p === cwd) return name;
|
|
3362
|
+
}
|
|
3363
|
+
return void 0;
|
|
3364
|
+
}
|
|
3365
|
+
/**
|
|
3366
|
+
* 计算某个 chat 实际生效的 idle watchdog 分钟数。
|
|
3367
|
+
* - per-chat override 存在(包括 0)→ 优先用它
|
|
3368
|
+
* - 否则用 config.kiro.idleTimeoutMinutes
|
|
3369
|
+
*/
|
|
3370
|
+
effectiveIdleMinutes(perChatOverride) {
|
|
3371
|
+
if (perChatOverride !== void 0) return perChatOverride;
|
|
3372
|
+
return this.config.kiro.idleTimeoutMinutes;
|
|
3373
|
+
}
|
|
3374
|
+
async handleTimeoutCmd(msg, session, cmd) {
|
|
3375
|
+
if (cmd.mode === "show") {
|
|
3376
|
+
const eff2 = this.effectiveIdleMinutes(session.idleTimeoutMinutes);
|
|
3377
|
+
const body = [
|
|
3378
|
+
`\u5F53\u524D\u9608\u503C\uFF1A**${eff2 > 0 ? `${eff2} \u5206\u949F` : "\u5173\u95ED"}**${session.idleTimeoutMinutes !== void 0 ? "\uFF08per-chat \u8986\u76D6\uFF09" : "\uFF08\u5168\u5C40\u9ED8\u8BA4\uFF09"}`,
|
|
3379
|
+
"",
|
|
3380
|
+
"<font color='grey'>\u7528\u6CD5</font>",
|
|
3381
|
+
"`/timeout 10` \u2014 \u6539\u6210 10 \u5206\u949F",
|
|
3382
|
+
"`/timeout off` \u2014 \u5173\u95ED",
|
|
3383
|
+
"`/timeout default` \u2014 \u56DE\u5F52\u5168\u5C40\u9ED8\u8BA4"
|
|
3384
|
+
].join("\n");
|
|
3385
|
+
await this.sendInteractiveCard(
|
|
3386
|
+
msg,
|
|
3387
|
+
buildAckCard({ state: "done", title: "\u23F1 Idle Watchdog", body })
|
|
3388
|
+
);
|
|
3389
|
+
return;
|
|
3390
|
+
}
|
|
3391
|
+
if (cmd.mode === "set") {
|
|
3392
|
+
await this.sessions.setIdleTimeout(msg.chatId, cmd.minutes, this.config.workspace.defaultCwd);
|
|
3393
|
+
await this.sendInteractiveCard(
|
|
3394
|
+
msg,
|
|
3395
|
+
buildAckCard({
|
|
3396
|
+
state: "done",
|
|
3397
|
+
title: "\u23F1 \u5DF2\u8BBE\u7F6E",
|
|
3398
|
+
body: `idle watchdog \u2192 \`${cmd.minutes}\` \u5206\u949F`
|
|
3399
|
+
})
|
|
3400
|
+
);
|
|
3401
|
+
return;
|
|
3402
|
+
}
|
|
3403
|
+
if (cmd.mode === "off") {
|
|
3404
|
+
await this.sessions.setIdleTimeout(msg.chatId, 0, this.config.workspace.defaultCwd);
|
|
3405
|
+
await this.sendInteractiveCard(
|
|
3406
|
+
msg,
|
|
3407
|
+
buildAckCard({
|
|
3408
|
+
state: "done",
|
|
3409
|
+
title: "\u23F1 \u5DF2\u5173\u95ED",
|
|
3410
|
+
body: "\u5F53\u524D chat \u4E0D\u4F1A\u56E0\u4E3A\u957F\u65F6\u95F4\u65E0\u8F93\u51FA\u800C\u88AB\u81EA\u52A8\u7EC8\u6B62\u3002"
|
|
3411
|
+
})
|
|
3412
|
+
);
|
|
3413
|
+
return;
|
|
3414
|
+
}
|
|
3415
|
+
await this.sessions.setIdleTimeout(msg.chatId, void 0, this.config.workspace.defaultCwd);
|
|
3416
|
+
const eff = this.effectiveIdleMinutes(void 0);
|
|
3417
|
+
await this.sendInteractiveCard(
|
|
3418
|
+
msg,
|
|
3419
|
+
buildAckCard({
|
|
3420
|
+
state: "done",
|
|
3421
|
+
title: "\u23F1 \u5DF2\u6062\u590D\u9ED8\u8BA4",
|
|
3422
|
+
body: `\u56DE\u5F52\u5168\u5C40\u9ED8\u8BA4\uFF1A${eff > 0 ? `${eff} \u5206\u949F` : "\u5173\u95ED"}`
|
|
3423
|
+
})
|
|
3424
|
+
);
|
|
3425
|
+
}
|
|
3426
|
+
/**
|
|
3427
|
+
* /doctor [描述]
|
|
3428
|
+
* 把最近 200 行结构化日志 + 用户描述拼成 prompt 喂给 Kiro 自诊断。
|
|
3429
|
+
* 走标准 runKiroTask 流程,享受所有流式卡片和 watchdog。
|
|
3430
|
+
*/
|
|
3431
|
+
async handleDoctorCmd(msg, description, cwd) {
|
|
3432
|
+
const lines = readRecentLogLines(200);
|
|
3433
|
+
const userDesc = description.trim() || "\uFF08\u65E0\uFF09";
|
|
3434
|
+
const prompt = [
|
|
3435
|
+
"\u4F60\u662F lark-kiro-bridge \u7684\u8FD0\u7EF4\u52A9\u624B\u3002\u4E0B\u9762\u662F\u8FD9\u4E2A\u6865\u63A5\u5668\u6700\u8FD1\u7684\u7ED3\u6784\u5316\u65E5\u5FD7\uFF08NDJSON\uFF09\uFF0C",
|
|
3436
|
+
"\u4EE5\u53CA\u7528\u6237\u63CF\u8FF0\u7684\u95EE\u9898\u3002\u8BF7\u57FA\u4E8E\u65E5\u5FD7\u627E\u51FA\u53EF\u80FD\u7684\u6545\u969C\u70B9\uFF0C\u7ED9\u51FA\u8BCA\u65AD\u7ED3\u8BBA\u548C\u4FEE\u590D\u5EFA\u8BAE\u3002",
|
|
3437
|
+
"\u53EA\u770B\u65E5\u5FD7\uFF0C\u4E0D\u8981\u5047\u8BBE\u5176\u4ED6\u72B6\u6001\u3002",
|
|
3438
|
+
"",
|
|
3439
|
+
"**\u7528\u6237\u63CF\u8FF0**",
|
|
3440
|
+
userDesc,
|
|
3441
|
+
"",
|
|
3442
|
+
"**\u6700\u8FD1\u65E5\u5FD7\uFF08\u6700\u591A 200 \u884C\uFF0C\u5DF2\u622A\u77ED\u957F\u884C\uFF09**",
|
|
3443
|
+
"```",
|
|
3444
|
+
lines.length > 0 ? lines.join("\n") : "\uFF08\u65E0\u65E5\u5FD7\uFF09",
|
|
3445
|
+
"```"
|
|
3446
|
+
].join("\n");
|
|
3447
|
+
const session = await this.sessions.get(msg.chatId, this.config.workspace.defaultCwd);
|
|
3448
|
+
await this.runKiroTask(msg, prompt, cwd, [], session.idleTimeoutMinutes);
|
|
3449
|
+
}
|
|
3450
|
+
/**
|
|
3451
|
+
* /model → 列出所有模型 + 当前选中(漂亮按钮卡片)
|
|
3452
|
+
* /model <name> → 切换模型,写入 config.json,立即生效
|
|
3453
|
+
* /model auto → 清除模型覆盖,回归 kiro-cli 默认(auto)
|
|
3454
|
+
*
|
|
3455
|
+
* 短名容错:/model sonnet-4.6 → 自动补 claude- 前缀
|
|
3456
|
+
*
|
|
3457
|
+
* 设计取舍:
|
|
3458
|
+
* - 切模型只改全局 config.json,不做 per-chat 覆盖(先做最少必要)
|
|
3459
|
+
* - 切完不需要 reconnect,下一条消息直接生效(spawn kiro-cli 时读最新 config)
|
|
3460
|
+
*/
|
|
3461
|
+
async handleModelCmd(msg, cmd, _cwd) {
|
|
3462
|
+
if (cmd.mode === "show") {
|
|
3463
|
+
await this.sendInteractiveCardAsync(
|
|
3464
|
+
msg,
|
|
3465
|
+
buildLoadingCard("\u67E5\u8BE2\u53EF\u7528\u6A21\u578B\u2026", "\u{1F39B}\uFE0F \u52A0\u8F7D\u6A21\u578B\u5217\u8868"),
|
|
3466
|
+
async () => {
|
|
3467
|
+
const list = await listModels(this.config.kiro.binPath);
|
|
3468
|
+
if (!list) {
|
|
3469
|
+
return buildAckCard({
|
|
3470
|
+
state: "error",
|
|
3471
|
+
body: "\u65E0\u6CD5\u83B7\u53D6\u6A21\u578B\u5217\u8868\uFF0C\u53EF\u80FD\u662F kiro-cli \u6CA1\u767B\u5F55\u6216\u4E0D\u5728 PATH\u3002\n\u7528 `/doctor` \u8BA9 Kiro \u81EA\u5DF1\u770B\u65E5\u5FD7\u3002"
|
|
3472
|
+
});
|
|
3473
|
+
}
|
|
3474
|
+
const current = this.config.kiro.model ?? list.defaultModel ?? "auto";
|
|
3475
|
+
return buildModelPickerCard({ current, list });
|
|
3476
|
+
}
|
|
3477
|
+
);
|
|
3478
|
+
return;
|
|
3479
|
+
}
|
|
3480
|
+
if (cmd.mode === "reset") {
|
|
3481
|
+
this.config = patchAndSaveConfig(this.config, (draft) => {
|
|
3482
|
+
delete draft.kiro.model;
|
|
3483
|
+
});
|
|
3484
|
+
clearModelCache();
|
|
3485
|
+
const list = await listModels(this.config.kiro.binPath);
|
|
3486
|
+
const fallback = list?.defaultModel ?? "auto";
|
|
3487
|
+
await this.sendInteractiveCard(
|
|
3488
|
+
msg,
|
|
3489
|
+
buildAckCard({
|
|
3490
|
+
state: "done",
|
|
3491
|
+
title: "\u2705 \u6A21\u578B\u5DF2\u6062\u590D\u9ED8\u8BA4",
|
|
3492
|
+
body: `\u5DF2\u6E05\u9664\u6A21\u578B\u8986\u76D6\uFF0C\u56DE\u5F52 kiro-cli \u9ED8\u8BA4\uFF08\`${fallback}\`\uFF09`
|
|
3493
|
+
})
|
|
3494
|
+
);
|
|
3495
|
+
return;
|
|
3496
|
+
}
|
|
3497
|
+
await this.sendInteractiveCardAsync(
|
|
3498
|
+
msg,
|
|
3499
|
+
buildLoadingCard(`\u5207\u6362\u5230 \`${cmd.name}\` \u2026`, "\u{1F39B}\uFE0F \u5207\u6362\u6A21\u578B"),
|
|
3500
|
+
async () => {
|
|
3501
|
+
const list = await listModels(this.config.kiro.binPath);
|
|
3502
|
+
const target = this.resolveModelName(cmd.name, list);
|
|
3503
|
+
if (list && target === void 0) {
|
|
3504
|
+
const valid = list.models.map((m) => `\`${m.name}\``).join("\u3001");
|
|
3505
|
+
return buildAckCard({
|
|
3506
|
+
state: "error",
|
|
3507
|
+
title: "\u274C \u6A21\u578B\u4E0D\u5B58\u5728",
|
|
3508
|
+
body: `\u6CA1\u6709\u540D\u4E3A \`${cmd.name}\` \u7684\u6A21\u578B\u3002
|
|
3509
|
+
|
|
3510
|
+
\u53EF\u7528\uFF1A${valid}
|
|
3511
|
+
|
|
3512
|
+
\u7528 \`/model\` \u67E5\u770B\u5B8C\u6574\u5217\u8868\u3002`
|
|
3513
|
+
});
|
|
3514
|
+
}
|
|
3515
|
+
const finalName = target ?? cmd.name;
|
|
3516
|
+
this.config = patchAndSaveConfig(this.config, (draft) => {
|
|
3517
|
+
draft.kiro.model = finalName;
|
|
3518
|
+
});
|
|
3519
|
+
return buildAckCard({
|
|
3520
|
+
state: "done",
|
|
3521
|
+
title: "\u2705 \u6A21\u578B\u5DF2\u5207\u6362",
|
|
3522
|
+
body: `\u5DF2\u5207\u6362\u5230 \`${finalName}\`\uFF08\u4E0B\u4E00\u6761\u6D88\u606F\u751F\u6548\uFF09`
|
|
3523
|
+
});
|
|
3524
|
+
}
|
|
3525
|
+
);
|
|
3526
|
+
}
|
|
3527
|
+
/**
|
|
3528
|
+
* 模型名解析:
|
|
3529
|
+
* - 列表里精确匹配 → 直接返回
|
|
3530
|
+
* - 列表里有 "claude-<name>" → 返回带前缀的全名(短名容错)
|
|
3531
|
+
* - 都不匹配 → 返回 undefined(让上层报错)
|
|
3532
|
+
* 列表为空(fetch 失败)时直接返回原名,不阻塞。
|
|
3533
|
+
*/
|
|
3534
|
+
resolveModelName(name, list) {
|
|
3535
|
+
if (!list) return name;
|
|
3536
|
+
if (list.models.some((m) => m.name === name)) return name;
|
|
3537
|
+
const prefixed = `claude-${name}`;
|
|
3538
|
+
if (list.models.some((m) => m.name === prefixed)) return prefixed;
|
|
3539
|
+
return void 0;
|
|
3540
|
+
}
|
|
3541
|
+
/**
|
|
3542
|
+
* 发送一张飞书 v2 交互卡片(带按钮的那种)作为对原消息的回复。
|
|
3543
|
+
* 跟 replyDoneCard 不同:不走 CardRenderer 的状态机,发一次就完事,
|
|
3544
|
+
* 不会再 patch;按钮回调走 onCardAction 流程。
|
|
3545
|
+
*/
|
|
3546
|
+
async sendInteractiveCard(msg, card) {
|
|
3547
|
+
try {
|
|
3548
|
+
await this.lark.replyCard(msg.messageId, card);
|
|
3549
|
+
} catch (e) {
|
|
3550
|
+
this.log.error({ err: e }, "sendInteractiveCard failed; falling back to text");
|
|
3551
|
+
try {
|
|
3552
|
+
await this.lark.sendText(msg.chatId, "\u274C \u5361\u7247\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u65E5\u5FD7");
|
|
3553
|
+
} catch {
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
/**
|
|
3558
|
+
* 命令型卡片的"先占位再 patch"模式。
|
|
3559
|
+
*
|
|
3560
|
+
* 用于命令本身需要做异步工作(spawn kiro-cli 等)才能算出最终卡片内容的场景:
|
|
3561
|
+
* 1. 先用 placeholderCard 立刻 reply 出去,让用户看到反馈
|
|
3562
|
+
* 2. 异步跑 buildFinalCard()
|
|
3563
|
+
* 3. 用 patchCard 替换成最终卡片
|
|
3564
|
+
*
|
|
3565
|
+
* 失败时直接发 fallback 错误卡片,不让用户卡在 placeholder。
|
|
3566
|
+
*/
|
|
3567
|
+
async sendInteractiveCardAsync(msg, placeholderCard, buildFinalCard) {
|
|
3568
|
+
let placeholderMessageId;
|
|
3569
|
+
try {
|
|
3570
|
+
placeholderMessageId = await this.lark.replyCard(msg.messageId, placeholderCard);
|
|
3571
|
+
} catch (e) {
|
|
3572
|
+
this.log.error({ err: e }, "placeholder card send failed");
|
|
3573
|
+
}
|
|
3574
|
+
let finalCard;
|
|
3575
|
+
try {
|
|
3576
|
+
finalCard = await buildFinalCard();
|
|
3577
|
+
} catch (e) {
|
|
3578
|
+
this.log.error({ err: e }, "buildFinalCard threw");
|
|
3579
|
+
finalCard = buildAckCard({
|
|
3580
|
+
state: "error",
|
|
3581
|
+
body: `\u274C \u547D\u4EE4\u6267\u884C\u5931\u8D25\uFF1A${e.message}`
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3584
|
+
if (placeholderMessageId) {
|
|
3585
|
+
try {
|
|
3586
|
+
await this.lark.patchCard(placeholderMessageId, finalCard);
|
|
3587
|
+
} catch (e) {
|
|
3588
|
+
this.log.error({ err: e }, "patch final card failed; sending fresh");
|
|
3589
|
+
await this.sendInteractiveCard(msg, finalCard);
|
|
3590
|
+
}
|
|
3591
|
+
} else {
|
|
3592
|
+
await this.sendInteractiveCard(msg, finalCard);
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3595
|
+
/**
|
|
3596
|
+
* 直接发卡片到 chatId(不 reply 任何特定消息)。
|
|
3597
|
+
* cardAction handler 用这个——按钮触发时不 reply 那条卡片本身(嵌套体验差),
|
|
3598
|
+
* 而是发新消息。
|
|
3599
|
+
*/
|
|
3600
|
+
async sendCardToChat(chatId, card) {
|
|
3601
|
+
try {
|
|
3602
|
+
await this.lark.sendCard(chatId, card);
|
|
3603
|
+
} catch (e) {
|
|
3604
|
+
this.log.error({ err: e }, "sendCardToChat failed; falling back to text");
|
|
3605
|
+
try {
|
|
3606
|
+
await this.lark.sendText(chatId, "\u274C \u5361\u7247\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u65E5\u5FD7");
|
|
3607
|
+
} catch {
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
/**
|
|
3612
|
+
* 处理用户点了卡片按钮的事件。
|
|
3613
|
+
* value 约定字段:{ action: 'xxx.yyy', ...payload }
|
|
3614
|
+
*
|
|
3615
|
+
* 安全:button 触发的命令也会经过 admin 校验(调用 needAdminForAction)。
|
|
3616
|
+
*/
|
|
3617
|
+
async handleCardAction(evt) {
|
|
3618
|
+
const chatTypeGuess = "group";
|
|
3619
|
+
if (!isUserAllowed(evt.senderOpenId, evt.chatId, chatTypeGuess, this.config)) {
|
|
3620
|
+
this.log.debug({ user: evt.senderOpenId }, "card action dropped by access control");
|
|
3621
|
+
return;
|
|
3622
|
+
}
|
|
3623
|
+
const action = String(evt.value["action"] ?? "");
|
|
3624
|
+
if (!action) {
|
|
3625
|
+
this.log.debug({ value: evt.value }, 'card action without "action" field, ignored');
|
|
3626
|
+
return;
|
|
3627
|
+
}
|
|
3628
|
+
if (this.actionNeedsAdmin(action) && !isAdmin(evt.senderOpenId, this.config)) {
|
|
3629
|
+
await this.sendCardToChat(
|
|
3630
|
+
evt.chatId,
|
|
3631
|
+
buildAckCard({ state: "error", body: "\u6B64\u64CD\u4F5C\u4EC5\u7BA1\u7406\u5458\u53EF\u7528" })
|
|
3632
|
+
);
|
|
3633
|
+
return;
|
|
3634
|
+
}
|
|
3635
|
+
const session = await this.sessions.get(evt.chatId, this.config.workspace.defaultCwd);
|
|
3636
|
+
switch (action) {
|
|
3637
|
+
case "model.show": {
|
|
3638
|
+
const list = await listModels(this.config.kiro.binPath);
|
|
3639
|
+
const current = this.config.kiro.model ?? list?.defaultModel ?? "auto";
|
|
3640
|
+
if (!list) {
|
|
3641
|
+
await this.sendCardToChat(
|
|
3642
|
+
evt.chatId,
|
|
3643
|
+
buildAckCard({ state: "error", body: "\u65E0\u6CD5\u83B7\u53D6\u6A21\u578B\u5217\u8868" })
|
|
3644
|
+
);
|
|
3645
|
+
return;
|
|
3646
|
+
}
|
|
3647
|
+
await this.sendCardToChat(evt.chatId, buildModelPickerCard({ current, list }));
|
|
3648
|
+
return;
|
|
3649
|
+
}
|
|
3650
|
+
case "model.refresh": {
|
|
3651
|
+
clearModelCache();
|
|
3652
|
+
const list = await listModels(this.config.kiro.binPath);
|
|
3653
|
+
const current = this.config.kiro.model ?? list?.defaultModel ?? "auto";
|
|
3654
|
+
if (!list) {
|
|
3655
|
+
await this.sendCardToChat(evt.chatId, buildAckCard({ state: "error", body: "\u5237\u65B0\u5931\u8D25" }));
|
|
3656
|
+
return;
|
|
3657
|
+
}
|
|
3658
|
+
await this.sendCardToChat(evt.chatId, buildModelPickerCard({ current, list }));
|
|
3659
|
+
return;
|
|
3660
|
+
}
|
|
3661
|
+
case "model.set": {
|
|
3662
|
+
const name = String(evt.value["name"] ?? "").trim();
|
|
3663
|
+
if (!name) return;
|
|
3664
|
+
const list = await listModels(this.config.kiro.binPath);
|
|
3665
|
+
const target = this.resolveModelName(name, list);
|
|
3666
|
+
if (list && target === void 0) {
|
|
3667
|
+
await this.sendCardToChat(
|
|
3668
|
+
evt.chatId,
|
|
3669
|
+
buildAckCard({ state: "error", body: `\u6CA1\u6709\u540D\u4E3A \`${name}\` \u7684\u6A21\u578B` })
|
|
3670
|
+
);
|
|
3671
|
+
return;
|
|
3672
|
+
}
|
|
3673
|
+
const finalName = target ?? name;
|
|
3674
|
+
this.config = patchAndSaveConfig(this.config, (draft) => {
|
|
3675
|
+
draft.kiro.model = finalName;
|
|
3676
|
+
});
|
|
3677
|
+
await this.sendCardToChat(
|
|
3678
|
+
evt.chatId,
|
|
3679
|
+
buildAckCard({
|
|
3680
|
+
state: "done",
|
|
3681
|
+
body: `\u5DF2\u5207\u6362\u6A21\u578B\uFF1A\`${finalName}\`\uFF08\u4E0B\u4E00\u6761\u6D88\u606F\u751F\u6548\uFF09`
|
|
3682
|
+
})
|
|
3683
|
+
);
|
|
3684
|
+
return;
|
|
3685
|
+
}
|
|
3686
|
+
case "model.reset": {
|
|
3687
|
+
this.config = patchAndSaveConfig(this.config, (draft) => {
|
|
3688
|
+
delete draft.kiro.model;
|
|
3689
|
+
});
|
|
3690
|
+
clearModelCache();
|
|
3691
|
+
const list = await listModels(this.config.kiro.binPath);
|
|
3692
|
+
const fallback = list?.defaultModel ?? "auto";
|
|
3693
|
+
await this.sendCardToChat(
|
|
3694
|
+
evt.chatId,
|
|
3695
|
+
buildAckCard({
|
|
3696
|
+
state: "done",
|
|
3697
|
+
body: `\u5DF2\u6E05\u9664\u6A21\u578B\u8986\u76D6\uFF0C\u56DE\u5F52 \`${fallback}\``
|
|
3698
|
+
})
|
|
3699
|
+
);
|
|
3700
|
+
return;
|
|
3701
|
+
}
|
|
3702
|
+
case "session.new": {
|
|
3703
|
+
await this.sessions.clearKiroSession(evt.chatId, session.currentCwd);
|
|
3704
|
+
await this.sendCardToChat(
|
|
3705
|
+
evt.chatId,
|
|
3706
|
+
buildAckCard({
|
|
3707
|
+
state: "done",
|
|
3708
|
+
body: `\u5DF2\u91CD\u7F6E \`${session.currentCwd}\` \u4E0B\u7684\u4F1A\u8BDD`
|
|
3709
|
+
})
|
|
3710
|
+
);
|
|
3711
|
+
return;
|
|
3712
|
+
}
|
|
3713
|
+
case "session.stop": {
|
|
3714
|
+
const ok = this.getPipeline(evt.chatId).abortCurrent();
|
|
3715
|
+
await this.sendCardToChat(
|
|
3716
|
+
evt.chatId,
|
|
3717
|
+
buildAckCard({
|
|
3718
|
+
state: ok ? "aborted" : "done",
|
|
3719
|
+
body: ok ? "\u5DF2\u53D1\u51FA\u4E2D\u6B62\u4FE1\u53F7" : "\u5F53\u524D\u6CA1\u6709\u8FDB\u884C\u4E2D\u7684\u4EFB\u52A1"
|
|
3720
|
+
})
|
|
3721
|
+
);
|
|
3722
|
+
return;
|
|
3723
|
+
}
|
|
3724
|
+
case "session.status": {
|
|
3725
|
+
const kiroSid = await this.sessions.getKiroSession(evt.chatId, session.currentCwd);
|
|
3726
|
+
const wsName = await this.workspaceNameOf(session.currentCwd);
|
|
3727
|
+
const idleMin = this.effectiveIdleMinutes(session.idleTimeoutMinutes);
|
|
3728
|
+
const cardOpts = {
|
|
3729
|
+
cwd: session.currentCwd,
|
|
3730
|
+
hasActiveTask: this.getPipeline(evt.chatId).hasActiveTask(),
|
|
3731
|
+
idleMinutes: idleMin,
|
|
3732
|
+
isPerChatOverride: session.idleTimeoutMinutes !== void 0
|
|
3733
|
+
};
|
|
3734
|
+
if (wsName !== void 0) cardOpts.workspaceName = wsName;
|
|
3735
|
+
if (kiroSid !== void 0) cardOpts.kiroSessionId = kiroSid;
|
|
3736
|
+
await this.sendCardToChat(evt.chatId, buildStatusCard(cardOpts));
|
|
3737
|
+
return;
|
|
3738
|
+
}
|
|
3739
|
+
case "ws.list": {
|
|
3740
|
+
const all = await this.workspaces.list();
|
|
3741
|
+
await this.sendCardToChat(
|
|
3742
|
+
evt.chatId,
|
|
3743
|
+
buildWorkspaceListCard({
|
|
3744
|
+
workspaces: all,
|
|
3745
|
+
currentCwd: session.currentCwd
|
|
3746
|
+
})
|
|
3747
|
+
);
|
|
3748
|
+
return;
|
|
3749
|
+
}
|
|
3750
|
+
case "ws.use": {
|
|
3751
|
+
const name = String(evt.value["name"] ?? "").trim();
|
|
3752
|
+
if (!name) return;
|
|
3753
|
+
const target = await this.workspaces.get(name);
|
|
3754
|
+
if (!target) {
|
|
3755
|
+
await this.sendCardToChat(
|
|
3756
|
+
evt.chatId,
|
|
3757
|
+
buildAckCard({ state: "error", body: `\u6CA1\u6709\u540D\u4E3A \`${name}\` \u7684\u5DE5\u4F5C\u533A` })
|
|
3758
|
+
);
|
|
3759
|
+
return;
|
|
3760
|
+
}
|
|
3761
|
+
try {
|
|
3762
|
+
const abs = validateCwd(target, this.config, session.currentCwd);
|
|
3763
|
+
await this.sessions.setCwd(evt.chatId, abs, this.config.workspace.defaultCwd);
|
|
3764
|
+
await this.sendCardToChat(
|
|
3765
|
+
evt.chatId,
|
|
3766
|
+
buildAckCard({
|
|
3767
|
+
state: "done",
|
|
3768
|
+
body: `\u5DF2\u5207\u6362\u5230\u5DE5\u4F5C\u533A \`${name}\` \u2192 \`${abs}\``
|
|
3769
|
+
})
|
|
3770
|
+
);
|
|
3771
|
+
} catch (e) {
|
|
3772
|
+
const m = e instanceof SecurityError ? e.message : String(e.message);
|
|
3773
|
+
await this.sendCardToChat(evt.chatId, buildAckCard({ state: "error", body: m }));
|
|
3774
|
+
}
|
|
3775
|
+
return;
|
|
3776
|
+
}
|
|
3777
|
+
case "config.show":
|
|
3778
|
+
case "config.edit": {
|
|
3779
|
+
const isEditMode = action === "config.edit";
|
|
3780
|
+
const card = isEditMode ? buildConfigFormCard({
|
|
3781
|
+
allowedUsers: this.config.access.allowedUsers,
|
|
3782
|
+
allowedChats: this.config.access.allowedChats,
|
|
3783
|
+
admins: this.config.access.admins,
|
|
3784
|
+
requireMentionInGroup: this.config.preferences.requireMentionInGroup,
|
|
3785
|
+
idleTimeoutMinutes: this.config.kiro.idleTimeoutMinutes
|
|
3786
|
+
}) : buildConfigViewCard({
|
|
3787
|
+
allowedUsers: this.config.access.allowedUsers,
|
|
3788
|
+
allowedChats: this.config.access.allowedChats,
|
|
3789
|
+
admins: this.config.access.admins,
|
|
3790
|
+
requireMentionInGroup: this.config.preferences.requireMentionInGroup,
|
|
3791
|
+
idleTimeoutMinutes: this.config.kiro.idleTimeoutMinutes,
|
|
3792
|
+
cardUpdateIntervalMs: this.config.preferences.cardUpdateIntervalMs,
|
|
3793
|
+
isAdmin: isAdmin(evt.senderOpenId, this.config)
|
|
3794
|
+
});
|
|
3795
|
+
await this.sendCardToChat(evt.chatId, card);
|
|
3796
|
+
return;
|
|
3797
|
+
}
|
|
3798
|
+
case "config.submit": {
|
|
3799
|
+
await this.handleConfigSubmit(evt);
|
|
3800
|
+
return;
|
|
3801
|
+
}
|
|
3802
|
+
default:
|
|
3803
|
+
this.log.debug({ action }, "unknown card action, ignored");
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
/** 哪些 action 是写操作,需要 admin */
|
|
3807
|
+
actionNeedsAdmin(action) {
|
|
3808
|
+
return action === "model.set" || action === "model.reset" || action === "ws.use" || action === "session.new" || action === "config.edit" || action === "config.submit";
|
|
3809
|
+
}
|
|
3810
|
+
/**
|
|
3811
|
+
* /config 命令:展示当前配置(只读卡片,admin 可见编辑按钮)
|
|
3812
|
+
*/
|
|
3813
|
+
async handleConfigCmd(msg) {
|
|
3814
|
+
const card = buildConfigViewCard({
|
|
3815
|
+
allowedUsers: this.config.access.allowedUsers,
|
|
3816
|
+
allowedChats: this.config.access.allowedChats,
|
|
3817
|
+
admins: this.config.access.admins,
|
|
3818
|
+
requireMentionInGroup: this.config.preferences.requireMentionInGroup,
|
|
3819
|
+
idleTimeoutMinutes: this.config.kiro.idleTimeoutMinutes,
|
|
3820
|
+
cardUpdateIntervalMs: this.config.preferences.cardUpdateIntervalMs,
|
|
3821
|
+
isAdmin: isAdmin(msg.senderOpenId, this.config)
|
|
3822
|
+
});
|
|
3823
|
+
await this.sendInteractiveCard(msg, card);
|
|
3824
|
+
}
|
|
3825
|
+
/**
|
|
3826
|
+
* 处理 config 表单提交。
|
|
3827
|
+
*
|
|
3828
|
+
* 流程:
|
|
3829
|
+
* 1. 解析 form_value(用户输入的逗号分隔列表 / 整数 / yes/no)
|
|
3830
|
+
* 2. 用 validateAccessChange 校验,防止把自己锁出去
|
|
3831
|
+
* 3. patchAndSaveConfig 落盘 + 立即生效
|
|
3832
|
+
* 4. 回一张确认卡片(同时显示新的 config view)
|
|
3833
|
+
*/
|
|
3834
|
+
async handleConfigSubmit(evt) {
|
|
3835
|
+
const fv = evt.formValue ?? {};
|
|
3836
|
+
const parseCsv = (raw) => String(raw ?? "").split(/[,,\s]+/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
3837
|
+
const allowedUsers = parseCsv(fv["allowedUsers"]);
|
|
3838
|
+
const allowedChats = parseCsv(fv["allowedChats"]);
|
|
3839
|
+
const admins = parseCsv(fv["admins"]);
|
|
3840
|
+
const requireMentionInGroup = String(fv["requireMentionInGroup"] ?? "yes") === "yes";
|
|
3841
|
+
const idleRaw = String(fv["idleTimeoutMinutes"] ?? "5").trim();
|
|
3842
|
+
const idleMin = Number(idleRaw);
|
|
3843
|
+
if (!Number.isFinite(idleMin) || idleMin < 0 || idleMin > 600) {
|
|
3844
|
+
await this.sendCardToChat(
|
|
3845
|
+
evt.chatId,
|
|
3846
|
+
buildAckCard({
|
|
3847
|
+
state: "error",
|
|
3848
|
+
body: `\u274C Idle watchdog \u5FC5\u987B\u662F 0~600 \u4E4B\u95F4\u7684\u6574\u6570\uFF0C\u6536\u5230 \`${idleRaw}\``
|
|
3849
|
+
})
|
|
3850
|
+
);
|
|
3851
|
+
return;
|
|
3852
|
+
}
|
|
3853
|
+
const accessErrors = validateAccessChange({
|
|
3854
|
+
submitterOpenId: evt.senderOpenId,
|
|
3855
|
+
next: { allowedUsers, allowedChats, admins }
|
|
3856
|
+
});
|
|
3857
|
+
if (accessErrors.length > 0) {
|
|
3858
|
+
await this.sendCardToChat(
|
|
3859
|
+
evt.chatId,
|
|
3860
|
+
buildAckCard({
|
|
3861
|
+
state: "error",
|
|
3862
|
+
title: "\u26A0\uFE0F \u914D\u7F6E\u4E0D\u5B89\u5168",
|
|
3863
|
+
body: accessErrors.join("\n\n")
|
|
3864
|
+
})
|
|
3865
|
+
);
|
|
3866
|
+
return;
|
|
3867
|
+
}
|
|
3868
|
+
this.config = patchAndSaveConfig(this.config, (draft) => {
|
|
3869
|
+
draft.access.allowedUsers = allowedUsers;
|
|
3870
|
+
draft.access.allowedChats = allowedChats;
|
|
3871
|
+
draft.access.admins = admins;
|
|
3872
|
+
draft.preferences.requireMentionInGroup = requireMentionInGroup;
|
|
3873
|
+
draft.kiro.idleTimeoutMinutes = Math.floor(idleMin);
|
|
3874
|
+
});
|
|
3875
|
+
this.log.info(
|
|
3876
|
+
{
|
|
3877
|
+
allowedUsersN: allowedUsers.length,
|
|
3878
|
+
allowedChatsN: allowedChats.length,
|
|
3879
|
+
adminsN: admins.length,
|
|
3880
|
+
requireMentionInGroup,
|
|
3881
|
+
idleMin: Math.floor(idleMin),
|
|
3882
|
+
by: evt.senderOpenId
|
|
3883
|
+
},
|
|
3884
|
+
"config updated via card form"
|
|
3885
|
+
);
|
|
3886
|
+
await this.sendCardToChat(
|
|
3887
|
+
evt.chatId,
|
|
3888
|
+
buildAckCard({
|
|
3889
|
+
state: "done",
|
|
3890
|
+
title: "\u2705 \u914D\u7F6E\u5DF2\u4FDD\u5B58",
|
|
3891
|
+
body: "\u6539\u52A8\u7ACB\u5373\u751F\u6548\uFF0C\u65E0\u9700\u91CD\u542F\u3002"
|
|
3892
|
+
})
|
|
3893
|
+
);
|
|
3894
|
+
await this.sendCardToChat(
|
|
3895
|
+
evt.chatId,
|
|
3896
|
+
buildConfigViewCard({
|
|
3897
|
+
allowedUsers: this.config.access.allowedUsers,
|
|
3898
|
+
allowedChats: this.config.access.allowedChats,
|
|
3899
|
+
admins: this.config.access.admins,
|
|
3900
|
+
requireMentionInGroup: this.config.preferences.requireMentionInGroup,
|
|
3901
|
+
idleTimeoutMinutes: this.config.kiro.idleTimeoutMinutes,
|
|
3902
|
+
cardUpdateIntervalMs: this.config.preferences.cardUpdateIntervalMs,
|
|
3903
|
+
isAdmin: isAdmin(evt.senderOpenId, this.config)
|
|
3904
|
+
})
|
|
3905
|
+
);
|
|
3906
|
+
}
|
|
3907
|
+
async replyErrorCard(msg, body, cwd) {
|
|
3908
|
+
const renderer = new CardRenderer({
|
|
3909
|
+
lark: this.lark,
|
|
3910
|
+
chatId: msg.chatId,
|
|
3911
|
+
replyToMessageId: msg.messageId,
|
|
3912
|
+
intervalMs: this.config.preferences.cardUpdateIntervalMs,
|
|
3913
|
+
logger: this.log,
|
|
3914
|
+
ctx: { cwd }
|
|
3915
|
+
});
|
|
3916
|
+
await renderer.open("error", body);
|
|
3917
|
+
await renderer.finalize("error", body);
|
|
3918
|
+
}
|
|
3919
|
+
/**
|
|
3920
|
+
* 把一条用户消息丢给 Kiro 处理。
|
|
3921
|
+
*
|
|
3922
|
+
* **包了一层 rapid-fire 合并**:先放 buffer,等 200ms 静默期再真正 submit;
|
|
3923
|
+
* 期间若同 chat 又来新消息,就追加到同一个 buffer,不会触发 abort+rerun。
|
|
3924
|
+
*
|
|
3925
|
+
* 真正的 Kiro 调用在 executeKiroTask 里。
|
|
3926
|
+
*/
|
|
3927
|
+
async runKiroTask(msg, prompt, cwd, mediaPaths = [], perChatIdleMin) {
|
|
3928
|
+
const skipMerge = prompt.length > 500 || prompt.includes("**\u6700\u8FD1\u65E5\u5FD7");
|
|
3929
|
+
if (skipMerge) {
|
|
3930
|
+
await this.executeKiroTask(msg, prompt, cwd, mediaPaths, perChatIdleMin);
|
|
3931
|
+
return;
|
|
3932
|
+
}
|
|
3933
|
+
const existing = this.mergeBuffers.get(msg.chatId);
|
|
3934
|
+
if (existing) {
|
|
3935
|
+
if (prompt) existing.texts.push(prompt);
|
|
3936
|
+
existing.mediaPaths.push(...mediaPaths);
|
|
3937
|
+
clearTimeout(existing.timer);
|
|
3938
|
+
existing.timer = setTimeout(() => {
|
|
3939
|
+
this.flushMergeBuffer(msg.chatId);
|
|
3940
|
+
}, this.MERGE_WINDOW_MS);
|
|
3941
|
+
this.log.debug(
|
|
3942
|
+
{ chatId: msg.chatId, accumulated: existing.texts.length },
|
|
3943
|
+
"merging rapid-fire message"
|
|
3944
|
+
);
|
|
3945
|
+
return;
|
|
3946
|
+
}
|
|
3947
|
+
const timer = setTimeout(() => {
|
|
3948
|
+
this.flushMergeBuffer(msg.chatId);
|
|
3949
|
+
}, this.MERGE_WINDOW_MS);
|
|
3950
|
+
this.mergeBuffers.set(msg.chatId, {
|
|
3951
|
+
anchor: msg,
|
|
3952
|
+
texts: prompt ? [prompt] : [],
|
|
3953
|
+
mediaPaths: [...mediaPaths],
|
|
3954
|
+
cwd,
|
|
3955
|
+
perChatIdleMin,
|
|
3956
|
+
timer
|
|
3957
|
+
});
|
|
3958
|
+
}
|
|
3959
|
+
/**
|
|
3960
|
+
* flush 当前 chat 的合并 buffer:把累计的文本拼一起,调 executeKiroTask。
|
|
3961
|
+
*/
|
|
3962
|
+
flushMergeBuffer(chatId) {
|
|
3963
|
+
const buf = this.mergeBuffers.get(chatId);
|
|
3964
|
+
if (!buf) return;
|
|
3965
|
+
this.mergeBuffers.delete(chatId);
|
|
3966
|
+
clearTimeout(buf.timer);
|
|
3967
|
+
const merged = buf.texts.filter((t) => t.length > 0).join("\n\n");
|
|
3968
|
+
if (buf.texts.length > 1) {
|
|
3969
|
+
this.log.info(
|
|
3970
|
+
{ chatId, mergedCount: buf.texts.length, totalLen: merged.length },
|
|
3971
|
+
"flushing merged rapid-fire batch"
|
|
3972
|
+
);
|
|
3973
|
+
}
|
|
3974
|
+
void this.executeKiroTask(
|
|
3975
|
+
buf.anchor,
|
|
3976
|
+
merged,
|
|
3977
|
+
buf.cwd,
|
|
3978
|
+
buf.mediaPaths,
|
|
3979
|
+
buf.perChatIdleMin
|
|
3980
|
+
).catch((e) => this.log.error({ err: e, chatId }, "flush merge buffer execute failed"));
|
|
3981
|
+
}
|
|
3982
|
+
/**
|
|
3983
|
+
* 真正把任务丢到 ChatPipeline 跑 Kiro 的实现(不含 rapid-fire 合并)。
|
|
3984
|
+
* - 在 ChatPipeline 里跑(自动 preempt)
|
|
3985
|
+
* - 用 RunCardController 流式刷新卡片(每个工具独立 panel)
|
|
3986
|
+
* - mediaPaths 非空时,把绝对路径作为前缀加到 prompt 前面
|
|
3987
|
+
* - perChatIdleMin 控制本次 idle watchdog 阈值
|
|
3988
|
+
*/
|
|
3989
|
+
async executeKiroTask(msg, prompt, cwd, mediaPaths = [], perChatIdleMin) {
|
|
3990
|
+
const pipeline = this.getPipeline(msg.chatId);
|
|
3991
|
+
const taskId = `${msg.eventId || msg.messageId}-${Date.now().toString(36)}`;
|
|
3992
|
+
const finalPrompt = mediaPaths.length ? mediaPaths.map((p) => `@${p}`).join(" ") + (prompt ? "\n\n" + prompt : "") : prompt;
|
|
3993
|
+
const idleMin = this.effectiveIdleMinutes(perChatIdleMin);
|
|
3994
|
+
const idleTimeoutMs = idleMin > 0 ? idleMin * 60 * 1e3 : 0;
|
|
3995
|
+
await pipeline.submit({
|
|
3996
|
+
id: taskId,
|
|
3997
|
+
run: async (signal) => {
|
|
3998
|
+
const ctrlOpts = {
|
|
3999
|
+
lark: this.lark,
|
|
4000
|
+
chatId: msg.chatId,
|
|
4001
|
+
replyToMessageId: msg.messageId,
|
|
4002
|
+
intervalMs: this.config.preferences.cardUpdateIntervalMs,
|
|
4003
|
+
logger: this.log
|
|
4004
|
+
};
|
|
4005
|
+
if (idleMin > 0) ctrlOpts.idleTimeoutMinutes = idleMin;
|
|
4006
|
+
const ctrl = new RunCardController(ctrlOpts);
|
|
4007
|
+
try {
|
|
4008
|
+
await ctrl.open();
|
|
4009
|
+
} catch (e) {
|
|
4010
|
+
this.log.error({ err: e }, "failed to open card; aborting task");
|
|
4011
|
+
return;
|
|
4012
|
+
}
|
|
4013
|
+
const resumeId = await this.sessions.getKiroSession(msg.chatId, cwd);
|
|
4014
|
+
const runOpts = {
|
|
4015
|
+
prompt: finalPrompt,
|
|
4016
|
+
cwd,
|
|
4017
|
+
binPath: this.config.kiro.binPath,
|
|
4018
|
+
trustedTools: this.config.kiro.trustedTools,
|
|
4019
|
+
timeoutMs: this.config.kiro.timeoutMs,
|
|
4020
|
+
idleTimeoutMs,
|
|
4021
|
+
signal,
|
|
4022
|
+
onChunk: (text) => ctrl.feed(text)
|
|
4023
|
+
};
|
|
4024
|
+
if (resumeId !== void 0) runOpts.resumeId = resumeId;
|
|
4025
|
+
if (this.config.kiro.model !== void 0) runOpts.model = this.config.kiro.model;
|
|
4026
|
+
if (this.config.kiro.agent !== void 0) runOpts.agent = this.config.kiro.agent;
|
|
4027
|
+
let result;
|
|
4028
|
+
try {
|
|
4029
|
+
result = await runKiro(runOpts);
|
|
4030
|
+
} catch (e) {
|
|
4031
|
+
await ctrl.finalize("error", e.message);
|
|
4032
|
+
return;
|
|
4033
|
+
}
|
|
4034
|
+
if (result.aborted) {
|
|
4035
|
+
await ctrl.finalize("interrupted");
|
|
4036
|
+
return;
|
|
4037
|
+
}
|
|
4038
|
+
if (result.idleTimedOut) {
|
|
4039
|
+
await ctrl.finalize("idle_timeout");
|
|
4040
|
+
return;
|
|
4041
|
+
}
|
|
4042
|
+
if (result.timedOut) {
|
|
4043
|
+
await ctrl.finalize(
|
|
4044
|
+
"error",
|
|
4045
|
+
`\u8D85\u8FC7 ${this.config.kiro.timeoutMs / 1e3}s \u672A\u5B8C\u6210\uFF0C\u5DF2\u5F3A\u5236\u7EC8\u6B62`
|
|
4046
|
+
);
|
|
4047
|
+
return;
|
|
4048
|
+
}
|
|
4049
|
+
if (result.exitCode !== 0) {
|
|
4050
|
+
await ctrl.finalize("error", `kiro-cli \u9000\u51FA\u7801 ${result.exitCode}`);
|
|
4051
|
+
return;
|
|
4052
|
+
}
|
|
4053
|
+
if (result.newSessionId && result.newSessionId !== resumeId) {
|
|
4054
|
+
await this.sessions.setKiroSession(msg.chatId, cwd, result.newSessionId);
|
|
4055
|
+
}
|
|
4056
|
+
await ctrl.finalize("done");
|
|
4057
|
+
}
|
|
4058
|
+
});
|
|
4059
|
+
}
|
|
4060
|
+
};
|
|
4061
|
+
|
|
4062
|
+
// src/daemon/registry.ts
|
|
4063
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync6 } from "fs";
|
|
4064
|
+
import lockfile3 from "proper-lockfile";
|
|
4065
|
+
import { z as z4 } from "zod";
|
|
4066
|
+
var ProcessEntrySchema = z4.object({
|
|
4067
|
+
pid: z4.number().int().positive(),
|
|
4068
|
+
appId: z4.string(),
|
|
4069
|
+
startedAt: z4.number().int().nonnegative(),
|
|
4070
|
+
cwd: z4.string(),
|
|
4071
|
+
/** 自动生成的短 id(前 6 位 pid 哈希),方便 /exit 1 这种交互 */
|
|
4072
|
+
shortId: z4.string()
|
|
4073
|
+
});
|
|
4074
|
+
var ProcessesFileSchema = z4.object({
|
|
4075
|
+
version: z4.literal(1).default(1),
|
|
4076
|
+
processes: z4.array(ProcessEntrySchema).default([])
|
|
4077
|
+
});
|
|
4078
|
+
var log7 = () => getLogger().child({ module: "process-registry" });
|
|
4079
|
+
function readFile3() {
|
|
4080
|
+
if (!existsSync6(PROCESSES_FILE)) {
|
|
4081
|
+
return ProcessesFileSchema.parse({});
|
|
4082
|
+
}
|
|
4083
|
+
try {
|
|
4084
|
+
const raw = readFileSync6(PROCESSES_FILE, "utf-8");
|
|
4085
|
+
const parsed = ProcessesFileSchema.safeParse(JSON.parse(raw));
|
|
4086
|
+
if (!parsed.success) return ProcessesFileSchema.parse({});
|
|
4087
|
+
return parsed.data;
|
|
4088
|
+
} catch {
|
|
4089
|
+
return ProcessesFileSchema.parse({});
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
function writeFile3(data) {
|
|
4093
|
+
ensureDataDirs();
|
|
4094
|
+
writeFileSync5(PROCESSES_FILE, JSON.stringify(data, null, 2) + "\n", { mode: 384 });
|
|
4095
|
+
}
|
|
4096
|
+
async function withLock3(fn) {
|
|
4097
|
+
ensureDataDirs();
|
|
4098
|
+
if (!existsSync6(PROCESSES_FILE)) {
|
|
4099
|
+
writeFileSync5(PROCESSES_FILE, "{}\n", { mode: 384 });
|
|
4100
|
+
}
|
|
4101
|
+
const release = await lockfile3.lock(PROCESSES_FILE, {
|
|
4102
|
+
retries: { retries: 5, minTimeout: 50, maxTimeout: 200 },
|
|
4103
|
+
stale: 1e4
|
|
4104
|
+
});
|
|
4105
|
+
try {
|
|
4106
|
+
return fn();
|
|
4107
|
+
} finally {
|
|
4108
|
+
await release();
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
function isAlive(pid) {
|
|
4112
|
+
try {
|
|
4113
|
+
process.kill(pid, 0);
|
|
4114
|
+
return true;
|
|
4115
|
+
} catch (e) {
|
|
4116
|
+
const code = e.code;
|
|
4117
|
+
return code === "EPERM";
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4120
|
+
function shortIdOf(pid) {
|
|
4121
|
+
return pid.toString(36).padStart(6, "0").slice(-6);
|
|
4122
|
+
}
|
|
4123
|
+
async function listProcesses() {
|
|
4124
|
+
return withLock3(() => {
|
|
4125
|
+
const data = readFile3();
|
|
4126
|
+
const alive = data.processes.filter((p) => isAlive(p.pid));
|
|
4127
|
+
if (alive.length !== data.processes.length) {
|
|
4128
|
+
data.processes = alive;
|
|
4129
|
+
writeFile3(data);
|
|
4130
|
+
}
|
|
4131
|
+
return alive;
|
|
4132
|
+
});
|
|
4133
|
+
}
|
|
4134
|
+
async function registerSelf(appId) {
|
|
4135
|
+
const entry = {
|
|
4136
|
+
pid: process.pid,
|
|
4137
|
+
appId,
|
|
4138
|
+
startedAt: Date.now(),
|
|
4139
|
+
cwd: process.cwd(),
|
|
4140
|
+
shortId: shortIdOf(process.pid)
|
|
4141
|
+
};
|
|
4142
|
+
await withLock3(() => {
|
|
4143
|
+
const data = readFile3();
|
|
4144
|
+
data.processes = data.processes.filter((p) => p.pid !== process.pid && isAlive(p.pid));
|
|
4145
|
+
data.processes.push(entry);
|
|
4146
|
+
writeFile3(data);
|
|
4147
|
+
});
|
|
4148
|
+
log7().info({ pid: entry.pid, appId, shortId: entry.shortId }, "process registered");
|
|
4149
|
+
return entry;
|
|
4150
|
+
}
|
|
4151
|
+
async function unregisterSelf() {
|
|
4152
|
+
await withLock3(() => {
|
|
4153
|
+
const data = readFile3();
|
|
4154
|
+
const before = data.processes.length;
|
|
4155
|
+
data.processes = data.processes.filter((p) => p.pid !== process.pid);
|
|
4156
|
+
if (data.processes.length !== before) writeFile3(data);
|
|
4157
|
+
});
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
// src/core/bootstrap.ts
|
|
4161
|
+
async function runBridge() {
|
|
4162
|
+
const log8 = getLogger();
|
|
4163
|
+
const config = loadConfig();
|
|
4164
|
+
log8.info(
|
|
4165
|
+
{
|
|
4166
|
+
appId: config.lark.appId,
|
|
4167
|
+
defaultCwd: config.workspace.defaultCwd,
|
|
4168
|
+
allowedRoots: config.workspace.allowedRoots,
|
|
4169
|
+
trustedTools: config.kiro.trustedTools,
|
|
4170
|
+
idleTimeoutMinutes: config.kiro.idleTimeoutMinutes
|
|
4171
|
+
},
|
|
4172
|
+
"lark-kiro-bridge starting"
|
|
4173
|
+
);
|
|
4174
|
+
pruneOldLogs(config.preferences.logRetentionDays);
|
|
4175
|
+
pruneOldMedia(24);
|
|
4176
|
+
const others = (await listProcesses()).filter(
|
|
4177
|
+
(p) => p.appId === config.lark.appId && p.pid !== process.pid
|
|
4178
|
+
);
|
|
4179
|
+
if (others.length > 0) {
|
|
4180
|
+
log8.warn(
|
|
4181
|
+
{ others: others.map((p) => ({ pid: p.pid, shortId: p.shortId })) },
|
|
4182
|
+
"another bridge process is running with the same appId; Lark events may be routed randomly between them"
|
|
4183
|
+
);
|
|
4184
|
+
}
|
|
4185
|
+
await registerSelf(config.lark.appId);
|
|
4186
|
+
const lark2 = new LarkClient({
|
|
4187
|
+
appId: config.lark.appId,
|
|
4188
|
+
appSecret: config.lark.appSecret,
|
|
4189
|
+
logger: log8
|
|
4190
|
+
});
|
|
4191
|
+
const sessions = new SessionStore();
|
|
4192
|
+
const workspaces = new WorkspaceStore();
|
|
4193
|
+
const larkRef = lark2;
|
|
4194
|
+
const startEventLoop = async () => {
|
|
4195
|
+
await larkRef.startEventLoop({
|
|
4196
|
+
onMessage: (msg) => dispatcher.handle(msg),
|
|
4197
|
+
onCardAction: (evt) => dispatcher.handleCardAction(evt),
|
|
4198
|
+
onReady: () => log8.info("\u{1F680} lark-kiro-bridge ready, waiting for messages")
|
|
4199
|
+
});
|
|
4200
|
+
};
|
|
4201
|
+
const dispatcher = new Dispatcher({
|
|
4202
|
+
config,
|
|
4203
|
+
lark: lark2,
|
|
4204
|
+
sessions,
|
|
4205
|
+
workspaces,
|
|
4206
|
+
logger: log8,
|
|
4207
|
+
onReconnect: async () => {
|
|
4208
|
+
log8.info("reconnect requested via /reconnect");
|
|
4209
|
+
try {
|
|
4210
|
+
larkRef.close();
|
|
4211
|
+
} catch (e) {
|
|
4212
|
+
log8.warn({ err: e }, "close before reconnect failed (ignored)");
|
|
4213
|
+
}
|
|
4214
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
4215
|
+
await startEventLoop();
|
|
4216
|
+
}
|
|
4217
|
+
});
|
|
4218
|
+
await startEventLoop();
|
|
4219
|
+
let stopped = false;
|
|
4220
|
+
const stop = async () => {
|
|
4221
|
+
if (stopped) return;
|
|
4222
|
+
stopped = true;
|
|
4223
|
+
log8.info("shutting down");
|
|
4224
|
+
try {
|
|
4225
|
+
larkRef.close();
|
|
4226
|
+
} catch {
|
|
4227
|
+
}
|
|
4228
|
+
await unregisterSelf().catch(() => void 0);
|
|
4229
|
+
};
|
|
4230
|
+
const signalHandler = async (sig) => {
|
|
4231
|
+
log8.info({ sig }, "received signal");
|
|
4232
|
+
await stop();
|
|
4233
|
+
process.exit(0);
|
|
4234
|
+
};
|
|
4235
|
+
process.once("SIGINT", signalHandler);
|
|
4236
|
+
process.once("SIGTERM", signalHandler);
|
|
4237
|
+
return { stop };
|
|
4238
|
+
}
|
|
4239
|
+
export {
|
|
4240
|
+
ChatPipeline,
|
|
4241
|
+
Dispatcher,
|
|
4242
|
+
LarkClient,
|
|
4243
|
+
SessionStore,
|
|
4244
|
+
WorkspaceStore,
|
|
4245
|
+
defaultConfig,
|
|
4246
|
+
getLogger,
|
|
4247
|
+
loadConfig,
|
|
4248
|
+
runBridge,
|
|
4249
|
+
runKiro,
|
|
4250
|
+
saveConfig
|
|
4251
|
+
};
|
|
4252
|
+
//# sourceMappingURL=index.js.map
|