feat(endpoint): 端点默认 body_rules 机制与 Codex 规则回填

- EndpointDefinition 新增 default_body_rules 字段,openai:cli/compact 配置 Codex 默认规则
- 创建端点时若未指定 body_rules 则自动填充对应格式的默认值
- 新增 GET /defaults/{api_format}/body-rules 接口查询默认规则
- 前端 EndpointFormDialog 增加"重置请求体"按钮,支持一键恢复默认
- Alembic 迁移回填已有 Codex 端点的默认 body_rules
- 新增 metadata 和 endpoint 创建默认值的单元测试
This commit is contained in:
fawney19
2026-03-02 22:08:39 +08:00
parent 3384c6d666
commit 0bff15f964
8 changed files with 392 additions and 1 deletions

View File

@@ -68,3 +68,11 @@ export async function deleteEndpoint(endpointId: string): Promise<{ message: str
const response = await client.delete(`/api/admin/endpoints/${endpointId}`)
return response.data
}
/**
* 获取指定 API 格式的默认请求体规则
*/
export async function getDefaultBodyRules(apiFormat: string): Promise<{ api_format: string; body_rules: BodyRule[] }> {
const response = await client.get(`/api/admin/endpoints/defaults/${encodeURIComponent(apiFormat)}/body-rules`)
return response.data
}

View File

@@ -204,6 +204,18 @@
<Plus class="w-3 h-3 mr-1" />
请求体
</Button>
<Button
v-if="isFixedProvider"
variant="ghost"
size="sm"
class="h-7 text-xs px-2"
title="重置请求体"
:disabled="resettingDefaultRulesEndpointId === endpoint.id"
@click="handleResetBodyRulesToDefault(endpoint)"
>
<RotateCcw class="w-3 h-3 mr-1" />
重置请求体
</Button>
</div>
</div>
<CollapsibleContent class="pt-3">
@@ -829,6 +841,7 @@ import { log } from '@/utils/logger'
import AlertDialog from '@/components/common/AlertDialog.vue'
import {
createEndpoint,
getDefaultBodyRules,
updateEndpoint,
deleteEndpoint,
type ProviderEndpoint,
@@ -1084,6 +1097,7 @@ function handleBodyRuleDragEnd(endpointId: string) {
// 状态
const addingEndpoint = ref(false)
const savingEndpointId = ref<string | null>(null)
const resettingDefaultRulesEndpointId = ref<string | null>(null)
const deletingEndpointId = ref<string | null>(null)
const togglingEndpointId = ref<string | null>(null)
const togglingFormatEndpointId = ref<string | null>(null)
@@ -1114,6 +1128,9 @@ function setBodyRuleHelpOpen(endpointId: string, open: boolean) {
// 每个端点的编辑状态(内联编辑)
const endpointEditStates = ref<Record<string, EndpointEditState>>({})
const defaultBodyRulesByFormat = ref<Record<string, BodyRule[]>>({})
const defaultBodyRulesLoaded = ref<Record<string, boolean>>({})
const loadingDefaultBodyRulesByFormat = ref<Record<string, boolean>>({})
// 系统保留的 header 名称(不允许用户设置)
const RESERVED_HEADERS = new Set([
@@ -1261,6 +1278,40 @@ const deleteConfirmDescription = computed(() => {
return `确定要删除 ${formatLabel} 端点吗?关联密钥将移除对该 API 格式的支持。`
})
async function loadDefaultBodyRulesForFormat(apiFormat: string, force = false): Promise<BodyRule[]> {
if (!apiFormat) return []
if (!force && defaultBodyRulesLoaded.value[apiFormat]) {
return defaultBodyRulesByFormat.value[apiFormat] || []
}
if (loadingDefaultBodyRulesByFormat.value[apiFormat]) {
return defaultBodyRulesByFormat.value[apiFormat] || []
}
loadingDefaultBodyRulesByFormat.value[apiFormat] = true
try {
const response = await getDefaultBodyRules(apiFormat)
const normalized = response.api_format || apiFormat
const rules = response.body_rules || []
defaultBodyRulesByFormat.value[normalized] = rules
defaultBodyRulesByFormat.value[apiFormat] = rules
defaultBodyRulesLoaded.value[normalized] = true
defaultBodyRulesLoaded.value[apiFormat] = true
return rules
} catch (error: unknown) {
defaultBodyRulesByFormat.value[apiFormat] = []
defaultBodyRulesLoaded.value[apiFormat] = true
log.warn('加载默认请求体规则失败', apiFormat, error)
return []
} finally {
loadingDefaultBodyRulesByFormat.value[apiFormat] = false
}
}
async function preloadDefaultBodyRules(endpoints: ProviderEndpoint[]): Promise<void> {
const formats = Array.from(new Set(endpoints.map(e => e.api_format).filter(Boolean)))
await Promise.all(formats.map(fmt => loadDefaultBodyRulesForFormat(fmt)))
}
// 获取指定 API 格式的默认路径
function getDefaultPath(apiFormat: string, baseUrl?: string): string {
const format = apiFormats.value.find(f => f.value === apiFormat)
@@ -2052,6 +2103,37 @@ function resetEndpointChanges(endpoint: ProviderEndpoint) {
endpointEditStates.value[endpoint.id] = initEndpointEditState(endpoint)
}
async function handleResetBodyRulesToDefault(endpoint: ProviderEndpoint) {
resettingDefaultRulesEndpointId.value = endpoint.id
try {
const defaultRules = await loadDefaultBodyRulesForFormat(endpoint.api_format, true)
if (!defaultRules.length) {
showError('该端点没有默认请求体规则')
return
}
if (!endpointEditStates.value[endpoint.id]) {
endpointEditStates.value[endpoint.id] = initEndpointEditState(endpoint)
}
const state = endpointEditStates.value[endpoint.id]
if (!state) return
const resetState = initEndpointEditState({
...endpoint,
body_rules: defaultRules,
})
state.bodyRules = resetState.bodyRules
endpointRulesExpanded.value[endpoint.id] = (state.rules.length + state.bodyRules.length) > 0
clearBodyRuleDragState(endpoint.id)
clearBodyRuleSelectOpen(endpoint.id)
success('已重置请求体为默认规则请点击保存生效')
} catch (error: unknown) {
showError(parseApiError(error, '重置失败'), '错误')
} finally {
resettingDefaultRulesEndpointId.value = null
}
}
// 将可编辑规则数组转换为 API 需要的 HeaderRule[]
function rulesToHeaderRules(rules: EditableRule[]): HeaderRule[] | null {
const result: HeaderRule[] = []
@@ -2126,6 +2208,7 @@ watch(() => props.modelValue, (open) => {
const hasRules = (endpoint.header_rules?.length || 0) + (endpoint.body_rules?.length || 0) > 0
endpointRulesExpanded.value[endpoint.id] = hasRules
}
void preloadDefaultBodyRules(localEndpoints.value)
} else {
// 关闭对话框时完全清空新端点表单
newEndpoint.value = { api_format: '', base_url: '', custom_path: '' }
@@ -2141,6 +2224,12 @@ watch(() => props.endpoints, (endpoints) => {
endpointEditStates.value[endpoint.id] = initEndpointEditState(endpoint)
}
}
const newFormats = localEndpoints.value
.filter(e => e.api_format && !defaultBodyRulesLoaded.value[e.api_format])
.map(e => ({ api_format: e.api_format }) as ProviderEndpoint)
if (newFormats.length) {
void preloadDefaultBodyRules(newFormats)
}
}
}, { deep: true })