docs: 更新架构文档、版本日志、TODO清单 (0.0.20)

- ARCHITECTURE.md: 新增任务轮询/文件上传/提交防抖/主动刷新定时器/失败分级五节,更新 hooks 目录结构,日期→06-12
- CHANGELOG.md: 新增 0.0.20 版本条目
- TODO.md: 标记公告接入/任务轮询为已完成,新增任务CSV导出/上传端点确认待办
- CLAUDE.md 保持不动(按规则不修改)
This commit is contained in:
2026-06-12 19:14:39 +08:00
parent 9e8c5b7109
commit 54d12aaf26
3 changed files with 216 additions and 7 deletions

View File

@@ -1,6 +1,6 @@
# 船长 · HeiXiu — 前端架构文档 # 船长 · HeiXiu — 前端架构文档
> 最后更新2026-06-09 > 最后更新2026-06-12
## 目录结构 ## 目录结构
@@ -14,6 +14,8 @@ src/
├── contexts/ # React Context 定义 + Consumer Hook ├── contexts/ # React Context 定义 + Consumer Hook
│ └── app-context.ts # AppContextValue 类型 + useAppContext() │ └── app-context.ts # AppContextValue 类型 + useAppContext()
├── hooks/ # 通用 Hook ├── hooks/ # 通用 Hook
│ ├── use-async.ts # 通用异步数据 HookuseAsyncData / useAsyncMutation
│ ├── use-api.ts # 业务 API Hook 集中管理Banner/Model/Task/Auth/Billing
│ ├── use-theme.ts # 主题切换 │ ├── use-theme.ts # 主题切换
│ └── use-updater.ts # 版本更新 │ └── use-updater.ts # 版本更新
├── pages/ # 页面 ├── pages/ # 页面
@@ -213,6 +215,49 @@ App 启动 → AppProvider useEffect
> 只有 RefreshErrorrefresh 接口返回 401才确认 token 真的过期。 > 只有 RefreshErrorrefresh 接口返回 401才确认 token 真的过期。
``` ```
### 3.2 主动刷新定时器v0.0.20
除被动 401 刷新外,系统通过 JWT `exp` 声明主动续期,避免长时间空闲后 access_token 过期:
```
登录/刷新成功
→ scheduleProactiveRefresh(access_token)
→ 解码 JWT exp 声明
→ setTimeout(delay = max(60s, remaining * 0.75))
→ 到期触发 → getStoredRefreshToken()
→ AuthAPI.refreshToken()
✅ 成功 → 更新双 token → 递归安排下一次刷新
❌ 失败 → 静默(等下次 401 走被动刷新)
```
- 主动刷新用 JWT 解码(`atob(token.split('.')[1])`)获取 `exp`,不依赖服务端 `expires_in`
- 在 75% 过期时间点触发,确保在被动 401 之前完成续期
- 至少间隔 60s防止 token 极短时频繁刷新)
- 退出登录时 `clearProactiveRefresh()` 清除定时器
### 3.3 刷新失败分级处理v0.0.20
`refreshHandler` 通过 Promise 状态区分失败级别,`handle401` 据此决定是否登出:
| 失败类型 | refreshHandler 行为 | handle401 响应 |
|---------|-------------------|---------------|
| refresh_token 过期/无效 | **throw** `RefreshTokenExpiredError` | 清 token + emit AUTH_REQUIRED → 弹登录 |
| 网络超时/无连接 | **return null** | 仅拒绝当前请求,保留 token → 下次再试 |
**关键改进**:临时网络故障不再误判为"需要重新登录"。用户在网络恢复后重试操作即可,无需手动登录。
```
handle401 内部:
refreshHandler()
.then(newToken =>
newToken → ✅ 用新 token 重试
null → ⚠️ 临时失败拒绝但不登出processQueue 失败,但 clearToken 不调用)
)
.catch(_err →
❌ 永久失败 → clearToken + emit(AUTH_REQUIRED) + 弹登录
)
```
### 4. 退出登录 ### 4. 退出登录
``` ```
@@ -382,3 +427,120 @@ export class AuthAPI {
- Mac 命令执行:`spawnSync` + 5s 超时,`ioreg`/`sysctl` 始终可用 - Mac 命令执行:`spawnSync` + 5s 超时,`ioreg`/`sysctl` 始终可用
- 共享同一套 `SHA256(normalizeIdentifier(combined))` 管线 - 共享同一套 `SHA256(normalizeIdentifier(combined))` 管线
- `getMachineCode()` 跨平台统一接口,自动选择最优采集方式 - `getMachineCode()` 跨平台统一接口,自动选择最优采集方式
---
## 任务状态轮询v0.0.20
### 架构:两层轮询 + Context 桥接
```
TaskHistory (列表页)
├─ useTaskPolling(tasks, onTerminal) ← 批量轮询 batchTaskStatus
│ └─ 仅轮询 POLLING_STATUSES 中的非终态任务
│ └─ 轮询结果合并到 polledTasks → dataSource
│ └─ 某任务变为终态 → onTerminal → setRefreshTick(+1) → 完整列表刷新
└─ useEffect: polledTasks 变化 → 同步 selectedTask 到 Context
OutputPreview (详情面板)
└─ useEffect: 监听 selectedTask.status 变化
└─ 非终态→终态 → useTaskDetail.refetch() ← 仅触发详情刷新,不独立轮询
```
**设计原则**
- 批量轮询负责广度(列表视图状态更新),详情刷新负责深度(终态后获取 output_assets
- OutputPreview **不独立调用** `getTaskStatus`,避免与批量轮询重复请求
- 两者通过 `HomeContext.selectedTask` 桥接,不直接通信
### 轮询实现细节
| 特性 | 实现 |
|------|------|
| 调度方式 | 递归 `setTimeout`(非 `setInterval`),避免请求堆积 |
| 间隔控制 | 服务端 `next_poll_ms`,本地 clamp 2s~30s |
| 错误重试 | 请求失败后 10s 重试 |
| 自动停止 | 所有非终态任务变为终态后停止轮询 |
| 闭包安全 | `mergedRef.current` 确保递归回调读到最新任务列表 |
| 非终态常量 | `POLLING_STATUSES = ['pending', 'submitted', 'processing']` 统一管理 |
### GET 数组参数序列化
axios 默认 `paramsSerializer` 对数组使用 `key[]=v1&key[]=v2`PHP/Rack bracket 风格),后端不识别。修复方案:
```typescript
// ❌ 错误axios 默认序列化为 task_ids[]=v1&task_ids[]=v2
get(url, { task_ids: ['id1', 'id2'] })
// ✅ 正确:逗号拼接为 task_ids=id1,id2跨框架兼容
get(url, { task_ids: params.task_ids.join(',') })
```
---
## 文件上传v0.0.20
### customRequest 集成模式
antd Upload 组件通过 `action` 属性使用自有的 XMLHttpRequest不走 axios 拦截器,导致 Token 缺失。解决方案:
```
useFileUpload() hook
└─ 返回 { customRequest } — 兼容 antd Upload.customRequest 签名
└─ 内部调用 request.upload(url, formData, onProgress)
└─ 复用 axios 实例Token 注入、超时、错误处理、业务码校验
```
```
ImageInput / ImageListInput / AudioListInput
└─ const { customRequest } = useFileUpload();
└─ <Upload customRequest={customRequest} beforeUpload={...} />
└─ beforeUpload 校验通过 → 返回 true → 放行至 customRequest
└─ customRequest 调用 request.upload() → 上传到 /api/v1/upload
└─ 成功后 antd 将 response 写入 file.response
└─ ModelInputForm 提交时读取 f.response?.url
```
### beforeUpload 语义
| 返回值 | 含义 |
|-------|------|
| `true` | 校验通过,交由 customRequest 实际上传 |
| `false` | 阻止上传(文件仅加入列表,不发起请求) |
| `Upload.LIST_IGNORE` | 校验失败,文件不加入列表 |
---
## 表单提交防抖v0.0.20
### 两级防护
```
handleSubmit()
├─ 1. 防抖节流2s
│ └─ lastClickTimeRef: now - last < 2s → "你点击的太快了"
├─ 2. 二次确认
│ └─ submitCountRef > 0 → Modal.confirm("您已提交,确认要再次提交吗?")
│ └─ 确认 → lastClickTimeRef = 0重置防抖→ doSubmit()
└─ 3. 执行提交
└─ doSubmit(): form.validateFields() → submitTask() → 检查返回值
```
### useRef 而非 useState
`lastClickTimeRef``submitCountRef` 使用 `useRef` 而非 `useState`,避免每次点击触发不必要的组件重渲染。
### useAsyncMutation 返回值判空
`useAsyncMutation.execute()` 内部 catch 所有错误并返回 `undefined`(不 throw。调用方应检查返回值而非 `try/catch`
```typescript
const result = await submitTask({...});
if (result !== undefined) {
submitCountRef.current += 1; // 成功
} else {
message.error(errorMessageRef.current || '任务提交失败');
}
```

View File

@@ -3,6 +3,44 @@
> 每次发版前修改此文件,`npm run build:win` 打包后会自动读取生成 update-info.json。 > 每次发版前修改此文件,`npm run build:win` 打包后会自动读取生成 update-info.json。
--- ---
## 0.0.202026-06-12
**任务状态轮询 + Token 刷新容错增强 + 提交防抖 + 文件上传 Hook**
### 任务状态轮询
- 新增 `useTaskPolling` hook批量轮询非终态任务状态递归 `setTimeout` + `next_poll_ms` 自适应间隔
- `TaskHistory` 集成批量轮询,`OutputPreview` 改为监听终态转换Context 桥接)触发 `refetch()`,消除重复请求
- `POLLING_STATUSES` 统一管理非终态状态,所有轮询逻辑引用此常量
- `batchTaskStatus` 使用 GET + 逗号拼接 `task_ids=a,b,c`,修复 axios 默认 bracket 序列化
### Token 刷新容错增强
- **主动刷新**JWT `exp` 解码驱动定时器,在 75% 过期点通过 `setTimeout` 提前续期
- **失败分级**`refreshHandler` 永久失败refresh_token 过期)→ **throw** → 清 token 弹登录;临时失败(网络超时)→ **return null** → 仅拒绝当前请求不登出
- 新增 `RefreshTokenExpiredError` 类区分失败类型
- `AppProvider` 登录/自动登录/退出 启停主动刷新定时器
### 提交按钮防抖
- 两级防护2s 节流防抖("你点击的太快了"+ 二次确认弹窗("您已提交,确认要再次提交吗?"
- `doSubmit` 抽取为 `useCallback``handleSubmit` 负责门控
- 修复 `useAsyncMutation.execute` 返回值判空(原 `try/catch` 为死代码)
### 文件上传 Hook
- 新增 `useFileUpload` hook封装 `request.upload()` 为 antd Upload 兼容 `customRequest`,复用全局拦截器
- `ImageInput` / `ImageListInput` / `AudioListInput` 集成 `customRequest`
- `uploadValidation.beforeUpload` 返回值修正:`false``true`(校验通过后放行至 `customRequest`
### 其他
- `AnnouncementDrawer` 接入公告 API + `EnumResolver`
- billing / announcement / banner 服务模块完善
- 文档ARCHITECTURE.md 新增轮询/Token刷新/上传/防抖章节
---
## 0.0.192026-06-11 ## 0.0.192026-06-11
**数据获取架构重构 + Mac 指纹采集 + Token 刷新容错 + 账单页面** **数据获取架构重构 + Mac 指纹采集 + Token 刷新容错 + 账单页面**

21
TODO.md
View File

@@ -1,6 +1,6 @@
# TODO — 船长·HeiXiu 待办与优化清单 # TODO — 船长·HeiXiu 待办与优化清单
> 最后更新2026-06-09 > 最后更新2026-06-12
--- ---
@@ -30,10 +30,17 @@
### 公告 ### 公告
- [x] 公告数据获取与展示AnnouncementDrawer + fetchAnnouncements API
- [ ] 公告已读/未读状态 - [ ] 公告已读/未读状态
- [ ] 公告红点/角标提醒(有新公告时导航栏图标变化) - [ ] 公告红点/角标提醒(有新公告时导航栏图标变化)
- [ ] 公告缓存(减少重复请求) - [ ] 公告缓存(减少重复请求)
### 任务
- [x] 任务状态轮询(批量轮询 + Context 桥接 + 自适应间隔)
- [ ] 任务列表导出 CSV 功能完善
- [ ] 任务重试/重新提交
--- ---
## 🟢 工程优化(优先级低) ## 🟢 工程优化(优先级低)
@@ -75,10 +82,12 @@
## 📋 下次开发计划 ## 📋 下次开发计划
1. **合规素材库页面**(功能待完善 — 按 QT 原版设计,以 Modal/Drawer 叠加而非路由跳转实现 1. **任务列表导出 CSV**`task.ts` 已定义 `exportTaskCSV` API前端未对接下载按钮
2. **各页面 nav 导航补齐**(后续页面优先使用叠加模式,遵循 QT 多窗口模型 → 见 WORKLOG 2026-06-07 2. **文件上传端点确认**`/api/v1/upload` 待后端确认,当前 useFileUpload 已完成前端集成
3. **leftWidth / rightWidth / taskPage 加 localStorage 持久化**UX 优化:刷新页面后保持用户偏好;已通过路由去路由化解决组件卸载问题,但 3. **合规素材库页面**(功能待完善 — 按 QT 原版设计,以 Modal/Drawer 叠加而非路由跳转实现)
4. **各页面 nav 导航补齐**(后续页面优先使用叠加模式,遵循 QT 多窗口模型 → 见 WORKLOG 2026-06-07
5. **leftWidth / rightWidth / taskPage 加 localStorage 持久化**UX 优化:刷新页面后保持用户偏好;已通过路由去路由化解决组件卸载问题,但
localStorage 持久层仍未实现) localStorage 持久层仍未实现)
4. **轮播图关闭按钮 localStorage 持久化**关闭按钮已实现待做localStorage 记录关闭时间戳,超过 N 天再显示) 6. **轮播图关闭按钮 localStorage 持久化**关闭按钮已实现待做localStorage 记录关闭时间戳,超过 N 天再显示)
5. **短信发送接入人机验证**(后期优化,需后端配合:`sendSms` 接口增加 `captcha_token` 参数 + 前端滑块/图形验证组件。当前仅靠 7. **短信发送接入人机验证**(后期优化,需后端配合:`sendSms` 接口增加 `captcha_token` 参数 + 前端滑块/图形验证组件。当前仅靠
60s 冷却按钮 + 后端限流,桌面端 ASAR 可解包 + DevTools 网络面板 + 代理抓包均能直接暴露 API纯前端限制对脚本攻击无效 60s 冷却按钮 + 后端限流,桌面端 ASAR 可解包 + DevTools 网络面板 + 代理抓包均能直接暴露 API纯前端限制对脚本攻击无效