@keyou007/agent-bot 0.1.14-alpha.1 → 0.1.14-alpha.3

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 (80) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README.md +48 -15
  3. package/README.zh.md +48 -15
  4. package/config.example.yaml +1 -1
  5. package/dist/cli/AutostartManager.js +431 -0
  6. package/dist/cli/AutostartManager.js.map +1 -0
  7. package/dist/cli/FeishuAppConfiguration.js +21 -13
  8. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  9. package/dist/cli/Initializer.js +35 -0
  10. package/dist/cli/Initializer.js.map +1 -1
  11. package/dist/cli/SelfUpdateRunner.js +552 -0
  12. package/dist/cli/SelfUpdateRunner.js.map +1 -0
  13. package/dist/cli/SelfUpdater.js +431 -0
  14. package/dist/cli/SelfUpdater.js.map +1 -0
  15. package/dist/cli/autostartOutput.js +63 -0
  16. package/dist/cli/autostartOutput.js.map +1 -0
  17. package/dist/cli/controlProtocol.js.map +1 -1
  18. package/dist/cli/help.js +94 -18
  19. package/dist/cli/help.js.map +1 -1
  20. package/dist/cli/taskGroupOptions.js +14 -11
  21. package/dist/cli/taskGroupOptions.js.map +1 -1
  22. package/dist/cli/taskListOutput.js +3 -0
  23. package/dist/cli/taskListOutput.js.map +1 -1
  24. package/dist/cli/taskTarget.js +99 -0
  25. package/dist/cli/taskTarget.js.map +1 -0
  26. package/dist/cli.js +661 -58
  27. package/dist/cli.js.map +1 -1
  28. package/dist/codex/CodexRuntime.js +8 -2
  29. package/dist/codex/CodexRuntime.js.map +1 -1
  30. package/dist/commands/CommandRouter.js +16 -1
  31. package/dist/commands/CommandRouter.js.map +1 -1
  32. package/dist/feishu/CardRenderer.js +266 -38
  33. package/dist/feishu/CardRenderer.js.map +1 -1
  34. package/dist/feishu/FeishuMarkdown.js +25 -1
  35. package/dist/feishu/FeishuMarkdown.js.map +1 -1
  36. package/dist/feishu/FeishuMessageClient.js +16 -0
  37. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  38. package/dist/feishu/FeishuTurnPresenter.js +42 -6
  39. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  40. package/dist/index.js +136 -2
  41. package/dist/index.js.map +1 -1
  42. package/dist/presentation/OutboundRouter.js +7 -0
  43. package/dist/presentation/OutboundRouter.js.map +1 -1
  44. package/dist/presentation/TurnStateReducer.js +21 -7
  45. package/dist/presentation/TurnStateReducer.js.map +1 -1
  46. package/dist/proxy/ProxySessionController.js +537 -13
  47. package/dist/proxy/ProxySessionController.js.map +1 -1
  48. package/dist/state/StateStore.js +53 -0
  49. package/dist/state/StateStore.js.map +1 -1
  50. package/docs/technical-reference.md +17 -7
  51. package/docs/technical-reference.zh.md +17 -7
  52. package/npm-shrinkwrap.json +2 -2
  53. package/package.json +2 -2
  54. package/skills/agent-bot/SKILL.md +109 -188
  55. package/src/cli/AutostartManager.ts +579 -0
  56. package/src/cli/FeishuAppConfiguration.ts +26 -7
  57. package/src/cli/Initializer.ts +57 -0
  58. package/src/cli/SelfUpdateRunner.ts +817 -0
  59. package/src/cli/SelfUpdater.ts +688 -0
  60. package/src/cli/autostartOutput.ts +75 -0
  61. package/src/cli/controlProtocol.ts +119 -1
  62. package/src/cli/help.ts +94 -18
  63. package/src/cli/taskGroupOptions.ts +27 -17
  64. package/src/cli/taskListOutput.ts +10 -0
  65. package/src/cli/taskTarget.ts +168 -0
  66. package/src/cli.ts +809 -65
  67. package/src/codex/CodexRuntime.ts +8 -2
  68. package/src/commands/CommandRouter.ts +14 -1
  69. package/src/commands/commandTypes.ts +2 -0
  70. package/src/feishu/CardRenderer.ts +313 -35
  71. package/src/feishu/FeishuMarkdown.ts +27 -1
  72. package/src/feishu/FeishuMessageClient.ts +33 -0
  73. package/src/feishu/FeishuTurnPresenter.ts +43 -6
  74. package/src/feishu/types.ts +1 -0
  75. package/src/index.ts +174 -2
  76. package/src/presentation/OutboundRouter.ts +7 -0
  77. package/src/presentation/TurnStateReducer.ts +21 -7
  78. package/src/proxy/ProxySessionController.ts +661 -16
  79. package/src/runtime/types.ts +1 -0
  80. package/src/state/StateStore.ts +56 -0
@@ -39,6 +39,13 @@ interface CreateGroupResponse {
39
39
  error?: unknown;
40
40
  }
41
41
 
42
+ interface DeleteGroupResponse {
43
+ code: number;
44
+ msg: string;
45
+ data?: Record<string, never>;
46
+ error?: unknown;
47
+ }
48
+
42
49
  interface UploadImageResponse {
43
50
  code: number;
44
51
  msg: string;
@@ -128,6 +135,31 @@ export class FeishuMessageClient implements FeishuOutbound {
128
135
  }
129
136
  }
130
137
 
138
+ async deleteGroup(chatId: string): Promise<void> {
139
+ try {
140
+ const token = await this.getTenantAccessToken();
141
+ const response = await fetch(
142
+ `https://open.feishu.cn/open-apis/im/v1/chats/${encodeURIComponent(chatId)}`,
143
+ {
144
+ method: "DELETE",
145
+ headers: { Authorization: `Bearer ${token}` },
146
+ },
147
+ );
148
+ const payload = (await response.json()) as DeleteGroupResponse;
149
+ if (!response.ok || payload.code !== 0) {
150
+ throw new FeishuApiError(
151
+ payload.msg || response.statusText,
152
+ payload.code,
153
+ payload,
154
+ "delete group",
155
+ response.status,
156
+ );
157
+ }
158
+ } catch (error) {
159
+ throw normalizeTransportError(error, "delete group");
160
+ }
161
+ }
162
+
131
163
  async addReaction(messageId: string, emojiType: string): Promise<string | undefined> {
132
164
  try {
133
165
  const token = await this.getTenantAccessToken();
@@ -731,6 +763,7 @@ export class FeishuApiError extends Error {
731
763
  readonly payload:
732
764
  | SendMessageResponse
733
765
  | CreateGroupResponse
766
+ | DeleteGroupResponse
734
767
  | UploadImageResponse
735
768
  | UploadFileResponse
736
769
  | ReactionResponse
@@ -42,12 +42,14 @@ export interface TurnPresentationStore {
42
42
  export interface FeishuTurnPresenterOptions {
43
43
  normalIntervalMs?: number;
44
44
  criticalGapMs?: number;
45
+ elapsedUpdateIntervalMs?: number;
45
46
  finalChunkLength?: number;
46
47
  onError?: (error: unknown) => void;
47
48
  finalRetryBackoffMs?: number[];
48
49
  }
49
50
 
50
51
  const MAX_FINAL_TABLES_PER_CARD = 5;
52
+ const DEFAULT_ELAPSED_UPDATE_INTERVAL_MS = 3_000;
51
53
 
52
54
  interface TurnEntry {
53
55
  contextKey: string;
@@ -58,6 +60,7 @@ interface TurnEntry {
58
60
  reactionId?: string;
59
61
  reactionEmoji?: string;
60
62
  scheduler?: CardUpdateScheduler<TurnViewState>;
63
+ elapsedTimer?: ReturnType<typeof setInterval>;
61
64
  finalizing?: Promise<void>;
62
65
  lastFinalizedStatus?: TurnViewState["status"];
63
66
  }
@@ -148,6 +151,7 @@ export class FeishuTurnPresenter {
148
151
  await entry.initializing;
149
152
  return state.turnId;
150
153
  } catch (error) {
154
+ this.stopElapsedUpdates(entry);
151
155
  if (this.pendingEntries.get(sessionId) === entry) this.pendingEntries.delete(sessionId);
152
156
  this.entries.delete(state.turnId);
153
157
  throw error;
@@ -158,7 +162,15 @@ export class FeishuTurnPresenter {
158
162
  const entry = this.pendingEntries.get(sessionId);
159
163
  if (!entry) return;
160
164
  this.pendingEntries.delete(sessionId);
161
- entry.state = { ...entry.state, status: "failed", error: message, completedAt: Date.now() };
165
+ this.stopElapsedUpdates(entry);
166
+ const completedAt = Date.now();
167
+ entry.state = {
168
+ ...entry.state,
169
+ status: "failed",
170
+ error: message,
171
+ completedAt,
172
+ durationMs: Math.max(0, completedAt - entry.state.startedAt),
173
+ };
162
174
  this.store.saveTurnSnapshot(entry.state.turnId, sessionId, entry.state, entry.contextKey);
163
175
  await entry.initializing;
164
176
  try {
@@ -178,24 +190,28 @@ export class FeishuTurnPresenter {
178
190
  ): Promise<void> {
179
191
  const snapshot = this.store.getTurnSnapshot(turnId);
180
192
  if (!isTurnViewState(snapshot) || snapshot.sessionId !== sessionId || isTerminalViewStatus(snapshot.status)) return;
193
+ const completedAt = Date.now();
181
194
  const state: TurnViewState = {
182
195
  ...snapshot,
183
196
  status: "cancelled",
184
197
  progressText: message,
185
198
  activeTool: undefined,
186
199
  approval: undefined,
187
- completedAt: Date.now(),
200
+ completedAt,
201
+ durationMs: Math.max(0, completedAt - snapshot.startedAt),
188
202
  };
189
203
  this.store.saveTurnSnapshot(turnId, sessionId, state, contextKey);
190
- const delivery = this.store.getTurnDelivery(turnId);
191
- if (delivery?.progressMessageId) {
192
- await this.outbound.updateInteractiveCard(delivery.progressMessageId, this.renderer.renderTurn(state));
193
- }
194
204
  const entry = this.entries.get(turnId);
195
205
  if (entry) {
206
+ entry.state = state;
207
+ this.stopElapsedUpdates(entry);
196
208
  this.entries.delete(turnId);
197
209
  if (this.pendingEntries.get(sessionId) === entry) this.pendingEntries.delete(sessionId);
198
210
  }
211
+ const delivery = this.store.getTurnDelivery(turnId);
212
+ if (delivery?.progressMessageId) {
213
+ await this.outbound.updateInteractiveCard(delivery.progressMessageId, this.renderer.renderTurn(state));
214
+ }
199
215
  }
200
216
 
201
217
  async appendSteerMessage(
@@ -287,6 +303,7 @@ export class FeishuTurnPresenter {
287
303
  if (!entry.scheduler) return;
288
304
 
289
305
  if (isTerminalEvent(event)) {
306
+ this.stopElapsedUpdates(entry);
290
307
  if (entry.finalizing) await entry.finalizing;
291
308
  const terminalState = entry.state;
292
309
  if (entry.lastFinalizedStatus === terminalState.status) return;
@@ -434,10 +451,30 @@ export class FeishuTurnPresenter {
434
451
  onError: this.options.onError,
435
452
  });
436
453
  entry.scheduler.seed(sentState);
454
+ this.startElapsedUpdates(entry);
437
455
  if (entry.state !== sentState) entry.scheduler.update(entry.state, "critical");
438
456
  await this.syncThinkingCardReaction(entry);
439
457
  }
440
458
 
459
+ private startElapsedUpdates(entry: TurnEntry): void {
460
+ if (entry.elapsedTimer || isTerminalViewStatus(entry.state.status)) return;
461
+ const intervalMs = Math.max(1, this.options.elapsedUpdateIntervalMs ?? DEFAULT_ELAPSED_UPDATE_INTERVAL_MS);
462
+ entry.elapsedTimer = setInterval(() => {
463
+ if (isTerminalViewStatus(entry.state.status)) {
464
+ this.stopElapsedUpdates(entry);
465
+ return;
466
+ }
467
+ if (!entry.historySnapshot) entry.scheduler?.update(entry.state);
468
+ }, intervalMs);
469
+ entry.elapsedTimer.unref?.();
470
+ }
471
+
472
+ private stopElapsedUpdates(entry: TurnEntry): void {
473
+ if (!entry.elapsedTimer) return;
474
+ clearInterval(entry.elapsedTimer);
475
+ entry.elapsedTimer = undefined;
476
+ }
477
+
441
478
  private async finalize(entry: TurnEntry, state: TurnViewState): Promise<void> {
442
479
  let terminalCardError: unknown;
443
480
  if (!entry.historySnapshot) {
@@ -50,6 +50,7 @@ export interface ChatUpdatedEvent {
50
50
 
51
51
  export interface FeishuOutbound {
52
52
  createGroup?(input: CreateGroupInput): Promise<CreatedGroup>;
53
+ deleteGroup?(chatId: string): Promise<void>;
53
54
  addReaction?(messageId: string, emojiType: string): Promise<string | undefined>;
54
55
  deleteReaction?(messageId: string, reactionId: string): Promise<void>;
55
56
  downloadImage?(messageId: string, imageKey: string): Promise<string>;
package/src/index.ts CHANGED
@@ -48,6 +48,12 @@ import {
48
48
  resolveSupervisorDiagnosticsPaths,
49
49
  } from "./supervision/SupervisorDiagnostics.js";
50
50
  import { refreshedSystemEnvironment } from "./supervision/systemEnvironment.js";
51
+ import {
52
+ assertSelfUpdatePlanPath,
53
+ launchSelfUpdateRunner,
54
+ readPendingSelfUpdate,
55
+ releaseSelfUpdatePlan,
56
+ } from "./cli/SelfUpdater.js";
51
57
 
52
58
  const processStartedAt = new Date();
53
59
  const agentBotVersion = readPackageVersion();
@@ -119,16 +125,26 @@ if (routes.length === 0) throw new Error("Neither Feishu nor console input is en
119
125
  const outbound = new OutboundRouter(routes);
120
126
  let shuttingDown = false;
121
127
  let restartRequested = false;
128
+ let pendingSelfUpdatePlanPath: string | undefined;
122
129
  const safeRestart = new SafeRestartScheduler({
123
130
  readActivity: () => store.getServerActivityState(),
124
- onReady: (reason, notificationTargets) => initiateRestart(reason, notificationTargets),
131
+ onReady: (reason, notificationTargets) => pendingSelfUpdatePlanPath
132
+ ? initiateSelfUpdate(reason, notificationTargets)
133
+ : initiateRestart(reason, notificationTargets),
125
134
  onStatus: (status) => safeRestartNotifier?.update(status),
126
135
  onStatusError: (error) => logger.warn({ error }, "Failed to publish safe restart status."),
127
136
  });
128
137
  const controller = new ProxySessionController(config, store, runtimes, outbound, logger, {
129
138
  supervised,
130
139
  restart: requestRestart,
131
- cancelSafeRestart: (scheduleId) => safeRestart.cancelScheduled(scheduleId),
140
+ cancelSafeRestart: async (scheduleId) => {
141
+ const cancelled = await safeRestart.cancelScheduled(scheduleId);
142
+ if (cancelled && pendingSelfUpdatePlanPath) {
143
+ releaseSelfUpdatePlan(pendingSelfUpdatePlanPath);
144
+ pendingSelfUpdatePlanPath = undefined;
145
+ }
146
+ return cancelled;
147
+ },
132
148
  rememberFeishuUserOpenId: (userOpenId) => {
133
149
  if (config.feishu.userOpenId?.startsWith("ou_")) return;
134
150
  const persisted = persistFeishuUserOpenIdIfMissing(defaultDotEnvPath(), userOpenId);
@@ -185,6 +201,27 @@ await controller.recoverInterruptedTasks().catch((error: unknown) => {
185
201
  logger.error({ error }, "Startup task recovery failed; the server will remain available.");
186
202
  });
187
203
  consoleConnector?.start();
204
+ recoverPendingSelfUpdate();
205
+
206
+ function recoverPendingSelfUpdate(): void {
207
+ const pending = readPendingSelfUpdate(agentBotHome());
208
+ if (!pending || pendingSelfUpdatePlanPath || restartRequested || safeRestart.scheduled) return;
209
+ let notificationTarget: RestartNotificationTarget | undefined;
210
+ try {
211
+ notificationTarget = inferControlRestartTarget(pending.plan.notificationSessionId);
212
+ } catch (error) {
213
+ logger.warn({ error }, "Could not restore the self-update notification target; update recovery will continue.");
214
+ }
215
+ pendingSelfUpdatePlanPath = pending.planPath;
216
+ safeRestart.schedule(
217
+ pending.plan.reason ?? `Resume Agent Bot update to ${pending.plan.toVersion}`,
218
+ notificationTarget,
219
+ );
220
+ logger.warn(
221
+ { planPath: pending.planPath, toVersion: pending.plan.toVersion },
222
+ "Recovered a prepared Agent Bot self-update after Worker restart.",
223
+ );
224
+ }
188
225
 
189
226
  async function requestRestart(
190
227
  contextKey: string,
@@ -199,6 +236,10 @@ async function requestRestart(
199
236
  await outbound.sendText(contextKey, "Agent Bot 已在重启中,请稍候。").catch(() => undefined);
200
237
  return;
201
238
  }
239
+ if (pendingSelfUpdatePlanPath) {
240
+ await outbound.sendText(contextKey, "Agent Bot 更新已在等待中;更新完成后服务会自动恢复。").catch(() => undefined);
241
+ return;
242
+ }
202
243
  if (force) {
203
244
  await initiateRestart("用户执行 /restart --force 命令", [notificationTarget]);
204
245
  return;
@@ -223,6 +264,7 @@ async function initiateRestart(
223
264
  return;
224
265
  }
225
266
  restartRequested = true;
267
+ pendingSelfUpdatePlanPath = undefined;
226
268
  await safeRestart.cancelCurrent();
227
269
  await safeRestartNotifier?.flush();
228
270
  const allNotificationTargets = mergeRestartNotificationTargets(
@@ -240,6 +282,55 @@ async function initiateRestart(
240
282
  ), 100);
241
283
  }
242
284
 
285
+ async function initiateSelfUpdate(
286
+ reason: string,
287
+ notificationTargets: RestartNotificationTarget[] = [],
288
+ ): Promise<void> {
289
+ if (restartRequested) {
290
+ await sendRestartTexts(notificationTargets, "Agent Bot 已在重启或更新中,请稍候。");
291
+ return;
292
+ }
293
+ const planPath = pendingSelfUpdatePlanPath;
294
+ if (!planPath) return;
295
+ restartRequested = true;
296
+ await safeRestartNotifier?.flush();
297
+ const allNotificationTargets = mergeRestartNotificationTargets(
298
+ notificationTargets,
299
+ safeRestartNotifier?.getNotificationTargets() ?? [],
300
+ );
301
+ await sendRestartTexts(
302
+ allNotificationTargets,
303
+ "Agent Bot 正在更新,完成或回滚后会自动恢复服务并发送启动状态通知。",
304
+ );
305
+ const environmentRefresh = refreshedSystemEnvironment();
306
+ if (environmentRefresh.error) {
307
+ logger.warn({ error: environmentRefresh.error }, "Failed to refresh the environment for Agent Bot update.");
308
+ }
309
+ try {
310
+ assertSelfUpdatePlanPath(planPath, agentBotHome());
311
+ launchSelfUpdateRunner(planPath, {
312
+ waitPids: [process.pid, ...(supervised ? [process.ppid] : [])],
313
+ environment: replacementSupervisorEnvironment(
314
+ reason,
315
+ environmentRefresh.environment,
316
+ allNotificationTargets,
317
+ ),
318
+ });
319
+ } catch (error) {
320
+ restartRequested = false;
321
+ releaseSelfUpdatePlan(planPath);
322
+ pendingSelfUpdatePlanPath = undefined;
323
+ logger.error({ error, planPath }, "Failed to launch Agent Bot self-update; keeping the current service online.");
324
+ await sendRestartTexts(
325
+ allNotificationTargets,
326
+ `Agent Bot 更新未启动,当前服务保持在线:${error instanceof Error ? error.message : String(error)}`,
327
+ );
328
+ return;
329
+ }
330
+ pendingSelfUpdatePlanPath = undefined;
331
+ setTimeout(() => void shutdown(STOP_EXIT_CODE), 100);
332
+ }
333
+
243
334
  async function handleControlRequest(request: ControlRequest): Promise<ControlResponse> {
244
335
  switch (request.action) {
245
336
  case "health":
@@ -268,6 +359,9 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
268
359
  },
269
360
  };
270
361
  case "server_restart": {
362
+ if (pendingSelfUpdatePlanPath) {
363
+ return { ok: false, message: "An Agent Bot update is already pending." };
364
+ }
271
365
  const notificationTarget = inferControlRestartTarget(request.notificationSessionId);
272
366
  if (request.mode === "safe") {
273
367
  const newlyScheduled = safeRestart.schedule(request.reason, notificationTarget);
@@ -284,18 +378,92 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
284
378
  ), 25);
285
379
  return { ok: true, message: "Immediate restart requested." };
286
380
  }
381
+ case "server_update": {
382
+ if (restartRequested || pendingSelfUpdatePlanPath || safeRestart.scheduled) {
383
+ return { ok: false, message: "A restart or update is already pending." };
384
+ }
385
+ assertSelfUpdatePlanPath(request.planPath, agentBotHome());
386
+ const notificationTarget = inferControlRestartTarget(request.notificationSessionId);
387
+ pendingSelfUpdatePlanPath = request.planPath;
388
+ safeRestart.schedule(request.reason, notificationTarget);
389
+ return {
390
+ ok: true,
391
+ message: "Agent Bot update scheduled after active tasks finish and the server becomes idle.",
392
+ };
393
+ }
287
394
  case "server_stop":
288
395
  safeRestart.cancel();
396
+ if (pendingSelfUpdatePlanPath) releaseSelfUpdatePlan(pendingSelfUpdatePlanPath);
397
+ pendingSelfUpdatePlanPath = undefined;
289
398
  setTimeout(() => void shutdown(STOP_EXIT_CODE), 25);
290
399
  return { ok: true, message: "Agent Bot server stop requested." };
291
400
  case "task_status":
292
401
  return { ok: true, data: await controller.controlGetTaskStatus(request.localSessionId) };
293
402
  case "task_stop":
294
403
  return { ok: true, message: await controller.controlStopTask(request.localSessionId) };
404
+ case "task_archive":
405
+ return { ok: true, data: await controller.controlArchiveTask(request.localSessionId) };
406
+ case "task_dismiss":
407
+ return { ok: true, data: await controller.controlDismissTask(request.localSessionId) };
295
408
  case "task_title":
296
409
  return { ok: true, message: await controller.controlSetTaskTitle(request.localSessionId, request.title) };
297
410
  case "task_prompt":
298
411
  return { ok: true, message: await controller.controlSendTaskPrompt(request.localSessionId, request.text) };
412
+ case "task_new":
413
+ return {
414
+ ok: true,
415
+ data: await controller.controlCreateTask(
416
+ request.localSessionId,
417
+ request.title,
418
+ request.cwd,
419
+ request.projectless === true,
420
+ request.agentName,
421
+ ),
422
+ };
423
+ case "task_fork":
424
+ return { ok: true, data: await controller.controlForkTask(request.localSessionId) };
425
+ case "task_switch":
426
+ return {
427
+ ok: true,
428
+ data: await controller.controlSwitchTask(
429
+ request.localSessionId,
430
+ request.targetLocalSessionId,
431
+ request.previous === true,
432
+ ),
433
+ };
434
+ case "task_queue":
435
+ return { ok: true, data: await controller.controlQueueTaskPrompt(request.localSessionId, request.text) };
436
+ case "task_agent":
437
+ return { ok: true, data: controller.controlTaskAgent(request.localSessionId, request.agentName) };
438
+ case "task_settings":
439
+ return {
440
+ ok: true,
441
+ data: await controller.controlTaskSettings(request.localSessionId, request.setting, request.value),
442
+ };
443
+ case "task_goal":
444
+ return {
445
+ ok: true,
446
+ data: await controller.controlTaskGoal(
447
+ request.localSessionId,
448
+ request.goalAction,
449
+ request.objective,
450
+ ),
451
+ };
452
+ case "task_turns":
453
+ return { ok: true, data: controller.controlListTaskTurns(request.localSessionId) };
454
+ case "task_reset":
455
+ return { ok: true, data: await controller.controlResetTaskToTurn(request.localSessionId, request.turnId) };
456
+ case "task_mute":
457
+ return { ok: true, data: controller.controlTaskMute(request.localSessionId, request.enabled) };
458
+ case "task_shell":
459
+ return { ok: true, data: await controller.controlRunTaskShell(request.localSessionId, request.command) };
460
+ case "task_directory":
461
+ return {
462
+ ok: true,
463
+ data: await controller.controlListTaskDirectory(request.localSessionId, request.directory, request.page),
464
+ };
465
+ case "task_send_file":
466
+ return { ok: true, data: { messageId: await controller.controlSendTaskFile(request.localSessionId, request.filePath) } };
299
467
  case "task_new_group":
300
468
  return {
301
469
  ok: true,
@@ -327,6 +495,10 @@ async function shutdown(
327
495
  ): Promise<void> {
328
496
  if (shuttingDown) return;
329
497
  shuttingDown = true;
498
+ if (pendingSelfUpdatePlanPath) {
499
+ releaseSelfUpdatePlan(pendingSelfUpdatePlanPath);
500
+ pendingSelfUpdatePlanPath = undefined;
501
+ }
330
502
  logger.info({ exitCode }, "Shutting down Agent Bot.");
331
503
  safeRestart.cancel();
332
504
  feishuConnector?.stop();
@@ -172,6 +172,13 @@ export class OutboundRouter {
172
172
  return createGroup.call(outbound, input);
173
173
  }
174
174
 
175
+ async deleteGroup(contextKey: string, chatId: string): Promise<void> {
176
+ const outbound = this.route(contextKey).outbound;
177
+ const deleteGroup = outbound.deleteGroup;
178
+ if (!deleteGroup) throw new Error("当前消息通道不支持解散飞书群。");
179
+ await deleteGroup.call(outbound, chatId);
180
+ }
181
+
175
182
  withReplyTarget<T>(
176
183
  contextKey: string,
177
184
  target: MessageReplyTarget | undefined,
@@ -102,31 +102,45 @@ export function reduceTurnEvent(state: TurnViewState, event: AgentEvent): TurnVi
102
102
  return { ...state, status: "waiting_for_approval", approval: event.request };
103
103
  case "approval_resolved":
104
104
  return { ...state, status: state.activeTool ? "tool_running" : "running", approval: undefined };
105
- case "turn_completed":
105
+ case "turn_completed": {
106
+ const durationMs = Math.max(0, event.durationMs ?? Date.now() - state.startedAt);
106
107
  return {
107
108
  ...state,
108
109
  status: "completed",
109
110
  activeTool: undefined,
110
111
  approval: undefined,
111
- completedAt: state.startedAt + (event.durationMs ?? Date.now() - state.startedAt),
112
- durationMs: event.durationMs ?? Date.now() - state.startedAt,
112
+ completedAt: state.startedAt + durationMs,
113
+ durationMs,
113
114
  finalResponse: appendGeneratedImageMarkdown(
114
115
  event.finalResponse,
115
116
  state.completedTools.flatMap((tool) =>
116
117
  tool.kind === "image_generation" && tool.imagePath ? [tool.imagePath] : []),
117
118
  ),
118
119
  };
119
- case "turn_cancelled":
120
- return { ...state, status: "cancelled", activeTool: undefined, approval: undefined, completedAt: Date.now() };
121
- case "turn_failed":
120
+ }
121
+ case "turn_cancelled": {
122
+ const completedAt = Date.now();
123
+ return {
124
+ ...state,
125
+ status: "cancelled",
126
+ activeTool: undefined,
127
+ approval: undefined,
128
+ completedAt,
129
+ durationMs: Math.max(0, completedAt - state.startedAt),
130
+ };
131
+ }
132
+ case "turn_failed": {
133
+ const completedAt = Date.now();
122
134
  return {
123
135
  ...state,
124
136
  status: "failed",
125
137
  activeTool: undefined,
126
138
  approval: undefined,
127
- completedAt: Date.now(),
139
+ completedAt,
140
+ durationMs: Math.max(0, completedAt - state.startedAt),
128
141
  error: bound(event.message),
129
142
  };
143
+ }
130
144
  }
131
145
  }
132
146