fix(admin): address Copilot review findings

- Reflect built-in defaults for boolean settings (ALLOW_PRIVATE_URLS
  defaults on) and allow clearing a saved boolean back to default,
  so the SSRF toggle matches actual runtime behavior.
- Harden JSON loading: filter settings values to strings only, and
  schema-validate stored ADMIN_PROVIDERS entries, dropping malformed
  ones instead of letting them reach runtime code.
- Set beforeunload returnValue so the unsaved-changes prompt shows in
  all browsers; reject non-finite numbers in settings validation.
- Fix README/CN/JA docs that claimed the panel auto-generates
  AI_MODELS_CONFIG (providers are merged at read time, not written).
- Add unit tests for corrupted-file value filtering and provider
  schema validation.
This commit is contained in:
dayuan.jiang
2026-06-12 11:34:11 +09:00
parent 3764b9b620
commit f96a0c5a18
15 changed files with 126 additions and 16 deletions

View File

@@ -91,7 +91,13 @@ export function loadAdminProviders(): StoredAdminProvider[] {
if (!raw) return []
try {
const parsed = JSON.parse(raw)
return Array.isArray(parsed) ? parsed : []
if (!Array.isArray(parsed)) return []
// Validate each entry's shape — a malformed/hand-edited value must
// not reach runtime code that assumes provider/models exist.
return parsed.flatMap((entry) => {
const result = AdminProviderSchema.safeParse(entry)
return result.success ? [result.data as StoredAdminProvider] : []
})
} catch {
console.error("[admin-providers] Failed to parse stored providers")
return []

View File

@@ -22,6 +22,9 @@ export interface SettingDef {
min?: number
max?: number
placeholder?: string
// Built-in default applied at runtime when the value is unset, so the UI
// can reflect actual behavior (e.g. ALLOW_PRIVATE_URLS defaults to "true").
default?: string
// Value is only picked up at process start (module-load readers)
restartRequired?: boolean
}
@@ -134,6 +137,8 @@ export const SETTINGS_REGISTRY: SettingDef[] = [
label: "Allow Private URLs",
description:
"Turn off to block requests to private IPs and internal hostnames (SSRF protection).",
// Unset means allowed at runtime (ssrf-protection: !== "false")
default: "true",
},
// ── Observability ────────────────────────────────────────────────

View File

@@ -29,8 +29,18 @@ export function loadSettings(): Record<string, string> {
try {
const raw = fs.readFileSync(getSettingsPath(), "utf8")
const parsed = JSON.parse(raw) as SettingsFile
cachedSettings =
parsed && typeof parsed.values === "object" ? parsed.values : {}
// Keep only string values — a hand-edited or corrupted file could
// hold null/arrays/numbers that would otherwise be overlaid onto
// process.env and coerce to junk like "[object Object]".
const values: Record<string, string> = {}
const raw_values =
parsed && typeof parsed.values === "object" && parsed.values
? parsed.values
: {}
for (const [key, value] of Object.entries(raw_values)) {
if (typeof value === "string") values[key] = value
}
cachedSettings = values
} catch (err: any) {
if (err?.code !== "ENOENT") {
console.error("[admin-settings] Failed to read settings file:", err)

View File

@@ -438,6 +438,7 @@
"hideValue": "Hide value",
"removeValue": "Remove value",
"removeValueTitle": "Remove the stored value",
"resetToDefault": "Reset to default",
"models": "Models",
"modelsDescription": "Server-side providers and models available to all users — no personal API key needed. The default provider's first model is used when users don't pick one.",
"addProviderHint": "Add a provider to offer server-side models to all users.",

View File

@@ -438,6 +438,7 @@
"hideValue": "値を非表示",
"removeValue": "値を削除",
"removeValueTitle": "保存された値を削除",
"resetToDefault": "デフォルトに戻す",
"models": "モデル",
"modelsDescription": "全ユーザーが利用できるサーバー側のプロバイダーとモデル——個人の API キーは不要です。ユーザーがモデルを選択しない場合、デフォルトプロバイダーの最初のモデルが使用されます。",
"addProviderHint": "プロバイダーを追加して、全ユーザーにサーバー側モデルを提供します。",

View File

@@ -438,6 +438,7 @@
"hideValue": "隱藏值",
"removeValue": "移除值",
"removeValueTitle": "移除已儲存的值",
"resetToDefault": "重設為預設",
"models": "模型",
"modelsDescription": "面向所有使用者的伺服器端 provider 與模型——無需個人 API 金鑰。當使用者未選擇模型時,使用預設 provider 的第一個模型。",
"addProviderHint": "新增一個 provider為所有使用者提供伺服器端模型。",

View File

@@ -438,6 +438,7 @@
"hideValue": "隐藏值",
"removeValue": "移除值",
"removeValueTitle": "移除已保存的值",
"resetToDefault": "恢复默认",
"models": "模型",
"modelsDescription": "面向所有用户的服务端 provider 和模型——无需个人 API 密钥。当用户未选择模型时,使用默认 provider 的第一个模型。",
"addProviderHint": "添加一个 provider为所有用户提供服务端模型。",