config: apply Nacos source review fixes

Made-with: Cursor
This commit is contained in:
GatewayJ 2026-03-19 11:09:30 +08:00
parent 7bae3314be
commit bda40c7e57
8 changed files with 101 additions and 3 deletions

View File

@ -7,6 +7,8 @@ import { formatConfigIssueLines, normalizeConfigIssues } from "../config/issue-f
import { CONFIG_PATH } from "../config/paths.js";
import { isBlockedObjectKey } from "../config/prototype-keys.js";
import { redactConfigObject } from "../config/redact-snapshot.js";
import { setConfigSource } from "../config/sources/current.js";
import { resolveConfigSource } from "../config/sources/resolve.js";
import {
coerceSecretRef,
isValidEnvSecretRefId,
@ -18,6 +20,7 @@ import {
import { validateConfigObjectRaw } from "../config/validation.js";
import { SecretProviderSchema } from "../config/zod-schema.core.js";
import { danger, info, success } from "../globals.js";
import { loadDotEnv } from "../infra/dotenv.js";
import type { RuntimeEnv } from "../runtime.js";
import { defaultRuntime } from "../runtime.js";
import {
@ -99,6 +102,13 @@ class ConfigSetDryRunValidationError extends Error {
}
}
function initializeConfigSourceForStandaloneConfigCli(): void {
// Standalone config commands run in a fresh process.
// Ensure config helpers read from the same source (file vs Nacos) as the gateway.
loadDotEnv({ quiet: true });
setConfigSource(resolveConfigSource(process.env));
}
function isIndexSegment(raw: string): boolean {
return /^[0-9]+$/.test(raw);
}
@ -969,6 +979,7 @@ export async function runConfigSet(opts: {
cliOptions: ConfigSetOptions;
runtime?: RuntimeEnv;
}) {
initializeConfigSourceForStandaloneConfigCli();
const runtime = opts.runtime ?? defaultRuntime;
try {
const isBatchMode = hasBatchMode(opts.cliOptions);
@ -1130,6 +1141,7 @@ export async function runConfigSet(opts: {
}
export async function runConfigGet(opts: { path: string; json?: boolean; runtime?: RuntimeEnv }) {
initializeConfigSourceForStandaloneConfigCli();
const runtime = opts.runtime ?? defaultRuntime;
try {
const parsedPath = parseRequiredPath(opts.path);
@ -1161,6 +1173,7 @@ export async function runConfigGet(opts: { path: string; json?: boolean; runtime
}
export async function runConfigUnset(opts: { path: string; runtime?: RuntimeEnv }) {
initializeConfigSourceForStandaloneConfigCli();
const runtime = opts.runtime ?? defaultRuntime;
try {
const parsedPath = parseRequiredPath(opts.path);
@ -1184,6 +1197,7 @@ export async function runConfigUnset(opts: { path: string; runtime?: RuntimeEnv
}
export async function runConfigFile(opts: { runtime?: RuntimeEnv }) {
initializeConfigSourceForStandaloneConfigCli();
const runtime = opts.runtime ?? defaultRuntime;
try {
const snapshot = await readConfigFileSnapshot();
@ -1195,6 +1209,7 @@ export async function runConfigFile(opts: { runtime?: RuntimeEnv }) {
}
export async function runConfigValidate(opts: { json?: boolean; runtime?: RuntimeEnv } = {}) {
initializeConfigSourceForStandaloneConfigCli();
const runtime = opts.runtime ?? defaultRuntime;
let outputPath = CONFIG_PATH ?? "openclaw.json";

View File

@ -10,12 +10,15 @@ import {
resolveStateDir,
resolveGatewayPort,
} from "../../config/config.js";
import { setConfigSource } from "../../config/sources/current.js";
import { resolveConfigSource } from "../../config/sources/resolve.js";
import { hasConfiguredSecretInput } from "../../config/types.secrets.js";
import { resolveGatewayAuth } from "../../gateway/auth.js";
import { startGatewayServer } from "../../gateway/server.js";
import type { GatewayWsLogStyle } from "../../gateway/ws-logging.js";
import { setGatewayWsLogStyle } from "../../gateway/ws-logging.js";
import { setVerbose } from "../../globals.js";
import { loadDotEnv } from "../../infra/dotenv.js";
import { GatewayLockError } from "../../infra/gateway-lock.js";
import { formatPortDiagnostics, inspectPortUsage } from "../../infra/ports.js";
import { cleanStaleGatewayProcessesSync } from "../../infra/restart-stale-pids.js";
@ -198,6 +201,11 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
await ensureDevGatewayConfig({ reset: Boolean(opts.reset) });
}
// Initialize config source (file vs Nacos) before preflight so port/bind and
// config-exists / gateway.mode checks use Nacos when OPENCLAW_CONFIG_SOURCE=nacos.
loadDotEnv({ quiet: true });
setConfigSource(resolveConfigSource(process.env));
const cfg = loadConfig();
const portOverride = parsePort(opts.port);
if (opts.port !== undefined && portOverride === null) {

View File

@ -12,12 +12,15 @@ import { formatCliCommand } from "../cli/command-format.js";
import type { OpenClawConfig } from "../config/config.js";
import { CONFIG_PATH, readConfigFileSnapshot, writeConfigFile } from "../config/config.js";
import { logConfigUpdated } from "../config/logging.js";
import { setConfigSource } from "../config/sources/current.js";
import { resolveConfigSource } from "../config/sources/resolve.js";
import { resolveSecretInputRef } from "../config/types.secrets.js";
import { resolveGatewayService } from "../daemon/service.js";
import { hasAmbiguousGatewayAuthModeConfig } from "../gateway/auth-mode-policy.js";
import { resolveGatewayAuth } from "../gateway/auth.js";
import { buildGatewayConnectionDetails } from "../gateway/call.js";
import { runStartupMatrixMigration } from "../gateway/server-startup-matrix-migration.js";
import { loadDotEnv } from "../infra/dotenv.js";
import { resolveOpenClawPackageRoot } from "../infra/openclaw-root.js";
import type { RuntimeEnv } from "../runtime.js";
import { defaultRuntime } from "../runtime.js";
@ -80,6 +83,11 @@ export async function doctorCommand(
printWizardHeader(runtime);
intro("OpenClaw doctor");
// Standalone CLI process: initialize config source (file vs Nacos)
// before any config reads/writes.
loadDotEnv({ quiet: true });
setConfigSource(resolveConfigSource(process.env));
const root = await resolveOpenClawPackageRoot({
moduleUrl: import.meta.url,
argv1: process.argv[1],

View File

@ -66,4 +66,39 @@ describe("createNacosConfigClient", () => {
expect(onChange).toHaveBeenCalled();
teardown();
});
it("subscribe backs off when listener returns HTTP error", async () => {
vi.useFakeTimers();
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
const fetchMock = vi.fn().mockResolvedValue({
ok: false,
status: 401,
statusText: "Unauthorized",
text: () => Promise.resolve(""),
});
const client = createNacosConfigClient({
serverAddr: "http://127.0.0.1:8848",
dataId: "openclaw.json",
group: "DEFAULT_GROUP",
fetch: fetchMock,
});
const onChange = vi.fn();
const teardown = client.subscribe(onChange);
// Flush the initial microtasks so poll() reaches the HTTP error branch.
await Promise.resolve();
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 5000);
teardown();
// Let the scheduled backoff settle and avoid leaving pending timers/promises.
await vi.advanceTimersByTimeAsync(5000);
expect(onChange).not.toHaveBeenCalled();
setTimeoutSpy.mockRestore();
vi.useRealTimers();
});
});

View File

@ -79,7 +79,11 @@ export function createNacosConfigClient(opts: NacosConfigClientOptions): NacosCo
body: body.toString(),
});
if (stopped) return;
if (res.ok) {
if (!res.ok) {
// Nacos can respond quickly with HTTP errors (401/403/5xx) when ACL/service is unhealthy.
// Add a backoff to avoid a tight POST loop hammering the server.
await new Promise((r) => setTimeout(r, 5000));
} else {
const text = await res.text();
if (text.trim()) {
try {

View File

@ -13,4 +13,21 @@ describe("buildSnapshotFromRaw", () => {
expect(snap.valid).toBe(true);
expect(snap.config?.gateway?.mode).toBe("local");
});
it("applies config.env to process.env for Nacos snapshots", async () => {
const envKey = "OPENAI_API_KEY";
const previous = process.env[envKey];
delete process.env[envKey];
try {
const raw = `{"env":{"${envKey}":"sk-test"}}`;
await buildSnapshotFromRaw(raw, "nacos:openclaw.json", { env: process.env });
expect(process.env[envKey]).toBe("sk-test");
} finally {
if (previous !== undefined) {
process.env[envKey] = previous;
} else {
delete process.env[envKey];
}
}
});
});

View File

@ -80,7 +80,13 @@ export async function buildSnapshotFromRaw(
typeof parsedRes.parsed === "object" &&
"env" in (parsedRes.parsed as object)
) {
applyConfigEnvVars(parsedRes.parsed as OpenClawConfig, envCopy);
const cfgWithEnv = parsedRes.parsed as OpenClawConfig;
// Nacos-backed snapshots should also hydrate the real process env so features
// that consult process.env later in the process see config.env values.
if (opts.env === process.env) {
applyConfigEnvVars(cfgWithEnv, opts.env);
}
applyConfigEnvVars(cfgWithEnv, envCopy);
}
const resolvedConfigRaw = resolveConfigEnvVars(parsedRes.parsed, envCopy, {
onMissing: (w) => envWarnings.push(w),

View File

@ -17,6 +17,7 @@ import {
loadConfig,
migrateLegacyConfig,
readConfigFileSnapshot,
getRuntimeConfigSourceSnapshot,
setRuntimeConfigSnapshot,
writeConfigFile,
} from "../config/config.js";
@ -557,7 +558,11 @@ export async function startGatewayServer(
// Nacos source: keep loadConfig() in sync with the config we use (no file path).
const configSource = getConfigSource();
if (configSource?.kind === "nacos") {
setRuntimeConfigSnapshot(cfgAtStart);
// Preserve the original Nacos "source" snapshot captured during secret activation.
// If we overwrite it with cfgAtStart, config.env and ${ENV} substitutions can lose
// their original surface values in subsequent config.get/set/patch operations.
const sourceSnapshot = getRuntimeConfigSourceSnapshot();
setRuntimeConfigSnapshot(cfgAtStart, sourceSnapshot ?? cfgAtStart);
}
initSubagentRegistry();