feat: track imported accounts per upstream key across platforms

Add upstream_key_account_links table mapping generated keys to remote
accounts per website/platform, surface imported_accounts on key
responses, and update website sync/routers to manage the links.
This commit is contained in:
liujing
2026-07-23 17:52:58 +08:00
parent ee89cd6119
commit df7e400e7b
17 changed files with 1081 additions and 537 deletions
+11 -1
View File
@@ -241,6 +241,14 @@ export interface GeneratedUpstreamKey {
imported_at: string | null
created_at: string | null
has_key_value: boolean
imported_accounts: Array<{
id: number
website_id: number
target_group: string
platform: string
remote_account_id: string
status: string
}>
}
export interface UpstreamRecharge {
@@ -351,6 +359,7 @@ export interface WebsiteGroup {
name: string
rate_multiplier: string | null
description?: string | null
platform?: string | null
raw: Record<string, any>
}
@@ -371,6 +380,7 @@ export interface GroupBindingData {
percent: number
algorithm: string
enabled: boolean
platform?: string | null
created_at: string
updated_at: string
}
@@ -516,7 +526,7 @@ export const websitesApi = {
upstream_key_ids: number[]
target_group_map: Record<string, string>
account_name_prefix: string
default_platform: string
default_platform?: string
platform_mode?: string
concurrency?: number
priority?: number
+15 -47
View File
@@ -190,7 +190,7 @@
<div class="binding-title">{{ binding.website_name }} / {{ binding.target_group_name || binding.target_group_id }}</div>
<div class="binding-meta">
{{ binding.algorithm === 'priority_weighted_plus_percent' ? `${algorithmLabel(binding.algorithm)} · 自动约 ${binding.percent}%` : `${algorithmLabel(binding.algorithm)} + ${binding.percent}%` }} ·
{{ binding.source_groups.length }} 个上游分组
{{ binding.source_groups.length }} 个上游分组 · {{ platformLabel(binding.platform || '') }}
</div>
</div>
<div class="binding-actions">
@@ -279,7 +279,7 @@
<el-form-item label="目标分组" prop="target_group_id">
<div class="custom-select-wrapper">
<el-select v-model="bindingForm.target_group_id" filterable style="width:100%" @change="onTargetGroupChange" class="custom-theme-select">
<el-option v-for="group in bindingWebsiteGroups" :key="group.id" :label="`${group.name} (${group.rate_multiplier ?? '—'})`" :value="group.id" />
<el-option v-for="group in bindingWebsiteGroups" :key="group.id" :label="`${group.name} · ${platformLabel(group.platform || '')} (${group.rate_multiplier ?? '—'})`" :value="group.id" />
</el-select>
</div>
</el-form-item>
@@ -603,24 +603,8 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="账号平台">
<el-select v-model="importAccountsForm.platform_mode" style="width:100%" @change="onPlatformModeChange">
<el-option label="自动识别(按 Key/分组名判断)" value="auto" />
<el-option label="手动选择" value="manual" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="固定平台(手动模式)">
<el-select v-model="importAccountsForm.default_platform" style="width:100%" :disabled="importAccountsForm.platform_mode === 'auto'">
<el-option label="OpenAI 兼容" value="openai" />
<el-option label="Anthropic" value="anthropic" />
<el-option label="Gemini" value="gemini" />
<el-option label="Grok" value="grok" />
<el-option label="Antigravity" value="antigravity" />
</el-select>
</el-form-item>
<el-col :xs="24" :sm="12">
<el-alert type="info" :closable="false" title="账号平台取自目标网站分组。" />
</el-col>
<el-col :span="24" style="margin-bottom:6px">
<el-button size="small" text :loading="syncingImportStatus" @click="syncImportStatus">
@@ -705,7 +689,7 @@
<el-option
v-for="item in importableGeneratedKeys"
:key="item.id!"
:label="`${item.group_name || item.group_id} / ${detectPlatform(item)} / ${item.key_name} / ${item.masked_key}`"
:label="`${item.group_name || item.group_id} / ${item.key_name} / ${item.masked_key}`"
:value="item.id!"
/>
</el-select>
@@ -714,8 +698,8 @@
<div class="result-title">目标分组映射</div>
<div v-for="group in selectedAccountGroups" :key="group.group_id" class="mapping-row">
<span class="mapping-label">{{ group.group_name || group.group_id }}</span>
<el-select v-model="importAccountsForm.target_group_map[group.group_id]" clearable filterable placeholder="可不选" style="width:280px">
<el-option v-for="target in importTargetGroups" :key="target.id" :label="`${target.name} (${target.rate_multiplier ?? '—'})`" :value="target.id" />
<el-select v-model="importAccountsForm.target_group_map[group.group_id]" clearable filterable placeholder="请选择目标分组" style="width:min(100%, 280px)">
<el-option v-for="target in importTargetGroups" :key="target.id" :label="`${target.name} · ${platformLabel(target.platform || '')} (${target.rate_multiplier ?? '—'})`" :value="target.id" />
</el-select>
</div>
</div>
@@ -725,7 +709,7 @@
<el-table :data="importAccountResults" size="small">
<el-table-column prop="source_group_name" label="来源分组" min-width="130" />
<el-table-column prop="account_name" label="账号管理账号" min-width="180" />
<el-table-column label="识别平台" width="120">
<el-table-column label="目标平台" width="120">
<template #default="{ row }">
<span>{{ platformLabel(row.platform) }}</span>
</template>
@@ -1291,8 +1275,6 @@ const importAccountsForm = ref({
upstream_key_ids: [] as number[],
target_group_map: {} as Record<string, string>,
account_name_prefix: 'SmartUp',
default_platform: 'openai',
platform_mode: 'auto',
concurrency: 100,
priority: 1,
auto_priority_by_rate: true,
@@ -1379,7 +1361,6 @@ function isImportableGeneratedKey(item: GeneratedUpstreamKey) {
&& item.has_key_value
&& item.status !== 'failed'
&& item.status !== 'orphaned'
&& !(item.imported_website_id === importAccountsForm.value.website_id && item.imported_account_id)
}
const importableGeneratedKeys = computed(() =>
@@ -1388,7 +1369,9 @@ const importableGeneratedKeys = computed(() =>
const importedGeneratedKeyCount = computed(() =>
importGeneratedKeys.value.filter((item) =>
item.imported_website_id === importAccountsForm.value.website_id && Boolean(item.imported_account_id),
item.imported_accounts.some(link =>
link.website_id === importAccountsForm.value.website_id && link.status === 'active',
),
).length,
)
@@ -2290,8 +2273,6 @@ async function openImportAccounts(site?: WebsiteData | null) {
upstream_key_ids: [],
target_group_map: {},
account_name_prefix: 'SmartUp',
default_platform: 'openai',
platform_mode: 'auto',
concurrency: 100,
priority: 1,
auto_priority_by_rate: true,
@@ -2330,21 +2311,6 @@ async function onImportAccountUpstreamChange(value: number) {
})
}
function onPlatformModeChange(value: string) {
if (value === 'auto') {
importAccountsForm.value.default_platform = 'openai'
}
}
function detectPlatform(item: { group_name?: string; group_id?: string; key_name?: string }) {
const text = `${item.group_name || ''} ${item.group_id || ''} ${item.key_name || ''}`.toLowerCase()
if (text.includes('claude') || text.includes('anthropic')) return 'Anthropic'
if (text.includes('gemini')) return 'Gemini'
if (text.includes('grok') || text.includes('xai')) return 'Grok'
if (text.includes('antigravity')) return 'Antigravity'
return 'OpenAI 兼容'
}
function platformLabel(platform: string) {
const map: Record<string, string> = {
openai: 'OpenAI 兼容',
@@ -2421,14 +2387,16 @@ async function submitImportAccounts() {
ElMessage.error('请选择要导入的上游 Key')
return
}
if (selectedAccountGroups.value.some(group => !importAccountsForm.value.target_group_map[group.group_id])) {
ElMessage.error('请为每个来源分组选择目标分组')
return
}
importingAccounts.value = true
try {
const res = await websitesApi.importAccountsFromUpstreamKeys(importAccountsForm.value.website_id, {
upstream_key_ids: importAccountsForm.value.upstream_key_ids,
target_group_map: importAccountsForm.value.target_group_map,
account_name_prefix: importAccountsForm.value.account_name_prefix,
default_platform: importAccountsForm.value.default_platform,
platform_mode: importAccountsForm.value.platform_mode,
concurrency: importAccountsForm.value.concurrency,
priority: importAccountsForm.value.priority,
auto_priority_by_rate: importAccountsForm.value.auto_priority_by_rate,