From 60be4e6210f9ff6d6f0cdd60c672d7c2447d5e13 Mon Sep 17 00:00:00 2001 From: sunlei Date: Sat, 25 Jul 2026 08:50:05 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85QQBot=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E6=8E=A8=E9=80=81=E8=BF=90=E7=BB=B4=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API.md | 13 +++ README.md | 16 ++++ src/modules/qqbot/core/schema/README.md | 100 ++++++++++++++++++++++++ 3 files changed, 129 insertions(+) diff --git a/API.md b/API.md index d9b3aaa..6081f4c 100644 --- a/API.md +++ b/API.md @@ -450,6 +450,19 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT 响应仅返回管理契约字段:source definition/field/variable 白名单;STUN 的 port-forward/DDNS 候选白名单;subscription、template、preview、binding/target 和 target option 视图。不会返回 adapter、entity/repository、`activeKey`、digest、软删除字段、账号内部 ID、事件 payload/delivery/lease/retry 状态、凭据、access token、Provider/OneBot/MQTT 原始对象。系统事件只能通过 Nest 内部 Outbox stager 暂存,不存在 publish、event、delivery、fan-out、retry 或 worker HTTP 发布接口。 +#### 内部事件与投递生命周期 + +- 只有一个既有有效端口直接变化为另一个有效端口的 `changed` 事件,才会在 endpoint-history 同一事务内以生产者 `eventId` 幂等写入 `qqbot_message_event` Outbox。首次 `published`、租约续期、仅 IPv4 变化、`withdrawn`、`restored`、重复事件和回滚事务都不产生消息事件;事务提交后才调用 `requestDrain()`。 +- Outbox 扇出状态为 `accepted`、`processing`、`retry`、`completed`、`failed`;投递状态为 `waiting_ddns`、`pending`、`processing`、`retry`、`success`、`failed`、`superseded`、`cancelled`。每个 runner 每次最多领取 50 行并设置 30 秒租约;启动后立即恢复且每 5 秒扫描。过期 `processing` 租约可由重启后的进程重新领取。 +- DDNS 未就绪的投递进入 `waiting_ddns` 并每 60 秒复检。只有对应 A 记录的 optimistic `synced` / `appliedAddress` 更新成功持久化后,`notifyDdnsSynced()` 才提前推进相关任务;发送前仍重检当前 endpoint、DDNS、订阅、绑定、目标和账号。更新 endpoint 使旧未完成任务成为 `superseded`,配置停用或删除使其成为 `cancelled`。 +- 扇出和投递的临时错误从 10 秒开始指数退避,单次最长 15 分钟,并在事件发生 24 小时后截止。`sendStrictPlainText()` 只选择投递冻结的 `selfId`,把正文作为一个 OneBot `text` segment 发送,并在成功时关联 `qqbot_send_log`。数据库唯一键保证事件和事件-目标任务幂等;OneBot 超时可重试,因此外部结果不明确时仍是可能重复的至少一次投递,而不是跨系统恰好一次。 + +#### SQL、发布与回滚 + +既有环境使用幂等增量入口 `sql/qqbot-init.sql`;只有一次性、可丢弃的全量初始化环境才依次使用 `sql/refactor-v3/00-full-schema.sql`、`01-seed-core.sql`、`99-verify.sql`。发布顺序是:备份六表及相关菜单/角色授权行 → 应用对应 SQL 入口 → 验证表、唯一/调度索引、默认模板与权限 → 先验证 API 再发布 Admin → 创建订阅和逐账号绑定 → 使用授权非生产目标完成有界 A→B 验收。 + +回滚先停用全部发布绑定,再回滚 Admin 和 API,并保留事件、投递及发送日志;Network Agent、端口转发、STUN Keeper 和 DDNS 继续运行。Jenkins/K8s 通过只证明版本已部署,不能替代真实 CRUD、页面或事件到消息的功能验收。当前已有实现和自动化 API 证据,但因缺少安全隔离的本地前置条件,真实本地 CRUD、Admin 页面、数据库支持的 Outbox/DDNS 流程和授权 QQ 投递仍未验证;本次文档变更没有推送、部署或执行生产 SQL。 + ### NapCat Runtime Profile | 方法 | 路径 | 说明 | diff --git a/README.md b/README.md index 4779683..d89be69 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,10 @@ System 网络管理以 MySQL 中的 TCP/UDP 端口转发期望状态为唯一事 QQBot 系统消息推送由全局消息订阅和模板、账号范围发布绑定及耐久事件/投递共同管理;账号接口严格使用路由 `selfId`,不会回退到其他机器人。群聊和私聊目标分别使用 `group` / `private`,Snowflake、QQ 账号和目标 ID 在 HTTP 与数据库边界始终保持字符串。系统事件只能通过内部 Outbox stager 暂存,不提供 publish/event/worker HTTP 路由;管理响应仅返回字段白名单,不暴露账号凭据、Provider/OneBot/MQTT 运行对象、原始事件载荷或内部持久化键。 +`qqbot_message_event` 同时承担事件 Outbox:扇出状态为 `accepted`、`processing`、`retry`、`completed`、`failed`;按目标冻结的 `qqbot_message_delivery` 状态为 `waiting_ddns`、`pending`、`processing`、`retry`、`success`、`failed`、`superseded`、`cancelled`。扇出和投递每次各领取最多 50 行,处理租约为 30 秒;进程启动后立即恢复,并每 5 秒扫描一次。`waiting_ddns` 每 60 秒持久化复检;临时错误从 10 秒开始指数退避,单次最长 15 分钟,并以事件发生后 24 小时为截止时间。 + +Network 端点历史事务提交后才调用 `requestDrain()`;DDNS 只有在 optimistic `synced` / `appliedAddress` 状态成功持久化后才调用 `notifyDdnsSynced()` 提前唤醒相关等待任务,周期扫描仍负责恢复漏唤醒。每次正式发送都重新检查来源和配置,只使用投递冻结的准确 `selfId`,正文作为一个 OneBot `text` segment 下发。数据库唯一键避免重复事件和重复事件-目标任务,但 OneBot 超时后的重试具有至少一次语义:超时结果不明确时,收件端仍可能收到重复消息。 + NapCat Runtime/Protocol Profile 已完成本地 API/Admin 实施,线上发布和账号闭环按 `docs/plans/2026-06-18-qqbot-napcat-runtime-protocol-profile-implementation-plan.md` 的 Task 10 执行。当前实现覆盖运行态/协议/会话行为/历史登录事件兼容表/风险模式表,真实物理设备风格 hostname/MAC,NapCat/OneBot 配置 hash,KT `zh_CN.UTF-8` 中国桌面派生镜像资产,只读 `/qqbot/napcat/runtime/detail` 证据接口,watchdog 离线巡检告警,以及 Admin 账号页“运行态”抽屉;不绕过 QQ/Tencent 验证码、不修改 QQ/NTQQ 签名协议、不启用 privileged/host network,也不做账号级每小时/每日累计发送预算。NapCat Chinese Desktop Runtime v20 使用 KT `NapCatQQ` fork 源码构建出的 `NapCat.Shell` artifact,并在 QQ `KickedOffLine` 后标记 native login service stale;API 在源 Docker 容器在线但 WebUI 明确 QQ 离线时会同容器调用 `RestartNapCat` 重启 NapCat worker,重建 QQCore login service 后再推进 quick/password/qrcode,不做 Docker 重建、补 env 或设备身份迁移,且同一个更新登录 session 只消费一次 worker restart 预算;v14 起还会对 QQ/NapCat/Xvfb 长期进程的 `/proc//mountinfo` 做 PID 级遮蔽,防止 `overlay`、`/vol1/docker`、`docker-init`、`/docker/containers`、`napcat-instances` 等宿主路径泄露;v15 修复扫码成功时 `QQLoginInfo` 晚于登录态写入造成的 QQ 号回读空窗;v16 在 native reset 缺少 `offline()` 时改用 `destroy()` 硬重置半登录服务,并让镜像 verify 等待 mountinfo guard 收敛;v17/v18 增加 WebUI 鉴权的 `/api/Debug/RuntimeViewProbe` 同进程诊断并修正 native maps 截断导致的 hook 证据假阴性;v19 保留 WebUI `RestartNapCat` 重启 worker 时的 `-q ` 快速登录参数,避免重启后退回无账号扫码;v20 保护 API 预写的 `/app/napcat/config`,避免上游首次解包 `NapCat.Shell/*` 覆盖 `bypass.*=true` 与 `o3HookMode=0`。镜像必须先用 `scripts/napcat-desktop-cn-stage-build.mjs` staged build context,生产 `QQBOT_NAPCAT_IMAGE` 应指向验证过的 `kt-napcat-desktop-cn:desktop-cn-v20` digest。`k8s/prod/api.yaml` 保留 `desktop-cn-v20` 稳定默认值;Jenkins `QQBOT_NAPCAT_IMAGE_OVERRIDE` 和 `QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE` 仅在填写时通过 `kubectl set env` 推广已验证运行时镜像/profile,空值会继续使用 manifest/default env。回滚时重新运行 Jenkins 并填入上一版 digest/profile,或清空两个 override 后重新部署 manifest 默认值。 运行时发布时,API 仓库不提交 `NapCat.Shell.zip`;生产镜像必须从 staged context 构建,`fork-artifact.json` 必须带完整 marker metadata,包括 upstream release tag/commit、fork commit、base image digest、Jenkins URL 和 artifact hashes。release evidence 里的 NapCat base image 必须用 digest pin。API Jenkins 只消费人工确认后的运行时推广参数,不自动合并上游、不自动构建隐藏镜像,也不在 override 为空时覆盖 K8s manifest 中的默认 env。 @@ -226,6 +230,18 @@ bash scripts/bangdream-render-smoke.sh --operation-key bangdream.event.stage --t 主线发布由 Jenkins 构建镜像、推送 NAS 本地 Registry,并滚动更新 K8s `kt-prod/kt-template-online-api`。推送后不能只看 Git push 成功,需要继续观察 Jenkins、K8s rollout、新 Pod 状态和至少一条真实运行态 smoke。 +QQBot 系统消息推送按以下顺序发布和回滚: + +1. 备份 `qqbot_message_subscription`、`qqbot_message_template`、`qqbot_message_publish_binding`、`qqbot_message_publish_target`、`qqbot_message_event`、`qqbot_message_delivery`,以及本功能相关的 `admin_menu` / `admin_role_menu` 行。 +2. 既有环境只应用审查后的幂等增量入口 `sql/qqbot-init.sql`;仅一次性、可丢弃的全量初始化环境按顺序使用 `sql/refactor-v3/00-full-schema.sql`、`01-seed-core.sql`、`99-verify.sql`,不要把两种入口混用。 +3. 验证六表、活动自然键与事件/事件-目标唯一键、事件和投递调度/租约索引、默认模板,以及页面/按钮菜单和角色授权。 +4. 先发布并验证 API 健康检查、旧 QQBot 发送能力和新只读接口,再发布并验证 Admin。 +5. 管理员显式创建订阅,并在每个发布账号中选择模板、配置群聊/私聊目标和启用绑定;随后用授权的非生产目标做一次有界 A→B 端口变化、DDNS 门禁、发送日志和幂等验收。 +6. 回滚时先停用全部消息推送绑定,再回滚 Admin 和 API;保留事件、投递和 `qqbot_send_log` 历史供审计,不停止 Network Agent、端口转发、STUN Keeper 或 DDNS。 +7. Jenkins/K8s 成功只属于部署证据,不能代替真实 CRUD、页面、Outbox/DDNS 或 QQ 投递功能验收。 + +当前分支已有实现和自动化 API 证据;由于缺少安全隔离的本地前置条件,真实本地 CRUD、Admin 页面、数据库支持的 Outbox/DDNS 流程和授权 QQ 投递仍未验证。本次文档变更没有推送、部署或执行生产 SQL,不能据此声明功能已上线或已完整验收。 + ## 来源与许可证 | 一级来源 | 使用方式 | License | diff --git a/src/modules/qqbot/core/schema/README.md b/src/modules/qqbot/core/schema/README.md index 5fc52a7..642bb91 100644 --- a/src/modules/qqbot/core/schema/README.md +++ b/src/modules/qqbot/core/schema/README.md @@ -40,6 +40,19 @@ Seed linkage: - Event and delivery dispatch/lease indexes support durable worker claims, retries, lease recovery, and source-resource supersession ordering. +Worker status relationships: + +- Events in `accepted` or due `retry` are claimable; an expired + `processing` lease is recoverable. `completed` and `failed` are terminal. +- Deliveries in due `pending`, `retry`, or `waiting_ddns` are claimable; an + expired `processing` lease is recoverable. `success`, `failed`, + `superseded`, and `cancelled` are terminal. +- A selected DDNS record advances relevant `waiting_ddns` work only after its + `synced` state and `applied_address` commit. The immediate wake is backed by + the persistent 60-second recheck. +- Terminal events, deliveries, and their send-log links remain history; normal + rollback does not physically delete them. + Verification SQL: ```sql @@ -76,4 +89,91 @@ WHERE table_schema = DATABASE() 'qqbot_message_delivery' ) ORDER BY table_name, index_name; + +SELECT fanout_status, COUNT(*) AS event_count +FROM qqbot_message_event +GROUP BY fanout_status +ORDER BY fanout_status; + +SELECT status, COUNT(*) AS delivery_count +FROM qqbot_message_delivery +GROUP BY status +ORDER BY status; + +SELECT COUNT(*) AS duplicate_event_id_groups +FROM ( + SELECT event_id + FROM qqbot_message_event + GROUP BY event_id + HAVING COUNT(*) > 1 +) AS duplicate_events; + +SELECT COUNT(*) AS duplicate_event_target_groups +FROM ( + SELECT message_event_id, publish_target_id + FROM qqbot_message_delivery + GROUP BY message_event_id, publish_target_id + HAVING COUNT(*) > 1 +) AS duplicate_deliveries; + +SELECT COUNT(*) AS invalid_success_send_log_links +FROM qqbot_message_delivery AS delivery +LEFT JOIN qqbot_send_log AS send_log + ON send_log.id = delivery.send_log_id +WHERE delivery.status = 'success' + AND ( + delivery.send_log_id IS NULL + OR send_log.id IS NULL + OR send_log.status <> 'success' + OR send_log.self_id <> delivery.self_id + OR send_log.target_type <> delivery.target_type + OR send_log.target_id <> delivery.target_id + ); + +SELECT 'event_due' AS summary, COUNT(*) AS item_count +FROM qqbot_message_event +WHERE fanout_status IN ('accepted', 'retry') + AND (next_fanout_at IS NULL OR next_fanout_at <= NOW(6)) +UNION ALL +SELECT 'event_expired_lease', COUNT(*) +FROM qqbot_message_event +WHERE fanout_status = 'processing' + AND fanout_lease_until <= NOW(6) +UNION ALL +SELECT 'delivery_due', COUNT(*) +FROM qqbot_message_delivery +WHERE status IN ('pending', 'retry', 'waiting_ddns') + AND next_attempt_at <= NOW(6) +UNION ALL +SELECT 'delivery_expired_lease', COUNT(*) +FROM qqbot_message_delivery +WHERE status = 'processing' + AND processing_lease_until <= NOW(6); + +SELECT 'active_bindings' AS summary, COUNT(*) AS item_count +FROM qqbot_message_publish_binding +WHERE enabled = 1 + AND is_deleted = 0 +UNION ALL +SELECT 'unfinished_events', COUNT(*) +FROM qqbot_message_event +WHERE fanout_status IN ('accepted', 'processing', 'retry') +UNION ALL +SELECT 'unfinished_deliveries', COUNT(*) +FROM qqbot_message_delivery +WHERE status IN ('waiting_ddns', 'pending', 'processing', 'retry'); ``` + +Before applying SQL, take a transaction-consistent backup of all six +`qqbot_message_*` push tables plus the relevant `admin_menu` and +`admin_role_menu` rows. Roll back by disabling publish bindings first, then +rolling back Admin and API while retaining events, deliveries, and send logs; +do not use `DROP TABLE` or history deletion as an application rollback. + +Verification output must contain only counts or ID/index summaries. Never query +or print `payload`, `rendered_message`, `target_id`, credentials, provider +objects, or production values. The implementation and automated API evidence +exist, but real local CRUD, browser pages, database-backed Outbox/DDNS flow, +and authorized QQ delivery remain unverified because safe local prerequisites +are unavailable; this documentation change performed no push, deployment, or +production SQL.