Unverified Commit b6d46fd5 authored by InCerryGit's avatar InCerryGit Committed by GitHub
Browse files

Merge branch 'Wei-Shaw:main' into main

parents fa68cbad fdd8499f
......@@ -32,6 +32,10 @@
{{ t('admin.accounts.refreshToken') }}
</button>
</template>
<button v-if="supportsPrivacy" @click="$emit('set-privacy', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm text-emerald-600 hover:bg-gray-100 dark:hover:bg-dark-700">
<Icon name="shield" size="sm" />
{{ t('admin.accounts.setPrivacy') }}
</button>
<div v-if="hasRecoverableState" class="my-1 border-t border-gray-100 dark:border-dark-700"></div>
<button v-if="hasRecoverableState" @click="$emit('recover-state', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm text-emerald-600 hover:bg-gray-100 dark:hover:bg-dark-700">
<Icon name="sync" size="sm" />
......@@ -55,7 +59,7 @@ import { Icon } from '@/components/icons'
import type { Account } from '@/types'
const props = defineProps<{ show: boolean; account: Account | null; position: { top: number; left: number } | null }>()
const emit = defineEmits(['close', 'test', 'stats', 'schedule', 'reauth', 'refresh-token', 'recover-state', 'reset-quota'])
const emit = defineEmits(['close', 'test', 'stats', 'schedule', 'reauth', 'refresh-token', 'recover-state', 'reset-quota', 'set-privacy'])
const { t } = useI18n()
const isRateLimited = computed(() => {
if (props.account?.rate_limit_reset_at && new Date(props.account.rate_limit_reset_at) > new Date()) {
......@@ -75,6 +79,9 @@ const isTempUnschedulable = computed(() => props.account?.temp_unschedulable_unt
const hasRecoverableState = computed(() => {
return props.account?.status === 'error' || Boolean(isRateLimited.value) || Boolean(isOverloaded.value) || Boolean(isTempUnschedulable.value)
})
const isAntigravityOAuth = computed(() => props.account?.platform === 'antigravity' && props.account?.type === 'oauth')
const isOpenAIOAuth = computed(() => props.account?.platform === 'openai' && props.account?.type === 'oauth')
const supportsPrivacy = computed(() => isAntigravityOAuth.value || isOpenAIOAuth.value)
const hasQuotaLimit = computed(() => {
return (props.account?.type === 'apikey' || props.account?.type === 'bedrock') && (
(props.account?.quota_limit ?? 0) > 0 ||
......
......@@ -31,7 +31,7 @@
</div>
<!-- Row 2: Plan type + Privacy mode (only if either exists) -->
<div v-if="planLabel || privacyBadge" class="inline-flex items-center overflow-hidden rounded-md">
<span v-if="planLabel" :class="['inline-flex items-center gap-1 px-1.5 py-1', typeClass]">
<span v-if="planLabel" :class="['inline-flex items-center gap-1 px-1.5 py-1', planBadgeClass]">
<span>{{ planLabel }}</span>
</span>
<span
......@@ -102,6 +102,8 @@ const planLabel = computed(() => {
return 'Pro'
case 'free':
return 'Free'
case 'abnormal':
return t('admin.accounts.subscriptionAbnormal')
default:
return props.planType
}
......@@ -139,18 +141,34 @@ const typeClass = computed(() => {
return 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
})
// Privacy badge — shows different states for OpenAI OAuth training setting
const planBadgeClass = computed(() => {
if (props.planType && props.planType.toLowerCase() === 'abnormal') {
return 'bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400'
}
return typeClass.value
})
// Privacy badge — shows different states for OpenAI/Antigravity OAuth privacy setting
const privacyBadge = computed(() => {
if (props.platform !== 'openai' || props.type !== 'oauth' || !props.privacyMode) return null
if (props.type !== 'oauth' || !props.privacyMode) return null
// 支持 OpenAI 和 Antigravity 平台
if (props.platform !== 'openai' && props.platform !== 'antigravity') return null
const shieldCheck = 'M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z'
const shieldX = 'M12 9v3.75m0-10.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285zM12 18h.008v.008H12V18z'
switch (props.privacyMode) {
// OpenAI states
case 'training_off':
return { label: 'Privacy', icon: shieldCheck, title: t('admin.accounts.privacyTrainingOff'), class: 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400' }
return { label: 'Private', icon: shieldCheck, title: t('admin.accounts.privacyTrainingOff'), class: 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400' }
case 'training_set_cf_blocked':
return { label: 'CF', icon: shieldX, title: t('admin.accounts.privacyCfBlocked'), class: 'bg-yellow-100 text-yellow-600 dark:bg-yellow-900/30 dark:text-yellow-400' }
case 'training_set_failed':
return { label: 'Fail', icon: shieldX, title: t('admin.accounts.privacyFailed'), class: 'bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400' }
// Antigravity states
case 'privacy_set':
return { label: 'Private', icon: shieldCheck, title: t('admin.accounts.privacyAntigravitySet'), class: 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400' }
case 'privacy_set_failed':
return { label: 'Fail', icon: shieldX, title: t('admin.accounts.privacyAntigravityFailed'), class: 'bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400' }
default:
return null
}
......
......@@ -1984,6 +1984,10 @@ export default {
privacyTrainingOff: 'Training data sharing disabled',
privacyCfBlocked: 'Blocked by Cloudflare, training may still be on',
privacyFailed: 'Failed to disable training',
privacyAntigravitySet: 'Telemetry and marketing emails disabled',
privacyAntigravityFailed: 'Privacy setting failed',
setPrivacy: 'Set Privacy',
subscriptionAbnormal: 'Abnormal',
// Capacity status tooltips
capacity: {
windowCost: {
......@@ -2300,7 +2304,9 @@ export default {
},
tlsFingerprint: {
label: 'TLS Fingerprint Simulation',
hint: 'Simulate Node.js/Claude Code client TLS fingerprint'
hint: 'Simulate Node.js/Claude Code client TLS fingerprint',
defaultProfile: 'Built-in Default',
randomProfile: 'Random'
},
sessionIdMasking: {
label: 'Session ID Masking',
......@@ -4171,6 +4177,14 @@ export default {
allowUngroupedKey: 'Allow Ungrouped Key Scheduling',
allowUngroupedKeyHint: 'When disabled, API Keys not assigned to any group cannot make requests (403 Forbidden). Keep disabled to ensure all Keys belong to a specific group.'
},
gatewayForwarding: {
title: 'Request Forwarding',
description: 'Control how requests are forwarded to upstream OAuth accounts',
fingerprintUnification: 'Fingerprint Unification',
fingerprintUnificationHint: 'Unify X-Stainless-* headers across users sharing the same OAuth account. Disabling passes through each client\'s original headers.',
metadataPassthrough: 'Metadata Passthrough',
metadataPassthroughHint: 'Pass through client\'s original metadata.user_id without rewriting. May improve upstream cache hit rates.',
},
site: {
title: 'Site Settings',
description: 'Customize site branding',
......@@ -4461,6 +4475,14 @@ export default {
thinkingSignatureHint: 'Automatically strip signatures and retry when upstream returns thinking block signature validation errors',
thinkingBudget: 'Thinking Budget Rectifier',
thinkingBudgetHint: 'Automatically set budget to 32000 and retry when upstream returns budget_tokens constraint error (≥1024)',
apikeySignature: 'API Key Signature Rectifier',
apikeySignatureHint:
'Automatically strip signatures and retry when API Key accounts receive signature-related errors (built-in patterns always apply)',
apikeyPatterns: 'Custom Match Patterns',
apikeyPatternsHint:
'Additional keywords matched against the response body (case-insensitive). Built-in patterns always apply; use these for supplementary matching.',
apikeyPatternPlaceholder: 'e.g., thinking_error',
addPattern: 'Add Pattern',
saved: 'Rectifier settings saved',
saveFailed: 'Failed to save rectifier settings'
},
......@@ -4568,6 +4590,62 @@ export default {
failedToSave: 'Failed to save rule',
failedToDelete: 'Failed to delete rule',
failedToToggle: 'Failed to toggle status'
},
// TLS Fingerprint Profiles
tlsFingerprintProfiles: {
title: 'TLS Fingerprint Profiles',
description: 'Manage TLS fingerprint profiles for simulating specific client TLS handshake characteristics',
createProfile: 'Create Profile',
editProfile: 'Edit Profile',
deleteProfile: 'Delete Profile',
noProfiles: 'No profiles configured',
createFirstProfile: 'Create your first TLS fingerprint profile',
columns: {
name: 'Name',
description: 'Description',
grease: 'GREASE',
alpn: 'ALPN',
actions: 'Actions'
},
form: {
pasteYaml: 'Paste YAML Configuration',
pasteYamlPlaceholder: 'Paste YAML output from TLS Fingerprint Collector here...',
pasteYamlHint: 'Paste the YAML copied from TLS Fingerprint Collector to auto-fill all fields.',
openCollector: 'Open Collector',
parseYaml: 'Parse YAML',
yamlParsed: 'YAML parsed successfully, fields auto-filled',
yamlParseFailed: 'Failed to parse YAML: name field not found',
name: 'Profile Name',
namePlaceholder: 'e.g. macOS Node.js v24',
description: 'Description',
descriptionPlaceholder: 'Optional description for this profile',
enableGrease: 'Enable GREASE',
enableGreaseHint: 'Insert GREASE values in TLS ClientHello extensions',
cipherSuites: 'Cipher Suites',
cipherSuitesHint: 'Comma-separated hex values, e.g. 0x1301, 0x1302, 0xc02c',
curves: 'Elliptic Curves',
curvesHint: 'Comma-separated curve IDs',
pointFormats: 'Point Formats',
signatureAlgorithms: 'Signature Algorithms',
alpnProtocols: 'ALPN Protocols',
alpnProtocolsHint: 'Comma-separated, e.g. h2, http/1.1',
supportedVersions: 'Supported TLS Versions',
keyShareGroups: 'Key Share Groups',
pskModes: 'PSK Modes',
extensions: 'Extensions'
},
deleteConfirm: 'Delete Profile',
deleteConfirmMessage: 'Are you sure you want to delete profile "{name}"? Accounts using this profile will fall back to the built-in default.',
createSuccess: 'Profile created successfully',
updateSuccess: 'Profile updated successfully',
deleteSuccess: 'Profile deleted successfully',
loadFailed: 'Failed to load profiles',
saveFailed: 'Failed to save profile',
deleteFailed: 'Failed to delete profile'
}
},
......
......@@ -2022,6 +2022,10 @@ export default {
privacyTrainingOff: '已关闭训练数据共享',
privacyCfBlocked: '被 Cloudflare 拦截,训练可能仍开启',
privacyFailed: '关闭训练数据共享失败',
privacyAntigravitySet: '已关闭遥测和营销邮件',
privacyAntigravityFailed: '隐私设置失败',
setPrivacy: '设置隐私',
subscriptionAbnormal: '异常',
// 容量状态提示
capacity: {
windowCost: {
......@@ -2444,7 +2448,9 @@ export default {
},
tlsFingerprint: {
label: 'TLS 指纹模拟',
hint: '模拟 Node.js/Claude Code 客户端的 TLS 指纹'
hint: '模拟 Node.js/Claude Code 客户端的 TLS 指纹',
defaultProfile: '内置默认',
randomProfile: '随机'
},
sessionIdMasking: {
label: '会话 ID 伪装',
......@@ -4334,6 +4340,14 @@ export default {
allowUngroupedKey: '允许未分组 Key 调度',
allowUngroupedKeyHint: '关闭后,未分配到任何分组的 API Key 将无法发起请求(返回 403)。建议保持关闭以确保所有 Key 都归属明确的分组。'
},
gatewayForwarding: {
title: '请求转发行为',
description: '控制请求转发到上游 OAuth 账号时的行为',
fingerprintUnification: '指纹统一化',
fingerprintUnificationHint: '统一共享同一 OAuth 账号的用户的 X-Stainless-* 请求头。关闭后透传客户端原始请求头。',
metadataPassthrough: 'Metadata 透传',
metadataPassthroughHint: '透传客户端原始 metadata.user_id,不进行重写。可能提高上游缓存命中率。',
},
site: {
title: '站点设置',
description: '自定义站点品牌',
......@@ -4625,6 +4639,14 @@ export default {
thinkingSignatureHint: '当上游返回 thinking block 签名校验错误时,自动去除签名并重试',
thinkingBudget: 'Thinking Budget 整流',
thinkingBudgetHint: '当上游返回 budget_tokens 约束错误(≥1024)时,自动将 budget 设为 32000 并重试',
apikeySignature: 'API Key 签名整流',
apikeySignatureHint:
'当 API Key 账号的上游返回签名相关错误时,自动去除签名并重试(内置规则始终生效)',
apikeyPatterns: '自定义匹配关键词',
apikeyPatternsHint:
'额外的关键词,匹配响应体中的内容(不区分大小写)。内置规则始终生效,此处用于补充额外匹配。',
apikeyPatternPlaceholder: '例如:thinking_error 或 签名无效',
addPattern: '添加关键词',
saved: '整流器设置保存成功',
saveFailed: '保存整流器设置失败'
},
......@@ -4732,6 +4754,62 @@ export default {
failedToSave: '保存规则失败',
failedToDelete: '删除规则失败',
failedToToggle: '切换状态失败'
},
// TLS 指纹模板
tlsFingerprintProfiles: {
title: 'TLS 指纹模板',
description: '管理 TLS 指纹模板,用于模拟特定客户端的 TLS 握手特征',
createProfile: '创建模板',
editProfile: '编辑模板',
deleteProfile: '删除模板',
noProfiles: '暂无模板',
createFirstProfile: '创建你的第一个 TLS 指纹模板',
columns: {
name: '名称',
description: '描述',
grease: 'GREASE',
alpn: 'ALPN',
actions: '操作'
},
form: {
pasteYaml: '粘贴 YAML 配置',
pasteYamlPlaceholder: '将 TLS 指纹采集器复制的 YAML 粘贴到这里...',
pasteYamlHint: '粘贴从 TLS 指纹采集器复制的 YAML 配置,自动填充所有字段。',
openCollector: '打开采集器',
parseYaml: '解析 YAML',
yamlParsed: 'YAML 解析成功,字段已自动填充',
yamlParseFailed: 'YAML 解析失败:未找到 name 字段',
name: '模板名称',
namePlaceholder: '例如 macOS Node.js v24',
description: '描述',
descriptionPlaceholder: '可选的模板描述',
enableGrease: '启用 GREASE',
enableGreaseHint: '在 TLS ClientHello 扩展中插入 GREASE 值',
cipherSuites: '密码套件',
cipherSuitesHint: '逗号分隔的十六进制值,例如 0x1301, 0x1302, 0xc02c',
curves: '椭圆曲线',
curvesHint: '逗号分隔的曲线 ID',
pointFormats: '点格式',
signatureAlgorithms: '签名算法',
alpnProtocols: 'ALPN 协议',
alpnProtocolsHint: '逗号分隔,例如 h2, http/1.1',
supportedVersions: '支持的 TLS 版本',
keyShareGroups: '密钥共享组',
pskModes: 'PSK 模式',
extensions: '扩展'
},
deleteConfirm: '删除模板',
deleteConfirmMessage: '确定要删除模板 "{name}" 吗?使用此模板的账号将回退到内置默认值。',
createSuccess: '模板创建成功',
updateSuccess: '模板更新成功',
deleteSuccess: '模板删除成功',
loadFailed: '加载模板失败',
saveFailed: '保存模板失败',
deleteFailed: '删除模板失败'
}
},
......
......@@ -724,6 +724,7 @@ export interface Account {
// TLS指纹伪装(仅 Anthropic OAuth/SetupToken 账号有效)
enable_tls_fingerprint?: boolean | null
tls_fingerprint_profile_id?: number | null
// 会话ID伪装(仅 Anthropic OAuth/SetupToken 账号有效)
// 启用后将在15分钟内固定 metadata.user_id 中的 session ID
......
......@@ -73,6 +73,16 @@
<span class="hidden md:inline">{{ t('admin.errorPassthrough.title') }}</span>
</button>
<!-- TLS Fingerprint Profiles -->
<button
@click="showTLSFingerprintProfiles = true"
class="btn btn-secondary"
:title="t('admin.tlsFingerprintProfiles.title')"
>
<Icon name="lock" size="md" class="mr-1.5" />
<span class="hidden md:inline">{{ t('admin.tlsFingerprintProfiles.title') }}</span>
</button>
<!-- Column Settings Dropdown -->
<div class="relative" ref="columnDropdownRef">
<button
......@@ -276,7 +286,7 @@
<AccountTestModal :show="showTest" :account="testingAcc" @close="closeTestModal" />
<AccountStatsModal :show="showStats" :account="statsAcc" @close="closeStatsModal" />
<ScheduledTestsPanel :show="showSchedulePanel" :account-id="scheduleAcc?.id ?? null" :model-options="scheduleModelOptions" @close="closeSchedulePanel" />
<AccountActionMenu :show="menu.show" :account="menu.acc" :position="menu.pos" @close="menu.show = false" @test="handleTest" @stats="handleViewStats" @schedule="handleSchedule" @reauth="handleReAuth" @refresh-token="handleRefresh" @recover-state="handleRecoverState" @reset-quota="handleResetQuota" />
<AccountActionMenu :show="menu.show" :account="menu.acc" :position="menu.pos" @close="menu.show = false" @test="handleTest" @stats="handleViewStats" @schedule="handleSchedule" @reauth="handleReAuth" @refresh-token="handleRefresh" @recover-state="handleRecoverState" @reset-quota="handleResetQuota" @set-privacy="handleSetPrivacy" />
<SyncFromCrsModal :show="showSync" @close="showSync = false" @synced="reload" />
<ImportDataModal :show="showImportData" @close="showImportData = false" @imported="handleDataImported" />
<BulkEditAccountModal :show="showBulkEdit" :account-ids="selIds" :selected-platforms="selPlatforms" :selected-types="selTypes" :proxies="proxies" :groups="groups" @close="showBulkEdit = false" @updated="handleBulkUpdated" />
......@@ -289,6 +299,7 @@
</label>
</ConfirmDialog>
<ErrorPassthroughRulesModal :show="showErrorPassthrough" @close="showErrorPassthrough = false" />
<TLSFingerprintProfilesModal :show="showTLSFingerprintProfiles" @close="showTLSFingerprintProfiles = false" />
</AppLayout>
</template>
......@@ -326,6 +337,7 @@ import AccountCapacityCell from '@/components/account/AccountCapacityCell.vue'
import PlatformTypeBadge from '@/components/common/PlatformTypeBadge.vue'
import Icon from '@/components/icons/Icon.vue'
import ErrorPassthroughRulesModal from '@/components/admin/ErrorPassthroughRulesModal.vue'
import TLSFingerprintProfilesModal from '@/components/admin/TLSFingerprintProfilesModal.vue'
import { buildOpenAIUsageRefreshKey } from '@/utils/accountUsageRefresh'
import { formatDateTime, formatRelativeTime } from '@/utils/format'
import type { Account, AccountPlatform, AccountType, Proxy as AccountProxy, AdminGroup, WindowStats, ClaudeModel } from '@/types'
......@@ -366,6 +378,7 @@ const showReAuth = ref(false)
const showTest = ref(false)
const showStats = ref(false)
const showErrorPassthrough = ref(false)
const showTLSFingerprintProfiles = ref(false)
const edAcc = ref<Account | null>(null)
const tempUnschedAcc = ref<Account | null>(null)
const deletingAcc = ref<Account | null>(null)
......@@ -1241,6 +1254,17 @@ const handleResetQuota = async (a: Account) => {
console.error('Failed to reset quota:', error)
}
}
const handleSetPrivacy = async (a: Account) => {
try {
const updated = await adminAPI.accounts.setPrivacy(a.id)
patchAccountInList(updated)
enterAutoRefreshSilentWindow()
appStore.showSuccess(t('common.success'))
} catch (error: any) {
console.error('Failed to set privacy:', error)
appStore.showError(error?.response?.data?.message || t('admin.accounts.privacyFailed'))
}
}
const handleDelete = (a: Account) => { deletingAcc.value = a; showDeleteDialog.value = true }
const confirmDelete = async () => { if(!deletingAcc.value) return; try { await adminAPI.accounts.delete(deletingAcc.value.id); showDeleteDialog.value = false; deletingAcc.value = null; reload() } catch (error) { console.error('Failed to delete account:', error) } }
const handleToggleSchedulable = async (a: Account) => {
......
......@@ -454,6 +454,72 @@
</div>
<Toggle v-model="rectifierForm.thinking_budget_enabled" />
</div>
<!-- API Key Signature Rectifier -->
<div class="flex items-center justify-between">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{{
t('admin.settings.rectifier.apikeySignature')
}}</label>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.settings.rectifier.apikeySignatureHint') }}
</p>
</div>
<Toggle v-model="rectifierForm.apikey_signature_enabled" />
</div>
<!-- Custom Patterns (only when apikey_signature_enabled) -->
<div
v-if="rectifierForm.apikey_signature_enabled"
class="ml-4 space-y-3 border-l-2 border-gray-200 pl-4 dark:border-dark-600"
>
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{{
t('admin.settings.rectifier.apikeyPatterns')
}}</label>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.settings.rectifier.apikeyPatternsHint') }}
</p>
</div>
<div
v-for="(_, index) in rectifierForm.apikey_signature_patterns"
:key="index"
class="flex items-center gap-2"
>
<input
v-model="rectifierForm.apikey_signature_patterns[index]"
type="text"
class="input input-sm flex-1"
:placeholder="t('admin.settings.rectifier.apikeyPatternPlaceholder')"
/>
<button
type="button"
@click="rectifierForm.apikey_signature_patterns.splice(index, 1)"
class="btn btn-ghost btn-xs text-red-500 hover:text-red-700"
>
<svg
class="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<button
type="button"
@click="rectifierForm.apikey_signature_patterns.push('')"
class="btn btn-ghost btn-xs text-primary-600 dark:text-primary-400"
>
+ {{ t('admin.settings.rectifier.addPattern') }}
</button>
</div>
</div>
<!-- Save Button -->
......@@ -1171,6 +1237,45 @@
</div>
</div>
</div>
<!-- Gateway Forwarding Behavior -->
<div class="card">
<div class="border-b border-gray-100 px-6 py-4 dark:border-dark-700">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
{{ t('admin.settings.gatewayForwarding.title') }}
</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ t('admin.settings.gatewayForwarding.description') }}
</p>
</div>
<div class="space-y-5 p-6">
<!-- Fingerprint Unification -->
<div class="flex items-center justify-between">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('admin.settings.gatewayForwarding.fingerprintUnification') }}
</label>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.settings.gatewayForwarding.fingerprintUnificationHint') }}
</p>
</div>
<Toggle v-model="form.enable_fingerprint_unification" />
</div>
<!-- Metadata Passthrough -->
<div class="flex items-center justify-between">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('admin.settings.gatewayForwarding.metadataPassthrough') }}
</label>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.settings.gatewayForwarding.metadataPassthroughHint') }}
</p>
</div>
<Toggle v-model="form.enable_metadata_passthrough" />
</div>
</div>
</div>
</div><!-- /Tab: Gateway Claude Code, Scheduling -->
<!-- Tab: General -->
......@@ -1971,7 +2076,9 @@ const rectifierSaving = ref(false)
const rectifierForm = reactive({
enabled: true,
thinking_signature_enabled: true,
thinking_budget_enabled: true
thinking_budget_enabled: true,
apikey_signature_enabled: false,
apikey_signature_patterns: [] as string[]
})
// Beta Policy 状态
......@@ -2066,7 +2173,10 @@ const form = reactive<SettingsForm>({
min_claude_code_version: '',
max_claude_code_version: '',
// 分组隔离
allow_ungrouped_key_scheduling: false
allow_ungrouped_key_scheduling: false,
// Gateway forwarding behavior
enable_fingerprint_unification: true,
enable_metadata_passthrough: false
})
const defaultSubscriptionGroupOptions = computed<DefaultSubscriptionGroupOption[]>(() =>
......@@ -2373,7 +2483,9 @@ async function saveSettings() {
identity_patch_prompt: form.identity_patch_prompt,
min_claude_code_version: form.min_claude_code_version,
max_claude_code_version: form.max_claude_code_version,
allow_ungrouped_key_scheduling: form.allow_ungrouped_key_scheduling
allow_ungrouped_key_scheduling: form.allow_ungrouped_key_scheduling,
enable_fingerprint_unification: form.enable_fingerprint_unification,
enable_metadata_passthrough: form.enable_metadata_passthrough
}
const updated = await adminAPI.settings.updateSettings(payload)
Object.assign(form, updated)
......@@ -2582,6 +2694,10 @@ async function loadRectifierSettings() {
try {
const settings = await adminAPI.settings.getRectifierSettings()
Object.assign(rectifierForm, settings)
// 确保 patterns 是数组(旧数据可能为 null)
if (!Array.isArray(rectifierForm.apikey_signature_patterns)) {
rectifierForm.apikey_signature_patterns = []
}
} catch (error: any) {
console.error('Failed to load rectifier settings:', error)
} finally {
......@@ -2595,9 +2711,16 @@ async function saveRectifierSettings() {
const updated = await adminAPI.settings.updateRectifierSettings({
enabled: rectifierForm.enabled,
thinking_signature_enabled: rectifierForm.thinking_signature_enabled,
thinking_budget_enabled: rectifierForm.thinking_budget_enabled
thinking_budget_enabled: rectifierForm.thinking_budget_enabled,
apikey_signature_enabled: rectifierForm.apikey_signature_enabled,
apikey_signature_patterns: rectifierForm.apikey_signature_patterns.filter(
(p) => p.trim() !== ''
)
})
Object.assign(rectifierForm, updated)
if (!Array.isArray(rectifierForm.apikey_signature_patterns)) {
rectifierForm.apikey_signature_patterns = []
}
appStore.showSuccess(t('admin.settings.rectifier.saved'))
} catch (error: any) {
appStore.showError(
......
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