- 知识库笔记列表支持按实体精确筛选(路由/服务/仓储层透传,关系图索引查询) - 自定义智能体技能白名单在 skill_view 与 skill_manage 的 list/view 别名上同样生效; config 未声明 skills 时视为无可见技能 - 嵌入圆桌主题解析默认深蓝,强制主题期间不再写 localStorage 覆盖主站选择 - 智能体提问建议拆出 agent-question-chips 组件,编辑器与详情面板联动优化 - 新增 qa-client 问答测试脚本 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
287 lines
8.4 KiB
TypeScript
287 lines
8.4 KiB
TypeScript
import { apiFetch } from "@/core/api";
|
|
import { getBackendBaseURL } from "@/core/config";
|
|
|
|
import type {
|
|
BatchIngestPayload,
|
|
BatchIngestResult,
|
|
GraphExportResult,
|
|
IngestThreadPayload,
|
|
KnowledgeListParams,
|
|
KnowledgeListResponse,
|
|
KnowledgeNote,
|
|
KnowledgeSearchItem,
|
|
VaultPage,
|
|
WikilinkResolutionMap,
|
|
} from "./types";
|
|
|
|
const baseUrl = () => `${getBackendBaseURL()}/api/knowledge`;
|
|
|
|
async function parseResponse<T>(response: Response): Promise<T> {
|
|
if (!response.ok) {
|
|
const error = await response
|
|
.json()
|
|
.catch(() => ({ detail: "Request failed" }));
|
|
const detail = error.detail;
|
|
if (typeof detail === "string") throw new Error(detail);
|
|
throw new Error("请求失败");
|
|
}
|
|
return response.json() as Promise<T>;
|
|
}
|
|
|
|
export async function listKnowledgeNotes(params: KnowledgeListParams = {}) {
|
|
const qs = new URLSearchParams();
|
|
if (params.keyword) qs.set("keyword", params.keyword);
|
|
if (params.tag) qs.set("tag", params.tag);
|
|
if (params.entity) qs.set("entity", params.entity);
|
|
if (params.source_type) qs.set("source_type", params.source_type);
|
|
if (params.status) qs.set("status", params.status);
|
|
qs.set("limit", String(params.limit ?? 20));
|
|
qs.set("offset", String(params.offset ?? 0));
|
|
return parseResponse<KnowledgeListResponse>(
|
|
await apiFetch(`${baseUrl()}/notes?${qs.toString()}`),
|
|
);
|
|
}
|
|
|
|
export async function getKnowledgeNote(noteId: string) {
|
|
return parseResponse<KnowledgeNote>(
|
|
await apiFetch(`${baseUrl()}/notes/${encodeURIComponent(noteId)}`),
|
|
);
|
|
}
|
|
|
|
export async function createKnowledgeNote(data: {
|
|
title: string;
|
|
content_md: string;
|
|
summary?: string | null;
|
|
tags?: string[];
|
|
status?: string;
|
|
}) {
|
|
const res = await parseResponse<{ note: KnowledgeNote }>(
|
|
await apiFetch(`${baseUrl()}/notes`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
}),
|
|
);
|
|
return res.note;
|
|
}
|
|
|
|
export async function updateKnowledgeNote(
|
|
noteId: string,
|
|
data: Partial<{
|
|
title: string;
|
|
content_md: string;
|
|
summary: string | null;
|
|
tags: string[];
|
|
status: string;
|
|
}>,
|
|
) {
|
|
return parseResponse<KnowledgeNote>(
|
|
await apiFetch(`${baseUrl()}/notes/${encodeURIComponent(noteId)}`, {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function deleteKnowledgeNote(noteId: string, hard = false) {
|
|
return parseResponse<{ ok: boolean; note_id: string }>(
|
|
await apiFetch(
|
|
`${baseUrl()}/notes/${encodeURIComponent(noteId)}${hard ? "?hard=true" : ""}`,
|
|
{ method: "DELETE" },
|
|
),
|
|
);
|
|
}
|
|
|
|
export async function searchKnowledge(query: string, limit = 8) {
|
|
const res = await parseResponse<{ items: KnowledgeSearchItem[] }>(
|
|
await apiFetch(`${baseUrl()}/search`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ query, mode: "keyword", limit }),
|
|
}),
|
|
);
|
|
return res.items;
|
|
}
|
|
|
|
export async function ingestThread(threadId: string, payload: IngestThreadPayload = {}) {
|
|
return parseResponse<{
|
|
note?: KnowledgeNote;
|
|
created?: boolean;
|
|
queued?: boolean;
|
|
}>(
|
|
await apiFetch(`${baseUrl()}/ingest/thread/${encodeURIComponent(threadId)}`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload),
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function ingestThreadsBatch(payload: BatchIngestPayload = {}) {
|
|
return parseResponse<BatchIngestResult>(
|
|
await apiFetch(`${baseUrl()}/ingest/threads/batch`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload),
|
|
}),
|
|
);
|
|
}
|
|
|
|
export interface IngestFileResult {
|
|
note: KnowledgeNote;
|
|
created: boolean;
|
|
notes: KnowledgeNote[];
|
|
llm_fallback: boolean;
|
|
}
|
|
|
|
/** Import a file (md/txt/pdf/word/ppt/excel) into the KB as wiki note(s). */
|
|
export async function ingestKnowledgeFile(
|
|
file: File,
|
|
opts: { title?: string; tags?: string; status?: string; model_name?: string | null } = {},
|
|
) {
|
|
const form = new FormData();
|
|
form.append("file", file);
|
|
if (opts.title) form.append("title", opts.title);
|
|
if (opts.tags) form.append("tags", opts.tags);
|
|
if (opts.status) form.append("status", opts.status);
|
|
if (opts.model_name) form.append("model_name", opts.model_name);
|
|
// No Content-Type header — the browser sets the multipart boundary itself.
|
|
return parseResponse<IngestFileResult>(
|
|
await apiFetch(`${baseUrl()}/ingest/file`, { method: "POST", body: form }),
|
|
);
|
|
}
|
|
|
|
/** Resolve `[[wikilink]]` targets to note ids / vault pages. */
|
|
export async function resolveWikilinks(targets: string[]) {
|
|
const res = await parseResponse<{ resolved: WikilinkResolutionMap }>(
|
|
await apiFetch(`${baseUrl()}/resolve`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ targets }),
|
|
}),
|
|
);
|
|
return res.resolved;
|
|
}
|
|
|
|
/** Resolve every `[[wikilink]]` inside a note's body in one call. */
|
|
export async function resolveNoteWikilinks(noteId: string) {
|
|
const res = await parseResponse<{ resolved: WikilinkResolutionMap }>(
|
|
await apiFetch(
|
|
`${baseUrl()}/notes/${encodeURIComponent(noteId)}/wikilinks`,
|
|
),
|
|
);
|
|
return res.resolved;
|
|
}
|
|
|
|
/** Fetch a raw vault meta/hub Markdown page (no DB note) by its path. */
|
|
export async function fetchVaultPage(path: string) {
|
|
return parseResponse<VaultPage>(
|
|
await apiFetch(`${baseUrl()}/vault-page?path=${encodeURIComponent(path)}`),
|
|
);
|
|
}
|
|
|
|
export async function exportKnowledgeGraph() {
|
|
return parseResponse<GraphExportResult>(
|
|
await apiFetch(`${baseUrl()}/export/graph`, { method: "POST" }),
|
|
);
|
|
}
|
|
|
|
/** Absolute URL of a served wiki-export file (for iframe / download). */
|
|
export function knowledgeExportFileUrl(filename: string) {
|
|
return `${baseUrl()}/export/files/${encodeURIComponent(filename)}`;
|
|
}
|
|
|
|
// --- phase 2/3/4 ---
|
|
|
|
export interface KnowledgeVersion {
|
|
id: string;
|
|
note_id: string;
|
|
title: string;
|
|
summary?: string | null;
|
|
content_md: string;
|
|
status: string;
|
|
tags: string[];
|
|
edited_by?: string | null;
|
|
created_at?: string | null;
|
|
}
|
|
|
|
export async function listNoteVersions(noteId: string) {
|
|
const res = await parseResponse<{ items: KnowledgeVersion[] }>(
|
|
await apiFetch(`${baseUrl()}/notes/${encodeURIComponent(noteId)}/versions`),
|
|
);
|
|
return res.items;
|
|
}
|
|
|
|
export interface DuplicateGroup {
|
|
primary: { id: string; title: string | null };
|
|
duplicates: Array<{ id: string; title: string | null; score: number }>;
|
|
}
|
|
|
|
export async function listDuplicates() {
|
|
const res = await parseResponse<{ groups: DuplicateGroup[] }>(
|
|
await apiFetch(`${baseUrl()}/duplicates`),
|
|
);
|
|
return res.groups;
|
|
}
|
|
|
|
export async function mergeKnowledgeNotes(primaryId: string, duplicateIds: string[]) {
|
|
return parseResponse<{ note: KnowledgeNote }>(
|
|
await apiFetch(`${baseUrl()}/merge`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ primary_id: primaryId, duplicate_ids: duplicateIds }),
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function reindexKnowledge() {
|
|
return parseResponse<{ reindexed: number; embedding_enabled: number }>(
|
|
await apiFetch(`${baseUrl()}/reindex`, { method: "POST" }),
|
|
);
|
|
}
|
|
|
|
export interface KnowledgeGraph {
|
|
nodes: Array<{ id: string; type: string; label: string; category?: string | null }>;
|
|
edges: Array<{ id: string; source: string; target: string; label: string; weight: number }>;
|
|
}
|
|
|
|
export async function getKnowledgeGraph() {
|
|
return parseResponse<KnowledgeGraph>(await apiFetch(`${baseUrl()}/graph`));
|
|
}
|
|
|
|
// --- graph blacklist ---
|
|
|
|
export interface GraphBlacklistItem {
|
|
id: string;
|
|
label: string;
|
|
created_by?: string | null;
|
|
created_at?: string | null;
|
|
}
|
|
|
|
export async function getGraphBlacklist() {
|
|
const res = await parseResponse<{ items: GraphBlacklistItem[] }>(
|
|
await apiFetch(`${baseUrl()}/graph/blacklist`),
|
|
);
|
|
return res.items;
|
|
}
|
|
|
|
/** Blacklist node labels (entity names / note titles); they disappear from the graph. */
|
|
export async function addGraphBlacklist(labels: string[]) {
|
|
return parseResponse<{ added: number; items: GraphBlacklistItem[] }>(
|
|
await apiFetch(`${baseUrl()}/graph/blacklist`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ labels }),
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function removeGraphBlacklist(entryId: string) {
|
|
return parseResponse<{ ok: boolean }>(
|
|
await apiFetch(`${baseUrl()}/graph/blacklist/${encodeURIComponent(entryId)}`, {
|
|
method: "DELETE",
|
|
}),
|
|
);
|
|
}
|