AI_PROJECT / src /ui-server /config-switch-store.mjs
chenchenaoyang's picture
Deploy from Codex
f395b70 verified
Raw
History Blame Contribute Delete
11.2 kB
import crypto from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
function nowIso() {
return new Date().toISOString();
}
function validationError(message, statusCode = 400) {
const error = new Error(message);
error.statusCode = statusCode;
return error;
}
function ensureTrailingNewline(text) {
const normalized = String(text ?? "").replace(/\r\n/g, "\n");
return normalized.endsWith("\n") ? normalized : `${normalized}\n`;
}
function normalizeTomlText(text) {
return String(text ?? "").replace(/\r\n/g, "\n").trimEnd();
}
function normalizeJsonText(text) {
return `${JSON.stringify(JSON.parse(String(text ?? "")), null, 2)}\n`;
}
async function fileExists(filePath) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
function defaultProviderDefinitions(homeDir = os.homedir()) {
return {
codex: {
provider: "codex",
label: "Codex 配置",
targetPaths: [
{
key: "authJsonText",
label: "auth.json",
path: path.join(homeDir, ".codex", "auth.json"),
kind: "json",
},
{
key: "configTomlText",
label: "config.toml",
path: path.join(homeDir, ".codex", "config.toml"),
kind: "toml",
},
],
},
"claude-code": {
provider: "claude-code",
label: "Claude Code 配置",
targetPaths: [
{
key: "settingsJsonText",
label: "settings.json",
path: path.join(homeDir, ".claude", "settings.json"),
kind: "json",
},
],
},
};
}
function makeEmptyState() {
return {
version: 1,
presetsByProvider: {
codex: [],
"claude-code": [],
},
lastAppliedPresetIdByProvider: {
codex: null,
"claude-code": null,
},
lastAppliedAtByProvider: {
codex: null,
"claude-code": null,
},
};
}
async function readOptionalText(filePath) {
try {
return await fs.readFile(filePath, "utf8");
} catch (error) {
if (error?.code === "ENOENT") return "";
throw error;
}
}
function normalizePresetInput(provider, input = {}) {
return {
id: input.id ? String(input.id) : "",
name: String(input.name ?? "").trim(),
provider,
payload:
provider === "codex"
? {
authJsonText: String(input.payload?.authJsonText ?? ""),
configTomlText: String(input.payload?.configTomlText ?? ""),
}
: {
settingsJsonText: String(input.payload?.settingsJsonText ?? ""),
},
};
}
function validatePreset(provider, preset) {
if (!preset.name) {
throw validationError("name 不能为空");
}
if (provider === "codex") {
if (!preset.payload.configTomlText.trim()) {
throw validationError("configTomlText 不能为空");
}
try {
normalizeJsonText(preset.payload.authJsonText);
} catch {
throw validationError("authJsonText 不是合法 JSON");
}
return;
}
try {
normalizeJsonText(preset.payload.settingsJsonText);
} catch {
throw validationError("settingsJsonText 不是合法 JSON");
}
}
function compareContent(kind, leftText, rightText) {
if (kind === "json") {
try {
return normalizeJsonText(leftText) === normalizeJsonText(rightText);
} catch {
return false;
}
}
return normalizeTomlText(leftText) === normalizeTomlText(rightText);
}
function prepareWrittenText(kind, text) {
if (kind === "json") return normalizeJsonText(text);
return ensureTrailingNewline(text);
}
export class ConfigSwitchStore {
constructor(dataDir, options = {}) {
this.dataDir = dataDir;
this.filePath = path.join(dataDir, "config-switch.json");
this.providerDefinitions = options.providerDefinitions || defaultProviderDefinitions();
this.state = makeEmptyState();
this.loaded = false;
}
requireProvider(provider) {
const normalized = String(provider || "");
if (!this.providerDefinitions[normalized]) {
throw validationError(`Unknown config-switch provider: ${provider}`, 404);
}
return normalized;
}
async load() {
if (this.loaded) return;
await fs.mkdir(this.dataDir, { recursive: true });
try {
const raw = JSON.parse(await fs.readFile(this.filePath, "utf8"));
this.state = {
...makeEmptyState(),
...raw,
presetsByProvider: {
...makeEmptyState().presetsByProvider,
...(raw.presetsByProvider || {}),
},
lastAppliedPresetIdByProvider: {
...makeEmptyState().lastAppliedPresetIdByProvider,
...(raw.lastAppliedPresetIdByProvider || {}),
},
lastAppliedAtByProvider: {
...makeEmptyState().lastAppliedAtByProvider,
...(raw.lastAppliedAtByProvider || {}),
},
};
} catch {
this.state = makeEmptyState();
await this.persist();
}
this.loaded = true;
}
async persist() {
await fs.mkdir(this.dataDir, { recursive: true });
await fs.writeFile(
this.filePath,
`${JSON.stringify(this.state, null, 2)}\n`,
"utf8",
);
}
findPreset(provider, presetId) {
const items = this.state.presetsByProvider[provider] || [];
const preset = items.find((item) => item.id === presetId) || null;
if (!preset) {
throw validationError(`Preset not found: ${presetId}`, 404);
}
return preset;
}
async serializeProvider(provider) {
const definition = this.providerDefinitions[provider];
const items = this.state.presetsByProvider[provider] || [];
const lastAppliedPresetId = this.state.lastAppliedPresetIdByProvider[provider] || null;
const currentFiles = {};
for (const target of definition.targetPaths) {
currentFiles[target.key] = await readOptionalText(target.path);
}
return {
provider,
label: definition.label,
targetPaths: definition.targetPaths.map((target) => ({
key: target.key,
label: target.label,
path: target.path,
})),
lastAppliedPresetId,
lastAppliedAt: this.state.lastAppliedAtByProvider[provider] || null,
presets: items
.map((item) => {
const matchesCurrent = definition.targetPaths.every((target) =>
compareContent(
target.kind,
item.payload?.[target.key] || "",
currentFiles[target.key] || "",
),
);
const status =
item.id === lastAppliedPresetId
? matchesCurrent
? "active"
: "drifted"
: "idle";
return {
...item,
status,
statusText:
status === "active"
? "已启用"
: status === "drifted"
? "已偏离"
: "未启用",
currentPayload:
status === "drifted"
? Object.fromEntries(
definition.targetPaths.map((target) => [
target.key,
currentFiles[target.key] || "",
]),
)
: null,
};
})
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)),
};
}
async getConfigSwitchData() {
await this.load();
return {
providers: {
codex: await this.serializeProvider("codex"),
"claude-code": await this.serializeProvider("claude-code"),
},
};
}
async upsertPreset(provider, input) {
await this.load();
const normalizedProvider = this.requireProvider(provider);
const preset = normalizePresetInput(normalizedProvider, input);
validatePreset(normalizedProvider, preset);
const now = nowIso();
const items = [...(this.state.presetsByProvider[normalizedProvider] || [])];
if (preset.id) {
const index = items.findIndex((item) => item.id === preset.id);
if (index < 0) {
throw validationError(`Preset not found: ${preset.id}`, 404);
}
items[index] = {
...items[index],
name: preset.name,
payload: preset.payload,
updatedAt: now,
};
} else {
items.unshift({
id: crypto.randomUUID(),
name: preset.name,
provider: normalizedProvider,
payload: preset.payload,
createdAt: now,
updatedAt: now,
});
}
this.state.presetsByProvider[normalizedProvider] = items;
await this.persist();
return this.getConfigSwitchData();
}
async deletePreset(provider, presetId) {
await this.load();
const normalizedProvider = this.requireProvider(provider);
const items = [...(this.state.presetsByProvider[normalizedProvider] || [])];
const index = items.findIndex((item) => item.id === presetId);
if (index < 0) {
throw validationError(`Preset not found: ${presetId}`, 404);
}
items.splice(index, 1);
this.state.presetsByProvider[normalizedProvider] = items;
if (this.state.lastAppliedPresetIdByProvider[normalizedProvider] === presetId) {
this.state.lastAppliedPresetIdByProvider[normalizedProvider] = null;
this.state.lastAppliedAtByProvider[normalizedProvider] = null;
}
await this.persist();
return this.getConfigSwitchData();
}
async copyPreset(provider, presetId) {
await this.load();
const normalizedProvider = this.requireProvider(provider);
const source = this.findPreset(normalizedProvider, presetId);
const now = nowIso();
const copied = {
...source,
id: crypto.randomUUID(),
name: `${source.name} - 副本`,
createdAt: now,
updatedAt: now,
};
this.state.presetsByProvider[normalizedProvider] = [
copied,
...(this.state.presetsByProvider[normalizedProvider] || []),
];
await this.persist();
return this.getConfigSwitchData();
}
async activatePreset(provider, presetId, options = {}) {
await this.load();
const normalizedProvider = this.requireProvider(provider);
if (options.confirmed !== true) {
throw validationError("Confirmation required before activating this preset.", 409);
}
const definition = this.providerDefinitions[normalizedProvider];
const preset = this.findPreset(normalizedProvider, presetId);
validatePreset(normalizedProvider, preset);
for (const target of definition.targetPaths) {
await fs.mkdir(path.dirname(target.path), { recursive: true });
await fs.writeFile(
target.path,
prepareWrittenText(target.kind, preset.payload[target.key] || ""),
"utf8",
);
}
this.state.lastAppliedPresetIdByProvider[normalizedProvider] = presetId;
this.state.lastAppliedAtByProvider[normalizedProvider] = nowIso();
await this.persist();
return {
...(await this.getConfigSwitchData()),
activation: {
provider: normalizedProvider,
presetId,
targetPaths: definition.targetPaths.map((target) => target.path),
},
};
}
}