diff --git a/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx b/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx index 2b108e3e..01800c52 100644 --- a/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx +++ b/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx @@ -400,6 +400,47 @@ export function ArtifactFileDetail({ } }, [displayContent, filepath]); + const handleOpenWriteFileInNewTab = useCallback(() => { + const source = displayContent ?? ""; + if (!source.trim()) { + toast.error("文件内容为空,暂时无法打开"); + return; + } + const blobType = + language === "html" + ? "text/html;charset=utf-8" + : language === "markdown" + ? "text/markdown;charset=utf-8" + : "text/plain;charset=utf-8"; + const blob = new Blob([source], { type: blobType }); + const url = URL.createObjectURL(blob); + window.open(url, "_blank", "noopener,noreferrer"); + window.setTimeout(() => URL.revokeObjectURL(url), 60_000); + }, [displayContent, language]); + + const handleDownloadWriteFile = useCallback(() => { + const source = displayContent ?? ""; + if (!source.trim()) { + toast.error("文件内容为空,暂时无法下载"); + return; + } + const blobType = + language === "html" + ? "text/html;charset=utf-8" + : language === "markdown" + ? "text/markdown;charset=utf-8" + : "text/plain;charset=utf-8"; + const blob = new Blob([source], { type: blobType }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = getFileName(filepath); + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }, [displayContent, filepath, language]); + return ( @@ -500,30 +541,32 @@ export function ArtifactFileDetail({ /> )} - {!isWriteFile && ( - { - try { - await openArtifactInNewTab({ - filepath, - threadId, - isMock, - publicShareCode, - }); - } catch (error) { - console.error(error); - toast.error( - error instanceof Error - ? error.message - : "Failed to open artifact", - ); - } - }} - /> - )} + { + if (isWriteFile) { + handleOpenWriteFileInNewTab(); + return; + } + try { + await openArtifactInNewTab({ + filepath, + threadId, + isMock, + publicShareCode, + }); + } catch (error) { + console.error(error); + toast.error( + error instanceof Error + ? error.message + : "Failed to open artifact", + ); + } + }} + /> {isCodeFile && ( )} - {!isWriteFile && ( - { - try { - await downloadArtifactFile({ - filepath, - threadId, - isMock, - publicShareCode, - }); - } catch (error) { - console.error(error); - toast.error( - error instanceof Error - ? error.message - : "Download failed", - ); - } - }} - /> - )} + { + if (isWriteFile) { + handleDownloadWriteFile(); + return; + } + try { + await downloadArtifactFile({ + filepath, + threadId, + isMock, + publicShareCode, + }); + } catch (error) { + console.error(error); + toast.error( + error instanceof Error + ? error.message + : "Download failed", + ); + } + }} + /> 0 ? (
- {allowMultiple ? "可多选(再次点击取消选择)" : "请选择一项(点击即发送)"} + {allowMultiple + ? "可多选(再次点击取消选择)" + : multiPending + ? "请选择一项" + : "请选择一项(点击即发送)"}
{options.map((option) => { const optionId = option.id || option.label; @@ -956,7 +965,7 @@ function ClarificationCard({ @@ -1001,10 +1010,10 @@ function ClarificationCard({
) : null} - {isPending && (allowMultiple || payload.allow_custom !== false) ? ( + {isPending && (allowMultiple || multiPending || payload.allow_custom !== false) ? ( isLastPending ? (
- {multiPending && allowMultiple ? ( + {multiPending ? ( 将一起提交以上 {pendingCount} 个问题的回答 @@ -1016,10 +1025,10 @@ function ClarificationCard({ onClick={() => void onSubmitAll()} > - {submitting ? "发送中…" : multiPending && allowMultiple ? "全部发送" : "发送回答"} + {submitting ? "发送中…" : multiPending ? "全部发送" : "发送回答"}
- ) : allowMultiple ? ( + ) : allowMultiple || multiPending ? (
选择后等待最后一题统一提交
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 c64b18a8..be865ea4 100644 --- a/frontend-web/src/open-canvas/components/ai-writing/WritingSetupChat.tsx +++ b/frontend-web/src/open-canvas/components/ai-writing/WritingSetupChat.tsx @@ -77,16 +77,24 @@ export function WritingSetupChat() { setThreadPrepared(true); }, [threadId, threadPrepared]); + // 生成中点提交按钮 = 暂停:取消当前 SSE 流(与主聊天页一致)。 const handleChatSubmit = useCallback( async (text: string) => { + if (thread.isLoading) { + void thread.stop(); + return; + } const trimmed = text.trim(); - if (!trimmed || thread.isLoading) return; + if (!trimmed) return; await ensureThreadExists(); await sendMessage(threadId, { text: trimmed, files: [] }); }, - [ensureThreadExists, sendMessage, thread.isLoading, threadId], + [thread, ensureThreadExists, sendMessage, threadId], ); + // 提交按钮状态:生成中显示「停止」方块,可点击暂停对话 + const submitStatus = thread.error ? 'error' : thread.isLoading ? 'streaming' : 'ready'; + const handleApprove = useCallback( async (payload: SetupWritingApprovalPayload) => { applyProposal(payload); @@ -150,10 +158,9 @@ export function WritingSetupChat() { void handleChatSubmit(text)}> - + 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 d3f3e48c..ae1398fe 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 @@ -377,7 +377,7 @@ def make_lead_agent(config: RunnableConfig): def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig): # Lazy import to avoid circular dependency from deerflow.tools import get_available_tools - from deerflow.tools.builtins import setup_agent, setup_writing + from deerflow.tools.builtins import ask_clarification_tool, setup_agent, setup_writing cfg = _get_runtime_config(config) resolved_app_config = app_config @@ -464,16 +464,16 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig): ) if is_writing_setup: - # 写作配置 Q&A:最小化 agent —— **只挂 setup_writing 一个工具** + 一份职责单一的独立提示。 + # 写作配置 Q&A:最小化 agent —— 只挂 setup_writing(提交配置)+ ask_clarification(弹澄清卡)。 # 关键:绝不给全套工具、绝不复用 lead 超级体提示,否则模型会把「新能源汽车市场分析」之类 # 长得像任务的输入直接做掉,而不是转成写作配置提交。 - # 末尾追加 WritingSetupRoundCapMiddleware:澄清最多 3 轮,到顶强制提交配置卡。 + # 末尾追加 WritingSetupRoundCapMiddleware:澄清最多 3 轮,到顶用 tool_choice 强制提交配置卡。 from deerflow.agents.lead_agent.prompt import build_writing_setup_prompt from deerflow.agents.middlewares.writing_setup_round_cap_middleware import WritingSetupRoundCapMiddleware return create_agent( model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, app_config=resolved_app_config), - tools=[setup_writing], + tools=[setup_writing, ask_clarification_tool], middleware=[ *_build_middlewares(config, model_name=model_name, app_config=resolved_app_config), WritingSetupRoundCapMiddleware(max_rounds=WRITING_SETUP_MAX_ROUNDS), 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 914d30f7..cea2a124 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 @@ -924,11 +924,11 @@ def build_writing_setup_prompt(article_types: list[dict] | None = None) -> str: 【铁律——最高优先级】 - 你**绝对不撰写**文章正文、分析内容、大纲内容或任何成稿文字。即使用户的输入看起来就是一个写作任务(例如「新能源汽车市场分析」「帮我写一篇……」),你也**只把它当作"要配置的写作目标"**,绝不替用户动笔、绝不直接完成这篇写作。 -- 你能做的动作只有两个:① 用自然语言向用户提问或确认;② 调用 `setup_writing` 工具。**除此之外你没有任何其它工具**,也不要假装调用别的工具、不要做检索、不要做研究。 +- 你只有两个工具:① `ask_clarification`(向用户提问澄清,需要时用);② `setup_writing`(提交写作配置)。**没有任何其它工具**,不要做检索、不要做研究、不要假装调用别的工具。 - 不要在回复里输出文章段落、要点清单或大纲(用户主动粘贴的大纲除外——原样放进 `outline` 参数)。 【工作流程】(与用户交互确认,但最多 3 轮内必须提交) -1. 读用户输入,先用一两句话回应并和用户**确认写作意图**;就缺失或模糊的关键项(文章类型、目标字数、目标读者)提问澄清,一次最多问 1-2 个最关键的问题。 +1. 读用户输入。当关键项(文章类型、目标字数、目标读者)缺失或模糊时,调用 `ask_clarification` 向用户提问——**优先给出 `options` 选项**让用户点选(例如目标字数给「800 / 1500 / 3000」,读者给「普通读者 / 行业专家 / 政策决策者」)。一次可就 1-2 个独立的关键项各发一个 ask_clarification。 2. 与用户的澄清来回**最多 2 次**。一旦关键信息明确、或已经问过 2 次,就**立即进入第 3 步提交**,缺失字段用合理默认值,绝不无休止追问。 3. 调用 `setup_writing` 提交配置: - `user_intent`:精炼成一句话的写作目标(必填)。 @@ -936,7 +936,7 @@ def build_writing_setup_prompt(article_types: list[dict] | None = None) -> str: - `word_count_target`:正整数;用户没明说就给合理默认(如 800)。 - `target_audience`:如「普通读者」「行业专家」;没提就填「普通读者」。 - `writing_mode`:用户强调「严格依据素材/不要编造」→ `strict`,否则 `loose`。 - - `material_source`:用户提到用「技能/技能检索」→ `skill`,否则 `knowledge_base`。 + - `material_source`:**默认 `knowledge_base`(知识库)**;只有当用户明确要求用「技能/技能检索」时才填 `skill`,否则一律 `knowledge_base`。 - `outline`:用户提供的大纲原文(保留其层级与标题),否则留空。 4. 调用 `setup_writing` 前,用**一句话**说明你将提交的配置要点;调用之后不要再输出任何文章内容。 diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/setup_writing_approval_middleware.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/setup_writing_approval_middleware.py index 78ff2973..d1dbf0d8 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/setup_writing_approval_middleware.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/setup_writing_approval_middleware.py @@ -67,13 +67,17 @@ class SetupWritingApprovalMiddleware(AgentMiddleware[SetupWritingApprovalMiddlew return None def _build_payload(self, args: dict[str, Any]) -> dict[str, Any]: + # 素材来源:仅当模型明确给出 "skill" 时才用技能检索;用户没明确要求就一律默认知识库。 + material_source = ( + "skill" if str(args.get("material_source") or "").strip() == "skill" else "knowledge_base" + ) return { "user_intent": str(args.get("user_intent") or "").strip(), "article_type": str(args.get("article_type") or "").strip(), "word_count_target": self._coerce_word_count(args.get("word_count_target")), "target_audience": str(args.get("target_audience") or "").strip(), "writing_mode": str(args.get("writing_mode") or "").strip(), - "material_source": str(args.get("material_source") or "").strip(), + "material_source": material_source, "outline": str(args.get("outline") or "").strip(), }