feat(ai_agent): 完善AI适配器和服务功能

- 添加OpenClaw和Hermes健康检查接口
- 实现OpenClaw工具调用功能
- 重构AI适配器使用常量定义
- 增加AI技能系统支持
- 更新配置文件中的默认模型提供者设置

refactor(scheduler): 使用常量替换硬编码值

- 将硬编码的actor值替换为ActorValue常量
- 将receive_id_type替换为FeishuReceiveIdType枚举

refactor(audit): 统一审计日志常量使用

- 将硬编码的actor、source、risk_level等值替换为对应常量
- 更新审核服务中的状态和操作常量引用

refactor(approvals): 标准化审批模块常量使用

- 将applicant默认值替换为ActorValue.API常量
- 使用ApprovalStatus常量替代硬编码状态值
- 更新审核操作常量引用
```
This commit is contained in:
2026-07-06 00:02:03 +08:00
parent d82116d637
commit aa81fc5321
36 changed files with 2328 additions and 337 deletions

View File

@@ -1,6 +1,7 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.core.constants import ActorValue
from app.core.database import get_db
from app.core.security import require_api_key
from app.modules.risk.service import RiskService
@@ -48,5 +49,5 @@ def risk_events(
@router.post("/events/generate")
def generate_risk_events(actor: str = "api", db: Session = Depends(get_db)) -> dict:
def generate_risk_events(actor: str = ActorValue.API, db: Session = Depends(get_db)) -> dict:
return RiskService(db).generate_events(actor=actor)

View File

@@ -5,14 +5,28 @@ from typing import Any
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.core.constants import ActorValue
from app.modules.audit.constants import (
AuditAction,
AuditRiskLevel,
AuditSource,
AuditTargetType,
)
from app.modules.audit.schemas import AuditLogCreate
from app.modules.audit.service import AuditService
from app.modules.business.constants import (
CLOSED_RISK_STATUSES,
DONE_STATUSES,
PROJECT_CLOSED_STATUSES,
SUPPLIER_RISK_LEVELS,
BusinessDomain,
RiskEventType,
RiskLevel,
StatusValue,
)
from app.modules.business.models import FundAccount, Project, RiskEvent, Supplier, WorkTask
from app.modules.business.service import serialize_model
DONE_STATUSES = {"完成", "已完成", "关闭", "done", "completed", "closed"}
CLOSED_RISK_STATUSES = {"closed", "resolved"}
class RiskService:
"""Evaluate rule-based business risk signals from internal ledgers."""
@@ -32,7 +46,7 @@ class RiskService:
stmt = select(Project).where(
Project.due_date.is_not(None),
Project.due_date < date.today(),
Project.status.notin_(["验收", "已完成", "复盘", "归档", "关闭", "closed"]),
Project.status.notin_(PROJECT_CLOSED_STATUSES),
)
return [serialize_model(item) for item in self.db.execute(stmt).scalars()]
@@ -49,8 +63,8 @@ class RiskService:
def supplier_risks(self) -> list[dict[str, Any]]:
stmt = select(Supplier).where(
(Supplier.blacklist_status != "normal")
| Supplier.risk_level.in_(["medium", "high"])
(Supplier.blacklist_status != StatusValue.NORMAL)
| Supplier.risk_level.in_(SUPPLIER_RISK_LEVELS)
)
return [serialize_model(item) for item in self.db.execute(stmt).scalars()]
@@ -75,7 +89,7 @@ class RiskService:
over_budget_projects = self.over_budget_projects()
fund_risks = self.fund_risks()
supplier_risks = self.supplier_risks()
open_events = self.list_events(status_filter="open")
open_events = self.list_events(status_filter=StatusValue.OPEN)
risk_score = (
len(overdue_tasks) * 1
+ len(delayed_projects) * 3
@@ -85,11 +99,11 @@ class RiskService:
+ len(open_events) * 2
)
if risk_score >= 15:
level = "high"
level = RiskLevel.HIGH
elif risk_score >= 5:
level = "medium"
level = RiskLevel.MEDIUM
else:
level = "low"
level = RiskLevel.LOW
return {
"risk_level": level,
"risk_score": Decimal(risk_score),
@@ -101,7 +115,7 @@ class RiskService:
"open_events": open_events,
}
def generate_events(self, actor: str = "api") -> dict[str, Any]:
def generate_events(self, actor: str = ActorValue.API) -> dict[str, Any]:
"""Generate or refresh risk-event ledger entries from current signals."""
payloads = self._build_event_payloads()
@@ -136,10 +150,10 @@ class RiskService:
AuditService(self.db).log(
AuditLogCreate(
actor=actor,
source="risk",
action="generate_events",
target_type="risk-events",
risk_level="medium",
source=AuditSource.RISK,
action=AuditAction.GENERATE_EVENTS,
target_type=AuditTargetType.RISK_EVENTS,
risk_level=AuditRiskLevel.MEDIUM,
response_payload={
"created": created,
"updated": updated,
@@ -170,17 +184,19 @@ class RiskService:
{
"code": f"RISK-TASK-OVERDUE-{task.id}",
"title": f"任务逾期:{task.title}",
"risk_type": "overdue_task",
"risk_level": "medium",
"status": "open",
"source_domain": "tasks",
"risk_type": RiskEventType.OVERDUE_TASK,
"risk_level": RiskLevel.MEDIUM,
"status": StatusValue.OPEN,
"source_domain": BusinessDomain.TASKS,
"source_record_id": str(task.id),
"project_code": task.project_code,
"owner": task.owner,
"due_date": task.due_date,
"detected_at": datetime.utcnow(),
"description": "任务已超过截止日期且未完成。",
"mitigation": "请负责人更新进度、明确阻塞项并给出新的完成时间。",
"mitigation": (
"请负责人更新进度、明确阻塞项并给出新的完成时间。"
),
"evidence": serialize_model(task),
}
)
@@ -190,26 +206,28 @@ class RiskService:
stmt = select(Project).where(
Project.due_date.is_not(None),
Project.due_date < date.today(),
Project.status.notin_(["验收", "已完成", "复盘", "归档", "关闭", "closed"]),
Project.status.notin_(PROJECT_CLOSED_STATUSES),
)
payloads = []
for project in self.db.execute(stmt).scalars():
level = "high" if project.progress_percent < 80 else "medium"
level = RiskLevel.HIGH if project.progress_percent < 80 else RiskLevel.MEDIUM
payloads.append(
{
"code": f"RISK-PROJECT-DELAY-{project.id}",
"title": f"项目延期:{project.name}",
"risk_type": "delayed_project",
"risk_type": RiskEventType.DELAYED_PROJECT,
"risk_level": level,
"status": "open",
"source_domain": "projects",
"status": StatusValue.OPEN,
"source_domain": BusinessDomain.PROJECTS,
"source_record_id": str(project.id),
"project_code": project.code,
"owner": project.owner,
"due_date": project.due_date,
"detected_at": datetime.utcnow(),
"description": "项目已超过计划截止日期且未进入完成状态。",
"mitigation": "请项目负责人提交延期原因、资源需求和纠偏计划。",
"mitigation": (
"请项目负责人提交延期原因、资源需求和纠偏计划。"
),
"evidence": serialize_model(project),
}
)
@@ -226,17 +244,19 @@ class RiskService:
{
"code": f"RISK-PROJECT-BUDGET-{project.id}",
"title": f"项目超预算:{project.name}",
"risk_type": "over_budget_project",
"risk_level": "high",
"status": "open",
"source_domain": "projects",
"risk_type": RiskEventType.OVER_BUDGET_PROJECT,
"risk_level": RiskLevel.HIGH,
"status": StatusValue.OPEN,
"source_domain": BusinessDomain.PROJECTS,
"source_record_id": str(project.id),
"project_code": project.code,
"owner": project.owner,
"due_date": project.due_date,
"detected_at": datetime.utcnow(),
"description": "项目实际成本已超过预算。",
"mitigation": "请复核预算科目、冻结非必要采购并补充审批依据。",
"mitigation": (
"请复核预算科目、冻结非必要采购并补充审批依据。"
),
"evidence": serialize_model(project),
}
)
@@ -250,15 +270,18 @@ class RiskService:
{
"code": f"RISK-FUND-{account.id}",
"title": f"资金低于安全线:{account.name}",
"risk_type": "fund_safety_line",
"risk_level": "high",
"status": "open",
"source_domain": "fund-accounts",
"risk_type": RiskEventType.FUND_SAFETY_LINE,
"risk_level": RiskLevel.HIGH,
"status": StatusValue.OPEN,
"source_domain": BusinessDomain.FUND_ACCOUNTS,
"source_record_id": str(account.id),
"owner": None,
"detected_at": datetime.utcnow(),
"description": "账户当前余额低于设置的安全线。",
"mitigation": "请财务确认收付款计划,并优先处理关键项目资金安排。",
"mitigation": (
"请财务确认收付款计划,"
"并优先处理关键项目资金安排。"
),
"evidence": serialize_model(account),
}
)
@@ -266,25 +289,31 @@ class RiskService:
def _supplier_risk_payloads(self) -> list[dict[str, Any]]:
stmt = select(Supplier).where(
(Supplier.blacklist_status != "normal")
| Supplier.risk_level.in_(["medium", "high"])
(Supplier.blacklist_status != StatusValue.NORMAL)
| Supplier.risk_level.in_(SUPPLIER_RISK_LEVELS)
)
payloads = []
for supplier in self.db.execute(stmt).scalars():
level = "high" if supplier.blacklist_status != "normal" else supplier.risk_level
level = (
RiskLevel.HIGH
if supplier.blacklist_status != StatusValue.NORMAL
else supplier.risk_level
)
payloads.append(
{
"code": f"RISK-SUPPLIER-{supplier.id}",
"title": f"供应商风险:{supplier.name}",
"risk_type": "supplier_risk",
"risk_type": RiskEventType.SUPPLIER_RISK,
"risk_level": level,
"status": "open",
"source_domain": "suppliers",
"status": StatusValue.OPEN,
"source_domain": BusinessDomain.SUPPLIERS,
"source_record_id": str(supplier.id),
"owner": supplier.contact,
"detected_at": datetime.utcnow(),
"description": "供应商风险等级或黑名单状态需要关注。",
"mitigation": "请采购负责人复核供应商准入、履约和替代方案。",
"mitigation": (
"请采购负责人复核供应商准入、履约和替代方案。"
),
"evidence": serialize_model(supplier),
}
)