aicodeswitch 5.1.1 → 5.1.3

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.
@@ -33,6 +33,8 @@ const config_metadata_1 = require("./config-metadata");
33
33
  const config_merge_1 = require("./config-merge");
34
34
  const config_managed_fields_1 = require("./config-managed-fields");
35
35
  const config_1 = require("./config");
36
+ const session_migration_1 = require("./session-migration");
37
+ const session_launcher_1 = require("./session-launcher");
36
38
  const appDir = path_1.default.join(os_1.default.homedir(), '.aicodeswitch');
37
39
  const legacyDataDir = path_1.default.join(appDir, 'data');
38
40
  const dataDir = path_1.default.join(appDir, 'fs-db');
@@ -922,7 +924,7 @@ const listInstalledSkills = () => {
922
924
  });
923
925
  return Array.from(result.values()).sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
924
926
  };
925
- const registerRoutes = (dbManager, proxyServer) => {
927
+ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, void 0, function* () {
926
928
  updateProxyConfig(dbManager.getConfig());
927
929
  app.get('/health', (_req, res) => res.json({ status: 'ok' }));
928
930
  // 鉴权相关路由 - 公开访问
@@ -1770,6 +1772,150 @@ ${instruction}
1770
1772
  dbManager.clearSessions();
1771
1773
  res.json(true);
1772
1774
  })));
1775
+ // ─── Session Route Binding API ───
1776
+ app.put('/api/sessions/:id/bind-route', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
1777
+ const sessionId = req.params.id;
1778
+ const { routeId } = req.body || {};
1779
+ if (!routeId) {
1780
+ res.status(400).json({ success: false, error: 'routeId is required' });
1781
+ return;
1782
+ }
1783
+ const session = dbManager.getSession(sessionId);
1784
+ if (!session) {
1785
+ res.status(404).json({ success: false, error: 'Session not found' });
1786
+ return;
1787
+ }
1788
+ const updatedSession = yield dbManager.bindSessionRoute(sessionId, routeId);
1789
+ if (!updatedSession) {
1790
+ res.status(400).json({ success: false, error: 'Route not found' });
1791
+ return;
1792
+ }
1793
+ res.json({ success: true, session: updatedSession });
1794
+ })));
1795
+ app.delete('/api/sessions/:id/bind-route', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
1796
+ const sessionId = req.params.id;
1797
+ const session = dbManager.getSession(sessionId);
1798
+ if (!session) {
1799
+ res.status(404).json({ success: false, error: 'Session not found' });
1800
+ return;
1801
+ }
1802
+ const result = yield dbManager.unbindSessionRoute(sessionId);
1803
+ res.json({ success: result });
1804
+ })));
1805
+ app.get('/api/routes/:id/bound-sessions', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
1806
+ const routeId = req.params.id;
1807
+ const route = dbManager.getRoute(routeId);
1808
+ if (!route) {
1809
+ res.status(404).json({ error: 'Route not found' });
1810
+ return;
1811
+ }
1812
+ const sessions = dbManager.getBoundSessions(routeId);
1813
+ res.json({
1814
+ routeId,
1815
+ sessions: sessions.map(s => ({
1816
+ id: s.id,
1817
+ title: s.title,
1818
+ targetType: s.targetType,
1819
+ requestCount: s.requestCount,
1820
+ totalTokens: s.totalTokens,
1821
+ lastRequestAt: s.lastRequestAt,
1822
+ })),
1823
+ });
1824
+ })));
1825
+ // ─── Session Migration API ───
1826
+ app.post('/api/sessions/:id/migration-preview', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
1827
+ var _a;
1828
+ const sessionId = req.params.id;
1829
+ const { targetTool, includeThinking, includeToolCalls, maxRounds } = req.body || {};
1830
+ if (!targetTool || !['claude-code', 'codex'].includes(targetTool)) {
1831
+ res.status(400).json({ error: 'Invalid targetTool. Must be "claude-code" or "codex".' });
1832
+ return;
1833
+ }
1834
+ try {
1835
+ const content = yield (0, session_migration_1.extractSessionContent)(dbManager, sessionId, {
1836
+ sourceSessionId: sessionId,
1837
+ targetTool,
1838
+ includeThinking: includeThinking === true,
1839
+ includeToolCalls: includeToolCalls !== false,
1840
+ maxRounds: typeof maxRounds === 'number' ? maxRounds : 0,
1841
+ });
1842
+ const preview = (0, session_migration_1.previewMigration)(content, targetTool);
1843
+ res.json(preview);
1844
+ }
1845
+ catch (err) {
1846
+ if ((_a = err.message) === null || _a === void 0 ? void 0 : _a.includes('not found')) {
1847
+ res.status(404).json({ error: err.message });
1848
+ }
1849
+ else {
1850
+ res.status(500).json({ error: err.message || 'Migration preview failed' });
1851
+ }
1852
+ }
1853
+ })));
1854
+ app.post('/api/sessions/:id/migrate', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
1855
+ var _a;
1856
+ const sessionId = req.params.id;
1857
+ const { targetTool, includeThinking, includeToolCalls, maxRounds, editedPrompt } = req.body || {};
1858
+ if (!targetTool || !['claude-code', 'codex'].includes(targetTool)) {
1859
+ res.status(400).json({ error: 'Invalid targetTool. Must be "claude-code" or "codex".' });
1860
+ return;
1861
+ }
1862
+ try {
1863
+ const content = yield (0, session_migration_1.extractSessionContent)(dbManager, sessionId, {
1864
+ sourceSessionId: sessionId,
1865
+ targetTool,
1866
+ includeThinking: includeThinking === true,
1867
+ includeToolCalls: includeToolCalls !== false,
1868
+ maxRounds: typeof maxRounds === 'number' ? maxRounds : 0,
1869
+ });
1870
+ const result = (0, session_migration_1.migrateSession)(content, targetTool, editedPrompt);
1871
+ res.json(result);
1872
+ }
1873
+ catch (err) {
1874
+ if ((_a = err.message) === null || _a === void 0 ? void 0 : _a.includes('not found')) {
1875
+ res.status(404).json({ error: err.message });
1876
+ }
1877
+ else {
1878
+ res.status(500).json({ error: err.message || 'Migration failed' });
1879
+ }
1880
+ }
1881
+ })));
1882
+ app.post('/api/sessions/:id/migrate-launch', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
1883
+ var _a;
1884
+ const sessionId = req.params.id;
1885
+ const { targetTool, includeThinking, includeToolCalls, maxRounds } = req.body || {};
1886
+ if (!targetTool || !['claude-code', 'codex'].includes(targetTool)) {
1887
+ res.status(400).json({ error: 'Invalid targetTool. Must be "claude-code" or "codex".' });
1888
+ return;
1889
+ }
1890
+ try {
1891
+ // First extract content and generate prompt
1892
+ const content = yield (0, session_migration_1.extractSessionContent)(dbManager, sessionId, {
1893
+ sourceSessionId: sessionId,
1894
+ targetTool,
1895
+ includeThinking: includeThinking === true,
1896
+ includeToolCalls: includeToolCalls !== false,
1897
+ maxRounds: typeof maxRounds === 'number' ? maxRounds : 0,
1898
+ });
1899
+ const { prompt } = (0, session_migration_1.migrateSession)(content, targetTool);
1900
+ // Resolve the project directory from session metadata
1901
+ const projectDir = (0, session_launcher_1.resolveProjectDir)(sessionId, content.sourceTool);
1902
+ // Write prompt to temp file
1903
+ const tempFilePath = (0, session_launcher_1.writePromptToTempFile)(prompt, sessionId);
1904
+ // Try to launch the target tool with the resolved project directory
1905
+ const result = yield (0, session_launcher_1.launchTargetWithFallback)(targetTool, tempFilePath, prompt, projectDir || undefined);
1906
+ // Schedule cleanup
1907
+ (0, session_launcher_1.cleanupTempFile)(tempFilePath);
1908
+ res.json(result);
1909
+ }
1910
+ catch (err) {
1911
+ if ((_a = err.message) === null || _a === void 0 ? void 0 : _a.includes('not found')) {
1912
+ res.status(404).json({ error: err.message });
1913
+ }
1914
+ else {
1915
+ res.status(500).json({ error: err.message || 'Launch migration failed' });
1916
+ }
1917
+ }
1918
+ })));
1773
1919
  app.get('/api/docs/recommend-vendors', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
1774
1920
  const resp = yield fetch('https://unpkg.com/aicodeswitch/docs/vendors-recommand.md');
1775
1921
  if (!resp.ok) {
@@ -1885,7 +2031,22 @@ ${instruction}
1885
2031
  res.json(result);
1886
2032
  })));
1887
2033
  app.put('/api/mcps/:id', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
1888
- const result = yield dbManager.updateMCP(req.params.id, req.body);
2034
+ const updateData = req.body;
2035
+ const oldMcp = dbManager.getMCP(req.params.id);
2036
+ const result = yield dbManager.updateMCP(req.params.id, updateData);
2037
+ // 如果targets发生变化,同步MCP配置到对应工具
2038
+ if (updateData.targets !== undefined) {
2039
+ const newTargets = updateData.targets;
2040
+ const oldTargets = (oldMcp === null || oldMcp === void 0 ? void 0 : oldMcp.targets) || [];
2041
+ // 需要同步的所有target(新增的 + 移除的都需要处理)
2042
+ const allAffectedTargets = new Set([...newTargets, ...oldTargets]);
2043
+ for (const target of allAffectedTargets) {
2044
+ const activeRouteId = dbManager.getActiveRouteIdForTool(target);
2045
+ if (activeRouteId) {
2046
+ yield writeMCPConfig(target);
2047
+ }
2048
+ }
2049
+ }
1889
2050
  res.json(result);
1890
2051
  })));
1891
2052
  app.delete('/api/mcps/:id', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
@@ -1947,10 +2108,83 @@ ${instruction}
1947
2108
  return true;
1948
2109
  }
1949
2110
  else if (targetType === 'codex') {
1950
- // Codex使用TOML格式,我们暂时不直接写入
1951
- // 需要后续处理Codex的MCP配置格式
1952
- // TODO: 实现 Codex MCP 配置写入
1953
- console.log('[MCP] Codex MCP配置写入暂未实现');
2111
+ // Codex使用TOML格式的 config.toml,MCP配置格式为 [mcp_servers.<name>]
2112
+ const codexDir = path_1.default.join(homeDir, '.codex');
2113
+ const codexConfigPath = path_1.default.join(codexDir, 'config.toml');
2114
+ if (!fs_1.default.existsSync(codexDir)) {
2115
+ fs_1.default.mkdirSync(codexDir, { recursive: true });
2116
+ }
2117
+ // 读取当前 config.toml
2118
+ let currentConfig = {};
2119
+ if (fs_1.default.existsSync(codexConfigPath)) {
2120
+ try {
2121
+ currentConfig = (0, config_merge_1.parseToml)(fs_1.default.readFileSync(codexConfigPath, 'utf-8'));
2122
+ }
2123
+ catch (error) {
2124
+ console.warn('[MCP] Failed to parse Codex config.toml:', error);
2125
+ }
2126
+ }
2127
+ // 清除已有的代理写入的 mcp_servers 条目(通过metadata追踪)
2128
+ const mcpMetaPath = path_1.default.join(codexDir, '.aicodeswitch_mcp_servers.json');
2129
+ let previousMcpIds = [];
2130
+ if (fs_1.default.existsSync(mcpMetaPath)) {
2131
+ try {
2132
+ previousMcpIds = JSON.parse(fs_1.default.readFileSync(mcpMetaPath, 'utf8'));
2133
+ for (const id of previousMcpIds) {
2134
+ if (currentConfig.mcp_servers && currentConfig.mcp_servers[id]) {
2135
+ delete currentConfig.mcp_servers[id];
2136
+ }
2137
+ }
2138
+ }
2139
+ catch (_a) {
2140
+ // ignore
2141
+ }
2142
+ }
2143
+ // 确保mcp_servers对象存在
2144
+ if (!currentConfig.mcp_servers) {
2145
+ currentConfig.mcp_servers = {};
2146
+ }
2147
+ // 写入所有启用的MCP
2148
+ const writtenMcpIds = [];
2149
+ for (const mcp of mcps) {
2150
+ const mcpConfig = {};
2151
+ if (mcp.type === 'stdio') {
2152
+ mcpConfig.command = mcp.command || '';
2153
+ if (mcp.args && mcp.args.length > 0) {
2154
+ mcpConfig.args = mcp.args;
2155
+ }
2156
+ // stdio 类型的环境变量写在 [mcp_servers.name.env] 子表中
2157
+ if (mcp.env && Object.keys(mcp.env).length > 0) {
2158
+ mcpConfig.env = Object.assign({}, mcp.env);
2159
+ }
2160
+ }
2161
+ else if (mcp.type === 'http') {
2162
+ // Codex 使用 Streamable HTTP 传输,url 字段
2163
+ mcpConfig.url = mcp.url || '';
2164
+ // HTTP 类型可选的 headers
2165
+ if (mcp.headers && Object.keys(mcp.headers).length > 0) {
2166
+ mcpConfig.headers = Object.assign({}, mcp.headers);
2167
+ }
2168
+ }
2169
+ else if (mcp.type === 'sse') {
2170
+ // SSE 传输也使用 url 字段
2171
+ mcpConfig.url = mcp.url || '';
2172
+ if (mcp.headers && Object.keys(mcp.headers).length > 0) {
2173
+ mcpConfig.headers = Object.assign({}, mcp.headers);
2174
+ }
2175
+ }
2176
+ currentConfig.mcp_servers[mcp.id] = mcpConfig;
2177
+ writtenMcpIds.push(mcp.id);
2178
+ }
2179
+ // 如果mcp_servers为空对象,删除该键
2180
+ if (Object.keys(currentConfig.mcp_servers).length === 0) {
2181
+ delete currentConfig.mcp_servers;
2182
+ }
2183
+ // 写回 config.toml
2184
+ (0, config_merge_1.atomicWriteFile)(codexConfigPath, (0, config_merge_1.stringifyToml)(currentConfig));
2185
+ // 保存已写入的MCP ID列表,用于后续清理
2186
+ fs_1.default.writeFileSync(mcpMetaPath, JSON.stringify(writtenMcpIds, null, 2));
2187
+ console.log(`[MCP] Codex MCP config written: ${writtenMcpIds.length} server(s)`);
1954
2188
  return true;
1955
2189
  }
1956
2190
  return false;
@@ -1976,6 +2210,45 @@ ${instruction}
1976
2210
  }
1977
2211
  return true;
1978
2212
  }
2213
+ else if (targetType === 'codex') {
2214
+ // 从 Codex config.toml 中移除指定的 MCP 条目
2215
+ const homeDir = os_1.default.homedir();
2216
+ const codexDir = path_1.default.join(homeDir, '.codex');
2217
+ const codexConfigPath = path_1.default.join(codexDir, 'config.toml');
2218
+ if (!fs_1.default.existsSync(codexConfigPath)) {
2219
+ return true;
2220
+ }
2221
+ let currentConfig = {};
2222
+ try {
2223
+ currentConfig = (0, config_merge_1.parseToml)(fs_1.default.readFileSync(codexConfigPath, 'utf-8'));
2224
+ }
2225
+ catch (error) {
2226
+ console.warn('[MCP] Failed to parse Codex config.toml for removal:', error);
2227
+ return false;
2228
+ }
2229
+ if (currentConfig.mcp_servers && currentConfig.mcp_servers[mcpId]) {
2230
+ delete currentConfig.mcp_servers[mcpId];
2231
+ // 如果mcp_servers为空对象,删除该键
2232
+ if (Object.keys(currentConfig.mcp_servers).length === 0) {
2233
+ delete currentConfig.mcp_servers;
2234
+ }
2235
+ (0, config_merge_1.atomicWriteFile)(codexConfigPath, (0, config_merge_1.stringifyToml)(currentConfig));
2236
+ // 更新metadata
2237
+ const mcpMetaPath = path_1.default.join(codexDir, '.aicodeswitch_mcp_servers.json');
2238
+ if (fs_1.default.existsSync(mcpMetaPath)) {
2239
+ try {
2240
+ const previousIds = JSON.parse(fs_1.default.readFileSync(mcpMetaPath, 'utf8'));
2241
+ const updatedIds = previousIds.filter(id => id !== mcpId);
2242
+ fs_1.default.writeFileSync(mcpMetaPath, JSON.stringify(updatedIds, null, 2));
2243
+ }
2244
+ catch (_a) {
2245
+ // ignore
2246
+ }
2247
+ }
2248
+ console.log(`[MCP] Removed MCP ${mcpId} from Codex config`);
2249
+ }
2250
+ return true;
2251
+ }
1979
2252
  return false;
1980
2253
  }
1981
2254
  catch (error) {
@@ -1983,7 +2256,29 @@ ${instruction}
1983
2256
  return false;
1984
2257
  }
1985
2258
  });
1986
- };
2259
+ // 服务启动时同步MCP配置到已激活的工具
2260
+ const allMcps = dbManager.getMCPs();
2261
+ const targetsToSync = new Set();
2262
+ for (const mcp of allMcps) {
2263
+ if (mcp.targets) {
2264
+ for (const target of mcp.targets) {
2265
+ targetsToSync.add(target);
2266
+ }
2267
+ }
2268
+ }
2269
+ for (const target of targetsToSync) {
2270
+ const activeRouteId = dbManager.getActiveRouteIdForTool(target);
2271
+ if (activeRouteId) {
2272
+ try {
2273
+ yield writeMCPConfig(target);
2274
+ console.log(`[Startup MCP Sync] MCP config synced for ${target}`);
2275
+ }
2276
+ catch (error) {
2277
+ console.error(`[Startup MCP Sync] Failed to sync MCP config for ${target}:`, error);
2278
+ }
2279
+ }
2280
+ }
2281
+ });
1987
2282
  const start = () => __awaiter(void 0, void 0, void 0, function* () {
1988
2283
  fs_1.default.mkdirSync(dataDir, { recursive: true });
1989
2284
  // 自动检测数据库类型并执行迁移(如果需要)
@@ -1999,11 +2294,16 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
1999
2294
  catch (error) {
2000
2295
  console.error('[Server] Tool config sync failed:', error);
2001
2296
  }
2297
+ // 清理旧的迁移临时文件
2298
+ try {
2299
+ (0, session_launcher_1.cleanupOldTempFiles)();
2300
+ }
2301
+ catch ( /* ignore */_a) { /* ignore */ }
2002
2302
  const proxyServer = new proxy_server_1.ProxyServer(dbManager, app);
2003
2303
  // Initialize proxy server and register proxy routes last
2004
2304
  proxyServer.initialize();
2005
2305
  // Register admin routes first
2006
- registerRoutes(dbManager, proxyServer);
2306
+ yield registerRoutes(dbManager, proxyServer);
2007
2307
  yield proxyServer.registerProxyRoutes();
2008
2308
  app.use(express_1.default.static(path_1.default.resolve(__dirname, '../ui')));
2009
2309
  // 404 处理程序 - 确保返回 JSON 而不是 HTML(放在所有路由和静态文件之后)
@@ -60,6 +60,7 @@ const type_migration_1 = require("./type-migration");
60
60
  const original_config_reader_1 = require("./original-config-reader");
61
61
  const compact_1 = require("./conversions/compact");
62
62
  const coding_plan_1 = require("./coding-plan");
63
+ const coding_plan_headers_1 = require("./coding-plan-headers");
63
64
  const SUPPORTED_TARGETS = ['claude-code', 'codex'];
64
65
  /** 默认模型列表 */
65
66
  const DEFAULT_MODELS = [
@@ -314,14 +315,30 @@ class ProxyServer {
314
315
  res.status(404).json({ error: { message: `API path ${apiPath} is not bound to any route. Please configure it in Route Mapping settings.` } });
315
316
  return;
316
317
  }
317
- // 加载绑定的路由
318
+ // 推断客户端格式
319
+ const clientFormat = apiPathToClientFormat(apiPath);
320
+ // 加载绑定的路由(默认从 API 路径绑定获取)
318
321
  const allRoutes = this.dbManager.getRoutes();
319
- const route = allRoutes.find((r) => r.id === binding.routeId);
322
+ let route = allRoutes.find((r) => r.id === binding.routeId);
323
+ // 会话级路由覆盖:优先检查会话是否绑定了特定路由
324
+ const sessionId = this.extractSessionIdForFormat(req, clientFormat);
325
+ if (sessionId) {
326
+ const session = this.dbManager.getSession(sessionId);
327
+ if (session === null || session === void 0 ? void 0 : session.routeId) {
328
+ const boundRoute = allRoutes.find((r) => r.id === session.routeId);
329
+ if (boundRoute) {
330
+ console.log(`[SESSION-ROUTE] API path ${apiPath} session ${sessionId} using bound route: ${boundRoute.name}`);
331
+ route = boundRoute;
332
+ }
333
+ else {
334
+ console.log(`[SESSION-ROUTE] Bound route ${session.routeId} not found for session ${sessionId}, clearing binding`);
335
+ this.dbManager.unbindSessionRoute(sessionId).catch(console.error);
336
+ }
337
+ }
338
+ }
320
339
  if (!route) {
321
340
  return res.status(404).json({ error: { message: `Bound route '${binding.routeId}' not found or inactive. Please check Route Mapping settings.` } });
322
341
  }
323
- // 推断客户端格式
324
- const clientFormat = apiPathToClientFormat(apiPath);
325
342
  // 复用完整的代理请求处理
326
343
  yield this.handleApiPathProxyRequest(req, res, route, clientFormat, apiPath);
327
344
  }));
@@ -872,6 +889,24 @@ class ProxyServer {
872
889
  }
873
890
  if (!tool)
874
891
  return undefined;
892
+ // 优先检查会话级路由绑定
893
+ const sessionId = this.defaultExtractSessionId(req, tool);
894
+ if (sessionId) {
895
+ const session = this.dbManager.getSession(sessionId);
896
+ if (session === null || session === void 0 ? void 0 : session.routeId) {
897
+ const boundRoute = this.dbManager.getRoute(session.routeId);
898
+ if (boundRoute) {
899
+ console.log(`[SESSION-ROUTE] Session ${sessionId} using bound route: ${boundRoute.name} (${boundRoute.id})`);
900
+ return boundRoute;
901
+ }
902
+ else {
903
+ // 路由已被删除,自动清除绑定
904
+ console.log(`[SESSION-ROUTE] Bound route ${session.routeId} not found for session ${sessionId}, clearing binding`);
905
+ this.dbManager.unbindSessionRoute(sessionId).catch(console.error);
906
+ }
907
+ }
908
+ }
909
+ // 回退到全局工具绑定
875
910
  const routeId = this.dbManager.getActiveRouteIdForTool(tool);
876
911
  if (!routeId)
877
912
  return undefined;
@@ -1040,7 +1075,9 @@ class ProxyServer {
1040
1075
  const isBlacklisted = yield this.dbManager.isServiceBlacklisted(service.id, routeId, rule.contentType);
1041
1076
  if (isBlacklisted)
1042
1077
  continue;
1043
- return service.name;
1078
+ const vendors = this.dbManager.getVendors();
1079
+ const vendor = vendors.find(v => v.id === service.vendorId);
1080
+ return vendor ? `${vendor.name}-${service.name}` : service.name;
1044
1081
  }
1045
1082
  return undefined;
1046
1083
  });
@@ -1049,7 +1086,7 @@ class ProxyServer {
1049
1086
  if (!forwardedToServiceName) {
1050
1087
  return '';
1051
1088
  }
1052
- return `;已自动转发给 ${forwardedToServiceName} 服务继续处理`;
1089
+ return `;已自动转发给「${forwardedToServiceName}」服务继续处理`;
1053
1090
  }
1054
1091
  /**
1055
1092
  * 解析规则的有效超时时间(毫秒)。
@@ -2427,6 +2464,10 @@ class ProxyServer {
2427
2464
  const bodyStr = JSON.stringify(requestBody);
2428
2465
  headers['content-length'] = Buffer.byteLength(bodyStr, 'utf8').toString();
2429
2466
  }
2467
+ // 编程套餐 Headers 覆盖:当服务启用了编程套餐时,替换为编程工具的标准 Headers
2468
+ if (service.enableCodingPlan) {
2469
+ (0, coding_plan_headers_1.applyCodingPlanHeaders)(headers, sourceType);
2470
+ }
2430
2471
  return headers;
2431
2472
  }
2432
2473
  resolveEffectiveApiKey(service) {
@@ -2599,6 +2640,23 @@ class ProxyServer {
2599
2640
  }
2600
2641
  return rawUserId;
2601
2642
  }
2643
+ /**
2644
+ * 根据客户端格式提取 session ID(用于标准 API 路径的会话级路由覆盖)
2645
+ */
2646
+ extractSessionIdForFormat(request, format) {
2647
+ var _a, _b;
2648
+ if (format === 'claude') {
2649
+ const rawUserId = (_b = (_a = request.body) === null || _a === void 0 ? void 0 : _a.metadata) === null || _b === void 0 ? void 0 : _b.user_id;
2650
+ return ProxyServer.extractSessionIdFromUserId(rawUserId);
2651
+ }
2652
+ // 对于 completions/responses/gemini 格式,尝试从 headers 中提取
2653
+ const sessionId = request.headers['session-id'] || request.headers['session_id'];
2654
+ if (typeof sessionId === 'string')
2655
+ return sessionId;
2656
+ if (Array.isArray(sessionId))
2657
+ return sessionId[0] || null;
2658
+ return null;
2659
+ }
2602
2660
  /**
2603
2661
  * 提取会话标题(默认方法)
2604
2662
  * 对于新会话,尝试从第一条消息的内容中提取标题
@@ -2685,6 +2743,7 @@ class ProxyServer {
2685
2743
  formatSessionTitle(text) {
2686
2744
  // 去除多余空白和换行符,替换为单个空格
2687
2745
  let formatted = text
2746
+ .replace(/<\/?session>/g, '') // 移除 <session></session> 标签
2688
2747
  .replace(/\s+/g, ' ') // 多个空白字符替换为单个空格
2689
2748
  .replace(/[\r\n]+/g, ' ') // 换行符替换为空格
2690
2749
  .trim();
@@ -2761,10 +2820,10 @@ class ProxyServer {
2761
2820
  * @param targetModel 目标模型名称(可选)
2762
2821
  * @returns 转换后往服务商API接口的数据
2763
2822
  */
2764
- transformRequestToUpstream(tool, source, payloadData, targetModel, providerConfig) {
2823
+ transformRequestToUpstream(tool, source, payloadData, targetModel, providerConfig, serverToolConfig) {
2765
2824
  const clientFormat = tool === 'codex' ? 'responses' : 'claude';
2766
2825
  const upstreamFormat = (0, index_1.sourceTypeToFormat)(source);
2767
- const result = (0, index_1.transformRequest)({ fromFormat: clientFormat, toFormat: upstreamFormat, body: payloadData, providerConfig });
2826
+ const result = (0, index_1.transformRequest)({ fromFormat: clientFormat, toFormat: upstreamFormat, body: payloadData, providerConfig, serverToolConfig });
2768
2827
  const body = result.body;
2769
2828
  // 模型覆盖:OpenAI 模型族保持原样,其余覆盖为 targetModel
2770
2829
  if (targetModel) {
@@ -2898,6 +2957,12 @@ class ProxyServer {
2898
2957
  const useOriginalConfig = (options === null || options === void 0 ? void 0 : options.useOriginalConfig) === true;
2899
2958
  let relayedForLog = !useOriginalConfig;
2900
2959
  let originalToolRequestBody = this.cloneRequestBody(req.body || {});
2960
+ // 请求体安全性清理:修复控制字符、无效 JSON arguments、undefined 值等问题
2961
+ const sanitizeResult = (0, index_1.sanitizeRequestBody)(originalToolRequestBody);
2962
+ if (sanitizeResult.changes.length > 0) {
2963
+ console.log(`[Body-Sanitize] ${sanitizeResult.changes.length} fix(es): ${sanitizeResult.changes.join('; ')}`);
2964
+ }
2965
+ originalToolRequestBody = sanitizeResult.body;
2901
2966
  let requestBody = this.cloneRequestBody(originalToolRequestBody) || {};
2902
2967
  let usageForLog;
2903
2968
  let logged = false;
@@ -3286,7 +3351,8 @@ class ProxyServer {
3286
3351
  const effectiveApiUrl = this.resolveEffectiveApiUrl(service);
3287
3352
  const effectiveModel = rule.targetModel || (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model);
3288
3353
  const providerConfig = (0, index_1.getReasoningConfig)(service.name || '', effectiveApiUrl || '', effectiveModel || '');
3289
- const transformedRequestBody = this.transformRequestToUpstream(targetType, sourceType, payloadForTransform, rule.targetModel, providerConfig);
3354
+ const serverToolConfig = (0, index_1.getServerToolSupport)(service.name || '', effectiveApiUrl || '');
3355
+ const transformedRequestBody = this.transformRequestToUpstream(targetType, sourceType, payloadForTransform, rule.targetModel, providerConfig, serverToolConfig);
3290
3356
  requestBody = (_b = transformedRequestBody !== null && transformedRequestBody !== void 0 ? transformedRequestBody : this.cloneRequestBody(originalToolRequestBody)) !== null && _b !== void 0 ? _b : {};
3291
3357
  // 对最终即将发送到上游的 Claude compact 请求再做一次兜底清理,
3292
3358
  // 避免中间转换/覆盖步骤重新引入未配对的 tool_use。
@@ -3405,7 +3471,9 @@ class ProxyServer {
3405
3471
  const parser = new streaming_1.SSEParserTransform();
3406
3472
  const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
3407
3473
  const serializer = new streaming_1.SSESerializerTransform();
3408
- const downstreamChunkCollector = new chunk_collector_1.ChunkCollectorTransform();
3474
+ const downstreamChunkCollector = new chunk_collector_1.ChunkCollectorTransform(() => {
3475
+ rules_status_service_1.rulesStatusBroadcaster.refreshRuleInUse(route.id, rule.id);
3476
+ });
3409
3477
  const compactResponseSanitizer = rule.contentType === 'compact' && targetType === 'claude-code'
3410
3478
  ? new ClaudeCompactResponseSanitizer()
3411
3479
  : null;
@@ -3921,6 +3989,12 @@ class ProxyServer {
3921
3989
  console.log(`\x1b[32m[ApiPathProxy]\x1b[0m path=${apiPath}, clientFormat=${clientFormat}, session=-, rule=${rule.id}(${rule.contentType}), vendor=${(vendor === null || vendor === void 0 ? void 0 : vendor.name) || '-'}, service=${service.name}`);
3922
3990
  const failoverEnabled = (options === null || options === void 0 ? void 0 : options.failoverEnabled) === true;
3923
3991
  let requestBody = this.cloneRequestBody(req.body || {});
3992
+ // 请求体安全性清理:修复控制字符、无效 JSON arguments、undefined 值等问题
3993
+ const sanitizeResult = (0, index_1.sanitizeRequestBody)(requestBody);
3994
+ if (sanitizeResult.changes.length > 0) {
3995
+ console.log(`[Body-Sanitize] ${sanitizeResult.changes.length} fix(es): ${sanitizeResult.changes.join('; ')}`);
3996
+ }
3997
+ requestBody = sanitizeResult.body;
3924
3998
  let usageForLog;
3925
3999
  let responseBodyForLog;
3926
4000
  let downstreamResponseBodyForLog;
@@ -3972,7 +4046,8 @@ class ProxyServer {
3972
4046
  const effectiveApiUrl = this.resolveEffectiveApiUrl(service);
3973
4047
  const effectiveModel = rule.targetModel || (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model);
3974
4048
  const providerConfig = (0, index_1.getReasoningConfig)(service.name || '', effectiveApiUrl || '', effectiveModel || '');
3975
- const transformedRequestBody = this.transformRequestByFormat(clientFormat, sourceType, payloadForTransform, rule.targetModel, providerConfig);
4049
+ const serverToolConfig = (0, index_1.getServerToolSupport)(service.name || '', effectiveApiUrl || '');
4050
+ const transformedRequestBody = this.transformRequestByFormat(clientFormat, sourceType, payloadForTransform, rule.targetModel, providerConfig, serverToolConfig);
3976
4051
  requestBody = (_a = transformedRequestBody !== null && transformedRequestBody !== void 0 ? transformedRequestBody : this.cloneRequestBody(requestBody)) !== null && _a !== void 0 ? _a : {};
3977
4052
  // Compact final sanitize
3978
4053
  if (rule.contentType === 'compact' && clientFormat === 'claude' && Array.isArray(requestBody === null || requestBody === void 0 ? void 0 : requestBody.messages)) {
@@ -4079,7 +4154,9 @@ class ProxyServer {
4079
4154
  const parser = new streaming_1.SSEParserTransform();
4080
4155
  const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
4081
4156
  const serializer = new streaming_1.SSESerializerTransform();
4082
- const downstreamChunkCollector = new chunk_collector_1.ChunkCollectorTransform();
4157
+ const downstreamChunkCollector = new chunk_collector_1.ChunkCollectorTransform(() => {
4158
+ rules_status_service_1.rulesStatusBroadcaster.refreshRuleInUse(route.id, rule.id);
4159
+ });
4083
4160
  responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
4084
4161
  const { converter, extractUsage } = this.transformSSEByFormat(clientFormat, sourceType);
4085
4162
  this.copyResponseHeaders(responseHeaders, res);
@@ -4184,9 +4261,9 @@ class ProxyServer {
4184
4261
  /**
4185
4262
  * 使用显式 clientFormat 进行请求转换(取代 tool → format 的硬编码映射)
4186
4263
  */
4187
- transformRequestByFormat(clientFormat, source, payloadData, targetModel, providerConfig) {
4264
+ transformRequestByFormat(clientFormat, source, payloadData, targetModel, providerConfig, serverToolConfig) {
4188
4265
  const upstreamFormat = (0, index_1.sourceTypeToFormat)(source);
4189
- const result = (0, index_1.transformRequest)({ fromFormat: clientFormat, toFormat: upstreamFormat, body: payloadData, providerConfig });
4266
+ const result = (0, index_1.transformRequest)({ fromFormat: clientFormat, toFormat: upstreamFormat, body: payloadData, providerConfig, serverToolConfig });
4190
4267
  const body = result.body;
4191
4268
  if (targetModel) {
4192
4269
  const isOpenAIModel = /^gpt-|o[123]/i.test(targetModel);
@@ -251,6 +251,22 @@ class RulesStatusBroadcaster {
251
251
  timestamp: Date.now(),
252
252
  });
253
253
  }
254
+ /**
255
+ * 刷新规则使用中的不活动定时器(轻量级,仅重置定时器,不修改状态)
256
+ * 用于 streaming 过程中持续保持 in_use 状态
257
+ */
258
+ refreshRuleInUse(routeId, ruleId) {
259
+ const currentStatus = this.ruleStates.get(ruleId);
260
+ // 仅当状态已经是 in_use 时才刷新定时器
261
+ if ((currentStatus === null || currentStatus === void 0 ? void 0 : currentStatus.status) !== 'in_use')
262
+ return;
263
+ const timeoutKey = `${routeId}:${ruleId}`;
264
+ this.clearRuleTimeout(timeoutKey);
265
+ const timeout = setTimeout(() => {
266
+ this.markRuleIdle(routeId, ruleId);
267
+ }, this.INACTIVITY_TIMEOUT);
268
+ this.ruleTimeouts.set(timeoutKey, timeout);
269
+ }
254
270
  /**
255
271
  * 更新规则使用量
256
272
  */