change v1

This commit is contained in:
lky-spec
2026-04-25 16:18:45 +08:00
parent 737f9bca95
commit c244e4cdf1
89 changed files with 17382 additions and 6198 deletions

View File

@@ -17,6 +17,7 @@ type Brain struct {
logger *slog.Logger
http *http.Client
stopCh chan struct{}
stopOnce sync.Once
recentSignals sync.Map // debounce
}
@@ -29,7 +30,11 @@ func NewBrain(agent *Agent, logger *slog.Logger) *Brain {
}
}
func (b *Brain) Stop() { close(b.stopCh) }
func (b *Brain) Stop() {
b.stopOnce.Do(func() {
close(b.stopCh)
})
}
// cleanStaleSignals removes debounce entries older than 30 minutes.
func (b *Brain) cleanStaleSignals() {
@@ -53,22 +58,26 @@ func (b *Brain) HandleSignal(sig Signal) {
emoji := map[string]string{"info": "", "warning": "⚠️", "critical": "🚨"}
e := emoji[sig.Severity]
if e == "" { e = "📊" }
if e == "" {
e = "📊"
}
b.agent.notifyAll(fmt.Sprintf("%s *%s*\n\n%s", e, sig.Title, sig.Detail))
}
func (b *Brain) StartNewsScan(interval time.Duration) {
seen := make(map[string]bool)
seenOrder := make([]string, 0, 1024)
safe.GoNamed("brain-news-scan", func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
cleanTick := 0
for {
select {
case <-b.stopCh: return
case <-b.stopCh:
return
case <-ticker.C:
b.scanNews(seen)
b.scanNews(seen, &seenOrder)
cleanTick++
if cleanTick%6 == 0 { // every ~30 min
b.cleanStaleSignals()
@@ -78,16 +87,20 @@ func (b *Brain) StartNewsScan(interval time.Duration) {
})
}
func (b *Brain) scanNews(seen map[string]bool) {
func (b *Brain) scanNews(seen map[string]bool, seenOrder *[]string) {
resp, err := b.http.Get("https://min-api.cryptocompare.com/data/v2/news/?lang=EN&sortOrder=latest")
if err != nil { return }
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b.logger.Debug("news API non-200", "status", resp.StatusCode)
return
}
body, err := safe.ReadAllLimited(resp.Body, 1024*1024) // 1MB limit
if err != nil { return }
if err != nil {
return
}
var result struct {
Data []struct {
@@ -99,39 +112,65 @@ func (b *Brain) scanNews(seen map[string]bool) {
PublishedOn int64 `json:"published_on"`
} `json:"Data"`
}
if err := json.Unmarshal(body, &result); err != nil { return }
if err := json.Unmarshal(body, &result); err != nil {
return
}
bullish := []string{"surge", "rally", "bullish", "breakout", "ath", "pump", "adoption"}
bearish := []string{"crash", "dump", "bearish", "sell-off", "plunge", "hack", "ban", "fraud"}
for _, d := range result.Data {
if seen[d.URL] { continue }
if seen[d.URL] {
continue
}
seen[d.URL] = true
if time.Since(time.Unix(d.PublishedOn, 0)) > 10*time.Minute { continue }
*seenOrder = append(*seenOrder, d.URL)
if time.Since(time.Unix(d.PublishedOn, 0)) > 10*time.Minute {
continue
}
lower := strings.ToLower(d.Title + " " + d.Body)
bc, brc := 0, 0
for _, w := range bullish { if strings.Contains(lower, w) { bc++ } }
for _, w := range bearish { if strings.Contains(lower, w) { brc++ } }
for _, w := range bullish {
if strings.Contains(lower, w) {
bc++
}
}
for _, w := range bearish {
if strings.Contains(lower, w) {
brc++
}
}
if bc == 0 && brc == 0 { continue }
if bc == 0 && brc == 0 {
continue
}
emoji := "📰"
sentiment := "NEUTRAL"
if bc > brc { emoji = "🟢"; sentiment = "BULLISH" }
if brc > bc { emoji = "🔴"; sentiment = "BEARISH" }
if bc > brc {
emoji = "🟢"
sentiment = "BULLISH"
}
if brc > bc {
emoji = "🔴"
sentiment = "BEARISH"
}
b.agent.notifyAll(fmt.Sprintf("%s *News*\n\n%s\n\n• Source: %s\n• Sentiment: %s",
emoji, d.Title, d.Source, sentiment))
}
// Evict ~half when seen map gets large (keep recent half to avoid re-notifying)
// Evict the oldest half when seen grows large so recent URLs stay deduped deterministically.
if len(seen) > 1000 {
i, half := 0, len(seen)/2
for k := range seen {
if i >= half { break }
delete(seen, k)
i++
half := len(seen) / 2
for i := 0; i < half && i < len(*seenOrder); i++ {
delete(seen, (*seenOrder)[i])
}
if half < len(*seenOrder) {
*seenOrder = append((*seenOrder)[:0], (*seenOrder)[half:]...)
} else {
*seenOrder = (*seenOrder)[:0]
}
}
}
@@ -143,7 +182,8 @@ func (b *Brain) StartMarketBriefs(hours []int) {
sent := make(map[string]bool)
for {
select {
case <-b.stopCh: return
case <-b.stopCh:
return
case now := <-ticker.C:
key := now.Format("2006-01-02-15")
for _, h := range hours {
@@ -159,21 +199,35 @@ func (b *Brain) StartMarketBriefs(hours []int) {
func (b *Brain) sendBrief(hour int) {
title := "☀️ *早间市场简报*"
if hour >= 18 { title = "🌙 *晚间市场简报*" }
if hour >= 18 {
title = "🌙 *晚间市场简报*"
}
// Fetch BTC/ETH prices for the brief
var btcPrice, ethPrice, btcChg, ethChg string
for _, sym := range []string{"BTCUSDT", "ETHUSDT"} {
resp, err := b.http.Get(fmt.Sprintf("https://fapi.binance.com/fapi/v1/ticker/24hr?symbol=%s", sym))
if err != nil { continue }
if err != nil {
continue
}
body, readErr := safe.ReadAllLimited(resp.Body, 64*1024) // 64KB limit
statusOK := resp.StatusCode == http.StatusOK
resp.Body.Close()
if readErr != nil || !statusOK { continue }
if readErr != nil || !statusOK {
continue
}
var t map[string]string
if err := json.Unmarshal(body, &t); err != nil { continue }
if sym == "BTCUSDT" { btcPrice = t["lastPrice"]; btcChg = t["priceChangePercent"] }
if sym == "ETHUSDT" { ethPrice = t["lastPrice"]; ethChg = t["priceChangePercent"] }
if err := json.Unmarshal(body, &t); err != nil {
continue
}
if sym == "BTCUSDT" {
btcPrice = t["lastPrice"]
btcChg = t["priceChangePercent"]
}
if sym == "ETHUSDT" {
ethPrice = t["lastPrice"]
ethChg = t["priceChangePercent"]
}
}
brief := fmt.Sprintf("%s\n\n• BTC: $%s (%s%%)\n• ETH: $%s (%s%%)\n\n_%s_",