1218 lines
53 KiB
Python
1218 lines
53 KiB
Python
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,
|
||
)
|
||
)
|