feat: 新增免费演出雷达工具与定投评估功能,优化周报提醒逻辑

1. 新增免费演出雷达工具:含秀动/猫眼/大麦平台抓取、本地代理鉴权配置
2. 新增建行定投评估模块:四大指数PE-TTM估值与定投档位建议
3. 新增推送告警白名单过滤功能,限制推送指定指数告警
4. 重构周报首页提醒逻辑,移至服务端处理并减少前端请求
5. 新增多套调试探针脚本与单元测试用例
This commit is contained in:
yangxiangyuan
2026-08-04 17:33:14 +08:00
parent af5d0f43a5
commit 6a7a2b089a
25 changed files with 3499 additions and 121 deletions
@@ -0,0 +1,99 @@
// 调试:排查首页「每周周报提醒」的判定依据
// 读取最近 3 周(W30/W31/W32)周报的真实标题/内容,并模拟前端 isWeeklyMeaningfulReport 判定
// 用法:node dev_test_scripts/debug/debug_weekly_reminder.js
const path = require('path')
const fs = require('fs')
const os = require('os')
// 复刻 index.js 的 env 加载逻辑(凭据零硬编码)
const userProfile = process.env.USERPROFILE || process.env.HOME || os.homedir()
const candidates = [path.join(userProfile, 'Toolbox_local_creds.env.local')]
try {
const usersRoot = path.join(path.parse(userProfile).root, 'Users')
if (fs.existsSync(usersRoot)) {
fs.readdirSync(usersRoot).forEach(name => {
const dir = path.join(usersRoot, name)
if (dir !== userProfile && fs.existsSync(dir)) candidates.push(path.join(dir, 'Toolbox_local_creds.env.local'))
})
}
} catch {}
for (const p of candidates) {
if (!fs.existsSync(p)) continue
fs.readFileSync(p, 'utf-8').split(/\r?\n/).forEach(line => {
const t = line.trim()
if (!t || t.startsWith('#')) return
const i = t.indexOf('=')
if (i > 0) {
const k = t.slice(0, i).trim()
const v = t.slice(i + 1).trim()
if (!process.env[k]) process.env[k] = v
}
})
}
process.chdir(path.join(__dirname, '..', '..'))
// 按 weekly.js 的启动顺序初始化
const { initCrypto } = require('../../src/server/weekly_crypto')
const { initDB } = require('../../src/server/weekly_db')
const cfgPath = path.join(process.cwd(), 'config', 'weekly.json')
let machineSalt = 'salt'
try { machineSalt = JSON.parse(fs.readFileSync(cfgPath, 'utf-8')).machine_salt || 'salt' } catch {}
initCrypto(process.env.SYSTEM_AUTH_PASS_93220 || process.env.WEEKLY_PASSWORD || '', machineSalt)
initDB(path.join(process.cwd(), 'data', 'weekly.db'), path.join(process.cwd(), 'data', 'weekly.enc'))
const core = require('../../src/server/weekly_core')
// 与 public/index.js 的 isWeeklyAutoTitle / isWeeklyMeaningfulReport 保持一致
const TEMPLATE_TEXT = '' // 前端 getWeeklyTemplatePlainText 的纯文本,这里只做关键项判断
const isAutoTitle = (title, weekId) => {
const t = String(title || '').trim()
if (!t) return true
if (t.includes('(自动新增)')) return true
if (weekId && t === `${weekId} 周报`) return true
return false
}
const isMeaningful = (report, weekId) => {
if (!report) return { ok: false, reason: '无周报记录' }
const title = String(report.title || '').trim()
const content = String(report.content || '').trim()
if (!title || !content) return { ok: false, reason: '标题或内容为空' }
if (isAutoTitle(title, weekId)) return { ok: false, reason: '标题仍为自动标题(含「自动新增」或等于「<weekId> 周报」)' }
if (content === '这是自动新增的周报。你可以修改。') return { ok: false, reason: '内容仍为自动占位文本' }
return { ok: true, reason: '' }
}
for (const w of ['2026-W30', '2026-W31', '2026-W32']) {
const r = core.getReportByWeek(w)
if (!r) {
console.log(`\n[${w}] 无周报记录 => 提醒判定:缺`)
continue
}
const judge = isMeaningful(r, w)
console.log(`\n[${w}] id=${r.id} updated_at=${r.updated_at}`)
console.log(` 标题: ${JSON.stringify(r.title)}`)
console.log(` 内容前80字: ${JSON.stringify(String(r.content || '').replace(/\s+/g, ' ').slice(0, 80))}`)
console.log(` 内容长度: ${String(r.content || '').length}`)
console.log(` 提醒判定: ${judge.ok ? '已写(不算缺)' : '缺(' + judge.reason + ')'}`)
}
// ---- 追加排查:reports 全表时间线 + W31/W32 下的 events + 相关日志 ----
const { getDB } = require('../../src/server/weekly_db')
const { dec } = require('../../src/server/weekly_crypto')
const db = getDB()
console.log('\n===== reports 时间线(最近 10 条) =====')
db.prepare(`SELECT id, week_id, created_at, updated_at FROM reports ORDER BY id DESC LIMIT 10`).all()
.forEach(r => console.log(` id=${r.id} week=${r.week_id} created=${r.created_at} updated=${r.updated_at}`))
for (const rid of [149, 150]) {
console.log(`\n===== report ${rid} 下的 events =====`)
const evs = db.prepare(`SELECT id, title_enc, title_iv, title_tag, deadline, created_at, updated_at FROM events WHERE report_id=? ORDER BY id`).all(rid)
if (evs.length === 0) console.log(' (无 events)')
evs.forEach(e => console.log(` ev_id=${e.id} title=${JSON.stringify(dec(e.title_enc, e.title_iv, e.title_tag))} deadline=${e.deadline} created=${e.created_at} updated=${e.updated_at}`))
}
console.log('\n===== logs 中 fitness/ext 相关(最近 15 条) =====')
try {
db.prepare(`SELECT action, details, created_at FROM logs WHERE action LIKE '%ext%' OR action LIKE '%fitness%' ORDER BY id DESC LIMIT 15`).all()
.forEach(l => console.log(` ${l.created_at} ${l.action} ${String(l.details || '').slice(0, 120)}`))
} catch (e) { console.log(' logs 查询失败:', e.message) }