2026-05-19 20:23:58 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import hashlib
|
|
|
|
|
|
import json
|
|
|
|
|
|
from datetime import UTC, date, datetime
|
|
|
|
|
|
from decimal import Decimal
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
from sqlalchemy import inspect, select, text
|
2026-05-17 08:38:41 +00:00
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
|
|
|
|
|
|
from app.core.agent_enums import (
|
|
|
|
|
|
AgentAssetContentType,
|
|
|
|
|
|
AgentAssetDomain,
|
|
|
|
|
|
AgentAssetStatus,
|
|
|
|
|
|
AgentAssetType,
|
|
|
|
|
|
AgentName,
|
|
|
|
|
|
AgentPermissionLevel,
|
|
|
|
|
|
AgentReviewStatus,
|
|
|
|
|
|
AgentRunSource,
|
|
|
|
|
|
AgentRunStatus,
|
|
|
|
|
|
AgentToolType,
|
|
|
|
|
|
)
|
|
|
|
|
|
from app.core.config import get_settings
|
|
|
|
|
|
from app.core.logging import get_logger
|
|
|
|
|
|
from app.db.base import Base
|
|
|
|
|
|
from app.db.session import get_session_factory
|
|
|
|
|
|
from app.models.agent_asset import AgentAsset, AgentAssetReview, AgentAssetVersion
|
|
|
|
|
|
from app.models.agent_run import AgentRun, AgentToolCall, SemanticParseLog
|
|
|
|
|
|
from app.models.audit_log import AuditLog
|
2026-05-19 20:23:58 +08:00
|
|
|
|
from app.models.financial_record import (
|
|
|
|
|
|
AccountsPayableRecord,
|
|
|
|
|
|
AccountsReceivableRecord,
|
|
|
|
|
|
ExpenseClaim,
|
|
|
|
|
|
ExpenseClaimItem,
|
|
|
|
|
|
)
|
|
|
|
|
|
from app.services.agent_asset_rule_library import AgentAssetRuleLibraryManager
|
|
|
|
|
|
from app.services.agent_asset_spreadsheet import (
|
|
|
|
|
|
AgentAssetSpreadsheetManager,
|
|
|
|
|
|
COMPANY_COMMUNICATION_EXPENSE_RULE_CODE,
|
|
|
|
|
|
COMPANY_COMMUNICATION_EXPENSE_RULE_FILENAME,
|
|
|
|
|
|
COMPANY_TRAVEL_EXPENSE_RULE_CODE,
|
|
|
|
|
|
COMPANY_TRAVEL_EXPENSE_RULE_FILENAME,
|
|
|
|
|
|
FINANCE_RULES_LIBRARY,
|
|
|
|
|
|
RISK_RULES_LIBRARY,
|
|
|
|
|
|
RuleSpreadsheetMeta,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
PLATFORM_DESTINATION_LOCATION_RULE_CODE = "risk.travel.destination_receipt_location"
|
|
|
|
|
|
PLATFORM_DESTINATION_LOCATION_RULE_FILENAME = "risk.travel.destination_receipt_location.json"
|
|
|
|
|
|
from app.services.expense_rule_runtime import (
|
|
|
|
|
|
build_scene_submission_standard_markdown,
|
|
|
|
|
|
build_travel_risk_control_standard_markdown,
|
|
|
|
|
|
)
|
2026-05-17 08:38:41 +00:00
|
|
|
|
|
|
|
|
|
|
logger = get_logger("app.services.agent_foundation")
|
|
|
|
|
|
|
|
|
|
|
|
DEMO_EXPENSE_CLAIM_SIGNATURES = {
|
|
|
|
|
|
(
|
|
|
|
|
|
"EXP-202605-001",
|
|
|
|
|
|
"张三",
|
|
|
|
|
|
"华南客户拜访差旅报销",
|
|
|
|
|
|
"3280.00",
|
|
|
|
|
|
"submitted",
|
|
|
|
|
|
),
|
|
|
|
|
|
(
|
|
|
|
|
|
"EXP-202605-002",
|
|
|
|
|
|
"李四",
|
|
|
|
|
|
"客户路演餐费",
|
|
|
|
|
|
"860.00",
|
|
|
|
|
|
"approved",
|
|
|
|
|
|
),
|
|
|
|
|
|
(
|
|
|
|
|
|
"EXP-202605-003",
|
|
|
|
|
|
"王五",
|
|
|
|
|
|
"市场活动会务差旅",
|
|
|
|
|
|
"3280.00",
|
|
|
|
|
|
"review",
|
|
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
DEMO_RECEIVABLE_SIGNATURES = {
|
|
|
|
|
|
("AR-202605-001", "客户A", "50000.00", "partial"),
|
|
|
|
|
|
("AR-202605-002", "客户B", "78000.00", "overdue"),
|
|
|
|
|
|
}
|
|
|
|
|
|
DEMO_PAYABLE_SIGNATURES = {
|
|
|
|
|
|
("AP-202605-001", "供应商A", "33000.00", "scheduled"),
|
|
|
|
|
|
("AP-202605-002", "供应商B", "96000.00", "overdue"),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
LEGACY_RULE_CODES = (
|
|
|
|
|
|
"rule.expense.duplicate_expense_check",
|
|
|
|
|
|
"rule.expense.travel_receipt_requirements",
|
|
|
|
|
|
"rule.ap.payment_dual_review",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-20 09:36:01 +08:00
|
|
|
|
ATTACHMENT_RULE_ASSET_CODE = "rule.expense.attachment_submission_requirements"
|
|
|
|
|
|
COMPANY_TRAVEL_RULE_VERSION = "v1.0.0"
|
|
|
|
|
|
COMPANY_COMMUNICATION_RULE_VERSION = "v1.0.0"
|
|
|
|
|
|
COMPANY_TRAVEL_RULE_SCENARIO_JSON = ("差旅",)
|
|
|
|
|
|
COMPANY_COMMUNICATION_RULE_SCENARIO_JSON = ("费用科目",)
|
2026-05-17 08:38:41 +00:00
|
|
|
|
|
|
|
|
|
|
ATTACHMENT_RULE_RUNTIME_CONFIG = {
|
|
|
|
|
|
"kind": "policy_rule_draft",
|
|
|
|
|
|
"version": 1,
|
|
|
|
|
|
"template_key": "attachment_requirement_v1",
|
|
|
|
|
|
"rule_name": "报销附件与单据完整性规则",
|
|
|
|
|
|
"scenario": "attachment_policy",
|
|
|
|
|
|
"source_document_name": "报销制度 / 单据与附件要求",
|
|
|
|
|
|
"review_required": True,
|
|
|
|
|
|
"target": {
|
|
|
|
|
|
"expense_types": [
|
|
|
|
|
|
"travel",
|
|
|
|
|
|
"hotel",
|
|
|
|
|
|
"transport",
|
|
|
|
|
|
"meal",
|
|
|
|
|
|
"office",
|
|
|
|
|
|
"meeting",
|
|
|
|
|
|
"training",
|
|
|
|
|
|
"communication",
|
|
|
|
|
|
"welfare",
|
|
|
|
|
|
"other",
|
|
|
|
|
|
],
|
|
|
|
|
|
"scene_codes": ["expense", "attachment_policy", "invoice_anomaly"],
|
|
|
|
|
|
},
|
|
|
|
|
|
"attachment_requirements": {
|
|
|
|
|
|
"min_attachment_count": 1,
|
|
|
|
|
|
"items": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"document_type": "vat_invoice",
|
|
|
|
|
|
"required": True,
|
|
|
|
|
|
"min_count": 1,
|
|
|
|
|
|
"description": "金额类报销原则上必须提供合法票据。",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"document_type": "receipt",
|
|
|
|
|
|
"required": False,
|
|
|
|
|
|
"min_count": 1,
|
|
|
|
|
|
"description": "特殊场景无发票时需补充收据与情况说明。",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"document_type": "flight_itinerary",
|
|
|
|
|
|
"required": False,
|
|
|
|
|
|
"min_count": 1,
|
|
|
|
|
|
"description": "差旅交通报销需提供行程单或等效凭证。",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"document_type": "hotel_invoice",
|
|
|
|
|
|
"required": False,
|
|
|
|
|
|
"min_count": 1,
|
|
|
|
|
|
"description": "住宿报销需提供酒店票据或等效住宿凭证。",
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
"manual_fill_required": False,
|
|
|
|
|
|
},
|
|
|
|
|
|
"missing_attachment_action": "block",
|
|
|
|
|
|
"output": {
|
|
|
|
|
|
"risk_code": "invoice_anomaly",
|
|
|
|
|
|
"action": "block",
|
|
|
|
|
|
"message": "附件或单据不完整,需补件后再提交。",
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def prepare_agent_foundation() -> None:
|
|
|
|
|
|
settings = get_settings()
|
|
|
|
|
|
if not settings.setup_completed:
|
|
|
|
|
|
logger.info("Agent foundation bootstrap skipped because setup is incomplete")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
session_factory = get_session_factory()
|
|
|
|
|
|
with session_factory() as db:
|
|
|
|
|
|
AgentFoundationService(db).ensure_foundation_ready()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AgentFoundationService:
|
|
|
|
|
|
def __init__(self, db: Session) -> None:
|
|
|
|
|
|
self.db = db
|
|
|
|
|
|
|
2026-05-19 20:23:58 +08:00
|
|
|
|
def ensure_foundation_ready(self) -> None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
Base.metadata.create_all(bind=self.db.get_bind())
|
|
|
|
|
|
self._ensure_agent_asset_schema()
|
|
|
|
|
|
self._seed_agent_assets()
|
2026-05-17 08:38:41 +00:00
|
|
|
|
self._sync_demo_financial_records()
|
|
|
|
|
|
self._seed_runs_and_logs()
|
|
|
|
|
|
self.db.commit()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
self.db.rollback()
|
|
|
|
|
|
logger.exception("Failed to prepare agent foundation")
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
def _sync_demo_financial_records(self) -> None:
|
|
|
|
|
|
if get_settings().seed_demo_financial_records:
|
|
|
|
|
|
self._seed_financial_records()
|
|
|
|
|
|
return
|
|
|
|
|
|
self._purge_demo_financial_records()
|
|
|
|
|
|
|
2026-05-19 20:23:58 +08:00
|
|
|
|
def _seed_agent_assets(self) -> None:
|
2026-05-17 08:38:41 +00:00
|
|
|
|
existing_codes = set(self.db.scalars(select(AgentAsset.code)).all())
|
|
|
|
|
|
if existing_codes:
|
|
|
|
|
|
self._top_up_agent_assets(existing_codes)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
attachment_rule = AgentAsset(
|
|
|
|
|
|
asset_type=AgentAssetType.RULE.value,
|
|
|
|
|
|
code=ATTACHMENT_RULE_ASSET_CODE,
|
|
|
|
|
|
name="报销附件与单据完整性规则",
|
|
|
|
|
|
description="统一定义报销提交时的附件数量、票据类型和补件处理口径,作为上线前待审核规则。",
|
|
|
|
|
|
domain=AgentAssetDomain.EXPENSE.value,
|
|
|
|
|
|
scenario_json=["expense", "risk_check", "attachment_policy", "invoice_anomaly"],
|
|
|
|
|
|
owner="财务制度管理组",
|
|
|
|
|
|
reviewer="高嘉禾",
|
2026-05-19 20:23:58 +08:00
|
|
|
|
status=AgentAssetStatus.REVIEW.value,
|
|
|
|
|
|
current_version="v1.0.0",
|
|
|
|
|
|
published_version=None,
|
|
|
|
|
|
working_version="v1.0.0",
|
2026-05-17 08:38:41 +00:00
|
|
|
|
config_json={
|
|
|
|
|
|
"severity": "high",
|
|
|
|
|
|
"enabled": False,
|
|
|
|
|
|
"runtime_kind": "policy_rule_draft",
|
|
|
|
|
|
"rule_template_key": "attachment_requirement_v1",
|
|
|
|
|
|
"rule_template_label": "附件要求模板",
|
|
|
|
|
|
"runtime_rule": ATTACHMENT_RULE_RUNTIME_CONFIG,
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
scene_submission_rule = AgentAsset(
|
|
|
|
|
|
asset_type=AgentAssetType.RULE.value,
|
|
|
|
|
|
code="rule.expense.scene_submission_standard",
|
|
|
|
|
|
name="报销场景提交与附件标准",
|
|
|
|
|
|
description="统一定义各报销场景的必填字段、附件类型要求和金额阈值。",
|
|
|
|
|
|
domain=AgentAssetDomain.EXPENSE.value,
|
|
|
|
|
|
scenario_json=["expense", "risk_check", "scene_policy", "attachment_policy"],
|
|
|
|
|
|
owner="费用运营组",
|
|
|
|
|
|
reviewer="顾承宇",
|
2026-05-19 20:23:58 +08:00
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
|
|
|
|
|
current_version="v1.0.0",
|
|
|
|
|
|
published_version="v1.0.0",
|
|
|
|
|
|
working_version="v1.0.0",
|
2026-05-17 08:38:41 +00:00
|
|
|
|
config_json={
|
|
|
|
|
|
"severity": "high",
|
|
|
|
|
|
"enabled": True,
|
|
|
|
|
|
"runtime_kind": "scene_matrix",
|
|
|
|
|
|
"rule_template_label": "系统内置场景矩阵规则",
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
2026-05-19 20:23:58 +08:00
|
|
|
|
travel_policy_rule = AgentAsset(
|
|
|
|
|
|
asset_type=AgentAssetType.RULE.value,
|
|
|
|
|
|
code="rule.expense.travel_risk_control_standard",
|
|
|
|
|
|
name="差旅报销风险管控制度",
|
2026-05-17 08:38:41 +00:00
|
|
|
|
description="统一定义差旅报销的行程闭环、酒店地点一致性、职级差标和风险处置口径。",
|
|
|
|
|
|
domain=AgentAssetDomain.EXPENSE.value,
|
|
|
|
|
|
scenario_json=["expense", "risk_check", "travel_policy", "travel_standard"],
|
|
|
|
|
|
owner="风控与审计部",
|
|
|
|
|
|
reviewer="顾承宇",
|
2026-05-19 20:23:58 +08:00
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
|
|
|
|
|
current_version="v1.1.0",
|
|
|
|
|
|
published_version="v1.1.0",
|
|
|
|
|
|
working_version="v1.1.0",
|
2026-05-17 08:38:41 +00:00
|
|
|
|
config_json={
|
|
|
|
|
|
"severity": "high",
|
|
|
|
|
|
"enabled": True,
|
|
|
|
|
|
"block_on_high_risk": True,
|
|
|
|
|
|
"warning_on_medium_risk": True,
|
|
|
|
|
|
"source_doc": "document/development/risks/travel-risk-control-standard.md",
|
|
|
|
|
|
"runtime_kind": "travel_policy",
|
2026-05-19 20:23:58 +08:00
|
|
|
|
"rule_template_key": "travel_standard_v1",
|
|
|
|
|
|
"rule_template_label": "差旅标准模板",
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
company_travel_rule = AgentAsset(
|
|
|
|
|
|
asset_type=AgentAssetType.RULE.value,
|
2026-05-20 09:36:01 +08:00
|
|
|
|
code=COMPANY_TRAVEL_EXPENSE_RULE_CODE,
|
|
|
|
|
|
name="公司差旅费报销规则",
|
|
|
|
|
|
description="通过 Excel 明细表维护差旅费报销标准、票据要求和审批口径。",
|
|
|
|
|
|
domain=AgentAssetDomain.EXPENSE.value,
|
|
|
|
|
|
scenario_json=list(COMPANY_TRAVEL_RULE_SCENARIO_JSON),
|
|
|
|
|
|
owner="财务制度管理组",
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
current_version=COMPANY_TRAVEL_RULE_VERSION,
|
|
|
|
|
|
published_version=COMPANY_TRAVEL_RULE_VERSION,
|
|
|
|
|
|
working_version=COMPANY_TRAVEL_RULE_VERSION,
|
|
|
|
|
|
config_json={
|
|
|
|
|
|
"severity": "medium",
|
|
|
|
|
|
"enabled": True,
|
2026-05-20 09:36:01 +08:00
|
|
|
|
"tag": "财务规则",
|
|
|
|
|
|
"detail_mode": "spreadsheet",
|
|
|
|
|
|
"rule_library": FINANCE_RULES_LIBRARY,
|
|
|
|
|
|
"scenario_category": COMPANY_TRAVEL_RULE_SCENARIO_JSON[0],
|
|
|
|
|
|
"ai_review_category": COMPANY_TRAVEL_RULE_SCENARIO_JSON[0],
|
|
|
|
|
|
"rule_template_label": "差旅报销 Excel 模板",
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
2026-05-19 20:23:58 +08:00
|
|
|
|
platform_risk_assets = self._build_platform_risk_seed_assets()
|
|
|
|
|
|
company_communication_rule = AgentAsset(
|
|
|
|
|
|
asset_type=AgentAssetType.RULE.value,
|
2026-05-20 09:36:01 +08:00
|
|
|
|
code=COMPANY_COMMUNICATION_EXPENSE_RULE_CODE,
|
|
|
|
|
|
name="公司通信费报销规则",
|
|
|
|
|
|
description="通过 Excel 明细表维护员工通信费报销标准、专项补充口径和审批要求。",
|
|
|
|
|
|
domain=AgentAssetDomain.EXPENSE.value,
|
|
|
|
|
|
scenario_json=list(COMPANY_COMMUNICATION_RULE_SCENARIO_JSON),
|
|
|
|
|
|
owner="财务制度管理组",
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
current_version=COMPANY_COMMUNICATION_RULE_VERSION,
|
|
|
|
|
|
published_version=COMPANY_COMMUNICATION_RULE_VERSION,
|
|
|
|
|
|
working_version=COMPANY_COMMUNICATION_RULE_VERSION,
|
|
|
|
|
|
config_json={
|
|
|
|
|
|
"severity": "medium",
|
|
|
|
|
|
"enabled": True,
|
2026-05-20 09:36:01 +08:00
|
|
|
|
"tag": "财务规则",
|
|
|
|
|
|
"detail_mode": "spreadsheet",
|
|
|
|
|
|
"rule_library": FINANCE_RULES_LIBRARY,
|
|
|
|
|
|
"scenario_category": COMPANY_COMMUNICATION_RULE_SCENARIO_JSON[0],
|
|
|
|
|
|
"ai_review_category": COMPANY_COMMUNICATION_RULE_SCENARIO_JSON[0],
|
|
|
|
|
|
"rule_template_label": "通信费报销 Excel 模板",
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
2026-05-19 20:23:58 +08:00
|
|
|
|
skill_expense_asset = AgentAsset(
|
|
|
|
|
|
asset_type=AgentAssetType.SKILL.value,
|
|
|
|
|
|
code="skill.expense.summary_lookup",
|
2026-05-17 08:38:41 +00:00
|
|
|
|
name="报销汇总查询技能",
|
|
|
|
|
|
description="根据时间、员工和部门汇总报销金额与单据数量。",
|
|
|
|
|
|
domain=AgentAssetDomain.EXPENSE.value,
|
|
|
|
|
|
scenario_json=["expense", "query", "summary"],
|
|
|
|
|
|
owner="平台研发组",
|
|
|
|
|
|
reviewer="陈硕",
|
2026-05-19 20:23:58 +08:00
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
|
|
|
|
|
current_version="v1.0.0",
|
|
|
|
|
|
published_version="v1.0.0",
|
|
|
|
|
|
working_version="v1.0.0",
|
2026-05-17 08:38:41 +00:00
|
|
|
|
config_json={"input_schema": ["time_range", "employee", "department"]},
|
|
|
|
|
|
)
|
|
|
|
|
|
skill_ar_asset = AgentAsset(
|
|
|
|
|
|
asset_type=AgentAssetType.SKILL.value,
|
|
|
|
|
|
code="skill.ar.aging_summary",
|
|
|
|
|
|
name="应收账龄汇总技能",
|
|
|
|
|
|
description="按客户、账龄和逾期状态汇总应收风险分布。",
|
|
|
|
|
|
domain=AgentAssetDomain.AR.value,
|
|
|
|
|
|
scenario_json=["accounts_receivable", "query", "aging_summary"],
|
|
|
|
|
|
owner="平台研发组",
|
|
|
|
|
|
reviewer="陈硕",
|
2026-05-19 20:23:58 +08:00
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
|
|
|
|
|
current_version="v1.0.0",
|
|
|
|
|
|
published_version="v1.0.0",
|
|
|
|
|
|
working_version="v1.0.0",
|
2026-05-17 08:38:41 +00:00
|
|
|
|
config_json={"input_schema": ["customer", "aging_bucket", "status"]},
|
|
|
|
|
|
)
|
|
|
|
|
|
invoice_mcp_asset = AgentAsset(
|
|
|
|
|
|
asset_type=AgentAssetType.MCP.value,
|
|
|
|
|
|
code="mcp.invoice.verify_mock",
|
|
|
|
|
|
name="发票验真 Mock 服务",
|
|
|
|
|
|
description="模拟发票验真、发票状态查询和异常降级说明。",
|
|
|
|
|
|
domain=AgentAssetDomain.SYSTEM.value,
|
|
|
|
|
|
scenario_json=["expense", "invoice_validation"],
|
|
|
|
|
|
owner="平台研发组",
|
|
|
|
|
|
reviewer="周悦宁",
|
2026-05-19 20:23:58 +08:00
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
|
|
|
|
|
current_version="v1.0.0",
|
|
|
|
|
|
published_version="v1.0.0",
|
|
|
|
|
|
working_version="v1.0.0",
|
2026-05-17 08:38:41 +00:00
|
|
|
|
config_json={"endpoint": "mock://invoice/verify", "timeout_ms": 1200},
|
|
|
|
|
|
)
|
|
|
|
|
|
ledger_mcp_asset = AgentAsset(
|
|
|
|
|
|
asset_type=AgentAssetType.MCP.value,
|
|
|
|
|
|
code="mcp.ledger.snapshot_mock",
|
|
|
|
|
|
name="总账快照 Mock 服务",
|
|
|
|
|
|
description="模拟返回应收、应付和费用汇总快照,供 Agent 查询和巡检。",
|
|
|
|
|
|
domain=AgentAssetDomain.SYSTEM.value,
|
|
|
|
|
|
scenario_json=["expense", "accounts_receivable", "accounts_payable"],
|
|
|
|
|
|
owner="平台研发组",
|
|
|
|
|
|
reviewer="周悦宁",
|
2026-05-19 20:23:58 +08:00
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
|
|
|
|
|
current_version="v1.0.0",
|
|
|
|
|
|
published_version="v1.0.0",
|
|
|
|
|
|
working_version="v1.0.0",
|
2026-05-17 08:38:41 +00:00
|
|
|
|
config_json={"endpoint": "mock://ledger/snapshot", "timeout_ms": 1500},
|
|
|
|
|
|
)
|
|
|
|
|
|
task_asset = AgentAsset(
|
|
|
|
|
|
asset_type=AgentAssetType.TASK.value,
|
|
|
|
|
|
code="task.hermes.daily_risk_scan",
|
|
|
|
|
|
name="Hermes 每日风险巡检",
|
|
|
|
|
|
description="每天早上巡检重复报销、金额超标、逾期应收和异常付款。",
|
|
|
|
|
|
domain=AgentAssetDomain.SYSTEM.value,
|
|
|
|
|
|
scenario_json=["schedule", "risk_check"],
|
|
|
|
|
|
owner="风控与审计部",
|
|
|
|
|
|
reviewer="顾承宇",
|
2026-05-19 20:23:58 +08:00
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
|
|
|
|
|
current_version="v1.0.0",
|
|
|
|
|
|
published_version="v1.0.0",
|
|
|
|
|
|
working_version="v1.0.0",
|
2026-05-17 08:38:41 +00:00
|
|
|
|
config_json={"cron": "0 9 * * *", "agent": AgentName.HERMES.value},
|
|
|
|
|
|
)
|
|
|
|
|
|
ar_summary_task = AgentAsset(
|
|
|
|
|
|
asset_type=AgentAssetType.TASK.value,
|
|
|
|
|
|
code="task.hermes.weekly_ar_summary",
|
|
|
|
|
|
name="Hermes 每周应收账龄汇总",
|
|
|
|
|
|
description="每周汇总逾期应收、账龄分布和客户风险变化。",
|
|
|
|
|
|
domain=AgentAssetDomain.SYSTEM.value,
|
|
|
|
|
|
scenario_json=["schedule", "accounts_receivable", "summary"],
|
|
|
|
|
|
owner="风控与审计部",
|
|
|
|
|
|
reviewer="顾承宇",
|
2026-05-19 20:23:58 +08:00
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
|
|
|
|
|
current_version="v1.0.0",
|
|
|
|
|
|
published_version="v1.0.0",
|
|
|
|
|
|
working_version="v1.0.0",
|
2026-05-17 08:38:41 +00:00
|
|
|
|
config_json={"cron": "0 10 * * 1", "agent": AgentName.HERMES.value},
|
|
|
|
|
|
)
|
|
|
|
|
|
rule_digest_task = AgentAsset(
|
|
|
|
|
|
asset_type=AgentAssetType.TASK.value,
|
|
|
|
|
|
code="task.hermes.rule_review_digest",
|
|
|
|
|
|
name="Hermes 规则待审摘要",
|
|
|
|
|
|
description="每天汇总待审规则、待补样例和被拒规则修订建议。",
|
|
|
|
|
|
domain=AgentAssetDomain.SYSTEM.value,
|
|
|
|
|
|
scenario_json=["schedule", "rule_center", "review_digest"],
|
|
|
|
|
|
owner="风控与审计部",
|
|
|
|
|
|
reviewer="顾承宇",
|
2026-05-19 20:23:58 +08:00
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
|
|
|
|
|
current_version="v1.0.0",
|
|
|
|
|
|
published_version="v1.0.0",
|
|
|
|
|
|
working_version="v1.0.0",
|
2026-05-17 08:38:41 +00:00
|
|
|
|
config_json={"cron": "0 18 * * *", "agent": AgentName.HERMES.value},
|
|
|
|
|
|
)
|
|
|
|
|
|
knowledge_index_task = AgentAsset(
|
|
|
|
|
|
asset_type=AgentAssetType.TASK.value,
|
|
|
|
|
|
code="task.hermes.knowledge_index_sync",
|
|
|
|
|
|
name="Hermes ??????",
|
|
|
|
|
|
description="?????????? LightRAG ???????",
|
|
|
|
|
|
domain=AgentAssetDomain.SYSTEM.value,
|
|
|
|
|
|
scenario_json=["schedule", "knowledge", "rule_center"],
|
|
|
|
|
|
owner="财务制度管理组",
|
|
|
|
|
|
reviewer="顾承宇",
|
2026-05-19 20:23:58 +08:00
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
|
|
|
|
|
current_version="v1.0.0",
|
|
|
|
|
|
published_version="v1.0.0",
|
|
|
|
|
|
working_version="v1.0.0",
|
|
|
|
|
|
config_json={"cron": "0 0 * * *", "agent": AgentName.HERMES.value},
|
2026-05-17 08:38:41 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self.db.add_all(
|
|
|
|
|
|
[
|
2026-05-19 20:23:58 +08:00
|
|
|
|
attachment_rule,
|
|
|
|
|
|
scene_submission_rule,
|
|
|
|
|
|
travel_policy_rule,
|
|
|
|
|
|
*platform_risk_assets,
|
|
|
|
|
|
company_travel_rule,
|
|
|
|
|
|
company_communication_rule,
|
|
|
|
|
|
skill_expense_asset,
|
|
|
|
|
|
skill_ar_asset,
|
|
|
|
|
|
invoice_mcp_asset,
|
2026-05-17 08:38:41 +00:00
|
|
|
|
ledger_mcp_asset,
|
|
|
|
|
|
task_asset,
|
|
|
|
|
|
ar_summary_task,
|
|
|
|
|
|
rule_digest_task,
|
|
|
|
|
|
knowledge_index_task,
|
|
|
|
|
|
]
|
2026-05-19 20:23:58 +08:00
|
|
|
|
)
|
|
|
|
|
|
self.db.flush()
|
|
|
|
|
|
|
|
|
|
|
|
company_travel_rule_meta = self._ensure_company_travel_rule_spreadsheet_seed(
|
|
|
|
|
|
company_travel_rule,
|
|
|
|
|
|
version=COMPANY_TRAVEL_RULE_VERSION,
|
|
|
|
|
|
actor_name="系统初始化",
|
|
|
|
|
|
)
|
|
|
|
|
|
company_communication_rule_meta = self._ensure_company_communication_rule_spreadsheet_seed(
|
|
|
|
|
|
company_communication_rule,
|
|
|
|
|
|
version=COMPANY_COMMUNICATION_RULE_VERSION,
|
|
|
|
|
|
actor_name="系统初始化",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self.db.add_all(
|
|
|
|
|
|
[
|
2026-05-17 08:38:41 +00:00
|
|
|
|
AgentAssetVersion(
|
|
|
|
|
|
asset=attachment_rule,
|
|
|
|
|
|
version="v0.9.0",
|
|
|
|
|
|
content=self._attachment_submission_requirement_markdown(
|
|
|
|
|
|
version_note="首版附件完整性规则草稿,覆盖基础票据与补件口径。",
|
|
|
|
|
|
include_review_note=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
content_type=AgentAssetContentType.MARKDOWN.value,
|
|
|
|
|
|
change_note="首版草稿。",
|
|
|
|
|
|
created_by="高嘉禾",
|
|
|
|
|
|
),
|
|
|
|
|
|
AgentAssetVersion(
|
|
|
|
|
|
asset=attachment_rule,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
content=self._attachment_submission_requirement_markdown(
|
|
|
|
|
|
version_note="补充票据缺失、收据替代和差旅等效凭证口径,待审核。",
|
|
|
|
|
|
include_review_note=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
content_type=AgentAssetContentType.MARKDOWN.value,
|
|
|
|
|
|
change_note="补充票据替代与差旅等效凭证口径,待审核。",
|
|
|
|
|
|
created_by="高嘉禾",
|
|
|
|
|
|
),
|
|
|
|
|
|
AgentAssetVersion(
|
|
|
|
|
|
asset=scene_submission_rule,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
content=self._scene_submission_standard_markdown(),
|
|
|
|
|
|
content_type=AgentAssetContentType.MARKDOWN.value,
|
|
|
|
|
|
change_note="首版报销场景提交标准,覆盖附件类型、必填字段和金额阈值。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
),
|
|
|
|
|
|
AgentAssetVersion(
|
|
|
|
|
|
asset=travel_policy_rule,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
content=self._travel_risk_control_standard_markdown(version="v1.0.0"),
|
|
|
|
|
|
content_type=AgentAssetContentType.MARKDOWN.value,
|
|
|
|
|
|
change_note="首版差旅制度执行规则,覆盖行程闭环与基础差标校验。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
),
|
2026-05-19 20:23:58 +08:00
|
|
|
|
AgentAssetVersion(
|
|
|
|
|
|
asset=travel_policy_rule,
|
|
|
|
|
|
version="v1.1.0",
|
|
|
|
|
|
content=self._travel_risk_control_standard_markdown(version="v1.1.0"),
|
|
|
|
|
|
content_type=AgentAssetContentType.MARKDOWN.value,
|
|
|
|
|
|
change_note="补充可执行规则块,供审核引擎直接消费差旅制度标准。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
),
|
|
|
|
|
|
*[
|
|
|
|
|
|
AgentAssetVersion(
|
|
|
|
|
|
asset=asset,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
content=self._platform_risk_rule_markdown(asset),
|
|
|
|
|
|
content_type=AgentAssetContentType.MARKDOWN.value,
|
|
|
|
|
|
change_note=f"平台通用风险规则:{asset.name}",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
)
|
|
|
|
|
|
for asset in platform_risk_assets
|
|
|
|
|
|
],
|
|
|
|
|
|
AgentAssetVersion(
|
|
|
|
|
|
asset=company_travel_rule,
|
|
|
|
|
|
version=COMPANY_TRAVEL_RULE_VERSION,
|
|
|
|
|
|
content=AgentAssetSpreadsheetManager.build_version_markdown(
|
|
|
|
|
|
rule_name=company_travel_rule.name,
|
|
|
|
|
|
version=COMPANY_TRAVEL_RULE_VERSION,
|
|
|
|
|
|
metadata=company_travel_rule_meta,
|
|
|
|
|
|
),
|
|
|
|
|
|
content_type=AgentAssetContentType.MARKDOWN.value,
|
|
|
|
|
|
change_note="初始化差旅费报销 Excel 规则表。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
),
|
|
|
|
|
|
AgentAssetVersion(
|
|
|
|
|
|
asset=company_communication_rule,
|
|
|
|
|
|
version=COMPANY_COMMUNICATION_RULE_VERSION,
|
|
|
|
|
|
content=AgentAssetSpreadsheetManager.build_version_markdown(
|
|
|
|
|
|
rule_name=company_communication_rule.name,
|
|
|
|
|
|
version=COMPANY_COMMUNICATION_RULE_VERSION,
|
|
|
|
|
|
metadata=company_communication_rule_meta,
|
|
|
|
|
|
),
|
|
|
|
|
|
content_type=AgentAssetContentType.MARKDOWN.value,
|
|
|
|
|
|
change_note="初始化通信费报销 Excel 规则表。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
),
|
|
|
|
|
|
AgentAssetVersion(
|
|
|
|
|
|
asset=skill_expense_asset,
|
|
|
|
|
|
version="v1.0.0",
|
2026-05-17 08:38:41 +00:00
|
|
|
|
content=self._json_content(
|
|
|
|
|
|
{
|
|
|
|
|
|
"inputs": ["time_range", "employee", "department"],
|
|
|
|
|
|
"outputs": ["total_amount", "claim_count"],
|
|
|
|
|
|
"dependencies": ["database.expense_claims"],
|
|
|
|
|
|
}
|
|
|
|
|
|
),
|
|
|
|
|
|
content_type=AgentAssetContentType.JSON.value,
|
|
|
|
|
|
change_note="初始化技能快照。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
),
|
|
|
|
|
|
AgentAssetVersion(
|
|
|
|
|
|
asset=skill_ar_asset,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
content=self._json_content(
|
|
|
|
|
|
{
|
|
|
|
|
|
"inputs": ["customer", "aging_bucket", "status"],
|
|
|
|
|
|
"outputs": ["receivable_total", "overdue_total", "customer_count"],
|
|
|
|
|
|
"dependencies": ["database.accounts_receivable"],
|
|
|
|
|
|
}
|
|
|
|
|
|
),
|
|
|
|
|
|
content_type=AgentAssetContentType.JSON.value,
|
|
|
|
|
|
change_note="初始化应收账龄技能快照。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
),
|
|
|
|
|
|
AgentAssetVersion(
|
|
|
|
|
|
asset=invoice_mcp_asset,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
content=self._json_content(
|
|
|
|
|
|
{
|
|
|
|
|
|
"service_type": "mock",
|
|
|
|
|
|
"auth_mode": "none",
|
|
|
|
|
|
"degrade_strategy": "return_stub_with_warning",
|
|
|
|
|
|
}
|
|
|
|
|
|
),
|
|
|
|
|
|
content_type=AgentAssetContentType.JSON.value,
|
|
|
|
|
|
change_note="初始化 MCP 快照。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
),
|
|
|
|
|
|
AgentAssetVersion(
|
|
|
|
|
|
asset=ledger_mcp_asset,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
content=self._json_content(
|
|
|
|
|
|
{
|
|
|
|
|
|
"service_type": "mock",
|
|
|
|
|
|
"auth_mode": "service_account",
|
|
|
|
|
|
"degrade_strategy": "return_cached_snapshot_with_warning",
|
|
|
|
|
|
}
|
|
|
|
|
|
),
|
|
|
|
|
|
content_type=AgentAssetContentType.JSON.value,
|
|
|
|
|
|
change_note="初始化总账快照 MCP。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
),
|
|
|
|
|
|
AgentAssetVersion(
|
|
|
|
|
|
asset=task_asset,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
content=self._json_content(
|
|
|
|
|
|
{
|
|
|
|
|
|
"task_type": "daily_risk_scan",
|
|
|
|
|
|
"schedule": "0 9 * * *",
|
|
|
|
|
|
"target_agent": AgentName.HERMES.value,
|
|
|
|
|
|
}
|
|
|
|
|
|
),
|
|
|
|
|
|
content_type=AgentAssetContentType.JSON.value,
|
|
|
|
|
|
change_note="初始化任务快照。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
),
|
|
|
|
|
|
AgentAssetVersion(
|
|
|
|
|
|
asset=ar_summary_task,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
content=self._json_content(
|
|
|
|
|
|
{
|
|
|
|
|
|
"task_type": "weekly_ar_summary",
|
|
|
|
|
|
"schedule": "0 10 * * 1",
|
|
|
|
|
|
"target_agent": AgentName.HERMES.value,
|
|
|
|
|
|
}
|
|
|
|
|
|
),
|
|
|
|
|
|
content_type=AgentAssetContentType.JSON.value,
|
|
|
|
|
|
change_note="初始化应收账龄汇总任务。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
),
|
|
|
|
|
|
AgentAssetVersion(
|
|
|
|
|
|
asset=rule_digest_task,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
content=self._json_content(
|
|
|
|
|
|
{
|
|
|
|
|
|
"task_type": "rule_review_digest",
|
|
|
|
|
|
"schedule": "0 18 * * *",
|
|
|
|
|
|
"target_agent": AgentName.HERMES.value,
|
|
|
|
|
|
}
|
|
|
|
|
|
),
|
|
|
|
|
|
content_type=AgentAssetContentType.JSON.value,
|
|
|
|
|
|
change_note="初始化规则待审摘要任务。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
),
|
|
|
|
|
|
AgentAssetVersion(
|
|
|
|
|
|
asset=knowledge_index_task,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
content=self._json_content(
|
|
|
|
|
|
{
|
|
|
|
|
|
"task_type": "knowledge_index_sync",
|
2026-05-19 20:23:58 +08:00
|
|
|
|
"schedule": "0 0 * * *",
|
2026-05-17 08:38:41 +00:00
|
|
|
|
"target_agent": AgentName.HERMES.value,
|
|
|
|
|
|
"folder": "报销制度",
|
|
|
|
|
|
"changed_only": True,
|
|
|
|
|
|
"index_engine": "lightrag",
|
|
|
|
|
|
}
|
|
|
|
|
|
),
|
|
|
|
|
|
content_type=AgentAssetContentType.JSON.value,
|
|
|
|
|
|
change_note="初始化制度知识与规则草稿形成任务。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
),
|
|
|
|
|
|
]
|
|
|
|
|
|
)
|
|
|
|
|
|
self.db.add_all(
|
|
|
|
|
|
[
|
|
|
|
|
|
AgentAssetReview(
|
|
|
|
|
|
asset=attachment_rule,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
reviewer="高嘉禾",
|
|
|
|
|
|
review_status=AgentReviewStatus.PENDING.value,
|
|
|
|
|
|
review_note="等待制度管理员确认收据替代与补件时限口径。",
|
|
|
|
|
|
reviewed_at=None,
|
|
|
|
|
|
),
|
|
|
|
|
|
AgentAssetReview(
|
|
|
|
|
|
asset=scene_submission_rule,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
review_status=AgentReviewStatus.APPROVED.value,
|
|
|
|
|
|
review_note="可作为报销场景统一审核标准正式执行。",
|
|
|
|
|
|
reviewed_at=datetime.now(UTC),
|
|
|
|
|
|
),
|
2026-05-19 20:23:58 +08:00
|
|
|
|
AgentAssetReview(
|
|
|
|
|
|
asset=travel_policy_rule,
|
|
|
|
|
|
version="v1.1.0",
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
review_status=AgentReviewStatus.APPROVED.value,
|
|
|
|
|
|
review_note="制度口径已确认,并已补充可执行配置供审核引擎读取。",
|
|
|
|
|
|
reviewed_at=datetime.now(UTC),
|
|
|
|
|
|
),
|
|
|
|
|
|
AgentAssetReview(
|
|
|
|
|
|
asset=company_travel_rule,
|
|
|
|
|
|
version=COMPANY_TRAVEL_RULE_VERSION,
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
review_status=AgentReviewStatus.APPROVED.value,
|
|
|
|
|
|
review_note="首版 Excel 规则表已确认,可作为财务规则使用。",
|
|
|
|
|
|
reviewed_at=datetime.now(UTC),
|
|
|
|
|
|
),
|
|
|
|
|
|
AgentAssetReview(
|
|
|
|
|
|
asset=company_communication_rule,
|
|
|
|
|
|
version=COMPANY_COMMUNICATION_RULE_VERSION,
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
review_status=AgentReviewStatus.APPROVED.value,
|
|
|
|
|
|
review_note="首版 Excel 规则表已确认,可作为财务规则使用。",
|
|
|
|
|
|
reviewed_at=datetime.now(UTC),
|
|
|
|
|
|
),
|
|
|
|
|
|
]
|
|
|
|
|
|
)
|
2026-05-17 08:38:41 +00:00
|
|
|
|
|
|
|
|
|
|
def _seed_financial_records(self) -> None:
|
|
|
|
|
|
if self.db.scalar(select(ExpenseClaim.id).limit(1)) is not None:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
claim_1 = ExpenseClaim(
|
|
|
|
|
|
claim_no="EXP-202605-001",
|
|
|
|
|
|
employee_name="张三",
|
|
|
|
|
|
department_name="财务共享中心",
|
|
|
|
|
|
project_code="PRJ-EXP-01",
|
|
|
|
|
|
expense_type="travel",
|
|
|
|
|
|
reason="华南客户拜访差旅报销",
|
|
|
|
|
|
location="深圳",
|
|
|
|
|
|
amount=Decimal("3280.00"),
|
|
|
|
|
|
currency="CNY",
|
|
|
|
|
|
invoice_count=3,
|
|
|
|
|
|
occurred_at=datetime(2026, 5, 6, 9, 0, tzinfo=UTC),
|
|
|
|
|
|
submitted_at=datetime(2026, 5, 7, 10, 20, tzinfo=UTC),
|
|
|
|
|
|
status="submitted",
|
|
|
|
|
|
approval_stage="finance_review",
|
|
|
|
|
|
risk_flags_json=["amount_over_limit"],
|
|
|
|
|
|
)
|
|
|
|
|
|
claim_1.items = [
|
|
|
|
|
|
ExpenseClaimItem(
|
|
|
|
|
|
item_date=date(2026, 5, 5),
|
|
|
|
|
|
item_type="hotel",
|
|
|
|
|
|
item_reason="客户拜访住宿",
|
|
|
|
|
|
item_location="深圳",
|
|
|
|
|
|
item_amount=Decimal("1880.00"),
|
|
|
|
|
|
invoice_id="INV-HOTEL-001",
|
|
|
|
|
|
),
|
|
|
|
|
|
ExpenseClaimItem(
|
|
|
|
|
|
item_date=date(2026, 5, 6),
|
|
|
|
|
|
item_type="transport",
|
|
|
|
|
|
item_reason="往返交通",
|
|
|
|
|
|
item_location="深圳",
|
|
|
|
|
|
item_amount=Decimal("1400.00"),
|
|
|
|
|
|
invoice_id="INV-TRANS-009",
|
|
|
|
|
|
),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
claim_2 = ExpenseClaim(
|
|
|
|
|
|
claim_no="EXP-202605-002",
|
|
|
|
|
|
employee_name="李四",
|
|
|
|
|
|
department_name="华东销售部",
|
|
|
|
|
|
project_code="PRJ-SALES-02",
|
|
|
|
|
|
expense_type="meal",
|
|
|
|
|
|
reason="客户路演餐费",
|
|
|
|
|
|
location="上海",
|
|
|
|
|
|
amount=Decimal("860.00"),
|
|
|
|
|
|
currency="CNY",
|
|
|
|
|
|
invoice_count=1,
|
|
|
|
|
|
occurred_at=datetime(2026, 5, 8, 12, 0, tzinfo=UTC),
|
|
|
|
|
|
submitted_at=datetime(2026, 5, 8, 18, 30, tzinfo=UTC),
|
|
|
|
|
|
status="approved",
|
|
|
|
|
|
approval_stage="completed",
|
|
|
|
|
|
risk_flags_json=[],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
claim_3 = ExpenseClaim(
|
|
|
|
|
|
claim_no="EXP-202605-003",
|
|
|
|
|
|
employee_name="王五",
|
|
|
|
|
|
department_name="市场品牌部",
|
|
|
|
|
|
project_code="PRJ-MKT-08",
|
|
|
|
|
|
expense_type="travel",
|
|
|
|
|
|
reason="市场活动会务差旅",
|
|
|
|
|
|
location="北京",
|
|
|
|
|
|
amount=Decimal("3280.00"),
|
|
|
|
|
|
currency="CNY",
|
|
|
|
|
|
invoice_count=2,
|
|
|
|
|
|
occurred_at=datetime(2026, 5, 6, 11, 30, tzinfo=UTC),
|
|
|
|
|
|
submitted_at=datetime(2026, 5, 8, 9, 10, tzinfo=UTC),
|
|
|
|
|
|
status="review",
|
|
|
|
|
|
approval_stage="risk_check",
|
|
|
|
|
|
risk_flags_json=["duplicate_expense"],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
ar_records = [
|
|
|
|
|
|
AccountsReceivableRecord(
|
|
|
|
|
|
receivable_no="AR-202605-001",
|
|
|
|
|
|
customer_id="CUS-A",
|
|
|
|
|
|
customer_name="客户A",
|
|
|
|
|
|
contract_no="CTR-AR-1001",
|
|
|
|
|
|
invoice_no="INV-AR-9001",
|
|
|
|
|
|
amount_receivable=Decimal("120000.00"),
|
|
|
|
|
|
amount_received=Decimal("70000.00"),
|
|
|
|
|
|
amount_outstanding=Decimal("50000.00"),
|
|
|
|
|
|
currency="CNY",
|
|
|
|
|
|
posting_date=date(2026, 4, 1),
|
|
|
|
|
|
due_date=date(2026, 4, 30),
|
|
|
|
|
|
aging_days=11,
|
|
|
|
|
|
status="partial",
|
|
|
|
|
|
risk_flags_json=[],
|
|
|
|
|
|
),
|
|
|
|
|
|
AccountsReceivableRecord(
|
|
|
|
|
|
receivable_no="AR-202605-002",
|
|
|
|
|
|
customer_id="CUS-B",
|
|
|
|
|
|
customer_name="客户B",
|
|
|
|
|
|
contract_no="CTR-AR-1002",
|
|
|
|
|
|
invoice_no="INV-AR-9002",
|
|
|
|
|
|
amount_receivable=Decimal("88000.00"),
|
|
|
|
|
|
amount_received=Decimal("10000.00"),
|
|
|
|
|
|
amount_outstanding=Decimal("78000.00"),
|
|
|
|
|
|
currency="CNY",
|
|
|
|
|
|
posting_date=date(2026, 3, 15),
|
|
|
|
|
|
due_date=date(2026, 4, 15),
|
|
|
|
|
|
aging_days=26,
|
|
|
|
|
|
status="overdue",
|
|
|
|
|
|
risk_flags_json=["ar_overdue"],
|
|
|
|
|
|
),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
ap_records = [
|
|
|
|
|
|
AccountsPayableRecord(
|
|
|
|
|
|
payable_no="AP-202605-001",
|
|
|
|
|
|
vendor_id="VEN-A",
|
|
|
|
|
|
vendor_name="供应商A",
|
|
|
|
|
|
invoice_no="INV-AP-5001",
|
|
|
|
|
|
amount_payable=Decimal("43000.00"),
|
|
|
|
|
|
amount_paid=Decimal("10000.00"),
|
|
|
|
|
|
amount_outstanding=Decimal("33000.00"),
|
|
|
|
|
|
currency="CNY",
|
|
|
|
|
|
posting_date=date(2026, 4, 20),
|
|
|
|
|
|
due_date=date(2026, 5, 12),
|
|
|
|
|
|
aging_days=0,
|
|
|
|
|
|
status="scheduled",
|
|
|
|
|
|
risk_flags_json=[],
|
|
|
|
|
|
),
|
|
|
|
|
|
AccountsPayableRecord(
|
|
|
|
|
|
payable_no="AP-202605-002",
|
|
|
|
|
|
vendor_id="VEN-B",
|
|
|
|
|
|
vendor_name="供应商B",
|
|
|
|
|
|
invoice_no="INV-AP-5002",
|
|
|
|
|
|
amount_payable=Decimal("96000.00"),
|
|
|
|
|
|
amount_paid=Decimal("0.00"),
|
|
|
|
|
|
amount_outstanding=Decimal("96000.00"),
|
|
|
|
|
|
currency="CNY",
|
|
|
|
|
|
posting_date=date(2026, 4, 10),
|
|
|
|
|
|
due_date=date(2026, 5, 5),
|
|
|
|
|
|
aging_days=6,
|
|
|
|
|
|
status="overdue",
|
|
|
|
|
|
risk_flags_json=["ap_overdue"],
|
|
|
|
|
|
),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
self.db.add_all([claim_1, claim_2, claim_3, *ar_records, *ap_records])
|
|
|
|
|
|
|
|
|
|
|
|
def _purge_demo_financial_records(self) -> None:
|
|
|
|
|
|
demo_claims = list(self.db.scalars(select(ExpenseClaim)).all())
|
|
|
|
|
|
for claim in demo_claims:
|
|
|
|
|
|
signature = (
|
|
|
|
|
|
str(claim.claim_no or "").strip(),
|
|
|
|
|
|
str(claim.employee_name or "").strip(),
|
|
|
|
|
|
str(claim.reason or "").strip(),
|
|
|
|
|
|
f"{Decimal(claim.amount or 0):.2f}",
|
|
|
|
|
|
str(claim.status or "").strip(),
|
|
|
|
|
|
)
|
|
|
|
|
|
if signature in DEMO_EXPENSE_CLAIM_SIGNATURES:
|
|
|
|
|
|
self.db.delete(claim)
|
|
|
|
|
|
|
|
|
|
|
|
demo_receivables = list(self.db.scalars(select(AccountsReceivableRecord)).all())
|
|
|
|
|
|
for record in demo_receivables:
|
|
|
|
|
|
signature = (
|
|
|
|
|
|
str(record.receivable_no or "").strip(),
|
|
|
|
|
|
str(record.customer_name or "").strip(),
|
|
|
|
|
|
f"{Decimal(record.amount_outstanding or 0):.2f}",
|
|
|
|
|
|
str(record.status or "").strip(),
|
|
|
|
|
|
)
|
|
|
|
|
|
if signature in DEMO_RECEIVABLE_SIGNATURES:
|
|
|
|
|
|
self.db.delete(record)
|
|
|
|
|
|
|
|
|
|
|
|
demo_payables = list(self.db.scalars(select(AccountsPayableRecord)).all())
|
|
|
|
|
|
for record in demo_payables:
|
|
|
|
|
|
signature = (
|
|
|
|
|
|
str(record.payable_no or "").strip(),
|
|
|
|
|
|
str(record.vendor_name or "").strip(),
|
|
|
|
|
|
f"{Decimal(record.amount_outstanding or 0):.2f}",
|
|
|
|
|
|
str(record.status or "").strip(),
|
|
|
|
|
|
)
|
|
|
|
|
|
if signature in DEMO_PAYABLE_SIGNATURES:
|
|
|
|
|
|
self.db.delete(record)
|
|
|
|
|
|
|
|
|
|
|
|
def _seed_runs_and_logs(self) -> None:
|
|
|
|
|
|
if self.db.scalar(select(AgentRun.id).limit(1)) is not None:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
task_asset = self.db.scalar(
|
|
|
|
|
|
select(AgentAsset).where(AgentAsset.code == "task.hermes.daily_risk_scan")
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
user_run = AgentRun(
|
|
|
|
|
|
run_id="run_user_20260511_001",
|
|
|
|
|
|
agent=AgentName.USER_AGENT.value,
|
|
|
|
|
|
source=AgentRunSource.USER_MESSAGE.value,
|
|
|
|
|
|
user_id="emp_001",
|
|
|
|
|
|
task_id=None,
|
|
|
|
|
|
ontology_json={"scenario": "expense", "intent": "query"},
|
|
|
|
|
|
route_json={"selected_agent": AgentName.USER_AGENT.value, "route_reason": "user query"},
|
|
|
|
|
|
permission_level=AgentPermissionLevel.READ.value,
|
|
|
|
|
|
status=AgentRunStatus.SUCCEEDED.value,
|
|
|
|
|
|
result_summary="已返回本周报销金额和风险摘要。",
|
|
|
|
|
|
started_at=datetime(2026, 5, 11, 8, 35, tzinfo=UTC),
|
|
|
|
|
|
finished_at=datetime(2026, 5, 11, 8, 35, 2, tzinfo=UTC),
|
|
|
|
|
|
)
|
|
|
|
|
|
hermes_run = AgentRun(
|
|
|
|
|
|
run_id="run_hermes_20260511_001",
|
|
|
|
|
|
agent=AgentName.HERMES.value,
|
|
|
|
|
|
source=AgentRunSource.SCHEDULE.value,
|
|
|
|
|
|
user_id=None,
|
|
|
|
|
|
task_id=task_asset.id if task_asset else None,
|
|
|
|
|
|
ontology_json={"scenario": "expense", "intent": "risk_check"},
|
|
|
|
|
|
route_json={
|
|
|
|
|
|
"selected_agent": AgentName.HERMES.value,
|
|
|
|
|
|
"route_reason": "scheduled risk scan",
|
|
|
|
|
|
},
|
|
|
|
|
|
permission_level=AgentPermissionLevel.READ.value,
|
|
|
|
|
|
status=AgentRunStatus.SUCCEEDED.value,
|
|
|
|
|
|
result_summary="Hermes 已生成今日风险巡检摘要。",
|
|
|
|
|
|
started_at=datetime(2026, 5, 11, 9, 0, tzinfo=UTC),
|
|
|
|
|
|
finished_at=datetime(2026, 5, 11, 9, 0, 4, tzinfo=UTC),
|
|
|
|
|
|
)
|
|
|
|
|
|
blocked_run = AgentRun(
|
|
|
|
|
|
run_id="run_user_20260511_002",
|
|
|
|
|
|
agent=AgentName.ORCHESTRATOR.value,
|
|
|
|
|
|
source=AgentRunSource.USER_MESSAGE.value,
|
|
|
|
|
|
user_id="emp_002",
|
|
|
|
|
|
task_id=None,
|
|
|
|
|
|
ontology_json={"scenario": "accounts_payable", "intent": "operate"},
|
|
|
|
|
|
route_json={
|
|
|
|
|
|
"selected_agent": AgentName.USER_AGENT.value,
|
|
|
|
|
|
"route_reason": "payment request",
|
|
|
|
|
|
},
|
|
|
|
|
|
permission_level=AgentPermissionLevel.APPROVAL_REQUIRED.value,
|
|
|
|
|
|
status=AgentRunStatus.BLOCKED.value,
|
|
|
|
|
|
result_summary="动作需要人工确认。",
|
|
|
|
|
|
error_message="直接付款属于高风险动作,已阻断自动执行。",
|
|
|
|
|
|
started_at=datetime(2026, 5, 11, 10, 5, tzinfo=UTC),
|
|
|
|
|
|
finished_at=datetime(2026, 5, 11, 10, 5, 1, tzinfo=UTC),
|
|
|
|
|
|
)
|
|
|
|
|
|
self.db.add_all([user_run, hermes_run, blocked_run])
|
|
|
|
|
|
self.db.flush()
|
|
|
|
|
|
|
|
|
|
|
|
self.db.add_all(
|
|
|
|
|
|
[
|
|
|
|
|
|
AgentToolCall(
|
|
|
|
|
|
run_id=user_run.run_id,
|
|
|
|
|
|
tool_type=AgentToolType.DATABASE.value,
|
|
|
|
|
|
tool_name="expense_claims.lookup",
|
|
|
|
|
|
request_json={"time_range": "this_week", "employee": "all"},
|
|
|
|
|
|
response_json={"claim_count": 3, "total_amount": "7420.00"},
|
|
|
|
|
|
status="succeeded",
|
|
|
|
|
|
duration_ms=48,
|
|
|
|
|
|
),
|
|
|
|
|
|
AgentToolCall(
|
|
|
|
|
|
run_id=hermes_run.run_id,
|
|
|
|
|
|
tool_type=AgentToolType.MCP.value,
|
|
|
|
|
|
tool_name="invoice.verify_mock",
|
|
|
|
|
|
request_json={"claim_no": "EXP-202605-003"},
|
|
|
|
|
|
response_json={
|
|
|
|
|
|
"warning": "external service degraded",
|
|
|
|
|
|
"fallback": "used mock response",
|
|
|
|
|
|
},
|
|
|
|
|
|
status="failed",
|
|
|
|
|
|
duration_ms=132,
|
|
|
|
|
|
error_message="mock upstream timeout",
|
|
|
|
|
|
),
|
|
|
|
|
|
AgentToolCall(
|
|
|
|
|
|
run_id=blocked_run.run_id,
|
|
|
|
|
|
tool_type=AgentToolType.RULE_ENGINE.value,
|
|
|
|
|
|
tool_name="permission.guard",
|
|
|
|
|
|
request_json={"action": "direct_payment"},
|
|
|
|
|
|
response_json={"requires_confirmation": True},
|
|
|
|
|
|
status="succeeded",
|
|
|
|
|
|
duration_ms=5,
|
|
|
|
|
|
),
|
|
|
|
|
|
SemanticParseLog(
|
|
|
|
|
|
run_id=user_run.run_id,
|
|
|
|
|
|
user_id="emp_001",
|
|
|
|
|
|
raw_query="查一下本周报销超标风险",
|
|
|
|
|
|
scenario="expense",
|
|
|
|
|
|
intent="risk_check",
|
|
|
|
|
|
entities_json=[],
|
|
|
|
|
|
time_range_json={"start_date": "2026-05-11", "end_date": "2026-05-17"},
|
|
|
|
|
|
metrics_json=["amount"],
|
|
|
|
|
|
constraints_json=[],
|
|
|
|
|
|
risk_flags_json=["amount_over_limit"],
|
|
|
|
|
|
permission_json={"level": AgentPermissionLevel.READ.value},
|
|
|
|
|
|
confidence=0.93,
|
|
|
|
|
|
),
|
|
|
|
|
|
SemanticParseLog(
|
|
|
|
|
|
run_id=blocked_run.run_id,
|
|
|
|
|
|
user_id="emp_002",
|
|
|
|
|
|
raw_query="帮我直接付款给供应商B",
|
|
|
|
|
|
scenario="accounts_payable",
|
|
|
|
|
|
intent="operate",
|
|
|
|
|
|
entities_json=[{"type": "vendor", "value": "供应商B"}],
|
|
|
|
|
|
time_range_json={},
|
|
|
|
|
|
metrics_json=["amount"],
|
|
|
|
|
|
constraints_json=[],
|
|
|
|
|
|
risk_flags_json=["ap_overdue"],
|
|
|
|
|
|
permission_json={"level": AgentPermissionLevel.APPROVAL_REQUIRED.value},
|
|
|
|
|
|
confidence=0.96,
|
|
|
|
|
|
),
|
|
|
|
|
|
]
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if self.db.scalar(select(AuditLog.id).limit(1)) is None:
|
|
|
|
|
|
self.db.add_all(
|
|
|
|
|
|
[
|
|
|
|
|
|
AuditLog(
|
|
|
|
|
|
actor="系统初始化",
|
|
|
|
|
|
action="save_rule_markdown",
|
|
|
|
|
|
resource_type="rule",
|
|
|
|
|
|
resource_id=ATTACHMENT_RULE_ASSET_CODE,
|
|
|
|
|
|
before_json=None,
|
|
|
|
|
|
after_json={"version": "v1.0.0"},
|
|
|
|
|
|
request_id="seed-audit-001",
|
|
|
|
|
|
),
|
|
|
|
|
|
AuditLog(
|
|
|
|
|
|
actor="高嘉禾",
|
|
|
|
|
|
action="review_rule",
|
|
|
|
|
|
resource_type="rule",
|
|
|
|
|
|
resource_id=ATTACHMENT_RULE_ASSET_CODE,
|
|
|
|
|
|
before_json={"review_status": "pending"},
|
|
|
|
|
|
after_json={"review_status": "pending"},
|
|
|
|
|
|
request_id="seed-audit-002",
|
|
|
|
|
|
),
|
|
|
|
|
|
AuditLog(
|
|
|
|
|
|
actor="系统初始化",
|
|
|
|
|
|
action="activate_rule",
|
|
|
|
|
|
resource_type="rule",
|
|
|
|
|
|
resource_id="rule.expense.scene_submission_standard",
|
|
|
|
|
|
before_json={"status": "draft"},
|
|
|
|
|
|
after_json={"status": "active"},
|
|
|
|
|
|
request_id="seed-audit-003",
|
|
|
|
|
|
),
|
|
|
|
|
|
AuditLog(
|
|
|
|
|
|
actor="Hermes",
|
|
|
|
|
|
action="update_task_status",
|
|
|
|
|
|
resource_type="task",
|
|
|
|
|
|
resource_id="task.hermes.daily_risk_scan",
|
|
|
|
|
|
before_json={"status": "idle"},
|
|
|
|
|
|
after_json={"status": "succeeded"},
|
|
|
|
|
|
request_id="seed-audit-004",
|
|
|
|
|
|
),
|
|
|
|
|
|
]
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _top_up_agent_assets(self, existing_codes: set[str]) -> None:
|
|
|
|
|
|
self._remove_legacy_rule_assets()
|
|
|
|
|
|
existing_codes = set(self.db.scalars(select(AgentAsset.code)).all())
|
|
|
|
|
|
|
|
|
|
|
|
attachment_rule = self.db.scalar(
|
|
|
|
|
|
select(AgentAsset).where(AgentAsset.code == ATTACHMENT_RULE_ASSET_CODE)
|
|
|
|
|
|
)
|
|
|
|
|
|
scene_submission_rule = self.db.scalar(
|
|
|
|
|
|
select(AgentAsset).where(AgentAsset.code == "rule.expense.scene_submission_standard")
|
|
|
|
|
|
)
|
2026-05-19 20:23:58 +08:00
|
|
|
|
travel_policy_rule = self.db.scalar(
|
|
|
|
|
|
select(AgentAsset).where(AgentAsset.code == "rule.expense.travel_risk_control_standard")
|
|
|
|
|
|
)
|
|
|
|
|
|
company_travel_rule = self.db.scalar(
|
|
|
|
|
|
select(AgentAsset).where(AgentAsset.code == COMPANY_TRAVEL_EXPENSE_RULE_CODE)
|
|
|
|
|
|
)
|
|
|
|
|
|
company_communication_rule = self.db.scalar(
|
|
|
|
|
|
select(AgentAsset).where(AgentAsset.code == COMPANY_COMMUNICATION_EXPENSE_RULE_CODE)
|
|
|
|
|
|
)
|
2026-05-17 08:38:41 +00:00
|
|
|
|
|
|
|
|
|
|
if ATTACHMENT_RULE_ASSET_CODE not in existing_codes:
|
|
|
|
|
|
attachment_rule = self._create_seed_asset(
|
|
|
|
|
|
asset_type=AgentAssetType.RULE.value,
|
|
|
|
|
|
code=ATTACHMENT_RULE_ASSET_CODE,
|
|
|
|
|
|
name="报销附件与单据完整性规则",
|
|
|
|
|
|
description="统一定义报销提交时的附件数量、票据类型和补件处理口径,作为上线前待审核规则。",
|
|
|
|
|
|
domain=AgentAssetDomain.EXPENSE.value,
|
|
|
|
|
|
scenario_json=["expense", "risk_check", "attachment_policy", "invoice_anomaly"],
|
|
|
|
|
|
owner="财务制度管理组",
|
|
|
|
|
|
reviewer="高嘉禾",
|
|
|
|
|
|
status=AgentAssetStatus.REVIEW.value,
|
|
|
|
|
|
current_version="v1.0.0",
|
|
|
|
|
|
config_json={
|
|
|
|
|
|
"severity": "high",
|
|
|
|
|
|
"enabled": False,
|
|
|
|
|
|
"runtime_kind": "policy_rule_draft",
|
|
|
|
|
|
"rule_template_key": "attachment_requirement_v1",
|
|
|
|
|
|
"rule_template_label": "附件要求模板",
|
|
|
|
|
|
"runtime_rule": ATTACHMENT_RULE_RUNTIME_CONFIG,
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-19 20:23:58 +08:00
|
|
|
|
if attachment_rule is not None:
|
|
|
|
|
|
if not str(attachment_rule.current_version or "").strip():
|
|
|
|
|
|
attachment_rule.current_version = "v1.0.0"
|
|
|
|
|
|
if not str(attachment_rule.working_version or "").strip():
|
|
|
|
|
|
attachment_rule.working_version = attachment_rule.current_version
|
|
|
|
|
|
attachment_rule.status = attachment_rule.status or AgentAssetStatus.REVIEW.value
|
2026-05-17 08:38:41 +00:00
|
|
|
|
attachment_rule.description = "统一定义报销提交时的附件数量、票据类型和补件处理口径,作为上线前待审核规则。"
|
|
|
|
|
|
attachment_rule.config_json = {
|
|
|
|
|
|
"severity": "high",
|
|
|
|
|
|
"enabled": False,
|
|
|
|
|
|
"runtime_kind": "policy_rule_draft",
|
|
|
|
|
|
"rule_template_key": "attachment_requirement_v1",
|
|
|
|
|
|
"rule_template_label": "附件要求模板",
|
|
|
|
|
|
"runtime_rule": ATTACHMENT_RULE_RUNTIME_CONFIG,
|
|
|
|
|
|
}
|
|
|
|
|
|
self._ensure_asset_version(
|
|
|
|
|
|
attachment_rule,
|
|
|
|
|
|
version="v0.9.0",
|
|
|
|
|
|
content=self._attachment_submission_requirement_markdown(
|
|
|
|
|
|
version_note="首版附件完整性规则草稿,覆盖基础票据与补件口径。",
|
|
|
|
|
|
include_review_note=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
content_type=AgentAssetContentType.MARKDOWN.value,
|
|
|
|
|
|
change_note="首版草稿。",
|
|
|
|
|
|
created_by="高嘉禾",
|
|
|
|
|
|
)
|
|
|
|
|
|
self._ensure_asset_version(
|
|
|
|
|
|
attachment_rule,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
content=self._attachment_submission_requirement_markdown(
|
|
|
|
|
|
version_note="补充票据缺失、收据替代和差旅等效凭证口径,待审核。",
|
|
|
|
|
|
include_review_note=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
content_type=AgentAssetContentType.MARKDOWN.value,
|
|
|
|
|
|
change_note="补充票据替代与差旅等效凭证口径,待审核。",
|
|
|
|
|
|
created_by="高嘉禾",
|
|
|
|
|
|
)
|
|
|
|
|
|
self._ensure_asset_review(
|
|
|
|
|
|
attachment_rule,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
reviewer="高嘉禾",
|
|
|
|
|
|
review_status=AgentReviewStatus.PENDING.value,
|
|
|
|
|
|
review_note="等待制度管理员确认收据替代与补件时限口径。",
|
|
|
|
|
|
reviewed_at=None,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if "rule.expense.scene_submission_standard" not in existing_codes:
|
|
|
|
|
|
scene_submission_rule = self._create_seed_asset(
|
|
|
|
|
|
asset_type=AgentAssetType.RULE.value,
|
|
|
|
|
|
code="rule.expense.scene_submission_standard",
|
|
|
|
|
|
name="报销场景提交与附件标准",
|
|
|
|
|
|
description="统一定义各报销场景的必填字段、附件类型要求和金额阈值。",
|
|
|
|
|
|
domain=AgentAssetDomain.EXPENSE.value,
|
|
|
|
|
|
scenario_json=["expense", "risk_check", "scene_policy", "attachment_policy"],
|
|
|
|
|
|
owner="费用运营组",
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
|
|
|
|
|
current_version="v1.0.0",
|
|
|
|
|
|
config_json={
|
|
|
|
|
|
"severity": "high",
|
|
|
|
|
|
"enabled": True,
|
|
|
|
|
|
"runtime_kind": "scene_matrix",
|
|
|
|
|
|
"rule_template_label": "系统内置场景矩阵规则",
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-19 20:23:58 +08:00
|
|
|
|
if scene_submission_rule is not None:
|
|
|
|
|
|
if not str(scene_submission_rule.current_version or "").strip():
|
|
|
|
|
|
scene_submission_rule.current_version = "v1.0.0"
|
|
|
|
|
|
if not str(scene_submission_rule.working_version or "").strip():
|
|
|
|
|
|
scene_submission_rule.working_version = scene_submission_rule.current_version
|
|
|
|
|
|
if not str(scene_submission_rule.published_version or "").strip():
|
|
|
|
|
|
scene_submission_rule.published_version = scene_submission_rule.current_version
|
|
|
|
|
|
scene_submission_rule.status = scene_submission_rule.status or AgentAssetStatus.ACTIVE.value
|
2026-05-17 08:38:41 +00:00
|
|
|
|
scene_submission_rule.description = "统一定义各报销场景的必填字段、附件类型要求和金额阈值。"
|
|
|
|
|
|
scene_submission_rule.config_json = {
|
|
|
|
|
|
"severity": "high",
|
|
|
|
|
|
"enabled": True,
|
|
|
|
|
|
"runtime_kind": "scene_matrix",
|
|
|
|
|
|
"rule_template_label": "系统内置场景矩阵规则",
|
|
|
|
|
|
}
|
|
|
|
|
|
self._ensure_asset_version(
|
|
|
|
|
|
scene_submission_rule,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
content=self._scene_submission_standard_markdown(),
|
|
|
|
|
|
content_type=AgentAssetContentType.MARKDOWN.value,
|
|
|
|
|
|
change_note="首版报销场景提交标准,覆盖附件类型、必填字段和金额阈值。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
)
|
|
|
|
|
|
self._ensure_asset_review(
|
|
|
|
|
|
scene_submission_rule,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
review_status=AgentReviewStatus.APPROVED.value,
|
|
|
|
|
|
review_note="可作为报销场景统一审核标准正式执行。",
|
|
|
|
|
|
reviewed_at=datetime.now(UTC),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if "rule.expense.travel_risk_control_standard" not in existing_codes:
|
|
|
|
|
|
travel_policy_rule = self._create_seed_asset(
|
|
|
|
|
|
asset_type=AgentAssetType.RULE.value,
|
|
|
|
|
|
code="rule.expense.travel_risk_control_standard",
|
|
|
|
|
|
name="差旅报销风险管控制度",
|
|
|
|
|
|
description="统一定义差旅报销的行程闭环、酒店地点一致性、职级差标和风险处置口径。",
|
|
|
|
|
|
domain=AgentAssetDomain.EXPENSE.value,
|
|
|
|
|
|
scenario_json=["expense", "risk_check", "travel_policy", "travel_standard"],
|
|
|
|
|
|
owner="风控与审计部",
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
|
|
|
|
|
current_version="v1.1.0",
|
|
|
|
|
|
config_json={
|
|
|
|
|
|
"severity": "high",
|
|
|
|
|
|
"enabled": True,
|
|
|
|
|
|
"block_on_high_risk": True,
|
|
|
|
|
|
"warning_on_medium_risk": True,
|
|
|
|
|
|
"source_doc": "document/development/risks/travel-risk-control-standard.md",
|
|
|
|
|
|
"runtime_kind": "travel_policy",
|
|
|
|
|
|
"rule_template_key": "travel_standard_v1",
|
|
|
|
|
|
"rule_template_label": "差旅标准模板",
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-19 20:23:58 +08:00
|
|
|
|
if travel_policy_rule is not None:
|
|
|
|
|
|
if not str(travel_policy_rule.current_version or "").strip():
|
|
|
|
|
|
travel_policy_rule.current_version = "v1.1.0"
|
|
|
|
|
|
if not str(travel_policy_rule.working_version or "").strip():
|
|
|
|
|
|
travel_policy_rule.working_version = travel_policy_rule.current_version
|
|
|
|
|
|
if not str(travel_policy_rule.published_version or "").strip():
|
|
|
|
|
|
travel_policy_rule.published_version = travel_policy_rule.current_version
|
|
|
|
|
|
travel_policy_rule.status = travel_policy_rule.status or AgentAssetStatus.ACTIVE.value
|
2026-05-17 08:38:41 +00:00
|
|
|
|
travel_policy_rule.config_json = {
|
|
|
|
|
|
"severity": "high",
|
|
|
|
|
|
"enabled": True,
|
|
|
|
|
|
"block_on_high_risk": True,
|
|
|
|
|
|
"warning_on_medium_risk": True,
|
|
|
|
|
|
"source_doc": "document/development/risks/travel-risk-control-standard.md",
|
|
|
|
|
|
"runtime_kind": "travel_policy",
|
|
|
|
|
|
"rule_template_key": "travel_standard_v1",
|
|
|
|
|
|
"rule_template_label": "差旅标准模板",
|
|
|
|
|
|
}
|
|
|
|
|
|
self._ensure_asset_version(
|
|
|
|
|
|
travel_policy_rule,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
content=self._travel_risk_control_standard_markdown(version="v1.0.0"),
|
|
|
|
|
|
content_type=AgentAssetContentType.MARKDOWN.value,
|
|
|
|
|
|
change_note="首版差旅制度执行规则,覆盖行程闭环与基础差标校验。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
)
|
|
|
|
|
|
self._ensure_asset_version(
|
|
|
|
|
|
travel_policy_rule,
|
|
|
|
|
|
version="v1.1.0",
|
|
|
|
|
|
content=self._travel_risk_control_standard_markdown(version="v1.1.0"),
|
|
|
|
|
|
content_type=AgentAssetContentType.MARKDOWN.value,
|
|
|
|
|
|
change_note="补充可执行规则块,供审核引擎直接消费差旅制度标准。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
)
|
|
|
|
|
|
self._ensure_asset_review(
|
|
|
|
|
|
travel_policy_rule,
|
|
|
|
|
|
version="v1.1.0",
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
review_status=AgentReviewStatus.APPROVED.value,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
review_note="制度口径已确认,并已补充可执行配置供审核引擎读取。",
|
|
|
|
|
|
reviewed_at=datetime.now(UTC),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self.sync_platform_risk_rules_from_library()
|
|
|
|
|
|
|
|
|
|
|
|
if COMPANY_TRAVEL_EXPENSE_RULE_CODE not in existing_codes:
|
|
|
|
|
|
company_travel_rule = self._create_seed_asset(
|
|
|
|
|
|
asset_type=AgentAssetType.RULE.value,
|
2026-05-20 09:36:01 +08:00
|
|
|
|
code=COMPANY_TRAVEL_EXPENSE_RULE_CODE,
|
|
|
|
|
|
name="公司差旅费报销规则",
|
|
|
|
|
|
description="通过 Excel 明细表维护差旅费报销标准、票据要求和审批口径。",
|
|
|
|
|
|
domain=AgentAssetDomain.EXPENSE.value,
|
|
|
|
|
|
scenario_json=list(COMPANY_TRAVEL_RULE_SCENARIO_JSON),
|
|
|
|
|
|
owner="财务制度管理组",
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
current_version=COMPANY_TRAVEL_RULE_VERSION,
|
|
|
|
|
|
config_json={
|
|
|
|
|
|
"severity": "medium",
|
2026-05-20 09:36:01 +08:00
|
|
|
|
"enabled": True,
|
|
|
|
|
|
"tag": "财务规则",
|
|
|
|
|
|
"detail_mode": "spreadsheet",
|
|
|
|
|
|
"scenario_category": COMPANY_TRAVEL_RULE_SCENARIO_JSON[0],
|
|
|
|
|
|
"ai_review_category": COMPANY_TRAVEL_RULE_SCENARIO_JSON[0],
|
|
|
|
|
|
"rule_template_label": "差旅报销 Excel 模板",
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
2026-05-19 20:23:58 +08:00
|
|
|
|
if COMPANY_COMMUNICATION_EXPENSE_RULE_CODE not in existing_codes:
|
|
|
|
|
|
company_communication_rule = self._create_seed_asset(
|
|
|
|
|
|
asset_type=AgentAssetType.RULE.value,
|
2026-05-20 09:36:01 +08:00
|
|
|
|
code=COMPANY_COMMUNICATION_EXPENSE_RULE_CODE,
|
|
|
|
|
|
name="公司通信费报销规则",
|
|
|
|
|
|
description="通过 Excel 明细表维护员工通信费报销标准、专项补充口径和审批要求。",
|
|
|
|
|
|
domain=AgentAssetDomain.EXPENSE.value,
|
|
|
|
|
|
scenario_json=list(COMPANY_COMMUNICATION_RULE_SCENARIO_JSON),
|
|
|
|
|
|
owner="财务制度管理组",
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
current_version=COMPANY_COMMUNICATION_RULE_VERSION,
|
|
|
|
|
|
config_json={
|
|
|
|
|
|
"severity": "medium",
|
2026-05-20 09:36:01 +08:00
|
|
|
|
"enabled": True,
|
|
|
|
|
|
"tag": "财务规则",
|
|
|
|
|
|
"detail_mode": "spreadsheet",
|
|
|
|
|
|
"scenario_category": COMPANY_COMMUNICATION_RULE_SCENARIO_JSON[0],
|
|
|
|
|
|
"ai_review_category": COMPANY_COMMUNICATION_RULE_SCENARIO_JSON[0],
|
|
|
|
|
|
"rule_template_label": "通信费报销 Excel 模板",
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if company_travel_rule is not None:
|
|
|
|
|
|
company_travel_rule.scenario_json = list(COMPANY_TRAVEL_RULE_SCENARIO_JSON)
|
|
|
|
|
|
if not str(company_travel_rule.current_version or "").strip():
|
|
|
|
|
|
company_travel_rule.current_version = COMPANY_TRAVEL_RULE_VERSION
|
2026-05-19 20:23:58 +08:00
|
|
|
|
if not str(company_travel_rule.working_version or "").strip():
|
|
|
|
|
|
company_travel_rule.working_version = company_travel_rule.current_version
|
|
|
|
|
|
if not str(company_travel_rule.published_version or "").strip():
|
|
|
|
|
|
company_travel_rule.published_version = company_travel_rule.current_version
|
|
|
|
|
|
if not str(company_travel_rule.status or "").strip():
|
|
|
|
|
|
company_travel_rule.status = AgentAssetStatus.ACTIVE.value
|
|
|
|
|
|
company_travel_rule.description = "通过 Excel 明细表维护差旅费报销标准、票据要求和审批口径。"
|
|
|
|
|
|
company_travel_rule.config_json = {
|
|
|
|
|
|
**(company_travel_rule.config_json or {}),
|
|
|
|
|
|
"severity": "medium",
|
|
|
|
|
|
"enabled": True,
|
2026-05-20 09:36:01 +08:00
|
|
|
|
"tag": "财务规则",
|
|
|
|
|
|
"detail_mode": "spreadsheet",
|
|
|
|
|
|
"rule_library": FINANCE_RULES_LIBRARY,
|
|
|
|
|
|
"scenario_category": COMPANY_TRAVEL_RULE_SCENARIO_JSON[0],
|
|
|
|
|
|
"ai_review_category": COMPANY_TRAVEL_RULE_SCENARIO_JSON[0],
|
|
|
|
|
|
"rule_template_label": "差旅报销 Excel 模板",
|
|
|
|
|
|
}
|
2026-05-19 20:23:58 +08:00
|
|
|
|
company_travel_rule_meta = self._ensure_company_travel_rule_spreadsheet_seed(
|
|
|
|
|
|
company_travel_rule,
|
|
|
|
|
|
version=str(company_travel_rule.current_version or COMPANY_TRAVEL_RULE_VERSION),
|
|
|
|
|
|
actor_name="系统初始化",
|
|
|
|
|
|
)
|
|
|
|
|
|
self._ensure_asset_version(
|
|
|
|
|
|
company_travel_rule,
|
|
|
|
|
|
version=str(company_travel_rule.current_version or COMPANY_TRAVEL_RULE_VERSION),
|
|
|
|
|
|
content=AgentAssetSpreadsheetManager.build_version_markdown(
|
|
|
|
|
|
rule_name=company_travel_rule.name,
|
|
|
|
|
|
version=str(company_travel_rule.current_version or COMPANY_TRAVEL_RULE_VERSION),
|
|
|
|
|
|
metadata=company_travel_rule_meta,
|
|
|
|
|
|
),
|
|
|
|
|
|
content_type=AgentAssetContentType.MARKDOWN.value,
|
|
|
|
|
|
change_note="初始化差旅费报销 Excel 规则表。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
)
|
|
|
|
|
|
if str(company_travel_rule.current_version or "").strip() == COMPANY_TRAVEL_RULE_VERSION:
|
|
|
|
|
|
self._ensure_asset_review(
|
|
|
|
|
|
company_travel_rule,
|
|
|
|
|
|
version=COMPANY_TRAVEL_RULE_VERSION,
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
review_status=AgentReviewStatus.APPROVED.value,
|
|
|
|
|
|
review_note="首版 Excel 规则表已确认,可作为财务规则使用。",
|
|
|
|
|
|
reviewed_at=datetime.now(UTC),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-20 09:36:01 +08:00
|
|
|
|
if company_communication_rule is not None:
|
|
|
|
|
|
company_communication_rule.scenario_json = list(COMPANY_COMMUNICATION_RULE_SCENARIO_JSON)
|
|
|
|
|
|
if not str(company_communication_rule.current_version or "").strip():
|
|
|
|
|
|
company_communication_rule.current_version = COMPANY_COMMUNICATION_RULE_VERSION
|
2026-05-19 20:23:58 +08:00
|
|
|
|
if not str(company_communication_rule.working_version or "").strip():
|
|
|
|
|
|
company_communication_rule.working_version = company_communication_rule.current_version
|
|
|
|
|
|
if not str(company_communication_rule.published_version or "").strip():
|
|
|
|
|
|
company_communication_rule.published_version = company_communication_rule.current_version
|
|
|
|
|
|
if not str(company_communication_rule.status or "").strip():
|
|
|
|
|
|
company_communication_rule.status = AgentAssetStatus.ACTIVE.value
|
|
|
|
|
|
company_communication_rule.description = "通过 Excel 明细表维护员工通信费报销标准、专项补充口径和审批要求。"
|
|
|
|
|
|
company_communication_rule.config_json = {
|
|
|
|
|
|
**(company_communication_rule.config_json or {}),
|
|
|
|
|
|
"severity": "medium",
|
|
|
|
|
|
"enabled": True,
|
2026-05-20 09:36:01 +08:00
|
|
|
|
"tag": "财务规则",
|
|
|
|
|
|
"detail_mode": "spreadsheet",
|
|
|
|
|
|
"rule_library": FINANCE_RULES_LIBRARY,
|
|
|
|
|
|
"scenario_category": COMPANY_COMMUNICATION_RULE_SCENARIO_JSON[0],
|
|
|
|
|
|
"ai_review_category": COMPANY_COMMUNICATION_RULE_SCENARIO_JSON[0],
|
|
|
|
|
|
"rule_template_label": "通信费报销 Excel 模板",
|
|
|
|
|
|
}
|
2026-05-19 20:23:58 +08:00
|
|
|
|
company_communication_rule_meta = self._ensure_company_communication_rule_spreadsheet_seed(
|
|
|
|
|
|
company_communication_rule,
|
|
|
|
|
|
version=str(company_communication_rule.current_version or COMPANY_COMMUNICATION_RULE_VERSION),
|
|
|
|
|
|
actor_name="系统初始化",
|
|
|
|
|
|
)
|
|
|
|
|
|
self._ensure_asset_version(
|
|
|
|
|
|
company_communication_rule,
|
|
|
|
|
|
version=str(company_communication_rule.current_version or COMPANY_COMMUNICATION_RULE_VERSION),
|
|
|
|
|
|
content=AgentAssetSpreadsheetManager.build_version_markdown(
|
|
|
|
|
|
rule_name=company_communication_rule.name,
|
|
|
|
|
|
version=str(company_communication_rule.current_version or COMPANY_COMMUNICATION_RULE_VERSION),
|
|
|
|
|
|
metadata=company_communication_rule_meta,
|
|
|
|
|
|
),
|
|
|
|
|
|
content_type=AgentAssetContentType.MARKDOWN.value,
|
|
|
|
|
|
change_note="初始化通信费报销 Excel 规则表。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
)
|
|
|
|
|
|
if str(company_communication_rule.current_version or "").strip() == COMPANY_COMMUNICATION_RULE_VERSION:
|
|
|
|
|
|
self._ensure_asset_review(
|
|
|
|
|
|
company_communication_rule,
|
|
|
|
|
|
version=COMPANY_COMMUNICATION_RULE_VERSION,
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
review_status=AgentReviewStatus.APPROVED.value,
|
|
|
|
|
|
review_note="首版 Excel 规则表已确认,可作为财务规则使用。",
|
|
|
|
|
|
reviewed_at=datetime.now(UTC),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if "skill.ar.aging_summary" not in existing_codes:
|
|
|
|
|
|
asset = self._create_seed_asset(
|
2026-05-17 08:38:41 +00:00
|
|
|
|
asset_type=AgentAssetType.SKILL.value,
|
|
|
|
|
|
code="skill.ar.aging_summary",
|
|
|
|
|
|
name="应收账龄汇总技能",
|
|
|
|
|
|
description="按客户、账龄和逾期状态汇总应收风险分布。",
|
|
|
|
|
|
domain=AgentAssetDomain.AR.value,
|
|
|
|
|
|
scenario_json=["accounts_receivable", "query", "aging_summary"],
|
|
|
|
|
|
owner="平台研发组",
|
|
|
|
|
|
reviewer="陈硕",
|
|
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
|
|
|
|
|
current_version="v1.0.0",
|
|
|
|
|
|
config_json={"input_schema": ["customer", "aging_bucket", "status"]},
|
|
|
|
|
|
)
|
|
|
|
|
|
self._ensure_asset_version(
|
|
|
|
|
|
asset,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
content=self._json_content(
|
|
|
|
|
|
{
|
|
|
|
|
|
"inputs": ["customer", "aging_bucket", "status"],
|
|
|
|
|
|
"outputs": ["receivable_total", "overdue_total", "customer_count"],
|
|
|
|
|
|
"dependencies": ["database.accounts_receivable"],
|
|
|
|
|
|
}
|
|
|
|
|
|
),
|
|
|
|
|
|
content_type=AgentAssetContentType.JSON.value,
|
|
|
|
|
|
change_note="初始化应收账龄技能快照。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if "mcp.ledger.snapshot_mock" not in existing_codes:
|
|
|
|
|
|
asset = self._create_seed_asset(
|
|
|
|
|
|
asset_type=AgentAssetType.MCP.value,
|
|
|
|
|
|
code="mcp.ledger.snapshot_mock",
|
|
|
|
|
|
name="总账快照 Mock 服务",
|
|
|
|
|
|
description="模拟返回应收、应付和费用汇总快照,供 Agent 查询和巡检。",
|
|
|
|
|
|
domain=AgentAssetDomain.SYSTEM.value,
|
|
|
|
|
|
scenario_json=["expense", "accounts_receivable", "accounts_payable"],
|
|
|
|
|
|
owner="平台研发组",
|
|
|
|
|
|
reviewer="周悦宁",
|
|
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
|
|
|
|
|
current_version="v1.0.0",
|
|
|
|
|
|
config_json={"endpoint": "mock://ledger/snapshot", "timeout_ms": 1500},
|
|
|
|
|
|
)
|
|
|
|
|
|
self._ensure_asset_version(
|
|
|
|
|
|
asset,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
content=self._json_content(
|
|
|
|
|
|
{
|
|
|
|
|
|
"service_type": "mock",
|
|
|
|
|
|
"auth_mode": "service_account",
|
|
|
|
|
|
"degrade_strategy": "return_cached_snapshot_with_warning",
|
|
|
|
|
|
}
|
|
|
|
|
|
),
|
|
|
|
|
|
content_type=AgentAssetContentType.JSON.value,
|
|
|
|
|
|
change_note="初始化总账快照 MCP。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if "task.hermes.weekly_ar_summary" not in existing_codes:
|
|
|
|
|
|
asset = self._create_seed_asset(
|
|
|
|
|
|
asset_type=AgentAssetType.TASK.value,
|
|
|
|
|
|
code="task.hermes.weekly_ar_summary",
|
|
|
|
|
|
name="Hermes 每周应收账龄汇总",
|
|
|
|
|
|
description="每周汇总逾期应收、账龄分布和客户风险变化。",
|
|
|
|
|
|
domain=AgentAssetDomain.SYSTEM.value,
|
|
|
|
|
|
scenario_json=["schedule", "accounts_receivable", "summary"],
|
|
|
|
|
|
owner="风控与审计部",
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
|
|
|
|
|
current_version="v1.0.0",
|
|
|
|
|
|
config_json={"cron": "0 10 * * 1", "agent": AgentName.HERMES.value},
|
|
|
|
|
|
)
|
|
|
|
|
|
self._ensure_asset_version(
|
|
|
|
|
|
asset,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
content=self._json_content(
|
|
|
|
|
|
{
|
|
|
|
|
|
"task_type": "weekly_ar_summary",
|
|
|
|
|
|
"schedule": "0 10 * * 1",
|
|
|
|
|
|
"target_agent": AgentName.HERMES.value,
|
|
|
|
|
|
}
|
|
|
|
|
|
),
|
|
|
|
|
|
content_type=AgentAssetContentType.JSON.value,
|
|
|
|
|
|
change_note="初始化应收账龄汇总任务。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if "task.hermes.rule_review_digest" not in existing_codes:
|
|
|
|
|
|
asset = self._create_seed_asset(
|
|
|
|
|
|
asset_type=AgentAssetType.TASK.value,
|
|
|
|
|
|
code="task.hermes.rule_review_digest",
|
|
|
|
|
|
name="Hermes 规则待审摘要",
|
|
|
|
|
|
description="每天汇总待审规则、待补样例和被拒规则修订建议。",
|
|
|
|
|
|
domain=AgentAssetDomain.SYSTEM.value,
|
|
|
|
|
|
scenario_json=["schedule", "rule_center", "review_digest"],
|
|
|
|
|
|
owner="风控与审计部",
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
|
|
|
|
|
current_version="v1.0.0",
|
|
|
|
|
|
config_json={"cron": "0 18 * * *", "agent": AgentName.HERMES.value},
|
|
|
|
|
|
)
|
|
|
|
|
|
self._ensure_asset_version(
|
|
|
|
|
|
asset,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
content=self._json_content(
|
|
|
|
|
|
{
|
|
|
|
|
|
"task_type": "rule_review_digest",
|
|
|
|
|
|
"schedule": "0 18 * * *",
|
|
|
|
|
|
"target_agent": AgentName.HERMES.value,
|
|
|
|
|
|
}
|
|
|
|
|
|
),
|
|
|
|
|
|
content_type=AgentAssetContentType.JSON.value,
|
|
|
|
|
|
change_note="初始化规则待审摘要任务。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-19 20:23:58 +08:00
|
|
|
|
if "task.hermes.knowledge_index_sync" not in existing_codes:
|
|
|
|
|
|
asset = self._create_seed_asset(
|
|
|
|
|
|
asset_type=AgentAssetType.TASK.value,
|
|
|
|
|
|
code="task.hermes.knowledge_index_sync",
|
2026-05-17 08:38:41 +00:00
|
|
|
|
name="Hermes ??????",
|
|
|
|
|
|
description="?????????? LightRAG ???????",
|
|
|
|
|
|
domain=AgentAssetDomain.SYSTEM.value,
|
|
|
|
|
|
scenario_json=["schedule", "knowledge", "rule_center"],
|
|
|
|
|
|
owner="财务制度管理组",
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
|
|
|
|
|
current_version="v1.0.0",
|
2026-05-19 20:23:58 +08:00
|
|
|
|
config_json={"cron": "0 0 * * *", "agent": AgentName.HERMES.value},
|
2026-05-17 08:38:41 +00:00
|
|
|
|
)
|
|
|
|
|
|
self._ensure_asset_version(
|
|
|
|
|
|
asset,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
content=self._json_content(
|
|
|
|
|
|
{
|
|
|
|
|
|
"task_type": "knowledge_index_sync",
|
2026-05-19 20:23:58 +08:00
|
|
|
|
"schedule": "0 0 * * *",
|
2026-05-17 08:38:41 +00:00
|
|
|
|
"target_agent": AgentName.HERMES.value,
|
|
|
|
|
|
"folder": "报销制度",
|
|
|
|
|
|
"changed_only": True,
|
|
|
|
|
|
}
|
|
|
|
|
|
),
|
|
|
|
|
|
content_type=AgentAssetContentType.JSON.value,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
change_note="初始化制度知识与规则草稿形成任务。",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_company_travel_rule_spreadsheet_seed(
|
|
|
|
|
|
self,
|
|
|
|
|
|
asset: AgentAsset,
|
|
|
|
|
|
*,
|
|
|
|
|
|
version: str,
|
|
|
|
|
|
actor_name: str,
|
|
|
|
|
|
):
|
|
|
|
|
|
manager = AgentAssetSpreadsheetManager()
|
|
|
|
|
|
manager.ensure_rule_library_dirs()
|
|
|
|
|
|
live_document = manager.store_rule_library_spreadsheet(
|
|
|
|
|
|
library=FINANCE_RULES_LIBRARY,
|
|
|
|
|
|
file_name=COMPANY_TRAVEL_EXPENSE_RULE_FILENAME,
|
|
|
|
|
|
content=self._read_or_build_company_travel_rule_file(manager),
|
|
|
|
|
|
actor_name=actor_name,
|
|
|
|
|
|
source="rule-library",
|
|
|
|
|
|
)
|
|
|
|
|
|
existing_document = (
|
|
|
|
|
|
asset.config_json.get("rule_document")
|
|
|
|
|
|
if isinstance(asset.config_json, dict)
|
|
|
|
|
|
else None
|
|
|
|
|
|
)
|
|
|
|
|
|
storage_key = (
|
|
|
|
|
|
str(existing_document.get("storage_key") or "").strip()
|
|
|
|
|
|
if isinstance(existing_document, dict)
|
|
|
|
|
|
else ""
|
|
|
|
|
|
)
|
|
|
|
|
|
if storage_key:
|
|
|
|
|
|
try:
|
|
|
|
|
|
existing_path = manager.resolve_storage_path(storage_key)
|
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
|
existing_path = None
|
|
|
|
|
|
if existing_path is not None and existing_path.exists():
|
|
|
|
|
|
asset.config_json = {
|
|
|
|
|
|
**(asset.config_json or {}),
|
|
|
|
|
|
"detail_mode": "spreadsheet",
|
|
|
|
|
|
"tag": "财务规则",
|
|
|
|
|
|
"rule_library": FINANCE_RULES_LIBRARY,
|
|
|
|
|
|
"rule_document": {
|
|
|
|
|
|
**AgentAssetSpreadsheetManager.build_rule_document_config(
|
|
|
|
|
|
live_document,
|
|
|
|
|
|
asset_version=version,
|
|
|
|
|
|
),
|
|
|
|
|
|
"storage_key": live_document.storage_key,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
return live_document
|
|
|
|
|
|
|
|
|
|
|
|
asset.config_json = {
|
|
|
|
|
|
**(asset.config_json or {}),
|
|
|
|
|
|
"detail_mode": "spreadsheet",
|
|
|
|
|
|
"tag": "财务规则",
|
|
|
|
|
|
"rule_library": FINANCE_RULES_LIBRARY,
|
|
|
|
|
|
"rule_document": {
|
|
|
|
|
|
**AgentAssetSpreadsheetManager.build_rule_document_config(
|
|
|
|
|
|
live_document,
|
|
|
|
|
|
asset_version=version,
|
|
|
|
|
|
),
|
|
|
|
|
|
"storage_key": live_document.storage_key,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
return live_document
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_company_communication_rule_spreadsheet_seed(
|
|
|
|
|
|
self,
|
|
|
|
|
|
asset: AgentAsset,
|
|
|
|
|
|
*,
|
|
|
|
|
|
version: str,
|
|
|
|
|
|
actor_name: str,
|
|
|
|
|
|
):
|
|
|
|
|
|
return self._ensure_finance_rule_spreadsheet_seed(
|
|
|
|
|
|
asset,
|
|
|
|
|
|
version=version,
|
|
|
|
|
|
actor_name=actor_name,
|
|
|
|
|
|
file_name=COMPANY_COMMUNICATION_EXPENSE_RULE_FILENAME,
|
|
|
|
|
|
fallback_sheet_name="通信费报销规则",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _read_or_build_company_travel_rule_file(
|
|
|
|
|
|
manager: AgentAssetSpreadsheetManager,
|
|
|
|
|
|
) -> bytes:
|
|
|
|
|
|
live_key = (
|
|
|
|
|
|
Path("rules")
|
|
|
|
|
|
/ FINANCE_RULES_LIBRARY
|
|
|
|
|
|
/ COMPANY_TRAVEL_EXPENSE_RULE_FILENAME
|
|
|
|
|
|
).as_posix()
|
|
|
|
|
|
live_path = manager.resolve_storage_path(live_key)
|
|
|
|
|
|
if live_path.exists():
|
|
|
|
|
|
return live_path.read_bytes()
|
|
|
|
|
|
return AgentAssetSpreadsheetManager.build_blank_rule_workbook("差旅费报销规则")
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_finance_rule_spreadsheet_seed(
|
|
|
|
|
|
self,
|
|
|
|
|
|
asset: AgentAsset,
|
|
|
|
|
|
*,
|
|
|
|
|
|
version: str,
|
|
|
|
|
|
actor_name: str,
|
|
|
|
|
|
file_name: str,
|
|
|
|
|
|
fallback_sheet_name: str,
|
|
|
|
|
|
):
|
|
|
|
|
|
manager = AgentAssetSpreadsheetManager()
|
|
|
|
|
|
manager.ensure_rule_library_dirs()
|
|
|
|
|
|
live_document = manager.store_rule_library_spreadsheet(
|
|
|
|
|
|
library=FINANCE_RULES_LIBRARY,
|
|
|
|
|
|
file_name=file_name,
|
|
|
|
|
|
content=self._read_or_build_finance_rule_file(
|
|
|
|
|
|
manager,
|
|
|
|
|
|
file_name=file_name,
|
|
|
|
|
|
fallback_sheet_name=fallback_sheet_name,
|
|
|
|
|
|
),
|
|
|
|
|
|
actor_name=actor_name,
|
|
|
|
|
|
source="rule-library",
|
|
|
|
|
|
)
|
|
|
|
|
|
existing_document = (
|
|
|
|
|
|
asset.config_json.get("rule_document")
|
|
|
|
|
|
if isinstance(asset.config_json, dict)
|
|
|
|
|
|
else None
|
|
|
|
|
|
)
|
|
|
|
|
|
storage_key = (
|
|
|
|
|
|
str(existing_document.get("storage_key") or "").strip()
|
|
|
|
|
|
if isinstance(existing_document, dict)
|
|
|
|
|
|
else ""
|
|
|
|
|
|
)
|
|
|
|
|
|
if storage_key:
|
|
|
|
|
|
try:
|
|
|
|
|
|
existing_path = manager.resolve_storage_path(storage_key)
|
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
|
existing_path = None
|
|
|
|
|
|
if existing_path is not None and existing_path.exists():
|
|
|
|
|
|
asset.config_json = {
|
|
|
|
|
|
**(asset.config_json or {}),
|
|
|
|
|
|
"detail_mode": "spreadsheet",
|
|
|
|
|
|
"tag": "财务规则",
|
|
|
|
|
|
"rule_library": FINANCE_RULES_LIBRARY,
|
|
|
|
|
|
"rule_document": {
|
|
|
|
|
|
**AgentAssetSpreadsheetManager.build_rule_document_config(
|
|
|
|
|
|
live_document,
|
|
|
|
|
|
asset_version=version,
|
|
|
|
|
|
),
|
|
|
|
|
|
"storage_key": live_document.storage_key,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
return live_document
|
|
|
|
|
|
|
|
|
|
|
|
asset.config_json = {
|
|
|
|
|
|
**(asset.config_json or {}),
|
|
|
|
|
|
"detail_mode": "spreadsheet",
|
|
|
|
|
|
"tag": "财务规则",
|
|
|
|
|
|
"rule_library": FINANCE_RULES_LIBRARY,
|
|
|
|
|
|
"rule_document": {
|
|
|
|
|
|
**AgentAssetSpreadsheetManager.build_rule_document_config(
|
|
|
|
|
|
live_document,
|
|
|
|
|
|
asset_version=version,
|
|
|
|
|
|
),
|
|
|
|
|
|
"storage_key": live_document.storage_key,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
return live_document
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _read_or_build_finance_rule_file(
|
|
|
|
|
|
manager: AgentAssetSpreadsheetManager,
|
|
|
|
|
|
*,
|
|
|
|
|
|
file_name: str,
|
|
|
|
|
|
fallback_sheet_name: str,
|
|
|
|
|
|
) -> bytes:
|
|
|
|
|
|
live_key = (
|
|
|
|
|
|
Path("rules")
|
|
|
|
|
|
/ FINANCE_RULES_LIBRARY
|
|
|
|
|
|
/ file_name
|
|
|
|
|
|
).as_posix()
|
|
|
|
|
|
live_path = manager.resolve_storage_path(live_key)
|
|
|
|
|
|
if live_path.exists():
|
|
|
|
|
|
return live_path.read_bytes()
|
|
|
|
|
|
return AgentAssetSpreadsheetManager.build_blank_rule_workbook(fallback_sheet_name)
|
|
|
|
|
|
|
|
|
|
|
|
def _create_seed_asset(
|
|
|
|
|
|
self,
|
|
|
|
|
|
*,
|
|
|
|
|
|
asset_type: str,
|
2026-05-17 08:38:41 +00:00
|
|
|
|
code: str,
|
|
|
|
|
|
name: str,
|
|
|
|
|
|
description: str,
|
|
|
|
|
|
domain: str,
|
|
|
|
|
|
scenario_json: list[str],
|
|
|
|
|
|
owner: str,
|
|
|
|
|
|
reviewer: str,
|
|
|
|
|
|
status: str,
|
|
|
|
|
|
current_version: str,
|
|
|
|
|
|
config_json: dict[str, object],
|
|
|
|
|
|
) -> AgentAsset:
|
|
|
|
|
|
asset = AgentAsset(
|
|
|
|
|
|
asset_type=asset_type,
|
|
|
|
|
|
code=code,
|
|
|
|
|
|
name=name,
|
|
|
|
|
|
description=description,
|
|
|
|
|
|
domain=domain,
|
|
|
|
|
|
scenario_json=scenario_json,
|
|
|
|
|
|
owner=owner,
|
|
|
|
|
|
reviewer=reviewer,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
status=status,
|
|
|
|
|
|
current_version=current_version,
|
|
|
|
|
|
published_version=current_version if status == AgentAssetStatus.ACTIVE.value else None,
|
|
|
|
|
|
working_version=current_version,
|
|
|
|
|
|
config_json=config_json,
|
|
|
|
|
|
)
|
2026-05-17 08:38:41 +00:00
|
|
|
|
self.db.add(asset)
|
|
|
|
|
|
self.db.flush()
|
|
|
|
|
|
return asset
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_asset_version(
|
|
|
|
|
|
self,
|
|
|
|
|
|
asset: AgentAsset,
|
|
|
|
|
|
*,
|
|
|
|
|
|
version: str,
|
|
|
|
|
|
content: str,
|
|
|
|
|
|
content_type: str,
|
|
|
|
|
|
change_note: str,
|
|
|
|
|
|
created_by: str,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
existing = self.db.scalar(
|
|
|
|
|
|
select(AgentAssetVersion).where(
|
|
|
|
|
|
AgentAssetVersion.asset_id == asset.id,
|
|
|
|
|
|
AgentAssetVersion.version == version,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
if existing is not None:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
self.db.add(
|
|
|
|
|
|
AgentAssetVersion(
|
|
|
|
|
|
asset_id=asset.id,
|
|
|
|
|
|
version=version,
|
|
|
|
|
|
content=content,
|
|
|
|
|
|
content_type=content_type,
|
|
|
|
|
|
change_note=change_note,
|
|
|
|
|
|
created_by=created_by,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_asset_review(
|
|
|
|
|
|
self,
|
|
|
|
|
|
asset: AgentAsset,
|
|
|
|
|
|
*,
|
|
|
|
|
|
version: str,
|
|
|
|
|
|
reviewer: str,
|
|
|
|
|
|
review_status: str,
|
|
|
|
|
|
review_note: str,
|
|
|
|
|
|
reviewed_at: datetime | None,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
existing = self.db.scalar(
|
|
|
|
|
|
select(AgentAssetReview).where(
|
|
|
|
|
|
AgentAssetReview.asset_id == asset.id,
|
|
|
|
|
|
AgentAssetReview.version == version,
|
|
|
|
|
|
AgentAssetReview.review_status == review_status,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
if existing is not None:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
self.db.add(
|
|
|
|
|
|
AgentAssetReview(
|
|
|
|
|
|
asset_id=asset.id,
|
|
|
|
|
|
version=version,
|
|
|
|
|
|
reviewer=reviewer,
|
|
|
|
|
|
review_status=review_status,
|
|
|
|
|
|
review_note=review_note,
|
|
|
|
|
|
reviewed_at=reviewed_at,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-19 20:23:58 +08:00
|
|
|
|
def _remove_legacy_rule_assets(self) -> None:
|
2026-05-17 08:38:41 +00:00
|
|
|
|
assets = list(
|
|
|
|
|
|
self.db.scalars(
|
|
|
|
|
|
select(AgentAsset).where(AgentAsset.code.in_(LEGACY_RULE_CODES))
|
|
|
|
|
|
).all()
|
|
|
|
|
|
)
|
|
|
|
|
|
for asset in assets:
|
|
|
|
|
|
self.db.delete(asset)
|
|
|
|
|
|
|
|
|
|
|
|
obsolete_logs = list(
|
|
|
|
|
|
self.db.scalars(
|
|
|
|
|
|
select(AuditLog).where(AuditLog.resource_id.in_(LEGACY_RULE_CODES))
|
|
|
|
|
|
).all()
|
|
|
|
|
|
)
|
2026-05-19 20:23:58 +08:00
|
|
|
|
for log in obsolete_logs:
|
|
|
|
|
|
self.db.delete(log)
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_agent_asset_schema(self) -> None:
|
|
|
|
|
|
bind = self.db.get_bind()
|
|
|
|
|
|
inspector = inspect(bind)
|
|
|
|
|
|
if "agent_assets" not in inspector.get_table_names():
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
column_names = {column["name"] for column in inspector.get_columns("agent_assets")}
|
|
|
|
|
|
migration_statements: list[str] = []
|
|
|
|
|
|
if "published_version" not in column_names:
|
|
|
|
|
|
migration_statements.append("ALTER TABLE agent_assets ADD COLUMN published_version VARCHAR(30)")
|
|
|
|
|
|
if "working_version" not in column_names:
|
|
|
|
|
|
migration_statements.append("ALTER TABLE agent_assets ADD COLUMN working_version VARCHAR(30)")
|
|
|
|
|
|
|
|
|
|
|
|
for statement in migration_statements:
|
|
|
|
|
|
self.db.execute(text(statement))
|
|
|
|
|
|
|
|
|
|
|
|
self.db.execute(
|
|
|
|
|
|
text(
|
|
|
|
|
|
"UPDATE agent_assets "
|
|
|
|
|
|
"SET working_version = COALESCE(working_version, current_version), "
|
|
|
|
|
|
"published_version = CASE "
|
|
|
|
|
|
"WHEN published_version IS NOT NULL THEN published_version "
|
|
|
|
|
|
"WHEN status = 'active' THEN current_version "
|
|
|
|
|
|
"ELSE published_version END"
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if migration_statements:
|
|
|
|
|
|
self.db.commit()
|
2026-05-17 08:38:41 +00:00
|
|
|
|
|
|
|
|
|
|
def _attachment_submission_requirement_markdown(
|
|
|
|
|
|
self,
|
|
|
|
|
|
*,
|
|
|
|
|
|
version_note: str,
|
|
|
|
|
|
include_review_note: bool,
|
|
|
|
|
|
) -> str:
|
|
|
|
|
|
sections = [
|
|
|
|
|
|
"# 报销附件与单据完整性规则",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"## 模板信息",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"- 模板键:`attachment_requirement_v1`",
|
|
|
|
|
|
"- 来源文档:报销制度 / 单据与附件要求",
|
|
|
|
|
|
"- 审核状态:待审核",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"## 目标",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"统一约束报销提交时的票据、附件与替代凭证要求,避免缺件、错件和无依据流转。",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"## 适用范围",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"适用于员工报销提交场景,重点覆盖差旅、住宿、交通、餐费、办公和其他费用的附件校验。",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"## 输入字段",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"- expense_type",
|
|
|
|
|
|
"- attachments",
|
|
|
|
|
|
"- invoice_count",
|
|
|
|
|
|
"- reason",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"## 判断规则",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"- 报销提交前至少需要 1 份有效附件。",
|
|
|
|
|
|
"- 金额类报销原则上应提供合法票据;特殊场景无发票时,必须补充收据与情况说明。",
|
|
|
|
|
|
"- 差旅交通报销需提供行程单或等效凭证;住宿报销需提供酒店票据或等效住宿凭证。",
|
|
|
|
|
|
"- 缺少必要附件时直接拦截,并提示补件后重新提交。",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"## 输出",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"- 风险编码:`invoice_anomaly`",
|
|
|
|
|
|
"- 默认动作:`block`",
|
|
|
|
|
|
"- 处理说明:附件或单据不完整时退回补充。",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"## 来源依据",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"- 报销制度对票据、附件、替代凭证和补件要求的统一约束。",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"## 审核约束",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"- 当前规则属于真实业务规则,但仍处于待审核状态。",
|
|
|
|
|
|
"- 上线前需由制度管理员确认收据替代、补件时限和特殊场景豁免口径。",
|
|
|
|
|
|
f"- 当前版本说明:{version_note}",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"## 管理员备注",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"需要结合公司正式报销制度,补充各场景附件替代口径与例外审批要求。",
|
|
|
|
|
|
]
|
|
|
|
|
|
if include_review_note:
|
|
|
|
|
|
sections.extend(["", "```expense-rule", json.dumps(ATTACHMENT_RULE_RUNTIME_CONFIG, ensure_ascii=False, indent=2), "```"])
|
|
|
|
|
|
return "\n".join(sections)
|
|
|
|
|
|
|
|
|
|
|
|
def _scene_submission_standard_markdown(self) -> str:
|
|
|
|
|
|
return self._markdown_content(build_scene_submission_standard_markdown())
|
|
|
|
|
|
|
|
|
|
|
|
def _travel_risk_control_standard_markdown(self, *, version: str = "v1.1.0") -> str:
|
|
|
|
|
|
return self._markdown_content(build_travel_risk_control_standard_markdown())
|
|
|
|
|
|
|
2026-05-19 20:23:58 +08:00
|
|
|
|
def _iter_platform_risk_manifests(self) -> list[tuple[str, dict[str, object]]]:
|
|
|
|
|
|
manager = AgentAssetRuleLibraryManager()
|
|
|
|
|
|
manifests: list[tuple[str, dict[str, object]]] = []
|
|
|
|
|
|
for file_name in sorted(manager.list_rule_library_json_files(library=RISK_RULES_LIBRARY)):
|
|
|
|
|
|
payload = manager.read_rule_library_json(library=RISK_RULES_LIBRARY, file_name=file_name)
|
|
|
|
|
|
if payload.get("enabled") is False:
|
|
|
|
|
|
continue
|
|
|
|
|
|
manifests.append((file_name, payload))
|
|
|
|
|
|
return manifests
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _resolve_platform_risk_category(manifest: dict[str, object]) -> str:
|
|
|
|
|
|
explicit = str(manifest.get("risk_category") or "").strip()
|
|
|
|
|
|
if explicit:
|
|
|
|
|
|
return explicit
|
|
|
|
|
|
|
|
|
|
|
|
rule_code = str(manifest.get("rule_code") or "").strip().lower()
|
|
|
|
|
|
applies_to = manifest.get("applies_to") if isinstance(manifest.get("applies_to"), dict) else {}
|
|
|
|
|
|
domains = {str(item or "").strip().lower() for item in applies_to.get("domains") or []}
|
|
|
|
|
|
expense_types = {
|
|
|
|
|
|
str(item or "").strip().lower() for item in applies_to.get("expense_types") or []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if rule_code.startswith("risk.invoice."):
|
|
|
|
|
|
return "发票"
|
|
|
|
|
|
if "meal" in domains or "entertainment" in expense_types:
|
|
|
|
|
|
return "餐饮招待"
|
|
|
|
|
|
if "transport" in expense_types or "consecutive_transport" in rule_code:
|
|
|
|
|
|
return "交通出行"
|
|
|
|
|
|
if "office" in expense_types:
|
|
|
|
|
|
return "办公物料"
|
|
|
|
|
|
if "travel" in domains or rule_code.startswith("risk.travel."):
|
|
|
|
|
|
return "差旅"
|
|
|
|
|
|
if rule_code.startswith("risk.expense."):
|
|
|
|
|
|
return "费用科目"
|
|
|
|
|
|
return "通用"
|
|
|
|
|
|
|
|
|
|
|
|
def _platform_risk_scenario_json(self, manifest: dict[str, object]) -> list[str]:
|
|
|
|
|
|
category = self._resolve_platform_risk_category(manifest)
|
|
|
|
|
|
return [category] if category else ["通用"]
|
|
|
|
|
|
|
|
|
|
|
|
def _platform_risk_config_json(self, file_name: str, manifest: dict[str, object]) -> dict[str, object]:
|
|
|
|
|
|
outcomes = manifest.get("outcomes") if isinstance(manifest.get("outcomes"), dict) else {}
|
|
|
|
|
|
fail_outcome = outcomes.get("fail") if isinstance(outcomes.get("fail"), dict) else {}
|
|
|
|
|
|
risk_category = self._resolve_platform_risk_category(manifest)
|
|
|
|
|
|
return {
|
|
|
|
|
|
"severity": str(fail_outcome.get("severity") or "medium"),
|
|
|
|
|
|
"enabled": True,
|
|
|
|
|
|
"tag": "风险规则",
|
|
|
|
|
|
"detail_mode": "json_risk",
|
|
|
|
|
|
"risk_category": risk_category,
|
|
|
|
|
|
"rule_library": RISK_RULES_LIBRARY,
|
|
|
|
|
|
"rule_document": {
|
|
|
|
|
|
"file_name": file_name,
|
|
|
|
|
|
"storage_key": f"rules/{RISK_RULES_LIBRARY}/{file_name}",
|
|
|
|
|
|
},
|
|
|
|
|
|
"ontology_signal": str(manifest.get("ontology_signal") or "").strip(),
|
|
|
|
|
|
"evaluator": str(manifest.get("evaluator") or "").strip(),
|
|
|
|
|
|
"source_ref": (
|
|
|
|
|
|
(manifest.get("metadata") or {}).get("source_ref")
|
|
|
|
|
|
if isinstance(manifest.get("metadata"), dict)
|
|
|
|
|
|
else ""
|
|
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def _build_platform_risk_seed_assets(self) -> list[AgentAsset]:
|
|
|
|
|
|
assets: list[AgentAsset] = []
|
|
|
|
|
|
for file_name, manifest in self._iter_platform_risk_manifests():
|
|
|
|
|
|
rule_code = str(manifest.get("rule_code") or "").strip()
|
|
|
|
|
|
if not rule_code:
|
|
|
|
|
|
continue
|
|
|
|
|
|
metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
|
|
|
|
|
|
source_ref = str(metadata.get("source_ref") or "").strip()
|
|
|
|
|
|
rule_description = str(manifest.get("description") or "").strip()
|
|
|
|
|
|
assets.append(
|
|
|
|
|
|
AgentAsset(
|
|
|
|
|
|
asset_type=AgentAssetType.RULE.value,
|
|
|
|
|
|
code=rule_code,
|
|
|
|
|
|
name=str(manifest.get("name") or rule_code),
|
|
|
|
|
|
description=rule_description
|
|
|
|
|
|
or f"平台通用风险规则:{source_ref or manifest.get('name') or rule_code}",
|
|
|
|
|
|
domain=AgentAssetDomain.EXPENSE.value,
|
|
|
|
|
|
scenario_json=self._platform_risk_scenario_json(manifest),
|
|
|
|
|
|
owner=str(metadata.get("owner") or "风控与审计部"),
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
|
|
|
|
|
current_version="v1.0.0",
|
|
|
|
|
|
published_version="v1.0.0",
|
|
|
|
|
|
working_version="v1.0.0",
|
|
|
|
|
|
config_json=self._platform_risk_config_json(file_name, manifest),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
return assets
|
|
|
|
|
|
|
|
|
|
|
|
def sync_platform_risk_rules_from_library(self) -> int:
|
|
|
|
|
|
existing_codes = set(self.db.scalars(select(AgentAsset.code)).all())
|
|
|
|
|
|
before_count = len(existing_codes)
|
|
|
|
|
|
self._ensure_platform_risk_rules_from_library(existing_codes)
|
|
|
|
|
|
self.db.flush()
|
|
|
|
|
|
after_codes = set(self.db.scalars(select(AgentAsset.code)).all())
|
|
|
|
|
|
synced = max(len(after_codes) - before_count, 0)
|
|
|
|
|
|
manifest_count = len(self._iter_platform_risk_manifests())
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
"Platform risk rules synced from library",
|
|
|
|
|
|
extra={"manifest_count": manifest_count, "created_count": synced, "total": len(after_codes)},
|
|
|
|
|
|
)
|
|
|
|
|
|
return manifest_count
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_platform_risk_rules_from_library(self, existing_codes: set[str]) -> None:
|
|
|
|
|
|
for file_name, manifest in self._iter_platform_risk_manifests():
|
|
|
|
|
|
rule_code = str(manifest.get("rule_code") or "").strip()
|
|
|
|
|
|
if not rule_code:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
|
|
|
|
|
|
source_ref = str(metadata.get("source_ref") or "").strip()
|
|
|
|
|
|
rule_description = str(manifest.get("description") or "").strip()
|
|
|
|
|
|
config_json = self._platform_risk_config_json(file_name, manifest)
|
|
|
|
|
|
scenario_json = self._platform_risk_scenario_json(manifest)
|
|
|
|
|
|
|
|
|
|
|
|
asset = self.db.scalar(select(AgentAsset).where(AgentAsset.code == rule_code))
|
|
|
|
|
|
if asset is None and rule_code not in existing_codes:
|
|
|
|
|
|
asset = self._create_seed_asset(
|
|
|
|
|
|
asset_type=AgentAssetType.RULE.value,
|
|
|
|
|
|
code=rule_code,
|
|
|
|
|
|
name=str(manifest.get("name") or rule_code),
|
|
|
|
|
|
description=rule_description
|
|
|
|
|
|
or f"平台通用风险规则:{source_ref or manifest.get('name') or rule_code}",
|
|
|
|
|
|
domain=AgentAssetDomain.EXPENSE.value,
|
|
|
|
|
|
scenario_json=scenario_json,
|
|
|
|
|
|
owner=str(metadata.get("owner") or "风控与审计部"),
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
|
|
|
|
|
current_version="v1.0.0",
|
|
|
|
|
|
config_json=config_json,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if asset is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
if not str(asset.current_version or "").strip():
|
|
|
|
|
|
asset.current_version = "v1.0.0"
|
|
|
|
|
|
if not str(asset.working_version or "").strip():
|
|
|
|
|
|
asset.working_version = asset.current_version
|
|
|
|
|
|
if not str(asset.published_version or "").strip():
|
|
|
|
|
|
asset.published_version = asset.current_version
|
|
|
|
|
|
asset.status = asset.status or AgentAssetStatus.ACTIVE.value
|
|
|
|
|
|
asset.name = str(manifest.get("name") or asset.name or rule_code)
|
|
|
|
|
|
if rule_description:
|
|
|
|
|
|
asset.description = rule_description
|
|
|
|
|
|
asset.config_json = config_json
|
|
|
|
|
|
asset.scenario_json = scenario_json
|
|
|
|
|
|
|
|
|
|
|
|
self._ensure_asset_version(
|
|
|
|
|
|
asset,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
content=self._platform_risk_rule_markdown(asset, manifest=manifest, file_name=file_name),
|
|
|
|
|
|
content_type=AgentAssetContentType.MARKDOWN.value,
|
|
|
|
|
|
change_note=f"平台通用风险规则:{asset.name}",
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
)
|
|
|
|
|
|
self._ensure_asset_review(
|
|
|
|
|
|
asset,
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
review_status=AgentReviewStatus.APPROVED.value,
|
|
|
|
|
|
review_note="平台内置风险规则,供提交验审与风险问答共用。",
|
|
|
|
|
|
reviewed_at=datetime.now(UTC),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _platform_risk_rule_markdown(
|
|
|
|
|
|
asset: AgentAsset,
|
|
|
|
|
|
*,
|
|
|
|
|
|
manifest: dict[str, object] | None = None,
|
|
|
|
|
|
file_name: str = "",
|
|
|
|
|
|
) -> str:
|
|
|
|
|
|
config = asset.config_json if isinstance(asset.config_json, dict) else {}
|
|
|
|
|
|
rule_document = config.get("rule_document") if isinstance(config.get("rule_document"), dict) else {}
|
|
|
|
|
|
resolved_file_name = file_name or str(rule_document.get("file_name") or "").strip()
|
|
|
|
|
|
evaluator = str(config.get("evaluator") or (manifest or {}).get("evaluator") or "").strip()
|
|
|
|
|
|
ontology_signal = str(config.get("ontology_signal") or (manifest or {}).get("ontology_signal") or "").strip()
|
|
|
|
|
|
source_ref = str(config.get("source_ref") or "").strip()
|
|
|
|
|
|
if not source_ref and isinstance(manifest, dict):
|
|
|
|
|
|
metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
|
|
|
|
|
|
source_ref = str(metadata.get("source_ref") or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
lines = [
|
|
|
|
|
|
f"# {asset.name}",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"## 规则类型",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"- 平台内置通用风险规则(`json_risk`)",
|
|
|
|
|
|
]
|
|
|
|
|
|
if evaluator:
|
|
|
|
|
|
lines.append(f"- 检查器:`{evaluator}`")
|
|
|
|
|
|
if ontology_signal:
|
|
|
|
|
|
lines.append(f"- 本体信号:`{ontology_signal}`")
|
|
|
|
|
|
if source_ref:
|
|
|
|
|
|
lines.extend(["", "## 来源", "", f"- {source_ref}"])
|
|
|
|
|
|
if resolved_file_name:
|
|
|
|
|
|
lines.extend(
|
|
|
|
|
|
[
|
|
|
|
|
|
"",
|
|
|
|
|
|
"## 配置文件",
|
|
|
|
|
|
"",
|
|
|
|
|
|
f"- `rules/{RISK_RULES_LIBRARY}/{resolved_file_name}`",
|
|
|
|
|
|
]
|
|
|
|
|
|
)
|
|
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _platform_destination_location_risk_markdown() -> str:
|
|
|
|
|
|
return AgentFoundationService._platform_risk_rule_markdown(
|
|
|
|
|
|
AgentAsset(name="申报地点与票据地点一致", config_json={"evaluator": "location_consistency"}),
|
|
|
|
|
|
manifest={
|
|
|
|
|
|
"evaluator": "location_consistency",
|
|
|
|
|
|
"ontology_signal": "location_mismatch",
|
|
|
|
|
|
"metadata": {"source_ref": "常用risk.txt / 一、出差类 / 行程不符"},
|
|
|
|
|
|
},
|
|
|
|
|
|
file_name=PLATFORM_DESTINATION_LOCATION_RULE_FILENAME,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-17 08:38:41 +00:00
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _markdown_content(content: str) -> str:
|
|
|
|
|
|
return content
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _json_content(content: dict[str, object]) -> str:
|
|
|
|
|
|
return json.dumps(content, ensure_ascii=False, sort_keys=True, indent=2)
|