chore: initial commit of ai site platform
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
219
ai-service/generation_rules.py
Normal file
219
ai-service/generation_rules.py
Normal file
@@ -0,0 +1,219 @@
|
||||
"""生成时由服务端注入的规则,对用户不可见。"""
|
||||
|
||||
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,原文显示。
|
||||
- 数据: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)
|
||||
Reference in New Issue
Block a user