feat: integrate WeKnora llmwiki knowledge flow

This commit is contained in:
吉鑫升 committed 2026-08-11 18:13:30 +08:00
1 parent a0a75a9964
commit e0c09f6704
63 files changed
+7045 -197

No files matched your search

+139 -4
View File
@@ -16,6 +16,7 @@ import { ArtifactTrigger } from "@/components/workspace/artifacts";
import { ChatBox, useThreadChat } from "@/components/workspace/chats";
import { ExportTrigger } from "@/components/workspace/export-trigger";
import { InputBox } from "@/components/workspace/input-box";
import { LlmWikiKnowledgeSelector } from "@/components/workspace/llmwiki-knowledge-selector";
import { MessageList } from "@/components/workspace/messages";
import { ThreadContext } from "@/components/workspace/messages/context";
import { QuestionNavigator } from "@/components/workspace/question-navigator";
@@ -24,7 +25,9 @@ import { TodoList } from "@/components/workspace/todo-list";
import { TokenUsageIndicator } from "@/components/workspace/token-usage-indicator";
import { Tooltip } from "@/components/workspace/tooltip";
import { normalizeAgentQuestions, useAgent } from "@/core/agents";
import { getAPIClient } from "@/core/api";
import { useI18n } from "@/core/i18n/hooks";
import { queueLlmWikiConversationDeposit } from "@/core/llmwiki/conversation-deposit";
import { useModels } from "@/core/models/hooks";
import { useNotification } from "@/core/notification/hooks";
import { saveLastAgentId, useThreadSettings } from "@/core/settings";
@@ -69,6 +72,26 @@ const TEMPLATE_BUILDER_AGENT_ID = "template-json-builder";
const RWFX_SQ_REPORT_CONTEXT_MAX_CHARS = 12000;
const RWFX_SQ_REPORT_CONTEXT_TAIL_CHARS = 2000;
function readPendingKnowledgeSelection(agentId: string | undefined): string[] | null {
if (typeof window === "undefined" || !agentId) return null;
try {
const raw = sessionStorage.getItem("pendingAgentMessage");
if (!raw) return null;
const parsed = JSON.parse(raw) as {
agentId?: string;
knowledgeBaseIds?: unknown;
};
if (parsed.agentId !== agentId || !Array.isArray(parsed.knowledgeBaseIds)) {
return null;
}
return parsed.knowledgeBaseIds
.map((item) => String(item).trim())
.filter(Boolean);
} catch {
return null;
}
}
function stringifyForHiddenContext(value: unknown): string {
if (typeof value === "string") return value;
try {
@@ -244,6 +267,79 @@ export default function AgentChatPage({
useThreadChat();
const [settings, setSettings] = useThreadSettings(threadId);
const { tokenUsageEnabled } = useModels();
const pendingKnowledgeSelection = useRef(
readPendingKnowledgeSelection(agent_id),
);
const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState<string[]>(
() => pendingKnowledgeSelection.current ?? [],
);
const [knowledgeSelectionReady, setKnowledgeSelectionReady] = useState(
() => pendingKnowledgeSelection.current !== null,
);
const [knowledgeSelectionLoading, setKnowledgeSelectionLoading] = useState(false);
const knowledgeSelectionThread = useRef<string | null>(null);
const knowledgeSelectionAgent = useRef(agent_id);
const activeThreadIdRef = useRef<string | null | undefined>(threadId);
const changeKnowledgeSelection = useCallback((ids: string[]) => {
setSelectedKnowledgeBaseIds(ids);
setKnowledgeSelectionReady(true);
}, []);
useEffect(() => {
activeThreadIdRef.current = threadId;
}, [threadId]);
useEffect(() => {
if (knowledgeSelectionAgent.current === agent_id) return;
knowledgeSelectionAgent.current = agent_id;
const pending = readPendingKnowledgeSelection(agent_id);
pendingKnowledgeSelection.current = pending;
knowledgeSelectionThread.current = null;
setSelectedKnowledgeBaseIds(pending ?? []);
setKnowledgeSelectionReady(pending !== null);
}, [agent_id]);
useEffect(() => {
if (isNewThread) {
if (knowledgeSelectionThread.current !== "__new__") {
setSelectedKnowledgeBaseIds(pendingKnowledgeSelection.current ?? []);
setKnowledgeSelectionReady(pendingKnowledgeSelection.current !== null);
}
knowledgeSelectionThread.current = "__new__";
setKnowledgeSelectionLoading(false);
return;
}
if (!threadId || knowledgeSelectionThread.current === threadId) return;
knowledgeSelectionThread.current = threadId;
setKnowledgeSelectionLoading(true);
void getAPIClient()
.threads.get(threadId)
.then((record) => {
if (knowledgeSelectionThread.current !== threadId) return;
const metadata = record.metadata as Record<string, unknown> | undefined;
const raw = metadata?.llmwiki_knowledge_base_ids;
if (Array.isArray(raw)) {
setSelectedKnowledgeBaseIds(
raw.map((item) => String(item).trim()).filter(Boolean),
);
setKnowledgeSelectionReady(true);
} else {
setSelectedKnowledgeBaseIds([]);
setKnowledgeSelectionReady(false);
}
})
.catch(() => {
if (knowledgeSelectionThread.current === threadId) {
setKnowledgeSelectionReady(false);
}
})
.finally(() => {
if (knowledgeSelectionThread.current === threadId) {
setKnowledgeSelectionLoading(false);
}
});
}, [isNewThread, threadId]);
useEffect(() => {
if (isNewThread) {
@@ -253,14 +349,34 @@ export default function AgentChatPage({
}, [isNewThread, threadId, agent_id]);
const { showNotification } = useNotification();
const conversationMetadata = useMemo(
() => ({
...(taskId ? taskThreadMetadata(taskContext) : {}),
...(knowledgeSelectionReady
? { llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds }
: {}),
}),
[knowledgeSelectionReady, selectedKnowledgeBaseIds, taskContext, taskId],
);
const [thread, sendMessage] = useThreadStream({
threadId: isNewThread ? undefined : threadId,
assistantId: agent_id,
context: { ...settings.context, agent_id },
context: {
...settings.context,
agent_id,
...(knowledgeSelectionReady
? { llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds }
: {}),
},
// 任务闸门:新建 thread 打 metadata.taskId 标签,供任务对话列表(1:N)按任务过滤;
// xdfx 额外打 metadata.actionId 标签,按「行动id」划分聊天记录(见 taskThreadMetadata)。
threadMetadata: taskId ? taskThreadMetadata(taskContext) : undefined,
threadMetadata:
Object.keys(conversationMetadata).length > 0
? conversationMetadata
: undefined,
onStart: (createdThreadId) => {
knowledgeSelectionThread.current = createdThreadId;
activeThreadIdRef.current = createdThreadId;
setThreadId(createdThreadId);
setIsNewThread(false);
// Keep the current component mounted while updating the hash route.
@@ -286,6 +402,12 @@ export default function AgentChatPage({
} catch {
/* 上报绝不影响问答 */
}
queueLlmWikiConversationDeposit({
threadId: activeThreadIdRef.current,
messages: state.messages,
assistantId: agent_id,
knowledgeBaseIds: selectedKnowledgeBaseIds,
});
if (document.hidden || !document.hasFocus()) {
let body = "Conversation finished";
const lastMessage = state.messages[state.messages.length - 1];
@@ -581,10 +703,23 @@ export default function AgentChatPage({
autoFocus
status={status}
context={settings.context}
disabled={isDemoMode}
disabled={isDemoMode || knowledgeSelectionLoading}
onContextChange={(context) =>
setSettings("context", context)
}
knowledgeSelector={
<LlmWikiKnowledgeSelector
source="agent"
agentId={agent_id}
value={selectedKnowledgeBaseIds}
onChange={changeKnowledgeSelection}
autoSelectAll={
isNewThread && pendingKnowledgeSelection.current === null
}
disabled={isDemoMode || knowledgeSelectionLoading}
label="知识空间"
/>
}
onFollowupsVisibilityChange={setShowFollowups}
onSubmit={handleSubmit}
onStop={handleStop}
@@ -636,7 +771,7 @@ export default function AgentChatPage({
assistantId={agent_id}
status={status}
context={settings.context}
disabled={isDemoMode}
disabled={isDemoMode || knowledgeSelectionLoading}
onContextChange={(context) =>
setSettings("context", context)
}
+110 -3
View File
@@ -16,6 +16,7 @@ import {
} from "@/components/workspace/chats";
import { ExportTrigger } from "@/components/workspace/export-trigger";
import { KnowledgeSaveTrigger } from "@/components/workspace/knowledge-save-trigger";
import { LlmWikiKnowledgeSelector } from "@/components/workspace/llmwiki-knowledge-selector";
import { InputBox } from "@/components/workspace/input-box";
import {
MessageList,
@@ -52,6 +53,7 @@ import {
useThreadSettings,
} from "@/core/settings";
import { useThreadStream } from "@/core/threads/hooks";
import { queueLlmWikiConversationDeposit } from "@/core/llmwiki/conversation-deposit";
import { useLayoutMode } from "@/core/system-settings/hooks";
import { useRouteBase } from "@/core/route-base";
import { textOfMessage } from "@/core/threads/utils";
@@ -121,12 +123,29 @@ export default function ChatPage() {
// In-memory only — explicitly overrides any value persisted by the old
// per-thread header toggle so stale localStorage can't re-enable it.
const [knowledgeRagEnabled] = useKnowledgeRagEnabled();
const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState<string[]>([]);
const [knowledgeSelectionReady, setKnowledgeSelectionReady] = useState(false);
const [knowledgeSelectionLoading, setKnowledgeSelectionLoading] = useState(false);
const knowledgeSelectionThread = useRef<string | null>(null);
const activeThreadIdRef = useRef<string | null | undefined>(threadId);
const changeKnowledgeSelection = useCallback((ids: string[]) => {
setSelectedKnowledgeBaseIds(ids);
setKnowledgeSelectionReady(true);
}, []);
const streamContext = useMemo(
() => ({
...settings.context,
knowledge_rag_enabled: knowledgeRagEnabled,
...(knowledgeSelectionReady
? { llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds }
: {}),
}),
[settings.context, knowledgeRagEnabled],
[
settings.context,
knowledgeRagEnabled,
knowledgeSelectionReady,
selectedKnowledgeBaseIds,
],
);
const [mounted, setMounted] = useState(false);
const { tokenUsageEnabled } = useModels();
@@ -150,6 +169,10 @@ export default function ChatPage() {
const { selectedArtifact } = useArtifacts();
useSpecificChatMode();
useEffect(() => {
activeThreadIdRef.current = threadId;
}, [threadId]);
useEffect(() => {
setMounted(true);
}, []);
@@ -158,12 +181,54 @@ export default function ChatPage() {
if (isNewThread) {
setHasStartedConversation(false);
setPromptPrefixOn(false);
setSelectedKnowledgeBaseIds([]);
setKnowledgeSelectionReady(false);
setKnowledgeSelectionLoading(false);
knowledgeSelectionThread.current = "__new__";
// Pre-select the agent the user last started a conversation with so a
// fresh chat defaults to it. Validated against the known agent list below.
setSelectedAgentId(getLastAgentId());
}
}, [isNewThread, threadId]);
// The knowledge-base choice belongs to the conversation, not to a browser
// component instance. Restore it from LangGraph thread metadata whenever an
// existing conversation is opened. An explicit empty array means the user
// deliberately disabled LLMWiki retrieval for that conversation.
useEffect(() => {
if (isNewThread || !threadId || knowledgeSelectionThread.current === threadId) {
return;
}
knowledgeSelectionThread.current = threadId;
setKnowledgeSelectionLoading(true);
void getAPIClient(isMock)
.threads.get(threadId)
.then((record) => {
if (knowledgeSelectionThread.current !== threadId) return;
const metadata = record.metadata as Record<string, unknown> | undefined;
const raw = metadata?.llmwiki_knowledge_base_ids;
if (Array.isArray(raw)) {
setSelectedKnowledgeBaseIds(
raw.map((item) => String(item).trim()).filter(Boolean),
);
setKnowledgeSelectionReady(true);
} else {
setSelectedKnowledgeBaseIds([]);
setKnowledgeSelectionReady(false);
}
})
.catch(() => {
if (knowledgeSelectionThread.current === threadId) {
setKnowledgeSelectionReady(false);
}
})
.finally(() => {
if (knowledgeSelectionThread.current === threadId) {
setKnowledgeSelectionLoading(false);
}
});
}, [isMock, isNewThread, threadId]);
// Drop a restored last-used agent that is no longer in the selector list
// (deleted / unpublished / no longer curated), so we never hand off to a
// dead agent. Wait for the list to load before deciding.
@@ -175,6 +240,8 @@ export default function ChatPage() {
if (known.length === 0) return;
if (!known.some((a) => a.id === selectedAgentId)) {
setSelectedAgentId(null);
setSelectedKnowledgeBaseIds([]);
setKnowledgeSelectionReady(false);
}
}, [selectedAgentId, selectorAgents, mineAgents, squareAgents, selectorLoading]);
@@ -183,8 +250,13 @@ export default function ChatPage() {
const [thread, sendMessage, isUploading] = useThreadStream({
threadId: isNewThread ? undefined : threadId,
context: streamContext,
threadMetadata: knowledgeSelectionReady
? { llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds }
: undefined,
isMock,
onStart: (createdThreadId) => {
knowledgeSelectionThread.current = createdThreadId;
activeThreadIdRef.current = createdThreadId;
setThreadId(createdThreadId);
setIsNewThread(false);
// Keep the current component mounted while updating the hash route.
@@ -210,6 +282,12 @@ export default function ChatPage() {
} catch {
/* 上报绝不影响问答 */
}
queueLlmWikiConversationDeposit({
threadId: activeThreadIdRef.current,
messages: state.messages,
assistantId: selectedAgentId,
knowledgeBaseIds: selectedKnowledgeBaseIds,
});
if (document.hidden || !document.hasFocus()) {
let body = "Conversation finished";
const lastMessage = state.messages.at(-1);
@@ -262,7 +340,12 @@ export default function ChatPage() {
void (async () => {
try {
const created = await getAPIClient(isMock).threads.create({
metadata: { agent_id: selectedAgentId },
metadata: {
agent_id: selectedAgentId,
...(knowledgeSelectionReady
? { llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds }
: {}),
},
});
sessionStorage.setItem(
"pendingAgentMessage",
@@ -270,6 +353,9 @@ export default function ChatPage() {
agentId: selectedAgentId,
threadId: created.thread_id,
text,
...(knowledgeSelectionReady
? { knowledgeBaseIds: selectedKnowledgeBaseIds }
: {}),
}),
);
saveLastAgentId(selectedAgentId);
@@ -328,6 +414,8 @@ export default function ChatPage() {
hideAdminPrompt,
preferencesLoading,
selectedAgentId,
selectedKnowledgeBaseIds,
knowledgeSelectionReady,
agentHandoffPending,
isMock,
router,
@@ -418,6 +506,8 @@ export default function ChatPage() {
// Passing null (the ✕ clear) wipes the stored preference.
const handleAgentChange = useCallback((agentId: string | null) => {
setSelectedAgentId(agentId);
setSelectedKnowledgeBaseIds([]);
setKnowledgeSelectionReady(false);
saveLastAgentId(agentId);
}, []);
const handleStop = useCallback(async () => {
@@ -620,7 +710,8 @@ export default function ChatPage() {
disabled={
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" ||
isUploading ||
agentHandoffPending
agentHandoffPending ||
knowledgeSelectionLoading
}
onContextChange={(context) =>
setSettings("context", context)
@@ -634,6 +725,22 @@ export default function ChatPage() {
}
: undefined
}
knowledgeSelector={
isLandingState ? (
<LlmWikiKnowledgeSelector
source={selectedAgentId ? "agent" : "all"}
agentId={selectedAgentId}
value={selectedKnowledgeBaseIds}
onChange={changeKnowledgeSelection}
autoSelectAll={!!selectedAgentId}
disabled={
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" ||
knowledgeSelectionLoading
}
label="知识空间"
/>
) : undefined
}
onFollowupsVisibilityChange={setShowFollowups}
onSubmit={handleSubmit}
onStop={handleStop}
+7 -2
View File
@@ -1,5 +1,5 @@
import { Loader2 } from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { toast } from "sonner";
@@ -530,7 +530,11 @@ function VaultPageDialog({
);
}
export default function KnowledgeBasePage() {
export default function KnowledgeBasePage({
headerExtraActions,
}: {
headerExtraActions?: ReactNode;
} = {}) {
const navigate = useNavigate();
const { noteId } = useParams<{ noteId: string }>();
@@ -653,6 +657,7 @@ export default function KnowledgeBasePage() {
onCreate={() => setCreateOpen(true)}
onManageFolders={() => setFoldersOpen(true)}
onManageTemplates={() => setTemplatesOpen(true)}
extraActions={headerExtraActions}
/>
<div
@@ -0,0 +1,91 @@
import { Loader2, Settings2 } from "lucide-react";
import { useState } from "react";
import { Navigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { getStoredAuth } from "@/core/auth";
import { useLlmWikiRuntime } from "@/strategy-components/api/llmwiki";
import WeKnoraKnowledgeWorkspace, {
ConnectionSettings,
} from "@/strategy-components/components/resource-management/llmwiki/WeKnoraKnowledgeWorkspace";
import WeKnoraKnowledgeDetailPage from "@/strategy-components/components/resource-management/llmwiki/WeKnoraKnowledgeDetailPage";
import KnowledgeBasePage from "./KnowledgeBasePage";
import KnowledgeGraphPage from "./KnowledgeGraphPage";
type LlmWikiKnowledgePageMode = "list" | "detail" | "graph";
function RuntimeLoading() {
return (
<div className="flex h-full min-h-64 items-center justify-center text-sm text-muted-foreground">
<Loader2 className="mr-2 size-5 animate-spin" />
正在读取 LLMWiki 运行模式
</div>
);
}
function AdminWeKnoraSettingsButton() {
const [open, setOpen] = useState(false);
return (
<>
<Button size="sm" variant="outline" onClick={() => setOpen(true)}>
<Settings2 className="mr-2 size-4" />
配置 WeKnora
</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle>LLMWiki 运行模式</DialogTitle>
</DialogHeader>
<ConnectionSettings />
</DialogContent>
</Dialog>
</>
);
}
function LegacyKnowledgePage() {
const isAdmin = getStoredAuth()?.system_role === "admin";
return (
<KnowledgeBasePage
headerExtraActions={isAdmin ? <AdminWeKnoraSettingsButton /> : undefined}
/>
);
}
export default function LlmWikiKnowledgePage({
mode = "list",
}: {
mode?: LlmWikiKnowledgePageMode;
}) {
const runtime = useLlmWikiRuntime();
if (runtime.isLoading) {
return <RuntimeLoading />;
}
if (runtime.isError || runtime.data?.provider !== "weknora") {
if (mode === "graph") {
return <KnowledgeGraphPage />;
}
return <LegacyKnowledgePage />;
}
if (mode === "graph") {
return <Navigate to="/page/workspace/knowledge" replace />;
}
if (mode === "detail") {
return <WeKnoraKnowledgeDetailPage />;
}
return <WeKnoraKnowledgeWorkspace />;
}
+12
View File
@@ -31,6 +31,7 @@ import {
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { ArtifactsProvider } from "@/components/workspace/artifacts";
import { LlmWikiKnowledgeSelector } from "@/components/workspace/llmwiki-knowledge-selector";
import {
MessageList,
type SetupAgentApprovalPayload,
@@ -95,6 +96,7 @@ export default function NewAgentPage() {
const [isCreatingAgent, setIsCreatingAgent] = useState(false);
const [isSavingDraft, setIsSavingDraft] = useState(false);
const [selectedSkills, setSelectedSkills] = useState<string[]>([]);
const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState<string[]>([]);
const [isSkillSelectionTouched, setIsSkillSelectionTouched] = useState(false);
const [detailSkillName, setDetailSkillName] = useState<string | null>(null);
// 恢复已有 bootstrap 会话时该 thread 已存在,无需再 create(置为 true,跳过创建)。
@@ -230,6 +232,7 @@ export default function NewAgentPage() {
model: modelOverride,
template_name: templateNameInput.trim(),
seat_skill_directive: seatSkillDirectiveInput,
llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds,
});
setAgent(updated);
return updated;
@@ -252,6 +255,7 @@ export default function NewAgentPage() {
model: modelOverride,
template_name: templateNameInput.trim(),
seat_skill_directive: seatSkillDirectiveInput,
llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds,
});
setAgentId(created.id);
setAgent(created);
@@ -280,6 +284,7 @@ export default function NewAgentPage() {
descriptionInput,
modelInput,
selectedSkills,
selectedKnowledgeBaseIds,
soulInput,
templateNameInput,
seatSkillDirectiveInput,
@@ -590,6 +595,13 @@ export default function NewAgentPage() {
</p>
</div>
<LlmWikiKnowledgeSelector
source="all"
value={selectedKnowledgeBaseIds}
onChange={setSelectedKnowledgeBaseIds}
variant="panel"
/>
<div className="space-y-2">
<label className="text-sm font-medium">SOUL.md</label>
<Textarea
+4 -5
View File
@@ -35,8 +35,7 @@ import AdminScheduledTasksPage from './AdminScheduledTasksPage';
import AdminSkillsPage from './AdminSkillsPage';
import ScheduledTaskRunDetailPage from './ScheduledTaskRunDetailPage';
import ScheduledTasksPage from './ScheduledTasksPage';
import KnowledgeBasePage from './KnowledgeBasePage';
import KnowledgeGraphPage from './KnowledgeGraphPage';
import LlmWikiKnowledgePage from './LlmWikiKnowledgePage';
import LlmMetricsPage from './LlmMetricsPage';
import CheckpointMigrationPage from './CheckpointMigrationPage';
import MemoryPage from './MemoryPage';
@@ -604,7 +603,7 @@ export default function WorkspaceRoutes({
path="knowledge"
element={
<WorkspaceLayout {...layoutProps}>
<KnowledgeBasePage />
<LlmWikiKnowledgePage />
</WorkspaceLayout>
}
/>
@@ -612,7 +611,7 @@ export default function WorkspaceRoutes({
path="knowledge/graph"
element={
<WorkspaceLayout {...layoutProps}>
<KnowledgeGraphPage />
<LlmWikiKnowledgePage mode="graph" />
</WorkspaceLayout>
}
/>
@@ -620,7 +619,7 @@ export default function WorkspaceRoutes({
path="knowledge/:noteId"
element={
<WorkspaceLayout {...layoutProps}>
<KnowledgeBasePage />
<LlmWikiKnowledgePage mode="detail" />
</WorkspaceLayout>
}
/>