docs: add reviewed Douyin main-worker dispatch plan
douyin-release-gate / verify (push) Failing after 4m32s

This commit is contained in:
2026-10-09 10:26:33 +08:00
parent 090f8b6e7a
commit 46dc915703
@@ -0,0 +1,388 @@
# 抖音大小号管理与消息调度融合方案
状态:**方案已复审,网关连接地址与JSON载荷两项前置问题已修复;其余门槛仍未完成,不能直接开启自动执行。**原方案任务仅文档;本次经确认另修网关两项基础问题及测试,不实施大小号业务、不执行真实关注或私信。
复审/修复日期:2026-10-09。源码基线:`douyin-pc d47affb`、`creator-hub 090f8b6`(含已验证的Agent/测试修复);前两轮分别基于 `f238d11`、`3610e3d`。方案基于以上已验证代码;大小号业务尚未实施,仍须完成所列前置门槛。
## 1. 需求与已确认决定
本方案将 `../douyin-pc` 的大小号管理与通知驱动调度融入当前项目,不移植其桌面界面、独立账号库或浏览器管理体系。
| 项目 | 已确认规则 |
| --- | --- |
| 大号定位 | 可选功能;普通自有账号无需成为大号或小号,原有使用方式不变 |
| 大号开关 | 每个大号独立开启、关闭;关闭一个大号不影响其他大号 |
| 小号来源 | 只选已经加入“我的账号”、已绑定真实抖音 UID 的账号;不新建占位账号,不重复创建环境 |
| 关系存储 | 使用独立大小号关系表;不在账号表增加 `owner_id` 或所属大号字段,表结构允许后续扩展多对多 |
| 账号关系 | 本期一个小号只能归属一个大号,以关系表的独立约束保证;大小号角色互斥,不允许自己选自己、共享小号或嵌套关系;本期不开放多对多 |
| 动作范围 | 自动关注、自动文字私信均纳入方案;两种动作可分别开启、关闭;不增加 AI 回复、群发或其他互动 |
| 历史通知 | 历史继续同步与展示,但不自动生成发送任务;仅处理大号功能开启后发生的新通知,重新开启不追发关闭期间通知 |
| 本次交付 | 源码梳理、融合设计、实施阶段、验收标准与 review;不实施业务功能 |
**产品边界变化:**当前私信管理仅允许手动文字发送。本方案中的自动文字私信是经本次确认的新增方向,不是当前能力。实施自动执行前,须在同一变更中更新 `AGENTS.md`,明确“手动聊天仍保留,另有用户显式开启的通知驱动调度”,而不是把现有手动限制解释为已允许自动发送。
## 2. `douyin-pc` 真实实现
### 2.1 角色、归属与开关
- `src/account_store.py` 将账号分为 `main`、`worker`;小号通过 `owner_id` 单一归属大号,`stopped` 单独控制小号参与调度。**这仅描述源项目现状;当前项目不采用账号上的 `owner_id` 归属方式,改用独立关系表。**
- 大号另有规则配置,包括规则开关、关注开关、私信开关、固定文字、执行间隔、目标冷却、作品筛选等。
- `set_owner` 改变归属时取消原组尚未执行的任务,**不把旧任务改派给新组**;删除后仍保存来源历史的处理方式不能直接照搬当前项目的完整删除规则。
- `src/account_engine.py` 的 `start_group` 会检查大号及组内小号的登录与运行条件,再启用监听、执行循环;它管理的是自己的桌面程序会话,不等于 CreatorHub 的环境、调度与监听开关。
### 2.2 通知到任务
1. 大号读取通知,核对来源账号身份,将点赞、评论、关注、转发通知及用户目标存入本地 SQLite 账本。
2. 同一通知中的多个用户分别形成目标,缓存键和目标键防止重复分配;读取成功不等于已执行动作。
3. 规则通过后,按**一条通知 event**轮流选择一个小号,通知内全部目标交给该小号;同一目标的关注与私信也留在同一小号。轮转推进即使没有实际生成任务也会发生,选人不等于已核验在线。
4. 跳过目标为来源大号或当前执行小号的情形;作品过滤、目标冷却、规则开关会影响分配,并不排除本组所有其他小号。
5. 实时通知优先于历史任务;无小号时 waiting 保留。总规则关闭后,新通知存为 ignored,不创建任务;已有 waiting/pending 暂停但保留,重新开启会继续分配/领取旧任务,后端没有强制积压确认。
6. 任务保存动作及文字等快照,后台领取后执行,结果再次写入账本。
### 2.3 执行、失败与重放
- `_claim` 按执行小号领取“同事件+同目标”的动作批次;新任务依赖为空,`perform_batch` 使用 `asyncio.gather` 并行关注与私信,**不是关注成功才发私信**。旧依赖兼容代码不代表新任务有先后链。worker 在批次间等待,批内没有动作间隔;间隔记录在内存中,重启会丢失。
- `pending / running / succeeded / failed / unknown / cancelled` 是账本中的不同含义;不能把领取、发送或读取成功算作平台确认成功。
- 一般异常保留 `unknown`,但源代码把 `success=false` 且整数非零 `status_code` 判为 `failed`,包含 1008,**没有 1008 专属未知结果处理**。此外 `MESSAGE_BUILD_FAILED` 在调用发送之后仍被判失败,有重复重放风险;本项目必须沿用自身更严格的结果未确认语义。
- 进程恢复将遗留 running 改 unknown,不直接重发。显式失败重放另建任务并保留旧记录,但会清共享冷却、只检查同动作活动任务,也没有消费原失败的标记;不能将其描述为完整安全重试。
- 源冷却 `last_at` 在领取提交 running 时写入,不是成功后写入;新事件按自身规则计算期限,调小配置可能缩短保护。融合方案改用明确截止时间和独立未知阻止。
- 关停包含停止领取、等待执行收尾与会话关闭;账本写入失败会阻断后续执行,避免继续做无法记账的外部动作。
- `docs/plan01.md` 是业务目标与约束,源码和 `src/test_plan01.py` 才能证明具体能力;文档描述不直接当成验收证据。
### 2.4 不照搬的部分
| 源项目机制 | 本项目处理 |
| --- | --- |
| 桌面账号库、`main/worker` 必选角色 | 复用真实自有账号,大号功能默认关闭,未配置账号维持普通用途 |
| 小号账号上的 `owner_id` | 使用独立大小号关系表,账号表不保存所属大号,关系结构支持后续多对多扩展 |
| 组启动批量启动小号浏览器 | 不暗中启动、登录或恢复账号;使用现有环境与明确的运行状态 |
| SQLite 与桌面线程队列 | 复用当前 PostgreSQL 与后台 worker,不增加第二套账本 |
| 定期页面轮询作为通知入口 | 目标为网关主动连接平台 `/v1/agent`,通知与业务任务共享连接;沿用实时信号与历史完整核对,不恢复旧 `/v1/channel` 或事件 HTTP 轮询回退 |
| 未开启时积压通知、历史下发、失败历史重放 | 本期不追发旧通知、不设历史下发或批量重放入口 |
| 源任务批内关注、私信并行,事件级轮转 | 本项目按目标轮转、按每个实际外部动作节流;双动作顺序执行并分别存结果,这是明确的设计差异 |
| 调整关系后的临时内存状态 | 关系修改与任务取消在数据库同一事务完成,执行前再次核对 |
## 3. 当前项目基础与差距
| 能力 | 当前真实情况 | 融合方式 |
| --- | --- | --- |
| 自有账号与环境 | 已有真实 UID、登录核验、暂停、网关、浏览器环境,未绑定环境与账号统一展示 | 不另建账号,绑定 ID、UID、profile_id、指纹 seed 不因角色改变 |
| 大号旧字段 | `AccountProfile.BigAccount` 等旧字段仍残留;前端无完整大小号功能 | 不把残留字段当成功能开关;新配置成为唯一角色依据,实施时移除废弃字段与接口输入 |
| 旧自动运营表 | 1017 等历史迁移有关系、策略、冷却表;`043_schema_consolidation.sql` 明确删除旧链路 | 不恢复旧策略引擎,不复用已执行迁移编号,新功能用新编号和新命名 |
| 通信方向 | Go与Python出站Agent均已接入 `/v1/agent`,旧HTTP服务/事件入口已删除;部署示例和网关页面已改为出站配置 | 直接复用现有Agent,不重做迁移;仍须验证真实SQL与共享网关并发/重连,不以接入代码完成代替全部执行保障 |
| 事件监听 | `RecordListenerDeliveries` 校验开关代次并同事务保存通知与检查点;Agent已支持事件推送、ack/nack与断线重推,Go订阅身份核验仍需补齐 | 沿用通知事务,新增调度输入;ACK 必须在提交后且对应原投递/订阅/代次,不在接收事务发起动作 |
| 网关任务 | 新 `gateway_task` 保存请求载荷、状态、响应状态/大小/错误;当前没有结果正文、业务操作关联或完整回包核对 | 复用其承载传输,不另建网关传输队列;业务动作结果不能直接照搬网关任务终态 |
| 私信收件箱 | 已有会话、文字发送、请求 ID 去重、发送前落库、`unknown` 结果及专用聊天身份核验 | 提取后端共享发送入口,手动与自动调用同一路径,不调用自己的 HTTP API |
| 关注 | `browser_gateway/platform/douyin.py` 已有关注动作表达式和执行路径 | 能复用动作入口,但尚无面向大小号调度的完整后端流程、结果账本和真实验收 |
| 防重复执行 | 网关已有操作ID、账号动作所有权标记与身份校验;新Agent有进程内已见task ID集合,重复时返回409 | 复用现有保护,但409不是原动作结果;需持久结果/原操作核对,不能将进程内去重当跨重启幂等 |
| 后台调度 | 当前 scheduler 负责作品、评论与指标计划 | 新消息调度独立职责,不挤占或改写采集游标、完成时间和指标计划 |
现有 `docs/better-douyin-整合计划.md` 不覆盖本次大小号调度;它不是本功能的实施依据。本方案不引入其中尚未确认的依赖。
**更新后的前置缺口:**持久化传输任务并不自动提供业务幂等。`gatewaySession` 当前只有一个 waiter,未按回包 `id` 匹配;下发忽略数据库领取布尔结果;异步下发无人持久化回包;同步结果保存错误被忽略,且响应正文仅在内存。这些问题必须在自动发送前解决,不能靠大小号队列外包一层锁掩盖根因。具体依据与门槛见 §8。
## 4. 融合后的业务规则
### 4.1 独立功能,不替代原账号状态
- 每个已绑定自有账号都可以配置大号功能,默认关闭。读取设置不创建配置记录、不占用大号角色;只有明确保存大号配置才建立角色关系,未使用功能的账号始终普通。
- 大号配置包含:功能开关、所选小号、四类通知开关、关注开关、文字私信开关及文字、动作间隔、目标冷却。
- 开启前必须至少选择一个合格小号、至少开启一种动作;开私信时文字不能为空。可单独关闭关注或私信;若要全部停止,明确关闭大号功能,不隐式改动其他开关。
- 普通账号、关闭的大号、小号均继续使用原有采集、作品分析、私信管理;角色配置不能暂停采集、停止浏览器或改变监听设置。
- 大号关闭保留配置与成员,便于再次开启;已配置为大号的账号不能作为其他大号的小号,即便其功能关闭。大号设置内另设明确确认的“解除大号配置”:仅在已关闭、成员已解除且无在途/不明操作时允许,清理该功能配置与调度记录,但不删除账号、环境、通知或聊天记录;确认文案明确调度记录将删除。之后才允许转换角色,不用“删除账号”代替角色解除。
- 大号删除时按现有完整删除规则清理自己的功能配置和任务,解除成员关系,**不删除成员账号或浏览器环境**;删除小号只清理其成员关系及所属任务,不删除大号,同时终结受影响输入并清理对应目标占用,防止大号留下永久活动触发。删除先进入禁止新接收与领取的状态,检查大号关联的所有小号写占用,而非只检查被删账号自己的浏览器;仍有在途或不明操作时拒绝最终删除,保留成员阻止状态,确定收尾后才清理。不能通过删记录把仍在执行的操作当成已停止。关闭监听仍按原规则保留历史,只阻止新调度。
### 4.2 开启与运行条件
配置保存与接收正常分开展示,不把“开关已开启”显示为“运行正常”。
大号可接收新调度的前提:功能开启、调度未暂停、业务正常、登录身份有效、监听开启且代次有效、浏览器实际运行。关注与私信涉及主动动作,禁言或其他异常业务状态不视为可正常执行。
小号候选列表仅显示已绑定账号,排除自身、已经属于其他组的账号、已配置为大号的账号;缺昵称只用前端现有缺省文案,不将 UID 当作名称。可显示暂时未运行的合格账号并说明原因,允许保存选择,但实际派发只选登录有效、调度未暂停、业务正常且浏览器实际运行的成员。身份或运行状态读取失败必须显示未知/失败,不假装可用。
开启不会自动打开事件监听、启动浏览器、恢复暂停或触发额外扫码。监听关闭时要求用户通过原入口开启;运行中关闭监听、暂停大号或大号失去登录时,终止本次调度代次,废止待执行任务。条件恢复后进入新的接收代次,**不接着发送受阻期间旧通知**。短暂读取失败先阻止发起并明确异常,不能未经核实写成账号已退出登录。
调度代次绑定大号的 `listener_generation` 与真实浏览器运行身份/连续性记录。监听关闭与账号暂停入口,必须在同一配置事务中使调度代次失效;明确停止浏览器的控制入口先禁止新领取,意外退出/身份失效在确认节点结束代次。不能只靠 worker 定期查询,否则快速关开会漏掉边界。重启的 `starting` 或 WS 重连不自动等于真实退出;恢复先核对绑定与运行连续性,无法证明期间持续满足条件时建立新边界并记录原因,不沿用旧边界追发。P0 必须确定网关运行身份/生命周期记录的取得方式,不以最新一次“正在运行”推断整个中断期间正常。
私信小号还必须**自身已开启监听且代次有效**:当前 `BeginPrivateMessage` 与发送服务均有此限制,本方案保留它,不用大号监听代替小号监听、不放宽现有手动发送边界。关注单独开启时不额外要求小号监听。启用私信前至少有一个满足该条件的成员;选择时明确展示“私信需开启该小号监听”,由用户通过原入口开启。小号监听关闭或代次变化时取消其尚未发起的私信及依赖组合,不暗中重新开启。
### 4.3 新通知与历史严格分离
为自动调度单独记录 `generation`、`accept_after` 与 `active/inactive`,不复用监听的历史标记作为唯一发送依据。
- 用户开启大号功能并满足接收条件时,在事务内保存本代边界;每次重新开启或确认恢复接收建立新代次。
- 仅支持点赞、评论、关注、转发;私信不作为自动触发源,避免循环回复。
- 通知必须在当前监听代次成功入库,每个目标 UID 及该互动的真实发生时间明确、严格晚于 `accept_after`、不晚于当前可信时间,且到任务入库及发起时仍处于同一有效大号代次。合并通知的最新时间不能直接替代其中每个旧用户的互动时间;无法证明目标本次互动时间时仅展示、不发送。
- 发生时间未知或异常未来时间:保存、展示并记录不调度原因,不用接收时间冒充发生时间。平台时间与边界统一精度;等于边界或精度不足以证明晚于边界时保守不发,记录 `skipped_boundary_ambiguous`,不宣称全部新互动均已触达。
- 历史同步只是投递通道,不一概丢弃:完整核对若找回的是当前有效代次中新发生的漏收通知,可按相同去重规则处理;边界前通知永远不发送。
- 无可用小号时该目标记为 `skipped_no_executor`,不无限积压、不等浏览器恢复后追发;用户能看到原因。此处刻意不沿用源项目的积压补派。
- WS 重投、历史核对、服务重启均不能让已作出处理决定的同一事件目标再次生成动作;历史补充资料不重新触发。平台合并的重复互动不能仅因列表内容变化就当成新事件;输入身份沿用当前事件去重规则,不另外猜测独立互动。
- 正常服务重启在监听代次与浏览器运行连续性均核验成立时保留有效代次及边界,允许补齐该代次中新发生的漏收;不把服务器重启本身误作用户关闭再开启。确认失效或无法证明连续性时结束旧代次、建立新边界并显示原因,不声称已补齐不可判定期间的自动触达。
### 4.4 分配、冷却、节流
- 以“规范化通知事件+互动用户”为一个触发单位。推荐默认四类通知均可触发,不加入源项目的作品选择/刷新功能,本期覆盖大号收到的合格通知。
- 按可用小号稳定排序轮流派发;每个目标的两个动作固定同一小号,轮转位置事务保存。没有成功生成任务时不消耗轮转位置。
- 排除目标为来源大号、执行小号以及本组任一小号的情况,防止组内互相关注、互发。
- 同一大号对同一目标在已有待执行、正在执行、结果不明或冷却中时不新增任务;不同类型通知共享这一限制,不通过换小号绕开。
- 同一小号不能归属多个大号。不同大号仍可能触达同一外部用户;本期不引入全局营销去重规则,但评审和手工验收必须检查用户对此行为的理解。
- 冷却推荐从**明确成功动作时间**计算;全部在发起前失败不占成功冷却;任何结果不明保留阻止状态,不能靠冷却到期自动解除。
- 动作间隔按执行小号约束,关注与私信各自占一次间隔,手动消息与自动动作共用串行占用;不以随机等待替代明确节流。
- 推荐首版默认动作间隔 30 秒、目标冷却 4 小时(沿用源项目量级,仍属实施默认值建议)。时长沿用项目“正整数+s/m/h”输入及整数秒存储,前后端使用同一上限,不加第二套格式。
- 配置变更不修改已经保存的目标、文本或动作快照。更新动作/文字/间隔/冷却或成员时,取消尚未开始的旧任务,未来通知用新版本;不静默改写旧任务或转派。保存未变化的配置是无操作,不增加版本/代次、不取消任务、不重置边界;上述配置均不改动采集设置。
### 4.5 两个动作的关系与结果
| 关注 | 私信 | 执行规则 |
| --- | --- | --- |
| 关 | 开 | 只发送配置文字,不暗中关注 |
| 开 | 关 | 只关注,不生成私信记录 |
| 开 | 开 | 先关注,确认关注成功或已经关注后,再按间隔发私信;关注失败/不明则私信不发并给出原因 |
私信不是“写了输入框就成功”;关注不是“脚本执行完就成功”。以平台明确结果为准,不伪造送达、已读或关注成功。
同一触发的每个动作独立记录状态。部分成功明确显示:例如“关注成功、私信结果未确认”,不合并成全部成功。关闭私信功能不会撤回已发送消息。
### 4.6 关闭与在途操作
- 关闭事务中:关闭功能、结束代次,取消尚未发起的业务动作及其仍为 pending 的关联 `gateway_task`;随后不能领取新的外部动作。只取消业务表、却让通道重连 dispatcher 继续补发,是不允许的。配置/成员变更和小号失效的取消同样覆盖两层。
- 已经发起的平台请求无法撤回;界面显示“关闭中/仍有执行结果待确认”,不是立即宣称所有操作已停止。
- 发起前必须再次核对功能开关、代次、配置版本、成员归属、账号状态及身份;检查与将任务转为 `sending` 必须在同一数据库临界区完成。已指派小号确认暂停、掉线、未登录或归属失效时取消其尚未发起的动作,不待恢复追发、不改派;状态读取失败保持阻止并显示未知,待核验后再作明确取消决定,不将读取错误伪装成未登录。
- 从传输任务 dispatched 提交到网络下发、再到 Agent 实际发起之间仍有窗口;关闭需纳入同一执行占用与传输领取协议。Agent 对未开始的失效/过期操作拒绝执行,无法证明尚未开始则按在途收尾或 unknown 保留阻止。不得用一次普通查询、或只在创建 gateway_task 时查开关,承诺“关闭后绝不会再发出之前已获准的请求”。
- 等待超时、停止超时或重启打断:先尝试条件取消确实仍未下发的关联传输任务;已经下发则按原 task_id+operation_id 核查,不把通道超时 failed 视为平台明确失败。无法确认则业务记 `unknown` 并阻止账号自动写操作,不回到待执行、不换小号补发;同操作迟到结果仍须持久化与核对。
- 有历史待确认结果时再次开启可保存配置,但不得解除阻止;显示真实原因,须先取得本方案要求补齐的、可关联原操作的确定性结果。现有标记与聊天最近记录不足以确认时持续阻止,不提供强行解锁或自动终止后重发。
## 5. 系统设计与模块边界
### 5.1 数据流
```text
抖音大号通知
→ 网关现有实时信号 / 历史完整核对
→ 网关主动连接平台 /v1/agent:event / event_ack
→ RecordListenerDeliveries:通知 + 检查点 + 调度输入标记,同一事务
→ 提交成功,确认原订阅、原代次的 WS 投递
→ 独立调度 worker:校验代次/时间/目标 → 去重/冷却 → 轮转分配
→ 保存业务动作快照 → 取得账号写操作占用 → 核对身份及开关
→ 在同一事务关联唯一 gateway_task,提交后才可下发
→ /v1/agent task:关注执行 / 共享文字私信发送服务
→ 匹配 task_id + operation_id 的结果凭据
→ 传输结果持久化 → 独立确认业务动作 / 私信结果 → creator SSE 刷新
```
接收事务只做数据库写入,不查询浏览器状态、不关注、不发私信、不下载封面;一个小号失效不能阻塞通知保存与确认。账号运行条件在调度阶段核验;系统必须有足够记录解释通知为什么未产生动作。
**现有多用户通知有入口缺口:**`normalize_notice` 遇多个不同 UID 会把单个 `interactor_uid` 置空,当前 `ListenerNotice` 与通知表也只存单互动用户。P1/P2 必须增加可信目标列表的网关规范化、WS 字段和事务内持久化,每个目标在 `creator_dispatch_inputs` 保存独立 UID、真实互动时间、昵称/secUID 与资料来源;不能只在 worker 里拆已丢失的目标,也不能从空 UID 猜用户。继续保留现有通知展示资料,但调度只使用新增的已核验目标列表,不提供旧协议发送兜底。对平台没有独立时间的合并用户,按边界不可判定跳过;补资料不重新激活旧决定。
### 5.2 存储方案
优先复用 PostgreSQL,不增加 Redis、BullMQ、独立队列服务或通用策略引擎。**传输复用已存在的 `gateway_task`;以下六种记录只承担业务关系、触发与执行占用职责,不复制通道收发及重连队列。**以下是逻辑表设计,**不是可执行迁移**;最终表名与编号在实施时核对最新数据库状态。
| 新表/记录 | 关键字段与约束 |
| --- | --- |
| `creator_dispatch_configs` | `main_account_id` 主键/FK;`enabled`、`generation`、绑定的 `listener_generation`/浏览器运行身份及连续性状态、`config_version`、`accept_after`、接收状态/原因;四类事件开关、两个动作开关、文字、间隔/冷却、轮转位置;账号只允许 douyin |
| `creator_dispatch_members` | 独立关系表;联合主键 `(main_account_id, worker_account_id)`,两列分别引用现有账号;大号≠小号;本期另设 `worker_account_id UNIQUE` 限制单一归属,该约束不作为关系标识;成员改动与旧任务取消同事务;关系删除不删小号账号 |
| `creator_dispatch_inputs` | 引用已保存通知与独立目标 UID;唯一 `(main_account_id, event_id, target_uid)`;该目标真实互动时间与资料来源、昵称/secUID、接收代次、处理决定/原因;新通知与不调度决定均可追踪;补资料不重置决定 |
| `creator_dispatch_actions` | 固定来源/目标/执行账号、代次/配置版本、动作/文字快照、触发 ID、前置动作 ID、唯一业务 `operation_id`、关联 `private_message_id`(私信动作)、固定 `agent_operation_id`、唯一 `gateway_task_id`、状态、实际发起/完成时间、平台结果/错误;唯一 `(input_id, action_kind)`;同操作不因超时另建传输任务 |
| `creator_dispatch_targets` | 主键 `(main_account_id, target_uid)`;活动触发、冷却截止、阻止原因;事务锁防止并发事件绕过去重与冷却;不明结果阻止独立于冷却 |
| `creator_account_write_slots` | `account_id` 主键;当前操作 ID、占用标识、占用/核对状态、`next_allowed_at`、时间;统一约束手动私信与自动关注/私信,间隔跨重启保留,不替代网关现有身份/所有权校验 |
**大小号归属只以 `creator_dispatch_members` 为准。**账号表、账号资料和浏览器环境均不增加 `owner_id`、所属大号字段或成员 ID 列表,也不保留另一份归属缓存作为事实来源。查询成员、候选排除、任务分配及执行前归属核验统一通过关系表;任务中的来源大号与执行账号只是当次执行快照,不用它们反推当前关系。
联合主键标识一对大小号,天然允许关系表将来保存多个大号关联同一小号;本期单一归属由单独的 `worker_account_id UNIQUE` 约束和业务校验实现。未来开放多对多时使用新编号迁移调整该约束,无需把归属字段从账号表搬出,也不修改已执行的迁移。本期不提前加入共享小号入口或额外配置。届时必须另行确认跨组优先级、公平排队和重复触达规则,继续沿用账号级写占用与节流;不能将移除唯一约束等同于多对多业务已经完成。
成员与角色互斥跨表约束采用统一数据库事务锁保护所有关系修改;不能只靠下拉框排除或一次查询。轮转、目标占用、任务生成同事务。多 worker 领取用 PostgreSQL 行锁与 `SKIP LOCKED`,领取失败不发起外部动作。
账号写操作占用**不得到期直接放行**:占用超时后进入核对/阻止状态。旧 worker 失联但网关仍在执行时,不能让新 worker 接管并再次发送。普通查询、采集可继续;涉及相同账号的主动写操作必须使用统一占用。普通账号的手动私信也走这个入口,避免角色变化前后的两套发送路径。
现有 `RunPrivateMessageSync` 启动会调用 `RecoverInterruptedPrivateMessages`,无条件将全库 sending 改 unknown;这会误伤其他实例仍执行的请求,并使其真实结果无法通过只更新 sending 的 `FinishPrivateMessage` 保存。P3 必须移除这条批量恢复路径,改为逐操作根据统一占用与结果凭据核对;新实例启动不能处理仍有有效执行者的请求,未知记录收到可核验的同操作迟到结果时允许幂等确认,不重新发送。
各账号外键使用现有内部 ID,平台 UID/通知 ID/消息 ID 按现有字符串规范保存,不能转成 JavaScript number;动作快照限制文字长度,配置约束正整数时长。为待处理输入、账号待执行动作及分页结果建立对应索引;删除配置与账号的外键策略须与现有完整删除流程一起验证,在途动作先收尾/阻止再删除,不能直接删账本来释放仍在执行的操作。
业务状态建议:`pending → claimed → queued → sending → succeeded / failed / unknown`;`queued` 表示已有传输任务但尚未下发,未发起可 `cancelled`,后续私信因关注失败可 `skipped`。`claimed` 确认尚无传输任务/未发起时可安全恢复;`queued` 恢复必须查原 `gateway_task`,不能重新插入;`sending` 必须核对,不自动退回。定义“明确失败”必须证明平台动作未成功,不能仅因通道 failed、HTTP 500 或本地保存异常就标业务 `failed`。
已发生外部动作但结果保存失败:保留原 `operation_id` 与数据库占用,阻止后续动作并记录错误;网关若已清除所有权标记,不能把“标记不存在”当作未发送证明,P3 必须补齐可按操作 ID 读取的结果记录或等价确认机制。重启只核对同一操作。不能为解决账本错误新建发送请求。
### 5.3 私信与关注复用
- 从现有 `privateMessageSender` 提取共享服务:输入账号、目标 UID、固定文字、请求 ID 与来源;保持聊天专用标签、UID 核验、请求先持久化、超时/SDK `1008` 不自动重发的现有语义。
- 自动私信 request ID 由持久化业务动作 `operation_id` 稳定派生;重复调用返回原发送记录,不生成新请求 ID。**当前私信真正发送给网关的 `operation_id` 是私信记录 `Message.ID`,不是 request_id。**方案保留这一真实语义,显式保存“业务 operation_id → request_id → private_message_id/agent_operation_id → gateway_task_id”关联;关注动作无私信记录,其 agent_operation_id 可与业务 operation_id 相同。核对只沿保存的关联,不能假设所有 ID 字符串相等。记录自动来源、大号与通知,手动界面仍显示真实消息与结果。
- 自动私信不以“有会话昵称”为发起前提;目标来自可信通知 UID,昵称读取失败不改发送对象。不得为自动任务创建占位账号或伪造消息已发送。
- 对关注复用现有网关动作入口,核对执行 UID、目标 UID、实际返回值、已经关注状态及错误语义;不能新建私有协议客户端。
- 主动动作的自动发起授权来自用户显式启用的对应配置和有效代次;不绕过现有动作参数与身份核验,不把旧字段当授权。
- 通知、私信、关注及状态/控制均使用现有Python Agent主动连接 `/v1/agent`。`Method/Path/Payload` 是通道内操作描述;`server/http.py` 保留处理函数不等于保留公开HTTP服务。旧服务/事件入口已删除,不重做Agent、不恢复旧拨号或HTTP回退;本方案补的是执行关联与结果保障。
### 5.4 两层任务关联与出站通道门槛
业务动作负责“为什么发、由谁发、是否还允许发”;`gateway_task` 负责同一操作的传输过程。二者不可合并,也不允许各自建立一套自动重试循环。
1. **唯一关联与原子入队:**一个不可逆业务动作对应固定业务 operation_id、固定 agent_operation_id 和唯一执行 gateway_task_id;身份读取及不会关注/发送的前置页面准备请求可有独立查询任务,不能将其回包当作执行结果。私信记录/请求关联、动作快照、账号占用、传输任务及关联在同一数据库事务保存,提交后才下发。两次独立写库会产生无法取消的孤立传输任务。共享发送服务须支持该关联,手动请求使用现有 request_id 作为同一逻辑操作的稳定依据。当前通道 `CreateGatewayTask` 每调用就插入一行,不能直接当作幂等入队接口。
2. **可靠领取:**通道只有数据库 pending→dispatched 条件更新实际成功的执行者才允许下发;失败或返回 false 不得继续发送。直接调用与 dispatcher 共用领取规则。再次收到同 task_id 时,Agent 只核对原操作或返回已保存结果,不重复执行。已下发、不明或失败后的业务动作不另建 task_id。
3. **取消与有效期:**关闭/变更事务同时取消 queued 业务动作及未下发传输任务;修复上线前核对既有无关联pending写任务,能关联原操作才核对,不能因补齐SQL/Agent后突然补发历史孤立任务。dispatcher 发出业务写任务前重新核对原动作代次、版本、归属、账号占用和明确截止时间。截止时间复用操作的既有超时预算,不新增用户配置,重连不能重置预算。过期/取消的写任务不因 pending 补发规则再次执行;记录证明未下发的取消原因。read-only 任务与写动作的重试条件分开。
4. **回包关联与并发:**同步等待按 gateway、连接所有权/运行代次、task_id 注册;接收每个结果校验其属于原任务、原账号动作,再唤醒对应等待者。单一 waiter 或“最后一个请求”不能支撑多个大号、小号及监听/采集共用网关。迟到结果不得交给下一个操作;无等待者的结果也必须保存。
5. **先保存再确认:**Agent 结果携带原 task_id、Agent使用的operation_id及足以核验的平台结果,按已保存映射关联业务操作。平台先持久化传输终态和小型业务结果凭据,再向 Agent 确认;Agent 在确认前保留同操作结果、重连补送结果而非重新执行动作。结果保存失败阻止后续写动作并记录错误,不向等待者宣称成功。可以保存必要 JSON/平台消息ID/关注确认信息,不能把封面、截图或大响应字节塞入任务表。P0 须定清结果凭据的保存位置与关联,当前 response_status/response_bytes 不足以恢复业务结果。
6. **终态分层:**网关 task succeeded 仅证明请求返回,不证明关注或消息成功;task failed 的超时/断线/保存失败不证明平台未执行。业务解析自己原操作的真实结果,分别更新 succeeded/failed/unknown。SDK1008、失联、平台成功但本地记账失败都保留原操作核对路径,不自动补发;当前存储需补齐接收同操作确定性迟到结果的能力,而不是拒绝终态后的回包。
7. **连接所有权与启动恢复:**新连接替代旧连接后,旧连接清理只能影响它持有的任务,不能将新连接及其他实例的任务终态化。平台启动不能无条件将全部网关离线、全部在途任务失败;每个任务必须能证明所属执行者/连接已失效,再进行核对。网关删除必须检查其 pending/dispatched 写任务和账号业务占用,不能通过 FK cascade 擦除在途账本;账号/大号删除检查关联 task_id,保留有未知结果的阻止。
8. **协议一致性:**Agent已按平台根地址构造 `/v1/agent`,支持HTTP/HTTPS、尾部斜杠及根路径前缀。任务payload直接接收JSON对象,null视为空对象,字符串/数组/标量明确返回400;不保留旧字符串解码兼容。task/result/event/ack/nack及订阅错误帧须两端真实对照,不让单账号 `type:error` 导致整个共享连接退出。验证使用生产帧与配置形状,而不是为测试改造出的特例。
9. **通知订阅隔离:**事件投递与 ACK 校验 gateway、连接所有权、alias、subscription、session_id、listener_generation;恢复是核对原订阅或建立新代次,不能仅按 alias 把旧批次写到新账号会话。队列溢出/退订必须用非阻塞隔离机制,不能在已满队列继续发送故障标记,也不能向已关闭队列写入;单账号积压不能阻塞同连接其他账号的结果、心跳与保存确认。
这是自动执行前必须满足的完整契约,不是声称 `77a90f7` 已实现,也不是要求P0实现全部大小号业务表。P0修复并验收通道层保障、明确可测试的业务契约;P1落实关系/动作表与传输关联,P3完成共享发送的原子入队、双层取消和业务结果核对;这些完成前不开放自动执行。优先补现有通道/共享服务,不另造表面兜底。
### 5.5 文件位置与接口职责
建议使用简单、按职责分离的文件,不做大型目录重构:
- `internal/creator/dispatch_config.go`:配置、成员、角色互斥、启停代次。
- `internal/creator/dispatch_store.go`:输入决定、去重、目标占用、轮转、任务及结果。
- `internal/creator/dispatch_worker.go`:独立运行循环、运行条件、领取、节流与恢复。
- 共享发送/执行服务放当前后端业务服务所在层,供手动 API 与调度调用;`internal/creator` 保持存储职责,不直接依赖 HTTP handler 或浏览器客户端。
- `internal/controlplane/api/`:大号配置读取/保存、任务结果分页;不增加历史发送或批量重发接口。
- 当前账号相关页面:大号设置及结果入口;分页结果沿用当前标签页记忆规则。
- `internal/environment/migrations/`:新增未使用的后续编号;禁止修改 1017、043、1051 等已执行脚本。
### 5.6 界面方案
保持 Ant Design 默认组件,不另造大小号管理系统或独立账号列表。
- 在“我的账号”的“更多”增加“大号设置”,未登录环境不显示;进入原账号详情中的专用设置位置,不增加列表页头操作行。
- 默认关闭。关闭时配置可以查看/编辑;启用时明确列出触发通知、小号、动作与文字,避免用户把开监听误认为会自动发送。
- 小号用可搜索、多选列表,昵称展示沿用现有规则;显示不可选择原因及已选账号的实时可用情况。
- “自动关注”“自动文字私信”两个独立开关,私信打开才要求文字;保存成功不宣称浏览器启动或任务正常。
- 关闭大号需确认,说明待执行任务取消、已发起请求无法撤回;取消确认不产生任何变化。
- 账号详情展示独立调度状态和任务结果:触发通知、目标真实昵称/资料缺失、所用小号、关注结果、私信结果、真实时间与原因。未知结果突出显示,但不覆盖现有采集 Tag。
- 事件聚合保留原只读通知视图及按监听开关隐藏历史的规则;可提供任务结果入口,不把私信插入事件聚合。
- 本期不增加历史下发、群发、AI 回复、任务拖拽改派或批量重试按钮。
## 6. 可观测性与错误处理
这是运行排错日志,不新增审计系统。关键节点关联 `main_account_id / worker_account_id / event_id / target_uid / generation / config_version / operation_id / agent_operation_id / private_message_id / gateway_task_id`。
必须记录:通知已保存、是否符合边界、未调度原因、成员及轮转选择、业务领取、关联 gateway_task_id、传输领取布尔结果、连接/订阅所有权、发起前身份/状态核验、真实发起、匹配的回包ID、平台返回类别、两层账本写入、结果确认、关闭取消、重启核对与阻止解除依据。日志能区分“已入队/已下发/平台已确认”,不能以同一个完成状态隐藏差异。
页面最少区分:关闭、待运行条件、正常接收、无可用小号、执行中、结果未确认、读取失败。缺状态不能显示正常;浏览器状态失读不使用保存的 runtime_id 冒充正在运行。错误不能用“兜底换号”“自动恢复暂停”“将 unknown 视为 failed”掩盖。
## 7. 实施阶段与完成标准
所有实现采用 TDD;相关业务单元测试覆盖率至少 65%。不主动编写或运行 E2E,不以读取成功代替真实动作验收。
| 阶段 | 实际范围 | 完成标准 |
| --- | --- | --- |
| P0:执行保障与现有通道验收 | 复用已完成Agent、已修入口/对象载荷、部署配置与网关页面;继续对齐错误帧契约并验证SQL空值/超时及1058部署执行状态;修复回包匹配、条件领取、结果持久化/确认、连接所有权、非阻塞订阅隔离;定清关联、原子入队/取消、迟到结果及生命周期;更新产品规则,大规模改动前建分支 | 实际配置→网关在线→任务/回包→通知保存ACK最小闭环成立;同网关多请求交错及重连不串号/重发;新库/已有数据/重复启动通过;平台结果有确定性凭据;未经授权不做真实发送;不能仅以Go单测通过验收 |
| P1:配置与角色 | 新业务表与 gateway_task 关联、默认关闭、大号设置、独立关系表与单一归属、原监听/暂停/运行入口联动失效、多用户目标协议及完整删除 | 新数据库、已有数据升级、重复启动都通过;并发绑定、角色互斥、未绑定环境限制有单测;不改变环境身份或采集状态 |
| P2:只生成可解释结果 | 通知与多用户目标同事务保存、边界/代次、去重/冷却、轮转、任务快照、结果列表;**外部执行尚未启用** | WS 重放/历史补资料不重复;边界前/缺时间/关闭期间不派发;无小号不积压;重启规则一致;UI 明确“执行能力未启用” |
| P3:共享执行与私信闭环 | 统一账号写占用与正确的进程锁释放,移除全库私信恢复;共享发送原子关联消息与传输任务;双层取消/领取、结果凭据/核对、request/message/Agent/task ID映射、节流;只允许关注关/私信开执行 | 同账号手动/自动不并发;超时、重启、1008、保存失败不重复发送;账号身份不一致阻止发起;错误可查;先完成一个大号一个小号的最小闭环 |
| P4:关注及组合动作 | 验证关注确认语义、已经关注、关注→私信前置关系与逐动作节流 | 关注/私信可独立关闭;两个动作固定同小号;关注失败/不明不发后续私信;部分成功与真实结果一致 |
| P5:多组和异常验收 | 多大号独立启停、多小号轮转、删除/换组/暂停/掉线/重启 | 一个组异常不影响其他组或通知保存;旧代次无新增动作;关闭显示真实在途状态;经用户授权以受控账号真机核对平台、数据库、页面同一动作 |
1058已增加临时DEFAULT,旧“非空表先加NOT NULL必失败”的静态问题已修复,不再把它列为当前阻断。仍须核对1058是否曾在部署环境执行:已执行环境不得以改同一编号代替新迁移;未执行环境按新版验证新库、有数据升级及重复启动。此次未实际运行PostgreSQL集成测试,不能凭SQL修订/测试用例存在宣称迁移全部验收;新编号据真实执行状态确定。
P1/P2 尚未具备执行能力,生产界面只能保存关闭状态的配置,不允许提前开启自动功能;模拟开启、代次与任务生成仅在测试数据中验证。如确需查看预览,记录必须为明确不可执行的终态,不得在 P3/P4 上线后转换成可发送任务。执行能力启用时由用户重新开启,保存新的有效边界,不接手此前阶段积压。P2 不得显示为“自动执行已完成”;P3 仅证明私信链路,关注开关在 P4 验证前不可开启,也不生成可执行的关注/组合任务;P4 开放时建立新边界,不执行之前阶段的组合记录。P4、P5 完成前不得宣称双动作与多组全部可用。
关键测试清单:
1. 默认关闭、普通账号不受影响;每个大号独立设置、关闭及重新开启。
2. 归属只读写独立关系表,账号与环境没有所属大号字段;联合主键阻止重复关系,独立唯一约束阻止本期共享;关系删除不删账号。账号未绑定不可选;自选、共享、角色嵌套及并发角色修改被拒绝;禁言/暂停/状态未知不执行。
3. 开启边界前、等于边界、关闭期间、缺时间、未来时间、旧代次、历史补资料与 WS 重投。
4. 同事件多用户、同用户多通知、并发输入、轮转顺序、同目标跨动作冷却、无可用小号不追发。
5. 两种动作三个有效组合;快照不改写;规则/文字/成员改变取消旧待执行任务;不转派旧任务。
6. 执行前改开关、改成员、换代次、监听关闭、账号暂停、身份变化及读取错误。
7. 手动与自动同时发送、多个 worker 同时领取、账号占用到期但网关仍执行;一实例仍在发送时另一实例启动不能改其状态或阻止真实结果保存。
8. 发起前失败、平台明确失败、平台成功但账本失败、请求超时、SDK1008、重启与关停超时;迟到结果只确认原操作,不新建任务或重复发送。
9. 首次迁移、已有 gateway 数据升级、重复启动、原账号/通知/私信保留与账号完整删除;旧脚本内容不变;网关删除不级联抹去在途账本。
10. 前端配置校验、保存失败保持输入、关闭确认、网关出站字段契约与状态误读防护;修改 antd 前查 CLI API,修改后 lint 警告清零。
11. 同网关不同账号的并发请求交错回包、未知ID、无等待者及迟到结果,不串号;写失败不确认结果,不宣称成功。
12. execute 与 dispatcher 争抢同pending、CAS返回false、重复task、旧连接断开与新连接/另一实例启动,均不重复下发或误改其他任务。
13. queued业务已取消而传输仍pending、关闭与领取同时发生、重连扫pending、超过绝对截止时间,不能补发旧代次动作;Agent无法确认未发起时保留unknown。
14. alias相同但subscription/session/generation不同、队列溢出、退订与投递并发,一个账号异常不阻塞同连接其他账号结果/心跳。
15. 原子入队与结果保存各节点崩溃、回包已收到但未保存、平台保存成功但Agent未收到确认,均沿同task/operation核对或补送结果,不重新执行动作。
16. 平台根地址生成正确 `/v1/agent`;按Go真实JSON对象发送POST、null/空对象可用、字符串/数组/标量明确拒绝;订阅错误帧不导致其他账号断线;正常403夹具须断言实际状态码,不以夹具内部异常冒充通过。
## 8. Review(77a90f7 复审)
### 8.1 此次更新已解决的事项
| 上轮问题 | 最新状态与依据 |
| --- | --- |
| Python出站Agent缺失 | `browser_gateway/agent.py`已实现连接、心跳、并发任务、事件ack/nack、断线重连;不再规划新建Agent |
| 旧HTTP/事件服务仍公开 | 入口改为Agent运行,旧HTTP服务端及旧事件通道模块已删除;操作处理函数仅复用于任务载荷 |
| 网关页面与部署示例未对齐 | `web/src/pages/gateways/index.tsx`、`deploy/browser-gateway.env.example`已展示出站连接设置及在线/从未连接状态 |
| 根地址未追加Agent入口 | 本次修复 `_connect` 构造 `/v1/agent`;HTTP/HTTPS、尾斜杠、路径前缀测试通过,平台夹具只接受正确入口 |
| Go对象载荷被再次json.loads | 本次修复 `_execute_task` 直接使用对象/null;非空POST协议单测验证载荷、64位ID字符串、Unicode文字与结果,不执行真实动作 |
| 连接拒绝夹具自身TypeError仍测试通过 | 夹具修正Response的Headers/body参数,并断言403,避免以500/404错误冒充正常拒绝 |
| 1058无默认值加NOT NULL导致旧表升级失败 | 新增列时提供临时DEFAULT,回填后移除DEFAULT;旧静态失败原因已消除。已有新库/旧表/重复启动测试定义,但真实部署执行状态与DB测试仍待核验 |
### 8.2 剩余阻断与风险
以下是当前源码及未验收门槛,**网关接入完成不等于大小号自动执行保障全部完成**。不把已解决的问题重复列为阻断,也不把文档修订写成代码修复。
| 严重性 | 问题与重现场景 | 源码依据 | 文档处理/前置要求 |
| --- | --- | --- | --- |
| 阻断 | 同网关两请求并发或超时后迟到回包,单waiter且不核对id,可能把一个账号结果交给另一个 | `gateway_channel.go:61–70,141–188,190–214` | 按task_id/连接所有权匹配并保存所有回包;多账号交错测试是门槛 |
| 阻断 | execute与dispatcher争抢pending,领取返回false仍发送;Agent进程内会拒绝重复ID,但409可能被错当原动作结果,重启后集合消失 | `gateway_channel.go:141–164,345–374`;`gateway_task.go:82–93`;`browser_gateway/agent.py:191–200` | 仅条件领取成功者下发;去重须关联原结果,不能仅返回409或依赖内存集合 |
| 阻断 | 只取消业务记录,pending网关任务重连后仍补发;当前每次调用都新建传输任务且无业务关联 | `gateway_channel.go:114–138,345–374`;`gateway_task.go:64–79` | 关联唯一task、原子入队、双层取消,发出前复核有效代次与绝对截止时间 |
| 阻断 | 异步结果无等待者不落库;同步落库错误被忽略;只保存响应大小/状态,重启无法确认消息真实结果 | `gateway_channel.go:161–188,204–214`;`gateway_task.go:19–29,97–115` | 结果按ID先持久化再确认,保留必要凭据;不以入库任务或返回200冒充发送成功 |
| 阻断 | 超时/断线将传输记failed并丢弃迟到结果,业务可能仍成功;终态条件不允许事后确定性结果核对 | `gateway_channel.go:170–188,204–214`;`gateway_task.go:97–115`;`internal/creator/private_messages.go:304–326` | 分开传输终态与业务unknown,同操作迟到结果可核验确认,绝不新建动作补发 |
| 高 | 旧连接fail能影响新连接任务;进程启动全库离线/failed,可能误伤另一实例仍在执行 | `gateway_channel.go:261–276,315–330`;`gateway_task.go:118–131`;`internal/controlplane/app/app.go:70–73` | 连接/执行所有权与按操作恢复;不是全库重置或单进程锁 |
| 高 | 仅按alias投递未校验subscription/session,旧订阅可能被写入新代次;溢出时向满队列写错误会阻塞接收 | `gateway_channel.go:230–252,287–298`;`event_listener_worker.go:219–284` | 核验原订阅身份;非阻塞隔离及投递/退订竞争测试,不阻塞同连接结果与心跳 |
| 高 | 网关删除FK级联擦除任务;大号删除若只看自身环境,会丢掉小号在途账本 | `1058_gateway_outbound_channel.sql:19–21`;`internal/account/deletion.go:94–113` | 关联业务与传输占用共同检查,未知/在途时拒绝最终删除,不通过删表解锁 |
| 阻断 | 新pending行response_status为NULL,扫描到int会报错;已插入但调用失败,不能创建/读取可用任务 | `1058_gateway_outbound_channel.sql:19–30`;`gateway_task.go:19–29,44–79,135–162` | 补齐生产SQL空值语义、确认失败无孤立写任务;不以内存桩测试代替真实SQL验证 |
| 高 | 创建浏览器调用预算11分钟,任务接口最多600秒,当前在下发前拒绝 | `internal/controlplane/api/environments.go:75–79`;`gateway_task.go:64–73` | 统一原业务超时与任务上限,验证启动及长操作,不隐藏拒绝或偷偷缩短预算 |
| 高 | Agent监听异常发送 `type:error`,Go不识别而断整条连接,会波及其他账号和在途任务 | `browser_gateway/agent.py:295–346`;`gateway_channel.go:190–214` | 错误帧契约对齐,仅隔离对应订阅,不用断全部任务掩盖单账号读取错误 |
| 阻断 | Agent断线只取消未开始future,已运行操作继续;结果仍向旧连接发送,丢失时无结果缓存/补送;重连后重复ID只回409 | `browser_gateway/agent.py:123–129,209–245` | 已开始动作沿原ID保留结果凭据并重连补送,不能当作未执行;unknown不解锁/重发 |
### 8.3 原方案继续保留的约束与本轮补充
- 大号可选且独立启停;小号来自真实自有账号;**归属只在独立关系表**,联合主键+本期单独唯一约束,账号不加owner字段,未来多对多另评跨组规则。
- 当前消息网关operation_id是私信记录ID,不等于request_id;本轮补齐业务/私信/Agent/传输ID映射,禁止猜测对应关系。
- `RunPrivateMessageSync` 全库sending恢复、`FinishPrivateMessage`仅更新sending的迟到结果限制仍须修改,不能将通道failed直接覆盖成私信failed。
- 当前 `Store.acquireAutomaticExecutionLock` 使用会话级 `pg_advisory_lock`,释放只调用 `sql.Conn.Close()`,没有显式解锁;连接回到池中不等于会话锁释放,复用前必须核对并修复释放/失败路径。不能用进程锁或不可靠的会话锁代替持久账号占用(`internal/creator/store.go:78–87`)。
- 只处理有效开启边界之后真实发生的通知;多用户目标与各自时间必须明确,缺失/合并时间不可证明则不发送。小号私信仍要求自身监听开启。
- 关注与私信独立开关;两者都开时顺序确认,逐动作节流、快照不改写、不改派。只允许同操作确定性结果解除unknown,不凭最近50条里没有消息或冷却超时解除。
- 普通账号不受影响;不改变环境身份、profile、指纹或采集游标;关闭/关系变更取消两层未开始任务,在途请求不能宣称可撤回。
- 旧策略表及big_account残留不视为功能;源项目并行批次、事件轮转、1008失败分类、积压重放仍不照搬。
### 8.4 验证状态与结论
本轮按77a90f7复审后,经确认仅修复网关地址与对象载荷;后台帧格式不变,不修改并发、重复下发、结果恢复或数据库等其他问题。文档已移除两项已修阻断并保留其余门槛。
- TDD先新增测试复现失败,再修改Agent:15个Agent测试通过;覆盖率81%,连同网关/入口/私信相关回归共89个Python单测通过,Agent覆盖率82%,超过65%要求。
- Go API/environment单元测试通过。根地址HTTP/HTTPS、尾斜杠/路径前缀、真实对象POST/null/空对象、非法载荷拒绝及正常403夹具均已验证;前置路由在测试中mock,没有真实关注/发送。
- 未配置PostgreSQL测试URL,数据库集成用例未执行;未进行真实动作或E2E。源码/测试通过不代替生产SQL、实际连接与多账号并发验收。
- Agent、页面/部署契约、1058临时DEFAULT及本次入口/对象载荷已解决;错误帧、SQL读取、并发回包/领取、通道结果持久化及迟到回包处理仍是P0门槛。
- 业务原子关联、账号写占用、双层取消及业务结果核对在P0定清契约,P1/P3落实并验证后才允许自动发送;本次仅改Agent两处及测试,未实施上述业务能力。
- 修订后独立复核确认产品边界未变;已澄清URL要求与P0/P1/P3职责,文档可作为P0起点,不是自动执行许可。
- 间隔30秒、冷却4小时、四类通知默认全开等仍是建议默认值,批准实施时确认;本轮不改变用户已确认的产品边界。
结论:**旧方案不能直接按原顺序实施。先补齐并验收出站通道与执行保障,再进入大小号配置、只读任务预览、私信与关注闭环;当前不得直接开启自动发送。**
## 9. 调研依据
### 9.1 本地源码
路径以各仓库为根,行号以本次基线为准,实施前需再次同步核对。
- `douyin-pc/src/account_store.py`:规则校验约 30–130;成员/停止约 770–855;历史与通知约 878–1230;分配约 1242–1355;领取约 1366–1450;执行结果/恢复约 1450–1632。
- `douyin-pc/src/account_engine.py`:组启动约 267–311;停止约 313–375;执行及结果分类约 499–599;worker 循环约 611–657。
- `douyin-pc/src/account_session.py`、`douyin_im.py`、`follow_user.py`:浏览器身份、具体动作与 SDK 错误;`src/test_plan01.py`、`docs/plan01.md`:目标与已有单测。
- `internal/creator/accounts.go`、`models.go`:现有账号资料及废弃大号字段。
- `internal/creator/event_listener.go:212` 起:`RecordListenerDeliveries` 与开关代次/事务。
- `internal/creator/private_messages.go:231` 起:`BeginPrivateMessage`、请求 ID 去重及结果状态。
- `internal/controlplane/api/private_messages.go:140` 起:现有发送服务、网关operation_id为Message.ID及专用聊天身份核验;`:178` 起私信同步启动恢复。
- `browser_gateway/platform/douyin.py`:`FOLLOW_SCRIPT`、`follow_expression`、`action`、浏览器持久化所有权标记;`browser_gateway/server/http.py:393–498`:动作执行与占用/阻止处理。
- `internal/creator/scheduler.go`:现有采集计划;`internal/account/deletion.go`:账号完整删除。
- `internal/environment/migrations/1017_creator.sql`、`043_schema_consolidation.sql`、`1051_private_messages.sql`:旧链路创建/删除与现有私信账本。
- `internal/environment/migrations/1058_gateway_outbound_channel.sql`、`internal/environment/gateway_task.go`:当前出站配置、传输账本、条件领取、终态/恢复与SQL空值;`internal/controlplane/api/gateway_channel.go`:匹配、下发、回包、订阅和连接清理。
- `browser_gateway/agent.py`、`browser_gateway/test_agent.py`:新Agent任务/事件生命周期、内存去重、断线和测试;旧事件通道文件已删除。
- `docs/gateway-outbound-channel-plan.md`、`AGENTS.md`:当前通道方向;`deploy/browser-gateway.env.example`、`web/src/pages/gateways/index.tsx`:已对齐的出站部署/前端接入契约。
- `internal/environment/migration_test.go`:新库、旧gateway有数据及重复迁移的测试定义,依赖显式PostgreSQL测试URL;此次未执行真实数据库验证。
- `internal/creator/store.go:78–87`:当前会话级advisory锁与连接池释放;使用前须核对实际释放,不根据旧索引描述推断。
- `web/src/pages/accounts/index.tsx`、`web/src/components/accounts/AccountManagementList.tsx`、`web/src/pages/accounts/$id/index.tsx`:自有账号列表与详情。
### 9.2 成熟产品模式参考
本次已读取官方页面;借鉴行为约定,不引入相关产品依赖。
- [BullMQ:Idempotent jobs](https://docs.bullmq.io/patterns/idempotent-jobs):重复执行不应改变最终结果;任务应小而原子。抖音发送不是天然幂等,所以本方案把可重试数据库操作与不可盲重试的外部动作分开,不能因为队列可重试就重发消息。
- [n8n:Concurrency control](https://docs.n8n.io/hosting/scaling/concurrency-control/):将排队与并发执行限制分开,执行能力不足不等于实际执行成功。本项目额外需要账号级串行,不照搬只有全局并发数的方案。
本期明确不新增:AI 回复、历史下发、共享小号、通用策略引擎、新账号库、新浏览器管理器、新事件轮询、批量自动重试。大小号不另做一套网关迁移或传输队列,但现有出站通道闭环与正确执行语义是不可省略的前置条件,不能将它列入排除项后直接自动发送。