feat: add desktop app hot update channel

This commit is contained in:
mac
2026-07-15 18:01:56 +08:00
parent 2dc15d99ad
commit 322807c68d
45 changed files with 292746 additions and 10 deletions
+354
View File
@@ -0,0 +1,354 @@
// translate.js —— Responses API ⇄ Chat Completions 协议转换。
// 让只支持 chat/completions 的中转(DeepSeek/GLM/Kimi/通义等)也能给 Codex(用 responses)用。
// 参考实现:sybil-solutions/codex-shim、BerriAI/litellm。
const crypto = require("crypto");
function genId(prefix) {
return prefix + Date.now().toString(36) + crypto.randomBytes(4).toString("hex");
}
// content parts / string → 纯文本
function partsToText(content) {
if (typeof content === "string") return content;
if (Array.isArray(content)) {
return content
.map((p) => {
if (typeof p === "string") return p;
if (p && (p.type === "input_text" || p.type === "output_text" || p.type === "text")) return p.text || "";
return "";
})
.join("");
}
return "";
}
// Responses 的 tool(扁平)→ Chat Completions 的 tool(嵌套在 function 下)
function toChatTool(t) {
if (!t) return null;
if (t.type === "function" && t.function) return t; // 已是嵌套
if (t.type === "function" || t.name) {
return {
type: "function",
function: {
name: t.name,
description: t.description || "",
parameters: t.parameters || { type: "object", properties: {} },
},
};
}
return null; // 托管型工具(web_search 等)无 chat 等价,先跳过
}
function toChatToolChoice(tc) {
if (typeof tc === "string") return tc;
if (tc && tc.type === "function" && tc.name) return { type: "function", function: { name: tc.name } };
return tc;
}
// —— 请求:Responses → Chat Completions ——
function responsesToChat(req, overrideModel) {
const messages = [];
if (req.instructions) messages.push({ role: "system", content: req.instructions });
const pending = [];
const flush = () => {
if (pending.length) {
messages.push({ role: "assistant", content: null, tool_calls: pending.splice(0) });
}
};
const input = req.input;
if (typeof input === "string") {
messages.push({ role: "user", content: input });
} else if (Array.isArray(input)) {
for (const item of input) {
const t = item.type;
if (t === "message" || (item.role && !t)) {
flush();
const role = item.role === "developer" ? "system" : item.role;
messages.push({ role, content: partsToText(item.content) });
} else if (t === "function_call") {
const callId = item.call_id || item.id || "call_0";
pending.push({ id: callId, type: "function", function: { name: item.name || "", arguments: item.arguments || "" } });
} else if (t === "function_call_output") {
flush();
const out = typeof item.output === "string" ? item.output : JSON.stringify(item.output);
messages.push({ role: "tool", tool_call_id: item.call_id, content: out });
}
// reasoning item → 丢弃(chat 上游用不到)
}
}
flush();
const out = { model: overrideModel || req.model, messages, stream: req.stream !== false };
if (Array.isArray(req.tools)) {
const tools = req.tools.map(toChatTool).filter(Boolean);
if (tools.length) out.tools = tools;
}
if (req.tool_choice) out.tool_choice = toChatToolChoice(req.tool_choice);
if (req.max_output_tokens) out.max_tokens = req.max_output_tokens;
if (req.temperature != null) out.temperature = req.temperature;
if (req.top_p != null) out.top_p = req.top_p;
if (req.parallel_tool_calls != null) out.parallel_tool_calls = req.parallel_tool_calls;
const eff = req.reasoning && req.reasoning.effort;
if (eff) out.reasoning_effort = eff;
if (out.stream) out.stream_options = { include_usage: true };
return out;
}
function mapUsage(u) {
if (!u) return null;
return {
input_tokens: u.prompt_tokens || 0,
input_tokens_details: { cached_tokens: (u.prompt_tokens_details && u.prompt_tokens_details.cached_tokens) || 0 },
output_tokens: u.completion_tokens || 0,
output_tokens_details: { reasoning_tokens: 0 },
total_tokens: u.total_tokens || 0,
};
}
// —— 响应:Chat Completions 流 → Responses 流事件(有状态转换器)——
class ChatToResponses {
constructor(model) {
this.model = model;
this.responseId = genId("resp_");
this.msgId = genId("msg_");
this.seq = 0;
this.outIndex = 0;
this.msgOpen = false;
this.msgClosed = false;
this.msgText = "";
this.msgOutputIndex = null;
this.tools = {}; // index → {id,name,arguments,outputIndex,closed}
this.usage = null;
this.started = false;
// 推理模型(DeepSeek-R1/v4、GLM、Kimi)的思考内容
this.reasonOpen = false;
this.reasonClosed = false;
this.reasonText = "";
this.reasonId = null;
this.reasonOutputIndex = null;
}
openReason() {
if (this.reasonOpen) return "";
this.reasonOpen = true;
this.reasonId = genId("rs_");
this.reasonOutputIndex = this.outIndex++;
let s = this.ev("response.output_item.added", {
output_index: this.reasonOutputIndex,
item: { id: this.reasonId, type: "reasoning", summary: [] },
});
s += this.ev("response.reasoning_summary_part.added", {
item_id: this.reasonId,
output_index: this.reasonOutputIndex,
summary_index: 0,
part: { type: "summary_text", text: "" },
});
return s;
}
closeReason() {
if (!this.reasonOpen || this.reasonClosed) return "";
this.reasonClosed = true;
let s = this.ev("response.reasoning_summary_text.done", {
item_id: this.reasonId,
output_index: this.reasonOutputIndex,
summary_index: 0,
text: this.reasonText,
});
s += this.ev("response.reasoning_summary_part.done", {
item_id: this.reasonId,
output_index: this.reasonOutputIndex,
summary_index: 0,
part: { type: "summary_text", text: this.reasonText },
});
s += this.ev("response.output_item.done", {
output_index: this.reasonOutputIndex,
item: { id: this.reasonId, type: "reasoning", summary: [{ type: "summary_text", text: this.reasonText }] },
});
return s;
}
ev(type, obj) {
const payload = Object.assign({ type, sequence_number: this.seq++ }, obj);
return `event: ${type}\ndata: ${JSON.stringify(payload)}\n\n`;
}
baseResponse(status, output) {
return {
id: this.responseId,
object: "response",
created_at: Math.floor(Date.now() / 1000),
status,
error: null,
incomplete_details: null,
instructions: null,
model: this.model,
output,
parallel_tool_calls: true,
previous_response_id: null,
reasoning: { effort: null, summary: null },
store: false,
temperature: 1.0,
text: { format: { type: "text" } },
tool_choice: "auto",
tools: [],
top_p: 1.0,
truncation: "disabled",
usage: this.usage,
metadata: {},
};
}
start() {
const r = this.baseResponse("in_progress", []);
return this.ev("response.created", { response: r }) + this.ev("response.in_progress", { response: r });
}
openMsg() {
if (this.msgOpen) return "";
this.msgOpen = true;
this.msgOutputIndex = this.outIndex++;
let s = this.ev("response.output_item.added", {
output_index: this.msgOutputIndex,
item: { id: this.msgId, type: "message", status: "in_progress", role: "assistant", content: [] },
});
s += this.ev("response.content_part.added", {
item_id: this.msgId,
output_index: this.msgOutputIndex,
content_index: 0,
part: { type: "output_text", text: "", annotations: [] },
});
return s;
}
closeMsg() {
if (!this.msgOpen || this.msgClosed) return "";
this.msgClosed = true;
let s = this.ev("response.output_text.done", {
item_id: this.msgId,
output_index: this.msgOutputIndex,
content_index: 0,
text: this.msgText,
logprobs: [],
});
s += this.ev("response.content_part.done", {
item_id: this.msgId,
output_index: this.msgOutputIndex,
content_index: 0,
part: { type: "output_text", text: this.msgText, annotations: [] },
});
s += this.ev("response.output_item.done", {
output_index: this.msgOutputIndex,
item: {
id: this.msgId,
type: "message",
status: "completed",
role: "assistant",
content: [{ type: "output_text", text: this.msgText, annotations: [] }],
},
});
return s;
}
onToolDelta(tc) {
let out = "";
const index = tc.index != null ? tc.index : 0;
const fn = tc.function || {};
let st = this.tools[index];
if (!st) {
out += this.closeReason(); // 先关推理
out += this.closeMsg(); // 再关文本消息,才开工具项
const callId = tc.id || "call_" + index;
st = { id: callId, name: fn.name || "", arguments: "", outputIndex: this.outIndex++, closed: false };
this.tools[index] = st;
out += this.ev("response.output_item.added", {
output_index: st.outputIndex,
item: { id: st.id, type: "function_call", status: "in_progress", call_id: st.id, name: st.name, arguments: "" },
});
} else if (fn.name) {
st.name += fn.name;
}
if (fn.arguments) {
st.arguments += fn.arguments;
out += this.ev("response.function_call_arguments.delta", {
item_id: st.id,
output_index: st.outputIndex,
delta: fn.arguments,
});
}
return out;
}
closeTool(st) {
if (st.closed) return "";
st.closed = true;
let s = this.ev("response.function_call_arguments.done", {
item_id: st.id,
output_index: st.outputIndex,
arguments: st.arguments,
});
s += this.ev("response.output_item.done", {
output_index: st.outputIndex,
item: { id: st.id, type: "function_call", status: "completed", call_id: st.id, name: st.name, arguments: st.arguments },
});
return s;
}
// 喂一个 chat 流 chunk(已解析的 JSON),返回要写给 Codex 的 Responses 事件串
onChunk(obj) {
let out = "";
if (!this.started) {
out += this.start();
this.started = true;
}
if (obj.usage) this.usage = mapUsage(obj.usage);
const choice = obj.choices && obj.choices[0];
if (!choice) return out;
const delta = choice.delta || {};
// 推理内容(reasoning_content)→ 思考事件
const rc = delta.reasoning_content || delta.reasoning;
if (rc) {
out += this.openReason();
this.reasonText += rc;
out += this.ev("response.reasoning_summary_text.delta", {
item_id: this.reasonId,
output_index: this.reasonOutputIndex,
summary_index: 0,
delta: rc,
});
}
if (delta.content) {
out += this.closeReason(); // 有正文了,先收尾推理
out += this.openMsg();
this.msgText += delta.content;
out += this.ev("response.output_text.delta", {
item_id: this.msgId,
output_index: this.msgOutputIndex,
content_index: 0,
delta: delta.content,
logprobs: [],
});
}
if (Array.isArray(delta.tool_calls)) {
for (const tc of delta.tool_calls) out += this.onToolDelta(tc);
}
return out;
}
finish() {
let out = "";
if (!this.started) {
out += this.start();
this.started = true;
}
out += this.closeReason();
out += this.closeMsg();
const sts = Object.values(this.tools).sort((a, b) => a.outputIndex - b.outputIndex);
for (const st of sts) out += this.closeTool(st);
const output = [];
if (this.reasonOpen)
output.push({ id: this.reasonId, type: "reasoning", summary: [{ type: "summary_text", text: this.reasonText }] });
if (this.msgOpen)
output.push({
id: this.msgId,
type: "message",
status: "completed",
role: "assistant",
content: [{ type: "output_text", text: this.msgText, annotations: [] }],
});
for (const st of sts)
output.push({ id: st.id, type: "function_call", status: "completed", call_id: st.id, name: st.name, arguments: st.arguments });
out += this.ev("response.completed", { response: this.baseResponse("completed", output) });
return out;
}
}
module.exports = { responsesToChat, ChatToResponses };