From bc36f6599e989578ecf56cc81cff833f6e9b2cc0 Mon Sep 17 00:00:00 2001 From: zhishi <1951671751@qq.com> Date: Sat, 28 Feb 2026 20:12:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=85=A5grsai?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/initDB.ts | 66 ++++++++++++++++++ src/router.ts | 56 ++++++++-------- src/routes/assets/generateAssets.ts | 1 + src/routes/other/testAI.ts | 1 - src/routes/storyboard/delStoryboard.ts | 1 - src/utils/ai/image/index.ts | 2 + src/utils/ai/image/owned/grsai.ts | 92 ++++++++++++++++++++++++++ src/utils/ai/image/owned/modelScope.ts | 1 - src/utils/ai/image/owned/runninghub.ts | 3 +- src/utils/ai/image/owned/volcengine.ts | 4 +- src/utils/ai/text/index.ts | 4 +- src/utils/ai/text/modelList.ts | 3 +- src/utils/ai/video/index.ts | 5 +- src/utils/ai/video/owned/grsai.ts | 76 +++++++++++++++++++++ src/utils/ai/video/owned/other.ts | 49 ++++++++------ 15 files changed, 306 insertions(+), 58 deletions(-) create mode 100644 src/utils/ai/image/owned/grsai.ts create mode 100644 src/utils/ai/video/owned/grsai.ts diff --git a/src/lib/initDB.ts b/src/lib/initDB.ts index 9672b57..03542e2 100644 --- a/src/lib/initDB.ts +++ b/src/lib/initDB.ts @@ -644,6 +644,11 @@ export default async (knex: Knex, forceInit: boolean = false): Promise => { manufacturer: "vidu", model: "viduq2", grid: 0, type: "ti2i" }, { manufacturer: "runninghub", model: "nanobanana", grid: 1, type: "ti2i" }, { manufacturer: "modelScope", model: "Qwen/Qwen-Image", grid: 1, type: "ti2i" }, + { manufacturer: "grsai", model: "nano-banana-fast", grid: 1, type: "ti2i" }, + { manufacturer: "grsai", model: "nano-banana-pro", grid: 1, type: "ti2i" }, + { manufacturer: "grsai", model: "nano-banana", grid: 1, type: "ti2i" }, + { manufacturer: "grsai", model: "nano-banana-2", grid: 1, type: "ti2i" }, + ]); }, }, @@ -1103,6 +1108,67 @@ export default async (knex: Knex, forceInit: boolean = false): Promise => audio: 0, type: JSON.stringify(["singleImage", "text"]), }, + { + id: 48, + manufacturer: "grsai", + model: "sora-2", + durationResolutionMap: JSON.stringify([{ duration: [10, 15], resolution: [] }]), + aspectRatio: JSON.stringify(["16:9", "9:16"]), + audio: 0, + type: JSON.stringify(["singleImage", "text"]), + }, + { + id: 49, + manufacturer: "grsai", + model: "veo3.1-pro", + durationResolutionMap: JSON.stringify([]), + aspectRatio: JSON.stringify(["16:9", "9:16"]), + audio: 0, + type: JSON.stringify(["startEndRequired", "text"]), + }, + + { + id: 50, + manufacturer: "grsai", + model: "veo3.1-pro-1080p", + durationResolutionMap: JSON.stringify([]), + aspectRatio: JSON.stringify(["16:9", "9:16"]), + audio: 0, + type: JSON.stringify(["startEndRequired", "text"]), + },{ + id: 51, + manufacturer: "grsai", + model: "veo3.1-pro-4k", + durationResolutionMap: JSON.stringify([]), + aspectRatio: JSON.stringify(["16:9", "9:16"]), + audio: 0, + type: JSON.stringify(["startEndRequired", "text"]), + },{ + id: 52, + manufacturer: "grsai", + model: "veo3.1-fast", + durationResolutionMap: JSON.stringify([]), + aspectRatio: JSON.stringify(["16:9", "9:16"]), + audio: 0, + type: JSON.stringify(["startEndRequired", "text"]), + }, + { + id: 53, + manufacturer: "grsai", + model: "veo3.1-fast-1080p", + durationResolutionMap: JSON.stringify([]), + aspectRatio: JSON.stringify(["16:9", "9:16"]), + audio: 0, + type: JSON.stringify(["startEndRequired", "text"]), + },{ + id: 54, + manufacturer: "grsai", + model: "veo3.1-fast-4k", + durationResolutionMap: JSON.stringify([]), + aspectRatio: JSON.stringify(["16:9", "9:16"]), + audio: 0, + type: JSON.stringify(["startEndRequired", "text"]), + }, ]); }, }, diff --git a/src/router.ts b/src/router.ts index 5830e05..cd4946e 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,4 +1,4 @@ -// @routes-hash e13311bd461bd8de8701383106557048 +// @routes-hash c97cf72361299980ea4b0c43549a0de8 import { Express } from "express"; import route1 from "./routes/assets/addAssets"; @@ -69,19 +69,20 @@ import route65 from "./routes/storyboard/uploadImage"; import route66 from "./routes/task/getTaskApi"; import route67 from "./routes/task/taskDetails"; import route68 from "./routes/user/getUser"; -import route69 from "./routes/video/addVideo"; -import route70 from "./routes/video/addVideoConfig"; -import route71 from "./routes/video/deleteVideoConfig"; -import route72 from "./routes/video/generatePrompt"; -import route73 from "./routes/video/generateVideo"; -import route74 from "./routes/video/getManufacturer"; -import route75 from "./routes/video/getVideo"; -import route76 from "./routes/video/getVideoConfigs"; -import route77 from "./routes/video/getVideoModel"; -import route78 from "./routes/video/getVideoStoryboards"; -import route79 from "./routes/video/reviseVideoStoryboards"; -import route80 from "./routes/video/saveVideo"; -import route81 from "./routes/video/upDateVideoConfig"; +import route69 from "./routes/user/saveUser"; +import route70 from "./routes/video/addVideo"; +import route71 from "./routes/video/addVideoConfig"; +import route72 from "./routes/video/deleteVideoConfig"; +import route73 from "./routes/video/generatePrompt"; +import route74 from "./routes/video/generateVideo"; +import route75 from "./routes/video/getManufacturer"; +import route76 from "./routes/video/getVideo"; +import route77 from "./routes/video/getVideoConfigs"; +import route78 from "./routes/video/getVideoModel"; +import route79 from "./routes/video/getVideoStoryboards"; +import route80 from "./routes/video/reviseVideoStoryboards"; +import route81 from "./routes/video/saveVideo"; +import route82 from "./routes/video/upDateVideoConfig"; export default async (app: Express) => { app.use("/assets/addAssets", route1); @@ -152,17 +153,18 @@ export default async (app: Express) => { app.use("/task/getTaskApi", route66); app.use("/task/taskDetails", route67); app.use("/user/getUser", route68); - app.use("/video/addVideo", route69); - app.use("/video/addVideoConfig", route70); - app.use("/video/deleteVideoConfig", route71); - app.use("/video/generatePrompt", route72); - app.use("/video/generateVideo", route73); - app.use("/video/getManufacturer", route74); - app.use("/video/getVideo", route75); - app.use("/video/getVideoConfigs", route76); - app.use("/video/getVideoModel", route77); - app.use("/video/getVideoStoryboards", route78); - app.use("/video/reviseVideoStoryboards", route79); - app.use("/video/saveVideo", route80); - app.use("/video/upDateVideoConfig", route81); + app.use("/user/saveUser", route69); + app.use("/video/addVideo", route70); + app.use("/video/addVideoConfig", route71); + app.use("/video/deleteVideoConfig", route72); + app.use("/video/generatePrompt", route73); + app.use("/video/generateVideo", route74); + app.use("/video/getManufacturer", route75); + app.use("/video/getVideo", route76); + app.use("/video/getVideoConfigs", route77); + app.use("/video/getVideoModel", route78); + app.use("/video/getVideoStoryboards", route79); + app.use("/video/reviseVideoStoryboards", route80); + app.use("/video/saveVideo", route81); + app.use("/video/upDateVideoConfig", route82); } diff --git a/src/routes/assets/generateAssets.ts b/src/routes/assets/generateAssets.ts index 3cdc1c3..883416e 100644 --- a/src/routes/assets/generateAssets.ts +++ b/src/routes/assets/generateAssets.ts @@ -124,6 +124,7 @@ export default router.post( assetsId: id, }); const apiConfig = await u.getPromptAi("assetsImage"); + try { const contentStr = await u.ai.image( { diff --git a/src/routes/other/testAI.ts b/src/routes/other/testAI.ts index 8296f83..4fe5d85 100644 --- a/src/routes/other/testAI.ts +++ b/src/routes/other/testAI.ts @@ -48,7 +48,6 @@ export default router.post( ); res.status(200).send(success(reply)); } catch (err) { - console.log("%c Line:51 🥟 err", "background:#e41a6a", err); const msg = u.error(err).message; console.error(msg); res.status(500).send(error(msg)); diff --git a/src/routes/storyboard/delStoryboard.ts b/src/routes/storyboard/delStoryboard.ts index 4f467c3..7045b4b 100644 --- a/src/routes/storyboard/delStoryboard.ts +++ b/src/routes/storyboard/delStoryboard.ts @@ -12,7 +12,6 @@ export default router.post( }), async (req, res) => { const { id } = req.body; - console.log("%c Line:15 🍕 id", "background:#f5ce50", id); await u.db("t_assets").where("id", id).delete(); res.status(200).send(success("分镜删除成功")); }, diff --git a/src/utils/ai/image/index.ts b/src/utils/ai/image/index.ts index 3826c08..ae1dfdf 100644 --- a/src/utils/ai/image/index.ts +++ b/src/utils/ai/image/index.ts @@ -11,6 +11,7 @@ import apimart from "./owned/apimart"; import other from "./owned/other"; import gemini from "./owned/gemini"; import modelScope from "./owned/modelScope"; +import grsai from "./owned/grsai"; const urlToBase64 = async (url: string): Promise => { const res = await axios.get(url, { responseType: "arraybuffer" }); @@ -28,6 +29,7 @@ const modelInstance = { // apimart: apimart, modelScope, other, + grsai } as const; export default async (input: ImageConfig, config: AIConfig) => { diff --git a/src/utils/ai/image/owned/grsai.ts b/src/utils/ai/image/owned/grsai.ts new file mode 100644 index 0000000..4bc2b10 --- /dev/null +++ b/src/utils/ai/image/owned/grsai.ts @@ -0,0 +1,92 @@ +import axios from "axios"; +import u from "@/utils"; +import { pollTask } from "@/utils/ai/utils"; +function getApiUrl(apiUrl: string) { + if (apiUrl.includes("|")) { + const parts = apiUrl.split("|"); + if (parts.length !== 2 || !parts[0].trim() || !parts[1].trim()) { + throw new Error("url 格式错误,请使用 url1|url2 格式"); + } + return { requestUrl: parts[0].trim(), queryUrl: parts[1].trim() }; + } + throw new Error("请填写正确的url"); +} +function template(replaceObj: Record, url: string) { + return url.replace(/\{(\w+)\}/g, (match, varName) => { + return replaceObj.hasOwnProperty(varName) ? replaceObj[varName] : match; + }); +} +async function processImages(imageBase64: string[]): Promise> { + let images = imageBase64.filter((img) => img?.trim()); + if (images.length === 0) return []; + + // 压缩所有图片到10MB以内 + images = await Promise.all(images.map((img) => u.imageTools.compressImage(img, "10mb"))); + + // 参考主体数量和参考图片数量之和不得超过10 + if (images.length > 6) { + const mergeImageList = images.splice(5); + const mergedImage = await u.imageTools.mergeImages(mergeImageList, "10mb"); + images.push(mergedImage); + } + + return images; +} + +export default async (input: ImageConfig, config: AIConfig): Promise => { + if (!config.apiKey) throw new Error("缺少API Key"); + const apiKey = config.apiKey.replace("Bearer ", ""); + + const defaultBaseURL = "https://grsai.dakka.com.cn/v1/draw/nano-banana|https://grsai.dakka.com.cn/v1/draw/result"; + + const { requestUrl, queryUrl } = getApiUrl(config.baseURL! || defaultBaseURL); + // 构建完整的提示词 + const fullPrompt = input.systemPrompt ? `${input.systemPrompt}\n\n${input.prompt}` : input.prompt; + + let mergedImage = await processImages(input.imageBase64 || []); + + const taskBody: Record = { + model: config.model, + prompt: fullPrompt, + imageSize: input.size, + aspectRatio: input.aspectRatio, + ...(mergedImage && mergedImage.length ? { urls: mergedImage } : {}), + webHook: "-1", + }; + + try { + + const { data } = await axios.post(requestUrl, taskBody, { headers: { Authorization: `Bearer ${apiKey}` } }); + + + if (data.code != 0) throw new Error(`任务提交失败: ${data ? JSON.stringify(data, null, 2) : "未知错误"}`); + + return await pollTask(async () => { + const { data: queryData } = await axios.post( + queryUrl, + { + id: data.data.id, + }, + { + headers: { Authorization: `Bearer ${apiKey}` }, + }, + ); + + if (queryData.code != 0) throw new Error(`查询任务失败: ${queryData ? JSON.stringify(queryData, null, 2) : "未知错误"}`); + const { status, results, error, failure_reason } = queryData.data || {}; + + if (status === "failed") { + return { completed: false, error: failure_reason + "\n" + error || "图片生成失败" }; + } + + if (status === "succeeded") { + return { completed: true, url: results?.[0].url }; + } + + return { completed: false }; + }); + } catch (error: any) { + const msg = u.error(error).message || "图片生成失败"; + throw new Error(msg); + } +}; diff --git a/src/utils/ai/image/owned/modelScope.ts b/src/utils/ai/image/owned/modelScope.ts index 4a13dee..6b4cfad 100644 --- a/src/utils/ai/image/owned/modelScope.ts +++ b/src/utils/ai/image/owned/modelScope.ts @@ -119,7 +119,6 @@ export default async (input: ImageConfig, config: AIConfig): Promise => return { completed: false }; }); } catch (error: any) { - console.error("%c Line:90 🥪 error", "background:#93c0a4", error.response?.data?.errors?.message); const msg = u.error(error).message || "图片生成失败"; throw new Error(msg); } diff --git a/src/utils/ai/image/owned/runninghub.ts b/src/utils/ai/image/owned/runninghub.ts index 79a00d5..59b9f84 100644 --- a/src/utils/ai/image/owned/runninghub.ts +++ b/src/utils/ai/image/owned/runninghub.ts @@ -68,11 +68,12 @@ export default async (input: ImageConfig, config: AIConfig): Promise => const apiKey = config.apiKey.replace("Bearer ", ""); const baseURL = "https://www.runninghub.cn"; const imageUrls = await Promise.all(input.imageBase64.map((base64Image) => uploadBase64ToRunninghub(base64Image, apiKey, baseURL))); + const fullPrompt = input.systemPrompt ? `${input.systemPrompt}\n\n${input.prompt}` : input.prompt; const endpoint = input.imageBase64.length === 0 ? "/openapi/v2/rhart-image-n-pro/text-to-image" : "/openapi/v2/rhart-image-n-pro/edit"; const taskRes = await axios.post( `https://www.runninghub.cn${endpoint}`, - { prompt: input.prompt, resolution: input.size, aspectRatio: input.aspectRatio, ...(imageUrls.length > 0 && { imageUrls }) }, + { prompt: fullPrompt, resolution: input.size, aspectRatio: input.aspectRatio, ...(imageUrls.length > 0 && { imageUrls }) }, { headers: { Authorization: "Bearer " + apiKey } }, ); const taskId = taskRes.data.taskId; diff --git a/src/utils/ai/image/owned/volcengine.ts b/src/utils/ai/image/owned/volcengine.ts index 919dbfb..d6a0458 100644 --- a/src/utils/ai/image/owned/volcengine.ts +++ b/src/utils/ai/image/owned/volcengine.ts @@ -18,9 +18,11 @@ export default async (input: ImageConfig, config: AIConfig): Promise => "4K": "2304x4096", }, }; + const fullPrompt = input.systemPrompt ? `${input.systemPrompt}\n\n${input.prompt}` : input.prompt; + const body: Record = { model: config.model, - prompt: input.prompt, + prompt: fullPrompt, size: sizeMap[input.aspectRatio][size], response_format: "url", sequential_image_generation: "disabled", diff --git a/src/utils/ai/text/index.ts b/src/utils/ai/text/index.ts index 3dd97f7..a967cec 100644 --- a/src/utils/ai/text/index.ts +++ b/src/utils/ai/text/index.ts @@ -30,7 +30,7 @@ const buildOptions = async (input: AIInput, config: AIConfig = {}) => { if (manufacturer == "other") { owned = modelList.find((m) => m.manufacturer === manufacturer); } else { - owned = modelList.find((m) => m.model === model); + owned = modelList.find((m) => m.model === model && m.manufacturer === manufacturer); if (!owned) owned = modelList.find((m) => m.manufacturer === manufacturer); } if (!owned) throw new Error("不支持的厂商"); @@ -54,7 +54,7 @@ const buildOptions = async (input: AIInput, config: AIConfig = {}) => { }; const output = input.output ? (outputBuilders[owned.responseFormat]?.(input.output) ?? null) : null; - const chatModelManufacturer = ["volcengine", "other", "openai", "modelScope"]; + const chatModelManufacturer = ["volcengine", "other", "openai", "modelScope","grsai"]; const modelFn = chatModelManufacturer.includes(owned.manufacturer) ? (modelInstance as OpenAIProvider).chat(model!) : modelInstance(model!); return { diff --git a/src/utils/ai/text/modelList.ts b/src/utils/ai/text/modelList.ts index a3c102c..c80edfe 100644 --- a/src/utils/ai/text/modelList.ts +++ b/src/utils/ai/text/modelList.ts @@ -30,13 +30,12 @@ const instanceMap = { openai: createOpenAI, zhipu: createZhipu, qwen: createQwen, - gemini: createGoogleGenerativeAI, - anthropic: createAnthropic, modelScope: (options: OpenAIProviderSettings) => createOpenAI({ ...options, headers: { ...options?.headers, "X-ModelScope-Async-Mode": "true" } }), xai: createXai, other: createOpenAI, + grsai:createOpenAI }; const modelList: Owned[] = [ // DeepSeek diff --git a/src/utils/ai/video/index.ts b/src/utils/ai/video/index.ts index 1524b07..6a7d07e 100644 --- a/src/utils/ai/video/index.ts +++ b/src/utils/ai/video/index.ts @@ -11,7 +11,7 @@ import runninghub from "./owned/runninghub"; import gemini from "./owned/gemini"; import apimart from "./owned/apimart"; import other from "./owned/other"; - +import grsai from "./owned/grsai"; const modelInstance = { volcengine: volcengine, kling: kling, @@ -20,7 +20,8 @@ const modelInstance = { gemini: gemini, runninghub: runninghub, apimart: apimart, - // other: other, + other: other, + grsai:grsai } as const; export default async (input: VideoConfig, config?: AIConfig) => { diff --git a/src/utils/ai/video/owned/grsai.ts b/src/utils/ai/video/owned/grsai.ts new file mode 100644 index 0000000..3121c38 --- /dev/null +++ b/src/utils/ai/video/owned/grsai.ts @@ -0,0 +1,76 @@ +import "../type"; +import fs from "fs"; +import path from "path"; +import axios from "axios"; +import { pollTask, validateVideoConfig } from "@/utils/ai/utils"; + +const buildInlineImage = (data: string) => ({ inlineData: { mimeType: "image/png", data } }); + +export default async (input: VideoConfig, config: AIConfig) => { + if (!config.model) throw new Error("缺少Model名称"); + if (!config.apiKey) throw new Error("缺少API Key"); + + // const { owned, images, hasStartEndType } = validateVideoConfig(input, config); + + const defaultBaseUrl = ["https://grsai.dakka.com.cn/v1/video/{model}", "https://grsai.dakka.com.cn/v1/draw/result"].join("|"); + + const [submitUrl, queryUrl] = (config.baseURL || defaultBaseUrl).split("|"); + + const headers = { Authorization: "Bearer " + config.apiKey }; + let inputObj: Record = {}; + let taskUrl = submitUrl.replace("{model}", config.model); + if (config.model.includes("veo")) { + inputObj = { + model: config.model, + prompt: input.prompt, + firstFrameUrl: "", + lastFrameUrl: "", + aspectRatio: input.aspectRatio, + webHook: "-1", + }; + inputObj.firstFrameUrl = input.imageBase64?.[0] ?? ""; + inputObj.lastFrameUrl = input.imageBase64?.[1] ?? ""; + + taskUrl = submitUrl.replace("{model}", "veo"); + } else { + inputObj = { + model: config.model, + prompt: input.prompt, + url: "", + aspectRatio: input.aspectRatio, + duration: +input.duration, + webHook: "-1", + }; + inputObj.url = input.imageBase64?.[0] ?? ""; + + taskUrl = submitUrl.replace("{model}", "sora-video"); + } + + const { data } = await axios.post(taskUrl, { ...inputObj }, { headers: { ...headers, "Content-Type": "application/json" } }); + + if (data.code != 0) throw new Error(`任务提交失败: ${data ? JSON.stringify(data, null, 2) : "未知错误"}`); + + return await pollTask(async () => { + const { data: queryData } = await axios.post( + queryUrl, + { + id: data.data.id, + }, + { + headers: { Authorization: `Bearer ${config.apiKey}` }, + }, + ); + if (queryData.code != 0) throw new Error(`查询任务失败: ${queryData ? JSON.stringify(queryData, null, 2) : "未知错误"}`); + const { status, error, failure_reason, url } = queryData.data || {}; + + if (status === "failed") { + return { completed: false, error: failure_reason + "\n" + error || "图片生成失败" }; + } + + if (status === "succeeded") { + return { completed: true, url: url }; + } + + return { completed: false }; + }); +}; diff --git a/src/utils/ai/video/owned/other.ts b/src/utils/ai/video/owned/other.ts index 3d4004d..fdc2f86 100644 --- a/src/utils/ai/video/owned/other.ts +++ b/src/utils/ai/video/owned/other.ts @@ -4,12 +4,11 @@ import sharp from "sharp"; import FormData from "form-data"; import { pollTask, validateVideoConfig } from "@/utils/ai/utils"; import { createOpenAI } from "@ai-sdk/openai"; - +import { experimental_generateVideo as generateVideo } from "ai"; export default async (input: VideoConfig, config: AIConfig) => { if (!config.apiKey) throw new Error("缺少API Key"); if (!config.baseURL) throw new Error("缺少baseURL"); // const { owned, images, hasTextType } = validateVideoConfig(input, config); - const [requestUrl, queryUrl] = config.baseURL.split("|"); const authorization = `Bearer ${config.apiKey}`; @@ -21,30 +20,40 @@ export default async (input: VideoConfig, config: AIConfig) => { // 根据 aspectRatio 设置 size const sizeMap: Record = { - "16:9": "1920x1080", - "9:16": "1080x1920", + "16:9": "1280x720", + "9:16": "720x1280", }; formData.append("size", sizeMap[input.aspectRatio] || "1920x1080"); + if (input.imageBase64 && input.imageBase64.length) { const base64Data = input.imageBase64[0]!.replace(/^data:image\/\w+;base64,/, ""); const buffer = Buffer.from(base64Data, "base64"); formData.append("input_reference", buffer, { filename: "image.jpg", contentType: "image/jpeg" }); } - const { data } = await axios.post(requestUrl, formData, { - headers: { "Content-Type": "application/json", Authorization: authorization, ...formData.getHeaders() }, - }); - if (data.status === "FAILED") throw new Error(`任务提交失败: ${data.errorMessage || "未知错误"}`); - const taskId = data.id; - return await pollTask(async () => { - const { data } = await axios.get(`${queryUrl.replace("{id}", taskId)}`, { - headers: { Authorization: authorization }, - }); - if (data.status === "SUCCESS") { - return data.results?.length ? { completed: true, url: data.results[0].url } : { completed: false, error: "任务成功但未返回视频链接" }; - } - if (data.status === "FAILED") return { completed: false, error: `任务失败: ${data.errorMessage || "未知错误"}` }; - if (data.status === "QUEUED" || data.status === "RUNNING") return { completed: false }; - return { completed: false, error: `未知状态: ${data.status}` }; - }); + const body = { + model: config.model, + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: input.prompt, + }, + ], + }, + ], + }; + const { data } = await axios.post( + config.baseURL, + { ...body }, + { + headers: { "Content-Type": "application/json", Authorization: authorization }, + }, + ); + + console.log("%c Line:49 🥓 data", "background:#ffdd4d", data); + + if (data.status === "FAILED") throw new Error(`任务提交失败: ${data.errorMessage || "未知错误"}`); };