feat(发布计划): 改按名字发(文件名 名字_日期_编号)+ 账号管理整页下线 + 自动切号一轮发完
发布计划不再绑设备、不再依赖账号台账:上传文件名 `名字_日期_编号`(标题行 `标题 || 正文_名字_日期_编号`),运行时读本机当前抖音昵称取计划;靠循环块 「一轮把本机待发的号跑完」自动切号,一直发到发完。 · 解析/配对换键:右锚定认名字(不许含下划线、不许纯数字 —— 不合法直接拒收并说清原因), 删掉三处"手机号必须命中台账"的校验,加索引 ix_video_plan_name_date(启动时补建) · 新步骤三个:读本机账号名字 / 切换账号(**按名字点控件**、切完回读校验、遇登录页停本设备)/ 循环本机待发的号(试过不回头、跑一轮没进展就换号、停止·超时·上限三个干净出口) · 发布骨架改 18 步:读名字 → 校验账号(cmp_source=account_target)→ 推送(**推送前重读昵称核对**) → 抖音流程 → 标记结果;push_release 的 account 默认 name · 账号管理整页下线:删 core/ledger.py(serial_of 迁到 core/device_pool.py)、6 个 /api/ledger*、 「账号」页与 ledger.js、设备池「账号」列、设备端账号块;发布计划升为一级页签; device_account 表与 42 行历史数据保留、仍在备份清单内(**停用不等于删数据**) · 人工入口「按名字找设备」:新增 core/account_probe.py(只读探测、跳过在跑任务的设备) + POST /api/video_plan/push_by_account,没设备登着的号逐条报出来; 推送时把"实际是谁发的"写进 posted_serial / posted_device_name · 通知:新增 task.account.switch_failed / task.account.need_login · schema v12(posted_*)/ v13(device.cur_account_name / cur_name_at) · 文档同步:TASK_DEV / API / DATA_MODEL / ARCHITECTURE / NOTIFY / DEVICE_AGENT / DEPLOY / DEVELOPMENT / README / .env.example(两个抖音控件选择器抓一次填 .env) · 新增 scripts/tests/:6 套用例 + run_all.py(临时库 + 假设备,不碰真实数据、不连真机)
This commit is contained in:
+23
-21
@@ -59,7 +59,7 @@ NEED_SELECTOR = ("click", "long_click", "wait_el", "swipe_until", "if_el")
|
||||
# 需要包名的步骤类型
|
||||
NEED_PACKAGE = ("open_app", "stop_app")
|
||||
# 需要非空 children 的容器
|
||||
NEED_CHILDREN = ("loop", "group")
|
||||
NEED_CHILDREN = ("loop", "group", "foreach_account")
|
||||
|
||||
# 有副作用的动作关键词:命中的步骤在 AI 通道只给 warning(人工复核),不拦
|
||||
DESTRUCTIVE_HINTS = ("评论", "发送", "发布", "投稿", "转发", "分享到",
|
||||
@@ -226,19 +226,17 @@ def validate_steps(steps, depth=1, path="steps", errors=None, warnings=None,
|
||||
# 所以在这里拦住,别让 AI 草稿悄悄变成"永远不命中"
|
||||
cmp_op = (params.get("cmp_op") or "").strip()
|
||||
cmp_src = (params.get("cmp_source") or "").strip()
|
||||
if cmp_src not in ("", "manual", "device", "all", "group"):
|
||||
if cmp_src not in ("", "manual", "release", "account_target"):
|
||||
errors.append(f"{here}.params.cmp_source: 不支持的候选值来源 {cmp_src!r}"
|
||||
f"(可用:空/manual/device/all/group)")
|
||||
if cmp_src == "group" and not (params.get("cmp_group") or "").strip():
|
||||
warnings.append(f"{here}: 候选值来源是「按设备分组」,但没填分组名 —— 取不到号")
|
||||
f"(可用:空/manual/release/account_target)")
|
||||
if cmp_op:
|
||||
from tasks.generic.task import CMP_OPS
|
||||
if cmp_op not in CMP_OPS:
|
||||
errors.append(f"{here}.params.cmp_op: 不支持的比对运算符 {cmp_op!r}"
|
||||
f"(可用:{'、'.join(CMP_OPS)})")
|
||||
elif not str(params.get("cmp_value") or "").strip() \
|
||||
and cmp_src not in ("device", "all", "group"):
|
||||
# 候选值来自台账时允许手填留空(台账取号就够);
|
||||
and cmp_src not in ("release", "account_target"):
|
||||
# 候选值来自发布计划 / 本次要发的名字时,允许手填留空(候选由平台给);
|
||||
# 其余情况仍要求填值,否则永远不命中
|
||||
errors.append(f"{here}.params.cmp_value: 选了文本比对"
|
||||
f"({cmp_op})就必须填比对的值,否则永远不命中")
|
||||
@@ -277,20 +275,24 @@ def validate_steps(steps, depth=1, path="steps", errors=None, warnings=None,
|
||||
f"(当前 min={lo!r} max={hi!r})")
|
||||
|
||||
if stype == "input_text":
|
||||
mode = params.get("mode", "random")
|
||||
if mode not in ("random", "fixed"):
|
||||
errors.append(f"{here}.params.mode: 必须是 random 或 fixed"
|
||||
f"(当前 {mode!r})")
|
||||
elif mode == "fixed":
|
||||
if not str(params.get("fixed_text") or "").strip():
|
||||
errors.append(f"{here}.params.fixed_text: mode=fixed 时不能为空")
|
||||
else:
|
||||
texts = params.get("texts")
|
||||
if not isinstance(texts, str) or not texts.strip():
|
||||
errors.append(f"{here}.params.texts: mode=random 时必须给候选"
|
||||
"文案(换行分隔,如 \"你好\\n不错\")")
|
||||
elif len([t for t in texts.split("\n") if t.strip()]) > MAX_TEXTS:
|
||||
errors.append(f"{here}.params.texts: 候选文案最多 {MAX_TEXTS} 条")
|
||||
# 文案来自「发布计划」(`text_source` 不是 manual)时 texts/fixed_text **都不参与输入** ——
|
||||
# 别按"手填"的规则要求候选文案(发布任务骨架里那两步就是这么配的:
|
||||
# mode 留默认、内容由计划给,靠 `release_title`/`release_body` 现取)
|
||||
if str(params.get("text_source") or "manual").strip() == "manual":
|
||||
mode = params.get("mode", "random")
|
||||
if mode not in ("random", "fixed"):
|
||||
errors.append(f"{here}.params.mode: 必须是 random 或 fixed"
|
||||
f"(当前 {mode!r})")
|
||||
elif mode == "fixed":
|
||||
if not str(params.get("fixed_text") or "").strip():
|
||||
errors.append(f"{here}.params.fixed_text: mode=fixed 时不能为空")
|
||||
else:
|
||||
texts = params.get("texts")
|
||||
if not isinstance(texts, str) or not texts.strip():
|
||||
errors.append(f"{here}.params.texts: mode=random 时必须给候选"
|
||||
"文案(换行分隔,如 \"你好\\n不错\")")
|
||||
elif len([t for t in texts.split("\n") if t.strip()]) > MAX_TEXTS:
|
||||
errors.append(f"{here}.params.texts: 候选文案最多 {MAX_TEXTS} 条")
|
||||
|
||||
if stype == "clipboard":
|
||||
if not str(params.get("text") or "").strip():
|
||||
|
||||
Reference in New Issue
Block a user