fix: P0+P1 security and quality fixes

P0-1: Token refresh race condition — reject all pending requests on refresh failure
P0-2: Remove X-Forwarded-For trust in rate limiting — use only ConnectInfo IP
P1-3: Template grid reactive — use useSaaSStore() hook instead of getState()
P1-4: Agent Template detail modal — show emoji, personality, soul_content, welcome_message,
      communication_style, source_id, scenarios, version
P1-5: adminRouting parse validation — type-safe llm_routing extraction from localStorage
P1-6: Remove unused @ant-design/charts dependency
P1-extra: Type addKeyMutation data parameter (replace any)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-31 09:17:04 +08:00
parent 49abd0fe89
commit 1d9283f335
7 changed files with 79 additions and 1401 deletions

View File

@@ -45,10 +45,18 @@ request.interceptors.request.use((config: InternalAxiosRequestConfig) => {
// ── 响应拦截器401 自动刷新 ──────────────────────────────
let isRefreshing = false
let pendingRequests: Array<(token: string) => void> = []
let pendingRequests: Array<{
resolve: (token: string) => void
reject: (error: unknown) => void
}> = []
function onTokenRefreshed(newToken: string) {
pendingRequests.forEach((cb) => cb(newToken))
pendingRequests.forEach(({ resolve }) => resolve(newToken))
pendingRequests = []
}
function onTokenRefreshFailed(error: unknown) {
pendingRequests.forEach(({ reject }) => reject(error))
pendingRequests = []
}
@@ -67,10 +75,13 @@ request.interceptors.response.use(
}
if (isRefreshing) {
return new Promise((resolve) => {
pendingRequests.push((newToken: string) => {
originalRequest.headers.Authorization = `Bearer ${newToken}`
resolve(request(originalRequest))
return new Promise((resolve, reject) => {
pendingRequests.push({
resolve: (newToken: string) => {
originalRequest.headers.Authorization = `Bearer ${newToken}`
resolve(request(originalRequest))
},
reject,
})
})
}
@@ -93,10 +104,12 @@ request.interceptors.response.use(
onTokenRefreshed(newToken)
originalRequest.headers.Authorization = `Bearer ${newToken}`
return request(originalRequest)
} catch {
} catch (refreshError) {
// 关键修复:刷新失败时 reject 所有等待中的请求,避免它们永远 hang
onTokenRefreshFailed(refreshError)
store.logout()
window.location.href = '/login'
return Promise.reject(error)
return Promise.reject(refreshError)
} finally {
isRefreshing = false
}
@@ -112,7 +125,14 @@ request.interceptors.response.use(
return Promise.reject(new ApiRequestError(error.response.status, body))
}
return Promise.reject(error)
// 网络错误统一包装为 ApiRequestError
return Promise.reject(
new ApiRequestError(0, {
error: 'network_error',
message: error.message || '网络连接失败,请检查网络后重试',
status: 0,
})
)
},
)