fix(report): 错误 hint 去除「任务中心」指向 + PDF 元数据 AI 生成标识
CI / lint / typecheck / import-linter / tests / secrets (push) Canceled after 0s
CI / lint / typecheck / import-linter / tests / secrets (push) Canceled after 0s
- errors.py:IDEMPOTENCY_CONFLICT / TASK_STATE_INVALID 的 hint 改为「首页报告」口径(App 无任务中心页,经 hint 透出会误导) - report.html.j2:head 增加 author/description/generator meta(WeasyPrint 落入 PDF 属性,AI 生成内容标识 CR-05) - test_report_export.py:+2 单测(模板 meta 渲染 + PDF 产物 Info 字典解压断言,不新增依赖) 多 agent 评审 R3/R4 产物;单测 576 passed
This commit is contained in:
@@ -3,6 +3,11 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{ meta.brand }} · {{ meta.title }}</title>
|
||||
<!-- CR-05 文件元数据隐式标识(N2'-①):WeasyPrint 会把 author/description/generator
|
||||
映射进 PDF 文档属性(Author/Subject/Creator);27 位内容编号随合规出口 publish_asset() 落地,此处不伪造 -->
|
||||
<meta name="author" content="美搭(AI 生成内容)">
|
||||
<meta name="description" content="本报告由人工智能生成,仅供参考。报告 {{ meta.report_id }} v{{ meta.version }},生成于 {{ date_text }}">
|
||||
<meta name="generator" content="美搭 AI 报告生成服务">
|
||||
<style>
|
||||
/* ── 页面与页脚:A4 分页,页脚带 AI 标识 + 免责语(合规 §合规与安全方案)── */
|
||||
@page {
|
||||
|
||||
@@ -80,7 +80,7 @@ ERROR_META: dict[ErrorCode, ErrorMeta] = {
|
||||
ErrorCode.BAD_REQUEST: ErrorMeta(400, "请求参数有误", "请检查后重试"),
|
||||
ErrorCode.VALIDATION_FAILED: ErrorMeta(422, "请求参数校验未通过", "请按提示修改后重试"),
|
||||
ErrorCode.IDEMPOTENCY_CONFLICT: ErrorMeta(
|
||||
409, "相同请求正在处理中", "请稍后查看任务中心,不要重复提交"
|
||||
409, "相同请求正在处理中", "请稍后在首页「报告」中查看"
|
||||
),
|
||||
ErrorCode.UPLOAD_REJECTED: ErrorMeta(
|
||||
400, "不支持的图片类型或超出大小限制", "请上传 20MB 以内的 JPG/PNG/WebP 图片"
|
||||
@@ -115,7 +115,7 @@ ERROR_META: dict[ErrorCode, ErrorMeta] = {
|
||||
ErrorCode.SMS_DAILY_LIMIT: ErrorMeta(429, "今日发送次数已达上限", "明天再试"),
|
||||
ErrorCode.INTERNAL_ERROR: ErrorMeta(500, "服务开小差了", "请稍后重试"),
|
||||
ErrorCode.DB_ERROR: ErrorMeta(500, "服务开小差了", "请稍后重试"),
|
||||
ErrorCode.TASK_STATE_INVALID: ErrorMeta(500, "任务状态异常", "请到任务中心重试"),
|
||||
ErrorCode.TASK_STATE_INVALID: ErrorMeta(500, "任务状态异常", "请稍后在首页「报告」中查看"),
|
||||
ErrorCode.CONFIG_ERROR: ErrorMeta(500, "服务配置异常", "请联系客服"),
|
||||
ErrorCode.NOT_IMPLEMENTED: ErrorMeta(501, "该能力尚未开放"),
|
||||
ErrorCode.UPSTREAM_ERROR: ErrorMeta(502, "外部服务暂时不可用", "请稍后重试"),
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import zlib
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import UTC, datetime
|
||||
@@ -24,6 +26,7 @@ from apps.api.main import create_app
|
||||
from apps.api.routers.reports import report_export_service_dep
|
||||
from packages.report.dimensions import DIMENSIONS
|
||||
from packages.report.export_service import ExportFile, ExportSubmission
|
||||
from packages.report.pdf import renderer as pdf_renderer
|
||||
from packages.report.pdf.renderer import (
|
||||
ReportPdfMeta,
|
||||
_reject_all_urls,
|
||||
@@ -115,6 +118,61 @@ def test_url_fetcher_rejects_everything_including_file_scheme() -> None:
|
||||
_reject_all_urls(url)
|
||||
|
||||
|
||||
# --- PDF 元数据 AI 声明(CR-05 隐式标识,R4 N2'-①) -----------------------
|
||||
|
||||
|
||||
def _render_template_html() -> str:
|
||||
"""用渲染器同款 Jinja 环境+上下文形状渲染 HTML(与 render_report_pdf 内部一致)。"""
|
||||
template = pdf_renderer._env.get_template("report.html.j2")
|
||||
return template.render(
|
||||
meta=META,
|
||||
date_text=META.generated_at.strftime("%Y年%m月%d日"),
|
||||
summary="",
|
||||
sections=[],
|
||||
dimensions=[],
|
||||
core_dimensions=[],
|
||||
ok_count=0,
|
||||
)
|
||||
|
||||
|
||||
def _decompressed_streams(pdf: bytes) -> list[bytes]:
|
||||
"""解出 PDF 里全部 FlateDecode 流——WeasyPrint 把 Info 字典放进压缩对象流,
|
||||
裸字节 grep 不到 /Author 等标记(zlib 为标准库,不新增依赖)。"""
|
||||
out: list[bytes] = []
|
||||
for m in re.finditer(rb"stream\r?\n", pdf):
|
||||
start = m.end()
|
||||
end = pdf.find(b"endstream", start)
|
||||
if end < 0:
|
||||
continue
|
||||
try:
|
||||
out.append(zlib.decompress(pdf[start:end]))
|
||||
except zlib.error:
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def test_report_html_declares_ai_generation_meta() -> None:
|
||||
html = _render_template_html()
|
||||
assert '<meta name="author" content="美搭(AI 生成内容)">' in html
|
||||
assert 'name="description"' in html
|
||||
# description 携带报告号/版本/日期(ReportPdfMeta 现成数据,不新增字段)
|
||||
assert "报告 101 v1" in html
|
||||
assert "生成于 2026年08月29日" in html
|
||||
|
||||
|
||||
def test_rendered_pdf_carries_ai_metadata() -> None:
|
||||
# WeasyPrint 把 meta author/description/generator 映射进 PDF 信息字典
|
||||
# (Author/Subject/Creator);值编码为带 BOM 的 UTF-16BE 十六进制文本串。
|
||||
pdf = render_report_pdf(_sample_result(), meta=META)
|
||||
joined = b"\n".join(_decompressed_streams(pdf))
|
||||
for marker in (b"/Author", b"/Subject", b"/Creator"):
|
||||
assert marker in joined, f"PDF 信息字典缺 {marker!r}"
|
||||
author_hex = "feff" + "美搭(AI 生成内容)".encode("utf-16-be").hex()
|
||||
assert author_hex.encode("ascii") in joined
|
||||
desc_hex = "feff" + "本报告由人工智能生成,仅供参考。报告 101 v1".encode("utf-16-be").hex()
|
||||
assert desc_hex.encode("ascii") in joined
|
||||
|
||||
|
||||
# --- 路由接线(假服务,不连库) --------------------------------------------
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user