Skip to content

Commit 930c4d1

Browse files
calderbuildclaude
andcommitted
feat: add AMap Autocomplete for precise location selection
Problem: Ambiguous addresses like "陈村地铁站" were being geocoded to wrong locations (Zhaoqing instead of Foshan Shunde). Solution: Implement AMap Autocomplete that lets users select exact locations from a dropdown. Selected coordinates are sent directly to backend, bypassing geocoding entirely. Changes: - api/index.py: Add LocationCoord model, /api/config/amap endpoint with separate AMAP_JS_API_KEY for frontend maps - public/meetspot_finder.html: Integrate AMap AutoComplete plugin (note: capital C in AMap 2.0), capture coordinates on selection - app/tool/meetspot_recommender.py: Accept pre_resolved_coords parameter, skip geocoding when coordinates provided - data/address_aliases.json: External file for address mappings - CLAUDE.md: Document postmortem system and address alias sources Tested: "陈村北(地铁站)" now correctly resolves to Foshan Shunde (113.245,22.976) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 2e3248a commit 930c4d1

File tree

5 files changed

+3907
-3485
lines changed

5 files changed

+3907
-3485
lines changed

CLAUDE.md

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,18 @@ python tests/test_seo.py http://localhost:8000 # SEO validation (standalone)
3131

3232
# Quality gates (run before PRs)
3333
black . && ruff check . && mypy app/
34+
35+
# Postmortem regression check (optional, runs in CI)
36+
python tools/postmortem_check.py # Check for known issue patterns
3437
```
3538

3639
**Key URLs**: Main UI (`/`), API docs (`/docs`), Health (`/health`)
3740

41+
## Repo Rules
42+
43+
- 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.
44+
- There are no Cursor/Copilot rule files in this repo (no `.cursorrules`, no `.cursor/rules/`, no `.github/copilot-instructions.md`).
45+
3846
## Environment Setup
3947

4048
**Conda**: `conda env create -f environment.yml && conda activate meetspot` (env name is `meetspot`, not `meetspot-dev`)
@@ -140,13 +148,39 @@ The `max_distance` filter applies after POI retrieval during ranking. To change
140148
`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.
141149

142150
### Adding Address Mappings
143-
In `_enhance_address()` method:
144-
- `university_mapping` dict for university abbreviations
145-
- `landmark_mapping` dict for city landmarks (prevents cross-city geocoding errors)
151+
Two sources for address resolution:
152+
1. **External file**: `data/address_aliases.json` - JSON file with `university_aliases` and `landmark_aliases` dicts. Preferred for new mappings.
153+
2. **Internal dicts**: `university_mapping` and `landmark_mapping` in `_enhance_address()` method of `meetspot_recommender.py`. Use for mappings requiring city prefixes (prevents cross-city geocoding errors).
146154

147155
### Adding Venue Themes
148156
Add entry to `PLACE_TYPE_CONFIG` with: Chinese name, Boxicons icons, 6 color values.
149157

158+
## Postmortem System
159+
160+
Automated regression prevention system that tracks historical fixes and warns when code changes might reintroduce past bugs.
161+
162+
### Structure
163+
```
164+
postmortem/
165+
PM-2025-001.yaml ... PM-2026-xxx.yaml # Historical fix documentation
166+
tools/
167+
postmortem_init.py # Generate initial knowledge base from git history
168+
postmortem_check.py # Check code changes against known patterns
169+
postmortem_generate.py # Generate postmortem for a single commit
170+
```
171+
172+
### CI Integration
173+
- `postmortem-check.yml`: Runs on PRs, warns if changes match known issue patterns
174+
- `postmortem-update.yml`: Auto-generates postmortem when `fix:` commits merge to main
175+
176+
### Adding New Postmortems
177+
When fixing a bug, the CI will auto-generate a postmortem. For manual creation:
178+
```bash
179+
python tools/postmortem_generate.py <commit-hash>
180+
```
181+
182+
Each postmortem YAML contains triggers (file patterns, function names, regex, keywords) that enable multi-dimensional pattern matching.
183+
150184
## Debugging
151185

152186
| Issue | Solution |

api/index.py

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@
4040
print(f"⚠️ 导入模块警告: {e}")
4141
config = None
4242
config_available = False
43+
# 创建 fallback logger(当 app.logger 导入失败时)
44+
import logging
45+
logging.basicConfig(level=logging.INFO)
46+
logger = logging.getLogger("meetspot")
47+
48+
# Fallback for init_db
49+
async def init_db():
50+
logger.warning("Database module not available, skipping init")
4351

4452
# 导入 Agent 模块(高内存消耗,暂时禁用以保证稳定性)
4553
agent_available = False # 禁用 Agent 模式,节省内存
@@ -196,6 +204,15 @@ class LocationRequest(BaseModel):
196204
venue_types: Optional[List[str]] = ["咖啡馆"]
197205
user_requirements: Optional[str] = ""
198206

207+
class LocationCoord(BaseModel):
208+
"""预解析的地址坐标信息(来自前端 Autocomplete 选择)"""
209+
name: str # 用户选择的地点名称
210+
address: str # 完整地址
211+
lng: float # 经度
212+
lat: float # 纬度
213+
city: Optional[str] = "" # 城市名
214+
215+
199216
class MeetSpotRequest(BaseModel):
200217
locations: List[str]
201218
keywords: Optional[str] = "咖啡馆"
@@ -205,6 +222,8 @@ class MeetSpotRequest(BaseModel):
205222
min_rating: Optional[float] = 0.0 # 最低评分 (0-5)
206223
max_distance: Optional[int] = 100000 # 最大距离 (米)
207224
price_range: Optional[str] = "" # 价格区间: economy/mid/high
225+
# 预解析坐标(可选,由前端 Autocomplete 提供)
226+
location_coords: Optional[List[LocationCoord]] = None
208227

209228
class AIChatRequest(BaseModel):
210229
message: str
@@ -267,6 +286,7 @@ class AIChatRequest(BaseModel):
267286

268287
# 环境变量配置(用于 Vercel)
269288
AMAP_API_KEY = os.getenv("AMAP_API_KEY", "")
289+
AMAP_JS_API_KEY = os.getenv("AMAP_JS_API_KEY", "") # JS API key for frontend map
270290
AMAP_SECURITY_JS_CODE = os.getenv("AMAP_SECURITY_JS_CODE", "")
271291

272292
# 创建 FastAPI 应用
@@ -770,15 +790,31 @@ async def _process_meetspot_request(request: MeetSpotRequest, start_time: float)
770790
recommender = CafeRecommender()
771791

772792
print("🚀 开始执行推荐...")
793+
# 转换 location_coords 为推荐器期望的格式
794+
pre_resolved_coords = None
795+
if request.location_coords:
796+
pre_resolved_coords = [
797+
{
798+
"name": coord.name,
799+
"address": coord.address,
800+
"lng": coord.lng,
801+
"lat": coord.lat,
802+
"city": coord.city or ""
803+
}
804+
for coord in request.location_coords
805+
]
806+
print(f"📍 使用前端预解析坐标: {len(pre_resolved_coords)} 个")
807+
773808
# 调用推荐工具
774809
result = await recommender.execute(
775810
locations=request.locations,
776-
keywords=request.keywords,
777-
place_type=request.place_type,
778-
user_requirements=request.user_requirements,
779-
min_rating=request.min_rating,
780-
max_distance=request.max_distance,
781-
price_range=request.price_range
811+
keywords=request.keywords or "咖啡馆",
812+
place_type=request.place_type or "",
813+
user_requirements=request.user_requirements or "",
814+
min_rating=request.min_rating or 0.0,
815+
max_distance=request.max_distance or 100000,
816+
price_range=request.price_range or "",
817+
pre_resolved_coords=pre_resolved_coords
782818
)
783819

784820
processing_time = time.time() - start_time
@@ -962,6 +998,34 @@ async def get_recommendations(request: LocationRequest):
962998
# 直接调用主端点并返回相同格式
963999
return await find_meetspot(meetspot_request)
9641000

1001+
1002+
@app.get("/api/config/amap")
1003+
async def get_amap_config():
1004+
"""返回 AMap 配置(用于前端地图和 Autocomplete)
1005+
1006+
Note: 前端需要 JS API key,与后端 geocoding 使用的 Web服务 key 不同
1007+
"""
1008+
# 优先使用 JS API key(前端地图专用)
1009+
js_api_key = AMAP_JS_API_KEY
1010+
security_js_code = AMAP_SECURITY_JS_CODE
1011+
1012+
# 从 config.toml 获取(如果存在)
1013+
if config and hasattr(config, "amap") and config.amap:
1014+
if not js_api_key:
1015+
js_api_key = getattr(config.amap, "js_api_key", "") or getattr(config.amap, "api_key", "")
1016+
if not security_js_code:
1017+
security_js_code = getattr(config.amap, "security_js_code", "")
1018+
1019+
# 最后回退到 Web服务 key(不推荐,可能无法加载地图)
1020+
if not js_api_key:
1021+
js_api_key = AMAP_API_KEY
1022+
1023+
return {
1024+
"api_key": js_api_key,
1025+
"security_js_code": security_js_code
1026+
}
1027+
1028+
9651029
@app.get("/api/status")
9661030
async def api_status():
9671031
"""API状态检查"""

0 commit comments

Comments
 (0)