Files
ai_site/ai-service/generation_rules.py
2026-07-31 10:19:22 +08:00

220 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""生成时由服务端注入的规则,对用户不可见。"""
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 Blueprintentities / apis / pages / widgets
适用于任意行业,禁止把所有应用生成成同一种通用绿主题 CRUD 壳。
还原原则(全局):
- 有截图/HTML默认一模一样meta.ui_preset=screenshot_faithfuldashboard 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。
- RESTGET/POST/PUT/DELETE看板 widgetskpi/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_daysvalue_field+secondary_fieldcycle_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)