feat(iit): Complete V3.1 QC engine + GCP business reports + AI timeline + bug fixes

V3.1 QC Engine:
- QcExecutor unified entry + D1-D7 dimension engines + three-level aggregation
- HealthScoreEngine + CompletenessEngine + ProtocolDeviationEngine + QcAggregator
- B4 flexible cron scheduling (project-level cronExpression + pg-boss dispatcher)
- Prisma migrations for qc_field_status, event_status, project_stats

GCP Business Reports (Phase A - 4 reports):
- D1 Eligibility: record_summary full list + qc_field_status D1 overlay
- D2 Completeness: data entry rate and missing rate aggregation
- D3/D4 Query Tracking: severity distribution from qc_field_status
- D6 Protocol Deviation: D6 dimension filtering
- 4 frontend table components + ReportsPage 5-tab restructure

AI Timeline Enhancement:
- SkillRunner outputs totalRules (33 actual rules vs 1 skill)
- iitQcCockpitController severity mapping fix (critical->red, warning->yellow)
- AiStreamPage expandable issue detail table with Chinese labels
- Event label localization (eventLabel from backend)

Business-side One-click Batch QC:
- DashboardPage batch QC button with SyncOutlined icon
- Auto-refresh QcReport cache after batch execution

Bug Fixes:
- dimension_code -> rule_category in 4 SQL queries
- D1 eligibility data source: record_summary full + qc_field_status overlay
- Timezone UTC -> Asia/Shanghai (QcReportService toBeijingTime helper)
- Pass rate calculation: passed/totalEvents instead of passed/totalRecords

Docs:
- Update IIT module status with GCP reports and bug fix milestones
- Update system status doc v6.6 with IIT progress

Tested: Backend compiles, frontend linter clean, batch QC verified
Made-with: Cursor
This commit is contained in:
2026-03-01 22:49:49 +08:00
parent 0b29fe88b5
commit 2030ebe28f
50 changed files with 8687 additions and 1492 deletions

View File

@@ -2,6 +2,7 @@
* AI 实时工作流水页 (Level 2)
*
* 以 Timeline 展示 Agent 每次质控的完整动作链AI 白盒化。
* 显示中文事件名、实际规则数、五层定位详情、最终判定状态。
*/
import React, { useState, useEffect, useCallback } from 'react';
@@ -16,6 +17,8 @@ import {
Button,
Badge,
Pagination,
Collapse,
Table,
} from 'antd';
import {
ThunderboltOutlined,
@@ -27,6 +30,7 @@ import {
RobotOutlined,
ApiOutlined,
BellOutlined,
FileSearchOutlined,
} from '@ant-design/icons';
import * as iitProjectApi from '../api/iitProjectApi';
import type { TimelineItem } from '../api/iitProjectApi';
@@ -34,10 +38,10 @@ import { useIitProject } from '../context/IitProjectContext';
const { Text } = Typography;
const STATUS_DOT: Record<string, { color: string; icon: React.ReactNode }> = {
PASS: { color: 'green', icon: <CheckCircleOutlined /> },
FAIL: { color: 'red', icon: <CloseCircleOutlined /> },
WARNING: { color: 'orange', icon: <WarningOutlined /> },
const STATUS_DOT: Record<string, { color: string; icon: React.ReactNode; label: string }> = {
PASS: { color: 'green', icon: <CheckCircleOutlined />, label: '通过' },
FAIL: { color: 'red', icon: <CloseCircleOutlined />, label: '严重' },
WARNING: { color: 'orange', icon: <WarningOutlined />, label: '警告' },
};
const TRIGGER_TAG: Record<string, { color: string; label: string }> = {
@@ -82,19 +86,52 @@ const AiStreamPage: React.FC = () => {
};
const timelineItems = items.map((item) => {
const dotCfg = STATUS_DOT[item.status] || { color: 'gray', icon: <ClockCircleOutlined /> };
const dotCfg = STATUS_DOT[item.status] || { color: 'gray', icon: <ClockCircleOutlined />, label: '未知' };
const triggerCfg = TRIGGER_TAG[item.triggeredBy] || { color: 'default', label: item.triggeredBy };
const { red, yellow } = item.details.issuesSummary;
const time = new Date(item.time);
const timeStr = time.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
const dateStr = time.toLocaleDateString('zh-CN');
const eventLabel = item.eventLabel || '';
const issues = item.details.issues || [];
const issueColumns = [
{
title: '规则',
dataIndex: 'ruleName',
width: 160,
render: (v: string, r: any) => (
<Space size={4}>
<Text>{v || r.ruleId}</Text>
</Space>
),
},
{ title: '字段', dataIndex: 'field', width: 110, render: (v: string) => v ? <Text code>{v}</Text> : '—' },
{ title: '问题描述', dataIndex: 'message', ellipsis: true },
{
title: '严重度',
dataIndex: 'severity',
width: 80,
render: (s: string) => (
<Tag color={s === 'critical' ? 'error' : 'warning'}>
{s === 'critical' ? '严重' : '警告'}
</Tag>
),
},
{
title: '实际值',
dataIndex: 'actualValue',
width: 90,
render: (v: string) => v ?? '—',
},
];
return {
color: dotCfg.color as any,
dot: dotCfg.icon,
children: (
<div style={{ paddingBottom: 8 }}>
{/* Header line */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<Text strong style={{ fontSize: 13, fontFamily: 'monospace' }}>{timeStr}</Text>
<Text type="secondary" style={{ fontSize: 11 }}>{dateStr}</Text>
@@ -102,11 +139,10 @@ const AiStreamPage: React.FC = () => {
{triggerCfg.label}
</Tag>
<Tag color={dotCfg.color} style={{ fontSize: 10, lineHeight: '18px', padding: '0 6px' }}>
{item.status}
{dotCfg.label}
</Tag>
</div>
{/* Description — the AI action chain */}
<div style={{
background: '#f8fafc',
borderRadius: 8,
@@ -118,18 +154,20 @@ const AiStreamPage: React.FC = () => {
<Space wrap size={4} style={{ marginBottom: 4 }}>
<RobotOutlined style={{ color: '#3b82f6' }} />
<Text> <Text code>{item.recordId}</Text></Text>
{item.formName && <Text type="secondary">[{item.formName}]</Text>}
{eventLabel && <Tag color="geekblue">{eventLabel}</Tag>}
</Space>
<div style={{ marginLeft: 20 }}>
<Space size={4}>
<ApiOutlined style={{ color: '#8b5cf6' }} />
<Text> {item.details.rulesEvaluated} </Text>
<Text> <Text strong>{item.details.rulesEvaluated}</Text> </Text>
<Text type="success"> {item.details.rulesPassed} </Text>
{item.details.rulesFailed > 0 && (
<Text type="danger">/ {item.details.rulesFailed} </Text>
)}
</Space>
</div>
{(red > 0 || yellow > 0) && (
<div style={{ marginLeft: 20, marginTop: 2 }}>
<Space size={4}>
@@ -141,6 +179,40 @@ const AiStreamPage: React.FC = () => {
</Space>
</div>
)}
{issues.length > 0 && (
<Collapse
ghost
size="small"
style={{ marginTop: 4 }}
items={[{
key: '1',
label: (
<Space size={4}>
<FileSearchOutlined style={{ color: '#64748b' }} />
<Text type="secondary"> {issues.length} </Text>
</Space>
),
children: (
<Table
dataSource={issues}
rowKey={(_, i) => `issue-${i}`}
size="small"
pagination={false}
columns={issueColumns}
/>
),
}]}
/>
)}
{issues.length === 0 && item.status === 'PASS' && (
<div style={{ marginLeft: 20, marginTop: 2 }}>
<Text type="success" style={{ fontSize: 12 }}>
<CheckCircleOutlined />
</Text>
</div>
)}
</div>
</div>
),
@@ -149,7 +221,6 @@ const AiStreamPage: React.FC = () => {
return (
<div>
{/* Header */}
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Space>
<Tag icon={<ThunderboltOutlined />} color="processing"></Tag>
@@ -172,7 +243,6 @@ const AiStreamPage: React.FC = () => {
</Space>
</div>
{/* Timeline */}
<Card>
{items.length > 0 ? (
<>