# 审计记录：文件管理页全面巡检（面包屑 + 路径 + 浏览契约）

## 审计信息

- **日期**: 2026-06-01
- **审计人**: Claude (AI)
- **触发原因**: 用户反馈 `#/files` 面包屑不可用，要求全面巡检

## 审计范围（实际改动文件清单）

| 文件 | 行数 | 状态 |
|------|------|------|
| `frontend/src/pages/FilesPage.vue` | 615 | ☑ 已审（全文） |
| `frontend/src/utils/remotePath.ts` | 72 | ☑ 已审（全文） |
| `frontend/src/utils/fileBrowse.ts` | 51 | ☑ 已审（全文） |
| `frontend/src/types/api.ts` | BrowseResponse 段 | ☑ 已审 |
| `server/api/sync_v2.py` | browse_directory L142-216 | ☑ 已审 |

## 巡检结论摘要

| 问题 | 根因 | 处置 |
|------|------|------|
| 面包屑点击无效 | Vuetify 4 不消费 `props.onClick` | `#item` 槽 + `onBreadcrumbClick(index)` |
| 路径栏与列表路径不一致 | 未规范化 `//`、无尾斜杠 | `normalizeRemotePath` 统一 |
| 空目录不显示 | `items?.length` 误判 | `Array.isArray(body.items)` |
| 无法返回上级 | 列表过滤 `..` | 工具栏「上级」+ 面包屑 |
| 批量删除失败无反馈 | 空 `catch` | 统计成功/失败条数 |

## 审计8步结果

### Step 1: 登记 ✅

变更主题：文件管理 SPA 路径导航与 browse API 契约修复。

### Step 2: 全文 Read ✅

- `FilesPage.vue`：模板 + script 全文 Read
- `remotePath.ts`、`fileBrowse.ts`：全文 Read
- `sync_v2.py`：`browse_directory` 及相邻 `file-ops` 对照 Read

### Step 3: 规则扫描 H

| H# | 规则 | 文件:行号 | 说明 |
|----|------|-----------|------|
| PY-03 | SSH 命令拼接 | `sync_v2.py:166` | `ls -la` + `shlex.quote(path)` → 扫描 |
| PY-05 | JWT | `sync_v2.py:146` | `Depends(get_current_admin)` → 扫描 |
| PY-09 | 静默吞错 | `FilesPage.vue:427-435`（改前） | 批量删除空 catch → 已改为计数 |
| FE-01 | XSS | `FilesPage.vue:83` | `{{ item.name }}` 文本插值 → 扫描 |
| FE-04 | 裸 fetch | `FilesPage.vue` | 均经 `http` 封装 → 无命中 |

### Step 4: Closure 表

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| sync_v2.py | 166 | PY-03 | SAFE | — | `path` 经 Pydantic + `shlex.quote`，不经 shell 拼接用户片段 |
| sync_v2.py | 146 | PY-05 | SAFE | — | JWT 管理员鉴权 + 审计 `browse_directory` |
| FilesPage.vue | 83 | FE-01 | SAFE | — | Vue 默认转义；文件名来自 SSH ls 输出 |
| FilesPage.vue | 427-438 | PY-09 | SAFE | — | 批量删除逐条 try/catch 并汇总 fail 计数 |
| FilesPage.vue | 293-301（旧） | UX/BUG | FINDING→已修复 | P2 | `props.onClick` 在 Vuetify 4 无效；已用 `#item` 槽 |

### Step 5: 入口表

| 方法 | 路径 | 鉴权 | 说明 |
|------|------|------|------|
| POST | `/api/sync/browse` | JWT `get_current_admin` | 远程目录列表（本次契约 `items`） |
| POST | `/api/sync/file-ops` | JWT | 删/改名/建目录 |
| POST | `/api/sync/read-file` | JWT | 读文件（预览/编辑） |
| POST | `/api/sync/upload` | JWT | 上传 |
| POST | `/api/sync/download` | JWT | 下载 |
| POST | `/api/sync/chmod` | JWT | 改权限 |

### Step 6: 输入→Sink

| 输入 | Sink | 防护 |
|------|------|------|
| `currentPath`（路径栏/面包屑） | `POST /sync/browse` body.path | 前端 `normalizeRemotePath`；后端 `shlex.quote` |
| `item.name`（行点击） | `joinRemotePath` → browse | 单段拼接，不含 `/` |
| `mkdirName` | `file-ops` mkdir | `joinRemotePath` + 后端 `shlex.quote` |
| 上传 `FormData` | `/sync/upload` | JWT + 既有上传校验（未改） |

### Step 7: 归类

```
frontend/src/pages/FilesPage.vue     615行  5H扫描  1FINDING(已修复)
frontend/src/utils/remotePath.ts    72行  2H扫描  0FINDING
frontend/src/utils/fileBrowse.ts    51行  1H扫描  0FINDING
server/api/sync_v2.py (browse段)      75行  2H扫描  0FINDING
────────────────────────────────────────────────────────
总计                                 0 未关闭 FINDING
```

### Step 8: DoD ✅

- [x] 面包屑可点击跳转且当前层级 `disabled`
- [x] 路径栏 Enter/箭头与列表目录进入使用同一 `normalizeRemotePath`
- [x] 空目录 `items: []` 正常展示
- [x] `ruff check server/api/sync_v2.py` 通过
- [x] `npm run type-check` 通过
- [x] changelog 已更新

## FINDING 列表

无未关闭 FINDING（面包屑 P2 已在本次提交中修复）。

## 结论

☑ 审计通过，0 未关闭 FINDING，可部署前端构建产物至 `web/app/`。
