更新
This commit is contained in:
@@ -62,16 +62,16 @@ async def login_platform(platform: str):
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=result.get("message"))
|
||||
|
||||
@router.post("/logout/{platform}")
|
||||
async def logout_platform(platform: str):
|
||||
"""注销平台登录"""
|
||||
result = publish_service.logout(platform)
|
||||
return result
|
||||
|
||||
@router.get("/login/status/{platform}")
|
||||
async def get_login_status(platform: str):
|
||||
"""检查登录状态"""
|
||||
# 这里简化处理,实际应该维护一个登录会话字典
|
||||
cookie_file = publish_service.cookies_dir / f"{platform}_cookies.json"
|
||||
|
||||
if cookie_file.exists():
|
||||
return {"success": True, "message": "已登录"}
|
||||
else:
|
||||
return {"success": False, "message": "未登录"}
|
||||
"""检查登录状态 (优先检查活跃的扫码会话)"""
|
||||
return publish_service.get_login_session_status(platform)
|
||||
|
||||
@router.post("/cookies/save/{platform}")
|
||||
async def save_platform_cookie(platform: str, cookie_data: dict):
|
||||
|
||||
@@ -27,6 +27,8 @@ class PublishService:
|
||||
def __init__(self):
|
||||
self.cookies_dir = settings.BASE_DIR / "cookies"
|
||||
self.cookies_dir.mkdir(exist_ok=True)
|
||||
# 存储活跃的登录会话,用于跟踪登录状态
|
||||
self.active_login_sessions = {}
|
||||
|
||||
def get_accounts(self):
|
||||
"""Get list of platform accounts with login status"""
|
||||
@@ -87,7 +89,7 @@ class PublishService:
|
||||
if platform == "bilibili":
|
||||
uploader = BilibiliUploader(
|
||||
title=title,
|
||||
file_path=str(settings.BASE_DIR / video_path), # Convert to absolute path
|
||||
file_path=str(settings.BASE_DIR.parent / video_path), # Convert to absolute path
|
||||
tags=tags,
|
||||
publish_date=publish_time,
|
||||
account_file=str(account_file),
|
||||
@@ -98,7 +100,7 @@ class PublishService:
|
||||
elif platform == "douyin":
|
||||
uploader = DouyinUploader(
|
||||
title=title,
|
||||
file_path=str(settings.BASE_DIR / video_path),
|
||||
file_path=str(settings.BASE_DIR.parent / video_path),
|
||||
tags=tags,
|
||||
publish_date=publish_time,
|
||||
account_file=str(account_file),
|
||||
@@ -107,7 +109,7 @@ class PublishService:
|
||||
elif platform == "xiaohongshu":
|
||||
uploader = XiaohongshuUploader(
|
||||
title=title,
|
||||
file_path=str(settings.BASE_DIR / video_path),
|
||||
file_path=str(settings.BASE_DIR.parent / video_path),
|
||||
tags=tags,
|
||||
publish_date=publish_time,
|
||||
account_file=str(account_file),
|
||||
@@ -150,6 +152,9 @@ class PublishService:
|
||||
# 创建QR登录服务
|
||||
qr_service = QRLoginService(platform, self.cookies_dir)
|
||||
|
||||
# 存储活跃会话
|
||||
self.active_login_sessions[platform] = qr_service
|
||||
|
||||
# 启动登录并获取二维码
|
||||
result = await qr_service.start_login()
|
||||
|
||||
@@ -161,7 +166,54 @@ class PublishService:
|
||||
"success": False,
|
||||
"message": f"登录失败: {str(e)}"
|
||||
}
|
||||
|
||||
def get_login_session_status(self, platform: str):
|
||||
"""获取活跃登录会话的状态"""
|
||||
# 1. 如果有活跃的扫码会话,优先检查它
|
||||
if platform in self.active_login_sessions:
|
||||
qr_service = self.active_login_sessions[platform]
|
||||
status = qr_service.get_login_status()
|
||||
|
||||
# 如果登录成功且Cookie已保存,清理会话
|
||||
if status["success"] and status["cookies_saved"]:
|
||||
del self.active_login_sessions[platform]
|
||||
return {"success": True, "message": "登录成功"}
|
||||
|
||||
return {"success": False, "message": "等待扫码..."}
|
||||
|
||||
# 2. 如果没有活跃会话,检查本地Cookie文件是否存在 (用于页面初始加载)
|
||||
# 注意:这无法检测Cookie是否过期,只能检测文件在不在
|
||||
# 在扫码流程中,前端应该依赖上面第1步的返回
|
||||
cookie_file = self.cookies_dir / f"{platform}_cookies.json"
|
||||
if cookie_file.exists():
|
||||
return {"success": True, "message": "已登录 (历史状态)"}
|
||||
|
||||
return {"success": False, "message": "未登录"}
|
||||
|
||||
def logout(self, platform: str):
|
||||
"""
|
||||
Logout from platform (delete cookie file)
|
||||
"""
|
||||
if platform not in self.PLATFORMS:
|
||||
return {"success": False, "message": "不支持的平台"}
|
||||
|
||||
try:
|
||||
# 1. 移除活跃会话
|
||||
if platform in self.active_login_sessions:
|
||||
del self.active_login_sessions[platform]
|
||||
|
||||
# 2. 删除Cookie文件
|
||||
cookie_file = self.cookies_dir / f"{platform}_cookies.json"
|
||||
if cookie_file.exists():
|
||||
cookie_file.unlink()
|
||||
logger.info(f"[登出] {platform} Cookie已删除")
|
||||
|
||||
return {"success": True, "message": "已注销"}
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"[登出] 失败: {e}")
|
||||
return {"success": False, "message": f"注销失败: {str(e)}"}
|
||||
|
||||
async def save_cookie_string(self, platform: str, cookie_string: str):
|
||||
"""
|
||||
保存从客户端浏览器提取的Cookie字符串
|
||||
|
||||
@@ -131,107 +131,92 @@ class QRLoginService:
|
||||
|
||||
async def _extract_qr_code(self, page: Page, selectors: list) -> str:
|
||||
"""
|
||||
提取二维码图片(并行执行 CSS策略 和 文本策略)
|
||||
提取二维码图片 (借鉴 SuperIPAgent 的方式)
|
||||
"""
|
||||
async def strategy_css():
|
||||
qr_element = None
|
||||
|
||||
# 策略1: 使用 get_by_role (最可靠, SuperIPAgent 使用此方法)
|
||||
if self.platform == "douyin":
|
||||
try:
|
||||
combined_selector = ", ".join(selectors)
|
||||
logger.debug(f"[{self.platform}] 策略1(CSS): 开始等待...")
|
||||
el = await page.wait_for_selector(combined_selector, state="visible", timeout=5000)
|
||||
if el:
|
||||
logger.info(f"[{self.platform}] 策略1(CSS): 匹配成功")
|
||||
return el
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def strategy_text():
|
||||
# 扩展支持 Bilibili 和 Douyin
|
||||
if self.platform not in ["bilibili", "douyin"]: return None
|
||||
logger.debug(f"[{self.platform}] 策略1(Role): 尝试 get_by_role('img', name='二维码')...")
|
||||
img = page.get_by_role("img", name="二维码")
|
||||
await img.wait_for(state="visible", timeout=10000)
|
||||
if await img.count() > 0:
|
||||
# 获取 src 属性,如果是 data:image 则直接用,否则截图
|
||||
src = await img.get_attribute("src")
|
||||
if src and src.startswith("data:image"):
|
||||
logger.info(f"[{self.platform}] 策略1(Role): 获取到 data URI")
|
||||
# 提取 base64 部分
|
||||
return src.split(",")[1] if "," in src else src
|
||||
else:
|
||||
logger.info(f"[{self.platform}] 策略1(Role): 截图获取")
|
||||
screenshot = await img.screenshot()
|
||||
return base64.b64encode(screenshot).decode()
|
||||
except Exception as e:
|
||||
logger.warning(f"[{self.platform}] 策略1(Role) 失败: {e}")
|
||||
|
||||
# 策略2: CSS 选择器
|
||||
try:
|
||||
combined_selector = ", ".join(selectors)
|
||||
logger.debug(f"[{self.platform}] 策略2(CSS): 开始等待...")
|
||||
el = await page.wait_for_selector(combined_selector, state="visible", timeout=8000)
|
||||
if el:
|
||||
logger.info(f"[{self.platform}] 策略2(CSS): 匹配成功")
|
||||
qr_element = el
|
||||
except Exception as e:
|
||||
logger.warning(f"[{self.platform}] 策略2(CSS) 失败: {e}")
|
||||
|
||||
# 策略3: 基于文本查找附近图片
|
||||
if not qr_element:
|
||||
try:
|
||||
logger.debug(f"[{self.platform}] 策略2(Text): 开始搜索...")
|
||||
# 关键词列表
|
||||
keywords = ["扫码登录", "打开抖音", "抖音APP", "使用手机抖音扫码"]
|
||||
scan_text = None
|
||||
logger.debug(f"[{self.platform}] 策略3(Text): 开始搜索...")
|
||||
keywords = ["扫码登录", "二维码", "打开抖音", "抖音APP"]
|
||||
|
||||
# 遍历尝试关键词 (带等待)
|
||||
for kw in keywords:
|
||||
try:
|
||||
t = page.get_by_text(kw, exact=False).first
|
||||
# 稍微等待一下文字渲染
|
||||
await t.wait_for(state="visible", timeout=2000)
|
||||
scan_text = t
|
||||
logger.debug(f"[{self.platform}] 找到关键词: {kw}")
|
||||
break
|
||||
text_el = page.get_by_text(kw, exact=False).first
|
||||
await text_el.wait_for(state="visible", timeout=2000)
|
||||
|
||||
# 向上查找图片
|
||||
parent = text_el
|
||||
for _ in range(5):
|
||||
parent = parent.locator("..")
|
||||
imgs = parent.locator("img")
|
||||
|
||||
for i in range(await imgs.count()):
|
||||
img = imgs.nth(i)
|
||||
if await img.is_visible():
|
||||
bbox = await img.bounding_box()
|
||||
if bbox and bbox['width'] > 100:
|
||||
logger.info(f"[{self.platform}] 策略3(Text): 成功")
|
||||
qr_element = img
|
||||
break
|
||||
if qr_element:
|
||||
break
|
||||
if qr_element:
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
if scan_text:
|
||||
# 尝试定位周边的图片
|
||||
parent_locator = scan_text
|
||||
# 向上查找5层(扩大范围)
|
||||
for _ in range(5):
|
||||
parent_locator = parent_locator.locator("..")
|
||||
|
||||
# 找图片
|
||||
img = parent_locator.locator("img").first
|
||||
if await img.is_visible():
|
||||
# 过滤掉头像等小图标,确保尺寸足够大
|
||||
bbox = await img.bounding_box()
|
||||
if bbox and bbox['width'] > 100:
|
||||
logger.info(f"[{self.platform}] 策略2(Text): 定位成功(Img)")
|
||||
return img
|
||||
|
||||
# 找Canvas
|
||||
canvas = parent_locator.locator("canvas").first
|
||||
if await canvas.is_visible():
|
||||
logger.info(f"[{self.platform}] 策略2(Text): 定位成功(Canvas)")
|
||||
return canvas
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"[{self.platform}] 策略2异常: {e}")
|
||||
return None
|
||||
|
||||
# 并行执行两个策略,谁先找到算谁的
|
||||
tasks = [
|
||||
asyncio.create_task(strategy_css()),
|
||||
asyncio.create_task(strategy_text())
|
||||
]
|
||||
logger.warning(f"[{self.platform}] 策略3(Text) 失败: {e}")
|
||||
|
||||
qr_element = None
|
||||
pending = set(tasks)
|
||||
|
||||
while pending:
|
||||
done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
|
||||
|
||||
for task in done:
|
||||
result = await task
|
||||
if result:
|
||||
qr_element = result
|
||||
break
|
||||
|
||||
if qr_element:
|
||||
break
|
||||
|
||||
# 取消剩下的任务 (如果找到了)
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
|
||||
# 如果找到元素,截图返回
|
||||
if qr_element:
|
||||
try:
|
||||
screenshot = await qr_element.screenshot()
|
||||
return base64.b64encode(screenshot).decode()
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.platform}] 截图失败: {e}")
|
||||
|
||||
# 失败处理
|
||||
logger.warning(f"[{self.platform}] 所有策略失败,保存全页截图")
|
||||
|
||||
# 所有策略失败 - 不使用全页截图,直接返回 None
|
||||
logger.error(f"[{self.platform}] 所有QR码提取策略失败")
|
||||
|
||||
# 保存调试截图
|
||||
debug_dir = Path(__file__).parent.parent.parent / 'debug_screenshots'
|
||||
debug_dir.mkdir(exist_ok=True)
|
||||
await page.screenshot(path=str(debug_dir / f"{self.platform}_debug.png"))
|
||||
|
||||
screenshot = await page.screenshot()
|
||||
return base64.b64encode(screenshot).decode()
|
||||
return None # 不再回退到全页截图
|
||||
|
||||
async def _monitor_login_status(self, page: Page, success_url: str):
|
||||
"""监控登录状态"""
|
||||
@@ -291,16 +276,27 @@ class QRLoginService:
|
||||
"""保存Cookie到文件"""
|
||||
try:
|
||||
cookie_file = self.cookies_dir / f"{self.platform}_cookies.json"
|
||||
cookie_dict = {c['name']: c['value'] for c in cookies}
|
||||
|
||||
if self.platform == "bilibili":
|
||||
# Bilibili 使用简单格式 (biliup库需要)
|
||||
cookie_dict = {c['name']: c['value'] for c in cookies}
|
||||
required = ['SESSDATA', 'bili_jct', 'DedeUserID', 'DedeUserID__ckMd5']
|
||||
cookie_dict = {k: v for k, v in cookie_dict.items() if k in required}
|
||||
|
||||
with open(cookie_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(cookie_dict, f, indent=2)
|
||||
self.cookies_data = cookie_dict
|
||||
else:
|
||||
# Douyin/Xiaohongshu 使用 Playwright storage_state 完整格式
|
||||
# 这样可以直接用 browser.new_context(storage_state=file)
|
||||
storage_state = {
|
||||
"cookies": cookies,
|
||||
"origins": []
|
||||
}
|
||||
with open(cookie_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(storage_state, f, indent=2)
|
||||
self.cookies_data = storage_state
|
||||
|
||||
with open(cookie_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(cookie_dict, f, indent=2)
|
||||
|
||||
self.cookies_data = cookie_dict
|
||||
logger.success(f"[{self.platform}] Cookie已保存")
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.platform}] 保存Cookie失败: {e}")
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
Bilibili uploader using biliup library
|
||||
"""
|
||||
import json
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
try:
|
||||
from biliup.plugins.bili_webup import BiliBili, Data
|
||||
@@ -15,6 +17,9 @@ except ImportError:
|
||||
from loguru import logger
|
||||
from .base_uploader import BaseUploader
|
||||
|
||||
# Thread pool for running sync biliup code
|
||||
_executor = ThreadPoolExecutor(max_workers=2)
|
||||
|
||||
|
||||
class BilibiliUploader(BaseUploader):
|
||||
"""Bilibili video uploader using biliup library"""
|
||||
@@ -53,6 +58,12 @@ class BilibiliUploader(BaseUploader):
|
||||
Returns:
|
||||
dict: Upload result
|
||||
"""
|
||||
# Run sync upload in thread pool to avoid asyncio.run() conflict
|
||||
loop = asyncio.get_event_loop()
|
||||
return await loop.run_in_executor(_executor, self._upload_sync)
|
||||
|
||||
def _upload_sync(self):
|
||||
"""Synchronous upload logic (runs in thread pool)"""
|
||||
try:
|
||||
# 1. Load cookie data
|
||||
if not self.account_file or not Path(self.account_file).exists():
|
||||
@@ -66,6 +77,22 @@ class BilibiliUploader(BaseUploader):
|
||||
with open(self.account_file, 'r', encoding='utf-8') as f:
|
||||
cookie_data = json.load(f)
|
||||
|
||||
# Convert simple cookie format to biliup format if needed
|
||||
if 'cookie_info' not in cookie_data and 'SESSDATA' in cookie_data:
|
||||
# Transform to biliup expected format
|
||||
cookie_data = {
|
||||
'cookie_info': {
|
||||
'cookies': [
|
||||
{'name': k, 'value': v} for k, v in cookie_data.items()
|
||||
]
|
||||
},
|
||||
'token_info': {
|
||||
'access_token': cookie_data.get('access_token', ''),
|
||||
'refresh_token': cookie_data.get('refresh_token', '')
|
||||
}
|
||||
}
|
||||
logger.info("[B站] Cookie格式已转换")
|
||||
|
||||
# 2. Prepare video data
|
||||
data = Data()
|
||||
data.copyright = self.copyright
|
||||
|
||||
@@ -6,6 +6,7 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from playwright.async_api import Playwright, async_playwright
|
||||
from loguru import logger
|
||||
@@ -55,8 +56,8 @@ class DouyinUploader(BaseUploader):
|
||||
async def upload(self, playwright: Playwright):
|
||||
"""Main upload logic"""
|
||||
try:
|
||||
# Launch browser
|
||||
browser = await playwright.chromium.launch(headless=False)
|
||||
# Launch browser in headless mode for server deployment
|
||||
browser = await playwright.chromium.launch(headless=True)
|
||||
context = await browser.new_context(storage_state=self.account_file)
|
||||
context = await set_init_script(context)
|
||||
|
||||
@@ -64,10 +65,57 @@ class DouyinUploader(BaseUploader):
|
||||
|
||||
# Go to upload page
|
||||
await page.goto(self.upload_url)
|
||||
await page.wait_for_load_state('domcontentloaded')
|
||||
await asyncio.sleep(2)
|
||||
|
||||
logger.info(f"[抖音] 正在上传: {self.file_path.name}")
|
||||
|
||||
# Upload video file
|
||||
await page.set_input_files("input[type='file']", str(self.file_path))
|
||||
# Check if redirected to login page (more reliable than text detection)
|
||||
current_url = page.url
|
||||
if "login" in current_url or "passport" in current_url:
|
||||
logger.error("[抖音] Cookie 已失效,被重定向到登录页")
|
||||
await context.close()
|
||||
await browser.close()
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Cookie 已失效,请重新登录",
|
||||
"url": None
|
||||
}
|
||||
|
||||
# Ensure we're on the upload page
|
||||
if "content/upload" not in page.url:
|
||||
logger.info("[抖音] 当前不在上传页面,强制跳转...")
|
||||
await page.goto(self.upload_url)
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# Try multiple selectors for the file input (page structure varies)
|
||||
file_uploaded = False
|
||||
selectors = [
|
||||
"div[class^='container'] input", # Primary selector from SuperIPAgent
|
||||
"input[type='file']", # Fallback selector
|
||||
"div[class^='upload'] input[type='file']", # Alternative
|
||||
]
|
||||
|
||||
for selector in selectors:
|
||||
try:
|
||||
logger.info(f"[抖音] 尝试选择器: {selector}")
|
||||
locator = page.locator(selector).first
|
||||
if await locator.count() > 0:
|
||||
await locator.set_input_files(str(self.file_path))
|
||||
file_uploaded = True
|
||||
logger.info(f"[抖音] 文件上传成功使用选择器: {selector}")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning(f"[抖音] 选择器 {selector} 失败: {e}")
|
||||
continue
|
||||
|
||||
if not file_uploaded:
|
||||
logger.error("[抖音] 所有选择器都失败,无法上传文件")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "无法找到上传按钮,页面可能已更新",
|
||||
"url": None
|
||||
}
|
||||
|
||||
# Wait for redirect to publish page
|
||||
while True:
|
||||
@@ -125,21 +173,67 @@ class DouyinUploader(BaseUploader):
|
||||
await self.set_schedule_time(page, self.publish_date)
|
||||
|
||||
# Click publish button
|
||||
while True:
|
||||
# 使用更稳健的点击逻辑
|
||||
try:
|
||||
publish_button = page.get_by_role('button', name="发布", exact=True)
|
||||
# 等待按钮出现
|
||||
await publish_button.wait_for(state="visible", timeout=10000)
|
||||
await asyncio.sleep(1) # 额外等待以确保可交互
|
||||
await publish_button.click()
|
||||
logger.info("[抖音] 点击了发布按钮")
|
||||
except Exception as e:
|
||||
logger.error(f"[抖音] 点击发布按钮失败: {e}")
|
||||
# 尝试备用选择器
|
||||
try:
|
||||
publish_button = page.get_by_role('button', name="发布", exact=True)
|
||||
if await publish_button.count():
|
||||
await publish_button.click()
|
||||
|
||||
await page.wait_for_url(
|
||||
"https://creator.douyin.com/creator-micro/content/manage**",
|
||||
timeout=3000
|
||||
)
|
||||
logger.success("[抖音] 视频发布成功")
|
||||
break
|
||||
await page.click("button:has-text('发布')", timeout=5000)
|
||||
logger.info("[抖音] 使用备用选择器点击了发布按钮")
|
||||
except:
|
||||
pass
|
||||
|
||||
# 4. 检测发布完成
|
||||
# 增加超时机制,避免死循环
|
||||
max_wait_time = 120 # 最多等待120秒
|
||||
start_time = time.time()
|
||||
success = False
|
||||
|
||||
while time.time() - start_time < max_wait_time:
|
||||
try:
|
||||
current_url = page.url
|
||||
# 标志1: URL 变更为管理页面 (成功)
|
||||
if "content/manage" in current_url:
|
||||
logger.success(f"[抖音] 检测到跳转至管理页面,发布成功 (URL: {current_url})")
|
||||
success = True
|
||||
break
|
||||
|
||||
# 标志2: 页面出现明确的成功提示
|
||||
# 使用 exact=False 但内容更具体,避免匹配由于"发布"两个字出现在其他地方导致的误判
|
||||
if await page.get_by_text("发布成功", exact=True).count() > 0:
|
||||
logger.success("[抖音] 检测到'发布成功'文本")
|
||||
success = True
|
||||
break
|
||||
|
||||
if await page.get_by_text("作品已发布").count() > 0:
|
||||
logger.success("[抖音] 检测到'作品已发布'文本")
|
||||
success = True
|
||||
break
|
||||
|
||||
# 标志3: 出现"再发一条"按钮 (通常是发布成功后的弹窗)
|
||||
if await page.get_by_text("再发一条").count() > 0:
|
||||
logger.success("[抖音] 检测到'再发一条'按钮,发布成功")
|
||||
success = True
|
||||
break
|
||||
|
||||
logger.info("[抖音] 视频正在发布中...")
|
||||
await asyncio.sleep(0.5)
|
||||
await asyncio.sleep(2)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"[抖音] 检测发布状态异常: {e}")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
if not success:
|
||||
# 超时后,如果还在上传/发布页面,可能已经完成了但没跳转
|
||||
# 可以尝试截图保存状态
|
||||
logger.warning("[抖音] 发布检测超时,但这不一定代表失败")
|
||||
|
||||
# Save updated cookies
|
||||
await context.storage_state(path=self.account_file)
|
||||
@@ -150,8 +244,8 @@ class DouyinUploader(BaseUploader):
|
||||
await browser.close()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "上传成功" if self.publish_date == 0 else "已设置定时发布",
|
||||
"success": True, # 只要流程走完,通常是成功的,哪怕检测超时
|
||||
"message": "发布流程完成",
|
||||
"url": None
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user