protocol-proxy 2.8.3 → 2.10.1
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 +45 -2
- package/lib/conversation-store.js +108 -0
- package/lib/mcp-client.js +423 -0
- package/lib/prompt-builder.js +94 -0
- package/lib/proxy-server.js +44 -5
- package/lib/skill-store.js +150 -0
- package/package.json +2 -1
- package/public/app.js +1102 -106
- package/public/index.html +250 -8
- package/public/style.css +458 -2
- package/server.js +1774 -191
package/lib/proxy-server.js
CHANGED
|
@@ -351,9 +351,13 @@ function createProxyApp(proxyConfigOrGetter) {
|
|
|
351
351
|
const requestStart = Date.now();
|
|
352
352
|
const proxyConfig = getProxyConfig();
|
|
353
353
|
const inboundProtocol = detectInboundProtocol(req, req.body);
|
|
354
|
-
|
|
354
|
+
let candidates = buildCandidates(proxyConfig);
|
|
355
355
|
|
|
356
|
-
|
|
356
|
+
// 请求级供应商/模型覆盖(来自助手 chat 端点的自定义头)
|
|
357
|
+
const overrideProviderId = req.headers['x-pp-provider-id'];
|
|
358
|
+
const overrideModel = req.headers['x-pp-model'];
|
|
359
|
+
|
|
360
|
+
if (candidates.length === 0 && !overrideProviderId) {
|
|
357
361
|
return res.status(500).json({ error: 'Proxy target not configured' });
|
|
358
362
|
}
|
|
359
363
|
|
|
@@ -362,8 +366,43 @@ function createProxyApp(proxyConfigOrGetter) {
|
|
|
362
366
|
const clientIP = req.ip || req.socket?.remoteAddress || '';
|
|
363
367
|
const proxyName = proxyConfig.name || '';
|
|
364
368
|
const inboundModel = req.body?.model;
|
|
365
|
-
|
|
366
|
-
|
|
369
|
+
let effectiveModel = proxyConfig.target?.defaultModel || inboundModel;
|
|
370
|
+
|
|
371
|
+
// 模型覆盖:优先级高于 defaultModel
|
|
372
|
+
if (overrideModel) {
|
|
373
|
+
effectiveModel = overrideModel;
|
|
374
|
+
}
|
|
375
|
+
let baseRequestBody = effectiveModel ? { ...req.body, model: effectiveModel } : { ...req.body };
|
|
376
|
+
|
|
377
|
+
// 供应商覆盖:筛选或动态构建候选
|
|
378
|
+
if (overrideProviderId) {
|
|
379
|
+
const filtered = candidates.filter(c => c.providerId === overrideProviderId);
|
|
380
|
+
if (filtered.length > 0) {
|
|
381
|
+
candidates = filtered;
|
|
382
|
+
} else {
|
|
383
|
+
// 不在代理候选池中 → 用附加头动态构建临时候选
|
|
384
|
+
const providerUrl = req.headers['x-pp-provider-url'];
|
|
385
|
+
const providerProtocol = req.headers['x-pp-provider-protocol'];
|
|
386
|
+
const providerKeys = req.headers['x-pp-provider-keys'];
|
|
387
|
+
if (providerUrl && providerProtocol) {
|
|
388
|
+
const tempCandidate = {
|
|
389
|
+
providerId: overrideProviderId,
|
|
390
|
+
providerName: overrideProviderId,
|
|
391
|
+
providerUrl,
|
|
392
|
+
protocol: providerProtocol,
|
|
393
|
+
apiKeys: providerKeys ? JSON.parse(providerKeys) : [],
|
|
394
|
+
models: [],
|
|
395
|
+
azureDeployment: '',
|
|
396
|
+
azureApiVersion: '',
|
|
397
|
+
model: '',
|
|
398
|
+
weight: 1,
|
|
399
|
+
};
|
|
400
|
+
candidates = [tempCandidate];
|
|
401
|
+
} else {
|
|
402
|
+
return res.status(400).json({ error: '指定的供应商不在代理候选列表中,且缺少供应商配置' });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
367
406
|
|
|
368
407
|
// Inject cached reasoning for OpenAI inbound (OpenAI protocol lacks reasoning_content)
|
|
369
408
|
if (inboundProtocol === 'openai') {
|
|
@@ -446,7 +485,7 @@ function createProxyApp(proxyConfigOrGetter) {
|
|
|
446
485
|
|
|
447
486
|
const targetUrl = buildTargetUrl(candidate, req.path, isStream, candidateModel);
|
|
448
487
|
// Forward client headers (preserve anthropic-beta, user-agent, etc.)
|
|
449
|
-
const skipHeaders = new Set(['host', 'connection', 'content-length', 'content-type', 'accept', 'authorization', 'x-api-key', 'anthropic-version']);
|
|
488
|
+
const skipHeaders = new Set(['host', 'connection', 'content-length', 'content-type', 'accept', 'authorization', 'x-api-key', 'anthropic-version', 'x-pp-provider-id', 'x-pp-model', 'x-pp-provider-url', 'x-pp-provider-protocol', 'x-pp-provider-keys']);
|
|
450
489
|
const headers = {};
|
|
451
490
|
for (const [key, val] of Object.entries(req.headers)) {
|
|
452
491
|
if (!skipHeaders.has(key.toLowerCase())) headers[key] = val;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const SYSTEM_DIR = path.join(__dirname, '..', 'skills', 'system');
|
|
6
|
+
const PRESET_DIR = path.join(__dirname, '..', 'skills', 'preset');
|
|
7
|
+
const USER_DIR = path.join(os.homedir(), '.protocol-proxy', 'skills');
|
|
8
|
+
|
|
9
|
+
let skills = {}; // name → { name, description, content, category }
|
|
10
|
+
|
|
11
|
+
function parseFrontmatter(text) {
|
|
12
|
+
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
13
|
+
if (!match) return { name: '', description: '', body: text };
|
|
14
|
+
const meta = match[1];
|
|
15
|
+
const body = match[2];
|
|
16
|
+
let name = '', description = '', mcpServers = [];
|
|
17
|
+
for (const line of meta.split('\n')) {
|
|
18
|
+
const trimmed = line.trim();
|
|
19
|
+
const nm = trimmed.match(/^name:\s*['"]?(.+?)['"]?\s*$/);
|
|
20
|
+
if (nm) name = nm[1];
|
|
21
|
+
const dm = trimmed.match(/^description:\s*['"]?(.+?)['"]?\s*$/);
|
|
22
|
+
if (dm) description = dm[1];
|
|
23
|
+
const mm = trimmed.match(/^mcp:\s*\[([^\]]*)\]/);
|
|
24
|
+
if (mm) mcpServers = mm[1].split(',').map(s => s.trim()).filter(Boolean);
|
|
25
|
+
}
|
|
26
|
+
return { name, description, mcpServers, body };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function listDir(dirPath) {
|
|
30
|
+
if (!fs.existsSync(dirPath)) return [];
|
|
31
|
+
const result = [];
|
|
32
|
+
function walk(dir, prefix) {
|
|
33
|
+
try {
|
|
34
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
35
|
+
if (entry.name.startsWith('.')) continue;
|
|
36
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
37
|
+
if (entry.isDirectory()) walk(path.join(dir, entry.name), rel);
|
|
38
|
+
else result.push(rel);
|
|
39
|
+
}
|
|
40
|
+
} catch (err) { console.error(`[skill-store] 读取目录失败:`, err.message); }
|
|
41
|
+
}
|
|
42
|
+
walk(dirPath, '');
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function loadFromDir(dir, category) {
|
|
47
|
+
if (!fs.existsSync(dir)) return;
|
|
48
|
+
try {
|
|
49
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (!entry.isDirectory()) continue;
|
|
52
|
+
const skillDir = path.join(dir, entry.name);
|
|
53
|
+
if (category === 'preset' && fs.existsSync(path.join(skillDir, '.deleted'))) continue;
|
|
54
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
55
|
+
if (!fs.existsSync(skillFile)) continue;
|
|
56
|
+
try {
|
|
57
|
+
const raw = fs.readFileSync(skillFile, 'utf8');
|
|
58
|
+
const { name, description, mcpServers, body } = parseFrontmatter(raw);
|
|
59
|
+
const skillName = name || entry.name;
|
|
60
|
+
const scripts = listDir(path.join(skillDir, 'scripts'));
|
|
61
|
+
const references = listDir(path.join(skillDir, 'reference'));
|
|
62
|
+
skills[skillName] = { name: skillName, description, mcpServers, content: body.trim(), category, dirPath: skillDir, scripts, references };
|
|
63
|
+
} catch (err) { console.error(`[skill-store] 加载 ${entry.name} 失败:`, err.message); }
|
|
64
|
+
}
|
|
65
|
+
} catch (err) { console.error(`[skill-store] 扫描 ${dir} 失败:`, err.message); }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function list() {
|
|
69
|
+
return Object.values(skills);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function get(name) {
|
|
73
|
+
return skills[name] || null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function create(name, description, content) {
|
|
77
|
+
if (skills[name]) return null; // 已存在
|
|
78
|
+
const dir = path.join(USER_DIR, name);
|
|
79
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
80
|
+
const frontmatter = `---\nname: ${name}\ndescription: ${description}\n---\n\n${content}`;
|
|
81
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), frontmatter, 'utf8');
|
|
82
|
+
skills[name] = { name, description, content, category: 'user' };
|
|
83
|
+
return skills[name];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function createFromUpload(files) {
|
|
87
|
+
const skillMd = files.find(f => f.path === 'SKILL.md');
|
|
88
|
+
if (!skillMd) return null;
|
|
89
|
+
const raw = Buffer.from(skillMd.content, 'base64').toString('utf8');
|
|
90
|
+
const { name: parsedName, description, mcpServers, body } = parseFrontmatter(raw);
|
|
91
|
+
if (!parsedName) return null;
|
|
92
|
+
if (skills[parsedName]) return null;
|
|
93
|
+
const dir = path.join(USER_DIR, parsedName);
|
|
94
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
95
|
+
for (const f of files) {
|
|
96
|
+
const safePath = f.path.replace(/\\/g, '/');
|
|
97
|
+
if (safePath.includes('..')) continue;
|
|
98
|
+
const target = path.join(dir, safePath);
|
|
99
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
100
|
+
fs.writeFileSync(target, Buffer.from(f.content, 'base64'));
|
|
101
|
+
}
|
|
102
|
+
const scripts = listDir(path.join(dir, 'scripts'));
|
|
103
|
+
const references = listDir(path.join(dir, 'reference'));
|
|
104
|
+
skills[parsedName] = { name: parsedName, description, mcpServers, content: body.trim(), category: 'user', dirPath: dir, scripts, references };
|
|
105
|
+
return skills[parsedName];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function update(name, description, content) {
|
|
109
|
+
const skill = skills[name];
|
|
110
|
+
if (!skill || skill.category !== 'user') return null;
|
|
111
|
+
const dir = path.join(USER_DIR, name);
|
|
112
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
113
|
+
const frontmatter = `---\nname: ${name}\ndescription: ${description}\n---\n\n${content}`;
|
|
114
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), frontmatter, 'utf8');
|
|
115
|
+
skill.description = description;
|
|
116
|
+
skill.content = content;
|
|
117
|
+
return skill;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function remove(name) {
|
|
121
|
+
const skill = skills[name];
|
|
122
|
+
if (!skill || skill.category === 'system') return false;
|
|
123
|
+
if (skill.category === 'user') {
|
|
124
|
+
const userPath = path.join(USER_DIR, name);
|
|
125
|
+
if (fs.existsSync(userPath)) {
|
|
126
|
+
fs.rmSync(userPath, { recursive: true, force: true });
|
|
127
|
+
if (fs.existsSync(userPath)) return false; // 删除失败
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (skill.category === 'preset') {
|
|
131
|
+
const presetPath = path.join(PRESET_DIR, name, '.deleted');
|
|
132
|
+
try { fs.writeFileSync(presetPath, '', 'utf8'); } catch (err) { console.error(`[skill-store] 写入 .deleted 失败:`, err.message); }
|
|
133
|
+
if (!fs.existsSync(presetPath)) return false; // 写入失败(目录只读等)
|
|
134
|
+
}
|
|
135
|
+
delete skills[name];
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function init() {
|
|
140
|
+
skills = {};
|
|
141
|
+
loadFromDir(SYSTEM_DIR, 'system');
|
|
142
|
+
loadFromDir(PRESET_DIR, 'preset');
|
|
143
|
+
loadFromDir(USER_DIR, 'user');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getAvailableForChat() {
|
|
147
|
+
return Object.values(skills).map(s => ({ name: s.name, description: s.description }));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = { init, list, get, create, createFromUpload, update, remove, getAvailableForChat };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "protocol-proxy",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.1",
|
|
4
4
|
"description": "OpenAI / Anthropic 协议转换透明代理",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"build": "pkg . --out-path=dist"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
29
30
|
"cors": "^2.8.5",
|
|
30
31
|
"express": "^4.19.2",
|
|
31
32
|
"ws": "^8.20.1"
|