Files
ai-contract-platform/backend/app/api/system.py
T

1218 lines
53 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, Header, HTTPException, status
from sqlalchemy import select, text
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.response import success_response
from app.db import get_db_session
from app.core.security import verify_password
from app.clients.shared_ai_platform import (
SharedAIPlatformClient,
get_shared_ai_platform_client,
resolve_shared_ai_platform_runtime_config,
)
from app.core.roles import ROLE_ADMIN
from app.models.user import User
from app.services.audit_log_service import AuditLogService
from app.services.auth_service import AuthService, DEFAULT_ADMIN_PASSWORD, DEFAULT_ADMIN_USERNAME
from app.services.governance_service import GovernanceService
from app.services.model_setting_service import ModelSettingService
from app.services.template_service import TemplateService
router = APIRouter(prefix="/system", tags=["system"])
WEAK_SECRET_KEYS = {
"change-me",
"change-me-in-production",
"change_me",
"change_me_to_a_random_64_plus_character_secret_for_production",
}
PLACEHOLDER_CONFIG_VALUES = {
"change-me",
"change-me-in-production",
"change_me",
"changeme",
"replace_me",
"replace-me",
"todo",
"tbd",
"your-secret-key",
"change_me_to_a_random_64_plus_character_secret_for_production",
"change_me_minio_access_key",
"change_me_minio_secret_key",
"change_me_shared_ai_api_key",
}
ROUTE_PERMISSION_SCOPE = "system"
ROUTE_PERMISSION_ACTIONS = {
"readiness": "system:readiness:get",
"shared_ai": "system:shared_ai:get",
"admin_snapshot": "system:admin_snapshot:get",
"p0_mainline": "system:p0_mainline:get",
"seed_pilot_demo_assets": "system:pilot_demo_assets:seed",
"pilot_scope": "system:pilot_scope:get",
"pilot_materials": "system:pilot_materials:get",
"pilot_error_feedback": "system:pilot_error_feedback:get",
"pilot_launch_pack": "system:pilot_launch_pack:get",
}
P0_MAINLINE_VERSION = "p0-mainline-alignment-2026-05-12"
P0_MAINLINE_SOURCE_DOCUMENT = "project-docs/AI合同起草与审查助手-P0实现主线拆分方案-2026-05-11.md"
P0_MAINLINE_PHASE_BOUNDARY = {
"mainline_scope": "Slice 1-8:接口契约、PaddleOCR 解析闭环、Dify 风格证据、自动审查、产品外壳、角色、演示资产、试点边界冻结。",
"next_stage_scope": "Slice 9+:页面可读性、试点材料、异常反馈、启动包等试点运营层能力。",
"rule": "主线推进先验证来源项目、落点归属、合同平台职责和验收口径;试点运营材料不能替代 P0 核心能力验收。",
}
P0_MAINLINE_CAPABILITIES: List[Dict[str, Any]] = [
{
"key": "p0_a_document_parsing",
"lane": "P0-A 文档解析",
"source_project": "D:\\AI Project\\PaddleOCR",
"absorbed_capability": "OCR、版面识别、PDF/图片文本抽取的能力模型。",
"target_layer": "共享文档解析层",
"contract_platform_responsibility": "上传入口、解析状态、审查接入、失败提示和审查详情证据展示。",
"acceptance": "PDF、图片、扫描件可进入审查,且失败时能看到解析状态、失败原因和下一步动作。",
"reference_policy": "只参考 PaddleOCR 的文档解析能力模型,不在合同业务层复制一套 OCR 引擎。",
"evidence": [
"review_service 输出 source_document、document_analysis、ocr_gate_status",
"/reviews/[id] 展示 OCR/PDF 解析证据",
"backend/tools/pdf_acceptance_gate.py 覆盖文本型 PDF、扫描件和图片 PDF",
],
},
{
"key": "p0_b_shared_ai_orchestration",
"lane": "P0-B 共享AI编排",
"source_project": "D:\\AI Project\\dify",
"absorbed_capability": "Workflow、知识库、Prompt/Model 留痕、运行日志和来源证据模型。",
"target_layer": "shared-ai-platform / shared-ai-platform-java",
"contract_platform_responsibility": "传入合同任务,展示 provider、model、prompt、workflow run、知识来源和执行状态。",
"acceptance": "审查和问答可追踪 provider/model/prompt/workflow run/knowledge source。",
"reference_policy": "吸收 Dify 的 workflow、knowledge、trace 模型,不恢复业务仓库直连 Dify runtime。",
"evidence": [
"shared_ai_platform_client 作为合同平台到共享 AI 平台的边界",
"ai_trace 暴露 provider/model/prompt_version/workflow_run_id",
"/reviews/[id] 展示 Dify 风格运行证据与知识来源",
],
},
{
"key": "p0_c_chat_experience",
"lane": "P0-C Chat体验",
"source_project": "D:\\AI Project\\NextChat",
"absorbed_capability": "会话历史、上下文、多轮追问、快捷追问和流式反馈体验。",
"target_layer": "共享对话层 + 合同 Chat 页",
"contract_platform_responsibility": "绑定审查上下文,支持围绕风险项继续追问,并保留合同业务语义。",
"acceptance": "审查详情可进入 Chat,Chat 可携带 review_context 和 conversation_id 持续追问。",
"reference_policy": "只吸收 NextChat 的交互体验,不把合同业务页面替换成通用聊天产品。",
"evidence": [
"chat API 接收 review_context 并转发共享 AI 平台",
"/chat 展示审查上下文、来源和异常反馈",
"/conversations/{conversation_id} 可回看会话上下文",
],
},
{
"key": "p0_d_product_shell",
"lane": "P0-D 产品外壳",
"source_project": "D:\\AI Project\\open-saas",
"absorbed_capability": "登录、团队、控制台、官网、试用入口和基础角色视图。",
"target_layer": "共享身份 / 产品壳层 + 合同业务首页",
"contract_platform_responsibility": "保留合同业务工作台、演示入口、试点申请、基础角色导航和权限拦截。",
"acceptance": "系统像正式试点产品而不是内部原型,管理员、法务、业务角色有清晰入口。",
"reference_policy": "吸收 open-saas 的产品壳和账号组织思路,不把合同产品改成通用 SaaS 模板。",
"evidence": [
"/demo-request 支持公开演示申请",
"/pilot-requests 支持管理员跟进试点申请",
"admin/legal/business 角色导航和用户管理已接入",
],
},
{
"key": "p0_e_auto_review_loop",
"lane": "P0-E 自动审查闭环",
"source_project": "现有系统 + D:\\AI Project\\dify workflow 思路",
"absorbed_capability": "生成到审查的流程编排、状态回填、报告入口和可追问上下文。",
"target_layer": "共享工作流层 + 合同业务层",
"contract_platform_responsibility": "生成结果映射审查任务,回填 auto_review 状态,并提供审查详情、Chat 和报告下载入口。",
"acceptance": "生成 -> 审查 -> 追问 -> 报告可跑通,且异常状态客户可读、QA 可验证。",
"reference_policy": "自动审查保留合同业务语义,底层执行状态尽量对齐共享 workflow 状态。",
"evidence": [
"contract_generation_service 回填 auto_review",
"/contracts 和 /contracts/[id] 提供审查详情、Chat、报告入口",
"slice4-auto-review-closed-loop-e2e 验证生成到报告链路",
],
},
]
P0_MAINLINE_GUARDRAILS: List[Dict[str, str]] = [
{
"key": "paddleocr_reference_only",
"label": "PaddleOCR 只作为解析能力参考",
"description": "合同平台消费结构化解析结果,不把 OCR 引擎硬塞进合同业务服务。",
},
{
"key": "dify_capability_model_only",
"label": "Dify 只吸收能力模型",
"description": "保留共享 AI 平台边界,不恢复业务仓库直连 Dify runtime。",
},
{
"key": "nextchat_experience_only",
"label": "NextChat 只吸收交互体验",
"description": "合同 Chat 页仍围绕审查上下文、风险项和法务追问展开。",
},
{
"key": "open_saas_shell_only",
"label": "open-saas 只吸收产品壳思路",
"description": "账号、角色和演示入口服务合同业务,不把平台改成通用 SaaS 模板。",
},
{
"key": "auto_review_business_semantics",
"label": "自动审查保留合同语义",
"description": "底层状态对齐共享 workflow,但页面和报告继续表达合同生成、审查、追问、报告闭环。",
},
]
P0_MAINLINE_ACCEPTANCE = [
"五条 P0 主线均暴露 source_project、target_layer、contract_platform_responsibility 和 acceptance。",
"readiness 内嵌 p0_mainline,QA 可在同一入口确认主线没有被试点运营材料替代。",
"前端 P0 主线矩阵展示来源项目、吸收能力、落点归属、合同平台职责、验收口径和证据。",
"主线 guardrails 明确禁止复制 Dify/NextChat/open-saas 整套产品或在业务仓库重复建设共享能力。",
]
PILOT_SCOPE_VERSION = "pilot-scope-2026-05-12"
PILOT_SCOPE_SOURCE_DOCUMENT = "project-docs/AI合同起草与审查助手-下一阶段实施建议-2026-05-11.md"
PILOT_SCOPE_INCLUDED: List[Dict[str, str]] = [
{
"key": "template_generation",
"label": "模板生成",
"owner": "business",
"acceptance": "可选择固定演示模板并生成合同文本",
},
{
"key": "contract_preview",
"label": "合同预览",
"owner": "business",
"acceptance": "生成后可查看合同正文和下载资产",
},
{
"key": "review_result",
"label": "审查结果",
"owner": "legal",
"acceptance": "合同文本、PDF 或图片输入可产出风险摘要和审查意见",
},
{
"key": "review_chat_followup",
"label": "Chat 审查追问",
"owner": "legal",
"acceptance": "可带入审查上下文连续追问依据、解释和修改建议",
},
{
"key": "ocr_pdf_processing",
"label": "OCR/PDF 处理",
"owner": "legal",
"acceptance": "普通 PDF、扫描件和图片合同有明确解析状态与失败原因",
},
{
"key": "report_download",
"label": "报告下载",
"owner": "legal",
"acceptance": "审查完成后可下载审查报告",
},
{
"key": "knowledge_enhancement",
"label": "知识库增强",
"owner": "admin",
"acceptance": "试点演示知识库可作为问答和审查依据来源",
},
]
PILOT_SCOPE_EXCLUDED: List[Dict[str, str]] = [
{
"key": "complex_approval",
"label": "复杂审批流",
"reason": "不作为试点启动前必交,保留到后续组织流程增强",
},
{
"key": "advanced_multi_department_permissions",
"label": "多部门高级权限",
"reason": "当前只冻结管理员、法务、业务三个基础角色",
},
{
"key": "advanced_reporting",
"label": "高级报表",
"reason": "试点阶段以审查报告和基础审计留痕为准",
},
{
"key": "large_scale_multi_tenant_strategy",
"label": "大规模多租户策略",
"reason": "先做单组织受控试点,再根据真实反馈扩展",
},
]
PILOT_SCOPE_DEFECT_POLICY: List[Dict[str, str]] = [
{
"level": "must_fix",
"label": "必须修复",
"description": "生成、审查、Chat、报告、登录、演示资产初始化等主链路不可用。",
},
{
"level": "pilot_blocking",
"label": "试点阻塞",
"description": "客户无法完成约定场景,或结果缺少明确状态、错误原因和下一步动作。",
},
{
"level": "later_enhancement",
"label": "后续增强",
"description": "复杂审批、多部门空间、高级报表和规模化 SaaS 能力。",
},
]
PILOT_DEMO_KNOWLEDGE_BASE_NAME = "AI合同试点演示知识库"
PILOT_DEMO_KNOWLEDGE_DOCUMENT_NAME = "pilot-contract-review-playbook.txt"
PILOT_DEMO_KNOWLEDGE_DOCUMENT_TEXT = """AI合同起草与审查助手试点演示知识卡
1. 采购合同演示重点:付款节点必须与验收标准、发票条件、违约责任保持一致。
2. 服务合同演示重点:服务范围、交付标准、验收周期、知识产权归属需要清晰。
3. 保密协议演示重点:保密信息范围、例外情形、存续期限、违约责任需要明确。
4. Chat追问建议:围绕高风险条款询问法律依据、修改建议和可替换条款。
5. 试点验收目标:验证合同生成、自动审查、连续追问、报告下载和知识来源展示。
"""
PILOT_MATERIALS_VERSION = "pilot-materials-2026-05-12"
PILOT_MATERIALS_SOURCE_DOCUMENT = "project-docs/AI合同起草与审查助手-下一阶段实施建议-2026-05-11.md"
PILOT_MATERIALS: List[Dict[str, Any]] = [
{
"key": "pilot_brief",
"title": "试点说明文档",
"audience": "客户负责人 / 项目经理",
"purpose": "说明试点目标、场景范围、角色安排和成功标准,避免试点过程中目标漂移。",
"status": "ready",
"deliverable": "AI合同起草与审查助手-试点说明.md",
"checklist": ["试点目标", "试点范围", "参与角色", "成功标准", "反馈节奏"],
},
{
"key": "user_guide",
"title": "用户使用手册",
"audience": "法务用户 / 业务用户",
"purpose": "让试点用户按固定路径完成合同生成、合同审查、Chat 追问和报告下载。",
"status": "ready",
"deliverable": "AI合同起草与审查助手-用户使用手册.md",
"checklist": ["登录入口", "合同生成", "发起审查", "风险项追问", "报告下载"],
},
{
"key": "admin_guide",
"title": "管理员手册",
"audience": "客户管理员 / 交付负责人",
"purpose": "指导管理员准备演示资产、维护试点用户、查看 readiness 和处理常见阻塞。",
"status": "ready",
"deliverable": "AI合同起草与审查助手-管理员手册.md",
"checklist": ["演示资产初始化", "试点用户管理", "权限边界", "知识库准备", "阻塞排查"],
},
{
"key": "feedback_template",
"title": "试点反馈模板",
"audience": "试点用户 / 客户负责人",
"purpose": "把客户反馈收敛到场景、任务、结果质量、可解释性和下一步建议,便于迭代排期。",
"status": "ready",
"deliverable": "AI合同起草与审查助手-试点反馈模板.md",
"checklist": ["使用场景", "输入样本", "结果评分", "问题描述", "期望改进"],
},
{
"key": "faq",
"title": "FAQ",
"audience": "试点用户 / 销售 / 交付团队",
"purpose": "提前回答账号、数据、OCR、审查结论、Chat 追问和报告下载相关问题。",
"status": "ready",
"deliverable": "AI合同起草与审查助手-FAQ.md",
"checklist": ["账号问题", "数据边界", "OCR/PDF", "审查可信度", "报告与导出"],
},
]
PILOT_FEEDBACK_TEMPLATE: List[Dict[str, str]] = [
{"key": "scenario", "label": "使用场景", "placeholder": "例如:采购合同预审 / 服务合同起草 / 保密协议复核"},
{"key": "input_sample", "label": "输入样本", "placeholder": "说明使用的模板、合同类型或上传文件"},
{"key": "result_rating", "label": "结果评分", "placeholder": "1-5 分,并说明主要原因"},
{"key": "issue", "label": "问题描述", "placeholder": "记录不准确、不清楚、缺少依据或操作不顺的地方"},
{"key": "expected_change", "label": "期望改进", "placeholder": "希望下一轮优先改进的功能或展示方式"},
]
PILOT_ERROR_FEEDBACK_VERSION = "pilot-error-feedback-2026-05-12"
PILOT_ERROR_FEEDBACK_SOURCE_DOCUMENT = "project-docs/AI合同起草与审查助手-下一阶段实施建议-2026-05-11.md"
PILOT_ERROR_FEEDBACK_ITEMS: List[Dict[str, Any]] = [
{
"key": "ocr_pdf_parse_failed",
"title": "OCR/PDF 未能提取正文",
"scope": "contract_review_upload",
"user_message": "系统未能从文件中提取可审查正文,请换成可复制文本 PDF / DOCX / TXT,或直接粘贴合同正文。",
"technical_signals": [
"source_document.parse_status=failed",
"source_document.failure_reason 非空",
"confidence_summary.document_analysis.ocr_quality.failure_message 非空",
],
"next_actions": [
"上传更清晰的扫描件或文本型 PDF",
"改用 DOCX / TXT",
"直接粘贴合同正文后重新发起审查",
],
"qa_selector": "review-document-parse",
},
{
"key": "pdf_quality_insufficient",
"title": "PDF 质量不足或疑似非合同原文",
"scope": "contract_review_upload",
"user_message": "当前 PDF 的文字质量不足,或疑似上传了平台审查报告,请使用合同原文重新上传。",
"technical_signals": [
"source_document.source_kind=scanned_pdf 或 platform_review_report_pdf",
"source_document.extraction_mode=reject_report_pdf 或 ocr_parse_failed",
"source_document.ocr_gate_status=failed",
],
"next_actions": [
"确认上传的是合同原文而不是审查报告",
"扫描件需提高分辨率并避免歪斜、遮挡",
"如急需审查,优先粘贴合同正文",
],
"qa_selector": "review-exception-feedback",
},
{
"key": "auto_review_failed",
"title": "自动审查未完成",
"scope": "auto_review_workflow",
"user_message": "自动审查未能完成,页面会显示当前阶段、失败原因和下一步建议;可重试时请从原任务重新发起。",
"technical_signals": [
"workflow_state.status=failed 或 partial",
"workflow_state.failure_reason 非空",
"workflow_state.next_action 非空",
"workflow_state.can_retry=true",
],
"next_actions": [
"先查看任务信息中的失败原因和当前阶段",
"可重试时使用原任务重试入口",
"不可重试时按页面建议更换输入材料后重新提交",
],
"qa_selector": "review-task-info",
},
{
"key": "chat_response_failed",
"title": "Chat 回答中断或为空",
"scope": "review_chat_followup",
"user_message": "问答生成中断、超时或返回为空时,页面会保留上一问题并提供重试入口。",
"technical_signals": [
"ChatStreamEvent.type=error",
"RequestError.kind=timeout 或 network",
"stream response body 为空",
],
"next_actions": [
"点击重试上一问题",
"缩短问题并聚焦一个风险点",
"返回审查详情确认上下文后再继续提问",
],
"qa_selector": "chat-exception-feedback",
},
]
PILOT_ERROR_FEEDBACK_ACCEPTANCE = [
"readiness 内嵌 pilot_error_feedback,QA 不登录也能读取标准口径",
"试点交付页展示 OCR/PDF、自动审查、Chat 三类异常的客户可读说明",
"审查详情页继续暴露 parse_status、failure_reason、next_action 和可重试状态",
"Chat 异常保留上一问题并展示可重试说明",
]
PILOT_LAUNCH_PACK_VERSION = "pilot-launch-pack-2026-05-12"
PILOT_LAUNCH_PACK_SOURCE_DOCUMENT = "project-docs/AI合同起草与审查助手-当前工作进度保存-2026-05-11.md"
PILOT_LAUNCH_SCRIPT_STEPS: List[Dict[str, Any]] = [
{
"key": "kickoff_alignment",
"title": "试点启动对齐",
"owner": "交付负责人",
"audience": "客户负责人 / 管理员 / 法务代表 / 业务代表",
"route": "/pilot-readiness",
"talk_track": "先确认试点目标、功能边界、账号角色、样本范围和反馈节奏,避免一边演示一边扩张需求。",
"evidence": ["pilot_scope.status=frozen", "pilot_materials.status=ready", "pilot_error_feedback.status=ready"],
"success_signal": "客户明确本轮试点只验证合同起草、自动审查、Chat 追问、报告和知识来源。",
},
{
"key": "business_contract_generation",
"title": "业务用户生成合同",
"owner": "业务用户",
"audience": "业务代表 / 客户负责人",
"route": "/contracts",
"talk_track": "从模板和变量进入合同初稿,让客户看到业务侧不用从空白文档开始。",
"evidence": ["合同生成结果可预览", "auto_review.review_id 已回填", "生成结果可进入审查详情"],
"success_signal": "业务用户能独立生成一份试点合同,并知道下一步自动审查入口。",
},
{
"key": "legal_auto_review",
"title": "法务审查与证据复核",
"owner": "法务用户",
"audience": "法务代表 / 管理员",
"route": "/review",
"talk_track": "上传或粘贴合同后查看风险总览、风险项、解析证据、运行证据和报告下载状态。",
"evidence": ["parse_status 可见", "workflow_state 可见", "ai_trace 可见", "report_status 可见"],
"success_signal": "法务用户能理解风险结论、证据来源、下一步动作和异常提示。",
},
{
"key": "review_chat_followup",
"title": "围绕审查结果继续追问",
"owner": "法务用户",
"audience": "法务代表 / 业务代表",
"route": "/chat",
"talk_track": "从审查详情带入上下文,继续追问风险依据、修改建议和谈判策略。",
"evidence": ["review_context 已带入", "conversation_id 可回看", "sources 独立展示", "chat-exception-feedback 可验证"],
"success_signal": "客户能感受到系统不是静态报告,而是可继续协作的审查助手。",
},
{
"key": "report_feedback_handoff",
"title": "报告交付与试点反馈",
"owner": "交付负责人",
"audience": "客户负责人 / QA",
"route": "/pilot-readiness",
"talk_track": "用报告、材料包和反馈模板收尾,约定下一轮样本、缺陷和改进优先级。",
"evidence": ["pilot_materials.feedback_template 存在", "pilot_launch_pack.acceptance 存在", "pilot_error_feedback.acceptance 存在"],
"success_signal": "客户能按模板反馈场景、输入样本、结果评分、问题和下一轮期望。",
},
]
PILOT_LAUNCH_READINESS_GATES: List[Dict[str, str]] = [
{
"key": "system_readiness",
"label": "系统 readiness 通过",
"check": "GET /api/v1/system/readiness",
"expected": "status=ready 且 failed_required=0",
},
{
"key": "pilot_scope_frozen",
"label": "试点范围已冻结",
"check": "pilot_scope.status",
"expected": "frozen",
},
{
"key": "pilot_materials_ready",
"label": "试点材料包已准备",
"check": "pilot_materials.status",
"expected": "ready",
},
{
"key": "error_feedback_ready",
"label": "异常反馈口径已准备",
"check": "pilot_error_feedback.status",
"expected": "ready",
},
{
"key": "demo_assets_seedable",
"label": "演示资产可初始化",
"check": "POST /api/v1/system/pilot-demo-assets/seed",
"expected": "管理员可触发且返回模板、知识库、试点角色",
},
]
PILOT_LAUNCH_ROLES: List[Dict[str, str]] = [
{"role": "交付负责人", "responsibility": "主持启动会、确认范围、记录阻塞与下一步动作。"},
{"role": "客户管理员", "responsibility": "确认试点账号、角色、样本边界和内部参与人。"},
{"role": "业务用户", "responsibility": "验证合同生成、模板变量、业务可理解性和输入便利性。"},
{"role": "法务用户", "responsibility": "验证审查结论、风险依据、Chat 追问、报告与异常提示。"},
{"role": "QA", "responsibility": "按 qa:slice12 和 readiness 证据复跑试点启动门禁。"},
]
PILOT_LAUNCH_BLOCKERS: List[Dict[str, str]] = [
{"key": "readiness_failed", "label": "readiness gate 未通过", "action": "先修复必需项,再启动客户演示。"},
{"key": "missing_pilot_roles", "label": "试点账号或角色缺失", "action": "由管理员创建 legal / business 账号后再演示。"},
{"key": "materials_missing", "label": "材料或反馈模板缺失", "action": "补齐试点材料包,确保客户知道如何反馈。"},
{"key": "unreadable_errors", "label": "异常提示客户不可理解", "action": "回到错误反馈面板核对对应 qa_selector。"},
]
PILOT_LAUNCH_ACCEPTANCE = [
"试点启动包可通过 API 和 readiness 同时读取",
"试点交付页展示完整客户演示脚本、启动门禁、角色分工和阻塞处理",
"演示脚本至少覆盖启动对齐、合同生成、自动审查、Chat 追问、报告与反馈五步",
"QA 可用 qa:slice12 复跑页面与接口闭环",
]
def _build_check(key: str, label: str, status: str, detail: str, required: bool = True) -> Dict[str, Any]:
return {
"key": key,
"label": label,
"status": status,
"detail": detail,
"required": required,
}
def _is_production_env() -> bool:
return settings.app_env.strip().lower() in {"production", "prod"}
def _clean_config_value(value: Optional[str]) -> str:
return (value or "").strip()
def _is_placeholder_config_value(value: Optional[str]) -> bool:
normalized_value = _clean_config_value(value).lower()
if not normalized_value:
return False
if normalized_value in PLACEHOLDER_CONFIG_VALUES:
return True
return any(token in normalized_value for token in PLACEHOLDER_CONFIG_VALUES if token.startswith("change_me"))
def _is_weak_minio_credential(value: Optional[str]) -> bool:
return _clean_config_value(value).lower() in {"minioadmin", "minio", "admin", "password"}
def _check_database(db: Session) -> Dict[str, Any]:
try:
db.execute(text("SELECT 1")).scalar()
return _build_check("database", "数据库连接", "ok", "数据库连接可用")
except Exception as exc:
return _build_check("database", "数据库连接", "failed", f"数据库连接失败:{exc.__class__.__name__}")
def _check_runtime_config() -> Dict[str, Any]:
secret_key = settings.secret_key.strip()
production = _is_production_env()
runtime_issues: List[str] = []
if not secret_key or secret_key.lower() in WEAK_SECRET_KEYS or _is_placeholder_config_value(secret_key) or len(secret_key) < 32:
runtime_issues.append("SECRET_KEY 为空、过短或仍为默认占位值")
if not _clean_config_value(settings.database_url) or _is_placeholder_config_value(settings.database_url):
runtime_issues.append("DATABASE_URL 为空或仍包含占位值")
if runtime_issues:
status_value = "failed" if production else "warning"
detail_prefix = "生产环境启动配置不完整" if production else "当前运行配置需要上线前确认"
return _build_check(
"runtime_config",
"运行配置",
status_value,
f"{detail_prefix}:{';'.join(runtime_issues)}",
required=production,
)
return _build_check("runtime_config", "运行配置", "ok", "关键运行配置已设置", required=False)
def _check_object_storage_config() -> Dict[str, Any]:
storage_backend = (settings.file_storage_backend or "minio").strip().lower()
production = _is_production_env()
if storage_backend == "local":
local_storage_dir = _clean_config_value(settings.local_storage_dir)
if not local_storage_dir:
return _build_check("object_storage_config", "对象存储配置", "failed", "本地文件存储目录未配置")
return _build_check(
"object_storage_config",
"对象存储配置",
"warning" if production else "ok",
f"当前使用本地文件存储:{local_storage_dir};生产环境建议使用 MinIO 或等价对象存储",
required=False,
)
if storage_backend != "minio":
return _build_check(
"object_storage_config",
"对象存储配置",
"failed",
f"未知文件存储后端:{storage_backend}",
)
missing_fields: List[str] = []
placeholder_fields: List[str] = []
weak_fields: List[str] = []
minio_config = {
"MINIO_ENDPOINT": settings.minio_endpoint,
"MINIO_BUCKET": settings.minio_bucket,
"MINIO_ACCESS_KEY": settings.minio_access_key,
"MINIO_SECRET_KEY": settings.minio_secret_key,
}
for field_name, value in minio_config.items():
if not _clean_config_value(value):
missing_fields.append(field_name)
elif _is_placeholder_config_value(value):
placeholder_fields.append(field_name)
if production:
for field_name in ("MINIO_ACCESS_KEY", "MINIO_SECRET_KEY"):
if _is_weak_minio_credential(minio_config[field_name]):
weak_fields.append(field_name)
if missing_fields or placeholder_fields or weak_fields:
issue_parts: List[str] = []
if missing_fields:
issue_parts.append(f"缺少 {', '.join(missing_fields)}")
if placeholder_fields:
issue_parts.append(f"{', '.join(placeholder_fields)} 仍为占位值")
if weak_fields:
issue_parts.append(f"{', '.join(weak_fields)} 仍为默认弱凭据")
return _build_check(
"object_storage_config",
"对象存储配置",
"failed",
";".join(issue_parts),
)
return _build_check(
"object_storage_config",
"对象存储配置",
"ok",
f"对象存储配置已就绪:{settings.minio_endpoint} / {settings.minio_bucket}",
)
def _has_model_settings_shared_ai_config(db: Optional[Session]) -> bool:
if db is None:
return False
try:
return ModelSettingService(db).has_any_shared_ai_platform_runtime_config()
except Exception:
return False
def _shared_ai_runtime_configured(db: Optional[Session]) -> bool:
base_url = _clean_config_value(settings.shared_ai_platform_base_url)
api_key = _clean_config_value(settings.shared_ai_platform_api_key)
env_configured = (
bool(base_url)
and bool(api_key)
and not _is_placeholder_config_value(base_url)
and not _is_placeholder_config_value(api_key)
)
return env_configured or _has_model_settings_shared_ai_config(db)
def _check_shared_ai_config(db: Optional[Session] = None) -> Dict[str, Any]:
base_url = _clean_config_value(settings.shared_ai_platform_base_url)
api_key = _clean_config_value(settings.shared_ai_platform_api_key)
legal_review_enabled = settings.shared_ai_platform_legal_review_enabled
production = _is_production_env()
base_url_configured = bool(base_url) and not _is_placeholder_config_value(base_url)
api_key_configured = bool(api_key) and not _is_placeholder_config_value(api_key)
if (base_url_configured and api_key_configured) or _has_model_settings_shared_ai_config(db):
return _build_check(
"shared_ai_config",
"共享 AI 配置",
"ok",
"共享 AI 远端地址与 API Key 已配置",
required=False,
)
if legal_review_enabled and production:
missing_parts = []
if not base_url_configured:
missing_parts.append("SHARED_AI_PLATFORM_BASE_URL")
if not api_key_configured:
missing_parts.append("SHARED_AI_PLATFORM_API_KEY")
return _build_check(
"shared_ai_config",
"共享 AI 配置",
"failed",
f"生产环境已启用远端法律审查,但 {', '.join(missing_parts)} 未配置或仍为占位值",
)
if legal_review_enabled:
return _build_check(
"shared_ai_config",
"共享 AI 配置",
"warning",
"共享 AI 远端法律审查已启用,但远端地址或 API Key 未完整配置;本地开发可继续,生产上线前必须补齐或关闭远端审查",
required=False,
)
return _build_check(
"shared_ai_config",
"共享 AI 配置",
"ok",
"共享 AI 远端审查未启用,当前按本地/非远端模式运行",
required=False,
)
def _check_default_admin_password(db: Session) -> Dict[str, Any]:
try:
admin = db.execute(select(User).where(User.username == DEFAULT_ADMIN_USERNAME)).scalar_one_or_none()
except Exception as exc:
return _build_check(
"default_admin_password",
"默认管理员密码",
"warning",
f"无法确认默认管理员密码状态:{exc.__class__.__name__}",
required=False,
)
if admin is None:
return _build_check(
"default_admin_password",
"默认管理员密码",
"warning",
"默认管理员账号尚未初始化",
required=False,
)
if verify_password(DEFAULT_ADMIN_PASSWORD, admin.password_hash):
return _build_check(
"default_admin_password",
"默认管理员密码",
"warning",
"默认管理员仍在使用初始验收密码,试点前应完成轮换",
required=False,
)
return _build_check("default_admin_password", "默认管理员密码", "ok", "默认管理员密码已轮换", required=False)
def _build_pilot_assets(password_rotated: bool) -> Dict[str, Any]:
return {
"default_admin": {
"username": DEFAULT_ADMIN_USERNAME,
"role": "admin",
"seed_endpoint": f"{settings.api_v1_prefix}/auth/seed-admin",
"password_rotated": password_rotated,
"rotation_required": not password_rotated,
},
"demo_flow": [
"登录默认管理员账号",
"发起一份高风险合同审查",
"从审查详情进入 Chat 追问修改建议",
"把审查上下文带入合同生成",
"上传知识库文档并验证重试语义",
"查看审计日志确认关键动作留痕",
],
"acceptance_gates": [
"前端 lint/build 通过",
"后端 pytest 通过",
"阶段 1/2/3 E2E 均可复跑",
"生产入口 health/readiness 通过",
],
}
def _build_p0_mainline_alignment() -> Dict[str, Any]:
return {
"version": P0_MAINLINE_VERSION,
"status": "aligned",
"source_document": P0_MAINLINE_SOURCE_DOCUMENT,
"updated_at": "2026-05-12T00:00:00+08:00",
"summary": "P0 主线聚焦五条能力吸收线,所有能力都要写清来源项目、落点归属、合同平台职责和验收口径。",
"phase_boundary": P0_MAINLINE_PHASE_BOUNDARY,
"capabilities": P0_MAINLINE_CAPABILITIES,
"guardrails": P0_MAINLINE_GUARDRAILS,
"acceptance": P0_MAINLINE_ACCEPTANCE,
}
def _build_pilot_scope() -> Dict[str, Any]:
return {
"version": PILOT_SCOPE_VERSION,
"status": "frozen",
"source_document": PILOT_SCOPE_SOURCE_DOCUMENT,
"updated_at": "2026-05-12T00:00:00+08:00",
"summary": "试点版只承诺合同起草、合同审查、OCR/PDF、Chat 追问、报告下载和知识库增强主链路。",
"included": PILOT_SCOPE_INCLUDED,
"excluded": PILOT_SCOPE_EXCLUDED,
"defect_policy": PILOT_SCOPE_DEFECT_POLICY,
}
def _build_pilot_materials() -> Dict[str, Any]:
return {
"version": PILOT_MATERIALS_VERSION,
"status": "ready",
"source_document": PILOT_MATERIALS_SOURCE_DOCUMENT,
"updated_at": "2026-05-12T00:00:00+08:00",
"summary": "试点材料包覆盖说明文档、用户手册、管理员手册、反馈模板和 FAQ,服务于受控试点启动与反馈收敛。",
"materials": PILOT_MATERIALS,
"feedback_template": PILOT_FEEDBACK_TEMPLATE,
"acceptance": [
"材料包可在试点交付页查看",
"材料清单覆盖 P0 要求的五类文档",
"反馈字段能支撑场景、结果质量和改进建议收敛",
],
}
def _build_pilot_error_feedback() -> Dict[str, Any]:
return {
"version": PILOT_ERROR_FEEDBACK_VERSION,
"status": "ready",
"source_document": PILOT_ERROR_FEEDBACK_SOURCE_DOCUMENT,
"updated_at": "2026-05-12T00:00:00+08:00",
"summary": "P0 异常反馈覆盖 OCR/PDF 解析、PDF 质量、自动审查和 Chat 问答中断,确保客户能理解,QA 能按字段和页面入口验证。",
"items": PILOT_ERROR_FEEDBACK_ITEMS,
"acceptance": PILOT_ERROR_FEEDBACK_ACCEPTANCE,
}
def _build_pilot_launch_pack() -> Dict[str, Any]:
return {
"version": PILOT_LAUNCH_PACK_VERSION,
"status": "ready",
"source_document": PILOT_LAUNCH_PACK_SOURCE_DOCUMENT,
"updated_at": "2026-05-12T00:00:00+08:00",
"summary": "试点启动包把客户演示脚本、启动门禁、角色分工和阻塞条件固定下来,用于从可交付版本进入小范围受控试点。",
"script_steps": PILOT_LAUNCH_SCRIPT_STEPS,
"readiness_gates": PILOT_LAUNCH_READINESS_GATES,
"roles": PILOT_LAUNCH_ROLES,
"blockers": PILOT_LAUNCH_BLOCKERS,
"acceptance": PILOT_LAUNCH_ACCEPTANCE,
}
def _require_admin_user(current_user: Dict[str, Any]):
if current_user.get("role") != ROLE_ADMIN:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="forbidden")
def _find_knowledge_base_by_name(items: List[Dict[str, Any]], name: str) -> Optional[Dict[str, Any]]:
normalized_name = name.strip().lower()
for item in items:
if str(item.get("name") or "").strip().lower() == normalized_name:
return item
return None
def _find_document_by_file_name(items: List[Dict[str, Any]], file_name: str) -> Optional[Dict[str, Any]]:
normalized_file_name = file_name.strip().lower()
for item in items:
if str(item.get("file_name") or "").strip().lower() == normalized_file_name:
return item
return None
def _seed_pilot_demo_knowledge(
current_user: Dict[str, Any],
shared_ai_platform_client: SharedAIPlatformClient,
) -> Dict[str, Any]:
knowledge_payload = shared_ai_platform_client.list_knowledge_bases(current_user, 1, 50)
knowledge_items = knowledge_payload.get("items", [])
knowledge_base = _find_knowledge_base_by_name(knowledge_items, PILOT_DEMO_KNOWLEDGE_BASE_NAME)
created_knowledge_base = False
if knowledge_base is None:
knowledge_base = shared_ai_platform_client.create_knowledge_base(
current_user,
PILOT_DEMO_KNOWLEDGE_BASE_NAME,
"用于固定试点演示的合同审查知识来源,覆盖采购、服务和保密协议。",
)
created_knowledge_base = True
knowledge_base_id = str(knowledge_base["id"])
documents_payload = shared_ai_platform_client.list_documents(current_user, knowledge_base_id, 1, 50)
document_items = documents_payload.get("items", [])
document = _find_document_by_file_name(document_items, PILOT_DEMO_KNOWLEDGE_DOCUMENT_NAME)
uploaded_document = False
if document is None:
document = shared_ai_platform_client.upload_document(
current_user=current_user,
knowledge_base_id=knowledge_base_id,
file_name=PILOT_DEMO_KNOWLEDGE_DOCUMENT_NAME,
mime_type="text/plain",
content=PILOT_DEMO_KNOWLEDGE_DOCUMENT_TEXT.encode("utf-8"),
)
uploaded_document = True
return {
"status": "ready",
"created_knowledge_base": created_knowledge_base,
"uploaded_document": uploaded_document,
"knowledge_base": knowledge_base,
"document": document,
}
def require_current_user(
authorization: Optional[str] = Header(default=None),
db: Session = Depends(get_db_session),
):
return AuthService(db).require_current_user(authorization)
def get_template_service(db: Session = Depends(get_db_session)) -> TemplateService:
return TemplateService(db)
def get_audit_log_service(db: Session = Depends(get_db_session)) -> AuditLogService:
return AuditLogService(db)
def get_governance_service(db: Session = Depends(get_db_session)) -> GovernanceService:
return GovernanceService(db)
def _build_shared_ai_status(db: Optional[Session] = None, current_user: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
tenant_id = str((current_user or {}).get("tenant_id") or "").strip() or None
runtime_config = resolve_shared_ai_platform_runtime_config(db, tenant_id) if db is not None else None
remote_configured = runtime_config is not None
mode = "remote-preferred" if remote_configured else "configuration-required"
return {
"mode": mode,
"remote_configured": remote_configured,
"base_url": runtime_config.base_url if runtime_config else (settings.shared_ai_platform_base_url.strip() or None),
"api_prefix": runtime_config.api_prefix if runtime_config else settings.shared_ai_platform_api_prefix,
"app_id": runtime_config.app_id if runtime_config else settings.shared_ai_platform_app_id,
"timeout_seconds": runtime_config.timeout_seconds if runtime_config else settings.shared_ai_platform_timeout_seconds,
"config_source": runtime_config.source if runtime_config else None,
"provider": runtime_config.provider if runtime_config else None,
"model": runtime_config.model if runtime_config else None,
"review_model": runtime_config.review_model if runtime_config else None,
"transport": {
"sdk": "local-shared-ai-sdk" if remote_configured else "disabled",
"fallback": "disabled",
},
"knowledge_defaults": {
"retrieval_mode": "hybrid" if remote_configured else "unknown",
},
"capabilities": {
"chat_stream": remote_configured,
"conversation_history": remote_configured,
"knowledge_bases": remote_configured,
"knowledge_documents": remote_configured,
"knowledge_retry": remote_configured,
"legal_review_domain_pack": remote_configured
and (runtime_config.legal_review_enabled if runtime_config else settings.shared_ai_platform_legal_review_enabled),
"local_legal_review_fallback": remote_configured
and (
runtime_config.legal_review_fallback_to_local
if runtime_config
else settings.shared_ai_platform_legal_review_fallback_to_local
),
},
}
def _build_admin_snapshot(
current_user: Dict[str, Any],
shared_ai_platform_client: SharedAIPlatformClient,
template_service: TemplateService,
audit_log_service: AuditLogService,
governance_service: GovernanceService,
) -> Dict[str, Any]:
knowledge_payload = shared_ai_platform_client.list_knowledge_bases(current_user, 1, 8)
knowledge_items = knowledge_payload.get("items", [])
retrieval_modes: Dict[str, int] = {}
for item in knowledge_items:
mode = str(item.get("retrieval_mode") or "default")
retrieval_modes[mode] = retrieval_modes.get(mode, 0) + 1
template_payload = template_service.list_templates(current_user, 1, 20, None)
template_items = template_payload.get("items", [])
template_type_counts: Dict[str, int] = {}
enabled_count = 0
for item in template_items:
if item.get("enabled"):
enabled_count += 1
contract_type = str(item.get("contract_type") or "unspecified")
template_type_counts[contract_type] = template_type_counts.get(contract_type, 0) + 1
audit_payload = audit_log_service.list_logs(current_user, 1, 6, None)
audit_items = audit_payload.get("items", [])
latest_audit = audit_items[0] if audit_items else None
governance_payload = governance_service.get_release_summary(current_user)
governance_summary = governance_payload.get("summary", {})
assignment_payload = governance_service.list_assignments(current_user, 1, 6, None, "pending", None, None)
assignment_items = assignment_payload.get("items", [])
return {
"generated_at": datetime.now(timezone.utc).isoformat(),
"knowledge_bases": {
"total": knowledge_payload.get("total", len(knowledge_items)),
"active_count": len([item for item in knowledge_items if item.get("status") == "active"]),
"retrieval_modes": retrieval_modes,
"items": [
{
"id": item.get("id"),
"name": item.get("name"),
"status": item.get("status"),
"document_count": item.get("document_count"),
"retrieval_mode": item.get("retrieval_mode"),
"updated_at": item.get("updated_at"),
}
for item in knowledge_items[:5]
],
},
"templates": {
"total": template_payload.get("total", len(template_items)),
"enabled_count": enabled_count,
"contract_type_counts": template_type_counts,
"items": [
{
"id": item.get("id"),
"name": item.get("name"),
"contract_type": item.get("contract_type"),
"enabled": item.get("enabled"),
"updated_at": item.get("updated_at"),
}
for item in template_items[:5]
],
},
"audit_logs": {
"total": audit_payload.get("total", len(audit_items)),
"latest_action": latest_audit.get("action") if latest_audit else None,
"latest_created_at": latest_audit.get("created_at") if latest_audit else None,
"items": [
{
"id": item.get("id"),
"action": item.get("action"),
"resource_type": item.get("resource_type"),
"created_at": item.get("created_at"),
"operator": item.get("operator"),
}
for item in audit_items[:5]
],
},
"governance": {
"total_assets": governance_summary.get("total_assets", 0),
"pending_assignments": governance_summary.get("pending_assignments", 0),
"in_review_assignments": governance_summary.get("in_review_assignments", 0),
"blocking_items": governance_summary.get("blocking_items", []),
"recent_batches": [
{
"id": item.get("id"),
"batch_name": item.get("batch_name"),
"status": item.get("status"),
}
for item in governance_payload.get("recent_batches", [])[:3]
],
"pending_assignment_items": [
{
"id": item.get("id"),
"assignment_role": item.get("assignment_role"),
"assignee_name": item.get("assignee_name"),
"status": item.get("status"),
"priority": item.get("priority"),
"asset_name": item.get("asset_name"),
}
for item in assignment_items[:5]
],
},
}
@router.get("/readiness")
def get_system_readiness(db: Session = Depends(get_db_session)):
default_admin_password_check = _check_default_admin_password(db)
checks: List[Dict[str, Any]] = [
_build_check("api", "API 服务", "ok", "FastAPI 服务已响应"),
_check_database(db),
_check_runtime_config(),
_check_object_storage_config(),
_check_shared_ai_config(db),
_build_check("auth_seed", "默认管理员初始化", "ok", "默认管理员初始化入口可用", required=False),
default_admin_password_check,
]
failed_required = [item for item in checks if item["required"] and item["status"] == "failed"]
warning_count = len([item for item in checks if item["status"] == "warning"])
status = "ready" if not failed_required else "degraded"
return success_response(
{
"status": status,
"generated_at": datetime.now(timezone.utc).isoformat(),
"app": {
"name": settings.app_name,
"environment": settings.app_env,
"api_prefix": settings.api_v1_prefix,
},
"deployment": {
"app_env": settings.app_env,
"api_prefix": settings.api_v1_prefix,
"file_storage_backend": (settings.file_storage_backend or "minio").strip().lower(),
"shared_ai_remote_configured": _shared_ai_runtime_configured(db),
"shared_ai_legal_review_enabled": settings.shared_ai_platform_legal_review_enabled,
},
"checks": checks,
"summary": {
"total": len(checks),
"failed_required": len(failed_required),
"warnings": warning_count,
},
"pilot_assets": _build_pilot_assets(default_admin_password_check["status"] == "ok"),
"p0_mainline": _build_p0_mainline_alignment(),
"pilot_scope": _build_pilot_scope(),
"pilot_materials": _build_pilot_materials(),
"pilot_error_feedback": _build_pilot_error_feedback(),
"pilot_launch_pack": _build_pilot_launch_pack(),
}
)
@router.get("/shared-ai")
def get_shared_ai_status(
current_user: dict = Depends(require_current_user),
db: Session = Depends(get_db_session),
):
return success_response(_build_shared_ai_status(db, current_user))
@router.get("/p0-mainline")
def get_p0_mainline_alignment():
return success_response(_build_p0_mainline_alignment())
@router.get("/pilot-scope")
def get_pilot_scope():
return success_response(_build_pilot_scope())
@router.get("/pilot-materials")
def get_pilot_materials():
return success_response(_build_pilot_materials())
@router.get("/pilot-error-feedback")
def get_pilot_error_feedback():
return success_response(_build_pilot_error_feedback())
@router.get("/pilot-launch-pack")
def get_pilot_launch_pack():
return success_response(_build_pilot_launch_pack())
@router.post("/pilot-demo-assets/seed")
def seed_pilot_demo_assets(
current_user: dict = Depends(require_current_user),
shared_ai_platform_client: SharedAIPlatformClient = Depends(get_shared_ai_platform_client),
template_service: TemplateService = Depends(get_template_service),
audit_log_service: AuditLogService = Depends(get_audit_log_service),
db: Session = Depends(get_db_session),
):
_require_admin_user(current_user)
templates_result = template_service.seed_default_templates(current_user)
knowledge_result = _seed_pilot_demo_knowledge(current_user, shared_ai_platform_client)
default_admin_password_check = _check_default_admin_password(db)
pilot_assets = _build_pilot_assets(default_admin_password_check["status"] == "ok")
result = {
"seeded_at": datetime.now(timezone.utc).isoformat(),
"templates": templates_result,
"knowledge": knowledge_result,
"demo_accounts": {
"default_admin": pilot_assets["default_admin"],
"roles": [ROLE_ADMIN, "legal", "business"],
},
"demo_flow": pilot_assets["demo_flow"],
"acceptance_gates": pilot_assets["acceptance_gates"],
}
audit_log_service.record_event(
tenant_id=current_user.get("tenant_id"),
user_id=current_user.get("id"),
action="seed_pilot_demo_assets",
resource_type="system",
resource_id=None,
detail_json={
"templates_created_count": templates_result.get("created_count", 0),
"templates_existing_count": templates_result.get("existing_count", 0),
"knowledge_status": knowledge_result.get("status"),
"knowledge_base_id": (knowledge_result.get("knowledge_base") or {}).get("id"),
"document_id": (knowledge_result.get("document") or {}).get("id"),
},
)
return success_response(result)
@router.get("/admin-snapshot")
def get_admin_snapshot(
current_user: dict = Depends(require_current_user),
shared_ai_platform_client: SharedAIPlatformClient = Depends(get_shared_ai_platform_client),
template_service: TemplateService = Depends(get_template_service),
audit_log_service: AuditLogService = Depends(get_audit_log_service),
governance_service: GovernanceService = Depends(get_governance_service),
):
return success_response(
_build_admin_snapshot(
current_user=current_user,
shared_ai_platform_client=shared_ai_platform_client,
template_service=template_service,
audit_log_service=audit_log_service,
governance_service=governance_service,
)
)