原始设计问题:
当自定义模型和云端模型都存储在 settings.json 中时,会出现race condition:
时间线:
T1: 实例A启动 → 读取 settings.json
{ cloudModels: [...], customModels: [] }
T2: 实例B启动 → 使用 /add-model 添加自定义模型
→ 保存 settings.json
{ cloudModels: [...], customModels: [新模型] }
T3: 实例A的云端更新逻辑触发
→ 调用 setValue('cloudModels', [...])
→ 保存整个settings对象(基于T1时读取的旧数据)
→ settings.json 被覆盖
{ cloudModels: [...], customModels: [] } ❌ 新模型丢失!
影响范围:
- ✅ 单实例运行:正常工作
- ❌ 多实例并发:自定义模型会被覆盖
- ❌ 云端更新频繁:配置不稳定
~/.deepv/
├── settings.json # 云端控制(云端模型、系统设置)
└── custom-models.json # 用户控制(自定义模型配置)
关键原则:
- 职责分离:云端数据和用户数据分开存储
- 避免冲突:不同实例操作不同文件
- 原子操作:使用临时文件+重命名保证数据完整性
{
"models": [
{
"id": "custom-openai-gpt4",
"displayName": "GPT-4 Turbo",
"provider": "openai",
"baseUrl": "https://api.openai.com/v1",
"apiKey": "${OPENAI_API_KEY}",
"modelId": "gpt-4-turbo",
"maxTokens": 128000,
"enabled": true
}
],
"_metadata": {
"version": "1.0",
"lastModified": "2025-01-13T10:30:00Z"
}
}{
"cloudModels": [
{
"name": "gemini-2.5-pro",
"displayName": "Gemini 2.5 Pro",
"creditsPerRequest": 6.0,
"available": true,
"maxToken": 1048576
}
]
}// 原子写入操作
export function saveCustomModels(models: CustomModelConfig[]): void {
// 1. 验证配置
validateAllModels(models);
// 2. 准备数据(带metadata)
const data = {
$schema: 'https://deepvlab.ai/schemas/custom-models.json',
models: models,
_metadata: {
version: '1.0',
lastModified: new Date().toISOString(),
}
};
// 3. 原子写入(防止文件损坏)
const tempFile = filePath + '.tmp';
fs.writeFileSync(tempFile, JSON.stringify(data, null, 2));
fs.renameSync(tempFile, filePath); // 原子操作
}关键特性:
- ✅ 原子写入:临时文件+重命名,避免写入中断导致文件损坏
- ✅ 数据验证:保存前完整验证,拒绝无效配置
- ✅ 元数据追踪:记录版本和修改时间
- ✅ 错误恢复:失败时不影响原文件
// 优先级:custom-models.json > config > settings.json
function getCustomModels() {
// 1. 从独立文件读取(推荐)
try {
return loadCustomModels(); // ~/.deepv/custom-models.json
} catch (error) {
console.warn('Failed to load from file');
}
// 2. 降级:从config读取(兼容性)
if (config) {
return config.getCustomModels();
}
// 3. 降级:从settings读取(向后兼容)
if (settings) {
return settings.merged.customModels;
}
return [];
}// 只写入独立文件
function saveCustomModel(model: CustomModelConfig) {
addOrUpdateCustomModel(model); // 直接写入 custom-models.json
// ❌ 不再修改 settings.json
}实例A: /add-model → 写入 custom-models.json
实例B: /add-model → 写入 custom-models.json
**结果:**后写入的覆盖先写入的(与原来一致)
**改进空间:**可以添加文件锁机制(如果未来需要)
实例A: /add-model → 写入 custom-models.json
实例A: 云端更新 → 写入 settings.json (cloudModels字段)
**结果:**✅ 两个文件独立,互不影响
实例A: 正常运行
实例B: /add-model → 写入 custom-models.json
实例A: 云端更新 → 写入 settings.json
**结果:**✅ 完美隔离,无冲突
情况1:旧版本升级
// 首次启动时自动迁移
if (settings.customModels && settings.customModels.length > 0) {
// 从 settings.json 迁移到 custom-models.json
saveCustomModels(settings.customModels);
// 可选:清理 settings.json 中的 customModels 字段
}情况2:新旧版本混用
// 读取时兼容两个来源
const models = [
...loadCustomModels(), // 新位置
...(settings.customModels || []) // 旧位置(兼容)
];| 操作 | 原方案 | 新方案 | 改进 |
|---|---|---|---|
| 读取云端模型 | 读settings.json (1次) | 读settings.json (1次) | 无变化 |
| 读取自定义模型 | 读settings.json (1次) | 读custom-models.json (1次) | 无变化 |
| 保存云端模型 | 写settings.json (整个文件) | 写settings.json (整个文件) | 无变化 |
| 保存自定义模型 | 写settings.json (整个文件) | 写custom-models.json (小文件) | ✅ 更快 |
| 并发安全性 | ❌ 可能冲突 | ✅ 完全隔离 | ✅ 大幅提升 |
settings.json(典型大小):
- 基础配置:~2KB
- 云端模型:~5KB (10-20个模型)
- 总计:~7KB
custom-models.json(典型大小):
- 用户配置:~1KB (2-5个模型)
- 元数据:~100B
优势:
- ✅ 自定义模型文件更小,写入更快
- ✅ 不影响settings.json的读写
- ✅ 更清晰的职责划分
// 错误的写入(可能损坏文件)
fs.writeFileSync(file, data); // ❌ 写入中断 → 文件损坏
// 正确的原子写入
fs.writeFileSync(tempFile, data);
fs.renameSync(tempFile, file); // ✅ 原子操作,不会损坏// 保存前验证
for (const model of models) {
const errors = validateCustomModelConfig(model);
if (errors.length > 0) {
throw new Error(`Invalid config: ${errors.join(', ')}`);
}
}// 基础操作
loadCustomModels(): CustomModelConfig[]
saveCustomModels(models: CustomModelConfig[]): void
// 便捷操作
addOrUpdateCustomModel(model: CustomModelConfig): void
deleteCustomModel(modelId: string): boolean
getCustomModel(modelId: string): CustomModelConfig | undefined
customModelExists(modelId: string): boolean
// 文件路径
getCustomModelsFilePath(): string// 向导完成时保存
handleWizardComplete(config: CustomModelConfig) {
addOrUpdateCustomModel(config); // 直接保存到独立文件
}
// 读取时合并
function getAvailableModels() {
const cloudModels = getCloudModels(); // from settings.json
const customModels = loadCustomModels(); // from custom-models.json
return [...cloudModels, ...customModels];
}describe('customModelsStorage', () => {
test('atomic write prevents corruption', () => {
// 模拟写入中断
// 验证原文件未损坏
});
test('validation rejects invalid configs', () => {
// 无效配置应该抛出错误
});
test('concurrent writes handle correctly', () => {
// 多个保存操作,验证最终一致性
});
});describe('multi-instance scenarios', () => {
test('instance A updates cloud, instance B adds custom', () => {
// 两个操作不应该冲突
});
test('backward compatibility with settings.json', () => {
// 旧版本配置应该能正常读取
});
});如果需要更严格的并发控制:
import * as lockfile from 'proper-lockfile';
export async function saveCustomModelsWithLock(models: CustomModelConfig[]) {
const release = await lockfile.lock(filePath);
try {
saveCustomModels(models);
} finally {
await release();
}
}当前:每次保存整个文件 未来:可以实现增量更新(如果模型很多时)
记录配置历史,支持回滚:
// custom-models.json (带历史记录)
{
"models": [...],
"_metadata": {
"version": "1.0",
"lastModified": "2025-01-14T10:30:00Z"
},
"_history": [
{ "timestamp": "...", "models": [...] }
]
}✅ 消除并发冲突:多实例安全运行 ✅ 云端更新隔离:云端模型更新不影响自定义模型 ✅ 原子操作保护:防止文件损坏 ✅ 清晰职责划分:配置来源一目了然 ✅ 向后兼容:支持从settings.json平滑迁移
- Single Responsibility:每个文件只负责一类配置
- Atomic Operations:关键操作使用原子性保证
- Fail-Safe:错误处理和降级策略
- Backward Compatible:兼容旧版本配置
| 文件 | 控制方 | 更新频率 | 内容 |
|---|---|---|---|
settings.json |
云端+用户 | 高(云端同步) | 系统设置、云端模型 |
custom-models.json |
用户 | 低(手动配置) | 自定义模型配置 |
这种设计确保了即使在复杂的多实例场景下,用户的自定义模型配置也能稳定可靠。