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(response: Response): Promise { 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; } 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( await apiFetch(`${baseUrl()}/notes?${qs.toString()}`), ); } export async function getKnowledgeNote(noteId: string) { return parseResponse( 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( 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( 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( 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( await apiFetch(`${baseUrl()}/vault-page?path=${encodeURIComponent(path)}`), ); } export async function exportKnowledgeGraph() { return parseResponse( 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(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", }), ); }