feat: model-caller v2.0 — 云雾API主用+DeepSeek备份 · 自动故障切换 · 钥匙已统一为ZY_LLM_API_KEY
This commit is contained in:
parent
6cb03400a5
commit
584ac6d2ea
@ -1,36 +1,86 @@
|
|||||||
// ═══════════════════════════════════════
|
// ═══════════════════════════════════════
|
||||||
// 铸渊开发Agent · 大模型调用模块
|
// HLDP-ZY://agents/zhuyuan-dev-agent/modules/model-caller
|
||||||
// 支持 OpenAI 兼容 API → DeepSeek/千问/等
|
// @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 https = require("https");
|
||||||
|
const http = require("http");
|
||||||
|
|
||||||
function getConfig() {
|
// ═══ API 源配置 ═══
|
||||||
const key = process.env.MODEL_API_KEY;
|
const API_SOURCES = [
|
||||||
const base = process.env.MODEL_API_BASE || "https://api.deepseek.com/v1";
|
{
|
||||||
const model = process.env.MODEL_NAME || "deepseek-chat";
|
name: "yunwu",
|
||||||
|
base: process.env.YUNWU_BASE_URL || "https://api.yunwu.ai/v1",
|
||||||
if (!key) {
|
key: process.env.ZY_LLM_API_KEY || process.env.MODEL_API_KEY,
|
||||||
console.warn("[ModelCaller] MODEL_API_KEY 未配置");
|
model: process.env.YUNWU_MODEL || "deepseek-chat",
|
||||||
return null;
|
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
|
||||||
}
|
}
|
||||||
return { key, base, model };
|
];
|
||||||
|
|
||||||
|
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 源
|
||||||
* @param {string} prompt - 输入提示
|
|
||||||
* @param {object} opts - { max_tokens, temperature }
|
|
||||||
* @returns {Promise<string>} 模型回复文本
|
|
||||||
*/
|
*/
|
||||||
async function call(prompt, opts = {}) {
|
function callAPI(source, prompt, opts) {
|
||||||
const config = getConfig();
|
|
||||||
if (!config) throw new Error("MODEL_API_KEY 未配置");
|
|
||||||
|
|
||||||
const body = JSON.stringify({
|
const body = JSON.stringify({
|
||||||
model: config.model,
|
model: source.model,
|
||||||
messages: [
|
messages: [
|
||||||
{ role: "system", content: "你是一个技术开发Agent。回答要精确、结构化、可执行。" },
|
{ role: "system", content: "你是一个技术开发Agent。回答要精确、结构化、可执行。使用中文。" },
|
||||||
{ role: "user", content: prompt }
|
{ role: "user", content: prompt }
|
||||||
],
|
],
|
||||||
max_tokens: opts.max_tokens || 4096,
|
max_tokens: opts.max_tokens || 4096,
|
||||||
@ -38,14 +88,19 @@ async function call(prompt, opts = {}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const url = new URL(config.base + "/chat/completions");
|
const url = new URL(source.base + "/chat/completions");
|
||||||
const req = https.request({
|
const lib = url.protocol === "https:" ? https : http;
|
||||||
|
|
||||||
|
const req = lib.request({
|
||||||
hostname: url.hostname,
|
hostname: url.hostname,
|
||||||
path: url.pathname,
|
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
||||||
|
path: url.pathname + url.search,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
timeout: source.timeout,
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": "Bearer " + config.key,
|
"Authorization": "Bearer " + source.key,
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json",
|
||||||
|
"Content-Length": Buffer.byteLength(body)
|
||||||
}
|
}
|
||||||
}, (res) => {
|
}, (res) => {
|
||||||
let data = "";
|
let data = "";
|
||||||
@ -53,18 +108,43 @@ async function call(prompt, opts = {}) {
|
|||||||
res.on("end", () => {
|
res.on("end", () => {
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(data);
|
const json = JSON.parse(data);
|
||||||
if (json.error) return reject(new Error(json.error.message || "API错误"));
|
if (json.error) {
|
||||||
resolve(json.choices?.[0]?.message?.content || "");
|
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) {
|
} catch (e) {
|
||||||
reject(new Error("解析失败: " + data.slice(0, 200)));
|
reject(new Error(source.name + " 解析失败: " + data.slice(0, 200)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on("error", reject);
|
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.write(body);
|
||||||
req.end();
|
req.end();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { call };
|
/**
|
||||||
|
* 获取当前状态
|
||||||
|
*/
|
||||||
|
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 };
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user