```
feat(ai_agent): 新增openclaw_hermes混合AI适配器 新增OpenClawHermesAdapter适配器,结合Hermes记忆功能和OpenClaw执行能力, 实现AI问答流程中的记忆召回、执行操作和记忆存储的完整闭环。 同时更新NoopAdapter提示信息,添加新的模型提供商选项。 feat(business): 新增考勤、工作报告和风险事件业务模型 新增AttendanceRecord、WorkReport、RiskEvent和LegacySyncRun四个业务模型, 扩展业务领域注册表,支持考勤管理、工作报告生成和风险事件跟踪等核心业务功能。 feat(reports): 实现考勤汇总和工作日报周报生成功能 新增attendance_summary方法用于统计每日考勤情况, 新增generate_work_report方法用于生成日/周经营报告, 包含任务完成情况、待处理事项和风险指标等综合信息。 feat(risk): 扩展风险管理API端点和供应商风险检测 新增供应商风险查询端点和风险事件管理端点, 提供风险事件列表查询和自动生成功能, 增强供应商风险评估能力。 feat(feishu): 添加考勤查询命令和风险摘要增强 集成考勤汇总查询功能到飞书命令系统, 在风险摘要中添加供应商风险和开放风险事件统计, 丰富日常经营管理信息展示。 refactor(service): 优化业务服务数据验证和类型转换 重构_model_payload函数实现数据验证和类型转换, 添加列值类型强制转换逻辑,提高API数据处理的准确性和安全性。 build(deps): 添加postgresql数据库驱动依赖 在Dockerfile中添加psycopg[binary]==3.2.3依赖包, 支持PostgreSQL数据库连接和操作。 chore(config): 更新.gitignore文件排除备份和迁移目录 在.gitignore中添加AGENTS.md.bak-*和migration/目录排除规则, 避免备份文件和本地迁移工作区被提交到版本控制系统。 ```
This commit is contained in:
@@ -1,13 +1,18 @@
|
||||
from datetime import date
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.modules.business.models import FundAccount, Project, WorkTask
|
||||
from app.modules.audit.schemas import AuditLogCreate
|
||||
from app.modules.audit.service import AuditService
|
||||
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."""
|
||||
@@ -19,7 +24,7 @@ class RiskService:
|
||||
stmt = select(WorkTask).where(
|
||||
WorkTask.due_date.is_not(None),
|
||||
WorkTask.due_date < date.today(),
|
||||
WorkTask.status.notin_(["完成", "已完成", "关闭"]),
|
||||
WorkTask.status.notin_(DONE_STATUSES),
|
||||
)
|
||||
return [serialize_model(item) for item in self.db.execute(stmt).scalars()]
|
||||
|
||||
@@ -27,7 +32,7 @@ class RiskService:
|
||||
stmt = select(Project).where(
|
||||
Project.due_date.is_not(None),
|
||||
Project.due_date < date.today(),
|
||||
Project.status.notin_(["验收", "已完成", "复盘", "归档", "关闭"]),
|
||||
Project.status.notin_(["验收", "已完成", "复盘", "归档", "关闭", "closed"]),
|
||||
)
|
||||
return [serialize_model(item) for item in self.db.execute(stmt).scalars()]
|
||||
|
||||
@@ -42,16 +47,42 @@ class RiskService:
|
||||
stmt = select(FundAccount).where(FundAccount.current_balance < FundAccount.safety_line)
|
||||
return [serialize_model(item) for item in self.db.execute(stmt).scalars()]
|
||||
|
||||
def supplier_risks(self) -> list[dict[str, Any]]:
|
||||
stmt = select(Supplier).where(
|
||||
(Supplier.blacklist_status != "normal")
|
||||
| Supplier.risk_level.in_(["medium", "high"])
|
||||
)
|
||||
return [serialize_model(item) for item in self.db.execute(stmt).scalars()]
|
||||
|
||||
def list_events(
|
||||
self,
|
||||
limit: int = 100,
|
||||
status_filter: str | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
stmt = select(RiskEvent).order_by(RiskEvent.id.desc()).limit(min(limit, 500))
|
||||
if status_filter:
|
||||
stmt = (
|
||||
select(RiskEvent)
|
||||
.where(RiskEvent.status == status_filter)
|
||||
.order_by(RiskEvent.id.desc())
|
||||
.limit(min(limit, 500))
|
||||
)
|
||||
return [serialize_model(item) for item in self.db.execute(stmt).scalars()]
|
||||
|
||||
def summary(self) -> dict[str, Any]:
|
||||
overdue_tasks = self.overdue_tasks()
|
||||
delayed_projects = self.delayed_projects()
|
||||
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")
|
||||
risk_score = (
|
||||
len(overdue_tasks) * 1
|
||||
+ len(delayed_projects) * 3
|
||||
+ len(over_budget_projects) * 4
|
||||
+ len(fund_risks) * 5
|
||||
+ len(supplier_risks) * 3
|
||||
+ len(open_events) * 2
|
||||
)
|
||||
if risk_score >= 15:
|
||||
level = "high"
|
||||
@@ -66,4 +97,195 @@ class RiskService:
|
||||
"delayed_projects": delayed_projects,
|
||||
"over_budget_projects": over_budget_projects,
|
||||
"fund_risks": fund_risks,
|
||||
"supplier_risks": supplier_risks,
|
||||
"open_events": open_events,
|
||||
}
|
||||
|
||||
def generate_events(self, actor: str = "api") -> dict[str, Any]:
|
||||
"""Generate or refresh risk-event ledger entries from current signals."""
|
||||
|
||||
payloads = self._build_event_payloads()
|
||||
created = 0
|
||||
updated = 0
|
||||
skipped = 0
|
||||
items: list[dict[str, Any]] = []
|
||||
|
||||
for payload in payloads:
|
||||
record = self.db.execute(
|
||||
select(RiskEvent).where(RiskEvent.code == payload["code"])
|
||||
).scalar_one_or_none()
|
||||
if record is None:
|
||||
record = RiskEvent(**payload)
|
||||
self.db.add(record)
|
||||
self.db.flush()
|
||||
created += 1
|
||||
action = "created"
|
||||
elif record.status in CLOSED_RISK_STATUSES:
|
||||
skipped += 1
|
||||
items.append({"action": "skipped", "risk_event": serialize_model(record)})
|
||||
continue
|
||||
else:
|
||||
for key, value in payload.items():
|
||||
if key != "code":
|
||||
setattr(record, key, value)
|
||||
updated += 1
|
||||
action = "updated"
|
||||
items.append({"action": action, "risk_event": serialize_model(record)})
|
||||
|
||||
self.db.commit()
|
||||
AuditService(self.db).log(
|
||||
AuditLogCreate(
|
||||
actor=actor,
|
||||
source="risk",
|
||||
action="generate_events",
|
||||
target_type="risk-events",
|
||||
risk_level="medium",
|
||||
response_payload={
|
||||
"created": created,
|
||||
"updated": updated,
|
||||
"skipped": skipped,
|
||||
},
|
||||
)
|
||||
)
|
||||
return {"created": created, "updated": updated, "skipped": skipped, "items": items}
|
||||
|
||||
def _build_event_payloads(self) -> list[dict[str, Any]]:
|
||||
payloads: list[dict[str, Any]] = []
|
||||
payloads.extend(self._overdue_task_payloads())
|
||||
payloads.extend(self._delayed_project_payloads())
|
||||
payloads.extend(self._over_budget_project_payloads())
|
||||
payloads.extend(self._fund_risk_payloads())
|
||||
payloads.extend(self._supplier_risk_payloads())
|
||||
return payloads
|
||||
|
||||
def _overdue_task_payloads(self) -> list[dict[str, Any]]:
|
||||
stmt = select(WorkTask).where(
|
||||
WorkTask.due_date.is_not(None),
|
||||
WorkTask.due_date < date.today(),
|
||||
WorkTask.status.notin_(DONE_STATUSES),
|
||||
)
|
||||
payloads = []
|
||||
for task in self.db.execute(stmt).scalars():
|
||||
payloads.append(
|
||||
{
|
||||
"code": f"RISK-TASK-OVERDUE-{task.id}",
|
||||
"title": f"任务逾期:{task.title}",
|
||||
"risk_type": "overdue_task",
|
||||
"risk_level": "medium",
|
||||
"status": "open",
|
||||
"source_domain": "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": "请负责人更新进度、明确阻塞项并给出新的完成时间。",
|
||||
"evidence": serialize_model(task),
|
||||
}
|
||||
)
|
||||
return payloads
|
||||
|
||||
def _delayed_project_payloads(self) -> list[dict[str, Any]]:
|
||||
stmt = select(Project).where(
|
||||
Project.due_date.is_not(None),
|
||||
Project.due_date < date.today(),
|
||||
Project.status.notin_(["验收", "已完成", "复盘", "归档", "关闭", "closed"]),
|
||||
)
|
||||
payloads = []
|
||||
for project in self.db.execute(stmt).scalars():
|
||||
level = "high" if project.progress_percent < 80 else "medium"
|
||||
payloads.append(
|
||||
{
|
||||
"code": f"RISK-PROJECT-DELAY-{project.id}",
|
||||
"title": f"项目延期:{project.name}",
|
||||
"risk_type": "delayed_project",
|
||||
"risk_level": level,
|
||||
"status": "open",
|
||||
"source_domain": "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": "请项目负责人提交延期原因、资源需求和纠偏计划。",
|
||||
"evidence": serialize_model(project),
|
||||
}
|
||||
)
|
||||
return payloads
|
||||
|
||||
def _over_budget_project_payloads(self) -> list[dict[str, Any]]:
|
||||
stmt = select(Project).where(
|
||||
Project.budget_amount > 0,
|
||||
Project.actual_amount > Project.budget_amount,
|
||||
)
|
||||
payloads = []
|
||||
for project in self.db.execute(stmt).scalars():
|
||||
payloads.append(
|
||||
{
|
||||
"code": f"RISK-PROJECT-BUDGET-{project.id}",
|
||||
"title": f"项目超预算:{project.name}",
|
||||
"risk_type": "over_budget_project",
|
||||
"risk_level": "high",
|
||||
"status": "open",
|
||||
"source_domain": "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": "请复核预算科目、冻结非必要采购并补充审批依据。",
|
||||
"evidence": serialize_model(project),
|
||||
}
|
||||
)
|
||||
return payloads
|
||||
|
||||
def _fund_risk_payloads(self) -> list[dict[str, Any]]:
|
||||
stmt = select(FundAccount).where(FundAccount.current_balance < FundAccount.safety_line)
|
||||
payloads = []
|
||||
for account in self.db.execute(stmt).scalars():
|
||||
payloads.append(
|
||||
{
|
||||
"code": f"RISK-FUND-{account.id}",
|
||||
"title": f"资金低于安全线:{account.name}",
|
||||
"risk_type": "fund_safety_line",
|
||||
"risk_level": "high",
|
||||
"status": "open",
|
||||
"source_domain": "fund-accounts",
|
||||
"source_record_id": str(account.id),
|
||||
"owner": None,
|
||||
"detected_at": datetime.utcnow(),
|
||||
"description": "账户当前余额低于设置的安全线。",
|
||||
"mitigation": "请财务确认收付款计划,并优先处理关键项目资金安排。",
|
||||
"evidence": serialize_model(account),
|
||||
}
|
||||
)
|
||||
return payloads
|
||||
|
||||
def _supplier_risk_payloads(self) -> list[dict[str, Any]]:
|
||||
stmt = select(Supplier).where(
|
||||
(Supplier.blacklist_status != "normal")
|
||||
| Supplier.risk_level.in_(["medium", "high"])
|
||||
)
|
||||
payloads = []
|
||||
for supplier in self.db.execute(stmt).scalars():
|
||||
level = "high" if supplier.blacklist_status != "normal" else supplier.risk_level
|
||||
payloads.append(
|
||||
{
|
||||
"code": f"RISK-SUPPLIER-{supplier.id}",
|
||||
"title": f"供应商风险:{supplier.name}",
|
||||
"risk_type": "supplier_risk",
|
||||
"risk_level": level,
|
||||
"status": "open",
|
||||
"source_domain": "suppliers",
|
||||
"source_record_id": str(supplier.id),
|
||||
"owner": supplier.contact,
|
||||
"detected_at": datetime.utcnow(),
|
||||
"description": "供应商风险等级或黑名单状态需要关注。",
|
||||
"mitigation": "请采购负责人复核供应商准入、履约和替代方案。",
|
||||
"evidence": serialize_model(supplier),
|
||||
}
|
||||
)
|
||||
return payloads
|
||||
|
||||
Reference in New Issue
Block a user