diff --git a/.github/workflows/postmortem-check.yml b/.github/workflows/postmortem-check.yml index cfd47bb..9353c48 100644 --- a/.github/workflows/postmortem-check.yml +++ b/.github/workflows/postmortem-check.yml @@ -1,24 +1,29 @@ name: Postmortem Check -on: - pull_request: - branches: [main, feature] +on: + pull_request: + branches: [main, feature] paths: - 'app/**' - 'api/**' - 'tools/**' - 'public/**' - push: - branches: [main] - paths: - - 'app/**' - - 'api/**' - - 'tools/**' - - 'public/**' - -jobs: - postmortem-check: - runs-on: ubuntu-latest + push: + branches: [main] + paths: + - 'app/**' + - 'api/**' + - 'tools/**' + - 'public/**' + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + postmortem-check: + runs-on: ubuntu-latest steps: - name: Checkout code @@ -58,59 +63,66 @@ jobs: --threshold 0.7 \ --output text 2>&1 | tee postmortem_output.txt - # Save output for PR comment - echo "has_matches=false" >> $GITHUB_OUTPUT - if grep -q "\[BLOCK\]\|\[WARN\]" postmortem_output.txt; then - echo "has_matches=true" >> $GITHUB_OUTPUT - fi - continue-on-error: true - - - name: Comment on PR - if: github.event_name == 'pull_request' && steps.check.outputs.has_matches == 'true' - uses: actions/github-script@v8 - with: - script: | - const fs = require('fs'); - const output = fs.readFileSync('postmortem_output.txt', 'utf8'); - - const body = `## Postmortem Alert - - This PR may touch code related to past incidents. Please review: - - \`\`\` - ${output.substring(0, 3000)} - \`\`\` - - Run locally with: - \`\`\`bash - python tools/postmortem_check.py --base ${{ github.event.pull_request.base.sha }} - \`\`\` - `; - - // Check if we already commented - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - - const existingComment = comments.find(c => - c.user.login === 'github-actions[bot]' && - c.body.includes('Postmortem Alert') - ); - - if (existingComment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existingComment.id, - body: body - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: body - }); - } + # Save output for PR comment + echo "has_matches=false" >> $GITHUB_OUTPUT + echo "has_blocking=false" >> $GITHUB_OUTPUT + if grep -q "\[BLOCK\]\|\[WARN\]" postmortem_output.txt; then + echo "has_matches=true" >> $GITHUB_OUTPUT + fi + if grep -q "\[BLOCK\]" postmortem_output.txt; then + echo "has_blocking=true" >> $GITHUB_OUTPUT + fi + continue-on-error: true + + - name: Comment on PR + if: github.event_name == 'pull_request' && steps.check.outputs.has_blocking == 'true' + uses: actions/github-script@v8 + continue-on-error: true + with: + script: | + const fs = require('fs'); + const output = fs.readFileSync('postmortem_output.txt', 'utf8'); + const body = `## Postmortem Alert + +This PR may touch code related to past incidents. Please review: + +\`\`\` +${output.substring(0, 3000)} +\`\`\` + +Run locally with: +\`\`\`bash +python tools/postmortem_check.py --base ${{ github.event.pull_request.base.sha }} +\`\`\` +`; + + try { + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existingComment = comments.find(c => + c.user.login === 'github-actions[bot]' && + c.body.includes('Postmortem Alert') + ); + + if (existingComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body: body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + } + } catch (error) { + console.log(`Skipping PR comment: ${error.message}`); + } diff --git a/api/index.py b/api/index.py index b8aa04c..dd8beca 100644 --- a/api/index.py +++ b/api/index.py @@ -27,6 +27,7 @@ from slowapi.middleware import SlowAPIMiddleware # WhiteNoise将通过StaticFiles中间件集成,不需要ASGI↔WSGI转换 from api.routers import auth, payment, seo_pages +from app.i18n import detect_language, t as _t # 导入应用模块 try: @@ -228,6 +229,7 @@ class MeetSpotRequest(BaseModel): class AIChatRequest(BaseModel): message: str conversation_history: Optional[List[dict]] = [] + lang: Optional[str] = "" # MeetSpot AI客服系统提示词 MEETSPOT_SYSTEM_PROMPT = """你是MeetSpot(聚点)的AI Agent智能助手。MeetSpot是一款多人会面地点推荐的AI Agent,核心解决"在哪见面最公平"的问题。 @@ -274,6 +276,43 @@ class AIChatRequest(BaseModel): - 回答简洁明了,使用中文 - 如果用户问无关问题,礼貌引导了解产品功能""" +MEETSPOT_SYSTEM_PROMPT_EN = """You are the MeetSpot AI Assistant. MeetSpot is an AI Agent for multi-person meeting point recommendations, solving the problem of "where to meet that's fair for everyone." + +## Core Positioning +MeetSpot is not a simple search tool — it's a complete AI Agent: +- Map apps search "near me"; MeetSpot searches "between us" +- Review apps find "good places"; MeetSpot finds "good places that are fair for everyone" + +## Technical Highlights +1. **Spherical Geometry**: Uses Haversine formula for true surface midpoint calculation, 15-20% more accurate than planar algorithms +2. **GPT-4o Smart Scoring**: AI evaluates venues across multiple dimensions (distance, rating, parking, ambiance, transit) +3. **5-Step Transparent Reasoning**: Parse addresses → Calculate midpoint → Search nearby → GPT-4o scoring → Generate recommendations +4. **Explainable AI**: Users can see how the Agent "thinks" at every step — fully transparent + +## Capabilities +- **Coverage**: 350+ cities across China, powered by Amap data +- **Venue Types**: 12 themes (cafes, restaurants, libraries, KTV, gyms, escape rooms, etc.) +- **Smart Recognition**: 60+ university abbreviations pre-loaded +- **Group Size**: 2-10 participants + +## Response Times +- Single scenario: 5-8 seconds +- Dual scenario: 8-12 seconds +- Agent complex mode: 15-30 seconds + +## How to Use +1. Enter 2+ participant locations (addresses, landmarks, or abbreviations) +2. Choose venue type(s) (multiple allowed) +3. Optional: Set special requirements (parking, quiet environment, etc.) +4. Click recommend — get AI Agent results in 5-30 seconds + +## Response Guidelines +- Use friendly, professional tone +- Emphasize MeetSpot is an AI Agent, not a simple search tool +- Highlight "fairness," "transparent explainability," and "GPT-4o smart scoring" +- Keep answers concise and clear, respond in English +- If users ask unrelated questions, politely guide them to explore the product""" + # 预设问题列表 PRESET_QUESTIONS = [ {"id": 1, "question": "MeetSpot是什么?", "category": "产品介绍"}, @@ -284,6 +323,15 @@ class AIChatRequest(BaseModel): {"id": 6, "question": "是否收费?", "category": "其他"}, ] +PRESET_QUESTIONS_EN = [ + {"id": 1, "question": "What is MeetSpot?", "category": "Product"}, + {"id": 2, "question": "How does the AI Agent work?", "category": "Technology"}, + {"id": 3, "question": "What venue types are supported?", "category": "Features"}, + {"id": 4, "question": "How fast are recommendations?", "category": "Performance"}, + {"id": 5, "question": "How is it different from map apps?", "category": "Comparison"}, + {"id": 6, "question": "Is it free to use?", "category": "Other"}, +] + # 环境变量配置(用于 Vercel) AMAP_API_KEY = os.getenv("AMAP_API_KEY", "") AMAP_JS_API_KEY = os.getenv("AMAP_JS_API_KEY", "") # JS API key for frontend map @@ -292,6 +340,30 @@ class AIChatRequest(BaseModel): # 免费次数限制 FREE_DAILY_LIMIT = int(os.getenv("FREE_DAILY_LIMIT", "1")) + +def _parse_cors_origins(raw_value: str) -> List[str]: + origins = [origin.strip() for origin in raw_value.split(",") if origin.strip()] + return origins or ["*"] + + +def _get_client_ip(request: Request) -> str: + """获取客户端 IP,优先读取代理头。""" + forwarded = request.headers.get("x-forwarded-for") + if forwarded: + return forwarded.split(",")[0].strip() + return request.client.host if request.client else "unknown" + + +def _quota_exceeded_response(used_today: int, start_time: float, lang: str = "zh") -> dict: + return { + "success": False, + "need_payment": True, + "message": _t("api.error.quota_exceeded", lang), + "free_used": used_today, + "free_limit": FREE_DAILY_LIMIT, + "processing_time": time.time() - start_time, + } + # 创建 FastAPI 应用 app = FastAPI( title="MeetSpot", @@ -327,11 +399,13 @@ async def startup_database(): raise -# 配置CORS +# 配置CORS(生产环境禁止 "*" + credentials 组合) +cors_origins = _parse_cors_origins(os.getenv("CORS_ALLOW_ORIGINS", "*")) +cors_allow_all = "*" in cors_origins app.add_middleware( CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, + allow_origins=["*"] if cors_allow_all else cors_origins, + allow_credentials=not cors_allow_all, allow_methods=["*"], allow_headers=["*"], ) @@ -361,10 +435,11 @@ async def add_cache_headers(request: Request, call_next): return response async def _rate_limit_handler(request: Request, exc: RateLimitExceeded): - """全局限流处理器.""" + """Global rate limit handler.""" + lang = detect_language(request) return JSONResponse( status_code=429, - content={"detail": "请求过于频繁, 请稍后再试"}, + content={"detail": _t("api.error.rate_limit", lang)}, ) app.state.limiter = seo_pages.limiter @@ -380,21 +455,25 @@ async def _rate_limit_handler(request: Request, exc: RateLimitExceeded): if os.path.exists(workspace_dir): app.mount("/workspace", StaticFiles(directory=workspace_dir), name="workspace") - print("✅ 挂载 /workspace 静态文件") + logger.info("mounted_static_workspace") if os.path.exists("public"): app.mount("/public", StaticFiles(directory="public"), name="public") - print("✅ 挂载 /public 静态文件") + logger.info("mounted_static_public") if os.path.exists("docs"): app.mount("/docs-static", StaticFiles(directory="docs"), name="docs-static") - print("✅ 挂载 /docs 静态文件") + logger.info("mounted_static_docs") if os.path.exists("static"): app.mount("/static", StaticFiles(directory="static"), name="static") - print("✅ 挂载 /static 静态文件") + logger.info("mounted_static_assets") + + if os.path.exists("locales"): + app.mount("/locales", StaticFiles(directory="locales"), name="locales") + logger.info("mounted_static_locales") except Exception as e: - print(f"⚠️ 静态文件挂载失败: {e}") + logger.warning(f"静态文件挂载失败: {e}") # 在Vercel环境下,静态文件挂载可能失败,这是正常的 app.include_router(auth.router) @@ -502,27 +581,29 @@ async def get_config(): # ==================== AI 客服接口 ==================== @app.get("/api/ai_chat/preset_questions") -async def get_preset_questions(): - """获取预设问题列表""" +async def get_preset_questions(raw_request: Request): + """Get preset question list, language-aware.""" + lang = detect_language(raw_request) + questions = PRESET_QUESTIONS_EN if lang == "en" else PRESET_QUESTIONS return { "success": True, - "questions": PRESET_QUESTIONS + "questions": questions } @app.post("/api/ai_chat") -async def ai_chat(request: AIChatRequest): - """AI客服聊天接口""" +async def ai_chat(request: AIChatRequest, raw_request: Request = None): + """AI chat endpoint - bilingual.""" start_time = time.time() + lang = request.lang or (detect_language(raw_request) if raw_request else "zh") try: - print(f"🤖 [AI客服] 收到消息: {request.message[:50]}...") + print(f"[AI Chat] message: {request.message[:50]}...") if not llm_available: - # LLM不可用时返回预设回复 - print("⚠️ LLM模块不可用,使用预设回复") + print("LLM module unavailable, using fallback") return { "success": True, - "response": "抱歉,AI客服暂时不可用。您可以直接使用我们的会面点推荐功能,或查看页面上的使用说明。如有问题请稍后再试。", + "response": _t("api.chat.fallback", lang), "processing_time": time.time() - start_time, "mode": "fallback" } @@ -530,18 +611,17 @@ async def ai_chat(request: AIChatRequest): # 获取LLM API配置 llm_api_key = os.getenv("LLM_API_KEY", "") llm_api_base = os.getenv("LLM_API_BASE", "https://newapi.deepwisdom.ai/v1") - llm_model = os.getenv("LLM_MODEL", "deepseek-chat") # 默认使用deepseek,中文能力强 + llm_model = os.getenv("LLM_MODEL", "deepseek-chat") if not llm_api_key: - print("⚠️ LLM_API_KEY未配置") + print("LLM_API_KEY not configured") return { "success": True, - "response": "AI客服配置中,请稍后再试。您也可以直接体验我们的会面点推荐功能!", + "response": _t("api.chat.configuring", lang), "processing_time": time.time() - start_time, "mode": "fallback" } - # 使用openai库直接调用(兼容DeepWisdom API) from openai import AsyncOpenAI client = AsyncOpenAI( @@ -549,22 +629,19 @@ async def ai_chat(request: AIChatRequest): base_url=llm_api_base ) - # 构建消息列表 + system_prompt = MEETSPOT_SYSTEM_PROMPT_EN if lang == "en" else MEETSPOT_SYSTEM_PROMPT messages = [ - {"role": "system", "content": MEETSPOT_SYSTEM_PROMPT} + {"role": "system", "content": system_prompt} ] - # 添加历史对话(最多保留最近5轮) if request.conversation_history: - recent_history = request.conversation_history[-10:] # 最多10条消息 + recent_history = request.conversation_history[-10:] messages.extend(recent_history) - # 添加当前用户消息 messages.append({"role": "user", "content": request.message}) - print(f"🚀 [AI客服] 调用LLM ({llm_model}),消息数: {len(messages)}") + print(f"[AI Chat] calling LLM ({llm_model}), messages: {len(messages)}") - # 调用LLM response = await client.chat.completions.create( model=llm_model, messages=messages, @@ -575,7 +652,7 @@ async def ai_chat(request: AIChatRequest): ai_response = response.choices[0].message.content processing_time = time.time() - start_time - print(f"✅ [AI客服] 回复生成成功,耗时: {processing_time:.2f}秒") + print(f"[AI Chat] response generated, time: {processing_time:.2f}s") return { "success": True, @@ -585,10 +662,10 @@ async def ai_chat(request: AIChatRequest): } except Exception as e: - print(f"💥 [AI客服] 错误: {str(e)}") + print(f"[AI Chat] error: {str(e)}") return { "success": False, - "response": f"抱歉,AI客服遇到了问题。您可以直接使用会面点推荐功能,或稍后再试。", + "response": _t("api.chat.error", lang), "error": str(e), "processing_time": time.time() - start_time, "mode": "error" @@ -686,56 +763,44 @@ async def find_meetspot(request: MeetSpotRequest, raw_request: Request = None): - 复杂请求: Agent模式 (深度分析,3-8秒) """ start_time = time.time() + client_ip = _get_client_ip(raw_request) if raw_request else None + lang = detect_language(raw_request) if raw_request else "zh" # 免费次数限制检查 - if raw_request and FREE_DAILY_LIMIT > 0: + if client_ip and FREE_DAILY_LIMIT > 0: try: from app.db.database import AsyncSessionLocal from app.db import payment_crud - forwarded = raw_request.headers.get("x-forwarded-for") - client_ip = ( - forwarded.split(",")[0].strip() - if forwarded - else (raw_request.client.host if raw_request.client else "unknown") - ) - async with AsyncSessionLocal() as db: used_today = await payment_crud.get_free_usage_today(db, client_ip) if used_today >= FREE_DAILY_LIMIT: - return { - "success": False, - "need_payment": True, - "message": "今日免费次数已用完,请购买 credits 继续使用", - "free_used": used_today, - "free_limit": FREE_DAILY_LIMIT, - "processing_time": time.time() - start_time, - } + return _quota_exceeded_response(used_today, start_time, lang) except Exception as e: # 免费次数检查失败不阻塞主流程 - print(f"免费次数检查异常(不影响请求): {e}") + logger.warning(f"免费次数检查异常(不影响请求): {e}") # 并发控制:排队处理,保证每个请求都能完成 async with _request_semaphore: result = await _process_meetspot_request(request, start_time) # 请求成功后记录免费使用 - if raw_request and FREE_DAILY_LIMIT > 0 and isinstance(result, dict) and result.get("success"): + if client_ip and FREE_DAILY_LIMIT > 0 and isinstance(result, dict) and result.get("success"): try: from app.db.database import AsyncSessionLocal from app.db import payment_crud - forwarded = raw_request.headers.get("x-forwarded-for") - client_ip = ( - forwarded.split(",")[0].strip() - if forwarded - else (raw_request.client.host if raw_request.client else "unknown") - ) - async with AsyncSessionLocal() as db: - await payment_crud.record_free_use(db, client_ip) + consumed, used_today = await payment_crud.try_consume_free_use( + db=db, + ip_address=client_ip, + daily_limit=FREE_DAILY_LIMIT, + ) + if not consumed: + logger.info("free_quota_race_lost") + return _quota_exceeded_response(used_today, start_time, lang) except Exception as e: - print(f"记录免费使用异常: {e}") + logger.warning(f"记录免费使用异常: {e}") return result @@ -744,25 +809,27 @@ async def _process_meetspot_request(request: MeetSpotRequest, start_time: float) """实际处理推荐请求的内部函数""" # 评估请求复杂度 complexity = assess_request_complexity(request) - print(f"🧠 [智能路由] 复杂度评估: {complexity['complexity_score']}分, 模式: {complexity['mode_name']}") + logger.info( + f"[智能路由] 复杂度评估: {complexity['complexity_score']}分, 模式: {complexity['mode_name']}" + ) if complexity['reasons']: - print(f" 原因: {', '.join(complexity['reasons'])}") + logger.info(f"[智能路由] 触发原因: {', '.join(complexity['reasons'])}") try: - print(f"📝 收到请求: {request.model_dump()}") + logger.debug(f"收到请求: {request.model_dump()}") # 检查配置 - if config: + if config and getattr(config, "amap", None): api_key = config.amap.api_key - print(f"✅ 使用配置文件中的API密钥: {api_key[:10]}...") + logger.info("using_amap_key_source=config") else: api_key = AMAP_API_KEY - print(f"✅ 使用环境变量中的API密钥: {api_key[:10]}...") + logger.info("using_amap_key_source=env") if not api_key: raise HTTPException( status_code=500, - detail="高德地图API密钥未配置,请设置AMAP_API_KEY环境变量或配置config.toml文件" + detail=_t("api.error.amap_not_configured", "zh") ) # ========== 智能路由:根据复杂度选择模式 ========== @@ -910,7 +977,7 @@ async def _process_meetspot_request(request: MeetSpotRequest, start_time: float) print("❌ 配置未加载") raise HTTPException( status_code=500, - detail="服务配置错误:无法加载推荐模块,请确保在本地环境运行或正确配置Vercel环境变量" + detail=_t("api.error.config_error", "zh") ) except Exception as e: @@ -1076,23 +1143,6 @@ async def api_status(): "timestamp": time.time() } -# 静态文件服务(替代WhiteNoise,使用FastAPI原生StaticFiles) -# StaticFiles自带gzip压缩和缓存控制 -if os.path.exists("static"): - app.mount("/static", StaticFiles(directory="static"), name="static") -if os.path.exists("public"): - app.mount("/public", StaticFiles(directory="public", html=True), name="public") - -# 添加缓存控制头(用于静态资源,HTML 除外) -@app.middleware("http") -async def add_cache_headers(request: Request, call_next): - response = await call_next(request) - path = request.url.path - # 对静态资源添加长期缓存,HTML 文件走上层中间件的短缓存策略 - if path.startswith(("/static/", "/public/")) and not path.endswith(".html"): - response.headers["Cache-Control"] = "public, max-age=31536000" # 1年 - return response - # Vercel 处理函数 app_instance = app diff --git a/api/routers/seo_pages.py b/api/routers/seo_pages.py index 420f5b7..1ed49d5 100644 --- a/api/routers/seo_pages.py +++ b/api/routers/seo_pages.py @@ -14,15 +14,24 @@ from slowapi.util import get_remote_address from api.services.seo_content import seo_content_generator as seo_generator +from app.i18n import get_translations, detect_language, DEFAULT_LANG router = APIRouter() templates = Jinja2Templates(directory="templates") limiter = Limiter(key_func=get_remote_address) +BASE_URL = "https://meetspot-irq2.onrender.com" -def _common_context() -> dict: - """每次请求时动态读取的公共模板变量.""" - return {"baidu_tongji_id": os.getenv("BAIDU_TONGJI_ID", "")} + +def _common_context(request: Request, lang: str = "zh") -> dict: + """每次请求时动态读取的公共模板变量 + i18n.""" + t = get_translations(lang) + return { + "request": request, + "baidu_tongji_id": os.getenv("BAIDU_TONGJI_ID", ""), + "t": t, + "lang": lang, + } @lru_cache(maxsize=128) @@ -52,64 +61,100 @@ def _build_schema_list(*schemas: Dict) -> List[Dict]: return [schema for schema in schemas if schema] -@router.get("/", response_class=HTMLResponse) -@limiter.limit("60/minute") -async def homepage(request: Request): - """首页 - 提供SEO友好内容.""" +def _get_faqs(lang: str) -> List[Dict[str, str]]: + """从翻译文件构建 FAQ 列表.""" + t = get_translations(lang) + faqs = [] + for i in range(1, 13): + q_key = f"faq.q{i}" + a_key = f"faq.a{i}" + if q_key in t and a_key in t: + faqs.append({"question": t[q_key], "answer": t[a_key]}) + return faqs + + +def _lang_prefix(lang: str) -> str: + return "/en" if lang == "en" else "" + - meta_tags = seo_generator.generate_meta_tags("homepage", {}) +def _hreflang_links(path: str) -> List[Dict[str, str]]: + """生成 hreflang 链接对.""" + zh_path = path + en_path = f"/en{path}" if path != "/" else "/en/" + return [ + {"lang": "zh", "href": f"{BASE_URL}{zh_path}"}, + {"lang": "en", "href": f"{BASE_URL}{en_path}"}, + {"lang": "x-default", "href": f"{BASE_URL}{zh_path}"}, + ] + + +# --------------------------------------------------------------------------- +# Homepage +# --------------------------------------------------------------------------- + +def _render_homepage(request: Request, lang: str): + t = get_translations(lang) + prefix = _lang_prefix(lang) + meta_tags = { + "title": t.get("seo.home.title", "MeetSpot"), + "description": t.get("seo.home.description", ""), + "keywords": "聚会地点推荐,中点计算,meeting location,midpoint", + } faq_schema = seo_generator.generate_schema_org( "faq", - { - "faqs": [ - { - "question": "MeetSpot如何计算最佳聚会地点?", - "answer": "我们使用球面几何算法计算所有参与者位置的地理中点, 再推荐附近高评分场所。", - }, - { - "question": "MeetSpot支持多少人的聚会?", - "answer": "默认支持2-10人, 满足大多数团队与家人聚会需求。", - }, - { - "question": "需要付费吗?", - "answer": "MeetSpot完全免费且开源, 无需注册即可使用。", - }, - ] - }, + {"faqs": _get_faqs(lang)[:3]}, ) schema_list = _build_schema_list( seo_generator.generate_schema_org("webapp", {}), seo_generator.generate_schema_org("website", {}), seo_generator.generate_schema_org("organization", {}), seo_generator.generate_schema_org( - "breadcrumb", {"items": [{"name": "Home", "url": "/"}]} + "breadcrumb", {"items": [{"name": "Home", "url": f"{prefix}/"}]} ), faq_schema, ) - + canonical = f"{BASE_URL}{prefix}/" if lang == "en" else f"{BASE_URL}/" return templates.TemplateResponse( "pages/home.html", { - "request": request, - "meta_title": meta_tags["title"], - "meta_description": meta_tags["description"], + **_common_context(request, lang), + "meta_title": meta_tags["title"][:60], + "meta_description": meta_tags["description"][:155], "meta_keywords": meta_tags["keywords"], - "canonical_url": "https://meetspot-irq2.onrender.com/", + "canonical_url": canonical, "schema_jsonld": schema_list, "breadcrumbs": [], "cities": load_cities(), - **_common_context(), + "hreflang": _hreflang_links("/"), }, ) -@router.get("/meetspot/{city_slug}", response_class=HTMLResponse) +@router.get("/", response_class=HTMLResponse) @limiter.limit("60/minute") -async def city_page(request: Request, city_slug: str): +async def homepage(request: Request): + return _render_homepage(request, "zh") + + +@router.get("/en/", response_class=HTMLResponse) +@router.get("/en", response_class=HTMLResponse) +@limiter.limit("60/minute") +async def homepage_en(request: Request): + return _render_homepage(request, "en") + + +# --------------------------------------------------------------------------- +# City page +# --------------------------------------------------------------------------- + +def _render_city_page(request: Request, city_slug: str, lang: str): city = _get_city_by_slug(city_slug) if not city: raise HTTPException(status_code=404, detail="City not found") + t = get_translations(lang) + prefix = _lang_prefix(lang) + city_name = city.get("name_en", city.get("name")) if lang == "en" else city.get("name") meta_tags = seo_generator.generate_meta_tags( "city_page", { @@ -118,269 +163,279 @@ async def city_page(request: Request, city_slug: str): "venue_types": city.get("popular_venues", []), }, ) - breadcrumb = seo_generator.generate_schema_org( - "breadcrumb", - { - "items": [ - {"name": "Home", "url": "/"}, - {"name": city.get("name"), "url": f"/meetspot/{city_slug}"}, - ] - }, - ) + breadcrumb_items = [ + {"name": t.get("seo.breadcrumb.home", "Home"), "url": f"{prefix}/"}, + {"name": city_name, "url": f"{prefix}/meetspot/{city_slug}"}, + ] schema_list = _build_schema_list( seo_generator.generate_schema_org("webapp", {}), seo_generator.generate_schema_org("website", {}), seo_generator.generate_schema_org("organization", {}), - breadcrumb, + seo_generator.generate_schema_org("breadcrumb", {"items": breadcrumb_items}), ) - city_content = seo_generator.generate_city_content(city) - + city_content = seo_generator.generate_city_content(city, lang=lang) + path = f"/meetspot/{city_slug}" return templates.TemplateResponse( "pages/city.html", { - "request": request, - "meta_title": meta_tags["title"], - "meta_description": meta_tags["description"], + **_common_context(request, lang), + "meta_title": meta_tags["title"][:60], + "meta_description": meta_tags["description"][:155], "meta_keywords": meta_tags["keywords"], - "canonical_url": f"https://meetspot-irq2.onrender.com/meetspot/{city_slug}", + "canonical_url": f"{BASE_URL}{prefix}{path}", "schema_jsonld": schema_list, - "breadcrumbs": [ - {"name": "首页", "url": "/"}, - {"name": city.get("name"), "url": f"/meetspot/{city_slug}"}, - ], + "breadcrumbs": breadcrumb_items, "city": city, "city_content": city_content, - **_common_context(), + "hreflang": _hreflang_links(path), }, ) -@router.get("/about", response_class=HTMLResponse) -@limiter.limit("30/minute") -async def about_page(request: Request): - meta_tags = seo_generator.generate_meta_tags("about", {}) +@router.get("/meetspot/{city_slug}", response_class=HTMLResponse) +@limiter.limit("60/minute") +async def city_page(request: Request, city_slug: str): + return _render_city_page(request, city_slug, "zh") + + +@router.get("/en/meetspot/{city_slug}", response_class=HTMLResponse) +@limiter.limit("60/minute") +async def city_page_en(request: Request, city_slug: str): + return _render_city_page(request, city_slug, "en") + + +# --------------------------------------------------------------------------- +# About +# --------------------------------------------------------------------------- + +def _render_about(request: Request, lang: str): + t = get_translations(lang) + prefix = _lang_prefix(lang) + meta_tags = { + "title": t.get("seo.about.title", "About MeetSpot"), + "description": t.get("seo.home.description", ""), + "keywords": "about MeetSpot,meeting algorithm", + } + breadcrumb_items = [ + {"name": t.get("seo.breadcrumb.home", "Home"), "url": f"{prefix}/"}, + {"name": t.get("seo.breadcrumb.about", "About"), "url": f"{prefix}/about"}, + ] schema_list = _build_schema_list( seo_generator.generate_schema_org("organization", {}), - seo_generator.generate_schema_org( - "breadcrumb", - { - "items": [ - {"name": "Home", "url": "/"}, - {"name": "About", "url": "/about"}, - ] - }, - ) + seo_generator.generate_schema_org("breadcrumb", {"items": breadcrumb_items}), ) + path = "/about" return templates.TemplateResponse( "pages/about.html", { - "request": request, - "meta_title": meta_tags["title"], - "meta_description": meta_tags["description"], + **_common_context(request, lang), + "meta_title": meta_tags["title"][:60], + "meta_description": meta_tags["description"][:155], "meta_keywords": meta_tags["keywords"], - "canonical_url": "https://meetspot-irq2.onrender.com/about", + "canonical_url": f"{BASE_URL}{prefix}{path}", "schema_jsonld": schema_list, - "breadcrumbs": [ - {"name": "首页", "url": "/"}, - {"name": "关于我们", "url": "/about"}, - ], - **_common_context(), + "breadcrumbs": breadcrumb_items, + "hreflang": _hreflang_links(path), }, ) -@router.get("/how-it-works", response_class=HTMLResponse) +@router.get("/about", response_class=HTMLResponse) @limiter.limit("30/minute") -async def how_it_works(request: Request): - meta_tags = seo_generator.generate_meta_tags("how_it_works", {}) +async def about_page(request: Request): + return _render_about(request, "zh") + + +@router.get("/en/about", response_class=HTMLResponse) +@limiter.limit("30/minute") +async def about_page_en(request: Request): + return _render_about(request, "en") + + +# --------------------------------------------------------------------------- +# How it works +# --------------------------------------------------------------------------- + +def _render_how_it_works(request: Request, lang: str): + t = get_translations(lang) + prefix = _lang_prefix(lang) + meta_tags = { + "title": t.get("seo.how.title", "How It Works - MeetSpot"), + "description": t.get("how.hero_desc", ""), + "keywords": "MeetSpot guide,how to use,meeting point tutorial", + } how_to_schema = seo_generator.generate_schema_org( "how_to", { - "name": "使用MeetSpot AI Agent规划公平会面", - "description": "5步AI推理流程, 从输入地址到生成推荐, 5-30秒内完成。", + "name": t.get("how.hero_title", "How It Works"), + "description": t.get("how.hero_desc", ""), "total_time": "PT1M", "steps": [ - { - "name": "解析地址", - "text": "AI智能识别地址/地标/简称,'北大'自动转换为'北京市海淀区北京大学',校验经纬度。", - }, - { - "name": "计算中心点", - "text": "使用球面几何(Haversine公式)计算地球曲面真实中点,数学上对每个人公平。", - }, - { - "name": "搜索周边场所", - "text": "在中心点周边搜索匹配场景的POI,支持咖啡馆、餐厅、图书馆等12种场景主题。", - }, - { - "name": "GPT-4o智能评分", - "text": "AI对候选场所进行多维度评分:距离、评分、停车、环境、交通便利度。", - }, - { - "name": "生成推荐", - "text": "综合排序输出最优推荐,包含地图、评分、导航链接,可直接分享给朋友。", - }, + {"name": t.get("how.step1_title", ""), "text": t.get("how.step1_desc", "")}, + {"name": t.get("how.step2_title", ""), "text": t.get("how.step2_desc", "")}, + {"name": t.get("how.step3_title", ""), "text": t.get("how.step3_desc", "")}, + {"name": t.get("how.step4_title", ""), "text": t.get("how.step4_desc", "")}, + {"name": t.get("how.step5_title", ""), "text": t.get("how.step5_desc", "")}, ], "tools": ["MeetSpot AI Agent", "AMap API", "GPT-4o"], - "supplies": ["参与者地址", "场景选择", "特殊需求(可选)"], + "supplies": [ + t.get("how.step1_title", "Participant addresses"), + t.get("how.step2_title", "Venue type"), + t.get("how.step3_title", "Special requirements"), + ], }, ) + breadcrumb_items = [ + {"name": t.get("seo.breadcrumb.home", "Home"), "url": f"{prefix}/"}, + {"name": t.get("seo.breadcrumb.guide", "Guide"), "url": f"{prefix}/how-it-works"}, + ] schema_list = _build_schema_list( seo_generator.generate_schema_org("website", {}), seo_generator.generate_schema_org("organization", {}), - seo_generator.generate_schema_org( - "breadcrumb", - { - "items": [ - {"name": "Home", "url": "/"}, - {"name": "How it Works", "url": "/how-it-works"}, - ] - }, - ), + seo_generator.generate_schema_org("breadcrumb", {"items": breadcrumb_items}), how_to_schema, ) + path = "/how-it-works" return templates.TemplateResponse( "pages/how_it_works.html", { - "request": request, - "meta_title": meta_tags["title"], - "meta_description": meta_tags["description"], + **_common_context(request, lang), + "meta_title": meta_tags["title"][:60], + "meta_description": meta_tags["description"][:155], "meta_keywords": meta_tags["keywords"], - "canonical_url": "https://meetspot-irq2.onrender.com/how-it-works", + "canonical_url": f"{BASE_URL}{prefix}{path}", "schema_jsonld": schema_list, - "breadcrumbs": [ - {"name": "首页", "url": "/"}, - {"name": "使用指南", "url": "/how-it-works"}, - ], - **_common_context(), + "breadcrumbs": breadcrumb_items, + "hreflang": _hreflang_links(path), }, ) -@router.get("/faq", response_class=HTMLResponse) +@router.get("/how-it-works", response_class=HTMLResponse) @limiter.limit("30/minute") -async def faq_page(request: Request): - meta_tags = seo_generator.generate_meta_tags("faq", {}) - faqs = [ - { - "question": "MeetSpot 是什么?", - "answer": "MeetSpot(聚点)是一个智能会面地点推荐系统,帮助多人找到最公平的聚会地点。无论是商务会谈、朋友聚餐还是学习讨论,都能快速找到合适的场所。", - }, - { - "question": "支持多少人一起查找?", - "answer": "支持 2-10 个参与者位置,系统会根据所有人的位置计算最佳中点。", - }, - { - "question": "支持哪些城市?", - "answer": "目前覆盖北京、上海、广州、深圳、杭州等 350+ 城市,使用高德地图数据,持续扩展中。", - }, - { - "question": "可以搜索哪些类型的场所?", - "answer": "支持咖啡馆、餐厅、图书馆、KTV、健身房、密室逃脱等多种场所类型,还可以同时搜索多种类型(如'咖啡馆+餐厅')。", - }, - { - "question": "如何保证推荐公平?", - "answer": "系统使用几何中心算法,确保每位参与者到聚会地点的距离都在合理范围内,没有人需要跑特别远。", - }, - { - "question": "推荐结果如何排序?", - "answer": "基于评分、距离、用户需求的综合排序算法,优先推荐评分高、距离中心近、符合特殊需求的场所。", - }, - { - "question": "可以输入简称吗?", - "answer": "支持!系统内置 60+ 大学简称映射,如'北大'会自动识别为'北京大学'。也支持输入地标名称如'国贸'、'东方明珠'等。", - }, - { - "question": "是否免费?需要注册吗?", - "answer": "完全免费使用,无需注册,直接输入地址即可获得推荐结果。", - }, - { - "question": "推荐速度如何?", - "answer": "AI Agent 会经历完整的5步推理流程:解析地址 → 计算中心点 → 搜索周边 → GPT-4o智能评分 → 生成推荐。单场景5-8秒,双场景8-12秒,复杂Agent模式15-30秒。", - }, - { - "question": "和高德地图有什么区别?", - "answer": "高德搜索'我附近',MeetSpot搜索'我们中间'。我们先用球面几何算出多人公平中点,再推荐那里的好店。这是高德/百度都没有的功能。", - }, - { - "question": "AI Agent是什么意思?", - "answer": "MeetSpot不是简单的搜索工具,而是一个AI Agent。它有5步完整的推理链条,使用GPT-4o进行多维度评分(距离、评分、停车、环境),你可以看到AI每一步是怎么'思考'的,完全透明可解释。", - }, - { - "question": "如何反馈问题或建议?", - "answer": "欢迎通过 GitHub Issues 反馈问题或建议,也可以发送邮件至 Johnrobertdestiny@gmail.com。", - }, +async def how_it_works(request: Request): + return _render_how_it_works(request, "zh") + + +@router.get("/en/how-it-works", response_class=HTMLResponse) +@limiter.limit("30/minute") +async def how_it_works_en(request: Request): + return _render_how_it_works(request, "en") + + +# --------------------------------------------------------------------------- +# FAQ +# --------------------------------------------------------------------------- + +def _render_faq(request: Request, lang: str): + t = get_translations(lang) + prefix = _lang_prefix(lang) + faqs = _get_faqs(lang) + meta_tags = { + "title": t.get("seo.faq.title", "FAQ - MeetSpot"), + "description": t.get("faq.hero_desc", ""), + "keywords": "MeetSpot FAQ,meeting point help", + } + breadcrumb_items = [ + {"name": t.get("seo.breadcrumb.home", "Home"), "url": f"{prefix}/"}, + {"name": t.get("seo.breadcrumb.faq", "FAQ"), "url": f"{prefix}/faq"}, ] schema_list = _build_schema_list( seo_generator.generate_schema_org("website", {}), seo_generator.generate_schema_org("organization", {}), seo_generator.generate_schema_org("faq", {"faqs": faqs}), - seo_generator.generate_schema_org( - "breadcrumb", - { - "items": [ - {"name": "Home", "url": "/"}, - {"name": "FAQ", "url": "/faq"}, - ] - }, - ), + seo_generator.generate_schema_org("breadcrumb", {"items": breadcrumb_items}), ) + path = "/faq" return templates.TemplateResponse( "pages/faq.html", { - "request": request, - "meta_title": meta_tags["title"], - "meta_description": meta_tags["description"], + **_common_context(request, lang), + "meta_title": meta_tags["title"][:60], + "meta_description": meta_tags["description"][:155], "meta_keywords": meta_tags["keywords"], - "canonical_url": "https://meetspot-irq2.onrender.com/faq", + "canonical_url": f"{BASE_URL}{prefix}{path}", "schema_jsonld": schema_list, - "breadcrumbs": [ - {"name": "首页", "url": "/"}, - {"name": "常见问题", "url": "/faq"}, - ], + "breadcrumbs": breadcrumb_items, "faqs": faqs, - **_common_context(), + "hreflang": _hreflang_links(path), }, ) +@router.get("/faq", response_class=HTMLResponse) +@limiter.limit("30/minute") +async def faq_page(request: Request): + return _render_faq(request, "zh") + + +@router.get("/en/faq", response_class=HTMLResponse) +@limiter.limit("30/minute") +async def faq_page_en(request: Request): + return _render_faq(request, "en") + + +# --------------------------------------------------------------------------- +# Sitemap & Robots +# --------------------------------------------------------------------------- + @router.api_route("/sitemap.xml", methods=["GET", "HEAD"]) async def sitemap(): - base_url = "https://meetspot-irq2.onrender.com" today = datetime.now().strftime("%Y-%m-%d") - urls = [ + pages = [ {"loc": "/", "priority": "1.0", "changefreq": "daily"}, {"loc": "/about", "priority": "0.8", "changefreq": "monthly"}, {"loc": "/faq", "priority": "0.8", "changefreq": "weekly"}, {"loc": "/how-it-works", "priority": "0.7", "changefreq": "monthly"}, ] - city_urls = [ - { - "loc": f"/meetspot/{city['slug']}", - "priority": "0.9", - "changefreq": "weekly", - } + city_pages = [ + {"loc": f"/meetspot/{city['slug']}", "priority": "0.9", "changefreq": "weekly"} for city in load_cities() ] + all_pages = pages + city_pages + entries = [] - for item in urls + city_urls: + for item in all_pages: + zh_url = f"{BASE_URL}{item['loc']}" + en_loc = f"/en{item['loc']}" if item["loc"] != "/" else "/en/" + en_url = f"{BASE_URL}{en_loc}" + hreflang_zh = f' ' + hreflang_en = f' ' + hreflang_default = f' ' + # Chinese URL entry entries.append( - f" \n {base_url}{item['loc']}\n {today}\n {item['changefreq']}\n {item['priority']}\n " + f" \n" + f" {zh_url}\n" + f" {today}\n" + f" {item['changefreq']}\n" + f" {item['priority']}\n" + f"{hreflang_zh}\n{hreflang_en}\n{hreflang_default}\n" + f" " ) + # English URL entry + entries.append( + f" \n" + f" {en_url}\n" + f" {today}\n" + f" {item['changefreq']}\n" + f" {item['priority']}\n" + f"{hreflang_zh}\n{hreflang_en}\n{hreflang_default}\n" + f" " + ) + sitemap_xml = ( - "\n" - "\n" + '\n' + '\n' + "\n".join(entries) + "\n" ) - # Long cache with stale-while-revalidate to handle Render cold starts - # CDN can serve stale content while revalidating in background return Response( content=sitemap_xml, media_type="application/xml", headers={ "Cache-Control": "public, max-age=86400, stale-while-revalidate=604800", - "X-Robots-Tag": "noindex", # Sitemap itself shouldn't be indexed + "X-Robots-Tag": "noindex", }, ) @@ -388,8 +443,7 @@ async def sitemap(): @router.api_route("/robots.txt", methods=["GET", "HEAD"]) async def robots_txt(): today = datetime.now().strftime("%Y-%m-%d") - robots = f"""# MeetSpot Robots.txt\n# Generated: {today}\n\nUser-agent: *\nAllow: /\nCrawl-delay: 1\n\nDisallow: /admin/\nDisallow: /api/internal/\nDisallow: /*.json$\n\nSitemap: https://meetspot-irq2.onrender.com/sitemap.xml\n\nUser-agent: Googlebot\nAllow: /\n\nUser-agent: Baiduspider\nAllow: /\n\nUser-agent: GPTBot\nDisallow: /\n\nUser-agent: CCBot\nDisallow: /\n""" - # Long cache with stale-while-revalidate to handle Render cold starts + robots = f"""# MeetSpot Robots.txt\n# Generated: {today}\n\nUser-agent: *\nAllow: /\nCrawl-delay: 1\n\nDisallow: /admin/\nDisallow: /api/internal/\nDisallow: /*.json$\n\nSitemap: {BASE_URL}/sitemap.xml\n\nUser-agent: Googlebot\nAllow: /\n\nUser-agent: Baiduspider\nAllow: /\n\nUser-agent: GPTBot\nDisallow: /\n\nUser-agent: CCBot\nDisallow: /\n""" return Response( content=robots, media_type="text/plain", diff --git a/api/services/seo_content.py b/api/services/seo_content.py index 2e585cd..4e91475 100644 --- a/api/services/seo_content.py +++ b/api/services/seo_content.py @@ -213,9 +213,13 @@ def generate_schema_org(self, page_type: str, data: Dict) -> Dict: } return {} - def generate_city_content(self, city_data: Dict) -> Dict[str, str]: + def generate_city_content(self, city_data: Dict, lang: str = "zh") -> Dict[str, str]: """生成城市页面内容块, 使用丰富的城市数据.""" + from app.i18n import get_translations + t = get_translations(lang) + city = city_data.get("name", "") + city_display = city_data.get("name_en", city) if lang == "en" else city city_en = city_data.get("name_en", "") tagline = city_data.get("tagline", "") description = city_data.get("description", "") @@ -254,59 +258,67 @@ def generate_city_content(self, city_data: Dict) -> Dict[str, str]:

{scenario}

{example}

''' + section_title = t.get("city.use_cases_title", "").replace("{city}", city_display) use_cases_html = f'''
-

{city}真实使用场景

+

{section_title}

{cases_items}
''' # 生成场所类型 - venues_html = "、".join(popular_venues[:4]) if popular_venues else "咖啡馆、餐厅" + joiner = ", " if lang == "en" else "、" + venues_html = joiner.join(popular_venues[:4]) if popular_venues else ("cafes, restaurants" if lang == "en" else "咖啡馆、餐厅") + + # Helper to resolve template strings + def _t(key: str) -> str: + return t.get(key, key).replace("{city}", city_display).replace("{count}", str(metro_lines)).replace("{venues}", venues_html) + + intro_title = f"{city_display} Meeting Point Finder - {city_en}" if lang == "en" else f"{city}聚会地点推荐 - {city_en}" content = { "intro": f'''
-

{city}聚会地点推荐 - {city_en}

+

{intro_title}

{tagline}

{description}

''', "features": f'''
-

为什么在{city}使用MeetSpot?

+

{_t("city.features_title")}

🚇
-

{metro_lines}条地铁线路

-

{city}地铁网络发达,MeetSpot优先推荐地铁站周边的聚会场所

+

{_t("city.metro_title")}

+

{_t("city.metro_desc")}

🎯
-

智能中点计算

-

球面几何算法确保每位参与者通勤距离公平均衡

+

{_t("city.midpoint_title")}

+

{_t("city.midpoint_desc")}

📍
-

本地精选场所

-

覆盖{city}{venues_html}等热门类型,高评分场所优先推荐

+

{_t("city.local_title")}

+

{_t("city.local_desc")}

''', "landmarks": f'''
-

{city}热门聚会区域

+

{_t("city.landmarks_title")}

-

地标商圈

+

{_t("city.landmarks_group")}

{landmarks_html}
-

商务中心

+

{_t("city.districts_group")}

{districts_html}
-

高校聚集区

+

{_t("city.universities_group")}

{universities_html}
@@ -316,7 +328,7 @@ def generate_city_content(self, city_data: Dict) -> Dict[str, str]: "local_tips": f'''
-

{city}聚会小贴士

+

{_t("city.tips_title")}

💡

{local_tips}

@@ -325,27 +337,27 @@ def generate_city_content(self, city_data: Dict) -> Dict[str, str]: "how_it_works": f'''
-

如何在{city}找到最佳聚会地点?

+

{_t("city.how_title")}

1
-

输入参与者位置

-

支持输入{city}任意地址、地标或高校名称(如{university_clusters[0] if university_clusters else "当地高校"})

+

{_t("city.how_step1_title")}

+

{_t("city.how_step1_desc")}

2
-

选择场所类型

-

根据聚会目的选择{venues_html}等场景

+

{_t("city.how_step2_title")}

+

{_t("city.how_step2_desc")}

3
-

获取智能推荐

-

系统自动计算地理中点,推荐{landmarks[0] if landmarks else "市中心"}等区域的高评分场所

+

{_t("city.how_step3_title")}

+

{_t("city.how_step3_desc")}

@@ -353,9 +365,9 @@ def generate_city_content(self, city_data: Dict) -> Dict[str, str]: "cta": f'''
-

开始规划{city}聚会

-

无需注册,输入地址即可获取推荐

- 立即使用 MeetSpot +

{_t("city.cta_title")}

+

{_t("city.cta_desc")}

+ {_t("city.cta_btn")}
''', } diff --git a/app/i18n.py b/app/i18n.py new file mode 100644 index 0000000..9b5bdd2 --- /dev/null +++ b/app/i18n.py @@ -0,0 +1,72 @@ +"""轻量级 i18n 模块 - JSON 翻译文件加载与缓存. + +启动时加载 locales/{lang}.json 到内存,提供 get_translations(lang) 函数。 +""" +from __future__ import annotations + +import json +import os +from typing import Dict + +_LOCALES_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "locales") +_cache: Dict[str, Dict[str, str]] = {} +SUPPORTED_LANGS = ("zh", "en") +DEFAULT_LANG = "zh" + + +def _load(lang: str) -> Dict[str, str]: + """从磁盘加载翻译文件并缓存.""" + filepath = os.path.join(_LOCALES_DIR, f"{lang}.json") + if not os.path.exists(filepath): + return {} + with open(filepath, "r", encoding="utf-8") as fh: + return json.load(fh) + + +def get_translations(lang: str) -> Dict[str, str]: + """返回指定语言的翻译字典(带内存缓存). + + 如果 lang 不在支持列表中,回退到默认语言。 + """ + if lang not in SUPPORTED_LANGS: + lang = DEFAULT_LANG + if lang not in _cache: + _cache[lang] = _load(lang) + return _cache[lang] + + +def t(key: str, lang: str = DEFAULT_LANG) -> str: + """翻译单个 key,找不到时返回 key 本身.""" + translations = get_translations(lang) + return translations.get(key, key) + + +def detect_language(request) -> str: + """从 FastAPI Request 对象检测语言. + + 优先级: URL 前缀 /en/ > Cookie 'lang' > Accept-Language header > 默认中文 + """ + # 1. URL 前缀 + path = request.url.path + if path.startswith("/en/") or path == "/en": + return "en" + + # 2. Cookie + lang_cookie = request.cookies.get("lang") + if lang_cookie in SUPPORTED_LANGS: + return lang_cookie + + # 3. Accept-Language header + accept_lang = request.headers.get("accept-language", "") + if accept_lang: + # 简单解析:检查 en 是否在 accept-language 中且优先级高于 zh + parts = accept_lang.lower().split(",") + for part in parts: + lang_tag = part.split(";")[0].strip() + if lang_tag.startswith("en"): + return "en" + if lang_tag.startswith("zh"): + return "zh" + + # 4. 默认中文 + return DEFAULT_LANG diff --git a/docs/plans/2026-03-08-feat-add-english-multilingual-plan.md b/docs/plans/2026-03-08-feat-add-english-multilingual-plan.md new file mode 100644 index 0000000..6163b25 --- /dev/null +++ b/docs/plans/2026-03-08-feat-add-english-multilingual-plan.md @@ -0,0 +1,274 @@ +--- +title: "feat: Add English Multilingual Support" +type: feat +date: 2026-03-08 +--- + +# feat: Add English Multilingual Support (i18n) + +## Overview + +MeetSpot 全站双语支持(中文 + 英文)。当前 ~540+ 中文硬编码字符串分布在 13 个文件中,无任何 i18n 基础设施。采用轻量级 JSON 翻译文件方案,避免引入重型框架(512MB Render 免费层限制)。 + +分两期交付: +- **Phase 1**: 营销页面 + Finder UI + API 响应 + SEO 页面 + AI Chat +- **Phase 2**: 生成的推荐结果 HTML(`_generate_html_content()`) + +## Problem Statement / Motivation + +MeetSpot 有国际用户访问(GitHub 520 stars 来自全球),但整站只有中文。英文用户无法理解界面、错误提示和 SEO 页面。多语言支持是扩大用户群的基础。 + +## Proposed Solution + +### Architecture: Lightweight JSON i18n + +``` +locales/ + zh.json # 中文翻译(从现有硬编码字符串提取) + en.json # 英文翻译 + +app/i18n.py # 翻译加载 + 缓存 + 工具函数 +``` + +**核心设计决策:** + +1. **翻译文件**: `locales/zh.json` + `locales/en.json`,启动时加载到内存缓存(~100KB,可忽略) +2. **语言检测链**: URL 前缀 `/en/` > Cookie `lang` > `Accept-Language` header > 默认中文 +3. **URL 路由**: 中文保持现有 URL(无前缀),英文加 `/en/` 前缀。向后兼容 +4. **Jinja2 模板**: 通过 context dict 注入翻译函数 `t()` +5. **Finder HTML**: 客户端 JS i18n,`data-i18n` 属性 + fetch JSON +6. **AMap 限制**: 保持高德地图,英文界面标注"地址建议为中文",Google Maps 作为后续迭代 + +### Translation Key Convention + +点分隔扁平 key,按页面/组件分组: + +```json +{ + "nav.home": "首页", + "nav.about": "关于", + "nav.guide": "使用指南", + "nav.faq": "FAQ", + "home.hero_title": "智能聚会地点推荐", + "home.hero_subtitle": "多人聚会,一键找到公平中点", + "finder.submit": "查找最佳会面点", + "finder.amap_note": "地址建议为中文(使用高德地图)", + "api.error.quota_exceeded": "今日免费次数已用完", + "api.error.rate_limit": "请求过于频繁, 请稍后再试", + "chat.welcome": "你好!我是 MeetSpot AI...", + "seo.home.title": "MeetSpot 聚点 - 多人聚会地点智能推荐", + "seo.home.description": "..." +} +``` + +### Route Architecture + +```python +# api/routers/seo_pages.py + +# 现有路由保持不变(中文) +@router.get("/") +async def homepage(request: Request): + return _render_page(request, "home", lang="zh") + +# 英文路由:/en/ 前缀 +@router.get("/en/") +async def homepage_en(request: Request): + return _render_page(request, "home", lang="en") + +# 共享渲染函数 +def _render_page(request, page_type, lang="zh"): + t = get_translations(lang) + context = {**_common_context(request), "t": t, "lang": lang} + return templates.TemplateResponse(f"pages/{page_type}.html", context) +``` + +### Language Detection Flow + +``` +Request arrives + ↓ +URL starts with /en/ ? → lang = "en" + ↓ no +Cookie "lang" exists? → lang = cookie value + ↓ no +Accept-Language contains "en"? → lang = "en" + ↓ no +lang = "zh" (default) +``` + +Cookie 设置:name=`lang`, path=`/`, expiry=365 days, SameSite=Lax + +### Language Toggle UI + +Header nav 右侧(FAQ 后面),简洁文本链接: +- 中文页面显示 "EN",点击跳转 `/en/` 对应页面并设置 cookie +- 英文页面显示 "中文",点击跳转无前缀页面并设置 cookie + +## Technical Considerations + +### AMap 地址输入限制 + +高德地图 Autocomplete API 返回中文地址建议。英文用户体验方案: +- Placeholder 文本改为英文(如 "Enter a location, e.g., Peking University") +- 添加提示信息:"Address suggestions are in Chinese (powered by Amap)" +- 后续迭代:英文用户切换到 Google Maps Autocomplete(需要 Google Maps API Key,独立 feature) + +### Finder HTML (standalone) i18n + +`public/meetspot_finder.html` 是独立 HTML,不经过 Jinja2。方案: + +1. 所有可翻译文本元素加 `data-i18n="key"` 属性 +2. 页面加载时 JS 检测语言(cookie > Accept-Language > 默认 zh) +3. Fetch `/locales/{lang}.json`,遍历 DOM 替换文本 +4. `data-type` 属性值保持中文(API contract 不变),显示文本从翻译文件读取 +5. 翻译文件缓存到 `localStorage` + +```javascript +// 伪代码 +async function initI18n() { + const lang = getCookie('lang') || detectBrowserLang() || 'zh'; + const translations = await fetch(`/locales/${lang}.json`).then(r => r.json()); + document.querySelectorAll('[data-i18n]').forEach(el => { + const key = el.dataset.i18n; + if (translations[key]) el.textContent = translations[key]; + }); + document.documentElement.lang = lang === 'en' ? 'en' : 'zh-CN'; +} +``` + +### AI Chat 双语 + +- 维护两个 system prompt:`MEETSPOT_SYSTEM_PROMPT_ZH` + `MEETSPOT_SYSTEM_PROMPT_EN` +- `/api/ai_chat` 端点接受 `language` 参数 +- Chat widget JS 根据当前页面语言传递 language 参数 +- Preset 问题和欢迎消息从翻译文件读取 + +### SEO 双语 + +- 英文页面:`/en/about`, `/en/faq`, `/en/how-it-works`, `/en/meetspot/{city_slug}` +- `` 动态设置 +- hreflang 标签双向链接:`` + `` +- Schema.org `inLanguage` 动态设置为 `"en"` 或 `"zh-CN"` +- Sitemap 包含所有英文 URL 变体 + `xhtml:link` hreflang +- Meta tags(title, description, keywords)从翻译文件读取 +- City slug 已经是拼音(`beijing`, `shanghai`),中英文共用 + +### 内存影响评估 + +| 项目 | 内存占用 | +|------|---------| +| 两个 JSON 翻译文件缓存 | ~100KB | +| 额外路由注册 | ~negligible | +| 无新依赖 | 0 | +| 总计 | ~100KB(在 512MB 限制内可忽略) | + +## Acceptance Criteria + +### Phase 1: 营销页面 + Finder UI + API + SEO + Chat + +- [x] `locales/zh.json` 包含所有提取的中文字符串(~540 keys) +- [x] `locales/en.json` 包含所有对应英文翻译 +- [x] `app/i18n.py` 实现翻译加载、缓存、`get_translations(lang)` 函数 +- [x] `templates/base.html` 支持动态 `lang` 属性和翻译上下文 +- [x] 所有 Jinja2 模板页面(home, about, faq, how-it-works, city)支持中英文 +- [x] Header 有语言切换按钮(EN / 中文) +- [x] `public/meetspot_finder.html` 客户端 i18n 正常工作 + - [x] 所有 UI 文本可切换 + - [x] `data-type` 值保持中文发送给 API + - [x] AMap 限制有英文提示 + - [x] 付费弹窗/额度提示支持英文 +- [x] API 响应支持英文错误消息(通过 `lang` 参数或 `Accept-Language`) +- [x] AI Chat 支持英文(system prompt, preset questions, welcome message) +- [x] SEO: `/en/` 页面有正确的 meta tags, hreflang, Schema.org inLanguage +- [x] Sitemap 包含英文 URL 变体 +- [x] Cookie 语言偏好持久化正常 +- [x] 自动语言检测(Accept-Language)正常工作 + +### Phase 2: 推荐结果 HTML(后续迭代) + +- [ ] `_generate_html_content()` 接受 `language` 参数 +- [ ] `MeetSpotRequest` 模型增加 `language` 字段 +- [ ] `PLACE_TYPE_CONFIG` 显示字符串支持英文 +- [ ] 结果页 HTML 所有标签、描述、提示支持英文 +- [ ] 搜索过程可视化(thinking steps)支持英文 + +## Implementation Phases + +### Phase 1A: 基础设施(~2h) + +| Task | Files | Notes | +|------|-------|-------| +| 创建 `app/i18n.py` | `app/i18n.py` (new) | 翻译加载、缓存、`get_translations()` | +| 提取中文字符串到 `locales/zh.json` | `locales/zh.json` (new) | 从 13 个文件提取 ~540 keys | +| 创建英文翻译 `locales/en.json` | `locales/en.json` (new) | 翻译所有 keys | +| 添加 `/locales/` 静态文件路由 | `api/index.py` | finder HTML 的 JS 需要 fetch | + +### Phase 1B: Jinja2 模板改造(~3h) + +| Task | Files | Notes | +|------|-------|-------| +| `base.html` 动态 lang + 翻译 | `templates/base.html` | nav, footer, modals | +| 语言切换按钮 | `templates/base.html` | Header nav 右侧 | +| `home.html` 翻译 | `templates/pages/home.html` | Hero, features, stats, CTA | +| `about.html` 翻译 | `templates/pages/about.html` | All content | +| `faq.html` 翻译 | `templates/pages/faq.html` | FAQ items from Python | +| `how_it_works.html` 翻译 | `templates/pages/how_it_works.html` | Steps, tips, CTA | +| `city.html` 翻译 | `templates/pages/city.html` | City-specific content | +| `ai_chat.html` 翻译 | `templates/components/ai_chat.html` | Widget text, presets | + +### Phase 1C: 路由 + SEO(~2h) + +| Task | Files | Notes | +|------|-------|-------| +| 英文路由注册 `/en/*` | `api/routers/seo_pages.py` | 共享渲染函数 | +| hreflang 标签 | `templates/base.html` | 双向链接 | +| Meta tags 双语 | `api/services/seo_content.py` | 从翻译文件读取 | +| Schema.org inLanguage | `api/services/seo_content.py` | 动态切换 | +| Sitemap 英文 URL | `api/routers/seo_pages.py` | 包含 xhtml:link | +| City content 英文 | `api/services/seo_content.py` | `generate_city_content()` | + +### Phase 1D: Finder HTML + API(~3h) + +| Task | Files | Notes | +|------|-------|-------| +| Finder HTML i18n JS | `public/meetspot_finder.html` | `data-i18n` + fetch JSON | +| 语言切换按钮(finder) | `public/meetspot_finder.html` | 独立于 base.html | +| AMap 限制提示 | `public/meetspot_finder.html` | 英文提示文本 | +| 付费弹窗翻译 | `public/meetspot_finder.html` | Credits, quota 相关 | +| API 错误消息本地化 | `api/index.py` | 语言检测 + 翻译 | +| AI Chat 英文 prompt | `api/index.py` | 双语 system prompt | +| AI Chat widget 语言传参 | `templates/components/ai_chat.html` | 发送 lang 参数 | + +## Dependencies & Risks + +| Risk | Impact | Mitigation | +|------|--------|------------| +| 翻译质量 | 英文文案不自然 | 可后续迭代优化,先保证功能完整 | +| AMap 限制 | 英文用户地址输入体验差 | 明确标注限制,后续加 Google Maps | +| 540+ 字符串提取遗漏 | 部分页面混合中英文 | grep 全量扫描验证 | +| SEO 权重分散 | 英文页面分走流量 | hreflang 正确配置,canonical 明确 | +| Cookie vs URL 冲突 | 语言不一致 | URL 前缀优先,cookie 只用于默认值 | + +## Success Metrics + +- 英文页面 Lighthouse 性能分数 >= 中文页面 +- 英文 SEO 页面被 Google 索引(通过 Search Console 验证) +- 英文 Finder 页面完整可用(除 AMap 地址为中文外) +- 无新增内存占用超过 1MB + +## References & Research + +### Internal References + +- 硬编码中文字符串分布:13 files, ~540+ strings(详见研究报告) +- SEO 路由架构:`api/routers/seo_pages.py` +- 翻译文本主要来源:`templates/base.html`, `templates/pages/home.html`, `public/meetspot_finder.html` +- 模板渲染上下文:`_common_context()` in `api/routers/seo_pages.py` +- 已有 SSR 动态注入模式:env var 通过 context dict 而非 `templates.env.globals`(见 CLAUDE.md Debugging) + +### Known Limitations + +- AMap Autocomplete 只返回中文地址建议(后续迭代 Google Maps) +- Phase 1 不含推荐结果 HTML 翻译(Phase 2) +- 城市页面 slug 共用拼音,不提供英文城市名 slug diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 0000000..9d2c08a --- /dev/null +++ b/locales/en.json @@ -0,0 +1,388 @@ +{ + "nav.home": "Home", + "nav.about": "About", + "nav.how_it_works": "Guide", + "nav.faq": "FAQ", + "nav.star_github": "Star on GitHub", + "nav.skip_to_main": "Skip to main content", + "nav.brand_tagline": "Fair Meetups", + + "footer.brand": "MeetSpot", + "footer.brand_desc": "AI-powered meeting point recommendation system. Uses spherical geometry to calculate the fairest midpoint, making every meetup efficient and convenient.", + "footer.author_label": "Author: ", + "footer.project_stat": "Open Source · Actively Maintained", + "footer.community": "Join Community", + "footer.wechat": "WeChat", + "footer.github_repo": "GitHub Repo", + "footer.author_blog": "Author's Blog", + "footer.quick_links": "Quick Links", + "footer.guide": "Guide", + "footer.faq": "FAQ", + "footer.about_project": "About", + "footer.sitemap": "Sitemap", + "footer.support": "Support Us", + "footer.support_desc": "If MeetSpot has been helpful to you", + "footer.buy_coffee": "Buy me a coffee", + "footer.support_note": "MeetSpot is open source. Your support means the world to us", + "footer.copyright": "© 2025-2026 MeetSpot · Built with ❤️ by", + + "modal.wechat_title": "Add on WeChat", + "modal.wechat_desc": "Scan to add as a friend and share your experience", + "modal.wechat_qr_alt": "WeChat QR Code", + "modal.wechat_qr_title": "Scan to add on WeChat", + "modal.close": "Close", + "modal.support_title": "Thank You for Your Support", + "modal.support_desc": "MeetSpot is a free, open-source project. Your support helps us keep improving", + "modal.support_qr_alt": "WeChat Pay QR Code", + "modal.support_qr_title": "Support MeetSpot via WeChat Pay", + "modal.support_message": "Every bit of support keeps us going", + "modal.other_support": "Other ways to support:", + "modal.star_github": "Star us on GitHub", + "modal.share_friends": "Share with friends", + "modal.submit_issue": "Submit an Issue to help improve", + "modal.contribute_code": "Contribute code and ideas", + + "home.agent_badge": "AI Agent", + "home.hero_title_line1": "MeetSpot AI Agent", + "home.hero_title_line2": "Smart Meeting Point Finder", + "home.hero_tagline": "Spherical geometry for fair midpoints + GPT-4o smart scoring", + "home.hero_tech_steps": "5-Step Reasoning", + "home.hero_cta": "Try It Free", + "home.stat_cities": "Cities Covered", + "home.stat_themes": "Venue Themes", + "home.stat_universities": "University Aliases", + "home.stat_people": "People Supported", + "home.community_title": "Join the MeetSpot Community", + "home.community_desc": "Connect with other users, share tips, and stay updated on new features", + "home.community_wechat": "Add on WeChat", + "home.thinking_badge": "AI Reasoning Flow", + "home.thinking_title": "5-Step Transparent Reasoning", + "home.thinking_subtitle": "MeetSpot isn't a simple search tool — it's an AI Agent with a complete reasoning chain", + "home.step1_title": "Parse Addresses", + "home.step1_desc": "Recognize addresses, landmarks & aliases", + "home.step2_title": "Calculate Midpoint", + "home.step2_desc": "Spherical geometry for fair center", + "home.step3_title": "Search Nearby", + "home.step3_desc": "POI venue discovery", + "home.step4_title": "GPT-4o Scoring", + "home.step4_desc": "Multi-dimensional smart scoring", + "home.step5_title": "Generate Results", + "home.step5_desc": "Output optimal recommendations", + "home.perf_single": "Single Theme", + "home.perf_single_time": "5-8s", + "home.perf_dual": "Dual Theme", + "home.perf_dual_time": "8-12s", + "home.perf_agent": "Agent Mode", + "home.perf_agent_time": "15-30s", + "home.content_badge": "AI Agent · Completely Free", + "home.content_heading": "Find the fairest midpoint for group meetups with one click", + "home.content_desc": "MeetSpot is an AI Agent for smart meeting point recommendations. Whether it's a business meeting at a cafe, dinner with friends, or a study session at a library, it calculates the optimal midpoint from everyone's locations and recommends top-rated venues nearby.", + "home.btn_use_now": "Try It Free", + "home.btn_guide": "Guide", + "home.btn_faq": "FAQ", + "home.cities_title": "Popular Cities", + "home.cities_desc": "Select a city for local recommendations", + "home.cities_toggle_show": "Show All ▼", + "home.cities_toggle_hide": "Collapse ▲", + "home.features_title": "Key Features", + "home.feat_geo_title": "Spherical Geometry Midpoint", + "home.feat_geo_desc": "Uses the Haversine formula to calculate the true midpoint on Earth's curved surface — 15-20% more accurate than simple averaging, mathematically fair.", + "home.feat_gpt_title": "GPT-4o Smart Scoring", + "home.feat_gpt_desc": "The AI Agent uses GPT-4o for multi-dimensional venue scoring (distance, rating, parking, ambiance), providing smart recommendations instead of simple sorting.", + "home.feat_themes_title": "12 Venue Themes", + "home.feat_themes_desc": "Cafes, restaurants, libraries, KTV, gyms, and more — 12 themes with support for multi-theme simultaneous search.", + "home.feat_transparent_title": "5-Step Transparent Reasoning", + "home.feat_transparent_desc": "The complete reasoning chain is visualized — see how the AI 'thinks' at every step, fully transparent and explainable.", + "home.feat_uni_title": "60+ University Aliases", + "home.feat_uni_desc": "Built-in alias mapping for 60+ Chinese universities. For example, typing 'PKU' is recognized as 'Peking University, Haidian, Beijing'.", + "home.feat_cities_title": "350+ Cities Covered", + "home.feat_cities_desc": "Powered by Amap data, covering Beijing, Shanghai, Guangzhou, Shenzhen, Hangzhou, and 350+ more cities across China.", + "home.cases_title": "Real User Stories", + "home.case1_title": "Cross-City Team Meetup", + "home.case1_desc": "A remote team spread across Beijing, Shanghai, and Shenzhen used MeetSpot to find a meeting point near Hangzhou's West Tech Corridor. Average commute dropped from 94 to 58 minutes — nearly 40% savings.", + "home.case2_title": "Community Event Planning", + "home.case2_desc": "A product community organized a 50-person workshop. After selecting 'coworking space + projector,' the recommendation list automatically showed budget ranges, transit options, and accessibility info.", + "home.case3_title": "Business Client Meeting", + "home.case3_desc": "When meeting partners from multiple cities, MeetSpot generates a professional venue recommendation report with clear data-backed reasoning for 'fair and convenient' venue selection.", + "home.cta_title": "Start Planning Your Next Meetup", + "home.cta_desc": "MeetSpot is completely free — no signup required. Have questions or suggestions? Let us know via GitHub Issues.", + "home.cta_btn_primary": "Create a Meetup Plan", + "home.cta_btn_secondary": "Learn More", + + "about.hero_title": "About MeetSpot", + "about.hero_desc": "MeetSpot is an AI Agent for multi-person meeting point recommendations. It uses spherical geometry to calculate the fair midpoint and GPT-4o for smart scoring, with 5-step transparent reasoning to find meeting spots that are fair for everyone.", + "about.story_title": "Our Story", + "about.story_p1": "MeetSpot started with a simple question: when friends are scattered across a city, how do you find a meeting spot that's fair for everyone?", + "about.story_p2": "The traditional approach is either making one person travel the farthest, or picking a random 'middle' spot. We believed there had to be a better solution — an algorithm-based, mathematically fair midpoint calculator.", + "about.story_p3": "That's how MeetSpot was born. From an internal tool to an online service covering 350+ cities, we've always held to one principle: keep everyone's travel time within a reasonable range.", + "about.values_title": "Core Values", + "about.val_fair_title": "Mathematical Fairness", + "about.val_fair_desc": "Uses spherical geometry (Haversine formula) to calculate the true midpoint on Earth's curved surface — 15-20% more accurate than simple averaging, ensuring balanced distances for everyone.", + "about.val_agent_title": "AI Agent", + "about.val_agent_desc": "Not a simple search tool, but a full AI Agent. 5-step transparent reasoning: parse address → calculate midpoint → search nearby → GPT-4o scoring → generate recommendation.", + "about.val_themes_title": "Multi-Theme Support", + "about.val_themes_desc": "12 venue themes (cafes, restaurants, libraries, KTV, etc.) with support for concurrent multi-theme search — get multiple types of recommendations at once.", + "about.val_scoring_title": "GPT-4o Smart Scoring", + "about.val_scoring_desc": "AI comprehensively evaluates rating, distance, theme match, and user requirements (parking, quiet, private rooms), providing smart ranking with recommendation reasons.", + "about.tech_title": "Tech Stack", + "about.tech_desc": "MeetSpot is built with a modern AI Agent tech stack:", + "about.tech_perf": "Performance: Single theme 5-8s, dual theme 8-12s, Agent mode 15-30s. The project is fully open source — check out the code on GitHub.", + "about.contact_title": "Contact Us", + "about.contact_desc": "Have questions, suggestions, or partnership ideas? Reach out through:", + "about.contact_email": "Email Us", + "about.contact_issues": "Report an Issue", + + "faq.hero_title": "FAQ", + "faq.hero_desc": "Frequently asked questions about MeetSpot", + "faq.contact_title": "Still have questions?", + "faq.contact_desc": "If you didn't find your answer above, feel free to reach out", + "faq.contact_email": "Email Us", + + "faq.q1": "What is MeetSpot?", + "faq.a1": "MeetSpot is a smart meeting point recommendation system that helps groups find the fairest place to meet. Whether it's a business meeting, dinner with friends, or a study session, it quickly finds the right venue.", + "faq.q2": "How many people can use it at once?", + "faq.a2": "Supports 2-10 participant locations. The system calculates the optimal midpoint based on everyone's positions.", + "faq.q3": "Which cities are supported?", + "faq.a3": "Currently covers 350+ cities including Beijing, Shanghai, Guangzhou, Shenzhen, Hangzhou, and more. Powered by Amap data and continuously expanding.", + "faq.q4": "What types of venues can I search?", + "faq.a4": "Supports cafes, restaurants, libraries, KTV, gyms, escape rooms, and more. You can also search multiple types simultaneously (e.g., 'cafe + restaurant').", + "faq.q5": "How does it ensure fairness?", + "faq.a5": "The system uses a geometric center algorithm to ensure every participant's distance to the meeting point is within a reasonable range — no one has to travel much farther than others.", + "faq.q6": "How are results ranked?", + "faq.a6": "Results are ranked by a comprehensive algorithm considering rating, distance, and user requirements. Venues with high ratings, close to the center, and matching special needs are prioritized.", + "faq.q7": "Can I use abbreviations?", + "faq.a7": "Yes! The system has 60+ built-in university alias mappings. For example, 'PKU' is automatically recognized as 'Peking University.' Landmark names like 'Guomao' and 'Oriental Pearl' are also supported.", + "faq.q8": "Is it free? Do I need to sign up?", + "faq.a8": "Completely free, no registration required. Just enter addresses and get recommendations instantly.", + "faq.q9": "How fast are the recommendations?", + "faq.a9": "The AI Agent goes through a complete 5-step reasoning process: parse address → calculate midpoint → search nearby → GPT-4o scoring → generate recommendation. Single theme: 5-8s, dual theme: 8-12s, complex Agent mode: 15-30s.", + "faq.q10": "How is this different from Google/Apple Maps?", + "faq.a10": "Maps search 'near me,' MeetSpot searches 'between us.' We first calculate the fair midpoint using spherical geometry, then recommend top venues there. This is a capability that standard map apps don't offer.", + "faq.q11": "What does AI Agent mean?", + "faq.a11": "MeetSpot isn't a simple search tool — it's an AI Agent with a complete 5-step reasoning chain. It uses GPT-4o for multi-dimensional scoring (distance, rating, parking, ambiance), and you can see how the AI 'thinks' at every step — fully transparent and explainable.", + "faq.q12": "How do I report issues or suggestions?", + "faq.a12": "Feel free to report issues or suggestions via GitHub Issues, or email us at Johnrobertdestiny@gmail.com.", + + "how.hero_title": "How It Works", + "how.hero_desc": "AI Agent 5-step transparent reasoning — find the fairest meeting point for everyone in seconds", + "how.step1_title": "Enter Participant Addresses", + "how.step1_desc": "Add 2-10 participant locations. You can enter detailed addresses, landmark names, or university abbreviations.", + "how.step1_tips_label": "Tips", + "how.step1_tip1": "Supports abbreviations: 'PKU' is auto-recognized as 'Peking University'", + "how.step1_tip2": "Supports landmarks: enter 'Guomao', 'Oriental Pearl', etc.", + "how.step1_tip3": "Supports mixed Chinese and English input", + "how.step2_title": "Choose Venue Type", + "how.step2_desc": "Select 1-3 venue types. The system will search all selected types simultaneously for more options.", + "how.step2_tips_label": "Supported Venue Types", + "how.step2_tip1": "Cafe — great for business meetings, casual hangouts", + "how.step2_tip2": "Restaurant — perfect for dinners, celebrations", + "how.step2_tip3": "Library — ideal for studying, quiet discussions", + "how.step2_tip4": "More: KTV, gym, escape room, and more", + "how.step3_title": "Set Special Requirements (Optional)", + "how.step3_desc": "Add special requirements based on your needs. The system will prioritize venues that match.", + "how.step3_tips_label": "Common Requirements", + "how.step3_tip1": "Easy parking — ideal for drivers", + "how.step3_tip2": "Quiet environment — ideal for business meetings", + "how.step3_tip3": "Private rooms — ideal for private gatherings", + "how.step3_tip4": "Near subway — ideal for public transit", + "how.step4_title": "AI Agent 5-Step Reasoning", + "how.step4_desc": "After clicking the recommend button, the AI Agent runs a complete 5-step reasoning process — you can watch every step:", + "how.step4_tips_label": "Agent Reasoning Process", + "how.step4_tip1": "Step 1 Parse addresses — recognize aliases, convert to coordinates", + "how.step4_tip2": "Step 2 Calculate midpoint — spherical geometry for fair center", + "how.step4_tip3": "Step 3 Search nearby — concurrent multi-theme POI search", + "how.step4_tip4": "Step 4 GPT-4o scoring — AI multi-dimensional evaluation", + "how.step4_tip5": "Step 5 Generate results — rank and output optimal recommendations", + "how.step5_title": "View Results & Navigate", + "how.step5_desc": "Results include map visualization, venue cards, and AI reasoning display. Single theme: 5-8s, dual theme: 8-12s, Agent mode: 15-30s.", + "how.step5_tips_label": "Results Include", + "how.step5_tip1": "Fair center point (green) and recommended venues (orange) on the map", + "how.step5_tip2": "Venue name, rating, and smart score (out of 100)", + "how.step5_tip3": "One-click navigation button to open in Amap", + "how.step5_tip4": "Transparent AI reasoning display with explainable recommendations", + "how.advanced_title": "AI Agent Capabilities", + "how.adv_geo_title": "Spherical Geometry Algorithm", + "how.adv_geo_desc": "Uses the Haversine formula to calculate the true midpoint on Earth's curved surface — 15-20% more accurate than simple lat/lng averaging.", + "how.adv_gpt_title": "GPT-4o Smart Scoring", + "how.adv_gpt_desc": "AI comprehensively evaluates rating, distance, theme match, and user requirements, providing multi-dimensional smart ranking.", + "how.adv_uni_title": "60+ University Aliases", + "how.adv_uni_desc": "Typing 'PKU' is automatically recognized as 'Peking University' — avoiding ambiguity and smartly matching the correct location.", + "how.adv_themes_title": "12 Venue Themes", + "how.adv_themes_desc": "Cafes, restaurants, libraries, KTV, gyms, escape rooms, and more — with support for concurrent multi-theme search.", + "how.cta_title": "Ready to Get Started?", + "how.cta_desc": "Experience AI Agent 5-step transparent reasoning — find the fairest meeting point for everyone in seconds", + "how.cta_btn": "Start Now", + + "chat.status": "Online · Powered by GPT-4o", + "chat.welcome_title": "MeetSpot AI Assistant", + "chat.welcome_subtitle": "Spherical Geometry + GPT-4o Smart Scoring · 5-Step Transparent Reasoning", + "chat.welcome_msg": "Hi! I'm the MeetSpot AI Assistant. I can help you with:

5-Step AI Reasoning: Address parsing → Midpoint calculation → Nearby search → GPT-4o scoring → Smart recommendation
Core Advantage: Spherical geometry algorithm — mathematically fair for everyone
Capabilities: 350+ cities · 12 themes · 60+ university aliases

How can I help you?", + "chat.presets_label": "Quick Questions", + "chat.preset_principle": "How It Works", + "chat.preset_principle_q": "How does the AI Agent work?", + "chat.preset_speed": "Speed", + "chat.preset_speed_q": "How fast are recommendations?", + "chat.preset_vs_amap": "vs Map Apps", + "chat.preset_vs_amap_q": "How is MeetSpot different from map apps?", + "chat.preset_pricing": "Pricing", + "chat.preset_pricing_q": "Is it free to use?", + "chat.input_placeholder": "Type your question...", + "chat.error_generic": "Sorry, I ran into an issue. Please try again later.", + "chat.error_network": "Network connection issue. Please check your connection and try again.", + + "finder.title": "MeetSpot - Smart Meeting Point Finder", + "finder.nav_home": "Home", + "finder.nav_guide": "Guide", + "finder.nav_faq": "FAQ", + "finder.nav_about": "About", + "finder.header_title": "MeetSpot", + "finder.header_desc": "Smart Meeting Point Finder — Find the perfect spot for every meetup", + "finder.step1": "Enter Locations", + "finder.step2": "Choose Theme", + "finder.step3": "Get Results", + "finder.section_locations": "Participant Locations", + "finder.section_locations_hint": "Enter 2-10 locations. The system will calculate the optimal midpoint", + "finder.location_placeholder_1": "Enter first location, e.g., Chaoyang District, Beijing", + "finder.location_placeholder_2": "Enter second location, e.g., Haidian District, Zhongguancun", + "finder.location_placeholder_n": "Enter location", + "finder.location_count_hint": "{count} locations entered. You can add more (up to 10)", + "finder.add_location": "Add More Locations", + "finder.section_themes": "Choose Venue Type", + "finder.section_themes_hint": "Select 1-3 themes for more precise recommendations", + "finder.cat_food": "Food & Drinks", + "finder.cat_leisure": "Leisure & Fun", + "finder.cat_culture": "Culture & Study", + "finder.cat_shopping": "Shopping", + "finder.type_cafe": "Cafe", + "finder.type_restaurant": "Restaurant", + "finder.type_teahouse": "Tea House", + "finder.type_bar": "Bar", + "finder.type_cinema": "Cinema", + "finder.type_arcade": "Arcade", + "finder.type_gym": "Gym", + "finder.type_library": "Library", + "finder.type_bookstore": "Bookstore", + "finder.type_museum": "Museum", + "finder.type_attraction": "Attraction", + "finder.type_mall": "Mall", + "finder.type_shopping_center": "Shopping Center", + "finder.type_park": "Park", + "finder.themes_hint": "Select your preferred venue types (up to 3)", + "finder.custom_keywords_placeholder": "Or enter custom keywords: swimming pool, yoga studio, escape room...", + "finder.section_filters": "Filters", + "finder.section_filters_hint": "Optional — helps find better matches", + "finder.min_rating": "Minimum Rating", + "finder.rating_any": "Any", + "finder.rating_3": "3.0+", + "finder.rating_35": "3.5+", + "finder.rating_4": "4.0+", + "finder.rating_45": "4.5+", + "finder.max_distance": "Max Distance", + "finder.distance_any": "Any", + "finder.distance_5km": "Within 5 km", + "finder.distance_10km": "Within 10 km", + "finder.distance_20km": "Within 20 km", + "finder.distance_30km": "Within 30 km", + "finder.distance_50km": "Within 50 km", + "finder.price_range": "Price Range", + "finder.price_any": "Any", + "finder.price_economy": "Budget", + "finder.price_mid": "Mid-Range", + "finder.price_high": "High-End", + "finder.section_requirements": "Special Requirements", + "finder.section_requirements_hint": "Optional — describe any special needs", + "finder.requirements_placeholder": "Describe your special requirements, e.g.:\n• Easy parking\n• Quiet environment\n• Good for business meetings", + "finder.common_requirements": "Common requirements (click to add)", + "finder.req_parking": "Easy Parking", + "finder.req_quiet": "Quiet", + "finder.req_wifi": "Wi-Fi", + "finder.req_business": "Business-Friendly", + "finder.req_children": "Kid-Friendly", + "finder.req_private_room": "Private Room", + "finder.req_long_stay": "Long Stay OK", + "finder.req_24h": "Open 24h", + "finder.submit_btn": "Find Best Meeting Point", + "finder.loading_title": "Analyzing", + "finder.loading_text": "Finding the best meeting point for you...", + "finder.loading_step1": "Parsing Addresses", + "finder.loading_step1_desc": "Retrieving coordinate data for each location", + "finder.loading_step2": "Calculating Midpoint", + "finder.loading_step2_desc": "Finding the fairest meeting position", + "finder.loading_step3": "Searching Nearby", + "finder.loading_step3_desc": "Discovering quality venues nearby", + "finder.loading_step4": "Smart Scoring", + "finder.loading_step4_desc": "Evaluating the best options", + "finder.loading_step5": "Generating Results", + "finder.loading_step5_desc": "Creating your personalized recommendation", + "finder.amap_note": "Address suggestions are in Chinese (powered by Amap)", + + "finder.payment_free_label": "Free Today", + "finder.payment_buy": "Buy", + "finder.payment_modal_title": "Daily Free Use Exhausted", + "finder.payment_modal_desc": "Purchase credits to continue, or try again tomorrow", + "finder.payment_buy_btn": "Buy Credits", + "finder.payment_later": "Maybe Later", + "finder.payment_balance_title": "Insufficient Balance", + "finder.payment_balance_desc": "Purchase more credits to continue getting recommendations", + "finder.payment_balance_prefix": "Current balance: ", + "finder.payment_balance_suffix": "credits", + + "finder.warn_max_types": "Limit Reached", + "finder.warn_max_types_msg": "You can select up to {max} venue types", + "finder.warn_max_locations": "Maximum 10 locations supported", + "finder.warn_min_locations": "At least two locations are required", + "finder.error_too_few": "Please enter at least two locations", + "finder.error_too_many": "Maximum 10 locations supported", + "finder.success_agent": "Agent Deep Analysis Complete", + "finder.success_agent_desc": "Personalized recommendations generated", + "finder.success_rule": "Quick Recommendation Complete", + "finder.success_rule_desc": "Best meeting points found for you", + "finder.info_quota_used": "Daily free use exhausted. Purchase credits or try again tomorrow", + "finder.warn_no_result": "No Results Found", + "finder.warn_no_result_desc": "Try different keywords or more specific addresses", + "finder.error_request": "Request Failed", + "finder.btn_buy_credits": "Buy Credits to Continue", + + "api.error.quota_exceeded": "Daily free quota exhausted. Please purchase credits to continue", + "api.error.rate_limit": "Too many requests. Please try again later", + "api.error.amap_not_configured": "Amap API key not configured. Please set the AMAP_API_KEY environment variable or configure config.toml", + "api.error.config_error": "Service configuration error: unable to load recommendation module. Please ensure correct environment setup", + "api.chat.fallback": "Sorry, the AI assistant is temporarily unavailable. You can use the meeting point finder directly, or check the guide on the page. Please try again later.", + "api.chat.configuring": "AI assistant is being configured. Please try again shortly. You can also try the meeting point finder directly!", + "api.chat.error": "Sorry, the AI assistant encountered an issue. You can use the meeting point finder directly, or try again later.", + + "seo.home.title": "MeetSpot - AI Meeting Point Finder for Groups", + "seo.home.description": "MeetSpot helps 2-10 people find the fairest meeting point. AI-powered recommendations for cafes, restaurants, libraries and more. Spherical geometry for fair midpoint calculation. 350+ cities, free to use.", + "seo.about.title": "About MeetSpot - Smart Meeting Point Finder", + "seo.faq.title": "FAQ - MeetSpot", + "seo.how.title": "How It Works - MeetSpot", + "seo.breadcrumb.home": "Home", + "seo.breadcrumb.about": "About", + "seo.breadcrumb.guide": "Guide", + "seo.breadcrumb.faq": "FAQ", + + "city.venues_header": "Popular Venue Types in {city}", + "city.features_title": "Why Use MeetSpot in {city}?", + "city.metro_title": "{count} Metro Lines", + "city.metro_desc": "{city} has an extensive metro network. MeetSpot prioritizes venues near metro stations", + "city.midpoint_title": "Smart Midpoint Calculation", + "city.midpoint_desc": "Spherical geometry algorithm ensures fair and balanced commute distances for every participant", + "city.local_title": "Curated Local Venues", + "city.local_desc": "Covering {venues} and more popular venue types in {city}. Top-rated venues are prioritized", + "city.landmarks_title": "Popular Meeting Areas in {city}", + "city.landmarks_group": "Landmarks & Districts", + "city.districts_group": "Business Centers", + "city.universities_group": "University Areas", + "city.use_cases_title": "Real Use Cases in {city}", + "city.tips_title": "Meetup Tips for {city}", + "city.how_title": "How to Find the Best Meeting Point in {city}?", + "city.how_step1_title": "Enter Participant Locations", + "city.how_step1_desc": "Supports any address, landmark, or university name in {city}", + "city.how_step2_title": "Choose Venue Type", + "city.how_step2_desc": "Select {venues} and other themes based on your meetup purpose", + "city.how_step3_title": "Get Smart Recommendations", + "city.how_step3_desc": "The system automatically calculates the geographic midpoint and recommends top-rated venues", + "city.cta_title": "Start Planning a Meetup in {city}", + "city.cta_desc": "No signup required — just enter addresses to get recommendations", + "city.cta_btn": "Use MeetSpot Now" +} diff --git a/locales/zh.json b/locales/zh.json new file mode 100644 index 0000000..139ceef --- /dev/null +++ b/locales/zh.json @@ -0,0 +1,388 @@ +{ + "nav.home": "首页", + "nav.about": "关于", + "nav.how_it_works": "使用指南", + "nav.faq": "FAQ", + "nav.star_github": "Star on GitHub", + "nav.skip_to_main": "跳转到主内容", + "nav.brand_tagline": "Fair Meetups", + + "footer.brand": "MeetSpot 聚点", + "footer.brand_desc": "智能聚会地点推荐系统,基于地理算法计算公平中点,让每次聚会都高效便捷。", + "footer.author_label": "作者:", + "footer.project_stat": "开源项目 · 持续更新中", + "footer.community": "加入社区", + "footer.wechat": "个人微信", + "footer.github_repo": "GitHub仓库", + "footer.author_blog": "作者博客", + "footer.quick_links": "快速链接", + "footer.guide": "使用指南", + "footer.faq": "常见问题", + "footer.about_project": "关于项目", + "footer.sitemap": "网站地图", + "footer.support": "支持项目", + "footer.support_desc": "如果MeetSpot对你有帮助", + "footer.buy_coffee": "请我喝杯咖啡", + "footer.support_note": "MeetSpot是开源项目,您的支持是最大动力", + "footer.copyright": "© 2025-2026 MeetSpot · Built with ❤️ by", + + "modal.wechat_title": "添加个人微信", + "modal.wechat_desc": "扫码添加好友,交流使用心得", + "modal.wechat_qr_alt": "个人微信二维码", + "modal.wechat_qr_title": "扫描二维码添加微信好友", + "modal.close": "关闭", + "modal.support_title": "感谢你的支持", + "modal.support_desc": "MeetSpot是完全免费的开源项目,你的赞赏将帮助项目持续改进", + "modal.support_qr_alt": "微信支付码", + "modal.support_qr_title": "使用微信赞赏MeetSpot", + "modal.support_message": "你的每一份支持都是最大的动力", + "modal.other_support": "也可以通过以下方式支持:", + "modal.star_github": "在GitHub上给项目点Star", + "modal.share_friends": "分享给需要的朋友", + "modal.submit_issue": "提交Issue帮助改进", + "modal.contribute_code": "贡献代码和想法", + + "home.agent_badge": "AI Agent", + "home.hero_title_line1": "MeetSpot AI Agent", + "home.hero_title_line2": "智能聚会地点推荐", + "home.hero_tagline": "球面几何计算公平中点 + GPT-4o智能评分", + "home.hero_tech_steps": "5步推理", + "home.hero_cta": "立即免费使用", + "home.stat_cities": "覆盖城市", + "home.stat_themes": "场景主题", + "home.stat_universities": "高校简称", + "home.stat_people": "支持人数", + "home.community_title": "加入MeetSpot用户社区", + "home.community_desc": "与用户交流使用心得,获取最新功能更新", + "home.community_wechat": "添加微信", + "home.thinking_badge": "AI 推理流程", + "home.thinking_title": "5步透明推理", + "home.thinking_subtitle": "MeetSpot不是简单的搜索工具,而是有完整推理链条的AI Agent", + "home.step1_title": "解析地址", + "home.step1_desc": "智能识别地址/地标/简称", + "home.step2_title": "计算中点", + "home.step2_desc": "球面几何公平中心", + "home.step3_title": "搜索周边", + "home.step3_desc": "POI场所检索", + "home.step4_title": "GPT-4o评分", + "home.step4_desc": "多维度智能评分", + "home.step5_title": "生成推荐", + "home.step5_desc": "输出最优结果", + "home.perf_single": "单场景", + "home.perf_single_time": "5-8秒", + "home.perf_dual": "双场景", + "home.perf_dual_time": "8-12秒", + "home.perf_agent": "Agent模式", + "home.perf_agent_time": "15-30秒", + "home.content_badge": "AI Agent · 完全免费", + "home.content_heading": "多人聚会,一键找到最公平的中点位置", + "home.content_desc": "MeetSpot(聚点)是一个智能会面地点推荐AI Agent。无论是咖啡馆商务会谈、餐厅朋友聚餐、还是图书馆学习讨论,都能根据多人位置快速计算最佳中点,推荐附近高评分场所。", + "home.btn_use_now": "立即免费使用", + "home.btn_guide": "使用指南", + "home.btn_faq": "常见问题", + "home.cities_title": "热门城市", + "home.cities_desc": "选择城市获取本地化推荐", + "home.cities_toggle_show": "查看全部 ▼", + "home.cities_toggle_hide": "收起 ▲", + "home.features_title": "核心功能", + "home.feat_geo_title": "球面几何中心点", + "home.feat_geo_desc": "使用Haversine公式计算地球曲面上的真实中点,比简单平均精确15-20%,数学上公平。", + "home.feat_gpt_title": "GPT-4o智能评分", + "home.feat_gpt_desc": "AI Agent使用GPT-4o对场所进行多维度评分(距离、评分、停车、环境),智能推荐而非简单排序。", + "home.feat_themes_title": "12种场景主题", + "home.feat_themes_desc": "咖啡馆、餐厅、图书馆、KTV、健身房等12种场景,支持多场景同时搜索。", + "home.feat_transparent_title": "5步透明推理", + "home.feat_transparent_desc": "完整推理链条可视化,你可以看到AI每一步是怎么「思考」的,完全透明可解释。", + "home.feat_uni_title": "60+高校简称", + "home.feat_uni_desc": "内置60+高校简称映射,「北大」自动识别为「北京市海淀区北京大学」。", + "home.feat_cities_title": "350+城市覆盖", + "home.feat_cities_desc": "基于高德地图数据,覆盖北京、上海、广州、深圳、杭州等350+城市。", + "home.cases_title": "真实用户案例", + "home.case1_title": "跨城团队聚会", + "home.case1_desc": "远程团队成员分散在北京、上海、深圳三地,MeetSpot建议在杭州城西科创大走廊附近会面,团队平均通勤时间从94分钟缩短到58分钟,节省近40%时间成本。", + "home.case2_title": "社区活动组织", + "home.case2_desc": "产品社区组织50人线下Workshop,选择「共享空间+投影设备」场景后,推荐列表自动显示预算区间、交通换乘和无障碍设施信息。", + "home.case3_title": "商务客户会议", + "home.case3_desc": "与多地合作伙伴举行会议时,MeetSpot生成专业的地点推荐报告,清晰展示选址依据,让「公平便捷」有数据支撑。", + "home.cta_title": "开始规划你的下一次聚会", + "home.cta_desc": "MeetSpot 完全免费、无需注册,直接开始使用。有问题或建议?欢迎通过 GitHub Issues 反馈。", + "home.cta_btn_primary": "立即创建聚会计划", + "home.cta_btn_secondary": "了解更多", + + "about.hero_title": "关于 MeetSpot", + "about.hero_desc": "MeetSpot(聚点)是一款多人会面地点推荐的 AI Agent。基于球面几何算法计算公平中点,结合 GPT-4o 智能评分,5步透明推理帮你找到对所有人都公平的聚会地点。", + "about.story_title": "我们的故事", + "about.story_p1": "MeetSpot 始于一个简单的问题:当多个朋友分散在城市不同角落时,如何找到对所有人都公平的聚会地点?", + "about.story_p2": "传统方式往往是让某一个人跑最远,或者随意选一个「中间」的地方。我们认为应该有更好的解决方案——一个基于算法的、公平的中点计算系统。", + "about.story_p3": "于是,MeetSpot 诞生了。从最初的内部工具,到现在覆盖 350+ 城市的在线服务,我们始终坚持一个原则:让每个人的路程都在合理范围内。", + "about.values_title": "核心价值", + "about.val_fair_title": "数学公平", + "about.val_fair_desc": "使用球面几何(Haversine公式)计算地球曲面真实中点,比简单平均精确15-20%,确保每个人距离均衡。", + "about.val_agent_title": "AI Agent", + "about.val_agent_desc": "不是简单搜索工具,而是完整的AI Agent。5步透明推理:解析地址 → 计算中点 → 搜索周边 → GPT-4o评分 → 生成推荐。", + "about.val_themes_title": "多场景支持", + "about.val_themes_desc": "12种场景主题(咖啡馆、餐厅、图书馆、KTV等),支持多场景并发搜索,一次获得多种推荐。", + "about.val_scoring_title": "GPT-4o智能评分", + "about.val_scoring_desc": "AI综合评估评分、距离、场景匹配、用户需求(停车、安静、包间),给出智能排序和推荐理由。", + "about.tech_title": "技术架构", + "about.tech_desc": "MeetSpot 采用现代化的 AI Agent 技术栈:", + "about.tech_perf": "性能指标:单场景 5-8 秒,双场景 8-12 秒,Agent模式 15-30 秒。项目完全开源,欢迎在 GitHub 上查看源代码。", + "about.contact_title": "联系我们", + "about.contact_desc": "有问题、建议或合作意向?欢迎通过以下方式联系:", + "about.contact_email": "邮箱联系", + "about.contact_issues": "问题反馈", + + "faq.hero_title": "常见问题", + "faq.hero_desc": "关于 MeetSpot 的常见问题解答", + "faq.contact_title": "还有其他问题?", + "faq.contact_desc": "如果以上内容没有解答你的疑问,欢迎通过以下方式联系我们", + "faq.contact_email": "邮箱联系", + + "faq.q1": "MeetSpot 是什么?", + "faq.a1": "MeetSpot(聚点)是一个智能会面地点推荐系统,帮助多人找到最公平的聚会地点。无论是商务会谈、朋友聚餐还是学习讨论,都能快速找到合适的场所。", + "faq.q2": "支持多少人一起查找?", + "faq.a2": "支持 2-10 个参与者位置,系统会根据所有人的位置计算最佳中点。", + "faq.q3": "支持哪些城市?", + "faq.a3": "目前覆盖北京、上海、广州、深圳、杭州等 350+ 城市,使用高德地图数据,持续扩展中。", + "faq.q4": "可以搜索哪些类型的场所?", + "faq.a4": "支持咖啡馆、餐厅、图书馆、KTV、健身房、密室逃脱等多种场所类型,还可以同时搜索多种类型(如'咖啡馆+餐厅')。", + "faq.q5": "如何保证推荐公平?", + "faq.a5": "系统使用几何中心算法,确保每位参与者到聚会地点的距离都在合理范围内,没有人需要跑特别远。", + "faq.q6": "推荐结果如何排序?", + "faq.a6": "基于评分、距离、用户需求的综合排序算法,优先推荐评分高、距离中心近、符合特殊需求的场所。", + "faq.q7": "可以输入简称吗?", + "faq.a7": "支持!系统内置 60+ 大学简称映射,如'北大'会自动识别为'北京大学'。也支持输入地标名称如'国贸'、'东方明珠'等。", + "faq.q8": "是否免费?需要注册吗?", + "faq.a8": "完全免费使用,无需注册,直接输入地址即可获得推荐结果。", + "faq.q9": "推荐速度如何?", + "faq.a9": "AI Agent 会经历完整的5步推理流程:解析地址 → 计算中心点 → 搜索周边 → GPT-4o智能评分 → 生成推荐。单场景5-8秒,双场景8-12秒,复杂Agent模式15-30秒。", + "faq.q10": "和高德地图有什么区别?", + "faq.a10": "高德搜索'我附近',MeetSpot搜索'我们中间'。我们先用球面几何算出多人公平中点,再推荐那里的好店。这是高德/百度都没有的功能。", + "faq.q11": "AI Agent是什么意思?", + "faq.a11": "MeetSpot不是简单的搜索工具,而是一个AI Agent。它有5步完整的推理链条,使用GPT-4o进行多维度评分(距离、评分、停车、环境),你可以看到AI每一步是怎么'思考'的,完全透明可解释。", + "faq.q12": "如何反馈问题或建议?", + "faq.a12": "欢迎通过 GitHub Issues 反馈问题或建议,也可以发送邮件至 Johnrobertdestiny@gmail.com。", + + "how.hero_title": "使用指南", + "how.hero_desc": "AI Agent 5步透明推理,几秒钟找到对所有人都公平的聚会地点", + "how.step1_title": "输入参与者地址", + "how.step1_desc": "添加 2-10 个参与者的位置。可以输入详细地址、地标名称或大学简称。", + "how.step1_tips_label": "小提示", + "how.step1_tip1": "支持简写:「北大」会自动识别为「北京大学」", + "how.step1_tip2": "支持地标:直接输入「国贸」、「东方明珠」等", + "how.step1_tip3": "支持中英文混输", + "how.step2_title": "选择场所类型", + "how.step2_desc": "选择 1-3 种场所类型,系统会同时搜索所有选中的类型,提供更多选择。", + "how.step2_tips_label": "支持的场所类型", + "how.step2_tip1": "咖啡馆 - 适合商务会谈、朋友小聚", + "how.step2_tip2": "餐厅 - 适合聚餐、庆祝活动", + "how.step2_tip3": "图书馆 - 适合学习、安静讨论", + "how.step2_tip4": "更多:KTV、健身房、密室逃脱等", + "how.step3_title": "设置特殊需求(可选)", + "how.step3_desc": "根据实际情况添加特殊需求,系统会优先推荐符合条件的场所。", + "how.step3_tips_label": "常用需求", + "how.step3_tip1": "停车方便 - 适合自驾出行", + "how.step3_tip2": "环境安静 - 适合商务会议", + "how.step3_tip3": "有包间 - 适合私密聚会", + "how.step3_tip4": "靠近地铁 - 适合公共交通出行", + "how.step4_title": "AI Agent 5步推理", + "how.step4_desc": "点击推荐按钮后,AI Agent 会执行完整的5步推理流程,你可以看到每一步的思考过程:", + "how.step4_tips_label": "Agent 推理过程", + "how.step4_tip1": "Step 1 解析地址 - 智能识别简称,转换为精确坐标", + "how.step4_tip2": "Step 2 计算中点 - 球面几何算法计算公平中心", + "how.step4_tip3": "Step 3 搜索周边 - 多场景并发搜索POI", + "how.step4_tip4": "Step 4 GPT-4o评分 - AI多维度智能评估", + "how.step4_tip5": "Step 5 生成推荐 - 排序输出最优结果", + "how.step5_title": "查看结果与导航", + "how.step5_desc": "推荐结果包含地图可视化、场所卡片、AI推理过程展示。单场景 5-8 秒,双场景 8-12 秒,Agent模式 15-30 秒。", + "how.step5_tips_label": "结果包含", + "how.step5_tip1": "地图上的公平中心点(绿色)和推荐场所(橙色)", + "how.step5_tip2": "场所名称、评分、智能评分(满分100)", + "how.step5_tip3": "一键导航按钮,直接跳转高德地图", + "how.step5_tip4": "AI推理过程透明展示,可解释推荐理由", + "how.advanced_title": "AI Agent 能力", + "how.adv_geo_title": "球面几何算法", + "how.adv_geo_desc": "使用 Haversine 公式计算地球曲面真实中点,比简单经纬度平均精确 15-20%。", + "how.adv_gpt_title": "GPT-4o 智能评分", + "how.adv_gpt_desc": "AI 综合评估评分、距离、场景匹配、用户需求,给出多维度智能排序。", + "how.adv_uni_title": "60+ 高校简称识别", + "how.adv_uni_desc": "输入「北大」自动识别为「北京大学」,避免歧义,智能匹配正确地点。", + "how.adv_themes_title": "12 种场景主题", + "how.adv_themes_desc": "咖啡馆、餐厅、图书馆、KTV、健身房、密室逃脱等,支持多场景并发搜索。", + "how.cta_title": "准备好开始了吗?", + "how.cta_desc": "体验 AI Agent 5步透明推理,几秒钟找到对所有人都公平的聚会地点", + "how.cta_btn": "立即开始使用", + + "chat.status": "在线 · GPT-4o 驱动", + "chat.welcome_title": "MeetSpot AI Agent 助手", + "chat.welcome_subtitle": "球面几何 + GPT-4o智能评分 · 5步透明推理", + "chat.welcome_msg": "您好!我是 MeetSpot AI Agent 助手,可以帮您了解:

5步AI推理:地址解析 → 中点计算 → 周边搜索 → GPT-4o评分 → 智能推荐
核心优势:球面几何算法,数学上对每个人都公平
产品能力:350+城市 · 12种场景 · 60+高校简称

请问有什么可以帮您的?", + "chat.presets_label": "快捷问题", + "chat.preset_principle": "AI Agent原理", + "chat.preset_principle_q": "AI Agent怎么工作的?", + "chat.preset_speed": "推荐速度", + "chat.preset_speed_q": "推荐需要多久?", + "chat.preset_vs_amap": "vs高德地图", + "chat.preset_vs_amap_q": "和高德地图有什么区别?", + "chat.preset_pricing": "是否收费", + "chat.preset_pricing_q": "是否收费?", + "chat.input_placeholder": "输入您的问题...", + "chat.error_generic": "抱歉,我遇到了一些问题。请稍后再试。", + "chat.error_network": "网络连接出现问题,请检查网络后重试。", + + "finder.title": "MeetSpot (聚点) - 智能会面点推荐", + "finder.nav_home": "首页", + "finder.nav_guide": "使用指南", + "finder.nav_faq": "常见问题", + "finder.nav_about": "关于我们", + "finder.header_title": "MeetSpot 聚点", + "finder.header_desc": "智能会面点推荐系统 - 让每次聚会都找到完美地点", + "finder.step1": "输入地点", + "finder.step2": "选择场景", + "finder.step3": "获取推荐", + "finder.section_locations": "参与者地点", + "finder.section_locations_hint": "输入2-10个地点,系统将计算最佳中点", + "finder.location_placeholder_1": "输入第一个地点,如:北京朝阳区望京", + "finder.location_placeholder_2": "输入第二个地点,如:海淀区中关村", + "finder.location_placeholder_n": "输入地点", + "finder.location_count_hint": "已输入{count}个地点,可添加更多(最多10个)", + "finder.add_location": "添加更多地点", + "finder.section_themes": "选择场景类型", + "finder.section_themes_hint": "可选择1-3种场景,推荐更精准", + "finder.cat_food": "美食饮品", + "finder.cat_leisure": "休闲娱乐", + "finder.cat_culture": "文化学习", + "finder.cat_shopping": "购物休闲", + "finder.type_cafe": "咖啡馆", + "finder.type_restaurant": "餐厅", + "finder.type_teahouse": "茶楼", + "finder.type_bar": "酒吧", + "finder.type_cinema": "电影院", + "finder.type_arcade": "游戏厅", + "finder.type_gym": "健身房", + "finder.type_library": "图书馆", + "finder.type_bookstore": "书店", + "finder.type_museum": "博物馆", + "finder.type_attraction": "景点", + "finder.type_mall": "商场", + "finder.type_shopping_center": "购物中心", + "finder.type_park": "公园", + "finder.themes_hint": "请选择您喜欢的场景类型(最多3个)", + "finder.custom_keywords_placeholder": "或输入自定义关键词:游泳馆、瑜伽馆、密室逃脱...", + "finder.section_filters": "筛选条件", + "finder.section_filters_hint": "可选,帮助您找到更匹配的场所", + "finder.min_rating": "最低评分", + "finder.rating_any": "不限制", + "finder.rating_3": "3.0分以上", + "finder.rating_35": "3.5分以上", + "finder.rating_4": "4.0分以上", + "finder.rating_45": "4.5分以上", + "finder.max_distance": "最大距离", + "finder.distance_any": "不限制", + "finder.distance_5km": "5公里内", + "finder.distance_10km": "10公里内", + "finder.distance_20km": "20公里内", + "finder.distance_30km": "30公里内", + "finder.distance_50km": "50公里内", + "finder.price_range": "价格区间", + "finder.price_any": "不限制", + "finder.price_economy": "经济实惠", + "finder.price_mid": "中等消费", + "finder.price_high": "高端消费", + "finder.section_requirements": "特殊需求", + "finder.section_requirements_hint": "可选,描述您的特殊要求", + "finder.requirements_placeholder": "请描述您的特殊需求,例如:\n• 停车方便\n• 环境安静\n• 适合商务洽谈", + "finder.common_requirements": "常用需求(点击添加)", + "finder.req_parking": "停车方便", + "finder.req_quiet": "环境安静", + "finder.req_wifi": "有Wi-Fi", + "finder.req_business": "适合商务", + "finder.req_children": "适合儿童", + "finder.req_private_room": "有包间", + "finder.req_long_stay": "可以久坐", + "finder.req_24h": "24小时营业", + "finder.submit_btn": "查找最佳会面点", + "finder.loading_title": "智能分析中", + "finder.loading_text": "正在为您寻找最佳会面点...", + "finder.loading_step1": "解析地址", + "finder.loading_step1_desc": "获取各位置坐标信息", + "finder.loading_step2": "计算中心点", + "finder.loading_step2_desc": "寻找最公平的会面位置", + "finder.loading_step3": "搜索周边场所", + "finder.loading_step3_desc": "发现附近优质地点", + "finder.loading_step4": "智能评分排序", + "finder.loading_step4_desc": "综合评估最佳选择", + "finder.loading_step5": "生成推荐", + "finder.loading_step5_desc": "为您定制专属方案", + "finder.amap_note": "地址建议使用中文(使用高德地图)", + + "finder.payment_free_label": "今日免费", + "finder.payment_buy": "购买", + "finder.payment_modal_title": "每日 1 次免费已用完", + "finder.payment_modal_desc": "可购买 credits 继续使用,或明日再试", + "finder.payment_buy_btn": "购买 Credits", + "finder.payment_later": "稍后再说", + "finder.payment_balance_title": "余额不足", + "finder.payment_balance_desc": "购买更多 credits 继续获取推荐", + "finder.payment_balance_prefix": "当前余额:", + "finder.payment_balance_suffix": "credits", + + "finder.warn_max_types": "已达上限", + "finder.warn_max_types_msg": "最多选择{max}个场景类型", + "finder.warn_max_locations": "最多支持10个地点", + "finder.warn_min_locations": "至少需要保留两个地点", + "finder.error_too_few": "请至少输入两个地点", + "finder.error_too_many": "最多支持10个地点", + "finder.success_agent": "Agent 深度分析完成", + "finder.success_agent_desc": "已为您生成个性化推荐方案", + "finder.success_rule": "快速推荐完成", + "finder.success_rule_desc": "已为您找到最佳会面点", + "finder.info_quota_used": "今日 1 次免费已用完,可购买 credits 或明日再试", + "finder.warn_no_result": "未找到结果", + "finder.warn_no_result_desc": "请尝试更换关键词或使用更详细的地址", + "finder.error_request": "请求失败", + "finder.btn_buy_credits": "购买 Credits 后使用", + + "api.error.quota_exceeded": "今日免费次数已用完,请购买 credits 继续使用", + "api.error.rate_limit": "请求过于频繁, 请稍后再试", + "api.error.amap_not_configured": "高德地图API密钥未配置,请设置AMAP_API_KEY环境变量或配置config.toml文件", + "api.error.config_error": "服务配置错误:无法加载推荐模块,请确保在本地环境运行或正确配置Vercel环境变量", + "api.chat.fallback": "抱歉,AI客服暂时不可用。您可以直接使用我们的会面点推荐功能,或查看页面上的使用说明。如有问题请稍后再试。", + "api.chat.configuring": "AI客服配置中,请稍后再试。您也可以直接体验我们的会面点推荐功能!", + "api.chat.error": "抱歉,AI客服遇到了问题。您可以直接使用会面点推荐功能,或稍后再试。", + + "seo.home.title": "MeetSpot 聚点 - 多人聚会地点智能推荐", + "seo.home.description": "MeetSpot 帮助 2-10 人快速找到公平的聚会中点,智能推荐咖啡馆、餐厅、图书馆等场所,球面几何算法计算公平中心,覆盖 350+ 城市,免费使用无需注册。", + "seo.about.title": "关于 MeetSpot 聚点 - 智能聚会地点推荐", + "seo.faq.title": "常见问题 - MeetSpot 聚点", + "seo.how.title": "使用指南 - MeetSpot 聚点", + "seo.breadcrumb.home": "首页", + "seo.breadcrumb.about": "关于我们", + "seo.breadcrumb.guide": "使用指南", + "seo.breadcrumb.faq": "常见问题", + + "city.venues_header": "{city}热门聚会场所类型", + "city.features_title": "为什么在{city}使用MeetSpot?", + "city.metro_title": "{count}条地铁线路", + "city.metro_desc": "{city}地铁网络发达,MeetSpot优先推荐地铁站周边的聚会场所", + "city.midpoint_title": "智能中点计算", + "city.midpoint_desc": "球面几何算法确保每位参与者通勤距离公平均衡", + "city.local_title": "本地精选场所", + "city.local_desc": "覆盖{city}{venues}等热门类型,高评分场所优先推荐", + "city.landmarks_title": "{city}热门聚会区域", + "city.landmarks_group": "地标商圈", + "city.districts_group": "商务中心", + "city.universities_group": "高校聚集区", + "city.use_cases_title": "{city}真实使用场景", + "city.tips_title": "{city}聚会小贴士", + "city.how_title": "如何在{city}找到最佳聚会地点?", + "city.how_step1_title": "输入参与者位置", + "city.how_step1_desc": "支持输入{city}任意地址、地标或高校名称", + "city.how_step2_title": "选择场所类型", + "city.how_step2_desc": "根据聚会目的选择{venues}等场景", + "city.how_step3_title": "获取智能推荐", + "city.how_step3_desc": "系统自动计算地理中点,推荐高评分场所", + "city.cta_title": "开始规划{city}聚会", + "city.cta_desc": "无需注册,输入地址即可获取推荐", + "city.cta_btn": "立即使用 MeetSpot" +} diff --git a/public/meetspot_finder.html b/public/meetspot_finder.html index 298ffda..36f6235 100644 --- a/public/meetspot_finder.html +++ b/public/meetspot_finder.html @@ -1660,6 +1660,14 @@ } + + + + + diff --git a/templates/base.html b/templates/base.html index 7035e07..cc3596b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,5 +1,5 @@ - + @@ -10,6 +10,12 @@ + {% if hreflang %} + {% for hl in hreflang %} + + {% endfor %} + {% endif %} + @@ -690,25 +696,31 @@ {% endif %} - 跳转到主内容 + {{ t['nav.skip_to_main'] }} @@ -735,63 +747,64 @@ @@ -799,20 +812,20 @@

添加个人微信

@@ -505,18 +505,14 @@

MeetSpot AI Agent

-
MeetSpot AI Agent 助手
-

球面几何 + GPT-4o智能评分 · 5步透明推理

+
{{ t['chat.welcome_title'] }}
+

{{ t['chat.welcome_subtitle'] }}

🤖
- 您好!我是 MeetSpot AI Agent 助手,可以帮您了解:

- • 5步AI推理:地址解析 → 中点计算 → 周边搜索 → GPT-4o评分 → 智能推荐
- • 核心优势:球面几何算法,数学上对每个人都公平
- • 产品能力:350+城市 · 12种场景 · 60+高校简称

- 请问有什么可以帮您的? + {{ t['chat.welcome_msg'] | safe }}
@@ -527,13 +523,13 @@
MeetSpot AI Agent 助手
- 快捷问题 + {{ t['chat.presets_label'] }}
- - - - + + + +
@@ -541,7 +537,7 @@
MeetSpot AI Agent 助手