feat(发布计划): 视频发布计划(批量上传配对 → 时间线 → 推送到手机 → 发布任务 → 分享链接)
一、平台侧(账号 → 发布计划页) - 新表 video_plan(schema v9→v10):账号×发布日期×编号 → 素材 + 标题 + 发布状态 + 分享链接; 状态机 pending/ready/pushing/publishing/done/failed/unknown/skipped(**failed 与 unknown 必须分开**: 推送阶段的失败可安全重试;碰过抖音之后的岔子只能算"结果未知",绝不自动重发) - 素材上传:文件名 `手机号_日期_编号`(编号可省)解析配对;标题 txt `标题内容_手机号_日期_编号`; 内容寻址落盘 data/videos/YYYY-MM/(sha1 分块算,同名不存两份),**不进整库备份**但进 manifest 反查 - 新蓝图 web/video_plan_api.py:上传/时间线/统计/单条增删改/推送到手机/标记结果/裁决/链接导出 CSV/ 任务列表与一键新建、**就地编辑**(GET/PUT /tasks/<id>)、**一键推送**(POST /push_all,按设备分组、设备内串行) - 账号页拆子分栏(台账 / 发布计划)+ static/admin/release.js;清理 job(04:41 僵尸回收+过期行、04:47 素材文件) - 上传体积:MAX_CONTENT_LENGTH(默认 2GiB)+ 413 JSON + nginx client_max_body_size(修现有 APK 上传隐患) 二、任务侧(平台推素材,抖音流程你自己写) - 新步骤 push_release「推送发布视频」:原子占位 → adb push → **touch 改成"现在"** → 清旧目录同名副本 → 触发扫描并**按路径**校验相册索引 → 标题写进剪贴板;默认目录 /sdcard/DCIM/Camera - 新步骤 mark_release「标记发布结果」:回写 done/failed/unknown,成功时抓作品分享链接、删手机素材 - input_text 支持 text_source=release_title(自动取计划标题 + 回读校验); if_el 的候选值来源新增 release(**本机当前发布计划**的抖音号/昵称,发布前校验"登的是不是要发的号") - build_release_steps 骨架 15 步:⓪ 亮屏 → ① 打开抖音(等首页) → ② 点「我」→ ③ 等抖音号出现 → ④ 条件判断(账号) → then ⑤ 推送 ⑥⑦⑧⑨⑩⑪⑫ 抖音点击/填标题 → ⑬ 标记 / else 发通知跳过 三、修(推送这一路的检测机制) - **uiautomator2 3.x 的 d.shell() 返回 ShellResponse(tuple 子类)不是 str**:`'x' in resp` 恒 False、 `.strip()` 不存在 → "推上去的文件大小不对"每次都判失败(文件其实推上去了)、相册校验永远报没进、 删除确认永远判没删掉。新增 publish_flow._sh() 统一取 .output;大小改成解析 ls -l 的大小列 - **adb push 保留本地 mtime** → 推 3 天前上传的素材在按时间排序的相册里排不到最前, "点第一个 = 刚推的那个"不成立 → 推完 touch - 相册校验**按路径**比(MediaStore 的 _data 会把目录小写、/storage/emulated/0 ≡ /sdcard), 只比文件名会被老目录的同名残留骗过去 - 屏幕没亮就启动抖音会永远停在启动页(UI 树为空)→ 后面"点我/等抖音号"必然 miss, 最后报成误导人的"账号不符" → 骨架第一步固定加「亮屏」,open_app 等「首页」出现 四、其它 - core/ledger.serial_of():设备名 → 当前地址(设备换 IP 后快照是错的) - 通知事件 task.video.published / task.video.failed;备份清单加 video_plan 与素材统计 - 文档同步:DATA_MODEL §2.11 + schema v10、API(新接口与语义)、TASK_DEV §4.7 专章、 ARCHITECTURE(账号页子分栏/release.js/两个 job)、DEPLOY(表数/nginx)、NOTIFY、DEVELOPMENT、README
This commit is contained in:
+292
-18
@@ -102,7 +102,8 @@ STEP_TYPES = [
|
||||
{"type": "wait_el", "label": "等待元素", "icon": "⏳",
|
||||
"params": {"selector_type": "xpath", "selector_value": "", "timeout": 10}},
|
||||
{"type": "input_text", "label": "输入文字", "icon": "⌨",
|
||||
"params": {"mode": "random", "texts": "你好\n有趣\n支持", "fixed_text": "", "clear_first": True}},
|
||||
"params": {"mode": "random", "texts": "你好\n有趣\n支持", "fixed_text": "",
|
||||
"clear_first": True, "text_source": "manual", "release_topics": ""}},
|
||||
{"type": "clipboard", "label": "剪贴板注入", "icon": "📋",
|
||||
"params": {"text": "", "paste": True}},
|
||||
{"type": "wait", "label": "等待", "icon": "⏱",
|
||||
@@ -118,6 +119,14 @@ STEP_TYPES = [
|
||||
"then": [], "else": []}},
|
||||
{"type": "mark_done", "label": "记为已做", "icon": "✅",
|
||||
"params": {"selector_type": "text", "selector_value": ""}},
|
||||
{"type": "push_release", "label": "推送发布视频", "icon": "📤",
|
||||
"params": {"date_source": "today", "fixed_date": "",
|
||||
"account": "device", "account_phone": "",
|
||||
"album_dir": "/sdcard/DCIM/Camera", "to_clipboard": True,
|
||||
"retry_failed": True, "max_same_run": 1}},
|
||||
{"type": "mark_release", "label": "标记发布结果", "icon": "🏁",
|
||||
"params": {"result": "published", "why": "", "capture_link": True,
|
||||
"delete_phone": True, "album_dir": "/sdcard/DCIM/Camera"}},
|
||||
{"type": "notify", "label": "发通知", "icon": "🔔",
|
||||
"params": {"title": "", "message": "", "level": "info"}},
|
||||
{"type": "stop_self", "label": "停止本设备", "icon": "⛔",
|
||||
@@ -234,6 +243,7 @@ class GenericStepsWorker(BaseWorker):
|
||||
# 「去重」条件最近一次解析出的身份:`mark_done` 留空时复用它,
|
||||
# 保证"检查"与"记账"用的是**同一个身份字符串**(否则 key 对不上、去重失效)
|
||||
self._last_ident = ""
|
||||
self._last_release = None # 「推送发布视频」推的那条计划,供「标记发布结果」用
|
||||
self._dedup_seen = False # 本轮是否跑过「去重」检查(区分"没配"与"配了但没读到")
|
||||
# 公共巡检(任务级配置,独立于步骤画布;见 core/patrol.py):
|
||||
# _wid 内部标识,用来记"下次到点 / 上次命中"两个节奏
|
||||
@@ -824,24 +834,217 @@ class GenericStepsWorker(BaseWorker):
|
||||
"""
|
||||
source = str(params.get("cmp_source") or "").strip()
|
||||
manual = split_cmp_values(manual_raw)
|
||||
if source not in ("device", "all", "group"):
|
||||
if source == "release":
|
||||
# 「发布计划」:本机**下一条将要推送**的那条计划(推送前校验账号就靠它)。
|
||||
# 候选值给两个:抖音号(纯号)+ 账号名称 —— 元素原文可能是
|
||||
# `抖音号:35377983067`,也可能只显示昵称,两个都可命中(OR 语义)。
|
||||
ids, note = self._release_candidates()
|
||||
elif source in ("device", "all", "group"):
|
||||
ids, note = ledger.douyin_ids(
|
||||
source, serial=self.serial,
|
||||
device_name=(self.ctx or {}).get("device_name", ""),
|
||||
group=str(params.get("cmp_group") or "").strip())
|
||||
else:
|
||||
return manual_raw, "" # 手填(默认):行为与以前完全一致
|
||||
ids, note = ledger.douyin_ids(
|
||||
source, serial=self.serial,
|
||||
device_name=(self.ctx or {}).get("device_name", ""),
|
||||
group=str(params.get("cmp_group") or "").strip())
|
||||
merged = list(ids) + [v for v in manual if v not in ids]
|
||||
if not ids:
|
||||
_log.warning(
|
||||
f"[{self.serial}] 条件判断取号为空(来源 {source}):{note};手填候选 "
|
||||
f"{len(manual)} 个 → "
|
||||
+ ("仅用手填值" if manual else "候选为空,这一步将永远不命中(走未找到分支)"))
|
||||
src_txt = (f"候选来自台账({source}) {len(ids)} 个号" if ids
|
||||
else f"台账取号为空:{note}")
|
||||
where = "发布计划" if source == "release" else f"台账({source})"
|
||||
src_txt = (f"候选来自{where} {len(ids)} 个号" if ids
|
||||
else f"{where}取号为空:{note}")
|
||||
if manual:
|
||||
src_txt += f",另加手填 {len(manual)} 个"
|
||||
return "\n".join(merged), src_txt
|
||||
|
||||
def _release_targets(self, params):
|
||||
"""推送/标记步骤共用的"取本机账号手机号"逻辑。返回 (phones, who)。"""
|
||||
from core import ledger
|
||||
phone = (params.get("account_phone") or "").strip()
|
||||
if (params.get("account") or "device") == "phone" and phone:
|
||||
return [phone], f"指定手机号 {phone}"
|
||||
_, rows, note = ledger.resolve_device(
|
||||
serial=self.serial, device_name=(self.ctx or {}).get("device_name", ""))
|
||||
return [r.get("phone") for r in rows if r.get("phone")], f"本机账号({note})"
|
||||
|
||||
def _release_candidates(self):
|
||||
"""条件判断的「候选值来源 = 发布计划」:本机要发的那条计划的 **抖音号 + 账号名称**。
|
||||
|
||||
优先用本次运行里「推送发布视频」已经拿到的那条(`_last_release`);
|
||||
还没有就**看**下一条(peek,不占位)—— 这正是"推送前先校验账号对不对"的用法:
|
||||
命中(当前登录的就是要发的那个号)→ 走 then 去推送/发布;未命中 → else 跳过并通知。
|
||||
|
||||
给两个候选值是刻意的:元素原文可能是 `抖音号:35377983067`,也可能只显示昵称,
|
||||
两个都算命中(候选值之间是 OR)。配「包含」运算符即可。
|
||||
"""
|
||||
from core import video_plan as vp
|
||||
plan = getattr(self, "_last_release", None)
|
||||
src = "本次已推送的那条"
|
||||
if not plan:
|
||||
phones, _who = self._release_targets({})
|
||||
plan = vp.peek_for_device(phones, vp._today()) if phones else None
|
||||
src = "本机下一条待发"
|
||||
if not plan:
|
||||
return [], "本机没有待发的发布计划"
|
||||
cands = [c for c in (plan.get("douyin_id"), plan.get("nickname")) if c]
|
||||
note = (f"{src}计划的抖音号 {plan.get('douyin_id')}"
|
||||
f"({plan.get('nickname') or plan.get('phone')} "
|
||||
f"{plan.get('release_date')} #{plan.get('seq')})")
|
||||
return cands, note
|
||||
|
||||
def _exec_push_release(self, d, params, depth=0):
|
||||
"""推送发布视频:把本机某天待发的计划**推到手机相册**(**不发布**)。
|
||||
|
||||
**平台只负责到这里**:推文件 + 触发相册刷新 + 把标题写进剪贴板;
|
||||
抖音里怎么发(打开→点+→相册→选视频→下一步→粘贴标题→发布)由你在这条步骤
|
||||
**后面自己画步骤**,最后放一个「标记发布结果」回写状态。
|
||||
|
||||
参数:`date_source`(today/fixed) `fixed_date` `account`(device/phone) `account_phone`
|
||||
`album_dir`(默认 `/sdcard/DCIM/Camera`)`to_clipboard`(True) `retry_failed`(True)
|
||||
`max_same_run`(1 —— 一次运行推几条;**多条时「标记」只认最后一条**,要发多条就重复"推送+标记"两步)
|
||||
|
||||
返回 None=本机没有待推的(跳过);True/False=至少推成一条/全失败。
|
||||
"""
|
||||
from tasks.generic import publish_flow as flow
|
||||
from core import video_plan as vp
|
||||
|
||||
date = ((params.get("fixed_date") or "").strip()
|
||||
if params.get("date_source") == "fixed" else vp._today()) or vp._today()
|
||||
phones, who = self._release_targets(params)
|
||||
if not phones:
|
||||
_log.warning(f"[{self.serial}] 推送发布视频:本机没有台账账号 → 跳过")
|
||||
self.set_action("本机没有台账账号 → 跳过")
|
||||
return None
|
||||
plans = vp.take_for_device(phones, date,
|
||||
limit=int(params.get("max_same_run") or 1),
|
||||
retry_failed=params.get("retry_failed", True) is not False)
|
||||
if not plans:
|
||||
_log.info(f"[{self.serial}] 推送发布视频:{who} 在 {date} 没有待推送的计划 → 跳过")
|
||||
self.set_action(f"{date} 没有待推送计划")
|
||||
return None
|
||||
|
||||
opts = {"album_dir": (params.get("album_dir") or "").strip() or flow.ALBUM_DIR,
|
||||
"to_clipboard": params.get("to_clipboard", True) is not False}
|
||||
okN = badN = 0
|
||||
for plan in plans:
|
||||
if self.stopped():
|
||||
break
|
||||
self._last_release = plan # 供后面的「标记发布结果」用
|
||||
self.set_action(f"推送中:{plan.get('nickname') or plan['phone']} "
|
||||
f"{plan['release_date']} #{plan['seq']}")
|
||||
ok, msg = flow.push_one(self.serial, plan, opts,
|
||||
log=lambda s: _log.info(f"[{self.serial}] {s}"))
|
||||
if ok:
|
||||
okN += 1
|
||||
# 相册校验结果**单独说一句**:文件上去了 ≠ 相册里点得到它,
|
||||
# 后面那步"点相册里第一个"靠的就是索引(不写这句,出了问题只能猜)
|
||||
row = vp.get_plan(plan["id"]) or {}
|
||||
if row.get("push_verify") == "no_index":
|
||||
_log.warning(f"[{self.serial}] ⚠ 素材推上去了但**没进相册索引**"
|
||||
f"(抖音选视频时可能看不到它)")
|
||||
else:
|
||||
badN += 1
|
||||
self._notify_publish(plan, ok=False, msg=msg)
|
||||
if okN and self._last_release:
|
||||
lr = self._last_release
|
||||
title = (lr.get("title") or "")[:20]
|
||||
self.set_action(f"已推送到手机:{lr.get('nickname') or lr.get('phone')} "
|
||||
f"{lr['release_date']} #{lr['seq']}"
|
||||
+ (f"(标题『{title}』已进剪贴板)" if title else "")
|
||||
+ ";发完记得放「标记发布结果」")
|
||||
_log.info(f"[{self.serial}] 已推到手机,标题『{title}』已进剪贴板 —— "
|
||||
f"后面用「粘贴」把它填进抖音,发完记得放一个「标记发布结果」")
|
||||
else:
|
||||
self.set_action(f"推送发布视频:成功 {okN} 条"
|
||||
+ (f",失败 {badN} 条" if badN else ""))
|
||||
return okN > 0 if (okN or badN) else None
|
||||
|
||||
def _exec_mark_release(self, d, params, depth=0):
|
||||
"""标记发布结果:把"发出去了没有"回写到计划行(**你自己发布流程的最后一步**)。
|
||||
|
||||
参数:`result` = `published`(发出去了) / `failed`(没发出去,可重试) / `unknown`(不确定)
|
||||
· `why` 失败原因(可选)· `capture_link`(True) 成功时顺带抓作品分享链接
|
||||
· `delete_phone`(True) 成功/失败后删掉手机上的素材
|
||||
|
||||
放在你画布的**成功分支**里填 `published`、失败分支里填 `failed`;
|
||||
拿不准有没有发出去就填 `unknown`(平台不会自动重发它)。
|
||||
返回 True=标记成功 / False=发布失败 / None=找不到对应的计划(跳过)。
|
||||
"""
|
||||
from tasks.generic import publish_flow as flow
|
||||
from core import video_plan as vp
|
||||
import uiautomator2 as u2
|
||||
|
||||
plan = getattr(self, "_last_release", None)
|
||||
if not plan:
|
||||
phones, _who = self._release_targets(params)
|
||||
plan = vp.last_pushed_for_device(phones) if phones else None
|
||||
if not plan:
|
||||
_log.warning(f"[{self.serial}] 标记发布结果:找不到对应的计划行 → 跳过"
|
||||
f"(前面要先有一个「推送发布视频」步骤)")
|
||||
self.set_action("没有可标记的计划")
|
||||
return None
|
||||
|
||||
result = (params.get("result") or "published").strip().lower()
|
||||
if result not in ("published", "failed", "unknown"):
|
||||
_log.warning(f"[{self.serial}] 标记发布结果:result={result!r} 认不出 → 按 unknown 处理")
|
||||
result = "unknown"
|
||||
why = (params.get("why") or "").strip()
|
||||
|
||||
# 手机上的素材:先删再回写状态 —— 删失败不影响发布结论。
|
||||
# 远程路径是**确定的**({相册目录}/{计划id}.mp4),所以这里能直接算出来,
|
||||
# 不用把"上次推到哪"存进库(存了还得担心不一致)
|
||||
if params.get("delete_phone", True) is not False:
|
||||
try:
|
||||
dd = d or u2.connect(self.serial)
|
||||
album = (params.get("album_dir") or "").strip() or flow.ALBUM_DIR
|
||||
flow.clean_phone_video(dd, f"{album}/{plan['id']}.mp4")
|
||||
except Exception as e:
|
||||
_log.warning(f"[{self.serial}] 删手机素材失败(不影响标记): {e}")
|
||||
|
||||
if result == "published":
|
||||
url = ""
|
||||
if params.get("capture_link", True) is not False:
|
||||
try:
|
||||
dd = d or u2.connect(self.serial)
|
||||
url, lmsg = flow.capture_share_link(dd, self.serial)
|
||||
_log.info(f"[{self.serial}] 抓分享链接:{url or ('失败:' + lmsg)}")
|
||||
except Exception as e:
|
||||
_log.warning(f"[{self.serial}] 抓分享链接异常: {e}")
|
||||
vp.mark_done(plan["id"], share_url=url, msg=why)
|
||||
self._notify_publish(dict(plan, share_url=url), ok=True, msg=why or "已标记发布成功")
|
||||
self.set_action(f"已标记发布成功({'链接 ' + url if url else '没抓到链接'})")
|
||||
return True
|
||||
if result == "failed":
|
||||
# **确认没发出去** → 直接落 `failed`(可重试),不能走 mark_failed('post')
|
||||
# —— 那会落到"结果未知"、反而不敢重试。这里人是亲眼确认过的。
|
||||
vp.set_status(plan["id"], vp.ST_FAILED, note=why or "人工标记:没发出去")
|
||||
self._notify_publish(plan, ok=False, msg=why or "人工标记:没发出去")
|
||||
self.set_action("已标记失败(下次任务会再推一次)")
|
||||
return False
|
||||
vp.mark_failed(plan["id"], "verify", why or "人工标记:结果不确定")
|
||||
self._notify_publish(plan, ok=False, msg=why or "人工标记:结果不确定")
|
||||
self.set_action("已标记「结果未知」(不会自动重发,需人工确认)")
|
||||
return False
|
||||
|
||||
def _notify_publish(self, plan, ok, msg):
|
||||
"""发布结果推通知(notify 零 DB:字段都在计划行里,不查库)。"""
|
||||
try:
|
||||
notifier.notify("task.video.published" if ok else "task.video.failed",
|
||||
serial=self.serial,
|
||||
device_name=(self.ctx or {}).get("device_name", ""),
|
||||
model=(self.ctx or {}).get("model", ""),
|
||||
phone=plan.get("phone", ""),
|
||||
nickname=plan.get("nickname", ""),
|
||||
release_date=plan.get("release_date", ""),
|
||||
seq=plan.get("seq", 0),
|
||||
title=(plan.get("title") or "")[:60],
|
||||
share_url=plan.get("share_url", ""),
|
||||
msg=msg)
|
||||
except Exception as e:
|
||||
_log.warning(f"发布通知发送失败(不影响发布):{e}")
|
||||
|
||||
def _dedup_policy(self):
|
||||
"""任务级去重有效期(`dedup_reset` / `dedup_hours`)→ (kind, hours)。
|
||||
|
||||
@@ -1168,17 +1371,46 @@ class GenericStepsWorker(BaseWorker):
|
||||
需先用"点击元素"步骤定位到输入框,本步骤只负责输入。
|
||||
mode=random: 从 texts(换行分隔)随机选一条
|
||||
mode=fixed: 输入 fixed_text
|
||||
|
||||
**`text_source`(可选):文字从哪来**
|
||||
· `manual`(默认)= 用上面的 mode(行为与以前完全一致)
|
||||
· `release_title` = **本机当前那条发布计划的标题**("推送发布视频"推的那条;
|
||||
本机没有就取最近一条已推送的)· 配 `release_topics` 还能自动加话题
|
||||
选发布计划来源时**会回读校验**(读输入框的 text):不校验的后果是
|
||||
"抖音不吃 send_keys → 发出一篇空文案,而每一步都显示成功";
|
||||
校验不过就自动改走剪贴板通道粘贴(设备端 Agent)再验一次。
|
||||
"""
|
||||
mode = params.get("mode", "random")
|
||||
if mode == "fixed":
|
||||
text = params.get("fixed_text", "")
|
||||
else:
|
||||
raw = params.get("texts", "")
|
||||
candidates = [t.strip() for t in raw.splitlines() if t.strip()]
|
||||
if not candidates:
|
||||
_log.warning("input_text 无候选文字")
|
||||
return
|
||||
text = random.choice(candidates)
|
||||
src = str(params.get("text_source") or "manual").strip()
|
||||
text = ""
|
||||
if src in ("release_title", "release_title_topics"):
|
||||
from core import video_plan as vp
|
||||
plan = getattr(self, "_last_release", None)
|
||||
if not plan:
|
||||
phones, _who = self._release_targets(params)
|
||||
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 "")
|
||||
self._last_release = self._last_release or plan
|
||||
_log.info(f"[{self.serial}] 输入文字取计划标题:"
|
||||
f"{plan.get('nickname') or plan.get('phone')} "
|
||||
f"{plan.get('release_date')} #{plan.get('seq')}")
|
||||
if not text:
|
||||
_log.warning(f"[{self.serial}] 输入文字:本机没有可用的发布计划标题 → 跳过"
|
||||
f"(前面要先有「推送发布视频」步骤)")
|
||||
return None
|
||||
if not text:
|
||||
mode = params.get("mode", "random")
|
||||
if mode == "fixed":
|
||||
text = params.get("fixed_text", "")
|
||||
else:
|
||||
raw = params.get("texts", "")
|
||||
candidates = [t.strip() for t in raw.splitlines() if t.strip()]
|
||||
if not candidates:
|
||||
_log.warning("input_text 无候选文字")
|
||||
return
|
||||
text = random.choice(candidates)
|
||||
if not text:
|
||||
return
|
||||
clear_first = params.get("clear_first", True)
|
||||
@@ -1199,6 +1431,48 @@ class GenericStepsWorker(BaseWorker):
|
||||
_log.warning(f"[{self.serial}] input_text 未找到输入框: {e}")
|
||||
except Exception as e2:
|
||||
_log.warning(f"[{self.serial}] input_text 失败: {e2}")
|
||||
# 取计划标题时**必须回读校验**:抖音的 EditText 有时不吃 send_keys,
|
||||
# 不校验就可能发出一篇空文案而每一步都显示成功
|
||||
if src in ("release_title", "release_title_topics"):
|
||||
return self._verify_input(d, text, clear_first)
|
||||
return None
|
||||
|
||||
def _verify_input(self, d, text, clear_first):
|
||||
"""回读输入框,确认文字真的进去了;不行就改走剪贴板通道再验一次。"""
|
||||
def _read():
|
||||
try:
|
||||
el = d(className="android.widget.EditText")
|
||||
if el.exists:
|
||||
return el.get_text(timeout=2) or ""
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
|
||||
for attempt in (1, 2):
|
||||
got = _read()
|
||||
if got and text[:12] in got:
|
||||
return True
|
||||
_log.warning(f"[{self.serial}] 文案回读不一致(第 {attempt} 次):"
|
||||
f"期望『{text[:20]}…』读到『{got[:20]}』")
|
||||
if attempt == 1:
|
||||
# 第 2 次改走剪贴板通道(设备端 Agent 写剪贴板 + 粘贴),中文不会变形
|
||||
try:
|
||||
from core.clipboard_helper import inject_clipboard
|
||||
okc, msgc = inject_clipboard(self.serial, text, d=d)
|
||||
if okc:
|
||||
d.jsonrpc.pasteClipboard()
|
||||
time.sleep(1)
|
||||
else:
|
||||
_log.warning(f"[{self.serial}] 剪贴板兜底注入失败: {msgc}")
|
||||
except Exception as e:
|
||||
_log.warning(f"[{self.serial}] 剪贴板兜底异常: {e}")
|
||||
time.sleep(1)
|
||||
got = _read()
|
||||
if got and text[:12] in got:
|
||||
return True
|
||||
_log.warning(f"[{self.serial}] ⚠ 文案没填进去(读回『{got[:30]}』)——"
|
||||
f"接着发布就会发出空文案,这一步判失败")
|
||||
return False
|
||||
|
||||
|
||||
@register_task
|
||||
|
||||
Reference in New Issue
Block a user