File size: 8,495 Bytes
6d60378 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | // report.go growth 域「对话活跃上报」接口:POST {billingBase}/v2/report。
// 照抄客户端 chat_request_send 事件形状(含 conversationId/mode/inputLength 等全字段,
// 勿用最小 3 字段,防上游后续加严)。事件必须带 userId(=账号 uid),缺失则服务端
// 200 但静默丢弃(实测见 REPORT-active-map.md §2)。
//
// 一条上报同时点亮 growth 连登 + 解锁 first_buddy 任务(领养前置)。
// 风控口径:每号每天 1 次即可(activity_hours 单时点),不做多时点高频上报。
package upstream
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
"time"
"github.com/linguo2625469/workbuddy2api-panel/internal/auth"
)
// reportPath 活跃上报通道(实测)。
const reportPath = "/v2/report"
// billingJSON 发 billing 域(billingBase,codebuddy.cn)请求并解信封;body 为 nil 时不带请求体。
// 与 travel.go 的 growthJSON 对称(growth 域走 chatBase + BillingHeaders;billing 域走 billingBase)。
// report/checkin 等 billing 端点共用:请求头统一 BillingHeaders,信封与错误语义同 doJSON。
func (c *Client) billingJSON(a *auth.Auth, method, path string, body any) (json.RawMessage, error) {
var rdr io.Reader
if body != nil {
raw, err := json.Marshal(body)
if err != nil {
return nil, err
}
rdr = bytes.NewReader(raw)
}
req, err := http.NewRequest(method, c.billingBase(a)+path, rdr)
if err != nil {
return nil, err
}
c.BillingHeaders(req, a)
return c.doJSON(req)
}
// billingMeterJSON 仅对 /billing/meter 族端点(get-user-resource / daily-checkin)
// 按 realm 走双路径 fallback:global 先无 /v2 前缀,ErrNotFound 时二次换有 /v2 前缀
// (上游新旧路径分叉);cn 单路径(有 /v2)现状不变。仅 global realm 才有多路径。
func (c *Client) billingMeterJSON(a *auth.Auth, paths []string, method string, body any) (json.RawMessage, error) {
var lastErr error
for i, path := range paths {
data, err := c.billingJSON(a, method, path, body)
if err == nil {
return data, nil
}
lastErr = err
// 仅 404 换路径(路径不存在才值得 fallback);其他错误直接返回。
var ue *Error
if !errors.As(err, &ue) || ue.Kind != ErrNotFound || i == len(paths)-1 {
return nil, err
}
}
return nil, lastErr
}
// billingRetryDelay 签到/余额等维护类计费调用瞬时错误重试的间隔基数。
// 独立变量供测试缩短(生产固定 2s:第 1 次重试等 2s、第 2 次等 4s)。
var billingRetryDelay = 2 * time.Second
// isTransientBillingErr 报告 err 是否值得对计费维护类调用做有界重试:
// 上游 5xx(ErrServer,实测偶发 "code 10000 / API request failed with status
// code: 500")或网络层错误(非 *Error 的传输失败)。业务错误(code!=0 的
// 已签到/参数错、4xx、限流)不重试——重试只会原样再失败一次。
func isTransientBillingErr(err error) bool {
if err == nil {
return false
}
var ue *Error
if errors.As(err, &ue) {
return ue.Kind == ErrServer
}
return true
}
// retryBillingTransient 对签到/余额这类低频维护调用做瞬时错误有界重试:
// 最多补打 2 次(间隔 2s、4s),首次成功或非瞬时错误立即返回。chat 热路径
// 不用本策略——它有自己的换号轮转语义,重试会放大在途请求。
func (c *Client) retryBillingTransient(fn func() error) error {
err := fn()
if err == nil || !isTransientBillingErr(err) {
return err
}
for i := 1; i <= 2; i++ {
time.Sleep(time.Duration(i) * billingRetryDelay)
if err = fn(); err == nil || !isTransientBillingErr(err) {
return err
}
}
return err
}
// chatRequestEvent 客户端 chat_request_send 事件完整形状(与 probe_active.py chat_event 对齐)。
// userId 为必填字段(= a.UID);conversationId 由调用方生成,无需真实会话。
type chatRequestEvent struct {
EventCode string `json:"eventCode"`
Timestamp int64 `json:"timestamp"`
ReportDelay int `json:"reportDelay"`
Mode string `json:"mode"`
ConversationID string `json:"conversationId"`
RequestID string `json:"requestId"`
InputLength int `json:"inputLength"`
RequestModelID string `json:"requestModelId"`
RequestModelName string `json:"requestModelName"`
IsPlan bool `json:"isPlan"`
IsAutoExecuteTerminal bool `json:"isAutoExecuteTerminal"`
IsAutoModify bool `json:"isAutoModify"`
CodebaseEnable bool `json:"codebaseEnable"`
MaxToken int `json:"maxToken"`
MaxSteps int `json:"maxSteps"`
Temperature int `json:"temperature"`
MaxRetries int `json:"maxRetries"`
MentionContexts []any `json:"mentionContexts"`
KnowledgeID []any `json:"knowledgeId"`
KnowledgeName []any `json:"knowledgeName"`
CodebaseID string `json:"codebaseId"`
MentionContextCount int `json:"mentionContextCount"`
Command string `json:"command"`
ExpertID string `json:"expertId"`
RecommendID string `json:"recommendId"`
SkillID string `json:"skillId"`
SkillCount int `json:"skillCount"`
TotalCount int `json:"totalCount"`
FileURI string `json:"fileUri"`
PresentAt int64 `json:"presentAt"`
TraceID string `json:"traceId"`
RootRequestID string `json:"rootRequestId"`
ParentConversationID string `json:"parentConversationId"`
AgentName string `json:"agentName"`
AgentType string `json:"agentType"`
UserID string `json:"userId"`
}
// ReportChatActivity 向上游发送一条对话活跃上报(chat_request_send)。
// conversationID 由调用方生成(如 wb2api-<ms>),无需真实会话——服务端不校验一致性。
// requestID 为本轮请求独立标识(多轮同会话上报时各条不同);空时回落 conversationID。
// 错误语义与 doJSON 一致:HTTP 非 2xx / 业务 code != 0 → *Error。
func (c *Client) ReportChatActivity(a *auth.Auth, conversationID, requestID string) error {
return c.ReportChatActivityModel(a, conversationID, requestID, "deepseek-v4-flash", "DeepSeek V4 Flash")
}
// ReportChatActivityModel 同上,但可指定上报携带的模型:供「体验某模型」类任务
// 对齐实际模型(如 Model_chat_GLM5.2 需 requestModelId=glm-5.2 与独立 requestID)。
func (c *Client) ReportChatActivityModel(a *auth.Auth, conversationID, requestID, modelID, modelName string) error {
if requestID == "" {
requestID = conversationID
}
if modelID == "" {
modelID = "deepseek-v4-flash"
}
if modelName == "" {
modelName = modelID
}
now := time.Now().UnixMilli()
ev := chatRequestEvent{
EventCode: "chat_request_send",
Timestamp: now,
ReportDelay: 0,
Mode: "craft",
ConversationID: conversationID,
RequestID: requestID,
InputLength: 12,
RequestModelID: modelID,
RequestModelName: modelName,
IsPlan: false,
IsAutoExecuteTerminal: false,
IsAutoModify: false,
CodebaseEnable: false,
MaxToken: 0,
MaxSteps: 0,
Temperature: 0,
MaxRetries: 0,
MentionContexts: []any{},
KnowledgeID: []any{},
KnowledgeName: []any{},
CodebaseID: "",
MentionContextCount: 0,
Command: "",
ExpertID: "",
RecommendID: "",
SkillID: "",
SkillCount: 0,
TotalCount: 0,
FileURI: "",
PresentAt: now,
TraceID: "",
RootRequestID: requestID,
ParentConversationID: conversationID,
AgentName: "default",
AgentType: "conversation",
UserID: a.UID,
}
raw, err := json.Marshal([]chatRequestEvent{ev})
if err != nil {
return err
}
_, err = c.billingJSON(a, http.MethodPost, reportPath, json.RawMessage(raw))
return err
}
|