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
|
import os
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
@ -145,15 +146,15 @@ async def _generate_document(task_id: str) -> None:
|
||||||
points = points_result.scalars().all()
|
points = points_result.scalars().all()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# 先收集所有 AI 调用参数
|
||||||
|
point_data = []
|
||||||
for point in points:
|
for point in points:
|
||||||
selected_text = point.selected_text or _get_selected_text(html_content, point.position)
|
selected_text = point.selected_text or _get_selected_text(html_content, point.position)
|
||||||
if not selected_text:
|
if not selected_text:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
ref_content = ""
|
ref_content = ""
|
||||||
if point.need_ref_file and point.ref_file_path:
|
if point.ref_file_path:
|
||||||
ref_content = await parse_reference_files(point.ref_file_path)
|
|
||||||
elif point.ref_file_path:
|
|
||||||
ref_content = await parse_reference_files(point.ref_file_path)
|
ref_content = await parse_reference_files(point.ref_file_path)
|
||||||
|
|
||||||
model_config = {"provider": "custom", "endpoint": "", "api_key": "", "extra_params": {}}
|
model_config = {"provider": "custom", "endpoint": "", "api_key": "", "extra_params": {}}
|
||||||
|
|
@ -169,8 +170,26 @@ async def _generate_document(task_id: str) -> None:
|
||||||
"extra_params": model.extra_params,
|
"extra_params": model.extra_params,
|
||||||
}
|
}
|
||||||
|
|
||||||
ai_result = await call_ai_model(model_config, point.prompt, ref_content)
|
point_data.append({
|
||||||
_replace_text_in_docx(doc, selected_text, ai_result)
|
"point": point,
|
||||||
|
"selected_text": selected_text,
|
||||||
|
"model_config": model_config,
|
||||||
|
"ref_content": ref_content,
|
||||||
|
})
|
||||||
|
|
||||||
|
# 并发调用所有 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()
|
await db.commit()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import type { GenerationPoint, AIModel } from "../types";
|
||||||
import * as templateApi from "../api/templates";
|
import * as templateApi from "../api/templates";
|
||||||
import * as pointApi from "../api/generationPoints";
|
import * as pointApi from "../api/generationPoints";
|
||||||
import * as modelApi from "../api/models";
|
import * as modelApi from "../api/models";
|
||||||
|
import request from "../api/request";
|
||||||
import GenerateModal from "../components/GenerateModal";
|
import GenerateModal from "../components/GenerateModal";
|
||||||
|
|
||||||
const { Sider, Content } = Layout;
|
const { Sider, Content } = Layout;
|
||||||
|
|
@ -315,25 +316,32 @@ export default function TemplateEditor() {
|
||||||
<Layout style={{ height: "calc(100% - 50px)", background: "#fff" }}>
|
<Layout style={{ height: "calc(100% - 50px)", background: "#fff" }}>
|
||||||
<Content style={{ position: "relative", overflow: "hidden" }}>
|
<Content style={{ position: "relative", overflow: "hidden" }}>
|
||||||
{showSelectionBtn && (
|
{showSelectionBtn && (
|
||||||
|
<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
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={handleCreatePoint}
|
onClick={handleCreatePoint}
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
zIndex: 1310,
|
|
||||||
top: 8,
|
|
||||||
left: 8,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
设为 AI 生成点
|
设为 AI 生成点
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<Editor
|
<Editor
|
||||||
tinymceScriptSrc="/tinymce/tinymce.min.js"
|
tinymceScriptSrc="/tinymce/tinymce.min.js"
|
||||||
onInit={handleEditorInit}
|
onInit={(_evt, editor) => {
|
||||||
value={htmlContent}
|
handleEditorInit(null, editor);
|
||||||
|
if (htmlContent) editor.setContent(htmlContent);
|
||||||
|
}}
|
||||||
init={{
|
init={{
|
||||||
height: "100%",
|
height: "100%",
|
||||||
menubar: true,
|
menubar: true,
|
||||||
|
|
@ -390,7 +398,23 @@ export default function TemplateEditor() {
|
||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<PaperClipOutlined />}
|
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
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import { Table, Button, Upload, Space, message, Popconfirm } from "antd";
|
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 { useNavigate } from "react-router-dom";
|
||||||
import type { TemplateListItem } from "../types";
|
import type { TemplateListItem } from "../types";
|
||||||
import * as templateApi from "../api/templates";
|
import * as templateApi from "../api/templates";
|
||||||
|
|
@ -26,17 +26,6 @@ export default function TemplateList() {
|
||||||
fetchTemplates();
|
fetchTemplates();
|
||||||
}, [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) => {
|
const handleDelete = async (id: string) => {
|
||||||
await templateApi.deleteTemplate(id);
|
await templateApi.deleteTemplate(id);
|
||||||
message.success("删除成功");
|
message.success("删除成功");
|
||||||
|
|
@ -99,9 +88,16 @@ export default function TemplateList() {
|
||||||
<Upload
|
<Upload
|
||||||
accept=".docx"
|
accept=".docx"
|
||||||
showUploadList={false}
|
showUploadList={false}
|
||||||
beforeUpload={(file) => {
|
customRequest={async ({ file, onSuccess, onError }: any) => {
|
||||||
handleUpload(file);
|
try {
|
||||||
return false;
|
await templateApi.uploadTemplate(file as File);
|
||||||
|
message.success("上传成功");
|
||||||
|
fetchTemplates();
|
||||||
|
onSuccess?.("ok");
|
||||||
|
} catch {
|
||||||
|
message.error("上传失败");
|
||||||
|
onError?.(new Error("上传失败"));
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button type="primary" icon={<PlusOutlined />}>
|
<Button type="primary" icon={<PlusOutlined />}>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue