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>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>{{ meta.brand }} · {{ meta.title }}</title>
|
<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>
|
<style>
|
||||||
/* ── 页面与页脚:A4 分页,页脚带 AI 标识 + 免责语(合规 §合规与安全方案)── */
|
/* ── 页面与页脚:A4 分页,页脚带 AI 标识 + 免责语(合规 §合规与安全方案)── */
|
||||||
@page {
|
@page {
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ ERROR_META: dict[ErrorCode, ErrorMeta] = {
|
|||||||
ErrorCode.BAD_REQUEST: ErrorMeta(400, "请求参数有误", "请检查后重试"),
|
ErrorCode.BAD_REQUEST: ErrorMeta(400, "请求参数有误", "请检查后重试"),
|
||||||
ErrorCode.VALIDATION_FAILED: ErrorMeta(422, "请求参数校验未通过", "请按提示修改后重试"),
|
ErrorCode.VALIDATION_FAILED: ErrorMeta(422, "请求参数校验未通过", "请按提示修改后重试"),
|
||||||
ErrorCode.IDEMPOTENCY_CONFLICT: ErrorMeta(
|
ErrorCode.IDEMPOTENCY_CONFLICT: ErrorMeta(
|
||||||
409, "相同请求正在处理中", "请稍后查看任务中心,不要重复提交"
|
409, "相同请求正在处理中", "请稍后在首页「报告」中查看"
|
||||||
),
|
),
|
||||||
ErrorCode.UPLOAD_REJECTED: ErrorMeta(
|
ErrorCode.UPLOAD_REJECTED: ErrorMeta(
|
||||||
400, "不支持的图片类型或超出大小限制", "请上传 20MB 以内的 JPG/PNG/WebP 图片"
|
400, "不支持的图片类型或超出大小限制", "请上传 20MB 以内的 JPG/PNG/WebP 图片"
|
||||||
@@ -115,7 +115,7 @@ ERROR_META: dict[ErrorCode, ErrorMeta] = {
|
|||||||
ErrorCode.SMS_DAILY_LIMIT: ErrorMeta(429, "今日发送次数已达上限", "明天再试"),
|
ErrorCode.SMS_DAILY_LIMIT: ErrorMeta(429, "今日发送次数已达上限", "明天再试"),
|
||||||
ErrorCode.INTERNAL_ERROR: ErrorMeta(500, "服务开小差了", "请稍后重试"),
|
ErrorCode.INTERNAL_ERROR: ErrorMeta(500, "服务开小差了", "请稍后重试"),
|
||||||
ErrorCode.DB_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.CONFIG_ERROR: ErrorMeta(500, "服务配置异常", "请联系客服"),
|
||||||
ErrorCode.NOT_IMPLEMENTED: ErrorMeta(501, "该能力尚未开放"),
|
ErrorCode.NOT_IMPLEMENTED: ErrorMeta(501, "该能力尚未开放"),
|
||||||
ErrorCode.UPSTREAM_ERROR: ErrorMeta(502, "外部服务暂时不可用", "请稍后重试"),
|
ErrorCode.UPSTREAM_ERROR: ErrorMeta(502, "外部服务暂时不可用", "请稍后重试"),
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import zlib
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from datetime import UTC, datetime
|
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 apps.api.routers.reports import report_export_service_dep
|
||||||
from packages.report.dimensions import DIMENSIONS
|
from packages.report.dimensions import DIMENSIONS
|
||||||
from packages.report.export_service import ExportFile, ExportSubmission
|
from packages.report.export_service import ExportFile, ExportSubmission
|
||||||
|
from packages.report.pdf import renderer as pdf_renderer
|
||||||
from packages.report.pdf.renderer import (
|
from packages.report.pdf.renderer import (
|
||||||
ReportPdfMeta,
|
ReportPdfMeta,
|
||||||
_reject_all_urls,
|
_reject_all_urls,
|
||||||
@@ -115,6 +118,61 @@ def test_url_fetcher_rejects_everything_including_file_scheme() -> None:
|
|||||||
_reject_all_urls(url)
|
_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