@@ -51,6 +51,9 @@ type Handler struct {
5151 // 增删 key 后调用,让鉴权中间件的 dbKeys 缓存立即过期。
5252 apiKeyCacheInvalidator func ()
5353
54+ // 对话采集器引用(main.go 注入,可空表示未启用)
55+ dialogCollector * proxy.DialogCollector
56+
5457 // 图表聚合内存缓存(10秒 TTL)
5558 chartCacheMu sync.RWMutex
5659 chartCacheData map [string ]* chartCacheEntry
@@ -96,6 +99,122 @@ func (h *Handler) InvalidateStatsCache() {
9699 h .listActiveCache .Store (nil )
97100}
98101
102+ // SetDialogCollector 注入对话采集器(main.go 在初始化时调用)。
103+ // 允许传入 nil(采集功能未启用时所有 dialog API 返回 disabled 状态)。
104+ func (h * Handler ) SetDialogCollector (c * proxy.DialogCollector ) {
105+ if h == nil {
106+ return
107+ }
108+ h .dialogCollector = c
109+ }
110+
111+ // GetDialogStats 查询对话采集运行时指标 + DB 累计量。
112+ // GET /api/admin/dialog-stats
113+ func (h * Handler ) GetDialogStats (c * gin.Context ) {
114+ out := gin.H {
115+ "installed" : h .dialogCollector != nil ,
116+ }
117+ if h .dialogCollector != nil {
118+ out ["runtime" ] = h .dialogCollector .Stats ()
119+ }
120+ if h .db != nil {
121+ ctx , cancel := context .WithTimeout (c .Request .Context (), 5 * time .Second )
122+ defer cancel ()
123+ if dbStats , err := h .db .GetDialogStats (ctx ); err == nil {
124+ out ["db" ] = dbStats
125+ } else {
126+ out ["db_error" ] = err .Error ()
127+ }
128+ }
129+ c .JSON (http .StatusOK , out )
130+ }
131+
132+ // ListDialogLogs 分页列出对话采集记录(不含 body 大字段)。
133+ // GET /api/admin/dialog-logs?endpoint=&model=&limit=50&offset=0
134+ func (h * Handler ) ListDialogLogs (c * gin.Context ) {
135+ if h .db == nil {
136+ c .JSON (http .StatusServiceUnavailable , gin.H {"error" : "db unavailable" })
137+ return
138+ }
139+ limit , _ := strconv .Atoi (c .Query ("limit" ))
140+ if limit <= 0 {
141+ limit = 50
142+ }
143+ offset , _ := strconv .Atoi (c .Query ("offset" ))
144+ if offset < 0 {
145+ offset = 0
146+ }
147+ ctx , cancel := context .WithTimeout (c .Request .Context (), 10 * time .Second )
148+ defer cancel ()
149+ rows , total , err := h .db .ListDialogLogs (ctx , database.DialogLogListParams {
150+ Endpoint : strings .TrimSpace (c .Query ("endpoint" )),
151+ Model : strings .TrimSpace (c .Query ("model" )),
152+ Limit : limit ,
153+ Offset : offset ,
154+ })
155+ if err != nil {
156+ c .JSON (http .StatusInternalServerError , gin.H {"error" : err .Error ()})
157+ return
158+ }
159+ c .JSON (http .StatusOK , gin.H {
160+ "items" : rows ,
161+ "total" : total ,
162+ "limit" : limit ,
163+ "offset" : offset ,
164+ })
165+ }
166+
167+ // GetDialogLogDetail 查看单条详情(含完整 body)。
168+ // GET /api/admin/dialog-logs/:id
169+ func (h * Handler ) GetDialogLogDetail (c * gin.Context ) {
170+ if h .db == nil {
171+ c .JSON (http .StatusServiceUnavailable , gin.H {"error" : "db unavailable" })
172+ return
173+ }
174+ id , err := strconv .ParseInt (c .Param ("id" ), 10 , 64 )
175+ if err != nil || id <= 0 {
176+ c .JSON (http .StatusBadRequest , gin.H {"error" : "invalid id" })
177+ return
178+ }
179+ ctx , cancel := context .WithTimeout (c .Request .Context (), 10 * time .Second )
180+ defer cancel ()
181+ detail , err := h .db .GetDialogLogByID (ctx , id )
182+ if err != nil {
183+ c .JSON (http .StatusInternalServerError , gin.H {"error" : err .Error ()})
184+ return
185+ }
186+ if detail == nil {
187+ c .JSON (http .StatusNotFound , gin.H {"error" : "not found" })
188+ return
189+ }
190+ c .JSON (http .StatusOK , detail )
191+ }
192+
193+ // ToggleDialogCollection 运行时开关,无需重启容器。
194+ // POST /api/admin/dialog-toggle body: {"enabled": true/false}
195+ func (h * Handler ) ToggleDialogCollection (c * gin.Context ) {
196+ if h .dialogCollector == nil {
197+ c .JSON (http .StatusServiceUnavailable , gin.H {
198+ "error" : gin.H {"message" : "采集器未启用(启动级 ENV 关闭或非 PG 后端)" , "type" : "not_available" },
199+ })
200+ return
201+ }
202+ var req struct {
203+ Enabled bool `json:"enabled"`
204+ }
205+ if err := c .ShouldBindJSON (& req ); err != nil {
206+ c .JSON (http .StatusBadRequest , gin.H {
207+ "error" : gin.H {"message" : "invalid body: " + err .Error (), "type" : "invalid_request" },
208+ })
209+ return
210+ }
211+ h .dialogCollector .SetEnabled (req .Enabled )
212+ c .JSON (http .StatusOK , gin.H {
213+ "enabled" : req .Enabled ,
214+ "runtime" : h .dialogCollector .Stats (),
215+ })
216+ }
217+
99218// getCachedListActive 返回带 5 秒 TTL 缓存的 ListActive 结果,
100219// 避免分页/筛选请求每次都走 DB 全量查询。
101220func (h * Handler ) getCachedListActive (ctx context.Context ) ([]* database.AccountRow , error ) {
@@ -197,6 +316,12 @@ func (h *Handler) RegisterRoutes(r *gin.Engine) {
197316 api .POST ("/proxies/batch-delete" , h .BatchDeleteProxies )
198317 api .POST ("/proxies/test" , h .TestProxy )
199318
319+ // 对话采集(dialog_logs)
320+ api .GET ("/dialog-stats" , h .GetDialogStats )
321+ api .POST ("/dialog-toggle" , h .ToggleDialogCollection )
322+ api .GET ("/dialog-logs" , h .ListDialogLogs )
323+ api .GET ("/dialog-logs/:id" , h .GetDialogLogDetail )
324+
200325 // OAuth 授权流程
201326 api .POST ("/oauth/generate-auth-url" , h .GenerateOAuthURL )
202327 api .POST ("/oauth/exchange-code" , h .ExchangeOAuthCode )
@@ -2276,6 +2401,7 @@ type settingsResponse struct {
22762401 RTManagerEnabled bool `json:"rt_manager_enabled"`
22772402 RTManagerPasswordSet bool `json:"rt_manager_password_set"`
22782403 FreeGPT55Enabled bool `json:"free_gpt55_enabled"`
2404+ PreferPaidEnabled bool `json:"prefer_paid_enabled"`
22792405}
22802406
22812407type updateSettingsReq struct {
@@ -2308,6 +2434,7 @@ type updateSettingsReq struct {
23082434 RTManagerPassword * string `json:"rt_manager_password"`
23092435 RTManagerEnabled * bool `json:"rt_manager_enabled"`
23102436 FreeGPT55Enabled * bool `json:"free_gpt55_enabled"`
2437+ PreferPaidEnabled * bool `json:"prefer_paid_enabled"`
23112438}
23122439
23132440// GetSettings 获取当前系统设置
@@ -2365,6 +2492,7 @@ func (h *Handler) GetSettings(c *gin.Context) {
23652492 RTManagerEnabled : rtManagerEnabled ,
23662493 RTManagerPasswordSet : rtManagerPasswordSet ,
23672494 FreeGPT55Enabled : h .store .GetFreeGPT55Enabled (),
2495+ PreferPaidEnabled : h .store .GetPreferPaidEnabled (),
23682496 })
23692497}
23702498
@@ -2646,6 +2774,12 @@ func (h *Handler) UpdateSettings(c *gin.Context) {
26462774 log .Printf ("设置已更新: free_gpt55_enabled = %t" , * req .FreeGPT55Enabled )
26472775 }
26482776
2777+ // prefer_paid_enabled:全局开关,false=prefer_free(默认省额度) / true=prefer_paid(体验优先)
2778+ if req .PreferPaidEnabled != nil {
2779+ h .store .SetPreferPaidEnabled (* req .PreferPaidEnabled )
2780+ log .Printf ("设置已更新: prefer_paid_enabled = %t" , * req .PreferPaidEnabled )
2781+ }
2782+
26492783 // 持久化保存到数据库
26502784 err := h .db .UpdateSystemSettings (c .Request .Context (), & database.SystemSettings {
26512785 MaxConcurrency : h .store .GetMaxConcurrency (),
@@ -2677,6 +2811,7 @@ func (h *Handler) UpdateSettings(c *gin.Context) {
26772811 RTManagerPassword : rtManagerPassword ,
26782812 RTManagerEnabled : rtManagerEnabled ,
26792813 FreeGPT55Enabled : h .store .GetFreeGPT55Enabled (),
2814+ PreferPaidEnabled : h .store .GetPreferPaidEnabled (),
26802815 })
26812816 if err != nil {
26822817 log .Printf ("无法持久化保存设置: %v" , err )
@@ -2732,6 +2867,7 @@ func (h *Handler) UpdateSettings(c *gin.Context) {
27322867 RTManagerEnabled : rtManagerEnabled ,
27332868 RTManagerPasswordSet : strings .TrimSpace (rtManagerPassword ) != "" ,
27342869 FreeGPT55Enabled : h .store .GetFreeGPT55Enabled (),
2870+ PreferPaidEnabled : h .store .GetPreferPaidEnabled (),
27352871 })
27362872}
27372873
0 commit comments