fix: Codex 自动压缩不触发 —— 接管时链接模型目录(绝对路径)进 config.toml (hot 1.5.83)

This commit is contained in:
mac
2026-08-16 12:45:32 +08:00
parent e9a261482c
commit 1b687c01e9
50 changed files with 10885 additions and 1161 deletions
+208 -10
View File
@@ -22,18 +22,51 @@ function uniqStrings(list) {
return Array.from(new Set((list || []).map((v) => String(v || "").trim()).filter(Boolean)));
}
function groupText(g) {
return `${(g && g.name) || ""} ${(g && g.model) || ""}`.toLowerCase();
}
function isSeedanceV2Model(model) {
return /^(?:bh2\.0-|sd2\.0-|sdvip|gz-sd)/i.test(String(model || "").trim());
}
function isSeedanceV2Group(g) {
const text = groupText(g);
return /seedance[\s_-]*v\s*2|seedancev2|seedance_v2/.test(text) || isSeedanceV2Model(g && g.model);
}
function isSeedanceGroup(g) {
return /seedance/.test(groupText(g));
}
function isGrokGroup(g) {
return /grok/.test(groupText(g));
}
function modelCandidatesForGroup(g) {
const out = [];
if (!g) return out;
if (g.model) out.push(g.model);
if (Array.isArray(g.oneMillionModels)) out.push(...g.oneMillionModels);
const name = String(g.name || "").toLowerCase();
const model = String(g.model || "").toLowerCase();
if (/grok/.test(name) || /grok/.test(model)) {
if (isGrokGroup(g)) {
out.push("grok-video-1.5", "grok-imagine-video-1.5", "grok-imagine-video");
}
if (/seedance/.test(name) || /seedance/.test(model)) {
if (isSeedanceV2Group(g)) {
out.push(
"bh2.0-fast-720p",
"bh2.0-720p",
"bh2.0-1080p",
"bh2.0-mini-720p",
"SD2.0-720P",
"SD2.0-720P-fast",
"SD2.0-1080P",
"sdvip720p",
"sdvip1080p",
"gz-sd720p",
"gz-sd1080p"
);
} else if (isSeedanceGroup(g)) {
out.push("seedance2.0", "seedance-2.0");
}
return uniqStrings(out);
@@ -283,9 +316,10 @@ async function resolveMat(v) {
throw new Error("素材需为 data URL 或公网 http(s) 地址");
}
// 当前中转是否可用:ready = 有可用 llmGroup。host 从 baseUrl 解析域名。
// 当前中转是否可用:ready = 有可用分组。画布节点会按 groupId 显式选用插件专用分组。
function status() {
const llm = router.llmGroup();
const usable = router.usableGroups();
const llm = router.llmGroup() || (usable && usable[0]);
if (!llm) return { ready: false, provider: null, reason: "未配置可用中转分组" };
return {
ready: true,
@@ -836,17 +870,161 @@ async function runGrokVideoLegacy(base, llm, params) {
}
}
// —— Seedance V2(Binghuo 原生):POST /v1/video/generations + GET /v1/video/generations/:task_id ——
// 对应 sub2api platform=seedance_v2,与 CLMM /v1/videos 契约分离,避免字段被改写。
function seedanceV2EndpointPath(endpointPath) {
const path = String(endpointPath || "").trim();
if (!path) return "/v1/video/generations";
// 若用户仍填 CLMM 路径,也归一到 V2 原生路径(sub2api 两边都能进同一 handler,但原生更稳)
if (/\/v1\/videos(?:\/generations)?$/i.test(path) || /\/videos(?:\/generations)?$/i.test(path)) {
return "/v1/video/generations";
}
return path;
}
function seedanceV2DurationSeconds(params, model) {
const raw = Number.parseInt(String((params && params.seconds) || ""), 10);
if (Number.isInteger(raw) && raw > 0) return raw;
const fixed = String(model || "").match(/(?:^|[-_])(\d+)s(?:$|[-_])/i);
if (fixed) return Number(fixed[1]) || 5;
return 5;
}
async function runSeedanceV2(base, llm, params) {
const model = String(params.model || "").trim();
const prompt = String(params.prompt || "").trim();
if (!model) return { ok: false, error: "Seedance V2 必须选择模型完整名称" };
if (!prompt) return { ok: false, error: "Seedance V2 必须填写提示词" };
let mode = String(params.mode || "text");
if (mode === "image") mode = "first";
const body = {
model,
prompt,
duration: seedanceV2DurationSeconds(params, model),
ratio: String(params.ratio || params.aspectRatio || "16:9"),
resolution: clmmResolutionFromParamsV2(params),
};
try {
if (mode === "first") {
const image = await resolveMat(params.imageUrl || params.image);
if (!image) return { ok: false, error: "Seedance V2 首帧图生视频需要首帧图" };
body.start_frame = [image];
} else if (mode === "firstlast") {
const first = await resolveMat(params.imageUrl || params.image);
const last = await resolveMat(params.lastFrameImage);
if (!first || !last) return { ok: false, error: "首尾帧模式需要首帧和尾帧两张图片" };
body.start_frame = [first];
body.end_frame = [last];
} else if (mode === "reference") {
const materials = Array.isArray(params.referenceImages) ? params.referenceImages : [];
const refs = (await Promise.all(materials.map((item) => resolveMat(item)))).filter(Boolean);
if (!refs.length) return { ok: false, error: "多参考图模式至少需要 1 张参考图" };
body.images = refs;
} else if (mode !== "text") {
return { ok: false, error: "Seedance V2 暂不支持参考视频模式,请改用 文生 / 首帧 / 多参考图" };
}
const createUrl = joinUrl(base, seedanceV2EndpointPath(params.endpointPath));
const debugBase = {
base: hostOf(base),
create_url: createUrl,
mode,
request: {
model: body.model,
duration: body.duration,
ratio: body.ratio,
resolution: body.resolution,
image_count: Array.isArray(body.images) ? body.images.length : 0,
has_start_frame: Array.isArray(body.start_frame) && body.start_frame.length > 0,
has_end_frame: Array.isArray(body.end_frame) && body.end_frame.length > 0,
},
};
seedanceDebugLog({ type: "seedance_v2_video_create", ...debugBase });
const cres = await fetch(createUrl, {
method: "POST",
headers: headers(llm.key),
body: JSON.stringify(body),
});
const rawCreate = await cres.text().catch(() => "");
if (!cres.ok) {
seedanceDebugLog({ type: "seedance_v2_video_create_failed", ...debugBase, status: cres.status, detail: rawCreate.slice(0, 500) });
return { ok: false, error: `Seedance V2 建任务失败(${cres.status}):POST ${createUrl} → ${rawCreate.slice(0, 300)}` };
}
let data;
try {
data = JSON.parse(rawCreate);
} catch {
seedanceDebugLog({ type: "seedance_v2_video_create_non_json", ...debugBase, detail: rawCreate.slice(0, 500) });
return { ok: false, error: `Seedance V2 建任务返回非 JSON:${rawCreate.slice(0, 300)}` };
}
const taskId = pickTaskId(data);
if (!taskId) {
seedanceDebugLog({ type: "seedance_v2_video_missing_task_id", ...debugBase, response: JSON.stringify(data).slice(0, 500) });
return { ok: false, error: `Seedance V2 建任务未返回 task_id:${JSON.stringify(data).slice(0, 300)}` };
}
seedanceDebugLog({ type: "seedance_v2_video_accepted", ...debugBase, task_id: taskId });
// 原生路径:/v1/video/generations/:task_id
// 若 createUrl 是 /v1/videos,则沿 createUrl/:id 轮询(sub2api 也会路由到 V2 status)
const pollUrl = /\/generations$/i.test(createUrl.replace(/\/$/, ""))
? createUrl.replace(/\/$/, "") + "/" + encodeURIComponent(taskId)
: createUrl.replace(/\/$/, "") + "/" + encodeURIComponent(taskId);
const pollHeaders = { Authorization: "Bearer " + (llm.key || ""), Accept: "application/json", "User-Agent": "codex-market" };
let lastPoll = "";
for (let i = 0; i < 300; i++) {
await sleep(i === 0 ? 1000 : 3000);
try {
const pr = await fetch(pollUrl, { headers: pollHeaders });
const rawPoll = await pr.text().catch(() => "");
if (!pr.ok) {
lastPoll = `HTTP ${pr.status}: ${rawPoll.slice(0, 200)}`;
continue;
}
const pd = JSON.parse(rawPoll);
const d = pd && pd.data && typeof pd.data === "object" && !Array.isArray(pd.data) ? pd.data : pd;
const st = normalizeVideoStatus((d && (d.status || d.state)) || (pd && pd.status));
lastPoll = "status=" + st;
if (st === "succeeded") {
const url = clmmVideoUrlV2(pd) || clmmVideoUrlV2(d);
if (!url) return { ok: false, error: `Seedance V2 任务完成但未找到视频地址:${JSON.stringify(pd).slice(0, 300)}` };
const b64 = await downloadAsBase64(url, trustedDownloadHeaders(url, llm));
seedanceDebugLog({ type: "seedance_v2_video_done", ...debugBase, task_id: taskId, url });
return { ok: true, mime: "video/mp4", dataUrl: toDataUrl("video/mp4", b64), url, _model: model };
}
if (st === "failed") {
const err = (d && d.error && (d.error.message || d.error)) || (d && (d.fail_reason || d.message || d.reason)) || (pd && pd.message) || st;
seedanceDebugLog({ type: "seedance_v2_video_failed", ...debugBase, task_id: taskId, status: st, response: rawPoll.slice(0, 500) });
return { ok: false, error: `Seedance V2 任务失败:${typeof err === "string" ? err : JSON.stringify(err)}` };
}
} catch (e) {
lastPoll = "请求异常:" + String((e && e.message) || e);
}
}
seedanceDebugLog({ type: "seedance_v2_video_timeout", ...debugBase, task_id: taskId, last_poll: lastPoll || "" });
return { ok: false, error: `Seedance V2 轮询 15 分钟未完成(task_id=${taskId},最近一次:${lastPoll || "无"})` };
} catch (e) {
return { ok: false, error: "Seedance V2 视频生成异常:" + String((e && e.message) || e) };
}
}
// —— CLMM 视频统一适配:Seedance/Grok 节点都通过中转的 /videos 契约提交 ——
// 文档契约:POST /videos,字段使用 aspect_ratio/resolution/size/seconds/mySeconds/reference_image_urls。
function clmmResolutionFromParamsV2(params) {
const value = String(params.resolution || params.vsize || "720p").trim();
return /^(480|720|1080)p$/i.test(value) ? value : "720p";
// 2K/4K 由中转和具体模型决定是否可用;客户端只负责保留并透传。
return /^(480|720|1080)p$/i.test(value) || /^(2|4)k$/i.test(value) ? value.toLowerCase() : "720p";
}
function clmmSizeFromParamsV2(params, resolution) {
const ratio = String(params.aspectRatio || params.ratio || "16:9").trim();
const match = ratio.match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);
const shortSide = /1080/i.test(resolution) ? 1080 : /480/i.test(resolution) ? 480 : 720;
// 采用常见视频尺寸:2K=2560x1440,4K=3840x2160(横屏基准短边)。
const shortSide = /4k/i.test(resolution) ? 2160 : /2k/i.test(resolution) ? 1440 : /1080/i.test(resolution) ? 1080 : /480/i.test(resolution) ? 480 : 720;
if (!match) return `${Math.round(shortSide * 16 / 9)}x${shortSide}`;
const rw = Number(match[1]);
const rh = Number(match[2]);
@@ -1016,7 +1194,14 @@ async function run({ kind, params }, onPartial) {
let r;
if (kind === "image") r = await runImage(base, g, p, onPartial);
else if (kind === "tts") r = await runTts(base, g, p);
else if (kind === "video") r = await runClmmVideoV2(base, g, p, seedanceDebugLog, "Seedance");
else if (kind === "video") {
// Seedance V2 分组走 Binghuo 原生;旧 Seedance/CLMM 分组继续走 /v1/videos
const forceV2 = isSeedanceV2Model(p.model) || /seedance[\s_-]*v\s*2|seedancev2|\/v1\/video\/generations/i.test(
String(p.endpointPath || "") + " " + String(g.name || "") + " " + String(g.model || "")
);
if (forceV2 || isSeedanceV2Group(g)) r = await runSeedanceV2(base, g, p);
else r = await runClmmVideoV2(base, g, p, seedanceDebugLog, "Seedance");
}
else if (kind === "grokVideo") r = await runClmmVideoV2(base, g, p, grokDebugLog, "Grok");
else return { ok: false, error: "未知的画布任务类型:" + kind };
const model = (r && r._model) || p.model || g.model;
@@ -1043,6 +1228,11 @@ function groups() {
model: g.model || "",
models: modelCandidatesForGroup(g),
host: hostOf(g.baseUrl),
codexImage: !!g.codexImage,
codexVideo: !!g.codexVideo,
seedanceV2: isSeedanceV2Group(g),
seedance: isSeedanceGroup(g),
grok: isGrokGroup(g),
})),
};
}
@@ -1066,4 +1256,12 @@ async function listModels(groupId) {
}
}
module.exports = { status, groups, run, listModels, downloadAsBase64 };
module.exports = {
status,
groups,
run,
listModels,
downloadAsBase64,
clmmResolutionFromParamsV2,
clmmSizeFromParamsV2,
};