feat: v0.8.2 安全纵深补全 · 协议保真 · 断链修复 — 图片SSRF/根MEMORY.md保护根治 · Anthropic thinking回传+pause_turn续传 · 2523 用例全量回归 + E2E 扩充
This commit is contained in:
@@ -23,7 +23,15 @@
|
||||
|
||||
import { app } from 'electron';
|
||||
import { join } from 'path';
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
||||
import {
|
||||
copyFileSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
} from 'fs';
|
||||
import log from 'electron-log';
|
||||
import { CONFIG_DEFAULTS, DEPRECATED_CONFIG_KEYS } from './database.service';
|
||||
import {
|
||||
@@ -34,6 +42,8 @@ import {
|
||||
|
||||
/** 全局配置文件路径(userData 下,与工作空间无关) */
|
||||
const GLOBAL_CONFIG_FILE = join(app.getPath('userData'), 'global-config.json');
|
||||
/** 最近一次成功落盘的备份(主文件损坏时的恢复源) */
|
||||
const GLOBAL_CONFIG_BACKUP = join(app.getPath('userData'), 'global-config.backup.json');
|
||||
|
||||
/** 全局配置 key 前缀清单(匹配这些前缀的 key 视为全局配置) */
|
||||
const GLOBAL_KEY_PREFIXES = [
|
||||
@@ -105,7 +115,28 @@ export class GlobalConfigService {
|
||||
try {
|
||||
if (existsSync(GLOBAL_CONFIG_FILE)) {
|
||||
const raw = readFileSync(GLOBAL_CONFIG_FILE, 'utf-8');
|
||||
this.data = JSON.parse(raw) as GlobalConfigData;
|
||||
try {
|
||||
this.data = JSON.parse(raw) as GlobalConfigData;
|
||||
} catch (parseErr) {
|
||||
// v0.8.2 P0-3: 主文件损坏自愈 —— 依次尝试:① 最近一次成功落盘的备份
|
||||
// 恢复(恢复成功即回写主文件);② 归档损坏文件后以空配置启动。
|
||||
// 此前 parse 失败会静默以空配置运行,用户全部全局配置(含 LLM 凭据)
|
||||
// 在下一次 set() 落盘时被覆盖丢失。
|
||||
log.error('[GlobalConfig] Main config file corrupted:', parseErr);
|
||||
this.data = this.recoverFromBackup();
|
||||
if (Object.keys(this.data).length > 0) {
|
||||
this.flush();
|
||||
log.warn('[GlobalConfig] Restored global config from backup file');
|
||||
} else {
|
||||
const archived = `${GLOBAL_CONFIG_FILE}.corrupt-${Date.now()}`;
|
||||
try {
|
||||
renameSync(GLOBAL_CONFIG_FILE, archived);
|
||||
log.warn(`[GlobalConfig] Corrupted config archived to ${archived}`);
|
||||
} catch {
|
||||
/* 归档失败不阻断启动 */
|
||||
}
|
||||
}
|
||||
}
|
||||
// v0.8.1 review: 清除已废除的配置键(分 Provider contextWindow / ollama.numCtx),
|
||||
// 与工作空间 DB 迁移 12 对齐 —— 全局层残留会使双源语义复活
|
||||
let purged = 0;
|
||||
@@ -140,6 +171,23 @@ export class GlobalConfigService {
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.8.2 P0-3: 从备份恢复(仅 initialize 的损坏自愈路径调用)。
|
||||
* 备份不存在或损坏时返回空对象。
|
||||
*/
|
||||
private recoverFromBackup(): GlobalConfigData {
|
||||
try {
|
||||
if (!existsSync(GLOBAL_CONFIG_BACKUP)) return {};
|
||||
const raw = readFileSync(GLOBAL_CONFIG_BACKUP, 'utf-8');
|
||||
const parsed = JSON.parse(raw) as GlobalConfigData;
|
||||
if (parsed && typeof parsed === 'object') return parsed;
|
||||
return {};
|
||||
} catch (err) {
|
||||
log.error('[GlobalConfig] Backup recovery failed:', err);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取全局配置(P0-1: 敏感 key 自动解密)
|
||||
*/
|
||||
@@ -236,12 +284,40 @@ export class GlobalConfigService {
|
||||
|
||||
/**
|
||||
* 落盘到 JSON 文件
|
||||
*
|
||||
* v0.8.2 P0-3 根治:此前直接 writeFileSync 覆写主文件 —— 写盘中途崩溃
|
||||
* (断电/强杀)会产生半截 JSON,下次启动 parse 失败后以空配置启动,
|
||||
* **全部全局配置(含 LLM 凭据)随之丢失**。现改为:
|
||||
* 1. 原子替换:先写同目录 tmp,再 renameSync 原子改名(与 write_file 工具 /
|
||||
* workspace.rewriteMemory 同口径,Windows 下 rename 覆盖已存在目标);
|
||||
* 2. 写后备份:成功落盘后同步维护 global-config.backup.json(尽力而为,
|
||||
* 失败仅告警)—— 主文件因外部因素损坏时的恢复源。
|
||||
*/
|
||||
private flush(): void {
|
||||
const tmpPath = `${GLOBAL_CONFIG_FILE}.tmp_${Date.now()}_${Math.random()
|
||||
.toString(36)
|
||||
.slice(2, 8)}`;
|
||||
try {
|
||||
writeFileSync(GLOBAL_CONFIG_FILE, JSON.stringify(this.data, null, 2), 'utf-8');
|
||||
writeFileSync(tmpPath, JSON.stringify(this.data, null, 2), 'utf-8');
|
||||
try {
|
||||
renameSync(tmpPath, GLOBAL_CONFIG_FILE);
|
||||
} catch (err) {
|
||||
try {
|
||||
unlinkSync(tmpPath);
|
||||
} catch {
|
||||
/* 忽略清理失败 */
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
} catch (err) {
|
||||
log.error('[GlobalConfig] Failed to write config file:', err);
|
||||
return;
|
||||
}
|
||||
// 备份为尽力而为:失败不影响主流程(备份缺失仅降低自愈成功率)
|
||||
try {
|
||||
copyFileSync(GLOBAL_CONFIG_FILE, GLOBAL_CONFIG_BACKUP);
|
||||
} catch (err) {
|
||||
log.warn(`[GlobalConfig] Backup write failed: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user