feat: 添加公司AI管理平台基础架构

添加了完整的FastAPI后端项目结构,包括:
- 环境配置文件(.env.example)和项目说明文档(README.md、AGENTS.md)
- Dockerfile用于容器化部署
- 核心基础设施:配置管理、数据库连接、调度器、安全认证
- 模块化设计:AI代理、审批流程、审计日志、业务台账等功能模块
- 支持多数据库连接(主库和遗留系统只读库)
- AI适配器支持OpenClaw、Hermes、OpenAI兼容接口
- 飞书集成、报表生成、风险监控等企业级功能
- 完整的依赖管理和测试指南
```
This commit is contained in:
2026-06-21 21:57:28 +08:00
commit 71ca804764
68 changed files with 3662 additions and 0 deletions

View File

@@ -0,0 +1 @@
"""Approval workflow module."""

View File

@@ -0,0 +1,29 @@
from datetime import datetime
from sqlalchemy import DateTime, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.core.database import Base
class ApprovalRequest(Base):
__tablename__ = "approval_requests"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
ticket_id: Mapped[str] = mapped_column(String(64), unique=True, index=True)
domain: Mapped[str] = mapped_column(String(128), index=True)
record_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
action: Mapped[str] = mapped_column(String(128), index=True)
applicant: Mapped[str] = mapped_column(String(128), default="api", index=True)
approver: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
status: Mapped[str] = mapped_column(String(32), default="pending", index=True)
reason: Mapped[str | None] = mapped_column(Text, nullable=True)
payload: Mapped[str | None] = mapped_column(Text, nullable=True)
decision_comment: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
updated_at: Mapped[datetime] = mapped_column(
DateTime,
default=datetime.utcnow,
onupdate=datetime.utcnow,
)
decided_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)

View File

@@ -0,0 +1,38 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.security import require_api_key
from app.modules.approvals.schemas import ApprovalCreate, ApprovalDecision, ApprovalRead
from app.modules.approvals.service import ApprovalService
router = APIRouter(dependencies=[Depends(require_api_key)])
@router.post("", response_model=ApprovalRead)
def create_approval(payload: ApprovalCreate, db: Session = Depends(get_db)):
return ApprovalService(db).create(payload)
@router.get("", response_model=list[ApprovalRead])
def list_approvals(
status: str | None = None,
limit: int = 100,
db: Session = Depends(get_db),
):
return ApprovalService(db).list(status_filter=status, limit=limit)
@router.get("/{ticket_id}", response_model=ApprovalRead)
def get_approval(ticket_id: str, db: Session = Depends(get_db)):
return ApprovalService(db).get_by_ticket(ticket_id)
@router.post("/{ticket_id}/approve", response_model=ApprovalRead)
def approve(ticket_id: str, payload: ApprovalDecision, db: Session = Depends(get_db)):
return ApprovalService(db).decide(ticket_id, payload.approver, True, payload.comment)
@router.post("/{ticket_id}/reject", response_model=ApprovalRead)
def reject(ticket_id: str, payload: ApprovalDecision, db: Session = Depends(get_db)):
return ApprovalService(db).decide(ticket_id, payload.approver, False, payload.comment)

View File

@@ -0,0 +1,37 @@
from datetime import datetime
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
class ApprovalCreate(BaseModel):
domain: str
record_id: str | None = None
action: str
applicant: str = "api"
reason: str | None = None
payload: dict[str, Any] = Field(default_factory=dict)
class ApprovalDecision(BaseModel):
approver: str
comment: str | None = None
class ApprovalRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
ticket_id: str
domain: str
record_id: str | None
action: str
applicant: str
approver: str | None
status: str
reason: str | None
payload: str | None
decision_comment: str | None
created_at: datetime
updated_at: datetime
decided_at: datetime | None

View File

@@ -0,0 +1,111 @@
import json
import uuid
from fastapi import HTTPException, status
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.modules.approvals.models import ApprovalRequest
from app.modules.approvals.schemas import ApprovalCreate
from app.modules.audit.schemas import AuditLogCreate
from app.modules.audit.service import AuditService
class ApprovalService:
"""Create, decide, and validate approval tickets for guarded actions."""
def __init__(self, db: Session):
self.db = db
self.audit = AuditService(db)
def create(self, payload: ApprovalCreate) -> ApprovalRequest:
ticket = ApprovalRequest(
ticket_id=f"APR-{uuid.uuid4().hex[:12].upper()}",
domain=payload.domain,
record_id=payload.record_id,
action=payload.action,
applicant=payload.applicant,
reason=payload.reason,
payload=json.dumps(payload.payload, ensure_ascii=False, default=str),
)
self.db.add(ticket)
self.db.commit()
self.db.refresh(ticket)
self.audit.log(
AuditLogCreate(
actor=payload.applicant,
source="approval",
action="approval.create",
target_type=payload.domain,
target_id=payload.record_id,
risk_level="medium",
request_payload=payload.model_dump(),
response_payload={"ticket_id": ticket.ticket_id, "status": ticket.status},
)
)
return ticket
def list(self, status_filter: str | None = None, limit: int = 100) -> list[ApprovalRequest]:
stmt = select(ApprovalRequest).order_by(ApprovalRequest.id.desc()).limit(min(limit, 500))
if status_filter:
stmt = stmt.where(ApprovalRequest.status == status_filter)
return list(self.db.execute(stmt).scalars())
def get_by_ticket(self, ticket_id: str) -> ApprovalRequest:
ticket = self.db.execute(
select(ApprovalRequest).where(ApprovalRequest.ticket_id == ticket_id)
).scalar_one_or_none()
if ticket is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Approval ticket not found",
)
return ticket
def decide(
self,
ticket_id: str,
approver: str,
approved: bool,
comment: str | None,
) -> ApprovalRequest:
ticket = self.get_by_ticket(ticket_id)
if ticket.status != "pending":
raise HTTPException(status_code=409, detail="Approval ticket already decided")
ticket.status = "approved" if approved else "rejected"
ticket.approver = approver
ticket.decision_comment = comment
from datetime import datetime
ticket.decided_at = datetime.utcnow()
self.db.commit()
self.db.refresh(ticket)
self.audit.log(
AuditLogCreate(
actor=approver,
source="approval",
action="approval.approve" if approved else "approval.reject",
target_type=ticket.domain,
target_id=ticket.record_id,
risk_level="high",
request_payload={"ticket_id": ticket_id, "comment": comment},
response_payload={"status": ticket.status},
)
)
return ticket
def is_approved_for(
self,
ticket_id: str,
domain: str,
record_id: str | int | None,
action: str,
) -> bool:
ticket = self.get_by_ticket(ticket_id)
if ticket.status != "approved":
return False
if ticket.domain != domain:
return False
if ticket.record_id and record_id is not None and str(ticket.record_id) != str(record_id):
return False
return ticket.action in {action, "update", f"update:{domain}", "*"}