Files
image2api/frontend/src/components/ImportModal.vue
T
chiyiandClaude Opus 4.8 3ef84bb9b3 feat: 自定义 OpenAI 兼容上游聚合 + 权重/并发调度
- custom provider:把任意 v1 端点当账号(base_url+key),按 model id 路由,
  直连不走代理;支持 images/generations、images/edits、Sora 式异步视频
- 调度器全局权重优先 + 每账号并发感知(上游账号可配并发,其余系统固定)
- 后台:添加/编辑上游、自定义模型表单(每档普通/代理价,留空=不支持)、
  账号权重/并发列与导入权重
- grok-video / nano-banana-2(runway) 写入硬编码目录(开源自带);
  去掉 540p,Adobe 视频最低 720p
- UI:SelectMenu 与输入框等高、测试耗时改用秒

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 21:59:46 +08:00

157 lines
8.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { ref, computed } from 'vue'
import { api, jsonBody } from '../api'
import { parseImportInput } from '../utils/import'
import Icon from './Icon.vue'
const emit = defineEmits(['close', 'imported'])
const input = ref('')
const weight = ref(0)
const status = ref('')
const isError = ref(false)
const submitting = ref(false)
// type → token pool (for the post-import weight PATCH).
const TYPE_POOL = { openai: 'chatgpt', adobe: 'adobe', runway: 'runway', leonardo: 'leonardo', krea: 'krea', imagine: 'imagine', grok: 'grok' }
// Live preview of what the parser would extract — updates as the user types
// so they can see whether their paste was understood before clicking import.
const detected = computed(() => {
const items = parseImportInput(input.value)
const openai = items.filter((x) => x.type === 'openai').length
const adobe = items.filter((x) => x.type === 'adobe').length
const runway = items.filter((x) => x.type === 'runway').length
const leonardo = items.filter((x) => x.type === 'leonardo').length
const krea = items.filter((x) => x.type === 'krea').length
const imagine = items.filter((x) => x.type === 'imagine').length
const grok = items.filter((x) => x.type === 'grok').length
return { total: items.length, openai, adobe, runway, leonardo, krea, imagine, grok }
})
function setStatus(text, err = false) {
status.value = text || ''
isError.value = err
}
async function doSmartImport() {
const items = parseImportInput(input.value)
if (!items.length) {
setStatus('未识别到任何 Cookie 或 JWT', true)
return
}
submitting.value = true
let ok = 0, fail = 0
const errs = []
for (let i = 0; i < items.length; i++) {
const it = items[i]
setStatus(`正在导入 ${i + 1}/${items.length} (${it.type})…`)
try {
const r = it.type === 'openai'
? await api('/tokens/import-chatgpt-token', jsonBody('POST', { access_token: it.value }))
: it.type === 'grok'
? await api('/tokens/import-grok-token', jsonBody('POST', { access_token: it.value }))
: it.type === 'runway'
? await api('/tokens/import-runway-token', jsonBody('POST', { access_token: it.value }))
: it.type === 'leonardo'
? await api('/tokens/import-leonardo-cookie', jsonBody('POST', { cookie: it.value }))
: it.type === 'krea'
? await api('/tokens/import-krea-cookie', jsonBody('POST', { cookie: it.value }))
: it.type === 'imagine'
? await api('/tokens/import-imagine-token', jsonBody('POST', { value: it.value }))
: await api('/tokens/import-adobe-cookie', jsonBody('POST', { cookie: it.value }))
if (r.ok) {
ok++
// Apply the chosen weight to the freshly-imported account (best-effort).
const w = Number(weight.value) || 0
const pool = TYPE_POOL[it.type]
if (w !== 0 && pool && r.data?.id) {
try { await api(`/tokens/${pool}/${r.data.id}`, jsonBody('PATCH', { weight: w })) } catch (_) {}
}
} else { fail++; errs.push(`${it.type}: ${r.data?.detail || r.status}`) }
} catch (e) {
fail++; errs.push(`${it.type}: ${e}`)
}
}
submitting.value = false
// Quota isn't checked here — the server probes each token off-thread and the
// account list flips pending → active/dead on its own.
if (fail === 0) {
setStatus(`✓ 导入 ${ok} 项 · 正在后台检测额度…`)
emit('imported')
setTimeout(() => emit('close'), 1000)
} else {
setStatus(`成功 ${ok} · 失败 ${fail} · ${errs.slice(0, 3).join(' | ')}`, true)
emit('imported')
}
}
</script>
<template>
<div class="fixed inset-0 z-50 bg-slate-900/40 backdrop-blur-sm flex items-start justify-center overflow-y-auto p-4"
@click.self="emit('close')">
<div class="card !shadow-xl mt-14 mb-14 w-full max-w-2xl">
<div class="px-5 py-4 border-b border-slate-100 flex items-center justify-between">
<h2 class="text-sm font-semibold">导入账号</h2>
<button @click="emit('close')" class="text-slate-400 hover:text-slate-700 transition-colors">
<Icon name="close" class="w-5 h-5" />
</button>
</div>
<div class="p-5">
<p class="text-xs text-slate-500 mb-3 leading-relaxed">自动识别
<strong class="text-slate-700">Adobe Cookie 字符串</strong>(<code class="px-1 bg-slate-100 rounded">k=v; k=v; ...</code>)
<strong class="text-slate-700">Cookie JSON 对象</strong>
<strong class="text-slate-700">Cookie 数组</strong>( Adobe 批量)
<strong class="text-slate-700">ChatGPT JWT</strong>(<code class="px-1 bg-slate-100 rounded">eyJhbGciOi...</code>)
<strong class="text-slate-700">Runway JWT</strong>(自动与 ChatGPT 区分)
<strong class="text-slate-700">Leonardo Cookie</strong>( better-auth)
<strong class="text-slate-700">Krea Cookie</strong>( sb-superb-auth)
<strong class="text-slate-700">Imagine Token</strong>(<code class="px-1 bg-slate-100 rounded">{"token","refreshToken","email","parentId"}</code>)
<strong class="text-slate-700">Grok SSO</strong>(grok.com <code class="px-1 bg-slate-100 rounded">sso</code> ,仅含 session_id,自动与 ChatGPT/Runway 区分)
<strong class="text-slate-700">多个 JWT</strong>(换行分隔)
全粘进来即可无需任何前缀
</p>
<textarea v-model="input" rows="10"
class="field font-mono text-xs resize-none"
placeholder="直接粘 Cookie 字符串 / JWT / JSON,自动识别"></textarea>
<div v-if="input.trim()" class="mt-2 flex items-center gap-2 text-xs">
<template v-if="detected.total">
<span class="text-emerald-600"> 识别到 <strong class="tabular-nums">{{ detected.total }}</strong> 个账号</span>
<span v-if="detected.openai" class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-emerald-700 bg-emerald-50 ring-1 ring-emerald-200">
OpenAI · <span class="tabular-nums">{{ detected.openai }}</span>
</span>
<span v-if="detected.adobe" class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-rose-700 bg-rose-50 ring-1 ring-rose-200">
Adobe · <span class="tabular-nums">{{ detected.adobe }}</span>
</span>
<span v-if="detected.runway" class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-violet-700 bg-violet-50 ring-1 ring-violet-200">
Runway · <span class="tabular-nums">{{ detected.runway }}</span>
</span>
<span v-if="detected.leonardo" class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-amber-700 bg-amber-50 ring-1 ring-amber-200">
Leonardo · <span class="tabular-nums">{{ detected.leonardo }}</span>
</span>
<span v-if="detected.krea" class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-sky-700 bg-sky-50 ring-1 ring-sky-200">
Krea · <span class="tabular-nums">{{ detected.krea }}</span>
</span>
<span v-if="detected.imagine" class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-teal-700 bg-teal-50 ring-1 ring-teal-200">
Imagine · <span class="tabular-nums">{{ detected.imagine }}</span>
</span>
<span v-if="detected.grok" class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-slate-700 bg-slate-100 ring-1 ring-slate-300">
Grok · <span class="tabular-nums">{{ detected.grok }}</span>
</span>
</template>
<span v-else class="text-rose-600">未识别到任何 Cookie JWT</span>
</div>
<div class="mt-3 flex items-center gap-2">
<label class="text-xs text-slate-500 whitespace-nowrap">权重(本批账号,高的优先)</label>
<input v-model.number="weight" type="number" class="field !w-24" placeholder="0" />
</div>
<button @click="doSmartImport" :disabled="submitting || !detected.total" class="btn-primary w-full mt-3">
{{ submitting ? '导入中…' : (detected.total ? `识别并导入 (${detected.total})` : '识别并导入') }}
</button>
<p v-if="status" class="text-xs mt-2" :class="isError ? 'text-rose-600' : 'text-emerald-600'">{{ status }}</p>
</div>
</div>
</div>
</template>