feat: govern knowledge assets and source citations
This commit is contained in:
@@ -5,10 +5,6 @@
|
||||
"ComputedRef": true,
|
||||
"DirectiveBinding": true,
|
||||
"EffectScope": true,
|
||||
"ElLoading": true,
|
||||
"ElMessage": true,
|
||||
"ElMessageBox": true,
|
||||
"ElNotification": true,
|
||||
"ExtractDefaultPropTypes": true,
|
||||
"ExtractPropTypes": true,
|
||||
"ExtractPublicPropTypes": true,
|
||||
|
||||
@@ -77,6 +77,7 @@ export type VectorIndexStatus = {
|
||||
qdrantDimension?: number;
|
||||
qdrantPoints?: number;
|
||||
fragments: number;
|
||||
ungovernedFragments: number;
|
||||
embeddedFragments: number;
|
||||
embeddingModel?: string;
|
||||
embeddingDimension?: number;
|
||||
|
||||
@@ -82,6 +82,384 @@ export type ProcessingRetryResponse = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type QualityAsset = {
|
||||
id: number;
|
||||
knowledgeId: number;
|
||||
attachmentId?: number;
|
||||
docId: string;
|
||||
sourceName: string;
|
||||
sourceType: string;
|
||||
sourceAuthority: string;
|
||||
sourceVersion: string;
|
||||
dataClass: string;
|
||||
lifecycleStatus: string;
|
||||
indexStatus: string;
|
||||
applicableRegion?: string;
|
||||
applicableProject?: string;
|
||||
applicableRole?: string;
|
||||
effectiveFrom?: string;
|
||||
effectiveTo?: string;
|
||||
currentVersionId: number;
|
||||
semanticAnalysisStatus: 'PENDING' | 'PROCESSING' | 'COMPLETE' | 'NOT_REQUIRED' | 'FAILED' | 'DEAD_LETTER';
|
||||
semanticLastError?: string;
|
||||
semanticRetryCount: number;
|
||||
issueCount: number;
|
||||
blockingIssueCount: number;
|
||||
updateTime: string;
|
||||
};
|
||||
|
||||
export type QualityIssue = {
|
||||
id: number;
|
||||
reasonCode: string;
|
||||
severity: 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL';
|
||||
gateType: 'HARD' | 'SOFT';
|
||||
evidenceJson: string;
|
||||
recommendedAction: string;
|
||||
detectorType: string;
|
||||
detectorVersion: string;
|
||||
status: string;
|
||||
createTime: string;
|
||||
};
|
||||
|
||||
export type QualityPrivacyPreview = {
|
||||
versionId: number;
|
||||
privacyStatus: 'NOT_PROCESSED' | 'CLEAN' | 'REDACTED' | 'BLOCKED';
|
||||
redactionPolicyVersion?: string;
|
||||
redactionSummaryJson?: string;
|
||||
redactedSourceName?: string;
|
||||
redactedContentPreview?: string;
|
||||
};
|
||||
|
||||
export type QualityConflict = {
|
||||
id: number;
|
||||
conflictType: 'POLARITY' | 'VALUE' | 'APPLICABILITY';
|
||||
status: 'PENDING_REVIEW' | 'CONFIRMED' | 'FALSE_POSITIVE' | 'RESOLVED';
|
||||
leftText: string;
|
||||
rightText: string;
|
||||
leftAssetId: number;
|
||||
leftSourceName: string;
|
||||
rightAssetId: number;
|
||||
rightSourceName: string;
|
||||
evidenceJson: string;
|
||||
reviewNote?: string;
|
||||
createTime: string;
|
||||
};
|
||||
|
||||
export type QualityReviewRequest = {
|
||||
reason: string;
|
||||
usageType: string;
|
||||
sourceAuthority: string;
|
||||
sourceVersion: string;
|
||||
applicableRegion?: string;
|
||||
applicableProject?: string;
|
||||
applicableRole?: string;
|
||||
effectiveFrom?: string;
|
||||
effectiveTo?: string;
|
||||
acceptedReasonCodes: string[];
|
||||
supersedesAssetId?: number;
|
||||
};
|
||||
|
||||
export type QualityPublishResult = {
|
||||
assetId: number;
|
||||
versionId: number;
|
||||
lifecycleStatus: string;
|
||||
indexStatus: string;
|
||||
fragments: number;
|
||||
reviewId?: number;
|
||||
};
|
||||
|
||||
export type LegacyStageResult = {
|
||||
discovered: number;
|
||||
staged: number;
|
||||
reparsed: number;
|
||||
quarantined: number;
|
||||
stagedAssetIds: number[];
|
||||
failedAttachmentIds: number[];
|
||||
nextCursor: number;
|
||||
moreAvailable: boolean;
|
||||
};
|
||||
|
||||
export type LegacyStagePreview = {
|
||||
discovered: number;
|
||||
sourceAvailable: number;
|
||||
sourceUnavailable: number;
|
||||
nextCursor: number;
|
||||
moreAvailable: boolean;
|
||||
};
|
||||
|
||||
export type KnowledgeGlossaryTerm = {
|
||||
id: number;
|
||||
term: string;
|
||||
canonicalTerm: string;
|
||||
aliases: string[];
|
||||
definition?: string;
|
||||
applicableRegion?: string;
|
||||
applicableProject?: string;
|
||||
applicableRole?: string;
|
||||
versionNo: number;
|
||||
status: 'DRAFT' | 'ACTIVE' | 'REJECTED' | 'DEPRECATED';
|
||||
reviewerId?: number;
|
||||
reviewReason?: string;
|
||||
reviewedTime?: string;
|
||||
createBy?: number;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
};
|
||||
|
||||
export type KnowledgeGlossaryDraft = {
|
||||
term: string;
|
||||
canonicalTerm: string;
|
||||
aliases: string[];
|
||||
definition?: string;
|
||||
applicableRegion?: string;
|
||||
applicableProject?: string;
|
||||
applicableRole?: string;
|
||||
};
|
||||
|
||||
export type ProcessingRuleStatus = 'DRAFT' | 'SHADOW' | 'PAUSED' | 'RETIRED';
|
||||
|
||||
export type ProcessingRule = {
|
||||
id: number;
|
||||
ruleCode: string;
|
||||
versionNo: number;
|
||||
stage: string;
|
||||
riskClass: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||
action: 'FLAG' | 'REVIEW' | 'QUARANTINE';
|
||||
implementationType: 'DETERMINISTIC' | 'STATISTICAL' | 'LLM_ASSISTED';
|
||||
scope: Record<string, unknown>;
|
||||
config: Record<string, unknown>;
|
||||
status: ProcessingRuleStatus;
|
||||
ownerId: number;
|
||||
supersedesRuleId?: number;
|
||||
shadowStartedTime?: string;
|
||||
retiredTime?: string;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
};
|
||||
|
||||
export type ProcessingRuleDraft = {
|
||||
ruleCode: string;
|
||||
stage: string;
|
||||
riskClass: ProcessingRule['riskClass'];
|
||||
action: ProcessingRule['action'];
|
||||
implementationType: ProcessingRule['implementationType'];
|
||||
scope: Record<string, unknown>;
|
||||
config: Record<string, unknown>;
|
||||
supersedesRuleId?: number;
|
||||
};
|
||||
|
||||
export type ProcessingRuleMetrics = {
|
||||
ruleId: number;
|
||||
sampleCount: number;
|
||||
matchedCount: number;
|
||||
falseAllowCount: number;
|
||||
falseBlockCount: number;
|
||||
agreementRate: number;
|
||||
falseAllowRate: number;
|
||||
falseBlockRate: number;
|
||||
};
|
||||
|
||||
export type RuleReviewSample = {
|
||||
id: number;
|
||||
ruleId: number;
|
||||
sourceEvaluationId?: number;
|
||||
assetId?: number;
|
||||
versionId?: number;
|
||||
sampleKey: string;
|
||||
samplingStrategy: 'RANDOM' | 'RISK_WEIGHTED' | 'MISMATCH' | 'MANUAL';
|
||||
status: 'PENDING' | 'REVIEWED';
|
||||
reviewResult?: 'CONFIRMED' | 'FALSE_ALLOW' | 'FALSE_BLOCK' | 'NOT_APPLICABLE';
|
||||
reviewNote?: string;
|
||||
sampledBy: number;
|
||||
sampledTime: string;
|
||||
reviewedBy?: number;
|
||||
reviewedTime?: string;
|
||||
};
|
||||
|
||||
export type RuleSamplingResult = {
|
||||
runId?: string;
|
||||
candidateCount: number;
|
||||
eligibleCount: number;
|
||||
createdCount: number;
|
||||
mismatchCount: number;
|
||||
};
|
||||
|
||||
export type GoldenDatasetStatus = 'DRAFT' | 'FROZEN' | 'RETIRED';
|
||||
|
||||
export type GoldenDataset = {
|
||||
id: number;
|
||||
datasetCode: string;
|
||||
versionNo: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
status: GoldenDatasetStatus;
|
||||
sampleCount: number;
|
||||
contentHash?: string;
|
||||
ownerId: number;
|
||||
frozenBy?: number;
|
||||
frozenTime?: string;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
};
|
||||
|
||||
export type GoldenDatasetDraft = {
|
||||
datasetCode: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type GoldenSample = {
|
||||
id: number;
|
||||
datasetId: number;
|
||||
sampleKey: string;
|
||||
riskClass: ProcessingRule['riskClass'];
|
||||
expectedDecision: 'PASS' | 'REVIEW' | 'BLOCK';
|
||||
reasonCodes: string[];
|
||||
evidenceRef: string;
|
||||
assetId?: number;
|
||||
versionId?: number;
|
||||
createdBy: number;
|
||||
createTime: string;
|
||||
};
|
||||
|
||||
export type GoldenSampleDraft = {
|
||||
sampleKey: string;
|
||||
riskClass: GoldenSample['riskClass'];
|
||||
expectedDecision: GoldenSample['expectedDecision'];
|
||||
reasonCodes: string[];
|
||||
evidenceRef: string;
|
||||
assetId?: number;
|
||||
versionId?: number;
|
||||
};
|
||||
|
||||
export type AcceptanceProfileStatus = 'DRAFT' | 'FROZEN' | 'RETIRED';
|
||||
|
||||
export type AcceptanceProfile = {
|
||||
id: number;
|
||||
ruleId: number;
|
||||
versionNo: number;
|
||||
riskClass: ProcessingRule['riskClass'];
|
||||
minTotalSamples: number;
|
||||
minGoldenSamples: number;
|
||||
minReviewedSamples: number;
|
||||
minAgreementRate: number;
|
||||
maxFalseAllowRate: number;
|
||||
maxFalseBlockRate: number;
|
||||
minReviewCoverageRate: number;
|
||||
status: AcceptanceProfileStatus;
|
||||
ownerId: number;
|
||||
freezeReason?: string;
|
||||
frozenBy?: number;
|
||||
frozenTime?: string;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
};
|
||||
|
||||
export type AcceptanceProfileDraft = Pick<
|
||||
AcceptanceProfile,
|
||||
| 'minTotalSamples'
|
||||
| 'minGoldenSamples'
|
||||
| 'minReviewedSamples'
|
||||
| 'minAgreementRate'
|
||||
| 'maxFalseAllowRate'
|
||||
| 'maxFalseBlockRate'
|
||||
| 'minReviewCoverageRate'
|
||||
>;
|
||||
|
||||
export type RuleReadiness = {
|
||||
evidenceReady: boolean;
|
||||
enforcementEnabled: boolean;
|
||||
ruleStatus: ProcessingRuleStatus;
|
||||
riskClass: ProcessingRule['riskClass'];
|
||||
profile?: AcceptanceProfile;
|
||||
metrics: {
|
||||
totalSamples: number;
|
||||
goldenSamples: number;
|
||||
matchedSamples: number;
|
||||
falseAllowCount: number;
|
||||
falseBlockCount: number;
|
||||
reviewedSamples: number;
|
||||
pendingMismatchReviews: number;
|
||||
agreementRate: number;
|
||||
falseAllowRate: number;
|
||||
falseBlockRate: number;
|
||||
reviewCoverageRate: number;
|
||||
};
|
||||
reasonCodes: string[];
|
||||
};
|
||||
|
||||
export type PipelineRun = {
|
||||
id: number;
|
||||
runId: string;
|
||||
assetId?: number;
|
||||
inputVersionId?: number;
|
||||
outputVersionId?: number;
|
||||
stage: string;
|
||||
processorName: string;
|
||||
processorVersion: string;
|
||||
triggerType: string;
|
||||
status: 'RUNNING' | 'SUCCEEDED' | 'FAILED' | 'CANCELLED';
|
||||
metrics: Record<string, unknown>;
|
||||
errorCode?: string;
|
||||
errorMessage?: string;
|
||||
requestedBy: number;
|
||||
startedTime: string;
|
||||
endedTime?: string;
|
||||
};
|
||||
|
||||
export type QualityIndexHealth = {
|
||||
pending: number;
|
||||
processing: number;
|
||||
failed: number;
|
||||
deadLetter: number;
|
||||
oldestPendingSeconds: number;
|
||||
staleAssets: number;
|
||||
vectorMatched: boolean;
|
||||
mysqlFragments?: number;
|
||||
ungovernedFragments?: number;
|
||||
qdrantPoints?: number;
|
||||
vectorMessage: string;
|
||||
healthy: boolean;
|
||||
};
|
||||
|
||||
export type QualitySnapshot = {
|
||||
totalAssets: number;
|
||||
reviewPending: number;
|
||||
quarantined: number;
|
||||
published: number;
|
||||
deprecated: number;
|
||||
openHardFindings: number;
|
||||
openSoftFindings: number;
|
||||
semanticDeadLetter: number;
|
||||
pendingDuplicateReviews: number;
|
||||
pendingConflictReviews: number;
|
||||
untraceablePublished: number;
|
||||
expiredPublished: number;
|
||||
publishedOpenFindings: number;
|
||||
publishedMembershipGap: number;
|
||||
publishedFragmentGap: number;
|
||||
successfulQueries24h: number;
|
||||
queriesMissingEvidence24h: number;
|
||||
traceabilityRate: number;
|
||||
queryEvidenceCoverage24h: number;
|
||||
indexHealth: QualityIndexHealth;
|
||||
healthy: boolean;
|
||||
};
|
||||
|
||||
export type QualityAlert = {
|
||||
id: number;
|
||||
alertCode: string;
|
||||
severity: 'WARNING' | 'ERROR' | 'CRITICAL';
|
||||
status: 'OPEN' | 'RESOLVED';
|
||||
message: string;
|
||||
evidenceJson?: string;
|
||||
occurrenceCount: number;
|
||||
firstSeenTime: string;
|
||||
lastSeenTime: string;
|
||||
resolvedTime?: string;
|
||||
detectorVersion: string;
|
||||
};
|
||||
|
||||
export type LocalImportRequest = {
|
||||
directory: string;
|
||||
category: string;
|
||||
@@ -128,6 +506,281 @@ export function retryProcessingTask(id: number): Promise<ApiResult<ProcessingRet
|
||||
});
|
||||
}
|
||||
|
||||
export function listQualityAssets(status = 'REVIEW_PENDING', limit = 100): Promise<ApiResult<QualityAsset[]>> {
|
||||
return request({
|
||||
url: '/api/knowledge/quality/assets',
|
||||
method: 'get',
|
||||
params: { status, limit }
|
||||
});
|
||||
}
|
||||
|
||||
export function listQualityIssues(assetId: number): Promise<ApiResult<QualityIssue[]>> {
|
||||
return request({
|
||||
url: `/api/knowledge/quality/assets/${assetId}/issues`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
export function getQualityPrivacyPreview(assetId: number): Promise<ApiResult<QualityPrivacyPreview>> {
|
||||
return request({
|
||||
url: `/api/knowledge/quality/assets/${assetId}/privacy-preview`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
export function listQualityConflicts(assetId: number): Promise<ApiResult<QualityConflict[]>> {
|
||||
return request({
|
||||
url: `/api/knowledge/quality/assets/${assetId}/conflicts`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
export function reviewQualityConflict(
|
||||
conflictId: number,
|
||||
data: { decision: 'CONFIRMED' | 'FALSE_POSITIVE' | 'RESOLVED'; note: string }
|
||||
): Promise<ApiResult<{ conflictId: number; status: string }>> {
|
||||
return request({
|
||||
url: `/api/knowledge/quality/conflicts/${conflictId}/review`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
export function getQualityIndexHealth(): Promise<ApiResult<QualityIndexHealth>> {
|
||||
return request({
|
||||
url: '/api/knowledge/quality/index-health',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
export function getQualityMetrics(): Promise<ApiResult<QualitySnapshot>> {
|
||||
return request({
|
||||
url: '/api/knowledge/quality/metrics',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
export function listQualityAlerts(includeResolved = false, limit = 100): Promise<ApiResult<QualityAlert[]>> {
|
||||
return request({
|
||||
url: '/api/knowledge/quality/alerts',
|
||||
method: 'get',
|
||||
params: { includeResolved, limit }
|
||||
});
|
||||
}
|
||||
|
||||
export function publishQualityAsset(assetId: number, data: QualityReviewRequest): Promise<ApiResult<QualityPublishResult>> {
|
||||
return request({
|
||||
url: `/api/knowledge/quality/assets/${assetId}/publish`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
export function withdrawQualityAsset(assetId: number, reason: string): Promise<ApiResult<QualityPublishResult>> {
|
||||
return request({
|
||||
url: `/api/knowledge/quality/assets/${assetId}/withdraw`,
|
||||
method: 'post',
|
||||
data: { reason }
|
||||
});
|
||||
}
|
||||
|
||||
export function stageLegacyQualityAssets(limit = 50, afterAttachmentId = 0): Promise<ApiResult<LegacyStageResult>> {
|
||||
return request({
|
||||
url: '/api/knowledge/quality/legacy/stage',
|
||||
method: 'post',
|
||||
params: { limit, afterAttachmentId },
|
||||
headers: { repeatSubmit: false }
|
||||
});
|
||||
}
|
||||
|
||||
export function previewLegacyQualityAssets(limit = 50, afterAttachmentId = 0): Promise<ApiResult<LegacyStagePreview>> {
|
||||
return request({
|
||||
url: '/api/knowledge/quality/legacy/preview',
|
||||
method: 'get',
|
||||
params: { limit, afterAttachmentId }
|
||||
});
|
||||
}
|
||||
|
||||
export function listKnowledgeGlossary(status: KnowledgeGlossaryTerm['status'] = 'ACTIVE', limit = 200): Promise<ApiResult<KnowledgeGlossaryTerm[]>> {
|
||||
return request({
|
||||
url: '/api/knowledge/quality/glossary',
|
||||
method: 'get',
|
||||
params: { status, limit }
|
||||
});
|
||||
}
|
||||
|
||||
export function createKnowledgeGlossaryDraft(data: KnowledgeGlossaryDraft): Promise<ApiResult<KnowledgeGlossaryTerm>> {
|
||||
return request({
|
||||
url: '/api/knowledge/quality/glossary',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
export function approveKnowledgeGlossary(id: number, reason: string): Promise<ApiResult<KnowledgeGlossaryTerm>> {
|
||||
return request({
|
||||
url: `/api/knowledge/quality/glossary/${id}/approve`,
|
||||
method: 'post',
|
||||
data: { reason }
|
||||
});
|
||||
}
|
||||
|
||||
export function deprecateKnowledgeGlossary(id: number, reason: string): Promise<ApiResult<KnowledgeGlossaryTerm>> {
|
||||
return request({
|
||||
url: `/api/knowledge/quality/glossary/${id}/deprecate`,
|
||||
method: 'post',
|
||||
data: { reason }
|
||||
});
|
||||
}
|
||||
|
||||
export function rejectKnowledgeGlossary(id: number, reason: string): Promise<ApiResult<KnowledgeGlossaryTerm>> {
|
||||
return request({
|
||||
url: `/api/knowledge/quality/glossary/${id}/reject`,
|
||||
method: 'post',
|
||||
data: { reason }
|
||||
});
|
||||
}
|
||||
|
||||
export function listProcessingRules(status: ProcessingRuleStatus = 'SHADOW', limit = 100): Promise<ApiResult<ProcessingRule[]>> {
|
||||
return request({
|
||||
url: '/api/knowledge/quality/rules',
|
||||
method: 'get',
|
||||
params: { status, limit }
|
||||
});
|
||||
}
|
||||
|
||||
export function createProcessingRuleDraft(data: ProcessingRuleDraft): Promise<ApiResult<ProcessingRule>> {
|
||||
return request({
|
||||
url: '/api/knowledge/quality/rules',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
export function transitionProcessingRule(ruleId: number, targetStatus: ProcessingRuleStatus, reason: string): Promise<ApiResult<ProcessingRule>> {
|
||||
return request({
|
||||
url: `/api/knowledge/quality/rules/${ruleId}/status`,
|
||||
method: 'post',
|
||||
data: { targetStatus, reason }
|
||||
});
|
||||
}
|
||||
|
||||
export function getProcessingRuleMetrics(ruleId: number): Promise<ApiResult<ProcessingRuleMetrics>> {
|
||||
return request({
|
||||
url: `/api/knowledge/quality/rules/${ruleId}/metrics`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
export function listRuleReviewSamples(
|
||||
ruleId: number,
|
||||
status: RuleReviewSample['status'] = 'PENDING',
|
||||
limit = 100
|
||||
): Promise<ApiResult<RuleReviewSample[]>> {
|
||||
return request({
|
||||
url: `/api/knowledge/quality/rules/${ruleId}/samples`,
|
||||
method: 'get',
|
||||
params: { status, limit }
|
||||
});
|
||||
}
|
||||
|
||||
export function reviewRuleSample(
|
||||
sampleId: number,
|
||||
data: { result: NonNullable<RuleReviewSample['reviewResult']>; note: string }
|
||||
): Promise<ApiResult<RuleReviewSample>> {
|
||||
return request({
|
||||
url: `/api/knowledge/quality/rules/samples/${sampleId}/review`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
export function scheduleRuleReviewSamples(limit = 200): Promise<ApiResult<RuleSamplingResult>> {
|
||||
return request({
|
||||
url: '/api/knowledge/quality/rules/samples/schedule',
|
||||
method: 'post',
|
||||
params: { limit },
|
||||
headers: { repeatSubmit: false }
|
||||
});
|
||||
}
|
||||
|
||||
export function listPipelineRuns(status = 'ALL', limit = 50): Promise<ApiResult<PipelineRun[]>> {
|
||||
return request({
|
||||
url: '/api/knowledge/quality/pipeline-runs',
|
||||
method: 'get',
|
||||
params: { status, limit }
|
||||
});
|
||||
}
|
||||
|
||||
export function listGoldenDatasets(status: GoldenDatasetStatus = 'DRAFT', limit = 100): Promise<ApiResult<GoldenDataset[]>> {
|
||||
return request({
|
||||
url: '/api/knowledge/quality/golden-datasets',
|
||||
method: 'get',
|
||||
params: { status, limit }
|
||||
});
|
||||
}
|
||||
|
||||
export function createGoldenDatasetDraft(data: GoldenDatasetDraft): Promise<ApiResult<GoldenDataset>> {
|
||||
return request({
|
||||
url: '/api/knowledge/quality/golden-datasets',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
export function listGoldenSamples(datasetId: number, limit = 200): Promise<ApiResult<GoldenSample[]>> {
|
||||
return request({
|
||||
url: `/api/knowledge/quality/golden-datasets/${datasetId}/samples`,
|
||||
method: 'get',
|
||||
params: { limit }
|
||||
});
|
||||
}
|
||||
|
||||
export function addGoldenSample(datasetId: number, data: GoldenSampleDraft): Promise<ApiResult<GoldenSample>> {
|
||||
return request({
|
||||
url: `/api/knowledge/quality/golden-datasets/${datasetId}/samples`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
export function freezeGoldenDataset(datasetId: number): Promise<ApiResult<GoldenDataset>> {
|
||||
return request({
|
||||
url: `/api/knowledge/quality/golden-datasets/${datasetId}/freeze`,
|
||||
method: 'post'
|
||||
});
|
||||
}
|
||||
|
||||
export function listAcceptanceProfiles(ruleId: number): Promise<ApiResult<AcceptanceProfile[]>> {
|
||||
return request({
|
||||
url: `/api/knowledge/quality/rules/${ruleId}/acceptance-profiles`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
export function createAcceptanceProfile(ruleId: number, data: AcceptanceProfileDraft): Promise<ApiResult<AcceptanceProfile>> {
|
||||
return request({
|
||||
url: `/api/knowledge/quality/rules/${ruleId}/acceptance-profiles`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
export function freezeAcceptanceProfile(ruleId: number, profileId: number, reason: string): Promise<ApiResult<AcceptanceProfile>> {
|
||||
return request({
|
||||
url: `/api/knowledge/quality/rules/${ruleId}/acceptance-profiles/${profileId}/freeze`,
|
||||
method: 'post',
|
||||
data: { reason }
|
||||
});
|
||||
}
|
||||
|
||||
export function getRuleReadiness(ruleId: number): Promise<ApiResult<RuleReadiness>> {
|
||||
return request({
|
||||
url: `/api/knowledge/quality/rules/${ruleId}/readiness`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
export function importLocalKnowledgeDocs(data: LocalImportRequest): Promise<ApiResult<LocalImportResponse>> {
|
||||
return request({
|
||||
url: '/api/knowledge/doc/import-local',
|
||||
|
||||
@@ -92,7 +92,10 @@ export type AnswerFeedbackItem = {
|
||||
position: string;
|
||||
source: string;
|
||||
reviewId?: number | null;
|
||||
requestId?: string | null;
|
||||
reviewStatus: string;
|
||||
reviewAction?: string | null;
|
||||
reviewNote?: string | null;
|
||||
reviewer?: string | null;
|
||||
reviewedTime?: string | null;
|
||||
createTime: string;
|
||||
@@ -102,6 +105,7 @@ export type AnswerFeedbackItem = {
|
||||
export type AnswerFeedbackReviewResponse = {
|
||||
id: number;
|
||||
reviewStatus: string;
|
||||
reviewAction: 'KEEP' | 'WITHDRAW';
|
||||
reviewer?: string | null;
|
||||
reviewedTime?: string | null;
|
||||
};
|
||||
@@ -194,10 +198,14 @@ export function listAnswerFeedback(status = '待复核', limit = 50): Promise<Ap
|
||||
});
|
||||
}
|
||||
|
||||
export function reviewAnswerFeedback(id: number): Promise<ApiResult<AnswerFeedbackReviewResponse>> {
|
||||
export function reviewAnswerFeedback(
|
||||
id: number,
|
||||
data: { action: 'KEEP' | 'WITHDRAW'; note: string }
|
||||
): Promise<ApiResult<AnswerFeedbackReviewResponse>> {
|
||||
return request({
|
||||
url: `/api/knowledge/answer-feedback/${id}/review`,
|
||||
method: 'post',
|
||||
data,
|
||||
headers: { repeatSubmit: false }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const read = (path: string) => readFileSync(resolve(process.cwd(), path), 'utf8');
|
||||
|
||||
describe('数据质量治理管理端契约', () => {
|
||||
it('历史纳管必须先走只读预览再执行写入', () => {
|
||||
const api = read('src/api/aihr/processing.ts');
|
||||
const page = read('src/views/knowledge/processing.vue');
|
||||
|
||||
expect(api).toContain("url: '/api/knowledge/quality/legacy/preview'");
|
||||
expect(api).toContain("method: 'get'");
|
||||
expect(page).toContain('await previewLegacyQualityAssets(50)');
|
||||
expect(page).toContain('不会自动批准或恢复生产检索');
|
||||
expect(page.indexOf('await previewLegacyQualityAssets(50)')).toBeLessThan(page.indexOf('await stageLegacyQualityAssets(50)'));
|
||||
});
|
||||
|
||||
it('术语采用草稿、人工批准、驳回、修订和停用流程', () => {
|
||||
const api = read('src/api/aihr/processing.ts');
|
||||
const page = read('src/views/knowledge/processing.vue');
|
||||
|
||||
expect(api).toContain('/api/knowledge/quality/glossary/${id}/approve');
|
||||
expect(api).toContain('/api/knowledge/quality/glossary/${id}/reject');
|
||||
expect(api).toContain('/api/knowledge/quality/glossary/${id}/deprecate');
|
||||
expect(page).toContain('人工批准前不会参与检索');
|
||||
expect(page).toContain('活动版本只扩展检索词,不作为回答事实');
|
||||
expect(page).toContain('openGlossaryDraft(row)');
|
||||
});
|
||||
|
||||
it('keeps review publishing disabled until quality details finish loading', () => {
|
||||
const page = read('src/views/knowledge/processing.vue');
|
||||
|
||||
expect(page).toContain('v-loading="qualityDetailLoading"');
|
||||
expect(page).toContain('!qualityDetailLoading && !qualityIssues.length');
|
||||
expect(page).toContain(
|
||||
':disabled="qualityDetailLoading || unresolvedQualityConflicts.length > 0 || !semanticAnalysisComplete || !privacyDerivativeReady"'
|
||||
);
|
||||
expect(page).toContain('if (selectedQualityAsset.value?.id !== asset.id) return;');
|
||||
});
|
||||
|
||||
it('shows only the governed privacy derivative and blocks versions without a residual scan', () => {
|
||||
const api = read('src/api/aihr/processing.ts');
|
||||
const page = read('src/views/knowledge/processing.vue');
|
||||
|
||||
expect(api).toContain('/api/knowledge/quality/assets/${assetId}/privacy-preview');
|
||||
expect(page).toContain('脱敏派生版本');
|
||||
expect(page).toContain('该历史版本尚未生成脱敏派生内容');
|
||||
expect(page).toContain("['CLEAN', 'REDACTED'].includes");
|
||||
expect(page).not.toContain('originalContentPreview');
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -131,7 +131,7 @@
|
||||
:loading="reviewingFeedbackId === row.id"
|
||||
@click="markFeedbackReviewed(row)"
|
||||
>
|
||||
标记已复核
|
||||
处理争议
|
||||
</el-button>
|
||||
<span v-else class="review-done">已复核</span>
|
||||
</template>
|
||||
@@ -174,7 +174,7 @@
|
||||
|
||||
<script setup name="KnowledgeSop" lang="ts">
|
||||
import { DocumentChecked, Reading, Tickets, Upload } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import type { UploadFile } from 'element-plus';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
@@ -309,7 +309,7 @@ const apiPill = computed(() => {
|
||||
const flowStatus = computed(() => {
|
||||
if (trainingGenerated.value) return { label: '已完成闭环', type: 'pill-success' };
|
||||
if (searched.value) return { label: `已命中 ${snippets.value.length} 段`, type: 'pill-primary' };
|
||||
return { label: '可引用 SOP 内容', type: 'pill-success' };
|
||||
return { label: '演示内容预览', type: 'pill-warning' };
|
||||
});
|
||||
const canReview = computed(() => apiMode.value === 'api' && Boolean(apiResult.value?.reviewId));
|
||||
const reviewMessage = computed(() => {
|
||||
@@ -323,7 +323,7 @@ const seedRecords = [
|
||||
title: '投诉处理 SOP v1.0',
|
||||
category: '投诉处理',
|
||||
version: 'v1.0',
|
||||
status: '已入库',
|
||||
status: '演示数据',
|
||||
owner: '品质组',
|
||||
summary: '业主投诉的接待、记录、派单、反馈和闭环确认。',
|
||||
source: 'demo'
|
||||
@@ -332,7 +332,7 @@ const seedRecords = [
|
||||
title: '催缴沟通 SOP v1.0',
|
||||
category: '催缴沟通',
|
||||
version: 'v1.0',
|
||||
status: '已入库',
|
||||
status: '演示数据',
|
||||
owner: '财务组',
|
||||
summary: '缴费提醒、费用争议回应、服务问题承接。',
|
||||
source: 'demo'
|
||||
@@ -423,13 +423,43 @@ async function loadPendingReviews() {
|
||||
}
|
||||
|
||||
async function markFeedbackReviewed(row: AnswerFeedbackItem) {
|
||||
reviewingFeedbackId.value = row.id;
|
||||
try {
|
||||
await reviewAnswerFeedback(row.id);
|
||||
let reviewAction: 'KEEP' | 'WITHDRAW';
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'保留资料表示该来源经核对仍可用;撤回来源会立即停止 MySQL 检索,并排队清除向量。',
|
||||
'处理答案争议',
|
||||
{
|
||||
confirmButtonText: '保留资料',
|
||||
cancelButtonText: '撤回来源',
|
||||
distinguishCancelAndClose: true,
|
||||
type: 'warning'
|
||||
}
|
||||
);
|
||||
reviewAction = 'KEEP';
|
||||
} catch (choice) {
|
||||
if (choice === 'close') return;
|
||||
reviewAction = 'WITHDRAW';
|
||||
}
|
||||
const { value: note } = await ElMessageBox.prompt(
|
||||
'选择“保留资料”表示该来源经核对仍可用;选择“撤回来源”会立即停止 MySQL 检索,并排队清除向量。',
|
||||
reviewAction === 'KEEP' ? '填写保留依据' : '填写撤回原因',
|
||||
{
|
||||
confirmButtonText: '提交复核',
|
||||
cancelButtonText: '取消',
|
||||
inputPlaceholder: '填写复核依据或撤回原因',
|
||||
inputValidator: (input) => Boolean(input?.trim()) || '请填写复核结论'
|
||||
}
|
||||
);
|
||||
reviewingFeedbackId.value = row.id;
|
||||
await reviewAnswerFeedback(row.id, {
|
||||
action: reviewAction,
|
||||
note: note.trim()
|
||||
});
|
||||
pendingReviews.value = pendingReviews.value.filter((item) => item.id !== row.id);
|
||||
ElMessage.success('答案已标记为已复核');
|
||||
} catch {
|
||||
ElMessage.error('答案复核失败');
|
||||
ElMessage.success(reviewAction === 'KEEP' ? '已记录保留依据' : '来源已撤回并进入索引清理队列');
|
||||
} catch (error) {
|
||||
if (error !== 'cancel' && error !== 'close') ElMessage.error('答案复核失败');
|
||||
} finally {
|
||||
reviewingFeedbackId.value = null;
|
||||
}
|
||||
@@ -461,14 +491,13 @@ async function handleDocSelected(uploadFile: UploadFile) {
|
||||
title: data.fileName,
|
||||
category: data.category.replace(' SOP', ''),
|
||||
version: '上传',
|
||||
status: '已入库',
|
||||
status: '待审核',
|
||||
owner: 'MinIO',
|
||||
summary: `OSS #${data.ossId},已解析 ${data.fragments} 个片段`,
|
||||
summary: `OSS #${data.ossId},已生成 ${data.fragments} 个候选片段,批准前不可检索`,
|
||||
source: 'uploaded'
|
||||
});
|
||||
queryText.value = data.snippets[0]?.text.slice(0, 40) || queryText.value;
|
||||
ElMessage.success(`已入库 ${data.fragments} 个片段`);
|
||||
await runSearch();
|
||||
ElMessage.success(`已提交审核,共 ${data.fragments} 个候选片段`);
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user