Files
xiaoli-cli-operit-plugins/scripts/siliconflow_draw.ts
T

669 lines
29 KiB
TypeScript
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.
/* METADATA
{
"name": "siliconflow_draw",
"display_name": {
"zh": "硅基流动绘图",
"en": "SiliconFlow Draw"
},
"description": {
"zh": "使用 SiliconFlow 官方图像与视频接口生成图片和视频。图片走 /v1/images/generations,视频走 /v1/video/submit + /v1/video/status;生成结果会立即下载到本地,避免官方临时链接过期。",
"en": "Generate images and videos with SiliconFlow official image and video APIs. Images use /v1/images/generations; videos use /v1/video/submit + /v1/video/status. Generated assets are downloaded locally immediately before temporary URLs expire."
},
"category": "Draw",
"env": [
{
"name": "SILICONFLOW_API_KEY",
"description": {
"zh": "SiliconFlow API Key(必填)",
"en": "SiliconFlow API key (required)"
},
"required": true
},
{
"name": "SILICONFLOW_API_BASE_URL",
"description": {
"zh": "SiliconFlow API Base URL(可选,不填默认 https://api.siliconflow.cn )",
"en": "SiliconFlow API base URL (optional; defaults to https://api.siliconflow.cn)"
},
"required": false
},
{
"name": "SILICONFLOW_IMAGE_MODEL",
"description": {
"zh": "默认生图模型(可选;例如 Kwai-Kolors/Kolors、Qwen/Qwen-Image)",
"en": "Default image model (optional), e.g. Kwai-Kolors/Kolors or Qwen/Qwen-Image"
},
"required": false
},
{
"name": "SILICONFLOW_VIDEO_MODEL",
"description": {
"zh": "默认生视频模型(可选;例如 Wan-AI/Wan2.2-T2V-A14B 或 Wan-AI/Wan2.2-I2V-A14B)",
"en": "Default video model (optional), e.g. Wan-AI/Wan2.2-T2V-A14B or Wan-AI/Wan2.2-I2V-A14B"
},
"required": false
}
],
"tools": [
{
"name": "draw_image",
"description": {
"zh": "调用 SiliconFlow 官方图片生成接口生图,下载到本地并返回 Markdown 图片提示。",
"en": "Generate images with SiliconFlow official image API, download locally, and return Markdown image hints."
},
"parameters": [
{ "name": "prompt", "description": { "zh": "生图提示词", "en": "Image prompt" }, "type": "string", "required": true },
{ "name": "model", "description": { "zh": "模型名;不传则优先取 SILICONFLOW_IMAGE_MODEL,再取默认值 Kwai-Kolors/Kolors", "en": "Model name; falls back to SILICONFLOW_IMAGE_MODEL, then Kwai-Kolors/Kolors" }, "type": "string", "required": false },
{ "name": "image_size", "description": { "zh": "图片尺寸,例如 1024x1024;未传时默认 1024x1024", "en": "Image size, e.g. 1024x1024; defaults to 1024x1024" }, "type": "string", "required": false },
{ "name": "negative_prompt", "description": { "zh": "负面提示词(可选)", "en": "Negative prompt (optional)" }, "type": "string", "required": false },
{ "name": "batch_size", "description": { "zh": "一次生成多少张图(可选)", "en": "Number of images to generate (optional)" }, "type": "number", "required": false },
{ "name": "num_inference_steps", "description": { "zh": "推理步数(可选)", "en": "Inference steps (optional)" }, "type": "number", "required": false },
{ "name": "guidance_scale", "description": { "zh": "提示词引导强度(可选)", "en": "Guidance scale (optional)" }, "type": "number", "required": false },
{ "name": "seed", "description": { "zh": "随机种子(可选)", "en": "Seed (optional)" }, "type": "number", "required": false },
{ "name": "file_name", "description": { "zh": "本地保存文件名(不含扩展名)", "en": "Local output filename without extension" }, "type": "string", "required": false },
{ "name": "api_base_url", "description": { "zh": "自定义 API Base URL(可选)", "en": "Custom API base URL (optional)" }, "type": "string", "required": false }
]
},
{
"name": "draw_video",
"description": {
"zh": "调用 SiliconFlow 官方视频生成接口生视频,轮询状态,下载到本地并返回本地视频链接提示。",
"en": "Generate videos with SiliconFlow official video API, poll status, download locally, and return local video link hints."
},
"parameters": [
{ "name": "prompt", "description": { "zh": "生视频提示词", "en": "Video prompt" }, "type": "string", "required": true },
{ "name": "model", "description": { "zh": "模型名;不传则优先取 SILICONFLOW_VIDEO_MODEL,若传了 image_url/image_path 默认走 Wan-AI/Wan2.2-I2V-A14B,否则默认 Wan-AI/Wan2.2-T2V-A14B", "en": "Model name; falls back to SILICONFLOW_VIDEO_MODEL. If image_url/image_path is provided, defaults to Wan-AI/Wan2.2-I2V-A14B; otherwise Wan-AI/Wan2.2-T2V-A14B" }, "type": "string", "required": false },
{ "name": "image_size", "description": { "zh": "视频尺寸,可选 1280x720、720x1280、960x960;默认 1280x720", "en": "Video size: 1280x720, 720x1280, or 960x960; defaults to 1280x720" }, "type": "string", "required": false },
{ "name": "negative_prompt", "description": { "zh": "负面提示词(可选)", "en": "Negative prompt (optional)" }, "type": "string", "required": false },
{ "name": "image_url", "description": { "zh": "图生视频输入图 URL(可选)", "en": "Input image URL for image-to-video (optional)" }, "type": "string", "required": false },
{ "name": "image_path", "description": { "zh": "图生视频输入图本地路径(可选,会转成 data URL 上传)", "en": "Local input image path for image-to-video (optional; converted to data URL)" }, "type": "string", "required": false },
{ "name": "seed", "description": { "zh": "随机种子(可选)", "en": "Seed (optional)" }, "type": "number", "required": false },
{ "name": "file_name", "description": { "zh": "本地保存文件名(不含扩展名)", "en": "Local output filename without extension" }, "type": "string", "required": false },
{ "name": "api_base_url", "description": { "zh": "自定义 API Base URL(可选)", "en": "Custom API base URL (optional)" }, "type": "string", "required": false },
{ "name": "poll_interval_ms", "description": { "zh": "轮询间隔毫秒数,默认 5000", "en": "Polling interval in ms, default 5000" }, "type": "number", "required": false },
{ "name": "max_wait_time_ms", "description": { "zh": "最大等待毫秒数,默认 600000", "en": "Maximum wait time in ms, default 600000" }, "type": "number", "required": false }
]
}
]
}*/
/// <reference path="./types/index.d.ts" />
const siliconflowDraw = (function () {
const HTTP_TIMEOUT_MS = 600000;
const client = OkHttp.newBuilder()
.connectTimeout(HTTP_TIMEOUT_MS)
.readTimeout(HTTP_TIMEOUT_MS)
.writeTimeout(HTTP_TIMEOUT_MS)
.build();
const DEFAULT_API_BASE_URL = "https://api.siliconflow.cn";
const DEFAULT_IMAGE_MODEL = "Kwai-Kolors/Kolors";
const DEFAULT_IMAGE_SIZE = "1024x1024";
const DEFAULT_VIDEO_TEXT_MODEL = "Wan-AI/Wan2.2-T2V-A14B";
const DEFAULT_VIDEO_IMAGE_MODEL = "Wan-AI/Wan2.2-I2V-A14B";
const DEFAULT_VIDEO_SIZE = "1280x720";
const DEFAULT_POLL_INTERVAL_MS = 5000;
const DEFAULT_MAX_WAIT_TIME_MS = 600000;
const VIDEO_SIZE_OPTIONS = ["1280x720", "720x1280", "960x960"];
const DRAW_ROOT_DIR = getPluginConfigDir("draw");
const STORAGE_DIR = `${DRAW_ROOT_DIR}/siliconflow_draw`;
const DRAWS_DIR = `${STORAGE_DIR}/draws`;
const VIDEOS_DIR = `${STORAGE_DIR}/videos`;
type JsonMap = Record<string, any>;
type ImageParams = {
prompt: string;
model?: string;
image_size?: string;
negative_prompt?: string;
batch_size?: number;
num_inference_steps?: number;
guidance_scale?: number;
seed?: number;
file_name?: string;
api_base_url?: string;
};
type VideoParams = {
prompt: string;
model?: string;
image_size?: string;
negative_prompt?: string;
image_url?: string;
image_path?: string;
seed?: number;
file_name?: string;
api_base_url?: string;
poll_interval_ms?: number;
max_wait_time_ms?: number;
};
type ImageApiResult = {
effective_model: string;
image_urls: string[];
seed: any;
};
type VideoTaskResult = {
api_base_url: string;
effective_model: string;
request_id: string;
seed: any;
};
type VideoStatusResult = {
status: string;
reason: string;
video_url: string;
seed: any;
};
type ImageFileItem = {
file_path: string;
file_uri: string;
markdown: string;
};
function getApiKey() {
const apiKey = getEnv("SILICONFLOW_API_KEY");
if (!apiKey) {
throw new Error("SILICONFLOW_API_KEY 未配置,请先在环境变量中设置硅基流动 API Key。");
}
return apiKey;
}
function getApiBaseUrl(customBaseUrl) {
const fromParam = String(customBaseUrl || "").trim();
if (fromParam) return fromParam;
const fromEnv = String(getEnv("SILICONFLOW_API_BASE_URL") || "").trim();
if (fromEnv) return fromEnv;
return DEFAULT_API_BASE_URL;
}
function joinUrl(baseUrl, path) {
const normalizedBase = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
const normalizedPath = path.startsWith("/") ? path.slice(1) : path;
return `${normalizedBase}${normalizedPath}`;
}
function getImageEndpoint(baseUrl) {
const trimmed = String(baseUrl || "").trim();
if (!trimmed) return joinUrl(DEFAULT_API_BASE_URL, "v1/images/generations");
if (trimmed.includes("/v1/images/generations")) return trimmed;
if (trimmed.endsWith("/v1")) return joinUrl(trimmed, "images/generations");
if (trimmed.endsWith("/v1/")) return joinUrl(trimmed, "images/generations");
return joinUrl(trimmed, "v1/images/generations");
}
function getVideoSubmitEndpoint(baseUrl) {
const trimmed = String(baseUrl || "").trim();
if (!trimmed) return joinUrl(DEFAULT_API_BASE_URL, "v1/video/submit");
if (trimmed.includes("/v1/video/submit")) return trimmed;
if (trimmed.endsWith("/v1")) return joinUrl(trimmed, "video/submit");
if (trimmed.endsWith("/v1/")) return joinUrl(trimmed, "video/submit");
return joinUrl(trimmed, "v1/video/submit");
}
function getVideoStatusEndpoint(baseUrl) {
const trimmed = String(baseUrl || "").trim();
if (!trimmed) return joinUrl(DEFAULT_API_BASE_URL, "v1/video/status");
if (trimmed.includes("/v1/video/status")) return trimmed;
if (trimmed.endsWith("/v1")) return joinUrl(trimmed, "video/status");
if (trimmed.endsWith("/v1/")) return joinUrl(trimmed, "video/status");
return joinUrl(trimmed, "v1/video/status");
}
function sanitizeFileName(name, prefix) {
const safe = String(name || "").replace(/[\\/:*?"<>|]/g, "_").trim();
if (!safe) return `${prefix}_${Date.now()}`;
return safe.substring(0, 80);
}
function buildFileBaseName(prompt, customName, prefix) {
if (customName && String(customName).trim().length > 0) {
return sanitizeFileName(customName, prefix);
}
const rawPrompt = String(prompt || "").trim();
const shortPrompt = rawPrompt.length > 40 ? `${rawPrompt.substring(0, 40)}...` : rawPrompt;
return `${sanitizeFileName(shortPrompt || prefix, prefix)}_${Date.now()}`;
}
function getErrorMessage(error) {
if (error instanceof Error) return error.message;
return String(error);
}
function guessMimeTypeFromPath(filePath) {
const lower = String(filePath || "").toLowerCase();
if (lower.endsWith(".png")) return "image/png";
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
if (lower.endsWith(".webp")) return "image/webp";
if (lower.endsWith(".gif")) return "image/gif";
return "image/png";
}
function guessExtensionFromUrl(url, fallbackExtension) {
const match = String(url || "").match(/\.([a-z0-9]{2,5})(?:[?#].*)?$/i);
if (match && match[1]) return match[1].toLowerCase();
return fallbackExtension;
}
function parsePositiveInteger(value, fieldName) {
if (value === undefined || value === null || value === "") return undefined;
const numberValue = Number(value);
if (!Number.isFinite(numberValue) || numberValue <= 0) {
throw new Error(`${fieldName} 必须是正整数。`);
}
return Math.floor(numberValue);
}
function parseFiniteNumber(value, fieldName) {
if (value === undefined || value === null || value === "") return undefined;
const numberValue = Number(value);
if (!Number.isFinite(numberValue)) {
throw new Error(`${fieldName} 必须是有效数字。`);
}
return numberValue;
}
function parseSeed(value) {
if (value === undefined || value === null || value === "") return undefined;
const numberValue = Number(value);
if (!Number.isFinite(numberValue)) {
throw new Error("seed 必须是有效数字。");
}
return Math.floor(numberValue);
}
function normalizeImageSize(imageSize, model) {
const trimmedModel = String(model || "").trim().toLowerCase();
const rawSize = String(imageSize || "").trim();
const isQwenEditModel = trimmedModel.startsWith("qwen/qwen-image-edit");
if (isQwenEditModel) {
if (rawSize) {
throw new Error("当前模型为 Qwen 图像编辑模型,官方文档标注不支持 image_size 参数,请去掉 image_size。");
}
return undefined;
}
if (!rawSize) return DEFAULT_IMAGE_SIZE;
if (!/^\d+x\d+$/i.test(rawSize)) {
throw new Error("image_size 格式必须是 widthxheight,例如 1024x1024。");
}
return rawSize.toLowerCase();
}
function normalizeVideoSize(imageSize) {
const rawSize = String(imageSize || "").trim();
if (!rawSize) return DEFAULT_VIDEO_SIZE;
if (!VIDEO_SIZE_OPTIONS.includes(rawSize)) {
throw new Error(`image_size 仅支持 ${VIDEO_SIZE_OPTIONS.join("、")}。`);
}
return rawSize;
}
function isProbablyUrl(value) {
return /^https?:\/\//i.test(String(value || "").trim());
}
async function ensureDirectories() {
const dirs = [DRAW_ROOT_DIR, STORAGE_DIR, DRAWS_DIR, VIDEOS_DIR];
for (const dir of dirs) {
try {
const result = await Tools.Files.mkdir(dir);
if (!result.successful) {
console.warn(`创建目录失败(可能已存在): ${dir} -> ${result.details}`);
}
} catch (error) {
console.warn(`创建目录异常: ${dir} -> ${getErrorMessage(error)}`);
}
}
}
async function readLocalImageAsDataUrl(filePath) {
const trimmedPath = String(filePath || "").trim();
if (!trimmedPath) {
throw new Error("image_path 不能为空。");
}
const existsResult = await Tools.Files.exists(trimmedPath);
if (!existsResult.exists) {
throw new Error(`本地图片不存在: ${trimmedPath}`);
}
const binaryResult = await Tools.Files.readBinary(trimmedPath);
const base64Content = binaryResult && binaryResult.contentBase64 ? String(binaryResult.contentBase64).trim() : "";
if (!base64Content) {
throw new Error(`读取本地图片失败: ${trimmedPath}`);
}
return `data:${guessMimeTypeFromPath(trimmedPath)};base64,${base64Content}`;
}
async function resolveVideoImageInput(imageUrl, imagePath) {
const trimmedUrl = String(imageUrl || "").trim();
const trimmedPath = String(imagePath || "").trim();
if (trimmedUrl && trimmedPath) {
throw new Error("image_url 和 image_path 只能二选一,请不要同时传。");
}
if (trimmedUrl) {
if (!isProbablyUrl(trimmedUrl)) {
throw new Error("image_url 必须是 http 或 https 链接。");
}
return trimmedUrl;
}
if (trimmedPath) {
return await readLocalImageAsDataUrl(trimmedPath);
}
return undefined;
}
async function executeJsonRequest(url: string, body: JsonMap): Promise<any> {
const apiKey = getApiKey();
const request = client
.newRequest()
.url(url)
.method("POST")
.headers({
"accept": "application/json",
"content-type": "application/json",
"Authorization": `Bearer ${apiKey}`
})
.body(JSON.stringify(body), "json");
const response = await request.build().execute();
if (!response.isSuccessful()) {
throw new Error(`HTTP ${response.statusCode}: ${response.content}`);
}
try {
return JSON.parse(response.content);
} catch (error) {
throw new Error(`解析响应 JSON 失败: ${getErrorMessage(error)}`);
}
}
async function callImageApi(params: ImageParams): Promise<ImageApiResult> {
const apiBaseUrl = getApiBaseUrl(params.api_base_url);
const endpoint = getImageEndpoint(apiBaseUrl);
const modelFromParam = String(params.model || "").trim();
const modelFromEnv = String(getEnv("SILICONFLOW_IMAGE_MODEL") || "").trim();
const effectiveModel = modelFromParam || modelFromEnv || DEFAULT_IMAGE_MODEL;
const body: JsonMap = {
model: effectiveModel,
prompt: String(params.prompt || "").trim()
};
const imageSize = normalizeImageSize(params.image_size, effectiveModel);
if (imageSize) body.image_size = imageSize;
const negativePrompt = String(params.negative_prompt || "").trim();
if (negativePrompt) body.negative_prompt = negativePrompt;
const batchSize = parsePositiveInteger(params.batch_size, "batch_size");
if (batchSize !== undefined) body.batch_size = batchSize;
const numInferenceSteps = parsePositiveInteger(params.num_inference_steps, "num_inference_steps");
if (numInferenceSteps !== undefined) body.num_inference_steps = numInferenceSteps;
const guidanceScale = parseFiniteNumber(params.guidance_scale, "guidance_scale");
if (guidanceScale !== undefined) body.guidance_scale = guidanceScale;
const seed = parseSeed(params.seed);
if (seed !== undefined) body.seed = seed;
const parsed: any = await executeJsonRequest(endpoint, body);
const images = Array.isArray(parsed && parsed.images) ? parsed.images : [];
const imageUrls = images
.map(item => item && item.url ? String(item.url).trim() : "")
.filter(item => item.length > 0);
if (imageUrls.length === 0) {
throw new Error("图片接口返回中未找到 images[].url,请检查模型与参数是否匹配。");
}
return {
effective_model: effectiveModel,
image_urls: imageUrls,
seed: parsed && parsed.seed !== undefined ? parsed.seed : (seed !== undefined ? seed : null)
};
}
async function createVideoTask(params: VideoParams, imageInput?: string): Promise<VideoTaskResult> {
const apiBaseUrl = getApiBaseUrl(params.api_base_url);
const endpoint = getVideoSubmitEndpoint(apiBaseUrl);
const modelFromParam = String(params.model || "").trim();
const modelFromEnv = String(getEnv("SILICONFLOW_VIDEO_MODEL") || "").trim();
const defaultVideoModel = imageInput ? DEFAULT_VIDEO_IMAGE_MODEL : DEFAULT_VIDEO_TEXT_MODEL;
const effectiveModel = modelFromParam || modelFromEnv || defaultVideoModel;
if (effectiveModel === DEFAULT_VIDEO_IMAGE_MODEL && !imageInput) {
throw new Error(`当前模型 ${DEFAULT_VIDEO_IMAGE_MODEL} 为图生视频模型,必须传 image_url 或 image_path。`);
}
const body: JsonMap = {
model: effectiveModel,
prompt: String(params.prompt || "").trim(),
image_size: normalizeVideoSize(params.image_size)
};
const negativePrompt = String(params.negative_prompt || "").trim();
if (negativePrompt) body.negative_prompt = negativePrompt;
if (imageInput) body.image = imageInput;
const seed = parseSeed(params.seed);
if (seed !== undefined) body.seed = seed;
const parsed: any = await executeJsonRequest(endpoint, body);
const requestId = parsed && parsed.requestId ? String(parsed.requestId).trim() : "";
if (!requestId) {
throw new Error("视频接口返回中未找到 requestId。");
}
return {
api_base_url: apiBaseUrl,
effective_model: effectiveModel,
request_id: requestId,
seed: seed !== undefined ? seed : null
};
}
async function queryVideoStatus(apiBaseUrl: string, requestId: string): Promise<VideoStatusResult> {
const endpoint = getVideoStatusEndpoint(apiBaseUrl);
const parsed: any = await executeJsonRequest(endpoint, { requestId });
const status = parsed && parsed.status ? String(parsed.status).trim() : "";
const reason = parsed && parsed.reason ? String(parsed.reason).trim() : "";
const videos: any[] = parsed && parsed.results && Array.isArray(parsed.results.videos) ? parsed.results.videos : [];
const videoUrl = videos.length > 0 && videos[0] && videos[0].url ? String(videos[0].url).trim() : "";
const seed = parsed && parsed.results && parsed.results.seed !== undefined ? parsed.results.seed : null;
return {
status,
reason,
video_url: videoUrl,
seed
};
}
function normalizeVideoStatus(status) {
return String(status || "").trim().toLowerCase();
}
async function draw_image(params: ImageParams) {
const prompt = String(params && params.prompt ? params.prompt : "").trim();
if (!prompt) {
throw new Error("prompt 不能为空。");
}
await ensureDirectories();
const apiResult = await callImageApi(params);
const baseName = buildFileBaseName(prompt, params.file_name, "siliconflow_image");
const files: ImageFileItem[] = [];
for (let index = 0; index < apiResult.image_urls.length; index += 1) {
const imageUrl = apiResult.image_urls[index];
const extension = guessExtensionFromUrl(imageUrl, "png");
const suffix = apiResult.image_urls.length > 1 ? `_${index + 1}` : "";
const filePath = `${DRAWS_DIR}/${baseName}${suffix}.${extension}`;
const downloadResult = await Tools.Files.download(imageUrl, filePath);
if (!downloadResult.successful) {
throw new Error(`下载图片失败: ${downloadResult.details}`);
}
const fileUri = `file://${filePath}`;
files.push({
file_path: filePath,
file_uri: fileUri,
markdown: `![AI生成的图片${apiResult.image_urls.length > 1 ? ` ${index + 1}` : ""}](${fileUri})`
});
}
const hintLines: string[] = [];
hintLines.push(`图片已生成并下载到 ${DRAWS_DIR}。`);
hintLines.push(`共生成 ${files.length} 张。`);
hintLines.push("");
hintLines.push("后续回答如果需要展示图片,请直接输出下面这些 Markdown:");
hintLines.push("");
for (const fileItem of files) {
hintLines.push(fileItem.markdown);
}
return {
prompt,
model: apiResult.effective_model,
seed: apiResult.seed,
file_path: files[0].file_path,
file_uri: files[0].file_uri,
markdown: files[0].markdown,
files,
hint: hintLines.join("\n")
};
}
async function draw_video(params: VideoParams) {
const prompt = String(params && params.prompt ? params.prompt : "").trim();
if (!prompt) {
throw new Error("prompt 不能为空。");
}
await ensureDirectories();
const imageInput = await resolveVideoImageInput(params.image_url, params.image_path);
const createdTask = await createVideoTask(params, imageInput);
const pollIntervalMs = parsePositiveInteger(params.poll_interval_ms, "poll_interval_ms") || DEFAULT_POLL_INTERVAL_MS;
const maxWaitTimeMs = parsePositiveInteger(params.max_wait_time_ms, "max_wait_time_ms") || DEFAULT_MAX_WAIT_TIME_MS;
const deadline = Date.now() + maxWaitTimeMs;
let latestStatus = "";
let latestReason = "";
let remoteVideoUrl = "";
let resultSeed = createdTask.seed;
while (Date.now() <= deadline) {
const statusResult = await queryVideoStatus(createdTask.api_base_url, createdTask.request_id);
latestStatus = statusResult.status;
latestReason = statusResult.reason;
resultSeed = statusResult.seed !== null && statusResult.seed !== undefined ? statusResult.seed : resultSeed;
const normalizedStatus = normalizeVideoStatus(statusResult.status);
if (normalizedStatus === "succeed") {
remoteVideoUrl = statusResult.video_url;
if (!remoteVideoUrl) {
throw new Error("视频任务已成功,但返回中未找到 results.videos[0].url。");
}
break;
}
if (normalizedStatus === "failed") {
throw new Error(`视频生成失败: ${latestReason || "接口未返回失败原因"}`);
}
await Tools.System.sleep(pollIntervalMs);
}
if (!remoteVideoUrl) {
throw new Error(`视频生成超时,最后状态为 ${latestStatus || "未知"}${latestReason ? `,原因:${latestReason}` : ""}`);
}
const baseName = buildFileBaseName(prompt, params.file_name, "siliconflow_video");
const extension = guessExtensionFromUrl(remoteVideoUrl, "mp4");
const filePath = `${VIDEOS_DIR}/${baseName}.${extension}`;
const downloadResult = await Tools.Files.download(remoteVideoUrl, filePath);
if (!downloadResult.successful) {
throw new Error(`下载视频失败: ${downloadResult.details}`);
}
const fileUri = `file://${filePath}`;
const markdownLink = `[点击查看生成的视频](${fileUri})`;
const htmlVideo = `<video controls src="${fileUri}"></video>`;
const hintLines: string[] = [];
hintLines.push(`视频已生成并下载到 ${VIDEOS_DIR}。`);
hintLines.push(`本地路径: ${filePath}`);
hintLines.push("");
hintLines.push("后续回答如果要给出视频,请优先返回这个本地链接:");
hintLines.push(markdownLink);
hintLines.push("");
hintLines.push("如果当前渲染环境支持 HTML 视频标签,也可以使用:");
hintLines.push(htmlVideo);
return {
prompt,
model: createdTask.effective_model,
request_id: createdTask.request_id,
seed: resultSeed,
file_path: filePath,
file_uri: fileUri,
remote_video_url: remoteVideoUrl,
markdown_link: markdownLink,
html_video: htmlVideo,
hint: hintLines.join("\n")
};
}
async function draw_image_wrapper(params: ImageParams) {
try {
const result = await draw_image(params || {});
complete({
success: true,
message: "SiliconFlow 图片生成成功,已下载到本地。",
data: result
});
} catch (error) {
console.error("draw_image 执行失败:", error);
complete({
success: false,
message: `SiliconFlow 图片生成失败: ${getErrorMessage(error)}`,
error_stack: error && error.stack ? error.stack : undefined
});
}
}
async function draw_video_wrapper(params: VideoParams) {
try {
const result = await draw_video(params || {});
complete({
success: true,
message: "SiliconFlow 视频生成成功,已下载到本地。",
data: result
});
} catch (error) {
console.error("draw_video 执行失败:", error);
complete({
success: false,
message: `SiliconFlow 视频生成失败: ${getErrorMessage(error)}`,
error_stack: error && error.stack ? error.stack : undefined
});
}
}
return {
draw_image: draw_image_wrapper,
draw_video: draw_video_wrapper
};
})();
exports.draw_image = siliconflowDraw.draw_image;
exports.draw_video = siliconflowDraw.draw_video;