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

@@ -0,0 +1,117 @@
from enum import StrEnum
class StatusValue(StrEnum):
INITIATED = "立项"
TODO = "待办"
DRAFT = "草稿"
POLICY_DRAFT = "草案"
VALID = "有效"
APPROVING = "审批中"
PENDING_APPROVAL = "待审批"
PENDING = "pending"
ACCEPTED = "验收"
COMPLETED_CN = "已完成"
DONE_CN = "完成"
REVIEWED = "复盘"
ARCHIVED = "归档"
CLOSED_CN = "关闭"
CLOSED = "closed"
RESOLVED = "resolved"
DONE = "done"
COMPLETED = "completed"
GENERATED = "已生成"
UNKNOWN = "未知"
PAID = "已付款"
UNPAID = "未付款"
UNDELIVERED = "未到货"
INVOICE_NOT_RECEIVED = "未收票"
NORMAL = "normal"
NORMAL_CN = "正常"
LATE = "迟到"
LEAVE_EARLY = "早退"
MISSING_PUNCH = "缺卡"
ABSENT = "旷工"
ABNORMAL = "异常"
OPEN = "open"
RUNNING = "running"
DRY_RUN = "dry_run"
class RiskLevel(StrEnum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class PriorityValue(StrEnum):
P2 = "P2"
class SourceSystem(StrEnum):
INTERNAL = "internal"
LEGACY_MYSQL = "legacy_mysql"
class AccountType(StrEnum):
BANK = "bank"
class VersionValue(StrEnum):
V1_0 = "v1.0"
class BusinessDomain(StrEnum):
TASKS = "tasks"
PROJECTS = "projects"
FUND_ACCOUNTS = "fund-accounts"
SUPPLIERS = "suppliers"
class RiskEventType(StrEnum):
OVERDUE_TASK = "overdue_task"
DELAYED_PROJECT = "delayed_project"
OVER_BUDGET_PROJECT = "over_budget_project"
FUND_SAFETY_LINE = "fund_safety_line"
SUPPLIER_RISK = "supplier_risk"
DONE_STATUSES = frozenset(
{
StatusValue.DONE_CN,
StatusValue.COMPLETED_CN,
StatusValue.CLOSED_CN,
StatusValue.DONE,
StatusValue.COMPLETED,
StatusValue.CLOSED,
}
)
PENDING_APPROVAL_STATUSES = frozenset(
{
StatusValue.DRAFT,
StatusValue.APPROVING,
StatusValue.PENDING_APPROVAL,
StatusValue.PENDING,
}
)
PROJECT_CLOSED_STATUSES = frozenset(
{
StatusValue.ACCEPTED,
StatusValue.COMPLETED_CN,
StatusValue.REVIEWED,
StatusValue.ARCHIVED,
StatusValue.CLOSED_CN,
StatusValue.CLOSED,
}
)
ATTENDANCE_ABNORMAL_STATUSES = frozenset(
{
StatusValue.LATE,
StatusValue.LEAVE_EARLY,
StatusValue.MISSING_PUNCH,
StatusValue.ABSENT,
StatusValue.ABNORMAL,
}
)
SUPPLIER_RISK_LEVELS = frozenset({RiskLevel.MEDIUM, RiskLevel.HIGH})
CLOSED_RISK_STATUSES = frozenset({StatusValue.CLOSED, StatusValue.RESOLVED})

View File

@@ -4,7 +4,16 @@ from decimal import Decimal
from sqlalchemy import JSON, Date, DateTime, Integer, Numeric, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.core.constants import ActorValue
from app.core.database import Base
from app.modules.business.constants import (
AccountType,
PriorityValue,
RiskLevel,
SourceSystem,
StatusValue,
VersionValue,
)
class TimestampMixin:
@@ -21,16 +30,16 @@ class Project(Base, TimestampMixin):
code: Mapped[str] = mapped_column(String(64), unique=True, index=True)
name: Mapped[str] = mapped_column(String(255), index=True)
owner: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
status: Mapped[str] = mapped_column(String(64), default="立项", index=True)
priority: Mapped[str] = mapped_column(String(32), default="P2")
status: Mapped[str] = mapped_column(String(64), default=StatusValue.INITIATED, index=True)
priority: Mapped[str] = mapped_column(String(32), default=PriorityValue.P2)
progress_percent: Mapped[int] = mapped_column(Integer, default=0)
risk_level: Mapped[str] = mapped_column(String(32), default="low", index=True)
risk_level: Mapped[str] = mapped_column(String(32), default=RiskLevel.LOW, index=True)
budget_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=0)
actual_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=0)
start_date: Mapped[date | None] = mapped_column(Date, nullable=True)
due_date: Mapped[date | None] = mapped_column(Date, nullable=True, index=True)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
source_system: Mapped[str] = mapped_column(String(64), default="internal")
source_system: Mapped[str] = mapped_column(String(64), default=SourceSystem.INTERNAL)
external_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
@@ -42,8 +51,8 @@ class WorkTask(Base, TimestampMixin):
title: Mapped[str] = mapped_column(String(255), index=True)
project_code: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
owner: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
status: Mapped[str] = mapped_column(String(64), default="待办", index=True)
priority: Mapped[str] = mapped_column(String(32), default="P2")
status: Mapped[str] = mapped_column(String(64), default=StatusValue.TODO, index=True)
priority: Mapped[str] = mapped_column(String(32), default=PriorityValue.P2)
due_date: Mapped[date | None] = mapped_column(Date, nullable=True, index=True)
completed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
blocker: Mapped[str | None] = mapped_column(Text, nullable=True)
@@ -62,9 +71,13 @@ class Procurement(Base, TimestampMixin):
budget_subject: Mapped[str | None] = mapped_column(String(128), nullable=True)
expected_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=0)
actual_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=0)
approval_status: Mapped[str] = mapped_column(String(64), default="草稿", index=True)
delivery_status: Mapped[str] = mapped_column(String(64), default="未到货", index=True)
payment_status: Mapped[str] = mapped_column(String(64), default="未付款", index=True)
approval_status: Mapped[str] = mapped_column(String(64), default=StatusValue.DRAFT, index=True)
delivery_status: Mapped[str] = mapped_column(
String(64),
default=StatusValue.UNDELIVERED,
index=True,
)
payment_status: Mapped[str] = mapped_column(String(64), default=StatusValue.UNPAID, index=True)
comparison_summary: Mapped[str | None] = mapped_column(Text, nullable=True)
@@ -80,9 +93,12 @@ class Expense(Base, TimestampMixin):
project_code: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
budget_subject: Mapped[str | None] = mapped_column(String(128), nullable=True)
payment_account: Mapped[str | None] = mapped_column(String(128), nullable=True)
invoice_status: Mapped[str] = mapped_column(String(64), default="未收票")
approval_status: Mapped[str] = mapped_column(String(64), default="草稿", index=True)
payment_status: Mapped[str] = mapped_column(String(64), default="未付款", index=True)
invoice_status: Mapped[str] = mapped_column(
String(64),
default=StatusValue.INVOICE_NOT_RECEIVED,
)
approval_status: Mapped[str] = mapped_column(String(64), default=StatusValue.DRAFT, index=True)
payment_status: Mapped[str] = mapped_column(String(64), default=StatusValue.UNPAID, index=True)
class FundAccount(Base, TimestampMixin):
@@ -91,12 +107,12 @@ class FundAccount(Base, TimestampMixin):
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
code: Mapped[str] = mapped_column(String(64), unique=True, index=True)
name: Mapped[str] = mapped_column(String(255), index=True)
account_type: Mapped[str] = mapped_column(String(64), default="bank")
account_type: Mapped[str] = mapped_column(String(64), default=AccountType.BANK)
current_balance: Mapped[Decimal] = mapped_column(Numeric(16, 2), default=0)
expected_receivable: Mapped[Decimal] = mapped_column(Numeric(16, 2), default=0)
expected_payable: Mapped[Decimal] = mapped_column(Numeric(16, 2), default=0)
safety_line: Mapped[Decimal] = mapped_column(Numeric(16, 2), default=0)
risk_level: Mapped[str] = mapped_column(String(32), default="low", index=True)
risk_level: Mapped[str] = mapped_column(String(32), default=RiskLevel.LOW, index=True)
note: Mapped[str | None] = mapped_column(Text, nullable=True)
@@ -108,8 +124,8 @@ class Policy(Base, TimestampMixin):
title: Mapped[str] = mapped_column(String(255), index=True)
policy_type: Mapped[str | None] = mapped_column(String(128), nullable=True)
owner_department: Mapped[str | None] = mapped_column(String(128), nullable=True)
version: Mapped[str] = mapped_column(String(32), default="v1.0")
status: Mapped[str] = mapped_column(String(64), default="草案", index=True)
version: Mapped[str] = mapped_column(String(32), default=VersionValue.V1_0)
status: Mapped[str] = mapped_column(String(64), default=StatusValue.POLICY_DRAFT, index=True)
effective_date: Mapped[date | None] = mapped_column(Date, nullable=True)
feishu_doc_url: Mapped[str | None] = mapped_column(String(1024), nullable=True)
summary: Mapped[str | None] = mapped_column(Text, nullable=True)
@@ -123,7 +139,7 @@ class Standard(Base, TimestampMixin):
title: Mapped[str] = mapped_column(String(255), index=True)
standard_type: Mapped[str | None] = mapped_column(String(128), nullable=True)
applies_to: Mapped[str | None] = mapped_column(String(255), nullable=True)
status: Mapped[str] = mapped_column(String(64), default="有效", index=True)
status: Mapped[str] = mapped_column(String(64), default=StatusValue.VALID, index=True)
check_items: Mapped[str | None] = mapped_column(Text, nullable=True)
remediation: Mapped[str | None] = mapped_column(Text, nullable=True)
policy_code: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
@@ -141,7 +157,7 @@ class PerformanceMetric(Base, TimestampMixin):
data_source: Mapped[str | None] = mapped_column(String(255), nullable=True)
auto_score: Mapped[Decimal] = mapped_column(Numeric(8, 2), default=0)
confirmed_score: Mapped[Decimal | None] = mapped_column(Numeric(8, 2), nullable=True)
status: Mapped[str] = mapped_column(String(64), default="草稿", index=True)
status: Mapped[str] = mapped_column(String(64), default=StatusValue.DRAFT, index=True)
class Supplier(Base, TimestampMixin):
@@ -155,8 +171,12 @@ class Supplier(Base, TimestampMixin):
quality_score: Mapped[Decimal] = mapped_column(Numeric(5, 2), default=0)
delivery_score: Mapped[Decimal] = mapped_column(Numeric(5, 2), default=0)
price_score: Mapped[Decimal] = mapped_column(Numeric(5, 2), default=0)
risk_level: Mapped[str] = mapped_column(String(32), default="low", index=True)
blacklist_status: Mapped[str] = mapped_column(String(32), default="normal", index=True)
risk_level: Mapped[str] = mapped_column(String(32), default=RiskLevel.LOW, index=True)
blacklist_status: Mapped[str] = mapped_column(
String(32),
default=StatusValue.NORMAL,
index=True,
)
class AttendanceRecord(Base, TimestampMixin):
@@ -171,9 +191,9 @@ class AttendanceRecord(Base, TimestampMixin):
work_date: Mapped[date] = mapped_column(Date, index=True)
check_in_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
check_out_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
status: Mapped[str] = mapped_column(String(64), default="正常", index=True)
status: Mapped[str] = mapped_column(String(64), default=StatusValue.NORMAL_CN, index=True)
location: Mapped[str | None] = mapped_column(String(255), nullable=True)
source_system: Mapped[str] = mapped_column(String(64), default="internal")
source_system: Mapped[str] = mapped_column(String(64), default=SourceSystem.INTERNAL)
external_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
note: Mapped[str | None] = mapped_column(Text, nullable=True)
@@ -185,7 +205,7 @@ class WorkReport(Base, TimestampMixin):
code: Mapped[str] = mapped_column(String(64), unique=True, index=True)
report_type: Mapped[str] = mapped_column(String(32), index=True)
title: Mapped[str] = mapped_column(String(255), index=True)
reporter: Mapped[str] = mapped_column(String(128), default="system", index=True)
reporter: Mapped[str] = mapped_column(String(128), default=ActorValue.SYSTEM, index=True)
department: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
project_code: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
period_start: Mapped[date] = mapped_column(Date, index=True)
@@ -193,8 +213,8 @@ class WorkReport(Base, TimestampMixin):
content: Mapped[str] = mapped_column(Text)
metrics: Mapped[dict | None] = mapped_column(JSON, nullable=True)
risk_summary: Mapped[dict | None] = mapped_column(JSON, nullable=True)
status: Mapped[str] = mapped_column(String(64), default="已生成", index=True)
source_system: Mapped[str] = mapped_column(String(64), default="internal")
status: Mapped[str] = mapped_column(String(64), default=StatusValue.GENERATED, index=True)
source_system: Mapped[str] = mapped_column(String(64), default=SourceSystem.INTERNAL)
class RiskEvent(Base, TimestampMixin):
@@ -204,8 +224,8 @@ class RiskEvent(Base, TimestampMixin):
code: Mapped[str] = mapped_column(String(64), unique=True, index=True)
title: Mapped[str] = mapped_column(String(255), index=True)
risk_type: Mapped[str] = mapped_column(String(64), index=True)
risk_level: Mapped[str] = mapped_column(String(32), default="medium", index=True)
status: Mapped[str] = mapped_column(String(32), default="open", index=True)
risk_level: Mapped[str] = mapped_column(String(32), default=RiskLevel.MEDIUM, index=True)
status: Mapped[str] = mapped_column(String(32), default=StatusValue.OPEN, index=True)
source_domain: Mapped[str] = mapped_column(String(128), index=True)
source_record_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
project_code: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
@@ -224,7 +244,7 @@ class LegacySyncRun(Base, TimestampMixin):
code: Mapped[str] = mapped_column(String(64), unique=True, index=True)
domain: Mapped[str] = mapped_column(String(128), index=True)
source_table: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
status: Mapped[str] = mapped_column(String(32), default="running", index=True)
status: Mapped[str] = mapped_column(String(32), default=StatusValue.RUNNING, index=True)
started_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
finished_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
created_count: Mapped[int] = mapped_column(Integer, default=0)

View File

@@ -2,15 +2,17 @@ from typing import Any
from pydantic import BaseModel, Field
from app.core.constants import ActorValue
class DomainRecordCreate(BaseModel):
data: dict[str, Any] = Field(..., description="Domain fields to create.")
actor: str = "api"
actor: str = ActorValue.API
class DomainRecordUpdate(BaseModel):
data: dict[str, Any] = Field(..., description="Domain fields to update.")
actor: str = "api"
actor: str = ActorValue.API
approval_ticket_id: str | None = Field(
default=None,
description="Required by policy for high-risk updates such as funds or performance.",

View File

@@ -10,6 +10,8 @@ from sqlalchemy import Select, func, select
from sqlalchemy.sql.schema import Column
from sqlalchemy.orm import Session
from app.core.constants import ActorValue
from app.modules.audit.constants import AuditRiskLevel, AuditSource
from app.modules.audit.schemas import AuditLogCreate
from app.modules.audit.service import AuditService
from app.modules.approvals.service import ApprovalService
@@ -99,7 +101,7 @@ class BusinessService:
self,
domain: str,
data: dict[str, Any],
actor: str = "api",
actor: str = ActorValue.API,
) -> dict[str, Any]:
model = get_domain_model(domain)
payload = _model_payload(model, data)
@@ -111,7 +113,7 @@ class BusinessService:
self.audit.log(
AuditLogCreate(
actor=actor,
source="api",
source=AuditSource.API,
action=f"create:{domain}",
target_type=domain,
target_id=str(record.id),
@@ -126,7 +128,7 @@ class BusinessService:
domain: str,
record_id: int,
data: dict[str, Any],
actor: str = "api",
actor: str = ActorValue.API,
approval_ticket_id: str | None = None,
) -> dict[str, Any]:
if domain in HIGH_RISK_DOMAINS:
@@ -160,11 +162,13 @@ class BusinessService:
self.audit.log(
AuditLogCreate(
actor=actor,
source="api",
source=AuditSource.API,
action=f"update:{domain}",
target_type=domain,
target_id=str(record.id),
risk_level="high" if domain in HIGH_RISK_DOMAINS else "low",
risk_level=(
AuditRiskLevel.HIGH if domain in HIGH_RISK_DOMAINS else AuditRiskLevel.LOW
),
request_payload={"data": data, "approval_ticket_id": approval_ticket_id},
response_payload=result,
)