feat: sync all features to image2api
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Production frontend: built from source, served by nginx on HTTP :80.
|
||||
# ---- build stage ----
|
||||
FROM node:22-alpine AS build
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# ---- serve stage ----
|
||||
FROM nginx:1.27-alpine
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
COPY nginx.prod.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
@@ -0,0 +1,44 @@
|
||||
# nginx for the docker production stack — HTTP on :80.
|
||||
# TLS + domain are handled externally; this just serves the SPA and proxies API.
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
client_max_body_size 50m;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# video / large-image generation can block minutes — avoid the 60s 504.
|
||||
proxy_connect_timeout 600s;
|
||||
proxy_send_timeout 600s;
|
||||
proxy_read_timeout 600s;
|
||||
|
||||
# Hashed build assets never change — cache hard.
|
||||
location /assets/ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
}
|
||||
|
||||
# SPA fallback; index.html must never be cached (else stale bundle hash).
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
}
|
||||
|
||||
# ---- API / media / health -> backend ----
|
||||
location ^~ /admin/api/ { proxy_pass http://backend:6666; add_header Cache-Control "no-store" always; }
|
||||
location ^~ /images/ { proxy_pass http://backend:6666; }
|
||||
location = /health { proxy_pass http://backend:6666; }
|
||||
# /v1 is per-API-key authenticated — never cache.
|
||||
location ^~ /v1/ {
|
||||
proxy_pass http://backend:6666;
|
||||
add_header Cache-Control "no-store" always;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ 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' }
|
||||
const TYPE_POOL = { openai: 'chatgpt', adobe: 'adobe', runway: 'runway', leonardo: 'leonardo', krea: 'krea', imagine: 'imagine', grok: 'grok', creativefabrica: 'creativefabrica' }
|
||||
|
||||
// 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.
|
||||
@@ -26,7 +26,8 @@ const detected = computed(() => {
|
||||
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 }
|
||||
const cf = items.filter((x) => x.type === 'creativefabrica').length
|
||||
return { total: items.length, openai, adobe, runway, leonardo, krea, imagine, grok, cf }
|
||||
})
|
||||
|
||||
function setStatus(text, err = false) {
|
||||
@@ -58,7 +59,9 @@ async function doSmartImport() {
|
||||
? 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 }))
|
||||
: it.type === 'creativefabrica'
|
||||
? await api('/tokens/import-creativefabrica-cookie', jsonBody('POST', { cookie: 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).
|
||||
@@ -107,6 +110,7 @@ async function doSmartImport() {
|
||||
<strong class="text-slate-700">Runway JWT</strong>(自动与 ChatGPT 区分)、
|
||||
<strong class="text-slate-700">Leonardo Cookie</strong>(须含 better-auth.session_data)、
|
||||
<strong class="text-slate-700">Krea Cookie</strong>(含 sb-superb-auth)、
|
||||
<strong class="text-slate-700">Creative Fabrica Cookie</strong>(含 cfauth_* 或 wordpress_logged_in_)、
|
||||
<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>(换行分隔)。
|
||||
@@ -142,6 +146,9 @@ async function doSmartImport() {
|
||||
<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>
|
||||
<span v-if="detected.cf" class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-fuchsia-700 bg-fuchsia-50 ring-1 ring-fuchsia-200">
|
||||
Creative Fabrica · <span class="tabular-nums">{{ detected.cf }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<span v-else class="text-rose-600">未识别到任何 Cookie 或 JWT</span>
|
||||
</div>
|
||||
|
||||
@@ -49,6 +49,13 @@ export function looksLikeKreaCookie(s) {
|
||||
return /sb-superb-auth-token/.test(s || '')
|
||||
}
|
||||
|
||||
// Creative Fabrica session cookies carry the cfauth_* family (cfauth_uid /
|
||||
// cfauth_sig / cfauth_utp) plus the WordPress SSO cookie — those markers tell
|
||||
// them apart from an Adobe/Krea/Leonardo cookie (all otherwise opaque strings).
|
||||
export function looksLikeCreativeFabricaCookie(s) {
|
||||
return /cfauth_/.test(s || '') || /cfAmpAnonymousId/.test(s || '') || /wordpress_logged_in_/.test(s || '')
|
||||
}
|
||||
|
||||
// An Imagine.art credential is a JSON object { token, refreshToken } (both JWTs).
|
||||
function isImagineObj(o) {
|
||||
return !!o && typeof o === 'object' &&
|
||||
@@ -67,6 +74,7 @@ function cookieType(v) {
|
||||
if (looksLikeImagineToken(v)) return 'imagine'
|
||||
if (looksLikeKreaCookie(v)) return 'krea'
|
||||
if (looksLikeLeonardoCookie(v)) return 'leonardo'
|
||||
if (looksLikeCreativeFabricaCookie(v)) return 'creativefabrica'
|
||||
return 'adobe'
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ function declared(v) {
|
||||
// 参考资产时返回 null。model 兼容 /managed-models(type) 与 /models(kind) 两种字段。
|
||||
export function mediaCaps(model, preset) {
|
||||
if ((model?.type || model?.kind) !== 'video') return null
|
||||
const isSeedance = /^seedance/.test(model?.id || '')
|
||||
// creativefabrica 上游只收图片参考(VIDEO_FRAME_TYPE_REFERENCE),不收视频/音频;
|
||||
// 它虽然叫 seedance,但要排除,否则会错误地显示「视频 3 音频 3」。
|
||||
const isSeedance = /^seedance/.test(model?.id || '') && (model?.provider || '') !== 'creativefabrica'
|
||||
const videos = declared(preset?.max_videos) ?? (isSeedance ? SEEDANCE_FALLBACK.videos : 0)
|
||||
const audios = declared(preset?.max_audios) ?? (isSeedance ? SEEDANCE_FALLBACK.audios : 0)
|
||||
if (!videos && !audios) return null
|
||||
|
||||
@@ -55,7 +55,7 @@ const stats = ref({
|
||||
total: 0, dead_total: 0,
|
||||
openai: { ...EMPTY_TYPE }, adobe: { ...EMPTY_TYPE }, runway: { ...EMPTY_TYPE },
|
||||
leonardo: { ...EMPTY_TYPE }, krea: { ...EMPTY_TYPE }, imagine: { ...EMPTY_TYPE },
|
||||
grok: { ...EMPTY_TYPE },
|
||||
grok: { ...EMPTY_TYPE }, creativefabrica: { ...EMPTY_TYPE },
|
||||
})
|
||||
|
||||
// 异常账号 = 已失效(401)被锁定的号(红色锁定行)。用于「一键删除异常账号」。
|
||||
@@ -69,6 +69,7 @@ function typePill(t) {
|
||||
leonardo: 'bg-amber-500/10 text-amber-300 ring-amber-400/30',
|
||||
krea: 'bg-sky-500/10 text-sky-300 ring-sky-400/30',
|
||||
imagine: 'bg-teal-500/10 text-teal-300 ring-teal-400/30',
|
||||
creativefabrica: 'bg-fuchsia-500/10 text-fuchsia-300 ring-fuchsia-400/30',
|
||||
}[t] || 'bg-white/[0.06] text-white/70 ring-white/15'
|
||||
}
|
||||
// 与后端 accountConcurrency 保持一致:adobe 普号固定 1 并发,会员号取配置值
|
||||
@@ -338,7 +339,7 @@ onMounted(() => { loadAccounts(); loadModelList() })
|
||||
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.total }}</div>
|
||||
<div class="text-[10px] text-white/35 mt-0.5">成功/失败/限额</div>
|
||||
</div>
|
||||
<div v-for="t in [['openai','OpenAI','text-emerald-300/80'],['adobe','Adobe','text-rose-300/80'],['runway','Runway','text-violet-300/80'],['leonardo','Leonardo','text-amber-300/80'],['krea','Krea','text-sky-300/80'],['imagine','Imagine','text-teal-300/80'],['grok','Grok','text-slate-300/80']]"
|
||||
<div v-for="t in [['openai','OpenAI','text-emerald-300/80'],['adobe','Adobe','text-rose-300/80'],['runway','Runway','text-violet-300/80'],['leonardo','Leonardo','text-amber-300/80'],['krea','Krea','text-sky-300/80'],['imagine','Imagine','text-teal-300/80'],['grok','Grok','text-slate-300/80'],['creativefabrica','Creative Fabrica','text-fuchsia-300/80']]"
|
||||
:key="t[0]" class="card p-4">
|
||||
<div class="text-[11px] uppercase tracking-wider" :class="t[2]">{{ t[1] }}</div>
|
||||
<div class="text-2xl font-semibold mt-1 tabular-nums">
|
||||
@@ -373,6 +374,9 @@ onMounted(() => { loadAccounts(); loadModelList() })
|
||||
<button @click="setFilter(() => typeFilter = 'grok')" class="fp" :class="typeFilter === 'grok' && 'fp-on'">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-slate-400"></span>Grok
|
||||
</button>
|
||||
<button @click="setFilter(() => typeFilter = 'creativefabrica')" class="fp" :class="typeFilter === 'creativefabrica' && 'fp-on'">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-fuchsia-400"></span>Creative Fabrica
|
||||
</button>
|
||||
</div>
|
||||
<div class="w-px h-5 bg-white/10"></div>
|
||||
<div class="flex items-center gap-1">
|
||||
@@ -507,7 +511,7 @@ onMounted(() => { loadAccounts(); loadModelList() })
|
||||
<span class="text-white/20">/</span>
|
||||
<span :class="a.video_remaining > 0 ? 'text-emerald-300' : 'text-rose-300'">{{ a.video_remaining }}</span>
|
||||
</span>
|
||||
<span v-else-if="(a.type === 'openai' || a.type === 'adobe' || a.type === 'runway' || a.type === 'leonardo' || a.type === 'krea' || a.type === 'imagine') && a.remaining != null && a.remaining !== -1"
|
||||
<span v-else-if="(a.type === 'openai' || a.type === 'adobe' || a.type === 'runway' || a.type === 'leonardo' || a.type === 'krea' || a.type === 'imagine' || a.type === 'creativefabrica') && a.remaining != null && a.remaining !== -1"
|
||||
class="font-mono font-semibold"
|
||||
:class="a.remaining > 0 ? 'text-emerald-300' : 'text-rose-300'">{{ a.remaining }}</span>
|
||||
<span v-else class="text-white/25" :title="a._quotaError || ''">—</span>
|
||||
|
||||
@@ -135,7 +135,9 @@ const maxAudiosRaw = computed(() => {
|
||||
const n = familyPreset.value?.max_audios
|
||||
return n === undefined || n === null ? null : Number(n)
|
||||
})
|
||||
const isSeedanceModel = computed(() => /^seedance/.test(model.value?.id || ''))
|
||||
// creativefabrica 上游只收图片参考,没有首尾帧/视频/音频,虽叫 seedance 也要排除。
|
||||
const isSeedanceModel = computed(() =>
|
||||
/^seedance/.test(model.value?.id || '') && (model.value?.provider || '') !== 'creativefabrica')
|
||||
// 支持图片以外的参考资产(视频/音频)的模型:seedance 系 + 预设声明了音视频上限的
|
||||
const supportsMediaRefs = computed(() =>
|
||||
isSeedanceModel.value || maxVideosRaw.value > 0 || maxAudiosRaw.value > 0)
|
||||
|
||||
Reference in New Issue
Block a user