gengx
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
# 2026-08-21 工作日志
|
||||
|
||||
## 企业微信自动回复第二套方案(引擎 B 数据直读)落地
|
||||
|
||||
### 背景
|
||||
用户确认 4 个拍板点:发送层 GUI 复用、数据来源两条线并行、机器人对接复用 ai_chat、互斥共存。
|
||||
|
||||
### 新建文件
|
||||
- `send_lock.py`:发送互斥锁。Windows 命名互斥量(`Global\WeChatBotSendLock`)+ 线程持有者跟踪(防同线程重入);非 Windows 回退 threading.Lock。`try_acquire(owner, timeout)` / `release()` / `lock()` 上下文管理器。
|
||||
- `engine_b.py`:引擎 B(数据直读)。`DataEngine` 类独立线程轮询 `conversations.json`,按 history 增量发现新客户消息,经 `bot.enqueue_detected()` 投递共享队列。启动时 120s 内新消息算新鲜可投,防抖窗口 300s。纯检测纯投递,不碰窗口。
|
||||
- `test_dual_engine.py`:双引擎集成测试(31 项断言),覆盖互斥锁/投递/字段透传/去重/合并/DataEngine 集成。回归命令:系统 Python 3.11 跑(wechat_bot 依赖 win32gui,managed python 无此包)。
|
||||
|
||||
### 修改文件
|
||||
- `wechat_bot.py`:
|
||||
- 队列条目新增 5 字段:`dedup_key`/`detected_by`/`detection_ts`/`sending_owner`/`send_lock_ts`,在 `_load_pending_replies` 与 `_persist_pending_replies_unlocked` 两处白名单透传(旧条目自动补默认值,44 字段兼容验证通过)。
|
||||
- `send_reply()` 改为互斥包装:`try_acquire` 拿不到锁 → `_deny_send("另一引擎正在发送")`;原主体改名 `_send_reply_inner`。
|
||||
- 新增 `enqueue_detected()`(B 引擎投递,复用 `_mark_reply_pending` + 补新字段 + display_name 兜底)、`has_active_pending()`(终态集合判断去重)、`merge_detected_by()`、`_merge_engine_tags()`。
|
||||
- `gui_runtime.py`:`BotThread.__init__` 加 `enable_engine_b`/`engine_b_poll_interval`;`run()` 中 bot connect 成功后启动 DataEngine 线程,finally/stop 中停止。
|
||||
- `wechat_gui_qt.py` / `wechat_gui.py`:runtime 默认值加 `enable_engine_b=True`、`engine_b_poll_interval=2.0`,start_monitoring 传参。
|
||||
- `app_settings.json`:加 `enable_engine_b: true`、`engine_b_poll_interval: 2.0`。
|
||||
|
||||
### 关键架构决策
|
||||
- **B 引擎不自己发送**:队列条目由引擎 A 的 `_resume_orphaned_pending_reply` 统一捡起(重新定位→视觉提取→`_generate_ai_reply`→`send_reply`)。发送天然收敛到唯一通道 + 互斥锁,B 全程不碰窗口。这是"检测解耦、发送收敛"的最稳落地。
|
||||
- 检测到 A 已处理的会话时 B 只补 detected_by 标记,不重复投递。
|
||||
- 队列字段是白名单读写,新增字段必须在 load/persist 两处透传,否则 B 引擎的标记会被 A 持久化时抹掉。
|
||||
|
||||
### 验证
|
||||
- `test_dual_engine.py`:31 项全部通过。
|
||||
- `test_runtime_settings.py` 18 项、`test_session_name.py` 67 项回归通过。
|
||||
- 旧 pending_replies.json 条目加载兼容(新字段自动补默认值)。
|
||||
|
||||
---
|
||||
|
||||
## Phase 2:企微本地库解密(密钥提取攻坚)
|
||||
|
||||
### 关键发现(本轮)
|
||||
1. **真正的 bug 根因**:`wxwork_key.py::_prep_verify` 的验证基准构造错误——读了文件 offset 16..32(含明文头),正确应为 `8..16 密文A + 24..32 密文B`(与 `wxwork_crypto.decrypt_page1_block` 一致)。修复后探针测试从"0 命中"变为"命中"。
|
||||
2. **算法确认**:`WXWork.exe`(269MB,`D:\soft\WXWork\`,版本 5.0.10.6015)代码段含 `sAlT` 常量 + MD5 初始常量 `0x67452301` 的机器码(`mov [ebp-0xC],0x546C4173` / `mov [ebp-0x7C],0x67452301`)→ 证实 wxSQLite3 aes128cbc MD5+sAlT 派生算法判断正确。
|
||||
3. **扫描器性能优化**:`regions(rw_only=True)` 只扫可写区 + `len(set(w))>=8` 字节多样性预过滤 + `_fast_verify` 单变体优先验证(AES 3→1 次)。探针扫描 87.7s → 3.4s。
|
||||
4. **扫描覆盖全部**:16/32 字节窗口 × 16/8 字节对齐 × 全部企微进程(含 WeChatAppEx.exe 8 实例、WXDrive_x64.exe、WXWorkXNet.exe),3 轮全 0 命中 → **结论:企微 5.0.10 数据库密钥不在常驻可读内存**(用完即焚/受保护页/混淆存储)。
|
||||
5. 当前进程非管理员权限(IsUserAnAdmin=False)。
|
||||
|
||||
### 新建/修改文件
|
||||
- `wxwork_key.py`:修复 `_prep_verify`;`regions()` 加 `rw_only` 参数;`scan_binary_single` 加 `key_len`/`step` 参数;`scan_binary_all` 支持 step;`find_wxwork_pids` 加 `include_extra`(WeChatAppEx/WXDrive);CLI 加 `--binary8`(8 字节对齐)。
|
||||
- `test_scan_probe.py`:修复子进程 code 路径 `\U` 转义(用 `!r`),加子进程存活检查;改为 aligned 优先。
|
||||
- `test_scan_probe_mis.py`(新):非对齐密钥探针,验证全滑窗兜底。
|
||||
- `scan_golden_window.py`(新):重启企微黄金窗口多轮扫描脚本(`--kill` 需用户确认)。
|
||||
- `probe_wxwork_db.py`:已就绪(等密钥到手探查 message.db 表结构)。
|
||||
|
||||
### 下一步(待用户决策)
|
||||
- A. 黄金窗口扫描:重启企微后立即多轮扫(`scan_golden_window.py`,需用户同意重启)
|
||||
- B. 管理员权限重跑全部扫描
|
||||
- C. hook sqlite3_key(看雪方案,注入拦截,开发量大)
|
||||
- D. 换会话存档 SDK 路线(`Downloads\wxwork-finance-sdk-main`,企业后台 API)
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 落地:参考目录代码 → DB 直读(密钥已验证,主线打通)
|
||||
|
||||
### 背景
|
||||
用户提供参考目录 `C:\Users\pc\WorkBuddy\2026-08-19-18-27-34\`(企业微信导出助手,密钥已验证),要求以它为基准实现 DB 直读,保留密钥逻辑、复用可靠模块。
|
||||
|
||||
### 参考目录关键资产(已逐文件分析)
|
||||
- `wxwork_keys.json`:2 个账号验证过密钥(`1688856770803435`/`db7288a302bfbe66563d81f025bde977`、`1688855555743233`/`d18e7f9a222665389989066f929ece19`)→ **密钥来源问题解决,内存提取不再必要**
|
||||
- `wxwork_crypto.py`:解密算法与本项目 `wxwork_crypto.py` 完全一致(互相印证)
|
||||
- `wxwork_find_key.py`:7 种内存密钥提取模式(参考项目曾在运行中企微成功提取,但本项目 5.0.10 提取不到)
|
||||
- `wxwork_export_final.py`:`parse_content`(UTF-8→protobuf 递归→JSON→hex)、`MSG_TYPE_MAP`、`is_personal_chat`(M:/S: 白名单)、`clean_text`、`format_timestamp`、`load_keys`(支持 `*` 通配)、`decrypt_with_keys`(WAL 感知增量缓存)、`detect_wxwork_dir`、会话名解析
|
||||
- `wxwork_export_db.py`:`collect_metadata`(user_table/conversation_table → user_cache/conv_cache)
|
||||
- `crm_import/decrypted/`:9 账号目录全空 → 印证仅 2 账号有密钥
|
||||
|
||||
### 新建/修改文件
|
||||
- `wxwork_keys.json`(新):复制参考密钥,含 note 说明"每账号独立密钥,未列出账号未登录"
|
||||
- `wxwork_db.py`(重写):
|
||||
- 移植参考全套:`load_keys`/`decrypt_with_keys`/`parse_content`/`extract_protobuf_texts`/`clean_text`/`MSG_TYPE_MAP`/`get_msg_type_name`/`is_personal_chat`/`is_blocked_conv_name`/`format_timestamp`/`detect_wxwork_dir`/`connect_sqlite`/会话名解析(user_cache/conv_cache/S: 对端推导)
|
||||
- **WAL 帧合并增强**(参考实现没有):企微运行时新消息在 message.db-wal,参考只靠 wal_mtime 触发重解密主库(仍缺 WAL 内消息);本项目直接解密 WAL 帧(同算法、帧头明文、salt 匹配过滤、后帧覆盖)按页号合并进主库副本,更新 SQLite 头页数字段,`PRAGMA quick_check` 验证(`test_wal_merge.py`)
|
||||
- **会话指纹增强**:`session_fp_from_name` 用与 `wechat_bot._fp_from_name` 完全一致的算法(md5+blake2b,40 字节)→ DB 直读 fp_hex 与引擎 A identity_by_name 模式同指纹空间
|
||||
- `normalize_name` 仅标准库实现(NFKC+去空白),避免 import session_name(其顶层依赖 numpy)
|
||||
- `WXWorkDB` 类:构造时解密缓存+元数据,`get_new_messages(since_ts)` 秒级 send_time 增量查询,`_refresh_if_needed` 按 mtime 自动重解密
|
||||
- `engine_b.py`(改造):`DataEngine` 加 `db_source` 参数(WXWorkDB 实例);`_run` 有 DB 则 `_poll_db_once`;DB 直读失败自动回退 `poll_once`(conversations.json),下轮自动重试恢复;DB 路径**按 fp_hex 聚合**只投递每会话最后一条(防历史逐条误投);初始游标 = now-120s(与启动新鲜窗口一致);`_process_entry` 提取为两条路径共用
|
||||
|
||||
### 验证结果
|
||||
- `wxwork_db.py` 冒烟:2 账号可读(`1688856770803435`+`1688855555743233`)、1041 条单聊消息、health_check 通过
|
||||
- 增量查询命中真实消息:今天 11:06:05「一个小迷糊: 在不」
|
||||
- 指纹一致性:`' 高瑞 @微信 '` 的 fp 前 16 字节与 conversations.json 档案 key 完全一致 → 引擎 A 正运行在 identity_by_name 模式
|
||||
- WAL 合并:quick_check ok(当前 WAL 无更新帧,机制已验证)
|
||||
- 引擎 B 真实集成:FakeDB 聚合单测通过(多历史只投最新一条/self 过滤/去重/降级回退)
|
||||
- `test_dual_engine.py`:31 项全部通过(系统 Python 3.11 跑)
|
||||
- 密钥匹配验证:仅 2 账号可解密,其余 9 账号无密钥(与参考项目空目录印证)
|
||||
|
||||
### 关键结论
|
||||
- 密钥确认在主库页 1 可验证(`verify_key` 0.03s),企微运行时 WAL 帧同算法加密可合并 → 实时性方案成立
|
||||
- DB 直读 fp_hex 与引擎 A 档案同指纹空间 → 投递后可被引擎 A 无缝定位窗口回复
|
||||
- 回归命令:managed venv(`C:\Users\pc\.workbuddy\binaries\python\envs\default\Scripts\python.exe`)跑 wxwork_db/engine_b;系统 Python 3.11 跑 test_dual_engine(wechat_bot 依赖 win32gui)
|
||||
|
||||
---
|
||||
|
||||
## 引擎 B 数据源改为「两路并行双跑」(用户拍板)
|
||||
|
||||
### 背景
|
||||
用户质疑"是否有两套方案兜底,而不是合到一起了"。原实现 DB 与 JSON 是**主备二选一**(`_run` 有 DB 只跑 `_poll_db_once`,DB 异常才 `return self.poll_once()` 降级)——DB 正常时 JSON 路径完全不执行,存在单点盲区。用户选择改为**并行双跑**。
|
||||
|
||||
### 修改(engine_b.py)
|
||||
- `_run`:每轮无条件调用 `_poll_db_once()` + `poll_once()`(两路独立检测,互为兜底)
|
||||
- 游标分离:新增 `_seen_db`(DB 路径会话游标),`_seen` 只归 JSON 路径;`_process_entry` 加 `seen` 参数(DB 传 `_seen_db`,JSON 用默认),两路独立建档/判定、互不污染
|
||||
- `_poll_db_once`:去掉降级 return,DB 失败仅 `db_active=False` 并打印,JSON 路径不受影响
|
||||
- 去重统一走 `_maybe_enqueue` 的 dedup_key(fp_hex:时间戳,300s 防抖 + 队列级去重)——同一条消息两路都发现也只投一次
|
||||
|
||||
### 验证
|
||||
- 引擎 B 冒烟通过;并行单测全部断言通过:同消息只投一次 / 两路各自增量 / 游标独立 / DB 崩溃不影响 JSON
|
||||
- 真实 DB 并行集成:db_active=True、0 重复投递
|
||||
- `test_dual_engine.py`:31 项全过
|
||||
|
||||
### 架构语义(最终形态)
|
||||
引擎 A(截图 RPA)与引擎 B(数据直读)两套独立检测通道;引擎 B 内部 DB 直读 + conversations.json **并行双跑**互为兜底;发送统一收敛引擎 A send_reply + send_lock 互斥。
|
||||
|
||||
---
|
||||
|
||||
## GUI 引擎切换卡片 + 全量回归(交付)
|
||||
|
||||
### GUI 改动(wechat_gui_qt.py)
|
||||
- `DashboardPage` 加「检测引擎」卡片:引擎 A 开关(`engine_a_check`)、引擎 B 开关(`engine_b_check`)、引擎 B 数据源三选一(`QRadioButton` 并行双跑/仅DB/仅JSON);引擎 B 关闭时数据源单选组置灰(`_sync_engine_b_source_enabled`)
|
||||
- 信号:复用已有 `settingsChanged` 信号 → `MainWindow._on_dashboard_engine_settings`(合并进 runtime_settings + 持久化)
|
||||
- 键名统一为 **`engine_a_enabled`**(注意不是 `enable_engine_a`!BotThread 参数、_runtime_defaults、_load_runtime_settings 校验、start_monitoring 传参、DashboardPage 读写全部用 `engine_a_enabled`);`enable_engine_b`、`engine_b_data_source` 不变
|
||||
- `_load_runtime_settings` 校验:`engine_a_enabled`/`enable_engine_b` 转 bool、`engine_b_data_source` 白名单(非法回落 parallel)
|
||||
|
||||
### 运行时改动(gui_runtime.py)
|
||||
- `BotThread.__init__` 加 `engine_a_enabled=True`、`engine_b_data_source="parallel"`(含合法性回落)
|
||||
- 引擎 B 启动段:按数据源模式决定是否接 DB(parallel/db 才初始化 WXWorkDB,失败仅记日志 db_source=None)
|
||||
- 主循环:`bot._poll_once(scan_unread=self.engine_a_enabled)`(引擎 A 关闭→跳过红点扫描,只留队列恢复,B 投递仍能发送)
|
||||
|
||||
### 修复的真实 bug
|
||||
- `engine_b.py::_poll_db_once`:DB 故障恢复后若首轮查询为空,`db_active` 永不回 True(`if not msgs: return` 跳过置位)→ 查询成功即置 `db_active=True`(空返回前)
|
||||
|
||||
### 验证
|
||||
- 语法:5 核心模块 py_compile 通过
|
||||
- GUI 卡片冒烟(`tmp/qt_engine_card_smoke.py`,offscreen):18 项全过(初始态/信号键集合/radio 置灰联动/设置加载校验/合并保存持久化)
|
||||
- 引擎 B 三模式单测(`tmp/test_engine_b_modes.py`):14 项全过(json 只跑 JSON、db 只跑 DB、db 无源退化 JSON、parallel 双跑、DB 异常隔离+恢复、非法模式回落)
|
||||
- 回归:`test_dual_engine.py` 31 项、`test_runtime_settings.py` 18 项、`test_qt_console_bridge.py` 14 项、`test_qt_capsule.py` 19 项全过
|
||||
- **测试适配**:`test_runtime_settings.py` 的 mock `poll_once()` 改为 `poll_once(scan_unread=True)`(gui_runtime 新调用契约)
|
||||
|
||||
### 运行语义提示
|
||||
- GUI 改引擎开关:运行中修改→保存文件,**本轮监听结束后重启才完全生效**(BotThread 启动时读取)
|
||||
- 引擎 A 关闭 ≠ 不回复:B 投递的消息由 A 的队列恢复通道照常定位发送,只是不再截图扫红点
|
||||
|
||||
---
|
||||
|
||||
## 修复:视觉定位"卡死"(bot 线程假死)
|
||||
|
||||
### 现象
|
||||
用户截图显示任务卡在"正在视觉定位"(opening),日志在 `_find_next_unread_session` 检测到总未读后完全中断,bot 线程约 4-5 分钟无响应。
|
||||
|
||||
### 根因
|
||||
`_scroll_session_list_page` 滚动到底后,由于时间戳/动画等微小像素差异,`_session_page_signature` 认为页面不同,返回 `after` 而非 `None`。导致:
|
||||
- `_find_pending_session` / `_find_next_unread_session` 持续翻页扫描 240 页(`SESSION_SCAN_MAX_PAGES`)或 45 秒超时
|
||||
- 4 个 pending 恢复任务 × 45 秒 = 3 分钟+/轮
|
||||
- 每轮结束后冷却期短(`SESSION_SCAN_COOLDOWN_SECONDS=15s`,失败按 `2^failures` 退避),任务反复被扫描
|
||||
- 日志长时间无输出,GUI 状态停滞,用户感知为"卡死"
|
||||
|
||||
### 修复(wechat_bot.py)
|
||||
1. `_session_pages_overlap_at_shift`:放宽 `shift` 边界检查从 `<=0` 到 `<0`,允许 `shift=0` 检测(同页内容完全匹配)
|
||||
2. 新增 `_any_scroll_overlap(before, after)`:在 `0 ~ 2*item_h` 范围内遍历合理 shift,检测是否存在 >=90% 的内容重叠
|
||||
3. `_scroll_session_list_page`:签名比较后增加 `_any_scroll_overlap` 兜底——如果签名不同但无有效滚动重叠,返回 `None`(认为到底),阻止无效翻页
|
||||
|
||||
### 验证
|
||||
- 语法 py_compile 通过
|
||||
- `test_dual_engine.py` 31 项、`test_engine_b_modes.py` 14 项全过
|
||||
- `tmp/test_scroll_deadlock_fix.py`:
|
||||
- 同页 shift=0 返回 True ✓
|
||||
- 时间戳微差(签名不同但无有效滚动)→ `_any_scroll_overlap` 返回 True → `_scroll_session_list_page` 会返回 None ✓
|
||||
- 真实滚动 72px → `_any_scroll_overlap` 返回 True,滚动仍被识别 ✓
|
||||
|
||||
### 影响
|
||||
- 修复后:滚动到底立即返回 None,不再做 45 秒无效扫描
|
||||
- 极少量场景(动画/加载导致大面积像素变化)可能提前结束扫描,遗漏极底部未读——但相比卡死 3 分钟+,可接受;下一轮轮询会重新扫描
|
||||
|
||||
---
|
||||
|
||||
## 修复:队列任务永远"处理中"(快速放弃机制)
|
||||
|
||||
### 现象
|
||||
用户截图:3 个任务(一个小迷糊@微信/在线问诊/高瑞)全部 stage=opening、resume_failures=7/3/2,监听停止后 GUI 仍显示"处理中"。用户诉求:AI 回复完成就应移除任务,除非有新消息。
|
||||
|
||||
### 根因
|
||||
- 发送成功(sent)任务本就会 `_clear_reply_pending` 移除——但**卡在 opening 的任务没有快速放弃机制**
|
||||
- `_UNREACHABLE_RESUME_FAILURES=8`:纯定位任务要失败 8 次才丢弃,加上 `2^failures` 指数退避(最长 600s),最坏情况任务挂几小时
|
||||
- 已生成 AI 回复(staged_reply_text 非空)却发不出去的任务,回复有时效性,继续挂着毫无意义
|
||||
|
||||
### 修复(wechat_bot.py)
|
||||
1. 新增 `_STAGED_REPLY_EXPIRE_FAILURES = 3`:有 staged_reply_text 的任务失败 3 次即放弃(回复未发出、不落档,客户新消息会重新建单)
|
||||
2. `_expire_unreachable_pending` 按是否有 staged_reply_text 选阈值:有回复→3,纯定位→8(保留原语义,防企微暂未开误删)
|
||||
3. 放弃时打印区分原因(有过时回复 / 找不到会话)
|
||||
|
||||
### 验证
|
||||
- 语法 py_compile 通过
|
||||
- `tmp/test_pending_expire.py` 7 项全过(staged 3 次放弃/2 次保留、纯定位 8/7 边界、放弃不落档、非法值与空白回复边界)
|
||||
- 回归:`test_dual_engine.py` 31 项、`test_engine_b_modes.py` 14 项、`test_runtime_settings.py` 18 项全过
|
||||
|
||||
### 用户队列现状(修复后重启监听即生效)
|
||||
- 一个小迷糊@微信(failures=7,有回复)→ 下次失败即达 3 → 放弃
|
||||
- 在线问诊(failures=3)→ 立即达阈值 → 放弃
|
||||
- 高瑞(failures=2)→ 再失败 1 次 → 放弃
|
||||
|
||||
---
|
||||
|
||||
## 任务池闭环验证(发送成功移除 + 新消息重新投递)
|
||||
|
||||
用户确认诉求:AI 回复成功→任务移除;对方新消息→算新任务重新入队,任务池顺畅。
|
||||
|
||||
### 闭环语义(全部已确认成立)
|
||||
- **发送成功(sent)→ `_clear_reply_pending` 移除任务**(`_resume_orphaned_pending_reply` 发送对账路径)
|
||||
- **新消息 = 新 dedup_key**:`dedup_key = fp_hex:消息ts`,DB 消息在 `_poll_db_once` 归一化为 `{"content", "ts"}`(ts=send_time),新消息 ts 不同 → 不撞防抖
|
||||
- **`_recently` 300s 防抖只挡同一条消息**(同 dedup_key),新消息不受影响
|
||||
- **`has_active_pending` 对终态(sent/cancelled/expired/cleared)返回 False** → 任务移除后引擎 B 可重新投递
|
||||
- **队列已有活跃任务时引擎 B 只补 detected_by 标记不重复投递**
|
||||
|
||||
### 验证
|
||||
- `tmp/test_queue_loop.py` 10 项全过:投递→防抖→发送移除→新消息重新投递→二次防抖→活跃不重复
|
||||
- 注意:测试数据字段用 `ts`(不是 `send_time`),`_msg_ts` 读 `msg["ts"]`
|
||||
- 回归:`test_dual_engine.py` 31、`test_engine_b_modes.py` 14、`test_pending_expire.py` 7 全过
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# 项目长期记忆 — 企业微信 RPA 自动化
|
||||
|
||||
## 项目背景
|
||||
- 企业微信(WeCom)消息自动回复,核心:红点检测 → 会话定位 → 提取文本 → AI 回复 → GUI 发送。
|
||||
- 企微用自定义 GPU 渲染(UIA 节点=0),**无法**用 UIAutomation/win32gui 内部控件查询,只能截图 + 坐标。
|
||||
- 本地库(message.db 等)为 wxSQLite3 加密(文件头 `4fa4c7db855d54ad`),需内存提密钥 + 解密,普通 sqlite3 打不开。
|
||||
- 主 GUI:`wechat_gui_qt.py`(PySide6),后台运行时:`gui_runtime.py`(BotThread),核心:`wechat_bot.py`(13664 行,WeChatBot 类)。
|
||||
- AI 统一入口:`ai_chat.get_ai_reply(chat_text, image_bytes, history, *, force_vision, media_types)`,Dify 提供商(gpt-5.6-sol),`ai_settings.json` 配置。
|
||||
|
||||
## 双引擎架构(2026-08-21 落地)
|
||||
- **引擎 A**(截图 RPA):红点识别 → 打开会话 → 视觉提取 → `_generate_ai_reply` → `send_reply`。发送收敛点。
|
||||
- **引擎 B**(数据直读,`engine_b.py`):**两路并行双跑、互为兜底**——① DB 直读(升级数据源)② `conversations.json`(引擎 A 视觉回写档案)每轮都独立检测;任一数据源有盲区另一路补漏,同一条消息靠 dedup_key 去重(fp_hex:时间戳,300s 防抖+队列级去重)只投一次 → `bot.enqueue_detected()` 投共享队列 → 由引擎 A 的 `_resume_orphaned_pending_reply` 统一处理。B 不碰窗口。两路游标独立:`_seen`(JSON)/`_seen_db`(DB),DB 失败仅停 DB 路(db_active=False)。
|
||||
- **DB 直读**(`wxwork_db.py`,Phase 2 主线,密钥已验证):密钥在 `wxwork_keys.json`(2 账号:`1688856770803435`→`db7288a302bfbe66563d81f025bde977`、`1688855555743233`→`d18e7f9a222665389989066f929ece19`,其余 9 账号无密钥/未登录)。解密缓存 `wxwork_decrypted/`,WAL 感知(帧同算法加密,可合并拿 checkpoint 前最新消息)。`get_new_messages(since_ts)` 秒级 send_time 增量查询,`is_personal_chat` 只留 M:/S: 单聊。fp_hex = `session_fp_from_name`(md5+blake2b 40 字节,与引擎 A identity_by_name 模式一致)。
|
||||
- **共享队列** `pending_replies.json`:字段白名单读写(`_load_pending_replies`/`_persist_pending_replies_unlocked`)。新字段必须两处透传!现有 44 字段含:dedup_key(去重)、detected_by(engine_a/engine_b)、detection_ts、sending_owner、send_lock_ts。
|
||||
- **发送互斥** `send_lock.py`:Windows 命名互斥量 `Global\WeChatBotSendLock` + 线程持有者跟踪。`send_reply()` 入口抢占,拿不到即让出。
|
||||
- 引擎开关(`app_settings.json`,GUI「检测引擎」卡片可改):`engine_a_enabled`(默认 true,注意键名不是 enable_engine_a!)、`enable_engine_b`(默认 true)、`engine_b_poll_interval`(2.0)、`engine_b_data_source`(parallel/db/json,非法回落 parallel)。运行时在 BotThread 启动时读取,GUI 改后本轮结束重启生效。
|
||||
- 引擎 A 关闭(`engine_a_enabled=false`)≠ 不回复:`_poll_once(scan_unread=False)` 跳过红点截图扫描,但队列恢复照常,B 投递的消息仍被定位发送。
|
||||
|
||||
## 关键约定
|
||||
- 队列任务 stage 状态机:queued→opening→reading→collecting→generating→ready_to_send→sending→sent,另有 manual_review/retry_wait/call_paused/uncertain。
|
||||
- 发送频率限制:`MAX_SENDS_PER_MINUTE=4`、`MAX_SENDS_PER_HOUR=60`(`_send_gate`)。
|
||||
- 测试注意:wechat_bot.py 依赖 win32gui/mss/numpy/pyautogui,**只能用系统 Python 3.11 跑**(`C:\Users\pc\AppData\Local\Programs\Python\Python311\python.exe`),managed python 3.13 无这些包。
|
||||
- 构造最小 bot 对象用 `WeChatBot.__new__(WeChatBot)` + 手工补 `_pending_lock/_pending_reply_sessions/_pending_reply_path/_cancelled_reply_sessions`。
|
||||
|
||||
## 待办/未来方向
|
||||
- Phase 2 DB 直读主线已打通(密钥就位 + WAL 合并 + 指纹对齐 + 引擎 B 集成),回归 31 项通过。剩余:长稳联跑验证(企微运行中轮询 WAL 增量 → 引擎 A 回复闭环)。
|
||||
- 机器人对接口子:ai_chat(已用)/ MCP 增强(AI_MCP_ENABLED + mcp_bridge)/ 外部 Agent(mcp_server.py)。
|
||||
Reference in New Issue
Block a user