feat: 数字员工财务报告体系与定时提醒及看板快照调度

- 新增数字员工财务报告生成、邮件投递与渲染调度器
- 引入员工画像扫描调度与定时提醒任务
- 完善财务看板快照、排行口径与部门人员占比计算
- 优化数字员工工作看板仪表盘与技能目录
- 增强前端总览页图表、工作台摘要与顶部导航栏交互
- 新增差旅申请规划推动提醒与报销创建会话状态管理
- 补充财务报告、看板调度、数字员工工作记录测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-03 09:25:23 +08:00
parent 0c74b4ab4a
commit 15006a05a7
114 changed files with 7356 additions and 650 deletions

View File

@@ -0,0 +1,123 @@
from __future__ import annotations
from datetime import UTC, datetime
from time import perf_counter
from typing import Any
from sqlalchemy.orm import Session
from app.core.agent_enums import (
AgentName,
AgentPermissionLevel,
AgentRunSource,
AgentRunStatus,
AgentToolType,
)
from app.services.agent_runs import AgentRunService
from app.services.hermes_employee_profile_scanner import HermesEmployeeProfileScannerService
EMPLOYEE_PROFILE_SCAN_TASK_TYPE = "employee_behavior_profile_scan"
EMPLOYEE_PROFILE_SCAN_TOOL_NAME = "digital_employee.employee_behavior_profile.scan"
class EmployeeProfileScanTaskService:
def __init__(self, db: Session) -> None:
self.db = db
def refresh_profiles(self, *, source: str = AgentRunSource.SCHEDULE.value) -> dict[str, Any]:
run_service = AgentRunService(self.db)
run = run_service.create_run(
agent=AgentName.HERMES.value,
source=source,
user_id="digital_employee",
ontology_json={
"scenario": "employee_behavior_profile",
"intent": "scan",
},
route_json={
"task_type": EMPLOYEE_PROFILE_SCAN_TASK_TYPE,
"job_type": EMPLOYEE_PROFILE_SCAN_TASK_TYPE,
"selected_agent": AgentName.HERMES.value,
"phase": "running",
"heartbeat_at": datetime.now(UTC).isoformat(),
},
permission_level=AgentPermissionLevel.READ.value,
status=AgentRunStatus.RUNNING.value,
)
timer = perf_counter()
try:
# 画像快照表的 source_task_log_id 外键指向 Hermes 任务日志。
# 这里用 agent_runs 记录数字员工轨迹,因此不写入该外键,避免错误关联。
summary = HermesEmployeeProfileScannerService(self.db).scan_employee_profiles(
log_id=None
)
duration_ms = int((perf_counter() - timer) * 1000)
report = self._build_report(summary)
response = {
"task_type": EMPLOYEE_PROFILE_SCAN_TASK_TYPE,
"summary": summary,
"report": report,
}
run_service.record_tool_call(
run_id=run.run_id,
tool_type=AgentToolType.DATABASE.value,
tool_name=EMPLOYEE_PROFILE_SCAN_TOOL_NAME,
request_json={"task_type": EMPLOYEE_PROFILE_SCAN_TASK_TYPE},
response_json=response,
status=AgentRunStatus.SUCCEEDED.value,
duration_ms=duration_ms,
)
run_service.merge_route_json(
run.run_id,
{
"phase": "succeeded",
"summary": summary,
"report": report,
"heartbeat_at": datetime.now(UTC).isoformat(),
},
status=AgentRunStatus.SUCCEEDED.value,
result_summary=(
"员工行为画像已生成:"
f"覆盖 {summary.get('target_employee_count', 0)} 人,"
f"快照 {summary.get('snapshot_count', 0)} 条,"
f"重点关注 {summary.get('high_attention_employee_count', 0)} 人。"
),
finished_at=datetime.now(UTC),
)
return response
except Exception as exc:
run_service.record_tool_call(
run_id=run.run_id,
tool_type=AgentToolType.DATABASE.value,
tool_name=EMPLOYEE_PROFILE_SCAN_TOOL_NAME,
request_json={"task_type": EMPLOYEE_PROFILE_SCAN_TASK_TYPE},
response_json={},
status=AgentRunStatus.FAILED.value,
duration_ms=int((perf_counter() - timer) * 1000),
error_message=str(exc),
)
run_service.merge_route_json(
run.run_id,
{
"phase": "failed",
"heartbeat_at": datetime.now(UTC).isoformat(),
},
status=AgentRunStatus.FAILED.value,
error_message=str(exc),
finished_at=datetime.now(UTC),
)
raise
@staticmethod
def _build_report(summary: dict[str, Any]) -> dict[str, Any]:
return {
"title": "员工财务行为画像扫描报告",
"targetEmployeeCount": int(summary.get("target_employee_count") or 0),
"profileSnapshotCount": int(summary.get("snapshot_count") or 0),
"highAttentionEmployeeCount": int(
summary.get("high_attention_employee_count") or 0
),
"windowDays": list(summary.get("window_days") or []),
"algorithmVersion": str(summary.get("algorithm_version") or ""),
"baselineSummary": summary.get("baseline_summary") or {},
}