feat: add remote control plane and whitelist reads
Build web service image / build (push) Successful in 1m53s
Build web service image / build (push) Successful in 1m53s
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
# WxAgent 远程协议草案 v1.0
|
||||
|
||||
> 对应:[`WxAgent-远程多节点控制与白名单数据上报开发计划.md`](./WxAgent-远程多节点控制与白名单数据上报开发计划.md)
|
||||
> 状态:本地闭环已冻结;生产 TLS、节点 Token 签发和数据保留策略需在部署环境配置。
|
||||
|
||||
## 1. 传输和认证
|
||||
|
||||
- 协议版本:`v1`。
|
||||
- 节点只主动向控制面发起 HTTP 请求;生产地址必须使用 HTTPS。HTTP 仅允许 `127.0.0.1`/`::1` 本地集成验证。
|
||||
- 节点请求使用 `Authorization: Bearer <node-token>`。Token 按节点签发、可撤销,不写入日志和控制面持久化文件。
|
||||
- Web 用户先调用 `POST /v1/auth/login`,使用返回的短期 Bearer 会话访问管理 API。未认证的业务请求返回 `401`。
|
||||
- 每个请求可携带 `X-Correlation-Id`;服务端响应同名响应头和 JSON `correlation_id`。
|
||||
- 节点 Token 只能访问对应 `node_id` 的注册、心跳、任务和事件接口;Web 会话不能调用节点接口。
|
||||
|
||||
## 2. 身份与授权
|
||||
|
||||
### 2.1 节点和账号
|
||||
|
||||
`node_id` 是节点 Token 的固定绑定,不接受请求体覆盖。任务始终绑定 `node_id + account_id`。节点只把本地显式配置的、可验证的账号放入注册摘要;不按昵称、PID 或窗口句柄推断账号身份。
|
||||
|
||||
### 2.2 会话白名单
|
||||
|
||||
本地白名单是唯一数据上报授权来源。有效绑定必须同时满足:
|
||||
|
||||
1. `account_id` 与当前活动账号一致;
|
||||
2. `chat_id` 是本地重新确认的稳定标识;
|
||||
3. `chat_type` 与绑定类型一致;
|
||||
4. 会话绑定 `enabled=true` 且 `identityVerified=true`;
|
||||
5. 全局和账号开关均为 `true`。
|
||||
|
||||
显示名称只能作为辅助信息,不进入授权判定。名称相同、候选不唯一、账号或稳定标识无法确认时,返回 `ChatIdentityUnconfirmed` 并丢弃会话内容。
|
||||
|
||||
v1 只允许上报白名单会话的 `message` 事件。普通任务结果、错误和诊断只允许不含会话标识、名称、正文、附件的控制元数据;`read-sessions`、`read-contacts` 和 `read-messages` 是例外,节点只可在本地白名单授权后返回读取内容,并在结果重试时保留原授权作用域。附件、语音和完整数据库均不在协议内。
|
||||
|
||||
## 3. 节点接口
|
||||
|
||||
### 3.1 注册
|
||||
|
||||
`POST /v1/nodes/register`
|
||||
|
||||
请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"node_id": "node-a",
|
||||
"agent_version": "1.0.0",
|
||||
"protocol_version": "v1",
|
||||
"capabilities": ["heartbeat", "poll-tasks", "send-text", "report-message"],
|
||||
"reporting_config_version": 3,
|
||||
"accounts": [
|
||||
{"account_id": "account-a", "active": true, "verified": true,
|
||||
"allowed_group_count": 1, "allowed_private_count": 0}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
控制面先认证 Token,再校验请求 `node_id` 与 Token 绑定一致,持久化节点后返回:
|
||||
|
||||
```json
|
||||
{"node_id":"node-a","status":"Online","authenticated":true,
|
||||
"correlation_id":"..."}
|
||||
```
|
||||
|
||||
### 3.2 心跳
|
||||
|
||||
`POST /v1/nodes/{node_id}/heartbeat`
|
||||
|
||||
心跳至少带协议/Agent 版本、节点状态、微信运行/登录状态、当前账号、队列长度、白名单配置版本和 Correlation ID。超时只将中心状态标记为 `Offline`,不删除任务和上报数据。
|
||||
|
||||
### 3.3 任务轮询和租约
|
||||
|
||||
`GET /v1/nodes/{node_id}/tasks?account_id={account_id}&wait_seconds=5`
|
||||
|
||||
`wait_seconds` 为可选的 0–30 秒 HTTPS 长轮询窗口;窗口到期或有任务时返回,不改变至少一次投递语义。控制面分配尚未接收的 `Pending` 任务并递增 `lease_generation`。轮询本身不代表节点已持久化接收;节点必须先落盘再调用确认接口:
|
||||
|
||||
- `POST /v1/nodes/{node_id}/tasks/{task_id}/ack`
|
||||
- `POST /v1/nodes/{node_id}/tasks/{task_id}/start`
|
||||
- `POST /v1/nodes/{node_id}/tasks/{task_id}/renew`
|
||||
- `POST /v1/nodes/{node_id}/tasks/{task_id}/result`
|
||||
|
||||
确认和开始请求都携带 `task_id`、`account_id`、`lease_generation`。租约过期、账号不匹配或旧代次请求返回冲突;旧代次结果不能覆盖终态。
|
||||
|
||||
v1 写命令白名单只有 `send-text`,载荷必须是:
|
||||
|
||||
```json
|
||||
{"target_id":"stable-session-id","text":"...","confirmed":true}
|
||||
```
|
||||
|
||||
不接受 Shell、PowerShell、文件系统路径或任意代码。节点必须在每个写步骤前重新检查账号、目标会话和桌面上下文;未能确认时停止,不抢焦点、不覆盖草稿。
|
||||
|
||||
### 3.4 只读任务
|
||||
|
||||
Web 可提交以下只读任务,仍沿用任务队列、租约、幂等和结果回传:
|
||||
|
||||
- `POST /v1/reads/sessions`:读取当前可见会话;节点只返回本地白名单中 `identityVerified=true` 的会话。
|
||||
- `POST /v1/reads/contacts`:读取联系人或群;节点按联系人稳定 ID 过滤,`groups_only` 必填。
|
||||
- `POST /v1/reads/messages`:读取一个已授权稳定 `chat_id` 的当前 UI 可见消息历史;不提供数据库全量历史。
|
||||
|
||||
三类请求的 `limit` 均为 1–200,默认 50,`offset` 不得为负。消息任务必须提供精确 `chat_id` 和可选 `include_content`;身份不唯一、未授权或当前不可见时拒绝,不通过名称回退。只读结果可以携带 `content`,但仍受 512 KiB 任务结果上限和中心终态/租约校验约束。
|
||||
|
||||
### 3.5 取消和状态
|
||||
|
||||
Web 调用 `POST /v1/tasks/{task_id}/cancel` 只设置一次 `cancel_requested_at`。任务主状态不因此伪造为已取消:
|
||||
|
||||
```text
|
||||
Pending → Accepted → Running → Succeeded | Failed | ResultUnconfirmed
|
||||
Pending/Accepted(确认未开始且无副作用)→ Cancelled/Expired
|
||||
```
|
||||
|
||||
`ResultUnconfirmed` 表示无法证明旧执行者停止或业务副作用是否发生,禁止自动重放。重复取消和重复结果回传幂等;终态不可被回退或覆盖。
|
||||
|
||||
## 4. 白名单事件
|
||||
|
||||
`POST /v1/nodes/{node_id}/events`
|
||||
|
||||
节点在序列化、入队、实际发送和每次重试前都调用同一授权判定。未授权事件不得调用该接口。服务端仍要求 `authorized=true`、正的配置/授权版本、稳定 `chat_id` 和正的 `event_seq`。
|
||||
|
||||
```json
|
||||
{
|
||||
"node_id":"node-a",
|
||||
"account_id":"account-a",
|
||||
"chat_id":"stable-group-id",
|
||||
"chat_type":"Group",
|
||||
"event_seq":12,
|
||||
"event_type":"message",
|
||||
"occurred_at":"2026-09-11T09:00:00Z",
|
||||
"content":"仅允许会话内容",
|
||||
"config_version":3,
|
||||
"authorization_version":3,
|
||||
"correlation_id":"...",
|
||||
"authorized":true
|
||||
}
|
||||
```
|
||||
|
||||
中心幂等键为:
|
||||
|
||||
```text
|
||||
node_id + account_id + chat_id + event_seq
|
||||
```
|
||||
|
||||
相同键和相同不可变内容返回 `duplicate=true`;相同键内容不同返回冲突,不覆盖已有事件。不同账号即使使用相同 `chat_id` 和 `event_seq` 也分别保存。撤销产生的序号空洞不补洞、不当作已接收。
|
||||
|
||||
## 5. Web 管理接口
|
||||
|
||||
- `POST /v1/auth/login`:用户名密码换取会话。
|
||||
- `GET /v1/nodes`:认证后查看节点和脱敏白名单摘要。
|
||||
- `POST /v1/tasks`:认证后创建白名单命令任务。
|
||||
- `POST /v1/reads/sessions`、`POST /v1/reads/contacts`、`POST /v1/reads/messages`:认证后创建只读任务;结果通过任务查询取得,并受节点本地白名单过滤。
|
||||
- `GET /v1/tasks`、`GET /v1/tasks/{task_id}`:认证后查看任务、控制元数据和已授权只读结果。
|
||||
- `POST /v1/tasks/{task_id}/cancel`:认证后设置取消意图。
|
||||
- `GET /v1/events`:认证后按节点/账号/会话查询中心已经持久化的白名单事件。
|
||||
- `GET /v1/audit`:认证后查询操作审计。
|
||||
- `GET /`:登录入口和只读仪表盘;页面不直接提供未认证业务数据。
|
||||
|
||||
Web 不能写入或扩大节点白名单,不能查询节点未授权会话内容,不能触发隐式重试。
|
||||
|
||||
## 6. 默认拒绝和错误
|
||||
|
||||
| 错误码 | 含义 |
|
||||
| --- | --- |
|
||||
| `Unauthorized` | 节点 Token 或 Web 会话缺失/无效 |
|
||||
| `NodeIdentityMismatch` | Token 与路径/请求节点不一致 |
|
||||
| `ChatNotAuthorized` | 会话不在本地白名单 |
|
||||
| `ChatIdentityUnconfirmed` | 稳定身份未确认 |
|
||||
| `ReportingNotAuthorized` | 本地授权边界拒绝上报 |
|
||||
| `LeaseMismatch` | 任务租约或执行代次已失效 |
|
||||
| `IdempotencyConflict` | 同一幂等键绑定了不同任务载荷 |
|
||||
| `EventIdempotencyConflict` | 同一事件键绑定了不同内容 |
|
||||
| `ResultUnconfirmed` | 不确定的 UI/业务副作用,不得自动重放 |
|
||||
| `ContentNotAllowed` | 非读取任务结果包含会话内容 |
|
||||
|
||||
错误响应不包含 Token、消息正文、联系人名称、附件和未授权 UI 树;只返回固定错误码、通用消息和 Correlation ID。
|
||||
@@ -552,3 +552,13 @@ Agent 只有在同时配置并成功验证以下两项后,才允许连接中
|
||||
11. 验证远程 Web 未认证访问拒绝、取消/不确定状态展示及执行期间人工干预后的安全停止。
|
||||
|
||||
第一阶段不修改基础计划的 M0–M6 顺序;远程能力从 R0 开始,在基础稳定性验收后推进。
|
||||
|
||||
## 12. 当前实现与验收记录(2026-09-12)
|
||||
|
||||
- 已实现 R0–R4 的最小闭环:Go 控制面、React/Vite 管理端并入 Go 二进制、节点 Bearer 认证、注册/心跳、任务租约/取消/幂等/不确定结果、账号显式上下文、白名单队列/撤销复核、审计和 CLI 配置。
|
||||
- 已实现多节点配置入口:控制面支持 `WXAGENT_NODE_TOKENS` JSON map;单节点环境变量仍兼容。节点 `service.json` 可通过 `remoteConfigurationFile` 使用 CLI 维护的远程配置。
|
||||
- 已实现消息事件的稳定会话标识要求:监听器只能在唯一 AutomationId 可确认时生成远程事件;本地事件总线不缓存消息正文,非白名单正文不入队、不持久化、不发送。
|
||||
- 已补充远程只读查询任务:`POST /v1/reads/sessions`、`POST /v1/reads/contacts`、`POST /v1/reads/messages`;结果沿用任务租约/状态/幂等模型,节点读取前按本地已验证白名单过滤,消息查询仅返回当前 UI 可见历史。
|
||||
- 自动化证据:`npm run build --prefix control-plane/web`;`cd control-plane && go test ./... && go vet ./...`;Core 测试 166/166、Service 测试 20/20;`dotnet build WxAgent.sln -c Release -p:EnableWindowsTargeting=true`;`scripts/remote-control-smoke.sh` 均通过。
|
||||
- 真机 `windows-direct-20260911` 已在已登录、未锁定会话中注册并保持 `Online`;`send-text` 任务已完成 `Pending → Running → Succeeded`,并通过文件传输助手本地历史读回唯一标记确认。远程读取已验证会话 1 条、私聊联系人 1 条、群 1 条、消息 3 条,四个读取任务均为 `Succeeded`。
|
||||
- 当前不能标记为生产 R5 完成:控制面仍是单进程 JSON 文件/HTTP MVP,生产 HTTPS/mTLS、Token/证书撤销与轮换、外部密钥管理、规模压测、备份恢复和完整保留策略仍待完成。`/api/v1/sessions/open` 的导航能力仍可能返回 `CapabilityDisabled`;远程消息历史不是数据库全量历史。详细部署步骤见 [`WxAgent-远程控制面架构与部署说明-v1.0.md`](./WxAgent-远程控制面架构与部署说明-v1.0.md)。
|
||||
|
||||
@@ -0,0 +1,770 @@
|
||||
# WxAgent 远程控制面架构与部署说明 v1.0
|
||||
|
||||
> 日期:2026-09-12
|
||||
> 状态:当前实现与真机验证基线;不是生产安全认证或规模验收报告。
|
||||
> 关联协议:[`WxAgent-远程协议草案-v1.0.md`](./WxAgent-远程协议草案-v1.0.md)
|
||||
> 关联计划:[`WxAgent-远程多节点控制与白名单数据上报开发计划.md`](./WxAgent-远程多节点控制与白名单数据上报开发计划.md)
|
||||
|
||||
本文说明当前仓库中 **Go 控制面、React 管理端、Windows Desktop Agent、节点本地 CLI** 的实际架构、数据边界、部署步骤、验证方法和已知限制。命令示例中的 Token、密码、账号标识和路径均使用占位符;不要把真实凭据写入仓库、命令历史或日志。
|
||||
|
||||
## 1. 适用范围与结论
|
||||
|
||||
当前闭环已经支持:
|
||||
|
||||
- 多节点注册、Bearer Token 认证、心跳和在线状态;
|
||||
- 控制面持久化任务队列、任务租约、幂等键、取消请求、结果和审计;
|
||||
- `send-text` 远程文本发送;
|
||||
- 远程读取可见会话、联系人/群和当前 UI 可见消息历史;
|
||||
- 节点本地白名单过滤、稳定身份校验、结果范围化保存和断线补传;
|
||||
- React 管理页面登录、节点/任务/事件/审计查询;
|
||||
- Gitea Actions 构建并推送控制面容器镜像。
|
||||
|
||||
当前明确不提供:
|
||||
|
||||
- 任意 Shell、PowerShell、文件系统或代码执行;
|
||||
- 控制面直接修改节点白名单;
|
||||
- 非白名单会话、完整微信数据库或数据库密钥上传;
|
||||
- 远程全量数据库历史读取;消息读取只来自当前微信 UI 可见历史;
|
||||
- 生产级 HTTPS/mTLS 终止、在线 Token 撤销/轮换、HA 和规模压测;
|
||||
- `/api/v1/sessions/open` 的远程导航能力。当前该接口可能返回 `409 CapabilityDisabled`,不影响只读任务接口。
|
||||
|
||||
## 2. 总体架构
|
||||
|
||||
```text
|
||||
┌──────────────────────────┐
|
||||
│ 浏览器 / 远程 Web 管理端 │
|
||||
│ 登录、节点、任务、审计 │
|
||||
└────────────┬─────────────┘
|
||||
│ HTTPS(内网验证可显式使用私有 HTTP)
|
||||
┌────────────▼─────────────┐
|
||||
│ Go Control Plane │
|
||||
│ API / Web / 认证 / 审计 │
|
||||
│ 任务队列 / 租约 / JSON存储 │
|
||||
└────────────┬─────────────┘
|
||||
│ 节点主动出站 HTTP/HTTPS
|
||||
│ 当前实现:心跳 + 最长30秒任务长轮询
|
||||
┌────────────▼─────────────┐
|
||||
│ Windows Desktop Agent │
|
||||
│ 注册、心跳、轮询、结果上报 │
|
||||
│ 本地任务账本 / 事件队列 │
|
||||
└────────────┬─────────────┘
|
||||
│ 同一微信窗口进入单一命令队列
|
||||
┌────────────▼─────────────┐
|
||||
│ FlaUI.UIA3 / Win32 │
|
||||
│ 已登录、未锁定的交互桌面 │
|
||||
└────────────┬─────────────┘
|
||||
│
|
||||
Weixin.exe
|
||||
|
||||
本机人工入口:WxAgent.Host CLI
|
||||
本机常驻入口:WxAgent.Tray → WxAgent.Service(127.0.0.1:5088)
|
||||
```
|
||||
|
||||
### 2.1 组件与代码边界
|
||||
|
||||
| 组件 | 目录 | 技术 | 职责 |
|
||||
| --- | --- | --- | --- |
|
||||
| Core | `node-agent/WxAgent.Core` | .NET 8 | 协议模型、白名单、账号上下文、任务账本、事件队列和纯逻辑规则 |
|
||||
| Windows | `node-agent/WxAgent.Windows` | .NET 8 Windows | FlaUI/UIA、Win32、微信会话/消息读取和写操作 |
|
||||
| Service | `node-agent/WxAgent.Service` | .NET 8 Windows | 本机 HTTP API、权限、操作队列、远程 Agent Hosted Service |
|
||||
| Host | `node-agent/WxAgent.Host` | .NET 8 Windows | CLI、`serve` 模式和诊断入口 |
|
||||
| Tray | `node-agent/WxAgent.Tray` | .NET 8 Windows | 交互式用户会话中的常驻托盘、服务生命周期和配置重载 |
|
||||
| Control Plane | `control-plane` | Go 1.23 | 节点 API、Web API、任务/事件/审计持久化和静态页面服务 |
|
||||
| Web | `control-plane/web` | React/Vite | 控制面管理页面,构建后嵌入 Go 二进制 |
|
||||
|
||||
当前控制面是单进程 MVP:Go 二进制内嵌 React 静态资源,数据写入一个 JSON 文件。没有独立数据库、消息队列、节点入站端口或本地 Web/MCP 暴露到远程网络。
|
||||
|
||||
### 2.2 当前传输模型与计划模型的区别
|
||||
|
||||
开发计划保留了未来 mTLS WebSocket/等价长连接的架构方向;**当前代码实际使用 HTTP/HTTPS 请求**:
|
||||
|
||||
1. 节点用 Bearer Token 调用注册接口;
|
||||
2. 节点周期性发送心跳,默认间隔 10 秒;
|
||||
3. 节点按账号轮询任务,服务端支持 `wait_seconds=0..30` 的长轮询;
|
||||
4. 节点确认、开始、续租并回传最终结果;
|
||||
5. 节点重启后从本地账本补传已完成但未确认的结果。
|
||||
|
||||
因此,部署当前版本不需要给 Windows 节点开放公网入站端口,也不需要部署 RabbitMQ。生产切换到 HTTPS/mTLS 或长连接前,应先完成独立协议、证书生命周期和回归验收。
|
||||
|
||||
## 3. 请求与任务流
|
||||
|
||||
### 3.1 远程写任务
|
||||
|
||||
```text
|
||||
Web 用户登录
|
||||
→ POST /v1/tasks
|
||||
→ 控制面原子写入 Pending
|
||||
→ 节点轮询取得租约 lease_generation
|
||||
→ 节点写入 remote-task-ledger.json
|
||||
→ POST ack,中心变为 Accepted
|
||||
→ 节点写入 Running,再 POST start
|
||||
→ 复核活动账号、UI 状态和目标会话
|
||||
→ 执行单窗口 UIA 写操作
|
||||
→ 节点先保存最终结果,再 POST result
|
||||
→ Web GET /v1/tasks/{task_id}
|
||||
```
|
||||
|
||||
任务绑定 `node_id + account_id + kind + idempotency_key`。同一节点/账号/幂等键使用不同载荷会返回 `IdempotencyConflict`;重复提交相同载荷返回原任务,不创建第二个任务。
|
||||
|
||||
`send-text` 的节点白名单载荷为:
|
||||
|
||||
```json
|
||||
{
|
||||
"target_id": "session_item_文件传输助手",
|
||||
"text": "仅用于已批准测试会话的文本",
|
||||
"confirmed": true
|
||||
}
|
||||
```
|
||||
|
||||
节点不接受 Shell、PowerShell、路径或任意代码。发送消息属于可能产生副作用的写操作,旧执行者是否停止无法证明时进入 `ResultUnconfirmed`,不得自动重放。
|
||||
|
||||
### 3.2 远程只读任务
|
||||
|
||||
远程读取复用同一任务队列,不增加节点入站读取 API:
|
||||
|
||||
| HTTP 接口 | 节点任务 kind | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `POST /v1/reads/sessions` | `read-sessions` | 读取当前可见会话,结果按白名单过滤 |
|
||||
| `POST /v1/reads/contacts` | `read-contacts` | 读取联系人或群,结果按联系人稳定 ID 过滤 |
|
||||
| `POST /v1/reads/messages` | `read-messages` | 读取一个已授权会话的当前 UI 可见历史 |
|
||||
|
||||
公共接口先由控制面校验请求结构并创建任务;节点执行前再次按当前本地配置校验:
|
||||
|
||||
- 全局上报开关必须开启;
|
||||
- 目标账号必须存在且启用;
|
||||
- 会话必须 `enabled=true`、`identityVerified=true`;
|
||||
- 数据类型必须被授权;
|
||||
- 消息任务的 `chat_id` 必须精确匹配唯一授权会话;
|
||||
- 会话读取结果按 `AutomationId` 过滤;联系人读取结果按数据库稳定联系人 ID 过滤。
|
||||
|
||||
分页参数 `limit` 为 1–200,默认 50;`offset` 不得为负。单次任务请求体上限为 64 KiB,最终结果上限为 512 KiB。消息结果不是完整数据库历史,也不承诺跨页面的事务快照。
|
||||
|
||||
### 3.3 任务状态与租约
|
||||
|
||||
```text
|
||||
Pending → Accepted → Running → Succeeded
|
||||
├→ Failed
|
||||
└→ ResultUnconfirmed
|
||||
|
||||
未开始且可证明无副作用:Pending/Accepted → Cancelled 或 Expired
|
||||
```
|
||||
|
||||
当前默认参数:
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
| --- | ---: | --- |
|
||||
| 任务租约 | 30 秒 | 控制面 `LeaseTTL` |
|
||||
| 心跳间隔 | 10 秒 | 节点周期 |
|
||||
| 心跳超时 | 45 秒 | Web 查询节点时标记 `Offline` |
|
||||
| Web 会话 | 8 小时 | 控制面内存会话 |
|
||||
| 任务轮询窗口 | 0–30 秒 | `wait_seconds` |
|
||||
| 事件正文 | 16 KiB | 节点协议上限 |
|
||||
| 任务载荷 | 64 KiB | 节点/中心共同限制 |
|
||||
| 任务结果 | 512 KiB | 含读取内容 |
|
||||
|
||||
取消请求只设置 `cancel_requested_at`,不把任务直接伪造为已取消。终态不能被旧租约或旧结果覆盖;节点重启发现 `Accepted/Running` 无最终结果时,生成 `ResultUnconfirmed`,不重放 UI 操作。
|
||||
|
||||
## 4. 认证、授权与隐私边界
|
||||
|
||||
### 4.1 两类身份
|
||||
|
||||
| 身份 | 认证方式 | 可访问接口 |
|
||||
| --- | --- | --- |
|
||||
| Web 用户 | `POST /v1/auth/login` 换取内存 Bearer 会话 | Web 节点、任务、读取、事件和审计接口 |
|
||||
| Windows 节点 | 节点专用 `Authorization: Bearer <node-token>` | 注册、心跳、任务轮询/租约/结果和事件上报 |
|
||||
|
||||
节点 Token 按 `node_id` 固定绑定。请求路径、注册体和 Token 中的节点 ID 不一致时拒绝。Token 只从启动环境读取,不写入控制面 JSON 数据文件;密码按当前 MVP 配置从环境读取并在内存中比较。
|
||||
|
||||
所有请求生成或传递 `X-Correlation-Id`;错误只返回固定错误码、通用消息和 Correlation ID。控制面响应设置 `Cache-Control: no-store`、`X-Content-Type-Options: nosniff` 和 `Referrer-Policy: no-referrer`。
|
||||
|
||||
### 4.2 本地白名单是唯一数据授权来源
|
||||
|
||||
节点本地 `remote.json` 中的 `reporting` 配置决定可上报范围,远程 Web 不能扩大它。有效授权必须同时满足:
|
||||
|
||||
1. 全局 `enabled=true`;
|
||||
2. 账号存在且 `enabled=true`;
|
||||
3. `account_id` 与已确认的当前账号一致;
|
||||
4. `chatId` 是稳定、唯一、重新确认过的标识;
|
||||
5. 会话类型一致;
|
||||
6. 会话 `enabled=true` 且 `identityVerified=true`;
|
||||
7. 数据类型允许。
|
||||
|
||||
非白名单或身份不确定的数据不得进入中心 API、中心文件、节点待发送队列或普通日志。任务权限不等于数据上报权限;读取任务即使由已认证 Web 用户创建,节点仍必须在读取前拒绝未授权会话。
|
||||
|
||||
任务结果只有读取任务可以携带 `content`。读取结果在节点账本中与 `ReportingScopes` 一起保存,补传时沿用该范围并再次经过当前授权检查;没有授权范围时只保存/回传控制元数据。
|
||||
|
||||
### 4.3 标识与数据范围
|
||||
|
||||
- 会话读取使用微信当前 UI 暴露并确认的稳定 `AutomationId`,当前示例形如 `session_item_*`;不使用昵称、PID、窗口句柄或 UIA RuntimeId 作为持久授权身份。
|
||||
- 联系人读取使用只读联系人数据库提供的稳定联系人 ID;显示名称只用于展示或辅助定位。
|
||||
- 消息读取要求指定一个精确 `chat_id`,且该 ID 在当前可见会话中唯一。
|
||||
- 当前只读消息历史来自微信 UI 可见内容,不是数据库全量扫描,也不通过远程接口暴露数据库密钥或原始数据库。
|
||||
- 默认真机验证只使用“文件传输助手”“Hao 豪”“吉祥三宝”“消息测试专用群组”。未得到明确授权不得操作其他真实联系人或群聊。
|
||||
|
||||
## 5. 控制面接口清单
|
||||
|
||||
### 5.1 健康检查与 Web 接口
|
||||
|
||||
```text
|
||||
GET /healthz
|
||||
POST /v1/auth/login
|
||||
GET /v1/nodes
|
||||
GET /v1/nodes/{node_id}
|
||||
POST /v1/tasks
|
||||
GET /v1/tasks?node_id=&account_id=&limit=
|
||||
GET /v1/tasks/{task_id}
|
||||
POST /v1/tasks/{task_id}/cancel
|
||||
POST /v1/reads/sessions
|
||||
POST /v1/reads/contacts
|
||||
POST /v1/reads/messages
|
||||
GET /v1/events
|
||||
GET /v1/audit
|
||||
GET /
|
||||
```
|
||||
|
||||
`/healthz` 不要求认证,只返回 `status`、协议版本和 Correlation ID。其余业务接口要求 Web Bearer 会话。
|
||||
|
||||
读取请求的公共字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"node_id": "windows-direct-20260911",
|
||||
"account_id": "<verified-account-id>",
|
||||
"idempotency_key": "read-sessions-20260912-001",
|
||||
"limit": 50,
|
||||
"offset": 0,
|
||||
"not_after": "2026-09-12T09:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
各接口追加字段:
|
||||
|
||||
```json
|
||||
// /v1/reads/contacts
|
||||
{"groups_only": false, "contains": "file"}
|
||||
|
||||
// /v1/reads/messages
|
||||
{"chat_id": "session_item_文件传输助手", "include_content": true}
|
||||
```
|
||||
|
||||
提交成功返回 `202 Accepted`:
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "<task-id>",
|
||||
"status": "Pending",
|
||||
"duplicate": false,
|
||||
"state_version": 1
|
||||
}
|
||||
```
|
||||
|
||||
随后使用 `GET /v1/tasks/{task_id}` 读取状态和结果。不要用重复提交代替轮询,也不要把 `Pending` 或 `Running` 当作业务成功。
|
||||
|
||||
### 5.2 节点接口
|
||||
|
||||
```text
|
||||
POST /v1/nodes/register
|
||||
POST /v1/nodes/{node_id}/heartbeat
|
||||
GET /v1/nodes/{node_id}/tasks?account_id=&wait_seconds=0..30
|
||||
POST /v1/nodes/{node_id}/tasks/{task_id}/ack
|
||||
POST /v1/nodes/{node_id}/tasks/{task_id}/start
|
||||
POST /v1/nodes/{node_id}/tasks/{task_id}/renew
|
||||
POST /v1/nodes/{node_id}/tasks/{task_id}/result
|
||||
POST /v1/nodes/{node_id}/events
|
||||
```
|
||||
|
||||
节点注册上报协议版本、Agent 版本、能力、白名单计数和账号摘要;心跳上报微信进程/登录/锁屏状态、活动账号、队列长度和配置版本。节点接口不能由 Web Bearer 会话调用。
|
||||
|
||||
### 5.3 常见错误
|
||||
|
||||
| 错误码 | 处理 |
|
||||
| --- | --- |
|
||||
| `Unauthorized` | 检查 Web 会话或节点 Token |
|
||||
| `NodeIdentityMismatch` | Token、路径和请求体中的节点 ID 不一致 |
|
||||
| `NodeNotReady` / `AccountNotReady` | 等待节点注册并确认账号 |
|
||||
| `ChatNotAuthorized` | 本地白名单未允许 |
|
||||
| `ChatIdentityUnconfirmed` | 稳定身份不唯一或未重新确认 |
|
||||
| `InvalidPagination` | `limit` 不在 1–200 或 `offset` 小于 0 |
|
||||
| `IdempotencyConflict` | 幂等键复用但任务参数不同 |
|
||||
| `LeaseMismatch` | 租约过期或执行代次不是当前值 |
|
||||
| `ContentNotAllowed` | 非读取任务试图回传内容 |
|
||||
| `ResultUnconfirmed` | 无法证明 UI/业务副作用或旧执行者已停止 |
|
||||
| `CapabilityDisabled` | 当前能力显式未启用,例如远程打开会话 |
|
||||
|
||||
## 6. 持久化与备份
|
||||
|
||||
### 6.1 控制面
|
||||
|
||||
容器内默认路径:
|
||||
|
||||
```text
|
||||
/data/control-plane-data.json
|
||||
```
|
||||
|
||||
`Store` 在单进程 Mutex 下执行读写,写入临时文件、`Sync` 后原子替换,文件权限为 `0600`。持久化对象包括:
|
||||
|
||||
```text
|
||||
PersistedState
|
||||
├── nodes 节点注册、状态、账号摘要、心跳
|
||||
├── tasks 任务载荷、租约、状态版本、结果
|
||||
├── events 已接收白名单事件
|
||||
└── audit 登录、任务和节点操作审计
|
||||
```
|
||||
|
||||
当前没有数据库事务、跨实例锁、自动 TTL、异地备份或 HA。升级/重建容器前必须备份 `/data/control-plane-data.json`,恢复时保持文件权限并确保只有一个控制面实例挂载该数据文件。
|
||||
|
||||
### 6.2 Windows 节点
|
||||
|
||||
建议部署目录:
|
||||
|
||||
```text
|
||||
C:\Users\Rogee\wx-agent\
|
||||
├── WxAgent.Host.exe
|
||||
├── WxAgent.Tray.exe
|
||||
├── service.json
|
||||
├── credentials.json
|
||||
├── remote.json
|
||||
├── data\remote-task-ledger.json
|
||||
├── data\remote-event-queue.json
|
||||
└── wxagent.log
|
||||
```
|
||||
|
||||
`remote.json` 由 CLI 原子保存;远程任务账本记录任务指纹、最终结果、ReportingScopes 和是否已上报。节点重启时不重放未完成 UI 任务。Token、凭据文件和含读取结果的账本必须使用 Windows 用户 ACL 保护,不应复制到工单、日志或仓库。
|
||||
|
||||
## 7. Gitea Actions 构建与发布
|
||||
|
||||
### 7.1 前置条件
|
||||
|
||||
仓库:`https://git.ipao.vip/rogee/wx-win-agent.git`
|
||||
工作流:`.gitea/workflows/build-web-image.yml`
|
||||
镜像构建文件:`control-plane/Dockerfile`
|
||||
|
||||
在 Gitea 账号级 Actions Secrets 配置:
|
||||
|
||||
```text
|
||||
REGISTRY_TOKEN=<可推送 git.ipao.vip 容器仓库的 Token>
|
||||
```
|
||||
|
||||
不要把 Token 放入 workflow、仓库变量、Dockerfile 或镜像层。工作流使用仓库所有者作为 Registry 用户名。
|
||||
|
||||
### 7.2 触发与标签
|
||||
|
||||
工作流在以下情况运行:
|
||||
|
||||
- `main` 分支 push;
|
||||
- `v*` Git 标签 push;
|
||||
- Gitea Actions 手动 `workflow_dispatch`。
|
||||
|
||||
镜像名为:
|
||||
|
||||
```text
|
||||
git.ipao.vip/rogee/wx-win-agent
|
||||
```
|
||||
|
||||
每次构建推送:
|
||||
|
||||
```text
|
||||
sha-<提交 SHA 前12位>
|
||||
```
|
||||
|
||||
`main` 额外推送 `latest`;`v1.0.0` 额外推送 `v1.0.0`。部署环境应优先固定 SHA 或版本标签,只有开发环境使用 `latest`。
|
||||
|
||||
### 7.3 发布前本地检查
|
||||
|
||||
```bash
|
||||
export PATH="$HOME/.dotnet:$PATH"
|
||||
|
||||
dotnet restore WxAgent.sln -p:EnableWindowsTargeting=true
|
||||
dotnet test tests/node-agent/WxAgent.Core.Tests -c Release
|
||||
dotnet test tests/node-agent/WxAgent.Service.Tests -c Release
|
||||
dotnet build WxAgent.sln -c Release -p:EnableWindowsTargeting=true
|
||||
|
||||
(cd control-plane && go test ./... && go vet ./...)
|
||||
npm --prefix control-plane/web ci --no-audit --no-fund
|
||||
npm --prefix control-plane/web run build
|
||||
git diff --check
|
||||
```
|
||||
|
||||
远程协议闭环检查:
|
||||
|
||||
```bash
|
||||
./scripts/remote-control-smoke.sh
|
||||
```
|
||||
|
||||
该脚本只使用测试节点/临时控制面,不操作真实微信联系人。
|
||||
|
||||
### 7.4 推送提交
|
||||
|
||||
确认工作树中没有凭据、临时配置和真机产物后:
|
||||
|
||||
```bash
|
||||
git status --short --untracked-files=all
|
||||
git diff --check
|
||||
git add .
|
||||
git diff --cached --stat
|
||||
git commit -m "feat: document and publish remote control plane"
|
||||
git push --set-upstream origin main
|
||||
```
|
||||
|
||||
推送后在 Gitea Actions 查看构建日志,并核对 SHA 镜像是否存在。不要在未经检查的情况下使用 `git add -f` 把被忽略的 Token、日志、`artifacts` 或用户配置加入提交。
|
||||
|
||||
## 8. 控制面部署
|
||||
|
||||
### 8.1 构建本地镜像
|
||||
|
||||
```bash
|
||||
docker build \
|
||||
--file control-plane/Dockerfile \
|
||||
--tag wxagent-control-plane:local \
|
||||
control-plane
|
||||
```
|
||||
|
||||
Dockerfile 分三阶段:Node 构建 React、Go 静态编译、distroless non-root 运行。容器监听 `8090`,数据卷为 `/data`,不需要在运行镜像中安装 Node 或 Go。
|
||||
|
||||
### 8.2 单机/内网验证部署
|
||||
|
||||
以下示例将控制面绑定到内网主机的 `10.1.1.104:18090`。真实 Token 和密码通过安全注入替换,不写入 shell 历史:
|
||||
|
||||
```bash
|
||||
export NODE_TOKEN='<node-token>'
|
||||
export WEB_PASSWORD='<web-password>'
|
||||
|
||||
mkdir -p /srv/wxagent/control-plane-data
|
||||
|
||||
docker rm -f wxagent-control-plane-direct 2>/dev/null || true
|
||||
docker run -d \
|
||||
--name wxagent-control-plane-direct \
|
||||
--restart unless-stopped \
|
||||
-p 10.1.1.104:18090:8090 \
|
||||
-v /srv/wxagent/control-plane-data:/data \
|
||||
-e WXAGENT_CONTROL_PLANE_ADDR=0.0.0.0:8090 \
|
||||
-e WXAGENT_CONTROL_PLANE_DATA=/data/control-plane-data.json \
|
||||
-e WXAGENT_NODE_TOKENS='{"windows-direct-20260911":"'"$NODE_TOKEN"'"}' \
|
||||
-e WXAGENT_WEB_USERS='{"admin":"'"$WEB_PASSWORD"'"}' \
|
||||
git.ipao.vip/rogee/wx-win-agent:sha-<12位SHA>
|
||||
|
||||
curl --fail http://10.1.1.104:18090/healthz
|
||||
```
|
||||
|
||||
更推荐使用 Compose、systemd 或编排平台的 Secret 注入功能,避免 Token 出现在 `docker inspect` 的长期配置、进程列表和 shell 历史中。当前镜像支持环境变量认证,但尚未内置外部 Secret Provider。
|
||||
|
||||
单节点兼容环境变量也可用:
|
||||
|
||||
```text
|
||||
WXAGENT_NODE_ID
|
||||
WXAGENT_NODE_TOKEN
|
||||
WXAGENT_WEB_USER
|
||||
WXAGENT_WEB_PASSWORD
|
||||
```
|
||||
|
||||
多节点/多 Web 用户使用 JSON map:
|
||||
|
||||
```text
|
||||
WXAGENT_NODE_TOKENS={"node-a":"token-a","node-b":"token-b"}
|
||||
WXAGENT_WEB_USERS={"admin":"password-a","auditor":"password-b"}
|
||||
```
|
||||
|
||||
### 8.3 生产 HTTPS/mTLS 门禁
|
||||
|
||||
当前 Go 进程直接提供 HTTP。生产不得把该 HTTP 端口直接暴露到公网;正式部署至少应:
|
||||
|
||||
1. 在受控入口终止 HTTPS,并配置可信证书、强 TLS 和安全 Header;
|
||||
2. 节点到入口使用 HTTPS;
|
||||
3. 按节点管理、轮换和撤销 Token,或在边界与节点接入层启用 mTLS;
|
||||
4. Token/密码放入外部密钥管理,不进入容器环境快照和日志;
|
||||
5. 仅允许管理端、节点网段访问对应路径;
|
||||
6. 对 `/data` 做加密备份、恢复演练和保留策略;
|
||||
7. 完成失效证书、重放、中心重启、节点断线和并发规模测试。
|
||||
|
||||
`allowInsecureHttp` 只用于显式允许的私有 IP 内网验证;公网 HTTP 永远拒绝。它不是生产加密替代方案。
|
||||
|
||||
## 9. Windows 节点部署
|
||||
|
||||
### 9.1 构建 self-contained 发布包
|
||||
|
||||
Linux 构建 Windows 节点:
|
||||
|
||||
```bash
|
||||
dotnet restore WxAgent.sln -p:EnableWindowsTargeting=true
|
||||
dotnet publish node-agent/WxAgent.Host \
|
||||
-c Release \
|
||||
-r win-x64 \
|
||||
--self-contained true \
|
||||
-p:EnableWindowsTargeting=true \
|
||||
-p:PublishSingleFile=true \
|
||||
-p:IncludeNativeLibrariesForSelfExtract=true \
|
||||
-p:PublishTrimmed=false
|
||||
|
||||
dotnet publish node-agent/WxAgent.Tray \
|
||||
-c Release \
|
||||
-r win-x64 \
|
||||
--self-contained true \
|
||||
-p:EnableWindowsTargeting=true \
|
||||
-p:PublishSingleFile=true \
|
||||
-p:IncludeNativeLibrariesForSelfExtract=true \
|
||||
-p:PublishTrimmed=false
|
||||
```
|
||||
|
||||
上传到已登录 Windows 用户目录(部署时可用 SSH/SCP,UI 操作不能在 Session 0 验证):
|
||||
|
||||
```bash
|
||||
scp -r node-agent/WxAgent.Host/bin/Release/net8.0-windows10.0.19041.0/win-x64/publish/* \
|
||||
rogee@10.1.1.101:'C:/Users/Rogee/wx-agent/'
|
||||
scp -r node-agent/WxAgent.Tray/bin/Release/net8.0-windows10.0.19041.0/win-x64/publish/* \
|
||||
rogee@10.1.1.101:'C:/Users/Rogee/wx-agent/'
|
||||
```
|
||||
|
||||
升级前先停止托盘进程或交互式启动任务,避免覆盖被占用的单文件发布包;复制完成后校验 SHA-256,再启动 `WxAgent.Tray.exe`。
|
||||
|
||||
### 9.2 本机配置文件
|
||||
|
||||
`service.json` 至少包含本机监听、凭据和数据目录;远程配置建议单独保存:
|
||||
|
||||
```json
|
||||
{
|
||||
"listenUrl": "http://127.0.0.1:5088",
|
||||
"allowExternal": false,
|
||||
"credentialFile": "C:\\Users\\Rogee\\wx-agent\\credentials.json",
|
||||
"dataDirectory": "C:\\Users\\Rogee\\wx-agent\\data",
|
||||
"remoteConfigurationFile": "C:\\Users\\Rogee\\wx-agent\\remote.json"
|
||||
}
|
||||
```
|
||||
|
||||
本机服务默认只监听 `127.0.0.1:5088`。除非有明确的隔离网络和访问控制,不要启用外部监听;该本机 API 不是控制面节点接入端口。
|
||||
|
||||
`remote.json` 结构如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"remote": {
|
||||
"authAddress": "https://control.example.com",
|
||||
"token": "<node-token>",
|
||||
"nodeId": "windows-node-a",
|
||||
"activeAccountId": "<verified-account-id>",
|
||||
"allowInsecureHttp": false
|
||||
},
|
||||
"reporting": {
|
||||
"enabled": true,
|
||||
"configVersion": 1,
|
||||
"accounts": [
|
||||
{
|
||||
"accountId": "<verified-account-id>",
|
||||
"enabled": true,
|
||||
"allowedChats": [
|
||||
{
|
||||
"type": "Private",
|
||||
"chatId": "<verified-private-id>",
|
||||
"enabled": true,
|
||||
"identityVerified": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
可以用 CLI 原子维护,不要手工编辑已在运行中的 Token:
|
||||
|
||||
```powershell
|
||||
$cfg = 'C:\Users\Rogee\wx-agent\remote.json'
|
||||
$env:WXAGENT_NODE_TOKEN = '<node-token>'
|
||||
|
||||
WxAgent.Host.exe remote auth set `
|
||||
--config $cfg `
|
||||
--address https://control.example.com `
|
||||
--token $env:WXAGENT_NODE_TOKEN `
|
||||
--node windows-node-a `
|
||||
--active-account <verified-account-id>
|
||||
|
||||
WxAgent.Host.exe remote reporting enable --config $cfg
|
||||
WxAgent.Host.exe remote reporting account-add --config $cfg --account <verified-account-id>
|
||||
WxAgent.Host.exe remote reporting account-enable --config $cfg --account <verified-account-id>
|
||||
```
|
||||
|
||||
内网 HTTP 验证必须显式 opt-in,并且地址必须是私有 IP:
|
||||
|
||||
```powershell
|
||||
WxAgent.Host.exe remote auth set `
|
||||
--config $cfg `
|
||||
--address http://10.1.1.104:18090 `
|
||||
--token $env:WXAGENT_NODE_TOKEN `
|
||||
--node windows-direct-20260911 `
|
||||
--active-account <verified-account-id> `
|
||||
--allow-insecure-http
|
||||
```
|
||||
|
||||
### 9.3 确认并允许会话
|
||||
|
||||
在微信已登录、桌面未锁定的交互式会话中获取并确认稳定 ID。名称仅用于人工确认,不作为授权键:
|
||||
|
||||
```powershell
|
||||
WxAgent.Host.exe doctor
|
||||
WxAgent.Host.exe inspect-ui --output artifacts\ui-tree.json
|
||||
WxAgent.Host.exe session list
|
||||
WxAgent.Host.exe remote reporting show --config $cfg
|
||||
```
|
||||
|
||||
确认后只把批准的测试会话加入白名单:
|
||||
|
||||
```powershell
|
||||
WxAgent.Host.exe remote reporting allow `
|
||||
--config $cfg `
|
||||
--account <verified-account-id> `
|
||||
--type private `
|
||||
--chat-id <verified-private-id> `
|
||||
--identity-verified
|
||||
|
||||
WxAgent.Host.exe remote reporting allow `
|
||||
--config $cfg `
|
||||
--account <verified-account-id> `
|
||||
--type group `
|
||||
--chat-id <verified-group-id> `
|
||||
--identity-verified
|
||||
|
||||
WxAgent.Host.exe remote probe run --config $cfg
|
||||
```
|
||||
|
||||
修改远程地址、Token 或节点 ID 后必须重启 Agent;修改 reporting 配置会在下一轮远程循环重新读取。可以从托盘菜单选择“重新加载配置”,或重启 `WxAgent.Tray.exe`。
|
||||
|
||||
### 9.4 交互式启动要求
|
||||
|
||||
UIA Agent 必须运行在微信所在的已登录、未锁定 Windows 用户会话中。建议使用该用户的登录启动任务运行 `WxAgent.Tray.exe --config ...`,而不是 Windows Service 或 Session 0。`--prevent-auto-lock` 会修改 Windows 电源/锁屏策略,只有明确批准时使用:
|
||||
|
||||
```powershell
|
||||
WxAgent.Tray.exe --config C:\Users\Rogee\wx-agent\service.json
|
||||
```
|
||||
|
||||
部署完成后至少执行:
|
||||
|
||||
```powershell
|
||||
WxAgent.Host.exe doctor
|
||||
WxAgent.Host.exe inspect-ui --output artifacts\ui-tree.json
|
||||
WxAgent.Host.exe smoke
|
||||
```
|
||||
|
||||
`smoke` 会向默认测试会话发送唯一标记,是写操作;生产或真实联系人验证前必须明确确认目标。
|
||||
|
||||
## 10. 远程验证操作手册
|
||||
|
||||
### 10.1 控制面登录
|
||||
|
||||
```bash
|
||||
curl --fail http://10.1.1.104:18090/healthz
|
||||
|
||||
curl --fail -c /tmp/wxagent.cookies \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"username":"admin","password":"<web-password>"}' \
|
||||
http://10.1.1.104:18090/v1/auth/login
|
||||
|
||||
curl --fail -b /tmp/wxagent.cookies \
|
||||
http://10.1.1.104:18090/v1/nodes
|
||||
```
|
||||
|
||||
实际客户端也可把登录响应中的 `access_token` 作为 `Authorization: Bearer` 使用。不要把响应 Token 写进共享日志。
|
||||
|
||||
### 10.2 读取会话、联系人和消息
|
||||
|
||||
以下示例使用 Web Bearer Token 占位符;每个读取调用应使用新的、可追踪的幂等键:
|
||||
|
||||
```bash
|
||||
BASE=http://10.1.1.104:18090
|
||||
AUTH="Authorization: Bearer <web-access-token>"
|
||||
|
||||
curl --fail -H "$AUTH" -H 'Content-Type: application/json' \
|
||||
-d '{"node_id":"windows-direct-20260911","account_id":"<account-id>","idempotency_key":"sessions-001","limit":50,"offset":0}' \
|
||||
"$BASE/v1/reads/sessions"
|
||||
|
||||
curl --fail -H "$AUTH" -H 'Content-Type: application/json' \
|
||||
-d '{"node_id":"windows-direct-20260911","account_id":"<account-id>","idempotency_key":"private-001","groups_only":false,"limit":50,"offset":0}' \
|
||||
"$BASE/v1/reads/contacts"
|
||||
|
||||
curl --fail -H "$AUTH" -H 'Content-Type: application/json' \
|
||||
-d '{"node_id":"windows-direct-20260911","account_id":"<account-id>","idempotency_key":"messages-001","chat_id":"<authorized-session-id>","include_content":true,"limit":20,"offset":0}' \
|
||||
"$BASE/v1/reads/messages"
|
||||
```
|
||||
|
||||
从响应取 `task_id` 后:
|
||||
|
||||
```bash
|
||||
curl --fail -H "$AUTH" "$BASE/v1/tasks/<task-id>"
|
||||
```
|
||||
|
||||
只有 `status=Succeeded` 且 `result.content` 存在时才读取内容;`Failed`、`Cancelled`、`Expired` 和 `ResultUnconfirmed` 都不能视为读取成功。
|
||||
|
||||
### 10.3 发送测试文本
|
||||
|
||||
只对已批准测试目标执行:
|
||||
|
||||
```bash
|
||||
curl --fail -H "$AUTH" -H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"node_id":"windows-direct-20260911",
|
||||
"account_id":"<account-id>",
|
||||
"kind":"send-text",
|
||||
"idempotency_key":"send-20260912-001",
|
||||
"payload":{
|
||||
"target_id":"session_item_文件传输助手",
|
||||
"text":"wxagent-controlled-test-<unique-marker>",
|
||||
"confirmed":true
|
||||
}
|
||||
}' \
|
||||
"$BASE/v1/tasks"
|
||||
```
|
||||
|
||||
发送任务必须结合任务终态和微信本地历史读回确认;`Succeeded` 代表 Agent 已确认完成调用,不代替业务侧读回核验。
|
||||
|
||||
## 11. 故障排查
|
||||
|
||||
| 现象 | 优先检查 |
|
||||
| --- | --- |
|
||||
| `/healthz` 失败 | 容器、端口映射、绑定地址、防火墙和 `/data` 权限 |
|
||||
| Web 登录 401 | `WXAGENT_WEB_USERS` 是否为合法 JSON map,密码是否与当前容器一致 |
|
||||
| 节点注册 401 | Token 是否与 `node_id` 一一绑定,节点是否读到最新 `remote.json` |
|
||||
| 节点 `Offline` | Windows 托盘进程、网络、心跳时间、控制面日志和节点 `remote status show` |
|
||||
| `SessionLocked` | 必须恢复微信所在的已登录、未锁定交互会话;Session 0 不算通过 |
|
||||
| `WechatNotLoggedIn` | 在微信桌面正常完成登录/手机确认,不绕过登录流程 |
|
||||
| `AccountContextUnconfirmed` | 重新确认活动账号和 UI 绑定,不能只看昵称 |
|
||||
| 读取返回 `ChatNotAuthorized` | 检查全局/账号/会话开关、类型、稳定 ID 和 `identityVerified` |
|
||||
| 读取返回空列表 | 先用本机 `session list`/联系人读取核对 ID 编码和当前可见范围 |
|
||||
| 读取返回 `ChatIdentityUnconfirmed` | 当前 UI 中不存在唯一匹配的稳定会话,禁止用昵称替代 |
|
||||
| `LeaseMismatch` | 任务已过租约或有旧执行者;不要重复创建写任务掩盖问题 |
|
||||
| `ResultUnconfirmed` | 人工核对微信是否已产生副作用;不得自动重放写操作 |
|
||||
| 修改 Token 后仍旧连接 | 节点远程地址/Token改变需要重启 Agent;控制面凭据改变需要重启容器 |
|
||||
| `/sessions/open` 返回 409 | 当前导航能力未启用;使用本地 UI 或现有只读读取任务,不把它当成传输故障 |
|
||||
|
||||
普通日志只记录错误码、类型、路径、耗时和 Correlation ID,不应通过日志回显消息正文、联系人名称、附件或凭据。排查时使用 Correlation ID 关联控制面审计和节点日志。
|
||||
|
||||
## 12. 当前验证证据与发布门禁
|
||||
|
||||
已完成的当前基线验证:
|
||||
|
||||
- 控制面 Docker 镜像构建成功,`/healthz` 返回 `ok/v1`;
|
||||
- Go `go test ./...`、`go vet ./...` 通过;
|
||||
- Core 测试 `166/166` 通过;Service 测试 `20/20` 通过;完整 solution Release build 通过;
|
||||
- `scripts/remote-control-smoke.sh` 通过,覆盖注册、心跳、任务租约、幂等、结果回传、读取结果保留和拒绝路径;
|
||||
- Windows 节点 `windows-direct-20260911` 在已登录、未锁定会话中上线,注册 `read-sessions`、`read-contacts`、`read-messages` 能力;
|
||||
- 真机 `send-text` 任务 `Pending → Running → Succeeded`,并用文件传输助手本地历史读回唯一标记;
|
||||
- 真机远程读取:会话 1 条、私聊联系人 1 条(`filehelper`)、群 1 条(`53271859539@chatroom`)、文件传输助手消息 3 条,四个读取任务均为 `Succeeded`。
|
||||
|
||||
提交或部署新版本前必须重新执行相关测试。以下条件满足前不能标记生产完成:
|
||||
|
||||
- 控制面 HTTPS、节点 HTTPS/mTLS 和密钥外置;
|
||||
- Token/证书轮换、撤销和泄露响应演练;
|
||||
- 单进程 JSON 存储的备份恢复、并发写入和容量上限验证;
|
||||
- 节点/控制面重启、断网、租约过期和 `ResultUnconfirmed` 人工核对;
|
||||
- 真实目标规模、长时间运行、数据保留和日志泄漏扫描;
|
||||
- 当前微信版本变化后的 `doctor`、脱敏 UI 树和 smoke 回归。
|
||||
|
||||
## 13. 相关文件
|
||||
|
||||
- `control-plane/Dockerfile`:控制面多阶段镜像构建;
|
||||
- `control-plane/server.go`、`protocol.go`、`store.go`:HTTP 路由、协议和持久化;
|
||||
- `control-plane/web/`:React 管理端;
|
||||
- `.gitea/workflows/build-web-image.yml`:Gitea 镜像发布;
|
||||
- `node-agent/WxAgent.Service/RemoteAgentHostedService.cs`:节点注册、轮询、任务执行和补传;
|
||||
- `node-agent/WxAgent.Core/ReportingAuthorization.cs`:统一本地白名单授权;
|
||||
- `node-agent/WxAgent.Core/RemoteTaskLedger.cs`:本地任务结果账本;
|
||||
- `node-agent/WxAgent.Host/RemoteCliCommands.cs`:远程认证、状态和白名单 CLI;
|
||||
- `docs/validation/Agent-Install-2026-09-11.md`:Windows 安装与真实验证记录;
|
||||
- `scripts/remote-control-smoke.sh`:Linux/临时控制面闭环检查。
|
||||
@@ -0,0 +1,46 @@
|
||||
# Agent 安装与真机验证(2026-09-11)
|
||||
|
||||
## 安装
|
||||
|
||||
- Windows 主机:`10.1.1.101`,已登录用户会话 `Session 1`。
|
||||
- 发布方式:Linux 上执行 .NET 8 `win-x64` self-contained single-file publish,复制到 `C:\Users\Rogee\wx-agent`。
|
||||
- 已部署并校验 SHA-256:`WxAgent.Host.exe`、`WxAgent.Tray.exe` 与本地发布产物一致。
|
||||
- 已注册 `WxAgent-Tray` 交互式登录启动任务;当前进程运行在 Session 1,服务监听 `127.0.0.1:5088`。
|
||||
|
||||
## 验证结果
|
||||
|
||||
| 命令 | 结果 | 证据 |
|
||||
| --- | --- | --- |
|
||||
| `WxAgent.Host.exe doctor` | 通过 | 5 个 Weixin 进程可读、1 个数据根目录、6/6 关键控件、无错误 |
|
||||
| `WxAgent.Host.exe inspect-ui --output ...` | 通过 | 157 个脱敏节点 |
|
||||
| `WxAgent.Host.exe smoke --output ...` | 通过 | 向“文件传输助手”发送唯一标记并确认,Fingerprint 已生成 |
|
||||
|
||||
远端证据保存在 `C:\Users\Rogee\wx-agent\artifacts\install-*.json`;普通输出未记录完整消息内容。
|
||||
|
||||
## 诊断记录
|
||||
|
||||
首次执行时 Session 1 为断开状态,`doctor` 返回 `SessionLocked`、`InputDesktopAvailable=false`,因此 smoke 未执行发送。执行 `tscon 1 /dest:console` 恢复为活动 console 后重试,`doctor`、`inspect-ui`、`smoke` 均返回 0。
|
||||
|
||||
## 本机 Docker 控制面连接验证
|
||||
|
||||
- 启动本机镜像 `wxagent-control-plane:local`,映射 `127.0.0.1:18090 -> 8090`;`/healthz` 返回 `ok/v1`,首页返回 455 字节。
|
||||
- 通过 SSH 反向端口转发将 Windows 节点的 `127.0.0.1:18090` 接入本机容器;节点 `windows-e2e-20260911` 成功注册。
|
||||
- 连续两次查询的 `last_heartbeat_at` 从 `2026-09-11T14:36:46Z` 更新到 `2026-09-11T14:37:02Z`,证明节点持续心跳连接;状态为 `Degraded` 仅因本次连接验证未配置活动账号,不是传输失败。
|
||||
- 验证结束后已恢复节点原始 `service.json`,停止临时容器和 SSH 隧道;`WxAgent-Tray` 保持本机登录启动配置。
|
||||
|
||||
## 内网直连复测
|
||||
|
||||
- 控制面容器改为绑定 `10.1.1.104:18090 -> 8090`,Windows 主机直连健康检查返回 `ok/v1`。
|
||||
- Agent 使用 `http://10.1.1.104:18090`,并通过 `--allow-insecure-http` 显式启用私有网段 HTTP;节点 `windows-direct-20260911` 成功注册。
|
||||
- 连续心跳从 `2026-09-11T14:55:13Z` 更新到 `2026-09-11T14:55:28Z`;`wechat_running=true`、`wechat_logged_in=true`、`session_locked=false`。
|
||||
- 已移除临时脚本;当前 Docker 容器使用 `unless-stopped` 运行,Agent 保持直连配置。
|
||||
|
||||
## 内网直连远程发送复测(2026-09-12)
|
||||
|
||||
- 微信登录状态曾因安全策略失效,重新扫码登录后继续;未操作真实联系人。
|
||||
- 当前 WeChat 4.1.13.63 不再暴露旧的头像资料窗口。Agent 改为通过稳定的 `MainView.main_tabbar.tabbar_setting` 打开 `PreferenceWindow`,读取“账号”区的昵称和微信号,完成绑定实时身份校验。
|
||||
- 清理旧窗口绑定后,账户重新绑定到当前 UI target;控制面节点 `windows-direct-20260911` 状态为 `Online`,`wechat_running=true`、`wechat_logged_in=true`、`session_locked=false`、账户 `verified=true`。
|
||||
- 控制面 `send-text` 任务 `9d_vZnrEBd9h3w42flhOpw` 经 `Pending → Running → Succeeded`;目标为 `session_item_文件传输助手`,Agent 本地消息历史读回同一唯一标记,确认消息已实际写入。
|
||||
- Agent 本地只读 API 复核:群列表 115 条,包含 `消息测试专用群组`(`53271859539@chatroom`);私聊联系人全量分页 10,584 条;当前可见会话 11 条;文件传输助手历史 3 条,包含本次标记。
|
||||
- Docker 控制面现已补充远程只读任务:`POST /v1/reads/sessions`、`POST /v1/reads/contacts`、`POST /v1/reads/messages`;结果通过 `GET /v1/tasks/{task_id}` 获取,并由节点在读取前执行严格白名单校验。历史仍是当前微信 UI 可见消息,不是数据库全量历史。
|
||||
- 真机远程读取复测通过:会话 1 条(文件传输助手),私聊白名单 1 条(`filehelper`),群白名单 1 条(`53271859539@chatroom`),文件传输助手历史 3 条且包含此前发送标记;四个读取任务均为 `Succeeded`。
|
||||
Reference in New Issue
Block a user