Skip to content

feat(channel/qq): 单聊接入官方流式消息 stream_messages - #1275

Open
qqqqqf-q wants to merge 1 commit into
felinics:mainfrom
qqqqqf-q:feat/qq-streaming
Open

qqqqqf-q wants to merge 1 commit into
felinics:mainfrom
qqqqqf-q:feat/qq-streaming

Conversation

@qqqqqf-q

Copy link
Copy Markdown
Member

背景

用户反馈 QQ 渠道两个问题:回复不实时(要等模型全量生成完才推送,长回复像服务器挂了)、markdown 渲染异常。

调研 QQ 官方 Bot API 现状:

  • QQ 已上线单聊官方流式端点 POST /v2/users/{openid}/stream_messages(分片携带同一 stream_msg_idindex 递增、input_state 1=生成中/10=结束,支持 content_type: markdown,限频 50 QPS)
  • 原生 markdown content 模式已对全部机器人开放(无需再申请模板,模板模式已废弃)
  • 群聊/频道无任何流式或消息编辑能力;被动回复有窗口+次数约束(群 5 分钟/5 次,单聊 4 次)

改动

1. C2C 单聊流式(stream.go/client.go)

  • OpenStream 在目标为 c2c、存在被动回复锚点 msg_id、markdownSupportenableStreaming 均开启时接入 stream_messages
  • 分片采用 replace 模式发送累计全文:前缀恒一致,即使 QQ 客户端静默丢中间分片(社区已知问题)最终内容也不受损
  • 约 1s 节流、首片即时下发;末片 input_state=10 完结
  • 任一分片失败即回退原有"攒到末包一次性发送",不产生半截消息
  • 新配置项 enableStreaming(默认开启);群聊/频道不经过此路径,行为不变

2. input_hint 续期(qq.go)

  • "正在输入"提示 60s 过期后自动续发,Completed/Failed 即停
  • 续发最多 3 次:hint 走被动回复配额(C2C 4 次/消息),避免超限

3. receiver 重连 backoff 5 分钟封顶(receive.go)

  • 原阶梯 30s 封顶,IP 白名单失效/频控类持续失败时会高频打爆 QQ 网关配额(实测触发 接口调用超过频率限制)

4. markdown 不做降级

  • 桌面端实测全部语法(标题/列表/引用/代码块含语法高亮/表格)正常渲染;移动端渲染差异属 QQ 客户端能力差异,API 侧无法感知对端平台

测试

自动化

  • go test ./internal/channel/adapters/qq/ 全绿,新增 7 例:累计分片内容与 index/stream_msg_id 递进、分片失败回退攒发、短回复单片、节流行为、stream shard 请求体结构、input hint 续期注册/停止/幂等
  • 既有攒发行为用例(group/channel target)原样通过,锁定未变更路径
  • golangci-lint run ./internal/channel/adapters/qq/... 干净;go test ./internal/arch/ 通过;pre-commit 钩子全仓测试绿

真机(QQ 桌面端 QQNT,个人开发者测试 bot,C2C 私聊)

  • 流式打字机效果确认;约 2000 字长文分段完整到达
  • 全程零 40034128(被动回复超限)、零 40007(前缀冲突)——实证流式分片不消耗被动回复次数(官方文档未写明这一点)
  • markdown:标题/列表/引用/代码块/表格均正常渲染;/help/model 等斜杠命令正常

未验证

  • 群聊:QQ 平台当前仅对企业开发者灰度群聊公开服务,个人认证主体无法将 bot 拉群,未能真机验证。该路径代码未变更,由既有单测锁定行为
  • 移动端 QQ 的 markdown 渲染(客户端能力差异,与本次改动无关)

@qqqqqf-q
qqqqqf-q requested a review from a team as a code owner September 15, 2026 10:57
@github-actions github-actions Bot added change:server Changes backend code, configuration, or API contracts needs:format Description needs template corrections; removed automatically once fixed size:M PR size uses the larger of added or deleted lines, excluding generated files labels Sep 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

@qqqqqf-q Please complete the following information using the template:

  • Select exactly one option for "Author": Human / Agent.
  • Select exactly one option for "Type": bug / feat / test.
  • Please complete "Summary".
  • Please complete "Validation".
  • Please complete "Screenshots / Recordings".
  • Please complete "Human QA".
  • Keep exactly one "Human QA passed" checkbox in "Human QA"; leave it unchecked until verified.

Editing the description triggers another check; format feedback does not block or cancel code CI.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 63df4b646a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/channel/adapters/qq/qq.go Outdated
}
a.inputHints[token] = stop
a.mu.Unlock()
go a.renewInputHint(ctx, client, target.ID, token, stop)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Detach hint renewal from the short-lived status context

For real inbound turns, notifyProcessingStarted passes a timeout context and executes defer cancel() immediately after ProcessingStarted returns (internal/channel/inbound/channel.go:2674-2676). Consequently, this goroutine sees ctx.Done() before its first 50-second tick and exits, so replies lasting over 60 seconds never renew the QQ typing hint. Use a turn-lifetime context or make the renewal loop depend only on its explicit stop channel.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-15T10:59:38.578536Z 63df4b6 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

qqqqqf-q added a commit to qqqqqf-q/Memoh that referenced this pull request Sep 15, 2026
Codex review (PR felinics#1275): 真实调用方 notifyProcessingStarted 用短超时
上下文包裹并在 ProcessingStarted 返回后立即 cancel,续期协程在首个
50s tick 前就随 ctx.Done 退出,>60s 的 turn 永远等不到续发。续期循环
改跑 context.WithoutCancel(ctx),只由 stop 通道终止;补取消后存活的
回归测试。
X 用户反馈 QQ 渠道回复不实时(等大模型全量生成完才推送)。QQ 官方
2026-07 上线单聊流式端点 /v2/users/{openid}/stream_messages,支持
markdown 分片;群聊/频道无流式或消息编辑能力,维持原行为。

- C2C 回复经 stream_messages 分片下发:replace 模式累计全文(前缀
  恒一致,QQ 客户端静默丢中间片也不损内容),约 1s 节流、首片即时,
  末片 input_state=10 完结;任一分片失败回退原有攒发
- 新增 enableStreaming 配置(默认开,仅 markdownSupport 开启时生效)
- input_hint 60s 过期自动续发(最多 3 次,避开 C2C 被动回复 4 次
  配额),Completed/Failed 停止续期;续期协程跑 WithoutCancel 上下
  文——真实调用方在 ProcessingStarted 返回后即 cancel 回调上下文
- receiver 重连阶梯 5 分钟封顶,修复白名单/频控类持续失败高频打爆
  网关配额
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:server Changes backend code, configuration, or API contracts needs:format Description needs template corrections; removed automatically once fixed size:M PR size uses the larger of added or deleted lines, excluding generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant