diff --git a/frontend-web/src/components/workspace/workspace-nav-studio.tsx b/frontend-web/src/components/workspace/workspace-nav-studio.tsx index 8567f7ce..7c11d54e 100644 --- a/frontend-web/src/components/workspace/workspace-nav-studio.tsx +++ b/frontend-web/src/components/workspace/workspace-nav-studio.tsx @@ -102,7 +102,7 @@ export function WorkspaceNavStudio() { - {/* 笔记本列表(最多展示 SIDEBAR_PREVIEW_COUNT 条) */} + {/* 空间列表(最多展示 SIDEBAR_PREVIEW_COUNT 条) */} {preview.map((nb) => ( setName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") void handleCreate(); }} - placeholder="笔记本名称" + placeholder="空间名称" disabled={creating} className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary disabled:opacity-50" /> diff --git a/frontend-web/src/core/config/index.ts b/frontend-web/src/core/config/index.ts index d6a8ade6..a33f0cfc 100644 --- a/frontend-web/src/core/config/index.ts +++ b/frontend-web/src/core/config/index.ts @@ -69,7 +69,7 @@ export function getLangGraphBaseURL(isMock?: boolean) { } } -/** 笔记本外部系统后端 API 地址,通过 VITE_AIPI_BACKENP_URL 配置。 */ +/** 空间外部系统后端 API 地址,通过 VITE_AIPI_BACKENP_URL 配置。 */ export function getNotebookApiBaseURL(): string { if (env.NEXT_PUBLIC_NOTEBOOK_API_BASE_URL) { return env.NEXT_PUBLIC_NOTEBOOK_API_BASE_URL.replace(/\/+$/, ""); diff --git a/frontend-web/src/core/studio/api.ts b/frontend-web/src/core/studio/api.ts index 5d779a87..f2aa636b 100644 --- a/frontend-web/src/core/studio/api.ts +++ b/frontend-web/src/core/studio/api.ts @@ -20,14 +20,16 @@ export async function fetchStudioToken(email: string): Promise { return token; } -export async function fetchStudioNotebooks(token: string): Promise { +export async function fetchStudioNotebooks( + token: string, +): Promise { const apiBase = base(); if (!apiBase) throw new Error("VITE_AIPI_FRONTEND_URL 未配置"); const res = await fetch(`${apiBase}/api/v1/searchspaces`, { headers: { Authorization: `Bearer ${token}` }, }); - if (!res.ok) throw new Error(`获取笔记本列表失败:${res.status}`); + if (!res.ok) throw new Error(`获取空间列表失败:${res.status}`); return res.json() as Promise; } @@ -62,13 +64,23 @@ export async function uploadSourceText( if (!apiBase) throw new Error("VITE_AIPI_FRONTEND_URL 未配置"); const res = await fetch(`${apiBase}/api/v1/documents/textupload`, { method: "POST", - headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, - body: JSON.stringify({ search_space_id: notebookId, content, title: title || "粘贴的文字" }), + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + search_space_id: notebookId, + content, + title: title || "粘贴的文字", + }), }); if (!res.ok) throw new Error(`上传失败:${res.status}`); } -export async function deleteSearchSpace(id: number, token: string): Promise { +export async function deleteSearchSpace( + id: number, + token: string, +): Promise { const apiBase = base(); if (!apiBase) throw new Error("VITE_AIPI_FRONTEND_URL 未配置"); const res = await fetch(`${apiBase}/api/v1/searchspaces/${id}`, { @@ -78,12 +90,19 @@ export async function deleteSearchSpace(id: number, token: string): Promise { +export async function renameSearchSpace( + id: number, + name: string, + token: string, +): Promise { const apiBase = base(); if (!apiBase) throw new Error("VITE_AIPI_FRONTEND_URL 未配置"); const res = await fetch(`${apiBase}/api/v1/searchspaces/${id}`, { method: "PATCH", - headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, body: JSON.stringify({ name }), }); if (!res.ok) throw new Error(`重命名失败:${res.status}`); diff --git a/frontend-web/src/core/studio/hooks.ts b/frontend-web/src/core/studio/hooks.ts index 9bf706bd..7d468134 100644 --- a/frontend-web/src/core/studio/hooks.ts +++ b/frontend-web/src/core/studio/hooks.ts @@ -4,7 +4,11 @@ import { useStream } from "@langchain/langgraph-sdk/react"; import { getStoredAuth } from "@/core/auth"; import { getAPIClient } from "@/core/api"; -import { fetchNotebookSources, fetchStudioNotebooks, fetchStudioToken } from "./api"; +import { + fetchNotebookSources, + fetchStudioNotebooks, + fetchStudioToken, +} from "./api"; import type { AgentThreadState } from "@/core/threads/types"; import type { SourceItem, StudioNotebook } from "./types"; @@ -30,7 +34,7 @@ export function useStudioNotebooks(enabled = true): { queryKey: ["studio", "notebooks", tokenQuery.data], queryFn: () => fetchStudioNotebooks(tokenQuery.data!), enabled: enabled && Boolean(tokenQuery.data), - // 笔记本列表不会高频变动;新建/删除路径已通过 invalidateQueries 主动失效 + // 空间列表不会高频变动;新建/删除路径已通过 invalidateQueries 主动失效 // (见 workspace-nav-studio.tsx),不需要短 staleTime + windowFocus 双兜底。 staleTime: 10 * 60 * 1000, retry: 1, @@ -105,7 +109,9 @@ export function useNotebookChat( const sourceIds = selectedSourceIds.length > 0 ? toDocIds(selectedSourceIds) - : sources.map((s) => s.document?.id).filter((id): id is number => id !== undefined); + : sources + .map((s) => s.document?.id) + .filter((id): id is number => id !== undefined); await thread.submit( { messages: [{ type: "human", content: text }] }, @@ -124,7 +130,14 @@ export function useNotebookChat( ); }; - return { thread, messages: thread.messages, isStreaming: thread.isLoading, send, threadId, setThreadId }; + return { + thread, + messages: thread.messages, + isStreaming: thread.isLoading, + send, + threadId, + setThreadId, + }; } export function useNotebookSources( @@ -161,7 +174,8 @@ export function useNotebookSources( return { sources: sourcesQuery.data ?? [], - isLoading: (!overrideToken && tokenQuery.isLoading) || sourcesQuery.isLoading, + isLoading: + (!overrideToken && tokenQuery.isLoading) || sourcesQuery.isLoading, isError: (!overrideToken && tokenQuery.isError) || sourcesQuery.isError, }; } diff --git a/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx b/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx index 9849b9ff..24de93d0 100644 --- a/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx +++ b/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx @@ -22,7 +22,7 @@ interface AIWritingFormProps { disabled?: boolean; /** workspace 入口:隐藏严格模式、目标读者 */ hideAdvancedFields?: boolean; - /** 预选笔记本 ID(传入时自动切换素材来源为「我的笔记」) */ + /** 预选空间 ID(传入时自动切换素材来源为「我的笔记」) */ initialNotebookId?: number; /** 预选文档 ID 列表 */ initialDocumentIds?: number[]; @@ -45,7 +45,7 @@ const MAX_INTENT_LENGTH = 500; const MATERIAL_SOURCE_OPTIONS: Array<{ value: MaterialSource; label: string; desc: string }> = [ { value: 'knowledge_base', label: '知识库', desc: '默认 web/intranet 检索' }, { value: 'skill', label: '技能检索', desc: '用已启用的技能编排检索' }, - { value: 'notebook', label: '我的笔记', desc: '从笔记本中检索素材' }, + { value: 'notebook', label: '我的笔记', desc: '从空间中检索素材' }, ]; const FIELD_LABEL_CLASS = 'text-[16px] font-semibold leading-none text-[#222]'; @@ -139,7 +139,7 @@ export function AIWritingForm({ function handleMaterialSourceChange(val: MaterialSource) { setMaterialSource(val); - // studioToken 模式下笔记本和文档已锁定,切换素材来源时不清空 + // studioToken 模式下空间和文档已锁定,切换素材来源时不清空 if (val !== 'notebook' && !studioToken) { setSelectedNotebookId(null); setSelectedDocumentIds([]); @@ -296,10 +296,10 @@ export function AIWritingForm({ - {/* 选了"我的笔记"才展示笔记本 / 文档选择器 */} + {/* 选了"我的笔记"才展示空间 / 文档选择器 */} {materialSource === 'notebook' && (
- {/* studioToken 模式:笔记本已锁定,不展示选择器 */} + {/* studioToken 模式:空间已锁定,不展示选择器 */} {!studioToken && ( setInput(e.target.value)} - placeholder="问问 AI..." - disabled={isStreaming} - className="flex-1 bg-transparent text-[15px] text-foreground placeholder:text-muted-foreground outline-none disabled:opacity-50" /> - {isStreaming && } - {models.length > 0 && ( - - )} - - - {/* 空状态提示 */} - {(!notebookThreads || notebookThreads.length === 0) && ( -
- - - - 笔记本对话会显示在这里 -
)} - {/* 历史线程列表 */} - {notebookThreads && notebookThreads.length > 0 && ( -
-

历史对话

-
- {(threadsExpanded ? notebookThreads : notebookThreads.slice(0, THREADS_LIMIT)).map((t) => { - const values = t.values as Record | undefined; - const msgs = values?.messages as Array<{ type: string; content: unknown }> | undefined; - const firstHuman = msgs?.find((m) => m.type === "human"); - const rawContent = firstHuman?.content; - const preview = - typeof rawContent === "string" - ? rawContent.slice(0, 60) - : Array.isArray(rawContent) - ? String((rawContent[0] as { text?: string })?.text ?? "").slice(0, 60) - : "对话"; - const title = (values?.title as string | undefined) || preview || "对话"; - const time = new Date(t.updated_at ?? "").toLocaleString("zh-CN", { - month: "numeric", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - }); - return ( - - ); - })} -
- {notebookThreads.length > THREADS_LIMIT && ( + {/* 聊天视图:顶部栏 */} + {isChatView && ( +
+
- )} +
+ {notebook.avatar_url ? ( + + ) : ( + + )} +
+ {notebook.name} +
+
+ + + 我的笔记 + + +
)} -
-
- )} - {/* 聊天视图:消息列表 */} - {isChatView && ( -
{ - const sup = (e.target as HTMLElement).closest("[data-chunk-id]"); - if (!sup) return; - const chunkId = Number(sup.dataset.chunkId); - if (!chunkId) return; - setCitationChunkId(chunkId); - setCitationDoc(null); - setCitationLoading(true); - const email = getStoredAuth()?.email ?? ""; - const tok = queryClient.getQueryData(["studio", "token", email]) ?? ""; - void fetchDocumentByChunk(chunkId, tok) - .then((doc) => setCitationDoc(doc)) - .finally(() => setCitationLoading(false)); - }} - > - -
- )} + {/* 列表视图:空间标题 + 历史线程列表 */} + {!isChatView && ( +
+ {/* 右上角跳转按钮 */} + + + 我的笔记 + - {/* 聊天视图的底部输入框 */} -
-
{ void handleSend(e); }} - className="mx-auto flex max-w-[720px] items-center gap-3 rounded-full bg-card px-5 py-3.5 shadow-md ring-1 ring-border" - > - - setInput(e.target.value)} - placeholder="问问 AI..." - disabled={isStreaming} - className="flex-1 bg-transparent text-[15px] text-foreground placeholder:text-muted-foreground outline-none disabled:opacity-50" - /> - {isStreaming && } - {models.length > 0 && ( - - )} - -
+ {/* 垂直居中主区域:图标 → 标题 → 来源 → 输入框 → 空状态/历史 */} +
+
+ {notebook.avatar_url ? ( + + ) : ( + + )} +
+

+ {notebook.name} +

+ - {/* 引用来源弹窗 */} - { if (!v) setCitationChunkId(null); }}> - - - 引用来源 - - {citationLoading && ( -
- -
- )} - {!citationLoading && citationDoc && ( -
-
- - {citationDoc.file_type || citationDoc.document_type} - - {citationDoc.title} -
- {citationDoc.chunks.find((c) => c.id === citationChunkId) && ( -
- {citationDoc.chunks.find((c) => c.id === citationChunkId)!.content} + {/* 输入框(列表视图内联) */} +
{ void handleSend(e); }} + className="relative z-10 mt-8 flex w-full max-w-[680px] items-center gap-3 rounded-full bg-card px-5 py-3.5 shadow-md ring-1 ring-border" + > + + setInput(e.target.value)} + placeholder="问问 AI..." + disabled={isStreaming} + className="flex-1 bg-transparent text-[15px] text-foreground placeholder:text-muted-foreground outline-none disabled:opacity-50" + /> + {isStreaming && } + {models.length > 0 && ( + + )} + + + {/* 空状态提示 */} + {(!notebookThreads || notebookThreads.length === 0) && ( +
+ + + + 空间对话会显示在这里 +
+ )} + + {/* 历史线程列表 */} + {notebookThreads && notebookThreads.length > 0 && ( +
+

历史对话

+
+ {(threadsExpanded ? notebookThreads : notebookThreads.slice(0, THREADS_LIMIT)).map((t) => { + const values = t.values as Record | undefined; + const msgs = values?.messages as Array<{ type: string; content: unknown }> | undefined; + const firstHuman = msgs?.find((m) => m.type === "human"); + const rawContent = firstHuman?.content; + const preview = + typeof rawContent === "string" + ? rawContent.slice(0, 60) + : Array.isArray(rawContent) + ? String((rawContent[0] as { text?: string })?.text ?? "").slice(0, 60) + : "对话"; + const title = (values?.title as string | undefined) || preview || "对话"; + const time = new Date(t.updated_at ?? "").toLocaleString("zh-CN", { + month: "numeric", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + return ( + + ); + })} +
+ {notebookThreads.length > THREADS_LIMIT && ( + + )} +
+ )}
- )} -
- )} - {!citationLoading && !citationDoc && ( -

加载失败,请重试。

- )} -
-
+
+ )} - {/* 来源 Dialog */} - setSourcesOpen(false)} - notebookName={notebook.name} - sources={sources} - isLoadingSources={isLoadingSources} - selectedSourceIds={selectedSourceIds} - onToggle={handleToggleSource} - onDelete={handleDeleteSource} - /> -
- - + {/* 聊天视图:消息列表 */} + {isChatView && ( +
{ + const sup = (e.target as HTMLElement).closest("[data-chunk-id]"); + if (!sup) return; + const chunkId = Number(sup.dataset.chunkId); + if (!chunkId) return; + setCitationChunkId(chunkId); + setCitationDoc(null); + setCitationLoading(true); + const email = getStoredAuth()?.email ?? ""; + const tok = queryClient.getQueryData(["studio", "token", email]) ?? ""; + void fetchDocumentByChunk(chunkId, tok) + .then((doc) => setCitationDoc(doc)) + .finally(() => setCitationLoading(false)); + }} + > + +
+ )} + + {/* 聊天视图的底部输入框 */} +
+
{ void handleSend(e); }} + className="mx-auto flex max-w-[720px] items-center gap-3 rounded-full bg-card px-5 py-3.5 shadow-md ring-1 ring-border" + > + + setInput(e.target.value)} + placeholder="问问 AI..." + disabled={isStreaming} + className="flex-1 bg-transparent text-[15px] text-foreground placeholder:text-muted-foreground outline-none disabled:opacity-50" + /> + {isStreaming && } + {models.length > 0 && ( + + )} + +
+ + {/* 引用来源弹窗 */} + { if (!v) setCitationChunkId(null); }}> + + + 引用来源 + + {citationLoading && ( +
+ +
+ )} + {!citationLoading && citationDoc && ( +
+
+ + {citationDoc.file_type || citationDoc.document_type} + + {citationDoc.title} +
+ {citationDoc.chunks.find((c) => c.id === citationChunkId) && ( +
+ {citationDoc.chunks.find((c) => c.id === citationChunkId)!.content} +
+ )} +
+ )} + {!citationLoading && !citationDoc && ( +

加载失败,请重试。

+ )} +
+
+ + {/* 来源 Dialog */} + setSourcesOpen(false)} + notebookName={notebook.name} + sources={sources} + isLoadingSources={isLoadingSources} + selectedSourceIds={selectedSourceIds} + onToggle={handleToggleSource} + onDelete={handleDeleteSource} + /> + + + ); } diff --git a/frontend-web/src/pages/StudioNotebooksPage.tsx b/frontend-web/src/pages/StudioNotebooksPage.tsx index 17d7cc22..a8952b76 100644 --- a/frontend-web/src/pages/StudioNotebooksPage.tsx +++ b/frontend-web/src/pages/StudioNotebooksPage.tsx @@ -180,7 +180,7 @@ export default function StudioNotebooksPage() {
{/* Header */}
-

笔记本

+

空间

{isError && ( -

加载笔记本失败,请稍后重试。

+

加载空间失败,请稍后重试。

)} {/* Grid */} @@ -220,7 +220,7 @@ export default function StudioNotebooksPage() { value={createName} onChange={(e) => setCreateName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") void handleCreate(); }} - placeholder="笔记本名称" + placeholder="空间名称" disabled={creating} className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary disabled:opacity-50" /> @@ -239,7 +239,7 @@ export default function StudioNotebooksPage() { !renaming && !v && setRenameTarget(null)}> - 重命名笔记本 + 重命名空间
@@ -272,12 +272,12 @@ export default function StudioNotebooksPage() { !deleting && !v && setDeleteTarget(null)}> - 删除笔记本 + 删除空间

确定要删除{deleteTarget ? 「{deleteTarget.name}」 : ""}吗? - 删除后笔记本内所有来源数据将一并移除,且无法恢复。 + 删除后空间内所有来源数据将一并移除,且无法恢复。

{deleteError &&

{deleteError}

}
diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py index 429d199c..d8b7b62b 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py @@ -416,7 +416,7 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict # ── 纯笔记模式 ── # 用户在表单选了「我的笔记」作为素材来源 → 跳过 web/intranet 检索, # 仅使用 Step 1.5 已拉取的 notebook_materials。 - # 与 UI 文案"从笔记本中检索素材"对齐。 + # 与 UI 文案"从空间中检索素材"对齐。 _emit(f"仅使用笔记素材,跳过 web/intranet 检索({len(notebook_materials)} 条)") all_results_nested = [] else: 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 cea2a124..6fbd6375 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 @@ -1078,7 +1078,7 @@ When a search, knowledge, skill, or MCP tool returns a result list: ## 角色 -你是一个笔记本 AI 助手,**只能**基于笔记本文档来源回答用户问题,禁止使用任何其他知识。 +你是一个空间 AI 助手,**只能**基于空间文档来源回答用户问题,禁止使用任何其他知识。 ## 可用知识来源 {source_line} @@ -1104,7 +1104,7 @@ When a search, knowledge, skill, or MCP tool returns a result list: - 未调用 `search_notebook_sources` 就直接回答 - 引用不在检索结果中的 chunk_id - 省略引用标记 -- 使用笔记本来源之外的知识 +- 使用空间来源之外的知识 """ return prompt diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/tools/builtins/notebook_search_tool.py b/offline-backend-20260512/backend/packages/harness/deerflow/tools/builtins/notebook_search_tool.py index 22c10f86..51ffd77d 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/tools/builtins/notebook_search_tool.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/tools/builtins/notebook_search_tool.py @@ -1,7 +1,7 @@ -"""笔记本 RAG 检索工具。 +"""空间 RAG 检索工具。 从 LangGraph configurable 中读取 notebook_search_space_id / notebook_source_ids / -notebook_token,调用外部检索接口返回相关文本块,供 LLM 在笔记本模式下引用回答。 +notebook_token,调用外部检索接口返回相关文本块,供 LLM 在空间模式下引用回答。 API base URL 通过环境变量 DEER_FLOW_NOTEBOOK_API_BASE 配置。 """