protocol-proxy 2.8.0 → 2.8.2
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/converters/anthropic-to-openai.js +14 -5
- package/lib/proxy-server.js +90 -4
- package/lib/request-log.js +27 -0
- package/lib/stats-store.js +26 -2
- package/lib/ws-server.js +46 -0
- package/package.json +4 -3
- package/public/app.js +1590 -1876
- package/public/index.html +603 -393
- package/public/style.css +1786 -1906
- package/server.js +118 -2
|
@@ -82,12 +82,15 @@ function convertMessage(msg, idMap) {
|
|
|
82
82
|
// 处理 content 数组
|
|
83
83
|
if (Array.isArray(msg.content)) {
|
|
84
84
|
const textParts = [];
|
|
85
|
+
const thinkingParts = [];
|
|
85
86
|
const toolResults = [];
|
|
86
87
|
const toolUses = [];
|
|
87
88
|
|
|
88
89
|
for (const block of msg.content) {
|
|
89
90
|
if (block.type === 'text') {
|
|
90
91
|
textParts.push(block.text);
|
|
92
|
+
} else if (block.type === 'thinking') {
|
|
93
|
+
thinkingParts.push(block.thinking);
|
|
91
94
|
} else if (block.type === 'tool_result') {
|
|
92
95
|
// 使用映射后的 OpenAI 格式 id
|
|
93
96
|
const openaiId = idMap?.get(block.tool_use_id) || block.tool_use_id;
|
|
@@ -112,13 +115,15 @@ function convertMessage(msg, idMap) {
|
|
|
112
115
|
|
|
113
116
|
// assistant 消息含 tool_use → 需要拆分为 assistant + tool
|
|
114
117
|
if (msg.role === 'assistant' && toolUses.length > 0) {
|
|
115
|
-
const
|
|
116
|
-
result.push({
|
|
118
|
+
const assistantMsg = {
|
|
117
119
|
role: 'assistant',
|
|
118
120
|
content: textParts.join('') || '',
|
|
119
121
|
tool_calls: toolUses,
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
+
};
|
|
123
|
+
if (thinkingParts.length > 0) {
|
|
124
|
+
assistantMsg.reasoning_content = thinkingParts.join('');
|
|
125
|
+
}
|
|
126
|
+
return [assistantMsg];
|
|
122
127
|
}
|
|
123
128
|
|
|
124
129
|
// user 消息含 tool_result → 拆分为多个 tool 消息
|
|
@@ -133,7 +138,11 @@ function convertMessage(msg, idMap) {
|
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
// 普通情况
|
|
136
|
-
|
|
141
|
+
const out = { role: msg.role, content: textParts.join('') };
|
|
142
|
+
if (msg.role === 'assistant' && thinkingParts.length > 0) {
|
|
143
|
+
out.reasoning_content = thinkingParts.join('');
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
137
146
|
}
|
|
138
147
|
|
|
139
148
|
return { role: msg.role, content: msg.content };
|
package/lib/proxy-server.js
CHANGED
|
@@ -8,6 +8,7 @@ const a2g = require('./converters/anthropic-to-gemini');
|
|
|
8
8
|
const g2a = require('./converters/gemini-to-anthropic');
|
|
9
9
|
const { recordUsage } = require('./stats-store');
|
|
10
10
|
const logger = require('./logger');
|
|
11
|
+
const requestLog = require('./request-log');
|
|
11
12
|
|
|
12
13
|
function createProxyApp(proxyConfigOrGetter) {
|
|
13
14
|
const getProxyConfig = typeof proxyConfigOrGetter === 'function'
|
|
@@ -81,9 +82,13 @@ function createProxyApp(proxyConfigOrGetter) {
|
|
|
81
82
|
function injectReasoningToMessages(messages) {
|
|
82
83
|
if (!Array.isArray(messages)) return;
|
|
83
84
|
for (const msg of messages) {
|
|
84
|
-
if (msg.role === 'assistant' && msg.reasoning_content === undefined) {
|
|
85
|
+
if (msg.role === 'assistant' && (msg.reasoning_content === undefined || msg.reasoning_content === null)) {
|
|
85
86
|
const reasoning = getReasoning(msg);
|
|
86
|
-
|
|
87
|
+
if (reasoning) {
|
|
88
|
+
msg.reasoning_content = reasoning;
|
|
89
|
+
} else {
|
|
90
|
+
delete msg.reasoning_content;
|
|
91
|
+
}
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
94
|
}
|
|
@@ -343,6 +348,7 @@ function createProxyApp(proxyConfigOrGetter) {
|
|
|
343
348
|
|
|
344
349
|
async function handleRequest(req, res) {
|
|
345
350
|
const requestId = `req-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
351
|
+
const requestStart = Date.now();
|
|
346
352
|
const proxyConfig = getProxyConfig();
|
|
347
353
|
const inboundProtocol = detectInboundProtocol(req, req.body);
|
|
348
354
|
const candidates = buildCandidates(proxyConfig);
|
|
@@ -353,6 +359,8 @@ function createProxyApp(proxyConfigOrGetter) {
|
|
|
353
359
|
|
|
354
360
|
const isStream = req.body?.stream === true;
|
|
355
361
|
const proxyId = proxyConfig.id || 'default';
|
|
362
|
+
const clientIP = req.ip || req.socket?.remoteAddress || '';
|
|
363
|
+
const proxyName = proxyConfig.name || '';
|
|
356
364
|
const inboundModel = req.body?.model;
|
|
357
365
|
const effectiveModel = proxyConfig.target?.defaultModel || inboundModel;
|
|
358
366
|
const baseRequestBody = effectiveModel ? { ...req.body, model: effectiveModel } : { ...req.body };
|
|
@@ -448,6 +456,7 @@ function createProxyApp(proxyConfigOrGetter) {
|
|
|
448
456
|
|
|
449
457
|
const maxKeyRetries = (candidate.apiKeys || []).filter(k => typeof k === 'object' ? k.enabled !== false : true).length || 1;
|
|
450
458
|
let lastKeyError = null;
|
|
459
|
+
let keyLabel = '';
|
|
451
460
|
|
|
452
461
|
for (let keyAttempt = 0; keyAttempt < maxKeyRetries; keyAttempt++) {
|
|
453
462
|
const currentKey = selectKey(candidate.providerId, candidate.apiKeys || []);
|
|
@@ -471,7 +480,7 @@ function createProxyApp(proxyConfigOrGetter) {
|
|
|
471
480
|
method: 'POST',
|
|
472
481
|
headers: keyHeaders,
|
|
473
482
|
body: JSON.stringify(targetBody),
|
|
474
|
-
signal: AbortSignal.timeout(300000),
|
|
483
|
+
signal: AbortSignal.timeout(proxyConfig.timeout || 300000),
|
|
475
484
|
});
|
|
476
485
|
|
|
477
486
|
if (!fetchRes.ok) {
|
|
@@ -493,7 +502,7 @@ function createProxyApp(proxyConfigOrGetter) {
|
|
|
493
502
|
recordSuccess(proxyId, candidate.providerId, Date.now() - startedAt);
|
|
494
503
|
const keyEntry = (candidate.apiKeys || []).find(k => (typeof k === 'string' ? k : k.key) === currentKey);
|
|
495
504
|
const alias = keyEntry && typeof keyEntry === 'object' ? keyEntry.alias : '';
|
|
496
|
-
|
|
505
|
+
keyLabel = alias ? `${alias}(…${currentKey.slice(-4)})` : (currentKey ? `…${currentKey.slice(-4)}` : '-');
|
|
497
506
|
logger.log(`[${requestId}] ✓ ${candidate.providerName} | model=${candidateModel || '(default)'} key=${keyLabel} (${Date.now() - startedAt}ms)`);
|
|
498
507
|
|
|
499
508
|
if (isStream) {
|
|
@@ -506,6 +515,7 @@ function createProxyApp(proxyConfigOrGetter) {
|
|
|
506
515
|
const decoder = new TextDecoder();
|
|
507
516
|
let streamUsage = null;
|
|
508
517
|
let responseText = '';
|
|
518
|
+
let reasoningText = '';
|
|
509
519
|
let toolCallCount = 0;
|
|
510
520
|
|
|
511
521
|
req.on('close', () => {
|
|
@@ -525,6 +535,7 @@ function createProxyApp(proxyConfigOrGetter) {
|
|
|
525
535
|
if (d.usage) streamUsage = d.usage;
|
|
526
536
|
const delta = d.choices?.[0]?.delta;
|
|
527
537
|
if (delta?.content) responseText += delta.content;
|
|
538
|
+
if (delta?.reasoning_content) reasoningText += delta.reasoning_content;
|
|
528
539
|
if (delta?.tool_calls) {
|
|
529
540
|
for (const tc of delta.tool_calls) {
|
|
530
541
|
if (tc.function?.name) toolCallCount++;
|
|
@@ -541,8 +552,26 @@ function createProxyApp(proxyConfigOrGetter) {
|
|
|
541
552
|
}
|
|
542
553
|
}
|
|
543
554
|
|
|
555
|
+
// Cache reasoning_content from streaming response for future requests
|
|
556
|
+
if (reasoningText && responseText) {
|
|
557
|
+
const msg = { content: responseText, tool_calls: null };
|
|
558
|
+
setReasoning(msg, reasoningText);
|
|
559
|
+
}
|
|
560
|
+
|
|
544
561
|
if (streamUsage) {
|
|
545
562
|
recordUsage(proxyConfig.id, candidate.providerName, candidateModel, streamUsage, false);
|
|
563
|
+
requestLog.add({
|
|
564
|
+
id: requestId, proxyId, proxyName, method: req.method, path: req.path,
|
|
565
|
+
inboundProtocol, targetProtocol: candidate.protocol,
|
|
566
|
+
providerName: candidate.providerName, model: candidateModel || '',
|
|
567
|
+
status: 'success', upstreamStatusCode: null,
|
|
568
|
+
latencyMs: Date.now() - startedAt,
|
|
569
|
+
promptTokens: streamUsage.prompt_tokens || streamUsage.input_tokens || 0,
|
|
570
|
+
completionTokens: streamUsage.completion_tokens || streamUsage.output_tokens || 0,
|
|
571
|
+
totalTokens: (streamUsage.prompt_tokens || streamUsage.input_tokens || 0)
|
|
572
|
+
+ (streamUsage.completion_tokens || streamUsage.output_tokens || 0),
|
|
573
|
+
isEstimated: false, stream: true, keyAlias: keyLabel, errorMessage: null, clientIP,
|
|
574
|
+
});
|
|
546
575
|
} else if (responseText || toolCallCount > 0) {
|
|
547
576
|
const inputTokens = estimateInputTokens(req.body);
|
|
548
577
|
const outputTokens = estimateTokens(responseText) + toolCallCount * 15;
|
|
@@ -550,6 +579,16 @@ function createProxyApp(proxyConfigOrGetter) {
|
|
|
550
579
|
prompt_tokens: inputTokens,
|
|
551
580
|
completion_tokens: outputTokens,
|
|
552
581
|
}, true);
|
|
582
|
+
requestLog.add({
|
|
583
|
+
id: requestId, proxyId, proxyName, method: req.method, path: req.path,
|
|
584
|
+
inboundProtocol, targetProtocol: candidate.protocol,
|
|
585
|
+
providerName: candidate.providerName, model: candidateModel || '',
|
|
586
|
+
status: 'success', upstreamStatusCode: null,
|
|
587
|
+
latencyMs: Date.now() - startedAt,
|
|
588
|
+
promptTokens: inputTokens, completionTokens: outputTokens,
|
|
589
|
+
totalTokens: inputTokens + outputTokens,
|
|
590
|
+
isEstimated: true, stream: true, keyAlias: keyLabel, errorMessage: null, clientIP,
|
|
591
|
+
});
|
|
553
592
|
}
|
|
554
593
|
|
|
555
594
|
if (sseConverter) {
|
|
@@ -559,6 +598,15 @@ function createProxyApp(proxyConfigOrGetter) {
|
|
|
559
598
|
} catch (err) {
|
|
560
599
|
recordFailure(proxyId, candidate.providerId);
|
|
561
600
|
logger.error(`[${requestId}] Stream error:`, err.message);
|
|
601
|
+
requestLog.add({
|
|
602
|
+
id: requestId, proxyId, proxyName, method: req.method, path: req.path,
|
|
603
|
+
inboundProtocol, targetProtocol: candidate.protocol,
|
|
604
|
+
providerName: candidate.providerName, model: candidateModel || '',
|
|
605
|
+
status: 'failure', upstreamStatusCode: null,
|
|
606
|
+
latencyMs: Date.now() - startedAt,
|
|
607
|
+
promptTokens: 0, completionTokens: 0, totalTokens: 0, isEstimated: false,
|
|
608
|
+
stream: true, keyAlias: keyLabel, errorMessage: err.message, clientIP,
|
|
609
|
+
});
|
|
562
610
|
if (!res.writableEnded) {
|
|
563
611
|
try {
|
|
564
612
|
res.write(`data: ${JSON.stringify({ error: { message: err.message, type: 'proxy_error' } })}\n\n`);
|
|
@@ -574,6 +622,18 @@ function createProxyApp(proxyConfigOrGetter) {
|
|
|
574
622
|
extractReasoningFromResponse(responseBody);
|
|
575
623
|
extractAnthropicThinking(responseBody);
|
|
576
624
|
recordUsage(proxyConfig.id, candidate.providerName, candidateModel, responseBody.usage);
|
|
625
|
+
requestLog.add({
|
|
626
|
+
id: requestId, proxyId, proxyName, method: req.method, path: req.path,
|
|
627
|
+
inboundProtocol, targetProtocol: candidate.protocol,
|
|
628
|
+
providerName: candidate.providerName, model: candidateModel || '',
|
|
629
|
+
status: 'success', upstreamStatusCode: fetchRes.status,
|
|
630
|
+
latencyMs: Date.now() - startedAt,
|
|
631
|
+
promptTokens: responseBody.usage?.prompt_tokens || responseBody.usage?.input_tokens || 0,
|
|
632
|
+
completionTokens: responseBody.usage?.completion_tokens || responseBody.usage?.output_tokens || 0,
|
|
633
|
+
totalTokens: (responseBody.usage?.prompt_tokens || responseBody.usage?.input_tokens || 0)
|
|
634
|
+
+ (responseBody.usage?.completion_tokens || responseBody.usage?.output_tokens || 0),
|
|
635
|
+
isEstimated: false, stream: false, keyAlias: keyLabel, errorMessage: null, clientIP,
|
|
636
|
+
});
|
|
577
637
|
const convertedBody = convertRes(responseBody);
|
|
578
638
|
return res.json(convertedBody);
|
|
579
639
|
} catch (err) {
|
|
@@ -584,6 +644,15 @@ function createProxyApp(proxyConfigOrGetter) {
|
|
|
584
644
|
}
|
|
585
645
|
recordFailure(proxyId, candidate.providerId);
|
|
586
646
|
logger.error(`[${requestId}] ✗ ${candidate.providerName} | model=${candidateModel || '(default)'} - ${err.message}`);
|
|
647
|
+
requestLog.add({
|
|
648
|
+
id: requestId, proxyId, proxyName, method: req.method, path: req.path,
|
|
649
|
+
inboundProtocol, targetProtocol: candidate.protocol,
|
|
650
|
+
providerName: candidate.providerName, model: candidateModel || '',
|
|
651
|
+
status: 'failure', upstreamStatusCode: err?.status || null,
|
|
652
|
+
latencyMs: Date.now() - startedAt,
|
|
653
|
+
promptTokens: 0, completionTokens: 0, totalTokens: 0, isEstimated: false,
|
|
654
|
+
stream: isStream, keyAlias: keyLabel, errorMessage: err.message, clientIP,
|
|
655
|
+
});
|
|
587
656
|
if (err?.status && !isRetryableStatus(err.status)) {
|
|
588
657
|
return res.status(err.status).json({ error: err.message });
|
|
589
658
|
}
|
|
@@ -596,10 +665,27 @@ function createProxyApp(proxyConfigOrGetter) {
|
|
|
596
665
|
if (lastKeyError) {
|
|
597
666
|
recordFailure(proxyId, candidate.providerId);
|
|
598
667
|
logger.error(`[${requestId}] ✗ ${candidate.providerName} | all keys rate-limited (429)`);
|
|
668
|
+
requestLog.add({
|
|
669
|
+
id: requestId, proxyId, proxyName, method: req.method, path: req.path,
|
|
670
|
+
inboundProtocol, targetProtocol: candidate.protocol,
|
|
671
|
+
providerName: candidate.providerName, model: candidateModel || '',
|
|
672
|
+
status: '429', upstreamStatusCode: 429,
|
|
673
|
+
latencyMs: Date.now() - startedAt,
|
|
674
|
+
promptTokens: 0, completionTokens: 0, totalTokens: 0, isEstimated: false,
|
|
675
|
+
stream: isStream, keyAlias: keyLabel, errorMessage: 'All keys rate-limited', clientIP,
|
|
676
|
+
});
|
|
599
677
|
}
|
|
600
678
|
} // end candidate loop
|
|
601
679
|
|
|
602
680
|
logger.error(`[${requestId}] 所有供应商均失败`);
|
|
681
|
+
requestLog.add({
|
|
682
|
+
id: requestId, proxyId, proxyName, method: req.method, path: req.path,
|
|
683
|
+
inboundProtocol, targetProtocol: '', providerName: 'N/A', model: effectiveModel || '',
|
|
684
|
+
status: 'failure', upstreamStatusCode: null,
|
|
685
|
+
latencyMs: Date.now() - requestStart,
|
|
686
|
+
promptTokens: 0, completionTokens: 0, totalTokens: 0, isEstimated: false,
|
|
687
|
+
stream: isStream, keyAlias: '', errorMessage: 'All providers failed', clientIP,
|
|
688
|
+
});
|
|
603
689
|
return res.status(502).json({ error: 'All providers failed' });
|
|
604
690
|
}
|
|
605
691
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const MAX_ENTRIES = 1000;
|
|
2
|
+
|
|
3
|
+
const entries = [];
|
|
4
|
+
let listener = null;
|
|
5
|
+
|
|
6
|
+
function add(entry) {
|
|
7
|
+
entry.timestamp = entry.timestamp || new Date().toISOString();
|
|
8
|
+
entries.unshift(entry);
|
|
9
|
+
if (entries.length > MAX_ENTRIES) entries.pop();
|
|
10
|
+
if (listener) {
|
|
11
|
+
try { listener(entry); } catch { /* ignore */ }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getAll(limit) {
|
|
16
|
+
return entries.slice(0, limit || MAX_ENTRIES);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getCount() {
|
|
20
|
+
return entries.length;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function onEntry(callback) {
|
|
24
|
+
listener = callback;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { add, getAll, getCount, onEntry, MAX_ENTRIES };
|
package/lib/stats-store.js
CHANGED
|
@@ -29,6 +29,7 @@ function addToBuffer(period, date, statsKey, prompt, completion, estimated) {
|
|
|
29
29
|
function flush() {
|
|
30
30
|
const stats = readStats();
|
|
31
31
|
if (!dirty) return stats;
|
|
32
|
+
if (!stats.hourly) stats.hourly = {};
|
|
32
33
|
if (!stats.daily) stats.daily = {};
|
|
33
34
|
if (!stats.monthly) stats.monthly = {};
|
|
34
35
|
|
|
@@ -92,15 +93,31 @@ function yearKey(d) {
|
|
|
92
93
|
return String(d.getFullYear());
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
function hourKey(d) {
|
|
97
|
+
return dateKey(d) + '-' + String(d.getHours()).padStart(2, '0');
|
|
98
|
+
}
|
|
99
|
+
|
|
95
100
|
// ==================== 分层合并 ====================
|
|
96
101
|
|
|
97
102
|
function mergeIfNeeded() {
|
|
98
103
|
const stats = flush();
|
|
99
104
|
let changed = false;
|
|
100
105
|
|
|
106
|
+
const todayStr = dateKey(new Date());
|
|
101
107
|
const currentMonth = monthKey(new Date()); // "2026-05"
|
|
102
108
|
const currentYear = String(new Date().getFullYear());
|
|
103
109
|
|
|
110
|
+
// 小时级:清理昨天及更早的数据(daily 已有汇总,hourly 仅保留当天)
|
|
111
|
+
if (stats.hourly) {
|
|
112
|
+
const todayStart = todayStr + '-00';
|
|
113
|
+
for (const hk of Object.keys(stats.hourly)) {
|
|
114
|
+
if (hk < todayStart) {
|
|
115
|
+
delete stats.hourly[hk];
|
|
116
|
+
changed = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
104
121
|
// 日 → 月:非当月的日级数据合并为月级
|
|
105
122
|
if (stats.daily) {
|
|
106
123
|
const toMerge = {};
|
|
@@ -183,6 +200,7 @@ function recordUsage(proxyId, provider, model, usage, estimated = false) {
|
|
|
183
200
|
if (!prompt && !completion) return;
|
|
184
201
|
|
|
185
202
|
const now = new Date();
|
|
203
|
+
const hk = hourKey(now);
|
|
186
204
|
const dk = dateKey(now);
|
|
187
205
|
const mk = monthKey(now);
|
|
188
206
|
|
|
@@ -195,6 +213,7 @@ function recordUsage(proxyId, provider, model, usage, estimated = false) {
|
|
|
195
213
|
];
|
|
196
214
|
|
|
197
215
|
for (const key of keys) {
|
|
216
|
+
addToBuffer('hourly', hk, key, prompt, completion, estimated);
|
|
198
217
|
addToBuffer('daily', dk, key, prompt, completion, estimated);
|
|
199
218
|
}
|
|
200
219
|
}
|
|
@@ -208,9 +227,14 @@ function getStats(opts = {}) {
|
|
|
208
227
|
const stats = readStats();
|
|
209
228
|
const src = stats[range] || {};
|
|
210
229
|
|
|
230
|
+
// hourly keys are "YYYY-MM-DD-HH", need to pad day-level dates for comparison
|
|
231
|
+
const isHourly = range === 'hourly';
|
|
232
|
+
const filterStart = startDate ? (isHourly ? startDate + '-00' : startDate) : null;
|
|
233
|
+
const filterEnd = endDate ? (isHourly ? endDate + '-99' : endDate) : null;
|
|
234
|
+
|
|
211
235
|
const dates = Object.keys(src).filter(d => {
|
|
212
|
-
if (
|
|
213
|
-
if (
|
|
236
|
+
if (filterStart && d < filterStart) return false;
|
|
237
|
+
if (filterEnd && d > filterEnd) return false;
|
|
214
238
|
return true;
|
|
215
239
|
}).sort();
|
|
216
240
|
|
package/lib/ws-server.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const logger = require('./logger');
|
|
2
|
+
|
|
3
|
+
let wss = null;
|
|
4
|
+
let WebSocketServer = null;
|
|
5
|
+
|
|
6
|
+
function init(httpServer) {
|
|
7
|
+
try {
|
|
8
|
+
const ws = require('ws');
|
|
9
|
+
WebSocketServer = ws.WebSocketServer || ws.Server;
|
|
10
|
+
wss = new WebSocketServer({ server: httpServer });
|
|
11
|
+
|
|
12
|
+
wss.on('connection', (ws) => {
|
|
13
|
+
logger.log('[WS] Client connected');
|
|
14
|
+
try { ws.send(JSON.stringify({ type: 'connected', count: getClientCount() })); } catch { /* ignore */ }
|
|
15
|
+
ws.on('close', () => { logger.log('[WS] Client disconnected'); });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
logger.log('[WS] WebSocket server initialized');
|
|
19
|
+
} catch (err) {
|
|
20
|
+
logger.error('[WS] Failed to initialize WebSocket server:', err.message);
|
|
21
|
+
wss = null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function broadcast(data) {
|
|
26
|
+
if (!wss) return;
|
|
27
|
+
const msg = JSON.stringify(data);
|
|
28
|
+
for (const client of wss.clients) {
|
|
29
|
+
if (client.readyState === 1 /* OPEN */) {
|
|
30
|
+
try { client.send(msg); } catch { /* ignore */ }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getClientCount() {
|
|
36
|
+
return wss ? wss.clients.size : 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function close() {
|
|
40
|
+
if (wss) {
|
|
41
|
+
try { wss.close(); } catch { /* ignore */ }
|
|
42
|
+
wss = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { init, broadcast, getClientCount, close };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "protocol-proxy",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.2",
|
|
4
4
|
"description": "OpenAI / Anthropic 协议转换透明代理",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"cors": "^2.8.5",
|
|
30
|
-
"express": "^4.19.2"
|
|
30
|
+
"express": "^4.19.2",
|
|
31
|
+
"ws": "^8.20.1"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"pkg": "^5.8.1"
|
|
@@ -48,4 +49,4 @@
|
|
|
48
49
|
"engines": {
|
|
49
50
|
"node": ">=20.0.0"
|
|
50
51
|
}
|
|
51
|
-
}
|
|
52
|
+
}
|