---
name: wecom-file-receive
description: 通过企业微信接收文件并对接现有工作流（合同审查、文件处理等）。企微群里文件无法同时@人，需要用户引用文件消息后@小Maggie触发。
version: 1.0.0
tags: [企业微信, 文件接收, workflow]
---

# 企业微信文件接收与处理

## 触发条件
- 用户在企微群里发送文件后，引用该文件消息并@小Maggie
- 收到的消息中包含文档附件（saved at路径）

## 文件接收机制

### 用户操作方式
1. 用户在企微群里直接发送文件（此时无法同时@人，小Maggie收不到）
2. 用户**回复那条文件消息**并@小Maggie（如"@小Maggie 审查这个合同"）
3. 网关从引用消息（quote）中提取文件的url和aeskey，下载并AES解密后保存到本地

### 文件保存位置
- 自动保存到 `~/.hermes/cache/documents/` 目录
- 文件名格式：`doc_<hash>_<原始文件名>`
- 消息中会显示 `The file is saved at: <完整路径>`

### 支持的文件类型
- 文档：.docx, .doc, .pdf, .xlsx, .xls
- 图片：.jpg, .png, .gif, .webp
- 其他：任意文件类型均可接收

## 收到文件后的通知规则

### 群里收到的文件
在**同一个群里**回复确认收到，列出文件名和发送人。

### 私信收到的文件
**不要主动往群里推送通知。** 在私信里回复确认收到即可。如需通知群里的人，等群里有新的@消息时顺便汇报，或私信相关人员。

### ⚠️ 为什么不主动推群消息（铁律）
企微 AI Bot 不能用 `APP_CMD_SEND` 主动发群消息，Hermes 底层会复用旧的 inbound `req_id` 通过 `APP_CMD_RESPONSE` 发送。这种"拿过期会话凭证推送非用户发起的消息"的行为有风险：
- 可能触发企微服务端风控/限流
- 短时间频繁重启后叠加此行为，曾导致部分用户的群@消息不再被推送（2026-06-08事件）
- sender 解析可能变成 "unknown"，发出不专业的消息
- 2026-06-09 Doro明确指令："不要往群里发消息，已经触发风险机制了"

**`auto_notify_new_file.sh` 守护进程**也遵守此规则——只走 `notify_doro`（私信），不调 `notify_group`。详见 uwf skill。

### ⚠️ `get_sender()` 发件人检测不可靠
`auto_notify_new_file.sh` 中的 `get_sender()` 函数查 `sessions` 表最近5分钟的最后一个 session 的 `user_id`，经常误判（如邱律师的文件被识别为WeiWei）。准确确认发件人的方法是查 `messages` 表：
```sql
SELECT s.user_id, m.content FROM messages m
JOIN sessions s ON m.session_id = s.id
WHERE m.content LIKE '%sent a document%文件名关键词%'
ORDER BY m.timestamp DESC LIMIT 3
```

### 通知方式选择
- **群里收到的文件** → 在同一个群里回复通知（这是正常的回复，有 reply_req_id）
- **私信收到的文件** → **私信 Doro 通知**，不要主动往群里推。原因：AI Bot 不能 APP_CMD_SEND 主动发群消息，Hermes 靠复用旧 req_id 的 APP_CMD_RESPONSE 发送——旧 req_id 可能已过期，且频繁对过期 req_id 发消息可能被企微风控

### 邱律师(QiuTing)的合同：收到就开干
邱律师私信发的合同文件（.docx/.doc/.pdf），收到后**不问处理方式，直接执行全流程**：
1. **私信回复邱律师**确认收到文件
2. 上传Nextcloud待审查目录
3. 启动review-contract workflow（`setsid`后台运行）
4. workflow全链路自动跑完（含final_review）
5. **小Maggie作为总负责人做终审质检**——发现问题自己修，确认无误才通知Doro
6. 终审通过→更新合同审查清单.xlsx→上传Nextcloud→私信Doro（简洁格式：`任务交付/【修】xxx.docx`）

⚠️ **总负责人原则**（Doro 2026-06-08确立）：Doro看到的必须是终审确认无误的。workflow的final_review只是机器检查，真正的终审是小Maggie的判断。

⚠️ 全程走Doro私信通知，不发群消息。

### 其他人的文件：通知后等指示
Doro、洪总等其他人发的文件，私信Doro汇报后**等Doro指示再处理**。

通知方法（私信Doro）：
使用 `send_message(target='wecom:doro', message='...')` 即可。

## 处理流程

### 场景一：合同审查
用户发文件并说"审查这个合同"时：

1. 从消息中获取文件保存路径
2. 将文件复制到 Nextcloud 待审查目录：
   ```bash
   docker cp <本地文件> nextcloud-nextcloud-1:/var/www/html/data/doro/files/Doro合同审查任务/待审查/<原始文件名>
   docker exec nextcloud-nextcloud-1 chown www-data:www-data <目标路径>
   docker exec -u www-data nextcloud-nextcloud-1 php occ files:scan doro --path='/doro/files/Doro合同审查任务/待审查/'
   ```
3. 按现有合同审查workflow执行（加载contract-reviewer + contract-editor skill）
4. 完成后交付到任务交付目录

### 场景二：文件提取/处理
用户发文件并要求提取内容、翻译、格式转换等：

1. 从消息中获取文件保存路径
2. 直接在本地处理（读取docx/pdf内容、提取表格等）
3. 按用户指示输出结果（生成新文件、上传到指定目录等）

### 场景三：文件转存
用户发文件并要求存到某个目录：

1. 从消息中获取文件保存路径
2. docker cp 到 Nextcloud 指定目录
3. chown + occ files:scan 同步

## 文件不在cache——检查Nextcloud直传

当用户说"我上传了XX文件"但 `~/.hermes/cache/documents/` 里没有时，文件可能是通过Nextcloud网页/客户端直接上传的（不经过企微）。

### 排查步骤
1. **先扫描Nextcloud**刷新文件索引：
   ```bash
   sudo docker exec nextcloud-nextcloud-1 php occ files:scan doro --path="/doro/files/<预期目录>"
   ```
2. **按文件名关键词搜索**整个Doro目录树：
   ```bash
   sudo docker exec nextcloud-nextcloud-1 bash -c 'find /var/www/html/data/doro/files/ -name "*关键词*"'
   ```
3. **检查 `.part` 文件**——Nextcloud分块上传的临时文件：
   ```bash
   sudo docker exec nextcloud-nextcloud-1 bash -c 'find /var/www/html/data/doro/files/ -name "*.part"'
   ```
   - 文件名格式：`<hash>.ocTransferId<number>.part`
   - `.part` 文件存在 = 上传正在进行中
   - 大小在增长 = 还在传，等它完成
   - `.part` 消失且没有新文件出现 = **上传失败/取消**，通知用户重新上传

### ⚠️ 注意事项
- `.part` 文件不会被 `occ files:scan` 发现（Nextcloud不索引临时文件），必须用 `find` 直接查文件系统
- 大文件上传可能很慢（通过Nextcloud网页上传取决于用户网速），不要过早判断失败
- 上传失败时`.part`文件会被Nextcloud自动清理，不留痕迹
- 确认上传失败后，告知用户"文件上传似乎中断了，请重新上传"

## 文件名处理
- cache中的文件名经过URL编码，需要解码还原原始文件名
- 上传到Nextcloud时使用原始文件名（从消息中提取）
- 遵循文件命名规则skill（file-naming-convention）

## 注意事项
1. **企微群文件限制**：文件消息无法同时@人，必须通过引用回复触发
2. **URL有效期**：企微文件下载URL有效期约5分钟，网关收到后立即下载，不存在过期问题
3. **AES加密**：企微文件传输经过AES加密，网关自动解密（已修复base64 padding问题）
4. **图片也支持**：图片同样通过引用回复方式接收，保存到cache/images/目录

## 审计：统计某天某人发了多少文件

当Doro问"邱律师昨天发了多少合同"时，**不能靠Nextcloud文件时间戳或cache/documents/时间戳**。原因：
- Nextcloud待审查目录的文件是小Maggie上传的，时间是上传时间不是接收时间
- 有些文件是更早期的积压件（如班组慰问品、夏阳代建在6月5日就在Nextcloud了）
- cache/documents/文件可能已被清理

### 方法1：查Nextcloud数据库（最可靠）

直接查MariaDB获取精确的存入时间和操作人。详见 `references/nextcloud-db-queries.md`。

```bash
# 查文件存入时间
docker exec nextcloud-db-1 mariadb -u nextcloud -p'Nc2026Db!Szw' nextcloud \
  -e "SELECT fileid, path, FROM_UNIXTIME(storage_mtime) as stored_utc FROM oc_filecache WHERE path LIKE '%关键词%';"

# 查是谁上传的（仅限通过Nextcloud UI上传的文件）
docker exec nextcloud-db-1 mariadb -u nextcloud -p'Nc2026Db!Szw' nextcloud \
  -e "SELECT FROM_UNIXTIME(timestamp) as time_utc, user, subject, file FROM oc_activity WHERE file LIKE '%关键词%';"
```

⚠️ docker cp + occ files:scan 上传的文件不会产生 oc_activity 记录。无记录=自动流程上传。

### 方法2：查gateway日志中的接收确认记录

```bash
# 北京时间6月8日 = UTC Jun 7 16:00 ~ Jun 8 16:00
journalctl --user -u hermes-gateway \
  --since "2026-06-07 16:00:00" --until "2026-06-08 16:00:00" \
  | grep "收到邱律师私信发来\|邱律师好.*文件已收到"
```

每条匹配 = 一次文件接收。注意：
- 同一份合同可能发了多次（如复达合同发了2次），要区分"文件发送次数"和"不同合同数"
- 时区转换：服务器UTC，Doro问的是北京时间，必须先转换再查
- 不要把上传/审查/交付时间当成接收时间

### 教训（2026-06-09）
Doro问"邱律师昨天发了多少合同"，小Maggie第一次回答5份（基于Nextcloud文件时间戳），被Doro纠正。查gateway日志后确认是6次文件发送、5份不同合同。

## Gateway断开期间的文件恢复

**问题**：企微WebSocket断开期间，私信文件消息收不到。
**症状**：`journalctl --user -u hermes-gateway | grep "WeCom.*websocket.*closed"`

**恢复方法**：
1. 重启gateway：`systemctl --user restart hermes-gateway`
2. WebSocket重连后，企微服务端通常会重新推送断开期间的消息（但不100%可靠）
3. 重启后检查cache/documents/是否有新文件到达
4. 如果文件没补回来，检查Nextcloud待审查目录是否已有同名文件（可能之前session已上传）
5. 都没有→私信邱律师："合同X的文件未收到，请重新发送"

**预防**：cron每15分钟检查WebSocket健康，发现断开及时重启，缩短丢消息窗口。

## needs_clarification处理（问邱律师，不问Doro）

当classifier无法判断顾问单位（如甲方信息空白）返回needs_clarification时：
1. **私信邱律师**询问："合同X甲方信息空白，请确认甲方单位"
2. 不问Doro——Doro是最终确认人，不应被拉进执行环节
3. 邱律师回复后，带补充信息创建新thread重新跑
4. 同时跳过该合同，继续处理其他排队的合同
