feat(knowledge): support standalone file ingestion and configurable wiki entry
Use neutral knowledge-service labels and sanitize user-facing task errors.
This commit is contained in:
1 parent
f9b6d7e322
commit
3ca5528640
24 files changed
+549
-54
No files matched your search
@@ -6,13 +6,22 @@ import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { useLlmWikiRuntime } from "@/strategy-components/api/llmwiki";
|
||||
import {
|
||||
loadKnowledgeBaseProductVersion,
|
||||
loadOrdinaryKnowledgeLanding,
|
||||
saveOrdinaryKnowledgeLanding,
|
||||
type OrdinaryKnowledgeLanding,
|
||||
type KnowledgeBaseProductVersion,
|
||||
updateKnowledgeBaseProductVersion,
|
||||
} from "@/core/system-settings/api";
|
||||
|
||||
export const KNOWLEDGE_PRODUCT_KEY = ["system-settings", "knowledge-base-product-version"] as const;
|
||||
const LANDING_KEY = ["system-settings", "ordinary-knowledge-landing"] as const;
|
||||
|
||||
export function useOrdinaryKnowledgeLanding() {
|
||||
return useQuery({ queryKey: LANDING_KEY, queryFn: loadOrdinaryKnowledgeLanding, staleTime: 0 });
|
||||
}
|
||||
export const KNOWLEDGE_NORMAL_VIEW_VALUE = "normal";
|
||||
export const KNOWLEDGE_NORMAL_VIEW_SEARCH = `?view=${KNOWLEDGE_NORMAL_VIEW_VALUE}`;
|
||||
|
||||
@@ -25,6 +34,14 @@ export function KnowledgeProductNav() {
|
||||
const navigate = useNavigate();
|
||||
const client = useQueryClient();
|
||||
const version = useKnowledgeBaseProductVersion();
|
||||
const landing = useOrdinaryKnowledgeLanding();
|
||||
const saveLanding = useMutation({
|
||||
mutationFn: saveOrdinaryKnowledgeLanding,
|
||||
onSuccess: (mode) => { client.setQueryData(LANDING_KEY, mode); toast.success("普通知识库默认页面已更新"); },
|
||||
onError: (error) => toast.error(error.message),
|
||||
});
|
||||
const runtime = useLlmWikiRuntime();
|
||||
const normalAvailable = runtime.data?.provider === "weknora" && Boolean(runtime.data.api_base_url?.trim());
|
||||
const isAdmin = getStoredAuth()?.system_role === "admin";
|
||||
const save = useMutation({
|
||||
mutationFn: updateKnowledgeBaseProductVersion,
|
||||
@@ -36,15 +53,20 @@ export function KnowledgeProductNav() {
|
||||
const assistant = location.pathname.includes("/knowledge/assistant");
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 border-b bg-card px-6 py-2">
|
||||
<Button size="sm" variant={assistant ? "ghost" : "secondary"} onClick={() => navigate(`/page/workspace/knowledge${KNOWLEDGE_NORMAL_VIEW_SEARCH}`)}>
|
||||
<div className="flex flex-wrap items-center gap-2 border-b bg-card px-6 py-2">
|
||||
<Button size="sm" disabled={!normalAvailable} title={normalAvailable ? "普通知识库" : "管理员配置知识服务地址后可用"} variant={assistant ? "ghost" : "secondary"} onClick={() => navigate(`/page/workspace/knowledge${KNOWLEDGE_NORMAL_VIEW_SEARCH}`)}>
|
||||
<BookOpen />普通知识库
|
||||
</Button>
|
||||
<Button size="sm" variant={assistant ? "secondary" : "ghost"} onClick={() => navigate("/page/workspace/knowledge/assistant")}>
|
||||
<Database />助手知识库
|
||||
</Button>
|
||||
{isAdmin && (
|
||||
<div className="ml-auto flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<div className="ml-auto flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
普通知识库默认页面
|
||||
<Select disabled={landing.isLoading || saveLanding.isPending} value={landing.data ?? "wiki"} onValueChange={(value) => saveLanding.mutate(value as OrdinaryKnowledgeLanding)}>
|
||||
<SelectTrigger className="h-8 w-40" aria-label="普通知识库默认页面"><SelectValue /></SelectTrigger>
|
||||
<SelectContent><SelectItem value="wiki">本系统 Wiki 索引</SelectItem><SelectItem value="weknora">知识库管理页面</SelectItem></SelectContent>
|
||||
</Select>
|
||||
默认产品版本
|
||||
<Select value={version.data ?? "v1"} onValueChange={(value) => save.mutate(value as KnowledgeBaseProductVersion)}>
|
||||
<SelectTrigger className="h-8 w-40"><SelectValue /></SelectTrigger>
|
||||
|
||||
@@ -205,21 +205,21 @@ export function SkillKnowledgePanel({ adminMode = false }: { adminMode?: boolean
|
||||
/>
|
||||
<Database className="size-4 text-muted-foreground" />
|
||||
<span className="text-sm">{target.target_name}</span>
|
||||
<Badge variant="outline" className="ml-auto text-[10px]">WeKnora Wiki</Badge>
|
||||
<Badge variant="outline" className="ml-auto text-[10px]">普通 Wiki 库</Badge>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
{(runtime.isLoading || knowledgeBases.isLoading) && <Loader2 className="m-4 size-5 animate-spin" />}
|
||||
{!runtime.isLoading && !knowledgeBases.isLoading && knowledgeBases.error && (
|
||||
<p className="p-3 text-xs text-destructive">
|
||||
读取 WeKnora 普通知识库失败:
|
||||
读取普通知识库失败:
|
||||
{knowledgeBases.error instanceof Error ? knowledgeBases.error.message : String(knowledgeBases.error)}
|
||||
</p>
|
||||
)}
|
||||
{!runtime.isLoading && !knowledgeBases.isLoading && !knowledgeBases.error && !targets.length && (
|
||||
<p className="p-3 text-xs text-muted-foreground">
|
||||
{runtime.data?.provider !== "weknora"
|
||||
? "当前未配置 WeKnora 普通知识库地址;技能需要先归纳到普通知识库,再导入助手知识库。"
|
||||
? "当前未配置普通知识库地址;技能需要先归纳到普通知识库,再导入助手知识库。"
|
||||
: "当前没有可写且已开启 Wiki 的普通知识库;请确认该库不是 FAQ/会话沉淀库,并已开启 Wiki。"}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -11,6 +11,22 @@ export interface PromptPrefixSettings {
|
||||
|
||||
export type KnowledgeBaseProductVersion = "v1" | "v2";
|
||||
|
||||
export type OrdinaryKnowledgeLanding = "wiki" | "weknora";
|
||||
|
||||
export async function loadOrdinaryKnowledgeLanding(): Promise<OrdinaryKnowledgeLanding> {
|
||||
const response = await apiFetch(`${getBackendBaseURL()}/api/system-settings/ordinary-knowledge-landing`);
|
||||
if (!response.ok) throw new Error("无法读取普通知识库默认页面设置");
|
||||
return ((await response.json()) as { mode: OrdinaryKnowledgeLanding }).mode;
|
||||
}
|
||||
|
||||
export async function saveOrdinaryKnowledgeLanding(mode: OrdinaryKnowledgeLanding): Promise<OrdinaryKnowledgeLanding> {
|
||||
const response = await apiFetch(`${getBackendBaseURL()}/api/system-settings/ordinary-knowledge-landing`, {
|
||||
method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mode }),
|
||||
});
|
||||
if (!response.ok) throw new Error("保存失败,仅管理员可配置普通知识库默认页面");
|
||||
return ((await response.json()) as { mode: OrdinaryKnowledgeLanding }).mode;
|
||||
}
|
||||
|
||||
export async function loadKnowledgeBaseProductVersion(): Promise<KnowledgeBaseProductVersion> {
|
||||
const response = await apiFetch(
|
||||
`${getBackendBaseURL()}/api/system-settings/knowledge-base-product-version`,
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
/** Neutral product terminology for system messages; never apply to knowledge content or API identifiers. */
|
||||
export function userFacingKnowledgeMessage(value: string): string {
|
||||
return value.replace(/weknora/gi, "知识服务");
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { userFacingKnowledgeMessage } from "@/lib/knowledge-message";
|
||||
import {
|
||||
ArrowLeft,
|
||||
BookOpenText,
|
||||
@@ -67,6 +68,7 @@ export default function AssistantKnowledgePage() {
|
||||
ReturnType<typeof searchAssistantWiki>
|
||||
> | null>(null);
|
||||
const upload = useRef<HTMLInputElement>(null);
|
||||
const documentUpload = useRef<HTMLInputElement>(null);
|
||||
const base = bases.data?.find((b) => b.id === baseId);
|
||||
const libraryPages = useMemo<LibraryWikiPage[]>(
|
||||
() =>
|
||||
@@ -159,8 +161,7 @@ export default function AssistantKnowledgePage() {
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">助手知识库</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
浏览分类
|
||||
Wiki、关联知识与来源。普通知识库向量化完成后自动沉淀到知识梳理。
|
||||
选择或新建知识库,上传文档后自动生成分类 Wiki、关联知识和向量索引。可独立使用,也可导入 普通知识库数据包;各库知识汇入知识梳理总库。
|
||||
</p>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
@@ -306,6 +307,12 @@ export default function AssistantKnowledgePage() {
|
||||
删除
|
||||
</Button>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<Button size="sm" disabled={actions.uploadDocument.isPending} onClick={() => documentUpload.current?.click()}>
|
||||
{actions.uploadDocument.isPending ? <Loader2 className="mr-2 size-4 animate-spin" /> : <Upload className="mr-2 size-4" />}
|
||||
上传文件生成 Wiki
|
||||
</Button>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -314,10 +321,23 @@ export default function AssistantKnowledgePage() {
|
||||
onClick={() => upload.current?.click()}
|
||||
>
|
||||
<Upload className="mr-2 size-4" />
|
||||
导入数据包
|
||||
导入 普通知识库数据包
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<input ref={documentUpload} type="file" multiple accept=".md,.markdown,.txt,.csv,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.odt,.ods,.odp" className="hidden"
|
||||
onChange={async (event) => {
|
||||
const files = Array.from(event.target.files || []);
|
||||
event.target.value = "";
|
||||
setTab("transfer");
|
||||
for (const file of files) {
|
||||
if (file.size > 100 * 1024 * 1024) { toast.error(`${file.name} 超过 100 MB,请拆分后上传`); continue; }
|
||||
try {
|
||||
await actions.uploadDocument.mutateAsync({ file, assistantBaseId: baseId });
|
||||
toast.success(`${file.name} 已上传,后台正在解析、生成 Wiki 并向量化`);
|
||||
} catch (error) { toast.error(`${file.name}:${(error as Error).message}`); }
|
||||
}
|
||||
}} />
|
||||
<input
|
||||
ref={upload}
|
||||
type="file"
|
||||
@@ -441,7 +461,8 @@ export default function AssistantKnowledgePage() {
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
上次同步:{new Date(s.updated_at).toLocaleString()}
|
||||
</p>
|
||||
{isAdmin && (
|
||||
{s.source_type === "file" && <p className="mt-2 text-xs text-muted-foreground">本地上传文档 · 处理进度及重试请查看导入记录</p>}
|
||||
{isAdmin && s.source_type === "weknora" && (
|
||||
<Button
|
||||
className="mt-4"
|
||||
size="sm"
|
||||
@@ -469,8 +490,10 @@ export default function AssistantKnowledgePage() {
|
||||
>
|
||||
<WikiPackageTransfers id={baseId} assistant />
|
||||
<h2 className="text-base font-semibold">导入与同步记录</h2>
|
||||
<p className="text-sm text-muted-foreground">可直接上传文档,独立生成 Wiki 并进行向量检索,无需配置普通知识库服务。支持 PDF、Word、Excel、PowerPoint、Markdown、TXT、CSV,单文件最大 100 MB。生成失败时可配置语言模型或编码器后重试。</p>
|
||||
{jobs.data?.map((j) => (
|
||||
<div key={j.id} className="rounded-xl border p-4">
|
||||
<p className="mb-2 font-medium">{sources.data?.find((source) => source.id === j.source_id)?.source_name || (j.trigger === "local_file_upload" ? "上传文档" : "知识数据同步")}</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="outline">
|
||||
{{
|
||||
@@ -484,6 +507,9 @@ export default function AssistantKnowledgePage() {
|
||||
{new Date(j.created_at).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-muted-foreground">{({ uploading: "接收文件", parsing: "解析文档", generating_wiki: "生成 Wiki", vectorizing: "正文向量化", importing: "保存知识并同步总库", queued: "等待处理", interrupted: "处理已中断" } as Record<string, string>)[j.phase] || j.phase}</p>
|
||||
{j.counts?.parts_total > 0 && <p className="text-xs text-muted-foreground">文档分段:{j.counts.parts_done || 0} / {j.counts.parts_total}</p>}
|
||||
{j.counts?.vectors_total > 0 && <p className="text-xs text-muted-foreground">向量化:{j.counts.vectors || 0} / {j.counts.vectors_total}</p>}
|
||||
<p className="mt-3 text-sm">
|
||||
{j.counts?.wiki_pages || 0} Wiki ·{" "}
|
||||
{j.counts?.vectors || 0} 向量 · {j.counts?.entities || 0}{" "}
|
||||
@@ -491,9 +517,15 @@ export default function AssistantKnowledgePage() {
|
||||
</p>
|
||||
{j.last_error_message && (
|
||||
<p className="mt-2 text-sm text-destructive">
|
||||
{j.last_error_message}
|
||||
{userFacingKnowledgeMessage(j.last_error_message)}
|
||||
</p>
|
||||
)}
|
||||
{j.trigger === "local_file_upload" && j.status !== "completed" && (
|
||||
<Button className="mt-2" size="sm" variant="outline" disabled={actions.retryDocument.isPending} onClick={async () => {
|
||||
try { await actions.retryDocument.mutateAsync(j.id); toast.success("已继续处理,复用已有解析和向量结果"); }
|
||||
catch (error) { toast.error((error as Error).message); }
|
||||
}}>重试 / 恢复处理</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</TabsContent>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { userFacingKnowledgeMessage } from "@/lib/knowledge-message";
|
||||
import { type FormEvent, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
AlertCircle,
|
||||
@@ -211,7 +212,7 @@ export default function EnterpriseResearchPage() {
|
||||
setTasks((current) => current.map((task) => task.id === taskId ? { ...task, active_report_job_id: null } : task));
|
||||
}
|
||||
if (event.type === "completed") toast.success("企业研究报告已完成");
|
||||
if (event.type === "failed") toast.error(event.job.error_message || "报告生成失败");
|
||||
if (event.type === "failed") toast.error(userFacingKnowledgeMessage(event.job.error_message || "报告生成失败"));
|
||||
}
|
||||
},
|
||||
}).catch((error: unknown) => {
|
||||
@@ -290,7 +291,7 @@ export default function EnterpriseResearchPage() {
|
||||
return;
|
||||
}
|
||||
if (!isWeKnoraReady) {
|
||||
toast.error("知识服务尚未启用,请先在系统中配置 WeKnora");
|
||||
toast.error("知识服务尚未启用,请先在系统中配置知识服务");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -442,7 +443,7 @@ export default function EnterpriseResearchPage() {
|
||||
<section>
|
||||
<Badge variant="secondary" className="mb-3"><Sparkles className="size-3" />阶段一:证据编排</Badge>
|
||||
<h2 className="text-xl font-semibold tracking-tight">建立一个可追溯的研究任务</h2>
|
||||
<p className="mt-2 max-w-xl text-sm leading-6 text-muted-foreground">页面独立于现有“深度研究”功能。任务、检索词与证据快照保存在 DeerFlow 后端;当前版本只使用已授权的 WeKnora 知识库,不会修改任何知识库或既有报告。</p>
|
||||
<p className="mt-2 max-w-xl text-sm leading-6 text-muted-foreground">页面独立于现有“深度研究”功能。任务、检索词与证据快照保存在 DeerFlow 后端;当前版本只使用已授权的 普通知识库,不会修改任何知识库或既有报告。</p>
|
||||
</section>
|
||||
|
||||
<Card>
|
||||
@@ -609,12 +610,12 @@ function TaskOverview({
|
||||
<CardContent><ol className="space-y-4">{template.stages.map((stage, index) => {
|
||||
const complete = task.status !== "collecting" && (index < 2 || task.sources.length > 0);
|
||||
const current = task.status === "collecting" && index === 1;
|
||||
return <li key={stage} className="flex gap-3"><span className={cn("flex size-7 shrink-0 items-center justify-center rounded-full border text-xs font-semibold", complete ? "border-emerald-300 bg-emerald-50 text-emerald-700" : current ? "border-primary bg-primary/10 text-primary" : "text-muted-foreground")}>{complete ? <CheckCircle2 className="size-4" /> : index + 1}</span><div className="min-w-0 pt-0.5"><p className="text-sm font-medium">{stage}</p><p className="mt-1 text-xs leading-5 text-muted-foreground">{index === 0 ? "已固定研究主题、范围和知识库权限。" : index === 1 ? task.status === "collecting" ? "正在调用 WeKnora 检索并去重证据。" : `已执行 ${task.queries.length} 个检索方向。` : index === 2 ? `${task.sources.length} 条去重后的资料可在右侧逐条核验。` : "研究计划会固定选用资料、章节结构和写作约束。"}</p></div></li>;
|
||||
return <li key={stage} className="flex gap-3"><span className={cn("flex size-7 shrink-0 items-center justify-center rounded-full border text-xs font-semibold", complete ? "border-emerald-300 bg-emerald-50 text-emerald-700" : current ? "border-primary bg-primary/10 text-primary" : "text-muted-foreground")}>{complete ? <CheckCircle2 className="size-4" /> : index + 1}</span><div className="min-w-0 pt-0.5"><p className="text-sm font-medium">{stage}</p><p className="mt-1 text-xs leading-5 text-muted-foreground">{index === 0 ? "已固定研究主题、范围和知识库权限。" : index === 1 ? task.status === "collecting" ? "正在调用知识服务检索并去重证据。" : `已执行 ${task.queries.length} 个检索方向。` : index === 2 ? `${task.sources.length} 条去重后的资料可在右侧逐条核验。` : "研究计划会固定选用资料、章节结构和写作约束。"}</p></div></li>;
|
||||
})}</ol></CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="gap-1"><CardTitle className="text-base">已执行检索词</CardTitle><CardDescription>检索词由模板和你的研究范围生成,结果仅来自当前账号可见的 WeKnora 知识库。</CardDescription></CardHeader>
|
||||
<CardContent><div className="flex flex-wrap gap-2">{task.queries.map((query) => <Badge key={query} variant="outline" className="max-w-full whitespace-normal py-1 text-left"><Search className="size-3" />{query}</Badge>)}</div>{task.warning ? <div className="mt-4 flex gap-2 rounded-md border border-amber-300 bg-amber-50 p-3 text-sm text-amber-900 dark:border-amber-900 dark:bg-amber-950/40 dark:text-amber-100"><AlertCircle className="mt-0.5 size-4 shrink-0" />{task.warning}</div> : null}</CardContent>
|
||||
<CardHeader className="gap-1"><CardTitle className="text-base">已执行检索词</CardTitle><CardDescription>检索词由模板和你的研究范围生成,结果仅来自当前账号可见的 普通知识库。</CardDescription></CardHeader>
|
||||
<CardContent><div className="flex flex-wrap gap-2">{task.queries.map((query) => <Badge key={query} variant="outline" className="max-w-full whitespace-normal py-1 text-left"><Search className="size-3" />{query}</Badge>)}</div>{task.warning ? <div className="mt-4 flex gap-2 rounded-md border border-amber-300 bg-amber-50 p-3 text-sm text-amber-900 dark:border-amber-900 dark:bg-amber-950/40 dark:text-amber-100"><AlertCircle className="mt-0.5 size-4 shrink-0" />{userFacingKnowledgeMessage(task.warning)}</div> : null}</CardContent>
|
||||
</Card>
|
||||
{canPlan ? <Card>
|
||||
<CardHeader className="gap-1"><CardTitle className="text-base">研究计划与选用资料</CardTitle><CardDescription>计划和资料选择会保存到当前任务;后续报告只能使用这里确认过的证据。</CardDescription></CardHeader>
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function LlmWikiKnowledgePage({
|
||||
return <Navigate to="/page/workspace/knowledge/assistant" replace />;
|
||||
}
|
||||
|
||||
if (runtime.isError || runtime.data?.provider !== "weknora") {
|
||||
if (runtime.isError || runtime.data?.provider !== "weknora" || !runtime.data?.api_base_url?.trim()) {
|
||||
if (mode === "list" || mode === "detail") {
|
||||
return <Navigate to="/page/workspace/knowledge/assistant" replace />;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiFetch } from "@/core/api/fetch-client";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
import { userFacingKnowledgeMessage } from "@/lib/knowledge-message";
|
||||
|
||||
const BASE = () => `${getBackendBaseURL()}/api/assistant-knowledge`;
|
||||
|
||||
@@ -113,7 +114,7 @@ async function readJson<T>(response: Response): Promise<T> {
|
||||
? (data as { detail: unknown }).detail
|
||||
: `请求失败 (${response.status})`;
|
||||
throw new Error(
|
||||
typeof detail === "string" ? detail : JSON.stringify(detail),
|
||||
userFacingKnowledgeMessage(typeof detail === "string" ? detail : JSON.stringify(detail)),
|
||||
);
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
@@ -408,6 +409,17 @@ export async function uploadAssistantWeKnoraPackage(input: {
|
||||
);
|
||||
}
|
||||
|
||||
export async function uploadAssistantDocument(input: { file: File; assistantBaseId?: string | null }) {
|
||||
const body = new FormData();
|
||||
body.set("file", input.file);
|
||||
if (input.assistantBaseId) body.set("assistant_base_id", input.assistantBaseId);
|
||||
return normalizeImportJob(await readJson<AssistantImportJob>(await apiFetch(`${BASE()}/imports/files`, { method: "POST", body })));
|
||||
}
|
||||
|
||||
export async function retryAssistantDocument(jobId: string) {
|
||||
return readJson<AssistantImportJob>(await apiFetch(`${BASE()}/import-jobs/${encodeURIComponent(jobId)}/retry-file`, { method: "POST" }));
|
||||
}
|
||||
|
||||
export async function deleteAssistantKnowledgeSource(sourceId: string) {
|
||||
const response = await apiFetch(
|
||||
`${BASE()}/sources/${encodeURIComponent(sourceId)}`,
|
||||
@@ -490,6 +502,8 @@ export function useAssistantKnowledgeActions() {
|
||||
mutationFn: uploadAssistantWeKnoraPackage,
|
||||
onSuccess: invalidate,
|
||||
}),
|
||||
uploadDocument: useMutation({ mutationFn: uploadAssistantDocument, onSuccess: invalidate }),
|
||||
retryDocument: useMutation({ mutationFn: retryAssistantDocument, onSuccess: invalidate }),
|
||||
reimportSource: useMutation({
|
||||
mutationFn: reimportAssistantKnowledgeSource,
|
||||
onSuccess: invalidate,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { userFacingKnowledgeMessage } from "@/lib/knowledge-message";
|
||||
import { apiFetch } from "@/core/api/fetch-client";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
|
||||
@@ -85,7 +86,7 @@ async function readJson<T>(response: Response, context: string): Promise<T> {
|
||||
const detail = payload && typeof payload === "object" && "detail" in payload
|
||||
? String((payload as { detail: unknown }).detail)
|
||||
: `${context}失败(${response.status})`;
|
||||
throw new Error(detail);
|
||||
throw new Error(userFacingKnowledgeMessage(detail));
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
@@ -249,6 +250,6 @@ export async function deleteEnterpriseResearchTask(taskId: string): Promise<void
|
||||
const detail = payload && typeof payload === "object" && "detail" in payload
|
||||
? String((payload as { detail: unknown }).detail)
|
||||
: `删除研究任务失败(${response.status})`;
|
||||
throw new Error(detail);
|
||||
throw new Error(userFacingKnowledgeMessage(detail));
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { getCsrfToken } from "@/core/api/csrf";
|
||||
import { getAuthorizationHeaderValue } from "@/core/auth";
|
||||
import { useLiveAccount } from "@/core/auth/use-live-account";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
import { userFacingKnowledgeMessage } from "@/lib/knowledge-message";
|
||||
|
||||
const API = () => `${getBackendBaseURL()}/api/llmwiki`;
|
||||
const PUBLIC_API = () => `${getBackendBaseURL()}/api/public/llmwiki`;
|
||||
@@ -366,10 +367,6 @@ export interface LlmWikiWikiPageUpdate {
|
||||
wiki_path?: string;
|
||||
}
|
||||
|
||||
function userFacingKnowledgeMessage(value: string): string {
|
||||
return value.replace(/weknora/gi, "知识服务");
|
||||
}
|
||||
|
||||
async function readJson<T>(response: Response, context: string): Promise<T> {
|
||||
if (!response.ok) {
|
||||
const payload = await response.json().catch(() => null as unknown);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { userFacingKnowledgeMessage } from "@/lib/knowledge-message";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiFetch } from "@/core/api/fetch-client";
|
||||
@@ -102,7 +103,7 @@ async function readJson<T>(response: Response): Promise<T> {
|
||||
data && typeof data === "object" && "detail" in data
|
||||
? JSON.stringify((data as { detail: unknown }).detail)
|
||||
: `请求失败 (${response.status})`;
|
||||
throw new Error(detail.replace(/^"|"$/g, ""));
|
||||
throw new Error(userFacingKnowledgeMessage(detail.replace(/^"|"$/g, "")));
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
+17
-10
@@ -17,6 +17,7 @@ import {
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useOrdinaryKnowledgeLanding } from "@/components/workspace/knowledge-product-nav";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -139,6 +140,7 @@ export function WeKnoraKnowledgeDetailPage() {
|
||||
const [frameError, setFrameError] = useState<string | null>(null);
|
||||
const isAdmin = getStoredAuth()?.system_role === "admin";
|
||||
const runtime = useLlmWikiRuntime();
|
||||
const landing = useOrdinaryKnowledgeLanding();
|
||||
const details = useLlmWikiKnowledgeBase(
|
||||
knowledgeBaseId || null,
|
||||
!!knowledgeBaseId,
|
||||
@@ -165,6 +167,8 @@ export function WeKnoraKnowledgeDetailPage() {
|
||||
searchParams.get("sourceChunkId") || searchParams.get("chunkId") || "";
|
||||
const documentId = searchParams.get("documentId") || "";
|
||||
const wikiSlug = searchParams.get("wikiSlug") || "";
|
||||
const defaultEntry = searchParams.get("view") !== "weknora" && !requestedTab && !wikiSlug && !sourceChunkId && !documentId;
|
||||
const showNativeIndex = defaultEntry && (landing.data ?? "wiki") === "wiki" && !!detailsWikiEnabled;
|
||||
const nativeWikiPageUrl =
|
||||
knowledgeBaseId && wikiSlug && preferredTab === "wiki"
|
||||
? buildWeKnoraWikiPagePath(knowledgeBaseId, wikiSlug)
|
||||
@@ -175,7 +179,7 @@ export function WeKnoraKnowledgeDetailPage() {
|
||||
setFrameSrc("");
|
||||
setFrameError(null);
|
||||
if (
|
||||
nativeWikiPageUrl ||
|
||||
nativeWikiPageUrl || landing.isLoading || showNativeIndex ||
|
||||
!knowledgeBaseId ||
|
||||
runtime.data?.provider !== "weknora" ||
|
||||
!detailsReady
|
||||
@@ -225,6 +229,8 @@ export function WeKnoraKnowledgeDetailPage() {
|
||||
documentId,
|
||||
knowledgeBaseId,
|
||||
nativeWikiPageUrl,
|
||||
landing.isLoading,
|
||||
showNativeIndex,
|
||||
preferredTab,
|
||||
runtime.data?.provider,
|
||||
sourceChunkId,
|
||||
@@ -253,7 +259,7 @@ export function WeKnoraKnowledgeDetailPage() {
|
||||
return <Navigate to={nativeWikiPageUrl} replace />;
|
||||
}
|
||||
|
||||
if (runtime.isLoading || details.isLoading) {
|
||||
if (runtime.isLoading || details.isLoading || landing.isLoading) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||
<Loader2 className="mr-2 size-5 animate-spin" />
|
||||
@@ -262,6 +268,10 @@ export function WeKnoraKnowledgeDetailPage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (showNativeIndex) {
|
||||
return <Navigate to={`/page/workspace/knowledge/${encodeURIComponent(knowledgeBaseId)}/wiki/view`} replace />;
|
||||
}
|
||||
|
||||
if (runtime.data?.provider !== "weknora")
|
||||
return <Navigate to={LIST_PATH} replace />;
|
||||
|
||||
@@ -313,7 +323,7 @@ export function WeKnoraKnowledgeDetailPage() {
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col bg-background">
|
||||
{canManageWiki && (
|
||||
{detailsWikiEnabled && (
|
||||
<div className="flex min-h-12 shrink-0 flex-wrap items-center justify-between gap-2 border-b px-4 py-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
{canEditCurrentWikiArticle && (
|
||||
@@ -334,6 +344,7 @@ export function WeKnoraKnowledgeDetailPage() {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<Button size="sm" variant="outline" onClick={() => navigate(`/page/workspace/knowledge/${encodeURIComponent(knowledgeBaseId)}/wiki/view`)}>查看 Wiki 索引</Button>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -348,18 +359,14 @@ export function WeKnoraKnowledgeDetailPage() {
|
||||
数据包任务
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
{canManageWiki && <Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`/page/workspace/knowledge/${encodeURIComponent(knowledgeBaseId)}/wiki`,
|
||||
)
|
||||
}
|
||||
onClick={() => navigate(`/page/workspace/knowledge/${encodeURIComponent(knowledgeBaseId)}/wiki`)}
|
||||
>
|
||||
<FileText className="mr-2 size-4" />
|
||||
Wiki管理
|
||||
</Button>
|
||||
</Button>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
import { userFacingKnowledgeMessage } from "@/lib/knowledge-message";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Archive,
|
||||
@@ -180,7 +181,7 @@ export default function WeKnoraPackageTransfersPage() {
|
||||
</div>
|
||||
{job.error && (
|
||||
<p className="mt-2 text-sm text-destructive">
|
||||
{job.error}
|
||||
{userFacingKnowledgeMessage(job.error)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+11
-10
@@ -153,11 +153,11 @@ export function WeKnoraWikiPageView({
|
||||
const pages = publicEmbed ? publicPages : privatePages;
|
||||
const remotePagesList = useMemo(() => pages.data?.pages || [], [pages.data?.pages]);
|
||||
const explicitIndexPage = useMemo(
|
||||
() => (publicEmbed ? remotePagesList.find(isWikiIndexPage) : undefined),
|
||||
() => remotePagesList.find(isWikiIndexPage),
|
||||
[publicEmbed, remotePagesList],
|
||||
);
|
||||
const generatedIndexPage = useMemo<LlmWikiWikiPage | undefined>(
|
||||
() => publicEmbed && remotePagesList.length > 0 && !explicitIndexPage
|
||||
() => remotePagesList.length > 0 && !explicitIndexPage
|
||||
? {
|
||||
id: "__generated_public_index__",
|
||||
slug: GENERATED_PUBLIC_INDEX_SLUG,
|
||||
@@ -181,14 +181,14 @@ export function WeKnoraWikiPageView({
|
||||
[generatedIndexPage, remotePagesList],
|
||||
);
|
||||
const landingPage = useMemo(() => selectDefaultWikiPage(pagesList), [pagesList]);
|
||||
const wikiSlug = requestedWikiSlug || (publicEmbed ? landingPage?.slug || "" : "");
|
||||
const wikiSlug = requestedWikiSlug || landingPage?.slug || "";
|
||||
const generatedIndexActive = generatedIndexPage?.slug === wikiSlug;
|
||||
const generatedIndexRequested =
|
||||
publicEmbed && wikiSlug === GENERATED_PUBLIC_INDEX_SLUG && !explicitIndexPage;
|
||||
wikiSlug === GENERATED_PUBLIC_INDEX_SLUG && !explicitIndexPage;
|
||||
const privatePage = useLlmWikiWikiPage(
|
||||
noteId || null,
|
||||
wikiSlug || null,
|
||||
!!noteId && !!wikiSlug && !publicEmbed,
|
||||
!!noteId && !!wikiSlug && !publicEmbed && !generatedIndexActive && !generatedIndexRequested,
|
||||
);
|
||||
const publicPage = usePublicLlmWikiWikiPage(
|
||||
noteId || null,
|
||||
@@ -377,15 +377,15 @@ export function WeKnoraWikiPageView({
|
||||
};
|
||||
|
||||
if (!wikiSlug) {
|
||||
if (publicEmbed && pages.isLoading) {
|
||||
if (pages.isLoading) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||
<Loader2 className="mr-2 size-5 animate-spin" />
|
||||
正在加载公开 Wiki
|
||||
正在加载 Wiki 索引
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (publicEmbed && pages.error) {
|
||||
if (pages.error) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-4 px-6 text-center">
|
||||
<div className="flex size-12 items-center justify-center rounded-full bg-destructive/10 text-destructive">
|
||||
@@ -405,9 +405,9 @@ export function WeKnoraWikiPageView({
|
||||
}
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-4 text-sm text-muted-foreground">
|
||||
{publicEmbed ? "该知识库暂无可展示的 Wiki 页面" : "当前链接缺少 Wiki 页面标识"}
|
||||
该知识库暂无可展示的 Wiki 页面
|
||||
{!publicEmbed && (
|
||||
<Button variant="outline" onClick={() => navigate(LIST_PATH)}>返回知识库</Button>
|
||||
<><Button variant="outline" onClick={() => navigate(LIST_PATH)}>返回知识库</Button><Button onClick={() => navigate(`${LIST_PATH}/${encodeURIComponent(noteId)}?view=weknora`)}>进入知识库管理,导入资料</Button></>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -492,6 +492,7 @@ export function WeKnoraWikiPageView({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => navigate(`${LIST_PATH}/${encodeURIComponent(noteId)}?view=weknora`)}>知识库管理</Button>
|
||||
{globalBase && isDeposited && <Button variant="outline" size="sm" onClick={() => navigate(`/page/workspace/knowledge/assistant?${new URLSearchParams({ base_id: globalBase.id, wikiSlug: pageData.slug })}`)}>在知识梳理中查看</Button>}
|
||||
<Button variant="outline" size="sm" onClick={() => void download("md")} disabled={downloading !== null}>
|
||||
{downloading === "md" ? <Loader2 className="size-4 animate-spin" /> : <Download className="size-4" />}
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
import { userFacingKnowledgeMessage } from "@/lib/knowledge-message";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Download, Loader2, Upload } from "lucide-react";
|
||||
import { useRef } from "react";
|
||||
@@ -110,7 +111,7 @@ export function WikiPackageTransfers({
|
||||
{job.file_size_bytes ? (
|
||||
<span>{(job.file_size_bytes / 1024 / 1024).toFixed(2)} MB</span>
|
||||
) : null}
|
||||
{job.error && <span className="text-destructive">{job.error}</span>}
|
||||
{job.error && <span className="text-destructive">{userFacingKnowledgeMessage(job.error)}</span>}
|
||||
{assistant &&
|
||||
job.status === "completed" &&
|
||||
job.file_exists &&
|
||||
|
||||
Reference in new issue
Block a user