feat(发布计划): 素材支持图片(图文)—— 上传视频/图片,发布流程共用一条 + 切号健壮性再修
用户实测反馈:"图片流程一样,现在也是点第一个" → 图片不做单独一套流程,只放开上传侧; 扩展名一路原样带到手机上(抖音按扩展名/MIME 判断视频还是图文,图片推成 .mp4 会变坏视频)。 · core/video_plan.py:VIDEO_EXTS + IMAGE_EXTS(jpg/jpeg/png/webp/heic) → MEDIA_EXTS; 报错文案改"不是视频/图片文件" · tasks/generic/publish_flow.push_video:ext 默认**从本地文件名取**(不再硬编码 .mp4); drop_old_copies 收到真实扩展名 · tasks/generic/task.py:删手机素材优先用推送时记下的 push_remote,其次按素材扩展名现算; 切号:等面板开稳再点(刚出现就点会被入场动画吞掉)、点完面板还开着(点的就是当前号) 就先关面板、一切以**回读**为准、回读窗口 12s→30s(换号会触发抖音整体重启,实测 77s 才回来); 失败时日志附带"当时屏幕上是什么"(亮屏/前台包/屏上文字)—— "点了没反应"一眼可判 · 骨架:先 stop_app 冷启动(抖音可能停在发布页/别的视频页,那种页面上没有「我」页签, 切号第一步就废);「校验账号」的选择器与「读名字」统一从 .env 取(漏了这一步会静默跳过 整个发布流程 —— 用户实际遇到的"直接完成、没发布"就是这个) · 界面:上传视频 → 上传视频/图片,accept 加 image/*,明细表"视频文件"→"素材文件",help 文案 · 文档:TASK_DEV §4.7 / API / DATA_MODEL / ARCHITECTURE / README 同步 · 测试:解析用例补图片(jpg/png/webp/大写 JPG 认,gif 不收)+ 改报错文案期望
This commit is contained in:
+40
-21
@@ -58,7 +58,12 @@ STAGE_STATUS = {"push": ST_FAILED, "scan": ST_FAILED,
|
||||
"post": ST_UNKNOWN, "verify": ST_UNKNOWN,
|
||||
STAGE_MANUAL: ST_UNKNOWN}
|
||||
|
||||
# 素材扩展名:**视频 + 图片**(图片就是抖音的"图文",发布流程一样:相册里点第一个)。
|
||||
# 扩展名必须一路带到手机上 —— 抖音是按扩展名/MIME 判断这是视频还是图片的,
|
||||
# 给图片起个 .mp4 的名字,相册里就成了一段坏视频。
|
||||
VIDEO_EXTS = (".mp4", ".mov", ".mkv", ".avi", ".3gp", ".flv", ".wmv", ".m4v", ".ts", ".webm")
|
||||
IMAGE_EXTS = (".jpg", ".jpeg", ".png", ".webp", ".heic")
|
||||
MEDIA_EXTS = VIDEO_EXTS + IMAGE_EXTS
|
||||
MAX_TITLE_LEN = 100 # 超过就拒收(**不静默截断**:截断会发出错文案,用户看不见)
|
||||
MAX_BODY_LEN = 1000 # 正文上限(抖音文案框放得下 1000 字;同样不截断,超了拒收)
|
||||
MAX_ATTEMPTS = 3 # 尝试上限,超了不再自动取(人工确认后重试可清零)
|
||||
@@ -166,7 +171,7 @@ def name_error(seg):
|
||||
|
||||
|
||||
def parse_video_name(filename):
|
||||
"""视频文件名 `名字_日期_编号` → (meta, error)。
|
||||
"""素材文件名 `名字_日期_编号` → (meta, error)。**视频和图片同一套规则**。
|
||||
|
||||
meta = {"name","date","seq"|None,"seq_given"};error 是给人看的拒收原因。
|
||||
|
||||
@@ -180,8 +185,9 @@ def parse_video_name(filename):
|
||||
if not name:
|
||||
return None, "文件名为空"
|
||||
stem, ext = os.path.splitext(name)
|
||||
if ext.lower() not in VIDEO_EXTS:
|
||||
return None, f"不是视频文件(支持 {'/'.join(e.lstrip('.') for e in VIDEO_EXTS)})"
|
||||
if ext.lower() not in MEDIA_EXTS:
|
||||
return None, (f"不是视频/图片文件(支持 "
|
||||
f"{'/'.join(e.lstrip('.') for e in MEDIA_EXTS)})")
|
||||
parts = [p.strip() for p in stem.split("_")]
|
||||
if len(parts) < 2:
|
||||
return None, "文件名格式不对,应为 名字_日期_编号(编号可省略,如 小王_20260912.mp4)"
|
||||
@@ -554,6 +560,12 @@ def build_release_steps():
|
||||
def sid(name):
|
||||
return f"step_rel_{name}_{uuid.uuid4().hex[:6]}"
|
||||
|
||||
def _name_sel():
|
||||
"""昵称控件的选择器(`config` 里抓一次填一次)—— 「读名字」和「校验账号」
|
||||
**必须是同一个控件**,否则校验那步会永远不命中(等于跳过整个发布流程)。"""
|
||||
from config import DOUYIN_NAME_SELECTOR_TYPE, DOUYIN_NAME_SELECTOR_VALUE
|
||||
return (DOUYIN_NAME_SELECTOR_TYPE or "xpath", DOUYIN_NAME_SELECTOR_VALUE)
|
||||
|
||||
def clk(name, label, sel_type="", sel_val=""):
|
||||
return {"id": sid(name), "type": "click", "label": label,
|
||||
"params": {"selector_type": sel_type or "xpath", "selector_value": sel_val,
|
||||
@@ -565,35 +577,35 @@ def build_release_steps():
|
||||
# **只用「包含」,绝不能用「不等于」** —— 读不到名字时候选为空,
|
||||
# "空 != 候选"会命中,把"读不到"误判成"可以发"。
|
||||
publish_flow_steps = [
|
||||
dict(build_push_step("⑦ 推送发布视频")),
|
||||
clk("open", "⑧ 点击抖音底部「+」(实测可用;改版了重新抓)",
|
||||
dict(build_push_step("⑧ 推送发布视频")),
|
||||
clk("open", "⑨ 点击抖音底部「+」(实测可用;改版了重新抓)",
|
||||
"descriptionContains", "拍摄"),
|
||||
clk("album", "⑨ 点击「相册」(没命中的话用抓取元素选它)"),
|
||||
clk("video", "⑩ 点击第一个视频(刚推的排最前;抓一次就行)"),
|
||||
clk("next", "⑪ 点击「下一步」"),
|
||||
clk("input", "⑫ 点击标题框(抖音发布页的文案框)"),
|
||||
clk("album", "⑩ 点击「相册」(没命中的话用抓取元素选它)"),
|
||||
clk("video", "⑪ 点击第一个视频(刚推的排最前;抓一次就行)"),
|
||||
clk("next", "⑫ 点击「下一步」"),
|
||||
clk("input", "⑬ 点击标题框(抖音发布页的标题框:hint「添加标题」)"),
|
||||
{"id": sid("title"), "type": "input_text",
|
||||
"label": "⑬ 填标题(自动取这一条计划的标题,会回读校验)",
|
||||
"label": "⑭ 填标题(自动取这一条计划的标题,会回读校验)",
|
||||
"params": {"text_source": "release_title", "release_topics": "",
|
||||
"clear_first": True, "mode": "random", "texts": "", "fixed_text": ""}},
|
||||
clk("body", "⑭ 点击正文框(发布页的正文框)"),
|
||||
clk("body", "⑮ 点击正文框(发布页的描述框:hint「添加作品描述」)"),
|
||||
{"id": sid("bodytxt"), "type": "input_text",
|
||||
"label": "⑮ 填正文(自动取这一条计划的正文,会回读校验)",
|
||||
"label": "⑯ 填正文(自动取这一条计划的正文,会回读校验)",
|
||||
"params": {"text_source": "release_body", "release_topics": "",
|
||||
"clear_first": True, "mode": "random", "texts": "", "fixed_text": ""}},
|
||||
clk("publish", "⑯ 点击「发布」"),
|
||||
dict(build_mark_step("⑰ 标记发布结果(成功走这里;失败分支再放一个 result=failed)")),
|
||||
clk("publish", "⑰ 点击「发布」"),
|
||||
dict(build_mark_step("⑱ 标记发布结果(成功走这里;失败分支再放一个 result=failed)")),
|
||||
]
|
||||
# 循环体:切号 → 读名字 → 校验 → 推送 → 发布 → 标记。
|
||||
# `switch_account` 自己会先看"当前是不是已经是目标"(第一个号不用切),
|
||||
# 也会处理"本机没登这个号"(直接跳过、不重试)。
|
||||
loop_body = [
|
||||
dict(build_switch_step("③ 切换账号(切到本次要发的那个名字;已在该号则直接过)")),
|
||||
dict(build_read_name_step("④ 读取本机当前账号名字")),
|
||||
dict(build_switch_step("④ 切换账号(切到本次要发的那个名字;已在该号则直接过)")),
|
||||
dict(build_read_name_step("⑤ 读取本机当前账号名字")),
|
||||
{"id": sid("check"), "type": "if_el",
|
||||
"label": "⑤ 校验账号:当前登的是不是这次要发的那个名字",
|
||||
"params": {"selector_type": "xpath",
|
||||
"selector_value": "",
|
||||
"label": "⑥ 校验账号:当前登的是不是这次要发的那个名字",
|
||||
"params": {"selector_type": _name_sel()[0],
|
||||
"selector_value": _name_sel()[1],
|
||||
"timeout": 8, "cmp_op": "包含", "cmp_source": "account_target",
|
||||
"cmp_value": "", "cmp_group": "",
|
||||
"ident_type": "text", "ident_value": "",
|
||||
@@ -608,12 +620,19 @@ def build_release_steps():
|
||||
return [
|
||||
{"id": sid("light"), "type": "screen_on", "label": "⓪ 亮屏",
|
||||
"params": {"probability": 100}},
|
||||
{"id": sid("openapp"), "type": "open_app", "label": "① 打开抖音(等「首页」出来)",
|
||||
# **先强杀再冷启动**(2026-10-12 实测踩出来的):抖音可能停在**发布页/编辑页/别人的
|
||||
# 视频页**(上一轮没发出去、或人手动停在那儿),`open_app` 只会把它拉到前台、
|
||||
# 还是那一页 —— 那一页**没有底部「我」页签**,切号第一步就失败,整轮什么都不发。
|
||||
# 冷启动保证起来是「首页」。
|
||||
{"id": sid("stopapp"), "type": "stop_app",
|
||||
"label": "① 结束抖音(冷启动,保证起来停在「首页」)",
|
||||
"params": {"package": "com.ss.android.ugc.aweme", "probability": 100}},
|
||||
{"id": sid("openapp"), "type": "open_app", "label": "② 打开抖音(等「首页」出来)",
|
||||
"params": {"package": "com.ss.android.ugc.aweme", "home_feature": "首页",
|
||||
"wait_home": True, "probability": 100}},
|
||||
# 一轮把本机待发的号跑完:数据源是**计划表**(谁还有待发内容谁就在里面),
|
||||
# 不需要任何账号维护;停止/超时/上限三个出口见 `_exec_foreach_account`
|
||||
dict(build_foreach_step(loop_body, "② 循环本机待发的号(一直切号发,直到发完)")),
|
||||
dict(build_foreach_step(loop_body, "③ 循环本机待发的号(一直切号发,直到发完)")),
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user