import { apiFetch, apiFetchForm } from "@/lib/api"; /** * 知识库(/api/knowledge/*)适配层。 * 权限:后端 require_knowledge_operator —— 仅管理员或运营角色,前端不做二次判断。 */ /** * 知识库集合白名单,必须与后端 `rag/milvus_collections.py::KNOWLEDGE_COLLECTIONS` 完全一致。 * 后端 `KnowledgeUploadService.confirm` 会校验 `collection_name`,不在白名单内直接 * 抛 400「知识库集合不在允许范围内」——所以 confirm 必须带上这个字段,不能为空。 */ export const KNOWLEDGE_COLLECTIONS = [ { value: "fin_fund_doc", label: "基金文档", description: "基金产品说明、公司业务资料", }, { value: "fin_faq", label: "常见问答", description: "FAQ 问答对,客服与投顾高频问题", }, { value: "fin_policy", label: "政策法规", description: "监管政策、合规文件", }, ] as const; /** 默认入库到基金文档集合(也是检索权重最高的一类业务资料)。 */ export const DEFAULT_KNOWLEDGE_COLLECTION = "fin_fund_doc"; /** 集合英文名 → 中文标签;未知集合原样返回,便于发现后端新增了集合。 */ export function collectionLabel(value?: string | null): string { if (!value) return ""; return KNOWLEDGE_COLLECTIONS.find((item) => item.value === value)?.label ?? value; } /** 校验集合是否在白名单内,供前端提交前前置拦截。 */ export function isValidCollection(value?: string | null): boolean { return !!value && KNOWLEDGE_COLLECTIONS.some((item) => item.value === value); } /** * 把后端返回的原始报错翻译成可执行的下一步提示。 * 后端 `api/chat/knowledge.py` 统一把 UploadValidationError 转成 400 + 原文, * 未捕获的异常则被全局处理器兜成 500「服务内部错误」,这里对两者都给出方向。 */ export function knowledgeErrorHint(message: string): string { const text = message ?? ""; if (text.includes("知识库集合不在允许范围内")) { return "请选择入库的知识库集合后再提交"; } if (text.includes("Unsupported document type")) { return "文件类型不支持,请上传 PDF / Word / Markdown / TXT 等文本类文档"; } if (text.includes("doc_id已存在")) { return "该文档已入库,请先删除旧文档或修改 doc_id"; } if (text.includes("Embedding维度必须为")) { return "向量维度与集合不一致,需按当前 embedding 模型重建集合"; } if (text.includes("文档切片为空")) { return "文档没切出任何内容,请确认文件不是空文档或纯图片扫描件"; } if (text.includes("服务内部错误")) { return "后端依赖异常(通常是向量库 Milvus 未启动或 embedding 服务不可用),请检查基础设施后重试"; } return text; } export interface KnowledgeDocument { doc_id?: string; id?: string; title?: string | null; filename?: string | null; collection_name?: string | null; chunk_count?: number | null; strategy?: string | null; create_time?: string | null; [key: string]: unknown; } export interface UploadPreview { filename?: string; strategy?: string; chunk_size?: number; chunk_overlap?: number; chunks?: unknown[]; preview?: unknown; [key: string]: unknown; } export const knowledgeApi = { list: () => apiFetch("/knowledge/documents"), get: (docId: string) => apiFetch(`/knowledge/documents/${encodeURIComponent(docId)}`), /** 第一步:上传文件拿切分预览,不落库。 */ preview: (form: FormData) => apiFetchForm("/knowledge/documents/preview", form), /** 第二步:确认入库(可复用 preview 返回的 doc_id)。 */ confirm: (form: FormData) => apiFetchForm("/knowledge/documents/confirm", form), remove: (docId: string) => apiFetch<{ doc_id?: string; deleted?: boolean; [key: string]: unknown }>( `/knowledge/documents/${encodeURIComponent(docId)}`, { method: "DELETE" } ), }; /** 组装知识库上传表单,空值不提交,避免后端把空串当成有效参数。 */ export function buildKnowledgeForm( file: File, extra: { title?: string; doc_id?: string; collection_name?: string; strategy?: string; chunk_size?: number | null; chunk_overlap?: number | null; } = {} ): FormData { const form = new FormData(); form.append("file", file); for (const [key, value] of Object.entries(extra)) { if (value !== undefined && value !== null && value !== "") { form.append(key, String(value)); } } return form; }