fix: 修复代码审查报告中的4个关键问题

1. 资源管理冗余(ForwardGemini双重Close)
   - 错误分支读取body后立即关闭原始body,用内存副本重新包装
   - defer添加nil guard,避免重复关闭
   - fallback成功时显式关闭旧body,确保连接释放

2. Schema校验丢失(cleanJSONSchema移除字段无感知)
   - 新增schemaCleaningWarningsEnabled()支持环境变量控制
   - 实现warnSchemaKeyRemovedOnce()在非release模式下告警
   - 移除关键验证字段时输出warning,包含key和path

3. UI响应式风险(UsersView操作菜单硬编码定位)
   - 菜单改为先粗定位、渲染后测量、再clamp到视口内
   - 添加max-height + overflow-auto,超出时可滚动
   - 增强交互:点击其它位置/滚动/resize自动关闭或重新定位

4. 身份补丁干扰(TransformClaudeToGemini默认注入)
   - 新增TransformOptions + TransformClaudeToGeminiWithOptions
   - 系统设置新增enable_identity_patch、identity_patch_prompt
   - 完整打通handler/dto/service/frontend配置链路
   - 默认保持启用,向后兼容现有行为

测试:
- 后端单测全量通过:go test ./...
- 前端类型检查通过:npm run typecheck
This commit is contained in:
IanShaw027
2026-01-04 22:49:40 +08:00
parent bfcc562c35
commit f60f943d0c
10 changed files with 163 additions and 16 deletions

View File

@@ -34,6 +34,10 @@ export interface SystemSettings {
turnstile_enabled: boolean
turnstile_site_key: string
turnstile_secret_key: string
// Identity patch configuration (Claude -> Gemini)
enable_identity_patch: boolean
identity_patch_prompt: string
}
/**

View File

@@ -756,7 +756,10 @@ const form = reactive<SystemSettings>({
// Cloudflare Turnstile
turnstile_enabled: false,
turnstile_site_key: '',
turnstile_secret_key: ''
turnstile_secret_key: '',
// Identity patch (Claude -> Gemini)
enable_identity_patch: true,
identity_patch_prompt: ''
})
function handleLogoUpload(event: Event) {

View File

@@ -37,7 +37,7 @@
</TablePageLayout>
<Teleport to="body">
<div v-if="activeMenuId !== null && menuPosition" class="action-menu-content fixed z-[9999] w-48 overflow-hidden rounded-xl bg-white shadow-lg ring-1 ring-black/5 dark:bg-dark-800" :style="{ top: menuPosition.top + 'px', left: menuPosition.left + 'px' }">
<div v-if="activeMenuId !== null && menuPosition" ref="actionMenuEl" class="action-menu-content fixed z-[9999] w-48 max-h-[calc(100vh-16px)] overflow-auto rounded-xl bg-white shadow-lg ring-1 ring-black/5 dark:bg-dark-800" :style="{ top: menuPosition.top + 'px', left: menuPosition.left + 'px' }">
<div class="py-1">
<template v-for="user in users" :key="user.id">
<template v-if="user.id === activeMenuId">
@@ -63,7 +63,7 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'; import { useAppStore } from '@/stores/app'
import { adminAPI } from '@/api/admin'; import { useTableLoader } from '@/composables/useTableLoader'
import type { User } from '@/types'
@@ -83,6 +83,7 @@ const { items: users, loading, params, pagination, load, reload, handlePageChang
const showCreateModal = ref(false); const showEditModal = ref(false); const showDeleteDialog = ref(false); const showApiKeysModal = ref(false)
const editingUser = ref<User | null>(null); const deletingUser = ref<User | null>(null); const viewingUser = ref<User | null>(null)
const activeMenuId = ref<number | null>(null); const menuPosition = ref<{ top: number; left: number } | null>(null)
const actionMenuEl = ref<HTMLElement | null>(null)
const showAllowedGroupsModal = ref(false); const allowedGroupsUser = ref<User | null>(null); const showBalanceModal = ref(false); const balanceUser = ref<User | null>(null); const balanceOperation = ref<'add' | 'subtract'>('add')
const columns = computed(() => [{ key: 'email', label: t('admin.users.columns.user'), sortable: true }, { key: 'role', label: t('admin.users.columns.role'), sortable: true }, { key: 'balance', label: t('admin.users.columns.balance'), sortable: true }, { key: 'status', label: t('admin.users.columns.status'), sortable: true }, { key: 'actions', label: t('admin.users.columns.actions') }])
@@ -98,7 +99,47 @@ const handleDeposit = (u: User) => { balanceUser.value = u; balanceOperation.val
const handleWithdraw = (u: User) => { balanceUser.value = u; balanceOperation.value = 'subtract'; showBalanceModal.value = true }
const closeBalanceModal = () => { showBalanceModal.value = false; balanceUser.value = null }
const handleToggleStatus = async (user: User) => { const next = user.status === 'active' ? 'disabled' : 'active'; try { await adminAPI.users.toggleStatus(user.id, next as any); appStore.showSuccess(t('common.success')); load() } catch {} }
const openActionMenu = (u: User, e: MouseEvent) => { if (activeMenuId.value === u.id) { activeMenuId.value = null; menuPosition.value = null } else { activeMenuId.value = u.id; menuPosition.value = { top: e.clientY, left: e.clientX - 150 } } }
const repositionActionMenu = (triggerRect?: DOMRect) => {
if (!menuPosition.value || !actionMenuEl.value) return
const rect = actionMenuEl.value.getBoundingClientRect()
const margin = 8
let top = menuPosition.value.top
let left = menuPosition.value.left
if (triggerRect) {
const spaceBelow = window.innerHeight - triggerRect.bottom
const spaceAbove = triggerRect.top
if (rect.height > spaceBelow && spaceAbove > spaceBelow) top = Math.max(margin, triggerRect.top - rect.height - 4)
}
if (left + rect.width + margin > window.innerWidth) left = window.innerWidth - rect.width - margin
if (left < margin) left = margin
if (top + rect.height + margin > window.innerHeight) top = window.innerHeight - rect.height - margin
if (top < margin) top = margin
menuPosition.value = { top, left }
}
const openActionMenu = async (u: User, e: MouseEvent) => {
e.stopPropagation()
if (activeMenuId.value === u.id) { closeActionMenu(); return }
const actionMenuWidthPx = 192 // w-48
const triggerEl = e.currentTarget as HTMLElement | null
const triggerRect = triggerEl?.getBoundingClientRect()
activeMenuId.value = u.id
if (triggerRect) menuPosition.value = { top: triggerRect.bottom + 4, left: triggerRect.right - actionMenuWidthPx }
else menuPosition.value = { top: e.clientY, left: e.clientX - actionMenuWidthPx }
await nextTick()
repositionActionMenu(triggerRect)
}
const closeActionMenu = () => { activeMenuId.value = null; menuPosition.value = null }
onMounted(load)
const handleDocumentClick = (evt: MouseEvent) => { if (activeMenuId.value === null) return; const target = evt.target as Node | null; if (target && actionMenuEl.value?.contains(target)) return; closeActionMenu() }
const handleWindowResize = () => repositionActionMenu()
const handleAnyScroll = () => closeActionMenu()
onMounted(() => { load(); document.addEventListener('click', handleDocumentClick); window.addEventListener('resize', handleWindowResize); window.addEventListener('scroll', handleAnyScroll, true) })
onBeforeUnmount(() => { document.removeEventListener('click', handleDocumentClick); window.removeEventListener('resize', handleWindowResize); window.removeEventListener('scroll', handleAnyScroll, true) })
</script>