diff --git a/app/platform/site_import/generator.py b/app/platform/site_import/generator.py index 5f60e97..12e5cac 100644 --- a/app/platform/site_import/generator.py +++ b/app/platform/site_import/generator.py @@ -63,7 +63,7 @@ GEN1_SYSTEM = """你是 B2B 外贸网站的行业分析师。分析用户提供 GEN2_SYSTEM = """你是 B2B 外贸客服提示词的撰写专家。根据上一步的结构化分析结果(JSON)与参考范例,为该网站生成客服提示词的全部占位符内容与品牌文案。 ## 规则(全部必须遵守) -- 严格只输出一个 JSON 对象:键名与「待生成键清单」逐字一致、一个不落;不要 markdown 代码栅栏、不要解释文字、不要新增清单外的键 +- 严格只输出一个**扁平** JSON 对象:所有键平铺在同一层(禁止按模板分组嵌套成 {"system": {...}, "extractor": {...}} 这类结构);键名与「待生成键清单」逐字一致、一个不落;不要 markdown 代码栅栏、不要解释文字、不要新增清单外的键 - 逐字填槽:只生成清单所列键的值,不复述模板固定段、不改写骨架结构 - 语言钉死:描述性内容用中文写;产品统称EN、面向客户的对话示例句(固定追问句/零上下文首轮例句/好差示例组/正例/负例/浏览例句/产品追问示例)按其语境语言(通常英文)写——抓取文本是西语/英语不改变本规则 - 数字纪律:示例与描述中出现的数字必须来自分析结果(即源站出现过的数字),禁止推算、换算、编造 @@ -178,6 +178,26 @@ def _parse_json(raw: str) -> dict | None: return obj if isinstance(obj, dict) else None +def _flatten_nested(obj: dict) -> dict: + """嵌套分组容错:模型把键按 {"system": {...}, "extractor": {...}} 分组时 + 自动展平为单层(Task 10 实测 glm-4-flash 的常见形态——清单里 + 「system 模板占位符」措辞诱导分组)。顶层已可直接识别的键不动; + dict 值的键并入顶层(子键覆盖散键同名时以子键为准),非 dict 值原位保留; + 无 dict 值时输出与输入等价。""" + recognizable = set(ALL_PLACEHOLDER_NAMES) | set(_BRAND_KEYS) | set(_TAIL_KEYS) + if any(k in recognizable for k in obj): + # 顶层已有可识别键——但混合形态(嵌套组+散键)仍需展开 dict 值 + if not any(isinstance(v, dict) for v in obj.values()): + return obj + flat: dict = {} + for k, v in obj.items(): + if isinstance(v, dict): + flat.update(v) + else: + flat[k] = v + return flat + + def _analysis_shape_ok(d) -> bool: """调用 1 schema 下限:company_name 为串 + product_lines 为列表。 @@ -210,8 +230,10 @@ async def _chat_json(provider, system: str, user: str, what: str, except asyncio.TimeoutError: raise GenerateError(f"{what}:AI 调用超时({AI_TIMEOUT:.0f}s)") parsed = _parse_json(raw) - if parsed is not None and shape_ok(parsed): - return parsed + if parsed is not None: + parsed = _flatten_nested(parsed) + if shape_ok(parsed): + return parsed raise GenerateError(f"{what}:输出无法解析为 JSON 或 schema 不符(已自动重试一次)") diff --git a/tests/test_site_import_generator.py b/tests/test_site_import_generator.py index 0671cc7..f1c6206 100644 --- a/tests/test_site_import_generator.py +++ b/tests/test_site_import_generator.py @@ -432,3 +432,33 @@ class TestCrawlWarningsMerge: draft = generator.build(gen, ANALYSIS_JSON, crawl, SOURCE_TEXT) assert "logo 为不支持的格式,已跳过" in draft["warnings"] assert "子页过薄已跳过:/thin" in draft["warnings"] + + +class TestNestedFlatten: + def test_嵌套分组自动展平(self): + """Task 10 实测:glm-4-flash 常把键按模板分组嵌套——展平容错。""" + from app.platform.site_import.generator import _flatten_nested + nested = {"system": {"身份描述": "x", "产品统称": "y"}, + "extractor": {"采购域": "z"}, "brand_title": "T"} + flat = _flatten_nested(nested) + assert flat == {"身份描述": "x", "产品统称": "y", "采购域": "z", "brand_title": "T"} + + def test_本来就扁平原样返回(self): + from app.platform.site_import.generator import _flatten_nested + flat_in = {"身份描述": "x", "brand_title": "T"} + assert _flatten_nested(dict(flat_in)) == flat_in + + def test_非dict子值不展平(self): + from app.platform.site_import.generator import _flatten_nested + weird = {"a": "字符串", "b": {"身份描述": "x"}} + assert _flatten_nested(weird) is weird or _flatten_nested(weird) == weird + + async def test_generate接受嵌套输出(self): + """端到端:fake 回嵌套 JSON 也能过 generate。""" + import json as _json + nested = {"system": {"身份描述": "x"}, "brand_title": "T"} + class NestedP: + async def chat(self, m, s): + return "```json\n" + _json.dumps(nested, ensure_ascii=False) + "\n```" + out = await generator.generate(NestedP(), ANALYSIS_JSON) + assert out.get("身份描述") == "x" and out.get("brand_title") == "T"