Files
xiaoli-cli/operit_py/qwen_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

373 lines
15 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": "qwen_draw",
# "display_name": {
# "zh": "Qwen 绘图",
# "en": "Qwen Draw"
# },
# "description": {
# "zh": "使用阿里云百炼/DashScope 文生图接口(通义万相/通义千问图像)根据提示词画图(异步任务轮询),将图片保存到本地 /sdcard/Download/Operit/plugins/draw/qwen_draw/draws/ 目录,并返回 Markdown 图片提示。",
# "en": "Generate images via Alibaba Cloud Model Studio (DashScope) text-to-image API (async task polling), save to /sdcard/Download/Operit/plugins/draw/qwen_draw/draws/, and return a Markdown image reference."
# },
# "env": [
# { "name": "DASHSCOPE_API_KEY", "description": { "zh": "DashScope API Key(必填)", "en": "DashScope API key (required)" }, "required": true },
# { "name": "DASHSCOPE_API_BASE_URL", "description": { "zh": "DashScope API Base URL(可选,不填则默认 https://dashscope.aliyuncs.com;国际站可用 https://dashscope-intl.aliyuncs.com)", "en": "DashScope API base URL (optional; defaults to https://dashscope.aliyuncs.com; intl: https://dashscope-intl.aliyuncs.com)" }, "required": false },
# { "name": "QWEN_IMAGE_MODEL", "description": { "zh": "默认文生图模型(可选;当 draw_image 未传 model 时使用,例如 qwen-image-plus 或 wan2.2-t2i-flash)", "en": "Default image model (optional; used when draw_image doesn't pass model), e.g. qwen-image-plus or wan2.2-t2i-flash" }, "required": false }
# ],
# "category": "Draw",
# "tools": [
# {
# "name": "draw_image",
# "description": { "zh": "根据提示词调用 DashScope 文生图接口生成图片,保存到本地并返回 Markdown 图片提示。", "en": "Generate an image via DashScope text-to-image API using a prompt, save it locally, and return a Markdown image reference." },
# "parameters": [
# { "name": "prompt", "description": { "zh": "绘图提示词(英文或中文皆可)", "en": "Prompt for image generation (Chinese or English)" }, "type": "string", "required": true },
# { "name": "model", "description": { "zh": "模型名称(可选;不传则使用环境变量 QWEN_IMAGE_MODEL,再不行使用默认值)", "en": "Model name (optional; falls back to env QWEN_IMAGE_MODEL, then default)" }, "type": "string", "required": false },
# { "name": "size", "description": { "zh": "输出图像分辨率,如 '1664*928' 或 '1024x1024'(可选)", "en": "Output image resolution, e.g. '1664*928' or '1024x1024' (optional)" }, "type": "string", "required": false },
# { "name": "n", "description": { "zh": "生成图片数量(可选,默认 1)", "en": "Number of images (optional; default 1)" }, "type": "number", "required": false },
# { "name": "negative_prompt", "description": { "zh": "负面提示词(可选)", "en": "Negative prompt (optional)" }, "type": "string", "required": false },
# { "name": "prompt_extend", "description": { "zh": "是否开启 prompt 智能改写(可选,默认 true)", "en": "Enable prompt extension (optional; default true)" }, "type": "boolean", "required": false },
# { "name": "watermark", "description": { "zh": "是否加水印(可选,默认 false)", "en": "Enable watermark (optional; default false)" }, "type": "boolean", "required": false },
# { "name": "file_name", "description": { "zh": "自定义保存到本地的文件名(不含路径和扩展名)", "en": "Custom output file name (without path or extension)" }, "type": "string", "required": false },
# { "name": "api_base_url", "description": { "zh": "DashScope API Base URL(不传则取环境变量 DASHSCOPE_API_BASE_URL 或默认 https://dashscope.aliyuncs.com )", "en": "DashScope API base URL (optional; falls back to env DASHSCOPE_API_BASE_URL or https://dashscope.aliyuncs.com)" }, "type": "string", "required": false },
# { "name": "poll_interval_ms", "description": { "zh": "轮询间隔(毫秒),默认 2000", "en": "Polling interval (milliseconds), default 2000" }, "type": "number", "required": false },
# { "name": "max_wait_time_ms", "description": { "zh": "最长等待时间(毫秒),默认 10 分钟", "en": "Max wait time (milliseconds), default 10 minutes" }, "type": "number", "required": false }
# ]
# }
# ]
# }
import json
import math
import re
import time
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://dashscope.aliyuncs.com"
DEFAULT_MODEL = "qwen-image-plus"
DRAW_ROOT_DIR = getPluginConfigDir("draw")
STORAGE_DIR = f"{DRAW_ROOT_DIR}/qwen_draw"
DRAWS_DIR = f"{STORAGE_DIR}/draws"
POLL_INTERVAL_MS = 2000
MAX_WAIT_TIME_MS = 600000
def _is_finite(n):
return isinstance(n, (int, float)) and not isinstance(n, bool) and math.isfinite(n)
def isRecord(value):
return isinstance(value, dict)
def getErrorMessage(error):
if isinstance(error, Exception):
return str(error)
return str(error)
def getErrorStack(error):
if isinstance(error, Exception):
import traceback
return traceback.format_exc()
return None
def normalizePositiveInt(value, fallback):
if value is None:
return fallback
n = value if isinstance(value, (int, float)) else int(str(value), 10)
if not _is_finite(n) or n <= 0:
return fallback
return int(n)
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 getApiKey():
apiKey = getEnv("DASHSCOPE_API_KEY")
if not apiKey:
raise Exception("DASHSCOPE_API_KEY 未配置,请在环境变量中设置 DashScope 的 API Key。")
return apiKey
def getApiBaseUrl(customBaseUrl=None):
fromParam = str(customBaseUrl or "").strip()
if fromParam:
return fromParam
fromEnv = str(getEnv("DASHSCOPE_API_BASE_URL") or "").strip()
if fromEnv:
return fromEnv
return DEFAULT_API_BASE_URL
def getTextToImageEndpoint(baseUrl):
trimmed = baseUrl.strip()
if not trimmed:
return joinUrl(DEFAULT_API_BASE_URL, "api/v1/services/aigc/text2image/image-synthesis")
if "/api/v1/services/aigc/text2image/image-synthesis" in trimmed:
return trimmed
if trimmed.endswith("/api/v1"):
return joinUrl(trimmed, "services/aigc/text2image/image-synthesis")
if trimmed.endswith("/api/v1/"):
return joinUrl(trimmed, "services/aigc/text2image/image-synthesis")
return joinUrl(trimmed, "api/v1/services/aigc/text2image/image-synthesis")
def getTaskEndpoint(baseUrl, taskId):
trimmed = baseUrl.strip()
safeBase = trimmed or DEFAULT_API_BASE_URL
if "/api/v1/tasks/" in safeBase:
idx = safeBase.index("/api/v1/tasks/")
return safeBase[:idx] + f"/api/v1/tasks/{taskId}"
if safeBase.endswith("/api/v1"):
return joinUrl(safeBase, f"tasks/{taskId}")
if safeBase.endswith("/api/v1/"):
return joinUrl(safeBase, f"tasks/{taskId}")
return joinUrl(safeBase, f"api/v1/tasks/{taskId}")
def sanitizeFileName(name):
safe = re.sub(r'[\\/:*?"<>|]', "_", str(name)).strip()
if not safe:
return f"qwen_draw_{int(time.time() * 1000)}"
return safe[:80]
def buildFileName(prompt, customName=None):
if customName and str(customName).strip():
return sanitizeFileName(customName)
shortPrompt = f"{prompt[:40]}..." if len(prompt) > 40 else prompt
base = sanitizeFileName(shortPrompt or "image")
timestamp = int(time.time() * 1000)
return f"{base}_{timestamp}"
def normalizeSize(size=None):
raw = str(size or "").strip()
if not raw:
return None
cleaned = raw.replace("×", "*").replace("x", "*").replace("X", "*")
cleaned = re.sub(r"\s+", "", cleaned)
if not re.match(r"^\d+\*\d+$", cleaned):
return None
return cleaned
def guessExtensionFromUrl(url):
match = re.search(r"\.(png|jpg|jpeg|webp|gif)(?:\?|#|$)", url, re.IGNORECASE)
if match and match.group(1):
return match.group(1).lower()
return "png"
async def ensureDirectories():
dirs = [DRAW_ROOT_DIR, STORAGE_DIR, DRAWS_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 e:
console.warn(f"创建目录异常: {dir_} -> {getErrorMessage(e)}")
async def createTask(params):
apiKey = getApiKey()
apiBaseUrl = getApiBaseUrl(params.get("api_base_url"))
endpoint = getTextToImageEndpoint(apiBaseUrl)
modelFromParam = str(params.get("model") or "").strip()
modelFromEnv = str(getEnv("QWEN_IMAGE_MODEL") or "").strip()
effectiveModel = modelFromParam or modelFromEnv or DEFAULT_MODEL
body = {
"model": effectiveModel,
"input": {
"prompt": params["prompt"]
},
"parameters": {
"n": int(params["n"]) if isinstance(params.get("n"), (int, float)) and _is_finite(params["n"]) and params["n"] > 0 else 1,
"prompt_extend": True if params.get("prompt_extend") is None else bool(params.get("prompt_extend")),
"watermark": False if params.get("watermark") is None else bool(params.get("watermark"))
}
}
normalizedSize = normalizeSize(params.get("size"))
if normalizedSize:
body["parameters"]["size"] = normalizedSize
negativePrompt = str(params.get("negative_prompt") or "").strip()
if negativePrompt:
body["parameters"]["negative_prompt"] = negativePrompt
headers = {
"accept": "application/json",
"content-type": "application/json",
"Authorization": f"Bearer {apiKey}",
"X-DashScope-Async": "enable"
}
request = client.newRequest().url(endpoint).method("POST").headers(headers).body(json.dumps(body), "json")
response = await request.build().execute()
if not response.isSuccessful():
raise Exception(f"DashScope 文生图创建任务失败: {response.statusCode} - {response.content}")
try:
parsed = json.loads(response.content)
except Exception as e:
raise Exception(f"解析 DashScope 创建任务响应失败: {getErrorMessage(e)}")
output = parsed.get("output") if isRecord(parsed) and isRecord(parsed.get("output")) else None
taskId = output.get("task_id") if (output and isinstance(output.get("task_id"), str)) else ""
if not taskId:
raise Exception(f"DashScope 创建任务响应中未找到 output.task_id: {response.content}")
return {"task_id": taskId, "effective_model": effectiveModel}
async def pollTask(params):
apiKey = getApiKey()
apiBaseUrl = getApiBaseUrl(params.get("api_base_url"))
pollIntervalMs = normalizePositiveInt(params.get("poll_interval_ms"), POLL_INTERVAL_MS)
maxWaitTimeMs = normalizePositiveInt(params.get("max_wait_time_ms"), MAX_WAIT_TIME_MS)
startTime = int(time.time() * 1000)
attempts = 0
while int(time.time() * 1000) - startTime < maxWaitTimeMs:
attempts += 1
endpoint = getTaskEndpoint(apiBaseUrl, params["task_id"])
headers = {
"accept": "application/json",
"Authorization": f"Bearer {apiKey}"
}
request = client.newRequest().url(endpoint).method("GET").headers(headers)
response = await request.build().execute()
if not response.isSuccessful():
raise Exception(f"DashScope 查询任务失败: {response.statusCode} - {response.content}")
try:
parsed = json.loads(response.content)
except Exception as e:
raise Exception(f"解析 DashScope 查询任务响应失败: {getErrorMessage(e)}")
output = parsed.get("output") if isRecord(parsed) and isRecord(parsed.get("output")) else None
taskStatus = str(output.get("task_status")) if (output and isinstance(output.get("task_status"), str)) else ""
if taskStatus == "SUCCEEDED":
results = output.get("results") if output else None
first = results[0] if (isinstance(results, list) and len(results) > 0) else None
url = first.get("url") if isRecord(first) else None
if not ((isinstance(url, str) or isinstance(url, (int, float))) and str(url).strip()):
raise Exception(f"任务已完成但未找到图片 URL: {response.content}")
return {"image_url": str(url), "task_status": taskStatus}
if taskStatus == "FAILED":
raise Exception(f"任务失败: {response.content}")
if attempts % 5 == 0:
console.log(f"任务状态: {taskStatus or 'UNKNOWN'},继续等待...")
await Tools.System.sleep(pollIntervalMs)
raise Exception(f"任务超时: 等待超过{math.ceil(maxWaitTimeMs / 60000)}分钟仍未完成")
async def draw_image(params):
if not params or not params.get("prompt") or not str(params["prompt"]).strip():
raise Exception("参数 prompt 不能为空。")
prompt = str(params["prompt"]).strip()
await ensureDirectories()
createResult = await createTask({
"prompt": prompt,
"model": params.get("model"),
"size": params.get("size"),
"n": params.get("n"),
"negative_prompt": params.get("negative_prompt"),
"prompt_extend": params.get("prompt_extend"),
"watermark": params.get("watermark"),
"api_base_url": params.get("api_base_url")
})
pollResult = await pollTask({
"task_id": createResult["task_id"],
"api_base_url": params.get("api_base_url"),
"poll_interval_ms": params.get("poll_interval_ms"),
"max_wait_time_ms": params.get("max_wait_time_ms")
})
ext = guessExtensionFromUrl(pollResult["image_url"])
baseName = buildFileName(prompt, params.get("file_name"))
filePath = f"{DRAWS_DIR}/{baseName}.{ext}"
downloadResult = await Tools.Files.download(pollResult["image_url"], filePath)
if not downloadResult.get("successful"):
raise Exception(f"下载图片失败: {downloadResult.get('details')}")
fileUri = f"file://{filePath}"
markdown = f"![AI生成的图片]({fileUri})"
hintLines = []
hintLines.append(f"图片已生成并保存在本地 {DRAWS_DIR}。")
hintLines.append(f"本地路径: {filePath}")
hintLines.append("")
hintLines.append("在后续回答中,请直接输出下面这一行 Markdown 来展示这张图片:")
hintLines.append("")
hintLines.append(markdown)
return {
"file_path": filePath,
"file_uri": fileUri,
"markdown": markdown,
"prompt": prompt,
"model": createResult["effective_model"],
"task_id": createResult["task_id"],
"task_status": pollResult["task_status"],
"image_url": pollResult["image_url"],
"hint": "\n".join(hintLines)
}
async def draw_image_wrapper(params):
try:
result = await draw_image(params)
complete({
"success": True,
"message": f"图片生成成功,已保存到 {DRAWS_DIR},并返回 Markdown 图片提示。",
"data": result
})
except Exception as error:
console.error("draw_image 执行失败:", error)
complete({
"success": False,
"message": f"图片生成失败: {getErrorMessage(error)}",
"error_stack": getErrorStack(error)
})
exports.draw_image = draw_image_wrapper