mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
feat: improve user experience
This commit is contained in:
@@ -2,6 +2,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"nofx/experience"
|
"nofx/experience"
|
||||||
|
"nofx/mcp"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -79,6 +80,16 @@ func Init() {
|
|||||||
|
|
||||||
// Initialize experience improvement (installation ID will be set after database init)
|
// Initialize experience improvement (installation ID will be set after database init)
|
||||||
experience.Init(cfg.ExperienceImprovement, "")
|
experience.Init(cfg.ExperienceImprovement, "")
|
||||||
|
|
||||||
|
// Set up AI token usage tracking callback
|
||||||
|
mcp.TokenUsageCallback = func(usage mcp.TokenUsage) {
|
||||||
|
experience.TrackAIUsage(experience.AIUsageEvent{
|
||||||
|
ModelProvider: usage.Provider,
|
||||||
|
ModelName: usage.Model,
|
||||||
|
InputTokens: usage.PromptTokens,
|
||||||
|
OutputTokens: usage.CompletionTokens,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the global configuration
|
// Get returns the global configuration
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ To help us improve the product experience, the "Software" sends **anonymous usag
|
|||||||
- Trade type (open/close position)
|
- Trade type (open/close position)
|
||||||
- Trade amount (USD value)
|
- Trade amount (USD value)
|
||||||
- Trading pair (e.g., BTCUSDT)
|
- Trading pair (e.g., BTCUSDT)
|
||||||
|
- AI model usage statistics:
|
||||||
|
- AI provider name (e.g., OpenAI, DeepSeek, Anthropic)
|
||||||
|
- AI model name (e.g., gpt-4o, deepseek-chat)
|
||||||
|
- Token consumption (input/output tokens per request)
|
||||||
- Anonymous identifiers (used for counting active numbers, not linked to personal identity):
|
- Anonymous identifiers (used for counting active numbers, not linked to personal identity):
|
||||||
- Installation ID: Identifies each independently deployed software instance
|
- Installation ID: Identifies each independently deployed software instance
|
||||||
- User ID: Identifies user accounts within the software (only for counting active users)
|
- User ID: Identifies user accounts within the software (only for counting active users)
|
||||||
@@ -80,6 +84,7 @@ To help us improve the product experience, the "Software" sends **anonymous usag
|
|||||||
- Your API keys, private keys, or any credentials
|
- Your API keys, private keys, or any credentials
|
||||||
- Your account addresses, usernames, or identity information
|
- Your account addresses, usernames, or identity information
|
||||||
- Specific trade prices, times, or order details
|
- Specific trade prices, times, or order details
|
||||||
|
- AI conversation content (prompts, responses, or trading decisions)
|
||||||
- Any information that could reverse-identify personal identity through the above anonymous IDs
|
- Any information that could reverse-identify personal identity through the above anonymous IDs
|
||||||
|
|
||||||
**How to Disable:**
|
**How to Disable:**
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ D. 体验改进计划(可选)
|
|||||||
- 交易类型(开仓/平仓)
|
- 交易类型(开仓/平仓)
|
||||||
- 交易金额(USD 数值)
|
- 交易金额(USD 数值)
|
||||||
- 交易币种(如 BTCUSDT)
|
- 交易币种(如 BTCUSDT)
|
||||||
|
- AI 模型使用统计:
|
||||||
|
- AI 服务商名称(如 OpenAI、DeepSeek、Anthropic)
|
||||||
|
- AI 模型名称(如 gpt-4o、deepseek-chat)
|
||||||
|
- Token 消耗量(每次请求的输入/输出 token 数)
|
||||||
- 匿名标识符(用于统计活跃数量,不关联个人身份):
|
- 匿名标识符(用于统计活跃数量,不关联个人身份):
|
||||||
- 安装实例 ID:标识每个独立部署的软件实例
|
- 安装实例 ID:标识每个独立部署的软件实例
|
||||||
- 用户 ID:标识软件内的用户账号(仅用于统计活跃用户数)
|
- 用户 ID:标识软件内的用户账号(仅用于统计活跃用户数)
|
||||||
@@ -82,6 +86,7 @@ D. 体验改进计划(可选)
|
|||||||
- 您的 API 密钥、私钥或任何凭证
|
- 您的 API 密钥、私钥或任何凭证
|
||||||
- 您的账户地址、用户名或身份信息
|
- 您的账户地址、用户名或身份信息
|
||||||
- 具体的交易价格、时间或订单详情
|
- 具体的交易价格、时间或订单详情
|
||||||
|
- AI 对话内容(提示词、回复或交易决策)
|
||||||
- 任何可通过上述匿名 ID 反向识别个人身份的信息
|
- 任何可通过上述匿名 ID 反向识别个人身份的信息
|
||||||
|
|
||||||
**如何关闭:**
|
**如何关闭:**
|
||||||
|
|||||||
@@ -37,6 +37,15 @@ type TradeEvent struct {
|
|||||||
TraderID string
|
TraderID string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AIUsageEvent struct {
|
||||||
|
UserID string
|
||||||
|
TraderID string
|
||||||
|
ModelProvider string // openai, deepseek, anthropic, etc.
|
||||||
|
ModelName string // gpt-4o, deepseek-chat, claude-3, etc.
|
||||||
|
InputTokens int
|
||||||
|
OutputTokens int
|
||||||
|
}
|
||||||
|
|
||||||
type telemetryPayload struct {
|
type telemetryPayload struct {
|
||||||
ClientID string `json:"client_id"`
|
ClientID string `json:"client_id"`
|
||||||
Events []telemetryEvent `json:"events"`
|
Events []telemetryEvent `json:"events"`
|
||||||
@@ -186,3 +195,46 @@ func TrackStartup(version string) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TrackAIUsage(event AIUsageEvent) {
|
||||||
|
if client == nil || !IsEnabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
client.mu.RLock()
|
||||||
|
installationID := client.installationID
|
||||||
|
client.mu.RUnlock()
|
||||||
|
|
||||||
|
payload := telemetryPayload{
|
||||||
|
ClientID: installationID,
|
||||||
|
Events: []telemetryEvent{
|
||||||
|
{
|
||||||
|
Name: "ai_usage",
|
||||||
|
Params: map[string]interface{}{
|
||||||
|
"model_provider": event.ModelProvider,
|
||||||
|
"model_name": event.ModelName,
|
||||||
|
"input_tokens": event.InputTokens,
|
||||||
|
"output_tokens": event.OutputTokens,
|
||||||
|
"total_tokens": event.InputTokens + event.OutputTokens,
|
||||||
|
"installation_id": installationID,
|
||||||
|
"user_id": event.UserID,
|
||||||
|
"trader_id": event.TraderID,
|
||||||
|
"engagement_time_msec": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, _ := json.Marshal(payload)
|
||||||
|
url := telemetryEndpoint + "?measurement_id=" + tid + "&api_secret=" + tk
|
||||||
|
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||||
|
if req != nil {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err == nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|||||||
@@ -99,6 +99,10 @@ func (c *ClaudeClient) parseMCPResponse(body []byte) (string, error) {
|
|||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
} `json:"content"`
|
} `json:"content"`
|
||||||
|
Usage struct {
|
||||||
|
InputTokens int `json:"input_tokens"`
|
||||||
|
OutputTokens int `json:"output_tokens"`
|
||||||
|
} `json:"usage"`
|
||||||
Error *struct {
|
Error *struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
@@ -117,6 +121,18 @@ func (c *ClaudeClient) parseMCPResponse(body []byte) (string, error) {
|
|||||||
return "", fmt.Errorf("Claude returned empty content, body: %s", string(body))
|
return "", fmt.Errorf("Claude returned empty content, body: %s", string(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Report token usage if callback is set
|
||||||
|
totalTokens := response.Usage.InputTokens + response.Usage.OutputTokens
|
||||||
|
if TokenUsageCallback != nil && totalTokens > 0 {
|
||||||
|
TokenUsageCallback(TokenUsage{
|
||||||
|
Provider: c.Provider,
|
||||||
|
Model: c.Model,
|
||||||
|
PromptTokens: response.Usage.InputTokens,
|
||||||
|
CompletionTokens: response.Usage.OutputTokens,
|
||||||
|
TotalTokens: totalTokens,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Find text content
|
// Find text content
|
||||||
for _, content := range response.Content {
|
for _, content := range response.Content {
|
||||||
if content.Type == "text" {
|
if content.Type == "text" {
|
||||||
|
|||||||
@@ -31,8 +31,20 @@ var (
|
|||||||
"stream error", // HTTP/2 stream error
|
"stream error", // HTTP/2 stream error
|
||||||
"INTERNAL_ERROR", // Server internal error
|
"INTERNAL_ERROR", // Server internal error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TokenUsageCallback is called after each AI request with token usage info
|
||||||
|
TokenUsageCallback func(usage TokenUsage)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TokenUsage represents token usage from AI API response
|
||||||
|
type TokenUsage struct {
|
||||||
|
Provider string
|
||||||
|
Model string
|
||||||
|
PromptTokens int
|
||||||
|
CompletionTokens int
|
||||||
|
TotalTokens int
|
||||||
|
}
|
||||||
|
|
||||||
// Client AI API configuration
|
// Client AI API configuration
|
||||||
type Client struct {
|
type Client struct {
|
||||||
Provider string
|
Provider string
|
||||||
@@ -226,6 +238,11 @@ func (client *Client) parseMCPResponse(body []byte) (string, error) {
|
|||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
} `json:"message"`
|
} `json:"message"`
|
||||||
} `json:"choices"`
|
} `json:"choices"`
|
||||||
|
Usage struct {
|
||||||
|
PromptTokens int `json:"prompt_tokens"`
|
||||||
|
CompletionTokens int `json:"completion_tokens"`
|
||||||
|
TotalTokens int `json:"total_tokens"`
|
||||||
|
} `json:"usage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(body, &result); err != nil {
|
if err := json.Unmarshal(body, &result); err != nil {
|
||||||
@@ -236,6 +253,17 @@ func (client *Client) parseMCPResponse(body []byte) (string, error) {
|
|||||||
return "", fmt.Errorf("API returned empty response")
|
return "", fmt.Errorf("API returned empty response")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Report token usage if callback is set
|
||||||
|
if TokenUsageCallback != nil && result.Usage.TotalTokens > 0 {
|
||||||
|
TokenUsageCallback(TokenUsage{
|
||||||
|
Provider: client.Provider,
|
||||||
|
Model: client.Model,
|
||||||
|
PromptTokens: result.Usage.PromptTokens,
|
||||||
|
CompletionTokens: result.Usage.CompletionTokens,
|
||||||
|
TotalTokens: result.Usage.TotalTokens,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return result.Choices[0].Message.Content, nil
|
return result.Choices[0].Message.Content, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user