fix: AI并发调用 + 上传刷新 + 按钮位置 + 内容撤回
- Celery 任务: asyncio.gather 并发调用所有 AI 模型(非串行) - TemplateList: beforeUpload→customRequest 修复上传后列表不刷新 - Editor: 设为AI生成点按钮移到右上角, 不再遮挡菜单栏 - Editor: uploadRefForPoints 替换为内联上传函数 - Editor: value(受控)→editor.setContent 修复编辑后内容立即撤回 - request 导入补充
This commit is contained in:
parent
786419419f
commit
7826a1af38
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 />}>
|
||||
|
|
|
|||
Loading…
Reference in New Issue