"""生成时由服务端注入的规则,对用户不可见。""" 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)