Commit 39e05a2d authored by shaw's avatar shaw
Browse files

feat: 新增全局错误透传规则功能

支持管理员配置上游错误如何返回给客户端:
- 新增 ErrorPassthroughRule 数据模型和 Ent Schema
- 实现规则的 CRUD API(/admin/error-passthrough-rules)
- 支持按错误码、关键词匹配,支持 any/all 匹配模式
- 支持按平台过滤(anthropic/openai/gemini/antigravity)
- 支持透传或自定义响应状态码和错误消息
- 实现两级缓存(Redis + 本地内存)和多实例同步
- 集成到 gateway_handler 的错误处理流程
- 新增前端管理界面组件
- 新增单元测试覆盖核心匹配逻辑

优化:
- 移除 refreshLocalCache 中的冗余排序(数据库已排序)
- 后端 Validate() 增加匹配条件非空校验
parent 1d8b6864
...@@ -3191,6 +3191,80 @@ export default { ...@@ -3191,6 +3191,80 @@ export default {
failedToSave: 'Failed to save settings', failedToSave: 'Failed to save settings',
failedToTestSmtp: 'SMTP connection test failed', failedToTestSmtp: 'SMTP connection test failed',
failedToSendTestEmail: 'Failed to send test email' failedToSendTestEmail: 'Failed to send test email'
},
// Error Passthrough Rules
errorPassthrough: {
title: 'Error Passthrough Rules',
description: 'Configure how upstream errors are returned to clients',
createRule: 'Create Rule',
editRule: 'Edit Rule',
deleteRule: 'Delete Rule',
noRules: 'No rules configured',
createFirstRule: 'Create your first error passthrough rule',
allPlatforms: 'All Platforms',
passthrough: 'Passthrough',
custom: 'Custom',
code: 'Code',
body: 'Body',
// Columns
columns: {
priority: 'Priority',
name: 'Name',
conditions: 'Conditions',
platforms: 'Platforms',
behavior: 'Behavior',
status: 'Status',
actions: 'Actions'
},
// Match Mode
matchMode: {
any: 'Code OR Keyword',
all: 'Code AND Keyword',
anyHint: 'Status code matches any error code, OR message contains any keyword',
allHint: 'Status code matches any error code, AND message contains any keyword'
},
// Form
form: {
name: 'Rule Name',
namePlaceholder: 'e.g., Context Limit Passthrough',
priority: 'Priority',
priorityHint: 'Lower values have higher priority',
description: 'Description',
descriptionPlaceholder: 'Describe the purpose of this rule...',
matchConditions: 'Match Conditions',
errorCodes: 'Error Codes',
errorCodesPlaceholder: '422, 400, 429',
errorCodesHint: 'Separate multiple codes with commas',
keywords: 'Keywords',
keywordsPlaceholder: 'One keyword per line\ncontext limit\nmodel not supported',
keywordsHint: 'One keyword per line, case-insensitive',
matchMode: 'Match Mode',
platforms: 'Platforms',
platformsHint: 'Leave empty to apply to all platforms',
responseBehavior: 'Response Behavior',
passthroughCode: 'Passthrough upstream status code',
responseCode: 'Custom status code',
passthroughBody: 'Passthrough upstream error message',
customMessage: 'Custom error message',
customMessagePlaceholder: 'Error message to return to client...',
enabled: 'Enable this rule'
},
// Messages
nameRequired: 'Please enter rule name',
conditionsRequired: 'Please configure at least one error code or keyword',
ruleCreated: 'Rule created successfully',
ruleUpdated: 'Rule updated successfully',
ruleDeleted: 'Rule deleted successfully',
deleteConfirm: 'Are you sure you want to delete rule "{name}"?',
failedToLoad: 'Failed to load rules',
failedToSave: 'Failed to save rule',
failedToDelete: 'Failed to delete rule',
failedToToggle: 'Failed to toggle status'
} }
}, },
......
...@@ -3362,6 +3362,80 @@ export default { ...@@ -3362,6 +3362,80 @@ export default {
failedToSave: '保存设置失败', failedToSave: '保存设置失败',
failedToTestSmtp: 'SMTP 连接测试失败', failedToTestSmtp: 'SMTP 连接测试失败',
failedToSendTestEmail: '发送测试邮件失败' failedToSendTestEmail: '发送测试邮件失败'
},
// Error Passthrough Rules
errorPassthrough: {
title: '错误透传规则',
description: '配置上游错误如何返回给客户端',
createRule: '创建规则',
editRule: '编辑规则',
deleteRule: '删除规则',
noRules: '暂无规则',
createFirstRule: '创建第一条错误透传规则',
allPlatforms: '所有平台',
passthrough: '透传',
custom: '自定义',
code: '状态码',
body: '消息体',
// Columns
columns: {
priority: '优先级',
name: '名称',
conditions: '匹配条件',
platforms: '平台',
behavior: '响应行为',
status: '状态',
actions: '操作'
},
// Match Mode
matchMode: {
any: '错误码 或 关键词',
all: '错误码 且 关键词',
anyHint: '状态码匹配任一错误码,或消息包含任一关键词',
allHint: '状态码匹配任一错误码,且消息包含任一关键词'
},
// Form
form: {
name: '规则名称',
namePlaceholder: '例如:上下文超限透传',
priority: '优先级',
priorityHint: '数值越小优先级越高,优先匹配',
description: '规则描述',
descriptionPlaceholder: '描述此规则的用途...',
matchConditions: '匹配条件',
errorCodes: '错误码',
errorCodesPlaceholder: '422, 400, 429',
errorCodesHint: '多个错误码用逗号分隔',
keywords: '关键词',
keywordsPlaceholder: '每行一个关键词\ncontext limit\nmodel not supported',
keywordsHint: '每行一个关键词,不区分大小写',
matchMode: '匹配模式',
platforms: '适用平台',
platformsHint: '不选择表示适用于所有平台',
responseBehavior: '响应行为',
passthroughCode: '透传上游状态码',
responseCode: '自定义状态码',
passthroughBody: '透传上游错误信息',
customMessage: '自定义错误信息',
customMessagePlaceholder: '返回给客户端的错误信息...',
enabled: '启用此规则'
},
// Messages
nameRequired: '请输入规则名称',
conditionsRequired: '请至少配置一个错误码或关键词',
ruleCreated: '规则创建成功',
ruleUpdated: '规则更新成功',
ruleDeleted: '规则删除成功',
deleteConfirm: '确定要删除规则 "{name}" 吗?',
failedToLoad: '加载规则失败',
failedToSave: '保存规则失败',
failedToDelete: '删除规则失败',
failedToToggle: '切换状态失败'
} }
}, },
......
...@@ -16,6 +16,16 @@ ...@@ -16,6 +16,16 @@
@sync="showSync = true" @sync="showSync = true"
@create="showCreate = true" @create="showCreate = true"
> >
<template #before>
<button
@click="showErrorPassthrough = true"
class="btn btn-secondary"
:title="t('admin.errorPassthrough.title')"
>
<Icon name="shield" size="md" class="mr-1.5" />
<span class="hidden md:inline">{{ t('admin.errorPassthrough.title') }}</span>
</button>
</template>
<template #after> <template #after>
<!-- Auto Refresh Dropdown --> <!-- Auto Refresh Dropdown -->
<div class="relative" ref="autoRefreshDropdownRef"> <div class="relative" ref="autoRefreshDropdownRef">
...@@ -221,6 +231,7 @@ ...@@ -221,6 +231,7 @@
<BulkEditAccountModal :show="showBulkEdit" :account-ids="selIds" :proxies="proxies" :groups="groups" @close="showBulkEdit = false" @updated="handleBulkUpdated" /> <BulkEditAccountModal :show="showBulkEdit" :account-ids="selIds" :proxies="proxies" :groups="groups" @close="showBulkEdit = false" @updated="handleBulkUpdated" />
<TempUnschedStatusModal :show="showTempUnsched" :account="tempUnschedAcc" @close="showTempUnsched = false" @reset="handleTempUnschedReset" /> <TempUnschedStatusModal :show="showTempUnsched" :account="tempUnschedAcc" @close="showTempUnsched = false" @reset="handleTempUnschedReset" />
<ConfirmDialog :show="showDeleteDialog" :title="t('admin.accounts.deleteAccount')" :message="t('admin.accounts.deleteConfirm', { name: deletingAcc?.name })" :confirm-text="t('common.delete')" :cancel-text="t('common.cancel')" :danger="true" @confirm="confirmDelete" @cancel="showDeleteDialog = false" /> <ConfirmDialog :show="showDeleteDialog" :title="t('admin.accounts.deleteAccount')" :message="t('admin.accounts.deleteConfirm', { name: deletingAcc?.name })" :confirm-text="t('common.delete')" :cancel-text="t('common.cancel')" :danger="true" @confirm="confirmDelete" @cancel="showDeleteDialog = false" />
<ErrorPassthroughRulesModal :show="showErrorPassthrough" @close="showErrorPassthrough = false" />
</AppLayout> </AppLayout>
</template> </template>
...@@ -252,6 +263,7 @@ import AccountGroupsCell from '@/components/account/AccountGroupsCell.vue' ...@@ -252,6 +263,7 @@ import AccountGroupsCell from '@/components/account/AccountGroupsCell.vue'
import AccountCapacityCell from '@/components/account/AccountCapacityCell.vue' import AccountCapacityCell from '@/components/account/AccountCapacityCell.vue'
import PlatformTypeBadge from '@/components/common/PlatformTypeBadge.vue' import PlatformTypeBadge from '@/components/common/PlatformTypeBadge.vue'
import Icon from '@/components/icons/Icon.vue' import Icon from '@/components/icons/Icon.vue'
import ErrorPassthroughRulesModal from '@/components/admin/ErrorPassthroughRulesModal.vue'
import { formatDateTime, formatRelativeTime } from '@/utils/format' import { formatDateTime, formatRelativeTime } from '@/utils/format'
import type { Account, Proxy, AdminGroup } from '@/types' import type { Account, Proxy, AdminGroup } from '@/types'
...@@ -271,6 +283,7 @@ const showDeleteDialog = ref(false) ...@@ -271,6 +283,7 @@ const showDeleteDialog = ref(false)
const showReAuth = ref(false) const showReAuth = ref(false)
const showTest = ref(false) const showTest = ref(false)
const showStats = ref(false) const showStats = ref(false)
const showErrorPassthrough = ref(false)
const edAcc = ref<Account | null>(null) const edAcc = ref<Account | null>(null)
const tempUnschedAcc = ref<Account | null>(null) const tempUnschedAcc = ref<Account | null>(null)
const deletingAcc = ref<Account | null>(null) const deletingAcc = ref<Account | null>(null)
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment