config: address Nacos PR review (listener body, onChange-on-change-only, backoff, runtime refresh, dotenv)
Made-with: Cursor
This commit is contained in:
parent
4d2d02e59c
commit
6f938c3a99
@ -1497,8 +1497,32 @@ export async function readBestEffortConfig(): Promise<OpenClawConfig> {
|
||||
return snapshot.valid ? loadConfig() : snapshot.config;
|
||||
}
|
||||
|
||||
function buildSnapshotFromRuntimeSnapshots(path: string): ConfigFileSnapshot | null {
|
||||
if (!runtimeConfigSnapshot) return null;
|
||||
const config = runtimeConfigSnapshot;
|
||||
const resolved = runtimeConfigSourceSnapshot ?? config;
|
||||
const raw = JSON.stringify(config);
|
||||
return {
|
||||
path,
|
||||
exists: true,
|
||||
raw,
|
||||
parsed: config as unknown,
|
||||
resolved: resolved as unknown,
|
||||
valid: true,
|
||||
config,
|
||||
hash: hashConfigRaw(raw),
|
||||
issues: [],
|
||||
warnings: [],
|
||||
legacyIssues: [],
|
||||
};
|
||||
}
|
||||
|
||||
export async function readConfigFileSnapshot(): Promise<ConfigFileSnapshot> {
|
||||
const source = getConfigSource();
|
||||
if (source?.kind === "nacos") {
|
||||
const fromRuntime = buildSnapshotFromRuntimeSnapshots("nacos:openclaw.json");
|
||||
if (fromRuntime) return fromRuntime;
|
||||
}
|
||||
if (source !== null) {
|
||||
return source.readSnapshot();
|
||||
}
|
||||
@ -1508,6 +1532,13 @@ export async function readConfigFileSnapshot(): Promise<ConfigFileSnapshot> {
|
||||
export async function readConfigFileSnapshotForWrite(): Promise<ReadConfigFileSnapshotForWriteResult> {
|
||||
const source = getConfigSource();
|
||||
if (source?.kind === "nacos") {
|
||||
const fromRuntime = buildSnapshotFromRuntimeSnapshots("nacos:openclaw.json");
|
||||
if (fromRuntime) {
|
||||
return {
|
||||
snapshot: fromRuntime,
|
||||
writeOptions: { expectedConfigPath: fromRuntime.path },
|
||||
};
|
||||
}
|
||||
const snapshot = await source.readSnapshot();
|
||||
return {
|
||||
snapshot,
|
||||
@ -1530,7 +1561,25 @@ export async function writeConfigFile(
|
||||
const issueMessage = issue?.message ?? "invalid";
|
||||
throw new Error(formatConfigValidationFailure(pathLabel, issueMessage));
|
||||
}
|
||||
setRuntimeConfigSnapshot(cfg);
|
||||
setRuntimeConfigSnapshot(cfg, cfg);
|
||||
const refreshHandler = runtimeConfigSnapshotRefreshHandler;
|
||||
if (refreshHandler) {
|
||||
try {
|
||||
const refreshed = await refreshHandler.refresh({ sourceConfig: cfg });
|
||||
if (refreshed) return;
|
||||
} catch (error) {
|
||||
try {
|
||||
refreshHandler.clearOnRefreshFailure?.();
|
||||
} catch {
|
||||
// Keep the original refresh failure as the surfaced error.
|
||||
}
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
throw new ConfigRuntimeRefreshError(
|
||||
`Config was written (Nacos in-memory), but runtime snapshot refresh failed: ${detail}`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
const io = createConfigIO();
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
/**
|
||||
* Nacos config client: fetch config via Open API and long-poll for changes.
|
||||
* No nacos npm dependency; plain HTTP only. Uses opts.fetch for test injection.
|
||||
* Listener uses Nacos v1 format: Listening-Configs=<dataId>%02<group>%02<contentMD5>%02<tenant>%01
|
||||
*/
|
||||
|
||||
import crypto from "node:crypto";
|
||||
|
||||
export type NacosConfigClientOptions = {
|
||||
serverAddr: string;
|
||||
dataId: string;
|
||||
@ -41,25 +44,32 @@ export function createNacosConfigClient(opts: NacosConfigClientOptions): NacosCo
|
||||
|
||||
const listenerUrl = `${base}/nacos/v1/cs/configs/listener`;
|
||||
|
||||
// MD5 of last-fetched content; used by listener so Nacos can compare and hold connection until change.
|
||||
let lastContentMD5 = "";
|
||||
|
||||
return {
|
||||
async fetchConfig(): Promise<string> {
|
||||
const res = await doFetch(getConfigUrl());
|
||||
if (!res.ok) {
|
||||
throw new Error(`Nacos get config failed: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
return res.text();
|
||||
const text = await res.text();
|
||||
lastContentMD5 = crypto.createHash("md5").update(text).digest("hex");
|
||||
return text;
|
||||
},
|
||||
|
||||
subscribe(onChange: () => void): () => void {
|
||||
let stopped = false;
|
||||
const STX = "\x02";
|
||||
const SOH = "\x01";
|
||||
|
||||
const poll = async (): Promise<void> => {
|
||||
if (stopped) return;
|
||||
const body = new URLSearchParams({
|
||||
dataId: opts.dataId,
|
||||
group: opts.group,
|
||||
});
|
||||
if (opts.tenant) body.set("tenant", opts.tenant);
|
||||
// Nacos v1 listener expects Listening-Configs=<dataId>%02<group>%02<contentMD5>%02<tenant>%01
|
||||
const tenant = opts.tenant ?? "";
|
||||
const listeningConfigs =
|
||||
`${opts.dataId}${STX}${opts.group}${STX}${lastContentMD5}${STX}${tenant}${SOH}`;
|
||||
const body = new URLSearchParams({ "Listening-Configs": listeningConfigs });
|
||||
try {
|
||||
const res = await doFetch(listenerUrl, {
|
||||
method: "POST",
|
||||
@ -71,10 +81,11 @@ export function createNacosConfigClient(opts: NacosConfigClientOptions): NacosCo
|
||||
});
|
||||
if (stopped) return;
|
||||
if (res.ok) {
|
||||
onChange();
|
||||
const text = await res.text();
|
||||
if (text.trim()) onChange();
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors; loop will retry or exit when stopped
|
||||
await new Promise((r) => setTimeout(r, 5000));
|
||||
}
|
||||
if (!stopped) {
|
||||
void poll();
|
||||
|
||||
@ -32,6 +32,7 @@ import {
|
||||
resolveControlUiRootOverrideSync,
|
||||
resolveControlUiRootSync,
|
||||
} from "../infra/control-ui-assets.js";
|
||||
import { loadDotEnv } from "../infra/dotenv.js";
|
||||
import { isDiagnosticsEnabled } from "../infra/diagnostic-events.js";
|
||||
import { logAcceptedEnvOption } from "../infra/env.js";
|
||||
import { createExecApprovalForwarder } from "../infra/exec-approval-forwarder.js";
|
||||
@ -381,6 +382,7 @@ export async function startGatewayServer(
|
||||
description: "raw stream log path override",
|
||||
});
|
||||
|
||||
loadDotEnv({ quiet: true });
|
||||
setConfigSource(resolveConfigSource(process.env));
|
||||
|
||||
let configSnapshot = await readConfigFileSnapshot();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user