diff --git a/CHANGELOG.md b/CHANGELOG.md index d1ffa0e..2aa8118 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # CHANGELOG - go/service -## v1.5.6 (2026-06-05) +## v1.5.9 (2026-06-05) +- **优化: 低代码环境深度对齐**: + - **JS 友好型 Header**: 引入 \`service.Header\` 包装类,提供大小写不敏感的 \`Get/Set/Add/Del\` 方法,提升脚本开发体验。 + - **Cookie 遮蔽**: 在 \`Request\` 中实现方法遮蔽,确保 JS 侧看到的 Cookie 参数均为简化的 \`Service_Cookie\`,彻底解决穿透问题。 + - **API 统一**: 将 \`Request.Headers()\` 重命名为 \`Request.Header()\`,与 \`Response.Header()\` 保持命名对齐。 +- **重构**: 给内部字段(如 \`*http.Request\`, \`ResponseWriter\`)增加 \`js:"-"\` 标签,精准管控对 JS 暴露的 API 边界。 +- **修复**: 解决了因 Header 包装导致的 Go 内部代码(\`handler.go\`, \`static.go\`)编译错误。 - **新特性: EnableWebDev 支持**: - 引入了 `service.EnableWebDev(config watch.Config)`,支持自动刷新页面的开发模式。 - **WebSocket 同步**: 自动注册 `/_watch` 服务,与文件监听器协同工作。 diff --git a/handler.go b/handler.go index 01fce74..040a23f 100644 --- a/handler.go +++ b/handler.go @@ -85,7 +85,7 @@ func (rh *RouteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // 过滤响应头 respHeaders := make(map[string]string) - for k, v := range response.Header() { + for k, v := range response.Header().H { respHeaders[k] = strings.Join(v, ", ") } @@ -310,7 +310,7 @@ func parseRequestArgs(request *Request, args map[string]any) { // Form params if request.Method == http.MethodPost || request.Method == http.MethodPut { - contentType := request.Header.Get("Content-Type") + contentType := request.Header().Get("Content-Type") if strings.HasPrefix(contentType, "application/json") { body, _ := io.ReadAll(request.Body) _ = request.Body.Close() @@ -430,9 +430,9 @@ func outputResult(response *Response, result any) { func (ws *webServer) handleClientKeys(request *Request, response *Response) { // SessionId if ws.usedSessionIdKey != "" { - sessionId := request.Header.Get(ws.usedSessionIdKey) + sessionId := request.Header().Get(ws.usedSessionIdKey) if sessionId == "" && !ws.Config.SessionWithoutCookie { - if ck, err := request.Cookie(ws.usedSessionIdKey); err == nil { + if ck := request.GetCookie(ws.usedSessionIdKey); ck != nil { sessionId = ck.Value } } @@ -451,15 +451,15 @@ func (ws *webServer) handleClientKeys(request *Request, response *Response) { }) } } - request.Header.Set(discover.HeaderSessionID, sessionId) + request.Request.Header.Set(discover.HeaderSessionID, sessionId) response.Header().Set(ws.usedSessionIdKey, sessionId) } // DeviceId if ws.usedDeviceIdKey != "" { - deviceId := request.Header.Get(ws.usedDeviceIdKey) + deviceId := request.Header().Get(ws.usedDeviceIdKey) if deviceId == "" && !ws.Config.DeviceWithoutCookie { - if ck, err := request.Cookie(ws.usedDeviceIdKey); err == nil { + if ck := request.GetCookie(ws.usedDeviceIdKey); ck != nil { deviceId = ck.Value } } @@ -475,7 +475,7 @@ func (ws *webServer) handleClientKeys(request *Request, response *Response) { }) } } - request.Header.Set(discover.HeaderDeviceID, deviceId) + request.Request.Header.Set(discover.HeaderDeviceID, deviceId) response.Header().Set(ws.usedDeviceIdKey, deviceId) } } diff --git a/js_export.go b/js_export.go index 5eb118e..35909b4 100644 --- a/js_export.go +++ b/js_export.go @@ -6,12 +6,12 @@ import ( func init() { jsmod.Register("service", map[string]any{ - // 类型占位工厂 (用于 AI 发现类型结构) - "newRequest": func() *Request { return &Request{} }, - "newResponse": func() *Response { return &Response{} }, - "newWebSocket": func() *WebSocketConn { return &WebSocketConn{} }, - "newSession": func() *Session { return &Session{} }, - "newFile": func() *jsUploadFile { return &jsUploadFile{} }, + // 类型占位工厂 (用于 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{} }, // 功能函数 "upgrade": Upgrade, diff --git a/request.go b/request.go index 9fec36a..d59aa55 100644 --- a/request.go +++ b/request.go @@ -33,13 +33,120 @@ func (f *UploadFile) Content() ([]byte, error) { return io.ReadAll(src) } +// Header 包装 http.Header 以提供 JS 友好的方法 +type Header struct { + H http.Header `js:"-"` +} + +func (h *Header) Get(key string) string { + return h.H.Get(key) +} + +func (h *Header) Set(key, value string) { + h.H.Set(key, value) +} + +func (h *Header) Add(key, value string) { + h.H.Add(key, value) +} + +func (h *Header) Del(key string) { + h.H.Del(key) +} + +func (h *Header) Values(key string) []string { + return h.H.Values(key) +} + // Request 封装 http.Request type Request struct { - *http.Request - contextValues map[string]any + *http.Request `js:"-"` + contextValues map[string]any `js:"-"` Id string } +// Cookie 简化的 JS 友好 Cookie 结构 +type Cookie struct { + Name string + Value string + Path string + Domain string + MaxAge int + Secure bool + HttpOnly bool +} + +func (r *Request) GetCookie(name string) *Cookie { + c, err := r.Request.Cookie(name) + if err != nil { + return nil + } + return &Cookie{ + Name: c.Name, + Value: c.Value, + Path: c.Path, + Domain: c.Domain, + MaxAge: c.MaxAge, + Secure: c.Secure, + HttpOnly: c.HttpOnly, + } +} + +func (r *Request) GetCookies() []*Cookie { + res := make([]*Cookie, 0) + for _, c := range r.Request.Cookies() { + res = append(res, &Cookie{ + Name: c.Name, + Value: c.Value, + Path: c.Path, + Domain: c.Domain, + MaxAge: c.MaxAge, + Secure: c.Secure, + HttpOnly: c.HttpOnly, + }) + } + return res +} + +// AddCookie 遮蔽原生的 AddCookie +func (r *Request) AddCookie(c *Cookie) { + if c == nil { + return + } + r.Request.AddCookie(&http.Cookie{ + Name: c.Name, + Value: c.Value, + Path: c.Path, + Domain: c.Domain, + MaxAge: c.MaxAge, + Secure: c.Secure, + HttpOnly: c.HttpOnly, + }) +} + +// CookiesNamed 遮蔽原生的 CookiesNamed +func (r *Request) CookiesNamed(name string) []*Cookie { + res := make([]*Cookie, 0) + for _, c := range r.Request.Cookies() { + if c.Name == name { + res = append(res, &Cookie{ + Name: c.Name, + Value: c.Value, + Path: c.Path, + Domain: c.Domain, + MaxAge: c.MaxAge, + Secure: c.Secure, + HttpOnly: c.HttpOnly, + }) + } + } + return res +} + +func (r *Request) Header() *Header { + return &Header{H: r.Request.Header} +} + // NewRequest 创建 Request 包装 func NewRequest(httpRequest *http.Request) *Request { return &Request{ @@ -68,11 +175,11 @@ func (r *Request) Get(key string) any { // MakeUrl 根据当前请求构建完整 URL func (r *Request) MakeUrl(path string) string { - scheme := r.Header.Get(discover.HeaderScheme) + scheme := r.Header().Get(discover.HeaderScheme) if scheme == "" { scheme = "http" } - host := r.Header.Get(discover.HeaderHost) + host := r.Header().Get(discover.HeaderHost) if host == "" { host = r.Host } @@ -81,24 +188,24 @@ func (r *Request) MakeUrl(path string) string { // DeviceId 获取设备 ID func (r *Request) DeviceId() string { - return r.Header.Get(discover.HeaderDeviceID) + return r.Header().Get(discover.HeaderDeviceID) } // SessionId 获取会话 ID func (r *Request) SessionId() string { - return r.Header.Get(discover.HeaderSessionID) + return r.Header().Get(discover.HeaderSessionID) } // SetUserId 设置用户 ID(传递给下游) func (r *Request) SetUserId(userId string) { - r.Header.Set(discover.HeaderUserID, userId) + r.Header().Set(discover.HeaderUserID, userId) } // ClientIp 获取真实 IP func (r *Request) ClientIp() string { - ip := r.Header.Get(discover.HeaderClientIP) + ip := r.Header().Get(discover.HeaderClientIP) if ip == "" { - ip = r.Header.Get(discover.HeaderForwardedFor) + ip = r.Header().Get(discover.HeaderForwardedFor) } if ip == "" { host, _, err := net.SplitHostPort(r.RemoteAddr) diff --git a/response.go b/response.go index 4fe6120..ad67a45 100644 --- a/response.go +++ b/response.go @@ -10,16 +10,31 @@ import ( // Response 封装 http.ResponseWriter type Response struct { Id string - Writer http.ResponseWriter + Writer http.ResponseWriter `js:"-"` Code int - body []byte - outLen int - changed bool - headerWritten bool - dontLog200 bool - dontLogArgs []string - ProxyHeader *http.Header - server *webServer + body []byte `js:"-"` + outLen int `js:"-"` + changed bool `js:"-"` + headerWritten bool `js:"-"` + dontLog200 bool `js:"-"` + dontLogArgs []string `js:"-"` + ProxyHeader *http.Header `js:"-"` + server *webServer `js:"-"` +} + +func (r *Response) SetCookie(cookie *Cookie) { + if cookie == nil { + return + } + http.SetCookie(r.Writer, &http.Cookie{ + Name: cookie.Name, + Value: cookie.Value, + Path: cookie.Path, + Domain: cookie.Domain, + MaxAge: cookie.MaxAge, + Secure: cookie.Secure, + HttpOnly: cookie.HttpOnly, + }) } // NewResponse 创建 Response 包装 @@ -32,11 +47,11 @@ func NewResponse(writer http.ResponseWriter, server *webServer) *Response { } // Header 获取响应头部 -func (r *Response) Header() http.Header { +func (r *Response) Header() *Header { if r.ProxyHeader != nil { - return *r.ProxyHeader + return &Header{H: *r.ProxyHeader} } - return r.Writer.Header() + return &Header{H: r.Writer.Header()} } // Write 写入响应内容 diff --git a/static.go b/static.go index e92caf0..c9a708b 100644 --- a/static.go +++ b/static.go @@ -132,7 +132,7 @@ func (ws *webServer) processStatic(requestPath string, request *Request, respons } // 检查 304 - if ifModifiedSince := request.Header.Get("If-Modified-Since"); ifModifiedSince != "" { + if ifModifiedSince := request.Header().Get("If-Modified-Since"); ifModifiedSince != "" { if t, err := time.Parse(http.TimeFormat, ifModifiedSince); err == nil { if time.Unix(info.ModTime, 0).Truncate(time.Second).Before(t.Truncate(time.Second)) || time.Unix(info.ModTime, 0).Truncate(time.Second).Equal(t.Truncate(time.Second)) {