Files

188 lines
7.4 KiB
JavaScript
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.
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}&current=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