feat: improve knowledge sync and entity alignment
This commit is contained in:
1 parent
7dbac9cc56
commit
dea4697ea0
38 files changed
+3742
-634
No files matched your search
@@ -56,7 +56,7 @@ type PageSidebarProps = {
|
||||
headerHidden?: boolean;
|
||||
};
|
||||
|
||||
type SettingsSection = "account" | "appearance" | "memory" | "tools" | "about";
|
||||
type SettingsSection = "account" | "appearance" | "memory" | "tools" | "encoder" | "about";
|
||||
|
||||
const hasActiveChild = (item: MenuItem, pathname: string): boolean =>
|
||||
menuItemContainsPath(item, pathname);
|
||||
@@ -270,6 +270,12 @@ function SecondaryNavSidebar({ headerHidden }: { headerHidden: boolean }) {
|
||||
模型配置
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem onClick={() => openSettings("encoder")}>
|
||||
<Cpu className="size-4" />
|
||||
本地编码器
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigate("/page/strategy/settings/model-concurrency")}
|
||||
@@ -585,6 +591,12 @@ function LegacyAccordionSidebar({
|
||||
模型配置
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem onClick={() => openSettings("encoder")}>
|
||||
<Cpu className="size-4" />
|
||||
本地编码器
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem onClick={() => navigate("/page/strategy/admin/login-whitelist")}>
|
||||
<ShieldCheck className="size-4" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { BellIcon, PaletteIcon, SparklesIcon, UserIcon, WrenchIcon } from "lucide-react";
|
||||
import { BellIcon, CpuIcon, PaletteIcon, SparklesIcon, UserIcon, WrenchIcon } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import {
|
||||
@@ -14,16 +14,19 @@ import { AboutSettingsPage } from "@/components/workspace/settings/about-setting
|
||||
import { AccountSettingsPage } from "@/components/workspace/settings/account-settings-page";
|
||||
import { AppearanceSettingsPage } from "@/components/workspace/settings/appearance-settings-page";
|
||||
import { MemorySettingsPage } from "@/components/workspace/settings/memory-settings-page";
|
||||
import { ToolSettingsPage } from "@/components/workspace/settings/tool-settings-page";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ToolSettingsPage } from "@/components/workspace/settings/tool-settings-page";
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LocalEncoderSettingsCard } from "@/strategy-components/components/resource-management/llmwiki/WeKnoraKnowledgeWorkspace";
|
||||
|
||||
type SettingsSection =
|
||||
| "account"
|
||||
| "appearance"
|
||||
| "memory"
|
||||
| "tools"
|
||||
| "about";
|
||||
| "memory"
|
||||
| "tools"
|
||||
| "encoder"
|
||||
| "about";
|
||||
|
||||
type SettingsDialogProps = React.ComponentProps<typeof Dialog> & {
|
||||
defaultSection?: SettingsSection;
|
||||
@@ -31,7 +34,8 @@ type SettingsDialogProps = React.ComponentProps<typeof Dialog> & {
|
||||
|
||||
export function SettingsDialog(props: SettingsDialogProps) {
|
||||
const { defaultSection = "appearance", ...dialogProps } = props;
|
||||
const { t } = useI18n();
|
||||
const { t } = useI18n();
|
||||
const isAdmin = getStoredAuth()?.system_role === "admin";
|
||||
const [activeSection, setActiveSection] =
|
||||
useState<SettingsSection>(defaultSection);
|
||||
|
||||
@@ -63,7 +67,10 @@ export function SettingsDialog(props: SettingsDialogProps) {
|
||||
// label: t.settings.sections.memory,
|
||||
// icon: BrainIcon,
|
||||
// },
|
||||
{ id: "tools", label: t.settings.sections.tools, icon: WrenchIcon },
|
||||
{ id: "tools", label: t.settings.sections.tools, icon: WrenchIcon },
|
||||
...(isAdmin
|
||||
? [{ id: "encoder", label: "本地编码器", icon: CpuIcon }]
|
||||
: []),
|
||||
{
|
||||
id: "account",
|
||||
label: "个人信息",
|
||||
@@ -75,7 +82,8 @@ export function SettingsDialog(props: SettingsDialogProps) {
|
||||
t.settings.sections.appearance,
|
||||
t.settings.sections.memory,
|
||||
t.settings.sections.tools,
|
||||
t.settings.sections.about,
|
||||
t.settings.sections.about,
|
||||
isAdmin,
|
||||
],
|
||||
);
|
||||
return (
|
||||
@@ -125,7 +133,8 @@ export function SettingsDialog(props: SettingsDialogProps) {
|
||||
<AppearanceSettingsPage onClose={() => props.onOpenChange?.(false)} />
|
||||
)}
|
||||
{activeSection === "memory" && <MemorySettingsPage />}
|
||||
{activeSection === "tools" && <ToolSettingsPage />}
|
||||
{activeSection === "tools" && <ToolSettingsPage />}
|
||||
{activeSection === "encoder" && isAdmin && <LocalEncoderSettingsCard />}
|
||||
{activeSection === "about" && <AboutSettingsPage />}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
@@ -102,9 +102,16 @@ import { cn } from "@/lib/utils";
|
||||
import { SettingsSection } from "./settings-section";
|
||||
import { SkillStateSheet } from "./skill-state-sheet";
|
||||
|
||||
export function SkillSettingsPage({ onClose }: { onClose?: () => void } = {}) {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
export function SkillSettingsPage({
|
||||
onClose,
|
||||
embedded = false,
|
||||
}: {
|
||||
onClose?: () => void;
|
||||
embedded?: boolean;
|
||||
} = {}) {
|
||||
const { t } = useI18n();
|
||||
if (embedded) return <SkillSettingsList onClose={onClose} />;
|
||||
return (
|
||||
<SettingsSection
|
||||
title={t.settings.skills.title}
|
||||
description={t.settings.skills.description}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Database, Loader2, RefreshCw, Search, Sparkles } from "lucide-react";
|
||||
import { CheckCircle2, Database, Loader2, RefreshCw, Search, Sparkles } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -8,8 +8,7 @@ import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { useAllSkillsAdmin } from "@/core/skills/hooks";
|
||||
import { useAllSkillsAdmin, useSkills } from "@/core/skills/hooks";
|
||||
import { useLlmWikiKnowledgeBases, useLlmWikiRuntime } from "@/strategy-components/api/llmwiki";
|
||||
import {
|
||||
type SkillKnowledgeTarget,
|
||||
@@ -33,17 +32,29 @@ const PHASE_LABELS: Record<string, string> = {
|
||||
completed: "完成",
|
||||
};
|
||||
|
||||
export function SkillKnowledgePanel() {
|
||||
const { skills, isLoading } = useAllSkillsAdmin(true);
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
queued: "等待处理",
|
||||
running: "处理中",
|
||||
awaiting_review: "等待管理员审核",
|
||||
completed: "已完成",
|
||||
partial_failed: "部分失败",
|
||||
failed: "失败",
|
||||
canceled: "已取消",
|
||||
};
|
||||
|
||||
export function SkillKnowledgePanel({ adminMode = false }: { adminMode?: boolean }) {
|
||||
const visibleSkills = useSkills();
|
||||
const allSkills = useAllSkillsAdmin(adminMode);
|
||||
const skills = adminMode ? allSkills.skills : visibleSkills.skills;
|
||||
const isLoading = adminMode ? allSkills.isLoading : visibleSkills.isLoading;
|
||||
const liveSkills = useMemo(() => skills.filter((skill) => (skill.state ?? "active") === "active"), [skills]);
|
||||
const [selectedSkills, setSelectedSkills] = useState<string[]>([]);
|
||||
const [skillQuery, setSkillQuery] = useState("");
|
||||
const [selectedTargetKey, setSelectedTargetKey] = useState<string | null>(null);
|
||||
const [reviewMode, setReviewMode] = useState<"auto_high_confidence" | "off">("off");
|
||||
const runtime = useLlmWikiRuntime();
|
||||
const knowledgeBases = useLlmWikiKnowledgeBases("all", runtime.data?.provider === "weknora");
|
||||
const bindings = useSkillKnowledgeBindings();
|
||||
const jobs = useSkillKnowledgeJobs();
|
||||
const bindings = useSkillKnowledgeBindings(undefined, !adminMode);
|
||||
const jobs = useSkillKnowledgeJobs(!adminMode);
|
||||
const actions = useSkillKnowledgeActions();
|
||||
|
||||
const targets = useMemo<SkillKnowledgeTarget[]>(() => {
|
||||
@@ -122,7 +133,7 @@ export function SkillKnowledgePanel() {
|
||||
const job = await actions.createJob.mutateAsync({
|
||||
skill_names: selectedSkills,
|
||||
targets: selectedTargets,
|
||||
review_mode: reviewMode,
|
||||
review_mode: "off",
|
||||
});
|
||||
toast.success(`已创建 ${job.total_count} 个归纳项`);
|
||||
setSelectedSkills([]);
|
||||
@@ -132,17 +143,17 @@ export function SkillKnowledgePanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid size-full min-h-0 grid-cols-[minmax(320px,0.9fr)_minmax(420px,1.1fr)] gap-4 overflow-hidden p-6">
|
||||
<div className="grid size-full min-h-0 gap-4 overflow-auto p-4 lg:grid-cols-[minmax(320px,0.9fr)_minmax(420px,1.1fr)] lg:overflow-hidden lg:p-6">
|
||||
<section className="flex min-h-0 flex-col rounded-lg border bg-card">
|
||||
<div className="border-b p-4">
|
||||
<h2 className="font-semibold">技能到知识库管理</h2>
|
||||
<p className="mt-1 text-xs text-muted-foreground">一次选择多个技能归纳到一个 WeKnora Wiki 知识库;代码与脚本不会被读取。</p>
|
||||
<h2 className="font-semibold">归纳技能知识</h2>
|
||||
<p className="mt-1 text-xs leading-5 text-muted-foreground">选择技能和一个目标知识库后,系统会在后台提取 Markdown 等知识资料,忽略 Python 等代码文件,自动生成 Wiki、关系和向量索引。</p>
|
||||
</div>
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
<div className="space-y-5 p-4">
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-sm font-medium">1. 选择技能({selectedSkills.length})</span>
|
||||
<span className="text-sm font-medium">第一步:选择技能(已选 {selectedSkills.length})</span>
|
||||
<Button variant="ghost" size="sm" onClick={toggleFilteredSkills}>
|
||||
{allFilteredSelected ? "清空当前结果" : "选择当前结果"}
|
||||
</Button>
|
||||
@@ -162,6 +173,13 @@ export function SkillKnowledgePanel() {
|
||||
<Checkbox checked={selectedSkills.includes(skill.name)} onCheckedChange={() => toggle(selectedSkills, skill.name, setSelectedSkills)} />
|
||||
<span className="truncate text-sm">{skill.name_zh || skill.name}</span>
|
||||
<span className="ml-auto truncate text-xs text-muted-foreground">{skill.name}</span>
|
||||
{(bindings.data ?? []).some((binding) => binding.skill_name === skill.name) && (
|
||||
<Badge variant="secondary" className="shrink-0 text-[10px]">
|
||||
{(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} 库`}
|
||||
</Badge>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
{!isLoading && !filteredSkills.length && (
|
||||
@@ -174,7 +192,7 @@ export function SkillKnowledgePanel() {
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-sm font-medium">2. 选择目标知识库({selectedTargetKey ? 1 : 0})</span>
|
||||
<span className="text-sm font-medium">第二步:选择一个目标知识库</span>
|
||||
</div>
|
||||
<div className="space-y-1 rounded-md border p-2">
|
||||
{targets.map((target) => {
|
||||
@@ -207,19 +225,12 @@ export function SkillKnowledgePanel() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium">审核策略</span>
|
||||
<Select value={reviewMode} onValueChange={(value) => setReviewMode(value as typeof reviewMode)}>
|
||||
<SelectTrigger className="w-52"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="auto_high_confidence">高置信自动通过</SelectItem>
|
||||
<SelectItem value="off">不阻塞写入</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="rounded-lg border bg-muted/30 p-3 text-xs leading-5 text-muted-foreground">
|
||||
第三步:提交后可离开本页。归纳、Wiki 生成、向量化及同步到“知识梳理”总库均在后台完成。
|
||||
</div>
|
||||
<Button className="w-full" disabled={actions.createJob.isPending} onClick={() => void submit()}>
|
||||
<Button className="w-full" disabled={actions.createJob.isPending || !selectedSkills.length || !selectedTargetKey} onClick={() => void submit()}>
|
||||
{actions.createJob.isPending ? <Loader2 className="animate-spin" /> : <Sparkles />}
|
||||
开始归纳({selectedSkills.length * (selectedTargetKey ? 1 : 0)} 项)
|
||||
开始归纳 {selectedSkills.length ? `${selectedSkills.length} 个技能` : ""}
|
||||
</Button>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
@@ -228,22 +239,26 @@ export function SkillKnowledgePanel() {
|
||||
<section className="flex min-h-0 flex-col gap-4 overflow-hidden">
|
||||
<div className="flex min-h-0 flex-1 flex-col rounded-lg border bg-card">
|
||||
<div className="flex items-center justify-between border-b p-4">
|
||||
<div><h2 className="font-semibold">归纳任务</h2><p className="text-xs text-muted-foreground">后台执行、失败可单项重试</p></div>
|
||||
<div><h2 className="font-semibold">我的归纳任务</h2><p className="text-xs text-muted-foreground">任务会自动刷新,失败项可直接重试</p></div>
|
||||
<Button size="icon" variant="ghost" onClick={() => void jobs.refetch()}><RefreshCw className="size-4" /></Button>
|
||||
</div>
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
<div className="space-y-3 p-4">
|
||||
{(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 (
|
||||
<div key={job.id} className="rounded-md border p-3">
|
||||
<div className="flex items-center gap-2 text-sm"><Badge variant={job.status === "completed" ? "default" : "secondary"}>{job.status}</Badge><span>{done}/{job.total_count} 项</span><span className="ml-auto text-xs text-muted-foreground">{new Date(job.created_at).toLocaleString()}</span></div>
|
||||
<div className="flex items-center gap-2 text-sm"><Badge variant={job.status === "completed" ? "default" : "secondary"}>{STATUS_LABELS[job.status] || job.status}</Badge><span>{done}/{job.total_count} 项</span><span className="ml-auto text-xs text-muted-foreground">{new Date(job.created_at).toLocaleString()}</span></div>
|
||||
<Progress className="my-2" value={progress} />
|
||||
{job.items?.map((item) => (
|
||||
<div key={item.id} className="mt-2 flex items-center gap-2 border-t pt-2 text-xs">
|
||||
<span className="max-w-36 truncate font-medium">{item.skill_name}</span>
|
||||
<span className="text-muted-foreground">{PHASE_LABELS[item.phase] || item.phase}</span>
|
||||
<span className="text-muted-foreground">{PHASE_LABELS[item.phase] || STATUS_LABELS[item.status] || item.phase}</span>
|
||||
<span className="ml-auto">{item.progress}%</span>
|
||||
{item.status === "failed" && <Button size="sm" variant="outline" onClick={() => actions.retry.mutate(item.id)}>重试</Button>}
|
||||
</div>
|
||||
@@ -256,8 +271,8 @@ export function SkillKnowledgePanel() {
|
||||
</ScrollArea>
|
||||
</div>
|
||||
<div className="max-h-80 overflow-auto rounded-lg border bg-card p-3 text-xs">
|
||||
<div className="mb-2 text-muted-foreground">当前有效绑定:{bindings.data?.length ?? 0};同名同类型实体按来源贡献合并。</div>
|
||||
<div className="space-y-2">{bindings.data?.map((binding) => <div key={binding.id} className="flex flex-wrap items-center gap-2 rounded border p-2"><span className="max-w-32 truncate font-medium">{binding.skill_name}</span><span className="max-w-40 truncate text-muted-foreground">→ {binding.target_name}</span><Badge variant="outline">{binding.status}</Badge><code className="max-w-40 truncate text-[10px] text-muted-foreground">{binding.remote_business_key}</code><Button size="sm" variant="outline" className="ml-auto" disabled={actions.resync.isPending} onClick={() => actions.resync.mutate(binding.id)}>重归纳</Button><Button size="sm" variant="ghost" onClick={() => { if (window.confirm("解除关联并保留目标知识内容?")) actions.detach.mutate({ id: binding.id, deleteContributions: false }); }}>解绑并保留</Button><Button size="sm" variant="destructive" onClick={() => { if (window.confirm("解除关联并删除此技能贡献?共享知识仍会保留其他来源。")) actions.detach.mutate({ id: binding.id, deleteContributions: true }); }}>解绑并删除贡献</Button></div>)}</div>
|
||||
<div className="mb-2 flex items-center gap-2 text-muted-foreground"><CheckCircle2 className="size-4 text-emerald-600" />已归纳关联:{bindings.data?.length ?? 0}</div>
|
||||
<div className="space-y-2">{bindings.data?.map((binding) => <div key={binding.id} className="flex flex-wrap items-center gap-2 rounded border p-2"><span className="max-w-40 truncate font-medium">{binding.skill_name}</span><span className="max-w-48 truncate text-muted-foreground">已归纳到 {binding.target_name}</span><Badge variant="outline">{STATUS_LABELS[binding.status] || binding.status}</Badge><Button size="sm" variant="outline" className="ml-auto" disabled={actions.resync.isPending} onClick={() => actions.resync.mutate(binding.id)}>重新归纳</Button><Button size="sm" variant="ghost" onClick={() => { if (window.confirm("解除关联并保留目标知识内容?")) actions.detach.mutate({ id: binding.id, deleteContributions: false }); }}>解除关联</Button>{adminMode && <Button size="sm" variant="destructive" onClick={() => { if (window.confirm("解除关联并删除此技能贡献?共享知识仍会保留其他来源。")) actions.detach.mutate({ id: binding.id, deleteContributions: true }); }}>删除贡献</Button>}</div>)}</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
<Settings2Icon />
|
||||
{t.common.settings}
|
||||
</DropdownMenuItem>
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSettingsDefaultSection("encoder");
|
||||
setSettingsOpen(true);
|
||||
}}
|
||||
>
|
||||
<CpuIcon />
|
||||
本地编码器
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
@@ -254,14 +265,6 @@ export function WorkspaceNavMenu() {
|
||||
技能管理
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigate("/page/workspace/admin/skills?tab=knowledge")}
|
||||
>
|
||||
<DatabaseZapIcon />
|
||||
技能到知识库管理
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigate("/page/workspace/admin/models")}
|
||||
|
||||
Reference in new issue
Block a user