Documentation
¶
Overview ¶
modules/robot/ais_broadcast.go
YUJ-1393 / PR#82 review #2 R2 (Jerry-Xin 2026-05-19 follow-up): dispatch helpers for the `mention.ais=1` ("@所有 AI") broadcast in the robot event listener (modules/robot/event.go).
Why this lives in its own file ============================== The robot event dispatcher already mixes DM friend-gate logic, mention.uids parsing, and `@username` text parsing into one for-loop. Adding the ais-broadcast path inline keeps the hot loop readable, but the helpers themselves (the gjson truthy check, the group-member → robot filter, the dedup append) are stateless and independently testable. Keeping them here means `go test ./modules/robot/... -run AisBroadcast` exercises the branch without needing the full robotMessageListen plumbing.
Scope ===== Only GROUP channels. PERSONAL DMs already dispatch via the realUID branch in robotMessageListen and have no notion of "all members". COMMUNITY_TOPIC support is a deliberate follow-up — it requires parent-group resolution (see parseThreadChannelID in modules/webhook/api.go) and was intentionally left out of this hotfix to keep the change surface small.
Module-side composer that wires (*Robot).groupService.GetMembers + (*Robot).db.exist into the pkg/mentionrewrite.ExpandAisToBotUIDs callback shape.
Mininglamp-OSS/octo-server#144: see modules/message/mention_expand.go for the design rationale (leaf-package invariant + per-row best- effort error handling). The robot ingress uses rb.db.exist directly rather than going back through rb.IService.ExistRobot to keep the dependency tree obvious — *Robot already owns the robotDB, and the IService surface is for cross-module callers.
modules/robot/normalize_bot_mention.go
YUJ-2531 / Mininglamp-OSS/octo-server#208: server-side guard that strips a bare `mention.all=1` from payloads delivered to bots and injects `mention.humans=1` in its place.
Why this exists =============== The three-state mention contract (octo-server#94 / #142) treats `mention.all=1` as the LEGACY `@所有人` signal. Read-side clients render the "@所有人" pill from it, but the new routing semantics are driven by `mention.humans=1` (notify humans) and `mention.ais=1` (notify bots). A legacy client that only emits `mention.all=1` produces a payload that a bot adapter cannot interpret cleanly — and the openclaw adapter (openclaw-channel-octo) lives on the user's own machine, so we cannot guarantee it is updated to map `all` → humans.
To make bot ingress robust regardless of adapter version, every payload that reaches a bot's event queue is normalized here:
- `mention.all` is stripped (bots must never see the bare legacy broadcast flag), and
- `mention.humans=1` is injected when it is not already present, preserving the "notify humans" intent the legacy `all=1` carried.
Human-client delivery is NOT affected: this helper is only invoked on the bot event-queue write paths (saveRobotMessage / enqueueBotEvent- Generic). The WuKongIM fan-out to human clients keeps `all=1` for rendering.
Contract (locked by normalize_bot_mention_test.go):
- Pure: never mutates the caller's `payload` byte slice; a new []byte is returned only when a rewrite actually happens.
- Only `mention.all` and `mention.humans` are touched. `mention.ais`, `mention.uids`, `mention.entities`, and every sibling key are preserved exactly.
- No-op when there is no truthy `mention.all`: the original bytes are returned unchanged (no allocation, no re-serialization).
- Idempotent: a second pass over a normalized payload is a no-op (the stripped `all` is already gone).
- Best-effort on malformed input: if the payload does not parse, or `mention` is not an object, the original bytes are returned. We MUST NOT drop the message.
- Numeric precision: int64 fields (e.g. message_id) survive the round trip via json.Decoder.UseNumber().
modules/robot/sanitize_robot_ingress.go
PR#82 review #2 R1 (Jerry-Xin 2026-05-19 follow-up): strip any reserved `__obo_*` top-level key from robot-supplied payloads at the legacy `/v1/robots/:robot_id/:app_key/sendMessage` ingress.
Why this exists =============== The OBO persona-clone fan-out listener (modules/bot_api/obo_fanout.go) breaks its dispatch → listener → fan-out loop by dropping any inbound message whose payload carries `__obo_processed__: true` (gate 3). The bot API (`/v1/bot/sendMessage`, modules/bot_api/send.go) rejects any `__obo_*` top-level key on bot client input, and the user message API (`/v1/message/send`, modules/message/api.go) silently strips it (see modules/message/sanitize_user_ingress.go). The legacy robot `sendMessage` endpoint (this file's target) was the third ingress and was the only one still letting `__obo_processed__: true` through unmodified — a misbehaving / malicious robot script could forge the marker and suppress its own persona-clone fan-out copy, silently breaking the OBO delivery guarantee.
Strip (vs. reject) at the robot ingress ======================================= We follow the user-API "silent strip" precedent rather than the bot-API "reject with 4xx" precedent for two reasons:
- The legacy robot endpoint predates the OBO reserved namespace and there is no expectation in its public contract that `__obo_*` is meaningful. A silent strip avoids breaking any real legacy caller that may have accidentally chosen a colliding payload key.
- The new bot API is the documented surface for new integrations; authors who target the modern endpoint get the loud 4xx so mistakes are caught early. The legacy endpoint is in maintenance mode — quiet correctness is preferable to forcing callers to update their schema for a server-only namespace.
Shared contract =============== Both ingresses (user + robot) share pkg/obopayload's prefix definition with the bot-API reject + the fan-out listener's gate-3 check so the three sites cannot drift apart. A future refactor renaming the marker key only has to touch pkg/obopayload.
Package robot · YUJ-644 / Mininglamp-OSS#33 / YUJ-660
PERSONAL DM 派发前服务端权威 space_id 注入。详见 modules/bot_api/space_inject.go 顶部注释。本文件是 /v1/robot/... 路由 的等价实现。
YUJ-660 R3 Finding A — fail-closed strip: 当 querySpaceIDByRobotID 因任何 原因(DB 错误 / ErrNotFound / 孤儿 Bot 返回 "")无法解析 SpaceID 时,本层 **必须删除** payload["space_id"],并 emit `client_space_id_stripped=true` 监控 warn。之前版本在 DB 错误路径 preserve client payload,攻击者可借此通 过 forged payload.space_id 跨 Space 派发。
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type GifResult ¶
type GifResult struct {
URL string `json:"url"` // gif完整路径
// option
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
}
gif 结果
type IService ¶
type IService interface {
// GetCreatorUID 带缓存地查询机器人的创建者 UID。
// 机器人不存在或无 creator_uid 时返回空字符串及 nil error;
// 仅在底层查询异常时才返回 error。
GetCreatorUID(robotID string) (string, error)
// EnqueueBotEvent appends a synthetic event for `robotID` to the bot
// event queue consumed by /v1/bot/events. Mirrors the schema used by
// (*Robot).saveRobotMessage so /v1/bot/events serves both organic and
// synthetic events transparently. Returns an error only when the
// Redis ZADD / GenSeq call fails.
EnqueueBotEvent(robotID string, message *config.MessageResp) error
// ExistRobot reports whether `uid` identifies an active robot
// (robot.status=1). Mininglamp-OSS/octo-server#144: the ingress
// chokepoint that expands `mention.ais=1` into `mention.uids` uses
// this to filter the channel's group-member list down to the bot
// subset, so legacy adapter bots that only inspect `mention.uids`
// still receive the `@所有 AI` broadcast over the WuKongIM payload.
//
// Returns false (no error) for unknown / disabled robots — callers
// can treat any non-nil error as a "lookup failed" and skip the
// expansion best-effort (an unexpanded broadcast is no worse than
// the pre-#144 state).
ExistRobot(uid string) (bool, error)
}
IService 为其他模块提供的窄接口,避免持有完整 *Robot 以及由此产生的循环依赖。 YUJ-60: 允许 bot 创建者撤回自己 bot 发的消息时,由 message 模块注入并调用。
YUJ-1424 (PR#82 Jerry-Xin review blocker, 2026-05-20): EnqueueBotEvent exposes the bot event queue write so cross-module callers (specifically the OBO fan-out path in modules/bot_api) can deliver synthetic events without going through WuKongIM → webhook → NotifyMessagesListeners. The webhook drops NoPersist=1 messages before notifying listeners (modules/webhook/api.go handleMessageNotify, by design — see the content-type-contract comment in modules/bot_api/obo_fanout.go), so the OBO fan-out copy (which intentionally sets NoPersist=1 to keep the copy out of chat history) never reaches the bot event queue. Direct enqueue bypasses that filter.
func NewService ¶
NewService 构造一个只读 robot 服务,满足 IService 接口。
type InlineQuery ¶
type InlineQueryResult ¶
type InlineQueryResult struct {
InlineQuerySID string `json:"inline_query_sid"`
// 结果类型
Type ResultType `json:"type"`
// 结果ID
ID string `json:"id"`
Results []map[string]interface{} `json:"results,omitempty"`
NextOffset string `json:"next_offset,omitempty"`
}
func (*InlineQueryResult) Check ¶
func (i *InlineQueryResult) Check() error
type Manager ¶
func NewManager ¶
type MessageReq ¶
type Robot ¶
func (*Robot) EnqueueBotEvent ¶ added in v1.4.0
func (rb *Robot) EnqueueBotEvent(robotID string, message *config.MessageResp) error
EnqueueBotEvent — IService — *Robot variant. Delegates to the same helper used by saveRobotMessage / Service.EnqueueBotEvent so the queue write semantics cannot drift between the listener fast-path and the cross-module synthetic path.
func (*Robot) ExistRobot ¶ added in v1.4.0
ExistRobot — IService — *Robot variant. Delegates to the embedded robotDB.exist so existing *Robot instances satisfy the wider IService surface introduced for Mininglamp-OSS/octo-server#144.
func (*Robot) GetCreatorUID ¶
GetCreatorUID 让 *Robot 同时实现 IService,便于已有 Robot 实例的场景直接复用。 内部委托给已有的 getCreatorUID(含 sync.Map 缓存)。
type RobotCMDType ¶
type RobotCMDType string
机器人命令类型
const ( None RobotCMDType = "none" Inline RobotCMDType = "inline" Link RobotCMDType = "link" )
type RobotStatus ¶
type RobotStatus int
机器人状态
const ( Enable RobotStatus = 1 DisEnable RobotStatus = 0 )
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service robot 模块对外暴露的只读服务实现,供其它模块注入使用。 与 *Robot 共享底层表结构,但不承担消息/事件监听等副作用, 因此可以被重复 New 出来而不会导致重复注册 listener。
func (*Service) EnqueueBotEvent ¶ added in v1.4.0
func (s *Service) EnqueueBotEvent(robotID string, message *config.MessageResp) error
EnqueueBotEvent — IService — synthetic-event delivery path. See the IService docstring for the YUJ-1424 / PR#82 R-blocker rationale. The queue schema (key, score, payload shape, expiry) MUST match (*Robot).saveRobotMessage exactly; if that helper's wire format ever changes, update both sites in lockstep so /v1/bot/events serves synthetic and organic events identically.
func (*Service) ExistRobot ¶ added in v1.4.0
ExistRobot — IService — Service variant. Delegates to the same robotDB.exist helper used by /v1/manager/robots etc., scoped to `status=1` (active robots only). See the IService docstring for the Mininglamp-OSS/octo-server#144 rationale.