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 专属功能已降级处理(保留接口,返回不支持提示)
This commit is contained in:
shuiyu1123 committed 2026-06-23 12:27:18 +00:00
1 parent 6fa759665d
commit dc77dfd304
48 files changed
+22273

No files matched your search

+584
View File
@@ -0,0 +1,584 @@
# 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