aicodeswitch 3.9.4 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -36
- package/UPGRADE.md +5 -3
- package/bin/restore.js +126 -22
- package/bin/start.js +29 -41
- package/bin/stop.js +3 -3
- package/bin/utils/config-helpers.js +198 -0
- package/dist/server/config-managed-fields.js +69 -0
- package/dist/server/config-merge.js +260 -0
- package/dist/server/database-factory.js +11 -181
- package/dist/server/fs-database.js +209 -72
- package/dist/server/main.js +365 -283
- package/dist/server/original-config-reader.js +154 -88
- package/dist/server/proxy-server.js +993 -387
- package/dist/server/rules-status-service.js +285 -54
- package/dist/server/transformers/chunk-collector.js +26 -4
- package/dist/server/transformers/streaming.js +2334 -280
- package/dist/server/transformers/transformers.js +1765 -0
- package/dist/ui/assets/index-GQBwe1Rm.js +514 -0
- package/dist/ui/index.html +1 -1
- package/package.json +4 -8
- package/schema/claude.schema.md +15 -14
- package/schema/deepseek-chat.schema.md +799 -0
- package/schema/{openai.schema.md → openai-chat-completions.schema.md} +9 -1083
- package/schema/openai-responses.schema.md +226196 -0
- package/schema/stream.md +2592 -0
- package/dist/server/database.js +0 -1609
- package/dist/server/migrate-to-fs.js +0 -353
- package/dist/server/transformers/claude-openai.js +0 -868
- package/dist/server/transformers/gemini.js +0 -625
- package/dist/ui/assets/index-BqSYpNgU.js +0 -511
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const toml = require('@iarna/toml');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TOML 解析器
|
|
7
|
+
*/
|
|
8
|
+
const parseToml = (content) => {
|
|
9
|
+
try {
|
|
10
|
+
return toml.parse(content);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
throw new Error(`Failed to parse TOML: ${error.message}`);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* TOML 序列化器
|
|
18
|
+
*/
|
|
19
|
+
const stringifyToml = (obj) => {
|
|
20
|
+
try {
|
|
21
|
+
return toml.stringify(obj);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
throw new Error(`Failed to stringify TOML: ${error.message}`);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 深拷贝
|
|
29
|
+
*/
|
|
30
|
+
const deepClone = (value) => JSON.parse(JSON.stringify(value));
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 判断字段路径是否被管理(支持前缀匹配)
|
|
34
|
+
*/
|
|
35
|
+
const isManagedPath = (fieldPath, managedFields) => {
|
|
36
|
+
return managedFields.some((managedField) => {
|
|
37
|
+
const managedPath = managedField.split('.');
|
|
38
|
+
if (fieldPath.length < managedPath.length) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < managedPath.length; i += 1) {
|
|
43
|
+
if (String(fieldPath[i]) !== managedPath[i]) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 深度获取对象值
|
|
53
|
+
*/
|
|
54
|
+
const deepGet = (obj, fieldPath) => {
|
|
55
|
+
let current = obj;
|
|
56
|
+
for (const segment of fieldPath) {
|
|
57
|
+
if (current === undefined || current === null) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
current = current[segment];
|
|
61
|
+
}
|
|
62
|
+
return current;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 深度设置对象值
|
|
67
|
+
*/
|
|
68
|
+
const deepSet = (obj, fieldPath, value) => {
|
|
69
|
+
let current = obj;
|
|
70
|
+
for (let i = 0; i < fieldPath.length - 1; i += 1) {
|
|
71
|
+
const key = fieldPath[i];
|
|
72
|
+
if (current[key] === undefined || current[key] === null || typeof current[key] !== 'object') {
|
|
73
|
+
current[key] = {};
|
|
74
|
+
}
|
|
75
|
+
current = current[key];
|
|
76
|
+
}
|
|
77
|
+
current[fieldPath[fieldPath.length - 1]] = value;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 收集所有叶子字段路径(数组视为叶子)
|
|
82
|
+
*/
|
|
83
|
+
const collectPaths = (obj, currentPath = [], allPaths = []) => {
|
|
84
|
+
if (obj === null || obj === undefined) {
|
|
85
|
+
if (currentPath.length > 0) {
|
|
86
|
+
allPaths.push(currentPath);
|
|
87
|
+
}
|
|
88
|
+
return allPaths;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (Array.isArray(obj)) {
|
|
92
|
+
if (currentPath.length > 0) {
|
|
93
|
+
allPaths.push(currentPath);
|
|
94
|
+
}
|
|
95
|
+
return allPaths;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (typeof obj !== 'object') {
|
|
99
|
+
if (currentPath.length > 0) {
|
|
100
|
+
allPaths.push(currentPath);
|
|
101
|
+
}
|
|
102
|
+
return allPaths;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const keys = Object.keys(obj);
|
|
106
|
+
if (keys.length === 0) {
|
|
107
|
+
if (currentPath.length > 0) {
|
|
108
|
+
allPaths.push(currentPath);
|
|
109
|
+
}
|
|
110
|
+
return allPaths;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const key of keys) {
|
|
114
|
+
collectPaths(obj[key], [...currentPath, key], allPaths);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return allPaths;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* JSON 合并函数
|
|
122
|
+
* 以 source 为基础,合并 other 中的非管理字段
|
|
123
|
+
*/
|
|
124
|
+
const mergeJsonSettings = (source, other, managedFields) => {
|
|
125
|
+
const result = deepClone(source);
|
|
126
|
+
const allPaths = collectPaths(other);
|
|
127
|
+
|
|
128
|
+
for (const fieldPath of allPaths) {
|
|
129
|
+
if (isManagedPath(fieldPath, managedFields)) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const value = deepGet(other, fieldPath);
|
|
134
|
+
if (value !== undefined) {
|
|
135
|
+
deepSet(result, fieldPath, deepClone(value));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return result;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* TOML 合并函数
|
|
144
|
+
* 以 source 为基础,合并 other 中的非管理字段
|
|
145
|
+
*/
|
|
146
|
+
const mergeTomlSettings = (source, other, managedFields) => {
|
|
147
|
+
const result = deepClone(source);
|
|
148
|
+
const allPaths = collectPaths(other);
|
|
149
|
+
|
|
150
|
+
for (const fieldPath of allPaths) {
|
|
151
|
+
if (isManagedPath(fieldPath, managedFields)) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const value = deepGet(other, fieldPath);
|
|
156
|
+
if (value !== undefined) {
|
|
157
|
+
deepSet(result, fieldPath, deepClone(value));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return result;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 原子性写入函数
|
|
166
|
+
* 先写入临时文件,然后原子性重命名
|
|
167
|
+
*/
|
|
168
|
+
const atomicWriteFile = (filePath, content) => {
|
|
169
|
+
const tempFile = path.join(path.dirname(filePath), `.tmp_${path.basename(filePath)}`);
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
// 确保目录存在
|
|
173
|
+
const dir = path.dirname(filePath);
|
|
174
|
+
if (!fs.existsSync(dir)) {
|
|
175
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 写入临时文件
|
|
179
|
+
fs.writeFileSync(tempFile, content, 'utf-8');
|
|
180
|
+
|
|
181
|
+
// 原子性重命名
|
|
182
|
+
fs.renameSync(tempFile, filePath);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
// 清理临时文件
|
|
185
|
+
if (fs.existsSync(tempFile)) {
|
|
186
|
+
fs.unlinkSync(tempFile);
|
|
187
|
+
}
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
module.exports = {
|
|
193
|
+
parseToml,
|
|
194
|
+
stringifyToml,
|
|
195
|
+
mergeJsonSettings,
|
|
196
|
+
mergeTomlSettings,
|
|
197
|
+
atomicWriteFile,
|
|
198
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getManagedFields = exports.CODEX_AUTH_MANAGED_FIELDS = exports.CODEX_CONFIG_MANAGED_FIELDS = exports.CLAUDE_JSON_MANAGED_FIELDS = exports.CLAUDE_SETTINGS_MANAGED_FIELDS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Claude Code settings.json 管理字段定义
|
|
6
|
+
*/
|
|
7
|
+
exports.CLAUDE_SETTINGS_MANAGED_FIELDS = [
|
|
8
|
+
{ path: ['env', 'ANTHROPIC_AUTH_TOKEN'] },
|
|
9
|
+
{ path: ['env', 'ANTHROPIC_BASE_URL'] },
|
|
10
|
+
{ path: ['env', 'API_TIMEOUT_MS'] },
|
|
11
|
+
{ path: ['env', 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC'] },
|
|
12
|
+
{ path: ['env', 'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS'], optional: true },
|
|
13
|
+
{ path: ['env', 'ANTHROPIC_DEFAULT_HAIKU_MODEL'], optional: true },
|
|
14
|
+
{ path: ['env', 'ANTHROPIC_DEFAULT_SONNET_MODEL'], optional: true },
|
|
15
|
+
{ path: ['env', 'ANTHROPIC_DEFAULT_OPUS_MODEL'], optional: true },
|
|
16
|
+
{ path: ['permissions'], optional: true },
|
|
17
|
+
{ path: ['skipDangerousModePermissionPrompt'], optional: true },
|
|
18
|
+
{ path: ['effortLevel'], optional: true },
|
|
19
|
+
{ path: ['model'], optional: true },
|
|
20
|
+
];
|
|
21
|
+
/**
|
|
22
|
+
* Claude Code .claude.json 管理字段定义
|
|
23
|
+
*/
|
|
24
|
+
exports.CLAUDE_JSON_MANAGED_FIELDS = [
|
|
25
|
+
{ path: ['hasCompletedOnboarding'] },
|
|
26
|
+
{ path: ['mcpServers'], optional: true },
|
|
27
|
+
];
|
|
28
|
+
/**
|
|
29
|
+
* Codex config.toml 管理字段定义
|
|
30
|
+
*/
|
|
31
|
+
exports.CODEX_CONFIG_MANAGED_FIELDS = [
|
|
32
|
+
{ path: ['model_provider'] },
|
|
33
|
+
{ path: ['model'] },
|
|
34
|
+
{ path: ['model_reasoning_effort'] },
|
|
35
|
+
{ path: ['disable_response_storage'] },
|
|
36
|
+
{ path: ['preferred_auth_method'] },
|
|
37
|
+
{ path: ['requires_openai_auth'] },
|
|
38
|
+
{ path: ['enableRouteSelection'] },
|
|
39
|
+
{ path: ['model_providers', 'aicodeswitch'], isSection: true },
|
|
40
|
+
];
|
|
41
|
+
/**
|
|
42
|
+
* Codex auth.json 管理字段定义
|
|
43
|
+
*/
|
|
44
|
+
exports.CODEX_AUTH_MANAGED_FIELDS = [
|
|
45
|
+
{ path: ['OPENAI_API_KEY'] },
|
|
46
|
+
];
|
|
47
|
+
/**
|
|
48
|
+
* 根据配置类型和文件路径获取管理字段列表
|
|
49
|
+
*/
|
|
50
|
+
const getManagedFields = (configType, filePath) => {
|
|
51
|
+
if (configType === 'claude') {
|
|
52
|
+
if (filePath.endsWith('settings.json')) {
|
|
53
|
+
return exports.CLAUDE_SETTINGS_MANAGED_FIELDS;
|
|
54
|
+
}
|
|
55
|
+
else if (filePath.endsWith('.claude.json')) {
|
|
56
|
+
return exports.CLAUDE_JSON_MANAGED_FIELDS;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else if (configType === 'codex') {
|
|
60
|
+
if (filePath.endsWith('config.toml')) {
|
|
61
|
+
return exports.CODEX_CONFIG_MANAGED_FIELDS;
|
|
62
|
+
}
|
|
63
|
+
else if (filePath.endsWith('auth.json')) {
|
|
64
|
+
return exports.CODEX_AUTH_MANAGED_FIELDS;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return [];
|
|
68
|
+
};
|
|
69
|
+
exports.getManagedFields = getManagedFields;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.safeWriteConfig = exports.atomicWriteFile = exports.mergeTomlConfig = exports.stringifyToml = exports.parseToml = exports.mergeJsonConfig = exports.isFieldManaged = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const toml_1 = __importDefault(require("@iarna/toml"));
|
|
10
|
+
/**
|
|
11
|
+
* 判断字段路径是否匹配
|
|
12
|
+
* @param fieldPath 要检查的字段路径
|
|
13
|
+
* @param managedPath 管理字段路径定义
|
|
14
|
+
* @returns 是否匹配
|
|
15
|
+
*/
|
|
16
|
+
const pathMatches = (fieldPath, managedPath) => {
|
|
17
|
+
if (fieldPath.length < managedPath.length) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
for (let i = 0; i < managedPath.length; i++) {
|
|
21
|
+
const fieldKey = fieldPath[i];
|
|
22
|
+
const managedKey = managedPath[i];
|
|
23
|
+
// 如果管理字段是通配符(数字索引),则匹配任何数字
|
|
24
|
+
if (typeof managedKey === 'number') {
|
|
25
|
+
if (typeof fieldKey !== 'number') {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else if (fieldKey !== managedKey) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* 判断字段是否被管理
|
|
37
|
+
* @param fieldPath 字段路径
|
|
38
|
+
* @param managedFields 管理字段列表
|
|
39
|
+
* @returns 是否被管理
|
|
40
|
+
*/
|
|
41
|
+
const isFieldManaged = (fieldPath, managedFields) => {
|
|
42
|
+
for (const managed of managedFields) {
|
|
43
|
+
if (pathMatches(fieldPath, managed.path)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
};
|
|
49
|
+
exports.isFieldManaged = isFieldManaged;
|
|
50
|
+
/**
|
|
51
|
+
* 递归遍历对象,收集叶子字段路径
|
|
52
|
+
* @param obj 要遍历的对象
|
|
53
|
+
* @param currentPath 当前路径
|
|
54
|
+
* @param allPaths 收集所有路径的数组
|
|
55
|
+
*/
|
|
56
|
+
const collectPaths = (obj, currentPath, allPaths) => {
|
|
57
|
+
// 基础类型/null:当前路径即叶子
|
|
58
|
+
if (obj === null || obj === undefined || typeof obj !== 'object') {
|
|
59
|
+
if (currentPath.length > 0) {
|
|
60
|
+
allPaths.push(currentPath);
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// 数组:递归收集元素叶子;空数组本身视为叶子
|
|
65
|
+
if (Array.isArray(obj)) {
|
|
66
|
+
if (obj.length === 0) {
|
|
67
|
+
if (currentPath.length > 0) {
|
|
68
|
+
allPaths.push(currentPath);
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
obj.forEach((item, index) => {
|
|
73
|
+
collectPaths(item, [...currentPath, index], allPaths);
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// 对象:递归收集子字段;空对象本身视为叶子
|
|
78
|
+
const keys = Object.keys(obj);
|
|
79
|
+
if (keys.length === 0) {
|
|
80
|
+
if (currentPath.length > 0) {
|
|
81
|
+
allPaths.push(currentPath);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
for (const key of keys) {
|
|
86
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
87
|
+
collectPaths(obj[key], [...currentPath, key], allPaths);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* 深度获取对象的值
|
|
93
|
+
* @param obj 目标对象
|
|
94
|
+
* @param path 字段路径
|
|
95
|
+
* @returns 字段值
|
|
96
|
+
*/
|
|
97
|
+
const deepGet = (obj, path) => {
|
|
98
|
+
let current = obj;
|
|
99
|
+
for (const key of path) {
|
|
100
|
+
if (current === null || current === undefined) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
current = current[key];
|
|
104
|
+
}
|
|
105
|
+
return current;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* 深度设置对象的值
|
|
109
|
+
* @param obj 目标对象
|
|
110
|
+
* @param path 字段路径
|
|
111
|
+
* @param value 要设置的值
|
|
112
|
+
*/
|
|
113
|
+
const deepSet = (obj, path, value) => {
|
|
114
|
+
let current = obj;
|
|
115
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
116
|
+
const key = path[i];
|
|
117
|
+
if (current[key] === undefined || current[key] === null || typeof current[key] !== 'object') {
|
|
118
|
+
current[key] = {};
|
|
119
|
+
}
|
|
120
|
+
current = current[key];
|
|
121
|
+
}
|
|
122
|
+
current[path[path.length - 1]] = value;
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* 合并 JSON 配置
|
|
126
|
+
* @param source 源配置(包含管理字段)
|
|
127
|
+
* @param other 其他配置(包含非管理字段)
|
|
128
|
+
* @param managedFields 管理字段列表
|
|
129
|
+
* @returns 合并后的配置
|
|
130
|
+
*/
|
|
131
|
+
const mergeJsonConfig = (source, other, managedFields) => {
|
|
132
|
+
// 复制源配置作为基础
|
|
133
|
+
const result = JSON.parse(JSON.stringify(source));
|
|
134
|
+
// 收集 other 中所有的字段路径
|
|
135
|
+
const allPaths = [];
|
|
136
|
+
collectPaths(other, [], allPaths);
|
|
137
|
+
// 对于每个路径,如果不是管理字段,则从 other 中复制到 result
|
|
138
|
+
for (const fieldPath of allPaths) {
|
|
139
|
+
if (!(0, exports.isFieldManaged)(fieldPath, managedFields)) {
|
|
140
|
+
const value = deepGet(other, fieldPath);
|
|
141
|
+
deepSet(result, fieldPath, value);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
};
|
|
146
|
+
exports.mergeJsonConfig = mergeJsonConfig;
|
|
147
|
+
/**
|
|
148
|
+
* 解析 TOML 配置文件
|
|
149
|
+
* @param content TOML 内容
|
|
150
|
+
* @returns 解析后的对象
|
|
151
|
+
*/
|
|
152
|
+
const parseToml = (content) => {
|
|
153
|
+
try {
|
|
154
|
+
return toml_1.default.parse(content);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
throw new Error(`Failed to parse TOML: ${error}`);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
exports.parseToml = parseToml;
|
|
161
|
+
/**
|
|
162
|
+
* 序列化对象为 TOML 格式
|
|
163
|
+
* @param obj 要序列化的对象
|
|
164
|
+
* @returns TOML 字符串
|
|
165
|
+
*/
|
|
166
|
+
const stringifyToml = (obj) => {
|
|
167
|
+
// 使用 @iarna/toml 库的 stringify 方法
|
|
168
|
+
try {
|
|
169
|
+
return toml_1.default.stringify(obj);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
throw new Error(`Failed to stringify TOML: ${error}`);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
exports.stringifyToml = stringifyToml;
|
|
176
|
+
/**
|
|
177
|
+
* 合并 TOML 配置
|
|
178
|
+
* @param source 源配置(包含管理字段)
|
|
179
|
+
* @param other 其他配置(包含非管理字段)
|
|
180
|
+
* @param managedFields 管理字段列表
|
|
181
|
+
* @returns 合并后的配置
|
|
182
|
+
*/
|
|
183
|
+
const mergeTomlConfig = (source, other, managedFields) => {
|
|
184
|
+
// 复制源配置作为基础
|
|
185
|
+
const result = JSON.parse(JSON.stringify(source));
|
|
186
|
+
// 收集 other 中所有的字段路径
|
|
187
|
+
const allPaths = [];
|
|
188
|
+
collectPaths(other, [], allPaths);
|
|
189
|
+
// 对于每个路径,如果不是管理字段,则从 other 中复制到 result
|
|
190
|
+
for (const fieldPath of allPaths) {
|
|
191
|
+
if (!(0, exports.isFieldManaged)(fieldPath, managedFields)) {
|
|
192
|
+
const value = deepGet(other, fieldPath);
|
|
193
|
+
deepSet(result, fieldPath, value);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
};
|
|
198
|
+
exports.mergeTomlConfig = mergeTomlConfig;
|
|
199
|
+
/**
|
|
200
|
+
* 原子性写入文件
|
|
201
|
+
* 先写入临时文件,然后重命名,确保写入失败时不会损坏原文件
|
|
202
|
+
* @param filePath 目标文件路径
|
|
203
|
+
* @param content 要写入的内容
|
|
204
|
+
*/
|
|
205
|
+
const atomicWriteFile = (filePath, content) => {
|
|
206
|
+
const dir = path_1.default.dirname(filePath);
|
|
207
|
+
const tempFile = path_1.default.join(dir, `.tmp_${path_1.default.basename(filePath)}`);
|
|
208
|
+
try {
|
|
209
|
+
// 确保目录存在
|
|
210
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
211
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
212
|
+
}
|
|
213
|
+
// 写入临时文件
|
|
214
|
+
fs_1.default.writeFileSync(tempFile, content, 'utf-8');
|
|
215
|
+
// 原子性重命名
|
|
216
|
+
fs_1.default.renameSync(tempFile, filePath);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
// 清理临时文件
|
|
220
|
+
if (fs_1.default.existsSync(tempFile)) {
|
|
221
|
+
fs_1.default.unlinkSync(tempFile);
|
|
222
|
+
}
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
exports.atomicWriteFile = atomicWriteFile;
|
|
227
|
+
/**
|
|
228
|
+
* 带回退的安全写入
|
|
229
|
+
* 如果写入失败,恢复原文件
|
|
230
|
+
* @param filePath 目标文件路径
|
|
231
|
+
* @param content 要写入的内容
|
|
232
|
+
* @returns 是否成功
|
|
233
|
+
*/
|
|
234
|
+
const safeWriteConfig = (filePath, content) => {
|
|
235
|
+
let originalContent = null;
|
|
236
|
+
try {
|
|
237
|
+
// 如果原文件存在,读取其内容
|
|
238
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
239
|
+
originalContent = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
240
|
+
}
|
|
241
|
+
// 原子性写入新内容
|
|
242
|
+
(0, exports.atomicWriteFile)(filePath, content);
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
console.error(`Failed to write ${filePath}:`, error);
|
|
247
|
+
// 如果写入失败且原文件存在,恢复原文件
|
|
248
|
+
if (originalContent !== null && !fs_1.default.existsSync(filePath)) {
|
|
249
|
+
try {
|
|
250
|
+
fs_1.default.writeFileSync(filePath, originalContent, 'utf-8');
|
|
251
|
+
console.log(`Restored ${filePath} from backup`);
|
|
252
|
+
}
|
|
253
|
+
catch (restoreError) {
|
|
254
|
+
console.error(`Failed to restore ${filePath}:`, restoreError);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
exports.safeWriteConfig = safeWriteConfig;
|