feat: 添加风险规则及 agent assets 功能增强
This commit is contained in:
@@ -27,7 +27,6 @@ from app.schemas.agent_asset import (
|
||||
AgentAssetRuleJsonWrite,
|
||||
AgentAssetSpreadsheetChangeRecordRead,
|
||||
AgentAssetUpdate,
|
||||
AgentAssetVersionCompareRead,
|
||||
AgentAssetVersionCreate,
|
||||
AgentAssetVersionRead,
|
||||
AgentAssetVersionTimelineItemRead,
|
||||
@@ -167,7 +166,7 @@ def get_agent_asset_spreadsheet_onlyoffice_config(
|
||||
db: DbSession,
|
||||
version: Annotated[
|
||||
str | None,
|
||||
Query(description="可选的规则版本号;不传时默认当前版本。"),
|
||||
Query(description="兼容旧前端的可选参数;表格规则始终打开当前规则表。"),
|
||||
] = None,
|
||||
) -> AgentAssetOnlyOfficeConfigRead:
|
||||
try:
|
||||
@@ -184,7 +183,7 @@ def get_agent_asset_spreadsheet_onlyoffice_config(
|
||||
"/{asset_id}/spreadsheet/content",
|
||||
response_class=FileResponse,
|
||||
summary="下载或预览规则 Excel 文件",
|
||||
description="按版本返回规则的 Excel 快照,用于浏览器预览或下载。",
|
||||
description="返回当前规则 Excel 文件,用于浏览器预览或下载。",
|
||||
)
|
||||
def get_agent_asset_spreadsheet_content(
|
||||
asset_id: str,
|
||||
@@ -192,7 +191,7 @@ def get_agent_asset_spreadsheet_content(
|
||||
db: DbSession,
|
||||
version: Annotated[
|
||||
str | None,
|
||||
Query(description="可选的规则版本号;不传时默认当前版本。"),
|
||||
Query(description="兼容旧前端的可选参数;不传时返回当前规则表。"),
|
||||
] = None,
|
||||
) -> FileResponse:
|
||||
try:
|
||||
@@ -215,18 +214,18 @@ def get_agent_asset_spreadsheet_content(
|
||||
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 临时访问令牌。"),
|
||||
],
|
||||
version: Annotated[
|
||||
str | None,
|
||||
Query(description="兼容旧 ONLYOFFICE URL;当前表格模式不再使用。"),
|
||||
] = None,
|
||||
) -> FileResponse:
|
||||
try:
|
||||
service = AgentAssetService(db)
|
||||
service.validate_rule_spreadsheet_access_token(asset_id, version, access_token)
|
||||
service.validate_rule_spreadsheet_access_token(asset_id, access_token)
|
||||
file_path, media_type, filename = service.get_rule_spreadsheet_content(
|
||||
asset_id,
|
||||
version=version,
|
||||
@@ -246,7 +245,7 @@ def get_agent_asset_spreadsheet_onlyoffice_content(
|
||||
response_model=AgentAssetRead,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="上传规则 Excel 文件",
|
||||
description="为指定规则上传新的 Excel 快照,并自动生成新规则版本。",
|
||||
description="为指定规则上传新的 Excel 文件,并记录本次表格修改。",
|
||||
)
|
||||
def upload_agent_asset_spreadsheet(
|
||||
asset_id: str,
|
||||
@@ -311,16 +310,16 @@ def import_agent_asset_spreadsheet_content(
|
||||
"/{asset_id}/spreadsheet/onlyoffice/callback",
|
||||
response_model=AgentAssetOnlyOfficeCallbackRead,
|
||||
summary="接收规则 Excel 的 ONLYOFFICE 回调",
|
||||
description="接收 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="打开编辑器时对应的规则版本号。"),
|
||||
],
|
||||
str | None,
|
||||
Query(description="兼容旧 ONLYOFFICE 回调;当前表格模式不再使用。"),
|
||||
] = None,
|
||||
actor_name: Annotated[
|
||||
str | None,
|
||||
Query(description="发起编辑的用户显示名。"),
|
||||
@@ -601,25 +600,3 @@ def get_agent_asset_version_timeline(
|
||||
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)
|
||||
|
||||
@@ -133,22 +133,10 @@ class AgentAssetSpreadsheetDiffSheetRead(BaseModel):
|
||||
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 AgentAssetSpreadsheetChangeRecordRead(BaseModel):
|
||||
id: str
|
||||
actor: str
|
||||
changed_at: datetime
|
||||
version: str | None = None
|
||||
summary: str
|
||||
sheet_changes: list[AgentAssetSpreadsheetDiffSheetRead] = Field(default_factory=list)
|
||||
cell_changes: list[AgentAssetSpreadsheetDiffCellRead] = Field(default_factory=list)
|
||||
|
||||
@@ -36,7 +36,6 @@ from app.schemas.agent_asset import (
|
||||
AgentAssetSpreadsheetDiffCellRead,
|
||||
AgentAssetSpreadsheetDiffSheetRead,
|
||||
AgentAssetUpdate,
|
||||
AgentAssetVersionCompareRead,
|
||||
AgentAssetVersionCreate,
|
||||
AgentAssetVersionRead,
|
||||
AgentAssetVersionTimelineItemRead,
|
||||
@@ -511,18 +510,16 @@ class AgentAssetService:
|
||||
return self._build_onlyoffice_spreadsheet_config(
|
||||
asset_id=asset_id,
|
||||
current_user=current_user,
|
||||
resolved_version=resolved_version,
|
||||
metadata=metadata,
|
||||
editable=resolved_version == PREVIEW_RULE_CURRENT_VERSION,
|
||||
)
|
||||
|
||||
asset = self._require_spreadsheet_rule(asset_id)
|
||||
resolved_version, metadata = self._resolve_current_spreadsheet_meta(asset)
|
||||
_, metadata = self._resolve_current_spreadsheet_meta(asset)
|
||||
editable = self._can_edit_current_spreadsheet(current_user)
|
||||
return self._build_onlyoffice_spreadsheet_config(
|
||||
asset_id=asset.id,
|
||||
current_user=current_user,
|
||||
resolved_version=resolved_version,
|
||||
metadata=metadata,
|
||||
editable=editable,
|
||||
)
|
||||
@@ -555,7 +552,6 @@ class AgentAssetService:
|
||||
def validate_rule_spreadsheet_access_token(
|
||||
self,
|
||||
asset_id: str,
|
||||
version: str,
|
||||
access_token: str,
|
||||
) -> None:
|
||||
onlyoffice_settings = resolve_onlyoffice_settings()
|
||||
@@ -571,7 +567,6 @@ class AgentAssetService:
|
||||
if (
|
||||
payload.get("scope") != "agent-asset-spreadsheet"
|
||||
or payload.get("asset_id") != asset_id
|
||||
or payload.get("version") != version
|
||||
):
|
||||
raise ValueError("ONLYOFFICE 文件访问令牌无效。")
|
||||
|
||||
@@ -604,7 +599,6 @@ class AgentAssetService:
|
||||
)
|
||||
changed_sheet_count = self._count_changed_sheets(sheet_changes, cell_changes)
|
||||
changed_cell_count = len(cell_changes)
|
||||
next_version = self._next_available_version(asset)
|
||||
|
||||
metadata = self._store_current_rule_spreadsheet(
|
||||
asset,
|
||||
@@ -613,45 +607,10 @@ class AgentAssetService:
|
||||
actor=actor,
|
||||
source=source,
|
||||
)
|
||||
snapshot_metadata = self.spreadsheet_manager.store_rule_library_spreadsheet_snapshot(
|
||||
library=self._resolve_spreadsheet_rule_library(asset),
|
||||
asset_id=asset.id,
|
||||
version=next_version,
|
||||
file_name=file_name,
|
||||
content=content,
|
||||
actor_name=actor,
|
||||
source=source,
|
||||
)
|
||||
operation_label = (
|
||||
change_note
|
||||
or (
|
||||
"ONLYOFFICE 在线编辑"
|
||||
if source == "onlyoffice"
|
||||
else f"上传并覆盖当前规则表:{normalized_name}"
|
||||
)
|
||||
)
|
||||
summary = self._build_spreadsheet_change_summary(
|
||||
operation_label,
|
||||
sheet_changes,
|
||||
cell_changes,
|
||||
)
|
||||
version_content = self.spreadsheet_manager.build_version_markdown(
|
||||
rule_name=asset.name,
|
||||
version=next_version,
|
||||
metadata=snapshot_metadata,
|
||||
)
|
||||
self.create_version(
|
||||
asset.id,
|
||||
AgentAssetVersionCreate(
|
||||
version=next_version,
|
||||
content=version_content,
|
||||
content_type=AgentAssetContentType.MARKDOWN,
|
||||
change_note=summary,
|
||||
created_by=actor,
|
||||
),
|
||||
actor=actor,
|
||||
request_id=request_id,
|
||||
)
|
||||
self.audit_service.log_action(
|
||||
actor=actor,
|
||||
action="edit_rule_spreadsheet",
|
||||
@@ -660,13 +619,11 @@ class AgentAssetService:
|
||||
before_json={"storage_key": current_metadata.storage_key},
|
||||
after_json={
|
||||
"summary": summary,
|
||||
"version": next_version,
|
||||
"changed_sheet_count": changed_sheet_count,
|
||||
"changed_cell_count": changed_cell_count,
|
||||
"sheet_changes": [item.model_dump() for item in sheet_changes],
|
||||
"cell_changes": [item.model_dump() for item in cell_changes[:500]],
|
||||
"storage_key": metadata.storage_key,
|
||||
"snapshot_storage_key": snapshot_metadata.storage_key,
|
||||
},
|
||||
request_id=request_id,
|
||||
)
|
||||
@@ -705,7 +662,7 @@ class AgentAssetService:
|
||||
self,
|
||||
asset_id: str,
|
||||
*,
|
||||
version: str,
|
||||
version: str | None = None,
|
||||
payload: dict[str, Any],
|
||||
actor_name: str | None = None,
|
||||
) -> None:
|
||||
@@ -721,8 +678,6 @@ class AgentAssetService:
|
||||
callback = self._parse_onlyoffice_callback(payload)
|
||||
if callback.status not in {2, 6} or not callback.download_url:
|
||||
return
|
||||
if str(version or "").strip() not in {"", "current", self._resolve_working_version(asset)}:
|
||||
return
|
||||
|
||||
_, current_metadata = self._resolve_current_spreadsheet_meta(asset)
|
||||
request = Request(
|
||||
@@ -924,44 +879,6 @@ class AgentAssetService:
|
||||
|
||||
return sorted(events, key=lambda item: item.event_time)
|
||||
|
||||
def compare_spreadsheet_versions(
|
||||
self,
|
||||
asset_id: str,
|
||||
*,
|
||||
base_version: str,
|
||||
target_version: str,
|
||||
) -> AgentAssetVersionCompareRead:
|
||||
self._ensure_ready()
|
||||
asset = self._require_spreadsheet_rule(asset_id)
|
||||
resolved_base, base_meta = self._resolve_spreadsheet_version_meta(
|
||||
asset,
|
||||
version=base_version,
|
||||
)
|
||||
resolved_target, target_meta = self._resolve_spreadsheet_version_meta(
|
||||
asset,
|
||||
version=target_version,
|
||||
)
|
||||
|
||||
base_workbook = self._load_spreadsheet_for_compare(base_meta)
|
||||
target_workbook = self._load_spreadsheet_for_compare(target_meta)
|
||||
sheet_changes, cell_changes = self._collect_workbook_changes(
|
||||
base_workbook,
|
||||
target_workbook,
|
||||
)
|
||||
added_sheet_count = sum(1 for item in sheet_changes if item.change_type == "added")
|
||||
removed_sheet_count = sum(1 for item in sheet_changes if item.change_type == "removed")
|
||||
|
||||
return AgentAssetVersionCompareRead(
|
||||
base_version=resolved_base,
|
||||
target_version=resolved_target,
|
||||
added_sheet_count=added_sheet_count,
|
||||
removed_sheet_count=removed_sheet_count,
|
||||
changed_sheet_count=self._count_changed_sheets(sheet_changes, cell_changes),
|
||||
changed_cell_count=len(cell_changes),
|
||||
sheet_changes=sheet_changes,
|
||||
cell_changes=cell_changes[:500],
|
||||
)
|
||||
|
||||
def list_spreadsheet_change_records(
|
||||
self,
|
||||
asset_id: str,
|
||||
@@ -981,8 +898,7 @@ class AgentAssetService:
|
||||
id=log.id,
|
||||
actor=log.actor,
|
||||
changed_at=log.created_at,
|
||||
version=str((log.after_json or {}).get("version") or "").strip() or None,
|
||||
summary=str((log.after_json or {}).get("summary") or "ONLYOFFICE 在线编辑保存。"),
|
||||
summary=str((log.after_json or {}).get("summary") or "表格内容已保存。"),
|
||||
sheet_changes=[
|
||||
AgentAssetSpreadsheetDiffSheetRead.model_validate(item)
|
||||
for item in ((log.after_json or {}).get("sheet_changes") or [])
|
||||
@@ -1292,7 +1208,6 @@ class AgentAssetService:
|
||||
*,
|
||||
asset_id: str,
|
||||
current_user: CurrentUserContext,
|
||||
resolved_version: str,
|
||||
metadata: RuleSpreadsheetMeta,
|
||||
editable: bool,
|
||||
) -> AgentAssetOnlyOfficeConfigRead:
|
||||
@@ -1307,21 +1222,21 @@ class AgentAssetService:
|
||||
|
||||
backend_base_url = onlyoffice_settings.backend_url.rstrip("/")
|
||||
public_url = onlyoffice_settings.public_url.rstrip("/")
|
||||
access_token = self._build_onlyoffice_access_token(asset_id, resolved_version)
|
||||
access_token = self._build_onlyoffice_access_token(asset_id)
|
||||
document_url = (
|
||||
f"{backend_base_url}{settings.api_v1_prefix}/agent-assets/{asset_id}/spreadsheet/onlyoffice/content"
|
||||
f"?version={resolved_version}&access_token={access_token}"
|
||||
f"?access_token={access_token}"
|
||||
)
|
||||
callback_url = (
|
||||
f"{backend_base_url}{settings.api_v1_prefix}/agent-assets/{asset_id}/spreadsheet/onlyoffice/callback"
|
||||
f"?version={resolved_version}&actor_name={quote(current_user.name)}"
|
||||
f"?actor_name={quote(current_user.name)}"
|
||||
)
|
||||
|
||||
config: dict[str, Any] = {
|
||||
"documentType": "cell",
|
||||
"document": {
|
||||
"fileType": Path(metadata.file_name).suffix.lstrip(".").lower() or "xlsx",
|
||||
"key": self._build_onlyoffice_document_key(asset_id, resolved_version, metadata),
|
||||
"key": self._build_onlyoffice_document_key(asset_id, metadata),
|
||||
"title": metadata.file_name,
|
||||
"url": document_url,
|
||||
"permissions": {
|
||||
@@ -1462,19 +1377,6 @@ class AgentAssetService:
|
||||
major, minor, patch = [int(item) for item in parts]
|
||||
return f"v{major}.{minor}.{patch + 1}"
|
||||
|
||||
@staticmethod
|
||||
def _can_edit_spreadsheet_version(
|
||||
asset: AgentAsset,
|
||||
current_user: CurrentUserContext,
|
||||
version: str,
|
||||
) -> bool:
|
||||
role_codes = {str(item).strip() for item in current_user.role_codes}
|
||||
can_edit = current_user.is_admin or "manager" in role_codes or "finance" in role_codes
|
||||
return (
|
||||
can_edit
|
||||
and AgentAssetService._resolve_working_version(asset) == str(version or "").strip()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _can_edit_current_spreadsheet(current_user: CurrentUserContext) -> bool:
|
||||
role_codes = {str(item).strip() for item in current_user.role_codes}
|
||||
@@ -1483,23 +1385,21 @@ class AgentAssetService:
|
||||
@staticmethod
|
||||
def _build_onlyoffice_document_key(
|
||||
asset_id: str,
|
||||
version: str,
|
||||
metadata: RuleSpreadsheetMeta,
|
||||
) -> str:
|
||||
fingerprint = metadata.checksum or metadata.updated_at or metadata.file_name
|
||||
raw_key = f"{asset_id}-{version}-{fingerprint}"
|
||||
raw_key = f"{asset_id}-{fingerprint}"
|
||||
return "".join(
|
||||
character if character.isalnum() or character in {"-", "_", ".", "="} else "_"
|
||||
for character in raw_key
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _build_onlyoffice_access_token(asset_id: str, version: str) -> str:
|
||||
def _build_onlyoffice_access_token(asset_id: str) -> str:
|
||||
onlyoffice_settings = resolve_onlyoffice_settings()
|
||||
payload = {
|
||||
"scope": "agent-asset-spreadsheet",
|
||||
"asset_id": asset_id,
|
||||
"version": version,
|
||||
}
|
||||
return jwt.encode(payload, onlyoffice_settings.jwt_secret, algorithm="HS256")
|
||||
|
||||
@@ -1646,7 +1546,6 @@ class AgentAssetService:
|
||||
|
||||
@staticmethod
|
||||
def _build_spreadsheet_change_summary(
|
||||
operation_label: str,
|
||||
sheet_changes: list[AgentAssetSpreadsheetDiffSheetRead],
|
||||
cell_changes: list[AgentAssetSpreadsheetDiffCellRead],
|
||||
) -> str:
|
||||
@@ -1655,15 +1554,15 @@ class AgentAssetService:
|
||||
| {item.sheet_name for item in cell_changes}
|
||||
)
|
||||
if not sheet_names:
|
||||
return f"{operation_label}:文件内容已保存,未发现单元格级差异。"
|
||||
return "文件内容已保存,未发现单元格级差异。"
|
||||
|
||||
preview = "、".join(sheet_names[:3])
|
||||
if len(sheet_names) > 3:
|
||||
preview = f"{preview} 等"
|
||||
sheet_text = f"涉及 {len(sheet_names)} 个工作表({preview})"
|
||||
if cell_changes:
|
||||
return f"{operation_label}:{sheet_text},共 {len(cell_changes)} 处单元格改动。"
|
||||
return f"{operation_label}:{sheet_text},工作表结构发生变化。"
|
||||
return f"{sheet_text},共 {len(cell_changes)} 处单元格改动。"
|
||||
return f"{sheet_text},工作表结构发生变化。"
|
||||
|
||||
def _next_available_version(self, asset: AgentAsset) -> str:
|
||||
candidate = self._increment_version(self._resolve_working_version(asset))
|
||||
|
||||
@@ -1,70 +1,70 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.logging import get_logger
|
||||
from app.models.audit_log import AuditLog
|
||||
from app.repositories.audit_log import AuditLogRepository
|
||||
from app.schemas.audit_log import AuditLogRead
|
||||
from app.services.agent_foundation import AgentFoundationService
|
||||
|
||||
logger = get_logger("app.services.audit")
|
||||
|
||||
|
||||
class AuditLogService:
|
||||
def __init__(self, db: Session) -> None:
|
||||
self.db = db
|
||||
self.repository = AuditLogRepository(db)
|
||||
|
||||
def list_logs(
|
||||
self,
|
||||
*,
|
||||
resource_type: str | None = None,
|
||||
resource_id: str | None = None,
|
||||
action: str | None = None,
|
||||
limit: int = 50,
|
||||
) -> list[AuditLogRead]:
|
||||
self._ensure_ready()
|
||||
items = self.repository.list(
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
action=action,
|
||||
limit=limit,
|
||||
)
|
||||
return [AuditLogRead.model_validate(item) for item in items]
|
||||
|
||||
def log_action(
|
||||
self,
|
||||
*,
|
||||
actor: str,
|
||||
action: str,
|
||||
resource_type: str,
|
||||
resource_id: str,
|
||||
before_json: dict[str, Any] | None = None,
|
||||
after_json: dict[str, Any] | None = None,
|
||||
request_id: str | None = None,
|
||||
) -> AuditLog:
|
||||
log = AuditLog(
|
||||
actor=actor,
|
||||
action=action,
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
before_json=before_json,
|
||||
after_json=after_json,
|
||||
request_id=request_id or uuid.uuid4().hex,
|
||||
)
|
||||
created = self.repository.create(log)
|
||||
logger.info(
|
||||
"Created audit log id=%s action=%s resource=%s:%s",
|
||||
created.id,
|
||||
created.action,
|
||||
created.resource_type,
|
||||
created.resource_id,
|
||||
)
|
||||
return created
|
||||
|
||||
def _ensure_ready(self) -> None:
|
||||
AgentFoundationService(self.db).ensure_foundation_ready()
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.logging import get_logger
|
||||
from app.models.audit_log import AuditLog
|
||||
from app.repositories.audit_log import AuditLogRepository
|
||||
from app.schemas.audit_log import AuditLogRead
|
||||
from app.services.agent_foundation import AgentFoundationService
|
||||
|
||||
logger = get_logger("app.services.audit")
|
||||
|
||||
|
||||
class AuditLogService:
|
||||
def __init__(self, db: Session) -> None:
|
||||
self.db = db
|
||||
self.repository = AuditLogRepository(db)
|
||||
|
||||
def list_logs(
|
||||
self,
|
||||
*,
|
||||
resource_type: str | None = None,
|
||||
resource_id: str | None = None,
|
||||
action: str | None = None,
|
||||
limit: int = 50,
|
||||
) -> list[AuditLogRead]:
|
||||
self._ensure_ready()
|
||||
items = self.repository.list(
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
action=action,
|
||||
limit=limit,
|
||||
)
|
||||
return [AuditLogRead.model_validate(item) for item in items]
|
||||
|
||||
def log_action(
|
||||
self,
|
||||
*,
|
||||
actor: str,
|
||||
action: str,
|
||||
resource_type: str,
|
||||
resource_id: str,
|
||||
before_json: dict[str, Any] | None = None,
|
||||
after_json: dict[str, Any] | None = None,
|
||||
request_id: str | None = None,
|
||||
) -> AuditLog:
|
||||
log = AuditLog(
|
||||
actor=actor,
|
||||
action=action,
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
before_json=before_json,
|
||||
after_json=after_json,
|
||||
request_id=request_id or uuid.uuid4().hex,
|
||||
)
|
||||
created = self.repository.create(log)
|
||||
logger.info(
|
||||
"Created audit log id=%s action=%s resource=%s:%s",
|
||||
created.id,
|
||||
created.action,
|
||||
created.resource_type,
|
||||
created.resource_id,
|
||||
)
|
||||
return created
|
||||
|
||||
def _ensure_ready(self) -> None:
|
||||
AgentFoundationService(self.db).ensure_foundation_ready()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -177,16 +177,16 @@ SLOT_LABELS = {
|
||||
}
|
||||
|
||||
DATE_TEXT_PATTERN = re.compile(r"(\d{4}[年/-]\d{1,2}[月/-]\d{1,2}日?)")
|
||||
AMOUNT_TEXT_PATTERN = re.compile(
|
||||
r"(\d+(?:\.\d+)?)\s*(?:万元|万员|万圆|万园|万块|万元整|元整|块钱|块|元|员|圆|园|万)"
|
||||
)
|
||||
AMOUNT_TEXT_PATTERN = re.compile(
|
||||
r"(\d+(?:\.\d+)?)\s*(?:万元|万员|万圆|万园|万块|万元整|元整|块钱|块|元|员|圆|园|万)"
|
||||
)
|
||||
DOCUMENT_AMOUNT_PATTERN = re.compile(
|
||||
r"(?:价税合计|合计金额|费用合计|订单(?:总)?金额|支付(?:金额)?|实付(?:金额)?|实收(?:金额)?|总(?:额|计|价)|票价|金额|车费|消费金额)"
|
||||
r"[::\s¥¥人民币]*([0-9]+(?:[.,][0-9]{1,2})?)"
|
||||
)
|
||||
DOCUMENT_CURRENCY_AMOUNT_PATTERN = re.compile(r"[¥¥]\s*([0-9]+(?:[.,][0-9]{1,2})?)")
|
||||
|
||||
SOURCE_LABELS = {
|
||||
SOURCE_LABELS = {
|
||||
"user_text": "用户描述",
|
||||
"user_form": "用户修改",
|
||||
"ocr": "票据识别",
|
||||
@@ -215,7 +215,7 @@ INFERRED_REASON_LABELS = {
|
||||
"welfare": "员工福利",
|
||||
"other": "其他费用",
|
||||
}
|
||||
SYSTEM_GENERATED_REASON_PREFIXES = (
|
||||
SYSTEM_GENERATED_REASON_PREFIXES = (
|
||||
"我上传了",
|
||||
"请按当前已识别信息",
|
||||
"请把当前上传的票据",
|
||||
@@ -225,20 +225,20 @@ SYSTEM_GENERATED_REASON_PREFIXES = (
|
||||
"我已修改识别信息",
|
||||
"查看报销草稿",
|
||||
"请解释一下当前这笔报销的合规风险和待补充项",
|
||||
)
|
||||
AMOUNT_UNIT_ALIASES = {
|
||||
"员": "元",
|
||||
"圆": "元",
|
||||
"园": "元",
|
||||
"块": "元",
|
||||
"块钱": "元",
|
||||
"元整": "元",
|
||||
"万员": "万元",
|
||||
"万圆": "万元",
|
||||
"万园": "万元",
|
||||
"万块": "万元",
|
||||
"万元整": "万元",
|
||||
}
|
||||
)
|
||||
AMOUNT_UNIT_ALIASES = {
|
||||
"员": "元",
|
||||
"圆": "元",
|
||||
"园": "元",
|
||||
"块": "元",
|
||||
"块钱": "元",
|
||||
"元整": "元",
|
||||
"万员": "万元",
|
||||
"万圆": "万元",
|
||||
"万园": "万元",
|
||||
"万块": "万元",
|
||||
"万元整": "万元",
|
||||
}
|
||||
|
||||
|
||||
class UserAgentService:
|
||||
@@ -1742,7 +1742,7 @@ class UserAgentService:
|
||||
if is_submitted:
|
||||
body = (
|
||||
f"主题:{subject}\n"
|
||||
f"结论:报销单已提交,当前节点为 {approval_stage or '审批中'}。\n"
|
||||
f"结论:报销单已提交,当前节点为 {approval_stage or '审批中'}。\n"
|
||||
"建议:后续可在个人报销列表中跟踪审批进度,必要时再补充说明或附件。\n"
|
||||
f"原始问题:{payload.message}"
|
||||
)
|
||||
@@ -2381,7 +2381,7 @@ class UserAgentService:
|
||||
if review_action == "next_step":
|
||||
if draft_payload is not None and draft_payload.status == "submitted":
|
||||
stage_text = draft_payload.approval_stage or "审批中"
|
||||
return f"报销单 {draft_payload.claim_no or ''} 已提交,当前节点为 {stage_text}。".strip()
|
||||
return f"报销单 {draft_payload.claim_no or ''} 已提交,当前节点为 {stage_text}。".strip()
|
||||
if payload.tool_payload.get("submission_blocked"):
|
||||
return str(payload.tool_payload.get("message") or "").strip() or "当前报销单暂时还不能提交审批。"
|
||||
return (
|
||||
@@ -2947,19 +2947,19 @@ class UserAgentService:
|
||||
"expense_type_code": "",
|
||||
}
|
||||
participants: list[str] = []
|
||||
for item in payload.ontology.entities:
|
||||
if item.type == "employee" and not values["employee_name"]:
|
||||
values["employee_name"] = item.value
|
||||
elif item.type == "customer" and not values["customer"]:
|
||||
values["customer"] = item.value
|
||||
elif item.type == "amount" and item.role != "threshold" and not values["amount"]:
|
||||
normalized_amount = str(item.normalized_value or "").strip()
|
||||
values["amount"] = f"{normalized_amount}元" if normalized_amount else item.value
|
||||
elif item.type == "expense_type" and not values["expense_type_code"]:
|
||||
values["expense_type_code"] = item.normalized_value
|
||||
values["expense_type"] = EXPENSE_TYPE_LABELS.get(
|
||||
item.normalized_value,
|
||||
item.value,
|
||||
for item in payload.ontology.entities:
|
||||
if item.type == "employee" and not values["employee_name"]:
|
||||
values["employee_name"] = item.value
|
||||
elif item.type == "customer" and not values["customer"]:
|
||||
values["customer"] = item.value
|
||||
elif item.type == "amount" and item.role != "threshold" and not values["amount"]:
|
||||
normalized_amount = str(item.normalized_value or "").strip()
|
||||
values["amount"] = f"{normalized_amount}元" if normalized_amount else item.value
|
||||
elif item.type == "expense_type" and not values["expense_type_code"]:
|
||||
values["expense_type_code"] = item.normalized_value
|
||||
values["expense_type"] = EXPENSE_TYPE_LABELS.get(
|
||||
item.normalized_value,
|
||||
item.value,
|
||||
)
|
||||
elif item.type in {"participant", "person"} and item.value.strip():
|
||||
participants.append(item.value.strip())
|
||||
@@ -3189,7 +3189,24 @@ class UserAgentService:
|
||||
evidence="来源于用户修改后的结构化表单。",
|
||||
)
|
||||
|
||||
inferred_reason = self._infer_reason_from_claim_groups(
|
||||
claim_groups=claim_groups,
|
||||
)
|
||||
reason_value = self._resolve_reason_text(self._resolve_reason_source_text(payload))
|
||||
if inferred_reason:
|
||||
return self._build_slot_value(
|
||||
value=inferred_reason,
|
||||
raw_value=reason_value or inferred_reason,
|
||||
normalized_value=inferred_reason,
|
||||
source="ocr",
|
||||
confidence=0.82,
|
||||
evidence=(
|
||||
"系统已根据票据识别结果预置场景类型;原始描述仍保留为补充说明。"
|
||||
if reason_value
|
||||
else "系统已根据票据识别场景补全通用事由,若需更具体说明可继续修改。"
|
||||
),
|
||||
)
|
||||
|
||||
if reason_value:
|
||||
return self._build_slot_value(
|
||||
value=reason_value,
|
||||
@@ -3199,19 +3216,6 @@ class UserAgentService:
|
||||
confidence=0.76,
|
||||
evidence="系统从用户原始描述中提取了本次费用事由,建议继续核对。",
|
||||
)
|
||||
|
||||
inferred_reason = self._infer_reason_from_claim_groups(
|
||||
claim_groups=claim_groups,
|
||||
)
|
||||
if inferred_reason:
|
||||
return self._build_slot_value(
|
||||
value=inferred_reason,
|
||||
raw_value=inferred_reason,
|
||||
normalized_value=inferred_reason,
|
||||
source="ocr",
|
||||
confidence=0.68,
|
||||
evidence="系统已根据票据识别场景补全通用事由,若需更具体说明可继续修改。",
|
||||
)
|
||||
return self._build_slot_value()
|
||||
|
||||
def _build_amount_slot(
|
||||
@@ -3358,17 +3362,17 @@ class UserAgentService:
|
||||
return self._build_slot_value()
|
||||
|
||||
@staticmethod
|
||||
def _normalize_amount_text(value: str) -> str:
|
||||
cleaned = str(value or "").strip()
|
||||
if not cleaned:
|
||||
return ""
|
||||
for alias, canonical in sorted(AMOUNT_UNIT_ALIASES.items(), key=lambda item: len(item[0]), reverse=True):
|
||||
cleaned = cleaned.replace(alias, canonical)
|
||||
match = AMOUNT_TEXT_PATTERN.search(cleaned)
|
||||
if not match:
|
||||
return cleaned
|
||||
number = float(match.group(1))
|
||||
return f"{number:.2f}元"
|
||||
def _normalize_amount_text(value: str) -> str:
|
||||
cleaned = str(value or "").strip()
|
||||
if not cleaned:
|
||||
return ""
|
||||
for alias, canonical in sorted(AMOUNT_UNIT_ALIASES.items(), key=lambda item: len(item[0]), reverse=True):
|
||||
cleaned = cleaned.replace(alias, canonical)
|
||||
match = AMOUNT_TEXT_PATTERN.search(cleaned)
|
||||
if not match:
|
||||
return cleaned
|
||||
number = float(match.group(1))
|
||||
return f"{number:.2f}元"
|
||||
|
||||
@staticmethod
|
||||
def _normalize_expense_type_input(value: str) -> tuple[str, str]:
|
||||
|
||||
@@ -1,139 +1,139 @@
|
||||
README.md
|
||||
pyproject.toml
|
||||
src/app/__init__.py
|
||||
src/app/main.py
|
||||
src/app/api/__init__.py
|
||||
src/app/api/deps.py
|
||||
src/app/api/router.py
|
||||
src/app/api/v1/__init__.py
|
||||
src/app/api/v1/router.py
|
||||
src/app/api/v1/endpoints/__init__.py
|
||||
src/app/api/v1/endpoints/agent_assets.py
|
||||
src/app/api/v1/endpoints/agent_runs.py
|
||||
src/app/api/v1/endpoints/audit_logs.py
|
||||
src/app/api/v1/endpoints/auth.py
|
||||
src/app/api/v1/endpoints/bootstrap.py
|
||||
src/app/api/v1/endpoints/employees.py
|
||||
src/app/api/v1/endpoints/health.py
|
||||
src/app/api/v1/endpoints/knowledge.py
|
||||
src/app/api/v1/endpoints/ocr.py
|
||||
src/app/api/v1/endpoints/ontology.py
|
||||
src/app/api/v1/endpoints/orchestrator.py
|
||||
src/app/api/v1/endpoints/reimbursements.py
|
||||
src/app/api/v1/endpoints/settings.py
|
||||
src/app/api/v1/endpoints/system_logs.py
|
||||
src/app/core/__init__.py
|
||||
src/app/core/admin_secret.py
|
||||
src/app/core/agent_enums.py
|
||||
src/app/core/bootstrap.py
|
||||
src/app/core/config.py
|
||||
src/app/core/logging.py
|
||||
src/app/core/openapi.py
|
||||
src/app/core/secret_box.py
|
||||
src/app/core/security.py
|
||||
src/app/db/__init__.py
|
||||
src/app/db/base.py
|
||||
src/app/db/base_class.py
|
||||
src/app/db/session.py
|
||||
src/app/middleware/__init__.py
|
||||
src/app/middleware/logging.py
|
||||
src/app/models/__init__.py
|
||||
src/app/models/agent_asset.py
|
||||
src/app/models/agent_conversation.py
|
||||
src/app/models/agent_run.py
|
||||
src/app/models/approval.py
|
||||
src/app/models/audit_log.py
|
||||
src/app/models/employee.py
|
||||
src/app/models/employee_change_log.py
|
||||
src/app/models/financial_record.py
|
||||
src/app/models/organization.py
|
||||
src/app/models/reimbursement.py
|
||||
src/app/models/role.py
|
||||
src/app/models/system_model_setting.py
|
||||
src/app/models/system_setting.py
|
||||
src/app/models/system_setting_secret.py
|
||||
src/app/repositories/__init__.py
|
||||
src/app/repositories/agent_asset.py
|
||||
src/app/repositories/agent_run.py
|
||||
src/app/repositories/audit_log.py
|
||||
src/app/repositories/employee.py
|
||||
src/app/repositories/reimbursement.py
|
||||
src/app/repositories/settings.py
|
||||
src/app/schemas/__init__.py
|
||||
src/app/schemas/agent_asset.py
|
||||
src/app/schemas/agent_run.py
|
||||
src/app/schemas/audit_log.py
|
||||
src/app/schemas/auth.py
|
||||
src/app/schemas/bootstrap.py
|
||||
src/app/schemas/common.py
|
||||
src/app/schemas/employee.py
|
||||
src/app/schemas/knowledge.py
|
||||
src/app/schemas/ocr.py
|
||||
src/app/schemas/ontology.py
|
||||
src/app/schemas/orchestrator.py
|
||||
src/app/schemas/reimbursement.py
|
||||
src/app/schemas/settings.py
|
||||
src/app/schemas/system_log.py
|
||||
src/app/schemas/user_agent.py
|
||||
src/app/services/__init__.py
|
||||
src/app/services/agent_asset_spreadsheet.py
|
||||
src/app/services/agent_assets.py
|
||||
src/app/services/agent_conversations.py
|
||||
src/app/services/agent_foundation.py
|
||||
src/app/services/agent_runs.py
|
||||
src/app/services/audit.py
|
||||
src/app/services/auth.py
|
||||
src/app/services/document_intelligence.py
|
||||
src/app/services/employee.py
|
||||
src/app/services/employee_seed.py
|
||||
src/app/services/expense_claims.py
|
||||
src/app/services/expense_rule_runtime.py
|
||||
src/app/services/hermes_sync.py
|
||||
src/app/services/knowledge.py
|
||||
src/app/services/knowledge_index_tasks.py
|
||||
src/app/services/knowledge_normalizer.py
|
||||
src/app/services/knowledge_rag.py
|
||||
src/app/services/knowledge_scheduler.py
|
||||
src/app/services/knowledge_sync.py
|
||||
src/app/services/model_connectivity.py
|
||||
src/app/services/ocr.py
|
||||
src/app/services/ontology.py
|
||||
src/app/services/orchestrator.py
|
||||
src/app/services/reimbursement.py
|
||||
src/app/services/runtime_chat.py
|
||||
src/app/services/settings.py
|
||||
src/app/services/system_hermes.py
|
||||
src/app/services/system_logs.py
|
||||
src/app/services/user_agent.py
|
||||
src/x_financial_server.egg-info/PKG-INFO
|
||||
src/x_financial_server.egg-info/SOURCES.txt
|
||||
src/x_financial_server.egg-info/dependency_links.txt
|
||||
src/x_financial_server.egg-info/requires.txt
|
||||
src/x_financial_server.egg-info/top_level.txt
|
||||
tests/test_agent_asset_onlyoffice_key.py
|
||||
tests/test_agent_asset_service.py
|
||||
tests/test_agent_asset_spreadsheet_import.py
|
||||
tests/test_agent_foundation_endpoints.py
|
||||
tests/test_agent_runs_service.py
|
||||
tests/test_auth_service.py
|
||||
tests/test_config_settings_reload.py
|
||||
tests/test_document_intelligence.py
|
||||
tests/test_employee_service.py
|
||||
tests/test_env_file_precedence.py
|
||||
tests/test_expense_claim_service.py
|
||||
tests/test_imports.py
|
||||
tests/test_knowledge_normalizer.py
|
||||
tests/test_knowledge_onlyoffice_config.py
|
||||
tests/test_knowledge_rag_service.py
|
||||
tests/test_knowledge_service.py
|
||||
tests/test_ocr_endpoints.py
|
||||
tests/test_ocr_service.py
|
||||
tests/test_ontology_service.py
|
||||
tests/test_openapi_schema.py
|
||||
tests/test_reimbursement_endpoints.py
|
||||
tests/test_runtime_chat_service.py
|
||||
tests/test_server_start_dependencies.py
|
||||
tests/test_settings_persistence.py
|
||||
tests/test_settings_service.py
|
||||
tests/test_system_logs_service.py
|
||||
README.md
|
||||
pyproject.toml
|
||||
src/app/__init__.py
|
||||
src/app/main.py
|
||||
src/app/api/__init__.py
|
||||
src/app/api/deps.py
|
||||
src/app/api/router.py
|
||||
src/app/api/v1/__init__.py
|
||||
src/app/api/v1/router.py
|
||||
src/app/api/v1/endpoints/__init__.py
|
||||
src/app/api/v1/endpoints/agent_assets.py
|
||||
src/app/api/v1/endpoints/agent_runs.py
|
||||
src/app/api/v1/endpoints/audit_logs.py
|
||||
src/app/api/v1/endpoints/auth.py
|
||||
src/app/api/v1/endpoints/bootstrap.py
|
||||
src/app/api/v1/endpoints/employees.py
|
||||
src/app/api/v1/endpoints/health.py
|
||||
src/app/api/v1/endpoints/knowledge.py
|
||||
src/app/api/v1/endpoints/ocr.py
|
||||
src/app/api/v1/endpoints/ontology.py
|
||||
src/app/api/v1/endpoints/orchestrator.py
|
||||
src/app/api/v1/endpoints/reimbursements.py
|
||||
src/app/api/v1/endpoints/settings.py
|
||||
src/app/api/v1/endpoints/system_logs.py
|
||||
src/app/core/__init__.py
|
||||
src/app/core/admin_secret.py
|
||||
src/app/core/agent_enums.py
|
||||
src/app/core/bootstrap.py
|
||||
src/app/core/config.py
|
||||
src/app/core/logging.py
|
||||
src/app/core/openapi.py
|
||||
src/app/core/secret_box.py
|
||||
src/app/core/security.py
|
||||
src/app/db/__init__.py
|
||||
src/app/db/base.py
|
||||
src/app/db/base_class.py
|
||||
src/app/db/session.py
|
||||
src/app/middleware/__init__.py
|
||||
src/app/middleware/logging.py
|
||||
src/app/models/__init__.py
|
||||
src/app/models/agent_asset.py
|
||||
src/app/models/agent_conversation.py
|
||||
src/app/models/agent_run.py
|
||||
src/app/models/approval.py
|
||||
src/app/models/audit_log.py
|
||||
src/app/models/employee.py
|
||||
src/app/models/employee_change_log.py
|
||||
src/app/models/financial_record.py
|
||||
src/app/models/organization.py
|
||||
src/app/models/reimbursement.py
|
||||
src/app/models/role.py
|
||||
src/app/models/system_model_setting.py
|
||||
src/app/models/system_setting.py
|
||||
src/app/models/system_setting_secret.py
|
||||
src/app/repositories/__init__.py
|
||||
src/app/repositories/agent_asset.py
|
||||
src/app/repositories/agent_run.py
|
||||
src/app/repositories/audit_log.py
|
||||
src/app/repositories/employee.py
|
||||
src/app/repositories/reimbursement.py
|
||||
src/app/repositories/settings.py
|
||||
src/app/schemas/__init__.py
|
||||
src/app/schemas/agent_asset.py
|
||||
src/app/schemas/agent_run.py
|
||||
src/app/schemas/audit_log.py
|
||||
src/app/schemas/auth.py
|
||||
src/app/schemas/bootstrap.py
|
||||
src/app/schemas/common.py
|
||||
src/app/schemas/employee.py
|
||||
src/app/schemas/knowledge.py
|
||||
src/app/schemas/ocr.py
|
||||
src/app/schemas/ontology.py
|
||||
src/app/schemas/orchestrator.py
|
||||
src/app/schemas/reimbursement.py
|
||||
src/app/schemas/settings.py
|
||||
src/app/schemas/system_log.py
|
||||
src/app/schemas/user_agent.py
|
||||
src/app/services/__init__.py
|
||||
src/app/services/agent_asset_spreadsheet.py
|
||||
src/app/services/agent_assets.py
|
||||
src/app/services/agent_conversations.py
|
||||
src/app/services/agent_foundation.py
|
||||
src/app/services/agent_runs.py
|
||||
src/app/services/audit.py
|
||||
src/app/services/auth.py
|
||||
src/app/services/document_intelligence.py
|
||||
src/app/services/employee.py
|
||||
src/app/services/employee_seed.py
|
||||
src/app/services/expense_claims.py
|
||||
src/app/services/expense_rule_runtime.py
|
||||
src/app/services/hermes_sync.py
|
||||
src/app/services/knowledge.py
|
||||
src/app/services/knowledge_index_tasks.py
|
||||
src/app/services/knowledge_normalizer.py
|
||||
src/app/services/knowledge_rag.py
|
||||
src/app/services/knowledge_scheduler.py
|
||||
src/app/services/knowledge_sync.py
|
||||
src/app/services/model_connectivity.py
|
||||
src/app/services/ocr.py
|
||||
src/app/services/ontology.py
|
||||
src/app/services/orchestrator.py
|
||||
src/app/services/reimbursement.py
|
||||
src/app/services/runtime_chat.py
|
||||
src/app/services/settings.py
|
||||
src/app/services/system_hermes.py
|
||||
src/app/services/system_logs.py
|
||||
src/app/services/user_agent.py
|
||||
src/x_financial_server.egg-info/PKG-INFO
|
||||
src/x_financial_server.egg-info/SOURCES.txt
|
||||
src/x_financial_server.egg-info/dependency_links.txt
|
||||
src/x_financial_server.egg-info/requires.txt
|
||||
src/x_financial_server.egg-info/top_level.txt
|
||||
tests/test_agent_asset_onlyoffice_key.py
|
||||
tests/test_agent_asset_service.py
|
||||
tests/test_agent_asset_spreadsheet_import.py
|
||||
tests/test_agent_foundation_endpoints.py
|
||||
tests/test_agent_runs_service.py
|
||||
tests/test_auth_service.py
|
||||
tests/test_config_settings_reload.py
|
||||
tests/test_document_intelligence.py
|
||||
tests/test_employee_service.py
|
||||
tests/test_env_file_precedence.py
|
||||
tests/test_expense_claim_service.py
|
||||
tests/test_imports.py
|
||||
tests/test_knowledge_normalizer.py
|
||||
tests/test_knowledge_onlyoffice_config.py
|
||||
tests/test_knowledge_rag_service.py
|
||||
tests/test_knowledge_service.py
|
||||
tests/test_ocr_endpoints.py
|
||||
tests/test_ocr_service.py
|
||||
tests/test_ontology_service.py
|
||||
tests/test_openapi_schema.py
|
||||
tests/test_reimbursement_endpoints.py
|
||||
tests/test_runtime_chat_service.py
|
||||
tests/test_server_start_dependencies.py
|
||||
tests/test_settings_persistence.py
|
||||
tests/test_settings_service.py
|
||||
tests/test_system_logs_service.py
|
||||
tests/test_user_agent_service.py
|
||||
Reference in New Issue
Block a user