diff --git a/frontend-web/src/open-canvas/components/ai-writing/InterventionCard.tsx b/frontend-web/src/open-canvas/components/ai-writing/InterventionCard.tsx index ec9007b2..6f652103 100644 --- a/frontend-web/src/open-canvas/components/ai-writing/InterventionCard.tsx +++ b/frontend-web/src/open-canvas/components/ai-writing/InterventionCard.tsx @@ -252,7 +252,12 @@ function OutlineConfirmCard({ outline: initialOutline, onSubmit, disabled }: Out const text = feedback.trim(); if (!text) return; setSubmitting(true); - await onSubmit({ action: 're_outline', outlineFeedback: text }); + // 带上卡片里的当前大纲,让 AI 在此基础上按要求增删/调整,而非从零重生成 + await onSubmit({ + action: 're_outline', + outlineFeedback: text, + editedOutline: editedOutline ?? undefined, + }); setSubmitting(false); } 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 0140f13c..cb36382a 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 @@ -112,11 +112,16 @@ async def pause_outline_confirm(state: AIWritingState) -> dict: # re_outline feedback = user_input.get("outlineFeedback", "") - return { + updates: dict = { "last_user_intervention": {**user_input, "_feedback": feedback}, "status": "writing_outline", "progress_events": [{"type": "resumed", "agent_name": "系统", "message": f"重新规划大纲({feedback[:30]})"}], } + # 用户在卡片里手动改过大纲 → 以手动版作为本次重规划的编辑基准 + raw_edited = user_input.get("editedOutline") + if raw_edited: + updates["current_outline"] = _normalize_outline(raw_edited) + return updates async def pause_draft_confirm(state: AIWritingState) -> dict: diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/writer_outline.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/writer_outline.py index bd3ad1e6..15f149fc 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/writer_outline.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/writer_outline.py @@ -2,13 +2,16 @@ from __future__ import annotations import asyncio +import json import logging from langchain_core.messages import HumanMessage, SystemMessage from langchain_core.runnables import RunnableConfig from ..state import AIWritingState, Outline, OutlineSection -from ..prompts.writer_prompts import OUTLINE_SYSTEM, OUTLINE_USER +from ..prompts.writer_prompts import ( + OUTLINE_SYSTEM, OUTLINE_USER, OUTLINE_REVISE_SYSTEM, OUTLINE_REVISE_USER, +) from ._utils import get_model, parse_json, _content_to_text, ThinkTagParser logger = logging.getLogger(__name__) @@ -42,20 +45,39 @@ async def writer_outline_node(state: AIWritingState, config: RunnableConfig) -> pkg = state.get("material_package") or {"keywords": [], "materials": [], "summary": ""} intervention = state.get("last_user_intervention") or {} - feedback_section = "" - if intervention.get("action") == "re_outline" and intervention.get("_feedback"): - feedback_section = f"\n重新规划要求:{intervention['_feedback']}" + feedback = "" + if intervention.get("action") == "re_outline": + feedback = intervention.get("_feedback") or intervention.get("outline_feedback") or "" elif intervention.get("outline_feedback"): - feedback_section = f"\n重新规划要求:{intervention['outline_feedback']}" + feedback = intervention.get("outline_feedback") or "" - user_prompt = OUTLINE_USER.format( - user_intent=state["user_intent"], - article_type=state.get("article_type", "analysis"), - target_audience=state.get("target_audience", "普通读者"), - word_count_target=state.get("word_count_target", 800), - materials_summary=_materials_summary(pkg), - feedback_section=feedback_section, - ) + # 重新规划且已有大纲 → 走「编辑模式」:把当前大纲喂给模型,按用户要求调整, + # 而非从零重生成(否则「加一节/删一节」这类指令没有可编辑的基准)。 + current_outline = state.get("current_outline") + is_revise = bool(feedback) and bool(current_outline) + + if is_revise: + system_prompt = OUTLINE_REVISE_SYSTEM + user_prompt = OUTLINE_REVISE_USER.format( + user_intent=state["user_intent"], + article_type=state.get("article_type", "analysis"), + target_audience=state.get("target_audience", "普通读者"), + word_count_target=state.get("word_count_target", 800), + current_outline=json.dumps(current_outline, ensure_ascii=False, indent=2), + feedback=feedback, + materials_summary=_materials_summary(pkg), + ) + else: + system_prompt = OUTLINE_SYSTEM + feedback_section = f"\n重新规划要求:{feedback}" if feedback else "" + user_prompt = OUTLINE_USER.format( + user_intent=state["user_intent"], + article_type=state.get("article_type", "analysis"), + target_audience=state.get("target_audience", "普通读者"), + word_count_target=state.get("word_count_target", 800), + materials_summary=_materials_summary(pkg), + feedback_section=feedback_section, + ) # SSE 流式推送 cfg = config.get("configurable", {}) @@ -70,7 +92,7 @@ async def writer_outline_node(state: AIWritingState, config: RunnableConfig) -> # 留在 try 外会硬崩溃;放进来则统一走下方的 status:error 兜底。 model = get_model(config) async for chunk in model.astream([ - SystemMessage(content=OUTLINE_SYSTEM), + SystemMessage(content=system_prompt), HumanMessage(content=user_prompt), ]): delta = _content_to_text(chunk.content) @@ -98,14 +120,18 @@ async def writer_outline_node(state: AIWritingState, config: RunnableConfig) -> }], } - fallback = { - "title": "未命名文章", - "sections": [ - {"section_title": "引言", "key_points": ["背景介绍"], "material_ids": []}, - {"section_title": "正文", "key_points": ["核心内容"], "material_ids": []}, - {"section_title": "结论", "key_points": ["总结"], "material_ids": []}, - ], - } + # 编辑模式解析失败时回退为「当前大纲」,避免把用户已有大纲弄丢 + if is_revise: + fallback = current_outline + else: + fallback = { + "title": "未命名文章", + "sections": [ + {"section_title": "引言", "key_points": ["背景介绍"], "material_ids": []}, + {"section_title": "正文", "key_points": ["核心内容"], "material_ids": []}, + {"section_title": "结论", "key_points": ["总结"], "material_ids": []}, + ], + } result = parse_json(full_text, fallback) outline = _parse_outline(result) diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/prompts/writer_prompts.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/prompts/writer_prompts.py index 2e98cc73..24fdc38a 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/prompts/writer_prompts.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/prompts/writer_prompts.py @@ -43,6 +43,52 @@ OUTLINE_USER = """写作意图:{user_intent} 请规划文章大纲(3-5个章节),只输出 JSON 格式。""" +# ── 重新规划(基于现有大纲的编辑)───────────────────────────────────────────── + +OUTLINE_REVISE_SYSTEM = """你是一名专业内容创作者(作家)。 +你的目标:根据用户的修改要求,调整一份【已有的文章大纲】。 +你的工作背景:你有十年内容创作经验,擅长精准理解修改意图并落实到大纲结构上。 + +你只能做以下事情: +1. 在「当前大纲」基础上,按「用户修改要求」做调整 +你不能写正文,只输出调整后的大纲。 + +【修改原则】(最高优先级) +- 准确识别并落实用户意图,常见操作:新增章节、删除章节、合并章节、拆分章节、调整章节顺序、修改章节标题、调整章节要点。 +- 只改用户明确提到的部分:用户没要求改动的章节,必须原样保留(标题、要点、material_ids 全部不变)。 +- 新增章节时:紧扣写作意图补充合理章节,并尽量关联可用素材的 material_ids。 +- 章节数量完全由用户要求决定,没有上下限——用户要加就加、要减就减。 + +【严禁思考与表态】(最高优先级) +- 禁止输出任何思考、分析、规划过程,禁止 标签及其中内容。 +- 禁止输出任何对用户说的话;收到输入后立即、直接输出最终 JSON。 + +【要点(key_points)的特殊要求】 +- 每个要点会作为文章里的二级小标题呈现,写成简洁名词性短语,15 字以内,不带句末标点。 +- 每个章节 2-4 个要点。 + +【输出要求】 +- 只输出一个调整后的【完整】大纲 JSON 对象,不要解释文字,不要 markdown 代码块。 +- 必须使用英文双引号 "。 +- material_ids 中的 ID 必须来自给定素材,没有合适素材时填 []。 +- 结构与「当前大纲」一致: +{"title": "...", "sections": [{"section_title": "...", "key_points": ["..."], "material_ids": ["..."]}]}""" + +OUTLINE_REVISE_USER = """写作意图:{user_intent} +文章类型:{article_type} +目标读者:{target_audience} +目标字数:{word_count_target}字 + +当前大纲(JSON): +{current_outline} + +用户修改要求:{feedback} + +可用素材摘要: +{materials_summary} + +请在「当前大纲」基础上落实上述「用户修改要求」,未提到的章节保持原样,输出调整后的完整大纲 JSON。""" + DRAFT_SECTION_SYSTEM = """你是一名专业内容创作者(作家)。 你的目标:根据大纲和素材,写出一段结构清晰、高质量的文章内容。 你的工作背景:你有十年内容创作经验,文字流畅,善于用事实说话。