AIA V2.0 Major Updates: - Add StreamingService with OpenAI Compatible format (backend/common/streaming) - Upgrade Chat component V2 with Ant Design X deep integration - Implement 12 intelligent agents (5 phases: topic/design/review/data/writing) - Create AgentHub with 100% prototype V11 restoration - Create ChatWorkspace with fullscreen immersive experience - Add ThinkingBlock for deep thinking display - Add useAIStream Hook for stream handling - Add ConversationList for conversation management Backend (~1300 lines): - common/streaming: OpenAI adapter and streaming service - modules/aia: 12 agents config, conversation service, attachment service - Unified API routes to /api/v1 (RVW, PKB, AIA modules) - Update authentication and permission helpers Frontend (~3500 lines): - modules/aia: AgentHub + ChatWorkspace + AgentCard components - shared/Chat: AIStreamChat, ThinkingBlock, useAIStream, useConversations - Update all modules API endpoints to v1 - Modern design with theme colors (blue/yellow/teal/purple) Documentation (~2500 lines): - AIA module status and development guide - Universal capabilities catalog (11 services) - Quick reference card - System overview updates - All module documentation synchronization Other Updates: - DC Tool C: Python operations and frontend components - IIT Manager: session memory and wechat service - PKB/RVW/ASL: API route updates - Docker configs and deployment scripts - Database migrations and scripts - Test files and documentation Tested: AIA streaming verified, authentication working, core features functional Status: AIA V2.0 completed (85%), all changes synchronized
276 lines
5.6 KiB
TypeScript
276 lines
5.6 KiB
TypeScript
/**
|
||
* DC模块 - 冲突检测服务
|
||
*
|
||
* 功能:
|
||
* - 比较双模型提取结果
|
||
* - 标记冲突字段
|
||
* - 计算冲突严重程度
|
||
* - 生成冲突报告
|
||
*
|
||
* 平台能力复用:
|
||
* - ✅ logger: 日志记录
|
||
*/
|
||
|
||
import { logger } from '../../../../common/logging/index.js';
|
||
|
||
export interface ConflictResult {
|
||
hasConflict: boolean;
|
||
conflictFields: string[];
|
||
conflictDetails: Array<{
|
||
fieldName: string;
|
||
valueA: string;
|
||
valueB: string;
|
||
similarity: number; // 0-1, 相似度
|
||
}>;
|
||
severity: 'low' | 'medium' | 'high';
|
||
}
|
||
|
||
export class ConflictDetectionService {
|
||
/**
|
||
* 检测冲突
|
||
*
|
||
* @param resultA DeepSeek结果
|
||
* @param resultB Qwen结果
|
||
* @returns 冲突分析结果
|
||
*/
|
||
detectConflict(resultA: Record<string, string>, resultB: Record<string, string>): ConflictResult {
|
||
try {
|
||
logger.info('[Conflict] Starting conflict detection');
|
||
|
||
const conflictFields: string[] = [];
|
||
const conflictDetails: ConflictResult['conflictDetails'] = [];
|
||
|
||
// 获取所有字段
|
||
const allFields = new Set([...Object.keys(resultA), ...Object.keys(resultB)]);
|
||
|
||
// 逐字段比较
|
||
for (const field of allFields) {
|
||
const valueA = resultA[field] || '';
|
||
const valueB = resultB[field] || '';
|
||
|
||
// 归一化后比较
|
||
const normalizedA = this.normalize(valueA);
|
||
const normalizedB = this.normalize(valueB);
|
||
|
||
if (normalizedA !== normalizedB) {
|
||
// 检测到冲突
|
||
const similarity = this.calculateSimilarity(normalizedA, normalizedB);
|
||
|
||
conflictFields.push(field);
|
||
conflictDetails.push({
|
||
fieldName: field,
|
||
valueA,
|
||
valueB,
|
||
similarity
|
||
});
|
||
}
|
||
}
|
||
|
||
// 计算严重程度
|
||
const severity = this.calculateSeverity(conflictFields.length, allFields.size);
|
||
|
||
const result: ConflictResult = {
|
||
hasConflict: conflictFields.length > 0,
|
||
conflictFields,
|
||
conflictDetails,
|
||
severity
|
||
};
|
||
|
||
logger.info('[Conflict] Detection completed', {
|
||
hasConflict: result.hasConflict,
|
||
conflictCount: conflictFields.length,
|
||
severity
|
||
});
|
||
|
||
return result;
|
||
|
||
} catch (error) {
|
||
logger.error('[Conflict] Detection failed', { error });
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 归一化文本
|
||
*
|
||
* - 去除空格
|
||
* - 转小写
|
||
* - 半角化
|
||
* - 数值归一化(3cm = 3.0cm = 3 cm)
|
||
*/
|
||
private normalize(value: string): string {
|
||
let normalized = String(value)
|
||
.toLowerCase()
|
||
.trim()
|
||
.replace(/\s+/g, '') // 去除所有空格
|
||
.replace(/[,。;:!?]/g, (match) => { // 全角转半角
|
||
return {
|
||
',': ',',
|
||
'。': '.',
|
||
';': ';',
|
||
':': ':',
|
||
'!': '!',
|
||
'?': '?'
|
||
}[match] || match;
|
||
});
|
||
|
||
// 数值归一化:提取数字
|
||
const numberMatch = normalized.match(/(\d+\.?\d*)\s*(cm|mm|kg|mg|ml|%)?/);
|
||
if (numberMatch) {
|
||
const num = parseFloat(numberMatch[1]);
|
||
const unit = numberMatch[2] || '';
|
||
normalized = `${num}${unit}`;
|
||
}
|
||
|
||
return normalized;
|
||
}
|
||
|
||
/**
|
||
* 计算文本相似度(Dice Coefficient)
|
||
*
|
||
* 范围:0-1,1表示完全相同
|
||
*/
|
||
private calculateSimilarity(a: string, b: string): number {
|
||
if (a === b) return 1;
|
||
if (!a || !b) return 0;
|
||
|
||
// 生成2-gram
|
||
const bigramsA = this.getBigrams(a);
|
||
const bigramsB = this.getBigrams(b);
|
||
|
||
if (bigramsA.size === 0 && bigramsB.size === 0) return 1;
|
||
if (bigramsA.size === 0 || bigramsB.size === 0) return 0;
|
||
|
||
// 计算交集
|
||
const intersection = new Set([...bigramsA].filter(x => bigramsB.has(x)));
|
||
|
||
// Dice系数:2 * |A ∩ B| / (|A| + |B|)
|
||
const similarity = (2 * intersection.size) / (bigramsA.size + bigramsB.size);
|
||
|
||
return similarity;
|
||
}
|
||
|
||
/**
|
||
* 生成2-gram集合
|
||
*/
|
||
private getBigrams(str: string): Set<string> {
|
||
const bigrams = new Set<string>();
|
||
for (let i = 0; i < str.length - 1; i++) {
|
||
bigrams.add(str.substring(i, i + 2));
|
||
}
|
||
return bigrams;
|
||
}
|
||
|
||
/**
|
||
* 计算冲突严重程度
|
||
*/
|
||
private calculateSeverity(conflictCount: number, totalFields: number): 'low' | 'medium' | 'high' {
|
||
const conflictRate = conflictCount / totalFields;
|
||
|
||
if (conflictRate === 0) return 'low';
|
||
if (conflictRate <= 0.3) return 'low'; // ≤30%
|
||
if (conflictRate <= 0.6) return 'medium'; // 30%-60%
|
||
return 'high'; // >60%
|
||
}
|
||
|
||
/**
|
||
* 批量检测冲突
|
||
*
|
||
* @param items 提取记录数组
|
||
* @returns 冲突统计
|
||
*/
|
||
batchDetect(items: Array<{ resultA: Record<string, string>; resultB: Record<string, string> }>): {
|
||
totalCount: number;
|
||
cleanCount: number;
|
||
conflictCount: number;
|
||
severityDistribution: Record<'low' | 'medium' | 'high', number>;
|
||
} {
|
||
let cleanCount = 0;
|
||
let conflictCount = 0;
|
||
const severityDistribution = { low: 0, medium: 0, high: 0 };
|
||
|
||
for (const item of items) {
|
||
const result = this.detectConflict(item.resultA, item.resultB);
|
||
|
||
if (result.hasConflict) {
|
||
conflictCount++;
|
||
severityDistribution[result.severity]++;
|
||
} else {
|
||
cleanCount++;
|
||
}
|
||
}
|
||
|
||
return {
|
||
totalCount: items.length,
|
||
cleanCount,
|
||
conflictCount,
|
||
severityDistribution
|
||
};
|
||
}
|
||
}
|
||
|
||
// 导出单例
|
||
export const conflictDetectionService = new ConflictDetectionService();
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|