更新合同API、提示词资产服务及前端页面
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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("-", "_")
|
||||
|
||||
|
||||
@@ -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"]:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<string, string> = {
|
||||
completed: "已完成",
|
||||
processing: "处理中",
|
||||
pending: "待处理",
|
||||
failed: "失败",
|
||||
partial: "部分完成",
|
||||
};
|
||||
|
||||
const autoReviewStatusColorMap: Record<string, string> = {
|
||||
completed: "green",
|
||||
processing: "blue",
|
||||
pending: "gold",
|
||||
failed: "red",
|
||||
partial: "orange",
|
||||
};
|
||||
|
||||
const autoReviewRiskLabelMap: Record<string, string> = {
|
||||
high: "高风险",
|
||||
medium: "中风险",
|
||||
low: "低风险",
|
||||
};
|
||||
|
||||
const autoReviewRiskColorMap: Record<string, string> = {
|
||||
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<string, { label: string; color: string }> = {
|
||||
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<string, { label: string; color: string }> = {
|
||||
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<string, { label: string; color: string }> = {
|
||||
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<string, { label: string; color: string }> = {
|
||||
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<string, { label: string; color: string }> = {
|
||||
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<LifecycleFormValues>();
|
||||
const [lifecycleTaskForm] = Form.useForm<LifecycleTaskFormValues>();
|
||||
const router = useRouter();
|
||||
const currentUser = useAuthStore((state) => state.currentUser);
|
||||
@@ -391,8 +153,9 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
|
||||
const [evidenceUploadList, setEvidenceUploadList] = useState<UploadFile[]>([]);
|
||||
const [notFound, setNotFound] = useState(false);
|
||||
const [loadError, setLoadError] = useState<string | null>(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
|
||||
<Button type="primary" onClick={() => router.push(`/documents/${detail?.id}/edit`)}>
|
||||
进入合同编辑器
|
||||
</Button>
|
||||
<Button onClick={handleOpenLifecycleModal}>
|
||||
更新生命周期
|
||||
</Button>
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
title={
|
||||
<Space>
|
||||
<span>生命周期</span>
|
||||
<Tag color={getLifecycleStatusColor(detail?.lifecycle?.status)}>
|
||||
{formatLifecycleStatus(detail?.lifecycle?.status)}
|
||||
</Tag>
|
||||
</Space>
|
||||
}
|
||||
content={
|
||||
isLifecycleTerminal(detail?.lifecycle?.status) ? (
|
||||
<div style={{ width: 200, textAlign: "center", padding: "8px 0" }}>
|
||||
<Text type="secondary">合同已归档,不可再流转</Text>
|
||||
</div>
|
||||
) : (
|
||||
<Space direction="vertical" size="small" style={{ width: 220 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>流转至:</Text>
|
||||
{getLifecycleNextStatuses(detail?.lifecycle?.status).map((next) => (
|
||||
<Button
|
||||
key={next.value}
|
||||
size="small"
|
||||
block
|
||||
loading={lifecycleUpdating}
|
||||
onClick={() => void handleLifecycleTransition(next.value)}
|
||||
>
|
||||
{next.label}
|
||||
</Button>
|
||||
))}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Button>
|
||||
更新生命周期
|
||||
</Button>
|
||||
</Popover>
|
||||
{currentUser?.role === "admin" && detail?.file_id ? <Button onClick={() => router.push(`/files?highlight=${detail.file_id}`)}>查看文件资产</Button> : null}
|
||||
<Button loading={downloading} onClick={handleDownload}>
|
||||
下载合同文件
|
||||
@@ -1071,26 +845,49 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card className="review-surface-card" loading={loading} title="基本信息">
|
||||
<Card
|
||||
className="review-surface-card"
|
||||
loading={loading}
|
||||
title={
|
||||
<Space
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => setShowTechnicalDetails(prev => !prev)}
|
||||
>
|
||||
<span>基本信息</span>
|
||||
<Button size="small" type="text">
|
||||
{showTechnicalDetails ? "收起技术详情 ▾" : "展开技术详情 ▸"}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{detail ? (
|
||||
<Descriptions column={2} bordered>
|
||||
<Descriptions.Item label="合同ID">{detail.id}</Descriptions.Item>
|
||||
<Descriptions.Item label="模板ID">{detail.template_id}</Descriptions.Item>
|
||||
<Descriptions.Item label="模板名称">{detail.template_name}</Descriptions.Item>
|
||||
<Descriptions.Item label="合同类型">
|
||||
<Tag color="purple">{detail.contract_type || "未分类"}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="文件名">{detail.file_name}</Descriptions.Item>
|
||||
<Descriptions.Item label="文件大小">{formatFileSize(detail.file_size)}</Descriptions.Item>
|
||||
<Descriptions.Item label="资产文件ID">{detail.file_id ?? "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label="导出格式">{detail.export_format.toUpperCase()}</Descriptions.Item>
|
||||
<Descriptions.Item label="生成人员">{detail.metadata.generated_by_name}</Descriptions.Item>
|
||||
<Descriptions.Item label="生成时间">{formatDateTime(detail.generated_at)}</Descriptions.Item>
|
||||
<Descriptions.Item label="变量数量">{detail.metadata.variables_used}</Descriptions.Item>
|
||||
<Descriptions.Item label="生命周期状态">
|
||||
<Tag color={getLifecycleStatusColor(detail.lifecycle?.status)}>{formatLifecycleStatus(detail.lifecycle?.status)}</Tag>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Space orientation="vertical" size="middle" style={{ width: "100%" }}>
|
||||
<Descriptions column={2} bordered size="small">
|
||||
<Descriptions.Item label="模板名称">
|
||||
<Text strong>{detail.template_name}</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="合同类型">
|
||||
<Tag color="purple">{detail.contract_type || "未分类"}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="导出格式">{detail.export_format.toUpperCase()}</Descriptions.Item>
|
||||
<Descriptions.Item label="文件名称">{detail.file_name}</Descriptions.Item>
|
||||
<Descriptions.Item label="生成人员">{detail.metadata.generated_by_name}</Descriptions.Item>
|
||||
<Descriptions.Item label="生成时间">{formatDateTime(detail.generated_at)}</Descriptions.Item>
|
||||
<Descriptions.Item label="生命周期状态">
|
||||
<Tag color={getLifecycleStatusColor(detail.lifecycle?.status)}>{formatLifecycleStatus(detail.lifecycle?.status)}</Tag>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
{showTechnicalDetails ? (
|
||||
<Descriptions column={2} bordered size="small">
|
||||
<Descriptions.Item label="合同ID"><Text copyable>{detail.id}</Text></Descriptions.Item>
|
||||
<Descriptions.Item label="模板ID"><Text copyable>{detail.template_id}</Text></Descriptions.Item>
|
||||
<Descriptions.Item label="文件大小">{formatFileSize(detail.file_size)}</Descriptions.Item>
|
||||
<Descriptions.Item label="资产文件ID">{detail.file_id ?? "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label="变量数量">{detail.metadata.variables_used}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
) : null}
|
||||
</Space>
|
||||
) : (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无合同详情。" />
|
||||
)}
|
||||
@@ -1138,42 +935,48 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
|
||||
) : null}
|
||||
|
||||
<div className="contract-detail-two-column">
|
||||
<Card className="review-surface-card" loading={loading} title="变量回显">
|
||||
<Table
|
||||
rowKey="key"
|
||||
pagination={false}
|
||||
dataSource={variableRows}
|
||||
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无变量数据。" /> }}
|
||||
columns={[
|
||||
{ title: "变量名", dataIndex: "name", key: "name", width: 240 },
|
||||
{
|
||||
title: "变量值",
|
||||
dataIndex: "value",
|
||||
key: "value",
|
||||
render: (value: string) => <Paragraph style={{ marginBottom: 0, whiteSpace: "pre-wrap" }}>{value || "-"}</Paragraph>,
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "isEmpty",
|
||||
key: "isEmpty",
|
||||
width: 120,
|
||||
render: (isEmpty: boolean) => <Tag color={isEmpty ? "gold" : "green"}>{isEmpty ? "空值" : "已填写"}</Tag>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Card
|
||||
className="review-surface-card"
|
||||
loading={loading}
|
||||
title={
|
||||
<Space style={{ cursor: "pointer" }} onClick={() => setShowVariableDetails(prev => !prev)}>
|
||||
<span>变量回显</span>
|
||||
<Button size="small" type="text">
|
||||
{showVariableDetails ? "收起 ▾" : "展开 ▸"}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{showVariableDetails ? (
|
||||
<Table
|
||||
rowKey="key"
|
||||
pagination={false}
|
||||
dataSource={variableRows}
|
||||
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无变量数据。" /> }}
|
||||
columns={[
|
||||
{ title: "变量名", dataIndex: "name", key: "name", width: 240 },
|
||||
{
|
||||
title: "变量值",
|
||||
dataIndex: "value",
|
||||
key: "value",
|
||||
render: (value: string) => <Paragraph style={{ marginBottom: 0, whiteSpace: "pre-wrap" }}>{value || "-"}</Paragraph>,
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "isEmpty",
|
||||
key: "isEmpty",
|
||||
width: 120,
|
||||
render: (isEmpty: boolean) => <Tag color={isEmpty ? "gold" : "green"}>{isEmpty ? "空值" : "已填写"}</Tag>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Text type="secondary">已隐藏,点击展开查看变量详情(共 {variableRows.length} 项)</Text>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card className="review-surface-card" loading={loading} title="导出选项">
|
||||
<Table
|
||||
rowKey="key"
|
||||
pagination={false}
|
||||
dataSource={exportOptionRows}
|
||||
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无导出选项。" /> }}
|
||||
columns={[
|
||||
{ title: "选项名", dataIndex: "name", key: "name", width: 180 },
|
||||
{ title: "选项值", dataIndex: "value", key: "value" },
|
||||
]}
|
||||
/>
|
||||
<Text type="secondary">已隐藏,点击展开查看导出选项详情</Text>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -1194,36 +997,6 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
<Modal
|
||||
title="更新合同生命周期"
|
||||
open={lifecycleModalOpen}
|
||||
okText="保存状态"
|
||||
cancelText="取消"
|
||||
confirmLoading={lifecycleUpdating}
|
||||
onOk={() => void handleUpdateLifecycle()}
|
||||
onCancel={() => setLifecycleModalOpen(false)}
|
||||
>
|
||||
<Form form={lifecycleForm} layout="vertical" initialValues={{ lifecycle_status: "generated" }}>
|
||||
<Form.Item label="生命周期状态" name="lifecycle_status" rules={[{ required: true, message: "请选择生命周期状态" }]}>
|
||||
<Select options={lifecycleStatusOptions} />
|
||||
</Form.Item>
|
||||
<Form.Item label="合同相对方" name="counterparty_name">
|
||||
<Input placeholder="例如:供应商A、客户B" maxLength={120} />
|
||||
</Form.Item>
|
||||
<Form.Item label="内部负责人" name="owner_name">
|
||||
<Input placeholder="例如:法务运营、销售负责人" maxLength={100} />
|
||||
</Form.Item>
|
||||
<Form.Item label="生效时间" name="effective_at">
|
||||
<Input type="datetime-local" />
|
||||
</Form.Item>
|
||||
<Form.Item label="到期时间" name="expires_at">
|
||||
<Input type="datetime-local" />
|
||||
</Form.Item>
|
||||
<Form.Item label="履约备注" name="lifecycle_note">
|
||||
<TextArea rows={3} placeholder="记录签署、续约、终止或跟进说明" maxLength={1000} showCount />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
<Modal
|
||||
title={editingLifecycleTask ? "编辑履约任务" : "新增履约任务"}
|
||||
open={lifecycleTaskModalOpen}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
HistoryOutlined,
|
||||
LinkOutlined,
|
||||
PlusOutlined,
|
||||
RocketOutlined,
|
||||
RollbackOutlined,
|
||||
StopOutlined,
|
||||
} from "@ant-design/icons";
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
recordPromptRunLink,
|
||||
rollbackPromptAsset,
|
||||
runPromptAssetEvaluation,
|
||||
seedFirstPriorityPromptBlueprint,
|
||||
unpublishPromptAsset,
|
||||
} from "@/services/prompt-asset";
|
||||
import { useAuthStore } from "@/stores/auth.store";
|
||||
@@ -390,6 +392,24 @@ export default function PromptAssetsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSeedFirstPriorityBlueprint = async () => {
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const result = await seedFirstPriorityPromptBlueprint();
|
||||
if (result.code !== 0) {
|
||||
messageApi.error(result.message || "第一优先级 Prompt 种入失败");
|
||||
return;
|
||||
}
|
||||
const summary = result.data.summary;
|
||||
messageApi.success(`已种入 ${summary.prompt_blueprint_count} 个 Prompt,新增 ${summary.created} 个,更新 ${summary.updated} 个,复用 ${summary.reused} 个`);
|
||||
await loadAssets(1, pageSize, promptTypeFilter, statusFilter);
|
||||
} catch (error) {
|
||||
messageApi.error(error instanceof Error ? error.message : "第一优先级 Prompt 种入失败");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRecordRunLink = async (values: RunLinkFormValues) => {
|
||||
if (!selectedDetail) {
|
||||
return;
|
||||
@@ -706,6 +726,9 @@ export default function PromptAssetsPage() {
|
||||
title="Prompt 资产列表"
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Button icon={<RocketOutlined />} loading={submitting} onClick={() => void handleSeedFirstPriorityBlueprint()}>
|
||||
种入第一优先级
|
||||
</Button>
|
||||
<Select
|
||||
value={promptTypeFilter}
|
||||
style={{ width: 180 }}
|
||||
|
||||
Reference in New Issue
Block a user