Merge pull request #1299 from DaydreamCoding/feat/antigravity-privacy-and-subscription
feat(antigravity): 自动隐私设置 + 订阅状态检测
This commit is contained in:
@@ -627,6 +627,16 @@ export async function batchRefresh(accountIds: number[]): Promise<BatchOperation
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Set privacy for an Antigravity OAuth account
|
||||
* @param id - Account ID
|
||||
* @returns Updated account
|
||||
*/
|
||||
export async function setPrivacy(id: number): Promise<Account> {
|
||||
const { data } = await apiClient.post<Account>(`/admin/accounts/${id}/set-privacy`)
|
||||
return data
|
||||
}
|
||||
|
||||
export const accountsAPI = {
|
||||
list,
|
||||
listWithEtag,
|
||||
@@ -663,7 +673,8 @@ export const accountsAPI = {
|
||||
importData,
|
||||
getAntigravityDefaultModelMapping,
|
||||
batchClearError,
|
||||
batchRefresh
|
||||
batchRefresh,
|
||||
setPrivacy
|
||||
}
|
||||
|
||||
export default accountsAPI
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
{{ t('admin.accounts.refreshToken') }}
|
||||
</button>
|
||||
</template>
|
||||
<button v-if="isAntigravityOAuth" @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,7 @@ 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 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: {
|
||||
|
||||
@@ -2022,6 +2022,10 @@ export default {
|
||||
privacyTrainingOff: '已关闭训练数据共享',
|
||||
privacyCfBlocked: '被 Cloudflare 拦截,训练可能仍开启',
|
||||
privacyFailed: '关闭训练数据共享失败',
|
||||
privacyAntigravitySet: '已关闭遥测和营销邮件',
|
||||
privacyAntigravityFailed: '隐私设置失败',
|
||||
setPrivacy: '设置隐私',
|
||||
subscriptionAbnormal: '异常',
|
||||
// 容量状态提示
|
||||
capacity: {
|
||||
windowCost: {
|
||||
|
||||
@@ -276,7 +276,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" />
|
||||
@@ -1241,6 +1241,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.privacyAntigravityFailed'))
|
||||
}
|
||||
}
|
||||
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) => {
|
||||
|
||||
Reference in New Issue
Block a user