feat(发布计划): 标题 + 正文(标题 || 正文 _手机号_日期_编号)+ 发布流程多一步填正文;账号名称全面下线

一、正文(video_plan.body,schema v10→v11)
- 标题 txt 一行里可带正文,用 `||` 分隔(标题/正文都常带下划线,不能再用下划线分列;
  只切第一处,正文里再出现 `||` 原样保留):`标题 || 正文 _手机号_日期_编号`
  正文可省(老文件照收);两种写法(标题在前 / 手机号在前)都能带正文
- 长度:标题 ≤100 字、正文 ≤1000 字,超了**拒收不截断**
- 「待发布」判据从"有没有标题"改成"**标题或正文有没有内容**"(`_sync_content_status` 一处定义,
  改标题/改正文/批量导入三处共用),只给正文的行也照样待发
- 导入配对:同标题补一条新正文**要收下**(不能当重复跳过);已有别的标题仍然拒收(不静默覆盖)

二、发布流程多一步
- 骨架 15 → **17 步**:⑪ 输入文字(计划的标题) → ⑫ 点击正文框 → ⑬ 输入文字(计划的正文) → ⑭ 发布 → ⑮ 标记
- 「输入文字」取值来源集中到 `RELEASE_TEXT_SOURCES`:release_title(_topics) / release_body(_topics) /
  release_title_body(标题+正文,只有一个输入框的抖音版本用);`build_caption(key=…)` 支持三种来源
- 任务编辑器与发布计划就地编辑器的下拉都补齐这 6 档;待抓的空选择器 5 → 6 个(多了正文框)

三、账号名称下线(用户要求:只要抖音号)
- 发布计划:卡片 / 明细表 / 搜索 / 导出 CSV 都不再有账号名称(CSV 改为
  发布日期/设备号/手机号/抖音号/编号/标题/正文/分享链接/发布时间)
- 账号台账:列表列 / 编辑弹窗 / 粘贴导入预览 / 设备维度弹窗 / 导入说明与示例都去掉;
  **粘贴导入忽略这一列**(不报错、不落库),库里的历史值保留(不删列)
- 设备端 Agent 身份页:平台不再下发名称(传空串,**不改 APK 也能生效**,避免老版本解析失败)

四、文档 / 测试
- DATA_MODEL:body 列 + schema 11;API:upload_titles(`||` 语义)、PUT {title,body}、CSV 列、骨架 17 步
- TASK_DEV:骨架 17 步、`input_text` 六个取值来源表、正文长度上限
- ARCHITECTURE:release.js 说明(正文、不显示账号名称)
- 测试(%TEMP% 被系统清理后重建):test_plan_body.py 33 项(解析/取内容/骨架/入库/导入配对)、
  smoke_app_body.py 10 项(整机装配:页面、PUT 改正文、时间线、CSV 表头)—— 全过
This commit is contained in:
2026-10-11 16:36:56 +08:00
parent a800d051ae
commit 6ee089e023
13 changed files with 196 additions and 95 deletions
+19 -7
View File
@@ -72,6 +72,16 @@ def _norm_legacy_xpath(sel_val):
# ================== 步骤类型定义(前端操作库 + 后端执行共用)==================
# 「输入文字」的取值来源:plan 的字段名 → build_caption 的 key。
# 集中一处定义,免得下面判据分叉(取了标题却当正文填)——加新来源只改这里。
RELEASE_TEXT_SOURCES = {
"release_title": "title",
"release_title_topics": "title",
"release_body": "body",
"release_body_topics": "body",
"release_title_body": "title_body",
}
STEP_TYPES = [
{"type": "open_app", "label": "打开App", "icon": "▶",
"params": {"package": "", "wait_home": False, "home_feature": ""}},
@@ -1376,13 +1386,15 @@ class GenericStepsWorker(BaseWorker):
· `manual`(默认)= 用上面的 mode(行为与以前完全一致)
· `release_title` = **本机当前那条发布计划的标题**("推送发布视频"推的那条;
本机没有就取最近一条已推送的)· 配 `release_topics` 还能自动加话题
· `release_body` = 那条计划的**正文**(同一套来源与回读校验)
· `release_title_body` = 标题 + 换行 + 正文(抖音只有一个输入框时用这个)
选发布计划来源时**会回读校验**(读输入框的 text):不校验的后果是
"抖音不吃 send_keys → 发出一篇空文案,而每一步都显示成功";
校验不过就自动改走剪贴板通道粘贴(设备端 Agent)再验一次。
"""
src = str(params.get("text_source") or "manual").strip()
text = ""
if src in ("release_title", "release_title_topics"):
if src in RELEASE_TEXT_SOURCES:
from core import video_plan as vp
plan = getattr(self, "_last_release", None)
if not plan:
@@ -1390,14 +1402,14 @@ class GenericStepsWorker(BaseWorker):
plan = vp.last_pushed_for_device(phones) if phones else None
if plan:
text, _err = vp.build_caption(
plan, params.get("release_topics") or ""
if src == "release_title_topics" else "")
plan, params.get("release_topics") or "",
key=RELEASE_TEXT_SOURCES[src])
self._last_release = self._last_release or plan
_log.info(f"[{self.serial}] 输入文字取计划标题:"
f"{plan.get('nickname') or plan.get('phone')} "
_log.info(f"[{self.serial}] 输入文字取计划{RELEASE_TEXT_SOURCES[src]}:"
f"{plan.get('douyin_id') or plan.get('phone')} "
f"{plan.get('release_date')} #{plan.get('seq')}")
if not text:
_log.warning(f"[{self.serial}] 输入文字:本机没有可用的发布计划标题 → 跳过"
_log.warning(f"[{self.serial}] 输入文字:本机没有可用的发布计划内容 → 跳过"
f"(前面要先有「推送发布视频」步骤)")
return None
if not text:
@@ -1433,7 +1445,7 @@ class GenericStepsWorker(BaseWorker):
_log.warning(f"[{self.serial}] input_text 失败: {e2}")
# 取计划标题时**必须回读校验**:抖音的 EditText 有时不吃 send_keys,
# 不校验就可能发出一篇空文案而每一步都显示成功
if src in ("release_title", "release_title_topics"):
if src in RELEASE_TEXT_SOURCES:
return self._verify_input(d, text, clear_first)
return None