feat: v0.8.2 安全纵深补全 · 协议保真 · 断链修复 — 图片SSRF/根MEMORY.md保护根治 · Anthropic thinking回传+pause_turn续传 · 2523 用例全量回归 + E2E 扩充
CI / 类型检查 + Lint + 单元测试 (push) Failing after 9m45s
CI / 全量测试 (Electron ABI) (push) Failing after 6m28s
CI / 产物编译验证 (push) Successful in 11m18s

This commit is contained in:
2026-09-08 14:30:27 +08:00
parent 69776e447f
commit 4cd6e997b5
86 changed files with 4303 additions and 956 deletions
+79 -3
View File
@@ -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}`);
}
}
}