From 7b30b687eba7598d85da340a59733a984af876f6 Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Sun, 28 Dec 2025 23:29:59 +0800 Subject: [PATCH] feat: improve user experience --- config/config.go | 11 +++++++ docs/i18n/en/PRIVACY POLICY.md | 5 +++ docs/i18n/zh-CN/PRIVACY POLICY.md | 5 +++ experience/experience.go | 52 +++++++++++++++++++++++++++++++ mcp/claude_client.go | 16 ++++++++++ mcp/client.go | 28 +++++++++++++++++ 6 files changed, 117 insertions(+) diff --git a/config/config.go b/config/config.go index 84532849..e22c9250 100644 --- a/config/config.go +++ b/config/config.go @@ -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 diff --git a/docs/i18n/en/PRIVACY POLICY.md b/docs/i18n/en/PRIVACY POLICY.md index 4102b866..c58a3c47 100644 --- a/docs/i18n/en/PRIVACY POLICY.md +++ b/docs/i18n/en/PRIVACY POLICY.md @@ -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:** diff --git a/docs/i18n/zh-CN/PRIVACY POLICY.md b/docs/i18n/zh-CN/PRIVACY POLICY.md index 65878579..f0da61aa 100644 --- a/docs/i18n/zh-CN/PRIVACY POLICY.md +++ b/docs/i18n/zh-CN/PRIVACY POLICY.md @@ -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 反向识别个人身份的信息 **如何关闭:** diff --git a/experience/experience.go b/experience/experience.go index a51774e4..b6a24430 100644 --- a/experience/experience.go +++ b/experience/experience.go @@ -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() + } + } + }() +} diff --git a/mcp/claude_client.go b/mcp/claude_client.go index 06a9c55b..d87b0d3f 100644 --- a/mcp/claude_client.go +++ b/mcp/claude_client.go @@ -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" { diff --git a/mcp/client.go b/mcp/client.go index bead83ef..3e778fb1 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -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 }