/** * 评查点管理页面 - 创建或编辑评查点规则 * * 功能概述: * - 支持创建新的评查点规则或编辑现有规则 * - 评查点包含基本信息、抽取设置和评查设置三大部分 * - 支持使用三种抽取方式: 大模型(LLM)抽取、多模态(VLM)抽取和正则表达式抽取 * - 支持配置各种类型的评查规则,如存在性检查、内容一致性检查等 * - 支持保存评查规则和保存草稿功能 * * 组件结构: * - PageHeader: 页面标题和保存按钮 * - BasicInfo: 评查点的基本信息设置,如名称、编码、风险级别等 * - ExtractionSettings: 抽取设置,配置从文档中抽取哪些字段及抽取方式 * - ReviewSettings: 评查设置,基于抽取字段配置评查规则、评查结果消息等 * - ActionButtons: 页面底部的操作按钮,包括保存、保存草稿和返回 * * 数据流转: * 1. 页面加载时检查URL参数,确定是新建还是编辑模式 * 2. 编辑模式下从API获取评查点数据并填充表单 * 3. ExtractionSettings通过RuleContext将抽取的字段传递给ReviewSettings * 4. 各组件的onChange回调收集表单变更并更新formData状态 * 5. 保存时将formData转换为API需要的格式并提交 * * @author 中国烟草AI合同及卷宗审核系统开发团队 */ import { type MetaFunction } from "@remix-run/node"; import { useState, useEffect, useCallback } from "react"; import { BasicInfo } from "~/components/rules/new/BasicInfo"; import { ExtractionSettings } from "~/components/rules/new/ExtractionSettings"; import { ReviewSettings } from "~/components/rules/new/ReviewSettings"; import { ActionButtons } from "~/components/rules/new/ActionButtons"; import { PageHeader } from "~/components/rules/new/PageHeader"; import rulesStyles from "~/styles/rules.css?url"; import { useNavigate, useLocation } from "@remix-run/react"; // 导入评查点模型定义和常量 import type { EvaluationPoint, LogicOperator, CompareMethod, FormatType, ComparisonOperator, MatchType, ProgrammingLanguage } from "~/models/evaluation_points"; import { EVALUATION_OPTIONS } from "~/models/evaluation_points"; import type { EvaluationPointGroup } from "~/models/evaluation_point_groups"; // 导入RuleContext上下文 import { RuleContext } from "~/contexts/RuleContext"; import { postgrestGet, postgrestPost, postgrestPut } from "~/api/postgrest-client"; import { toastService } from '~/components/ui/Toast'; import type { UserRole } from '~/root'; export const meta: MetaFunction = () => { return [ { title: "评查点管理 - 中国烟草AI合同及卷宗审核系统" }, { name: "description", content: "创建或修改评查点,设置规则参数" } ]; }; export function links() { return [{ rel: "stylesheet", href: rulesStyles }]; } export const handle = { breadcrumb: "评查点管理", previousRoute: () => { if (typeof window !== 'undefined') { const searchParams = new URLSearchParams(window.location.search); const mode = searchParams.get('mode'); const id = searchParams.get('id'); if (mode || id) { return { title: "评查点列表", to: `/rules` }; } } return undefined; } }; // 添加规则配置接口 interface BaseRuleConfig { availableFields?: string[]; } interface ExistsRuleConfig extends BaseRuleConfig { fields: string[]; logic: LogicOperator; selectedFields?: string[]; existsLogic?: string; } interface ConsistencyRuleConfig extends BaseRuleConfig { pairs: Array<{sourceField: string; targetField: string; compareMethod: CompareMethod}>; logic: LogicOperator; logicRelation?: string; initialSourceField?: string; initialTargetField?: string; initialCompareMethod?: string; } interface FormatRuleConfig extends BaseRuleConfig { field: string; formatType: FormatType; parameters: string; checkField?: string; formatParams?: string; } interface LogicRuleConfig extends BaseRuleConfig { conditions: { field: string; operator: ComparisonOperator; value: string; }[]; logic: LogicOperator; logicRelation?: string; initialField?: string; initialOperator?: string; initialValue?: string; } interface RegexRuleConfig extends BaseRuleConfig { field: string; pattern: string; matchType: MatchType; checkField?: string; regexPattern?: string; } interface AIRuleConfig extends BaseRuleConfig { model: string; temperature: number; prompt: string; } interface CodeRuleConfig extends BaseRuleConfig { language: ProgrammingLanguage; code: string; } type RuleConfig = ExistsRuleConfig | ConsistencyRuleConfig | FormatRuleConfig | LogicRuleConfig | RegexRuleConfig | AIRuleConfig | CodeRuleConfig; export default function RuleNew() { const navigate = useNavigate(); const location = useLocation(); const [isEditMode, setIsEditMode] = useState(false); const [isLoading, setIsLoading] = useState(false); const [instanceKey, setInstanceKey] = useState('new'); const [userRole, setUserRole] = useState('common'); const [formData, setFormData] = useState({}); const [evaluationPointGroups, setEvaluationPointGroups] = useState([]); // 检查用户是否为开发者角色 const isDeveloper = userRole === 'developer'; // 添加用于共享的字段数据状态 const [extractionFields, setExtractionFields] = useState([]); /** * 从表单数据中提取所有字段 * 用于编辑模式下初始化字段数据 */ const extractFieldsFromFormData = useCallback((data: EvaluationPoint) => { if (!data || !data.extraction_config) return []; const fields: string[] = []; const config = data.extraction_config; // 提取LLM字段 if (config.llm && Array.isArray(config.llm.fields)) { fields.push(...config.llm.fields); } // 提取VLM字段 if (config.vlm && Array.isArray(config.vlm.fields)) { const vlmFields = config.vlm.fields.map((f: string | { name: string, type: string }) => typeof f === 'string' ? f : f.name ); fields.push(...vlmFields); } // 提取正则字段 if (config.regex && Array.isArray(config.regex.fields)) { const regexFields = config.regex.fields.map((f: { field: string, pattern: string }) => f.field).filter(Boolean); fields.push(...regexFields); } return fields; }, []); /** * 重置表单数据到默认状态 */ const resetFormData = useCallback(() => { // console.log("重置表单数据到默认状态"); setFormData({ name: '', code: '', risk: 'low', is_enabled: true, description: '', references_laws: { name: '', articles: [], content: '' }, evaluation_point_groups_pid: null, evaluation_point_groups_id: null, extraction_config: { llm: { fields: [], prompt_setting: { type: 'system', template: '' } }, vlm: { fields: [], prompt_setting: { type: 'system', template: '' } }, regex: { fields: [] } }, evaluation_config: { logicType: 'and', customLogic: '', rules: [] }, pass_message: '文档检查通过,符合规范要求。', fail_message: '文档存在以下问题,请修改后重新提交。', suggestion_message: '', suggestion_message_type: 'warning', post_action: 'none', action_config: '', score: 0 }); setExtractionFields([]); // 生成新的实例键,强制所有子组件重新渲染 setInstanceKey(`new_${Date.now()}`); }, []); /** * 获取评查点数据 * 编辑模式下从API获取指定ID的评查点数据 * @param id 评查点ID */ const fetchEvaluationPoint = useCallback(async (id: number) => { try { setIsLoading(true); // console.log(`获取评查点数据,ID: ${id}`); // 使用 postgrestGet 替代直接调用 fetch const postgrestParams = { filter: { 'id': `eq.${id}` } }; const response = await postgrestGet('evaluation_points', postgrestParams); if (response.data && Array.isArray(response.data) && response.data[0]) { if (response.data.length > 0) { try { // 使用JSON序列化和反序列化来进行深拷贝,避免浏览器差异 const originalData = response.data[0]; const jsonString = JSON.stringify(originalData); const data = JSON.parse(jsonString); // console.log("数据已经过深拷贝处理,避免浏览器兼容性问题"); // 设置表单数据 setFormData(data); // 初始化extractionFields const extractedFields = extractFieldsFromFormData(data); setExtractionFields(extractedFields); // 设置编辑模式的实例键 setInstanceKey(`edit_${id}_${Date.now()}`); } catch (jsonError) { console.error('JSON处理错误:', jsonError); toastService.error(`数据处理错误: ${jsonError instanceof Error ? jsonError.message : '未知错误'}`); resetFormData(); navigate('/rules'); } } else { console.error('获取数据失败: 返回数据为空'); toastService.error('获取数据失败: 返回数据为空'); resetFormData(); navigate('/rules'); } } else { throw new Error(`响应状态: ${response.error}`); } } catch (error) { console.error('获取评查点数据失败:', error); toastService.error(`获取评查点数据失败: ${error instanceof Error ? error.message : '未知错误'}`); // 获取数据失败时返回上一页 resetFormData(); navigate('/rules'); } finally { setIsLoading(false); } }, [navigate, extractFieldsFromFormData, resetFormData]); /** * 获取评查点组数据 * 从API获取所有评查点组,用于基本信息表单中选择 */ const fetchEvaluationPointGroups = useCallback(async () => { try { // console.log("获取评查点组数据"); const response = await postgrestGet('evaluation_point_groups'); if (response.data && Array.isArray(response.data) && response.data.length > 0) { setEvaluationPointGroups(response.data); } } catch (error) { console.error('获取评查点组数据失败:', error); // 显示错误提示但不影响应用继续使用 toastService.error(`获取评查点组数据失败: ${error instanceof Error ? error.message : '未知错误'}\n将使用默认数据`); } }, []); const handleSave = async () => { // console.log("保存评查点", formData); // 验证必填字段 if (!formData.name?.trim()) { toastService.warning("评查点名称不能为空"); return; } if (!formData.code?.trim()) { toastService.warning("评查点编码不能为空"); return; } // 显示保存中状态 setIsLoading(true); try { // 创建一个符合数据库模式的数据副本 const cleanedData = { id: formData.id, name: formData.name?.trim(), code: formData.code?.trim(), risk: formData.risk || 'low', is_enabled: formData.is_enabled !== undefined ? formData.is_enabled : true, description: formData.description || '', references_laws: formData.references_laws || null, evaluation_point_groups_pid: formData.evaluation_point_groups_pid || null, evaluation_point_groups_id: formData.evaluation_point_groups_id || null, extraction_config: { llm: { fields: Array.isArray(formData.extraction_config?.llm?.fields) ? [...formData.extraction_config.llm.fields] : [], prompt_setting: { type: formData.extraction_config?.llm?.prompt_setting?.type || 'system', template: formData.extraction_config?.llm?.prompt_setting?.template || '' } }, vlm: { fields: Array.isArray(formData.extraction_config?.vlm?.fields) ? [...formData.extraction_config.vlm.fields] : [], prompt_setting: { type: formData.extraction_config?.vlm?.prompt_setting?.type || 'system', template: formData.extraction_config?.vlm?.prompt_setting?.template || '' } }, regex: { fields: Array.isArray(formData.extraction_config?.regex?.fields) ? [...formData.extraction_config.regex.fields] : [] } }, evaluation_config: { logicType: formData.evaluation_config?.logicType || 'and', customLogic: formData.evaluation_config?.customLogic || '', rules: Array.isArray(formData.evaluation_config?.rules) ? formData.evaluation_config.rules.map(rule => ({ id: rule.id || '1', type: rule.type || '', config: rule.config || {} })) : [] }, pass_message: formData.pass_message || '文档检查通过,符合规范要求。', fail_message: formData.fail_message || '文档存在以下问题,请修改后重新提交。', suggestion_message: formData.suggestion_message || '', suggestion_message_type: formData.suggestion_message_type || 'warning', post_action: formData.post_action || 'none', action_config: formData.action_config || '', score: formData.score !== undefined ? Number(formData.score) : 0 }; // 获取当前所有有效的抽取字段 const currentExtractionFields = extractionFields.map(field => { // 处理字段名,去掉类型后缀(如果有) if (field.includes('_')) { return field.split('_')[0]; } return field; }); // 去重,确保不会有重复字段 const validFields = [...new Set(currentExtractionFields)]; // console.log("当前有效的抽取字段:", validFields); // 重要:这段代码解决了字段删除后,评查配置中仍保留已删除字段的问题 // 在保存前,我们会确保所有规则中引用的字段都是当前有效的抽取字段 // 这样即使用户在界面操作中删除了某个抽取字段,相关的评查规则也会被自动清理 // 确保rules中的每个配置对象都被正确处理 if (cleanedData.evaluation_config && Array.isArray(cleanedData.evaluation_config.rules)) { cleanedData.evaluation_config.rules = cleanedData.evaluation_config.rules .filter(rule => rule && rule.type) // 确保规则有类型 .map(rule => { // 根据规则类型确保config中有必要的字段 const config = { ...rule.config } as RuleConfig; switch (rule.type) { case 'exists': if (!Array.isArray((config as ExistsRuleConfig).fields)) (config as ExistsRuleConfig).fields = []; // 过滤掉不存在于当前抽取字段中的字段 if (Array.isArray((config as ExistsRuleConfig).fields)) { (config as ExistsRuleConfig).fields = (config as ExistsRuleConfig).fields.filter( field => validFields.includes(field) ); } if (!(config as ExistsRuleConfig).logic) (config as ExistsRuleConfig).logic = 'and'; // 删除不必要的字段 delete (config as ExistsRuleConfig & {availableFields?: string}).availableFields; delete (config as ExistsRuleConfig).selectedFields; delete (config as ExistsRuleConfig).existsLogic; break; case 'consistency': if (!Array.isArray((config as ConsistencyRuleConfig).pairs)) (config as ConsistencyRuleConfig).pairs = []; // 过滤掉包含不存在于当前抽取字段中的字段的配对 if (Array.isArray((config as ConsistencyRuleConfig).pairs)) { (config as ConsistencyRuleConfig).pairs = (config as ConsistencyRuleConfig).pairs.filter( pair => validFields.includes(pair.sourceField) && validFields.includes(pair.targetField) ); } if (!(config as ConsistencyRuleConfig).logic) (config as ConsistencyRuleConfig).logic = 'and'; delete (config as ConsistencyRuleConfig & {availableFields?: string}).availableFields; delete (config as ConsistencyRuleConfig).logicRelation; delete (config as ConsistencyRuleConfig).initialSourceField; delete (config as ConsistencyRuleConfig).initialTargetField; delete (config as ConsistencyRuleConfig).initialCompareMethod; break; case 'format': // 检查字段是否存在于当前抽取字段中 if ((config as FormatRuleConfig).field && !validFields.includes((config as FormatRuleConfig).field)) { (config as FormatRuleConfig).field = ''; } if (!(config as FormatRuleConfig).field) (config as FormatRuleConfig).field = ''; if (!(config as FormatRuleConfig).formatType) (config as FormatRuleConfig).formatType = 'date'; if (!(config as FormatRuleConfig).parameters) (config as FormatRuleConfig).parameters = ''; delete (config as FormatRuleConfig & {availableFields?: string}).availableFields; delete (config as FormatRuleConfig).checkField; delete (config as FormatRuleConfig).formatParams; break; case 'logic': if (!Array.isArray((config as LogicRuleConfig).conditions)) (config as LogicRuleConfig).conditions = []; // 过滤掉包含不存在于当前抽取字段中的字段的条件 if (Array.isArray((config as LogicRuleConfig).conditions)) { (config as LogicRuleConfig).conditions = (config as LogicRuleConfig).conditions.filter( condition => validFields.includes(condition.field) ); } if (!(config as LogicRuleConfig).logic) (config as LogicRuleConfig).logic = 'and'; delete (config as LogicRuleConfig & {availableFields?: string}).availableFields; delete (config as LogicRuleConfig).logicRelation; delete (config as LogicRuleConfig).initialField; delete (config as LogicRuleConfig).initialOperator; delete (config as LogicRuleConfig).initialValue; break; case 'regex': // 检查字段是否存在于当前抽取字段中 if ((config as RegexRuleConfig).field && !validFields.includes((config as RegexRuleConfig).field)) { (config as RegexRuleConfig).field = ''; } if (!(config as RegexRuleConfig).field) (config as RegexRuleConfig).field = ''; if (!(config as RegexRuleConfig).pattern) (config as RegexRuleConfig).pattern = ''; if (!(config as RegexRuleConfig).matchType) (config as RegexRuleConfig).matchType = 'match'; delete (config as RegexRuleConfig & {availableFields?: string}).availableFields; delete (config as RegexRuleConfig).checkField; delete (config as RegexRuleConfig).regexPattern; break; case 'ai': if (!(config as AIRuleConfig).model) (config as AIRuleConfig).model = 'qwen14b'; if (typeof (config as AIRuleConfig).temperature !== 'number') (config as AIRuleConfig).temperature = 0.1; if (!(config as AIRuleConfig).prompt) (config as AIRuleConfig).prompt = ''; delete (config as AIRuleConfig & {availableFields?: string}).availableFields; break; case 'code': if (!(config as CodeRuleConfig).language) (config as CodeRuleConfig).language = 'javascript'; if (!(config as CodeRuleConfig).code) (config as CodeRuleConfig).code = ''; delete (config as CodeRuleConfig & {availableFields?: string}).availableFields; break; } return { id: rule.id, type: rule.type, config }; }); } // console.log("当前评查配置-----------------:", formData.evaluation_config); // 如果是新建模式,则删除id字段 if (!isEditMode) { delete cleanedData.id; } try { // 使用JSON序列化和反序列化来进行深拷贝,避免浏览器差异 const jsonString = JSON.stringify(cleanedData); const finalData = JSON.parse(jsonString); // 确保extraction_config和evaluation_config是有效的JSON对象 try { JSON.parse(JSON.stringify(finalData.extraction_config)); } catch (e) { throw new Error("extraction_config 格式无效"); } try { JSON.parse(JSON.stringify(finalData.evaluation_config)); } catch (e) { throw new Error("evaluation_config 格式无效"); } // 检查JSON字符串长度,如果太长可能会被截断 const dataLength = jsonString.length; const maxLength = 65536; // 通常PostgreSQL的jsonb列可以存储的最大长度 if (dataLength > maxLength) { throw new Error(`数据大小超过限制 (${dataLength} > ${maxLength})`); } // console.log("准备提交到API的数据(已经过深拷贝处理):", finalData); // console.log("JSON数据长度:", dataLength); let response; if (isEditMode) { response = await postgrestPut('evaluation_points', finalData, {id: formData.id!}); } else { response = await postgrestPost('evaluation_points', finalData); } if (response.error) { if (response.error.includes('evaluation_points_code_key')) { toastService.error('在基本信息中:评查点编码已存在,请修改后保存。'); } else { toastService.error(`系统繁忙: ${response.error}`); } setIsLoading(false); } else if (response.data && Array.isArray(response.data) && response.data.length > 0) { // 获取新创建或更新的评查点ID const savedPointId = response.data[0]?.id; if (savedPointId) { // 显示成功消息 toastService.success(`评查点${isEditMode ? '更新' : '创建'}成功!`); // 保存成功后跳转到编辑页面并重新加载数据 navigate(`/rules-new?id=${savedPointId}`, { replace: true }); // 重新获取评查点数据 await fetchEvaluationPoint(savedPointId); } else { // 无法获取ID的情况 toastService.warning(`评查点${isEditMode ? '更新' : '创建'}成功,但无法获取ID。正在返回列表页面。`); navigate('/rules'); } } else { toastService.error('系统繁忙'); } } catch (jsonError) { console.error("JSON处理错误:", jsonError); toastService.error(`数据处理错误: ${jsonError instanceof Error ? jsonError.message : '未知错误'}`); setIsLoading(false); return; } } catch (error) { console.error("数据处理错误:", error); toastService.error(`数据处理错误: ${error instanceof Error ? error.message : '未知错误'}`); setIsLoading(false); } } const handleSaveDraft = () => { console.log("保存草稿"); } const handleBasicInfoChange = (data: Record) => { setFormData(prev => ({ ...prev, ...data })); } const handleExtractionSettingsChange = (data: Record) => { setFormData(prev => ({ ...prev, ...data })); // 提取并处理字段数据 const processFields = () => { // 使用类型断言处理字段访问 const extractionConfig = data.extraction_config as { llm?: { fields?: string[] }; vlm?: { fields?: Array }; regex?: { fields?: Array<{ field: string, pattern: string }> }; } | undefined; if (!extractionConfig) return; const llmFields = extractionConfig.llm?.fields || []; const vlmFields = extractionConfig.vlm?.fields || []; const regexFields = (extractionConfig.regex?.fields || []).map((f: { field: string }) => f.field); // 合并所有字段 const allFields = [ ...llmFields, ...vlmFields.map((f: string | { name: string }) => typeof f === 'string' ? f : f.name), ...regexFields ].filter(Boolean); setExtractionFields(allFields); }; // 处理字段数据 if (data.extraction_config) { processFields(); } }; const handleReviewSettingsChange = (data: Record) => { // console.log("评查设置变更:", data); // 检查数据中是否包含evaluation_config对象 if (data.evaluation_config) { // 确保formData.evaluation_config存在并具有必要的默认属性 const currentConfig = formData.evaluation_config || { logicType: 'and', customLogic: '', rules: [] }; // console.log("当前评查配置:", currentConfig); // console.log("变更评查配置:", data.evaluation_config); // 合并评查配置数据 const mergedConfig = { ...currentConfig, ...(data.evaluation_config as object) }; // console.log("合并评查配置:", data.evaluation_config); // 更新表单数据 setFormData(prev => ({ ...prev, evaluation_config: mergedConfig // evaluation_config: data.evaluation_config as typeof prev.evaluation_config })); } else { // 处理其他普通字段 setFormData(prev => ({ ...prev, ...data })); } } /** * 添加事件监听,处理抽取字段更新 */ useEffect(() => { // 定义事件处理函数 const handleExtractionFieldsUpdated = (event: CustomEvent<{fields: string[]}>) => { const { fields } = event.detail; if (Array.isArray(fields) && fields.length > 0) { // 更新字段数据 setExtractionFields(fields); } }; // 添加事件监听 window.addEventListener('extraction-fields-updated', handleExtractionFieldsUpdated as EventListener); // 清理函数 return () => { window.removeEventListener('extraction-fields-updated', handleExtractionFieldsUpdated as EventListener); }; }, []); /** * 页面加载时初始化处理 * 1. 检查URL参数,判断是新建还是编辑模式 * 2. 编辑模式下获取评查点数据 * 3. 获取评查点组数据(用于表单选择项) */ useEffect(() => { const searchParams = new URLSearchParams(location.search); const id = searchParams.get('id'); const mode = searchParams.get('mode'); // 从sessionStorage获取用户角色 if (typeof window !== 'undefined') { const userRoleFromSession = sessionStorage.getItem('userRole') as UserRole || 'common'; setUserRole(userRoleFromSession); } // 编辑或复制模式下设置加载状态 if (id || mode === 'copy') { setIsLoading(true); } // 设置编辑模式 if (mode && mode === 'copy') { setIsEditMode(false); } else { setIsEditMode(!!id); } if (id) { // 编辑模式:获取数据 fetchEvaluationPoint(parseInt(id)); } else { // 新建模式:重置表单数据 resetFormData(); } // 获取评查点组数据 fetchEvaluationPointGroups(); }, [location.search, fetchEvaluationPoint, fetchEvaluationPointGroups, resetFormData]); // 渲染页面内容 return (
{/* 页面标题和右上角保存按钮 */} {/* 加载状态显示 */} {isLoading ? (
加载中...
) : ( {/* 使用key属性强制在模式切换时重新渲染所有组件 */}
{/* 评查点基本信息设置 */}
{/* 抽取设置 - 配置从文档中提取的字段 */}
{/* 评查设置 - 配置评查规则、消息等 */}
{/* 底部操作按钮区域 */}
)}
); }