Initial commit: clean project structure

- Backend: FastAPI + SQLAlchemy + Celery (Python 3.11+)
- Frontend: Vue 3 + TypeScript + Pinia + Tailwind
- Admin Frontend: separate Vue 3 app for management
- Docker Compose: 9 services orchestration
- Specs: design prototypes, memory system PRD, product roadmap

Cleanup performed:
- Removed temporary debug scripts from backend root
- Removed deprecated admin_app.py (embedded UI)
- Removed duplicate docs from admin-frontend
- Updated .gitignore for Vite cache and egg-info
This commit is contained in:
zhangtuo
2026-01-20 18:20:03 +08:00
commit e9d7f8832a
241 changed files with 33070 additions and 0 deletions

View File

@@ -0,0 +1,475 @@
<template>
<main class="max-w-7xl mx-auto px-4 py-8">
<BaseCard v-if="!isLoggedIn" class="max-w-md mx-auto mt-20" padding="lg">
<h1 class="text-3xl font-bold gradient-text mb-4 text-center">DreamWeaver 控制台</h1>
<p class="text-sm text-gray-500 mb-8 text-center">请登录以管理 AI 计算引擎与策略</p>
<form @submit.prevent="login" class="space-y-6">
<BaseInput v-model="loginForm.username" label="管理员账号" required placeholder="admin" />
<BaseInput v-model="loginForm.password" label="密钥密码" type="password" required />
<div class="flex items-center justify-between mt-6">
<span v-if="loginError" class="text-sm text-red-500 font-medium">{{ loginError }}</span>
<BaseButton type="submit" class="w-full">进入控制台</BaseButton>
</div>
</form>
</BaseCard>
<div v-else class="space-y-8">
<!-- Header -->
<header class="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
<div>
<h1 class="text-3xl font-bold gradient-text">引擎调度中心</h1>
<p class="text-sm text-gray-500 mt-1">Provider Orchestration & Strategy</p>
</div>
<div class="flex items-center gap-3">
<div class="bg-blue-50 text-blue-700 px-3 py-1 rounded-full text-xs font-medium flex items-center gap-1">
<span class="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></span>
系统运行中
</div>
<BaseButton variant="ghost" @click="logout" size="sm">退出</BaseButton>
</div>
</header>
<!-- Main Content Grid -->
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
<!-- Left: Status & Defaults -->
<div class="lg:col-span-1 space-y-6">
<BaseCard padding="md" title="出厂默认策略 (.env)">
<div class="space-y-4">
<div v-for="(providers, type) in defaults" :key="type" class="p-3 bg-gray-50 rounded-lg border border-gray-100">
<div class="flex items-center gap-2 mb-2">
<span class="text-xs font-bold uppercase tracking-wider text-gray-400">{{ type }}</span>
</div>
<div class="flex flex-wrap gap-2">
<button
v-for="p in providers"
:key="p"
@click="cloneDefault(type, p)"
class="px-2 py-1 text-xs bg-white border border-gray-200 rounded text-gray-600 font-mono hover:border-indigo-300 hover:text-indigo-600 hover:shadow-sm transition-all cursor-pointer"
title="点击基于此默认配置创建"
>
{{ p }}
</button>
</div>
</div>
<p class="text-xs text-gray-400 mt-2 px-1">
* 当数据库中未配置或全部禁用时系统将回退到上述出厂设置
</p>
</div>
</BaseCard>
<BaseCard padding="md" title="可用驱动 (Adapters)">
<div class="flex flex-wrap gap-2">
<span v-for="adapter in availableAdapters" :key="adapter"
class="px-2 py-1 text-xs bg-indigo-50 text-indigo-700 rounded-full border border-indigo-100">
{{ adapter.split(':')[1] }} <span class="opacity-50 text-[10px]">({{ adapter.split(':')[0] }})</span>
</span>
</div>
</BaseCard>
</div>
<!-- Right: Active Manager -->
<div class="lg:col-span-3 space-y-6">
<!-- Tabs -->
<div class="flex space-x-1 bg-gray-100 p-1 rounded-xl w-fit">
<button
v-for="tab in ['text', 'image', 'tts', 'storybook']"
:key="tab"
@click="activeTab = tab"
class="px-6 py-2 rounded-lg text-sm font-medium transition-all duration-200"
:class="activeTab === tab ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-500 hover:text-gray-700'"
>
{{ tab.toUpperCase() }}
</button>
</div>
<!-- Provider Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Add New Card -->
<button @click="openCreateModal" class="flex flex-col items-center justify-center p-8 border-2 border-dashed border-gray-200 rounded-2xl hover:border-indigo-300 hover:bg-indigo-50 transition-all group min-h-[200px]">
<div class="w-12 h-12 rounded-full bg-white shadow-sm flex items-center justify-center mb-3 group-hover:scale-110 transition-transform">
<svg class="w-6 h-6 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
</div>
<span class="text-sm font-medium text-gray-600 group-hover:text-indigo-600">添加新的 {{ activeTab }} 引擎</span>
</button>
<!-- Existing Cards -->
<div v-for="p in filteredProviders" :key="p.id"
class="relative p-6 bg-white rounded-2xl border transition-all duration-200 group hover:shadow-lg"
:class="p.enabled ? 'border-gray-200' : 'border-gray-100 opacity-75 bg-gray-50'"
>
<!-- Enabled Toggle -->
<div class="absolute top-4 right-4 z-10">
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" :checked="p.enabled" @change="toggleEnabled(p)" class="sr-only peer">
<div class="w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-indigo-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-indigo-600"></div>
</label>
</div>
<div class="flex items-start justify-between mb-4">
<div>
<h3 class="font-bold text-lg text-gray-900">{{ p.name }}</h3>
<div class="flex items-center gap-2 mt-1">
<span class="px-2 py-0.5 rounded text-xs font-mono bg-gray-100 text-gray-600 border border-gray-200">
{{ p.adapter }}
</span>
<span v-if="p.model" class="px-2 py-0.5 rounded text-xs bg-blue-50 text-blue-600 border border-blue-100 truncate max-w-[120px]">
{{ p.model }}
</span>
</div>
</div>
</div>
<div class="space-y-2 text-sm text-gray-500 mb-6">
<div class="flex justify-between">
<span>Priority:</span>
<span class="font-medium text-gray-700">{{ p.priority }}</span>
</div>
<div class="flex justify-between">
<span>API Key:</span>
<span :class="p.has_api_key ? 'text-green-600' : 'text-yellow-600'">
{{ p.has_api_key ? '● Configured' : '○ Not Set (Env?)' }}
</span>
</div>
</div>
<div class="flex items-center gap-2 pt-4 border-t border-gray-100 opacity-0 group-hover:opacity-100 transition-opacity">
<BaseButton size="sm" variant="secondary" class="flex-1" @click="edit(p)">配置</BaseButton>
<BaseButton size="sm" variant="ghost" class="text-red-500 hover:bg-red-50" @click="remove(p)">删除</BaseButton>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Edit/Create Modal -->
<div v-if="editing" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm" @click.self="reset">
<BaseCard class="w-full max-w-2xl max-h-[90vh] overflow-y-auto" padding="lg" @click.stop>
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold gradient-text">{{ form.id ? '编辑引擎配置' : '添加新引擎' }}</h2>
<button @click="reset" class="text-gray-400 hover:text-gray-600">
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<form @submit.prevent="submit" class="grid grid-cols-1 md:grid-cols-2 gap-6">
<BaseInput v-model="form.name" label="名称 (显示名)" placeholder="如: Pro GPT-4" required />
<BaseSelect
v-model="form.adapter"
label="驱动程序 (Adapter)"
:options="adapterOptions"
required
description="选择底层的 API 驱动协议"
/>
<BaseInput v-model="form.model" label="模型名称 (Model)" placeholder="如: gpt-4o, minimax-v2" description="具体调用的模型ID" />
<BaseInput v-model.number="form.priority" label="优先级 (0-100)" type="number" description="数字越大越优先" />
<div class="md:col-span-2 p-4 bg-gray-50 rounded-xl border border-gray-100 space-y-4">
<h3 class="text-sm font-bold text-gray-700">密钥与连接</h3>
<BaseInput v-model="form.api_key" label="API Key" type="password" placeholder="留空则使用 .env 配置" :required="!form.id && !form.config_ref" />
<BaseInput v-model="form.api_base" label="API Endpoint / Group ID" placeholder="https://... 或 Group ID" />
<BaseInput v-model="form.config_ref" label="Fallback Env Var" placeholder="如: OPENAI_API_KEY (高级)" />
</div>
<!-- MiniMax Specific Config -->
<div v-if="form.adapter === 'minimax'" class="md:col-span-2 p-4 bg-indigo-50 rounded-xl border border-indigo-100 space-y-4">
<h3 class="text-sm font-bold text-indigo-700">MiniMax 语音参数</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<BaseInput v-model="form.config_json.voice_id" label="音色 ID (Voice ID)" placeholder="male-qn-qingse" />
<BaseInput v-model="form.config_json.emotion" label="情感 (Emotion)" placeholder="happy / sad / angry" />
<div class="space-y-2">
<div class="flex justify-between">
<label class="text-xs font-semibold text-gray-600">语速 (Speed)</label>
<span class="text-xs text-indigo-600 font-mono">{{ form.config_json.speed || 1.0 }}x</span>
</div>
<input type="range" v-model.number="form.config_json.speed" min="0.5" max="2.0" step="0.1" class="w-full accent-indigo-600" />
</div>
<div class="space-y-2">
<div class="flex justify-between">
<label class="text-xs font-semibold text-gray-600">音量 (Volume)</label>
<span class="text-xs text-indigo-600 font-mono">{{ form.config_json.vol || 1.0 }}</span>
</div>
<input type="range" v-model.number="form.config_json.vol" min="0.1" max="5.0" step="0.1" class="w-full accent-indigo-600" />
</div>
<div class="space-y-2">
<div class="flex justify-between">
<label class="text-xs font-semibold text-gray-600">音高 (Pitch)</label>
<span class="text-xs text-indigo-600 font-mono">{{ form.config_json.pitch || 0 }}</span>
</div>
<input type="range" v-model.number="form.config_json.pitch" min="-12" max="12" step="1" class="w-full accent-indigo-600" />
</div>
</div>
</div>
<!-- CQTAI Specific Config -->
<div v-if="form.adapter === 'cqtai'" class="md:col-span-2 p-4 bg-purple-50 rounded-xl border border-purple-100 space-y-4">
<h3 class="text-sm font-bold text-purple-700">CQTAI 画图参数</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<BaseSelect
v-model="form.config_json.aspect_ratio"
label="画面比例 (Aspect Ratio)"
:options="[
{ value: '1:1', label: '1:1 (正方形)' },
{ value: '16:9', label: '16:9 (横屏)' },
{ value: '9:16', label: '9:16 (竖屏)' },
{ value: '4:3', label: '4:3 (传统)' },
{ value: '3:4', label: '3:4 (海报)' }
]"
placeholder="默认 1:1"
/>
<BaseSelect
v-model="form.config_json.resolution"
label="分辨率 (Resolution)"
:options="[
{ value: '1K', label: '1K (标准)' },
{ value: '2K', label: '2K (高清)' },
{ value: '4K', label: '4K (超清)' }
]"
placeholder="默认 1K"
/>
</div>
</div>
<!-- Antigravity Specific Config -->
<div v-if="form.adapter === 'antigravity'" class="md:col-span-2 p-4 bg-emerald-50 rounded-xl border border-emerald-100 space-y-4">
<h3 class="text-sm font-bold text-emerald-700">Antigravity 画图参数 (OpenAI Compatible)</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<BaseSelect
v-model="form.config_json.size"
label="图像尺寸 (Size)"
:options="[
{ value: '1024x1024', label: '1024x1024 (1:1 正方形)' },
{ value: '1280x720', label: '1280x720 (16:9 横屏)' },
{ value: '720x1280', label: '720x1280 (9:16 竖屏)' },
{ value: '1216x896', label: '1216x896 (4:3 传统)' }
]"
placeholder="默认 1024x1024"
/>
<div class="flex items-center">
<p class="text-xs text-emerald-600">
💡 使用 Gemini 3 Pro Image 模型通过 OpenAI 兼容接口生成图像
</p>
</div>
</div>
</div>
<div class="md:col-span-2 flex justify-end gap-3 pt-4 border-t border-gray-100">
<BaseButton variant="secondary" type="button" @click="reset">取消</BaseButton>
<BaseButton type="submit">{{ form.id ? '保存变更' : '立即创建' }}</BaseButton>
</div>
</form>
</BaseCard>
</div>
</main>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import BaseButton from '../components/ui/BaseButton.vue'
import BaseCard from '../components/ui/BaseCard.vue'
import BaseInput from '../components/ui/BaseInput.vue'
import BaseSelect from '../components/ui/BaseSelect.vue'
// Types
type Provider = {
id: string
name: string
type: string
adapter: string
model?: string
api_base?: string
has_api_key?: boolean
priority: number
enabled: boolean
config_ref?: string
weight?: number
timeout_ms?: number
max_retries?: number
config_json: Record<string, any>
}
// State
// State
const loginForm = ref({ username: '', password: '' })
const loginError = ref('')
const isLoggedIn = ref(!!sessionStorage.getItem('admin_auth'))
const activeTab = ref('text')
const providers = ref<Provider[]>([])
const defaults = ref<Record<string, string[]>>({})
const availableAdapters = ref<string[]>([])
const editing = ref(false)
const form = ref<Partial<Provider> & { api_key?: string; config_json: Record<string, any> }>({
type: 'text',
priority: 10,
enabled: true,
config_json: {}
})
const apiBase = import.meta.env.VITE_API_BASE || ''
function getAuthHeader(): string {
return sessionStorage.getItem('admin_auth') || ''
}
// Actions
async function login() {
loginError.value = ''
const auth = 'Basic ' + btoa(loginForm.value.username + ':' + loginForm.value.password)
try {
const res = await fetch(`${apiBase}/admin/providers`, {
headers: { Authorization: auth },
})
if (res.ok) {
sessionStorage.setItem('admin_auth', auth)
isLoggedIn.value = true
// 触发 NavBar 更新(如果在同一页面)- 实际上我们需要强制刷新或使用事件总线,但最简单的是 reload
// 不过 AdminProviders 是视图组件NavBar 是布局组件,它们状态不共享是个问题
// 鉴于 NavBar 也依赖 sessionStorage 但不响应,这里的页面刷新是必要的
window.location.reload()
} else {
loginError.value = '鉴权失败'
}
} catch {
loginError.value = '网络不可达'
}
}
function logout() {
sessionStorage.removeItem('admin_auth')
isLoggedIn.value = false
providers.value = []
window.location.reload()
}
async function loadData() {
if (!isLoggedIn.value) return
const headers = { Authorization: getAuthHeader() }
// Parallel fetch
const [pRes, dRes, aRes] = await Promise.all([
fetch(`${apiBase}/admin/providers`, { headers }),
fetch(`${apiBase}/admin/providers/defaults`, { headers }),
fetch(`${apiBase}/admin/providers/adapters`, { headers })
])
if (pRes.ok) providers.value = await pRes.json()
if (dRes.ok) defaults.value = await dRes.json()
if (aRes.ok) availableAdapters.value = await aRes.json()
}
// Computed
const filteredProviders = computed(() => {
return providers.value
.filter(p => p.type === activeTab.value)
.sort((a, b) => b.priority - a.priority)
})
const adapterOptions = computed(() => {
return availableAdapters.value
.filter(a => a.startsWith(activeTab.value + ':'))
.map(a => {
const name = a.split(':')[1]
return { value: name, label: name } // e.g. 'gemini', 'openai'
})
})
// UI Actions
function cloneDefault(type: string, name: string) {
// 根据默认名称推断配置
// 大多数默认 provider 的 name 就是 adapter name (如 gemini, openai, cqtai)
// 如果未来有别名,这里可以做映射
activeTab.value = type // 切换到对应 tab
form.value = {
type: type,
name: name,
adapter: name, // Default assumption
priority: 10,
enabled: true,
weight: 1,
timeout_ms: 60000,
max_retries: 1,
config_json: {}
}
editing.value = true
}
function openCreateModal() {
form.value = {
type: activeTab.value,
priority: 10,
enabled: true,
weight: 1,
timeout_ms: 60000,
max_retries: 1,
config_json: {}
}
editing.value = true
}
function edit(p: Provider) {
const { has_api_key, ...rest } = p
form.value = { ...rest, api_key: '', config_json: rest.config_json || {} } // Clear key for security, user re-enters if needed
editing.value = true
}
function reset() {
editing.value = false
form.value = { config_json: {} }
}
async function submit() {
const method = form.value.id ? 'PUT' : 'POST'
const url = form.value.id
? `${apiBase}/admin/providers/${form.value.id}`
: `${apiBase}/admin/providers`
await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
Authorization: getAuthHeader()
},
body: JSON.stringify(form.value)
})
await loadData()
reset()
}
async function remove(p: Provider) {
if(!confirm(`确认删除 ${p.name}?`)) return
await fetch(`${apiBase}/admin/providers/${p.id}`, {
method: 'DELETE',
headers: { Authorization: getAuthHeader() }
})
await loadData()
}
async function toggleEnabled(p: Provider) {
await fetch(`${apiBase}/admin/providers/${p.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: getAuthHeader()
},
body: JSON.stringify({ enabled: !p.enabled })
})
await loadData()
}
onMounted(() => {
if (isLoggedIn.value) loadData()
})
</script>