feat: govern knowledge assets and source citations

This commit is contained in:
key
2026-08-02 01:43:43 +08:00
parent cafb836cda
commit 699cc08050
144 changed files with 17205 additions and 453 deletions
-4
View File
@@ -5,10 +5,6 @@
"ComputedRef": true,
"DirectiveBinding": true,
"EffectScope": true,
"ElLoading": true,
"ElMessage": true,
"ElMessageBox": true,
"ElNotification": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
+1
View File
@@ -77,6 +77,7 @@ export type VectorIndexStatus = {
qdrantDimension?: number;
qdrantPoints?: number;
fragments: number;
ungovernedFragments: number;
embeddedFragments: number;
embeddingModel?: string;
embeddingDimension?: number;
+653
View File
@@ -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',
+9 -1
View File
@@ -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
+43 -14
View File
@@ -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;
}