# 智能机器人 URL 回调验证排障指南

> ⚠️ **重要警告**：即使 URL 回调验证通过，**Hermes 仍无法在 URL 回调模式下回复**。切到 URL 回调后 WeCom API 返回 `errcode 846601: websocket mode not enabled for this robot`——所有 `send` 调用全部失败。URL 回调要求 HTTP 同步返回回复，而 Hermes 的 LLM 推理是异步的。**智能机器人必须使用 WebSocket 长连接模式**。本指南仅用于理解回调验证原理和排障 API 插件等场景。

## 架构

| 方案 | 端口 | 路径 | 状态 |
|------|------|------|------|
| **Hermes 内置 wecom_callback** | 8645 | `/wecom/callback` | ✅ 推荐 |
| legacy webhook_receiver.py | 5000 | `/wx-callback` | ❌ 不推荐 |

Hermes 内置平台配置在 `~/.hermes/config.yaml` → `platforms.wecom_callback`。不需要额外部署 Flask 服务。

## 排障步骤（按顺序）

### 1. 确认服务在跑

```bash
# Hermes 方案
ss -tlnp | grep 8645
# 应有 python 进程监听 8645

# 遗留方案
ss -tlnp | grep 5000
```

### 2. 确认隧道通

```bash
# Hermes 方案
curl -v "https://bot.gdcjgk.net/wecom/callback?echostr=test"
# 应返回 "signature verification failed"（不是 404/502）

# 遗留方案
curl -v "https://bot.gdcjgk.net/wx-callback?echostr=test"
```

### 3. Cloudflare Tunnel 配置陷阱

Cloudflare Zero Trust 后台手动添加的 **Public Hostname 会覆盖 config.yml 中的 ingress 规则**。如果两边都配了同一个 hostname，以后台为准。

修改隧道路由时要么全用后台、要么全用 config.yml，避免混用。

验证当前配置实际加载了什么：
```bash
# 找到 cloudflared 进程日志
journalctl -u cloudflared --no-pager -n 5 2>/dev/null
# 或直接看后台进程输出
ps aux | grep cloudflared
# 日志中找 "Updated to new configuration" 那行
```

### 4. 查日志看是否收到请求

```bash
# Hermes 方案
tail -50 ~/.hermes/logs/gateway.log | grep -i "wecom"

# 遗留方案
journalctl -u webhook-receiver --no-pager -n 20
```

看是否有回调请求到达。如果没有 → 企微请求没到服务器（检查隧道/防火墙）。
如果有但返回 403 → Token 或 AES Key 不匹配。

### 5. 独立验证 Token（SHA1 签名）

**把 Token 验证和 AES Key 验证分开**。

从日志中取一条完整的验证请求，提取 `msg_signature`、`timestamp`、`nonce`、`echostr`：

```python
import hashlib

token = 'YOUR_TOKEN'
timestamp = '1781506952'      # 从日志取
nonce = '1781140073'          # 从日志取
echostr = 'D+MgujX1IF...'    # 从日志取（URL解码后）

sort_str = ''.join(sorted([token, timestamp, nonce, echostr]))
computed = hashlib.sha1(sort_str.encode()).hexdigest()

print(f'Computed: {computed}')
print(f'Expected: {expected_from_log}')
print(f'Match: {computed == expected_from_log}')
```

- **签名匹配** → Token 正确，问题在 AES Key
- **签名不匹配** → Token 不对，两边没对齐

### 6. 独立验证 AES Key（解密 echostr）

Token 正确但解密失败 → AES Key 不匹配。

```python
import base64, struct
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding

aes_key_str = 'YOUR_AES_KEY'  # 43 chars
echostr_b64 = 'D+MgujX1IF...'  # 从日志取

aes_key = base64.b64decode(aes_key_str + '=')
encrypted = base64.b64decode(echostr_b64)

cipher = Cipher(algorithms.AES(aes_key), modes.CBC(aes_key[:16]), backend=default_backend())
decryptor = cipher.decryptor()
padded = decryptor.update(encrypted) + decryptor.finalize()

try:
    unpadder = padding.PKCS7(128).unpadder()
    decrypted = unpadder.update(padded) + unpadder.finalize()
    content = decrypted[16:]
    msg_len = struct.unpack('>I', content[:4])[0]
    result = content[4:4+msg_len].decode('utf-8')
    print(f'SUCCESS: {result}')
except Exception as e:
    print(f'AES KEY MISMATCH: {e}')
```

**常见失败原因**：

| 错误 | 根因 |
|------|------|
| `Invalid padding bytes` | AES Key 不匹配 |
| `Incorrect padding` | AES Key 长度不对（不是标准 43 字符） |
| 签名不匹配 | Token 不匹配 |

### 7. 常见根因：流程顺序错误

**错误流程**（会导致 AES Key 不匹配）：
1. 企微后台随机生成 → 抄下来发过来
2. 更新 config.yaml
3. 用户在企微后台**又点了一次随机生成**（值变了）
4. 或者用户**没保存**就切换页面再回来（自动重新生成）

**正确流程**：
1. 企微后台随机生成 → **立即截图/复制发过来，趁还没保存**
2. 更新 config.yaml → 重启 Hermes gateway
3. 回到企微后台**保持原值不变** → 点保存

### 8. 回调 URL 路径

- **Hermes 内置**：`https://bot.gdcjgk.net/wecom/callback`
- **遗留方案**：`https://bot.gdcjgk.net/wx-callback`

注意路径不同，不要混用。

## 预期结果

Token 签名匹配 + AES 解密成功 → 企微保存通过 → 回调模式生效。

但 **URL 回调模式与 Hermes 不兼容**（见顶部警告）。验证通过后应切回 WebSocket 长连接模式。
