feat: 知识库文件夹/模板、技能压缩设置与摘要窗口调优
- 知识库新增文件夹与模板支持(模型/仓储/服务/迁移),前端知识管理组件 - 岗位管理模型与接口完善 - 技能新增压缩设置页与相关存储/工具能力 - 系统设置扩展配置项 - 摘要中间件 keep 窗口改用 token 预算(16000),减少重复压缩 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
e6662cf673
commit
5ebbb82d66
54 files changed
+2658
-100
No files matched your search
@@ -16,6 +16,12 @@ import { Input } from "@/components/ui/input";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { EmptyDocumentState } from "@/components/wiki/EmptyDocumentState";
|
||||
import {
|
||||
ExtractTemplateManageDialog,
|
||||
FolderManageDialog,
|
||||
FolderSelect,
|
||||
TemplateSelect,
|
||||
} from "@/components/wiki/KnowledgeManage";
|
||||
import { KnowledgeSidebar } from "@/components/wiki/KnowledgeSidebar";
|
||||
import { WikiContent } from "@/components/wiki/WikiContent";
|
||||
import { WikiHeader } from "@/components/wiki/WikiHeader";
|
||||
@@ -28,6 +34,7 @@ import { ModelNameSelect } from "@/components/workspace/model-name-select";
|
||||
import {
|
||||
useKnowledgeActions,
|
||||
useKnowledgeDuplicates,
|
||||
useKnowledgeFolders,
|
||||
useKnowledgeNote,
|
||||
useKnowledgeNotes,
|
||||
useNoteWikilinks,
|
||||
@@ -70,11 +77,13 @@ function CreateNoteDialog({
|
||||
const [title, setTitle] = useState("");
|
||||
const [tags, setTags] = useState("");
|
||||
const [content, setContent] = useState("");
|
||||
const [folder, setFolder] = useState<string | null>(null);
|
||||
|
||||
const reset = () => {
|
||||
setTitle("");
|
||||
setTags("");
|
||||
setContent("");
|
||||
setFolder(null);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@@ -91,6 +100,7 @@ function CreateNoteDialog({
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean),
|
||||
status: "approved",
|
||||
folder,
|
||||
});
|
||||
toast.success("知识已创建");
|
||||
onOpenChange(false);
|
||||
@@ -119,6 +129,10 @@ function CreateNoteDialog({
|
||||
value={tags}
|
||||
onChange={(e) => setTags(e.target.value)}
|
||||
/>
|
||||
<label className="flex items-center justify-between gap-2 text-sm">
|
||||
目录
|
||||
<FolderSelect value={folder} onChange={setFolder} triggerClassName="w-[14rem]" />
|
||||
</label>
|
||||
<Textarea
|
||||
placeholder="Markdown 正文"
|
||||
rows={12}
|
||||
@@ -152,6 +166,8 @@ function BatchImportDialog({
|
||||
const [skipExisting, setSkipExisting] = useState(true);
|
||||
// Optional model override for extraction (null = configured default).
|
||||
const [model, setModel] = useState<string | null>(null);
|
||||
const [template, setTemplate] = useState<string | null>(null);
|
||||
const [folder, setFolder] = useState<string | null>(null);
|
||||
|
||||
const handleRun = async () => {
|
||||
try {
|
||||
@@ -161,6 +177,8 @@ function BatchImportDialog({
|
||||
status: "approved",
|
||||
include_sources: true,
|
||||
model_name: model,
|
||||
template_id: template,
|
||||
folder,
|
||||
});
|
||||
const base = `批量导入完成:新增 ${result.created},跳过 ${result.skipped},失败 ${result.failed}`;
|
||||
if (result.llm_fallback && result.llm_fallback > 0) {
|
||||
@@ -217,6 +235,25 @@ function BatchImportDialog({
|
||||
triggerClassName="h-9 w-[11rem]"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex items-center justify-between gap-2 text-sm">
|
||||
抽取模板
|
||||
<TemplateSelect
|
||||
value={template}
|
||||
onChange={setTemplate}
|
||||
scope="thread"
|
||||
disabled={batchIngest.isPending}
|
||||
triggerClassName="w-[11rem]"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex items-center justify-between gap-2 text-sm">
|
||||
归入目录
|
||||
<FolderSelect
|
||||
value={folder}
|
||||
onChange={setFolder}
|
||||
disabled={batchIngest.isPending}
|
||||
triggerClassName="w-[11rem]"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
@@ -244,12 +281,16 @@ function ImportFileDialog({
|
||||
const [title, setTitle] = useState("");
|
||||
const [tags, setTags] = useState("");
|
||||
const [model, setModel] = useState<string | null>(null);
|
||||
const [template, setTemplate] = useState<string | null>(null);
|
||||
const [folder, setFolder] = useState<string | null>(null);
|
||||
|
||||
const reset = () => {
|
||||
setFile(null);
|
||||
setTitle("");
|
||||
setTags("");
|
||||
setModel(null);
|
||||
setTemplate(null);
|
||||
setFolder(null);
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
@@ -265,6 +306,8 @@ function ImportFileDialog({
|
||||
tags: tags.trim() || undefined,
|
||||
status: "approved",
|
||||
model_name: model,
|
||||
template_id: template,
|
||||
folder,
|
||||
},
|
||||
});
|
||||
const count = result.notes?.length ?? 1;
|
||||
@@ -330,6 +373,25 @@ function ImportFileDialog({
|
||||
triggerClassName="h-9 w-[11rem]"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex items-center justify-between gap-2 text-sm">
|
||||
抽取模板
|
||||
<TemplateSelect
|
||||
value={template}
|
||||
onChange={setTemplate}
|
||||
scope="document"
|
||||
disabled={ingestFile.isPending}
|
||||
triggerClassName="w-[11rem]"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex items-center justify-between gap-2 text-sm">
|
||||
归入目录
|
||||
<FolderSelect
|
||||
value={folder}
|
||||
onChange={setFolder}
|
||||
disabled={ingestFile.isPending}
|
||||
triggerClassName="w-[11rem]"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
@@ -478,6 +540,8 @@ export default function KnowledgeBasePage() {
|
||||
const [batchOpen, setBatchOpen] = useState(false);
|
||||
const [importOpen, setImportOpen] = useState(false);
|
||||
const [dedupOpen, setDedupOpen] = useState(false);
|
||||
const [foldersOpen, setFoldersOpen] = useState(false);
|
||||
const [templatesOpen, setTemplatesOpen] = useState(false);
|
||||
const [vaultPath, setVaultPath] = useState<string | null>(null);
|
||||
|
||||
const params = useMemo(
|
||||
@@ -491,8 +555,13 @@ export default function KnowledgeBasePage() {
|
||||
);
|
||||
|
||||
const { data, isLoading } = useKnowledgeNotes(params);
|
||||
const { data: folders = [] } = useKnowledgeFolders();
|
||||
const notes = data?.items ?? [];
|
||||
const tree = useMemo(() => buildKnowledgeTree(notes), [notes]);
|
||||
const folderPaths = useMemo(() => folders.map((f) => f.path), [folders]);
|
||||
const tree = useMemo(
|
||||
() => buildKnowledgeTree(notes, folderPaths),
|
||||
[notes, folderPaths],
|
||||
);
|
||||
|
||||
const { data: note, isLoading: noteLoading } = useKnowledgeNote(noteId);
|
||||
const { data: wikiLinks } = useNoteWikilinks(noteId);
|
||||
@@ -571,6 +640,8 @@ export default function KnowledgeBasePage() {
|
||||
onDedup={() => setDedupOpen(true)}
|
||||
onGraph={() => navigate("/page/workspace/knowledge/graph")}
|
||||
onCreate={() => setCreateOpen(true)}
|
||||
onManageFolders={() => setFoldersOpen(true)}
|
||||
onManageTemplates={() => setTemplatesOpen(true)}
|
||||
/>
|
||||
|
||||
<div
|
||||
@@ -636,6 +707,8 @@ export default function KnowledgeBasePage() {
|
||||
<ImportFileDialog open={importOpen} onOpenChange={setImportOpen} />
|
||||
<BatchImportDialog open={batchOpen} onOpenChange={setBatchOpen} />
|
||||
<DedupDialog open={dedupOpen} onOpenChange={setDedupOpen} />
|
||||
<FolderManageDialog open={foldersOpen} onOpenChange={setFoldersOpen} />
|
||||
<ExtractTemplateManageDialog open={templatesOpen} onOpenChange={setTemplatesOpen} />
|
||||
<VaultPageDialog
|
||||
path={vaultPath}
|
||||
onOpenChange={(open) => {
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
|
||||
import { Loader2Icon } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
useSkillCompressionSettings,
|
||||
useUpdateSkillCompressionSettings,
|
||||
} from "@/core/system-settings/hooks";
|
||||
|
||||
/**
|
||||
* 管理员「技能压缩」配置页。
|
||||
*
|
||||
* 开启后,只有被标记为「常驻(免压缩)」的技能会全文注入系统提示词;其余已启用
|
||||
* 技能会被压缩(从提示词正文剔除),由智能体在需要时通过 `search_skills` 工具
|
||||
* 按关键词检索调出。关闭时行为不变 —— 所有已启用技能全文注入。
|
||||
*/
|
||||
export default function SkillCompressionSettingsPage({
|
||||
embedded = false,
|
||||
}: {
|
||||
embedded?: boolean;
|
||||
} = {}) {
|
||||
const { data, isLoading, error } = useSkillCompressionSettings();
|
||||
const update = useUpdateSkillCompressionSettings();
|
||||
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [keepIndex, setKeepIndex] = useState(true);
|
||||
const [searchTopN, setSearchTopN] = useState(5);
|
||||
const initializedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || initializedRef.current) return;
|
||||
setEnabled(data.enabled);
|
||||
setKeepIndex(data.keep_index_in_prompt);
|
||||
setSearchTopN(data.search_top_n);
|
||||
initializedRef.current = true;
|
||||
}, [data]);
|
||||
|
||||
const handleSave = async () => {
|
||||
const topN = Math.max(1, Math.min(Math.round(searchTopN) || 5, 50));
|
||||
try {
|
||||
await update.mutateAsync({
|
||||
enabled,
|
||||
keep_index_in_prompt: keepIndex,
|
||||
search_top_n: topN,
|
||||
});
|
||||
setSearchTopN(topN);
|
||||
toast.success("已保存");
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "保存失败");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
embedded
|
||||
? "flex w-full flex-col gap-6"
|
||||
: "mx-auto flex w-full max-w-2xl flex-col gap-6 px-6 py-10"
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<h1 className={embedded ? "text-base font-semibold" : "text-xl font-semibold"}>
|
||||
技能压缩配置
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
技能过多会让系统提示词变得臃肿、增加 token 消耗。开启「技能压缩」后,只有被标记为
|
||||
「常驻(免压缩)」的技能会全文注入系统提示词;其余已启用技能会被压缩,智能体在需要时
|
||||
通过 <code>search_skills</code> 工具按关键词检索调出。关闭时行为不变 ——
|
||||
所有已启用技能全文注入。某个技能是否常驻,在该技能的详情页单独开启。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<Loader2Icon className="text-muted-foreground size-6 animate-spin" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-destructive rounded-xl border border-dashed py-16 text-center text-sm">
|
||||
加载失败:{error.message}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-5 rounded-xl border bg-white px-5 py-5 dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-sm font-medium">启用技能压缩</div>
|
||||
<div className="text-muted-foreground mt-0.5 text-xs">
|
||||
关闭时,所有已启用技能照旧全文注入系统提示词(默认行为)。
|
||||
</div>
|
||||
</div>
|
||||
<Switch checked={enabled} onCheckedChange={setEnabled} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-sm font-medium">保留技能名索引</div>
|
||||
<div className="text-muted-foreground mt-0.5 text-xs">
|
||||
开启后,被压缩的技能仍以「仅名字」的紧凑列表出现在提示词里,帮助智能体知道存在
|
||||
哪些技能;关闭则完全隐藏,只能靠 search_skills 检索发现。
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={keepIndex}
|
||||
disabled={!enabled}
|
||||
onCheckedChange={setKeepIndex}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<p className="text-sm font-medium">检索返回条数</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
search_skills 工具每次最多返回的匹配技能数量(1–50)。
|
||||
</p>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={50}
|
||||
value={searchTopN}
|
||||
disabled={!enabled}
|
||||
onChange={(e) => setSearchTopN(Number(e.target.value))}
|
||||
className="w-28"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleSave} disabled={update.isPending}>
|
||||
{update.isPending && (
|
||||
<Loader2Icon className="mr-1.5 size-4 animate-spin" />
|
||||
)}
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import MemoryPage from './MemoryPage';
|
||||
import PromptPrefixSettingsPage from './PromptPrefixSettingsPage';
|
||||
import PrivateSquareSettingsPage from './PrivateSquareSettingsPage';
|
||||
import PublishSquaresSettingsPage from './PublishSquaresSettingsPage';
|
||||
import SkillCompressionSettingsPage from './SkillCompressionSettingsPage';
|
||||
import SkillEvolutionPage from './SkillEvolutionPage';
|
||||
import SkillsPage from './SkillsPage';
|
||||
import UsersPage from './UsersPage';
|
||||
@@ -534,6 +535,18 @@ export default function WorkspaceRoutes({
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="settings/skill-compression"
|
||||
element={
|
||||
getStoredAuth()?.system_role === 'admin' ? (
|
||||
<WorkspaceLayout {...layoutProps}>
|
||||
<SkillCompressionSettingsPage />
|
||||
</WorkspaceLayout>
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="settings/llm-metrics"
|
||||
element={
|
||||
|
||||
Reference in new issue
Block a user