release: v0.1.3
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
bin
|
bin
|
||||||
|
.gocache
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -33,16 +33,20 @@ git status
|
|||||||
|
|
||||||
**推荐使用 [Taskfile](https://taskfile.dev/) 进行项目构建**
|
**推荐使用 [Taskfile](https://taskfile.dev/) 进行项目构建**
|
||||||
|
|
||||||
调试模式下可通过环境变量开启本地代理/跳过 SSL 校验:
|
可通过环境变量配置本地代理 / 跳过 SSL 校验,但只有在设置页手动开启调试后才会生效:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
CKWK_DEBUG_PROXY=http://127.0.0.1:9000
|
CKWK_DEBUG_PROXY=http://127.0.0.1:9000
|
||||||
CKWK_DEBUG_SKIP_SSL_VERIFY=true
|
CKWK_DEBUG_SKIP_SSL_VERIFY=true
|
||||||
```
|
```
|
||||||
|
|
||||||
`release` 模式下会自动忽略这两个调试开关。
|
也可以通过环境变量让后端启动时默认开启调试:
|
||||||
|
|
||||||
调试日志 WS 仅在 `debug` 模式开启,连接地址:
|
```shell
|
||||||
|
CKWK_DEBUG_ENABLED=true
|
||||||
|
```
|
||||||
|
|
||||||
|
调试日志 WS 在后端调试已开启时可用,连接地址:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
ws://127.0.0.1:8080/api/debug/logs/ws
|
ws://127.0.0.1:8080/api/debug/logs/ws
|
||||||
|
|||||||
@@ -7,17 +7,27 @@ import (
|
|||||||
"ckwk/pkg/request"
|
"ckwk/pkg/request"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/antchfx/htmlquery"
|
"github.com/antchfx/htmlquery"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"resty.dev/v3"
|
"resty.dev/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrLoginTimeout = errors.New("登录超时,请重新登录")
|
||||||
|
ErrSessionRemoved = errors.New("session_manager 已删除失效会话")
|
||||||
|
ErrReloginSkipped = errors.New("当前会话缺少账号密码,无法自动重新登录")
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxAutoReloginRetries = 3
|
||||||
|
|
||||||
type WK struct {
|
type WK struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
@@ -29,6 +39,10 @@ type WK struct {
|
|||||||
LoginRegexp *regexp.Regexp
|
LoginRegexp *regexp.Regexp
|
||||||
CourseIDRegexp *regexp.Regexp
|
CourseIDRegexp *regexp.Regexp
|
||||||
TimeRegexp *regexp.Regexp
|
TimeRegexp *regexp.Regexp
|
||||||
|
|
||||||
|
authMu sync.Mutex
|
||||||
|
sessionID string
|
||||||
|
sessionManager *SessionManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWK(username, password, host string, cookies []*http.Cookie) *WK {
|
func NewWK(username, password, host string, cookies []*http.Cookie) *WK {
|
||||||
@@ -40,9 +54,9 @@ func NewWK(username, password, host string, cookies []*http.Cookie) *WK {
|
|||||||
reqCfg := &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",
|
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",
|
||||||
VerifySSL: true,
|
VerifySSL: true,
|
||||||
Debug: conf.IsDebugMode(),
|
Debug: conf.IsRuntimeDebugEnabled(),
|
||||||
}
|
}
|
||||||
if conf.IsDebugMode() {
|
if conf.IsRuntimeDebugEnabled() {
|
||||||
reqCfg.Proxy = conf.DebugProxy
|
reqCfg.Proxy = conf.DebugProxy
|
||||||
reqCfg.VerifySSL = !conf.DebugSkipSSLVerify
|
reqCfg.VerifySSL = !conf.DebugSkipSSLVerify
|
||||||
}
|
}
|
||||||
@@ -63,13 +77,41 @@ func NewWK(username, password, host string, cookies []*http.Cookie) *WK {
|
|||||||
CourseIDRegexp: regexp.MustCompile(`\?courseId=(\d+)`),
|
CourseIDRegexp: regexp.MustCompile(`\?courseId=(\d+)`),
|
||||||
TimeRegexp: regexp.MustCompile(`\d{4}-\d{2}-\d{2}`),
|
TimeRegexp: regexp.MustCompile(`\d{4}-\d{2}-\d{2}`),
|
||||||
}
|
}
|
||||||
if len(cookies) == 0 && username != "" {
|
|
||||||
wk.Login()
|
|
||||||
}
|
|
||||||
|
|
||||||
return wk
|
return wk
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (wk *WK) bindSession(sm *SessionManager, sessionID string) {
|
||||||
|
wk.sessionManager = sm
|
||||||
|
wk.sessionID = sessionID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wk *WK) prepareRequestClient() {
|
||||||
|
if wk == nil || wk.Req == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &request.Config{
|
||||||
|
UserAgent: request.DefaultUserAgent,
|
||||||
|
VerifySSL: true,
|
||||||
|
Debug: conf.IsRuntimeDebugEnabled(),
|
||||||
|
}
|
||||||
|
if conf.IsRuntimeDebugEnabled() {
|
||||||
|
cfg.Proxy = conf.DebugProxy
|
||||||
|
cfg.VerifySSL = !conf.DebugSkipSSLVerify
|
||||||
|
}
|
||||||
|
|
||||||
|
request.ApplyConfig(wk.Req, cfg)
|
||||||
|
if len(wk.Cookies) > 0 {
|
||||||
|
wk.Req.SetCookies(wk.Cookies)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wk *WK) newRequest() *resty.Request {
|
||||||
|
wk.prepareRequestClient()
|
||||||
|
return wk.Req.R()
|
||||||
|
}
|
||||||
|
|
||||||
// Cookies: returns cookies
|
// Cookies: returns cookies
|
||||||
func (wk *WK) Cookie() []*http.Cookie {
|
func (wk *WK) Cookie() []*http.Cookie {
|
||||||
return wk.Cookies
|
return wk.Cookies
|
||||||
@@ -83,8 +125,7 @@ func (wk *WK) SetCookies(cs []*http.Cookie) {
|
|||||||
|
|
||||||
// Code: Get Verify Code
|
// Code: Get Verify Code
|
||||||
func (wk *WK) Code() (string, error) {
|
func (wk *WK) Code() (string, error) {
|
||||||
resp, err := wk.Req.
|
resp, err := wk.newRequest().
|
||||||
R().
|
|
||||||
SetQueryParam("r", fmt.Sprint(common.RandFloat64())).
|
SetQueryParam("r", fmt.Sprint(common.RandFloat64())).
|
||||||
Get(fmt.Sprintf("https://%s/service/code", wk.Host))
|
Get(fmt.Sprintf("https://%s/service/code", wk.Host))
|
||||||
|
|
||||||
@@ -96,8 +137,7 @@ func (wk *WK) Code() (string, error) {
|
|||||||
return "", fmt.Errorf("获取验证码失败: code: %d", resp.StatusCode())
|
return "", fmt.Errorf("获取验证码失败: code: %d", resp.StatusCode())
|
||||||
}
|
}
|
||||||
var result CodeResp
|
var result CodeResp
|
||||||
_, err = wk.Req.
|
_, err = wk.newRequest().
|
||||||
R().
|
|
||||||
SetFormData(map[string]string{
|
SetFormData(map[string]string{
|
||||||
"image": base64.StdEncoding.EncodeToString(resp.Bytes()),
|
"image": base64.StdEncoding.EncodeToString(resp.Bytes()),
|
||||||
"probability": "false",
|
"probability": "false",
|
||||||
@@ -128,8 +168,7 @@ func (wk *WK) Login() (bool, error) {
|
|||||||
return false, fmt.Errorf("以达到最大重试次数,验证码获取失败,登录终止。")
|
return false, fmt.Errorf("以达到最大重试次数,验证码获取失败,登录终止。")
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := wk.Req.
|
resp, err := wk.newRequest().
|
||||||
R().
|
|
||||||
SetFormData(map[string]string{
|
SetFormData(map[string]string{
|
||||||
"username": wk.Username,
|
"username": wk.Username,
|
||||||
"password": wk.Password,
|
"password": wk.Password,
|
||||||
@@ -141,25 +180,50 @@ func (wk *WK) Login() (bool, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("请求登录失败: %w", err)
|
return false, fmt.Errorf("请求登录失败: %w", err)
|
||||||
}
|
}
|
||||||
matchs := wk.LoginRegexp.FindStringSubmatch(string(resp.Bytes()))
|
result, err := wk.parseLoginResp(resp.Bytes())
|
||||||
if len(matchs) <= 1 {
|
|
||||||
return false, fmt.Errorf("没有找到匹配字符串")
|
|
||||||
}
|
|
||||||
|
|
||||||
var result LoginResp
|
|
||||||
err = json.Unmarshal([]byte(matchs[1]), &result)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("解析 json data 失败: %w", err)
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !result.Status {
|
if !result.Status {
|
||||||
return false, fmt.Errorf("登录失败: %s", result.Msg)
|
return false, fmt.Errorf("登录失败: %s", loginErrorMessage(result))
|
||||||
}
|
}
|
||||||
wk.SetCookies(resp.Cookies())
|
wk.SetCookies(resp.Cookies())
|
||||||
log.Info("登录成功", zap.Any("cookies", wk.Cookies))
|
log.Info("登录成功", zap.Any("cookies", wk.Req.Cookies()))
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (wk *WK) parseLoginResp(body []byte) (LoginResp, error) {
|
||||||
|
var result LoginResp
|
||||||
|
if len(body) == 0 {
|
||||||
|
return result, fmt.Errorf("登录响应为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
matchs := wk.LoginRegexp.FindStringSubmatch(string(body))
|
||||||
|
if len(matchs) > 1 {
|
||||||
|
if err := json.Unmarshal([]byte(strings.TrimSpace(matchs[1])), &result); err != nil {
|
||||||
|
return result, fmt.Errorf("解析登录响应失败: %w", err)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(body, &result); err != nil {
|
||||||
|
return result, fmt.Errorf("解析登录响应失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loginErrorMessage(result LoginResp) string {
|
||||||
|
if msg := strings.TrimSpace(result.FormError.Code); msg != "" {
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
if msg := strings.TrimSpace(result.Msg); msg != "" {
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
return "未知错误"
|
||||||
|
}
|
||||||
|
|
||||||
// CourseParse: 课程解析
|
// CourseParse: 课程解析
|
||||||
func (wk *WK) CourseParse(content string) ([]Course, error) {
|
func (wk *WK) CourseParse(content string) ([]Course, error) {
|
||||||
courses := make([]Course, 0)
|
courses := make([]Course, 0)
|
||||||
@@ -237,8 +301,7 @@ func (wk *WK) CourseParse(content string) ([]Course, error) {
|
|||||||
// CourseGet: 课程获取
|
// CourseGet: 课程获取
|
||||||
func (wk *WK) CourseGet(kind CourseKind) ([]Course, error) {
|
func (wk *WK) CourseGet(kind CourseKind) ([]Course, error) {
|
||||||
var courses []Course
|
var courses []Course
|
||||||
resp, err := wk.Req.
|
resp, err := wk.newRequest().
|
||||||
R().
|
|
||||||
SetQueryParam("kind", string(kind)).
|
SetQueryParam("kind", string(kind)).
|
||||||
Get(fmt.Sprintf("https://%s/user/index", wk.Host))
|
Get(fmt.Sprintf("https://%s/user/index", wk.Host))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -292,8 +355,7 @@ func (wk *WK) UserInfoParse(content string) (User, error) {
|
|||||||
// UserGet: 用户信息获取
|
// UserGet: 用户信息获取
|
||||||
func (wk *WK) UserInfoGet() (User, error) {
|
func (wk *WK) UserInfoGet() (User, error) {
|
||||||
var user User
|
var user User
|
||||||
resp, err := wk.Req.
|
resp, err := wk.newRequest().
|
||||||
R().
|
|
||||||
Get(fmt.Sprintf("https://%s/user/member", wk.Host))
|
Get(fmt.Sprintf("https://%s/user/member", wk.Host))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, fmt.Errorf("获取用户信息页面失败: %w", err)
|
return user, fmt.Errorf("获取用户信息页面失败: %w", err)
|
||||||
@@ -309,9 +371,8 @@ func (wk *WK) UserInfoGet() (User, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Online: 保持账号状态
|
// Online: 保持账号状态
|
||||||
func (wk *WK) Online() (bool, error) {
|
func (wk *WK) performOnline() (bool, error) {
|
||||||
resp, err := wk.Req.
|
resp, err := wk.newRequest().
|
||||||
R().
|
|
||||||
SetHeaders(map[string]string{
|
SetHeaders(map[string]string{
|
||||||
"x-requested-with": "XMLHttpRequest",
|
"x-requested-with": "XMLHttpRequest",
|
||||||
"Accept": "application/json, text/javascript, */*; q=0.01",
|
"Accept": "application/json, text/javascript, */*; q=0.01",
|
||||||
@@ -321,12 +382,19 @@ func (wk *WK) Online() (bool, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("保持账号状态失败: %w", err)
|
return false, fmt.Errorf("保持账号状态失败: %w", err)
|
||||||
}
|
}
|
||||||
|
if isLoginTimeoutBody(resp.Bytes()) {
|
||||||
|
return false, fmt.Errorf("保持账号状态失败: %w", ErrLoginTimeout)
|
||||||
|
}
|
||||||
log.Info("保持账号状态", zap.Any("resp", string(resp.Bytes())))
|
log.Info("保持账号状态", zap.Any("resp", string(resp.Bytes())))
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (wk *WK) Online() (bool, error) {
|
||||||
|
return withAutoRelogin(wk, "保持账号状态", wk.performOnline)
|
||||||
|
}
|
||||||
|
|
||||||
// Study: 学习
|
// Study: 学习
|
||||||
func (wk *WK) Study(nodeID, studyID, studyTime string, status StudyStatus) (*StudyResp, error) {
|
func (wk *WK) performStudy(nodeID, studyID, studyTime string, status StudyStatus) (*StudyResp, error) {
|
||||||
var data map[string]string
|
var data map[string]string
|
||||||
switch status {
|
switch status {
|
||||||
case StudyStart:
|
case StudyStart:
|
||||||
@@ -363,8 +431,7 @@ func (wk *WK) Study(nodeID, studyID, studyTime string, status StudyStatus) (*Stu
|
|||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("传入的学习状态不匹配")
|
return nil, fmt.Errorf("传入的学习状态不匹配")
|
||||||
}
|
}
|
||||||
resp, err := wk.Req.
|
resp, err := wk.newRequest().
|
||||||
R().
|
|
||||||
SetHeaders(map[string]string{
|
SetHeaders(map[string]string{
|
||||||
"x-requested-with": "XMLHttpRequest",
|
"x-requested-with": "XMLHttpRequest",
|
||||||
"Accept": "application/json, text/javascript, */*; q=0.01",
|
"Accept": "application/json, text/javascript, */*; q=0.01",
|
||||||
@@ -376,6 +443,9 @@ func (wk *WK) Study(nodeID, studyID, studyTime string, status StudyStatus) (*Stu
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("进行学习失败: %w", err)
|
return nil, fmt.Errorf("进行学习失败: %w", err)
|
||||||
}
|
}
|
||||||
|
if isLoginTimeoutBody(resp.Bytes()) {
|
||||||
|
return nil, fmt.Errorf("进行学习失败: %w", ErrLoginTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
var result StudyResp
|
var result StudyResp
|
||||||
if err := json.Unmarshal(resp.Bytes(), &result); err != nil {
|
if err := json.Unmarshal(resp.Bytes(), &result); err != nil {
|
||||||
@@ -386,6 +456,12 @@ func (wk *WK) Study(nodeID, studyID, studyTime string, status StudyStatus) (*Stu
|
|||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (wk *WK) Study(nodeID, studyID, studyTime string, status StudyStatus) (*StudyResp, error) {
|
||||||
|
return withAutoRelogin(wk, "进行学习", func() (*StudyResp, error) {
|
||||||
|
return wk.performStudy(nodeID, studyID, studyTime, status)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// GetStudyList: 获取学习记录
|
// GetStudyList: 获取学习记录
|
||||||
func (wk *WK) GetStudyList(courseID, page string) (*AllRecordResp[StudyList], error) {
|
func (wk *WK) GetStudyList(courseID, page string) (*AllRecordResp[StudyList], error) {
|
||||||
return GetRecords[StudyList](wk, RecordStudy, courseID, page)
|
return GetRecords[StudyList](wk, RecordStudy, courseID, page)
|
||||||
@@ -407,3 +483,101 @@ func (wk *WK) GetExamList(courseID, page string) (*AllRecordResp[ExamList], erro
|
|||||||
func (wk *WK) GetDiscussList(courseID, page string) (*AllRecordResp[ExamList], error) {
|
func (wk *WK) GetDiscussList(courseID, page string) (*AllRecordResp[ExamList], error) {
|
||||||
return GetRecords[ExamList](wk, RecordStudy, courseID, page)
|
return GetRecords[ExamList](wk, RecordStudy, courseID, page)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isLoginTimeoutBody(body []byte) bool {
|
||||||
|
if len(body) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload struct {
|
||||||
|
Offline int `json:"offline"`
|
||||||
|
Status bool `json:"status"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &payload); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload.Offline == 1 && !payload.Status && strings.Contains(payload.Msg, "登录超时")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wk *WK) reloginForExpiredSession(action string, attempt int) error {
|
||||||
|
wk.authMu.Lock()
|
||||||
|
defer wk.authMu.Unlock()
|
||||||
|
|
||||||
|
if strings.TrimSpace(wk.Username) == "" || strings.TrimSpace(wk.Password) == "" {
|
||||||
|
return fmt.Errorf("%w: 检测到 %s 登录超时", ErrReloginSkipped, action)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warn("检测到登录超时,开始自动重新登录",
|
||||||
|
zap.String("action", action),
|
||||||
|
zap.String("host", wk.Host),
|
||||||
|
zap.String("username", wk.Username),
|
||||||
|
zap.Int("attempt", attempt),
|
||||||
|
zap.Int("maxAttempts", maxAutoReloginRetries),
|
||||||
|
)
|
||||||
|
|
||||||
|
ok, err := wk.Login()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("第 %d 次自动重新登录失败: %w", attempt, err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("第 %d 次自动重新登录失败", attempt)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("自动重新登录成功",
|
||||||
|
zap.String("action", action),
|
||||||
|
zap.String("host", wk.Host),
|
||||||
|
zap.String("username", wk.Username),
|
||||||
|
zap.Int("attempt", attempt),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wk *WK) removeBoundSession(action string, cause error) {
|
||||||
|
if wk == nil || wk.sessionManager == nil || wk.sessionID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warn("自动重新登录失败,删除失效会话",
|
||||||
|
zap.String("action", action),
|
||||||
|
zap.String("session_id", wk.sessionID),
|
||||||
|
zap.String("host", wk.Host),
|
||||||
|
zap.String("username", wk.Username),
|
||||||
|
zap.Error(cause),
|
||||||
|
)
|
||||||
|
wk.sessionManager.Del(wk.sessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func withAutoRelogin[T any](wk *WK, action string, fn func() (T, error)) (T, error) {
|
||||||
|
result, err := fn()
|
||||||
|
if !errors.Is(err, ErrLoginTimeout) {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lastErr := err
|
||||||
|
for attempt := 1; attempt <= maxAutoReloginRetries; attempt++ {
|
||||||
|
reloginErr := wk.reloginForExpiredSession(action, attempt)
|
||||||
|
if reloginErr != nil {
|
||||||
|
lastErr = reloginErr
|
||||||
|
if errors.Is(reloginErr, ErrReloginSkipped) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err = fn()
|
||||||
|
if !errors.Is(err, ErrLoginTimeout) {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
}
|
||||||
|
|
||||||
|
wk.removeBoundSession(action, lastErr)
|
||||||
|
|
||||||
|
var zero T
|
||||||
|
if lastErr == nil {
|
||||||
|
lastErr = ErrLoginTimeout
|
||||||
|
}
|
||||||
|
return zero, fmt.Errorf("%w: %s 自动重新登录 %d 次后仍未恢复: %v", ErrSessionRemoved, action, maxAutoReloginRetries, lastErr)
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,12 +7,17 @@ type CodeResp struct {
|
|||||||
Data string `json:"data"`
|
Data string `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LoginFormError struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
}
|
||||||
|
|
||||||
// 登录响应
|
// 登录响应
|
||||||
type LoginResp struct {
|
type LoginResp struct {
|
||||||
RefreshCode int `json:"refresh_code"`
|
RefreshCode int `json:"refresh_code"`
|
||||||
Status bool `json:"status"`
|
Status bool `json:"status"`
|
||||||
Msg string `json:"msg"`
|
Msg string `json:"msg"`
|
||||||
Back string `json:"back"`
|
Back string `json:"back"`
|
||||||
|
FormError LoginFormError `json:"formError"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type StudyResp struct {
|
type StudyResp struct {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ func (m *SessionManager) Store(wk *WK) string {
|
|||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
item.LastValue = time.Now()
|
item.LastValue = time.Now()
|
||||||
|
wk.bindSession(m, oldID)
|
||||||
item.Instance = wk
|
item.Instance = wk
|
||||||
item.cancel = cancel
|
item.cancel = cancel
|
||||||
m.sessions[oldID] = item
|
m.sessions[oldID] = item
|
||||||
@@ -59,6 +60,7 @@ func (m *SessionManager) Store(wk *WK) string {
|
|||||||
|
|
||||||
sessionID := uuid.New().String()
|
sessionID := uuid.New().String()
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
wk.bindSession(m, sessionID)
|
||||||
|
|
||||||
m.userToSession[userKey] = sessionID
|
m.userToSession[userKey] = sessionID
|
||||||
m.sessions[sessionID] = SessionItem{
|
m.sessions[sessionID] = SessionItem{
|
||||||
|
|||||||
@@ -51,8 +51,7 @@ type Course struct {
|
|||||||
|
|
||||||
func GetRecords[T any](wk *WK, rType RecordType, courseID, page string) (*AllRecordResp[T], error) {
|
func GetRecords[T any](wk *WK, rType RecordType, courseID, page string) (*AllRecordResp[T], error) {
|
||||||
log.Debug("获取记录信息", zap.String("host", wk.Host), zap.String("type", string(rType)))
|
log.Debug("获取记录信息", zap.String("host", wk.Host), zap.String("type", string(rType)))
|
||||||
resp, err := wk.Req.
|
resp, err := wk.newRequest().
|
||||||
R().
|
|
||||||
SetQueryParams(map[string]string{
|
SetQueryParams(map[string]string{
|
||||||
"courseId": courseID,
|
"courseId": courseID,
|
||||||
"page": page,
|
"page": page,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package conf
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 构建信息
|
// 构建信息
|
||||||
@@ -17,23 +18,30 @@ var (
|
|||||||
|
|
||||||
DebugProxy string = ""
|
DebugProxy string = ""
|
||||||
DebugSkipSSLVerify bool = false
|
DebugSkipSSLVerify bool = false
|
||||||
|
|
||||||
|
runtimeDebugEnabled atomic.Bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if !IsDebugMode() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if proxy := os.Getenv("CKWK_DEBUG_PROXY"); proxy != "" {
|
if proxy := os.Getenv("CKWK_DEBUG_PROXY"); proxy != "" {
|
||||||
DebugProxy = proxy
|
DebugProxy = proxy
|
||||||
}
|
}
|
||||||
DebugSkipSSLVerify = parseEnvBool("CKWK_DEBUG_SKIP_SSL_VERIFY")
|
DebugSkipSSLVerify = parseEnvBool("CKWK_DEBUG_SKIP_SSL_VERIFY")
|
||||||
|
runtimeDebugEnabled.Store(parseEnvBool("CKWK_DEBUG_ENABLED"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsDebugMode() bool {
|
func IsBuildDebugMode() bool {
|
||||||
return !strings.EqualFold(Mode, "release")
|
return !strings.EqualFold(Mode, "release")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsRuntimeDebugEnabled() bool {
|
||||||
|
return runtimeDebugEnabled.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetRuntimeDebugEnabled(enabled bool) {
|
||||||
|
runtimeDebugEnabled.Store(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
func parseEnvBool(key string) bool {
|
func parseEnvBool(key string) bool {
|
||||||
value := strings.TrimSpace(os.Getenv(key))
|
value := strings.TrimSpace(os.Getenv(key))
|
||||||
switch strings.ToLower(value) {
|
switch strings.ToLower(value) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"ckwk/internal/ckwk"
|
"ckwk/internal/ckwk"
|
||||||
"ckwk/internal/dto"
|
"ckwk/internal/dto"
|
||||||
"ckwk/pkg/log"
|
"ckwk/pkg/log"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@@ -44,25 +45,22 @@ func (h *WKHandler) Login(ctx *gin.Context) {
|
|||||||
ctx.JSON(200, dto.Error(-1, "登录失败:请提供账号密码或有效的 Token,并确保 Host 正确"))
|
ctx.JSON(200, dto.Error(-1, "登录失败:请提供账号密码或有效的 Token,并确保 Host 正确"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if req.Token == "" {
|
||||||
userinfo, err := wk.UserInfoGet()
|
ok, err := wk.Login()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.JSON(200, dto.Error(-1, err.Error()))
|
ctx.JSON(200, dto.Error(-1, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !ok {
|
||||||
courses, err := wk.CourseGet(req.Status)
|
ctx.JSON(200, dto.Error(-1, "登录失败"))
|
||||||
if err != nil {
|
return
|
||||||
ctx.JSON(200, dto.Error(-1, err.Error()))
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionID := h.Session.Store(wk)
|
sessionID := h.Session.Store(wk)
|
||||||
|
|
||||||
ctx.JSON(200, dto.Success(map[string]any{
|
ctx.JSON(200, dto.Success(map[string]any{
|
||||||
"session_id": sessionID,
|
"session_id": sessionID,
|
||||||
"user": userinfo,
|
|
||||||
"courses": courses,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +74,10 @@ func (h *WKHandler) Online(ctx *gin.Context) {
|
|||||||
|
|
||||||
flag, err := wk.Online()
|
flag, err := wk.Online()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, ckwk.ErrSessionRemoved) {
|
||||||
|
ctx.JSON(http.StatusUnauthorized, dto.Error(401, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.JSON(200, dto.Error(-1, err.Error()))
|
ctx.JSON(200, dto.Error(-1, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -153,6 +155,10 @@ func (h *WKHandler) Study(ctx *gin.Context) {
|
|||||||
|
|
||||||
result, err := wk.Study(req.NodeID, req.StudyID, req.StudyTime, req.Status)
|
result, err := wk.Study(req.NodeID, req.StudyID, req.StudyTime, req.Status)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, ckwk.ErrSessionRemoved) {
|
||||||
|
ctx.JSON(http.StatusUnauthorized, dto.Error(401, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.JSON(200, dto.Error(-1, err.Error()))
|
ctx.JSON(200, dto.Error(-1, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"ckwk/internal/conf"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -19,13 +20,44 @@ var debugLogUpgrader = websocket.Upgrader{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type debugConfigReq struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DebugConfig(ctx *gin.Context) {
|
||||||
|
ctx.JSON(http.StatusOK, dto.Success(map[string]any{
|
||||||
|
"enabled": conf.IsRuntimeDebugEnabled(),
|
||||||
|
"proxy": conf.DebugProxy,
|
||||||
|
"skip_ssl_verify": conf.DebugSkipSSLVerify,
|
||||||
|
"build_mode": conf.Mode,
|
||||||
|
"proxy_configured": conf.DebugProxy != "",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateDebugConfig(ctx *gin.Context) {
|
||||||
|
var req debugConfigReq
|
||||||
|
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||||
|
ctx.JSON(http.StatusBadRequest, dto.Error(400, "请求参数错误"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conf.SetRuntimeDebugEnabled(req.Enabled)
|
||||||
|
DebugConfig(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
func DebugLogs(ctx *gin.Context) {
|
func DebugLogs(ctx *gin.Context) {
|
||||||
|
if !ensureDebugEnabled(ctx) {
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.JSON(http.StatusOK, dto.Success(map[string]any{
|
ctx.JSON(http.StatusOK, dto.Success(map[string]any{
|
||||||
"list": log.Entries(),
|
"list": log.Entries(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func DebugLogsDownload(ctx *gin.Context) {
|
func DebugLogsDownload(ctx *gin.Context) {
|
||||||
|
if !ensureDebugEnabled(ctx) {
|
||||||
|
return
|
||||||
|
}
|
||||||
entries := log.Entries()
|
entries := log.Entries()
|
||||||
content, err := json.MarshalIndent(entries, "", " ")
|
content, err := json.MarshalIndent(entries, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -40,6 +72,9 @@ func DebugLogsDownload(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DebugLogWS(ctx *gin.Context) {
|
func DebugLogWS(ctx *gin.Context) {
|
||||||
|
if !ensureDebugEnabled(ctx) {
|
||||||
|
return
|
||||||
|
}
|
||||||
conn, err := debugLogUpgrader.Upgrade(ctx.Writer, ctx.Request, nil)
|
conn, err := debugLogUpgrader.Upgrade(ctx.Writer, ctx.Request, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -86,3 +121,12 @@ func DebugLogWS(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ensureDebugEnabled(ctx *gin.Context) bool {
|
||||||
|
if conf.IsRuntimeDebugEnabled() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusForbidden, dto.Error(403, "调试功能未开启,请先在设置页手动开启"))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Version(ctx *gin.Context) {
|
func Version(ctx *gin.Context) {
|
||||||
ctx.JSON(200, dto.Success(map[string]string{
|
ctx.JSON(200, dto.Success(map[string]any{
|
||||||
"Mode": conf.Mode,
|
"Mode": conf.Mode,
|
||||||
"Version": conf.Version,
|
"Version": conf.Version,
|
||||||
"BuildAt": conf.BuildAt,
|
"BuildAt": conf.BuildAt,
|
||||||
"GitAuthor": conf.GitAuthor,
|
"GitAuthor": conf.GitAuthor,
|
||||||
"GitEmail": conf.GitEmail,
|
"GitEmail": conf.GitEmail,
|
||||||
"GitCommit": conf.GitCommit,
|
"GitCommit": conf.GitCommit,
|
||||||
|
"DebugEnabled": conf.IsRuntimeDebugEnabled(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"ckwk/internal/conf"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -32,7 +33,12 @@ func (w *debugBodyWriter) WriteString(data string) (int, error) {
|
|||||||
|
|
||||||
func DebugHTTPLog() gin.HandlerFunc {
|
func DebugHTTPLog() gin.HandlerFunc {
|
||||||
return func(ctx *gin.Context) {
|
return func(ctx *gin.Context) {
|
||||||
if isDebugLogRoute(ctx.Request.URL.Path) {
|
if !conf.IsRuntimeDebugEnabled() {
|
||||||
|
ctx.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !shouldCaptureBackendRoute(ctx.Request.URL.Path) {
|
||||||
ctx.Next()
|
ctx.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -85,5 +91,13 @@ func truncate(value string, limit int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isDebugLogRoute(path string) bool {
|
func isDebugLogRoute(path string) bool {
|
||||||
return strings.HasPrefix(path, "/api/debug/ws/logs")
|
return strings.HasPrefix(path, "/api/debug/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldCaptureBackendRoute(path string) bool {
|
||||||
|
if !strings.HasPrefix(path, "/api/") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return !isDebugLogRoute(path)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if conf.Mode != gin.ReleaseMode {
|
if conf.IsBuildDebugMode() {
|
||||||
AllowOrigins = []string{"*"}
|
AllowOrigins = []string{"*"}
|
||||||
AllowMethods = []string{"*"}
|
AllowMethods = []string{"*"}
|
||||||
AllowHeaders = []string{"*"}
|
AllowHeaders = []string{"*"}
|
||||||
@@ -48,9 +48,7 @@ func SetupRouter() *gin.Engine {
|
|||||||
AllowCredentials: true,
|
AllowCredentials: true,
|
||||||
MaxAge: 12 * time.Hour,
|
MaxAge: 12 * time.Hour,
|
||||||
}))
|
}))
|
||||||
if conf.IsDebugMode() {
|
r.Use(middleware.DebugHTTPLog())
|
||||||
r.Use(middleware.DebugHTTPLog())
|
|
||||||
}
|
|
||||||
wkHandler := handler.NewWKHandler()
|
wkHandler := handler.NewWKHandler()
|
||||||
sessionMiddleware := middleware.SessionMiddleware(wkHandler.Session)
|
sessionMiddleware := middleware.SessionMiddleware(wkHandler.Session)
|
||||||
// schedule.StartCron(wkHandler.Session)
|
// schedule.StartCron(wkHandler.Session)
|
||||||
@@ -68,14 +66,14 @@ func SetupRouter() *gin.Engine {
|
|||||||
{
|
{
|
||||||
api.POST("/login", wkHandler.Login)
|
api.POST("/login", wkHandler.Login)
|
||||||
api.Any("/version", handler.Version)
|
api.Any("/version", handler.Version)
|
||||||
if conf.IsDebugMode() {
|
debug := api.Group("/debug")
|
||||||
debug := api.Group("/debug")
|
{
|
||||||
{
|
debug.GET("/config", handler.DebugConfig)
|
||||||
debug.GET("/logs", handler.DebugLogs)
|
debug.POST("/config", handler.UpdateDebugConfig)
|
||||||
debug.GET("/logs/download", handler.DebugLogsDownload)
|
debug.GET("/logs", handler.DebugLogs)
|
||||||
debug.GET("/logs/ws", handler.DebugLogWS)
|
debug.GET("/logs/download", handler.DebugLogsDownload)
|
||||||
debug.GET("/ws/logs", handler.DebugLogWS)
|
debug.GET("/logs/ws", handler.DebugLogWS)
|
||||||
}
|
debug.GET("/ws/logs", handler.DebugLogWS)
|
||||||
}
|
}
|
||||||
v1 := api.Group("/v1")
|
v1 := api.Group("/v1")
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ckwk/pkg/log"
|
"ckwk/pkg/log"
|
||||||
@@ -19,7 +20,7 @@ var (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
DefaultUserAgent = "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"
|
DefaultUserAgent = "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"
|
||||||
DefaultTimeout = 10 * time.Second
|
DefaultTimeout = 30 * time.Second
|
||||||
DefaultDebugBody = 4 * 1024
|
DefaultDebugBody = 4 * 1024
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,67 +41,91 @@ func DefaultConfg() *Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient 创建一个标准的 Resty 客户端
|
func normalizeConfig(cfg *Config) *Config {
|
||||||
func NewClient(cfg *Config) *resty.Client {
|
|
||||||
defaults := DefaultConfg()
|
defaults := DefaultConfg()
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
cfg = defaults
|
return defaults
|
||||||
} else {
|
}
|
||||||
// 合并零值,避免调用方只覆盖部分字段时丢失默认超时和 User-Agent。
|
|
||||||
if cfg.Timeout <= 0 {
|
if cfg.Timeout <= 0 {
|
||||||
cfg.Timeout = defaults.Timeout
|
cfg.Timeout = defaults.Timeout
|
||||||
}
|
}
|
||||||
if cfg.UserAgent == "" {
|
if cfg.UserAgent == "" {
|
||||||
cfg.UserAgent = defaults.UserAgent
|
cfg.UserAgent = defaults.UserAgent
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTransport(cfg *Config) *http.Transport {
|
||||||
|
baseTransport, ok := http.DefaultTransport.(*http.Transport)
|
||||||
|
if !ok {
|
||||||
|
baseTransport = &http.Transport{}
|
||||||
|
}
|
||||||
|
|
||||||
|
transport := baseTransport.Clone()
|
||||||
|
transport.TLSClientConfig = &tls.Config{
|
||||||
|
InsecureSkipVerify: !cfg.VerifySSL,
|
||||||
|
}
|
||||||
|
transport.Proxy = http.ProxyFromEnvironment
|
||||||
|
|
||||||
|
if cfg.Proxy != "" {
|
||||||
|
proxyURL, err := url.Parse(cfg.Proxy)
|
||||||
|
if err == nil {
|
||||||
|
transport.Proxy = http.ProxyURL(proxyURL)
|
||||||
|
} else {
|
||||||
|
log.Warn("代理地址解析失败,已忽略")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client := resty.New()
|
return transport
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApplyConfig(client *resty.Client, cfg *Config) {
|
||||||
|
cfg = normalizeConfig(cfg)
|
||||||
|
|
||||||
client.SetHeader("User-Agent", cfg.UserAgent)
|
client.SetHeader("User-Agent", cfg.UserAgent)
|
||||||
client.SetTimeout(cfg.Timeout)
|
client.SetTimeout(cfg.Timeout)
|
||||||
client.SetRetryCount(3)
|
client.SetRetryCount(3)
|
||||||
|
client.SetTransport(buildTransport(cfg))
|
||||||
|
client.SetDebug(cfg.Debug)
|
||||||
|
client.SetDebugBodyLimit(DefaultDebugBody)
|
||||||
|
}
|
||||||
|
|
||||||
client.SetTLSClientConfig(&tls.Config{
|
// NewClient 创建一个标准的 Resty 客户端
|
||||||
InsecureSkipVerify: !cfg.VerifySSL,
|
func NewClient(cfg *Config) *resty.Client {
|
||||||
|
client := resty.New()
|
||||||
|
client.OnDebugLog(func(debugLog *resty.DebugLog) {
|
||||||
|
fields := map[string]any{
|
||||||
|
"request": map[string]any{
|
||||||
|
"host": debugLog.Request.Host,
|
||||||
|
"uri": debugLog.Request.URI,
|
||||||
|
"method": debugLog.Request.Method,
|
||||||
|
"proto": debugLog.Request.Proto,
|
||||||
|
"header": log.SanitizeHeaders(debugLog.Request.Header),
|
||||||
|
"attempt": debugLog.Request.Attempt,
|
||||||
|
"body": log.SanitizeBody(debugLog.Request.Header.Get("Content-Type"), debugLog.Request.Body),
|
||||||
|
},
|
||||||
|
"response": map[string]any{
|
||||||
|
"statusCode": debugLog.Response.StatusCode,
|
||||||
|
"status": debugLog.Response.Status,
|
||||||
|
"proto": debugLog.Response.Proto,
|
||||||
|
"receivedAt": debugLog.Response.ReceivedAt.Format(time.RFC3339Nano),
|
||||||
|
"durationMs": debugLog.Response.Duration.Milliseconds(),
|
||||||
|
"size": debugLog.Response.Size,
|
||||||
|
"header": log.SanitizeHeaders(debugLog.Response.Header),
|
||||||
|
"body": log.SanitizeBody(debugLog.Response.Header.Get("Content-Type"), debugLog.Response.Body),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if debugLog.TraceInfo != nil {
|
||||||
|
if traceJSON, err := json.Marshal(debugLog.TraceInfo); err == nil {
|
||||||
|
fields["trace"] = json.RawMessage(traceJSON)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Capture(zapcore.DebugLevel, "resty", "outbound exchange", fields)
|
||||||
})
|
})
|
||||||
|
client.SetDebugLogFormatter(nil)
|
||||||
if cfg.Proxy != "" {
|
ApplyConfig(client, cfg)
|
||||||
client.SetProxy(cfg.Proxy)
|
|
||||||
}
|
|
||||||
if cfg.Debug {
|
|
||||||
client.SetDebug(true)
|
|
||||||
client.SetDebugBodyLimit(DefaultDebugBody)
|
|
||||||
client.OnDebugLog(func(debugLog *resty.DebugLog) {
|
|
||||||
fields := map[string]any{
|
|
||||||
"request": map[string]any{
|
|
||||||
"host": debugLog.Request.Host,
|
|
||||||
"uri": debugLog.Request.URI,
|
|
||||||
"method": debugLog.Request.Method,
|
|
||||||
"proto": debugLog.Request.Proto,
|
|
||||||
"header": log.SanitizeHeaders(debugLog.Request.Header),
|
|
||||||
"attempt": debugLog.Request.Attempt,
|
|
||||||
"body": log.SanitizeBody(debugLog.Request.Header.Get("Content-Type"), debugLog.Request.Body),
|
|
||||||
},
|
|
||||||
"response": map[string]any{
|
|
||||||
"statusCode": debugLog.Response.StatusCode,
|
|
||||||
"status": debugLog.Response.Status,
|
|
||||||
"proto": debugLog.Response.Proto,
|
|
||||||
"receivedAt": debugLog.Response.ReceivedAt.Format(time.RFC3339Nano),
|
|
||||||
"durationMs": debugLog.Response.Duration.Milliseconds(),
|
|
||||||
"size": debugLog.Response.Size,
|
|
||||||
"header": log.SanitizeHeaders(debugLog.Response.Header),
|
|
||||||
"body": log.SanitizeBody(debugLog.Response.Header.Get("Content-Type"), debugLog.Response.Body),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if debugLog.TraceInfo != nil {
|
|
||||||
if traceJSON, err := json.Marshal(debugLog.TraceInfo); err == nil {
|
|
||||||
fields["trace"] = json.RawMessage(traceJSON)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Capture(zapcore.DebugLevel, "resty", "outbound exchange", fields)
|
|
||||||
})
|
|
||||||
client.SetDebugLogFormatter(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|||||||
Submodule web/frontend updated: 1396592141...13f0be162b
Reference in New Issue
Block a user