feat(core): finalize rvw stability updates and pending module changes

Summary:
- Harden RVW prompt protocol handling and methodology review flow with 20-checkpoint coverage, divide-and-conquer execution, and timeout tuning
- Update RVW frontend methodology report rendering to show real structured outputs and grouped checkpoint sections
- Include pending backend/frontend updates across IIT admin, SSA, extraction forensics, and related integration files
- Sync system and RVW status documentation, deployment checklist, and RVW architecture/plan docs

Validation:
- Verified lint diagnostics for touched RVW backend/frontend files show no new errors
- Kept backup dump files and local test artifacts untracked

Made-with: Cursor
This commit is contained in:
2026-03-14 00:00:04 +08:00
parent 6edfad032f
commit ba464082cb
35 changed files with 1575 additions and 268 deletions

View File

@@ -278,12 +278,12 @@ export interface PlatformUserOption {
role?: string;
}
export async function searchPlatformUsers(search: string): Promise<PlatformUserOption[]> {
export async function searchPlatformUsers(projectId: string, search: string): Promise<PlatformUserOption[]> {
if (!search || search.length < 2) return [];
const response = await apiClient.get('/api/admin/users', {
params: { search, pageSize: 20 },
const response = await apiClient.get(`${BASE_URL}/${projectId}/users/search`, {
params: { search, limit: 20 },
});
const items = response.data.data?.data || response.data.data?.items || response.data.data?.list || [];
const items = response.data.data || [];
return items.map((u: any) => ({
id: u.id,
name: u.name || '',

View File

@@ -105,7 +105,11 @@ const IitMemberManagePage: React.FC = () => {
setUserSearching(true);
searchTimerRef.current = setTimeout(async () => {
try {
const users = await iitProjectApi.searchPlatformUsers(value);
if (!selectedProjectId) {
setUserSearchOptions([]);
return;
}
const users = await iitProjectApi.searchPlatformUsers(selectedProjectId, value);
setUserSearchOptions(users);
} catch {
setUserSearchOptions([]);

View File

@@ -951,7 +951,7 @@ const UserMappingTab: React.FC<UserMappingTabProps> = ({ projectId }) => {
setUserSearching(true);
searchTimerRef.current = setTimeout(async () => {
try {
const users = await iitProjectApi.searchPlatformUsers(value);
const users = await iitProjectApi.searchPlatformUsers(projectId, value);
setUserSearchOptions(users);
} catch {
setUserSearchOptions([]);

View File

@@ -1,7 +1,7 @@
/**
* 方法学评估报告组件 - 专业版
*/
import { XCircle, AlertTriangle, CheckCircle, Microscope, Lightbulb, MapPin, TrendingUp } from 'lucide-react';
import { XCircle, AlertTriangle, CheckCircle, Microscope } from 'lucide-react';
import type { MethodologyReviewResult } from '../types';
interface MethodologyReportProps {
@@ -9,16 +9,32 @@ interface MethodologyReportProps {
}
export default function MethodologyReport({ data }: MethodologyReportProps) {
const totalIssues = data.parts.reduce((sum, part) => sum + part.issues.length, 0);
const majorIssues = data.parts.reduce((sum, part) => sum + part.issues.filter(i => i.severity === 'major').length, 0);
const conclusion = data.conclusion || '未给出';
const checkpoints = data.checkpoints || [];
const majorCheckpointIssues = checkpoints.filter(cp => cp.status === 'major_issue');
const minorCheckpointIssues = checkpoints.filter(cp => cp.status === 'minor_issue');
const totalIssues = checkpoints.length > 0
? checkpoints.filter(cp => cp.status === 'major_issue' || cp.status === 'minor_issue').length
: data.parts.reduce((sum, part) => sum + part.issues.length, 0);
const majorIssues = checkpoints.length > 0
? checkpoints.filter(cp => cp.status === 'major_issue').length
: data.parts.reduce((sum, part) => sum + part.issues.filter(i => i.severity === 'major').length, 0);
const minorIssues = totalIssues - majorIssues;
const uncoveredCount = checkpoints.filter(cp => cp.status === 'not_mentioned').length;
const getSeverityStyle = (severity: 'major' | 'minor') => {
return severity === 'major'
? { icon: <XCircle className="w-4 h-4 text-red-500" />, label: '严重', badge: 'bg-red-100 text-red-700 border-red-200' }
: { icon: <AlertTriangle className="w-4 h-4 text-amber-500" />, label: '轻微', badge: 'bg-amber-100 text-amber-700 border-amber-200' };
const getCheckpointStyle = (status: 'pass' | 'minor_issue' | 'major_issue' | 'not_mentioned') => {
if (status === 'pass') return { text: '通过', cls: 'bg-green-100 text-green-700' };
if (status === 'minor_issue') return { text: '一般问题', cls: 'bg-amber-100 text-amber-700' };
if (status === 'major_issue') return { text: '严重问题', cls: 'bg-red-100 text-red-700' };
return { text: '未覆盖', cls: 'bg-slate-100 text-slate-600' };
};
const checkpointSections = [
{ title: '一、科研设计评估 (Scientific Design)', start: 1, end: 9 },
{ title: '二、统计学方法描述评估 (Statistical Methodology)', start: 10, end: 14 },
{ title: '三、统计分析与结果评估 (Analysis & Results)', start: 15, end: 20 },
];
return (
<div className="space-y-6 fade-in">
{/* 总览卡片 */}
@@ -32,7 +48,13 @@ export default function MethodologyReport({ data }: MethodologyReportProps) {
<div className="flex gap-4">
<div className="flex items-center gap-2 px-3 py-1.5 bg-slate-50 rounded-lg border border-slate-200">
<span className="text-sm text-slate-600"> <span className="font-bold text-slate-800">{data.parts.length}</span> </span>
<span className="text-sm text-slate-600">
<span className="font-bold text-slate-800">{checkpoints.length || 20}</span>
</span>
</div>
<div className="flex items-center gap-2 px-3 py-1.5 bg-indigo-50 rounded-lg border border-indigo-100">
<span className="text-sm text-indigo-600">稿</span>
<span className="text-sm font-semibold text-indigo-700">{conclusion}</span>
</div>
{totalIssues === 0 ? (
<div className="flex items-center gap-2 px-3 py-1.5 bg-green-50 rounded-lg border border-green-100">
@@ -59,100 +81,97 @@ export default function MethodologyReport({ data }: MethodologyReportProps) {
</div>
</div>
{/* 分项详情标题 */}
<div className="flex items-center gap-3">
<TrendingUp className="w-5 h-5 text-purple-500" />
<h3 className="font-bold text-base text-slate-800"></h3>
<span className="text-xs text-slate-400 bg-slate-100 px-2 py-0.5 rounded"> {data.parts.length} </span>
</div>
{/* 按专家要求的报告结构快速呈现 */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div className="p-6 space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-slate-800">1. </h4>
<p className="text-sm text-slate-600 leading-relaxed">{data.summary || '未生成总体评价。'}</p>
</div>
{/* 分项详情 */}
<div className="space-y-4">
{data.parts.map((part, partIndex) => (
<div key={partIndex} className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
{/* 分项头部 */}
<div className={`px-5 py-4 border-b ${part.issues.length === 0 ? 'bg-green-50/50 border-green-100' : 'bg-slate-50 border-slate-100'}`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{part.issues.length === 0 ? (
<CheckCircle className="w-5 h-5 text-green-500" />
) : (
<AlertTriangle className="w-5 h-5 text-amber-500" />
)}
<h4 className="font-semibold text-slate-800">{part.part}</h4>
</div>
{part.issues.length === 0 ? (
<span className="px-2.5 py-1 rounded-md text-xs font-medium bg-green-100 text-green-700 flex items-center gap-1">
<CheckCircle className="w-3 h-3" />
</span>
) : (
<span className="px-2.5 py-1 rounded-md text-xs font-medium bg-amber-100 text-amber-700">
{part.issues.length}
</span>
)}
</div>
</div>
{/* 问题列表 */}
{part.issues.length > 0 && (
<div className="divide-y divide-gray-50">
{part.issues.map((issue, issueIndex) => {
const severity = getSeverityStyle(issue.severity);
return (
<div key={issueIndex} className="px-5 py-4">
<div className="flex items-start gap-4">
<div className="mt-0.5">{severity.icon}</div>
<div className="flex-1 space-y-3">
{/* 问题标题和严重程度 */}
<div className="flex items-center gap-2 flex-wrap">
<span className="font-semibold text-slate-800">{issue.type}</span>
<span className={`px-2 py-0.5 rounded text-xs font-medium border ${severity.badge}`}>
{severity.label}
</span>
</div>
{/* 问题描述 */}
<p className="text-sm text-slate-600 leading-relaxed">{issue.description}</p>
{/* 位置信息 */}
{issue.location && (
<div className="flex items-center gap-2 text-xs text-slate-400">
<MapPin className="w-3.5 h-3.5" />
<span>{issue.location}</span>
</div>
)}
{/* 改进建议 */}
{issue.suggestion && (
<div className="bg-indigo-50/50 rounded-lg p-3 border border-indigo-100">
<div className="flex items-start gap-2">
<Lightbulb className="w-4 h-4 text-indigo-500 mt-0.5 flex-shrink-0" />
<div>
<p className="text-xs font-semibold text-indigo-600 mb-1"></p>
<p className="text-sm text-slate-700">{issue.suggestion}</p>
</div>
</div>
</div>
)}
<div className="space-y-2">
<h4 className="text-sm font-semibold text-slate-800">2. </h4>
{totalIssues === 0 ? (
<p className="text-sm text-green-700"></p>
) : (
<div className="space-y-3">
{majorCheckpointIssues.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-semibold text-red-600"></p>
<div className="space-y-2">
{majorCheckpointIssues.map((cp) => (
<div key={`major-${cp.id}`} className="rounded-lg border border-red-100 bg-red-50/50 p-3">
<p className="text-sm font-medium text-slate-800">{cp.id}. {cp.item}</p>
<p className="mt-1 text-sm text-slate-600">{cp.finding}</p>
{cp.suggestion && <p className="mt-1 text-sm text-slate-700">{cp.suggestion}</p>}
</div>
</div>
))}
</div>
);
})}
</div>
)}
{/* 无问题时的简洁显示 */}
{part.issues.length === 0 && (
<div className="px-5 py-4 text-sm text-green-600 flex items-center gap-2">
<CheckCircle className="w-4 h-4" />
<span></span>
</div>
)}
{minorCheckpointIssues.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-semibold text-amber-600"></p>
<div className="space-y-2">
{minorCheckpointIssues.map((cp) => (
<div key={`minor-${cp.id}`} className="rounded-lg border border-amber-100 bg-amber-50/50 p-3">
<p className="text-sm font-medium text-slate-800">{cp.id}. {cp.item}</p>
<p className="mt-1 text-sm text-slate-600">{cp.finding}</p>
{cp.suggestion && <p className="mt-1 text-sm text-slate-700">{cp.suggestion}</p>}
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
))}
<div className="space-y-2">
<h4 className="text-sm font-semibold text-slate-800">3. 稿</h4>
<div className="inline-flex items-center gap-2 px-3 py-1.5 bg-indigo-50 rounded-lg border border-indigo-100">
<span className="text-sm text-indigo-600"></span>
<span className="text-sm font-semibold text-indigo-700">{conclusion}</span>
</div>
</div>
</div>
</div>
{checkpoints.length > 0 && (
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div className="p-6 space-y-5">
<div className="flex items-center justify-between">
<h4 className="text-sm font-semibold text-slate-800">20</h4>
<span className={`text-xs px-2 py-1 rounded ${uncoveredCount > 0 ? 'bg-amber-100 text-amber-700' : 'bg-green-100 text-green-700'}`}>
{uncoveredCount > 0 ? `仍有 ${uncoveredCount} 项未覆盖` : '20项已覆盖'}
</span>
</div>
{checkpointSections.map(section => {
const sectionItems = checkpoints.filter(cp => cp.id >= section.start && cp.id <= section.end);
return (
<div key={section.title} className="space-y-3">
<h5 className="text-sm font-semibold text-slate-800">{section.title}</h5>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{sectionItems.map(cp => {
const style = getCheckpointStyle(cp.status);
return (
<div key={cp.id} className="border border-slate-100 rounded-lg p-3 space-y-2">
<div className="flex items-center justify-between gap-2">
<p className="text-sm font-medium text-slate-800">{cp.id}. {cp.item}</p>
<span className={`text-xs px-2 py-0.5 rounded ${style.cls}`}>{style.text}</span>
</div>
<p className="text-xs text-slate-600">{cp.finding}</p>
</div>
);
})}
</div>
</div>
);
})}
</div>
</div>
)}
</div>
);
}

View File

@@ -89,10 +89,20 @@ export interface MethodologyPart {
issues: MethodologyIssue[];
}
export interface MethodologyCheckpoint {
id: number;
item: string;
status: 'pass' | 'minor_issue' | 'major_issue' | 'not_mentioned';
finding: string;
suggestion?: string;
}
// 方法学评估结果
export interface MethodologyReviewResult {
overall_score: number;
summary: string;
conclusion?: '直接接收' | '小修' | '大修' | '拒稿';
checkpoints?: MethodologyCheckpoint[];
parts: MethodologyPart[];
}

View File

@@ -44,6 +44,7 @@ import { AskUserCard } from './AskUserCard';
import type { AskUserResponseData } from './AskUserCard';
import { ThinkingBlock } from '@/shared/components/Chat';
import type { ClarificationCardData, IntentResult } from '../types';
import { safeRandomUUID } from '../utils/id';
export const SSAChatPane: React.FC = () => {
const navigate = useNavigate();
@@ -228,7 +229,7 @@ export const SSAChatPane: React.FC = () => {
} else {
// 没有 session未上传数据走老的 generatePlan 流程
addMessage({
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: 'user',
content: query,
createdAt: new Date().toISOString(),
@@ -248,7 +249,7 @@ export const SSAChatPane: React.FC = () => {
const selectedLabel = Object.values(selections).join(', ');
addMessage({
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: 'user',
content: selectedLabel,
createdAt: new Date().toISOString(),
@@ -263,7 +264,7 @@ export const SSAChatPane: React.FC = () => {
if (resp.needsClarification && resp.clarificationCards?.length > 0) {
addMessage({
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: 'assistant',
content: '还需要确认一下:',
createdAt: new Date().toISOString(),

View File

@@ -8,6 +8,7 @@ import { useCallback, useState } from 'react';
import apiClient from '@/common/api/axios';
import { getAccessToken } from '@/framework/auth/api';
import { useSSAStore } from '../stores/ssaStore';
import { safeRandomUUID } from '../utils/id';
import type { AnalysisPlan, ExecutionResult, SSAMessage } from '../types';
import {
Document,
@@ -119,7 +120,7 @@ export function useAnalysis(): UseAnalysisReturn {
try {
const userMessage: SSAMessage = {
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: 'user',
content: query,
createdAt: new Date().toISOString(),
@@ -146,7 +147,7 @@ export function useAnalysis(): UseAnalysisReturn {
} as any);
const planMessage: SSAMessage = {
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: 'assistant',
content: `已生成分析方案:${plan.toolName}\n\n${plan.description}`,
artifactType: 'sap',
@@ -156,7 +157,7 @@ export function useAnalysis(): UseAnalysisReturn {
addMessage(planMessage);
addMessage({
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: 'assistant',
content: '请确认数据映射并执行分析。',
artifactType: 'confirm',
@@ -221,7 +222,7 @@ export function useAnalysis(): UseAnalysisReturn {
});
addMessage({
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: 'assistant',
content: result.interpretation || '分析完成,请查看右侧结果面板。',
artifactType: 'result',

View File

@@ -16,6 +16,7 @@
import { useState, useCallback, useRef } from 'react';
import { getAccessToken, isTokenExpired, refreshAccessToken } from '@/framework/auth/api';
import { useSSAStore } from '../stores/ssaStore';
import { safeRandomUUID } from '../utils/id';
import type { AskUserEventData, AskUserResponseData } from '../components/AskUserCard';
import type { WorkflowPlan, AgentStepResult } from '../types';
@@ -243,7 +244,7 @@ export function useSSAChat(): UseSSAChatReturn {
const isAgentAction = !!finalMetadata?.agentAction || isAgentInlineInstruction;
const assistantMsgId = crypto.randomUUID();
const assistantMsgId = safeRandomUUID();
const assistantPlaceholder: ChatMessage = {
id: assistantMsgId,
role: 'assistant',
@@ -257,7 +258,7 @@ export function useSSAChat(): UseSSAChatReturn {
setChatMessages(prev => [...prev, assistantPlaceholder]);
} else {
const userMsg: ChatMessage = {
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: 'user',
content,
status: 'complete',
@@ -359,7 +360,7 @@ export function useSSAChat(): UseSSAChatReturn {
if (parsed.type === 'agent_planning') {
const { pushAgentExecution, setWorkspaceOpen, setActivePane } = useSSAStore.getState();
pushAgentExecution({
id: parsed.executionId || crypto.randomUUID(),
id: parsed.executionId || safeRandomUUID(),
sessionId: sessionId,
query: content,
retryCount: 0,
@@ -524,7 +525,7 @@ export function useSSAChat(): UseSSAChatReturn {
const execId = curExec?.id;
const execQuery = curExec?.query || content;
setChatMessages(prev => [...prev, {
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: 'assistant' as const,
content: `✅ 分析完成:${execQuery}`,
status: 'complete' as const,
@@ -664,7 +665,7 @@ export function useSSAChat(): UseSSAChatReturn {
const auditContent = AUDIT_MESSAGES[action];
// 1. 追加审计轨迹消息(系统风格,不是用户消息)
const auditMsgId = crypto.randomUUID();
const auditMsgId = safeRandomUUID();
setChatMessages(prev => [...prev, {
id: auditMsgId,
role: 'assistant' as const,

View File

@@ -8,6 +8,7 @@ import { useCallback, useRef } from 'react';
import apiClient from '@/common/api/axios';
import { getAccessToken } from '@/framework/auth/api';
import { useSSAStore } from '../stores/ssaStore';
import { safeRandomUUID } from '../utils/id';
import type { AnalysisRecord } from '../stores/ssaStore';
import type {
DataProfile,
@@ -72,7 +73,7 @@ export function useWorkflow(): UseWorkflowReturn {
const profile: DataProfile = response.data.profile;
setDataProfile(profile);
addMessage({
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: 'assistant',
content: `数据质量核查完成:${profile.quality_grade}级 (${profile.quality_score}分)`,
artifactType: 'profile',
@@ -112,7 +113,7 @@ export function useWorkflow(): UseWorkflowReturn {
setWorkspaceOpen(true);
addMessage({
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: 'assistant',
content: `已生成分析方案:${plan.title}\n共 ${plan.total_steps} 个分析步骤`,
artifactType: 'sap',
@@ -120,7 +121,7 @@ export function useWorkflow(): UseWorkflowReturn {
createdAt: new Date().toISOString(),
});
addMessage({
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: 'assistant',
content: '请确认分析计划并开始执行。',
artifactType: 'confirm',
@@ -183,7 +184,7 @@ export function useWorkflow(): UseWorkflowReturn {
setActivePane('sap');
setWorkspaceOpen(true);
addMessage({
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: 'assistant',
content: `已生成分析方案:${data.plan.title}\n共 ${data.plan.total_steps} 个分析步骤`,
artifactType: 'sap',
@@ -346,7 +347,7 @@ export function useWorkflow(): UseWorkflowReturn {
: String(firstErr);
}
addMessage({
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: 'assistant',
content: `分析执行失败:${errText}`,
artifactType: 'execution',
@@ -368,7 +369,7 @@ export function useWorkflow(): UseWorkflowReturn {
if (conclusion) {
setActivePane('result');
addMessage({
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: 'assistant',
content: `分析完成!${conclusion.executive_summary?.slice(0, 100) || '查看右侧结果面板获取详细信息'}`,
artifactType: 'result',
@@ -377,7 +378,7 @@ export function useWorkflow(): UseWorkflowReturn {
});
} else if (hasAnySuccess) {
addMessage({
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: 'assistant',
content: '分析执行完成!',
artifactType: 'result',
@@ -386,7 +387,7 @@ export function useWorkflow(): UseWorkflowReturn {
});
} else {
addMessage({
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: 'assistant',
content: '分析执行完成,但未生成结论报告。',
createdAt: new Date().toISOString(),

View File

@@ -0,0 +1,19 @@
/**
* 兼容性 ID 生成器:
* - 安全上下文HTTPS / localhost优先使用 crypto.randomUUID()
* - 非安全上下文(如内网 HTTP自动降级避免 runtime 报错
*/
export function safeRandomUUID(): string {
try {
if (typeof globalThis !== 'undefined' && globalThis.crypto?.randomUUID) {
return globalThis.crypto.randomUUID();
}
} catch {
// ignore and fallback
}
const time = Date.now().toString(36);
const rand = Math.random().toString(36).slice(2, 10);
return `ssa-${time}-${rand}`;
}