Files

1 line
16 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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 <commit-hash>\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"}}}}