feat: improve user experience

This commit is contained in:
tinkle-community
2025-12-28 23:29:59 +08:00
parent 98ba88b548
commit 7b30b687eb
6 changed files with 117 additions and 0 deletions
+11
View File
@@ -2,6 +2,7 @@ package config
import (
"nofx/experience"
"nofx/mcp"
"os"
"strconv"
"strings"
@@ -79,6 +80,16 @@ func Init() {
// Initialize experience improvement (installation ID will be set after database init)
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
+5
View File
@@ -71,6 +71,10 @@ To help us improve the product experience, the "Software" sends **anonymous usag
- Trade type (open/close position)
- Trade amount (USD value)
- 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):
- Installation ID: Identifies each independently deployed software instance
- 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 account addresses, usernames, or identity information
- 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
**How to Disable:**
+5
View File
@@ -73,6 +73,10 @@ D. 体验改进计划(可选)
- 交易类型(开仓/平仓)
- 交易金额(USD 数值)
- 交易币种(如 BTCUSDT
- AI 模型使用统计:
- AI 服务商名称(如 OpenAI、DeepSeek、Anthropic
- AI 模型名称(如 gpt-4o、deepseek-chat
- Token 消耗量(每次请求的输入/输出 token 数)
- 匿名标识符(用于统计活跃数量,不关联个人身份):
- 安装实例 ID:标识每个独立部署的软件实例
- 用户 ID:标识软件内的用户账号(仅用于统计活跃用户数)
@@ -82,6 +86,7 @@ D. 体验改进计划(可选)
- 您的 API 密钥、私钥或任何凭证
- 您的账户地址、用户名或身份信息
- 具体的交易价格、时间或订单详情
- AI 对话内容(提示词、回复或交易决策)
- 任何可通过上述匿名 ID 反向识别个人身份的信息
**如何关闭:**
+52
View File
@@ -37,6 +37,15 @@ type TradeEvent struct {
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 {
ClientID string `json:"client_id"`
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()
}
}
}()
}
+16
View File
@@ -99,6 +99,10 @@ func (c *ClaudeClient) parseMCPResponse(body []byte) (string, error) {
Type string `json:"type"`
Text string `json:"text"`
} `json:"content"`
Usage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
} `json:"usage"`
Error *struct {
Type string `json:"type"`
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))
}
// 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
for _, content := range response.Content {
if content.Type == "text" {
+28
View File
@@ -31,8 +31,20 @@ var (
"stream error", // HTTP/2 stream 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
type Client struct {
Provider string
@@ -226,6 +238,11 @@ func (client *Client) parseMCPResponse(body []byte) (string, error) {
Content string `json:"content"`
} `json:"message"`
} `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 {
@@ -236,6 +253,17 @@ func (client *Client) parseMCPResponse(body []byte) (string, error) {
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
}