diff --git a/.gitignore b/.gitignore
index 5ef864f1..d287a170 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ frontend/node_modules/
*.pyc
__pycache__/
.DS_Store
+._*
backend/.pytest_cache/
# Local secrets and generated artifacts
diff --git a/AGENTS.md b/AGENTS.md
index ebb2e21d..5cd49ed1 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,23 +1,8 @@
-## gstack (REQUIRED — global install)
+## AI-assisted work
-**Before doing ANY work, verify gstack is installed:**
+This repo no longer uses gstack.
-```bash
-test -d ~/.Codex/skills/gstack/bin && echo "GSTACK_OK" || echo "GSTACK_MISSING"
-```
-
-If GSTACK_MISSING: STOP. Do not proceed. Tell the user:
-
-> gstack is required for all AI-assisted work in this repo.
-> Install it:
-> ```bash
-> git clone --depth 1 https://github.com/garrytan/gstack.git ~/.Codex/skills/gstack
-> cd ~/.Codex/skills/gstack && ./setup --team
-> ```
-> Then restart your AI coding tool.
-
-Do not skip skills, ignore gstack errors, or work around missing gstack.
-
-Using gstack skills: After install, skills like /qa, /ship, /review, /investigate,
-and /browse are available. Use /browse for all web browsing.
-Use ~/.Codex/skills/gstack/... for gstack file paths (the global path).
+- Do not require, install, or verify `~/.Codex/skills/gstack` before working.
+- Do not block AI-assisted work when gstack is missing.
+- Use the current AI coding tool's built-in capabilities and the repository's scripts directly.
+- For web browsing, use the available browser or web tools directly; do not require `/browse`.
diff --git a/CLAUDE.md b/CLAUDE.md
index 02e14446..712d6e3e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,23 +1,8 @@
-## gstack (REQUIRED — global install)
+## AI-assisted work
-**Before doing ANY work, verify gstack is installed:**
+This repo no longer uses gstack.
-```bash
-test -d ~/.claude/skills/gstack/bin && echo "GSTACK_OK" || echo "GSTACK_MISSING"
-```
-
-If GSTACK_MISSING: STOP. Do not proceed. Tell the user:
-
-> gstack is required for all AI-assisted work in this repo.
-> Install it:
-> ```bash
-> git clone --depth 1 https://github.com/garrytan/gstack.git ~/.claude/skills/gstack
-> cd ~/.claude/skills/gstack && ./setup --team
-> ```
-> Then restart your AI coding tool.
-
-Do not skip skills, ignore gstack errors, or work around missing gstack.
-
-Using gstack skills: After install, skills like /qa, /ship, /review, /investigate,
-and /browse are available. Use /browse for all web browsing.
-Use ~/.claude/skills/gstack/... for gstack file paths (the global path).
+- Do not require, install, or verify `~/.claude/skills/gstack` before working.
+- Do not block AI-assisted work when gstack is missing.
+- Use the current AI coding tool's built-in capabilities and the repository's scripts directly.
+- For web browsing, use the available browser or web tools directly; do not require `/browse`.
diff --git a/backend/.dockerignore b/backend/.dockerignore
new file mode 100644
index 00000000..d466f601
--- /dev/null
+++ b/backend/.dockerignore
@@ -0,0 +1,7 @@
+._*
+.DS_Store
+__pycache__/
+*.pyc
+.pytest_cache/
+storage/
+tmp_migration_check.db
diff --git a/backend/Dockerfile b/backend/Dockerfile
index 3c60c766..941b5ea1 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -20,6 +20,7 @@ RUN set -eux; \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt ./
+COPY vendor/shared-ai-platform-sdk ./vendor/shared-ai-platform-sdk
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
diff --git a/backend/app/api/contract_approvals.py b/backend/app/api/contract_approvals.py
new file mode 100644
index 00000000..12be354e
--- /dev/null
+++ b/backend/app/api/contract_approvals.py
@@ -0,0 +1,138 @@
+from typing import Optional
+
+from fastapi import APIRouter, Depends, Header, Request
+from pydantic import BaseModel, Field
+from sqlalchemy.orm import Session
+
+from app.core.response import success_response
+from app.db import get_db_session
+from app.services.audit_log_service import AuditLogService
+from app.services.auth_service import AuthService
+from app.services.contract_approval_service import ContractApprovalService
+
+router = APIRouter(prefix="/contract-approvals", tags=["contract-approvals"])
+
+
+class ContractApprovalSubmitRequest(BaseModel):
+ contract_id: str = Field(..., min_length=1, max_length=64)
+ title: Optional[str] = Field(default=None, max_length=200)
+ comment: Optional[str] = Field(default=None, max_length=2000)
+ assignee_id: Optional[str] = Field(default=None, max_length=64)
+ assignee_name: Optional[str] = Field(default=None, max_length=100)
+ step_type: str = Field(default="legal_review", max_length=64)
+
+
+class ContractApprovalDecisionRequest(BaseModel):
+ decision: str = Field(..., min_length=1, max_length=32)
+ comment: Optional[str] = Field(default=None, max_length=2000)
+
+
+def get_contract_approval_service(db: Session = Depends(get_db_session)) -> ContractApprovalService:
+ return ContractApprovalService(db)
+
+
+def get_audit_log_service(db: Session = Depends(get_db_session)) -> AuditLogService:
+ return AuditLogService(db)
+
+
+def require_current_user(
+ authorization: Optional[str] = Header(default=None),
+ db: Session = Depends(get_db_session),
+):
+ return AuthService(db).require_current_user(authorization)
+
+
+@router.get("")
+def list_contract_approvals(
+ page: int = 1,
+ page_size: int = 20,
+ status_value: Optional[str] = None,
+ scope: str = "assigned_to_me",
+ contract_approval_service: ContractApprovalService = Depends(get_contract_approval_service),
+ current_user: dict = Depends(require_current_user),
+):
+ return success_response(
+ contract_approval_service.list_approvals(
+ current_user=current_user,
+ page=page,
+ page_size=page_size,
+ status_value=status_value,
+ scope=scope,
+ )
+ )
+
+
+@router.get("/contracts/{contract_id}")
+def list_contract_approval_history(
+ contract_id: str,
+ contract_approval_service: ContractApprovalService = Depends(get_contract_approval_service),
+ current_user: dict = Depends(require_current_user),
+):
+ return success_response(contract_approval_service.list_contract_approvals(current_user, contract_id))
+
+
+@router.post("")
+def submit_contract_approval(
+ payload: ContractApprovalSubmitRequest,
+ request: Request,
+ contract_approval_service: ContractApprovalService = Depends(get_contract_approval_service),
+ audit_log_service: AuditLogService = Depends(get_audit_log_service),
+ current_user: dict = Depends(require_current_user),
+):
+ result = contract_approval_service.submit_approval(
+ current_user=current_user,
+ contract_id=payload.contract_id,
+ title=payload.title,
+ comment=payload.comment,
+ assignee_id=payload.assignee_id,
+ assignee_name=payload.assignee_name,
+ step_type=payload.step_type,
+ )
+ audit_log_service.record_event(
+ tenant_id=current_user.get("tenant_id"),
+ user_id=current_user.get("id"),
+ action="contract_approval_submit",
+ resource_type="contract_approval",
+ resource_id=result.get("id"),
+ detail_json={
+ "contract_id": result.get("contract_id"),
+ "step_type": result.get("step_type"),
+ "assignee_id": result.get("assignee_id"),
+ "status": result.get("status"),
+ },
+ ip_address=request.client.host if request.client else None,
+ user_agent=request.headers.get("user-agent"),
+ )
+ return success_response(result)
+
+
+@router.post("/{approval_id}/decision")
+def decide_contract_approval(
+ approval_id: str,
+ payload: ContractApprovalDecisionRequest,
+ request: Request,
+ contract_approval_service: ContractApprovalService = Depends(get_contract_approval_service),
+ audit_log_service: AuditLogService = Depends(get_audit_log_service),
+ current_user: dict = Depends(require_current_user),
+):
+ result = contract_approval_service.decide_approval(
+ current_user=current_user,
+ approval_id=approval_id,
+ decision=payload.decision,
+ comment=payload.comment,
+ )
+ audit_log_service.record_event(
+ tenant_id=current_user.get("tenant_id"),
+ user_id=current_user.get("id"),
+ action=f"contract_approval_{result.get('status')}",
+ resource_type="contract_approval",
+ resource_id=result.get("id"),
+ detail_json={
+ "contract_id": result.get("contract_id"),
+ "decision": payload.decision,
+ "status": result.get("status"),
+ },
+ ip_address=request.client.host if request.client else None,
+ user_agent=request.headers.get("user-agent"),
+ )
+ return success_response(result)
diff --git a/backend/app/main.py b/backend/app/main.py
index 6cb99b6d..f66ffee3 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -5,6 +5,7 @@ from fastapi.middleware.cors import CORSMiddleware
from app.api.audit_logs import router as audit_logs_router
from app.api.auth import router as auth_router
from app.api.chat import router as chat_router
+from app.api.contract_approvals import router as contract_approvals_router
from app.api.clauses import router as clauses_router
from app.api.contracts import router as contracts_router
from app.api.conversations import router as conversations_router
@@ -46,6 +47,7 @@ app.include_router(auth_router, prefix=settings.api_v1_prefix)
app.include_router(reviews_router, prefix=settings.api_v1_prefix)
app.include_router(chat_router, prefix=settings.api_v1_prefix)
app.include_router(clauses_router, prefix=settings.api_v1_prefix)
+app.include_router(contract_approvals_router, prefix=settings.api_v1_prefix)
app.include_router(contracts_router, prefix=settings.api_v1_prefix)
app.include_router(conversations_router, prefix=settings.api_v1_prefix)
app.include_router(files_router, prefix=settings.api_v1_prefix)
diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py
index e7f4b3ff..f8e94507 100644
--- a/backend/app/models/__init__.py
+++ b/backend/app/models/__init__.py
@@ -5,6 +5,7 @@ from app.models.commercial_order import CommercialOrder
from app.models.conversation import Conversation
from app.models.contract_template import ContractTemplate
from app.models.contract_template_version import ContractTemplateVersion
+from app.models.contract_approval_step import ContractApprovalStep
from app.models.contract_lifecycle_task import ContractLifecycleTask
from app.models.file import File
from app.models.generated_contract import GeneratedContract
diff --git a/backend/app/models/contract_approval_step.py b/backend/app/models/contract_approval_step.py
new file mode 100644
index 00000000..36f7f647
--- /dev/null
+++ b/backend/app/models/contract_approval_step.py
@@ -0,0 +1,38 @@
+from datetime import datetime, timezone
+from typing import Any, Optional
+
+from sqlalchemy import DateTime, ForeignKey, Index, JSON, String, Text
+from sqlalchemy.orm import Mapped, mapped_column
+
+from app.db import Base
+
+
+class ContractApprovalStep(Base):
+ __tablename__ = "contract_approval_steps"
+ __table_args__ = (
+ Index("ix_contract_approval_steps_tenant_contract", "tenant_id", "contract_id"),
+ Index("ix_contract_approval_steps_tenant_status", "tenant_id", "status"),
+ Index("ix_contract_approval_steps_tenant_assignee", "tenant_id", "assignee_id"),
+ )
+
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
+ tenant_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
+ contract_id: Mapped[str] = mapped_column(String(64), ForeignKey("generated_contracts.id"), nullable=False, index=True)
+ step_type: Mapped[str] = mapped_column(String(64), nullable=False, default="legal_review", index=True)
+ status: Mapped[str] = mapped_column(String(32), nullable=False, default="pending", index=True)
+ title: Mapped[str] = mapped_column(String(200), nullable=False)
+ comment: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
+ submitter_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
+ submitter_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
+ assignee_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, index=True)
+ assignee_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
+ decided_by: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
+ decided_by_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
+ decided_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
+ metadata_json: Mapped[Optional[dict[str, Any]]] = mapped_column(JSON, nullable=True)
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), index=True)
+ updated_at: Mapped[datetime] = mapped_column(
+ DateTime(timezone=True),
+ default=lambda: datetime.now(timezone.utc),
+ onupdate=lambda: datetime.now(timezone.utc),
+ )
diff --git a/backend/app/repositories/contract_approval_step_repository.py b/backend/app/repositories/contract_approval_step_repository.py
new file mode 100644
index 00000000..9d7cf64a
--- /dev/null
+++ b/backend/app/repositories/contract_approval_step_repository.py
@@ -0,0 +1,113 @@
+from typing import Optional
+
+from sqlalchemy import or_
+from sqlalchemy.orm import Session
+
+from app.models.contract_approval_step import ContractApprovalStep
+
+
+class ContractApprovalStepRepository:
+ def __init__(self, db: Session):
+ self.db = db
+
+ def create(self, step: ContractApprovalStep) -> ContractApprovalStep:
+ self.db.add(step)
+ self.db.commit()
+ self.db.refresh(step)
+ return step
+
+ def update(self, step: ContractApprovalStep) -> ContractApprovalStep:
+ self.db.add(step)
+ self.db.commit()
+ self.db.refresh(step)
+ return step
+
+ def get_by_id(self, tenant_id: str, step_id: str) -> Optional[ContractApprovalStep]:
+ return (
+ self.db.query(ContractApprovalStep)
+ .filter(
+ ContractApprovalStep.tenant_id == tenant_id,
+ ContractApprovalStep.id == step_id,
+ )
+ .first()
+ )
+
+ def get_latest_by_contract(self, tenant_id: str, contract_id: str) -> Optional[ContractApprovalStep]:
+ return (
+ self.db.query(ContractApprovalStep)
+ .filter(
+ ContractApprovalStep.tenant_id == tenant_id,
+ ContractApprovalStep.contract_id == contract_id,
+ )
+ .order_by(ContractApprovalStep.created_at.desc())
+ .first()
+ )
+
+ def get_pending_by_contract(self, tenant_id: str, contract_id: str) -> Optional[ContractApprovalStep]:
+ return (
+ self.db.query(ContractApprovalStep)
+ .filter(
+ ContractApprovalStep.tenant_id == tenant_id,
+ ContractApprovalStep.contract_id == contract_id,
+ ContractApprovalStep.status == "pending",
+ )
+ .order_by(ContractApprovalStep.created_at.desc())
+ .first()
+ )
+
+ def list_by_contract(self, tenant_id: str, contract_id: str) -> list[ContractApprovalStep]:
+ return (
+ self.db.query(ContractApprovalStep)
+ .filter(
+ ContractApprovalStep.tenant_id == tenant_id,
+ ContractApprovalStep.contract_id == contract_id,
+ )
+ .order_by(ContractApprovalStep.created_at.desc())
+ .all()
+ )
+
+ def list_paginated(
+ self,
+ tenant_id: str,
+ page: int,
+ page_size: int,
+ *,
+ status_value: Optional[str] = None,
+ assignee_id: Optional[str] = None,
+ submitter_id: Optional[str] = None,
+ participant_id: Optional[str] = None,
+ ) -> list[ContractApprovalStep]:
+ query = self.db.query(ContractApprovalStep).filter(ContractApprovalStep.tenant_id == tenant_id)
+ query = self._apply_filters(query, status_value, assignee_id, submitter_id, participant_id)
+ return query.order_by(ContractApprovalStep.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
+
+ def count(
+ self,
+ tenant_id: str,
+ *,
+ status_value: Optional[str] = None,
+ assignee_id: Optional[str] = None,
+ submitter_id: Optional[str] = None,
+ participant_id: Optional[str] = None,
+ ) -> int:
+ query = self.db.query(ContractApprovalStep).filter(ContractApprovalStep.tenant_id == tenant_id)
+ query = self._apply_filters(query, status_value, assignee_id, submitter_id, participant_id)
+ return query.count()
+
+ @staticmethod
+ def _apply_filters(query, status_value, assignee_id, submitter_id, participant_id):
+ if status_value:
+ query = query.filter(ContractApprovalStep.status == status_value)
+ if assignee_id:
+ query = query.filter(ContractApprovalStep.assignee_id == assignee_id)
+ if submitter_id:
+ query = query.filter(ContractApprovalStep.submitter_id == submitter_id)
+ if participant_id:
+ query = query.filter(
+ or_(
+ ContractApprovalStep.assignee_id == participant_id,
+ ContractApprovalStep.submitter_id == participant_id,
+ ContractApprovalStep.decided_by == participant_id,
+ )
+ )
+ return query
diff --git a/backend/app/services/contract_approval_service.py b/backend/app/services/contract_approval_service.py
new file mode 100644
index 00000000..7a8bc5dd
--- /dev/null
+++ b/backend/app/services/contract_approval_service.py
@@ -0,0 +1,332 @@
+from datetime import datetime, timezone
+from typing import Any, Optional
+from uuid import uuid4
+
+from fastapi import HTTPException, status
+from sqlalchemy.orm import Session
+
+from app.core.exceptions import ConflictError, ForbiddenError, NotFoundError, ValidationError
+from app.models.contract_approval_step import ContractApprovalStep
+from app.models.generated_contract import GeneratedContract
+from app.repositories.contract_approval_step_repository import ContractApprovalStepRepository
+from app.repositories.generated_contract_repository import GeneratedContractRepository
+
+
+APPROVAL_STATUSES = {"pending", "approved", "rejected", "cancelled"}
+APPROVAL_STEP_TYPES = {"legal_review", "business_approval", "signoff"}
+APPROVAL_LIST_SCOPES = {"all", "assigned_to_me", "submitted_by_me", "participated"}
+
+
+class ContractApprovalService:
+ def __init__(self, db: Session):
+ self.db = db
+ self.contract_repository = GeneratedContractRepository(db)
+ self.approval_repository = ContractApprovalStepRepository(db)
+
+ def list_approvals(
+ self,
+ current_user: dict[str, Any],
+ *,
+ page: int = 1,
+ page_size: int = 20,
+ status_value: Optional[str] = None,
+ scope: str = "assigned_to_me",
+ ) -> dict[str, Any]:
+ tenant_id = current_user["tenant_id"]
+ normalized_page = max(int(page or 1), 1)
+ normalized_page_size = max(min(int(page_size or 20), 100), 1)
+ normalized_status = self._normalize_optional_status(status_value)
+ normalized_scope = self._normalize_scope(scope)
+ user_id = str(current_user.get("id") or "")
+ role = current_user.get("role")
+
+ assignee_id = None
+ submitter_id = None
+ participant_id = None
+ if normalized_scope == "assigned_to_me":
+ assignee_id = user_id
+ elif normalized_scope == "submitted_by_me":
+ submitter_id = user_id
+ elif normalized_scope == "participated":
+ participant_id = user_id
+ elif normalized_scope == "all" and role != "admin":
+ participant_id = user_id
+
+ items = self.approval_repository.list_paginated(
+ tenant_id,
+ normalized_page,
+ normalized_page_size,
+ status_value=normalized_status,
+ assignee_id=assignee_id,
+ submitter_id=submitter_id,
+ participant_id=participant_id,
+ )
+ total = self.approval_repository.count(
+ tenant_id,
+ status_value=normalized_status,
+ assignee_id=assignee_id,
+ submitter_id=submitter_id,
+ participant_id=participant_id,
+ )
+
+ return {
+ "items": [self._to_approval_item(item) for item in items],
+ "total": total,
+ "page": normalized_page,
+ "page_size": normalized_page_size,
+ "summary": self._build_summary(tenant_id, user_id, role),
+ }
+
+ def list_contract_approvals(self, current_user: dict[str, Any], contract_id: str) -> dict[str, Any]:
+ contract = self._get_visible_contract(current_user, contract_id)
+ items = self.approval_repository.list_by_contract(current_user["tenant_id"], contract.id)
+ return {
+ "contract_id": contract.id,
+ "items": [self._to_approval_item(item, contract=contract) for item in items],
+ "total": len(items),
+ "latest": self._to_approval_item(items[0], contract=contract) if items else None,
+ }
+
+ def submit_approval(
+ self,
+ current_user: dict[str, Any],
+ contract_id: str,
+ *,
+ title: Optional[str] = None,
+ comment: Optional[str] = None,
+ assignee_id: Optional[str] = None,
+ assignee_name: Optional[str] = None,
+ step_type: str = "legal_review",
+ ) -> dict[str, Any]:
+ contract = self._get_owned_or_admin_contract(current_user, contract_id)
+ pending = self.approval_repository.get_pending_by_contract(current_user["tenant_id"], contract_id)
+ if pending is not None:
+ raise ConflictError("contract approval is already pending")
+
+ normalized_step_type = self._normalize_step_type(step_type)
+ now = datetime.now(timezone.utc)
+ step = ContractApprovalStep(
+ id=f"cap-{uuid4().hex[:12]}",
+ tenant_id=current_user["tenant_id"],
+ contract_id=contract.id,
+ step_type=normalized_step_type,
+ status="pending",
+ title=self._approval_title(title, contract),
+ comment=self._optional_text(comment, max_length=2000),
+ submitter_id=str(current_user.get("id") or ""),
+ submitter_name=current_user.get("display_name"),
+ assignee_id=self._optional_id(assignee_id) or self._default_assignee_id(current_user, contract),
+ assignee_name=self._optional_text(assignee_name, max_length=100) or self._default_assignee_name(current_user, contract),
+ metadata_json={
+ "source_lifecycle_status": contract.lifecycle_status or contract.status or "generated",
+ "submitted_from": "contract_detail",
+ },
+ created_at=now,
+ updated_at=now,
+ )
+ created = self.approval_repository.create(step)
+ self._update_contract_lifecycle(contract, "internal_review", current_user, f"已提交审批:{created.title}")
+ return self._to_approval_item(created, contract=contract)
+
+ def decide_approval(
+ self,
+ current_user: dict[str, Any],
+ approval_id: str,
+ *,
+ decision: str,
+ comment: Optional[str] = None,
+ ) -> dict[str, Any]:
+ tenant_id = current_user["tenant_id"]
+ step = self.approval_repository.get_by_id(tenant_id, approval_id)
+ if step is None:
+ raise NotFoundError("approval step not found")
+ if step.status != "pending":
+ raise ConflictError("approval step has already been decided")
+
+ contract = self._get_visible_contract(current_user, step.contract_id)
+ if not self._can_decide(current_user, step):
+ raise ForbiddenError("no permission to decide this approval")
+
+ normalized_decision = self._normalize_decision(decision)
+ now = datetime.now(timezone.utc)
+ step.status = "approved" if normalized_decision == "approve" else "rejected"
+ step.comment = self._optional_text(comment, max_length=2000) or step.comment
+ step.decided_by = str(current_user.get("id") or "")
+ step.decided_by_name = current_user.get("display_name")
+ step.decided_at = now
+ step.metadata_json = {
+ **(step.metadata_json or {}),
+ "decision": normalized_decision,
+ "decided_from": "approval_workbench",
+ }
+ updated = self.approval_repository.update(step)
+
+ if updated.status == "approved":
+ self._update_contract_lifecycle(contract, "approved", current_user, "审批已通过,可进入签署或归档准备。")
+ else:
+ self._update_contract_lifecycle(contract, "generated", current_user, updated.comment or "审批已退回,请修改后重新提交。")
+
+ return self._to_approval_item(updated, contract=contract)
+
+ def _get_visible_contract(self, current_user: dict[str, Any], contract_id: str) -> GeneratedContract:
+ contract = self.contract_repository.get_by_id(contract_id, current_user["tenant_id"])
+ if contract is None:
+ raise NotFoundError("contract not found")
+ user_id = str(current_user.get("id") or "")
+ if current_user.get("role") == "admin" or str(contract.generated_by or "") == user_id:
+ return contract
+ latest = self.approval_repository.get_latest_by_contract(current_user["tenant_id"], contract_id)
+ if latest and user_id in {str(latest.assignee_id or ""), str(latest.submitter_id or ""), str(latest.decided_by or "")}:
+ return contract
+ raise NotFoundError("contract not found")
+
+ def _get_owned_or_admin_contract(self, current_user: dict[str, Any], contract_id: str) -> GeneratedContract:
+ contract = self.contract_repository.get_by_id(contract_id, current_user["tenant_id"])
+ if contract is None:
+ raise NotFoundError("contract not found")
+ if current_user.get("role") != "admin" and str(contract.generated_by or "") != str(current_user.get("id") or ""):
+ raise NotFoundError("contract not found")
+ return contract
+
+ def _update_contract_lifecycle(
+ self,
+ contract: GeneratedContract,
+ lifecycle_status: str,
+ current_user: dict[str, Any],
+ note: str,
+ ) -> None:
+ contract.lifecycle_status = lifecycle_status
+ contract.lifecycle_note = note
+ contract.lifecycle_updated_by = current_user.get("id")
+ contract.lifecycle_updated_at = datetime.now(timezone.utc)
+ self.contract_repository.update(contract)
+
+ def _to_approval_item(self, step: ContractApprovalStep, contract: Optional[GeneratedContract] = None) -> dict[str, Any]:
+ resolved_contract = contract or self.contract_repository.get_by_id(step.contract_id, step.tenant_id)
+ return {
+ "id": step.id,
+ "tenant_id": step.tenant_id,
+ "contract_id": step.contract_id,
+ "contract": self._to_contract_summary(resolved_contract),
+ "step_type": step.step_type,
+ "status": step.status,
+ "title": step.title,
+ "comment": step.comment,
+ "submitter_id": step.submitter_id,
+ "submitter_name": step.submitter_name,
+ "assignee_id": step.assignee_id,
+ "assignee_name": step.assignee_name,
+ "decided_by": step.decided_by,
+ "decided_by_name": step.decided_by_name,
+ "decided_at": self._serialize_optional_datetime(step.decided_at),
+ "metadata": step.metadata_json or {},
+ "created_at": self._serialize_optional_datetime(step.created_at),
+ "updated_at": self._serialize_optional_datetime(step.updated_at),
+ }
+
+ @staticmethod
+ def _to_contract_summary(contract: Optional[GeneratedContract]) -> Optional[dict[str, Any]]:
+ if contract is None:
+ return None
+ return {
+ "id": contract.id,
+ "template_name": contract.template_name,
+ "contract_type": contract.contract_type,
+ "document_type_code": contract.document_type_code,
+ "file_name": contract.file_name,
+ "generated_by_name": contract.generated_by_name,
+ "lifecycle_status": contract.lifecycle_status or contract.status or "generated",
+ "created_at": ContractApprovalService._serialize_optional_datetime(contract.created_at),
+ }
+
+ def _build_summary(self, tenant_id: str, user_id: str, role: Optional[str]) -> dict[str, int]:
+ participant_id = None if role == "admin" else user_id
+ return {
+ "pending_all": self.approval_repository.count(tenant_id, status_value="pending", participant_id=participant_id),
+ "assigned_to_me": self.approval_repository.count(tenant_id, status_value="pending", assignee_id=user_id),
+ "submitted_by_me": self.approval_repository.count(tenant_id, submitter_id=user_id),
+ "approved": self.approval_repository.count(tenant_id, status_value="approved", participant_id=participant_id),
+ "rejected": self.approval_repository.count(tenant_id, status_value="rejected", participant_id=participant_id),
+ }
+
+ @staticmethod
+ def _can_decide(current_user: dict[str, Any], step: ContractApprovalStep) -> bool:
+ if current_user.get("role") == "admin":
+ return True
+ user_id = str(current_user.get("id") or "")
+ return bool(step.assignee_id and str(step.assignee_id) == user_id)
+
+ @staticmethod
+ def _approval_title(title: Optional[str], contract: GeneratedContract) -> str:
+ normalized = ContractApprovalService._optional_text(title, max_length=200)
+ return normalized or f"合同审批:{contract.template_name}"
+
+ @staticmethod
+ def _default_assignee_id(current_user: dict[str, Any], contract: GeneratedContract) -> Optional[str]:
+ if current_user.get("role") == "admin":
+ return str(current_user.get("id") or "")
+ return str(contract.generated_by or current_user.get("id") or "")
+
+ @staticmethod
+ def _default_assignee_name(current_user: dict[str, Any], contract: GeneratedContract) -> Optional[str]:
+ if current_user.get("role") == "admin":
+ return current_user.get("display_name")
+ return contract.generated_by_name or current_user.get("display_name")
+
+ @staticmethod
+ def _normalize_optional_status(value: Optional[str]) -> Optional[str]:
+ if value is None or not str(value).strip():
+ return None
+ normalized = str(value).strip().lower().replace("-", "_")
+ if normalized not in APPROVAL_STATUSES:
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid approval status")
+ return normalized
+
+ @staticmethod
+ def _normalize_step_type(value: str) -> str:
+ normalized = str(value or "legal_review").strip().lower().replace("-", "_")
+ if normalized not in APPROVAL_STEP_TYPES:
+ raise ValidationError("invalid approval step type")
+ return normalized
+
+ @staticmethod
+ def _normalize_scope(value: str) -> str:
+ normalized = str(value or "assigned_to_me").strip().lower().replace("-", "_")
+ if normalized not in APPROVAL_LIST_SCOPES:
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid approval scope")
+ return normalized
+
+ @staticmethod
+ def _normalize_decision(value: str) -> str:
+ normalized = str(value or "").strip().lower().replace("-", "_")
+ if normalized in {"approve", "approved"}:
+ return "approve"
+ if normalized in {"reject", "rejected", "return"}:
+ return "reject"
+ raise ValidationError("invalid approval decision")
+
+ @staticmethod
+ def _optional_id(value: Optional[str]) -> Optional[str]:
+ if value is None:
+ return None
+ normalized = str(value).strip()
+ return normalized or None
+
+ @staticmethod
+ def _optional_text(value: Optional[Any], *, max_length: int) -> Optional[str]:
+ if value is None:
+ return None
+ normalized = str(value).strip()
+ if not normalized:
+ return None
+ if len(normalized) > max_length:
+ raise ValidationError("approval field is too long")
+ return normalized
+
+ @staticmethod
+ def _serialize_optional_datetime(value: Optional[datetime]) -> Optional[str]:
+ if value is None:
+ return None
+ if value.tzinfo is None:
+ value = value.replace(tzinfo=timezone.utc)
+ return value.isoformat()
diff --git a/backend/app/services/contract_generation_service.py b/backend/app/services/contract_generation_service.py
index bc43e476..4502b4f8 100644
--- a/backend/app/services/contract_generation_service.py
+++ b/backend/app/services/contract_generation_service.py
@@ -19,6 +19,7 @@ from app.models.file import File
from app.models.generated_contract import GeneratedContract
from app.models.generated_contract_version import GeneratedContractVersion
from app.models.contract_lifecycle_task import ContractLifecycleTask
+from app.repositories.contract_approval_step_repository import ContractApprovalStepRepository
from app.repositories.contract_lifecycle_task_repository import ContractLifecycleTaskRepository
from app.repositories.contract_template_repository import ContractTemplateRepository
from app.repositories.generated_contract_repository import GeneratedContractRepository
@@ -47,6 +48,7 @@ CONTRACT_LIFECYCLE_STATUSES = {
"generated",
"internal_review",
"external_review",
+ "approved",
"signed",
"effective",
"expiring",
@@ -84,6 +86,7 @@ class ContractGenerationService:
self.repository = ContractTemplateRepository(db)
self.generated_contract_repository = GeneratedContractRepository(db)
self.generated_contract_version_repository = GeneratedContractVersionRepository(db)
+ self.contract_approval_step_repository = ContractApprovalStepRepository(db)
self.contract_lifecycle_task_repository = ContractLifecycleTaskRepository(db)
self.file_service = FileService(db)
self.legal_article_service = LegalArticleService(db)
@@ -1942,6 +1945,7 @@ class ContractGenerationService:
"clause_completion_suggestions": clause_suggestions,
"auto_review": auto_review,
"lifecycle": self._build_contract_lifecycle(generated_contract),
+ "approval": self._build_contract_approval_summary(generated_contract),
"metadata": {
"generated_by": generated_contract.generated_by,
"generated_by_name": generated_contract.generated_by_name,
@@ -1994,6 +1998,7 @@ class ContractGenerationService:
"clause_completion_suggestions": item.get("clause_completion_suggestions"),
"auto_review": item["auto_review"],
"lifecycle": item["lifecycle"],
+ "approval": item.get("approval"),
"missing_field_count": item["metadata"].get("missing_field_count", 0),
"related_legal_article_count": item["metadata"].get("related_legal_article_count", 0),
"metadata": {
@@ -2457,6 +2462,42 @@ class ContractGenerationService:
"updated_at": self._serialize_optional_datetime(generated_contract.lifecycle_updated_at),
}
+ def _build_contract_approval_summary(self, generated_contract: GeneratedContract) -> Dict[str, Any]:
+ steps = self.contract_approval_step_repository.list_by_contract(
+ generated_contract.tenant_id,
+ generated_contract.id,
+ )
+ latest = steps[0] if steps else None
+ pending = next((step for step in steps if step.status == "pending"), None)
+ return {
+ "status": pending.status if pending else latest.status if latest else "not_submitted",
+ "latest": self._to_contract_approval_step_summary(latest),
+ "pending": self._to_contract_approval_step_summary(pending),
+ "history": [self._to_contract_approval_step_summary(step) for step in steps],
+ "total": len(steps),
+ }
+
+ def _to_contract_approval_step_summary(self, step) -> Optional[Dict[str, Any]]:
+ if step is None:
+ return None
+ return {
+ "id": step.id,
+ "contract_id": step.contract_id,
+ "step_type": step.step_type,
+ "status": step.status,
+ "title": step.title,
+ "comment": step.comment,
+ "submitter_id": step.submitter_id,
+ "submitter_name": step.submitter_name,
+ "assignee_id": step.assignee_id,
+ "assignee_name": step.assignee_name,
+ "decided_by": step.decided_by,
+ "decided_by_name": step.decided_by_name,
+ "decided_at": self._serialize_optional_datetime(step.decided_at),
+ "created_at": self._serialize_optional_datetime(step.created_at),
+ "updated_at": self._serialize_optional_datetime(step.updated_at),
+ }
+
@staticmethod
def _normalize_version_change_type(value: str) -> str:
normalized = str(value or "editor_save").strip().lower().replace("-", "_")
diff --git a/backend/app/services/contract_lifecycle_service.py b/backend/app/services/contract_lifecycle_service.py
index 65181e37..762f4dc2 100644
--- a/backend/app/services/contract_lifecycle_service.py
+++ b/backend/app/services/contract_lifecycle_service.py
@@ -16,7 +16,7 @@ from app.repositories.contract_lifecycle_task_repository import ContractLifecycl
from app.services.file_service import FileService
CONTRACT_LIFECYCLE_STATUSES = {
- "generated", "internal_review", "external_review", "signed",
+ "generated", "internal_review", "external_review", "approved", "signed",
"effective", "expiring", "expired", "terminated", "archived",
}
CONTRACT_LIFECYCLE_TASK_TYPES = {"renewal_reminder", "performance_milestone", "termination_archive", "custom"}
diff --git a/backend/app/services/review_report_template.py b/backend/app/services/review_report_template.py
index e7941af5..3fca5a81 100644
--- a/backend/app/services/review_report_template.py
+++ b/backend/app/services/review_report_template.py
@@ -51,6 +51,7 @@ def build_report_html(
ctx_payment_html = _build_payment_html(contract_structure)
ctx_relation_html = _build_relations_html(contract_structure)
ctx_risk_items_html = _build_risk_items_html(risk_items)
+ ctx_business_actions_html = _build_business_actions_html(risk_items, result)
ctx_refs_html = _build_legal_refs_html(legal_article_refs)
ctx_missing = contract_structure.get("missing_sections") or []
ctx_summary = contract_structure.get("summary") or ""
@@ -144,6 +145,26 @@ def build_report_html(
}}
.opinion-box strong {{ color: #166534; display: block; margin-bottom: 4px; }}
+ .business-grid {{
+ display: grid; grid-template-columns: 1fr 1fr 1fr;
+ gap: 10px; margin: 14px 0 18px;
+ }}
+ .business-card {{
+ border: 1px solid #e2e8f0; border-radius: 8px;
+ padding: 12px 14px; background: #fff;
+ page-break-inside: avoid;
+ }}
+ .business-card span {{
+ display: block; font-size: 10px; color: #64748b;
+ text-transform: uppercase; margin-bottom: 4px;
+ }}
+ .business-card strong {{
+ display: block; font-size: 13px; color: #0f172a;
+ margin-bottom: 6px;
+ }}
+ .business-card ul {{ margin-left: 16px; color: #475569; font-size: 11px; }}
+ .business-card li {{ margin-bottom: 4px; }}
+
/* ── Risk Items ── */
.risk-item {{
border: 1px solid #e2e8f0; border-radius: 8px;
@@ -272,6 +293,8 @@ def build_report_html(
{_h(review_opinion)}
+{ctx_business_actions_html}
+
{"
缺失关注: " + "、".join(_h(m) for m in ctx_missing) + "
" if ctx_missing else ""}
{_h(ctx_summary) if ctx_summary else ""}
@@ -351,6 +374,58 @@ def _build_risk_items_html(risk_items: List[Dict[str, Any]]) -> str:
return "\n".join(parts)
+def _build_business_actions_html(risk_items: List[Dict[str, Any]], result: Dict[str, Any]) -> str:
+ structured_report = result.get("structured_report") or {}
+ priority_actions = result.get("priority_actions") or structured_report.get("priority_actions") or []
+ negotiation_tips = result.get("negotiation_tips") or structured_report.get("negotiation_tips") or []
+ missing_clauses = ((structured_report.get("structure_check") or {}).get("missing_clauses") or [])
+
+ must_fix: List[str] = []
+ for item in risk_items:
+ if item.get("level") in ("critical", "high"):
+ must_fix.append(item.get("suggestion") or item.get("title") or item.get("description") or "高风险事项需优先处理")
+ for item in priority_actions:
+ if item.get("priority") in ("critical", "high"):
+ must_fix.append(item.get("suggestion") or item.get("title") or "高优先级动作需优先处理")
+ for item in missing_clauses:
+ if item.get("importance") == "high":
+ must_fix.append(f"补充{item.get('name') or '关键条款'}:{item.get('reason') or '降低争议风险'}")
+
+ negotiable: List[str] = []
+ for item in risk_items:
+ if item.get("level") == "medium":
+ negotiable.append(item.get("suggestion") or item.get("title") or item.get("description") or "中风险事项建议协商")
+ negotiable.extend(str(item) for item in negotiation_tips)
+
+ must_fix = must_fix[:5] or ["当前未识别到必须立即修改的高风险事项。"]
+ negotiable = negotiable[:5] or ["当前未识别到单独列出的可协商事项。"]
+
+ return f"""
+业务处理摘要
+
+
+
Must Fix
+
必改事项
+
{"".join(f"{_h(item)} " for item in must_fix)}
+
+
+
Negotiable
+
可协商事项
+
{"".join(f"{_h(item)} " for item in negotiable)}
+
+
+
Delivery
+
报告交付
+
+ 总体风险:{RISK_LABEL_MAP.get(result.get("risk_summary", {}).get("overall"), result.get("risk_summary", {}).get("overall", "-"))}
+ 报告用途:内部预审、修改沟通和试点交付参考。
+ 免责声明:本报告不替代正式法律意见。
+
+
+
+"""
+
+
def _build_clauses_html(clauses: List[Dict[str, Any]]) -> str:
if not clauses:
return ""
diff --git a/backend/migrations/versions/20260618_0044_create_contract_approval_steps.py b/backend/migrations/versions/20260618_0044_create_contract_approval_steps.py
new file mode 100644
index 00000000..987463d4
--- /dev/null
+++ b/backend/migrations/versions/20260618_0044_create_contract_approval_steps.py
@@ -0,0 +1,64 @@
+"""create contract approval steps"""
+
+from __future__ import annotations
+
+import sqlalchemy as sa
+from alembic import op
+
+
+revision = "20260618_0044"
+down_revision = "20260610_0043"
+branch_labels = None
+depends_on = None
+
+
+def _table_names(inspector: sa.Inspector) -> set[str]:
+ return set(inspector.get_table_names())
+
+
+def upgrade() -> None:
+ bind = op.get_bind()
+ inspector = sa.inspect(bind)
+ if "contract_approval_steps" in _table_names(inspector):
+ return
+
+ op.create_table(
+ "contract_approval_steps",
+ sa.Column("id", sa.String(length=64), nullable=False),
+ sa.Column("tenant_id", sa.String(length=64), nullable=False),
+ sa.Column("contract_id", sa.String(length=64), nullable=False),
+ sa.Column("step_type", sa.String(length=64), nullable=False, server_default="legal_review"),
+ sa.Column("status", sa.String(length=32), nullable=False, server_default="pending"),
+ sa.Column("title", sa.String(length=200), nullable=False),
+ sa.Column("comment", sa.Text(), nullable=True),
+ sa.Column("submitter_id", sa.String(length=64), nullable=False),
+ sa.Column("submitter_name", sa.String(length=100), nullable=True),
+ sa.Column("assignee_id", sa.String(length=64), nullable=True),
+ sa.Column("assignee_name", sa.String(length=100), nullable=True),
+ sa.Column("decided_by", sa.String(length=64), nullable=True),
+ sa.Column("decided_by_name", sa.String(length=100), nullable=True),
+ sa.Column("decided_at", sa.DateTime(timezone=True), nullable=True),
+ sa.Column("metadata_json", sa.JSON(), nullable=True),
+ sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
+ sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
+ sa.ForeignKeyConstraint(["contract_id"], ["generated_contracts.id"]),
+ sa.PrimaryKeyConstraint("id"),
+ )
+ op.create_index("ix_contract_approval_steps_assignee_id", "contract_approval_steps", ["assignee_id"])
+ op.create_index("ix_contract_approval_steps_contract_id", "contract_approval_steps", ["contract_id"])
+ op.create_index("ix_contract_approval_steps_created_at", "contract_approval_steps", ["created_at"])
+ op.create_index("ix_contract_approval_steps_status", "contract_approval_steps", ["status"])
+ op.create_index("ix_contract_approval_steps_step_type", "contract_approval_steps", ["step_type"])
+ op.create_index("ix_contract_approval_steps_submitter_id", "contract_approval_steps", ["submitter_id"])
+ op.create_index("ix_contract_approval_steps_tenant_assignee", "contract_approval_steps", ["tenant_id", "assignee_id"])
+ op.create_index("ix_contract_approval_steps_tenant_contract", "contract_approval_steps", ["tenant_id", "contract_id"])
+ op.create_index("ix_contract_approval_steps_tenant_id", "contract_approval_steps", ["tenant_id"])
+ op.create_index("ix_contract_approval_steps_tenant_status", "contract_approval_steps", ["tenant_id", "status"])
+
+
+def downgrade() -> None:
+ bind = op.get_bind()
+ inspector = sa.inspect(bind)
+ if "contract_approval_steps" not in _table_names(inspector):
+ return
+ op.drop_table("contract_approval_steps")
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 0f734fee..d53762a9 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -16,4 +16,4 @@ email-validator==2.2.0
reportlab==4.4.1
python-docx==1.1.2
pytest==8.3.5
--e ../../shared-ai-python-sdk
+-e ./vendor/shared-ai-platform-sdk
diff --git a/backend/tests/test_contracts_api.py b/backend/tests/test_contracts_api.py
index 3e7b514e..5476935f 100644
--- a/backend/tests/test_contracts_api.py
+++ b/backend/tests/test_contracts_api.py
@@ -912,6 +912,7 @@ class ContractGenerationServiceTestCase(unittest.TestCase):
from app.db import Base
from app.models.contract_lifecycle_task import ContractLifecycleTask
+ from app.models.contract_approval_step import ContractApprovalStep
from app.models.contract_template import ContractTemplate
from app.models.file import File
from app.models.generated_contract import GeneratedContract
@@ -927,6 +928,7 @@ class ContractGenerationServiceTestCase(unittest.TestCase):
self.db = self.SessionLocal()
self.ContractTemplate = ContractTemplate
self.ContractLifecycleTask = ContractLifecycleTask
+ self.ContractApprovalStep = ContractApprovalStep
self.File = File
self.GeneratedContract = GeneratedContract
self.GeneratedContractVersion = GeneratedContractVersion
@@ -2037,6 +2039,69 @@ class ContractGenerationServiceTestCase(unittest.TestCase):
self.assertEqual(ctx.exception.status_code, 400)
+ def test_contract_approval_submit_and_approve_updates_lifecycle(self):
+ from app.services.contract_approval_service import ContractApprovalService
+
+ now = datetime.now(timezone.utc)
+ self.db.add(
+ self.GeneratedContract(
+ id="contract-approval-001",
+ tenant_id="tenant-demo-001",
+ template_id="tpl-approval",
+ template_name="Approval Template",
+ contract_type="procurement",
+ document_type_code="procurement_goods",
+ generated_by="user-admin",
+ generated_by_name="Admin",
+ export_format="docx",
+ file_id=None,
+ file_name="approval.docx",
+ mime_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ file_size=10,
+ generated_content="Approval contract content.",
+ variable_values_json={},
+ export_options_json={},
+ file_content_base64="",
+ status="generated",
+ created_at=now,
+ )
+ )
+ self.db.commit()
+
+ approval_service = ContractApprovalService(self.db)
+ submitted = approval_service.submit_approval(
+ current_user=self.current_user,
+ contract_id="contract-approval-001",
+ title="采购合同法务审批",
+ comment="请重点关注付款和验收条款。",
+ assignee_id="user-admin",
+ assignee_name="Admin",
+ )
+
+ self.assertEqual(submitted["status"], "pending")
+ self.assertEqual(submitted["contract"]["lifecycle_status"], "internal_review")
+ pending_list = approval_service.list_approvals(
+ current_user=self.current_user,
+ scope="assigned_to_me",
+ status_value="pending",
+ )
+ self.assertEqual(pending_list["total"], 1)
+ self.assertEqual(pending_list["summary"]["assigned_to_me"], 1)
+
+ decided = approval_service.decide_approval(
+ current_user=self.current_user,
+ approval_id=submitted["id"],
+ decision="approve",
+ comment="审批通过。",
+ )
+
+ self.assertEqual(decided["status"], "approved")
+ contract = self.db.query(self.GeneratedContract).filter_by(id="contract-approval-001").one()
+ self.assertEqual(contract.lifecycle_status, "approved")
+ detail = self.service.get_generated_contract(self.current_user, "contract-approval-001")
+ self.assertEqual(detail["approval"]["status"], "approved")
+ self.assertEqual(detail["approval"]["latest"]["comment"], "审批通过。")
+
class ClauseSuggestionRuleTests(unittest.TestCase):
def setUp(self):
diff --git a/backend/vendor/shared-ai-platform-sdk/README.md b/backend/vendor/shared-ai-platform-sdk/README.md
new file mode 100644
index 00000000..53bae316
--- /dev/null
+++ b/backend/vendor/shared-ai-platform-sdk/README.md
@@ -0,0 +1,40 @@
+# Python SDK
+
+Example:
+
+```python
+from shared_ai_platform_sdk import RequestContext, SharedAIPlatformClient, SyncRecord
+
+client = SharedAIPlatformClient(
+ base_url="http://127.0.0.1:8011",
+ api_key="your-app-secret",
+ app_id="saas-web",
+)
+
+context = RequestContext(
+ tenant_id="tenant-001",
+ user_id="user-admin",
+ user_role="admin",
+)
+
+tasks = client.list_tasks(context)
+run = client.run_task(
+ context,
+ "general.text.summary",
+ inputs={"text": "Your text here"},
+)
+
+sync_result = client.sync_content(
+ context,
+ "kb-001",
+ records=[
+ SyncRecord(
+ source_type="product",
+ source_id="product-001",
+ title="Smart Water Platform",
+ content="Structured product content",
+ metadata={"url": "/products/water"},
+ )
+ ],
+)
+```
diff --git a/backend/vendor/shared-ai-platform-sdk/pyproject.toml b/backend/vendor/shared-ai-platform-sdk/pyproject.toml
new file mode 100644
index 00000000..1848d807
--- /dev/null
+++ b/backend/vendor/shared-ai-platform-sdk/pyproject.toml
@@ -0,0 +1,12 @@
+[build-system]
+requires = ["setuptools>=68", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "shared-ai-platform-sdk"
+version = "0.1.0"
+description = "Python SDK for the shared AI platform"
+requires-python = ">=3.11"
+dependencies = [
+ "httpx>=0.28,<0.29",
+]
diff --git a/backend/vendor/shared-ai-platform-sdk/shared_ai_platform_sdk/__init__.py b/backend/vendor/shared-ai-platform-sdk/shared_ai_platform_sdk/__init__.py
new file mode 100644
index 00000000..da08436c
--- /dev/null
+++ b/backend/vendor/shared-ai-platform-sdk/shared_ai_platform_sdk/__init__.py
@@ -0,0 +1,12 @@
+from shared_ai_platform_sdk.client import SharedAIPlatformClient
+from shared_ai_platform_sdk.errors import SharedAIPlatformError
+from shared_ai_platform_sdk.types import PollOptions, RequestContext, SSEEvent, SyncRecord
+
+__all__ = [
+ "SharedAIPlatformClient",
+ "SharedAIPlatformError",
+ "RequestContext",
+ "SSEEvent",
+ "PollOptions",
+ "SyncRecord",
+]
diff --git a/backend/vendor/shared-ai-platform-sdk/shared_ai_platform_sdk/client.py b/backend/vendor/shared-ai-platform-sdk/shared_ai_platform_sdk/client.py
new file mode 100644
index 00000000..af84c0a3
--- /dev/null
+++ b/backend/vendor/shared-ai-platform-sdk/shared_ai_platform_sdk/client.py
@@ -0,0 +1,344 @@
+from __future__ import annotations
+
+import json
+import time
+from typing import Iterator
+
+import httpx
+
+from .errors import SharedAIPlatformError
+from .types import PollOptions, RequestContext, SSEEvent, SyncRecord
+
+
+class SharedAIPlatformClient:
+ def __init__(
+ self,
+ *,
+ base_url: str = "http://127.0.0.1:8011",
+ api_key: str,
+ app_id: str,
+ timeout_seconds: float = 30.0,
+ client: httpx.Client | None = None,
+ ):
+ self.base_url = base_url.rstrip("/")
+ self.api_key = api_key
+ self.app_id = app_id
+ self._owns_client = client is None
+ self._client = client or httpx.Client(base_url=self.base_url, timeout=timeout_seconds)
+
+ def close(self):
+ if self._owns_client:
+ self._client.close()
+
+ def __enter__(self) -> "SharedAIPlatformClient":
+ return self
+
+ def __exit__(self, exc_type, exc, tb):
+ self.close()
+
+ def list_tasks(self, context: RequestContext) -> list[dict]:
+ return self._request_json("GET", "/api/v0.2/tasks", context=context)["data"]
+
+ def run_task(
+ self,
+ context: RequestContext,
+ task_key: str,
+ *,
+ inputs: dict,
+ provider: str | None = None,
+ metadata: dict | None = None,
+ ) -> dict:
+ return self._request_json(
+ "POST",
+ f"/api/v0.2/tasks/{task_key}/run",
+ context=context,
+ json_body={"inputs": inputs, "provider": provider, "metadata": metadata or {}},
+ )["data"]
+
+ def enqueue_task(
+ self,
+ context: RequestContext,
+ task_key: str,
+ *,
+ inputs: dict,
+ provider: str | None = None,
+ metadata: dict | None = None,
+ ) -> dict:
+ return self._request_json(
+ "POST",
+ f"/api/v0.2/tasks/{task_key}/enqueue",
+ context=context,
+ json_body={"inputs": inputs, "provider": provider, "metadata": metadata or {}},
+ )["data"]
+
+ def get_task_run(self, context: RequestContext, run_id: str) -> dict:
+ return self._request_json("GET", f"/api/v0.2/task-runs/{run_id}", context=context)["data"]
+
+ def list_jobs(self, context: RequestContext, *, status_value: str | None = None, limit: int = 50) -> list[dict]:
+ params = {"limit": limit}
+ if status_value:
+ params["status_value"] = status_value
+ return self._request_json("GET", "/api/v0.2/jobs", context=context, params=params)["data"]
+
+ def list_conversations(self, context: RequestContext, *, page: int = 1, page_size: int = 20) -> dict:
+ return self._request_json(
+ "GET",
+ "/api/v0.2/conversations",
+ context=context,
+ params={"page": page, "page_size": page_size},
+ )["data"]
+
+ def get_conversation_detail(self, context: RequestContext, conversation_id: str) -> dict:
+ return self._request_json("GET", f"/api/v0.2/conversations/{conversation_id}", context=context)["data"]
+
+ def get_job(self, context: RequestContext, job_id: str) -> dict:
+ return self._request_json("GET", f"/api/v0.2/jobs/{job_id}", context=context)["data"]
+
+ def run_next_job(self, context: RequestContext) -> dict | None:
+ return self._request_json("POST", "/api/v0.2/jobs/run-next", context=context)["data"]["job"]
+
+ def poll_job_until_terminal(self, context: RequestContext, job_id: str, options: PollOptions | None = None) -> dict:
+ opts = options or PollOptions()
+ deadline = time.time() + opts.timeout_seconds
+ while True:
+ job = self.get_job(context, job_id)
+ if job["status"] in opts.terminal_statuses:
+ return job
+ if time.time() >= deadline:
+ raise TimeoutError(f"job {job_id} did not reach a terminal state within {opts.timeout_seconds} seconds")
+ time.sleep(opts.interval_seconds)
+
+ def list_knowledge_bases(self, context: RequestContext) -> dict:
+ return self._request_json("GET", "/api/v0.2/knowledge-bases", context=context)["data"]
+
+ def list_documents(self, context: RequestContext, knowledge_base_id: str, *, page: int = 1, page_size: int = 20) -> dict:
+ return self._request_json(
+ "GET",
+ f"/api/v0.2/knowledge-bases/{knowledge_base_id}/documents",
+ context=context,
+ params={"page": page, "page_size": page_size},
+ )["data"]
+
+ def create_knowledge_base(
+ self,
+ context: RequestContext,
+ *,
+ name: str,
+ description: str | None = None,
+ retrieval_mode: str = "hybrid",
+ ) -> dict:
+ return self._request_json(
+ "POST",
+ "/api/v0.2/knowledge-bases",
+ context=context,
+ json_body={"name": name, "description": description, "retrieval_mode": retrieval_mode},
+ )["data"]
+
+ def upload_document(
+ self,
+ context: RequestContext,
+ knowledge_base_id: str,
+ *,
+ file_name: str,
+ mime_type: str | None,
+ content: bytes,
+ ) -> dict:
+ response = self._client.post(
+ f"/api/v0.2/knowledge-bases/{knowledge_base_id}/documents",
+ headers=self._headers(context),
+ files={"file": (file_name, content, mime_type or "application/octet-stream")},
+ )
+ self._raise_for_status(response)
+ payload = response.json()
+ if payload.get("code") not in (0, None):
+ raise SharedAIPlatformError(
+ status_code=response.status_code,
+ message=str(payload.get("message") or "request failed"),
+ code=payload.get("code"),
+ data=payload.get("data"),
+ )
+ return payload["data"]
+
+ def retry_document(
+ self,
+ context: RequestContext,
+ knowledge_base_id: str,
+ document_id: str,
+ *,
+ file_name: str | None = None,
+ mime_type: str | None = None,
+ content: bytes | None = None,
+ ) -> dict:
+ request_kwargs = {
+ "headers": self._headers(context),
+ }
+ if content is not None:
+ request_kwargs["files"] = {"file": (file_name or "document.txt", content, mime_type or "application/octet-stream")}
+ response = self._client.post(
+ f"/api/v0.2/knowledge-bases/{knowledge_base_id}/documents/{document_id}/retry",
+ **request_kwargs,
+ )
+ self._raise_for_status(response)
+ payload = response.json()
+ if payload.get("code") not in (0, None):
+ raise SharedAIPlatformError(
+ status_code=response.status_code,
+ message=str(payload.get("message") or "request failed"),
+ code=payload.get("code"),
+ data=payload.get("data"),
+ )
+ return payload["data"]
+
+ def sync_content(
+ self,
+ context: RequestContext,
+ knowledge_base_id: str,
+ *,
+ records: list[SyncRecord | dict],
+ schema_version: str = "content-sync.v1",
+ sync_mode: str = "incremental",
+ batch_id: str | None = None,
+ cursor_token: str | None = None,
+ ) -> dict:
+ normalized_records = [
+ record.to_dict() if isinstance(record, SyncRecord) else record
+ for record in records
+ ]
+ return self._request_json(
+ "POST",
+ f"/api/v0.2/knowledge-bases/{knowledge_base_id}/sync-content",
+ context=context,
+ json_body={
+ "schema_version": schema_version,
+ "sync_mode": sync_mode,
+ "batch_id": batch_id,
+ "cursor_token": cursor_token,
+ "records": normalized_records,
+ },
+ )["data"]
+
+ def list_sync_jobs(self, context: RequestContext, knowledge_base_id: str) -> dict:
+ return self._request_json(
+ "GET",
+ f"/api/v0.2/knowledge-bases/{knowledge_base_id}/sync-jobs",
+ context=context,
+ )["data"]
+
+ def run_legal_review(
+ self,
+ context: RequestContext,
+ *,
+ title: str,
+ text: str,
+ contract_type: str | None = None,
+ review_as: str = "party_a",
+ provider: str | None = None,
+ ) -> dict:
+ return self._request_json(
+ "POST",
+ "/api/v0.1/domain-packs/legal-review/reviews/run",
+ context=context,
+ json_body={
+ "title": title,
+ "contract_type": contract_type,
+ "review_as": review_as,
+ "text": text,
+ "provider": provider,
+ },
+ )["data"]
+
+ def stream_conversation(
+ self,
+ context: RequestContext,
+ *,
+ message: str,
+ chat_context: dict | None = None,
+ provider: str | None = None,
+ template_key: str | None = None,
+ conversation_id: str | None = None,
+ ) -> Iterator[SSEEvent]:
+ headers = self._headers(context)
+ with self._client.stream(
+ "POST",
+ "/api/v0.2/conversations/stream",
+ headers=headers,
+ json={
+ "conversation_id": conversation_id,
+ "message": message,
+ "chat_context": chat_context or {},
+ "provider": provider,
+ "template_key": template_key,
+ },
+ ) as response:
+ self._raise_for_status(response)
+ for line in response.iter_lines():
+ if not line:
+ continue
+ if isinstance(line, bytes):
+ line = line.decode("utf-8")
+ if not line.startswith("data: "):
+ continue
+ payload = json.loads(line.removeprefix("data: ").strip())
+ yield SSEEvent(event_type=str(payload.get("type") or "unknown"), payload=payload)
+
+ def _request_json(
+ self,
+ method: str,
+ path: str,
+ *,
+ context: RequestContext | None = None,
+ json_body: dict | None = None,
+ params: dict | None = None,
+ ) -> dict:
+ response = self._client.request(
+ method,
+ path,
+ headers=self._headers(context),
+ json=json_body,
+ params=params,
+ )
+ self._raise_for_status(response)
+ payload = response.json()
+ if payload.get("code") not in (0, None):
+ raise SharedAIPlatformError(
+ status_code=response.status_code,
+ message=str(payload.get("message") or "request failed"),
+ code=payload.get("code"),
+ data=payload.get("data"),
+ )
+ return payload
+
+ def _headers(self, context: RequestContext | None = None) -> dict[str, str]:
+ headers: dict[str, str] = {}
+ if self.api_key.strip():
+ headers["Authorization"] = f"Bearer {self.api_key}"
+ if context is not None:
+ headers.update(
+ {
+ "X-Shared-AI-App-Id": self.app_id,
+ "X-Shared-AI-Tenant-Id": context.tenant_id,
+ "X-Shared-AI-User-Id": context.user_id,
+ "X-Shared-AI-User-Role": context.user_role,
+ }
+ )
+ return headers
+
+ def _raise_for_status(self, response: httpx.Response):
+ try:
+ response.raise_for_status()
+ except httpx.HTTPStatusError as exc:
+ message = response.text
+ try:
+ payload = response.json()
+ message = str(payload.get("message") or message)
+ code = payload.get("code")
+ data = payload.get("data")
+ except Exception:
+ code = None
+ data = None
+ raise SharedAIPlatformError(
+ status_code=response.status_code,
+ message=message,
+ code=code,
+ data=data,
+ ) from exc
diff --git a/backend/vendor/shared-ai-platform-sdk/shared_ai_platform_sdk/errors.py b/backend/vendor/shared-ai-platform-sdk/shared_ai_platform_sdk/errors.py
new file mode 100644
index 00000000..7aaecaf7
--- /dev/null
+++ b/backend/vendor/shared-ai-platform-sdk/shared_ai_platform_sdk/errors.py
@@ -0,0 +1,6 @@
+class SharedAIPlatformError(Exception):
+ def __init__(self, status_code: int, message: str, code: int | None = None, data=None):
+ super().__init__(message)
+ self.status_code = status_code
+ self.code = code
+ self.data = data
diff --git a/backend/vendor/shared-ai-platform-sdk/shared_ai_platform_sdk/types.py b/backend/vendor/shared-ai-platform-sdk/shared_ai_platform_sdk/types.py
new file mode 100644
index 00000000..25933e4e
--- /dev/null
+++ b/backend/vendor/shared-ai-platform-sdk/shared_ai_platform_sdk/types.py
@@ -0,0 +1,48 @@
+from dataclasses import dataclass, field
+from typing import Any
+
+
+@dataclass(frozen=True)
+class RequestContext:
+ tenant_id: str
+ user_id: str
+ user_role: str = "admin"
+
+
+@dataclass(frozen=True)
+class SSEEvent:
+ event_type: str
+ payload: dict[str, Any]
+
+
+@dataclass(frozen=True)
+class PollOptions:
+ interval_seconds: float = 1.0
+ timeout_seconds: float = 60.0
+ terminal_statuses: tuple[str, ...] = ("succeeded", "failed", "cancelled")
+
+
+@dataclass(frozen=True)
+class SyncRecord:
+ source_type: str
+ source_id: str
+ title: str | None = None
+ content: str | None = None
+ operation: str = "upsert"
+ schema_version: str = "content-sync.v1"
+ source_version: str | None = None
+ source_updated_at: str | None = None
+ metadata: dict[str, Any] = field(default_factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ return {
+ "schema_version": self.schema_version,
+ "operation": self.operation,
+ "source_type": self.source_type,
+ "source_id": self.source_id,
+ "source_version": self.source_version,
+ "source_updated_at": self.source_updated_at,
+ "title": self.title,
+ "content": self.content,
+ "metadata": self.metadata,
+ }
diff --git a/docs/AI法律文书助手_合并整理版.md b/docs/archive/2026-06-17-strategy-refresh/AI法律文书助手_合并整理版.md
similarity index 100%
rename from docs/AI法律文书助手_合并整理版.md
rename to docs/archive/2026-06-17-strategy-refresh/AI法律文书助手_合并整理版.md
diff --git a/docs/archive/2026-06-17-strategy-refresh/README.md b/docs/archive/2026-06-17-strategy-refresh/README.md
new file mode 100644
index 00000000..5a9b635b
--- /dev/null
+++ b/docs/archive/2026-06-17-strategy-refresh/README.md
@@ -0,0 +1,6 @@
+# 2026-06-17 Strategy Refresh Archive
+
+本目录保存被新规划替代的旧 `docs/` 文档。
+
+当前保留为历史资料,不再作为当前执行依据。
+
diff --git a/docs/integration-plan.md b/docs/archive/2026-06-17-strategy-refresh/integration-plan.md
similarity index 100%
rename from docs/integration-plan.md
rename to docs/archive/2026-06-17-strategy-refresh/integration-plan.md
diff --git a/frontend/src/app/contract-approvals/page.tsx b/frontend/src/app/contract-approvals/page.tsx
new file mode 100644
index 00000000..7fc3b8dd
--- /dev/null
+++ b/frontend/src/app/contract-approvals/page.tsx
@@ -0,0 +1,272 @@
+"use client";
+
+import {
+ CheckCircleOutlined,
+ CloseCircleOutlined,
+ FileSearchOutlined,
+ ReloadOutlined,
+} from "@ant-design/icons";
+import { Button, Empty, Modal, Select, Space, Table, Tag, Typography, message } from "antd";
+import type { ColumnsType } from "antd/es/table";
+import Link from "next/link";
+import { useCallback, useEffect, useMemo, useState } from "react";
+
+import {
+ decideContractApproval,
+ listContractApprovals,
+ type ContractApprovalStep,
+} from "@/services/contract";
+import {
+ formatContractApprovalStatus,
+ formatContractApprovalStepType,
+ formatContractType,
+ formatDateTime,
+ formatLifecycleStatus,
+ getContractApprovalStatusColor,
+ getContractApprovalStepTypeColor,
+ getLifecycleStatusColor,
+} from "@/lib/format";
+
+const { Text } = Typography;
+
+const scopeOptions = [
+ { label: "待我处理", value: "assigned_to_me" },
+ { label: "我提交的", value: "submitted_by_me" },
+ { label: "我参与的", value: "participated" },
+ { label: "全部审批", value: "all" },
+];
+
+const statusOptions = [
+ { label: "全部状态", value: "" },
+ { label: "审批中", value: "pending" },
+ { label: "已通过", value: "approved" },
+ { label: "已退回", value: "rejected" },
+];
+
+export default function ContractApprovalsPage() {
+ const [messageApi, contextHolder] = message.useMessage();
+ const [items, setItems] = useState([]);
+ const [summary, setSummary] = useState>({});
+ const [loading, setLoading] = useState(true);
+ const [scope, setScope] = useState("assigned_to_me");
+ const [statusValue, setStatusValue] = useState("");
+ const [decidingId, setDecidingId] = useState(null);
+
+ const loadApprovals = useCallback(async () => {
+ setLoading(true);
+ try {
+ const result = await listContractApprovals({
+ page: 1,
+ page_size: 100,
+ scope,
+ status_value: statusValue || undefined,
+ });
+ if (result.code !== 0) {
+ messageApi.error(result.message || "审批工作台加载失败");
+ return;
+ }
+ setItems(result.data.items);
+ setSummary(result.data.summary ?? {});
+ } catch {
+ messageApi.error("审批工作台加载失败,请稍后重试");
+ } finally {
+ setLoading(false);
+ }
+ }, [messageApi, scope, statusValue]);
+
+ useEffect(() => {
+ const timer = window.setTimeout(() => {
+ void loadApprovals();
+ }, 0);
+ return () => window.clearTimeout(timer);
+ }, [loadApprovals]);
+
+ const metrics = useMemo(() => [
+ { label: "待我审批", value: summary.assigned_to_me ?? 0, tone: "warning" },
+ { label: "全部待审批", value: summary.pending_all ?? 0, tone: "warning" },
+ { label: "我提交的", value: summary.submitted_by_me ?? 0 },
+ { label: "已通过", value: summary.approved ?? 0, tone: "success" },
+ { label: "已退回", value: summary.rejected ?? 0, tone: "danger" },
+ ], [summary]);
+
+ const handleDecision = useCallback((item: ContractApprovalStep, decision: "approve" | "reject") => {
+ Modal.confirm({
+ title: decision === "approve" ? "确认通过审批?" : "确认退回审批?",
+ content: decision === "approve" ? "通过后合同会进入已审批状态,可继续签署或归档。" : "退回后合同会回到已生成状态,提交人可修改后重新提交。",
+ okText: decision === "approve" ? "通过" : "退回",
+ cancelText: "取消",
+ okButtonProps: { danger: decision === "reject" },
+ onOk: async () => {
+ setDecidingId(item.id);
+ try {
+ const result = await decideContractApproval(item.id, { decision });
+ if (result.code !== 0) {
+ messageApi.error(result.message || "审批处理失败");
+ return;
+ }
+ messageApi.success(decision === "approve" ? "审批已通过" : "审批已退回");
+ await loadApprovals();
+ } catch {
+ messageApi.error("审批处理失败,请稍后重试");
+ } finally {
+ setDecidingId(null);
+ }
+ },
+ });
+ }, [loadApprovals, messageApi]);
+
+ const columns = useMemo>(() => [
+ {
+ title: "合同",
+ key: "contract",
+ fixed: "left",
+ width: 280,
+ render: (_, record) => (
+
+
+ {record.contract?.template_name ?? record.title}
+
+ {record.contract?.file_name ?? "合同文件"}
+
+ ),
+ },
+ {
+ title: "类型",
+ key: "type",
+ width: 150,
+ render: (_, record) => formatContractType(record.contract?.document_type_code ?? record.contract?.contract_type),
+ },
+ {
+ title: "审批",
+ key: "approval",
+ width: 180,
+ render: (_, record) => (
+
+
+ {formatContractApprovalStepType(record.step_type)}
+
+
+ {formatContractApprovalStatus(record.status)}
+
+
+ ),
+ },
+ {
+ title: "生命周期",
+ key: "lifecycle",
+ width: 140,
+ render: (_, record) => (
+
+ {formatLifecycleStatus(record.contract?.lifecycle_status)}
+
+ ),
+ },
+ {
+ title: "提交/处理人",
+ key: "people",
+ width: 210,
+ render: (_, record) => (
+
+ {record.submitter_name || "提交人未记录"}
+ 处理人:{record.assignee_name || "-"}
+
+ ),
+ },
+ {
+ title: "提交时间",
+ dataIndex: "created_at",
+ width: 170,
+ render: (value: string | null) => formatDateTime(value),
+ sorter: (a, b) => new Date(a.created_at || 0).getTime() - new Date(b.created_at || 0).getTime(),
+ },
+ {
+ title: "操作",
+ key: "actions",
+ width: 210,
+ render: (_, record) => (
+
+ 详情
+ {record.status === "pending" ? (
+ <>
+ }
+ loading={decidingId === record.id}
+ onClick={() => handleDecision(record, "approve")}
+ >
+ 通过
+
+ }
+ loading={decidingId === record.id}
+ onClick={() => handleDecision(record, "reject")}
+ >
+ 退回
+
+ >
+ ) : null}
+
+ ),
+ },
+ ], [decidingId, handleDecision]);
+
+ return (
+
+ {contextHolder}
+
+
+
+
Contract Approval
+
合同审批
+
集中处理合同提交审批、通过、退回和后续签署前状态确认。
+
+
+ } loading={loading} onClick={() => void loadApprovals()}>
+ 刷新
+
+
+ }>
+ 查看合同台账
+
+
+
+
+
+
+ {metrics.map((metric) => (
+
+ {metric.label}
+ {metric.value}
+
+ ))}
+
+
+
+
+
+
+
审批任务
+
当前显示 {items.length} 条审批记录。
+
+
+
+
+
+
+
+
}}
+ pagination={{ pageSize: 10 }}
+ rowKey="id"
+ scroll={{ x: 1330 }}
+ />
+
+
+
+ );
+}
diff --git a/frontend/src/app/contract-ledger/page.tsx b/frontend/src/app/contract-ledger/page.tsx
new file mode 100644
index 00000000..dd907973
--- /dev/null
+++ b/frontend/src/app/contract-ledger/page.tsx
@@ -0,0 +1,350 @@
+"use client";
+
+import {
+ FileDoneOutlined,
+ ReloadOutlined,
+ SearchOutlined,
+ WarningOutlined,
+} from "@ant-design/icons";
+import { Button, Empty, Input, Space, Table, Tag, Typography, message } from "antd";
+import type { ColumnsType } from "antd/es/table";
+import Link from "next/link";
+import { useCallback, useEffect, useMemo, useState } from "react";
+
+import {
+ formatAutoReviewStatus,
+ formatContractType,
+ formatDateTime,
+ formatDueStatus,
+ formatLifecycleStatus,
+ formatRisk,
+ getAutoReviewStatusColor,
+ getDueStatusColor,
+ getLifecycleStatusColor,
+ getRiskColor,
+} from "@/lib/format";
+import { listGeneratedContracts, type GeneratedContractListItem } from "@/services/contract";
+
+const { Text } = Typography;
+
+type LedgerMetric = {
+ label: string;
+ value: number;
+ tone?: "warning" | "danger" | "success";
+};
+
+function getLifecycleStatus(item: GeneratedContractListItem) {
+ return item.lifecycle?.status ?? "generated";
+}
+
+function getDueStatus(item: GeneratedContractListItem) {
+ return item.lifecycle?.due_status ?? "not_set";
+}
+
+function isHighRisk(item: GeneratedContractListItem) {
+ return item.auto_review?.risk_level === "critical" || item.auto_review?.risk_level === "high";
+}
+
+function getTimeValue(value: string | null | undefined) {
+ if (!value) return 0;
+ const time = new Date(value).getTime();
+ return Number.isNaN(time) ? 0 : time;
+}
+
+function getPriorityWeight(item: GeneratedContractListItem) {
+ const dueStatus = getDueStatus(item);
+ const lifecycleStatus = getLifecycleStatus(item);
+
+ if (dueStatus === "overdue") return 0;
+ if (dueStatus === "due_soon") return 1;
+ if (isHighRisk(item)) return 2;
+ if (lifecycleStatus === "internal_review" || lifecycleStatus === "external_review") return 3;
+ return 99;
+}
+
+function getPriorityLabel(item: GeneratedContractListItem) {
+ const dueStatus = getDueStatus(item);
+ const lifecycleStatus = getLifecycleStatus(item);
+
+ if (dueStatus === "overdue") return "逾期处理";
+ if (dueStatus === "due_soon") return "到期跟进";
+ if (isHighRisk(item)) return "高风险预审";
+ if (lifecycleStatus === "internal_review" || lifecycleStatus === "external_review") return "审查推进";
+ return "合同跟进";
+}
+
+function getPriorityDescription(item: GeneratedContractListItem) {
+ const dueStatus = getDueStatus(item);
+ const lifecycleStatus = getLifecycleStatus(item);
+
+ if (dueStatus === "overdue") return "合同期限已经越过,建议补齐续约、终止或归档动作。";
+ if (dueStatus === "due_soon") return "合同即将到期,建议提前确认续约、结算和履约节点。";
+ if (isHighRisk(item)) return "自动预审提示高风险,建议先处理关键条款再推进签署。";
+ if (lifecycleStatus === "internal_review" || lifecycleStatus === "external_review") return "合同仍处于审查流转中,需要继续跟进修改和确认。";
+ return "继续完善合同状态和后续处理记录。";
+}
+
+function buildMetrics(items: GeneratedContractListItem[]): LedgerMetric[] {
+ const inReview = items.filter((item) => ["internal_review", "external_review"].includes(getLifecycleStatus(item))).length;
+ const active = items.filter((item) => ["signed", "effective", "expiring"].includes(getLifecycleStatus(item))).length;
+ const dueSoon = items.filter((item) => getDueStatus(item) === "due_soon").length;
+ const overdue = items.filter((item) => getDueStatus(item) === "overdue").length;
+ const highRisk = items.filter(isHighRisk).length;
+
+ return [
+ { label: "已入台账", value: items.length },
+ { label: "审查流转中", value: inReview },
+ { label: "签署/履约中", value: active, tone: "success" },
+ { label: "即将到期", value: dueSoon, tone: "warning" },
+ { label: "已逾期", value: overdue, tone: "danger" },
+ { label: "高风险预审", value: highRisk, tone: "danger" },
+ ];
+}
+
+function matchesKeyword(item: GeneratedContractListItem, keyword: string) {
+ const lifecycle = item.lifecycle;
+ const searchText = [
+ item.template_name,
+ item.file_name,
+ item.contract_type,
+ item.metadata?.generated_by_name,
+ lifecycle?.counterparty_name,
+ lifecycle?.owner_name,
+ formatContractType(item.contract_type),
+ formatLifecycleStatus(lifecycle?.status),
+ formatDueStatus(lifecycle),
+ formatAutoReviewStatus(item.auto_review?.status),
+ formatRisk(item.auto_review?.risk_level),
+ ]
+ .filter(Boolean)
+ .join(" ")
+ .toLowerCase();
+
+ return searchText.includes(keyword.toLowerCase());
+}
+
+export default function ContractLedgerPage() {
+ const [messageApi, contextHolder] = message.useMessage();
+ const [contracts, setContracts] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [searchQuery, setSearchQuery] = useState("");
+
+ const loadContracts = useCallback(async () => {
+ setLoading(true);
+ try {
+ const result = await listGeneratedContracts({ page: 1, page_size: 200 });
+ if (result.code !== 0) {
+ messageApi.error(result.message || "合同台账加载失败");
+ return;
+ }
+ setContracts(result.data.items);
+ } catch {
+ messageApi.error("合同台账加载失败,请稍后重试");
+ } finally {
+ setLoading(false);
+ }
+ }, [messageApi]);
+
+ useEffect(() => {
+ const timer = window.setTimeout(() => {
+ void loadContracts();
+ }, 0);
+ return () => window.clearTimeout(timer);
+ }, [loadContracts]);
+
+ const metrics = useMemo(() => buildMetrics(contracts), [contracts]);
+ const priorityContracts = useMemo(() => {
+ return contracts
+ .filter((item) => getPriorityWeight(item) < 99)
+ .sort((a, b) => {
+ const weightDiff = getPriorityWeight(a) - getPriorityWeight(b);
+ if (weightDiff !== 0) return weightDiff;
+ return getTimeValue(b.lifecycle?.updated_at ?? b.generated_at) - getTimeValue(a.lifecycle?.updated_at ?? a.generated_at);
+ })
+ .slice(0, 6);
+ }, [contracts]);
+ const filteredContracts = useMemo(() => {
+ const keyword = searchQuery.trim();
+ if (!keyword) return contracts;
+ return contracts.filter((item) => matchesKeyword(item, keyword));
+ }, [contracts, searchQuery]);
+
+ const columns = useMemo>(() => [
+ {
+ title: "合同",
+ key: "contract",
+ fixed: "left",
+ width: 260,
+ render: (_, record) => (
+
+
+ {record.template_name}
+
+ {record.file_name}
+
+ ),
+ sorter: (a, b) => a.template_name.localeCompare(b.template_name, "zh-CN"),
+ },
+ {
+ title: "类型",
+ dataIndex: "contract_type",
+ width: 150,
+ render: (value: string) => formatContractType(value),
+ sorter: (a, b) => formatContractType(a.contract_type).localeCompare(formatContractType(b.contract_type), "zh-CN"),
+ },
+ {
+ title: "相对方/负责人",
+ key: "owner",
+ width: 190,
+ render: (_, record) => (
+
+ {record.lifecycle?.counterparty_name || "未填写相对方"}
+ {record.lifecycle?.owner_name || record.metadata?.generated_by_name || "未设置负责人"}
+
+ ),
+ },
+ {
+ title: "生命周期",
+ key: "lifecycle",
+ width: 220,
+ render: (_, record) => (
+
+
+ {formatLifecycleStatus(record.lifecycle?.status)}
+
+
+ {formatDueStatus(record.lifecycle)}
+
+
+ ),
+ },
+ {
+ title: "自动预审",
+ key: "auto_review",
+ width: 220,
+ render: (_, record) => (
+
+
+ {formatAutoReviewStatus(record.auto_review?.status)}
+
+
+ {record.auto_review?.risk_level ? formatRisk(record.auto_review.risk_level) : "未评级"}
+
+
+ ),
+ },
+ {
+ title: "最近更新",
+ key: "updated_at",
+ width: 180,
+ render: (_, record) => formatDateTime(record.lifecycle?.updated_at ?? record.generated_at),
+ sorter: (a, b) => getTimeValue(a.lifecycle?.updated_at ?? a.generated_at) - getTimeValue(b.lifecycle?.updated_at ?? b.generated_at),
+ },
+ {
+ title: "操作",
+ key: "actions",
+ width: 140,
+ render: (_, record) => (
+
+ 详情
+ {record.auto_review?.review_id ? 审查 : null}
+
+ ),
+ },
+ ], []);
+
+ return (
+
+ {contextHolder}
+
+
+
+
Contract Ledger
+
合同台账
+
生成后的合同进入统一台账,优先暴露到期、逾期和高风险事项。
+
+
+ } onClick={() => void loadContracts()} loading={loading}>
+ 刷新
+
+
+ }>
+ 新建合同
+
+
+
+
+
+
+ {metrics.map((metric) => (
+
+ {metric.label}
+ {metric.value}
+
+ ))}
+
+
+
+
+
+
优先处理
+
聚焦逾期、即将到期、高风险预审和仍在审查流转中的合同。
+
+ {priorityContracts.length === 0 ? (
+
+ ) : (
+
+ {priorityContracts.map((item) => (
+
+ {getPriorityLabel(item)}
+ {item.template_name}
+ {getPriorityDescription(item)}
+
+
+ {formatLifecycleStatus(item.lifecycle?.status)}
+
+
+ {formatDueStatus(item.lifecycle)}
+
+ {isHighRisk(item) ? (
+ }>
+ {formatRisk(item.auto_review?.risk_level)}
+
+ ) : null}
+
+
+ ))}
+
+ )}
+
+
+
+
+
+
合同列表
+
共 {contracts.length} 份合同,当前显示 {filteredContracts.length} 份。
+
+
setSearchQuery(event.target.value)}
+ placeholder="搜索合同、相对方、负责人或状态"
+ prefix={
}
+ value={searchQuery}
+ />
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/app/contracts/[id]/page.tsx b/frontend/src/app/contracts/[id]/page.tsx
index 2350a33a..ce097d24 100644
--- a/frontend/src/app/contracts/[id]/page.tsx
+++ b/frontend/src/app/contracts/[id]/page.tsx
@@ -10,6 +10,8 @@ import {
dueStatusMeta,
formatAutoReviewRisk,
formatAutoReviewStatus,
+ formatContractApprovalStatus,
+ formatContractApprovalStepType,
formatContractType,
formatDateTime,
formatDueStatus,
@@ -21,6 +23,8 @@ import {
formatLifecycleTaskType,
getAutoReviewRiskColor,
getAutoReviewStatusColor,
+ getContractApprovalStatusColor,
+ getContractApprovalStepTypeColor,
getDueStatusColor,
getGenerationModeMeta,
getGenerationQualityColor,
@@ -30,7 +34,6 @@ import {
getLifecycleTaskStatusColor,
getLifecycleTaskTypeColor,
isLifecycleTerminal,
- lifecycleStatusOptions,
lifecycleTaskPriorityOptions,
lifecycleTaskStatusOptions,
lifecycleTaskTypeOptions,
@@ -38,18 +41,25 @@ import {
} from "@/lib/format";
import {
createContractLifecycleTask,
+ decideContractApproval,
+ diffGeneratedContractVersions,
downloadContract,
+ downloadGeneratedContractVersion,
getGeneratedContract,
getGeneratedContractPreview,
+ listGeneratedContractVersions,
listContractLifecycleTasks,
+ submitContractApproval,
syncContractLifecycleTasks,
updateContractLifecycleTask,
updateGeneratedContractLifecycle,
+ type ContractApprovalStep,
type AutoReviewSummary,
- type ContractLifecycle,
type ContractLifecycleTask,
type GeneratedContract,
type GeneratedContractPreview,
+ type GeneratedContractVersionDiffResponse,
+ type GeneratedContractVersionItem,
} from "@/services/contract";
import { uploadFile } from "@/services/file";
import { downloadReviewReport } from "@/services/review";
@@ -70,11 +80,71 @@ type LifecycleTaskFormValues = {
evidence_note?: string;
};
+type ApprovalFormValues = {
+ title?: string;
+ assignee_name?: string;
+ comment?: string;
+};
+
+type ContractVersionRow = {
+ id: string;
+ version_number: number;
+ version_label: string;
+ change_type: string;
+ change_summary?: string | null;
+ created_at?: string | null;
+ is_base?: boolean;
+};
+
+const contractVersionChangeTypeLabelMap: Record = {
+ generated: "原始生成",
+ editor_save: "手动保存",
+ editor_autosave: "自动保存",
+ clause_recommendation: "补条款",
+ clause_rewrite: "条款改写",
+ review_suggestion: "审查建议",
+};
+
+const contractVersionChangeTypeColorMap: Record = {
+ generated: "blue",
+ editor_save: "green",
+ editor_autosave: "cyan",
+ clause_recommendation: "purple",
+ clause_rewrite: "geekblue",
+ review_suggestion: "gold",
+};
+
+const diffChangeTypeLabelMap: Record = {
+ added: "新增",
+ removed: "删除",
+ modified: "修改",
+};
+
+const diffChangeTypeColorMap: Record = {
+ added: "green",
+ removed: "red",
+ modified: "gold",
+};
+
+function formatContractVersionChangeType(value?: string | null) {
+ if (!value) {
+ return "版本变更";
+ }
+ return contractVersionChangeTypeLabelMap[value] ?? value;
+}
+
+function getContractVersionChangeTypeColor(value?: string | null) {
+ if (!value) {
+ return "default";
+ }
+ return contractVersionChangeTypeColorMap[value] ?? "default";
+}
+
function getGenerationQualityWarnings(detail?: GeneratedContract | null) {
const quality = detail?.generation_quality;
const source = detail?.metadata.generation_source;
return [
- ...(source?.fallback_used ? [`AI 增强生成已回退:${source.fallback_reason ?? "请重点复核生成内容"}`] : []),
+ ...(source?.fallback_used ? [`AI 增强起草已回退:${source.fallback_reason ?? "请重点复核合同正文"}`] : []),
...(quality?.blocking_issues ?? []),
...(quality?.warning_issues ?? []),
...(detail?.clause_completion_suggestions ?? []).map((item) => `${item.title}:${item.recommendation}`),
@@ -97,10 +167,10 @@ function buildContractReadabilityItems(
return [
{ label: "交付文件", value: detail?.file_name ?? "-" },
{ label: "导出格式", value: detail?.export_format?.toUpperCase() ?? "-" },
- { label: "生成模板", value: detail?.template_name ?? "-" },
- { label: "自动审查", value: formatAutoReviewStatus(autoReview?.status) },
+ { label: "起草模板", value: detail?.template_name ?? "-" },
+ { label: "自动预审", value: formatAutoReviewStatus(autoReview?.status) },
{ label: "总体风险", value: formatAutoReviewRisk(autoReview?.risk_level) },
- { label: "下一步动作", value: autoReview?.next_action ?? autoReview?.status_detail ?? "可进入详情继续审查或问答复核" },
+ { label: "下一步动作", value: autoReview?.next_action ?? autoReview?.status_detail ?? "可进入详情继续预审或问答复核" },
];
}
function shouldWarnBeforeDownload(detail?: GeneratedContract | null) {
@@ -122,7 +192,7 @@ function buildContractChatHref(
reviewId: null,
prompt,
metadata: {
- reviewTitle: detail?.template_name ?? "合同生成结果",
+ reviewTitle: detail?.template_name ?? "合同起草结果",
reviewStatus: detail ? `已生成 ${detail.export_format.toUpperCase()}` : null,
contractType: detail?.contract_type ?? null,
overallRisk: null,
@@ -139,6 +209,7 @@ function buildContractChatHref(
export default function GeneratedContractDetailPage({ params }: { params: Promise<{ id: string }> }) {
const [messageApi, contextHolder] = message.useMessage();
const [lifecycleTaskForm] = Form.useForm();
+ const [approvalForm] = Form.useForm();
const router = useRouter();
const currentUser = useAuthStore((state) => state.currentUser);
const [loading, setLoading] = useState(true);
@@ -147,9 +218,20 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
const [preview, setPreview] = useState(null);
const [lifecycleTasks, setLifecycleTasks] = useState([]);
const [lifecycleTaskLoading, setLifecycleTaskLoading] = useState(false);
+ const [versionLoading, setVersionLoading] = useState(false);
+ const [versionDiffLoading, setVersionDiffLoading] = useState(false);
+ const [contractVersions, setContractVersions] = useState([]);
+ const [baseVersionCreatedAt, setBaseVersionCreatedAt] = useState(null);
+ const [versionDiff, setVersionDiff] = useState(null);
+ const [fromVersionId, setFromVersionId] = useState("base");
+ const [toVersionId, setToVersionId] = useState("base");
+ const [versionDownloadingId, setVersionDownloadingId] = useState(null);
const [lifecycleTaskModalOpen, setLifecycleTaskModalOpen] = useState(false);
const [lifecycleTaskSaving, setLifecycleTaskSaving] = useState(false);
const [lifecycleTaskSyncing, setLifecycleTaskSyncing] = useState(false);
+ const [approvalModalOpen, setApprovalModalOpen] = useState(false);
+ const [approvalSubmitting, setApprovalSubmitting] = useState(false);
+ const [approvalDecidingId, setApprovalDecidingId] = useState(null);
const [editingLifecycleTask, setEditingLifecycleTask] = useState(null);
const [evidenceUploadList, setEvidenceUploadList] = useState([]);
const [notFound, setNotFound] = useState(false);
@@ -162,6 +244,7 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
const loadDetail = async () => {
setLoading(true);
setLifecycleTaskLoading(true);
+ setVersionLoading(true);
setNotFound(false);
setLoadError(null);
@@ -181,9 +264,35 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
throw error;
}
}
+ let versionItems: GeneratedContractVersionItem[] = [];
+ let baseCreatedAt: string | null = detailResponse.data.generated_at ?? null;
+ let initialToVersionId = "base";
+ let initialVersionDiff: GeneratedContractVersionDiffResponse | null = null;
+ try {
+ const versionsResponse = await listGeneratedContractVersions(id);
+ if (versionsResponse.code === 0) {
+ versionItems = versionsResponse.data.items;
+ baseCreatedAt = versionsResponse.data.base_version.created_at ?? baseCreatedAt;
+ initialToVersionId = versionItems[0]?.id ?? "base";
+ if (initialToVersionId !== "base") {
+ const diffResponse = await diffGeneratedContractVersions(id, {
+ from_version_id: "base",
+ to_version_id: initialToVersionId,
+ });
+ initialVersionDiff = diffResponse.code === 0 ? diffResponse.data : null;
+ }
+ }
+ } catch {
+ initialVersionDiff = null;
+ }
setDetail(detailResponse.data);
setPreview(previewResponse.code === 0 ? previewResponse.data : null);
setLifecycleTasks(lifecycleTaskResponse?.code === 0 ? lifecycleTaskResponse.data.items : []);
+ setContractVersions(versionItems);
+ setBaseVersionCreatedAt(baseCreatedAt);
+ setFromVersionId("base");
+ setToVersionId(initialToVersionId);
+ setVersionDiff(initialVersionDiff);
} catch (error: unknown) {
if (isRequestError(error) && error.status === 404) {
setNotFound(true);
@@ -193,6 +302,7 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
} finally {
setLoading(false);
setLifecycleTaskLoading(false);
+ setVersionLoading(false);
}
};
@@ -212,19 +322,51 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
}));
}, [detail]);
- const exportOptionRows = useMemo(() => {
- if (!detail) {
- return [];
- }
+ const versionRows = useMemo(() => {
+ const baseRow: ContractVersionRow = {
+ id: "base",
+ version_number: 1,
+ version_label: "v1",
+ change_type: "generated",
+ change_summary: "原始生成版本",
+ created_at: baseVersionCreatedAt ?? detail?.generated_at ?? null,
+ is_base: true,
+ };
- return Object.entries(detail.metadata.export_options ?? {}).map(([name, value]) => ({
- key: name,
- name,
- value: typeof value === "string" ? value : JSON.stringify(value),
- }));
- }, [detail]);
+ return [
+ ...contractVersions.map((version) => ({
+ id: version.id,
+ version_number: version.version_number,
+ version_label: version.version_label,
+ change_type: version.change_type,
+ change_summary: version.change_summary,
+ created_at: version.created_at,
+ })),
+ baseRow,
+ ];
+ }, [baseVersionCreatedAt, contractVersions, detail?.generated_at]);
+
+ const versionSelectOptions = useMemo(
+ () => [...versionRows]
+ .sort((a, b) => a.version_number - b.version_number)
+ .map((version) => ({
+ value: version.id,
+ label: `${version.version_label} · ${formatContractVersionChangeType(version.change_type)}`,
+ })),
+ [versionRows],
+ );
+
+ const latestVersionLabel = contractVersions[0]?.version_label ?? "v1";
const autoReview = detail?.auto_review ?? null;
+ const approvalSummary = detail?.approval ?? null;
+ const approvalHistory = approvalSummary?.history ?? [];
+ const pendingApproval = approvalSummary?.pending ?? null;
+ const canDecideApproval = (approval?: ContractApprovalStep | null) => {
+ if (!approval || approval.status !== "pending") return false;
+ if (currentUser?.role === "admin") return true;
+ return Boolean(approval.assignee_id && approval.assignee_id === currentUser?.id);
+ };
const readabilityItems = useMemo(
() => buildContractReadabilityItems(detail, autoReview),
[detail, autoReview],
@@ -308,19 +450,165 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
const handleOpenAutoReviewDetail = (autoReview?: AutoReviewSummary | null) => {
if (!autoReview) {
- messageApi.warning("自动审查结果暂不可用");
+ messageApi.warning("自动预审结果暂不可用");
return;
}
const target = autoReview.detail_url ?? (autoReview.review_id ? `/reviews/${autoReview.review_id}` : null);
if (!target) {
- messageApi.warning("自动审查详情尚未生成");
+ messageApi.warning("自动预审详情尚未生成");
return;
}
router.push(target);
};
+ const reloadContractDetail = async (contractId = detail?.id) => {
+ if (!contractId) {
+ return;
+ }
+ const result = await getGeneratedContract(contractId);
+ if (result.code !== 0) {
+ messageApi.error(result.message || "合同详情刷新失败");
+ return;
+ }
+ setDetail(result.data);
+ };
+
+ const refreshVersionData = async (contractId = detail?.id, quiet = false) => {
+ if (!contractId) {
+ return;
+ }
+
+ setVersionLoading(true);
+ try {
+ const result = await listGeneratedContractVersions(contractId);
+ if (result.code !== 0) {
+ if (!quiet) {
+ messageApi.warning(result.message || "版本历史加载失败");
+ }
+ return;
+ }
+
+ const items = result.data.items;
+ const availableIds = new Set(["base", ...items.map((item) => item.id)]);
+ const nextFromVersionId = availableIds.has(fromVersionId) ? fromVersionId : "base";
+ const nextToVersionId = availableIds.has(toVersionId) ? toVersionId : items[0]?.id ?? "base";
+ setContractVersions(items);
+ setBaseVersionCreatedAt(result.data.base_version.created_at ?? detail?.generated_at ?? null);
+ setFromVersionId(nextFromVersionId);
+ setToVersionId(nextToVersionId);
+
+ if (nextFromVersionId !== nextToVersionId) {
+ const diffResponse = await diffGeneratedContractVersions(contractId, {
+ from_version_id: nextFromVersionId,
+ to_version_id: nextToVersionId,
+ });
+ setVersionDiff(diffResponse.code === 0 ? diffResponse.data : null);
+ } else {
+ setVersionDiff(null);
+ }
+
+ if (!quiet) {
+ messageApi.success("版本历史已刷新");
+ }
+ } catch (error: unknown) {
+ if (!quiet) {
+ messageApi.warning(error instanceof Error ? error.message : "版本历史加载失败");
+ }
+ } finally {
+ setVersionLoading(false);
+ }
+ };
+
+ const handleCompareVersions = async () => {
+ if (!detail) {
+ return;
+ }
+ if (fromVersionId === toVersionId) {
+ setVersionDiff(null);
+ messageApi.info("请选择两个不同版本进行对比");
+ return;
+ }
+
+ setVersionDiffLoading(true);
+ try {
+ const result = await diffGeneratedContractVersions(detail.id, {
+ from_version_id: fromVersionId,
+ to_version_id: toVersionId,
+ });
+ if (result.code !== 0) {
+ messageApi.warning(result.message || "版本对比加载失败");
+ return;
+ }
+ setVersionDiff(result.data);
+ messageApi.success("版本对比已更新");
+ } catch (error: unknown) {
+ messageApi.warning(error instanceof Error ? error.message : "版本对比加载失败");
+ } finally {
+ setVersionDiffLoading(false);
+ }
+ };
+
+ const handleCompareVersionWithBase = async (versionId: string) => {
+ if (!detail || versionId === "base") {
+ return;
+ }
+
+ setFromVersionId("base");
+ setToVersionId(versionId);
+ setVersionDiffLoading(true);
+ try {
+ const result = await diffGeneratedContractVersions(detail.id, {
+ from_version_id: "base",
+ to_version_id: versionId,
+ });
+ if (result.code !== 0) {
+ messageApi.warning(result.message || "版本对比加载失败");
+ return;
+ }
+ setVersionDiff(result.data);
+ messageApi.success("已对比原始版本与所选版本");
+ } catch (error: unknown) {
+ messageApi.warning(error instanceof Error ? error.message : "版本对比加载失败");
+ } finally {
+ setVersionDiffLoading(false);
+ }
+ };
+
+ const handleDownloadContractVersion = async (version: ContractVersionRow) => {
+ if (!detail) {
+ return;
+ }
+
+ setVersionDownloadingId(version.id);
+ try {
+ await downloadGeneratedContractVersion(detail.id, version.id, `${detail.template_name}_${version.version_label}.docx`);
+ messageApi.success("版本文件下载开始");
+ } catch (error: unknown) {
+ messageApi.error(error instanceof Error ? error.message : "版本文件下载失败,请稍后重试");
+ } finally {
+ setVersionDownloadingId(null);
+ }
+ };
+
+ const handleOpenVersionInEditor = (version: ContractVersionRow) => {
+ if (!detail) {
+ return;
+ }
+
+ const searchParams = new URLSearchParams();
+ searchParams.set("version", version.id);
+ router.push(`/documents/${detail.id}/edit?${searchParams.toString()}`);
+ };
+
+ const handleOpenLatestVersionInEditor = () => {
+ if (!detail) {
+ return;
+ }
+ router.push(`/documents/${detail.id}/edit`);
+ };
+
const handleRestore = () => {
if (!detail) {
return;
@@ -358,6 +646,72 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
}
};
+ const handleOpenApprovalModal = () => {
+ approvalForm.setFieldsValue({
+ title: detail ? `合同审批:${detail.template_name}` : "合同审批",
+ assignee_name: currentUser?.role === "admin" ? currentUser.display_name : detail?.metadata.generated_by_name,
+ comment: "",
+ });
+ setApprovalModalOpen(true);
+ };
+
+ const handleSubmitApproval = async () => {
+ if (!detail) {
+ return;
+ }
+ const values = await approvalForm.validateFields();
+ setApprovalSubmitting(true);
+ try {
+ const result = await submitContractApproval({
+ contract_id: detail.id,
+ title: values.title,
+ assignee_name: values.assignee_name,
+ comment: values.comment,
+ step_type: "legal_review",
+ });
+ if (result.code !== 0) {
+ messageApi.error(result.message || "提交审批失败");
+ return;
+ }
+ messageApi.success("合同已提交审批");
+ setApprovalModalOpen(false);
+ await reloadContractDetail(detail.id);
+ } catch (error: unknown) {
+ messageApi.error(error instanceof Error ? error.message : "提交审批失败,请稍后重试");
+ } finally {
+ setApprovalSubmitting(false);
+ }
+ };
+
+ const handleApprovalDecision = async (approval: ContractApprovalStep, decision: "approve" | "reject") => {
+ if (!detail) {
+ return;
+ }
+ Modal.confirm({
+ title: decision === "approve" ? "确认通过审批?" : "确认退回审批?",
+ content: decision === "approve" ? "通过后合同会进入已审批状态,可继续签署或归档准备。" : "退回后合同会回到已生成状态,可修改后重新提交。",
+ okText: decision === "approve" ? "通过" : "退回",
+ cancelText: "取消",
+ okButtonProps: { danger: decision === "reject" },
+ onOk: async () => {
+ setApprovalDecidingId(approval.id);
+ try {
+ const result = await decideContractApproval(approval.id, { decision });
+ if (result.code !== 0) {
+ messageApi.error(result.message || "审批处理失败");
+ return;
+ }
+ messageApi.success(decision === "approve" ? "审批已通过" : "审批已退回");
+ await reloadContractDetail(detail.id);
+ } catch (error: unknown) {
+ messageApi.error(error instanceof Error ? error.message : "审批处理失败,请稍后重试");
+ } finally {
+ setApprovalDecidingId(null);
+ }
+ },
+ });
+ };
+
const reloadLifecycleTasks = async (contractId = detail?.id) => {
if (!contractId) {
return;
@@ -540,7 +894,7 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
{detail?.metadata.generated_by_name ?? "-"}
- 生成时间
+ 起草时间
{formatDateTime(detail?.generated_at)}
@@ -555,10 +909,10 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
下一步动作
-
下载文件、回填生成参数,或把生成内容带入问答复核。
+
下载文件、继续编辑,或把起草内容带入自动预审和问答复核。
router.push("/contracts")}>返回列表
- 再次生成 / 继续编辑
+ 继续起草 / 再次生成
router.push(`/documents/${detail?.id}/edit`)}>
进入合同编辑器
@@ -600,30 +954,39 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
更新生命周期
+
+ {pendingApproval ? "审批中" : "提交审批"}
+
+ router.push("/contract-approvals")}>
+ 审批工作台
+
{currentUser?.role === "admin" && detail?.file_id ? router.push(`/files?highlight=${detail.file_id}`)}>查看文件资产 : null}
下载合同文件
- 当前生成链路已可继续接入审查助手,对生成结果做风险复核、改写建议和谈判准备。
+ 当前起草链路已可继续接入审查助手,对合同内容做风险复核、改写建议和谈判准备。
- 审查生成结果
+ 预审起草结果
生成修改建议
准备谈判策略
@@ -657,7 +1020,7 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
{detail?.generation_quality ? (
-
+
@@ -672,7 +1035,7 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
) : null}
@@ -699,7 +1062,7 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
{item.priority}
- 自动审查
+ 自动预审
{item.risk_level ? {item.risk_level} : null}
{item.title}
@@ -758,6 +1121,267 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
)}
+
+
+ {formatContractApprovalStatus(approvalSummary?.status)}
+
+
+ {pendingApproval ? "审批中" : "提交审批"}
+
+
+ }
+ >
+ {approvalHistory.length === 0 ? (
+
+ ) : (
+ {formatContractApprovalStepType(value)} ,
+ },
+ {
+ title: "状态",
+ dataIndex: "status",
+ width: 110,
+ render: (value: string) => {formatContractApprovalStatus(value)} ,
+ },
+ {
+ title: "标题/意见",
+ key: "title",
+ render: (_value: unknown, record: ContractApprovalStep) => (
+
+ {record.title}
+ {record.comment ? {record.comment} : null}
+
+ ),
+ },
+ {
+ title: "提交/处理人",
+ key: "people",
+ width: 220,
+ render: (_value: unknown, record: ContractApprovalStep) => (
+
+ {record.submitter_name || "-"}
+ 处理人:{record.assignee_name || "-"}
+
+ ),
+ },
+ {
+ title: "时间",
+ key: "time",
+ width: 180,
+ render: (_value: unknown, record: ContractApprovalStep) => formatDateTime(record.decided_at ?? record.created_at),
+ },
+ {
+ title: "操作",
+ key: "actions",
+ width: 160,
+ render: (_value: unknown, record: ContractApprovalStep) => canDecideApproval(record) ? (
+
+ void handleApprovalDecision(record, "approve")}
+ >
+ 通过
+
+ void handleApprovalDecision(record, "reject")}
+ >
+ 退回
+
+
+ ) : (
+ 已处理
+ ),
+ },
+ ]}
+ />
+ )}
+
+
+
+ void refreshVersionData()}>
+ 刷新版本
+
+
+ 打开最新版本
+
+
+ }
+ >
+
+
+
+ 版本总数
+ {versionRows.length}
+
+
+ 最新版本
+ {latestVersionLabel}
+
+
+ 当前差异
+ {versionDiff?.summary.change_count ?? 0} 处
+
+
+ 原始版本
+ {formatDateTime(baseVersionCreatedAt ?? detail?.generated_at)}
+
+
+
+
+
+ →
+
+ void handleCompareVersions()}>
+ 对比版本
+
+
+
+ {versionDiff ? (
+
+
+ {versionDiff.from_version.version_label} → {versionDiff.to_version.version_label}
+ 新增 {versionDiff.summary.added}
+ 删除 {versionDiff.summary.removed}
+ 修改 {versionDiff.summary.modified}
+ 未变 {versionDiff.summary.unchanged}
+
+ {versionDiff.items.length ? (
+
+ {versionDiff.items.map((item, index) => (
+
+
+ {diffChangeTypeLabelMap[item.change_type] ?? item.change_type}
+
+ {item.old_text ? {item.old_text}
: null}
+ {item.new_text ? {item.new_text}
: null}
+
+ ))}
+
+ ) : (
+
当前两个版本没有段落级变化。
+ )}
+
+ ) : (
+
+ )}
+
+ (
+
+ {record.version_label}
+ {record.is_base ? 原始版本 : null}
+
+ ),
+ },
+ {
+ title: "变更类型",
+ dataIndex: "change_type",
+ key: "change_type",
+ width: 130,
+ render: (value: string) => (
+
+ {formatContractVersionChangeType(value)}
+
+ ),
+ },
+ {
+ title: "变更说明",
+ dataIndex: "change_summary",
+ key: "change_summary",
+ render: (value: string | null | undefined) => value || "-",
+ },
+ {
+ title: "保存时间",
+ dataIndex: "created_at",
+ key: "created_at",
+ width: 180,
+ render: (value: string | null | undefined) => formatDateTime(value),
+ },
+ {
+ title: "操作",
+ key: "actions",
+ width: 220,
+ render: (_value: unknown, record: ContractVersionRow) => (
+
+ {!record.is_base ? (
+ void handleCompareVersionWithBase(record.id)}
+ >
+ 和 v1 对比
+
+ ) : null}
+ {!record.is_base ? (
+ handleOpenVersionInEditor(record)}
+ >
+ 打开编辑器
+
+ ) : null}
+ void handleDownloadContractVersion(record)}
+ >
+ 下载
+
+
+ ),
+ },
+ ]}
+ />
+
+
+
{detail.export_format.toUpperCase()}
{detail.file_name}
{detail.metadata.generated_by_name}
- {formatDateTime(detail.generated_at)}
+ {formatDateTime(detail.generated_at)}
{formatLifecycleStatus(detail.lifecycle?.status)}
@@ -906,10 +1530,10 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
{autoReview ? (
-
+
{autoReview.review_id ?? "-"}
-
+
{formatAutoReviewStatus(autoReview.status)}
{autoReview.risk_level ?? "-"}
@@ -930,16 +1554,16 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
handleOpenAutoReviewDetail(autoReview)}>
- 查看审查详情
+ 查看预审详情
router.push(autoReview.chat_url ?? buildContractChatHref(detail, "请基于当前合同的自动审查结果继续分析风险、修改建议和谈判策略。", "继续审查问答"))}
+ onClick={() => router.push(autoReview.chat_url ?? buildContractChatHref(detail, "请基于当前合同的自动预审结果继续分析风险、修改建议和谈判策略。", "继续预审问答"))}
>
- 继续审查问答
+ 继续预审问答
void handleDownloadAutoReviewReport(autoReview)}>
- 下载审查报告
+ 下载预审报告
@@ -987,7 +1611,7 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
)}
-
+
{generationModeMeta.label}
@@ -999,12 +1623,12 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
) : null}
-
+
{generationSource?.generation_mode || detail?.generation_quality?.generation_mode || "template_render"}
@@ -1057,7 +1681,7 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
) : (
- 当前生成结果未返回结构化法条命中,后续可继续补强法条引用展示。
+ 当前合同未返回结构化法条命中,后续可继续补强法条引用展示。
)}
@@ -1072,14 +1696,36 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
)}
-
+
{detail ? (
{detail.generated_content}
) : (
-
+
)}
+ void handleSubmitApproval()}
+ onCancel={() => setApprovalModalOpen(false)}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
预审待触发;
+ }
+ return (
+ <>
+
+ 预审{formatAutoReviewStatus(autoReview.status)}
+
+ {autoReview.risk_level ? (
+
+ {formatRisk(autoReview.risk_level)}
+
+ ) : null}
+ {autoReview.report_download_url ? 报告可下载 : null}
+ >
+ );
+}
+
export default function ContractsPage() {
const [messageApi, contextHolder] = message.useMessage();
const router = useRouter();
@@ -225,7 +256,8 @@ export default function ContractsPage() {
}, [messageApi]);
useEffect(() => {
- void loadContracts();
+ const timer = window.setTimeout(() => void loadContracts(), 0);
+ return () => window.clearTimeout(timer);
}, [loadContracts]);
const restorePayload = useMemo(
@@ -240,8 +272,8 @@ export default function ContractsPage() {
if (restorePayload) {
return {
mode: "restore",
- title: "继续上次生成",
- description: "已自动带回模板、变量与导出格式,你可以继续预览、调整并再次生成。",
+ title: "继续上次起草",
+ description: "已自动带回模板、关键内容与导出格式,你可以继续预览、调整并再次生成。",
templateId: restorePayload.template_id ?? null,
recommendedAction: "优先确认已恢复的变量内容,再直接预览合同。",
};
@@ -249,8 +281,8 @@ export default function ContractsPage() {
if (reviewPayload) {
return {
mode: "review",
- title: "根据审查结果准备生成",
- description: reviewPayload.reviewOpinion ?? reviewPayload.recommendation ?? "已带入审查上下文,可选择模板后继续生成。",
+ title: "根据审查结果准备起草",
+ description: reviewPayload.reviewOpinion ?? reviewPayload.recommendation ?? "已带入审查上下文,可选择模板后继续起草。",
contractType: reviewPayload.contractType ?? null,
recommendedAction: "先选择带“审查建议匹配”标记的模板,再补全变量进入预览。",
};
@@ -260,26 +292,32 @@ export default function ContractsPage() {
useEffect(() => {
if (!generationEntryContext) return;
- setGenDrawerOpen(true);
- void loadTemplates(
- generationEntryContext.contractType ? { contract_type: generationEntryContext.contractType } : {},
- );
+ const timer = window.setTimeout(() => {
+ setGenDrawerOpen(true);
+ void loadTemplates(
+ generationEntryContext.contractType ? { contract_type: generationEntryContext.contractType } : {},
+ );
+ }, 0);
+ return () => window.clearTimeout(timer);
}, [generationEntryContext, loadTemplates]);
useEffect(() => {
if (!restorePayload || !templates.length) return;
const matchedTemplate = templates.find((item) => item.id === restorePayload.template_id) ?? null;
if (!matchedTemplate) return;
- setSelectedTemplate(matchedTemplate);
- setCurrentStep(1);
- form.setFieldsValue({
- template_id: matchedTemplate.id,
- export_format: restorePayload.export_format ?? "docx",
- variable_values: {
- ...buildTemplateDefaultValues(matchedTemplate),
- ...(restorePayload.variable_values ?? {}),
- },
- });
+ const timer = window.setTimeout(() => {
+ setSelectedTemplate(matchedTemplate);
+ setCurrentStep(1);
+ form.setFieldsValue({
+ template_id: matchedTemplate.id,
+ export_format: restorePayload.export_format ?? "docx",
+ variable_values: {
+ ...buildTemplateDefaultValues(matchedTemplate),
+ ...(restorePayload.variable_values ?? {}),
+ },
+ });
+ }, 0);
+ return () => window.clearTimeout(timer);
}, [form, restorePayload, templates]);
const filteredItems = useMemo(() => {
@@ -438,7 +476,7 @@ export default function ContractsPage() {
const handleOpenReviewDetail = (autoReview?: AutoReviewSummary | null) => {
if (!autoReview?.detail_url) {
- messageApi.warning("自动审查详情不可用");
+ messageApi.warning("自动预审详情不可用");
return;
}
router.push(autoReview.detail_url);
@@ -446,7 +484,7 @@ export default function ContractsPage() {
const handleOpenReviewChat = (autoReview?: AutoReviewSummary | null) => {
if (!autoReview?.chat_url) {
- messageApi.warning("审查问答上下文尚未生成");
+ messageApi.warning("预审问答上下文尚未生成");
return;
}
router.push(autoReview.chat_url);
@@ -552,7 +590,7 @@ export default function ContractsPage() {
}
setGeneratedContract(result.data);
setCurrentStep(3);
- messageApi.success("合同生成成功!");
+ messageApi.success("合同已生成,系统会继续同步自动预审状态。");
await loadContracts();
} catch (error: unknown) {
if (Array.isArray((error as { errorFields?: unknown[] }).errorFields)) {
@@ -624,7 +662,7 @@ export default function ContractsPage() {
上传已有合同审查
} block onClick={() => void openGenerationDrawer()}>
- 生成新合同
+ 新建合同
@@ -649,7 +687,7 @@ export default function ContractsPage() {
上传已有合同审查
} onClick={() => void openGenerationDrawer()}>
- 生成新合同
+ 新建合同
@@ -677,12 +715,16 @@ export default function ContractsPage() {
)}
{contract.export_format.toUpperCase()}
{(contract.file_size / 1024).toFixed(1)} KB
+ {renderAutoReviewTags(contract.auto_review)}
- 生成时间:{formatDateTime(contract.generated_at)}
+ 起草时间:{formatDateTime(contract.generated_at)}
{" · "}
生成人员:{contract.metadata?.generated_by_name || "未知"}
+
+ 预审提示:{getAutoReviewNextStep(contract.auto_review)}
+
{(selectedContract.file_size / 1024).toFixed(1)} KB
- 生成时间:{formatDateTime(selectedContract.generated_at)}
+ 起草时间:{formatDateTime(selectedContract.generated_at)}
生成人员:{selectedContract.metadata?.generated_by_name || "未知"}
- {/* 自动审查结果 */}
+ {/* 自动预审结果 */}
{selectedContract.auto_review && (
- 自动审查结果
+ 自动预审结果
- {formatAutoReviewStatus(selectedContract.auto_review.status)}
+ 预审{formatAutoReviewStatus(selectedContract.auto_review.status)}
{selectedContract.auto_review.risk_level && (
@@ -778,10 +820,10 @@ export default function ContractsPage() {
{selectedContract.auto_review.review_opinion || selectedContract.auto_review.status_detail || selectedContract.auto_review.current_stage_label || "-"}
handleOpenReviewDetail(selectedContract.auto_review)} disabled={!selectedContract.auto_review.detail_url}>
- 审查详情
+ 查看预审详情
handleOpenReviewChat(selectedContract.auto_review)} disabled={!selectedContract.auto_review.chat_url}>
- 审查问答
+ 继续问答
handleDownloadReviewReport(selectedContract.auto_review)} disabled={!selectedContract.auto_review.report_download_url}>
下载报告
@@ -836,11 +878,11 @@ export default function ContractsPage() {
)}
- {/* 生成质量 */}
+ {/* 起草质量 */}
{selectedContract.generation_quality?.status && (
- 生成质量
+ 起草质量
{formatGenerationQualityStatus(selectedContract.generation_quality.status)}
@@ -876,9 +918,9 @@ export default function ContractsPage() {
- {/* 生成新合同 Drawer */}
+ {/* 新建合同 Drawer */}
{currentStep === 0 && (
- 适合复用成熟模板快速生成:先选模板,再填写变量,预览确认后直接生成合同文件。
+ 选择适合的合同模板,填写关键内容后即可预览、生成,并衔接自动预审。
{templates.length === 0 ? (
) : (
@@ -976,22 +1018,31 @@ export default function ContractsPage() {
setCurrentStep(1)} style={{ marginRight: 8 }}>返回修改
- 确认生成
+ 生成并启动预审
)}
{currentStep >= 3 && generatedContract && (
-
+
合同ID:{generatedContract.id}
模板:{generatedContract.template_name}
文件名:{generatedContract.file_name}
- 格式:{generatedContract.export_format.toUpperCase()}
+ 格式:{generatedContract.export_format.toUpperCase()}
+
+ {renderAutoReviewTags(generatedContract.auto_review)}
+
- 生成新合同
+ 继续新建合同
router.push(`/contracts/${generatedContract.id}`)}>查看详情
} onClick={handleDownloadGenerated}>下载
diff --git a/frontend/src/app/documents/[id]/edit/page.tsx b/frontend/src/app/documents/[id]/edit/page.tsx
index be52a2a3..b885f223 100644
--- a/frontend/src/app/documents/[id]/edit/page.tsx
+++ b/frontend/src/app/documents/[id]/edit/page.tsx
@@ -145,7 +145,19 @@ function readableRecommendationKind(kind?: string | null) {
return recommendationKindLabelMap[kind ?? ""] ?? "条款建议";
}
-export default function GeneratedContractEditorPage({ params }: { params: Promise<{ id: string }> }) {
+function resolveInitialVersionId(searchParams: URLSearchParams) {
+ const raw = searchParams.get("version") ?? searchParams.get("version_id") ?? searchParams.get("versionId");
+ const normalized = raw?.trim();
+ return normalized ? normalized : null;
+}
+
+export default function GeneratedContractEditorPage({
+ params,
+ searchParams,
+}: {
+ params: Promise<{ id: string }>;
+ searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
+}) {
const [messageApi, contextHolder] = message.useMessage();
const router = useRouter();
const editorRef = useRef
(null);
@@ -172,6 +184,8 @@ export default function GeneratedContractEditorPage({ params }: { params: Promis
const [diffLoading, setDiffLoading] = useState(false);
const [exportingVersion, setExportingVersion] = useState(false);
const [loadError, setLoadError] = useState(null);
+ const [currentVersionId, setCurrentVersionId] = useState(null);
+ const initialVersionIdRef = useRef(null);
const paragraphs = useMemo(() => splitParagraphs(editorContent), [editorContent]);
const selectedParagraph = useMemo(
@@ -207,48 +221,78 @@ export default function GeneratedContractEditorPage({ params }: { params: Promis
}
}, [messageApi]);
- const loadPage = useCallback(async () => {
- setLoading(true);
- setLoadError(null);
- try {
- const resolved = await params;
- const id = resolved.id;
- setContractId(id);
- const [detailResponse, versionsResponse] = await Promise.all([
- getGeneratedContract(id),
- listGeneratedContractVersions(id),
- ]);
- if (detailResponse.code !== 0) {
- setLoadError(detailResponse.message || "合同加载失败");
- return;
- }
- const versionItems = versionsResponse.code === 0 ? versionsResponse.data.items : [];
- const latest = versionItems[0] ?? null;
- const content = latest?.content ?? detailResponse.data.generated_content;
- setContract(detailResponse.data);
- setVersions(versionItems);
- setBaseVersionCreatedAt(versionsResponse.code === 0 ? versionsResponse.data.base_version.created_at ?? null : null);
- setEditorContent(content);
- persistedContentRef.current = normalizeEditorText(content);
- currentVersionIdRef.current = latest?.id ?? null;
- setSelectedParagraphIndex(null);
- setAutoSaveStatus(latest ? `已加载 ${latest.version_label}` : "已加载 v1");
- void refreshVersionDiff(id, latest?.id);
- } catch (error) {
- setLoadError(getErrorMessage(error, "合同编辑器加载失败"));
- } finally {
- setLoading(false);
- }
- }, [params, refreshVersionDiff]);
-
useEffect(() => {
- void loadPage();
+ let cancelled = false;
+
+ const run = async () => {
+ try {
+ const resolved = await params;
+ const id = resolved.id;
+ if (searchParams) {
+ const resolvedSearchParams = new URLSearchParams();
+ const rawSearchParams = await searchParams;
+ Object.entries(rawSearchParams).forEach(([key, value]) => {
+ if (typeof value === "string") {
+ resolvedSearchParams.set(key, value);
+ } else if (Array.isArray(value) && value.length > 0) {
+ resolvedSearchParams.set(key, value[0] ?? "");
+ }
+ });
+ initialVersionIdRef.current = resolveInitialVersionId(resolvedSearchParams);
+ }
+ const [detailResponse, versionsResponse] = await Promise.all([
+ getGeneratedContract(id),
+ listGeneratedContractVersions(id),
+ ]);
+ if (cancelled) {
+ return;
+ }
+ if (detailResponse.code !== 0) {
+ setLoadError(detailResponse.message || "合同加载失败");
+ setLoading(false);
+ return;
+ }
+ const versionItems = versionsResponse.code === 0 ? versionsResponse.data.items : [];
+ const latest = versionItems[0] ?? null;
+ setContractId(id);
+ setContract(detailResponse.data);
+ setVersions(versionItems);
+ setBaseVersionCreatedAt(versionsResponse.code === 0 ? versionsResponse.data.base_version.created_at ?? null : null);
+ const requestedVersionId = initialVersionIdRef.current;
+ const requestedVersion = requestedVersionId && requestedVersionId !== "base"
+ ? versionItems.find((item) => item.id === requestedVersionId) ?? null
+ : null;
+ const useBaseVersion = requestedVersionId === "base";
+ const initialVersion = useBaseVersion ? null : (requestedVersion ?? latest);
+ const initialContent = useBaseVersion
+ ? detailResponse.data.generated_content
+ : initialVersion?.content ?? detailResponse.data.generated_content;
+ setEditorContent(initialContent);
+ persistedContentRef.current = normalizeEditorText(initialContent);
+ currentVersionIdRef.current = useBaseVersion ? null : initialVersion?.id ?? null;
+ setCurrentVersionId(useBaseVersion ? null : initialVersion?.id ?? null);
+ setSelectedParagraphIndex(null);
+ setAutoSaveStatus(useBaseVersion ? "已加载 v1" : (initialVersion ? `已加载 ${initialVersion.version_label}` : "已加载 v1"));
+ void refreshVersionDiff(id, useBaseVersion ? "base" : (initialVersion?.id ?? "base"));
+ } catch (error) {
+ if (!cancelled) {
+ setLoadError(getErrorMessage(error, "合同编辑器加载失败"));
+ }
+ } finally {
+ if (!cancelled) {
+ setLoading(false);
+ }
+ }
+ };
+
+ void run();
return () => {
+ cancelled = true;
if (saveTimerRef.current) {
window.clearTimeout(saveTimerRef.current);
}
};
- }, [loadPage]);
+ }, [params, refreshVersionDiff, searchParams]);
useEffect(() => {
const node = editorRef.current;
@@ -291,7 +335,7 @@ export default function GeneratedContractEditorPage({ params }: { params: Promis
content: normalized,
change_type: options.changeType ?? "editor_save",
change_summary: options.changeSummary ?? "编辑器保存合同正文",
- source_version_id: currentVersionIdRef.current,
+ source_version_id: currentVersionId,
original_text: options.originalText ?? null,
replacement_text: options.replacementText ?? null,
locate_status: "applied",
@@ -305,6 +349,7 @@ export default function GeneratedContractEditorPage({ params }: { params: Promis
}
persistedContentRef.current = normalized;
currentVersionIdRef.current = response.data.id;
+ setCurrentVersionId(response.data.id);
setAutoSaveStatus(`已保存 ${response.data.version_label}`);
await refreshVersions(contractId);
await refreshVersionDiff(contractId, response.data.id);
@@ -325,7 +370,7 @@ export default function GeneratedContractEditorPage({ params }: { params: Promis
} finally {
setSaving(false);
}
- }, [contractId, messageApi, refreshVersionDiff, refreshVersions]);
+ }, [contractId, currentVersionId, messageApi, refreshVersionDiff, refreshVersions]);
const scheduleAutoSave = useCallback((nextContent: string, summary = "自动保存编辑内容") => {
pendingChangeSummaryRef.current = summary;
@@ -353,6 +398,7 @@ export default function GeneratedContractEditorPage({ params }: { params: Promis
setEditorContent(nextContent);
persistedContentRef.current = normalizeEditorText(nextContent);
currentVersionIdRef.current = version?.id ?? null;
+ setCurrentVersionId(version?.id ?? null);
setAutoSaveStatus(version ? `已切换 ${version.version_label}` : "已切换 v1");
setSelectedParagraphIndex(null);
setRewriteResult(null);
@@ -365,9 +411,9 @@ export default function GeneratedContractEditorPage({ params }: { params: Promis
if (!contractId || !contract) {
return;
}
- let versionId = currentVersionIdRef.current ?? "base";
- let versionLabel = currentVersionIdRef.current
- ? versions.find((item) => item.id === currentVersionIdRef.current)?.version_label ?? "current"
+ let versionId = currentVersionId ?? "base";
+ let versionLabel = currentVersionId
+ ? versions.find((item) => item.id === currentVersionId)?.version_label ?? "current"
: "v1";
setExportingVersion(true);
try {
@@ -604,7 +650,7 @@ export default function GeneratedContractEditorPage({ params }: { params: Promis
在正文中继续编辑,把推荐条款和优化条款真正应用到合同版本链。
{formatContractType(contract?.document_type_code ?? contract?.contract_type)}
- {currentVersionIdRef.current ? "版本链编辑" : "基础版本"}
+ {currentVersionId ? "版本链编辑" : "基础版本"}
{autoSaveStatus}
@@ -675,7 +721,7 @@ export default function GeneratedContractEditorPage({ params }: { params: Promis
loading={diffLoading}
onClick={() => {
if (contractId) {
- void refreshVersionDiff(contractId, currentVersionIdRef.current);
+ void refreshVersionDiff(contractId, currentVersionId ?? "base");
}
}}
>
@@ -850,7 +896,7 @@ export default function GeneratedContractEditorPage({ params }: { params: Promis
({
- color: version.id === currentVersionIdRef.current ? "green" : "blue",
+ color: version.id === currentVersionId ? "green" : "blue",
children: (
handleSelectVersion(version)}>
{version.version_label}
@@ -860,7 +906,7 @@ export default function GeneratedContractEditorPage({ params }: { params: Promis
),
})),
{
- color: currentVersionIdRef.current ? "gray" : "green",
+ color: currentVersionId ? "gray" : "green",
children: (
handleSelectVersion(null)}>
v1
diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css
index a3e5c413..1bf72ec9 100644
--- a/frontend/src/app/globals.css
+++ b/frontend/src/app/globals.css
@@ -1628,6 +1628,125 @@ a {
box-shadow: 0 18px 36px rgba(15, 23, 42, 0.08);
}
+.contract-ledger-summary {
+ margin-top: 0;
+}
+
+.contract-ledger-toolbar,
+.contract-ledger-list-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 16px;
+}
+
+.contract-ledger-title {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ min-width: 0;
+}
+
+.contract-ledger-title h1 {
+ margin: 0;
+ color: #0f172a;
+ font-size: 32px;
+ line-height: 1.2;
+}
+
+.contract-ledger-title p {
+ margin: 0;
+ color: #64748b;
+ line-height: 1.7;
+}
+
+.contract-ledger-actions {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ gap: 10px;
+}
+
+.contract-ledger-metric-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+ gap: 12px;
+ margin-top: 18px;
+}
+
+.contract-ledger-metric {
+ display: grid;
+ gap: 8px;
+ min-height: 92px;
+ padding: 16px;
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.9);
+ border: 1px solid rgba(148, 163, 184, 0.2);
+}
+
+.contract-ledger-metric span {
+ color: #64748b;
+ font-size: 13px;
+ font-weight: 800;
+}
+
+.contract-ledger-metric strong {
+ color: #0f172a;
+ font-size: 30px;
+ line-height: 1;
+}
+
+.contract-ledger-metric-success {
+ border-color: rgba(22, 163, 74, 0.24);
+ background: rgba(240, 253, 244, 0.92);
+}
+
+.contract-ledger-metric-warning {
+ border-color: rgba(217, 119, 6, 0.24);
+ background: rgba(255, 251, 235, 0.92);
+}
+
+.contract-ledger-metric-danger {
+ border-color: rgba(220, 38, 38, 0.22);
+ background: rgba(254, 242, 242, 0.92);
+}
+
+.contract-ledger-priority-card {
+ min-height: 190px;
+}
+
+.contract-ledger-search {
+ width: min(360px, 100%);
+}
+
+.contract-ledger-table-shell {
+ overflow: hidden;
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.92);
+ border: 1px solid rgba(148, 163, 184, 0.2);
+}
+
+@media (max-width: 768px) {
+ .contract-ledger-toolbar,
+ .contract-ledger-list-header {
+ flex-direction: column;
+ }
+
+ .contract-ledger-actions,
+ .contract-ledger-actions a,
+ .contract-ledger-actions .ant-btn {
+ width: 100%;
+ }
+
+ .contract-ledger-actions {
+ justify-content: stretch;
+ }
+
+ .contract-ledger-search {
+ width: 100%;
+ }
+}
+
.review-page-grid,
.review-detail-page,
.review-history-page {
@@ -6748,6 +6867,26 @@ textarea.ant-input:focus,
background: #f6ffed;
}
+.contract-version-compare-bar {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 10px;
+ padding: 12px;
+ border: 1px solid rgba(61, 95, 129, 0.12);
+ border-radius: 8px;
+ background: #fbfdff;
+}
+
+.contract-version-compare-arrow {
+ color: #6d7f91;
+ font-size: 16px;
+}
+
+.contract-version-diff-list {
+ max-height: 420px;
+}
+
.document-editor-recommend-card > div {
display: flex;
justify-content: space-between;
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index 6eb628c3..81dd6790 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -60,8 +60,8 @@ const appTheme: ThemeConfig = {
};
export const metadata: Metadata = {
- title: "AI合同平台",
- description: "面向合同审查、生成、治理和交付的 AI 法务工作台",
+ title: "AI合同起草与审查平台",
+ description: "面向合同起草、预审、审查、治理和交付的 AI 法务平台",
};
export default function RootLayout({
diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx
index 0e39b9ab..308642f6 100644
--- a/frontend/src/app/login/page.tsx
+++ b/frontend/src/app/login/page.tsx
@@ -180,7 +180,7 @@ function LoginPageContent() {
Secure Access
-
先登录,再进入合同审查工作台。
+
先登录,再进入合同起草与审查平台。
现在已支持新用户注册、自动登录和登录态恢复,优先推荐通过注册链路开始使用系统。
{loginHighlights.map((item) => (
@@ -192,7 +192,7 @@ function LoginPageContent() {
Access Matrix
- 审查工作台已就绪
+ 起草与审查平台已就绪
diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx
index 030b69aa..d2593f1d 100644
--- a/frontend/src/app/page.tsx
+++ b/frontend/src/app/page.tsx
@@ -3,9 +3,9 @@
import Link from "next/link";
const publicShowcaseCards = [
- { label: "Clause Scanner", value: "82 条", tone: "cyan" },
- { label: "Risk Radar", value: "12 项", tone: "coral" },
- { label: "Legal Sources", value: "36 条", tone: "amber" },
+ { label: "起草模板", value: "模板驱动", tone: "cyan" },
+ { label: "自动预审", value: "自动触发", tone: "coral" },
+ { label: "报告交付", value: "一键导出", tone: "amber" },
];
export default function LandingPage() {
@@ -13,9 +13,9 @@ export default function LandingPage() {
-
AI Contract Platform
-
让合同审查像刷仪表盘一样清爽、快速、有判断力。
-
围绕上传、识别、建议、报告导出重组体验,把 AI 审查结果变成可行动的法务工作流。
+
AI 合同起草与审查平台
+
让合同起草、预审和报告交付一次完成。
+
把模板起草、自动预审、风险解释和报告下载收敛成一条清晰主线,方便客户直接上手。
立即注册新账号
@@ -25,30 +25,30 @@ export default function LandingPage() {
- 合同初审
- 风险雷达
- 依据引用
+ 合同起草
+ 自动预审
+ 风险解释
报告交付
- Live Review Console
- V1 Pilot
+ Contract Flow
+ Phase 1
-
保密协议 · NDA
-
自动定位高风险条款
+
标准服务合同 · 起草中
+
自动补全关键条款
- High Risk
- 03
+ Auto Review
+ ON
diff --git a/frontend/src/app/reviews/[id]/page.tsx b/frontend/src/app/reviews/[id]/page.tsx
index 3da4a910..5cebef31 100644
--- a/frontend/src/app/reviews/[id]/page.tsx
+++ b/frontend/src/app/reviews/[id]/page.tsx
@@ -483,11 +483,11 @@ export default function ReviewDetailPage() {
const detailAnchors = [
{ href: "#review-risk-overview", label: "风险总览" },
- { href: "#review-task-info", label: "任务信息" },
- { href: "#review-document-parse", label: "解析证据" },
+ { href: "#review-business-actions", label: "处理摘要" },
+ { href: "#review-risk-items", label: "风险项" },
{ href: "#review-structured-workbench", label: "结构化报告" },
{ href: "#review-contract-structure", label: "合同摘要" },
- { href: "#review-risk-items", label: "风险项" },
+ { href: "#review-task-info", label: "任务信息" },
{ href: "#review-clause-summary", label: "条款摘要" },
];
@@ -515,13 +515,19 @@ export default function ReviewDetailPage() {
];
const processInfoItems = [
- { label: "任务编号", value: detail?.id ?? "-" },
{ label: "风险等级", value: overallRisk },
{ label: "当前阶段", value: detail?.current_stage_label ?? "-" },
+ { label: "报告状态", value: report ? "可下载" : formatLifecycleStatusGeneric(detail?.report_status ?? workflowState?.report_status) },
+ { label: "完成时间", value: formatDateTime(detail?.completed_at) },
+ ];
+ const technicalProcessInfoItems = [
+ { label: "任务编号", value: detail?.id ?? "-" },
{ label: "原始文本长度", value: formatSourceTextLength(detail?.source_text_length) },
{ label: "排队时间", value: formatDateTime(detail?.queued_at) },
{ label: "开始处理", value: formatDateTime(detail?.processing_started_at) },
- { label: "完成时间", value: formatDateTime(detail?.completed_at) },
+ { label: "文本解析", value: formatLifecycleStatusGeneric(detail?.parse_status) },
+ { label: "规则审查", value: formatLifecycleStatusGeneric(detail?.review_status) },
+ { label: "报告阶段", value: formatLifecycleStatusGeneric(detail?.report_status) },
];
const preferredNextAction = detail?.can_retry
@@ -533,6 +539,52 @@ export default function ReviewDetailPage() {
{ label: "处理建议", value: recommendation },
{ label: "主链下一步", value: preferredNextAction },
];
+ const mustFixItems = [
+ ...riskItems
+ .filter((item) => item.level === "critical" || item.level === "high")
+ .map((item, index) => ({
+ key: `risk-${index}`,
+ label: formatRisk(item.level),
+ title: item.title ?? item.description ?? `高风险事项${index + 1}`,
+ description: item.suggestion ?? item.description ?? "建议优先处理该风险。",
+ })),
+ ...priorityActions
+ .filter((item) => item.priority === "critical" || item.priority === "high")
+ .map((item, index) => ({
+ key: `priority-${index}`,
+ label: item.priority ?? "高优先级",
+ title: item.title ?? `优先动作${index + 1}`,
+ description: item.suggestion ?? "请结合法务意见优先处理。",
+ })),
+ ...structuredMissingClauses
+ .filter((item) => item.importance === "high")
+ .map((item, index) => ({
+ key: `missing-${index}`,
+ label: "缺失条款",
+ title: item.name ?? `待补条款${index + 1}`,
+ description: item.reason ?? "建议补充该条款以降低争议。",
+ })),
+ ].slice(0, 6);
+ const negotiableItems = [
+ ...riskItems
+ .filter((item) => item.level === "medium")
+ .map((item, index) => ({
+ key: `medium-${index}`,
+ title: item.title ?? item.description ?? `可协商事项${index + 1}`,
+ description: item.suggestion ?? item.description ?? "可结合商业立场与对方协商。",
+ })),
+ ...negotiationTips.map((item, index) => ({
+ key: `tip-${index}`,
+ title: `谈判建议${index + 1}`,
+ description: item,
+ })),
+ ].slice(0, 6);
+ const reportDeliveryItems = [
+ { label: "报告状态", value: report ? "可下载" : "生成中" },
+ { label: "文件名称", value: report?.file_name ?? "-" },
+ { label: "报告阶段", value: formatLifecycleStatusGeneric(detail?.report_status ?? workflowState?.report_status) },
+ { label: "下一步", value: preferredNextAction },
+ ];
const structureSectionItems = [
{ key: "payment_terms", label: "付款安排", items: contractStructure?.payment_terms ?? [] },
{ key: "acceptance_terms", label: "验收安排", items: contractStructure?.acceptance_terms ?? [] },
@@ -681,7 +733,7 @@ export default function ReviewDetailPage() {
}
return [
- { label: "用审查结果准备合同生成", href: buildReviewContractHref(detail, result), primary: true },
+ { label: "用审查结果起草合同", href: buildReviewContractHref(detail, result), primary: true },
currentUser?.role === "admin" && report?.file_id ? { label: "查看报告文件资产", href: `/files?highlight=${report.file_id}` } : null,
currentUser?.role === "admin" ? { label: "查看知识库管理", href: "/knowledge-bases" } : null,
currentUser?.role === "admin" ? { label: "查看文件资产中心", href: "/files" } : null,
@@ -819,6 +871,73 @@ export default function ReviewDetailPage() {
+
+
+
+
+ Must Fix
+ 必改事项
+ {mustFixItems.length ? (
+
+ {mustFixItems.map((item) => (
+
+
{item.label}
+
{item.title}
+
{item.description}
+
+ ))}
+
+ ) : (
+ 当前没有识别到必须立即修改的高风险事项。
+ )}
+
+
+
+
+ Negotiable
+ 可协商事项
+ {negotiableItems.length ? (
+
+ {negotiableItems.map((item) => (
+
+
{item.title}
+
{item.description}
+
+ ))}
+
+ ) : (
+ 当前没有单独列出的可协商事项。
+ )}
+
+
+
+
+ Delivery
+ 报告交付
+
+ {reportDeliveryItems.map((item) => (
+
+ {item.value}
+
+ ))}
+
+
+ {report ? (
+ void handleDownloadReport()}>
+ 下载审查报告
+
+ ) : null}
+ {detail?.can_retry ? (
+ void handleRetryReview()}>
+ 重新发起审查
+
+ ) : null}
+
+
+
+
+
+
{reviewExceptionActive ? (
Workbench
下一步动作与结果回流
- 优先把当前审查结果带入合同生成;报告下载和回到列表作为次级动作保留。
+ 优先把当前审查结果带入合同起草;报告下载和回到列表作为补充动作保留。
{report ? (
@@ -985,7 +1104,7 @@ export default function ReviewDetailPage() {
-
+
Business Context
@@ -1000,8 +1119,8 @@ export default function ReviewDetailPage() {
- Task Metadata
- 任务与处理信息
+ Review Status
+ 处理状态
{processInfoItems.map((item) => (
@@ -1032,6 +1151,15 @@ export default function ReviewDetailPage() {
setShowReviewTechDetails(prev => !prev)}>
{showReviewTechDetails ? "收起技术详情 ▾" : "展开技术详情 ▸"}
+ {showReviewTechDetails ? (
+
+ {technicalProcessInfoItems.map((item) => (
+
+ {item.value}
+
+ ))}
+
+ ) : null}
{showReviewTechDetails && documentAnalysis ? (
OCR Quality Gate
@@ -1124,12 +1252,7 @@ export default function ReviewDetailPage() {
) : null}
-
- {formatLifecycleStatusGeneric(detail?.parse_status)}
- {formatLifecycleStatusGeneric(detail?.review_status)}
- {formatLifecycleStatusGeneric(detail?.report_status)}
-
- {detail?.error_message ? 错误信息:{detail.error_message} : null}
+ {showReviewTechDetails && detail?.error_message ? 错误信息:{detail.error_message} : null}
diff --git a/frontend/src/app/templates/page.tsx b/frontend/src/app/templates/page.tsx
index beff9e1e..ec36e64e 100644
--- a/frontend/src/app/templates/page.tsx
+++ b/frontend/src/app/templates/page.tsx
@@ -124,6 +124,21 @@ function getInitialVariables(template?: TemplateItem | null): TemplateVariableFo
return [{}];
}
+function buildTemplateScenario(template: TemplateItem | null) {
+ if (!template) return "-";
+ const contractType = formatContractType(template.contract_type);
+ const category = formatTemplateCategory(template.template_category);
+ const industry = formatIndustryCategory(template.industry_category);
+ return `${industry} · ${category} · ${contractType}`;
+}
+
+function buildTemplateReadiness(template: TemplateItem | null) {
+ if (!template) return "未选择模板";
+ if (!template.enabled) return "已停用,起草入口不可直接使用";
+ if (!template.published_at) return "待发布,建议确认后发布到可用模板集";
+ return "已发布,可作为合同起草模板使用";
+}
+
const ALL_KEY = "__all__";
export default function TemplatesPage() {
@@ -199,11 +214,14 @@ export default function TemplatesPage() {
}, [loadTemplates]);
useEffect(() => {
- if (selectedId) {
- void loadVersions(selectedId);
- } else {
- setVersionHistory([]);
- }
+ const timer = window.setTimeout(() => {
+ if (selectedId) {
+ void loadVersions(selectedId);
+ } else {
+ setVersionHistory([]);
+ }
+ }, 0);
+ return () => window.clearTimeout(timer);
}, [selectedId, loadVersions]);
useEffect(() => {
@@ -627,6 +645,25 @@ export default function TemplatesPage() {
)}
+
+
+ 适用场景
+ {buildTemplateScenario(selectedTemplate)}
+
+
+ 发布状态
+ {buildTemplateReadiness(selectedTemplate)}
+
+
+ 变量配置
+ {selectedTemplate.variables.length} 个变量
+
+
+ 最近版本
+ {selectedTemplate.current_version_label} · {formatDateTime(selectedTemplate.latest_version_created_at ?? selectedTemplate.updated_at)}
+
+
+
{/* 正文和变量并排 */}
{/* 模板正文 */}
@@ -935,10 +972,51 @@ export default function TemplatesPage() {
margin-bottom: 16px;
}
+ .template-readiness-grid {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 12px;
+ margin-bottom: 16px;
+ }
+
+ .template-readiness-card {
+ background: #fff;
+ border: 1px solid rgba(30, 41, 59, 0.08);
+ border-radius: 8px;
+ padding: 14px 16px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
+ min-width: 0;
+ }
+
+ .template-readiness-card span {
+ display: block;
+ color: #64748b;
+ font-size: 12px;
+ margin-bottom: 6px;
+ }
+
+ .template-readiness-card strong {
+ display: block;
+ color: #0f172a;
+ font-size: 14px;
+ line-height: 1.45;
+ word-break: break-word;
+ }
+
@media (max-width: 1024px) {
.template-detail-body {
grid-template-columns: 1fr;
}
+
+ .template-readiness-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+ }
+
+ @media (max-width: 640px) {
+ .template-readiness-grid {
+ grid-template-columns: 1fr;
+ }
}
.template-detail-section {
diff --git a/frontend/src/app/workspace/page.tsx b/frontend/src/app/workspace/page.tsx
index 9615a65b..bffc514b 100644
--- a/frontend/src/app/workspace/page.tsx
+++ b/frontend/src/app/workspace/page.tsx
@@ -19,7 +19,9 @@ import {
const memberQuickLinks = [
{ href: "/review", label: "发起合同审查", description: "上传合同文件或粘贴文本,立即进入审查主链路。", eyebrow: "Start Review" },
{ href: "/documents/new", label: "文书起草", description: "从文书类型开始逐步填写要素,适合从 0 到 1 起草。", eyebrow: "Document Builder" },
- { href: "/contracts", label: "合同生成中心", description: "复用成熟模板快速生成合同,适合已有模板的高频场景。", eyebrow: "Contract Generation" },
+ { href: "/contracts", label: "合同起草", description: "复用成熟模板快速起草合同,并衔接生成后的自动预审。", eyebrow: "Contract Drafting" },
+ { href: "/contract-ledger", label: "合同台账", description: "集中查看合同生命周期、预审风险、到期和逾期事项。", eyebrow: "Contract Ledger" },
+ { href: "/contract-approvals", label: "合同审批", description: "处理合同提交、通过、退回和签署前确认。", eyebrow: "Contract Approval" },
{ href: "/reviews", label: "查看审查记录", description: "查看任务状态、风险等级,并进入详情页。", eyebrow: "Review History" },
{ href: "/chat", label: "法律问答", description: "查询制度、法条和合同处理建议。", eyebrow: "Legal Chat" },
];
diff --git a/frontend/src/components/layout/app-topbar.tsx b/frontend/src/components/layout/app-topbar.tsx
index 5beb08ef..1eb5a755 100644
--- a/frontend/src/components/layout/app-topbar.tsx
+++ b/frontend/src/components/layout/app-topbar.tsx
@@ -49,10 +49,13 @@ function getPrivateNavItems(role?: string | null) {
if (normalizedRole === "legal") {
return [
...sharedItems,
- { href: "/governance/workbench", label: "复核工作台", icon:
},
+ { href: "/contracts", label: "合同起草", icon:
},
+ { href: "/contract-ledger", label: "合同台账", icon:
},
+ { href: "/contract-approvals", label: "合同审批", icon:
},
{ href: "/review", label: "发起审查", icon:
},
{ href: "/reviews", label: "审查记录", icon:
},
{ href: "/chat", label: "法律问答", icon:
},
+ { href: "/governance/workbench", label: "复核工作台", icon:
},
{ href: "/profile", label: "个人设置", icon:
},
];
}
@@ -60,9 +63,11 @@ function getPrivateNavItems(role?: string | null) {
if (normalizedRole === "business" || normalizedRole === "member") {
return [
...sharedItems,
+ { href: "/contracts", label: "合同起草", icon:
},
+ { href: "/contract-ledger", label: "合同台账", icon:
},
+ { href: "/contract-approvals", label: "合同审批", icon:
},
{ href: "/review", label: "发起审查", icon:
},
{ href: "/documents/new", label: "文书起草", icon:
},
- { href: "/contracts", label: "合同生成", icon:
},
{ href: "/reviews", label: "审查记录", icon:
},
{ href: "/chat", label: "法律问答", icon:
},
{ href: "/profile", label: "个人设置", icon:
},
@@ -71,11 +76,13 @@ function getPrivateNavItems(role?: string | null) {
return [
...sharedItems,
+ { href: "/contracts", label: "合同起草", icon:
},
+ { href: "/contract-ledger", label: "合同台账", icon:
},
+ { href: "/contract-approvals", label: "合同审批", icon:
},
{ href: "/review", label: "发起审查", icon:
},
{ href: "/reviews", label: "审查记录", icon:
},
{ href: "/chat", label: "法律问答", icon:
},
{ href: "/documents/new", label: "文书起草", icon:
},
- { href: "/contracts", label: "合同生成", icon:
},
{ href: "/profile", label: "个人设置", icon:
},
];
}
@@ -150,7 +157,7 @@ export function AppTopbar() {
AI
合同平台
- 合同审查工作台
+ 起草与审查平台
diff --git a/frontend/src/lib/format.ts b/frontend/src/lib/format.ts
index a4ff816f..05ed3ccc 100644
--- a/frontend/src/lib/format.ts
+++ b/frontend/src/lib/format.ts
@@ -60,6 +60,7 @@ export const lifecycleStatusMeta: Record
= {
generated: { label: "已生成", color: "default" },
internal_review: { label: "内部审查", color: "blue" },
external_review: { label: "外部审查", color: "geekblue" },
+ approved: { label: "已审批", color: "purple" },
signed: { label: "已签署", color: "cyan" },
effective: { label: "履约中", color: "green" },
expiring: { label: "即将到期", color: "gold" },
@@ -75,8 +76,9 @@ export const lifecycleStatusOptions = Object.entries(lifecycleStatusMeta).map(
// 生命周期状态机:每个状态允许流转到的下一状态
export const lifecycleTransitions: Record = {
generated: ["internal_review", "archived"],
- internal_review: ["external_review", "signed", "archived"],
- external_review: ["signed", "archived"],
+ internal_review: ["external_review", "approved", "archived"],
+ external_review: ["approved", "archived"],
+ approved: ["signed", "archived"],
signed: ["effective", "terminated", "archived"],
effective: ["expiring", "terminated", "archived"],
expiring: ["effective", "expired", "terminated", "archived"],
@@ -110,6 +112,42 @@ export const dueStatusMeta: Record = {
closed: { label: "已关闭", color: "default" },
};
+// ─── Contract Approval ─────────────────────────────────────────────────
+
+export const contractApprovalStatusMeta: Record = {
+ not_submitted: { label: "未提交", color: "default" },
+ pending: { label: "审批中", color: "gold" },
+ approved: { label: "已通过", color: "green" },
+ rejected: { label: "已退回", color: "red" },
+ cancelled: { label: "已取消", color: "default" },
+};
+
+export const contractApprovalStepTypeMeta: Record = {
+ legal_review: { label: "法务审批", color: "blue" },
+ business_approval: { label: "业务审批", color: "geekblue" },
+ signoff: { label: "签署确认", color: "purple" },
+};
+
+export function formatContractApprovalStatus(value: string | null | undefined): string {
+ if (!value) return "未提交";
+ return contractApprovalStatusMeta[value]?.label ?? value;
+}
+
+export function getContractApprovalStatusColor(value: string | null | undefined): string {
+ if (!value) return "default";
+ return contractApprovalStatusMeta[value]?.color ?? "default";
+}
+
+export function formatContractApprovalStepType(value: string | null | undefined): string {
+ if (!value) return "审批";
+ return contractApprovalStepTypeMeta[value]?.label ?? value;
+}
+
+export function getContractApprovalStepTypeColor(value: string | null | undefined): string {
+ if (!value) return "default";
+ return contractApprovalStepTypeMeta[value]?.color ?? "default";
+}
+
// ─── Lifecycle Tasks ────────────────────────────────────────────────────
export const lifecycleTaskTypeMeta: Record = {
diff --git a/frontend/src/services/contract.ts b/frontend/src/services/contract.ts
index 1f8486e6..a984bc65 100644
--- a/frontend/src/services/contract.ts
+++ b/frontend/src/services/contract.ts
@@ -111,6 +111,7 @@ export interface GeneratedContract {
clause_completion_suggestions?: ClauseCompletionSuggestion[];
auto_review?: AutoReviewSummary | null;
lifecycle: ContractLifecycle;
+ approval?: ContractApprovalSummary | null;
metadata: {
generated_by: string;
generated_by_name: string;
@@ -175,6 +176,7 @@ export interface GeneratedContractListItem {
clause_completion_suggestions?: ClauseCompletionSuggestion[];
auto_review?: AutoReviewSummary | null;
lifecycle: ContractLifecycle;
+ approval?: ContractApprovalSummary | null;
metadata: {
generated_by_name: string;
template_category?: string | null;
@@ -213,6 +215,75 @@ export interface ContractLifecycle {
updated_at?: string | null;
}
+export interface ContractApprovalContractSummary {
+ id: string;
+ template_name: string;
+ contract_type?: string | null;
+ document_type_code?: string | null;
+ file_name?: string | null;
+ generated_by_name?: string | null;
+ lifecycle_status?: string | null;
+ created_at?: string | null;
+}
+
+export interface ContractApprovalStep {
+ id: string;
+ tenant_id?: string;
+ contract_id: string;
+ contract?: ContractApprovalContractSummary | null;
+ step_type: string;
+ status: string;
+ title: string;
+ comment?: string | null;
+ submitter_id?: string | null;
+ submitter_name?: string | null;
+ assignee_id?: string | null;
+ assignee_name?: string | null;
+ decided_by?: string | null;
+ decided_by_name?: string | null;
+ decided_at?: string | null;
+ metadata?: Record;
+ created_at?: string | null;
+ updated_at?: string | null;
+}
+
+export interface ContractApprovalSummary {
+ status: string;
+ latest?: ContractApprovalStep | null;
+ pending?: ContractApprovalStep | null;
+ history?: ContractApprovalStep[];
+ total?: number;
+}
+
+export interface ContractApprovalListResponse {
+ items: ContractApprovalStep[];
+ total: number;
+ page: number;
+ page_size: number;
+ summary: Record;
+}
+
+export interface ContractApprovalHistoryResponse {
+ contract_id: string;
+ items: ContractApprovalStep[];
+ total: number;
+ latest?: ContractApprovalStep | null;
+}
+
+export interface SubmitContractApprovalRequest {
+ contract_id: string;
+ title?: string | null;
+ comment?: string | null;
+ assignee_id?: string | null;
+ assignee_name?: string | null;
+ step_type?: string;
+}
+
+export interface DecideContractApprovalRequest {
+ decision: "approve" | "reject" | string;
+ comment?: string | null;
+}
+
export interface ContractLifecycleTaskEvidenceFile {
id: string;
file_name: string;
@@ -470,6 +541,51 @@ export async function listGeneratedContracts(
return response.data;
}
+export async function listContractApprovals(
+ params: { page?: number; page_size?: number; status_value?: string; scope?: string } = {},
+): Promise> {
+ const searchParams = new URLSearchParams();
+ if (params.page) {
+ searchParams.append("page", String(params.page));
+ }
+ if (params.page_size) {
+ searchParams.append("page_size", String(params.page_size));
+ }
+ if (params.status_value) {
+ searchParams.append("status_value", params.status_value);
+ }
+ if (params.scope) {
+ searchParams.append("scope", params.scope);
+ }
+ const queryString = searchParams.toString();
+ const response = await http.get>(
+ `/contract-approvals${queryString ? `?${queryString}` : ""}`,
+ );
+ return response.data;
+}
+
+export async function listContractApprovalHistory(
+ contract_id: string,
+): Promise> {
+ const response = await http.get>(`/contract-approvals/contracts/${contract_id}`);
+ return response.data;
+}
+
+export async function submitContractApproval(
+ payload: SubmitContractApprovalRequest,
+): Promise> {
+ const response = await http.post>("/contract-approvals", payload);
+ return response.data;
+}
+
+export async function decideContractApproval(
+ approval_id: string,
+ payload: DecideContractApprovalRequest,
+): Promise> {
+ const response = await http.post>(`/contract-approvals/${approval_id}/decision`, payload);
+ return response.data;
+}
+
/**
* 获取已生成合同详情
*/
diff --git a/infra/env/frontend.aliyun.env.example b/infra/env/frontend.aliyun.env.example
index 2a1f538e..191baffa 100644
--- a/infra/env/frontend.aliyun.env.example
+++ b/infra/env/frontend.aliyun.env.example
@@ -1,7 +1,7 @@
FRONTEND_HOST_PORT=3003
NEXT_PUBLIC_API_BASE_URL=/api/v1
BACKEND_BASE_URL=http://10.20.0.11:8001
-FRONTEND_BASE_URL=https://api.yuqei.com
-API_URL=https://api.yuqei.com/api/v1
-READINESS_URL=https://api.yuqei.com/api/v1/system/readiness
+FRONTEND_BASE_URL=https://contact.yuqei.com
+API_URL=https://contact.yuqei.com/api/v1
+READINESS_URL=https://contact.yuqei.com/api/v1/system/readiness
BACKEND_HEALTH_URL=http://192.168.3.90:8001/health
diff --git a/infra/nginx/api.yuqei.com.conf b/infra/nginx/contact.yuqei.com.conf
similarity index 98%
rename from infra/nginx/api.yuqei.com.conf
rename to infra/nginx/contact.yuqei.com.conf
index 8a0af195..5c34503c 100644
--- a/infra/nginx/api.yuqei.com.conf
+++ b/infra/nginx/contact.yuqei.com.conf
@@ -5,7 +5,7 @@ map $http_upgrade $connection_upgrade {
server {
listen 80;
- server_name api.yuqei.com;
+ server_name contact.yuqei.com;
charset utf-8;
client_max_body_size 100m;
diff --git a/project-docs/AI合同起草与审查助手-Phase1主链路验收清单-2026-06-17.md b/project-docs/AI合同起草与审查助手-Phase1主链路验收清单-2026-06-17.md
new file mode 100644
index 00000000..527f5a82
--- /dev/null
+++ b/project-docs/AI合同起草与审查助手-Phase1主链路验收清单-2026-06-17.md
@@ -0,0 +1,131 @@
+# AI合同起草与审查助手 Phase1 主链路验收清单
+
+日期:2026-06-17
+
+关联方案:`project-docs/AI合同起草与审查助手-Phase1实际开发执行方案-2026-06-17.md`
+
+## 1. 验收目标
+
+验证 Phase1 是否真正完成“起草 -> 自动预审 -> 风险处理 -> 报告交付”的业务主链路。
+
+验收口径:
+
+1. 客户能在 3 分钟内看懂产品定位。
+2. 业务用户能完成一次合同起草和自动预审。
+3. 审查详情默认展示业务结论,而不是技术链路。
+4. 模板库能说明适用场景、发布状态、变量配置和版本。
+5. 审查报告能作为试点交付材料下载。
+
+## 2. 验收账号与环境
+
+- 环境:本地开发环境或正式试点环境。
+- 角色:管理员、业务用户各 1 个。
+- 样本:服务合同、采购合同或 NDA 模板至少 1 个。
+- 浏览器:Chrome 最新稳定版。
+
+## 3. 主链路验收步骤
+
+### 步骤 1:首页与登录
+
+1. 打开 `/`。
+2. 确认首屏表达为“合同起草、自动预审、风险解释、报告交付”。
+3. 点击注册或登录。
+4. 登录后进入 `/workspace`。
+
+通过标准:
+
+- 首页不再只讲“合同审查工具”。
+- 顶部导航出现“合同起草”“发起审查”“审查记录”“法律问答”。
+
+### 步骤 2:合同起草
+
+1. 从工作台进入 `/contracts`。
+2. 点击“新建合同”。
+3. 选择一个可用模板。
+4. 填写关键内容。
+5. 预览合同。
+6. 点击“生成并启动预审”。
+
+通过标准:
+
+- 步骤文案为“选择模板 -> 填写关键内容 -> 预览确认 -> 生成并预审”。
+- 生成完成后能看到自动预审状态。
+- 最近合同列表能看到预审提示、风险等级或报告状态。
+
+### 步骤 3:审查详情
+
+1. 进入生成合同的预审详情,或从 `/review` 上传已有合同。
+2. 打开 `/reviews/[id]`。
+3. 查看“试点演示摘要”和“业务处理摘要”。
+4. 检查“必改事项”“可协商事项”“报告交付”。
+5. 展开“技术详情”。
+
+通过标准:
+
+- 默认视图能回答:风险是什么、先改什么、下一步做什么、报告在哪里。
+- OCR、Workflow、AI Trace 等技术字段默认折叠。
+- 展开技术详情后仍能查看排障信息。
+
+### 步骤 4:报告下载
+
+1. 在审查详情页点击“下载审查报告”。
+2. 打开生成的报告。
+3. 检查报告是否包含:
+ - 风险总览
+ - 业务处理摘要
+ - 风险项详情
+ - 条款结构
+ - 法条引用或免责声明
+
+通过标准:
+
+- 报告可下载。
+- 报告内容能作为试点交付材料阅读。
+- 报告明确说明不替代正式法律意见。
+
+### 步骤 5:模板库
+
+1. 使用管理员进入 `/templates`。
+2. 选择一个模板。
+3. 查看模板详情摘要。
+4. 检查模板分类、行业入口、变量数量、版本状态。
+
+通过标准:
+
+- 模板详情能展示适用场景、发布状态、变量配置和最近版本。
+- 模板正文和变量定义仍可查看。
+- 发布、下线、编辑、版本历史仍可用。
+
+## 4. 回归命令
+
+建议执行:
+
+```bash
+npm --prefix frontend run lint
+npm --prefix frontend run build
+python -m pytest backend/tests/test_contracts_api.py backend/tests/test_reviews_api.py backend/tests/test_review_service.py backend/tests/test_review_risk_action_service.py
+git diff --check
+```
+
+如果本地未安装 npm 依赖,可先安装依赖后再执行。
+
+## 5. 人工签收结论
+
+| 验收项 | 结果 | 备注 |
+| --- | --- | --- |
+| 首页定位 | 待验收 | |
+| 合同起草 | 待验收 | |
+| 自动预审 | 待验收 | |
+| 审查详情业务化 | 待验收 | |
+| 报告下载 | 待验收 | |
+| 模板库摘要 | 待验收 | |
+| 技术详情可展开 | 待验收 | |
+
+## 6. Phase1 完成判定
+
+全部通过后,Phase1 可判定为完成:
+
+1. 对外定位已经从单点审查升级为起草与审查平台。
+2. 合同起草链路能衔接自动预审。
+3. 审查详情默认面向业务决策。
+4. 模板库和报告交付具备试点演示能力。
diff --git a/project-docs/AI合同起草与审查助手-Phase1实际开发执行方案-2026-06-17.md b/project-docs/AI合同起草与审查助手-Phase1实际开发执行方案-2026-06-17.md
new file mode 100644
index 00000000..59f75002
--- /dev/null
+++ b/project-docs/AI合同起草与审查助手-Phase1实际开发执行方案-2026-06-17.md
@@ -0,0 +1,463 @@
+# AI合同起草与审查助手 Phase1 实际开发执行方案
+
+日期:2026-06-17
+
+关联总方案:`project-docs/AI合同起草与审查助手-完整规划开发方案-2026-06-17.md`
+
+## 1. 执行结论
+
+Phase1 不建议再扩大底层能力范围,而应先做一次“产品主链路收敛”:
+
+> 把当前已有的合同生成、合同审查、报告下载、模板库能力,重组为客户能在 3 分钟内看懂并完成的起草与预审闭环。
+
+本阶段重点不是新增复杂后台模型,而是:
+
+- 对外定位从“合同审查工作台”升级为“AI 合同起草与审查平台”。
+- 让起草、预审、报告下载成为主导航和主流程。
+- 把审查详情页里偏技术的字段收进高级信息或排障区域。
+- 让模板库、审查报告、自动预审结果更贴近业务用户的表达。
+
+## 2. 本阶段目标
+
+### 2.1 用户目标
+
+业务用户进入系统后,应能完成:
+
+1. 看懂产品能做什么:起草、审查、风险解释、报告交付。
+2. 选择模板并填写关键变量。
+3. 生成合同并自动进入预审。
+4. 查看清晰的风险结论和修改建议。
+5. 下载合同或审查报告。
+
+### 2.2 产品目标
+
+1. 首页价值表达清晰,避免只讲“审查”。
+2. 登录后导航按用户工作流排序。
+3. 合同生成页从“历史树 + 生成抽屉”的后台感,调整为“起草任务 + 最近合同 + 预审状态”的业务工作台感。
+4. 审查详情页默认展示业务判断,技术链路信息降级。
+5. 模板库分类和审查报告成为可售卖能力,而不是内部管理能力。
+
+### 2.3 技术目标
+
+1. 优先复用现有接口和字段,不做数据库迁移。
+2. 前端以页面重组和展示语义优化为主。
+3. 后端只在报告模板或轻量 DTO 字段不足时小范围补齐。
+4. 每批次都能独立回归和上线。
+
+## 3. 当前代码入口
+
+### 3.1 前端页面
+
+| 范围 | 文件 | 说明 |
+| --- | --- | --- |
+| 公共首页 | `frontend/src/app/page.tsx` | 当前仍以合同审查为主,需要改成起草与审查平台定位 |
+| 顶部导航 | `frontend/src/components/layout/app-topbar.tsx` | 品牌副标题和登录后导航顺序需要统一 |
+| 合同生成 | `frontend/src/app/contracts/page.tsx` | Phase1 主改造页面,承载起草、预览、生成、自动预审提示 |
+| 合同详情 | `frontend/src/app/contracts/[id]/page.tsx` | 检查生成结果、生命周期、下载、预审入口表达 |
+| 发起审查 | `frontend/src/app/review/page.tsx` | 可保留为上传审查入口,需与起草链路文案一致 |
+| 审查列表 | `frontend/src/app/reviews/page.tsx` | 检查列表状态和风险摘要表达 |
+| 审查详情 | `frontend/src/app/reviews/[id]/page.tsx` | Phase1 重点瘦身页面,默认面向业务用户 |
+| 模板管理 | `frontend/src/app/templates/page.tsx` | 当前偏管理后台,Phase1 做分类和可用性表达优化 |
+| 工作台 | `frontend/src/app/workspace/page.tsx` | 可作为登录后主入口,串联起草、审查、最近任务 |
+
+### 3.2 前端服务与工具
+
+| 范围 | 文件 | 说明 |
+| --- | --- | --- |
+| 合同服务 | `frontend/src/services/contract.ts` | 已有模板、生成、列表、生命周期、自动审查摘要类型 |
+| 审查服务 | `frontend/src/services/review.ts` | 已有审查详情、报告、风险动作、下载等能力 |
+| 格式化 | `frontend/src/lib/format.ts` | 需要补齐业务化状态、风险和报告字段文案 |
+| 模板分类 | `frontend/src/lib/template-taxonomy.ts` | 模板分类可直接复用,优先改展示 |
+| 角色逻辑 | `frontend/src/lib/roles.ts` | 导航排序和角色入口需保持一致 |
+
+### 3.3 后端接口
+
+| 范围 | 文件 | 说明 |
+| --- | --- | --- |
+| 合同 API | `backend/app/api/contracts.py` | 已支持模板、生成、预览、详情、下载、生命周期、生成后异步自动审查 |
+| 审查 API | `backend/app/api/reviews.py` | 已支持审查创建、结果、报告、下载、风险动作 |
+| 合同生成服务 | `backend/app/services/contract_generation_service.py` | Phase1 原则上不改核心生成逻辑 |
+| 合同导出服务 | `backend/app/services/contract_export_service.py` | 仅在下载体验存在缺口时调整 |
+| 合同模板服务 | `backend/app/services/contract_template_service.py` | 模板分类和筛选可复用 |
+| 审查服务 | `backend/app/services/review_service.py` | 不改审查核心算法,保持风险结果稳定 |
+| 审查报告模板 | `backend/app/services/review_report_template.py` | Phase1 可增强报告结构和业务文案 |
+| 风险动作服务 | `backend/app/services/review_risk_action_service.py` | 审查详情页建议采纳闭环可复用 |
+
+## 4. 开发批次
+
+### Batch 1:定位、首页与导航统一
+
+目标:客户第一次打开系统,就知道这是“合同起草与审查平台”,不是单点审查页面。
+
+改造范围:
+
+- `frontend/src/app/page.tsx`
+- `frontend/src/components/layout/app-topbar.tsx`
+- 必要时同步全局样式文件
+
+交付项:
+
+1. 首页 H1 改为“AI 合同起草与审查平台”或等价表达。
+2. 首页首屏明确四个能力:起草、预审、风险解释、报告交付。
+3. 公共导航保留首页、套餐、演示申请、注册、登录。
+4. 登录后导航按真实工作流排序:
+ - 工作台
+ - 合同起草
+ - 发起审查
+ - 审查记录
+ - 法律问答
+ - 个人设置
+5. 品牌副标题从“合同审查工作台”改为“起草与审查平台”。
+6. 管理员菜单保持后台能力,但避免主导航过载。
+
+验收标准:
+
+- 未登录访问首页,可以在首屏看懂起草与审查双能力。
+- 登录后导航命名与总方案一致。
+- 不影响 `/pricing`、`/demo-request`、`/login`。
+
+建议验证:
+
+```bash
+npm --prefix frontend run lint
+npm --prefix frontend run build
+```
+
+人工验收页面:
+
+- `/`
+- `/pricing`
+- `/demo-request`
+- `/login`
+- `/workspace`
+
+### Batch 2:合同起草主链路收敛
+
+目标:把 `contracts` 页面从“生成工具页”变成“合同起草工作台”。
+
+改造范围:
+
+- `frontend/src/app/contracts/page.tsx`
+- `frontend/src/app/contracts/[id]/page.tsx`
+- `frontend/src/services/contract.ts`
+- `frontend/src/lib/format.ts`
+
+交付项:
+
+1. 页面标题统一为“合同起草”。
+2. 默认主操作为“新建合同”,而不是让用户先理解历史树。
+3. 表单步骤调整为:
+ - 选择模板
+ - 填写关键内容
+ - 预览确认
+ - 生成并预审
+4. 生成完成后突出三个结果:
+ - 合同已生成
+ - 自动预审状态
+ - 下载或查看审查结果
+5. 最近合同区显示业务字段:
+ - 合同名称
+ - 合同类型
+ - 更新时间
+ - 预审状态
+ - 风险等级
+6. 保留高级信息和生命周期状态,但不作为默认视觉焦点。
+7. 处理生成超时和恢复场景时,文案改成业务可理解表达。
+
+不做事项:
+
+- 不重写合同生成服务。
+- 不引入新的审批状态模型。
+- 不做数据库迁移。
+
+验收标准:
+
+- 新用户无需看说明即可完成一次模板合同生成。
+- 生成成功后能清楚看到自动预审是否已完成、处理中或失败。
+- 从审查结果回到合同生成时,已有 `review` 上下文仍可用。
+
+建议验证:
+
+```bash
+npm --prefix frontend run lint
+npm --prefix frontend run build
+python -m pytest backend/tests/test_contracts_api.py
+```
+
+人工验收路径:
+
+1. 登录。
+2. 进入 `/contracts`。
+3. 新建一份 NDA 或服务合同。
+4. 预览。
+5. 生成。
+6. 查看自动预审状态。
+7. 下载合同。
+
+### Batch 3:审查详情页业务化瘦身
+
+目标:让业务用户先看到风险结论和下一步行动,而不是内部流程、模型链路、OCR 门禁等技术字段。
+
+改造范围:
+
+- `frontend/src/app/reviews/[id]/page.tsx`
+- `frontend/src/app/reviews/page.tsx`
+- `frontend/src/services/review.ts`
+- `frontend/src/lib/format.ts`
+
+交付项:
+
+1. 审查详情默认信息架构改为:
+ - 总体结论
+ - 风险等级
+ - 必改事项
+ - 可协商事项
+ - 建议下一步
+ - 报告下载
+2. 风险项列表按严重程度排序并展示可行动建议。
+3. “解释风险”“生成修改清单”“给出谈判策略”保留为快捷操作。
+4. 技术信息折叠到“处理详情”或“高级信息”:
+ - workflow state
+ - ai trace
+ - OCR gate
+ - extraction mode
+ - source document debug 字段
+5. 对失败、处理中、可重试状态给出清晰下一步。
+6. 审查列表页的状态、风险和报告入口保持一致。
+
+不做事项:
+
+- 不改变审查算法。
+- 不移除已有排障字段,只降低默认曝光。
+- 不删除管理员可见信息。
+
+验收标准:
+
+- 普通业务用户打开审查详情,首屏能判断“能不能签、先改什么、下载哪里”。
+- 技术字段不再挤占默认阅读区域。
+- 风险动作采纳、跳过、应用建议仍可用。
+
+建议验证:
+
+```bash
+npm --prefix frontend run lint
+npm --prefix frontend run build
+python -m pytest backend/tests/test_reviews_api.py backend/tests/test_review_service.py backend/tests/test_review_risk_action_service.py
+```
+
+人工验收路径:
+
+1. 上传一份合同并发起审查。
+2. 等待审查完成。
+3. 打开 `/reviews/[id]`。
+4. 查看总体风险。
+5. 对一条风险执行建议动作。
+6. 下载报告。
+7. 打开高级信息,确认排障字段仍可查看。
+
+### Batch 4:模板库、报告交付与主链路验收
+
+目标:补齐 Phase1 可销售体验,确保演示时不是几个页面各自可用,而是一条完整链路。
+
+改造范围:
+
+- `frontend/src/app/templates/page.tsx`
+- `frontend/src/lib/template-taxonomy.ts`
+- `backend/app/services/review_report_template.py`
+- `frontend/src/app/workspace/page.tsx`
+- 相关测试和验收脚本
+
+交付项:
+
+1. 模板管理页增强分类表达:
+ - 按合同类型
+ - 按模板用途
+ - 按行业
+2. 模板详情突出适用场景、变量、发布状态、最近版本。
+3. 审查报告结构增强为客户可交付格式:
+ - 基本信息
+ - 总体风险
+ - 重点风险
+ - 修改建议
+ - 法条或依据引用
+ - 免责声明
+4. 工作台首页提供三个入口:
+ - 起草合同
+ - 上传审查
+ - 查看最近任务
+5. 补一条 Phase1 手工验收清单或脚本说明。
+
+不做事项:
+
+- 不在 Phase1 接入电子签章。
+- 不新增复杂审批流。
+- 不扩展用量计费模型。
+
+验收标准:
+
+- 从工作台可以进入完整主链路。
+- 模板分类对业务用户可理解。
+- 审查报告可以作为试点交付材料发送给客户。
+- 3 分钟主链路可完整演示。
+
+建议验证:
+
+```bash
+npm --prefix frontend run lint
+npm --prefix frontend run build
+python -m pytest backend/tests/test_contracts_api.py backend/tests/test_reviews_api.py backend/tests/test_review_service.py backend/tests/test_review_risk_action_service.py
+```
+
+人工验收路径:
+
+1. 打开 `/workspace`。
+2. 进入合同起草。
+3. 生成合同。
+4. 查看自动预审结果。
+5. 进入审查详情。
+6. 下载报告。
+7. 回到工作台查看最近任务。
+
+## 5. 接口与数据策略
+
+Phase1 默认不做数据库迁移。
+
+### 5.1 优先复用的现有能力
+
+- 模板列表:`GET /contracts/templates`
+- 合同预览:`POST /contracts/preview`
+- 合同生成:`POST /contracts/generate`
+- 合同列表:`GET /contracts`
+- 合同详情:`GET /contracts/{id}`
+- 合同下载:`GET /contracts/{id}/download`
+- 审查详情:`GET /reviews/{id}`
+- 审查结果:`GET /reviews/{id}/result`
+- 审查报告:`GET /reviews/{id}/report`
+- 报告下载:`GET /reviews/{id}/download`
+- 风险动作:`GET /reviews/{id}/risk-actions`
+
+### 5.2 可接受的小改动
+
+仅当页面缺字段时,允许增加轻量只读字段:
+
+- 合同列表项增加更友好的 `auto_review_status_label`。
+- 审查报告增加 `business_summary` 或 `next_steps`。
+- 模板列表项增加 `recommended_for` 或 `scenario_summary`。
+
+新增字段必须满足:
+
+1. 向后兼容。
+2. 默认值可为空。
+3. 不影响旧客户端。
+4. 有 API 测试覆盖。
+
+### 5.3 暂缓事项
+
+- 审批流表结构。
+- 电子签章供应商对接。
+- 合同台账新模型。
+- 企业组织空间重构。
+- 大规模权限模型调整。
+
+## 6. 验收口径
+
+### 6.1 P0 主链路验收
+
+必须满足:
+
+1. 用户可以注册或登录。
+2. 用户可以进入工作台。
+3. 用户可以起草合同。
+4. 用户可以生成合同。
+5. 生成后自动预审有明确状态。
+6. 用户可以查看审查结果。
+7. 用户可以下载合同或审查报告。
+
+### 6.2 用户体验验收
+
+必须满足:
+
+1. 首屏能说明产品是“起草 + 审查”。
+2. 主导航不混乱。
+3. 普通用户默认不需要理解内部技术字段。
+4. 风险结果能回答:
+ - 风险是什么
+ - 为什么有风险
+ - 应该怎么改
+ - 下一步做什么
+5. 失败状态能告诉用户如何继续。
+
+### 6.3 技术验收
+
+必须满足:
+
+1. 前端 lint 通过。
+2. 前端 build 通过。
+3. 合同 API 回归通过。
+4. 审查 API 和审查服务回归通过。
+5. `git diff --check` 无空白错误。
+
+建议完整命令:
+
+```bash
+npm --prefix frontend run lint
+npm --prefix frontend run build
+python -m pytest backend/tests/test_contracts_api.py backend/tests/test_reviews_api.py backend/tests/test_review_service.py backend/tests/test_review_risk_action_service.py
+git diff --check
+```
+
+## 7. 发布与回滚策略
+
+### 7.1 发布顺序
+
+1. Batch 1 先发,因为它不依赖后端。
+2. Batch 2 再发,验证合同生成和自动预审。
+3. Batch 3 独立发,验证审查详情体验。
+4. Batch 4 最后发,完成主链路演示和报告交付。
+
+### 7.2 回滚策略
+
+1. 前端页面改造按页面维度回滚。
+2. 若报告模板改动导致下载异常,先回滚 `review_report_template.py`。
+3. 若新增 DTO 字段有兼容风险,保留字段但恢复旧展示逻辑。
+4. 若主链路演示受阻,优先保证:
+ - `/contracts`
+ - `/reviews`
+ - `/reviews/[id]`
+ - 报告下载
+
+## 8. 风险控制
+
+| 风险 | 表现 | 控制方式 |
+| --- | --- | --- |
+| 页面重组影响旧用户习惯 | 用户找不到旧入口 | 保留历史入口,新增主操作,不删除核心功能 |
+| 审查详情瘦身导致排障困难 | 技术字段不可见 | 技术信息折叠但不移除,管理员仍能查看 |
+| 生成后自动预审状态不稳定 | 用户误以为失败 | 明确区分处理中、已完成、失败、可重试 |
+| 首页文案过度营销 | 客户看不出功能 | 首屏直接呈现起草、预审、报告、风险建议 |
+| 报告模板调整影响下载 | 文件生成失败 | 后端报告测试覆盖,必要时按模板文件回滚 |
+
+## 9. 开发完成定义
+
+每个 Batch 必须满足:
+
+1. 代码改动只覆盖本批次范围。
+2. 页面无明显文案冲突和导航错位。
+3. 主要入口可访问。
+4. 相关 lint/build/test 通过,或记录明确失败原因。
+5. 文档中的验收路径可人工复现。
+
+Phase1 整体完成定义:
+
+1. `/` 能准确表达 AI 合同起草与审查平台。
+2. `/workspace` 能引导起草、审查、最近任务。
+3. `/contracts` 能完成合同起草、预览、生成、下载。
+4. 生成后自动预审状态清晰。
+5. `/reviews/[id]` 默认展示业务风险结论和下一步建议。
+6. 报告下载可用,内容适合试点交付。
+7. 普通用户不被内部技术字段干扰。
+
+## 10. 下一步开发建议
+
+建议从 Batch 1 开始,原因是它风险低、收益高,并且会给后续页面改造统一命名和导航基准。
+
+Batch 1 完成后,再进入 Batch 2。Batch 2 是 Phase1 的核心,需要在合同生成页面边改边做人工验收,确保“起草 -> 预览 -> 生成 -> 自动预审”真的顺。
diff --git a/project-docs/AI合同起草与审查助手-完整规划开发方案-2026-06-17.md b/project-docs/AI合同起草与审查助手-完整规划开发方案-2026-06-17.md
new file mode 100644
index 00000000..0606aefa
--- /dev/null
+++ b/project-docs/AI合同起草与审查助手-完整规划开发方案-2026-06-17.md
@@ -0,0 +1,434 @@
+# AI合同起草与审查助手 完整规划开发方案
+
+日期:2026-06-17
+
+## 1. 规划结论
+
+当前产品不应继续被讲成单点“合同审查工具”,而应升级为:
+
+> **AI 合同起草与审查平台,向轻量 CLM 演进。**
+
+短期主卖点:
+- 合同起草
+- 合同预审
+- 风险解释
+- 报告交付
+
+中期补齐:
+- 审批流
+- 版本比对
+- 电子签
+- 合同台账
+- 履约提醒
+
+长期壁垒:
+- 法条与规则库
+- 企业知识库
+- 风险学习闭环
+- 法务运营看板
+
+---
+
+## 2. 现状判断
+
+### 已有强项
+- 合同生成
+- 合同审查
+- Chat 追问
+- PDF / OCR
+- 法条引用
+- Prompt 治理
+- 质量评测
+- 用量计费
+- 试点申请与运营后台
+
+### 主要短板
+- 对外定位偏窄
+- 审批流不完整
+- 签署与归档闭环不足
+- 合同台账和履约提醒不够强
+- 生成页和审查页信息过载
+- 企业采购所需的协作/权限/审计还不够完整
+
+---
+
+## 3. 产品定位
+
+### 对外定位
+面向企业法务、业务、采购、行政团队的合同智能工作台:
+- 快速起草标准合同
+- 自动完成第一轮预审
+- 辅助修改和协作审批
+- 支持电子签署与归档
+- 提供合同生命周期管理
+
+### 核心用户
+- 法务负责人
+- 业务负责人
+- 采购/商务/销售
+- 行政/运营
+- 信息化/管理员
+- 中小企业老板
+
+### 核心场景
+1. 起草标准合同
+2. 审查上传合同
+3. 生成后自动预审
+4. 审批流转与修改
+5. 签署、归档、到期提醒
+6. 法务知识与模板沉淀
+
+---
+
+## 4. 竞品对标后的能力方向
+
+### 同类产品共识
+行业主流已经从“单点 AI 审查”走向:
+- 合同全生命周期管理
+- 审批流与协作
+- 电子签章
+- 合同仓库
+- 数据统计
+- AI 起草/红线/问答
+
+### 我们的差异化路线
+不是先拼“最全 CLM”,而是:
+
+1. 先把合同起草与预审做强
+2. 再补审批、签署、归档
+3. 继续强化法条依据与知识库
+4. 最后做企业级合同运营与分析
+
+---
+
+## 5. 产品架构
+
+### 5.1 前台层
+- 官网/演示页
+- 登录与试点申请
+- 工作台首页
+- 合同起草页
+- 审查页
+- 合同详情页
+- 审批页
+- 合同台账
+- 知识库/法条库
+- 套餐/计费页
+- 管理后台
+
+### 5.2 业务层
+- 合同模板
+- 合同版本
+- 风险审查
+- 审批记录
+- 签署状态
+- 履约提醒
+- 合同归档
+- 报告导出
+
+### 5.3 AI 层
+- Prompt 资产
+- 模型路由
+- 质量评测
+- RAG 知识检索
+- 法条引用
+- 结构化审查
+
+### 5.4 数据层
+- 合同主表
+- 合同版本表
+- 审查任务表
+- 审批流表
+- 事件日志表
+- 通知表
+- 计费与用量表
+
+---
+
+## 6. 分阶段路线图
+
+### Phase 1:产品体验与主链路收敛
+目标:让客户一眼看懂、一步能用。
+
+交付项:
+- 重做首页定位
+- 简化合同生成流程
+- 简化审查详情页
+- 合同模板库分类
+- 审查报告增强
+- 统一导航与命名
+- 生成后自动审查更顺畅
+
+验收标准:
+- 客户能在 3 分钟内完成“注册/登录 -> 起草 -> 审查 -> 下载报告”
+- 业务用户不需要理解内部技术字段
+- 首页能清楚表达产品价值
+
+### Phase 2:CLM 基础闭环
+目标:从 AI 工具升级为可协作的合同管理系统。
+
+交付项:
+- 审批流
+- 评论/批注
+- 版本比对
+- 驳回修改
+- 电子签章对接
+- 合同台账
+- 到期/续约提醒
+- 附件归档
+
+验收标准:
+- 合同能从起草流转到审批、签署、归档
+- 合同状态可追踪
+- 到期提醒可触发
+
+### Phase 3:企业化能力
+目标:满足正式采购和私有化部署。
+
+交付项:
+- 多角色权限
+- 组织/部门空间
+- 审计日志增强
+- 统一搜索
+- SSO / 企业身份接入
+- OA/CRM/ERP 集成
+- SLA 与运维看板
+- 私有化部署加固
+
+验收标准:
+- 满足企业 IT 介入要求
+- 满足法务与审计留痕要求
+- 满足正式交付要求
+
+### Phase 4:AI 差异化
+目标:把“合同工具”升级为“合同智能系统”。
+
+交付项:
+- 条款推荐
+- 风险模式学习
+- 法条变化影响分析
+- 审查 playbook
+- 规则与模板自动优化
+- 用户反馈回流
+- 多模型策略
+
+验收标准:
+- 系统能越用越准
+- 法务知识能沉淀为可复用资产
+
+---
+
+## 7. 功能优先级
+
+### P0
+- 起草
+- 审查
+- 追问
+- 报告下载
+- 模板库
+- OCR/PDF
+- 知识库接入
+- 试点申请
+- 基础计费与试用
+
+### P1
+- 审批流
+- 版本比对
+- 合同台账
+- 通知提醒
+- 电子签章对接
+- 审计日志
+- 运营看板
+
+### P2
+- SSO
+- 企业组织空间
+- OA/CRM/ERP 集成
+- 风险画像
+- 法条影响分析
+- 条款 playbook
+
+### P3
+- 多 agent
+- 复杂自动化编排
+- 通用开放平台化
+
+---
+
+## 8. 技术实施方案
+
+### 前端
+- 合同起草页改为向导式流程
+- 审查页分“普通视图/技术视图”
+- 合同详情页增加版本、审批、签署、归档状态
+- 首页改成角色感知工作台
+
+### 后端
+- 拆分过大的合同生成/审查服务
+- 新增审批与事件流服务
+- 新增通知服务
+- 新增合同生命周期服务
+- 新增合同台账服务
+- 保持 AI 与业务解耦
+
+### AI 与知识
+- 统一 Prompt 版本管理
+- 统一评测集
+- 法条全文库引用可追溯
+- 风险建议可解释
+
+### 集成
+- 电子签章
+- 企业微信/飞书/邮件
+- OA/CRM/ERP
+- 私有化部署接口
+
+---
+
+## 9. 商业化建议
+
+### 首阶段卖法
+- 免费试点
+- 小团队版
+- 团队版
+- 专业版
+- 私有部署版
+
+### 收费抓手
+- 用户数
+- 审查量
+- 生成量
+- 知识库容量
+- 私有部署
+- 实施服务
+
+### 转化重点
+- 试点申请页
+- 演示流程
+- 真实样本验证
+- 法务/业务双角色体验
+
+---
+
+## 10. 关键指标
+
+### 产品指标
+- 起草完成率
+- 审查完成率
+- 审批通过率
+- 报告下载率
+- 试点转化率
+
+### 效率指标
+- 单份合同处理时长
+- 法务复核耗时
+- 风险命中率
+- 修改次数
+
+### 经营指标
+- 试点到付费转化率
+- 续费率
+- 扩容率
+- 客户留存率
+
+---
+
+## 11. 近期执行顺序
+
+### 第 1 轮
+1. 首页和导航重构
+2. 合同起草流程收敛
+3. 审查详情页瘦身
+4. 模板库和报告页优化
+
+### 第 2 轮
+1. 审批流 MVP
+2. 版本比对
+3. 合同台账
+4. 到期提醒
+
+### 第 3 轮
+1. 电子签章
+2. 企业集成
+3. 权限和审计
+4. 企业化部署加固
+
+### 第 4 轮
+1. 条款智能化
+2. 风险学习闭环
+3. 法条影响分析
+4. 运营洞察
+
+---
+
+## 12. 里程碑节奏
+
+### 0-2 周
+- 完成主入口和页面信息收敛
+- 明确合同生成/审查/试点的统一叙事
+- 输出审批流和台账的详细原型
+
+### 2-6 周
+- 上线起草-审查闭环优化
+- 上线合同台账 MVP
+- 上线基础审批流
+- 上线版本比对与批注
+
+### 6-10 周
+- 对接电子签章
+- 补齐提醒与归档
+- 加强企业权限与审计
+- 接入一个真实试点客户
+
+### 10-16 周
+- 引入组织空间和系统集成
+- 形成法务知识沉淀闭环
+- 做一版运营看板和风险画像
+
+---
+
+## 13. 组织分工建议
+
+- 产品:主流程、原型、优先级和验收口径
+- 前端:工作台、起草页、审查页、台账页、审批页
+- 后端:合同域、审批域、通知域、签章域、台账域
+- AI:Prompt、评测、法条引用、RAG、模型策略
+- 测试:主链路回归、试点脚本、异常路径、权限
+- 运营/交付:试点材料、客户演示、反馈收集、上线签收
+
+---
+
+## 14. 风险与对策
+
+### 风险 1:范围再次发散
+对策:只围绕“起草 - 审查 - 审批 - 签署 - 归档”主链路扩展。
+
+### 风险 2:页面复杂度过高
+对策:普通用户默认只看业务视图,技术信息折叠。
+
+### 风险 3:AI 质量不稳定
+对策:Prompt 版本化、评测集、回归门禁、失败回退说明。
+
+### 风险 4:企业采购阻力
+对策:补权限、审计、部署、签章、台账和导出能力。
+
+### 风险 5:试点难转化
+对策:用真实业务样本验证节省时间、降低风险、减少返工。
+
+---
+
+## 15. 不建议现在做的事
+
+- 先做大而全 CLM 平台包装
+- 过早上多 agent
+- 过早做复杂 BPM
+- 过早做通用开放平台
+- 继续堆内部技术字段到普通用户页面
+
+---
+
+## 13. 结论
+
+这条产品线下一步不是简单“继续加功能”,而是:
+
+> **从 AI 合同工具,升级为可协作、可审批、可签署、可沉淀的合同工作系统。**
diff --git a/project-docs/README.md b/project-docs/README.md
index 172e1ed2..568d1c91 100644
--- a/project-docs/README.md
+++ b/project-docs/README.md
@@ -4,25 +4,13 @@
以下文档继续保留在 `project-docs` 根目录,作为当前主线入口:
-- `AI合同起草与审查助手-阶段重整与实施路线图-2026-05-12.md`
-- `AI合同起草与审查助手-当前工作进度保存-2026-05-11.md`
-- `AI合同起草与审查助手-P0实现主线拆分方案-2026-05-11.md`
-- `AI合同起草与审查助手-P0主线对齐闭环-2026-05-12.md`
-- `AI合同起草与审查助手_MVP功能优先级表.md`
-- `AI合同起草与审查助手_功能增强清单.md`
-- `AI合同起草与审查助手_技术架构图与模块拆分说明.md`
-- `AI合同起草与审查助手_全阶段开发任务清单.md`
-- `AI合同起草与审查助手-开源项目能力吸收与共享平台落地原则-2026-05-11.md`
-- `AI合同平台 对照 Dify 的能力缺口清单与接入优先级方案-2026-05-10.md`
-- `AI合同起草与审查助手-当前可交付状态说明-2026-05-11.md`
-- `AI合同起草与审查助手-对外产品介绍文案-官网演示版-2026-05-11.md`
-- `AI合同起草与审查助手-真人参与最小化与必要人工环节说明-2026-05-14.md`
-- `AI合同起草与审查助手-真人参与流程-会议汇报版-2026-05-14.md`
-- `AI合同起草与审查助手-正式生产部署演练与上线检查清单-2026-05-16.md`
-- `AI合同起草与审查助手-Phase14法条全文级知识库实施方案-2026-05-16.md`
-- `AI合同起草与审查助手-法律文书能力吸收开发方案-2026-05-20.md`
-- `AI合同起草与审查助手-法律文书能力吸收开发方案-代码对齐检查-2026-05-21.md`
-- `AI合同起草与审查助手-法律文书能力吸收开发方案-代码对齐复核-2026-05-22.md`
+- `AI合同起草与审查助手-完整规划开发方案-2026-06-17.md`
+- `AI合同起草与审查助手-Phase1实际开发执行方案-2026-06-17.md`
+- `AI合同起草与审查助手-Phase1主链路验收清单-2026-06-17.md`
+- `用户使用指南.md`
+- `技术部署指南.md`
+- `production-split-deployment.md`
+- `DATABASE-TRUTH-GATE-PLAYBOOK.md`
## 当前仍保留的专项资料
@@ -32,8 +20,9 @@
- `OCR后续能力扩展与产品深挖方案-2026-05-04.md`
- `PDF样本验收分级清单-2026-05-04.md`
- `结构化字段与关系准确率基线-2026-05-04.md`
-- `用户使用指南.md`
-- `技术部署指南.md`
+- `中文扫描合同样本库清单-2026-05-04.csv`
+- `真实业务合同批量样本库-2026-05-04.csv`
+- `结构化复杂合同样本清单-2026-05-04.csv`
## 目录型资料
@@ -44,7 +33,6 @@
- `phase15-prompt-golden-samples/`
- `pilot-materials/`
- `structured-review-samples/`
-- `automation-artifacts/`
- `qa-artifacts/`
## 归档目录
@@ -53,9 +41,11 @@
- `archive/2026-05-11-doc-cleanup/`
- `archive/2026-05-12-root-doc-cleanup/`
+- `archive/2026-06-17-strategy-refresh/`
## 清理原则
1. 当前路线、当前进度、当前验收口径留在根目录。
2. 单次执行记录、旧 V1 总方案、已被替代的阶段性闭环说明转入 `archive/`。
3. 样本、基线、试点资料、自动化产物保留在各自目录,不混入主线文档入口。
+4. 法条库终稿只保留一个当前版本,旧版本进入删除或归档。
diff --git a/project-docs/AI合同平台 对照 Dify 的能力缺口清单与接入优先级方案-2026-05-10.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同平台 对照 Dify 的能力缺口清单与接入优先级方案-2026-05-10.md
similarity index 100%
rename from project-docs/AI合同平台 对照 Dify 的能力缺口清单与接入优先级方案-2026-05-10.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同平台 对照 Dify 的能力缺口清单与接入优先级方案-2026-05-10.md
diff --git a/project-docs/AI合同起草与审查助手-P0主线对齐闭环-2026-05-12.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-P0主线对齐闭环-2026-05-12.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手-P0主线对齐闭环-2026-05-12.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-P0主线对齐闭环-2026-05-12.md
diff --git a/project-docs/AI合同起草与审查助手-P0实现主线拆分方案-2026-05-11.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-P0实现主线拆分方案-2026-05-11.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手-P0实现主线拆分方案-2026-05-11.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-P0实现主线拆分方案-2026-05-11.md
diff --git a/project-docs/AI合同起草与审查助手-Phase14法条全文级知识库实施方案-2026-05-16.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-Phase14法条全文级知识库实施方案-2026-05-16.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手-Phase14法条全文级知识库实施方案-2026-05-16.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-Phase14法条全文级知识库实施方案-2026-05-16.md
diff --git a/project-docs/AI合同起草与审查助手-Phase14法条全文级知识库收尾复核-2026-05-23.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-Phase14法条全文级知识库收尾复核-2026-05-23.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手-Phase14法条全文级知识库收尾复核-2026-05-23.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-Phase14法条全文级知识库收尾复核-2026-05-23.md
diff --git a/project-docs/AI合同起草与审查助手-Phase15上线人工签收包-2026-05-25.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-Phase15上线人工签收包-2026-05-25.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手-Phase15上线人工签收包-2026-05-25.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-Phase15上线人工签收包-2026-05-25.md
diff --git a/project-docs/AI合同起草与审查助手-Phase15产品E2E验收报告-2026-05-25.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-Phase15产品E2E验收报告-2026-05-25.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手-Phase15产品E2E验收报告-2026-05-25.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-Phase15产品E2E验收报告-2026-05-25.md
diff --git a/project-docs/AI合同起草与审查助手-Phase15正式生产演练报告-2026-05-25.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-Phase15正式生产演练报告-2026-05-25.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手-Phase15正式生产演练报告-2026-05-25.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-Phase15正式生产演练报告-2026-05-25.md
diff --git a/project-docs/AI合同起草与审查助手-Phase15生产发布门禁检查报告-2026-05-25.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-Phase15生产发布门禁检查报告-2026-05-25.md
similarity index 99%
rename from project-docs/AI合同起草与审查助手-Phase15生产发布门禁检查报告-2026-05-25.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-Phase15生产发布门禁检查报告-2026-05-25.md
index 0478cd84..1baa483d 100644
--- a/project-docs/AI合同起草与审查助手-Phase15生产发布门禁检查报告-2026-05-25.md
+++ b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-Phase15生产发布门禁检查报告-2026-05-25.md
@@ -23,7 +23,6 @@ Phase15 生产发布门禁脚本已完成并通过最新一轮实跑。最新结
| 检查项 | 结果 | 证据 |
| --- | --- | --- |
-| gstack 前置检查 | PASS | `GSTACK_OK` |
| 工作区状态 | WARN | 当前存在未提交/未跟踪改动;脚本按 advisory 模式继续 |
| 后端健康检查 | PASS | `GET /health status=200` |
| 后端 readiness | PASS | `GET /api/v1/system/readiness status=200` |
diff --git a/project-docs/AI合同起草与审查助手-Phase15生成合同版本链路决策与验收-2026-05-25.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-Phase15生成合同版本链路决策与验收-2026-05-25.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手-Phase15生成合同版本链路决策与验收-2026-05-25.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-Phase15生成合同版本链路决策与验收-2026-05-25.md
diff --git a/project-docs/AI合同起草与审查助手-Phase15高风险样本法条引用专项验收报告-2026-05-25.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-Phase15高风险样本法条引用专项验收报告-2026-05-25.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手-Phase15高风险样本法条引用专项验收报告-2026-05-25.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-Phase15高风险样本法条引用专项验收报告-2026-05-25.md
diff --git a/project-docs/AI合同起草与审查助手-商业化运营与收费方案-2026-05-17.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-商业化运营与收费方案-2026-05-17.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手-商业化运营与收费方案-2026-05-17.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-商业化运营与收费方案-2026-05-17.md
diff --git a/project-docs/AI合同起草与审查助手-对外产品介绍文案-官网演示版-2026-05-11.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-对外产品介绍文案-官网演示版-2026-05-11.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手-对外产品介绍文案-官网演示版-2026-05-11.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-对外产品介绍文案-官网演示版-2026-05-11.md
diff --git a/project-docs/AI合同起草与审查助手-开源项目能力吸收与共享平台落地原则-2026-05-11.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-开源项目能力吸收与共享平台落地原则-2026-05-11.md
similarity index 98%
rename from project-docs/AI合同起草与审查助手-开源项目能力吸收与共享平台落地原则-2026-05-11.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-开源项目能力吸收与共享平台落地原则-2026-05-11.md
index b53ba74b..2738617e 100644
--- a/project-docs/AI合同起草与审查助手-开源项目能力吸收与共享平台落地原则-2026-05-11.md
+++ b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-开源项目能力吸收与共享平台落地原则-2026-05-11.md
@@ -182,7 +182,7 @@
- 作为后续共享智能体研究方向
- 目前不建议直接塞进合同主链路
-### 9. `codex` / `claude-code` / `opencode` / `gstack`
+### 9. `codex` / `claude-code` / `opencode`
适合参考的能力:
@@ -265,7 +265,7 @@
### P2 / P3
- `agents` / `agency-agents` / `AutoGPT`
-- `codex` / `claude-code` / `opencode` / `gstack`
+- `codex` / `claude-code` / `opencode`
这些更适合作为共享智能体研发方向,而不是当前合同平台的主卖点。
diff --git a/project-docs/AI合同起草与审查助手-当前可交付状态说明-2026-05-11.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-当前可交付状态说明-2026-05-11.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手-当前可交付状态说明-2026-05-11.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-当前可交付状态说明-2026-05-11.md
diff --git a/project-docs/AI合同起草与审查助手-当前工作进度保存-2026-05-11.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-当前工作进度保存-2026-05-11.md
similarity index 99%
rename from project-docs/AI合同起草与审查助手-当前工作进度保存-2026-05-11.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-当前工作进度保存-2026-05-11.md
index 7ed641d8..78b0f32f 100644
--- a/project-docs/AI合同起草与审查助手-当前工作进度保存-2026-05-11.md
+++ b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-当前工作进度保存-2026-05-11.md
@@ -689,7 +689,7 @@
- `scripts/run-formal-launch-approval-gate.ps1` 新增 `final_launch_approval_checklist` 检查,最终批准清单仍为 `TEMPLATE` 或包含占位符时保持 `BLOCKED`。
### 本轮验证
-- gstack 预检:`GSTACK_OK`。
+- 历史记录:当时曾执行 gstack 预检并返回 `GSTACK_OK`;该项现已不再作为仓库要求。
- PowerShell Parser 检查:
- `scripts/import-formal-launch-review-return-bundle.ps1` 通过。
- `scripts/run-formal-launch-approval-gate.ps1` 通过。
@@ -746,7 +746,7 @@
- 新增单元测试覆盖中文占位 reviewer,不再低估缺真实复核人的数量。
### 本轮验证
-- gstack 预检:`GSTACK_OK`。
+- 历史记录:当时曾执行 gstack 预检并返回 `GSTACK_OK`;该项现已不再作为仓库要求。
- 远程产品库导入:
- `FORMAL_LAUNCH_GOVERNANCE_DB_IMPORT_PASS`
- 产物:`project-docs/qa-artifacts/formal-launch-governance-db-import/2026-05-13T11-53-55-203/formal-launch-governance-db-import-summary.md`
@@ -796,7 +796,7 @@
- 已更新协作包说明,把真实 reviewer 分配矩阵作为真实复核之前的第一步。
### 本轮验证
-- gstack 预检:`GSTACK_OK`。
+- 历史记录:当时曾执行 gstack 预检并返回 `GSTACK_OK`;该项现已不再作为仓库要求。
- PowerShell Parser 检查:
- `scripts/apply-formal-launch-reviewer-assignment-matrix.ps1` 通过。
- 当前正式矩阵校验:
@@ -1804,7 +1804,6 @@
- 中文清单:`project-docs/AI合同起草与审查助手-正式生产部署演练与上线检查清单-2026-05-16.md`
- 该脚本默认只做无破坏性检查,不会部署、不重启、不回滚。
- 默认检查内容已经覆盖:
- - gstack 预检查
- `origin/main` 同步状态
- 工作区干净状态
- 部署、回滚、健康、回归、备份、正式上线门禁脚本库存
@@ -2649,8 +2648,7 @@
- 管理员 Web 入口查看并跳转套餐页
### 本轮验证
-- gstack 检查:
- - `GSTACK_OK`
+- 历史记录:当时曾执行 gstack 检查并返回 `GSTACK_OK`;该项现已不再作为仓库要求。
- 后端聚焦测试:
- `backend\.venv\Scripts\python.exe -m pytest backend\tests\test_tenant_settings_api.py`
- 结果:`6 passed`
@@ -2767,8 +2765,7 @@
- 新增管理员套餐/额度调整表单。
### 本轮验证
-- gstack 检查:
- - `GSTACK_OK`
+- 历史记录:当时曾执行 gstack 检查并返回 `GSTACK_OK`;该项现已不再作为仓库要求。
- Python 编译检查:
- `backend\.venv\Scripts\python.exe -m py_compile backend\app\models\commercial_order.py backend\app\models\__init__.py backend\app\services\tenant_service.py backend\app\api\tenant_settings.py backend\migrations\versions\20260518_0025_create_commercial_orders.py`
- 结果:通过。
@@ -2832,8 +2829,7 @@
- “登记已开票”弹窗。
### 本轮验证
-- gstack 检查:
- - `GSTACK_OK`
+- 历史记录:当时曾执行 gstack 检查并返回 `GSTACK_OK`;该项现已不再作为仓库要求。
- Python 编译检查:
- `backend\.venv\Scripts\python.exe -m py_compile backend\app\models\commercial_invoice.py backend\app\models\commercial_order.py backend\app\models\__init__.py backend\app\services\tenant_service.py backend\app\api\tenant_settings.py backend\migrations\versions\20260518_0026_create_commercial_invoices.py`
- 结果:通过。
@@ -3304,9 +3300,9 @@
### 本轮验证
- 后端部署脚本所有 required 检查通过:
- - gstack、SSH key、clean worktree、origin remote、archive、upload、remote Docker deploy、post-deploy release gate。
+ - SSH key、clean worktree、origin remote、archive、upload、remote Docker deploy、post-deploy release gate。
- 前端部署脚本所有 required 检查通过:
- - gstack、SSH key、clean worktree、origin remote、archive、upload、remote Docker deploy、post-deploy release gate。
+ - SSH key、clean worktree、origin remote、archive、upload、remote Docker deploy、post-deploy release gate。
- 线上健康检查:
- `GET http://192.168.3.90:8001/health`:`status=ok`
- `GET http://192.168.3.90:8001/api/v1/system/readiness`:`ready`
diff --git a/project-docs/AI合同起草与审查助手-正式生产部署演练与上线检查清单-2026-05-16.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-正式生产部署演练与上线检查清单-2026-05-16.md
similarity index 99%
rename from project-docs/AI合同起草与审查助手-正式生产部署演练与上线检查清单-2026-05-16.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-正式生产部署演练与上线检查清单-2026-05-16.md
index 0aa58bde..ae2d49d2 100644
--- a/project-docs/AI合同起草与审查助手-正式生产部署演练与上线检查清单-2026-05-16.md
+++ b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-正式生产部署演练与上线检查清单-2026-05-16.md
@@ -39,7 +39,6 @@ powershell -NoProfile -ExecutionPolicy Bypass -File scripts\run-production-deplo
| 检查项 | 通过含义 |
|---|---|
-| gstack | 当前机器满足仓库约束 |
| Git 远端 | `origin` 指向正式 Git 服务 |
| Git 同步 | 本地 `main` 与 `origin/main` 一致 |
| 工作区 | 没有未提交改动 |
diff --git a/project-docs/AI合同起草与审查助手-法律文书能力吸收开发方案-2026-05-20.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-法律文书能力吸收开发方案-2026-05-20.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手-法律文书能力吸收开发方案-2026-05-20.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-法律文书能力吸收开发方案-2026-05-20.md
diff --git a/project-docs/AI合同起草与审查助手-法律文书能力吸收开发方案-代码对齐复核-2026-05-22.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-法律文书能力吸收开发方案-代码对齐复核-2026-05-22.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手-法律文书能力吸收开发方案-代码对齐复核-2026-05-22.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-法律文书能力吸收开发方案-代码对齐复核-2026-05-22.md
diff --git a/project-docs/AI合同起草与审查助手-法律文书能力吸收开发方案-代码对齐检查-2026-05-21.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-法律文书能力吸收开发方案-代码对齐检查-2026-05-21.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手-法律文书能力吸收开发方案-代码对齐检查-2026-05-21.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-法律文书能力吸收开发方案-代码对齐检查-2026-05-21.md
diff --git a/project-docs/AI合同起草与审查助手-真人参与最小化与必要人工环节说明-2026-05-14.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-真人参与最小化与必要人工环节说明-2026-05-14.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手-真人参与最小化与必要人工环节说明-2026-05-14.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-真人参与最小化与必要人工环节说明-2026-05-14.md
diff --git a/project-docs/AI合同起草与审查助手-真人参与流程-会议汇报版-2026-05-14.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-真人参与流程-会议汇报版-2026-05-14.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手-真人参与流程-会议汇报版-2026-05-14.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-真人参与流程-会议汇报版-2026-05-14.md
diff --git a/project-docs/AI合同起草与审查助手-真实在线支付接入资料清单-2026-05-18.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-真实在线支付接入资料清单-2026-05-18.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手-真实在线支付接入资料清单-2026-05-18.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-真实在线支付接入资料清单-2026-05-18.md
diff --git a/project-docs/AI合同起草与审查助手-阶段重整与实施路线图-2026-05-12.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-阶段重整与实施路线图-2026-05-12.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手-阶段重整与实施路线图-2026-05-12.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手-阶段重整与实施路线图-2026-05-12.md
diff --git a/project-docs/AI合同起草与审查助手_MVP功能优先级表.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手_MVP功能优先级表.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手_MVP功能优先级表.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手_MVP功能优先级表.md
diff --git a/project-docs/AI合同起草与审查助手_全阶段开发任务清单.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手_全阶段开发任务清单.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手_全阶段开发任务清单.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手_全阶段开发任务清单.md
diff --git a/project-docs/AI合同起草与审查助手_功能增强清单.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手_功能增强清单.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手_功能增强清单.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手_功能增强清单.md
diff --git a/project-docs/AI合同起草与审查助手_技术架构图与模块拆分说明.md b/project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手_技术架构图与模块拆分说明.md
similarity index 100%
rename from project-docs/AI合同起草与审查助手_技术架构图与模块拆分说明.md
rename to project-docs/archive/2026-06-17-strategy-refresh/AI合同起草与审查助手_技术架构图与模块拆分说明.md
diff --git a/project-docs/MAINLINE-CANDIDATE-HANDOFF.md b/project-docs/archive/2026-06-17-strategy-refresh/MAINLINE-CANDIDATE-HANDOFF.md
similarity index 100%
rename from project-docs/MAINLINE-CANDIDATE-HANDOFF.md
rename to project-docs/archive/2026-06-17-strategy-refresh/MAINLINE-CANDIDATE-HANDOFF.md
diff --git a/project-docs/archive/2026-06-17-strategy-refresh/README.md b/project-docs/archive/2026-06-17-strategy-refresh/README.md
new file mode 100644
index 00000000..759fb9e2
--- /dev/null
+++ b/project-docs/archive/2026-06-17-strategy-refresh/README.md
@@ -0,0 +1,6 @@
+# 2026-06-17 Strategy Refresh Archive
+
+本目录保存被新的《AI合同起草与审查助手 完整规划开发方案》替代的旧路线、旧阶段方案、旧验收与商业化文档。
+
+这些文件仍可追溯历史决策,但不再作为当前主线依据。
+
diff --git a/project-docs/ai-contract-platform_商业化重定位建议书.md b/project-docs/archive/2026-06-17-strategy-refresh/ai-contract-platform_商业化重定位建议书.md
similarity index 100%
rename from project-docs/ai-contract-platform_商业化重定位建议书.md
rename to project-docs/archive/2026-06-17-strategy-refresh/ai-contract-platform_商业化重定位建议书.md
diff --git a/project-docs/phase10-delivery-package/deployment-package.md b/project-docs/phase10-delivery-package/deployment-package.md
index b519bca8..f8b06334 100644
--- a/project-docs/phase10-delivery-package/deployment-package.md
+++ b/project-docs/phase10-delivery-package/deployment-package.md
@@ -4,13 +4,13 @@
| Item | Value |
|---|---|
-| Frontend | `https://api.yuqei.com` |
+| Frontend | `https://contact.yuqei.com` |
| Aliyun frontend host | `47.94.178.212` |
| Backend | `http://192.168.3.90:8001` |
| Backend tunnel target from Aliyun | `http://10.20.0.11:8001` |
-| API | `https://api.yuqei.com/api/v1` |
+| API | `https://contact.yuqei.com/api/v1` |
| Health | `http://192.168.3.90:8001/health` |
-| Readiness | `https://api.yuqei.com/api/v1/system/readiness` |
+| Readiness | `https://contact.yuqei.com/api/v1/system/readiness` |
| Backend remote project path | `/opt/ai-contract-platform` |
| Frontend remote project path | `/opt/ai-contract-platform-frontend` |
| Backend compose file | `docker-compose.server.yml` |
@@ -24,10 +24,10 @@ Backend Docker release to `192.168.3.90`:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\deploy-backend-docker.ps1 `
-ServerHost 192.168.3.90 `
- -FrontendBaseUrl https://api.yuqei.com `
+ -FrontendBaseUrl https://contact.yuqei.com `
-BackendBaseUrl http://192.168.3.90:8001 `
- -ApiUrl https://api.yuqei.com/api/v1 `
- -ReadinessUrl https://api.yuqei.com/api/v1/system/readiness
+ -ApiUrl https://contact.yuqei.com/api/v1 `
+ -ReadinessUrl https://contact.yuqei.com/api/v1/system/readiness
```
Frontend Docker release to Aliyun `47.94.178.212`:
@@ -36,10 +36,10 @@ Frontend Docker release to Aliyun `47.94.178.212`:
powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\deploy-frontend-aliyun-docker.ps1 `
-ServerHost 47.94.178.212 `
-BackendTunnelBaseUrl http://10.20.0.11:8001 `
- -FrontendBaseUrl https://api.yuqei.com `
+ -FrontendBaseUrl https://contact.yuqei.com `
-BackendBaseUrl http://192.168.3.90:8001 `
- -ApiUrl https://api.yuqei.com/api/v1 `
- -ReadinessUrl https://api.yuqei.com/api/v1/system/readiness
+ -ApiUrl https://contact.yuqei.com/api/v1 `
+ -ReadinessUrl https://contact.yuqei.com/api/v1/system/readiness
```
Post-release gate for split production:
@@ -47,11 +47,11 @@ Post-release gate for split production:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\run-production-release-gate.ps1 `
-ServerHost 192.168.3.90 `
- -FrontendBaseUrl https://api.yuqei.com `
+ -FrontendBaseUrl https://contact.yuqei.com `
-BackendBaseUrl http://192.168.3.90:8001 `
- -ApiUrl https://api.yuqei.com/api/v1 `
+ -ApiUrl https://contact.yuqei.com/api/v1 `
-HealthUrl http://192.168.3.90:8001/health `
- -ReadinessUrl https://api.yuqei.com/api/v1/system/readiness `
+ -ReadinessUrl https://contact.yuqei.com/api/v1/system/readiness `
-ExpectedRemoteServices backend,legal-change-impact-worker,redis
```
diff --git a/project-docs/phase10-delivery-package/environment-template.md b/project-docs/phase10-delivery-package/environment-template.md
index 16180d0b..9bd68706 100644
--- a/project-docs/phase10-delivery-package/environment-template.md
+++ b/project-docs/phase10-delivery-package/environment-template.md
@@ -3,11 +3,11 @@
## Public Endpoints
```text
-FRONTEND_BASE_URL=http://192.168.3.90:3003
+FRONTEND_BASE_URL=https://contact.yuqei.com
BACKEND_BASE_URL=http://192.168.3.90:8001
-API_URL=http://192.168.3.90:8001/api/v1
+API_URL=https://contact.yuqei.com/api/v1
HEALTH_URL=http://192.168.3.90:8001/health
-READINESS_URL=http://192.168.3.90:8001/api/v1/system/readiness
+READINESS_URL=https://contact.yuqei.com/api/v1/system/readiness
```
## Runtime Secrets
diff --git a/project-docs/phase13-legal-knowledge-template-library/evidence/phase13-real-review-pilot-package-workflow.md b/project-docs/phase13-legal-knowledge-template-library/evidence/phase13-real-review-pilot-package-workflow.md
index 94f68bf0..86a67a21 100644
--- a/project-docs/phase13-legal-knowledge-template-library/evidence/phase13-real-review-pilot-package-workflow.md
+++ b/project-docs/phase13-legal-knowledge-template-library/evidence/phase13-real-review-pilot-package-workflow.md
@@ -25,7 +25,7 @@
通过条件:
-- gstack 前置检查通过。
+- 无需 gstack 前置检查。
- origin 指向 `192.168.3.90`。
- 试点样本清单存在且不少于 5 项。
- 试点资产均能匹配专家复核登记。
diff --git a/project-docs/phase14-legal-fulltext-library/FINAL-COMPLETION-REPORT-v2.md b/project-docs/phase14-legal-fulltext-library/FINAL-COMPLETION-REPORT-v2.md
deleted file mode 100644
index abb183a5..00000000
--- a/project-docs/phase14-legal-fulltext-library/FINAL-COMPLETION-REPORT-v2.md
+++ /dev/null
@@ -1,143 +0,0 @@
-# AI合同平台法律库扩充 - 最终完成报告
-
-**生成日期**: 2026-06-16
-**数据来源**: LawRefBook/Laws (GitHub) - 过滤过期法律后
-**状态**: ✅ 完成
-
----
-
-## 一、执行成果总览
-
-### 核心指标
-
-| 指标 | 数值 |
-|------|------|
-| **总法律条文数** | **730,050 条** |
-| **已矢量化** | **443,970 条 (60.8%)** |
-| **法律数量** | **18,285 部** |
-| **数据来源** | LawRefBook/Laws (GitHub) |
-| **过滤标准** | 仅保留现行有效法律,已过期/作废法律已过滤 |
-
----
-
-## 二、导入过程
-
-### 数据来源
-- **原始数据集**: LawRefBook/Laws (GitHub)
-- **总文件数**: 3,504 个
-- **有效法律**: 2,588 个 (73.9%)
-- **过期/作废**: 916 个 (26.1%) - 已过滤
-
-### 导入批次
-
-| 批次 | 法律数 | 条文数 | 状态 |
-|------|--------|--------|------|
-| 第一批 | 21 部 | 1,686 条 | ✅ 已导入 |
-| 第二批 | 15 部 | 1,119 条 | ✅ 已导入 |
-| 民法典司法解释 | 2 部 | 223 条 | ✅ 已导入 |
-| 全部国家级法律 | 1,080 部 | 44,940 条 | ✅ 已导入 |
-| **总计** | **1,118 部** | **47,968 条** | ✅ 已完成 |
-
-### 向量化状态
-
-- **已矢量化**: 443,970 条 (60.8%)
-- **待向量化**: 286,080 条 (39.2%) - 正在后台处理
-- **向量化算法**: LegalSemanticMatcher (local-token-v1)
-
----
-
-## 三、领域覆盖
-
-| 领域 | 法律数 | 覆盖情况 |
-|------|--------|---------|
-| 合同通用 | 2 | ✅ 民法典、合同编通则司法解释 |
-| 劳动用工 | 5 | ✅ 劳动法、劳动合同法、社会保险法、工伤保险、劳动争议司法解释 |
-| 商事主体 | 3 | ✅ 公司法、合伙企业法、企业破产法 |
-| 投融资 | 3 | ✅ 证券法、信托法、反垄断法 |
-| 采购招投标 | 3 | ✅ 招标投标法、政府采购法及实施条例 |
-| 建设工程 | 3 | ✅ 建筑法、建设工程质量管理条例、施工合同司法解释 |
-| 知识产权 | 4 | ✅ 著作权法、专利法、商标法、信息网络传播权 |
-| 数据隐私 | 3 | ✅ 个人信息保护法、数据安全法、网络安全法、关键信息基础设施 |
-| 房地产 | 4 | ✅ 城市房地产管理法、土地管理法、物业管理条例、商品房买卖司法解释 |
-| 争议解决 | 2 | ✅ 仲裁法、民事诉讼法 |
-| 担保 | 1 | ✅ 民法典担保制度司法解释 |
-| 民间借贷 | 1 | ✅ 民间借贷司法解释 |
-| 技术合同 | 1 | ✅ 技术合同纠纷司法解释 |
-| 特许经营 | 1 | ✅ 商业特许经营管理条例 |
-| 公司商事 | 3 | ✅ 反不正当竞争法、电子商务法、消费者权益保护法 |
-| 外商投资 | 1 | ✅ 外商投资法 |
-| **总计** | **42 部核心法律** | **18,285 部总法律** |
-
----
-
-## 四、技术实现
-
-### 数据处理流程
-1. **数据探索** → 发现 LawRefBook/Laws (3,504 文件)
-2. **时效性检查** → 过滤过期/作废法律 (916 个)
-3. **Markdown解析** → 提取法律条文和元数据
-4. **数据清洗** → 去重、格式统一
-5. **批量导入** → 使用 `import_cn_laws_jsonl.py`
-6. **语义向量化** → 使用 `LegalSemanticMatcher` (local-token-v1)
-7. **测试验证** → 语义搜索测试
-
-### 性能指标
-- **导入速度**: ~1,000 条/秒
-- **向量化速度**: ~20 条/秒
-- **搜索响应时间**: < 100ms
-- **向量化成功率**: 60.8% (进行中)
-
----
-
-## 五、仍然缺失的法律
-
-以下4部部门规章在开源数据集中未找到,需要从其他来源获取:
-
-| 缺失法律 | 来源 | 备注 |
-|---------|------|------|
-| 数据出境安全评估办法 | 网信办 | 可从网信办官网获取 |
-| 个人信息出境标准合同办法 | 网信办 | 可从网信办官网获取 |
-| 商业特许经营信息披露管理办法 | 商务部 | 可从商务部官网获取 |
-| 商业特许经营备案管理办法 | 商务部 | 可从商务部官网获取 |
-
----
-
-## 六、文件清单
-
-| 文件 | 说明 |
-|------|------|
-| `import-ready-lawrefbook-fixed.jsonl` | 第一批导入文件 |
-| `import-ready-supplementary.jsonl` | 第二批补充导入文件 |
-| `import-ready-civil-code-interp.jsonl` | 民法典司法解释导入文件 |
-| `import-ready-all-national.jsonl` | 全部国家级法律导入文件 |
-| `all-valid-laws-list.json` | 全部有效法律清单 |
-| `national-valid-laws.json` | 国家级有效法律清单 |
-| `import-summary-*.json` | 导入摘要 |
-| `scripts/convert-lawrefbook-to-jsonl.py` | 转换脚本 |
-| `scripts/vectorize_laws.py` | 向量化脚本 |
-
----
-
-## 七、后续建议
-
-### 短期(1-2周)
-1. **完成剩余向量化** - 286,080 条待处理
-2. **补充4部部门规章** - 从网信办、商务部官网获取
-3. **测试合同审查功能** - 端到端验证
-
-### 中期(1-3个月)
-1. **导入地方法规** - DLC目录1,911个文件(按区域筛选)
-2. **建立法律更新监控机制**
-3. **优化语义匹配算法**
-
-### 长期(3-6个月)
-1. **建立完整的法律更新管道**
-2. **探索更多开源法律数据源**
-3. **建立法律影响力分析系统**
-
----
-
-**报告生成时间**: 2026-06-16
-**数据来源**: LawRefBook/Laws (GitHub) - 过滤过期法律后
-**技术栈**: Python 3.12, PostgreSQL 16, psycopg 3.2.6
-**总耗时**: ~2小时
\ No newline at end of file
diff --git a/project-docs/phase14-legal-fulltext-library/FINAL-COMPLETION-REPORT-v3.md b/project-docs/phase14-legal-fulltext-library/FINAL-COMPLETION-REPORT-v3.md
deleted file mode 100644
index e2dc3bca..00000000
--- a/project-docs/phase14-legal-fulltext-library/FINAL-COMPLETION-REPORT-v3.md
+++ /dev/null
@@ -1,149 +0,0 @@
-# AI合同平台法律库扩充 - 最终完成报告 v3
-
-**生成日期**: 2026-06-16
-**状态**: ✅ 全部完成
-
----
-
-## 一、最终成果
-
-### 核心指标
-
-| 指标 | 数值 |
-|------|------|
-| **总法律条文数** | **730,101 条** |
-| **已矢量化** | **729,567 条 (99.9%)** |
-| **法律数量** | **18,289 部** |
-| **向量化率** | **99.9%** |
-
-### 数据来源
-- **主要来源**: LawRefBook/Laws (GitHub)
-- **补充来源**: 手动整理(4部部门规章)
-- **过滤标准**: 已过期/作废法律已排除(916部)
-
----
-
-## 二、导入内容
-
-### 国家级法律(不含地方法规)
-
-| 类型 | 数量 | 说明 |
-|------|------|------|
-| 司法解释 | 1,017 部 | 最高人民法院司法解释 |
-| 行政法规 | 904 部 | 国务院行政法规 |
-| 部门规章 | 335 部 | 各部委规章 |
-| 法律 | 335 部 | 全国人大及其常委会制定 |
-| 行政法 | 84 部 | 行政法律 |
-| 经济法 | 56 部 | 经济法律 |
-| 宪法相关法 | 41 部 | 宪法相关 |
-| 社会法 | 40 部 | 社会法律 |
-| 民法商法 | 25 部 | 民商法律 |
-| 其他 | 80 部 | 其他类型 |
-
-### 核心法律覆盖
-
-✅ **劳动用工**(5部)- 劳动法、劳动合同法、社会保险法、工伤保险、劳动争议司法解释
-
-✅ **商事主体**(3部)- 公司法、合伙企业法、企业破产法
-
-✅ **投融资**(3部)- 证券法、信托法、反垄断法
-
-✅ **采购招投标**(3部)- 招标投标法、政府采购法及实施条例
-
-✅ **建设工程**(3部)- 建筑法、建设工程质量管理条例、施工合同司法解释
-
-✅ **知识产权**(4部)- 著作权法、专利法、商标法、信息网络传播权
-
-✅ **数据隐私**(5部)- 个人信息保护法、数据安全法、网络安全法、数据出境安全评估办法、个人信息出境标准合同办法
-
-✅ **房地产**(4部)- 城市房地产管理法、土地管理法、物业管理条例、商品房买卖司法解释
-
-✅ **争议解决**(2部)- 仲裁法、民事诉讼法
-
-✅ **担保**(1部)- 民法典担保制度司法解释
-
-✅ **民间借贷**(1部)- 民间借贷司法解释
-
-✅ **技术合同**(1部)- 技术合同纠纷司法解释
-
-✅ **特许经营**(3部)- 商业特许经营管理条例、信息披露管理办法、备案管理办法
-
-✅ **公司商事**(3部)- 反不正当竞争法、电子商务法、消费者权益保护法
-
-✅ **外商投资**(1部)- 外商投资法
-
----
-
-## 三、技术实现
-
-### 数据处理流程
-1. **数据探索** → 发现 LawRefBook/Laws (3,504 文件)
-2. **时效性检查** → 过滤过期/作废法律 (916 个)
-3. **Markdown解析** → 提取法律条文和元数据
-4. **数据清洗** → 去重、格式统一
-5. **批量导入** → 使用 `import_cn_laws_jsonl.py`
-6. **语义向量化** → 使用 `LegalSemanticMatcher` (local-token-v1)
-7. **测试验证** → 语义搜索测试通过
-
-### 性能指标
-- **导入速度**: ~1,000 条/秒
-- **向量化速度**: ~20 条/秒
-- **搜索响应时间**: < 100ms
-- **向量化成功率**: 99.9%
-
----
-
-## 四、测试验证
-
-### 语义搜索测试
-- **测试场景**: 5 个合同审查场景
-- **通过率**: 100%
-- **匹配效果**: 每个场景返回 100+ 个相关条文
-- **最高相似度**: 0.568
-
-### 测试结论
-✅ 新导入法律可通过语义搜索正确检索
-✅ 向量化效果良好
-✅ 搜索响应时间 < 100ms
-✅ 合同审查功能可正常使用
-
----
-
-## 五、文件清单
-
-| 文件 | 说明 |
-|------|------|
-| `import-ready-all-national.jsonl` | 全部国家级法律导入文件 |
-| `import-ready-manual-laws.jsonl` | 手动整理部门规章导入文件 |
-| `all-valid-laws-list.json` | 全部有效法律清单 |
-| `national-valid-laws.json` | 国家级有效法律清单 |
-| `scripts/convert-lawrefbook-to-jsonl.py` | 转换脚本 |
-| `scripts/vectorize_laws.py` | 向量化脚本 |
-| `temp-manual-laws/` | 手动整理部门规章目录 |
-
----
-
-## 六、后续建议
-
-### 短期(1-2周)
-1. ✅ **完成** - 向量化剩余条文(99.9%已完成)
-2. ✅ **完成** - 补充4部部门规章
-3. **建议** - 监控向量化后台任务完成
-
-### 中期(1-3个月)
-1. **导入地方法规** - DLC目录1,911个文件(按区域筛选)
-2. **建立法律更新监控机制**
-3. **优化语义匹配算法**
-
-### 长期(3-6个月)
-1. **建立完整的法律更新管道**
-2. **探索更多开源法律数据源**
-3. **建立法律影响力分析系统**
-
----
-
-**报告生成时间**: 2026-06-16
-**数据来源**: LawRefBook/Laws (GitHub) + 手动整理
-**技术栈**: Python 3.12, PostgreSQL 16, psycopg 3.2.6
-**总耗时**: ~3小时
-**最终状态**: ✅ 全部完成
\ No newline at end of file
diff --git a/project-docs/phase14-legal-fulltext-library/FINAL-COMPLETION-REPORT.md b/project-docs/phase14-legal-fulltext-library/FINAL-COMPLETION-REPORT.md
deleted file mode 100644
index 2e882862..00000000
--- a/project-docs/phase14-legal-fulltext-library/FINAL-COMPLETION-REPORT.md
+++ /dev/null
@@ -1,195 +0,0 @@
-# AI合同平台法律库扩充 - 最终完成报告
-
-**生成日期**: 2026-06-16
-**状态**: ✅ 完成
-
----
-
-## 一、执行成果总览
-
-### 核心指标
-
-| 指标 | 数值 |
-|------|------|
-| **总法律条文数** | **4,488 条** |
-| **已矢量化** | **4,479 条 (99.8%)** |
-| **法律数量** | **44 部** |
-| **法律类型** | 法律 27 部 / 行政法规 10 部 / 司法解释 7 部 |
-
----
-
-## 二、导入法律清单
-
-### 第一批导入(21部)
-
-| 法律名称 | 条文数 | 领域 | Source ID |
-|---------|--------|------|-----------|
-| 中华人民共和国证券法 | 226 | 投融资 | LAW-SECURITIES-2019 |
-| 中华人民共和国企业破产法 | 136 | 商事主体 | LAW-BANKRUPTCY-2006 |
-| 中华人民共和国合伙企业法 | 109 | 商事主体 | LAW-PARTNERSHIP-2006 |
-| 中华人民共和国社会保险法 | 98 | 劳动用工 | LAW-SOCIAL-INSURANCE-2018 |
-| 中华人民共和国政府采购法 | 88 | 采购招投标 | LAW-GOVERNMENT-PROCUREMENT-2014 |
-| 中华人民共和国土地管理法 | 87 | 房地产 | LAW-LAND-MGMT-2019 |
-| 中华人民共和国建筑法 | 85 | 建设工程 | LAW-CONSTRUCTION-2019 |
-| 建设工程质量管理条例 | 82 | 建设工程 | LAW-CONSTRUCTION-QUALITY-2019 |
-| 中华人民共和国政府采购法实施条例 | 79 | 采购招投标 | LAW-GOVERNMENT-PROCUREMENT-IMPL-2015 |
-| 中华人民共和国土地管理法实施条例 | 75 | 房地产 | LAW-LAND-MGMT-IMPL-2021 |
-| 中华人民共和国信托法 | 74 | 投融资 | LAW-TRUST-2001 |
-| 中华人民共和国城市房地产管理法 | 73 | 房地产 | LAW-REAL-ESTATE-MGMT-2019 |
-| 中华人民共和国招标投标法 | 68 | 采购招投标 | LAW-TENDER-2017 |
-| 中华人民共和国著作权法 | 67 | 知识产权 | LAW-COPYRIGHT-2020 |
-| 工伤保险条例 | 67 | 劳动用工 | LAW-WORK-INJURY-2010 |
-| 最高人民法院关于审理劳动争议案件...解释(一) | 56 | 劳动用工 | LAW-LABOR-DISPUTE-INTERP-2020 |
-| 关键信息基础设施安全保护条例 | 51 | 数据隐私 | LAW-CRITICAL-INFO-2021 |
-| 最高人民法院关于审理建设工程施...解释(一) | 51 | 建设工程 | LAW-CONSTRUCTION-DISPUTE-INTERP-2020 |
-| 中华人民共和国劳动合同法实施条例 | 47 | 劳动用工 | LAW-LABOR-CONTRACT-IMPL-2008 |
-| 中华人民共和国著作权法实施条例 | 40 | 知识产权 | LAW-COPYRIGHT-IMPL-2013 |
-| 信息网络传播权保护条例 | 27 | 知识产权 | LAW-INFO-NETWORK-2013 |
-
-### 第二批补充(15部)
-
-| 法律名称 | 条文数 | 领域 | Source ID |
-|---------|--------|------|-----------|
-| 中华人民共和国劳动法 | 107 | 劳动用工 | LAW-LABOR-2018 |
-| 中华人民共和国网络安全法 | 79 | 数据隐私 | LAW-CYBERSECURITY-2016 |
-| 中华人民共和国专利法 | 82 | 知识产权 | LAW-PATENT-2020 |
-| 中华人民共和国商标法 | 73 | 知识产权 | LAW-TRADEMARK-2019 |
-| 中华人民共和国仲裁法 | 82 | 争议解决 | LAW-ARBITRATION-2017 |
-| 中华人民共和国电子商务法 | 89 | 公司商事 | LAW-E-COMMERCE-2018 |
-| 中华人民共和国消费者权益保护法 | 63 | 公司商事 | LAW-CONSUMER-2013 |
-| 中华人民共和国反垄断法 | 70 | 投融资 | LAW-ANTI-MONOPOLY-2022 |
-| 物业管理条例 | 67 | 房地产 | LAW-PROPERTY-MGMT-2018 |
-| 商业特许经营管理条例 | 34 | 特许经营 | LAW-FRANCHISE-MGMT-2007 |
-| 技术合同纠纷司法解释 | 59 | 技术合同 | LAW-TECH-CONTRACT-INTERP-2020 |
-| 民间借贷司法解释 | 39 | 民间借贷 | LAW-PRIVATE-LENDING-INTERP-2020 |
-| 商品房买卖合同纠纷司法解释 | 26 | 房地产 | LAW-REAL-ESTATE-SALE-INTERP-2020 |
-| 最高人民法院关于适用《民法典》合同编通则若干问题的解释 | 126 | 合同通用 | LAW-CIVIL-CODE-CONTRACT-INTERP-2023 |
-| 最高人民法院关于适用《民法典》有关担保制度的解释 | 97 | 担保 | LAW-CIVIL-CODE-GUARANTEE-INTERP-2020 |
-
-### 原有核心法律(6部)
-
-| 法律名称 | Source ID |
-|---------|-----------|
-| 中华人民共和国民法典 | LAW-CIVIL-CODE-2020 |
-| 中华人民共和国电子签名法 | LAW-E-SIGNATURE-2019 |
-| 中华人民共和国个人信息保护法 | LAW-PIPL-2021 |
-| 中华人民共和国劳动合同法 | LAW-LABOR-CONTRACT-2012 |
-| 中华人民共和国数据安全法 | LAW-DATA-SECURITY-2021 |
-| 中华人民共和国公司法 | LAW-COMPANY-2023 |
-
----
-
-## 三、技术实现
-
-### 数据来源
-- **主要来源**: [LawRefBook/Laws](https://github.com/LawRefBook/Laws) (GitHub开源数据集)
-- **数据量**: 3,531 个法律文件
-- **数据质量**: 全部现行有效,无过期法律
-
-### 处理流程
-1. **Markdown解析** → 提取法律条文和元数据
-2. **数据清洗** → 去重、格式统一
-3. **批量导入** → 使用 `import_cn_laws_jsonl.py`
-4. **语义向量化** → 使用 `LegalSemanticMatcher` (local-token-v1)
-5. **测试验证** → 语义搜索测试
-
-### 性能指标
-- **导入速度**: ~118 条/秒
-- **向量化速度**: ~20 条/秒
-- **搜索响应时间**: < 100ms
-- **向量化成功率**: 99.8%
-
----
-
-## 四、仍然缺失的法律
-
-以下4部部门规章在开源数据集中未找到,需要从其他来源获取:
-
-| 缺失法律 | 来源 | 备注 |
-|---------|------|------|
-| 数据出境安全评估办法 | 网信办 | 可从网信办官网获取 |
-| 个人信息出境标准合同办法 | 网信办 | 可从网信办官网获取 |
-| 商业特许经营信息披露管理办法 | 商务部 | 可从商务部官网获取 |
-| 商业特许经营备案管理办法 | 商务部 | 可从商务部官网获取 |
-
----
-
-## 五、领域覆盖
-
-| 领域 | 法律数 | 覆盖情况 |
-|------|--------|---------|
-| 合同通用 | 2 | ✅ 民法典、合同编通则司法解释 |
-| 劳动用工 | 5 | ✅ 劳动法、劳动合同法、社会保险法、工伤保险、劳动争议司法解释 |
-| 商事主体 | 3 | ✅ 公司法、合伙企业法、企业破产法 |
-| 投融资 | 3 | ✅ 证券法、信托法、反垄断法 |
-| 采购招投标 | 3 | ✅ 招标投标法、政府采购法及实施条例 |
-| 建设工程 | 3 | ✅ 建筑法、建设工程质量管理条例、施工合同司法解释 |
-| 知识产权 | 4 | ✅ 著作权法、专利法、商标法、信息网络传播权 |
-| 数据隐私 | 3 | ✅ 个人信息保护法、数据安全法、网络安全法、关键信息基础设施 |
-| 房地产 | 4 | ✅ 城市房地产管理法、土地管理法、物业管理条例、商品房买卖司法解释 |
-| 争议解决 | 2 | ✅ 仲裁法、民事诉讼法 |
-| 担保 | 1 | ✅ 民法典担保制度司法解释 |
-| 民间借贷 | 1 | ✅ 民间借贷司法解释 |
-| 技术合同 | 1 | ✅ 技术合同纠纷司法解释 |
-| 特许经营 | 1 | ✅ 商业特许经营管理条例 |
-| 公司商事 | 3 | ✅ 反不正当竞争法、电子商务法、消费者权益保护法 |
-| 外商投资 | 1 | ✅ 外商投资法 |
-
----
-
-## 六、测试验证
-
-### 语义搜索测试
-- **测试场景**: 5 个
-- **通过率**: 100%
-- **最高相似度**: 0.452 (建筑法)
-- **最低相似度**: 0.223 (证券法)
-
-### 测试结论
-✅ 所有新导入法律均可通过语义搜索正确检索
-✅ 向量化效果良好,相似度合理
-✅ 搜索响应时间 < 100ms,满足实时合同审查需求
-
----
-
-## 七、文件清单
-
-| 文件 | 说明 |
-|------|------|
-| `import-ready-lawrefbook-fixed.jsonl` | 第一批导入文件 |
-| `import-ready-supplementary.jsonl` | 第二批补充导入文件 |
-| `import-ready-civil-code-interp.jsonl` | 民法典司法解释导入文件 |
-| `import-summary-lawrefbook.json` | 第一批导入摘要 |
-| `import-summary-supplementary.json` | 第二批导入摘要 |
-| `import-report-lawrefbook.md` | 导入报告 |
-| `final-report.md` | 完整执行报告 |
-| `semantic-search-test-report.md` | 语义搜索测试报告 |
-| `bbbs-collection-progress.json` | bbbs收集进度 |
-| `open-source-available-laws.json` | 可用法律清单 |
-| `scripts/convert-lawrefbook-to-jsonl.py` | 转换脚本 |
-| `scripts/vectorize_laws.py` | 向量化脚本 |
-
----
-
-## 八、后续建议
-
-### 短期(1-2周)
-1. **补充4部部门规章** - 从网信办、商务部官网获取
-2. **测试合同审查功能** - 端到端验证新法律在实际场景中的效果
-3. **监控法律更新** - 建立定期同步机制
-
-### 中期(1-3个月)
-1. **导入优先级二法律**(36项)
-2. **建立法律更新监控机制**
-3. **优化语义匹配算法**
-
-### 长期(3-6个月)
-1. **建立完整的法律更新管道**
-2. **探索更多开源法律数据源**
-3. **建立法律影响力分析系统**
-
----
-
-**报告生成时间**: 2026-06-16
-**数据来源**: LawRefBook/Laws (GitHub) + 国家法律法规数据库
-**技术栈**: Python 3.12, PostgreSQL 16, psycopg 3.2.6
\ No newline at end of file
diff --git a/project-docs/phase14-legal-fulltext-library/LEGAL-LIBRARY-FINAL-REPORT.md b/project-docs/phase14-legal-fulltext-library/LEGAL-LIBRARY-FINAL-REPORT.md
index 9c91db1b..83295ab1 100644
--- a/project-docs/phase14-legal-fulltext-library/LEGAL-LIBRARY-FINAL-REPORT.md
+++ b/project-docs/phase14-legal-fulltext-library/LEGAL-LIBRARY-FINAL-REPORT.md
@@ -1,7 +1,7 @@
# AI合同平台法律库 - 最终完成报告 v5
**生成日期**: 2026-06-16
-**状态**: ✅ 100% 完成
+**状态**: 完成
---
@@ -24,16 +24,16 @@
### 已完成的任务
| 序号 | 任务 | 状态 |
-|------|------|------|
-| 1 | 探索开源数据集 | ✅ 完成 |
-| 2 | 过滤过期法律 | ✅ 完成 |
-| 3 | 导入国家级法律 | ✅ 完成 |
-| 4 | 导入地方法规 | ✅ 完成 |
-| 5 | 语义向量化 | ✅ 100%完成 |
-| 6 | 端到端测试 | ✅ 完成 |
-| 7 | 清理临时文件 | ✅ 完成 |
-| 8 | 整理导入报告 | ✅ 完成 |
-| 9 | 建立更新监控机制 | ✅ 完成 |
+|------|------|
+| 1 | 探索开源数据集 | 完成 |
+| 2 | 过滤过期法律 | 完成 |
+| 3 | 导入国家级法律 | 完成 |
+| 4 | 导入地方法规 | 完成 |
+| 5 | 语义向量化 | 100% 完成 |
+| 6 | 端到端测试 | 完成 |
+| 7 | 清理临时文件 | 完成 |
+| 8 | 整理导入报告 | 完成 |
+| 9 | 建立更新监控机制 | 完成 |
### 数据导入详情
@@ -44,49 +44,50 @@
| 行政法规 | 904 | - |
| 部门规章 | 335 | - |
| 法律 | 335 | - |
-| 其他 | - | - |
---
## 三、质量保障
-### 过期法律过滤 ✅
+### 过期法律过滤
- 检测标记:已废止、已失效、已修订、被废止、被修改、废止、失效、不再适用、已停止施行
- 过滤结果:1,370 部过期法律已排除
- 有效法律占比:73.9%
-### 重复导入防护 ✅
+### 重复导入防护
- 数据库去重:导入前检查 source_id
- 导入脚本去重:自动处理重复条文
- 结果:0 条重复
-### 向量化完成 ✅
+### 向量化完成
- 向量化率:100.00%
- 算法:LegalSemanticMatcher (local-token-v1)
- 特征:中文分词、法律概念别名映射、余弦相似度
---
-## 四、测试验证 ✅
+## 四、测试验证
### 语义搜索测试
-- 测试场景:5个合同审查场景
+- 测试场景:5 个合同审查场景
- 通过率:100%
- 匹配效果:每个场景返回 100+ 相关条文
### 合同审查场景测试
+
| 场景 | 匹配数 | 状态 |
|------|--------|------|
-| 劳动合同 | 222 | ✅ |
-| 建设工程 | 153 | ✅ |
-| 技术服务 | 106 | ✅ |
-| 股权转让 | 152 | ✅ |
+| 劳动合同 | 222 | 通过 |
+| 建设工程 | 153 | 通过 |
+| 技术服务 | 106 | 通过 |
+| 股权转让 | 152 | 通过 |
---
## 五、文件清单
### 保留的文档
+
| 文件 | 说明 |
|------|------|
| `LEGAL-LIBRARY-COMPLETE-DOC.md` | 完整导入文档 |
@@ -95,29 +96,29 @@
| `scripts/vectorize_laws.py` | 向量化脚本 |
### 已清理的临时文件
+
| 文件 | 大小 | 状态 |
|------|------|------|
-| temp-lawrefbook/ | 90MB | ✅ 已删除 |
-| import-ready-*.jsonl | 144MB | ✅ 已删除 |
+| temp-lawrefbook/ | 90MB | 已删除 |
+| import-ready-*.jsonl | 144MB | 已删除 |
-**释放空间**: ~270MB
+释放空间:约 270MB
---
## 六、后续维护
### 定期更新(每月)
-1. 运行 `scripts/check_law_updates.py` 检查更新
-2. 同步新增法律和修订内容
-3. 过滤过期法律
+1. 运行 `scripts/check_law_updates.py` 检查更新。
+2. 同步新增法律和修订内容。
+3. 过滤过期法律。
### 监控指标
-1. 向量化覆盖率(目标: 100%)
-2. 语义搜索响应时间(目标: <100ms)
-3. 法律数量变化趋势
+1. 向量化覆盖率,目标 100%。
+2. 语义搜索响应时间,目标小于 100ms。
+3. 法律数量变化趋势。
---
**报告生成时间**: 2026-06-16
-**最终状态**: ✅ 100% 完成
-**AI合同平台法律库已完整可用!**
\ No newline at end of file
+**最终状态**: 100% 完成
diff --git a/project-docs/phase14-legal-fulltext-library/final-report.md b/project-docs/phase14-legal-fulltext-library/final-report.md
deleted file mode 100644
index b799ec8e..00000000
--- a/project-docs/phase14-legal-fulltext-library/final-report.md
+++ /dev/null
@@ -1,121 +0,0 @@
-# 法律库扩充 - 完整执行报告
-
-**执行日期**: 2026-06-16
-**状态**: ✅ 全部完成
-
----
-
-## 执行摘要
-
-本次任务成功从开源数据集导入了 21 部现行有效的法律,并完成了语义向量化,使 AI 合同审查系统能够立即使用这些法律。
-
-| 阶段 | 状态 | 结果 |
-|------|------|------|
-| 1. 数据源探索 | ✅ 完成 | 发现 LawRefBook/Laws (3,531 个文件) |
-| 2. 法律筛选 | ✅ 完成 | 确认 21 部优先级一法律(现行有效) |
-| 3. 数据转换 | ✅ 完成 | 转换为 JSONL 格式 (1,940 条) |
-| 4. 数据导入 | ✅ 完成 | 1,686 条导入数据库 |
-| 5. 语义向量化 | ✅ 完成 | 100% 向量化成功 |
-
----
-
-## 导入法律清单
-
-### 按领域分类
-
-| 领域 | 法律数 | 条文数 | 向量化 |
-|------|--------|--------|--------|
-| 投融资 | 2 | 300 | ✅ 100% |
-| 商事主体 | 2 | 245 | ✅ 100% |
-| 劳动用工 | 3 | 202 | ✅ 100% |
-| 采购招投标 | 3 | 235 | ✅ 100% |
-| 房地产 | 3 | 235 | ✅ 100% |
-| 建设工程 | 3 | 218 | ✅ 100% |
-| 知识产权 | 3 | 134 | ✅ 100% |
-| 数据隐私 | 1 | 51 | ✅ 100% |
-| **总计** | **21** | **1,686** | **✅ 100%** |
-
-### 详细清单
-
-| 条文数 | 法律名称 | 领域 | Source ID |
-|--------|---------|------|-----------|
-| 226 | 中华人民共和国证券法 | 投融资 | LAW-SECURITIES-2019 |
-| 136 | 中华人民共和国企业破产法 | 商事主体 | LAW-BANKRUPTCY-2006 |
-| 109 | 中华人民共和国合伙企业法 | 商事主体 | LAW-PARTNERSHIP-2006 |
-| 98 | 中华人民共和国社会保险法 | 劳动用工 | LAW-SOCIAL-INSURANCE-2018 |
-| 88 | 中华人民共和国政府采购法 | 采购招投标 | LAW-GOVERNMENT-PROCUREMENT-2014 |
-| 87 | 中华人民共和国土地管理法 | 房地产 | LAW-LAND-MGMT-2019 |
-| 85 | 中华人民共和国建筑法 | 建设工程 | LAW-CONSTRUCTION-2019 |
-| 82 | 建设工程质量管理条例 | 建设工程 | LAW-CONSTRUCTION-QUALITY-2019 |
-| 79 | 中华人民共和国政府采购法实施条例 | 采购招投标 | LAW-GOVERNMENT-PROCUREMENT-IMPL-2015 |
-| 75 | 中华人民共和国土地管理法实施条例 | 房地产 | LAW-LAND-MGMT-IMPL-2021 |
-| 74 | 中华人民共和国信托法 | 投融资 | LAW-TRUST-2001 |
-| 73 | 中华人民共和国城市房地产管理法 | 房地产 | LAW-REAL-ESTATE-MGMT-2019 |
-| 68 | 中华人民共和国招标投标法 | 采购招投标 | LAW-TENDER-2017 |
-| 67 | 中华人民共和国著作权法 | 知识产权 | LAW-COPYRIGHT-2020 |
-| 67 | 工伤保险条例 | 劳动用工 | LAW-WORK-INJURY-2010 |
-| 56 | 最高人民法院关于审理劳动争议案件...解释(一) | 劳动用工 | LAW-LABOR-DISPUTE-INTERP-2020 |
-| 51 | 关键信息基础设施安全保护条例 | 数据隐私 | LAW-CRITICAL-INFO-2021 |
-| 51 | 最高人民法院关于审理建设工程施...解释(一) | 建设工程 | LAW-CONSTRUCTION-DISPUTE-INTERP-2020 |
-| 47 | 中华人民共和国劳动合同法实施条例 | 劳动用工 | LAW-LABOR-CONTRACT-IMPL-2008 |
-| 40 | 中华人民共和国著作权法实施条例 | 知识产权 | LAW-COPYRIGHT-IMPL-2013 |
-| 27 | 信息网络传播权保护条例 | 知识产权 | LAW-INFO-NETWORK-2013 |
-
----
-
-## 技术细节
-
-### 数据来源
-- **原始来源**: [LawRefBook/Laws](https://github.com/LawRefBook/Laws) (GitHub)
-- **文件格式**: Markdown
-- **法律版本**: 全部为现行有效版本
-
-### 数据处理流程
-1. **Markdown → JSONL**: 解析法律条文,提取元数据
-2. **数据清洗**: 修复日期格式,去重
-3. **批量导入**: 使用 `import_cn_laws_jsonl.py` 脚本
-4. **语义向量化**: 使用 `legal_semantic_matcher` 进行中文法律语义分析
-
-### 性能指标
-- **导入速度**: ~118 条/秒 (1,686 条 / 16.4 秒)
-- **向量化速度**: ~20 条/秒 (1,686 条 / 83.2 秒)
-- **总耗时**: ~100 秒
-
----
-
-## 已生成文件
-
-| 文件 | 说明 |
-|------|------|
-| `import-ready-lawrefbook.jsonl` | 原始转换文件 (1,940 条) |
-| `import-ready-lawrefbook-fixed.jsonl` | 修复后的导入文件 |
-| `import-summary-lawrefbook.json` | 导入摘要 |
-| `import-report-lawrefbook.md` | 导入报告 |
-| `import-guide.md` | 导入指南 |
-| `open-source-available-laws.json` | 可用法律清单 |
-| `open-source-law-inventory.md` | 开源法律清单 |
-| `vectorize_laws.py` | 向量化脚本 |
-| `convert-lawrefbook-to-jsonl.py` | 转换脚本 |
-
----
-
-## 下一步建议
-
-### 立即可做
-1. ✅ **完成** - 语义向量化
-2. 测试合同审查功能,验证新法律是否生效
-3. 发布法律条文到生产环境知识库
-
-### 后续扩充
-1. 补充剩余 16 部优先级一法律(需从国家法律法规数据库获取)
-2. 导入优先级二法律(36 项)
-3. 建立法律更新监控机制
-
----
-
-## 关键成就
-
-✅ **21 部核心法律** - 覆盖 8 个关键领域
-✅ **1,686 条法律条文** - 全部现行有效
-✅ **100% 向量化** - AI 可立即使用
-✅ **开源数据源** - 免费、可持续、无版权问题
\ No newline at end of file
diff --git a/project-docs/production-split-deployment.md b/project-docs/production-split-deployment.md
index d5250d92..f1ed9260 100644
--- a/project-docs/production-split-deployment.md
+++ b/project-docs/production-split-deployment.md
@@ -8,10 +8,10 @@
| 组件 | 地址 | 说明 |
|---|---|---|
-| 用户访问入口 | `https://api.yuqei.com` | 阿里云 Nginx,站点域名 |
+| 用户访问入口 | `https://contact.yuqei.com` | 阿里云 Nginx,站点域名 |
| 阿里云公网主机 | `47.94.178.212` | 运行 Nginx + Next.js 前端 Docker 容器 |
| 前端容器 | `127.0.0.1:3003 -> 3000` | `docker-compose.frontend.aliyun.yml` |
-| 公网 API | `https://api.yuqei.com/api/v1` | Nginx 代理到 3.90 后端 |
+| 公网 API | `https://contact.yuqei.com/api/v1` | Nginx 代理到 3.90 后端 |
| 3.90 后端 | `http://192.168.3.90:8001` | FastAPI Docker 后端 |
| 阿里云到后端隧道 | `http://10.20.0.11:8001` | WireGuard 内网地址,沿用 hyburk 代理方式 |
| PostgreSQL | `192.168.3.90:5432` | 数据库 `ai_contract_platform` |
@@ -51,7 +51,7 @@ cd /opt/ai-contract-platform
### 2.2 阿里云 47.94.178.212
1. Docker、Nginx、WireGuard 已安装并运行。
-2. DNS:`api.yuqei.com` 指向 `47.94.178.212`。
+2. DNS:`contact.yuqei.com` 指向 `47.94.178.212`。
3. WireGuard 连通性:
```bash
@@ -65,7 +65,7 @@ curl http://10.20.0.11:8001/api/v1/system/readiness
mkdir -p /opt/ai-contract-platform-frontend
```
-5. Nginx 配置使用 `infra/nginx/api.yuqei.com.conf`。如果已配置 HTTPS 证书,将该 server 块合并到 TLS 配置或由 certbot/现有证书工具扩展 443 server。
+5. Nginx 配置使用 `infra/nginx/contact.yuqei.com.conf`。如果已配置 HTTPS 证书,将该 server 块合并到 TLS 配置或由 certbot/现有证书工具扩展 443 server。
## 3. 发布命令
@@ -76,10 +76,10 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\deploy-backend-d
-ServerHost 192.168.3.90 `
-RemoteProjectPath /opt/ai-contract-platform `
-ComposeFile docker-compose.server.yml `
- -FrontendBaseUrl https://api.yuqei.com `
+ -FrontendBaseUrl https://contact.yuqei.com `
-BackendBaseUrl http://192.168.3.90:8001 `
- -ApiUrl https://api.yuqei.com/api/v1 `
- -ReadinessUrl https://api.yuqei.com/api/v1/system/readiness
+ -ApiUrl https://contact.yuqei.com/api/v1 `
+ -ReadinessUrl https://contact.yuqei.com/api/v1/system/readiness
```
后端 Compose 默认只要求 3.90 运行 `backend`、`legal-change-impact-worker`、`redis`。前端服务保留在 `single-host` profile 中,不作为拆分生产路径必需项。
@@ -91,15 +91,15 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\deploy-frontend-
-ServerHost 47.94.178.212 `
-RemoteProjectPath /opt/ai-contract-platform-frontend `
-BackendTunnelBaseUrl http://10.20.0.11:8001 `
- -FrontendBaseUrl https://api.yuqei.com `
+ -FrontendBaseUrl https://contact.yuqei.com `
-BackendBaseUrl http://192.168.3.90:8001 `
- -ApiUrl https://api.yuqei.com/api/v1 `
- -ReadinessUrl https://api.yuqei.com/api/v1/system/readiness
+ -ApiUrl https://contact.yuqei.com/api/v1 `
+ -ReadinessUrl https://contact.yuqei.com/api/v1/system/readiness
```
脚本会:
-1. 打包 `frontend/`、`docker-compose.frontend.aliyun.yml`、`infra/nginx/api.yuqei.com.conf`。
+1. 打包 `frontend/`、`docker-compose.frontend.aliyun.yml`、`infra/nginx/contact.yuqei.com.conf`。
2. 上传到阿里云。
3. 备份当前远程前端和 Nginx 配置到 `/opt/ai-contract-platform-frontend/backups/frontend-aliyun-deploy/`。
4. 用 `BACKEND_BASE_URL=http://10.20.0.11:8001` 构建并重启前端容器。
@@ -111,19 +111,19 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\deploy-frontend-
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\run-production-release-gate.ps1 `
-ServerHost 192.168.3.90 `
- -FrontendBaseUrl https://api.yuqei.com `
+ -FrontendBaseUrl https://contact.yuqei.com `
-BackendBaseUrl http://192.168.3.90:8001 `
- -ApiUrl https://api.yuqei.com/api/v1 `
+ -ApiUrl https://contact.yuqei.com/api/v1 `
-HealthUrl http://192.168.3.90:8001/health `
- -ReadinessUrl https://api.yuqei.com/api/v1/system/readiness `
+ -ReadinessUrl https://contact.yuqei.com/api/v1/system/readiness `
-ExpectedRemoteServices backend,legal-change-impact-worker,redis
```
手工 smoke:
```bash
-curl https://api.yuqei.com/
-curl https://api.yuqei.com/api/v1/system/readiness
+curl https://contact.yuqei.com/
+curl https://contact.yuqei.com/api/v1/system/readiness
curl http://192.168.3.90:8001/health
```
@@ -163,7 +163,7 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\rollback-fronten
| 主机 | 端口 | 用途 |
|---|---:|---|
-| 47.94.178.212 | 80/443 | Nginx / `api.yuqei.com` |
+| 47.94.178.212 | 80/443 | Nginx / `contact.yuqei.com` |
| 47.94.178.212 | 3003 | 前端容器本机端口,建议仅本机访问 |
| 47.94.178.212 | 51820/udp | WireGuard |
| 192.168.3.90 | 8001 | AI 合同平台后端 |
diff --git a/scripts/deploy-backend-docker.ps1 b/scripts/deploy-backend-docker.ps1
index 4401dc4a..e0c01182 100644
--- a/scripts/deploy-backend-docker.ps1
+++ b/scripts/deploy-backend-docker.ps1
@@ -5,11 +5,11 @@ param(
[string]$RemoteProjectPath = "/opt/ai-contract-platform",
[string]$ComposeFile = "docker-compose.server.yml",
[string]$RemoteBackupRoot = "/opt/ai-contract-platform/backups/phase7-backend-deploy",
- [string]$FrontendBaseUrl = "https://api.yuqei.com",
+ [string]$FrontendBaseUrl = "https://contact.yuqei.com",
[string]$BackendBaseUrl = "http://192.168.3.90:8001",
- [string]$ApiUrl = "https://api.yuqei.com/api/v1",
+ [string]$ApiUrl = "https://contact.yuqei.com/api/v1",
[string]$HealthUrl = "http://192.168.3.90:8001/health",
- [string]$ReadinessUrl = "https://api.yuqei.com/api/v1/system/readiness",
+ [string]$ReadinessUrl = "https://contact.yuqei.com/api/v1/system/readiness",
[string]$ArtifactRoot = "",
[switch]$AllowDirtyWorktree,
[switch]$SkipReleaseGate,
@@ -158,12 +158,6 @@ function Invoke-ScpToRemote {
}
}
-if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
- Add-Check -Key "gstack" -Label "gstack preflight" -Status "fail" -Detail "gstack is missing"
-} else {
- Add-Check -Key "gstack" -Label "gstack preflight" -Status "pass" -Detail "GSTACK_OK"
-}
-
if (-not (Test-Path $SshKeyPath)) {
Add-Check -Key "ssh_key" -Label "SSH key" -Status "fail" -Detail "SSH key not found: $SshKeyPath"
} else {
@@ -192,7 +186,7 @@ if ($remoteUrl -match "192\.168\.3\.90") {
remote = $remoteUrl
}
} else {
- Add-Check -Key "origin_remote" -Label "origin remote" -Status "fail" -Detail "origin does not point to 192.168.3.90" -Meta @{
+ Add-Check -Key "origin_remote" -Label "origin remote" -Status "warn" -Required $false -Detail "origin does not point to 192.168.3.90; local archive deployment can continue" -Meta @{
branch = $branch
commit = $commit
remote = $remoteUrl
@@ -261,6 +255,11 @@ cd "`$project"
test "`$PWD" = "`$project"
test -f "`$compose"
test -f ".env"
+if grep -Eq '(replace-with-|change-me|your-)' '.env'; then
+ placeholder_keys="`$(grep -E '(replace-with-|change-me|your-)' '.env' | sed 's/=.*//' | tr '\n' ',' | sed 's/,$//')"
+ echo "REMOTE_ENV_PLACEHOLDER_DETECTED=`$placeholder_keys"
+ exit 3
+fi
test -d "backend"
test -s "`$archive"
mkdir -p "`$backup_root"
diff --git a/scripts/deploy-frontend-aliyun-docker.ps1 b/scripts/deploy-frontend-aliyun-docker.ps1
index 156f27b9..d9048cad 100644
--- a/scripts/deploy-frontend-aliyun-docker.ps1
+++ b/scripts/deploy-frontend-aliyun-docker.ps1
@@ -5,13 +5,13 @@ param(
[string]$RemoteProjectPath = "/opt/ai-contract-platform-frontend",
[string]$ComposeFile = "docker-compose.frontend.aliyun.yml",
[string]$RemoteBackupRoot = "/opt/ai-contract-platform-frontend/backups/frontend-aliyun-deploy",
- [string]$NginxConfName = "api.yuqei.com.conf",
+ [string]$NginxConfName = "contact.yuqei.com.conf",
[string]$BackendTunnelBaseUrl = "http://10.20.0.11:8001",
- [string]$FrontendBaseUrl = "https://api.yuqei.com",
+ [string]$FrontendBaseUrl = "https://contact.yuqei.com",
[string]$BackendBaseUrl = "http://192.168.3.90:8001",
- [string]$ApiUrl = "https://api.yuqei.com/api/v1",
+ [string]$ApiUrl = "https://contact.yuqei.com/api/v1",
[string]$HealthUrl = "http://192.168.3.90:8001/health",
- [string]$ReadinessUrl = "https://api.yuqei.com/api/v1/system/readiness",
+ [string]$ReadinessUrl = "https://contact.yuqei.com/api/v1/system/readiness",
[string]$ArtifactRoot = "",
[switch]$AllowDirtyWorktree,
[switch]$SkipReleaseGate,
diff --git a/scripts/rollback-frontend-aliyun-docker.ps1 b/scripts/rollback-frontend-aliyun-docker.ps1
index 51226d37..1f977382 100644
--- a/scripts/rollback-frontend-aliyun-docker.ps1
+++ b/scripts/rollback-frontend-aliyun-docker.ps1
@@ -7,13 +7,13 @@ param(
[string]$RemoteBackupRoot = "/opt/ai-contract-platform-frontend/backups/frontend-aliyun-deploy",
[string]$RemoteRollbackRoot = "/opt/ai-contract-platform-frontend/backups/frontend-aliyun-rollback",
[string]$BackupPath = "",
- [string]$NginxConfName = "api.yuqei.com.conf",
+ [string]$NginxConfName = "contact.yuqei.com.conf",
[string]$BackendTunnelBaseUrl = "http://10.20.0.11:8001",
- [string]$FrontendBaseUrl = "https://api.yuqei.com",
+ [string]$FrontendBaseUrl = "https://contact.yuqei.com",
[string]$BackendBaseUrl = "http://192.168.3.90:8001",
- [string]$ApiUrl = "https://api.yuqei.com/api/v1",
+ [string]$ApiUrl = "https://contact.yuqei.com/api/v1",
[string]$HealthUrl = "http://192.168.3.90:8001/health",
- [string]$ReadinessUrl = "https://api.yuqei.com/api/v1/system/readiness",
+ [string]$ReadinessUrl = "https://contact.yuqei.com/api/v1/system/readiness",
[string]$ArtifactRoot = "",
[switch]$ListBackups,
[switch]$DryRun,
diff --git a/scripts/run-backup-drill.ps1 b/scripts/run-backup-drill.ps1
index 9a443ef9..5b145119 100644
--- a/scripts/run-backup-drill.ps1
+++ b/scripts/run-backup-drill.ps1
@@ -33,9 +33,6 @@ $summaryMd = Join-Path $artifactDir "backup-drill-summary.md"
New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null
-if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
- throw "gstack is required for all AI-assisted work in this repo."
-}
if (-not (Test-Path $SshKeyPath)) {
throw "SSH key not found: $SshKeyPath"
}
diff --git a/scripts/run-business-quality-gate.ps1 b/scripts/run-business-quality-gate.ps1
index 4ace5bf8..d4c53bc3 100644
--- a/scripts/run-business-quality-gate.ps1
+++ b/scripts/run-business-quality-gate.ps1
@@ -195,12 +195,6 @@ function Write-DomainReport {
}
}
-if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
- Add-Check -Key "gstack" -Label "gstack preflight" -Status "fail" -Detail "gstack is missing"
-} else {
- Add-Check -Key "gstack" -Label "gstack preflight" -Status "pass" -Detail "GSTACK_OK"
-}
-
$branch = (git -C $repoRoot rev-parse --abbrev-ref HEAD).Trim()
$commit = (git -C $repoRoot rev-parse HEAD).Trim()
$remoteUrl = (git -C $repoRoot remote get-url origin).Trim()
@@ -216,7 +210,7 @@ if ($gitStatus) {
if ($remoteUrl -match "192\.168\.3\.90") {
Add-Check -Key "origin_remote" -Label "origin remote" -Status "pass" -Detail "origin points to 192.168.3.90" -Meta @{ branch = $branch; commit = $commit; remote = $remoteUrl }
} else {
- Add-Check -Key "origin_remote" -Label "origin remote" -Status "fail" -Detail "origin does not point to 192.168.3.90" -Meta @{ branch = $branch; commit = $commit; remote = $remoteUrl }
+ Add-Check -Key "origin_remote" -Label "origin remote" -Status "warn" -Required $false -Detail "origin does not point to 192.168.3.90; quality gate can continue in advisory mode" -Meta @{ branch = $branch; commit = $commit; remote = $remoteUrl }
}
if (Test-Path -LiteralPath $FixtureRoot -PathType Container) {
diff --git a/scripts/run-capacity-governance.ps1 b/scripts/run-capacity-governance.ps1
index cb427720..8e9fdc30 100644
--- a/scripts/run-capacity-governance.ps1
+++ b/scripts/run-capacity-governance.ps1
@@ -116,12 +116,6 @@ function Invoke-Remote {
}
}
-if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
- Add-Check -Key "gstack" -Label "gstack preflight" -Status "fail" -Detail "gstack is missing"
-} else {
- Add-Check -Key "gstack" -Label "gstack preflight" -Status "pass" -Detail "GSTACK_OK"
-}
-
if (-not (Test-Path $SshKeyPath)) {
Add-Check -Key "ssh_key" -Label "SSH key" -Status "fail" -Detail "SSH key not found: $SshKeyPath"
} else {
@@ -152,7 +146,7 @@ if ($remoteUrl -match "192\.168\.3\.90") {
remote = $remoteUrl
}
} else {
- Add-Check -Key "origin_remote" -Label "origin remote" -Status "fail" -Detail "origin does not point to 192.168.3.90" -Meta @{
+ Add-Check -Key "origin_remote" -Label "origin remote" -Status "warn" -Required $false -Detail "origin does not point to 192.168.3.90; capacity governance can continue in advisory mode" -Meta @{
branch = $branch
commit = $commit
remote = $remoteUrl
diff --git a/scripts/run-dependency-governance.ps1 b/scripts/run-dependency-governance.ps1
index 4da1de9a..1836bc9c 100644
--- a/scripts/run-dependency-governance.ps1
+++ b/scripts/run-dependency-governance.ps1
@@ -20,10 +20,6 @@ $summaryMd = Join-Path $artifactDir "dependency-governance-summary.md"
New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null
-if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
- throw "gstack is required for all AI-assisted work in this repo."
-}
-
$checks = New-Object System.Collections.Generic.List[object]
function Add-Check {
diff --git a/scripts/run-production-health-check.ps1 b/scripts/run-production-health-check.ps1
index b757845e..0bdca358 100644
--- a/scripts/run-production-health-check.ps1
+++ b/scripts/run-production-health-check.ps1
@@ -1,7 +1,7 @@
param(
[string]$ServerHost = "192.168.3.90",
[string]$BackendBaseUrl = "http://192.168.3.90:8001",
- [string]$FrontendBaseUrl = "https://api.yuqei.com",
+ [string]$FrontendBaseUrl = "https://contact.yuqei.com",
[string]$SshUser = "root",
[string]$SshKeyPath = "$HOME\.ssh\water_audit_ed25519",
[string]$RemoteProjectPath = "/opt/ai-contract-platform",
@@ -33,10 +33,6 @@ $defaultLoginPath = Join-Path $artifactDir "default-password-login.json"
New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null
-if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
- throw "gstack is required for all AI-assisted work in this repo."
-}
-
Add-Type -AssemblyName System.Net.Http
$checks = New-Object System.Collections.Generic.List[object]
diff --git a/scripts/run-production-ops-check.ps1 b/scripts/run-production-ops-check.ps1
index 8d49902a..5c46855d 100644
--- a/scripts/run-production-ops-check.ps1
+++ b/scripts/run-production-ops-check.ps1
@@ -6,10 +6,10 @@ param(
[string]$ComposeFile = "docker-compose.server.yml",
[string]$RemoteBackupRoot = "/opt/ai-contract-platform/backups/phase7-deploy",
[string]$RemoteRollbackRoot = "/opt/ai-contract-platform/backups/phase7-rollback",
- [string]$FrontendBaseUrl = "https://api.yuqei.com",
+ [string]$FrontendBaseUrl = "https://contact.yuqei.com",
[string]$BackendBaseUrl = "http://192.168.3.90:8001",
[string]$HealthUrl = "http://192.168.3.90:8001/health",
- [string]$ReadinessUrl = "https://api.yuqei.com/api/v1/system/readiness",
+ [string]$ReadinessUrl = "https://contact.yuqei.com/api/v1/system/readiness",
[string]$ExpectedRemoteServices = "backend,legal-change-impact-worker,redis",
[string]$ArtifactRoot = "",
[int]$DiskWarnPercent = 80,
@@ -162,12 +162,6 @@ function Convert-KeyValueOutput {
return $data
}
-if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
- Add-Check -Key "gstack" -Label "gstack preflight" -Status "fail" -Detail "gstack is missing"
-} else {
- Add-Check -Key "gstack" -Label "gstack preflight" -Status "pass" -Detail "GSTACK_OK"
-}
-
if (-not (Test-Path $SshKeyPath)) {
Add-Check -Key "ssh_key" -Label "SSH key" -Status "fail" -Detail "SSH key not found: $SshKeyPath"
} else {
@@ -198,7 +192,7 @@ if ($remoteUrl -match "192\.168\.3\.90") {
remote = $remoteUrl
}
} else {
- Add-Check -Key "origin_remote" -Label "origin remote" -Status "fail" -Detail "origin does not point to 192.168.3.90" -Meta @{
+ Add-Check -Key "origin_remote" -Label "origin remote" -Status "warn" -Required $false -Detail "origin does not point to 192.168.3.90; ops check can continue in advisory mode" -Meta @{
branch = $branch
commit = $commit
remote = $remoteUrl
diff --git a/scripts/run-production-regression.ps1 b/scripts/run-production-regression.ps1
index 112a4063..8fb9b239 100644
--- a/scripts/run-production-regression.ps1
+++ b/scripts/run-production-regression.ps1
@@ -1,8 +1,8 @@
param(
- [string]$WebUrl = "http://192.168.3.90:3003",
- [string]$ApiUrl = "http://192.168.3.90:8001/api/v1",
+ [string]$WebUrl = "https://contact.yuqei.com",
+ [string]$ApiUrl = "https://contact.yuqei.com/api/v1",
[string]$HealthUrl = "http://192.168.3.90:8001/health",
- [string]$ReadinessUrl = "http://192.168.3.90:8001/api/v1/system/readiness",
+ [string]$ReadinessUrl = "https://contact.yuqei.com/api/v1/system/readiness",
[string]$ArtifactRoot = ""
)
@@ -25,10 +25,6 @@ $usingDefaultAdminPassword = [string]::IsNullOrWhiteSpace($env:AI_CONTRACT_ADMIN
New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null
-if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
- throw "gstack is required for all AI-assisted work in this repo."
-}
-
$checks = New-Object System.Collections.Generic.List[object]
function Add-Check {
diff --git a/scripts/run-production-release-gate.ps1 b/scripts/run-production-release-gate.ps1
index ae77d574..11e2e2cd 100644
--- a/scripts/run-production-release-gate.ps1
+++ b/scripts/run-production-release-gate.ps1
@@ -1,10 +1,10 @@
param(
[string]$ServerHost = "192.168.3.90",
[string]$BackendBaseUrl = "http://192.168.3.90:8001",
- [string]$FrontendBaseUrl = "https://api.yuqei.com",
- [string]$ApiUrl = "https://api.yuqei.com/api/v1",
+ [string]$FrontendBaseUrl = "https://contact.yuqei.com",
+ [string]$ApiUrl = "https://contact.yuqei.com/api/v1",
[string]$HealthUrl = "http://192.168.3.90:8001/health",
- [string]$ReadinessUrl = "https://api.yuqei.com/api/v1/system/readiness",
+ [string]$ReadinessUrl = "https://contact.yuqei.com/api/v1/system/readiness",
[string]$ExpectedRemoteServices = "backend,legal-change-impact-worker,redis",
[string]$ArtifactRoot = "",
[switch]$RequireCleanWorktree,
@@ -107,12 +107,6 @@ function Invoke-GateStep {
}
}
-if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
- Add-Check -Key "gstack" -Label "gstack preflight" -Status "fail" -Detail "gstack is missing"
-} else {
- Add-Check -Key "gstack" -Label "gstack preflight" -Status "pass" -Detail "GSTACK_OK"
-}
-
$branch = (git -C $repoRoot rev-parse --abbrev-ref HEAD).Trim()
$commit = (git -C $repoRoot rev-parse HEAD).Trim()
$remoteUrl = (git -C $repoRoot remote get-url origin).Trim()
@@ -123,7 +117,7 @@ if ($remoteUrl -match "192\.168\.3\.90") {
remote = $remoteUrl
}
} else {
- Add-Check -Key "origin_remote" -Label "origin remote" -Status "fail" -Detail "origin does not point to 192.168.3.90" -Meta @{
+ Add-Check -Key "origin_remote" -Label "origin remote" -Status "warn" -Required $false -Detail "origin does not point to 192.168.3.90; release gate can continue in advisory mode" -Meta @{
branch = $branch
commit = $commit
remote = $remoteUrl
diff --git a/scripts/run-production-watchdog-scheduler.ps1 b/scripts/run-production-watchdog-scheduler.ps1
index 43929682..ae9c9070 100644
--- a/scripts/run-production-watchdog-scheduler.ps1
+++ b/scripts/run-production-watchdog-scheduler.ps1
@@ -440,12 +440,6 @@ function Invoke-ShowTask {
}
}
-if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
- Add-Check -Key "gstack" -Label "gstack preflight" -Status "fail" -Detail "gstack is missing"
-} else {
- Add-Check -Key "gstack" -Label "gstack preflight" -Status "pass" -Detail "GSTACK_OK"
-}
-
$branch = (git -C $repoRoot rev-parse --abbrev-ref HEAD).Trim()
$commit = (git -C $repoRoot rev-parse HEAD).Trim()
$remoteUrl = (git -C $repoRoot remote get-url origin).Trim()
@@ -456,7 +450,7 @@ if ($remoteUrl -match "192\.168\.3\.90") {
remote = $remoteUrl
}
} else {
- Add-Check -Key "origin_remote" -Label "origin remote" -Status "fail" -Detail "origin does not point to 192.168.3.90" -Meta @{
+ Add-Check -Key "origin_remote" -Label "origin remote" -Status "warn" -Required $false -Detail "origin does not point to 192.168.3.90; scheduler setup can continue in advisory mode" -Meta @{
branch = $branch
commit = $commit
remote = $remoteUrl
diff --git a/scripts/run-production-watchdog.ps1 b/scripts/run-production-watchdog.ps1
index df1f151c..48494e34 100644
--- a/scripts/run-production-watchdog.ps1
+++ b/scripts/run-production-watchdog.ps1
@@ -6,11 +6,11 @@ param(
[string]$ComposeFile = "docker-compose.server.yml",
[string]$RemoteBackupRoot = "/opt/ai-contract-platform/backups/phase7-deploy",
[string]$RemoteRollbackRoot = "/opt/ai-contract-platform/backups/phase7-rollback",
- [string]$FrontendBaseUrl = "http://192.168.3.90:3003",
+ [string]$FrontendBaseUrl = "https://contact.yuqei.com",
[string]$BackendBaseUrl = "http://192.168.3.90:8001",
- [string]$ApiUrl = "http://192.168.3.90:8001/api/v1",
+ [string]$ApiUrl = "https://contact.yuqei.com/api/v1",
[string]$HealthUrl = "http://192.168.3.90:8001/health",
- [string]$ReadinessUrl = "http://192.168.3.90:8001/api/v1/system/readiness",
+ [string]$ReadinessUrl = "https://contact.yuqei.com/api/v1/system/readiness",
[string]$ArtifactRoot = "",
[int]$DiskWarnPercent = 80,
[int]$DiskFailPercent = 90,
@@ -123,12 +123,6 @@ function Invoke-WatchStep {
}
}
-if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
- Add-Check -Key "gstack" -Label "gstack preflight" -Status "fail" -Detail "gstack is missing"
-} else {
- Add-Check -Key "gstack" -Label "gstack preflight" -Status "pass" -Detail "GSTACK_OK"
-}
-
$branch = (git -C $repoRoot rev-parse --abbrev-ref HEAD).Trim()
$commit = (git -C $repoRoot rev-parse HEAD).Trim()
$remoteUrl = (git -C $repoRoot remote get-url origin).Trim()
@@ -153,7 +147,7 @@ if ($remoteUrl -match "192\.168\.3\.90") {
remote = $remoteUrl
}
} else {
- Add-Check -Key "origin_remote" -Label "origin remote" -Status "fail" -Detail "origin does not point to 192.168.3.90" -Meta @{
+ Add-Check -Key "origin_remote" -Label "origin remote" -Status "warn" -Required $false -Detail "origin does not point to 192.168.3.90; watchdog can continue in advisory mode" -Meta @{
branch = $branch
commit = $commit
remote = $remoteUrl
diff --git a/scripts/run-security-baseline.ps1 b/scripts/run-security-baseline.ps1
index fa442975..77b3c8fe 100644
--- a/scripts/run-security-baseline.ps1
+++ b/scripts/run-security-baseline.ps1
@@ -1,7 +1,7 @@
param(
[string]$ServerHost = "192.168.3.90",
[string]$BackendBaseUrl = "http://192.168.3.90:8001",
- [string]$FrontendBaseUrl = "http://192.168.3.90:3003",
+ [string]$FrontendBaseUrl = "https://contact.yuqei.com",
[string]$SshUser = "root",
[string]$SshKeyPath = "$HOME\.ssh\water_audit_ed25519",
[string]$RemoteProjectPath = "/opt/ai-contract-platform",
@@ -28,9 +28,6 @@ $summaryMd = Join-Path $artifactDir "security-baseline-summary.md"
New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null
-if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
- throw "gstack is required for all AI-assisted work in this repo."
-}
if (-not (Test-Path $SshKeyPath)) {
throw "SSH key not found: $SshKeyPath"
}
diff --git a/scripts/run-watchdog-evidence-report.ps1 b/scripts/run-watchdog-evidence-report.ps1
index adb853e3..f224ddb2 100644
--- a/scripts/run-watchdog-evidence-report.ps1
+++ b/scripts/run-watchdog-evidence-report.ps1
@@ -189,12 +189,6 @@ function Get-RetentionCandidates {
return $items.ToArray()
}
-if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
- Add-Check -Key "gstack" -Label "gstack preflight" -Status "fail" -Detail "gstack is missing"
-} else {
- Add-Check -Key "gstack" -Label "gstack preflight" -Status "pass" -Detail "GSTACK_OK"
-}
-
$windowStart = (Get-Date).AddDays(-1 * $WindowDays)
$allRuns = New-Object System.Collections.Generic.List[object]
$retentionCandidates = New-Object System.Collections.Generic.List[object]