protocol-proxy 2.0.7 → 2.1.0
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/lib/config-store.js +8 -5
- package/lib/converters/anthropic-to-openai.js +8 -2
- package/lib/converters/openai-to-anthropic.js +7 -1
- package/lib/converters/sse-helpers.js +0 -30
- package/lib/detector.js +2 -2
- package/lib/proxy-manager.js +1 -1
- package/lib/proxy-server.js +20 -9
- package/package.json +3 -3
- package/public/app.js +17 -5
- package/public/index.html +2 -1
- package/server.js +38 -6
package/lib/config-store.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const os = require('os');
|
|
4
|
+
const crypto = require('crypto');
|
|
4
5
|
|
|
5
6
|
const CONFIG_PATH = path.join(os.homedir(), '.protocol-proxy', 'proxies.json');
|
|
6
7
|
|
|
@@ -15,7 +16,7 @@ function migrateOldConfig() {
|
|
|
15
16
|
const dir = path.dirname(CONFIG_PATH);
|
|
16
17
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
17
18
|
fs.copyFileSync(OLD_CONFIG_PATH, CONFIG_PATH);
|
|
18
|
-
} catch {}
|
|
19
|
+
} catch (err) { console.error('[Config] 迁移旧配置失败:', err.message); }
|
|
19
20
|
}
|
|
20
21
|
migrateOldConfig();
|
|
21
22
|
|
|
@@ -33,7 +34,7 @@ function migrateTargetToProvider(config) {
|
|
|
33
34
|
if (!proxy.providerId) {
|
|
34
35
|
const t = proxy.target;
|
|
35
36
|
const provider = {
|
|
36
|
-
id:
|
|
37
|
+
id: crypto.randomUUID(),
|
|
37
38
|
name: t.providerName || t.providerUrl,
|
|
38
39
|
url: t.providerUrl,
|
|
39
40
|
protocol: t.protocol || 'openai',
|
|
@@ -119,7 +120,9 @@ function saveConfig(config) {
|
|
|
119
120
|
if (!fs.existsSync(dir)) {
|
|
120
121
|
fs.mkdirSync(dir, { recursive: true });
|
|
121
122
|
}
|
|
122
|
-
|
|
123
|
+
const tmpPath = CONFIG_PATH + '.tmp';
|
|
124
|
+
fs.writeFileSync(tmpPath, JSON.stringify(normalizedConfig, null, 2), 'utf-8');
|
|
125
|
+
fs.renameSync(tmpPath, CONFIG_PATH);
|
|
123
126
|
configCache = normalizedConfig;
|
|
124
127
|
return true;
|
|
125
128
|
} catch (err) {
|
|
@@ -141,7 +144,7 @@ function getProviderById(id) {
|
|
|
141
144
|
function addProvider(provider) {
|
|
142
145
|
const config = loadConfig();
|
|
143
146
|
config.providers = config.providers || [];
|
|
144
|
-
provider.id = provider.id ||
|
|
147
|
+
provider.id = provider.id || crypto.randomUUID();
|
|
145
148
|
provider.models = normalizeModels(provider.models);
|
|
146
149
|
config.providers.push(provider);
|
|
147
150
|
saveConfig(config);
|
|
@@ -182,7 +185,7 @@ function getProxyById(id) {
|
|
|
182
185
|
function addProxy(proxy) {
|
|
183
186
|
const config = loadConfig();
|
|
184
187
|
config.proxies = config.proxies || [];
|
|
185
|
-
proxy.id = proxy.id ||
|
|
188
|
+
proxy.id = proxy.id || crypto.randomUUID();
|
|
186
189
|
config.proxies.push(proxy);
|
|
187
190
|
saveConfig(config);
|
|
188
191
|
return proxy;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Anthropic → OpenAI 协议转换
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
const {
|
|
5
|
+
const { encodeAnthropicEvent } = require('./sse-helpers');
|
|
6
6
|
|
|
7
7
|
// ==================== 请求转换 ====================
|
|
8
8
|
|
|
@@ -175,11 +175,17 @@ function convertResponse(openaiBody) {
|
|
|
175
175
|
// tool_calls → tool_use
|
|
176
176
|
if (message.tool_calls) {
|
|
177
177
|
for (const tc of message.tool_calls) {
|
|
178
|
+
let input = {};
|
|
179
|
+
try {
|
|
180
|
+
input = tc.function?.arguments ? JSON.parse(tc.function.arguments) : {};
|
|
181
|
+
} catch {
|
|
182
|
+
input = {};
|
|
183
|
+
}
|
|
178
184
|
content.push({
|
|
179
185
|
type: 'tool_use',
|
|
180
186
|
id: tc.id,
|
|
181
187
|
name: tc.function?.name,
|
|
182
|
-
input
|
|
188
|
+
input,
|
|
183
189
|
});
|
|
184
190
|
}
|
|
185
191
|
}
|
|
@@ -76,11 +76,17 @@ function convertMessage(msg) {
|
|
|
76
76
|
content.push({ type: 'text', text: msg.content });
|
|
77
77
|
}
|
|
78
78
|
for (const tc of msg.tool_calls) {
|
|
79
|
+
let input = {};
|
|
80
|
+
try {
|
|
81
|
+
input = tc.function?.arguments ? JSON.parse(tc.function.arguments) : {};
|
|
82
|
+
} catch {
|
|
83
|
+
input = {};
|
|
84
|
+
}
|
|
79
85
|
content.push({
|
|
80
86
|
type: 'tool_use',
|
|
81
87
|
id: tc.id,
|
|
82
88
|
name: tc.function?.name,
|
|
83
|
-
input
|
|
89
|
+
input,
|
|
84
90
|
});
|
|
85
91
|
}
|
|
86
92
|
return { role: 'assistant', content };
|
|
@@ -2,35 +2,6 @@
|
|
|
2
2
|
* SSE 解析与编码辅助函数
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
function parseSSELines(buffer) {
|
|
6
|
-
const lines = buffer.split('\n');
|
|
7
|
-
const events = [];
|
|
8
|
-
let currentEvent = { event: null, data: null };
|
|
9
|
-
|
|
10
|
-
for (const line of lines) {
|
|
11
|
-
const trimmed = line.trim();
|
|
12
|
-
if (trimmed === '') {
|
|
13
|
-
// 空行表示一个事件结束
|
|
14
|
-
if (currentEvent.data !== null) {
|
|
15
|
-
events.push(currentEvent);
|
|
16
|
-
}
|
|
17
|
-
currentEvent = { event: null, data: null };
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
if (trimmed.startsWith('event:')) {
|
|
21
|
-
currentEvent.event = trimmed.slice(6).trim();
|
|
22
|
-
} else if (trimmed.startsWith('data:')) {
|
|
23
|
-
const data = trimmed.slice(5).trim();
|
|
24
|
-
currentEvent.data = currentEvent.data === null ? data : currentEvent.data + '\n' + data;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// 如果最后一行没有空行结束,保留未完成的事件
|
|
29
|
-
const remainder = currentEvent.data !== null ? currentEvent : null;
|
|
30
|
-
|
|
31
|
-
return { events, remainder };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
5
|
function encodeOpenAIEvent(obj) {
|
|
35
6
|
return `data: ${JSON.stringify(obj)}\n\n`;
|
|
36
7
|
}
|
|
@@ -44,7 +15,6 @@ function encodeAnthropicEvent(eventName, obj) {
|
|
|
44
15
|
}
|
|
45
16
|
|
|
46
17
|
module.exports = {
|
|
47
|
-
parseSSELines,
|
|
48
18
|
encodeOpenAIEvent,
|
|
49
19
|
encodeOpenAIDone,
|
|
50
20
|
encodeAnthropicEvent,
|
package/lib/detector.js
CHANGED
package/lib/proxy-manager.js
CHANGED
|
@@ -38,10 +38,10 @@ class ProxyManager {
|
|
|
38
38
|
|
|
39
39
|
return new Promise((resolve) => {
|
|
40
40
|
entry.server.close(() => {
|
|
41
|
+
this.servers.delete(id);
|
|
41
42
|
console.log(`[Proxy] ${entry.config.name} stopped on port ${entry.config.port}`);
|
|
42
43
|
resolve(true);
|
|
43
44
|
});
|
|
44
|
-
this.servers.delete(id);
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
|
package/lib/proxy-server.js
CHANGED
|
@@ -15,24 +15,31 @@ function createProxyApp(proxyConfigOrGetter) {
|
|
|
15
15
|
const reasoningCache = new Map();
|
|
16
16
|
const MAX_CACHE_SIZE = 100;
|
|
17
17
|
|
|
18
|
-
function
|
|
19
|
-
|
|
18
|
+
function getReasoningKey(msg) {
|
|
19
|
+
const toolIds = msg.tool_calls?.map(t => t.id).join(',') || '';
|
|
20
|
+
return msg.content + '|' + toolIds;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function setReasoning(msg, reasoning) {
|
|
24
|
+
if (!msg?.content || !reasoning) return;
|
|
25
|
+
const key = getReasoningKey(msg);
|
|
20
26
|
if (reasoningCache.size >= MAX_CACHE_SIZE) {
|
|
21
27
|
const firstKey = reasoningCache.keys().next().value;
|
|
22
28
|
reasoningCache.delete(firstKey);
|
|
23
29
|
}
|
|
24
|
-
reasoningCache.set(
|
|
30
|
+
reasoningCache.set(key, reasoning);
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
function getReasoning(
|
|
28
|
-
|
|
33
|
+
function getReasoning(msg) {
|
|
34
|
+
if (!msg?.content) return undefined;
|
|
35
|
+
return reasoningCache.get(getReasoningKey(msg));
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
function injectReasoningToMessages(messages) {
|
|
32
39
|
if (!Array.isArray(messages)) return;
|
|
33
40
|
for (const msg of messages) {
|
|
34
|
-
if (msg.role === 'assistant') {
|
|
35
|
-
const reasoning = getReasoning(msg
|
|
41
|
+
if (msg.role === 'assistant' && msg.reasoning_content === undefined) {
|
|
42
|
+
const reasoning = getReasoning(msg);
|
|
36
43
|
// DeepSeek 等 reasoning model 要求 assistant message 必须包含 reasoning_content 字段
|
|
37
44
|
msg.reasoning_content = reasoning || '';
|
|
38
45
|
}
|
|
@@ -43,7 +50,7 @@ function createProxyApp(proxyConfigOrGetter) {
|
|
|
43
50
|
const choice = body.choices?.[0];
|
|
44
51
|
const message = choice?.message;
|
|
45
52
|
if (message?.role === 'assistant' && message.reasoning_content) {
|
|
46
|
-
setReasoning(message
|
|
53
|
+
setReasoning(message, message.reasoning_content);
|
|
47
54
|
}
|
|
48
55
|
}
|
|
49
56
|
|
|
@@ -85,7 +92,7 @@ function createProxyApp(proxyConfigOrGetter) {
|
|
|
85
92
|
const targetProtocol = target.protocol;
|
|
86
93
|
const isStream = req.body?.stream === true;
|
|
87
94
|
|
|
88
|
-
console.log(`[${requestId}] ⬅️ ${inboundProtocol.toUpperCase()} → ${targetProtocol.toUpperCase()} | path=${req.path}`);
|
|
95
|
+
console.log(`[${requestId}] ⬅️ ${(inboundProtocol || 'unknown').toUpperCase()} → ${targetProtocol.toUpperCase()} | path=${req.path}`);
|
|
89
96
|
|
|
90
97
|
// 决定转换方向
|
|
91
98
|
let convertReq, convertRes, createSSEConv;
|
|
@@ -158,6 +165,10 @@ function createProxyApp(proxyConfigOrGetter) {
|
|
|
158
165
|
const reader = fetchRes.body.getReader();
|
|
159
166
|
const decoder = new TextDecoder();
|
|
160
167
|
|
|
168
|
+
req.on('close', () => {
|
|
169
|
+
try { reader.cancel(); } catch (err) { /* ignore */ }
|
|
170
|
+
});
|
|
171
|
+
|
|
161
172
|
try {
|
|
162
173
|
while (true) {
|
|
163
174
|
const { done, value } = await reader.read();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "protocol-proxy",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "OpenAI / Anthropic 协议转换透明代理",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
"public/**/*"
|
|
41
41
|
],
|
|
42
42
|
"targets": [
|
|
43
|
-
"
|
|
43
|
+
"node20-win-x64"
|
|
44
44
|
],
|
|
45
45
|
"outputPath": "dist",
|
|
46
46
|
"options": [
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
]
|
|
49
49
|
},
|
|
50
50
|
"engines": {
|
package/public/app.js
CHANGED
|
@@ -40,6 +40,7 @@ function initProviderDropdown() {
|
|
|
40
40
|
const dropdown = document.getElementById('provider-dropdown');
|
|
41
41
|
const addNameInput = document.getElementById('provider-add-name');
|
|
42
42
|
const addUrlInput = document.getElementById('provider-add-url');
|
|
43
|
+
const addKeyInput = document.getElementById('provider-add-key');
|
|
43
44
|
const addBtn = document.getElementById('provider-add-btn');
|
|
44
45
|
|
|
45
46
|
trigger.addEventListener('click', (e) => {
|
|
@@ -49,6 +50,7 @@ function initProviderDropdown() {
|
|
|
49
50
|
editingProviderId = null;
|
|
50
51
|
addNameInput.value = '';
|
|
51
52
|
addUrlInput.value = '';
|
|
53
|
+
addKeyInput.value = '';
|
|
52
54
|
addUrlInput.disabled = false;
|
|
53
55
|
addBtn.textContent = '添加';
|
|
54
56
|
renderProviderOptions();
|
|
@@ -80,10 +82,13 @@ function initProviderDropdown() {
|
|
|
80
82
|
});
|
|
81
83
|
} else {
|
|
82
84
|
// 新增模式
|
|
85
|
+
const body = { name, url };
|
|
86
|
+
const key = addKeyInput.value.trim();
|
|
87
|
+
if (key) body.apiKey = key;
|
|
83
88
|
res = await fetch('/api/providers', {
|
|
84
89
|
method: 'POST',
|
|
85
90
|
headers: { 'Content-Type': 'application/json' },
|
|
86
|
-
body: JSON.stringify(
|
|
91
|
+
body: JSON.stringify(body),
|
|
87
92
|
});
|
|
88
93
|
}
|
|
89
94
|
if (!res.ok) {
|
|
@@ -522,6 +527,7 @@ function closeModal() {
|
|
|
522
527
|
document.getElementById('model-dropdown').classList.remove('open');
|
|
523
528
|
document.getElementById('provider-dropdown').classList.remove('open');
|
|
524
529
|
editingId = null;
|
|
530
|
+
editingProviderId = null;
|
|
525
531
|
}
|
|
526
532
|
|
|
527
533
|
async function handleSubmit(e) {
|
|
@@ -551,13 +557,19 @@ async function handleSubmit(e) {
|
|
|
551
557
|
if (protocol) providerUpdates.protocol = protocol;
|
|
552
558
|
if (Object.keys(providerUpdates).length > 0) {
|
|
553
559
|
try {
|
|
554
|
-
await fetch(`/api/providers/${providerId}`, {
|
|
560
|
+
const res = await fetch(`/api/providers/${providerId}`, {
|
|
555
561
|
method: 'PUT',
|
|
556
562
|
headers: { 'Content-Type': 'application/json' },
|
|
557
563
|
body: JSON.stringify(providerUpdates),
|
|
558
564
|
});
|
|
565
|
+
if (!res.ok) {
|
|
566
|
+
const err = await res.json();
|
|
567
|
+
showToast('供应商配置保存失败: ' + (err.error || '未知错误'), true);
|
|
568
|
+
}
|
|
559
569
|
await loadProviders();
|
|
560
|
-
} catch {
|
|
570
|
+
} catch (err) {
|
|
571
|
+
showToast('供应商配置保存失败: ' + err.message, true);
|
|
572
|
+
}
|
|
561
573
|
}
|
|
562
574
|
|
|
563
575
|
const payload = {
|
|
@@ -601,7 +613,7 @@ async function startProxy(id) {
|
|
|
601
613
|
await fetch(`/api/proxies/${id}/start`, { method: 'POST' });
|
|
602
614
|
await loadProxies();
|
|
603
615
|
} catch (err) {
|
|
604
|
-
|
|
616
|
+
showToast('启动失败: ' + err.message, true);
|
|
605
617
|
}
|
|
606
618
|
}
|
|
607
619
|
|
|
@@ -610,7 +622,7 @@ async function stopProxy(id) {
|
|
|
610
622
|
await fetch(`/api/proxies/${id}/stop`, { method: 'POST' });
|
|
611
623
|
await loadProxies();
|
|
612
624
|
} catch (err) {
|
|
613
|
-
|
|
625
|
+
showToast('停止失败: ' + err.message, true);
|
|
614
626
|
}
|
|
615
627
|
}
|
|
616
628
|
|
package/public/index.html
CHANGED
|
@@ -84,7 +84,8 @@
|
|
|
84
84
|
<div class="model-dropdown-options" id="provider-dropdown-options"></div>
|
|
85
85
|
<div class="model-add-section">
|
|
86
86
|
<input type="text" class="model-add-input" id="provider-add-name" placeholder="供应商名称">
|
|
87
|
-
<input type="
|
|
87
|
+
<input type="text" class="model-add-input" id="provider-add-url" placeholder="https://api.example.com">
|
|
88
|
+
<input type="password" class="model-add-input" id="provider-add-key" placeholder="API Key (可选)">
|
|
88
89
|
<button type="button" class="btn btn-primary btn-sm" id="provider-add-btn">添加</button>
|
|
89
90
|
</div>
|
|
90
91
|
</div>
|
package/server.js
CHANGED
|
@@ -10,7 +10,9 @@ const PID_FILE = path.join(os.tmpdir(), 'protocol-proxy.pid');
|
|
|
10
10
|
const pkg = require('./package.json');
|
|
11
11
|
|
|
12
12
|
function writePid() {
|
|
13
|
-
try { fs.writeFileSync(PID_FILE, String(process.pid)); } catch {
|
|
13
|
+
try { fs.writeFileSync(PID_FILE, String(process.pid)); } catch (err) {
|
|
14
|
+
console.error('[PID] 写入失败:', err.message);
|
|
15
|
+
}
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
function readPid() {
|
|
@@ -18,7 +20,9 @@ function readPid() {
|
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
function removePid() {
|
|
21
|
-
try { fs.unlinkSync(PID_FILE); } catch {
|
|
23
|
+
try { fs.unlinkSync(PID_FILE); } catch (err) {
|
|
24
|
+
console.error('[PID] 删除失败:', err.message);
|
|
25
|
+
}
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
function isProcessAlive(pid) {
|
|
@@ -136,6 +140,17 @@ async function init() {
|
|
|
136
140
|
|
|
137
141
|
app.use(cors());
|
|
138
142
|
app.use(express.json());
|
|
143
|
+
|
|
144
|
+
// 访问日志
|
|
145
|
+
app.use((req, res, next) => {
|
|
146
|
+
const start = Date.now();
|
|
147
|
+
res.on('finish', () => {
|
|
148
|
+
const duration = Date.now() - start;
|
|
149
|
+
console.log(`[HTTP] ${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`);
|
|
150
|
+
});
|
|
151
|
+
next();
|
|
152
|
+
});
|
|
153
|
+
|
|
139
154
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
140
155
|
|
|
141
156
|
// ==================== 辅助函数 ====================
|
|
@@ -383,6 +398,19 @@ async function init() {
|
|
|
383
398
|
});
|
|
384
399
|
});
|
|
385
400
|
|
|
401
|
+
// 健康检查
|
|
402
|
+
app.get('/api/health', (req, res) => {
|
|
403
|
+
res.json({
|
|
404
|
+
status: 'ok',
|
|
405
|
+
version: pkg.version,
|
|
406
|
+
uptime: process.uptime(),
|
|
407
|
+
proxies: {
|
|
408
|
+
total: configStore.getProxies().length,
|
|
409
|
+
running: proxyManager.getRunningPorts().length,
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
386
414
|
// 前端首页
|
|
387
415
|
app.get('/', (req, res) => {
|
|
388
416
|
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
|
@@ -393,13 +421,13 @@ async function init() {
|
|
|
393
421
|
|
|
394
422
|
// 启动所有已配置的代理
|
|
395
423
|
const proxies = configStore.getProxies();
|
|
396
|
-
|
|
424
|
+
await Promise.all(proxies.map(async (proxy) => {
|
|
397
425
|
try {
|
|
398
426
|
await startProxyWithProvider(proxy);
|
|
399
427
|
} catch (err) {
|
|
400
428
|
console.error(`[Init] Failed to start proxy ${proxy.name}:`, err.message);
|
|
401
429
|
}
|
|
402
|
-
}
|
|
430
|
+
}));
|
|
403
431
|
|
|
404
432
|
app.listen(PORT, () => {
|
|
405
433
|
const adminUrl = `http://localhost:${PORT}`;
|
|
@@ -416,7 +444,9 @@ process.on('SIGINT', async () => {
|
|
|
416
444
|
try {
|
|
417
445
|
const proxyManager = require('./lib/proxy-manager');
|
|
418
446
|
await proxyManager.stopAll();
|
|
419
|
-
} catch {
|
|
447
|
+
} catch (err) {
|
|
448
|
+
console.error('[Shutdown] stopAll error:', err.message);
|
|
449
|
+
}
|
|
420
450
|
process.exit(0);
|
|
421
451
|
});
|
|
422
452
|
|
|
@@ -425,7 +455,9 @@ process.on('SIGTERM', async () => {
|
|
|
425
455
|
try {
|
|
426
456
|
const proxyManager = require('./lib/proxy-manager');
|
|
427
457
|
await proxyManager.stopAll();
|
|
428
|
-
} catch {
|
|
458
|
+
} catch (err) {
|
|
459
|
+
console.error('[Shutdown] stopAll error:', err.message);
|
|
460
|
+
}
|
|
429
461
|
process.exit(0);
|
|
430
462
|
});
|
|
431
463
|
|