feat(rvw): harden json parsing and finalize 0316 rollout
Stabilize RVW editorial and methodology JSON parsing in production with layered repair and fallback handling, then publish the paired frontend task-level language selector updates. Also reset deployment checklist, record the 0316 deployment summary, and refresh the SAE runtime status with latest backend/frontend IPs. Made-with: Cursor
This commit is contained in:
@@ -2,7 +2,13 @@
|
||||
* RVW模块API
|
||||
*/
|
||||
import apiClient from '../../../common/api/axios';
|
||||
import type { ReviewTask, ReviewReport, ApiResponse, AgentType } from '../types';
|
||||
import type {
|
||||
ReviewTask,
|
||||
ReviewReport,
|
||||
ApiResponse,
|
||||
AgentType,
|
||||
EditorialBaseStandard,
|
||||
} from '../types';
|
||||
|
||||
const API_BASE = '/api/v1/rvw';
|
||||
|
||||
@@ -45,8 +51,15 @@ export async function getTaskReport(taskId: string): Promise<ReviewReport> {
|
||||
}
|
||||
|
||||
// 运行审查任务(返回jobId供轮询)
|
||||
export async function runTask(taskId: string, agents: AgentType[]): Promise<{ taskId: string; jobId: string }> {
|
||||
const response = await apiClient.post<ApiResponse<{ taskId: string; jobId: string }>>(`${API_BASE}/tasks/${taskId}/run`, { agents });
|
||||
export async function runTask(
|
||||
taskId: string,
|
||||
agents: AgentType[],
|
||||
editorialBaseStandard?: EditorialBaseStandard
|
||||
): Promise<{ taskId: string; jobId: string }> {
|
||||
const response = await apiClient.post<ApiResponse<{ taskId: string; jobId: string }>>(`${API_BASE}/tasks/${taskId}/run`, {
|
||||
agents,
|
||||
editorialBaseStandard,
|
||||
});
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.error || '运行失败');
|
||||
}
|
||||
@@ -54,8 +67,16 @@ export async function runTask(taskId: string, agents: AgentType[]): Promise<{ ta
|
||||
}
|
||||
|
||||
// 批量运行审查任务
|
||||
export async function batchRunTasks(taskIds: string[], agents: AgentType[]): Promise<void> {
|
||||
const response = await apiClient.post<ApiResponse<void>>(`${API_BASE}/tasks/batch/run`, { taskIds, agents });
|
||||
export async function batchRunTasks(
|
||||
taskIds: string[],
|
||||
agents: AgentType[],
|
||||
editorialBaseStandard?: EditorialBaseStandard
|
||||
): Promise<void> {
|
||||
const response = await apiClient.post<ApiResponse<void>>(`${API_BASE}/tasks/batch/run`, {
|
||||
taskIds,
|
||||
agents,
|
||||
editorialBaseStandard,
|
||||
});
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.error || '批量运行失败');
|
||||
}
|
||||
|
||||
@@ -1,20 +1,35 @@
|
||||
/**
|
||||
* 智能体选择弹窗
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { PlayCircle, X } from 'lucide-react';
|
||||
import type { AgentType } from '../types';
|
||||
import type { AgentType, EditorialBaseStandard } from '../types';
|
||||
|
||||
interface AgentModalProps {
|
||||
visible: boolean;
|
||||
taskCount: number;
|
||||
onClose: () => void;
|
||||
onConfirm: (agents: AgentType[]) => void;
|
||||
onConfirm: (agents: AgentType[], editorialBaseStandard?: EditorialBaseStandard) => void;
|
||||
defaultEditorialBaseStandard?: EditorialBaseStandard;
|
||||
isSubmitting?: boolean; // 🔒 防止重复提交
|
||||
}
|
||||
|
||||
export default function AgentModal({ visible, taskCount, onClose, onConfirm, isSubmitting = false }: AgentModalProps) {
|
||||
export default function AgentModal({
|
||||
visible,
|
||||
taskCount,
|
||||
onClose,
|
||||
onConfirm,
|
||||
defaultEditorialBaseStandard = 'zh',
|
||||
isSubmitting = false,
|
||||
}: AgentModalProps) {
|
||||
const [selectedAgents, setSelectedAgents] = useState<AgentType[]>(['editorial']);
|
||||
const [editorialBaseStandard, setEditorialBaseStandard] = useState<EditorialBaseStandard>(defaultEditorialBaseStandard);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
setSelectedAgents(['editorial']);
|
||||
setEditorialBaseStandard(defaultEditorialBaseStandard);
|
||||
}, [visible, defaultEditorialBaseStandard]);
|
||||
|
||||
const toggleAgent = (agent: AgentType) => {
|
||||
if (selectedAgents.includes(agent)) {
|
||||
@@ -29,14 +44,17 @@ export default function AgentModal({ visible, taskCount, onClose, onConfirm, isS
|
||||
|
||||
const handleConfirm = () => {
|
||||
// 只调用onConfirm,让调用方控制关闭时机
|
||||
onConfirm(selectedAgents);
|
||||
onConfirm(
|
||||
selectedAgents,
|
||||
selectedAgents.includes('editorial') ? editorialBaseStandard : undefined
|
||||
);
|
||||
};
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-slate-900/50 z-50 flex items-center justify-center backdrop-blur-sm">
|
||||
<div className="bg-white rounded-2xl shadow-2xl w-[400px] overflow-hidden transform transition-all scale-100 fade-in">
|
||||
<div className="bg-white rounded-2xl shadow-2xl w-[560px] max-w-[92vw] overflow-hidden transform transition-all scale-100 fade-in">
|
||||
{/* 头部 */}
|
||||
<div className="bg-slate-900 p-5 text-white flex items-center justify-between">
|
||||
<h3 className="font-bold text-lg flex items-center gap-2">
|
||||
@@ -76,6 +94,34 @@ export default function AgentModal({ visible, taskCount, onClose, onConfirm, isS
|
||||
</div>
|
||||
<span className="tag tag-blue">快速</span>
|
||||
</label>
|
||||
|
||||
{selectedAgents.includes('editorial') && (
|
||||
<div className="rounded-xl border border-sky-200 bg-sky-50 p-4 mt-2">
|
||||
<div className="text-sm font-bold text-slate-800 mb-2">稿约基线语言</div>
|
||||
<div className="flex items-center gap-8">
|
||||
<label className="flex items-center gap-2 text-sm text-slate-700 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="editorialBaseStandard"
|
||||
checked={editorialBaseStandard === 'zh'}
|
||||
onChange={() => setEditorialBaseStandard('zh')}
|
||||
className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-500"
|
||||
/>
|
||||
中文
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm text-slate-700 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="editorialBaseStandard"
|
||||
checked={editorialBaseStandard === 'en'}
|
||||
onChange={() => setEditorialBaseStandard('en')}
|
||||
className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-500"
|
||||
/>
|
||||
英文
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 方法学智能体 */}
|
||||
<label
|
||||
@@ -122,6 +168,7 @@ export default function AgentModal({ visible, taskCount, onClose, onConfirm, isS
|
||||
</div>
|
||||
<span className="tag tag-pink">专业</span>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
|
||||
@@ -14,10 +14,18 @@ import {
|
||||
TaskDetail,
|
||||
} from '../components';
|
||||
import * as api from '../api';
|
||||
import type { ReviewTask, ReviewReport, TaskFilters, AgentType } from '../types';
|
||||
import type {
|
||||
ReviewTask,
|
||||
ReviewReport,
|
||||
TaskFilters,
|
||||
AgentType,
|
||||
EditorialBaseStandard,
|
||||
} from '../types';
|
||||
import { useAuth } from '../../../framework/auth';
|
||||
import '../styles/index.css';
|
||||
|
||||
export default function Dashboard() {
|
||||
const { user } = useAuth();
|
||||
// ==================== State ====================
|
||||
const [currentView, setCurrentView] = useState<'dashboard' | 'archive'>('dashboard');
|
||||
const [tasks, setTasks] = useState<ReviewTask[]>([]);
|
||||
@@ -27,6 +35,8 @@ export default function Dashboard() {
|
||||
const [agentModalVisible, setAgentModalVisible] = useState(false);
|
||||
const [pendingTaskForRun, setPendingTaskForRun] = useState<ReviewTask | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false); // 🔒 防止重复提交
|
||||
const [defaultEditorialBaseStandard, setDefaultEditorialBaseStandard] =
|
||||
useState<EditorialBaseStandard>('zh');
|
||||
|
||||
// 报告详情
|
||||
const [reportDetail, setReportDetail] = useState<ReviewReport | null>(null);
|
||||
@@ -64,6 +74,18 @@ export default function Dashboard() {
|
||||
loadTasks();
|
||||
}, [loadTasks]);
|
||||
|
||||
useEffect(() => {
|
||||
const tenantCode = user?.tenantCode;
|
||||
if (!tenantCode) return;
|
||||
fetch(`/api/v1/public/tenant-config/${tenantCode}`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
const base = data?.data?.editorialDefaultStandard;
|
||||
setDefaultEditorialBaseStandard(base === 'en' ? 'en' : 'zh');
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [user?.tenantCode]);
|
||||
|
||||
// 轮询更新进行中的任务
|
||||
useEffect(() => {
|
||||
const processingTasks = tasks.filter(t =>
|
||||
@@ -119,7 +141,7 @@ export default function Dashboard() {
|
||||
setAgentModalVisible(true);
|
||||
};
|
||||
|
||||
const handleConfirmRun = async (agents: AgentType[]) => {
|
||||
const handleConfirmRun = async (agents: AgentType[], editorialBaseStandard?: EditorialBaseStandard) => {
|
||||
// 🔒 防止重复提交
|
||||
if (isSubmitting) {
|
||||
console.warn('[Dashboard] 已在提交中,忽略重复请求');
|
||||
@@ -138,7 +160,7 @@ export default function Dashboard() {
|
||||
if (taskToRun) {
|
||||
// 单个任务 - 启动后跳转到详情页显示进度
|
||||
message.loading({ content: '正在启动审查...', key: 'run' });
|
||||
const { jobId } = await api.runTask(taskToRun.id, agents);
|
||||
const { jobId } = await api.runTask(taskToRun.id, agents, editorialBaseStandard);
|
||||
message.success({ content: '审查已启动', key: 'run', duration: 2 });
|
||||
|
||||
// 更新任务状态后跳转到详情页(传递jobId)
|
||||
@@ -159,7 +181,7 @@ export default function Dashboard() {
|
||||
}
|
||||
|
||||
message.loading({ content: `正在启动 ${pendingIds.length} 个任务...`, key: 'run' });
|
||||
await api.batchRunTasks(pendingIds, agents);
|
||||
await api.batchRunTasks(pendingIds, agents, editorialBaseStandard);
|
||||
message.success({ content: `${pendingIds.length} 个任务已启动`, key: 'run', duration: 2 });
|
||||
setSelectedIds([]);
|
||||
}
|
||||
@@ -282,6 +304,7 @@ export default function Dashboard() {
|
||||
<AgentModal
|
||||
visible={agentModalVisible}
|
||||
taskCount={pendingTaskForRun ? 1 : selectedIds.length}
|
||||
defaultEditorialBaseStandard={defaultEditorialBaseStandard}
|
||||
onClose={() => {
|
||||
setAgentModalVisible(false);
|
||||
setPendingTaskForRun(null);
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { message } from 'antd';
|
||||
import * as api from '../api';
|
||||
import type { ReviewTask, AgentType } from '../types';
|
||||
import type { ReviewTask, AgentType, EditorialBaseStandard } from '../types';
|
||||
import AgentModal from '../components/AgentModal';
|
||||
|
||||
// ── 内联 SVG 状态图标 ─────────────────────────────────────────────
|
||||
@@ -137,6 +137,7 @@ function StatusBadge({ status }: { status: ReviewTask['status'] }) {
|
||||
|
||||
export default function TenantDashboard() {
|
||||
const navigate = useNavigate();
|
||||
const { tenantSlug } = useParams<{ tenantSlug: string }>();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [tasks, setTasks] = useState<ReviewTask[]>([]);
|
||||
@@ -147,6 +148,19 @@ export default function TenantDashboard() {
|
||||
const [agentModalVisible, setAgentModalVisible] = useState(false);
|
||||
const [pendingTask, setPendingTask] = useState<ReviewTask | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [defaultEditorialBaseStandard, setDefaultEditorialBaseStandard] =
|
||||
useState<EditorialBaseStandard>('zh');
|
||||
|
||||
useEffect(() => {
|
||||
if (!tenantSlug) return;
|
||||
fetch(`/api/v1/public/tenant-config/${tenantSlug}`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
const base = data?.data?.editorialDefaultStandard;
|
||||
setDefaultEditorialBaseStandard(base === 'en' ? 'en' : 'zh');
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [tenantSlug]);
|
||||
|
||||
// ── 数据加载 ──────────────────────────────────────────────────
|
||||
const loadTasks = useCallback(async () => {
|
||||
@@ -265,7 +279,7 @@ export default function TenantDashboard() {
|
||||
setAgentModalVisible(true);
|
||||
};
|
||||
|
||||
const handleConfirmRun = async (agents: AgentType[]) => {
|
||||
const handleConfirmRun = async (agents: AgentType[], editorialBaseStandard?: EditorialBaseStandard) => {
|
||||
if (isSubmitting || !pendingTask) return;
|
||||
const task = pendingTask;
|
||||
setAgentModalVisible(false);
|
||||
@@ -273,7 +287,7 @@ export default function TenantDashboard() {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
message.loading({ content: '正在启动审查…', key: 'run' });
|
||||
const { jobId } = await api.runTask(task.id, agents);
|
||||
const { jobId } = await api.runTask(task.id, agents, editorialBaseStandard);
|
||||
message.success({ content: '审查已启动', key: 'run', duration: 2 });
|
||||
// 对齐旧版体验:启动后立即进入“审稿过程页”,动态查看进度与分模块结果
|
||||
navigate(`${task.id}?jobId=${encodeURIComponent(jobId)}`);
|
||||
@@ -502,6 +516,7 @@ export default function TenantDashboard() {
|
||||
<AgentModal
|
||||
visible={agentModalVisible}
|
||||
taskCount={1}
|
||||
defaultEditorialBaseStandard={defaultEditorialBaseStandard}
|
||||
onClose={() => { setAgentModalVisible(false); setPendingTask(null); }}
|
||||
onConfirm={handleConfirmRun}
|
||||
isSubmitting={isSubmitting}
|
||||
|
||||
@@ -15,6 +15,7 @@ export type TaskStatus =
|
||||
|
||||
// 智能体类型
|
||||
export type AgentType = 'editorial' | 'methodology' | 'clinical';
|
||||
export type EditorialBaseStandard = 'zh' | 'en';
|
||||
|
||||
// 审查任务
|
||||
export interface ReviewTask {
|
||||
@@ -23,6 +24,7 @@ export interface ReviewTask {
|
||||
fileSize: number;
|
||||
status: TaskStatus;
|
||||
selectedAgents: AgentType[];
|
||||
editorialBaseStandard?: EditorialBaseStandard;
|
||||
wordCount?: number;
|
||||
overallScore?: number;
|
||||
editorialScore?: number;
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user