Files
xiaoli-cli/operit_py/siliconflow_draw.py
T
shuiyu1123 dc77dfd304 feat: 将 47 个 Operit TypeScript 脚本全面翻译为 Python
- 新增 operit_runtime.py: 跨平台 Python 运行时
  - 实现 Tools.System/Files/Net/UI/Memory/FFmpeg/Workflow/Chat/SoftwareSettings/Tasker
  - 实现 OkHttp 链式 HTTP 客户端兼容层
  - 实现 complete()/exports/console 全局对象
  - 实现 OperitScriptLoader 脚本加载器
  - 支持 Windows/macOS/Linux 三平台

- 新增 operit_py/ 目录: 47 个 Python 翻译脚本
  - 搜索类: duckduckgo, google_search, tavily, zhipu_search, various_search, crossref
  - 绘图类: openai_draw, qwen_draw, xai_draw, minimax_draw, siliconflow_draw, zhipu_draw, nanobanana_draw
  - 媒体类: ffmpeg, file_converter, douyin_download, bilibili_tools, jmcomic
  - 文件类: extended_file_tools, hex_editor, reader, beeimg_image_uploader
  - 网络类: extended_http_tools, network_test
  - 自动化类: browser, automatic_ui_base, automatic_ui_subagent, automatic_bilibili_assistant, automatic_xiaohongshu_assistant, automatic_baidu_map_assistant
  - 系统类: system_tools, super_admin, daily_life, operit_editor
  - 对话类: ai_chat, extended_chat, qq_intelligent
  - 工作流类: workflow, vflow_trigger, tasker
  - 工具类: time, code_runner, quick_start, extended_memory_tools, baidu_map, 12306, pdf_vision_parser

- 测试结果: 47/47 语法检查通过, 38 脚本加载成功, 280 个工具注册
- Android 专属功能已降级处理(保留接口,返回不支持提示)
2026-06-23 12:27:18 +00:00

585 lines
26 KiB
Python
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 }
# ]
# }
# ]
# }
import json
import math
import re
import time
import traceback
HTTP_TIMEOUT_MS = 600000
client = OkHttp.newBuilder().connectTimeout(HTTP_TIMEOUT_MS).readTimeout(HTTP_TIMEOUT_MS).writeTimeout(HTTP_TIMEOUT_MS).build()
DEFAULT_API_BASE_URL = "https://api.siliconflow.cn"
DEFAULT_IMAGE_MODEL = "Kwai-Kolors/Kolors"
DEFAULT_IMAGE_SIZE = "1024x1024"
DEFAULT_VIDEO_TEXT_MODEL = "Wan-AI/Wan2.2-T2V-A14B"
DEFAULT_VIDEO_IMAGE_MODEL = "Wan-AI/Wan2.2-I2V-A14B"
DEFAULT_VIDEO_SIZE = "1280x720"
DEFAULT_POLL_INTERVAL_MS = 5000
DEFAULT_MAX_WAIT_TIME_MS = 600000
VIDEO_SIZE_OPTIONS = ["1280x720", "720x1280", "960x960"]
DRAW_ROOT_DIR = getPluginConfigDir("draw")
STORAGE_DIR = f"{DRAW_ROOT_DIR}/siliconflow_draw"
DRAWS_DIR = f"{STORAGE_DIR}/draws"
VIDEOS_DIR = f"{STORAGE_DIR}/videos"
def _is_finite(n):
return isinstance(n, (int, float)) and not isinstance(n, bool) and math.isfinite(n)
def getApiKey():
apiKey = getEnv("SILICONFLOW_API_KEY")
if not apiKey:
raise Exception("SILICONFLOW_API_KEY 未配置,请先在环境变量中设置硅基流动 API Key。")
return apiKey
def getApiBaseUrl(customBaseUrl=None):
fromParam = str(customBaseUrl or "").strip()
if fromParam:
return fromParam
fromEnv = str(getEnv("SILICONFLOW_API_BASE_URL") or "").strip()
if fromEnv:
return fromEnv
return DEFAULT_API_BASE_URL
def joinUrl(baseUrl, path):
normalizedBase = baseUrl if baseUrl.endswith("/") else f"{baseUrl}/"
normalizedPath = path[1:] if path.startswith("/") else path
return f"{normalizedBase}{normalizedPath}"
def getImageEndpoint(baseUrl):
trimmed = str(baseUrl or "").strip()
if not trimmed:
return joinUrl(DEFAULT_API_BASE_URL, "v1/images/generations")
if "/v1/images/generations" in trimmed:
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")
def getVideoSubmitEndpoint(baseUrl):
trimmed = str(baseUrl or "").strip()
if not trimmed:
return joinUrl(DEFAULT_API_BASE_URL, "v1/video/submit")
if "/v1/video/submit" in trimmed:
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")
def getVideoStatusEndpoint(baseUrl):
trimmed = str(baseUrl or "").strip()
if not trimmed:
return joinUrl(DEFAULT_API_BASE_URL, "v1/video/status")
if "/v1/video/status" in trimmed:
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")
def sanitizeFileName(name, prefix):
safe = re.sub(r'[\\/:*?"<>|]', "_", str(name or "")).strip()
if not safe:
return f"{prefix}_{int(time.time() * 1000)}"
return safe[:80]
def buildFileBaseName(prompt, customName, prefix):
if customName and len(str(customName).strip()) > 0:
return sanitizeFileName(customName, prefix)
rawPrompt = str(prompt or "").strip()
shortPrompt = f"{rawPrompt[:40]}..." if len(rawPrompt) > 40 else rawPrompt
return f"{sanitizeFileName(shortPrompt or prefix, prefix)}_{int(time.time() * 1000)}"
def getErrorMessage(error):
if isinstance(error, Exception):
return str(error)
return str(error)
def guessMimeTypeFromPath(filePath):
lower = str(filePath or "").lower()
if lower.endswith(".png"):
return "image/png"
if lower.endswith(".jpg") or lower.endswith(".jpeg"):
return "image/jpeg"
if lower.endswith(".webp"):
return "image/webp"
if lower.endswith(".gif"):
return "image/gif"
return "image/png"
def guessExtensionFromUrl(url, fallbackExtension):
match = re.search(r"\.([a-z0-9]{2,5})(?:[?#].*)?$", str(url or ""), re.IGNORECASE)
if match and match.group(1):
return match.group(1).lower()
return fallbackExtension
def parsePositiveInteger(value, fieldName):
if value is None or value == "":
return None
try:
numberValue = float(value)
except (TypeError, ValueError):
raise Exception(f"{fieldName} 必须是正整数。")
if not _is_finite(numberValue) or numberValue <= 0:
raise Exception(f"{fieldName} 必须是正整数。")
return int(numberValue)
def parseFiniteNumber(value, fieldName):
if value is None or value == "":
return None
try:
numberValue = float(value)
except (TypeError, ValueError):
raise Exception(f"{fieldName} 必须是有效数字。")
if not _is_finite(numberValue):
raise Exception(f"{fieldName} 必须是有效数字。")
return numberValue
def parseSeed(value):
if value is None or value == "":
return None
try:
numberValue = float(value)
except (TypeError, ValueError):
raise Exception("seed 必须是有效数字。")
if not _is_finite(numberValue):
raise Exception("seed 必须是有效数字。")
return int(numberValue)
def normalizeImageSize(imageSize, model):
trimmedModel = str(model or "").strip().lower()
rawSize = str(imageSize or "").strip()
isQwenEditModel = trimmedModel.startswith("qwen/qwen-image-edit")
if isQwenEditModel:
if rawSize:
raise Exception("当前模型为 Qwen 图像编辑模型,官方文档标注不支持 image_size 参数,请去掉 image_size。")
return None
if not rawSize:
return DEFAULT_IMAGE_SIZE
if not re.match(r"^\d+x\d+$", rawSize, re.IGNORECASE):
raise Exception("image_size 格式必须是 widthxheight,例如 1024x1024。")
return rawSize.lower()
def normalizeVideoSize(imageSize):
rawSize = str(imageSize or "").strip()
if not rawSize:
return DEFAULT_VIDEO_SIZE
if rawSize not in VIDEO_SIZE_OPTIONS:
raise Exception(f"image_size 仅支持 {'、'.join(VIDEO_SIZE_OPTIONS)}。")
return rawSize
def isProbablyUrl(value):
return bool(re.match(r"^https?://", str(value or "").strip(), re.IGNORECASE))
async def ensureDirectories():
dirs = [DRAW_ROOT_DIR, STORAGE_DIR, DRAWS_DIR, VIDEOS_DIR]
for dir_ in dirs:
try:
result = await Tools.Files.mkdir(dir_)
if not result.get("successful"):
console.warn(f"创建目录失败(可能已存在): {dir_} -> {result.get('details')}")
except Exception as error:
console.warn(f"创建目录异常: {dir_} -> {getErrorMessage(error)}")
async def readLocalImageAsDataUrl(filePath):
trimmedPath = str(filePath or "").strip()
if not trimmedPath:
raise Exception("image_path 不能为空。")
existsResult = await Tools.Files.exists(trimmedPath)
if not existsResult.get("exists"):
raise Exception(f"本地图片不存在: {trimmedPath}")
binaryResult = await Tools.Files.readBinary(trimmedPath)
base64Content = str(binaryResult.get("contentBase64") if binaryResult and binaryResult.get("contentBase64") else "").strip() if binaryResult else ""
if not base64Content:
raise Exception(f"读取本地图片失败: {trimmedPath}")
return f"data:{guessMimeTypeFromPath(trimmedPath)};base64,{base64Content}"
async def resolveVideoImageInput(imageUrl, imagePath):
trimmedUrl = str(imageUrl or "").strip()
trimmedPath = str(imagePath or "").strip()
if trimmedUrl and trimmedPath:
raise Exception("image_url 和 image_path 只能二选一,请不要同时传。")
if trimmedUrl:
if not isProbablyUrl(trimmedUrl):
raise Exception("image_url 必须是 http 或 https 链接。")
return trimmedUrl
if trimmedPath:
return await readLocalImageAsDataUrl(trimmedPath)
return None
async def executeJsonRequest(url, body):
apiKey = getApiKey()
request = client.newRequest().url(url).method("POST").headers({
"accept": "application/json",
"content-type": "application/json",
"Authorization": f"Bearer {apiKey}"
}).body(json.dumps(body), "json")
response = await request.build().execute()
if not response.isSuccessful():
raise Exception(f"HTTP {response.statusCode}: {response.content}")
try:
return json.loads(response.content)
except Exception as error:
raise Exception(f"解析响应 JSON 失败: {getErrorMessage(error)}")
async def callImageApi(params):
apiBaseUrl = getApiBaseUrl(params.get("api_base_url"))
endpoint = getImageEndpoint(apiBaseUrl)
modelFromParam = str(params.get("model") or "").strip()
modelFromEnv = str(getEnv("SILICONFLOW_IMAGE_MODEL") or "").strip()
effectiveModel = modelFromParam or modelFromEnv or DEFAULT_IMAGE_MODEL
body = {
"model": effectiveModel,
"prompt": str(params.get("prompt") or "").strip()
}
imageSize = normalizeImageSize(params.get("image_size"), effectiveModel)
if imageSize:
body["image_size"] = imageSize
negativePrompt = str(params.get("negative_prompt") or "").strip()
if negativePrompt:
body["negative_prompt"] = negativePrompt
batchSize = parsePositiveInteger(params.get("batch_size"), "batch_size")
if batchSize is not None:
body["batch_size"] = batchSize
numInferenceSteps = parsePositiveInteger(params.get("num_inference_steps"), "num_inference_steps")
if numInferenceSteps is not None:
body["num_inference_steps"] = numInferenceSteps
guidanceScale = parseFiniteNumber(params.get("guidance_scale"), "guidance_scale")
if guidanceScale is not None:
body["guidance_scale"] = guidanceScale
seed = parseSeed(params.get("seed"))
if seed is not None:
body["seed"] = seed
parsed = await executeJsonRequest(endpoint, body)
images = parsed.get("images") if (isinstance(parsed, dict) and isinstance(parsed.get("images"), list)) else []
imageUrls = [str(item.get("url")).strip() for item in images if item and item.get("url")]
imageUrls = [u for u in imageUrls if len(u) > 0]
if len(imageUrls) == 0:
raise Exception("图片接口返回中未找到 images[].url,请检查模型与参数是否匹配。")
return {
"effective_model": effectiveModel,
"image_urls": imageUrls,
"seed": parsed.get("seed") if (isinstance(parsed, dict) and parsed.get("seed") is not None) else (seed if seed is not None else None)
}
async def createVideoTask(params, imageInput=None):
apiBaseUrl = getApiBaseUrl(params.get("api_base_url"))
endpoint = getVideoSubmitEndpoint(apiBaseUrl)
modelFromParam = str(params.get("model") or "").strip()
modelFromEnv = str(getEnv("SILICONFLOW_VIDEO_MODEL") or "").strip()
defaultVideoModel = DEFAULT_VIDEO_IMAGE_MODEL if imageInput else DEFAULT_VIDEO_TEXT_MODEL
effectiveModel = modelFromParam or modelFromEnv or defaultVideoModel
if effectiveModel == DEFAULT_VIDEO_IMAGE_MODEL and not imageInput:
raise Exception(f"当前模型 {DEFAULT_VIDEO_IMAGE_MODEL} 为图生视频模型,必须传 image_url 或 image_path。")
body = {
"model": effectiveModel,
"prompt": str(params.get("prompt") or "").strip(),
"image_size": normalizeVideoSize(params.get("image_size"))
}
negativePrompt = str(params.get("negative_prompt") or "").strip()
if negativePrompt:
body["negative_prompt"] = negativePrompt
if imageInput:
body["image"] = imageInput
seed = parseSeed(params.get("seed"))
if seed is not None:
body["seed"] = seed
parsed = await executeJsonRequest(endpoint, body)
requestId = str(parsed.get("requestId")).strip() if (isinstance(parsed, dict) and parsed.get("requestId")) else ""
if not requestId:
raise Exception("视频接口返回中未找到 requestId。")
return {
"api_base_url": apiBaseUrl,
"effective_model": effectiveModel,
"request_id": requestId,
"seed": seed if seed is not None else None
}
async def queryVideoStatus(apiBaseUrl, requestId):
endpoint = getVideoStatusEndpoint(apiBaseUrl)
parsed = await executeJsonRequest(endpoint, {"requestId": requestId})
status = str(parsed.get("status")).strip() if (isinstance(parsed, dict) and parsed.get("status")) else ""
reason = str(parsed.get("reason")).strip() if (isinstance(parsed, dict) and parsed.get("reason")) else ""
results = parsed.get("results") if isinstance(parsed, dict) else None
videos = results.get("videos") if (isinstance(results, dict) and isinstance(results.get("videos"), list)) else []
videoUrl = str(videos[0].get("url")).strip() if (len(videos) > 0 and videos[0] and videos[0].get("url")) else ""
seed = results.get("seed") if (isinstance(results, dict) and results.get("seed") is not None) else None
return {
"status": status,
"reason": reason,
"video_url": videoUrl,
"seed": seed
}
def normalizeVideoStatus(status):
return str(status or "").strip().lower()
async def draw_image(params):
prompt = str(params.get("prompt") if params and params.get("prompt") else "").strip()
if not prompt:
raise Exception("prompt 不能为空。")
await ensureDirectories()
apiResult = await callImageApi(params)
baseName = buildFileBaseName(prompt, params.get("file_name"), "siliconflow_image")
files = []
for index in range(len(apiResult["image_urls"])):
imageUrl = apiResult["image_urls"][index]
extension = guessExtensionFromUrl(imageUrl, "png")
suffix = f"_{index + 1}" if len(apiResult["image_urls"]) > 1 else ""
filePath = f"{DRAWS_DIR}/{baseName}{suffix}.{extension}"
downloadResult = await Tools.Files.download(imageUrl, filePath)
if not downloadResult.get("successful"):
raise Exception(f"下载图片失败: {downloadResult.get('details')}")
fileUri = f"file://{filePath}"
files.append({
"file_path": filePath,
"file_uri": fileUri,
"markdown": f"![AI生成的图片{f' {index + 1}' if len(apiResult['image_urls']) > 1 else ''}]({fileUri})"
})
hintLines = []
hintLines.append(f"图片已生成并下载到 {DRAWS_DIR}。")
hintLines.append(f"共生成 {len(files)} 张。")
hintLines.append("")
hintLines.append("后续回答如果需要展示图片,请直接输出下面这些 Markdown:")
hintLines.append("")
for fileItem in files:
hintLines.append(fileItem["markdown"])
return {
"prompt": 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": files,
"hint": "\n".join(hintLines)
}
async def draw_video(params):
prompt = str(params.get("prompt") if params and params.get("prompt") else "").strip()
if not prompt:
raise Exception("prompt 不能为空。")
await ensureDirectories()
imageInput = await resolveVideoImageInput(params.get("image_url"), params.get("image_path"))
createdTask = await createVideoTask(params, imageInput)
pollIntervalMs = parsePositiveInteger(params.get("poll_interval_ms"), "poll_interval_ms") or DEFAULT_POLL_INTERVAL_MS
maxWaitTimeMs = parsePositiveInteger(params.get("max_wait_time_ms"), "max_wait_time_ms") or DEFAULT_MAX_WAIT_TIME_MS
deadline = int(time.time() * 1000) + maxWaitTimeMs
latestStatus = ""
latestReason = ""
remoteVideoUrl = ""
resultSeed = createdTask["seed"]
while int(time.time() * 1000) <= deadline:
statusResult = await queryVideoStatus(createdTask["api_base_url"], createdTask["request_id"])
latestStatus = statusResult["status"]
latestReason = statusResult["reason"]
resultSeed = statusResult["seed"] if (statusResult["seed"] is not None) else resultSeed
normalizedStatus = normalizeVideoStatus(statusResult["status"])
if normalizedStatus == "succeed":
remoteVideoUrl = statusResult["video_url"]
if not remoteVideoUrl:
raise Exception("视频任务已成功,但返回中未找到 results.videos[0].url。")
break
if normalizedStatus == "failed":
raise Exception(f"视频生成失败: {latestReason or '接口未返回失败原因'}")
await Tools.System.sleep(pollIntervalMs)
if not remoteVideoUrl:
raise Exception(f"视频生成超时,最后状态为 {latestStatus or '未知'}{f',原因:{latestReason}' if latestReason else ''}")
baseName = buildFileBaseName(prompt, params.get("file_name"), "siliconflow_video")
extension = guessExtensionFromUrl(remoteVideoUrl, "mp4")
filePath = f"{VIDEOS_DIR}/{baseName}.{extension}"
downloadResult = await Tools.Files.download(remoteVideoUrl, filePath)
if not downloadResult.get("successful"):
raise Exception(f"下载视频失败: {downloadResult.get('details')}")
fileUri = f"file://{filePath}"
markdownLink = f"[点击查看生成的视频]({fileUri})"
htmlVideo = f'<video controls src="{fileUri}"></video>'
hintLines = []
hintLines.append(f"视频已生成并下载到 {VIDEOS_DIR}。")
hintLines.append(f"本地路径: {filePath}")
hintLines.append("")
hintLines.append("后续回答如果要给出视频,请优先返回这个本地链接:")
hintLines.append(markdownLink)
hintLines.append("")
hintLines.append("如果当前渲染环境支持 HTML 视频标签,也可以使用:")
hintLines.append(htmlVideo)
return {
"prompt": 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": "\n".join(hintLines)
}
async def draw_image_wrapper(params=None):
if params is None:
params = {}
try:
result = await draw_image(params)
complete({
"success": True,
"message": "SiliconFlow 图片生成成功,已下载到本地。",
"data": result
})
except Exception as error:
console.error("draw_image 执行失败:", error)
complete({
"success": False,
"message": f"SiliconFlow 图片生成失败: {getErrorMessage(error)}",
"error_stack": traceback.format_exc()
})
async def draw_video_wrapper(params=None):
if params is None:
params = {}
try:
result = await draw_video(params)
complete({
"success": True,
"message": "SiliconFlow 视频生成成功,已下载到本地。",
"data": result
})
except Exception as error:
console.error("draw_video 执行失败:", error)
complete({
"success": False,
"message": f"SiliconFlow 视频生成失败: {getErrorMessage(error)}",
"error_stack": traceback.format_exc()
})
exports.draw_image = draw_image_wrapper
exports.draw_video = draw_video_wrapper