mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-21 14:19:18 +08:00
fix(frontend): 修复 Grok OAuth 建号缺上游配置入口与 JSON 导入渲染崩溃
- CreateAccountModal 为 Grok OAuth 新增自定义上游地址与请求头覆写区块,
经统一校验注入授权码兑换/RT 批量/SSO 批量三条创建路径(授权码路径
在兑换前校验,避免烧掉一次性 code)
- JSON 导入示例文案内嵌花括号被 vue-i18n 消息编译器当占位符解析,
渲染时抛 Invalid token in placeholder 炸掉面板:示例移出 i18n 硬编码;
同类修复 settings 的死键 claudeOAuthSystemPromptBlocksPlaceholder(删)
与 Hint 中被吞掉的 {billing_header}(字面量转义)
- 新增 localesMessageCompile 守卫测试:预编译中英全部文案,拦截非法占位符
- 面板打开后 scrollIntoView + 聚焦,避免在弹窗滚动容器视野外展开
- 删除「填入模板」按钮及三平台模板常量(产品决策:无意义)
- 抽取 HeaderOverrideEditor 共享组件,消除 Create/Edit/BulkEdit 三处重复
This commit is contained in:
@@ -538,75 +538,10 @@
|
||||
{{ t('admin.accounts.headerOverride.bulkReplaceHint') }}
|
||||
</p>
|
||||
|
||||
<div v-if="headerOverrideRows.length > 0" class="space-y-2">
|
||||
<div
|
||||
v-for="(row, index) in headerOverrideRows"
|
||||
:key="getHeaderOverrideRowKey(row)"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<input
|
||||
v-model="row.name"
|
||||
type="text"
|
||||
class="input flex-1"
|
||||
:placeholder="t('admin.accounts.headerOverride.namePlaceholder')"
|
||||
/>
|
||||
<input
|
||||
v-model="row.value"
|
||||
type="text"
|
||||
class="input flex-1"
|
||||
:placeholder="t('admin.accounts.headerOverride.valuePlaceholder')"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg p-2 text-red-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
|
||||
@click="removeHeaderOverrideRow(index)"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="w-full rounded-lg border-2 border-dashed border-gray-300 px-4 py-2 text-gray-600 transition-colors hover:border-gray-400 hover:text-gray-700 dark:border-dark-500 dark:text-gray-400 dark:hover:border-dark-400 dark:hover:text-gray-300"
|
||||
@click="addHeaderOverrideRow"
|
||||
>
|
||||
<svg class="mr-1 inline h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
{{ t('admin.accounts.headerOverride.addRow') }}
|
||||
</button>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-if="headerOverrideTemplatePlatform"
|
||||
type="button"
|
||||
class="rounded-lg bg-primary-50 px-3 py-1 text-xs text-primary-700 transition-colors hover:bg-primary-100 dark:bg-primary-900/30 dark:text-primary-400 dark:hover:bg-primary-900/50"
|
||||
@click="fillHeaderOverrideTemplate"
|
||||
>
|
||||
+ {{ t('admin.accounts.headerOverride.fillTemplate') }}
|
||||
</button>
|
||||
<HeaderOverrideJsonTools
|
||||
:rows="headerOverrideRows"
|
||||
@update:rows="headerOverrideRows = $event"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.headerOverride.emptyValueHint') }}
|
||||
</p>
|
||||
<HeaderOverrideEditor
|
||||
:rows="headerOverrideRows"
|
||||
@update:rows="headerOverrideRows = $event"
|
||||
/>
|
||||
</div>
|
||||
<p v-else class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.headerOverride.bulkDisableHint') }}
|
||||
@@ -1277,17 +1212,15 @@ import {
|
||||
buildModelMappingObject as buildModelMappingPayload,
|
||||
getPresetMappingsByPlatform
|
||||
} from '@/composables/useModelWhitelist'
|
||||
import HeaderOverrideJsonTools from '@/components/account/HeaderOverrideJsonTools.vue'
|
||||
import HeaderOverrideEditor from '@/components/account/HeaderOverrideEditor.vue'
|
||||
import {
|
||||
buildHeaderOverridesObject,
|
||||
getHeaderOverrideTemplate,
|
||||
isHeaderOverrideCapable,
|
||||
validateHeaderOverrideRows,
|
||||
HEADER_OVERRIDE_ENABLED_CREDENTIAL_KEY,
|
||||
HEADER_OVERRIDES_CREDENTIAL_KEY,
|
||||
type HeaderOverrideRow
|
||||
} from '@/components/account/credentialsBuilder'
|
||||
import { createStableObjectKeyResolver } from '@/utils/stableObjectKey'
|
||||
import {
|
||||
OPENAI_WS_MODE_CTX_POOL,
|
||||
OPENAI_WS_MODE_OFF,
|
||||
@@ -1436,37 +1369,6 @@ const customErrorCodeInput = ref<number | null>(null)
|
||||
const interceptWarmupRequests = ref(false)
|
||||
const headerOverrideEnabled = ref(false)
|
||||
const headerOverrideRows = ref<HeaderOverrideRow[]>([])
|
||||
const getHeaderOverrideRowKey = createStableObjectKeyResolver<HeaderOverrideRow>('bulk-header-override-row')
|
||||
|
||||
const addHeaderOverrideRow = () => {
|
||||
headerOverrideRows.value.push({ name: '', value: '' })
|
||||
}
|
||||
|
||||
const removeHeaderOverrideRow = (index: number) => {
|
||||
headerOverrideRows.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 模板仅在所选账号平台唯一时可用:混合 anthropic+openai 选择无法确定用哪套模板,
|
||||
// 误填会把另一平台的专有头写进所有所选账号
|
||||
const headerOverrideTemplatePlatform = computed(() => {
|
||||
return targetSelectedPlatforms.value.length === 1 ? targetSelectedPlatforms.value[0] : null
|
||||
})
|
||||
|
||||
// 模板按钮:填入所选平台的标准客户端请求头名称(值留空),跳过已存在的同名行
|
||||
const fillHeaderOverrideTemplate = () => {
|
||||
const platform = headerOverrideTemplatePlatform.value
|
||||
if (!platform) return
|
||||
const existing = new Set(
|
||||
headerOverrideRows.value.map((row) => row.name.trim().toLowerCase()).filter(Boolean)
|
||||
)
|
||||
const rows = headerOverrideRows.value.filter((row) => row.name.trim() || row.value.trim())
|
||||
for (const row of getHeaderOverrideTemplate(platform)) {
|
||||
if (!existing.has(row.name)) {
|
||||
rows.push(row)
|
||||
}
|
||||
}
|
||||
headerOverrideRows.value = rows
|
||||
}
|
||||
const proxyId = ref<number | null>(null)
|
||||
const concurrency = ref(1)
|
||||
const loadFactor = ref<number | null>(null)
|
||||
|
||||
@@ -1533,74 +1533,10 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="headerOverrideRows.length > 0" class="space-y-2">
|
||||
<div
|
||||
v-for="(row, index) in headerOverrideRows"
|
||||
:key="getHeaderOverrideRowKey(row)"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<input
|
||||
v-model="row.name"
|
||||
type="text"
|
||||
class="input flex-1"
|
||||
:placeholder="t('admin.accounts.headerOverride.namePlaceholder')"
|
||||
/>
|
||||
<input
|
||||
v-model="row.value"
|
||||
type="text"
|
||||
class="input flex-1"
|
||||
:placeholder="t('admin.accounts.headerOverride.valuePlaceholder')"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="removeHeaderOverrideRow(index)"
|
||||
class="rounded-lg p-2 text-red-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@click="addHeaderOverrideRow"
|
||||
class="w-full rounded-lg border-2 border-dashed border-gray-300 px-4 py-2 text-gray-600 transition-colors hover:border-gray-400 hover:text-gray-700 dark:border-dark-500 dark:text-gray-400 dark:hover:border-dark-400 dark:hover:text-gray-300"
|
||||
>
|
||||
<svg class="mr-1 inline h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
{{ t('admin.accounts.headerOverride.addRow') }}
|
||||
</button>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@click="fillHeaderOverrideTemplate"
|
||||
class="rounded-lg bg-primary-50 px-3 py-1 text-xs text-primary-700 transition-colors hover:bg-primary-100 dark:bg-primary-900/30 dark:text-primary-400 dark:hover:bg-primary-900/50"
|
||||
>
|
||||
+ {{ t('admin.accounts.headerOverride.fillTemplate') }}
|
||||
</button>
|
||||
<HeaderOverrideJsonTools
|
||||
:rows="headerOverrideRows"
|
||||
@update:rows="headerOverrideRows = $event"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.headerOverride.emptyValueHint') }}
|
||||
</p>
|
||||
<HeaderOverrideEditor
|
||||
:rows="headerOverrideRows"
|
||||
@update:rows="headerOverrideRows = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1968,6 +1904,90 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Grok OAuth Custom Upstream URL (仅改写转发端点,OAuth 授权/刷新不受影响) -->
|
||||
<div
|
||||
v-if="form.platform === 'grok' && isOAuthFlow"
|
||||
class="border-t border-gray-200 pt-4 dark:border-dark-600"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.grokCustomBaseUrl.title') }}</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.grokCustomBaseUrl.hint') }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="grok-custom-base-url-toggle"
|
||||
@click="grokOAuthCustomBaseUrlEnabled = !grokOAuthCustomBaseUrlEnabled"
|
||||
:class="[
|
||||
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
||||
grokOAuthCustomBaseUrlEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||
grokOAuthCustomBaseUrlEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="grokOAuthCustomBaseUrlEnabled">
|
||||
<input
|
||||
v-model="grokOAuthBaseUrl"
|
||||
type="text"
|
||||
class="input"
|
||||
data-testid="grok-custom-base-url-input"
|
||||
:placeholder="t('admin.accounts.grokCustomBaseUrl.placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grok OAuth Header Override (OAuth 类型没有 apikey 容器,需要独立区域) -->
|
||||
<div
|
||||
v-if="form.platform === 'grok' && isOAuthFlow"
|
||||
class="border-t border-gray-200 pt-4 dark:border-dark-600"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.headerOverride.title') }}</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.headerOverride.hint') }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="headerOverrideEnabled = !headerOverrideEnabled"
|
||||
:class="[
|
||||
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
||||
headerOverrideEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||
headerOverrideEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="headerOverrideEnabled" class="space-y-3">
|
||||
<div class="rounded-lg bg-blue-50 p-3 dark:bg-blue-900/20">
|
||||
<p class="text-xs text-blue-700 dark:text-blue-400">
|
||||
<Icon name="exclamationCircle" size="sm" class="mr-1 inline" :stroke-width="2" />
|
||||
{{ t('admin.accounts.headerOverride.info') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<HeaderOverrideEditor
|
||||
:rows="headerOverrideRows"
|
||||
@update:rows="headerOverrideRows = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OpenAI OAuth Model Mapping (OAuth 类型没有 apikey 容器,需要独立的模型映射区域) -->
|
||||
<div
|
||||
v-if="(form.platform === 'openai' || form.platform === 'grok') && isOAuthFlow"
|
||||
@@ -3499,12 +3519,11 @@ import ProxyAdBanner from '@/components/common/ProxyAdBanner.vue'
|
||||
import GroupSelector from '@/components/common/GroupSelector.vue'
|
||||
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
||||
import QuotaLimitCard from '@/components/account/QuotaLimitCard.vue'
|
||||
import HeaderOverrideJsonTools from '@/components/account/HeaderOverrideJsonTools.vue'
|
||||
import HeaderOverrideEditor from '@/components/account/HeaderOverrideEditor.vue'
|
||||
import {
|
||||
applyAntigravityProjectID,
|
||||
applyHeaderOverride,
|
||||
applyInterceptWarmup,
|
||||
getHeaderOverrideTemplate,
|
||||
isHeaderOverrideCapable,
|
||||
validateHeaderOverrideRows,
|
||||
type HeaderOverrideRow
|
||||
@@ -3695,26 +3714,40 @@ const customErrorCodeInput = ref<number | null>(null)
|
||||
const headerOverrideEnabled = ref(false)
|
||||
const headerOverrideRows = ref<HeaderOverrideRow[]>([])
|
||||
|
||||
const addHeaderOverrideRow = () => {
|
||||
headerOverrideRows.value.push({ name: '', value: '' })
|
||||
}
|
||||
// Grok OAuth:自定义上游地址(base_url 仅改写转发端点,OAuth 授权/刷新不受影响)
|
||||
const grokOAuthCustomBaseUrlEnabled = ref(false)
|
||||
const grokOAuthBaseUrl = ref('')
|
||||
|
||||
const removeHeaderOverrideRow = (index: number) => {
|
||||
headerOverrideRows.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 模板按钮:填入标准客户端请求头名称(值留空),跳过已存在的同名行
|
||||
const fillHeaderOverrideTemplate = () => {
|
||||
const existing = new Set(
|
||||
headerOverrideRows.value.map((row) => row.name.trim().toLowerCase()).filter(Boolean)
|
||||
)
|
||||
const rows = headerOverrideRows.value.filter((row) => row.name.trim() || row.value.trim())
|
||||
for (const row of getHeaderOverrideTemplate(form.platform)) {
|
||||
if (!existing.has(row.name)) {
|
||||
rows.push(row)
|
||||
// Grok OAuth 三条创建路径(授权码/RT 批量/SSO 批量)共用的前置校验。
|
||||
// 授权码路径必须在兑换 code 之前调用,避免校验失败时白白消耗一次性授权码。
|
||||
const validateGrokOAuthUpstreamConfig = (): boolean => {
|
||||
if (grokOAuthCustomBaseUrlEnabled.value) {
|
||||
const trimmed = grokOAuthBaseUrl.value.trim()
|
||||
if (!trimmed) {
|
||||
appStore.showError(t('admin.accounts.grokCustomBaseUrl.required'))
|
||||
return false
|
||||
}
|
||||
if (!/^https?:\/\//i.test(trimmed)) {
|
||||
appStore.showError(t('admin.accounts.grokCustomBaseUrl.invalid'))
|
||||
return false
|
||||
}
|
||||
}
|
||||
headerOverrideRows.value = rows
|
||||
if (headerOverrideEnabled.value) {
|
||||
const headerError = validateHeaderOverrideRows(headerOverrideRows.value)
|
||||
if (headerError) {
|
||||
appStore.showError(t(`admin.accounts.headerOverride.${headerError}`))
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 把已通过校验的自定义上游地址与请求头覆写写入 credentials
|
||||
const applyGrokOAuthUpstreamConfig = (credentials: Record<string, unknown>) => {
|
||||
if (grokOAuthCustomBaseUrlEnabled.value) {
|
||||
credentials.base_url = grokOAuthBaseUrl.value.trim()
|
||||
}
|
||||
applyHeaderOverride(credentials, headerOverrideEnabled.value, headerOverrideRows.value, 'create')
|
||||
}
|
||||
const interceptWarmupRequests = ref(false)
|
||||
const autoPauseOnExpired = ref(true)
|
||||
@@ -3780,7 +3813,6 @@ const vertexServiceAccountDragActive = ref(false)
|
||||
const tempUnschedEnabled = ref(false)
|
||||
const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
|
||||
const getModelMappingKey = createStableObjectKeyResolver<ModelMapping>('create-model-mapping')
|
||||
const getHeaderOverrideRowKey = createStableObjectKeyResolver<HeaderOverrideRow>('create-header-override-row')
|
||||
const getOpenAICompactModelMappingKey = createStableObjectKeyResolver<ModelMapping>('create-openai-compact-model-mapping')
|
||||
const getAntigravityModelMappingKey = createStableObjectKeyResolver<ModelMapping>('create-antigravity-model-mapping')
|
||||
const getTempUnschedRuleKey = createStableObjectKeyResolver<TempUnschedRuleForm>('create-temp-unsched-rule')
|
||||
@@ -4184,10 +4216,12 @@ watch(
|
||||
anthropicAPIKeyAuthScheme.value = 'x_api_key'
|
||||
webSearchEmulationMode.value = 'default'
|
||||
}
|
||||
// 请求头覆写为平台相关配置(模板/常用头集合不同),切换平台时清空,
|
||||
// 避免上一平台的模板行被提交到新平台账号
|
||||
// 请求头覆写为平台相关配置(常用头集合不同),切换平台时清空,
|
||||
// 避免上一平台的配置行被提交到新平台账号
|
||||
headerOverrideEnabled.value = false
|
||||
headerOverrideRows.value = []
|
||||
grokOAuthCustomBaseUrlEnabled.value = false
|
||||
grokOAuthBaseUrl.value = ''
|
||||
// Reset OAuth states
|
||||
oauth.resetState()
|
||||
openaiOAuth.resetState()
|
||||
@@ -4579,6 +4613,8 @@ const resetForm = () => {
|
||||
customErrorCodeInput.value = null
|
||||
headerOverrideEnabled.value = false
|
||||
headerOverrideRows.value = []
|
||||
grokOAuthCustomBaseUrlEnabled.value = false
|
||||
grokOAuthBaseUrl.value = ''
|
||||
interceptWarmupRequests.value = false
|
||||
autoPauseOnExpired.value = true
|
||||
openaiPassthroughEnabled.value = false
|
||||
@@ -5194,6 +5230,7 @@ const handleGrokValidateRT = async (refreshTokenInput: string) => {
|
||||
grokOAuth.error.value = t('admin.accounts.oauth.grok.pleaseEnterRefreshToken')
|
||||
return
|
||||
}
|
||||
if (!validateGrokOAuthUpstreamConfig()) return
|
||||
|
||||
grokOAuth.loading.value = true
|
||||
grokOAuth.error.value = ''
|
||||
@@ -5214,6 +5251,7 @@ const handleGrokValidateRT = async (refreshTokenInput: string) => {
|
||||
}
|
||||
|
||||
const credentials = grokOAuth.buildCredentials(tokenInfo)
|
||||
applyGrokOAuthUpstreamConfig(credentials)
|
||||
const extra = grokOAuth.buildExtraInfo(tokenInfo)
|
||||
const accountName = refreshTokens.length > 1 ? `${form.name || tokenInfo.email || 'Grok OAuth Account'} #${i + 1}` : (form.name || tokenInfo.email || 'Grok OAuth Account')
|
||||
|
||||
@@ -5277,11 +5315,13 @@ const handleGrokImportSSO = async (ssoInput: string) => {
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token)
|
||||
if (ssoTokens.length === 0) return
|
||||
if (!validateGrokOAuthUpstreamConfig()) return
|
||||
|
||||
grokOAuth.loading.value = true
|
||||
grokOAuth.error.value = ''
|
||||
|
||||
const credentials: Record<string, unknown> = {}
|
||||
applyGrokOAuthUpstreamConfig(credentials)
|
||||
const modelMapping = buildModelMappingObject(modelRestrictionMode.value, allowedModels.value, modelMappings.value)
|
||||
if (modelMapping) {
|
||||
credentials.model_mapping = modelMapping
|
||||
@@ -5904,6 +5944,7 @@ const handleAntigravityExchange = async (authCode: string) => {
|
||||
// Grok OAuth 授权码兑换
|
||||
const handleGrokExchange = async (authCode: string) => {
|
||||
if (!authCode.trim() || !grokOAuth.sessionId.value) return
|
||||
if (!validateGrokOAuthUpstreamConfig()) return
|
||||
|
||||
grokOAuth.loading.value = true
|
||||
grokOAuth.error.value = ''
|
||||
@@ -5926,6 +5967,7 @@ const handleGrokExchange = async (authCode: string) => {
|
||||
if (!tokenInfo) return
|
||||
|
||||
const credentials = grokOAuth.buildCredentials(tokenInfo)
|
||||
applyGrokOAuthUpstreamConfig(credentials)
|
||||
const extra = grokOAuth.buildExtraInfo(tokenInfo)
|
||||
await createAccountAndFinish('grok', 'oauth', credentials, extra)
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -497,74 +497,10 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="headerOverrideRows.length > 0" class="space-y-2">
|
||||
<div
|
||||
v-for="(row, index) in headerOverrideRows"
|
||||
:key="getHeaderOverrideRowKey(row)"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<input
|
||||
v-model="row.name"
|
||||
type="text"
|
||||
class="input flex-1"
|
||||
:placeholder="t('admin.accounts.headerOverride.namePlaceholder')"
|
||||
/>
|
||||
<input
|
||||
v-model="row.value"
|
||||
type="text"
|
||||
class="input flex-1"
|
||||
:placeholder="t('admin.accounts.headerOverride.valuePlaceholder')"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="removeHeaderOverrideRow(index)"
|
||||
class="rounded-lg p-2 text-red-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@click="addHeaderOverrideRow"
|
||||
class="w-full rounded-lg border-2 border-dashed border-gray-300 px-4 py-2 text-gray-600 transition-colors hover:border-gray-400 hover:text-gray-700 dark:border-dark-500 dark:text-gray-400 dark:hover:border-dark-400 dark:hover:text-gray-300"
|
||||
>
|
||||
<svg class="mr-1 inline h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
{{ t('admin.accounts.headerOverride.addRow') }}
|
||||
</button>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@click="fillHeaderOverrideTemplate"
|
||||
class="rounded-lg bg-primary-50 px-3 py-1 text-xs text-primary-700 transition-colors hover:bg-primary-100 dark:bg-primary-900/30 dark:text-primary-400 dark:hover:bg-primary-900/50"
|
||||
>
|
||||
+ {{ t('admin.accounts.headerOverride.fillTemplate') }}
|
||||
</button>
|
||||
<HeaderOverrideJsonTools
|
||||
:rows="headerOverrideRows"
|
||||
@update:rows="headerOverrideRows = $event"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.headerOverride.emptyValueHint') }}
|
||||
</p>
|
||||
<HeaderOverrideEditor
|
||||
:rows="headerOverrideRows"
|
||||
@update:rows="headerOverrideRows = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2630,7 +2566,7 @@ import ProxyAdBanner from '@/components/common/ProxyAdBanner.vue'
|
||||
import GroupSelector from '@/components/common/GroupSelector.vue'
|
||||
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
||||
import QuotaLimitCard from '@/components/account/QuotaLimitCard.vue'
|
||||
import HeaderOverrideJsonTools from '@/components/account/HeaderOverrideJsonTools.vue'
|
||||
import HeaderOverrideEditor from '@/components/account/HeaderOverrideEditor.vue'
|
||||
import {
|
||||
applyAntigravityProjectID,
|
||||
applyHeaderOverride,
|
||||
@@ -2638,7 +2574,6 @@ import {
|
||||
applyPlanType,
|
||||
buildPlanTypeOptions,
|
||||
readPlanType,
|
||||
getHeaderOverrideTemplate,
|
||||
isCustomGrokBaseUrl,
|
||||
isHeaderOverrideCapable,
|
||||
splitHeaderOverridesObject,
|
||||
@@ -2788,27 +2723,6 @@ const headerOverrideCapable = computed(
|
||||
const grokOAuthCustomBaseUrlEnabled = ref(false)
|
||||
const grokOAuthBaseUrl = ref('')
|
||||
|
||||
const addHeaderOverrideRow = () => {
|
||||
headerOverrideRows.value.push({ name: '', value: '' })
|
||||
}
|
||||
|
||||
const removeHeaderOverrideRow = (index: number) => {
|
||||
headerOverrideRows.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 模板按钮:填入标准客户端请求头名称(值留空),跳过已存在的同名行
|
||||
const fillHeaderOverrideTemplate = () => {
|
||||
const existing = new Set(
|
||||
headerOverrideRows.value.map((row) => row.name.trim().toLowerCase()).filter(Boolean)
|
||||
)
|
||||
const rows = headerOverrideRows.value.filter((row) => row.name.trim() || row.value.trim())
|
||||
for (const row of getHeaderOverrideTemplate(props.account?.platform || '')) {
|
||||
if (!existing.has(row.name)) {
|
||||
rows.push(row)
|
||||
}
|
||||
}
|
||||
headerOverrideRows.value = rows
|
||||
}
|
||||
const interceptWarmupRequests = ref(false)
|
||||
const autoPauseOnExpired = ref(false)
|
||||
const autoPause5hThreshold = ref<number | null>(null)
|
||||
@@ -2825,7 +2739,6 @@ const isSyncingAntigravityUpstream = ref(false)
|
||||
const tempUnschedEnabled = ref(false)
|
||||
const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
|
||||
const getModelMappingKey = createStableObjectKeyResolver<ModelMapping>('edit-model-mapping')
|
||||
const getHeaderOverrideRowKey = createStableObjectKeyResolver<HeaderOverrideRow>('edit-header-override-row')
|
||||
const getOpenAICompactModelMappingKey = createStableObjectKeyResolver<ModelMapping>('edit-openai-compact-model-mapping')
|
||||
const getAntigravityModelMappingKey = createStableObjectKeyResolver<ModelMapping>('edit-antigravity-model-mapping')
|
||||
const getTempUnschedRuleKey = createStableObjectKeyResolver<TempUnschedRuleForm>('edit-temp-unsched-rule')
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div v-if="rows.length > 0" class="space-y-2">
|
||||
<div
|
||||
v-for="(row, index) in rows"
|
||||
:key="getHeaderOverrideRowKey(row)"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<input
|
||||
v-model="row.name"
|
||||
type="text"
|
||||
class="input flex-1"
|
||||
:placeholder="t('admin.accounts.headerOverride.namePlaceholder')"
|
||||
/>
|
||||
<input
|
||||
v-model="row.value"
|
||||
type="text"
|
||||
class="input flex-1"
|
||||
:placeholder="t('admin.accounts.headerOverride.valuePlaceholder')"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg p-2 text-red-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
|
||||
@click="removeRow(index)"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="w-full rounded-lg border-2 border-dashed border-gray-300 px-4 py-2 text-gray-600 transition-colors hover:border-gray-400 hover:text-gray-700 dark:border-dark-500 dark:text-gray-400 dark:hover:border-dark-400 dark:hover:text-gray-300"
|
||||
@click="addRow"
|
||||
>
|
||||
<svg class="mr-1 inline h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
{{ t('admin.accounts.headerOverride.addRow') }}
|
||||
</button>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<HeaderOverrideJsonTools :rows="rows" @update:rows="emit('update:rows', $event)" />
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.headerOverride.emptyValueHint') }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { createStableObjectKeyResolver } from '@/utils/stableObjectKey'
|
||||
import HeaderOverrideJsonTools from './HeaderOverrideJsonTools.vue'
|
||||
import type { HeaderOverrideRow } from './credentialsBuilder'
|
||||
|
||||
const props = defineProps<{
|
||||
rows: HeaderOverrideRow[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:rows', rows: HeaderOverrideRow[]): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const getHeaderOverrideRowKey = createStableObjectKeyResolver<HeaderOverrideRow>(
|
||||
'header-override-row'
|
||||
)
|
||||
|
||||
const addRow = () => {
|
||||
emit('update:rows', [...props.rows, { name: '', value: '' }])
|
||||
}
|
||||
|
||||
const removeRow = (index: number) => {
|
||||
emit('update:rows', props.rows.filter((_, i) => i !== index))
|
||||
}
|
||||
</script>
|
||||
@@ -15,12 +15,13 @@
|
||||
{{ t('admin.accounts.headerOverride.copyJson') }}
|
||||
</button>
|
||||
|
||||
<div v-if="showImportPanel" class="w-full space-y-2">
|
||||
<div v-if="showImportPanel" ref="importPanelRef" class="w-full space-y-2">
|
||||
<textarea
|
||||
ref="importTextareaRef"
|
||||
v-model="importText"
|
||||
rows="5"
|
||||
class="input font-mono text-xs"
|
||||
:placeholder="t('admin.accounts.headerOverride.importJsonPlaceholder')"
|
||||
:placeholder="IMPORT_JSON_PLACEHOLDER"
|
||||
></textarea>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@@ -45,7 +46,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
@@ -67,13 +68,25 @@ const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
const { copyToClipboard } = useClipboard()
|
||||
|
||||
// JSON 示例语言中立,且花括号会被 vue-i18n 消息编译器当作插值占位符解析
|
||||
// 导致渲染时抛 SyntaxError,因此不走 i18n。
|
||||
const IMPORT_JSON_PLACEHOLDER = '{"user-agent": "my-client/1.0", "x-relay-token": "..."}'
|
||||
|
||||
const showImportPanel = ref(false)
|
||||
const importText = ref('')
|
||||
const importPanelRef = ref<HTMLElement | null>(null)
|
||||
const importTextareaRef = ref<HTMLTextAreaElement | null>(null)
|
||||
|
||||
const hasNamedRows = computed(() => props.rows.some((row) => row.name.trim()))
|
||||
|
||||
const toggleImportPanel = () => {
|
||||
// 面板在弹窗滚动容器(.modal-body)内向下展开,可能落在当前视野之外,
|
||||
// 打开后必须主动滚入视野,否则看起来像点击无效。
|
||||
const toggleImportPanel = async () => {
|
||||
showImportPanel.value = !showImportPanel.value
|
||||
if (!showImportPanel.value) return
|
||||
await nextTick()
|
||||
importPanelRef.value?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
|
||||
importTextareaRef.value?.focus({ preventScroll: true })
|
||||
}
|
||||
|
||||
const closeImportPanel = () => {
|
||||
|
||||
@@ -16,4 +16,16 @@ describe('CreateAccountModal Grok account types', () => {
|
||||
expect(source).toContain("form.platform === 'grok'")
|
||||
expect(source).toContain("? 'xai-...'")
|
||||
})
|
||||
|
||||
it('exposes custom upstream URL and header override for the OAuth create flow', () => {
|
||||
expect(source).toContain('data-testid="grok-custom-base-url-toggle"')
|
||||
expect(source).toContain('data-testid="grok-custom-base-url-input"')
|
||||
expect(source).toContain('form.platform === \'grok\' && isOAuthFlow')
|
||||
})
|
||||
|
||||
it('validates and applies upstream config on all three Grok OAuth create paths', () => {
|
||||
// 授权码兑换 / RT 批量 / SSO 批量 3 处调用(定义为箭头函数,不计入)
|
||||
expect(source.match(/validateGrokOAuthUpstreamConfig\(\)/g)?.length).toBe(3)
|
||||
expect(source.match(/applyGrokOAuthUpstreamConfig\(credentials\)/g)?.length).toBe(3)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
applyPlanType,
|
||||
buildHeaderOverridesObject,
|
||||
buildPlanTypeOptions,
|
||||
getHeaderOverrideTemplate,
|
||||
isCustomGrokBaseUrl,
|
||||
isHeaderOverrideCapable,
|
||||
parseHeaderOverridesJson,
|
||||
@@ -265,39 +264,6 @@ describe('buildHeaderOverridesObject / splitHeaderOverridesObject', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('getHeaderOverrideTemplate', () => {
|
||||
it('returns Claude Code CLI headers with empty values for anthropic', () => {
|
||||
const rows = getHeaderOverrideTemplate('anthropic')
|
||||
expect(rows.every((r) => r.value === '')).toBe(true)
|
||||
const names = rows.map((r) => r.name)
|
||||
expect(names).toContain('user-agent')
|
||||
expect(names).toContain('x-app')
|
||||
expect(names).toContain('anthropic-beta')
|
||||
expect(names).toContain('x-stainless-lang')
|
||||
expect(validateHeaderOverrideRows(rows)).toBeNull()
|
||||
})
|
||||
|
||||
it('returns Codex CLI headers with empty values for openai', () => {
|
||||
const rows = getHeaderOverrideTemplate('openai')
|
||||
expect(rows.every((r) => r.value === '')).toBe(true)
|
||||
const names = rows.map((r) => r.name)
|
||||
expect(names).toContain('user-agent')
|
||||
expect(names).toContain('originator')
|
||||
expect(names).toContain('openai-beta')
|
||||
expect(validateHeaderOverrideRows(rows)).toBeNull()
|
||||
})
|
||||
|
||||
it('returns Grok forwarding headers with empty values for grok', () => {
|
||||
const rows = getHeaderOverrideTemplate('grok')
|
||||
expect(rows.every((r) => r.value === '')).toBe(true)
|
||||
const names = rows.map((r) => r.name)
|
||||
expect(names).toContain('user-agent')
|
||||
expect(names).toContain('x-xai-token-auth')
|
||||
expect(names).toContain('x-grok-client-version')
|
||||
expect(validateHeaderOverrideRows(rows)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('applyHeaderOverride', () => {
|
||||
it('create + enabled: writes enabled flag and overrides object', () => {
|
||||
const creds: Record<string, unknown> = { api_key: 'sk' }
|
||||
|
||||
@@ -87,46 +87,6 @@ function isValidHeaderOverrideName(name: string): boolean {
|
||||
return HEADER_NAME_PATTERN.test(name)
|
||||
}
|
||||
|
||||
/** 模板:Claude Code CLI 标准客户端请求头(值留空由管理员填写) */
|
||||
const ANTHROPIC_HEADER_OVERRIDE_TEMPLATE = [
|
||||
'user-agent',
|
||||
'x-app',
|
||||
'anthropic-beta',
|
||||
'anthropic-version',
|
||||
'anthropic-dangerous-direct-browser-access',
|
||||
'x-stainless-lang',
|
||||
'x-stainless-package-version',
|
||||
'x-stainless-os',
|
||||
'x-stainless-arch',
|
||||
'x-stainless-runtime',
|
||||
'x-stainless-runtime-version',
|
||||
'x-stainless-retry-count',
|
||||
'x-stainless-timeout'
|
||||
]
|
||||
|
||||
/** 模板:Codex CLI 标准客户端请求头(值留空由管理员填写) */
|
||||
const OPENAI_HEADER_OVERRIDE_TEMPLATE = [
|
||||
'user-agent',
|
||||
'originator',
|
||||
'openai-beta',
|
||||
'version',
|
||||
'accept',
|
||||
'accept-language'
|
||||
]
|
||||
|
||||
/** 模板:Grok 转发常用请求头(第三方转发网关通常需要的身份/准入头,值留空由管理员填写) */
|
||||
const GROK_HEADER_OVERRIDE_TEMPLATE = ['user-agent', 'x-xai-token-auth', 'x-grok-client-version']
|
||||
|
||||
export function getHeaderOverrideTemplate(platform: string): HeaderOverrideRow[] {
|
||||
const names =
|
||||
platform === 'openai'
|
||||
? OPENAI_HEADER_OVERRIDE_TEMPLATE
|
||||
: platform === 'grok'
|
||||
? GROK_HEADER_OVERRIDE_TEMPLATE
|
||||
: ANTHROPIC_HEADER_OVERRIDE_TEMPLATE
|
||||
return names.map((name) => ({ name, value: '' }))
|
||||
}
|
||||
|
||||
/** 与后端 maxHeaderOverride* 常量保持一致 */
|
||||
const HEADER_OVERRIDE_MAX_ENTRIES = 64
|
||||
const HEADER_OVERRIDE_MAX_NAME_LENGTH = 200
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { baseCompile } from '@intlify/message-compiler'
|
||||
|
||||
import en from '../locales/en'
|
||||
import zh from '../locales/zh'
|
||||
|
||||
// vue-i18n 在运行时才编译消息:文案里未转义的花括号(如内嵌 JSON 示例
|
||||
// "{\"user-agent\": ...}")会在渲染时抛 "Invalid token in placeholder",
|
||||
// 直接炸掉整个组件树,且构建期完全无感。本测试把全部文案预编译一遍,
|
||||
// 将该类问题固化为显式失败。字面量花括号请用 {'{'} / {'}'} 转义,
|
||||
// 或将语言中立的示例文本(如 JSON)移出 i18n。
|
||||
function collectCompileErrors(node: unknown, path: string, out: string[]): void {
|
||||
if (typeof node === 'string') {
|
||||
baseCompile(node, {
|
||||
onError: (err) => {
|
||||
out.push(`${path}: ${err.message}`)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
if (Array.isArray(node)) {
|
||||
node.forEach((item, index) => collectCompileErrors(item, `${path}[${index}]`, out))
|
||||
return
|
||||
}
|
||||
if (node && typeof node === 'object') {
|
||||
for (const [key, value] of Object.entries(node as Record<string, unknown>)) {
|
||||
collectCompileErrors(value, path ? `${path}.${key}` : key, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('locale messages compile', () => {
|
||||
it.each([
|
||||
['zh', zh],
|
||||
['en', en]
|
||||
] as const)('%s messages all compile without placeholder errors', (locale, messages) => {
|
||||
const errors: string[] = []
|
||||
collectCompileErrors(messages, locale, errors)
|
||||
expect(errors).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -578,9 +578,7 @@ export default {
|
||||
namePlaceholder: 'Header name (e.g. user-agent)',
|
||||
valuePlaceholder: 'Override value (leave empty to skip)',
|
||||
addRow: 'Add Header',
|
||||
fillTemplate: 'Fill Template',
|
||||
importJson: 'Import JSON',
|
||||
importJsonPlaceholder: '{"user-agent": "my-client/1.0", "x-relay-token": "..."}',
|
||||
importJsonApply: 'Parse & Fill',
|
||||
importJsonCancel: 'Cancel',
|
||||
importJsonHint: 'Paste a flat JSON object (header name → value). Parsing replaces the current rows.',
|
||||
|
||||
@@ -321,8 +321,7 @@ export default {
|
||||
claudeOAuthSystemPromptPlaceholder: 'Leave empty to use the built-in Claude Code expansion prompt.',
|
||||
claudeOAuthSystemPromptHint: 'Legacy compatibility: controls only the third injected system block.',
|
||||
claudeOAuthSystemPromptBlocks: 'Claude OAuth System Blocks',
|
||||
claudeOAuthSystemPromptBlocksPlaceholder: 'Leave empty to use the built-in 3 blocks. Supports an array or {"blocks": [...]}.',
|
||||
claudeOAuthSystemPromptBlocksHint: 'Each block is saved as JSON with enabled, type, text, and optional cache_control. {billing_header} stays dynamic per request; the Claude Code identity and expansion prompts can be edited directly or restored from presets.',
|
||||
claudeOAuthSystemPromptBlocksHint: "Each block is saved as JSON with enabled, type, text, and optional cache_control. {'{'}billing_header{'}'} stays dynamic per request; the Claude Code identity and expansion prompts can be edited directly or restored from presets.",
|
||||
systemBlockTitle: 'System Block {index}',
|
||||
systemBlockPreset: 'Preset',
|
||||
systemBlockPresetBilling: 'Billing header',
|
||||
|
||||
@@ -671,9 +671,7 @@ export default {
|
||||
namePlaceholder: '请求头名称(如 user-agent)',
|
||||
valuePlaceholder: '覆写值(留空表示不覆写)',
|
||||
addRow: '添加请求头',
|
||||
fillTemplate: '填入模板',
|
||||
importJson: 'JSON 导入',
|
||||
importJsonPlaceholder: '{"user-agent": "my-client/1.0", "x-relay-token": "..."}',
|
||||
importJsonApply: '解析并填入',
|
||||
importJsonCancel: '取消',
|
||||
importJsonHint: '粘贴扁平 JSON 对象(请求头名 → 值),解析后将整体替换当前列表。',
|
||||
|
||||
@@ -315,8 +315,7 @@ export default {
|
||||
claudeOAuthSystemPromptPlaceholder: '留空时使用内置 Claude Code 扩展提示词。',
|
||||
claudeOAuthSystemPromptHint: '兼容旧配置:仅控制第三个注入的 system block。',
|
||||
claudeOAuthSystemPromptBlocks: 'Claude OAuth System Blocks',
|
||||
claudeOAuthSystemPromptBlocksPlaceholder: '留空时使用内置 3 个 blocks。支持数组或 {"blocks": [...]}。',
|
||||
claudeOAuthSystemPromptBlocksHint: '每个 block 会保存为带 enabled、type、text、可选 cache_control 的 JSON。{billing_header} 会按请求动态生成;Claude Code 身份提示词和扩展提示词可直接编辑,也可用预设恢复默认值。',
|
||||
claudeOAuthSystemPromptBlocksHint: "每个 block 会保存为带 enabled、type、text、可选 cache_control 的 JSON。{'{'}billing_header{'}'} 会按请求动态生成;Claude Code 身份提示词和扩展提示词可直接编辑,也可用预设恢复默认值。",
|
||||
systemBlockTitle: 'System Block {index}',
|
||||
systemBlockPreset: '预设',
|
||||
systemBlockPresetBilling: 'Billing Header',
|
||||
|
||||
Reference in New Issue
Block a user