feat: 重构HTTP/TCP客户端并增强全局设置与并发安全

This commit is contained in:
mogumc committed 2026-05-27 20:34:52 +08:00
1 parent ad5884804c
commit 2e9b2f8050
11 files changed
+580 -347

No files matched your search

+142 -212
View File
@@ -14,6 +14,17 @@ import (
"time"
)
// maxBodySize 限制响应体最大读取量为 50MB,防止 OOM
const maxBodySize = 50 << 20
// 默认 HTTP 客户端,复用连接池
var defaultHTTPClient = &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
},
}
// HttpRequest 请求结构体
type HttpRequest struct {
Method string `json:"method"` // HTTP 方法
@@ -64,39 +75,40 @@ type FileData struct {
Path string `json:"path"` // 文件路径
}
// SendRequest 发送 HTTP 请求
func SendRequest(req *HttpRequest) *HttpResponse {
resp := &HttpResponse{
Headers: make(map[string]string),
}
if req.URL == "" {
resp.Error = "请求地址不能为空"
return resp
}
// 补全协议
// normalizeURL 补全协议前缀
func normalizeURL(req *HttpRequest) {
if !strings.HasPrefix(req.URL, "http://") && !strings.HasPrefix(req.URL, "https://") {
req.URL = "https://" + req.URL
}
}
// 设置超时
// buildHTTPClient 创建带代理和 TLS 配置的 HTTP 客户端
// 无自定义代理且不跳过验证时复用默认客户端
func buildHTTPClient(req *HttpRequest) *http.Client {
// 无自定义设置时复用默认客户端(共享连接池)
if req.Proxy == "" && !req.Insecure {
if req.Timeout > 0 {
return &http.Client{
Timeout: time.Duration(req.Timeout) * time.Second,
Transport: defaultHTTPClient.Transport,
}
}
return defaultHTTPClient
}
// 有自定义设置时创建新客户端
timeout := time.Duration(req.Timeout) * time.Second
if timeout == 0 {
timeout = 30 * time.Second
}
// 创建 HTTP 客户端
client := &http.Client{
Timeout: timeout,
}
client := &http.Client{Timeout: timeout}
// 配置代理
if req.Proxy != "" {
proxyURL, err := url.Parse(req.Proxy)
if err != nil {
resp.Error = fmt.Sprintf("代理地址格式错误: %v", err)
return resp
// 代理格式错误,降级为默认客户端
return defaultHTTPClient
}
client.Transport = &http.Transport{
Proxy: http.ProxyURL(proxyURL),
@@ -106,72 +118,108 @@ func SendRequest(req *HttpRequest) *HttpResponse {
}
} else if req.Insecure {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
// 创建请求体
return client
}
// setupCookies 将请求中的 Cookie 设置到客户端 Jar
func setupCookies(client *http.Client, req *HttpRequest) {
if len(req.Cookies) == 0 {
return
}
cookieJar, _ := cookiejar.New(nil)
var cookieList []*http.Cookie
for name, value := range req.Cookies {
cookieList = append(cookieList, &http.Cookie{Name: name, Value: value})
}
parsedURL, _ := url.Parse(req.URL)
cookieJar.SetCookies(parsedURL, cookieList)
client.Jar = cookieJar
}
// setRequestHeaders 设置自定义请求头(host 单独处理)
func setRequestHeaders(httpReq *http.Request, req *HttpRequest) {
for key, value := range req.Headers {
if strings.ToLower(key) == "host" {
httpReq.Host = value
} else {
httpReq.Header.Set(key, value)
}
}
}
// parseHTTPResponse 从 http.Response 解析到 HttpResponse
func parseHTTPResponse(httpResp *http.Response, resp *HttpResponse, start time.Time) {
resp.Time = time.Since(start).String()
// 限制最大读取量防止 OOM
body, err := io.ReadAll(io.LimitReader(httpResp.Body, maxBodySize))
if err != nil {
resp.Error = fmt.Sprintf("读取响应失败: %v", err)
return
}
resp.StatusCode = httpResp.StatusCode
resp.Status = httpResp.Status
resp.Body = string(body)
resp.ContentLength = httpResp.ContentLength
resp.Size = int64(len(body))
resp.ContentType = httpResp.Header.Get("Content-Type")
resp.Headers = make(map[string]string)
for key, values := range httpResp.Header {
resp.Headers[key] = strings.Join(values, "; ")
}
resp.Cookies = convertCookies(httpResp.Cookies())
}
// SendRequest 发送 HTTP 请求
func SendRequest(req *HttpRequest) *HttpResponse {
resp := &HttpResponse{}
if req.URL == "" {
resp.Error = "请求地址不能为空"
return resp
}
normalizeURL(req)
client := buildHTTPClient(req)
// 构造请求体
var bodyReader io.Reader
var err error
contentType := req.ContentType
if req.Method != http.MethodGet && req.Method != http.MethodHead {
switch req.BodyType {
case "file":
// 文件上传
if req.FilePath != "" {
file, err := os.Open(req.FilePath)
bodyReader, contentType, err = buildFileBody(req.FilePath)
if err != nil {
resp.Error = fmt.Sprintf("打开文件失败: %v", err)
resp.Error = err.Error()
return resp
}
defer file.Close()
// 获取文件信息
fileInfo, err := file.Stat()
if err != nil {
resp.Error = fmt.Sprintf("获取文件信息失败: %v", err)
return resp
}
// 使用 multipart 上传
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", fileInfo.Name())
if err != nil {
resp.Error = fmt.Sprintf("创建表单文件失败: %v", err)
return resp
}
if _, err := io.Copy(part, file); err != nil {
resp.Error = fmt.Sprintf("复制文件内容失败: %v", err)
return resp
}
writer.Close()
bodyReader = body
contentType = writer.FormDataContentType()
}
case "form":
// 表单数据
if req.Body != "" {
bodyReader = strings.NewReader(req.Body)
if contentType == "" {
contentType = "application/x-www-form-urlencoded"
}
}
case "json":
// JSON 数据
if req.Body != "" {
bodyReader = strings.NewReader(req.Body)
if contentType == "" {
contentType = "application/json"
}
}
default:
// 文本数据
if req.Body != "" {
bodyReader = strings.NewReader(req.Body)
if contentType == "" {
@@ -181,46 +229,20 @@ func SendRequest(req *HttpRequest) *HttpResponse {
}
}
// 创建请求
httpReq, err := http.NewRequest(req.Method, req.URL, bodyReader)
if err != nil {
resp.Error = fmt.Sprintf("创建请求失败: %v", err)
return resp
}
// 设置 Content-Type
if contentType != "" {
httpReq.Header.Set("Content-Type", contentType)
}
// 设置自定义请求头(需要单独处理 host)
for key, value := range req.Headers {
if strings.ToLower(key) == "host" {
httpReq.Host = value
} else {
httpReq.Header.Set(key, value)
}
}
setRequestHeaders(httpReq, req)
setupCookies(client, req)
// 设置 Cookie
if len(req.Cookies) > 0 {
cookieJar, _ := cookiejar.New(nil)
var cookieList []*http.Cookie
for name, value := range req.Cookies {
cookieList = append(cookieList, &http.Cookie{
Name: name,
Value: value,
})
}
parsedURL, _ := url.Parse(req.URL)
cookieJar.SetCookies(parsedURL, cookieList)
client.Jar = cookieJar
}
// 记录开始时间
start := time.Now()
// 发送请求
httpResp, err := client.Do(httpReq)
if err != nil {
resp.Error = fmt.Sprintf("请求失败: %v", err)
@@ -228,95 +250,31 @@ func SendRequest(req *HttpRequest) *HttpResponse {
}
defer httpResp.Body.Close()
// 记录耗时
resp.Time = time.Since(start).String()
// 读取响应体
body, err := io.ReadAll(httpResp.Body)
if err != nil {
resp.Error = fmt.Sprintf("读取响应失败: %v", err)
return resp
}
// 设置响应
resp.StatusCode = httpResp.StatusCode
resp.Status = httpResp.Status
resp.Body = string(body)
resp.ContentLength = httpResp.ContentLength
resp.Size = int64(len(body))
// 获取 Content-Type
resp.ContentType = httpResp.Header.Get("Content-Type")
// 获取响应头
for key, values := range httpResp.Header {
resp.Headers[key] = strings.Join(values, "; ")
}
// 获取响应 Cookie
resp.Cookies = convertCookies(httpResp.Cookies())
parseHTTPResponse(httpResp, resp, start)
return resp
}
// SendRequestWithFiles 带文件的请求
// SendRequestWithFiles 带多文件上传的请求
func SendRequestWithFiles(req *HttpRequest, files []FileData) *HttpResponse {
resp := &HttpResponse{
Headers: make(map[string]string),
}
resp := &HttpResponse{}
if req.URL == "" {
resp.Error = "请求地址不能为空"
return resp
}
// 补全协议
if !strings.HasPrefix(req.URL, "http://") && !strings.HasPrefix(req.URL, "https://") {
req.URL = "https://" + req.URL
}
normalizeURL(req)
// 设置超时
timeout := time.Duration(req.Timeout) * time.Second
if timeout == 0 {
timeout = 30 * time.Second
}
client := buildHTTPClient(req)
// 创建 HTTP 客户端
client := &http.Client{
Timeout: timeout,
}
// 配置代理
if req.Proxy != "" {
proxyURL, err := url.Parse(req.Proxy)
if err != nil {
resp.Error = fmt.Sprintf("代理地址格式错误: %v", err)
return resp
}
client.Transport = &http.Transport{
Proxy: http.ProxyURL(proxyURL),
TLSClientConfig: &tls.Config{
InsecureSkipVerify: req.Insecure,
},
}
} else if req.Insecure {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
}
// 创建 multipart 请求
// 构造 multipart 请求体
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// 添加文本字段
if req.Body != "" {
_ = writer.WriteField("text", req.Body)
}
// 添加文件
for _, file := range files {
f, err := os.Open(file.Path)
if err != nil {
@@ -339,44 +297,17 @@ func SendRequestWithFiles(req *HttpRequest, files []FileData) *HttpResponse {
writer.Close()
// 创建请求
httpReq, err := http.NewRequest(req.Method, req.URL, body)
if err != nil {
resp.Error = fmt.Sprintf("创建请求失败: %v", err)
return resp
}
// 设置 Content-Type
httpReq.Header.Set("Content-Type", writer.FormDataContentType())
setRequestHeaders(httpReq, req)
setupCookies(client, req)
// 设置自定义请求头(需要单独处理 host)
for key, value := range req.Headers {
if strings.ToLower(key) == "host" {
httpReq.Host = value
} else {
httpReq.Header.Set(key, value)
}
}
// 设置 Cookie
if len(req.Cookies) > 0 {
cookieJar, _ := cookiejar.New(nil)
var cookieList []*http.Cookie
for name, value := range req.Cookies {
cookieList = append(cookieList, &http.Cookie{
Name: name,
Value: value,
})
}
parsedURL, _ := url.Parse(req.URL)
cookieJar.SetCookies(parsedURL, cookieList)
client.Jar = cookieJar
}
// 记录开始时间
start := time.Now()
// 发送请求
httpResp, err := client.Do(httpReq)
if err != nil {
resp.Error = fmt.Sprintf("请求失败: %v", err)
@@ -384,37 +315,36 @@ func SendRequestWithFiles(req *HttpRequest, files []FileData) *HttpResponse {
}
defer httpResp.Body.Close()
// 记录耗时
resp.Time = time.Since(start).String()
// 读取响应体
respBody, err := io.ReadAll(httpResp.Body)
if err != nil {
resp.Error = fmt.Sprintf("读取响应失败: %v", err)
return resp
}
// 设置响应
resp.StatusCode = httpResp.StatusCode
resp.Status = httpResp.Status
resp.Body = string(respBody)
resp.ContentLength = httpResp.ContentLength
resp.Size = int64(len(respBody))
// 获取 Content-Type
resp.ContentType = httpResp.Header.Get("Content-Type")
// 获取响应头
for key, values := range httpResp.Header {
resp.Headers[key] = strings.Join(values, "; ")
}
// 获取响应 Cookie
resp.Cookies = convertCookies(httpResp.Cookies())
parseHTTPResponse(httpResp, resp, start)
return resp
}
// buildFileBody 构造单文件上传的 multipart 请求体
func buildFileBody(filePath string) (io.Reader, string, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, "", fmt.Errorf("打开文件失败: %v", err)
}
defer file.Close()
fileInfo, err := file.Stat()
if err != nil {
return nil, "", fmt.Errorf("获取文件信息失败: %v", err)
}
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", fileInfo.Name())
if err != nil {
return nil, "", fmt.Errorf("创建表单文件失败: %v", err)
}
if _, err := io.Copy(part, file); err != nil {
return nil, "", fmt.Errorf("复制文件内容失败: %v", err)
}
writer.Close()
return body, writer.FormDataContentType(), nil
}
// ParseHeaders 解析请求头字符串
func ParseHeaders(headerStr string) map[string]string {
headers := make(map[string]string)
+33 -16
View File
@@ -7,6 +7,7 @@ import (
"path/filepath"
"sort"
"strings"
"sync"
"time"
)
@@ -32,13 +33,17 @@ type RequestLog struct {
// LogManager 日志管理器
type LogManager struct {
mu sync.RWMutex
logDir string
logs []*RequestLog
maxLogs int
fileName string
}
var logManager *LogManager
var (
logManager *LogManager
logManagerOnce sync.Once
)
// InitLogManager 初始化日志管理器
func InitLogManager(logDir string) {
@@ -51,11 +56,13 @@ func InitLogManager(logDir string) {
logManager.loadLogs()
}
// GetLogManager 获取日志管理器
// GetLogManager 获取日志管理器(并发安全的单例)
func GetLogManager() *LogManager {
if logManager == nil {
InitLogManager("logs")
}
logManagerOnce.Do(func() {
if logManager == nil {
InitLogManager("logs")
}
})
return logManager
}
@@ -97,6 +104,9 @@ func (m *LogManager) saveLogs() error {
// AddLog 添加日志
func (m *LogManager) AddLog(log *RequestLog) error {
m.mu.Lock()
defer m.mu.Unlock()
// 生成序号
if len(m.logs) > 0 {
log.ID = m.logs[0].ID + 1
@@ -119,11 +129,15 @@ func (m *LogManager) AddLog(log *RequestLog) error {
// GetLogs 获取所有日志
func (m *LogManager) GetLogs() []*RequestLog {
m.mu.RLock()
defer m.mu.RUnlock()
return m.logs
}
// GetLog 获取单条日志
func (m *LogManager) GetLog(id int) *RequestLog {
m.mu.RLock()
defer m.mu.RUnlock()
for _, log := range m.logs {
if log.ID == id {
return log
@@ -134,6 +148,8 @@ func (m *LogManager) GetLog(id int) *RequestLog {
// DeleteLog 删除日志
func (m *LogManager) DeleteLog(id int) error {
m.mu.Lock()
defer m.mu.Unlock()
for i, log := range m.logs {
if log.ID == id {
m.logs = append(m.logs[:i], m.logs[i+1:]...)
@@ -145,12 +161,16 @@ func (m *LogManager) DeleteLog(id int) error {
// ClearLogs 清空日志
func (m *LogManager) ClearLogs() error {
m.mu.Lock()
defer m.mu.Unlock()
m.logs = make([]*RequestLog, 0)
return m.saveLogs()
}
// SearchLogs 搜索日志
func (m *LogManager) SearchLogs(keyword string) []*RequestLog {
m.mu.RLock()
defer m.mu.RUnlock()
var result []*RequestLog
keyword = strings.ToLower(keyword)
@@ -171,17 +191,14 @@ func CreateLogFromResponse(req *HttpRequest, resp *HttpResponse) *RequestLog {
// 解析 URL
host := ""
path := req.URL
if strings.HasPrefix(req.URL, "http://") {
parts := strings.SplitN(strings.TrimPrefix(req.URL, "http://"), "/", 2)
host = parts[0]
if len(parts) > 1 {
path = "/" + parts[1]
}
} else if strings.HasPrefix(req.URL, "https://") {
parts := strings.SplitN(strings.TrimPrefix(req.URL, "https://"), "/", 2)
host = parts[0]
if len(parts) > 1 {
path = "/" + parts[1]
for _, prefix := range []string{"http://", "https://"} {
if strings.HasPrefix(req.URL, prefix) {
parts := strings.SplitN(strings.TrimPrefix(req.URL, prefix), "/", 2)
host = parts[0]
if len(parts) > 1 {
path = "/" + parts[1]
}
break
}
}
+127 -14
View File
@@ -11,6 +11,7 @@ import (
"webhelper/global"
"github.com/gorilla/websocket"
"golang.org/x/net/proxy"
)
// WebSocketClient WebSocket 客户端
@@ -27,6 +28,7 @@ type WebSocketClient struct {
type WebSocketConfig struct {
URL string `json:"url"`
Headers map[string]string `json:"headers"`
Proxy string `json:"proxy"` // 代理地址,支持 http:// 和 socks5://
Insecure bool `json:"insecure"`
}
@@ -37,7 +39,10 @@ type WebSocketMessage struct {
Time string `json:"time"`
}
var wsClient *WebSocketClient
var (
wsClient *WebSocketClient
wsClientOnce sync.Once
)
// NewWebSocketClient 创建 WebSocket 客户端
func NewWebSocketClient() *WebSocketClient {
@@ -67,13 +72,23 @@ func (c *WebSocketClient) Connect(config *WebSocketConfig, onMessage func(string
header.Set(key, value)
}
// 创建 Dialer(最简配置)
// 创建 Dialer
dialer := &websocket.Dialer{
HandshakeTimeout: 10 * time.Second,
}
// 配置代理
proxyAddr := global.NormalizeProxy(config.Proxy)
if proxyAddr != "" {
proxyURL, err := url.Parse(proxyAddr)
if err != nil {
return fmt.Errorf("代理地址格式错误: %v", err)
}
dialer.Proxy = http.ProxyURL(proxyURL)
}
// 连接
global.Log.Infof("正在连接 WebSocket: %s", u.String())
global.Log.Infof("正在连接 WebSocket: %s (proxy=%s)", u.String(), proxyAddr)
conn, _, err := dialer.Dial(u.String(), header)
if err != nil {
return fmt.Errorf("连接失败: %v", err)
@@ -150,11 +165,11 @@ func (c *WebSocketClient) IsConnected() bool {
return c.isConnected
}
// GetWebSocketClient 获取 WebSocket 客户端
// GetWebSocketClient 获取 WebSocket 客户端(并发安全的单例)
func GetWebSocketClient() *WebSocketClient {
if wsClient == nil {
wsClientOnce.Do(func() {
wsClient = NewWebSocketClient()
}
})
return wsClient
}
@@ -194,8 +209,9 @@ type TCPClient struct {
// TCPConfig TCP 配置
type TCPConfig struct {
Host string `json:"host"`
Port int `json:"port"`
Host string `json:"host"`
Port int `json:"port"`
Proxy string `json:"proxy"` // 代理地址,支持 socks5://
}
// TCPMessage TCP 消息
@@ -205,7 +221,10 @@ type TCPMessage struct {
Time string `json:"time"`
}
var tcpClient *TCPClient
var (
tcpClient *TCPClient
tcpClientOnce sync.Once
)
// NewTCPClient 创建 TCP 客户端
func NewTCPClient() *TCPClient {
@@ -224,7 +243,43 @@ func (c *TCPClient) Connect(config *TCPConfig, onMessage func(string), onError f
}
addr := fmt.Sprintf("%s:%d", config.Host, config.Port)
conn, err := net.DialTimeout("tcp", addr, 10*time.Second)
// 配置代理
proxyAddr := global.NormalizeProxy(config.Proxy)
var conn net.Conn
var err error
if proxyAddr != "" {
// 通过代理连接
proxyURL, parseErr := url.Parse(proxyAddr)
if parseErr != nil {
return fmt.Errorf("代理地址格式错误: %v", parseErr)
}
switch proxyURL.Scheme {
case "socks5", "socks4":
// SOCKS 代理
auth := &proxy.Auth{}
if proxyURL.User != nil {
auth.User = proxyURL.User.Username()
if p, ok := proxyURL.User.Password(); ok {
auth.Password = p
}
}
dialer, proxyErr := proxy.SOCKS5("tcp", proxyURL.Host, auth, proxy.Direct)
if proxyErr != nil {
return fmt.Errorf("创建 SOCKS 代理失败: %v", proxyErr)
}
conn, err = dialer.Dial("tcp", addr)
default:
// HTTP 代理(通过 CONNECT 隧道)
conn, err = httpConnectProxy(addr, proxyAddr)
}
} else {
// 直连
conn, err = net.DialTimeout("tcp", addr, 10*time.Second)
}
if err != nil {
return fmt.Errorf("连接失败: %v", err)
}
@@ -235,12 +290,70 @@ func (c *TCPClient) Connect(config *TCPConfig, onMessage func(string), onError f
c.onError = onError
c.onClose = onClose
global.Log.Infof("TCP 连接成功: %s (proxy=%s)", addr, proxyAddr)
// 启动消息监听
go c.readMessages()
return nil
}
// httpConnectProxy 通过 HTTP CONNECT 代理建立 TCP 连接
func httpConnectProxy(target, proxyAddr string) (net.Conn, error) {
proxyURL, err := url.Parse(proxyAddr)
if err != nil {
return nil, err
}
// 连接代理
conn, err := net.DialTimeout("tcp", proxyURL.Host, 10*time.Second)
if err != nil {
return nil, err
}
// 发送 CONNECT 请求
connectReq := fmt.Sprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", target, target)
_, err = conn.Write([]byte(connectReq))
if err != nil {
conn.Close()
return nil, err
}
// 读取响应
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
conn.Close()
return nil, err
}
// 检查 200 状态
resp := string(buf[:n])
if !contains(resp, "200") {
conn.Close()
return nil, fmt.Errorf("代理连接失败: %s", resp)
}
return conn, nil
}
// contains 检查字符串是否包含子串
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsSubstr(s, substr))
}
func containsSubstr(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
// tcpBufSize TCP 读取缓冲区大小,64KB 覆盖绝大多数调试场景
const tcpBufSize = 64 << 10
// readMessages 读取消息
func (c *TCPClient) readMessages() {
defer func() {
@@ -252,7 +365,7 @@ func (c *TCPClient) readMessages() {
}
}()
buf := make([]byte, 4096)
buf := make([]byte, tcpBufSize)
for {
n, err := c.conn.Read(buf)
if err != nil {
@@ -300,11 +413,11 @@ func (c *TCPClient) IsConnected() bool {
return c.isConnected
}
// GetTCPClient 获取 TCP 客户端
// GetTCPClient 获取 TCP 客户端(并发安全的单例)
func GetTCPClient() *TCPClient {
if tcpClient == nil {
tcpClientOnce.Do(func() {
tcpClient = NewTCPClient()
}
})
return tcpClient
}
+112 -24
View File
@@ -1,30 +1,51 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useI18n } from '../composables/useI18n'
import { GetALLLang, GetCurrentLang, SetLanguage, GetLangTextMap } from '../../wailsjs/go/service/App'
import {
GetALLLang, GetCurrentLang, SetLanguage, GetLangTextMap,
GetProxy, SetProxy, GetTimeout, SetTimeout,
GetLogLevel, SetLogLevel
} from '../../wailsjs/go/service/App'
const { t, textMap } = useI18n()
const settings = ref({
proxyEnabled: false,
proxyHost: '',
proxyPort: 7890
proxyURL: '',
timeout: 30,
logLevel: 'info'
})
const languages = ref([])
const currentLang = ref('')
// 加载语言列表
const logLevels = [
{ label: 'Debug', value: 'debug' },
{ label: 'Info', value: 'info' },
{ label: 'Warn', value: 'warn' },
{ label: 'Error', value: 'error' }
]
// 加载所有设置
onMounted(async () => {
try {
const langInfos = await GetALLLang()
const [langInfos, proxy, timeoutCfg, logLevel] = await Promise.all([
GetALLLang(),
GetProxy(),
GetTimeout(),
GetLogLevel()
])
languages.value = langInfos.map(info => ({
code: info.language_code,
name: info.language_name
}))
currentLang.value = await GetCurrentLang() || 'zh-CN'
if (proxy) settings.value.proxyURL = proxy.url || ''
if (timeoutCfg) settings.value.timeout = timeoutCfg.timeout || 30
settings.value.logLevel = logLevel || 'info'
} catch (e) {
console.error('获取语言列表失败:', e)
console.error('获取设置失败:', e)
}
})
@@ -40,19 +61,46 @@ const handleLangChange = async (langCode) => {
}
}
const saveSettings = () => {
localStorage.setItem('webhelper-settings', JSON.stringify(settings.value))
ElMessage.success(t('settings.msg.saved'))
// 保存所有设置
const saveSettings = async () => {
try {
await Promise.all([
SetProxy(settings.value.proxyURL),
SetTimeout(settings.value.timeout),
SetLogLevel(settings.value.logLevel)
])
ElMessage.success(t('settings.msg.saved'))
} catch (e) {
ElMessage.error('保存设置失败: ' + e)
}
}
const resetSettings = () => {
// 恢复默认
const resetSettings = async () => {
settings.value = {
proxyEnabled: false,
proxyHost: '',
proxyPort: 7890
proxyURL: '',
timeout: 30,
logLevel: 'info'
}
const defaultLang = 'zh-CN'
try {
await Promise.all([
SetProxy(''),
SetTimeout(30),
SetLogLevel('info'),
SetLanguage(defaultLang)
])
currentLang.value = defaultLang
const map = await GetLangTextMap()
textMap.value = map || {}
ElMessage.info(t('settings.msg.reset'))
} catch (e) {
ElMessage.error('恢复默认失败: ' + e)
}
localStorage.removeItem('webhelper-settings')
ElMessage.info(t('settings.msg.reset'))
}
</script>
@@ -62,7 +110,10 @@ const resetSettings = () => {
<span>{{ t('settings.title') }}</span>
</div>
<el-form :model="settings" label-width="100px" class="settings-form">
<el-form :model="settings" label-width="120px" class="settings-form">
<!-- 外观 -->
<el-divider content-position="left">{{ t('settings.form.appearance_divider') }}</el-divider>
<el-form-item :label="t('settings.form.language_label')">
<el-select v-model="currentLang" style="width: 200px" @change="handleLangChange">
<el-option
@@ -74,18 +125,37 @@ const resetSettings = () => {
</el-select>
</el-form-item>
<el-divider content-position="left">{{ t('settings.form.proxy_divider') }}</el-divider>
<!-- 网络 -->
<el-divider content-position="left">{{ t('settings.form.network_divider') }}</el-divider>
<el-form-item :label="t('settings.form.enable_proxy_label')">
<el-switch v-model="settings.proxyEnabled" />
<el-form-item :label="t('settings.form.proxy_label')">
<el-input
v-model="settings.proxyURL"
:placeholder="t('settings.form.proxy_placeholder')"
style="width: 320px"
clearable
/>
<div class="form-tip">{{ t('settings.form.proxy_tip') }}</div>
</el-form-item>
<el-form-item v-if="settings.proxyEnabled" :label="t('settings.form.proxy_address_label')">
<el-input v-model="settings.proxyHost" :placeholder="t('settings.form.proxy_address_placeholder')" style="width: 200px" />
<el-form-item :label="t('settings.form.timeout_label')">
<el-input-number v-model="settings.timeout" :min="1" :max="300" />
<span class="form-unit">{{ t('settings.form.timeout_unit') }}</span>
</el-form-item>
<el-form-item v-if="settings.proxyEnabled" :label="t('settings.form.proxy_port_label')">
<el-input-number v-model="settings.proxyPort" :min="1" :max="65535" />
<!-- 日志 -->
<el-divider content-position="left">{{ t('settings.form.log_divider') }}</el-divider>
<el-form-item :label="t('settings.form.log_level_label')">
<el-select v-model="settings.logLevel" style="width: 200px">
<el-option
v-for="level in logLevels"
:key="level.value"
:label="level.label"
:value="level.value"
/>
</el-select>
<span class="form-tip-inline">{{ t('settings.form.log_level_tip') }}</span>
</el-form-item>
<el-divider />
@@ -119,4 +189,22 @@ const resetSettings = () => {
.settings-form {
max-width: 600px;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 4px;
line-height: 1.4;
}
.form-unit {
margin-left: 8px;
color: #606266;
}
.form-tip-inline {
margin-left: 8px;
font-size: 12px;
color: #909399;
}
</style>
+19 -6
View File
@@ -1,13 +1,14 @@
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useI18n } from '../composables/useI18n'
import {
SendHttpRequest,
OpenFileSelect,
GetRequestLogs,
DeleteRequestLog,
import {
SendHttpRequest,
OpenFileSelect,
GetRequestLogs,
DeleteRequestLog,
ClearRequestLogs,
SearchRequestLogs
SearchRequestLogs,
GetTimeout
} from '../../wailsjs/go/service/App'
import { Delete, Upload, Notebook, Search } from '@element-plus/icons-vue'
@@ -29,6 +30,18 @@ const timeout = ref(30)
const insecure = ref(false)
const saveLog = ref(true)
// 加载全局默认设置
onMounted(async () => {
try {
const cfg = await GetTimeout()
if (cfg) {
timeout.value = cfg.timeout || 30
}
} catch (e) {
console.error('获取默认设置失败:', e)
}
})
// 响应展示相关
const responseTab = ref('body')
+16 -16
View File
@@ -198,28 +198,28 @@ export namespace global {
export namespace service {
export class SystemInfo {
os: string;
arch: string;
num_cpu: number;
hostname: string;
go_ver: string;
time: string;
process_name: string;
export class ProxyConfig {
url: string;
static createFrom(source: any = {}) {
return new SystemInfo(source);
return new ProxyConfig(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.os = source["os"];
this.arch = source["arch"];
this.num_cpu = source["num_cpu"];
this.hostname = source["hostname"];
this.go_ver = source["go_ver"];
this.time = source["time"];
this.process_name = source["process_name"];
this.url = source["url"];
}
}
export class TimeoutConfig {
timeout: number;
static createFrom(source: any = {}) {
return new TimeoutConfig(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.timeout = source["timeout"];
}
}
+10 -6
View File
@@ -14,8 +14,6 @@ export function DisconnectSocket(arg1:string):Promise<boolean>;
export function ExecuteJs(arg1:string):Promise<api.JsExecResult>;
export function Flashtime():Promise<void>;
export function GetALLLang():Promise<Array<global.LanguageInfo>>;
export function GetCurrentLang():Promise<string>;
@@ -32,15 +30,17 @@ export function GetLogLevel():Promise<string>;
export function GetProcessName():Promise<string>;
export function GetProxy():Promise<service.ProxyConfig>;
export function GetProxyAddress():Promise<string>;
export function GetRequestLog(arg1:number):Promise<api.RequestLog>;
export function GetRequestLogs():Promise<Array<api.RequestLog>>;
export function GetSystemInfo():Promise<service.SystemInfo>;
export function GetTimeout():Promise<service.TimeoutConfig>;
export function Gettestjson():Promise<string>;
export function Greet(arg1:string):Promise<string>;
export function GetVersion():Promise<string>;
export function IsSocketConnected(arg1:string):Promise<boolean>;
@@ -64,6 +64,10 @@ export function SetLanguage(arg1:string):Promise<boolean>;
export function SetLogLevel(arg1:string):Promise<boolean>;
export function SetProxy(arg1:string):Promise<boolean>;
export function SetTimeout(arg1:number):Promise<boolean>;
export function WindowClose():Promise<void>;
export function WindowMinimise():Promise<void>;
+20 -12
View File
@@ -22,10 +22,6 @@ export function ExecuteJs(arg1) {
return window['go']['service']['App']['ExecuteJs'](arg1);
}
export function Flashtime() {
return window['go']['service']['App']['Flashtime']();
}
export function GetALLLang() {
return window['go']['service']['App']['GetALLLang']();
}
@@ -58,6 +54,14 @@ export function GetProcessName() {
return window['go']['service']['App']['GetProcessName']();
}
export function GetProxy() {
return window['go']['service']['App']['GetProxy']();
}
export function GetProxyAddress() {
return window['go']['service']['App']['GetProxyAddress']();
}
export function GetRequestLog(arg1) {
return window['go']['service']['App']['GetRequestLog'](arg1);
}
@@ -66,16 +70,12 @@ export function GetRequestLogs() {
return window['go']['service']['App']['GetRequestLogs']();
}
export function GetSystemInfo() {
return window['go']['service']['App']['GetSystemInfo']();
export function GetTimeout() {
return window['go']['service']['App']['GetTimeout']();
}
export function Gettestjson() {
return window['go']['service']['App']['Gettestjson']();
}
export function Greet(arg1) {
return window['go']['service']['App']['Greet'](arg1);
export function GetVersion() {
return window['go']['service']['App']['GetVersion']();
}
export function IsSocketConnected(arg1) {
@@ -122,6 +122,14 @@ export function SetLogLevel(arg1) {
return window['go']['service']['App']['SetLogLevel'](arg1);
}
export function SetProxy(arg1) {
return window['go']['service']['App']['SetProxy'](arg1);
}
export function SetTimeout(arg1) {
return window['go']['service']['App']['SetTimeout'](arg1);
}
export function WindowClose() {
return window['go']['service']['App']['WindowClose']();
}
+29 -1
View File
@@ -1,11 +1,39 @@
package global
import "strings"
type GConfig struct {
Language string `json:"language"`
LogDir string `json:"log_dir"`
ProxyURL string `json:"proxy_url"` // 代理地址,支持 http:// 和 socks5://
Timeout int `json:"timeout"` // 默认请求超时(秒)
LogLevel string `json:"log_level"` // 日志等级:debug/info/warn/error
}
var GlobalConfig = &GConfig{
Language: "zh-CN",
LogDir: "logs",
}
ProxyURL: "",
Timeout: 30,
LogLevel: "info",
}
// GetProxy 获取全局代理地址,未设置或为空时返回空字符串
func GetProxy() string {
return NormalizeProxy(GlobalConfig.ProxyURL)
}
// NormalizeProxy 规范化代理地址:无协议前缀时默认 http://
func NormalizeProxy(addr string) string {
addr = strings.TrimSpace(addr)
if addr == "" {
return ""
}
// 已有协议前缀,直接返回
if strings.HasPrefix(addr, "http://") || strings.HasPrefix(addr, "https://") ||
strings.HasPrefix(addr, "socks5://") || strings.HasPrefix(addr, "socks4://") {
return addr
}
// 无前缀,默认 http://
return "http://" + addr
}
+1 -1
View File
@@ -7,6 +7,7 @@ require (
github.com/gorilla/websocket v1.5.3
github.com/sirupsen/logrus v1.9.3
github.com/wailsapp/wails/v2 v2.12.0
golang.org/x/net v0.35.0
)
require (
@@ -37,7 +38,6 @@ require (
github.com/wailsapp/go-webview2 v1.0.22 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
)
+71 -39
View File
@@ -5,10 +5,8 @@ import (
"fmt"
"os"
"path/filepath"
goruntime "runtime"
"sort"
"strings"
"time"
"webhelper/api"
"webhelper/global"
@@ -29,18 +27,6 @@ func (a *App) Startup(ctx context.Context) {
a.ctx = ctx
}
func (a *App) Greet(name string) string {
return greet(name)
}
func (a *App) Flashtime() {
flashtime(a)
}
func (a *App) Gettestjson() string {
return getJSONString()
}
func (a *App) GetLangTextMap() map[string]string {
return global.GetLangTextMap()
}
@@ -69,6 +55,52 @@ func (a *App) GetCurrentLang() string {
return global.GlobalConfig.Language
}
// ProxyConfig 代理配置
type ProxyConfig struct {
URL string `json:"url"`
}
// TimeoutConfig 超时配置
type TimeoutConfig struct {
Timeout int `json:"timeout"`
}
// GetProxy 获取全局代理配置
func (a *App) GetProxy() ProxyConfig {
return ProxyConfig{
URL: global.GlobalConfig.ProxyURL,
}
}
// SetProxy 设置全局代理配置
func (a *App) SetProxy(url string) bool {
global.GlobalConfig.ProxyURL = url
global.Log.Infof("全局代理已更新: %s", url)
return true
}
// GetProxyAddress 获取全局代理地址字符串(已规范化),供 HTTP 请求使用
func (a *App) GetProxyAddress() string {
return global.GetProxy()
}
// GetTimeout 获取默认超时设置
func (a *App) GetTimeout() TimeoutConfig {
return TimeoutConfig{
Timeout: global.GlobalConfig.Timeout,
}
}
// SetTimeout 设置默认超时
func (a *App) SetTimeout(timeout int) bool {
if timeout < 1 {
timeout = 30
}
global.GlobalConfig.Timeout = timeout
global.Log.Infof("默认超时已更新: %d", timeout)
return true
}
func (a *App) GetLogFiles() []string {
logDir := global.GlobalConfig.LogDir
entries, err := os.ReadDir(logDir)
@@ -108,28 +140,26 @@ func (a *App) SetLogLevel(level string) bool {
switch strings.ToLower(level) {
case "debug":
global.SetLogLevel(logrus.DebugLevel)
global.Log.Debug("日志等级已切换为 Debug")
global.GlobalConfig.LogLevel = "debug"
case "info":
global.SetLogLevel(logrus.InfoLevel)
global.Log.Info("日志等级已切换为 Info")
global.GlobalConfig.LogLevel = "info"
case "warn":
global.SetLogLevel(logrus.WarnLevel)
global.Log.Warn("日志等级已切换为 Warn")
global.GlobalConfig.LogLevel = "warn"
case "error":
global.SetLogLevel(logrus.ErrorLevel)
global.Log.Error("日志等级已切换为 Error")
global.GlobalConfig.LogLevel = "error"
default:
global.Log.Warnf("未知的日志等级: %s", level)
return false
}
global.Log.Infof("日志等级已切换为: %s", strings.ToUpper(global.GlobalConfig.LogLevel))
return true
}
func (a *App) GetLogLevel() string {
if global.Log == nil {
return "info"
}
return strings.ToUpper(global.Log.GetLevel().String())
return global.GlobalConfig.LogLevel
}
func (a *App) WindowMinimise() {
@@ -144,20 +174,6 @@ func (a *App) WindowClose() {
runtime.Quit(a.ctx)
}
func (a *App) GetSystemInfo() SystemInfo {
hostname, _ := os.Hostname()
return SystemInfo{
OS: goruntime.GOOS,
Arch: goruntime.GOARCH,
NumCPU: goruntime.NumCPU(),
Hostname: hostname,
GoVer: goruntime.Version(),
Time: time.Now().Format(time.DateTime),
ProcessName: global.GetProcessName(),
}
}
func (a *App) GetProcessName() string {
return global.GetProcessName()
}
@@ -248,12 +264,26 @@ func (a *App) SendHttpRequest(method, url, headers, cookies, proxy, bodyType, bo
// 解析 Cookie
cookieMap := api.ParseCookies(cookies)
// 代理优先级:请求级代理 > 全局代理
requestProxy := global.NormalizeProxy(proxy)
if requestProxy == "" {
requestProxy = global.GetProxy()
}
// 超时优先级:请求级 > 全局默认
if timeout <= 0 {
timeout = global.GlobalConfig.Timeout
}
// 日志保存优先级:显式传入 > 全局默认
// saveLog 由前端传入,这里不覆盖
req := &api.HttpRequest{
Method: method,
URL: url,
Headers: headerMap,
Cookies: cookieMap,
Proxy: proxy,
Proxy: requestProxy,
BodyType: bodyType,
Body: body,
FilePath: filePath,
@@ -366,8 +396,9 @@ func (a *App) ConnectSocket(protocol, host string, port int, path string, header
if protocol == "tcp" {
// TCP 连接
config := &api.TCPConfig{
Host: host,
Port: port,
Host: host,
Port: port,
Proxy: global.GetProxy(),
}
err := api.ConnectTCP(config,
@@ -409,6 +440,7 @@ func (a *App) ConnectSocket(protocol, host string, port int, path string, header
config := &api.WebSocketConfig{
URL: url,
Headers: headerMap,
Proxy: global.GetProxy(),
}
err := api.ConnectWebSocket(config,