Initial open-source release (MIT): image2api AI gateway
Full Go backend + Vue 3 frontend, OpenAI-compatible API, multi-provider account pools, billing/admin, Docker one-command deploy with auto HTTPS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
<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 status = ref('')
|
||||
const isError = ref(false)
|
||||
const submitting = ref(false)
|
||||
|
||||
// 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
|
||||
return { total: items.length, openai, adobe, runway, leonardo, krea, imagine }
|
||||
})
|
||||
|
||||
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 === '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++
|
||||
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">多个 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>
|
||||
</template>
|
||||
<span v-else class="text-rose-600">未识别到任何 Cookie 或 JWT</span>
|
||||
</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>
|
||||
Reference in New Issue
Block a user