Native emoji reactions

Reactions use WeCom's topic/post_emoji endpoint. They add or remove the bot's native emoji membership on an existing post or reply; they do not publish emoji text or create a new post/comment. They use the same proposal, operator approval, read-only and uncertain-write protections as text writes.

Discover types and inspect a target

# Offline catalog: the 115 documented numeric types and official display labels.
wecom-channel-cli reaction types

# Current native reactions on a post, or on one reply beneath it.
wecom-channel-cli --use-context engineering reaction list --post POST_ID
wecom-channel-cli --use-context engineering reaction list --post POST_ID --reply REPLY_ID

The catalog is bounded local schema data in the standard list envelope. Labels retain WeCom's official spelling, including localized labels. For example, type 1 is [+1], 3 is [DONE], 6 is [OK], 69 is [收到], and 115 is [鼓掌]. Use the numeric type; arbitrary Unicode emoji and guessed identifiers are not accepted as native reaction types.

reaction list reads the target through the post/reply content API and returns only curated reaction groups. Each untrusted item contains emoji_type, user_list, and bot_reacted for the bot bound to this context. An absent target is an error, not an empty reaction list. Ambiguous or missing member identities fail instead of claiming the bot did not react. The command does not return message bodies, download URLs, cookies or raw upstream fields.

The source envelope retains the channel/post IDs and, for a reply target, source.reply_id, so merged output streams cannot confuse the two target kinds.

Discover targets with post list, reply list, search get or a verified event. In mention-only mode, pass the matching --event EVENT_ID when reading reactions; otherwise agent reads require read_all. No actor-selection flag can impersonate another member or remove their reactions.

Operator policy and permission paths

Reactions are disabled by default. The operator must explicitly set allow_reactions: true for the channel. Permission is independent of allow_posts and allow_replies; permitting one does not enable the others.

reaction_scope Required local authority Required WeCom access
mentioned (also the default when omitted) Matching authenticated topic_at_bot event for the exact post/reply, no older than 24 hours Access to react to the mentioned content
channel Explicit operator policy plus read_all: true Bot permission to read all channel content and proactively send

For a polling deployment with proactive bot access, the channel policy can be:

{
  "id": "CHANNEL_ID",
  "senders": [],
  "read_all": true,
  "observe_all": true,
  "allow_posts": false,
  "allow_replies": false,
  "allow_reactions": true,
  "reaction_scope": "channel"
}

A full read-only starter is in reactions.example.json. It needs no callback listener. Proposing remains available with broker read-only policy, but actual approval execution requires the operator to deliberately set read_only: false after reviewing the deployment. WeCom enforces its own bot permissions; local policy is a restriction, not a remote permission grant.

If --event is supplied, it must match even under channel scope. A mention on a reply authorizes that exact reply, not another reply or its parent post. Eligibility is checked both when creating the proposal and immediately before execution. Polling never manufactures mention events, and read_all alone never enables proactive reactions. Removing a reaction follows the same permission rules; an expired mention does not grant indefinite removal access.

Propose, review, apply

# Add a native +1 on the post under an operator-configured channel scope.
wecom-channel-cli --use-context engineering reaction propose \
  --post POST_ID --emoji-type 1 --action add --key acknowledge-post-1 --dry-run

wecom-channel-cli --use-context engineering reaction propose \
  --post POST_ID --emoji-type 1 --action add --key acknowledge-post-1

# Remove a native DONE reaction from the exact mentioned reply.
wecom-channel-cli --use-context engineering reaction propose \
  --post POST_ID --reply REPLY_ID --emoji-type 3 --action remove \
  --event EVENT_ID --key remove-done-1 --dry-run

Every proposal binds the channel, post, optional reply, native type, add/remove action, event when supplied, policy and idempotency key. There is no default action or toggle operation. Reusing a key with a different target, type or action fails. Dry-run uses the real builder with no state changes or upstream request.

The trusted operator inspects the proposal and approves its exact digest through the private socket, outside the agent's tool authority:

wecom-channel-cli --use-context engineering \
  --socket /run/wecom-channel/engineering-admin/admin.sock proposal get --id PROPOSAL_ID
wecom-channel-cli --use-context engineering \
  --socket /run/wecom-channel/engineering-admin/admin.sock proposal approve \
  --id PROPOSAL_ID --digest REVIEWED_DIGEST --yes --dry-run
wecom-channel-cli --use-context engineering \
  --socket /run/wecom-channel/engineering-admin/admin.sock proposal approve \
  --id PROPOSAL_ID --digest REVIEWED_DIGEST --yes

Review the exact reaction with the human just as for a textual reply: a small gesture still expresses an action under the bot's identity. Agent sockets cannot approve it, and --allow-writes cannot bypass server policy or operator approval.

Success and uncertain outcomes

The provider acknowledges post_emoji with errcode: 0 and no new resource ID. The proposal becomes sent, meaning the action was acknowledged; remote_id remains absent. Its immutable request contains the target/type/action receipt. Do not invent a reaction ID or interpret this as a newly created message.

The broker persists sending before the request. A timeout or ambiguous response becomes uncertain, and a crash may retain sending. Neither can be replayed. Use reaction list on the exact post/reply (or the WeCom UI) to inspect bot_reacted for that emoji type; an absent group means no membership was returned for that type on the existing target. After independently reconciling the desired state, the operator can resolve the proposal with its ID/digest and --outcome sent or not_sent. Omit --remote-id for reactions; it is rejected because the API creates no independent resource. Resolution changes the local ledger only.

Adding and removing are distinct reviewed intentions with separate keys. An opposite action is not an automatic rollback for an uncertain operation. Watching posts/replies does not emit a dedicated reaction-change stream; reaction membership alone is excluded from polling fingerprints. Refresh reaction list explicitly when current membership matters.

Verification and sources

Tests cover native wire bodies, add/remove targets, acknowledgment without IDs, permission and expiry checks, read-only/digest/replay gates, uncertain resolution, reaction projection and real CLI-process flows against synthetic APIs. Live native reactions have not been exercised against the test tenant yet.

The official mention response API documents the 24-hour mention path. The proactive API also documents post_emoji, allowing a separate operator-approved channel scope. The content API supplies emoji_list and the type catalog. These are native channel operations, separate from posting emoji text.