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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user