robot

package
v1.8.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 4, 2026 License: Apache-2.0 Imports: 48 Imported by: 0

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):

  1. Pure: never mutates the caller's `payload` byte slice; a new []byte is returned only when a rewrite actually happens.
  2. Only `mention.all` and `mention.humans` are touched. `mention.ais`, `mention.uids`, `mention.entities`, and every sibling key are preserved exactly.
  3. No-op when there is no truthy `mention.all`: the original bytes are returned unchanged (no allocation, no re-serialization).
  4. Idempotent: a second pass over a normalized payload is a no-op (the stripped `all` is already gone).
  5. 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.
  6. 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:

  1. 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.
  2. 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 Entitiy

type Entitiy struct {
	Length int    `json:"length"`
	Offset int    `json:"offset"`
	Type   string `json:"type"`
}

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

func NewService(ctx *config.Context) IService

NewService 构造一个只读 robot 服务,满足 IService 接口。

type InlineQuery

type InlineQuery struct {
	SID         string `json:"sid"`
	ChannelID   string `json:"channel_id"`
	ChannelType uint8  `json:"channel_type"`
	FromUID     string `json:"from_uid"` // 发送者uid
	Query       string `json:"query"`    // 查询关键字
	Offset      string `json:"offset"`
}

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

type Manager struct {
	log.Log
	// contains filtered or unexported fields
}

func NewManager

func NewManager(ctx *config.Context) *Manager

func (*Manager) Route

func (m *Manager) Route(r *wkhttp.WKHttp)

路由配置

type MessageReq

type MessageReq struct {
	Setting     uint8                  `json:"setting"`
	ChannelID   string                 `json:"channel_id"`
	ChannelType uint8                  `json:"channel_type"`
	StreamNo    string                 `json:"stream_no"`
	Entities    []*Entitiy             `json:"entities"`
	Payload     map[string]interface{} `json:"payload"`
}

type ResultType

type ResultType string
const (
	ResultTypeGIF ResultType = "gif"
)

type Robot

type Robot struct {
	log.Log
	// contains filtered or unexported fields
}

func New

func New(ctx *config.Context) *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

func (rb *Robot) ExistRobot(uid string) (bool, error)

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

func (rb *Robot) GetCreatorUID(robotID string) (string, error)

GetCreatorUID 让 *Robot 同时实现 IService,便于已有 Robot 实例的场景直接复用。 内部委托给已有的 getCreatorUID(含 sync.Map 缓存)。

func (*Robot) Route

func (rb *Robot) Route(r *wkhttp.WKHttp)

Route 路由配置

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

func (s *Service) ExistRobot(uid string) (bool, error)

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.

func (*Service) GetCreatorUID

func (s *Service) GetCreatorUID(robotID string) (string, error)

GetCreatorUID 查询机器人的创建者 UID,带 sync.Map 缓存。 未命中(bot 不存在)时返回空串 + nil,调用方据此判定为“非 bot / 无 owner”。

type TypingReq

type TypingReq struct {
	ChannelID   string `json:"channel_id"`
	ChannelType uint8  `json:"channel_type"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL