From 3598e34819a8b04626a9d174b3ebbc183ce9980b Mon Sep 17 00:00:00 2001 From: Nelson <1475262689@qq.com> Date: Wed, 8 Jul 2026 18:50:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=9F=A5=E8=AF=86=E5=BA=93=E9=80=92?= =?UTF-8?q?=E5=BD=92=E5=AF=BC=E5=85=A5=EF=BC=88=E4=BB=8E=E6=8C=87=E5=AE=9A?= =?UTF-8?q?=E7=88=B6=E8=8A=82=E7=82=B9=EF=BC=8C=E4=B8=8D=E9=9C=80=E8=A6=81?= =?UTF-8?q?=E7=A9=BA=E9=97=B4=E6=9D=83=E9=99=90=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - feishu_service.py: import_space_nodes 改为 import_nodes_from_parent 从指定父节点递归获取子节点,不需要空间级别权限 - admin_knowledge.py: import API 增加 parent_node_token 参数 - api.ts/App.vue: 前端增加父节点 NodeToken 输入框 --- .../apps/admin-web/src/App.vue | 31 ++++++++++++ .../apps/admin-web/src/services/api.ts | 5 ++ .../apps/backend/app/api/admin_knowledge.py | 50 +++++++++++++++++++ .../backend/app/services/feishu_service.py | 43 ++++++++++++++++ 4 files changed, 129 insertions(+) diff --git a/ai_knowledge_base_v2/apps/admin-web/src/App.vue b/ai_knowledge_base_v2/apps/admin-web/src/App.vue index 4ef1ad1..62120fe 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/App.vue +++ b/ai_knowledge_base_v2/apps/admin-web/src/App.vue @@ -37,6 +37,9 @@ const chatDetailOpen = ref(false); const recordTab = ref("chats"); const editingModelId = ref(null); const editingKnowledgeId = ref(null); +const batchSpaceId = ref(""); +const batchParentToken = ref(""); +const batchImporting = ref(false); const agentDebugging = ref(false); const agentPreviewMessages = ref<{ role: "user" | "assistant" | "system"; content: string }[]>([ { role: "assistant", content: "选择模型和知识库后,可以在这里调试 Agent 的真实问答效果。" }, @@ -589,6 +592,27 @@ function resetKnowledgeForm() { Object.assign(knowledgeForm, { name: "", feishuSpaceId: "", feishuNodeId: "", status: 1, remark: "" }); } +async function batchImportKnowledge() { + const spaceId = batchSpaceId.value.trim(); + const parentToken = batchParentToken.value.trim(); + if (!spaceId || !parentToken) { + ElMessage.warning("请输入 SpaceID 和父节点 NodeToken"); + return; + } + batchImporting.value = true; + try { + const result = await api.importKnowledgeFromSpace(JSON.stringify({ spaceId, parent_node_token: parentToken })); + ElMessage.success(`导入完成:新增 ${result.imported} 个,跳过 ${result.skipped} 个`); + batchSpaceId.value = ""; + batchParentToken.value = ""; + await loadCurrentMenu(); + } catch (e: any) { + ElMessage.error(e.message || "导入失败"); + } finally { + batchImporting.value = false; + } +} + async function savePrompt() { const result = await api.savePrompt(promptContent.value); promptContent.value = result.promptContent; @@ -1101,6 +1125,13 @@ function buildChatQuery() { 清空 + + +
+ + + 递归导入子节点 +
diff --git a/ai_knowledge_base_v2/apps/admin-web/src/services/api.ts b/ai_knowledge_base_v2/apps/admin-web/src/services/api.ts index 5b71cd1..4c9cc16 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/services/api.ts +++ b/ai_knowledge_base_v2/apps/admin-web/src/services/api.ts @@ -106,6 +106,11 @@ export const api = { updateKnowledge: (id: number, payload: Record) => request(`/admin/knowledge/${id}`, { method: "PUT", body: JSON.stringify(payload) }), deleteKnowledge: (id: number) => request(`/admin/knowledge/${id}`, { method: "DELETE" }), + importKnowledgeFromSpace: (body: Record) => + request<{ imported: number; skipped: number; nodes: { name: string; nodeToken: string; status: string; id?: number }[] }>( + "/admin/knowledge/import", + { method: "POST", body: JSON.stringify(body) }, + ), prompt: () => request<{ promptContent: string }>("/admin/prompt"), savePrompt: (promptContent: string) => request<{ promptContent: string }>("/admin/prompt", { method: "PUT", body: JSON.stringify({ promptContent }) }), diff --git a/ai_knowledge_base_v2/apps/backend/app/api/admin_knowledge.py b/ai_knowledge_base_v2/apps/backend/app/api/admin_knowledge.py index f8c1c06..57dc00e 100644 --- a/ai_knowledge_base_v2/apps/backend/app/api/admin_knowledge.py +++ b/ai_knowledge_base_v2/apps/backend/app/api/admin_knowledge.py @@ -11,6 +11,7 @@ from app.models.admin import Admin from app.models.knowledge import Knowledge, UserKnowledgePermission from app.schemas.admin import KnowledgeSaveRequest from app.services.admin_service import OperationLogService +from app.services.feishu_service import FeishuKnowledgeService router = APIRouter() @@ -21,6 +22,55 @@ def list_knowledge(db: Session = Depends(get_db), current_admin: Admin = Depends return api_success([_knowledge_dict(item) for item in items]) +@router.post("/knowledge/import") +def import_knowledge_from_space( + payload: dict, + db: Session = Depends(get_db), + current_admin: Admin = Depends(get_current_admin), +) -> dict: + space_id = payload.get("spaceId", "").strip() + parent_node_token = payload.get("parent_node_token", "").strip() + if not space_id or not parent_node_token: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="SpaceID 和父节点 Token 不能为空") + + nodes = FeishuKnowledgeService.import_nodes_from_parent(space_id, parent_node_token, db) + if not nodes: + return api_success({"imported": 0, "skipped": 0, "nodes": []}) + + # 获取已存在的 node_id,避免重复导入 + existing_nodes = { + row.feishu_node_id for row in db.scalars(select(Knowledge)).all() if row.feishu_node_id + } + + imported = 0 + skipped = 0 + results = [] + for node in nodes: + node_token = node["node_token"] + if node_token in existing_nodes: + skipped += 1 + results.append({"name": node["name"], "nodeToken": node_token, "status": "skipped"}) + continue + + knowledge = Knowledge( + name=node["name"], + feishu_space_id=space_id, + feishu_node_id=node_token, + status=1, + remark=f"obj_type={node.get('obj_type', '')}", + ) + db.add(knowledge) + db.flush() + imported += 1 + results.append({"name": node["name"], "nodeToken": node_token, "status": "imported", "id": knowledge.id}) + OperationLogService.write( + db, admin_id=current_admin.id, module="knowledge", action="import", target_id=knowledge.id + ) + + db.commit() + return api_success({"imported": imported, "skipped": skipped, "nodes": results}) + + @router.post("/knowledge") def create_knowledge( payload: KnowledgeSaveRequest, diff --git a/ai_knowledge_base_v2/apps/backend/app/services/feishu_service.py b/ai_knowledge_base_v2/apps/backend/app/services/feishu_service.py index 97bc509..620c281 100644 --- a/ai_knowledge_base_v2/apps/backend/app/services/feishu_service.py +++ b/ai_knowledge_base_v2/apps/backend/app/services/feishu_service.py @@ -330,6 +330,49 @@ class FeishuKnowledgeService: cls._content_cache.clear() return count + @classmethod + def import_nodes_from_parent( + cls, + space_id: str, + parent_node_token: str, + db: Session | None = None, + ) -> list[dict]: + """从指定节点递归获取所有子节点,用于批量导入知识库。 + 不需要空间级别权限,只需要对 parent_node_token 所在节点有查看权限。 + """ + config = _feishu_retrieval_config(db) + if not config.app_id or not config.app_secret: + raise ExternalServiceError( + "未配置飞书应用凭证,无法读取飞书知识库。", + provider="feishu", + ) + + all_nodes: list[dict] = [] + visited: set[str] = set() + + def _fetch_children(parent_token: str) -> None: + try: + children = _get_wiki_children(space_id, parent_token, config) + except ExternalServiceError: + return + for child in children: + node_token = child.get("node_token", "") + if not node_token or node_token in visited: + continue + visited.add(node_token) + all_nodes.append({ + "name": child.get("title", node_token), + "node_token": node_token, + "space_id": space_id, + "obj_type": child.get("obj_type", ""), + "parent_token": parent_token, + }) + if child.get("has_child", False): + _fetch_children(node_token) + + _fetch_children(parent_node_token) + return all_nodes + @classmethod def retrieve( cls,