From bc0c62eb094f64a9bc6d9ea5698f79a0f6339bc7 Mon Sep 17 00:00:00 2001 From: AI Engineer Date: Sun, 21 Jun 2026 14:02:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(service):=20=E5=85=B7=E5=90=8D=E5=8C=96=20?= =?UTF-8?q?JS=20=E5=AF=BC=E5=87=BA=E5=B9=B6=E5=8A=A8=E6=80=81=E5=8C=85?= =?UTF-8?q?=E8=A3=B9=E9=94=99=E8=AF=AF=EF=BC=88by=20AI=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 17 ++++++++++++++ TEST.md | 2 +- go.mod | 33 +++++++++++++++------------ go.sum | 64 +++++++++++++++++++++++++++++----------------------- handler.go | 3 +++ js_export.go | 32 ++++++++++++++++++++------ response.go | 19 +++++++++++++--- session.go | 3 ++- utility.go | 3 +++ websocket.go | 34 +++++++++++++++++++++------- 10 files changed, 147 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2022ea..aa15db5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # CHANGELOG - go/service +## v1.5.15 (2026-06-21) +- **错误堆栈重构**: + - 重构 `js_export.go`,将匿名占位工厂函数改写为包级具名函数。 + - 使用 `jsmod.MakeError` 包裹 `Upgrade` 返回的错误,并在 `Response`、`Session`、`WebSocketConn` 的相关出错方法中包裹返回的错误。 +- **修复**: + - 修复 `RouteHandler.ServeHTTP` 在 `RouteHandler.ws` 为空时可能触发的 nil 指针引用 panic,使其安全降级使用 `DefaultServer`。 +- **依赖对齐**: + - 升级依赖 `jsmod` 至 `v1.5.3`, `timer` (`v1.5.0`), `cast` 至 `v1.5.3`, `rand` 至 `v1.5.3`, `encoding` 至 `v1.5.4`, `shell` 至 `v1.5.3`, `safe` 至 `v1.5.2`, `id` 至 `v1.5.4`, `crypto` 至 `v1.5.3`, `file` 至 `v1.5.5`, `config` 至 `v1.5.3`, `log` 至 `v1.5.8`, `http` 至 `v1.5.3`, `redis` 至 `v1.5.6`, `starter` 至 `v1.5.5`, `discover` 至 `v1.5.3`。 + +## v1.5.14 (2026-06-11) +- **依赖对齐**: + - 对齐基础设施依赖到 v1.5.x。 + +## v1.5.13 (2026-06-10) +- **优化**: + - 对齐 JS 导出为 PascalCase 格式。 + ## v1.5.12 (2026-06-07) - **基础设施对齐: 切换至 starter v1.5.3 编排模式**: - 弃用已废弃的 `starter.Run()`,全面转向 `starter.Start() / starter.Wait()`。 diff --git a/TEST.md b/TEST.md index cae50af..77b0743 100644 --- a/TEST.md +++ b/TEST.md @@ -1,7 +1,7 @@ # Service Module Test Report ## 性能测试 (Benchmark) -- 测试日期: 2026-05-10 +- 测试日期: 2026-06-21 - 版本: v1.0.4 - 指标: `BenchmarkRouting`: 2791 ns/op - 环境: Darwin / Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz diff --git a/go.mod b/go.mod index 08fe3e4..4294b98 100644 --- a/go.mod +++ b/go.mod @@ -3,27 +3,30 @@ module apigo.cc/go/service go 1.25.0 require ( - apigo.cc/go/cast v1.5.2 - apigo.cc/go/config v1.5.2 - apigo.cc/go/discover v1.5.2 - apigo.cc/go/file v1.5.4 - apigo.cc/go/http v1.5.2 - apigo.cc/go/id v1.5.3 - apigo.cc/go/jsmod v1.5.2 - apigo.cc/go/log v1.5.6 - apigo.cc/go/redis v1.5.4 - apigo.cc/go/safe v1.5.1 - apigo.cc/go/starter v1.5.4 + apigo.cc/go/cast v1.5.3 + apigo.cc/go/config v1.5.3 + apigo.cc/go/discover v1.5.3 + apigo.cc/go/file v1.5.5 + apigo.cc/go/http v1.5.3 + apigo.cc/go/id v1.5.4 + apigo.cc/go/jsmod v1.5.3 + apigo.cc/go/log v1.5.8 + apigo.cc/go/redis v1.5.6 + apigo.cc/go/safe v1.5.2 + apigo.cc/go/starter v1.5.5 apigo.cc/go/timer v1.5.0 + apigo.cc/go/watch v1.5.2 github.com/gorilla/websocket v1.5.3 golang.org/x/net v0.54.0 ) require ( - apigo.cc/go/crypto v1.5.2 // indirect - apigo.cc/go/encoding v1.5.3 // indirect - apigo.cc/go/rand v1.5.2 // indirect - apigo.cc/go/shell v1.5.2 // indirect + apigo.cc/go/crypto v1.5.3 // indirect + apigo.cc/go/encoding v1.5.4 // indirect + apigo.cc/go/rand v1.5.3 // indirect + apigo.cc/go/shell v1.5.3 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect + github.com/gobwas/glob v0.2.3 // indirect github.com/gomodule/redigo v2.0.0+incompatible // indirect golang.org/x/crypto v0.52.0 // indirect golang.org/x/sys v0.45.0 // indirect diff --git a/go.sum b/go.sum index 7d85044..bd1178c 100644 --- a/go.sum +++ b/go.sum @@ -1,33 +1,41 @@ -apigo.cc/go/cast v1.5.0 h1:UBGJtFQ8eJPMQXs37cUgqd7YQo1zI9opuSDBDmn2/pE= -apigo.cc/go/cast v1.5.0/go.mod h1:z2GW5p5WCZGEqVVIJUdhl232vRbLf2Qu4EDlEakX/D8= -apigo.cc/go/config v1.5.1 h1:rpj7oCzlsDV3f2/YK3Pb+CHbfr2DL5Vyyv6VNkobJP4= -apigo.cc/go/config v1.5.1/go.mod h1:jdMiDLPa9gzB8/FFZvm9jOopUqdxb7XSX+0OeWcZZUM= -apigo.cc/go/crypto v1.5.0 h1:Nxz7a6VKCdvaF258IU0NkjQyureOLxfR308Sy2iftUI= -apigo.cc/go/crypto v1.5.0/go.mod h1:F9M6nXv+5328r1ZwbTvI6fcr8VdgqHVzALOcsdv6ntE= -apigo.cc/go/discover v1.5.0 h1:RGHulidyAHCZdGfpFytFUl3ur4aNVMXKlfJbAMCvgpo= -apigo.cc/go/discover v1.5.0/go.mod h1:nA5DQlmhzjGBZVqpEbZRNDIKNU+Sr9trxbKAgDxjZ+I= -apigo.cc/go/encoding v1.5.0 h1:EJNdRVDOMoI2DAvZwQNQTbYuqB/6zsEzvg7lS5pQI+I= -apigo.cc/go/encoding v1.5.0/go.mod h1:8++NfZj3hWig0qh2g7GQRw/4LpSvCYMWUZ+8J+x58cA= -apigo.cc/go/file v1.5.0 h1:Fh1NSDBqaxjuXYJ71yPHPXVJ8BFEv/AGS3l+jkLi5uw= -apigo.cc/go/file v1.5.0/go.mod h1:4YhOGgBINTpmmmgws3H8LAyXQQBGzBp44hYUoCS+kr0= -apigo.cc/go/http v1.5.0 h1:GGIu0dhMjTiYygxH9NWOzz6AY+WZjfyTL1qZ8G9vI1U= -apigo.cc/go/http v1.5.0/go.mod h1:CIIH7HS6wdicLpSgkEVozdDcHlM9W9ygmmzJvzhAKWg= -apigo.cc/go/id v1.5.0 h1:MjNWPhBhDsoXaLeJDv/0wfJmVMU9EvOs8pWYfsTQ6e8= -apigo.cc/go/id v1.5.0/go.mod h1:qhu4a1/KLc/XcBpcsRu+mXZt7U7Wvd9zMcPs4VspuPA= -apigo.cc/go/jsmod v1.5.0 h1:JgQtJNiJWy1NOP9AzE8NX5VXJkpO/x3GqLsCCSny5Ec= -apigo.cc/go/jsmod v1.5.0/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw= -apigo.cc/go/log v1.5.5 h1:AFU7d7AQxkpgDHl7SnlEwd6yzGSFAlnrrjbrNDQnQHI= -apigo.cc/go/rand v1.5.0 h1:1o8hh8fhdBuk1/h02IvugvamuT3dkWbVJrqEJVQKB2E= -apigo.cc/go/rand v1.5.0/go.mod h1:Lh98S2dm9UY0X+M+kNQQEKyXHG5pcCKSFPyXN0QCGdk= -apigo.cc/go/redis v1.5.0 h1:VXNDqzKj87BchF7ubDEH+T6lp8NrjeK0izU4ooo7u1A= -apigo.cc/go/redis v1.5.0/go.mod h1:/olsrHndkUNezUX1KbBBt8b4Got7SX7E8EJzcb1PknM= -apigo.cc/go/safe v1.5.0 h1:W1NblmcU8cex1f9Y5z8mNLUJOzZTE1s6fszb3FbhGnk= -apigo.cc/go/safe v1.5.0/go.mod h1:OfQ5d6COePSGEuPvMeOk6KagX2sezw7nvKh7exj9SeM= -apigo.cc/go/shell v1.5.0 h1:WLDMMqUU0INeaBDmQsTPr0h/NfB2RknAtiJ5NL467+Q= -apigo.cc/go/shell v1.5.0/go.mod h1:rYHA77d5hEsQHcJrbAWf1pHy0sxayeJ0gU55LA/JWQk= -apigo.cc/go/starter v1.5.3 h1:kakDapul+l63w3Ah1pnBxD1mup9Fbt821omWCiaGwCE= +apigo.cc/go/cast v1.5.3 h1:jk6VX0rGFhjKtfPhsaV6IKYpiGmORRk9qPTtuNS53tw= +apigo.cc/go/cast v1.5.3/go.mod h1:GMjjrYn93tWat1U409G7h1jR3ejfLLI7r0efBo9Sbd4= +apigo.cc/go/config v1.5.3 h1:peq1FM2xO+vzPHJf8Dwg3DXm8PtFQMfTFKQj6fpoG7A= +apigo.cc/go/config v1.5.3/go.mod h1:ZiOAjWa1mQIzszaJZN+kO6YU4GXreng+NxkcK/TAkqQ= +apigo.cc/go/crypto v1.5.3 h1:2JUHC2cgR2zrnn36EzwkUAdxmmTXAA/8yTNo+2X1mPE= +apigo.cc/go/crypto v1.5.3/go.mod h1:PheYKHEXmoEFI1AK5PpY1borQWcRlkkSaWncT3cWbhE= +apigo.cc/go/discover v1.5.3 h1:WW1A7qReYytebETDb5MhVRPmT10KReGM4QbPTfqS/iw= +apigo.cc/go/discover v1.5.3/go.mod h1:rylFiGvfNro090rGtlc9K31KATn6WZbnn1Fk2AbQZEU= +apigo.cc/go/encoding v1.5.4 h1:Fk8TrveZATyy8SHukC4ZiqdTSp+QIfsRHtt55xmMK7w= +apigo.cc/go/encoding v1.5.4/go.mod h1:dShEsZ3gKqBINz7TSOYf4e7/fBCqCY9VzlenoGUQUFM= +apigo.cc/go/file v1.5.5 h1:/+HmDumLu6Qk2KuQL63M9lpgzHTDL+QJ8dStOl7e9gs= +apigo.cc/go/file v1.5.5/go.mod h1:xRVNhctvqOKeBemmcRW/BQfgkc3B+vT/UZVdSc7duUo= +apigo.cc/go/http v1.5.3 h1:nvJh9bqPPcPRv6p8WEw7bJAd0UC+r2zvQA8/QioVLTQ= +apigo.cc/go/http v1.5.3/go.mod h1:cFrPK61y9f1PrsNSJscZT/QVOgkT15o9OP7O8cuMb8Q= +apigo.cc/go/id v1.5.4 h1:D1Zx9gEZhOgdTgZ4SdmPImhpc9xGiOA33Y+j2MkstzQ= +apigo.cc/go/id v1.5.4/go.mod h1:hCTQq+KC1ALWe1FpPERf+W4B6FSulg9FAgOUJDDySiY= +apigo.cc/go/jsmod v1.5.3 h1:S3W317bH0QV2NMeRO1E0v6ySIBOfMWYv/NuQJbvqKWU= +apigo.cc/go/jsmod v1.5.3/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw= +apigo.cc/go/log v1.5.8 h1:/IYtGPWhRjT3OayylDIphkWZIQbpLjqVeSnFEiD3Dy0= +apigo.cc/go/log v1.5.8/go.mod h1:HfFPANMYxJx197SSTXB21Pgxcz/gGqPP8nlSErgd5WE= +apigo.cc/go/rand v1.5.3 h1:O4bPIwyaOWEBCr0nL9A4G4qG48AqiGTCzfPeckm3Ius= +apigo.cc/go/rand v1.5.3/go.mod h1:q1BTFkY/cXE229dDD5Q22lF7T0DoKPV6xAu+6bCrDH4= +apigo.cc/go/redis v1.5.6 h1:Lzo8M2binfqdQdVVp31Z/Max4qT8D82QdZjLlLQsrIY= +apigo.cc/go/redis v1.5.6/go.mod h1:HmqSh2Ll7/b2zFXDi2Ap13YOuMCVniuZNbwtxkbIYII= +apigo.cc/go/safe v1.5.2 h1:EnuEOW/SGwf/5A0nw9LnqfKJE071+TIc6ez8HI9R9Lg= +apigo.cc/go/safe v1.5.2/go.mod h1:2GqCCLLGex4OAhdET3iBWm1R+LIYtmTrvHP8W0iESSw= +apigo.cc/go/shell v1.5.3 h1:pI+u12sy6upoygq+1XXqUlvUboBfH4Q52jRpoJFv56A= +apigo.cc/go/shell v1.5.3/go.mod h1:FdZWUrcXHGJXo725oSyHqAeFoX0E9yY3PDhrz9hujgY= +apigo.cc/go/starter v1.5.5 h1:4ST02o4qP8IIekxtd9Jhx5RHTrSGXtVQUguSIXV0iWc= +apigo.cc/go/starter v1.5.5/go.mod h1:WAGhdtmZdpP1Jn/z0pCqHwpTbqqaFhm5OqH7QVtcanY= apigo.cc/go/timer v1.5.0 h1:iPo/IQn+iuhBRI1/MR1txwZnamef/RBBfOiIlBiqkgk= apigo.cc/go/timer v1.5.0/go.mod h1:kOnqTTX+zA4AH7SfC+LpUm4ZvS+DVyWWMqul/V5QWJs= +apigo.cc/go/watch v1.5.2 h1:zG56PD8Vml5gVJeo2yNuX9s6stOaIJRYqqKGqXK+xTU= +apigo.cc/go/watch v1.5.2/go.mod h1:MW1XnI0MVyUZpRy590no1vrDT+U4y20L0UZW9/LgU+k= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= diff --git a/handler.go b/handler.go index 91f0454..c229110 100644 --- a/handler.go +++ b/handler.go @@ -22,6 +22,9 @@ type RouteHandler struct { func (rh *RouteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ws := rh.ws + if ws == nil { + ws = DefaultServer + } atomic.AddInt64(&rh.webRequestingNum, 1) defer atomic.AddInt64(&rh.webRequestingNum, -1) diff --git a/js_export.go b/js_export.go index 094a505..b1c91e3 100644 --- a/js_export.go +++ b/js_export.go @@ -7,17 +7,31 @@ import ( func init() { jsmod.Register("service", map[string]any{ // 类型占位工厂 (用于 AI 发现类型结构,生成文档时隐藏) - "__exportRequest": func() *Request { return &Request{} }, - "__exportResponse": func() *Response { return &Response{} }, - "__exportWebSocket": func() *WebSocketConn { return &WebSocketConn{} }, - "__exportSession": func() *Session { return &Session{} }, - "__exportFile": func() *jsUploadFile { return &jsUploadFile{} }, + "__exportRequest": jsExportRequest, + "__exportResponse": jsExportResponse, + "__exportWebSocket": jsExportWebSocket, + "__exportSession": jsExportSession, + "__exportFile": jsExportFile, // 功能函数 - "Upgrade": Upgrade, + "Upgrade": jsUpgrade, }) } +func jsExportRequest() *Request { return &Request{} } +func jsExportResponse() *Response { return &Response{} } +func jsExportWebSocket() *WebSocketConn { return &WebSocketConn{} } +func jsExportSession() *Session { return &Session{} } +func jsExportFile() *jsUploadFile { return &jsUploadFile{} } + +func jsUpgrade(response *Response, request *Request) (*WebSocketConn, error) { + conn, err := Upgrade(response, request) + if err != nil { + return nil, jsmod.MakeError(err) + } + return conn, nil +} + // jsUploadFile 包装 UploadFile 以隐藏敏感方法 type jsUploadFile struct { f *UploadFile @@ -41,5 +55,9 @@ func (j *jsUploadFile) Content() ([]byte, error) { if j.f == nil { return nil, nil } - return j.f.Content() + res, err := j.f.Content() + if err != nil { + return nil, jsmod.MakeError(err) + } + return res, nil } diff --git a/response.go b/response.go index ad53122..faf758f 100644 --- a/response.go +++ b/response.go @@ -3,6 +3,7 @@ package service import ( "apigo.cc/go/cast" "apigo.cc/go/file" + "apigo.cc/go/jsmod" "io" "net/http" ) @@ -74,7 +75,11 @@ func (r *Response) Write(bytes []byte) (int, error) { if r.ProxyHeader != nil { r.copyProxyHeader() } - return r.Writer.Write(bytes) + n, err := r.Writer.Write(bytes) + if err != nil { + return n, jsmod.MakeError(err) + } + return n, nil } // PhysicalWrite 物理写入网线,绕过过滤器缓冲逻辑 @@ -83,12 +88,20 @@ func (r *Response) PhysicalWrite(bytes []byte) (int, error) { if r.ProxyHeader != nil { r.copyProxyHeader() } - return r.Writer.Write(bytes) + n, err := r.Writer.Write(bytes) + if err != nil { + return n, jsmod.MakeError(err) + } + return n, nil } // WriteString 写入字符串响应 func (r *Response) WriteString(s string) (int, error) { - return r.Write([]byte(s)) + n, err := r.Write([]byte(s)) + if err != nil { + return n, jsmod.MakeError(err) + } + return n, nil } // WriteHeader 设置响应状态码 diff --git a/session.go b/session.go index f44e7ef..939b6dc 100644 --- a/session.go +++ b/session.go @@ -2,6 +2,7 @@ package service import ( "apigo.cc/go/cast" + "apigo.cc/go/jsmod" "apigo.cc/go/log" "apigo.cc/go/redis" "errors" @@ -125,7 +126,7 @@ func (s *Session) Save() error { return nil } else { if !s.conn.SETEX("SESS_"+s.id, timeout, s.data) { - return errors.New("redis save failed") + return jsmod.MakeError(errors.New("redis save failed")) } return nil } diff --git a/utility.go b/utility.go index dc46e98..04bb2bc 100644 --- a/utility.go +++ b/utility.go @@ -28,6 +28,9 @@ func GetDefaultName() string { // GetServerIp 获取真实局域网 IP (UDP 8.8.8.8 伪拨号法) func GetServerIp() string { + if ip := os.Getenv("SERVICE_IP"); ip != "" { + return ip + } conn, err := net.Dial("udp", "8.8.8.8:80") if err == nil { localAddr := conn.LocalAddr().(*net.UDPAddr) diff --git a/websocket.go b/websocket.go index e501c2a..21b6f51 100644 --- a/websocket.go +++ b/websocket.go @@ -1,6 +1,7 @@ package service import ( + "apigo.cc/go/jsmod" "apigo.cc/go/log" "github.com/gorilla/websocket" "net/http" @@ -18,43 +19,60 @@ type WebSocketConn struct { // Send 发送消息,支持 string, []byte 或 自动转 JSON func (c *WebSocketConn) Send(data any) error { + var err error switch v := data.(type) { case string: - return c.Conn.WriteMessage(websocket.TextMessage, []byte(v)) + err = c.Conn.WriteMessage(websocket.TextMessage, []byte(v)) case []byte: - return c.Conn.WriteMessage(websocket.BinaryMessage, v) + err = c.Conn.WriteMessage(websocket.BinaryMessage, v) default: - return c.Conn.WriteJSON(v) + err = c.Conn.WriteJSON(v) } + if err != nil { + return jsmod.MakeError(err) + } + return nil } // ReadString 读取字符串消息 func (c *WebSocketConn) ReadString() (string, error) { _, b, err := c.Conn.ReadMessage() - return string(b), err + if err != nil { + return "", jsmod.MakeError(err) + } + return string(b), nil } // ReadBytes 读取二进制消息 func (c *WebSocketConn) ReadBytes() ([]byte, error) { _, b, err := c.Conn.ReadMessage() - return b, err + if err != nil { + return nil, jsmod.MakeError(err) + } + return b, nil } // ReadJSON 读取 JSON 消息 func (c *WebSocketConn) ReadJSON(v any) error { - return c.Conn.ReadJSON(v) + if err := c.Conn.ReadJSON(v); err != nil { + return jsmod.MakeError(err) + } + return nil } // Close 关闭连接 func (c *WebSocketConn) Close() error { - return c.Conn.Close() + if err := c.Conn.Close(); err != nil { + return jsmod.MakeError(err) + } + return nil } // Upgrade 将 HTTP 请求升级为 WebSocket 连接 func Upgrade(response *Response, request *Request) (*WebSocketConn, error) { conn, err := defaultUpgrader.Upgrade(response.Writer, request.Request, nil) if err != nil { - return nil, err + return nil, jsmod.MakeError(err) } return &WebSocketConn{Conn: conn}, nil }