fix(#1718/#1714): 微信回调state误杀修复+错误透传防连点、上传失败完整可观测、哈希阈值降至64MB、昵称不预填 (#1723)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 27s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 28s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m28s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m39s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m12s
AI Code Review / AI Code Review (pull_request) Failing after 6m15s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 0s
CI/CD Pipeline / Check push changed paths (push) Successful in 1s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 21s
CI/CD Pipeline / Build Staging API Image (push) Successful in 21s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 22s
CI/CD Pipeline / Integration Tests (push) Successful in 1m44s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m30s
CI/CD Pipeline / Validate - Style (push) Successful in 2m20s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m19s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m14s
CI/CD Pipeline / Unit Tests (push) Successful in 8m4s
CI/CD Pipeline / Validate - Security (push) Successful in 16m53s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 255h51m59s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 255h52m3s
CI/CD Pipeline / Build Production Web Image (push) Failing after 255h52m3s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 256h8m32s
CI/CD Pipeline / Deploy Production (push) Failing after 255h51m59s
CI/CD Pipeline / Frontend Lint (push) Failing after 256h8m58s
CI/CD Pipeline / Build Production API Image (push) Failing after 255h52m3s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 256h6m56s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 256h6m56s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 256h27m48s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 256h29m25s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 256h29m30s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 256h29m31s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 256h29m25s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 256h29m34s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 256h29m35s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 256h29m36s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 256h29m35s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 256h29m37s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 256h31m30s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 256h31m30s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 256h31m30s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 256h31m33s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 256h31m34s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 256h31m34s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 256h31m35s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 256h8m58s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 256h31m35s
CI/CD Pipeline / PR Build API Image (push) Failing after 256h8m58s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 256h31m37s
CI/CD Pipeline / CI Gate (push) Failing after 256h26m38s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 256h43m7s
CI/CD Pipeline / PR Build Web Image (push) Failing after 256h43m32s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 256h43m36s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 256h41m30s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 256h43m6s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 257h4m4s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 257h4m8s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 257h4m10s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 257h6m5s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 257h6m8s

This commit was merged in pull request #1723.
This commit is contained in:
2026-09-05 22:52:49 +08:00
parent c8b1c4b8ff
commit cdcb032e45
15 changed files with 468 additions and 55 deletions
+9 -1
View File
@@ -126,7 +126,15 @@ export const prepareDirectUploadHandle = async (data: {
/** 本次逻辑上传的幂等 token,prepare/complete 一致、重试复用 */
clientUploadId?: string
}): Promise<DirectUploadHandle> => {
const project = await getOrCreateDefaultProject()
// 默认项目初始化失败(项目列表接口异常/自动创建失败)给出独立、明确的提示,
// 不与 prepare 的签名接口错误混在一起
let project: Awaited<ReturnType<typeof getOrCreateDefaultProject>>
try {
project = await getOrCreateDefaultProject()
} catch (err) {
const reason = err instanceof Error ? err.message : "网络异常"
throw new Error(`初始化默认项目失败,无法开始上传:${reason}`)
}
const prepared = await prepareDirectUpload({
project_id: project.id,
+7 -7
View File
@@ -13,10 +13,10 @@
* 重试复用同一 ID,重新入队才生成新 ID)
*/
/** 大文件抽样阈值:超过此大小只哈希头尾片段,避免上传前长时间卡 UI */
export const HASH_FULL_READ_LIMIT = 256 * 1024 * 1024 // 256MB
/** 抽样读取的头尾片段大小(各 8MB) */
export const HASH_SAMPLE_CHUNK = 8 * 1024 * 1024
/** 全量哈希阈值:≤64MB 全量读入计算;超过即走头尾抽样,避免 100~256MB 视频被整文件读进内存卡死页面 */
export const HASH_FULL_READ_LIMIT = 64 * 1024 * 1024 // 64MB
/** 抽样读取的头尾片段大小(各 16MB) */
export const HASH_SAMPLE_CHUNK = 16 * 1024 * 1024
/** 计算指纹时,文件在队列中已存在的状态(已失败的可以重试,不算重复) */
export type DedupExcludeStatus = "error" | "done"
@@ -95,10 +95,10 @@ function toHex(buffer: ArrayBuffer): string {
/**
* 计算文件内容 SHA-256(hex,64 字符,与后端 file_hash 字段长度一致)。
* - ≤256MB:全量哈希,内容一致必然一致
* - >256MB:哈希「头部 8MB + 尾部 8MB + 文件大小」,视频素材体积大、
* - ≤64MB:全量哈希,内容一致必然一致
* - >64MB:哈希「头部 16MB + 尾部 16MB + 文件大小」,视频素材体积大、
* 头部含 moov 元数据、尾部含 mdat 结尾,抽样碰撞概率可忽略,
* 且避免上传前对 2GB 文件全量读取造成长时间卡顿
* 且避免 100~256MB 视频被整文件读进内存导致页面卡死/崩溃
*
* 运行环境不支持 crypto.subtle(非安全上下文/老浏览器)时返回空字符串,
* 调用方据此降级为不传 hash(后端仍有幂等 token + 同文件名兜底去重)。
+132
View File
@@ -0,0 +1,132 @@
/**
* 统一错误信息提取
* 把 axios 错误(后端 detail / FastAPI 校验错误 / HTTP 状态码)、XHR/OSS 错误、
* 网络/超时错误、普通 Error 统一转成「可直接展示给用户」的中文信息。
*
* 与 api/client.ts 响应拦截器的提示口径保持一致;拦截器负责全局 toast,
* 页面/队列卡片用本工具把真实原因展示在持久位置(回调页、失败卡片等)。
*/
import type { AxiosError } from "axios"
/** 后端错误响应体可能出现的字段(FastAPI:detail;历史接口:message/msg) */
interface ErrorBody {
detail?: unknown
message?: unknown
msg?: unknown
}
/** FastAPI 422 校验错误单项 */
interface ValidationItem {
loc?: (string | number)[]
msg?: string
}
/** 从后端响应体提取人类可读信息(detail 可能是字符串、对象、422 数组) */
function extractBodyMessage(data: unknown): string {
if (!data || typeof data !== "object") return ""
const body = data as ErrorBody
const walk = (val: unknown): string => {
if (typeof val === "string") return val
if (Array.isArray(val)) {
// FastAPI 422: [{loc, msg, type}, ...] → 取每条 msg 拼接
const parts = val
.map((item) => {
if (typeof item === "string") return item
if (item && typeof item === "object") {
const v = item as ValidationItem
if (typeof v.msg === "string") {
const field = Array.isArray(v.loc) ? v.loc.filter((x) => x !== "body").join(".") : ""
return field ? `${field}: ${v.msg}` : v.msg
}
return walk(item)
}
return ""
})
.filter(Boolean)
return parts.join(";")
}
if (val && typeof val === "object") {
const obj = val as Record<string, unknown>
if (typeof obj.message === "string") return obj.message
if (typeof obj.msg === "string") return obj.msg
if (typeof obj.detail === "string") return obj.detail
if (obj.message && typeof obj.message === "object") return walk(obj.message)
if (obj.msg && typeof obj.msg === "object") return walk(obj.msg)
try {
return JSON.stringify(val)
} catch {
return ""
}
}
return ""
}
return walk(body.detail) || walk(body.message) || walk(body.msg)
}
/** 无响应体时按 HTTP 状态码给出兜底提示(与 client.ts 拦截器口径一致) */
function statusFallback(status: number): string {
switch (status) {
case 400:
return "请求参数有误(HTTP 400)"
case 401:
return "登录状态已失效,请重新登录(HTTP 401)"
case 403:
return "没有权限执行该操作(HTTP 403)"
case 404:
return "请求的资源不存在(HTTP 404)"
case 409:
return "操作冲突,资源状态已变化(HTTP 409)"
case 413:
return "文件过大,请缩小后重试(HTTP 413)"
case 415:
return "不支持的文件格式(HTTP 415)"
case 429:
return "操作过于频繁,请稍后再试(HTTP 429)"
case 503:
return "服务暂不可用,请稍后再试(HTTP 503)"
default:
if (status >= 500) return `服务器繁忙,请稍后再试(HTTP ${status})`
return `请求失败(HTTP ${status})`
}
}
/**
* 从任意抛出值提取可展示的错误信息。
* @param fallback 全部提取失败时的兜底文案
*/
export function getErrorMessage(err: unknown, fallback = "操作失败,请稍后重试"): string {
if (!err) return fallback
// axios 错误(后端 JSON 响应 / HTTP 错误状态)
const ax = err as AxiosError<ErrorBody>
if (ax.isAxiosError || (typeof ax === "object" && "response" in (ax as object))) {
// 超时
if (ax.code === "ECONNABORTED" || /timeout/i.test(ax.message || "")) {
return "请求超时,请检查网络后重试"
}
const resp = ax.response
if (resp) {
const bodyMsg = extractBodyMessage(resp.data)
if (bodyMsg) return bodyMsg
return statusFallback(resp.status)
}
// 请求已发出但无响应(断网/CORS/DNS)
if (ax.request) return "网络连接异常,请检查网络设置"
return ax.message || fallback
}
if (err instanceof Error) {
// XHR 直传 OSS 失败等场景自带详细 message(含 HTTP 状态 + OSS Code/Message)
if (err.message) return err.message
}
if (typeof err === "string") return err
return fallback
}
/** client.ts 拦截器是否已对该错误弹过全局 toast(__msgShown 标记) */
export function isErrorMsgShown(err: unknown): boolean {
return Boolean((err as { __msgShown?: boolean } | null)?.__msgShown)
}