优化违禁词
This commit is contained in:
@@ -130,6 +130,7 @@ func (h *UserGenerationHandler) Test(c *gin.Context) {
|
|||||||
Resolution string `json:"resolution"`
|
Resolution string `json:"resolution"`
|
||||||
Duration string `json:"duration"`
|
Duration string `json:"duration"`
|
||||||
ReferenceImages []string `json:"reference_images"`
|
ReferenceImages []string `json:"reference_images"`
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&body); err != nil {
|
if err := c.ShouldBindJSON(&body); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "invalid request body"})
|
c.JSON(http.StatusBadRequest, gin.H{"detail": "invalid request body"})
|
||||||
@@ -143,6 +144,7 @@ func (h *UserGenerationHandler) Test(c *gin.Context) {
|
|||||||
Resolution: body.Resolution,
|
Resolution: body.Resolution,
|
||||||
Duration: body.Duration,
|
Duration: body.Duration,
|
||||||
ReferenceImages: body.ReferenceImages,
|
ReferenceImages: body.ReferenceImages,
|
||||||
|
AccountID: body.AccountID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ type UserGenerateRequest struct {
|
|||||||
Resolution string
|
Resolution string
|
||||||
Duration string
|
Duration string
|
||||||
ReferenceImages []string
|
ReferenceImages []string
|
||||||
|
// AccountID pins an admin test to one specific provider account (账号生图测试).
|
||||||
|
AccountID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserGenerationService) Generate(ctx context.Context, user *model.User, in UserGenerateRequest) (map[string]any, error) {
|
func (s *UserGenerationService) Generate(ctx context.Context, user *model.User, in UserGenerateRequest) (map[string]any, error) {
|
||||||
@@ -102,6 +104,7 @@ func (s *UserGenerationService) AdminTest(ctx context.Context, user *model.User,
|
|||||||
AspectRatio: in.Ratio,
|
AspectRatio: in.Ratio,
|
||||||
Resolution: in.Resolution,
|
Resolution: in.Resolution,
|
||||||
ReferenceImages: in.ReferenceImages,
|
ReferenceImages: in.ReferenceImages,
|
||||||
|
AccountID: in.AccountID,
|
||||||
})
|
})
|
||||||
default:
|
default:
|
||||||
return s.v1.prepareAdminTestImage(ctx, principal, V1ImageRequest{
|
return s.v1.prepareAdminTestImage(ctx, principal, V1ImageRequest{
|
||||||
@@ -110,6 +113,7 @@ func (s *UserGenerationService) AdminTest(ctx context.Context, user *model.User,
|
|||||||
AspectRatio: in.Ratio,
|
AspectRatio: in.Ratio,
|
||||||
Resolution: in.Resolution,
|
Resolution: in.Resolution,
|
||||||
ReferenceImages: in.ReferenceImages,
|
ReferenceImages: in.ReferenceImages,
|
||||||
|
AccountID: in.AccountID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,6 +203,9 @@ type V1ImageRequest struct {
|
|||||||
// used to build absolute, directly-downloadable output URLs. Empty falls
|
// used to build absolute, directly-downloadable output URLs. Empty falls
|
||||||
// back to a relative "/images/..." path.
|
// back to a relative "/images/..." path.
|
||||||
BaseURL string
|
BaseURL string
|
||||||
|
// AccountID pins the generation to one specific provider account (admin
|
||||||
|
// account-test). Empty keeps the normal pool selection with failover.
|
||||||
|
AccountID string
|
||||||
}
|
}
|
||||||
|
|
||||||
type V1VideoRequest struct {
|
type V1VideoRequest struct {
|
||||||
@@ -214,6 +217,8 @@ type V1VideoRequest struct {
|
|||||||
ReferenceImages []string
|
ReferenceImages []string
|
||||||
// BaseURL — see V1ImageRequest.BaseURL.
|
// BaseURL — see V1ImageRequest.BaseURL.
|
||||||
BaseURL string
|
BaseURL string
|
||||||
|
// AccountID — see V1ImageRequest.AccountID.
|
||||||
|
AccountID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewV1Service(cfg *config.Config, models *repo.ModelRepository, users *repo.UserRepository, events *repo.EventRepository, tokens *repo.TokenRepository, settings *repo.SiteSettingRepository, cgroups *repo.ConcurrencyGroupRepository, conc *ConcurrencyService, adobeClient *adobe.Client, chatGPTClient *chatgpt.Client, runwayClient *runway.Client, leonardoClient *leonardo.Client, kreaClient *krea.Client, imagineClient *imagine.Client, grokClient *grok.Client, customClient *custom.Client, store *storage.Client) *V1Service {
|
func NewV1Service(cfg *config.Config, models *repo.ModelRepository, users *repo.UserRepository, events *repo.EventRepository, tokens *repo.TokenRepository, settings *repo.SiteSettingRepository, cgroups *repo.ConcurrencyGroupRepository, conc *ConcurrencyService, adobeClient *adobe.Client, chatGPTClient *chatgpt.Client, runwayClient *runway.Client, leonardoClient *leonardo.Client, kreaClient *krea.Client, imagineClient *imagine.Client, grokClient *grok.Client, customClient *custom.Client, store *storage.Client) *V1Service {
|
||||||
@@ -1547,6 +1552,7 @@ func (s *V1Service) generateAdobeImage(ctx context.Context, eventID string, mode
|
|||||||
active = append(active, item)
|
active = append(active, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
active = pinTestAccount(items, active, in.AccountID)
|
||||||
if len(active) == 0 {
|
if len(active) == 0 {
|
||||||
return nil, ErrNoProviderAccount
|
return nil, ErrNoProviderAccount
|
||||||
}
|
}
|
||||||
@@ -1602,6 +1608,7 @@ func (s *V1Service) generateAdobeVideo(ctx context.Context, eventID string, mode
|
|||||||
active = append(active, item)
|
active = append(active, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
active = pinTestAccount(items, active, in.AccountID)
|
||||||
if len(active) == 0 {
|
if len(active) == 0 {
|
||||||
return nil, "", ErrNoProviderAccount
|
return nil, "", ErrNoProviderAccount
|
||||||
}
|
}
|
||||||
@@ -1686,6 +1693,7 @@ func (s *V1Service) generateRunwayVideo(ctx context.Context, eventID string, mod
|
|||||||
}
|
}
|
||||||
active = append(active, item)
|
active = append(active, item)
|
||||||
}
|
}
|
||||||
|
active = pinTestAccount(items, active, in.AccountID)
|
||||||
if len(active) == 0 {
|
if len(active) == 0 {
|
||||||
return nil, "", ErrNoProviderAccount
|
return nil, "", ErrNoProviderAccount
|
||||||
}
|
}
|
||||||
@@ -1827,6 +1835,7 @@ func (s *V1Service) generateCustomImage(ctx context.Context, eventID string, mod
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
active = pinTestAccount(active, active, in.AccountID)
|
||||||
if len(active) == 0 {
|
if len(active) == 0 {
|
||||||
return nil, ErrNoProviderAccount
|
return nil, ErrNoProviderAccount
|
||||||
}
|
}
|
||||||
@@ -1894,6 +1903,7 @@ func (s *V1Service) generateCustomVideo(ctx context.Context, eventID string, mod
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
active = pinTestAccount(active, active, in.AccountID)
|
||||||
if len(active) == 0 {
|
if len(active) == 0 {
|
||||||
return nil, "", ErrNoProviderAccount
|
return nil, "", ErrNoProviderAccount
|
||||||
}
|
}
|
||||||
@@ -2058,6 +2068,7 @@ func (s *V1Service) generateGrokVideo(ctx context.Context, eventID string, model
|
|||||||
}
|
}
|
||||||
active = append(active, item)
|
active = append(active, item)
|
||||||
}
|
}
|
||||||
|
active = pinTestAccount(items, active, in.AccountID)
|
||||||
if len(active) == 0 {
|
if len(active) == 0 {
|
||||||
return nil, "", ErrNoProviderAccount
|
return nil, "", ErrNoProviderAccount
|
||||||
}
|
}
|
||||||
@@ -2161,6 +2172,7 @@ func (s *V1Service) generateRunwayImage(ctx context.Context, eventID string, mod
|
|||||||
}
|
}
|
||||||
active = append(active, item)
|
active = append(active, item)
|
||||||
}
|
}
|
||||||
|
active = pinTestAccount(items, active, in.AccountID)
|
||||||
if len(active) == 0 {
|
if len(active) == 0 {
|
||||||
return nil, ErrNoProviderAccount
|
return nil, ErrNoProviderAccount
|
||||||
}
|
}
|
||||||
@@ -2281,6 +2293,7 @@ func (s *V1Service) generateChatGPTImage(ctx context.Context, eventID string, mo
|
|||||||
active = append(active, item)
|
active = append(active, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
active = pinTestAccount(items, active, in.AccountID)
|
||||||
if len(active) == 0 {
|
if len(active) == 0 {
|
||||||
return nil, ErrNoProviderAccount
|
return nil, ErrNoProviderAccount
|
||||||
}
|
}
|
||||||
@@ -2387,6 +2400,7 @@ func (s *V1Service) generateLeonardoImage(ctx context.Context, eventID string, m
|
|||||||
}
|
}
|
||||||
active = append(active, item)
|
active = append(active, item)
|
||||||
}
|
}
|
||||||
|
active = pinTestAccount(items, active, in.AccountID)
|
||||||
if len(active) == 0 {
|
if len(active) == 0 {
|
||||||
return nil, ErrNoProviderAccount
|
return nil, ErrNoProviderAccount
|
||||||
}
|
}
|
||||||
@@ -2538,6 +2552,7 @@ func (s *V1Service) generateKreaImage(ctx context.Context, eventID string, model
|
|||||||
active = append(active, item)
|
active = append(active, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
active = pinTestAccount(items, active, in.AccountID)
|
||||||
if len(active) == 0 {
|
if len(active) == 0 {
|
||||||
return nil, ErrNoProviderAccount
|
return nil, ErrNoProviderAccount
|
||||||
}
|
}
|
||||||
@@ -2614,6 +2629,7 @@ func (s *V1Service) generateImagineImage(ctx context.Context, eventID string, mo
|
|||||||
active = append(active, item)
|
active = append(active, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
active = pinTestAccount(items, active, in.AccountID)
|
||||||
if len(active) == 0 {
|
if len(active) == 0 {
|
||||||
return nil, ErrNoProviderAccount
|
return nil, ErrNoProviderAccount
|
||||||
}
|
}
|
||||||
@@ -3110,6 +3126,23 @@ func (s *V1Service) nextCursor(pool string) uint64 {
|
|||||||
// cycles in fixed order regardless of fails or last_used. The fall-through
|
// cycles in fixed order regardless of fails or last_used. The fall-through
|
||||||
// retry chain is preserved — on failure the caller's loop simply continues to
|
// retry chain is preserved — on failure the caller's loop simply continues to
|
||||||
// the next account in rotation order.
|
// the next account in rotation order.
|
||||||
|
// pinTestAccount narrows account selection to the single account requested by
|
||||||
|
// an admin 账号生图测试. The pinned account is taken from the pool's full list
|
||||||
|
// (bypassing active/dead/limited filters) so a limited or disabled account can
|
||||||
|
// still be probed. Returns nil when the account isn't in this pool.
|
||||||
|
func pinTestAccount(items, active []model.TokenAccount, accountID string) []model.TokenAccount {
|
||||||
|
id := strings.TrimSpace(accountID)
|
||||||
|
if id == "" {
|
||||||
|
return active
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
if item.ID == id && strings.TrimSpace(item.Value) != "" {
|
||||||
|
return []model.TokenAccount{item}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *V1Service) rotateRoundRobin(pool string, items []model.TokenAccount) {
|
func (s *V1Service) rotateRoundRobin(pool string, items []model.TokenAccount) {
|
||||||
if len(items) <= 1 {
|
if len(items) <= 1 {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, onUnmounted } from 'vue'
|
||||||
|
import { api, jsonBody } from '../api'
|
||||||
|
import Icon from './Icon.vue'
|
||||||
|
import SelectMenu from './SelectMenu.vue'
|
||||||
|
|
||||||
|
// 账号生成测试:固定使用某个 provider 账号跑一次生成(图像或视频)。
|
||||||
|
// 模型列表由 AccountsView 打开页面时预加载好传入,弹窗即开即用。
|
||||||
|
const props = defineProps({
|
||||||
|
account: { type: Object, required: true }, // { pool, id, email, type }
|
||||||
|
allModels: { type: Array, default: () => [] },
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
|
// 该账号所属 provider 的全部模型(图像 + 视频)。
|
||||||
|
const models = computed(() =>
|
||||||
|
props.allModels.filter((m) => (m.provider || '') === props.account.pool))
|
||||||
|
const selectedModel = ref('')
|
||||||
|
if (models.value.length) selectedModel.value = models.value[0].alias || models.value[0].id
|
||||||
|
|
||||||
|
const modelOptions = computed(() => models.value.map((m) => ({
|
||||||
|
value: m.alias || m.id,
|
||||||
|
label: `${m.alias || m.id}${m.type === 'video' ? ' · 视频' : ''}`,
|
||||||
|
})))
|
||||||
|
const currentModel = computed(() =>
|
||||||
|
models.value.find((m) => (m.alias || m.id) === selectedModel.value) || null)
|
||||||
|
const isVideo = computed(() => currentModel.value?.type === 'video')
|
||||||
|
|
||||||
|
const prompt = ref('a cute cat sitting on a desk, studio lighting')
|
||||||
|
|
||||||
|
const busy = ref(false)
|
||||||
|
const status = ref('')
|
||||||
|
const error = ref('')
|
||||||
|
const resultUrl = ref('')
|
||||||
|
const resultKind = ref('')
|
||||||
|
|
||||||
|
// Gateway-timeout recovery — same policy as TestModal: the backend detaches the
|
||||||
|
// render from the request, so on a 524/504 we keep polling /jobs/mine?source=admin.
|
||||||
|
const GATEWAY_TIMEOUT = new Set([0, 408, 504, 520, 521, 522, 523, 524, 525])
|
||||||
|
let recoverTimer = null
|
||||||
|
let recoverJobId = ''
|
||||||
|
let recoverSubmitTs = 0
|
||||||
|
onUnmounted(() => clearTimeout(recoverTimer))
|
||||||
|
|
||||||
|
function firstDuration(m) {
|
||||||
|
const keys = Object.keys(m?.duration_prices || {})
|
||||||
|
.sort((a, b) => parseFloat(a) - parseFloat(b))
|
||||||
|
return keys[0] || '5s'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
if (!selectedModel.value) { error.value = '请选择模型'; return }
|
||||||
|
if (!prompt.value.trim()) { error.value = '请输入指令'; return }
|
||||||
|
busy.value = true
|
||||||
|
error.value = ''
|
||||||
|
status.value = isVideo.value ? '正在生成视频 (约 1–3 分钟)…' : '正在生成…'
|
||||||
|
resultUrl.value = ''
|
||||||
|
resultKind.value = ''
|
||||||
|
const m = currentModel.value
|
||||||
|
const payload = {
|
||||||
|
model: selectedModel.value,
|
||||||
|
prompt: prompt.value,
|
||||||
|
ratio: (m?.ratios && m.ratios[0]) || (isVideo.value ? '16x9' : '1:1'),
|
||||||
|
resolution: (m?.resolutions && m.resolutions[0]) || '',
|
||||||
|
account_id: props.account.id,
|
||||||
|
}
|
||||||
|
if (isVideo.value) payload.duration = firstDuration(m)
|
||||||
|
recoverSubmitTs = Date.now()
|
||||||
|
const r = await api('/test', jsonBody('POST', payload))
|
||||||
|
if (r.ok && r.data?.url) {
|
||||||
|
busy.value = false
|
||||||
|
resultUrl.value = r.data.url
|
||||||
|
resultKind.value = r.data.kind || (isVideo.value ? 'video' : 'image')
|
||||||
|
status.value = `完成 · ${r.data.provider} · ${(r.data.elapsed_ms / 1000).toFixed(1)}s`
|
||||||
|
} else if (GATEWAY_TIMEOUT.has(r.status)) {
|
||||||
|
status.value = isVideo.value ? '生成视频中 (约 1–3 分钟)…' : '生成中…'
|
||||||
|
recoverJobId = ''
|
||||||
|
clearTimeout(recoverTimer)
|
||||||
|
recoverTimer = setTimeout(recover, 3000)
|
||||||
|
} else {
|
||||||
|
busy.value = false
|
||||||
|
status.value = ''
|
||||||
|
error.value = r.data?.detail || `失败 (${r.status})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recover() {
|
||||||
|
const r = await api('/jobs/mine?source=admin')
|
||||||
|
if (!r.ok) { recoverTimer = setTimeout(recover, 3000); return }
|
||||||
|
const { pending, latest } = r.data || {}
|
||||||
|
if (pending) {
|
||||||
|
recoverJobId = pending.id
|
||||||
|
recoverTimer = setTimeout(recover, 3000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const mine = latest && (
|
||||||
|
(recoverJobId && latest.id === recoverJobId) ||
|
||||||
|
(!recoverJobId && latest.ts && latest.ts * 1000 >= recoverSubmitTs - 2000)
|
||||||
|
)
|
||||||
|
if (mine && latest.status === 'success' && latest.url) {
|
||||||
|
busy.value = false
|
||||||
|
resultUrl.value = latest.url
|
||||||
|
resultKind.value = latest.kind || (isVideo.value ? 'video' : 'image')
|
||||||
|
status.value = `完成 · ${(latest.elapsed_ms / 1000).toFixed(1)}s`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (mine && latest.status === 'failed') {
|
||||||
|
busy.value = false
|
||||||
|
status.value = ''
|
||||||
|
error.value = latest.error || '生成失败'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
recoverTimer = setTimeout(recover, 3000)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm flex items-start justify-center overflow-y-auto p-4"
|
||||||
|
@click.self="emit('close')">
|
||||||
|
<div class="card !shadow-2xl my-12 w-full max-w-lg">
|
||||||
|
<div class="px-5 py-4 border-b border-white/[0.06] flex items-center justify-between">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<h2 class="text-sm font-semibold">账号生成测试</h2>
|
||||||
|
<div class="text-xs text-white/45 font-mono truncate">{{ account.email || account.id }} · {{ account.pool }}</div>
|
||||||
|
</div>
|
||||||
|
<button @click="emit('close')" class="text-white/40 hover:text-white transition-colors">
|
||||||
|
<Icon name="close" class="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-5 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="lbl">模型</label>
|
||||||
|
<div v-if="!models.length" class="text-xs text-amber-300">该账号所属 provider ({{ account.pool }}) 没有可用的模型</div>
|
||||||
|
<SelectMenu v-else v-model="selectedModel" :options="modelOptions" placeholder="选择模型" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="lbl">指令</label>
|
||||||
|
<textarea v-model="prompt" rows="3" class="field resize-none" placeholder="输入测试指令…"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button @click="run" :disabled="busy || !models.length" class="btn-primary w-full">
|
||||||
|
<Icon name="spark" class="w-4 h-4" /> {{ busy ? (isVideo ? '生成中…(请耐心等待)' : '生成中…') : '生成' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<p v-if="status" class="text-xs text-white/55">{{ status }}</p>
|
||||||
|
<p v-if="error" class="text-xs text-rose-300 break-all">{{ error }}</p>
|
||||||
|
|
||||||
|
<div v-if="resultUrl" class="rounded-xl ring-1 ring-white/10 bg-white/[0.03] overflow-hidden grid place-items-center min-h-[220px]">
|
||||||
|
<video v-if="resultKind === 'video'" :src="resultUrl" controls autoplay
|
||||||
|
class="max-w-full max-h-[420px] object-contain" />
|
||||||
|
<img v-else :src="resultUrl" class="max-w-full max-h-[360px] object-contain" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.lbl {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgb(255 255 255 / 0.55);
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,6 +5,7 @@ import { fmtTs, fmtIso, fmtDate, fmtClock } from '../utils/format'
|
|||||||
import ImportModal from '../components/ImportModal.vue'
|
import ImportModal from '../components/ImportModal.vue'
|
||||||
import UpstreamModal from '../components/UpstreamModal.vue'
|
import UpstreamModal from '../components/UpstreamModal.vue'
|
||||||
import AccountEditModal from '../components/AccountEditModal.vue'
|
import AccountEditModal from '../components/AccountEditModal.vue'
|
||||||
|
import AccountTestModal from '../components/AccountTestModal.vue'
|
||||||
import Icon from '../components/Icon.vue'
|
import Icon from '../components/Icon.vue'
|
||||||
|
|
||||||
const rows = ref([])
|
const rows = ref([])
|
||||||
@@ -16,6 +17,14 @@ const editingUpstream = ref(null)
|
|||||||
function editUpstream(a) { editingUpstream.value = a; showUpstream.value = true }
|
function editUpstream(a) { editingUpstream.value = a; showUpstream.value = true }
|
||||||
const editingAccount = ref(null)
|
const editingAccount = ref(null)
|
||||||
function editAccount(a) { editingAccount.value = a }
|
function editAccount(a) { editingAccount.value = a }
|
||||||
|
const testingAccount = ref(null)
|
||||||
|
function testAccount(a) { testingAccount.value = a }
|
||||||
|
// 预加载模型列表,让「生图测试」弹窗即开即用(不显示加载中)。
|
||||||
|
const allModels = ref([])
|
||||||
|
async function loadModelList() {
|
||||||
|
const r = await api('/managed-models')
|
||||||
|
allModels.value = r.data?.data || []
|
||||||
|
}
|
||||||
// Reflect the saved values in the table without a full reload.
|
// Reflect the saved values in the table without a full reload.
|
||||||
function applyEdit(payload) {
|
function applyEdit(payload) {
|
||||||
const row = editingAccount.value
|
const row = editingAccount.value
|
||||||
@@ -299,7 +308,7 @@ async function deleteSelected() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadAccounts)
|
onMounted(() => { loadAccounts(); loadModelList() })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -391,7 +400,7 @@ onMounted(loadAccounts)
|
|||||||
<button v-if="!rows.length" @click="showImport = true" class="btn-soft mt-1">导入第一个</button>
|
<button v-if="!rows.length" @click="showImport = true" class="btn-soft mt-1">导入第一个</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table v-else class="w-full text-sm table-fixed min-w-[1040px]">
|
<table v-else class="w-full text-sm table-fixed min-w-[1080px]">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col class="w-9" /> <!-- select -->
|
<col class="w-9" /> <!-- select -->
|
||||||
<col /> <!-- identity (flex) -->
|
<col /> <!-- identity (flex) -->
|
||||||
@@ -404,7 +413,7 @@ onMounted(loadAccounts)
|
|||||||
<col class="w-28" /> <!-- last used -->
|
<col class="w-28" /> <!-- last used -->
|
||||||
<col class="w-40" /> <!-- inflight/success/fail -->
|
<col class="w-40" /> <!-- inflight/success/fail -->
|
||||||
<col class="w-16" /> <!-- status switch -->
|
<col class="w-16" /> <!-- status switch -->
|
||||||
<col class="w-24" /> <!-- actions -->
|
<col class="w-32" /> <!-- actions -->
|
||||||
</colgroup>
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="text-[10px] uppercase tracking-[0.2em] text-white/40 border-b border-white/[0.06]">
|
<tr class="text-[10px] uppercase tracking-[0.2em] text-white/40 border-b border-white/[0.06]">
|
||||||
@@ -523,6 +532,9 @@ onMounted(loadAccounts)
|
|||||||
<!-- actions -->
|
<!-- actions -->
|
||||||
<td class="px-3 py-3.5 align-middle whitespace-nowrap">
|
<td class="px-3 py-3.5 align-middle whitespace-nowrap">
|
||||||
<div class="flex items-center justify-end gap-2">
|
<div class="flex items-center justify-end gap-2">
|
||||||
|
<button @click="testAccount(a)" class="act" title="生图测试">
|
||||||
|
<Icon name="spark" class="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
<button v-if="a.type !== 'custom'" @click="editAccount(a)" class="act" title="编辑">
|
<button v-if="a.type !== 'custom'" @click="editAccount(a)" class="act" title="编辑">
|
||||||
<Icon name="config" class="w-3.5 h-3.5" />
|
<Icon name="config" class="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
@@ -557,6 +569,7 @@ onMounted(loadAccounts)
|
|||||||
<ImportModal v-if="showImport" @close="showImport = false" @imported="loadAccounts" />
|
<ImportModal v-if="showImport" @close="showImport = false" @imported="loadAccounts" />
|
||||||
<UpstreamModal v-if="showUpstream" :account="editingUpstream" @close="showUpstream = false; editingUpstream = null" @imported="loadAccounts" />
|
<UpstreamModal v-if="showUpstream" :account="editingUpstream" @close="showUpstream = false; editingUpstream = null" @imported="loadAccounts" />
|
||||||
<AccountEditModal v-if="editingAccount" :account="editingAccount" @saved="applyEdit" @close="editingAccount = null" />
|
<AccountEditModal v-if="editingAccount" :account="editingAccount" @saved="applyEdit" @close="editingAccount = null" />
|
||||||
|
<AccountTestModal v-if="testingAccount" :account="testingAccount" :all-models="allModels" @close="testingAccount = null" />
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user