panrouter 5.0.0 → 5.0.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/relay_client.cjs +184 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panrouter",
3
- "version": "5.0.0",
3
+ "version": "5.0.1",
4
4
  "description": "PanRouter 客户端 v5.0 — 自愈式组网,单例保护,指数退避重连,实时状态推送",
5
5
  "type": "module",
6
6
  "bin": {
package/relay_client.cjs CHANGED
@@ -16,6 +16,168 @@ const WebSocket = globalThis.WebSocket;
16
16
  const WS_OPEN = WebSocket.OPEN;
17
17
  const net = require('net');
18
18
 
19
+ // ─── Anthropic ↔ OpenAI 格式转换(内嵌,不动服务端和管理版) ──
20
+ const MODEL_MAP = { "combo": "deepseek-v4-flash-free" };
21
+ const DEFAULT_MODEL = "deepseek-v4-flash-free";
22
+
23
+ function claudeToOpenAI(body) {
24
+ const result = { messages: [], stream: false };
25
+ const raw = body.model || "";
26
+ const name = raw.includes("/") ? raw.split("/").pop() : raw;
27
+ result.model = MODEL_MAP[name] || MODEL_MAP[raw] || DEFAULT_MODEL;
28
+
29
+ if (body.max_tokens) result.max_tokens = body.max_tokens;
30
+ if (body.temperature !== undefined) result.temperature = body.temperature;
31
+ if (body.top_p !== undefined) result.top_p = body.top_p;
32
+ if (body.system) {
33
+ const txt = Array.isArray(body.system)
34
+ ? body.system.map(s => s.text || "").filter(Boolean).join("\n")
35
+ : String(body.system);
36
+ if (txt) result.messages.push({ role: "system", content: txt });
37
+ }
38
+ if (Array.isArray(body.messages)) {
39
+ for (const m of body.messages) {
40
+ const c = convertMsg(m);
41
+ if (Array.isArray(c)) result.messages.push(...c);
42
+ else if (c) result.messages.push(c);
43
+ }
44
+ }
45
+ if (Array.isArray(body.tools)) {
46
+ result.tools = body.tools.map(t => ({
47
+ type: "function",
48
+ function: { name: t.name, description: String(t.description || ""), parameters: t.input_schema || { type: "object", properties: {} } },
49
+ }));
50
+ }
51
+ if (body.tool_choice) {
52
+ const tc = body.tool_choice;
53
+ if (tc.type === "tool") result.tool_choice = { type: "function", function: { name: tc.name } };
54
+ else result.tool_choice = tc.type === "any" ? "required" : tc.type;
55
+ }
56
+ if (body.stop_sequences) result.stop = Array.isArray(body.stop_sequences) ? body.stop_sequences : [body.stop_sequences];
57
+ return result;
58
+ }
59
+
60
+ function convertMsg(m) {
61
+ if (m.role === "user") return convertUser(m);
62
+ if (m.role === "assistant") return convertAssistant(m);
63
+ if (m.role === "tool") return { role: "tool", tool_call_id: m.tool_use_id, content: flatText(m.content) };
64
+ return null;
65
+ }
66
+
67
+ function convertUser(m) {
68
+ if (typeof m.content === "string") return { role: "user", content: m.content };
69
+ if (!Array.isArray(m.content)) return { role: "user", content: "" };
70
+ const parts = [], toolResults = [];
71
+ for (const b of m.content) {
72
+ if (b.type === "text") parts.push(b.text);
73
+ if (b.type === "tool_result") toolResults.push({ role: "tool", tool_call_id: b.tool_use_id, content: flatText(b.content) });
74
+ }
75
+ if (toolResults.length > 0) {
76
+ if (parts.length) toolResults.push({ role: "user", content: parts.join("") });
77
+ return toolResults;
78
+ }
79
+ return { role: "user", content: parts.join("") };
80
+ }
81
+
82
+ function convertAssistant(m) {
83
+ if (typeof m.content === "string") return m.content ? { role: "assistant", content: m.content } : null;
84
+ if (!Array.isArray(m.content)) return null;
85
+ const texts = [], calls = [];
86
+ for (const b of m.content) {
87
+ if (b.type === "text") texts.push(b.text);
88
+ if (b.type === "thinking") texts.push(b.thinking || "");
89
+ if (b.type === "tool_use") calls.push({ id: b.id, type: "function", function: { name: b.name, arguments: JSON.stringify(b.input || {}) } });
90
+ }
91
+ const r = { role: "assistant" };
92
+ if (texts.length && calls.length === 0) r.content = texts.join("");
93
+ if (calls.length > 0) r.tool_calls = calls;
94
+ if (calls.length === 0 && texts.length === 0) r.content = "";
95
+ return r;
96
+ }
97
+
98
+ function flatText(c) {
99
+ if (typeof c === "string") return c;
100
+ if (Array.isArray(c)) return c.filter(x => x.type === "text").map(x => x.text).join("\n");
101
+ return "";
102
+ }
103
+
104
+ // OpenAI 非流式响应 → Anthropic 格式
105
+ function openaiToClaudeResponse(data) {
106
+ if (!data || !data.choices?.[0]?.message) return null;
107
+ const choice = data.choices[0];
108
+ const msg = choice.message;
109
+
110
+ const result = {
111
+ id: `msg_${(data.id || "").replace("chatcmpl-", "")}`,
112
+ type: "message",
113
+ role: "assistant",
114
+ model: data.model || "unknown",
115
+ content: [],
116
+ stop_reason: null,
117
+ stop_sequence: null,
118
+ usage: {
119
+ input_tokens: data.usage?.prompt_tokens || 0,
120
+ output_tokens: data.usage?.completion_tokens || 0,
121
+ },
122
+ };
123
+
124
+ const finishMap = { stop: "end_turn", tool_calls: "tool_use", length: "max_tokens", content_filter: "content_filter" };
125
+ result.stop_reason = finishMap[choice.finish_reason] || choice.finish_reason || null;
126
+
127
+ if (msg.content) {
128
+ result.content.push({ type: "text", text: msg.content });
129
+ }
130
+ if (msg.tool_calls) {
131
+ for (const tc of msg.tool_calls) {
132
+ let input = {};
133
+ try { input = JSON.parse(tc.function?.arguments || "{}"); } catch {}
134
+ result.content.push({
135
+ type: "tool_use",
136
+ id: tc.id,
137
+ name: tc.function?.name || "",
138
+ input,
139
+ });
140
+ }
141
+ }
142
+
143
+ return result;
144
+ }
145
+
146
+ // 判断请求是否为 Anthropic 格式(需要转换)
147
+ // 规则:有 messages 且至少一条 message 的 content 是数组(Anthropic 内容块)
148
+ function isAnthropicFormat(body) {
149
+ if (!body || typeof body !== "object" || !Array.isArray(body.messages)) return false;
150
+ // 如果第一条 message 的 content 是数组(含 type/text 等字段),是 Anthropic 格式
151
+ return body.messages.some(m => Array.isArray(m.content) && m.content.length > 0 && typeof m.content[0] === "object" && m.content[0].type);
152
+ }
153
+
154
+ // 转换 Anthropic 请求并调用 OpenCode,再转回 Anthropic 格式
155
+ async function callOpenCodeWithConversion(body) {
156
+ const openAIBody = claudeToOpenAI(body);
157
+ log(`已转换 Anthropic→OpenAI: model=${openAIBody.model} msgs=${openAIBody.messages.length}`, "CONVERT");
158
+
159
+ const res = await fetch("https://opencode.ai/zen/v1/chat/completions", {
160
+ method: "POST",
161
+ headers: {
162
+ "Content-Type": "application/json",
163
+ Authorization: "Bearer public",
164
+ "x-opencode-client": "desktop",
165
+ },
166
+ body: JSON.stringify(openAIBody),
167
+ });
168
+
169
+ const data = await res.json();
170
+
171
+ if (!res.ok) {
172
+ log(`OpenCode API 错误: HTTP ${res.status}`, "ERROR");
173
+ return { error: { message: data.error?.message || `HTTP ${res.status}` } };
174
+ }
175
+
176
+ const claudeResp = openaiToClaudeResponse(data);
177
+ log(`已转换 OpenAI→Anthropic: stop_reason=${claudeResp?.stop_reason || "?"}`, "CONVERT");
178
+ return claudeResp;
179
+ }
180
+
19
181
  // ─── 命令行参数解析 ──────────────────────────────────────────
20
182
  function parseArgs() {
21
183
  const args = process.argv.slice(2);
@@ -250,19 +412,28 @@ function executeAndRespond(task, body) {
250
412
  function callOpenAIAndRespond(task) {
251
413
  const body = task.body || task;
252
414
 
253
- fetch("https://opencode.ai/zen/v1/chat/completions", {
254
- method: "POST",
255
- headers: {
256
- "Content-Type": "application/json",
257
- Authorization: "Bearer public",
258
- "x-opencode-client": "desktop",
259
- },
260
- body: JSON.stringify(body),
261
- }).then(async (r) => {
262
- const data = await r.json();
263
- const content = data?.choices?.[0]?.message?.content || "";
264
- log(`AI ${r.status === 200 ? "成功" : "失败"}: ${content.slice(0, 60)}`, "AI");
265
- sendResponse(task.request_id, data);
415
+ // 自动判断:如果 body 是 Anthropic 格式(有 messages 且不含 OpenAI 特征),自动转换
416
+ const needsConversion = isAnthropicFormat(body);
417
+
418
+ const doRequest = needsConversion
419
+ ? callOpenCodeWithConversion(body)
420
+ : fetch("https://opencode.ai/zen/v1/chat/completions", {
421
+ method: "POST",
422
+ headers: {
423
+ "Content-Type": "application/json",
424
+ Authorization: "Bearer public",
425
+ "x-opencode-client": "desktop",
426
+ },
427
+ body: JSON.stringify(body),
428
+ }).then(async (r) => {
429
+ const data = await r.json();
430
+ const content = data?.choices?.[0]?.message?.content || "";
431
+ log(`AI ${r.status === 200 ? "成功" : "失败"}: ${content.slice(0, 60)}`, "AI");
432
+ return data;
433
+ });
434
+
435
+ doRequest.then(response => {
436
+ sendResponse(task.request_id, response);
266
437
  }).catch(e => {
267
438
  log(`AI 异常: ${e.message}`, "AI");
268
439
  sendResponse(task.request_id, { error: e.message });