{"repo": "calderbuild/meetspot", "files": {"CLAUDE.md": {"d0b3ea95da4415fa9de59faab890357b328e6237": {"t": 1785674333.0053139, "n": 15134, "h": "10da8420b49a44fa", "c": "# CLAUDE.md\r\n\r\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\r\n\r\n## Project Overview\r\n\r\nMeetSpot is an **AI Agent** for multi-person meeting point recommendations. Users provide locations and requirements; the Agent calculates the geographic center and recommends optimal venues. Built with FastAPI and Python 3.11+, uses Amap (Gaode Map) for 国内场景 + Google Maps Platform for 国际场景, and OpenRouter (default `openai/gpt-4o-mini`) for semantic scoring + AI chat.\r\n\r\n**Live Demo**: https://meetspot-irq2.onrender.com\r\n\r\n## Quick Reference\r\n\r\n```bash\r\n# Environment\r\nconda activate meetspot # Or: source venv/bin/activate\r\n\r\n# Development\r\nuvicorn api.index:app --reload # Preferred for iteration\r\npython web_server.py # Full stack with auto env detection\r\n\r\n# Test the main endpoint\r\ncurl -X POST \"http://127.0.0.1:8000/api/find_meetspot\" \\\r\n -H \"Content-Type: application/json\" \\\r\n -d '{\"locations\": [\"北京大学\", \"清华大学\"], \"keywords\": \"咖啡馆\"}'\r\n\r\n# Testing\r\npytest tests/ -v # Full suite\r\npytest tests/test_file.py::test_name -v # Single test\r\npytest --cov=app tests/ # Coverage (target: 80%)\r\npython tests/test_seo.py http://localhost:8000 # SEO validation (standalone)\r\n\r\n# NOTE: tests/ is gitignored -- tests exist locally but are not in the repo\r\n\r\n# Quality gates (run before PRs)\r\nblack . && ruff check . && mypy app/\r\n\r\n# Postmortem regression check (optional, runs in CI)\r\npython tools/postmortem_check.py # Check for known issue patterns\r\n```\r\n\r\n**Key URLs**: Main UI (`/`), API docs (`/docs`), Health (`/health`)\r\n\r\n## Repo Rules\r\n\r\n- Follow `AGENTS.md` for repo-local guidelines (style, structure, what not to commit). In particular: runtime-generated files under `workspace/js_src/` must not be committed.\r\n- Active feature plans live in `docs/plans/` (Markdown with YAML frontmatter, date-prefixed). Check there before implementing new features to avoid conflicting with planned work.\r\n\r\n## Environment Setup\r\n\r\n**Conda**: `conda env create -f environment.yml && conda activate meetspot` (env name is `meetspot`, not `meetspot-dev`)\r\n**Pip**: `python3.11 -m venv venv && source venv/bin/activate && pip install -r requirements.txt`\r\n\r\n**Required Environment Variables**:\r\n- `AMAP_API_KEY` - Gaode Map API key (required)\r\n- `AMAP_SECURITY_JS_CODE` - JS security code for frontend map\r\n- `LLM_API_KEY` - OpenRouter / OpenAI / 兼容服务的 API key(生产用 OpenRouter `sk-or-v1-...` 形式)\r\n- `LLM_API_BASE` - API base URL (default: `https://openrouter.ai/api/v1`)\r\n- `LLM_MODEL` - Model name (default: `openai/gpt-4o-mini`,OpenRouter slug 格式,可换 `google/gemini-2.5-flash` / `anthropic/claude-haiku-4-5` 等)\r\n- `GOOGLE_MAPS_API_KEY` - Google Maps Platform key(国际场景必需,缺失时 /en/ 路径地图静默降级)\r\n\r\n**Local Config**: Copy `config/config.toml.example` to `config/config.toml` and fill in API keys. Alternatively, create a `.env` file with the environment variables above.\r\n\r\n## Architecture\r\n\r\n### Request Flow\r\n\r\n```\r\nPOST /api/find_meetspot\r\n ↓\r\nComplexity Router (assess_request_complexity)\r\n ↓\r\nRule+LLM Mode (Agent mode disabled for memory savings on free tier)\r\n ↓\r\n5-Step Pipeline: Geocode → Center Calc → POI Search → Ranking → HTML Gen\r\n```\r\n\r\nComplexity scoring: +10/location, +15 for complex keywords, +10 for special requirements. Currently all requests use Rule+LLM mode (see Concurrency & Memory Budget).\r\n\r\n### Entry Points\r\n- `web_server.py` - Main entry, auto-detects production vs development\r\n- `api/index.py` - FastAPI app with all endpoints, middleware, rate limiting (slowapi), CORS, and request concurrency control (MAX_CONCURRENT_REQUESTS = 3)\r\n- `npm run dev` / `npm start` - Proxy to the same Python entry point for platforms that expect Node scripts\r\n\r\n### Three-Tier Configuration (Graceful Degradation)\r\n\r\n| Mode | Trigger | What Works |\r\n|------|---------|------------|\r\n| Full | `config/config.toml` exists | All features, TOML-based config |\r\n| Simplified | `RAILWAY_ENVIRONMENT` set | Uses `app/config_simple.py` |\r\n| Minimal | Only `AMAP_API_KEY` env var | `MinimalConfig` class in `api/index.py`, basic recommendations only |\r\n\r\n### Core Components\r\n\r\n```\r\napp/tool/meetspot_recommender.py # Main recommendation engine (CafeRecommender class)\r\n |- _enhance_address() # 10 hardcoded aliases with city prefix\r\n |- PLACE_TYPE_CONFIG dict # 12 venue themes with colors, icons\r\n |- BRAND_FEATURES dict # 50+ brand profiles with feature scores\r\n |- _rank_places() # 100-point scoring algorithm\r\n |- _generate_html_content() # Standalone HTML with Amap JS API\r\n |- geocode_cache (max 30) # LRU-style address cache (reduced for free tier)\r\n |- poi_cache (max 15) # LRU-style POI cache (reduced for free tier)\r\n\r\ndata/address_aliases.json # 48 university + 5 landmark abbreviation mappings\r\napp/design_tokens.py # WCAG AA color palette, CSS generation\r\napi/routers/seo_pages.py # SEO landing pages + /compare page\r\napi/services/seo_content.py # SEOContentGenerator: meta tags, JSON-LD schema, city page snippets\r\n```\r\n\r\n### Frontend Address Input\r\n\r\n`public/meetspot_finder.html` uses AMap Autocomplete API for real-time address suggestions. When users select from dropdown, coordinates are pre-resolved client-side, bypassing backend geocoding. Falls back to backend geocoding when manual text is entered.\r\n\r\n### AI Chat\r\n\r\n`/api/ai_chat` (POST) — bilingual conversational interface. Accepts `message`, `language`, and `conversation_history`. Returns streaming or full response using the configured LLM backend (DeepSeek/OpenAI/OpenRouter). System prompts: `MEETSPOT_SYSTEM_PROMPT` (zh) and `MEETSPOT_SYSTEM_PROMPT_EN` (en) defined in `api/index.py`.\r\n\r\n`/api/ai_chat/preset_questions` (GET) — returns language-specific suggested questions for the chat UI.\r\n\r\n### LLM Scoring (Agent Mode)\r\n\r\nWhen Agent Mode is enabled, final venue scores blend rule-based and LLM semantic analysis:\r\n```\r\nFinal Score = Rule Score * 0.4 + LLM Score * 0.6\r\n```\r\n\r\n**Current status**: `agent_available = False` in `api/index.py` (line 59) — hardcoded off for memory reasons. `/api/find_meetspot_agent` endpoint exists but returns disabled error. Re-enabling requires bumping Render tier.\r\n\r\n### Token Counting\r\n\r\n`app/llm.py` uses UTF-8 byte length estimation (`len(text.encode(\"utf-8\")) // 3`) instead of tiktoken. This avoids loading tiktoken's ~80MB model data. Precision is sufficient for internal token limit checks -- not used for billing or exact truncation.\r\n\r\n### i18n System\r\n\r\nLightweight JSON-based translations in `app/i18n.py`. Supported languages: `en` (default), `zh`. Translation files: `locales/{lang}.json`.\r\n\r\nLanguage detection priority in `detect_language()`: URL prefix (`/en/` or `/zh/`) > Cookie (`lang`) > `Accept-Language` header > default `en` (国际化优先)。\r\n\r\n**`/` 路由特殊**:`api/routers/seo_pages.py` 的根路径硬默认英文(仅 cookie `lang=zh` 时才返回中文),不依赖 Accept-Language。中文用户走 `/zh/` 显式路径或点击导航中文按钮(设置 cookie)。\r\n\r\nBoth `locales/zh.json` and `locales/en.json` are complete and in sync. To add a new language: create `locales/{lang}.json` with all keys from `locales/en.json`, then add the lang code to `SUPPORTED_LANGS` in `app/i18n.py`.\r\n\r\n### 地图 Provider 双轨架构\r\n\r\n`app/tool/meetspot_recommender.py` 的 `CafeRecommender` 根据 `language` 参数自动切换地图 provider:\r\n\r\n| 路径 | provider | 客户端 | 前端 |\r\n|------|---------|--------|------|\r\n| `/` (默认) / `/en/*` | google | `app/tool/google_maps_client.py` (Places API New v1) | Google Maps JS API + `places.Autocomplete` |\r\n| `/zh/*` 或 cookie/Header zh | amap | 内联高德 REST 调用 | 高德 JS API + `AMap.AutoComplete` |\r\n\r\n切换点:`execute()` 入口设置 `self.map_provider`;`_geocode` 和 `_search_pois` 内部按字段分发;`_generate_html_content` 中 `map_script_block` 变量在 `if self.map_provider == \"google\"` 分支构造对应 JS 模板。Google client 输出严格归一化为高德 POI 格式(`location=\"lng,lat\"` 字符串、`biz_ext.rating` 字符串),让 `_rank_places` 逻辑无需感知 provider 差异。\r\n\r\n**Key 配置**:`AMAP_API_KEY` + `GOOGLE_MAPS_API_KEY` 环境变量,仅有高德时只走国内路径(向后兼容)。前端通过 `/api/config/amap` 和 `/api/config/google_maps` 获取 key。\r\n\r\n### Concurrency & Memory Budget\r\n\r\n`MAX_CONCURRENT_REQUESTS = 3` semaphore in `api/index.py` prevents OOM on Render's 512MB free tier. Rate limiting via slowapi. Agent mode disabled (`agent_available = False`) for the same reason. If re-enabling Agent mode or raising concurrency, monitor memory on the hosting tier.\r\n\r\n### Optional Components\r\n\r\nDatabase layer (`app/db/`, `app/models/`) is optional -- core recommendation works without it. Used for auth/payment/social features. Supports PostgreSQL (Supabase via asyncpg, with pgbouncer transaction mode hardening) as primary, SQLite + aiosqlite as local fallback. Controlled by `DATABASE_URL` env var in `app/db/database.py`.\r\n\r\nPayment integration: 302.ai checkout via `api/routers/payment.py`. Config: `PAY302_APP_ID`, `PAY302_SECRET`, `PAY302_API_URL` env vars. Free daily limit (`FREE_DAILY_LIMIT`, default 1) + credit purchase system (`CREDIT_PRICE_CENTS`, `CREDITS_PER_PURCHASE`). Signature verification in `app/payment/signature.py`.\r\n\r\nAPI routers:\r\n- `api/routers/auth.py` — SMS auth: `POST /api/auth/send_code`, `POST /api/auth/verify_code`, `GET /api/auth/me`\r\n- `api/routers/payment.py` — 302.ai checkout: `/create`, `/webhook`, `/success`, `/status/{id}`, `/balance`, `/free-remaining`\r\n- `api/routers/seo_pages.py` — SEO pages including `/compare` and `/en/compare`\r\n\r\n## Key Patterns\r\n\r\n### Ranking Algorithm\r\nEdit `_rank_places()` in `meetspot_recommender.py`:\r\n- Base: 30 points (rating x 6)\r\n- Popularity: 20 points (log-scaled reviews)\r\n- Distance: 25 points (500m = full score, decays)\r\n- Scenario: 15 points (keyword match)\r\n- Requirements: 10 points (parking/quiet/business)\r\n\r\n### Distance Filtering\r\nTwo-stage distance handling in `meetspot_recommender.py`:\r\n1. **POI Search**: Amap API `radius` parameter (hardcoded 5000m, fallback to 50000m) in `_search_places()` calls\r\n2. **Post-filter**: `max_distance` parameter in `_rank_places()` (default 100km, in meters)\r\n\r\n### Brand Knowledge Base\r\n`BRAND_FEATURES` dict in `meetspot_recommender.py` contains 50+ brand profiles (Starbucks, Haidilao, etc.) with feature scores (0.0-1.0) for: quiet, WiFi, business, parking, child-friendly, 24h. Used in requirements matching - brands scoring >=0.7 satisfy the requirement. Place types prefixed with `_` (e.g., `_library`) provide defaults.\r\n\r\n### Adding Address Mappings\r\n**Preferred**: Edit `data/address_aliases.json` -- maps abbreviations to full names (e.g., \"北大\" -> \"北京大学\"). This file has `university_aliases` and `landmark_aliases` sections.\r\n\r\n**For cross-city ambiguity**: Add to the `alias_to_fullname` dict inside `_enhance_address()` in `meetspot_recommender.py` with full city prefix (e.g., \"华工\" -> \"广东省广州市华南理工大学\"). These hardcoded aliases include city/district for geocoding precision.\r\n\r\n### Adding Venue Themes\r\nAdd entry to `PLACE_TYPE_CONFIG` with: Chinese name, Boxicons icons, 6 color values.\r\n\r\n### Sitemap lastmod\r\n`seo_pages.py` sitemap 用静态 `CONTENT_DATES` 字典,不要用 `datetime.now()`。Google 会忽略不可靠的 lastmod 信号。内容实际更新时手动修改日期。\r\n\r\n## Postmortem System\r\n\r\nAutomated regression prevention system that tracks historical fixes and warns when code changes might reintroduce past bugs.\r\n\r\n### Structure\r\n```\r\npostmortem/\r\n PM-2025-001.yaml ... PM-2026-xxx.yaml # Historical fix documentation\r\ntools/\r\n postmortem_init.py # Generate initial knowledge base from git history\r\n postmortem_check.py # Check code changes against known patterns\r\n postmortem_generate.py # Generate postmortem for a single commit\r\n```\r\n\r\n### CI Integration\r\n- `postmortem-check.yml`: Runs on PRs, warns if changes match known issue patterns\r\n- `postmortem-update.yml`: Auto-generates postmortem when `fix:` commits merge to main\r\n\r\n### Adding New Postmortems\r\nWhen fixing a bug, the CI will auto-generate a postmortem. For manual creation:\r\n```bash\r\npython tools/postmortem_generate.py \r\n```\r\n\r\nEach postmortem YAML contains triggers (file patterns, function names, regex, keywords) that enable multi-dimensional pattern matching.\r\n\r\n## Debugging\r\n\r\n| Issue | Solution |\r\n|-------|----------|\r\n| `未找到AMAP_API_KEY` | Set environment variable |\r\n| Import errors in production | Check MinimalConfig fallback |\r\n| Wrong city geocoding | Add to `_enhance_address()` alias dict with city prefix |\r\n| SSR 页面 env var 读取为空 | 勿用 `templates.env.globals[\"key\"] = os.getenv(...)`(模块导入时求值);改用 `TemplateResponse` context 字典在每次请求时动态传入(见 `_common_context()` in `api/routers/seo_pages.py`) |\r\n| Render OOM (512MB) | Heavy deps removed (jieba/tiktoken); caches reduced (30/15 limits); Agent mode disabled. If OOM recurs, check `pip list` for new heavy imports |\r\n| asyncpg + pgbouncer errors | `app/db/database.py` disables prepared statement cache and uses dynamic statement names. If adding raw SQL, avoid named prepared statements |\r\n| `meetspot_finder.html` 缺全局功能 | 独立静态页不继承 `base.html`,新增全局功能(GA4、schema、trackEvent)需单独在该文件处理 |\r\n| flake8 E999 f-string 反斜杠 | Python 3.11 不允许 f-string 表达式含反斜杠(`\\\"`),需先提取为变量再插值 |\r\n| `git push` 被 postmortem CI 自动 commit reject | push 前先 `git pull --rebase origin main` |\r\n\r\n**Logging**: Uses loguru via `app/logger.py`. `/health` endpoint shows config status.\r\n\r\n## Deployment\r\n\r\nHosted on Render free tier (512MB RAM, cold starts after 15min idle).\r\nService ID: `srv-d2di8295pdvs73eu3re0`. Render CLI installed (`brew install render`), workspace set.\r\n\r\n**Redeploy**: Push to `main` branch triggers auto-deploy. For manual restart without code changes:\r\n```bash\r\ngit commit --allow-empty -m \"chore: trigger redeploy\" && git push origin main\r\n```\r\n\r\n**Render CLI**: `render deploys list srv-d2di8295pdvs73eu3re0 --confirm -o json` to check deploy status. Environment variables managed via API (`PUT /v1/services/{id}/env-vars` -- WARNING: PUT is full replacement, always GET first and merge).\r\n\r\n**Generated artifacts**: HTML files in `workspace/js_src/` are runtime-generated and should not be committed.\r\n\r\n## CI/CD\r\n\r\n9 GitHub Actions workflows in `.github/workflows/`:\r\n\r\n| Workflow | Trigger | Purpose |\r\n|----------|---------|---------|\r\n| `ci.yml` | Push/PR | Python 3.11 & 3.12 tests, flake8, Docker build |\r\n| `ci-simple.yml` | Push/PR | Lightweight CI variant |\r\n| `ci-clean.yml` | Push/PR | Clean CI variant |\r\n| `postmortem-check.yml` | PRs | Warns if changes match known bug patterns |\r\n| `postmortem-update.yml` | `fix:` commits to main | Auto-generates postmortem YAML |\r\n| `keep-alive.yml` | Cron | Prevents Render free tier cold starts |\r\n| `lighthouse-ci.yml` | On demand | Performance metrics |\r\n| `update-badges.yml` | On demand | Update repo badges |\r\n| `auto-merge-clean.yml` | Dependabot PRs | Auto-merge dependency updates |\r\n\r\n## Gitignore Gotchas\r\n\r\nThe `.gitignore` has unusually broad patterns -- be aware:\r\n- `tests/` directory and all test-like files (`*test*.py`, `test_*.py`) are gitignored. Tests exist locally only.\r\n- `Dockerfile` and `docker-compose.yml` are gitignored.\r\n- `workspace/js_src/` is gitignored (runtime-generated HTML).\r\n"}}}}