feat: implement core API and generation services for AI image, video, and chat functionalities, user history, and point management.

This commit is contained in:
公司git 2026-01-20 09:29:01 +08:00
parent ced1020235
commit 824508f6a4
4 changed files with 125 additions and 89 deletions

View File

@ -5,6 +5,14 @@
### 1. 环境准备 ### 1. 环境准备
确保已安装 Python 3.8+ 和 PostgreSQL / Redis。 确保已安装 Python 3.8+ 和 PostgreSQL / Redis。
**创建虚拟环境**
```bash
# 如果 python 命令不可用,请尝试使用 py
python -m venv .venv
# 或者
py -m venv .venv
```
**激活虚拟环境** (推荐) **激活虚拟环境** (推荐)
```bash ```bash
# Windows (PowerShell) # Windows (PowerShell)

View File

@ -84,6 +84,7 @@ def generate():
"prompt": prompt, "prompt": prompt,
"model": model_value, "model": model_value,
"response_format": "url", "response_format": "url",
"n": int(data.get('n', 1)), # 添加数量
"aspect_ratio": data.get('ratio') "aspect_ratio": data.get('ratio')
} }
image_data = data.get('image_data', []) image_data = data.get('image_data', [])

View File

@ -28,6 +28,7 @@ def validate_generation_request(user, data):
is_premium = data.get('is_premium', False) is_premium = data.get('is_premium', False)
input_key = data.get('apiKey') input_key = data.get('apiKey')
model_value = data.get('model') model_value = data.get('model')
n = int(data.get('n', 1)) # 获取请求中的数量
target_api = Config.AI_API target_api = Config.AI_API
api_key = None api_key = None
@ -50,13 +51,17 @@ def validate_generation_request(user, data):
else: else:
return None, None, 0, False, "可用积分已耗尽,请充值或切换至自定义 Key 模式" return None, None, 0, False, "可用积分已耗尽,请充值或切换至自定义 Key 模式"
cost = get_model_cost(model_value, is_video=False) # 计算单价
base_cost = get_model_cost(model_value, is_video=False)
if use_trial and is_premium: if use_trial and is_premium:
cost *= 2 base_cost *= 2
# 总消耗 = 单价 * 数量
cost = base_cost * n
if use_trial: if use_trial:
if user.points < cost: if user.points < cost:
return None, None, cost, True, "可用积分不足" return None, None, cost, True, f"可用积分不足(本次需要 {cost} 积分)"
return api_key, target_api, cost, use_trial, None return api_key, target_api, cost, use_trial, None

View File

@ -195,6 +195,8 @@ async function init() {
if (modeTrialBtn) modeTrialBtn.onclick = () => switchMode('trial'); if (modeTrialBtn) modeTrialBtn.onclick = () => switchMode('trial');
if (modeKeyBtn) modeKeyBtn.onclick = () => switchMode('key'); if (modeKeyBtn) modeKeyBtn.onclick = () => switchMode('key');
if (isPremiumCheckbox) isPremiumCheckbox.onchange = () => updateCostPreview(); if (isPremiumCheckbox) isPremiumCheckbox.onchange = () => updateCostPreview();
const numSelect = document.getElementById('numSelect');
if (numSelect) numSelect.onchange = () => updateCostPreview();
// 历史记录控制 // 历史记录控制
const historyDrawer = document.getElementById('historyDrawer'); const historyDrawer = document.getElementById('historyDrawer');
@ -305,13 +307,16 @@ function fillSelect(id, list) {
function updateCostPreview() { function updateCostPreview() {
const modelSelect = document.getElementById('modelSelect'); const modelSelect = document.getElementById('modelSelect');
const costPreview = document.getElementById('costPreview'); const costPreview = document.getElementById('costPreview');
const numSelect = document.getElementById('numSelect');
const isPremium = document.getElementById('isPremium')?.checked || false; const isPremium = document.getElementById('isPremium')?.checked || false;
const selectedOption = modelSelect.options[modelSelect.selectedIndex]; const selectedOption = modelSelect.options[modelSelect.selectedIndex];
if (currentMode === 'trial' && selectedOption) { if (currentMode === 'trial' && selectedOption) {
let cost = parseInt(selectedOption.getAttribute('data-cost') || 0); let baseCost = parseInt(selectedOption.getAttribute('data-cost') || 0);
if (isPremium) cost *= 2; // 优质模式 2 倍积分 let num = parseInt(numSelect?.value || 1);
costPreview.innerText = `本次生成将消耗 ${cost} 积分`; let totalCost = baseCost * num;
if (isPremium) totalCost *= 2; // 优质模式 2 倍积分
costPreview.innerText = `本次生成将消耗 ${totalCost} 积分`;
costPreview.classList.remove('hidden'); costPreview.classList.remove('hidden');
} else { } else {
costPreview.classList.add('hidden'); costPreview.classList.add('hidden');
@ -498,23 +503,26 @@ document.getElementById('submitBtn').onclick = async () => {
image_data = await Promise.all(uploadedFiles.map(f => readFileAsBase64(f))); image_data = await Promise.all(uploadedFiles.map(f => readFileAsBase64(f)));
} }
// 2. 并行启动多个生成任务 // 2. 发起单次生成请求 (包含数量 n)
btnText.innerText = `AI 构思中 (0/${num})...`; btnText.innerText = `AI 构思中...`;
let finishedCount = 0;
const startTask = async (index) => { // 预先创建 Slot (对应数量)
const slots = [];
for (let i = 0; i < num; i++) {
const slot = document.createElement('div'); const slot = document.createElement('div');
slot.className = 'image-frame relative bg-white/50 animate-pulse min-h-[200px] flex items-center justify-center rounded-[2.5rem] border border-slate-100 shadow-sm'; slot.className = 'image-frame relative bg-white/50 animate-pulse min-h-[200px] flex items-center justify-center rounded-[2.5rem] border border-slate-100 shadow-sm';
slot.innerHTML = `<div class="text-slate-400 text-[10px] font-bold italic">正在排队中...</div>`; slot.innerHTML = `<div class="text-slate-400 text-[10px] font-bold italic">正在排队中...</div>`;
grid.appendChild(slot); grid.appendChild(slot);
slots.push(slot);
}
try { // 3. 提交任务
// 1. 发起生成请求,获取任务 ID
const r = await fetch('/api/generate', { const r = await fetch('/api/generate', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
mode: currentMode, mode: currentMode,
n: num, // 发送数量
is_premium: document.getElementById('isPremium')?.checked || false, is_premium: document.getElementById('isPremium')?.checked || false,
apiKey: currentMode === 'key' ? apiKey : '', apiKey: currentMode === 'key' ? apiKey : '',
prompt: document.getElementById('manualPrompt').value, prompt: document.getElementById('manualPrompt').value,
@ -527,16 +535,18 @@ document.getElementById('submitBtn').onclick = async () => {
const res = await r.json(); const res = await r.json();
if (res.error) throw new Error(res.error); if (res.error) throw new Error(res.error);
// 如果直接返回了 data (比如聊天模型),直接显示 // 如果直接返回了 data (比如聊天模型),直接显示在第一个 slot
if (res.data) { if (res.data) {
displayResult(slot, res.data[0]); displayResult(slots[0], res.data[0]);
// 移除其他多余的 slots
for (let i = 1; i < slots.length; i++) slots[i].remove();
return; return;
} }
// 2. 轮询任务状态 // 4. 轮询任务状态
const taskId = res.task_id; const taskId = res.task_id;
let pollCount = 0; let pollCount = 0;
const maxPolls = 500; // 最多轮询约 16 分钟 (2s * 500 = 1000s) const maxPolls = 500;
while (pollCount < maxPolls) { while (pollCount < maxPolls) {
await new Promise(resolve => setTimeout(resolve, 2000)); await new Promise(resolve => setTimeout(resolve, 2000));
@ -546,34 +556,59 @@ document.getElementById('submitBtn').onclick = async () => {
const statusRes = await statusR.json(); const statusRes = await statusR.json();
if (statusRes.status === 'complete') { if (statusRes.status === 'complete') {
const imgUrl = statusRes.urls[0]; const urls = statusRes.urls;
currentGeneratedUrls.push(imgUrl); currentGeneratedUrls = urls;
displayResult(slot, { url: imgUrl });
finishedCount++; // 填充所有 slots
btnText.innerText = `AI 构思中 (${finishedCount}/${num})...`; urls.forEach((url, idx) => {
if (slots[idx]) {
displayResult(slots[idx], { url });
}
});
// 如果返回的数量少于 slots 数量,移除多余的
if (urls.length < slots.length) {
for (let i = urls.length; i < slots.length; i++) slots[i].remove();
}
btnText.innerText = `生成完成`;
if (currentMode === 'trial') checkAuth(); if (currentMode === 'trial') checkAuth();
return; // 任务正常结束 return;
} else if (statusRes.status === 'error') { } else if (statusRes.status === 'error') {
throw new Error(statusRes.message || "生成失败"); throw new Error(statusRes.message || "生成失败");
} else { } else {
// 继续轮询状态显示 // 更新所有 slot 的轮询状态显示
slots.forEach(slot => {
slot.innerHTML = `<div class="text-slate-400 text-[10px] font-bold italic">AI 正在努力创作中 (${pollCount * 2}s)...</div>`; slot.innerHTML = `<div class="text-slate-400 text-[10px] font-bold italic">AI 正在努力创作中 (${pollCount * 2}s)...</div>`;
});
} }
} }
throw new Error("生成超时,请稍后在历史记录中查看"); throw new Error("生成超时,请稍后在历史记录中查看");
} catch (e) { } catch (e) {
slot.className = 'image-frame relative bg-red-50/50 flex items-center justify-center rounded-[2.5rem] border border-red-100 p-6'; // 在第一个 slot 显示错误
if (grid.firstChild) {
const firstSlot = grid.firstChild;
firstSlot.className = 'image-frame relative bg-red-50/50 flex items-center justify-center rounded-[2.5rem] border border-red-100 p-6';
if (e.message.includes('401') || e.message.includes('请先登录')) { if (e.message.includes('401') || e.message.includes('请先登录')) {
slot.innerHTML = `<div class="text-red-400 text-[10px] font-bold text-center">登录已过期,请重新登录</div>`; firstSlot.innerHTML = `<div class="text-red-400 text-[10px] font-bold text-center">登录已过期,请重新登录</div>`;
} else { } else {
slot.innerHTML = `<div class="text-red-400 text-[10px] font-bold text-center">生成异常: ${e.message}</div>`; firstSlot.innerHTML = `<div class="text-red-400 text-[10px] font-bold text-center">生成异常: ${e.message}</div>`;
}
// 移除其他 slots
while (grid.children.length > 1) {
grid.lastChild.remove();
} }
} }
}; } finally {
btn.disabled = false;
btnText.innerText = "立即生成作品";
document.getElementById('statusInfo').classList.add('hidden');
}
};
// 提取结果展示逻辑 // 提取结果展示逻辑
const displayResult = (slot, data) => { const displayResult = (slot, data) => {
if (data.type === 'text') { if (data.type === 'text') {
slot.className = 'image-frame relative bg-white border border-slate-100 p-8 rounded-[2.5rem] shadow-xl overflow-y-auto max-h-[600px]'; slot.className = 'image-frame relative bg-white border border-slate-100 p-8 rounded-[2.5rem] shadow-xl overflow-y-auto max-h-[600px]';
slot.innerHTML = `<div class="prose prose-slate prose-sm max-w-none text-slate-600 font-medium leading-relaxed">${data.content.replace(/\n/g, '<br>')}</div>`; slot.innerHTML = `<div class="prose prose-slate prose-sm max-w-none text-slate-600 font-medium leading-relaxed">${data.content.replace(/\n/g, '<br>')}</div>`;
@ -592,19 +627,6 @@ document.getElementById('submitBtn').onclick = async () => {
`; `;
} }
lucide.createIcons(); lucide.createIcons();
};
const tasks = Array.from({ length: num }, (_, i) => startTask(i));
await Promise.all(tasks);
} catch (e) {
showToast('创作引擎中断: ' + e.message, 'error');
document.getElementById('placeholder').classList.remove('hidden');
} finally {
btn.disabled = false;
btnText.innerText = "立即生成作品";
document.getElementById('statusInfo').classList.add('hidden');
}
}; };
init(); init();