From 6a5e7c1b847e7566a8a8e56e7dd9a7909e373498 Mon Sep 17 00:00:00 2001 From: Autumn-27 <53844290+Autumn-27@users.noreply.github.com> Date: Tue, 18 Aug 2026 09:01:56 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=BB=BA=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=8F=B3=E4=BE=A7=E6=8A=BD=E5=B1=89(45%?= =?UTF-8?q?=E5=AE=BD)+=20=E6=94=AF=E6=8C=81=E4=B8=8A=E4=BC=A0=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=86=99=E5=85=A5=E6=8F=8F=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 表单从居中弹窗(Dialog)换成右侧 Sheet,宽 45vw(窄屏全宽),整屏高度可 滚动,长表单不再受弹窗高度限制;头/脚固定、中间字段区独立滚动。 - 高级参数(超时/心跳/首个意图)收进「高级设置」Collapsible 折叠,常用路径 只露 描述/目标/LLM;描述与目标 textarea 加高(min-h-32)。 - 描述下方加「上传文件」组件(可多选):建任务前任务无 ID,新增上传 scope=staging,文件落到 /drafts//uploads/,后端返回绝对 路径(chatAttachment.Abs),前端把绝对路径以提示词形式追加到描述末尾,供 worker 用 Read/Bash 打开;再次上传追加到同一标记块。draftId 用 crypto.randomUUID(非安全上下文降级),创建成功后清空。 --- server/chatupload.go | 16 +- web/src/app/(main)/function/tasks/page.tsx | 318 ++++++++++++++------- web/src/lib/api.ts | 4 +- web/src/lib/types.ts | 1 + 4 files changed, 230 insertions(+), 109 deletions(-) diff --git a/server/chatupload.go b/server/chatupload.go index 496007c..7ac55e1 100644 --- a/server/chatupload.go +++ b/server/chatupload.go @@ -26,17 +26,23 @@ type chatAttachment struct { Name string `json:"name"` Path string `json:"path"` Size int64 `json:"size"` + // Abs 是落盘的绝对路径(m.dir 已是绝对)。建任务前暂存(scope=staging)时前端要用它把 + // 提示词写进描述;task/session 走 composeAgentMessage 在后端拼路径,不依赖此字段。 + Abs string `json:"abs,omitempty"` } // chatUpload implements method-1 file support: it saves one or more files into a chat's // working dir under uploads/, so the agent opens them with its existing Read/Bash tools // and the sent message carries their paths. No LLM-layer change, no multimodal. // -// POST /api/chat/upload?scope=task|session&id=, multipart field "file" (repeatable). -// Returns {attachments:[{name,path,size}]}. Target dir mirrors the agent CWD layout: +// POST /api/chat/upload?scope=task|session|staging&id=, multipart field "file" +// (repeatable). Returns {attachments:[{name,path,size,abs}]}. Target dir mirrors the +// agent CWD layout: // // scope=task → /tasks//uploads/ // scope=session → /sessions//uploads/ +// scope=staging → /drafts//uploads/ (建任务前暂存:任务尚无 ID, +// 文件先落这里,前端按返回的 abs 绝对路径写进任务描述) func (s *Server) chatUpload(w http.ResponseWriter, r *http.Request) { var sub string switch r.URL.Query().Get("scope") { @@ -44,8 +50,10 @@ func (s *Server) chatUpload(w http.ResponseWriter, r *http.Request) { sub = "tasks" case "session": sub = "sessions" + case "staging": + sub = "drafts" default: - writeErr(w, 400, "scope 必须是 task 或 session") + writeErr(w, 400, "scope 必须是 task / session / staging") return } id := r.URL.Query().Get("id") @@ -80,7 +88,7 @@ func (s *Server) chatUpload(w http.ResponseWriter, r *http.Request) { return } base := filepath.Base(dest) - out = append(out, chatAttachment{Name: base, Path: "uploads/" + base, Size: hdr.Size}) + out = append(out, chatAttachment{Name: base, Path: "uploads/" + base, Size: hdr.Size, Abs: dest}) } writeJSON(w, 200, map[string]any{"attachments": out}) } diff --git a/web/src/app/(main)/function/tasks/page.tsx b/web/src/app/(main)/function/tasks/page.tsx index 6a2ddc8..90fbcd0 100644 --- a/web/src/app/(main)/function/tasks/page.tsx +++ b/web/src/app/(main)/function/tasks/page.tsx @@ -10,6 +10,9 @@ import { StarIcon, SearchIcon, XIcon, + ChevronRightIcon, + PaperclipIcon, + Loader2Icon, } from "lucide-react"; import { StatusBadge } from "@/components/status-badge"; @@ -28,15 +31,20 @@ import { TableRow, } from "@/components/ui/table"; import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; import { Select, SelectContent, @@ -46,7 +54,30 @@ import { } from "@/components/ui/select"; import { TablePagination } from "@/components/table-pagination"; import { api } from "@/lib/api"; -import type { Task, TaskStatus, LLMProfile } from "@/lib/types"; +import type { Task, TaskStatus, LLMProfile, ChatAttachment } from "@/lib/types"; + +// fmtBytes renders a human file size for the upload manifest. +function fmtBytes(n: number): string { + if (n >= 1 << 20) return `${(n / (1 << 20)).toFixed(1)} MB`; + if (n >= 1 << 10) return `${(n / (1 << 10)).toFixed(1)} KB`; + return `${n} B`; +} + +// UPLOAD_MARKER labels the auto-appended block of uploaded-file paths inside the task +// description, so re-uploads append under the same block instead of adding a new header. +const UPLOAD_MARKER = "【上传文件(绝对路径)】"; + +// appendUploads folds newly-uploaded files' ABSOLUTE paths into the description as a +// Read/Bash-friendly manifest — the worker opens them by path. Keeps one marked block: +// first upload adds the header, later uploads append bullets under it. +function appendUploads(desc: string, atts: ChatAttachment[]): string { + const bullets = atts.map((a) => `- ${a.abs ?? a.path}(${fmtBytes(a.size)})`).join("\n"); + if (desc.includes(UPLOAD_MARKER)) { + return `${desc.replace(/\s*$/, "")}\n${bullets}\n`; + } + const head = desc.trim() ? `${desc.replace(/\s*$/, "")}\n\n` : ""; + return `${head}${UPLOAD_MARKER} worker 可用 Read/Bash 按路径打开:\n${bullets}\n`; +} // ACTIVE_PROFILE is the sentinel Select value for "use the global active profile". const ACTIVE_PROFILE = "__active__"; @@ -114,6 +145,11 @@ export default function TasksPage() { const [heartbeatMin, setHeartbeatMin] = React.useState("10"); // planner 心跳(分钟);默认10,下限10(与后端一致) const [seedFirstIntent, setSeedFirstIntent] = React.useState(false); // 创建时下发种子意图,worker 免等首轮 planner 直接开跑;默认关闭,走标准先规划再执行 const [open, setOpen] = React.useState(false); + // 方式1 文件上传:建任务前把文件暂存到 drafts//uploads/,拿回绝对路径追加进描述。 + const [uploading, setUploading] = React.useState(false); + const [uploadCount, setUploadCount] = React.useState(0); + const draftIdRef = React.useRef(""); + const fileInputRef = React.useRef(null); const [query, setQuery] = React.useState(""); const [statusFilter, setStatusFilter] = React.useState("all"); const [page, setPage] = React.useState(1); @@ -166,6 +202,29 @@ export default function TasksPage() { return () => clearInterval(i); }, []); + // pickFiles uploads the chosen files into this draft's staging dir and appends their + // absolute paths to the description; the task's agents open them by path via Read/Bash. + async function pickFiles(files: FileList | null) { + if (!files || files.length === 0) return; + // crypto.randomUUID 仅在安全上下文可用(https/localhost);经 IP+http 访问时降级。 + if (!draftIdRef.current) { + draftIdRef.current = + globalThis.crypto?.randomUUID?.() ?? + `d${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`; + } + setUploading(true); + try { + const r = await api.chatUpload("staging", draftIdRef.current, Array.from(files)); + setDescription((prev) => appendUploads(prev, r.attachments)); + setUploadCount((n) => n + r.attachments.length); + } catch (e) { + toast.error("上传失败:" + (e as Error).message); + } finally { + setUploading(false); + if (fileInputRef.current) fileInputRef.current.value = ""; // allow re-picking the same file + } + } + async function createTask() { if (!description.trim() || !goal.trim()) { toast.error("请填写描述与目标"); @@ -183,6 +242,8 @@ export default function TasksPage() { setTimeoutMin(""); setHeartbeatMin("10"); setSeedFirstIntent(false); + setUploadCount(0); + draftIdRef.current = ""; setOpen(false); load(); } catch (e) { @@ -239,106 +300,157 @@ export default function TasksPage() { {filtered.length}/{tasks.length} 条 - - + + - - - - 新建任务 - - 填写测试对象与目标。 - - -
-
- -