feat(weixin-ilink,gateway): 会话过期显式化 — 不再静默变哑/假 OK 重连,面板可见「需重扫」
- 适配器新增过期状态机(plugins/channels/weixin_ilink/session_state.go):getupdates/sendmessage/getuploadurl 收到 errcode=-14 即 markExpired(ERROR 仅一次,只带 bot_id,token 永不入日志),扫码登录拿到新 token 或配置换新 token 时清除(惰性比对) - Connect/HealthCheck 在过期时返回 ErrSessionExpired(可与 ErrNotConnected 区分):不再对死 token 设 connected=true 报假成功;发送路径同样返回该错误而不是笼统 ErrNotConnected - 网关:POST /admin/channels/open 遇过期 → 409 session_expired + 明确文案(不再吞错回 200);GET /admin/channels 摘要新增 needsRescan/lastError(经可选接口类型断言读取,取不到不伪造) - channelbridge:读循环非取消退出时把自身记账的 connected 置 false(热重载被替换者退出不抹掉新实例) - login/login_admin:过期时 StartLogin 允许重新出码、LoginStatus 如实报 failed(原先可能把死 token 当 confirmed 重新落盘) - Docs/api-client v0.39;新增 5 组测试 + 变异验证(去掉标记/恢复假成功/网关不填字段 均 FAIL)
This commit is contained in:
1 parent
2092094d0a
commit
cdc370dc82
14 files changed
+975
-30
No files matched your search
@@ -30,8 +30,8 @@ type sendResult struct {
|
||||
// context_token, so the platform can associate the reply with the right
|
||||
// conversation (design/08 §8).
|
||||
func (a *Adapter) SendMessage(ctx context.Context, resp adapter.UnifiedResponse) error {
|
||||
if !a.IsConnected() {
|
||||
return adapter.ErrNotConnected
|
||||
if err := a.sendGuard(); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, m := range resp.Messages {
|
||||
peer, _ := recipient(m)
|
||||
@@ -51,8 +51,8 @@ func (a *Adapter) SendMessage(ctx context.Context, resp adapter.UnifiedResponse)
|
||||
// messaged, the push is rejected with ErrProactiveWindow and Capabilities.
|
||||
// Proactive is only true so callers know the capability exists.
|
||||
func (a *Adapter) SendProactive(ctx context.Context, msg adapter.UnifiedMessage) error {
|
||||
if !a.IsConnected() {
|
||||
return adapter.ErrNotConnected
|
||||
if err := a.sendGuard(); err != nil {
|
||||
return err
|
||||
}
|
||||
peer, _ := recipient(msg)
|
||||
if peer == "" {
|
||||
@@ -73,6 +73,20 @@ func (a *Adapter) SendProactive(ctx context.Context, msg adapter.UnifiedMessage)
|
||||
return a.sendToOne(ctx, peer, msg)
|
||||
}
|
||||
|
||||
// sendGuard reports the error a send must surface before touching the network: an
|
||||
// expired session (iLink already refused this token with errcode=-14) reads as
|
||||
// ErrSessionExpired — the owner must re-scan — while any other down session stays
|
||||
// ErrNotConnected.
|
||||
func (a *Adapter) sendGuard() error {
|
||||
if expired, reason := a.expiredState(); expired {
|
||||
return fmt.Errorf("weixin_ilink: send: %w: %s", ErrSessionExpired, reason)
|
||||
}
|
||||
if !a.IsConnected() {
|
||||
return adapter.ErrNotConnected
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendToOne sends one normalized message to a peer as a native iLink message,
|
||||
// composing its item_list from any text plus uploaded media.
|
||||
func (a *Adapter) sendToOne(ctx context.Context, peer string, m adapter.UnifiedMessage) error {
|
||||
@@ -152,6 +166,10 @@ func (a *Adapter) sendNative(ctx context.Context, peer, ctxToken string, items [
|
||||
return fmt.Errorf("weixin_ilink: sendmessage: %w", err)
|
||||
}
|
||||
if out.Errcode == errSessionExpired {
|
||||
// The send path sees the same dead token as the poll path: record the
|
||||
// expired state so the adapter stops claiming a live session and the panel
|
||||
// can ask for a re-scan.
|
||||
a.markExpired(rescanReasonSendMessage)
|
||||
return ErrSessionExpired
|
||||
}
|
||||
if out.Ret != 0 {
|
||||
@@ -172,6 +190,12 @@ func (a *Adapter) getUploadURL(ctx context.Context) (string, error) {
|
||||
bytes.NewReader(body), &raw); err != nil {
|
||||
return "", fmt.Errorf("weixin_ilink: getuploadurl: %w", err)
|
||||
}
|
||||
if intOf(raw["errcode"]) == errSessionExpired {
|
||||
// Media upload is part of the send path, so the same dead token is recorded
|
||||
// here too rather than degrading into "no url returned".
|
||||
a.markExpired(rescanReasonUploadURL)
|
||||
return "", ErrSessionExpired
|
||||
}
|
||||
u := firstString(raw, "upload_url", "url", "uploadUrl", "cdn_url")
|
||||
if u == "" {
|
||||
return "", fmt.Errorf("weixin_ilink: getuploadurl returned no url")
|
||||
|
||||
Reference in new issue
Block a user