@keyou007/agent-bot 0.1.10 → 0.1.12

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 (121) hide show
  1. package/.env.example +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +55 -43
  4. package/README.zh.md +55 -43
  5. package/assets/agent-bot-logo.png +0 -0
  6. package/config.example.yaml +11 -1
  7. package/dist/acp/AcpSessionManager.js +78 -46
  8. package/dist/acp/AcpSessionManager.js.map +1 -1
  9. package/dist/cli/FeishuAppConfiguration.js +1 -1
  10. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  11. package/dist/cli/FeishuAppRegistration.js +2 -2
  12. package/dist/cli/FeishuAppRegistration.js.map +1 -1
  13. package/dist/cli/Initializer.js +1 -1
  14. package/dist/cli/Initializer.js.map +1 -1
  15. package/dist/cli/ServerStarter.js +7 -2
  16. package/dist/cli/ServerStarter.js.map +1 -1
  17. package/dist/cli/controlProtocol.js.map +1 -1
  18. package/dist/cli/help.js +20 -16
  19. package/dist/cli/help.js.map +1 -1
  20. package/dist/cli/taskGroupOptions.js +95 -0
  21. package/dist/cli/taskGroupOptions.js.map +1 -0
  22. package/dist/cli.js +74 -2
  23. package/dist/cli.js.map +1 -1
  24. package/dist/codex/AppServerConnection.js +15 -1
  25. package/dist/codex/AppServerConnection.js.map +1 -1
  26. package/dist/codex/CodexLocalActivityDetector.js +4 -1
  27. package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
  28. package/dist/codex/CodexProcessManager.js +1 -0
  29. package/dist/codex/CodexProcessManager.js.map +1 -1
  30. package/dist/codex/CodexRuntime.js +112 -11
  31. package/dist/codex/CodexRuntime.js.map +1 -1
  32. package/dist/commands/CommandRouter.js +64 -49
  33. package/dist/commands/CommandRouter.js.map +1 -1
  34. package/dist/config/loadConfig.js +11 -1
  35. package/dist/config/loadConfig.js.map +1 -1
  36. package/dist/config/schema.js +6 -1
  37. package/dist/config/schema.js.map +1 -1
  38. package/dist/console/ConsoleConnector.js +1 -1
  39. package/dist/console/ConsoleConnector.js.map +1 -1
  40. package/dist/console/ConsoleTurnPresenter.js +15 -8
  41. package/dist/console/ConsoleTurnPresenter.js.map +1 -1
  42. package/dist/deprecated-cli.js +5 -0
  43. package/dist/deprecated-cli.js.map +1 -0
  44. package/dist/feishu/CardRenderer.js +350 -15
  45. package/dist/feishu/CardRenderer.js.map +1 -1
  46. package/dist/feishu/FeishuMessageClient.js +51 -5
  47. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  48. package/dist/feishu/FeishuTurnPresenter.js +17 -8
  49. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  50. package/dist/feishu/transport.js +1 -1
  51. package/dist/feishu/transport.js.map +1 -1
  52. package/dist/index.js +38 -19
  53. package/dist/index.js.map +1 -1
  54. package/dist/presentation/OutboundRouter.js +2 -2
  55. package/dist/presentation/OutboundRouter.js.map +1 -1
  56. package/dist/presentation/TurnStateReducer.js +2 -1
  57. package/dist/presentation/TurnStateReducer.js.map +1 -1
  58. package/dist/proxy/ProxySessionController.js +665 -225
  59. package/dist/proxy/ProxySessionController.js.map +1 -1
  60. package/dist/runtime/AcpRuntimeAdapter.js +1 -0
  61. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  62. package/dist/runtime/AgentRuntimeRegistry.js +7 -4
  63. package/dist/runtime/AgentRuntimeRegistry.js.map +1 -1
  64. package/dist/runtime/createAgentRuntimeRegistry.js +22 -0
  65. package/dist/runtime/createAgentRuntimeRegistry.js.map +1 -0
  66. package/dist/startup/SessionMetadataHydrator.js +2 -2
  67. package/dist/startup/SessionMetadataHydrator.js.map +1 -1
  68. package/dist/startup/StartupNotifier.js +1 -0
  69. package/dist/startup/StartupNotifier.js.map +1 -1
  70. package/dist/state/StateStore.js +21 -15
  71. package/dist/state/StateStore.js.map +1 -1
  72. package/dist/supervision/SafeRestartScheduler.js +5 -0
  73. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  74. package/dist/supervision/systemEnvironment.js +129 -0
  75. package/dist/supervision/systemEnvironment.js.map +1 -0
  76. package/dist/supervisor.js +13 -1
  77. package/dist/supervisor.js.map +1 -1
  78. package/docs/technical-reference.md +35 -21
  79. package/docs/technical-reference.zh.md +35 -21
  80. package/npm-shrinkwrap.json +4 -3
  81. package/package.json +4 -2
  82. package/skills/agent-bot/SKILL.md +73 -30
  83. package/src/acp/AcpSessionManager.ts +73 -54
  84. package/src/cli/FeishuAppConfiguration.ts +2 -2
  85. package/src/cli/FeishuAppRegistration.ts +4 -4
  86. package/src/cli/Initializer.ts +2 -2
  87. package/src/cli/ServerStarter.ts +8 -3
  88. package/src/cli/controlProtocol.ts +21 -1
  89. package/src/cli/help.ts +20 -16
  90. package/src/cli/taskGroupOptions.ts +149 -0
  91. package/src/cli.ts +98 -4
  92. package/src/codex/AppServerConnection.ts +21 -1
  93. package/src/codex/CodexLocalActivityDetector.ts +8 -1
  94. package/src/codex/CodexProcessManager.ts +1 -0
  95. package/src/codex/CodexRuntime.ts +137 -15
  96. package/src/commands/CommandRouter.ts +71 -49
  97. package/src/commands/commandTypes.ts +6 -11
  98. package/src/config/loadConfig.ts +11 -1
  99. package/src/config/schema.ts +7 -1
  100. package/src/console/ConsoleConnector.ts +1 -1
  101. package/src/console/ConsoleTurnPresenter.ts +21 -8
  102. package/src/deprecated-cli.ts +9 -0
  103. package/src/feishu/CardRenderer.ts +423 -15
  104. package/src/feishu/FeishuMessageClient.ts +66 -3
  105. package/src/feishu/FeishuTurnPresenter.ts +22 -3
  106. package/src/feishu/transport.ts +1 -1
  107. package/src/index.ts +54 -24
  108. package/src/presentation/OutboundRouter.ts +15 -3
  109. package/src/presentation/TurnStateReducer.ts +2 -0
  110. package/src/presentation/turnViewTypes.ts +1 -0
  111. package/src/proxy/ProxySessionController.ts +870 -228
  112. package/src/runtime/AcpRuntimeAdapter.ts +1 -0
  113. package/src/runtime/AgentRuntimeRegistry.ts +7 -6
  114. package/src/runtime/createAgentRuntimeRegistry.ts +25 -0
  115. package/src/runtime/types.ts +18 -0
  116. package/src/startup/SessionMetadataHydrator.ts +2 -2
  117. package/src/startup/StartupNotifier.ts +1 -0
  118. package/src/state/StateStore.ts +29 -15
  119. package/src/supervision/SafeRestartScheduler.ts +5 -0
  120. package/src/supervision/systemEnvironment.ts +174 -0
  121. package/src/supervisor.ts +12 -1
@@ -190,6 +190,7 @@ export class AcpRuntimeAdapter implements AgentRuntime {
190
190
  }
191
191
 
192
192
  close(): void {
193
+ this.acp.shutdown();
193
194
  this.sessions.clear();
194
195
  this.activeTurns.clear();
195
196
  this.toolsBySession.clear();
@@ -1,15 +1,16 @@
1
- import type { AgentConfig } from "../config/schema.js";
2
1
  import type { AgentRuntime, RuntimeKind } from "./types.js";
3
2
 
4
3
  export class AgentRuntimeRegistry {
5
- constructor(private readonly runtimes: Record<RuntimeKind, AgentRuntime>) {}
4
+ constructor(private readonly runtimes: Record<string, AgentRuntime>) {}
6
5
 
7
- forAgent(agent: Pick<AgentConfig, "kind">): AgentRuntime {
8
- return this.runtimes[agent.kind];
6
+ forAgent(agentName: string): AgentRuntime {
7
+ const runtime = this.runtimes[agentName];
8
+ if (!runtime) throw new Error(`Unknown agent runtime: ${agentName}`);
9
+ return runtime;
9
10
  }
10
11
 
11
- get(kind: RuntimeKind): AgentRuntime {
12
- return this.runtimes[kind];
12
+ entries(kind?: RuntimeKind): Array<[agentName: string, runtime: AgentRuntime]> {
13
+ return Object.entries(this.runtimes).filter(([, runtime]) => kind === undefined || runtime.kind === kind);
13
14
  }
14
15
 
15
16
  close(): void {
@@ -0,0 +1,25 @@
1
+ import type { Logger } from "pino";
2
+ import { AcpProcessManager } from "../acp/AcpProcessManager.js";
3
+ import { AcpSessionManager } from "../acp/AcpSessionManager.js";
4
+ import { CodexProcessManager } from "../codex/CodexProcessManager.js";
5
+ import { CodexRuntime } from "../codex/CodexRuntime.js";
6
+ import type { AppConfig } from "../config/schema.js";
7
+ import { AcpRuntimeAdapter } from "./AcpRuntimeAdapter.js";
8
+ import { AgentRuntimeRegistry } from "./AgentRuntimeRegistry.js";
9
+ import type { AgentRuntime } from "./types.js";
10
+
11
+ export function createAgentRuntimeRegistry(config: AppConfig, logger: Logger): AgentRuntimeRegistry {
12
+ const runtimes: Record<string, AgentRuntime> = {};
13
+ for (const [agentName, agent] of Object.entries(config.agents)) {
14
+ const runtimeLogger = logger.child({ agentName, runtimeKind: agent.kind });
15
+ if (agent.kind === "app-server") {
16
+ const processManager = new CodexProcessManager(agent.command, agent.args, agent.env, runtimeLogger);
17
+ runtimes[agentName] = new CodexRuntime(processManager, runtimeLogger);
18
+ continue;
19
+ }
20
+ const processManager = new AcpProcessManager(runtimeLogger);
21
+ const sessionManager = new AcpSessionManager(agentName, agent, processManager, runtimeLogger);
22
+ runtimes[agentName] = new AcpRuntimeAdapter(sessionManager);
23
+ }
24
+ return new AgentRuntimeRegistry(runtimes);
25
+ }
@@ -109,6 +109,7 @@ export interface RemoteSessionSummary {
109
109
  cwd: string;
110
110
  source: string;
111
111
  status: RemoteSessionStatus;
112
+ modelProvider?: string;
112
113
  model?: string;
113
114
  reasoningEffort?: string;
114
115
  permissionMode?: PermissionMode;
@@ -144,6 +145,7 @@ export interface RuntimeSession {
144
145
  agentName: string;
145
146
  cwd: string;
146
147
  title?: string;
148
+ modelProvider?: string;
147
149
  model?: string;
148
150
  reasoningEffort?: string;
149
151
  permissionMode: PermissionMode;
@@ -155,6 +157,7 @@ export interface CreateRuntimeSessionInput {
155
157
  agentName: string;
156
158
  cwd: string;
157
159
  title?: string;
160
+ modelProvider?: string;
158
161
  model?: string;
159
162
  reasoningEffort?: string;
160
163
  permissionMode: PermissionMode;
@@ -180,6 +183,19 @@ export interface ModelOption {
180
183
  defaultReasoningEffort?: string;
181
184
  }
182
185
 
186
+ export interface ModelProviderOption {
187
+ id: string;
188
+ displayName?: string;
189
+ isDefault?: boolean;
190
+ }
191
+
192
+ export interface RuntimeExecutionSettings {
193
+ modelProvider: string;
194
+ model: string;
195
+ reasoningEffort: string;
196
+ permissionMode: PermissionMode;
197
+ }
198
+
183
199
  export interface ReasoningEffortOption {
184
200
  value: string;
185
201
  description?: string;
@@ -208,8 +224,10 @@ export interface AgentRuntime {
208
224
  setModel(sessionId: string, model: string): Promise<void>;
209
225
  setReasoningEffort(sessionId: string, effort: string): Promise<void>;
210
226
  setPermissionMode(sessionId: string, mode: PermissionMode): Promise<void>;
227
+ setExecutionSettings?(sessionId: string, settings: RuntimeExecutionSettings): Promise<RuntimeSession>;
211
228
  respondToApproval(sessionId: string, requestId: string, decision: ApprovalDecision): Promise<void>;
212
229
  listModels(): Promise<ModelOption[]>;
230
+ listModelProviders?(): Promise<ModelProviderOption[]>;
213
231
  onEvent(listener: (event: RuntimeEvent) => void): () => void;
214
232
  close(): void;
215
233
  }
@@ -16,7 +16,7 @@ export class SessionMetadataHydrator {
16
16
  && session.status === "running"
17
17
  && session.lastTurnStatus === "running"
18
18
  ) {
19
- const runtime = this.runtimes.get("codex");
19
+ const runtime = this.runtimes.forAgent(session.agentName);
20
20
  const loaded = runtime.getSession(session.localSessionId) ?? await runtime.resumeSession({
21
21
  localSessionId: session.localSessionId,
22
22
  remoteSessionId: session.remoteSessionId,
@@ -38,7 +38,7 @@ export class SessionMetadataHydrator {
38
38
  }
39
39
  if (session.title || !session.runtimeKind || !session.remoteSessionId) return session;
40
40
  if (session.runtimeKind === "codex" && !session.lastTurnId) return session;
41
- const metadata = await this.runtimes.get(session.runtimeKind).readSessionMetadata(session.remoteSessionId);
41
+ const metadata = await this.runtimes.forAgent(session.agentName).readSessionMetadata(session.remoteSessionId);
42
42
  const title = normalizeTaskTitle(metadata.title);
43
43
  if (title) this.store.updateRuntimeSession(session.localSessionId, { title });
44
44
  return this.store.getSession(session.localSessionId) ?? session;
@@ -84,6 +84,7 @@ export class StartupNotifier {
84
84
  ? {
85
85
  id: session.remoteSessionId ?? session.localSessionId,
86
86
  title: session.title,
87
+ modelProvider: session.modelProvider,
87
88
  model: session.model,
88
89
  reasoningEffort: session.reasoningEffort,
89
90
  permissionMode: session.permissionMode,
@@ -32,6 +32,7 @@ export interface SessionRecord {
32
32
  runtimeKind?: "acp" | "codex";
33
33
  remoteSessionId?: string;
34
34
  title?: string;
35
+ modelProvider?: string;
35
36
  model?: string;
36
37
  reasoningEffort?: string;
37
38
  permissionMode?: "auto" | "confirm";
@@ -100,6 +101,7 @@ interface SessionRow {
100
101
  runtime_kind: "acp" | "codex" | null;
101
102
  remote_session_id: string | null;
102
103
  title: string | null;
104
+ model_provider: string | null;
103
105
  model: string | null;
104
106
  reasoning_effort: string | null;
105
107
  permission_mode: "auto" | "confirm" | null;
@@ -149,10 +151,11 @@ export class StateStore {
149
151
  this.ensureTurnSnapshotColumns();
150
152
  this.ensureChatContextColumns();
151
153
  this.initializeContextSessionMappings();
154
+ this.db.exec("DROP INDEX IF EXISTS idx_sessions_remote_session_id_unique");
152
155
  this.reconcileDuplicateRemoteSessions();
153
156
  this.db.exec(`
154
- CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_remote_session_id_unique
155
- ON sessions(remote_session_id)
157
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_agent_remote_session_unique
158
+ ON sessions(agent_name, remote_session_id)
156
159
  WHERE remote_session_id IS NOT NULL
157
160
  `);
158
161
  this.db.exec(`
@@ -520,7 +523,7 @@ export class StateStore {
520
523
  patch: Partial<
521
524
  Pick<
522
525
  SessionRecord,
523
- "runtimeKind" | "remoteSessionId" | "title" | "model" | "reasoningEffort" | "permissionMode" | "lastTurnId" | "lastTurnStatus"
526
+ "runtimeKind" | "remoteSessionId" | "title" | "modelProvider" | "model" | "reasoningEffort" | "permissionMode" | "lastTurnId" | "lastTurnStatus"
524
527
  >
525
528
  >,
526
529
  ): void {
@@ -533,7 +536,7 @@ export class StateStore {
533
536
  .prepare(
534
537
  `
535
538
  UPDATE sessions
536
- SET runtime_kind = ?, remote_session_id = ?, title = ?, model = ?, reasoning_effort = ?, permission_mode = ?,
539
+ SET runtime_kind = ?, remote_session_id = ?, title = ?, model_provider = ?, model = ?, reasoning_effort = ?, permission_mode = ?,
537
540
  last_turn_id = ?, last_turn_status = ?, updated_at = ?
538
541
  WHERE local_session_id = ?
539
542
  `,
@@ -542,6 +545,7 @@ export class StateStore {
542
545
  patch.runtimeKind ?? existing.runtimeKind ?? null,
543
546
  patch.remoteSessionId ?? existing.remoteSessionId ?? existing.acpSessionId ?? null,
544
547
  patch.title ?? existing.title ?? null,
548
+ patch.modelProvider ?? existing.modelProvider ?? null,
545
549
  patch.model ?? existing.model ?? null,
546
550
  patch.reasoningEffort ?? existing.reasoningEffort ?? null,
547
551
  patch.permissionMode ?? existing.permissionMode ?? null,
@@ -814,7 +818,11 @@ export class StateStore {
814
818
  };
815
819
  }
816
820
 
817
- findSessionByRemoteSessionId(remoteSessionId: string, contextKey?: string): SessionRecord | undefined {
821
+ findSessionByRemoteSessionId(
822
+ remoteSessionId: string,
823
+ contextKey?: string,
824
+ agentName?: string,
825
+ ): SessionRecord | undefined {
818
826
  const row = contextKey
819
827
  ? this.db
820
828
  .prepare(`
@@ -823,17 +831,20 @@ export class StateStore {
823
831
  JOIN context_sessions ON context_sessions.local_session_id = sessions.local_session_id
824
832
  WHERE sessions.remote_session_id = ?
825
833
  AND context_sessions.context_key = ?
834
+ AND (? IS NULL OR sessions.agent_name = ?)
826
835
  AND sessions.status != 'closed'
827
836
  LIMIT 1
828
837
  `)
829
- .get(remoteSessionId, contextKey) as SessionRow | undefined
838
+ .get(remoteSessionId, contextKey, agentName ?? null, agentName ?? null) as SessionRow | undefined
830
839
  : this.db
831
840
  .prepare(`
832
841
  SELECT * FROM sessions
833
- WHERE remote_session_id = ? AND status != 'closed'
842
+ WHERE remote_session_id = ?
843
+ AND (? IS NULL OR agent_name = ?)
844
+ AND status != 'closed'
834
845
  ORDER BY created_at ASC LIMIT 1
835
846
  `)
836
- .get(remoteSessionId) as SessionRow | undefined;
847
+ .get(remoteSessionId, agentName ?? null, agentName ?? null) as SessionRow | undefined;
837
848
  return row ? { ...mapSession(row), ...(contextKey ? { contextKey } : {}) } : undefined;
838
849
  }
839
850
 
@@ -1034,21 +1045,21 @@ export class StateStore {
1034
1045
 
1035
1046
  private reconcileDuplicateRemoteSessions(): void {
1036
1047
  const duplicateIds = this.db.prepare(`
1037
- SELECT remote_session_id
1048
+ SELECT agent_name, remote_session_id
1038
1049
  FROM sessions
1039
1050
  WHERE remote_session_id IS NOT NULL
1040
- GROUP BY remote_session_id
1051
+ GROUP BY agent_name, remote_session_id
1041
1052
  HAVING count(*) > 1
1042
- `).all() as Array<{ remote_session_id: string }>;
1053
+ `).all() as Array<{ agent_name: string; remote_session_id: string }>;
1043
1054
  if (duplicateIds.length === 0) return;
1044
1055
 
1045
1056
  const reconcile = this.db.transaction(() => {
1046
- for (const { remote_session_id: remoteSessionId } of duplicateIds) {
1057
+ for (const { agent_name: agentName, remote_session_id: remoteSessionId } of duplicateIds) {
1047
1058
  const rows = this.db.prepare(`
1048
1059
  SELECT * FROM sessions
1049
- WHERE remote_session_id = ?
1060
+ WHERE agent_name = ? AND remote_session_id = ?
1050
1061
  ORDER BY created_at ASC
1051
- `).all(remoteSessionId) as SessionRow[];
1062
+ `).all(agentName, remoteSessionId) as SessionRow[];
1052
1063
  const canonical = rows[0];
1053
1064
  if (!canonical) continue;
1054
1065
  const latest = [...rows].sort((left, right) => right.updated_at.localeCompare(left.updated_at))[0] ?? canonical;
@@ -1086,7 +1097,7 @@ export class StateStore {
1086
1097
 
1087
1098
  this.db.prepare(`
1088
1099
  UPDATE sessions
1089
- SET agent_name = ?, cwd = ?, acp_session_id = ?, runtime_kind = ?, title = ?, model = ?,
1100
+ SET agent_name = ?, cwd = ?, acp_session_id = ?, runtime_kind = ?, title = ?, model_provider = ?, model = ?,
1090
1101
  reasoning_effort = ?, permission_mode = ?, last_turn_id = ?, last_turn_status = ?,
1091
1102
  status = ?, updated_at = ?
1092
1103
  WHERE local_session_id = ?
@@ -1096,6 +1107,7 @@ export class StateStore {
1096
1107
  latest.acp_session_id,
1097
1108
  latest.runtime_kind,
1098
1109
  latest.title,
1110
+ latest.model_provider,
1099
1111
  latest.model,
1100
1112
  latest.reasoning_effort,
1101
1113
  latest.permission_mode,
@@ -1124,6 +1136,7 @@ export class StateStore {
1124
1136
  ["runtime_kind", "TEXT"],
1125
1137
  ["remote_session_id", "TEXT"],
1126
1138
  ["title", "TEXT"],
1139
+ ["model_provider", "TEXT"],
1127
1140
  ["model", "TEXT"],
1128
1141
  ["reasoning_effort", "TEXT"],
1129
1142
  ["permission_mode", "TEXT"],
@@ -1206,6 +1219,7 @@ function mapSession(row: SessionRow): SessionRecord {
1206
1219
  runtimeKind: row.runtime_kind ?? (row.acp_session_id ? "acp" : undefined),
1207
1220
  remoteSessionId: row.remote_session_id ?? row.acp_session_id ?? undefined,
1208
1221
  title: row.title ?? undefined,
1222
+ modelProvider: row.model_provider ?? undefined,
1209
1223
  model: row.model ?? undefined,
1210
1224
  reasoningEffort: row.reasoning_effort ?? undefined,
1211
1225
  permissionMode: row.permission_mode ?? undefined,
@@ -64,6 +64,11 @@ export class SafeRestartScheduler {
64
64
  this.clear();
65
65
  }
66
66
 
67
+ async cancelCurrent(): Promise<boolean> {
68
+ if (!this.reason) return false;
69
+ return this.cancelScheduled(this.scheduleId);
70
+ }
71
+
67
72
  async cancelScheduled(expectedScheduleId: number): Promise<boolean> {
68
73
  const reason = this.reason;
69
74
  if (!reason || this.scheduleId !== expectedScheduleId) return false;
@@ -0,0 +1,174 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import path from "node:path";
3
+
4
+ export interface WindowsEnvironmentSnapshot {
5
+ machine: Record<string, string>;
6
+ user: Record<string, string>;
7
+ }
8
+
9
+ export interface EnvironmentRefreshResult {
10
+ environment: NodeJS.ProcessEnv;
11
+ refreshed: boolean;
12
+ pathChanged: boolean;
13
+ error?: string;
14
+ }
15
+
16
+ type WindowsEnvironmentLoader = () => WindowsEnvironmentSnapshot;
17
+
18
+ const PROCESS_LOCAL_ENVIRONMENT_PREFIXES = ["AGENT_BOT_", "FEISHU_"];
19
+
20
+ export function refreshedSystemEnvironment(
21
+ inherited: NodeJS.ProcessEnv = process.env,
22
+ platform: NodeJS.Platform = process.platform,
23
+ loadWindowsEnvironment: WindowsEnvironmentLoader = readWindowsEnvironment,
24
+ ): EnvironmentRefreshResult {
25
+ const fallback = { ...inherited };
26
+ if (platform !== "win32") {
27
+ return { environment: fallback, refreshed: false, pathChanged: false };
28
+ }
29
+
30
+ try {
31
+ const snapshot = loadWindowsEnvironment();
32
+ const environment = mergeWindowsEnvironment(inherited, snapshot);
33
+ return {
34
+ environment,
35
+ refreshed: true,
36
+ pathChanged: environmentValue(environment, "Path") !== environmentValue(inherited, "Path"),
37
+ };
38
+ } catch (error) {
39
+ return {
40
+ environment: fallback,
41
+ refreshed: false,
42
+ pathChanged: false,
43
+ error: error instanceof Error ? error.message : String(error),
44
+ };
45
+ }
46
+ }
47
+
48
+ export function mergeWindowsEnvironment(
49
+ inherited: NodeJS.ProcessEnv,
50
+ snapshot: WindowsEnvironmentSnapshot,
51
+ ): NodeJS.ProcessEnv {
52
+ const environment = { ...inherited };
53
+ applyEnvironment(environment, snapshot.machine);
54
+ applyEnvironment(environment, snapshot.user);
55
+
56
+ const machinePath = environmentValue(snapshot.machine, "Path")?.trim();
57
+ const userPath = environmentValue(snapshot.user, "Path")?.trim();
58
+ if (machinePath || userPath) {
59
+ const inheritedPath = environmentValue(inherited, "Path")?.trim();
60
+ setEnvironmentValue(
61
+ environment,
62
+ "Path",
63
+ mergePathValues(
64
+ expandEnvironmentReferences(
65
+ [machinePath, userPath, inheritedPath].filter(Boolean).join(";"),
66
+ environment,
67
+ ),
68
+ ),
69
+ );
70
+ }
71
+ return environment;
72
+ }
73
+
74
+ function readWindowsEnvironment(): WindowsEnvironmentSnapshot {
75
+ const systemRoot = process.env.SystemRoot ?? "C:\\Windows";
76
+ const executable = path.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
77
+ const script = [
78
+ "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)",
79
+ "$machine = @{}",
80
+ "foreach ($entry in [Environment]::GetEnvironmentVariables([EnvironmentVariableTarget]::Machine).GetEnumerator()) { $machine[[string]$entry.Key] = [string]$entry.Value }",
81
+ "$user = @{}",
82
+ "foreach ($entry in [Environment]::GetEnvironmentVariables([EnvironmentVariableTarget]::User).GetEnumerator()) { $user[[string]$entry.Key] = [string]$entry.Value }",
83
+ "[pscustomobject]@{ machine = $machine; user = $user } | ConvertTo-Json -Compress -Depth 3",
84
+ ].join("; ");
85
+ const result = spawnSync(executable, [
86
+ "-NoLogo",
87
+ "-NoProfile",
88
+ "-NonInteractive",
89
+ "-ExecutionPolicy",
90
+ "Bypass",
91
+ "-Command",
92
+ script,
93
+ ], {
94
+ encoding: "utf8",
95
+ windowsHide: true,
96
+ timeout: 5_000,
97
+ maxBuffer: 8 * 1024 * 1024,
98
+ });
99
+ if (result.error) throw result.error;
100
+ if (result.status !== 0) {
101
+ throw new Error(result.stderr.trim() || `PowerShell exited with code ${result.status ?? "unknown"}.`);
102
+ }
103
+ const parsed = JSON.parse(result.stdout) as Partial<WindowsEnvironmentSnapshot>;
104
+ return {
105
+ machine: environmentRecord(parsed.machine),
106
+ user: environmentRecord(parsed.user),
107
+ };
108
+ }
109
+
110
+ function environmentRecord(value: unknown): Record<string, string> {
111
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
112
+ return Object.fromEntries(
113
+ Object.entries(value).flatMap(([name, entry]) =>
114
+ typeof entry === "string" ? [[name, entry]] : []),
115
+ );
116
+ }
117
+
118
+ function applyEnvironment(
119
+ target: NodeJS.ProcessEnv,
120
+ source: Record<string, string>,
121
+ ): void {
122
+ for (const [name, value] of Object.entries(source)) {
123
+ if (name.toLowerCase() === "path" || isProcessLocalEnvironmentName(name)) continue;
124
+ setEnvironmentValue(target, name, value);
125
+ }
126
+ }
127
+
128
+ function isProcessLocalEnvironmentName(name: string): boolean {
129
+ const normalized = name.toUpperCase();
130
+ return PROCESS_LOCAL_ENVIRONMENT_PREFIXES.some((prefix) => normalized.startsWith(prefix));
131
+ }
132
+
133
+ function setEnvironmentValue(environment: NodeJS.ProcessEnv, name: string, value: string): void {
134
+ for (const existingName of Object.keys(environment)) {
135
+ if (existingName !== name && existingName.toLowerCase() === name.toLowerCase()) {
136
+ delete environment[existingName];
137
+ }
138
+ }
139
+ environment[name] = value;
140
+ }
141
+
142
+ function environmentValue(
143
+ environment: NodeJS.ProcessEnv | Record<string, string>,
144
+ name: string,
145
+ ): string | undefined {
146
+ const key = Object.keys(environment).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
147
+ return key === undefined ? undefined : environment[key];
148
+ }
149
+
150
+ function expandEnvironmentReferences(value: string, environment: NodeJS.ProcessEnv): string {
151
+ let expanded = value;
152
+ for (let attempt = 0; attempt < 5; attempt += 1) {
153
+ const next = expanded.replace(/%([^%]+)%/g, (match, name: string) =>
154
+ environmentValue(environment, name) ?? match);
155
+ if (next === expanded) break;
156
+ expanded = next;
157
+ }
158
+ return expanded;
159
+ }
160
+
161
+ function mergePathValues(value: string): string {
162
+ const seen = new Set<string>();
163
+ return value
164
+ .split(";")
165
+ .map((entry) => entry.trim())
166
+ .filter((entry) => {
167
+ if (!entry) return false;
168
+ const normalized = entry.replace(/[\\/]+$/, "").toLowerCase();
169
+ if (seen.has(normalized)) return false;
170
+ seen.add(normalized);
171
+ return true;
172
+ })
173
+ .join(";");
174
+ }
package/src/supervisor.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  STOP_EXIT_CODE,
14
14
  } from "./supervision/restartPolicy.js";
15
15
  import { SupervisorDiagnostics, nodeDiagnosticReportArguments } from "./supervision/SupervisorDiagnostics.js";
16
+ import { refreshedSystemEnvironment } from "./supervision/systemEnvironment.js";
16
17
 
17
18
  const childEntry = fileURLToPath(new URL("./index.js", import.meta.url));
18
19
  const config = loadConfig();
@@ -38,13 +39,23 @@ async function startChild(): Promise<void> {
38
39
  const restartReason = nextStartReason;
39
40
  nextStartReason = "Supervisor 重新拉起进程";
40
41
  const workerStderr = diagnostics.openWorkerStderr();
42
+ const environmentRefresh = refreshedSystemEnvironment();
43
+ if (environmentRefresh.error) {
44
+ writeSupervisorLog("environment_refresh_failed", { error: environmentRefresh.error });
45
+ } else if (environmentRefresh.refreshed) {
46
+ writeSupervisorLog("environment_refreshed", { pathChanged: environmentRefresh.pathChanged });
47
+ }
41
48
  try {
42
49
  child = spawn(process.execPath, [
43
50
  ...nodeDiagnosticReportArguments(diagnostics.paths.crashReportDirectory),
44
51
  childEntry,
45
52
  ], {
46
53
  cwd: process.cwd(),
47
- env: { ...process.env, AGENT_BOT_SUPERVISED: "1", AGENT_BOT_RESTART_REASON: restartReason },
54
+ env: {
55
+ ...environmentRefresh.environment,
56
+ AGENT_BOT_SUPERVISED: "1",
57
+ AGENT_BOT_RESTART_REASON: restartReason,
58
+ },
48
59
  stdio: ["ignore", "ignore", workerStderr],
49
60
  windowsHide: true,
50
61
  });