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:
1 parent
6fa759665d
commit
dc77dfd304
48 files changed
+22273
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user