const axios = require('axios') const fs = require('fs') const path = require('path') const Database = require('better-sqlite3') const { logJSON } = require('./logger') const SH_LAT = 31.2304 const SH_LON = 121.4737 const fetchWeather = async (lat, lon) => { const latitude = Number(lat) || SH_LAT const longitude = Number(lon) || SH_LON try { const urlWeather = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,apparent_temperature,precipitation&hourly=precipitation_probability&daily=temperature_2m_max,temperature_2m_min&timezone=auto` const urlAir = `https://air-quality-api.open-meteo.com/v1/air-quality?latitude=${latitude}&longitude=${longitude}&hourly=us_aqi,pm2_5,european_aqi&timezone=auto` const [wRes, aRes] = await Promise.all([ axios.get(urlWeather, { timeout: 8000 }), axios.get(urlAir, { timeout: 8000 }) ]) const w = wRes.data || {} const a = aRes.data || {} const cur = (w.current || {}) let prob = null try { const times = (w.hourly && w.hourly.time) || [] const probs = (w.hourly && w.hourly.precipitation_probability) || [] const nowIso = new Date().toISOString().slice(0,13) const idx = times.findIndex(t => String(t).slice(0,13) === nowIso) if (idx >= 0) prob = probs[idx] } catch {} const hAqi = (a.hourly || {}) let tmin = null, tmax = null try { const dTimes = (w.daily && w.daily.time) || [] const dMin = (w.daily && w.daily.temperature_2m_min) || [] const dMax = (w.daily && w.daily.temperature_2m_max) || [] const today = new Date().toISOString().slice(0,10) const di = dTimes.findIndex(t => String(t).slice(0,10) === today) if (di >= 0) { tmin = dMin[di]; tmax = dMax[di] } } catch {} const info = { source: 'open-meteo', city: '上海', temperature: cur.temperature_2m, apparent_temperature: cur.apparent_temperature, precipitation: cur.precipitation, precipitation_probability: prob, us_aqi: hAqi.us_aqi ? hAqi.us_aqi[0] : undefined, european_aqi: hAqi.european_aqi ? hAqi.european_aqi[0] : undefined, pm2_5: hAqi.pm2_5 ? hAqi.pm2_5[0] : undefined, temperature_min: tmin, temperature_max: tmax } logJSON('wall.weather', { latitude, longitude, info }, 'wall') return info } catch (e) { logJSON('wall.weather.error', { error: String(e.message || e) }, 'wall') return { source: 'open-meteo', city: '上海', temperature: null, apparent_temperature: null, precipitation: null, precipitation_probability: null, us_aqi: null, pm2_5: null } } } module.exports = { fetchWeather } const cfgPath = path.join(process.cwd(), 'config', 'wall.json') if (!fs.existsSync(cfgPath)) fs.writeFileSync(cfgPath, JSON.stringify({ autoplay_ms: 45000, swipe_pause_ms: 5 * 60 * 1000, max_cards_on_screen: 2, background: { url: "", mode: "tile", size: 120, opacity: 0.08 }, cards: [ { title: '轻断食 · 吃什么', items: ['清汤/水', '清蒸蛋/少量蛋白', '蔬菜少油', '避免高糖与精制', '避免冷饮'], meta: '场景:晨间/午后 · 时长≤3秒读' }, { title: '冷水澡 · 准备步骤', items: ['热身2–3分钟', '先温水→冷水渐进', '重点避头部直冲', '备干毛巾与保暖', '注意心率与眩晕'], meta: '场景:沐浴前 · 安全优先' }, { title: '跑步 · 前置准备', items: ['热身动态拉伸', '检查鞋与袜', '补水少量', '路线与天气确认', '佩戴反光/夜跑灯'], meta: '场景:运动前 · 目标安全' } ] }, null, 2)) const getConfig = () => JSON.parse(fs.readFileSync(cfgPath, 'utf-8')) const setConfig = cfg => fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2)) module.exports.getConfig = getConfig module.exports.setConfig = setConfig const dataDir = path.join(process.cwd(), 'data') if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true }) const wallDbPath = path.join(dataDir, 'call.db') const wallDb = new Database(wallDbPath) wallDb.exec(`CREATE TABLE IF NOT EXISTS wall_cards ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, items_json TEXT NOT NULL, meta TEXT, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) );`) const countCardsRow = () => { try { const row = wallDb.prepare('SELECT COUNT(*) AS c FROM wall_cards').get() if (row && typeof row.c === 'number') return row.c return 0 } catch { return 0 } } const ensureInitialCardsFromConfig = () => { try { if (countCardsRow() > 0) return const cfg = getConfig() const cards = Array.isArray(cfg.cards) ? cfg.cards : [] if (!cards.length) return const insert = wallDb.prepare(`INSERT INTO wall_cards (title, items_json, meta) VALUES (@title, @items_json, @meta)`) const tx = wallDb.transaction((list) => { for (const c of list) { const items = Array.isArray(c.items) ? c.items : [] insert.run({ title: String(c.title || ''), items_json: JSON.stringify(items), meta: String(c.meta || '') }) } }) tx(cards) try { logJSON('wall.cards.migrate_from_config', { count: cards.length }, 'wall') } catch {} } catch (e) { try { logJSON('wall.cards.migrate_from_config.error', { error: String(e.message || e) }, 'wall') } catch {} } } ensureInitialCardsFromConfig() const listCardsStmt = wallDb.prepare(`SELECT id, title, items_json, meta, created_at, updated_at FROM wall_cards ORDER BY id ASC`) const insertCardStmt = wallDb.prepare(`INSERT INTO wall_cards (title, items_json, meta) VALUES (@title, @items_json, @meta)`) const updateCardStmt = wallDb.prepare(`UPDATE wall_cards SET title=@title, items_json=@items_json, meta=@meta, updated_at=datetime('now') WHERE id=@id`) const deleteCardStmt = wallDb.prepare(`DELETE FROM wall_cards WHERE id=?`) const normalizeItems = (items) => { if (Array.isArray(items)) return items.map(x => String(x || '')) const text = String(items || '') if (!text.trim()) return [] return text.split(/\r?\n/).map(s => s.trim()).filter(Boolean) } const listCards = () => { const rows = listCardsStmt.all() return rows.map(r => { let items = [] try { const parsed = JSON.parse(r.items_json || '[]') if (Array.isArray(parsed)) items = parsed.map(x => String(x || '')) } catch {} return { id: r.id, title: r.title || '', items, meta: r.meta || '', created_at: r.created_at, updated_at: r.updated_at } }) } const createCard = (payload) => { const items = normalizeItems(payload.items) const items_json = JSON.stringify(items) const title = String(payload.title || '') const meta = String(payload.meta || '') const info = insertCardStmt.run({ title, items_json, meta }) try { logJSON('wall.card.create', { id: info.lastInsertRowid, title }, 'wall') } catch {} return info.lastInsertRowid } const updateCardById = (id, payload) => { const items = normalizeItems(payload.items) const items_json = JSON.stringify(items) const title = String(payload.title || '') const meta = String(payload.meta || '') updateCardStmt.run({ id, title, items_json, meta }) try { logJSON('wall.card.update', { id, title }, 'wall') } catch {} } const deleteCardById = (id) => { deleteCardStmt.run(id) try { logJSON('wall.card.delete', { id }, 'wall') } catch {} } module.exports.listCards = listCards module.exports.createCard = createCard module.exports.updateCardById = updateCardById module.exports.deleteCardById = deleteCardById