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:
2026-10-12 10:13:49 +08:00
parent 1e8b97e386
commit 1b65ffe089
13 changed files with 167 additions and 65 deletions
+2 -2
View File
@@ -139,7 +139,7 @@
| GET | `/api/done_marks?job=&limit=` | L | 去重记录列表 + 统计(`{marks, stats{total,devices,identities,today_devices,today_identities}}`) |
| POST | `/api/done_marks/delete` | T | 删一条去重记录(`{id}`)→ 该设备/身份下次会重新执行 |
| POST | `/api/done_marks/clear` | T | 清空某任务的全部去重记录(`{job}`)→ 整批重跑 |
| POST | `/api/video_plan/upload_video` | D | 上传单个视频素材(文件名 `名字_日期_编号`)→ 解析名字 + 入计划 |
| POST | `/api/video_plan/upload_video` | D | 上传单个素材(**视频或图片**,文件名 `名字_日期_编号`)→ 解析名字 + 入计划 |
| POST | `/api/video_plan/upload_titles` | D | 上传标题(txt 文本或文件)→ 配到已有计划行 |
| GET | `/api/video_plan/timeline` | D | 发布计划时间线(按日期分组的卡片 + 统计 + 磁盘) |
| GET | `/api/video_plan/stats` | D | 今天要发/已发/待标题/过期未发 + 素材占用与磁盘余量 |
@@ -506,7 +506,7 @@
| 接口 | 请求 | 响应要点 |
|------|------|---------|
| `POST /api/video_plan/upload_video` | multipart `file`(单文件)+ `replace` | `{ok,name,action:add\|skip\|replace,plan,msg}`;解析不通过 → **400 + `error`(人话原因)**。前端**逐文件串行**发(才能给每个文件一条进度与结果) |
| `POST /api/video_plan/upload_video` | multipart `file`(单文件,**视频或图片**)+ `replace` | `{ok,name,action:add\|skip\|replace,plan,msg}`;解析不通过 → **400 + `error`(人话原因)**。前端**逐文件串行**发(才能给每个文件一条进度与结果) |
| `POST /api/video_plan/upload_titles` | `{text, dry_run}` 或 multipart `file`(txt) | `{ok, counts:{total,attached,skipped,rejected}, rows:[{line,name,date,seq,title,body,action,error}], msg}`;**一行里可带正文**:`标题 || 正文_名字_日期_编号`(`||` 分隔,正文可省,只切第一处);**只有这一种写法**(名字在日期前一段,右锚定);**编号可省略**;标题/正文里可以有下划线和 `#话题`;空行忽略,`#` 开头的行**只有认不出名字/日期时**才当注释;拒收条件:名字里带下划线/是纯数字/是手机号、日期不合法、标题为空或 >100 字、该槽位已有标题、没有对应视频 |
| `GET /api/video_plan/timeline` | `?range=today\|week\|all&from=&to=&status=&q=&limit=` | `{ok, days:[{date,weekday,total,done,plans:[…]}], stats, disk, today}` |
| `GET /api/video_plan/stats` | — | `{ok, stats, disk, today}`;`stats` 里**三个口径都给**:`today_ready`(今天要发几个号,按发布日期)/ `today_done`(今天完成)/ `published_today`(今天实际发的,按 `published_at`,补发也算今天) |
+1 -1
View File
@@ -297,7 +297,7 @@ connecting ──获取设备──▶ u2 连接 ──▶ running ──▶ set
| `base.js` | esc / CSRF / 权限 / API 封装 / Toast / Tab 与子分栏切换 / 模态框 / 常量表 | 所有模块的公共底座 |
| `markdown.js` | 轻量 Markdown 渲染(`renderMarkdown`) | 无 CDN 依赖;**先转义再套标记** |
| `list.js` | 统一列表组件:搜索 + 分页 + 排序 | 状态注册表 `_LIST_PAGERS`;`setListPager` **不重置**页码/搜索/排序(避免轮询刷新打断用户) |
| `release.js` | 发布计划页(时间线按日期分组 / 多选视频**串行**上传 + 逐文件进度与结果 / 标题+正文 txt 上传(`标题 || 正文_名字_日期_编号`,卡片与明细**以名字为主**)/ 推送到手机 + **一键推送**(当天全部,**按名字找设备**:实时探测哪台在线设备登着这个号,逐条报出"没有设备登着"的号)、相册校验徽章与裁决 / 链接复制与导出 CSV / **发布任务的就地编辑**) | 服务层 `core/video_plan.py`(实时探测在 `core/account_probe.py`);上传前把队列**按文件名排序**并让用户确认解析结果(顺序 = "无编号按顺序配"的依据)。就地编辑只放常用字段(名字/目标/时间/启停/逐步参数 + 抓取元素),更复杂的编排走「打开通用编辑器」→ `openTaskModal`;步骤路径里 `then/else/children` 在**步骤的 params 下**(`_rpWalk`),走错这一层会把分支当数组下标用 |
| `release.js` | 发布计划页(时间线按日期分组 / 多选**素材(视频/图片)**串行上传 + 逐文件进度与结果 / 标题+正文 txt 上传(`标题 || 正文_名字_日期_编号`,卡片与明细**以名字为主**)/ 推送到手机 + **一键推送**(当天全部,**按名字找设备**:实时探测哪台在线设备登着这个号,逐条报出"没有设备登着"的号)、相册校验徽章与裁决 / 链接复制与导出 CSV / **发布任务的就地编辑**) | 服务层 `core/video_plan.py`(实时探测在 `core/account_probe.py`);上传前把队列**按文件名排序**并让用户确认解析结果(顺序 = "无编号按顺序配"的依据)。就地编辑只放常用字段(名字/目标/时间/启停/逐步参数 + 抓取元素),更复杂的编排走「打开通用编辑器」→ `openTaskModal`;步骤路径里 `then/else/children` 在**步骤的 params 下**(`_rpWalk`),走错这一层会把分支当数组下标用 |
| `monitor.js` | 监控页:设备表(含**电量列**,按 `battery.tier` 上色)、批量操作、异常汇总、任务概况卡片(含覆盖设备 chip) | 5s 轮询 + 脏检查(签名不变不重渲染) |
| `editor.js` | 步骤编辑器(拖拽 / 参数表单 / 条件分支 / 元素抓取 / 测试此步骤)+ `saveTask` | 最大的前端文件;`_stepEditor` 单例;设备选择卡 `_devCard` 供「抓取元素」「测试此步骤」共用(**名字优先**,型号·地址作副标题) |
| `tasks.js` | 任务 Tab:任务 CRUD / 调度解析 / 运行窗口 + 自定义动作 | |
+2 -2
View File
@@ -244,7 +244,7 @@
「发布计划」页维护;服务层 `core/video_plan.py`,接口 `/api/video_plan/*`(见 [API.md](API.md) §2.14),
任务步骤「发布视频」按它自动发布(见 [TASK_DEV.md](TASK_DEV.md) §4.7)。
**素材文件**落在 `data/videos/YYYY-MM/`(文件名 `{sha1[:12]}_{安全原名}`,内容寻址),**不进整库备份**(§6)。
**素材文件(视频/图片)**落在 `data/videos/YYYY-MM/`(文件名 `{sha1[:12]}_{安全原名}`,内容寻址),**不进整库备份**(§6)。
| 列 | 类型 | 说明 |
|----|------|------|
@@ -255,7 +255,7 @@
| `seq` / `seq_auto` | Integer / Boolean | 编号从 **1** 起(不用 0 表示"无");`seq_auto`=号是自动分配的 |
| `title` | Text | **标题**(发布页的「标题」框;标题 txt 配对写入) |
| `body` | Text | **正文/文案**(发布页的文案框,可带 `#话题`)—— 标题 txt 一行里用 `||` 分隔:`标题 || 正文_名字_日期_编号`;**标题与正文都为空的行不会被发布** |
| `video_file` / `video_name` / `video_size` / `video_sha1` | 各自 | 落盘相对路径 / 原始名 / 大小 / 内容指纹(重复上传判据) |
| `video_file` / `video_name` / `video_size` / `video_sha1` | 各自 | **素材**(视频或图片,看扩展名):落盘相对路径 / 原始名 / 大小 / 内容指纹(重复上传判据)。推送时**扩展名原样带到手机**——抖音按它判断视频还是图文 |
| `status` | String(16) index | 见下方状态机 |
| `stage` | String(16) | 失败发生在哪一步:`push`/`scan`/`post`/`verify` |
| `attempts` | Integer | 尝试次数(上限 3,超了不再自动取) |
+4
View File
@@ -466,6 +466,10 @@ from .generic import task # 触发 @register_task(当前唯一任务类型)
**全部按「名字」走**(名字 = 上传文件名里的那个名字 = 抖音昵称):计划行认名字,
运行时读本机当前账号名字,**读不到名字就当"本机没有号"、什么都不发**。
**素材可以是视频,也可以是图片**(扩展名各带各的,一路原样带到手机上 —— 抖音按它判断
这是视频还是「图文」):**发布流程是同一条**(相册里点第一个素材 → 下一步 → 填标题/正文 → 发布),
所以任务画布不用为图片另写一套。
**分工**(这是刻意的设计):**平台只管"素材到手机 + 状态记账",抖音里怎么发由你画** ——
因为发布流程跟账号、版本、界面都在变,写死在平台里改不动。