aicodeswitch 5.2.4 → 5.2.6
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 +1 -0
- package/bin/utils/config-helpers.js +2 -1
- package/bin/utils/managed-fields.js +1 -1
- package/dist/server/config-managed-fields.js +1 -1
- package/dist/server/config-merge.js +2 -1
- package/dist/server/conversions/detector.js +13 -0
- package/dist/server/conversions/index.js +2 -1
- package/dist/server/conversions/pairs/responses-responses/request.js +7 -8
- package/dist/server/fs-database.js +179 -0
- package/dist/server/main.js +39 -9
- package/dist/server/proxy-server.js +10 -6
- package/dist/ui/assets/index-KBja3wfR.js +822 -0
- package/dist/ui/assets/index-MjMlew6J.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-Cws89pD2.js +0 -828
- package/dist/ui/assets/index-CzfKxImD.css +0 -1
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ AI Code Switch 是帮助你在本地管理 AI 编程工具接入大模型的工
|
|
|
11
11
|
- 视频演示:[https://www.bilibili.com/video/BV1uEznBuEJd/](https://www.bilibili.com/video/BV1uEznBuEJd/?from=github)
|
|
12
12
|
- 1分钟让Claude Code接入GLM国产模型:[https://www.bilibili.com/video/BV1a865B8ErA/](https://www.bilibili.com/video/BV1a865B8ErA/)
|
|
13
13
|
- 1分钟让Codex接入免费模型Agnes:[https://www.bilibili.com/video/BV1tQ7Q63Eva/](https://www.bilibili.com/video/BV1tQ7Q63Eva/) Codex接入Deepseek可参考该方法实现
|
|
14
|
+
- 在线教程:[https://space.bilibili.com/37442719/lists/8333248](https://space.bilibili.com/37442719/lists/8333248)
|
|
14
15
|
|
|
15
16
|
## 功能要点
|
|
16
17
|
|
|
@@ -70,7 +70,8 @@ const deepSet = (obj, fieldPath, value) => {
|
|
|
70
70
|
for (let i = 0; i < fieldPath.length - 1; i += 1) {
|
|
71
71
|
const key = fieldPath[i];
|
|
72
72
|
if (current[key] === undefined || current[key] === null || typeof current[key] !== 'object') {
|
|
73
|
-
|
|
73
|
+
// 下一级路径为数字索引时创建数组,否则创建对象(正确还原数组结构)
|
|
74
|
+
current[key] = typeof fieldPath[i + 1] === 'number' ? [] : {};
|
|
74
75
|
}
|
|
75
76
|
current = current[key];
|
|
76
77
|
}
|
|
@@ -18,7 +18,7 @@ const CLAUDE_SETTINGS_MANAGED_FIELDS = [
|
|
|
18
18
|
'env.ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
19
19
|
'env.ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
20
20
|
'env.ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
21
|
-
'permissions',
|
|
21
|
+
'permissions.defaultMode',
|
|
22
22
|
'skipDangerousModePermissionPrompt',
|
|
23
23
|
'effortLevel',
|
|
24
24
|
'model',
|
|
@@ -16,7 +16,7 @@ exports.CLAUDE_SETTINGS_MANAGED_FIELDS = [
|
|
|
16
16
|
{ path: ['env', 'ANTHROPIC_DEFAULT_HAIKU_MODEL'], optional: true },
|
|
17
17
|
{ path: ['env', 'ANTHROPIC_DEFAULT_SONNET_MODEL'], optional: true },
|
|
18
18
|
{ path: ['env', 'ANTHROPIC_DEFAULT_OPUS_MODEL'], optional: true },
|
|
19
|
-
{ path: ['permissions'], optional: true },
|
|
19
|
+
{ path: ['permissions', 'defaultMode'], optional: true },
|
|
20
20
|
{ path: ['skipDangerousModePermissionPrompt'], optional: true },
|
|
21
21
|
{ path: ['effortLevel'], optional: true },
|
|
22
22
|
{ path: ['model'], optional: true },
|
|
@@ -115,7 +115,8 @@ const deepSet = (obj, path, value) => {
|
|
|
115
115
|
for (let i = 0; i < path.length - 1; i++) {
|
|
116
116
|
const key = path[i];
|
|
117
117
|
if (current[key] === undefined || current[key] === null || typeof current[key] !== 'object') {
|
|
118
|
-
|
|
118
|
+
// 下一级路径为数字索引时创建数组,否则创建对象(正确还原 collectPaths 收集到的数组结构)
|
|
119
|
+
current[key] = typeof path[i + 1] === 'number' ? [] : {};
|
|
119
120
|
}
|
|
120
121
|
current = current[key];
|
|
121
122
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.detectRequestFormat = detectRequestFormat;
|
|
4
|
+
exports.isOfficialOpenAiApi = isOfficialOpenAiApi;
|
|
4
5
|
exports.sourceTypeToFormat = sourceTypeToFormat;
|
|
5
6
|
/**
|
|
6
7
|
* Detect the format of the incoming request based on path and body structure.
|
|
@@ -48,6 +49,18 @@ function detectRequestFormat(path, body) {
|
|
|
48
49
|
// Default to completions
|
|
49
50
|
return 'completions';
|
|
50
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* 判断上游 URL 是否为 OpenAI 官方(含 Azure OpenAI)端点。
|
|
54
|
+
*
|
|
55
|
+
* 官方端点完整支持 Responses API 的私有扩展(custom 自定义工具、
|
|
56
|
+
* tool_search/web_search 等内置工具、reasoning.effort、text.verbosity 等),
|
|
57
|
+
* 因此 responses→responses 直连时无需降级兼容。其余第三方 Responses 提供商
|
|
58
|
+
* (火山方舟/豆包等)不支持这些扩展,需要走 downgradeResponsesRequest。
|
|
59
|
+
*/
|
|
60
|
+
function isOfficialOpenAiApi(apiUrl) {
|
|
61
|
+
const u = (apiUrl || '').toLowerCase();
|
|
62
|
+
return u.includes('api.openai.com') || u.includes('.openai.azure.com');
|
|
63
|
+
}
|
|
51
64
|
/**
|
|
52
65
|
* Determine the upstream format from a SourceType string.
|
|
53
66
|
* Maps the legacy SourceType values to the new Format type.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* is driven by the ReasoningConfig passed through TransformRequestOptions.
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.processCompactResponse = exports.prepareCompactRequest = exports.buildCompactedResponse = exports.extractSummaryFromResponse = exports.buildCompactUpstreamRequest = exports.buildCompactionPrompt = exports.COMPACTION_SYSTEM_PROMPT = exports.isCodexCompactRequest = exports.isLastClaudeMessageCompact = exports.isClaudeCompactRequest = exports.extractMessageContent = exports.extractConversationText = exports.sanitizeRequestBody = exports.getServerToolSupport = exports.getReasoningConfig = exports.sourceTypeToFormat = exports.detectRequestFormat = void 0;
|
|
18
|
+
exports.processCompactResponse = exports.prepareCompactRequest = exports.buildCompactedResponse = exports.extractSummaryFromResponse = exports.buildCompactUpstreamRequest = exports.buildCompactionPrompt = exports.COMPACTION_SYSTEM_PROMPT = exports.isCodexCompactRequest = exports.isLastClaudeMessageCompact = exports.isClaudeCompactRequest = exports.extractMessageContent = exports.extractConversationText = exports.sanitizeRequestBody = exports.getServerToolSupport = exports.getReasoningConfig = exports.isOfficialOpenAiApi = exports.sourceTypeToFormat = exports.detectRequestFormat = void 0;
|
|
19
19
|
exports.transformRequest = transformRequest;
|
|
20
20
|
exports.transformResponse = transformResponse;
|
|
21
21
|
exports.createStreamConverter = createStreamConverter;
|
|
@@ -23,6 +23,7 @@ exports.buildTargetBody = buildTargetBody;
|
|
|
23
23
|
var detector_js_1 = require("./detector.js");
|
|
24
24
|
Object.defineProperty(exports, "detectRequestFormat", { enumerable: true, get: function () { return detector_js_1.detectRequestFormat; } });
|
|
25
25
|
Object.defineProperty(exports, "sourceTypeToFormat", { enumerable: true, get: function () { return detector_js_1.sourceTypeToFormat; } });
|
|
26
|
+
Object.defineProperty(exports, "isOfficialOpenAiApi", { enumerable: true, get: function () { return detector_js_1.isOfficialOpenAiApi; } });
|
|
26
27
|
var providers_js_1 = require("./thinking/providers.js");
|
|
27
28
|
Object.defineProperty(exports, "getReasoningConfig", { enumerable: true, get: function () { return providers_js_1.getReasoningConfig; } });
|
|
28
29
|
const providers_js_2 = require("./thinking/providers.js");
|
|
@@ -8,13 +8,12 @@
|
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.downgradeResponsesRequest = downgradeResponsesRequest;
|
|
10
10
|
/**
|
|
11
|
-
* Responses API
|
|
12
|
-
* OpenAI 私有扩展(custom、
|
|
13
|
-
*
|
|
11
|
+
* 第三方 Responses API 提供商仅支持 function 类型工具。
|
|
12
|
+
* OpenAI 私有扩展(apply_patch 的 custom、MCP 的 namespace、
|
|
13
|
+
* tool_search、web_search、file_search、code_interpreter 等)一律剥离,
|
|
14
|
+
* 直接转发会导致 400 `unknown tool type` 错误。
|
|
15
|
+
* 采用 function 白名单而非黑名单,避免新增私有类型时遗漏。
|
|
14
16
|
*/
|
|
15
|
-
const NON_STANDARD_TOOL_TYPES = new Set([
|
|
16
|
-
'custom', 'tool_search', 'web_search', 'file_search', 'code_interpreter',
|
|
17
|
-
]);
|
|
18
17
|
/**
|
|
19
18
|
* Responses API 降级兼容时需移除的顶层字段集合。
|
|
20
19
|
* 这些字段非所有 Responses API 提供商都支持,开启降级兼容时会被移除以避免 400 错误。
|
|
@@ -67,9 +66,9 @@ function downgradeResponsesRequest(body) {
|
|
|
67
66
|
sanitized = Object.assign({}, sanitized);
|
|
68
67
|
return sanitized;
|
|
69
68
|
};
|
|
70
|
-
// 1.
|
|
69
|
+
// 1. 仅保留 function 类型工具,剥离 custom/namespace/tool_search/web_search 等私有类型
|
|
71
70
|
if (Array.isArray(body.tools)) {
|
|
72
|
-
const filteredTools = body.tools.filter((t) =>
|
|
71
|
+
const filteredTools = body.tools.filter((t) => t && t.type === 'function');
|
|
73
72
|
if (filteredTools.length !== body.tools.length) {
|
|
74
73
|
sanitized = Object.assign(Object.assign({}, sanitized), { tools: filteredTools });
|
|
75
74
|
}
|
|
@@ -34,12 +34,17 @@ const DEFAULT_CODEX_REASONING_EFFORT = 'high';
|
|
|
34
34
|
const DEFAULT_FAILOVER_RECOVERY_SECONDS = 30;
|
|
35
35
|
const VALID_CLAUDE_EFFORT_LEVELS = ['low', 'medium', 'high', 'max'];
|
|
36
36
|
const DEFAULT_CLAUDE_EFFORT_LEVEL = 'medium';
|
|
37
|
+
const VALID_CLAUDE_PERMISSION_DEFAULT_MODES = ['default', 'acceptEdits', 'plan', 'auto', 'dontAsk', 'bypassPermissions'];
|
|
38
|
+
const DEFAULT_CLAUDE_PERMISSION_DEFAULT_MODE = 'default';
|
|
37
39
|
const isCodexReasoningEffort = (value) => {
|
|
38
40
|
return typeof value === 'string' && VALID_CODEX_REASONING_EFFORTS.includes(value);
|
|
39
41
|
};
|
|
40
42
|
const isClaudeEffortLevel = (value) => {
|
|
41
43
|
return typeof value === 'string' && VALID_CLAUDE_EFFORT_LEVELS.includes(value);
|
|
42
44
|
};
|
|
45
|
+
const isClaudePermissionDefaultMode = (value) => {
|
|
46
|
+
return typeof value === 'string' && VALID_CLAUDE_PERMISSION_DEFAULT_MODES.includes(value);
|
|
47
|
+
};
|
|
43
48
|
const isValidAutocompactPct = (v) => {
|
|
44
49
|
return typeof v === 'number' && Number.isInteger(v) && v >= 1 && v <= 100;
|
|
45
50
|
};
|
|
@@ -1208,6 +1213,7 @@ class FileSystemDatabaseManager {
|
|
|
1208
1213
|
ruleGlobalTimeout: undefined,
|
|
1209
1214
|
enableAgentTeams: false,
|
|
1210
1215
|
enableBypassPermissionsSupport: false,
|
|
1216
|
+
claudePermissionsDefaultMode: DEFAULT_CLAUDE_PERMISSION_DEFAULT_MODE,
|
|
1211
1217
|
claudeEffortLevel: DEFAULT_CLAUDE_EFFORT_LEVEL,
|
|
1212
1218
|
autocompactPctOverride: undefined,
|
|
1213
1219
|
claudeDefaultModel: undefined,
|
|
@@ -1228,6 +1234,12 @@ class FileSystemDatabaseManager {
|
|
|
1228
1234
|
if (!isClaudeEffortLevel(this.config.claudeEffortLevel)) {
|
|
1229
1235
|
this.config.claudeEffortLevel = DEFAULT_CLAUDE_EFFORT_LEVEL;
|
|
1230
1236
|
}
|
|
1237
|
+
if (!isClaudePermissionDefaultMode(this.config.claudePermissionsDefaultMode)) {
|
|
1238
|
+
// 缺失或非法时,按旧 enableBypassPermissionsSupport 推导(迁移兼容)
|
|
1239
|
+
this.config.claudePermissionsDefaultMode = this.config.enableBypassPermissionsSupport === true
|
|
1240
|
+
? 'bypassPermissions'
|
|
1241
|
+
: DEFAULT_CLAUDE_PERMISSION_DEFAULT_MODE;
|
|
1242
|
+
}
|
|
1231
1243
|
if (typeof this.config.autocompactPctOverride !== 'undefined' && !isValidAutocompactPct(this.config.autocompactPctOverride)) {
|
|
1232
1244
|
this.config.autocompactPctOverride = undefined;
|
|
1233
1245
|
}
|
|
@@ -1252,6 +1264,7 @@ class FileSystemDatabaseManager {
|
|
|
1252
1264
|
const hasGlobalToolConfig = !!this.config &&
|
|
1253
1265
|
(Object.prototype.hasOwnProperty.call(this.config, 'enableAgentTeams') ||
|
|
1254
1266
|
Object.prototype.hasOwnProperty.call(this.config, 'enableBypassPermissionsSupport') ||
|
|
1267
|
+
Object.prototype.hasOwnProperty.call(this.config, 'claudePermissionsDefaultMode') ||
|
|
1255
1268
|
Object.prototype.hasOwnProperty.call(this.config, 'codexModelReasoningEffort'));
|
|
1256
1269
|
if (!hasGlobalToolConfig) {
|
|
1257
1270
|
const getPreferredRoute = (targetType) => {
|
|
@@ -2318,6 +2331,11 @@ class FileSystemDatabaseManager {
|
|
|
2318
2331
|
if (!isClaudeEffortLevel(merged.claudeEffortLevel)) {
|
|
2319
2332
|
merged.claudeEffortLevel = DEFAULT_CLAUDE_EFFORT_LEVEL;
|
|
2320
2333
|
}
|
|
2334
|
+
if (!isClaudePermissionDefaultMode(merged.claudePermissionsDefaultMode)) {
|
|
2335
|
+
merged.claudePermissionsDefaultMode = merged.enableBypassPermissionsSupport === true
|
|
2336
|
+
? 'bypassPermissions'
|
|
2337
|
+
: DEFAULT_CLAUDE_PERMISSION_DEFAULT_MODE;
|
|
2338
|
+
}
|
|
2321
2339
|
if (typeof merged.autocompactPctOverride !== 'undefined' && !isValidAutocompactPct(merged.autocompactPctOverride)) {
|
|
2322
2340
|
merged.autocompactPctOverride = undefined;
|
|
2323
2341
|
}
|
|
@@ -3048,6 +3066,167 @@ class FileSystemDatabaseManager {
|
|
|
3048
3066
|
yield this.saveSessions();
|
|
3049
3067
|
});
|
|
3050
3068
|
}
|
|
3069
|
+
/**
|
|
3070
|
+
* 批量清理过期会话
|
|
3071
|
+
* 以最后请求时间为基准,清理 lastRequestAt 早于 beforeTimestamp 的会话。
|
|
3072
|
+
* @param beforeTimestamp 时间阈值(毫秒),最后请求时间严格早于此值的会话将被处理
|
|
3073
|
+
* @param options.onlyLogs 仅清空关联日志,保留会话本身
|
|
3074
|
+
* @returns 受影响会话数 / 删除的日志数
|
|
3075
|
+
*/
|
|
3076
|
+
cleanupSessionsByAge(beforeTimestamp_1) {
|
|
3077
|
+
return __awaiter(this, arguments, void 0, function* (beforeTimestamp, options = {}) {
|
|
3078
|
+
const targetSessions = this.sessions.filter(s => s.lastRequestAt < beforeTimestamp);
|
|
3079
|
+
if (targetSessions.length === 0) {
|
|
3080
|
+
return { sessionsAffected: 0, logsDeleted: 0 };
|
|
3081
|
+
}
|
|
3082
|
+
const targetIds = new Set(targetSessions.map(s => s.id));
|
|
3083
|
+
const { logsDeleted } = yield this.deleteLogsBySessionIds(targetIds);
|
|
3084
|
+
if (!options.onlyLogs) {
|
|
3085
|
+
this.sessions = this.sessions.filter(s => !targetIds.has(s.id));
|
|
3086
|
+
yield this.saveSessions();
|
|
3087
|
+
}
|
|
3088
|
+
return { sessionsAffected: targetSessions.length, logsDeleted };
|
|
3089
|
+
});
|
|
3090
|
+
}
|
|
3091
|
+
/**
|
|
3092
|
+
* 删除指定会话集合关联的所有日志条目,并维护分片文件与索引的一致性。
|
|
3093
|
+
* 多个会话可能共享同一分片,按分片重写并修正其余会话的 index 引用。
|
|
3094
|
+
* 与 addLog 共享分片写入锁(shardWriteLocks),避免并发写入竞争。
|
|
3095
|
+
*/
|
|
3096
|
+
deleteLogsBySessionIds(sessionIds) {
|
|
3097
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
3098
|
+
if (sessionIds.size === 0)
|
|
3099
|
+
return { logsDeleted: 0 };
|
|
3100
|
+
// 1. 按分片文件收集待删除的 index(基于内存索引快照)
|
|
3101
|
+
const shardDeletes = new Map();
|
|
3102
|
+
for (const sid of sessionIds) {
|
|
3103
|
+
const refs = this.sessionLogIndex.get(sid);
|
|
3104
|
+
if (!refs)
|
|
3105
|
+
continue;
|
|
3106
|
+
for (const ref of refs) {
|
|
3107
|
+
let set = shardDeletes.get(ref.filename);
|
|
3108
|
+
if (!set) {
|
|
3109
|
+
set = new Set();
|
|
3110
|
+
shardDeletes.set(ref.filename, set);
|
|
3111
|
+
}
|
|
3112
|
+
set.add(ref.index);
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
let logsDeleted = 0;
|
|
3116
|
+
// 2. 逐分片重写(在分片锁内执行,与该分片的 addLog 串行)
|
|
3117
|
+
for (const [filename, deleteSet] of shardDeletes) {
|
|
3118
|
+
let shardRemoved = 0;
|
|
3119
|
+
const prevLock = this.shardWriteLocks.get(filename) || Promise.resolve();
|
|
3120
|
+
const task = prevLock.then(() => __awaiter(this, void 0, void 0, function* () {
|
|
3121
|
+
const shardLogs = yield this.loadLogShard(filename);
|
|
3122
|
+
if (shardLogs.length === 0)
|
|
3123
|
+
return;
|
|
3124
|
+
// 构建 oldIndex -> newIndex 映射(仅保留项)
|
|
3125
|
+
const newIndexMap = new Map();
|
|
3126
|
+
const newLogs = [];
|
|
3127
|
+
let removed = 0;
|
|
3128
|
+
for (let i = 0; i < shardLogs.length; i++) {
|
|
3129
|
+
if (deleteSet.has(i)) {
|
|
3130
|
+
removed++;
|
|
3131
|
+
continue;
|
|
3132
|
+
}
|
|
3133
|
+
newIndexMap.set(i, newLogs.length);
|
|
3134
|
+
newLogs.push(shardLogs[i]);
|
|
3135
|
+
}
|
|
3136
|
+
if (removed === 0)
|
|
3137
|
+
return;
|
|
3138
|
+
shardRemoved = removed;
|
|
3139
|
+
// 重写或删除分片文件
|
|
3140
|
+
if (newLogs.length === 0) {
|
|
3141
|
+
try {
|
|
3142
|
+
yield promises_1.default.unlink(path_1.default.join(this.logsDir, filename));
|
|
3143
|
+
}
|
|
3144
|
+
catch (_a) {
|
|
3145
|
+
// 文件可能已被其它清理流程删除,忽略
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
else {
|
|
3149
|
+
yield this.saveLogShard(filename, newLogs);
|
|
3150
|
+
}
|
|
3151
|
+
// 更新分片索引
|
|
3152
|
+
const shardIndex = this.logShardsIndex.find(s => s.filename === filename);
|
|
3153
|
+
if (shardIndex) {
|
|
3154
|
+
if (newLogs.length === 0) {
|
|
3155
|
+
this.logShardsIndex = this.logShardsIndex.filter(s => s.filename !== filename);
|
|
3156
|
+
}
|
|
3157
|
+
else {
|
|
3158
|
+
shardIndex.count = newLogs.length;
|
|
3159
|
+
let minTs = Infinity;
|
|
3160
|
+
let maxTs = -Infinity;
|
|
3161
|
+
for (const l of newLogs) {
|
|
3162
|
+
if (l.timestamp < minTs)
|
|
3163
|
+
minTs = l.timestamp;
|
|
3164
|
+
if (l.timestamp > maxTs)
|
|
3165
|
+
maxTs = l.timestamp;
|
|
3166
|
+
}
|
|
3167
|
+
shardIndex.startTime = minTs;
|
|
3168
|
+
shardIndex.endTime = maxTs;
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
// 修正引用了该分片的非目标会话的 refs(index 偏移)
|
|
3172
|
+
for (const [sid, refs] of this.sessionLogIndex) {
|
|
3173
|
+
if (sessionIds.has(sid))
|
|
3174
|
+
continue;
|
|
3175
|
+
let touched = false;
|
|
3176
|
+
const updated = [];
|
|
3177
|
+
for (const ref of refs) {
|
|
3178
|
+
if (ref.filename !== filename) {
|
|
3179
|
+
updated.push(ref);
|
|
3180
|
+
continue;
|
|
3181
|
+
}
|
|
3182
|
+
if (deleteSet.has(ref.index)) {
|
|
3183
|
+
touched = true;
|
|
3184
|
+
continue;
|
|
3185
|
+
}
|
|
3186
|
+
const ni = newIndexMap.get(ref.index);
|
|
3187
|
+
if (ni !== undefined) {
|
|
3188
|
+
if (ni !== ref.index)
|
|
3189
|
+
touched = true;
|
|
3190
|
+
updated.push(Object.assign(Object.assign({}, ref), { index: ni }));
|
|
3191
|
+
}
|
|
3192
|
+
else {
|
|
3193
|
+
touched = true;
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
if (touched) {
|
|
3197
|
+
if (updated.length === 0) {
|
|
3198
|
+
this.sessionLogIndex.delete(sid);
|
|
3199
|
+
}
|
|
3200
|
+
else {
|
|
3201
|
+
this.sessionLogIndex.set(sid, updated);
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
}));
|
|
3206
|
+
this.shardWriteLocks.set(filename, task);
|
|
3207
|
+
try {
|
|
3208
|
+
yield task;
|
|
3209
|
+
logsDeleted += shardRemoved;
|
|
3210
|
+
}
|
|
3211
|
+
catch (err) {
|
|
3212
|
+
console.error(`[Database] Failed to cleanup shard ${filename}:`, err);
|
|
3213
|
+
}
|
|
3214
|
+
finally {
|
|
3215
|
+
if (this.shardWriteLocks.get(filename) === task) {
|
|
3216
|
+
this.shardWriteLocks.delete(filename);
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
// 3. 清除目标会话的索引条目
|
|
3221
|
+
for (const sid of sessionIds) {
|
|
3222
|
+
this.sessionLogIndex.delete(sid);
|
|
3223
|
+
}
|
|
3224
|
+
yield this.saveLogsIndex();
|
|
3225
|
+
yield this.saveSessionLogIndexNow();
|
|
3226
|
+
this.logsCountCache = null;
|
|
3227
|
+
return { logsDeleted };
|
|
3228
|
+
});
|
|
3229
|
+
}
|
|
3051
3230
|
// 新增方法:获取单个 session
|
|
3052
3231
|
getSession(id) {
|
|
3053
3232
|
return this.sessions.find(s => s.id === id) || null;
|
package/dist/server/main.js
CHANGED
|
@@ -219,13 +219,18 @@ const asyncHandler = (handler) => (req, res, next) => {
|
|
|
219
219
|
};
|
|
220
220
|
const VALID_CLAUDE_EFFORT_LEVELS = ['low', 'medium', 'high', 'max'];
|
|
221
221
|
const DEFAULT_CLAUDE_EFFORT_LEVEL = 'medium';
|
|
222
|
+
const VALID_CLAUDE_PERMISSION_DEFAULT_MODES = ['default', 'acceptEdits', 'plan', 'auto', 'dontAsk', 'bypassPermissions'];
|
|
223
|
+
const DEFAULT_CLAUDE_PERMISSION_DEFAULT_MODE = 'default';
|
|
222
224
|
const isClaudeEffortLevel = (value) => {
|
|
223
225
|
return typeof value === 'string' && VALID_CLAUDE_EFFORT_LEVELS.includes(value);
|
|
224
226
|
};
|
|
227
|
+
const isClaudePermissionDefaultMode = (value) => {
|
|
228
|
+
return typeof value === 'string' && VALID_CLAUDE_PERMISSION_DEFAULT_MODES.includes(value);
|
|
229
|
+
};
|
|
225
230
|
const isValidAutocompactPct = (v) => {
|
|
226
231
|
return typeof v === 'number' && Number.isInteger(v) && v >= 1 && v <= 100;
|
|
227
232
|
};
|
|
228
|
-
const writeClaudeConfig = (_dbManager_1, enableAgentTeams_1, enableBypassPermissionsSupport_1, effortLevel_1, defaultModel_1, autocompactPctOverride_1, ...args_1) => __awaiter(void 0, [_dbManager_1, enableAgentTeams_1, enableBypassPermissionsSupport_1, effortLevel_1, defaultModel_1, autocompactPctOverride_1, ...args_1], void 0, function* (_dbManager, enableAgentTeams, enableBypassPermissionsSupport, effortLevel, defaultModel, autocompactPctOverride, options = {}) {
|
|
233
|
+
const writeClaudeConfig = (_dbManager_1, enableAgentTeams_1, enableBypassPermissionsSupport_1, permissionsDefaultMode_1, effortLevel_1, defaultModel_1, autocompactPctOverride_1, ...args_1) => __awaiter(void 0, [_dbManager_1, enableAgentTeams_1, enableBypassPermissionsSupport_1, permissionsDefaultMode_1, effortLevel_1, defaultModel_1, autocompactPctOverride_1, ...args_1], void 0, function* (_dbManager, enableAgentTeams, enableBypassPermissionsSupport, permissionsDefaultMode, effortLevel, defaultModel, autocompactPctOverride, options = {}) {
|
|
229
234
|
var _a;
|
|
230
235
|
try {
|
|
231
236
|
const homeDir = os_1.default.homedir();
|
|
@@ -289,11 +294,17 @@ const writeClaudeConfig = (_dbManager_1, enableAgentTeams_1, enableBypassPermiss
|
|
|
289
294
|
const proxySettings = {
|
|
290
295
|
env: claudeSettingsEnv
|
|
291
296
|
};
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
+
// 解析默认权限模式:bypassPermissions 必须门控开启才生效,否则降级为 default
|
|
298
|
+
let claudeDefaultMode = isClaudePermissionDefaultMode(permissionsDefaultMode)
|
|
299
|
+
? permissionsDefaultMode
|
|
300
|
+
: DEFAULT_CLAUDE_PERMISSION_DEFAULT_MODE;
|
|
301
|
+
if (claudeDefaultMode === 'bypassPermissions' && enableBypassPermissionsSupport !== true) {
|
|
302
|
+
claudeDefaultMode = 'default';
|
|
303
|
+
}
|
|
304
|
+
proxySettings.permissions = {
|
|
305
|
+
defaultMode: claudeDefaultMode
|
|
306
|
+
};
|
|
307
|
+
if (claudeDefaultMode === 'bypassPermissions') {
|
|
297
308
|
proxySettings.skipDangerousModePermissionPrompt = true;
|
|
298
309
|
}
|
|
299
310
|
// 如果设置了 effortLevel,添加对应的配置项
|
|
@@ -665,7 +676,7 @@ const syncConfigsOnServerStartup = (dbManager) => __awaiter(void 0, void 0, void
|
|
|
665
676
|
const claudeEffortLevel = isClaudeEffortLevel(config.claudeEffortLevel)
|
|
666
677
|
? config.claudeEffortLevel
|
|
667
678
|
: DEFAULT_CLAUDE_EFFORT_LEVEL;
|
|
668
|
-
const claudeWritten = yield writeClaudeConfig(dbManager, config.enableAgentTeams, config.enableBypassPermissionsSupport, claudeEffortLevel, config.claudeDefaultModel, config.autocompactPctOverride);
|
|
679
|
+
const claudeWritten = yield writeClaudeConfig(dbManager, config.enableAgentTeams, config.enableBypassPermissionsSupport, config.claudePermissionsDefaultMode, claudeEffortLevel, config.claudeDefaultModel, config.autocompactPctOverride);
|
|
669
680
|
console.log(`[Startup Config Sync] Claude Code config ${claudeWritten ? 'written' : 'skipped'}`);
|
|
670
681
|
const modelReasoningEffort = isCodexReasoningEffort(config.codexModelReasoningEffort)
|
|
671
682
|
? config.codexModelReasoningEffort
|
|
@@ -678,7 +689,7 @@ const syncConfigsOnGlobalConfigUpdate = (dbManager) => __awaiter(void 0, void 0,
|
|
|
678
689
|
const claudeEffortLevel = isClaudeEffortLevel(config.claudeEffortLevel)
|
|
679
690
|
? config.claudeEffortLevel
|
|
680
691
|
: DEFAULT_CLAUDE_EFFORT_LEVEL;
|
|
681
|
-
const claudeUpdated = yield writeClaudeConfig(dbManager, config.enableAgentTeams, config.enableBypassPermissionsSupport, claudeEffortLevel, config.claudeDefaultModel, config.autocompactPctOverride, { allowOverwriteRefresh: true });
|
|
692
|
+
const claudeUpdated = yield writeClaudeConfig(dbManager, config.enableAgentTeams, config.enableBypassPermissionsSupport, config.claudePermissionsDefaultMode, claudeEffortLevel, config.claudeDefaultModel, config.autocompactPctOverride, { allowOverwriteRefresh: true });
|
|
682
693
|
console.log(`[Config Update Sync] Claude Code config ${claudeUpdated ? 'written' : 'skipped'}`);
|
|
683
694
|
const modelReasoningEffort = isCodexReasoningEffort(config.codexModelReasoningEffort)
|
|
684
695
|
? config.codexModelReasoningEffort
|
|
@@ -2003,13 +2014,19 @@ ${instruction}
|
|
|
2003
2014
|
const appConfig = dbManager.getConfig();
|
|
2004
2015
|
const requestedEnableAgentTeams = req.body.enableAgentTeams;
|
|
2005
2016
|
const requestedBypass = req.body.enableBypassPermissionsSupport;
|
|
2017
|
+
const requestedMode = req.body.permissionsDefaultMode;
|
|
2006
2018
|
const enableAgentTeams = typeof requestedEnableAgentTeams === 'boolean'
|
|
2007
2019
|
? requestedEnableAgentTeams
|
|
2008
2020
|
: appConfig.enableAgentTeams;
|
|
2009
2021
|
const enableBypassPermissionsSupport = typeof requestedBypass === 'boolean'
|
|
2010
2022
|
? requestedBypass
|
|
2011
2023
|
: appConfig.enableBypassPermissionsSupport;
|
|
2012
|
-
const
|
|
2024
|
+
const permissionsDefaultMode = isClaudePermissionDefaultMode(requestedMode)
|
|
2025
|
+
? requestedMode
|
|
2026
|
+
: isClaudePermissionDefaultMode(appConfig.claudePermissionsDefaultMode)
|
|
2027
|
+
? appConfig.claudePermissionsDefaultMode
|
|
2028
|
+
: DEFAULT_CLAUDE_PERMISSION_DEFAULT_MODE;
|
|
2029
|
+
const result = yield writeClaudeConfig(dbManager, enableAgentTeams, enableBypassPermissionsSupport, permissionsDefaultMode, undefined, appConfig.claudeDefaultModel, appConfig.autocompactPctOverride);
|
|
2013
2030
|
applyWriteLocalRecords(proxyServer);
|
|
2014
2031
|
res.json(result);
|
|
2015
2032
|
})));
|
|
@@ -2185,6 +2202,19 @@ ${instruction}
|
|
|
2185
2202
|
dbManager.clearSessions();
|
|
2186
2203
|
res.json(true);
|
|
2187
2204
|
})));
|
|
2205
|
+
// 清理过期会话(基于最后请求时间),可选择仅清空关联日志而保留会话本身
|
|
2206
|
+
app.post('/api/sessions/cleanup', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2207
|
+
var _a, _b;
|
|
2208
|
+
const beforeDays = Number((_a = req.body) === null || _a === void 0 ? void 0 : _a.beforeDays);
|
|
2209
|
+
const onlyLogs = Boolean((_b = req.body) === null || _b === void 0 ? void 0 : _b.onlyLogs);
|
|
2210
|
+
if (!Number.isFinite(beforeDays) || !Number.isInteger(beforeDays) || beforeDays < 1 || beforeDays > 15) {
|
|
2211
|
+
res.status(400).json({ error: 'beforeDays 必须为 1-15 之间的整数' });
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
const beforeTimestamp = Date.now() - beforeDays * 24 * 60 * 60 * 1000;
|
|
2215
|
+
const result = yield dbManager.cleanupSessionsByAge(beforeTimestamp, { onlyLogs });
|
|
2216
|
+
res.json(result);
|
|
2217
|
+
})));
|
|
2188
2218
|
// ─── Session Route Binding API ───
|
|
2189
2219
|
app.put('/api/sessions/:id/bind-route', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2190
2220
|
const sessionId = req.params.id;
|
|
@@ -3248,10 +3248,10 @@ class ProxyServer {
|
|
|
3248
3248
|
* @param targetModel 目标模型名称(可选)
|
|
3249
3249
|
* @returns 转换后往服务商API接口的数据
|
|
3250
3250
|
*/
|
|
3251
|
-
transformRequestToUpstream(tool, source, payloadData, targetModel, providerConfig, serverToolConfig) {
|
|
3251
|
+
transformRequestToUpstream(tool, source, payloadData, targetModel, providerConfig, serverToolConfig, sanitizeBody) {
|
|
3252
3252
|
const clientFormat = tool === 'codex' ? 'responses' : 'claude';
|
|
3253
3253
|
const upstreamFormat = (0, index_1.sourceTypeToFormat)(source);
|
|
3254
|
-
const result = (0, index_1.transformRequest)({ fromFormat: clientFormat, toFormat: upstreamFormat, body: payloadData, providerConfig, serverToolConfig });
|
|
3254
|
+
const result = (0, index_1.transformRequest)({ fromFormat: clientFormat, toFormat: upstreamFormat, body: payloadData, providerConfig, serverToolConfig, sanitizeBody });
|
|
3255
3255
|
const body = result.body;
|
|
3256
3256
|
// 模型覆盖:OpenAI 模型族保持原样,其余覆盖为 targetModel
|
|
3257
3257
|
if (targetModel) {
|
|
@@ -3878,7 +3878,9 @@ class ProxyServer {
|
|
|
3878
3878
|
const effectiveModel = rule.targetModel || (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model);
|
|
3879
3879
|
const providerConfig = (0, index_1.getReasoningConfig)(service.name || '', effectiveApiUrl || '', effectiveModel || '');
|
|
3880
3880
|
const serverToolConfig = (0, index_1.getServerToolSupport)(service.name || '', effectiveApiUrl || '');
|
|
3881
|
-
|
|
3881
|
+
// responses→responses 直连非 OpenAI 官方端点时,需降级兼容(剥离 custom/namespace 等私有工具与非标准字段)
|
|
3882
|
+
const sanitizeBody = clientFormat === 'responses' && (0, index_1.sourceTypeToFormat)(sourceType) === 'responses' && !(0, index_1.isOfficialOpenAiApi)(effectiveApiUrl || '');
|
|
3883
|
+
const transformedRequestBody = this.transformRequestToUpstream(targetType, sourceType, payloadForTransform, rule.targetModel, providerConfig, serverToolConfig, sanitizeBody);
|
|
3882
3884
|
requestBody = (_b = transformedRequestBody !== null && transformedRequestBody !== void 0 ? transformedRequestBody : this.cloneRequestBody(originalToolRequestBody)) !== null && _b !== void 0 ? _b : {};
|
|
3883
3885
|
// 对最终即将发送到上游的 Claude compact 请求再做一次兜底清理,
|
|
3884
3886
|
// 避免中间转换/覆盖步骤重新引入未配对的 tool_use。
|
|
@@ -4737,7 +4739,9 @@ class ProxyServer {
|
|
|
4737
4739
|
const effectiveModel = rule.targetModel || (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model);
|
|
4738
4740
|
const providerConfig = (0, index_1.getReasoningConfig)(service.name || '', effectiveApiUrl || '', effectiveModel || '');
|
|
4739
4741
|
const serverToolConfig = (0, index_1.getServerToolSupport)(service.name || '', effectiveApiUrl || '');
|
|
4740
|
-
|
|
4742
|
+
// responses→responses 直连非 OpenAI 官方端点时,需降级兼容(剥离 custom/namespace 等私有工具与非标准字段)
|
|
4743
|
+
const sanitizeBody = clientFormat === 'responses' && (0, index_1.sourceTypeToFormat)(sourceType) === 'responses' && !(0, index_1.isOfficialOpenAiApi)(effectiveApiUrl || '');
|
|
4744
|
+
const transformedRequestBody = this.transformRequestByFormat(clientFormat, sourceType, payloadForTransform, rule.targetModel, providerConfig, serverToolConfig, sanitizeBody);
|
|
4741
4745
|
requestBody = (_a = transformedRequestBody !== null && transformedRequestBody !== void 0 ? transformedRequestBody : this.cloneRequestBody(requestBody)) !== null && _a !== void 0 ? _a : {};
|
|
4742
4746
|
// Compact final sanitize
|
|
4743
4747
|
if (rule.contentType === 'compact' && clientFormat === 'claude' && Array.isArray(requestBody === null || requestBody === void 0 ? void 0 : requestBody.messages)) {
|
|
@@ -4992,9 +4996,9 @@ class ProxyServer {
|
|
|
4992
4996
|
/**
|
|
4993
4997
|
* 使用显式 clientFormat 进行请求转换(取代 tool → format 的硬编码映射)
|
|
4994
4998
|
*/
|
|
4995
|
-
transformRequestByFormat(clientFormat, source, payloadData, targetModel, providerConfig, serverToolConfig) {
|
|
4999
|
+
transformRequestByFormat(clientFormat, source, payloadData, targetModel, providerConfig, serverToolConfig, sanitizeBody) {
|
|
4996
5000
|
const upstreamFormat = (0, index_1.sourceTypeToFormat)(source);
|
|
4997
|
-
const result = (0, index_1.transformRequest)({ fromFormat: clientFormat, toFormat: upstreamFormat, body: payloadData, providerConfig, serverToolConfig });
|
|
5001
|
+
const result = (0, index_1.transformRequest)({ fromFormat: clientFormat, toFormat: upstreamFormat, body: payloadData, providerConfig, serverToolConfig, sanitizeBody });
|
|
4998
5002
|
const body = result.body;
|
|
4999
5003
|
if (targetModel) {
|
|
5000
5004
|
const isOpenAIModel = /^gpt-|o[123]/i.test(targetModel);
|