refactor(service): deep alignment with low-code environment (Header/Cookie/Shadowing)

This commit is contained in:
AI Engineer 2026-06-05 19:05:21 +08:00
parent ff34d11c9b
commit 94a4be81ec
6 changed files with 165 additions and 37 deletions

View File

@ -1,6 +1,12 @@
# CHANGELOG - go/service # 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 支持**: - **新特性: EnableWebDev 支持**:
- 引入了 `service.EnableWebDev(config watch.Config)`,支持自动刷新页面的开发模式。 - 引入了 `service.EnableWebDev(config watch.Config)`,支持自动刷新页面的开发模式。
- **WebSocket 同步**: 自动注册 `/_watch` 服务,与文件监听器协同工作。 - **WebSocket 同步**: 自动注册 `/_watch` 服务,与文件监听器协同工作。

View File

@ -85,7 +85,7 @@ func (rh *RouteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 过滤响应头 // 过滤响应头
respHeaders := make(map[string]string) respHeaders := make(map[string]string)
for k, v := range response.Header() { for k, v := range response.Header().H {
respHeaders[k] = strings.Join(v, ", ") respHeaders[k] = strings.Join(v, ", ")
} }
@ -310,7 +310,7 @@ func parseRequestArgs(request *Request, args map[string]any) {
// Form params // Form params
if request.Method == http.MethodPost || request.Method == http.MethodPut { 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") { if strings.HasPrefix(contentType, "application/json") {
body, _ := io.ReadAll(request.Body) body, _ := io.ReadAll(request.Body)
_ = request.Body.Close() _ = request.Body.Close()
@ -430,9 +430,9 @@ func outputResult(response *Response, result any) {
func (ws *webServer) handleClientKeys(request *Request, response *Response) { func (ws *webServer) handleClientKeys(request *Request, response *Response) {
// SessionId // SessionId
if ws.usedSessionIdKey != "" { if ws.usedSessionIdKey != "" {
sessionId := request.Header.Get(ws.usedSessionIdKey) sessionId := request.Header().Get(ws.usedSessionIdKey)
if sessionId == "" && !ws.Config.SessionWithoutCookie { 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 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) response.Header().Set(ws.usedSessionIdKey, sessionId)
} }
// DeviceId // DeviceId
if ws.usedDeviceIdKey != "" { if ws.usedDeviceIdKey != "" {
deviceId := request.Header.Get(ws.usedDeviceIdKey) deviceId := request.Header().Get(ws.usedDeviceIdKey)
if deviceId == "" && !ws.Config.DeviceWithoutCookie { 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 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) response.Header().Set(ws.usedDeviceIdKey, deviceId)
} }
} }

View File

@ -6,12 +6,12 @@ import (
func init() { func init() {
jsmod.Register("service", map[string]any{ jsmod.Register("service", map[string]any{
// 类型占位工厂 (用于 AI 发现类型结构) // 类型占位工厂 (用于 AI 发现类型结构,生成文档时隐藏)
"newRequest": func() *Request { return &Request{} }, "__exportRequest": func() *Request { return &Request{} },
"newResponse": func() *Response { return &Response{} }, "__exportResponse": func() *Response { return &Response{} },
"newWebSocket": func() *WebSocketConn { return &WebSocketConn{} }, "__exportWebSocket": func() *WebSocketConn { return &WebSocketConn{} },
"newSession": func() *Session { return &Session{} }, "__exportSession": func() *Session { return &Session{} },
"newFile": func() *jsUploadFile { return &jsUploadFile{} }, "__exportFile": func() *jsUploadFile { return &jsUploadFile{} },
// 功能函数 // 功能函数
"upgrade": Upgrade, "upgrade": Upgrade,

View File

@ -33,13 +33,120 @@ func (f *UploadFile) Content() ([]byte, error) {
return io.ReadAll(src) 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 // Request 封装 http.Request
type Request struct { type Request struct {
*http.Request *http.Request `js:"-"`
contextValues map[string]any contextValues map[string]any `js:"-"`
Id string 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 包装 // NewRequest 创建 Request 包装
func NewRequest(httpRequest *http.Request) *Request { func NewRequest(httpRequest *http.Request) *Request {
return &Request{ return &Request{
@ -68,11 +175,11 @@ func (r *Request) Get(key string) any {
// MakeUrl 根据当前请求构建完整 URL // MakeUrl 根据当前请求构建完整 URL
func (r *Request) MakeUrl(path string) string { func (r *Request) MakeUrl(path string) string {
scheme := r.Header.Get(discover.HeaderScheme) scheme := r.Header().Get(discover.HeaderScheme)
if scheme == "" { if scheme == "" {
scheme = "http" scheme = "http"
} }
host := r.Header.Get(discover.HeaderHost) host := r.Header().Get(discover.HeaderHost)
if host == "" { if host == "" {
host = r.Host host = r.Host
} }
@ -81,24 +188,24 @@ func (r *Request) MakeUrl(path string) string {
// DeviceId 获取设备 ID // DeviceId 获取设备 ID
func (r *Request) DeviceId() string { func (r *Request) DeviceId() string {
return r.Header.Get(discover.HeaderDeviceID) return r.Header().Get(discover.HeaderDeviceID)
} }
// SessionId 获取会话 ID // SessionId 获取会话 ID
func (r *Request) SessionId() string { func (r *Request) SessionId() string {
return r.Header.Get(discover.HeaderSessionID) return r.Header().Get(discover.HeaderSessionID)
} }
// SetUserId 设置用户 ID传递给下游 // SetUserId 设置用户 ID传递给下游
func (r *Request) SetUserId(userId string) { func (r *Request) SetUserId(userId string) {
r.Header.Set(discover.HeaderUserID, userId) r.Header().Set(discover.HeaderUserID, userId)
} }
// ClientIp 获取真实 IP // ClientIp 获取真实 IP
func (r *Request) ClientIp() string { func (r *Request) ClientIp() string {
ip := r.Header.Get(discover.HeaderClientIP) ip := r.Header().Get(discover.HeaderClientIP)
if ip == "" { if ip == "" {
ip = r.Header.Get(discover.HeaderForwardedFor) ip = r.Header().Get(discover.HeaderForwardedFor)
} }
if ip == "" { if ip == "" {
host, _, err := net.SplitHostPort(r.RemoteAddr) host, _, err := net.SplitHostPort(r.RemoteAddr)

View File

@ -10,16 +10,31 @@ import (
// Response 封装 http.ResponseWriter // Response 封装 http.ResponseWriter
type Response struct { type Response struct {
Id string Id string
Writer http.ResponseWriter Writer http.ResponseWriter `js:"-"`
Code int Code int
body []byte body []byte `js:"-"`
outLen int outLen int `js:"-"`
changed bool changed bool `js:"-"`
headerWritten bool headerWritten bool `js:"-"`
dontLog200 bool dontLog200 bool `js:"-"`
dontLogArgs []string dontLogArgs []string `js:"-"`
ProxyHeader *http.Header ProxyHeader *http.Header `js:"-"`
server *webServer 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 包装 // NewResponse 创建 Response 包装
@ -32,11 +47,11 @@ func NewResponse(writer http.ResponseWriter, server *webServer) *Response {
} }
// Header 获取响应头部 // Header 获取响应头部
func (r *Response) Header() http.Header { func (r *Response) Header() *Header {
if r.ProxyHeader != nil { if r.ProxyHeader != nil {
return *r.ProxyHeader return &Header{H: *r.ProxyHeader}
} }
return r.Writer.Header() return &Header{H: r.Writer.Header()}
} }
// Write 写入响应内容 // Write 写入响应内容

View File

@ -132,7 +132,7 @@ func (ws *webServer) processStatic(requestPath string, request *Request, respons
} }
// 检查 304 // 检查 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 t, err := time.Parse(http.TimeFormat, ifModifiedSince); err == nil {
if time.Unix(info.ModTime, 0).Truncate(time.Second).Before(t.Truncate(time.Second)) || 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)) { time.Unix(info.ModTime, 0).Truncate(time.Second).Equal(t.Truncate(time.Second)) {