Commit Graph

32 Commits

Author SHA1 Message Date
276e24e830 更新 monitor_web.py
Signed-off-by: zikai <1621362626@qq.com>
2026-06-05 08:57:11 +00:00
ylytdeng
e826b1a565 refactor: 删除 tkinter app_gui.py, 完全切到 Web UI 作为唯一入口
## 决策背景

用户反馈 "windows GUI 太难看了, 还是完全用 webUI 吧, 用户可以选择"。

tkinter app_gui.py (PR #107 引入, 929 行) 的问题:
- 中文字体下渲染糊 ("WeChat Decrypt 工具箱" 标题模糊)
- 90 年代 Windows 控件风格, 不暗色不现代
- 只能跑 Windows, 不跨平台
- 没法远程访问
- 跟 Web UI 维护两套, 重复

Web UI (monitor_web.py) 已经完全对齐功能:
- 8 个工具按钮 (3 tab 分组: 个人微信 / 企微 / 工具)
- 终止按钮 + 实时日志推送
- 导出筛选模态框 (会话搜索/复选/格式选择, close #112)
- 跟实时消息监听共享 SSE 通道
- Lucide SVG icon 统一风格 (无 emoji)
- 暗色主题 + design tokens + 玻璃质感顶栏

## 改动

### 删除
- \`app_gui.py\` (929 行 tkinter GUI)

### monitor_web.py
拆 \`main()\` → \`_start_monitor_if_ready()\` + 精简的 \`main()\`:

之前: keys 不存在 → \`sys.exit(1)\` 直接挂

现在: keys 不存在 → 跳过监听线程启动, 仅起 Web UI 服务。
用户从工具箱点 "① 提取密钥 + 解密数据库" 跑完后重启进程, 监听自动激活。
这样 exe 用户第一次双击时不会报错挂掉, 而是看到 Web UI 工具箱可以
直接用。

新启动流程: \`_start_monitor_if_ready\` 检查 keys 文件 / session.db
密钥 / session.db 路径都 OK 才启 monitor_thread, 任一不满足都给友好
提示但不退出。

### WeChatDecrypt.spec
- 入口 \`app_gui.py\` → \`monitor_web.py\`
- datas 清单补全 (加 export_all_chats / chat_export_helpers /
  batch_decrypt_images / transcribe_chat 等之前漏的)
- hiddenimports 显式列 Crypto / zstandard / pilk (避免 PyInstaller
  漏 detect)

### build.bat
- 删 30 行重复的 --add-data 清单 (跟 .spec 漂移风险)
- 改成 \`pyinstaller --noconfirm WeChatDecrypt.spec\`
- 单一 source of truth 是 .spec
- 完成提示从 "GUI 启动" 改成 "双击 → 浏览器开 Web UI"

### EXE_USAGE.md
完全重写为 Web UI 视角:
- 快速开始: 双击 exe → 浏览器自动打开
- 工具箱 3 tab 各自能力详解
- 导出筛选模态框使用说明
- 任务终止说明
- 输出目录布局
- 远程访问说明
- 结尾解释为什么去掉了 tkinter

### README.md
- 三平台 quick-start 里 "Windows GUI / EXE" → "Windows Web UI / EXE"
- 文件清单: \`app_gui.py\` 那行 → \`monitor_web.py\` (新身份: Web UI
  总入口)
- 技术细节里 "GUI 工具箱" 章节重写: 强调 Web UI 优势 (筛选模态框 /
  终止按钮 / 跨平台 / 远程访问 / 跟监听共存) + 末尾 1 句历史说明
  解释 tkinter 已删除

## 实测

- python monitor_web.py: keys 在 → 正常启动监听 + Web UI
- python monitor_web.py: 假装 keys 不在 → 跳过监听, Web UI 仍能开,
  用户从工具箱点 "① 提取密钥" 后重启即激活
- 测试 185/185 通过

## 没改 (留 follow-up)

- 实际打包 .spec 验证 (需要 Win 跑 pyinstaller, 没在 CI 跑)
- monitor_web 启动后**自动**检测 keys 文件 mtime 变化重启监听, 不需要
  用户手动重启进程 (现在的设计是"重启进程才激活")
2026-05-17 19:22:47 +08:00
ylytdeng
273fe65a07 perf(monitor_web): 主循环 hot path 不再触发 message DB 全量解密 (修 8-125s spike)
## 根因

用户反馈实时消息延迟从亚秒级 → 8-125 秒。

后端 log:
  [perf] decrypt=576页/47.8ms, query=46.6ms
  ... 总耗时=10381.0ms / 44964.4ms / 125398.8ms ...

解密+查询只 95ms, 但总耗时 8-125 秒。源头:

PR #106 (commit 1aa12c8, issue #42) 引入了 _lookup_latest_message,
在 check_updates 主循环里每个新消息都调:

  dec_path = self.db_cache.get(db_key)
            ^^^^^^^^^^^^^^^^^^^^^^^^
            mtime 变化时同步 full_decrypt 整个 message_N.db (~10s)

微信写消息时 message_N.db mtime 跟着变 → get() 触发全量解密 →
主循环阻塞 10 秒。多个 session 同时更新就叠加成几十秒。

之前 db_cache.get 主要在 _check_hidden_messages (走 _hidden_executor
后台线程) 调用, 不阻塞主线程。PR #106 把它带到了主线程 hot path。

证据 (log 里清晰可见):
  [cache] message\message_0.db 全量解密 10551ms
  [18:23:31 延迟=14.0s] [...] 莫名感触: ...   ← 实测消息延迟 14 秒

## 修复

MonitorDBCache 加 peek(rel_key) 方法, **不触发**重新解密, 只读
当前已解密文件路径 (可能 stale 1 个 mtime 周期):

  def peek(self, rel_key):
      out_path = os.path.join(self.tmp_dir, out_name)
      return out_path if os.path.exists(out_path) else None

_lookup_latest_message 把 self.db_cache.get(db_key) 改成 peek(db_key)。

## Trade-off

stale cache 可能让 _lookup_latest_message 查不到刚写入的 local_id,
返回 (None, None)。check_updates 会:
- 跳过加 _shown_keys (issue #79 的去重保险)
- 跳过用 full_content 替换 summary (issue #42 的扩展正文)

但**两个 fallback 路径都正常**:
- 1 秒后 _check_hidden_messages (异步线程) 会用 db_cache.get 等待
  解密完, 拿到 local_id 并 emit hidden 消息 (issue #79 不破坏)
- 第一次推送仍用 SessionTable.summary 的 80 字短截断, 后续如果 user
  开了详情自然加载完整 (issue #42 退化为"原始行为", 不影响主流程)

权衡: 用"偶发 80 字摘要" 换 "无 8-125 秒延迟"。

## 实测预期

修复后主循环总耗时应回到 < 200ms (跟解密+查询 95ms 同量级)。
SessionTable 跑 hidden_executor 仍然能补抓密集消息 (issue #79 保留)。

## 副作用 (无)

- get() 行为不变, hidden 路径仍同步等解密 (它在后台线程, 不影响主线程)
- peek() 是新加方法, 不影响现有调用方
- 测试 185/185 通过
2026-05-17 19:03:44 +08:00
ylytdeng
dd9db0b25b perf(monitor_web): UI 性能优化 + toolsPanel 默认收起
## 用户反馈

"为啥我感觉网页的实时消息没那么快了"

## 数据

后端 log 分析 60 次扫描:
- 19 次总耗时 > 5 秒, 最长 125 秒
- 平均 7585ms (正常应该 < 200ms)
- 但 decrypt 只 47ms, query 46ms — **慢在别处**

## 这次修复 (前端 UI 部分)

### 1. toolsPanel 改回默认收起
之前为了 headless 截图改的默认展开, 让消息容器占用屏幕高度从满屏
变成 ~500px, 用户感受是"消息一直在滚, 看不清"。改回默认隐藏 (用户
点 🛠️ 工具 按钮才展开)。

### 2. 去掉 backdrop-filter blur(20px)
header 之前用 backdrop-filter:blur(20px) 实现"玻璃质感", 但每次 SSE
推消息触发 reflow 都要 GPU 重绘整个 header (尤其低端机)。改回纯 CSS
渐变背景, 视觉差异不大但性能好很多。

## 没解决的 (后端固有问题)

backend log 显示 spike 早在 18:34 第一行就有 (62 秒), 远早于 UI 改动。
监控主循环 monitor_thread 的 check_updates() 总耗时偶发飙到 8-125 秒,
但解密+查询只 95ms。**剩余时间在 SSE broadcast / 图片任务排队 /
emoji lookup / protobuf 解析等次要路径**。

这是独立性能问题, 跟 PR #107 / Web UI 改动无关。开 issue 跟进:

→ 见 follow-up issue (会另开)
2026-05-17 18:54:11 +08:00
ylytdeng
b93e85a1a0 feat(monitor_web): 导出筛选模态框 — 不再一点就跑全量 (closes #112)
## 痛点

之前 Web UI 工具箱里 "③ 导出聊天" / "⑦ 企业微信导出" 一点就跑全量。
用户 3142 个个人微信会话 + 14 个企微会话, 全量导出几个 GB JSON 几小时,
还没法只导某几个群。

## 设计

点击导出按钮 → 弹模态框选会话 + 格式 → 确认后才跑。

```
┌────────────────────────────────────────┐
│  导出个人微信聊天             [×]      │
├────────────────────────────────────────┤
│  🔍 按名字 / wxid 搜索...               │
│  ┌──────────────────────────────────┐ │
│  │ ☐ [群]   交易所消息    2026-05-14│ │
│  │ ☐ [单聊] 张三          2026-05-13│ │
│  │ ☐ [公众号] xxx日报     2026-05-10│ │
│  │ ...                              │ │
│  └──────────────────────────────────┘ │
│  [全选] [清空] [选最近30天活跃]  已选 N│
│                                        │
│  格式 (仅企微) ☑CSV ☐HTML ☐JSON         │
│                                        │
│         [取消]  [确认导出 →]            │
└────────────────────────────────────────┘
```

## 实现拆解

### Backend
1. **GET /api/sessions?source=wechat|wxwork** — 列会话
   - wechat: 从 decrypted/session/session.db SessionTable 读, 拼合
     contact.db 的 nick_name/remark, type 分群/单聊/公众号
   - wxwork: 从 wxwork_decrypted/session.db conversation_table 读,
     id 前缀分 R/S/E/Y → 群/单聊/外部/其他
   - 按 last_ts 降序

2. **TOOL_TASKS schema 加 build_steps + needs_modal**
   - 旧固定任务: `steps: [cmd, ...]`
   - 新动态任务: `build_steps: fn(users, formats) → [cmd, ...]`
   - export_all / wxwork_export 用 build_steps + needs_modal 标记

3. **_run_tool_task 接收 args**, 优先调 build_steps 生成 cmd

4. **POST /api/tool body 接收 args**: `{task, args: {users:[...], formats:[...]}}`

### CLI 脚本
- `export_all_chats.py` 加 `--users wxid1,wxid2` (alt: env WECHAT_EXPORT_USERS),
  在加载完 sessions 后做白名单过滤, 空集报错退出
- `export_wxwork_messages.py` 原本就支持 `--conversation` (multi-arg) +
  `--formats csv,html,json`, 不动它, monitor_web 拼 argv 即可

### Frontend
- HTML 加 `#exportModal` 模态框骨架 (overlay + dialog + search + list +
  format checkboxes + footer buttons)
- CSS .modal-* 一套 (用 design tokens, 跟整体暗色风格统一)
- JS:
  - NEEDS_MODAL = {export_all: 'export_wechat', wxwork_export: 'export_wxwork'}
  - runTool 拦截这两个 task → openExportModal
  - openExportModal fetch /api/sessions → renderSessions (复选框列表)
  - filterSessions 实时搜索过滤
  - selectAllSessions / selectRecentSessions(30) 批量选择
  - confirmExport 收集 selected usernames + formats → runToolWithArgs
- runTool 拆成 runTool (entry, 拦截/取消) + runToolWithArgs (实际跑)

## 实测验证

```
$ curl -s "http://localhost:5678/api/sessions?source=wxwork"
[{"username": "R:358645240322", "name": "交易所消息", "type": "群", "last_ts": 1778658597, ...},
 {"username": "E:10223", "name": "E:10223", "type": "外部", ...}, ...]

$ curl -s "http://localhost:5678/api/sessions?source=wechat"
[{"username": "46222992238@chatroom", "name": "月下健人", "type": "群", "last_ts": 1779014016, ...}, ...]
```

API 返回正确, 个人微信 3142 个 / 企微 14 个会话, 按 last_ts 降序。
测试 185/185 通过。

## 副带改进

- runToolWithArgs 拆出来后, fix 之前一个小 bug: error 路径里的
  `b.textContent = b.dataset.origText` 改成 innerHTML 路径 (一致性)

## 仍未跟进 (follow-up)

- export_all_chats 加 `--format csv,html` 多格式输出 (个人微信脚本
  目前只支持 JSON, 模态框格式选项对它隐藏了, 看 #109 推进)
- 个人微信导出 sessions 列表性能: 3142 个一次 fetch ~5MB JSON,
  搜索 + render 在低端机可能卡, 可加分页 / 虚拟列表
2026-05-17 18:35:44 +08:00
ylytdeng
a1e79f764d fix(monitor_web): 消息按 timestamp 排序插入,不再按到达顺序倒序
## 痛点

用户截图显示消息列表完全乱序:
  18:13:54 (top)
  18:21:59
  18:19:46
  18:21:51 (bottom)
无法按时间阅读。

## 根因

addMsg 行 2216:
  M.insertBefore(d, M.firstChild);

每条新 msg 永远插最前面, **完全按到达顺序倒序**:
- SSE 推送的实时消息 (timestamp 大) 跟 hidden 补抓的旧消息 (timestamp 小)
  按到达顺序穿插
- /api/history 初始化时按 ASC 顺序 forEach addMsg 也是这种倒序行为
- 结果显示顺序跟 timestamp 没有任何关系

## 修复

按 timestamp 找正确插入位置 (降序: 大 ts 在顶, 符合"最新在上"日志流惯例):

  d.dataset.ts = m.timestamp || 0;
  // 遍历现有 children, 找第一个 ts 比新消息小的位置插入
  for(let i=0; i<kids.length; i++){
    const existingTs = +(kids[i].dataset.ts || 0);
    if(ts > existingTs){
      M.insertBefore(d, kids[i]);
      inserted = true;
      break;
    }
  }
  if(!inserted) M.appendChild(d);  // 比所有都早, 放最底

O(n) 但常见情况 (新消息最大 ts) O(1) 命中。200 条上限 fast enough。

## 实测

修复前:
  18:13:54 / 18:21:59 / 18:19:46 / 18:21:51  ← 乱

修复后预期 (降序):
  18:21:59 / 18:21:51 / 18:19:46 / 18:13:54  ← 最新在顶

测试 185/185 通过 (改动只动 addMsg 排序逻辑)。
2026-05-17 18:24:45 +08:00
ylytdeng
a01d326f40 style(monitor_web): 工具区 emoji 全部换成 Lucide 风格 SVG icon
## 痛点

用户: "最讨厌的就是 emoji"

工具区原来用了 10 处 emoji 作 icon: 🛠️ ⚙️ 📱 🏢 🔧 ⚠️ 💡 📡 🛑 。
emoji 的问题:
- 跨平台渲染不一致 (Apple/Win/Linux 字体不同, 大小色调全飘)
- 暗色主题里花花绿绿的"装饰感"破坏现代极简风
- 字号控制不了, 跟周围文字基线不对齐

## 实现

### SVG symbol library 内嵌
- body 开头加 <svg style="position:absolute"> 内嵌 10 个 <symbol>
- 全部 Lucide 风格: viewBox 24x24, stroke="currentColor" stroke-width=2,
  线条 round cap/join
- 使用 <svg class="i"><use href="#i-xxx"/></svg> 引用
- currentColor 自动跟文字色

### 通用 .i CSS
.i      16x16 (默认, 标签/按钮)
.i-sm   13x13 (chip 内)
.i-lg   20x20
.i-xl   32x32 (空状态)
.spin   配 @keyframes spin 1s linear infinite, 给 loader 用

### 替换清单

| 位置 | 之前 | 现在 |
|---|---|---|
| header 工具按钮 | 🛠️ 工具 | #i-wrench + 工具 |
| header 设置按钮 | ⚙️ | #i-settings |
| tab 个人微信 | 📱 个人微信 | #i-chat + 个人微信 |
| tab 企业微信 | 🏢 企业微信 | #i-briefcase + 企业微信 |
| tab 工具 | 🔧 工具 | #i-sliders + 工具 |
| 前置警告 (微信/企微 tab) | ⚠️ 前置 | #i-alert + 前置 |
| 前置信息 (工具 tab) | 💡 跟微信... | .info 主题 + #i-info + 文字 |
| 空状态 | 📡 等待新消息 | #i-radio (i-xl, opacity .4) |
| 终止按钮 (运行中) | 🛑 终止 | #i-stop (实心方块) + 终止 |
| 运行中状态 |  运行中 | #i-loader + .spin 旋转 |

(消息流里的 msg type emoji 风险大, 本次不动, follow-up)

### 副带改动
- .tool-prereq 加 .info 变体 (绿蓝色, 区别 warn 黄)
- .tool-prereq::before ● 去掉 (跟 SVG icon 重复)
- 按钮 cancel 状态用 innerHTML 保留 SVG (之前 textContent 会丢)

## 实测

headless chrome 截图 (179KB), 全部 emoji 消失,
线性 icon 协调一致, 暗色主题没花花绿绿装饰感。

测试 185/185 通过。

剩余 emoji (本次不动):
- mcp_server/monitor_web msg type icon (560-561 line): 📞 ⚙️ ↩️ 📨 等,
  这是消息渲染逻辑, 单独 PR follow-up
- Notification API 的 icon 字段 (2176, 2178): 浏览器通知配图,
  emoji 实际不显示需要 url, 无影响
2026-05-17 18:22:13 +08:00
ylytdeng
cf0e67b50b feat(monitor_web): 加终止按钮 + voice_to_mp3 优雅降级
## 痛点

用户反馈:
1. 没终止按钮 - 任务一旦点了, 跑死(✗ 失败)或跑长(解密 30s+)都没法停
2. ⑧ 语音转 MP3 报 ModuleNotFoundError: 'pilk', 直接 traceback

## Patch 1: 终止任务

### Backend
- _tool_running 加 proc / cancelled 字段
- _run_tool_task 把 proc 暴露到 _tool_running, 每轮 stdout read 后
  检查 cancelled 标志, 命中就 break
- 新加 POST /api/tool/cancel 路由: proc.terminate() + 1.5s 后 kill,
  设 cancelled=True 让 tool runner 收尾
- tool_done 事件加 cancelled 字段

### Frontend
- 任务运行时, 触发按钮临时变成 "🛑 终止" + 红色脉冲动画
- 再点一下就调 /api/tool/cancel
- tool_done 收到后还原按钮文本和颜色
- "⊘ 已终止" 状态徽章替代 "✓ 完成"

### CSS
- .tool-task-btn.cancel: 实心红渐变 + box-shadow + pulseRed 1.5s 动画

## Patch 2: voice_to_mp3 优雅降级

之前直接 `import pilk` 失败时打 traceback, 用户看不懂。改成:

  try: import pilk
  except ImportError:
      print 友好提示 + pip install pilk 命令 + 退出

加 ffmpeg 在 PATH 检查 (pilk 解码后需要 ffmpeg 编 MP3), 给三平台
安装指引。

requirements.txt 加注释说明 pilk 还需要 ffmpeg 配合, pyinstaller
仅打包需要(开发不必装)。

## 测试

185/185 通过 (不影响现有功能)。

实测 web UI:
- 点 ⑧ 语音转 MP3 → 立刻看到友好提示而不是 traceback
- 点任何任务 → 按钮变红色"🛑 终止" → 再点 → 任务收尾 → 状态变"⊘ 已终止"

## 仍未跟进 (follow-up)

- 导出筛选: ③ 导出全部聊天 / ⑦ 企微导出 一点就跑全量很可怕。
  应该弹模态框选会话 + 格式. 单独开 issue #112 跟进, 需要:
  - 新加 GET /api/sessions 路由列会话
  - export_all_chats / export_wxwork_messages 加 --filter 参数
  - 前端模态框 UI
2026-05-17 18:14:28 +08:00
ylytdeng
fb260063ef style(monitor_web): Web UI polish - 4 类 design 改进
跟 design-critique skill 评审后, 实施 4 项可执行改进:

## 1. 🔴 修 header flex overflow bug

原: .stats { margin-left:auto } 在浏览器窄时占主轴空间, 把后面的
🛠️ 工具 + ⚙️ 按钮挤出视野 (用户截图反复出现"看不到按钮")。

改:
  .stats: 加 min-width:0 + overflow:hidden + white-space:nowrap
  .header: 加 flex-wrap:wrap + row-gap:8px 兜底换行
  .tools-btn / .settings-btn: 加 flex-shrink:0 不许被压缩

## 2. 加 design tokens (:root)

20+ 处硬编码颜色 / 间距 / 圆角散落, 改一处怕漏一处。统一:
  颜色: --bg / --bg-elev / --surface / --border / --accent / --warn ...
  间距: --s1..s6 (4/8/12/16/24/32)
  字号: --t1..t6 (11/12/13/15/18/24)
  圆角: --r1 (6) / --r2 (10) / --r3 (14) / --r-pill (999)
  阴影: --shadow-1 / --shadow-2 / --shadow-glow

工具区 CSS 全量切到 token, 老消息流 CSS 保持原样不动 (避免回归)。

## 3. 视觉强化 (tab + primary button)

Tab:
  - active 加 linear-gradient 渐变背景 (透明 → accent-bg)
  - ::after 蓝色下划线 + box-shadow glow 发光
  - hover 微微抬亮
  - 切换有 fadeIn 200ms 动画

Primary 按钮 (Step 1 主操作):
  - 改成实心渐变 #4fc3f7 → #29b6f6 (之前是空心边框, 跟普通按钮区别太小)
  - 加 box-shadow 立体感 + inset 高光
  - hover 上浮 2px + 阴影放大
  - 跟普通按钮 (透明 surface) 主次关系清晰

前置条件:
  - 之前: 黄色左边框 + 灰文字 (像 form error)
  - 现在: 紧凑 pill (背景 + 圆角 999), 头部加 ● 点
  - 显眼但不焦虑

## 4. 上台阶的小招

  body: radial-gradient(ellipse at top, #14142a, #0a0a0f) 顶部柔光
  body: 中文字体优先 PingFang SC / HarmonyOS / Source Han Sans
        (Segoe UI 渲染中文糊)
  body: antialiased + optimizeLegibility
  header: backdrop-filter blur(20px) + position:sticky 玻璃质感
  日志框: JetBrains Mono > SF Mono > Consolas, line-height 1.55

## 默认展开 toolsPanel

之前默认隐藏要先点 🛠️ 工具 按钮才能用, 用户进来一脸懵。改成默认展开:
toolsPanel 加 class="show"。toggleTools() 仍能切换 (隐藏后可还原)。

## 实测

headless chrome 截图确认:
- 🟢 三个按钮全可见
- 🟢 tab active 蓝色高亮 + 发光下划线
- 🟢 STEP 1 主按钮 primary 蓝色实心 突出
- 🟢 STEP 2 普通按钮 透明 layer
- 🟢 前置条件 pill 黄色 chip
- 🟢 背景 radial gradient 顶部柔光过渡

测试 185/185 通过。

## 跟评审建议的对照

| critique 建议 | 实施 |
|---|---|
| 修 header flex bug |  |
| 加 design tokens 系统化 |  (工具区全量, 老消息流保持原状) |
| tab active 渐变 + 发光下划线 |  |
| primary 按钮实心渐变 + 阴影 |  |
| body radial-gradient 背景 |  |
| 中文字体栈优先苹方 |  |
| header backdrop-filter |  |
| 前置条件 pill 化 |  |
2026-05-17 18:09:58 +08:00
ylytdeng
62b015337a refactor(monitor_web): Web UI 工具箱按产品分 tab,不再混杂个人/企微/工具
## 反馈

用户实测后说"产品没设计好,企微和个人微信应该分开"。原版把 8 个按钮平
铺成两行,虽有"个人微信:" / "朋友圈/企微:" label 但只是文字注释,视觉
分组弱,工作流顺序也不明确 (新用户不知道该先点哪个再点哪个)。

## 重设计 (3 个 tab + 工作流 Step 分组)

```
[📱 个人微信]  [🏢 企业微信]  [🔧 工具]   <- tab 切换

📱 个人微信 (active)
⚠️ 前置: 微信 PC 版正在运行且已登录
─────────────────────────────────────────
STEP 1 — 解密
  [① 提取密钥 + 解密数据库] (primary)
  [② 提取图片密钥]

STEP 2 — 导出/解码 (可独立运行,需先 Step 1)
  [③ 导出全部聊天 (JSON)]
  [④ 批量解密 .dat 图片]
  [⑤ 朋友圈解密 + 导出]

[实时日志区]
```

```
🏢 企业微信
⚠️ 前置: 企业微信 PC 版正在运行且已登录 (独立于个人微信)
─────────────────────────────────────────
STEP 1 — 解密
  [① 提取密钥 + 解密数据库] (primary)

STEP 2 — 导出
  [② 导出聊天 (CSV/HTML/JSON)]

[实时日志区]
```

```
🔧 工具
💡 跟微信/企微进程无关,只读已解密产物
─────────────────────────────────────────
语音 / 转码
  [语音转 MP3 (需 ffmpeg in PATH)]

[实时日志区]
```

## 改动细节

### CSS
- 加 `.tool-tabs` / `.tool-tab` / `.tool-pane` (tab 头 + 内容切换)
- 加 `.tool-prereq` (黄色边框前置条件提示)
- 加 `.tool-step` + `.tool-step-label` (大写小灰字步骤标签)
- 加 `.primary` 给 Step 1 主操作按钮更显眼的边框

### JS
- `switchToolTab(name)` 切换 tab,记录到 `window.__activeToolPane`
- `runTool` 把日志写到当前激活 pane 的 `toolLog_<pane>` 而不是单一全局
- SSE `tool_log` 监听同样按 `__activeToolPane` dispatch 到对应日志框
- DOMContentLoaded 时同时绑 tab click + 任务 button click

### Backend
- TOOL_TASKS 不动 (still 8 个),只是前端重新组织
- 一次只跑一个任务的约束 (_tool_lock) 不变

## 优势

- 工作流明确: 新用户进来直接看到"前置 + Step 1 → Step 2"
- 视觉分组: tab 切换比平铺一目了然
- 跟现有 SSE / settings panel 不冲突
- 浏览器 hard refresh 即可看到新版

Tests: 185/185 通过
2026-05-17 17:50:08 +08:00
ylytdeng
bf5d16d48c feat(monitor_web): 加 Web UI 工具箱 - 替代 tkinter app_gui.py 的 8 个按钮
把 PR #107 引入的 tkinter GUI (app_gui.py, 929 行) 整套功能搬到现有
monitor_web 的浏览器 UI 上。复用已有的 SSE 通道,后端跑子进程,实时把
stdout 推到浏览器。

## 实现

### Backend (~110 行)
- `TOOL_TASKS` dict 定义 8 个任务 (cmd 串列表)
- `_run_tool_task(job_id, task_name)` 后台线程顺序跑 steps,
  subprocess.Popen + readline → broadcast_sse 推 event=tool_log
- 完成后推 event=tool_done
- `Handler.do_POST` 加 /api/tool 路由,接 {"task":"..."} 触发
- _tool_lock 保证全局同时只跑 1 个任务,避免两个解密挤内存
- 子进程环境传 WECHAT_DECRYPT_NONINTERACTIVE=1, 走脚本里已有的非交互
  分支 (自动选最近账号等)
- subprocess 用 CREATE_NO_WINDOW 隐藏黑窗 (Win)

### Frontend (~130 行 HTML/CSS/JS)
- header 加 🛠️ 工具 按钮
- toggleTools() 切换 #toolsPanel 显隐 (默认隐藏)
- 8 个按钮分两行: 个人微信 (① 解密 / ② 图片密钥 / ③ 导出聊天 / ④ 批量
  解图片) + 朋友圈/企微 (⑤ 朋友圈 / ⑥ 企微解密 / ⑦ 企微导出 / ⑧ 语音
  转 MP3)
- 黑色日志区,Monaco/Consolas 字体,实时 append (auto-scroll 到底)
- 状态条  运行中 / ✓ 完成 / ✗ 失败
- es.addEventListener('tool_log' / 'tool_done') 跟现有 message 事件
  共存

### 跟现有监听共存
- 不动 messages 容器和消息流逻辑
- 跟 settings 面板共用一套 header 按钮风格
- SSE 通道复用 /stream, 不另起 channel

## 实测

WXWork 在跑的情况下点 ⑥ 企微解密, 浏览器实时显示:
- find_wxwork_keys cipher 结构体扫描进度
- enc_key=cc68fa7d...4c (16 字节)
- decrypt_wxwork_db 17/17 db 解密成功
- 完成后 wxwork_keys.json / wxwork_decrypted/*.db mtime 全部更新

测试 185/185 通过 (不影响现有消息监听逻辑)。

## 对比 app_gui.py

| | tkinter (app_gui.py) | Web UI (monitor_web) |
|---|---|---|
| 外观 | 90 年代 Windows | 暗色现代渐变 |
| 中文字体 | 渲染糊 | 浏览器原生清晰 |
| 跨平台 | 名义支持实际只 Win | macOS/Linux/Win 浏览器都行 |
| 远程访问 |  | bind 0.0.0.0 即可 |
| 维护 | 单独 929 行 stdlib tkinter | 跟监听共享 SSE/HTML |
| 包大小 (打包后) | 多带 tkinter dll | 走 monitor_web 已有的 stdlib |

## 后续

仍开着 (留 follow-up):
- 导出 ③ / ⑦ 当前是"全量"模式; tkinter 里弹了"选会话"对话框,
  Web 版要做 /api/sessions + 模态框 (估 0.5 天)
- 任务完成后给"打开输出目录"按钮 (15 分钟)
- 删 app_gui.py + 改 build.bat 把 monitor_web 当 exe 入口 (1 小时)
- 数据浏览 Tab (浏览器看解密后的 db / 朋友圈 timeline) - P3
2026-05-17 17:36:12 +08:00
Belugary
1aa12c86fa fix(monitor_web): use full message content, not truncated session summary (#42)
## Problem

In `--web` mode, messages longer than ~90 chars get truncated in the
SSE feed. `SessionMonitor.check_updates()` pushes `SessionTable.summary`
to clients, but `summary` is WeChat's own ~80-char preview kept for the
client-side chat list — not the full message body.

## Fix

`_lookup_latest_local_id` already hits `Msg_<md5(username)>` for the
row at `(username, create_time)` to obtain `local_id` for #79's
dedup. Extend the same query to also return `message_content` and
`WCDB_CT_message_content`, and use it to replace `summary` when the
DB body is longer:

    SELECT local_id, message_content, WCDB_CT_message_content
    FROM [Msg_<md5>]
    WHERE create_time = ?
    ORDER BY local_id DESC LIMIT 1

Same row, same query — zero additional IO vs. the prior `MAX(local_id)`.

Renamed to `_lookup_latest_message` to reflect the new
`(local_id, content)` return shape. zstd handling and `wxid_xxx:\n`
group-prefix stripping mirror the existing `SessionTable.summary`
logic in `check_updates`, so the SSE `content` field stays in the same
format clients already render.

Replacement is conservative — only swaps in `full_content` when
strictly longer than `summary`. This never shortens existing behavior
and degrades cleanly if the message-DB write hasn't landed yet (the
SessionTable-vs-message_N.db timing race that #79 already documented).

## Scope

- `monitor_web.py`: one helper extended + one call site adjusted. No
  schema change, no new dependency, no client/UI change.
- `_check_hidden_messages` cold path is untouched — its same-second
  multi-message coverage still runs as before.
2026-05-17 16:44:48 +08:00
ylytdeng
eb544b2bd6 refactor: monitor_web 复用 _extract_transfer_info 避免双份维护
PR #85 把转账消息解析放在了 mcp_server._extract_transfer_info(处理
snake/camel 字段名漂移 + 未知 paysubtype 兜底),但 monitor_web.py
内联重新实现了一遍 paysubtype 标签表 + camelCase fallback。

后果:将来 WeChat 新增 paysubtype 时需要两处改,容易漂。

修复:monitor_web 改为调 mcp_server._extract_transfer_info,跟
chat_export_helpers._extract_transfer_extras 走同一条路径。

UI 行为零变化:
- 已知 paysubtype 显示中文 label(同原行为)
- 未知 paysubtype 显示空串(避免"未知(paysubtype=N)"在 UI 出现)
- 字段抽取/截断逻辑不变

本地验证 OLD vs NEW 字节级一致。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:11:19 +08:00
Belugary
f03df51561 feat: parse WeChat transfer messages (appmsg type=2000) (#85)
Add structured parsing for transfer messages so they no longer fall
through to the generic `[链接/文件]` fallback in chat history exports.
Mirrors the dispatch + helper pattern PR #65 (merged-forward type=19)
established for `base_type=49` appmsg sub-types.

## What is added

**Helpers (mcp_server.py):**
- `_TRANSFER_PAYSUBTYPE_LABEL` — maps the 6 community-consensus paysubtypes
  (1 发起 / 3 已收款 / 4 已退还 / 5 过期已退还 / 7 待领取 / 8 已领取);
  unknown values degrade to `未知(paysubtype=N)` so a new variant in a
  future WeChat build is visible rather than silently dropped.
- `_extract_transfer_info(appmsg)` — pulls fields out of `<wcpayinfo>`,
  with snake/camelCase fallback (`feedesc`/`feeDesc`, `pay_memo`/`paymemo`)
  observed across WeChat versions.
- `_format_transfer_message_text(appmsg, title)` — one-line render
  for chat history: `[转账·已收款] ¥100.00 备注: lunch`.

**Dispatch (mcp_server.py):**
- `_format_app_message_text` gains an `app_type == 2000` branch that
  routes to `_format_transfer_message_text`. `get_chat_history`,
  `export_chat`, `export_all_chats` and `monitor_web` all inherit
  automatically.

**New MCP tool (mcp_server.py):**
- `decode_transfer(chat_name, local_id, create_time=0)` — full
  structured view: direction, amount, memo, payer/receiver wxid,
  transfer id, transcation id, begin/invalid timestamps. Uses the
  same multi-shard scan + ambiguity-by-create_time pattern as
  `decode_file_message` / `decode_record_item`.

**CLI wrapper:**
- `decode_transfer.py` at the repo root — argparse wrapper that prints
  the same text as the MCP tool, returning non-zero exit when the
  message can't be decoded (script-friendly).

**JSON export (chat_export_helpers.py + export_chat.py + export_all_chats.py):**
- `_extract_content` now returns `(rendered, extras)`. `extras` carries
  structured fields when a message type has more signal than the
  human-readable string (currently: transfers → `type:"transfer" +
  transfer:{direction, fee_desc, pay_memo, ...}`). The channel is
  forward-compatible — future additions (video号 metadata, expanded
  merged-forward, etc.) flow through the same shape without changing
  the caller signature. JSON consumers that only read `content` are
  unaffected; the change is additive.

**monitor_web (monitor_web.py):**
- Backend dispatch branch + orange-yellow `.msg-transfer` card CSS +
  `renderRich` JS handler.

## Tests

12 new cases in `tests/test_record_decoders.py`:

- `TransferPaysubTypeLabelTests` — locks the 6-value label table.
- `ExtractTransferInfoTests` (6 cases) — full field round-trip, missing
  `<wcpayinfo>` fallback, snake/camelCase variants, unknown paysubtype
  degradation, empty paysubtype handling.
- `FormatTransferMessageTextTests` (4 cases) — initiate / received-with-memo /
  missing-wcpayinfo / missing-fee-desc.
- `AppMessageDispatchTransferTests` — `_format_app_message_text` routes
  type=2000 correctly so `get_chat_history` / `export_chat` both pick
  it up.

All fixtures use synthetic placeholder values (`wxid_payer_synth`,
`¥100.00`, `1` + 27×`0`); no real PII or transaction IDs.

## Scope

7 files, +546 / -15 (additions only — no behavior change for existing
message types). All 180 tests pass locally (168 baseline + 12 new).
2026-05-12 21:03:08 +08:00
ylytdeng
c45c107f45 fix: 密集消息遗漏(issue #79)— 去重 key 加 local_id
根因:_shown_keys 之前用 (username, timestamp, msg_type) 当 key,导致
同秒同类型多条消息(如"逐条转发"10 条文字)的去重 key 完全相同。
SessionTable 触发 emit 第一条后把 key 加进 _shown_keys,
_check_hidden_messages 查到剩余 N-1 条时全部命中"已显示",全部跳过。
juneleung 实测 "10 丢 4"。

本地用解密后的 message_message_0.db 验证:
  - 真实数据存在 4 条同秒消息(local_id 72480..72483)
  - 旧逻辑:1/4 收到
  - 新逻辑:4/4 收到

改动:

1. _shown_keys 改用 (username, local_id) 精确去重
2. 新增 _lookup_latest_local_id(username, timestamp) — SessionTable 触发
   推送时查 message_N.db 拿对应 local_id
3. _check_hidden_messages 的 SQL 加 local_id 字段,过滤循环用 local_id
4. _shown_keys 清理逻辑改为按数量上限(local_id 不能按时间 prune)

时机风险:SessionTable 写入比 message DB 早几毫秒,_lookup_latest_local_id
可能查不到 → 返回 None,跳过加 key。_check_hidden_messages 1 秒后查到
该消息时自己加 key,结果是偶发轻微重复(比丢消息好)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 13:39:56 +08:00
ylytdeng
15cfdcd4cc fix: 改名/改备注/改群名时联系人缓存不刷新(issue #67)
之前 commit e86e00d 的修复只覆盖「新增联系人不在缓存」的场景:

    if username not in self.contact_names:
        refresh()

改名/改备注/改群名时 username 一直在缓存里,永远跳过刷新,
导致显示老名字。

改成检测 contact.db mtime 变化触发全量 reload,受 30 秒 cooldown
节流(避免微信高频写 contact.db 时 CPU 抖动)。三种变更场景统一覆盖:

- 新增联系人(原 #46 / e86e00d 场景)
- 修改备注名(issue #67)
- 修改群名

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 20:54:18 +08:00
Belugary
ec921dd897 fix: monitor_web 用 webbrowser.open 替代 cmd.exe 实现跨平台开浏览器 (#70)
之前 main() 启动 HTTP server 后用 `os.system('cmd.exe /c start <url>')`
自动开浏览器, 这条命令在非 Windows 平台 cmd.exe 不存在, os.system 返回
非零退出码 (不抛异常, 外层 except 抓不到), 调用静默失败 → 自动开浏览器
功能在 Linux / macOS 完全失效; 同时 shell 会把 `cmd.exe: command not found`
写到终端 stderr 干扰用户.

改用 Python 标准库 webbrowser.open(), 跨平台自动选默认浏览器, 无新增依赖.
2026-05-05 22:47:39 +08:00
ylytdeng
e86e00df87 fix: 新联系人/新群名称不刷新(issue #46)
之前的修复 load_contact_names() 读的是 decrypted/contact/contact.db
静态快照,新加联系人不在里面,所以"自动刷新"实际不生效。

现改为通过 db_cache 实时解密源 contact.db 再加载,确保新增联系人
即时可见。db_cache 内部靠 mtime 检测变化,微信写入后下次查询会触发
重新解密。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 13:55:51 +08:00
ylytdeng
69a2f44240 feat: /api/history 支持按群过滤和增量拉取,更新 README API 文档
- /api/history 新增 chat、since、limit 参数
- README 新增 HTTP API 端点说明和联系人标签工具文档

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:43:41 +08:00
ylytdeng
7eb29b03e8 feat: 新增联系人标签查询功能
解析 contact.db 的 contact_label 表和 extra_buffer protobuf Field #30,
支持查询标签列表及指定标签下的成员。

- mcp_server.py: 新增 get_contact_tags / get_tag_members MCP 工具
- monitor_web.py: 新增 /api/tags JSON 端点,支持 ?name= 过滤

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 09:54:21 +08:00
ylytdeng
b80e7d1c14 fix: 新群/新联系人自动刷新联系人缓存
检测到消息的用户名不在联系人缓存中时,自动重新加载
contact.db,解决新建群聊一直显示 chatroom ID 的问题。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 18:43:34 +08:00
ylytdeng
944546beb1 fix: 统一所有 JSON 文件读写为 UTF-8 编码
Windows 中文环境默认编码为 GBK,未指定 encoding 会导致
config.json/all_keys.json 解析失败。修复 9 个文件共 17 处。

Closes #32

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 14:32:37 +08:00
ylytdeng
7020409543 fix: full_decrypt 写入前自动创建输出目录
full_decrypt 打开 out_path 写入时未创建父目录,
首次运行 monitor_web 且 decrypted/ 不存在时会报
FileNotFoundError。

Fixes #22

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 17:21:11 +08:00
ylytdeng
030680eb85 fix: 修复短时间大量消息丢失问题
旧逻辑用 `if ts == prev_ts: continue` 粗暴跳过上轮时间戳的所有消息,
但同一秒内可能有多条不同消息(如连续转发公众号文章),导致只显示
最后一条,其余丢失。

改为用 (username, timestamp, msg_type) 精确去重:
- 主消息和 hidden 消息显示后都记录到 _shown_keys
- 过滤时精确匹配已显示的消息,不再按时间戳整体跳过
- _shown_keys 每轮清理过期条目(保留 5 分钟),防止内存泄漏

Fixes #20

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 19:52:46 +08:00
PeanutSplash
6d9b2c0fe4 refactor(find_all_keys): extract shared key scan logic 2026-03-07 21:35:24 +08:00
PeanutSplash
f9c338b48d feat: add Linux support with cross-platform memory scanning
- Add Linux memory scanner (`find_all_keys_linux.py`) using `/proc/<pid>/mem`,
  same approach as Windows/macOS — no GDB, no function offsets, no restart needed
- Extract Windows-specific code to `find_all_keys_windows.py`
- Make `find_all_keys.py` a platform dispatcher (Windows / Linux)
- Add `key_utils.py` for cross-platform path matching (`/` vs `\` in all_keys.json)
- Update `config.py` with Linux auto-detection of db_storage paths
- Update all consumers (decrypt_db, monitor, monitor_web, mcp_server) to use
  `get_key_info()` for platform-agnostic key lookup

Tested on remote Linux container: 15/15 DBs scanned, decrypted, and verified.
2026-03-07 21:35:24 +08:00
ylytdeng
2b03a81a8f fix: 统一路径分隔符为正斜杠,修复 macOS/Linux 兼容性
all_keys.json 中的 key 统一使用 `/` 作为路径分隔符,
消除 Windows 反斜杠硬编码,确保跨平台兼容。

涉及文件: find_all_keys.py, decrypt_db.py, monitor.py,
monitor_web.py, mcp_server.py, decode_image.py, latency_test.py

Fixes #17

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 00:53:48 +08:00
ylytdeng
3b4b811cc3 fix: 清理调试代码 + 提升服务稳定性
- 移除 _debug_log、signal handler、atexit 等调试代码
- 添加 allow_reuse_address 防止重启端口占用
- warmup 线程加外层异常捕获防止静默崩溃
- 恢复启动自动打开浏览器

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 20:41:36 +08:00
ylytdeng
121fa9f7bd feat: 自动检测WeChat路径 + 通知过滤规则
- config.py: 自动从 %APPDATA% ini 读取数据盘符,扫描 xwechat_files
  找到 db_storage 路径,多账号时交互选择,首次运行免手动配置
- monitor_web.py: 右侧设置面板支持自定义通知规则(群名/发送人模糊
  匹配),命中时触发浏览器通知 + 蜂鸣声 + 金色高亮,规则存 localStorage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 22:58:30 +08:00
ylytdeng
c85367ff08 feat: 富媒体内容解析、表情包显示、组合消息修复
- 表情包内联显示: emoticon.db CDN映射 + 下载缓存
- 富媒体内容: 链接卡片/文件/视频号/小程序/引用/位置等完整渲染
- 修复文字+图片组合消息丢失 (前端去重key加消息类型)
- 新增隐藏消息检测: 异步查message DB找回同秒内其他消息
- MonitorDBCache线程安全: per-key锁防并发解密损坏
- Web UI优化: 气泡样式/群聊发送者/图片点击放大

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 11:55:11 +08:00
ylytdeng
da7525db95 Add image decryption and inline preview for WeChat V2 format
Support all three .dat encryption formats:
- Old XOR format: single-byte XOR, auto-detect key from magic bytes
- V1 format: AES-ECB with fixed key (md5("0")[:16]) + XOR tail
- V2 format (2025-08+): AES-128-ECB + raw middle + XOR tail

New files:
- decode_image.py: unified image decryption module (XOR/V1/V2)
- find_image_key.py: extract AES key from WeChat process memory
- find_image_key_monitor.py: continuous monitoring version for key capture

monitor_web.py changes:
- Inline image preview in Web UI with async decryption
- MonitorDBCache for mtime-based DB decryption caching
- username-to-DB mapping for image resolution chain
- /img/ endpoint for serving decoded images
- SSE image_update events for real-time preview updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 00:30:01 +08:00
joshua-deng
4c91eb34ef WeChat 4.0 database decryptor and real-time message monitor
Extract encryption keys from Weixin.exe process memory, decrypt all
SQLCipher 4 databases, and monitor new messages via Web UI with ~100ms latency.
2026-02-28 12:03:38 +08:00