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:
164
ARCHITECTURE.md
164
ARCHITECTURE.md
@@ -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 # 通用异步数据 Hook(useAsyncData / 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
|
|||||||
> 只有 RefreshError(refresh 接口返回 401)才确认 token 真的过期。
|
> 只有 RefreshError(refresh 接口返回 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 || '任务提交失败');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
38
CHANGELOG.md
38
CHANGELOG.md
@@ -3,6 +3,44 @@
|
|||||||
> 每次发版前修改此文件,`npm run build:win` 打包后会自动读取生成 update-info.json。
|
> 每次发版前修改此文件,`npm run build:win` 打包后会自动读取生成 update-info.json。
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 0.0.20(2026-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.19(2026-06-11)
|
## 0.0.19(2026-06-11)
|
||||||
|
|
||||||
**数据获取架构重构 + Mac 指纹采集 + Token 刷新容错 + 账单页面**
|
**数据获取架构重构 + Mac 指纹采集 + Token 刷新容错 + 账单页面**
|
||||||
|
|||||||
21
TODO.md
21
TODO.md
@@ -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,纯前端限制对脚本攻击无效)
|
||||||
|
|||||||
Reference in New Issue
Block a user