refactor(common-config): consolidate hooks and migrate Gemini to ENV format

- Delete redundant wrapper hooks (useCommonConfigSnippet, useGeminiCommonConfig)
- Change Gemini common config from JSON to ENV format (.env style)
- Add backend validation with forbidden keys filtering (GEMINI_API_KEY, GOOGLE_GEMINI_BASE_URL)
- Fix localStorage migration to skip empty parsed snippets
- Add error handling for silent JSON parse failures
- Clean up debug logs and unused types
This commit is contained in:
YoVinchen
2026-01-30 10:16:57 +08:00
parent b8a53f9e36
commit aa1231903f
17 changed files with 198 additions and 323 deletions
-3
View File
@@ -53,15 +53,12 @@ export function DeepLinkImportDialog() {
const unlistenImport = listen<DeepLinkImportRequest>(
"deeplink-import",
async (event) => {
console.log("Deep link import event received:", event.payload);
// If config is present, merge it to get the complete configuration
if (event.payload.config || event.payload.configUrl) {
try {
const mergedRequest = await deeplinkApi.mergeDeeplinkConfig(
event.payload,
);
console.log("Config merged successfully:", mergedRequest);
setRequest(mergedRequest);
} catch (error) {
console.error("Failed to merge config:", error);
@@ -18,6 +18,7 @@ interface GeminiCommonConfigModalProps {
/**
* GeminiCommonConfigModal - Common Gemini configuration editor modal
* Allows editing of common env snippet shared across Gemini providers
* Uses ENV format (KEY=VALUE) instead of JSON
*/
export const GeminiCommonConfigModal: React.FC<
GeminiCommonConfigModalProps
@@ -88,13 +89,14 @@ export const GeminiCommonConfigModal: React.FC<
<JsonEditor
value={value}
onChange={onChange}
placeholder={`{
"GEMINI_MODEL": "gemini-3-pro-preview"
}`}
placeholder={`# Gemini 通用配置
# 格式: KEY=VALUE
GEMINI_MODEL=gemini-2.5-pro`}
darkMode={isDarkMode}
rows={16}
showValidation={true}
language="json"
showValidation={false}
language="javascript"
/>
{error && (
+58 -38
View File
@@ -66,13 +66,16 @@ import {
useCodexConfigState,
useApiKeyLink,
useTemplateValues,
useCommonConfigSnippet,
useCodexCommonConfig,
useSpeedTestEndpoints,
useCodexTomlValidation,
useGeminiConfigState,
useGeminiCommonConfig,
} from "./hooks";
import { useCommonConfigBase } from "@/hooks/useCommonConfigBase";
import {
claudeAdapter,
createGeminiAdapter,
} from "@/hooks/commonConfigAdapters";
import { useProvidersQuery } from "@/lib/query/queries";
/**
@@ -469,6 +472,17 @@ export function ProviderForm({
});
// 使用通用配置片段 hook (仅 Claude 模式)
// 直接使用 useCommonConfigBase + claudeAdapter
const claudeCommonConfig = useCommonConfigBase({
adapter: claudeAdapter,
inputValue: form.getValues("settingsConfig"),
onInputChange: (config) => form.setValue("settingsConfig", config),
initialData: appId === "claude" ? initialData : undefined,
selectedPresetId: selectedPresetId ?? undefined,
enabled: appId === "claude",
});
// 解构 Claude 通用配置
const {
useCommonConfig,
commonConfigSnippet,
@@ -478,17 +492,10 @@ export function ProviderForm({
isExtracting: isClaudeExtracting,
handleExtract: handleClaudeExtract,
isLoading: isClaudeCommonConfigLoading,
finalConfig: claudeFinalConfig,
finalValue: claudeFinalConfig,
getPendingCommonConfigSnippet: getPendingClaudeCommonConfigSnippet,
markCommonConfigSaved: markClaudeCommonConfigSaved,
} = useCommonConfigSnippet({
settingsConfig: form.getValues("settingsConfig"),
onConfigChange: (config) => form.setValue("settingsConfig", config),
initialData: appId === "claude" ? initialData : undefined,
selectedPresetId: selectedPresetId ?? undefined,
enabled: appId === "claude",
currentProviderId: providerId,
});
} = claudeCommonConfig;
// 使用 Codex 通用配置片段 hook (仅 Codex 模式)
const {
@@ -537,14 +544,16 @@ export function ProviderForm({
(key: string) => {
originalHandleGeminiApiKeyChange(key);
// 同步更新 settingsConfig
let config: { env?: Record<string, unknown> };
try {
const config = JSON.parse(form.getValues("settingsConfig") || "{}");
if (!config.env) config.env = {};
config.env.GEMINI_API_KEY = key.trim();
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
config = JSON.parse(form.getValues("settingsConfig") || "{}");
} catch {
// ignore
// 解析失败时初始化为有效结构
config = { env: {} };
}
if (!config.env) config.env = {};
config.env.GEMINI_API_KEY = key.trim();
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
},
[originalHandleGeminiApiKeyChange, form],
);
@@ -553,14 +562,16 @@ export function ProviderForm({
(url: string) => {
originalHandleGeminiBaseUrlChange(url);
// 同步更新 settingsConfig
let config: { env?: Record<string, unknown> };
try {
const config = JSON.parse(form.getValues("settingsConfig") || "{}");
if (!config.env) config.env = {};
config.env.GOOGLE_GEMINI_BASE_URL = url.trim().replace(/\/+$/, "");
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
config = JSON.parse(form.getValues("settingsConfig") || "{}");
} catch {
// ignore
// 解析失败时初始化为有效结构
config = { env: {} };
}
if (!config.env) config.env = {};
config.env.GOOGLE_GEMINI_BASE_URL = url.trim().replace(/\/+$/, "");
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
},
[originalHandleGeminiBaseUrlChange, form],
);
@@ -569,19 +580,36 @@ export function ProviderForm({
(model: string) => {
originalHandleGeminiModelChange(model);
// 同步更新 settingsConfig
let config: { env?: Record<string, unknown> };
try {
const config = JSON.parse(form.getValues("settingsConfig") || "{}");
if (!config.env) config.env = {};
config.env.GEMINI_MODEL = model.trim();
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
config = JSON.parse(form.getValues("settingsConfig") || "{}");
} catch {
// ignore
// 解析失败时初始化为有效结构
config = { env: {} };
}
if (!config.env) config.env = {};
config.env.GEMINI_MODEL = model.trim();
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
},
[originalHandleGeminiModelChange, form],
);
// 使用 Gemini 通用配置 hook (仅 Gemini 模式)
// 直接使用 useCommonConfigBase + createGeminiAdapter
const geminiAdapter = useMemo(
() => createGeminiAdapter({ envStringToObj, envObjToString }),
[envStringToObj, envObjToString],
);
const geminiCommonConfig = useCommonConfigBase({
adapter: geminiAdapter,
inputValue: geminiEnv,
onInputChange: handleGeminiEnvChange,
initialData: appId === "gemini" ? initialData : undefined,
selectedPresetId: selectedPresetId ?? undefined,
enabled: appId === "gemini",
});
// 解构 Gemini 通用配置
const {
useCommonConfig: useGeminiCommonConfigFlag,
commonConfigSnippet: geminiCommonConfigSnippet,
@@ -591,18 +619,10 @@ export function ProviderForm({
isExtracting: isGeminiExtracting,
handleExtract: handleGeminiExtract,
isLoading: isGeminiCommonConfigLoading,
finalEnv: geminiFinalEnv,
finalValue: geminiFinalEnv,
getPendingCommonConfigSnippet: getPendingGeminiCommonConfigSnippet,
markCommonConfigSaved: markGeminiCommonConfigSaved,
} = useGeminiCommonConfig({
envValue: geminiEnv,
onEnvChange: handleGeminiEnvChange,
envStringToObj,
envObjToString,
initialData: appId === "gemini" ? initialData : undefined,
selectedPresetId: selectedPresetId ?? undefined,
currentProviderId: providerId,
});
} = geminiCommonConfig;
const supportsCommonConfig =
appId === "claude" || appId === "codex" || appId === "gemini";
@@ -1041,8 +1061,8 @@ export function ProviderForm({
const configObj = geminiConfig.trim() ? JSON.parse(geminiConfig) : {};
// 如果启用了通用配置,只保存与通用配置不同的部分(自定义配置)
if (useGeminiCommonConfigFlag && geminiCommonConfigSnippet.trim()) {
// Parse common config as JSON (flat {"KEY": "VALUE"} format)
// Note: geminiCommonConfigSnippet is stored as JSON by useGeminiCommonConfig hook
// Parse common config snippet (supports ENV/Flat JSON/Wrapped JSON formats)
// Uses parseGeminiCommonConfigSnippet for unified parsing
const { env: commonEnvObj, warning } = parseGeminiCommonConfig(
geminiCommonConfigSnippet.trim(),
);
@@ -11,6 +11,4 @@ export { useCodexTomlValidation } from "./useCodexTomlValidation";
export { useGeminiConfigState } from "./useGeminiConfigState";
// Common Config Hooks
export { useCommonConfigSnippet } from "./useCommonConfigSnippet";
export { useCodexCommonConfig } from "./useCodexCommonConfig";
export { useGeminiCommonConfig } from "./useGeminiCommonConfig";
@@ -10,12 +10,6 @@ import { codexAdapter } from "@/hooks/commonConfigAdapters";
import { extractTomlDifference } from "@/utils/tomlConfigMerge";
import type { ProviderMeta } from "@/types";
/** TOML 校验错误码 */
export type TomlValidationErrorCode =
| "TOML_SYNTAX_ERROR"
| "TOML_PARSE_FAILED"
| "";
interface UseCodexCommonConfigProps {
/**
* 当前 Codex 配置(可能是纯 TOML 或 JSON wrapper 格式)
@@ -1,104 +0,0 @@
import { useMemo } from "react";
import {
useCommonConfigBase,
type UseCommonConfigBaseReturn,
} from "@/hooks/useCommonConfigBase";
import { claudeAdapter } from "@/hooks/commonConfigAdapters";
import type { ProviderMeta } from "@/types";
interface UseCommonConfigSnippetProps {
/**
* 当前配置(用于显示和运行时合并)
* 新架构:传入自定义配置,返回最终配置
*/
settingsConfig: string;
/**
* 配置变化回调
*/
onConfigChange: (config: string) => void;
/**
* 初始数据(编辑模式)
*/
initialData?: {
settingsConfig?: Record<string, unknown>;
meta?: ProviderMeta;
};
/**
* 当前选中的预设 ID
*/
selectedPresetId?: string;
/**
* 当 false 时跳过所有逻辑,返回禁用状态。默认:true
*/
enabled?: boolean;
/**
* 当前正在编辑的供应商 ID
*/
currentProviderId?: string;
}
export interface UseCommonConfigSnippetReturn {
/** 是否启用通用配置 */
useCommonConfig: boolean;
/** 通用配置片段 (JSON 格式) */
commonConfigSnippet: string;
/** 通用配置错误信息 */
commonConfigError: string;
/** 是否正在加载 */
isLoading: boolean;
/** 是否正在提取 */
isExtracting: boolean;
/** 通用配置开关处理函数 */
handleCommonConfigToggle: (checked: boolean) => void;
/** 通用配置片段变化处理函数 */
handleCommonConfigSnippetChange: (snippet: string) => void;
/** 从当前配置提取通用配置 */
handleExtract: () => Promise<void>;
/** 最终配置(运行时合并结果,只读) */
finalConfig: string;
/** 是否有待保存的通用配置变更 */
hasUnsavedCommonConfig: boolean;
/** 获取待保存的通用配置片段(用于 handleSubmit */
getPendingCommonConfigSnippet: () => string | null;
/** 标记通用配置已保存 */
markCommonConfigSaved: () => void;
}
/**
* 管理 Claude 通用配置片段
*
* 基于 useCommonConfigBase 泛型 Hook + Claude JSON 适配器实现。
*/
export function useCommonConfigSnippet({
settingsConfig,
onConfigChange,
initialData,
selectedPresetId,
enabled = true,
}: UseCommonConfigSnippetProps): UseCommonConfigSnippetReturn {
const adapter = useMemo(() => claudeAdapter, []);
const base: UseCommonConfigBaseReturn<string> = useCommonConfigBase({
adapter,
inputValue: settingsConfig,
onInputChange: onConfigChange,
initialData,
selectedPresetId,
enabled,
});
return {
useCommonConfig: base.useCommonConfig,
commonConfigSnippet: base.commonConfigSnippet,
commonConfigError: base.commonConfigError,
isLoading: base.isLoading,
isExtracting: base.isExtracting,
handleCommonConfigToggle: base.handleCommonConfigToggle,
handleCommonConfigSnippetChange: base.handleCommonConfigSnippetChange,
handleExtract: base.handleExtract,
finalConfig: base.finalValue,
hasUnsavedCommonConfig: base.hasUnsavedCommonConfig,
getPendingCommonConfigSnippet: base.getPendingCommonConfigSnippet,
markCommonConfigSaved: base.markCommonConfigSaved,
};
}
@@ -1,118 +0,0 @@
import { useMemo } from "react";
import {
useCommonConfigBase,
type UseCommonConfigBaseReturn,
} from "@/hooks/useCommonConfigBase";
import { createGeminiAdapter } from "@/hooks/commonConfigAdapters";
import type { ProviderMeta } from "@/types";
interface UseGeminiCommonConfigProps {
/**
* 当前 env 值(字符串格式,如 "KEY=VALUE\nKEY2=VALUE2"
*/
envValue: string;
/**
* env 变化回调
*/
onEnvChange: (env: string) => void;
/**
* 字符串转对象
*/
envStringToObj: (envString: string) => Record<string, string>;
/**
* 对象转字符串
*/
envObjToString: (envObj: Record<string, unknown>) => string;
/**
* 初始数据(编辑模式)
*/
initialData?: {
settingsConfig?: Record<string, unknown>;
meta?: ProviderMeta;
};
/**
* 当前选中的预设 ID
*/
selectedPresetId?: string;
/**
* 当前正在编辑的供应商 ID
*/
currentProviderId?: string;
}
export interface UseGeminiCommonConfigReturn {
/** 是否启用通用配置 */
useCommonConfig: boolean;
/** 通用配置片段 (JSON 格式) */
commonConfigSnippet: string;
/** 通用配置错误信息 */
commonConfigError: string;
/** 是否正在加载 */
isLoading: boolean;
/** 是否正在提取 */
isExtracting: boolean;
/** 通用配置开关处理函数 */
handleCommonConfigToggle: (checked: boolean) => void;
/** 通用配置片段变化处理函数 */
handleCommonConfigSnippetChange: (snippet: string) => void;
/** 从当前配置提取通用配置 */
handleExtract: () => Promise<void>;
/** 最终 env 对象(运行时合并结果,只读) */
finalEnv: Record<string, string>;
/** 是否有待保存的通用配置变更 */
hasUnsavedCommonConfig: boolean;
/** 获取待保存的通用配置片段(用于 handleSubmit */
getPendingCommonConfigSnippet: () => string | null;
/** 标记通用配置已保存 */
markCommonConfigSaved: () => void;
}
/**
* 管理 Gemini 通用配置片段
*
* 基于 useCommonConfigBase 泛型 Hook + Gemini ENV/JSON 适配器实现。
*
* 架构:
* - envValue:传入当前 env 字符串
* - commonConfigSnippet:存储在数据库中的通用配置片段
* - finalEnv:运行时计算 = merge(commonConfig, customEnv)
* - 开启/关闭通用配置只改变 enabled 状态,不修改 envValue
*/
export function useGeminiCommonConfig({
envValue,
onEnvChange,
envStringToObj,
envObjToString,
initialData,
selectedPresetId,
}: UseGeminiCommonConfigProps): UseGeminiCommonConfigReturn {
// 创建适配器(需要转换函数)
const adapter = useMemo(
() => createGeminiAdapter({ envStringToObj, envObjToString }),
[envStringToObj, envObjToString],
);
const base: UseCommonConfigBaseReturn<Record<string, string>> =
useCommonConfigBase({
adapter,
inputValue: envValue,
onInputChange: onEnvChange,
initialData,
selectedPresetId,
});
return {
useCommonConfig: base.useCommonConfig,
commonConfigSnippet: base.commonConfigSnippet,
commonConfigError: base.commonConfigError,
isLoading: base.isLoading,
isExtracting: base.isExtracting,
handleCommonConfigToggle: base.handleCommonConfigToggle,
handleCommonConfigSnippetChange: base.handleCommonConfigSnippetChange,
handleExtract: base.handleExtract,
finalEnv: base.finalValue,
hasUnsavedCommonConfig: base.hasUnsavedCommonConfig,
getPendingCommonConfigSnippet: base.getPendingCommonConfigSnippet,
markCommonConfigSaved: base.markCommonConfigSaved,
};
}
+4 -2
View File
@@ -249,7 +249,7 @@ export const codexAdapter: CommonConfigAdapter<string, string> = {
// ============================================================================
const GEMINI_LEGACY_STORAGE_KEY = "cc-switch:gemini-common-config-snippet";
const GEMINI_DEFAULT_SNIPPET = "{}";
const GEMINI_DEFAULT_SNIPPET = "";
export interface GeminiAdapterOptions {
/** 字符串转对象 */
@@ -306,7 +306,9 @@ export function createGeminiAdapter(
) {
return t("geminiConfig.commonConfigInvalidValues");
}
return t("geminiConfig.invalidJsonFormat");
return t("geminiConfig.invalidEnvFormat", {
defaultValue: "配置格式错误",
});
}
if (Object.keys(result.env).length === 0) {
+6 -1
View File
@@ -234,7 +234,9 @@ export function useCommonConfigBase<TConfig, TFinal>({
);
if (legacySnippet && legacySnippet.trim()) {
const parsed = adapter.parseSnippet(legacySnippet);
if (!parsed.error) {
// 只有在解析成功且有有效内容时才迁移
// 这避免了将 "{}" 这样的空 JSON 迁移为"有效配置"
if (!parsed.error && adapter.hasValidContent(legacySnippet)) {
await configApi.setCommonConfigSnippet(
adapter.appKey,
legacySnippet,
@@ -246,6 +248,9 @@ export function useCommonConfigBase<TConfig, TFinal>({
console.log(
`[迁移] ${adapter.appKey} 通用配置已从 localStorage 迁移到数据库`,
);
} else {
// 解析失败或无有效内容,清理 localStorage 不迁移
window.localStorage.removeItem(adapter.legacyStorageKey);
}
}
} catch (e) {
+1
View File
@@ -584,6 +584,7 @@
"saveFailed": "Save failed: {{error}}",
"extractedConfigInvalid": "Extracted config format is invalid",
"invalidJsonFormat": "Common config snippet format error (must be valid JSON)",
"invalidEnvFormat": "Config format error (use KEY=VALUE format, one per line)",
"commonConfigInvalidKeys": "Common config snippet must not include GOOGLE_GEMINI_BASE_URL or GEMINI_API_KEY (found: {{keys}})",
"commonConfigInvalidValues": "Common config snippet values must be strings",
"noCommonConfigToApply": "Common config snippet is empty or has no applicable entries",
+1
View File
@@ -584,6 +584,7 @@
"saveFailed": "保存に失敗しました: {{error}}",
"extractedConfigInvalid": "抽出した設定のフォーマットが不正です",
"invalidJsonFormat": "共通設定スニペットの形式が不正です(有効な JSON でなければなりません)",
"invalidEnvFormat": "設定フォーマットエラー(KEY=VALUE 形式を使用してください、各行に1つ)",
"commonConfigInvalidKeys": "共通設定スニペットに GOOGLE_GEMINI_BASE_URL または GEMINI_API_KEY を含めることはできません(検出: {{keys}}",
"commonConfigInvalidValues": "共通設定スニペットの値は文字列である必要があります",
"noCommonConfigToApply": "共通設定スニペットが空、または適用できる項目がありません",
+1
View File
@@ -584,6 +584,7 @@
"saveFailed": "保存失败: {{error}}",
"extractedConfigInvalid": "提取的配置格式错误",
"invalidJsonFormat": "通用配置片段格式错误(必须是有效的 JSON)",
"invalidEnvFormat": "配置格式错误(请使用 KEY=VALUE 格式,每行一个)",
"commonConfigInvalidKeys": "通用配置片段不能包含 GOOGLE_GEMINI_BASE_URL 或 GEMINI_API_KEY(发现:{{keys}}",
"commonConfigInvalidValues": "通用配置片段的值必须是字符串",
"noCommonConfigToApply": "通用配置片段为空或没有可写入的内容",
+5 -10
View File
@@ -81,7 +81,10 @@ export async function getCommonConfigSnippet(
* 设置通用配置片段(统一接口)
* @param appType - 应用类型(claude/codex/gemini
* @param snippet - 通用配置片段(原始字符串)
* @throws 如果格式无效Claude/Gemini 验证 JSONCodex 暂不验证)
* @throws 如果格式无效
* - Claude: 验证 JSON 格式
* - Gemini: 验证 ENV/JSON 格式,拒绝包含禁用键(GEMINI_API_KEY/GOOGLE_GEMINI_BASE_URL
* - Codex: TOML 格式,暂不验证
*/
export async function setCommonConfigSnippet(
appType: AppType,
@@ -98,7 +101,7 @@ export async function setCommonConfigSnippet(
*
* @param appType - 应用类型(claude/codex/gemini
* @param options - 可选:提取来源
* @returns 提取的通用配置片段(JSON/TOML 字符串
* @returns 提取的通用配置片段(Claude: JSON, Codex: TOML, Gemini: ENV
*/
export type ExtractCommonConfigSnippetOptions = {
settingsConfig?: string;
@@ -201,10 +204,6 @@ async function executeSyncWithLock(appType: AppType): Promise<void> {
// 输出结果
if (result.error) {
console.warn(`[syncCommonConfig] ${appType} 同步失败: ${result.error}`);
} else if (result.count > 0) {
console.log(
`[syncCommonConfig] 共更新 ${result.count}${appType} 供应商`,
);
}
// 通知回调
@@ -321,15 +320,11 @@ async function doSyncCommonConfigToProviders(
},
},
};
console.log(
`[syncCommonConfig] 供应商 ${id} 检测到通用配置,已补写 meta`,
);
}
try {
await providersApi.update(providerToSave, appType);
updatedCount++;
console.log(`[syncCommonConfig] 已更新供应商 ${id}`);
} catch (updateError) {
errors.push(`供应商 ${id}: 保存失败 - ${String(updateError)}`);
}
+24 -20
View File
@@ -63,13 +63,26 @@ export const hasCommonConfigSnippet = (
}
};
// 检查 Gemini 配置是否已包含通用配置片段(env JSON)
/**
* 检查 Gemini 配置是否已包含通用配置片段
*
* 支持三种 snippet 格式:
* - ENV 格式: KEY=VALUE (一行一个)
* - Flat JSON: {"KEY": "VALUE", ...}
* - Wrapped JSON: {"env": {"KEY": "VALUE", ...}}
*
* @param jsonString - Provider 的 settingsConfig (JSON 字符串)
* @param snippetString - 通用配置片段 (ENV/JSON 格式)
* @returns 是否已包含通用配置
*/
export const hasGeminiCommonConfigSnippet = (
jsonString: string,
snippetString: string,
): boolean => {
try {
if (!snippetString.trim()) return false;
// 解析 provider 的 settingsConfig
const config = jsonString ? JSON.parse(jsonString) : {};
if (!isPlainObject(config)) return false;
const envValue = (config as Record<string, unknown>).env;
@@ -79,27 +92,18 @@ export const hasGeminiCommonConfigSnippet = (
unknown
>;
const parsed = JSON.parse(snippetString);
if (!isPlainObject(parsed)) return false;
// 使用 parseGeminiCommonConfigSnippet 解析 snippet (支持 ENV/JSON)
const parseResult = parseGeminiCommonConfigSnippet(snippetString, {
strictForbiddenKeys: false, // 过滤禁用键而非报错
});
const entries = Object.entries(parsed).filter(
(entry): entry is [string, string] => {
const [key, value] = entry;
if (
GEMINI_COMMON_ENV_FORBIDDEN_KEYS.includes(
key as GeminiForbiddenEnvKey,
)
) {
return false;
}
if (typeof value !== "string") return false;
return value.trim().length > 0;
},
);
// 解析失败或为空
if (parseResult.error || Object.keys(parseResult.env).length === 0) {
return false;
}
if (entries.length === 0) return false;
return entries.every(([key, value]) => {
// 检查所有有效键值对是否都存在于 provider.env 中
return Object.entries(parseResult.env).every(([key, value]) => {
const current = env[key];
return typeof current === "string" && current === value.trim();
});