# 战损、基地、地图战区 — 专项调试

只关心这三块时，按下面数据源 + 排查顺序即可。

---

## 一、战损 (combat_losses)

### 数据流

```
RSS 新闻(标题+摘要/正文) → 爬虫流水线 run_full_pipeline
  → extract_from_news(text) → combat_losses_delta { us: { personnel_killed, ... }, iran: { ... } }
  → db_merge.merge() → 按「增量」叠加到 combat_losses 表
  → POST /api/crawler/notify → Node 重载 DB
  → getSituation() 读 combat_losses → 前端 CombatLossesPanel / CombatLossesOtherPanel
```

- **表**：`combat_losses`（side=us / iran），字段含 personnel_killed、personnel_wounded、bases_destroyed、bases_damaged、aircraft、drones、missiles 等。
- **初始值**：`node server/seed.js` 会写入美/伊两行。
- **更新条件**：只有新闻里**明确出现可解析的伤亡/装备数字**（如「2 名美军死亡」「14 人受伤」「1 架战机受损」）时，提取器才会输出 `combat_losses_delta`，merge 才会叠加。

### 提取器选择（三选一）

| 环境变量 | 使用模块 | 说明 |
|----------|----------|------|
| `DASHSCOPE_API_KEY` 已设 | `extractor_dashscope` | 通义抽取，精度较好 |
| 未设通义 且 `CLEANER_AI_DISABLED≠1` | `extractor_ai` | 需本机 Ollama（如 llama3.1） |
| 未设通义 且 `CLEANER_AI_DISABLED=1` | `extractor_rules` | 规则正则，无需模型 |

### 验证命令

```bash
# API 返回的战损
curl -s http://localhost:3001/api/situation | jq '{
  us: .usForces.combatLosses.personnelCasualties,
  iran: .iranForces.combatLosses.personnelCasualties,
  conflictStats: .conflictStats
}'

# 表内原始值
sqlite3 server/data.db "SELECT side, personnel_killed, personnel_wounded, bases_destroyed, bases_damaged, aircraft FROM combat_losses"
```

### 常见问题

| 现象 | 可能原因 | 处理 |
|------|----------|------|
| 战损数字从不变化 | 1) 只跑了 main.py 未跑 gdelt<br>2) 新闻里没有明确伤亡/装备数字<br>3) 提取器未启用或报错（Ollama 未起、通义未配） | 跑 `npm run gdelt`；用带数字的新闻测；看爬虫日志是否有提取/merge 报错 |
| 数字暴增一次 | 提取器把「累计总数」当成本条增量 | 已用 MAX_DELTA_PER_MERGE 做单次上限；可查 db_merge.py |
| 想用已有事件脉络重算战损 | 历史新闻当时未做提取 | `curl -X POST http://localhost:8000/crawler/backfill` 用 situation_update 最近 50 条重新提取并 merge |

---

## 二、基地 (key_location)

### 数据流

```
RSS 新闻 → extract_from_news → key_location_updates: [ { name_keywords, side, status, damage_level } ]
  → db_merge.merge() → UPDATE key_location SET status=?, damage_level=? WHERE side=? AND (name LIKE ? OR ...)
  → getSituation() 读 key_location → 前端 BaseStatusPanel(美) / IranBaseStatusPanel(伊) / WarMap 据点层
```

- **表**：`key_location`（side=us / iran），字段含 name、lat、lng、type、region、**status**、**damage_level**。
- **初始数据**：seed 写入大量美/伊据点和基地（含 name）；**爬虫只更新已有行的 status、damage_level**，不新增行。
- **匹配规则**：提取器的 `name_keywords`（如 `阿萨德|asad`）会按 **LIKE '%关键词%'** 与 `key_location.name` 匹配。例如 name 为「阿萨德空军基地」时，关键词「阿萨德」能匹配。

### 规则提取器支持的基地关键词（与 seed name 对应关系）

- **美军**：阿萨德|阿因|asad → 匹配 seed「阿萨德空军基地」「阿因·阿萨德」；巴格达 → 巴格达外交支援中心；乌代德|卡塔尔 → 乌代德空军基地；埃尔比勒 → 埃尔比勒空军基地；因吉尔利克|土耳其 → 因吉尔利克空军基地；苏尔坦|沙特 → 苏尔坦亲王空军基地；坦夫|叙利亚 → 坦夫驻军；达夫拉|阿联酋 → 达夫拉空军基地；内瓦提姆|拉蒙|以色列 → 内瓦提姆/拉蒙等；赛利耶、巴林、科威特 等。
- **伊朗**：阿巴斯港、德黑兰、布什尔、伊斯法罕、纳坦兹、米纳布、霍尔木兹 等（seed 中需有对应 name 的伊朗据点）。

若 seed 里没有某据点，或 name 与关键词完全对不上（例如英文报道只写 "Al-Asad" 而 seed 只有「阿萨德空军基地」），规则里已含 asad/阿萨德，一般能匹配；若仍不匹配，可查 `key_location.name` 与 extractor_rules.py / extractor_dashscope 的 name_keywords 是否有一致子串。

### 验证命令

```bash
# 被标为遭袭的据点
curl -s http://localhost:3001/api/situation | jq '[.usForces.keyLocations[], .iranForces.keyLocations[]] | map(select(.status == "attacked")) | length'

# 表内 status / damage_level
sqlite3 server/data.db "SELECT side, name, status, damage_level FROM key_location WHERE status != 'operational' OR damage_level IS NOT NULL LIMIT 20"
```

### 常见问题

| 现象 | 可能原因 | 处理 |
|------|----------|------|
| 基地状态从不更新 | 1) 新闻未提及「某基地遭袭」类表述<br>2) 提取的 name_keywords 与 key_location.name 无法 LIKE 匹配 | 确认 seed 的 name 含中文/英文与提取器关键词一致；或扩展 extractor 的 name_keywords |
| 地图/基地面板无据点 | key_location 表空 | 先执行 `node server/seed.js` |

---

## 三、地图战区 / 冲突点 (gdelt_events + conflict_stats)

### 数据流

- **正常模式**：`fetch_gdelt_events()` 请求 GDELT API → 解析为事件列表 → `_write_to_db(events)` 写入 `gdelt_events` 和 `conflict_stats`（总事件数、高影响事件数、估计伤亡、打击次数等）。
- **GDELT 不可用**：设 `GDELT_DISABLED=1` 时，`fetch_news()` 里在流水线结束后调 `_rss_to_gdelt_fallback()`，用 **situation_update 最近 50 条** 按 summary 推断经纬度（`_infer_coords`）和 impact_score（由 severity 映射），写入 `gdelt_events`，这样地图仍有冲突点。

前端 WarMap 根据 `conflictEvents`（= gdelt_events）的 impact_score 分绿/橙/红三层显示；战损区「冲突统计」来自 `conflict_stats`。

### 验证命令

```bash
# 冲突点条数 + 冲突统计
curl -s http://localhost:3001/api/situation | jq '{ conflictEvents: (.conflictEvents | length), conflictStats: .conflictStats }'

# 表内
sqlite3 server/data.db "SELECT COUNT(*) FROM gdelt_events"
sqlite3 server/data.db "SELECT * FROM conflict_stats WHERE id = 1"
```

### 常见问题

| 现象 | 可能原因 | 处理 |
|------|----------|------|
| 地图没有冲突点 | 1) gdelt_events 表空<br>2) 未跑 gdelt 或 GDELT 被墙且未开 RSS 回填 | 跑 `npm run gdelt`；国内可设 `GDELT_DISABLED=1`，靠 situation_update 回填 |
| 冲突点不更新 | 爬虫未调 notify，或 Node/爬虫用的不是同一个 data.db | 确认 API_BASE、DB_PATH 一致；看 Node 终端是否有 `[crawler/notify] DB 已重载` |
| conflict_stats 全 0 | 从未成功写入过 gdelt_events（GDELT 与 RSS 回填都未执行） | 先让 gdelt_events 有数据（见上） |

---

## 四、一键检查（仅战损 / 基地 / 地图）

在项目根执行：

```bash
./scripts/debug-panels-focus.sh
```

会检查：API 是否通、`combat_losses` / `key_location` / `gdelt_events` / `conflict_stats` 行数及关键字段、并给出简短结论。需已启动 API（`npm run api`）；可选 `jq`、`sqlite3` 以输出更全。

详细逐板块说明见 `docs/DEBUG_PANELS.md`。
