fix(agent): 修复规则中心表格版本和修改记录
补齐规则资产 JSON 读写接口和前端调用,修复 AuditView 导入缺失。 Excel 在线编辑改为比对所有页签并生成最近修改记录,版本快照统一保存到 rules/finance-rules/.versions。 隔离规则表测试存储,避免测试或旧入口写入真实规则目录与 storage/agent_assets。
This commit is contained in:
@@ -10,7 +10,6 @@ from urllib.parse import quote
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
import jwt
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import CurrentUserContext
|
||||
@@ -44,7 +43,6 @@ from app.schemas.agent_asset import (
|
||||
)
|
||||
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,
|
||||
@@ -52,8 +50,9 @@ from app.services.agent_asset_spreadsheet import (
|
||||
FINANCE_RULES_LIBRARY,
|
||||
RISK_RULES_LIBRARY,
|
||||
RULE_LIBRARY_NAMES,
|
||||
RuleSpreadsheetMeta,
|
||||
SPREADSHEET_MIME_TYPE,
|
||||
AgentAssetSpreadsheetManager,
|
||||
RuleSpreadsheetMeta,
|
||||
)
|
||||
from app.services.agent_foundation import AgentFoundationService
|
||||
from app.services.audit import AuditLogService
|
||||
@@ -410,7 +409,8 @@ class AgentAssetService:
|
||||
file_path = self.spreadsheet_manager.resolve_storage_path(metadata.storage_key)
|
||||
if not file_path.exists():
|
||||
raise FileNotFoundError(metadata.file_name)
|
||||
snapshot_meta = self.spreadsheet_manager.store_spreadsheet(
|
||||
snapshot_meta = self.spreadsheet_manager.store_rule_library_spreadsheet_snapshot(
|
||||
library=self._resolve_spreadsheet_rule_library(asset),
|
||||
asset_id=asset.id,
|
||||
version=target_version,
|
||||
file_name=metadata.file_name,
|
||||
@@ -542,7 +542,11 @@ class AgentAssetService:
|
||||
return file_path, metadata.mime_type, metadata.file_name
|
||||
|
||||
asset = self._require_spreadsheet_rule(asset_id)
|
||||
_, metadata = self._resolve_current_spreadsheet_meta(asset)
|
||||
requested_version = str(version or "").strip()
|
||||
if requested_version and requested_version != "current":
|
||||
_, metadata = self._resolve_spreadsheet_version_meta(asset, version=requested_version)
|
||||
else:
|
||||
_, metadata = self._resolve_current_spreadsheet_meta(asset)
|
||||
file_path = self.spreadsheet_manager.resolve_storage_path(metadata.storage_key)
|
||||
if not file_path.exists():
|
||||
raise FileNotFoundError(metadata.file_name)
|
||||
@@ -594,6 +598,14 @@ class AgentAssetService:
|
||||
|
||||
_, current_metadata = self._resolve_current_spreadsheet_meta(asset)
|
||||
file_name = current_metadata.file_name or self._resolve_default_spreadsheet_file_name(asset)
|
||||
sheet_changes, cell_changes = self._collect_workbook_changes_from_content(
|
||||
current_metadata,
|
||||
content,
|
||||
)
|
||||
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,
|
||||
file_name=file_name,
|
||||
@@ -601,6 +613,45 @@ 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",
|
||||
@@ -608,12 +659,14 @@ class AgentAssetService:
|
||||
resource_id=asset.id,
|
||||
before_json={"storage_key": current_metadata.storage_key},
|
||||
after_json={
|
||||
"summary": change_note or f"上传并覆盖当前规则表:{normalized_name}",
|
||||
"changed_sheet_count": 0,
|
||||
"changed_cell_count": 0,
|
||||
"sheet_changes": [],
|
||||
"cell_changes": [],
|
||||
"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,
|
||||
)
|
||||
@@ -682,56 +735,16 @@ class AgentAssetService:
|
||||
if current_metadata.checksum and current_metadata.checksum == self._hash_bytes(content):
|
||||
return
|
||||
|
||||
from io import BytesIO
|
||||
from openpyxl import load_workbook
|
||||
try:
|
||||
base_workbook = self._load_spreadsheet_for_compare(current_metadata)
|
||||
target_workbook = load_workbook(BytesIO(content), read_only=False, data_only=False)
|
||||
sheet_changes, cell_changes = self._collect_workbook_changes(
|
||||
base_workbook, target_workbook
|
||||
)
|
||||
changed_sheet_count = len(
|
||||
{item.sheet_name for item in sheet_changes}
|
||||
| {item.sheet_name for item in cell_changes}
|
||||
)
|
||||
changed_cell_count = len(cell_changes)
|
||||
|
||||
if changed_cell_count > 0 or changed_sheet_count > 0:
|
||||
change_note = f"ONLYOFFICE 在线编辑:涉及 {changed_sheet_count} 个 Sheet,共 {changed_cell_count} 处改动。"
|
||||
else:
|
||||
change_note = "ONLYOFFICE 在线编辑保存。"
|
||||
except Exception:
|
||||
sheet_changes = []
|
||||
cell_changes = []
|
||||
changed_sheet_count = 0
|
||||
changed_cell_count = 0
|
||||
change_note = "ONLYOFFICE 在线编辑保存。"
|
||||
|
||||
resolved_actor_name = str(actor_name or "").strip() or (
|
||||
callback.users[0] if callback.users else "ONLYOFFICE"
|
||||
)
|
||||
self._store_current_rule_spreadsheet(
|
||||
asset,
|
||||
file_name=current_metadata.file_name,
|
||||
self.upload_rule_spreadsheet(
|
||||
asset.id,
|
||||
filename=current_metadata.file_name,
|
||||
content=content,
|
||||
actor=resolved_actor_name,
|
||||
source="onlyoffice",
|
||||
)
|
||||
if changed_sheet_count > 0 or changed_cell_count > 0:
|
||||
self.audit_service.log_action(
|
||||
actor=resolved_actor_name,
|
||||
action="edit_rule_spreadsheet",
|
||||
resource_type=asset.asset_type,
|
||||
resource_id=asset.id,
|
||||
before_json={"storage_key": current_metadata.storage_key},
|
||||
after_json={
|
||||
"summary": change_note,
|
||||
"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]],
|
||||
},
|
||||
)
|
||||
|
||||
def _ensure_ready(self) -> None:
|
||||
AgentFoundationService(self.db).ensure_foundation_ready()
|
||||
@@ -782,7 +795,8 @@ class AgentAssetService:
|
||||
|
||||
if (
|
||||
asset.asset_type == AgentAssetType.RULE.value
|
||||
and str((asset.config_json or {}).get("detail_mode") or "").strip().lower() == "spreadsheet"
|
||||
and str((asset.config_json or {}).get("detail_mode") or "").strip().lower()
|
||||
== "spreadsheet"
|
||||
):
|
||||
metadata = self.spreadsheet_manager.parse_version_markdown(str(source.content or ""))
|
||||
if metadata is None:
|
||||
@@ -919,60 +933,30 @@ class AgentAssetService:
|
||||
) -> 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)
|
||||
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)
|
||||
base_sheet_names = set(base_workbook.sheetnames)
|
||||
target_sheet_names = set(target_workbook.sheetnames)
|
||||
|
||||
sheet_changes: list[AgentAssetSpreadsheetDiffSheetRead] = []
|
||||
for sheet_name in sorted(target_sheet_names - base_sheet_names):
|
||||
sheet_changes.append(
|
||||
AgentAssetSpreadsheetDiffSheetRead(sheet_name=sheet_name, change_type="added")
|
||||
)
|
||||
for sheet_name in sorted(base_sheet_names - target_sheet_names):
|
||||
sheet_changes.append(
|
||||
AgentAssetSpreadsheetDiffSheetRead(sheet_name=sheet_name, change_type="removed")
|
||||
)
|
||||
|
||||
cell_changes: list[AgentAssetSpreadsheetDiffCellRead] = []
|
||||
changed_sheets: set[str] = set()
|
||||
for sheet_name in sorted(base_sheet_names & target_sheet_names):
|
||||
base_sheet = base_workbook[sheet_name]
|
||||
target_sheet = target_workbook[sheet_name]
|
||||
max_row = max(base_sheet.max_row, target_sheet.max_row)
|
||||
max_column = max(base_sheet.max_column, target_sheet.max_column)
|
||||
for row_index in range(1, max_row + 1):
|
||||
for column_index in range(1, max_column + 1):
|
||||
before_value = base_sheet.cell(row=row_index, column=column_index).value
|
||||
after_value = target_sheet.cell(row=row_index, column=column_index).value
|
||||
if before_value == after_value:
|
||||
continue
|
||||
changed_sheets.add(sheet_name)
|
||||
if before_value in (None, ""):
|
||||
change_type = "added"
|
||||
elif after_value in (None, ""):
|
||||
change_type = "removed"
|
||||
else:
|
||||
change_type = "modified"
|
||||
cell_changes.append(
|
||||
AgentAssetSpreadsheetDiffCellRead(
|
||||
sheet_name=sheet_name,
|
||||
cell=target_sheet.cell(row=row_index, column=column_index).coordinate,
|
||||
change_type=change_type,
|
||||
before_value=before_value,
|
||||
after_value=after_value,
|
||||
)
|
||||
)
|
||||
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=len(target_sheet_names - base_sheet_names),
|
||||
removed_sheet_count=len(base_sheet_names - target_sheet_names),
|
||||
changed_sheet_count=len(changed_sheets),
|
||||
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],
|
||||
@@ -997,6 +981,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 在线编辑保存。"),
|
||||
sheet_changes=[
|
||||
AgentAssetSpreadsheetDiffSheetRead.model_validate(item)
|
||||
@@ -1379,9 +1364,10 @@ class AgentAssetService:
|
||||
|
||||
file_name = PREVIEW_RULE_VERSION_FILENAMES[resolved_version]
|
||||
storage_key = (
|
||||
Path("agent_assets")
|
||||
Path("rules")
|
||||
/ FINANCE_RULES_LIBRARY
|
||||
/ ".versions"
|
||||
/ PREVIEW_RULE_ASSET_ID
|
||||
/ "rule_spreadsheets"
|
||||
/ resolved_version
|
||||
/ file_name
|
||||
).as_posix()
|
||||
@@ -1404,7 +1390,8 @@ class AgentAssetService:
|
||||
source="preview",
|
||||
)
|
||||
|
||||
metadata = self.spreadsheet_manager.store_spreadsheet(
|
||||
metadata = self.spreadsheet_manager.store_rule_library_spreadsheet_snapshot(
|
||||
library=FINANCE_RULES_LIBRARY,
|
||||
asset_id=PREVIEW_RULE_ASSET_ID,
|
||||
version=resolved_version,
|
||||
file_name=file_name,
|
||||
@@ -1436,7 +1423,8 @@ class AgentAssetService:
|
||||
return
|
||||
|
||||
actor_name = callback.users[0] if callback.users else "ONLYOFFICE"
|
||||
self.spreadsheet_manager.store_spreadsheet(
|
||||
self.spreadsheet_manager.store_rule_library_spreadsheet_snapshot(
|
||||
library=FINANCE_RULES_LIBRARY,
|
||||
asset_id=PREVIEW_RULE_ASSET_ID,
|
||||
version=resolved_version,
|
||||
file_name=metadata.file_name,
|
||||
@@ -1482,7 +1470,10 @@ class AgentAssetService:
|
||||
) -> 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()
|
||||
return (
|
||||
can_edit
|
||||
and AgentAssetService._resolve_working_version(asset) == str(version or "").strip()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _can_edit_current_spreadsheet(current_user: CurrentUserContext) -> bool:
|
||||
@@ -1495,7 +1486,8 @@ class AgentAssetService:
|
||||
version: str,
|
||||
metadata: RuleSpreadsheetMeta,
|
||||
) -> str:
|
||||
raw_key = f"{asset_id}-{version}-{metadata.checksum or metadata.updated_at or metadata.file_name}"
|
||||
fingerprint = metadata.checksum or metadata.updated_at or metadata.file_name
|
||||
raw_key = f"{asset_id}-{version}-{fingerprint}"
|
||||
return "".join(
|
||||
character if character.isalnum() or character in {"-", "_", ".", "="} else "_"
|
||||
for character in raw_key
|
||||
@@ -1570,6 +1562,7 @@ class AgentAssetService:
|
||||
|
||||
def _load_spreadsheet_for_compare(self, metadata: RuleSpreadsheetMeta):
|
||||
from io import BytesIO
|
||||
|
||||
from openpyxl import load_workbook
|
||||
|
||||
file_path = self.spreadsheet_manager.resolve_storage_path(metadata.storage_key)
|
||||
@@ -1577,6 +1570,19 @@ class AgentAssetService:
|
||||
raise FileNotFoundError(metadata.file_name)
|
||||
return load_workbook(BytesIO(file_path.read_bytes()), read_only=False, data_only=False)
|
||||
|
||||
def _collect_workbook_changes_from_content(
|
||||
self,
|
||||
base_metadata: RuleSpreadsheetMeta,
|
||||
target_content: bytes,
|
||||
) -> tuple[list[AgentAssetSpreadsheetDiffSheetRead], list[AgentAssetSpreadsheetDiffCellRead]]:
|
||||
from io import BytesIO
|
||||
|
||||
from openpyxl import load_workbook
|
||||
|
||||
base_workbook = self._load_spreadsheet_for_compare(base_metadata)
|
||||
target_workbook = load_workbook(BytesIO(target_content), read_only=False, data_only=False)
|
||||
return self._collect_workbook_changes(base_workbook, target_workbook)
|
||||
|
||||
def _collect_workbook_changes(
|
||||
self, base_workbook, target_workbook
|
||||
) -> tuple[list[AgentAssetSpreadsheetDiffSheetRead], list[AgentAssetSpreadsheetDiffCellRead]]:
|
||||
@@ -1621,8 +1627,50 @@ class AgentAssetService:
|
||||
)
|
||||
)
|
||||
|
||||
for sheet_name in sorted({item.sheet_name for item in cell_changes}):
|
||||
sheet_changes.append(
|
||||
AgentAssetSpreadsheetDiffSheetRead(sheet_name=sheet_name, change_type="modified")
|
||||
)
|
||||
|
||||
return sheet_changes, cell_changes
|
||||
|
||||
@staticmethod
|
||||
def _count_changed_sheets(
|
||||
sheet_changes: list[AgentAssetSpreadsheetDiffSheetRead],
|
||||
cell_changes: list[AgentAssetSpreadsheetDiffCellRead],
|
||||
) -> int:
|
||||
return len(
|
||||
{item.sheet_name for item in sheet_changes}
|
||||
| {item.sheet_name for item in cell_changes}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _build_spreadsheet_change_summary(
|
||||
operation_label: str,
|
||||
sheet_changes: list[AgentAssetSpreadsheetDiffSheetRead],
|
||||
cell_changes: list[AgentAssetSpreadsheetDiffCellRead],
|
||||
) -> str:
|
||||
sheet_names = sorted(
|
||||
{item.sheet_name for item in sheet_changes}
|
||||
| {item.sheet_name for item in cell_changes}
|
||||
)
|
||||
if not sheet_names:
|
||||
return f"{operation_label}:文件内容已保存,未发现单元格级差异。"
|
||||
|
||||
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},工作表结构发生变化。"
|
||||
|
||||
def _next_available_version(self, asset: AgentAsset) -> str:
|
||||
candidate = self._increment_version(self._resolve_working_version(asset))
|
||||
while self.repository.get_version(asset.id, candidate) is not None:
|
||||
candidate = self._increment_version(candidate)
|
||||
return candidate
|
||||
|
||||
@staticmethod
|
||||
def _extract_restore_source_version(change_note: str | None) -> str | None:
|
||||
normalized = str(change_note or "").strip()
|
||||
|
||||
Reference in New Issue
Block a user