From 8a5a6ae28e4d9ed648b6c18f3d22239d25d6a0d1 Mon Sep 17 00:00:00 2001 From: orenzhang <41963680+OrenZhang@users.noreply.github.com> Date: Mon, 18 May 2026 19:32:25 +0800 Subject: [PATCH 1/2] feat(risk): enhance risk info structure with detailed risk items --- .../common/risk/risk-block-dialog.tsx | 3 +- .../components/common/risk/risk-info-box.tsx | 39 ++++++++-- frontend/lib/services/core/api-client.ts | 74 ++++++++++++++++--- internal/apps/oauth/risk.go | 38 ++++++++-- 4 files changed, 131 insertions(+), 23 deletions(-) diff --git a/frontend/components/common/risk/risk-block-dialog.tsx b/frontend/components/common/risk/risk-block-dialog.tsx index b3f003b..8e8432c 100644 --- a/frontend/components/common/risk/risk-block-dialog.tsx +++ b/frontend/components/common/risk/risk-block-dialog.tsx @@ -18,7 +18,8 @@ function isRiskInfo(value: unknown): value is RiskInfo { const riskInfo = value as Partial; return ( typeof riskInfo.risk_level === 'string' && - Array.isArray(riskInfo.risk_labels) + Array.isArray(riskInfo.risk_labels) && + Array.isArray(riskInfo.risks) ); } diff --git a/frontend/components/common/risk/risk-info-box.tsx b/frontend/components/common/risk/risk-info-box.tsx index f26393f..546ce42 100644 --- a/frontend/components/common/risk/risk-info-box.tsx +++ b/frontend/components/common/risk/risk-info-box.tsx @@ -1,9 +1,16 @@ import {Badge} from '@/components/ui/badge'; import {cn} from '@/lib/utils'; +export interface RiskItem { + label: string; + value?: string; + desc?: string; +} + export interface RiskInfo { risk_level: string; risk_labels: string[]; + risks: RiskItem[]; } export function RiskInfoBox({ @@ -16,7 +23,7 @@ export function RiskInfoBox({ labelClassName?: string; }) { return ( -
+
{riskInfo?.risk_level || '未知'}
-
- {riskInfo?.risk_labels.length ? ( - riskInfo.risk_labels.map((label) => ( - - {label} - +
+ {riskInfo?.risks.length ? ( + riskInfo.risks.map((risk) => ( +
+ + {risk.label} + + {risk.desc ? ( +

+ {risk.desc} +

+ ) : null} +
)) + ) : riskInfo?.risk_labels.length ? ( +
+ {riskInfo.risk_labels.map((label) => ( + + {label} + + ))} +
) : ( 暂无风险标签详情 diff --git a/frontend/lib/services/core/api-client.ts b/frontend/lib/services/core/api-client.ts index 876446f..22c9832 100644 --- a/frontend/lib/services/core/api-client.ts +++ b/frontend/lib/services/core/api-client.ts @@ -16,12 +16,20 @@ const apiClient = axios.create({ const RISK_LEVEL_HEADER = 'x-credit-risk-level'; const RISK_LABELS_HEADER = 'x-credit-risk-labels'; +const RISK_ITEMS_HEADER = 'x-credit-risks'; const RISK_BLOCKED_CODE = 'RISK_BLOCKED'; const RISK_BLOCKED_EVENT = 'credit-risk-blocked'; +interface RiskItem { + label: string; + value?: string; + desc?: string; +} + interface RiskInfo { risk_level: string; risk_labels: string[]; + risks: RiskItem[]; } class ApiClientError extends Error { @@ -59,31 +67,75 @@ function getHeader( return typeof directValue === 'string' ? directValue : undefined; } -function decodeRiskLabels(value?: string): string[] { +function decodeBase64JSON(value?: string): unknown { if (!value || typeof window === 'undefined') return []; try { const binary = window.atob(value); const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)); const json = new TextDecoder().decode(bytes); - const labels = JSON.parse(json); - return Array.isArray(labels) ? - labels.filter((label): label is string => typeof label === 'string') : - []; + return JSON.parse(json); } catch { return []; } } +function normalizeRiskItems(value: unknown): RiskItem[] { + if (!Array.isArray(value)) return []; + + return value.reduce((items, item) => { + if (!item || typeof item !== 'object') return items; + + const label = + 'label' in item ? (item as { label?: unknown }).label : undefined; + if (typeof label !== 'string' || !label.trim()) return items; + + const value = + 'value' in item ? (item as { value?: unknown }).value : undefined; + const desc = + 'desc' in item ? (item as { desc?: unknown }).desc : undefined; + items.push({ + label: label.trim(), + value: typeof value === 'string' ? value.trim() : undefined, + desc: typeof desc === 'string' ? desc.trim() : undefined, + }); + + return items; + }, []); +} + +function normalizeRiskLabels(value: unknown): string[] { + return Array.isArray(value) ? + value + .filter( + (label): label is string => + typeof label === 'string' && !!label.trim(), + ) + .map((label) => label.trim()) : + []; +} + +function riskLabelsFromItems(items: RiskItem[]): string[] { + return items.map((item) => item.label).filter(Boolean); +} + function riskInfoFromHeaders( headers: AxiosResponse['headers'], ): RiskInfo | null { const riskLevel = getHeader(headers, RISK_LEVEL_HEADER); if (!riskLevel) return null; + const risks = normalizeRiskItems( + decodeBase64JSON(getHeader(headers, RISK_ITEMS_HEADER)), + ); + const labels = normalizeRiskLabels( + decodeBase64JSON(getHeader(headers, RISK_LABELS_HEADER)), + ); + return { risk_level: riskLevel, - risk_labels: decodeRiskLabels(getHeader(headers, RISK_LABELS_HEADER)), + risk_labels: labels.length ? labels : riskLabelsFromItems(risks), + risks, }; } @@ -98,13 +150,17 @@ function riskInfoFromDetails(details: unknown): RiskInfo | null { 'risk_labels' in details ? (details as { risk_labels?: unknown }).risk_labels : undefined; + const riskItems = + 'risks' in details ? (details as { risks?: unknown }).risks : undefined; if (typeof riskLevel !== 'string' || !riskLevel) return null; + const risks = normalizeRiskItems(riskItems); + const labels = normalizeRiskLabels(riskLabels); + return { risk_level: riskLevel, - risk_labels: Array.isArray(riskLabels) ? - riskLabels.filter((label): label is string => typeof label === 'string') : - [], + risk_labels: labels.length ? labels : riskLabelsFromItems(risks), + risks, }; } diff --git a/internal/apps/oauth/risk.go b/internal/apps/oauth/risk.go index 1cc7437..ef97f5a 100644 --- a/internal/apps/oauth/risk.go +++ b/internal/apps/oauth/risk.go @@ -45,6 +45,7 @@ const ( riskLevelHeader = "X-Credit-Risk-Level" riskLabelsHeader = "X-Credit-Risk-Labels" + riskItemsHeader = "X-Credit-Risks" exposeHeader = "Access-Control-Expose-Headers" riskBlockedCode = "RISK_BLOCKED" @@ -54,6 +55,7 @@ const ( type openAPIUserRiskItem struct { Label string `json:"label"` Value string `json:"value"` + Desc string `json:"desc"` } type openAPIUserRiskResponse struct { @@ -63,8 +65,9 @@ type openAPIUserRiskResponse struct { } type riskBlockDetails struct { - RiskLevel string `json:"risk_level"` - RiskLabels []string `json:"risk_labels"` + RiskLevel string `json:"risk_level"` + RiskLabels []string `json:"risk_labels"` + Risks []openAPIUserRiskItem `json:"risks"` } func checkOpenAPIUserRisk(ctx context.Context, userID uint64) (*openAPIUserRiskResponse, bool) { @@ -157,37 +160,46 @@ func applyOpenAPIUserRisk(c *gin.Context, risk *openAPIUserRiskResponse) bool { } labels := riskLabels(risk) + items := riskItems(risk) cfg := config.Config.OpenAPIRisk if containsString(cfg.BlockRiskLevels, risk.RiskLevel) { - setRiskHeaders(c, risk.RiskLevel, labels) + setRiskHeaders(c, risk.RiskLevel, labels, items) c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ "error_code": riskBlockedCode, "error_msg": riskBlockedMsg, "details": riskBlockDetails{ RiskLevel: risk.RiskLevel, RiskLabels: labels, + Risks: items, }, }) return true } if containsString(cfg.PromptRiskLevels, risk.RiskLevel) { - setRiskHeaders(c, risk.RiskLevel, labels) + setRiskHeaders(c, risk.RiskLevel, labels, items) } return false } -func setRiskHeaders(c *gin.Context, riskLevel string, labels []string) { +func setRiskHeaders(c *gin.Context, riskLevel string, labels []string, items []openAPIUserRiskItem) { labelsJSON, err := json.Marshal(labels) if err != nil { logger.ErrorF(c.Request.Context(), "[OpenAPIRisk] marshal risk labels failed: %v", err) return } + itemsJSON, err := json.Marshal(items) + if err != nil { + logger.ErrorF(c.Request.Context(), "[OpenAPIRisk] marshal risk items failed: %v", err) + return + } + c.Header(riskLevelHeader, riskLevel) c.Header(riskLabelsHeader, base64.StdEncoding.EncodeToString(labelsJSON)) - appendExposeHeaders(c, riskLevelHeader, riskLabelsHeader) + c.Header(riskItemsHeader, base64.StdEncoding.EncodeToString(itemsJSON)) + appendExposeHeaders(c, riskLevelHeader, riskLabelsHeader, riskItemsHeader) } func appendExposeHeaders(c *gin.Context, names ...string) { @@ -228,6 +240,20 @@ func riskLabels(risk *openAPIUserRiskResponse) []string { return labels } +func riskItems(risk *openAPIUserRiskResponse) []openAPIUserRiskItem { + items := make([]openAPIUserRiskItem, 0, len(risk.Risks)) + for _, item := range risk.Risks { + item.Label = strings.TrimSpace(item.Label) + item.Value = strings.TrimSpace(item.Value) + item.Desc = strings.TrimSpace(item.Desc) + if item.Label == "" { + continue + } + items = append(items, item) + } + return items +} + func containsString(values []string, target string) bool { target = strings.TrimSpace(target) for _, value := range values { From dc1d3642e1cee31f8cff70da298895420cd3c68d Mon Sep 17 00:00:00 2001 From: orenzhang <41963680+OrenZhang@users.noreply.github.com> Date: Mon, 18 May 2026 19:36:25 +0800 Subject: [PATCH 2/2] feat(risk): update risk warning toast styling for improved visibility --- frontend/components/common/risk/risk-warning-toast.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/components/common/risk/risk-warning-toast.tsx b/frontend/components/common/risk/risk-warning-toast.tsx index 9e451d8..e36e4b9 100644 --- a/frontend/components/common/risk/risk-warning-toast.tsx +++ b/frontend/components/common/risk/risk-warning-toast.tsx @@ -31,6 +31,12 @@ export function showRiskWarningToast(riskInfo: RiskInfo) { id: RISK_TOAST_ID, duration: Infinity, dismissible: true, + style: { + background: 'transparent', + border: 'none', + boxShadow: 'none', + padding: 0, + }, onDismiss: () => { isRiskWarningDismissed = true; },