mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 09:24:36 +08:00
feat(agent): surface the AI500 index board in chat, tools, and sidebar
- provider/nofxos: GetAI500ListCached — 5min TTL cache with stale fallback on upstream failure; ResolveClient routes through the claw402 gateway when a wallet key is configured (user's claw402 model key -> CLAW402_WALLET_KEY env -> direct nofxos) - new GET /api/ai500 endpoint serving the score-sorted board - new get_ai500_list agent tool + prompt rule: when the user wants coin picks or creates a strategy without naming coins, consult AI500's high-scoring entries by default - web: AI500 sidebar panel (rank, score badge, gain since entry, 5min auto-refresh); clicking an entry asks the agent to analyze it
This commit is contained in:
60
provider/nofxos/ai500_cache.go
Normal file
60
provider/nofxos/ai500_cache.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package nofxos
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ai500CacheTTL bounds how often the AI500 board is re-fetched. The list is
|
||||
// refreshed upstream on the order of minutes, every claw402-routed call costs
|
||||
// money, and the agent UI polls this for display — so short staleness is
|
||||
// preferable to per-render upstream calls.
|
||||
const ai500CacheTTL = 5 * time.Minute
|
||||
|
||||
type ai500CacheStore struct {
|
||||
mu sync.Mutex
|
||||
coins []CoinData
|
||||
fetchedAt time.Time
|
||||
}
|
||||
|
||||
var ai500Cache = &ai500CacheStore{}
|
||||
|
||||
// fetchAI500ListFn is swappable in tests.
|
||||
var fetchAI500ListFn = func(c *Client) ([]CoinData, error) {
|
||||
return c.GetAI500List()
|
||||
}
|
||||
|
||||
// GetAI500ListCached returns the AI500 coin list served from a TTL cache.
|
||||
// When the upstream fetch fails and stale data exists, the stale board is
|
||||
// served instead of an error so displays keep working through flakiness.
|
||||
func GetAI500ListCached(c *Client) ([]CoinData, error) {
|
||||
ai500Cache.mu.Lock()
|
||||
defer ai500Cache.mu.Unlock()
|
||||
|
||||
hasCache := len(ai500Cache.coins) > 0
|
||||
if hasCache && time.Since(ai500Cache.fetchedAt) < ai500CacheTTL {
|
||||
return copyCoinData(ai500Cache.coins), nil
|
||||
}
|
||||
|
||||
coins, err := fetchAI500ListFn(c)
|
||||
if err != nil {
|
||||
if hasCache {
|
||||
log.Printf("⚠️ AI500 fetch failed (%v); serving cached list from %s",
|
||||
err, ai500Cache.fetchedAt.Format(time.RFC3339))
|
||||
return copyCoinData(ai500Cache.coins), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ai500Cache.coins = coins
|
||||
ai500Cache.fetchedAt = time.Now()
|
||||
return copyCoinData(coins), nil
|
||||
}
|
||||
|
||||
// copyCoinData returns a defensive copy so callers cannot mutate the cache.
|
||||
func copyCoinData(coins []CoinData) []CoinData {
|
||||
out := make([]CoinData, len(coins))
|
||||
copy(out, coins)
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user