feat: 初始提交(仅核心代码,已排除大文件)

This commit is contained in:
yangxiangyuan
2026-07-08 10:29:50 +08:00
commit be7c5842f1
623 changed files with 332897 additions and 0 deletions
@@ -0,0 +1,82 @@
{
"name_comment": "显示的名",
"name": "道琼斯工业指",
"card_order": 13.5,
"card_type": "overseas",
"enabled_comment": "是否启用该监",
"enabled": true,
"display_comment": "是否显示该卡",
"display": true,
"schedule_comment": "刷新频率 (",
"schedule": "10m",
"symbol_comment": "监控的代",
"symbol": "usDJI",
"url_comment": "点击卡片右上角房子图标跳转的地址",
"url": "https://gu.qq.com/usDJI",
"source_comment": "数据来源 (",
"source": "yahoo_probe",
"alerts_comment": "预警阈值配",
"alerts": {
"change_percent_down_comment": "跌幅预警阈",
"change_percent_down": -2,
"urgent_abs_change_percent_comment": "严重告警阈",
"urgent_abs_change_percent": 5,
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
"urgent_cooldown_minutes": 120,
"change_percent_up_comment": "涨幅预警阈",
"change_percent_up": 2
},
"email_comment": "邮件通知配置",
"email": {
"enabled_comment": "是否启用邮件通知",
"enabled": true,
"from_comment": "发件人邮",
"from": "yangdafe@gmail.com",
"to_comment": "收件人邮箱列",
"to": [
"yangxiangyuan@umer.com.cn"
],
"smtp_comment": "SMTP 服务器配",
"smtp": {
"host": "smtp.gmail.com",
"port": 465,
"secure": true,
"auth": {
"user": "yangdafe@gmail.com",
"pass": "{{GMAIL_SMTP_PASS}}"
}
}
},
"email_reminder_enabled_comment": "是否发送邮件提",
"email_reminder_enabled": true,
"calendar_reminder_enabled_comment": "是否写入日历提醒",
"calendar_reminder_enabled": true,
"push_targets_comment": "推送目标列",
"push_targets": [
{
"url_comment": "推送接口地址",
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
"enabled_comment": "是否启用",
"enabled": true,
"key_comment": "Weekly 工具",
"key": ""
},
{
"url_comment": "推送接口地址",
"url": "http://localhost:{{PORT}}/api/calendar/events",
"enabled_comment": "是否启用",
"enabled": true
}
],
"market_status": {
"timezone": "America/New_York",
"sessions": [
{
"start": "09:30",
"end": "16:00"
}
],
"display_label": "道琼"
},
"market_status_comment": "Defines the source market timezone and trading sessions for status display."
}
+121
View File
@@ -0,0 +1,121 @@
const iconv = require('iconv-lite')
const { fetchYahoo } = require('../../marketsClient')
const lastAlertTimes = new Map()
const parseTencentLine = (line) => {
const m = line.match(/^v_(\w+)="([^"]*)/)
if (!m) return null
const symbol = m[1]
const body = m[2]
let parts = body.split('~')
if (parts.length < 3 && body.includes(',')) parts = body.split(',')
const name = parts[1]
const price = parseFloat(parts[3])
const prevClose = parseFloat(parts[4])
if (isNaN(price) || isNaN(prevClose)) return null
const change = price - prevClose
const pct = prevClose ? (change / prevClose) * 100 : 0
let captured_at = parts[30] || ''
if (captured_at.length === 14 && !captured_at.includes('-')) {
captured_at = captured_at.replace(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/, '$1-$2-$3 $4:$5:$6')
}
return { symbol, name, price, prevClose, change, change_percent: pct, captured_at }
}
const fetchTencent = async (axios, symbols) => {
if (!symbols || symbols.length === 0) return []
const url = `http://qt.gtimg.cn/q=${symbols.join(',')}`
try {
const res = await axios.get(url, { responseType: 'arraybuffer', timeout: 10000, validateStatus: () => true })
if (!res || res.status >= 400) return []
const text = iconv.decode(Buffer.from(res.data), 'gb18030')
const lines = String(text || '').split(/;\s*/).filter(Boolean)
const out = []
for (const line of lines) {
const r = parseTencentLine(line)
if (r) out.push({ ...r, source: 'tencent' })
}
return out
} catch {
return []
}
}
const checkAlerts = (data, config) => {
const alerts = []
const addAlert = (key, title, message, level = 'warning') => {
const d = new Date()
const utc = d.getTime() + (d.getTimezoneOffset() * 60000)
const bjDate = new Date(utc + (3600000 * 8))
const year = bjDate.getFullYear()
const month = String(bjDate.getMonth() + 1).padStart(2, '0')
const day = String(bjDate.getDate()).padStart(2, '0')
const hour = bjDate.getHours()
const slot = hour >= 0 && hour < 12 ? 'am' : 'pm'
const currentSlotKey = `${year}-${month}-${day}-${slot}`
const lastSlotKey = lastAlertTimes.get(key)
if (lastSlotKey !== currentSlotKey) {
alerts.push({ id: key, title, message, level })
lastAlertTimes.set(key, currentSlotKey)
}
}
if (config.alerts.change_percent_down && data.change_percent <= config.alerts.change_percent_down) {
const currentDrop = Math.floor(Math.abs(data.change_percent))
const key = `drop_${currentDrop}`
if (currentDrop >= Math.abs(config.alerts.change_percent_down)) {
addAlert(key, `${config.name}跌幅预警`, `当前跌幅 ${data.change_percent.toFixed(2)}% (>= ${currentDrop}%)`)
}
}
if (config.alerts.change_percent_up && data.change_percent >= config.alerts.change_percent_up) {
const currentRise = Math.floor(data.change_percent)
const key = `rise_${currentRise}`
if (currentRise >= config.alerts.change_percent_up) {
addAlert(key, `${config.name}涨幅预警`, `当前涨幅 ${data.change_percent.toFixed(2)}% (>= ${currentRise}%)`)
}
}
return alerts
}
module.exports = {
run: async (context, config) => {
const { axios, logJSON } = context
let item = null
if (String(config.source || '').toLowerCase() === 'yahoo_probe') {
const quotes = await fetchYahoo([config.symbol])
item = quotes.find(q => q.symbol === config.symbol)
} else {
const quotes = await fetchTencent(axios, [config.symbol])
item = quotes.find(q => q.symbol === config.symbol)
}
// Fallback: try Tencent if Yahoo failed, or vice versa
if (!item) {
if (String(config.source || '').toLowerCase() === 'yahoo_probe') {
const fb = await fetchTencent(axios, [config.symbol])
item = fb.find(q => q.symbol === config.symbol)
} else {
const fb = await fetchYahoo([config.symbol])
item = fb.find(q => q.symbol === config.symbol)
}
}
if (!item) {
logJSON('skill.fetch.error', { skill: config.name, message: 'Data not found' }, 'market_skills')
return null
}
const data = [{
label: config.name,
symbol: config.symbol,
source: config.source,
name: item.name,
price: item.price,
prevClose: item.prevClose,
change: item.change,
change_percent: item.change_percent,
captured_at: item.captured_at,
market_status: config.market_status
}]
const alerts = checkAlerts(item, config)
return { data, alerts }
}
}