fix: Codex 自动压缩不触发 —— 接管时链接模型目录(绝对路径)进 config.toml (hot 1.5.83)
This commit is contained in:
+208
-10
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user