fix(sync): add in-flight lock and callback support for common config sync

- Add SyncResult interface for structured sync results
- Add SyncResultCallback type for async notification
- Add syncInFlight lock per appType to prevent concurrent syncs
- Add executeSyncWithLock function for concurrency control
- Add meta fallback detection with automatic backfill
- Improve error handling with detailed error messages
This commit is contained in:
YoVinchen
2026-01-25 03:38:45 +08:00
parent 53dd0a90f3
commit b692bb4053
+478
View File
@@ -1,8 +1,71 @@
// 配置相关 API
import { invoke } from "@tauri-apps/api/core";
import type { Provider } from "@/types";
import { providersApi } from "./providers";
import {
hasCommonConfigSnippet,
hasTomlCommonConfigSnippet,
hasGeminiCommonConfigSnippet,
} from "@/utils/providerConfigUtils";
export type AppType = "claude" | "codex" | "gemini";
// ============================================================================
// 同步防抖管理
// ============================================================================
/** 同步结果类型 */
export interface SyncResult {
/** 成功更新的供应商数量 */
count: number;
/** 错误信息(如果有) */
error?: string;
/** 是否已排队等待执行(debounce 中) */
queued?: boolean;
}
/** 同步结果回调类型 */
export type SyncResultCallback = (result: SyncResult) => void;
// 每个 appType 的 debounce 定时器
const syncDebounceTimers: Record<
AppType,
ReturnType<typeof setTimeout> | null
> = {
claude: null,
codex: null,
gemini: null,
};
// 每个 appType 的同步锁(防止并发)
const syncInFlight: Record<AppType, boolean> = {
claude: false,
codex: false,
gemini: false,
};
// 每个 appType 的最新同步参数(用于 single-flight
const pendingSyncParams: Record<
AppType,
{
oldSnippet: string;
newSnippet: string;
updateFn: (
settingsConfig: string,
oldSnippet: string,
newSnippet: string,
) => { updatedConfig: string; error?: string };
currentProviderId?: string;
onComplete?: SyncResultCallback;
} | null
> = {
claude: null,
codex: null,
gemini: null,
};
const SYNC_DEBOUNCE_MS = 500;
/**
* 获取 Claude 通用配置片段(已废弃,使用 getCommonConfigSnippet
* @returns 通用配置片段(JSON 字符串),如果不存在则返回 null
@@ -75,3 +138,418 @@ export async function extractCommonConfigSnippet(
return invoke<string>("extract_common_config_snippet", args);
}
// ============================================================================
// 通用配置同步功能
// ============================================================================
/**
* 同步通用配置到所有启用了该配置的供应商(带 debounce + 同步锁)
*
* 当通用配置片段被修改时,需要更新所有启用了通用配置的供应商的 settingsConfig
* 使用 debounce + single-flight + in-flight lock 模式,避免快速编辑时的并发问题
*
* @param appType - 应用类型
* @param oldSnippet - 旧的通用配置片段
* @param newSnippet - 新的通用配置片段(空字符串表示清空/移除)
* @param updateFn - 更新函数,用于替换配置中的通用配置片段
* @param currentProviderId - 当前正在编辑的供应商 ID(跳过,因为已在编辑器中更新)
* @param onComplete - 同步完成后的回调,用于通知 UI 层
*/
export function syncCommonConfigToProviders(
appType: AppType,
oldSnippet: string,
newSnippet: string,
updateFn: (
settingsConfig: string,
oldSnippet: string,
newSnippet: string,
) => { updatedConfig: string; error?: string },
currentProviderId?: string,
onComplete?: SyncResultCallback,
): void {
// 保存最新的同步参数(覆盖之前的,实现 single-flight
pendingSyncParams[appType] = {
oldSnippet,
newSnippet,
updateFn,
currentProviderId,
onComplete,
};
// 清除之前的定时器
if (syncDebounceTimers[appType]) {
clearTimeout(syncDebounceTimers[appType]!);
}
// 设置新的定时器,在 debounce 后执行实际同步
syncDebounceTimers[appType] = setTimeout(() => {
executeSyncWithLock(appType);
}, SYNC_DEBOUNCE_MS);
}
/**
* 带锁执行同步(防止并发)
*/
async function executeSyncWithLock(appType: AppType): Promise<void> {
// 如果正在同步中,等待下一次调度
if (syncInFlight[appType]) {
// 已有同步在执行,参数已保存,等同步完成后会检查是否需要再次执行
return;
}
// 获取最新的参数
const params = pendingSyncParams[appType];
if (!params) {
return;
}
// 清除参数并设置锁
pendingSyncParams[appType] = null;
syncDebounceTimers[appType] = null;
syncInFlight[appType] = true;
try {
// 执行实际同步
const result = await doSyncCommonConfigToProviders(
appType,
params.oldSnippet,
params.newSnippet,
params.updateFn,
params.currentProviderId,
);
// 输出结果
if (result.error) {
console.warn(`[syncCommonConfig] ${appType} 同步失败: ${result.error}`);
} else if (result.count > 0) {
console.log(
`[syncCommonConfig] 共更新 ${result.count}${appType} 供应商`,
);
}
// 通知回调
if (params.onComplete) {
params.onComplete(result);
}
} finally {
// 释放锁
syncInFlight[appType] = false;
// 检查是否有新的待执行参数(在同步期间又有新的请求)
if (pendingSyncParams[appType]) {
// 递归执行,处理新的请求
executeSyncWithLock(appType);
}
}
}
/**
* 实际执行同步的内部函数
* @returns 同步结果,包含更新数量和可能的错误信息
*/
async function doSyncCommonConfigToProviders(
appType: AppType,
oldSnippet: string,
newSnippet: string,
updateFn: (
settingsConfig: string,
oldSnippet: string,
newSnippet: string,
) => { updatedConfig: string; error?: string },
currentProviderId?: string,
): Promise<SyncResult> {
try {
const providers = await providersApi.getAll(appType);
let updatedCount = 0;
const errors: string[] = [];
for (const [id, provider] of Object.entries(providers)) {
// 跳过当前正在编辑的供应商
if (id === currentProviderId) {
continue;
}
// 获取当前配置字符串(提前获取,用于内容检测)
const settingsConfigStr = getSettingsConfigString(provider, appType);
if (!settingsConfigStr) {
continue;
}
// 检查是否启用了通用配置
const metaByApp = provider.meta?.commonConfigEnabledByApp;
const resolvedMetaEnabled =
metaByApp?.[appType] ?? provider.meta?.commonConfigEnabled;
let isEnabled: boolean;
let needsMetaBackfill = false;
if (resolvedMetaEnabled !== undefined) {
// meta 有明确值,直接使用
isEnabled = resolvedMetaEnabled;
} else {
// meta 缺失,回退到内容检测(旧/新片段均可触发)
isEnabled = detectCommonConfigEnabledByContent(
appType,
settingsConfigStr,
oldSnippet,
newSnippet,
);
// 如果检测到启用,标记需要补写 meta
if (isEnabled) {
needsMetaBackfill = true;
}
}
if (!isEnabled) {
continue;
}
// 使用更新函数替换配置
const { updatedConfig, error } = updateFn(
settingsConfigStr,
oldSnippet,
newSnippet,
);
if (error) {
errors.push(`供应商 ${id}: ${error}`);
continue;
}
// 更新供应商配置
const updateResult = updateProviderSettingsConfig(
provider,
updatedConfig,
appType,
);
if (updateResult.error) {
errors.push(`供应商 ${id}: ${updateResult.error}`);
continue;
}
// 如果需要补写 meta,添加 commonConfigEnabledByApp
let providerToSave = updateResult.provider;
if (needsMetaBackfill) {
providerToSave = {
...providerToSave,
meta: {
...providerToSave.meta,
commonConfigEnabledByApp: {
...providerToSave.meta?.commonConfigEnabledByApp,
[appType]: true,
},
},
};
console.log(
`[syncCommonConfig] 供应商 ${id} 检测到通用配置,已补写 meta`,
);
}
try {
await providersApi.update(providerToSave, appType);
updatedCount++;
console.log(`[syncCommonConfig] 已更新供应商 ${id}`);
} catch (updateError) {
errors.push(`供应商 ${id}: 保存失败 - ${String(updateError)}`);
}
}
if (errors.length > 0) {
return {
count: updatedCount,
error: `部分供应商更新失败: ${errors.join("; ")}`,
};
}
return { count: updatedCount };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
count: 0,
error: `同步失败: ${errorMessage}`,
};
}
}
function detectCommonConfigEnabledByContent(
appType: AppType,
settingsConfigStr: string,
oldSnippet: string,
newSnippet: string,
): boolean {
const candidates = [oldSnippet, newSnippet].filter(
(snippet) => typeof snippet === "string" && snippet.trim(),
);
if (candidates.length === 0) return false;
switch (appType) {
case "codex":
return candidates.some((snippet) =>
hasTomlCommonConfigSnippet(settingsConfigStr, snippet),
);
case "gemini":
return candidates.some((snippet) =>
hasGeminiCommonConfigSnippet(settingsConfigStr, snippet),
);
case "claude":
return candidates.some((snippet) =>
hasCommonConfigSnippet(settingsConfigStr, snippet),
);
default:
return false;
}
}
/** 更新供应商配置的结果 */
interface UpdateProviderResult {
provider: Provider;
error?: string;
}
/**
* 从供应商获取配置字符串
*/
function getSettingsConfigString(
provider: Provider,
appType: AppType,
): string | null {
const config = provider.settingsConfig;
if (!config) return null;
switch (appType) {
case "claude":
// Claude: settingsConfig 直接是 JSON 对象
return typeof config === "string" ? config : JSON.stringify(config);
case "codex":
// Codex: settingsConfig.config 是 TOML 字符串
// 先校验 settingsConfig 是对象类型
if (typeof config === "string") {
// settingsConfig 是字符串(可能是之前保存失败的情况)
// 尝试解析为 JSON
try {
const parsed = JSON.parse(config);
if (
typeof parsed === "object" &&
parsed !== null &&
typeof parsed.config === "string"
) {
return parsed.config;
}
} catch {
console.warn(
`[getSettingsConfigString] Codex provider settingsConfig 是无效字符串,跳过`,
);
}
return null;
}
if (typeof config === "object" && config !== null) {
const codexConfig = (config as Record<string, unknown>).config;
return typeof codexConfig === "string" ? codexConfig : null;
}
return null;
case "gemini":
// Gemini: settingsConfig 是包含 env 字段的 JSON 对象
// replaceGeminiCommonConfigSnippet 期望接收完整的 settingsConfig JSON
return typeof config === "string" ? config : JSON.stringify(config);
default:
return null;
}
}
/**
* 更新供应商的 settingsConfig
* @returns 更新结果,包含更新后的 provider 和可能的错误信息
*/
function updateProviderSettingsConfig(
provider: Provider,
updatedConfig: string,
appType: AppType,
): UpdateProviderResult {
switch (appType) {
case "claude":
// Claude: 直接替换 settingsConfig
try {
const parsed = JSON.parse(updatedConfig);
if (typeof parsed !== "object" || parsed === null) {
return {
provider,
error: "解析后的配置不是有效的 JSON 对象",
};
}
return {
provider: {
...provider,
settingsConfig: parsed,
},
};
} catch (e) {
return {
provider,
error: `JSON 解析失败: ${e instanceof Error ? e.message : String(e)}`,
};
}
case "codex": {
// Codex: 更新 settingsConfig.config
// 先确保 settingsConfig 是对象类型
let baseConfig: Record<string, unknown>;
const currentConfig = provider.settingsConfig;
if (typeof currentConfig === "string") {
// 尝试解析字符串
try {
const parsed = JSON.parse(currentConfig);
baseConfig =
typeof parsed === "object" && parsed !== null ? parsed : {};
} catch {
baseConfig = {};
}
} else if (typeof currentConfig === "object" && currentConfig !== null) {
baseConfig = currentConfig as Record<string, unknown>;
} else {
baseConfig = {};
}
return {
provider: {
...provider,
settingsConfig: {
...baseConfig,
config: updatedConfig,
},
},
};
}
case "gemini":
// Gemini: 直接替换 settingsConfig
try {
const parsed = JSON.parse(updatedConfig);
if (typeof parsed !== "object" || parsed === null) {
return {
provider,
error: "解析后的配置不是有效的 JSON 对象",
};
}
return {
provider: {
...provider,
settingsConfig: parsed,
},
};
} catch (e) {
return {
provider,
error: `JSON 解析失败: ${e instanceof Error ? e.message : String(e)}`,
};
}
default:
return { provider };
}
}