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.
@@ -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
- const candidates = buildCandidates(proxyConfig);
354
+ let candidates = buildCandidates(proxyConfig);
355
355
 
356
- if (candidates.length === 0) {
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
- const effectiveModel = proxyConfig.target?.defaultModel || inboundModel;
366
- const baseRequestBody = effectiveModel ? { ...req.body, model: effectiveModel } : { ...req.body };
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.8.3",
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"