Files
deerflow-code/frontend-web/src/core/knowledge/api.ts
T
吉鑫升andClaude Fable 5 28098b3155 feat: 知识库实体筛选、技能白名单收紧、嵌入主题与提问组件优化
- 知识库笔记列表支持按实体精确筛选(路由/服务/仓储层透传,关系图索引查询)
- 自定义智能体技能白名单在 skill_view 与 skill_manage 的 list/view 别名上同样生效;
  config 未声明 skills 时视为无可见技能
- 嵌入圆桌主题解析默认深蓝,强制主题期间不再写 localStorage 覆盖主站选择
- 智能体提问建议拆出 agent-question-chips 组件,编辑器与详情面板联动优化
- 新增 qa-client 问答测试脚本

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:09:32 +08:00

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",
}),
);
}