@oneworks/channel-wechat
v0.1.0-alpha.0
Published
WechatApi channel implementation for receiving WeChat message callbacks and replying through WechatApi.
Downloads
78
Readme
@oneworks/channel-wechat
WechatApi channel implementation for receiving WeChat message callbacks and replying through WechatApi.
Platform References
- WechatApi docs entry: https://post.wechatapi.net/a2
- API manager / token console: https://newmanager.wechatapi.net
- Message callback / API usage notes: https://post.wechatapi.net/doc-4217385
- Send text API used by this package: https://post.wechatapi.net/message/posttext
- Send image API used by
oneworks-channelmanual sends: https://post.wechatapi.net/message/postimage - Send emoji API used by
oneworks-channelmanual sends: https://post.wechatapi.net/message/postemoji - Download image API used for inbound image callbacks: https://post.wechatapi.net/message/downloadimage
Token
The channel token is the WechatApi TokenId.
According to the WechatApi onboarding page, open the API manager, enable or purchase API access, then go to access control and copy the TokenId. Configure that value as channels.<key>.token. callbackToken is optional and defaults to the same value.
Do not commit real tokens. Keep local values in .oo.dev.config.json or another private config layer.
Minimal Config
{
"server": {
"public": {
"schema": "https",
"domain": "bot.example.com",
"port": 443
}
},
"channels": {
"erjie": {
"type": "wechat",
"title": "erjie",
"token": "WechatApi-TokenId",
"appId": "wx_xxx",
"webhookSecret": "replace-with-a-random-secret",
"multimodalModel": "gpt-5.5",
"autoReconnectOnStart": true,
"access": {
"admins": ["wxid_admin"]
}
}
}
}The public callback URL is derived from server.public and the channel key:
https://bot.example.com/channels/wechat/erjie/webhook?secret=<webhookSecret>server.publicPaths is not required for channel webhooks. The server always allows /channels/*/*/webhook on public hosts and still blocks other paths unless explicitly configured. Set enableWebhook: false on one channel to disable that channel's webhook.
Public Exposure
WechatApi must be able to send an HTTPS POST request to the OneWorks server.
Recommended options:
- Production: expose the server through a stable reverse proxy or Cloudflare Tunnel, then set
server.public.schema,server.public.domain, and optionalserver.public.port. - Temporary validation: use a tunnel such as localtunnel against the local server port, then put the tunnel domain in
server.public.domain.
The channel auto-registers the callback with WechatApi /login/setCallback when server.public or channel-level serverBaseUrl can produce a callback URL. Disable this with autoRegisterCallback: false if the callback should be managed manually in the WechatApi console.
Set autoReconnectOnStart: true when the WechatApi app can remain online while callbacks stop being pushed after a local restart. The channel will call /login/reconnection for the configured appId before registering the callback again.
Runtime Behavior
- Incoming callback path:
POST /channels/wechat/<channelKey>/webhook. - Secret check: query
secret,x-oneworks-channel-secret, orx-wechatapi-secret. - Supported inbound messages:
TypeName: "AddMsg"with textMsgType: 1, imageMsgType: 3, voiceMsgType: 34, videoMsgType: 43, emojiMsgType: 47, and app/share/fileMsgType: 49. - WeChat GIF emoji callbacks are downloaded and sampled into three PNG images: first frame, middle frame, and last frame. Each image
contentItemkeeps both a preview data URL and a temporary local file path so Codex can receive it as a real local image attachment. - Static image callbacks are passed to the agent with image
contentItemswhen the callback includes inline image bytes, a downloadable image URL, or WechatApi/message/downloadImagecan convert the callback XML into a temporaryfileUrl. Voice, video, and app/share/file callbacks are converted to structured text summaries until a platform media fetch path is available. - Set channel-level
multimodalModelwhen the default model is optimized for text and does not reliably understand image attachments. Image and GIF-frame messages use that model; ordinary text keeps the default session behavior. - Outbound direct auto replies:
POST /message/postTextwithappId,toWxid, andcontent. - Manual
oneworks-channel ... send '{ "type": "image", "src": "https://..." }'sends URL images throughPOST /message/postImage. - Manual
oneworks-channel emoji send <id> --platform wechatsends WeChat custom emoji throughPOST /message/postEmojiwithappId,toWxid,emojiMd5, andemojiSizeresolved from the shared emoji registry. Useoneworks-channel emoji save <id> --platform wechat --emoji-md5 ... --emoji-size 102357to add known send metadata, andoneworks-channel emoji annotate <id> --platform wechat --tag 赞同 --note "适合回应认可、赞赏或没问题"to teach the agent what the emoji means. - Manual WeChat group text mentions use the shared
mentionsshape and are converted to WechatApiatson send. Agents should calloneworks-channel send --at <wxid> "@昵称 ..."for one user, repeat--ator use--ats wxid_a,wxid_bfor multiple users, and use--at-all "@所有人 ..."fornotify@all. The visible message text must still contain the@mention text. - WeChat direct sessions still keep a compatibility auto-delivery path for the first assistant text and final stop text, de-duplicating identical content. Channel prompts prefer deliberate external replies through
oneworks-channel, and Chat History / stop text should stay as a short internal summary after a manual send. WeChat group sessions do not auto-send runtime progress; send deliberate group-visible messages withoneworks-channel. - In WeChat group chats, a leading mention of the bot is removed before command/session dispatch. The channel resolves the bot's per-chatroom
displayNamevia/group/getChatroomMemberList, then falls back tonickNameor the configured channeltitle, so@二介 /helpenters the pipeline as/help. - WeChat group inbound text prefixes include the raw sender wxid plus available member names, for example
[wxid_xxx(昵称)(群内名)]:\n消息. Duplicate labels are omitted whennickNameanddisplayNameare the same. - Group chat ordinary text is debounced by the server before dispatch. Configure
groupMessageDebounceMsper channel to change the merge window, or set it to0to disable. Slash commands still run immediately. - If
appIdis not configured, the channel can learn it from incoming callback payloads, but proactive replies after restart are more reliable whenappIdis configured. - Messages from the logged-in bot wxid itself are ignored. WeChat Team system-account direct callbacks from
weixin, and WechatApi system/sync callbacks such asMsgType: 51orMsgType: 10002, are also ignored and should not produce replies. - When
access.adminsis missing or empty, nobody is treated as an admin. During channel startup, the server log will include an authorization command such as/authorize-admin <token>. - The authorization command is intentionally not sent through WeChat. Supported text messages before authorization receive only:
管理员尚未初始化,请联系服务维护者获取授权指令。Send the exact logged command back through the same channel to add the sender to access.admins. Restarting the service creates a new in-memory bootstrap token. Multiple text messages before authorization can each receive the generic bootstrap reply.
Health check example:
curl -i 'https://bot.example.com/channels/wechat/erjie/webhook?secret=<webhookSecret>' \
-H 'content-type: application/json' \
-d '{"TypeName":"HealthCheck"}'WechatApi Login Runbook
The OneWorks channel does not keep the WeChat account online by itself; it depends on the WechatApi app instance identified by appId.
- Check whether the account is online:
curl -sS '<apiBaseUrl>/login/checkOnline' \
-H 'content-type: application/json' \
-H 'VideosApi-token: <token>' \
-d '{"appId":"<appId>"}'data: true means WechatApi considers the account online.
- If the account is offline, get a login QR code and scan it:
curl -sS '<apiBaseUrl>/login/getLoginQrCode' \
-H 'content-type: application/json' \
-H 'VideosApi-token: <token>' \
-d '{"appId":"<appId>","type":"mac"}'Render either data.qrImgBase64 or data.qrUrl, scan it in WeChat, then poll /login/checkLogin with the returned uuid about every 5 seconds until the response includes login data or a failure. WechatApi documents the QR code timeout as about 120 seconds.
curl -sS '<apiBaseUrl>/login/checkLogin' \
-H 'content-type: application/json' \
-H 'VideosApi-token: <token>' \
-d '{"appId":"<appId>","uuid":"<uuid>","autoSliding":true}'- After a successful login, register the callback again if the server was restarted while the account was offline:
curl -sS '<apiBaseUrl>/login/setCallback' \
-H 'content-type: application/json' \
-H 'VideosApi-token: <token>' \
-d '{"token":"<callbackToken-or-token>","callbackUrl":"https://bot.example.com/channels/wechat/erjie/webhook?secret=<webhookSecret>"}'- If the phone shows the account online but OneWorks receives no text callbacks, call WechatApi
/login/reconnectionand then re-test with a plain text message:
curl -sS '<apiBaseUrl>/login/reconnection' \
-H 'content-type: application/json' \
-H 'VideosApi-token: <token>' \
-d '{"appId":"<appId>"}'Test with a plain text message from another WeChat account. Messages sent by the bot's own logged-in account can show up as self/sync callbacks and will be ignored.
macOS PhDDNS / PeanutHull Notes
When using Oray PeanutHull on macOS, prefer the official PhDDNS.app tunnel state for a domain that was configured in the GUI. Launching the standalone SDK phtunnel with only AppID/AppKey can create a different device SN with no mappings, so the public domain can resolve and complete TLS but still return an empty response.
For a local server on port 8787, the active mapping should look like:
<domain>:443 -> 127.0.0.1:8787Useful checks:
curl -sS http://127.0.0.1:8787/api/auth/status
curl -sS http://127.0.0.1:16062/ora_service/getstatusHealthy PeanutHull state includes:
status: 1for the device.hsk.is_forward: true.hsk.mappings[]contains the expected domain,host: "127.0.0.1",service: 8787,port: 443, andstatus_code: 0.
If ora_service/getmgrurl returns an empty url/account, or fw_service/GetStatus returns mappings: [], the tunnel process is online but the current device is not bound to the domain mapping. Start the official PhDDNS app or bind the mapping to the current device before debugging OneWorks.
Troubleshooting
- Public health check returns
200, but no reply: inspect the server log.ignored webhook because payload is not a supported inbound text messagemeans the callback reached the server but the message type is not currently dispatchable. - Logs show
received inbound webhook, but no WeChat message: look for[wechat] sending text reply via postText,[wechat] sent text reply via postText, or[wechat] failed to send text reply via postText. - If WechatApi reconnects or re-registers a callback, it may replay older
AddMsgcallbacks. OneWorks stores inbound message ids in SQLite for cross-restart deduplication and drops callbacks whoseCreateTimeis clearly older than the current handler startup window. - Two identical generic bootstrap replies are usually two plain text messages arriving before an admin has been initialized.
- Callback registration failures such as
push msg erroften mean the public URL was not reachable at registration time. Verify the public health check and call/login/setCallbackagain. - Logs show WechatApi system callbacks such as
MsgType: 51/10002, but user text is not arriving: call/login/reconnection, then/login/setCallback; for local long-running setups setautoReconnectOnStart: true.
