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

1
app/core/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Core infrastructure."""

60
app/core/config.py Normal file
View File

@@ -0,0 +1,60 @@
from functools import lru_cache
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""Runtime settings loaded from environment variables and `.env`."""
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
app_name: str = "Company AI Management Platform"
app_env: str = "local"
debug: bool = False
api_prefix: str = "/api/v1"
api_key: str | None = None
cors_origins: list[str] = Field(default_factory=lambda: ["*"])
database_url: str = "mysql+pymysql://root:password@127.0.0.1:3306/company_ai?charset=utf8mb4"
legacy_database_url: str | None = None
legacy_project_query: str | None = None
legacy_project_code_prefix: str = "LEGACY"
redis_url: str = "redis://127.0.0.1:6379/0"
feishu_base_url: str = "https://open.feishu.cn/open-apis"
feishu_app_id: str | None = None
feishu_app_secret: str | None = None
feishu_verification_token: str | None = None
feishu_encrypt_key: str | None = None
feishu_default_chat_id: str | None = None
model_provider: str = "noop"
openclaw_base_url: str = "http://127.0.0.1:18789"
openclaw_api_key: str | None = None
hermes_base_url: str = "http://127.0.0.1:8080"
hermes_api_key: str | None = None
direct_llm_base_url: str = "https://api.openai.com/v1"
direct_llm_api_key: str | None = None
direct_llm_model: str = "gpt-4.1-mini"
scheduler_enabled: bool = False
daily_brief_cron_hour: int = 9
daily_brief_cron_minute: int = 0
weekly_project_report_day_of_week: str = "mon"
weekly_project_report_cron_hour: int = 9
weekly_project_report_cron_minute: int = 30
@field_validator("cors_origins", mode="before")
@classmethod
def parse_cors_origins(cls, value: str | list[str]) -> list[str]:
if isinstance(value, list):
return value
return [item.strip() for item in value.split(",") if item.strip()]
@lru_cache
def get_settings() -> Settings:
"""Return cached application settings."""
return Settings()

51
app/core/database.py Normal file
View File

@@ -0,0 +1,51 @@
from collections.abc import Generator
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
from app.core.config import get_settings
class Base(DeclarativeBase):
"""Base class for SQLAlchemy ORM models."""
pass
settings = get_settings()
engine = create_engine(settings.database_url, pool_pre_ping=True, pool_recycle=3600)
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, expire_on_commit=False)
legacy_engine = (
create_engine(settings.legacy_database_url, pool_pre_ping=True, pool_recycle=3600)
if settings.legacy_database_url
else None
)
LegacySessionLocal = (
sessionmaker(bind=legacy_engine, autoflush=False, autocommit=False, expire_on_commit=False)
if legacy_engine
else None
)
def get_db() -> Generator[Session, None, None]:
"""Yield an application database session for FastAPI dependencies."""
db = SessionLocal()
try:
yield db
finally:
db.close()
def get_legacy_db() -> Generator[Session, None, None]:
"""Yield a legacy database session when the legacy connection is configured."""
if LegacySessionLocal is None:
raise RuntimeError("LEGACY_DATABASE_URL is not configured")
db = LegacySessionLocal()
try:
yield db
finally:
db.close()

82
app/core/scheduler.py Normal file
View File

@@ -0,0 +1,82 @@
from fastapi import FastAPI
from app.core.config import get_settings
def attach_scheduler(app: FastAPI) -> None:
"""Attach optional APScheduler jobs to the FastAPI application."""
settings = get_settings()
if not settings.scheduler_enabled:
return
from apscheduler.schedulers.background import BackgroundScheduler
from app.core.database import SessionLocal
from app.modules.reports.service import ReportService
scheduler = BackgroundScheduler(timezone="Asia/Shanghai")
def run_daily_brief() -> None:
db = SessionLocal()
try:
report = ReportService(db).daily_brief()
app.state.last_daily_brief = report
if (
settings.feishu_app_id
and settings.feishu_app_secret
and settings.feishu_default_chat_id
):
ReportService(db).push_report(
report,
receive_id=settings.feishu_default_chat_id,
receive_id_type="chat_id",
actor="scheduler",
)
finally:
db.close()
def run_project_weekly() -> None:
db = SessionLocal()
try:
report = ReportService(db).project_weekly()
app.state.last_project_weekly = report
if (
settings.feishu_app_id
and settings.feishu_app_secret
and settings.feishu_default_chat_id
):
ReportService(db).push_report(
report,
receive_id=settings.feishu_default_chat_id,
receive_id_type="chat_id",
actor="scheduler",
)
finally:
db.close()
scheduler.add_job(
run_daily_brief,
trigger="cron",
hour=settings.daily_brief_cron_hour,
minute=settings.daily_brief_cron_minute,
id="daily_brief_push",
replace_existing=True,
)
scheduler.add_job(
run_project_weekly,
trigger="cron",
day_of_week=settings.weekly_project_report_day_of_week,
hour=settings.weekly_project_report_cron_hour,
minute=settings.weekly_project_report_cron_minute,
id="project_weekly_push",
replace_existing=True,
)
@app.on_event("startup")
def start_scheduler() -> None:
scheduler.start()
@app.on_event("shutdown")
def stop_scheduler() -> None:
scheduler.shutdown(wait=False)

13
app/core/security.py Normal file
View File

@@ -0,0 +1,13 @@
from fastapi import Header, HTTPException, status
from app.core.config import get_settings
def require_api_key(x_api_key: str | None = Header(default=None)) -> None:
"""Validate the optional internal API key header."""
settings = get_settings()
if not settings.api_key:
return
if x_api_key != settings.api_key:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API key")