feat(space): 共享工作区增量3b —— KB 知识库按 Space 共享(存储 re-key)
把 3a 的 Space 作用域推到 KB 层:KB/Doc/DocLink owner→space_id,作用域键 owner/name→space_id/name,同空间成员共享知识库(检索/入库/文库/双链/图谱)。 后端: - store: KB/Doc/DocLink 加 space_id,唯一索引 (owner,*)→(space_id,*),owner 降级创建人; 查询全改 space 作用域;SaveDoc/ListVault/GetDocByID/DeleteDocByID/ReplaceDocLinks/ ResolveInboundLinks/ListLinks 改 space;tenantIDForSpace 补异步入库租户 - MigrateKBSpaces 启动迁移(space_id 回填 + 唯一索引换新,同 Agent 顺序坑规避) - scopedKB owner/name→space_id/name;IngestJob 契约加 SpaceID;enqueueIngest/runIngest 穿 space;MinIO 对象键改 space/kb/doc(避免跨空间同名撞键,老键不透明不迁) - KB 写路由(create/ingest/ingest_file/note/delete)挂 RequireSpaceRole(member):viewer 只读 - 存储层重灌迁移端点 POST /admin/migrate-kb-storage(异步):为存量文档入队新 space 作用域的重灌作业(复用 JetStream 入库 worker 池),先删旧键;避免同步重嵌撑爆 HTTP 超时 桌面端: - KbView 收 spaceId(变则重拉库)+spaceReadOnly(viewer 禁建库/入库/文件/笔记);VaultPanel 同 验证(gateway+mcp-go+Milvus/Neo4j/embedding 全栈): - PG 迁移: 20/21 KB + 50/54 doc 回填 space_id(4 未迁=pre-多租户 owner='wt' 空租户遗留, 正确跳过),唯一索引 idx_kb_sn/idx_doc_skn 换新、旧索引删除 - KB 共享: member 见共享库 / viewer 建库·入库 403 / 切回个人空间隔离(看不到) - 全向量链路: RagA 入库(真 dashscope embedding)→ RagB(空间member)检索命中 RagA 内容 - 存储重灌: 端点异步入队 49 作业(worker 池背压处理),重灌后老文档在新 space 键可检索 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -114,7 +114,7 @@ function dedupTriples(ts: Triple[]): Triple[] {
|
||||
}
|
||||
|
||||
// 知识库:owner 隔离 + 项目/案件/文件夹组织;Tab 分(入库 / 文库(Obsidian 式) / 检索 / 图谱)。
|
||||
export function KbView({ identity }: { identity: Identity }) {
|
||||
export function KbView({ identity, spaceId = "", spaceReadOnly = false }: { identity: Identity; spaceId?: string; spaceReadOnly?: boolean }) {
|
||||
const toast = useToast();
|
||||
const [kbs, setKbs] = useState<KbInfo[]>([]);
|
||||
const [kb, setKb] = useState("default");
|
||||
@@ -150,11 +150,13 @@ export function KbView({ identity }: { identity: Identity }) {
|
||||
/* 降级用默认库 */
|
||||
}
|
||||
}, [identity]);
|
||||
// 切换工作区(spaceId 变)后重拉该空间的知识库(共享工作区隔离)。
|
||||
useEffect(() => {
|
||||
void refreshKbs();
|
||||
}, [refreshKbs]);
|
||||
}, [refreshKbs, spaceId]);
|
||||
|
||||
const onCreate = async () => {
|
||||
if (spaceReadOnly) return toast.push("error", "当前工作区你是只读成员(viewer),无权新建知识库");
|
||||
const name = newName.trim();
|
||||
if (!name) return;
|
||||
try {
|
||||
@@ -212,6 +214,7 @@ export function KbView({ identity }: { identity: Identity }) {
|
||||
};
|
||||
|
||||
const onIngest = async () => {
|
||||
if (spaceReadOnly) return toast.push("error", "当前工作区你是只读成员(viewer),无权入库");
|
||||
if (!text.trim()) return;
|
||||
try {
|
||||
const job = await ingestKb(identity, kb, text);
|
||||
@@ -223,6 +226,7 @@ export function KbView({ identity }: { identity: Identity }) {
|
||||
};
|
||||
|
||||
const ingestFiles = (list: FileList | File[] | null | undefined) => {
|
||||
if (spaceReadOnly) return toast.push("error", "当前工作区你是只读成员(viewer),无权入库");
|
||||
const arr = Array.from(list ?? []);
|
||||
arr.forEach((file, idx) => {
|
||||
const fid = `${file.name}-${idx}-${stamp()}-${Math.round(file.size)}`;
|
||||
@@ -387,7 +391,7 @@ export function KbView({ identity }: { identity: Identity }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === "vault" && <VaultPanel identity={identity} kb={kb} />}
|
||||
{tab === "vault" && <VaultPanel identity={identity} kb={kb} spaceReadOnly={spaceReadOnly} />}
|
||||
|
||||
{tab === "search" && (
|
||||
<div className="h-full overflow-y-auto p-4">
|
||||
@@ -436,7 +440,7 @@ export function KbView({ identity }: { identity: Identity }) {
|
||||
|
||||
// VaultPanel:Obsidian 式文库 —— 列表(仅元数据) / 正文按需加载 / [[双链]]可点 / 反链 / 笔记关系图。
|
||||
// 列表与正文分离 + 链接走服务端索引,不再一次拉回整库正文,可扛十几万字大文件。
|
||||
function VaultPanel({ identity, kb }: { identity: Identity; kb: string }) {
|
||||
function VaultPanel({ identity, kb, spaceReadOnly = false }: { identity: Identity; kb: string; spaceReadOnly?: boolean }) {
|
||||
const toast = useToast();
|
||||
const [docs, setDocs] = useState<VaultDoc[]>([]);
|
||||
const [links, setLinks] = useState<DocLink[]>([]);
|
||||
@@ -528,6 +532,7 @@ function VaultPanel({ identity, kb }: { identity: Identity; kb: string }) {
|
||||
setDraft(content);
|
||||
};
|
||||
const onSave = async () => {
|
||||
if (spaceReadOnly) return toast.push("error", "当前工作区你是只读成员(viewer),无权编辑笔记");
|
||||
const name = (creatingNew ? draftName : current?.name ?? "").trim();
|
||||
if (!name || !draft.trim()) {
|
||||
toast.push("error", "笔记名与内容不能为空");
|
||||
|
||||
Reference in New Issue
Block a user