fix(agent): 修复规则中心表格版本和修改记录

补齐规则资产 JSON 读写接口和前端调用,修复 AuditView 导入缺失。

Excel 在线编辑改为比对所有页签并生成最近修改记录,版本快照统一保存到 rules/finance-rules/.versions。

隔离规则表测试存储,避免测试或旧入口写入真实规则目录与 storage/agent_assets。
This commit is contained in:
caoxiaozhu
2026-05-19 15:41:53 +00:00
parent 9472813739
commit d460ee0fe7
13 changed files with 782 additions and 167 deletions

View File

@@ -69,26 +69,13 @@ class AgentAssetSpreadsheetManager:
actor_name: str,
source: str = "upload",
) -> RuleSpreadsheetMeta:
normalized_name = Path(str(file_name or "").strip()).name.strip()
if not normalized_name:
raise ValueError("规则表文件名不能为空。")
if not content:
raise ValueError("规则表文件内容不能为空。")
relative_path = Path("agent_assets") / asset_id / "rule_spreadsheets" / version / normalized_name
target_path = (self.storage_root / relative_path).resolve()
target_path.parent.mkdir(parents=True, exist_ok=True)
target_path.write_bytes(content)
mime_type = mimetypes.guess_type(normalized_name)[0] or SPREADSHEET_MIME_TYPE
return RuleSpreadsheetMeta(
file_name=normalized_name,
storage_key=relative_path.as_posix(),
mime_type=mime_type,
size_bytes=len(content),
checksum=hashlib.sha256(content).hexdigest(),
updated_at=datetime.now(UTC).isoformat(),
updated_by=str(actor_name or "system").strip() or "system",
return self.store_rule_library_spreadsheet_snapshot(
library=FINANCE_RULES_LIBRARY,
asset_id=asset_id,
version=version,
file_name=file_name,
content=content,
actor_name=actor_name,
source=source,
)
@@ -117,7 +104,74 @@ class AgentAssetSpreadsheetManager:
try:
target_path.relative_to(self.rule_root)
except ValueError:
raise ValueError("规则库文件路径不合法。")
raise ValueError("规则库文件路径不合法。") from None
target_path.parent.mkdir(parents=True, exist_ok=True)
target_path.write_bytes(content)
mime_type = mimetypes.guess_type(normalized_name)[0] or SPREADSHEET_MIME_TYPE
return RuleSpreadsheetMeta(
file_name=normalized_name,
storage_key=relative_path.as_posix(),
mime_type=mime_type,
size_bytes=len(content),
checksum=hashlib.sha256(content).hexdigest(),
updated_at=datetime.now(UTC).isoformat(),
updated_by=str(actor_name or "system").strip() or "system",
source=source,
)
def store_rule_library_spreadsheet_snapshot(
self,
*,
library: str,
asset_id: str,
version: str,
file_name: str,
content: bytes,
actor_name: str,
source: str = "rule-library-version",
) -> RuleSpreadsheetMeta:
normalized_library = str(library or "").strip()
if normalized_library not in RULE_LIBRARY_NAMES:
raise ValueError("规则库目录不合法。")
raw_asset_id = str(asset_id or "").strip()
raw_version = str(version or "").strip()
normalized_asset_id = Path(raw_asset_id).name.strip()
normalized_version = Path(raw_version).name.strip()
normalized_name = Path(str(file_name or "").strip()).name.strip()
if (
not normalized_asset_id
or normalized_asset_id in {".", ".."}
or normalized_asset_id != raw_asset_id
):
raise ValueError("规则资产 ID 不合法。")
if (
not normalized_version
or normalized_version in {".", ".."}
or normalized_version != raw_version
):
raise ValueError("规则表版本号不合法。")
if not normalized_name:
raise ValueError("规则表文件名不能为空。")
if not content:
raise ValueError("规则表文件内容不能为空。")
self.ensure_rule_library_dirs()
relative_path = (
Path("rules")
/ normalized_library
/ ".versions"
/ normalized_asset_id
/ normalized_version
/ normalized_name
)
target_path = (SERVER_DIR / relative_path).resolve()
try:
target_path.relative_to(self.rule_root)
except ValueError:
raise ValueError("规则库版本文件路径不合法。") from None
target_path.parent.mkdir(parents=True, exist_ok=True)
target_path.write_bytes(content)
@@ -149,7 +203,7 @@ class AgentAssetSpreadsheetManager:
try:
resolved.relative_to(allowed_root)
except ValueError:
raise FileNotFoundError("规则表文件不存在。")
raise FileNotFoundError("规则表文件不存在。") from None
return resolved
@staticmethod
@@ -230,11 +284,46 @@ class AgentAssetSpreadsheetManager:
def build_company_travel_rule_template() -> bytes:
standard_rows = [
["费用分类", "适用场景", "票据要求", "报销标准", "审批要求", "备注"],
["长途交通", "飞机、高铁、火车等跨城出行", "行程单、车票、发票", "据实报销", "超预算需直属领导审批", "优先选择公共交通"],
["住宿费", "出差住宿", "酒店发票、入住清单", "一线城市 650/晚;二线城市 500/晚;其他城市 380/晚", "超标需总监审批", "协议酒店优先"],
["市内交通", "出租车、网约车、地铁、公交", "发票或电子行程单", "150/天", "超限需补充说明", "夜间或无公共交通场景可豁免"],
["餐补", "出差期间日常补助", "无需票据", "120/天", "系统自动核定", "当天往返默认不享受"],
["招待餐费", "客户接待或项目宴请", "餐饮发票、参与人清单", "300/人", "需业务负责人审批", "需关联客户或项目"],
[
"长途交通",
"飞机、高铁、火车等跨城出行",
"行程单、车票、发票",
"据实报销",
"超预算需直属领导审批",
"优先选择公共交通",
],
[
"住宿费",
"出差住宿",
"酒店发票、入住清单",
"一线城市 650/晚;二线城市 500/晚;其他城市 380/晚",
"超标需总监审批",
"协议酒店优先",
],
[
"市内交通",
"出租车、网约车、地铁、公交",
"发票或电子行程单",
"150/天",
"超限需补充说明",
"夜间或无公共交通场景可豁免",
],
[
"餐补",
"出差期间日常补助",
"无需票据",
"120/天",
"系统自动核定",
"当天往返默认不享受",
],
[
"招待餐费",
"客户接待或项目宴请",
"餐饮发票、参与人清单",
"300/人",
"需业务负责人审批",
"需关联客户或项目",
],
]
instruction_rows = [
["字段", "填写说明"],
@@ -308,21 +397,41 @@ def _build_xlsx_bytes(sheets: list[tuple[str, list[list[object]]]]) -> bytes:
def _build_content_types_xml(sheets: list[tuple[str, list[list[object]]]]) -> str:
overrides = [
'<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>',
'<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>',
'<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>',
'<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>',
(
'<Override PartName="/xl/workbook.xml" '
'ContentType="application/vnd.openxmlformats-officedocument.'
'spreadsheetml.sheet.main+xml"/>'
),
(
'<Override PartName="/xl/styles.xml" '
'ContentType="application/vnd.openxmlformats-officedocument.'
'spreadsheetml.styles+xml"/>'
),
(
'<Override PartName="/docProps/core.xml" '
'ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>'
),
(
'<Override PartName="/docProps/app.xml" '
'ContentType="application/vnd.openxmlformats-officedocument.'
'extended-properties+xml"/>'
),
]
overrides.extend(
[
f'<Override PartName="/xl/worksheets/sheet{index}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>'
(
f'<Override PartName="/xl/worksheets/sheet{index}.xml" '
'ContentType="application/vnd.openxmlformats-officedocument.'
'spreadsheetml.worksheet+xml"/>'
)
for index, _ in enumerate(sheets, start=1)
]
)
return (
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
'<Default Extension="rels" '
'ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
'<Default Extension="xml" ContentType="application/xml"/>'
f'{"".join(overrides)}'
"</Types>"
@@ -333,9 +442,15 @@ def _build_root_rels_xml() -> str:
return (
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>'
'<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>'
'<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>'
'<Relationship Id="rId1" '
'Type="http://schemas.openxmlformats.org/officeDocument/2006/'
'relationships/officeDocument" Target="xl/workbook.xml"/>'
'<Relationship Id="rId2" '
'Type="http://schemas.openxmlformats.org/package/2006/relationships/'
'metadata/core-properties" Target="docProps/core.xml"/>'
'<Relationship Id="rId3" '
'Type="http://schemas.openxmlformats.org/officeDocument/2006/'
'relationships/extended-properties" Target="docProps/app.xml"/>'
"</Relationships>"
)
@@ -347,11 +462,16 @@ def _build_app_xml(sheets: list[tuple[str, list[list[object]]]]) -> str:
sheet_count = len(sheets)
return (
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" '
'<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/'
'extended-properties" '
'xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">'
'<Application>Microsoft Excel</Application>'
f"<HeadingPairs><vt:vector size=\"2\" baseType=\"variant\"><vt:variant><vt:lpstr>Worksheets</vt:lpstr></vt:variant><vt:variant><vt:i4>{sheet_count}</vt:i4></vt:variant></vt:vector></HeadingPairs>"
f"<TitlesOfParts><vt:vector size=\"{sheet_count}\" baseType=\"lpstr\">{titles}</vt:vector></TitlesOfParts>"
'<HeadingPairs><vt:vector size="2" baseType="variant">'
"<vt:variant><vt:lpstr>Worksheets</vt:lpstr></vt:variant>"
f"<vt:variant><vt:i4>{sheet_count}</vt:i4></vt:variant>"
"</vt:vector></HeadingPairs>"
f'<TitlesOfParts><vt:vector size="{sheet_count}" baseType="lpstr">'
f"{titles}</vt:vector></TitlesOfParts>"
"</Properties>"
)
@@ -359,7 +479,8 @@ def _build_app_xml(sheets: list[tuple[str, list[list[object]]]]) -> str:
def _build_core_xml(created_at: str) -> str:
return (
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" '
'<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/'
'2006/metadata/core-properties" '
'xmlns:dc="http://purl.org/dc/elements/1.1/" '
'xmlns:dcterms="http://purl.org/dc/terms/" '
'xmlns:dcmitype="http://purl.org/dc/dcmitype/" '
@@ -392,7 +513,11 @@ def _build_workbook_xml(sheets: list[tuple[str, list[list[object]]]]) -> str:
def _build_workbook_rels_xml(sheets: list[tuple[str, list[list[object]]]]) -> str:
relationships = "".join(
[
f'<Relationship Id="rId{index}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet{index}.xml"/>'
(
f'<Relationship Id="rId{index}" '
'Type="http://schemas.openxmlformats.org/officeDocument/2006/'
f'relationships/worksheet" Target="worksheets/sheet{index}.xml"/>'
)
for index, _ in enumerate(sheets, start=1)
]
)
@@ -414,10 +539,15 @@ def _build_styles_xml() -> str:
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
'<fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>'
'<fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>'
'<fills count="2"><fill><patternFill patternType="none"/></fill>'
'<fill><patternFill patternType="gray125"/></fill></fills>'
'<borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>'
'<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>'
'<cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs>'
'<cellStyleXfs count="1">'
'<xf numFmtId="0" fontId="0" fillId="0" borderId="0"/>'
"</cellStyleXfs>"
'<cellXfs count="1">'
'<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>'
"</cellXfs>"
'<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>'
'</styleSheet>'
)