feat: 实现知识同步审核发布生命周期

This commit is contained in:
2026-07-11 19:04:16 +08:00
parent 73fdfed0e5
commit d2d43de6d3
6 changed files with 972 additions and 0 deletions

View File

@@ -50,6 +50,8 @@ def import_knowledge_from_space(
feishu_space_id=space_id,
feishu_node_id=node_token,
status=1,
knowledge_type="general",
review_mode="manual",
remark=f"obj_type={info.get('obj_type', '')}",
)
db.add(knowledge)
@@ -74,6 +76,8 @@ def create_knowledge(
feishu_node_id=payload.feishuNodeId,
status=payload.status,
remark=payload.remark,
knowledge_type=payload.knowledgeType,
review_mode=payload.reviewMode,
)
db.add(knowledge)
db.flush()
@@ -96,6 +100,8 @@ def update_knowledge(
knowledge.feishu_node_id = payload.feishuNodeId
knowledge.status = payload.status
knowledge.remark = payload.remark
knowledge.knowledge_type = payload.knowledgeType
knowledge.review_mode = payload.reviewMode
db.add(knowledge)
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="update", target_id=knowledge.id)
db.commit()
@@ -137,6 +143,18 @@ def _knowledge_dict(knowledge: Knowledge) -> dict:
"feishuNodeId": knowledge.feishu_node_id,
"status": knowledge.status,
"remark": knowledge.remark,
"sourceTitle": knowledge.source_title,
"knowledgeType": knowledge.knowledge_type,
"reviewMode": knowledge.review_mode,
"lifecycleStatus": knowledge.lifecycle_status,
"sourceStatus": knowledge.source_status,
"sourceError": knowledge.source_error,
"manifestConfirmed": knowledge.manifest_confirmed == 1,
"currentVersionId": knowledge.current_version_id,
"pendingVersionId": knowledge.pending_version_id,
"lastSyncedAt": knowledge.last_synced_at,
"lastPublishedAt": knowledge.last_published_at,
"migrationStatus": knowledge.migration_status,
"createdAt": knowledge.created_at,
"updatedAt": knowledge.updated_at,
}

View File

@@ -0,0 +1,401 @@
from __future__ import annotations
import json
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.dependencies import get_current_admin
from app.core.responses import api_success
from app.models.admin import Admin
from app.models.knowledge import (
Knowledge,
KnowledgeCard,
KnowledgeChunk,
KnowledgeManifest,
KnowledgeSection,
KnowledgeSyncJob,
KnowledgeVersion,
)
from app.schemas.knowledge import (
KnowledgeLifecycleRequest,
KnowledgePublishRequest,
KnowledgeReviewRequest,
KnowledgeStatusRequest,
KnowledgeSyncRequest,
)
from app.services.admin_service import OperationLogService
from app.services.knowledge_pipeline_service import KnowledgePipelineService
router = APIRouter()
@router.get("/knowledge/{knowledge_id}/detail")
def detail(
knowledge_id: int,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
_require_super_admin(current_admin)
knowledge = _knowledge(db, knowledge_id)
current = db.get(KnowledgeVersion, knowledge.current_version_id) if knowledge.current_version_id else None
pending = db.get(KnowledgeVersion, knowledge.pending_version_id) if knowledge.pending_version_id else None
manifest = None
manifest_version_id = knowledge.pending_version_id or knowledge.current_version_id
if manifest_version_id:
manifest = db.scalar(select(KnowledgeManifest).where(KnowledgeManifest.version_id == manifest_version_id))
return api_success(
{
"knowledge": _knowledge_detail(knowledge),
"currentVersion": _version_dict(current),
"pendingVersion": _version_dict(pending),
"manifest": _manifest_dict(manifest),
}
)
@router.get("/knowledge/{knowledge_id}/versions")
def versions(
knowledge_id: int,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
_require_super_admin(current_admin)
_knowledge(db, knowledge_id)
rows = db.scalars(
select(KnowledgeVersion)
.where(KnowledgeVersion.knowledge_id == knowledge_id)
.order_by(KnowledgeVersion.version_no.desc())
).all()
return api_success([_version_dict(item) for item in rows])
@router.get("/knowledge/version/{version_id}/content")
def version_content(
version_id: int,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
_require_super_admin(current_admin)
version = _version(db, version_id)
sections = db.scalars(
select(KnowledgeSection).where(KnowledgeSection.version_id == version.id).order_by(KnowledgeSection.sort_order)
).all()
cards = db.scalars(
select(KnowledgeCard).where(KnowledgeCard.version_id == version.id).order_by(KnowledgeCard.id)
).all()
manifest = db.scalar(select(KnowledgeManifest).where(KnowledgeManifest.version_id == version.id))
return api_success(
{
"version": _version_dict(version),
"manifest": _manifest_dict(manifest),
"sections": [
{
"id": item.id,
"sectionKey": item.section_key,
"title": item.title,
"content": item.content,
"sourceStart": item.source_start,
"sourceEnd": item.source_end,
"contentHash": item.content_hash,
}
for item in sections
],
"cards": [
{
"id": item.id,
"sectionId": item.section_id,
"title": item.title,
"summary": item.summary,
"coreConclusion": item.core_conclusion,
"applicableQuestions": item.applicable_questions,
"inapplicableQuestions": item.inapplicable_questions,
"keywords": _json_value(item.keywords),
"synonyms": _json_value(item.synonyms),
"riskLevel": item.risk_level,
"sourceRange": item.source_range,
"reviewStatus": item.review_status,
}
for item in cards
],
}
)
@router.get("/knowledge/{knowledge_id}/sync-jobs")
def sync_jobs(
knowledge_id: int,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
_require_super_admin(current_admin)
rows = db.scalars(
select(KnowledgeSyncJob)
.where(KnowledgeSyncJob.knowledge_id == knowledge_id)
.order_by(KnowledgeSyncJob.id.desc())
.limit(50)
).all()
return api_success([_job_dict(item) for item in rows])
@router.post("/knowledge/{knowledge_id}/sync")
async def synchronize(
knowledge_id: int,
payload: KnowledgeSyncRequest,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
_require_super_admin(current_admin)
knowledge = _knowledge(db, knowledge_id)
if knowledge.lifecycle_status == "archived":
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="归档知识库不能同步")
try:
job = await KnowledgePipelineService.synchronize(
db, knowledge, admin_id=current_admin.id, mode=payload.mode
)
except Exception as exc:
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f"知识同步失败:{exc}") from exc
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="sync", target_id=knowledge.id)
db.commit()
return api_success(_job_dict(job))
@router.post("/knowledge/{knowledge_id}/manifest/{version_id}/confirm")
def confirm_manifest(
knowledge_id: int,
version_id: int,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
_require_super_admin(current_admin)
knowledge, version = _knowledge_version(db, knowledge_id, version_id)
try:
KnowledgePipelineService.confirm_manifest(db, knowledge, version, current_admin.id)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="confirm_manifest", target_id=knowledge.id)
db.commit()
return api_success()
@router.post("/knowledge/{knowledge_id}/version/{version_id}/review")
def review(
knowledge_id: int,
version_id: int,
payload: KnowledgeReviewRequest,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
_require_super_admin(current_admin)
knowledge, version = _knowledge_version(db, knowledge_id, version_id)
if version.quality_status == "blocked" and payload.action == "approve":
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="自动质检存在阻断项,不能审核通过")
KnowledgePipelineService.review_version(db, knowledge, version, current_admin.id, payload.action, payload.reason)
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action=f"review_{payload.action}", target_id=knowledge.id)
db.commit()
return api_success()
@router.post("/knowledge/{knowledge_id}/version/{version_id}/publish")
def publish(
knowledge_id: int,
version_id: int,
payload: KnowledgePublishRequest,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
_require_super_admin(current_admin)
knowledge, version = _knowledge_version(db, knowledge_id, version_id)
try:
KnowledgePipelineService.publish_version(db, knowledge, version, current_admin.id, payload.keepOpen)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="publish", target_id=knowledge.id)
db.commit()
return api_success(_knowledge_detail(knowledge))
@router.post("/knowledge/{knowledge_id}/version/{version_id}/rollback")
def rollback(
knowledge_id: int,
version_id: int,
payload: KnowledgePublishRequest,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
_require_super_admin(current_admin)
knowledge, version = _knowledge_version(db, knowledge_id, version_id)
try:
KnowledgePipelineService.rollback(db, knowledge, version, current_admin.id, payload.keepOpen)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="rollback", target_id=knowledge.id)
db.commit()
return api_success(_knowledge_detail(knowledge))
@router.put("/knowledge/{knowledge_id}/open-status")
def open_status(
knowledge_id: int,
payload: KnowledgeStatusRequest,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
_require_super_admin(current_admin)
knowledge = _knowledge(db, knowledge_id)
if payload.open:
if not knowledge.current_version_id:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="知识库没有正式发布版本")
if knowledge.manifest_confirmed != 1:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="首次知识库说明卡尚未确认")
if knowledge.source_status != "normal":
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="飞书源文章异常,不能开放")
if knowledge.lifecycle_status != "active":
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="归档知识库不能开放")
knowledge.status = 1 if payload.open else 0
db.add(knowledge)
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="open" if payload.open else "close", target_id=knowledge.id)
db.commit()
return api_success(_knowledge_detail(knowledge))
@router.put("/knowledge/{knowledge_id}/lifecycle")
def lifecycle(
knowledge_id: int,
payload: KnowledgeLifecycleRequest,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
_require_super_admin(current_admin)
knowledge = _knowledge(db, knowledge_id)
knowledge.lifecycle_status = payload.status
if payload.status == "archived":
knowledge.status = 0
db.add(knowledge)
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action=payload.status, target_id=knowledge.id)
db.commit()
return api_success(_knowledge_detail(knowledge))
@router.get("/knowledge-migration/summary")
def migration_summary(
db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)
) -> dict:
_require_super_admin(current_admin)
rows = db.scalars(select(Knowledge).order_by(Knowledge.id)).all()
counts: dict[str, int] = {}
for item in rows:
counts[item.migration_status] = counts.get(item.migration_status, 0) + 1
return api_success({"total": len(rows), "counts": counts, "items": [_knowledge_detail(item) for item in rows]})
def _require_super_admin(admin: Admin) -> None:
if admin.role is not None and admin.role.code != "super_admin":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="仅超级管理员可以执行此操作")
def _knowledge(db: Session, knowledge_id: int) -> Knowledge:
item = db.get(Knowledge, knowledge_id)
if item is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="知识库不存在")
return item
def _version(db: Session, version_id: int) -> KnowledgeVersion:
item = db.get(KnowledgeVersion, version_id)
if item is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="知识版本不存在")
return item
def _knowledge_version(db: Session, knowledge_id: int, version_id: int) -> tuple[Knowledge, KnowledgeVersion]:
knowledge = _knowledge(db, knowledge_id)
version = _version(db, version_id)
if version.knowledge_id != knowledge.id:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="版本不属于该知识库")
return knowledge, version
def _knowledge_detail(item: Knowledge) -> dict:
return {
"id": item.id,
"name": item.name,
"sourceTitle": item.source_title,
"feishuSpaceId": item.feishu_space_id,
"feishuNodeId": item.feishu_node_id,
"knowledgeType": item.knowledge_type,
"reviewMode": item.review_mode,
"open": item.status == 1,
"lifecycleStatus": item.lifecycle_status,
"sourceStatus": item.source_status,
"sourceError": item.source_error,
"manifestConfirmed": item.manifest_confirmed == 1,
"currentVersionId": item.current_version_id,
"pendingVersionId": item.pending_version_id,
"lastSyncedAt": item.last_synced_at,
"lastPublishedAt": item.last_published_at,
"migrationStatus": item.migration_status,
}
def _version_dict(item: KnowledgeVersion | None) -> dict | None:
if item is None:
return None
return {
"id": item.id,
"knowledgeId": item.knowledge_id,
"snapshotId": item.snapshot_id,
"versionNo": item.version_no,
"status": item.status,
"qualityStatus": item.quality_status,
"changeSummary": _json_value(item.change_summary),
"qualityReport": _json_value(item.quality_report),
"processingRuleVersion": item.processing_rule_version,
"generationModel": item.generation_model,
"createdAt": item.created_at,
"publishedAt": item.published_at,
}
def _manifest_dict(item: KnowledgeManifest | None) -> dict | None:
if item is None:
return None
return {
"id": item.id,
"versionId": item.version_id,
"purpose": item.purpose,
"applicableQuestions": item.applicable_questions,
"inapplicableQuestions": item.inapplicable_questions,
"coreTopics": item.core_topics,
"entities": item.entities,
"boundaries": item.boundaries,
"changeLevel": item.change_level,
"confirmed": item.confirmed == 1,
}
def _job_dict(item: KnowledgeSyncJob) -> dict:
return {
"id": item.id,
"knowledgeId": item.knowledge_id,
"mode": item.mode,
"status": item.status,
"stage": item.stage,
"progress": item.progress,
"sourceError": item.source_error,
"resultSummary": _json_value(item.result_summary),
"createdAt": item.created_at,
"startedAt": item.started_at,
"finishedAt": item.finished_at,
}
def _json_value(raw: str | None):
if not raw:
return None
try:
return json.loads(raw)
except json.JSONDecodeError:
return raw

View File

@@ -6,6 +6,7 @@ from app.api import (
admin_auth,
admin_dashboard,
admin_knowledge,
admin_knowledge_lifecycle,
admin_records,
admin_settings,
admin_users,
@@ -24,5 +25,6 @@ api_router.include_router(admin_auth.router, prefix="/admin", tags=["admin"])
api_router.include_router(admin_dashboard.router, prefix="/admin", tags=["admin"])
api_router.include_router(admin_users.router, prefix="/admin", tags=["admin"])
api_router.include_router(admin_knowledge.router, prefix="/admin", tags=["admin"])
api_router.include_router(admin_knowledge_lifecycle.router, prefix="/admin", tags=["admin-knowledge-lifecycle"])
api_router.include_router(admin_settings.router, prefix="/admin", tags=["admin"])
api_router.include_router(admin_records.router, prefix="/admin", tags=["admin"])