From 7342cc08d343e67aadc705df605452a6d35197a5 Mon Sep 17 00:00:00 2001 From: MT-Mint <798521692@qq.com> Date: Wed, 18 Mar 2026 11:28:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=A7=BB=E6=A4=8D=20Titan=20main=20?= =?UTF-8?q?=E5=88=86=E6=94=AF=20skill=20=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/workspace/chats/[thread_id]/page.tsx | 241 +++++++++++++++--- .../workspace/chats/use-thread-chat.ts | 159 +++++++++++- .../workspace/messages/message-list-item.tsx | 76 +++++- frontend/src/core/skills/api.ts | 112 ++++++++ frontend/src/core/threads/hooks.ts | 36 ++- frontend/src/core/uploads/api.ts | 40 ++- 6 files changed, 609 insertions(+), 55 deletions(-) diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index 1aefe39f..0277cbab 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx @@ -1,7 +1,9 @@ "use client"; -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { ListTodoIcon } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { toast } from "sonner"; import { type PromptInputMessage } from "@/components/ai-elements/prompt-input"; import { Button } from "@/components/ui/button"; @@ -28,26 +30,148 @@ import { useArtifacts } from "@/components/workspace/artifacts"; import { useI18n } from "@/core/i18n/hooks"; import { useNotification } from "@/core/notification/hooks"; import { useLocalSettings } from "@/core/settings"; +// [移植自 main 分支 ef9a071] 导入 skill 初始化 API +import { bootstrapRemoteSkill } from "@/core/skills"; import { useThreadStream } from "@/core/threads/hooks"; import { textOfMessage } from "@/core/threads/utils"; import { env } from "@/env"; import { cn } from "@/lib/utils"; export default function ChatPage() { + console.log("[ChatPage] ========== COMPONENT RENDER =========="); + + const router = useRouter(); const { t } = useI18n(); const [settings, setSettings] = useLocalSettings(); const [showExitDialog, setShowExitDialog] = useState(false); + const [showErrorDialog, setShowErrorDialog] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); const { fullscreen } = useArtifacts(); - const { threadId, isNewThread, setIsNewThread, isMock } = useThreadChat(); + // [移植自 main 分支 4119fdc, e2fdfa7, ef9a071] 解构扩展的返回值 + const { + threadId, + isNewThread, + setIsNewThread, + isMock, + uploadTarget, + createNewSession, + skillBootstrap, + } = useThreadChat(); + + console.log("[ChatPage] useThreadChat result:"); + console.log("[ChatPage] threadId:", threadId); + console.log("[ChatPage] isNewThread:", isNewThread); + console.log("[ChatPage] isMock:", isMock); + console.log("[ChatPage] uploadTarget:", uploadTarget); + console.log("[ChatPage] createNewSession:", createNewSession); + console.log("[ChatPage] skillBootstrap:", skillBootstrap); + useSpecificChatMode(); const { showNotification } = useNotification(); + // [移植自 main 分支 ef9a071] skill 初始化状态 + const [isSkillBootstrapping, setIsSkillBootstrapping] = useState(false); + const skillBootstrappedKeyRef = useRef(null); + + // [移植自 main 分支 ef9a071] skill 初始化 effect + useEffect(() => { + console.log("[ChatPage] skillBootstrap effect triggered"); + console.log("[ChatPage] threadId:", threadId); + console.log("[ChatPage] skillBootstrap:", skillBootstrap); + + if (!threadId || !skillBootstrap?.contentId) { + console.log("[ChatPage] skillBootstrap: skipping (no threadId or no contentId)"); + setIsSkillBootstrapping(false); + return; + } + + const languageType = skillBootstrap.languageType ?? 0; + const initKey = `${threadId}:${skillBootstrap.contentId}:${languageType}`; + + console.log("[ChatPage] initKey:", initKey); + console.log("[ChatPage] alreadyBootstrapped:", skillBootstrappedKeyRef.current); + + if (skillBootstrappedKeyRef.current === initKey) { + console.log("[ChatPage] skillBootstrap already done for key:", initKey); + return; + } + + let cancelled = false; + + const runBootstrap = async () => { + console.log("[ChatPage] ========== SKILL BOOTSTRAP START =========="); + console.log("[ChatPage] threadId:", threadId); + console.log("[ChatPage] contentId:", skillBootstrap.contentId); + console.log("[ChatPage] languageType:", languageType); + console.log("[ChatPage] target_dir: /mnt/user-data/uploads/skill"); + + setIsSkillBootstrapping(true); + + // 使用 toast 显示加载状态 + const toastId = toast.loading("正在初始化 Skill 文件...", { + // description: "请稍候,正在从远程服务器获取 Skill 配置", + duration: 20000, + icon: false, + }); + + try { + const result = await bootstrapRemoteSkill({ + thread_id: threadId, + content_id: skillBootstrap.contentId, + language_type: languageType, + target_dir: "/mnt/user-data/uploads/skill", + clear_target: true, + }); + + console.log("[ChatPage] bootstrapRemoteSkill result:", result); + + if (!cancelled) { + skillBootstrappedKeyRef.current = initKey; + setIsSkillBootstrapping(false); + console.log("[ChatPage] ========== SKILL BOOTSTRAP SUCCESS =========="); + + // 使用 toast 显示成功状态 + toast.success(`已加载 Skill #${skillBootstrap.contentId}大模型将根据情况触发此 Skill`, { + id: toastId, + icon: false, + }); + } else { + console.log("[ChatPage] bootstrap cancelled, not updating state"); + toast.dismiss(toastId); + } + } catch (error) { + console.error("[ChatPage] ========== SKILL BOOTSTRAP FAILED =========="); + if (!cancelled) { + const message = error instanceof Error ? error.message : "Skill 初始化失败"; + console.error("[ChatPage] error message:", message); + setIsSkillBootstrapping(false); + + // 使用 DevDialog 显示错误状态 + toast.dismiss(toastId); + setErrorMessage(message); + setShowErrorDialog(true); + + showNotification("Skill 初始化失败", { body: message }); + } + } + }; + + void runBootstrap(); + + return () => { + console.log("[ChatPage] skillBootstrap effect cleanup"); + cancelled = true; + }; + }, [threadId, skillBootstrap, showNotification]); + const [thread, sendMessage] = useThreadStream({ threadId: isNewThread ? undefined : threadId, context: settings.context, isMock, + // [移植自 main 分支 4119fdc] 传递 uploadTarget + uploadTarget, onStart: () => { setIsNewThread(false); // ! Important: Never use next.js router for navigation in this case, otherwise it will cause the thread to re-mount and lose all states. Use native history API instead. @@ -73,51 +197,72 @@ export default function ChatPage() { const handleSubmit = useCallback( (message: PromptInputMessage) => { + console.log("[ChatPage] ========== handleSubmit =========="); + console.log("[ChatPage] message.text:", message.text?.substring(0, 100)); + console.log("[ChatPage] message.files:", message.files?.length || 0); + console.log("[ChatPage] isSkillBootstrapping:", isSkillBootstrapping); + console.log("[ChatPage] threadId:", threadId); + + // [移植自 main 分支 ef9a071] skill 初始化中禁止提交 + if (isSkillBootstrapping) { + console.log("[ChatPage] handleSubmit BLOCKED: skill bootstrapping in progress"); + return; + } + + console.log("[ChatPage] handleSubmit: calling sendMessage"); void sendMessage(threadId, message); }, - [sendMessage, threadId], + [sendMessage, threadId, isSkillBootstrapping], ); const handleStop = useCallback(async () => { + console.log("[ChatPage] handleStop called"); await thread.stop(); }, [thread]); return ( -
+
- {/* 返回查看结果左箭头 */} -
- -
+ + + + +
+ )} + {/* 新会话时用空 div 占位保持布局 */} + {isNewThread &&
}
@@ -162,13 +307,13 @@ export default function ChatPage() { >
} - disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"} + // [移植自 main 分支 ef9a071] skill 初始化中禁用输入 + disabled={ + env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" || + isSkillBootstrapping + } onContextChange={(context) => setSettings("context", context)} onSubmit={handleSubmit} onStop={handleStop} @@ -212,7 +361,31 @@ export default function ChatPage() { + + + + + {/* Skill 初始化错误对话框 */} + + + + Skill 初始化失败 + +

+ {errorMessage} +

+ + diff --git a/frontend/src/components/workspace/chats/use-thread-chat.ts b/frontend/src/components/workspace/chats/use-thread-chat.ts index b3164485..b8943827 100644 --- a/frontend/src/components/workspace/chats/use-thread-chat.ts +++ b/frontend/src/components/workspace/chats/use-thread-chat.ts @@ -1,29 +1,168 @@ "use client"; import { useParams, usePathname, useSearchParams } from "next/navigation"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { uuid } from "@/core/utils/uuid"; -export function useThreadChat() { +// [移植自 main 分支 4119fdc, e2fdfa7] 扩展返回类型 +export interface ThreadChatResult { + threadId: string; + isNewThread: boolean; + setIsNewThread: (value: boolean) => void; + isMock: boolean; + // [移植自 main 分支 4119fdc] 上传目标 + uploadTarget?: "skill"; + // [移植自 main 分支 e2fdfa7] 是否创建新会话 + createNewSession: boolean; + // [移植自 main 分支 ef9a071] skill 初始化参数 + skillBootstrap?: { + contentId: number; + languageType: number; + }; +} + +export function useThreadChat(): ThreadChatResult { + console.log("[useThreadChat] ========== HOOK CALLED =========="); + const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>(); const pathname = usePathname(); const searchParams = useSearchParams(); - const [threadId, setThreadId] = useState(() => { - return threadIdFromPath === "new" ? uuid() : threadIdFromPath; + + console.log("[useThreadChat] threadIdFromPath:", threadIdFromPath); + console.log("[useThreadChat] pathname:", pathname); + + // [移植自 main 分支 4119fdc] 从 URL 参数获取 thread_id + const queryThreadId = searchParams.get("thread_id")?.trim(); + console.log("[useThreadChat] queryThreadId from URL:", queryThreadId); + + // 打印所有 URL 参数 + console.log("[useThreadChat] All URL params:"); + searchParams.forEach((value, key) => { + console.log(` ${key}: ${value}`); }); - const [isNewThread, setIsNewThread] = useState( - () => threadIdFromPath === "new", - ); + // [移植自 main 分支 e2fdfa7] 判断是否创建新会话 + // isnew=false 表示复用现有会话,其他情况创建新会话 + const createNewSession = useMemo(() => { + if (threadIdFromPath !== "new") { + console.log("[useThreadChat] createNewSession: false (not new route)"); + return false; + } + const isnewParam = searchParams.get("isnew")?.trim().toLowerCase(); + const result = isnewParam !== "false"; + console.log("[useThreadChat] isnew param:", isnewParam, "-> createNewSession:", result); + return result; + }, [threadIdFromPath, searchParams]); + + // [移植自 main 分支 e2fdfa7] UI模式仅依赖路由:/workspace/chats/new 总是"新页面"模式 + const isNewThread = useMemo(() => { + const result = threadIdFromPath === "new"; + console.log("[useThreadChat] isNewThread:", result); + return result; + }, [threadIdFromPath]); + + // [移植自 main 分支 4119fdc] 获取上传目标 + const uploadTarget = useMemo(() => { + const target = searchParams.get("upload_target")?.trim().toLowerCase(); + console.log("[useThreadChat] upload_target from URL:", target); + const result = target === "skill" ? ("skill" as const) : undefined; + console.log("[useThreadChat] uploadTarget result:", result); + return result; + }, [searchParams]); + + // [移植自 main 分支 ef9a071] 获取 skill 初始化参数 + const skillBootstrap = useMemo(() => { + console.log("[useThreadChat] --- Parsing skillBootstrap params ---"); + const skillIdRaw = searchParams.get("skill_id")?.trim(); + console.log("[useThreadChat] skill_id raw:", skillIdRaw); + + if (!skillIdRaw) { + console.log("[useThreadChat] skillBootstrap: undefined (no skill_id)"); + return undefined; + } + + const contentId = Number(skillIdRaw); + console.log("[useThreadChat] contentId parsed:", contentId, "isFinite:", Number.isFinite(contentId)); + + if (!Number.isFinite(contentId)) { + console.log("[useThreadChat] skillBootstrap: undefined (invalid contentId)"); + return undefined; + } + + const languageTypeRaw = + searchParams.get("languageType")?.trim() ?? + searchParams.get("language_type")?.trim(); + const languageType = languageTypeRaw + ? Number(languageTypeRaw) + : 0; + + console.log("[useThreadChat] languageType raw:", languageTypeRaw, "parsed:", languageType); + + const result = { + contentId, + languageType: Number.isFinite(languageType) ? languageType : 0, + }; + console.log("[useThreadChat] skillBootstrap result:", result); + return result; + }, [searchParams]); + + // [修复] 使用 useRef 缓存生成的 threadId,避免 React StrictMode 下重复生成 + const threadIdRef = useRef(null); + const isNewThreadRef = useRef(null); + + // 仅在首次渲染时生成 threadId + if (threadIdRef.current === null) { + if (threadIdFromPath === "new") { + // [移植自 main 分支 4119fdc] 优先使用 URL 中的 thread_id + threadIdRef.current = queryThreadId || uuid(); + isNewThreadRef.current = true; + console.log("[useThreadChat] initial threadId (new route):", threadIdRef.current, "queryThreadId:", queryThreadId); + } else { + threadIdRef.current = threadIdFromPath; + isNewThreadRef.current = false; + console.log("[useThreadChat] initial threadId (existing):", threadIdRef.current); + } + } + + const [threadId, setThreadId] = useState(threadIdRef.current); + const [isNewThreadState, setIsNewThread] = useState(isNewThreadRef.current); useEffect(() => { + console.log("[useThreadChat] useEffect: pathname changed to:", pathname); if (pathname.endsWith("/new")) { + console.log("[useThreadChat] setting isNewThread=true"); setIsNewThread(true); - setThreadId(uuid()); + // [移植自 main 分支 4119fdc] 优先使用 URL 中的 thread_id + // 只有当 ref 中的值不是当前 queryThreadId 时才更新 + const newThreadId = queryThreadId || threadIdRef.current || uuid(); + if (newThreadId !== threadId) { + console.log("[useThreadChat] updating threadId:", newThreadId); + threadIdRef.current = newThreadId; + setThreadId(newThreadId); + } } - }, [pathname]); + }, [pathname, queryThreadId, threadId]); + const isMock = searchParams.get("mock") === "true"; - return { threadId, isNewThread, setIsNewThread, isMock }; + console.log("[useThreadChat] isMock:", isMock); + + console.log("[useThreadChat] ========== FINAL RESULT =========="); + console.log("[useThreadChat] threadId:", threadId); + console.log("[useThreadChat] isNewThread:", isNewThread); + console.log("[useThreadChat] createNewSession:", createNewSession); + console.log("[useThreadChat] uploadTarget:", uploadTarget); + console.log("[useThreadChat] skillBootstrap:", skillBootstrap ? JSON.stringify(skillBootstrap) : undefined); + console.log("[useThreadChat] ======================================"); + + return { + threadId, + isNewThread, + setIsNewThread, + isMock, + uploadTarget, + createNewSession, + skillBootstrap, + }; } diff --git a/frontend/src/components/workspace/messages/message-list-item.tsx b/frontend/src/components/workspace/messages/message-list-item.tsx index af137f29..a03d6399 100644 --- a/frontend/src/components/workspace/messages/message-list-item.tsx +++ b/frontend/src/components/workspace/messages/message-list-item.tsx @@ -1,7 +1,8 @@ import type { Message } from "@langchain/langgraph-sdk"; import { FileIcon, Loader2Icon } from "lucide-react"; import { useParams } from "next/navigation"; -import { memo, useMemo, type ImgHTMLAttributes } from "react"; +// [移植自 main 分支 ef9a071] 添加 useState +import { memo, useMemo, useState, type ImgHTMLAttributes } from "react"; import rehypeKatex from "rehype-katex"; import { Loader } from "@/components/ai-elements/loader"; @@ -18,6 +19,8 @@ import { } from "@/components/ai-elements/reasoning"; import { Task, TaskTrigger } from "@/components/ai-elements/task"; import { Badge } from "@/components/ui/badge"; +// [移植自 main 分支 ef9a071] 添加 Button +import { Button } from "@/components/ui/button"; import { resolveArtifactURL } from "@/core/artifacts/utils"; import { useI18n } from "@/core/i18n/hooks"; import { @@ -27,6 +30,8 @@ import { stripUploadedFilesTag, type FileInMessage, } from "@/core/messages/utils"; +// [移植自 main 分支 ef9a071] 添加 materializeSkillYaml +import { materializeSkillYaml } from "@/core/skills"; import { useRehypeSplitWordsIntoSpans } from "@/core/rehype"; import { humanMessagePlugins } from "@/core/streamdown"; import { cn } from "@/lib/utils"; @@ -262,6 +267,12 @@ function isImageFile(filename: string): boolean { return IMAGE_EXTENSIONS.includes(getFileExt(filename)); } +// [移植自 main 分支 ef9a071] 检测 YAML 文件 +function isYamlFile(filename: string): boolean { + const ext = getFileExt(filename); + return ext === "yaml" || ext === "yml"; +} + /** * Format bytes to human-readable size string */ @@ -309,6 +320,48 @@ function RichFileCard({ const { t } = useI18n(); const isUploading = file.status === "uploading"; const isImage = isImageFile(file.filename); + // [移植自 main 分支 ef9a071] YAML 文件解析状态 + const isYaml = isYamlFile(file.filename); + + // [移植自 main 分支 ef9a071] YAML 文件解析状态 + const [isMaterializing, setIsMaterializing] = useState(false); + const [materializeMessage, setMaterializeMessage] = useState( + null, + ); + + // [移植自 main 分支 ef9a071] YAML 文件解析处理函数 + const handleMaterializeYaml = async () => { + if (isMaterializing || !file.path) return; + + console.log("[RichFileCard] ========== handleMaterializeYaml START =========="); + console.log("[RichFileCard] threadId:", threadId); + console.log("[RichFileCard] file.path:", file.path); + console.log("[RichFileCard] file.filename:", file.filename); + + setIsMaterializing(true); + setMaterializeMessage(null); + + try { + const result = await materializeSkillYaml({ + thread_id: threadId, + path: file.path, + target_dir: "/mnt/user-data/uploads/skill", + clear_target: true, + }); + + console.log("[RichFileCard] materializeSkillYaml result:", result); + setMaterializeMessage( + `已创建 ${result.created_files} 个文件 / ${result.created_directories} 个目录`, + ); + } catch (error) { + const message = error instanceof Error ? error.message : "解析失败"; + console.error("[RichFileCard] materializeSkillYaml failed:", message); + setMaterializeMessage(`失败: ${message}`); + } finally { + setIsMaterializing(false); + console.log("[RichFileCard] ========== handleMaterializeYaml END =========="); + } + }; if (isUploading) { return ( @@ -380,6 +433,27 @@ function RichFileCard({ {formatBytes(file.size)}
+ {/* [移植自 main 分支 ef9a071] 注释掉测试按钮,后续根据需求再决定是否保留 */} + {/* {isYaml && ( +
+ + {materializeMessage && ( + + {materializeMessage} + + )} +
+ )} */}
); } diff --git a/frontend/src/core/skills/api.ts b/frontend/src/core/skills/api.ts index b6a358f0..0b12b90f 100644 --- a/frontend/src/core/skills/api.ts +++ b/frontend/src/core/skills/api.ts @@ -35,6 +35,39 @@ export interface InstallSkillResponse { message: string; } +// [移植自 main 分支 ef9a071] 添加 skill yaml 解析和远程 skill 初始化 API +export interface MaterializeSkillYamlRequest { + thread_id: string; + path: string; + target_dir?: string; + clear_target?: boolean; +} + +export interface MaterializeSkillYamlResponse { + success: boolean; + target_dir: string; + created_directories: number; + created_files: number; + message: string; +} + +export interface BootstrapRemoteSkillRequest { + thread_id: string; + content_id: number; + language_type?: number; + target_dir?: string; + clear_target?: boolean; +} + +export interface BootstrapRemoteSkillResponse { + success: boolean; + target_dir: string; + created_directories: number; + created_files: number; + sandbox_id: string; + message: string; +} + export async function installSkill( request: InstallSkillRequest, ): Promise { @@ -60,3 +93,82 @@ export async function installSkill( return response.json(); } + +// [移植自 main 分支 ef9a071] 解析 skill.yaml 文件并创建目录结构 +export async function materializeSkillYaml( + request: MaterializeSkillYamlRequest, +): Promise { + console.log("[skills/api] ========== materializeSkillYaml START =========="); + console.log("[skills/api] request:", JSON.stringify(request, null, 2)); + console.log("[skills/api] API URL:", `${getBackendBaseURL()}/api/skills/materialize-yaml`); + + const response = await fetch( + `${getBackendBaseURL()}/api/skills/materialize-yaml`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(request), + }, + ); + + console.log("[skills/api] response status:", response.status, response.statusText); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + const errorMessage = + errorData.detail ?? `HTTP ${response.status}: ${response.statusText}`; + console.error("[skills/api] materializeSkillYaml FAILED:", errorMessage); + console.error("[skills/api] error data:", errorData); + throw new Error(errorMessage); + } + + const result = await response.json(); + console.log("[skills/api] materializeSkillYaml SUCCESS:", result); + console.log("[skills/api] ========== materializeSkillYaml END =========="); + return result; +} + +// [移植自 main 分支 ef9a071] 从远程平台获取 skill 并初始化 +export async function bootstrapRemoteSkill( + request: BootstrapRemoteSkillRequest, +): Promise { + console.log("[skills/api] ========== bootstrapRemoteSkill START =========="); + console.log("[skills/api] request:", JSON.stringify(request, null, 2)); + console.log("[skills/api] thread_id:", request.thread_id); + console.log("[skills/api] content_id:", request.content_id); + console.log("[skills/api] language_type:", request.language_type); + console.log("[skills/api] target_dir:", request.target_dir); + console.log("[skills/api] API URL:", `${getBackendBaseURL()}/api/skills/bootstrap-remote`); + + const response = await fetch( + `${getBackendBaseURL()}/api/skills/bootstrap-remote`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(request), + }, + ); + + console.log("[skills/api] response status:", response.status, response.statusText); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + const errorMessage = + errorData.detail ?? `HTTP ${response.status}: ${response.statusText}`; + console.error("[skills/api] bootstrapRemoteSkill FAILED:", errorMessage); + console.error("[skills/api] error data:", errorData); + throw new Error(errorMessage); + } + + const result = await response.json(); + console.log("[skills/api] bootstrapRemoteSkill SUCCESS:", result); + console.log("[skills/api] created_directories:", result.created_directories); + console.log("[skills/api] created_files:", result.created_files); + console.log("[skills/api] sandbox_id:", result.sandbox_id); + console.log("[skills/api] ========== bootstrapRemoteSkill END =========="); + return result; +} diff --git a/frontend/src/core/threads/hooks.ts b/frontend/src/core/threads/hooks.ts index 79377c18..37c5e75c 100644 --- a/frontend/src/core/threads/hooks.ts +++ b/frontend/src/core/threads/hooks.ts @@ -14,6 +14,8 @@ import type { LocalSettings } from "../settings"; import { useUpdateSubtask } from "../tasks/context"; import type { UploadedFileInfo } from "../uploads"; import { uploadFiles } from "../uploads"; +// [移植自 main 分支 4119fdc] 导入 UploadTarget 类型 +import type { UploadTarget } from "../uploads/api"; import type { AgentThread, AgentThreadState } from "./types"; @@ -26,6 +28,8 @@ export type ThreadStreamOptions = { threadId?: string | null | undefined; context: LocalSettings["context"]; isMock?: boolean; + // [移植自 main 分支 4119fdc] 上传目标 + uploadTarget?: UploadTarget; onStart?: (threadId: string) => void; onFinish?: (state: AgentThreadState) => void; onToolEnd?: (event: ToolEndEvent) => void; @@ -35,10 +39,17 @@ export function useThreadStream({ threadId, context, isMock, + uploadTarget, onStart, onFinish, onToolEnd, }: ThreadStreamOptions) { + console.log("[threads/hooks] ========== useThreadStream INIT =========="); + console.log("[threads/hooks] threadId:", threadId); + console.log("[threads/hooks] context mode:", context?.mode); + console.log("[threads/hooks] isMock:", isMock); + console.log("[threads/hooks] uploadTarget:", uploadTarget); + const { t } = useI18n(); // Track the thread ID that is currently streaming to handle thread changes during streaming const [onStreamThreadId, setOnStreamThreadId] = useState(() => threadId); @@ -175,8 +186,26 @@ export function useThreadStream({ message: PromptInputMessage, extraContext?: Record, ) => { + console.log("[threads/hooks] ========== sendMessage START =========="); + console.log("[threads/hooks] threadId:", threadId); + console.log("[threads/hooks] message.text:", message.text?.substring(0, 100)); + console.log("[threads/hooks] message.files:", message.files?.length || 0); + const text = message.text.trim(); + // [移植自 main 分支 ef9a071] 空提交保护:忽略空消息提交(避免页面初始化时的意外副作用) + const hasFiles = !!(message.files && message.files.length > 0); + if (!text && !hasFiles) { + console.log("[threads/hooks] sendMessage: IGNORING empty submit (no text, no files)"); + console.log("[threads/hooks] ========== sendMessage END (ignored) =========="); + return; + } + + console.log("[threads/hooks] sendMessage proceeding:"); + console.log("[threads/hooks] text length:", text.length); + console.log("[threads/hooks] hasFiles:", hasFiles); + console.log("[threads/hooks] uploadTarget:", uploadTarget); + // Capture current count before showing optimistic messages prevMsgCountRef.current = thread.messages.length; @@ -258,7 +287,10 @@ export function useThreadStream({ } if (files.length > 0) { - const uploadResponse = await uploadFiles(threadId, files); + // [移植自 main 分支 4119fdc] 传递 uploadTarget 参数 + const uploadResponse = await uploadFiles(threadId, files, { + target: uploadTarget, + }); uploadedFileInfo = uploadResponse.files; // Update optimistic human message with uploaded status + paths @@ -345,7 +377,7 @@ export function useThreadStream({ throw error; } }, - [thread, _handleOnStart, t.uploads.uploadingFiles, context, queryClient], + [thread, _handleOnStart, t.uploads.uploadingFiles, context, queryClient, uploadTarget], ); // Merge thread with optimistic messages for display diff --git a/frontend/src/core/uploads/api.ts b/frontend/src/core/uploads/api.ts index 35725515..ec5d9fd3 100644 --- a/frontend/src/core/uploads/api.ts +++ b/frontend/src/core/uploads/api.ts @@ -29,35 +29,59 @@ export interface ListFilesResponse { count: number; } +// [移植自 main 分支 4119fdc] 上传目标类型 +export type UploadTarget = "skill"; + /** * Upload files to a thread */ export async function uploadFiles( threadId: string, files: File[], + options?: { target?: UploadTarget }, ): Promise { + console.log("[uploads/api] ========== uploadFiles START =========="); + console.log("[uploads/api] threadId:", threadId); + console.log("[uploads/api] files count:", files.length); + files.forEach((file, i) => { + console.log(`[uploads/api] file[${i}]:`, file.name, file.size, file.type); + }); + console.log("[uploads/api] options:", options); + const formData = new FormData(); files.forEach((file) => { formData.append("files", file); }); - const response = await fetch( - `${getBackendBaseURL()}/api/threads/${threadId}/uploads`, - { - method: "POST", - body: formData, - }, - ); + // [移植自 main 分支 4119fdc] 支持指定上传目标 + if (options?.target) { + formData.append("upload_target", options.target); + console.log("[uploads/api] upload_target set to:", options.target); + } + + const url = `${getBackendBaseURL()}/api/threads/${threadId}/uploads`; + console.log("[uploads/api] POST URL:", url); + + const response = await fetch(url, { + method: "POST", + body: formData, + }); + + console.log("[uploads/api] response status:", response.status, response.statusText); if (!response.ok) { const error = await response .json() .catch(() => ({ detail: "Upload failed" })); + console.error("[uploads/api] uploadFiles FAILED:", error); throw new Error(error.detail ?? "Upload failed"); } - return response.json(); + const result = await response.json(); + console.log("[uploads/api] uploadFiles SUCCESS:", result); + console.log("[uploads/api] ========== uploadFiles END =========="); + return result; } /**