feat(channel): 出站消息 id 采集(可选 seam + qq_obv11 两分支取回)

「接她的话」缺的 A 层:平台给她自己那条出站消息分配的 id,此前在解析的那一刻
就被丢掉(HTTP 只校 retcode、WS 写完不等回声、契约层没有 id 出口)。

- internal/channel/adapter/outbound.go(新):SendReceipt/SendOutcome/SendIDStatus
  + 可选接口 OutboundReceiptAdapter + 调用方入口 SendWithReceipt。**不加进
  PlatformAdapter** —— 四个未接线适配器与全部桩实现一行不改(向后兼容硬指标);
  也不用回调:id 是某一次投递的属性,必须随那次投递返回。
- qq_obv11:HTTP 从响应信封 data.message_id 取;WS 登记 send_msg 的 echo 等待者
  (复用 probe.go 的 action 等待表与 routeActionResponse,不另开通道)。取不到
  一律空 id + 三态可分辨(reported/absent/unreported),绝不编造。
- send_msg echo 键改用单调计数器(旧 UnixNano 在 Windows 时钟粒度下会撞键,
  撞键即拿不到自己的回执)。
- 既有 SendMessage/SendProactive 路径行为不变(wantReceipt=false 不等待)。

验收:go build ./... + go vet ./... + go test ./... 全绿(cmd/cyrene、internal/webui
的 setup 失败是 worktree 缺 dist 的既有环境限制,base 同样失败)。
This commit is contained in:
AskaEth committed 2026-10-13 20:38:12 +08:00
1 parent a6f3817061
commit b504df01e0
8 files changed
+1053 -63

No files matched your search

+22 -12
View File
@@ -135,15 +135,24 @@ type Adapter struct {
// 事故正是把两者混为一谈:WS 通 ≠ QQ 账号在线。由 mu 保护。
account accountStatus
// probeMu / probeWait 是 WS 上「action 请求 → echo 回执」的等待表;probeEvery
// 是两次 get_status 探活之间的正常间隔(失败按 policy 退避)。见 probe.go。
probeMu sync.Mutex
probeWait map[string]chan *obAPIResponse
// actionMu / actionWait 是 WS 上「出站 action 请求 → echo 回执」的等待表:探活
// (get_status,probe.go)与出站回执采集(send_msg,outbound_id.go)共用同一张表、
// 同一条 routeActionResponse —— echo 前缀不同(status_ / send_),永不撞键。
// probeEvery 是两次 get_status 探活之间的正常间隔(失败按 policy 退避)。见 probe.go。
actionMu sync.Mutex
actionWait map[string]chan *obAPIResponse
probeEvery time.Duration
probeTimeout time.Duration
// sendReceiptTimeout 是 SendMessageWithReceipt 在 WS 上等待 send_msg 回执的上限
// (见 outbound_id.go;测试可收紧)。HTTP 端不涉及:响应本来就要等。
sendReceiptTimeout time.Duration
// probeSeq 是 WS 探活 echo 的单调序号(见 probe.go 的 nextProbeEcho):唯一性不依赖
// 时钟精度,两次探活绝不会撞在同一个 echo 上。
probeSeq atomic.Uint64
// sendSeq 是 WS 出站 send_msg echo 的单调序号(见 outbound_id.go 的 nextSendEcho),
// 与 probeSeq 同理:同一毫秒内的多次发送绝不能撞在同一个 echo 上,否则先登记的那次
// 发送会拿不到自己的回执(旧实现用 UnixNano,Windows 时钟粒度下真会撞)。
sendSeq atomic.Uint64
}
// Compile-time conformance with the channel contract.
@@ -178,14 +187,15 @@ func NewAdapter(cfg Config, opts ...Option) *Adapter {
cfg.ProactiveWindow = defaultProactiveWindow
}
a := &Adapter{
cfg: cfg,
groups: map[string]string{},
newConn: make(chan struct{}, 1),
limiter: newRateLimiter(cfg.MaxProactivePerWindow, cfg.ProactiveWindow),
policy: defaultReconnectPolicy(),
probeWait: map[string]chan *obAPIResponse{},
probeEvery: defaultProbeInterval,
probeTimeout: defaultProbeTimeout,
cfg: cfg,
groups: map[string]string{},
newConn: make(chan struct{}, 1),
limiter: newRateLimiter(cfg.MaxProactivePerWindow, cfg.ProactiveWindow),
policy: defaultReconnectPolicy(),
actionWait: map[string]chan *obAPIResponse{},
probeEvery: defaultProbeInterval,
probeTimeout: defaultProbeTimeout,
sendReceiptTimeout: defaultSendReceiptTimeout,
}
for _, opt := range opts {
opt(a)