
























@@ -18,9 +18,7 @@ import {
1818} from "./config-reload-plan.js";
1919import { createExecApprovalIosPushDelivery } from "./exec-approval-ios-push.js";
2020import { ExecApprovalManager } from "./exec-approval-manager.js";
21-import { createExecApprovalHandlers } from "./server-methods/exec-approval.js";
22-import { createPluginApprovalHandlers } from "./server-methods/plugin-approval.js";
23-import { createSecretsHandlers } from "./server-methods/secrets.js";
21+import type { GatewayRequestHandler, GatewayRequestHandlers } from "./server-methods/types.js";
2422import {
2523disconnectStaleSharedGatewayAuthClients,
2624setCurrentSharedGatewaySessionGeneration,
@@ -39,6 +37,20 @@ type ReloadSecretsResult = {
3937warningCount: number;
4038};
413940+function createLazyHandler(
41+method: string,
42+loadHandlers: () => Promise<GatewayRequestHandlers>,
43+): GatewayRequestHandler {
44+return async (opts) => {
45+const handlers = await loadHandlers();
46+const handler = handlers[method];
47+if (!handler) {
48+throw new Error(`lazy gateway handler not found: ${method}`);
49+}
50+await handler(opts);
51+};
52+}
53+4254export function createGatewayAuxHandlers(params: {
4355log: GatewayAuxHandlerLogger;
4456activateRuntimeSecrets: ActivateRuntimeSecrets;
@@ -53,15 +65,25 @@ export function createGatewayAuxHandlers(params: {
5365const execApprovalManager = new ExecApprovalManager();
5466const execApprovalForwarder = createExecApprovalForwarder();
5567const execApprovalIosPushDelivery = createExecApprovalIosPushDelivery({ log: params.log });
56-const execApprovalHandlers = createExecApprovalHandlers(execApprovalManager, {
57-forwarder: execApprovalForwarder,
58-iosPushDelivery: execApprovalIosPushDelivery,
59-});
68+let execApprovalHandlersPromise: Promise<GatewayRequestHandlers> | null = null;
69+const loadExecApprovalHandlers = () =>
70+(execApprovalHandlersPromise ??= import("./server-methods/exec-approval.js").then(
71+({ createExecApprovalHandlers }) =>
72+createExecApprovalHandlers(execApprovalManager, {
73+forwarder: execApprovalForwarder,
74+iosPushDelivery: execApprovalIosPushDelivery,
75+}),
76+));
6077const buildReloadPlan = params.buildReloadPlan ?? buildGatewayReloadPlan;
6178const pluginApprovalManager = new ExecApprovalManager<PluginApprovalRequestPayload>();
62-const pluginApprovalHandlers = createPluginApprovalHandlers(pluginApprovalManager, {
63-forwarder: execApprovalForwarder,
64-});
79+let pluginApprovalHandlersPromise: Promise<GatewayRequestHandlers> | null = null;
80+const loadPluginApprovalHandlers = () =>
81+(pluginApprovalHandlersPromise ??= import("./server-methods/plugin-approval.js").then(
82+({ createPluginApprovalHandlers }) =>
83+createPluginApprovalHandlers(pluginApprovalManager, {
84+forwarder: execApprovalForwarder,
85+}),
86+));
6587// Serialize the entire `secrets.reload` path (activation + channel restart)
6688// so concurrent callers cannot overlap the stop/start loop and so the
6789// "before" snapshot used for the reload-plan diff is always the snapshot
@@ -83,132 +105,168 @@ export function createGatewayAuxHandlers(params: {
83105reloadInFlight = run;
84106return run;
85107};
86-const secretsHandlers = createSecretsHandlers({
87-reloadSecrets: () =>
88-runExclusiveReload(async () => {
89-const previousSnapshot = getActiveSecretsRuntimeSnapshot();
90-if (!previousSnapshot) {
91-throw new Error("Secrets runtime snapshot is not active.");
92-}
93-// Snapshot both `current` and `required` because
94-// `setCurrentSharedGatewaySessionGeneration` can clear `required` as
95-// a side effect of activating a new generation. Restoring only
96-// `current` on rollback would leave `required` cleared and weaken
97-// shared-gateway auth-generation enforcement after a failed reload.
98-const previousSharedGatewaySessionGeneration =
99-params.sharedGatewaySessionGenerationState.current;
100-const previousSharedGatewaySessionGenerationRequired =
101-params.sharedGatewaySessionGenerationState.required;
102-let nextSharedGatewaySessionGeneration = previousSharedGatewaySessionGeneration;
103-let sharedGatewaySessionGenerationChanged = false;
104-const stoppedChannels: ChannelKind[] = [];
105-const restartedChannels = new Set<ChannelKind>();
106-try {
107-const prepared = await params.activateRuntimeSecrets(previousSnapshot.sourceConfig, {
108-reason: "reload",
109-activate: true,
110-});
111-nextSharedGatewaySessionGeneration =
112-params.resolveSharedGatewaySessionGenerationForConfig(prepared.config);
113-const plan = buildReloadPlan(diffConfigPaths(previousSnapshot.config, prepared.config));
114-setCurrentSharedGatewaySessionGeneration(
115-params.sharedGatewaySessionGenerationState,
116-nextSharedGatewaySessionGeneration,
117-);
118-sharedGatewaySessionGenerationChanged =
119-previousSharedGatewaySessionGeneration !== nextSharedGatewaySessionGeneration;
120-if (sharedGatewaySessionGenerationChanged) {
121-disconnectStaleSharedGatewayAuthClients({
122-clients: params.clients,
123-expectedGeneration: nextSharedGatewaySessionGeneration,
124-});
125-}
126-if (plan.restartChannels.size > 0) {
127-const restartChannels = [...plan.restartChannels];
128-if (
129-isTruthyEnvValue(process.env.OPENCLAW_SKIP_CHANNELS) ||
130-isTruthyEnvValue(process.env.OPENCLAW_SKIP_PROVIDERS)
131-) {
132-throw new Error(
133-`secrets.reload requires restarting channels: ${restartChannels.join(", ")}`,
134-);
135-}
136-const restartFailures: ChannelKind[] = [];
137-for (const channel of restartChannels) {
138-params.logChannels.info(`restarting ${channel} channel after secrets reload`);
139-// Track for rollback before awaiting stopChannel: if stopChannel
140-// throws after partially stopping the channel (for example, a
141-// plugin hook rejects after the runtime already closed the
142-// socket), we still need the outer catch to attempt restart so
143-// the channel is not left down after a failed reload.
144-stoppedChannels.push(channel);
108+let secretsHandlersPromise: Promise<GatewayRequestHandlers> | null = null;
109+const loadSecretsHandlers = () =>
110+(secretsHandlersPromise ??= import("./server-methods/secrets.js").then(
111+({ createSecretsHandlers }) =>
112+createSecretsHandlers({
113+reloadSecrets: () =>
114+runExclusiveReload(async () => {
115+const previousSnapshot = getActiveSecretsRuntimeSnapshot();
116+if (!previousSnapshot) {
117+throw new Error("Secrets runtime snapshot is not active.");
118+}
119+// Snapshot both `current` and `required` because
120+// `setCurrentSharedGatewaySessionGeneration` can clear `required` as
121+// a side effect of activating a new generation. Restoring only
122+// `current` on rollback would leave `required` cleared and weaken
123+// shared-gateway auth-generation enforcement after a failed reload.
124+const previousSharedGatewaySessionGeneration =
125+params.sharedGatewaySessionGenerationState.current;
126+const previousSharedGatewaySessionGenerationRequired =
127+params.sharedGatewaySessionGenerationState.required;
128+let nextSharedGatewaySessionGeneration = previousSharedGatewaySessionGeneration;
129+let sharedGatewaySessionGenerationChanged = false;
130+const stoppedChannels: ChannelKind[] = [];
131+const restartedChannels = new Set<ChannelKind>();
145132try {
146-await params.stopChannel(channel);
147-await params.startChannel(channel);
148-restartedChannels.add(channel);
149-} catch {
150-params.logChannels.info(
151-`failed to restart ${channel} channel after secrets reload`,
133+const prepared = await params.activateRuntimeSecrets(
134+ previousSnapshot.sourceConfig,
135+ {
136+ reason: "reload",
137+ activate: true,
138+},
152139);
153-restartFailures.push(channel);
154-}
155-}
156-if (restartFailures.length > 0) {
157-throw new Error(
158-`failed to restart channels after secrets reload: ${restartFailures.join(", ")}`,
159-);
160-}
161-}
162-return { warningCount: prepared.warnings.length };
163-} catch (err) {
164-activateSecretsRuntimeSnapshot(previousSnapshot);
165-params.sharedGatewaySessionGenerationState.current =
166-previousSharedGatewaySessionGeneration;
167-params.sharedGatewaySessionGenerationState.required =
168-previousSharedGatewaySessionGenerationRequired;
169-if (sharedGatewaySessionGenerationChanged) {
170-disconnectStaleSharedGatewayAuthClients({
171-clients: params.clients,
172-expectedGeneration: previousSharedGatewaySessionGeneration,
173-});
174-}
175-for (const channel of stoppedChannels) {
176-params.logChannels.info(`rolling back ${channel} channel after secrets reload failure`);
177-try {
178-if (restartedChannels.has(channel)) {
179-await params.stopChannel(channel);
140+nextSharedGatewaySessionGeneration =
141+params.resolveSharedGatewaySessionGenerationForConfig(prepared.config);
142+const plan = buildReloadPlan(
143+diffConfigPaths(previousSnapshot.config, prepared.config),
144+);
145+setCurrentSharedGatewaySessionGeneration(
146+params.sharedGatewaySessionGenerationState,
147+nextSharedGatewaySessionGeneration,
148+);
149+sharedGatewaySessionGenerationChanged =
150+previousSharedGatewaySessionGeneration !== nextSharedGatewaySessionGeneration;
151+if (sharedGatewaySessionGenerationChanged) {
152+disconnectStaleSharedGatewayAuthClients({
153+clients: params.clients,
154+expectedGeneration: nextSharedGatewaySessionGeneration,
155+});
156+}
157+if (plan.restartChannels.size > 0) {
158+const restartChannels = [...plan.restartChannels];
159+if (
160+isTruthyEnvValue(process.env.OPENCLAW_SKIP_CHANNELS) ||
161+isTruthyEnvValue(process.env.OPENCLAW_SKIP_PROVIDERS)
162+) {
163+throw new Error(
164+`secrets.reload requires restarting channels: ${restartChannels.join(", ")}`,
165+);
166+}
167+const restartFailures: ChannelKind[] = [];
168+for (const channel of restartChannels) {
169+params.logChannels.info(`restarting ${channel} channel after secrets reload`);
170+// Track for rollback before awaiting stopChannel: if stopChannel
171+// throws after partially stopping the channel (for example, a
172+// plugin hook rejects after the runtime already closed the
173+// socket), we still need the outer catch to attempt restart so
174+// the channel is not left down after a failed reload.
175+stoppedChannels.push(channel);
176+try {
177+await params.stopChannel(channel);
178+await params.startChannel(channel);
179+restartedChannels.add(channel);
180+} catch {
181+params.logChannels.info(
182+`failed to restart ${channel} channel after secrets reload`,
183+);
184+restartFailures.push(channel);
185+}
186+}
187+if (restartFailures.length > 0) {
188+throw new Error(
189+`failed to restart channels after secrets reload: ${restartFailures.join(", ")}`,
190+);
191+}
192+}
193+return { warningCount: prepared.warnings.length };
194+} catch (err) {
195+activateSecretsRuntimeSnapshot(previousSnapshot);
196+params.sharedGatewaySessionGenerationState.current =
197+previousSharedGatewaySessionGeneration;
198+params.sharedGatewaySessionGenerationState.required =
199+previousSharedGatewaySessionGenerationRequired;
200+if (sharedGatewaySessionGenerationChanged) {
201+disconnectStaleSharedGatewayAuthClients({
202+clients: params.clients,
203+expectedGeneration: previousSharedGatewaySessionGeneration,
204+});
205+}
206+for (const channel of stoppedChannels) {
207+params.logChannels.info(
208+`rolling back ${channel} channel after secrets reload failure`,
209+);
210+try {
211+if (restartedChannels.has(channel)) {
212+await params.stopChannel(channel);
213+}
214+await params.startChannel(channel);
215+} catch {
216+params.logChannels.info(
217+`failed to roll back ${channel} channel after secrets reload`,
218+);
219+}
220+}
221+throw err;
180222}
181-await params.startChannel(channel);
182-} catch {
183-params.logChannels.info(
184-`failed to roll back ${channel} channel after secrets reload`,
185-);
223+}),
224+log: params.log,
225+resolveSecrets: async ({ commandName, targetIds }) => {
226+const { assignments, diagnostics, inactiveRefPaths } =
227+resolveCommandSecretsFromActiveRuntimeSnapshot({
228+ commandName,
229+targetIds: new Set(targetIds),
230+});
231+if (assignments.length === 0) {
232+return {
233+assignments: [] as CommandSecretAssignment[],
234+ diagnostics,
235+ inactiveRefPaths,
236+};
186237}
187-}
188-throw err;
189-}
190-}),
191-log: params.log,
192-resolveSecrets: async ({ commandName, targetIds }) => {
193-const { assignments, diagnostics, inactiveRefPaths } =
194-resolveCommandSecretsFromActiveRuntimeSnapshot({
195- commandName,
196-targetIds: new Set(targetIds),
197-});
198-if (assignments.length === 0) {
199-return { assignments: [] as CommandSecretAssignment[], diagnostics, inactiveRefPaths };
200-}
201-return { assignments, diagnostics, inactiveRefPaths };
202-},
203-});
238+return { assignments, diagnostics, inactiveRefPaths };
239+},
240+}),
241+));
204242205243return {
206244 execApprovalManager,
207245 pluginApprovalManager,
208246extraHandlers: {
209- ...execApprovalHandlers,
210- ...pluginApprovalHandlers,
211- ...secretsHandlers,
247+"exec.approval.get": createLazyHandler("exec.approval.get", loadExecApprovalHandlers),
248+"exec.approval.list": createLazyHandler("exec.approval.list", loadExecApprovalHandlers),
249+"exec.approval.request": createLazyHandler("exec.approval.request", loadExecApprovalHandlers),
250+"exec.approval.waitDecision": createLazyHandler(
251+"exec.approval.waitDecision",
252+loadExecApprovalHandlers,
253+),
254+"exec.approval.resolve": createLazyHandler("exec.approval.resolve", loadExecApprovalHandlers),
255+"plugin.approval.list": createLazyHandler("plugin.approval.list", loadPluginApprovalHandlers),
256+"plugin.approval.request": createLazyHandler(
257+"plugin.approval.request",
258+loadPluginApprovalHandlers,
259+),
260+"plugin.approval.waitDecision": createLazyHandler(
261+"plugin.approval.waitDecision",
262+loadPluginApprovalHandlers,
263+),
264+"plugin.approval.resolve": createLazyHandler(
265+"plugin.approval.resolve",
266+loadPluginApprovalHandlers,
267+),
268+"secrets.reload": createLazyHandler("secrets.reload", loadSecretsHandlers),
269+"secrets.resolve": createLazyHandler("secrets.resolve", loadSecretsHandlers),
212270},
213271};
214272}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。