fix:增面

This commit is contained in:
Daniel
2026-03-03 22:42:21 +08:00
parent 09ec2e3a69
commit 86e50debec
13 changed files with 1486 additions and 0 deletions

View File

@@ -0,0 +1,140 @@
#!/usr/bin/env node
/**
* 检查爬虫写入的数据:条数 + 最近内容situation_update、news_content、gdelt_events
* 用法(项目根目录): node scripts/check-crawler-data.cjs
* 可选:先启动爬虫 npm run gdelt再启动 API 或直接运行本脚本读 DB
*/
const path = require('path')
const http = require('http')
const projectRoot = path.resolve(__dirname, '..')
process.chdir(projectRoot)
const db = require('../server/db')
const CRAWLER_URL = process.env.CRAWLER_URL || 'http://localhost:8000'
const SHOW_ROWS = 10
function fetchCrawlerStatus() {
return new Promise((resolve) => {
const url = new URL(`${CRAWLER_URL}/crawler/status`)
const req = http.request(
{ hostname: url.hostname, port: url.port || 80, path: url.pathname, method: 'GET', timeout: 3000 },
(res) => {
let body = ''
res.on('data', (c) => (body += c))
res.on('end', () => {
try {
resolve(JSON.parse(body))
} catch {
resolve(null)
}
})
}
)
req.on('error', () => resolve(null))
req.end()
})
}
async function run() {
console.log('========================================')
console.log('爬虫数据检查(条数 + 最近内容)')
console.log('========================================\n')
// ---------- 爬虫服务状态(可选)----------
const status = await fetchCrawlerStatus()
if (status) {
console.log('--- 爬虫服务状态 GET /crawler/status ---')
console.log(' db_path:', status.db_path)
console.log(' db_exists:', status.db_exists)
console.log(' situation_update_count:', status.situation_update_count)
console.log(' last_fetch_items:', status.last_fetch_items, '(本轮抓取条数)')
console.log(' last_fetch_inserted:', status.last_fetch_inserted, '(去重后新增)')
if (status.last_fetch_error) console.log(' last_fetch_error:', status.last_fetch_error)
console.log('')
} else {
console.log('--- 爬虫服务 ---')
console.log(' 未启动或不可达:', CRAWLER_URL)
console.log('')
}
// ---------- situation_update事件脉络看板「近期更新」----------
let situationUpdateRows = []
let situationUpdateCount = 0
try {
situationUpdateCount = db.prepare('SELECT COUNT(*) as c FROM situation_update').get().c
situationUpdateRows = db
.prepare(
'SELECT id, timestamp, category, summary, severity FROM situation_update ORDER BY timestamp DESC LIMIT ?'
)
.all(SHOW_ROWS)
} catch (e) {
console.log('situation_update 表读取失败:', e.message)
}
console.log('--- situation_update事件脉络---')
console.log(' 总条数:', situationUpdateCount)
if (situationUpdateRows.length > 0) {
console.log(' 最近', situationUpdateRows.length, '条:')
situationUpdateRows.forEach((r, i) => {
const summary = (r.summary || '').slice(0, 50)
console.log(` ${i + 1}. [${r.timestamp}] ${r.category}/${r.severity} ${summary}${summary.length >= 50 ? '…' : ''}`)
})
}
console.log('')
// ---------- news_content资讯表爬虫去重后写入----------
let newsCount = 0
let newsRows = []
try {
newsCount = db.prepare('SELECT COUNT(*) as c FROM news_content').get().c
newsRows = db
.prepare(
'SELECT title, summary, source, published_at, category, severity FROM news_content ORDER BY published_at DESC LIMIT ?'
)
.all(SHOW_ROWS)
} catch (e) {
console.log('news_content 表读取失败:', e.message)
}
console.log('--- news_content资讯表---')
console.log(' 总条数:', newsCount)
if (newsRows.length > 0) {
console.log(' 最近', newsRows.length, '条:')
newsRows.forEach((r, i) => {
const title = (r.title || '').slice(0, 45)
console.log(` ${i + 1}. [${r.published_at || ''}] ${r.source || ''} ${title}${title.length >= 45 ? '…' : ''}`)
if (r.summary) console.log(` summary: ${(r.summary || '').slice(0, 60)}`)
})
}
console.log('')
// ---------- gdelt_events地图冲突点----------
let gdeltCount = 0
let gdeltRows = []
try {
gdeltCount = db.prepare('SELECT COUNT(*) as c FROM gdelt_events').get().c
gdeltRows = db
.prepare('SELECT event_id, event_time, title, impact_score FROM gdelt_events ORDER BY event_time DESC LIMIT 5')
.all()
} catch (e) {
console.log('gdelt_events 表读取失败:', e.message)
}
console.log('--- gdelt_events地图冲突点---')
console.log(' 总条数:', gdeltCount)
if (gdeltRows.length > 0) {
console.log(' 最近 5 条:')
gdeltRows.forEach((r, i) => {
const title = (r.title || '').slice(0, 50)
console.log(` ${i + 1}. [${r.event_time}] impact=${r.impact_score} ${title}${title.length >= 50 ? '…' : ''}`)
})
}
console.log('========================================')
}
db.initDb().then(() => run()).catch((err) => {
console.error('失败:', err.message)
process.exit(1)
})

78
scripts/debug-panels-focus.sh Executable file
View File

@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# 仅检查:战损、基地、地图战区 三块数据
# 用法: ./scripts/debug-panels-focus.sh
set -e
API_URL="${API_URL:-http://localhost:3001}"
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
DB_PATH="${DB_PATH:-$PROJECT_ROOT/server/data.db}"
echo "=========================================="
echo "战损 / 基地 / 地图战区 — 数据检查"
echo "API: $API_URL | DB: $DB_PATH"
echo "=========================================="
echo ""
# ---------- API 连通 ----------
if ! curl -sf "$API_URL/api/health" >/dev/null 2>&1; then
echo "✗ API 无响应,请先运行: npm run api"
exit 1
fi
echo "✓ API 正常"
echo ""
SIT=$(curl -sf "$API_URL/api/situation" 2>/dev/null || echo "{}")
# ---------- 1. 战损 ----------
echo "[1] 战损 (combat_losses)"
if command -v jq &>/dev/null; then
us_k=$(echo "$SIT" | jq -r '.usForces.combatLosses.personnelCasualties.killed // "?"')
us_w=$(echo "$SIT" | jq -r '.usForces.combatLosses.personnelCasualties.wounded // "?"')
ir_k=$(echo "$SIT" | jq -r '.iranForces.combatLosses.personnelCasualties.killed // "?"')
ir_w=$(echo "$SIT" | jq -r '.iranForces.combatLosses.personnelCasualties.wounded // "?"')
echo " 美军 阵亡=$us_k 受伤=$us_w | 伊朗 阵亡=$ir_k 受伤=$ir_w"
echo " conflictStats: $(echo "$SIT" | jq -c '.conflictStats')"
else
echo " (安装 jq 可显示详细数字)"
fi
if [[ -f "$DB_PATH" ]] && command -v sqlite3 &>/dev/null; then
echo " 表 combat_losses:"
sqlite3 "$DB_PATH" "SELECT side, personnel_killed, personnel_wounded, bases_destroyed, bases_damaged FROM combat_losses" 2>/dev/null | while read -r line; do echo " $line"; done
fi
echo " 数据来源: seed 初始;爬虫从新闻提取 combat_losses_delta 后 db_merge 增量叠加。不更新→检查是否跑 gdelt、提取器是否输出、新闻是否含伤亡数字。"
echo ""
# ---------- 2. 基地 ----------
echo "[2] 基地 (key_location)"
if command -v jq &>/dev/null; then
us_loc=$(echo "$SIT" | jq -r '.usForces.keyLocations | length')
ir_loc=$(echo "$SIT" | jq -r '.iranForces.keyLocations | length')
us_attacked=$(echo "$SIT" | jq -r '[.usForces.keyLocations[] | select(.status == "attacked")] | length')
ir_attacked=$(echo "$SIT" | jq -r '[.iranForces.keyLocations[] | select(.status == "attacked")] | length')
echo " 美军 据点=$us_loc 遭袭=$us_attacked | 伊朗 据点=$ir_loc 遭袭=$ir_attacked"
fi
if [[ -f "$DB_PATH" ]] && command -v sqlite3 &>/dev/null; then
echo " 表 key_location 遭袭/有损伤的:"
sqlite3 "$DB_PATH" "SELECT side, name, status, damage_level FROM key_location WHERE status != 'operational' OR damage_level IS NOT NULL LIMIT 10" 2>/dev/null | while read -r line; do echo " $line"; done
fi
echo " 数据来源: seed 写入全部据点;爬虫只更新 status/damage_level需 name_keywords 与 name LIKE 匹配。不更新→检查新闻是否提基地遭袭、关键词与 seed name 是否一致。"
echo ""
# ---------- 3. 地图战区 ----------
echo "[3] 地图战区 (gdelt_events + conflict_stats)"
if command -v jq &>/dev/null; then
ev_cnt=$(echo "$SIT" | jq -r '.conflictEvents | length')
echo " conflictEvents 条数: $ev_cnt"
echo " conflictStats: $(echo "$SIT" | jq -c '.conflictStats')"
fi
if [[ -f "$DB_PATH" ]] && command -v sqlite3 &>/dev/null; then
n_ev=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM gdelt_events" 2>/dev/null || echo "0")
echo " 表 gdelt_events 行数: $n_ev"
sqlite3 "$DB_PATH" "SELECT total_events, high_impact_events, estimated_casualties, estimated_strike_count FROM conflict_stats WHERE id = 1" 2>/dev/null | while read -r line; do echo " conflict_stats: $line"; done
fi
echo " 数据来源: GDELT API 写入;或 GDELT_DISABLED=1 时由 situation_update 回填。无点→跑 gdelt 或开启 RSS 回填。"
echo ""
echo "=========================================="
echo "详细说明与排查顺序见: docs/DEBUG_战损_基地_地图.md"
echo "=========================================="

83
scripts/debug-panels.sh Executable file
View File

@@ -0,0 +1,83 @@
#!/usr/bin/env bash
# 看板板块数据快速检查:各表/API 与板块对应关系,便于逐项 debug
# 用法: ./scripts/debug-panels.sh
# 依赖: curl可选 jq、sqlite3 以输出更清晰
set -e
API_URL="${API_URL:-http://localhost:3001}"
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
DB_PATH="${DB_PATH:-$PROJECT_ROOT/server/data.db}"
echo "=========================================="
echo "看板板块数据检查 (DEBUG_PANELS)"
echo "API: $API_URL | DB: $DB_PATH"
echo "=========================================="
echo ""
# ---------- 1. API 健康与态势摘要 ----------
echo "[1] API 与态势摘要"
if ! curl -sf "$API_URL/api/health" >/dev/null 2>&1; then
echo " ✗ API 无响应,请先运行: npm run api"
echo " 后续表检查将跳过(依赖 API 或直接读 DB"
else
echo " ✓ API 正常"
SIT=$(curl -sf "$API_URL/api/situation" 2>/dev/null || echo "{}")
if command -v jq &>/dev/null; then
echo " lastUpdated: $(echo "$SIT" | jq -r '.lastUpdated // "?"')"
echo " recentUpdates: $(echo "$SIT" | jq -r '.recentUpdates | length') 条 → 事件脉络"
echo " conflictEvents: $(echo "$SIT" | jq -r '.conflictEvents | length') 条 → 地图冲突点"
echo " us powerIndex: $(echo "$SIT" | jq -r '.usForces.powerIndex.overall') → 顶栏/战力图"
echo " iran powerIndex: $(echo "$SIT" | jq -r '.iranForces.powerIndex.overall')"
echo " us keyLocations: $(echo "$SIT" | jq -r '.usForces.keyLocations | length') 条 → 美国基地/地图"
echo " iran keyLocations: $(echo "$SIT" | jq -r '.iranForces.keyLocations | length') 条 → 伊朗基地/地图"
echo " us combatLosses: killed=$(echo "$SIT" | jq -r '.usForces.combatLosses.personnelCasualties.killed') wounded=$(echo "$SIT" | jq -r '.usForces.combatLosses.personnelCasualties.wounded')"
echo " wallStreet points: $(echo "$SIT" | jq -r '.usForces.wallStreetInvestmentTrend | length') → 华尔街图"
echo " retaliation: $(echo "$SIT" | jq -r '.iranForces.retaliationSentiment') (history: $(echo "$SIT" | jq -r '.iranForces.retaliationSentimentHistory | length') 条)"
else
echo " (安装 jq 可显示详细字段) 态势已拉取,长度: ${#SIT}"
fi
fi
echo ""
# ---------- 2. 各表行数(直接读 DB----------
echo "[2] 数据库表行数(与板块对应)"
if ! [[ -f "$DB_PATH" ]]; then
echo " ✗ 数据库文件不存在: $DB_PATH"
echo " 请先 seed: node server/seed.js 或 启动 API 后由 initDb 创建"
elif ! command -v sqlite3 &>/dev/null; then
echo " (未安装 sqlite3跳过表统计。可安装后重试)"
else
TABLES="force_summary power_index force_asset key_location combat_losses wall_street_trend retaliation_current retaliation_history situation_update situation gdelt_events conflict_stats news_content"
for t in $TABLES; do
n=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM $t" 2>/dev/null || echo "?")
case "$t" in
force_summary) desc="力量摘要(美/伊)" ;;
power_index) desc="战力指数 → 顶栏/战力图" ;;
force_asset) desc="资产列表 → 左右侧摘要" ;;
key_location) desc="据点 → 地图/美伊基地面板" ;;
combat_losses) desc="战损 → 战损面板" ;;
wall_street_trend) desc="华尔街趋势图" ;;
retaliation_current) desc="报复当前值" ;;
retaliation_history) desc="报复历史 → 仪表盘" ;;
situation_update) desc="事件脉络 → 时间线" ;;
situation) desc="updated_at → 顶栏时间" ;;
gdelt_events) desc="冲突点 → 地图图层" ;;
conflict_stats) desc="冲突统计 → 战损区" ;;
news_content) desc="资讯表 → /api/news" ;;
*) desc="" ;;
esac
printf " %-22s %6s %s\n" "$t" "$n" "$desc"
done
fi
echo ""
# ---------- 3. 板块健康简要判断 ----------
echo "[3] 板块数据来源与可能问题"
echo " • 仅 seed、爬虫不写: force_summary, power_index, force_asset"
echo " • 爬虫可更新: situation_update(事件脉络), key_location(基地状态), combat_losses(战损), retaliation_*, wall_street_trend, gdelt_events"
echo " • 事件脉络不更新 → 检查爬虫是否启动、是否调用 POST /api/crawler/notify"
echo " • 战损/基地不更新 → 检查是否跑 npm run gdelt、提取器是否输出、新闻是否含相关表述"
echo " • 地图无冲突点 → 检查 gdelt_events 是否有数据、GDELT 或 RSS 回填是否执行"
echo ""
echo "详细逐板块说明见: docs/DEBUG_PANELS.md"
echo "=========================================="

17
scripts/run-crawler-range.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# 按时间范围跑一轮爬虫RSS仅保留指定起始时间之后的条目
# 用法:
# ./scripts/run-crawler-range.sh # 默认从 2026-02-28 0:00 到现在
# ./scripts/run-crawler-range.sh 2026-02-25T00:00:00
#
# GDELT 时间范围需在启动 gdelt 服务时设置,例如:
# GDELT_TIMESPAN=3d npm run gdelt
set -e
START="${1:-2026-02-28T00:00:00}"
cd "$(dirname "$0")/.."
echo "RSS 抓取时间范围: 仅保留 ${START} 之后"
echo "运行: cd crawler && CRAWL_START_DATE=${START} python run_once.py"
echo ""
export CRAWL_START_DATE="$START"
(cd crawler && python3 run_once.py)

81
scripts/verify-panels.cjs Normal file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env node
/**
* 代码层执行看板验证:直接调用 getSituation() 与 DB输出战损 / 基地 / 地图战区 结果。
* 用法(项目根目录): node scripts/verify-panels.cjs
*/
const path = require('path')
const projectRoot = path.resolve(__dirname, '..')
process.chdir(projectRoot)
const db = require('../server/db')
const { getSituation } = require('../server/situationData')
function run() {
const s = getSituation()
console.log('========================================')
console.log('看板数据验证(与 API getSituation 一致)')
console.log('========================================\n')
console.log('lastUpdated:', s.lastUpdated)
console.log('')
// ---------- 1. 战损 ----------
console.log('--- [1] 战损 combat_losses ---')
const us = s.usForces.combatLosses
const ir = s.iranForces.combatLosses
console.log('美军 阵亡:', us.personnelCasualties.killed, '受伤:', us.personnelCasualties.wounded)
console.log('美军 基地毁/损:', us.bases.destroyed, '/', us.bases.damaged)
console.log('美军 战机/舰艇/装甲/车辆:', us.aircraft, us.warships, us.armor, us.vehicles)
console.log('伊朗 阵亡:', ir.personnelCasualties.killed, '受伤:', ir.personnelCasualties.wounded)
console.log('伊朗 基地毁/损:', ir.bases.destroyed, '/', ir.bases.damaged)
console.log('平民合计 killed/wounded:', s.civilianCasualtiesTotal.killed, s.civilianCasualtiesTotal.wounded)
console.log('conflictStats:', JSON.stringify(s.conflictStats))
console.log('')
// ---------- 2. 基地(与看板口径一致:美军仅 type===Base伊朗为 Base/Port/Nuclear/Missile----------
console.log('--- [2] 基地 key_location ---')
const usLoc = s.usForces.keyLocations || []
const irLoc = s.iranForces.keyLocations || []
const usBases = usLoc.filter((l) => l.type === 'Base')
const irBases = irLoc.filter((l) => ['Base', 'Port', 'Nuclear', 'Missile'].includes(l.type))
const usAttacked = usBases.filter((l) => l.status === 'attacked')
const irAttacked = irBases.filter((l) => l.status === 'attacked')
console.log('美军 总基地数(仅Base):', usBases.length, '| 遭袭:', usAttacked.length, '(与看板「美军基地态势」一致)')
console.log('伊朗 总基地数(Base/Port/Nuclear/Missile):', irBases.length, '| 遭袭:', irAttacked.length, '(与看板「伊朗基地态势」一致)')
if (usAttacked.length > 0) {
console.log('美军遭袭示例:', usAttacked.slice(0, 3).map((l) => `${l.name}(${l.status},damage=${l.damage_level})`).join(', '))
}
if (irAttacked.length > 0) {
console.log('伊朗遭袭示例:', irAttacked.slice(0, 3).map((l) => `${l.name}(${l.status},damage=${l.damage_level})`).join(', '))
}
console.log('')
// ---------- 3. 地图战区 ----------
console.log('--- [3] 地图战区 gdelt_events + conflict_stats ---')
const events = s.conflictEvents || []
console.log('conflictEvents 条数:', events.length)
console.log('conflictStats:', JSON.stringify(s.conflictStats))
if (events.length > 0) {
console.log('最近 3 条:', events.slice(0, 3).map((e) => `${e.event_time} ${(e.title || '').slice(0, 40)} impact=${e.impact_score}`))
}
console.log('')
// ---------- 附加:事件脉络 ----------
const updates = s.recentUpdates || []
console.log('--- [附] 事件脉络 situation_update ---')
console.log('recentUpdates 条数:', updates.length)
if (updates.length > 0) {
console.log('最新 1 条:', updates[0].timestamp, (updates[0].summary || '').slice(0, 50))
}
console.log('========================================')
}
db
.initDb()
.then(() => run())
.catch((err) => {
console.error('验证失败:', err.message)
process.exit(1)
})