ai-task-board-bridge 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +66 -15
  2. package/dist/app-server-client.js +1 -1
  3. package/dist/bridge.js +1 -1
  4. package/dist/cli.js +60 -18
  5. package/dist/cli.js.map +1 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/installer.d.ts +13 -0
  11. package/dist/installer.d.ts.map +1 -0
  12. package/dist/installer.js +85 -0
  13. package/dist/installer.js.map +1 -0
  14. package/dist/kimi-runtime/acp-client.d.ts +59 -0
  15. package/dist/kimi-runtime/acp-client.d.ts.map +1 -0
  16. package/dist/kimi-runtime/acp-client.js +341 -0
  17. package/dist/kimi-runtime/acp-client.js.map +1 -0
  18. package/dist/kimi-runtime/board-client.d.ts +91 -0
  19. package/dist/kimi-runtime/board-client.d.ts.map +1 -0
  20. package/dist/kimi-runtime/board-client.js +286 -0
  21. package/dist/kimi-runtime/board-client.js.map +1 -0
  22. package/dist/kimi-runtime/bridge.d.ts +60 -0
  23. package/dist/kimi-runtime/bridge.d.ts.map +1 -0
  24. package/dist/kimi-runtime/bridge.js +773 -0
  25. package/dist/kimi-runtime/bridge.js.map +1 -0
  26. package/dist/kimi-runtime/cli.d.ts +3 -0
  27. package/dist/kimi-runtime/cli.d.ts.map +1 -0
  28. package/dist/kimi-runtime/cli.js +82 -0
  29. package/dist/kimi-runtime/cli.js.map +1 -0
  30. package/dist/kimi-runtime/config.d.ts +30 -0
  31. package/dist/kimi-runtime/config.d.ts.map +1 -0
  32. package/dist/kimi-runtime/config.js +121 -0
  33. package/dist/kimi-runtime/config.js.map +1 -0
  34. package/dist/kimi-runtime/index.d.ts +7 -0
  35. package/dist/kimi-runtime/index.d.ts.map +1 -0
  36. package/dist/kimi-runtime/index.js +7 -0
  37. package/dist/kimi-runtime/index.js.map +1 -0
  38. package/dist/kimi-runtime/setup.d.ts +23 -0
  39. package/dist/kimi-runtime/setup.d.ts.map +1 -0
  40. package/dist/kimi-runtime/setup.js +496 -0
  41. package/dist/kimi-runtime/setup.js.map +1 -0
  42. package/dist/kimi-runtime/utils.d.ts +13 -0
  43. package/dist/kimi-runtime/utils.d.ts.map +1 -0
  44. package/dist/kimi-runtime/utils.js +105 -0
  45. package/dist/kimi-runtime/utils.js.map +1 -0
  46. package/package.json +9 -4
@@ -0,0 +1,773 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import path from "node:path";
3
+ import { KIMI_FALLBACK_MODEL_CATALOG, KimiAcpClient, modelCatalogFromConfigOptions, } from "./acp-client.js";
4
+ import { actionableBoardError, BoardClient, commandError, commandString, } from "./board-client.js";
5
+ import { directoryForWorkingDirectory, loadConfiguration, workingDirectoryForKey, } from "./config.js";
6
+ import { appendBoundedText, delay, errorMessage, errorStatus, isPersistentClientError, redactText, sanitizeValue, stringValue, } from "./utils.js";
7
+ const ACP_PROTOCOL = "kimi-acp/v1";
8
+ const IMAGE_MIME_TYPES = new Set([
9
+ "image/png",
10
+ "image/jpeg",
11
+ "image/webp",
12
+ "image/gif",
13
+ ]);
14
+ export class TurnLimiter {
15
+ limit;
16
+ active = 0;
17
+ waiters = [];
18
+ constructor(limit) {
19
+ this.limit = limit;
20
+ if (!Number.isInteger(limit) || limit < 1) {
21
+ throw new Error("TurnLimiter limit 必须是正整数");
22
+ }
23
+ }
24
+ acquire(signal) {
25
+ if (signal.aborted)
26
+ return Promise.reject(signal.reason);
27
+ if (this.active < this.limit) {
28
+ this.active += 1;
29
+ return Promise.resolve(this.releaseFunction());
30
+ }
31
+ return new Promise((resolve, reject) => {
32
+ const waiter = {
33
+ resolve,
34
+ reject,
35
+ signal,
36
+ abort: () => undefined,
37
+ };
38
+ waiter.abort = () => {
39
+ const index = this.waiters.indexOf(waiter);
40
+ if (index >= 0)
41
+ this.waiters.splice(index, 1);
42
+ reject(signal.reason);
43
+ };
44
+ signal.addEventListener("abort", waiter.abort, { once: true });
45
+ this.waiters.push(waiter);
46
+ });
47
+ }
48
+ releaseFunction() {
49
+ let released = false;
50
+ return () => {
51
+ if (released)
52
+ return;
53
+ released = true;
54
+ this.active = Math.max(0, this.active - 1);
55
+ this.drain();
56
+ };
57
+ }
58
+ drain() {
59
+ while (this.active < this.limit && this.waiters.length > 0) {
60
+ const waiter = this.waiters.shift();
61
+ if (!waiter)
62
+ return;
63
+ waiter.signal.removeEventListener("abort", waiter.abort);
64
+ if (waiter.signal.aborted) {
65
+ waiter.reject(waiter.signal.reason);
66
+ continue;
67
+ }
68
+ this.active += 1;
69
+ waiter.resolve(this.releaseFunction());
70
+ }
71
+ }
72
+ }
73
+ export async function prepareKimiSession(acp, session, configuration, requestedModel, requestedReasoningEffort, existingOptions) {
74
+ let options = existingOptions
75
+ ? [...existingOptions]
76
+ : await acp.resumeSession(session);
77
+ if (requestedModel) {
78
+ options = await acp.setConfigValue(session.sessionId, options, "model", requestedModel);
79
+ }
80
+ if (requestedReasoningEffort) {
81
+ options = await acp.setConfigValue(session.sessionId, options, "thought_level", requestedReasoningEffort);
82
+ }
83
+ options = await acp.setConfigValue(session.sessionId, options, "mode", configuration.agentMode);
84
+ return {
85
+ options,
86
+ model: acp.selectedValue(session.sessionId, "model"),
87
+ reasoningEffort: acp.selectedValue(session.sessionId, "thought_level"),
88
+ };
89
+ }
90
+ class TurnRecorder {
91
+ assistant = "";
92
+ reasoningCharacters = 0;
93
+ toolCalls = 0;
94
+ failedToolCalls = 0;
95
+ planEntries = 0;
96
+ usageUpdate = null;
97
+ consume(notification) {
98
+ const update = notification.update;
99
+ switch (update.sessionUpdate) {
100
+ case "agent_message_chunk":
101
+ if (update.content.type === "text") {
102
+ this.assistant = appendBoundedText(this.assistant, update.content.text);
103
+ }
104
+ return;
105
+ case "agent_thought_chunk":
106
+ if (update.content.type === "text") {
107
+ this.reasoningCharacters += update.content.text.length;
108
+ }
109
+ return;
110
+ case "tool_call":
111
+ this.toolCalls += 1;
112
+ if (update.status === "failed")
113
+ this.failedToolCalls += 1;
114
+ return;
115
+ case "tool_call_update":
116
+ if (update.status === "failed")
117
+ this.failedToolCalls += 1;
118
+ return;
119
+ case "plan":
120
+ this.planEntries = update.entries.length;
121
+ return;
122
+ case "usage_update":
123
+ this.usageUpdate = { used: update.used, size: update.size };
124
+ return;
125
+ default:
126
+ return;
127
+ }
128
+ }
129
+ resultData(stopReason, usage, model, reasoningEffort) {
130
+ return {
131
+ protocol: ACP_PROTOCOL,
132
+ phase: "completed",
133
+ stop_reason: stopReason,
134
+ model,
135
+ reasoning_effort: reasoningEffort,
136
+ tool_call_count: this.toolCalls,
137
+ failed_tool_call_count: this.failedToolCalls,
138
+ plan_entry_count: this.planEntries,
139
+ reasoning_characters_observed: this.reasoningCharacters,
140
+ usage: sanitizeValue(usage ?? this.usageUpdate),
141
+ };
142
+ }
143
+ }
144
+ class SessionWorker {
145
+ info;
146
+ boardSession;
147
+ configuration;
148
+ board;
149
+ acp;
150
+ limiter;
151
+ onModelChanged;
152
+ stopController = new AbortController();
153
+ activeClaim = null;
154
+ runPromise = null;
155
+ stopping = false;
156
+ activePrompt = false;
157
+ constructor(info, boardSession, configuration, board, acp, limiter, onModelChanged) {
158
+ this.info = info;
159
+ this.boardSession = boardSession;
160
+ this.configuration = configuration;
161
+ this.board = board;
162
+ this.acp = acp;
163
+ this.limiter = limiter;
164
+ this.onModelChanged = onModelChanged;
165
+ }
166
+ get sessionId() {
167
+ return this.info.sessionId;
168
+ }
169
+ get busy() {
170
+ return this.activeClaim !== null || this.activePrompt;
171
+ }
172
+ update(info, boardSession) {
173
+ this.info = info;
174
+ this.boardSession = boardSession;
175
+ }
176
+ start() {
177
+ if (!this.runPromise)
178
+ this.runPromise = this.run();
179
+ return this.runPromise;
180
+ }
181
+ async stop(reason = "Kimi Bridge 正在停止") {
182
+ if (this.stopping)
183
+ return;
184
+ this.stopping = true;
185
+ this.stopController.abort(new Error(reason));
186
+ if (this.activePrompt) {
187
+ await this.acp.cancel(this.info.sessionId).catch(() => undefined);
188
+ }
189
+ await Promise.race([this.runPromise ?? Promise.resolve(), delay(3_000)]);
190
+ }
191
+ async run() {
192
+ let heartbeatTimer = null;
193
+ try {
194
+ await this.heartbeat();
195
+ heartbeatTimer = setInterval(() => {
196
+ if (this.stopping)
197
+ return;
198
+ const operation = this.activeClaim
199
+ ? this.board.heartbeatClaim(this.boardSession.id, this.activeClaim, this.configuration.leaseSeconds, this.stopController.signal)
200
+ : this.heartbeat();
201
+ void operation.catch((error) => {
202
+ if (!this.stopping) {
203
+ process.stderr.write(`Kimi Session ${this.info.sessionId} 心跳失败:${errorMessage(error)}\n`);
204
+ }
205
+ });
206
+ }, 45_000);
207
+ while (!this.stopping)
208
+ await this.runOneIteration();
209
+ }
210
+ catch (error) {
211
+ if (!this.stopping)
212
+ throw error;
213
+ }
214
+ finally {
215
+ if (heartbeatTimer)
216
+ clearInterval(heartbeatTimer);
217
+ if (this.activeClaim) {
218
+ await this.board
219
+ .releaseTask(this.boardSession.id, this.activeClaim, "Kimi Bridge 已停止")
220
+ .catch(() => undefined);
221
+ this.activeClaim = null;
222
+ }
223
+ }
224
+ }
225
+ async runOneIteration() {
226
+ let release = null;
227
+ try {
228
+ release = await this.limiter.acquire(this.stopController.signal);
229
+ let task;
230
+ try {
231
+ task = await this.board.claimTask(this.boardSession.id, this.configuration.leaseSeconds, this.stopController.signal);
232
+ }
233
+ catch (error) {
234
+ if (errorStatus(error) === 409) {
235
+ release();
236
+ release = null;
237
+ await delay(Math.max(10_000, this.configuration.pollIntervalMs), this.stopController.signal);
238
+ return;
239
+ }
240
+ throw error;
241
+ }
242
+ if (!task) {
243
+ release();
244
+ release = null;
245
+ await delay(this.configuration.pollIntervalMs, this.stopController.signal);
246
+ return;
247
+ }
248
+ this.activeClaim = task;
249
+ process.stdout.write(`开始 Kimi 任务 [${this.info.sessionId.slice(0, 8)}]:${task.title}\n`);
250
+ try {
251
+ await this.executeTask(task);
252
+ process.stdout.write(`完成 Kimi 任务 [${this.info.sessionId.slice(0, 8)}]:${task.title}\n`);
253
+ }
254
+ catch (error) {
255
+ if (this.stopping) {
256
+ await this.board
257
+ .releaseTask(this.boardSession.id, task, "Kimi Bridge 正在停止")
258
+ .catch(() => undefined);
259
+ }
260
+ else {
261
+ const reason = redactText(errorMessage(error), 10_000);
262
+ await this.board
263
+ .failTask(this.boardSession.id, task, reason, this.stopController.signal)
264
+ .catch(() => undefined);
265
+ process.stderr.write(`Kimi 任务失败 [${this.info.sessionId}]:${reason}\n`);
266
+ }
267
+ }
268
+ finally {
269
+ this.activeClaim = null;
270
+ }
271
+ }
272
+ finally {
273
+ release?.();
274
+ }
275
+ }
276
+ async executeTask(task) {
277
+ await this.board.reportProgress(this.boardSession.id, task, this.stopController.signal);
278
+ const prepared = await prepareKimiSession(this.acp, this.info, this.configuration, stringValue(task.model), stringValue(task.reasoning_effort));
279
+ this.onModelChanged(this.info.sessionId, prepared.model);
280
+ const text = [
281
+ task.description?.trim() || task.title,
282
+ task.acceptance_criteria
283
+ ? `\n\n验收条件:\n${task.acceptance_criteria}`
284
+ : "",
285
+ ].join("");
286
+ const artifacts = await this.board.taskArtifacts(this.boardSession.id, task.id, this.stopController.signal);
287
+ const images = artifacts.filter((artifact) => IMAGE_MIME_TYPES.has(artifact.mime_type));
288
+ if (images.length > 0 && !this.acp.supportsImages) {
289
+ throw new Error("当前 Kimi Code ACP 不支持图片输入");
290
+ }
291
+ const prompt = [{ type: "text", text }];
292
+ for (const artifact of images) {
293
+ const image = await this.board.downloadImage(this.boardSession.id, artifact, this.stopController.signal);
294
+ prompt.push({ type: "image", ...image });
295
+ }
296
+ const recorder = new TurnRecorder();
297
+ const unsubscribe = this.acp.subscribe(this.info.sessionId, (notification) => recorder.consume(notification));
298
+ this.activePrompt = true;
299
+ this.acp.setTaskActive(this.info.sessionId, true);
300
+ let response;
301
+ try {
302
+ response = await this.acp.prompt(this.info.sessionId, task.id, prompt);
303
+ }
304
+ finally {
305
+ this.activePrompt = false;
306
+ this.acp.setTaskActive(this.info.sessionId, false);
307
+ unsubscribe();
308
+ await this.acp.closeSession(this.info.sessionId).catch(() => undefined);
309
+ }
310
+ if (response.stopReason === "refusal") {
311
+ throw new Error("Kimi 拒绝执行本轮任务");
312
+ }
313
+ if (response.stopReason === "cancelled") {
314
+ throw new Error("Kimi 本轮任务已取消");
315
+ }
316
+ const resultData = recorder.resultData(response.stopReason, response.usage, prepared.model, prepared.reasoningEffort);
317
+ if (recorder.assistant) {
318
+ await this.board.reportAssistantMessage(this.boardSession.id, this.info.sessionId, task, recorder.assistant, resultData, this.stopController.signal);
319
+ }
320
+ await this.board.completeTask(this.boardSession.id, task, recorder.assistant || `Kimi Code 已结束本轮(${response.stopReason})`, resultData, this.stopController.signal);
321
+ }
322
+ async heartbeat() {
323
+ const session = await this.board.heartbeatPresence(this.boardSession.id, this.stopController.signal);
324
+ if (session)
325
+ this.boardSession = session;
326
+ }
327
+ }
328
+ export class KimiBridge {
329
+ configuration;
330
+ acp;
331
+ stopController = new AbortController();
332
+ board;
333
+ limiter;
334
+ runtimeInstanceId = randomUUID();
335
+ workers = new Map();
336
+ workerRuns = new Map();
337
+ knownModels = new Map();
338
+ createdSessions = new Map();
339
+ managedSessionIds = new Set();
340
+ modelCatalog;
341
+ reportSequence = 0;
342
+ runtimeLeaseClaimed = false;
343
+ leaseSafetyDeadline = 0;
344
+ leaseRenewalPromise = null;
345
+ stopping = false;
346
+ fatalError = null;
347
+ stopPromise = null;
348
+ inventoryReady = false;
349
+ constructor(configuration, acp) {
350
+ this.configuration = configuration;
351
+ this.acp = acp;
352
+ this.board = new BoardClient(configuration, () => this.stopping);
353
+ this.limiter = new TurnLimiter(configuration.maxConcurrentTurns);
354
+ this.modelCatalog = KIMI_FALLBACK_MODEL_CATALOG.map((model) => ({
355
+ ...model,
356
+ input_modalities: acp.supportsImages ? ["text", "image"] : ["text"],
357
+ supported_reasoning_efforts: model.supported_reasoning_efforts.map((effort) => ({ ...effort })),
358
+ }));
359
+ acp.onUnexpectedExit((error) => this.markFatal(error));
360
+ }
361
+ async run() {
362
+ if (this.configuration.approvalMode === "accept") {
363
+ process.stderr.write("警告:KIMI_BRIDGE_APPROVAL_MODE=accept 会自动批准与当前看板任务关联的 Kimi 权限请求\n");
364
+ }
365
+ if (this.configuration.agentMode === "yolo") {
366
+ process.stderr.write("高风险警告:KIMI_BRIDGE_MODE=yolo 会让 Kimi Code 自动执行所有工具操作\n");
367
+ }
368
+ process.stdout.write(`已连接 ${this.acp.agentDescription}\n`);
369
+ const initialSessions = await this.listSessions();
370
+ await this.discoverModelCatalog(initialSessions[0]);
371
+ await this.establishRuntimeLease();
372
+ if (this.stopping)
373
+ return this.finishRun();
374
+ this.leaseRenewalPromise = this.renewRuntimeLease();
375
+ let nextInventorySyncAt = 0;
376
+ while (!this.stopping) {
377
+ if (Date.now() >= nextInventorySyncAt) {
378
+ try {
379
+ await this.syncWorkers();
380
+ nextInventorySyncAt = Date.now() + this.configuration.syncIntervalMs;
381
+ }
382
+ catch (error) {
383
+ if (this.stopping)
384
+ break;
385
+ if (isPersistentClientError(error)) {
386
+ this.markFatal(actionableBoardError(error));
387
+ break;
388
+ }
389
+ process.stderr.write(`同步 Kimi Sessions 失败:${errorMessage(error)}\n`);
390
+ }
391
+ }
392
+ if (this.stopping)
393
+ break;
394
+ try {
395
+ if (this.inventoryReady && (await this.processThreadCommands())) {
396
+ await this.syncWorkers();
397
+ nextInventorySyncAt = Date.now() + this.configuration.syncIntervalMs;
398
+ }
399
+ }
400
+ catch (error) {
401
+ if (this.stopping)
402
+ break;
403
+ if (isPersistentClientError(error)) {
404
+ this.markFatal(actionableBoardError(error));
405
+ break;
406
+ }
407
+ process.stderr.write(`处理 Kimi Session 管理指令失败:${errorMessage(error)}\n`);
408
+ }
409
+ try {
410
+ await delay(Math.min(this.configuration.commandPollIntervalMs, Math.max(1_000, nextInventorySyncAt - Date.now())), this.stopController.signal);
411
+ }
412
+ catch {
413
+ break;
414
+ }
415
+ }
416
+ return this.finishRun();
417
+ }
418
+ async stop() {
419
+ if (!this.stopPromise)
420
+ this.stopPromise = this.stopBridge();
421
+ return this.stopPromise;
422
+ }
423
+ async finishRun() {
424
+ await this.stop();
425
+ if (this.fatalError)
426
+ throw this.fatalError;
427
+ }
428
+ async stopBridge() {
429
+ this.stopping = true;
430
+ this.stopController.abort(new Error("Kimi Bridge 正在停止"));
431
+ await Promise.allSettled([...this.workers.values()].map((worker) => worker.stop()));
432
+ await this.acp.close();
433
+ await Promise.allSettled([...this.workerRuns.values()]);
434
+ await (this.leaseRenewalPromise ?? Promise.resolve()).catch(() => undefined);
435
+ await this.releaseRuntimeLease();
436
+ }
437
+ markFatal(error) {
438
+ if (this.fatalError || this.stopping)
439
+ return;
440
+ this.fatalError = error;
441
+ this.stopping = true;
442
+ this.stopController.abort(error);
443
+ for (const worker of this.workers.values()) {
444
+ void worker.stop(error.message);
445
+ }
446
+ }
447
+ configurationStatus(releaseRuntime = false) {
448
+ this.reportSequence += 1;
449
+ const firstDirectory = this.configuration.workingDirectories[0];
450
+ return {
451
+ runtime_instance_id: this.runtimeInstanceId,
452
+ report_sequence: this.reportSequence,
453
+ lease_seconds: this.configuration.runtimeLeaseSeconds,
454
+ release_runtime: releaseRuntime,
455
+ applied_version: null,
456
+ effective: {
457
+ enabled: true,
458
+ include_thread_titles: this.configuration.includeSessionTitles,
459
+ max_threads: this.configuration.maxThreads,
460
+ max_concurrent_turns: this.configuration.maxConcurrentTurns,
461
+ sync_history: false,
462
+ history_turn_limit: 1,
463
+ working_directories: this.configuration.workingDirectories.map((directory) => ({
464
+ directory_key: directory.key,
465
+ name: directory.name,
466
+ working_directory: directory.workingDirectory,
467
+ })),
468
+ },
469
+ constraints: {
470
+ remote_configuration_enabled: false,
471
+ allow_thread_titles: this.configuration.includeSessionTitles,
472
+ max_threads: this.configuration.maxThreads,
473
+ max_concurrent_turns: this.configuration.maxConcurrentTurns,
474
+ thread_scope: "cwd",
475
+ working_directory: firstDirectory?.workingDirectory ?? process.cwd(),
476
+ fixed_thread: false,
477
+ permission_mode: "inherit",
478
+ approval_mode: this.configuration.approvalMode === "accept" ? "accept" : "decline",
479
+ allow_history_sync: false,
480
+ max_history_turns: 1,
481
+ allow_working_directory_configuration: false,
482
+ },
483
+ error: null,
484
+ };
485
+ }
486
+ async exchangeRuntimeLease(release = false) {
487
+ const startedAt = Date.now();
488
+ await this.board.exchangeConfiguration(this.configurationStatus(release), release ? undefined : this.stopController.signal, release ? 1_500 : 5_000);
489
+ if (release) {
490
+ this.runtimeLeaseClaimed = false;
491
+ this.leaseSafetyDeadline = 0;
492
+ return;
493
+ }
494
+ this.runtimeLeaseClaimed = true;
495
+ this.leaseSafetyDeadline =
496
+ startedAt + this.configuration.runtimeLeaseSeconds * 1_000 - 5_000;
497
+ }
498
+ async establishRuntimeLease() {
499
+ let attempt = 0;
500
+ while (!this.stopping) {
501
+ attempt += 1;
502
+ try {
503
+ await this.exchangeRuntimeLease();
504
+ return;
505
+ }
506
+ catch (error) {
507
+ if (this.stopping)
508
+ return;
509
+ if (errorStatus(error) === 409) {
510
+ if (attempt === 1 || attempt % 10 === 0) {
511
+ process.stderr.write("同一连接的旧 Bridge 租约仍有效;Kimi Bridge 等待接管\n");
512
+ }
513
+ }
514
+ else if (isPersistentClientError(error)) {
515
+ throw actionableBoardError(error);
516
+ }
517
+ else if (attempt === 1 || attempt % 10 === 0) {
518
+ process.stderr.write(`尚未取得 Kimi Bridge 运行租约:${errorMessage(error)}\n`);
519
+ }
520
+ await delay(Math.min(500 * 2 ** Math.min(attempt - 1, 4), 5_000), this.stopController.signal).catch(() => undefined);
521
+ }
522
+ }
523
+ }
524
+ async renewRuntimeLease() {
525
+ const interval = Math.max(1_000, Math.floor((this.configuration.runtimeLeaseSeconds * 1_000) / 3));
526
+ while (!this.stopping) {
527
+ try {
528
+ await delay(interval, this.stopController.signal);
529
+ }
530
+ catch {
531
+ return;
532
+ }
533
+ if (Date.now() >= this.leaseSafetyDeadline) {
534
+ this.markFatal(new Error("Kimi Bridge 运行租约已超过本地安全期限,停止所有 worker"));
535
+ return;
536
+ }
537
+ try {
538
+ await this.exchangeRuntimeLease();
539
+ }
540
+ catch (error) {
541
+ if (this.stopping)
542
+ return;
543
+ if (isPersistentClientError(error)) {
544
+ this.markFatal(actionableBoardError(error));
545
+ return;
546
+ }
547
+ process.stderr.write(`Kimi Bridge 运行租约续租失败:${errorMessage(error)}\n`);
548
+ if (Date.now() >= this.leaseSafetyDeadline) {
549
+ this.markFatal(new Error("Kimi Bridge 无法在安全期限前续租", { cause: error }));
550
+ return;
551
+ }
552
+ }
553
+ }
554
+ }
555
+ async releaseRuntimeLease() {
556
+ if (!this.runtimeLeaseClaimed)
557
+ return;
558
+ await this.exchangeRuntimeLease(true).catch(() => undefined);
559
+ }
560
+ async discoverModelCatalog(session) {
561
+ if (!session) {
562
+ process.stdout.write(`Kimi 当前没有可用于探测的 Session,使用 ${this.modelCatalog.length} 个兼容模型\n`);
563
+ return;
564
+ }
565
+ try {
566
+ const options = await this.acp.resumeSession(session);
567
+ const catalog = modelCatalogFromConfigOptions(options, this.acp.supportsImages);
568
+ if (catalog?.length)
569
+ this.modelCatalog = catalog;
570
+ this.knownModels.set(session.sessionId, this.acp.selectedValue(session.sessionId, "model"));
571
+ await this.acp.closeSession(session.sessionId).catch(() => undefined);
572
+ process.stdout.write(`已从 Kimi ACP 读取 ${this.modelCatalog.length} 个模型\n`);
573
+ }
574
+ catch (error) {
575
+ process.stderr.write(`读取 Kimi 模型目录失败,使用兼容目录:${errorMessage(error)}\n`);
576
+ }
577
+ }
578
+ async listSessions() {
579
+ const discovered = new Map();
580
+ for (const directory of this.configuration.workingDirectories) {
581
+ const sessions = await this.acp.listAllSessions(directory.workingDirectory);
582
+ for (const session of sessions) {
583
+ if (!directoryForWorkingDirectory(session.cwd, [directory]))
584
+ continue;
585
+ discovered.set(session.sessionId, session);
586
+ this.createdSessions.delete(session.sessionId);
587
+ }
588
+ }
589
+ for (const [sessionId, session] of this.createdSessions) {
590
+ if (!discovered.has(sessionId) &&
591
+ directoryForWorkingDirectory(session.cwd, this.configuration.workingDirectories)) {
592
+ discovered.set(sessionId, session);
593
+ }
594
+ }
595
+ return [...discovered.values()]
596
+ .sort((left, right) => Date.parse(right.updatedAt ?? "") - Date.parse(left.updatedAt ?? "") ||
597
+ left.sessionId.localeCompare(right.sessionId))
598
+ .slice(0, this.configuration.maxThreads);
599
+ }
600
+ inventoryThread(session) {
601
+ const directory = directoryForWorkingDirectory(session.cwd, this.configuration.workingDirectories);
602
+ if (!directory) {
603
+ throw new Error(`Kimi Session ${session.sessionId} 不在工作目录白名单中`);
604
+ }
605
+ const fallbackName = `Kimi · ${directory.name} · ${session.sessionId.slice(0, 8)}`;
606
+ const localTitle = stringValue(session.title);
607
+ const name = this.configuration.sessionNamePrefix
608
+ ? `${this.configuration.sessionNamePrefix} · ${this.configuration.includeSessionTitles && localTitle
609
+ ? localTitle
610
+ : session.sessionId.slice(0, 8)}`
611
+ : this.configuration.includeSessionTitles && localTitle
612
+ ? localTitle
613
+ : fallbackName;
614
+ return {
615
+ external_conversation_ref: session.sessionId,
616
+ name: name.slice(0, 200),
617
+ platform: "kimi-code",
618
+ model: this.knownModels.get(session.sessionId) ?? null,
619
+ working_directory: session.cwd,
620
+ directory_key: directory.key,
621
+ capabilities: this.configuration.capabilities,
622
+ archived: false,
623
+ };
624
+ }
625
+ async syncWorkers() {
626
+ const sessions = await this.listSessions();
627
+ const visibleIds = new Set(sessions.map((session) => session.sessionId));
628
+ for (const [sessionId, worker] of this.workers) {
629
+ if (visibleIds.has(sessionId))
630
+ continue;
631
+ await worker.stop("Kimi Session 已从设备清单移除");
632
+ this.workers.delete(sessionId);
633
+ }
634
+ const boardSessions = await this.board.syncSessions(sessions.map((session) => this.inventoryThread(session)), this.configuration.workingDirectories, this.modelCatalog, this.stopController.signal);
635
+ this.managedSessionIds = visibleIds;
636
+ this.inventoryReady = true;
637
+ for (const info of sessions) {
638
+ const boardSession = boardSessions.get(info.sessionId);
639
+ if (!boardSession) {
640
+ process.stderr.write(`看板未返回 Kimi Session ${info.sessionId} 的映射\n`);
641
+ continue;
642
+ }
643
+ const existing = this.workers.get(info.sessionId);
644
+ if (boardSession.deletion_requested_at) {
645
+ if (existing) {
646
+ await existing.stop("Kimi Session 正在等待 Web 删除指令");
647
+ this.workers.delete(info.sessionId);
648
+ }
649
+ continue;
650
+ }
651
+ if (existing) {
652
+ existing.update(info, boardSession);
653
+ continue;
654
+ }
655
+ const worker = new SessionWorker(info, boardSession, this.configuration, this.board, this.acp, this.limiter, (sessionId, model) => this.knownModels.set(sessionId, model));
656
+ this.workers.set(info.sessionId, worker);
657
+ const run = worker.start().catch((error) => {
658
+ this.workers.delete(info.sessionId);
659
+ if (this.stopping)
660
+ return;
661
+ if (errorStatus(error) === 403 &&
662
+ error?.code ===
663
+ "SESSION_NOT_AUTHORIZED") {
664
+ process.stderr.write(`Kimi Session ${info.sessionId} 已被看板停用;worker 已退出\n`);
665
+ }
666
+ else if (isPersistentClientError(error)) {
667
+ this.markFatal(actionableBoardError(error));
668
+ }
669
+ else {
670
+ process.stderr.write(`Kimi worker ${info.sessionId} 已退出:${errorMessage(error)}\n`);
671
+ }
672
+ });
673
+ this.workerRuns.set(info.sessionId, run);
674
+ void run.finally(() => this.workerRuns.delete(info.sessionId));
675
+ process.stdout.write(`已连接 Kimi Session:${info.sessionId}\n`);
676
+ }
677
+ process.stdout.write(`Kimi Bridge 已同步 ${sessions.length} 个 Session,最多并行 ${this.configuration.maxConcurrentTurns} 个 turn\n`);
678
+ }
679
+ async processThreadCommands() {
680
+ let inventoryChanged = false;
681
+ for (let processed = 0; processed < 10 && !this.stopping; processed += 1) {
682
+ const command = await this.board.claimThreadCommand(this.runtimeInstanceId, this.stopController.signal);
683
+ if (!command)
684
+ break;
685
+ try {
686
+ const externalThreadId = await this.executeThreadCommand(command);
687
+ await this.board.completeThreadCommand(this.runtimeInstanceId, command.id, { succeeded: true, externalThreadId }, this.stopController.signal);
688
+ inventoryChanged = true;
689
+ process.stdout.write(`Web Kimi Session 指令已完成:${command.action} (${externalThreadId ?? command.id})\n`);
690
+ }
691
+ catch (error) {
692
+ if (this.stopping)
693
+ throw error;
694
+ const message = commandError(error);
695
+ await this.board.completeThreadCommand(this.runtimeInstanceId, command.id, { succeeded: false, error: message }, this.stopController.signal);
696
+ process.stderr.write(`Web Kimi Session 指令 ${command.action} 失败:${message}\n`);
697
+ }
698
+ }
699
+ return inventoryChanged;
700
+ }
701
+ async executeThreadCommand(command) {
702
+ if (command.action === "create") {
703
+ if (this.managedSessionIds.size >= this.configuration.maxThreads) {
704
+ throw new Error("已达到 Kimi Bridge 的 Session 数量上限");
705
+ }
706
+ const name = commandString(command.name);
707
+ if (!name)
708
+ throw new Error("新建 Kimi Session 指令缺少名称");
709
+ const cwd = workingDirectoryForKey(command.directory_key, this.configuration.workingDirectories);
710
+ const created = await this.acp.newSession(cwd);
711
+ const info = {
712
+ sessionId: created.sessionId,
713
+ cwd: path.resolve(cwd),
714
+ title: name,
715
+ updatedAt: new Date().toISOString(),
716
+ };
717
+ try {
718
+ await prepareKimiSession(this.acp, info, this.configuration, commandString(command.model), commandString(command.reasoning_effort), created.configOptions);
719
+ }
720
+ catch (error) {
721
+ await this.acp.deleteSession(created.sessionId).catch((deleteError) => {
722
+ process.stderr.write(`回滚新建 Kimi Session ${created.sessionId} 失败:${errorMessage(deleteError)}\n`);
723
+ });
724
+ throw error;
725
+ }
726
+ this.knownModels.set(created.sessionId, this.acp.selectedValue(created.sessionId, "model"));
727
+ await this.acp.closeSession(created.sessionId).catch(() => undefined);
728
+ this.createdSessions.set(created.sessionId, info);
729
+ this.managedSessionIds.add(created.sessionId);
730
+ return created.sessionId;
731
+ }
732
+ const sessionId = commandString(command.external_thread_id);
733
+ if (!sessionId)
734
+ throw new Error("Kimi Session 指令缺少目标 ID");
735
+ if (command.action === "rename") {
736
+ throw new Error("当前 Kimi ACP 不提供 Session 改名能力");
737
+ }
738
+ const managed = this.managedSessionIds.has(sessionId);
739
+ if (!managed && (command.attempt_count ?? 1) > 1)
740
+ return sessionId;
741
+ if (!managed)
742
+ throw new Error("目标 Kimi Session 不在受管清单中");
743
+ const worker = this.workers.get(sessionId);
744
+ if (worker?.busy)
745
+ throw new Error("Kimi Session 正在执行任务,暂时不能删除");
746
+ if (worker) {
747
+ await worker.stop("用户从 Web Console 删除了 Kimi Session");
748
+ this.workers.delete(sessionId);
749
+ }
750
+ await this.acp.deleteSession(sessionId);
751
+ this.createdSessions.delete(sessionId);
752
+ this.knownModels.delete(sessionId);
753
+ this.managedSessionIds.delete(sessionId);
754
+ return sessionId;
755
+ }
756
+ }
757
+ export async function runBridgeCli() {
758
+ const configuration = loadConfiguration();
759
+ const acp = await KimiAcpClient.start(configuration);
760
+ const bridge = new KimiBridge(configuration, acp);
761
+ const stop = () => void bridge.stop();
762
+ process.once("SIGINT", stop);
763
+ process.once("SIGTERM", stop);
764
+ try {
765
+ await bridge.run();
766
+ }
767
+ finally {
768
+ process.removeListener("SIGINT", stop);
769
+ process.removeListener("SIGTERM", stop);
770
+ await bridge.stop();
771
+ }
772
+ }
773
+ //# sourceMappingURL=bridge.js.map