@keyou007/agent-bot 0.1.16-alpha.4 → 0.1.17-alpha.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 (45) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +4 -2
  3. package/README.zh.md +4 -2
  4. package/config.example.yaml +8 -0
  5. package/dist/cli/help.js +4 -4
  6. package/dist/cli/taskTarget.js +7 -0
  7. package/dist/cli/taskTarget.js.map +1 -1
  8. package/dist/cli.js +12 -12
  9. package/dist/cli.js.map +1 -1
  10. package/dist/codex/CodexRuntime.js +40 -12
  11. package/dist/codex/CodexRuntime.js.map +1 -1
  12. package/dist/config/AgentExecutionDefaultsStore.js +71 -0
  13. package/dist/config/AgentExecutionDefaultsStore.js.map +1 -0
  14. package/dist/config/loadConfig.js +8 -0
  15. package/dist/config/loadConfig.js.map +1 -1
  16. package/dist/config/schema.js +7 -0
  17. package/dist/config/schema.js.map +1 -1
  18. package/dist/feishu/CardRenderer.js +29 -27
  19. package/dist/feishu/CardRenderer.js.map +1 -1
  20. package/dist/index.js +17 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/proxy/ProxySessionController.js +255 -30
  23. package/dist/proxy/ProxySessionController.js.map +1 -1
  24. package/dist/state/StateStore.js +62 -9
  25. package/dist/state/StateStore.js.map +1 -1
  26. package/dist/supervision/restartNotificationTarget.js +9 -0
  27. package/dist/supervision/restartNotificationTarget.js.map +1 -0
  28. package/docs/technical-reference.md +7 -3
  29. package/docs/technical-reference.zh.md +7 -3
  30. package/npm-shrinkwrap.json +2 -2
  31. package/package.json +1 -1
  32. package/skills/agent-bot/SKILL.md +3 -1
  33. package/src/cli/help.ts +4 -4
  34. package/src/cli/taskTarget.ts +11 -0
  35. package/src/cli.ts +16 -20
  36. package/src/codex/CodexRuntime.ts +38 -10
  37. package/src/config/AgentExecutionDefaultsStore.ts +71 -0
  38. package/src/config/loadConfig.ts +8 -0
  39. package/src/config/schema.ts +13 -1
  40. package/src/feishu/CardRenderer.ts +30 -22
  41. package/src/index.ts +17 -2
  42. package/src/proxy/ProxySessionController.ts +330 -40
  43. package/src/runtime/types.ts +8 -0
  44. package/src/state/StateStore.ts +100 -9
  45. package/src/supervision/restartNotificationTarget.ts +12 -0
@@ -1112,6 +1112,18 @@ function remoteSessionSummary(thread: CodexThreadSnapshot): RemoteSessionSummary
1112
1112
  const lastText = [...(lastTurn?.items ?? [])]
1113
1113
  .reverse()
1114
1114
  .find((item) => typeof item.text === "string" && item.text.trim())?.text?.trim();
1115
+ const completedTurns = (thread.turns ?? []).flatMap((turn) => {
1116
+ if (turn.status !== "completed") return [];
1117
+ const startedAt = remoteTurnStartedAt(turn);
1118
+ return [{
1119
+ id: turn.id,
1120
+ prompt: extractTurnUserPrompt(turn),
1121
+ startedAt,
1122
+ completedAt: startedAt === undefined
1123
+ ? undefined
1124
+ : startedAt + Math.max(0, turn.durationMs ?? 0),
1125
+ }];
1126
+ });
1115
1127
  return {
1116
1128
  id: thread.id,
1117
1129
  title: normalizeTaskTitle(thread.name) ?? normalizeTaskTitle(thread.preview),
@@ -1136,25 +1148,41 @@ function remoteSessionSummary(thread: CodexThreadSnapshot): RemoteSessionSummary
1136
1148
  lastTurnCompletedToolCount: toolCounts?.completed,
1137
1149
  lastTurnFailedToolCount: toolCounts?.failed,
1138
1150
  lastTurnRunningToolCount: toolCounts?.running,
1151
+ completedTurns,
1139
1152
  };
1140
1153
  }
1141
1154
 
1142
1155
  function extractLastUserPrompt(turns: CodexTurnSnapshot[] | undefined): string | undefined {
1143
1156
  for (const turn of [...(turns ?? [])].reverse()) {
1144
- for (const item of [...(turn.items ?? [])].reverse()) {
1145
- if (item.type !== "userMessage") continue;
1146
- const text = item.content
1147
- ?.filter((content) => content.type === "text" && typeof content.text === "string")
1148
- .map((content) => content.text!.trim())
1149
- .filter(Boolean)
1150
- .join("\n\n")
1151
- || item.text?.trim();
1152
- if (text) return text;
1153
- }
1157
+ const prompt = extractTurnUserPrompt(turn);
1158
+ if (prompt) return prompt;
1159
+ }
1160
+ return undefined;
1161
+ }
1162
+
1163
+ function extractTurnUserPrompt(turn: CodexTurnSnapshot): string | undefined {
1164
+ for (const item of [...(turn.items ?? [])].reverse()) {
1165
+ if (item.type !== "userMessage") continue;
1166
+ const text = item.content
1167
+ ?.filter((content) => content.type === "text" && typeof content.text === "string")
1168
+ .map((content) => content.text!.trim())
1169
+ .filter(Boolean)
1170
+ .join("\n\n")
1171
+ || item.text?.trim();
1172
+ if (text) return text;
1154
1173
  }
1155
1174
  return undefined;
1156
1175
  }
1157
1176
 
1177
+ function remoteTurnStartedAt(turn: CodexTurnSnapshot): number | undefined {
1178
+ return remoteTimestampMs(turn.startedAt ?? undefined);
1179
+ }
1180
+
1181
+ function remoteTimestampMs(value: number | undefined): number | undefined {
1182
+ if (value === undefined || !Number.isFinite(value)) return undefined;
1183
+ return value >= 10_000_000_000 ? value : value * 1_000;
1184
+ }
1185
+
1158
1186
  function summarizeTurnTools(turn: CodexTurnSnapshot): {
1159
1187
  total: number;
1160
1188
  completed: number;
@@ -0,0 +1,71 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { isMap, parseDocument } from "yaml";
4
+ import type { AgentExecutionDefaults } from "./schema.js";
5
+
6
+ const DEFAULT_KEYS = ["modelProvider", "model", "reasoningEffort", "permissionMode"] as const;
7
+
8
+ export function writeAgentExecutionDefaults(
9
+ configPath: string,
10
+ agentName: string,
11
+ defaults: AgentExecutionDefaults,
12
+ ): boolean {
13
+ const original = fs.readFileSync(configPath, "utf8");
14
+ const document = parseDocument(original);
15
+ if (document.errors.length > 0) {
16
+ throw new Error(`Could not update configuration file ${configPath}: ${document.errors[0]?.message ?? "invalid YAML"}`);
17
+ }
18
+ if (!isMap(document.contents)) {
19
+ throw new Error(`Configuration file must contain a YAML mapping: ${configPath}`);
20
+ }
21
+ const agent = document.getIn(["agents", agentName], true);
22
+ if (!isMap(agent)) {
23
+ throw new Error(`Cannot save defaults for an Agent that is not configured: ${agentName}`);
24
+ }
25
+ const existingDefaults = agent.get("defaults", true);
26
+ if (existingDefaults !== undefined && !isMap(existingDefaults)) {
27
+ throw new Error(`Configuration agents.${agentName}.defaults must contain a YAML mapping: ${configPath}`);
28
+ }
29
+ if (!isMap(existingDefaults)) agent.set("defaults", {});
30
+ const target = agent.get("defaults", true);
31
+ if (!isMap(target)) throw new Error(`Could not create defaults for Agent: ${agentName}`);
32
+
33
+ for (const key of DEFAULT_KEYS) {
34
+ const value = defaults[key];
35
+ if (value !== undefined) target.set(key, value);
36
+ }
37
+
38
+ const updated = document.toString();
39
+ if (updated === original) return false;
40
+ writeFileAtomically(configPath, updated);
41
+ return true;
42
+ }
43
+
44
+ function writeFileAtomically(filePath: string, contents: string): void {
45
+ const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
46
+ let temporaryFile: number | undefined;
47
+ try {
48
+ temporaryFile = fs.openSync(temporaryPath, "wx", fs.statSync(filePath).mode);
49
+ fs.writeFileSync(temporaryFile, contents, "utf8");
50
+ fs.fsyncSync(temporaryFile);
51
+ fs.closeSync(temporaryFile);
52
+ temporaryFile = undefined;
53
+ fs.renameSync(temporaryPath, filePath);
54
+ syncDirectory(path.dirname(filePath));
55
+ } finally {
56
+ if (temporaryFile !== undefined) fs.closeSync(temporaryFile);
57
+ fs.rmSync(temporaryPath, { force: true });
58
+ }
59
+ }
60
+
61
+ function syncDirectory(directory: string): void {
62
+ let handle: number | undefined;
63
+ try {
64
+ handle = fs.openSync(directory, "r");
65
+ fs.fsyncSync(handle);
66
+ } catch (error) {
67
+ if (process.platform !== "win32") throw error;
68
+ } finally {
69
+ if (handle !== undefined) fs.closeSync(handle);
70
+ }
71
+ }
@@ -38,6 +38,12 @@ agents:
38
38
  - "--listen"
39
39
  - "stdio://"
40
40
  env: {}
41
+ # Updated automatically by the Provider, Model, Thinking, and Permission settings.
42
+ defaults:
43
+ # modelProvider: "openai"
44
+ # model: "gpt-5"
45
+ # reasoningEffort: "high"
46
+ permissionMode: "auto"
41
47
 
42
48
  traex:
43
49
  kind: "app-server"
@@ -48,6 +54,8 @@ agents:
48
54
  - "--listen"
49
55
  - "stdio://"
50
56
  env: {}
57
+ defaults:
58
+ permissionMode: "auto"
51
59
 
52
60
  defaults:
53
61
  agent: "codex"
@@ -40,12 +40,20 @@ const agentKindSchema = z.union([
40
40
  z.literal("codex").transform(() => "app-server" as const),
41
41
  ]).default("acp");
42
42
 
43
+ export const agentExecutionDefaultsSchema = z.object({
44
+ modelProvider: z.string().min(1).optional(),
45
+ model: z.string().min(1).optional(),
46
+ reasoningEffort: z.string().min(1).optional(),
47
+ permissionMode: z.enum(["auto", "confirm"]).optional(),
48
+ }).default({});
49
+
43
50
  export const agentConfigSchema = z.object({
44
51
  kind: agentKindSchema,
45
52
  title: z.string().min(1),
46
53
  command: z.string().min(1),
47
54
  args: z.array(z.string()).default([]),
48
55
  env: envRecordSchema,
56
+ defaults: agentExecutionDefaultsSchema,
49
57
  });
50
58
 
51
59
  export const appConfigSchema = z.object({
@@ -84,6 +92,10 @@ export const appConfigSchema = z.object({
84
92
  }).default({ level: "info", path: DEFAULT_LOG_PATH }),
85
93
  });
86
94
 
87
- export type AgentConfig = z.infer<typeof agentConfigSchema>;
95
+ export type AgentExecutionDefaults = z.infer<typeof agentExecutionDefaultsSchema>;
96
+ type ParsedAgentConfig = z.infer<typeof agentConfigSchema>;
97
+ export type AgentConfig = Omit<ParsedAgentConfig, "defaults"> & {
98
+ defaults?: AgentExecutionDefaults;
99
+ };
88
100
  export type GroupNameFormatConfig = z.infer<typeof groupNameFormatSchema>;
89
101
  export type AppConfig = z.infer<typeof appConfigSchema>;
@@ -225,7 +225,9 @@ export interface ResetHistoryCardEntry extends TaskListCardEntry {
225
225
  sequence: number;
226
226
  graphNodeLine: string;
227
227
  graphConnectorLine?: string;
228
+ timestamp?: string;
228
229
  running?: boolean;
230
+ resetting?: boolean;
229
231
  }
230
232
 
231
233
  export interface ResetHistoryCardView {
@@ -1328,9 +1330,11 @@ export class CardRenderer {
1328
1330
  entry.graphNodeLine,
1329
1331
  entry.graphConnectorLine,
1330
1332
  entry.lines,
1333
+ entry.timestamp,
1331
1334
  action,
1332
1335
  entry.current === true,
1333
1336
  entry.running === true,
1337
+ entry.resetting === true,
1334
1338
  ));
1335
1339
  });
1336
1340
  }
@@ -2415,15 +2419,8 @@ function inlineCode(value: string): string {
2415
2419
  }
2416
2420
 
2417
2421
  function formatStartupTime(value: Date): string {
2418
- return new Intl.DateTimeFormat("zh-CN", {
2419
- year: "numeric",
2420
- month: "2-digit",
2421
- day: "2-digit",
2422
- hour: "2-digit",
2423
- minute: "2-digit",
2424
- second: "2-digit",
2425
- hourCycle: "h23",
2426
- }).format(value);
2422
+ const pad = (part: number) => String(part).padStart(2, "0");
2423
+ return `${value.getFullYear()}/${pad(value.getMonth() + 1)}/${pad(value.getDate())} ${pad(value.getHours())}:${pad(value.getMinutes())}:${pad(value.getSeconds())}`;
2427
2424
  }
2428
2425
 
2429
2426
  function permissionModeLabel(mode?: PermissionMode): string {
@@ -2633,16 +2630,30 @@ function resetHistoryEntryRow(
2633
2630
  graphNodeLine: string,
2634
2631
  graphConnectorLine: string | undefined,
2635
2632
  lines: string[],
2633
+ timestamp: string | undefined,
2636
2634
  action: TaskListCardAction | undefined,
2637
2635
  current: boolean,
2638
2636
  running: boolean,
2637
+ resetting: boolean,
2639
2638
  ): Record<string, unknown> {
2639
+ const branchConnectorLine = graphConnectorLine && /[╱╲]/u.test(graphConnectorLine)
2640
+ ? graphConnectorLine
2641
+ : undefined;
2640
2642
  const graph = [
2641
- `<font color='${current ? "green" : running ? "orange" : "blue"}'>${escapeCardHtml(graphNodeLine)}</font>`,
2642
- ...(graphConnectorLine
2643
- ? [`<font color='grey'>${escapeCardHtml(graphConnectorLine)}</font>`]
2643
+ `<font color='${current ? "green" : running || resetting ? "orange" : "blue"}'>${escapeCardHtml(graphNodeLine)}</font>`,
2644
+ ...(branchConnectorLine
2645
+ ? [`<font color='grey'>${escapeCardHtml(branchConnectorLine)}</font>`]
2644
2646
  : []),
2645
2647
  ].join("\n");
2648
+ const trailingElements = current
2649
+ ? [markdown("✅ 当前")]
2650
+ : resetting
2651
+ ? [markdown("⏳ 正在 Reset")]
2652
+ : running
2653
+ ? [markdown("⏳ 运行中")]
2654
+ : action
2655
+ ? [taskActionElement(action)]
2656
+ : [];
2646
2657
  return {
2647
2658
  tag: "column_set",
2648
2659
  flex_mode: "none",
@@ -2664,25 +2675,22 @@ function resetHistoryEntryRow(
2664
2675
  weight: 1,
2665
2676
  vertical_align: "top",
2666
2677
  elements: [
2667
- markdown(lines[0] ?? ""),
2678
+ markdown([
2679
+ lines[0] ?? "",
2680
+ ...(timestamp ? [`<font color='grey'>${escapeCardHtml(timestamp)}</font>`] : []),
2681
+ ].join(" ")),
2668
2682
  ...lines.slice(1).map((line) => ({
2669
2683
  ...markdown(`<font color='grey'>${line}</font>`),
2670
2684
  text_size: "notation",
2671
2685
  })),
2672
2686
  ],
2673
2687
  },
2674
- {
2688
+ ...(trailingElements.length > 0 ? [{
2675
2689
  tag: "column",
2676
2690
  width: "auto",
2677
2691
  vertical_align: "top",
2678
- elements: current
2679
- ? [markdown("✅ 当前")]
2680
- : running
2681
- ? [markdown("⏳ 运行中")]
2682
- : action
2683
- ? [taskActionElement(action)]
2684
- : [],
2685
- },
2692
+ elements: trailingElements,
2693
+ }] : []),
2686
2694
  ],
2687
2695
  };
2688
2696
  }
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ import { controlEndpoint, type ControlRequest, type ControlResponse } from "./cl
6
6
  import { readPackageVersion } from "./cli/packageVersion.js";
7
7
  import { loadConfig } from "./config/loadConfig.js";
8
8
  import { persistFeishuUserOpenIdIfMissing } from "./config/FeishuUserOpenIdStore.js";
9
+ import { writeAgentExecutionDefaults } from "./config/AgentExecutionDefaultsStore.js";
9
10
  import {
10
11
  agentBotHome,
11
12
  defaultConfigPath,
@@ -42,6 +43,7 @@ import {
42
43
  import { SafeRestartScheduler } from "./supervision/SafeRestartScheduler.js";
43
44
  import type { RestartNotificationTarget } from "./supervision/SafeRestartScheduler.js";
44
45
  import { SafeRestartNotifier } from "./supervision/SafeRestartNotifier.js";
46
+ import { privateRestartNotificationTarget } from "./supervision/restartNotificationTarget.js";
45
47
  import {
46
48
  nodeDiagnosticReportArguments,
47
49
  prepareCrashReportDirectory,
@@ -72,9 +74,10 @@ const logger = createLogger(config);
72
74
  const store = new StateStore(config.storage.sqlitePath);
73
75
 
74
76
  const configuredConfigPath = process.env.AGENT_BOT_CONFIG?.trim();
77
+ const activeConfigPath = configuredConfigPath ? resolveUserPath(configuredConfigPath) : defaultConfigPath();
75
78
  const agentEnvironmentContext: AgentEnvironmentContext = {
76
79
  profilePath: agentBotHome(),
77
- configPath: configuredConfigPath ? resolveUserPath(configuredConfigPath) : defaultConfigPath(),
80
+ configPath: activeConfigPath,
78
81
  larkAppId: config.feishu.appId,
79
82
  larkUserOpenId: config.feishu.userOpenId,
80
83
  };
@@ -156,6 +159,10 @@ const controller = new ProxySessionController(config, store, runtimes, outbound,
156
159
  "Stored the default Lark user Open ID from the first private chat message.",
157
160
  );
158
161
  },
162
+ persistAgentExecutionDefaults: (agentName, defaults) => {
163
+ const changed = writeAgentExecutionDefaults(activeConfigPath, agentName, defaults);
164
+ logger.info({ agentName, changed }, "Stored Agent execution defaults.");
165
+ },
159
166
  });
160
167
 
161
168
  if (feishuOutbound) {
@@ -362,7 +369,7 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
362
369
  if (pendingSelfUpdatePlanPath) {
363
370
  return { ok: false, message: "An Agent Bot update is already pending." };
364
371
  }
365
- const notificationTarget = inferControlRestartTarget(request.notificationSessionId);
372
+ const notificationTarget = inferServerRestartTarget(request.notificationSessionId);
366
373
  if (request.mode === "safe") {
367
374
  const newlyScheduled = safeRestart.schedule(request.reason, notificationTarget);
368
375
  return {
@@ -571,6 +578,14 @@ function inferControlRestartTarget(notificationSessionId?: string): RestartNotif
571
578
  return session ? restartNotificationTargetForSession(session) : undefined;
572
579
  }
573
580
 
581
+ function inferServerRestartTarget(notificationSessionId?: string): RestartNotificationTarget {
582
+ if (notificationSessionId?.trim()) {
583
+ const target = inferControlRestartTarget(notificationSessionId);
584
+ if (target) return target;
585
+ }
586
+ return privateRestartNotificationTarget(config.feishu.userOpenId);
587
+ }
588
+
574
589
  function restartNotificationTargetForSession(
575
590
  session: { localSessionId: string; contextKey: string },
576
591
  ): RestartNotificationTarget {