From ba5aeefe98d86ac650a67c71f414bccefc7c17bb Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Tue, 16 Jun 2026 17:59:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai-writing):=20=E7=A0=94=E7=A9=B6=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E7=8B=AC=E7=AB=8B=E5=AE=9E=E4=BD=93=20+=20=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E9=A1=B5=20+=20=E5=86=85=E7=BD=AE=E8=8C=83=E4=BE=8B?= =?UTF-8?q?=20+=20=E7=82=B9=E5=87=BB=E7=9B=B4=E6=8E=A5=E8=BF=9B=E5=85=A5?= =?UTF-8?q?=E5=86=99=E4=BD=9C=EF=BC=880616=20#5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端: - 新持久化包 research_templates(表 ai_writing_research_templates,create_all 建表,注册 models/__init__),deps 接 research_template_store - 路由 /api/ai-writing/research-templates CRUD(在 ai_writing.py) - 内置范例 网摘/周报/要讯(DEFAULT_RESEARCH_TEMPLATES + seed_defaults,表空时由 list 接口幂等播种) - tests/test_research_templates.py(9 passed) 前端: - api/ai-writing-research-templates.ts + hooks/useResearchTemplates.ts - 配置页 ResearchTemplatesConfigPage(/page/canvas/research-templates)CRUD - ResearchSidebar 研究模板改用研究模板实体;点击回填整张表单并直接 startWriting 进入写作;顶部齿轮进配置页 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../api/ai-writing-research-templates.ts | 75 ++++ .../components/ai-writing/ResearchSidebar.tsx | 138 ++++--- .../open-canvas/hooks/useResearchTemplates.ts | 60 +++ .../pages/ResearchTemplatesConfigPage.tsx | 374 ++++++++++++++++++ .../open-canvas/routes/OpenCanvasRoutes.tsx | 2 + .../backend/app/gateway/deps.py | 9 + .../backend/app/gateway/routers/ai_writing.py | 130 ++++++ .../deerflow/persistence/models/__init__.py | 2 + .../research_templates/__init__.py | 15 + .../persistence/research_templates/model.py | 35 ++ .../persistence/research_templates/sql.py | 221 +++++++++++ .../backend/tests/test_research_templates.py | 124 ++++++ 12 files changed, 1141 insertions(+), 44 deletions(-) create mode 100644 frontend-web/src/open-canvas/api/ai-writing-research-templates.ts create mode 100644 frontend-web/src/open-canvas/hooks/useResearchTemplates.ts create mode 100644 frontend-web/src/open-canvas/pages/ResearchTemplatesConfigPage.tsx create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/research_templates/__init__.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/research_templates/model.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/research_templates/sql.py create mode 100644 offline-backend-20260512/backend/tests/test_research_templates.py diff --git a/frontend-web/src/open-canvas/api/ai-writing-research-templates.ts b/frontend-web/src/open-canvas/api/ai-writing-research-templates.ts new file mode 100644 index 00000000..05d9e1a8 --- /dev/null +++ b/frontend-web/src/open-canvas/api/ai-writing-research-templates.ts @@ -0,0 +1,75 @@ +import { getBackendBaseURL } from '@/core/config'; +import { apiFetch } from '@/core/api'; + +/** 课题研究模板:一组写作表单预设(需求 0616 #5)。 */ +export interface ResearchTemplate { + id: string; + name: string; + description: string | null; + /** 引用的文章类型 key(对应 ArticleType.key);null = 不设。 */ + article_type_key: string | null; + outline: string | null; + material_source: 'general' | 'notebook'; + word_count: number; + audience: string | null; + strict_mode: boolean; + topic: string | null; + sort_order: number; + created_at: string; + updated_at: string; +} + +export interface CreateResearchTemplateRequest { + name: string; + description?: string | null; + article_type_key?: string | null; + outline?: string | null; + material_source?: 'general' | 'notebook'; + word_count?: number; + audience?: string | null; + strict_mode?: boolean; + topic?: string | null; + sort_order?: number; +} + +export type UpdateResearchTemplateRequest = Partial; + +const BASE = () => `${getBackendBaseURL()}/api/ai-writing/research-templates`; + +export async function listResearchTemplates(): Promise { + const resp = await apiFetch(BASE()); + if (!resp.ok) throw new Error(`获取研究模板失败: ${resp.status}`); + return resp.json(); +} + +export async function createResearchTemplate( + data: CreateResearchTemplateRequest, +): Promise { + const resp = await apiFetch(BASE(), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + if (!resp.ok) throw new Error(`创建研究模板失败: ${resp.status}`); + return resp.json(); +} + +export async function updateResearchTemplate( + id: string, + data: UpdateResearchTemplateRequest, +): Promise { + const resp = await apiFetch(`${BASE()}/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + if (resp.status === 404) throw new Error(`研究模板不存在`); + if (!resp.ok) throw new Error(`更新研究模板失败: ${resp.status}`); + return resp.json(); +} + +export async function deleteResearchTemplate(id: string): Promise { + const resp = await apiFetch(`${BASE()}/${id}`, { method: 'DELETE' }); + if (resp.status === 404) throw new Error(`研究模板不存在`); + if (!resp.ok) throw new Error(`删除研究模板失败: ${resp.status}`); +} diff --git a/frontend-web/src/open-canvas/components/ai-writing/ResearchSidebar.tsx b/frontend-web/src/open-canvas/components/ai-writing/ResearchSidebar.tsx index c7d0aeec..d464b872 100644 --- a/frontend-web/src/open-canvas/components/ai-writing/ResearchSidebar.tsx +++ b/frontend-web/src/open-canvas/components/ai-writing/ResearchSidebar.tsx @@ -1,7 +1,8 @@ "use client"; -import { BookOpen, FileText, LayoutGrid, Loader2, Plus } from "lucide-react"; +import { BookOpen, FileText, LayoutGrid, Loader2, Plus, Settings2 } from "lucide-react"; import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; import { useQueryClient } from "@tanstack/react-query"; import { Button } from "@/components/ui/button"; @@ -16,15 +17,17 @@ import { createSearchSpace } from "@/core/studio/api"; import { useStudioNotebooks } from "@/core/studio/hooks"; import { cn } from "@/lib/utils"; -import { useArticleTypes } from "../../hooks/useArticleTypes"; +import { useResearchTemplates } from "../../hooks/useResearchTemplates"; +import type { ResearchTemplate } from "../../api/ai-writing-research-templates"; import { useWritingForm } from "../../contexts/WritingFormContext"; +import { useAIWriting } from "../../contexts/AIWritingContext"; /** * 课题研究左栏(需求 0616 #5)。 - * - 上半「研究模板」= 文章类型(``useArticleTypes``);点击预填表单(设置文章类型 + - * 默认字数 + 展开写作配置),沿用现有 articleType 设置逻辑。 - * - 下半「笔记本」= 我的空间(``useStudioNotebooks``);点击设为当前课题研究的素材来源 - * (materialSource='notebook' + 选中该本)。 + * - 上半「研究模板」= 研究模板实体(``useResearchTemplates``);点击一键预填整张写作表单 + * (文章类型 / 大纲 / 搜索来源 / 字数 / 受众 / 严格模式 / 主题)并展开配置。顶部「配置」 + * 入口进入模板配置页维护这些模板。 + * - 下半「笔记本」= 我的空间(``useStudioNotebooks``);点击在右侧内嵌打开该笔记本页。 */ export function ResearchSidebar({ viewingNotebookId, @@ -38,14 +41,25 @@ export function ResearchSidebar({ /** 点击「所有空间」→ 在右侧内嵌打开空间列表页。 */ onOpenAllSpaces: () => void; }) { - const { articleTypes, refresh } = useArticleTypes(); + const navigate = useNavigate(); + const { templates, refresh } = useResearchTemplates(); + const { startWriting } = useAIWriting(); const { notebooks, isLoading: notebooksLoading, isError: notebooksError, } = useStudioNotebooks(); - const { articleType, setArticleType, setWordCountInput, setFormExpanded } = - useWritingForm(); + const { + setIntent, + setArticleType, + setOutlineText, + setShowOutline, + setMaterialSource, + setWordCountInput, + setAudience, + setStrictMode, + setFormExpanded, + } = useWritingForm(); const queryClient = useQueryClient(); // 新建空间弹窗(与 workspace-nav-studio 同款)。 @@ -78,50 +92,86 @@ export function ResearchSidebar({ } }; - const handleTemplateClick = (key: string, defaultWordCount: number) => { - setArticleType(key); - if (defaultWordCount > 0) setWordCountInput(String(defaultWordCount)); + // 点击研究模板 → ①回填整张写作表单(保持写作态左栏显示与运行配置一致) + // ②直接以模板预设进入 AI 写作流程(startWriting)。 + const handleTemplateClick = (tpl: ResearchTemplate) => { + const intent = tpl.topic?.trim() ?? ""; + const articleType = tpl.article_type_key || "analysis"; + const outline = tpl.outline?.trim() || ""; + const audience = tpl.audience?.trim() || "普通读者"; + const wordCount = tpl.word_count > 0 ? tpl.word_count : 800; + const writingMode = tpl.strict_mode ? "strict" : "loose"; + + // 回填表单上下文(写作态左栏据此展示)。 + if (intent) setIntent(intent); + if (tpl.article_type_key) setArticleType(tpl.article_type_key); + if (outline) { + setOutlineText(outline); + setShowOutline(true); + } + setMaterialSource(tpl.material_source); + setWordCountInput(String(wordCount)); + if (tpl.audience?.trim()) setAudience(audience); + setStrictMode(tpl.strict_mode); setFormExpanded(true); + + // 无主题/意图无法发起写作 → 仅回填表单,由用户补充后手动开始。 + if (!intent) return; + void startWriting({ + userIntent: intent, + userOutline: outline || undefined, + articleType, + targetAudience: audience, + wordCountTarget: wordCount, + keywordCount: 1, + writingMode, + materialSource: tpl.material_source, + }); }; return (