From f01789a17e2c6dcd67bc8e5514e72126f80c1474 Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Wed, 10 Jun 2026 19:38:58 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E5=A4=9A=E6=99=BA=E8=83=BD=E4=BD=93?= =?UTF-8?q?=E4=BC=9A=E5=95=86=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/roundtable-planning/api/intent.ts | 16 +++ .../roundtable-planning/api/multi-agent.ts | 8 ++ .../components/Step1Panel.tsx | 104 ++++++++++++++- .../components/Step2Panel.tsx | 85 +++++++++++- .../hooks/useStep1Intent.ts | 66 +++++++++- .../hooks/useStep2Orchestration.ts | 121 +++++++++++++----- .../pages/RoundtablePlanningPage.tsx | 2 +- .../backend/app/gateway/routers/intent.py | 20 ++- .../app/gateway/routers/multi_agent.py | 38 +++++- 9 files changed, 411 insertions(+), 49 deletions(-) diff --git a/frontend-web/src/roundtable-planning/api/intent.ts b/frontend-web/src/roundtable-planning/api/intent.ts index f88e2327..1fa934e1 100644 --- a/frontend-web/src/roundtable-planning/api/intent.ts +++ b/frontend-web/src/roundtable-planning/api/intent.ts @@ -142,10 +142,24 @@ export interface IntentSummary { */ export type IntentStreamPhase = "connecting" | "connected" | "streaming" | "tool_calling"; +/** + * 用户上传文件的引用(已经过 `/api/threads/{thread_id}/uploads`)。透传给后端 + * `additional_kwargs.files`,由 UploadsMiddleware 注入文件清单并解锁 read_file 等工具。 + * 字段与普通聊天的 `FileInMessage` 对齐:`path` 用上传返回的 `virtual_path`。 + */ +export interface IntentFileRef { + filename: string; + size: number; + path: string; + status?: "uploading" | "uploaded"; +} + export interface StreamIntentRequest { threadId: string; message: string; model?: string; + /** Step 1 用户上传的参考文件(可选)。 */ + files?: IntentFileRef[]; signal?: AbortSignal; /** Fires for every AI text chunk parsed out of LangGraph messages-tuple frames. */ onTextDelta?: (text: string, messageId: string) => void; @@ -318,6 +332,8 @@ export async function streamIntent(req: StreamIntentRequest): Promise 0 ? req.files : undefined, }), signal: req.signal, }); diff --git a/frontend-web/src/roundtable-planning/api/multi-agent.ts b/frontend-web/src/roundtable-planning/api/multi-agent.ts index cfb76908..961fab42 100644 --- a/frontend-web/src/roundtable-planning/api/multi-agent.ts +++ b/frontend-web/src/roundtable-planning/api/multi-agent.ts @@ -227,6 +227,12 @@ export interface StreamMultiAgentRequest { * (平时只看广播摘要)。不进 prompt → 不调工具就不耗 token。 */ peerDeliveries?: Array<{ name: string; content: string }>; + /** + * 用户随干预消息上传的参考文件(已经过 `/api/threads/{thread_id}/uploads`)。 + * 透传给后端 `additional_kwargs.files`,由 UploadsMiddleware 注入清单 + 解锁 + * read_file 等工具。字段 `path` 用上传返回的 `virtual_path`。 + */ + files?: Array<{ filename: string; size: number; path: string; status?: string }>; signal?: AbortSignal; /** Fires for every non-status data frame the upstream emits. */ onUpstreamFrame?: (frame: unknown) => void; @@ -447,6 +453,8 @@ export async function streamMultiAgent( parallel_no_file: req.parallelNoFile ?? false, // 仅 special 用;前序席位完整交付,后端注入 context 供 read_peer_delivery 按需取全文。 peer_deliveries: req.peerDeliveries ?? [], + // 用户随干预上传的参考文件;仅在有文件时带上,后端注入 additional_kwargs.files。 + files: req.files && req.files.length > 0 ? req.files : undefined, }), signal: req.signal, }); diff --git a/frontend-web/src/roundtable-planning/components/Step1Panel.tsx b/frontend-web/src/roundtable-planning/components/Step1Panel.tsx index d27c53b1..168b4c23 100644 --- a/frontend-web/src/roundtable-planning/components/Step1Panel.tsx +++ b/frontend-web/src/roundtable-planning/components/Step1Panel.tsx @@ -14,8 +14,10 @@ import { useRef, useState } from "react"; import { CheckCircle, + FileText, Loader2, MessageSquare, + Paperclip, Pause, Repeat, Save, @@ -25,6 +27,13 @@ import { X, } from "lucide-react"; +/** 人类可读的文件大小(B / KB / MB)。 */ +function formatFileSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / 1024 / 1024).toFixed(1)} MB`; +} + import { Button } from "@/components/ui/button"; import { Man1Avatar, RobotAvatar } from "@/roundtable-planning/components/Avatars"; import { @@ -62,7 +71,7 @@ export interface Step1PanelProps { setIntentClarificationSelections: React.Dispatch< React.SetStateAction> >; - onSendMessage: (text: string) => void; + onSendMessage: (text: string, files?: File[]) => void; onStopIntent: () => void; onSkipQuestion: () => void; onFollowUp: () => void; @@ -101,13 +110,34 @@ export function Step1Panel(props: Step1PanelProps) { // 输入框完全是本地状态 —— 提到 Page 层会导致每次打字都重渲整页。 const [inputText, setInputText] = useState(""); + // 待发送的本地附件(点发送时连同文本一起上传)。 + const [attachedFiles, setAttachedFiles] = useState([]); + const fileInputRef = useRef(null); const charactersLeft = inputText.length; + const handlePickFiles = (e: React.ChangeEvent) => { + const picked = Array.from(e.target.files ?? []); + if (picked.length > 0) { + // 去重(按 名称+大小),追加到已选列表。 + setAttachedFiles((prev) => { + const seen = new Set(prev.map((f) => `${f.name}:${f.size}`)); + return [...prev, ...picked.filter((f) => !seen.has(`${f.name}:${f.size}`))]; + }); + } + // 清空 input 值,允许再次选同名文件。 + e.target.value = ""; + }; + + const removeAttachedFile = (idx: number) => { + setAttachedFiles((prev) => prev.filter((_, i) => i !== idx)); + }; + const handleSend = () => { - if (!inputText.trim()) return; - onSendMessage(inputText); + if (!inputText.trim() && attachedFiles.length === 0) return; + onSendMessage(inputText, attachedFiles.length > 0 ? attachedFiles : undefined); setInputText(""); + setAttachedFiles([]); }; return ( @@ -150,6 +180,33 @@ export function Step1Panel(props: Step1PanelProps) { {/* Bottom input area */}
+ {/* 已选附件预览(点发送时随消息上传) */} + {attachedFiles.length > 0 && ( +
+ {attachedFiles.map((file, idx) => ( + + + + {file.name} + + + {formatFileSize(file.size)} + + + + ))} +
+ )}