feat: 增强 agent_assets 功能,支持更多资产操作
This commit is contained in:
@@ -2,19 +2,32 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Header, HTTPException, Query, status
|
from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query, status
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.api.deps import get_db
|
from app.api.deps import (
|
||||||
|
CurrentUserContext,
|
||||||
|
get_current_user,
|
||||||
|
get_db,
|
||||||
|
require_admin_user,
|
||||||
|
require_rule_editor_user,
|
||||||
|
require_rule_reviewer_user,
|
||||||
|
)
|
||||||
from app.schemas.agent_asset import (
|
from app.schemas.agent_asset import (
|
||||||
AgentAssetCreate,
|
AgentAssetCreate,
|
||||||
AgentAssetListItem,
|
AgentAssetListItem,
|
||||||
|
AgentAssetOnlyOfficeCallbackRead,
|
||||||
|
AgentAssetOnlyOfficeCallbackWrite,
|
||||||
|
AgentAssetOnlyOfficeConfigRead,
|
||||||
AgentAssetRead,
|
AgentAssetRead,
|
||||||
AgentAssetReviewCreate,
|
AgentAssetReviewCreate,
|
||||||
AgentAssetReviewRead,
|
AgentAssetReviewRead,
|
||||||
|
AgentAssetVersionCompareRead,
|
||||||
AgentAssetUpdate,
|
AgentAssetUpdate,
|
||||||
AgentAssetVersionCreate,
|
AgentAssetVersionCreate,
|
||||||
AgentAssetVersionRead,
|
AgentAssetVersionRead,
|
||||||
|
AgentAssetVersionTimelineItemRead,
|
||||||
)
|
)
|
||||||
from app.schemas.common import ErrorResponse
|
from app.schemas.common import ErrorResponse
|
||||||
from app.services.agent_assets import AgentAssetService
|
from app.services.agent_assets import AgentAssetService
|
||||||
@@ -29,6 +42,10 @@ RequestIdHeader = Annotated[
|
|||||||
str | None,
|
str | None,
|
||||||
Header(description="外部请求 ID,用于串联审计日志和上游调用链。"),
|
Header(description="外部请求 ID,用于串联审计日志和上游调用链。"),
|
||||||
]
|
]
|
||||||
|
CurrentUser = Annotated[CurrentUserContext, Depends(get_current_user)]
|
||||||
|
AdminUser = Annotated[CurrentUserContext, Depends(require_admin_user)]
|
||||||
|
RuleEditorUser = Annotated[CurrentUserContext, Depends(require_rule_editor_user)]
|
||||||
|
RuleReviewerUser = Annotated[CurrentUserContext, Depends(require_rule_reviewer_user)]
|
||||||
|
|
||||||
|
|
||||||
def _handle_asset_error(exc: Exception) -> None:
|
def _handle_asset_error(exc: Exception) -> None:
|
||||||
@@ -93,6 +110,185 @@ def get_agent_asset(asset_id: str, db: DbSession) -> AgentAssetRead:
|
|||||||
return asset
|
return asset
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/{asset_id}/spreadsheet/onlyoffice-config",
|
||||||
|
response_model=AgentAssetOnlyOfficeConfigRead,
|
||||||
|
summary="读取规则 Excel 的 ONLYOFFICE 配置",
|
||||||
|
description="为规则详情页中的 Excel 规则表生成 ONLYOFFICE 配置。",
|
||||||
|
)
|
||||||
|
def get_agent_asset_spreadsheet_onlyoffice_config(
|
||||||
|
asset_id: str,
|
||||||
|
current_user: CurrentUser,
|
||||||
|
db: DbSession,
|
||||||
|
version: Annotated[
|
||||||
|
str | None,
|
||||||
|
Query(description="可选的规则版本号;不传时默认当前版本。"),
|
||||||
|
] = None,
|
||||||
|
) -> AgentAssetOnlyOfficeConfigRead:
|
||||||
|
try:
|
||||||
|
return AgentAssetService(db).build_rule_spreadsheet_onlyoffice_config(
|
||||||
|
asset_id,
|
||||||
|
current_user,
|
||||||
|
version=version,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
_handle_asset_error(exc)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/{asset_id}/spreadsheet/content",
|
||||||
|
response_class=FileResponse,
|
||||||
|
summary="下载或预览规则 Excel 文件",
|
||||||
|
description="按版本返回规则的 Excel 快照,用于浏览器预览或下载。",
|
||||||
|
)
|
||||||
|
def get_agent_asset_spreadsheet_content(
|
||||||
|
asset_id: str,
|
||||||
|
_: CurrentUser,
|
||||||
|
db: DbSession,
|
||||||
|
version: Annotated[
|
||||||
|
str | None,
|
||||||
|
Query(description="可选的规则版本号;不传时默认当前版本。"),
|
||||||
|
] = None,
|
||||||
|
) -> FileResponse:
|
||||||
|
try:
|
||||||
|
file_path, media_type, filename = AgentAssetService(db).get_rule_spreadsheet_content(
|
||||||
|
asset_id,
|
||||||
|
version=version,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
_handle_asset_error(exc)
|
||||||
|
|
||||||
|
return FileResponse(file_path, media_type=media_type, filename=filename)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/{asset_id}/spreadsheet/onlyoffice/content",
|
||||||
|
response_class=FileResponse,
|
||||||
|
summary="供 ONLYOFFICE 读取规则 Excel 源文件",
|
||||||
|
description="使用短时令牌供 ONLYOFFICE 拉取规则表源文件。",
|
||||||
|
)
|
||||||
|
def get_agent_asset_spreadsheet_onlyoffice_content(
|
||||||
|
asset_id: str,
|
||||||
|
db: DbSession,
|
||||||
|
version: Annotated[
|
||||||
|
str,
|
||||||
|
Query(min_length=1, description="规则版本号。"),
|
||||||
|
],
|
||||||
|
access_token: Annotated[
|
||||||
|
str,
|
||||||
|
Query(min_length=1, description="ONLYOFFICE 临时访问令牌。"),
|
||||||
|
],
|
||||||
|
) -> FileResponse:
|
||||||
|
try:
|
||||||
|
service = AgentAssetService(db)
|
||||||
|
service.validate_rule_spreadsheet_access_token(asset_id, version, access_token)
|
||||||
|
file_path, media_type, filename = service.get_rule_spreadsheet_content(
|
||||||
|
asset_id,
|
||||||
|
version=version,
|
||||||
|
)
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc)) from exc
|
||||||
|
except Exception as exc:
|
||||||
|
_handle_asset_error(exc)
|
||||||
|
|
||||||
|
return FileResponse(file_path, media_type=media_type, filename=filename)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/{asset_id}/spreadsheet/upload",
|
||||||
|
response_model=AgentAssetRead,
|
||||||
|
status_code=status.HTTP_201_CREATED,
|
||||||
|
summary="上传规则 Excel 文件",
|
||||||
|
description="为指定规则上传新的 Excel 快照,并自动生成新规则版本。",
|
||||||
|
)
|
||||||
|
def upload_agent_asset_spreadsheet(
|
||||||
|
asset_id: str,
|
||||||
|
content: Annotated[
|
||||||
|
bytes,
|
||||||
|
Body(
|
||||||
|
media_type="application/octet-stream",
|
||||||
|
description="待上传的 Excel 文件二进制内容。",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
filename: Annotated[str, Query(min_length=1, description="原始文件名。")],
|
||||||
|
current_user: RuleEditorUser,
|
||||||
|
db: DbSession,
|
||||||
|
x_request_id: RequestIdHeader = None,
|
||||||
|
) -> AgentAssetRead:
|
||||||
|
try:
|
||||||
|
return AgentAssetService(db).upload_rule_spreadsheet(
|
||||||
|
asset_id,
|
||||||
|
filename=filename,
|
||||||
|
content=content,
|
||||||
|
actor=current_user.name,
|
||||||
|
request_id=x_request_id,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
_handle_asset_error(exc)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/{asset_id}/spreadsheet/import-content",
|
||||||
|
response_model=AgentAssetRead,
|
||||||
|
status_code=status.HTTP_201_CREATED,
|
||||||
|
summary="导入规则 Excel 表格内容",
|
||||||
|
description="读取上传 Excel 中的工作表内容,写回当前规则表;保留当前规则文件名与规则身份。",
|
||||||
|
)
|
||||||
|
def import_agent_asset_spreadsheet_content(
|
||||||
|
asset_id: str,
|
||||||
|
content: Annotated[
|
||||||
|
bytes,
|
||||||
|
Body(
|
||||||
|
media_type="application/octet-stream",
|
||||||
|
description="待导入的 Excel 文件二进制内容。",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
filename: Annotated[str, Query(min_length=1, description="上传文件原始文件名。")],
|
||||||
|
current_user: RuleEditorUser,
|
||||||
|
db: DbSession,
|
||||||
|
x_request_id: RequestIdHeader = None,
|
||||||
|
) -> AgentAssetRead:
|
||||||
|
try:
|
||||||
|
return AgentAssetService(db).import_rule_spreadsheet_content(
|
||||||
|
asset_id,
|
||||||
|
filename=filename,
|
||||||
|
content=content,
|
||||||
|
actor=current_user.name,
|
||||||
|
request_id=x_request_id,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
_handle_asset_error(exc)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/{asset_id}/spreadsheet/onlyoffice/callback",
|
||||||
|
response_model=AgentAssetOnlyOfficeCallbackRead,
|
||||||
|
summary="接收规则 Excel 的 ONLYOFFICE 回调",
|
||||||
|
description="接收 ONLYOFFICE 回写内容,并自动生成新的规则版本。",
|
||||||
|
)
|
||||||
|
def handle_agent_asset_spreadsheet_onlyoffice_callback(
|
||||||
|
asset_id: str,
|
||||||
|
payload: AgentAssetOnlyOfficeCallbackWrite,
|
||||||
|
db: DbSession,
|
||||||
|
version: Annotated[
|
||||||
|
str,
|
||||||
|
Query(min_length=1, description="打开编辑器时对应的规则版本号。"),
|
||||||
|
],
|
||||||
|
) -> AgentAssetOnlyOfficeCallbackRead:
|
||||||
|
try:
|
||||||
|
AgentAssetService(db).handle_rule_spreadsheet_onlyoffice_callback(
|
||||||
|
asset_id,
|
||||||
|
version=version,
|
||||||
|
payload=payload.model_dump(),
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
_handle_asset_error(exc)
|
||||||
|
|
||||||
|
return AgentAssetOnlyOfficeCallbackRead()
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"",
|
"",
|
||||||
response_model=AgentAssetRead,
|
response_model=AgentAssetRead,
|
||||||
@@ -237,11 +433,22 @@ def create_agent_asset_version(
|
|||||||
def create_agent_asset_review(
|
def create_agent_asset_review(
|
||||||
asset_id: str,
|
asset_id: str,
|
||||||
payload: AgentAssetReviewCreate,
|
payload: AgentAssetReviewCreate,
|
||||||
|
current_user: CurrentUser,
|
||||||
db: DbSession,
|
db: DbSession,
|
||||||
x_actor: ActorHeader = None,
|
x_actor: ActorHeader = None,
|
||||||
x_request_id: RequestIdHeader = None,
|
x_request_id: RequestIdHeader = None,
|
||||||
) -> AgentAssetReviewRead:
|
) -> AgentAssetReviewRead:
|
||||||
try:
|
try:
|
||||||
|
role_codes = {item.strip() for item in current_user.role_codes}
|
||||||
|
if payload.review_status.value == "pending":
|
||||||
|
if not (
|
||||||
|
current_user.is_admin
|
||||||
|
or "manager" in role_codes
|
||||||
|
or "finance" in role_codes
|
||||||
|
):
|
||||||
|
raise PermissionError("只有财务人员或高级管理人员可以提交审核。")
|
||||||
|
elif not (current_user.is_admin or "manager" in role_codes):
|
||||||
|
raise PermissionError("只有高级管理人员可以审核规则。")
|
||||||
return AgentAssetService(db).create_review(
|
return AgentAssetService(db).create_review(
|
||||||
asset_id,
|
asset_id,
|
||||||
payload,
|
payload,
|
||||||
@@ -270,6 +477,7 @@ def create_agent_asset_review(
|
|||||||
)
|
)
|
||||||
def activate_agent_asset(
|
def activate_agent_asset(
|
||||||
asset_id: str,
|
asset_id: str,
|
||||||
|
_: RuleReviewerUser,
|
||||||
db: DbSession,
|
db: DbSession,
|
||||||
x_actor: ActorHeader = None,
|
x_actor: ActorHeader = None,
|
||||||
x_request_id: RequestIdHeader = None,
|
x_request_id: RequestIdHeader = None,
|
||||||
@@ -282,3 +490,68 @@ def activate_agent_asset(
|
|||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
_handle_asset_error(exc)
|
_handle_asset_error(exc)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/{asset_id}/versions/{version}/restore",
|
||||||
|
response_model=AgentAssetRead,
|
||||||
|
summary="基于历史版本恢复工作稿",
|
||||||
|
description="复制指定历史版本内容生成新的工作版本,用于误上线后的快速恢复与重新审核。",
|
||||||
|
)
|
||||||
|
def restore_agent_asset_version(
|
||||||
|
asset_id: str,
|
||||||
|
version: str,
|
||||||
|
current_user: RuleReviewerUser,
|
||||||
|
db: DbSession,
|
||||||
|
x_actor: ActorHeader = None,
|
||||||
|
x_request_id: RequestIdHeader = None,
|
||||||
|
) -> AgentAssetRead:
|
||||||
|
try:
|
||||||
|
return AgentAssetService(db).restore_version_as_working_copy(
|
||||||
|
asset_id,
|
||||||
|
version,
|
||||||
|
actor=(x_actor or current_user.name or "system").strip() or "system",
|
||||||
|
request_id=x_request_id,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
_handle_asset_error(exc)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/{asset_id}/version-timeline",
|
||||||
|
response_model=list[AgentAssetVersionTimelineItemRead],
|
||||||
|
summary="读取规则版本流转时间线",
|
||||||
|
description="返回规则版本创建、提交审核、审核结果和正式上线等流转事件。",
|
||||||
|
)
|
||||||
|
def get_agent_asset_version_timeline(
|
||||||
|
asset_id: str,
|
||||||
|
_: CurrentUser,
|
||||||
|
db: DbSession,
|
||||||
|
) -> list[AgentAssetVersionTimelineItemRead]:
|
||||||
|
try:
|
||||||
|
return AgentAssetService(db).list_version_timeline(asset_id)
|
||||||
|
except Exception as exc:
|
||||||
|
_handle_asset_error(exc)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/{asset_id}/versions/compare",
|
||||||
|
response_model=AgentAssetVersionCompareRead,
|
||||||
|
summary="比较两个规则表版本",
|
||||||
|
description="对比两个 Excel 规则表版本的工作表变化与单元格级差异。",
|
||||||
|
)
|
||||||
|
def compare_agent_asset_spreadsheet_versions(
|
||||||
|
asset_id: str,
|
||||||
|
_: CurrentUser,
|
||||||
|
db: DbSession,
|
||||||
|
base_version: Annotated[str, Query(min_length=1, description="基准版本号")],
|
||||||
|
target_version: Annotated[str, Query(min_length=1, description="对比版本号")],
|
||||||
|
) -> AgentAssetVersionCompareRead:
|
||||||
|
try:
|
||||||
|
return AgentAssetService(db).compare_spreadsheet_versions(
|
||||||
|
asset_id,
|
||||||
|
base_version=base_version,
|
||||||
|
target_version=target_version,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
_handle_asset_error(exc)
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ class AgentAsset(Base):
|
|||||||
reviewer: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
reviewer: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||||
status: Mapped[str] = mapped_column(String(20), index=True, default="draft")
|
status: Mapped[str] = mapped_column(String(20), index=True, default="draft")
|
||||||
current_version: Mapped[str | None] = mapped_column(String(30), nullable=True)
|
current_version: Mapped[str | None] = mapped_column(String(30), nullable=True)
|
||||||
|
published_version: Mapped[str | None] = mapped_column(String(30), nullable=True)
|
||||||
|
working_version: Mapped[str | None] = mapped_column(String(30), nullable=True)
|
||||||
config_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
|
config_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at: Mapped[datetime] = mapped_column(
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ class AgentAssetUpdate(BaseModel):
|
|||||||
reviewer: str | None = Field(default=None, max_length=100)
|
reviewer: str | None = Field(default=None, max_length=100)
|
||||||
status: AgentAssetStatus | None = None
|
status: AgentAssetStatus | None = None
|
||||||
current_version: str | None = Field(default=None, max_length=30)
|
current_version: str | None = Field(default=None, max_length=30)
|
||||||
|
published_version: str | None = Field(default=None, max_length=30)
|
||||||
|
working_version: str | None = Field(default=None, max_length=30)
|
||||||
config_json: dict[str, Any] | None = None
|
config_json: dict[str, Any] | None = None
|
||||||
|
|
||||||
|
|
||||||
@@ -74,6 +76,58 @@ class AgentAssetReviewRead(BaseModel):
|
|||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class AgentAssetOnlyOfficeConfigRead(BaseModel):
|
||||||
|
documentServerUrl: str
|
||||||
|
config: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class AgentAssetOnlyOfficeCallbackRead(BaseModel):
|
||||||
|
error: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class AgentAssetOnlyOfficeCallbackWrite(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="allow")
|
||||||
|
|
||||||
|
status: int = Field(description="ONLYOFFICE 回调状态码。")
|
||||||
|
url: str | None = Field(default=None, description="文档下载地址,状态为 2 或 6 时使用。")
|
||||||
|
users: list[str] = Field(default_factory=list, description="当前编辑用户列表。")
|
||||||
|
|
||||||
|
|
||||||
|
class AgentAssetVersionTimelineItemRead(BaseModel):
|
||||||
|
event_type: str
|
||||||
|
version: str
|
||||||
|
actor: str
|
||||||
|
event_time: datetime
|
||||||
|
title: str
|
||||||
|
description: str = ""
|
||||||
|
note: str | None = None
|
||||||
|
source_version: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class AgentAssetSpreadsheetDiffCellRead(BaseModel):
|
||||||
|
sheet_name: str
|
||||||
|
cell: str
|
||||||
|
change_type: str
|
||||||
|
before_value: Any | None = None
|
||||||
|
after_value: Any | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class AgentAssetSpreadsheetDiffSheetRead(BaseModel):
|
||||||
|
sheet_name: str
|
||||||
|
change_type: str
|
||||||
|
|
||||||
|
|
||||||
|
class AgentAssetVersionCompareRead(BaseModel):
|
||||||
|
base_version: str
|
||||||
|
target_version: str
|
||||||
|
added_sheet_count: int = 0
|
||||||
|
removed_sheet_count: int = 0
|
||||||
|
changed_sheet_count: int = 0
|
||||||
|
changed_cell_count: int = 0
|
||||||
|
sheet_changes: list[AgentAssetSpreadsheetDiffSheetRead] = Field(default_factory=list)
|
||||||
|
cell_changes: list[AgentAssetSpreadsheetDiffCellRead] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
class AgentAssetVersionRead(BaseModel):
|
class AgentAssetVersionRead(BaseModel):
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
@@ -86,6 +140,9 @@ class AgentAssetVersionRead(BaseModel):
|
|||||||
created_by: str
|
created_by: str
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
is_current: bool = False
|
is_current: bool = False
|
||||||
|
is_published: bool = False
|
||||||
|
is_working: bool = False
|
||||||
|
lifecycle_state: str = "history"
|
||||||
|
|
||||||
|
|
||||||
class AgentAssetListItem(BaseModel):
|
class AgentAssetListItem(BaseModel):
|
||||||
@@ -102,6 +159,8 @@ class AgentAssetListItem(BaseModel):
|
|||||||
reviewer: str | None
|
reviewer: str | None
|
||||||
status: str
|
status: str
|
||||||
current_version: str | None
|
current_version: str | None
|
||||||
|
published_version: str | None
|
||||||
|
working_version: str | None
|
||||||
config_json: dict[str, Any]
|
config_json: dict[str, Any]
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from datetime import UTC, date, datetime
|
from datetime import UTC, date, datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import inspect, select, text
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.agent_enums import (
|
from app.core.agent_enums import (
|
||||||
@@ -32,6 +34,13 @@ from app.models.financial_record import (
|
|||||||
ExpenseClaim,
|
ExpenseClaim,
|
||||||
ExpenseClaimItem,
|
ExpenseClaimItem,
|
||||||
)
|
)
|
||||||
|
from app.services.agent_asset_spreadsheet import (
|
||||||
|
AgentAssetSpreadsheetManager,
|
||||||
|
COMPANY_TRAVEL_EXPENSE_RULE_CODE,
|
||||||
|
COMPANY_TRAVEL_EXPENSE_RULE_FILENAME,
|
||||||
|
FINANCE_RULES_LIBRARY,
|
||||||
|
RuleSpreadsheetMeta,
|
||||||
|
)
|
||||||
from app.services.expense_rule_runtime import (
|
from app.services.expense_rule_runtime import (
|
||||||
build_scene_submission_standard_markdown,
|
build_scene_submission_standard_markdown,
|
||||||
build_travel_risk_control_standard_markdown,
|
build_travel_risk_control_standard_markdown,
|
||||||
@@ -78,6 +87,7 @@ LEGACY_RULE_CODES = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
ATTACHMENT_RULE_ASSET_CODE = "rule.expense.attachment_submission_requirements"
|
ATTACHMENT_RULE_ASSET_CODE = "rule.expense.attachment_submission_requirements"
|
||||||
|
COMPANY_TRAVEL_RULE_VERSION = "v1.0.0"
|
||||||
|
|
||||||
ATTACHMENT_RULE_RUNTIME_CONFIG = {
|
ATTACHMENT_RULE_RUNTIME_CONFIG = {
|
||||||
"kind": "policy_rule_draft",
|
"kind": "policy_rule_draft",
|
||||||
@@ -159,6 +169,7 @@ class AgentFoundationService:
|
|||||||
def ensure_foundation_ready(self) -> None:
|
def ensure_foundation_ready(self) -> None:
|
||||||
try:
|
try:
|
||||||
Base.metadata.create_all(bind=self.db.get_bind())
|
Base.metadata.create_all(bind=self.db.get_bind())
|
||||||
|
self._ensure_agent_asset_schema()
|
||||||
self._seed_agent_assets()
|
self._seed_agent_assets()
|
||||||
self._sync_demo_financial_records()
|
self._sync_demo_financial_records()
|
||||||
self._seed_runs_and_logs()
|
self._seed_runs_and_logs()
|
||||||
@@ -191,6 +202,8 @@ class AgentFoundationService:
|
|||||||
reviewer="高嘉禾",
|
reviewer="高嘉禾",
|
||||||
status=AgentAssetStatus.REVIEW.value,
|
status=AgentAssetStatus.REVIEW.value,
|
||||||
current_version="v1.0.0",
|
current_version="v1.0.0",
|
||||||
|
published_version=None,
|
||||||
|
working_version="v1.0.0",
|
||||||
config_json={
|
config_json={
|
||||||
"severity": "high",
|
"severity": "high",
|
||||||
"enabled": False,
|
"enabled": False,
|
||||||
@@ -211,6 +224,8 @@ class AgentFoundationService:
|
|||||||
reviewer="顾承宇",
|
reviewer="顾承宇",
|
||||||
status=AgentAssetStatus.ACTIVE.value,
|
status=AgentAssetStatus.ACTIVE.value,
|
||||||
current_version="v1.0.0",
|
current_version="v1.0.0",
|
||||||
|
published_version="v1.0.0",
|
||||||
|
working_version="v1.0.0",
|
||||||
config_json={
|
config_json={
|
||||||
"severity": "high",
|
"severity": "high",
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
@@ -229,6 +244,8 @@ class AgentFoundationService:
|
|||||||
reviewer="顾承宇",
|
reviewer="顾承宇",
|
||||||
status=AgentAssetStatus.ACTIVE.value,
|
status=AgentAssetStatus.ACTIVE.value,
|
||||||
current_version="v1.1.0",
|
current_version="v1.1.0",
|
||||||
|
published_version="v1.1.0",
|
||||||
|
working_version="v1.1.0",
|
||||||
config_json={
|
config_json={
|
||||||
"severity": "high",
|
"severity": "high",
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
@@ -240,6 +257,28 @@ class AgentFoundationService:
|
|||||||
"rule_template_label": "差旅标准模板",
|
"rule_template_label": "差旅标准模板",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
company_travel_rule = AgentAsset(
|
||||||
|
asset_type=AgentAssetType.RULE.value,
|
||||||
|
code=COMPANY_TRAVEL_EXPENSE_RULE_CODE,
|
||||||
|
name="公司差旅费报销规则",
|
||||||
|
description="通过 Excel 明细表维护差旅费报销标准、票据要求和审批口径。",
|
||||||
|
domain=AgentAssetDomain.EXPENSE.value,
|
||||||
|
scenario_json=["expense", "travel_policy", "travel_standard"],
|
||||||
|
owner="财务制度管理组",
|
||||||
|
reviewer="顾承宇",
|
||||||
|
status=AgentAssetStatus.ACTIVE.value,
|
||||||
|
current_version=COMPANY_TRAVEL_RULE_VERSION,
|
||||||
|
published_version=COMPANY_TRAVEL_RULE_VERSION,
|
||||||
|
working_version=COMPANY_TRAVEL_RULE_VERSION,
|
||||||
|
config_json={
|
||||||
|
"severity": "medium",
|
||||||
|
"enabled": True,
|
||||||
|
"tag": "财务规则",
|
||||||
|
"detail_mode": "spreadsheet",
|
||||||
|
"rule_library": FINANCE_RULES_LIBRARY,
|
||||||
|
"rule_template_label": "差旅报销 Excel 模板",
|
||||||
|
},
|
||||||
|
)
|
||||||
skill_expense_asset = AgentAsset(
|
skill_expense_asset = AgentAsset(
|
||||||
asset_type=AgentAssetType.SKILL.value,
|
asset_type=AgentAssetType.SKILL.value,
|
||||||
code="skill.expense.summary_lookup",
|
code="skill.expense.summary_lookup",
|
||||||
@@ -251,6 +290,8 @@ class AgentFoundationService:
|
|||||||
reviewer="陈硕",
|
reviewer="陈硕",
|
||||||
status=AgentAssetStatus.ACTIVE.value,
|
status=AgentAssetStatus.ACTIVE.value,
|
||||||
current_version="v1.0.0",
|
current_version="v1.0.0",
|
||||||
|
published_version="v1.0.0",
|
||||||
|
working_version="v1.0.0",
|
||||||
config_json={"input_schema": ["time_range", "employee", "department"]},
|
config_json={"input_schema": ["time_range", "employee", "department"]},
|
||||||
)
|
)
|
||||||
skill_ar_asset = AgentAsset(
|
skill_ar_asset = AgentAsset(
|
||||||
@@ -264,6 +305,8 @@ class AgentFoundationService:
|
|||||||
reviewer="陈硕",
|
reviewer="陈硕",
|
||||||
status=AgentAssetStatus.ACTIVE.value,
|
status=AgentAssetStatus.ACTIVE.value,
|
||||||
current_version="v1.0.0",
|
current_version="v1.0.0",
|
||||||
|
published_version="v1.0.0",
|
||||||
|
working_version="v1.0.0",
|
||||||
config_json={"input_schema": ["customer", "aging_bucket", "status"]},
|
config_json={"input_schema": ["customer", "aging_bucket", "status"]},
|
||||||
)
|
)
|
||||||
invoice_mcp_asset = AgentAsset(
|
invoice_mcp_asset = AgentAsset(
|
||||||
@@ -277,6 +320,8 @@ class AgentFoundationService:
|
|||||||
reviewer="周悦宁",
|
reviewer="周悦宁",
|
||||||
status=AgentAssetStatus.ACTIVE.value,
|
status=AgentAssetStatus.ACTIVE.value,
|
||||||
current_version="v1.0.0",
|
current_version="v1.0.0",
|
||||||
|
published_version="v1.0.0",
|
||||||
|
working_version="v1.0.0",
|
||||||
config_json={"endpoint": "mock://invoice/verify", "timeout_ms": 1200},
|
config_json={"endpoint": "mock://invoice/verify", "timeout_ms": 1200},
|
||||||
)
|
)
|
||||||
ledger_mcp_asset = AgentAsset(
|
ledger_mcp_asset = AgentAsset(
|
||||||
@@ -290,6 +335,8 @@ class AgentFoundationService:
|
|||||||
reviewer="周悦宁",
|
reviewer="周悦宁",
|
||||||
status=AgentAssetStatus.ACTIVE.value,
|
status=AgentAssetStatus.ACTIVE.value,
|
||||||
current_version="v1.0.0",
|
current_version="v1.0.0",
|
||||||
|
published_version="v1.0.0",
|
||||||
|
working_version="v1.0.0",
|
||||||
config_json={"endpoint": "mock://ledger/snapshot", "timeout_ms": 1500},
|
config_json={"endpoint": "mock://ledger/snapshot", "timeout_ms": 1500},
|
||||||
)
|
)
|
||||||
task_asset = AgentAsset(
|
task_asset = AgentAsset(
|
||||||
@@ -303,6 +350,8 @@ class AgentFoundationService:
|
|||||||
reviewer="顾承宇",
|
reviewer="顾承宇",
|
||||||
status=AgentAssetStatus.ACTIVE.value,
|
status=AgentAssetStatus.ACTIVE.value,
|
||||||
current_version="v1.0.0",
|
current_version="v1.0.0",
|
||||||
|
published_version="v1.0.0",
|
||||||
|
working_version="v1.0.0",
|
||||||
config_json={"cron": "0 9 * * *", "agent": AgentName.HERMES.value},
|
config_json={"cron": "0 9 * * *", "agent": AgentName.HERMES.value},
|
||||||
)
|
)
|
||||||
ar_summary_task = AgentAsset(
|
ar_summary_task = AgentAsset(
|
||||||
@@ -316,6 +365,8 @@ class AgentFoundationService:
|
|||||||
reviewer="顾承宇",
|
reviewer="顾承宇",
|
||||||
status=AgentAssetStatus.ACTIVE.value,
|
status=AgentAssetStatus.ACTIVE.value,
|
||||||
current_version="v1.0.0",
|
current_version="v1.0.0",
|
||||||
|
published_version="v1.0.0",
|
||||||
|
working_version="v1.0.0",
|
||||||
config_json={"cron": "0 10 * * 1", "agent": AgentName.HERMES.value},
|
config_json={"cron": "0 10 * * 1", "agent": AgentName.HERMES.value},
|
||||||
)
|
)
|
||||||
rule_digest_task = AgentAsset(
|
rule_digest_task = AgentAsset(
|
||||||
@@ -329,6 +380,8 @@ class AgentFoundationService:
|
|||||||
reviewer="顾承宇",
|
reviewer="顾承宇",
|
||||||
status=AgentAssetStatus.ACTIVE.value,
|
status=AgentAssetStatus.ACTIVE.value,
|
||||||
current_version="v1.0.0",
|
current_version="v1.0.0",
|
||||||
|
published_version="v1.0.0",
|
||||||
|
working_version="v1.0.0",
|
||||||
config_json={"cron": "0 18 * * *", "agent": AgentName.HERMES.value},
|
config_json={"cron": "0 18 * * *", "agent": AgentName.HERMES.value},
|
||||||
)
|
)
|
||||||
knowledge_index_task = AgentAsset(
|
knowledge_index_task = AgentAsset(
|
||||||
@@ -342,6 +395,8 @@ class AgentFoundationService:
|
|||||||
reviewer="顾承宇",
|
reviewer="顾承宇",
|
||||||
status=AgentAssetStatus.ACTIVE.value,
|
status=AgentAssetStatus.ACTIVE.value,
|
||||||
current_version="v1.0.0",
|
current_version="v1.0.0",
|
||||||
|
published_version="v1.0.0",
|
||||||
|
working_version="v1.0.0",
|
||||||
config_json={"cron": "0 0 * * *", "agent": AgentName.HERMES.value},
|
config_json={"cron": "0 0 * * *", "agent": AgentName.HERMES.value},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -350,6 +405,7 @@ class AgentFoundationService:
|
|||||||
attachment_rule,
|
attachment_rule,
|
||||||
scene_submission_rule,
|
scene_submission_rule,
|
||||||
travel_policy_rule,
|
travel_policy_rule,
|
||||||
|
company_travel_rule,
|
||||||
skill_expense_asset,
|
skill_expense_asset,
|
||||||
skill_ar_asset,
|
skill_ar_asset,
|
||||||
invoice_mcp_asset,
|
invoice_mcp_asset,
|
||||||
@@ -362,6 +418,12 @@ class AgentFoundationService:
|
|||||||
)
|
)
|
||||||
self.db.flush()
|
self.db.flush()
|
||||||
|
|
||||||
|
company_travel_rule_meta = self._ensure_company_travel_rule_spreadsheet_seed(
|
||||||
|
company_travel_rule,
|
||||||
|
version=COMPANY_TRAVEL_RULE_VERSION,
|
||||||
|
actor_name="系统初始化",
|
||||||
|
)
|
||||||
|
|
||||||
self.db.add_all(
|
self.db.add_all(
|
||||||
[
|
[
|
||||||
AgentAssetVersion(
|
AgentAssetVersion(
|
||||||
@@ -410,6 +472,18 @@ class AgentFoundationService:
|
|||||||
change_note="补充可执行规则块,供审核引擎直接消费差旅制度标准。",
|
change_note="补充可执行规则块,供审核引擎直接消费差旅制度标准。",
|
||||||
created_by="系统初始化",
|
created_by="系统初始化",
|
||||||
),
|
),
|
||||||
|
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(
|
AgentAssetVersion(
|
||||||
asset=skill_expense_asset,
|
asset=skill_expense_asset,
|
||||||
version="v1.0.0",
|
version="v1.0.0",
|
||||||
@@ -553,6 +627,14 @@ class AgentFoundationService:
|
|||||||
review_note="制度口径已确认,并已补充可执行配置供审核引擎读取。",
|
review_note="制度口径已确认,并已补充可执行配置供审核引擎读取。",
|
||||||
reviewed_at=datetime.now(UTC),
|
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),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -916,6 +998,9 @@ class AgentFoundationService:
|
|||||||
travel_policy_rule = self.db.scalar(
|
travel_policy_rule = self.db.scalar(
|
||||||
select(AgentAsset).where(AgentAsset.code == "rule.expense.travel_risk_control_standard")
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
if ATTACHMENT_RULE_ASSET_CODE not in existing_codes:
|
if ATTACHMENT_RULE_ASSET_CODE not in existing_codes:
|
||||||
attachment_rule = self._create_seed_asset(
|
attachment_rule = self._create_seed_asset(
|
||||||
@@ -940,8 +1025,11 @@ class AgentFoundationService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if attachment_rule is not None:
|
if attachment_rule is not None:
|
||||||
attachment_rule.current_version = "v1.0.0"
|
if not str(attachment_rule.current_version or "").strip():
|
||||||
attachment_rule.status = AgentAssetStatus.REVIEW.value
|
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
|
||||||
attachment_rule.description = "统一定义报销提交时的附件数量、票据类型和补件处理口径,作为上线前待审核规则。"
|
attachment_rule.description = "统一定义报销提交时的附件数量、票据类型和补件处理口径,作为上线前待审核规则。"
|
||||||
attachment_rule.config_json = {
|
attachment_rule.config_json = {
|
||||||
"severity": "high",
|
"severity": "high",
|
||||||
@@ -1003,8 +1091,13 @@ class AgentFoundationService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if scene_submission_rule is not None:
|
if scene_submission_rule is not None:
|
||||||
scene_submission_rule.current_version = "v1.0.0"
|
if not str(scene_submission_rule.current_version or "").strip():
|
||||||
scene_submission_rule.status = AgentAssetStatus.ACTIVE.value
|
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
|
||||||
scene_submission_rule.description = "统一定义各报销场景的必填字段、附件类型要求和金额阈值。"
|
scene_submission_rule.description = "统一定义各报销场景的必填字段、附件类型要求和金额阈值。"
|
||||||
scene_submission_rule.config_json = {
|
scene_submission_rule.config_json = {
|
||||||
"severity": "high",
|
"severity": "high",
|
||||||
@@ -1054,8 +1147,13 @@ class AgentFoundationService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if travel_policy_rule is not None:
|
if travel_policy_rule is not None:
|
||||||
travel_policy_rule.current_version = "v1.1.0"
|
if not str(travel_policy_rule.current_version or "").strip():
|
||||||
travel_policy_rule.status = AgentAssetStatus.ACTIVE.value
|
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
|
||||||
travel_policy_rule.config_json = {
|
travel_policy_rule.config_json = {
|
||||||
"severity": "high",
|
"severity": "high",
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
@@ -1091,6 +1189,73 @@ class AgentFoundationService:
|
|||||||
reviewed_at=datetime.now(UTC),
|
reviewed_at=datetime.now(UTC),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if COMPANY_TRAVEL_EXPENSE_RULE_CODE not in existing_codes:
|
||||||
|
company_travel_rule = self._create_seed_asset(
|
||||||
|
asset_type=AgentAssetType.RULE.value,
|
||||||
|
code=COMPANY_TRAVEL_EXPENSE_RULE_CODE,
|
||||||
|
name="公司差旅费报销规则",
|
||||||
|
description="通过 Excel 明细表维护差旅费报销标准、票据要求和审批口径。",
|
||||||
|
domain=AgentAssetDomain.EXPENSE.value,
|
||||||
|
scenario_json=["expense", "travel_policy", "travel_standard"],
|
||||||
|
owner="财务制度管理组",
|
||||||
|
reviewer="顾承宇",
|
||||||
|
status=AgentAssetStatus.ACTIVE.value,
|
||||||
|
current_version=COMPANY_TRAVEL_RULE_VERSION,
|
||||||
|
config_json={
|
||||||
|
"severity": "medium",
|
||||||
|
"enabled": True,
|
||||||
|
"tag": "财务规则",
|
||||||
|
"detail_mode": "spreadsheet",
|
||||||
|
"rule_template_label": "差旅报销 Excel 模板",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if company_travel_rule is not None:
|
||||||
|
if not str(company_travel_rule.current_version or "").strip():
|
||||||
|
company_travel_rule.current_version = COMPANY_TRAVEL_RULE_VERSION
|
||||||
|
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,
|
||||||
|
"tag": "财务规则",
|
||||||
|
"detail_mode": "spreadsheet",
|
||||||
|
"rule_library": FINANCE_RULES_LIBRARY,
|
||||||
|
"rule_template_label": "差旅报销 Excel 模板",
|
||||||
|
}
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
|
||||||
if "skill.ar.aging_summary" not in existing_codes:
|
if "skill.ar.aging_summary" not in existing_codes:
|
||||||
asset = self._create_seed_asset(
|
asset = self._create_seed_asset(
|
||||||
asset_type=AgentAssetType.SKILL.value,
|
asset_type=AgentAssetType.SKILL.value,
|
||||||
@@ -1238,6 +1403,104 @@ class AgentFoundationService:
|
|||||||
created_by="系统初始化",
|
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():
|
||||||
|
metadata = RuleSpreadsheetMeta(
|
||||||
|
file_name=str(existing_document.get("file_name") or COMPANY_TRAVEL_EXPENSE_RULE_FILENAME),
|
||||||
|
storage_key=storage_key,
|
||||||
|
mime_type=str(existing_document.get("mime_type") or "").strip()
|
||||||
|
or "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
size_bytes=int(existing_document.get("size_bytes") or existing_path.stat().st_size),
|
||||||
|
checksum=hashlib.sha256(existing_path.read_bytes()).hexdigest(),
|
||||||
|
updated_at=str(existing_document.get("updated_at") or "").strip()
|
||||||
|
or datetime.now(UTC).isoformat(),
|
||||||
|
updated_by=str(existing_document.get("updated_by") or actor_name).strip()
|
||||||
|
or actor_name,
|
||||||
|
source=str(existing_document.get("source") or "seed").strip() or "seed",
|
||||||
|
)
|
||||||
|
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 metadata
|
||||||
|
|
||||||
|
live_content = manager.resolve_storage_path(live_document.storage_key).read_bytes()
|
||||||
|
metadata = manager.store_spreadsheet(
|
||||||
|
asset_id=asset.id,
|
||||||
|
version=version,
|
||||||
|
file_name=COMPANY_TRAVEL_EXPENSE_RULE_FILENAME,
|
||||||
|
content=live_content,
|
||||||
|
actor_name=actor_name,
|
||||||
|
source="seed",
|
||||||
|
)
|
||||||
|
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 metadata
|
||||||
|
|
||||||
|
@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 _create_seed_asset(
|
def _create_seed_asset(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -1264,6 +1527,8 @@ class AgentFoundationService:
|
|||||||
reviewer=reviewer,
|
reviewer=reviewer,
|
||||||
status=status,
|
status=status,
|
||||||
current_version=current_version,
|
current_version=current_version,
|
||||||
|
published_version=current_version if status == AgentAssetStatus.ACTIVE.value else None,
|
||||||
|
working_version=current_version,
|
||||||
config_json=config_json,
|
config_json=config_json,
|
||||||
)
|
)
|
||||||
self.db.add(asset)
|
self.db.add(asset)
|
||||||
@@ -1348,6 +1613,36 @@ class AgentFoundationService:
|
|||||||
for log in obsolete_logs:
|
for log in obsolete_logs:
|
||||||
self.db.delete(log)
|
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()
|
||||||
|
|
||||||
def _attachment_submission_requirement_markdown(
|
def _attachment_submission_requirement_markdown(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ function buildQuery(params = {}) {
|
|||||||
search.set('limit', String(params.limit))
|
search.set('limit', String(params.limit))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (params.version) {
|
||||||
|
search.set('version', String(params.version))
|
||||||
|
}
|
||||||
|
|
||||||
if (params.agent) {
|
if (params.agent) {
|
||||||
search.set('agent', params.agent)
|
search.set('agent', params.agent)
|
||||||
}
|
}
|
||||||
@@ -80,10 +84,66 @@ export function fetchAgentAssetDetail(assetId) {
|
|||||||
return apiRequest(`/agent-assets/${assetId}`)
|
return apiRequest(`/agent-assets/${assetId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchAgentAssetSpreadsheetOnlyOfficeConfig(assetId, version = '') {
|
||||||
|
const query = buildQuery({ version })
|
||||||
|
return apiRequest(`/agent-assets/${assetId}/spreadsheet/onlyoffice-config${query}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchAgentAssetSpreadsheetBlob(assetId, version = '', disposition = 'inline') {
|
||||||
|
const search = new URLSearchParams()
|
||||||
|
if (version) {
|
||||||
|
search.set('version', String(version).trim())
|
||||||
|
}
|
||||||
|
if (disposition) {
|
||||||
|
search.set('disposition', String(disposition).trim())
|
||||||
|
}
|
||||||
|
const query = search.toString()
|
||||||
|
return apiRequest(`/agent-assets/${assetId}/spreadsheet/content${query ? `?${query}` : ''}`, {
|
||||||
|
responseType: 'blob',
|
||||||
|
contentType: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadAgentAssetSpreadsheet(assetId, file, options = {}) {
|
||||||
|
return apiRequest(
|
||||||
|
`/agent-assets/${assetId}/spreadsheet/upload?filename=${encodeURIComponent(file.name)}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: file,
|
||||||
|
contentType: file.type || 'application/octet-stream',
|
||||||
|
headers: buildWriteHeaders(options)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function importAgentAssetSpreadsheetContent(assetId, file, options = {}) {
|
||||||
|
return apiRequest(
|
||||||
|
`/agent-assets/${assetId}/spreadsheet/import-content?filename=${encodeURIComponent(file.name)}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: file,
|
||||||
|
contentType: file.type || 'application/octet-stream',
|
||||||
|
headers: buildWriteHeaders(options)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchAgentAssetVersions(assetId, limit = 5) {
|
export function fetchAgentAssetVersions(assetId, limit = 5) {
|
||||||
return apiRequest(`/agent-assets/${assetId}/versions${buildQuery({ limit })}`)
|
return apiRequest(`/agent-assets/${assetId}/versions${buildQuery({ limit })}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchAgentAssetVersionTimeline(assetId) {
|
||||||
|
return apiRequest(`/agent-assets/${assetId}/version-timeline`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compareAgentAssetSpreadsheetVersions(assetId, baseVersion, targetVersion) {
|
||||||
|
const query = new URLSearchParams({
|
||||||
|
base_version: String(baseVersion || '').trim(),
|
||||||
|
target_version: String(targetVersion || '').trim()
|
||||||
|
})
|
||||||
|
return apiRequest(`/agent-assets/${assetId}/versions/compare?${query.toString()}`)
|
||||||
|
}
|
||||||
|
|
||||||
export function updateAgentAsset(assetId, payload, options = {}) {
|
export function updateAgentAsset(assetId, payload, options = {}) {
|
||||||
return apiRequest(`/agent-assets/${assetId}`, {
|
return apiRequest(`/agent-assets/${assetId}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
@@ -115,6 +175,13 @@ export function activateAgentAsset(assetId, options = {}) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function restoreAgentAssetVersion(assetId, version, options = {}) {
|
||||||
|
return apiRequest(`/agent-assets/${assetId}/versions/${encodeURIComponent(version)}/restore`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: buildWriteHeaders(options)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchAgentRuns(params = {}) {
|
export function fetchAgentRuns(params = {}) {
|
||||||
return apiRequest(`/agent-runs${buildQuery(params)}`)
|
return apiRequest(`/agent-runs${buildQuery(params)}`)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user