feat: implement core API and generation services for AI image, video, and chat functionalities, user history, and point management.
This commit is contained in:
parent
ced1020235
commit
824508f6a4
@ -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)
|
||||||
|
|||||||
@ -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', [])
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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,108 +503,103 @@ 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,
|
||||||
model: document.getElementById('modelSelect').value,
|
model: document.getElementById('modelSelect').value,
|
||||||
ratio: document.getElementById('ratioSelect').value,
|
ratio: document.getElementById('ratioSelect').value,
|
||||||
size: document.getElementById('sizeSelect').value,
|
size: document.getElementById('sizeSelect').value,
|
||||||
image_data // 发送 Base64 数组
|
image_data // 发送 Base64 数组
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
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]);
|
||||||
return;
|
// 移除其他多余的 slots
|
||||||
}
|
for (let i = 1; i < slots.length; i++) slots[i].remove();
|
||||||
|
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));
|
||||||
pollCount++;
|
pollCount++;
|
||||||
|
|
||||||
const statusR = await fetch(`/api/task_status/${taskId}`);
|
const statusR = await fetch(`/api/task_status/${taskId}`);
|
||||||
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 (currentMode === 'trial') checkAuth();
|
if (slots[idx]) {
|
||||||
return; // 任务正常结束
|
displayResult(slots[idx], { url });
|
||||||
} else if (statusRes.status === 'error') {
|
|
||||||
throw new Error(statusRes.message || "生成失败");
|
|
||||||
} else {
|
|
||||||
// 继续轮询状态显示
|
|
||||||
slot.innerHTML = `<div class="text-slate-400 text-[10px] font-bold italic">AI 正在努力创作中 (${pollCount * 2}s)...</div>`;
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
throw new Error("生成超时,请稍后在历史记录中查看");
|
|
||||||
|
|
||||||
} catch (e) {
|
// 如果返回的数量少于 slots 数量,移除多余的
|
||||||
slot.className = 'image-frame relative bg-red-50/50 flex items-center justify-center rounded-[2.5rem] border border-red-100 p-6';
|
if (urls.length < slots.length) {
|
||||||
if (e.message.includes('401') || e.message.includes('请先登录')) {
|
for (let i = urls.length; i < slots.length; i++) slots[i].remove();
|
||||||
slot.innerHTML = `<div class="text-red-400 text-[10px] font-bold text-center">登录已过期,请重新登录</div>`;
|
|
||||||
} else {
|
|
||||||
slot.innerHTML = `<div class="text-red-400 text-[10px] font-bold text-center">生成异常: ${e.message}</div>`;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 提取结果展示逻辑
|
btnText.innerText = `生成完成`;
|
||||||
const displayResult = (slot, data) => {
|
if (currentMode === 'trial') checkAuth();
|
||||||
if (data.type === 'text') {
|
return;
|
||||||
slot.className = 'image-frame relative bg-white border border-slate-100 p-8 rounded-[2.5rem] shadow-xl overflow-y-auto max-h-[600px]';
|
} else if (statusRes.status === 'error') {
|
||||||
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>`;
|
throw new Error(statusRes.message || "生成失败");
|
||||||
} else {
|
} else {
|
||||||
const imgUrl = data.url;
|
// 更新所有 slot 的轮询状态显示
|
||||||
slot.className = 'image-frame group relative animate-in zoom-in-95 duration-700 flex flex-col items-center justify-center overflow-hidden bg-white shadow-2xl transition-all hover:shadow-indigo-100/50';
|
slots.forEach(slot => {
|
||||||
slot.innerHTML = `
|
slot.innerHTML = `<div class="text-slate-400 text-[10px] font-bold italic">AI 正在努力创作中 (${pollCount * 2}s)...</div>`;
|
||||||
<div class="w-full h-full flex items-center justify-center bg-slate-50/20 p-2">
|
});
|
||||||
<img src="${imgUrl}" class="max-w-full max-h-[60vh] md:max-h-[70vh] object-contain rounded-2xl shadow-sm transition-transform duration-500 group-hover:scale-[1.01]">
|
|
||||||
</div>
|
|
||||||
<div class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity bg-slate-900/40 backdrop-blur-[2px] rounded-[2.5rem]">
|
|
||||||
<button onclick="downloadImage('${imgUrl}')" class="bg-white/90 p-4 rounded-full text-indigo-600 shadow-xl hover:scale-110 transition-transform">
|
|
||||||
<i data-lucide="download-cloud" class="w-6 h-6"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
lucide.createIcons();
|
}
|
||||||
};
|
throw new Error("生成超时,请稍后在历史记录中查看");
|
||||||
|
|
||||||
const tasks = Array.from({ length: num }, (_, i) => startTask(i));
|
|
||||||
await Promise.all(tasks);
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('创作引擎中断: ' + e.message, 'error');
|
// 在第一个 slot 显示错误
|
||||||
document.getElementById('placeholder').classList.remove('hidden');
|
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('请先登录')) {
|
||||||
|
firstSlot.innerHTML = `<div class="text-red-400 text-[10px] font-bold text-center">登录已过期,请重新登录</div>`;
|
||||||
|
} else {
|
||||||
|
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 {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btnText.innerText = "立即生成作品";
|
btnText.innerText = "立即生成作品";
|
||||||
@ -607,6 +607,28 @@ document.getElementById('submitBtn').onclick = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 提取结果展示逻辑
|
||||||
|
const displayResult = (slot, data) => {
|
||||||
|
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.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>`;
|
||||||
|
} else {
|
||||||
|
const imgUrl = data.url;
|
||||||
|
slot.className = 'image-frame group relative animate-in zoom-in-95 duration-700 flex flex-col items-center justify-center overflow-hidden bg-white shadow-2xl transition-all hover:shadow-indigo-100/50';
|
||||||
|
slot.innerHTML = `
|
||||||
|
<div class="w-full h-full flex items-center justify-center bg-slate-50/20 p-2">
|
||||||
|
<img src="${imgUrl}" class="max-w-full max-h-[60vh] md:max-h-[70vh] object-contain rounded-2xl shadow-sm transition-transform duration-500 group-hover:scale-[1.01]">
|
||||||
|
</div>
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity bg-slate-900/40 backdrop-blur-[2px] rounded-[2.5rem]">
|
||||||
|
<button onclick="downloadImage('${imgUrl}')" class="bg-white/90 p-4 rounded-full text-indigo-600 shadow-xl hover:scale-110 transition-transform">
|
||||||
|
<i data-lucide="download-cloud" class="w-6 h-6"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
lucide.createIcons();
|
||||||
|
};
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
// 修改密码弹窗控制
|
// 修改密码弹窗控制
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user