aicodeswitch 5.1.2 → 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/conversions/index.js +8 -0
- package/dist/server/conversions/utils/tool-result.js +35 -0
- package/dist/server/fs-database.js +58 -0
- package/dist/server/main.js +151 -0
- package/dist/server/proxy-server.js +71 -8
- 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
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 编程套餐 Headers 覆盖模块
|
|
4
|
+
*
|
|
5
|
+
* 当 APIService 启用 enableCodingPlan 时,将发送到上游的请求 Headers
|
|
6
|
+
* 覆盖为对应编程工具(Claude Code / Codex)的标准 Headers,
|
|
7
|
+
* 使供应商验证通过。
|
|
8
|
+
*/
|
|
9
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
|
+
};
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.applyCodingPlanHeaders = applyCodingPlanHeaders;
|
|
14
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
15
|
+
/**
|
|
16
|
+
* 代理已设置的需要保留的 Headers
|
|
17
|
+
* 这些 Headers 由 buildUpstreamHeaders 设置,不能被覆盖删除
|
|
18
|
+
*/
|
|
19
|
+
const KEEP_HEADERS = new Set([
|
|
20
|
+
'authorization', // 认证头
|
|
21
|
+
'x-api-key', // Claude 认证头
|
|
22
|
+
'x-goog-api-key', // Gemini 认证头
|
|
23
|
+
'content-type', // 内容类型
|
|
24
|
+
'accept', // 接受类型
|
|
25
|
+
'accept-encoding', // 编码
|
|
26
|
+
'connection', // 连接
|
|
27
|
+
'content-length', // 内容长度
|
|
28
|
+
'anthropic-version', // Claude API 版本
|
|
29
|
+
]);
|
|
30
|
+
/**
|
|
31
|
+
* 构建 Claude Code 标准请求 Headers
|
|
32
|
+
*/
|
|
33
|
+
function buildClaudeCodeHeaders(sessionId) {
|
|
34
|
+
return {
|
|
35
|
+
'user-agent': 'claude-cli/2.1.168 (external, claude-vscode, agent-sdk/0.3.168)',
|
|
36
|
+
'x-claude-code-session-id': sessionId,
|
|
37
|
+
'x-stainless-arch': 'arm64',
|
|
38
|
+
'x-stainless-lang': 'js',
|
|
39
|
+
'x-stainless-os': 'MacOS',
|
|
40
|
+
'x-stainless-package-version': '0.94.0',
|
|
41
|
+
'x-stainless-retry-count': '0',
|
|
42
|
+
'x-stainless-runtime': 'node',
|
|
43
|
+
'x-stainless-runtime-version': 'v24.3.0',
|
|
44
|
+
'x-stainless-timeout': '3000',
|
|
45
|
+
'anthropic-beta': 'claude-code-20250219,context-1m-2025-08-07,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07,effort-2025-11-24',
|
|
46
|
+
'anthropic-dangerous-direct-browser-access': 'true',
|
|
47
|
+
'x-app': 'cli',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 构建 Codex 标准请求 Headers
|
|
52
|
+
*/
|
|
53
|
+
function buildCodexHeaders(sessionId) {
|
|
54
|
+
return {
|
|
55
|
+
'x-codex-beta-features': 'terminal_resize_reflow,remote_compaction_v2',
|
|
56
|
+
'x-codex-turn-metadata': JSON.stringify({
|
|
57
|
+
session_id: sessionId,
|
|
58
|
+
thread_id: sessionId,
|
|
59
|
+
thread_source: 'user',
|
|
60
|
+
turn_id: crypto_1.default.randomUUID(),
|
|
61
|
+
sandbox: 'none',
|
|
62
|
+
workspace_kind: 'project',
|
|
63
|
+
request_kind: 'turn',
|
|
64
|
+
}),
|
|
65
|
+
'x-codex-window-id': `${sessionId}:0`,
|
|
66
|
+
'x-client-request-id': sessionId,
|
|
67
|
+
'session-id': sessionId,
|
|
68
|
+
'thread-id': sessionId,
|
|
69
|
+
'originator': 'codex_vscode',
|
|
70
|
+
'user-agent': 'codex_vscode/0.137.0-alpha.4 (Mac OS 26.5.0; arm64) unknown (VS Code; 26.602.40724)',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 判断 sourceType 是否为 Claude 源
|
|
75
|
+
*/
|
|
76
|
+
function isClaudeSourceType(sourceType) {
|
|
77
|
+
return sourceType === 'claude' || sourceType === 'claude-chat';
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 判断 sourceType 是否为 OpenAI 源
|
|
81
|
+
*/
|
|
82
|
+
function isOpenAISourceType(sourceType) {
|
|
83
|
+
return sourceType === 'openai' || sourceType === 'openai-chat';
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 应用编程工具 Headers 覆盖
|
|
87
|
+
*
|
|
88
|
+
* 当 service.enableCodingPlan 为 true 时调用。
|
|
89
|
+
* 清除原始请求中无关的 Headers,注入对应编程工具的标准 Headers。
|
|
90
|
+
*
|
|
91
|
+
* - Claude 源(claude/claude-chat)→ 注入 Claude Code Headers
|
|
92
|
+
* - OpenAI 源(openai/openai-chat)→ 注入 Codex Headers
|
|
93
|
+
* - Gemini 源不处理,保持原样
|
|
94
|
+
*
|
|
95
|
+
* @param headers 当前已构建的上游 Headers(会被原地修改)
|
|
96
|
+
* @param sourceType 上游服务的源类型
|
|
97
|
+
*/
|
|
98
|
+
function applyCodingPlanHeaders(headers, sourceType) {
|
|
99
|
+
const isClaude = isClaudeSourceType(sourceType);
|
|
100
|
+
const isOpenAI = isOpenAISourceType(sourceType);
|
|
101
|
+
// Gemini 源不需要 Headers 覆盖
|
|
102
|
+
if (!isClaude && !isOpenAI) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const sessionId = crypto_1.default.randomUUID();
|
|
106
|
+
// 1. 删除不在保留列表中的 Headers
|
|
107
|
+
for (const key of Object.keys(headers)) {
|
|
108
|
+
if (!KEEP_HEADERS.has(key.toLowerCase())) {
|
|
109
|
+
delete headers[key];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// 2. 注入编程工具标准 Headers
|
|
113
|
+
// 2. 注入编程工具标准 Headers
|
|
114
|
+
const toolHeaders = isClaude
|
|
115
|
+
? buildClaudeCodeHeaders(sessionId)
|
|
116
|
+
: buildCodexHeaders(sessionId);
|
|
117
|
+
for (const [key, value] of Object.entries(toolHeaders)) {
|
|
118
|
+
headers[key] = value;
|
|
119
|
+
}
|
|
120
|
+
console.log(`\x1b[36m[CodingPlan-Headers]\x1b[0m Applied ${isClaude ? 'Claude Code' : 'Codex'} header override for upstream sourceType=${sourceType}`);
|
|
121
|
+
}
|
|
@@ -91,6 +91,7 @@ const response_js_12 = require("./pairs/gemini-responses/response.js");
|
|
|
91
91
|
const streaming_js_12 = require("./pairs/gemini-responses/streaming.js");
|
|
92
92
|
// --- Provider-driven post-processing ---
|
|
93
93
|
const mapper_js_2 = require("./thinking/mapper.js");
|
|
94
|
+
const tool_result_js_1 = require("./utils/tool-result.js");
|
|
94
95
|
const effort_js_1 = require("./thinking/effort.js");
|
|
95
96
|
// ============================================================
|
|
96
97
|
// Public API: Request Transformation
|
|
@@ -294,6 +295,13 @@ function buildTargetBody(options) {
|
|
|
294
295
|
if (toFormat === 'claude' && result.thinking && result.messages) {
|
|
295
296
|
result.messages = (0, mapper_js_2.fixThinkingHistory)(result.messages, 'claude');
|
|
296
297
|
}
|
|
298
|
+
// --- Ensure tool_result blocks have id for Claude-compatible providers ---
|
|
299
|
+
// Some providers (e.g. GLM) require an id field on tool_result content blocks,
|
|
300
|
+
// but standard Claude API tool_result blocks only have tool_use_id without id.
|
|
301
|
+
if (toFormat === 'claude' && result.messages) {
|
|
302
|
+
const { messages: patchedMessages } = (0, tool_result_js_1.ensureToolResultIds)(result.messages);
|
|
303
|
+
result.messages = patchedMessages;
|
|
304
|
+
}
|
|
297
305
|
return result;
|
|
298
306
|
}
|
|
299
307
|
/** Identity converter that passes events through unchanged */
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tool result content block utilities.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ensureToolResultIds = ensureToolResultIds;
|
|
7
|
+
/**
|
|
8
|
+
* 为所有缺少 id 的 tool_result 块补上 id。
|
|
9
|
+
*
|
|
10
|
+
* 部分 Claude 兼容端点(如 GLM)要求 tool_result 内容块必须包含 id 字段,
|
|
11
|
+
* 但标准 Claude API 的 tool_result 块仅有 tool_use_id 而不带 id。
|
|
12
|
+
*
|
|
13
|
+
* id 取值策略:优先使用 tool_use_id(与对应的 tool_use.id 保持一致),
|
|
14
|
+
* 若 tool_use_id 也不存在则生成唯一 id。
|
|
15
|
+
*/
|
|
16
|
+
function ensureToolResultIds(messages) {
|
|
17
|
+
let totalPatched = 0;
|
|
18
|
+
const result = messages.map(msg => {
|
|
19
|
+
if (msg.role !== 'user' || !Array.isArray(msg.content))
|
|
20
|
+
return msg;
|
|
21
|
+
let patched = false;
|
|
22
|
+
const newContent = msg.content.map((b) => {
|
|
23
|
+
if (b.type === 'tool_result' && !b.id) {
|
|
24
|
+
patched = true;
|
|
25
|
+
totalPatched++;
|
|
26
|
+
// 使用 tool_use_id 作为 id,保持与对应 tool_use 块的 id 一致
|
|
27
|
+
const id = b.tool_use_id || `toolu_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`;
|
|
28
|
+
return Object.assign(Object.assign({}, b), { id });
|
|
29
|
+
}
|
|
30
|
+
return b;
|
|
31
|
+
});
|
|
32
|
+
return patched ? Object.assign(Object.assign({}, msg), { content: newContent }) : msg;
|
|
33
|
+
});
|
|
34
|
+
return { messages: result, patchedCount: totalPatched };
|
|
35
|
+
}
|
|
@@ -1597,6 +1597,18 @@ class FileSystemDatabaseManager {
|
|
|
1597
1597
|
yield this.saveRules();
|
|
1598
1598
|
this.routes.splice(index, 1);
|
|
1599
1599
|
yield this.saveRoutes();
|
|
1600
|
+
// 级联清理:清除绑定到该路由的会话的绑定关系
|
|
1601
|
+
let sessionChanged = false;
|
|
1602
|
+
for (const session of this.sessions) {
|
|
1603
|
+
if (session.routeId === id) {
|
|
1604
|
+
session.routeId = undefined;
|
|
1605
|
+
session.routeName = undefined;
|
|
1606
|
+
sessionChanged = true;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
if (sessionChanged) {
|
|
1610
|
+
yield this.saveSessions();
|
|
1611
|
+
}
|
|
1600
1612
|
return true;
|
|
1601
1613
|
});
|
|
1602
1614
|
}
|
|
@@ -3052,6 +3064,11 @@ class FileSystemDatabaseManager {
|
|
|
3052
3064
|
existing.highIqRuleId = session.highIqRuleId;
|
|
3053
3065
|
if (Object.prototype.hasOwnProperty.call(session, 'highIqEnabledAt'))
|
|
3054
3066
|
existing.highIqEnabledAt = session.highIqEnabledAt;
|
|
3067
|
+
// 保留已有的路由绑定(不传入时不覆盖)
|
|
3068
|
+
if (session.routeId !== undefined)
|
|
3069
|
+
existing.routeId = session.routeId;
|
|
3070
|
+
if (session.routeName !== undefined)
|
|
3071
|
+
existing.routeName = session.routeName;
|
|
3055
3072
|
}
|
|
3056
3073
|
else {
|
|
3057
3074
|
// 创建新 session
|
|
@@ -3071,11 +3088,52 @@ class FileSystemDatabaseManager {
|
|
|
3071
3088
|
highIqMode: session.highIqMode,
|
|
3072
3089
|
highIqRuleId: session.highIqRuleId,
|
|
3073
3090
|
highIqEnabledAt: session.highIqEnabledAt,
|
|
3091
|
+
routeId: session.routeId,
|
|
3092
|
+
routeName: session.routeName,
|
|
3074
3093
|
});
|
|
3075
3094
|
}
|
|
3076
3095
|
// 异步保存(不阻塞)
|
|
3077
3096
|
this.saveSessions().catch(console.error);
|
|
3078
3097
|
}
|
|
3098
|
+
/**
|
|
3099
|
+
* 绑定会话到路由
|
|
3100
|
+
*/
|
|
3101
|
+
bindSessionRoute(sessionId, routeId) {
|
|
3102
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
3103
|
+
const session = this.sessions.find(s => s.id === sessionId);
|
|
3104
|
+
if (!session)
|
|
3105
|
+
return null;
|
|
3106
|
+
const route = this.routes.find(r => r.id === routeId);
|
|
3107
|
+
if (!route)
|
|
3108
|
+
return null;
|
|
3109
|
+
session.routeId = routeId;
|
|
3110
|
+
session.routeName = route.name;
|
|
3111
|
+
yield this.saveSessions();
|
|
3112
|
+
return session;
|
|
3113
|
+
});
|
|
3114
|
+
}
|
|
3115
|
+
/**
|
|
3116
|
+
* 解绑会话路由
|
|
3117
|
+
*/
|
|
3118
|
+
unbindSessionRoute(sessionId) {
|
|
3119
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
3120
|
+
const session = this.sessions.find(s => s.id === sessionId);
|
|
3121
|
+
if (!session)
|
|
3122
|
+
return false;
|
|
3123
|
+
session.routeId = undefined;
|
|
3124
|
+
session.routeName = undefined;
|
|
3125
|
+
yield this.saveSessions();
|
|
3126
|
+
return true;
|
|
3127
|
+
});
|
|
3128
|
+
}
|
|
3129
|
+
/**
|
|
3130
|
+
* 获取绑定到指定路由的所有会话
|
|
3131
|
+
*/
|
|
3132
|
+
getBoundSessions(routeId) {
|
|
3133
|
+
return this.sessions
|
|
3134
|
+
.filter(s => s.routeId === routeId)
|
|
3135
|
+
.sort((a, b) => b.lastRequestAt - a.lastRequestAt);
|
|
3136
|
+
}
|
|
3079
3137
|
// 新增方法:获取规则黑名单状态
|
|
3080
3138
|
getRuleBlacklistStatus(serviceId, routeId, contentType) {
|
|
3081
3139
|
return __awaiter(this, void 0, void 0, function* () {
|
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');
|
|
@@ -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) {
|
|
@@ -2148,6 +2294,11 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
2148
2294
|
catch (error) {
|
|
2149
2295
|
console.error('[Server] Tool config sync failed:', error);
|
|
2150
2296
|
}
|
|
2297
|
+
// 清理旧的迁移临时文件
|
|
2298
|
+
try {
|
|
2299
|
+
(0, session_launcher_1.cleanupOldTempFiles)();
|
|
2300
|
+
}
|
|
2301
|
+
catch ( /* ignore */_a) { /* ignore */ }
|
|
2151
2302
|
const proxyServer = new proxy_server_1.ProxyServer(dbManager, app);
|
|
2152
2303
|
// Initialize proxy server and register proxy routes last
|
|
2153
2304
|
proxyServer.initialize();
|
|
@@ -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();
|
|
@@ -3412,7 +3471,9 @@ class ProxyServer {
|
|
|
3412
3471
|
const parser = new streaming_1.SSEParserTransform();
|
|
3413
3472
|
const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
|
|
3414
3473
|
const serializer = new streaming_1.SSESerializerTransform();
|
|
3415
|
-
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
|
+
});
|
|
3416
3477
|
const compactResponseSanitizer = rule.contentType === 'compact' && targetType === 'claude-code'
|
|
3417
3478
|
? new ClaudeCompactResponseSanitizer()
|
|
3418
3479
|
: null;
|
|
@@ -4093,7 +4154,9 @@ class ProxyServer {
|
|
|
4093
4154
|
const parser = new streaming_1.SSEParserTransform();
|
|
4094
4155
|
const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
|
|
4095
4156
|
const serializer = new streaming_1.SSESerializerTransform();
|
|
4096
|
-
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
|
+
});
|
|
4097
4160
|
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
4098
4161
|
const { converter, extractUsage } = this.transformSSEByFormat(clientFormat, sourceType);
|
|
4099
4162
|
this.copyResponseHeaders(responseHeaders, res);
|
|
@@ -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
|
*/
|