feat: add debug log stream support

This commit is contained in:
2026-04-02 23:27:46 +08:00
parent f1c16e89f0
commit 9ec25b94f1
15 changed files with 624 additions and 15 deletions

View File

@@ -37,11 +37,17 @@ func NewWK(username, password, host string, cookies []*http.Cookie) *WK {
return nil
}
req := request.NewClient(&request.Config{
reqCfg := &request.Config{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0",
// Proxy: "http://127.0.0.1:9000",
// VerifySSL: false,
})
VerifySSL: true,
Debug: conf.IsDebugMode(),
}
if conf.IsDebugMode() {
reqCfg.Proxy = conf.DebugProxy
reqCfg.VerifySSL = !conf.DebugSkipSSLVerify
}
req := request.NewClient(reqCfg)
if len(cookies) > 0 {
req.SetCookies(cookies)
}

View File

@@ -37,8 +37,14 @@ func (m *SessionManager) Store(wk *WK) string {
userKey := wk.Host + ":" + wk.Username
if oldID, exists := m.userToSession[userKey]; exists {
item := m.sessions[oldID]
if item.cancel != nil {
item.cancel()
}
ctx, cancel := context.WithCancel(context.Background())
item.LastValue = time.Now()
item.Instance = wk
item.cancel = cancel
m.sessions[oldID] = item
log.Info("用户已存在,复用旧 Session",
@@ -46,6 +52,8 @@ func (m *SessionManager) Store(wk *WK) string {
zap.String("user", userKey),
)
go m.KeepAlive(ctx, oldID)
return oldID
}
@@ -61,18 +69,20 @@ func (m *SessionManager) Store(wk *WK) string {
log.Info("创建新 Session", zap.String("id", sessionID))
go m.KeepAlive(ctx, sessionID, wk)
go m.KeepAlive(ctx, sessionID)
return sessionID
}
// Get: 获取指定 session id 的 wk
func (m *SessionManager) Get(sessionID string) (*WK, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
m.mu.Lock()
defer m.mu.Unlock()
item, ok := m.sessions[sessionID]
if ok {
item.LastValue = time.Now()
m.sessions[sessionID] = item
return item.Instance, true
}
return nil, false
@@ -96,7 +106,7 @@ func (m *SessionManager) Del(sessionID string) {
}
}
func (m *SessionManager) KeepAlive(ctx context.Context, id string, wk *WK) {
func (m *SessionManager) KeepAlive(ctx context.Context, id string) {
ticker := time.NewTicker(2 * time.Minute)
defer ticker.Stop()
@@ -108,7 +118,15 @@ func (m *SessionManager) KeepAlive(ctx context.Context, id string, wk *WK) {
log.Info("KeepAlive 已停止", zap.String("id", id))
return
case <-ticker.C:
_, err := wk.Online()
m.mu.RLock()
item, ok := m.sessions[id]
m.mu.RUnlock()
if !ok || item.Instance == nil {
log.Info("Session 已不存在,停止 KeepAlive", zap.String("id", id))
return
}
_, err := item.Instance.Online()
if err != nil {
log.Error("自动保活请求失败", zap.Error(err))
}

View File

@@ -1,5 +1,10 @@
package conf
import (
"os"
"strings"
)
// 构建信息
var (
Mode string = "debug"
@@ -9,4 +14,32 @@ var (
GitAuthor string = "unknown"
GitEmail string = "unknown"
GitCommit string = "unknown"
DebugProxy string = ""
DebugSkipSSLVerify bool = false
)
func init() {
if !IsDebugMode() {
return
}
if proxy := os.Getenv("CKWK_DEBUG_PROXY"); proxy != "" {
DebugProxy = proxy
}
DebugSkipSSLVerify = parseEnvBool("CKWK_DEBUG_SKIP_SSL_VERIFY")
}
func IsDebugMode() bool {
return !strings.EqualFold(Mode, "release")
}
func parseEnvBool(key string) bool {
value := strings.TrimSpace(os.Getenv(key))
switch strings.ToLower(value) {
case "1", "true", "yes", "on":
return true
default:
return false
}
}

View File

@@ -0,0 +1,65 @@
package handler
import (
"net/http"
"time"
"ckwk/pkg/log"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
var debugLogUpgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func DebugLogWS(ctx *gin.Context) {
conn, err := debugLogUpgrader.Upgrade(ctx.Writer, ctx.Request, nil)
if err != nil {
return
}
defer conn.Close()
subID, ch := log.Subscribe()
defer log.Unsubscribe(subID)
for _, entry := range log.Entries() {
if err := conn.WriteJSON(entry); err != nil {
return
}
}
done := make(chan struct{})
go func() {
defer close(done)
for {
if _, _, err := conn.ReadMessage(); err != nil {
return
}
}
}()
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-done:
return
case entry, ok := <-ch:
if !ok {
return
}
if err := conn.WriteJSON(entry); err != nil {
return
}
case <-ticker.C:
if err := conn.WriteMessage(websocket.PingMessage, []byte("ping")); err != nil {
return
}
}
}
}

View File

@@ -0,0 +1,89 @@
package middleware
import (
"bytes"
"io"
"net/http"
"strings"
"time"
"ckwk/pkg/log"
"github.com/gin-gonic/gin"
"go.uber.org/zap/zapcore"
)
const maxDebugBodySize = 4 * 1024
type debugBodyWriter struct {
gin.ResponseWriter
body bytes.Buffer
}
func (w *debugBodyWriter) Write(data []byte) (int, error) {
w.body.Write(data)
return w.ResponseWriter.Write(data)
}
func (w *debugBodyWriter) WriteString(data string) (int, error) {
w.body.WriteString(data)
return w.ResponseWriter.WriteString(data)
}
func DebugHTTPLog() gin.HandlerFunc {
return func(ctx *gin.Context) {
if isDebugLogRoute(ctx.Request.URL.Path) {
ctx.Next()
return
}
startAt := time.Now()
requestBody := readRequestBody(ctx.Request)
writer := &debugBodyWriter{ResponseWriter: ctx.Writer}
ctx.Writer = writer
ctx.Next()
fields := map[string]any{
"method": ctx.Request.Method,
"path": ctx.Request.URL.Path,
"rawQuery": ctx.Request.URL.RawQuery,
"status": writer.Status(),
"durationMs": time.Since(startAt).Milliseconds(),
"clientIP": ctx.ClientIP(),
"requestHeader": log.SanitizeHeaders(ctx.Request.Header),
"requestBody": truncate(log.SanitizeBody(ctx.ContentType(), requestBody), maxDebugBodySize),
"responseHeader": log.SanitizeHeaders(http.Header(writer.Header().Clone())),
"responseBody": truncate(log.SanitizeBody(writer.Header().Get("Content-Type"), writer.body.String()), maxDebugBodySize),
"responseSize": writer.Size(),
"handler": ctx.HandlerName(),
"abortWithErrors": ctx.Errors.ByType(gin.ErrorTypeAny).String(),
}
log.Capture(zapcore.DebugLevel, "http", "incoming exchange", fields)
}
}
func readRequestBody(r *http.Request) string {
if r == nil || r.Body == nil {
return ""
}
body, err := io.ReadAll(r.Body)
if err != nil {
return ""
}
r.Body = io.NopCloser(bytes.NewBuffer(body))
return string(body)
}
func truncate(value string, limit int) string {
if len(value) <= limit {
return value
}
return value[:limit] + "...(truncated)"
}
func isDebugLogRoute(path string) bool {
return strings.HasPrefix(path, "/api/debug/ws/logs")
}

View File

@@ -48,6 +48,9 @@ func SetupRouter() *gin.Engine {
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
if conf.IsDebugMode() {
r.Use(middleware.DebugHTTPLog())
}
wkHandler := handler.NewWKHandler()
sessionMiddleware := middleware.SessionMiddleware(wkHandler.Session)
// schedule.StartCron(wkHandler.Session)
@@ -65,6 +68,12 @@ func SetupRouter() *gin.Engine {
{
api.POST("/login", wkHandler.Login)
api.Any("/version", handler.Version)
if conf.IsDebugMode() {
debug := api.Group("/debug")
{
debug.GET("/ws/logs", handler.DebugLogWS)
}
}
v1 := api.Group("/v1")
{
v1.GET("/host", wkHandler.Host)