Files
yangxiangyuan 6a7a2b089a feat: 新增免费演出雷达工具与定投评估功能,优化周报提醒逻辑
1. 新增免费演出雷达工具:含秀动/猫眼/大麦平台抓取、本地代理鉴权配置
2. 新增建行定投评估模块:四大指数PE-TTM估值与定投档位建议
3. 新增推送告警白名单过滤功能,限制推送指定指数告警
4. 重构周报首页提醒逻辑,移至服务端处理并减少前端请求
5. 新增多套调试探针脚本与单元测试用例
2026-08-04 17:33:14 +08:00

181 lines
7.8 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.
// 调试:验证 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) })