Authorize data sync on connected agent registration
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
|
||||
## 后续专项计划
|
||||
|
||||
- [会话消息同步与分账号存储开发计划](WxAgent-会话消息同步与分账号存储开发计划.md):2026-09-21 新增,现已完成 P0–P4 实现与白名单测试账号验收;长期 endurance、断电和多控制面 HA 仍按专项记录作为后续运维范围。不改变本文第一阶段范围,后续会话消息持久化范围以专项计划为准。
|
||||
- [会话消息同步与分账号存储开发计划](WxAgent-会话消息同步与分账号存储开发计划.md):2026-09-21 新增,现已完成 P0–P4 实现与 Agent 连接授权验收;长期 endurance、断电和多控制面 HA 仍按专项记录作为后续运维范围。不改变本文第一阶段范围,后续会话消息持久化范围以专项计划为准。
|
||||
|
||||
## 1. 结论
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
## 1. 背景与问题定义
|
||||
|
||||
本计划承接 [基础开发计划](WxAgent-CSharp-开发计划.md) 和 [远程控制与白名单上报计划](WxAgent-远程多节点控制与白名单数据上报开发计划.md)。本专项对会话/消息查询存储作增量扩展,不改变微信只读数据库边界、单窗口 UI 命令队列、显式账号切换和白名单原则,不建设 SaaS 或跨账号聚合界面。
|
||||
本计划承接 [基础开发计划](WxAgent-CSharp-开发计划.md) 和 [远程控制与白名单上报计划](WxAgent-远程多节点控制与白名单数据上报开发计划.md)。本专项对会话/消息查询存储作增量扩展,不改变微信只读数据库边界或单窗口 UI 命令队列;Agent 安装并连接控制面即完成已验证账号的数据同步授权,不再要求单独确认,不建设 SaaS 或跨账号聚合界面。
|
||||
|
||||
2026-09-21 排查得到的事实:
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
- 有界初始化、消息增量、重连补传、版本与来源信息。
|
||||
- 部分/完整快照语义、权限收回、容量和保留策略。
|
||||
- 单账号备份恢复、schema 迁移、有限多账号负载验证。
|
||||
- 只允许已验证且当前允许采集的账号产生新数据;非活动账号只查已同步历史,不暗中切换微信账号。
|
||||
- 只允许已验证且当前 Agent 已连接的账号产生新数据;非活动账号只查已同步历史,不暗中切换微信账号。
|
||||
|
||||
### 2.3 不包含
|
||||
|
||||
@@ -251,7 +251,7 @@ control-plane-data/
|
||||
- [x] 设置保留期/容量预算、连接上限、清理与 checkpoint,验证慢查询和写入积压。
|
||||
- [x] 演练账号库备份恢复、平台回退后游标对账、撤销授权和备份保留处理。
|
||||
- [x] 完成 §10 的专项最小验证并记录未测范围,不把单次 smoke 当作长期稳定性结论。
|
||||
- [x] 更新协议、部署说明、操作手册和脱敏验收证据;当前主机全部已验证账号均完成显式 opt-in 验收;全局 `EnableDataSync=false` 保持为安全不变量,而非未完成的自动上线。
|
||||
- [x] 更新协议、部署说明、操作手册和脱敏验收证据;Agent 连接后自动授予当前主机全部已验证账号的数据同步权限,无需单独确认。`EnableDataSync` 仅保留为旧配置兼容字段,不再作为授权门槛。
|
||||
|
||||
P2–P4 脱敏验收记录:[`docs/validation/WxAgent-会话消息同步-P4-验收记录.md`](validation/WxAgent-会话消息同步-P4-验收记录.md)。
|
||||
|
||||
|
||||
@@ -182,21 +182,20 @@ Pending → Accepted → Running → Succeeded
|
||||
|
||||
所有请求生成或传递 `X-Correlation-Id`;错误只返回固定错误码、通用消息和 Correlation ID。控制面响应设置 `Cache-Control: no-store`、`X-Content-Type-Options: nosniff` 和 `Referrer-Policy: no-referrer`。
|
||||
|
||||
### 4.2 本地白名单是唯一数据授权来源
|
||||
### 4.2 Agent 连接是数据授权边界
|
||||
|
||||
节点本地 `remote.json` 中的 `reporting` 配置决定可上报范围,远程 Web 不能扩大它。有效授权必须同时满足:
|
||||
Agent 安装并成功连接控制面即完成节点及数据同步授权,无需额外用户确认。连接注册会为当前已验证账号授予规范化只读数据范围;远程 Web 不能扩大到原始数据库、密钥或未脱敏内容,也不能改变只读边界。
|
||||
|
||||
1. 全局 `enabled=true`;
|
||||
2. 账号存在且 `enabled=true`;
|
||||
3. `account_id` 与已确认的当前账号一致;
|
||||
4. `chatId` 是稳定、唯一、重新确认过的标识;
|
||||
5. 会话类型一致;
|
||||
6. 会话 `enabled=true` 且 `identityVerified=true`;
|
||||
7. 数据类型允许。
|
||||
有效数据授权仍要求:
|
||||
|
||||
非白名单或身份不确定的数据不得进入中心 API、中心文件、节点待发送队列或普通日志。任务权限不等于数据上报权限;读取任务即使由已认证 Web 用户创建,节点仍必须在读取前拒绝未授权会话。
|
||||
1. 节点连接认证成功;
|
||||
2. 账号身份已验证且稳定;
|
||||
3. 数据来自只读数据库/UI 快照的规范化记录;
|
||||
4. 数据类型属于当前同步协议允许范围。
|
||||
|
||||
任务结果只有读取任务可以携带 `content`。读取结果在节点账本中与 `ReportingScopes` 一起保存,补传时沿用该范围并再次经过当前授权检查;没有授权范围时只保存/回传控制元数据。
|
||||
非验证账号、原始微信数据库、数据库密钥、未脱敏 UI 树和普通日志中的完整内容仍不得进入中心 API、中心文件或节点待发送队列。连接授权只覆盖数据同步;验证写操作、消息监听和其他副作用继续由各自运行开关控制。
|
||||
|
||||
任务结果只有读取任务可以携带 `content`。读取结果在节点账本中与连接授权代次一起保存,补传时沿用该代次并再次经过当前节点认证检查;没有连接授权时只保存/回传控制元数据。
|
||||
|
||||
### 4.3 标识与数据范围
|
||||
|
||||
@@ -578,7 +577,7 @@ scp -r node-agent/WxAgent.Tray/bin/Release/net8.0-windows10.0.19041.0/win-x64/pu
|
||||
}
|
||||
```
|
||||
|
||||
当前版本不提供任何命令行配置入口,也不接受通过 CLI 修改远程凭据或上报白名单。Windows Agent 只通过双击 `WxAgent.Tray.exe` 启动;本机监听、访问凭据、验证写操作、后台消息监听、自动锁屏以及控制面连接(地址、节点 ID、Token、活动账号和 TLS 文件)均在“服务设置...”的“远程连接”页维护。保存远程连接时可将旧 `remote.json` 配置迁移到托盘管理的 `service.json`;上报白名单仍遵循本地配置和默认关闭边界,不得用命令行绕过该边界。
|
||||
当前版本不提供任何命令行配置入口,也不接受通过 CLI 修改远程凭据或上报白名单。Windows Agent 只通过双击 `WxAgent.Tray.exe` 启动;本机监听、访问凭据、验证写操作、后台消息监听、自动锁屏以及控制面连接(地址、节点 ID、Token、活动账号和 TLS 文件)均在“服务设置...”的“远程连接”页维护。保存远程连接时可将旧 `remote.json` 配置迁移到托盘管理的 `service.json`;上报范围由 Agent 连接授权和已验证账号身份统一确定,不再要求单独填写或确认白名单;不得用命令行伪造节点身份或绕过只读边界。
|
||||
|
||||
### 9.3 诊断与只读确认
|
||||
|
||||
|
||||
@@ -144,21 +144,19 @@ msgs=$(curl -sS -o "$root/msg" -w '%{http_code}' -H "Authorization: Bearer $web_
|
||||
sync=$(curl -sS -o "$root/sync" -w '%{http_code}' -H "Authorization: Bearer $web_token" "http://127.0.0.1:8090/v1/data/accounts/$ACCOUNT_ID/sync-status")
|
||||
reject_payload=$(jq -nc --arg account "$ACCOUNT_ID" '{node_id:"local-node",account_id:$account,batch_id:"revoked-batch-harness",source_generation:$account,stream_key:"messages",sequence:5,cursor_start:"4",cursor_end:"5",payload_hash:"revoked-batch-hash",coverage_state:"complete",conversations:[],messages:[{message_id:"revoked-message-harness",chat_id:"filehelper",chat_type:"Private",source_message_id:"revoked-source-harness",direction:"incoming",message_type:"text",text:"fixture-revoked",source_time:"2026-09-22T03:00:00Z",observed_at:"2026-09-22T03:00:00Z",source_version:"harness",payload_hash:"revoked-message-hash"}]}')
|
||||
batch_rejected=$(curl -sS -o "$root/rejected" -w '%{http_code}' -X POST -H "Authorization: Bearer $NODE_TOKEN" -H 'Content-Type: application/json' --data-binary "$reject_payload" http://127.0.0.1:8090/v1/data/batches)
|
||||
# Start the agent only after revocation. Its reconciliation/flush path must drop the
|
||||
# pre-existing queue and keep it empty instead of collecting or retransmitting.
|
||||
# Reconnect after revocation. The connection itself is the authorization boundary,
|
||||
# so registration restores the verified account without a separate confirmation.
|
||||
revoked_process=$(start_tray)
|
||||
revoked_queue=$(queue_json)
|
||||
sleep 12
|
||||
revoked_queue_after=$(queue_json)
|
||||
echo "{\"event\":\"authorization-revoke\",\"pending_process\":$pending_process,\"pending_queue\":$pending_queue,\"revoke\":$revoke,\"conversations\":$conv,\"messages\":$msgs,\"sync_status\":$sync,\"batch_rejected\":$batch_rejected,\"revoked_process\":$revoked_process,\"revoked_queue\":$revoked_queue,\"revoked_queue_after\":$revoked_queue_after}"
|
||||
run_ps "$stop_ps" >/dev/null 2>&1
|
||||
restore_process=$(start_tray)
|
||||
sleep 5
|
||||
web_token=$(curl -fsS -X POST http://127.0.0.1:8090/v1/auth/login -H 'Content-Type: application/json' -d "{\"username\":\"admin\",\"password\":\"$WEB_PASSWORD\"}" | jq -r .access_token)
|
||||
conv=$(curl -sS -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $web_token" "http://127.0.0.1:8090/v1/data/accounts/$ACCOUNT_ID/conversations")
|
||||
msgs=$(curl -sS -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $web_token" "http://127.0.0.1:8090/v1/data/accounts/$ACCOUNT_ID/messages?chat_id=filehelper&limit=10")
|
||||
restore_queue=$(queue_json)
|
||||
echo "{\"event\":\"authorization-restore\",\"process\":$restore_process,\"conversations\":$conv,\"messages\":$msgs,\"queue\":$restore_queue}"
|
||||
reconnect_conv=$(curl -sS -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $web_token" "http://127.0.0.1:8090/v1/data/accounts/$ACCOUNT_ID/conversations")
|
||||
reconnect_msgs=$(curl -sS -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $web_token" "http://127.0.0.1:8090/v1/data/accounts/$ACCOUNT_ID/messages?chat_id=filehelper&limit=10")
|
||||
reconnect_sync=$(curl -sS -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $web_token" "http://127.0.0.1:8090/v1/data/accounts/$ACCOUNT_ID/sync-status")
|
||||
reconnect_queue=$(queue_json)
|
||||
echo "{\"event\":\"authorization-revoke\",\"pending_process\":$pending_process,\"pending_queue\":$pending_queue,\"revoke\":$revoke,\"conversations\":$conv,\"messages\":$msgs,\"sync_status\":$sync,\"batch_rejected\":$batch_rejected,\"revoked_process\":$revoked_process,\"revoked_queue\":$revoked_queue,\"revoked_queue_after\":$revoked_queue_after,\"reconnect_conversations\":$reconnect_conv,\"reconnect_messages\":$reconnect_msgs,\"reconnect_sync_status\":$reconnect_sync,\"reconnect_queue\":$reconnect_queue}"
|
||||
echo "{\"event\":\"authorization-restore\",\"process\":$revoked_process,\"conversations\":$reconnect_conv,\"messages\":$reconnect_msgs,\"queue\":$reconnect_queue}"
|
||||
|
||||
before_shard=$(shard_json)
|
||||
old_server_pid=$server_pid
|
||||
|
||||
@@ -17,7 +17,7 @@ jq -s -e '
|
||||
($e["offline-queue"].queue.queue_pending > 0 and $e["offline-queue"].queue.queue_bytes > 0 and $e["offline-queue"].process.session == 1) and
|
||||
($e["replay"].queue.queue_pending == 0 and $e["replay"].sync.state == "complete" and $e["replay"].shard.coverage == "complete" and $e["replay"].shard.integrity == "ok") and
|
||||
($e["agent-restart"].process.session == 1 and $e["agent-restart"].before.confirmed_sequence == $e["agent-restart"].after.confirmed_sequence and $e["agent-restart"].after.state == "complete") and
|
||||
($e["authorization-revoke"].pending_process.session == 1 and $e["authorization-revoke"].pending_queue.queue_pending > 0 and $e["authorization-revoke"].pending_queue.queue_bytes > 0 and $e["authorization-revoke"].revoke == 200 and $e["authorization-revoke"].conversations == 403 and $e["authorization-revoke"].messages == 403 and $e["authorization-revoke"].sync_status == 200 and $e["authorization-revoke"].batch_rejected == 403 and $e["authorization-revoke"].revoked_process.session == 1 and $e["authorization-revoke"].revoked_queue.queue_pending == 0 and $e["authorization-revoke"].revoked_queue_after.queue_pending == 0) and
|
||||
($e["authorization-revoke"].pending_process.session == 1 and $e["authorization-revoke"].pending_queue.queue_pending > 0 and $e["authorization-revoke"].pending_queue.queue_bytes > 0 and $e["authorization-revoke"].revoke == 200 and $e["authorization-revoke"].conversations == 403 and $e["authorization-revoke"].messages == 403 and $e["authorization-revoke"].sync_status == 200 and $e["authorization-revoke"].batch_rejected == 403 and $e["authorization-revoke"].revoked_process.session == 1 and $e["authorization-revoke"].reconnect_conversations == 200 and $e["authorization-revoke"].reconnect_messages == 200 and $e["authorization-revoke"].reconnect_sync_status == 200 and $e["authorization-revoke"].reconnect_queue.queue_pending == 0) and
|
||||
($e["authorization-restore"].process.session == 1 and $e["authorization-restore"].conversations == 200 and $e["authorization-restore"].messages == 200 and $e["authorization-restore"].queue.queue_pending == 0) and
|
||||
($e["control-plane-restart"].before.integrity == "ok" and $e["control-plane-restart"].after.integrity == "ok" and $e["control-plane-restart"].before.messages == $e["control-plane-restart"].after.messages and $e["control-plane-restart"].before.batches == $e["control-plane-restart"].after.batches)
|
||||
' "$file" >/dev/null
|
||||
|
||||
Reference in New Issue
Block a user