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.
@@ -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: 'provider-' + Date.now(),
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
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(normalizedConfig, null, 2), 'utf-8');
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 || 'provider-' + Date.now();
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 || 'proxy-' + Date.now();
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 { encodeOpenAIEvent, encodeOpenAIDone, encodeAnthropicEvent } = require('./sse-helpers');
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: tc.function?.arguments ? JSON.parse(tc.function.arguments) : {},
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: tc.function?.arguments ? JSON.parse(tc.function.arguments) : {},
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
@@ -31,8 +31,8 @@ function detectInboundProtocol(req, body) {
31
31
  }
32
32
  }
33
33
 
34
- // 无法确定时,默认 openai
35
- return 'openai';
34
+ // 无法确定时,返回 null,由调用方决定是否透传
35
+ return null;
36
36
  }
37
37
 
38
38
  module.exports = { detectInboundProtocol };
@@ -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
 
@@ -15,24 +15,31 @@ function createProxyApp(proxyConfigOrGetter) {
15
15
  const reasoningCache = new Map();
16
16
  const MAX_CACHE_SIZE = 100;
17
17
 
18
- function setReasoning(content, reasoning) {
19
- if (!content || !reasoning) return;
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(content, reasoning);
30
+ reasoningCache.set(key, reasoning);
25
31
  }
26
32
 
27
- function getReasoning(content) {
28
- return reasoningCache.get(content);
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.content);
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.content, message.reasoning_content);
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.7",
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
- "node18-win-x64"
43
+ "node20-win-x64"
44
44
  ],
45
45
  "outputPath": "dist",
46
46
  "options": [
47
- "--experimental-fetch"
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({ name, url }),
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
- alert('启动失败: ' + err.message);
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
- alert('停止失败: ' + err.message);
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="url" class="model-add-input" id="provider-add-url" placeholder="https://api.example.com">
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
- for (const proxy of proxies) {
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