navigation-menu-generator
帮助设计、优化和审核网站导航菜单,提升SEO和用户体验。
利用 Gemini Vision 和 AI 图像生成,从餐厅网址、PDF 或照片打造美观的 HTML 照片菜单。
openclaw skills install @ademczuk/menuvision命令、参数、文件名以原文为准
从 URL、PDF 或照片为任何餐厅构建精美的 HTML 照片菜单。
当用户想要为餐厅创建数字菜单时使用。触发词:"构建菜单"、"创建餐厅菜单"、"从 PDF 生成菜单"、"从照片生成菜单"、"数字菜单"、"menuvision"。
1. 提取: URL/PDF/照片 → menu_data.json (Gemini Vision)
2. 生成: menu_data.json → images/*.jpg (Gemini Image)
3. 构建: menu_data.json + images → Menu.html (CSS/JS 内联,图片相对路径)AI 助手会创建以下脚本:
| 脚本 | 用途 |
|---|---|
extract_menu.py | 从 URL/PDF/照片提取菜单数据 → 结构化 JSON |
generate_images.py | 通过 Gemini Image 生成食物照片 |
build_menu.py | 从 JSON + 图片构建 HTML 菜单(CSS/JS 内联,图片为相对路径) |
publish_menu.py | (可选)将 HTML 发布到 GitHub Pages |
所有三个流程阶段共享相同的 JSON 模式。AI 助手必须使用这些字段名称——任何偏差都会破坏流程。
{
"restaurant": {
"name": "餐厅名称(如可见)",
"cuisine": "菜系类型(中餐、印度菜、奥地利菜、日本料理等)",
"tagline": "任何副标题或标语"
},
"sections": [
{
"title": "分类名称(主要语言)",
"title_secondary": "分类名称(次要语言,如有;否则为空字符串)",
"category": "food 或 drink",
"note": "分类备注(例如“配米饭”、“周一至周五 11-15点”)",
"items": [
{
"code": "M1",
"name": "菜品名称(主要语言)",
"name_secondary": "名称(次要语言,如有)",
"description": "简短描述(主要语言)",
"description_secondary": "描述(次要语言,如有)",
"price": "12,90",
"price_prefix": "",
"allergens": "A C F",
"dietary": ["vegan", "spicy"],
"variants": []
}
]
}
],
"allergen_legend": {
"A": "含麸质的谷物",
"B": "甲壳类动物"
},
"metadata": {
"languages": ["German", "English"],
"currency": "EUR"
}
}| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
restaurant.name | 字符串 | 是 | HTML 标题中显示的餐厅名称 |
restaurant.cuisine | 字符串 | 是 | 传递给 build_food_prompt() 作为菜系上下文 |
restaurant.tagline | 字符串 | 否 | HTML 标题中的副标题行 |
sections[].title | 字符串 | 是 | 主要语言的分类标题 |
sections[].title_secondary | 字符串 | 否 | 次要语言的分类标题 |
sections[].category | "food" 或 "drink" | 是 | 决定食品网格或饮品列表布局。仅 "food" 类项目会生成图片 |
sections[].note | 字符串 | 否 | 分类级别备注(例如“配米饭”、“周一至周五 11-15点”) |
items[].code | 字符串 | 是 | 每个菜品唯一。与图片文件名关联。使用已有编码(M1, K2)或生成(A1, A2) |
items[].name | 字符串 | 是 | 主要语言。对于中日韩菜单,此为中文/日文/韩文名称 |
items[].name_secondary | 字符串 | 否 | 次要语言。对于中日韩菜单,此为英文/拉丁名称 |
items[].description | 字符串 | 否 | 简短描述。传递给 build_food_prompt() 用于图片生成 |
items[].description_secondary | 字符串 | 否 | 次要语言的描述 |
items[].price | 字符串 | 是 | 保留原始格式("12,90" 而非 "12.90") |
items[].price_prefix | 字符串 | 否 | 例如 "ab"(起价)、"ca."(约) |
items[].variants | 数组 | 否 | [{"label": "6块", "price": "8,90"}, ...] — 主价格设为最小变体的价格 |
items[].allergens | 字符串 | 否 | 按打印格式以空格分隔的编码:"A C F" |
items[].dietary | 数组 | 否 | ["vegan", "vegetarian", "spicy", "gluten-free", "halal", "kosher"] |
allergen_legend | 对象 | 否 | 过敏原编码到显示名称的映射:{"A": "含麸质的谷物", ...} |
metadata.currency | 字符串 | 是 | ISO 代码:"EUR", "USD", "JPY", "CNY", "THB" 等 |
metadata.languages | 数组 | 否 | 检测到的菜单语言:["German", "English"] |
将以下精确提示词发送给 Gemini。它定义了模式和提取规则。不要改写。
技能:MenuVision
版本:1.0.1
分块:2/6
你是一个餐厅菜单数据提取器。请分析以下菜单内容,并将所有项目提取为结构化 JSON。
返回以下精确的 JSON 结构:
{
"restaurant": {
"name": "餐厅名称(如果可见)",
"cuisine": "菜系类型(中餐、印度菜、奥地利菜、日料等)",
"tagline": "副标题或标语"
},
"sections": [
{
"title": "分区名称(主语言)",
"title_secondary": "分区名称(第二语言,若有,否则为空字符串)",
"category": "food 或 drink",
"note": "分区的备注(如‘配米饭’、‘周一至周五 11-15点’)",
"items": [
{
"code": "M1",
"name": "菜品名称(主语言)",
"name_secondary": "第二语言名称(若有)",
"description": "简要描述(主语言)",
"description_secondary": "第二语言描述(若有)",
"price": "12,90",
"price_prefix": "",
"allergens": "A C F",
"dietary": ["vegan", "spicy"],
"variants": []
}
]
}
],
"allergen_legend": {
"A": "麸质",
"B": "甲壳类"
},
"metadata": {
"languages": ["德语", "英语"],
"currency": "EUR"
}
}关键规则:
price_prefix。variants 数组: [{"label": "6 个", "price": "8,90"}, {"label": "12 个", "price": "15,90"}]
此时将主 price 设置为最小变体的价格。
allergen_legend 中。只返回有效的 JSON。不要使用 markdown 代码块,不要附加解释文字。
对于基于图像输入(截图、PDF 页面、照片),在基本提示前添加一行上下文:
EXTRACTION_PROMPT_VISION = (
"You are a restaurant menu data extractor. "
"This is a photo/scan of a restaurant menu page.\n\n"
"Return this exact JSON structure:"
+ EXTRACTION_PROMPT.split("Return this exact JSON structure:")[1]
)然后每种输入类型添加各自的前缀:
| 输入类型 | 在 EXTRACTION_PROMPT_VISION 前添加的前缀 |
|---|---|
| 截图 | "This is a screenshot of a restaurant menu webpage at {url}. Extract ALL visible menu items.\n\n" |
| PDF 页面 | "This is page {n} of a restaurant menu PDF. Extract ALL menu items from this page.\n\n" |
| 照片 | "This is a photograph of a restaurant menu. Extract ALL visible menu items.\n\n" |
| 文本(静态 HTML) | 直接使用 EXTRACTION_PROMPT(无需视觉变体) |
import os
from google import genai
client = genai.Client(api_key=os.environ["GOOGLE_API_KEY"])
def gemini_config():
return genai.types.GenerateContentConfig(
max_output_tokens=65536, # 64K — 大菜单需要
response_mime_type="application/json", # JSON 模式 — 关键
)
# 模型: gemini-2.5-flash (默认)
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=prompt_text, # 或 [image, prompt_text] (视觉输入)
config=gemini_config(),
)
# 始终检查截断
if response.candidates[0].finish_reason.name == "MAX_TOKENS":
print("WARNING: Response truncated. Menu may be incomplete.")使用以下精确函数。它模拟随意手机拍照的美感,使菜单看起来更真实。
def build_food_prompt(name: str, description: str, cuisine: str = "") -> str:
cuisine_context = f" {cuisine}" if cuisine else ""
food_desc = f"{name}"
if description and description != name:
food_desc += f" ({description})"
return (
f"A photo of {food_desc} at a{cuisine_context} restaurant. "
f"Taken casually with a phone from across the table at a 45-degree angle. "
f"The plate sits on a dark wooden table and takes up only 30% of the frame. "
f"Lots of visible table surface around the plate. Chopsticks, napkins, "
f"a glass of water, and small side dishes scattered naturally nearby. "
f"Blurred restaurant interior in the background — other diners, pendant lights, "
f"wooden chairs visible but out of focus. Warm ambient lighting. "
f"NOT a close-up. NOT professional food photography. "
f"It looks like someone quickly snapped a photo before eating."
)import os, io
from PIL import Image
from google import genai
client = genai.Client(api_key=os.environ["GOOGLE_API_KEY"])
def generate_gemini(client, name, description, output_path, cuisine=""):
prompt = build_food_prompt(name, description, cuisine)
response = client.models.generate_content(
model="gemini-2.5-flash-image", # 注意:不是 gemini-2.5-flash(该模型仅支持文本)
contents=prompt,
config=genai.types.GenerateContentConfig(
response_modalities=["TEXT", "IMAGE"], # 关键——请求图像输出
),
)
# 从响应部分中提取生成的图像
for part in response.candidates[0].content.parts:
if part.inline_data is not None:
img = Image.open(io.BytesIO(part.inline_data.data)).convert("RGB")
# 中心裁剪为正方形,调整为 800x800
w, h = img.size
side = min(w, h)
left = (w - side) // 2
top = (h - side) // 2
img = img.crop((left, top, left + side, top + side))
img = img.resize((800, 800), Image.LANCZOS)
img.save(str(output_path), "JPEG", quality=82)
return
raise RuntimeError("Gemini 响应中未找到图像")仅对 category == "food" 的章节生成图像。饮品在 HTML 输出中仅以纯文本列表形式显示。
菜单可以是任何语言。流水线通过双语字段和智能提示路由进行处理。
name / description = 主要语言(菜单主要使用的语言)name_secondary / description_secondary = 次要语言(若为双语)CJK 字符会导致图像提示不佳。在调用 build_food_prompt() 之前,请切换为拉丁名称:
def prepare_for_image_gen(name, name_secondary, description):
"""为图像提示使用拉丁字母名称。CJK → 使用次要名称。"""
display_name = name
if name_secondary:
if any(ord(c) > 0x2E80 for c in name): # CJK/韩文/假名检测
display_name = name_secondary
description = description or name
else:
description = description or name_secondary
return display_name, description**ord(c) > 0x2E80 覆盖的 Unicode 范围:**
name 呈现为大号显示文本name_secondary 在其下方以较小字体显示Noto Sans SC、Noto Sans JP、Noto Sans KR)所有文件名均从餐厅名称或来源 URL 推导:
stem = "shoyu" # 从 URL 域名、PDF 文件名或餐厅名称推导
data_file = f"menu_data_{stem}.json"
images_dir = Path(f"images/{stem}")
html_file = f"{restaurant_name}_Menu.html" # 例如:"Shoyu_Menu.html"images/{restaurant_stem}/{code}.jpg
# restaurant_stem = 数据文件名去掉 "menu_data_" 前缀
# 示例:menu_data_shoyu.json → images/shoyu/M1.jpg返回 POSIX 风格的路径字符串,并带有 ./ 前缀,以实现跨平台 HTML 兼容性:
def find_image(code: str, images_dir: Path):
"""返回指向图像的相对 POSIX 路径字符串,若无则返回 None。"""
if not images_dir.is_dir():
return None
rel = images_dir.as_posix()
if not rel.startswith("./"):
rel = "./" + rel
# 1. 精确匹配
for ext in ("jpg", "jpeg", "webp", "png"):
candidate = images_dir / f"{code}.{ext}"
if candidate.exists():
return f"{rel}/{code}.{ext}"
# 2. 不区分大小写的回退
for f in images_dir.iterdir():
if f.stem.lower() == code.lower() and f.suffix.lower() in (".jpg", ".jpeg", ".webp", ".png"):
return f"{rel}/{f.name}"
return None{RestaurantName}_Menu.html # CSS/JS 内联,图像为相对文件路径构建脚本使用 find_image() 解析每个食品项的照片,若无图像则回退为渐变色 SVG 占位符。
python
import base64
import html as html_mod
GRADIENT_COLORS = [
("#c41e3a", "#8b0000"), ("#ff6b6b", "#ee5a24"), ("#fdcb6e", "#e17055"),
("#00b894", "#00cec9"), ("#6c5ce7", "#a29bfe"), ("#e17055", "#d63031"),
("#00cec9", "#0984e3"), ("#fab1a0", "#e17055"), ("#e8a87c", "#d4956b"),
("#fd79a8", "#e84393"),
]
def make_placeholder_svg(code: str, name: str, secondary: str = "") -> str:
"""Generate a base64-encoded SVG placeholder when no image exists."""
idx = hash(code) % len(GRADIENT_COLORS)
c1, c2 = GRADIENT_COLORS[idx]
display = html_mod.escape(secondary[:12] if secondary else name[:12])
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="220" height="180" viewBox="0 0 220 180">
<defs><linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:{c1}"/>
<stop offset="100%" style="stop-color:{c2}"/>
</linearGradient></defs>
<rect width="220" height="180" fill="url(#g)" rx="12"/>
<text x="110" y="75" text-anchor="middle" fill="rgba(255,255,255,0.25)" font-size="56" font-family="serif">{html_mod.escape(code)}</text>
<text x="110" y="120" text-anchor="middle" fill="white" font-size="26" font-family="serif" opacity="0.9">{display}</text>
<text x="110" y="148" text-anchor="middle" fill="rgba(255,255,255,0.6)" font-size="11" font-family="sans-serif">{html_mod.escape(name[:30])}</text>
</svg>'''
b64 = base64.b64encode(svg.encode("utf-8")).decode("ascii")
return f"data:image/svg+xml;base64,{b64}"
def image_tag(code: str, name: str, secondary: str, images_dir: Path, portable: bool = False) -> str:
"""Return <img> tag — real image OR gradient SVG placeholder.
If portable=True, embed the real image as base64 data URI for single-file output."""
real = find_image(code, images_dir)
if real:
if portable:
img_path = images_dir.parent / real # resolve relative path
with open(img_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode("ascii")
return f'<img src="data:image/jpeg;base64,{b64}" alt="{html_mod.escape(name)}">'
return f'<img src="{html_mod.escape(real)}" alt="{html_mod.escape(name)}" loading="lazy">'
else:
src = make_placeholder_svg(code, name, secondary)
return f'<img src="{src}" alt="{html_mod.escape(name)}">'
### 输出模式
HTML 生成器支持两种输出模式,由 `--portable` 标志控制:
| 模式 | 标志 | 图片 | 输出 | 使用场景 |
|------|------|--------|--------|----------|
| **便携式**(默认) | `--portable` 或未设置 `GITHUB_*` 环境变量 | Base64 嵌入 HTML | 单个自包含 `.html` 文件 | 本地打开、邮件发送、拖放到任意主机 |
| **部署式** | `--no-portable` 或已设置 `GITHUB_*` 环境变量 | 相对路径(`./images/stem/code.jpg`) | HTML + `images/` 目录 | GitHub Pages、Netlify 等静态托管 |
**便携模式**将所有食物图片以 base64 数据 URI 的形式直接嵌入 HTML。文件体积较大(80 项菜单约 4~6 MB),但输出为单文件,无需任何托管设置即可在所有环境使用。未设置 `GITHUB_*` 环境变量时默认使用此模式。
**部署模式**使用相对图片路径,需要将 HTML 文件和 `images/` 目录一起托管。当发布到 GitHub Pages 或任何静态托管服务时使用此模式。
---
## 鲁棒性模式
### 重试逻辑
所有 Gemini API 调用应在临时故障时重试:import time
def call_with_retry(fn, *args, max_retries=3, **kwargs):
"""Retry API calls with exponential backoff."""
for attempt in range(max_retries):
try:
return fn(*args, **kwargs)
except Exception as e:
if attempt == max_retries - 1:
raise
wait = 2 ** attempt
print(f" Retry {attempt + 1}/{max_retries} in {wait}s: {e}")
time.sleep(wait)
### JSON 响应解析
Gemini 有时会将 JSON 包裹在 Markdown 围栏中或产生尾随逗号。防御性解析——先尝试原始解析,仅作为最后手段应用尾随逗号修复(无条件的修复可能会破坏包含 `,]` 模式的有效 JSON 字符串):import re, json
def parse_gemini_json(raw: str) -> dict:
"""Parse JSON from Gemini, handling markdown fences and quirks."""
text = raw.strip()
# Strip markdown code fences
if text.startswith("```"):
text = re.sub(r"^```\w*\n?", "", text)
text = re.sub(r"\n?```$", "", text)
text = text.strip()
# Try direct parse first
try:
return json.loads(text)
except json.JSONDecodeError:
pass
# Try extracting JSON object from surrounding text
match = re.search(r"\{.*\}", text, re.DOTALL)
if match:
candidate = match.group(0)
try:
return json.loads(candidate)
except json.JSONDecodeError:
pass
# Fix trailing commas and retry
candidate = re.sub(r",\s*([\]}])", r"\1", candidate)
try:
return json.loads(candidate)
except json.JSONDecodeError:
pass
# Last resort: fix trailing commas on original
text = re.sub(r",\s*([\]}])", r"\1", text)
return json.loads(text)
### 后处理
提取后,执行这些清理工作:def generate_codes(data: dict) -> dict:
"""Ensure every item has a unique code. Generates sequential codes per section
if items have empty/missing codes (e.g. A1, A2 for appetizers, M1, M2 for mains)."""
# ... assign prefix by section title, increment counter per section
return data
def normalize_prices(data: dict) -> dict:
"""Normalize price formats: numeric → string, strip currency symbols,
preserve comma/period format as-is."""
# ... convert float/int to string, strip €/$, etc.
return data
### 货币映射
将 ISO 货币代码映射到 HTML 输出的显示符号:CURRENCY_MAP = {
"EUR": "€", "USD": "$", "GBP": "£", "CHF": "CHF ",
"JPY": "¥", "CNY": "¥", "INR": "₹", "AUD": "A$",
"CAD": "C$", "SEK": "kr ", "NOK": "kr ", "DKK": "kr ",
"THB": "฿", "KRW": "₩", "HKD": "HK$", "SGD": "S$",
"CZK": "Kč ", "HUF": "Ft ", "PLN": "zł ", "TRY": "₺",
}
requests 获取页面 density = len(soup.get_text(strip=True)) / len(raw_html)
r"[$€£¥₹CHF]\s*\d+[.,]\d{2}|\d+[.,]\d{2}\s*[$€£¥₹]"),强制密度为 1.0(视为静态)seen_codes = set(),对于每个块中每个项目的 item["code"],如果已在 seen_codes 中则跳过。仅保留去重后仍有项目的 sections。€ 药丸),可循环或打开选择器:EUR、USD、AUD、CAD、GBP。使用构建时嵌入的快照汇率,在客户端转换所有显示价格。更新网格叠加、账单总计、饮品价格和变体价格。源货币来自 metadata.currency。./images/{stem}/{code}.jpg),仅 Google Fonts 为外部资源 family=Noto+Sans+SC:wght@400;700&family=Noto+Sans+JP:wght@400;700&family=Noto+Sans+KR:wght@400;700
font-family 堆栈:主要字体,然后是 'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR', sans-serif内嵌在 HTML 输出中的极简货币切换,全部在客户端完成,运行时无需 API 调用。
实现:
RATES 对象,包含构建时的快照汇率(基准:USD)metadata.currency 读取data-price 属性中(而非原始字符串如 "12,90")€)€)、USD ($)、GBP (£)、AUD (A$)、CAD (C$)data-price 值并更新显示文本convertPrice() 使用 SOURCE_CURRENCY 和 currentCurrency 转换localStorage价格解析辅助函数(构建时 — 将字符串价格转换为数字,用于 data-price 属性):
import re
def _parse_price_numeric(price: str) -> str:
"""将价格字符串解析为数字浮点数,用于 data-price 属性。"""
matches = re.findall(r"(\d+[.,]\d+)", price)
if matches:
return str(float(matches[-1].replace(",", ".")))
return "0"
# 在 HTML 模板中用法:
# <div class="price" data-price="{_parse_price_numeric(item['price'])}">€12,90</div>// 构建时嵌入的快照汇率(基准:USD)
const RATES = { EUR: 0.92, USD: 1.00, GBP: 0.79, AUD: 1.54, CAD: 1.36 };
const SYMBOLS = { EUR: "€", USD: "$", GBP: "£", AUD: "A$", CAD: "C$" };
const SOURCE_CURRENCY = "EUR"; // 来自 metadata.currency
function convertPrice(amount, fromCurrency, toCurrency) {
const inUSD = amount / RATES[fromCurrency];
return inUSD * RATES[toCurrency];
}
// 应用于:网格覆盖价格、饮品列表价格、变体价格
// 以及收据/选择选项卡总计(所有带 data-price 属性的元素)构建脚本应在构建时获取当前汇率(离线时使用合理的默认值)。价格以目标货币显示两位小数,并使用目标地区的格式。
--name "Restaurant Name" # 头部品牌文本
--tagline "Cuisine · City" # 副标题
--accent "#ff6b00" # 主色(药丸、激活选项卡、饮品价格)
--bg "#0a0a0a" # 背景颜色| 组件 | 成本 |
|---|---|
| 提取(每页) | ~$0.001 |
| 图像生成(每道菜品) | $0.039 |
| 80 道菜品 | ~$3.12 |
| 时间(80 道菜品) | ~8 分钟 |
饮品不生成图像(纯文本列表),因此实际成本取决于菜品与饮品的比例。
需要 Python 3.9+。
必需:
google-genai(提取 + 图像生成)Pillow(图像处理)对于 HTML 链接:
requests(HTTP 获取)beautifulsoup4(HTML 解析)对于 JS 渲染网站:
playwright(无头浏览器截图)对于 PDF 文件:
PyMuPDF(PDF 转图像)pip install google-genai Pillow requests beautifulsoup4 PyMuPDF
pip install playwright && playwright install chromiumGOOGLE_API_KEY — 提取和图像生成必需GITHUB_PAT — GitHub Pages 发布必需GITHUB_OWNER — 你的 GitHub 用户名(默认:从 git 配置读取)GITHUB_REPO — 你的 GitHub Pages 仓库名称(默认:menus)当未设置任何 GITHUB_* 环境变量时,流水线会生成一个自包含的 HTML 文件,其中图片以 base64 内嵌。用户可以:
无需设置托管环境,除了 GOOGLE_API_KEY 外无需任何 API 密钥,也无需配置 git。
对于希望拥有带多个菜单的永久展示页的用户:
your-username/menus)main 分支上启用 GitHub Pagesexport GITHUB_PAT="your-personal-access-token" # 必填——用于 git push 认证
export GITHUB_OWNER="your-username" # 必填——你的 GitHub 用户名
export GITHUB_REPO="menus" # 可选——默认为 "menus"重要: publish_menu.py 必须从环境变量中读取 GITHUB_OWNER 和 GITHUB_REPO。切勿将特定用户的仓库硬编码。生成的代码应动态构建仓库 URL:
owner = os.environ["GITHUB_OWNER"]
repo = os.environ.get("GITHUB_REPO", "menus")
GITHUB_REPO = f"{owner}/{repo}"
GITHUB_PAGES_BASE = f"https://{owner}.github.io/{repo}"python publish_menu.py Restaurant_Menu.html --name "Restaurant" --tagline "Cuisine · City" --cuisine Type展示页地址:https://<your-username>.github.io/<repo>/
publish_menu.py 将菜单仓库克隆到本地文件系统的一个临时目录中(git clone --depth=1),将文件复制到那里,提交并推送。这避免了在 Docker 容器中直接操作已挂载卷时出现的所有 NTFS 绑定挂载权限问题。
关键实现细节:
git clone --depth=1 到 tempfile.mkdtemp() 目录(本地文件系统,拥有正确的 POSIX 权限)shutil.copy()(而非 copy2)复制 HTML 和图片——避免在 NTFS 上因 os.chmod() 出现 EPERM 错误find_image_dirs 的正则表达式使用 [^/"]+(而非 [a-z_]+)以匹配图片目录名中的 Unicode 字符.meta_ JSON 附属文件index.htmlGITHUB_PAT 环境变量进行身份验证推送MENUS_REPO_DIR(绑定挂载路径)仅用于 --list 只读查询| 端点 | 发送的数据 | 用途 |
|---|---|---|
generativelanguage.googleapis.com | 菜单文本、页面截图、PDF 页面图片、食物照片提示词 | Gemini API 用于提取(JSON 模式)和图片生成 |
| 目标餐厅 URL | 仅 HTTP GET | 获取菜单页面 HTML 用于提取 |
api.github.com | 生成的 HTML 文件、图片文件 | 将菜单发布到 GitHub Pages(可选,需要 GITHUB_PAT) |
fonts.googleapis.com | 无(HTML 输出中的 CSS 链接) | Google Fonts 在浏览器中打开菜单 HTML 时由客户端加载 |
没有分析、遥测或跟踪功能。除上述端点外,不会向任何端点发送数据。
GOOGLE_API_KEY 从环境变量中读取,绝不会硬编码或记录到日志中images/ 目录中。发布时,仅上传到用户自己的 GitHub Pages 仓库已收录 1 个 Skill