fix: Z41 preserve product semantics during generation

Replace the silent generic-record fallback for product requests and expose semantic validation diagnostics before publishing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
whm
2026-08-07 18:23:11 +08:00
parent 154f849f57
commit 9cdbd7f722
8 changed files with 411 additions and 68 deletions

View File

@@ -151,6 +151,68 @@ def infer_item_label(prompt: str, sheet_label: str) -> str:
return "记录" return "记录"
def is_product_intent(prompt: str) -> bool:
p = (prompt or "").lower()
return any(k in p for k in ("商品", "产品", "商城", "product", "catalog"))
def has_product_semantics(draft: dict[str, Any]) -> bool:
entities = draft.get("entities") or []
names = {
str(f.get("name") or "").lower()
for ent in entities
if isinstance(ent, dict)
for f in (ent.get("fields") or [])
if isinstance(f, dict)
}
entity_text = " ".join(
f"{ent.get('name', '')} {ent.get('label', '')}".lower()
for ent in entities
if isinstance(ent, dict)
)
field_text = " ".join(
f"{f.get('name', '')} {f.get('label', '')}".lower()
for ent in entities
if isinstance(ent, dict)
for f in (ent.get("fields") or [])
if isinstance(f, dict)
)
has_name = bool(names & {"name", "product_name", "title"}) or any(
key in field_text for key in ("商品名称", "产品名称")
)
has_price = bool(names & {"price", "unit_price", "sale_price"}) or "价格" in field_text
has_image = bool(names & {"image", "image_url", "cover", "cover_image"}) or any(
key in field_text for key in ("商品图片", "产品图片", "主图", "封面")
)
return ("product" in entity_text or "商品" in entity_text or "产品" in entity_text) and has_name and has_price and has_image
def validate_generated_semantics(
prompt: str,
draft: dict[str, Any],
*,
image_count: int,
html_count: int,
) -> list[str]:
warnings: list[str] = []
meta = draft.setdefault("meta", {})
if image_count == 0 and html_count == 0:
if meta.get("ui_preset") == "screenshot_faithful":
meta["ui_preset"] = "default"
warnings.append("未提供截图或 HTML已移除错误的 screenshot_faithful 预设")
source = meta.setdefault("source", {})
source.pop("screenshot_faithful", None)
for page in draft.get("pages") or []:
if not isinstance(page, dict):
continue
layout = page.get("layout")
if isinstance(layout, dict) and layout.get("preset") == "screenshot_faithful":
layout["preset"] = "default"
if is_product_intent(prompt) and not has_product_semantics(draft):
warnings.append("商品需求缺少名称、价格、图片等核心字段,已判定为语义不合格")
return warnings
def parse_ui_hints(prompt: str, image_count: int, html_count: int = 0) -> dict[str, Any]: def parse_ui_hints(prompt: str, image_count: int, html_count: int = 0) -> dict[str, Any]:
"""从用户需求提取展示元信息;截图/HTML 忠实策略由 generation_rules 决定。""" """从用户需求提取展示元信息;截图/HTML 忠实策略由 generation_rules 决定。"""
p = prompt or "" p = prompt or ""
@@ -631,15 +693,13 @@ def harden_draft(draft: dict[str, Any], fallback: dict[str, Any]) -> dict[str, A
if isinstance(rp, dict) and rp.get("entity"): if isinstance(rp, dict) and rp.get("entity"):
rp["entity"] = resolve_entity(rp.get("entity")) rp["entity"] = resolve_entity(rp.get("entity"))
# 截图忠实LLM 不得抹掉;有图或分区结构时强制恢复(不限行业) # 截图忠实只继承启发式基线中的明确证据;禁止 LLM 仅凭通用 widget 结构误设。
fb_meta = fallback.get("meta") or {} fb_meta = fallback.get("meta") or {}
fb_src = fb_meta.get("source") or {} fb_src = fb_meta.get("source") or {}
force_faithful = ( force_faithful = (
fb_meta.get("ui_preset") == "screenshot_faithful" fb_meta.get("ui_preset") == "screenshot_faithful"
or fb_src.get("screenshot_faithful") is True or fb_src.get("screenshot_faithful") is True
or bool(fb_src.get("image_refs")) or bool(fb_src.get("image_refs"))
or looks_like_faithful_dashboard(fallback)
or looks_like_faithful_dashboard(draft)
) )
if force_faithful: if force_faithful:
if fb_meta.get("platform_title"): if fb_meta.get("platform_title"):
@@ -1169,6 +1229,7 @@ def build_blueprint(
primary_filters: list[str] = [] primary_filters: list[str] = []
primary_list_cols: list[str] = [] primary_list_cols: list[str] = []
label = "数据" label = "数据"
product_intent = is_product_intent(prompt)
sheets = (excel_meta or {}).get("sheets") or [] sheets = (excel_meta or {}).get("sheets") or []
relations = (excel_meta or {}).get("relations") or [] relations = (excel_meta or {}).get("relations") or []
@@ -1176,6 +1237,8 @@ def build_blueprint(
if sheets: if sheets:
for si, sheet in enumerate(sheets): for si, sheet in enumerate(sheets):
ename = slugify(sheet.get("name") or f"record_{si}", fallback=f"record_{si}") ename = slugify(sheet.get("name") or f"record_{si}", fallback=f"record_{si}")
if si == 0 and product_intent:
ename = "product"
if ename in {"sheet1", "sheet", "sheet_1"}: if ename in {"sheet1", "sheet", "sheet_1"}:
ename = "record" if si == 0 else f"record_{si}" ename = "record" if si == 0 else f"record_{si}"
fields, filters, sorts, list_cols, confs = _fields_from_sheet(sheet) fields, filters, sorts, list_cols, confs = _fields_from_sheet(sheet)
@@ -1213,7 +1276,8 @@ def build_blueprint(
primary_fields = fields primary_fields = fields
primary_filters = filters primary_filters = filters
primary_list_cols = list_cols primary_list_cols = list_cols
label = ent["label"] label = "商品" if product_intent else ent["label"]
ent["label"] = label
rc = sheet.get("row_count") rc = sheet.get("row_count")
if rc: if rc:
warnings.append(f"主表 {ename} 识别到约 {rc} 行,发布后请导入完整 xlsx/json") warnings.append(f"主表 {ename} 识别到约 {rc} 行,发布后请导入完整 xlsx/json")
@@ -1235,9 +1299,27 @@ def build_blueprint(
) )
if relations: if relations:
warnings.append(f"已按 JSON 字段关联生成 {len(relations)} 条表关系") warnings.append(f"已按 JSON 字段关联生成 {len(relations)} 条表关系")
else:
confidences.append(0.65 if product_intent else 0.55)
if product_intent:
warnings.append("未上传商品数据文件,已按商品语义生成可编辑字段草案")
primary_entity = "product"
label = "商品"
primary_fields = [
{"name": "id", "label": "ID", "type": "bigint", "nullable": False, "ui": {"widget": "hidden", "listable": False}},
{"name": "name", "label": "商品名称", "type": "string", "nullable": False, "max_length": 160, "ui": {"widget": "input", "listable": True, "sortable": True}},
{"name": "price", "label": "价格", "type": "decimal", "nullable": False, "precision": 12, "scale": 2, "ui": {"widget": "number", "listable": True, "sortable": True}},
{"name": "image", "label": "商品图片", "type": "file_ref", "nullable": True, "ui": {"widget": "upload", "listable": True}},
{"name": "category", "label": "分类", "type": "string", "nullable": True, "max_length": 80, "ui": {"widget": "input", "listable": True, "filterable": True}},
{"name": "stock", "label": "库存", "type": "int", "nullable": True, "ui": {"widget": "number", "listable": True, "sortable": True}},
{"name": "description", "label": "商品详情", "type": "text", "nullable": True, "ui": {"widget": "textarea", "listable": False}},
{"name": "status", "label": "状态", "type": "enum", "nullable": True, "enum_values": ["上架", "下架"], "ui": {"widget": "select", "listable": True, "filterable": True}},
]
primary_filters = ["category", "status"]
primary_list_cols = ["image", "name", "price", "category", "stock", "status"]
entities = [{"name": "product", "table": "product", "label": label, "primary_key": "id", "fields": primary_fields, "indexes": []}]
else: else:
warnings.append("未上传数据文件,已生成通用文本字段草案") warnings.append("未上传数据文件,已生成通用文本字段草案")
confidences.append(0.55)
primary_fields = [ primary_fields = [
{"name": "id", "label": "ID", "type": "bigint", "nullable": False, "ui": {"widget": "hidden", "listable": False}}, {"name": "id", "label": "ID", "type": "bigint", "nullable": False, "ui": {"widget": "hidden", "listable": False}},
{"name": "title", "label": "标题", "type": "string", "nullable": False, "max_length": 128, "ui": {"widget": "input", "listable": True, "sortable": True}}, {"name": "title", "label": "标题", "type": "string", "nullable": False, "max_length": 128, "ui": {"widget": "input", "listable": True, "sortable": True}},
@@ -1248,10 +1330,17 @@ def build_blueprint(
primary_list_cols = ["title", "content", "status"] primary_list_cols = ["title", "content", "status"]
entities = [{"name": "record", "table": "record", "label": "数据", "primary_key": "id", "fields": primary_fields, "indexes": []}] entities = [{"name": "record", "table": "record", "label": "数据", "primary_key": "id", "fields": primary_fields, "indexes": []}]
resources = [{ resources = [{
"entity": "record", "entity": primary_entity,
"path": "/records", "path": f"/{primary_entity}s",
"operations": ["list", "get", "create", "update", "delete", "import", "export"], "operations": ["list", "get", "create", "update", "delete", "import", "export"],
"list": {"default_page_size": 50, "max_page_size": 2000, "allowed_filters": primary_filters, "allowed_sorts": ["title"]}, "list": {
"default_page_size": 50,
"max_page_size": 2000,
"allowed_filters": primary_filters,
"allowed_sorts": [
f["name"] for f in primary_fields if (f.get("ui") or {}).get("sortable")
][:12],
},
}] }]
if image_count > 0 and wants_screenshot_layout(prompt or "", image_count, html_count): if image_count > 0 and wants_screenshot_layout(prompt or "", image_count, html_count):
@@ -1372,6 +1461,23 @@ def build_blueprint(
}, },
}, },
] ]
if product_intent and primary_entity == "product":
main_pages.append(
{
"id": "product_edit",
"title": "编辑商品",
"route": "/product/edit",
"type": "form_edit",
"entity": "product",
"layout": {
"form_fields": [
f["name"] for f in primary_fields if f["name"] not in _SYSTEM_FIELDS
],
"actions": ["edit"],
"action_labels": {"edit": "保存修改", "cancel": "取消"},
},
}
)
if has_dashboard: if has_dashboard:
dash_widgets = build_dashboard_widgets( dash_widgets = build_dashboard_widgets(
primary_fields, primary_filters, primary_list_cols, primary_entity, item_label, prompt or "" primary_fields, primary_filters, primary_list_cols, primary_entity, item_label, prompt or ""
@@ -1610,8 +1716,10 @@ async def generate(
) )
trace.stage("build_blueprint", "启发式草案") trace.stage("build_blueprint", "启发式草案")
# 启发式意图/页面判断只读取用户输入SYSTEM_RULES/视觉摘要仅供 LLM
# 否则规则文本中的 screenshot_faithful、概览等词会污染无附件生成。
draft, warnings, confidence = build_blueprint( draft, warnings, confidence = build_blueprint(
effective_prompt, excel_meta, storage_mode, image_count, html_count prompt_for_hints, excel_meta, storage_mode, image_count, html_count
) )
# url.config 抓包:导航页签 / 空接口告警 # url.config 抓包:导航页签 / 空接口告警
if excel_meta and isinstance(excel_meta, dict): if excel_meta and isinstance(excel_meta, dict):
@@ -1712,9 +1820,43 @@ async def generate(
warnings.extend(llm_notes) warnings.extend(llm_notes)
trace.notes(llm_notes) trace.notes(llm_notes)
draft = harden_draft(draft, baseline) draft = harden_draft(draft, baseline)
fallback_reasons = [
note
for note in llm_notes
if any(marker in note for marker in ("回退", "本地启发式", "结构无效", "调用失败", "未配置"))
]
fallback_used = bool(fallback_reasons)
fallback_reason = "".join(fallback_reasons)
validation_warnings = validate_generated_semantics(
user_prompt,
draft,
image_count=image_count,
html_count=html_count,
)
if is_product_intent(user_prompt) and not has_product_semantics(draft):
draft = json.loads(json.dumps(baseline))
validation_warnings.append("已恢复商品语义启发式草案,未采用通用 record 蓝图")
fallback_used = True
fallback_reason = (
(fallback_reason + "") if fallback_reason else ""
) + "semantic_validation_restored_product_blueprint"
validation_warnings.extend(
validate_generated_semantics(
user_prompt,
draft,
image_count=image_count,
html_count=html_count,
)
)
warnings.extend(validation_warnings)
trace.notes(validation_warnings)
draft.setdefault("meta", {}).setdefault("source", {}) draft.setdefault("meta", {}).setdefault("source", {})
draft["meta"]["source"]["prompt"] = user_prompt draft["meta"]["source"]["prompt"] = user_prompt
draft["meta"]["source"]["llm_provider"] = llm_provider draft["meta"]["source"]["llm_provider"] = llm_provider
draft["meta"]["source"]["fallback_used"] = fallback_used
if fallback_reason:
draft["meta"]["source"]["fallback_reason"] = fallback_reason
draft["meta"]["source"]["validation_warnings"] = validation_warnings
if model_arg: if model_arg:
draft["meta"]["source"]["llm_model"] = model_arg draft["meta"]["source"]["llm_model"] = model_arg
if upload is not None and upload.filename: if upload is not None and upload.filename:
@@ -1792,6 +1934,7 @@ async def generate(
) )
conf = float(draft.get("meta", {}).get("confidence") or confidence) conf = float(draft.get("meta", {}).get("confidence") or confidence)
require_confirm = conf < 0.7 or fallback_used or bool(validation_warnings)
log_payload = trace.as_payload() log_payload = trace.as_payload()
draft.setdefault("meta", {}).setdefault("source", {}) draft.setdefault("meta", {}).setdefault("source", {})
draft["meta"]["source"]["generate_log_file"] = log_payload.get("log_file") or "" draft["meta"]["source"]["generate_log_file"] = log_payload.get("log_file") or ""
@@ -1799,7 +1942,10 @@ async def generate(
"draft": draft, "draft": draft,
"warnings": warnings, "warnings": warnings,
"confidence": conf, "confidence": conf,
"require_confirm": True, "require_confirm": require_confirm,
"fallback_used": fallback_used,
"fallback_reason": fallback_reason,
"validation_warnings": validation_warnings,
"llm_provider": llm_provider, "llm_provider": llm_provider,
"llm_model": model_arg or "", "llm_model": model_arg or "",
"fidelity": fidelity_report, "fidelity": fidelity_report,

View File

@@ -119,8 +119,7 @@ def wants_screenshot_layout(prompt: str, image_count: int, html_count: int = 0)
return False return False
if image_count > 0 or html_count > 0: if image_count > 0 or html_count > 0:
return True return True
if any(k in p for k in _SCREENSHOT_OPT_IN): # 只有“还原截图”等文字但没有实际截图/HTML不能启用截图专用 preset。
return True
return False return False

View File

@@ -0,0 +1,109 @@
import copy
import unittest
from app import (
build_blueprint,
has_product_semantics,
validate_generated_semantics,
)
from generation_rules import wants_screenshot_layout
class Z41GenerationTests(unittest.TestCase):
def test_product_prompt_builds_product_blueprint_without_attachment(self):
draft, warnings, confidence = build_blueprint(
"商品展示页",
None,
"schema_per_app",
0,
0,
)
self.assertTrue(has_product_semantics(draft))
self.assertEqual(draft["entities"][0]["name"], "product")
self.assertEqual(draft["apis"]["resources"][0]["path"], "/products")
self.assertEqual(draft["meta"]["ui_preset"], "default")
self.assertGreater(confidence, 0.55)
self.assertTrue(any("商品语义" in warning for warning in warnings))
self.assertIn("编辑商品", [page["title"] for page in draft["pages"]])
def test_product_validator_rejects_generic_record_blueprint(self):
generic, _, _ = build_blueprint(
"普通记录页",
None,
"schema_per_app",
0,
0,
)
warnings = validate_generated_semantics(
"商品展示页",
generic,
image_count=0,
html_count=0,
)
self.assertFalse(has_product_semantics(generic))
self.assertTrue(any("语义不合格" in warning for warning in warnings))
def test_product_excel_fields_stay_on_product_entity(self):
excel_meta = {
"sheets": [
{
"name": "商品表",
"headers": ["商品名称", "价格", "商品图片", "分类"],
"columns": {
"商品名称": ["咖啡"],
"价格": [19.9],
"商品图片": ["coffee.png"],
"分类": ["饮品"],
},
"inferred": {
"商品名称": {"type": "string", "enum_values": []},
"价格": {"type": "decimal", "enum_values": []},
"商品图片": {"type": "file_ref", "enum_values": []},
"分类": {"type": "string", "enum_values": []},
},
}
]
}
draft, _, _ = build_blueprint(
"商品展示页",
excel_meta,
"schema_per_app",
0,
0,
)
self.assertEqual(draft["entities"][0]["name"], "product")
self.assertEqual(draft["entities"][0]["label"], "商品")
self.assertTrue(has_product_semantics(draft))
def test_no_media_removes_hallucinated_screenshot_preset(self):
draft, _, _ = build_blueprint(
"商品展示页",
None,
"schema_per_app",
0,
0,
)
hallucinated = copy.deepcopy(draft)
hallucinated["meta"]["ui_preset"] = "screenshot_faithful"
hallucinated["pages"][0]["layout"]["preset"] = "screenshot_faithful"
warnings = validate_generated_semantics(
"商品展示页",
hallucinated,
image_count=0,
html_count=0,
)
self.assertEqual(hallucinated["meta"]["ui_preset"], "default")
self.assertEqual(hallucinated["pages"][0]["layout"]["preset"], "default")
self.assertTrue(any("screenshot_faithful" in warning for warning in warnings))
self.assertFalse(wants_screenshot_layout("请还原截图", 0, 0))
if __name__ == "__main__":
unittest.main()

File diff suppressed because one or more lines are too long

2
web/dist/index.html vendored
View File

@@ -7,7 +7,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,700&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,700&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
<script type="module" crossorigin src="/assets/index-D2ZGs_V_.js"></script> <script type="module" crossorigin src="/assets/index-BT0DQG2x.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CtqUfnO-.css"> <link rel="stylesheet" crossorigin href="/assets/index-CtqUfnO-.css">
</head> </head>
<body> <body>

View File

@@ -417,6 +417,12 @@ export default function App() {
: `还原度 ${fid.final_score ?? "—"}%(目标 ${fid.target}%),请查看差异后继续微调或再生成。`; : `还原度 ${fid.final_score ?? "—"}%(目标 ${fid.target}%),请查看差异后继续微调或再生成。`;
} }
const runId = res.generate_log?.run_id ? ` run=${res.generate_log.run_id}` : ""; const runId = res.generate_log?.run_id ? ` run=${res.generate_log.run_id}` : "";
const validationMsg = res.validation_warnings?.length
? `语义校验:${res.validation_warnings.join("")}`
: "";
const fallbackMsg = res.fallback_used
? `本次使用了回退方案${res.fallback_reason ? `${res.fallback_reason}` : ""},请确认字段后再发布。`
: "";
let draftNote = ""; let draftNote = "";
if (session && res.draft?.meta?.slug) { if (session && res.draft?.meta?.slug) {
try { try {
@@ -440,7 +446,7 @@ export default function App() {
} }
} }
setInfo( setInfo(
`草稿置信度 ${res.confidence}${fidMsg}${draftNote}确认发布后将打开模块页。${runId}` `草稿置信度 ${res.confidence}${fallbackMsg}${validationMsg}${fidMsg}${draftNote}确认发布后将打开模块页。${runId}`
); );
} catch (e: any) { } catch (e: any) {
setError(e.message || String(e)); setError(e.message || String(e));

View File

@@ -246,6 +246,9 @@ export async function generateDraft(
warnings: string[]; warnings: string[];
confidence: number; confidence: number;
require_confirm: boolean; require_confirm: boolean;
fallback_used?: boolean;
fallback_reason?: string;
validation_warnings?: string[];
llm_provider?: string; llm_provider?: string;
llm_model?: string; llm_model?: string;
fidelity?: { fidelity?: {

View File

@@ -1,6 +1,6 @@
# 联调后修改意见 · 宇恒松离线(形态 B # 联调后修改意见 · 宇恒松离线(形态 B
> 初稿2026-08-01 · 修订至 **2026-08-07**+Z39 跨模块数据隔离/本地表唯一键 > 初稿2026-08-01 · 修订至 **2026-08-07**+Z41 商品需求被生成通用记录模板
> 焦点:**§0.2**(按负责方);**改代码前须先写入本意见**(见 §0.0 > 焦点:**§0.2**(按负责方);**改代码前须先写入本意见**(见 §0.0
> 来源:宇恒 `yuhengyihao_client` ↔ 智建(生产 `aisite.yuxindazhineng.com` > 来源:宇恒 `yuhengyihao_client` ↔ 智建(生产 `aisite.yuxindazhineng.com`
> 依据:`松离线-dbsync方案-最终版.md`、`宇恒-松离线数据同步使用文档.md` > 依据:`松离线-dbsync方案-最终版.md`、`宇恒-松离线数据同步使用文档.md`
@@ -35,6 +35,8 @@
| **Z37 SyncPage LWW 403 吓人** | **智建已落实(须生产 pull** | SyncPage 无冲突队列;中性提示 + 空通道说明conflicts 403 静默§5.35 | | **Z37 SyncPage LWW 403 吓人** | **智建已落实(须生产 pull** | SyncPage 无冲突队列;中性提示 + 空通道说明conflicts 403 静默§5.35 |
| **Z38 绑定即用 + 返回上一级** | **智建已落实(须生产 pull** | 绑定自动补齐建站权限超管公司视图全局返回平台§5.36 | | **Z38 绑定即用 + 返回上一级** | **智建已落实(须生产 pull** | 绑定自动补齐建站权限超管公司视图全局返回平台§5.36 |
| **Z39 跨模块串数据/页面复用** | **双方已改;智建须生产 pull** | 智能体共享库内按 tenant+slug 分 Schema页面路由防竞态/旧蓝图回执、capsule、日志可核对映射和 revision§5.37 | | **Z39 跨模块串数据/页面复用** | **双方已改;智建须生产 pull** | 智能体共享库内按 tenant+slug 分 Schema页面路由防竞态/旧蓝图回执、capsule、日志可核对映射和 revision§5.37 |
| **Z40 模块选择项过多** | **宇恒已改;智建无需** | 模块数 ≤10 保持按钮单选;>10 自动改为下拉选择§5.38 |
| **Z41 商品需求生成通用记录页** | **智建已落实(须生产 pull** | 商品意图生成商品字段;语义不合格自动恢复;无媒体禁止 screenshot preset回执带 fallback/validation§5.39 |
| **仍关注** | 用法 | 通道断了靠智建自愈;表数据靠宇恒双向指纹 / 数据恢复;不是「再点启动」 | | **仍关注** | 用法 | 通道断了靠智建自愈;表数据靠宇恒双向指纹 / 数据恢复;不是「再点启动」 |
### 0.0 修改流程(冻结) ### 0.0 修改流程(冻结)
@@ -81,6 +83,7 @@
| **Z34** | **换机按账号恢复2026-08-06 · 宇恒半程已改完)**:有手机号时可静默 ticket/confirm。**完整「仅宇恒 ID」见智建 Z34b已落实** | | **Z34** | **换机按账号恢复2026-08-06 · 宇恒半程已改完)**:有手机号时可静默 ticket/confirm。**完整「仅宇恒 ID」见智建 Z34b已落实** |
| **Z36** | **编辑发布无增量中文取消2026-08-07 · 宇恒已改完;智建 Z36 已落实)**:宇恒侧预处理;智建 `host_meta_updated` / `[NO_BLUEPRINT_DELTA]`§5.34 | | **Z36** | **编辑发布无增量中文取消2026-08-07 · 宇恒已改完;智建 Z36 已落实)**:宇恒侧预处理;智建 `host_meta_updated` / `[NO_BLUEPRINT_DELTA]`§5.34 |
| **Z39-YH** | **本地模块镜像表唯一键2026-08-07 · 宇恒已改完)**:表名始终包含唯一 `slug`;发布建表、自动导入、独立导入、手动新增统一传递模块展示名/slug/实体标签。见 §5.37 | | **Z39-YH** | **本地模块镜像表唯一键2026-08-07 · 宇恒已改完)**:表名始终包含唯一 `slug`;发布建表、自动导入、独立导入、手动新增统一传递模块展示名/slug/实体标签。见 §5.37 |
| **Z40** | **模块选择控件阈值2026-08-07 · 宇恒已改完)**:选项 ≤10 使用按钮单选,>10 使用下拉;覆盖导入、编辑、查询、删除、打开模块等公共选择表单。**智建无需改** |
#### A. 宇恒 · 配合注意(非阻塞新开发) #### A. 宇恒 · 配合注意(非阻塞新开发)
@@ -108,6 +111,7 @@
| **Z37** | SyncPage 无冲突队列;中性 LWW 说明 +「该公司暂无通道」空态;`listSyncConflicts` 403 静默;见 §5.35 | | **Z37** | SyncPage 无冲突队列;中性 LWW 说明 +「该公司暂无通道」空态;`listSyncConflicts` 403 静默;见 §5.35 |
| **Z38** | 超管进公司后顶栏「返回平台工作台」;绑定默认可带模块读/发权或列表明示权限;见 §5.36 | | **Z38** | 超管进公司后顶栏「返回平台工作台」;绑定默认可带模块读/发权或列表明示权限;见 §5.36 |
| **Z39-ZJ** | 智能体共享库不再把各模块都落到 `public`;改为 `app_t{tenant}_{slug}`,长 slug 带哈希防碰撞;页面切换防旧请求覆盖;见 §5.37 | | **Z39-ZJ** | 智能体共享库不再把各模块都落到 `public`;改为 `app_t{tenant}_{slug}`,长 slug 带哈希防碰撞;页面切换防旧请求覆盖;见 §5.37 |
| **Z41** | 无附件商品需求生成 `product`(名称/价格/图片/分类/库存/详情);商品 Excel 保留字段并归入 product回执明示 fallback 与语义告警;见 §5.39 |
> 产品一句:**账号已绑定 ⇒ 落点信息固定;通道没了平台自动补,终端无需手填 channel_id。** > 产品一句:**账号已绑定 ⇒ 落点信息固定;通道没了平台自动补,终端无需手填 channel_id。**
@@ -115,7 +119,7 @@
| 优先级 | 编号 | 项 | 说明 | | 优先级 | 编号 | 项 | 说明 |
|--------|------|----|------| |--------|------|----|------|
| — | — | **当前开发项已清** | Z38、Z39 已落实;剩 B 生产 pull 与联调验收。 | | — | — | **当前开发项已清** | Z41 已落实;剩 B 生产 pull 与联调验收。 |
#### B‴. 智建 · 本次明确不改Z15 #### B‴. 智建 · 本次明确不改Z15
@@ -131,7 +135,7 @@
| 优先级 | 项 | 说明 | | 优先级 | 项 | 说明 |
|--------|----|------| |--------|----|------|
| **P0** | **生产 pull 本批** | Z10d + fingerprint + Z14c + **Z12h** + **数据恢复** + **Z34b** + **Z35** + **Z36** + **Z37** + **Z38** + **Z39**`bash ./restart.sh --pull` | | **P0** | **生产 pull 本批** | Z10d + fingerprint + Z14c + **Z12h** + **数据恢复** + **Z34b** + **Z35** + **Z36** + **Z37** + **Z38** + **Z39** + **Z41**`bash ./restart.sh --pull` |
| **P0** | 成员手机 | 「宇信达」绑 **`13531041944`**;勿超管号 | | **P0** | 成员手机 | 「宇信达」绑 **`13531041944`**;勿超管号 |
| **P0** | 通道表白名单 | **勿**「填入测试默认」;形态 B 可空 | | **P0** | 通道表白名单 | **勿**「填入测试默认」;形态 B 可空 |
| **P1** | 凭票 Secret | `YuhengTicket.Secret``YXD_YUHENG_TICKET_SECRET` | | **P1** | 凭票 Secret | `YuhengTicket.Secret``YXD_YUHENG_TICKET_SECRET` |
@@ -1304,6 +1308,82 @@ POST /api/v1/agent/sync/channels/{id}/pull
4. 两次发布使用相同页面结构时,页面 API base path、Schema 和构建 revision 仍分别对应各自 slug。 4. 两次发布使用相同页面结构时,页面 API base path、Schema 和构建 revision 仍分别对应各自 slug。
5. 日志可直接看到每次请求的 requested slug 与最终物理 Schema/table且不存在“找不到映射后回退默认模块”。 5. 日志可直接看到每次请求的 requested slug 与最终物理 Schema/table且不存在“找不到映射后回退默认模块”。
### 5.38 【Z40 · 2026-08-07】模块选择超过 10 项时改用下拉
**状态****宇恒已改并通过 10/11 项边界测试;智建无需修改**。
#### 交互约定
1. 模块选项 **不超过 10 个**:保持当前横向按钮单选,便于直接点击。
2. 模块选项 **超过 10 个**:自动改为下拉选择,避免卡片过高、按钮换行拥挤。
3. 统一在公共 `ask_pick_from_list()` 实现,覆盖导入、编辑、查询、删除、打开模块等模块选择流程。
4. 提交值仍为原模块选项文本,后端 slug 映射和接口契约不变。
#### 验收
1. 10 个模块时组件类型为 `radio`11 个模块时组件类型为 `select`
2. 两种组件提交后均能准确取得所选模块 slug。
3. 智建 API、权限和发布逻辑无需调整。
### 5.39 【Z41 · 2026-08-07】商品展示需求被生成为通用记录 CRUD
**状态****智建已落实并通过回归测试(须生产 pull宇恒无需改**。
#### 现象与实查
模块显示名 `11`slug=`m_17ba079149`。用户生成需求明确为:
```text
商品展示页
```
生产 `GET /api/v1/apps/m_17ba079149/blueprint` 实查却返回:
- entity`record`label=`数据`
- fields`id / title / content / status`ID、标题、内容、状态
- pages`记录列表 / 新增记录 / 记录看板`
- status 枚举:`草稿 / 已发布`
- confidence`0.55`
- `meta.ui_preset=screenshot_faithful`,但本次需求未提供页面截图;
- generate run`18ac1b7071`
- 平台日志:`/runtime/logs/generate/run_18ac1b7071.log`
因此导入表单显示“数据|字段: ID、标题、内容、状态”并不是前端随机选错而是**智建 generate 阶段保存的蓝图本身已经是通用记录模板**。后续“导入数据”只向既有 resource 写数据,不会把记录页自动改造成商品页。
#### 智建排查/修改(须)
1. 读取生产日志 `/runtime/logs/generate/run_18ac1b7071.log`核对模型原始输出、JSON 修复、蓝图标准化、默认实体补齐各阶段,明确在哪一步把“商品展示页”变成 `record/title/content/status`
2. 全仓检查是否存在“生成失败/字段为空 → 固定补 `record + title/content/status`”的硬编码 fallback若存在不得在业务意图明确时静默套用。
3. 检查 `screenshot_faithful` 的设置条件:没有截图/HTML 时不得误设该 preset也不得生成截图专用的 section radios/legend 配置。
4. 增加业务语义校验prompt 命中“商品/商城/产品”时,蓝图至少应包含商品语义实体与字段,如名称、价格、图片、分类、库存、详情;若最终只有通用 record 字段,应判生成不合格并自动重试或明确失败。
5. `confidence=0.55` 这类低置信度结果不得无提示直接进入发布响应须带可理解的低置信度原因、fallback 标记及重试建议。
6. generate 回执增加 `fallback_used``fallback_reason``validation_warnings`,便于宇恒在推发布表单前阻断明显错误蓝图。
7. 商品 Excel 在 generate 时作为附件上传后,字段推断必须进入商品 entity若只在发布后执行独立“导入数据”平台应明确提示“导入不会改变既有页面/字段结构”。
#### 智建落实记录2026-08-07
1. **根因确认**:无数据附件时 `build_blueprint()` 无条件生成 `record + title/content/status`,置信度固定为 `0.55`;同时启发式函数误读注入 `SYSTEM_RULES` 后的 `effective_prompt`,规则文本自身含 `screenshot_faithful/概览`,因此无媒体也误加截图 preset/看板。LLM 又被要求保留既有结构;该链路与生产蓝图特征完全一致。
2. **商品语义基线**prompt 命中商品/产品/商城时,无附件生成 `product` 及商品名称、价格、图片、分类、库存、详情、上下架状态,并生成商品列表、新增、编辑页;不再进入通用 record 分支。
3. **商品附件归属**:商品 prompt 携带 Excel/CSV 时,主表实体固定归入 `product`,字段仍由真实表头推断;语义校验同时识别规范字段名和中文字段标签。
4. **防错误回退**LLM 润色后再次检查商品名称/价格/图片核心语义;若被改回通用 record则恢复已验证的商品启发式草案并把原因写入回执和 generate log。
5. **提示词与截图 preset 收口**:启发式意图、名称和页面判断只读取用户 prompt及真实 HTML 按钮),系统规则/视觉摘要仅供 LLM`screenshot_faithful` 只允许由实际图片/HTML 证据触发,并移除“仅凭 dashboard widget 结构强制截图模式”的路径。无媒体时即使 LLM 误设,也会在最终校验中清回 `default`
6. **可诊断回执**generate 增加 `fallback_used``fallback_reason``validation_warnings`,同值写入 `meta.source`;前端明确展示回退原因和语义告警,低置信度/回退结果要求确认后再发布。
7. **测试**:新增 4 项回归,覆盖无附件商品蓝图、通用 record 拒绝、商品 Excel 归属及无媒体清除 screenshot preset。
#### 宇恒配合
1. 当前生成请求已正确把用户文本“商品展示页”放入 prompt且发布后读取到的就是上述平台蓝图宇恒未将商品字段替换为通用字段。
2. 待智建回执提供 `fallback_used/validation_warnings` 后,宇恒可在发布前显示警告并禁止无确认发布。
3. 独立导入继续按蓝图字段预检;商品表头与 `ID/标题/内容/状态` 不匹配时应明确提示重新生成/编辑模块,而不是让用户误以为导入会重做页面。
#### 验收
1. 输入“商品展示页”且无附件,生成商品列表/商品详情/新增或编辑商品页面,不出现“记录列表/新增记录”通用壳。
2. entity/fields 至少覆盖商品名称、价格、图片、分类等核心语义;可按需求追加库存、状态和详情。
3. 无截图时 `ui_preset` 不得为 `screenshot_faithful`
4. 人为让模型输出无效 JSON平台不得静默发布通用 record 模板;应重试或返回明确 fallback/validation 错误。
5. 使用商品 Excel 参与生成时,页面列和表单字段与 Excel 商品表头匹配。
## 6. 联系与附件 ## 6. 联系与附件
- **待改清单(优先看)**§0.2**当前开发项已清 · B 运维 pull****改代码前先写本意见**§0.0);配合见 **§0.3** - **待改清单(优先看)**§0.2**当前开发项已清 · B 运维 pull****改代码前先写本意见**§0.0);配合见 **§0.3**