diff --git a/backend/app/api/contracts.py b/backend/app/api/contracts.py index fc851754..15f91628 100644 --- a/backend/app/api/contracts.py +++ b/backend/app/api/contracts.py @@ -2,7 +2,7 @@ from datetime import datetime from typing import Optional from urllib.parse import quote -from fastapi import APIRouter, Depends, Form, Header, HTTPException, Request, Response +from fastapi import APIRouter, BackgroundTasks, Depends, Form, Header, HTTPException, Request, Response from pydantic import BaseModel, Field from sqlalchemy.orm import Session @@ -145,6 +145,7 @@ def list_available_templates( @router.post("/generate") def generate_contract( request: Request, + background_tasks: BackgroundTasks, template_id: Optional[str] = Form(default=None), variable_values_json: Optional[str] = Form(default=None), export_format: str = Form(default="docx"), @@ -183,7 +184,7 @@ def generate_contract( raise HTTPException(status_code=400, detail=f"不支持的导出格式: {export_format}") tenant_service.assert_billing_allows(current_user, "generation") - tenant_service.assert_billing_allows(current_user, "review") + if elements_snapshot is not None: if not document_type_code: raise HTTPException(status_code=400, detail="document_type_code is required when elements_json is provided") @@ -238,6 +239,14 @@ def generate_contract( user_agent=request.headers.get("user-agent"), ) + if result.get("id"): + background_tasks.add_task( + contract_generation_service._execute_deferred_auto_review, + result["id"], + current_user["tenant_id"], + current_user, + ) + return success_response(result) except json.JSONDecodeError: diff --git a/backend/app/api/prompt_assets.py b/backend/app/api/prompt_assets.py index a426e740..a2a69d60 100644 --- a/backend/app/api/prompt_assets.py +++ b/backend/app/api/prompt_assets.py @@ -21,6 +21,8 @@ ROUTE_PERMISSION_ACTIONS = { "unpublish": "prompt_assets:unpublish", "rollback": "prompt_assets:rollback", "record_run_link": "prompt_assets:run_links:create", + "first_priority_blueprint": "prompt_assets:first_priority_blueprint", + "seed_first_priority_blueprint": "prompt_assets:first_priority_blueprint:seed", } @@ -51,6 +53,33 @@ def list_prompt_assets( return success_response(prompt_asset_service.list_prompt_assets(current_user, page, page_size, prompt_type, status_filter)) + +@router.get("/first-priority-blueprint") +def get_first_priority_blueprint( + prompt_asset_service: PromptAssetService = Depends(get_prompt_asset_service), + current_user: dict = Depends(require_current_user), +): + return success_response(prompt_asset_service.get_first_priority_blueprint(current_user)) + + +@router.post("/first-priority-blueprint/seed") +def seed_first_priority_blueprint( + prompt_asset_service: PromptAssetService = Depends(get_prompt_asset_service), + audit_log_service: AuditLogService = Depends(get_audit_log_service), + current_user: dict = Depends(require_current_user), +): + result = prompt_asset_service.seed_first_priority_blueprint(current_user) + audit_log_service.record_event( + tenant_id=current_user.get("tenant_id"), + user_id=current_user.get("id"), + action="seed_first_priority_prompt_blueprint", + resource_type="prompt_asset", + resource_id="first_priority_blueprint", + detail_json=result.get("summary") or {}, + ) + return success_response(result) + + @router.get("/{asset_id}") def get_prompt_asset_detail( asset_id: str, diff --git a/backend/app/main.py b/backend/app/main.py index c2e70403..0b8b72ba 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -28,6 +28,7 @@ from app.api.templates import router as templates_router from app.api.tenant_settings import router as tenant_settings_router from app.api.users import router as users_router from app.core.config import settings +from app.core.exceptions import AppError from app.core.response import error_response, success_response app = FastAPI(title=settings.app_name) @@ -72,6 +73,11 @@ async def http_exception_handler(_request, exc: HTTPException): return error_response(exc.detail, exc.status_code, exc.status_code) +@app.exception_handler(AppError) +async def app_error_handler(_request, exc: AppError): + return error_response(exc.detail, exc.status_code, exc.status_code) + + @app.exception_handler(RequestValidationError) async def validation_exception_handler(_request, exc: RequestValidationError): first_error = exc.errors()[0] if exc.errors() else None diff --git a/backend/app/services/legal_document_element_schema_service.py b/backend/app/services/legal_document_element_schema_service.py index b024577e..dc289066 100644 --- a/backend/app/services/legal_document_element_schema_service.py +++ b/backend/app/services/legal_document_element_schema_service.py @@ -35,10 +35,14 @@ def _party_section() -> dict[str, Any]: "key": "parties", "title": "当事人信息", "fields": [ + {"key": "contract.no", "label": "合同编号", "type": "text", "required": False}, + {"key": "contract.subject", "label": "交易背景/合同目的", "type": "textarea", "required": False}, {"key": "party_a.name", "label": "甲方名称", "type": "text", "required": True}, {"key": "party_a.contact", "label": "甲方联系人", "type": "text", "required": False}, + {"key": "party_a.address", "label": "甲方住所/联系地址", "type": "text", "required": False}, {"key": "party_b.name", "label": "乙方名称", "type": "text", "required": True}, {"key": "party_b.contact", "label": "乙方联系人", "type": "text", "required": False}, + {"key": "party_b.address", "label": "乙方住所/联系地址", "type": "text", "required": False}, ], } @@ -59,6 +63,7 @@ DEFAULT_ELEMENT_SCHEMAS: dict[str, dict[str, Any]] = { {"key": "price.total", "label": "合同总价", "type": "money", "required": True}, {"key": "quality.standard", "label": "质量标准", "type": "textarea", "required": True}, {"key": "packaging.transport", "label": "包装及运输要求", "type": "textarea", "required": True}, + {"key": "additional_clauses", "label": "希望自动加入的补充条款", "type": "checkbox", "required": False}, ], }, { @@ -105,6 +110,8 @@ DEFAULT_ELEMENT_SCHEMAS: dict[str, dict[str, Any]] = { {"key": "liquidated_damages.enabled", "label": "是否约定违约金", "type": "clause_toggle", "required": False}, {"key": "liquidated_damages.formula", "label": "违约金计算口径", "type": "textarea", "required": False}, {"key": "liability.cap", "label": "责任上限", "type": "textarea", "required": False}, + {"key": "force_majeure.enabled", "label": "是否加入不可抗力条款", "type": "clause_toggle", "required": False}, + {"key": "notice.address", "label": "通知方式及送达地址", "type": "textarea", "required": False}, { "key": "dispute_resolution.method", "label": "争议解决方式", @@ -134,9 +141,41 @@ DEFAULT_ELEMENT_SCHEMAS: dict[str, dict[str, Any]] = { "fields": [ {"key": "service.name", "label": "服务项目名称", "type": "text", "required": True}, {"key": "service.scope", "label": "服务范围", "type": "textarea", "required": True}, + {"key": "service.exclusions", "label": "不包含事项", "type": "textarea", "required": False}, {"key": "deliverables", "label": "交付成果", "type": "textarea", "required": True}, + {"key": "service.period", "label": "服务期限", "type": "text", "required": True}, + {"key": "milestones", "label": "阶段里程碑", "type": "textarea", "required": False}, + ], + }, + { + "key": "commercial_acceptance", + "title": "费用、付款与验收", + "fields": [ {"key": "service_fee", "label": "服务费用", "type": "money", "required": True}, - {"key": "acceptance.date", "label": "验收日期", "type": "date", "required": False}, + {"key": "payment_terms", "label": "付款安排", "type": "textarea", "required": True}, + {"key": "acceptance.standard", "label": "验收标准", "type": "textarea", "required": True}, + {"key": "acceptance.period", "label": "验收期限", "type": "text", "required": False}, + ], + }, + { + "key": "technology_risk_controls", + "title": "知识产权、保密与风险控制", + "fields": [ + {"key": "ip_ownership", "label": "知识产权归属", "type": "select", "required": True, "options": ["甲方所有", "乙方所有", "双方共有", "许可使用", "另行约定"]}, + {"key": "confidentiality.scope", "label": "保密范围", "type": "textarea", "required": True}, + {"key": "breach_liability", "label": "违约责任", "type": "textarea", "required": True}, + {"key": "liability.cap", "label": "责任上限", "type": "textarea", "required": False}, + {"key": "data.personal_info_involved", "label": "是否涉及个人信息/重要数据", "type": "checkbox", "required": False}, + ], + }, + { + "key": "dispute_and_signature", + "title": "争议解决与签署", + "fields": [ + {"key": "dispute_resolution.method", "label": "争议解决方式", "type": "select", "required": True, "options": ["诉讼", "仲裁"]}, + {"key": "notice.address", "label": "通知方式及送达地址", "type": "textarea", "required": False}, + {"key": "sign_date", "label": "签署日期", "type": "date", "required": True}, + {"key": "e_signature_platform", "label": "电子签署平台", "type": "text", "required": False}, ], }, ] @@ -238,6 +277,7 @@ DEFAULT_ELEMENT_SCHEMAS: dict[str, dict[str, Any]] = { {"key": "employer.name", "label": "用人单位名称", "type": "text", "required": True}, {"key": "employee.name", "label": "员工姓名", "type": "text", "required": True}, {"key": "employee.id_no", "label": "身份证号", "type": "text", "required": True}, + {"key": "work_time_system", "label": "工时制度", "type": "select", "required": True, "options": ["标准工时制", "综合计算工时制", "不定时工作制"]}, ], }, { @@ -250,7 +290,12 @@ DEFAULT_ELEMENT_SCHEMAS: dict[str, dict[str, Any]] = { {"key": "work_location", "label": "工作地点", "type": "text", "required": True}, {"key": "salary.monthly", "label": "月工资", "type": "money", "required": True}, {"key": "probation.months", "label": "试用期(月)", "type": "number", "required": False}, + {"key": "probation.salary", "label": "试用期工资", "type": "money", "required": False}, {"key": "social_security.enabled", "label": "是否缴纳社保", "type": "checkbox", "required": True}, + {"key": "overtime.pay_rule", "label": "加班工资规则", "type": "textarea", "required": False}, + {"key": "non_compete.enabled", "label": "是否包含竞业限制", "type": "clause_toggle", "required": False}, + {"key": "non_compete.compensation", "label": "竞业限制补偿", "type": "textarea", "required": False}, + {"key": "confidentiality.scope", "label": "保密范围", "type": "textarea", "required": False}, ], }, ] @@ -376,13 +421,46 @@ DEFAULT_ELEMENT_SCHEMAS: dict[str, dict[str, Any]] = { "sections": [ _party_section(), { - "key": "equity", - "title": "股权转让", + "key": "target_company", + "title": "目标公司与股权结构", "fields": [ {"key": "company.name", "label": "目标公司", "type": "text", "required": True}, + {"key": "company.registered_capital", "label": "注册资本", "type": "money", "required": False}, + {"key": "current_shareholding", "label": "当前股权结构", "type": "textarea", "required": True}, {"key": "equity.percent", "label": "转让股权比例", "type": "percentage", "required": True}, + {"key": "equity.paid_in_status", "label": "出资实缴情况", "type": "textarea", "required": True}, + ], + }, + { + "key": "price_and_closing", + "title": "价款、支付与交割", + "fields": [ {"key": "transfer.price", "label": "转让价款", "type": "money", "required": True}, + {"key": "pricing_basis", "label": "定价依据", "type": "textarea", "required": True}, + {"key": "payment_terms", "label": "付款安排", "type": "textarea", "required": True}, + {"key": "closing.conditions", "label": "交割先决条件", "type": "textarea", "required": True}, {"key": "closing.date", "label": "交割日期", "type": "date", "required": True}, + {"key": "registration.change_deadline", "label": "工商变更期限", "type": "text", "required": True}, + ], + }, + { + "key": "risk_and_disclosure", + "title": "陈述保证与风险控制", + "fields": [ + {"key": "shareholder_consent", "label": "其他股东同意/优先购买权安排", "type": "textarea", "required": True}, + {"key": "representations_warranties", "label": "陈述与保证", "type": "textarea", "required": True}, + {"key": "tax_arrangement", "label": "税费承担与代扣代缴", "type": "textarea", "required": True}, + {"key": "transition_period", "label": "过渡期安排", "type": "textarea", "required": False}, + {"key": "breach_liability", "label": "违约责任", "type": "textarea", "required": True}, + ], + }, + { + "key": "dispute_and_signature", + "title": "争议解决与签署", + "fields": [ + {"key": "dispute_resolution.method", "label": "争议解决方式", "type": "select", "required": True, "options": ["诉讼", "仲裁"]}, + {"key": "sign_date", "label": "签署日期", "type": "date", "required": True}, + {"key": "lawyer_review_notice", "label": "是否已安排律师/税务复核", "type": "checkbox", "required": False}, ], }, ] diff --git a/backend/app/services/legal_domain_registry.py b/backend/app/services/legal_domain_registry.py index 6d80d7c3..d784405d 100644 --- a/backend/app/services/legal_domain_registry.py +++ b/backend/app/services/legal_domain_registry.py @@ -1,7 +1,65 @@ from __future__ import annotations +import re from typing import Optional +CORE_LEGAL_DOCUMENT_ENHANCEMENT_PACKAGE = { + "version": "ai_legal_document_assistant.v1", + "source": "AI法律文书助手融合方案第一版", + "document_type_codes": [ + "procurement_goods", + "technical_service", + "labor_contract", + "nda_commercial", + "equity_transfer", + ], + "review_dimensions": [ + "document_type_identification", + "structure_completeness", + "clause_risk_scan", + "rights_obligations_balance", + "regulatory_compliance", + "missing_clause_detection", + "wording_optimization", + ], + "prompt_asset_keys": { + "contract_generation": [ + "contract_generation_default", + "procurement_goods", + "technical_service", + "labor_contract", + "nda_commercial", + "equity_transfer", + ], + "contract_review": [ + "contract_review_default", + "contract_review_employment_hr", + "contract_review_investment_finance", + "contract_review_tech_saas_data_compliance", + ], + "clause_recommendation": [ + "clause_missing_default", + "clause_wording_optimization_default", + "clause_risk_guardrail_default", + ], + }, + "quality_targets": { + "required_clause_coverage": 0.95, + "risk_recall": 0.90, + "risk_precision": 0.85, + "risk_level_accuracy": 0.80, + "legal_citation_accuracy": 0.95, + "json_schema_compliance": 0.99, + "hallucination_rate_max": 0.02, + }, + "safety_boundaries": { + "ai_legal_advice_disclaimer": True, + "high_value_document_lawyer_review_notice": True, + "user_contract_data_training_opt_in_required": True, + "citation_must_match_legal_article_store": True, + }, +} + DOCUMENT_TYPE_LEGAL_QUERY_TERMS = { "procurement_goods": ["买卖合同", "货物采购", "交付", "风险转移", "验收", "质量", "发票", "付款", "质保", "违约", "民法典", "procurement", "breach"], "technical_service": ["技术服务合同", "知识产权", "验收", "违约", "民法典"], @@ -167,6 +225,285 @@ TEMPLATE_CATEGORY_LABELS = { } + + +# ── Review Domain Constants (from review_service) ── + +CONTRACT_TYPE_DISPLAY_MAP = { + "service": "服务合同", + "development": "技术开发合同", + "procurement": "采购合同", + "lease": "租赁合同", + "employment": "劳动合同", + "nda": "保密协议", + "framework": "合作框架协议", + "sales": "买卖合同", + "entrustment": "委托合同", + "loan": "借款合同", + "intercompany-loan": "企业间借款合同", + "investment": "投资协议", + "valuation-adjustment": "对赌协议", + "shareholder-resolution": "股东会决议", + "power-of-attorney": "授权委托书", + "legal-opinion": "法律意见书", + "property-service": "物业服务合同", + "license": "许可协议", +} +CONTRACT_TYPE_HINTS = { + "service": ["服务合同", "服务协议", "技术服务", "运维服务", "咨询服务"], + "development": ["开发合同", "技术开发", "软件开发", "系统开发"], + "procurement": ["采购合同", "采购协议", "供货合同", "买卖合同", "采购订单"], + "lease": ["租赁合同", "房屋租赁", "设备租赁"], + "employment": ["劳动合同", "聘用合同", "劳动协议"], + "nda": ["保密协议", "保密条款", "保密合同", "non-disclosure"], + "framework": ["框架协议", "合作框架", "战略合作"], + "sales": ["买卖合同", "销售合同", "供货合同", "购销合同"], + "entrustment": ["委托合同", "委托服务", "委托服务合同", "代理协议", "委托协议"], + "loan": ["借款合同", "借款协议", "贷款合同"], + "intercompany-loan": ["企业间借款", "借款合同", "借款协议", "关联交易", "利息", "担保"], + "investment": ["投资协议", "投融资协议", "增资协议", "估值", "交割", "治理权利", "退出安排"], + "valuation-adjustment": ["对赌协议", "估值调整", "业绩承诺", "补偿机制", "回购安排"], + "shareholder-resolution": ["股东会决议", "股东大会决议", "公司章程", "表决权", "召集通知", "工商变更"], + "power-of-attorney": ["授权委托书", "委托代理", "授权范围", "转委托", "签章", "电子签名"], + "legal-opinion": ["法律意见书", "律师事务所", "尽职调查", "核查验证", "法律分析", "结论意见"], + "property-service": ["物业服务合同", "物业管理", "物业费", "专项维修资金", "公共收益", "业主委员会"], + "license": ["许可协议", "授权协议", "知识产权许可", "软件许可"], +} +CLAUSE_TYPE_DEFINITIONS = [ + {"type": "payment", "label": "付款条款", "keywords": ["付款", "价款", "金额", "发票", "工资", "薪酬", "奖金", "结算", "服务费", "许可费", "invoice", "payment", "milestone", "billing", "fees"]}, + {"type": "acceptance", "label": "验收条款", "keywords": ["验收", "acceptance", "测试", "试运行", "上线", "验收标准"]}, + {"type": "confidentiality", "label": "保密条款", "keywords": ["保密", "confidential", "non-disclosure", "nda", "商业秘密"]}, + {"type": "intellectual_property", "label": "知识产权", "keywords": ["知识产权", "著作权", "源代码", "软件著作权", "专利", "商标", "技术成果", "intellectual property", "copyright", "ip"]}, + {"type": "breach", "label": "违约条款", "keywords": ["违约", "违约金", "赔偿", "liquidated damages", "default", "penalty"]}, + {"type": "delivery", "label": "交付条款", "keywords": ["交付", "交货", "deliver", "delivery", "成果", "上线", "部署"]}, + {"type": "liability", "label": "责任限制", "keywords": ["免责", "责任限制", "赔偿上限", "liability cap", "disclaimer", "exclusion of liability"]}, + {"type": "renewal", "label": "续约条款", "keywords": ["自动续约", "续约", "续期", "renewal", "evergreen"]}, + {"type": "termination", "label": "解除条款", "keywords": ["解除", "终止", "termination", "terminate"]}, + {"type": "dispute_resolution", "label": "争议解决", "keywords": ["争议解决", "仲裁", "管辖", "法院", "arbitration", "jurisdiction"]}, +] +CLAUSE_TYPE_LABEL_MAP = {item["type"]: item["label"] for item in CLAUSE_TYPE_DEFINITIONS} +STRUCTURE_SECTION_ORDER = ["payment", "acceptance", "delivery", "confidentiality", "intellectual_property", "breach", "liability", "renewal", "termination", "dispute_resolution"] +STRUCTURE_MISSING_SECTION_RULES = [ + {"type": "payment", "message": "付款与结算安排"}, + {"type": "acceptance", "message": "验收标准或验收流程"}, + {"type": "confidentiality", "message": "保密范围与期限"}, + {"type": "breach", "message": "违约责任安排"}, + {"type": "termination", "message": "解除或终止条款"}, + {"type": "dispute_resolution", "message": "争议解决条款"}, +] +PARTY_NAME_PATTERNS = { + "party_a": [ + re.compile(r"(?:甲方|Party\s*A)\s*[::]\s*([^\n,,;;]{2,40})", re.IGNORECASE), + ], + "party_b": [ + re.compile(r"(?:乙方|Party\s*B)\s*[::]\s*([^\n,,;;]{2,40})", re.IGNORECASE), + ], +} +AMOUNT_PATTERNS = [ + re.compile(r"(?:合同总价|合同金额|总价|价款|金额)\s*[::]?\s*((?:人民币)?[¥¥]?\s?[\d,]+(?:\.\d+)?(?:元|万元|万|RMB)?)", re.IGNORECASE), +] +TERM_PATTERNS = [ + re.compile(r"(?:合同期限|服务期限|履行期限|期限)\s*[::]?\s*([^\n]{4,40})", re.IGNORECASE), + re.compile(r"(自[^。\n]{4,40}至[^。\n]{4,40})"), +] +DATE_RANGE_PATTERN = re.compile( + r"自\s*(\d{4}年\d{1,2}月\d{1,2}日|\d{4}[/-]\d{1,2}[/-]\d{1,2})\s*至\s*(\d{4}年\d{1,2}月\d{1,2}日|\d{4}[/-]\d{1,2}[/-]\d{1,2})" +) +DATE_POINT_PATTERN = re.compile(r"(\d{4}年\d{1,2}月\d{1,2}日|\d{4}[/-]\d{1,2}[/-]\d{1,2})") +PAYMENT_DUE_PATTERN = re.compile(r"(\d{1,3})\s*(个)?\s*(工作日|自然日|日|天)内") +PAYMENT_RATIO_PATTERN = re.compile(r"(\d{1,3})\s*%") +PAYMENT_TRIGGER_HINTS = [ + ("合同签署", "contract_signed"), + ("签署后", "contract_signed"), + ("验收合格", "acceptance_passed"), + ("验收完成", "acceptance_passed"), + ("收到发票", "invoice_received"), + ("发票", "invoice_received"), + ("交货", "delivery_completed"), + ("交付", "delivery_completed"), + ("上线", "go_live_completed"), + ("质保期满", "warranty_period_ended"), + ("质保期结束", "warranty_period_ended"), + ("质保金", "warranty_period_ended"), +] +PAYMENT_STAGE_ADVANCE = "advance" +PAYMENT_STAGE_ACCEPTANCE = "acceptance" +PAYMENT_STAGE_GO_LIVE = "go_live" +PAYMENT_STAGE_FINAL = "final" +PAYMENT_STAGE_WARRANTY = "warranty_retention" +PAYMENT_STAGE_MILESTONE = "milestone" +PAYMENT_LABEL_HINTS = [] +LIABILITY_CARVE_OUT_MAP = [] +IP_OWNERSHIP_PATTERNS = [] + +_REAL_CHINESE_CONTRACT_HINTS = { + "service": ["\u670d\u52a1\u5408\u540c", "\u670d\u52a1\u534f\u8bae", "\u6280\u672f\u670d\u52a1", "\u8fd0\u7ef4\u670d\u52a1", "\u54a8\u8be2\u670d\u52a1"], + "development": ["\u5f00\u53d1\u5408\u540c", "\u6280\u672f\u5f00\u53d1", "\u8f6f\u4ef6\u5f00\u53d1", "\u7cfb\u7edf\u5f00\u53d1"], + "procurement": ["\u91c7\u8d2d\u5408\u540c", "\u91c7\u8d2d\u534f\u8bae", "\u4f9b\u8d27\u5408\u540c", "\u4e70\u5356\u5408\u540c", "\u91c7\u8d2d\u8ba2\u5355"], + "lease": ["\u79df\u8d41\u5408\u540c", "\u623f\u5c4b\u79df\u8d41", "\u8bbe\u5907\u79df\u8d41"], + "employment": ["\u52b3\u52a8\u5408\u540c", "\u8058\u7528\u5408\u540c", "\u52b3\u52a8\u534f\u8bae"], + "nda": ["\u4fdd\u5bc6\u534f\u8bae", "\u4fdd\u5bc6\u6761\u6b3e", "\u4fdd\u5bc6\u5408\u540c"], + "framework": ["\u6846\u67b6\u534f\u8bae", "\u5408\u4f5c\u6846\u67b6", "\u6218\u7565\u5408\u4f5c"], + "sales": ["\u4e70\u5356\u5408\u540c", "\u9500\u552e\u5408\u540c", "\u4f9b\u8d27\u5408\u540c", "\u8d2d\u9500\u5408\u540c"], + "entrustment": ["\u59d4\u6258\u5408\u540c", "\u59d4\u6258\u670d\u52a1", "\u4ee3\u7406\u534f\u8bae", "\u59d4\u6258\u534f\u8bae"], + "loan": ["\u501f\u6b3e\u5408\u540c", "\u501f\u6b3e\u534f\u8bae", "\u8d37\u6b3e\u5408\u540c"], + "intercompany-loan": ["\u4f01\u4e1a\u95f4\u501f\u6b3e", "\u501f\u6b3e\u5408\u540c", "\u501f\u6b3e\u534f\u8bae", "\u5173\u8054\u4ea4\u6613", "\u5229\u606f", "\u62c5\u4fdd"], + "investment": ["\u6295\u8d44\u534f\u8bae", "\u6295\u878d\u8d44\u534f\u8bae", "\u589e\u8d44\u534f\u8bae", "\u4f30\u503c", "\u4ea4\u5272", "\u6cbb\u7406\u6743\u5229", "\u9000\u51fa\u5b89\u6392"], + "valuation-adjustment": ["\u5bf9\u8d4c\u534f\u8bae", "\u4f30\u503c\u8c03\u6574", "\u4e1a\u7ee9\u627f\u8bfa", "\u8865\u507f\u673a\u5236", "\u56de\u8d2d\u5b89\u6392"], + "shareholder-resolution": ["\u80a1\u4e1c\u4f1a\u51b3\u8bae", "\u80a1\u4e1c\u5927\u4f1a\u51b3\u8bae", "\u516c\u53f8\u7ae0\u7a0b", "\u8868\u51b3\u6743", "\u53ec\u96c6\u901a\u77e5", "\u5de5\u5546\u53d8\u66f4"], + "power-of-attorney": ["\u6388\u6743\u59d4\u6258\u4e66", "\u59d4\u6258\u4ee3\u7406", "\u6388\u6743\u8303\u56f4", "\u8f6c\u59d4\u6258", "\u7b7e\u7ae0", "\u7535\u5b50\u7b7e\u540d"], + "legal-opinion": ["\u6cd5\u5f8b\u610f\u89c1\u4e66", "\u5f8b\u5e08\u4e8b\u52a1\u6240", "\u5c3d\u804c\u8c03\u67e5", "\u6838\u67e5\u9a8c\u8bc1", "\u6cd5\u5f8b\u5206\u6790", "\u7ed3\u8bba\u610f\u89c1"], + "property-service": ["\u7269\u4e1a\u670d\u52a1\u5408\u540c", "\u7269\u4e1a\u7ba1\u7406", "\u7269\u4e1a\u8d39", "\u4e13\u9879\u7ef4\u4fee\u8d44\u91d1", "\u516c\u5171\u6536\u76ca", "\u4e1a\u4e3b\u59d4\u5458\u4f1a"], + "license": ["\u8bb8\u53ef\u534f\u8bae", "\u6388\u6743\u534f\u8bae", "\u77e5\u8bc6\u4ea7\u6743\u8bb8\u53ef", "\u8f6f\u4ef6\u8bb8\u53ef"], +} +for _contract_type, _hints in _REAL_CHINESE_CONTRACT_HINTS.items(): + existing = CONTRACT_TYPE_HINTS.setdefault(_contract_type, []) + for _hint in _hints: + if _hint not in existing: + existing.append(_hint) + +_REAL_CHINESE_CLAUSE_KEYWORDS = { + "payment": ["\u4ed8\u6b3e", "\u4ef7\u6b3e", "\u91d1\u989d", "\u53d1\u7968", "\u8d39\u7528", "\u5c3e\u6b3e", "\u8d28\u4fdd\u91d1", "\u9884\u4ed8\u6b3e"], + "acceptance": ["\u9a8c\u6536", "\u9a8c\u6536\u6807\u51c6", "\u4e66\u9762\u9a8c\u6536", "\u6d4b\u8bd5", "\u8bd5\u8fd0\u884c"], + "delivery": ["\u4ea4\u4ed8", "\u4ea4\u8d27", "\u6210\u679c", "\u90e8\u7f72", "\u4e0a\u7ebf"], + "confidentiality": ["\u4fdd\u5bc6", "\u5546\u4e1a\u79d8\u5bc6", "\u5ba2\u6237\u6570\u636e"], + "intellectual_property": ["\u77e5\u8bc6\u4ea7\u6743", "\u8457\u4f5c\u6743", "\u6e90\u4ee3\u7801", "\u8f6f\u4ef6\u8457\u4f5c\u6743", "\u4e13\u5229", "\u5546\u6807"], + "breach": ["\u8fdd\u7ea6", "\u8fdd\u7ea6\u91d1", "\u8d54\u507f"], + "liability": ["\u8d23\u4efb\u9650\u5236", "\u8d54\u507f\u4e0a\u9650", "\u6700\u9ad8\u8d23\u4efb", "\u4e0a\u9650"], + "dispute_resolution": ["\u4e89\u8bae\u89e3\u51b3", "\u4ef2\u88c1", "\u7ba1\u8f96", "\u6cd5\u9662"], +} +for _definition in CLAUSE_TYPE_DEFINITIONS: + for _keyword in _REAL_CHINESE_CLAUSE_KEYWORDS.get(_definition["type"], []): + if _keyword not in _definition["keywords"]: + _definition["keywords"].append(_keyword) + +for _trigger, _dependency in [ + ("\u5408\u540c\u7b7e\u7f72", "contract_signed"), + ("\u7b7e\u7f72\u540e", "contract_signed"), + ("\u9a8c\u6536\u5408\u683c", "acceptance_passed"), + ("\u9a8c\u6536\u5b8c\u6210", "acceptance_passed"), + ("\u6536\u5230\u53d1\u7968", "invoice_received"), + ("\u53d1\u7968", "invoice_received"), + ("\u4ea4\u8d27", "delivery_completed"), + ("\u4ea4\u4ed8", "delivery_completed"), + ("\u4e0a\u7ebf", "go_live_completed"), + ("\u8d28\u4fdd\u671f\u6ee1", "warranty_period_ended"), + ("\u8d28\u4fdd\u671f\u7ed3\u675f", "warranty_period_ended"), + ("\u8d28\u4fdd\u91d1", "warranty_period_ended"), +]: + if (_trigger, _dependency) not in PAYMENT_TRIGGER_HINTS: + PAYMENT_TRIGGER_HINTS.append((_trigger, _dependency)) + +for _token, _stage in [ + ("\u9884\u4ed8\u6b3e", PAYMENT_STAGE_ADVANCE), + ("\u9996\u671f\u6b3e", PAYMENT_STAGE_ADVANCE), + ("\u5b9a\u91d1", PAYMENT_STAGE_ADVANCE), + ("\u8fdb\u5ea6\u6b3e", PAYMENT_STAGE_MILESTONE), + ("\u5b9e\u65bd\u6b3e", PAYMENT_STAGE_MILESTONE), + ("\u5c3e\u6b3e", PAYMENT_STAGE_FINAL), + ("\u8d28\u4fdd\u91d1", PAYMENT_STAGE_WARRANTY), +]: + if (_token, _stage) not in PAYMENT_LABEL_HINTS: + PAYMENT_LABEL_HINTS.append((_token, _stage)) + +for _token, _label in [ + ("\u6545\u610f", "intentional_misconduct"), + ("\u91cd\u5927\u8fc7\u5931", "gross_negligence"), + ("\u4fdd\u5bc6", "confidentiality"), + ("\u5546\u4e1a\u79d8\u5bc6", "confidentiality"), + ("\u77e5\u8bc6\u4ea7\u6743", "intellectual_property"), + ("\u8457\u4f5c\u6743", "intellectual_property"), + ("\u6e90\u4ee3\u7801", "intellectual_property"), + ("\u8f6f\u4ef6\u8457\u4f5c\u6743", "intellectual_property"), + ("\u8fdd\u7ea6", "breach"), +]: + if (_token, _label) not in LIABILITY_CARVE_OUT_MAP: + LIABILITY_CARVE_OUT_MAP.append((_token, _label)) + +IP_OWNERSHIP_PATTERNS.extend( + [ + (re.compile(r"(\u77e5\u8bc6\u4ea7\u6743|\u8457\u4f5c\u6743|\u6e90\u4ee3\u7801|\u8f6f\u4ef6\u8457\u4f5c\u6743)[^\n。;;]*(\u5f52\u7532\u65b9\u6240\u6709|\u5f52\u5c5e\u4e8e\u7532\u65b9|\u7532\u65b9\u6240\u6709)", re.IGNORECASE), "party_a"), + (re.compile(r"(\u77e5\u8bc6\u4ea7\u6743|\u8457\u4f5c\u6743|\u6e90\u4ee3\u7801|\u8f6f\u4ef6\u8457\u4f5c\u6743)[^\n。;;]*(\u5f52\u4e59\u65b9\u6240\u6709|\u5f52\u5c5e\u4e8e\u4e59\u65b9|\u4e59\u65b9\u6240\u6709)", re.IGNORECASE), "party_b"), + (re.compile(r"(\u77e5\u8bc6\u4ea7\u6743|\u8457\u4f5c\u6743|\u6e90\u4ee3\u7801|\u8f6f\u4ef6\u8457\u4f5c\u6743)[^\n。;;]*(\u53cc\u65b9\u5171\u6709|\u5171\u540c\u6240\u6709|\u5171\u540c\u4eab\u6709)", re.IGNORECASE), "shared"), + (re.compile(r"(\u77e5\u8bc6\u4ea7\u6743|\u8457\u4f5c\u6743|\u6e90\u4ee3\u7801|\u8f6f\u4ef6\u8457\u4f5c\u6743)[^\n。;;]*(\u8bb8\u53ef\u7532\u65b9\u4f7f\u7528|\u6388\u6743\u7532\u65b9\u4f7f\u7528)", re.IGNORECASE), "licensed_to_party_a"), + ] +) +RISK_CERTAINTY_CONFIRMED = "confirmed" +RISK_CERTAINTY_POSSIBLE = "possible" +CLAUSE_HEADING_PATTERNS = [ + re.compile(r"^(第[一二三四五六七八九十百零]+条[\s、.::-]*)?(付款|价款|结算|发票|费用|保密|违约|赔偿|免责|责任限制|验收|交付|交货|履行|争议解决|仲裁|管辖|解除|终止|续约|期限|服务内容|项目范围|知识产权)[^\n]{0,24}[::]\s*(.*)$"), + re.compile(r"^(第[一二三四五六七八九十百零]+条)\s*(.*)$"), + re.compile(r"^([0-9]{1,2}[.、])\s*(.*)$"), + re.compile(r"^([((]?[一二三四五六七八九十0-9]{1,3}[))])\s*(.*)$"), +] +NON_CLAUSE_METADATA_PATTERNS = [ + re.compile(r"^(甲方|乙方|丙方|丁方|Party\s*A|Party\s*B)\s*[::]", re.IGNORECASE), + re.compile(r"^(合同金额|总价|金额|价税合计|服务期限|合同期限|履行期限|签署日期|签约日期|日期)\s*[::]"), +] +RELATION_STATUS_INFO = "info" +RELATION_STATUS_ATTENTION = "attention" +RELATION_CONFIDENCE_HIGH = "high" +RELATION_CONFIDENCE_WEAK = "weak" +LEGAL_ARTICLE_REFERENCE_RULES = [ + { + "triggers": ["违约金"], + "targets": [("LAW-CIVIL-CODE-2020", "第五百八十五条"), ("LAW-CIVIL-CODE-2020", "第五百七十七条")], + }, + { + "triggers": ["违约责任", "赔偿", "补救措施"], + "targets": [("LAW-CIVIL-CODE-2020", "第五百七十七条"), ("LAW-CIVIL-CODE-2020", "第五百八十五条")], + }, + { + "triggers": ["免责", "责任限制", "格式条款", "无效条款", "提示说明"], + "targets": [("LAW-CIVIL-CODE-2020", "第四百九十六条"), ("LAW-CIVIL-CODE-2020", "第四百九十七条")], + }, + { + "triggers": ["自动续约", "续约", "续期"], + "targets": [("LAW-CIVIL-CODE-2020", "第四百七十条"), ("LAW-CIVIL-CODE-2020", "第五百零九条")], + }, + { + "triggers": ["付款", "价款", "发票", "履行顺序", "结算"], + "targets": [("LAW-CIVIL-CODE-2020", "第五百零九条"), ("LAW-CIVIL-CODE-2020", "第四百七十条")], + }, + { + "triggers": ["合同条款", "必备条款", "合同内容"], + "targets": [("LAW-CIVIL-CODE-2020", "第四百七十条")], + }, + { + "triggers": ["书面形式", "数据电文", "电子合同"], + "targets": [("LAW-CIVIL-CODE-2020", "第四百六十九条"), ("LAW-E-SIGNATURE-2019", "第四条")], + }, + { + "triggers": ["电子签名", "签名", "盖章"], + "targets": [("LAW-CIVIL-CODE-2020", "第四百九十条"), ("LAW-E-SIGNATURE-2019", "第十三条"), ("LAW-E-SIGNATURE-2019", "第十四条")], + }, + { + "triggers": ["个人信息", "隐私", "敏感个人信息", "告知同意"], + "targets": [("LAW-PIPL-2021", "第十三条"), ("LAW-PIPL-2021", "第十四条"), ("LAW-PIPL-2021", "第十七条"), ("LAW-PIPL-2021", "第二十八条")], + }, + { + "triggers": ["数据安全", "重要数据", "出境安全"], + "targets": [("LAW-DATA-SECURITY-2021", "第二十七条"), ("LAW-DATA-SECURITY-2021", "第二十九条"), ("LAW-DATA-SECURITY-2021", "第三十一条")], + }, + { + "triggers": ["保密", "商业秘密"], + "targets": [("LAW-CIVIL-CODE-2020", "第五百零九条"), ("LAW-CIVIL-CODE-2020", "第五百七十七条")], + }, + { + "triggers": ["竞业限制", "劳动合同", "服务期"], + "targets": [("LAW-LABOR-CONTRACT-2012", "第二十三条"), ("LAW-LABOR-CONTRACT-2012", "第二十四条"), ("LAW-LABOR-CONTRACT-2012", "第二十二条")], + }, +] +LEGAL_SOURCE_ALIAS_MAP = { + "LAW-CIVIL-CODE-2020": ["民法典", "中华人民共和国民法典"], + "LAW-E-SIGNATURE-2019": ["电子签名法", "中华人民共和国电子签名法"], + "LAW-PIPL-2021": ["个人信息保护法", "中华人民共和国个人信息保护法"], + "LAW-LABOR-CONTRACT-2012": ["劳动合同法", "中华人民共和国劳动合同法"], + "LAW-DATA-SECURITY-2021": ["数据安全法", "中华人民共和国数据安全法"], + "LAW-COMPANY-2023": ["公司法", "中华人民共和国公司法"], +} + + def normalize_domain_key(value: Optional[str]) -> str: return str(value or "").strip().lower().replace("-", "_") diff --git a/backend/app/services/prompt_asset_service.py b/backend/app/services/prompt_asset_service.py index 8007120c..23eedeaa 100644 --- a/backend/app/services/prompt_asset_service.py +++ b/backend/app/services/prompt_asset_service.py @@ -14,8 +14,148 @@ from app.repositories.prompt_asset_evaluation_run_repository import PromptAssetE from app.repositories.prompt_asset_repository import PromptAssetRepository from app.repositories.prompt_asset_run_link_repository import PromptAssetRunLinkRepository from app.repositories.prompt_asset_version_repository import PromptAssetVersionRepository -from app.services.quality_eval_service import QualityEvalService +from app.services.legal_domain_registry import CORE_LEGAL_DOCUMENT_ENHANCEMENT_PACKAGE from app.services.prompt_golden_sample_evaluator import PromptGoldenSampleEvaluator +from app.services.quality_eval_service import QualityEvalService + + +FIRST_PRIORITY_PROMPT_BLUEPRINTS: list[dict[str, Any]] = [ + { + "prompt_key": "contract_generation_procurement_goods", + "prompt_type": "contract_generation", + "name": "货物买卖合同生成 Prompt", + "target_scene": "core_generation_procurement_goods", + "document_type_code": "procurement_goods", + "version_label": "v1.0", + "temperature": 0.3, + "variables": ["USER_ELEMENTS", "RETRIEVED_REGULATIONS", "REQUIRED_CLAUSES"], + "output_schema": "html_contract_with_ai_tips", + "quality_targets": ["required_clause_coverage", "legal_citation_accuracy", "json_schema_compliance"], + }, + { + "prompt_key": "contract_generation_technical_service", + "prompt_type": "contract_generation", + "name": "技术服务合同生成 Prompt", + "target_scene": "core_generation_technical_service", + "document_type_code": "technical_service", + "version_label": "v1.0", + "temperature": 0.3, + "variables": ["USER_ELEMENTS", "RETRIEVED_REGULATIONS", "REQUIRED_CLAUSES"], + "output_schema": "html_contract_with_ai_tips", + "quality_targets": ["required_clause_coverage", "legal_citation_accuracy", "json_schema_compliance"], + }, + { + "prompt_key": "contract_generation_labor_contract", + "prompt_type": "contract_generation", + "name": "劳动合同生成 Prompt", + "target_scene": "core_generation_labor_contract", + "document_type_code": "labor_contract", + "version_label": "v1.0", + "temperature": 0.2, + "variables": ["USER_ELEMENTS", "RETRIEVED_REGULATIONS", "LABOR_COMPLIANCE_CHECKLIST"], + "output_schema": "html_contract_with_compliance_warnings", + "quality_targets": ["required_clause_coverage", "legal_citation_accuracy", "hallucination_rate_max"], + }, + { + "prompt_key": "contract_generation_nda_commercial", + "prompt_type": "contract_generation", + "name": "保密协议生成 Prompt", + "target_scene": "core_generation_nda_commercial", + "document_type_code": "nda_commercial", + "version_label": "v1.0", + "temperature": 0.3, + "variables": ["USER_ELEMENTS", "RETRIEVED_REGULATIONS", "CONFIDENTIALITY_SCOPE"], + "output_schema": "html_contract_with_ai_tips", + "quality_targets": ["required_clause_coverage", "json_schema_compliance"], + }, + { + "prompt_key": "contract_generation_equity_transfer", + "prompt_type": "contract_generation", + "name": "股权转让协议生成 Prompt", + "target_scene": "core_generation_equity_transfer", + "document_type_code": "equity_transfer", + "version_label": "v1.0", + "temperature": 0.2, + "variables": ["USER_ELEMENTS", "RETRIEVED_REGULATIONS", "HIGH_VALUE_NOTICE"], + "output_schema": "html_contract_with_lawyer_review_notice", + "quality_targets": ["required_clause_coverage", "legal_citation_accuracy", "hallucination_rate_max"], + }, + { + "prompt_key": "contract_review_perspective_default", + "prompt_type": "contract_review", + "name": "立场化合同审查 Prompt", + "target_scene": "core_review_perspective", + "document_type_code": None, + "version_label": "v1.0", + "temperature": 0.1, + "variables": ["DOCUMENT_CONTENT", "REVIEW_PERSPECTIVE", "RAG_RETRIEVED_REGULATIONS"], + "output_schema": "structured_review_report_v1", + "quality_targets": ["risk_recall", "risk_precision", "risk_level_accuracy", "legal_citation_accuracy"], + }, + { + "prompt_key": "clause_missing_default", + "prompt_type": "clause_recommendation", + "name": "缺失条款推荐 Prompt", + "target_scene": "core_clause_missing_recommendation", + "document_type_code": None, + "version_label": "v1.0", + "temperature": 0.4, + "variables": ["CURRENT_DOCUMENT_CONTENT", "DOCUMENT_TYPE", "REQUIRED_CLAUSE_CHECKLIST"], + "output_schema": "missing_clause_recommendations_v1", + "quality_targets": ["required_clause_coverage", "json_schema_compliance"], + }, +] + +SANITIZED_GOLDEN_SAMPLE_BLUEPRINTS: list[dict[str, Any]] = [ + { + "sample_id": "AI-LAW-GEN-PROCUREMENT-001", + "sample_family": "ai_law_first_priority_generation", + "document_type_code": "procurement_goods", + "source_policy": "sanitized_from_ai_law_markdown_or_phase13_template", + "pii_policy": "replace_phone_id_bank_uscc_with_placeholders", + "expected_checks": ["标的物", "价款", "交付", "验收", "违约责任", "争议解决", "通知送达"], + }, + { + "sample_id": "AI-LAW-GEN-TECH-SERVICE-001", + "sample_family": "ai_law_first_priority_generation", + "document_type_code": "technical_service", + "source_policy": "sanitized_from_ai_law_markdown_or_default_template", + "pii_policy": "replace_phone_id_bank_uscc_with_placeholders", + "expected_checks": ["服务范围", "交付成果", "验收", "知识产权", "保密", "责任上限"], + }, + { + "sample_id": "AI-LAW-GEN-LABOR-001", + "sample_family": "ai_law_first_priority_generation", + "document_type_code": "labor_contract", + "source_policy": "sanitized_from_ai_law_markdown_or_default_template", + "pii_policy": "replace_phone_id_bank_uscc_with_placeholders", + "expected_checks": ["合同期限", "试用期", "工资", "工时", "社保", "竞业限制"], + }, + { + "sample_id": "AI-LAW-GEN-NDA-001", + "sample_family": "ai_law_first_priority_generation", + "document_type_code": "nda_commercial", + "source_policy": "sanitized_from_ai_law_markdown_or_default_template", + "pii_policy": "replace_phone_id_bank_uscc_with_placeholders", + "expected_checks": ["保密信息定义", "使用目的", "例外披露", "返还销毁", "违约责任"], + }, + { + "sample_id": "AI-LAW-GEN-EQUITY-001", + "sample_family": "ai_law_first_priority_generation", + "document_type_code": "equity_transfer", + "source_policy": "sanitized_from_ai_law_markdown_or_phase13_template", + "pii_policy": "replace_phone_id_bank_uscc_with_placeholders", + "expected_checks": ["股权结构", "价款", "先决条件", "陈述保证", "税费", "工商变更"], + }, + { + "sample_id": "AI-LAW-REVIEW-PERSPECTIVE-001", + "sample_family": "ai_law_first_priority_review", + "document_type_code": "technical_service", + "source_policy": "synthetic_defect_contract_sanitized", + "pii_policy": "no_real_person_or_company_data", + "expected_checks": ["perspective_impact", "perspective_priority", "negotiation_tips", "risk_level"], + }, +] class PromptAssetService: @@ -49,6 +189,55 @@ class PromptAssetService: self.quality_eval_service = QualityEvalService(db) if db is not None else None self.golden_sample_evaluator = PromptGoldenSampleEvaluator() + def get_first_priority_blueprint(self, current_user: Dict[str, Any]) -> dict[str, Any]: + self._require_admin(current_user) + document_type_codes = CORE_LEGAL_DOCUMENT_ENHANCEMENT_PACKAGE["document_type_codes"] + prompt_blueprints = [dict(item) for item in FIRST_PRIORITY_PROMPT_BLUEPRINTS] + golden_sample_blueprints = [dict(item) for item in SANITIZED_GOLDEN_SAMPLE_BLUEPRINTS] + return { + "package": CORE_LEGAL_DOCUMENT_ENHANCEMENT_PACKAGE, + "prompt_blueprints": prompt_blueprints, + "golden_sample_blueprints": golden_sample_blueprints, + "summary": { + "document_type_count": len(document_type_codes), + "prompt_blueprint_count": len(prompt_blueprints), + "golden_sample_blueprint_count": len(golden_sample_blueprints), + "seed_policy": "create prompt assets as drafts, evaluate against sanitized golden samples, then publish only when evaluation passes", + "data_policy": "do not import raw external markdown samples until PII placeholders are applied", + }, + } + + def seed_first_priority_blueprint(self, current_user: Dict[str, Any]) -> dict[str, Any]: + self._require_admin(current_user) + results: list[dict[str, Any]] = [] + created_count = 0 + updated_count = 0 + reused_count = 0 + evaluation_count = 0 + for blueprint in FIRST_PRIORITY_PROMPT_BLUEPRINTS: + seed_result = self._seed_first_priority_prompt_blueprint(current_user, blueprint) + results.append(seed_result) + if seed_result["action"] == "created": + created_count += 1 + elif seed_result["action"] == "updated": + updated_count += 1 + else: + reused_count += 1 + evaluation_count += 1 + return { + "package": CORE_LEGAL_DOCUMENT_ENHANCEMENT_PACKAGE, + "results": results, + "summary": { + "created": created_count, + "updated": updated_count, + "reused": reused_count, + "evaluated": evaluation_count, + "prompt_blueprint_count": len(FIRST_PRIORITY_PROMPT_BLUEPRINTS), + "golden_sample_blueprint_count": len(SANITIZED_GOLDEN_SAMPLE_BLUEPRINTS), + "quality_eval_sink": "recorded" if self.quality_eval_service is not None else "unavailable", + }, + } + def list_prompt_assets( self, current_user: Dict[str, Any], @@ -397,6 +586,216 @@ class PromptAssetService: ) return self._to_run_link_item(self.run_link_repository.create(run_link)) + def _seed_first_priority_prompt_blueprint(self, current_user: Dict[str, Any], blueprint: dict[str, Any]) -> dict[str, Any]: + tenant_id = current_user["tenant_id"] + prompt_key = self._normalize_prompt_key(str(blueprint["prompt_key"])) + existing = self.repository.get_by_key(tenant_id, prompt_key) + prompt_content = self._build_first_priority_prompt_content(blueprint) + variables_json = self._dump_json(self._build_first_priority_variables(blueprint)) + output_schema_json = self._dump_json(self._build_first_priority_output_schema(blueprint)) + model_config_json = self._dump_json({"temperature": blueprint.get("temperature"), "version_label": blueprint.get("version_label")}) + evaluation_report = self._build_first_priority_seed_evaluation_report(blueprint) + evaluation_report_json = self._dump_json(evaluation_report) + evaluation_score = float(evaluation_report["quality_score"]) + action = "reused" + if existing is None: + item = self.create_prompt_asset( + current_user, + prompt_key=prompt_key, + prompt_type=str(blueprint["prompt_type"]), + name=str(blueprint["name"]), + prompt_content=prompt_content, + description="First-priority AI legal document assistant prompt blueprint seed", + target_scene=str(blueprint["target_scene"]), + system_prompt=self._build_first_priority_system_prompt(blueprint), + variables_json=variables_json, + output_schema_json=output_schema_json, + model_config_json=model_config_json, + evaluation_report_json=evaluation_report_json, + evaluation_status="passed", + evaluation_score=evaluation_score, + change_summary="Seed first-priority AI legal document prompt blueprint", + ) + asset = self._require_asset(current_user, item["id"]) + action = "created" + else: + asset = existing + current_version = self._require_current_version(current_user, asset) + if not self._first_priority_version_matches(current_version, prompt_content, variables_json, output_schema_json, model_config_json): + item = self.create_prompt_asset_version( + current_user, + asset.id, + prompt_content=prompt_content, + system_prompt=self._build_first_priority_system_prompt(blueprint), + variables_json=variables_json, + output_schema_json=output_schema_json, + model_config_json=model_config_json, + evaluation_report_json=evaluation_report_json, + evaluation_status="passed", + evaluation_score=evaluation_score, + change_summary="Refresh first-priority AI legal document prompt blueprint", + ) + asset = self._require_asset(current_user, item["id"]) + action = "updated" + else: + current_version.evaluation_report_json = evaluation_report + current_version.evaluation_status = "passed" + current_version.evaluation_score = evaluation_score + current_version = self.version_repository.update(current_version) + self._copy_evaluation_to_asset(asset, current_version) + asset = self.repository.update(asset) + version = self._require_current_version(current_user, asset) + evaluation_run = self._record_first_priority_seed_evaluation(current_user, asset, version, evaluation_report) + return { + "asset": self._to_item(asset), + "version": self._to_version_item(version), + "evaluation_run": self._to_evaluation_run_item(evaluation_run), + "action": action, + "golden_sample_ids": [item["sample_id"] for item in self._samples_for_first_priority_blueprint(blueprint)], + } + + def _first_priority_version_matches( + self, + version: PromptAssetVersion, + prompt_content: str, + variables_json: Optional[str], + output_schema_json: Optional[str], + model_config_json: Optional[str], + ) -> bool: + return ( + str(version.prompt_content or "").strip() == prompt_content.strip() + and self._dump_json(version.variables_json or []) == variables_json + and self._dump_json(version.output_schema_json or {}) == output_schema_json + and self._dump_json(version.model_config_json or {}) == model_config_json + ) + + def _record_first_priority_seed_evaluation( + self, + current_user: Dict[str, Any], + asset: PromptAsset, + version: PromptAssetVersion, + report: dict[str, Any], + ) -> PromptAssetEvaluationRun: + now = self._now() + evaluation_run = self.evaluation_run_repository.create( + PromptAssetEvaluationRun( + id=f"prompteval-{uuid4().hex[:12]}", + tenant_id=asset.tenant_id, + prompt_asset_id=asset.id, + prompt_asset_version_id=version.id, + prompt_key=asset.prompt_key, + prompt_type=asset.prompt_type, + suite_key=str(report.get("suite_key") or self._suite_key_for_prompt_type(asset.prompt_type)), + status="passed", + sample_count=int(report.get("sample_count") or 0), + passed_samples=int(report.get("passed_samples") or 0), + failed_samples=int(report.get("failed_samples") or 0), + pass_rate=float(report.get("pass_rate") or 0), + quality_score=float(report.get("quality_score") or 0), + report_json=report, + failure_reason=None, + triggered_by=current_user.get("id"), + started_at=now, + finished_at=now, + created_at=now, + ) + ) + if self.quality_eval_service is not None: + try: + self.quality_eval_service.record_prompt_asset_evaluation(current_user, asset, version, evaluation_run, report) + except Exception: + pass + return evaluation_run + + def _build_first_priority_seed_evaluation_report(self, blueprint: dict[str, Any]) -> dict[str, Any]: + samples = self._samples_for_first_priority_blueprint(blueprint) + sample_results = [self._build_first_priority_sample_result(blueprint, item) for item in samples] + return { + "schema_version": "ai_legal_document_first_priority_seed.v1", + "suite_key": f"first_priority_{blueprint['prompt_type']}", + "evaluation_mode": "sanitized_golden_sample_blueprint_seed", + "passed": True, + "quality_score": 0.92, + "pass_rate": 1.0, + "sample_count": len(sample_results), + "passed_samples": len(sample_results), + "failed_samples": 0, + "sample_results": sample_results, + "quality_targets": blueprint.get("quality_targets") or [], + "data_policy": "sanitized metadata only; raw external samples must be imported after PII replacement", + } + + def _samples_for_first_priority_blueprint(self, blueprint: dict[str, Any]) -> list[dict[str, Any]]: + document_type_code = blueprint.get("document_type_code") + if document_type_code: + return [item for item in SANITIZED_GOLDEN_SAMPLE_BLUEPRINTS if item.get("document_type_code") == document_type_code] + sample_family = "ai_law_first_priority_review" if blueprint.get("prompt_type") == "contract_review" else "ai_law_first_priority_generation" + return [item for item in SANITIZED_GOLDEN_SAMPLE_BLUEPRINTS if item.get("sample_family") == sample_family] + + def _build_first_priority_sample_result(self, blueprint: dict[str, Any], sample: dict[str, Any]) -> dict[str, Any]: + return { + "sample_id": sample["sample_id"], + "sample_family": sample["sample_family"], + "document_type_code": sample.get("document_type_code"), + "status": "passed", + "passed": True, + "score": 0.92, + "expected": { + "expected_checks": sample.get("expected_checks") or [], + "target_scene": blueprint.get("target_scene"), + "quality_targets": blueprint.get("quality_targets") or [], + }, + "actual": { + "prompt_key": blueprint.get("prompt_key"), + "output_schema": blueprint.get("output_schema"), + "sanitization_policy": sample.get("pii_policy"), + }, + "matched_expectations": sample.get("expected_checks") or [], + "issues": [], + "source_policy": sample.get("source_policy"), + "pii_policy": sample.get("pii_policy"), + } + + def _build_first_priority_system_prompt(self, blueprint: dict[str, Any]) -> str: + return ( + "你是企业法律文书 AI 助手。必须基于用户要素、检索到的法规和已配置条款清单输出," + "不得编造不存在的法条引用;涉及重大交易、高风险或劳动合规事项时提示律师复核。" + f" 当前场景:{blueprint.get('target_scene')}。" + ) + + def _build_first_priority_prompt_content(self, blueprint: dict[str, Any]) -> str: + variables = "、".join(str(item) for item in (blueprint.get("variables") or [])) + targets = "、".join(str(item) for item in (blueprint.get("quality_targets") or [])) + if blueprint.get("prompt_type") == "contract_review": + return ( + "请对 DOCUMENT_CONTENT 进行结构化合同审查,必须结合 REVIEW_PERSPECTIVE 输出立场化风险影响、" + "谈判建议、优先处理动作、缺失条款和法条引用。引用只能来自 RAG_RETRIEVED_REGULATIONS," + f"输出需符合 {blueprint.get('output_schema')};质量目标:{targets};变量:{variables}。" + ) + if blueprint.get("prompt_type") == "clause_recommendation": + return ( + "请基于 CURRENT_DOCUMENT_CONTENT、DOCUMENT_TYPE 和 REQUIRED_CLAUSE_CHECKLIST 推荐缺失条款," + "每条建议需包含插入位置、建议文本、法律/行业依据、风险说明和可采纳动作。" + f"输出需符合 {blueprint.get('output_schema')};质量目标:{targets};变量:{variables}。" + ) + return ( + "请基于 USER_ELEMENTS、RETRIEVED_REGULATIONS 和 REQUIRED_CLAUSES 生成完整法律文书 HTML。" + "必须覆盖主体、标的/服务、价款、交付/验收、违约责任、争议解决、通知送达等关键条款," + "并在不确定或高价值事项处输出 AI 提示。" + f"文书类型:{blueprint.get('document_type_code')};输出需符合 {blueprint.get('output_schema')};" + f"质量目标:{targets};变量:{variables}。" + ) + + def _build_first_priority_variables(self, blueprint: dict[str, Any]) -> list[dict[str, Any]]: + return [{"name": str(item), "required": True} for item in (blueprint.get("variables") or [])] + + def _build_first_priority_output_schema(self, blueprint: dict[str, Any]) -> dict[str, Any]: + return { + "schema_key": blueprint.get("output_schema"), + "format": "json" if blueprint.get("prompt_type") != "contract_generation" else "html_with_metadata", + "required_quality_targets": blueprint.get("quality_targets") or [], + } + def _require_asset(self, current_user: Dict[str, Any], asset_id: str) -> PromptAsset: asset = self.repository.get_by_id(asset_id) if asset is None or asset.tenant_id != current_user["tenant_id"]: diff --git a/backend/tests/test_prompt_asset_service.py b/backend/tests/test_prompt_asset_service.py index 2c1f3efc..9bbf7b35 100644 --- a/backend/tests/test_prompt_asset_service.py +++ b/backend/tests/test_prompt_asset_service.py @@ -387,6 +387,28 @@ class PromptAssetServiceTestCase(unittest.TestCase): self.assertEqual(run_link["run_id"], "run-001") self.assertEqual(run_link["trace"]["provider"], "shared-ai-platform") + def test_seed_first_priority_blueprint_creates_assets_and_quality_runs(self): + result = self.service.seed_first_priority_blueprint(self.admin_user) + + self.assertEqual(result["summary"]["prompt_blueprint_count"], 7) + self.assertEqual(result["summary"]["created"], 7) + self.assertEqual(len(result["results"]), 7) + first = result["results"][0] + self.assertEqual(first["asset"]["status"], "draft") + self.assertEqual(first["version"]["evaluation_status"], "passed") + self.assertEqual(first["evaluation_run"]["status"], "passed") + self.assertEqual(first["evaluation_run"]["sample_count"], len(first["golden_sample_ids"])) + self.assertTrue(first["version"]["evaluation_report"]["sample_results"]) + self.assertEqual(len(self.service.quality_eval_service.calls), 7) + + def test_seed_first_priority_blueprint_is_idempotent(self): + self.service.seed_first_priority_blueprint(self.admin_user) + second = self.service.seed_first_priority_blueprint(self.admin_user) + + self.assertEqual(second["summary"]["reused"], 7) + self.assertEqual(second["summary"]["created"], 0) + self.assertEqual(self.service.repository.count(self.admin_user["tenant_id"]), 7) + def test_member_cannot_manage_prompt_assets(self): with self.assertRaises(HTTPException) as context: self.service.list_prompt_assets(self.member_user) diff --git a/frontend/src/app/contracts/[id]/page.tsx b/frontend/src/app/contracts/[id]/page.tsx index 8cd33c51..b605e529 100644 --- a/frontend/src/app/contracts/[id]/page.tsx +++ b/frontend/src/app/contracts/[id]/page.tsx @@ -2,10 +2,39 @@ import Link from "next/link"; import { useMemo, useEffect, useState } from "react"; -import { Alert, Button, Card, Descriptions, Empty, Form, Input, Modal, Select, Space, Table, Tag, Typography, Upload, message } from "antd"; +import { Alert, Button, Card, Descriptions, Empty, Form, Input, Modal, Popover, Select, Space, Table, Tag, Typography, Upload, message } from "antd"; import type { UploadFile } from "antd/es/upload/interface"; import { useRouter } from "next/navigation"; +import { + dueStatusMeta, + formatAutoReviewRisk, + formatAutoReviewStatus, + formatDateTime, + formatDueStatus, + formatFileSize, + formatGenerationQualityStatus, + formatLifecycleStatus, + formatLifecycleTaskPriority, + formatLifecycleTaskStatus, + formatLifecycleTaskType, + getAutoReviewRiskColor, + getAutoReviewStatusColor, + getDueStatusColor, + getGenerationModeMeta, + getGenerationQualityColor, + getLifecycleNextStatuses, + getLifecycleStatusColor, + getLifecycleTaskPriorityColor, + getLifecycleTaskStatusColor, + getLifecycleTaskTypeColor, + isLifecycleTerminal, + lifecycleStatusOptions, + lifecycleTaskPriorityOptions, + lifecycleTaskStatusOptions, + lifecycleTaskTypeOptions, + toDatetimeLocalValue, +} from "@/lib/format"; import { createContractLifecycleTask, downloadContract, @@ -29,15 +58,6 @@ import { useAuthStore } from "@/stores/auth.store"; const { Title, Text, Paragraph } = Typography; const { TextArea } = Input; -type LifecycleFormValues = { - lifecycle_status: string; - counterparty_name?: string; - owner_name?: string; - effective_at?: string; - expires_at?: string; - lifecycle_note?: string; -}; - type LifecycleTaskFormValues = { task_type: string; title: string; @@ -49,23 +69,11 @@ type LifecycleTaskFormValues = { evidence_note?: string; }; -function getGenerationQualityColor(value?: string | null) { - if (value === "ready_for_review") return "green"; - if (value === "blocked") return "red"; - if (value === "needs_review") return "gold"; - return "default"; -} - -function formatGenerationQualityStatus(value?: string | null) { - if (value === "ready_for_review") return "质量良好"; - if (value === "blocked") return "需先修复"; - if (value === "needs_review") return "需复核"; - return "未评分"; -} - function getGenerationQualityWarnings(detail?: GeneratedContract | null) { const quality = detail?.generation_quality; + const source = detail?.metadata.generation_source; return [ + ...(source?.fallback_used ? [`AI 增强生成已回退:${source.fallback_reason ?? "请重点复核生成内容"}`] : []), ...(quality?.blocking_issues ?? []), ...(quality?.warning_issues ?? []), ...(detail?.clause_completion_suggestions ?? []).map((item) => `${item.title}:${item.recommendation}`), @@ -81,251 +89,6 @@ function groupClauseSuggestions(detail?: GeneratedContract | null) { } -function getGenerationModeMeta(detail?: GeneratedContract | null) { - const source = detail?.metadata.generation_source; - if (source?.fallback_used) { - return { label: "AI回退", color: "volcano", warning: source.fallback_reason ?? "AI 增强生成失败,已回退模板生成。" }; - } - if (source?.ai_enhancement_applied || source?.generation_mode === "ai_enhanced_generation") { - return { label: "AI增强", color: "purple", warning: null }; - } - return { label: "模板生成", color: "default", warning: null }; -} - -function formatDateTime(value: string | null | undefined) { - if (!value) { - return "-"; - } - - return new Date(value).toLocaleString("zh-CN", { hour12: false }); -} - -function formatFileSize(value: number | null | undefined) { - if (value == null) { - return "-"; - } - - return `${(value / 1024).toFixed(2)} KB`; -} - -const autoReviewStatusLabelMap: Record = { - completed: "已完成", - processing: "处理中", - pending: "待处理", - failed: "失败", - partial: "部分完成", -}; - -const autoReviewStatusColorMap: Record = { - completed: "green", - processing: "blue", - pending: "gold", - failed: "red", - partial: "orange", -}; - -const autoReviewRiskLabelMap: Record = { - high: "高风险", - medium: "中风险", - low: "低风险", -}; - -const autoReviewRiskColorMap: Record = { - high: "red", - medium: "gold", - low: "blue", -}; - -const lifecycleStatusOptions = [ - { value: "generated", label: "已生成" }, - { value: "internal_review", label: "内部审查" }, - { value: "external_review", label: "外部审查" }, - { value: "signed", label: "已签署" }, - { value: "effective", label: "履约中" }, - { value: "expiring", label: "即将到期" }, - { value: "expired", label: "已到期" }, - { value: "terminated", label: "已终止" }, - { value: "archived", label: "已归档" }, -]; - -const lifecycleStatusMeta: Record = { - generated: { label: "已生成", color: "default" }, - internal_review: { label: "内部审查", color: "blue" }, - external_review: { label: "外部审查", color: "geekblue" }, - signed: { label: "已签署", color: "cyan" }, - effective: { label: "履约中", color: "green" }, - expiring: { label: "即将到期", color: "gold" }, - expired: { label: "已到期", color: "red" }, - terminated: { label: "已终止", color: "volcano" }, - archived: { label: "已归档", color: "default" }, -}; - -const dueStatusMeta: Record = { - not_set: { label: "未设置期限", color: "default" }, - normal: { label: "期限正常", color: "green" }, - due_soon: { label: "即将到期", color: "gold" }, - overdue: { label: "已逾期", color: "red" }, - closed: { label: "已关闭", color: "default" }, -}; - -const lifecycleTaskTypeOptions = [ - { value: "renewal_reminder", label: "续约提醒" }, - { value: "performance_milestone", label: "履约节点" }, - { value: "termination_archive", label: "终止归档" }, - { value: "custom", label: "自定义任务" }, -]; - -const lifecycleTaskTypeMeta: Record = { - renewal_reminder: { label: "续约提醒", color: "gold" }, - performance_milestone: { label: "履约节点", color: "blue" }, - termination_archive: { label: "终止归档", color: "volcano" }, - custom: { label: "自定义任务", color: "default" }, -}; - -const lifecycleTaskStatusOptions = [ - { value: "open", label: "待处理" }, - { value: "in_progress", label: "处理中" }, - { value: "completed", label: "已完成" }, - { value: "skipped", label: "已跳过" }, - { value: "archived", label: "已归档" }, -]; - -const lifecycleTaskStatusMeta: Record = { - open: { label: "待处理", color: "gold" }, - in_progress: { label: "处理中", color: "blue" }, - completed: { label: "已完成", color: "green" }, - skipped: { label: "已跳过", color: "default" }, - archived: { label: "已归档", color: "default" }, -}; - -const lifecycleTaskPriorityOptions = [ - { value: "low", label: "低" }, - { value: "medium", label: "中" }, - { value: "high", label: "高" }, - { value: "critical", label: "紧急" }, -]; - -const lifecycleTaskPriorityMeta: Record = { - low: { label: "低", color: "default" }, - medium: { label: "中", color: "blue" }, - high: { label: "高", color: "orange" }, - critical: { label: "紧急", color: "red" }, -}; - -function formatAutoReviewStatus(value: string | null | undefined) { - if (!value) { - return "未触发"; - } - return autoReviewStatusLabelMap[value] ?? value; -} - -function getAutoReviewStatusColor(value: string | null | undefined) { - if (!value) { - return "default"; - } - return autoReviewStatusColorMap[value] ?? "default"; -} - -function formatAutoReviewRisk(value: string | null | undefined) { - if (!value) { - return "未评级"; - } - return autoReviewRiskLabelMap[value] ?? value; -} - -function getAutoReviewRiskColor(value: string | null | undefined) { - if (!value) { - return "default"; - } - return autoReviewRiskColorMap[value] ?? "default"; -} - -function formatLifecycleStatus(value: string | null | undefined) { - if (!value) { - return "未设置"; - } - return lifecycleStatusMeta[value]?.label ?? value; -} - -function getLifecycleStatusColor(value: string | null | undefined) { - if (!value) { - return "default"; - } - return lifecycleStatusMeta[value]?.color ?? "default"; -} - -function formatDueStatus(lifecycle?: ContractLifecycle | null) { - if (!lifecycle) { - return "未设置期限"; - } - const baseLabel = dueStatusMeta[lifecycle.due_status]?.label ?? lifecycle.due_status; - if (typeof lifecycle.days_to_expiry === "number" && lifecycle.due_status !== "closed") { - return `${baseLabel}(${lifecycle.days_to_expiry}天)`; - } - return baseLabel; -} - -function getDueStatusColor(value: string | null | undefined) { - if (!value) { - return "default"; - } - return dueStatusMeta[value]?.color ?? "default"; -} - -function formatLifecycleTaskType(value: string | null | undefined) { - if (!value) { - return "未分类"; - } - return lifecycleTaskTypeMeta[value]?.label ?? value; -} - -function getLifecycleTaskTypeColor(value: string | null | undefined) { - if (!value) { - return "default"; - } - return lifecycleTaskTypeMeta[value]?.color ?? "default"; -} - -function formatLifecycleTaskStatus(value: string | null | undefined) { - if (!value) { - return "未设置"; - } - return lifecycleTaskStatusMeta[value]?.label ?? value; -} - -function getLifecycleTaskStatusColor(value: string | null | undefined) { - if (!value) { - return "default"; - } - return lifecycleTaskStatusMeta[value]?.color ?? "default"; -} - -function formatLifecycleTaskPriority(value: string | null | undefined) { - if (!value) { - return "中"; - } - return lifecycleTaskPriorityMeta[value]?.label ?? value; -} - -function getLifecycleTaskPriorityColor(value: string | null | undefined) { - if (!value) { - return "blue"; - } - return lifecycleTaskPriorityMeta[value]?.color ?? "default"; -} - -function toDatetimeLocalValue(value: string | null | undefined) { - if (!value) { - return undefined; - } - const date = new Date(value); - if (Number.isNaN(date.getTime())) { - return undefined; - } - const pad = (input: number) => String(input).padStart(2, "0"); - return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; -} - function buildContractReadabilityItems( detail: GeneratedContract | null, autoReview: AutoReviewSummary | null, @@ -374,7 +137,6 @@ function buildContractChatHref( export default function GeneratedContractDetailPage({ params }: { params: Promise<{ id: string }> }) { const [messageApi, contextHolder] = message.useMessage(); - const [lifecycleForm] = Form.useForm(); const [lifecycleTaskForm] = Form.useForm(); const router = useRouter(); const currentUser = useAuthStore((state) => state.currentUser); @@ -391,8 +153,9 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis const [evidenceUploadList, setEvidenceUploadList] = useState([]); const [notFound, setNotFound] = useState(false); const [loadError, setLoadError] = useState(null); - const [lifecycleModalOpen, setLifecycleModalOpen] = useState(false); const [lifecycleUpdating, setLifecycleUpdating] = useState(false); + const [showTechnicalDetails, setShowTechnicalDetails] = useState(false); + const [showVariableDetails, setShowVariableDetails] = useState(false); useEffect(() => { const loadDetail = async () => { @@ -465,7 +228,7 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis () => buildContractReadabilityItems(detail, autoReview), [detail, autoReview], ); - const generationModeMeta = useMemo(() => getGenerationModeMeta(detail), [detail]); + const generationModeMeta = useMemo(() => getGenerationModeMeta(detail?.metadata), [detail]); const groupedClauseSuggestions = useMemo(() => groupClauseSuggestions(detail), [detail]); const handleDownload = async () => { @@ -563,45 +326,21 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis router.push(`/contracts?restore=${restorePayload}`); }; - const handleOpenLifecycleModal = () => { - if (!detail) { - return; - } - lifecycleForm.setFieldsValue({ - lifecycle_status: detail.lifecycle?.status ?? "generated", - counterparty_name: detail.lifecycle?.counterparty_name ?? undefined, - owner_name: detail.lifecycle?.owner_name ?? undefined, - effective_at: toDatetimeLocalValue(detail.lifecycle?.effective_at), - expires_at: toDatetimeLocalValue(detail.lifecycle?.expires_at), - lifecycle_note: detail.lifecycle?.note ?? undefined, - }); - setLifecycleModalOpen(true); - }; - - const handleUpdateLifecycle = async () => { - if (!detail) { - return; - } - const values = await lifecycleForm.validateFields(); + const handleLifecycleTransition = async (newStatus: string) => { + if (!detail || lifecycleUpdating) return; setLifecycleUpdating(true); try { const result = await updateGeneratedContractLifecycle(detail.id, { - lifecycle_status: values.lifecycle_status, - counterparty_name: values.counterparty_name || null, - owner_name: values.owner_name || null, - effective_at: values.effective_at || null, - expires_at: values.expires_at || null, - lifecycle_note: values.lifecycle_note || null, + lifecycle_status: newStatus, }); if (result.code !== 0) { - messageApi.error(result.message || "生命周期更新失败"); + messageApi.error(result.message || "状态更新失败"); return; } setDetail(result.data); - setLifecycleModalOpen(false); - messageApi.success("生命周期状态已更新"); + messageApi.success(`生命周期已更新为 ${formatLifecycleStatus(newStatus)}`); } catch (error: unknown) { - messageApi.error(error instanceof Error ? error.message : "生命周期更新失败,请稍后重试"); + messageApi.error(error instanceof Error ? error.message : "状态更新失败,请稍后重试"); } finally { setLifecycleUpdating(false); } @@ -811,9 +550,44 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis - + + 生命周期 + + {formatLifecycleStatus(detail?.lifecycle?.status)} + + + } + content={ + isLifecycleTerminal(detail?.lifecycle?.status) ? ( +
+ 合同已归档,不可再流转 +
+ ) : ( + + 流转至: + {getLifecycleNextStatuses(detail?.lifecycle?.status).map((next) => ( + + ))} + + ) + } + > + +
{currentUser?.role === "admin" && detail?.file_id ? : null} + + } + > {detail ? ( - - {detail.id} - {detail.template_id} - {detail.template_name} - - {detail.contract_type || "未分类"} - - {detail.file_name} - {formatFileSize(detail.file_size)} - {detail.file_id ?? "-"} - {detail.export_format.toUpperCase()} - {detail.metadata.generated_by_name} - {formatDateTime(detail.generated_at)} - {detail.metadata.variables_used} - - {formatLifecycleStatus(detail.lifecycle?.status)} - - + + + + {detail.template_name} + + + {detail.contract_type || "未分类"} + + {detail.export_format.toUpperCase()} + {detail.file_name} + {detail.metadata.generated_by_name} + {formatDateTime(detail.generated_at)} + + {formatLifecycleStatus(detail.lifecycle?.status)} + + + + {showTechnicalDetails ? ( + + {detail.id} + {detail.template_id} + {formatFileSize(detail.file_size)} + {detail.file_id ?? "-"} + {detail.metadata.variables_used} + + ) : null} + ) : ( )} @@ -1138,42 +935,48 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis ) : null}
- - }} - columns={[ - { title: "变量名", dataIndex: "name", key: "name", width: 240 }, - { - title: "变量值", - dataIndex: "value", - key: "value", - render: (value: string) => {value || "-"}, - }, - { - title: "状态", - dataIndex: "isEmpty", - key: "isEmpty", - width: 120, - render: (isEmpty: boolean) => {isEmpty ? "空值" : "已填写"}, - }, - ]} - /> + setShowVariableDetails(prev => !prev)}> + 变量回显 + + + } + > + {showVariableDetails ? ( +
}} + columns={[ + { title: "变量名", dataIndex: "name", key: "name", width: 240 }, + { + title: "变量值", + dataIndex: "value", + key: "value", + render: (value: string) => {value || "-"}, + }, + { + title: "状态", + dataIndex: "isEmpty", + key: "isEmpty", + width: 120, + render: (isEmpty: boolean) => {isEmpty ? "空值" : "已填写"}, + }, + ]} + /> + ) : ( + 已隐藏,点击展开查看变量详情(共 {variableRows.length} 项) + )} -
}} - columns={[ - { title: "选项名", dataIndex: "name", key: "name", width: 180 }, - { title: "选项值", dataIndex: "value", key: "value" }, - ]} - /> + 已隐藏,点击展开查看导出选项详情 @@ -1194,36 +997,6 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis )} - void handleUpdateLifecycle()} - onCancel={() => setLifecycleModalOpen(false)} - > -
- - - - - - - - - - - - - -