fix: AI并发调用 + 上传刷新 + 按钮位置 + 内容撤回

- Celery 任务: asyncio.gather 并发调用所有 AI 模型(非串行)
- TemplateList: beforeUpload→customRequest 修复上传后列表不刷新
- Editor: 设为AI生成点按钮移到右上角, 不再遮挡菜单栏
- Editor: uploadRefForPoints 替换为内联上传函数
- Editor: value(受控)→editor.setContent 修复编辑后内容立即撤回
- request 导入补充
This commit is contained in:
zwt13703 2026-07-06 21:50:29 +08:00
parent 786419419f
commit 7826a1af38
3 changed files with 77 additions and 38 deletions

View File

@ -1,3 +1,4 @@
import asyncio
import os
from io import BytesIO
from datetime import datetime, timezone
@ -145,15 +146,15 @@ async def _generate_document(task_id: str) -> None:
points = points_result.scalars().all()
try:
# 先收集所有 AI 调用参数
point_data = []
for point in points:
selected_text = point.selected_text or _get_selected_text(html_content, point.position)
if not selected_text:
continue
ref_content = ""
if point.need_ref_file and point.ref_file_path:
ref_content = await parse_reference_files(point.ref_file_path)
elif point.ref_file_path:
if point.ref_file_path:
ref_content = await parse_reference_files(point.ref_file_path)
model_config = {"provider": "custom", "endpoint": "", "api_key": "", "extra_params": {}}
@ -169,10 +170,28 @@ async def _generate_document(task_id: str) -> None:
"extra_params": model.extra_params,
}
ai_result = await call_ai_model(model_config, point.prompt, ref_content)
_replace_text_in_docx(doc, selected_text, ai_result)
point_data.append({
"point": point,
"selected_text": selected_text,
"model_config": model_config,
"ref_content": ref_content,
})
await db.commit()
# 并发调用所有 AI 模型
async def _call_one(pd):
try:
return await call_ai_model(pd["model_config"], pd["point"].prompt, pd["ref_content"])
except Exception as e:
return f"[生成失败: {e}]"
coros = [_call_one(pd) for pd in point_data]
ai_results = await asyncio.gather(*coros)
# 按顺序替换文本
for pd, ai_result in zip(point_data, ai_results):
_replace_text_in_docx(doc, pd["selected_text"], str(ai_result))
await db.commit()
result_dir = get_storage_dir(RESULTS_DIR)
result_path = os.path.join(result_dir, f"{task_id}.docx")

View File

@ -16,6 +16,7 @@ import type { GenerationPoint, AIModel } from "../types";
import * as templateApi from "../api/templates";
import * as pointApi from "../api/generationPoints";
import * as modelApi from "../api/models";
import request from "../api/request";
import GenerateModal from "../components/GenerateModal";
const { Sider, Content } = Layout;
@ -315,25 +316,32 @@ export default function TemplateEditor() {
<Layout style={{ height: "calc(100% - 50px)", background: "#fff" }}>
<Content style={{ position: "relative", overflow: "hidden" }}>
{showSelectionBtn && (
<Button
type="primary"
size="small"
icon={<PlusOutlined />}
onClick={handleCreatePoint}
style={{
position: "absolute",
zIndex: 1310,
top: 8,
left: 8,
}}
>
AI
</Button>
<div style={{
position: "absolute",
zIndex: 1310,
top: 0,
right: 8,
padding: "4px 8px",
background: "#fff",
borderRadius: "0 0 4px 4px",
boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
}}>
<Button
type="primary"
size="small"
icon={<PlusOutlined />}
onClick={handleCreatePoint}
>
AI
</Button>
</div>
)}
<Editor
tinymceScriptSrc="/tinymce/tinymce.min.js"
onInit={handleEditorInit}
value={htmlContent}
onInit={(_evt, editor) => {
handleEditorInit(null, editor);
if (htmlContent) editor.setContent(htmlContent);
}}
init={{
height: "100%",
menubar: true,
@ -390,7 +398,23 @@ export default function TemplateEditor() {
type="text"
size="small"
icon={<PaperClipOutlined />}
onClick={() => uploadRefForPoints([point.id])}
onClick={async () => {
const input = document.createElement("input");
input.type = "file";
input.multiple = true;
input.accept = ".txt,.docx,.pdf";
input.onchange = async (e) => {
const files = (e.target as HTMLInputElement).files;
if (!files?.length) return;
const fd = new FormData();
Array.from(files).forEach((f) => fd.append("ref_files", f));
await request.post(`/generation-points/${point.id}/upload-ref`, fd);
message.success("上传成功");
const updated = await pointApi.listGenerationPoints(id!);
setPoints(updated);
};
input.click();
}}
/>
)}
<Button

View File

@ -1,6 +1,6 @@
import { useEffect, useState, useCallback } from "react";
import { Table, Button, Upload, Space, message, Popconfirm } from "antd";
import { UploadOutlined, EditOutlined, DeleteOutlined, DownloadOutlined, PlusOutlined } from "@ant-design/icons";
import { EditOutlined, DeleteOutlined, DownloadOutlined, PlusOutlined } from "@ant-design/icons";
import { useNavigate } from "react-router-dom";
import type { TemplateListItem } from "../types";
import * as templateApi from "../api/templates";
@ -26,17 +26,6 @@ export default function TemplateList() {
fetchTemplates();
}, [fetchTemplates]);
const handleUpload = async (file: File) => {
try {
await templateApi.uploadTemplate(file);
message.success("上传成功");
fetchTemplates();
} catch {
message.error("上传失败");
}
return false;
};
const handleDelete = async (id: string) => {
await templateApi.deleteTemplate(id);
message.success("删除成功");
@ -99,9 +88,16 @@ export default function TemplateList() {
<Upload
accept=".docx"
showUploadList={false}
beforeUpload={(file) => {
handleUpload(file);
return false;
customRequest={async ({ file, onSuccess, onError }: any) => {
try {
await templateApi.uploadTemplate(file as File);
message.success("上传成功");
fetchTemplates();
onSuccess?.("ok");
} catch {
message.error("上传失败");
onError?.(new Error("上传失败"));
}
}}
>
<Button type="primary" icon={<PlusOutlined />}>