# baoyu-workbench-frontend-pitfalls — feishu api

## 103. 飞书文件夹消息网关不处理（2026-09-05）

**症状**：用户说"发完了"，但 agent 本地（`~/.hermes/cache/documents/`、Downloads、飞书容器）都找不到新文件。

**根因**：用户发的是飞书「文件夹」消息（`type=folder`），Hermes 网关只处理 file/document 类型，folder 消息收到后不下载、不展开、直接丢弃（日志只有 `Received raw message type=folder`，无后续 Inbound/media 记录）。

**诊断**：`grep 'Received raw message type=' ~/.hermes/logs/gateway.log | tail -20`，看用户发文件那条是 file/document 还是 folder。

**解决**：让用户**逐个发文件**（file/document 类型），或**打包 zip**（zip 是 file 类型能收）。收到 zip 用 `ditto -x -k` 解压（`unzip` 会 GBK 乱码中文文件名）。



## 53. Feishu open_id 按 App 独立——跨 App 推送必须重新获取（2026-07-27）

**症状**：用 `cli_aaee6a2f90b89bc1`（工作台 App）推送消息，收件人 `receive_id_type=open_id` + 旧 App 的 open_id → HTTP 400。

**根因**：飞书的 `open_id` 是 **per-app** 的。`cli_aaee6a2f90b89bc1` 和 `cli_aac5af01fd625bdd` 对同一用户产生不同 open_id。`batch_get_id` API 也只对已通过 OAuth 登录该 App 的用户返回 open_id——用户必须先在该 App 的上下文（飞书工作台入口）登录一次。

**正确流程**：
1. 用户从飞书工作台入口打开应用（触发 OAuth → 创建 open_id）
2. 用该 App 的 credential 调用 `batch_get_id` 获取 open_id
3. `receive_id_type=open_id` + 该 open_id 发送消息

**App 权限清单**（给全员推送必须全部开通）：机器能力 + `im:message` + `contact:user.id:readonly` + 发布

**Workaround**：`receive_id_type=email` 对非应用创建者不可用（HTTP 400）。只能走 open_id → OAuth 路径。



## 71. 客诉列表按创建时间倒序（2026-07-28）

`renderComplaints()` 中数据加载后加排序：
```javascript
items.sort(function(a, b) { return (b.created || '').localeCompare(a.created || ''); });
```



## 15. MQL 数组解析与 workitem_related_multi_select 限制

`moql_field_list` 的值有三种格式：
- **字符串**：直接赋值
- **`{key_label_value: {key, label}}`**：取 label
- **`[{key_label_value: {...}}, ...]`**：数组，循环展开

`mcp_pull_all` 已处理数组展开，但 **`workitem_related_multi_select` 字段在 MQL 中返回空**（即使有数据）。`field_79aa0b`（客户对应商机）和 `field_ac5caf`（客诉客户）都是这种类型。`feishuCustomerLink` 已内建 fallback：无 opp_ids 时走客户详情链接。



## 16. MQL_MAP 双份维护陷阱

`销售工作台服务.py` 有**两处** `MQL_MAP`（GET handler 和 POST refresh handler），修改时必须两处同时更新。今天漏改第二处导致 refresh 端点一直用旧的 issue 表查询客诉（0 结果）。



## 33. 飞书卡片消息推送 API（2026-07-27）

### 获取 access_token
App: `cli_aac5af01fd625bdd`, Secret: `FxAZsdQWVRuPOIK8byWDccwNTan7AadN`
POST `/open-apis/auth/v3/tenant_access_token/internal`

### 发送卡片（带跳转按钮）
`POST /open-apis/im/v1/messages?receive_id_type=open_id`（推荐 open_id，email 偶发 400）
卡片含 button 元素：`url = WORKBENCH_URL + '#orders'`（跳转订单管理）
已知 open_id：刘新元=`ou_fd1028935f5d87fac78bd6356cbe395e`

### receive_id_type 选择

| 方式 | 稳定性 | 备注 |
|------|:--:|------|
| open_id | ✅ | 可通过已知列表硬编码兜底 |
| email | ⚠️ | 有时 400，需收件人在 App 可用范围内 |
| user_id | ❌ | MCP user_key ≠ API user_id |

### 按销售员推送

详见 `references/feishu-per-creator-push.md`。核心：工作台 App `cli_aaee6a2f90b89bc1` 需开通机器人能力 + `im:message` 权限 + 发布后才能推给全员。watchdog 已实现按创建者分组推送，待权限就绪切换 App ID 即可。

### 飞书桌面端 URL 行为（2026-07-27 实测）

**卡片 `url` 按钮**：桌面端飞书**总是打开外部浏览器**，不能内嵌。

**卡片内联 markdown 链接**（`[📋 查看](url)` 在 `lark_md` 块中）：桌面端同样打开外部浏览器。

**`applink.feishu.cn`**：`https://applink.feishu.cn/client/app_link/open?appId=...&url=...` — 测试无效，飞书只做重定向到外部浏览器。

**结论**：桌面端飞书无法内嵌打开网页。这是平台限制。移动端默认内嵌。

### sessionStorage 跨登录持久化 URL 参数（2026-07-27 终版，已验证可行）

**问题**：飞书卡片点击 → 打开工作台 URL `?tracking=SFxxx` → 用户登录 → URL 参数在登录流程中可能丢失（OAuth 重定向/WebView 截断/飞书吞 hash）。

**失败方案清单**（均不可靠）：
- `window.location.search` 实时读取——OAuth 重定向后 search 变更
- `window.location.hash`——桌面端飞书 WebView 截断 `#` 及之后内容
- `_pendingTracking` 在脚本加载时捕获——时序不够可靠
- `#orders:SFxxx` 格式——飞书 WebView 直接吞掉 `:` 后面
- `finishLogin` 中 `setTimeout` 读 `window.location.search`——OAuth 重定向后 search 已变

**终版方案（sessionStorage — 2026-07-27 已验证）**：

```javascript
// 1. 页面加载瞬间存入 sessionStorage（在 <script> 最开头）
(function(){
  var s = window.location.search;
  var m = s.match(/[?&]tracking=([^&]+)/);
  if (m) sessionStorage.setItem('bior_tracking', decodeURIComponent(m[1]));
})();

// 2. finishLogin 中读取并执行跳转
var trk = sessionStorage.getItem('bior_tracking');
if (trk) {
  sessionStorage.removeItem('bior_tracking');
  orderState.filterText = trk;
  setTimeout(function() {
    switchTab('orders');
    setTimeout(function() { toggleShipmentDetail(trk); }, 800);
  }, 2000);  // 等 loadData 完成
}
```

**为什么 sessionStorage 可靠**：存于浏览器同 tab 生命周期，不受 URL 变更影响。`setItem` 在页面第一行 JS 执行，早于任何重定向/登录逻辑。

**注意**：`finishLogin` 中的 `setTimeout` 延迟需要足够长（2s+）以确保 `loadData` 的 `Promise.all` 完成且 `render()` 已执行。太短会因 DOM 未建立而失败。

### 54. H2月度目标图表用全公司数据——改为传 kpi（2026-07-27）

**症状**：赵云浩看板"📊 H2月度目标 vs 实际"图表显示全公司数据（目标几千万），而非个人数据。

**根因**：`drawH2CompareChart()` 直接读 `state.data.h2Targets` 和 `state.data.kpiProgress`（全局 JSON），而非已按销售员计算的 `kpi.h2Targets`/`kpi.h2Actuals`。

**修复**：
```javascript
// 改函数签名，传入已计算好的个人 kpi
function drawH2CompareChart(kpi) {
  var targets = (kpi.h2Targets || []).slice(0, 6);
  var actuals = (kpi.h2Actuals || [0,0,0,0,0,0]).slice(0, 6);
  // ...
}
// 调用处
drawH2CompareChart(kpi);
```

### 58. 访前准备拜访卡生成——跨数据源聚合（2026-07-27）

**功能**：访前准备 Tab 中提供客户搜索 + 卡片类型选择 + 一键生成 + 存档管理。

**数据源交叉映射**：
- `state.data.mcpCustomers.customers[]` → 客户名/联系人/operators/dept/level
- `state.data.customers`（ERP）→ 2026 销售额 / 累计采购
- `state.data.shipments.orders[]` → 订单数 / 价税合计 / 最近订单日期
- `state.data.complaints.items[]` → 客诉数（按客户名匹配）

**权限分层函数 `getVisibleCustomers()`**：
```javascript
function getVisibleCustomers() {
  if (role === 'admin') return mcpAll;           // 刘新元：全部
  if (role === 'manager') return mcpAll.filter(  // 刘欣：拓展部
    c => c.dept === (dept==='销售拓展部'?'诊断原料销售拓展部':dept));
  if (dept === '浙江' || name === '吴云')        // 吴云：浙江
    return mcpAll.filter(c => c.dept === '浙江');
  if (dept === '广东' || name === '韩远怀')      // 韩远怀：广东
    return mcpAll.filter(c => c.dept === '广东');
  return mcpAll.filter(c =>                      // 普通销售：本人
    c.owner === name || (c.operators||[]).indexOf(name) >= 0);
}
```

### 后端 API: `/api/visit-card`（2026-07-27）

**端点**：POST `/api/visit-card`（在 `销售工作台服务.py` 中）
**请求**：`{"customer": "圣湘生物"}`
**响应**：`{"ok": true, "customer": "圣湘生物", "note": "...", "generated_at": "..."}`

**架构**：API 为调研数据预留接口。已集成到前端 `vpDoResearch()` 中，通过 `VISIT_CARD_API = Tailscale Funnel URL` 调用。前端首先生成静态卡片（MCP/ERP数据），然后异步调 API 获取 IMA 知识库 + web 搜索补充数据。

**IMA 限制**：`search_knowledge` 仅搜索标题，不能搜索内容。详见 `references/ima-api-limitations.md`。

**关键文件**：
- `工具/index.html` → `renderVisitPrep()` + `vpGenerate()` + `vpDoResearch()`
- `脚本/销售工作台服务.py` → `do_POST` 中 `path == '/api/visit-card'` 分支

**服务重启后验证**：
```bash
curl -s --max-time 5 http://127.0.0.1:8000/api/visit-card \
  -X POST -H "Content-Type: application/json" -d '{"customer":"圣湘"}'
# 返回 {"ok": true, ...} 即正常
```

### 存档区权限隔离（2026-07-27 终版）

已生成的拜访卡在存档区必须按销售员隔离：从文件名提取客户名 → 与 `getVisibleCustomers()` 匹配。详见 `references/manifest-and-funnel-pitfalls.md` 第 4 节。

### 请求队列双写机制

访前准备"生成拜访卡" → `localStorage`（即时显示）+ API（Funnel → 本地文件队列）。Hermes Agent 轮询文件队列处理。Funnel 不通时请求仅留 localStorage，Agent 看不到。详见 `references/manifest-and-funnel-pitfalls.md` 第 6 节。



## 31. 飞书 OAuth 回调 URL 不能分享给 Agent（2026-07-27）

飞书免登 URL 携带 `?code=...&state=login` 的 `code` 是**一次性 OAuth 授权码**。Agent 用任何工具（browser/web_extract/curl）访问都会**消耗这个 code**，导致用户端登录失败。

**安全规则**：
- 用户发送带 `code=` 的 URL → 立即告知不可访问，建议用户先完成登录
- 用户需替换为不带 code 的干净 URL（如 `https://.../workbench/index.html`）再让 Agent 检查
- `state=login` 也是登录流标识，同样不应重放
