fix(knowledge): audit impact preview owners
This commit is contained in:
parent
8c1696c59a
commit
bc63a35e88
@ -148,7 +148,7 @@ fork yudao-cloud:保留 `system/infra/framework/gateway/member/pay/bpm/report/mp
|
||||
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|---|:---:|---|
|
||||
| **AI 编排** (muse-module-ai) | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | **72%** | 未实跑/可编译 | 较好有偏差 | high | ADR 级完整包隔离仍未完成(已有最小 ArchUnit 守卫);前端全 MSW,SSE 疑漂移;真实 New-API 未端到端验收 |
|
||||
| **作品/编辑器** (muse-module-content) | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | **82%** | 未实跑/可编译 | 高度一致 | high | **前端 Accept Suggestion 断链**:后端齐全但 studio 未接 suggestion-merges,AI 候选进正文用户端不可用 |
|
||||
| **知识库** (muse-module-knowledge) | ✅ | ⚠ | ✅ | ⚠ | ✅ | ✅ | **72%** | 未实跑/可编译 | 高度一致 | high | 前端 2/3 面缺失(草稿确认/绑定/发布/图谱无 hook);Task4 跨 BC owner 未实现;recheck/worker 默认未生效 |
|
||||
| **知识库** (muse-module-knowledge) | ✅ | ⚠ | ✅ | ⚠ | ✅ | ✅ | **72%** | 未实跑/可编译 | 高度一致 | high | 前端 2/3 面缺失(草稿确认/绑定/发布/图谱无 hook);Task4 installed/source-binding owner 计数已按投影事实审计,document/export owner 仍后置;recheck/worker 默认未生效 |
|
||||
| **市场** (muse-module-market) | ✅ | ⚠ | ✅ | ⚠ | ✅ | ✅ | **82%** | 未实跑/可编译 | 高度一致 | high | studio 仅消费侧(浏览/授权/安装);**生产侧发布/上架/申诉无 UI,飞轮 UI 层断裂** |
|
||||
| **元治理/MetaSchema** (muse-module-meta) | ✅ | ❌ | ✅ | ⚠ | ✅ | ✅ | **82%** | 未实跑/可编译 | 高度一致 | high | **impact-preview facade 仅 Unavailable→写链路运行期端到端阻断**;facade-api 缺失,跨 BC 投影消费契约未建立 |
|
||||
| **事件/SSE** (muse-module-events) | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | **88%** | 未实跑/可编译 | 后端一致/前端漂移 | high | 前端 connectAIStream 契约漂移(被 mock 掩盖,真实联调会失败);统一事件客户端为孤儿无 UI 消费 |
|
||||
@ -237,7 +237,7 @@ fork yudao-cloud:保留 `system/infra/framework/gateway/member/pay/bpm/report/mp
|
||||
**实现摘要:** 本仓最完整业务模块之一。**59 端点(app 36 + admin 23)与契约 48 path/59 method 精确对齐**;service 共 6443 行(DocumentService 1068/KnowledgeBaseService 951/DraftService 646)。`confirmKnowledgeDraft` 真把 Shadow 草稿物化为 Canonical 实体/关系(entityMapper/relationMapper.insert),带冲突检测、决策审计、commandId 幂等、sourceVersion 追踪。RAGFlow 是真 HTTP:`HttpRagFlowKnowledgeRuntimeClient`(621 行)调 `/api/v1/datasets`、`/api/v1/retrieval`、`/run_graphrag`、`/knowledge_graph`,Bearer + SSRF 防护 + 超时 + 18 类 FailureClass,并有 `Unavailable*` 优雅降级(ADR-006)。绑定预检存 sourceSnapshotId/authorizationSnapshotId/handoffHash;发布 readiness/snapshot;source-event 传播;outbox worker。DB:V5(7 表)+ V14(18 表)+ V18 ≈ 26 张。测试 38 文件 209 @Test,含真实 RAGFlow 验收 IT。前端 `useKnowledge.ts`(405 行)真调 `/knowledge-bases`、`/installed-knowledge-bases`、`/documents`,但仅覆盖约 1/3 面。
|
||||
|
||||
**桩与缺口:**
|
||||
- **跨 BC owner 计数未实现(Task 4):** installed KB owner / document owner count / export_task_owner 以 'unsupported'/'not implemented in Task 4' 占位返回。
|
||||
- **Task 4 owner 计数仍有边界:** installed/source-binding owner 已按 `muse_knowledge_source_binding_projection.owner_user_id` 输出审计计数;document owner count / export_task_owner 仍以 unsupported/fail-closed 后置,不能伪造跨 BC owner 事实。
|
||||
- **recheck 仅记状态**,未做外部来源真实重校验('external source validation is not configured')。
|
||||
- linkUrl 材料化 fail-closed(无 SSRF allowlist/隔离抓取前一律拒绝),功能未通。
|
||||
- **outbox 发布 worker 默认关闭**,启用需运维配置。
|
||||
@ -254,7 +254,7 @@ fork yudao-cloud:保留 `system/infra/framework/gateway/member/pay/bpm/report/mp
|
||||
- `MuseKnowledgeDraftService.java:194`(recheck 'external source validation is not configured')
|
||||
- `HttpRagFlowKnowledgeRuntimeClient.java:1-621`(真 HttpClient 调四端点 + Bearer)
|
||||
- `UnavailableRagFlowKnowledgeRuntimeClient.java:9-60`(ADR-006 降级)
|
||||
- `MuseKnowledgeBaseService.java:221-248`(Task 4 scoped 缺口占位)
|
||||
- `MuseKnowledgeBaseService.java:211-248`(impact preview 聚合 binding/projection/task/policy 事实;document/export owner 仍 scoped 后置)
|
||||
- `MuseKnowledgeGraphQueryService.java:41-69`(读 Canonical 投影非实时图)
|
||||
- `sql/muse/V5/V14/V18`(7+18+outbox 表)
|
||||
- `muse-studio/src/pages/KnowledgePage.tsx:18,53,266`(仅 KB list + MaterialManager + CreateKBModal;drafts/bindings/publish/graph 无 hook)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -5,5 +5,5 @@
|
||||
- **目标(owner 职责)**:Local KB / User KB / 全局知识处理 / Knowledge Draft / Knowledge Source Binding / 投影与索引状态。
|
||||
- **边界(不可违反)**:**进 Local KB 唯一入口=用户显式确认草稿或手动修正**;全书解析章节确认只产/推 Draft,不写正式知识;来源优先级 Local KB > 用户绑定 > 市场/全局;依赖 RAGFlow GraphRAG(不可用时 Unavailable 优雅降级);守 BC 边界不碰他域 `.dal`([bc-boundaries](../../.agents/rules/bc-boundaries.md))。
|
||||
- **out-of-scope**:正文写入(归 content)。
|
||||
- **现状**:只读评估 72%;后端最完整模块之一(59 端点对齐契约),RAGFlow 真 HTTP + SSRF 防护;installed-KB 删除记录回显 bug 已修(2026-06-19:`MuseKnowledgeBindingMapperTest` + `MuseInstalledKnowledgeBaseServiceTest` 10/10,删除时写 `deleted=true` 且列表排除 `binding_status=deleted`)。
|
||||
- **关键风险 / TODO**:前端仅约 1/3 面(草稿确认/绑定/发布/图谱缺 hook);**Task4 跨 BC owner 计数未实现**(占位 'not implemented');recheck/worker 默认未生效。
|
||||
- **现状**:只读评估 72%;后端最完整模块之一(59 端点对齐契约),RAGFlow 真 HTTP + SSRF 防护;installed-KB 删除记录回显 bug 已修(2026-06-19:`MuseKnowledgeBindingMapperTest` + `MuseInstalledKnowledgeBaseServiceTest` 10/10,删除时写 `deleted=true` 且列表排除 `binding_status=deleted`);impact preview 已按 `muse_knowledge_source_binding_projection.owner_user_id` 输出 installed/source-binding owner 审计计数(2026-06-19:`MuseKnowledgeBaseServiceTest` + `MuseKnowledgeSourceBindingProjectionMapperTest` 30/30)。
|
||||
- **关键风险 / TODO**:前端仅约 1/3 面(草稿确认/绑定/发布/图谱缺 hook);Task4 剩余 owner 缺口已收窄为 **document owner count / export_task_owner** 仍不能伪造,需后续 owner 事实或外部服务接入;recheck/worker 默认未生效。
|
||||
|
||||
@ -211,15 +211,20 @@ public class MuseKnowledgeBaseService {
|
||||
preview.setStatus("ready");
|
||||
long affectedBindings = knowledgeBindingMapper.countByKbId(kbId);
|
||||
long affectedInstalls = sourceBindingProjectionMapper.countByKbId(kbId);
|
||||
MuseKnowledgeSourceBindingProjectionMapper.FanoutOwnerAuditSummary ownerAuditSummary =
|
||||
sourceBindingProjectionMapper.selectOwnerAuditSummaryByKbId(kbId);
|
||||
long affectedWorks = knowledgeBindingMapper.countDistinctWorksByKbId(kbId);
|
||||
long runningTasks = processingTaskMapper.countRunningByKbId(kbId);
|
||||
long activePolicies = accessPolicyMapper.countActiveByKbId(kbId);
|
||||
preview.setImpactSummary(JsonUtils.toJsonString(impactSummary(reqVO.getActionType(), reqVO.getTargetVersion(),
|
||||
draftId, affectedBindings, affectedInstalls, affectedWorks, runningTasks, activePolicies)));
|
||||
draftId, affectedBindings, affectedInstalls, affectedWorks, runningTasks, activePolicies,
|
||||
ownerAuditSummary)));
|
||||
preview.setRiskSummary(JsonUtils.toJsonString(Map.of(
|
||||
"riskSummary", riskSummary(affectedBindings, affectedInstalls, runningTasks),
|
||||
"unsupported", List.of("unsupported_no_installed_kb_owner", "unsupported_no_export_task_owner"),
|
||||
"unknown", List.of("installed knowledge base ownership is not implemented in Task 4"))));
|
||||
"ownerAuditSummary", ownerAuditSummary(ownerAuditSummary),
|
||||
"unsupported", List.of("unsupported_no_export_task_owner"),
|
||||
"unknown", ownerAuditSummary.invalidOwnerProjectionCount() > 0
|
||||
? List.of("invalid_installed_kb_owner_projection") : List.of())));
|
||||
preview.setExpiresAt(LocalDateTime.now().plusMinutes(30));
|
||||
preview.setTenantId(TenantContextHolder.getRequiredTenantId());
|
||||
impactPreviewMapper.insert(preview);
|
||||
@ -777,7 +782,9 @@ public class MuseKnowledgeBaseService {
|
||||
|
||||
private Map<String, Object> impactSummary(String actionType, Integer targetVersion, String draftId,
|
||||
long affectedBindings, long affectedInstalls, long affectedWorks,
|
||||
long runningTasks, long activePolicies) {
|
||||
long runningTasks, long activePolicies,
|
||||
MuseKnowledgeSourceBindingProjectionMapper.FanoutOwnerAuditSummary
|
||||
ownerAuditSummary) {
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("actionType", actionType);
|
||||
summary.put("targetVersion", targetVersion);
|
||||
@ -792,11 +799,23 @@ public class MuseKnowledgeBaseService {
|
||||
summary.put("affectedInstalls", affectedInstalls);
|
||||
summary.put("affectedWorks", affectedWorks);
|
||||
summary.put("runningTasks", runningTasks);
|
||||
summary.put("unknown", List.of("installed_kb_owner"));
|
||||
// 安装来源 owner 只来自 Knowledge 投影表,不跨 BC 查询或补造 owner 事实。
|
||||
summary.put("ownerAuditSummary", ownerAuditSummary(ownerAuditSummary));
|
||||
summary.put("unknown", ownerAuditSummary.invalidOwnerProjectionCount() > 0
|
||||
? List.of("invalid_installed_kb_owner_projection") : List.of());
|
||||
summary.put("unsupported", List.of("export_task_owner"));
|
||||
return summary;
|
||||
}
|
||||
|
||||
private Map<String, Object> ownerAuditSummary(
|
||||
MuseKnowledgeSourceBindingProjectionMapper.FanoutOwnerAuditSummary summary) {
|
||||
return Map.of(
|
||||
"totalProjectionCount", summary.totalProjectionCount(),
|
||||
"validOwnerProjectionCount", summary.validOwnerProjectionCount(),
|
||||
"invalidOwnerProjectionCount", summary.invalidOwnerProjectionCount(),
|
||||
"distinctTargetOwnerCount", summary.distinctTargetOwnerCount());
|
||||
}
|
||||
|
||||
private String policyImpact(String actionType) {
|
||||
return ACTION_POLICY_PUBLISH.equals(actionType) ? "policy_will_change" : "unchanged";
|
||||
}
|
||||
|
||||
@ -78,6 +78,24 @@ public interface MuseKnowledgeSourceBindingProjectionMapper extends BaseMapperX<
|
||||
return new FanoutOwnerAuditSummary(total, valid, total - valid, distinctOwners);
|
||||
}
|
||||
|
||||
default FanoutOwnerAuditSummary selectOwnerAuditSummaryByKbId(Long kbId) {
|
||||
List<MuseKnowledgeSourceBindingProjectionDO> projections = selectList(
|
||||
new LambdaQueryWrapperX<MuseKnowledgeSourceBindingProjectionDO>()
|
||||
.eq(MuseKnowledgeSourceBindingProjectionDO::getKbId, kbId)
|
||||
.eq(MuseKnowledgeSourceBindingProjectionDO::getDeleted, false));
|
||||
// owner_user_id 是 Knowledge 本域读模型已落库事实;只统计有效正数 owner,不跨 BC 猜测归属。
|
||||
long total = projections.size();
|
||||
long valid = projections.stream()
|
||||
.filter(projection -> projection.getOwnerUserId() != null && projection.getOwnerUserId() > 0)
|
||||
.count();
|
||||
long distinctOwners = projections.stream()
|
||||
.map(MuseKnowledgeSourceBindingProjectionDO::getOwnerUserId)
|
||||
.filter(ownerUserId -> ownerUserId != null && ownerUserId > 0)
|
||||
.distinct()
|
||||
.count();
|
||||
return new FanoutOwnerAuditSummary(total, valid, total - valid, distinctOwners);
|
||||
}
|
||||
|
||||
default int updateStatusByKbId(Long kbId, String status, String actionPolicy, String lastEventId) {
|
||||
return updateStatusByKbId(kbId, status, actionPolicy, lastEventId, null, null);
|
||||
}
|
||||
|
||||
@ -348,6 +348,8 @@ class MuseKnowledgeBaseServiceTest extends BaseMockitoUnitTest {
|
||||
when(knowledgeBindingMapper.countByKbId(4001L)).thenReturn(2L);
|
||||
when(knowledgeBindingMapper.countDistinctWorksByKbId(4001L)).thenReturn(2L);
|
||||
when(sourceBindingProjectionMapper.countByKbId(4001L)).thenReturn(1L);
|
||||
when(sourceBindingProjectionMapper.selectOwnerAuditSummaryByKbId(4001L))
|
||||
.thenReturn(new MuseKnowledgeSourceBindingProjectionMapper.FanoutOwnerAuditSummary(4L, 3L, 1L, 2L));
|
||||
when(processingTaskMapper.countRunningByKbId(4001L)).thenReturn(3L);
|
||||
when(accessPolicyMapper.countActiveByKbId(4001L)).thenReturn(1L);
|
||||
|
||||
@ -367,7 +369,10 @@ class MuseKnowledgeBaseServiceTest extends BaseMockitoUnitTest {
|
||||
&& inserted.getImpactSummary().contains("\"affectedInstalls\":1")
|
||||
&& inserted.getImpactSummary().contains("\"runningTasks\":3")
|
||||
&& inserted.getImpactSummary().contains("\"authorization\"")
|
||||
&& inserted.getRiskSummary().contains("unsupported_no_installed_kb_owner")));
|
||||
&& inserted.getImpactSummary().contains("\"validOwnerProjectionCount\":3")
|
||||
&& inserted.getRiskSummary().contains("\"distinctTargetOwnerCount\":2")
|
||||
&& inserted.getRiskSummary().contains("invalid_installed_kb_owner_projection")
|
||||
&& !inserted.getRiskSummary().contains("unsupported_no_installed_kb_owner")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -378,6 +383,8 @@ class MuseKnowledgeBaseServiceTest extends BaseMockitoUnitTest {
|
||||
when(knowledgeBindingMapper.countByKbId(4001L)).thenReturn(2L);
|
||||
when(knowledgeBindingMapper.countDistinctWorksByKbId(4001L)).thenReturn(2L);
|
||||
when(sourceBindingProjectionMapper.countByKbId(4001L)).thenReturn(1L);
|
||||
when(sourceBindingProjectionMapper.selectOwnerAuditSummaryByKbId(4001L))
|
||||
.thenReturn(new MuseKnowledgeSourceBindingProjectionMapper.FanoutOwnerAuditSummary(1L, 1L, 0L, 1L));
|
||||
when(processingTaskMapper.countRunningByKbId(4001L)).thenReturn(0L);
|
||||
when(accessPolicyMapper.countActiveByKbId(4001L)).thenReturn(1L);
|
||||
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
package cn.iocoder.muse.module.knowledge.dal.mysql.muse;
|
||||
|
||||
import cn.iocoder.muse.module.knowledge.dal.dataobject.muse.MuseKnowledgeSourceBindingProjectionDO;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Knowledge 来源绑定投影 Mapper 语义测试。
|
||||
*/
|
||||
class MuseKnowledgeSourceBindingProjectionMapperTest {
|
||||
|
||||
@BeforeAll
|
||||
static void initMybatisPlusTableInfo() {
|
||||
// 轻量 Mapper 测试不启动 Spring,需要先初始化 Lambda 字段映射缓存。
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
|
||||
MuseKnowledgeSourceBindingProjectionDO.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_countOwnerAuditSummaryByKbIdFromProjectionFacts() {
|
||||
MuseKnowledgeSourceBindingProjectionMapper mapper =
|
||||
mock(MuseKnowledgeSourceBindingProjectionMapper.class, CALLS_REAL_METHODS);
|
||||
doReturn(List.of(
|
||||
projection(2001L),
|
||||
projection(2001L),
|
||||
projection(2002L),
|
||||
projection(0L),
|
||||
projection(null)))
|
||||
.when(mapper).selectList(org.mockito.ArgumentMatchers.<Wrapper<MuseKnowledgeSourceBindingProjectionDO>>any());
|
||||
|
||||
MuseKnowledgeSourceBindingProjectionMapper.FanoutOwnerAuditSummary summary =
|
||||
mapper.selectOwnerAuditSummaryByKbId(4001L);
|
||||
|
||||
assertEquals(5L, summary.totalProjectionCount());
|
||||
assertEquals(3L, summary.validOwnerProjectionCount());
|
||||
assertEquals(2L, summary.invalidOwnerProjectionCount());
|
||||
assertEquals(2L, summary.distinctTargetOwnerCount());
|
||||
ArgumentCaptor<Wrapper<MuseKnowledgeSourceBindingProjectionDO>> captor = ArgumentCaptor.forClass(Wrapper.class);
|
||||
verify(mapper).selectList(captor.capture());
|
||||
String sqlSegment = normalizeSql(captor.getValue());
|
||||
assertTrue(sqlSegment.contains("kb_id ="));
|
||||
assertTrue(sqlSegment.contains("deleted ="));
|
||||
assertTrue(((AbstractWrapper<?, ?, ?>) captor.getValue()).getParamNameValuePairs().containsValue(4001L));
|
||||
assertTrue(((AbstractWrapper<?, ?, ?>) captor.getValue()).getParamNameValuePairs().containsValue(false));
|
||||
}
|
||||
|
||||
private static MuseKnowledgeSourceBindingProjectionDO projection(Long ownerUserId) {
|
||||
MuseKnowledgeSourceBindingProjectionDO projection = new MuseKnowledgeSourceBindingProjectionDO();
|
||||
projection.setOwnerUserId(ownerUserId);
|
||||
return projection;
|
||||
}
|
||||
|
||||
private static String normalizeSql(Wrapper<MuseKnowledgeSourceBindingProjectionDO> wrapper) {
|
||||
return wrapper.getSqlSegment().replaceAll("\\s+", " ").toLowerCase(Locale.ROOT).trim();
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user