feat: 添加仪表板路由和响应数据脱敏功能

- 添加了仪表板模块路由并集成到主路由器中
- 实现了敏感数据响应脱敏配置和功能
- 增加了 Feishu 审批卡片操作处理功能
- 支持通过任务队列异步推送日常简报和项目周报
- 添加了风险事件生成的任务队列支持
- 在 smoke 测试中增加了相关功能验证

refactor: 格式化模型注册模块导入列表

- 将单行导入列表改为多行格式以提高可读性
```
This commit is contained in:
2026-07-07 18:33:07 +08:00
parent fbd0aaa9e4
commit 4d09d8e2e3
21 changed files with 641 additions and 25 deletions

View File

@@ -29,6 +29,7 @@ class Settings(BaseSettings):
approval_api_key: str | None = None
approval_api_actor: str = ActorValue.APPROVER
cors_origins: list[str] = Field(default_factory=lambda: ["*"])
mask_sensitive_responses: bool = True
database_url: str = "sqlite:///./company_ai.db"
legacy_database_url: str | None = None
@@ -63,6 +64,9 @@ class Settings(BaseSettings):
direct_llm_model: str = "gpt-4.1-mini"
scheduler_enabled: bool = False
task_queue_enabled: bool = False
task_queue_always_eager: bool = False
celery_result_backend_url: str | None = None
daily_brief_cron_hour: int = 9
daily_brief_cron_minute: int = 0
weekly_project_report_day_of_week: str = "mon"

54
app/core/masking.py Normal file
View File

@@ -0,0 +1,54 @@
from typing import Any
from app.core.config import get_settings
MASKED_VALUE = "[MASKED]"
SENSITIVE_RESPONSE_KEYS = frozenset(
{
"account_number",
"api_key",
"bank_account",
"card_no",
"direct_llm_api_key",
"email",
"feishu_app_secret",
"feishu_verification_token",
"hermes_api_key",
"id_card",
"mobile",
"openclaw_gateway_token",
"password",
"payment_account",
"phone",
"secret",
"tenant_access_token",
"token",
}
)
def mask_configured(value: Any) -> Any:
"""Mask sensitive response fields when response masking is enabled."""
settings = get_settings()
if not settings.mask_sensitive_responses:
return value
return mask_sensitive(value)
def mask_sensitive(value: Any) -> Any:
if isinstance(value, dict):
masked: dict[str, Any] = {}
for key, item in value.items():
key_text = str(key)
if key_text.lower() in SENSITIVE_RESPONSE_KEYS:
masked[key_text] = MASKED_VALUE
else:
masked[key_text] = mask_sensitive(item)
return masked
if isinstance(value, list):
return [mask_sensitive(item) for item in value]
if isinstance(value, tuple):
return [mask_sensitive(item) for item in value]
return value

View File

@@ -15,6 +15,7 @@ def attach_scheduler(app: FastAPI) -> None:
from apscheduler.schedulers.background import BackgroundScheduler
from app.core.database import SessionLocal
from app.core.task_queue import enqueue_daily_brief_push, enqueue_project_weekly_push
from app.modules.reports.service import ReportService
scheduler = BackgroundScheduler(timezone="Asia/Shanghai")
@@ -29,6 +30,13 @@ def attach_scheduler(app: FastAPI) -> None:
and settings.feishu_app_secret
and settings.feishu_default_chat_id
):
if settings.task_queue_enabled:
app.state.last_daily_brief_dispatch = enqueue_daily_brief_push(
receive_id=settings.feishu_default_chat_id,
receive_id_type=FeishuReceiveIdType.CHAT_ID,
actor=ActorValue.SCHEDULER,
)
return
ReportService(db).push_report(
report,
receive_id=settings.feishu_default_chat_id,
@@ -48,6 +56,13 @@ def attach_scheduler(app: FastAPI) -> None:
and settings.feishu_app_secret
and settings.feishu_default_chat_id
):
if settings.task_queue_enabled:
app.state.last_project_weekly_dispatch = enqueue_project_weekly_push(
receive_id=settings.feishu_default_chat_id,
receive_id_type=FeishuReceiveIdType.CHAT_ID,
actor=ActorValue.SCHEDULER,
)
return
ReportService(db).push_report(
report,
receive_id=settings.feishu_default_chat_id,

107
app/core/task_queue.py Normal file
View File

@@ -0,0 +1,107 @@
from collections.abc import Callable
from typing import Any
from app.core.config import get_settings
from app.modules.feishu.constants import FeishuReceiveIdType
TASK_PUSH_DAILY_BRIEF = "reports.push_daily_brief"
TASK_PUSH_PROJECT_WEEKLY = "reports.push_project_weekly"
TASK_GENERATE_RISK_EVENTS = "risks.generate_events"
def dispatch_task(
task_name: str,
kwargs: dict[str, Any],
inline: Callable[[], Any],
) -> dict[str, Any]:
"""Dispatch a task through Celery when enabled, otherwise run inline."""
settings = get_settings()
if settings.task_queue_enabled:
from app.tasks import celery_app
async_result = celery_app.signature(task_name, kwargs=kwargs).apply_async()
return {
"queued": True,
"mode": "celery",
"task_name": task_name,
"task_id": async_result.id,
}
return {
"queued": False,
"mode": "inline",
"task_name": task_name,
"result": inline(),
}
def enqueue_daily_brief_push(
receive_id: str | None = None,
receive_id_type: str = FeishuReceiveIdType.CHAT_ID,
actor: str = "scheduler",
) -> dict[str, Any]:
def inline() -> Any:
from app.core.database import SessionLocal
from app.modules.reports.service import ReportService
db = SessionLocal()
try:
report = ReportService(db).daily_brief()
return ReportService(db).push_report(report, receive_id, receive_id_type, actor)
finally:
db.close()
return dispatch_task(
TASK_PUSH_DAILY_BRIEF,
{
"receive_id": receive_id,
"receive_id_type": receive_id_type,
"actor": actor,
},
inline,
)
def enqueue_project_weekly_push(
receive_id: str | None = None,
receive_id_type: str = FeishuReceiveIdType.CHAT_ID,
actor: str = "scheduler",
) -> dict[str, Any]:
def inline() -> Any:
from app.core.database import SessionLocal
from app.modules.reports.service import ReportService
db = SessionLocal()
try:
report = ReportService(db).project_weekly()
return ReportService(db).push_report(report, receive_id, receive_id_type, actor)
finally:
db.close()
return dispatch_task(
TASK_PUSH_PROJECT_WEEKLY,
{
"receive_id": receive_id,
"receive_id_type": receive_id_type,
"actor": actor,
},
inline,
)
def enqueue_risk_event_generation(actor: str = "scheduler") -> dict[str, Any]:
def inline() -> Any:
from app.core.database import SessionLocal
from app.modules.risk.service import RiskService
db = SessionLocal()
try:
return RiskService(db).generate_events(actor=actor)
finally:
db.close()
return dispatch_task(
TASK_GENERATE_RISK_EVENTS,
{"actor": actor},
inline,
)