# baoyu-workbench-ops — details

### 🔴 三层截断根因（改版前）
1. **生成层**：`数据/gen_daily_brief_v2.py` 自己切片 —— `uniq_green[:12]` / `uniq_red[:12]` / `notables[:22]` / `comp_ranked[:18]`，但标题写的是**真实总数**（如「积极进展（44条）」下面只有 12 条）→ 标题与内容自相矛盾，是「显示不完整」的真正源头。**修复：去掉所有切片，全量输出**（v2 已改，备份 `gen_daily_brief_v2.py.bak_20260917`）。
2. **卡片层**：`push_daily_brief_card.py` 的 `TRUNCATE`（客诉12/竞品8/进展10/风险8/关注14/回款10）。
3. **协议层**：飞书卡片 JSON 30KB 上限 → 注定无法承载全量。


### ✅ 新链路（全部确定性脚本，零 LLM 消耗）
| 时间 | cron | 脚本 |
|------|------|------|
| 8:30 | `销售日报-生成(8:30)` b60c79153e2c | `~/.hermes/scripts/daily_brief_gen.sh` → `BRIEF_USE_CACHE_COMPLAINTS=1 python3 数据/gen_daily_brief_v2.py` |
| 8:35 | `销售日报-推送(8:35)` 3a16409d3361 | `~/.hermes/scripts/daily_brief_html_push.sh` → guard(latest.md 非今日则重跑 v2) → `脚本/gen_daily_brief_html.py --deploy` → `脚本/push_daily_brief_html.py` |

两 job 均为 `--script --no-agent --deliver local --failure-deliver origin`（脚本自推送，stdout 不进聊天；失败才通知）。


### 交付物
- `数据/daily_briefs/宝锐销售日报_<日期>.html`（自包含 + 可编辑图层：**默认阅读模式**，链接随手可点；点「✏️ 编辑」才进批注态，编辑态下链接照样能跳。生成处 `脚本/gen_daily_brief_html.py` 传 `apply_edit(..., editable_default=False)`）
- CDN：`/daily/<日期>.html` + `/daily/index.html`（归档页，最新在上）→ `https://bier-sales-d0gatbvlx288724e9-1458438710.tcloudbaseapp.com/daily/<日期>.html`
- 飞书卡片只放：KPI 6 数字 + 今日关注前 6 条 + 「📄 打开完整日报」「🗂 历史归档」两个按钮 + HTML 附件（见下方权限）


### 新增「管理视角」板块（HTML 独有）
- **月度累计金额**：`erp_amount.json.monthly` + 部门拆分
- **近 14 天 ERP 日入账**：`erp_amount.json.daily`
- **未关闭客诉超期分层**：`mcp_complaints.json`（`days` 字段）→ >180 / 90-180 / 30-90 / <30 天
- **责任人 × 最长挂起**（暴露长期挂起集中度）+ 挂起最久 15 条明细


### ⚠️ 周末回退

周一 morning cron → yesterday=周日(0条) → 回退周六(0条) → 回退周五(94条) → 使用周五数据。简报标题保留当天日期，标注 `📅 活动数据来源：8月7日（周五），8/8-8/9周末无活动记录`。最多回退4天，全空则生成空简报。


### logo.png 缺失
`工具/logo.png` 需部署到 CloudBase `/workbench/logo.png`：
```bash
tcb hosting deploy 工具/logo.png workbench/logo.png -e bier-sales-...
```


### 常见故障

| 症状 | 根因 | 修复 |
|------|------|------|
| "model provider failed after retries" | DeepSeek 安全审核拒绝("Content Exists Risk")或连接超时 | 重启 Gateway，检查飞书会话是否需要 /reset |
| 回复3-7字 | 飞书会话累积1000+条消息未/reset → 上下文爆炸 | `hermes gateway restart` 清空会话 |
| WebSocket断开 | DNS间歇性解析失败（`open.feishu.cn`） | 网络恢复后自动重连，无需手动干预 |


### 健康监控

```bash
bash ~/.hermes/scripts/gateway_health_check.sh  # 手动诊断
# Cron 每2小时自动检测（job ID: f0d05a94bef0），仅报警不自动重启
```

**🔴 gateway_health_check.sh 自身可靠性（2026-08-10 修复）**：监控脚本曾有三重 bug 导致永远报错退出，形同虚设：

1. **pgrep 模式不匹配**：`pgrep -f "hermes.gateway"` 找不到实际进程 `hermes_cli.main gateway run`
2. **API Key 写死为占位符**：`curl ... -H "Authorization: Bearer ***"` 而非 `"Authorization: Bearer ${DEEPSEEK_API_KEY}"`
3. **不加载环境变量**：缺少 `source ~/.hermes/.env`，导致 `$DEEPSEEK_API_KEY` 为空

**修复要点**：① pgrep 模式改为 `hermes_cli.main.*gateway` ② 用 `$DEEPSEEK_API_KEY` 变量 ③ 脚本开头 `source ~/.hermes/.env`。每次修改健康检查脚本后手动跑一次 `bash gateway_health_check.sh; echo $?` 确认 exit 0。


### Tailscale 端口冲突

Tailscale Serve 可能占用 8765 导致 HTTP 服务启动失败：
```bash
tailscale serve status    # 诊断
tailscale serve reset     # 释放端口
```

## Funnel 配置

```bash
# 工作台 + API 服务器端口 8765
tailscale funnel --bg http://127.0.0.1:8765
# → https://macbook-air-2.tail6864e8.ts.net/

# 验证
curl -s https://macbook-air-2.tail6864e8.ts.net/
```

**依赖**：Mac 必须开机 + Tailscale 在线。Funnel 断开时飞书免登降级为手动登录。


### 关键文件

| 文件 | 作用 |
|------|------|
| `数据/pull_orders.py` | MCP 拉订单 → `mcp_orders_input.json` |
| `脚本/shipment_pipeline.py` | 读订单 → 查顺丰轨迹 → `shipment_status.json` |
| `~/.hermes/scripts/sf_delivery_watchdog.py` | 全流程：拉订单+查轨迹+diff状态+飞书推送 |
| `~/.hermes/scripts/daily_full_refresh.py` | 全量：MCP 管线(客诉/活动/订单/KPI) + 顺丰 |
| `~/.hermes/scripts/sf_watchdog_state.json` | 轨迹状态快照(用于diff) |
| `脚本/refresh_kpi.py` | KPI刷新（从飞书导出Excel → kpi_progress.json，备用） |
| `脚本/sf_bulk_refresh.py` | 顺丰批量路由查询（纯查询+写入，无推送，用于快速补全） |
| `脚本/check_sync.py` | 数据变更检测（hash对比本地vs已部署状态） |
| `~/.hermes/scripts/cb_sync_data.sh` | 批量部署数据JSON到CloudBase |


### 定时任务
| 早 9:00 | `daily_full_refresh.py` | 全量（客诉+活动+订单+KPI+顺丰） |
| 下午 3:00 | `sf_delivery_watchdog.py` | 仅顺丰轨迹 |
| 晚 10:00 | `sf_delivery_watchdog.py` | 仅顺丰轨迹 |


### 常见问题

**订单重复**：`pull_orders.py` 必须按 `单据编号` 去重。飞书中一个订单对应多个工作项（不同产品行），只按 `work_item_id` 去重不够。已在脚本中修复。

**轨迹状态错判**：`fine_status()` 必须用 opCode 优先判断（80=已签收, 204/30=派送中, 54=已揽收），文本匹配仅作兜底。"已派送成功"含"派送"但opCode=80，文本匹配会误判为"派送中"。已在 watchdog 中修复。

**"月结卡号不匹配"**：顺丰 API 的常规提示，表示需要手机号后4位验证。有轨迹数据时忽略此提示；无轨迹数据时才是有问题的订单（手机号不匹配或缺失）。

**飞书推送 open_id 解析失败（2026-07-29 修复）**：
1. App 凭据：必须用 `cli_aaee6a2f90b89bc1`（工作台 App）而非 `cli_aac5af01fd625bdd`（旧 SF App 缺少通讯录权限，`batch_get_id` 返回 400）
2. 邮箱格式：必须用订单 `creatorEmail` 字段（拼音），禁止 `creator + '@biori.com'`（中文名）→ 导致推送跳过
3. `_personnel.json` 路径：`数据/pricing/_personnel.json`（不在根目录），仅含中文名无邮箱

## 拜访卡部署

```bash
bash ~/.hermes/scripts/deploy_visit_card.sh <文件路径>
```

## 数据目录维护（2026-07-28）


### cb_sync.sh 文件清单

仅上传管线产出的动态 JSON（9 个）：`mcp_customers`、`mcp_complaints`、`opportunity_sandbox`、`shipment_status`、`kpi_dashboard`、`kpi_progress`、`my_activities`、`_key_customers`、`_personnel`。

静态文件（`h1_monthly`、`h2_targets`、`three_year_monthly`、`my_kpis`）一次性部署，不上传。


### 🔴 服务依赖的外部资源不得放 /tmp（重启即静默失效）

服务/管线依赖的二进制、模型、模板必须放**工作区稳定路径**（如 `报销助手/工具/bin/`），**不要放 `/tmp`**。macOS 重启会清空 /tmp —— 依赖消失后相关功能**静默降级**（不报错，只是字段大面积变空），排查时极难定位。脚本统一写「稳定路径优先 + `/tmp` 兜底」，并把源码（如 `ocr_pdf.swift`）一起归档到同目录以便随时重编。


### 🔴 cron config drift 跳过（2026-09-01）

数据刷新 cron（如 `46742443f5a8`）报 `last_status=error`，错误信息：
`RuntimeError: [drift_skip:silent] Skipped to prevent unintended spend: global inference config drifted ... (provider 'deepseek' -> 'custom'; model 'deepseek/deepseek-v4-pro' -> 'deepseek-v4-pro')`

**根因**：cron 创建时 pin 了 provider/model，后来全局模型配置变了，Hermes 检测到配置漂移、为防误花钱直接跳过任务（不发 LLM 调用），任务永远不执行、数据不刷新。

**修复**：pin 到当前配置：
```bash
hermes cron edit <job_id> --provider custom --model deepseek-v4-pro
```

**排查**：`cronjob list` 看 `last_status=error` 的任务 → 读 `~/.hermes/cron/output/<job_id>/<最新>.md` 找 `drift_skip` → 用当前全局 provider/model pin。

**注意**：drift_skip 与数据脚本本身无关（脚本没问题），是任务被 Hermes 机制跳过。体检看到 cron error 先查这个，别误判为脚本 bug。

## 飞书聊天传文件接收（2026-09-05）

用户通过飞书给 agent 传数据文件（如月报数据包）时，先确认文件是否真正落到本地，再谈处理。


### zip 中文文件名 GBK 乱码 → ditto 解压

**症状**：`unzip -l xxx.zip` 中文文件名全 `?????`。

**修复**：用 macOS 原生 `ditto -x -k "xxx.zip" "目标目录"`（正确处理中文名，不要用 unzip）。解压后 `find` 确认目录名正常。

## 本机交付物分发（用户点名文件 → 发给他）

用户常一条消息只写一个产出物名 + "发我"（可连续发多条）。期望：自己全盘定位 → 直接发附件 → 补一行说明（大小 + 更新时间），不要反问路径、不要复述文件内容。

流程：
1. **定位**：终端 `find ~/Desktop/Hermes输出-工作类 -iname "*关键词*"`（逐级模糊，关键词取文件名里最有辨识度的一段）。
2. **核验**：`stat -f '修改时间: %Sm  大小: %z bytes' -t '%Y-%m-%d %H:%M' "$F"` —— 既验证文件真实存在，又直接给出要写进说明行的大小/时间。
3. **发送**：回复中单起一行 `MEDIA:/绝对路径`，紧跟一行「文件名（大小，更新时间）」。


### 🔴 定位陷阱

- **`search_files(target='files')` 不能当主定位手段**：macOS 上它自动跳过 TCC 保护目录（Desktop/Documents/Downloads/Library/Movies/Music/Pictures），而交付目录 `~/Desktop/Hermes输出-工作类/` 正在其中 → 返回 `total_count:0` + 一条 warnings，**看起来像文件不存在**。改用终端 `find`，或把 `path` 直接指到受保护目录本身（如 `path='/Users/liuxinyuan/Desktop'`）。
- **归档路径下是软链接，不是文件本体**：`归档/<销售员>/<类型>/xxx.html` 常是 ~108 字节的软链接，本体在 `~/Library/Application Support/宝锐工作台/生成文件/files/<时间戳>-<hash>.html`。**`stat` 看到百来字节就是软链接信号** → 用 `readlink` 取真身并发送本体（MEDIA 指向软链接可能发出空壳）。发送时附一句「归档路径为软链接，发的是本体」。
- 兜底查找顺序：`归档/<销售员>/` → `~/Library/Application Support/宝锐工作台/生成文件/files/` → 全盘 `mdfind -name "关键词"`。

## AI 助手回答超时诊断 + 流式修复（2026-09-08）

**症状**：AI 助手 Tab 报「⚠️ AI 回答超时或服务不可用，请稍后重试」，用户感觉"通路断了"。


### 诊断：先分清「超时」还是「通路硬断」

- 看 `~/.hermes/logs/workbench.error.log` 里 `/api/chat` 的 500 traceback：若是 `BrokenPipeError: [Errno 32] Broken pipe`（在 `do_POST→_send_json→wfile.write` 链），= **服务端已算完答案、往客户端写响应时客户端已断开** = 前端 240s 硬超时主动 abort。这是"超时"，不是通路断。
- 通路硬断会报 `Connection refused` / `gateway_auth_failed` / 模型 500，不会是 BrokenPipe。
- 快速验证链路：`POST http://127.0.0.1:8765/api/chat`（body 带 `user.name`）看是否 200 秒回；`curl http://127.0.0.1:8642/v1/models -H "Authorization: Bearer $API_SERVER_KEY"` 看网关 API 通不通（不带 Bearer 会误报 auth 失败）。


### 根因（三层叠加）

1. **思考模式开启**：`providers.deepseek.extra_body` 有 `thinking:enabled` + `reasoning_effort:medium` → 复杂题先生成隐藏推理，首 token 前可静默 60-180s。
2. **串行检索**：`ai_chat.handle()` 调 LLM 前依次 `_search_background`(MCP飞书活动 15-30s) + `_search_knowledge_base`(IMA 8s) + `_load_customer360`。
3. **硬超时太紧**：前端 `CHAT_TIMEOUT_MS=240000` + 后端 `_call_hermes` `timeout=180`。


### 关键事实

- **8642 = Hermes 网关 OpenAI 兼容 API**（ai.hermes.gateway，model=`hermes-agent`），鉴权 key 取 `~/.hermes/.env` 的 `API_SERVER_KEY`（ai_chat.py 有 fallback `nas-file-share-token-2026`）。
- **改 Hermes 配置不能用文件编辑**——安全护栏拒绝 agent 直接写 `~/.hermes/config.yaml`。用 `hermes config set <dotted.path> <value>`（如 `providers.deepseek.extra_body.thinking.type disabled`），`hermes config get` 验证。
- **思考模式保留时复杂题"思考静默期"仍无字输出**（前端显示"正在思考回答…"，实测首 token 前约 89s），是固有代价非故障；如需进度可加节流心跳。


### 🔴 流式前端 `!r.ok` 静默丢错（卡"正在连接…"）

fetch 的 `.then(function(r){ if(!r.ok){ return r.json().then(...).catch(...) } })` 分支若只是 return 一个 `{_err}` 对象、**没有后续 .then 处理它，也没有 tryNext() 或 settle()**，则非 200 响应（如 CloudBase 相对路径 `/api/chat` 返回 404）会**静默结束 promise 链**：不降级下一个端点、不弹错，界面永远停在"正在连接…"。

```javascript
// ❌ 静默丢错
if (!r.ok) { return r.json().then(function(j){ return {_err:...}; }).catch(function(){ return {_err:...}; }); }
// ✅ 立即降级
if (!r.ok) { if (timer) clearTimeout(timer); console.error('[AI助手] HTTP '+r.status+': '+url); tryNext(); return; }
```

**诊断信号**：用户报"正在连接…"卡死但**无错误弹窗** = 非 200 静默丢错；用户报"正在连接…×N + ⚠️回答超时" = 所有端点抛异常走 catch→tryNext 耗尽。前端 CHAT_APIS 用相对路径 `/api/chat` 优先（同源免跨域），再兜底绝对 Funnel/localhost 地址。
