783 lines
38 KiB
Markdown
783 lines
38 KiB
Markdown
---
|
||
title: 微信 UIA 本地代理渠道设计(weixin-uia)
|
||
created: 2026-05-09
|
||
status: draft
|
||
related:
|
||
- docs/superpowers/specs/2026-05-08-weixin-group-channel-design.md
|
||
- docs/superpowers/plans/2026-05-08-weixin-group-channel.md
|
||
---
|
||
|
||
# 微信 UIA 本地代理渠道设计(weixin-uia)
|
||
|
||
> **⚠️ OBSOLETE 2026-05-14**: UIA 路线在微信 4.1.9.54 经 PoC(`tools/uia_probe/probe.ps1`)验证彻底失效(Qt 自绘 + `MMUIRenderSubWindowHW` 硬件加速渲染层 → UIA 树只有 3 节点 0 交互控件;讲述人 / 注册表 AccessibilityTemp / `QT_ACCESSIBILITY=1` 环境变量 / `StructureChangedEventHandler` 伪客户端全部无效)。
|
||
> 新方案见 `2026-05-14-neta-desktop-op-design.md` v4(通用桌面 GUI Agent,WeixinAdapter 是第一个 application adapter)。
|
||
> 本文件保留作历史参考。
|
||
|
||
## Context
|
||
|
||
Neta 当前仅接入 iLink ClawBot(渠道类型 `weixin`),只能做一对一私聊助手,**不能接收微信群消息**——详见前 spec `2026-05-08-weixin-group-channel-design.md` 的"后记 · iLink ClawBot 群消息接入的事实更正"节。
|
||
|
||
经过多轮技术评估,**进入个人微信群收集客户问题并让 agent 介入处理**的唯一可行路径是:在用户本机 PC 微信上做 UI Automation(UIA)代理。本 spec 基于以下决策:
|
||
|
||
- 走 Microsoft 官方 .NET 8 `System.Windows.Automation`(不走 Python wxauto、不走 WeChatFerry DLL 注入)
|
||
- 新增独立桥接进程 `Neta.WeChatBridge`,与 `Neta.Tray` 平级,复用现有 `tray-secret` IPC 模式
|
||
- iLink ClawBot 渠道(`weixin`)保留为**个人助手**;UIA 渠道(`weixin-uia`)专门做**群聊助手**
|
||
- 今天 2026-05-08 已实施的 20 个 task 绝大部分抽象可直接复用(chat_scope / agent_executor chatScope / session 构造 / 群管理 UI 等)
|
||
|
||
本 spec 负责描述 UIA 渠道从 0 到 MVP 的完整架构、消息采集、触发策略、前端扩展、与现有代码的增量关系。
|
||
|
||
## 总体架构
|
||
|
||
```
|
||
┌──────────────── Windows 用户桌面会话 ────────────────┐
|
||
│ │
|
||
│ PC 微信(用户的个人微信号,已登录) │
|
||
│ ↑ (System.Windows.Automation COM API) │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ Neta.WeChatBridge.exe(新增 .NET 8 进程) │
|
||
│ ├─ UIA 监听器(StructureChanged 事件 + 切窗) │
|
||
│ ├─ 消息采集(文本 / 图片 / @ / 引用) │
|
||
│ ├─ 图片从 WeChat Files 目录拷到 dataDir │
|
||
│ ├─ 版本白名单校验 │
|
||
│ └─ 暴露 localhost HTTP + x-neta-tray-secret │
|
||
│ ↓ │
|
||
│ Neta backend (Node.js) │
|
||
│ ├─ service/weixin_uia.ts ← 客户端 │
|
||
│ │ ├─ POST /send 发回复 │
|
||
│ │ └─ GET /rooms 拉群列表快照 │
|
||
│ ├─ controller/open/weixin_uia.ts ← 入站入口 │
|
||
│ │ 接 bridge 推送的群消息 │
|
||
│ ├─ service/agent_channel.ts 按 channel.type │
|
||
│ │ 分流:'weixin' → iLink / 'weixin-uia' │
|
||
│ │ → bridge;同一套 routeInboundMessage │
|
||
│ ├─ 群聊 archive → SQLite (dataDir/wechat- │
|
||
│ │ archive-<cid>.db) │
|
||
│ └─ 被 trigger 命中的消息 → session_entry │
|
||
│ (MySQL 主库) │
|
||
│ │
|
||
│ Neta.Tray(已存在,负责拉起 backend + bridge) │
|
||
└──────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 关键决策
|
||
|
||
- **channel.type 多态**:iLink ClawBot = `weixin`(仅 DM);UIA = `weixin-uia`(仅群聊)。两种 channel 可同时存在
|
||
- **wxid ↔ weixin-uia channel 一对一**(硬约束):同一 wxid 最多绑一个 UIA channel。handshake 时发现多个会返回 409 要求手工清理;前端新建 UIA channel 页同 wxid 已存在时拒绝
|
||
- **严格职责划分**:ClawBot 即使收到群消息也丢弃;UIA 即使收到 DM 也丢弃(防止双渠道重复响应)
|
||
- **进程模型**:Tray 启动时拉起 `backend.exe` + `WeChatBridge.exe`,共享 tray-secret
|
||
- **双库存储**:SQLite 存全量原始群消息(含被拒绝的);MySQL session_entry 只存被 agent 接纳的
|
||
- **图片策略**:从微信 `%APPDATA%\Tencent\WeChat\WeChat Files\<wxid>\FileStorage\Image\` 拷到 `dataDir/wechat-uploads/<channelId>/YYYY-MM/<hash>.jpg`,backend 通过现有 `/files` 静态服务暴露
|
||
- **触发判定**:硬规则 `at_mention` / `all`(all 模式让 agent 自行判断是否相关并通过返回空串/`[SKIP]` 决定不回),不做 LLM 意图过滤的独立一层。**`prefix` 模式在新渠道中弃用**——数据库字段保留以兼容存量 iLink 数据,`decideGroupAcceptance` 纯函数仍能读取旧配置,但 `updatePolicy` service 层拒绝新写入 `prefix`,前端不暴露该选项
|
||
- **IPC 复用**:Bridge ↔ Backend 使用 `localhost HTTP + x-neta-tray-secret`,与 Tray ↔ Backend 现有模式完全一致
|
||
- **Bridge URL 注册**:bridge handshake 时必须在 body 里带 `bridgeBaseUrl`(`http://127.0.0.1:<port>`)告知 backend 自己的监听地址,backend 据此构造 HTTP 客户端用于 `/send` `/enable-room` 等主动调用
|
||
- **Room 启用同步**:前端点"启用监听" → backend 更新 `group.status=1` → **backend 主动调 `bridge /enable-room`** 让 bridge 实时订阅;disable 同理。bridge 重启后通过 handshake 响应的 `enabledRooms` 批量恢复订阅
|
||
|
||
### MVP 与 v2 边界
|
||
|
||
MVP 覆盖:
|
||
- 文本消息完整往返
|
||
- 图片消息采集 + 本地归档 + agent vision 可见
|
||
- @ 检测、引用消息的结构化 prompt 投影
|
||
- 多群切窗监听、每群独立 agent 绑定
|
||
- 群内回复身份(silent / `【AI 助手】` 前缀)
|
||
- Bridge 崩溃自愈、版本白名单
|
||
|
||
v2(不在本 spec 范围):
|
||
- 语音转文字
|
||
- 视频、文件下载
|
||
- 跨群全局人物志
|
||
- bridge 主动发图片/文件到群
|
||
|
||
---
|
||
|
||
## 数据模型
|
||
|
||
### 复用 / 扩展已有表
|
||
|
||
**`netaclaw_agent_channel`**(今天 task 已用)
|
||
- `type` 字段新增合法值 `weixin-uia`(与现有 `weixin` 并存)
|
||
- `credential` JSON 对 `weixin-uia` 的含义:
|
||
```ts
|
||
{ wxid: 'wxid_xxx', nickname: '张三', wechatVersion: '3.9.11.17' }
|
||
```
|
||
不再有 iLink token / accountId——UIA 渠道认的是"本机 PC 微信登录的这个号"
|
||
- `config.group` JSON 扩展:
|
||
```ts
|
||
group: {
|
||
botAlias?: string; // @ 匹配用(已有)
|
||
replyIdentity?: 'silent' | 'ai_prefix'; // 新:默认 silent
|
||
}
|
||
```
|
||
|
||
**`netaclaw_agent_channel_group`**(今天 task 新建)
|
||
- 字段基本不变
|
||
- `triggerMode` 允许值从 3 档收敛为 **2 档**:`at_mention` / `all`。`prefix` 已**弃用**——`updatePolicy` service 层拒绝写入 `prefix`;`decideGroupAcceptance` 纯函数仍容忍 DB 里残留的 `prefix` 记录(不 break 存量 iLink 数据);前端 UI 只给 2 个选项
|
||
- 新增字段 `replyIdentityOverride?: 'silent' | 'ai_prefix' | null`(每群可覆盖 channel 级默认)
|
||
- 新增字段 `boundAgentId: number | null`(每群可独立绑定一个 agent,覆盖 channel 级默认 agent)
|
||
- `status` 字段引入第三态 `-1 = 已否决(ignored)`:用户显式"忽略"过的群不再在"待审批"横幅提醒
|
||
|
||
### 新增表 `netaclaw_wechat_archive`(SQLite)
|
||
|
||
独立 SQLite 文件存放于 `dataDir/wechat-archive-<channelId>.db`,每个 UIA 渠道独立一个文件(便于备份/清理)。
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| `id` | INTEGER PK | 自增 |
|
||
| `roomId` | TEXT | 群稳定 id(见下) |
|
||
| `msgId` | TEXT | UIA 合成的消息唯一键(见下),UNIQUE |
|
||
| `senderWxid` | TEXT nullable | 发送者 wxid(UIA 大多数情况读不到) |
|
||
| `senderName` | TEXT | 发送者群内昵称 |
|
||
| `msgType` | TEXT | `text` / `image` / `file` / `voice` / `video` / `system` / `quote` |
|
||
| `content` | TEXT | 文本内容(图片类型下为 OCR 文本或空) |
|
||
| `attachmentPath` | TEXT nullable | 本地文件路径(`dataDir/wechat-uploads/...`) |
|
||
| `quotedRef` | TEXT nullable | 引用消息 JSON(被引用者 + 原文本预览) |
|
||
| `atList` | TEXT nullable | 被 @ 的人(JSON 数组) |
|
||
| `receivedAt` | TEXT | bridge 观测时间戳(ISO8601) |
|
||
| `triggerAccepted` | INTEGER 0/1 | 是否通过 trigger gate |
|
||
| `triggerReason` | TEXT nullable | 拒绝时的 reason |
|
||
| `sessionEntryId` | TEXT nullable | 关联 MySQL session_entry id |
|
||
| `createdAt` | TEXT | 入库时间 |
|
||
|
||
索引:`(roomId, receivedAt)`、`UNIQUE(msgId)`、`(triggerAccepted)`
|
||
|
||
**`msgId` 合成规则**:`sha256(roomId + '|' + senderName + '|' + content + '|' + receivedAt.slice(0,16))`,精度到分钟。避免切窗重复读同一消息时重复入库。
|
||
|
||
**`roomId` 稳定 id 策略**:微信 UI 不暴露群的原生 id。采取 `${channelId}:room:${sha1(roomName)}` 作为稳定 id,群被用户改名后视为新群(重新发现、重新审批)。
|
||
|
||
### 主库 session_entry 不变
|
||
|
||
被 trigger 接纳的群消息继续按现有 session_entry 机制落 MySQL,sessionId 规则沿用 `channel:<cid>:weixin:group:<roomId>`——前端 agent 对话页透明复用。
|
||
|
||
### 数据流向
|
||
|
||
```
|
||
UIA 事件 → bridge → POST /inbound → Node backend
|
||
↓ (SQLite 写入全量原始消息,triggerAccepted=0)
|
||
↓
|
||
routeInboundMessage → decideGroupAcceptance
|
||
↓ ↓
|
||
↓ (reject: SQLite 已记录,结束)
|
||
↓ (accept)
|
||
↓
|
||
SQLite 回写 triggerAccepted=1 + sessionEntryId
|
||
↓
|
||
agentExecutor.execute → finalizeAssistantEntry 写 MySQL session_entry
|
||
↓
|
||
POST /send 回 bridge → UIA 模拟发送(若 finalContent 非空且非 [SKIP])
|
||
```
|
||
|
||
---
|
||
|
||
## 消息采集与图片处理
|
||
|
||
### UIA 能读到的群消息字段
|
||
|
||
从微信主窗口右侧"聊天框"的 `ListControl(Name='消息')` 里,每条消息是一个 `ListItem`:
|
||
|
||
```
|
||
ListItem
|
||
├── Button / Text:发送者昵称(群内显示名,不是 wxid)
|
||
├── Pane(消息气泡)
|
||
│ ├── Text:纯文本内容
|
||
│ │ 或 Image + TextBlock:图片消息(含文件名)
|
||
│ │ 或 Button:文件消息(含文件名和大小)
|
||
│ │ 或 Button:引用消息("XXX: 原文..."+ 本条回复)
|
||
│ └── 右键菜单(上下文菜单)
|
||
└── Text:时间戳
|
||
```
|
||
|
||
对每种消息类型的处理:
|
||
|
||
| 消息类型 | 采集策略 | 落 SQLite |
|
||
|---|---|---|
|
||
| **文本** | 直接读 `Text.Name` | `msgType='text'`, `content` |
|
||
| **@** | 解析文本中 `@<alias>` + UIA 如果给 at list 则优先取 | `atList` JSON |
|
||
| **引用** | 读气泡顶部的 "XXX: 原文..." 子控件 | `quotedRef` JSON |
|
||
| **图片** | DAT 解密 / 拷贝,见下 | `msgType='image'`, `attachmentPath` |
|
||
| **文件** | 从 WeChat Files 目录拷出,见下 | `msgType='file'`, `attachmentPath` |
|
||
| **语音** | MVP 不处理;v2 右键"转文字" | `msgType='voice'`, `content` 留空 |
|
||
| **视频** | 只记 metadata(文件名),不下载 | `msgType='video'`, 无 attachmentPath |
|
||
| **系统消息**(撤回通知等) | 读文本 | `msgType='system'` |
|
||
| **表情/动图** | 商店贴图忽略;自定义 GIF 按图片处理 | `msgType='image'` 或丢弃 |
|
||
|
||
### 图片与文件的获取路径
|
||
|
||
**不用 UIA 模拟右键"另存为"**(慢、UI 闪烁)。PC 微信把图片/文件缓存在固定目录,bridge 直接访问文件系统:
|
||
|
||
```
|
||
%APPDATA%\Tencent\WeChat\WeChat Files\<wxid>\FileStorage\
|
||
├── Image\YYYY-MM\<hash>.dat ← 加密图片(固定 XOR)
|
||
├── Image\YYYY-MM\<hash>.jpg ← 解密缓存(微信打开过会生成)
|
||
└── File\YYYY-MM\<filename>.xxx ← 未加密文件
|
||
```
|
||
|
||
Bridge 的做法:
|
||
1. UIA 检测到图片消息到达
|
||
2. 读气泡里的文件名/时间戳定位目录
|
||
3. 轮询匹配最新的 `.dat`(图片)或文件名(文件)
|
||
4. 图片:如果 `.jpg` 缓存已存在用 `.jpg`;否则用公开的 DAT-XOR 算法解密 `.dat`(每字节异或一个常量,识别文件头决定异或 key)
|
||
5. 拷贝到 `dataDir/wechat-uploads/<channelId>/YYYY-MM/<msgIdHash>.ext`
|
||
6. 本地路径落到 SQLite `attachmentPath`
|
||
|
||
**fallback**:解密失败或找不到 → 记 `.dat` 原路径,agent 对话页渲染"⚠️ 图片无法显示",不丢整条消息。
|
||
|
||
### 发送回复
|
||
|
||
1. Backend POST `/send` 给 bridge:`{ roomName, text, atList? }`
|
||
2. Bridge 调 UIA:切到该群窗口 → `SetFocus` 输入框 → `SendKeys` 文本 → `SendKeys(Enter)`
|
||
3. 读最后一条"自己发送的消息"气泡确认时间戳 > 发送前,回 200
|
||
|
||
**MVP 不支持发送图片/文件到群**——agent 回复限定为文本。
|
||
|
||
### 身份控制(`replyIdentity`)
|
||
|
||
- `silent`(默认):直接发 `${text}`
|
||
- `ai_prefix`:发 `【AI 助手】${text}`
|
||
|
||
### 发送者 wxid 的获取
|
||
|
||
UIA 大多数情况只能读到"群内昵称"而非 wxid。存储策略:
|
||
- SQLite 存 `senderName`(必有)
|
||
- `senderWxid` 绝大多数为 NULL
|
||
- 启动时主动拉群成员列表,建立 wxid ↔ 当前昵称 映射表(v2 补)
|
||
- agent 看到的 message 以 `[senderName]: content` 前缀呈现,让 LLM 基于上下文自行区分
|
||
|
||
### 去重 / 首次发现
|
||
|
||
- `msgId` UNIQUE 约束避免切窗重复读
|
||
- 首次发现群**不 backfill 历史消息**,只认"现在能看到的最新一条"为起点,避免一次性喂给 agent 几百条堆积消息
|
||
|
||
### 端到端延迟预估
|
||
|
||
- StructureChanged 事件触发 → < 100ms
|
||
- 切窗 → 读列表 → 切回:约 500ms
|
||
- 图片落地(含缓存等待):1-3 秒
|
||
- 发送回复:约 500ms
|
||
|
||
**典型延迟**:文本 ~1 秒,图片 ~2-3 秒。
|
||
|
||
---
|
||
|
||
## 多群监听策略(切窗调度)
|
||
|
||
### 事件源
|
||
|
||
Bridge 在微信主窗口上注册:
|
||
- **`StructureChangedEvent`**:订阅左侧会话列表(Sessions 栏)
|
||
- **`AutomationPropertyChangedEvent(NameProperty)`**:订阅会话列表项的文本变化(= 最新消息预览变了 = 有新消息)
|
||
|
||
**事件源 = 会话列表,而非聊天框**。切窗后读聊天框只是事件触发后的动作。
|
||
|
||
### 切窗调度队列
|
||
|
||
所有切窗串行化(UIA 对同一窗口不支持并发):
|
||
|
||
```
|
||
事件触发 → enqueue(roomName)
|
||
↓
|
||
单线程 worker:
|
||
├─ 从队列 dequeue 一个 roomName
|
||
├─ 校验该群 channel_group.status=1(enabled)
|
||
│ 未启用/未审批 → SQLite "发现但未审批" + 通知 backend(前端红点)
|
||
│ 已否决(-1) → 直接丢弃
|
||
│ 已启用(1) → 继续
|
||
├─ UIA: 点击左侧会话列表对应条目 → 右侧聊天框出现该群
|
||
├─ 采集新消息(与"上次该群已知 msgId"去重)
|
||
├─ 写 SQLite
|
||
├─ 调 backend POST /inbound
|
||
└─ 完成后不切回原窗口,等下一个事件触发
|
||
```
|
||
|
||
**不切回原窗口**:用户可能原本在看某个群,来回切会让体验崩坏。只在需要时切到目标群,结束停在那里。用户自己想看别的群会手动切走——bridge 下次事件也是切到其它群,对用户无额外干扰。
|
||
|
||
### 队列合并与背压
|
||
|
||
- **去重合并**:同一 roomName 连续来多条事件,只处理一次切窗——切到该群后一次性读完所有未读
|
||
- **超限丢弃**:队列 >50 待处理 room → warn 日志 + 丢 oldest 保 newest
|
||
- **单次超时**:单次切窗 3 秒超时 → 跳过该群、保留队列
|
||
- **并发上限**:单 bridge = 单 PC 微信实例 = 单点串行。不存在真并发
|
||
|
||
### Bridge ↔ Backend 通信方向
|
||
|
||
**Bridge 主动推 inbound,Backend 主动调 send/rooms/health**。不用 long-poll。理由:
|
||
- 与 Tray → Backend 现有 IPC 方向一致
|
||
- Backend 定期调 `GET bridge/health` 检测 bridge 存活
|
||
|
||
HTTP 接口清单:
|
||
|
||
```
|
||
Bridge 暴露(被 backend 调):
|
||
GET /health → { ok, wxid, nickname, wechatVersion }
|
||
GET /rooms → 能看到的群列表
|
||
POST /send { roomName, text, atList? } → 发送回复
|
||
POST /enable-room { roomName } → 加入监听
|
||
POST /disable-room { roomName } → 取消监听
|
||
GET /diag → 诊断信息(队列长度、最近错误)
|
||
|
||
Backend 暴露(被 bridge 调):
|
||
POST /open/netaclaw/channel/uia/handshake
|
||
{ wxid, nickname, wechatVersion, bridgeBaseUrl }
|
||
→ { channelId, enabledRooms: string[] } 认领 channel + 返回已启用群列表
|
||
POST /open/netaclaw/channel/uia/inbound
|
||
{ channelId, roomName, senderName, msgType, content, attachmentPath?,
|
||
atList?, quotedRef?, receivedAt, rawHash }
|
||
→ 入站消息
|
||
```
|
||
|
||
> `handshake` 不再接受 `channelId?` 请求字段——bridge 启动时不知道 channelId,只能靠 wxid 查找;channelId 是响应字段。`bridgeBaseUrl` 为必填(形如 `http://127.0.0.1:<port>`),backend 据此注册 HTTP 客户端用于后续 `/send` `/enable-room` 等主动调用。
|
||
|
||
两端用 `x-neta-tray-secret` 鉴权,与 Tray ↔ Backend 共用。
|
||
|
||
### Bridge 冷启动序列
|
||
|
||
1. 读 PC 微信进程版本 → 校验白名单 → 不匹配则 exit(1) + tray 通知
|
||
2. 定位微信主窗口 → 拿 `wxid` / `nickname`
|
||
3. `POST /handshake` → backend 认领 channel,返回该 channel 已启用的群列表
|
||
4. 订阅会话列表的 StructureChanged + Name 事件
|
||
5. 主动扫一次当前会话列表,把未在启用列表里的新群上报为"待审批"
|
||
6. 进入 worker 循环
|
||
|
||
### 重启不回灌
|
||
|
||
Bridge 重启后**只处理重启后新出现的事件**,不补齐 SQLite 已记录消息的空窗期——避免一次性塞几百条历史消息给 agent。
|
||
|
||
### 运行时约束
|
||
|
||
- 微信主窗口不能最小化到系统托盘(可以置于屏幕外/移到虚拟桌面但**不能完全最小化**)——UIA 对 minimized 窗口读不到控件树
|
||
- 同时只能登录一个微信号(PC 微信本身限制)
|
||
- Bridge 运行期间用户操作微信正常——bridge 只在事件触发时短暂切窗,平时不干扰
|
||
|
||
---
|
||
|
||
## Backend 分流与触发策略
|
||
|
||
### `agent_channel.routeInboundMessage` 按 channel.type 分流
|
||
|
||
今天 task 里 `routeInboundMessage` 已经是"传输层无关"的同步分发器。本 spec 只加一个入口适配:
|
||
|
||
```
|
||
Bridge POST → controller: /open/netaclaw/channel/uia/inbound
|
||
body: { channelId, roomName, senderName, msgType, content, attachmentPath?, atList?, quotedRef?, rawHash, receivedAt }
|
||
↓
|
||
agentChannelService.ingestUiaInbound(...)
|
||
↓
|
||
构造"伪 message"(shape 与 iLink 入站一致,让下游 routeInboundMessage 无感):
|
||
{
|
||
from_user_id: senderName, // 群内昵称充当 sender
|
||
room_id: roomIdStable, // ${channelId}:room:${sha1(roomName)}
|
||
message_id: rawHash, // bridge 合成的去重 key
|
||
item_list: [{ type: 1, text_item: { text: content } }],
|
||
at_user_list: atList,
|
||
attachments: attachmentPath ? [{ kind: msgType, path: attachmentPath }] : undefined,
|
||
quoted_ref: quotedRef,
|
||
}
|
||
↓
|
||
routeInboundMessage(channel, state, message) ← 今天 task 已实现的同步分流
|
||
↓ (群路径)
|
||
├─ decideChatScope → kind='group'
|
||
├─ 查 pendingClarify 短路
|
||
├─ senderQueue 按 roomId 入队(群级共享串行,已实现)
|
||
└─ handleInboundMessage 异步处理
|
||
```
|
||
|
||
### handleInboundMessage 的增量改动
|
||
|
||
- 触发策略从 3 档 → 2 档:`at_mention` / `all`。**`prefix` 在 service 写入层(`updatePolicy`)抛 400 拒绝**;`decideGroupAcceptance` 纯函数仍处理 `prefix`(兼容存量数据);前端 UI 只显示 2 档
|
||
- `all` 模式下 acceptance 无条件通过,但传给 agent 的 `userMessageMetadata` 多带 `shouldEvaluate: true` 标志,提示 agent "你要自己判断相关性"
|
||
- agent 返回 `finalContent === ''` 或以 `[SKIP]` 开头 → `handleInboundMessage` **不调 sendText**(agent 沉默)
|
||
|
||
### 每群绑定 agent(`boundAgentId`)
|
||
|
||
```ts
|
||
const group = await this.groupService.findByKey(channel.id, scope.chatId);
|
||
const effectiveAgentId = group.boundAgentId ?? channel.agentId;
|
||
if (!effectiveAgentId) {
|
||
this.logger.warn('[UIA] group_no_agent_bound channelId=%s roomId=%s', channel.id, scope.chatId);
|
||
return;
|
||
}
|
||
// boundAgentId 覆盖时,channel.agentName 可能不对(指向 fallback agent),传 undefined 让
|
||
// agentExecutor 内部按 effectiveAgentId 查正确 agentName,避免 prompt 里 agent 身份错乱。
|
||
const effectiveAgentName =
|
||
effectiveAgentId === channel.agentId ? (channel.agentName || undefined) : undefined;
|
||
```
|
||
|
||
### 图片附件喂给 agent 的通路
|
||
|
||
伪 message 里带 `attachments: [{ kind: 'image', path: '...' }]`——复用 Neta 现有 `runAgent.userMessageMetadata.attachments` 通路(`gateway/server.ts:564` 已支持),agent 看到带图片附件的 user message,能调 vision 工具识图。
|
||
|
||
### 引用 / @ 的 prompt 投影
|
||
|
||
给 LLM 的 user message 不是裸 content,而是**结构化上下文渲染**(在 `handleInboundMessage` 构造 cleanedText 时生成):
|
||
|
||
```
|
||
[群: 产品研发群]
|
||
[发送人: 小王]
|
||
[被引用: 老板(18:32): 这个方案明天就上线]
|
||
[被@的人: 张三, @小神(bot)]
|
||
|
||
消息正文:
|
||
确定可以上线吗?@小神 帮忙 double check 一下
|
||
```
|
||
|
||
### Clarify / Risk 在群路径的处理
|
||
|
||
- **Clarify**:沿用今天 task 的"发起者绑定 + 5 分钟超时 + 超时 short-circuit 成 Decline"。clarify 回复的身份遵循 `replyIdentity` 配置
|
||
- **Risk**:沿用"直接 block + 回群里发拒绝文字"。拒绝文字也遵循 `replyIdentity`
|
||
|
||
### 发送路径
|
||
|
||
```ts
|
||
if (result.content.trim() === '' || result.content.startsWith('[SKIP]')) {
|
||
// agent 选择不回。更新 SQLite.sessionEntryId,不调 bridge /send
|
||
return;
|
||
}
|
||
const finalText = resolveReplyIdentity(channel, group) === 'ai_prefix'
|
||
? `【AI 助手】${result.content}`
|
||
: result.content;
|
||
await this.uiaBridgeClient.sendText(roomName, finalText);
|
||
await this.groupService.touchActive(channel.id, scope.chatId);
|
||
```
|
||
|
||
### Bridge 离线时 backend 的处理
|
||
|
||
- Backend 每 30 秒 `GET bridge/health`,连续 2 次失败 → 标记 channel `loginStatus='disconnected'`,前端卡片显示离线
|
||
- 离线期间:bridge 不推消息、agent 不执行
|
||
- 重连:channel 自动转回 `connected`,继续运转
|
||
|
||
### 严格的跨渠道去重
|
||
|
||
同一 wxid 可能同时存在 iLink ClawBot 渠道 + UIA 渠道。为避免重复响应:
|
||
- iLink ClawBot 收到 `room_id` 非空的消息 → 按现有逻辑**丢弃**(今天 task 已有)
|
||
- UIA 收到 DM(`chat_type='dm'`) → 日志 warn + **丢弃**,不调 agent
|
||
- 这个约束写进 `routeInboundMessage` 的 channel.type 分流入口
|
||
|
||
---
|
||
|
||
## 前端 UX 调整
|
||
|
||
### 频道管理页(`channel-management.vue`)
|
||
|
||
drawer 新增**频道类型**下拉:
|
||
|
||
```
|
||
○ 微信 ClawBot(个人助手 · 仅私聊) ← 现有 type='weixin'
|
||
● 微信本地代理(群聊助手 · 需 PC 微信) ← 新增 type='weixin-uia'
|
||
```
|
||
|
||
按 type 动态渲染表单字段:
|
||
|
||
| 字段 | weixin | weixin-uia |
|
||
|---|---|---|
|
||
| 扫码登录 | ✅ | ❌ |
|
||
| `wxid` 手填 | ❌(由 iLink 返回) | ✅ **必填**(从 PC 微信"设置→关于"复制,handshake 时 bridge 按此匹配本频道并回填 nickname/wechatVersion) |
|
||
| 机器人昵称 `botAlias` | ✅(必填) | ✅(选填,默认用微信昵称) |
|
||
| 默认 Agent | ✅(必填) | ✅(可空,群可各自绑) |
|
||
| 群内回复身份 `replyIdentity` | ❌ 不适用 | ✅ `silent` / `ai_prefix` 下拉 |
|
||
| Bridge 连接状态 | ❌ | ✅ 展示 `已连接 / 未就绪 / 版本不兼容` |
|
||
|
||
卡片元数据行的徽标(今天 task 已有)仍有效:`群聊 X/Y`。UIA 渠道加 `微信版本不兼容` 红色 tag(版本白名单判定失败时)。
|
||
|
||
### 群聊管理抽屉(`channel-group-panel.vue`)
|
||
|
||
今天 task 已建好整个组件,本 spec 扩展:
|
||
|
||
- **触发策略** 从 3 档改 2 档:`@机器人` / `所有消息(由 agent 自行判断是否回复)`
|
||
- **每群绑定 agent**(新字段,默认空,fallback 到 channel 默认):
|
||
```
|
||
绑定 agent:[下拉选择] 未选 → 使用频道默认 Agent
|
||
```
|
||
- **回复身份覆盖**(新字段):
|
||
```
|
||
回复身份:○ 跟随频道 ○ 隐形 ○ 【AI 助手】前缀
|
||
```
|
||
- **待审批横幅**(对应"被动发现 + 前端审批"决策):
|
||
- 列表顶部 `新发现 N 个群(待审批)` 橙色横幅
|
||
- 点击横幅 → 过滤 status=0 的群
|
||
- 每个新群卡片加两个按钮:`启用监听` / `忽略`
|
||
- "忽略" → status=-1,不再在横幅重复提醒
|
||
|
||
### agent 对话页(`chat.vue`)
|
||
|
||
- sessionId 仍是 `channel:<cid>:weixin:group:<roomId>`;`buildFallbackTitle` 今天已支持,显示 `微信群 · ...`
|
||
- **新增**:消息气泡左上角小标识,区分 DM 和群消息(按 sessionId 模式判断,不引入 metadata.chat)
|
||
|
||
### 归档查看页(`wechat-archive-panel.vue`)
|
||
|
||
SQLite 原始消息归档查看抽屉,入口放在群聊管理面板每个群卡片里 `查看归档` 按钮:
|
||
|
||
- 按 roomId 过滤
|
||
- 按 triggerAccepted 筛选(全部 / 只看已接纳 / 只看被拒绝)
|
||
- 时间轴展示,图片/文件可点开预览
|
||
- 支持"标记为有价值 → 转存到 MySQL 业务表"(v2 兑现,UI 先预留按钮)
|
||
|
||
### Neta.Tray 菜单扩展
|
||
|
||
```
|
||
Neta AI
|
||
├─ 状态:运行中
|
||
├─ 打开控制台
|
||
├─ 微信桥接:
|
||
│ ├─ 状态:✅ 已连接(小明,微信 3.9.11.17)
|
||
│ ├─ 重启桥接
|
||
│ └─ 查看日志
|
||
└─ 退出
|
||
```
|
||
|
||
Tray 负责 bridge 进程生命周期(新增 `BridgeProcessManager`,模式参照 `BackendProcessManager`)。
|
||
|
||
---
|
||
|
||
## Neta.WeChatBridge 项目结构与部署
|
||
|
||
### .NET 项目骨架
|
||
|
||
```
|
||
packages/windows-tray/
|
||
├── Neta.Tray/ ← 已存在
|
||
├── Neta.Tray.Tests/ ← 已存在
|
||
├── Neta.WeChatBridge/ ← 新增
|
||
│ ├── Neta.WeChatBridge.csproj (net8.0-windows, OutputType=Exe)
|
||
│ ├── Program.cs 启动入口:读取参数 / 启 HTTP / 初始化 UIA
|
||
│ ├── Config/
|
||
│ │ ├── VersionProfile.cs 微信版本适配 profile
|
||
│ │ └── VersionProfiles.yaml 可热更新的控件名/路径配置
|
||
│ ├── Uia/
|
||
│ │ ├── WeChatWindow.cs 主窗口定位 + 子控件缓存
|
||
│ │ ├── SessionListWatcher.cs 会话列表事件订阅(事件源)
|
||
│ │ ├── ChatBoxReader.cs 切窗 + 读消息列表
|
||
│ │ ├── MessageSender.cs 发文本 + @
|
||
│ │ └── AttachmentExtractor.cs 图片/文件从 WeChat Files 目录拷出
|
||
│ ├── Scheduling/
|
||
│ │ └── RoomEventQueue.cs 切窗串行队列 + 去重合并
|
||
│ ├── Http/
|
||
│ │ ├── BridgeHttpServer.cs Kestrel 托管的本地 HTTP
|
||
│ │ ├── Endpoints/
|
||
│ │ │ ├── HealthEndpoint.cs
|
||
│ │ │ ├── RoomsEndpoint.cs
|
||
│ │ │ ├── SendEndpoint.cs
|
||
│ │ │ └── EnableDisableEndpoint.cs
|
||
│ │ └── TraySecretAuth.cs 与 tray 共用 secret
|
||
│ ├── Backend/
|
||
│ │ └── BackendClient.cs 推送 inbound 到 backend
|
||
│ └── Runtime/
|
||
│ ├── BridgeRuntimeInfo.cs 读取 tray 传来的 bootstrap 参数
|
||
│ └── GracefulShutdown.cs SIGTERM / Ctrl+C 处理
|
||
└── Neta.WeChatBridge.Tests/ 新增(xUnit)
|
||
```
|
||
|
||
### 启动参数与 IPC 凭证
|
||
|
||
Neta.Tray 启动时生成 `tray-secret`,同时传给 `backend.exe` 和 `bridge.exe`:
|
||
|
||
```
|
||
WeChatBridge.exe \
|
||
--tray-secret <secret> \
|
||
--backend-url http://127.0.0.1:<port> \
|
||
--data-dir <dir> \
|
||
--bridge-port <port>
|
||
```
|
||
|
||
与现有 `BackendProcessManager` 模式一致。
|
||
|
||
### `BridgeProcessManager`(新增,参照 `BackendProcessManager`)
|
||
|
||
```csharp
|
||
public sealed class BridgeProcessManager {
|
||
public static ProcessStartInfo BuildBridgeStartInfo(string exe, string traySecret, string backendUrl, string dataDir, int port);
|
||
public Process Start(string exe, string traySecret, string backendUrl, string dataDir, int port);
|
||
public bool IsBridgeProcessAlive(int pid);
|
||
public void KillProcess(int pid);
|
||
public void WaitForExit(int pid, TimeSpan timeout);
|
||
}
|
||
```
|
||
|
||
**Tray 启动顺序**:
|
||
1. 启 backend,等 `/status.ready`
|
||
2. 启 bridge,等 `GET bridge/health` 返回 200
|
||
3. Bridge `POST /handshake` → backend 标记 channel `loginStatus='connected'`
|
||
|
||
**崩溃自愈**:按 `BackendProcessManager` 同样的 `IsBridgeProcessAlive` 轮询;最多 3 次重启(30 秒间隔),超限后 tray 气泡通知用户。
|
||
|
||
### 安装包分发
|
||
|
||
Neta 已有 Windows 安装包构建脚本 `packages/backend/scripts/build-windows-installer.js`。改动:
|
||
- `dotnet publish -c Release -r win-x64 --self-contained=false` 发布 bridge.exe(依赖系统 .NET 8 运行时,与 Tray 一致)
|
||
- 安装包把 `bridge.exe` 放到 `{installDir}/bin/bridge/bridge.exe`
|
||
- Tray 启动时按固定相对路径找 bridge
|
||
- 升级时覆盖 bridge,**不动** `%APPDATA%/Neta/` 下的用户数据
|
||
|
||
### 运行时数据目录
|
||
|
||
```
|
||
%APPDATA%/Neta/
|
||
├── runtime/tray-info.json ← 已有
|
||
├── wechat-archive-<cid>.db ← 新增 SQLite
|
||
├── wechat-uploads/<cid>/<YYYY-MM>/*.{jpg,mp4,...} ← 新增附件缓存
|
||
└── logs/bridge.log ← 新增 bridge 日志
|
||
```
|
||
|
||
### 版本白名单 profile
|
||
|
||
`Config/VersionProfiles.yaml` 形如:
|
||
|
||
```yaml
|
||
profiles:
|
||
- version: "3.9.11.17"
|
||
mainWindowClass: "WeChatMainWndForPC"
|
||
sessionListName: "会话"
|
||
messageListName: "消息"
|
||
searchBoxName: "搜索"
|
||
inputBoxName: "输入"
|
||
imageCacheDir: "FileStorage/Image/{YYYY-MM}"
|
||
- version: "3.9.12.x"
|
||
inherit: "3.9.11.17" # 继承上一个 profile,仅覆盖差异
|
||
```
|
||
|
||
Bridge 启动时读 PC 微信 `WeChat.exe` 的 `FileVersion`,匹配 profile。未匹配 → exit(1) + tray 气泡通知"该微信版本未经适配"。
|
||
|
||
### 监控与诊断
|
||
|
||
- Bridge `GET /diag`(需 tray-secret)返回:
|
||
- PC 微信版本号
|
||
- 当前会话列表数量 / 已启用群数
|
||
- 最近切窗耗时 p50/p95
|
||
- 队列长度
|
||
- 最近 5 条 UIA 错误堆栈
|
||
- Tray 菜单"查看日志"打开 `%APPDATA%/Neta/logs/bridge.log`
|
||
- 前端"频道管理 → weixin-uia 卡片"点击可调 `/diag` 展示运维信息
|
||
|
||
### 测试策略
|
||
|
||
- **Neta.WeChatBridge.Tests**(xUnit)
|
||
- 纯逻辑单测:消息 hash 去重、队列合并、版本 profile 匹配、YAML 解析
|
||
- UIA 集成测试:CI 环境没 PC 微信 → 整体 skip;只在本地开发者机器跑
|
||
- **端到端手工验证清单**(对齐 spec 最后节)
|
||
|
||
---
|
||
|
||
## 与今天 2026-05-08 实施代码的关系
|
||
|
||
### 完全复用(零修改)
|
||
|
||
| 模块 | 复用内容 |
|
||
|---|---|
|
||
| `runtime/chat_scope.ts` | `decideChatScope` / sessionId 构造器 / dispatchKey / clarify key / `decideGroupAcceptance`(`prefix` 分支保留不删,仅前端不暴露)/ `detectAtMention` / strippers |
|
||
| `runtime/errors.ts` | `GroupInteractionDeclineError` + `withClarifyTimeout` |
|
||
| `runtime/tool_execution_metadata.ts` | 纯函数完全无关 |
|
||
| `service/agent_executor.ts` | `chatScope` + `GroupInteractionDecline` short-circuit 全部不动 |
|
||
| `service/agent_channel.ts` 的 `routeInboundMessage` / `handleInboundMessage` 主体 / senderQueue / pendingClarify 逻辑 | 框架完全不动,仅在 `routeInboundMessage` 入口处加 channel.type 分流 switch |
|
||
| `service/agent_channel_group.ts`:upsertOnInbound / toggle / updatePolicy / list / cascadeDeleteByChannel / statsByChannels | 不动 |
|
||
| `controller/admin/agent_channel_group.ts`(list/toggle/updatePolicy/rename/delete) | 不动 |
|
||
| 前端 `channel-group-panel.vue` 主体结构 | 不动 |
|
||
| 前端 `agent/views/chat.vue` 深链接 + `buildFallbackTitle` | 不动 |
|
||
|
||
### 小改(增量扩展)
|
||
|
||
| 模块 | 改动 |
|
||
|---|---|
|
||
| `entity/agent_channel_group.ts` | 加可选字段 `boundAgentId: number \| null` / `replyIdentityOverride: string \| null` |
|
||
| `service/agent_channel_group.ts.updatePolicy` | triggerMode 合法集合改为 `at_mention` / `all`;`prefix` 调用返回 400(保留字段语法以兼容存量数据) |
|
||
| `service/agent_channel.ts.runLoop` | 按 channel.type 分流:`weixin` 走现有 iLink 逻辑;`weixin-uia` 不起 runLoop(由 bridge 主动 POST 触发) |
|
||
| `service/agent_channel.ts.handleInboundMessage` | + `replyIdentity` 包装回复文字;+ `boundAgentId` 覆盖 default agent;+ agent 返回 `[SKIP]`/空串时跳过 sendText |
|
||
| `service/agent_channel.ts` | DM/group 反向丢弃约束(UIA 拒收 DM;ClawBot 拒收群) |
|
||
| 前端 `channel-management.vue` | type 下拉加 `weixin-uia`;drawer 字段按 type 动态渲染;UIA 渠道隐藏扫码按钮;新增 bridge 状态 tag |
|
||
| 前端 `channel-group-panel.vue` | + 回复身份下拉;+ 绑定 agent 下拉;+ "待审批" 横幅 + "忽略" 按钮 |
|
||
|
||
### 全新增
|
||
|
||
| 模块 | 用途 |
|
||
|---|---|
|
||
| `packages/windows-tray/Neta.WeChatBridge/` 整个项目 | .NET 8 UIA 桥接进程 |
|
||
| `packages/windows-tray/Neta.WeChatBridge.Tests/` | bridge 单测 |
|
||
| `Neta.Tray/BridgeProcessManager.cs` | 拉起 / 监控 bridge |
|
||
| `service/weixin_uia.ts` | Node 侧 bridge HTTP 客户端(send / rooms / health) |
|
||
| `service/wechat_archive.ts` | SQLite 归档服务 |
|
||
| `entity/wechat_archive_entry.ts`(或 SQL DDL) | SQLite archive 表定义 |
|
||
| `controller/admin/wechat_archive.ts` | 前端查看归档 / 筛选 / 导出 |
|
||
| `controller/open/weixin_uia.ts` | bridge → backend 的 inbound / handshake / room-list-snapshot |
|
||
| 前端 `components/wechat-archive-panel.vue` | 归档查看抽屉 |
|
||
|
||
### 今天 task 已验证资产保留
|
||
|
||
- 117/117 单测保持有效(增量改动不破坏)
|
||
- iLink 渠道部分的 SQL migration 保留
|
||
- 已部署到测试库的 `netaclaw_agent_channel_group` 表保留,仅 `ALTER TABLE ADD COLUMN` 加 2 个可选字段
|
||
|
||
### 改造工作量粗估
|
||
|
||
| 阶段 | 内容 | 工作量 |
|
||
|---|---|---|
|
||
| Phase A | Neta.WeChatBridge 项目骨架 + UIA 消息读取 POC | 3-5 天 |
|
||
| Phase B | Bridge 完整能力(多群切窗 / 图片 / 发送 / 身份) | 5-7 天 |
|
||
| Phase C | Backend 适配(controller + weixin_uia.ts + wechat_archive) | 3 天 |
|
||
| Phase D | 前端扩展(type 下拉 / 每群绑 agent / 待审批 / 归档页) | 3 天 |
|
||
| Phase E | Tray 拉起 bridge + 生命周期 + 安装包 | 2 天 |
|
||
| Phase F | 端到端手工验证清单 + 版本白名单 profile | 2-3 天 |
|
||
| **合计** | | **约 3-4 周(一个全职工程师)** |
|
||
|
||
### MVP 降级方案
|
||
|
||
如果 Phase B 的"图片 DAT 解密"或 Phase C 的"SQLite 归档"成本超预期,可砍 MVP:
|
||
- 仅文本消息(图片记 metadata 但 attachmentPath 留空,UI 显示"⚠️ 图片不可用")
|
||
- 仅 MySQL session_entry(SQLite 归档推迟到 v2)
|
||
|
||
MVP 覆盖 80% 核心需求,后续追加。
|
||
|
||
---
|
||
|
||
## 验证
|
||
|
||
### 单元测试
|
||
|
||
| 文件 | 覆盖 |
|
||
|---|---|
|
||
| `Neta.WeChatBridge.Tests` | 消息 hash 去重 / 队列合并 / 版本 profile 匹配 / YAML 解析 / DAT-XOR 解密单测(含已知向量) |
|
||
| `test/modules/netaclaw/runtime/chat_scope.test.ts`(已有)| triggerMode='all' + agent 返回 [SKIP] 不发送 |
|
||
| `test/modules/netaclaw/service/agent_channel.uia.test.ts`(新增) | UIA 渠道 routeInboundMessage 分流;UIA 拒 DM;ClawBot 拒 group;replyIdentity 包装;boundAgentId 覆盖 |
|
||
| `test/modules/netaclaw/service/wechat_archive.test.ts`(新增) | SQLite 写入 / 查询 / 状态回写 |
|
||
| `test/modules/netaclaw/service/weixin_uia.test.ts`(新增) | bridge HTTP 客户端 mock;超时 / 重试 / 鉴权 header |
|
||
|
||
### 端到端手工验证(部署到 Windows 测试机)
|
||
|
||
1. 装 PC 微信 3.9.11.17 + 登录测试号
|
||
2. 装 Neta Windows 包,Tray 启动 → backend + bridge 都拉起 → frontend 频道页可见
|
||
3. 创建 weixin-uia channel → bridge handshake → 看到 wxid/nickname 自动填充
|
||
4. 在测试群里说一句"hello"(任何成员)→ bridge 检测到事件 → 切窗采集 → 上报 backend → 前端"待审批"横幅出现该群
|
||
5. 启用监听 + 选 `at_mention` + 填 botAlias=小神
|
||
6. 群里发 `@小神 你好` → agent 回复 → bot 在群里发"已收到,请问需要什么帮助" → 用户看到的发送方就是测试号自己
|
||
7. 切到 `all` 模式 → 群里随便说 → agent 通过 system prompt 判定相关性 → 不相关时返回 [SKIP] 不发
|
||
8. 群里发图 → SQLite 落归档 → 前端归档抽屉能预览到图片
|
||
9. 关掉 PC 微信 → bridge `/health` 失败 → channel 状态变 disconnected
|
||
10. 重开 PC 微信 → bridge 自愈 → channel 重连
|
||
11. 故意装 PC 微信 4.x(不在白名单)→ bridge 启动失败 → tray 气泡通知"版本不兼容"
|
||
12. 一个微信号同时绑 ClawBot channel + UIA channel:DM 走 ClawBot、群走 UIA,不重复响应
|
||
13. 删 channel → group 表级联清;SQLite 归档保留(按设计是否随删可在 spec 决策——当前**不级联删 archive**,作为审计记录)
|
||
14. 群被改名 → 视为新群,又出现在"待审批"
|
||
|
||
### 可观测性
|
||
|
||
新增日志:
|
||
- `[UIA-Bridge] handshake channelId=%s wxid=%s version=%s`
|
||
- `[UIA-Bridge] room discovered roomName=%s`
|
||
- `[UIA-Bridge] window switch roomName=%s elapsedMs=%d`
|
||
- `[UIA-Bridge] attachment extracted msgId=%s path=%s`
|
||
- `[AgentChannel] uia inbound channelId=%s roomName=%s sender=%s msgType=%s`
|
||
- `[AgentChannel] uia agent skipped reason=empty_or_skip channelId=%s roomId=%s`
|
||
- `[AgentChannel] uia bridge offline channelId=%s consecutiveFailures=%d`
|
||
|
||
---
|
||
|
||
## 非目标 / 不在本 spec 范围
|
||
|
||
- **WeChatFerry / DLL 注入**:本 spec 不引入注入式 hook 路径。如未来需要更高性能(毫秒级,不切窗),再单起 spec
|
||
- **iPad/Mac 协议模拟(PadLocal/Gewechat)**:付费服务/灰色项目,不纳入
|
||
- **企业微信 WeCom**:协议不允许接入个人微信群,不纳入
|
||
- **批量发图/文件到群**:MVP 限定 agent 回复纯文本
|
||
- **语音消息处理**:v2 才做(UIA 模拟右键"转文字"或本地 ASR)
|
||
- **跨群人物志合并**:v2 才做
|
||
- **同时多账号**:MVP 一个 bridge 实例 = 一个 PC 微信 = 一个微信号;多账号需多机器
|
||
- **Bridge 主动建群 / 邀请成员**:永远不做(伦理 + 微信风控)
|
||
- **Computer Use 视觉路径**(LLM 看截图操作鼠标):性能差、成本高,不纳入
|
||
- **WebView 协议(已被微信清理)**:不可行
|