fix: allow direct knowledge deletion
This commit is contained in:
@@ -310,9 +310,9 @@ async function archive(item: KnowledgeItem) {
|
|||||||
|
|
||||||
async function remove(item: KnowledgeItem) {
|
async function remove(item: KnowledgeItem) {
|
||||||
if (!await confirmAction(
|
if (!await confirmAction(
|
||||||
`将物理删除“${item.name}”及关联数据。该操作不可恢复,并可能影响历史检索记录定位。`,
|
`将永久删除“${item.name}”的知识正文、版本、章节、切片和同步记录,并立即停止 Agent 使用。审计日志仍会保留,该操作不可恢复。`,
|
||||||
"物理删除知识库",
|
"永久删除知识库",
|
||||||
{ confirmButtonText: "确认删除", cancelButtonText: "取消", type: "error" },
|
{ confirmButtonText: "永久删除", cancelButtonText: "取消", type: "error" },
|
||||||
)) return;
|
)) return;
|
||||||
await api.deleteKnowledge(item.id);
|
await api.deleteKnowledge(item.id);
|
||||||
ElMessage.success("知识库已删除");
|
ElMessage.success("知识库已删除");
|
||||||
@@ -402,7 +402,7 @@ async function confirmAction(
|
|||||||
<el-table-column label="当前版本" width="130"><template #default="{ row }"><span>{{ row.currentVersionId ? `#${row.currentVersionId}` : '尚未同步' }}</span></template></el-table-column>
|
<el-table-column label="当前版本" width="130"><template #default="{ row }"><span>{{ row.currentVersionId ? `#${row.currentVersionId}` : '尚未同步' }}</span></template></el-table-column>
|
||||||
<el-table-column label="状态" width="210"><template #default="{ row }"><div class="knowledge-status-cell"><el-switch v-model="row.status" :active-value="1" :inactive-value="0" active-text="开放" inactive-text="关闭" inline-prompt :loading="operatingId === row.id" :disabled="row.lifecycleStatus === 'archived'" :before-change="() => toggleOpen(row)" /><el-tag :type="statusType(row.sourceStatus)">{{ row.sourceStatus === 'normal' ? '源正常' : '源异常' }}</el-tag></div></template></el-table-column>
|
<el-table-column label="状态" width="210"><template #default="{ row }"><div class="knowledge-status-cell"><el-switch v-model="row.status" :active-value="1" :inactive-value="0" active-text="开放" inactive-text="关闭" inline-prompt :loading="operatingId === row.id" :disabled="row.lifecycleStatus === 'archived'" :before-change="() => toggleOpen(row)" /><el-tag :type="statusType(row.sourceStatus)">{{ row.sourceStatus === 'normal' ? '源正常' : '源异常' }}</el-tag></div></template></el-table-column>
|
||||||
<el-table-column prop="lastSyncedAt" label="最近同步" width="180" />
|
<el-table-column prop="lastSyncedAt" label="最近同步" width="180" />
|
||||||
<el-table-column label="操作" width="220" fixed="right"><template #default="{ row }"><TableRowActions><el-button link type="primary" @click="openDetail(row)">详情</el-button><el-button link type="primary" :disabled="row.lifecycleStatus === 'archived'" :loading="operatingId === row.id" @click="sync(row)">同步最新版</el-button><el-dropdown trigger="click" :teleported="true" @command="handleMore($event, row)"><el-button link type="primary">更多</el-button><template #dropdown><el-dropdown-menu><el-dropdown-item command="detail">查看详情</el-dropdown-item><el-dropdown-item command="preview">进入 Agent 预览</el-dropdown-item><el-dropdown-item v-if="row.lifecycleStatus === 'active'" command="archive" divided>归档</el-dropdown-item><el-dropdown-item v-else command="restore" divided>恢复</el-dropdown-item><el-dropdown-item command="delete" :disabled="row.lifecycleStatus !== 'archived'" divided>删除</el-dropdown-item></el-dropdown-menu></template></el-dropdown></TableRowActions></template></el-table-column>
|
<el-table-column label="操作" width="220" fixed="right"><template #default="{ row }"><TableRowActions><el-button link type="primary" @click="openDetail(row)">详情</el-button><el-button link type="primary" :disabled="row.lifecycleStatus === 'archived'" :loading="operatingId === row.id" @click="sync(row)">同步最新版</el-button><el-dropdown trigger="click" :teleported="true" @command="handleMore($event, row)"><el-button link type="primary">更多</el-button><template #dropdown><el-dropdown-menu><el-dropdown-item command="detail">查看详情</el-dropdown-item><el-dropdown-item command="preview">进入 Agent 预览</el-dropdown-item><el-dropdown-item v-if="row.lifecycleStatus === 'active'" command="archive" divided>归档</el-dropdown-item><el-dropdown-item v-else command="restore" divided>恢复</el-dropdown-item><el-dropdown-item command="delete" divided>永久删除</el-dropdown-item></el-dropdown-menu></template></el-dropdown></TableRowActions></template></el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<AdminPagination :page="pager.page" :page-size="pager.pageSize" :total="pager.total" @change="load" />
|
<AdminPagination :page="pager.page" :page-size="pager.pageSize" :total="pager.total" @change="load" />
|
||||||
|
|
||||||
|
|||||||
@@ -192,12 +192,13 @@ def delete_knowledge(
|
|||||||
) -> dict:
|
) -> dict:
|
||||||
_require_super_admin(current_admin)
|
_require_super_admin(current_admin)
|
||||||
knowledge = _get_knowledge(db, knowledge_id)
|
knowledge = _get_knowledge(db, knowledge_id)
|
||||||
if knowledge.lifecycle_status != "archived" or knowledge.status != 0:
|
# 删除操作本身会立即停止开放并清理正式内容,不再要求管理员先执行“关闭、归档”两步。
|
||||||
raise HTTPException(
|
knowledge.status = 0
|
||||||
status_code=status.HTTP_409_CONFLICT,
|
knowledge.lifecycle_status = "archived"
|
||||||
detail="物理删除前必须先关闭并归档知识库",
|
db.add(knowledge)
|
||||||
)
|
db.flush()
|
||||||
# 清理早期按用户授权口径留下的历史关联,避免外键阻止知识库删除。
|
|
||||||
|
# 清理按用户授权口径留下的历史关联,避免外键阻止知识库删除。
|
||||||
permissions = db.scalars(
|
permissions = db.scalars(
|
||||||
select(UserKnowledgePermission).where(UserKnowledgePermission.knowledge_id == knowledge.id)
|
select(UserKnowledgePermission).where(UserKnowledgePermission.knowledge_id == knowledge.id)
|
||||||
).all()
|
).all()
|
||||||
@@ -212,9 +213,16 @@ def delete_knowledge(
|
|||||||
db.flush()
|
db.flush()
|
||||||
for row in db.scalars(select(KnowledgeSourceSnapshot).where(KnowledgeSourceSnapshot.knowledge_id == knowledge.id)).all():
|
for row in db.scalars(select(KnowledgeSourceSnapshot).where(KnowledgeSourceSnapshot.knowledge_id == knowledge.id)).all():
|
||||||
db.delete(row)
|
db.delete(row)
|
||||||
db.delete(knowledge)
|
|
||||||
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="delete", target_id=knowledge.id)
|
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="delete", target_id=knowledge.id)
|
||||||
|
db.delete(knowledge)
|
||||||
|
try:
|
||||||
db.commit()
|
db.commit()
|
||||||
|
except IntegrityError as exc:
|
||||||
|
db.rollback()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail="知识库仍有关联数据,删除未执行,请刷新后重试",
|
||||||
|
) from exc
|
||||||
return api_success()
|
return api_success()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from sqlalchemy import create_engine, select
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
|
from app.api.admin_knowledge import delete_knowledge
|
||||||
from app.api.admin_knowledge_lifecycle import batch_sync, batch_update_metadata, update_metadata
|
from app.api.admin_knowledge_lifecycle import batch_sync, batch_update_metadata, update_metadata
|
||||||
from app.models import Base
|
from app.models import Base
|
||||||
from app.models.admin import Admin, Role
|
from app.models.admin import Admin, Role
|
||||||
@@ -134,6 +135,20 @@ def test_batch_metadata_update_reports_missing_items():
|
|||||||
assert db.scalar(select(Knowledge).where(Knowledge.id == item.id)).knowledge_type == "qa"
|
assert db.scalar(select(Knowledge).where(Knowledge.id == item.id)).knowledge_type == "qa"
|
||||||
|
|
||||||
|
|
||||||
|
def test_active_knowledge_can_be_deleted_without_manual_archive_steps():
|
||||||
|
with _database() as db:
|
||||||
|
item = _knowledge(db)
|
||||||
|
knowledge_id = item.id
|
||||||
|
|
||||||
|
response = delete_knowledge(knowledge_id, db, _admin())
|
||||||
|
|
||||||
|
assert response["code"] == 0
|
||||||
|
assert db.get(Knowledge, knowledge_id) is None
|
||||||
|
assert db.scalars(select(KnowledgeVersion).where(KnowledgeVersion.knowledge_id == knowledge_id)).all() == []
|
||||||
|
assert db.scalars(select(KnowledgeManifest).where(KnowledgeManifest.knowledge_id == knowledge_id)).all() == []
|
||||||
|
assert db.scalars(select(KnowledgeSourceSnapshot).where(KnowledgeSourceSnapshot.knowledge_id == knowledge_id)).all() == []
|
||||||
|
|
||||||
|
|
||||||
def test_batch_sync_continues_after_an_item_fails_and_reports_each_result():
|
def test_batch_sync_continues_after_an_item_fails_and_reports_each_result():
|
||||||
with _database() as db:
|
with _database() as db:
|
||||||
first = _knowledge(db, 1)
|
first = _knowledge(db, 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user