687 lines
26 KiB
Python
687 lines
26 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import UTC, date, datetime, timedelta
|
|
from typing import Any
|
|
|
|
from sqlalchemy import or_, select
|
|
from sqlalchemy.orm import Session, selectinload
|
|
|
|
from app.core.agent_enums import AgentName, AgentRunSource
|
|
from app.db.schema_ownership import create_legacy_schema
|
|
from app.models.agent_run import AgentRun, AgentToolCall
|
|
from app.schemas.digital_employee_dashboard import DigitalEmployeeDashboardRead
|
|
|
|
SUCCESS_STATUSES = {"success", "succeeded", "ok", "done", "completed"}
|
|
FAILED_STATUSES = {"failed", "failure", "error", "errored"}
|
|
RUNNING_STATUSES = {"running", "pending"}
|
|
|
|
TASK_CODE_TO_TYPE = {
|
|
"task.hermes.global_risk_scan": "global_risk_scan",
|
|
"task.hermes.employee_behavior_profile_scan": "employee_behavior_profile_scan",
|
|
"task.hermes.risk_rule_discovery": "risk_clue_collect",
|
|
"task.hermes.finance_policy_knowledge_organize": "finance_policy_knowledge_organize",
|
|
"task.hermes.finance_policy_clause_extract": "finance_policy_clause_extract",
|
|
"task.hermes.expense_policy_alignment": "expense_policy_alignment",
|
|
"task.hermes.risk_rule_template_organize": "risk_rule_template_organize",
|
|
"task.hermes.department_expense_baseline_accumulate": "department_expense_baseline_accumulate",
|
|
"task.hermes.supplier_risk_profile_accumulate": "supplier_risk_profile_accumulate",
|
|
"task.hermes.false_positive_sample_accumulate": "false_positive_sample_accumulate",
|
|
"task.hermes.risk_feedback_sample_accumulate": "risk_feedback_sample_accumulate",
|
|
"task.hermes.multi_evidence_consistency_evaluate": "multi_evidence_consistency_evaluate",
|
|
"task.hermes.travel_spatiotemporal_consistency_evaluate": (
|
|
"travel_spatiotemporal_consistency_evaluate"
|
|
),
|
|
"task.hermes.budget_overrun_precontrol_evaluate": "budget_overrun_precontrol_evaluate",
|
|
"task.hermes.supplier_abnormal_relation_evaluate": "supplier_abnormal_relation_evaluate",
|
|
"task.hermes.risk_algorithm_replay_evaluate": "risk_algorithm_replay_evaluate",
|
|
"task.hermes.policy_gap_rule_optimization": "policy_gap_rule_optimization",
|
|
}
|
|
|
|
TASK_SPECS: dict[str, dict[str, str]] = {
|
|
"global_risk_scan": {
|
|
"label": "财务风险图谱巡检",
|
|
"category": "评估",
|
|
"color": "var(--theme-primary)",
|
|
},
|
|
"employee_behavior_profile_scan": {
|
|
"label": "员工行为画像巡检",
|
|
"category": "积累",
|
|
"color": "var(--chart-blue)",
|
|
},
|
|
"risk_clue_collect": {
|
|
"label": "风险线索归集",
|
|
"category": "升级",
|
|
"color": "var(--chart-amber)",
|
|
},
|
|
"finance_policy_knowledge_organize": {
|
|
"label": "知识制度整理",
|
|
"category": "整理",
|
|
"color": "var(--success)",
|
|
},
|
|
"knowledge_index_sync": {
|
|
"label": "知识制度整理",
|
|
"category": "整理",
|
|
"color": "var(--success)",
|
|
},
|
|
"llm_wiki_sync": {
|
|
"label": "知识制度整理",
|
|
"category": "整理",
|
|
"color": "var(--success)",
|
|
},
|
|
"llm_wiki_rule_formation": {
|
|
"label": "知识制度整理",
|
|
"category": "整理",
|
|
"color": "var(--success)",
|
|
},
|
|
"finance_policy_clause_extract": {
|
|
"label": "制度条款结构化抽取",
|
|
"category": "整理",
|
|
"color": "var(--success)",
|
|
},
|
|
"expense_policy_alignment": {
|
|
"label": "报销政策口径对齐",
|
|
"category": "整理",
|
|
"color": "var(--success)",
|
|
},
|
|
"risk_rule_template_organize": {
|
|
"label": "规则命中样本整理",
|
|
"category": "整理",
|
|
"color": "var(--success)",
|
|
},
|
|
"department_expense_baseline_accumulate": {
|
|
"label": "部门费用基线沉淀",
|
|
"category": "积累",
|
|
"color": "var(--chart-blue)",
|
|
},
|
|
"supplier_risk_profile_accumulate": {
|
|
"label": "供应商风险画像沉淀",
|
|
"category": "积累",
|
|
"color": "var(--chart-blue)",
|
|
},
|
|
"false_positive_sample_accumulate": {
|
|
"label": "历史误报样本沉淀",
|
|
"category": "积累",
|
|
"color": "var(--chart-blue)",
|
|
},
|
|
"risk_feedback_sample_accumulate": {
|
|
"label": "风险观察反馈样本沉淀",
|
|
"category": "积累",
|
|
"color": "var(--chart-blue)",
|
|
},
|
|
"multi_evidence_consistency_evaluate": {
|
|
"label": "多源证据一致性评估",
|
|
"category": "评估",
|
|
"color": "var(--theme-primary)",
|
|
},
|
|
"travel_spatiotemporal_consistency_evaluate": {
|
|
"label": "差旅时空一致性评估",
|
|
"category": "评估",
|
|
"color": "var(--theme-primary)",
|
|
},
|
|
"budget_overrun_precontrol_evaluate": {
|
|
"label": "预算超限预警评估",
|
|
"category": "评估",
|
|
"color": "var(--theme-primary)",
|
|
},
|
|
"supplier_abnormal_relation_evaluate": {
|
|
"label": "供应商异常关系评估",
|
|
"category": "评估",
|
|
"color": "var(--theme-primary)",
|
|
},
|
|
"risk_algorithm_replay_evaluate": {
|
|
"label": "风险算法回放升级",
|
|
"category": "升级",
|
|
"color": "var(--chart-amber)",
|
|
},
|
|
"policy_gap_rule_optimization": {
|
|
"label": "制度缺口优化建议",
|
|
"category": "升级",
|
|
"color": "var(--chart-amber)",
|
|
},
|
|
"finance_dashboard_snapshot": {
|
|
"label": "财务看板指标快照",
|
|
"category": "积累",
|
|
"color": "var(--chart-blue)",
|
|
},
|
|
"digital_employee_reminder_scan": {
|
|
"label": "定时提醒扫描",
|
|
"category": "整理",
|
|
"color": "var(--success)",
|
|
},
|
|
}
|
|
|
|
CATEGORY_SPECS = {
|
|
"积累": {"color": "var(--chart-blue)", "description": "沉淀画像、基线和反馈样本"},
|
|
"升级": {"color": "var(--chart-amber)", "description": "输出待复核线索和优化建议"},
|
|
"整理": {"color": "var(--success)", "description": "整理制度、条款、知识和样本"},
|
|
"评估": {"color": "var(--theme-primary)", "description": "评估异常、风险和一致性"},
|
|
}
|
|
|
|
|
|
class DigitalEmployeeDashboardService:
|
|
def __init__(self, db: Session) -> None:
|
|
self.db = db
|
|
|
|
def build_dashboard(self, *, days: int = 7, limit: int = 300) -> DigitalEmployeeDashboardRead:
|
|
window_days = max(1, min(int(days or 7), 30))
|
|
window_limit = max(1, min(int(limit or 300), 1000))
|
|
self._ensure_storage_ready()
|
|
now = datetime.now(UTC)
|
|
start = now - timedelta(days=window_days - 1)
|
|
labels = self._date_labels(start.date(), window_days)
|
|
|
|
all_runs = self._fetch_runs(start=start, limit=window_limit)
|
|
runs = [run for run in all_runs if self._is_digital_employee_run(run)]
|
|
totals = self._build_totals(runs)
|
|
|
|
return DigitalEmployeeDashboardRead(
|
|
window_days=window_days,
|
|
generated_at=now.isoformat(),
|
|
has_real_data=bool(runs),
|
|
totals=totals,
|
|
daily_work=self._daily_work(labels, runs),
|
|
task_distribution=self._task_distribution(runs),
|
|
category_distribution=self._category_distribution(runs),
|
|
recent_runs=self._recent_runs(runs),
|
|
)
|
|
|
|
def _ensure_storage_ready(self) -> None:
|
|
create_legacy_schema(self.db.get_bind())
|
|
|
|
def _fetch_runs(self, *, start: datetime, limit: int) -> list[AgentRun]:
|
|
stmt = (
|
|
select(AgentRun)
|
|
.options(selectinload(AgentRun.tool_calls))
|
|
.where(
|
|
AgentRun.started_at >= start,
|
|
or_(
|
|
AgentRun.agent == AgentName.HERMES.value,
|
|
AgentRun.source == AgentRunSource.SCHEDULE.value,
|
|
),
|
|
)
|
|
.order_by(AgentRun.started_at.desc())
|
|
.limit(limit)
|
|
)
|
|
return list(self.db.scalars(stmt).all())
|
|
|
|
def _build_totals(self, runs: list[AgentRun]) -> dict[str, Any]:
|
|
metrics = self._sum_metrics(runs)
|
|
success_runs = sum(1 for run in runs if self._is_success(run.status))
|
|
failed_runs = sum(1 for run in runs if self._is_failed(run.status))
|
|
running_runs = sum(1 for run in runs if self._is_running(run.status))
|
|
total_runs = len(runs)
|
|
business_outputs = (
|
|
metrics["risk_observations"]
|
|
+ metrics["risk_clues"]
|
|
+ metrics["profile_snapshots"]
|
|
+ metrics["knowledge_documents"]
|
|
+ metrics["finance_snapshots"]
|
|
+ metrics["reminders"]
|
|
)
|
|
|
|
return {
|
|
"totalRuns": total_runs,
|
|
"successRuns": success_runs,
|
|
"failedRuns": failed_runs,
|
|
"runningRuns": running_runs,
|
|
"toolCalls": sum(len(run.tool_calls) for run in runs),
|
|
"businessOutputs": business_outputs,
|
|
"riskObservations": metrics["risk_observations"],
|
|
"riskClues": metrics["risk_clues"],
|
|
"profileSnapshots": metrics["profile_snapshots"],
|
|
"knowledgeDocuments": metrics["knowledge_documents"],
|
|
"financeDashboardSnapshots": metrics["finance_snapshots"],
|
|
"reminders": metrics["reminders"],
|
|
"successRate": self._percent(success_runs, total_runs),
|
|
"failureRate": self._percent(failed_runs, total_runs),
|
|
}
|
|
|
|
def _daily_work(self, labels: list[str], runs: list[AgentRun]) -> list[dict[str, Any]]:
|
|
rows = {
|
|
label: {
|
|
"date": label,
|
|
"total": 0,
|
|
"success": 0,
|
|
"failed": 0,
|
|
"running": 0,
|
|
"riskObservations": 0,
|
|
"riskClues": 0,
|
|
"profileSnapshots": 0,
|
|
"knowledgeDocuments": 0,
|
|
"financeDashboardSnapshots": 0,
|
|
"reminders": 0,
|
|
"businessOutputs": 0,
|
|
}
|
|
for label in labels
|
|
}
|
|
|
|
for run in runs:
|
|
label = self._date_label(run.started_at)
|
|
if label not in rows:
|
|
continue
|
|
row = rows[label]
|
|
metrics = self._extract_run_metrics(run)
|
|
row["total"] += 1
|
|
if self._is_success(run.status):
|
|
row["success"] += 1
|
|
elif self._is_failed(run.status):
|
|
row["failed"] += 1
|
|
elif self._is_running(run.status):
|
|
row["running"] += 1
|
|
row["riskObservations"] += metrics["risk_observations"]
|
|
row["riskClues"] += metrics["risk_clues"]
|
|
row["profileSnapshots"] += metrics["profile_snapshots"]
|
|
row["knowledgeDocuments"] += metrics["knowledge_documents"]
|
|
row["financeDashboardSnapshots"] += metrics["finance_snapshots"]
|
|
row["reminders"] += metrics["reminders"]
|
|
row["businessOutputs"] += (
|
|
metrics["risk_observations"]
|
|
+ metrics["risk_clues"]
|
|
+ metrics["profile_snapshots"]
|
|
+ metrics["knowledge_documents"]
|
|
+ metrics["finance_snapshots"]
|
|
+ metrics["reminders"]
|
|
)
|
|
|
|
return [rows[label] for label in labels]
|
|
|
|
def _task_distribution(self, runs: list[AgentRun]) -> list[dict[str, Any]]:
|
|
buckets: dict[str, dict[str, Any]] = {}
|
|
for run in runs:
|
|
task_type = self._resolve_task_type(run)
|
|
spec = self._task_spec(task_type)
|
|
bucket = buckets.setdefault(
|
|
task_type or "unknown",
|
|
{
|
|
"taskType": task_type or "unknown",
|
|
"name": spec["label"],
|
|
"category": spec["category"],
|
|
"count": 0,
|
|
"success": 0,
|
|
"failed": 0,
|
|
"value": 0,
|
|
"color": spec["color"],
|
|
},
|
|
)
|
|
bucket["count"] += 1
|
|
bucket["value"] += 1
|
|
if self._is_success(run.status):
|
|
bucket["success"] += 1
|
|
elif self._is_failed(run.status):
|
|
bucket["failed"] += 1
|
|
|
|
return sorted(buckets.values(), key=lambda item: (-item["count"], item["name"]))[:8]
|
|
|
|
def _category_distribution(self, runs: list[AgentRun]) -> list[dict[str, Any]]:
|
|
rows = {
|
|
category: {
|
|
"name": category,
|
|
"value": 0,
|
|
"count": 0,
|
|
"success": 0,
|
|
"failed": 0,
|
|
"color": spec["color"],
|
|
"description": spec["description"],
|
|
}
|
|
for category, spec in CATEGORY_SPECS.items()
|
|
}
|
|
for run in runs:
|
|
category = self._task_spec(self._resolve_task_type(run))["category"]
|
|
row = rows.setdefault(
|
|
category,
|
|
{
|
|
"name": category,
|
|
"value": 0,
|
|
"count": 0,
|
|
"success": 0,
|
|
"failed": 0,
|
|
"color": "var(--theme-primary)",
|
|
"description": "其他数字员工工作",
|
|
},
|
|
)
|
|
row["value"] += 1
|
|
row["count"] += 1
|
|
if self._is_success(run.status):
|
|
row["success"] += 1
|
|
elif self._is_failed(run.status):
|
|
row["failed"] += 1
|
|
return list(rows.values())
|
|
|
|
def _recent_runs(self, runs: list[AgentRun]) -> list[dict[str, Any]]:
|
|
rows = []
|
|
for run in sorted(runs, key=lambda item: item.started_at, reverse=True)[:8]:
|
|
task_type = self._resolve_task_type(run)
|
|
spec = self._task_spec(task_type)
|
|
rows.append(
|
|
{
|
|
"runId": run.run_id,
|
|
"taskType": task_type or "unknown",
|
|
"taskLabel": spec["label"],
|
|
"category": spec["category"],
|
|
"status": run.status,
|
|
"statusLabel": self._status_label(run.status),
|
|
"statusTone": self._status_tone(run.status),
|
|
"source": run.source,
|
|
"sourceLabel": self._source_label(run.source),
|
|
"startedAt": self._iso(run.started_at),
|
|
"finishedAt": self._iso(run.finished_at),
|
|
"durationMs": self._duration_ms(run),
|
|
"summary": self._summary_text(run),
|
|
"metrics": self._extract_run_metrics(run),
|
|
}
|
|
)
|
|
return rows
|
|
|
|
def _sum_metrics(self, runs: list[AgentRun]) -> dict[str, int]:
|
|
totals = self._empty_metrics()
|
|
for run in runs:
|
|
metrics = self._extract_run_metrics(run)
|
|
for key in totals:
|
|
totals[key] += int(metrics.get(key) or 0)
|
|
return totals
|
|
|
|
def _extract_run_metrics(self, run: AgentRun) -> dict[str, int]:
|
|
summary = self._extract_run_summary(run)
|
|
route_json = run.route_json or {}
|
|
metrics = self._empty_metrics()
|
|
metrics["risk_observations"] = self._first_int(
|
|
summary,
|
|
("risk_observation_count", "risk_observations", "created_observation_count"),
|
|
)
|
|
metrics["risk_clues"] = self._first_int(
|
|
summary,
|
|
("risk_clue_count", "risk_clues", "created_clue_count"),
|
|
)
|
|
metrics["profile_snapshots"] = self._first_int(
|
|
summary,
|
|
("snapshot_count", "profile_snapshot_count", "profile_snapshots"),
|
|
)
|
|
if self._resolve_task_type(run) == "finance_dashboard_snapshot":
|
|
metrics["profile_snapshots"] = 0
|
|
metrics["finance_snapshots"] = self._first_int(
|
|
summary,
|
|
("finance_snapshot_count", "dashboard_snapshot_count"),
|
|
)
|
|
metrics["knowledge_documents"] = max(
|
|
self._first_int(
|
|
summary,
|
|
("knowledge_document_count", "document_count", "processed_document_count"),
|
|
),
|
|
self._list_length(summary, ("document_ids", "requested_document_ids")),
|
|
self._list_length(route_json, ("document_ids", "requested_document_ids")),
|
|
)
|
|
metrics["scanned_claims"] = self._first_int(summary, ("scanned_claim_count", "claim_count"))
|
|
metrics["target_employees"] = self._first_int(
|
|
summary,
|
|
("target_employee_count", "employee_count"),
|
|
)
|
|
metrics["rule_hits"] = self._first_int(summary, ("rule_hit_count", "rule_hits"))
|
|
metrics["facts"] = self._first_int(summary, ("fact_count", "facts"))
|
|
metrics["reminders"] = self._first_int(
|
|
summary,
|
|
(
|
|
"reminder_count",
|
|
"reminders",
|
|
"approval_pending_count",
|
|
"budget_reminder_count",
|
|
),
|
|
)
|
|
return metrics
|
|
|
|
@staticmethod
|
|
def _empty_metrics() -> dict[str, int]:
|
|
return {
|
|
"risk_observations": 0,
|
|
"risk_clues": 0,
|
|
"profile_snapshots": 0,
|
|
"finance_snapshots": 0,
|
|
"knowledge_documents": 0,
|
|
"scanned_claims": 0,
|
|
"target_employees": 0,
|
|
"rule_hits": 0,
|
|
"facts": 0,
|
|
"reminders": 0,
|
|
}
|
|
|
|
def _extract_run_summary(self, run: AgentRun) -> dict[str, Any]:
|
|
task_type = self._resolve_task_type(run)
|
|
matched_tool = self._matched_tool_call(run, task_type)
|
|
if matched_tool is None:
|
|
return run.route_json or {}
|
|
response = matched_tool.response_json or {}
|
|
if isinstance(response, dict) and isinstance(response.get("summary"), dict):
|
|
return response["summary"]
|
|
return response if isinstance(response, dict) else {}
|
|
|
|
def _matched_tool_call(self, run: AgentRun, task_type: str) -> AgentToolCall | None:
|
|
digital_tools = [
|
|
tool
|
|
for tool in run.tool_calls
|
|
if str(tool.tool_name or "").startswith("digital_employee.")
|
|
]
|
|
for tool in run.tool_calls:
|
|
candidates = [
|
|
(tool.request_json or {}).get("task_type"),
|
|
(tool.request_json or {}).get("job_type"),
|
|
(tool.response_json or {}).get("report_type"),
|
|
(tool.response_json or {}).get("task_type"),
|
|
(tool.response_json or {}).get("job_type"),
|
|
self._task_type_from_tool_name(tool.tool_name),
|
|
]
|
|
if task_type and task_type in {self._normalize_task_type(item) for item in candidates}:
|
|
return tool
|
|
if digital_tools:
|
|
return digital_tools[0]
|
|
return run.tool_calls[0] if run.tool_calls else None
|
|
|
|
def _is_digital_employee_run(self, run: AgentRun) -> bool:
|
|
task_type = self._resolve_task_type(run)
|
|
if task_type in TASK_SPECS:
|
|
return True
|
|
if run.agent == AgentName.HERMES.value:
|
|
return True
|
|
if run.source == AgentRunSource.SCHEDULE.value and task_type:
|
|
return True
|
|
route_json = run.route_json or {}
|
|
if str(route_json.get("selected_agent") or "").strip() == AgentName.HERMES.value:
|
|
return True
|
|
return any(
|
|
str(tool.tool_name or "").startswith("digital_employee.")
|
|
for tool in run.tool_calls
|
|
)
|
|
|
|
def _resolve_task_type(self, run: AgentRun) -> str:
|
|
route_json = run.route_json or {}
|
|
route_candidates = [
|
|
route_json.get("job_type"),
|
|
route_json.get("task_type"),
|
|
route_json.get("report_type"),
|
|
route_json.get("task_code"),
|
|
route_json.get("code"),
|
|
]
|
|
for candidate in route_candidates:
|
|
normalized = self._normalize_task_type(candidate)
|
|
if normalized:
|
|
return normalized
|
|
|
|
for tool in run.tool_calls:
|
|
for candidate in (
|
|
(tool.request_json or {}).get("task_type"),
|
|
(tool.request_json or {}).get("job_type"),
|
|
(tool.response_json or {}).get("report_type"),
|
|
(tool.response_json or {}).get("task_type"),
|
|
(tool.response_json or {}).get("job_type"),
|
|
self._task_type_from_tool_name(tool.tool_name),
|
|
):
|
|
normalized = self._normalize_task_type(candidate)
|
|
if normalized:
|
|
return normalized
|
|
return ""
|
|
|
|
@staticmethod
|
|
def _normalize_task_type(value: Any) -> str:
|
|
text = str(value or "").strip()
|
|
if not text:
|
|
return ""
|
|
text = TASK_CODE_TO_TYPE.get(text, text)
|
|
if text.startswith("task.hermes."):
|
|
text = text.removeprefix("task.hermes.")
|
|
text = text.replace("-", "_").replace(".", "_")
|
|
if text == "risk_rule_discovery":
|
|
return "risk_clue_collect"
|
|
return text
|
|
|
|
@staticmethod
|
|
def _task_type_from_tool_name(value: str | None) -> str:
|
|
name = str(value or "")
|
|
if "financial_risk_graph" in name:
|
|
return "global_risk_scan"
|
|
if "employee_behavior_profile" in name:
|
|
return "employee_behavior_profile_scan"
|
|
if "reminder" in name:
|
|
return "digital_employee_reminder_scan"
|
|
if "finance_policy_knowledge" in name:
|
|
return "finance_policy_knowledge_organize"
|
|
if "risk_clue" in name:
|
|
return "risk_clue_collect"
|
|
return ""
|
|
|
|
@staticmethod
|
|
def _task_spec(task_type: str) -> dict[str, str]:
|
|
return TASK_SPECS.get(
|
|
task_type,
|
|
{
|
|
"label": "数字员工工作",
|
|
"category": "评估",
|
|
"color": "var(--theme-primary)",
|
|
},
|
|
)
|
|
|
|
def _summary_text(self, run: AgentRun) -> str:
|
|
text = str(run.result_summary or "").strip()
|
|
if text:
|
|
return text
|
|
summary = self._extract_run_summary(run)
|
|
for key in ("message", "summary", "result_summary"):
|
|
value = str(summary.get(key) or "").strip()
|
|
if value:
|
|
return value
|
|
if run.error_message:
|
|
return str(run.error_message)
|
|
return "暂无摘要。"
|
|
|
|
@staticmethod
|
|
def _first_int(payload: Any, keys: tuple[str, ...]) -> int:
|
|
if isinstance(payload, dict):
|
|
for key in keys:
|
|
value = payload.get(key)
|
|
if isinstance(value, (int, float)) and value > 0:
|
|
return int(value)
|
|
for value in payload.values():
|
|
found = DigitalEmployeeDashboardService._first_int(value, keys)
|
|
if found:
|
|
return found
|
|
if isinstance(payload, list):
|
|
for value in payload:
|
|
found = DigitalEmployeeDashboardService._first_int(value, keys)
|
|
if found:
|
|
return found
|
|
return 0
|
|
|
|
@staticmethod
|
|
def _list_length(payload: Any, keys: tuple[str, ...]) -> int:
|
|
if isinstance(payload, dict):
|
|
for key in keys:
|
|
value = payload.get(key)
|
|
if isinstance(value, list):
|
|
return len(value)
|
|
for value in payload.values():
|
|
found = DigitalEmployeeDashboardService._list_length(value, keys)
|
|
if found:
|
|
return found
|
|
if isinstance(payload, list):
|
|
for value in payload:
|
|
found = DigitalEmployeeDashboardService._list_length(value, keys)
|
|
if found:
|
|
return found
|
|
return 0
|
|
|
|
@staticmethod
|
|
def _percent(value: int | float, total: int | float) -> float:
|
|
if not total:
|
|
return 0.0
|
|
return round((float(value) / float(total)) * 100, 1)
|
|
|
|
@staticmethod
|
|
def _duration_ms(run: AgentRun) -> int:
|
|
if not run.finished_at:
|
|
return 0
|
|
try:
|
|
finished_at = DigitalEmployeeDashboardService._as_utc(run.finished_at)
|
|
started_at = DigitalEmployeeDashboardService._as_utc(run.started_at)
|
|
return max(0, int((finished_at - started_at).total_seconds() * 1000))
|
|
except TypeError:
|
|
return 0
|
|
|
|
@staticmethod
|
|
def _date_labels(start_date: date, days: int) -> list[str]:
|
|
return [(start_date + timedelta(days=index)).strftime("%m-%d") for index in range(days)]
|
|
|
|
@staticmethod
|
|
def _date_label(value: datetime | None) -> str:
|
|
if value is None:
|
|
return ""
|
|
return DigitalEmployeeDashboardService._as_utc(value).strftime("%m-%d")
|
|
|
|
@staticmethod
|
|
def _iso(value: datetime | None) -> str:
|
|
if value is None:
|
|
return ""
|
|
return DigitalEmployeeDashboardService._as_utc(value).isoformat()
|
|
|
|
@staticmethod
|
|
def _as_utc(value: datetime) -> datetime:
|
|
if value.tzinfo is None:
|
|
return value.replace(tzinfo=UTC)
|
|
return value.astimezone(UTC)
|
|
|
|
@staticmethod
|
|
def _is_success(status: str | None) -> bool:
|
|
return str(status or "").strip().lower() in SUCCESS_STATUSES
|
|
|
|
@staticmethod
|
|
def _is_failed(status: str | None) -> bool:
|
|
return str(status or "").strip().lower() in FAILED_STATUSES
|
|
|
|
@staticmethod
|
|
def _is_running(status: str | None) -> bool:
|
|
return str(status or "").strip().lower() in RUNNING_STATUSES
|
|
|
|
def _status_label(self, status: str | None) -> str:
|
|
if self._is_success(status):
|
|
return "成功"
|
|
if self._is_failed(status):
|
|
return "失败"
|
|
if self._is_running(status):
|
|
return "运行中"
|
|
return str(status or "其他")
|
|
|
|
def _status_tone(self, status: str | None) -> str:
|
|
if self._is_success(status):
|
|
return "success"
|
|
if self._is_failed(status):
|
|
return "danger"
|
|
if self._is_running(status):
|
|
return "warning"
|
|
return "neutral"
|
|
|
|
@staticmethod
|
|
def _source_label(source: str | None) -> str:
|
|
labels = {
|
|
"schedule": "定时任务",
|
|
"system_event": "系统事件",
|
|
"user_message": "用户触发",
|
|
}
|
|
text = str(source or "").strip()
|
|
return labels.get(text, text or "未标记")
|