baodan/tests/ppt_poster_optimization_test.py

825 lines
30 KiB
Python
Raw Normal View History

主要完成内容: 修复 PPT 异步任务无法生成的问题,包括任务变量引用错误、失败状态回写、心跳缺失任务恢复。 脱敏改为保司/产品后台统一配置,生成端不再让用户选择;任务创建时保存策略快照。 保司支持独立控制 PPT、海报 Logo 显示。 PPT 核验新增吸烟状态、币种及三个条件字段。 利益演示、退保提取调整为警告,不再阻止生成。 PPT 生成完成后可以直接返回数据核验页修改。 建立不同险种、单图/长图共六套海报字段画像。 PPT“生成场景”支持后台新增、启停和删除。 保司、产品、PPT 模板、文案模板均支持安全删除。 内置模板禁止删除,只允许停用;存在关联数据时拒绝危险删除。 补充策略变更及删除审计日志。 更新 API 文档、部署文档及修复计划实施记录。 关键交付文件: [数据库迁移 migrate_027.py](D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_027.py) [海报字段画像 field_profiles.py](D:/work/code/python/coding/baodanagent/api/insurance/poster/field_profiles.py) [动态场景服务 scenarios.py](D:/work/code/python/coding/baodanagent/api/insurance/ppt/scenarios.py) [新增回归测试](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) [优化修复计划书](D:/work/code/python/coding/baodanagent/docs/保险智能客服系统_PPT与海报优化修复计划书_20260731.md) 验证结果: 核心链路测试:37 passed,1 skipped 扩展回归测试:140 passed PPT 渲染器测试:6 passed 前端生产构建:通过 Python 编译检查:通过 完整测试集:190 passed,1 failed 唯一失败为 tests/test_chat_save.py::test_chat_logs_query 未建立 Flask application context,与本次 PPT/海报链路无关。
2026-07-31 14:10:24 +08:00
"""PPT/海报优化规则回归测试。"""
import asyncio
主要完成内容: 修复 PPT 异步任务无法生成的问题,包括任务变量引用错误、失败状态回写、心跳缺失任务恢复。 脱敏改为保司/产品后台统一配置,生成端不再让用户选择;任务创建时保存策略快照。 保司支持独立控制 PPT、海报 Logo 显示。 PPT 核验新增吸烟状态、币种及三个条件字段。 利益演示、退保提取调整为警告,不再阻止生成。 PPT 生成完成后可以直接返回数据核验页修改。 建立不同险种、单图/长图共六套海报字段画像。 PPT“生成场景”支持后台新增、启停和删除。 保司、产品、PPT 模板、文案模板均支持安全删除。 内置模板禁止删除,只允许停用;存在关联数据时拒绝危险删除。 补充策略变更及删除审计日志。 更新 API 文档、部署文档及修复计划实施记录。 关键交付文件: [数据库迁移 migrate_027.py](D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_027.py) [海报字段画像 field_profiles.py](D:/work/code/python/coding/baodanagent/api/insurance/poster/field_profiles.py) [动态场景服务 scenarios.py](D:/work/code/python/coding/baodanagent/api/insurance/ppt/scenarios.py) [新增回归测试](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) [优化修复计划书](D:/work/code/python/coding/baodanagent/docs/保险智能客服系统_PPT与海报优化修复计划书_20260731.md) 验证结果: 核心链路测试:37 passed,1 skipped 扩展回归测试:140 passed PPT 渲染器测试:6 passed 前端生产构建:通过 Python 编译检查:通过 完整测试集:190 passed,1 failed 唯一失败为 tests/test_chat_save.py::test_chat_logs_query 未建立 Flask application context,与本次 PPT/海报链路无关。
2026-07-31 14:10:24 +08:00
import sys
from pathlib import Path
from types import SimpleNamespace
主要完成内容: 修复 PPT 异步任务无法生成的问题,包括任务变量引用错误、失败状态回写、心跳缺失任务恢复。 脱敏改为保司/产品后台统一配置,生成端不再让用户选择;任务创建时保存策略快照。 保司支持独立控制 PPT、海报 Logo 显示。 PPT 核验新增吸烟状态、币种及三个条件字段。 利益演示、退保提取调整为警告,不再阻止生成。 PPT 生成完成后可以直接返回数据核验页修改。 建立不同险种、单图/长图共六套海报字段画像。 PPT“生成场景”支持后台新增、启停和删除。 保司、产品、PPT 模板、文案模板均支持安全删除。 内置模板禁止删除,只允许停用;存在关联数据时拒绝危险删除。 补充策略变更及删除审计日志。 更新 API 文档、部署文档及修复计划实施记录。 关键交付文件: [数据库迁移 migrate_027.py](D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_027.py) [海报字段画像 field_profiles.py](D:/work/code/python/coding/baodanagent/api/insurance/poster/field_profiles.py) [动态场景服务 scenarios.py](D:/work/code/python/coding/baodanagent/api/insurance/ppt/scenarios.py) [新增回归测试](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) [优化修复计划书](D:/work/code/python/coding/baodanagent/docs/保险智能客服系统_PPT与海报优化修复计划书_20260731.md) 验证结果: 核心链路测试:37 passed,1 skipped 扩展回归测试:140 passed PPT 渲染器测试:6 passed 前端生产构建:通过 Python 编译检查:通过 完整测试集:190 passed,1 failed 唯一失败为 tests/test_chat_save.py::test_chat_logs_query 未建立 Flask application context,与本次 PPT/海报链路无关。
2026-07-31 14:10:24 +08:00
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "api"))
def test_currency_aliases_and_unknown_are_not_fabricated():
from insurance.ppt.normalizer import _normalize_currency
assert _normalize_currency("USB") == "USD"
assert _normalize_currency("RMB") == "CNY"
assert _normalize_currency("") == "CNY"
assert _normalize_currency("XYZ") is None
assert _normalize_currency(None) is None
def test_optional_review_fields_are_normalized_without_becoming_required():
from insurance.ppt.normalizer import normalize_savings_plan
plan = normalize_savings_plan({
"product_name": "储蓄计划",
"insured": {"age": 35, "gender": "", "smoker": ""},
"policy": {
"currency": "RMB",
"annual_premium": 10000,
"premium_payment_period": 5,
"basic_plan_annual_premium": 9000,
"basic_sum_insured": 500000,
"first_year_amount_due": 9800,
},
})
assert plan["insured"]["smoker"] == "no"
assert plan["policy"]["currency"] == "CNY"
assert plan["policy"]["basicPlanAnnualPremium"] == 9000
assert plan["policy"]["basicSumInsured"] == 500000
assert plan["policy"]["firstYearAmountDue"] == 9800
主要修复: 海报加载失败根因:html-to-image 给 blob: 底图地址追加缓存参数,导致地址失效。现已关闭该行为。 合成失败后再次点击“重新生成”,会复用已有 AI 底图,只重试浏览器合成和上传,避免重复调用 AI。 增加底图加载、图表超时、导出失败、尺寸越界等分阶段错误提示。 修复计划书 (cid:数字) 字体乱码被误判为正常文本的问题,现在会正确转入 OCR。 增加繁体中文 OCR 运行支持。 补齐 SIUL 文件名中的确定字段,并且不会覆盖正文已识别数据。 增加“首期规划保费/償還至形成基金所需保費”等保费标签识别。 修正 IUL 年龄、保额、退保价值、缴费期、公司信息等字段映射。 LLM 返回空对象或缺字段时不再视为成功。 增加错误利益数值和年龄/保单年度错位校验。 修复依赖版本降级导致 API/Worker 无法启动的风险。 真实计划书复验结果: 产品:Manulife SIUL 3 投保年龄:48 岁 性别:女性 吸烟状态:非吸烟 币种:USD 基本保额:3,000,000 年缴保费:80,060 缴费期:5 年 利益演示:识别到 10 行 验证结果: 后端相关回归测试:91 passed 前端生产构建:通过 API、数据库、Redis、存储、数据表健康检查:全部正常 Celery Worker:已重启并连接 Redis 真实 PDF:确认进入 OCR,不再使用 (cid:...) 乱码 前端构建目录由运行容器挂载,修复已生效
2026-07-31 23:35:45 +08:00
def test_iul_key_fields_accept_raw_and_normalized_names_without_data_loss():
from insurance.ppt.normalizer import normalize_iul_plan
plan = normalize_iul_plan({
"productName": "Manulife SIUL 3",
"insured": {"age": 48, "gender": "female", "smoker": "non-smoker"},
"policy": {
"currency": "USD",
"sumInsured": 3000000,
"annualPremium": 80060,
"payYears": 5,
"coverage_period": "终身",
},
"benefit_illustration": [
{"policy_year": 1, "total_surrender_value": 31600},
{"policy_year": 10, "total_surrender_value": 76800},
],
})
assert plan["productName"] == "Manulife SIUL 3"
assert plan["insured"]["age"] == 48
assert plan["insured"]["smoker"] == "no"
assert plan["policy"]["sumInsured"] == 3000000
assert plan["policy"]["annualPremium"] == 80060
assert plan["policy"]["payYears"] == 5
assert plan["benefitRows"][0]["age"] == 48
assert plan["benefitRows"][1]["age"] == 57
assert plan["benefitRows"][1]["totalSurrenderValue"] == 76800
def test_iul_implausibly_tiny_benefit_values_block_generation():
from insurance.ppt.validator import validate_formal_iul_plan
issues = validate_formal_iul_plan({
"productName": "Manulife SIUL 3",
"insured": {"age": 48, "smoker": "no"},
"policy": {
"currency": "USD", "sumInsured": 3000000,
"annualPremium": 80060, "paymentPeriod": "5",
},
"indexAccounts": [{"name": "S&P 500"}],
"benefitRows": [
{"policyYear": year, "age": 48 + year - 1, "totalSurrenderValue": value}
for year, value in [(1, 56), (10, 85), (20, 114), (30, 122)]
],
"source": {"pdfHash": "hash"},
})
assert any(
issue.code == "IUL_BENEFIT_VALUE_IMPLAUSIBLE" and issue.level == "error"
for issue in issues
)
def test_ppt_generation_preserves_company_selected_for_uploaded_file():
source = (
Path(__file__).resolve().parents[1]
/ "api/insurance/generation/celery_tasks.py"
).read_text(encoding="utf-8")
assert 'normalized["companyId"] = ext.get("companyId") or company_id or ""' in source
assert "if not company_id:" in source
主要完成内容: 修复 PPT 异步任务无法生成的问题,包括任务变量引用错误、失败状态回写、心跳缺失任务恢复。 脱敏改为保司/产品后台统一配置,生成端不再让用户选择;任务创建时保存策略快照。 保司支持独立控制 PPT、海报 Logo 显示。 PPT 核验新增吸烟状态、币种及三个条件字段。 利益演示、退保提取调整为警告,不再阻止生成。 PPT 生成完成后可以直接返回数据核验页修改。 建立不同险种、单图/长图共六套海报字段画像。 PPT“生成场景”支持后台新增、启停和删除。 保司、产品、PPT 模板、文案模板均支持安全删除。 内置模板禁止删除,只允许停用;存在关联数据时拒绝危险删除。 补充策略变更及删除审计日志。 更新 API 文档、部署文档及修复计划实施记录。 关键交付文件: [数据库迁移 migrate_027.py](D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_027.py) [海报字段画像 field_profiles.py](D:/work/code/python/coding/baodanagent/api/insurance/poster/field_profiles.py) [动态场景服务 scenarios.py](D:/work/code/python/coding/baodanagent/api/insurance/ppt/scenarios.py) [新增回归测试](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) [优化修复计划书](D:/work/code/python/coding/baodanagent/docs/保险智能客服系统_PPT与海报优化修复计划书_20260731.md) 验证结果: 核心链路测试:37 passed,1 skipped 扩展回归测试:140 passed PPT 渲染器测试:6 passed 前端生产构建:通过 Python 编译检查:通过 完整测试集:190 passed,1 failed 唯一失败为 tests/test_chat_save.py::test_chat_logs_query 未建立 Flask application context,与本次 PPT/海报链路无关。
2026-07-31 14:10:24 +08:00
def test_benefit_and_withdrawal_issues_never_block_savings_generation():
from insurance.ppt.validator import validate_formal_savings_plan
issues = validate_formal_savings_plan({
"productName": "储蓄计划",
"insured": {"age": 35, "smoker": "no"},
"policy": {"currency": "USD", "annualPremium": 10000, "payYears": 5},
"benefitRows": [
{
"policyYear": 1,
"guaranteedCashValue": 100,
"totalSurrenderValue": 50,
}
],
"withdrawalRows": [{"policyYear": 1}, {"policyYear": 3}],
"source": {"pdfHash": "hash"},
})
affected = [
issue for issue in issues
if issue.section in ("benefitRows", "withdrawalRows")
]
assert affected
assert all(issue.level == "warn" for issue in affected)
主要改动: 修正 benefit_illustration JSON 契约,兼容安全的数组返回格式,并把具体缺失字段反馈给 LLM 重试。[llm_client.py (line 184)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/llm_client.py:184) LLM 利益表失败时保留有效正则结果,不再把已有数据全部丢掉。[extraction.py (line 1237)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:1237) Female、Male、男女等误识别结果会用用户选择的产品名纠正。[extraction.py (line 521)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:521) 解析缓存升级至 v7,旧错误缓存自动失效。 新增真正的“退保价值”页签,数据来自利益演示中的年度退保价值,不再错误依赖提领数据。[PptDataReview.vue (line 318)](D:/work/code/python/coding/baodanagent/frontend/src/pages/components/ppt/PptDataReview.vue:318) “提领方案”继续作为独立可选情景;0 行不再显示待校对,也不会影响生成 PPT。[validator.py (line 98)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/validator.py:98) 提领表识别支持跨行标题及“提领/领取”等名称。[regex_extractor.py (line 619)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/regex_extractor.py:619) 补充了对应回归测试。[ppt_poster_optimization_test.py](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) 验证结果: PPT/解析专项测试:51 passed, 1 skipped 新增问题回归测试:32 passed 前端生产构建:通过 Python 语法检查:通过 git diff --check:通过
2026-08-01 01:56:33 +08:00
def test_missing_optional_withdrawal_plan_does_not_require_review():
from insurance.ppt.validator import validate_formal_savings_plan
issues = validate_formal_savings_plan({
"productName": "储蓄计划",
"insured": {"age": 35, "smoker": "no"},
"policy": {"currency": "USD", "annualPremium": 10000, "payYears": 5},
"benefitRows": [
{"policyYear": year, "sourcePage": 10, "totalSurrenderValue": year * 1000}
for year in range(1, 31)
],
"withdrawalRows": [],
"source": {"pdfHash": "hash"},
})
assert not any(issue.code == "WITHDRAWAL_ROWS_MISSING" for issue in issues)
主要完成内容: 修复 PPT 异步任务无法生成的问题,包括任务变量引用错误、失败状态回写、心跳缺失任务恢复。 脱敏改为保司/产品后台统一配置,生成端不再让用户选择;任务创建时保存策略快照。 保司支持独立控制 PPT、海报 Logo 显示。 PPT 核验新增吸烟状态、币种及三个条件字段。 利益演示、退保提取调整为警告,不再阻止生成。 PPT 生成完成后可以直接返回数据核验页修改。 建立不同险种、单图/长图共六套海报字段画像。 PPT“生成场景”支持后台新增、启停和删除。 保司、产品、PPT 模板、文案模板均支持安全删除。 内置模板禁止删除,只允许停用;存在关联数据时拒绝危险删除。 补充策略变更及删除审计日志。 更新 API 文档、部署文档及修复计划实施记录。 关键交付文件: [数据库迁移 migrate_027.py](D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_027.py) [海报字段画像 field_profiles.py](D:/work/code/python/coding/baodanagent/api/insurance/poster/field_profiles.py) [动态场景服务 scenarios.py](D:/work/code/python/coding/baodanagent/api/insurance/ppt/scenarios.py) [新增回归测试](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) [优化修复计划书](D:/work/code/python/coding/baodanagent/docs/保险智能客服系统_PPT与海报优化修复计划书_20260731.md) 验证结果: 核心链路测试:37 passed,1 skipped 扩展回归测试:140 passed PPT 渲染器测试:6 passed 前端生产构建:通过 Python 编译检查:通过 完整测试集:190 passed,1 failed 唯一失败为 tests/test_chat_save.py::test_chat_logs_query 未建立 Flask application context,与本次 PPT/海报链路无关。
2026-07-31 14:10:24 +08:00
def test_brand_policy_controls_company_product_and_logo_independently():
from insurance.ppt.masking import apply_brand_policy, build_brand_policy
company = {
"id": "c1",
"displayName": "真实保司",
"maskedDisplayName": "保X",
"maskingEnabled": True,
"logoEnabled": False,
"logoUrl": "/logo.png",
}
product = {
"id": "p1",
"displayName": "真实产品",
"maskedDisplayName": "产X",
"maskingEnabled": False,
}
policy = build_brand_policy(company, [product])
masked_company, masked_product = apply_brand_policy(company, product, policy)
assert masked_company["displayName"] == "保X"
assert masked_company["logoUrl"] == ""
assert masked_product["displayName"] == "真实产品"
def test_all_six_core_poster_profiles_have_distinct_content_budgets():
from insurance.poster.field_profiles import get_field_profile
profiles = {
(plan_type, output_mode): get_field_profile(plan_type, output_mode)
for plan_type in ("savings", "ci", "iul")
for output_mode in ("single", "long")
}
assert len(profiles) == 6
for plan_type in ("savings", "ci", "iul"):
assert profiles[(plan_type, "single")]["maxFeatureCount"] == 3
assert profiles[(plan_type, "long")]["maxFeatureCount"] == 6
def test_single_poster_never_contains_full_benefit_table():
from insurance.poster.content_builder import build_poster_content
rows = [
{"policy_year": year, "total_surrender": year * 1000}
for year in range(1, 31)
]
content = build_poster_content(
{"benefit_illustration": rows},
{"features": [{"title": str(index)} for index in range(8)]},
output_mode="single",
plan_type="savings",
)
assert len(content["benefit_table"]) <= 1
assert len(content["features"]) <= 3
def test_custom_scenario_is_compatible_only_with_its_calculation_mode(monkeypatch):
from insurance.ppt import scenarios
monkeypatch.setattr(scenarios, "get_scenario_config", lambda _code: {
"generationMode": "single",
"baseScenario": None,
})
assert scenarios.template_scenario_compatible("retirement", "generic_single")
assert not scenarios.template_scenario_compatible("retirement", "generic_compare")
已按计划书完成本轮核心 P0/P1 修复,覆盖用户反馈的三个主要问题。 主要结果: PPT: 已完成步骤可自由返回,刷新后保留当前步骤。 结果页改为固定三栏视口,缩略图和质量面板独立滚动。 修复画布宽度计算和宽高自适应缩放。 返回配置页时恢复上次模板。 Worker 禁止静默替换模板,不兼容时明确失败。 结果页显示实际应用的模板。 海报解析与合规: 修复 insured/policy 嵌套字段映射。 增加 partial/failed 解析质量判断和缺失字段提示。 传入险种、产品、保司和别名上下文。 修复性别、币种归一化和错误字段计数。 新增服务端字符级合规接口与生成前复检。 不合规文字可高亮、点击定位并选中对应文字。 海报编辑: AI 图片现在作为背景资产,不再替换整个海报。 单图、长图生成后仍可编辑标题、正文、CTA、数据和卖点。 背景生成失败不会清空当前编辑内容。 下载时合成背景、文字、数据、图表和免责声明。 增加可编辑文档快照、最终 PNG 保存和历史继续编辑。 新增数据库迁移:[migrate_028.py](/D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_028.py) 验证结果: 专项及相关后端测试:26 passed, 1 skipped Python 全模块编译检查:通过 前端生产构建:通过 git diff --check:通过 全量测试收集受本机缺少 python-pptx 依赖影响,报错为 ModuleNotFoundError: pptx,不是本次修改产生的测试失败。
2026-07-31 15:20:37 +08:00
def test_poster_case_mapping_reads_nested_insured_and_policy_fields():
from insurance.poster.tasks import _map_extract_plan_fields
mapped = _map_extract_plan_fields({
"insured": {"age": 35, "gender": "female"},
"policy": {
"currency": "RMB",
"sum_insured": 500000,
"annual_premium": 100000,
"premium_payment_period": 5,
"coverage_period": "终身",
},
"benefit_illustration": [{"policy_year": 10, "total_surrender_value": 800000}],
}, "savings", "success")
assert mapped["age"] == 35
assert mapped["gender"] == ""
assert mapped["currency"] == "CNY"
assert mapped["sum_assured"] == 500000
assert mapped["annual_premium"] == 100000
assert mapped["premium_term"] == 5
assert mapped["coverage_period"] == "终身"
assert mapped["benefit_table"][0]["policy_year"] == 10
assert mapped["meta"]["status"] == "parsed"
主要改动: PPT 表格现在按槽位解析,正确保留 —、空列和单空格表格,不再发生金额左移。[regex_extractor.py (line 470)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/regex_extractor.py:470) 补齐年龄、累计保费、非保证现金价值、非保证身故赔偿等别名。 IUL 使用专属 LLM 提取结构,并加强年度、年龄、账户价值完整性校验。[extraction.py (line 741)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:741) PPT 缓存升级到 v6,旧错误缓存会自动失效。 提领方案不再与普通退保价值混淆,相关提示语已统一。 海报摘要年度金额与收益图表改为同一数据源;缺少部分里程碑时会从原始真实年度补足,不会隐藏图表。[content_builder.py (line 51)](D:/work/code/python/coding/baodanagent/api/insurance/poster/content_builder.py:51) 系统计算、利益表派生、人工修改增加来源标记。 增加“重新解析”功能,旧海报计划书无需重新上传。[routes.py (line 266)](D:/work/code/python/coding/baodanagent/api/insurance/poster/routes.py:266) ECharts 导出增加双帧就绪检测和事件竞态保护,不再依赖不存在的 .once()。 PPT 手机端改为可编辑数据卡片,320px 操作栏自动纵向排列;补齐按钮语义、键盘焦点及 44px 触控区域。[PptDataReview.vue (line 195)](D:/work/code/python/coding/baodanagent/frontend/src/pages/components/ppt/PptDataReview.vue:195) 验证结果: 核心专项测试:145 passed 除既有聊天日志测试外的测试集:236 passed 前端生产构建:通过 Python 语法检查:通过 git diff --check:通过 Impeccable 前端检测:无发现 全量测试仅剩一个与本次无关的既有失败:test_chat_logs_query 缺少 Flask application context
2026-08-01 01:22:07 +08:00
def test_poster_case_mapping_keeps_product_and_surrender_milestones():
from insurance.poster.tasks import _map_extract_plan_fields
mapped = _map_extract_plan_fields({
"insured": {"age": 35, "gender": "female", "smoker_status": "no"},
"policy": {
"currency": "USD", "sum_insured": 500000,
"annual_premium": 10000, "premium_payment_period": 5,
},
"benefit_illustration": [
{"policy_year": 1, "death_benefit": 500000, "total_surrender_value": 1000},
{"policy_year": 10, "total_surrender_value": 31600},
{"policy_year": 20, "total_surrender_value": 76800},
{"policy_year": 30, "total_surrender_value": 129300},
],
}, "savings", "success", product_context={
"productName": "宏挚传承",
"companyName": "宏利",
})
assert mapped["product_name"] == "宏挚传承"
assert mapped["company_name"] == "宏利"
assert mapped["smoking_status"] == "非吸烟"
assert mapped["total_premium"] == 50000
assert mapped["initial_death_benefit"] == 500000
assert mapped["surrender_value_10"] == 31600
assert mapped["surrender_value_20"] == 76800
assert mapped["surrender_value_30"] == 129300
provenance = mapped["meta"]["provenance"]
assert provenance["total_premium"]["source"] == "system_derived"
assert provenance["initial_death_benefit"]["source"] == "system_derived"
assert provenance["surrender_value_20"]["from"] == "benefit_table.year_20"
def test_poster_content_accepts_canonical_snake_case_benefit_fields():
from insurance.poster.content_builder import build_poster_content
content = build_poster_content({
"benefit_illustration": [
{
"policy_year": year,
"guaranteed_cash_value": year * 100,
"total_surrender_value": year * 1000,
}
for year in (10, 20, 30)
],
}, {}, output_mode="long", plan_type="savings")
assert content["benefit_table"][0]["guaranteed"] == 1000
assert content["benefit_table"][0]["totalSurrender"] == 10000
def test_regex_benefit_table_keeps_age_separate_from_money():
from insurance.ppt.regex_extractor import extract_insurance_regex
data = extract_insurance_regex("""
产品名称Manulife SIUL 3
受保人年龄47
年缴保费80,060
保单年度 受保人年龄 缴付保费总额 退保总值
1 48 80,060 31,600
8 55 640,480 76,800
""")
first = data["benefit_illustration"][0]
assert first["policy_year"] == 1
assert first["age"] == 48
assert first["total_premium_paid"] == 80060
assert first["total_surrender_value"] == 31600
def test_regex_age_only_table_derives_policy_year_from_explicit_issue_age():
from insurance.ppt.regex_extractor import extract_insurance_regex
data = extract_insurance_regex("""
产品名称储蓄计划
受保人年龄47
年缴保费10,000
受保人年龄 退保价值
48 1,000
49 2,000
""")
rows = data["benefit_illustration"]
assert [row["policy_year"] for row in rows] == [1, 2]
assert [row["age"] for row in rows] == [48, 49]
assert rows[0]["policy_year_source"] == "derived_from_age"
def test_regex_iul_table_preserves_dash_placeholders_without_column_shift():
from insurance.ppt.regex_extractor import extract_insurance_regex
data = extract_insurance_regex("""
产品名称Manulife SIUL 3
受保人年龄47
年缴保费80,060
保单年度 年龄 累计保费 保证现金价值 保证账户价值 非保证账户价值 非保证现金价值 退保总值 非保证身故赔偿
37 2,962,220 2,000 1,000 85 10,000
44 3,522,640 3,000 2,000 92 12,000
""")
row = data["benefit_illustration"][0]
assert row["policy_year"] == 37
assert row["age"] is None
assert row["total_premium_paid"] == 2962220
assert row["guaranteed_cash_value"] == 2000
assert row["guaranteed_account_value"] is None
assert row["non_guaranteed_account_value"] is None
assert row["non_guaranteed_cash_value"] == 1000
assert row["total_surrender_value"] == 85
assert row["non_guaranteed_death_benefit"] == 10000
assert data["benefit_illustration"][1]["total_premium_paid"] == 3522640
assert data["benefit_illustration"][1]["total_surrender_value"] == 92
def test_regex_quality_gate_rejects_age_like_surrender_values():
from insurance.ppt.extraction import _regex_quality_gate
passed, problems = _regex_quality_gate({
"product_name": "Manulife SIUL 3",
"insured": {"age": 47},
"policy": {"sum_insured": 3000000, "annual_premium": 80060},
"index_accounts": [{"name": "S&P 500"}],
"benefit_illustration": [
{"policy_year": year, "total_surrender_value": age}
for year, age in ((1, 49), (8, 56), (37, 85), (44, 92))
],
}, "iul")
assert passed is False
assert "benefit_illustration.total_surrender_value_implausible" in problems
def test_regex_quality_gate_rejects_iul_rows_without_iul_value_columns():
from insurance.ppt.extraction import _regex_quality_gate
passed, problems = _regex_quality_gate({
"product_name": "Manulife SIUL 3",
"insured": {"age": 47},
"policy": {"sum_insured": 3000000, "annual_premium": 80060},
"index_accounts": [{"name": "S&P 500"}],
"benefit_illustration": [
{"policy_year": year, "total_premium_paid": 80060 * year, "total_surrender_value": 10000 * year}
for year in (1, 8, 20)
],
}, "iul")
assert passed is False
assert "benefit_illustration.iul_value_columns" in problems
def test_iul_split_fallback_requests_iul_specific_fields():
import asyncio
from types import SimpleNamespace
from insurance.ppt.extraction import _llm_extract_split
prompts = []
class FakeClient:
async def structured_output(self, **kwargs):
prompt = kwargs["prompt"]
prompts.append(prompt)
if "身份和保单字段" in prompt:
return {
"product_name": "SIUL",
"insured": {"age": 47, "gender": "male"},
"policy": {"sum_insured": 3000000},
"index_accounts": [{"name": "S&P 500"}],
}, SimpleNamespace(tokens={})
if "利益演示表" in prompt:
return {"benefit_illustration": []}, SimpleNamespace(tokens={})
return {"withdrawal_illustration": []}, SimpleNamespace(tokens={})
主要修改: 利益表按最多两页、9000字符分块调用 LLM,成功分块按保单年度合并、去重、排序。[extraction.py (line 125)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:125) 身份字段只发送关键页面;没有提领关键词时不再发送整份 PDF 做空提领请求。 单个利益分块失败不会丢失其他成功分块,并记录具体分块编号和错误。[extraction.py (line 1161)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:1161) RemoteProtocolError、连接超时、408/429/部分5xx现在会按配置执行真实退避重试;DeepSeek默认最多调用3次。[llm_client.py (line 600)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/llm_client.py:600) 只有一个供应商时,错误信息会明确显示“已配置1个供应商”,不再误导为存在多个备用供应商。 利益表分块失败或储蓄险年度数据不足时,结果标记为 partial,不再伪装完整成功。[extraction.py (line 750)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:750) 会话显示“解析完成,部分文件需校对”,同时保留可人工修正的数据。 日志增加文件名、文件序号、最终状态、利益行数,以及身份/利益各分块/提领的独立耗时、token和错误信息。[celery_tasks.py (line 181)](D:/work/code/python/coding/baodanagent/api/insurance/generation/celery_tasks.py:181) 缓存版本升级至 v8,旧解析缓存自动失效。 验证结果: PPT专项测试:54 passed, 1 skipped 大范围测试:231 passed, 1 skipped 仅剩既有聊天日志测试缺少 Flask application context 完整测试收集另受本机缺少 python-pptx 影响 Python语法检查:通过 前端生产构建:通过 git diff --check:通过
2026-08-01 02:25:35 +08:00
asyncio.run(_llm_extract_split(
"[PAGE 1]\nIUL 计划书\n\n"
"[PAGE 2]\n提领方案 保单年度 提取金额 退保价值",
"iul",
FakeClient(),
))
主要改动: PPT 表格现在按槽位解析,正确保留 —、空列和单空格表格,不再发生金额左移。[regex_extractor.py (line 470)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/regex_extractor.py:470) 补齐年龄、累计保费、非保证现金价值、非保证身故赔偿等别名。 IUL 使用专属 LLM 提取结构,并加强年度、年龄、账户价值完整性校验。[extraction.py (line 741)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:741) PPT 缓存升级到 v6,旧错误缓存会自动失效。 提领方案不再与普通退保价值混淆,相关提示语已统一。 海报摘要年度金额与收益图表改为同一数据源;缺少部分里程碑时会从原始真实年度补足,不会隐藏图表。[content_builder.py (line 51)](D:/work/code/python/coding/baodanagent/api/insurance/poster/content_builder.py:51) 系统计算、利益表派生、人工修改增加来源标记。 增加“重新解析”功能,旧海报计划书无需重新上传。[routes.py (line 266)](D:/work/code/python/coding/baodanagent/api/insurance/poster/routes.py:266) ECharts 导出增加双帧就绪检测和事件竞态保护,不再依赖不存在的 .once()。 PPT 手机端改为可编辑数据卡片,320px 操作栏自动纵向排列;补齐按钮语义、键盘焦点及 44px 触控区域。[PptDataReview.vue (line 195)](D:/work/code/python/coding/baodanagent/frontend/src/pages/components/ppt/PptDataReview.vue:195) 验证结果: 核心专项测试:145 passed 除既有聊天日志测试外的测试集:236 passed 前端生产构建:通过 Python 语法检查:通过 git diff --check:通过 Impeccable 前端检测:无发现 全量测试仅剩一个与本次无关的既有失败:test_chat_logs_query 缺少 Flask application context
2026-08-01 01:22:07 +08:00
assert "index_accounts" in prompts[0]
assert "guaranteed_account_value" in prompts[1]
assert "non_guaranteed_cash_value" in prompts[1]
assert "non_guaranteed_death_benefit" in prompts[1]
主要改动: 修正 benefit_illustration JSON 契约,兼容安全的数组返回格式,并把具体缺失字段反馈给 LLM 重试。[llm_client.py (line 184)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/llm_client.py:184) LLM 利益表失败时保留有效正则结果,不再把已有数据全部丢掉。[extraction.py (line 1237)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:1237) Female、Male、男女等误识别结果会用用户选择的产品名纠正。[extraction.py (line 521)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:521) 解析缓存升级至 v7,旧错误缓存自动失效。 新增真正的“退保价值”页签,数据来自利益演示中的年度退保价值,不再错误依赖提领数据。[PptDataReview.vue (line 318)](D:/work/code/python/coding/baodanagent/frontend/src/pages/components/ppt/PptDataReview.vue:318) “提领方案”继续作为独立可选情景;0 行不再显示待校对,也不会影响生成 PPT。[validator.py (line 98)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/validator.py:98) 提领表识别支持跨行标题及“提领/领取”等名称。[regex_extractor.py (line 619)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/regex_extractor.py:619) 补充了对应回归测试。[ppt_poster_optimization_test.py](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) 验证结果: PPT/解析专项测试:51 passed, 1 skipped 新增问题回归测试:32 passed 前端生产构建:通过 Python 语法检查:通过 git diff --check:通过
2026-08-01 01:56:33 +08:00
assert "根对象只能包含 benefit_illustration 字段" in prompts[1]
assert "根对象只能包含 withdrawal_illustration 字段" in prompts[2]
主要修改: 利益表按最多两页、9000字符分块调用 LLM,成功分块按保单年度合并、去重、排序。[extraction.py (line 125)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:125) 身份字段只发送关键页面;没有提领关键词时不再发送整份 PDF 做空提领请求。 单个利益分块失败不会丢失其他成功分块,并记录具体分块编号和错误。[extraction.py (line 1161)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:1161) RemoteProtocolError、连接超时、408/429/部分5xx现在会按配置执行真实退避重试;DeepSeek默认最多调用3次。[llm_client.py (line 600)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/llm_client.py:600) 只有一个供应商时,错误信息会明确显示“已配置1个供应商”,不再误导为存在多个备用供应商。 利益表分块失败或储蓄险年度数据不足时,结果标记为 partial,不再伪装完整成功。[extraction.py (line 750)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:750) 会话显示“解析完成,部分文件需校对”,同时保留可人工修正的数据。 日志增加文件名、文件序号、最终状态、利益行数,以及身份/利益各分块/提领的独立耗时、token和错误信息。[celery_tasks.py (line 181)](D:/work/code/python/coding/baodanagent/api/insurance/generation/celery_tasks.py:181) 缓存版本升级至 v8,旧解析缓存自动失效。 验证结果: PPT专项测试:54 passed, 1 skipped 大范围测试:231 passed, 1 skipped 仅剩既有聊天日志测试缺少 Flask application context 完整测试收集另受本机缺少 python-pptx 影响 Python语法检查:通过 前端生产构建:通过 git diff --check:通过
2026-08-01 02:25:35 +08:00
def test_split_extraction_chunks_benefit_pages_and_merges_rows():
import asyncio
import re
from types import SimpleNamespace
from insurance.ppt.extraction import _llm_extract_split
prompts = []
class FakeClient:
async def structured_output(self, **kwargs):
prompt = kwargs["prompt"]
prompts.append(prompt)
response = SimpleNamespace(tokens={"input": 10, "output": 5}, latency_ms=10)
if "身份和保单字段" in prompt:
return {
"product_name": "储蓄计划",
"insured": {"age": 35, "gender": "female"},
"policy": {"currency": "USD", "sum_insured": None,
"annual_premium": 10000, "premium_payment_period": 5},
}, response
page_numbers = [int(value) for value in re.findall(r"\[PAGE (\d+)\]", prompt)]
return {
"benefit_illustration": [
{"policy_year": page, "total_surrender_value": page * 1000, "source_page": page}
for page in page_numbers
],
}, response
pdf_text = "[PAGE 1]\n产品名称:储蓄计划\n受保人年龄35\n"
pdf_text += "\n\n".join(
f"[PAGE {page}]\n保单年度 保证现金价值 退保价值\n{page} 100 {page * 1000}"
for page in range(2, 7)
)
data, _response = asyncio.run(_llm_extract_split(pdf_text, "savings", FakeClient()))
benefit_prompts = [
prompt for prompt in prompts
if "利益演示表" in prompt and "当前分块" in prompt
]
assert len(benefit_prompts) == 3
assert [row["policy_year"] for row in data["benefit_illustration"]] == [2, 3, 4, 5, 6]
assert data["_meta"]["split_extraction"]["benefit"]["status"] == "success"
assert data["_meta"]["split_extraction"]["benefit"]["chunkCount"] == 3
def test_incomplete_savings_benefit_rows_are_partial():
from insurance.ppt.extraction import assess_extraction_payload
status, message = assess_extraction_payload({
"product_name": "储蓄计划",
"insured": {"age": 35},
"policy": {"annual_premium": 10000},
"benefit_illustration": [
{"policy_year": year, "total_surrender_value": year * 1000}
for year in (1, 10, 20)
],
"_meta": {
"split_extraction": {
"benefit": {"status": "failed", "errors": ["RemoteProtocolError"]},
},
},
}, "savings")
assert status == "partial"
assert "利益演示数据不足" in message
def test_llm_client_retries_retryable_transport_error(monkeypatch):
import asyncio
import httpx
from insurance.ppt import llm_client as llm_module
client = llm_module.LLMClient.__new__(llm_module.LLMClient)
config = llm_module.LLMProviderConfig(
name="deepseek", base_url="https://example.com/v1", model="model", max_retries=2,
)
client._configs = [(config, "secret")]
client._limiters = {}
client._active_idx = 0
client._timeout_ms = 180000
client._try_load_db_config = lambda: None
attempts = []
async def fake_provider(*_args, **_kwargs):
attempts.append(1)
if len(attempts) == 1:
raise httpx.RemoteProtocolError("incomplete chunked read")
return llm_module.LLMResponse(content='{"ok": true}', provider="deepseek")
async def no_wait(_seconds):
return None
monkeypatch.setattr(llm_module, "_call_provider", fake_provider)
monkeypatch.setattr(llm_module.asyncio, "sleep", no_wait)
response = asyncio.run(client._call([{"role": "user", "content": "test"}], json_mode=True))
assert response.provider == "deepseek"
assert len(attempts) == 2
主要改动: 修正 benefit_illustration JSON 契约,兼容安全的数组返回格式,并把具体缺失字段反馈给 LLM 重试。[llm_client.py (line 184)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/llm_client.py:184) LLM 利益表失败时保留有效正则结果,不再把已有数据全部丢掉。[extraction.py (line 1237)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:1237) Female、Male、男女等误识别结果会用用户选择的产品名纠正。[extraction.py (line 521)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:521) 解析缓存升级至 v7,旧错误缓存自动失效。 新增真正的“退保价值”页签,数据来自利益演示中的年度退保价值,不再错误依赖提领数据。[PptDataReview.vue (line 318)](D:/work/code/python/coding/baodanagent/frontend/src/pages/components/ppt/PptDataReview.vue:318) “提领方案”继续作为独立可选情景;0 行不再显示待校对,也不会影响生成 PPT。[validator.py (line 98)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/validator.py:98) 提领表识别支持跨行标题及“提领/领取”等名称。[regex_extractor.py (line 619)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/regex_extractor.py:619) 补充了对应回归测试。[ppt_poster_optimization_test.py](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) 验证结果: PPT/解析专项测试:51 passed, 1 skipped 新增问题回归测试:32 passed 前端生产构建:通过 Python 语法检查:通过 git diff --check:通过
2026-08-01 01:56:33 +08:00
def test_single_required_array_schema_normalizes_safe_equivalent_roots():
from insurance.ppt.llm_client import _normalize_schema_root
schema = {
"type": "object",
"required": ["benefit_illustration"],
"properties": {"benefit_illustration": {"type": "array"}},
}
assert _normalize_schema_root([{"policy_year": 1}], schema) == {
"benefit_illustration": [{"policy_year": 1}],
}
assert _normalize_schema_root({"rows": [{"policy_year": 2}]}, schema) == {
"benefit_illustration": [{"policy_year": 2}],
}
assert _normalize_schema_root({"message": "not rows"}, schema) == {"message": "not rows"}
assert _normalize_schema_root({"message": []}, schema) == {"message": []}
def test_structured_output_repair_receives_the_missing_field_error():
import asyncio
from types import SimpleNamespace
from insurance.ppt.llm_client import LLMClient
client = LLMClient.__new__(LLMClient)
responses = iter([
SimpleNamespace(content='{"message": "missing root"}'),
SimpleNamespace(content='{"benefit_illustration": []}'),
])
calls = []
async def fake_call(messages, **_kwargs):
calls.append(messages)
return next(responses)
client._call = fake_call
schema = {
"type": "object",
"required": ["benefit_illustration"],
"properties": {"benefit_illustration": {"type": "array"}},
}
parsed, _response = asyncio.run(client.structured_output("extract", schema=schema))
assert parsed == {"benefit_illustration": []}
assert "缺少必填字段: benefit_illustration" in calls[1][-1]["content"]
def test_withdrawal_regex_accepts_title_on_previous_line():
from insurance.ppt.regex_extractor import extract_insurance_regex
data = extract_insurance_regex("""
产品名称储蓄计划
受保人年龄35
年缴保费10,000
计划提领方案
保单年度 total withdrawn surrender value after
10 50,000 180,000
20 150,000 360,000
""")
rows = data["withdrawal_illustration"]
assert [row["policy_year"] for row in rows] == [10, 20]
assert rows[0]["total_withdrawn"] == 50000
def test_gender_token_is_not_accepted_as_product_name():
from insurance.ppt.extraction import _is_obviously_invalid_product_name
assert _is_obviously_invalid_product_name("Female")
assert _is_obviously_invalid_product_name("")
assert not _is_obviously_invalid_product_name("环盈活储蓄保险计划")
主要改动: PPT 表格现在按槽位解析,正确保留 —、空列和单空格表格,不再发生金额左移。[regex_extractor.py (line 470)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/regex_extractor.py:470) 补齐年龄、累计保费、非保证现金价值、非保证身故赔偿等别名。 IUL 使用专属 LLM 提取结构,并加强年度、年龄、账户价值完整性校验。[extraction.py (line 741)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:741) PPT 缓存升级到 v6,旧错误缓存会自动失效。 提领方案不再与普通退保价值混淆,相关提示语已统一。 海报摘要年度金额与收益图表改为同一数据源;缺少部分里程碑时会从原始真实年度补足,不会隐藏图表。[content_builder.py (line 51)](D:/work/code/python/coding/baodanagent/api/insurance/poster/content_builder.py:51) 系统计算、利益表派生、人工修改增加来源标记。 增加“重新解析”功能,旧海报计划书无需重新上传。[routes.py (line 266)](D:/work/code/python/coding/baodanagent/api/insurance/poster/routes.py:266) ECharts 导出增加双帧就绪检测和事件竞态保护,不再依赖不存在的 .once()。 PPT 手机端改为可编辑数据卡片,320px 操作栏自动纵向排列;补齐按钮语义、键盘焦点及 44px 触控区域。[PptDataReview.vue (line 195)](D:/work/code/python/coding/baodanagent/frontend/src/pages/components/ppt/PptDataReview.vue:195) 验证结果: 核心专项测试:145 passed 除既有聊天日志测试外的测试集:236 passed 前端生产构建:通过 Python 语法检查:通过 git diff --check:通过 Impeccable 前端检测:无发现 全量测试仅剩一个与本次无关的既有失败:test_chat_logs_query 缺少 Flask application context
2026-08-01 01:22:07 +08:00
def test_long_poster_fills_partial_milestones_with_real_rows():
from insurance.poster.content_builder import build_poster_content
content = build_poster_content({
"benefit_table": [
{"policy_year": year, "total_surrender_value": year * 1000}
for year in (1, 10, 11, 12)
],
}, {}, output_mode="long", plan_type="savings")
assert len(content["benefit_table"]) >= 3
assert 10 in [row["year"] for row in content["benefit_table"]]
def test_poster_explicit_milestone_overrides_chart_row():
from insurance.poster.content_builder import build_poster_content
content = build_poster_content({
"surrender_value_10": 99999,
"benefit_table": [
{"policy_year": year, "total_surrender_value": year * 1000}
for year in (10, 20, 30)
],
}, {}, output_mode="long", plan_type="savings")
row_10 = next(row for row in content["benefit_table"] if row["year"] == 10)
assert row_10["totalSurrender"] == 99999
def test_frontend_chart_and_mobile_rules_cover_reported_failures():
root = Path(__file__).resolve().parents[1]
chart = (root / "frontend/src/components/poster/long/PosterBenefitChart.vue").read_text(encoding="utf-8")
ppt_page = (root / "frontend/src/pages/PptPage.vue").read_text(encoding="utf-8")
review = (root / "frontend/src/pages/components/ppt/PptDataReview.vue").read_text(encoding="utf-8")
assert ".once('finished'" not in chart
assert "instance.on('finished'" in chart
assert "instance.off('finished'" in chart
assert "@container" not in ppt_page
assert "@media (max-width: 767px)" in ppt_page
assert 'aria-label="返回 PPT 工作区列表"' in ppt_page
assert "transition: width" not in ppt_page
assert "mobile-data-card" in review
主要改动: 修正 benefit_illustration JSON 契约,兼容安全的数组返回格式,并把具体缺失字段反馈给 LLM 重试。[llm_client.py (line 184)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/llm_client.py:184) LLM 利益表失败时保留有效正则结果,不再把已有数据全部丢掉。[extraction.py (line 1237)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:1237) Female、Male、男女等误识别结果会用用户选择的产品名纠正。[extraction.py (line 521)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:521) 解析缓存升级至 v7,旧错误缓存自动失效。 新增真正的“退保价值”页签,数据来自利益演示中的年度退保价值,不再错误依赖提领数据。[PptDataReview.vue (line 318)](D:/work/code/python/coding/baodanagent/frontend/src/pages/components/ppt/PptDataReview.vue:318) “提领方案”继续作为独立可选情景;0 行不再显示待校对,也不会影响生成 PPT。[validator.py (line 98)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/validator.py:98) 提领表识别支持跨行标题及“提领/领取”等名称。[regex_extractor.py (line 619)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/regex_extractor.py:619) 补充了对应回归测试。[ppt_poster_optimization_test.py](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) 验证结果: PPT/解析专项测试:51 passed, 1 skipped 新增问题回归测试:32 passed 前端生产构建:通过 Python 语法检查:通过 git diff --check:通过
2026-08-01 01:56:33 +08:00
assert "退保价值 · ${getSurrenderRows(currentExt).length} 行" in review
assert ':data="getSurrenderRows(currentExt)"' in review
主要改动: PPT 表格现在按槽位解析,正确保留 —、空列和单空格表格,不再发生金额左移。[regex_extractor.py (line 470)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/regex_extractor.py:470) 补齐年龄、累计保费、非保证现金价值、非保证身故赔偿等别名。 IUL 使用专属 LLM 提取结构,并加强年度、年龄、账户价值完整性校验。[extraction.py (line 741)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:741) PPT 缓存升级到 v6,旧错误缓存会自动失效。 提领方案不再与普通退保价值混淆,相关提示语已统一。 海报摘要年度金额与收益图表改为同一数据源;缺少部分里程碑时会从原始真实年度补足,不会隐藏图表。[content_builder.py (line 51)](D:/work/code/python/coding/baodanagent/api/insurance/poster/content_builder.py:51) 系统计算、利益表派生、人工修改增加来源标记。 增加“重新解析”功能,旧海报计划书无需重新上传。[routes.py (line 266)](D:/work/code/python/coding/baodanagent/api/insurance/poster/routes.py:266) ECharts 导出增加双帧就绪检测和事件竞态保护,不再依赖不存在的 .once()。 PPT 手机端改为可编辑数据卡片,320px 操作栏自动纵向排列;补齐按钮语义、键盘焦点及 44px 触控区域。[PptDataReview.vue (line 195)](D:/work/code/python/coding/baodanagent/frontend/src/pages/components/ppt/PptDataReview.vue:195) 验证结果: 核心专项测试:145 passed 除既有聊天日志测试外的测试集:236 passed 前端生产构建:通过 Python 语法检查:通过 git diff --check:通过 Impeccable 前端检测:无发现 全量测试仅剩一个与本次无关的既有失败:test_chat_logs_query 缺少 Flask application context
2026-08-01 01:22:07 +08:00
assert "提领方案" in review
主要改动: 修正 benefit_illustration JSON 契约,兼容安全的数组返回格式,并把具体缺失字段反馈给 LLM 重试。[llm_client.py (line 184)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/llm_client.py:184) LLM 利益表失败时保留有效正则结果,不再把已有数据全部丢掉。[extraction.py (line 1237)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:1237) Female、Male、男女等误识别结果会用用户选择的产品名纠正。[extraction.py (line 521)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:521) 解析缓存升级至 v7,旧错误缓存自动失效。 新增真正的“退保价值”页签,数据来自利益演示中的年度退保价值,不再错误依赖提领数据。[PptDataReview.vue (line 318)](D:/work/code/python/coding/baodanagent/frontend/src/pages/components/ppt/PptDataReview.vue:318) “提领方案”继续作为独立可选情景;0 行不再显示待校对,也不会影响生成 PPT。[validator.py (line 98)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/validator.py:98) 提领表识别支持跨行标题及“提领/领取”等名称。[regex_extractor.py (line 619)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/regex_extractor.py:619) 补充了对应回归测试。[ppt_poster_optimization_test.py](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) 验证结果: PPT/解析专项测试:51 passed, 1 skipped 新增问题回归测试:32 passed 前端生产构建:通过 Python 语法检查:通过 git diff --check:通过
2026-08-01 01:56:33 +08:00
assert "提领/提款方案属于可选情景" in review
主要改动: PPT 表格现在按槽位解析,正确保留 —、空列和单空格表格,不再发生金额左移。[regex_extractor.py (line 470)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/regex_extractor.py:470) 补齐年龄、累计保费、非保证现金价值、非保证身故赔偿等别名。 IUL 使用专属 LLM 提取结构,并加强年度、年龄、账户价值完整性校验。[extraction.py (line 741)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:741) PPT 缓存升级到 v6,旧错误缓存会自动失效。 提领方案不再与普通退保价值混淆,相关提示语已统一。 海报摘要年度金额与收益图表改为同一数据源;缺少部分里程碑时会从原始真实年度补足,不会隐藏图表。[content_builder.py (line 51)](D:/work/code/python/coding/baodanagent/api/insurance/poster/content_builder.py:51) 系统计算、利益表派生、人工修改增加来源标记。 增加“重新解析”功能,旧海报计划书无需重新上传。[routes.py (line 266)](D:/work/code/python/coding/baodanagent/api/insurance/poster/routes.py:266) ECharts 导出增加双帧就绪检测和事件竞态保护,不再依赖不存在的 .once()。 PPT 手机端改为可编辑数据卡片,320px 操作栏自动纵向排列;补齐按钮语义、键盘焦点及 44px 触控区域。[PptDataReview.vue (line 195)](D:/work/code/python/coding/baodanagent/frontend/src/pages/components/ppt/PptDataReview.vue:195) 验证结果: 核心专项测试:145 passed 除既有聊天日志测试外的测试集:236 passed 前端生产构建:通过 Python 语法检查:通过 git diff --check:通过 Impeccable 前端检测:无发现 全量测试仅剩一个与本次无关的既有失败:test_chat_logs_query 缺少 Flask application context
2026-08-01 01:22:07 +08:00
已按计划书完成本轮核心 P0/P1 修复,覆盖用户反馈的三个主要问题。 主要结果: PPT: 已完成步骤可自由返回,刷新后保留当前步骤。 结果页改为固定三栏视口,缩略图和质量面板独立滚动。 修复画布宽度计算和宽高自适应缩放。 返回配置页时恢复上次模板。 Worker 禁止静默替换模板,不兼容时明确失败。 结果页显示实际应用的模板。 海报解析与合规: 修复 insured/policy 嵌套字段映射。 增加 partial/failed 解析质量判断和缺失字段提示。 传入险种、产品、保司和别名上下文。 修复性别、币种归一化和错误字段计数。 新增服务端字符级合规接口与生成前复检。 不合规文字可高亮、点击定位并选中对应文字。 海报编辑: AI 图片现在作为背景资产,不再替换整个海报。 单图、长图生成后仍可编辑标题、正文、CTA、数据和卖点。 背景生成失败不会清空当前编辑内容。 下载时合成背景、文字、数据、图表和免责声明。 增加可编辑文档快照、最终 PNG 保存和历史继续编辑。 新增数据库迁移:[migrate_028.py](/D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_028.py) 验证结果: 专项及相关后端测试:26 passed, 1 skipped Python 全模块编译检查:通过 前端生产构建:通过 git diff --check:通过 全量测试收集受本机缺少 python-pptx 依赖影响,报错为 ModuleNotFoundError: pptx,不是本次修改产生的测试失败。
2026-07-31 15:20:37 +08:00
def test_poster_case_mapping_does_not_report_empty_result_as_parsed():
from insurance.poster.tasks import _map_extract_plan_fields
mapped = _map_extract_plan_fields({}, "savings", "partial")
assert mapped["meta"]["status"] == "failed"
assert mapped["meta"]["validFieldCount"] == 0
assert "age" in mapped["meta"]["missingFields"]
def test_poster_compliance_returns_all_character_ranges_and_revision():
from insurance.poster.compliance import check_copy_compliance
result = check_copy_compliance({
"headline": "保证收益",
"body": "并非无风险,也不是零风险。",
"call_to_action": "立即咨询",
})
assert result["status"] == "block"
assert result["revision"]
assert [(issue["field"], issue["text"]) for issue in result["issues"]] == [
("headline", "保证"),
("body", "无风险"),
("body", "零风险"),
]
assert result["issues"][0]["start"] == 0
assert result["issues"][0]["end"] == 2
def test_poster_compliance_warns_and_returns_direct_replacement():
from insurance.poster.compliance import check_copy_compliance
result = check_copy_compliance({
"headline": "行业领先的保障方案",
"body": "具体内容以正式合同为准。",
"call_to_action": "了解详情",
})
assert result["status"] == "warn"
assert result["issues"][0]["severity"] == "warn"
assert result["issues"][0]["replacement"] == "具有特色"
def test_poster_generation_requires_template_before_other_processing():
from insurance.poster.service import PosterService
result = PosterService().generate_poster("user-1", {
"copyContent": {
"headline": "保障方案",
"body": "具体内容以正式合同为准。",
"call_to_action": "了解详情",
},
})
assert result == {"code": 1001, "message": "请选择海报模板", "data": None}
def test_poster_case_mapping_preserves_parse_diagnostics():
from insurance.poster.tasks import _map_extract_plan_fields
mapped = _map_extract_plan_fields({
"insured": {"age": 35},
"policy": {"currency": "USD", "annual_premium": 10000, "premium_payment_period": 5},
"_meta": {"method": "regex+ocr", "low_quality_pages": [3, 8]},
"_provenance": {"insured.age": {"source": "ocr", "confidence": 0.8}},
}, "savings", "partial")
assert mapped["meta"]["method"] == "regex+ocr"
assert mapped["meta"]["lowQualityPages"] == [3, 8]
assert mapped["meta"]["provenance"]["insured.age"]["confidence"] == 0.8
def test_poster_page_selection_prefers_summary_and_excludes_withdrawal_scenarios():
from insurance.ppt.extraction import _select_poster_pages
pdf_text = "\n".join([
"[PAGE 1]\n受保人 年龄 性别 保单货币 年缴保费",
"[PAGE 2]\n款项提取说明 保单年度 退保价值 10 20 30",
"[PAGE 3]\n基本计划说明摘要 保单年度 退保价值 保证现金 1 5 10 15 20 25 30",
"[PAGE 4]\n悲观情景 保单年度 退保价值 10 20 30",
])
selected = _select_poster_pages(pdf_text, "savings")
assert "[PAGE 1]" in selected
assert "[PAGE 3]" in selected
assert "[PAGE 2]" not in selected
assert "[PAGE 4]" not in selected
def test_compact_poster_extraction_uses_one_call_and_keeps_target_years(monkeypatch):
from insurance.ppt import extraction
from insurance.ppt.llm_client import llm_client
pdf_text = (
"[PAGE 1]\n受保人 年龄 35 性别 男 保单货币 USD 年缴保费 100000\n"
"[PAGE 2]\n说明摘要 保单年度 退保价值 保证现金 10 20 30"
)
monkeypatch.setattr(extraction, "_extract_pdf_text", lambda _path: (pdf_text, []))
calls = []
async def fake_structured_output(**kwargs):
calls.append(kwargs["prompt"])
return {
"product_name": "测试储蓄计划",
"insured": {"age": 35, "gender": "male", "smoking_status": "non-smoker"},
"policy": {
"currency": "USD", "sum_insured": 500000,
"annual_premium": 100000, "first_year_amount_due": 92000,
"premium_payment_period": 5,
},
"benefit_illustration": [
{"policy_year": 10, "surrender_value": {"total": 300000}},
{"policy_year": 20, "total_surrender_value": 700000},
{"policy_year": 30, "total_surrender_value": 1200000},
{"policy_year": 40, "total_surrender_value": 1800000},
],
}, SimpleNamespace(latency_ms=120)
monkeypatch.setattr(llm_client, "structured_output", fake_structured_output)
progress = []
result = asyncio.run(extraction.ExtractionOrchestrator(use_cache=False).extract_for_poster(
"poster.pdf",
plan_type="savings",
progress_callback=lambda value, message: progress.append((value, message)),
))
assert len(calls) == 1
assert "提领、提款或压力情景" in calls[0]
assert [row["policy_year"] for row in result["benefit_illustration"]] == [10, 20, 30]
assert result["benefit_illustration"][0]["total_surrender_value"] == 300000
assert result["policy"]["annual_premium"] == 100000
assert result["policy"]["first_year_amount_due"] == 92000
assert result["_meta"]["method"] == "poster_compact"
assert progress[-1][0] == 90
def test_poster_source_polling_uses_backend_progress_without_local_timeout():
root = Path(__file__).resolve().parents[1]
source = (root / "frontend/src/components/poster/workspace/PosterSourcePanel.vue").read_text(encoding="utf-8")
assert "MAX_POLL_RETRIES" not in source
assert "setInterval" not in source
assert "data?.parseProgress" in source
assert "data.parseError" in source
assert "网络连接不稳定,后台仍在解析" in source
def test_poster_case_serializes_parsed_data_and_hides_unsafe_errors():
from insurance.models.poster_case_upload import PosterCaseUpload
record = PosterCaseUpload(
user_id="user-1",
product_id="product-1",
source_file_url="poster.pdf",
parse_status="failed",
parse_progress=100,
parse_error='Traceback: File "/app/insurance/tasks.py"',
parsed_data='{"annual_premium": 100000}',
product_snapshot_json='{"name": "测试产品"}',
)
data = record.to_dict()
assert data["parsedData"] == {"annual_premium": 100000}
assert data["productSnapshot"] == {"name": "测试产品"}
assert data["parseError"] == "解析失败,请重试;如多次失败请联系管理员"