diff --git a/plugins/sdk/dev-host/server.mjs b/plugins/sdk/dev-host/server.mjs index 35f93d4e15..c9fc61e380 100644 --- a/plugins/sdk/dev-host/server.mjs +++ b/plugins/sdk/dev-host/server.mjs @@ -1,7 +1,7 @@ import http from "node:http"; import { randomUUID, randomBytes } from "node:crypto"; -import { readFile, realpath, watch } from "node:fs/promises"; -import { resolve, relative, isAbsolute, sep } from "node:path"; +import { readFile, realpath, watch, mkdir, writeFile, rename } from "node:fs/promises"; +import { resolve, relative, isAbsolute, sep, join } from "node:path"; import semver from "semver"; import { Sidecar, protocolName } from "./sidecar.mjs"; import { ConnectionStore, lifecyclePayload, providerFor, summary, validateRecord } from "./connections.mjs"; @@ -84,7 +84,25 @@ export async function createMockHost(options) { lifecycleQueue = task.catch(() => {}); return task; }; - let autoReload = false; + // Developer preferences such as auto-reload outlive a dev host restart; + // persist them next to the connection store with the same atomic pattern. + const settingsDirectory = resolve(options.dataDir), + settingsFile = join(settingsDirectory, "settings.json"); + const readSettings = async () => { + try { + const parsed = JSON.parse(await readFile(settingsFile, "utf8")); + return parsed && typeof parsed === "object" ? parsed : {}; + } catch { + return {}; + } + }; + const writeSettings = async (settings) => { + await mkdir(settingsDirectory, { recursive: true, mode: 0o700 }); + const temporary = join(settingsDirectory, `.settings-${randomUUID()}.tmp`); + await writeFile(temporary, JSON.stringify({ version: 1, ...settings }, null, 2), { mode: 0o600, flag: "wx" }); + await rename(temporary, settingsFile); + }; + let autoReload = (await readSettings()).autoReload === true; const backendReload = new AutoReload(async () => { try { await serialize(async () => { @@ -94,6 +112,7 @@ export async function createMockHost(options) { broadcast("auto-reload-error", {}); } }); + backendReload.enable(autoReload); const broadcast = (type, payload) => { const message = `data: ${JSON.stringify({ type, ...payload })}\n\n`; for (const stream of streams) { @@ -250,6 +269,11 @@ export async function createMockHost(options) { if (typeof p.enabled !== "boolean") throw new Error("Invalid automatic reload setting"); autoReload = p.enabled; backendReload.enable(autoReload); + try { + await writeSettings({ autoReload }); + } catch (error) { + diagnostics.record("error", "build", "自动重载设置保存失败", { reason: String(error.message || error) }); + } broadcast("auto-reload", { enabled: autoReload }); diagnostics.record("info", "build", autoReload ? "自动重载已启用" : "自动重载已关闭"); return { enabled: autoReload }; diff --git a/plugins/sdk/dev-host/tests/server.test.mjs b/plugins/sdk/dev-host/tests/server.test.mjs index 2e5b80859b..994c60aee7 100644 --- a/plugins/sdk/dev-host/tests/server.test.mjs +++ b/plugins/sdk/dev-host/tests/server.test.mjs @@ -262,6 +262,30 @@ test("source watching is opt-in and automatic restarts preserve saved connection await new Promise((resolve) => setTimeout(resolve, 650)); assert.equal(builds, 1); }); + +test("auto-reload preference persists across dev host restarts", async (t) => { + const first = await fixture(t); + assert.equal((await first.request("auto-reload", { enabled: true })).value.enabled, true); + await first.host.close(); + const second = await createMockHost({ + project: first.root, + uiRoot: "ui", + backend: process.execPath, + diagnostics: new Diagnostics(() => {}), + backendArgs: [fileURLToPath(new URL("./echo-sidecar.mjs", import.meta.url))], + dataDir: join(first.root, "data"), + shellHtml: join(first.root, "ui/index.html"), + port: 0, + backendWatch: first.root, + buildBackend: async () => {}, + }); + t.after(async () => { + await second.close(); + await rm(first.root, { recursive: true, force: true }); + }); + const bootstrap = await (await fetch(`${second.origin}/api/bootstrap`)).json(); + assert.equal(bootstrap.autoReload, true); +}); test("agent diagnostics is read-only, bounded and does not log its own polling", async (t) => { const { host } = await fixture(t, true); const url = `${host.origin}/api/diagnostics`,