diff --git a/docs/superpowers/plans/2026-08-06-yunke-customerlist-alignment.md b/docs/superpowers/plans/2026-08-06-yunke-customerlist-alignment.md
index f0ddafc..1d8f498 100644
--- a/docs/superpowers/plans/2026-08-06-yunke-customerlist-alignment.md
+++ b/docs/superpowers/plans/2026-08-06-yunke-customerlist-alignment.md
@@ -653,6 +653,14 @@ git commit -m "test(customers): e2e verify CustomerList alignment vs 云客"
- **WA「添加时间」= createTime**:按云客显示(字符串直显)。语义上 createTime 严格说是记录创建时间,与 Plan 1「不把 createTime 当 add_time 填充结构化列」不冲突——此处仅展示,不参与筛选。
- **不复刻**:导出HTML聊天记录按钮(spec:不引入装饰功能)。
+## Task 2 quality review 记录(非本 plan 范围,后续/backlog)
+
+Task 2 quality review ✅ Approved,6 个 Minor 不阻断;**#1**(query 变化时 reset 两个 tab 的 page,避免非 active tab 留 stale page)/ **#2**(简化 `fromTypeText`,去掉与 `fromTypeLabel` 重复的 null 处理)已修并入 commit。以下 4 项为项目范围/非回归,记录待后续:
+- **顶栏「全部渠道」radio(MainLayout)在 /customers 上点击** → `setChannel('all')` → 本页 channel watch 映射到 `wechat`,静默把 WhatsApp tab 切回微信。根因在 MainLayout 全局 radio + `'all'` 语义(StaffList 同样存在)。后续可考虑:CustomerList/StaffList 这类按 channel 强制分 tab 的页面,禁用/隐藏顶栏 `'all'` 选项。
+- **「清除部门筛选」按钮** `router.replace({name:'customers'})` 会丢全部 query,若同时有 `owner_account_id` 会一并清除(StaffList 同模式)。如需只清部门,改为保留 owner query。
+- 加载失败无显式「重试」按钮(切 tab 可触发重试,因 `loaded=false`)。StaffList 同。
+- `loadTab` 无 AbortController,慢网快切可能短暂显示 stale 页(项目范围,StaffList 同)。
+
---
## 后续 Plan(不在本 plan 范围)
diff --git a/docs/superpowers/plans/2026-08-06-yunke-missing-operations.md b/docs/superpowers/plans/2026-08-06-yunke-missing-operations.md
new file mode 100644
index 0000000..3821d81
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-06-yunke-missing-operations.md
@@ -0,0 +1,743 @@
+# 云客沟通板块漏操作补丁 Implementation Plan(重写)
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 补全云客「沟通」板块已实现页面里被漏掉/判错的表格内操作——微信员工 5 个统计数字→明细 modal、主badge→聊天、员工姓名→详情(纠正误判的手机号)、modal 内客户微信→聊天记录、微信客户/微信群的「导出HTML聊天记录」(含时间选择)。
+
+**Architecture:** 前端为主:`StaffList.vue` 微信 tab 把 4 列纯文本数字改成可点→复用现成的 `YkStaffDetailModal`(数据源调现有 `/api/customers`);`YkStaffDetailModal` 的 `Col` 加可选 `onClick` 支持二级跳转;`CustomerList.vue` 微信 tab 操作列加导出按钮 + 时间选择 dialog;`GroupList.vue` 微信群 tab 操作列加导出按钮(群无时间选择)。后端仅新增 1 个 `GET /api/export/chat-html` 端点(DM/群通用 + 时间范围),TDD。
+
+**Tech Stack:** FastAPI + SQLAlchemy 2.0 async(后端);Vue3 + TS + Element Plus + Pinia(前端);后端测试 pytest + `make_client` fixture(in-memory sqlite)。
+
+**Spec/事实依据:** `docs/superpowers/specs/2026-08-06-yunke-comm-operations-audit.md`(2026-08-06 真机核对表,cursor:pointer 全覆盖 + 零采信)。
+**参考既有:** Plan1 `StaffList.vue`(WhatsApp tab 已用 `YkStaffDetailModal` 的 `openAddedModal`/`openTalkedModal` 是现成模板)。
+
+---
+
+## 范围
+
+**本 plan 覆盖**(已实现页面的表格内漏操作):
+- 微信员工:沟通数 3 数字(沟通人数/有效沟通数/跟进客户数)→ modal;及时回复 2 数字(员工最后回复数/未回复客户数)→ modal;「主」badge→聊天;员工姓名→drawer(**纠正**:原 missing-operations 写的「手机号」是误判,手机号不可点,可点的是员工姓名);modal 内客户微信→聊天记录。
+- 微信客户:操作列加「导出HTML聊天记录」+ 时间选择 dialog。
+- 微信群:操作列加「导出HTML聊天记录」。
+- 后端:`GET /api/export/chat-html`(DM 用 talker_id / 群用 room_id + start/end 时间范围)。
+
+**不在本 plan 范围**:
+- 语音通话列表 CallList(微信 voice-call-list + WA wavoicecalllist + 长语音 modal)—— 独立 Plan 4,前置抓取修复。
+- 工具栏批量操作(批量语音下载/全部导出/高级筛选)—— 随 CallList。
+- WA 侧(员工/客户/群)—— Plan1/2/3 已对齐,本轮真机复核无新漏操作。
+- 精确 KPI 度量还原(见下「数据源说明」)。
+
+### ⚠️ 数据源说明(重要设计简化,执行前已知)
+
+云客微信员工 5 个数字 modal 各有独立 KPI 语义(沟通人数/有效沟通数/跟进客户数/员工最后回复数/未回复客户数 是**不同**客户集合)。yunkes 的 `Customer` 表**无「最后回复方向」汇总字段**,要精确区分(尤其「员工最后回复数」vs「未回复客户数」需逐客户查最后一条 `Message.is_send`)需新增 message 聚合端点,超出「补丁」定位。
+
+**本 plan 决策**:5 个数字 modal 的 `fetcher` 统一调 `/api/customers?channel=wechat&owner_account_id={员工账号}`(该员工的客户列表),modal **列结构**按云客对齐(有效沟通数 modal 额外显示 `send_num`/`receive_num` 列,其余 4 个用基础列)。即**交互完整**(点任意数字→弹对应标题/列的 modal),**度量精确区分**(已回复 vs 未回复)作为已知简化,留待后续 message 聚合端点独立增强。此简化须在收尾验收时确认产品可接受。
+
+---
+
+## 运行命令(验证用)
+
+- 后端:`cd yunke-server && DB_URL="sqlite+aiosqlite:///./smoke.db" .venv/Scripts/python.exe -m uvicorn app.api.main:app --port 8000 --log-level warning`
+- 后端测试:`cd yunke-server && .venv/Scripts/python.exe -m pytest tests/ -q`
+- 前端:`cd yunkes && npm run dev`
+- 前端类型检查:`cd yunkes && npx vue-tsc -b`
+
+---
+
+## File Map
+
+**后端(yunke-server/):**
+- Create `app/api/routers/export.py` — `GET /api/export/chat-html`(拉 messages + 渲染 HTML + attachment 下载)
+- Modify `app/api/main.py` — 注册 export router(参照其他 router 注册,约 L38-48 import + L59-67 include_router)
+- Create `tests/test_api_export.py` — 导出端点 TDD(`make_client` 范式)
+
+**前端(yunkes/src/):**
+- Modify `components/YkStaffDetailModal.vue` — `Col` 接口加 `onClick?: (row) => void`,渲染时该列包 `el-button link`
+- Modify `views/StaffList.vue` — 微信 tab:4 列数字→可点 modal、主badge→viewChat、员工姓名→openDrawer、modal 客户微信列 onClick
+- Modify `views/CustomerList.vue` — 微信 tab 操作列(L301-307)加导出按钮 + 时间选择 dialog + `exportChatHtml`
+- Modify `views/GroupList.vue` — 微信群 tab 操作列(L270-276)加导出按钮 + `exportChatHtml`
+- Modify `api/client.ts` — 加 `downloadBlob(url, params)` helper(`http.get` + `responseType:'blob'`)
+
+---
+
+## Task 1: 后端导出 HTML 端点(TDD)
+
+> 事实:`Message` 有 `is_send`(Boolean 方向)/`timestamp`(ms)/`talker_id`/`room_id`/`text`/`channel`/`owner_account_id`,**无 direction**。`get_session` 无参(`app.api.deps`)。测试用 `make_client`(**无 async_engine fixture**),范式见 `tests/test_api_messages.py` 的 `_seed`。
+
+**Files:**
+- Create: `yunke-server/tests/test_api_export.py`
+- Create: `yunke-server/app/api/routers/export.py`
+- Modify: `yunke-server/app/api/main.py`
+
+- [ ] **Step 1: 写失败测试**(`tests/test_api_export.py`):
+```python
+from sqlalchemy.ext.asyncio import AsyncSession
+from app.db.models import Message
+
+
+async def _seed(session: AsyncSession) -> None:
+ session.add_all([
+ Message(msg_id="dm1", channel="wechat", owner_account_id="wx_owner",
+ talker_id="wx_friend", room_id=None, text="你好",
+ timestamp=1000, is_send=False, type="text", type_raw=1),
+ Message(msg_id="dm2", channel="wechat", owner_account_id="wx_owner",
+ talker_id="wx_friend", room_id=None, text="在的",
+ timestamp=2000, is_send=True, type="text", type_raw=1),
+ Message(msg_id="g1", channel="wechat", owner_account_id="wx_owner",
+ talker_id="room_topic", room_id="g1@chatroom", text="群消息",
+ timestamp=3000, is_send=True, type="text", type_raw=1),
+ Message(msg_id="g2", channel="wechat", owner_account_id="wx_owner",
+ talker_id="room_topic", room_id="g1@chatroom", text="范围外",
+ timestamp=9999999, is_send=True, type="text", type_raw=1),
+ ])
+ await session.commit()
+
+
+async def test_export_dm_returns_html(make_client):
+ client = await make_client(_seed)
+ r = await client.get("/api/export/chat-html", params={
+ "channel": "wechat", "owner_account_id": "wx_owner", "talker_id": "wx_friend"})
+ assert r.status_code == 200
+ assert r.headers["content-type"].startswith("text/html")
+ assert "attachment" in r.headers["content-disposition"]
+ body = r.text
+ assert "你好" in body and "在的" in body
+ assert "wx_owner" in body
+
+
+async def test_export_group_with_time_range(make_client):
+ client = await make_client(_seed)
+ r = await client.get("/api/export/chat-html", params={
+ "channel": "wechat", "owner_account_id": "wx_owner", "room_id": "g1@chatroom",
+ "start": 0, "end": 5000})
+ assert r.status_code == 200
+ assert "群消息" in r.text
+ assert "范围外" not in r.text # end=5000 过滤掉 timestamp=9999999
+
+
+async def test_export_empty(make_client):
+ client = await make_client(_seed)
+ r = await client.get("/api/export/chat-html", params={
+ "channel": "wechat", "owner_account_id": "wx_owner", "talker_id": "none"})
+ assert r.status_code == 200
+ assert r.headers["content-type"].startswith("text/html")
+```
+
+- [ ] **Step 2: 跑测试确认 FAIL**
+```bash
+cd yunke-server && .venv/Scripts/python.exe -m pytest tests/test_api_export.py -x -q
+```
+预期:FAIL(404 路由不存在)。
+
+- [ ] **Step 3: 实现 export router**(`app/api/routers/export.py`):
+```python
+"""GET /api/export/chat-html — 导出一段 1:1 / 群聊天为独立 HTML 文件。
+
+对齐云客微信客户列表「导出HTML聊天记录」:按 (channel, owner, talker_id|room_id)
+拉消息(可选 start/end 时间范围),渲染自包含 HTML 聊天页(is_send 决定左右气泡),
+作为 attachment 返回下载。
+"""
+from __future__ import annotations
+
+from datetime import datetime, timezone
+from html import escape
+from typing import Annotated
+
+import sqlalchemy as sa
+from fastapi import APIRouter, Depends, Query, Response
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.api.deps import get_session
+from app.db.models import Message
+
+router = APIRouter(prefix="/api/export", tags=["export"])
+
+SessionDep = Annotated[AsyncSession, Depends(get_session)]
+
+
+def _ts_text(ts: int | None) -> str:
+ if not ts:
+ return ""
+ try:
+ return datetime.fromtimestamp(ts / 1000, tz=timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M:%S")
+ except (ValueError, OSError, OverflowError):
+ return str(ts)
+
+
+def _render_chat_html(rows: list[Message], channel: str, owner: str, peer: str) -> str:
+ bubbles = []
+ for m in rows:
+ side = "right" if m.is_send else "left"
+ ts = escape(_ts_text(getattr(m, "timestamp", None)))
+ text = escape(getattr(m, "text", "") or "")
+ bubbles.append(
+ f'
'
+ )
+ title = f"聊天记录 · {escape(channel)} · {escape(owner)} / {escape(peer)}"
+ return f"""
+{title}{title}
+{''.join(bubbles) or '暂无消息
'}
+"""
+
+
+@router.get("/chat-html")
+async def export_chat_html(
+ session: SessionDep,
+ channel: Annotated[str, Query()],
+ owner_account_id: Annotated[str, Query()],
+ talker_id: Annotated[str | None, Query()] = None,
+ room_id: Annotated[str | None, Query()] = None,
+ start: Annotated[int | None, Query(ge=0)] = None,
+ end: Annotated[int | None, Query(ge=0)] = None,
+) -> Response:
+ q = (
+ sa.select(Message)
+ .where(Message.channel == channel)
+ .where(Message.owner_account_id == owner_account_id)
+ )
+ peer = ""
+ if room_id:
+ q = q.where(Message.room_id == room_id)
+ peer = room_id
+ elif talker_id:
+ # DM:限定 room_id 为空,避免误捞群消息
+ q = q.where(Message.talker_id == talker_id).where(
+ (Message.room_id == None) | (Message.room_id == "") # noqa: E711
+ )
+ peer = talker_id
+ if start is not None:
+ q = q.where(Message.timestamp >= start)
+ if end is not None:
+ q = q.where(Message.timestamp <= end)
+ q = q.order_by(sa.nullslast(Message.timestamp.asc()), Message.msg_id.asc())
+ rows = (await session.execute(q)).scalars().all()
+ html = _render_chat_html(list(rows), channel, owner_account_id, peer)
+ safe = peer.replace("@", "_").replace("/", "_") or "all"
+ filename = f"chat-{channel}-{owner_account_id}-{safe}.html".replace("@", "_").replace("/", "_")
+ return Response(
+ content=html,
+ media_type="text/html; charset=utf-8",
+ headers={"Content-Disposition": f'attachment; filename="{filename}"'},
+ )
+```
+
+- [ ] **Step 4: 注册 router**(`app/api/main.py`,参照现有 import 块 L38-48 + include_router 块 L59-67 加两行):
+```python
+from app.api.routers import export # 加入 import 列表
+app.include_router(export.router) # 加入 include_router 列表
+```
+
+- [ ] **Step 5: 跑测试确认 PASS**
+```bash
+cd yunke-server && .venv/Scripts/python.exe -m pytest tests/test_api_export.py -x -q
+```
+
+- [ ] **Step 6: 全后端回归**
+```bash
+cd yunke-server && .venv/Scripts/python.exe -m pytest tests/ -q
+```
+预期:全 PASS(原 ≥330 + 新增 3)。
+
+- [ ] **Step 7: Commit**
+```bash
+git add yunke-server/app/api/routers/export.py yunke-server/app/api/main.py yunke-server/tests/test_api_export.py
+git commit -m "$(cat <<'EOF'
+feat(api): GET /api/export/chat-html — 导出 1:1/群聊天为 HTML
+
+支持 DM(talker_id) 与 群(room_id),可选 start/end 时间范围;
+is_send 决定气泡左右。对齐云客微信客户/群列表「导出HTML聊天记录」。
+
+Co-Authored-By: Claude
+EOF
+)"
+```
+
+---
+
+## Task 2: 前端 downloadBlob helper
+
+> 事实:`api/client.ts` 的 `http`(baseURL `/api`)响应拦截器返回 `response.data`;对 `responseType:'blob'` 也会原样透传 blob。**无现成下载 helper**。鉴权走同域 cookie,axios 自动带。
+
+**Files:** Modify `yunkes/src/api/client.ts`
+
+- [ ] **Step 1: 加 `downloadBlob`**(`client.ts` 末尾,`getJson` 之后):
+```ts
+/** 以 blob 下载(用于 HTML 导出等二进制/文本附件下载)。返回建议文件名(来自 content-disposition,缺失则 undefined)。 */
+export async function downloadBlob(
+ url: string,
+ params?: Record,
+): Promise {
+ const res = await http.get(url, { params, responseType: 'blob' })
+ const blob = res as unknown as Blob
+ // 从响应头取文件名(拦截器只解包 body,需用 raw 拦截;此处 res 已是 blob,header 丢失 → 用固定名)
+ const filename = 'chat-export.html'
+ const objUrl = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = objUrl
+ a.download = filename
+ document.body.appendChild(a)
+ a.click()
+ document.body.removeChild(a)
+ URL.revokeObjectURL(objUrl)
+ return filename
+}
+```
+> 说明:响应拦截器(client.ts L19-34)解包了 `response.data`,拿不到 `Content-Disposition` 头,故用固定文件名。可接受(云客导出文件名也非关键)。若需真实文件名,需另开 `http.getRaw` 走 `AxiosResponse`,本 plan 不做。
+
+- [ ] **Step 2: 类型检查**
+```bash
+cd yunkes && npx vue-tsc -b
+```
+预期:exit 0。
+
+- [ ] **Step 3: Commit**
+```bash
+git add yunkes/src/api/client.ts
+git commit -m "$(cat <<'EOF'
+feat(api): add downloadBlob helper for blob attachment download
+
+Co-Authored-By: Claude
+EOF
+)"
+```
+
+---
+
+## Task 3: YkStaffDetailModal Col 加 onClick 支持
+
+> 事实:`YkStaffDetailModal.vue` 的 `Col` 接口(L9-26)= `{prop,label,width?,minWidth?,align?,formatter?}`;列由 `v-for="c in cols"` 渲染(L57-91)。**无 defineExpose**,只能 v-model + props 驱动。目的:让 modal 内「客户微信」列可点→跳聊天记录(Task 7 用)。
+
+**Files:** Modify `yunkes/src/components/YkStaffDetailModal.vue`
+
+- [ ] **Step 1: Col 接口加 `onClick`**(L9-17 的 `interface Col`,L18-26 是 defineProps):
+```ts
+interface Col {
+ prop: string
+ label: string
+ width?: number | string
+ minWidth?: number | string
+ align?: 'left' | 'center' | 'right'
+ formatter?: (row: any, column: any, cellValue: any, index: number) => string
+ onClick?: (row: any) => void // 新增:该列单元格可点(渲染为 link button)
+}
+```
+
+- [ ] **Step 2: 模板列渲染支持 onClick**(找到 `v-for="c in cols"` 的 ``,其 `` 内)。把单元格默认文本渲染改为:有 `onClick` 则包 `el-button link`:
+```html
+
+
+
+ {{ c.formatter ? c.formatter(row, c, row[c.prop], 0) : row[c.prop] }}
+
+
+
+```
+> 没有 `onClick` 的列保留 `:formatter` 原生渲染(el-table 默认 slot 为空时走 prop+formatter)。implementer 先读 L57-91 现有模板对齐写法,保留无 onClick 列的原行为。
+
+- [ ] **Step 3: 类型检查**
+```bash
+cd yunkes && npx vue-tsc -b
+```
+预期:exit 0(现有 StaffList 用法未传 onClick,向后兼容)。
+
+- [ ] **Step 4: Commit**
+```bash
+git add yunkes/src/components/YkStaffDetailModal.vue
+git commit -m "$(cat <<'EOF'
+feat(modal): YkStaffDetailModal Col 支持可选 onClick(单元格可点)
+
+Co-Authored-By: Claude
+EOF
+)"
+```
+
+---
+
+## Task 4: StaffList 微信员工「沟通数」3 数字 → modal
+
+> 事实:微信 tab 沟通数列 `StaffList.vue` L489-504,3 个 `` 纯文本:`chatCount`(L493 沟通人数) / `contactCount`(L497 有效沟通数) / `followCount`(L501 跟进客户数)。`YkStaffDetailModal` 已 import(L7)/已用(L673-678),状态 `detailOpen/detailTitle/detailFetcher/detailCols`(L226-232),`baseCustomerCols`(L237-242)。WhatsApp tab 的 `openAddedModal`(L244-255)/`openTalkedModal`(L257-272) 是现成模板。`getCustomers` 在 `@/api`(index.ts L46-60)。
+
+**Files:** Modify `yunkes/src/views/StaffList.vue`
+
+- [ ] **Step 1: 读现有模板对齐**。先 Read `StaffList.vue` L226-272(detail 状态 + baseCustomerCols + openAddedModal/openTalkedModal)+ L673-678(modal 用法),理解 `detailFetcher` 怎么调 `getCustomers`、列怎么定义。新增的微信 modal 照此模式。
+
+- [ ] **Step 2: 加 3 个微信沟通数 modal 开启函数**(script 内,`openTalkedModal` 之后)。fetcher 统一调 `getCustomers({channel:'wechat', owner_account_id: row.account_id, ...分页})`:
+```ts
+/** 微信员工「沟通数」3 数字 → 明细 modal(数据源:该员工客户列表,度量精确区分见 plan 数据源说明)。 */
+function openWechatCommModal(row: Account, title: string, withMsgCols: boolean): void {
+ detailTitle.value = title
+ detailCols.value = withMsgCols
+ ? [
+ { prop: 'friend_nickname', label: '客户微信', minWidth: 160 },
+ { prop: 'send_num', label: '发送消息数', width: 110, align: 'center' },
+ { prop: 'receive_num', label: '接收消息数', width: 110, align: 'center' },
+ ]
+ : [{ prop: 'friend_nickname', label: '客户微信', minWidth: 160 }]
+ detailFetcher.value = async (page: number, page_size: number) =>
+ getCustomers({ channel: 'wechat', owner_account_id: row.account_id, page, page_size })
+ detailOpen.value = true
+}
+```
+> `getCustomers` 已在 L5 import。列对齐云客:沟通人数/跟进客户数 = 基础列;有效沟通数 = 基础列 + 发送/接收消息数。员工信息/员工微信列本 plan 省略(modal 已是某员工的明细,复用 modal 标题体现)。
+
+- [ ] **Step 3: 沟通数列 3 个 span 改可点**(L489-504)。把 3 个 `{{ cellNum(row, '...') }}` 改为 `el-button link`:
+```html
+
+{{ cellNum(row, 'chatCount') }}
+
+{{ cellNum(row, 'contactCount') }}
+
+{{ cellNum(row, 'followCount') }}
+```
+
+- [ ] **Step 4: 类型检查**
+```bash
+cd yunkes && npx vue-tsc -b
+```
+预期:exit 0。
+
+- [ ] **Step 5: 浏览器验证**(`/staff` 微信 tab)→ 点沟通人数/有效沟通数/跟进客户数 → 分别弹对应标题 modal,有效沟通数 modal 多两列。
+
+- [ ] **Step 6: Commit**
+```bash
+git add yunkes/src/views/StaffList.vue
+git commit -m "$(cat <<'EOF'
+feat(staff): 微信员工沟通数 3 数字 → 明细 modal(补 Plan1 误判漏做的操作)
+
+Co-Authored-By: Claude
+EOF
+)"
+```
+
+---
+
+## Task 5: StaffList 微信员工「及时回复」2 数字 → modal
+
+> 事实:及时回复列 L507-522,`saleSendLastNum`(L511 员工最后回复数) / `saleSendLastNumRate`(L515 回复率,**不可点**) / `customerSendLastNum`(L519 未回复客户数)。
+
+**Files:** Modify `yunkes/src/views/StaffList.vue`
+
+- [ ] **Step 1: 复用 Task4 的 `openWechatCommModal`**(同模式,基础列)。及时回复 2 数字:
+```html
+
+{{ cellNum(row, 'saleSendLastNum') }}
+
+{{ cellText(row, 'saleSendLastNumRate') }}
+
+{{ cellNum(row, 'customerSendLastNum') }}
+```
+
+- [ ] **Step 2: 类型检查 + 浏览器验证**(点 2 数字弹 modal;回复率不可点)
+```bash
+cd yunkes && npx vue-tsc -b
+```
+
+- [ ] **Step 3: Commit**
+```bash
+git add yunkes/src/views/StaffList.vue
+git commit -m "$(cat <<'EOF'
+feat(staff): 微信员工及时回复 2 数字 → 明细 modal(回复率不可点)
+
+Co-Authored-By: Claude
+EOF
+)"
+```
+
+---
+
+## Task 6: StaffList 微信员工 主badge→聊天 + 员工姓名→drawer(纠正手机号误判)
+
+> 事实:员工信息列 主/副badge 是 ``(L410-416,`duoTagText`),云客点「主」badge→聊天窗口;微信昵称 L417-419 已绑 `viewChat`;员工姓名 L422-424 纯 ``,云客点→员工详情页;微信手机号 L426-428 纯 span **不可点**。`openDrawer(row)` L334-337 已存在。`viewChat(row)` L196-219 跳 yunkes `/chat`(聊天记录,降级云客 wechat-window)。
+> **纠正**:原 missing-operations plan Task1「微信手机号→openDrawer」是误判——云客 `button.ant-btn-link` 包的是**员工姓名**(带 history 图标),手机号不可点。本 task 改员工姓名。
+
+**Files:** Modify `yunkes/src/views/StaffList.vue`
+
+- [ ] **Step 1: 主badge 可点→viewChat**(L410-416 的 ``)加 `@click.stop="viewChat(row)"` 与 `style="cursor:pointer"`:
+```html
+{{ duoTagText(row) }}
+```
+> 云客点主badge→wechat-window 聊天窗口;yunkes 降级跳 `/chat` 聊天记录(同昵称)。implementer 读 L410-416 现有 el-tag 写法对齐属性。
+
+- [ ] **Step 2: 员工姓名 可点→openDrawer**(L422-424):
+```html
+{{ cellText(row, 'userName') }}
+```
+> 云客跳员工详情页 `customer/userDetailNew`;yunkes 无该页,降级开 drawer(全字段,复用现有 `openDrawer`)。**手机号 L426-428 保持纯 span 不变**(云客也不可点)。
+
+- [ ] **Step 3: 类型检查 + 浏览器验证**(点主badge/员工姓名→分别跳聊天/开 drawer;手机号不可点)
+```bash
+cd yunkes && npx vue-tsc -b
+```
+
+- [ ] **Step 4: Commit**
+```bash
+git add yunkes/src/views/StaffList.vue
+git commit -m "$(cat <<'EOF'
+feat(staff): 微信员工 主badge→聊天 + 员工姓名→drawer(纠正手机号误判)
+
+云客跳详情页的是员工姓名(button+history),非手机号;手机号不可点。
+
+Co-Authored-By: Claude
+EOF
+)"
+```
+
+---
+
+## Task 7: StaffList modal 内客户微信 → 聊天记录
+
+> 事实:依赖 Task3 的 `Col.onClick`。云客 modal 内客户微信列→开 `wechat/detail/message` 聊天记录;yunkes 降级跳 `/chat/{channel}/{owner}/{talker}`(ChatTimeline)。modal 列数据是 `Customer`(`friend_nickname`/`friend_id`/`owner_account_id`/`channel`)。
+
+**Files:** Modify `yunkes/src/views/StaffList.vue`
+
+- [ ] **Step 1: 给 `openWechatCommModal` 的基础列加 `onClick`**(修改 Task4 Step2 加的列定义,给「客户微信」列加 onClick):
+```ts
+import type { Customer } from '@/api/types' // 若未 import 则加(参考 CustomerList 用法)
+
+function openWechatCommModal(row: Account, title: string, withMsgCols: boolean): void {
+ detailTitle.value = title
+ const goChat = (r: Customer) => {
+ if (!r.owner_account_id || !r.friend_id) return
+ router.push({ name: 'chat', params: { channel: r.channel, ownerId: r.owner_account_id, talkerId: r.friend_id } })
+ }
+ detailCols.value = withMsgCols
+ ? [
+ { prop: 'friend_nickname', label: '客户微信', minWidth: 160, onClick: goChat },
+ { prop: 'send_num', label: '发送消息数', width: 110, align: 'center' },
+ { prop: 'receive_num', label: '接收消息数', width: 110, align: 'center' },
+ ]
+ : [{ prop: 'friend_nickname', label: '客户微信', minWidth: 160, onClick: goChat }]
+ detailFetcher.value = async (page: number, page_size: number) =>
+ getCustomers({ channel: 'wechat', owner_account_id: row.account_id, page, page_size })
+ detailOpen.value = true
+}
+```
+> `router` 已在 L3 import/use。`chat` 路由名参照 CustomerList viewChat(L147-160 用 `name:'chat'`)。implementer 确认 `Customer` 类型 import 路径(`@/api/types`)。
+
+- [ ] **Step 2: 类型检查 + 浏览器验证**(点沟通数 modal 内客户微信→跳该客户聊天记录页)
+```bash
+cd yunkes && npx vue-tsc -b
+```
+
+- [ ] **Step 3: Commit**
+```bash
+git add yunkes/src/views/StaffList.vue
+git commit -m "$(cat <<'EOF'
+feat(staff): 微信员工明细 modal 内客户微信 → 聊天记录(二级操作)
+
+Co-Authored-By: Claude
+EOF
+)"
+```
+
+---
+
+## Task 8: CustomerList 微信客户「导出HTML聊天记录」+ 时间选择
+
+> 事实:微信 tab 操作列 L301-307(单按钮「查看聊天记录」);WhatsApp tab L373-379(**勿改**)。`viewChat` L147-160。云客导出HTML→先弹「请选择时间」modal→确定下载。`downloadBlob`(Task2)。导出参数:channel/owner_account_id/talker_id/start/end。
+
+**Files:** Modify `yunkes/src/views/CustomerList.vue`
+
+- [ ] **Step 1: 加导出相关状态 + 函数**(script 内,`viewChat` 附近):
+```ts
+import { downloadBlob } from '@/api/client'
+
+const exportOpen = ref(false)
+const exportRow = ref(null)
+const exportRange = ref<[number, number] | null>(null) // [startMs, endMs]
+
+function openExport(row: Customer): void {
+ if (!row.owner_account_id || !row.friend_id) {
+ ElMessage.warning('该客户缺少归属账号/客户标识,无法导出')
+ return
+ }
+ exportRow.value = row
+ exportRange.value = null
+ exportOpen.value = true
+}
+
+async function confirmExport(): Promise {
+ const row = exportRow.value
+ if (!row) return
+ const params: Record = {
+ channel: row.channel,
+ owner_account_id: row.owner_account_id,
+ talker_id: row.friend_id,
+ }
+ if (exportRange.value) {
+ // value-format="X" 返回秒级字符串,后端要毫秒
+ params.start = Number(exportRange.value[0]) * 1000
+ params.end = Number(exportRange.value[1]) * 1000
+ }
+ exportOpen.value = false
+ await downloadBlob('/export/chat-html', params)
+}
+```
+> `ref`/`ElMessage` 已在文件顶部 import(确认);`Customer` 类型已用。时间范围用 element-plus date-picker(毫秒时间戳)。
+
+- [ ] **Step 2: 微信 tab 操作列加导出按钮 + 加宽**(L301-307):
+```html
+
+
+ 查看聊天记录
+ 导出HTML聊天记录
+
+
+```
+> **仅微信 tab(L301-307)改**,WhatsApp tab(L373-379)不动(云客 WA 客户无导出按钮)。
+
+- [ ] **Step 3: 加时间选择 dialog**(模板末尾,关闭标签前):
+```html
+
+
+
+
+
+
+
+ 取 消
+ 确 定
+
+
+```
+> `value-format="X"` 返回秒级 unix 字符串;需转毫秒。改 `confirmExport` 里:`params.start = Number(exportRange.value[0]) * 1000`(若 date-picker 返回字符串)。implementer 按 element-plus 实际返回类型调整(`X` 返回 string 秒)。
+
+- [ ] **Step 4: 类型检查 + 浏览器验证**(微信客户 tab→点导出→弹时间选择→确定→下载 HTML 含消息;不选时间→导出全部)
+```bash
+cd yunkes && npx vue-tsc -b
+```
+
+- [ ] **Step 5: Commit**
+```bash
+git add yunkes/src/views/CustomerList.vue
+git commit -m "$(cat <<'EOF'
+feat(customers): 微信客户操作列加「导出HTML聊天记录」+ 时间选择
+
+对齐云客:先弹「请选择时间」→ 确定后调 /api/export/chat-html 下载。
+
+Co-Authored-By: Claude
+EOF
+)"
+```
+
+---
+
+## Task 9: GroupList 微信群「导出HTML聊天记录」
+
+> 事实:微信群 tab 操作列 L270-276(单按钮);WA 群 L345-351(**勿改**,云客 WA 群无导出)。`viewChat` L137-146。群导出用 `room_id`(非 talker_id)。
+
+**Files:** Modify `yunkes/src/views/GroupList.vue`
+
+- [ ] **Step 1: 加 `exportGroupHtml` 函数**(script 内,`viewChat` 附近):
+```ts
+import { downloadBlob } from '@/api/client'
+
+/** 微信群「导出HTML聊天记录」→ 后端 /api/export/chat-html(room_id)。云客此操作在微信群列表。 */
+async function exportGroupHtml(row: Group): Promise {
+ if (!row.owner_account_id || !row.room_id) {
+ ElMessage.warning('该群缺少归属账号/群标识,无法导出')
+ return
+ }
+ await downloadBlob('/export/chat-html', {
+ channel: row.channel,
+ owner_account_id: row.owner_account_id,
+ room_id: row.room_id,
+ })
+}
+```
+> 群导出**不加时间选择**(云客群列表导出按钮直接导出,无时间 modal——仅微信客户有「请选择时间」)。若实测发现群也有时间选择,implementer 补;本 plan 按客户列表采集事实(群操作列点采集时未见时间 modal,直接下载)。`Group`/`ElMessage` 已 import。
+
+- [ ] **Step 2: 微信群 tab 操作列加导出按钮 + 加宽**(L270-276):
+```html
+
+
+ 查看聊天记录
+ 导出HTML聊天记录
+
+
+```
+> **仅微信群 tab(L270-276)改**,WA 群(L345-351)不动。
+
+- [ ] **Step 3: 类型检查 + 浏览器验证**(微信群 tab→点导出→下载 HTML 含群消息)
+```bash
+cd yunkes && npx vue-tsc -b
+```
+
+- [ ] **Step 4: Commit**
+```bash
+git add yunkes/src/views/GroupList.vue
+git commit -m "$(cat <<'EOF'
+feat(groups): 微信群操作列加「导出HTML聊天记录」(补 Plan3 漏的操作)
+
+Co-Authored-By: Claude
+EOF
+)"
+```
+
+---
+
+## Task 10: 端到端验证 + 回归
+
+- [ ] **Step 1: 起后端 + 前端**(若未在跑)。
+- [ ] **Step 2: 后端 curl 抽查**(用已有 wechat customer 的 owner+friend):
+```bash
+curl -s -o /tmp/chat.html "http://localhost:8000/api/export/chat-html?channel=wechat&owner_account_id=wxid_fnkf5b0iaqrb22&talker_id=wxid_p6417n8wveno22" && head -c 300 /tmp/chat.html
+```
+预期:返回 HTML(含 `聊天记录` + 消息文本)。无消息则「暂无消息」。
+- [ ] **Step 3: 浏览器 `/staff` 微信 tab**:① 点沟通人数/有效沟通数/跟进客户数/员工最后回复数/未回复客户数 → 各弹对应标题 modal(有效沟通数多2列);② modal 内点客户微信 → 跳聊天记录;③ 点主badge → 跳聊天;④ 点员工姓名 → 开 drawer;⑤ 手机号不可点。
+- [ ] **Step 4: 浏览器 `/customers` 微信 tab** → 点导出HTML → 选时间 → 确定 → 下载 HTML(含消息)。
+- [ ] **Step 5: 浏览器 `/groups` 微信群 tab** → 点导出HTML → 下载 HTML。
+- [ ] **Step 6: 全回归**
+```bash
+cd yunke-server && .venv/Scripts/python.exe -m pytest tests/ -q
+cd yunkes && npx vue-tsc -b
+```
+预期:pytest 全绿(≥333),vue-tsc exit 0。
+
+---
+
+## 收尾验收
+
+- [ ] `vue-tsc -b` exit 0;pytest ≥333 绿(新增 3 export 测试)。
+- [ ] 微信员工 5 数字均可点→弹对应 modal(有效沟通数多发送/接收消息数列);回复率不可点;主badge→聊天;员工姓名→drawer;手机号不可点;modal 内客户微信→聊天记录。
+- [ ] 微信客户导出HTML:时间选择 dialog → 下载 HTML。
+- [ ] 微信群导出HTML:直接下载 HTML。
+- [ ] WA 两侧(员工/客户/群)操作不受影响(Plan1/2/3 已对齐,本 plan 未改 WA)。
+- [ ] **数据源简化已确认产品可接受**:5 个数字 modal 统一用员工客户列表,度量精确区分(已回复 vs 未回复)留待后续 message 聚合端点。
+
+---
+
+## 不在本 plan 范围(显式)
+
+- 语音通话列表 CallList(微信 voice-call-list + WA wavoicecalllist + 长语音列表 modal + 录音播放/手动匹配/录音异常检查)—— 独立 Plan 4,前置抓取修复(voice-call-list/wavoicecalllist 抓取缺失)。
+- 工具栏批量操作(批量语音下载/全部导出/高级筛选)—— 随 CallList。
+- 员工详情独立页(用户选降级 drawer,不建)。
+- 5 个数字 modal 的精确 KPI 度量还原(需 message 聚合端点,独立增强)。
+- 导出 HTML 的真实文件名(响应拦截器限制,用固定名;如需真实文件名另开 getRaw helper)。
diff --git a/docs/superpowers/specs/2026-08-06-yunke-comm-operations-audit.md b/docs/superpowers/specs/2026-08-06-yunke-comm-operations-audit.md
new file mode 100644
index 0000000..6c5786e
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-06-yunke-comm-operations-audit.md
@@ -0,0 +1,258 @@
+# 云客「沟通」板块操作全面核对表
+
+> 采集时间:2026-08-06|采集源:crm2.yunkecn.com(登录态真机核对)|范围:沟通板块 6 个列表(微信/WA × 员工/客户/语音通话)
+> 用途:作为重写 `2026-08-06-yunke-missing-operations.md` plan 的事实依据。
+
+## 核对方法(关键纠错)
+
+- **可点判定**:`getComputedStyle(el).cursor === 'pointer'`,覆盖 `` / `