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 在低端机可能卡, 可加分页 / 虚拟列表
This commit is contained in:
ylytdeng
2026-05-17 18:35:44 +08:00
parent a1e79f764d
commit b93e85a1a0
2 changed files with 324 additions and 13 deletions

View File

@@ -299,6 +299,12 @@ def main():
action="store_true",
help="预览模式:显示将导出的会话数和新消息数,不实际写入",
)
parser.add_argument(
"--users",
default=None,
help="只导出指定 username 的会话, 逗号分隔 (如 wxid_xxx,12345@chatroom). "
"为空时导出全部 (旧行为). 也可用 env WECHAT_EXPORT_USERS",
)
args = parser.parse_args()
script_dir = os.path.dirname(os.path.abspath(__file__))
@@ -338,6 +344,18 @@ def main():
print(f"会话数据库查询失败: {e}", file=sys.stderr)
sys.exit(1)
# username 白名单过滤 (--users 参数 / WECHAT_EXPORT_USERS 环境变量)
users_filter_raw = args.users or os.environ.get("WECHAT_EXPORT_USERS", "")
if users_filter_raw.strip():
wanted = {u.strip() for u in users_filter_raw.split(",") if u.strip()}
before = len(sessions)
sessions = [u for u in sessions if u in wanted]
print(f"按 --users 过滤: {before}{len(sessions)} 会话")
if not sessions:
print(f"[!] 指定的 username 列表跟会话表没交集 (wanted={list(wanted)[:5]}...)",
file=sys.stderr)
sys.exit(1)
names = mcp_server.get_contact_names()
# 显示模式信息