Skip to content

libraries: Add the OneBot v11 channel - #1

Merged
AkaneTan merged 42 commits into
masterfrom
feature/channel-onebot
Oct 26, 2026
Merged

AkaneTan merged 42 commits into
masterfrom
feature/channel-onebot

Conversation

@Yos-X

@Yos-X Yos-X commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

OneBot v11 的客户端 Channel,位于 libraries/channels/onebot,按协议域、传输、消息映射、渠道与实例级 API、agent 集成推进。

协议域不涉及传输,也不引库。含 21 种消息段(含实现方上报的 file 段),认不出的段保留原始 data;三种消息形态;CQ 编解码;四类事件,认不出的保留原始 JSON;38 个公开动作与隐藏动作,_async 与 _rate_limited 由后缀派生;结果分层。

四种通信各自独立,每实例选一种。http 的对测覆盖成功、拒绝、连不上、鉴权失败、畸形应答,另有 token 不泄漏的断言;http_post 覆盖验签、路径、账号绑定与快速操作回写;ws 按 echo 关联读回应答并读事件;ws_reverse 由对端拨号接入,并经该连接完成一次调用。另有一个运行于真实 runtime 的集成测试,覆盖实现上报消息至回复经 send_private_msg 发出。

消息映射把上报的消息、引用、媒体与未寻址的群消息分开处理:群消息需 @ 本实例或引用本实例的消息才交给 agent,命令按 / 前缀识别并查管理员门禁;每个聊天需先配对,未配对的聊天只放行配对码。批准请求以回复该条消息的方式应答,/approve 与 /deny 走命令路径。非消息上报经 ChannelNotice 与 ChannelPoints.NOTICE 发布,订阅方贡献 ObserverHook 即可收到,未配对的聊天照常发布。工具面是一张声明表加两个工具(onebot.read 只读、onebot.act 改动),共 22 个动作,加动作即加一行;凭据类、raw、快速操作、重启与清缓存不在表内,无法由模型提出。文件参数经 SDK 的 HardFloor 判定路径,与 fs 工具同一套判决;超过单次请求可携带的文件走实现方的分块流上传。回合进行中的动作经渠道事件上报到聊天:第一步在开始时以现在时发出,已结束的步骤按 8 秒限流合并为过去时的一条消息,措辞与 telegram 渠道同一套键值,可由实例配置项 progress 关闭。回合进行中模型自己说的话经 preview 上报,同样受该间隔限流,每条只含未说过的部分;只在回合已经开始使用工具之后才发,因为在那之前的话就是紧随其后的回答。两个开关分别控制:progress 管动作,streaming 管模型的话。大文件上传声明了 5 分钟超时,文件读取与写入在 IO 调度器上进行。

对渠道之外模块的改动:plugin-sdk 增加 ToolContext.trigger(带默认值,实现者不必改)与 ChannelNotice、ChannelPoints.NOTICE;agent 的框架标明消息 id 与引用 id,并把 trigger 交给工具;provider-common 在渠道只报了图片种类时按字节判断格式。依赖方面引入 org.java-websocket:Java-WebSocket:1.6.0,仅出现在插件模块与版本目录,唯一传递依赖为 slf4j-api。

以下边界本 PR 不处理:

  • preview 为空实现。OneBot 没有改写已发消息的能力,streaming 为 false;回合进行中的进展改由限流后的新消息呈现,每个动作只有开始态被订阅。
  • 反向 ws 无法在握手期返回 401。Java-WebSocket 1.6.0 的 WebSocketServer 没有可覆写的手握钩子,只能在 onOpen 校验后按 1008 关闭。
  • CommandRules 是 telegram 那份的副本,形状正确而落点待定,应随 telegram 那份一起上移 channel-common。
  • ReplySink.abandon 为空实现,是撤回本实例回复的天然位置。
  • 本实例已发消息的 id 未随记录持久化,跨渠道的“我发过什么消息”仍无来源。
  • 群文件覆盖上传、列出与删除;移动、改名、转发与文件夹的增删不在本 PR。
  • message.v1 这一格式名是否视为冻结契约尚未确定,本 PR 原地修改了 v1 的内容。

验证命令为 ./gradlew :libraries:channels:onebot:spotlessCheck :libraries:channels:onebot:test :app:installDist,插件模块 214 个测试通过,spotless 与 allWarningsAsErrors 通过。最近一次 CI 运行报出两处失败,已在本分支修复:testkit 的样本未列出 ToolContext 新增的 trigger 声明;Telegram 的批准引用测试未跟上框架中消息 id 与引用 id 的变化。

Windows 上的失败与本 PR 无关,分两类,均由 fix/platform 分支(PR #3)处理:一类是断言写死 POSIX 的路径、换行与错误文案;另一类是上游 e7747d8 把 Path.of("/dev") 写入默认保护位置清单,Windows 上该路径为无盘符的 \dev,PathCanonicalizer 无法处理,AgentFloor 构造失败导致 runtime 无法启动,同一缺陷另使 writeAtomically 在无 POSIX 权限的宿主上无法建文件。

Copilot AI 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.

🟡 Changes recommended

Transport result handling, HTTP POST operation, temporary-session routing, and receiver resource limits contain correctness and security defects.

12 open findings
What changed in this PR

Adds a OneBot v11 channel with protocol models, authenticated HTTP/WebSocket transports, runtime mapping, typed APIs, tests, and app integration.

Changes:

  • Implements OneBot messages, events, actions, results, and authentication.
  • Adds HTTP, HTTP POST, forward WebSocket, and reverse WebSocket communication.
  • Integrates the channel into the runtime and example configuration with broad tests.
File Description
app/​build.gradle.kts Adds the OneBot module.
app/​src/​dist/​config/​alexandrite.example.json Adds example OneBot configuration.
gradle/​libs.versions.toml Registers Java-WebSocket.
libraries/​channels/​onebot/​build.gradle.kts Configures module dependencies.
OneBotPlugin.kt Declares the plugin.
api/​OneBotApi.kt Defines the public typed API.
api/​OneBotApiImpl.kt Implements API dispatch and decoding.
auth/​OneBotAuth.kt Implements tokens, signatures, and redaction.
channel/​OneBotChannel.kt Implements channel delivery.
channel/​OneBotInbox.kt Submits incoming messages.
channel/​OneBotLink.kt Selects and manages transports.
config/​OneBotConfig.kt Defines plugin and instance configuration.
mapping/​OneBotMessages.kt Maps SDK and OneBot messages.
protocol/​OneBotIds.kt Defines typed identifiers.
protocol/​OneBotStatus.kt Defines statuses and return codes.
protocol/​api/​OneBotAction.kt Models actions and suffixes.
protocol/​api/​OneBotApiTypes.kt Defines typed response payloads.
protocol/​api/​OneBotRegistry.kt Registers standard actions.
protocol/​api/​OneBotRequests.kt Defines request payloads.
protocol/​event/​OneBotEvent.kt Models OneBot events.
protocol/​event/​OneBotEventCodec.kt Decodes event objects.
protocol/​message/​CqCode.kt Implements CQ encoding.
protocol/​message/​OneBotMessage.kt Models message shapes.
protocol/​message/​OneBotSegment.kt Models message segments.
protocol/​message/​OneBotSegmentCodec.kt Encodes and decodes segments.
protocol/​result/​OneBotResult.kt Models layered call results.
transport/​Connection.kt Provides shared connection state.
transport/​OneBotForwardWebSocket.kt Implements forward WebSocket transport.
transport/​OneBotHttpApi.kt Implements HTTP API calls.
transport/​OneBotHttpPostReceiver.kt Receives HTTP event reports.
transport/​OneBotReverseWebSocket.kt Implements reverse WebSocket transport.
transport/​OneBotSettings.kt Builds transport settings.
transport/​OneBotWire.kt Configures wire JSON handling.
OneBotChannelIntegrationTest.kt Tests runtime message round trips.
auth/​OneBotAuthTest.kt Tests authentication behavior.
channel/​OneBotHttpTransportTest.kt Tests HTTP calls and failures.
mapping/​OneBotMessagesTest.kt Tests SDK message mapping.
protocol/​api/​OneBotRegistryTest.kt Tests action registration.
protocol/​event/​OneBotEventCodecTest.kt Tests event decoding.
protocol/​message/​CqCodeTest.kt Tests CQ and segment codecs.
transport/​OneBotHttpPostReceiverTest.kt Tests report reception.
transport/​WsTransportTest.kt Tests both WebSocket modes.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@Yos-X

Yos-X commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator Author

已按评审处理,逐条说明对应提交。

  • X-Self-ID 大小写:请求头在读 head 时已折成小写,查询却用标准拼写,因此该头从未被读到——已修复 6469a4c。原先那条"缺少账号则拒绝"的用例是靠这个缺陷才通过的,现改为报文不带账号,另加一条只靠请求头命名账号的用例。
  • 通过 socket 返回的应答按应答处理:已修复 6469a4c。正向与反向 WebSocket 的应答与 HTTP 应答走同一套状态分类。
  • echo 计数器非原子:已修复 6469a4c。
  • 声明长度超限的报文不返回状态:已修复 f5ac753。分配前比较上限,并返回 413;声明的那点字节以 8 KiB 为块读出丢弃,使仍在发送的客户端能读到该响应。
  • 临时会话的回复发到群:已修复 951cbaf。新增 OneBotMessages.privateTemporary,action 与 params 都先判断临时会话。
  • autoEscape 无法保留 CQ 原文:已修复 951cbaf。消息按写入时的形状发送,auto_escape 只由"整条消息是一段文本"这一种形状请求。
  • 应答的 data 按对象读取,返回数组的动作对不上类型:已修复 371ffb5。data 按原形状保留,Ok 的类型参数放宽为 JsonElement,补了数组与对象两条用例。
  • 未知 message_type 被写入 postType:已修复 371ffb5。
  • 两个配置项可配置但未被消费:已修复 371ffb5,采用删除。
  • 事件溢出丢弃没有计数:已修复 371ffb5。trySend 在两种丢弃策略下都返回成功,因此计数由"已报告减已读减容量"得出。
  • 反向连接关闭时挂起的调用不结束:已修复 ee04a72。

未处理的最后一条:http_post 实例发出的动作一律 unreachable。这一条不是实现缺陷,而是该传输本身的形状。标准里的 http_post 是事件推送通道:实现方作为 HTTP 客户端把事件 POST 到本实例,本实例应答后连接即结束,因此没有可供回程的连接,本实例也没有指向实现方的地址。发出调用需要指向实现方的连接,而这条传输不提供,标准也没有要求它提供。标准另在 api 段规定了实现方自己的 HTTP 服务端地址,所以需要回程时应当配置一个 http 或 ws 实例,与 http_post 实例并存,共用同一账号。

因此这里有一个选择,属于配置面的决定:保持现状,用一个 http_post 实例收事件、另一个 http 或 ws 实例发调用;或者为 http_post 增加一个可选 endpoint,配了就同时建立 HTTP API 客户端,一个实例完成收发。该配置项尚未添加。

验证::libraries:channels:onebot:test 通过,82 个测试。

Copilot AI 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.

🟡 Changes recommended

Transport error handling, reverse-WebSocket isolation, event accounting, serialization, and configuration contain unresolved correctness and security issues.

5 open findings
12 resolved since last review
Previously missed (10)

In code that hasn't changed since last review

Medium severity Convert WebSocket transport failures to Unreachable results

libraries/​channels/​onebot/​src/​main/​kotlin/​org/​foedusprogramme/​alexandrite/​channel/​onebot/​channel/​OneBotLink.kt:79

WebSocket transport failures escape from this method instead of becoming OneBotResult.Unreachable. A disconnected socket throws IllegalStateException, and the per-call timeout throws TimeoutCancellationException, so both Channel.send and the public typed API can throw while the HTTP path returns a result. Convert transport-owned timeout/connection failures here while still rethrowing caller cancellation.

Medium severity Validate numeric reply IDs before constructing MessageId

libraries/​channels/​onebot/​src/​main/​kotlin/​org/​foedusprogramme/​alexandrite/​channel/​onebot/​mapping/​OneBotMessages.kt:121

This blindly constructs a numeric MessageId, but this same mapper creates delivered refs named sent and async when no platform ID is available. Reusing either delivered ref as replyTo therefore throws before sending, rather than returning a delivery failure. Validate that the target is a numeric OneBot message ID and handle unavailable/synthetic refs explicitly.

Medium severity Preserve retryability when mapping HTTP failures

libraries/​channels/​onebot/​src/​main/​kotlin/​org/​foedusprogramme/​alexandrite/​channel/​onebot/​mapping/​OneBotMessages.kt:176

The mapping discards OneBotFailure.retryable for HTTP failures. In particular, an HTTP 5xx result is retryable by OneBotFailure, but it reaches Delivery.NotDelivered as non-retryable UNKNOWN, preventing normal retries. Pass the failure's retryability through (and classify retryable HTTP status failures as transient if appropriate).

Medium severity Do not use member card as group chat title

libraries/​channels/​onebot/​src/​main/​kotlin/​org/​foedusprogramme/​alexandrite/​channel/​onebot/​mapping/​OneBotMessages.kt:224

A group member's card is the sender's display name, not the group's title. Putting it into ChatInfo.title mislabels every group chat with whichever member spoke most recently. The event has no standard group-name field, so leave the title null unless it is obtained separately.

Medium severity Serialize typed request IDs using numeric ID serializers

libraries/​channels/​onebot/​src/​main/​kotlin/​org/​foedusprogramme/​alexandrite/​channel/​onebot/​protocol/​api/​OneBotRequests.kt:32

All typed request IDs are written as JSON strings here and in the remaining request classes, bypassing the ID serializers that deliberately emit a number when it fits (OneBotIds.kt:109-112). OneBot declares these parameters as numeric, so strict WS/JSON implementations can reject otherwise valid typed calls. Encode UserId, GroupId, and MessageId through their serializers (falling back to strings only when outside Long).

Medium severity Reject non-object JSON report elements

libraries/​channels/​onebot/​src/​main/​kotlin/​org/​foedusprogramme/​alexandrite/​channel/​onebot/​protocol/​event/​OneBotEventCodec.kt:26

A valid JSON scalar or array is converted into a synthetic Unknown event and therefore accepted by the HTTP receiver as a report, even though events must be objects; its original payload is also lost. Reject non-object elements (and have each transport classify/ignore that decode failure) instead of fabricating an event with zero IDs and empty raw JSON.

Medium severity Preserve GroupBan subType during decoding

libraries/​channels/​onebot/​src/​main/​kotlin/​org/​foedusprogramme/​alexandrite/​channel/​onebot/​protocol/​event/​OneBotEventCodec.kt:123

The codec computes subType but does not pass it to GroupBan, so both ban and lift_ban events expose subType == null despite the public model having that property. Preserve the wire value as the other notice decoders do.

Medium severity Omit null optional segment parameters from JSON

libraries/​channels/​onebot/​src/​main/​kotlin/​org/​foedusprogramme/​alexandrite/​channel/​onebot/​protocol/​message/​OneBotSegmentCodec.kt:79

Optional segment parameters are emitted as explicit JSON null values. explicitNulls = false does not remove JsonNull already inserted into a manually built object, so a simple Image(file = ...) sends null type, url, flags, and timeout instead of omitting optional fields as required by the request contract. Add each optional key only when non-null; the same pattern recurs in the record, video, poke, share, location, music, and node branches.

Medium severity Do not enqueue rejected reports as accepted

libraries/​channels/​onebot/​src/​main/​kotlin/​org/​foedusprogramme/​alexandrite/​channel/​onebot/​transport/​OneBotHttpPostReceiver.kt:171

Rejected reports have already been enqueued and counted as accepted before the callback's decision. This contradicts Rejected's contract and lets downstream processing act on a request answered with 4xx. Decide first, and only report/increment the accepted and quick-operation branches.

Medium severity Use plain text content type for rejection details

libraries/​channels/​onebot/​src/​main/​kotlin/​org/​foedusprogramme/​alexandrite/​channel/​onebot/​transport/​OneBotHttpPostReceiver.kt:195

Rejection bodies are plain text, but this labels every non-204 body as JSON. HTTP clients that honor the media type will try to parse text such as the signature... as JSON and fail to expose the actual reason. Use text/plain; charset=utf-8 for detail, retaining JSON only for quick-operation objects.

🧠 Review effort: Balanced

@Yos-X

Yos-X commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator Author

第二轮评审的处理结果。

按线程给出的 5 条发现,均已在 f1367d5 修复:接收器的读取线程改为有界线程池与排队位、超限以 503 拒绝,整请求另加截止时间(头部与正文的读取循环都检查);反向监听器只读取活动对端的帧,且只在关闭活动对端时才清理状态并结束等待中的调用;丢弃数改为在队列报告"从未交付"时递增;那条请求头用例改为正文不命名账号,使通过只能来自请求头路径。

正文中另一节"Previously missed"的 10 条也已逐条核实,全部修复:

  • 拒绝的报文先入队并计为已接受 —— b7b7346。决策先取,只有被接受或需要快速操作的报文才报告与计数。
  • 拒绝原因以 application/json 返回 —— b7b7346。拒绝为 text/plain; charset=utf-8,快速操作仍为 JSON。
  • GroupBan 丢失 sub_type —— b7b7346。
  • socket 调用抛出而非返回结果 —— b7b7346。连接断开与调用超时都转为 OneBotResult.Unreachable,调用方主动取消仍原样抛出。
  • 失败映射丢弃可重试性 —— b7b7346。先取传输给出的 retryable,5xx 因此成为可重试。
  • 可选段参数写成 JSON null —— b7b7346。仅在存在时写入。
  • 用成员名片作为群标题 —— 771f66d。消息不携带群名,标题保持为 null。
  • 非对象 JSON 被合成为事件 —— 771f66d。改为拒绝,传输层按无法读取的报文处理。
  • 回复目标直接构造数字 MessageId —— 3d 系列提交。合成引用 sent、async 不指向实现的任何消息,遇到时省略回复段并正常发出,而不是在发送前抛出。
  • 类型化请求的 id 以字符串写出 —— 同一提交。UserId、GroupId、MessageId 均经数字序列化器写出,超出 Long 时才回落为字符串。

验证::libraries:channels:onebot:test 通过,85 个测试。

Copilot AI 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.

🟡 Changes recommended

Unresolved configuration, protocol, security, and WebSocket lifecycle defects can cause startup failure, data corruption, denial of service, and delayed calls.

3 open findings
5 resolved since last review
Previously missed (4)

In code that hasn't changed since last review

Medium severity Redact all non-empty configured secrets

libraries/​channels/​onebot/​src/​main/​kotlin/​org/​foedusprogramme/​alexandrite/​channel/​onebot/​auth/​OneBotAuth.kt:85

Values shorter than four characters are deliberately left unmasked, although access tokens and signing secrets have no minimum length. A non-2xx implementation response can echo such a token into OneBotFailure.message, violating this API's no-secret guarantee. Redact every non-empty configured value instead.

Medium severity Avoid using WebSocket echo as message ID

libraries/​channels/​onebot/​src/​main/​kotlin/​org/​foedusprogramme/​alexandrite/​channel/​onebot/​mapping/​OneBotMessages.kt:167

A socket response always carries this client's echo, but that value is only a correlation key, not the platform's message ID; toString() also includes JSON quotes for a string echo. When a successful send omits message_id, this therefore exposes a fabricated quoted ID instead of the documented sent fallback. Do not use echo as a message reference.

Medium severity Separate CQ text escaping from parameter escaping

libraries/​channels/​onebot/​src/​main/​kotlin/​org/​foedusprogramme/​alexandrite/​channel/​onebot/​protocol/​message/​CqCode.kt:174

CQ escaping treats plain text and parameter values differently: commas are escaped only inside parameter values. Because this shared function also encodes Text, ordinary text commas become non-standard ,; the matching decoder also corrupts a literal , received in plain text. Split text escaping/unescaping from parameter escaping/unescaping.

Medium severity Fail pending calls when closing the forward WebSocket

libraries/​channels/​onebot/​src/​main/​kotlin/​org/​foedusprogramme/​alexandrite/​channel/​onebot/​transport/​OneBotForwardWebSocket.kt:72

Closing a forward WebSocket does not complete or clear client.awaiting. Cancelling the connection loop can prevent its post-onClose failPending call, so callers can remain suspended until firstByteTimeoutMillis after close() has returned. Atomically stop new sends and fail all pending deferreds before cancelling the scope.

🧠 Review effort: Balanced

@Yos-X

Yos-X commented Oct 10, 2026

Copy link
Copy Markdown
Collaborator Author

第三轮评审的处理结果,全部已在 a4c5c32 修复。

按线程给出的 3 条:

  • 超限报文的排空路径绕过了整请求截止时间,未认证的发送方可以逐字节慢发占满读取线程 —— 已修复。drain 接收并检查同一个截止时间。
  • 新对端接管时,旧 socket 上等待中的调用被留在映射里,而其帧已不再读取,因此必然等到超时 —— 已修复。先结束这些调用并关闭旧 socket,再设为活动对端。
  • 该用例的发送时长(2 秒)短于截止时间(5 秒),因此没有截止时间也能通过 —— 已修复。改为发送 120 次共 12 秒,断言上界收紧到 8 秒。

正文中"Previously missed"的 4 条:

  • 短于四个字符的配置值未被脱敏,实现方回显该 token 时会进入失败信息 —— 已修复。凡非空值一律替换,token 与签名密钥没有最小长度。
  • 投递把 socket 应答的 echo 当作消息 id,而它只是本客户端用于配对的关联键,toString() 还会带上 JSON 引号 —— 已修复。应答未给出消息 id 时回落到 sent。
  • 正文与参数值共用一套转义,而逗号只在参数内表示结束,导致普通正文的逗号被写成 ,,正文里字面的 , 又被读成逗号 —— 已修复。两者分开转义与反转义,原先断言旧行为的两条用例改为断言新行为。
  • 关闭正向 WebSocket 时,取消作用域可能先于关闭回调结束等待中的调用,close() 返回后调用方仍会挂起到超时 —— 已修复。等待中的调用先结束。

验证::libraries:channels:onebot:test 通过,85 个测试。20 条评审线程全部解决。

@Yos-X
Yos-X force-pushed the feature/channel-onebot branch 2 times, most recently from e63893b to 65f771e Compare October 18, 2026 17:24
Yos-X added 5 commits October 19, 2026 01:31
Reads and writes the v11 event, action, message and result shapes, and serves forward WebSocket, reverse WebSocket and HTTP POST transports with token auth.
Maps v11 events, CQ codes and segments onto incoming messages, notices and quotes, and pairs chats with their OneBot ids.
Adds the channel front end and reply sink, the sender and reply registry, command permissions, approval questions, activity summaries and the restart-safe sent-message ledger.
Adds the onebot.read and onebot.act tools, upload and group-file support, and the media reader that turns files and images into what a turn can use.
Adds the notice and points a channel can offer, and gives a tool the message its turn answers so it can act on that message by id.
@Yos-X
Yos-X force-pushed the feature/channel-onebot branch from 65f771e to 723b906 Compare October 18, 2026 17:39
Yos-X added 10 commits October 19, 2026 01:53
Names the message id in the prompt, names the quoted message's id, and carries the trigger through the tool context, with the golden files updated.
Recognizes the bytes of common image formats when a channel named only the kind, so the model receives the right media type.
Covers protocol codecs, transports, mapping, pairing, commands, approvals, progress, media, tools and a channel end to end.
Exchanges the app credentials for a token, keeps it fresh, decodes the open platform payloads, and runs the WebSocket gateway with identify, heartbeat, resume and reconnect.
Maps C2C and group messages, mentions, quotes and Markdown, and parses slash commands with the channel's permission matrix before a turn starts.
Adds the reply sink and sender for passive C2C and group replies, single-chat streaming with append chunks, Markdown adaptation, typing state, message ids for quotes, and the words hook for non-streaming chats.
Asks approvals with Markdown keyboard buttons, publishes command menus and panels, uploads files in the platform's chunks, and adds the qqbot.act tool.
Covers token and gateway behaviour, config, mapping, menus, commands, progress, sending, uploads, tools and a channel end to end.
Wires both channel plugins into the app, their example config and Chinese texts, and updates the tests that list the built-in plugins.
Adds the channel text catalogues the plugins read their replies from.
@Yos-X
Yos-X force-pushed the feature/channel-onebot branch from 723b906 to a48dd51 Compare October 18, 2026 17:54
Yos-X added 27 commits October 19, 2026 02:28
The platform can send a button press as soon as the approval message is delivered, which may be before sendApproval returns. Register the chat before the send, remove it when delivery fails, and hold the send in the integration test so the press arrives during it.
An approval can be asked by a delegated run, so close every stream open in the approval's chat instead of only the asking turn's. Serialize the reply's stream and detached-run state, and close a stream before a final message carrying files, which the stream cannot carry.
Adds the commands the Telegram rules already have: heartbeat and schedule, timezone, home, link, unlink and untaint. A member could otherwise be refused a command Telegram allows.
Removes the onebot settings key the config does not have, adds the allowed chats list, and checks every example channel section against its config descriptor.
A chat shares one progress ledger between its replies. Serialize that ledger, guard the activity sender against a second launch, and close the sender exactly once; finish both reply endings through cleanup so a cancellation cannot leak the reply registry or the chat's progress.
A socket call that times out is a failed delivery or unavailable approval, not a cancelled turn or withdrawn request. The inbox also keeps the ids it took, so an implementation that reports one message twice asks the agent for one turn.
After startup, a missing gateway, a token failure or a connection that never becomes ready no longer ends the loop. Close the socket on a failed ready, keep one heartbeat, and drop a session the close code says can no longer be resumed.
A model can write an image target that names a local path. The implementation would read that path and post it to the chat, outside the floor. Only an http or https target becomes an image segment; anything else stays as the text the model wrote.
Keep only the most recent 1024 quote references per chat, and cap an OneBot approval body at 1500 characters so a large diff cannot make the approval undeliverable.
A group whose bot_state call failed was remembered as asked and never looked up again, so a full-mode mention could stop naming the bot until restart. Forget the group after a failed lookup.
The platform fixes are what the channel branch runs on a host whose paths are not POSIX, and the Windows fixes for command.exec are based on them. This branch takes them from fix/platform rather than from master, so both branches still reach master separately: the PR for the channel branch shows them only until the platform branch is merged there.

Resolutions: the example config takes master's git server example and keeps the onebot and qqbot sections; the version catalog keeps java-websocket beside jline and maven-publish; ToolExecutor passes the channel's trigger and the platform's call sandboxes to the tool context.
The channel branch passes the turn's trigger in the call as well, so the call does not fit a line any more and spotlessCheck asks for the wrapped form.
…into

The master merge gives a channel AgentCatalog, ChatLinks and ChatSettings instead of one AgentControl, makes Pairings a final class to delegate to, and reports a chat's language through ChatSettings.snapshot(chat).effectiveLanguage. The gate, the command rules, the pairing desk and the approval text of both channels follow that, and the approval tests build a request without a chain and without always rules, as the new constructor means them.
The open platform refuses a menu or command panel with 40030013 when it holds more items than it takes, and says in that answer's limit how many it takes, which is fewer than its documentation promises. A refused configuration is published once more with that many items, and the failure a second refusal leaves names the limit and the panels the scope already holds.

The log said a menu of every command was published although only the first ten items were: it names the items now, and how many commands there were.
cmd, powershell, pwsh, cscript, wscript, mshta, rundll32, regsvr32 and wsl are launchers like sh and bash: what a call gives them is code, which the floor cannot see, so they are opaque and no standing rule covers them. Their names are compared without case on a Windows host, where PowerShell.exe and cmd spell themselves that way.

An opaque program's argument that names no path of the host is script text and no path to refuse: a command line that is passed to a shell is not refused as blocked_floor any more, and it is written as a file only when that is wanted. Every other program still refuses a path-shaped word that the host cannot name.
An answer of 40030013 need not carry the limit it mentions, so a refused menu or panel is retried with five items then, and a refusal that leaves one names the answer itself and how many panels the scope holds. Past the panels the platform keeps for one bot it says that the console has to give way, since no number of items helps there.
The platform counts a character that is not ASCII as two units, and 40030013 is what it answers when a description is over its thirty units rather than when a panel holds too many items: a host whose language is Chinese therefore had every command panel refused, since thirty Chinese characters are sixty units. The item counts of a menu and a panel, and the item names, are cut to the units the platform documents, and a test checks a Chinese and an ASCII description.
Buying the access token happened outside the try of QqBotApi.call, so an unreachable token endpoint threw out of every call that needed one. QqBotGateway named its address through such a call outside any catch, so that exception ended the reconnect loop and no further connection was tried until the runtime restarted.

The token purchase is now part of the call's try and yields a failed answer. Naming the gateway is caught and retried, the startup tries it up to START_ATTEMPTS before giving up, and the gateway's scope logs an unexpected failure instead of ending without a line. Reading one frame of the platform is guarded as well, so a frame that fails to read does not end the inbox.

Tests: a refused token answers Failed instead of throwing; a gateway whose token is refused twice still connects. :libraries:channels:qqbot:spotlessCheck and :test pass: 72 tests, 0 failures.
origin/master moved from c2fbf47 to df7c55c, 15 commits. Conflicts and how they were settled:

- app/build.gradle.kts: kept the onebot and qqbot module dependencies, and added the provider-common one the app now imports.
- libraries/tools/exec/Planner.kt, Programs.kt and ExecWindowsTest.kt: took the origin/master versions. 45d95a8 reworks the Windows program handling on top of the platform module and covers the cmd batch arguments, so the branch's 7e1a926 version is superseded.
- app/src/dist/config/alexandrite.example.json: kept the channels section and the app.updates settings, and the formatting of origin/master.

Verification on Windows 11, JDK 21: :app:installDist; spotlessCheck on app, agent, tools, onebot, qqbot, plugin-sdk, provider-common, testkit, internal and runtime; internal 176 tests / 16 failed / 10 skipped, tools 214 / 2 / 76, onebot 265 / 0, qqbot 72 / 0, app targeted 139 / 38 (the same 38 that origin/master alone fails on this host), agent targeted 93 / 0 / 1, testkit 243 / 0 / 1, provider-common 140 / 0, plugin-sdk 222 / 2, runtime 312 / 1 / 14. The 16 internal, 2 tools, 38 app, 2 plugin-sdk and 1 runtime failures are the recorded Windows platform baselines, not regressions.
The build publishes every module except the app and the examples, so the two channel modules need a description or their POM fails. Their plugin SDK dependency becomes an API dependency and their plugin entry classes become internal, as the Telegram module has them.
…ts admins

The channel modules take the SDK of the merged master: ChatSettings under sdk.turn, ApprovalRequest built by its builder with alwaysRules, FloorCheck read with an else branch, TurnEvent.Content.Preview, and ReplySink.abandon taking vouched.

OneBot takes an operator only from the instance's admins, not from a group owner's or administrator's role. The config checks that admins are numbers and that allowedChats are private or group keys.

An approval of a member's turn goes only to an administrator chat that paired, and the instance warns at start about administrator chats that did not pair and about an empty admins list.

The provider's own image reading is dropped: the merged master normalizes an image before a wire API sends it.
…s that never ran

OneBot and QQ list every rule that allowing a call for good grants, and QQ's always button names no rule so the body can name each one. OneBot answers /approve and /deny in any case, and keeps approvalMinutes between one and 1440 minutes. Two QQ integration tests returned Termination, which JUnit does not discover; they return Unit now and run.
The OneBot and QQ approval texts gained an always-rules line and a short always label; the app's Chinese packs carry them so every language pack keeps the keys of en.properties.
@AkaneTan
AkaneTan merged commit b35c49d into master Oct 26, 2026
2 checks passed
@Yos-X
Yos-X deleted the feature/channel-onebot branch October 26, 2026 15:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants