feat: 生成点表单重构 - 参考文件改为开关 + 备注字段
- 模型新增 need_ref_file(bool) + remark(text) 字段 - 生成点创建表单移除直接上传,改为「需要参考文件」开关 + 备注文本框 - 测试按钮:检测 need_ref → 弹出文件选择 → 上传后提取文本 + 调AI - 生成任务:检查 ref_file_path,有则提取文本并入 prompt - 前后端完整联调,TypeScript 零错误
This commit is contained in:
parent
c11ea22e88
commit
57cdb01ec8
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"hash": "1e9afdbb",
|
||||
"configHash": "ac7453e4",
|
||||
"lockfileHash": "e3b0c442",
|
||||
"browserHash": "29840d3b",
|
||||
"optimized": {},
|
||||
"chunks": {}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"type": "module"
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
"""Add need_ref_file and remark to generation_points
|
||||
|
||||
Revision ID: b4b91d4466e4
|
||||
Revises: 011bde04d871
|
||||
Create Date: 2026-07-06 18:27:03.054325
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = 'b4b91d4466e4'
|
||||
down_revision: Union[str, None] = '011bde04d871'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('generation_points', sa.Column('need_ref_file', sa.Boolean(), nullable=False))
|
||||
op.add_column('generation_points', sa.Column('remark', sa.Text(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('generation_points', 'remark')
|
||||
op.drop_column('generation_points', 'need_ref_file')
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -26,6 +26,8 @@ async def create_generation_point(
|
|||
model_id: uuid.UUID | None = Form(None),
|
||||
order: int = Form(0),
|
||||
selected_text: str | None = Form(None),
|
||||
need_ref_file: bool = Form(False),
|
||||
remark: str | None = Form(None),
|
||||
ref_file: UploadFile | None = File(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
|
|
@ -53,6 +55,8 @@ async def create_generation_point(
|
|||
order=order,
|
||||
ref_file_path=ref_file_path,
|
||||
selected_text=selected_text,
|
||||
need_ref_file=need_ref_file,
|
||||
remark=remark,
|
||||
)
|
||||
db.add(point)
|
||||
await db.flush()
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
import os
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File
|
||||
from fastapi.responses import Response
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.core.database import get_db
|
||||
from app.core.security_middleware import validate_file_extension
|
||||
from app.models.generation_task import GenerationTask
|
||||
from app.models.generation_point import GenerationPoint
|
||||
from app.models.template import Template
|
||||
from app.models.ai_model import AIModel
|
||||
from app.schemas.generation_task import TaskResponse, GenerateResponse, SingleTestRequest, SingleTestResponse
|
||||
from app.services.document_processor import docx_to_pdf_bytes
|
||||
from app.services.file_storage import get_file_content
|
||||
from app.services.file_storage import get_file_content, save_upload, REF_FILES_DIR
|
||||
from app.services.ai_adapter import call_ai_model
|
||||
from app.services.ref_parser import parse_reference_file
|
||||
from app.tasks.generate import generate_document
|
||||
|
|
@ -123,6 +124,7 @@ async def cancel_task(task_id: str, db: AsyncSession = Depends(get_db)):
|
|||
@router.post("/generation-points/{point_id}/test", response_model=SingleTestResponse)
|
||||
async def test_single_point(
|
||||
point_id: str,
|
||||
ref_file: UploadFile | None = File(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
point_result = await db.execute(select(GenerationPoint).where(GenerationPoint.id == point_id))
|
||||
|
|
@ -130,6 +132,15 @@ async def test_single_point(
|
|||
if not point:
|
||||
raise HTTPException(status_code=404, detail="生成点不存在")
|
||||
|
||||
if point.need_ref_file and not point.ref_file_path and not ref_file:
|
||||
raise HTTPException(status_code=400, detail="此生成点需要上传参考文件")
|
||||
|
||||
if ref_file and ref_file.filename:
|
||||
validate_file_extension(ref_file.filename)
|
||||
tmp_path = await save_upload(ref_file, REF_FILES_DIR)
|
||||
else:
|
||||
tmp_path = point.ref_file_path
|
||||
|
||||
model_config = {"provider": "custom", "endpoint": "", "api_key": "", "extra_params": {}}
|
||||
if point.model_id:
|
||||
model_result = await db.execute(select(AIModel).where(AIModel.id == point.model_id))
|
||||
|
|
@ -143,8 +154,8 @@ async def test_single_point(
|
|||
}
|
||||
|
||||
ref_content = None
|
||||
if point.ref_file_path:
|
||||
ref_content = await parse_reference_file(point.ref_file_path)
|
||||
if tmp_path:
|
||||
ref_content = await parse_reference_file(tmp_path)
|
||||
|
||||
try:
|
||||
ai_result = await call_ai_model(model_config, point.prompt, ref_content)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ class GenerationPoint(Base, UUIDMixin, TimestampMixin):
|
|||
UUID(as_uuid=True), ForeignKey("models.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
ref_file_path: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
need_ref_file: Mapped[bool] = mapped_column(default=False, nullable=False)
|
||||
remark: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
order: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
||||
selected_text: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ class GenerationPointCreate(BaseModel):
|
|||
model_id: uuid.UUID | None = None
|
||||
order: int = 0
|
||||
selected_text: str | None = None
|
||||
need_ref_file: bool = False
|
||||
remark: str | None = None
|
||||
|
||||
|
||||
class GenerationPointUpdate(BaseModel):
|
||||
|
|
@ -18,6 +20,8 @@ class GenerationPointUpdate(BaseModel):
|
|||
model_id: uuid.UUID | None = None
|
||||
order: int | None = None
|
||||
selected_text: str | None = None
|
||||
need_ref_file: bool | None = None
|
||||
remark: str | None = None
|
||||
|
||||
|
||||
class GenerationPointResponse(BaseModel):
|
||||
|
|
@ -27,6 +31,8 @@ class GenerationPointResponse(BaseModel):
|
|||
prompt: str
|
||||
model_id: uuid.UUID | None = None
|
||||
ref_file_path: str | None = None
|
||||
need_ref_file: bool = False
|
||||
remark: str | None = None
|
||||
order: int
|
||||
selected_text: str | None = None
|
||||
created_at: datetime
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ export async function createGenerationPoint(body: {
|
|||
model_id?: string;
|
||||
order?: number;
|
||||
selected_text?: string;
|
||||
need_ref_file?: boolean;
|
||||
remark?: string;
|
||||
ref_file?: File;
|
||||
}) {
|
||||
const formData = new FormData();
|
||||
|
|
@ -24,6 +26,8 @@ export async function createGenerationPoint(body: {
|
|||
if (body.model_id) formData.append("model_id", body.model_id);
|
||||
if (body.order !== undefined) formData.append("order", String(body.order));
|
||||
if (body.selected_text) formData.append("selected_text", body.selected_text);
|
||||
if (body.need_ref_file) formData.append("need_ref_file", "true");
|
||||
if (body.remark) formData.append("remark", body.remark);
|
||||
if (body.ref_file) formData.append("ref_file", body.ref_file);
|
||||
const { data } = await request.post<GenerationPoint>("/generation-points", formData);
|
||||
return data;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { useEffect, useState, useRef, useCallback } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Layout, Button, Modal, Form, Input, Select, Upload,
|
||||
Space, message, Spin, Progress, Popconfirm, Card, Collapse,
|
||||
Layout, Button, Modal, Form, Input, Select,
|
||||
Space, message, Spin, Progress, Popconfirm, Card, Switch,
|
||||
} from "antd";
|
||||
import {
|
||||
PlusOutlined, DeleteOutlined, EditOutlined, SaveOutlined,
|
||||
|
|
@ -128,6 +128,8 @@ export default function TemplateEditor() {
|
|||
pointForm.setFieldsValue({
|
||||
prompt: point.prompt,
|
||||
model_id: point.model_id,
|
||||
need_ref_file: point.need_ref_file,
|
||||
remark: point.remark,
|
||||
});
|
||||
setPointModalOpen(true);
|
||||
};
|
||||
|
|
@ -146,6 +148,8 @@ export default function TemplateEditor() {
|
|||
const updateData: Record<string, unknown> = {};
|
||||
if (values.prompt) updateData.prompt = values.prompt;
|
||||
if (values.model_id) updateData.model_id = values.model_id;
|
||||
updateData.need_ref_file = values.need_ref_file || false;
|
||||
if (values.remark !== undefined) updateData.remark = values.remark;
|
||||
await pointApi.updateGenerationPoint(editingPoint.id, updateData);
|
||||
message.success("更新成功");
|
||||
} else if (position) {
|
||||
|
|
@ -156,6 +160,9 @@ export default function TemplateEditor() {
|
|||
model_id: values.model_id,
|
||||
order: points.length,
|
||||
selected_text: selectionRef.current?.text,
|
||||
need_ref_file: values.need_ref_file || false,
|
||||
remark: values.remark || "",
|
||||
});
|
||||
ref_file: refFile,
|
||||
});
|
||||
message.success("创建成功");
|
||||
|
|
@ -213,6 +220,35 @@ export default function TemplateEditor() {
|
|||
};
|
||||
|
||||
const handleTestPoint = async (pointId: string) => {
|
||||
const point = points.find((p) => p.id === pointId);
|
||||
if (!point) return;
|
||||
|
||||
if (point.need_ref_file) {
|
||||
const fileInput = document.createElement("input");
|
||||
fileInput.type = "file";
|
||||
fileInput.accept = ".txt,.docx,.pdf";
|
||||
fileInput.onchange = async (e: Event) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
message.loading({ content: "测试中...", key: "test" });
|
||||
const formData = new FormData();
|
||||
formData.append("ref_file", file);
|
||||
const { default: request } = await import("../api/request");
|
||||
const { data } = await request.post<{ result: string }>(
|
||||
`/generation-points/${pointId}/test`,
|
||||
formData
|
||||
);
|
||||
message.success({ content: "测试完成", key: "test" });
|
||||
Modal.info({ title: "AI 生成结果", content: data.result, width: 600 });
|
||||
} catch {
|
||||
message.error({ content: "测试失败", key: "test" });
|
||||
}
|
||||
};
|
||||
fileInput.click();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
message.loading({ content: "测试中...", key: "test" });
|
||||
const result = await pointApi.testGenerationPoint(pointId);
|
||||
|
|
@ -473,13 +509,12 @@ export default function TemplateEditor() {
|
|||
options={models.map((m) => ({ label: m.name, value: m.id }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
{!editingPoint && (
|
||||
<Form.Item name="ref_file" label="参考文件" valuePropName="fileList" getValueFromEvent={(e) => e.fileList}>
|
||||
<Upload maxCount={1} beforeUpload={() => false}>
|
||||
<Button icon={<PlusOutlined />}>上传参考文件</Button>
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item name="need_ref_file" label="需要参考文件" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input.TextArea rows={2} placeholder="补充说明(可选)" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,10 @@ export interface GenerationPoint {
|
|||
prompt: string;
|
||||
model_id: string | null;
|
||||
ref_file_path: string | null;
|
||||
need_ref_file: boolean;
|
||||
remark: string | null;
|
||||
order: number;
|
||||
selected_text: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue