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>
This commit is contained in:
@@ -3,12 +3,16 @@ import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { api, jsonBody } from '../api'
|
||||
import { fmtTs, fmtIso, fmtDate, fmtClock } from '../utils/format'
|
||||
import ImportModal from '../components/ImportModal.vue'
|
||||
import UpstreamModal from '../components/UpstreamModal.vue'
|
||||
import Icon from '../components/Icon.vue'
|
||||
|
||||
const rows = ref([])
|
||||
const loading = ref(false)
|
||||
const quotaStatus = ref('')
|
||||
const showImport = ref(false)
|
||||
const showUpstream = ref(false)
|
||||
const editingUpstream = ref(null)
|
||||
function editUpstream(a) { editingUpstream.value = a; showUpstream.value = true }
|
||||
|
||||
const typeFilter = ref('') // '' | 'openai' | 'adobe' | 'runway' | 'leonardo'
|
||||
const statusFilter = ref('') // '' | 'active' | 'quota' | 'disabled'
|
||||
@@ -97,6 +101,13 @@ const pageNumbers = computed(() => {
|
||||
|
||||
let pendingTimer = null
|
||||
|
||||
// Inline-edit an account field (weight / concurrency) → PATCH /tokens/{pool}/{id}.
|
||||
async function saveField(row, field, value) {
|
||||
const v = Number(value)
|
||||
if (Number.isNaN(v)) return
|
||||
await api(`/tokens/${row.pool}/${row.id}`, jsonBody('PATCH', { [field]: v }))
|
||||
}
|
||||
|
||||
async function loadAccounts() {
|
||||
loading.value = true
|
||||
quotaStatus.value = ''
|
||||
@@ -361,10 +372,13 @@ onMounted(loadAccounts)
|
||||
<button @click="showImport = true" class="btn-primary">
|
||||
<Icon name="plus" class="w-3.5 h-3.5" /> 导入账号
|
||||
</button>
|
||||
<button @click="editingUpstream = null; showUpstream = true" class="btn-soft">
|
||||
<Icon name="plus" class="w-3.5 h-3.5" /> 添加上游
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card overflow-hidden">
|
||||
<div class="card overflow-x-auto">
|
||||
<div v-if="loading && !rows.length" class="text-center text-sm text-white/40 py-20">加载中…</div>
|
||||
<div v-else-if="!filtered.length" class="flex flex-col items-center gap-3 text-white/40 py-20">
|
||||
<span class="w-14 h-14 rounded-2xl bg-white/[0.04] grid place-items-center">
|
||||
@@ -374,18 +388,20 @@ onMounted(loadAccounts)
|
||||
<button v-if="!rows.length" @click="showImport = true" class="btn-soft mt-1">导入第一个</button>
|
||||
</div>
|
||||
|
||||
<table v-else class="w-full text-sm table-fixed">
|
||||
<table v-else class="w-full text-sm table-fixed min-w-[1040px]">
|
||||
<colgroup>
|
||||
<col class="w-9" /> <!-- select -->
|
||||
<col /> <!-- identity (flex) -->
|
||||
<col class="w-20" /> <!-- type -->
|
||||
<col class="w-24" /> <!-- remaining -->
|
||||
<col class="w-16" /> <!-- weight -->
|
||||
<col class="w-16" /> <!-- concurrency -->
|
||||
<col class="w-32" /> <!-- reset -->
|
||||
<col class="w-28" /> <!-- created -->
|
||||
<col class="w-28" /> <!-- last used -->
|
||||
<col class="w-40" /> <!-- inflight/success/fail -->
|
||||
<col class="w-16" /> <!-- status switch -->
|
||||
<col class="w-16" /> <!-- actions -->
|
||||
<col class="w-24" /> <!-- actions -->
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr class="text-[10px] uppercase tracking-[0.2em] text-white/40 border-b border-white/[0.06]">
|
||||
@@ -396,6 +412,8 @@ onMounted(loadAccounts)
|
||||
<th class="text-left px-5 py-3 font-medium">账户</th>
|
||||
<th class="text-left px-3 py-3 font-medium">类型</th>
|
||||
<th class="text-right px-3 py-3 font-medium">额度</th>
|
||||
<th class="text-center px-3 py-3 font-medium">权重</th>
|
||||
<th class="text-center px-3 py-3 font-medium">并发</th>
|
||||
<th class="text-left px-3 py-3 font-medium">恢复时间</th>
|
||||
<th class="text-left px-3 py-3 font-medium">创建时间</th>
|
||||
<th class="text-left px-3 py-3 font-medium">最后使用</th>
|
||||
@@ -445,6 +463,20 @@ onMounted(loadAccounts)
|
||||
:class="a.remaining > 0 ? 'text-emerald-300' : 'text-rose-300'">{{ a.remaining }}{{ a.type === 'grok' ? '%' : '' }}</span>
|
||||
<span v-else class="text-white/25" :title="a._quotaError || ''">—</span>
|
||||
</td>
|
||||
<!-- weight (editable, all accounts) -->
|
||||
<td class="px-3 py-3.5 align-middle text-center whitespace-nowrap">
|
||||
<input type="number" :value="a.weight" @change="saveField(a, 'weight', $event.target.value)"
|
||||
class="w-12 bg-white/5 border border-white/10 rounded px-1 py-0.5 text-xs text-center tabular-nums focus:border-indigo-400 outline-none"
|
||||
title="权重(高的优先)" />
|
||||
</td>
|
||||
<!-- concurrency (editable only for custom upstreams; others = system fixed) -->
|
||||
<td class="px-3 py-3.5 align-middle text-center whitespace-nowrap">
|
||||
<input v-if="a.type === 'custom'" type="number" min="1" :value="a.concurrency || 1"
|
||||
@change="saveField(a, 'concurrency', $event.target.value)"
|
||||
class="w-12 bg-white/5 border border-white/10 rounded px-1 py-0.5 text-xs text-center tabular-nums focus:border-indigo-400 outline-none"
|
||||
title="并发数(仅上游可设)" />
|
||||
<span v-else class="text-white/25 text-xs" title="系统固定">{{ a.type === 'grok' ? 10 : 1 }}</span>
|
||||
</td>
|
||||
<!-- reset_after -->
|
||||
<td class="px-3 py-3.5 align-middle text-xs whitespace-nowrap">
|
||||
<div v-if="a.reset_after" class="leading-tight" :title="fmtIso(a.reset_after)">
|
||||
@@ -494,10 +526,15 @@ onMounted(loadAccounts)
|
||||
</button>
|
||||
</td>
|
||||
<!-- actions -->
|
||||
<td class="px-3 py-3.5 align-middle text-right whitespace-nowrap">
|
||||
<button @click="deleteAccount(a.pool, a.id)" class="act danger" title="删除">
|
||||
<Icon name="trash" class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<td class="px-3 py-3.5 align-middle whitespace-nowrap">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button v-if="a.type === 'custom'" @click="editUpstream(a)" class="act" title="编辑上游">
|
||||
<Icon name="config" class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button @click="deleteAccount(a.pool, a.id)" class="act danger" title="删除">
|
||||
<Icon name="trash" class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -520,6 +557,7 @@ onMounted(loadAccounts)
|
||||
</div>
|
||||
|
||||
<ImportModal v-if="showImport" @close="showImport = false" @imported="loadAccounts" />
|
||||
<UpstreamModal v-if="showUpstream" :account="editingUpstream" @close="showUpstream = false; editingUpstream = null" @imported="loadAccounts" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -3,12 +3,14 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { api } from '../api'
|
||||
import Icon from '../components/Icon.vue'
|
||||
import ModelFormModal from '../components/ModelFormModal.vue'
|
||||
import CustomModelModal from '../components/CustomModelModal.vue'
|
||||
import TestModal from '../components/TestModal.vue'
|
||||
import { points } from '../credits'
|
||||
|
||||
const models = ref([])
|
||||
const loading = ref(false)
|
||||
const showForm = ref(false)
|
||||
const showCustom = ref(false)
|
||||
const editing = ref(null) // null = add, object = edit
|
||||
const testing = ref(null) // model being tested, or null
|
||||
|
||||
@@ -120,6 +122,9 @@ onMounted(loadModels)
|
||||
<button @click="openAdd" class="btn-primary">
|
||||
<Icon name="plus" class="w-3.5 h-3.5" /> 新增模型
|
||||
</button>
|
||||
<button @click="showCustom = true" class="btn-soft">
|
||||
<Icon name="plus" class="w-3.5 h-3.5" /> 自定义模型
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
@@ -268,6 +273,7 @@ onMounted(loadModels)
|
||||
</div>
|
||||
|
||||
<ModelFormModal v-if="showForm" :model="editing" @close="showForm = false" @saved="onSaved" />
|
||||
<CustomModelModal v-if="showCustom" @close="showCustom = false" @saved="() => { showCustom = false; loadModels() }" />
|
||||
<TestModal v-if="testing" :model="testing" @close="testing = null" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user