# workbuddy 任务 — 修 P0 安全评审遗留 5 项

**触发日期**: 2026-06-14
**来源**: Claude 评审(主报告: `docs/评审报告/workbuddy-2026-06-14-P0安全.md`)
**Gitea 仓(公网 Funnel URL)**: `https://ds923plus.tail58d872.ts.net/simon/wecom_it_smart_desk`
**Gitea 仓(内网 LAN,快)**: `http://100.85.152.112:8418/simon/wecom_it_smart_desk`
**当前 HEAD**: `9b6f477` (含 workbuddy 任务清单)
**workbuddy token**: 见 `.workbuddy/config.json` 的 `gitea.token`(用户已配)

---

## ▶▶▶ 任务清单(按严重度,5 项)起

### 🔴 1. [P0] 修 ws.ts:用 Sec-WebSocket-Protocol 携带 token

**文件**: `frontend-agent/src/composables/useWebSocket.ts:106-110`

**问题**: 当前代码:
```ts
ws = new WebSocket(wsUrl, [], {
  headers: { Authorization: `Bearer ${agentStore.token}` }
})
```
浏览器原生 WebSocket API 第 3 参数 options 没有 `headers` 字段。**Chromium / Firefox / Safari 全部忽略**。token 实际**未发送**。

**修复**:
1. 前端改成:
   ```ts
   ws = new WebSocket(wsUrl, [`bearer.${agentStore.token}`])
   ```
2. 服务端 `backend/app/api/ws.py` 改:
   ```python
   # 优先从 subprotocol 取
   subprotocol = websocket.headers.get("sec-websocket-protocol", "")
   if subprotocol.startswith("bearer."):
       token = subprotocol[7:]
   else:
       auth_header = request.headers.get("Authorization", "")
       if auth_header.startswith("Bearer "):
           token = auth_header[7:]
       else:
           token = request.query_params.get("token", "")
   ```
3. H5 端 `h5_websocket_endpoint` 同改

### 🔴 2. [P0] 加 nginx access_log 关闭

**文件**:
- `nginx.conf`(根目录)
- `deploy-server/nginx.conf`

**修复**: 找 `location /api/` 段,在前后加:
```nginx
location /ws/ {
    access_log off;
}
```

### 🟡 3. [P1] 修 model `Mapped[str]` 类型 bug

**文件**: `backend/app/models/agent.py:142-148`

**问题**: `Mapped[str]` + `nullable=True` + `default=None` 严格模式下 `None` 赋值会报错。

**修复**:
```python
from typing import Optional
...
password_hash: Mapped[Optional[str]] = mapped_column(
    String(128),
    nullable=True,
    default=None,
    comment="本地密码哈希（bcrypt）",
)
```

### 🟡 4. [P1] 修降级放行必须 password 验证

**文件**: `backend/app/api/agents.py` `agent_login` 函数(企微 API 不可达分支)

**问题**: 走 "已注册坐席降级放行" 路径时,**不强制 password 验证**。P0-#5 加的 password 字段被绕过。

**修复**: 在降级放行分支检测:
```python
# 已有 agent 且 password_hash 存在 → 必须走 password 验证
if agent and agent.password_hash:
    if not body.password:
        raise AppException(1011, "请输入本地密码")
    if not bcrypt.verify(body.password, agent.password_hash):
        raise AppException(1011, "本地密码错误")
    # 通过后放行
```

### 🟡 5. [P1] requirements.txt 缺 passlib 依赖

**文件**: `backend/requirements.txt`

**问题**: `agents.py` 用了 `from passlib.hash import bcrypt`,但 requirements.txt **没加**。生产部署会 ImportError。

**修复**: 加一行:
```
passlib[bcrypt]==1.7.4
```
或(推荐,passlib 2024 停维护):
```
bcrypt==4.1.2
```
后者需同步改 `agents.py`:
```python
import bcrypt
# 哈希
bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
# 验证
bcrypt.checkpw(password.encode('utf-8'), agent.password_hash.encode('utf-8'))
```

## ▼▼▼ 任务清单止

---

## 🔄 工作流(等 workbuddy 修完 5 项后)

1. workbuddy 修完 → 提交 commit 到 Gitea
2. 通知 Claude 评审
3. Claude 评审(2/5 改成 5/5 完成)
4. 合并 + 推 main
5. 关 #18

## 🔴 token 状态(用户已配)

- 用户给 Claude 的 token: `255eeaf88b...`(已撤销请用户)
- workbuddy 自己的 token: `workbuddy-claude`(在 `.workbuddy/config.json`)
- 推 Gitea 走 HTTPS(SSH 2222 不可达 + .ssh 权限锁)

## 关联

- 评审主报告: `docs/评审报告/workbuddy-2026-06-14-P0安全.md`
- 风险跟踪表: 第十节(5 项遗留追踪)
- Claude 记忆: `review-p0-security-2026-06-14.md`
- Gitea 仓: `https://ds923plus.tail58d872.ts.net/simon/wecom_it_smart_desk` (公网 Funnel)
