# Composer 2.5 — 文件管理器重构 P0 执行任务

## 立即任务：JWT Token 提前续期（消除进目录双刷新）

### 必读
- 设计文档：`docs/design/specs/2026-06-02-files-refactor-design.md`
- 重点章节：§1.2 根因、§3 P0、§8 立即执行项

### 根因摘要
用 cursor-ide-browser 实测：每次进目录发 2 次 `/api/sync/browse`（间隔 ~1.3s）。
原因：access token 过期 → 第 1 次 401 → `/auth/refresh` → 重试。
解决：发请求前检查 token exp，距过期 < 5 min 主动刷新。

### 改动清单（按顺序）

#### 步骤 1 — `frontend/src/stores/auth.ts`
新增：
- `tokenExp`：从 `token`（JWT）payload 解码 `exp`（unix 秒），无 token 返回 0
- `isTokenExpiringSoon(thresholdSec = 300)`：`exp > 0 && exp - Math.floor(Date.now()/1000) < thresholdSec`

JWT 解码（不验签）：
```ts
function decodeJwtExp(token: string): number {
  try {
    const payload = token.split('.')[1]
    const json = atob(payload.replace(/-/g, '+').replace(/_/g, '/'))
    const obj = JSON.parse(json) as { exp?: number }
    return typeof obj.exp === 'number' ? obj.exp : 0
  } catch { return 0 }
}
```

#### 步骤 2 — `frontend/src/api/index.ts`
在 `fetchAuthed` 函数内、`fetch(url, ...)` 之前插入：

```ts
// 主动续期：距过期 < 5min 且非 auth 路径 → 先刷新再发请求
if (!_retry && !AUTH_SKIP_REFRESH.has(path)) {
  const auth = useAuthStore()
  if (auth.token && auth.isTokenExpiringSoon(300)) {
    await tryRefreshSession()
  }
}
```

保留现有 401 兜底重试不变（兜底用）。

#### 步骤 3 — 验证 `frontend/src/pages/FilesPage.vue`
确认：
- 无 `console.trace` 残留
- `_browseSeq` 序号保护保留（防极端并发兜底）

#### 步骤 4 — changelog
创建 `docs/changelog/2026-06-02-files-token-proactive-refresh.md`，至少 10 行：
- 日期、变更摘要、动机（双刷新根因）
- 涉及文件：`frontend/src/stores/auth.ts`、`frontend/src/api/index.ts`
- 验证方式：cursor-ide-browser hook `window.fetch` 监控
- 是否需迁移：否
- 是否需重启：仅前端重新部署

#### 步骤 5 — 构建 + 部署
```powershell
cd c:\Users\uzuma\Desktop\svn\Nexus\frontend
npx vite build
```

```bash
# WSL 中执行
cd /mnt/c/Users/uzuma/Desktop/svn/Nexus
tar czf /tmp/nf.tar.gz -C web/app index.html assets/
scp -i /tmp/nexus_key2 -o StrictHostKeyChecking=no /tmp/nf.tar.gz root@47.254.123.106:/tmp/
ssh -i /tmp/nexus_key2 -o StrictHostKeyChecking=no root@47.254.123.106 \
  'cd /www/wwwroot/api.synaglobal.vip/web/app && \
   rm -f assets/FilesPage-*.js assets/FilesPage-*.css && \
   rm -f assets/index-*.js assets/index-*.css && \
   tar xzf /tmp/nf.tar.gz && rm /tmp/nf.tar.gz'
```

#### 步骤 6 — 浏览器验证（必跑）
用 cursor-ide-browser MCP：

1. `browser_navigate` 到 `https://api.synaglobal.vip/app/#/login`
2. CDP `Runtime.evaluate` 填入 admin / Nexus@2026 → 点击登录
3. 注入 fetch 监控：
   ```js
   window.__realFetch = window.fetch.bind(window);
   window.__apiLog = [];
   window.fetch = (...args) => {
     const url = typeof args[0]==='string' ? args[0] : args[0]?.url;
     if (url?.includes('/sync/browse') || url?.includes('/auth/refresh')) {
       window.__apiLog.push({t: Date.now(), url});
     }
     return window.__realFetch(...args);
   };
   ```
4. `browser_navigate` 到 `https://api.synaglobal.vip/app/#/files?server_id=8`
5. 程序化点击 5 个不同目录：
   ```js
   document.querySelector('table tbody tr').click()
   ```
   每次等 2s
6. 读取 `JSON.stringify(window.__apiLog)`

**验收**：
- `/sync/browse` 出现次数 ≤ 5（每个目录 1 次）
- `/auth/refresh` 出现次数 = 1（启动时主动续期 1 次，或 0 次如未临近过期）
- **不出现 `/sync/browse` 和 `/auth/refresh` 两两紧贴的模式**（旧行为：browse → refresh → browse）

#### 步骤 7 — 提交
```bash
git add frontend/src/stores/auth.ts frontend/src/api/index.ts \
        docs/changelog/2026-06-02-files-token-proactive-refresh.md \
        docs/reports/2026-06-02-token-refresh-verification.md
git commit -m "fix(auth): JWT 主动续期，消除文件管理器进目录双 HTTP 请求

距过期 5min 主动刷新，避免业务请求触发 401 重试。
根因：每次进目录发 2 次 /sync/browse（401 → refresh → 重试）。
验证：cursor-ide-browser CDP hook fetch，进 5 目录 ≤ 5 次请求。"
git push origin main
```

### 不要做
- 不要重构 FilesPage.vue（P0 不动）
- 不要改后端
- 不要删除现有 `_browseSeq` 序号保护
- 不要把 401 兜底重试逻辑去掉（保留作为防御）

### 完成标志
- [ ] `tsc --noEmit` 通过
- [ ] `npx vite build` 成功
- [ ] 部署成功（health check OK）
- [ ] 浏览器验证：5 次点击 = 5 次 browse 请求（不是 10）
- [ ] changelog ≥ 10 行
- [ ] 验证报告含截图与 fetch log
- [ ] git push 完成

### 完成后回报
列出：
1. 实际改动文件清单
2. 浏览器验证日志（apiLog 完整 JSON）
3. 部署后的 bundle hash（FilesPage-*.js 与 index-*.js）
4. git commit hash
