refactor(knowledge): keep latest version only
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
"""keep only the latest knowledge version
|
||||
|
||||
Revision ID: 0012_knowledge_latest_only
|
||||
Revises: 0011_knowledge_auto_publish
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
revision = "0012_knowledge_latest_only"
|
||||
down_revision = "0011_knowledge_auto_publish"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute("UPDATE sys_knowledge SET pending_version_id = NULL")
|
||||
for table in (
|
||||
"sys_knowledge_review_record",
|
||||
"sys_knowledge_card",
|
||||
"sys_knowledge_chunk",
|
||||
"sys_knowledge_section",
|
||||
"sys_knowledge_manifest",
|
||||
):
|
||||
op.execute(
|
||||
f"""
|
||||
DELETE detail
|
||||
FROM {table} AS detail
|
||||
JOIN sys_knowledge AS knowledge ON knowledge.id = detail.knowledge_id
|
||||
WHERE knowledge.current_version_id IS NULL
|
||||
OR detail.version_id <> knowledge.current_version_id
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
DELETE version
|
||||
FROM sys_knowledge_version AS version
|
||||
JOIN sys_knowledge AS knowledge ON knowledge.id = version.knowledge_id
|
||||
WHERE knowledge.current_version_id IS NULL
|
||||
OR version.id <> knowledge.current_version_id
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
DELETE snapshot
|
||||
FROM sys_knowledge_source_snapshot AS snapshot
|
||||
LEFT JOIN sys_knowledge_version AS version ON version.snapshot_id = snapshot.id
|
||||
WHERE version.id IS NULL
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Removed historical content cannot be reconstructed from the latest version.
|
||||
pass
|
||||
@@ -23,7 +23,6 @@ from app.schemas.knowledge import (
|
||||
KnowledgeLifecycleRequest,
|
||||
KnowledgeBatchMetadataUpdateRequest,
|
||||
KnowledgeMetadataUpdateRequest,
|
||||
KnowledgeRollbackRequest,
|
||||
KnowledgeStatusRequest,
|
||||
)
|
||||
from app.services.admin_service import OperationLogService
|
||||
@@ -113,22 +112,6 @@ def detail(
|
||||
)
|
||||
|
||||
|
||||
@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,
|
||||
@@ -218,29 +201,6 @@ async def synchronize(
|
||||
return api_success(_job_dict(job))
|
||||
|
||||
|
||||
@router.post("/knowledge/{knowledge_id}/version/{version_id}/rollback")
|
||||
def rollback(
|
||||
knowledge_id: int,
|
||||
version_id: int,
|
||||
payload: KnowledgeRollbackRequest,
|
||||
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 knowledge.current_version_id == version.id:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="目标版本已是当前正式版本")
|
||||
if payload.keepOpen and knowledge.lifecycle_status != "active":
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="归档知识库不能在回滚后开放")
|
||||
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,
|
||||
@@ -340,14 +300,6 @@ def _version(db: Session, version_id: int) -> KnowledgeVersion:
|
||||
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,
|
||||
|
||||
@@ -3,10 +3,6 @@ from __future__ import annotations
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class KnowledgeRollbackRequest(BaseModel):
|
||||
keepOpen: bool = False
|
||||
|
||||
|
||||
class KnowledgeStatusRequest(BaseModel):
|
||||
open: bool
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import re
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.knowledge import (
|
||||
@@ -15,6 +15,7 @@ from app.models.knowledge import (
|
||||
KnowledgeChunk,
|
||||
KnowledgeManifest,
|
||||
KnowledgePublishLog,
|
||||
KnowledgeReviewRecord,
|
||||
KnowledgeSection,
|
||||
KnowledgeSourceSnapshot,
|
||||
KnowledgeSyncJob,
|
||||
@@ -24,7 +25,7 @@ from app.services.external_errors import ExternalServiceError
|
||||
from app.services.feishu_async_service import AsyncFeishuKnowledgeService
|
||||
from app.services.feishu_service import _feishu_retrieval_config
|
||||
|
||||
PROCESSING_RULE_VERSION = "knowledge-pipeline-v2-auto-publish"
|
||||
PROCESSING_RULE_VERSION = "knowledge-pipeline-v3-latest-only"
|
||||
MAX_SECTION_CHARS = 2800
|
||||
MAX_CHUNK_CHARS = 650
|
||||
|
||||
@@ -114,6 +115,8 @@ class KnowledgePipelineService:
|
||||
else:
|
||||
job.status = "unchanged"
|
||||
job.result_summary = json.dumps({"changed": False}, ensure_ascii=False)
|
||||
if latest_version is not None:
|
||||
cls._prune_old_versions(db, knowledge, latest_version)
|
||||
job.stage = "completed"
|
||||
job.progress = 100
|
||||
job.finished_at = _now()
|
||||
@@ -305,7 +308,7 @@ class KnowledgePipelineService:
|
||||
keep_open: bool,
|
||||
action: str,
|
||||
) -> None:
|
||||
"""Atomically activate a validated version and preserve rollback history."""
|
||||
"""Atomically replace the active version with validated latest content."""
|
||||
if version.quality_status == "blocked":
|
||||
raise ValueError("质检未通过的版本不能生效")
|
||||
if keep_open and knowledge.lifecycle_status != "active":
|
||||
@@ -313,17 +316,6 @@ class KnowledgePipelineService:
|
||||
if keep_open and knowledge.source_status != "normal":
|
||||
raise ValueError("飞书源文章异常,不能替换正式版本")
|
||||
previous_version_id = knowledge.current_version_id
|
||||
if previous_version_id:
|
||||
previous = db.get(KnowledgeVersion, previous_version_id)
|
||||
if previous and previous.id != version.id:
|
||||
previous.status = "superseded"
|
||||
db.add(previous)
|
||||
previous_pending_id = knowledge.pending_version_id
|
||||
if previous_pending_id and previous_pending_id not in {previous_version_id, version.id}:
|
||||
previous_pending = db.get(KnowledgeVersion, previous_pending_id)
|
||||
if previous_pending and previous_pending.status not in {"published", "superseded"}:
|
||||
previous_pending.status = "discarded"
|
||||
db.add(previous_pending)
|
||||
version.status = "published"
|
||||
version.published_by = admin_id
|
||||
version.published_at = _now()
|
||||
@@ -347,34 +339,36 @@ class KnowledgePipelineService:
|
||||
),
|
||||
]
|
||||
)
|
||||
db.flush()
|
||||
KnowledgePipelineService._prune_old_versions(db, knowledge, version)
|
||||
|
||||
@staticmethod
|
||||
def rollback(db: Session, knowledge: Knowledge, version: KnowledgeVersion, admin_id: int, keep_open: bool) -> None:
|
||||
if version.status not in {"published", "superseded"}:
|
||||
raise ValueError("只能回滚到历史正式版本")
|
||||
previous_id = knowledge.current_version_id
|
||||
if previous_id:
|
||||
previous = db.get(KnowledgeVersion, previous_id)
|
||||
if previous:
|
||||
previous.status = "superseded"
|
||||
db.add(previous)
|
||||
version.status = "published"
|
||||
knowledge.current_version_id = version.id
|
||||
knowledge.status = 1 if keep_open else 0
|
||||
knowledge.last_published_at = _now()
|
||||
db.add_all(
|
||||
[
|
||||
knowledge,
|
||||
version,
|
||||
KnowledgePublishLog(
|
||||
knowledge_id=knowledge.id,
|
||||
from_version_id=previous_id,
|
||||
to_version_id=version.id,
|
||||
action="rollback",
|
||||
keep_open=knowledge.status,
|
||||
operated_by=admin_id,
|
||||
),
|
||||
]
|
||||
def _prune_old_versions(db: Session, knowledge: Knowledge, keep: KnowledgeVersion) -> None:
|
||||
"""Keep only the active version and its derived retrieval data."""
|
||||
old_version_ids = list(db.scalars(
|
||||
select(KnowledgeVersion.id).where(
|
||||
KnowledgeVersion.knowledge_id == knowledge.id,
|
||||
KnowledgeVersion.id != keep.id,
|
||||
)
|
||||
).all())
|
||||
if old_version_ids:
|
||||
for model in (
|
||||
KnowledgeReviewRecord,
|
||||
KnowledgeCard,
|
||||
KnowledgeChunk,
|
||||
KnowledgeSection,
|
||||
KnowledgeManifest,
|
||||
):
|
||||
db.execute(delete(model).where(model.version_id.in_(old_version_ids)))
|
||||
db.execute(delete(KnowledgeVersion).where(KnowledgeVersion.id.in_(old_version_ids)))
|
||||
|
||||
# Snapshots are source copies for a version. Once that version is gone,
|
||||
# retaining the old snapshot would still be retaining knowledge history.
|
||||
db.execute(
|
||||
delete(KnowledgeSourceSnapshot).where(
|
||||
KnowledgeSourceSnapshot.knowledge_id == knowledge.id,
|
||||
KnowledgeSourceSnapshot.id != keep.snapshot_id,
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -128,29 +128,7 @@ def test_batch_metadata_update_reports_missing_items():
|
||||
assert db.scalar(select(Knowledge).where(Knowledge.id == item.id)).knowledge_type == "qa"
|
||||
|
||||
|
||||
def test_rollback_switches_published_version_statuses():
|
||||
with _database() as db:
|
||||
item = _knowledge(db)
|
||||
old_version = db.get(KnowledgeVersion, item.current_version_id)
|
||||
snapshot = db.scalar(select(KnowledgeSourceSnapshot).where(KnowledgeSourceSnapshot.knowledge_id == item.id))
|
||||
target = KnowledgeVersion(
|
||||
knowledge_id=item.id,
|
||||
snapshot_id=snapshot.id,
|
||||
version_no=0,
|
||||
status="superseded",
|
||||
quality_status="passed",
|
||||
processing_rule_version="test-v1",
|
||||
)
|
||||
db.add(target)
|
||||
db.flush()
|
||||
KnowledgePipelineService.rollback(db, item, target, admin_id=1, keep_open=True)
|
||||
db.commit()
|
||||
assert item.current_version_id == target.id
|
||||
assert target.status == "published"
|
||||
assert old_version.status == "superseded"
|
||||
|
||||
|
||||
def test_sync_automatically_replaces_current_version_and_keeps_rollback_history():
|
||||
def test_sync_automatically_replaces_current_version_and_removes_history():
|
||||
source_content = (
|
||||
"# 一、课程说明\n这是更新后的课程内容,同步成功后应当立即生效。\n\n"
|
||||
"# 二、练习一:静水流深静心\n选择舒适安全的姿势,觉察呼吸和身体,不适时立即停止。"
|
||||
@@ -174,7 +152,12 @@ def test_sync_automatically_replaces_current_version_and_keeps_rollback_history(
|
||||
|
||||
db.refresh(item)
|
||||
current = db.get(KnowledgeVersion, item.current_version_id)
|
||||
previous = db.get(KnowledgeVersion, previous_version_id)
|
||||
versions = db.scalars(
|
||||
select(KnowledgeVersion).where(KnowledgeVersion.knowledge_id == item.id)
|
||||
).all()
|
||||
snapshots = db.scalars(
|
||||
select(KnowledgeSourceSnapshot).where(KnowledgeSourceSnapshot.knowledge_id == item.id)
|
||||
).all()
|
||||
manifest = db.scalar(select(KnowledgeManifest).where(KnowledgeManifest.version_id == current.id))
|
||||
cards = db.scalars(select(KnowledgeCard).where(KnowledgeCard.version_id == current.id)).all()
|
||||
publish_log = db.scalar(
|
||||
@@ -185,7 +168,9 @@ def test_sync_automatically_replaces_current_version_and_keeps_rollback_history(
|
||||
assert job.mode == "replace"
|
||||
assert current.status == "published"
|
||||
assert current.quality_status == "passed"
|
||||
assert previous.status == "superseded"
|
||||
assert db.get(KnowledgeVersion, previous_version_id) is None
|
||||
assert [version.id for version in versions] == [current.id]
|
||||
assert [snapshot.id for snapshot in snapshots] == [current.snapshot_id]
|
||||
assert item.pending_version_id is None
|
||||
assert item.status == 1
|
||||
assert item.migration_status == "completed"
|
||||
|
||||
Reference in New Issue
Block a user