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:
@@ -151,6 +151,68 @@ def infer_item_label(prompt: str, sheet_label: str) -> str:
|
||||
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]:
|
||||
"""从用户需求提取展示元信息;截图/HTML 忠实策略由 generation_rules 决定。"""
|
||||
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"):
|
||||
rp["entity"] = resolve_entity(rp.get("entity"))
|
||||
|
||||
# 截图忠实:LLM 不得抹掉;有图或分区结构时强制恢复(不限行业)
|
||||
# 截图忠实只继承启发式基线中的明确证据;禁止 LLM 仅凭通用 widget 结构误设。
|
||||
fb_meta = fallback.get("meta") or {}
|
||||
fb_src = fb_meta.get("source") or {}
|
||||
force_faithful = (
|
||||
fb_meta.get("ui_preset") == "screenshot_faithful"
|
||||
or fb_src.get("screenshot_faithful") is True
|
||||
or bool(fb_src.get("image_refs"))
|
||||
or looks_like_faithful_dashboard(fallback)
|
||||
or looks_like_faithful_dashboard(draft)
|
||||
)
|
||||
if force_faithful:
|
||||
if fb_meta.get("platform_title"):
|
||||
@@ -1169,6 +1229,7 @@ def build_blueprint(
|
||||
primary_filters: list[str] = []
|
||||
primary_list_cols: list[str] = []
|
||||
label = "数据"
|
||||
product_intent = is_product_intent(prompt)
|
||||
|
||||
sheets = (excel_meta or {}).get("sheets") or []
|
||||
relations = (excel_meta or {}).get("relations") or []
|
||||
@@ -1176,6 +1237,8 @@ def build_blueprint(
|
||||
if sheets:
|
||||
for si, sheet in enumerate(sheets):
|
||||
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"}:
|
||||
ename = "record" if si == 0 else f"record_{si}"
|
||||
fields, filters, sorts, list_cols, confs = _fields_from_sheet(sheet)
|
||||
@@ -1213,7 +1276,8 @@ def build_blueprint(
|
||||
primary_fields = fields
|
||||
primary_filters = filters
|
||||
primary_list_cols = list_cols
|
||||
label = ent["label"]
|
||||
label = "商品" if product_intent else ent["label"]
|
||||
ent["label"] = label
|
||||
rc = sheet.get("row_count")
|
||||
if rc:
|
||||
warnings.append(f"主表 {ename} 识别到约 {rc} 行,发布后请导入完整 xlsx/json")
|
||||
@@ -1235,9 +1299,27 @@ def build_blueprint(
|
||||
)
|
||||
if 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:
|
||||
warnings.append("未上传数据文件,已生成通用文本字段草案")
|
||||
confidences.append(0.55)
|
||||
primary_fields = [
|
||||
{"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}},
|
||||
@@ -1248,10 +1330,17 @@ def build_blueprint(
|
||||
primary_list_cols = ["title", "content", "status"]
|
||||
entities = [{"name": "record", "table": "record", "label": "数据", "primary_key": "id", "fields": primary_fields, "indexes": []}]
|
||||
resources = [{
|
||||
"entity": "record",
|
||||
"path": "/records",
|
||||
"entity": primary_entity,
|
||||
"path": f"/{primary_entity}s",
|
||||
"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):
|
||||
@@ -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:
|
||||
dash_widgets = build_dashboard_widgets(
|
||||
primary_fields, primary_filters, primary_list_cols, primary_entity, item_label, prompt or ""
|
||||
@@ -1610,8 +1716,10 @@ async def generate(
|
||||
)
|
||||
|
||||
trace.stage("build_blueprint", "启发式草案")
|
||||
# 启发式意图/页面判断只读取用户输入;SYSTEM_RULES/视觉摘要仅供 LLM,
|
||||
# 否则规则文本中的 screenshot_faithful、概览等词会污染无附件生成。
|
||||
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 抓包:导航页签 / 空接口告警
|
||||
if excel_meta and isinstance(excel_meta, dict):
|
||||
@@ -1712,9 +1820,43 @@ async def generate(
|
||||
warnings.extend(llm_notes)
|
||||
trace.notes(llm_notes)
|
||||
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["meta"]["source"]["prompt"] = user_prompt
|
||||
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:
|
||||
draft["meta"]["source"]["llm_model"] = model_arg
|
||||
if upload is not None and upload.filename:
|
||||
@@ -1792,6 +1934,7 @@ async def generate(
|
||||
)
|
||||
|
||||
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()
|
||||
draft.setdefault("meta", {}).setdefault("source", {})
|
||||
draft["meta"]["source"]["generate_log_file"] = log_payload.get("log_file") or ""
|
||||
@@ -1799,7 +1942,10 @@ async def generate(
|
||||
"draft": draft,
|
||||
"warnings": warnings,
|
||||
"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_model": model_arg or "",
|
||||
"fidelity": fidelity_report,
|
||||
|
||||
@@ -119,8 +119,7 @@ def wants_screenshot_layout(prompt: str, image_count: int, html_count: int = 0)
|
||||
return False
|
||||
if image_count > 0 or html_count > 0:
|
||||
return True
|
||||
if any(k in p for k in _SCREENSHOT_OPT_IN):
|
||||
return True
|
||||
# 只有“还原截图”等文字但没有实际截图/HTML,不能启用截图专用 preset。
|
||||
return False
|
||||
|
||||
|
||||
|
||||
109
ai-service/test_z41_generation.py
Normal file
109
ai-service/test_z41_generation.py
Normal 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
2
web/dist/index.html
vendored
@@ -7,7 +7,7 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<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" />
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -417,6 +417,12 @@ export default function App() {
|
||||
: `还原度 ${fid.final_score ?? "—"}%(目标 ${fid.target}%),请查看差异后继续微调或再生成。`;
|
||||
}
|
||||
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 = "";
|
||||
if (session && res.draft?.meta?.slug) {
|
||||
try {
|
||||
@@ -440,7 +446,7 @@ export default function App() {
|
||||
}
|
||||
}
|
||||
setInfo(
|
||||
`草稿置信度 ${res.confidence}。${fidMsg}${draftNote}确认发布后将打开模块页。${runId}`
|
||||
`草稿置信度 ${res.confidence}。${fallbackMsg}${validationMsg}${fidMsg}${draftNote}确认发布后将打开模块页。${runId}`
|
||||
);
|
||||
} catch (e: any) {
|
||||
setError(e.message || String(e));
|
||||
|
||||
@@ -246,6 +246,9 @@ export async function generateDraft(
|
||||
warnings: string[];
|
||||
confidence: number;
|
||||
require_confirm: boolean;
|
||||
fallback_used?: boolean;
|
||||
fallback_reason?: string;
|
||||
validation_warnings?: string[];
|
||||
llm_provider?: string;
|
||||
llm_model?: string;
|
||||
fidelity?: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 联调后修改意见 · 宇恒松离线(形态 B)
|
||||
|
||||
> 初稿:2026-08-01 · 修订至 **2026-08-07**(+Z39 跨模块数据隔离/本地表唯一键)
|
||||
> 初稿:2026-08-01 · 修订至 **2026-08-07**(+Z41 商品需求被生成通用记录模板)
|
||||
> 焦点:**§0.2**(按负责方);**改代码前须先写入本意见**(见 §0.0)
|
||||
> 来源:宇恒 `yuhengyihao_client` ↔ 智建(生产 `aisite.yuxindazhineng.com`)
|
||||
> 依据:`松离线-dbsync方案-最终版.md`、`宇恒-松离线数据同步使用文档.md`
|
||||
@@ -35,6 +35,8 @@
|
||||
| **Z37 SyncPage LWW 403 吓人** | **智建已落实(须生产 pull)** | SyncPage 无冲突队列;中性提示 + 空通道说明;conflicts 403 静默(§5.35) |
|
||||
| **Z38 绑定即用 + 返回上一级** | **智建已落实(须生产 pull)** | 绑定自动补齐建站权限;超管公司视图全局返回平台(§5.36) |
|
||||
| **Z39 跨模块串数据/页面复用** | **双方已改;智建须生产 pull** | 智能体共享库内按 tenant+slug 分 Schema;页面路由防竞态/旧蓝图;回执、capsule、日志可核对映射和 revision(§5.37) |
|
||||
| **Z40 模块选择项过多** | **宇恒已改;智建无需** | 模块数 ≤10 保持按钮单选;>10 自动改为下拉选择(§5.38) |
|
||||
| **Z41 商品需求生成通用记录页** | **智建已落实(须生产 pull)** | 商品意图生成商品字段;语义不合格自动恢复;无媒体禁止 screenshot preset;回执带 fallback/validation(§5.39) |
|
||||
| **仍关注** | 用法 | 通道断了靠智建自愈;表数据靠宇恒双向指纹 / 数据恢复;不是「再点启动」 |
|
||||
|
||||
### 0.0 修改流程(冻结)
|
||||
@@ -81,6 +83,7 @@
|
||||
| **Z34** | **换机按账号恢复(2026-08-06 · 宇恒半程已改完)**:有手机号时可静默 ticket/confirm。**完整「仅宇恒 ID」见智建 Z34b(已落实)** |
|
||||
| **Z36** | **编辑发布无增量中文取消(2026-08-07 · 宇恒已改完;智建 Z36 已落实)**:宇恒侧预处理;智建 `host_meta_updated` / `[NO_BLUEPRINT_DELTA]`(§5.34) |
|
||||
| **Z39-YH** | **本地模块镜像表唯一键(2026-08-07 · 宇恒已改完)**:表名始终包含唯一 `slug`;发布建表、自动导入、独立导入、手动新增统一传递模块展示名/slug/实体标签。见 §5.37 |
|
||||
| **Z40** | **模块选择控件阈值(2026-08-07 · 宇恒已改完)**:选项 ≤10 使用按钮单选,>10 使用下拉;覆盖导入、编辑、查询、删除、打开模块等公共选择表单。**智建无需改** |
|
||||
|
||||
#### A′. 宇恒 · 配合注意(非阻塞新开发)
|
||||
|
||||
@@ -108,6 +111,7 @@
|
||||
| **Z37** | SyncPage 无冲突队列;中性 LWW 说明 +「该公司暂无通道」空态;`listSyncConflicts` 403 静默;见 §5.35 |
|
||||
| **Z38** | 超管进公司后顶栏「返回平台工作台」;绑定默认可带模块读/发权或列表明示权限;见 §5.36 |
|
||||
| **Z39-ZJ** | 智能体共享库不再把各模块都落到 `public`;改为 `app_t{tenant}_{slug}`,长 slug 带哈希防碰撞;页面切换防旧请求覆盖;见 §5.37 |
|
||||
| **Z41** | 无附件商品需求生成 `product`(名称/价格/图片/分类/库存/详情);商品 Excel 保留字段并归入 product;回执明示 fallback 与语义告警;见 §5.39 |
|
||||
|
||||
> 产品一句:**账号已绑定 ⇒ 落点信息固定;通道没了平台自动补,终端无需手填 channel_id。**
|
||||
|
||||
@@ -115,7 +119,7 @@
|
||||
|
||||
| 优先级 | 编号 | 项 | 说明 |
|
||||
|--------|------|----|------|
|
||||
| — | — | **当前开发项已清** | Z38、Z39 已落实;剩 B′ 生产 pull 与联调验收。 |
|
||||
| — | — | **当前开发项已清** | Z41 已落实;剩 B′ 生产 pull 与联调验收。 |
|
||||
|
||||
#### 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** | 通道表白名单 | **勿**「填入测试默认」;形态 B 可空 |
|
||||
| **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。
|
||||
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. 联系与附件
|
||||
|
||||
- **待改清单(优先看)**:§0.2(**当前开发项已清 · B′ 运维 pull**);**改代码前先写本意见**(§0.0);配合见 **§0.3**
|
||||
|
||||
Reference in New Issue
Block a user