@keyou007/agent-bot 0.1.11 → 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 (82) hide show
  1. package/.env.example +1 -1
  2. package/CHANGELOG.md +10 -0
  3. package/README.md +41 -35
  4. package/README.zh.md +41 -35
  5. package/config.example.yaml +11 -1
  6. package/dist/cli/FeishuAppConfiguration.js +1 -1
  7. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  8. package/dist/cli/FeishuAppRegistration.js +2 -2
  9. package/dist/cli/FeishuAppRegistration.js.map +1 -1
  10. package/dist/cli/Initializer.js +1 -1
  11. package/dist/cli/Initializer.js.map +1 -1
  12. package/dist/cli/ServerStarter.js +7 -2
  13. package/dist/cli/ServerStarter.js.map +1 -1
  14. package/dist/cli/controlProtocol.js.map +1 -1
  15. package/dist/cli/help.js +20 -16
  16. package/dist/cli/help.js.map +1 -1
  17. package/dist/cli/taskGroupOptions.js +95 -0
  18. package/dist/cli/taskGroupOptions.js.map +1 -0
  19. package/dist/cli.js +74 -2
  20. package/dist/cli.js.map +1 -1
  21. package/dist/codex/CodexRuntime.js +35 -9
  22. package/dist/codex/CodexRuntime.js.map +1 -1
  23. package/dist/config/loadConfig.js +11 -1
  24. package/dist/config/loadConfig.js.map +1 -1
  25. package/dist/config/schema.js +6 -1
  26. package/dist/config/schema.js.map +1 -1
  27. package/dist/console/ConsoleConnector.js +1 -1
  28. package/dist/console/ConsoleConnector.js.map +1 -1
  29. package/dist/console/ConsoleTurnPresenter.js +14 -7
  30. package/dist/console/ConsoleTurnPresenter.js.map +1 -1
  31. package/dist/deprecated-cli.js +5 -0
  32. package/dist/deprecated-cli.js.map +1 -0
  33. package/dist/feishu/CardRenderer.js +5 -4
  34. package/dist/feishu/CardRenderer.js.map +1 -1
  35. package/dist/feishu/FeishuTurnPresenter.js +9 -5
  36. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  37. package/dist/feishu/transport.js +1 -1
  38. package/dist/feishu/transport.js.map +1 -1
  39. package/dist/index.js +20 -2
  40. package/dist/index.js.map +1 -1
  41. package/dist/presentation/OutboundRouter.js +2 -2
  42. package/dist/presentation/OutboundRouter.js.map +1 -1
  43. package/dist/presentation/TurnStateReducer.js +2 -1
  44. package/dist/presentation/TurnStateReducer.js.map +1 -1
  45. package/dist/proxy/ProxySessionController.js +78 -22
  46. package/dist/proxy/ProxySessionController.js.map +1 -1
  47. package/dist/runtime/createAgentRuntimeRegistry.js +1 -1
  48. package/dist/runtime/createAgentRuntimeRegistry.js.map +1 -1
  49. package/dist/supervision/systemEnvironment.js +129 -0
  50. package/dist/supervision/systemEnvironment.js.map +1 -0
  51. package/dist/supervisor.js +13 -1
  52. package/dist/supervisor.js.map +1 -1
  53. package/docs/technical-reference.md +21 -17
  54. package/docs/technical-reference.zh.md +21 -17
  55. package/npm-shrinkwrap.json +4 -3
  56. package/package.json +3 -2
  57. package/skills/agent-bot/SKILL.md +61 -28
  58. package/src/cli/FeishuAppConfiguration.ts +2 -2
  59. package/src/cli/FeishuAppRegistration.ts +4 -4
  60. package/src/cli/Initializer.ts +2 -2
  61. package/src/cli/ServerStarter.ts +8 -3
  62. package/src/cli/controlProtocol.ts +21 -1
  63. package/src/cli/help.ts +20 -16
  64. package/src/cli/taskGroupOptions.ts +149 -0
  65. package/src/cli.ts +98 -4
  66. package/src/codex/CodexRuntime.ts +46 -13
  67. package/src/config/loadConfig.ts +11 -1
  68. package/src/config/schema.ts +7 -1
  69. package/src/console/ConsoleConnector.ts +1 -1
  70. package/src/console/ConsoleTurnPresenter.ts +20 -7
  71. package/src/deprecated-cli.ts +9 -0
  72. package/src/feishu/CardRenderer.ts +5 -4
  73. package/src/feishu/FeishuTurnPresenter.ts +15 -1
  74. package/src/feishu/transport.ts +1 -1
  75. package/src/index.ts +33 -2
  76. package/src/presentation/OutboundRouter.ts +15 -3
  77. package/src/presentation/TurnStateReducer.ts +2 -0
  78. package/src/presentation/turnViewTypes.ts +1 -0
  79. package/src/proxy/ProxySessionController.ts +166 -22
  80. package/src/runtime/createAgentRuntimeRegistry.ts +1 -1
  81. package/src/supervision/systemEnvironment.ts +174 -0
  82. package/src/supervisor.ts +12 -1
@@ -148,10 +148,23 @@ interface ResolvedThreadForkAnchor {
148
148
  }
149
149
 
150
150
  interface CreatedFeishuGroupContext {
151
+ chatId: string;
151
152
  contextKey: string;
152
153
  name: string;
153
154
  }
154
155
 
156
+ interface CreatedFeishuTaskGroup {
157
+ group: CreatedFeishuGroupContext;
158
+ task: SessionRecord;
159
+ }
160
+
161
+ export interface ControlTaskGroupResult {
162
+ sourceLocalSessionId: string;
163
+ sourceTurnId?: string;
164
+ group: CreatedFeishuGroupContext;
165
+ task: SessionRecord;
166
+ }
167
+
155
168
  export interface ProxyLifecycle {
156
169
  supervised?: boolean;
157
170
  restart(contextKey: string, force: boolean): Promise<void>;
@@ -615,6 +628,7 @@ export class ProxySessionController {
615
628
  responseContextKey,
616
629
  scopedRecord.title,
617
630
  scopedRecord.cwd,
631
+ this.agentLabel(scopedRecord.agentName),
618
632
  );
619
633
  const promptMessageId = await this.outbound.withReplyTarget(
620
634
  responseContextKey,
@@ -641,6 +655,82 @@ export class ProxySessionController {
641
655
  return `The Prompt was posted to the original chat and submitted to the task: ${record.title ?? record.remoteSessionId ?? record.localSessionId}`;
642
656
  }
643
657
 
658
+ async controlCreateTaskGroup(
659
+ localSessionId: string,
660
+ requestedTitle: string | undefined,
661
+ userOpenId: string | undefined,
662
+ requestedProjectCwd?: string,
663
+ forceProjectless = false,
664
+ requestedAgentName?: string,
665
+ ): Promise<ControlTaskGroupResult> {
666
+ const source = this.store.getSession(localSessionId);
667
+ if (!source || source.status === "closed") throw new Error(`Task not found: ${localSessionId}`);
668
+ const agentName = requestedAgentName?.trim() || source.agentName;
669
+ const agent = this.ensureAgent(agentName);
670
+ if (forceProjectless && agent.kind !== "app-server") {
671
+ throw new Error("task newgroup --nodir is only available for Codex agents.");
672
+ }
673
+ const sourceContextKey = this.outbound.getSessionContextKey(localSessionId) ?? source.contextKey;
674
+ const replyTarget = this.outbound.getSessionReplyTarget(localSessionId);
675
+ const boundProjectCwd = forceProjectless
676
+ ? undefined
677
+ : requestedProjectCwd === undefined
678
+ ? detectProjectlessWorkspace(source.cwd) ? undefined : source.cwd
679
+ : resolveUserPath(requestedProjectCwd);
680
+ const created = await this.outbound.withReplyTarget(sourceContextKey, replyTarget, () =>
681
+ this.createFeishuGroupWithTask(
682
+ sourceContextKey,
683
+ agentName,
684
+ requestedTitle,
685
+ userOpenId,
686
+ boundProjectCwd,
687
+ source.agentName === agentName
688
+ ? {
689
+ modelProvider: source.modelProvider,
690
+ model: source.model,
691
+ reasoningEffort: source.reasoningEffort,
692
+ permissionMode: source.permissionMode,
693
+ }
694
+ : {},
695
+ ));
696
+ return {
697
+ sourceLocalSessionId: source.localSessionId,
698
+ group: created.group,
699
+ task: created.task,
700
+ };
701
+ }
702
+
703
+ async controlForkTaskGroup(
704
+ localSessionId: string,
705
+ requestedTitle: string | undefined,
706
+ userOpenId: string | undefined,
707
+ ): Promise<ControlTaskGroupResult> {
708
+ const source = this.store.getSession(localSessionId);
709
+ if (!source || source.status === "closed") throw new Error(`Task not found: ${localSessionId}`);
710
+ const sourceContextKey = this.outbound.getSessionContextKey(localSessionId) ?? source.contextKey;
711
+ const replyTarget = this.outbound.getSessionReplyTarget(localSessionId);
712
+ const plan = await this.prepareForkSession(sourceContextKey, localSessionId, requestedTitle);
713
+ const prepared: ForkGroupSessionPlan = {
714
+ plan,
715
+ sourceDescription: plan.forkedFromHistoricalTurn
716
+ ? "当前任务最近已完成轮次"
717
+ : "当前任务最新轮次",
718
+ };
719
+ const forked = await this.outbound.withReplyTarget(sourceContextKey, replyTarget, () =>
720
+ this.forkPreparedSessionToFeishuGroup(
721
+ sourceContextKey,
722
+ prepared,
723
+ userOpenId ?? "",
724
+ "当前任务",
725
+ ));
726
+ return {
727
+ sourceLocalSessionId: source.localSessionId,
728
+ sourceTurnId: plan.lastTurnId,
729
+ group: forked.group,
730
+ task: forked.task,
731
+ };
732
+ }
733
+
644
734
  private cardActionContextKey(action: CardAction): string {
645
735
  const explicit = typeof action.value.contextKey === "string" ? action.value.contextKey : undefined;
646
736
  if (explicit && baseChatContextKey(explicit) === baseChatContextKey(action.contextKey)) return explicit;
@@ -676,8 +766,8 @@ export class ProxySessionController {
676
766
  await this.runShellCommand(contextKey, command.command);
677
767
  return;
678
768
  case "new":
679
- if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "codex") {
680
- throw new Error("/new --nodir 仅支持 Codex Agent。");
769
+ if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "app-server") {
770
+ throw new Error("/new --nodir 仅支持 App Server Agent。");
681
771
  }
682
772
  await this.createSession(
683
773
  contextKey,
@@ -696,8 +786,8 @@ export class ProxySessionController {
696
786
  );
697
787
  return;
698
788
  case "newgroup":
699
- if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "codex") {
700
- throw new Error("/newgroup --nodir 仅支持 Codex Agent。");
789
+ if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "app-server") {
790
+ throw new Error("/newgroup --nodir 仅支持 App Server Agent。");
701
791
  }
702
792
  await this.createFeishuGroup(
703
793
  contextKey,
@@ -772,7 +862,13 @@ export class ProxySessionController {
772
862
  const turnContextKey = session.lastTurnId
773
863
  ? this.store.getTurnContextKey(session.lastTurnId) ?? context.contextKey
774
864
  : context.contextKey;
775
- this.outbound.registerSession(session.localSessionId, turnContextKey, session.title, session.cwd);
865
+ this.outbound.registerSession(
866
+ session.localSessionId,
867
+ turnContextKey,
868
+ session.title,
869
+ session.cwd,
870
+ this.agentLabel(session.agentName),
871
+ );
776
872
  if (!session.lastTurnId) continue;
777
873
  void this.outbound.resumeDelivery(session.localSessionId, turnContextKey, session.lastTurnId).catch((error: unknown) => {
778
874
  this.logger.warn({ error, sessionId: session.localSessionId }, "Failed to restore persisted turn delivery.");
@@ -838,7 +934,13 @@ export class ProxySessionController {
838
934
  ): Promise<void> {
839
935
  const configuredRuntime = this.runtimes.forAgent(record.agentName);
840
936
  if (!configuredRuntime.getSession(record.localSessionId)) {
841
- this.outbound.registerSession(record.localSessionId, contextKey, record.title, record.cwd);
937
+ this.outbound.registerSession(
938
+ record.localSessionId,
939
+ contextKey,
940
+ record.title,
941
+ record.cwd,
942
+ this.agentLabel(record.agentName),
943
+ );
842
944
  await this.outbound.startPendingTurn(record.localSessionId, contextKey, record.title, replyTarget, text);
843
945
  }
844
946
  let loaded: LoadedSession;
@@ -1098,7 +1200,13 @@ export class ProxySessionController {
1098
1200
  permissionMode: source.permissionMode ?? "auto",
1099
1201
  });
1100
1202
  this.store.setCurrentSession(message.contextKey, localSessionId);
1101
- this.outbound.registerSession(localSessionId, message.contextKey, forkTitle, source.cwd);
1203
+ this.outbound.registerSession(
1204
+ localSessionId,
1205
+ message.contextKey,
1206
+ forkTitle,
1207
+ source.cwd,
1208
+ this.agentLabel(source.agentName),
1209
+ );
1102
1210
 
1103
1211
  try {
1104
1212
  const forked = await runtime.forkSession({
@@ -1411,7 +1519,13 @@ export class ProxySessionController {
1411
1519
  lastTurnStatus: plan.lastTurnStatus,
1412
1520
  });
1413
1521
  this.store.setCurrentSession(contextKey, localSessionId);
1414
- this.outbound.registerSession(localSessionId, contextKey, forked.title ?? plan.forkTitle, plan.cwd);
1522
+ this.outbound.registerSession(
1523
+ localSessionId,
1524
+ contextKey,
1525
+ forked.title ?? plan.forkTitle,
1526
+ plan.cwd,
1527
+ this.agentLabel(plan.agentName),
1528
+ );
1415
1529
  this.store.audit(contextKey, "session_forked", {
1416
1530
  sourceLocalSessionId: plan.source?.localSessionId,
1417
1531
  sourceRemoteSessionId: plan.remoteSessionId,
@@ -1566,7 +1680,7 @@ export class ProxySessionController {
1566
1680
  const agent = this.ensureAgent(agentName);
1567
1681
  const localSessionId = createId("sess");
1568
1682
  const initialTitle = normalizeTaskTitle(requestedTitle ?? prompt ?? "");
1569
- const sessionCwd = cwd === undefined && agent.kind === "codex"
1683
+ const sessionCwd = cwd === undefined && agent.kind === "app-server"
1570
1684
  ? createProjectlessWorkspace({ prompt: initialTitle }).cwd
1571
1685
  : path.resolve(cwd ?? this.config.defaults.cwd);
1572
1686
  const record = this.store.createSession({ localSessionId, contextKey, agentName, cwd: sessionCwd, status: "starting" });
@@ -1580,7 +1694,13 @@ export class ProxySessionController {
1580
1694
  });
1581
1695
  }
1582
1696
  this.store.setCurrentSession(contextKey, localSessionId);
1583
- this.outbound.registerSession(localSessionId, contextKey, initialTitle, sessionCwd);
1697
+ this.outbound.registerSession(
1698
+ localSessionId,
1699
+ contextKey,
1700
+ initialTitle,
1701
+ sessionCwd,
1702
+ this.agentLabel(agentName),
1703
+ );
1584
1704
  const runtime = this.runtimes.forAgent(agentName);
1585
1705
  try {
1586
1706
  if (prepareTurn) {
@@ -1666,9 +1786,9 @@ export class ProxySessionController {
1666
1786
  userId: string | undefined,
1667
1787
  boundProjectCwd: string | undefined,
1668
1788
  executionSettings: SessionExecutionSettings,
1669
- ): Promise<void> {
1789
+ ): Promise<CreatedFeishuTaskGroup> {
1670
1790
  const explicitTitle = normalizeTaskTitle(requestedTitle);
1671
- const taskTitle = explicitTitle ?? "新任务";
1791
+ const taskTitle = explicitTitle ?? `新任务 (${formatGroupDateSuffix(new Date())})`;
1672
1792
  const group = await this.createFeishuGroupContext(
1673
1793
  sourceContextKey,
1674
1794
  agentName,
@@ -1676,7 +1796,7 @@ export class ProxySessionController {
1676
1796
  userId,
1677
1797
  boundProjectCwd,
1678
1798
  "/newgroup",
1679
- !explicitTitle,
1799
+ false,
1680
1800
  );
1681
1801
 
1682
1802
  let task: SessionRecord;
@@ -1719,6 +1839,7 @@ export class ProxySessionController {
1719
1839
  sourceContextKey,
1720
1840
  `已创建飞书群:${group.name},并创建新任务 ${taskDescription}。`,
1721
1841
  );
1842
+ return { group, task };
1722
1843
  }
1723
1844
 
1724
1845
  private async forkCurrentSessionToFeishuGroup(
@@ -1766,7 +1887,7 @@ export class ProxySessionController {
1766
1887
  prepared: ForkGroupSessionPlan,
1767
1888
  userId: string,
1768
1889
  sourceSummary: string,
1769
- ): Promise<void> {
1890
+ ): Promise<CreatedFeishuTaskGroup> {
1770
1891
  const { plan } = prepared;
1771
1892
  const boundProjectCwd = detectProjectlessWorkspace(plan.cwd) ? undefined : plan.cwd;
1772
1893
  const group = await this.createFeishuGroupContext(
@@ -1809,6 +1930,7 @@ export class ProxySessionController {
1809
1930
  sourceContextKey,
1810
1931
  `已将${sourceSummary} Fork 到飞书群:${group.name};新群当前任务为 ${taskDescription}。`,
1811
1932
  );
1933
+ return { group, task: forked.record };
1812
1934
  }
1813
1935
 
1814
1936
  private async createFeishuGroupContext(
@@ -1836,7 +1958,7 @@ export class ProxySessionController {
1836
1958
  this.store.recordChatContext(groupContextKey, "group");
1837
1959
  this.store.getOrCreateUserContext(groupContextKey, agentName);
1838
1960
  if (boundProjectCwd) this.store.setBoundProjectCwd(groupContextKey, boundProjectCwd);
1839
- return { contextKey: groupContextKey, name: group.name };
1961
+ return { chatId: group.chatId, contextKey: groupContextKey, name: group.name };
1840
1962
  }
1841
1963
 
1842
1964
  private async loadSession(record: SessionRecord): Promise<LoadedSession> {
@@ -1847,7 +1969,13 @@ export class ProxySessionController {
1847
1969
  const pending = this.sessionLoads.get(record.localSessionId);
1848
1970
  if (pending) return pending;
1849
1971
 
1850
- this.outbound.registerSession(record.localSessionId, record.contextKey, record.title, record.cwd);
1972
+ this.outbound.registerSession(
1973
+ record.localSessionId,
1974
+ record.contextKey,
1975
+ record.title,
1976
+ record.cwd,
1977
+ this.agentLabel(record.agentName),
1978
+ );
1851
1979
  const loading = (async (): Promise<LoadedSession> => {
1852
1980
  if (record.lastTurnId) {
1853
1981
  try {
@@ -1874,14 +2002,14 @@ export class ProxySessionController {
1874
2002
  model: record.model,
1875
2003
  reasoningEffort: record.reasoningEffort,
1876
2004
  permissionMode,
1877
- activeTurnId: agent.kind === "codex" && record.status === "running" && record.lastTurnStatus === "running"
2005
+ activeTurnId: agent.kind === "app-server" && record.status === "running" && record.lastTurnStatus === "running"
1878
2006
  ? record.lastTurnId
1879
2007
  : undefined,
1880
2008
  lastTurnId: record.lastTurnId,
1881
2009
  lastTurnStatus: record.lastTurnStatus,
1882
2010
  });
1883
2011
  } catch (error) {
1884
- if (!(agent.kind === "codex" && !record.lastTurnId && isMissingRolloutError(error))) throw error;
2012
+ if (!(agent.kind === "app-server" && !record.lastTurnId && isMissingRolloutError(error))) throw error;
1885
2013
  this.logger.warn({ error, sessionId: record.localSessionId }, "Codex task has no rollout; creating a replacement task.");
1886
2014
  session = await runtime.createSession({
1887
2015
  localSessionId: record.localSessionId,
@@ -2480,7 +2608,7 @@ export class ProxySessionController {
2480
2608
  }
2481
2609
  const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!);
2482
2610
  const agent = this.ensureAgent(context.defaultAgent);
2483
- if (agent.kind !== "codex") throw new Error("Goal 模式仅支持 Codex App Server 任务。");
2611
+ if (agent.kind !== "app-server") throw new Error("Goal 模式仅支持 App Server 任务。");
2484
2612
  record = await this.createSession(
2485
2613
  contextKey,
2486
2614
  context.defaultAgent,
@@ -2919,7 +3047,13 @@ export class ProxySessionController {
2919
3047
  await this.assertSessionTurnOwnership(existing, runtime);
2920
3048
  this.store.attachSessionToContext(contextKey, existing.localSessionId);
2921
3049
  this.store.setCurrentSession(contextKey, existing.localSessionId);
2922
- this.outbound.registerSession(existing.localSessionId, contextKey, existing.title, existing.cwd);
3050
+ this.outbound.registerSession(
3051
+ existing.localSessionId,
3052
+ contextKey,
3053
+ existing.title,
3054
+ existing.cwd,
3055
+ this.agentLabel(existing.agentName),
3056
+ );
2923
3057
  await this.outbound.sendText(contextKey, `已切换到任务:${existing.title ?? existing.remoteSessionId ?? taskId}`);
2924
3058
  return;
2925
3059
  }
@@ -2946,7 +3080,13 @@ export class ProxySessionController {
2946
3080
  lastTurnStatus: mapRemoteTurnStatus(remote.lastTurnStatus),
2947
3081
  });
2948
3082
  this.store.setCurrentSession(contextKey, localSessionId);
2949
- this.outbound.registerSession(localSessionId, contextKey, remote.title ?? remote.preview, remote.cwd);
3083
+ this.outbound.registerSession(
3084
+ localSessionId,
3085
+ contextKey,
3086
+ remote.title ?? remote.preview,
3087
+ remote.cwd,
3088
+ this.agentLabel(agentName),
3089
+ );
2950
3090
  await this.outbound.sendText(
2951
3091
  contextKey,
2952
3092
  `已切换到任务:${remote.title ?? remote.preview ?? remote.id}。历史消息不会重新发送。`,
@@ -3320,9 +3460,13 @@ export class ProxySessionController {
3320
3460
  return agent;
3321
3461
  }
3322
3462
 
3463
+ private agentLabel(agentName: string): string {
3464
+ return this.ensureAgent(agentName).title;
3465
+ }
3466
+
3323
3467
  private isCodexSession(session: SessionRecord): boolean {
3324
3468
  if (session.runtimeKind) return session.runtimeKind === "codex";
3325
- return this.config.agents[session.agentName]?.kind === "codex";
3469
+ return this.config.agents[session.agentName]?.kind === "app-server";
3326
3470
  }
3327
3471
 
3328
3472
  private currentSession(contextKey: string): SessionRecord | undefined {
@@ -12,7 +12,7 @@ export function createAgentRuntimeRegistry(config: AppConfig, logger: Logger): A
12
12
  const runtimes: Record<string, AgentRuntime> = {};
13
13
  for (const [agentName, agent] of Object.entries(config.agents)) {
14
14
  const runtimeLogger = logger.child({ agentName, runtimeKind: agent.kind });
15
- if (agent.kind === "codex") {
15
+ if (agent.kind === "app-server") {
16
16
  const processManager = new CodexProcessManager(agent.command, agent.args, agent.env, runtimeLogger);
17
17
  runtimes[agentName] = new CodexRuntime(processManager, runtimeLogger);
18
18
  continue;
@@ -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
  });