fix: 图生视频节点进度解析兼容百分号格式,K26/K3/Seedance统一走cf-api异步接口,K26新增seed参数

- 修复 K_video_image2video.py 进度值 "10%" 解析报错:rstrip('%') 后 float→int 安全转换
- K26 节点拆分为图生视频/首尾帧两个独立节点,删除旧的合并节点 K_video.py
- K26/K3 视频节点及 Seedance 客户端统一改用 get_async_api_base_url (cf-api.o1key.com)
- K3 节点模式选项从 标准/专家 改为 720p/1080p,与后端一致
- K_video_image2video.py 和 K_video_firstlast.py 新增 ComfyUI 原生 seed 参数

Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
o1key
2026-05-07 09:48:24 +08:00
co-authored by Claude Opus 4.6
parent 844401dbb2
commit 1a813bfd1d
10 changed files with 333 additions and 49 deletions
+5 -3
View File
@@ -12,7 +12,7 @@ Comfyui_o1key - ComfyUI 自定义节点集合
import ssl
from .nodes import NanoBananaPro, BatchNanoBananaPro, GoogleGemini, LoadFile, ImageStitchPro, SaveCleanImage, BatchCleanMetadata, VideoPreview, GoogleVeo, FluxImageEdit, UniversalLLMChat, KlingVideo, KlingFirstLastFrame, KlingMotionControlTest, AspectRatioPreset, MultiResPreview, BatchImagesO1key, Seedance, SeedanceMultiModal, StreamPreview, DoubaoImage, O1keyGPTImage, KVideo
from .nodes import NanoBananaPro, BatchNanoBananaPro, GoogleGemini, LoadFile, ImageStitchPro, SaveCleanImage, BatchCleanMetadata, VideoPreview, GoogleVeo, FluxImageEdit, UniversalLLMChat, KlingVideo, KlingFirstLastFrame, KlingMotionControlTest, AspectRatioPreset, MultiResPreview, BatchImagesO1key, Seedance, SeedanceMultiModal, StreamPreview, DoubaoImage, O1keyGPTImage, KVideoFirstLast, KVideoImage2Video
from .nodes import K3Video, K3VideoFirstLast, K3MotionControl, K3MotionVideoCheck, NanoBananaV2, NanoBananaV2Batch
# 报错弹框友好文案(不修改原节点代码,仅在外层统一处理)
@@ -74,7 +74,8 @@ NODE_CLASS_MAPPINGS = {
"StreamPreview": StreamPreview,
"DoubaoImage": DoubaoImage,
"O1keyGPTImage": O1keyGPTImage,
"KVideo": KVideo,
"KVideoFirstLast": KVideoFirstLast,
"KVideoImage2Video": KVideoImage2Video,
"K3Video": K3Video,
"K3VideoFirstLast": K3VideoFirstLast,
"K3MotionControl": K3MotionControl,
@@ -106,7 +107,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"StreamPreview": "流式文本预览",
"DoubaoImage": "豆包生图",
"O1keyGPTImage": "o1key GPT Image",
"KVideo": "K26 图生视频",
"KVideoFirstLast": "K26 图生视频(首尾帧)",
"KVideoImage2Video": "K26 图生视频",
"K3Video": "K3 图生视频 自研",
"K3VideoFirstLast": "首尾帧 K3 自研",
"K3MotionControl": "动作控制 K3 自研",
+2 -2
View File
@@ -10,7 +10,7 @@ from typing import Any, Callable, Dict, Optional
import aiohttp
from ..utils.config import get_api_key_or_raise, get_api_base_url
from ..utils.config import get_api_key_or_raise, get_async_api_base_url
class SeedanceClient:
@@ -30,7 +30,7 @@ class SeedanceClient:
def __init__(self):
self.api_key = get_api_key_or_raise()
self.base_url = get_api_base_url()
self.base_url = get_async_api_base_url()
def _headers(self) -> Dict[str, str]:
return {
+4 -4
View File
@@ -13,7 +13,7 @@ import tempfile
import aiohttp
from ..utils.config import get_api_key_or_raise, get_api_base_url
from ..utils.config import get_api_key_or_raise, get_async_api_base_url
from ..utils.r2_uploader import upload_video, upload_image
from ..utils.image_utils import tensor_to_pil
@@ -107,7 +107,7 @@ class K3MotionControl:
"参考视频": ("VIDEO",),
"提示词": ("STRING", {"multiline": True, "default": ""}),
"模型": (["v3", "v2-6"], {"default": "v3"}),
"模式": (["标准", "专家"], {"default": "专家"}),
"模式": (["720p", "1080p"], {"default": "1080p"}),
"时长": ([5, 10, 15, 20, 25, 30], {"default": 5}),
"角色朝向": (["图片", "视频"], {"default": "图片"}),
"保留原声": (["打开", "关闭"], {"default": "打开"}),
@@ -125,14 +125,14 @@ class K3MotionControl:
async def generate(self, 参考图片, 参考视频, 提示词, 保留原声, 角色朝向, 模式, 模型, 时长, seed, **kwargs):
api_key = get_api_key_or_raise()
base_url = get_api_base_url()
base_url = get_async_api_base_url()
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
# ── 参数映射 ──────────────────────────────────────────────────
mode_api = "std" if 模式 == "标准" else "pro"
mode_api = "std" if 模式 == "720p" else "pro"
model_name = f"kling-{模型}-motion-{mode_api}-{时长}s"
character_orientation = "image" if 角色朝向 == "图片" else "video"
keep_sound = "yes" if 保留原声 == "打开" else "no"
+5 -5
View File
@@ -11,7 +11,7 @@ import tempfile
import aiohttp
from ..utils.config import get_api_key_or_raise, get_api_base_url
from ..utils.config import get_api_key_or_raise, get_async_api_base_url
from ..utils.image_utils import tensor_to_pil, encode_image_to_base64
try:
@@ -25,8 +25,8 @@ except Exception:
# ── 常量 ──────────────────────────────────────────────────────────────────────
_MODEL_BASE = "kling-v3" # 动态拼接为 kling-v3-{模式}-{时长}s-{voice}
_MODES = ["标准", "专家", "4K"]
_MODE_MAP = {"标准": "std", "专家": "pro", "4K": "4k"}
_MODES = ["720p", "1080p", "4K"]
_MODE_MAP = {"720p": "std", "1080p": "pro", "4K": "4k"}
_MULTI_SHOT_OPTIONS = [
"禁用",
@@ -112,7 +112,7 @@ class K3Video:
"负向提示词": ("STRING", {"multiline": True, "default": ""}),
"时长": ([5, 10, 15], {"default": 5}),
"生成音频": (["关闭", "打开"], {"default": "关闭"}),
"模式": (_MODES, {"default": "标准"}),
"模式": (_MODES, {"default": "720p"}),
"seed": ("INT", {
"default": 0, "min": 0, "max": 2147483647,
"tooltip": "seed 仅控制节点是否重新运行,结果本身不可复现。",
@@ -139,7 +139,7 @@ class K3Video:
async def generate(self, 多镜头, 起始帧, 提示词, 负向提示词, 时长, 生成音频, 模式, seed, **kwargs):
api_key = get_api_key_or_raise()
base_url = get_api_base_url()
base_url = get_async_api_base_url()
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
+5 -5
View File
@@ -10,7 +10,7 @@ import tempfile
import aiohttp
from ..utils.config import get_api_key_or_raise, get_api_base_url
from ..utils.config import get_api_key_or_raise, get_async_api_base_url
from ..utils.image_utils import tensor_to_pil, encode_image_to_base64
try:
@@ -24,8 +24,8 @@ except Exception:
# ── 常量 ──────────────────────────────────────────────────────────────────────
_MODEL_BASE = "kling-v3"
_MODES = ["标准", "专家", "4K"]
_MODE_MAP = {"标准": "std", "专家": "pro", "4K": "4k"}
_MODES = ["720p", "1080p", "4K"]
_MODE_MAP = {"720p": "std", "1080p": "pro", "4K": "4k"}
_ENDPOINT_CREATE = "/v1/video/generations"
_ENDPOINT_STATUS = "/v1/video/generations/{task_id}"
@@ -93,7 +93,7 @@ class K3VideoFirstLast:
"负向提示词": ("STRING", {"multiline": True, "default": ""}),
"时长": ([5, 10, 15], {"default": 5}),
"生成音频": (["关闭", "打开"], {"default": "关闭"}),
"模式": (_MODES, {"default": "标准"}),
"模式": (_MODES, {"default": "720p"}),
"seed": ("INT", {
"default": 0, "min": 0, "max": 2147483647,
"tooltip": "seed 仅控制节点是否重新运行,结果本身不可复现。",
@@ -111,7 +111,7 @@ class K3VideoFirstLast:
async def generate(self, 起始帧, 提示词, 负向提示词, 时长, 生成音频, 模式, seed, 尾帧=None):
api_key = get_api_key_or_raise()
base_url = get_api_base_url()
base_url = get_async_api_base_url()
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
+53 -15
View File
@@ -4,12 +4,13 @@ K26 图生视频节点
import asyncio
import json
import math
import os
import tempfile
import aiohttp
from ..utils.config import get_api_key_or_raise, get_api_base_url
from ..utils.config import get_api_key_or_raise, get_async_api_base_url
from ..utils.image_utils import tensor_to_pil, encode_image_to_base64
try:
@@ -30,13 +31,20 @@ _POLL_INIT = 3
_POLL_MAX = 15
def _image_to_base64(tensor) -> str:
def _image_to_base64(tensor, scale=1.0) -> str:
from PIL import Image
pil = tensor_to_pil(tensor)
return encode_image_to_base64(pil[0], format="PNG")
img = pil[0]
if scale < 1.0:
w, h = img.size
new_w = max(1, int(w * scale))
new_h = max(1, int(h * scale))
img = img.resize((new_w, new_h), Image.LANCZOS)
return encode_image_to_base64(img, format="PNG")
class KVideo:
"""K26 图生视频节点"""
class KVideoFirstLast:
"""K26 图生视频节点(首尾帧)"""
@classmethod
def INPUT_TYPES(cls):
@@ -44,9 +52,13 @@ class KVideo:
"required": {
"起始帧": ("IMAGE",),
"提示词": ("STRING", {"multiline": True, "default": ""}),
"模式": (["pro"],),
"模式": (["1080p"],),
"时长": ([5, 10],),
"生成音频": (["关闭", "打开"], {"default": "关闭"}),
"seed": ("INT", {
"default": 0, "min": 0, "max": 2147483647,
"tooltip": "seed 仅控制节点是否重新运行,结果本身不可复现。",
}),
},
"optional": {
"尾帧": ("IMAGE",),
@@ -58,30 +70,56 @@ class KVideo:
FUNCTION = "generate"
CATEGORY = "comfyui_o1key/KVideo"
async def generate(self, 起始帧, 提示词, 模式, 时长, 生成音频="关闭", 尾帧=None):
async def generate(self, 起始帧, 提示词, 模式, 时长, 生成音频="关闭", 尾帧=None, seed=0):
api_key = get_api_key_or_raise()
base_url = get_api_base_url()
base_url = get_async_api_base_url()
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
# ── 动态拼接模型名 ────────────────────────────────────────────
mode_api = "pro" # 1080p 映射为 pro
voice = "voice" if 生成音频 == "打开" else "novoice"
model_name = f"{_MODEL_BASE}-{模式}-{时长}s-{voice}"
model_name = f"{_MODEL_BASE}-{mode_api}-{时长}s-{voice}"
# ── 构建请求体 ────────────────────────────────────────────────
# ── 构建请求体(超过 10MB 自动缩放图片)────────────────────────
MAX_BODY = 10 * 1024 * 1024
scale = 1.0
print(f"[K26 图生视频] 请求体大小限制: 10MB,超出将自动缩放图片")
while True:
body = {
"model": model_name,
"prompt": 提示词.strip(),
"image": _image_to_base64(起始帧),
"mode": 模式,
"image": _image_to_base64(起始帧, scale),
"mode": mode_api,
"duration": 时长,
}
if 生成音频 == "打开":
body["generate_audio"] = True
if 尾帧 is not None:
body["metadata"] = {"image_tail": _image_to_base64(尾帧)}
body["metadata"] = {"image_tail": _image_to_base64(尾帧, scale)}
body_str = json.dumps(body, ensure_ascii=False)
body_size = len(body_str.encode("utf-8"))
if body_size <= MAX_BODY:
print(f"[K26 图生视频] 请求体大小: {body_size / 1024 / 1024:.2f}MB"
+ (f"(已缩放至 {scale:.1%}" if scale < 1.0 else ""))
break
# 等比缩放:图片像素面积与 base64 长度近似线性
target_ratio = MAX_BODY / body_size
scale = scale * math.sqrt(target_ratio) * 0.95 # 5% 安全余量
if scale < 0.01:
raise RuntimeError("图片缩放后仍超过10MB限制,请使用更小的参考图")
w, h = tensor_to_pil(起始帧)[0].size
print(f"[K26 图生视频] 请求体 {body_size / 1024 / 1024:.2f}MB 超限,"
f"自动缩放至 {scale:.1%}{int(w * scale)}x{int(h * scale)}")
# ── 进度条 ────────────────────────────────────────────────────
try:
@@ -205,9 +243,9 @@ class KVideo:
NODE_CLASS_MAPPINGS = {
"KVideo": KVideo,
"KVideoFirstLast": KVideoFirstLast,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"KVideo": "K26 图生视频",
"KVideoFirstLast": "K26 图生视频(首尾帧)",
}
+236
View File
@@ -0,0 +1,236 @@
"""
K26 图生视频节点
支持 720p 和 1080p 模式
"""
import asyncio
import json
import math
import os
import tempfile
import aiohttp
from ..utils.config import get_api_key_or_raise, get_async_api_base_url
from ..utils.image_utils import tensor_to_pil, encode_image_to_base64
try:
from comfy_api.latest import InputImpl
import folder_paths
_FOLDER_PATHS_OK = True
except ImportError:
_FOLDER_PATHS_OK = False
# 模型基础名,运行时动态拼接完整名称
_MODEL_BASE = "kling-v2-6"
# API 端点
_ENDPOINT_CREATE = "/v1/video/generations"
_ENDPOINT_STATUS = "/v1/video/generations/{task_id}"
_POLL_INIT = 3
_POLL_MAX = 15
def _image_to_base64(tensor, scale=1.0) -> str:
from PIL import Image
pil = tensor_to_pil(tensor)
img = pil[0]
if scale < 1.0:
w, h = img.size
new_w = max(1, int(w * scale))
new_h = max(1, int(h * scale))
img = img.resize((new_w, new_h), Image.LANCZOS)
return encode_image_to_base64(img, format="PNG")
class KVideoImage2Video:
"""K26 图生视频节点"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"起始帧": ("IMAGE",),
"提示词": ("STRING", {"multiline": True, "default": ""}),
"模式": (["720p", "1080p"], {"default": "720p"}),
"时长": ([5, 10], {"default": 5}),
"生成音频": (["关闭", "打开"], {"default": "关闭"}),
"seed": ("INT", {
"default": 0, "min": 0, "max": 2147483647,
"tooltip": "seed 仅控制节点是否重新运行,结果本身不可复现。",
}),
},
}
RETURN_TYPES = ("VIDEO",)
RETURN_NAMES = ("视频",)
FUNCTION = "generate"
CATEGORY = "comfyui_o1key/KVideo"
async def generate(self, 起始帧, 提示词, 模式, 时长, 生成音频="关闭", seed=0):
api_key = get_api_key_or_raise()
base_url = get_async_api_base_url()
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
# ── 动态拼接模型名 ────────────────────────────────────────────
mode_api = "std" if 模式 == "720p" else "pro"
voice = "voice" if 生成音频 == "打开" else "novoice"
model_name = f"{_MODEL_BASE}-{mode_api}-{时长}s-{voice}"
# ── 构建请求体(超过 10MB 自动缩放图片)────────────────────────
MAX_BODY = 10 * 1024 * 1024
scale = 1.0
print(f"[K26 图生视频] 请求体大小限制: 10MB,超出将自动缩放图片")
while True:
body = {
"model": model_name,
"prompt": 提示词.strip(),
"image": _image_to_base64(起始帧, scale),
"mode": mode_api,
"duration": 时长,
}
if 生成音频 == "打开":
body["generate_audio"] = True
body_str = json.dumps(body, ensure_ascii=False)
body_size = len(body_str.encode("utf-8"))
if body_size <= MAX_BODY:
print(f"[K26 图生视频] 请求体大小: {body_size / 1024 / 1024:.2f}MB"
+ (f"(已缩放至 {scale:.1%}" if scale < 1.0 else ""))
break
# 等比缩放:图片像素面积与 base64 长度近似线性
target_ratio = MAX_BODY / body_size
scale = scale * math.sqrt(target_ratio) * 0.95 # 5% 安全余量
if scale < 0.01:
raise RuntimeError("图片缩放后仍超过10MB限制,请使用更小的参考图")
w, h = tensor_to_pil(起始帧)[0].size
print(f"[K26 图生视频] 请求体 {body_size / 1024 / 1024:.2f}MB 超限,"
f"自动缩放至 {scale:.1%}{int(w * scale)}x{int(h * scale)}")
# ── 进度条 ────────────────────────────────────────────────────
try:
from comfy.utils import ProgressBar
pbar = ProgressBar(100)
except Exception:
pbar = None
def _stage(s: str):
if s == "submitting":
print("[K26 图生视频] 提交中...")
if pbar: pbar.update_absolute(0, 100)
elif s.startswith("submitted:"):
print(f"[K26 图生视频] 任务已提交 → {s.split(':', 1)[1]}")
if pbar: pbar.update_absolute(5, 100)
elif s == "downloading":
print("[K26 图生视频] 下载视频...")
if pbar: pbar.update_absolute(99, 100)
elif s == "done":
print("[K26 图生视频] 完成")
if pbar: pbar.update_absolute(100, 100)
def _progress(pct: int):
if pbar: pbar.update_absolute(5 + int(pct * 0.94), 100)
# ── 保存路径(临时文件,避免与下游保存节点重复落盘)──────────────────
tmp_fd, save_path = tempfile.mkstemp(suffix=".mp4", prefix="k26_")
connector = aiohttp.TCPConnector(ssl=False, force_close=True)
async with aiohttp.ClientSession(connector=connector) as session:
# 1. 提交
_stage("submitting")
create_url = f"{base_url}{_ENDPOINT_CREATE}"
async with session.post(create_url, json=body, headers=headers) as resp:
if resp.status != 200:
err_text = await resp.text()
raise RuntimeError(f"提交失败 ({resp.status}): {err_text}")
sr = await resp.json()
task_id = sr.get("task_id") or sr.get("id")
if not task_id:
raise RuntimeError(f"API 未返回 task_id,响应:{sr}")
_stage(f"submitted:{task_id}")
# 2. 轮询
status_url = f"{base_url}{_ENDPOINT_STATUS.format(task_id=task_id)}"
interval = _POLL_INIT
video_url = None
while True:
await asyncio.sleep(interval)
async with session.get(status_url, headers=headers) as resp:
if resp.status != 200:
err_text = await resp.text()
raise RuntimeError(f"查询失败 ({resp.status}): {err_text}")
sr = await resp.json()
data = sr.get("data", {}) or {}
status = (sr.get("status") or data.get("status") or "").lower()
pct_raw = str(data.get("progress", 0)).strip().rstrip('%')
try:
pct = max(0, min(100, int(float(pct_raw))))
except (ValueError, TypeError):
pct = 0
print(f"[K26 图生视频] 生成中 {pct}%")
_progress(pct)
if status in ("success", "completed", "done", "finished", "succeed"):
# 提取视频 URL
video_url = (
data.get("video_url")
or data.get("result_url")
or data.get("url")
or (data.get("result", {}) or {}).get("url")
or sr.get("video_url")
or sr.get("url")
)
break
if status in ("failed", "fail"):
err_info = data.get("error") or sr.get("error") or {}
err_msg = (err_info.get("message", "未知错误")
if isinstance(err_info, dict) else str(err_info))
raise RuntimeError(f"K26 生成失败:{err_msg}")
await asyncio.sleep(interval)
interval = min(interval * 1.5, _POLL_MAX)
if not video_url:
raise RuntimeError(f"API 未返回视频 URL,响应:{sr}")
# 3. 下载
_stage("downloading")
async with session.get(video_url, allow_redirects=True) as resp:
if resp.status != 200:
raise RuntimeError(f"视频下载失败 ({resp.status})")
os.close(tmp_fd)
with open(save_path, "wb") as f:
async for chunk in resp.content.iter_chunked(8192):
f.write(chunk)
_stage("done")
if _FOLDER_PATHS_OK:
return (InputImpl.VideoFromFile(save_path),)
return (save_path,)
NODE_CLASS_MAPPINGS = {
"KVideoImage2Video": KVideoImage2Video,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"KVideoImage2Video": "K26 图生视频",
}
+3 -2
View File
@@ -21,9 +21,10 @@ from .seedance_video import Seedance, SeedanceMultiModal
from .nano_banana_v2 import NanoBananaV2, NanoBananaV2Batch, AsyncImageGenerator, BatchAsyncImageGenerator
from .doubao_image import DoubaoImage
from .gpt_image import O1keyGPTImage
from .K_video import KVideo
from .K_video_firstlast import KVideoFirstLast
from .K_video_image2video import KVideoImage2Video
from .K3_video import K3Video
from .K3_video_firstlast import K3VideoFirstLast
from .K3_motion_control import K3MotionControl, K3MotionVideoCheck
__all__ = ['NanoBananaV2', 'NanoBananaV2Batch', 'NanoBananaPro', 'BatchNanoBananaPro', 'GoogleGemini', 'LoadFile', 'ImageStitchPro', 'SaveCleanImage', 'BatchCleanMetadata', 'VideoPreview', 'KlingVideo', 'KlingFirstLastFrame', 'KlingMotionControlTest', 'AspectRatioPreset', 'GoogleVeo', 'FluxImageEdit', 'UniversalLLMChat', 'MultiResPreview', 'BatchImagesO1key', 'Seedance', 'SeedanceMultiModal', 'StreamPreview', 'DoubaoImage', 'O1keyGPTImage', 'KVideo', 'K3Video', 'K3VideoFirstLast', 'K3MotionControl', 'K3MotionVideoCheck', 'AsyncImageGenerator', 'BatchAsyncImageGenerator']
__all__ = ['NanoBananaV2', 'NanoBananaV2Batch', 'NanoBananaPro', 'BatchNanoBananaPro', 'GoogleGemini', 'LoadFile', 'ImageStitchPro', 'SaveCleanImage', 'BatchCleanMetadata', 'VideoPreview', 'KlingVideo', 'KlingFirstLastFrame', 'KlingMotionControlTest', 'AspectRatioPreset', 'GoogleVeo', 'FluxImageEdit', 'UniversalLLMChat', 'MultiResPreview', 'BatchImagesO1key', 'Seedance', 'SeedanceMultiModal', 'StreamPreview', 'DoubaoImage', 'O1keyGPTImage', 'KVideoFirstLast', 'KVideoImage2Video', 'K3Video', 'K3VideoFirstLast', 'K3MotionControl', 'K3MotionVideoCheck', 'AsyncImageGenerator', 'BatchAsyncImageGenerator']
+6 -1
View File
@@ -63,6 +63,7 @@ DEBUG_LOG_ENABLED = False
REQUEST_LOG_ENABLED = False
_POLL_INTERVAL = 2 # 轮询间隔(秒)
_INTERRUPT_CHECK_INTERVAL = 0.1 # 取消检查间隔(秒)
_MAX_WAIT_TIME = 900 # 单任务最大等待时间(秒)
@@ -374,7 +375,11 @@ class NanoBananaV2:
friendly_msg = self._friendly_error(error_msg)
raise RuntimeError(f"任务失败: {friendly_msg}")
elif status in ("SUBMITTED", "IN_PROGRESS"):
await asyncio.sleep(_POLL_INTERVAL)
# 分段 sleep,每 0.1 秒检查一次取消信号
sleep_iterations = int(_POLL_INTERVAL / _INTERRUPT_CHECK_INTERVAL)
for _ in range(sleep_iterations):
self._check_interrupt()
await asyncio.sleep(_INTERRUPT_CHECK_INTERVAL)
else:
raise RuntimeError(f"未知任务状态: {status}")
+4 -2
View File
@@ -18,7 +18,8 @@ async def _presign(filename: str, content_type: str) -> tuple:
api_key = get_api_key_or_raise()
base_url = get_api_base_url()
async with aiohttp.ClientSession() as session:
connector = aiohttp.TCPConnector(ssl=False)
async with aiohttp.ClientSession(connector=connector) as session:
async with session.post(
f"{base_url}/v1/storage/presign",
headers={"Authorization": f"Bearer {api_key}"},
@@ -35,7 +36,8 @@ async def _presign(filename: str, content_type: str) -> tuple:
async def _put_upload(upload_url: str, data: bytes, content_type: str):
"""用预签名 URL 直传文件到 R2(不带 Authorization"""
async with aiohttp.ClientSession() as session:
connector = aiohttp.TCPConnector(ssl=False)
async with aiohttp.ClientSession(connector=connector) as session:
async with session.put(
upload_url,
data=data,