- 1. 选择技能({selectedSkills.length})
+ 第一步:选择技能(已选 {selectedSkills.length})
@@ -162,6 +173,13 @@ export function SkillKnowledgePanel() {
toggle(selectedSkills, skill.name, setSelectedSkills)} />
{skill.name_zh || skill.name}
{skill.name}
+ {(bindings.data ?? []).some((binding) => binding.skill_name === skill.name) && (
+
+ {(bindings.data ?? []).some((binding) => binding.skill_name === skill.name && `${binding.target_type}:${binding.target_id}` === selectedTargetKey)
+ ? "已在目标库"
+ : `已归纳 ${(bindings.data ?? []).filter((binding) => binding.skill_name === skill.name).length} 库`}
+
+ )}
))}
{!isLoading && !filteredSkills.length && (
@@ -174,7 +192,7 @@ export function SkillKnowledgePanel() {
- 2. 选择目标知识库({selectedTargetKey ? 1 : 0})
+ 第二步:选择一个目标知识库
{targets.map((target) => {
@@ -207,19 +225,12 @@ export function SkillKnowledgePanel() {
)}
-
-
审核策略
-
+
+ 第三步:提交后可离开本页。归纳、Wiki 生成、向量化及同步到“知识梳理”总库均在后台完成。
-
@@ -228,22 +239,26 @@ export function SkillKnowledgePanel() {
-
+
void jobs.refetch()}>
{(jobs.data ?? []).map((job) => {
- const done = job.success_count + job.partial_failed_count + job.failed_count;
+ const countedDone = job.success_count + job.partial_failed_count + job.failed_count;
+ const itemDone = (job.items ?? []).filter((item) =>
+ ["completed", "partial_failed", "failed", "canceled"].includes(item.status),
+ ).length;
+ const done = Math.max(countedDone, itemDone);
const progress = job.total_count ? Math.round((done / job.total_count) * 100) : 0;
return (
-
{job.status}{done}/{job.total_count} 项{new Date(job.created_at).toLocaleString()}
+
{STATUS_LABELS[job.status] || job.status}{done}/{job.total_count} 项{new Date(job.created_at).toLocaleString()}
{job.items?.map((item) => (
{item.skill_name}
- {PHASE_LABELS[item.phase] || item.phase}
+ {PHASE_LABELS[item.phase] || STATUS_LABELS[item.status] || item.phase}
{item.progress}%
{item.status === "failed" && actions.retry.mutate(item.id)}>重试}
@@ -256,8 +271,8 @@ export function SkillKnowledgePanel() {
-
当前有效绑定:{bindings.data?.length ?? 0};同名同类型实体按来源贡献合并。
-
{bindings.data?.map((binding) =>
{binding.skill_name}→ {binding.target_name}{binding.status}{binding.remote_business_key} actions.resync.mutate(binding.id)}>重归纳 { if (window.confirm("解除关联并保留目标知识内容?")) actions.detach.mutate({ id: binding.id, deleteContributions: false }); }}>解绑并保留 { if (window.confirm("解除关联并删除此技能贡献?共享知识仍会保留其他来源。")) actions.detach.mutate({ id: binding.id, deleteContributions: true }); }}>解绑并删除贡献
)}
+
已归纳关联:{bindings.data?.length ?? 0}
+
{bindings.data?.map((binding) =>
{binding.skill_name}已归纳到 {binding.target_name}{STATUS_LABELS[binding.status] || binding.status} actions.resync.mutate(binding.id)}>重新归纳 { if (window.confirm("解除关联并保留目标知识内容?")) actions.detach.mutate({ id: binding.id, deleteContributions: false }); }}>解除关联{adminMode && { if (window.confirm("解除关联并删除此技能贡献?共享知识仍会保留其他来源。")) actions.detach.mutate({ id: binding.id, deleteContributions: true }); }}>删除贡献}
)}
diff --git a/frontend-web/src/components/workspace/workspace-nav-menu.tsx b/frontend-web/src/components/workspace/workspace-nav-menu.tsx
index d3f48f8e..57c71201 100644
--- a/frontend-web/src/components/workspace/workspace-nav-menu.tsx
+++ b/frontend-web/src/components/workspace/workspace-nav-menu.tsx
@@ -83,7 +83,7 @@ export function WorkspaceNavMenu() {
const isAdmin = getStoredAuth()?.system_role === "admin";
const [settingsOpen, setSettingsOpen] = useState(false);
const [settingsDefaultSection, setSettingsDefaultSection] = useState<
- "appearance" | "memory" | "tools" | "about"
+ "appearance" | "memory" | "tools" | "encoder" | "about"
>("appearance");
const [notificationOpen, setNotificationOpen] = useState(false);
const [mounted, setMounted] = useState(false);
@@ -189,6 +189,17 @@ export function WorkspaceNavMenu() {
{t.common.settings}
+ {isAdmin && (
+ {
+ setSettingsDefaultSection("encoder");
+ setSettingsOpen(true);
+ }}
+ >
+
+ 本地编码器
+
+ )}
{isAdmin && (
@@ -254,14 +265,6 @@ export function WorkspaceNavMenu() {
技能管理
)}
- {isAdmin && (
- navigate("/page/workspace/admin/skills?tab=knowledge")}
- >
-
- 技能到知识库管理
-
- )}
{isAdmin && (
navigate("/page/workspace/admin/models")}
diff --git a/frontend-web/src/pages/AdminSkillsPage.tsx b/frontend-web/src/pages/AdminSkillsPage.tsx
index 6ac3ffc4..e69fad27 100644
--- a/frontend-web/src/pages/AdminSkillsPage.tsx
+++ b/frontend-web/src/pages/AdminSkillsPage.tsx
@@ -3,7 +3,6 @@
import {
ChevronLeftIcon,
ChevronRightIcon,
- DatabaseZap,
Replace,
SearchIcon,
SparklesIcon,
@@ -20,7 +19,6 @@ import {
WorkspaceHeader,
} from "@/components/workspace/workspace-container";
import { SkillRow } from "@/components/workspace/settings/skill-settings-page";
-import { SkillKnowledgePanel } from "@/components/workspace/skills/SkillKnowledgePanel";
import { getStoredAuth } from "@/core/auth";
import { useAllSkillsAdmin } from "@/core/skills/hooks";
import { desensitize } from "@/lib/desensitize";
@@ -37,7 +35,7 @@ export default function AdminSkillsPage() {
const knowledgeBindings = useSkillKnowledgeBindings();
const activeTab = useMemo(() => {
const tab = searchParams.get("tab");
- return tab === "addresses" || tab === "knowledge" ? tab : "skills";
+ return tab === "addresses" ? tab : "skills";
}, [searchParams]);
const changeTab = (value: string) => {
@@ -89,10 +87,6 @@ export default function AdminSkillsPage() {
技能地址管理
-
-
- 技能到知识库管理
-
@@ -189,12 +183,6 @@ export default function AdminSkillsPage() {
>
-
-
-
diff --git a/frontend-web/src/pages/AssistantKnowledgePage.tsx b/frontend-web/src/pages/AssistantKnowledgePage.tsx
index 55a6d2ea..c60de7ca 100644
--- a/frontend-web/src/pages/AssistantKnowledgePage.tsx
+++ b/frontend-web/src/pages/AssistantKnowledgePage.tsx
@@ -2,10 +2,12 @@ import {
ArrowLeft,
BookOpenText,
Database,
+ Edit3,
Loader2,
Plus,
RefreshCw,
Search,
+ Trash2,
Upload,
} from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
@@ -17,12 +19,14 @@ import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
+ DialogDescription,
+ DialogFooter,
DialogHeader,
DialogTitle,
- DialogFooter,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Textarea } from "@/components/ui/textarea";
import { KnowledgeProductNav } from "@/components/workspace/knowledge-product-nav";
import { getStoredAuth } from "@/core/auth";
import { buildAppHashUrl } from "@/core/llmwiki/source-links";
@@ -33,6 +37,7 @@ import {
useAssistantKnowledgeBases,
useAssistantKnowledgeSources,
useAssistantWikiPages,
+ type AssistantKnowledgeBase,
} from "@/strategy-components/api/assistant-knowledge";
import {
WikiLibraryReader,
@@ -53,6 +58,8 @@ export default function AssistantKnowledgePage() {
const client = useQueryClient();
const [tab, setTab] = useState("wiki");
const [createOpen, setCreateOpen] = useState(false);
+ const [editingBase, setEditingBase] =
+ useState
(null);
const [name, setName] = useState("");
const [query, setQuery] = useState("");
const [searching, setSearching] = useState(false);
@@ -124,6 +131,25 @@ export default function AssistantKnowledgePage() {
};
const refresh = () =>
void client.invalidateQueries({ queryKey: ["assistant-knowledge"] });
+ const removeBase = async (target: AssistantKnowledgeBase) => {
+ if (target.is_global) {
+ toast.error("知识梳理总库承担全库沉淀,不能删除");
+ return;
+ }
+ if (
+ !window.confirm(
+ `确定删除助手知识库“${target.name}”吗?库内 Wiki、向量、实体、关系和导入记录都会删除。`,
+ )
+ )
+ return;
+ try {
+ await actions.deleteBase.mutateAsync(target.id);
+ if (baseId === target.id) setParams({});
+ toast.success("助手知识库已删除");
+ } catch (error) {
+ toast.error((error as Error).message);
+ }
+ };
return (
@@ -150,34 +176,69 @@ export default function AssistantKnowledgePage() {
)}
{bases.data?.map((b) => (
-
setParams({ base_id: b.id })}
- className="group rounded-2xl border bg-card p-6 text-left shadow-sm transition hover:border-primary/40 hover:shadow-md"
+ className="group overflow-hidden rounded-2xl border bg-card text-left shadow-sm transition hover:border-primary/40 hover:shadow-md"
>
-
-
-
+
setParams({ base_id: b.id })}
+ className="w-full p-6 text-left"
+ >
+
+
+
+
+ {b.is_global && (
+
全库知识梳理
+ )}
- {b.is_global && (
- 全库知识梳理
- )}
-
-
- {b.name}
-
-
- {b.is_global
- ? "汇聚普通知识库的分析成果,保留分类、标签和知识关联。"
- : b.description ||
- "独立的 Wiki 知识空间,支持导入、导出与向量检索。"}
-
-
- {b.page_count || 0} Wiki
- {b.source_count || 0} 来源
- {b.relation_count || 0} 关系
-
-
+
+ {b.name}
+
+
+ {b.description ||
+ (b.is_global
+ ? "汇聚普通知识库的分析成果,保留分类、标签和知识关联。"
+ : "独立的 Wiki 知识空间,支持导入、导出与向量检索。")}
+
+
+ {b.page_count || 0} Wiki
+ {b.source_count || 0} 来源
+ {b.relation_count || 0} 关系
+
+
+ {isAdmin && (
+
+ setEditingBase(b)}
+ >
+
+ 编辑
+
+ {!b.is_global && (
+ void removeBase(b)}
+ >
+
+ 删除
+
+ )}
+ {b.is_global && (
+
+ 系统大库不可删除
+
+ )}
+
+ )}
+
))}
{!bases.isLoading && !bases.data?.length && isAdmin && (
@@ -222,6 +283,29 @@ export default function AssistantKnowledgePage() {
刷新
+ {isAdmin && (
+
base && setEditingBase(base)}
+ >
+
+ 编辑
+
+ )}
+ {isAdmin && base && !base.is_global && (
+
void removeBase(base)}
+ >
+
+ 删除
+
+ )}
{isAdmin && (
Wiki({base?.page_count || 0})
- 向量检索
+ 知识检索
来源与同步
{isAdmin && (
导入 / 导出记录
@@ -274,6 +358,7 @@ export default function AssistantKnowledgePage() {
o.mapping_id)?.mapping_id
}
+ initiallyCollapseTree
/>
@@ -310,26 +396,36 @@ export default function AssistantKnowledgePage() {
<>
{searchResult.retrieval_mode === "vector"
- ? "向量检索 · 余弦相似度"
+ ? "知识正文 / 片段 · 向量检索"
: searchResult.retrieval_mode === "empty"
? "没有匹配结果"
- : "关键词检索(暂无匹配模型的向量)"}
+ : "知识正文 / 片段 · 关键词检索(暂无匹配模型的向量)"}
{searchResult.results.map((p) => (
navigatePage(p.slug)}
- className="block w-full max-w-4xl rounded-xl border p-5 text-left hover:bg-muted"
+ type="button"
+ onClick={() => {
+ if (p.page_type !== "chunk") navigatePage(p.slug);
+ }}
+ className={`block w-full max-w-4xl rounded-xl border p-5 text-left ${p.page_type === "chunk" ? "cursor-default" : "hover:bg-muted"}`}
>
{p.title}
+ {p.page_type === "chunk" && (
+
+ 知识片段
+
+ )}
{p.score?.toFixed(4)}
- {p.summary || p.content_markdown}
+ {p.matched_section?.content ||
+ p.matched_content ||
+ p.content_markdown}
))}
@@ -425,6 +521,111 @@ export default function AssistantKnowledgePage() {
+ {editingBase && (
+
setEditingBase(null)}
+ onSave={async (input) => {
+ try {
+ await actions.updateBase.mutateAsync({
+ id: editingBase.id,
+ ...input,
+ });
+ setEditingBase(null);
+ toast.success("助手知识库信息已保存");
+ } catch (error) {
+ toast.error((error as Error).message);
+ }
+ }}
+ />
+ )}
);
}
+
+function EditAssistantBaseDialog({
+ base,
+ pending,
+ onClose,
+ onSave,
+}: {
+ base: AssistantKnowledgeBase;
+ pending: boolean;
+ onClose: () => void;
+ onSave: (input: { name: string; description: string }) => Promise;
+}) {
+ const [name, setName] = useState(base.name);
+ const [description, setDescription] = useState(base.description || "");
+
+ return (
+
+ );
+}
diff --git a/frontend-web/src/pages/SkillsPage.tsx b/frontend-web/src/pages/SkillsPage.tsx
index f9a5ea9d..a3c40ab5 100644
--- a/frontend-web/src/pages/SkillsPage.tsx
+++ b/frontend-web/src/pages/SkillsPage.tsx
@@ -1,9 +1,55 @@
+import { BookOpenCheck, Sparkles } from "lucide-react";
+import { useMemo } from "react";
+import { useSearchParams } from "react-router-dom";
+
import { SkillSettingsPage } from "@/components/workspace/settings/skill-settings-page";
+import { SkillKnowledgePanel } from "@/components/workspace/skills/SkillKnowledgePanel";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
export default function SkillsPage() {
+ const [searchParams, setSearchParams] = useSearchParams();
+ const tab = useMemo(
+ () => (searchParams.get("tab") === "knowledge" ? "knowledge" : "skills"),
+ [searchParams],
+ );
+
+ const changeTab = (value: string) => {
+ const next = new URLSearchParams(searchParams);
+ if (value === "knowledge") next.set("tab", "knowledge");
+ else next.delete("tab");
+ setSearchParams(next, { replace: true });
+ };
+
return (
-
-
-
+
+
+
+
+
+
+
+
+
);
}
diff --git a/frontend-web/src/pages/WorkspaceRoutes.tsx b/frontend-web/src/pages/WorkspaceRoutes.tsx
index c6b47dfd..a95ea626 100644
--- a/frontend-web/src/pages/WorkspaceRoutes.tsx
+++ b/frontend-web/src/pages/WorkspaceRoutes.tsx
@@ -1,85 +1,92 @@
-import { useEffect, useState, type CSSProperties, type ReactNode } from 'react';
-import { Navigate, Route, Routes, useLocation, useParams } from 'react-router-dom';
+import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
+import {
+ Navigate,
+ Route,
+ Routes,
+ useLocation,
+ useParams,
+} from "react-router-dom";
-import { getStoredAuth } from '@/core/auth';
-import { getAPIClient } from '@/core/api';
-import { isSentimentAgentVisible } from '@/core/sentiment-agent/config';
+import { getStoredAuth } from "@/core/auth";
+import { getAPIClient } from "@/core/api";
+import { isSentimentAgentVisible } from "@/core/sentiment-agent/config";
import {
resolveChatsIframeEntryRedirect,
resolveStandardChatFallback,
-} from '@/core/page-layout/chats-iframe-route';
-import { isEmbeddedHost } from '@/core/embed/frame-context';
+} from "@/core/page-layout/chats-iframe-route";
+import { isEmbeddedHost } from "@/core/embed/frame-context";
-import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
-import { PromptInputProvider } from '@/components/ai-elements/prompt-input';
+import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
+import { PromptInputProvider } from "@/components/ai-elements/prompt-input";
import {
CompactShortcutButtons,
LayoutModeSwitcher,
ThemeSwitcher,
-} from '@/components/workspace/appearance-controls';
-import { WorkspaceSidebar } from '@/components/workspace/workspace-sidebar';
-import { UserIdentityBadge } from '@/components/workspace/user-identity-badge';
-import { CommandPalette } from '@/components/workspace/command-palette';
-import { Toaster } from 'sonner';
-import { SubtasksProvider } from '@/core/tasks/context';
-import { ArtifactsProvider } from '@/components/workspace/artifacts';
-import { DocumentRewritePresentationProvider } from '@/core/document-rewrite/context';
+} from "@/components/workspace/appearance-controls";
+import { WorkspaceSidebar } from "@/components/workspace/workspace-sidebar";
+import { UserIdentityBadge } from "@/components/workspace/user-identity-badge";
+import { CommandPalette } from "@/components/workspace/command-palette";
+import { Toaster } from "sonner";
+import { SubtasksProvider } from "@/core/tasks/context";
+import { ArtifactsProvider } from "@/components/workspace/artifacts";
+import { DocumentRewritePresentationProvider } from "@/core/document-rewrite/context";
-import { RouteBaseProvider } from '@/core/route-base';
-import { agentRefOfThread } from '@/core/threads/utils';
+import { RouteBaseProvider } from "@/core/route-base";
+import { agentRefOfThread } from "@/core/threads/utils";
-import ChatsPage from './ChatsPage';
-import ChatPage from './ChatPage';
-import ArtifactLibraryPage from './ArtifactLibraryPage';
-import IframeChatPage from './IframeChatPage';
-import AgentsPage from './AgentsPage';
-import AgentChatPage from './AgentChatPage';
-import SentimentAgentChatPage from './SentimentAgentChatPage';
-import NewAgentPage from './NewAgentPage';
-import AdminAgentsPage from './AdminAgentsPage';
-import AdminScheduledTasksPage from './AdminScheduledTasksPage';
-import AdminSkillsPage from './AdminSkillsPage';
-import ScheduledTaskRunDetailPage from './ScheduledTaskRunDetailPage';
-import ScheduledTasksPage from './ScheduledTasksPage';
-import LlmWikiKnowledgePage from './LlmWikiKnowledgePage';
-import AssistantKnowledgePage from './AssistantKnowledgePage';
-import WeKnoraWikiManagementPage from '@/strategy-components/components/resource-management/llmwiki/WeKnoraWikiManagementPage';
-import WeKnoraWikiPageView from '@/strategy-components/components/resource-management/llmwiki/WeKnoraWikiPageView';
-import MarkdownDocxEditorPage from './MarkdownDocxEditorPage';
-import LlmMetricsPage from './LlmMetricsPage';
-import CheckpointMigrationPage from './CheckpointMigrationPage';
-import MemoryPage from './MemoryPage';
-import PromptPrefixSettingsPage from './PromptPrefixSettingsPage';
-import PrivateSquareSettingsPage from './PrivateSquareSettingsPage';
-import ModelConcurrencySettingsPage from './ModelConcurrencySettingsPage';
-import ModelManagementPage from './ModelManagementPage';
-import PublishSquaresSettingsPage from './PublishSquaresSettingsPage';
-import SkillCompressionSettingsPage from './SkillCompressionSettingsPage';
-import SkillEvolutionPage from './SkillEvolutionPage';
-import SkillsPage from './SkillsPage';
-import UsersPage from './UsersPage';
-import AdminUserActivityPage from './AdminUserActivityPage';
-import AdminMemoryPage from './AdminMemoryPage';
-import AdminUserHubPage from './AdminUserHubPage';
-import TagsPage from './TagsPage';
-import PositionsPage from './PositionsPage';
-import PositionRolesPage from './PositionRolesPage';
-import PluginManagementPage from './PluginManagementPage';
-import RecommendedQuestionsPage from './RecommendedQuestionsPage';
-import AIWritingPage from '@/open-canvas/pages/AIWritingPage';
-import DeepResearchPage from './DeepResearchPage';
-import ReportStructureCreatePage from './deep-research/ReportStructureCreatePage';
-import EnterpriseResearchPage from './EnterpriseResearchPage';
-import StudioNotebooksPage from './StudioNotebooksPage';
-import StudioNotebookDetailPage from './StudioNotebookDetailPage';
-import RoundtablePlanningPage from '@/roundtable-planning/pages/RoundtablePlanningPage';
-import PositionRoundtablePage from '@/position-roundtable/pages/PositionRoundtablePage';
-import EightBfRoundtablePage from '@/eightbf-roundtable/pages/EightBfRoundtablePage';
-import BusinessChainEditorPage from '@/roundtable-planning/pages/BusinessChainEditorPage';
-import BusinessChainListPage from '@/roundtable-planning/pages/BusinessChainListPage';
-import DispatchChainPreviewPage from '@/roundtable-planning/pages/DispatchChainPreviewPage';
-import '@/roundtable-planning/styles/roundtable-planning.css';
-import { cn } from '@/lib/utils';
+import ChatsPage from "./ChatsPage";
+import ChatPage from "./ChatPage";
+import ArtifactLibraryPage from "./ArtifactLibraryPage";
+import IframeChatPage from "./IframeChatPage";
+import AgentsPage from "./AgentsPage";
+import AgentChatPage from "./AgentChatPage";
+import SentimentAgentChatPage from "./SentimentAgentChatPage";
+import NewAgentPage from "./NewAgentPage";
+import AdminAgentsPage from "./AdminAgentsPage";
+import AdminScheduledTasksPage from "./AdminScheduledTasksPage";
+import AdminSkillsPage from "./AdminSkillsPage";
+import ScheduledTaskRunDetailPage from "./ScheduledTaskRunDetailPage";
+import ScheduledTasksPage from "./ScheduledTasksPage";
+import LlmWikiKnowledgePage from "./LlmWikiKnowledgePage";
+import AssistantKnowledgePage from "./AssistantKnowledgePage";
+import WeKnoraWikiManagementPage from "@/strategy-components/components/resource-management/llmwiki/WeKnoraWikiManagementPage";
+import WeKnoraWikiPageView from "@/strategy-components/components/resource-management/llmwiki/WeKnoraWikiPageView";
+import WeKnoraPackageTransfersPage from "@/strategy-components/components/resource-management/llmwiki/WeKnoraPackageTransfersPage";
+import MarkdownDocxEditorPage from "./MarkdownDocxEditorPage";
+import LlmMetricsPage from "./LlmMetricsPage";
+import CheckpointMigrationPage from "./CheckpointMigrationPage";
+import MemoryPage from "./MemoryPage";
+import PromptPrefixSettingsPage from "./PromptPrefixSettingsPage";
+import PrivateSquareSettingsPage from "./PrivateSquareSettingsPage";
+import ModelConcurrencySettingsPage from "./ModelConcurrencySettingsPage";
+import ModelManagementPage from "./ModelManagementPage";
+import PublishSquaresSettingsPage from "./PublishSquaresSettingsPage";
+import SkillCompressionSettingsPage from "./SkillCompressionSettingsPage";
+import SkillEvolutionPage from "./SkillEvolutionPage";
+import SkillsPage from "./SkillsPage";
+import UsersPage from "./UsersPage";
+import AdminUserActivityPage from "./AdminUserActivityPage";
+import AdminMemoryPage from "./AdminMemoryPage";
+import AdminUserHubPage from "./AdminUserHubPage";
+import TagsPage from "./TagsPage";
+import PositionsPage from "./PositionsPage";
+import PositionRolesPage from "./PositionRolesPage";
+import PluginManagementPage from "./PluginManagementPage";
+import RecommendedQuestionsPage from "./RecommendedQuestionsPage";
+import AIWritingPage from "@/open-canvas/pages/AIWritingPage";
+import DeepResearchPage from "./DeepResearchPage";
+import ReportStructureCreatePage from "./deep-research/ReportStructureCreatePage";
+import EnterpriseResearchPage from "./EnterpriseResearchPage";
+import StudioNotebooksPage from "./StudioNotebooksPage";
+import StudioNotebookDetailPage from "./StudioNotebookDetailPage";
+import RoundtablePlanningPage from "@/roundtable-planning/pages/RoundtablePlanningPage";
+import PositionRoundtablePage from "@/position-roundtable/pages/PositionRoundtablePage";
+import EightBfRoundtablePage from "@/eightbf-roundtable/pages/EightBfRoundtablePage";
+import BusinessChainEditorPage from "@/roundtable-planning/pages/BusinessChainEditorPage";
+import BusinessChainListPage from "@/roundtable-planning/pages/BusinessChainListPage";
+import DispatchChainPreviewPage from "@/roundtable-planning/pages/DispatchChainPreviewPage";
+import "@/roundtable-planning/styles/roundtable-planning.css";
+import { cn } from "@/lib/utils";
function NotebookDetailPageWithKey() {
const { notebookId } = useParams<{ notebookId: string }>();
@@ -94,12 +101,12 @@ function NewAgentPageWithKey() {
function shouldShowCompactTopActions(pathname: string): boolean {
return (
- pathname === '/page/workspace/agents' ||
- pathname === '/page/workspace/skills' ||
- pathname === '/page/workspace/memory' ||
- pathname === '/page/workspace/library' ||
- pathname === '/page/workspace/knowledge' ||
- pathname.startsWith('/page/workspace/knowledge/')
+ pathname === "/page/workspace/agents" ||
+ pathname === "/page/workspace/skills" ||
+ pathname === "/page/workspace/memory" ||
+ pathname === "/page/workspace/library" ||
+ pathname === "/page/workspace/knowledge" ||
+ pathname.startsWith("/page/workspace/knowledge/")
);
}
@@ -157,47 +164,52 @@ export function WorkspaceLayout({
// 全量模式下通用问答(/page/workspace/chats*)的历史侧栏默认展开;其余紧凑页
// (如智能体对话,为聊天区更宽而默认收起)维持原默认。
const pathname = useLocation().pathname;
- const isGeneralQa = pathname.startsWith('/page/workspace/chats');
+ const isGeneralQa = pathname.startsWith("/page/workspace/chats");
const showCompactTopActions =
- headerHidden && !compactActionsHidden && shouldShowCompactTopActions(pathname);
+ headerHidden &&
+ !compactActionsHidden &&
+ shouldShowCompactTopActions(pathname);
const pageSidebarWidth = pageSidebarHidden
- ? '0rem'
+ ? "0rem"
: pageSidebarDark
? pageSidebarCollapsed
- ? '0rem'
- : '118px'
+ ? "0rem"
+ : "118px"
: pageSidebarCollapsed
- ? '4rem'
- : '16rem';
+ ? "4rem"
+ : "16rem";
return (
undefined : undefined}
+ onOpenChange={
+ forceWorkspaceSidebarCollapsed ? () => undefined : undefined
+ }
disableToggle={forceWorkspaceSidebarCollapsed}
>
{!workspaceSidebarHidden && (
@@ -207,7 +219,7 @@ export function WorkspaceLayout({
/>
)}
{showCompactTopActions ? (
<>
@@ -244,9 +256,14 @@ export function ChatRuntime({ children }: { children: ReactNode }) {
*/
function NewChatContent() {
const location = useLocation();
- const agentId = new URLSearchParams(location.search).get('agent');
+ const agentId = new URLSearchParams(location.search).get("agent");
if (agentId) {
- return ;
+ return (
+
+ );
}
return ;
}
@@ -393,8 +410,8 @@ function AgentAwareChatThreadContent() {
};
// Task-bound threads need their task host to preserve the purpose gate,
// action partitioning, and other task-specific controls.
- const isTaskThread = typeof record.metadata?.taskId === 'string';
- setAgentId(isTaskThread ? null : agentRefOfThread(record) ?? null);
+ const isTaskThread = typeof record.metadata?.taskId === "string";
+ setAgentId(isTaskThread ? null : (agentRefOfThread(record) ?? null));
})
.catch(() => {
if (!cancelled) setAgentId(null);
@@ -537,11 +554,18 @@ export default function WorkspaceRoutes({
/>
}
+ element={
+
+ }
/>
}
+ element={
+
+ }
/>
+
) : (
)
@@ -605,7 +632,9 @@ export default function WorkspaceRoutes({
: undefined
+ headerHidden && !compactActionsHidden ? (
+
+ ) : undefined
}
/>
@@ -704,7 +733,9 @@ export default function WorkspaceRoutes({
element={
-
+
}
@@ -714,7 +745,9 @@ export default function WorkspaceRoutes({
element={
-
+
}
@@ -724,7 +757,9 @@ export default function WorkspaceRoutes({
element={
-
+
}
@@ -794,6 +829,14 @@ export default function WorkspaceRoutes({
}
/>
+
+
+
+ }
+ />
-
+
}
/>
@@ -834,7 +879,7 @@ export default function WorkspaceRoutes({
@@ -859,7 +904,7 @@ export default function WorkspaceRoutes({
@@ -871,7 +916,7 @@ export default function WorkspaceRoutes({
@@ -883,7 +928,7 @@ export default function WorkspaceRoutes({
@@ -903,7 +948,7 @@ export default function WorkspaceRoutes({
@@ -915,7 +960,7 @@ export default function WorkspaceRoutes({
@@ -927,7 +972,7 @@ export default function WorkspaceRoutes({
@@ -939,7 +984,7 @@ export default function WorkspaceRoutes({
@@ -951,7 +996,7 @@ export default function WorkspaceRoutes({
@@ -963,7 +1008,7 @@ export default function WorkspaceRoutes({
@@ -975,7 +1020,7 @@ export default function WorkspaceRoutes({
@@ -987,7 +1032,7 @@ export default function WorkspaceRoutes({
@@ -999,7 +1044,7 @@ export default function WorkspaceRoutes({
@@ -1011,7 +1056,7 @@ export default function WorkspaceRoutes({
@@ -1023,7 +1068,7 @@ export default function WorkspaceRoutes({
@@ -1035,7 +1080,7 @@ export default function WorkspaceRoutes({
diff --git a/frontend-web/src/strategy-components/api/assistant-knowledge.ts b/frontend-web/src/strategy-components/api/assistant-knowledge.ts
index 6e287bea..f3eb8af0 100644
--- a/frontend-web/src/strategy-components/api/assistant-knowledge.ts
+++ b/frontend-web/src/strategy-components/api/assistant-knowledge.ts
@@ -39,6 +39,17 @@ export interface AssistantWikiPage {
out_links?: string[];
source_refs?: string[];
origins?: { mapping_id?: string; source_name: string; slug: string }[];
+ matched_content?: string;
+ matched_section?: {
+ id?: string;
+ section_index: number;
+ heading?: string | null;
+ content: string;
+ score: number;
+ source_chunk_id?: string | null;
+ embedding_model?: string | null;
+ embedding_fingerprint?: string | null;
+ };
}
export interface AssistantWikiRevision {
@@ -177,6 +188,32 @@ export async function createAssistantKnowledgeBase(input: {
return data.base;
}
+export async function updateAssistantKnowledgeBase(input: {
+ id: string;
+ name: string;
+ description?: string;
+}) {
+ const data = await readJson<{ base: AssistantKnowledgeBase }>(
+ await apiFetch(`${BASE()}/bases/${encodeURIComponent(input.id)}`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ name: input.name,
+ description: input.description || "",
+ }),
+ }),
+ );
+ return data.base;
+}
+
+export async function deleteAssistantKnowledgeBase(id: string) {
+ const response = await apiFetch(
+ `${BASE()}/bases/${encodeURIComponent(id)}`,
+ { method: "DELETE" },
+ );
+ if (!response.ok) await readJson(response);
+}
+
function withBaseParam(baseId?: string | null) {
const params = new URLSearchParams();
if (baseId) params.set("base_id", baseId);
@@ -441,6 +478,14 @@ export function useAssistantKnowledgeActions() {
mutationFn: createAssistantKnowledgeBase,
onSuccess: invalidate,
}),
+ updateBase: useMutation({
+ mutationFn: updateAssistantKnowledgeBase,
+ onSuccess: invalidate,
+ }),
+ deleteBase: useMutation({
+ mutationFn: deleteAssistantKnowledgeBase,
+ onSuccess: invalidate,
+ }),
uploadWeKnoraPackage: useMutation({
mutationFn: uploadAssistantWeKnoraPackage,
onSuccess: invalidate,
diff --git a/frontend-web/src/strategy-components/api/llmwiki.ts b/frontend-web/src/strategy-components/api/llmwiki.ts
index a02c6d74..6111c40b 100644
--- a/frontend-web/src/strategy-components/api/llmwiki.ts
+++ b/frontend-web/src/strategy-components/api/llmwiki.ts
@@ -1,6 +1,8 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiFetch } from "@/core/api/fetch-client";
+import { getCsrfToken } from "@/core/api/csrf";
+import { getAuthorizationHeaderValue } from "@/core/auth";
import { useLiveAccount } from "@/core/auth/use-live-account";
import { getBackendBaseURL } from "@/core/config";
@@ -220,7 +222,14 @@ export interface LlmWikiWikiIndex {
export interface LlmWikiWikiIndexStatus {
knowledge_base_id: string;
- state: "not_synced" | "idle" | "running" | "partial" | "failed" | "rebuilding" | "rebuild_required";
+ state:
+ | "not_synced"
+ | "idle"
+ | "running"
+ | "partial"
+ | "failed"
+ | "rebuilding"
+ | "rebuild_required";
local_index_enabled: boolean;
wiki_index_enabled: boolean;
fully_vectorized: boolean;
@@ -256,6 +265,25 @@ export interface LlmWikiWikiIndexStatus {
}>;
}
+export interface LocalWikiEncoderStatus {
+ provider: string;
+ model: string;
+ dimensions?: number | null;
+ state: "idle" | "starting" | "ready" | "failed";
+ running: boolean;
+ last_error?: string | null;
+ started_at?: string | null;
+ ready_at?: string | null;
+ offline_only: boolean;
+ model_path_configured: boolean;
+ model_path_exists: boolean;
+ model_files_ready: boolean;
+ process_isolation: boolean;
+ model_path: string;
+ uploaded_files?: number;
+ uploaded_bytes?: number;
+}
+
export interface LlmWikiSourceContext {
knowledge_base_name?: string;
document_title?: string;
@@ -345,14 +373,16 @@ function userFacingKnowledgeMessage(value: string): string {
async function readJson(response: Response, context: string): Promise {
if (!response.ok) {
const payload = await response.json().catch(() => null as unknown);
- const rawDetail = payload && typeof payload === "object" && "detail" in payload
- ? (payload as { detail: unknown }).detail
- : null;
- const detail = rawDetail && typeof rawDetail === "object" && "message" in rawDetail
- ? String((rawDetail as { message: unknown }).message)
- : rawDetail
- ? String(rawDetail)
- : `${context}失败(${response.status})`;
+ const rawDetail =
+ payload && typeof payload === "object" && "detail" in payload
+ ? (payload as { detail: unknown }).detail
+ : null;
+ const detail =
+ rawDetail && typeof rawDetail === "object" && "message" in rawDetail
+ ? String((rawDetail as { message: unknown }).message)
+ : rawDetail
+ ? String(rawDetail)
+ : `${context}失败(${response.status})`;
throw new Error(userFacingKnowledgeMessage(detail));
}
if (response.status === 204) return undefined as T;
@@ -360,10 +390,7 @@ async function readJson(response: Response, context: string): Promise {
}
export async function getLlmWikiRuntime(): Promise {
- return readJson(
- await apiFetch(`${API()}/runtime`),
- "读取 LLMWiki 运行模式",
- );
+ return readJson(await apiFetch(`${API()}/runtime`), "读取 LLMWiki 运行模式");
}
export async function listLlmWikiKnowledgeBases(
@@ -421,10 +448,10 @@ export async function getLlmWikiSourceContext(
knowledge_base_id: knowledgeBaseId,
chunk_id: chunkId,
});
- const payload = await readJson<{ success: boolean; data: LlmWikiSourceContext }>(
- await apiFetch(`${API()}/sources/context?${params}`),
- "加载参考文献原文",
- );
+ const payload = await readJson<{
+ success: boolean;
+ data: LlmWikiSourceContext;
+ }>(await apiFetch(`${API()}/sources/context?${params}`), "加载参考文献原文");
return payload.data;
}
@@ -537,9 +564,13 @@ export async function getLlmWikiProtectedFileUrl(
return `${PUBLIC_API()}/files/${encodeURIComponent(access.token)}`;
}
-export async function getLlmWikiGraph(knowledgeBaseId: string): Promise {
+export async function getLlmWikiGraph(
+ knowledgeBaseId: string,
+): Promise {
return readJson(
- await apiFetch(`${API()}/knowledge-bases/${encodeURIComponent(knowledgeBaseId)}/graph?limit=200`),
+ await apiFetch(
+ `${API()}/knowledge-bases/${encodeURIComponent(knowledgeBaseId)}/graph?limit=200`,
+ ),
"读取知识图谱",
);
}
@@ -550,7 +581,10 @@ export async function listLlmWikiWikiPages(
pageSize: number,
query = "",
): Promise {
- const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) });
+ const params = new URLSearchParams({
+ page: String(page),
+ page_size: String(pageSize),
+ });
if (query.trim()) params.set("query", query.trim());
return readJson(
await apiFetch(
@@ -596,6 +630,80 @@ export async function vectorizeLlmWikiKnowledgeBase(
);
}
+export async function getLocalWikiEncoderStatus(): Promise {
+ return readJson(
+ await apiFetch(`${API()}/local-encoder/status`),
+ "读取本地编码器状态",
+ );
+}
+
+export async function startLocalWikiEncoder(): Promise {
+ return readJson(
+ await apiFetch(`${API()}/local-encoder/start`, { method: "POST" }),
+ "启动本地编码器",
+ );
+}
+
+export async function configureLocalWikiEncoder(
+ modelPath: string,
+): Promise {
+ return readJson(
+ await apiFetch(`${API()}/local-encoder/config`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ model_path: modelPath }),
+ }),
+ "保存本地编码器目录",
+ );
+}
+
+export async function uploadLocalWikiEncoder(
+ files: File[],
+ onProgress?: (percent: number) => void,
+): Promise {
+ const body = new FormData();
+ for (const file of files) {
+ const webkitPath = (file as File & { webkitRelativePath?: string }).webkitRelativePath || file.name;
+ const parts = webkitPath.replace(/\\/g, "/").split("/").filter(Boolean);
+ const relativePath = parts.length > 1 ? parts.slice(1).join("/") : parts[0];
+ body.append("files", file, file.name);
+ body.append("paths", relativePath);
+ }
+ return new Promise((resolve, reject) => {
+ const request = new XMLHttpRequest();
+ request.open("POST", `${API()}/local-encoder/upload`);
+ request.withCredentials = true;
+ const authorization = getAuthorizationHeaderValue();
+ const csrf = getCsrfToken();
+ if (authorization) request.setRequestHeader("Authorization", authorization);
+ if (csrf) request.setRequestHeader("X-CSRF-Token", csrf);
+ request.upload.onprogress = (event) => {
+ if (event.lengthComputable && event.total > 0) {
+ onProgress?.(Math.min(99, Math.round((event.loaded / event.total) * 100)));
+ }
+ };
+ request.onerror = () => reject(new Error("上传本地编码器失败:网络连接中断"));
+ request.onload = () => {
+ let payload: unknown = null;
+ try {
+ payload = JSON.parse(request.responseText || "null");
+ } catch {
+ payload = null;
+ }
+ if (request.status < 200 || request.status >= 300) {
+ const detail = payload && typeof payload === "object" && "detail" in payload
+ ? (payload as { detail: unknown }).detail
+ : `请求失败 (${request.status})`;
+ reject(new Error(typeof detail === "string" ? detail : JSON.stringify(detail)));
+ return;
+ }
+ onProgress?.(100);
+ resolve(payload as LocalWikiEncoderStatus);
+ };
+ request.send(body);
+ });
+}
+
export async function exportLlmWikiWikiExcel(
knowledgeBaseId: string,
knowledgeBaseName = "LLMWiki",
@@ -808,7 +916,10 @@ export async function listPublicLlmWikiWikiPages(
pageSize: number,
query = "",
): Promise {
- const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) });
+ const params = new URLSearchParams({
+ page: String(page),
+ page_size: String(pageSize),
+ });
if (query.trim()) params.set("query", query.trim());
return readJson(
await apiFetch(
@@ -887,9 +998,7 @@ export async function resetLlmWikiSettings(): Promise {
);
}
-export async function testLlmWikiConnection(
- apiBaseUrl: string,
-): Promise {
+export async function testLlmWikiConnection(apiBaseUrl: string): Promise {
await readJson(
await apiFetch(`${SETTINGS_API()}/test`, {
method: "POST",
@@ -914,10 +1023,7 @@ export function useLlmWikiRuntime() {
});
}
-export function useLlmWikiKnowledgeBases(
- scope: LlmWikiScope,
- enabled = true,
-) {
+export function useLlmWikiKnowledgeBases(scope: LlmWikiScope, enabled = true) {
const { userId } = useLiveAccount();
return useQuery({
queryKey: knowledgeBasesKey(userId, scope),
@@ -933,7 +1039,13 @@ export function useAgentLlmWikiKnowledgeBases(
) {
const { userId } = useLiveAccount();
return useQuery({
- queryKey: ["llmwiki", "agents", agentId, "knowledge-bases", userId ?? "anonymous"],
+ queryKey: [
+ "llmwiki",
+ "agents",
+ agentId,
+ "knowledge-bases",
+ userId ?? "anonymous",
+ ],
queryFn: () => listAgentLlmWikiKnowledgeBases(agentId!),
enabled: enabled && !!agentId,
});
@@ -945,7 +1057,12 @@ export function useLlmWikiKnowledgeBase(
) {
const { userId } = useLiveAccount();
return useQuery({
- queryKey: ["llmwiki", "knowledge-base", knowledgeBaseId, userId ?? "anonymous"],
+ queryKey: [
+ "llmwiki",
+ "knowledge-base",
+ knowledgeBaseId,
+ userId ?? "anonymous",
+ ],
queryFn: () => getLlmWikiKnowledgeBase(knowledgeBaseId!),
enabled: enabled && !!knowledgeBaseId,
refetchInterval: 8_000,
@@ -972,7 +1089,13 @@ export function useLlmWikiDocument(
) {
const { userId } = useLiveAccount();
return useQuery({
- queryKey: ["llmwiki", "document", knowledgeBaseId, documentId, userId ?? "anonymous"],
+ queryKey: [
+ "llmwiki",
+ "document",
+ knowledgeBaseId,
+ documentId,
+ userId ?? "anonymous",
+ ],
queryFn: () => getLlmWikiDocument(knowledgeBaseId!, documentId!),
enabled: enabled && !!knowledgeBaseId && !!documentId,
});
@@ -987,13 +1110,25 @@ export function useLlmWikiDocumentChunks(
) {
const { userId } = useLiveAccount();
return useQuery({
- queryKey: ["llmwiki", "document-chunks", knowledgeBaseId, documentId, page, pageSize, userId ?? "anonymous"],
- queryFn: () => listLlmWikiDocumentChunks(knowledgeBaseId!, documentId!, page, pageSize),
+ queryKey: [
+ "llmwiki",
+ "document-chunks",
+ knowledgeBaseId,
+ documentId,
+ page,
+ pageSize,
+ userId ?? "anonymous",
+ ],
+ queryFn: () =>
+ listLlmWikiDocumentChunks(knowledgeBaseId!, documentId!, page, pageSize),
enabled: enabled && !!knowledgeBaseId && !!documentId,
});
}
-export function useLlmWikiGraph(knowledgeBaseId: string | null, enabled = true) {
+export function useLlmWikiGraph(
+ knowledgeBaseId: string | null,
+ enabled = true,
+) {
const { userId } = useLiveAccount();
return useQuery({
queryKey: ["llmwiki", "graph", knowledgeBaseId, userId ?? "anonymous"],
@@ -1011,8 +1146,17 @@ export function useLlmWikiWikiPages(
) {
const { userId } = useLiveAccount();
return useQuery({
- queryKey: ["llmwiki", "wiki-pages", knowledgeBaseId, page, pageSize, query, userId ?? "anonymous"],
- queryFn: () => listLlmWikiWikiPages(knowledgeBaseId!, page, pageSize, query),
+ queryKey: [
+ "llmwiki",
+ "wiki-pages",
+ knowledgeBaseId,
+ page,
+ pageSize,
+ query,
+ userId ?? "anonymous",
+ ],
+ queryFn: () =>
+ listLlmWikiWikiPages(knowledgeBaseId!, page, pageSize, query),
enabled: enabled && !!knowledgeBaseId,
refetchInterval: 10_000,
});
@@ -1025,7 +1169,13 @@ export function useLlmWikiWikiPage(
) {
const { userId } = useLiveAccount();
return useQuery({
- queryKey: ["llmwiki", "wiki-page", knowledgeBaseId, slug, userId ?? "anonymous"],
+ queryKey: [
+ "llmwiki",
+ "wiki-page",
+ knowledgeBaseId,
+ slug,
+ userId ?? "anonymous",
+ ],
queryFn: () => getLlmWikiWikiPage(knowledgeBaseId!, slug!),
enabled: enabled && !!knowledgeBaseId && !!slug,
});
@@ -1051,8 +1201,16 @@ export function usePublicLlmWikiWikiPages(
enabled = true,
) {
return useQuery({
- queryKey: ["public-llmwiki", "wiki-pages", knowledgeBaseId, page, pageSize, query],
- queryFn: () => listPublicLlmWikiWikiPages(knowledgeBaseId!, page, pageSize, query),
+ queryKey: [
+ "public-llmwiki",
+ "wiki-pages",
+ knowledgeBaseId,
+ page,
+ pageSize,
+ query,
+ ],
+ queryFn: () =>
+ listPublicLlmWikiWikiPages(knowledgeBaseId!, page, pageSize, query),
enabled: enabled && !!knowledgeBaseId,
staleTime: 60_000,
});
@@ -1089,7 +1247,12 @@ export function useLlmWikiWikiIndexStatus(
) {
const { userId } = useLiveAccount();
return useQuery({
- queryKey: ["llmwiki", "wiki-index-status", knowledgeBaseId, userId ?? "anonymous"],
+ queryKey: [
+ "llmwiki",
+ "wiki-index-status",
+ knowledgeBaseId,
+ userId ?? "anonymous",
+ ],
queryFn: () => getLlmWikiWikiIndexStatus(knowledgeBaseId!),
enabled: enabled && !!knowledgeBaseId,
refetchInterval: (query) => {
@@ -1118,8 +1281,14 @@ export function useLlmWikiActions() {
onSuccess: invalidateKnowledge,
}),
update: useMutation({
- mutationFn: ({ id, ...input }: { id: string; name?: string; description?: string }) =>
- updateLlmWikiKnowledgeBase(id, input),
+ mutationFn: ({
+ id,
+ ...input
+ }: {
+ id: string;
+ name?: string;
+ description?: string;
+ }) => updateLlmWikiKnowledgeBase(id, input),
onSuccess: invalidateKnowledge,
}),
remove: useMutation({
@@ -1137,8 +1306,15 @@ export function useLlmWikiActions() {
onSuccess: invalidateKnowledge,
}),
createManual: useMutation({
- mutationFn: ({ id, title, content }: { id: string; title: string; content: string }) =>
- createManualLlmWikiDocument(id, { title, content }),
+ mutationFn: ({
+ id,
+ title,
+ content,
+ }: {
+ id: string;
+ title: string;
+ content: string;
+ }) => createManualLlmWikiDocument(id, { title, content }),
onSuccess: invalidateKnowledge,
}),
removeDocument: useMutation({
@@ -1152,13 +1328,25 @@ export function useLlmWikiActions() {
onSuccess: invalidateKnowledge,
}),
createWikiPage: useMutation({
- mutationFn: ({ id, input }: { id: string; input: LlmWikiWikiPageCreate }) =>
- createLlmWikiWikiPage(id, input),
+ mutationFn: ({
+ id,
+ input,
+ }: {
+ id: string;
+ input: LlmWikiWikiPageCreate;
+ }) => createLlmWikiWikiPage(id, input),
onSuccess: invalidateKnowledge,
}),
updateWikiPage: useMutation({
- mutationFn: ({ id, slug, input }: { id: string; slug: string; input: LlmWikiWikiPageUpdate }) =>
- updateLlmWikiWikiPage(id, slug, input),
+ mutationFn: ({
+ id,
+ slug,
+ input,
+ }: {
+ id: string;
+ slug: string;
+ input: LlmWikiWikiPageUpdate;
+ }) => updateLlmWikiWikiPage(id, slug, input),
onSuccess: invalidateKnowledge,
}),
removeWikiPage: useMutation({
@@ -1188,8 +1376,14 @@ export function useLlmWikiSettingsActions() {
await queryClient.invalidateQueries({ queryKey: ["llmwiki"] });
};
return {
- save: useMutation({ mutationFn: saveLlmWikiSettings, onSuccess: invalidate }),
- reset: useMutation({ mutationFn: resetLlmWikiSettings, onSuccess: invalidate }),
+ save: useMutation({
+ mutationFn: saveLlmWikiSettings,
+ onSuccess: invalidate,
+ }),
+ reset: useMutation({
+ mutationFn: resetLlmWikiSettings,
+ onSuccess: invalidate,
+ }),
test: useMutation({ mutationFn: testLlmWikiConnection }),
};
}
diff --git a/frontend-web/src/strategy-components/api/skill-knowledge.ts b/frontend-web/src/strategy-components/api/skill-knowledge.ts
index cc8f7ccc..ae6a5ca3 100644
--- a/frontend-web/src/strategy-components/api/skill-knowledge.ts
+++ b/frontend-web/src/strategy-components/api/skill-knowledge.ts
@@ -109,9 +109,11 @@ async function readJson(response: Response): Promise {
export async function listSkillKnowledgeBindings(
skillNames?: string[],
+ mineOnly = false,
): Promise {
const params = new URLSearchParams();
for (const name of skillNames ?? []) params.append("skill_names", name);
+ if (mineOnly) params.set("mine_only", "true");
const response = await apiFetch(`${BASE()}/bindings?${params.toString()}`);
return (await readJson<{ bindings: SkillKnowledgeBinding[] }>(response)).bindings;
}
@@ -131,8 +133,9 @@ export async function createSkillKnowledgeJob(input: {
);
}
-export async function listSkillKnowledgeJobs(): Promise {
- const response = await apiFetch(`${BASE()}/sync-jobs`);
+export async function listSkillKnowledgeJobs(mineOnly = false): Promise {
+ const params = mineOnly ? "?mine_only=true" : "";
+ const response = await apiFetch(`${BASE()}/sync-jobs${params}`);
return (await readJson<{ jobs: SkillKnowledgeJob[] }>(response)).jobs;
}
@@ -221,18 +224,18 @@ export async function updateSkillKnowledgeAutoSyncSettings(
const KEY = "skill-knowledge";
-export function useSkillKnowledgeBindings(skillNames?: string[]) {
+export function useSkillKnowledgeBindings(skillNames?: string[], mineOnly = false) {
return useQuery({
- queryKey: [KEY, "bindings", ...(skillNames ?? [])],
- queryFn: () => listSkillKnowledgeBindings(skillNames),
+ queryKey: [KEY, "bindings", mineOnly ? "mine" : "all", ...(skillNames ?? [])],
+ queryFn: () => listSkillKnowledgeBindings(skillNames, mineOnly),
refetchOnWindowFocus: false,
});
}
-export function useSkillKnowledgeJobs() {
+export function useSkillKnowledgeJobs(mineOnly = false) {
return useQuery({
- queryKey: [KEY, "jobs"],
- queryFn: listSkillKnowledgeJobs,
+ queryKey: [KEY, "jobs", mineOnly ? "mine" : "all"],
+ queryFn: () => listSkillKnowledgeJobs(mineOnly),
refetchInterval: (query) =>
(query.state.data ?? []).some((job) => ["queued", "running"].includes(job.status))
? 1500
diff --git a/frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraKnowledgeDetailPage.tsx b/frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraKnowledgeDetailPage.tsx
index b7bbcfed..64e4dcab 100644
--- a/frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraKnowledgeDetailPage.tsx
+++ b/frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraKnowledgeDetailPage.tsx
@@ -1,15 +1,31 @@
-import { AlertCircle, Download, Edit3, FileText, Loader2, RefreshCw, Trash2 } from "lucide-react";
-import { useCallback, useEffect, useRef, useState } from "react";
-import { Navigate, useNavigate, useParams, useSearchParams } from "react-router-dom";
+import {
+ AlertCircle,
+ Archive,
+ Edit3,
+ FileText,
+ Loader2,
+ RefreshCw,
+ Trash2,
+} from "lucide-react";
+import { useEffect, useRef, useState } from "react";
+import {
+ Navigate,
+ useNavigate,
+ useParams,
+ useSearchParams,
+} from "react-router-dom";
import { toast } from "sonner";
-import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { MarkdownCodeEditor } from "@/components/wiki/MarkdownCodeEditor";
-import { WikiPackageTransfers } from "./WikiPackageTransfers";
import { getBackendBaseURL } from "@/core/config";
import { getStoredAuth } from "@/core/auth";
import { buildWeKnoraWikiPagePath } from "@/core/llmwiki/source-links";
@@ -21,20 +37,8 @@ import {
useLlmWikiWikiPage,
useLlmWikiRuntime,
} from "@/strategy-components/api/llmwiki";
-import {
- createLlmWikiExportPackage,
- listLlmWikiExportPackages,
- type LlmWikiExportPackageJob,
-} from "@/strategy-components/api/assistant-knowledge";
const LIST_PATH = "/page/workspace/knowledge";
-const RUNNING_EXPORT_STATUSES = new Set(["queued", "running"]);
-const EXPORT_STATUS_LABELS: Record = {
- queued: "排队中",
- running: "整理中",
- completed: "已完成",
- failed: "失败",
-};
type DeerFlowThemePayload = {
type: "deerflow-weknora-theme";
@@ -44,7 +48,10 @@ type DeerFlowThemePayload = {
function cssVar(name: string, fallback = ""): string {
if (typeof window === "undefined") return fallback;
- return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
+ return (
+ getComputedStyle(document.documentElement).getPropertyValue(name).trim() ||
+ fallback
+ );
}
function collectThemePayload(): DeerFlowThemePayload {
@@ -56,15 +63,27 @@ function collectThemePayload(): DeerFlowThemePayload {
theme: isDark ? "dark" : "light",
vars: {
primary: cssVar("--primary", isDark ? "#2b81ff" : "#0062d9"),
- primaryForeground: cssVar("--button-foreground", cssVar("--primary-foreground", "#ffffff")),
+ primaryForeground: cssVar(
+ "--button-foreground",
+ cssVar("--primary-foreground", "#ffffff"),
+ ),
background: cssVar("--background", isDark ? "#0e172b" : "#ffffff"),
foreground: cssVar("--foreground", isDark ? "#eff2f5" : "#202a3a"),
card: cssVar("--card", isDark ? "#1a2539" : "#ffffff"),
- cardForeground: cssVar("--card-foreground", cssVar("--foreground", isDark ? "#eff2f5" : "#202a3a")),
+ cardForeground: cssVar(
+ "--card-foreground",
+ cssVar("--foreground", isDark ? "#eff2f5" : "#202a3a"),
+ ),
muted: cssVar("--muted", isDark ? "#232e40" : "#f3f5f8"),
- mutedForeground: cssVar("--muted-foreground", isDark ? "#8e9aa4" : "#7f7f7f"),
+ mutedForeground: cssVar(
+ "--muted-foreground",
+ isDark ? "#8e9aa4" : "#7f7f7f",
+ ),
border: cssVar("--border", isDark ? "rgba(255,255,255,0.12)" : "#dbdbdb"),
- popover: cssVar("--popover", cssVar("--card", isDark ? "#1a2539" : "#ffffff")),
+ popover: cssVar(
+ "--popover",
+ cssVar("--card", isDark ? "#1a2539" : "#ffffff"),
+ ),
destructive: cssVar("--destructive", "#d54941"),
},
};
@@ -78,7 +97,8 @@ function postThemeToFrame(frame: HTMLIFrameElement | null) {
try {
const currentUrl = frame.contentWindow.location.href;
if (!currentUrl || currentUrl === "about:blank") return;
- if (new URL(currentUrl, window.location.href).origin !== targetOrigin) return;
+ if (new URL(currentUrl, window.location.href).origin !== targetOrigin)
+ return;
} catch {
// Reading location throws once the iframe has reached the cross-origin backend.
}
@@ -99,26 +119,9 @@ function buildFrameUrl(framePath: string) {
: framePath;
return new URL(path, backendOrigin);
}
- return new URL(`${backendBase || backendOrigin}/${framePath.replace(/^\/+/, "")}`);
-}
-
-function formatBytes(value: number | undefined | null) {
- const bytes = Number(value || 0);
- if (!Number.isFinite(bytes) || bytes <= 0) return "—";
- const units = ["B", "KB", "MB", "GB", "TB"];
- let current = bytes;
- let unitIndex = 0;
- while (current >= 1024 && unitIndex < units.length - 1) {
- current /= 1024;
- unitIndex += 1;
- }
- return `${current >= 10 || unitIndex === 0 ? current.toFixed(0) : current.toFixed(1)} ${units[unitIndex]}`;
-}
-
-function formatTime(value: string | undefined | null) {
- if (!value) return "—";
- const time = new Date(value);
- return Number.isNaN(time.getTime()) ? "—" : time.toLocaleString();
+ return new URL(
+ `${backendBase || backendOrigin}/${framePath.replace(/^\/+/, "")}`,
+ );
}
export function WeKnoraKnowledgeDetailPage() {
@@ -126,33 +129,19 @@ export function WeKnoraKnowledgeDetailPage() {
knowledge_base_id: strategyKnowledgeBaseId,
noteId: workspaceKnowledgeBaseId,
} = useParams();
- const knowledgeBaseId = strategyKnowledgeBaseId ?? workspaceKnowledgeBaseId ?? "";
+ const knowledgeBaseId =
+ strategyKnowledgeBaseId ?? workspaceKnowledgeBaseId ?? "";
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const frameRef = useRef(null);
const [frameSrc, setFrameSrc] = useState("");
const [frameVersion, setFrameVersion] = useState(0);
const [frameError, setFrameError] = useState(null);
- const [exportPackageLoading, setExportPackageLoading] = useState(false);
- const [exportJobsLoading, setExportJobsLoading] = useState(false);
- const [exportJobs, setExportJobs] = useState([]);
const isAdmin = getStoredAuth()?.system_role === "admin";
const runtime = useLlmWikiRuntime();
- const details = useLlmWikiKnowledgeBase(knowledgeBaseId || null, !!knowledgeBaseId);
-
- const refreshExportJobs = useCallback(
- async (silent = false) => {
- if (!knowledgeBaseId || !isAdmin) return;
- if (!silent) setExportJobsLoading(true);
- try {
- setExportJobs(await listLlmWikiExportPackages(knowledgeBaseId));
- } catch (error) {
- if (!silent) toast.error(error instanceof Error ? error.message : String(error));
- } finally {
- if (!silent) setExportJobsLoading(false);
- }
- },
- [isAdmin, knowledgeBaseId],
+ const details = useLlmWikiKnowledgeBase(
+ knowledgeBaseId || null,
+ !!knowledgeBaseId,
);
const detailsReady = !!details.data;
@@ -162,25 +151,35 @@ export function WeKnoraKnowledgeDetailPage() {
details.data?.wiki_index_enabled === true ||
details.data?.indexing_strategy?.wiki_enabled === true ||
details.data?.capabilities?.wiki_enabled === true ||
- (details.data?.wiki_config && Object.keys(details.data.wiki_config).length > 0);
+ (details.data?.wiki_config &&
+ Object.keys(details.data.wiki_config).length > 0);
const preferredTab =
- requestedTab === "documents" || requestedTab === "wiki" || requestedTab === "graph"
+ requestedTab === "documents" ||
+ requestedTab === "wiki" ||
+ requestedTab === "graph"
? requestedTab
: detailsWikiEnabled
? "wiki"
: "documents";
- const sourceChunkId = searchParams.get("sourceChunkId") || searchParams.get("chunkId") || "";
+ const sourceChunkId =
+ searchParams.get("sourceChunkId") || searchParams.get("chunkId") || "";
const documentId = searchParams.get("documentId") || "";
const wikiSlug = searchParams.get("wikiSlug") || "";
- const nativeWikiPageUrl = knowledgeBaseId && wikiSlug && preferredTab === "wiki"
- ? buildWeKnoraWikiPagePath(knowledgeBaseId, wikiSlug)
- : "";
+ const nativeWikiPageUrl =
+ knowledgeBaseId && wikiSlug && preferredTab === "wiki"
+ ? buildWeKnoraWikiPagePath(knowledgeBaseId, wikiSlug)
+ : "";
useEffect(() => {
let cancelled = false;
setFrameSrc("");
setFrameError(null);
- if (nativeWikiPageUrl || !knowledgeBaseId || runtime.data?.provider !== "weknora" || !detailsReady) {
+ if (
+ nativeWikiPageUrl ||
+ !knowledgeBaseId ||
+ runtime.data?.provider !== "weknora" ||
+ !detailsReady
+ ) {
return () => {
cancelled = true;
};
@@ -190,30 +189,47 @@ export function WeKnoraKnowledgeDetailPage() {
const resolvedWikiSlug =
wikiSlug ||
(sourceChunkId
- ? (await getLlmWikiSourceContext(knowledgeBaseId, sourceChunkId).catch(
- () => null,
- ))?.wiki_page?.slug
+ ? (
+ await getLlmWikiSourceContext(
+ knowledgeBaseId,
+ sourceChunkId,
+ ).catch(() => null)
+ )?.wiki_page?.slug
: "");
- const session = await createLlmWikiWeKnoraEmbedSession(knowledgeBaseId, preferredTab);
- if (cancelled) return;
- const framePath = session.frame_path || "";
- const nextUrl = buildFrameUrl(framePath);
- nextUrl.searchParams.set("theme", collectThemePayload().theme);
- if (sourceChunkId) nextUrl.searchParams.set("sourceChunkId", sourceChunkId);
- if (documentId) nextUrl.searchParams.set("documentId", documentId);
- if (resolvedWikiSlug) nextUrl.searchParams.set("wikiSlug", resolvedWikiSlug);
- setFrameSrc(nextUrl.toString());
- })()
- .catch((error: unknown) => {
- if (cancelled) return;
- const message = error instanceof Error ? error.message : "创建知识库详情会话失败";
- setFrameError(message);
- });
+ const session = await createLlmWikiWeKnoraEmbedSession(
+ knowledgeBaseId,
+ preferredTab,
+ );
+ if (cancelled) return;
+ const framePath = session.frame_path || "";
+ const nextUrl = buildFrameUrl(framePath);
+ nextUrl.searchParams.set("theme", collectThemePayload().theme);
+ if (sourceChunkId)
+ nextUrl.searchParams.set("sourceChunkId", sourceChunkId);
+ if (documentId) nextUrl.searchParams.set("documentId", documentId);
+ if (resolvedWikiSlug)
+ nextUrl.searchParams.set("wikiSlug", resolvedWikiSlug);
+ setFrameSrc(nextUrl.toString());
+ })().catch((error: unknown) => {
+ if (cancelled) return;
+ const message =
+ error instanceof Error ? error.message : "创建知识库详情会话失败";
+ setFrameError(message);
+ });
return () => {
cancelled = true;
};
- }, [detailsReady, documentId, knowledgeBaseId, nativeWikiPageUrl, preferredTab, runtime.data?.provider, sourceChunkId, wikiSlug]);
+ }, [
+ detailsReady,
+ documentId,
+ knowledgeBaseId,
+ nativeWikiPageUrl,
+ preferredTab,
+ runtime.data?.provider,
+ sourceChunkId,
+ wikiSlug,
+ ]);
useEffect(() => {
if (!frameSrc) return;
@@ -233,22 +249,6 @@ export function WeKnoraKnowledgeDetailPage() {
};
}, [frameSrc]);
- useEffect(() => {
- if (!isAdmin || !knowledgeBaseId || runtime.data?.provider !== "weknora") {
- setExportJobs([]);
- return;
- }
- void refreshExportJobs();
- }, [isAdmin, knowledgeBaseId, refreshExportJobs, runtime.data?.provider]);
-
- useEffect(() => {
- if (!isAdmin || !knowledgeBaseId || !exportJobs.some((job) => RUNNING_EXPORT_STATUSES.has(job.status))) {
- return;
- }
- const timer = window.setInterval(() => void refreshExportJobs(true), 5000);
- return () => window.clearInterval(timer);
- }, [exportJobs, isAdmin, knowledgeBaseId, refreshExportJobs]);
-
if (nativeWikiPageUrl) {
return ;
}
@@ -262,7 +262,8 @@ export function WeKnoraKnowledgeDetailPage() {
);
}
- if (runtime.data?.provider !== "weknora") return ;
+ if (runtime.data?.provider !== "weknora")
+ return ;
if (frameError) {
return (
@@ -271,7 +272,9 @@ export function WeKnoraKnowledgeDetailPage() {