Enable Binding-scoped agent push/pull, empty-table schema ensure, SyncPage inspect/drop-table, default module import, and agent-bound publish docs from the 宇恒联调意见. Co-authored-by: Cursor <cursoragent@cursor.com>
223 lines
8.3 KiB
Python
223 lines
8.3 KiB
Python
"""生成时由服务端注入的规则,对用户不可见。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from typing import Any
|
||
|
||
# 用户明确不要按截图还原时才退出忠实模式
|
||
_SCREENSHOT_OPT_OUT = (
|
||
"不要还原截图",
|
||
"不按截图",
|
||
"无需还原截图",
|
||
"忽略截图布局",
|
||
"自定义布局",
|
||
"改成通用后台",
|
||
"不要一模一样",
|
||
"重新设计界面",
|
||
"不要照搬截图",
|
||
)
|
||
|
||
_SCREENSHOT_OPT_IN = (
|
||
"还原截图",
|
||
"对齐截图",
|
||
"一模一样",
|
||
"截图还原",
|
||
"screenshot_faithful",
|
||
)
|
||
|
||
DEFAULT_ACTION_LABELS: dict[str, str] = {
|
||
"create": "新增",
|
||
"refresh": "刷新",
|
||
"import": "导入 Excel",
|
||
"export": "导出 Excel",
|
||
"edit": "编辑",
|
||
"delete": "删除",
|
||
"save": "保存",
|
||
"cancel": "取消",
|
||
"back": "返回",
|
||
"search": "查询",
|
||
}
|
||
|
||
# 较长别名优先匹配
|
||
_ACTION_ALIASES: list[tuple[str, str]] = [
|
||
("导入 excel", "import"),
|
||
("导出 excel", "export"),
|
||
("导入xlsx", "import"),
|
||
("导出xlsx", "export"),
|
||
("导入csv", "import"),
|
||
("导出csv", "export"),
|
||
("导入", "import"),
|
||
("导出", "export"),
|
||
("新建", "create"),
|
||
("新增", "create"),
|
||
("添加", "create"),
|
||
("创建", "create"),
|
||
("刷新", "refresh"),
|
||
("重载", "refresh"),
|
||
("编辑", "edit"),
|
||
("修改", "edit"),
|
||
("删除", "delete"),
|
||
("移除", "delete"),
|
||
("保存", "save"),
|
||
("取消", "cancel"),
|
||
("返回", "back"),
|
||
("查询", "search"),
|
||
("搜索", "search"),
|
||
("筛选", "search"),
|
||
]
|
||
|
||
SYSTEM_RULES = """
|
||
[系统规则 · 用户不可见]
|
||
你是业务前端生成器。输出 App Blueprint(entities / apis / pages / widgets)。
|
||
适用于任意行业,禁止把所有应用生成成同一种通用绿主题 CRUD 壳。
|
||
|
||
还原原则(全局):
|
||
- 有截图/HTML:默认一模一样(meta.ui_preset=screenshot_faithful,dashboard layout.preset=screenshot_faithful)。
|
||
用户文字点名要改的才改;未提及的顶栏/导航/筛选/图表/表格/按键/配色保持与截图一致。
|
||
- 生成闭环(有截图时由服务端自动执行):视觉模型摘录 → 代码模型出蓝图 → **发布并截取 Web 真页面**
|
||
→ 视觉模型对照原图与真页面打分 → 按差异再改蓝图,直到还原度≥95%
|
||
(环境变量:FIDELITY_TARGET / FIDELITY_MAX_ROUNDS / FIDELITY_REAL_SCREEN)。
|
||
- 无截图:严格按用户文字与数据字段还原;勿套默认「记录列表/概览」。
|
||
- 业务名称、平台抬头、页签、按钮文案必须来自用户/截图/HTML,禁止擅自换成套话。
|
||
- 尽量写全 meta.platform_title / project_context / ui.nav_items / ui.filter_radios /
|
||
ui.select_field / ui.filter_hint / ui.*_side_label / ui.table_title(无材料则留空,禁止编造行业词)。
|
||
|
||
其它:
|
||
- 功能按键写入 pages[].layout.actions + action_labels,原文显示。
|
||
- 业务表 apis.resources[].operations **默认**含 list/get/create/update/delete/**import**/**export**;
|
||
list 页 layout.actions **默认**含 create/edit/delete/export/**import**/refresh。
|
||
需求未提「导入」也要加;仅用户明确禁止导入/导出时才去掉。
|
||
- 数据:Excel/CSV 表头→字段;JSON list→表;并行数组先展平再关联。snake_case;高基数不做 enum。
|
||
- REST:GET/POST/PUT/DELETE;看板 widgets(kpi/line_chart/status_strip/table/bar/pie)按截图或需求取舍。
|
||
- 全量导入:校验失败只记错误,不提前中断。
|
||
""".strip()
|
||
|
||
SCREENSHOT_RULES = """
|
||
[截图忠实 · 用户不可见 · 适用于任意业务]
|
||
已上传界面截图。目标:展示页与截图一模一样(screenshot_faithful)。
|
||
必须从截图提取并写入蓝图:平台抬头、系统名称、页签文案、筛选控件(单选/下拉)、工具栏按键原文、
|
||
主区分区(含左侧竖标签文案)、图表类型与系列名、表格列名。
|
||
主图为「着色测点/短棒 + 设计曲线」时:meta.ui.chart_style=section_marks,并写
|
||
value_field / color_field / design_field / legend_items / y_axis_label / invert_y。
|
||
监督条为上灰下绿双段时:status_strip.variant=stacked_days,value_field+secondary_field,cycle_days。
|
||
禁止只有侧标「主图」却不配 line_chart widget 或让主图区空白。
|
||
多张图若为同一页展开/收起/滚动态:合并为一套布局,以信息最完整的一帧为准,不要生成两套互斥页面。
|
||
禁止套用其它行业模板(含铁路沉降专用文案),除非截图本身就是该业务。
|
||
""".strip()
|
||
|
||
|
||
def screenshot_opted_out(prompt: str) -> bool:
|
||
p = prompt or ""
|
||
return any(k in p for k in _SCREENSHOT_OPT_OUT)
|
||
|
||
|
||
def wants_screenshot_layout(prompt: str, image_count: int, html_count: int = 0) -> bool:
|
||
"""有截图或页面 HTML 即默认忠实还原;用户显式退出除外。"""
|
||
p = prompt or ""
|
||
if screenshot_opted_out(p):
|
||
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
|
||
return False
|
||
|
||
|
||
def _map_button_label(raw: str) -> tuple[str, str] | None:
|
||
"""返回 (action_key, display_label)。"""
|
||
label = re.sub(r"\s+", " ", (raw or "").strip()).strip("。.;;")
|
||
if not label or len(label) > 32:
|
||
return None
|
||
low = label.lower().replace(" ", "")
|
||
for alias, key in _ACTION_ALIASES:
|
||
a = alias.replace(" ", "")
|
||
if low == a or low.startswith(a) or a in low:
|
||
return key, label
|
||
slug = re.sub(r"[^a-z0-9_]+", "_", low)[:24].strip("_") or "action"
|
||
return f"custom_{slug}", label
|
||
|
||
|
||
def parse_action_bar(prompt: str) -> dict[str, Any]:
|
||
"""从用户说明/截图理解中解析功能按键,得到 actions 顺序与 action_labels。"""
|
||
p = prompt or ""
|
||
labels: dict[str, str] = {}
|
||
order: list[str] = []
|
||
|
||
def add(raw: str) -> None:
|
||
mapped = _map_button_label(raw)
|
||
if not mapped:
|
||
return
|
||
key, lab = mapped
|
||
if key not in labels:
|
||
order.append(key)
|
||
labels[key] = lab
|
||
|
||
for m in re.finditer(
|
||
r"(?:操作|功能按键|按键|按钮|工具栏|工具条)[::]\s*(.+)",
|
||
p,
|
||
):
|
||
chunk = m.group(1).splitlines()[0]
|
||
chunk = re.split(r"[。;;((]", chunk, 1)[0]
|
||
for part in re.split(r"[、,,/||]\s*", chunk):
|
||
add(part)
|
||
|
||
for m in re.finditer(
|
||
r"(?:包含|含有|提供|支持)[::]?\s*((?:新增|新建|刷新|导入|导出|编辑|删除|查询|保存)[^。\n]{0,40})",
|
||
p,
|
||
):
|
||
for part in re.split(r"[、,,/||]\s*", m.group(1)):
|
||
add(part)
|
||
|
||
for m in re.finditer(r"(?:工具栏|顶栏)?按钮[::\s]+(.+)", p):
|
||
chunk = m.group(1).splitlines()[0][:80]
|
||
for part in re.split(r"[、,,/||\s]{1,}", chunk):
|
||
if part.strip():
|
||
add(part)
|
||
|
||
if not order:
|
||
return {}
|
||
|
||
for k in ("edit", "delete"):
|
||
if k not in labels:
|
||
labels[k] = DEFAULT_ACTION_LABELS[k]
|
||
|
||
toolbar = [k for k in order if k not in ("edit", "delete")]
|
||
actions = toolbar + [k for k in ("edit", "delete") if k in labels]
|
||
seen: set[str] = set()
|
||
uniq: list[str] = []
|
||
for k in actions:
|
||
if k not in seen:
|
||
seen.add(k)
|
||
uniq.append(k)
|
||
|
||
return {"actions": uniq, "action_labels": labels, "toolbar_from_user": True}
|
||
|
||
|
||
def compose_effective_prompt(
|
||
user_prompt: str,
|
||
image_count: int,
|
||
vision_text: str = "",
|
||
html_layout_text: str = "",
|
||
html_count: int = 0,
|
||
) -> str:
|
||
"""把系统规则拼进有效提示词;控制台只传用户原文。"""
|
||
parts = [SYSTEM_RULES]
|
||
if wants_screenshot_layout(user_prompt, image_count, html_count):
|
||
parts.append(SCREENSHOT_RULES)
|
||
if html_count:
|
||
parts.append(
|
||
"[HTML 布局优先 · 用户不可见]\n"
|
||
"已上传浏览器另存为的 HTML/MHTML。文案与控件名以 HTML 为准;分区与配色以截图为准。"
|
||
)
|
||
user = (user_prompt or "").strip()
|
||
if user:
|
||
parts.append("[用户需求]\n" + user)
|
||
else:
|
||
parts.append("[用户需求]\n(未填写文字需求:仅依据数据文件、截图与 HTML 生成。)")
|
||
if html_layout_text:
|
||
parts.append(html_layout_text)
|
||
if vision_text:
|
||
parts.append("[截图理解]\n" + vision_text)
|
||
return "\n\n".join(parts)
|