Files
AskaEth 9253fef92a
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(qq_obv11): OneBot v11 四端齐备(裁决 1.4 / 12.4.1)
- 四端=ws×client / ws×server(原有,补语义注释)+ **http×client**(新增 http_client.go:
  调 OneBot HTTP API、请求-响应、retcode 判成败、token 只走 Authorization 头)+
  **http×server**(新增 http_server.go:监听 listen_addr 收上报、fail-closed 鉴权、10MiB 体上限)
- 配置面:适配器新增 transport / api_base_url / report_path(json tag 沿用既有口径);
  env 兜底 CHANNEL_QQ_OBV11_{TRANSPORT,API_BASE_URL,REPORT_PATH};**默认值=现状**(ws+server)
- 出站分流:session.go 发送路径按 transport 走 WS 写帧或 HTTP POST(不改它则 HTTP 端无法出站)
- 端口豁免边界:http+server 沿用 AGENTS §9 已登记的同一豁免(design/13 §0 原文即含 HTTP server),
  且 listen_addr 为空时报错、**绝不开端口**(与 ws 侧同名校验成对)
- 关键取舍(已上报):OneBot 的 HTTP 是「动作 + 上报」两半,mode 决定**哪一半必需**、
  另一半给齐字段就一并启用 ⇒ 单个适配器即可「收上报 + 回消息」,四端仍各自可独立选择与测试
- 复核:build/vet 绿 + qq_obv11/cmd 包测试绿 + `-race` 干净;10 条新用例(HTTP 两端全部用本地
  httptest 假 OneBot,未连真实 QQ);变异 3 条(伪造 retcode 成功 / 去掉上报鉴权门 / 默认值翻 http)已还原
- 未接:get_group_list(后续)、无 token 时放行(与既有 ws_server 一致,如需 fail-closed 请裁决)
2026-10-04 13:58:18 +08:00

78 lines
2.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package qq_obv11
// 本文件实现四个端里的 **ws + server**(裁决 1.4):Cyrene 监听 `listen_addr`,等
// OneBot 实现(NapCat / go-cqhttp 的「反向 WS」)反连上来——连接由 OneBot 发起
// (OneBot→Cyrene),建立后收发都走这条长连接。端口必须由配置显式给出,默认不开
// (AGENTS §9 的登记豁免)。鉴权与 HTTP server 端一致:配置了 access_token 时,
// Authorization: Bearer 或 ?access_token= 必须匹配,否则 403。
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"strings"
)
// connectServer starts the HTTP listener in server mode. It returns once the
// listener is bound; the session becomes live when a OneBot client connects.
func (a *Adapter) connectServer(ctx context.Context) error {
if a.cfg.ListenAddr == "" {
return errors.New("qq-obv11: server mode requires listenAddr")
}
mux := http.NewServeMux()
mux.HandleFunc("/", a.wsHandler)
ln, err := net.Listen("tcp", a.cfg.ListenAddr)
if err != nil {
return fmt.Errorf("qq-obv11: listen %s: %w", a.cfg.ListenAddr, err)
}
// 先把 *http.Server 落在局部变量上再交给 goroutine:Disconnect 会把 a.srv 置 nil,
// 直接读 a.srv 会与它构成数据竞争。
srv := &http.Server{Handler: mux}
a.mu.Lock()
a.ln = ln
a.srv = srv
a.mu.Unlock()
go func() {
if serveErr := srv.Serve(ln); serveErr != nil && serveErr != http.ErrServerClosed {
a.log.Warn("websocket server stopped", "err", serveErr)
}
}()
a.log.Info("websocket server listening (server mode)", "addr", ln.Addr().String())
return nil
}
// wsHandler is the HTTP handler that upgrades one inbound OneBot client
// connection to a WebSocket session.
func (a *Adapter) wsHandler(w http.ResponseWriter, r *http.Request) {
if a.cfg.AccessToken != "" && !a.checkAuth(r) {
http.Error(w, "Forbidden: invalid access_token", http.StatusForbidden)
return
}
conn, err := upgradeWS(w, r)
if err != nil {
a.log.Warn("websocket upgrade failed", "err", err)
return
}
a.setConn(conn)
a.log.Info("onebot client connected (server mode)", "remote", r.RemoteAddr)
}
// checkAuth validates the access token carried by an upgrade request. OneBot
// implementations pass it either as an access_token query parameter or as an
// Authorization: Bearer header.
func (a *Adapter) checkAuth(r *http.Request) bool {
if got := r.URL.Query().Get("access_token"); got != "" {
return got == a.cfg.AccessToken
}
auth := r.Header.Get("Authorization")
if strings.HasPrefix(auth, "Bearer ") {
return strings.TrimPrefix(auth, "Bearer ") == a.cfg.AccessToken
}
return false
}