diff --git a/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx b/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx index fad55ff1..cbf26130 100644 --- a/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx +++ b/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx @@ -3,6 +3,7 @@ import { PenLine, SlidersHorizontal } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; +import { cn } from '@/lib/utils'; import { Select, SelectContent, @@ -67,6 +68,7 @@ export function AIWritingForm({ strictMode, setStrictMode, addReferences, setAddReferences, materialSource, setMaterialSource, + skipResearch, setSkipResearch, // 笔记来源(search_space_id + document_id 列表)已上提到 WritingFormContext, // 以便左栏「笔记本」点击可直接设为素材来源(需求 0616 #5)。 selectedNotebookId, setSelectedNotebookId, @@ -164,13 +166,16 @@ export function AIWritingForm({ : audience.trim() || DEFAULT_AUDIENCE, wordCountTarget: wordCount, keywordCount: DEFAULT_KEYWORD_COUNT, - writingMode: hideAdvancedFields ? 'loose' : strictMode ? 'strict' : 'loose', + // 跳过素材收集时强制 loose(无素材可严格依据,后端也会强制) + writingMode: skipResearch ? 'loose' : hideAdvancedFields ? 'loose' : strictMode ? 'strict' : 'loose', addReferences, - materialSource: materialSource, + skipResearch, + // 跳过素材收集时检索类型无意义,归一为 general + materialSource: skipResearch ? 'general' : materialSource, modelName: selectedModel?.name || modelName || undefined, - // 只有选了"我的空间"才传笔记参数 - noteSearchSpaceId: materialSource === 'notebook' ? (selectedNotebookId ?? undefined) : undefined, - noteSourceIds: materialSource === 'notebook' && selectedNotebookId != null ? selectedDocumentIds : undefined, + // 只有选了"我的空间"且未跳过素材收集才传笔记参数 + noteSearchSpaceId: !skipResearch && materialSource === 'notebook' ? (selectedNotebookId ?? undefined) : undefined, + noteSourceIds: !skipResearch && materialSource === 'notebook' && selectedNotebookId != null ? selectedDocumentIds : undefined, }); } @@ -278,6 +283,25 @@ export function AIWritingForm({ + {/* 不收集素材,直接用通用知识写作 */} +
+ setSkipResearch(e.target.checked)} + disabled={disabled} + className="mt-0.5 size-3.5 shrink-0 cursor-pointer accent-primary" + /> + +
+ + {!skipResearch && (
setStrictMode(e.target.checked)} - disabled={disabled} - className="mt-0.5 size-3.5 shrink-0 cursor-pointer accent-primary" + disabled={disabled || skipResearch} + className="mt-0.5 size-3.5 shrink-0 cursor-pointer accent-primary disabled:cursor-not-allowed" /> -
diff --git a/frontend-web/src/open-canvas/components/ai-writing/AIWritingHistoryView.tsx b/frontend-web/src/open-canvas/components/ai-writing/AIWritingHistoryView.tsx index de75eee7..207b587e 100644 --- a/frontend-web/src/open-canvas/components/ai-writing/AIWritingHistoryView.tsx +++ b/frontend-web/src/open-canvas/components/ai-writing/AIWritingHistoryView.tsx @@ -26,6 +26,7 @@ function normalizeCompletedIntervention(rec: Record): Completed if (rr) out.reviewResult = normalizeReviewResult(rr); if (out.draftTitle == null && rec.draft_title != null) out.draftTitle = rec.draft_title; if (out.draftWordCount == null && rec.draft_word_count != null) out.draftWordCount = rec.draft_word_count; + if (out.userInput == null && rec.user_input != null) out.userInput = rec.user_input; return out as unknown as CompletedIntervention; } diff --git a/frontend-web/src/open-canvas/components/ai-writing/AIWritingTimeline.tsx b/frontend-web/src/open-canvas/components/ai-writing/AIWritingTimeline.tsx index c86a652d..81edb0fb 100644 --- a/frontend-web/src/open-canvas/components/ai-writing/AIWritingTimeline.tsx +++ b/frontend-web/src/open-canvas/components/ai-writing/AIWritingTimeline.tsx @@ -1,6 +1,7 @@ import type { CompletedIntervention, ProgressEvent } from '../../types/ai-writing'; import { TimelineStep } from './TimelineStep'; import { CompletedInterventionCard } from './CompletedInterventionCard'; +import { UserRequirementCard } from './UserRequirementCard'; interface AIWritingTimelineProps { events: ProgressEvent[]; @@ -48,8 +49,13 @@ export function AIWritingTimeline({ awaitUserIdx += 1; if (completedRecord) { - // 已完成的干预点 → 渲染折叠历史卡 - slot = ; + // 已完成的干预点 → 先以独立样式回显用户的输入要求,再渲染折叠历史卡 + slot = ( +
+ + +
+ ); } else if (currentPause && isLast) { // 当前活跃干预点 → 渲染操作卡 slot = currentInterventionSlot; diff --git a/frontend-web/src/open-canvas/components/ai-writing/UserRequirementCard.tsx b/frontend-web/src/open-canvas/components/ai-writing/UserRequirementCard.tsx new file mode 100644 index 00000000..3ed3b5b9 --- /dev/null +++ b/frontend-web/src/open-canvas/components/ai-writing/UserRequirementCard.tsx @@ -0,0 +1,50 @@ +import { UserRound } from 'lucide-react'; +import type { CompletedIntervention } from '../../types/ai-writing'; + +/** + * 时间线里「用户输入交互的要求」独立样式卡 —— 与 AI 步骤(灰)、完成卡(绿)、 + * 等待卡(黄)在视觉上明确区分:靛蓝主色 + 用户图标 + 顶部要求类型标签。 + * + * 触发场景(record.userInput 非空时): + * 素材确认 → 按用户要求补充检索内容 + * 大纲确认 → 用户提出大纲修改意见 + * 草稿确认 → 用户提出写作修改意见 + * 编辑意见处理 → 用户在编辑审核时提出处理意见 + */ + +function requirementLabel(record: CompletedIntervention): string { + switch (record.pausePoint) { + case 'material_confirm': + return '补充检索内容'; + case 'outline_confirm': + return '修改大纲意见'; + case 'draft_confirm': + return '修改写作意见'; + case 'review_confirm': + return '编辑审核意见'; + default: + return '我的要求'; + } +} + +interface Props { + record: CompletedIntervention; +} + +export function UserRequirementCard({ record }: Props) { + if (!record.userInput) return null; + + return ( +
+
+ + + 我的要求 · {requirementLabel(record)} + +
+

+ {record.userInput} +

+
+ ); +} diff --git a/frontend-web/src/open-canvas/components/ai-writing/WritingSetupChat.tsx b/frontend-web/src/open-canvas/components/ai-writing/WritingSetupChat.tsx index adbdc80e..746b03af 100644 --- a/frontend-web/src/open-canvas/components/ai-writing/WritingSetupChat.tsx +++ b/frontend-web/src/open-canvas/components/ai-writing/WritingSetupChat.tsx @@ -15,6 +15,8 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { Switch } from '@/components/ui/switch'; +import { Zap } from 'lucide-react'; import { ArtifactsProvider } from '@/components/workspace/artifacts'; import { MessageList, @@ -59,6 +61,8 @@ export function WritingSetupChat({ const threadId = useMemo(() => uuid(), []); const [threadPrepared, setThreadPrepared] = useState(false); const [chatModel, setChatModel] = useState(''); + // 快捷模式:开启后跳过逐项选项澄清(文章类型/字数/读者用默认),给标题直接生成大纲。 + const [quickMode, setQuickMode] = useState(false); const { models, isLoading: modelsLoading } = useModels(); const { articleTypes } = useArticleTypes(); // 缺省回填写作表单;onApprove 传入时脱离 WritingFormProvider(如模板创建页)。 @@ -85,6 +89,7 @@ export function WritingSetupChat({ context: { mode: 'flash', is_writing_setup: true, + writing_setup_quick: quickMode, valid_article_types: validArticleTypes, model_name: chatModel || undefined, }, @@ -133,7 +138,8 @@ export function WritingSetupChat({ typeof payload.word_count_target === 'number' && payload.word_count_target > 0 ? payload.word_count_target : Math.max(100, parseInt(formCtx?.wordCountInput ?? '', 10) || 800); - const materialSource = formCtx?.materialSource ?? 'general'; + const skipResearch = formCtx?.skipResearch ?? false; + const materialSource = skipResearch ? 'general' : (formCtx?.materialSource ?? 'general'); const request: AIWritingRequest = { userIntent: payload.user_intent?.trim() || formCtx?.intent?.trim() || '', @@ -145,9 +151,11 @@ export function WritingSetupChat({ targetAudience: payload.target_audience?.trim() || formCtx?.audience?.trim() || '普通读者', wordCountTarget: wordCount, keywordCount: 1, - writingMode: payload.writing_mode === 'strict' ? 'strict' : 'loose', + // 跳过素材收集时强制 loose + writingMode: skipResearch ? 'loose' : payload.writing_mode === 'strict' ? 'strict' : 'loose', // 跟随表单的「添加参考文献」开关(默认开)。 addReferences: formCtx?.addReferences ?? true, + skipResearch, materialSource, modelName: chatModel || formCtx?.modelName || undefined, noteSearchSpaceId: @@ -258,9 +266,25 @@ export function WritingSetupChat({ )} - - 输入想法,按 Enter 直接解析填充 - + {/* 快捷模式开关:放在模型选择右侧。开启后跳过逐项选项确认,给标题直接出大纲。 */} + void handleChatSubmit(text)}> void; materialSource: 'general' | 'notebook'; setMaterialSource: (v: 'general' | 'notebook') => void; + /** 不收集素材,直接用通用知识写作:勾选后跳过素材检索阶段(含素材确认暂停点)。默认关。 */ + skipResearch: boolean; + setSkipResearch: (v: boolean) => void; /** 我的空间(笔记本)来源:选中的 search_space_id;上提到 Context 以便左栏「笔记本」点击直接设为素材来源。 */ selectedNotebookId: number | null; setSelectedNotebookId: (v: number | null) => void; @@ -86,6 +89,7 @@ export function WritingFormProvider({ const [strictMode, setStrictMode] = useState(false); const [addReferences, setAddReferences] = useState(true); const [materialSource, setMaterialSource] = useState<'general' | 'notebook'>(initialMaterialSource ?? 'general'); + const [skipResearch, setSkipResearch] = useState(false); const [selectedNotebookId, setSelectedNotebookId] = useState(initialNotebookId ?? null); const [selectedDocumentIds, setSelectedDocumentIds] = useState(initialDocumentIds ?? []); const [modelName, setModelName] = useState(''); @@ -123,12 +127,13 @@ export function WritingFormProvider({ strictMode, setStrictMode, addReferences, setAddReferences, materialSource, setMaterialSource, + skipResearch, setSkipResearch, selectedNotebookId, setSelectedNotebookId, selectedDocumentIds, setSelectedDocumentIds, modelName, setModelName, applyProposal, }), - [intent, formExpanded, showOutline, outlineText, articleType, wordCountInput, audience, strictMode, addReferences, materialSource, selectedNotebookId, selectedDocumentIds, modelName, applyProposal], + [intent, formExpanded, showOutline, outlineText, articleType, wordCountInput, audience, strictMode, addReferences, materialSource, skipResearch, selectedNotebookId, selectedDocumentIds, modelName, applyProposal], ); return {children}; diff --git a/frontend-web/src/open-canvas/hooks/useAIWritingStream.ts b/frontend-web/src/open-canvas/hooks/useAIWritingStream.ts index 143f72fd..0795b7b7 100644 --- a/frontend-web/src/open-canvas/hooks/useAIWritingStream.ts +++ b/frontend-web/src/open-canvas/hooks/useAIWritingStream.ts @@ -705,6 +705,8 @@ export type AIWritingValues = { article_type_detail?: string; /** 是否加参考文献(正文 [n] 角标 + 文末「参考文献」列表)。 */ add_references?: boolean; + /** 不收集素材,直接用通用知识写作(researcher 节点据此短路跳过检索)。 */ + skip_research?: boolean; status?: string; material_package?: unknown; current_outline?: unknown; diff --git a/frontend-web/src/open-canvas/pages/ResearchTemplateEditorPage.tsx b/frontend-web/src/open-canvas/pages/ResearchTemplateEditorPage.tsx index adcdf976..efc1cb8d 100644 --- a/frontend-web/src/open-canvas/pages/ResearchTemplateEditorPage.tsx +++ b/frontend-web/src/open-canvas/pages/ResearchTemplateEditorPage.tsx @@ -143,7 +143,7 @@ export default function ResearchTemplateEditorPage() { const payload = toPayload(form); if (editing && id) await update(id, payload); else await create(payload); - navigate(LIST_PATH); + navigate(LIST_PATH, { replace: true }); } catch (e) { setFormError(e instanceof Error ? e.message : '保存失败'); setSaving(false); @@ -154,7 +154,7 @@ export default function ResearchTemplateEditorPage() {
-
@@ -168,7 +168,7 @@ export default function ResearchTemplateEditorPage() {
{formError && {formError}} -
diff --git a/frontend-web/src/open-canvas/types/ai-writing.ts b/frontend-web/src/open-canvas/types/ai-writing.ts index 407d6535..151e132a 100644 --- a/frontend-web/src/open-canvas/types/ai-writing.ts +++ b/frontend-web/src/open-canvas/types/ai-writing.ts @@ -267,6 +267,9 @@ export interface AIWritingRequest { addReferences?: boolean; /** 检索类型:general=通用检索(素材收集专家按配置技能检索,默认知识库检索技能)/ notebook=我的空间 */ materialSource: "general" | "notebook"; + /** 不收集素材,直接用通用知识写作:勾选后跳过整个素材检索流程(含素材确认暂停点), + * 直接进入大纲规划;后端强制 loose 写作模式。默认 false。 */ + skipResearch?: boolean; /** 笔记来源(可选):空间 search_space_id */ noteSearchSpaceId?: number; /** 笔记来源(可选):空间内指定文档的 document_id 列表,空数组表示检索整个空间 */ @@ -316,6 +319,10 @@ export interface CompletedIntervention { | "review_confirm" | "section_help"; action: string; + /** 用户在该干预点输入的自然语言要求(补充检索内容 / 大纲修改意见 / 写作修改意见 / + * 编辑审核处理意见)。仅当用户实际输入了文字时存在,用于在时间线里以「我的要求」 + * 独立样式回显——区别于 AI 步骤与绿色完成卡。 */ + userInput?: string; // material_confirm materials?: AIMaterial[]; // outline_confirm diff --git a/offline-backend-20260512/backend/app/gateway/services.py b/offline-backend-20260512/backend/app/gateway/services.py index 761ee3eb..1b09e2be 100644 --- a/offline-backend-20260512/backend/app/gateway/services.py +++ b/offline-backend-20260512/backend/app/gateway/services.py @@ -239,6 +239,7 @@ _CONTEXT_CONFIGURABLE_KEYS: frozenset[str] = frozenset( "is_bootstrap", "is_writing_setup", "valid_article_types", + "writing_setup_quick", "excluded_tools", "skill_stop_names", "skill_stop_message", diff --git a/offline-backend-20260512/backend/config.yaml b/offline-backend-20260512/backend/config.yaml index 01729927..7a3e3a0b 100644 --- a/offline-backend-20260512/backend/config.yaml +++ b/offline-backend-20260512/backend/config.yaml @@ -10,18 +10,18 @@ gateway: port: 8001 models: - # - name: deepseek-chat - # display_name: DeepSeek Chat - # use: langchain_openai:ChatOpenAI - # model: deepseek-chat - # api_key: sk-35345154983047aba9e12faa86c5f62c - # base_url: https://api.deepseek.com - # request_timeout: 600.0 - # max_retries: 2 - # max_tokens: 4096 - # temperature: 1.0 - # supports_vision: false - # supports_thinking: true + - name: deepseek-chat + display_name: DeepSeek Chat + use: langchain_openai:ChatOpenAI + model: deepseek-chat + api_key: sk-686b7bc000be4b7cbed6dccb3897c411 + base_url: https://api.deepseek.com + request_timeout: 600.0 + max_retries: 2 + max_tokens: 4096 + temperature: 1.0 + supports_vision: false + supports_thinking: true - name: zai-org/GLM-5-FP8 display_name: zai-org/GLM-5-FP8 diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/graph.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/graph.py index d09bcbbe..e79d1af3 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/graph.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/graph.py @@ -62,6 +62,17 @@ async def pause_material_confirm(state: AIWritingState) -> dict: approved_ids = set(user_input.get("approvedMaterialIds") or [m["id"] for m in pkg["materials"]]) filtered = [m for m in pkg["materials"] if m["id"] in approved_ids] updated_pkg = {**pkg, "materials": filtered} + # 用户自带大纲:researcher 已在「搜集素材」阶段解析大纲并归类素材 → 跳过大纲生成步, + # 仅把被用户取消勾选的素材从各章节 material_ids 里剔除,直接进入大纲确认。 + prebuilt = state.get("current_outline") + if prebuilt and (state.get("user_outline_raw") or "").strip(): + return { + "material_package": updated_pkg, + "current_outline": _prune_outline_material_ids(prebuilt, approved_ids), + "last_user_intervention": user_input, + "status": "awaiting_outline_confirm", + "progress_events": [{"type": "resumed", "agent_name": "系统", "message": "已采用你提供的大纲并归类素材,请确认大纲"}], + } return { "material_package": updated_pkg, "last_user_intervention": user_input, @@ -90,6 +101,21 @@ async def pause_material_confirm(state: AIWritingState) -> dict: } +def _prune_outline_material_ids(outline: dict, approved_ids: set) -> dict: + """把大纲各章节 material_ids 里、用户在素材确认卡取消勾选的素材编号剔除掉。 + + 用户自带大纲时素材归类发生在「搜集素材」阶段(researcher),早于素材确认; + 用户在确认卡里去掉某些素材后,用这个函数把对应编号从大纲剔除,保持一致。 + """ + if not outline: + return outline + sections = [] + for s in outline.get("sections", []): + kept = [mid for mid in (s.get("material_ids") or []) if mid in approved_ids] + sections.append({**s, "material_ids": kept}) + return {**outline, "sections": sections} + + def _normalize_outline(outline: dict) -> dict: """将前端回传的 camelCase 大纲统一为后端 snake_case 格式。""" if not outline: @@ -318,7 +344,13 @@ def after_researcher(state: AIWritingState) -> str: def after_material_pause(state: AIWritingState) -> str: - return "researcher" if state["status"] == "researching" else "writer_outline" + status = state["status"] + if status == "researching": + return "researcher" + # 用户自带大纲:researcher 已在搜集阶段整理好大纲 → 跳过 writer_outline 生成步,直接确认大纲。 + if status == "awaiting_outline_confirm": + return "pause_outline" + return "writer_outline" def after_outline_pause(state: AIWritingState) -> str: @@ -396,9 +428,9 @@ def make_ai_writing_graph(config=None): g.set_entry_point("intent_parser") g.add_edge("intent_parser", "researcher") g.add_conditional_edges("researcher", after_researcher, - {"pause_material": "pause_material", "writer_draft": "writer_draft"}) + {"pause_material": "pause_material", "writer_draft": "writer_draft", "writer_outline": "writer_outline"}) g.add_conditional_edges("pause_material", after_material_pause, - {"researcher": "researcher", "writer_outline": "writer_outline"}) + {"researcher": "researcher", "writer_outline": "writer_outline", "pause_outline": "pause_outline"}) g.add_edge("writer_outline", "pause_outline") g.add_conditional_edges("pause_outline", after_outline_pause, {"writer_outline": "writer_outline", "writer_draft": "writer_draft"}) diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py index 95ec3f9f..26047a51 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py @@ -399,6 +399,22 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict def _emit(message: str) -> None: writer({"type": "research_progress", "message": message}) + # 跳过素材收集:用户在初始表单勾选「不收集素材,直接用通用知识写作」。 + # 仅在**首次**进入(无任何 pause 干预 = intervention 为空)时短路,避免后续 + # 「补充检索 / 按输入追加」等再入研究节点时被误跳过。直接产出空素材包、强制 + # loose 写作模式(无素材可严格依据),并跳过素材确认暂停点直奔大纲规划。 + if state.get("skip_research") and not intervention: + empty_pkg = MaterialPackage(keywords=[], materials=[], summary="用户选择不收集素材,直接用通用知识写作") + return { + "material_package": empty_pkg, + "writing_mode": "loose", + "status": "writing_outline", + "progress_events": [ + {"type": "step_start", "agent_name": researcher_label, "message": "已跳过素材收集,直接用通用知识写作"}, + {"type": "materials_ready", "agent_name": researcher_label, "message": "未收集素材(通用知识写作)"}, + ], + } + # Step 1: 提取关键词 kw_result: dict = {} # 走"用户输入 / 追加"分支时跳过 LLM 调用,notice 拼接需要兜底值 if is_user_query_search: @@ -856,13 +872,36 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict step_start_msg = f"通用检索(技能编排 {len(skill_steps)} 步):{', '.join(keywords[:3])}" else: step_start_msg = f"搜索关键词:{', '.join(keywords[:3])}" + + progress_events: list[dict] = [ + {"type": "step_start", "agent_name": researcher_label, "message": step_start_msg}, + *notices, + {"type": "materials_ready", "agent_name": researcher_label, "message": summary}, + ] + extra_updates: dict = {} + # 用户自带大纲:在「搜集素材」阶段顺便解析大纲并把素材归类到各章节, + # 从而跳过 writer_outline 的大纲生成步(见 graph after_material_pause 路由)。 + # 补充检索(is_supplement)直接回写作、沿用原大纲,不在此整理。 + if not is_supplement and (state.get("user_outline_raw") or "").strip(): + from .writer_outline import build_user_outline_with_materials + + try: + organized = await build_user_outline_with_materials(state, config, pkg) + except Exception: + logger.warning("researcher: 整理用户大纲失败(非致命),回退到大纲生成步", exc_info=True) + organized = None + if organized: + extra_updates["current_outline"] = organized + progress_events.append({ + "type": "outline_ready", + "agent_name": researcher_label, + "message": f"已按你提供的大纲整理素材,共 {len(organized.get('sections', []))} 个章节", + }) + return { "material_package": pkg, # 补充检索:直接回写作(沿用原大纲);常规检索:进入素材确认暂停点 "status": "writing_draft" if is_supplement else "awaiting_material_confirm", - "progress_events": [ - {"type": "step_start", "agent_name": researcher_label, "message": step_start_msg}, - *notices, - {"type": "materials_ready", "agent_name": researcher_label, "message": summary}, - ], + **extra_updates, + "progress_events": progress_events, } diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/state.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/state.py index 34e3535a..71156c0f 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/state.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/state.py @@ -144,6 +144,10 @@ class AIWritingState(TypedDict): # notebook=我的空间(仅笔记检索)。旧值 knowledge_base / skill 由 researcher # 节点按 general 兼容处理。 material_source: str + # 跳过素材收集:用户在初始表单勾选「不收集素材,直接用通用知识写作」时为 True。 + # researcher 节点在**首次**进入(无任何 pause 干预)时短路,跳过整个检索流程直接 + # 去大纲规划;强制 loose 写作模式(没有素材可严格依据)。 + skip_research: bool # 用户在写作表单选定的主模型。大纲/写作/审核严格使用它(不回退默认)。 # 由 intent_parser 在首次 invocation 从 configurable.model_name 捕获进 state, # 经 checkpoint 持久化 —— interrupt/resume 是新 invocation、其 config 不带原 diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/agent.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/agent.py index faf6abf4..90ab19ec 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/agent.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/agent.py @@ -437,6 +437,8 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig): # 走写作配置专用 prompt,只负责帮用户把写作表单聊清并提交,不真正开始写作。 is_writing_setup = cfg.get("is_writing_setup", False) writing_setup_article_types = cfg.get("valid_article_types") if is_writing_setup else None + # 快捷模式:跳过逐项选项澄清,给标题直接出大纲(见 build_writing_setup_prompt)。 + writing_setup_quick = bool(cfg.get("writing_setup_quick", False)) if is_writing_setup else False memory_injection_enabled = bool(cfg.get("memory_injection_enabled", True)) rag_mode_enabled = bool(cfg.get("rag_mode_enabled", False)) writing_mode = bool(cfg.get("writing_mode", False)) @@ -522,7 +524,7 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig): *_build_middlewares(config, model_name=model_name, app_config=resolved_app_config), WritingSetupRoundCapMiddleware(max_rounds=WRITING_SETUP_MAX_ROUNDS), ], - system_prompt=build_writing_setup_prompt(writing_setup_article_types), + system_prompt=build_writing_setup_prompt(writing_setup_article_types, quick_mode=writing_setup_quick), state_schema=ThreadState, ) diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/prompt.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/prompt.py index e74190ca..b1c1c900 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/prompt.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/prompt.py @@ -1116,13 +1116,17 @@ def _build_bootstrap_section() -> str: """ -def build_writing_setup_prompt(article_types: list[dict] | None = None) -> str: +def build_writing_setup_prompt(article_types: list[dict] | None = None, *, quick_mode: bool = False) -> str: """写作配置 Q&A 的**独立**系统提示(不复用 lead-agent 超级体提示)。 关键:写作配置助手绝不能像 lead agent 那样「自己把任务做掉」——用户输入常常长得像一个 可执行任务(如「新能源汽车市场分析」),若复用 lead 提示 + 全套工具,模型会直接去研究/写 分析。这里用一份职责单一、口吻强硬的独立提示,配合「只挂 setup_writing 一个工具」,把模型 死死框在「聊清需求 → 调 setup_writing」上。 + + ``quick_mode``(快捷模式):跳过「逐项选项澄清」——文章类型/字数/读者等一律取合理默认, + **不调用 ``ask_clarification``**,用户一给出标题/主题就**直接生成大纲**;大纲仍交用户确认, + 确认后再调 ``setup_writing`` 提交。供前端「快捷模式」开关使用。 """ if article_types: type_lines = "\n".join( @@ -1141,18 +1145,33 @@ def build_writing_setup_prompt(article_types: list[dict] | None = None) -> str: "`article_type_detail` 里用一句话说明其写作侧重;不确定可留空,交给用户在表单里选。" ) + if quick_mode: + workflow_block = """【工作流程——快捷模式】 +1. **不澄清、用默认**:**禁止**调用 `ask_clarification`。文章类型、目标字数、目标读者等一律自行取**合理默认**(字数默认 800,读者默认「普通读者」,文章类型按意图判断),**绝不**就这些选项反问用户。 +2. **拿到标题/主题就直接出大纲**:用户一给出标题或写作主题,**立刻**以**普通回复文本**输出一份**多级层级**的中文大纲——用 Markdown 标题层级(`#` 一级、`##` 二级,必要时 `###` 三级),每节可用**一句话**点明这一节要写什么,但**绝不写正文**;大纲规模与默认字数匹配。**在回复的最后一句**明确请用户确认,例如:「以上是建议大纲,你看是否需要调整?确认后我就整理写作配置、开始写作。」这一步**不要调用任何工具**,输出完大纲就结束本轮、等待用户回应。 +3. **按反馈灵活改大纲**:用户要求调整(增删章节、改层级、换角度等)就**重新输出修订后的完整大纲**,并再次在最后一句请其确认;如此反复,直到用户表示满意/确认。 +4. **提交配置**:用户确认大纲后,调用 `setup_writing` 提交:""" + else: + workflow_block = """【工作流程】 +1. **聊清需求**:读用户输入,当关键项(文章类型、目标字数、目标读者)缺失或模糊时,调用 `ask_clarification` 提问——**优先给出 `options` 选项**让用户点选(如目标字数「800 / 1500 / 3000」、读者「普通读者 / 行业专家 / 政策决策者」)。澄清**最多 2 次**,信息够了就进入下一步,不要无休止追问。 +2. **生成大纲(先于确认意图)**:信息足够后,直接以**普通回复文本**输出一份**多级层级**的中文大纲——用 Markdown 标题层级(`#` 一级、`##` 二级,必要时 `###` 三级),每节可用**一句话**点明这一节要写什么,但**绝不写正文**;大纲规模与目标字数匹配。**在回复的最后一句**明确请用户确认,例如:「以上是建议大纲,你看是否需要调整?确认后我就整理写作配置、开始写作。」这一步**不要调用任何工具**,输出完大纲就结束本轮、等待用户回应。 +3. **按反馈灵活改大纲**:用户要求调整(增删章节、改层级、换角度等)就**重新输出修订后的完整大纲**,并再次在最后一句请其确认;如此反复,直到用户表示满意/确认。用户若只是补充信息也照常更新大纲再确认。 +4. **提交配置**:用户确认大纲后,调用 `setup_writing` 提交:""" + + quick_law = ( + "\n- **快捷模式**:禁止调用 `ask_clarification` 反问选项;缺失项一律取合理默认,用户给出标题/主题后**立刻生成大纲**。" + if quick_mode + else "" + ) + return f"""你是「AI 写作」功能的**配置助手**。你的职责分两段:① 先与用户把写作需求聊清楚,**先生成并与用户确认一份大纲**;② 待用户对大纲满意后,再调用 `setup_writing` 工具提交写作配置(含已确认的大纲),交用户开始写作。 【铁律——最高优先级】 - 你**绝对不撰写**文章正文、分析内容或任何成稿文字。即使用户的输入看起来就是一个写作任务(例如「新能源汽车市场分析」「帮我写一篇……」),你也**只产出「大纲」(仅结构与标题)与「写作配置」**,绝不替用户动笔、绝不直接完成这篇写作。 - 你只有两个工具:① `ask_clarification`(向用户提问澄清,需要时用);② `setup_writing`(提交写作配置)。**没有任何其它工具**,不要做检索、不要做研究、不要假装调用别的工具。 -- `setup_writing` **只能在用户明确确认大纲之后**才调用;在那之前绝不调用它。 +- `setup_writing` **只能在用户明确确认大纲之后**才调用;在那之前绝不调用它。{quick_law} -【工作流程】 -1. **聊清需求**:读用户输入,当关键项(文章类型、目标字数、目标读者)缺失或模糊时,调用 `ask_clarification` 提问——**优先给出 `options` 选项**让用户点选(如目标字数「800 / 1500 / 3000」、读者「普通读者 / 行业专家 / 政策决策者」)。澄清**最多 2 次**,信息够了就进入下一步,不要无休止追问。 -2. **生成大纲(先于确认意图)**:信息足够后,直接以**普通回复文本**输出一份**多级层级**的中文大纲——用 Markdown 标题层级(`#` 一级、`##` 二级,必要时 `###` 三级),每节可用**一句话**点明这一节要写什么,但**绝不写正文**;大纲规模与目标字数匹配。**在回复的最后一句**明确请用户确认,例如:「以上是建议大纲,你看是否需要调整?确认后我就整理写作配置、开始写作。」这一步**不要调用任何工具**,输出完大纲就结束本轮、等待用户回应。 -3. **按反馈灵活改大纲**:用户要求调整(增删章节、改层级、换角度等)就**重新输出修订后的完整大纲**,并再次在最后一句请其确认;如此反复,直到用户表示满意/确认。用户若只是补充信息也照常更新大纲再确认。 -4. **提交配置**:用户确认大纲后,调用 `setup_writing` 提交: +{workflow_block} - `user_intent`:精炼成一句话的写作目标(必填)。 - `article_type`:见下方【文章类型】——优先选已有类型的 key;都不贴切才自拟一个新的中文类型名。 - `article_type_detail`:**仅当 article_type 是新类型时**,用一句话说明该类型的写作侧重/特征;命中已有类型则留空。