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.
- package/dist/server/coding-plan-headers.js +121 -0
- package/dist/server/config-managed-fields.js +1 -0
- package/dist/server/conversions/body-sanitizer.js +138 -0
- package/dist/server/conversions/index.js +46 -21
- package/dist/server/conversions/server-tool/mapper.js +49 -0
- package/dist/server/conversions/server-tool/providers.js +40 -0
- package/dist/server/conversions/thinking/mapper.js +21 -0
- package/dist/server/conversions/utils/tool-result.js +35 -0
- package/dist/server/fs-database.js +58 -0
- package/dist/server/main.js +308 -8
- package/dist/server/proxy-server.js +91 -14
- package/dist/server/rules-status-service.js +16 -0
- package/dist/server/session-launcher.js +282 -0
- package/dist/server/session-migration.js +419 -0
- package/dist/server/transformers/chunk-collector.js +28 -1
- package/dist/ui/assets/claude-XtpLmGtF.webp +0 -0
- package/dist/ui/assets/index-CMoQtBmK.css +1 -0
- package/dist/ui/assets/index-CXdNTFiX.js +532 -0
- package/dist/ui/assets/openai-CPEiZpaN.webp +0 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-BHR12ImE.css +0 -1
- package/dist/ui/assets/index-CumAhpXg.js +0 -517
package/dist/server/main.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
*/
|