refactor(weixin-ilink): 措辞纠正 —「会话过期」→「会话被 iLink 拒绝」(零逻辑改动)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

事实(主人 2026-10-03 确认):iLink 的 bot token 是长期凭据(一个月前扫码的仍可用);errcode=-14 是 iLink 侧拒绝/作废该会话,不是时间性过期;且至今未观测到真实 -14(当日全部日志 0 次)。此前代码把这些写成「过期」,属沿用的旧命名,现纠正。
- 纯改名/文案:ErrSessionExpired→ErrSessionRejected、errSessionExpired→errcodeSessionRejected、expired/expiredToken→rejected/rejectedToken、markExpired/expiredLocked/expiredState→markRejected/rejectedLocked/rejectedState;409 错误码 session_expired→session_rejected
- 行为一字未改:显式标记 / Connect 不假 OK / 面板 needsRescan+lastError / 扫码或换新 token 即清除 全部保留
- 微信协议自身的「二维码过期」与 auth 的 JWT/refresh 过期一律未动(那些是真·时间性过期)
- Docs/api-client v0.39 补事实说明;顺手收尾 3 处残留注释(admin.go / channelbridge/inbound.go / readloop_test.go)
This commit is contained in:
AskaEth committed 2026-10-03 18:26:27 +08:00
1 parent cdc370dc82
commit a51aadeef4
14 files changed
+205 -192

No files matched your search

+1 -1
View File
@@ -24,7 +24,7 @@ import (
// messages alone.
//
// When the read path returns with readCtx still alive, the adapter stopped
// reading on its own — today that is the weixin-iLink expired session
// reading on its own — today that is the weixin-iLink rejecting the session
// (errcode=-14, see plugins/channels/weixin_ilink/session_state.go), which used to
// leave the bridge claiming the channel was connected while it could no longer
// deliver anything. A cancelled readCtx is a deliberate stop (bridge Stop or a
@@ -10,7 +10,7 @@ import (
)
// deadSessionAdapter mimics a channel whose session dies while the bridge is
// running — today the weixin-iLink expired bot token (iLink errcode=-14): Connect
// running — today the iLink session rejection (errcode=-14): Connect
// reports success, the read loop then returns on its own (nothing cancelled it)
// and IsConnected reports false from that point on.
type deadSessionAdapter struct {
+1 -1
View File
@@ -178,7 +178,7 @@ func (s *Server) adminListChannels(w http.ResponseWriter, r *http.Request) {
continue
}
sum := ChannelSummary{Name: platform, Connected: ch.IsConnected()}
// Optional rescan capability (weixin-iLink expired token, v0.39): reported
// Optional rescan capability (weixin-iLink rejected session, v0.39): reported
// only when the adapter really asks for a re-scan, so a healthy adapter
// keeps the fields omitted rather than shipping a fabricated value.
if ra, ok := asRescanAware(cap.Value); ok {
@@ -18,7 +18,7 @@ import (
// rescanStubAdapter is a channel adapter that also exposes the optional rescan
// capability, exactly as *weixin_ilink.Adapter does once iLink answers
// errcode=-14 (expired bot token, re-scan required).
// errcode=-14 (session rejected, re-scan required).
type rescanStubAdapter struct {
name string
connected bool
@@ -58,12 +58,12 @@ var (
)
// TestAdminChannelsReportsNeedsRescan asserts the panel can tell "needs a fresh
// scan" apart from plain "not connected": the expired adapter's row carries
// scan" apart from plain "not connected": the rejected adapter's row carries
// needsRescan + the reason, and a healthy adapter omits both rather than shipping
// a fabricated value.
func TestAdminChannelsReportsNeedsRescan(t *testing.T) {
t.Parallel()
const reason = "iLink getupdates 返回 errcode=-14(bot token 已过期)"
const reason = "iLink getupdates 返回 errcode=-14(本次会话已被 iLink 拒绝)"
caps := capreg.New()
// Registered through the wrapper, the way the composition root registers it.
if err := caps.Register(capreg.Capability{
@@ -96,10 +96,10 @@ func TestAdminChannelsReportsNeedsRescan(t *testing.T) {
t.Fatalf("no weixin-ilink row in %+v", items)
}
if wx.Connected {
t.Error("weixin-ilink connected = true, want false while the token is expired")
t.Error("weixin-ilink connected = true, want false while iLink rejects the session")
}
if !wx.NeedsRescan {
t.Error("weixin-ilink needsRescan = false, want true (expired token must ask for a re-scan)")
t.Error("weixin-ilink needsRescan = false, want true (a rejected session must ask for a re-scan)")
}
if wx.LastError != reason {
t.Errorf("weixin-ilink lastError = %q, want %q", wx.LastError, reason)
@@ -135,10 +135,10 @@ func TestAdminChannelsReportsNeedsRescan(t *testing.T) {
}
}
// TestAdminChannelOpenExpiredSessionReturns409 asserts the open endpoint answers
// honestly for a session a reconnect cannot fix: 409 session_expired with the
// TestAdminChannelOpenRejectedSessionReturns409 asserts the open endpoint answers
// honestly for a session a reconnect cannot fix: 409 session_rejected with the
// re-scan instruction, never a 200 that would make the panel show "connected".
func TestAdminChannelOpenExpiredSessionReturns409(t *testing.T) {
func TestAdminChannelOpenRejectedSessionReturns409(t *testing.T) {
t.Parallel()
caps := capreg.New()
if err := caps.Register(capreg.Capability{
@@ -146,8 +146,8 @@ func TestAdminChannelOpenExpiredSessionReturns409(t *testing.T) {
Kind: capreg.KindChannel,
Value: &rescanStubAdapter{
name: "weixin-ilink", rescan: true,
reason: "iLink bot token 已过期",
connectErr: weixin_ilink.ErrSessionExpired,
reason: "iLink 返回 errcode=-14(本次会话已被 iLink 拒绝)",
connectErr: weixin_ilink.ErrSessionRejected,
},
}); err != nil {
t.Fatalf("register weixin-ilink: %v", err)
@@ -167,8 +167,8 @@ func TestAdminChannelOpenExpiredSessionReturns409(t *testing.T) {
if err := json.Unmarshal(decodeBody(t, body)["error"], &eobj); err != nil {
t.Fatalf("decode error envelope: %v (body=%s)", err, body)
}
if eobj.Code != "session_expired" {
t.Fatalf("error code = %q, want session_expired", eobj.Code)
if eobj.Code != "session_rejected" {
t.Fatalf("error code = %q, want session_rejected", eobj.Code)
}
if !strings.Contains(eobj.Message, "扫码") {
t.Fatalf("error message = %q, want the re-scan instruction", eobj.Message)
@@ -198,7 +198,7 @@ func TestAdminChannelsRescanFromRealIlinkAdapter(t *testing.T) {
t.Fatalf("new weixin adapter: %v", err)
}
if err := ad.Connect(context.Background()); err != nil {
t.Fatalf("connect before expiry: %v", err)
t.Fatalf("connect before rejection: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -231,10 +231,10 @@ func TestAdminChannelsRescanFromRealIlinkAdapter(t *testing.T) {
t.Fatalf("channels = %+v, want one weixin-ilink row", items)
}
if items[0].Connected {
t.Error("connected = true although iLink rejected the token")
t.Error("connected = true although iLink rejected the session")
}
if !items[0].NeedsRescan {
t.Error("needsRescan = false, want true for an expired iLink session")
t.Error("needsRescan = false, want true for a rejected iLink session")
}
if items[0].LastError == "" {
t.Error("lastError is empty, want the re-scan reason")
@@ -240,9 +240,9 @@ func (s *Server) adminChannelOpen(w http.ResponseWriter, r *http.Request) {
// the weixin-iLink bot token was rejected by iLink (errcode=-14), so the
// owner needs a fresh scan-code login. Answer 409 with that instruction
// instead of pretending the channel opened (the panel marks it connected).
if errors.Is(err, weixin_ilink.ErrSessionExpired) {
writeError(w, apperr.Wrap(apperr.ErrConflict, "session_expired",
"该渠道会话已过期(需要重新扫码登录):请用「扫码登录」重新绑定后再连接"))
if errors.Is(err, weixin_ilink.ErrSessionRejected) {
writeError(w, apperr.Wrap(apperr.ErrConflict, "session_rejected",
"该渠道会话已被 iLink 拒绝(errcode=-14):请用「扫码登录」重新绑定后再连接"))
return
}
// weixin-iLink with no token yet must not hang nor read as a credential