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 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
- current[key] = {};
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
- current[key] = {};
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 非标准 tool 类型集合。
12
- * OpenAI 私有扩展(custom、tool_searchweb_search 等),
13
- * 其他 Responses API 提供商不支持这些类型,直接转发会导致 400 错误。
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. 过滤掉非标准 tool 类型,仅保留 function 类型
69
+ // 1. 仅保留 function 类型工具,剥离 custom/namespace/tool_search/web_search 等私有类型
71
70
  if (Array.isArray(body.tools)) {
72
- const filteredTools = body.tools.filter((t) => !NON_STANDARD_TOOL_TYPES.has(t.type));
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;
@@ -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
- // 如果开启对bypassPermissions的支持,添加对应的配置项
293
- if (enableBypassPermissionsSupport) {
294
- proxySettings.permissions = {
295
- defaultMode: "bypassPermissions"
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 result = yield writeClaudeConfig(dbManager, enableAgentTeams, enableBypassPermissionsSupport, undefined, appConfig.claudeDefaultModel, appConfig.autocompactPctOverride);
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
- const transformedRequestBody = this.transformRequestToUpstream(targetType, sourceType, payloadForTransform, rule.targetModel, providerConfig, serverToolConfig);
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
- const transformedRequestBody = this.transformRequestByFormat(clientFormat, sourceType, payloadForTransform, rule.targetModel, providerConfig, serverToolConfig);
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);