mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-06 20:41:14 +08:00
- /api/ai500 returned a success/data envelope but the web httpClient
wraps the raw body as data, so the panel read coins as undefined and
showed the empty state; return a flat {coins,count} body like
/api/symbols
- the agent rendered AI500 rankings as a markdown table that the chat
UI flattens into an unreadable line: tool note + system prompt now
mandate numbered lists (one coin per line) and ban tables outright
123 lines
3.5 KiB
Go
123 lines
3.5 KiB
Go
package agent
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"nofx/provider/nofxos"
|
|
"nofx/store"
|
|
)
|
|
|
|
const (
|
|
ai500DefaultLimit = 20
|
|
ai500MaxLimit = 100
|
|
)
|
|
|
|
// fetchAI500ForTool is swappable in tests. It resolves a nofxos client
|
|
// (routed through claw402 when a wallet key is available) and returns the
|
|
// cached AI500 board.
|
|
var fetchAI500ForTool = func(walletKey string) ([]nofxos.CoinData, error) {
|
|
return nofxos.GetAI500ListCached(nofxos.ResolveClient(walletKey))
|
|
}
|
|
|
|
// Claw402WalletKeyForStoreUser returns the wallet private key of the user's
|
|
// enabled claw402 model, if any, so data requests can be routed through the
|
|
// claw402 payment gateway on the user's own account.
|
|
func Claw402WalletKeyForStoreUser(st *store.Store, storeUserID string) string {
|
|
if st == nil {
|
|
return ""
|
|
}
|
|
if strings.TrimSpace(storeUserID) == "" {
|
|
storeUserID = "default"
|
|
}
|
|
models, err := st.AIModel().List(storeUserID)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
for _, model := range models {
|
|
if model == nil || !model.Enabled {
|
|
continue
|
|
}
|
|
if strings.EqualFold(strings.TrimSpace(model.Provider), "claw402") && len(model.APIKey) > 0 {
|
|
return string(model.APIKey)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// AI500BoardEntry is the display shape for one AI500 constituent.
|
|
type AI500BoardEntry struct {
|
|
Pair string `json:"pair"`
|
|
Score float64 `json:"score"`
|
|
MaxScore float64 `json:"max_score"`
|
|
IncreasePercent float64 `json:"increase_percent"`
|
|
StartPrice float64 `json:"start_price"`
|
|
StartTime int64 `json:"start_time"`
|
|
}
|
|
|
|
// AI500Board returns the AI500 constituents sorted by score (descending),
|
|
// truncated to limit.
|
|
func AI500Board(walletKey string, limit int) ([]AI500BoardEntry, error) {
|
|
if limit <= 0 {
|
|
limit = ai500DefaultLimit
|
|
}
|
|
if limit > ai500MaxLimit {
|
|
limit = ai500MaxLimit
|
|
}
|
|
coins, err := fetchAI500ForTool(walletKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sorted := make([]nofxos.CoinData, len(coins))
|
|
copy(sorted, coins)
|
|
sort.SliceStable(sorted, func(i, j int) bool {
|
|
return sorted[i].Score > sorted[j].Score
|
|
})
|
|
if len(sorted) > limit {
|
|
sorted = sorted[:limit]
|
|
}
|
|
out := make([]AI500BoardEntry, 0, len(sorted))
|
|
for _, coin := range sorted {
|
|
out = append(out, AI500BoardEntry{
|
|
Pair: coin.Pair,
|
|
Score: coin.Score,
|
|
MaxScore: coin.MaxScore,
|
|
IncreasePercent: coin.IncreasePercent,
|
|
StartPrice: coin.StartPrice,
|
|
StartTime: coin.StartTime,
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// toolGetAI500List exposes the AI500 board to the chat agent.
|
|
func (a *Agent) toolGetAI500List(storeUserID, argsJSON string) string {
|
|
var args struct {
|
|
Limit int `json:"limit"`
|
|
}
|
|
if strings.TrimSpace(argsJSON) != "" {
|
|
if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
|
|
return fmt.Sprintf(`{"error":"invalid arguments: %s"}`, err)
|
|
}
|
|
}
|
|
|
|
walletKey := Claw402WalletKeyForStoreUser(a.store, storeUserID)
|
|
entries, err := AI500Board(walletKey, args.Limit)
|
|
if err != nil {
|
|
return fmt.Sprintf(`{"error":"failed to fetch AI500 list: %s"}`, err)
|
|
}
|
|
|
|
payload, err := json.Marshal(map[string]any{
|
|
"status": "ok",
|
|
"count": len(entries),
|
|
"coins": entries,
|
|
"note": "AI500 is an AI-scored crypto index; score is 0-100, increase_percent is the gain since the coin entered the index. Present this in chat as a short numbered list, one coin per line, e.g. \"1. BEAT — 评分 84.2,入选以来 +404.1%\". NEVER use a markdown table — the chat UI does not render tables.",
|
|
})
|
|
if err != nil {
|
|
return fmt.Sprintf(`{"error":"failed to serialize AI500 list: %s"}`, err)
|
|
}
|
|
return string(payload)
|
|
}
|