fix(frontend): generic fallback at every error-copy exit (ledger #11)

- new api_errors `fallback` key (3 languages); naked i18n keys can no longer
  reach users when a code has no translation (gateway codes, pre-CI legacy)
- client (6 exits): SSE/WS/chat-input/route/request-layer fall back to generic
  Chinese; t(key, vars) kept so {{xxx}} templates still interpolate
- platform (5 exits): fall back status_message -> generic copy (admin keeps
  backend diagnostics); chatErrorMessage's literal "error" fallback removed
This commit is contained in:
dolphin
2026-07-23 22:37:51 +08:00
parent 8f41e6a017
commit 3790392dff
19 changed files with 35 additions and 19 deletions
+8 -1
View File
@@ -121,7 +121,14 @@ export const translateApiErrorMessage = (data: any) => {
if (statusMessageKey && i18next.exists(statusMessageKey)) {
return i18next.t(statusMessageKey, data?.data);
}
return statusMessage || (statusCodeKey ? i18next.t(statusCodeKey, data?.data) : "");
// Last resort keeps t(key, vars) so {{xxx}} templates still interpolate from
// data.data; defaultValue guards the truly-untranslated case — never the raw key.
return (
statusMessage ||
(statusCodeKey
? String(i18next.t(statusCodeKey, { ...(data?.data || {}), defaultValue: String(i18next.t("api_errors.fallback")) }))
: "")
);
};
// License degradation (gateway returns 11001): throttle the toast so a burst of
+1 -1
View File
@@ -227,7 +227,7 @@ export default function useSSE(
// localize
const _data = {
text: localize(`api_errors.${data?.status_code}`, data?.data),
text: localize(`api_errors.${data?.status_code}`, { ...(data?.data || {}), defaultValue: localize('api_errors.fallback') }),
}
errorHandler({ data: _data, submission: { ...submission, userMessage } as EventSubmission });
});
@@ -392,5 +392,6 @@
"24005": "Invalid quota_config or department_id value",
"90001": "You do not have permission to access the admin backend. Please contact the administrator to request access if needed.",
"90002": "Your current role does not have permission to access the workbench. Please contact the administrator if needed.",
"personIdAlreadyExists": "Person ID already exists"
"personIdAlreadyExists": "Person ID already exists",
"fallback": "The operation failed. Please try again later."
}
@@ -392,5 +392,6 @@
"24005": "quota_configまたはdepartment_idの値が無効です。",
"90001": "現在のアカウントには管理バックエンドへのアクセス権限がありません。必要な場合は、管理者に連絡して権限を開通してください。",
"90002": "現在のロールにはワークベンチへのアクセス権限がありません。必要な場合は管理者に連絡してください。",
"personIdAlreadyExists": "Person ID は既に存在します"
"personIdAlreadyExists": "Person ID は既に存在します",
"fallback": "操作に失敗しました。しばらくしてから再試行してください。"
}
@@ -392,5 +392,6 @@
"24005": "quota_config 或 department_id 配置无效。",
"90001": "您当前角色没有访问管理后台的权限。如有需要,请联系管理员开通。",
"90002": "您当前角色没有访问工作台的权限。如有需要,请联系管理员开通。",
"personIdAlreadyExists": "人员 ID 已存在"
"personIdAlreadyExists": "人员 ID 已存在",
"fallback": "操作失败,请稍后重试"
}
@@ -37,7 +37,7 @@ export default function ChatInput({ readOnly, v }) {
// ... Placeholder 和 AutoFocus 逻辑保持不变 ...
const placholder = useMemo(() => {
return inputDisabled ?
(inputMsg.code ? localize(`api_errors.${inputMsg.code}`, inputMsg.data) : ' ')
(inputMsg.code ? localize(`api_errors.${inputMsg.code}`, { ...(inputMsg.data || {}), defaultValue: localize('api_errors.fallback') }) : ' ')
: localize('com_ui_please_enter_question')
}, [inputDisabled, inputMsg, localize]);
@@ -204,7 +204,7 @@ export const useWebSocket = (helpers) => {
}
if (![10421, 13002].includes(code)) {
showToast({
message: code === 500 ? message : localize(`api_errors.${String(code)}`, data.message?.data),
message: code === 500 ? message : localize(`api_errors.${String(code)}`, { ...(data.message?.data || {}), defaultValue: localize('api_errors.fallback') }),
severity: NotificationSeverity.ERROR,
})
} else {
+1 -1
View File
@@ -55,7 +55,7 @@ const useErrorPrompt = () => {
useEffect(() => {
if (error) {
showToast({ message: localize(`api_errors.${error}`), status: 'error' });
showToast({ message: localize(`api_errors.${error}`, { defaultValue: localize('api_errors.fallback') }), status: 'error' });
}
}, []);
};
@@ -392,5 +392,6 @@
"24005": "Invalid quota_config or department_id value",
"90001": "You do not have permission to access the admin backend. Please contact the administrator to request access if needed.",
"90002": "Your current role does not have permission to access the workbench. Please contact the administrator if needed.",
"personIdAlreadyExists": "Person ID already exists"
"personIdAlreadyExists": "Person ID already exists",
"fallback": "The operation failed. Please try again later."
}
@@ -392,5 +392,6 @@
"24005": "quota_configまたはdepartment_idの値が無効です。",
"90001": "現在のアカウントには管理バックエンドへのアクセス権限がありません。必要な場合は、管理者に連絡して権限を開通してください。",
"90002": "現在のロールにはワークベンチへのアクセス権限がありません。必要な場合は管理者に連絡してください。",
"personIdAlreadyExists": "Person ID は既に存在します"
"personIdAlreadyExists": "Person ID は既に存在します",
"fallback": "操作に失敗しました。しばらくしてから再試行してください。"
}
@@ -392,5 +392,6 @@
"24005": "quota_config 或 department_id 配置无效。",
"90001": "您当前角色没有访问管理后台的权限。如有需要,请联系管理员开通。",
"90002": "您当前角色没有访问工作台的权限。如有需要,请联系管理员开通。",
"personIdAlreadyExists": "人员 ID 已存在"
"personIdAlreadyExists": "人员 ID 已存在",
"fallback": "操作失败,请稍后重试"
}
@@ -392,5 +392,6 @@
"24005": "Invalid quota_config or department_id value",
"90001": "You do not have permission to access the admin backend. Please contact the administrator to request access if needed.",
"90002": "Your current role does not have permission to access the workbench. Please contact the administrator if needed.",
"personIdAlreadyExists": "Person ID already exists"
"personIdAlreadyExists": "Person ID already exists",
"fallback": "The operation failed. Please try again later."
}
@@ -392,5 +392,6 @@
"24005": "quota_configまたはdepartment_idの値が無効です。",
"90001": "現在のアカウントには管理バックエンドへのアクセス権限がありません。必要な場合は、管理者に連絡して権限を開通してください。",
"90002": "現在のロールにはワークベンチへのアクセス権限がありません。必要な場合は管理者に連絡してください。",
"personIdAlreadyExists": "Person ID は既に存在します"
"personIdAlreadyExists": "Person ID は既に存在します",
"fallback": "操作に失敗しました。しばらくしてから再試行してください。"
}
@@ -392,5 +392,6 @@
"24005": "quota_config 或 department_id 配置无效。",
"90001": "您当前角色没有访问管理后台的权限。如有需要,请联系管理员开通。",
"90002": "您当前角色没有访问工作台的权限。如有需要,请联系管理员开通。",
"personIdAlreadyExists": "人员 ID 已存在"
"personIdAlreadyExists": "人员 ID 已存在",
"fallback": "操作失败,请稍后重试"
}
+1 -1
View File
@@ -185,7 +185,7 @@ export default function App() {
useEffect(() => {
if (window.url_error) {
toast({
description: t(`api_errors:${window.url_error}`, { defaultValue: String(window.url_error) }),
description: t(`api_errors:${window.url_error}`, { defaultValue: t('api_errors:fallback') }),
variant: 'error',
});
delete window.url_error
@@ -33,7 +33,7 @@ export function resolveChatErrorMessage(
payload: ChatErrorPayload | null | undefined,
translate: Translate,
): string {
const fallbackMessage = payload?.status_message || "error";
const fallbackMessage = payload?.status_message || String(translate("api_errors:fallback") ?? "");
const statusCode = payload?.status_code;
const interpolationParams =
@@ -283,7 +283,7 @@ export default function ChatInput({ autoRun, version, clear, form, wsUrl, onBefo
}
// 记录
const errorMsg = status_code == 500 ? status_message || data.message.message : t(`api_errors:${status_code}`, params)
const errorMsg = status_code == 500 ? status_message || data.message.message : t(`api_errors:${status_code}`, { ...(params || {}), defaultValue: status_message || t('api_errors:fallback') })
addNotification({
type: 'error',
title: 'runtime error',
@@ -64,7 +64,7 @@ export const useErrorPrompt = () => {
useEffect(() => {
if (error) {
toast({ description: t(`api_errors:${error}`), variant: 'error' });
toast({ description: t(`api_errors:${error}`, { defaultValue: t('api_errors:fallback') }), variant: 'error' });
// Clear the 'error' parameter from the URL
const newUrl = window.location.origin + window.location.pathname;
@@ -93,7 +93,7 @@ export const StatusIndicator: React.FC<StatusIndicatorProps> = ({ status, remark
}
const mediaMessage = formatMediaParseFailureMessage(obj, t);
if (mediaMessage) return mediaMessage;
return t(`api_errors:${obj.status_code}`, obj.data)
return t(`api_errors:${obj.status_code}`, { ...(obj.data || {}), defaultValue: obj.status_message || t('api_errors:fallback') })
} catch (error) {
return trimmedRemark
}