feat: 新增免费演出雷达工具与定投评估功能,优化周报提醒逻辑
1. 新增免费演出雷达工具:含秀动/猫眼/大麦平台抓取、本地代理鉴权配置 2. 新增建行定投评估模块:四大指数PE-TTM估值与定投档位建议 3. 新增推送告警白名单过滤功能,限制推送指定指数告警 4. 重构周报首页提醒逻辑,移至服务端处理并减少前端请求 5. 新增多套调试探针脚本与单元测试用例
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
// 调试:验证 free_show_local_agent v2 的 CDP + 手写 WebSocket 管道
|
||||
// 仅用 about:blank 等无害 URL,不访问大麦(保护服务器 IP)
|
||||
// 用法:node dev_test_scripts/debug/debug_agent_cdp.js
|
||||
'use strict'
|
||||
const http = require('http')
|
||||
const net = require('net')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const crypto = require('crypto')
|
||||
const { spawn } = require('child_process')
|
||||
|
||||
const CDP_PORT = 9322 // 避开代理默认 9222
|
||||
const PROFILE = path.join(__dirname, '.edge_debug_profile')
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
||||
|
||||
function findBrowser() {
|
||||
const candidates = [
|
||||
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
||||
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
|
||||
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
|
||||
]
|
||||
for (const p of candidates) if (fs.existsSync(p)) return p
|
||||
return null
|
||||
}
|
||||
|
||||
function cdpHttp(method, p) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request({ host: '127.0.0.1', port: CDP_PORT, path: p, method, timeout: 5000 }, res => {
|
||||
let body = ''
|
||||
res.on('data', c => { body += c })
|
||||
res.on('end', () => { try { resolve(JSON.parse(body)) } catch (e) { reject(new Error('parse fail')) } })
|
||||
})
|
||||
req.on('timeout', () => req.destroy(new Error('timeout')))
|
||||
req.on('error', reject)
|
||||
req.end()
|
||||
})
|
||||
}
|
||||
|
||||
// ---- 与代理同款最小 WS 客户端(复制自 free_show_local_agent.js) ----
|
||||
function parseWsFrame(buf) {
|
||||
if (buf.length < 2) return null
|
||||
const fin = (buf[0] & 0x80) !== 0
|
||||
const opcode = buf[0] & 0x0f
|
||||
const masked = (buf[1] & 0x80) !== 0
|
||||
let len = buf[1] & 0x7f
|
||||
let off = 2
|
||||
if (len === 126) { if (buf.length < 4) return null; len = buf.readUInt16BE(2); off = 4 }
|
||||
else if (len === 127) { if (buf.length < 10) return null; len = Number(buf.readBigUInt64BE(2)); off = 10 }
|
||||
let mask = null
|
||||
if (masked) { if (buf.length < off + 4) return null; mask = buf.slice(off, off + 4); off += 4 }
|
||||
if (buf.length < off + len) return null
|
||||
let payload = buf.slice(off, off + len)
|
||||
if (mask) { payload = Buffer.from(payload); for (let i = 0; i < payload.length; i++) payload[i] ^= mask[i % 4] }
|
||||
return { fin, opcode, payload, total: off + len }
|
||||
}
|
||||
function encodeWsFrame(text) {
|
||||
const payload = Buffer.from(text, 'utf8')
|
||||
const len = payload.length
|
||||
let header
|
||||
if (len < 126) header = Buffer.from([0x81, 0x80 | len])
|
||||
else if (len < 65536) { header = Buffer.from([0x81, 0x80 | 126, 0, 0]); header.writeUInt16BE(len, 2) }
|
||||
else { header = Buffer.from([0x81, 0x80 | 127, 0, 0, 0, 0, 0, 0, 0, 0]); header.writeBigUInt64BE(BigInt(len), 2) }
|
||||
const mask = crypto.randomBytes(4)
|
||||
const masked = Buffer.from(payload)
|
||||
for (let i = 0; i < masked.length; i++) masked[i] ^= mask[i % 4]
|
||||
return Buffer.concat([header, mask, masked])
|
||||
}
|
||||
function wsConnect(wsUrl) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const u = new URL(wsUrl)
|
||||
const sock = net.connect(Number(u.port) || 80, u.hostname || '127.0.0.1')
|
||||
const key = crypto.randomBytes(16).toString('base64')
|
||||
const handshake = ['GET ' + (u.pathname || '/') + (u.search || '') + ' HTTP/1.1', 'Host: ' + u.host, 'Upgrade: websocket', 'Connection: Upgrade', 'Sec-WebSocket-Key: ' + key, 'Sec-WebSocket-Version: 13', '', ''].join('\r\n')
|
||||
let upgraded = false
|
||||
let buf = Buffer.alloc(0)
|
||||
let fragBuf = null
|
||||
const handlers = { message: null, close: null }
|
||||
const client = {
|
||||
send(t) { try { sock.write(encodeWsFrame(t)) } catch (e) {} },
|
||||
close() { try { sock.end() } catch (e) {} },
|
||||
onMessage(fn) { handlers.message = fn },
|
||||
onClose(fn) { handlers.close = fn }
|
||||
}
|
||||
sock.on('connect', () => sock.write(handshake))
|
||||
sock.on('data', chunk => {
|
||||
buf = Buffer.concat([buf, chunk])
|
||||
if (!upgraded) {
|
||||
const idx = buf.indexOf('\r\n\r\n')
|
||||
if (idx < 0) return
|
||||
if (!/^HTTP\/1\.1\s+101/.test(buf.slice(0, idx).toString())) { reject(new Error('握手失败')); sock.end(); return }
|
||||
upgraded = true
|
||||
buf = buf.slice(idx + 4)
|
||||
resolve(client)
|
||||
}
|
||||
while (true) {
|
||||
const frame = parseWsFrame(buf)
|
||||
if (!frame) break
|
||||
buf = buf.slice(frame.total)
|
||||
if (frame.opcode === 0x8) { if (handlers.close) handlers.close(); return }
|
||||
if (frame.opcode === 0x9) { sock.write(Buffer.from([0x8a, 0x80, 0, 0, 0, 0])); continue }
|
||||
if (frame.opcode === 0xA) continue
|
||||
if (frame.opcode === 0x1 || frame.opcode === 0x2) {
|
||||
if (frame.fin) { const full = fragBuf ? Buffer.concat([fragBuf, frame.payload]) : frame.payload; fragBuf = null; if (handlers.message) handlers.message(full.toString('utf8')) }
|
||||
else fragBuf = frame.payload
|
||||
} else if (frame.opcode === 0x0) {
|
||||
fragBuf = fragBuf ? Buffer.concat([fragBuf, frame.payload]) : frame.payload
|
||||
if (frame.fin) { const full = fragBuf; fragBuf = null; if (handlers.message) handlers.message(full.toString('utf8')) }
|
||||
}
|
||||
}
|
||||
})
|
||||
sock.on('error', e => { if (!upgraded) reject(e); else if (handlers.close) handlers.close() })
|
||||
sock.on('close', () => { if (handlers.close) handlers.close() })
|
||||
})
|
||||
}
|
||||
function makeCdp(ws) {
|
||||
let nextId = 1
|
||||
const pending = new Map()
|
||||
ws.onMessage(text => {
|
||||
let msg = null
|
||||
try { msg = JSON.parse(text) } catch (e) { return }
|
||||
if (msg && msg.id && pending.has(msg.id)) { const cb = pending.get(msg.id); pending.delete(msg.id); cb(msg) }
|
||||
})
|
||||
return {
|
||||
send(method, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = nextId++
|
||||
const timer = setTimeout(() => { if (pending.has(id)) { pending.delete(id); reject(new Error('CDP 超时: ' + method)) } }, 15000)
|
||||
pending.set(id, msg => { clearTimeout(timer); resolve(msg) })
|
||||
ws.send(JSON.stringify({ id, method, params: params || {} }))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = findBrowser()
|
||||
if (!browser) { console.log('FAIL: 未找到浏览器'); return }
|
||||
console.log('浏览器:', browser)
|
||||
const child = spawn(browser, [
|
||||
'--headless=new', '--disable-gpu',
|
||||
'--remote-debugging-port=' + CDP_PORT,
|
||||
'--user-data-dir=' + PROFILE,
|
||||
'--disable-blink-features=AutomationControlled',
|
||||
'--exclude-switches=enable-automation',
|
||||
'--no-first-run', '--no-default-browser-check',
|
||||
'about:blank'
|
||||
], { detached: true, stdio: 'ignore' })
|
||||
child.unref()
|
||||
|
||||
let ver = null
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await sleep(500)
|
||||
ver = await cdpHttp('GET', '/json/version').catch(() => null)
|
||||
if (ver) break
|
||||
}
|
||||
if (!ver) { console.log('FAIL: CDP 未就绪'); try { child.kill() } catch (e) {}; return }
|
||||
console.log('PASS: /json/version =>', ver.Browser || ver['User-Agent'] || 'ok')
|
||||
|
||||
let tab = null
|
||||
try { tab = await cdpHttp('PUT', '/json/new?' + encodeURIComponent('about:blank')) }
|
||||
catch (e) { tab = await cdpHttp('GET', '/json/new?' + encodeURIComponent('about:blank')).catch(() => null) }
|
||||
if (!tab || !tab.webSocketDebuggerUrl) { console.log('FAIL: 开标签失败'); try { child.kill() } catch (e) {}; return }
|
||||
console.log('PASS: /json/new =>', tab.id)
|
||||
|
||||
const ws = await wsConnect(tab.webSocketDebuggerUrl)
|
||||
const cdp = makeCdp(ws)
|
||||
const r1 = await cdp.send('Runtime.evaluate', { expression: '1+1', returnByValue: true })
|
||||
console.log('PASS: evaluate 1+1 =>', r1 && r1.result && r1.result.result ? r1.result.result.value : 'null')
|
||||
const r2 = await cdp.send('Runtime.evaluate', { expression: 'navigator.webdriver', returnByValue: true })
|
||||
console.log('INFO: navigator.webdriver =>', r2 && r2.result && r2.result.result ? r2.result.result.value : 'null')
|
||||
ws.close()
|
||||
await cdpHttp('GET', '/json/close/' + tab.id).catch(() => null)
|
||||
console.log('PASS: 标签已关闭')
|
||||
|
||||
try { child.kill() } catch (e) {}
|
||||
console.log('DONE')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch(e => { console.log('FAIL:', e.message || e); process.exit(1) })
|
||||
Reference in New Issue
Block a user