feat: 知识库递归导入(从指定父节点,不需要空间权限)

- feishu_service.py: import_space_nodes 改为 import_nodes_from_parent
  从指定父节点递归获取子节点,不需要空间级别权限
- admin_knowledge.py: import API 增加 parent_node_token 参数
- api.ts/App.vue: 前端增加父节点 NodeToken 输入框
This commit is contained in:
2026-07-08 18:50:46 +08:00
parent f4196612d4
commit 3598e34819
4 changed files with 129 additions and 0 deletions

View File

@@ -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,

View File

@@ -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,