151 lines
4.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ═══════════════════════════════════════
// HLDP-ZY://agents/zhuyuan-dev-agent/modules/model-caller
// @guardian: ICE-GL-ZY001 · 铸渊
// @sovereign: TCS-0002∞ · 冰朔
// @copyright: 国作登字-2026-A-00037559
// ═══════════════════════════════════════
// 模型调用模块 v2.0 — 云雾API主用 + DeepSeek官方备份
// 主: https://api.yunwu.ai/v1 (OpenAI兼容)
// 备: https://api.deepseek.com/v1 (OpenAI兼容)
// 失败自动切换,不做数据丢失
// ═══════════════════════════════════════
const https = require("https");
const http = require("http");
// ═══ API 源配置 ═══
const API_SOURCES = [
{
name: "yunwu",
base: process.env.YUNWU_BASE_URL || "https://api.yunwu.ai/v1",
key: process.env.ZY_LLM_API_KEY || process.env.MODEL_API_KEY,
model: process.env.YUNWU_MODEL || "deepseek-chat",
timeout: 60000
},
{
name: "deepseek",
base: process.env.DEEPSEEK_BASE_URL || "https://api.deepseek.com/v1",
key: process.env.DEEPSEEK_API_KEY || process.env.ZY_LLM_API_KEY,
model: process.env.DEEPSEEK_MODEL || "deepseek-chat",
timeout: 60000
}
];
let currentSourceIndex = 0;
let sourceFailCounts = {};
/**
* 调用大模型 — 主源失败自动切备份
*/
async function call(prompt, opts = {}) {
const maxTries = API_SOURCES.length;
let lastError = null;
for (let attempt = 0; attempt < maxTries; attempt++) {
const source = API_SOURCES[(currentSourceIndex + attempt) % API_SOURCES.length];
if (!source.key) {
console.warn("[ModelCaller] " + source.name + " 未配置key跳过");
continue;
}
try {
console.log("[ModelCaller] 调用 " + source.name + " · 模型 " + source.model);
const result = await callAPI(source, prompt, opts);
console.log("[ModelCaller] " + source.name + " 返回 " + result.length + " 字符");
// 成功 → 记录此源可用
sourceFailCounts[source.name] = 0;
return result;
} catch (err) {
console.warn("[ModelCaller] " + source.name + " 失败: " + err.message);
sourceFailCounts[source.name] = (sourceFailCounts[source.name] || 0) + 1;
lastError = err;
// 切换到下一个源
if (attempt < maxTries - 1) {
console.log("[ModelCaller] 切换备用源...");
}
}
}
throw new Error("所有API源均失败。最后错误: " + (lastError ? lastError.message : "未知"));
}
/**
* 调用单个 API 源
*/
function callAPI(source, prompt, opts) {
const body = JSON.stringify({
model: source.model,
messages: [
{ role: "system", content: "你是一个技术开发Agent。回答要精确、结构化、可执行。使用中文。" },
{ role: "user", content: prompt }
],
max_tokens: opts.max_tokens || 4096,
temperature: opts.temperature || 0.3
});
return new Promise((resolve, reject) => {
const url = new URL(source.base + "/chat/completions");
const lib = url.protocol === "https:" ? https : http;
const req = lib.request({
hostname: url.hostname,
port: url.port || (url.protocol === "https:" ? 443 : 80),
path: url.pathname + url.search,
method: "POST",
timeout: source.timeout,
headers: {
"Authorization": "Bearer " + source.key,
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body)
}
}, (res) => {
let data = "";
res.on("data", (c) => data += c);
res.on("end", () => {
try {
const json = JSON.parse(data);
if (json.error) {
reject(new Error(source.name + " API错误: " + (json.error.message || JSON.stringify(json.error))));
return;
}
const content = json.choices?.[0]?.message?.content || "";
if (!content) {
reject(new Error(source.name + " 空回复"));
return;
}
resolve(content);
} catch (e) {
reject(new Error(source.name + " 解析失败: " + data.slice(0, 200)));
}
});
});
req.on("timeout", () => {
req.destroy();
reject(new Error(source.name + " 请求超时 " + source.timeout + "ms"));
});
req.on("error", (e) => reject(new Error(source.name + " 网络错误: " + e.message)));
req.write(body);
req.end();
});
}
/**
* 获取当前状态
*/
function getStatus() {
return {
primary: API_SOURCES[0].name,
backup: API_SOURCES[1] ? API_SOURCES[1].name : "none",
failCounts: sourceFailCounts,
currentIndex: currentSourceIndex
};
}
module.exports = { call, getStatus };