Push a local file (image / video / document) to a WeChat user as a real
attachment by calling the QwenPaw wechat channel's own ILinkClient.
Why this skill exists: qwenpaw channels send and the cron agent delivery
path are text-only — media blocks from send_file_to_user get dropped in the
channel renderer (show_tool_results=False on wechat). The wechat channel's
native ILinkClient however implements the full attachment pipeline
(send_image / send_video / send_file → AES-128-ECB encrypt → upload to
WeChat CDN novac2c.cdn.weixin.qq.com → sendmessage). This skill extracts
that client from the installed QwenPaw Desktop app, caches the extraction, and
sends directly. Verified end-to-end on QwenPaw 2.2.0 (image + .doc + .txt).
Prerequisites
- QwenPaw Desktop installed at
/Applications/QwenPaw Desktop.app(override via envQWENPAW_APP). - WeChat channel enabled and QR-login completed (
bot_tokenpresent in~/.qwenpaw/workspaces/<agent>/agent.json). - The recipient has messaged the bot on WeChat at least once recently; the
context token lives in
~/.qwenpaw/wechat_context_tokens.json.
Execution
This skill ships with a batch file scripts/send.json.
Call run_tool_batch strictly in the format below, using file_path to
load the file. Do not construct your own actions list inline. Use the
absolute directory path of this skill (visible in your system prompt) to
build file_path.
run_tool_batch(
file_path="<this skill dir>/scripts/send.json",
args={
"skill_dir": "<this skill dir>",
"op": "send",
"file_path": "/absolute/path/to/file.png",
"mode": "auto",
"user": "",
"name": ""
}
)
Pass all parameters below; "" means "use the default behaviour". The first
run extracts and caches channel code (~10s); later runs take ~2s.
Batch Parameters
skill_dir: absolute path of this skill's directory (the folder holding this SKILL.md). Required.op:"send"to deliver,"check"for a full self-diagnosis without sending (imports, cache, tokens all verified). Usecheckwhen debugging.file_path: absolute path of the local file to deliver. Required. Example:/Users/you/.qwenpaw/workspaces/default/media/abc_photo.png.mode:auto(default, routes by extension: png/jpg/jpeg/gif/bmp/webp/ heic → image message; mp4/mov/avi/mkv/webm → video; everything else → file message). Forceimage|video|fileonly when the user explicitly asks (e.g. "把图片当文件发" →filefor lossless original).user: WeChat user id (...@im.wechat). Leave""to auto-use the single logged-in user. Required only when multiple users have context tokens.name: display filename for file messages.""uses the real name with the 32-hex download-hash prefix auto-stripped (media uploads are prefixed likec2121b06..._photo.png; without stripping, WeChat shows the ugly prefix).
Batch failure handling
If run_tool_batch fails (ok: false or mid-way error):
- First verify all Batch Parameters were passed with real values (never
args={}). - Run with
op: "check"to localize:bot_token empty→ user must redo QR login in QwenPaw;no context_token for <user>→ ask the user to send any message to the bot on WeChat, then retry;not found in app archive/ layout errors → add--refreshsemantics by deleting the stale cache dir under~/.cache/qwenpaw-wechat-send/(ask before deleting). - If still failing, fall back to manual step-by-step (below) and tell the user the batch needs fixing.
Step-by-step reference
The batch wraps one script; these notes explain its internals for debugging:
- Re-exec bundled Python. PYZ bytecode is Python 3.11;
marshal.loadsrequires the same minor version. The scriptos.execvs to<app>/Contents/Resources/binaries/python-runtime/python/bin/python3.11(guard envWECHAT_SEND_REEXEC=1prevents loops). Dead end to skip: the system python3 (3.9) cannot read the pycs. - Parse PyInstaller archive in-process (
load_pyz): cookie magicMEI\014\013\012\013\016viarfind; v21 cookie is 88 bytes (!8sIIii64s, detected byb'python'at cookie+24), v20 is 24 bytes (!8siiii); TOC entries!IIIBc+ name; thePYZ.pyzentry holdsPYZ\0+ pyc magic + marshal TOC{name: (ispkg, pos, len)}.extract_treewrites sourceless.pyc=pyc_magic + 12 zero bytes + zlib-decompressed code. Dead end to skip: full-archive extraction (pyinstxtractor) dumps ~500MB — unnecessary; this extracts only needed trees into~/.cache/qwenpaw-wechat-send/<exe-size>-<mtime>/. - Stub package, never real imports.
client.pycuses relative imports (from .utils,from ...exceptions), so buildpkg/qwenpaw/app/channels/wechat/{client,utils}.pycwith empty__init__.pyfiles and aChannelError(Exception)stub at bothqwenpaw.exceptionsandqwenpaw.app.exceptions. Importing the realqwenpawpackage pulls pydantic and fails — skip that dead end. - Dependency closure
PRELOAD: httpx, httpcore, h11, anyio, sniffio, idna, h2, hyperframe, hpack, exceptiongroup, typing_extensions, Crypto. Two traps encoded in the script: (a)httpcoreswallows a missing async backend into a plainImportError("Running with asyncio requires httpcore[asyncio]") — so validation must actually constructhttpx.AsyncClient(), not justimport httpx; (b)certifimust come from_internal(hascacert.pem) — extracting the PYZ copy (no data file) shadows it and SSL fails. pycryptodome native.sos are mirrored from_internal/Cryptointo the cached Crypto tree. A lazy fill loop catches any otherModuleNotFoundErrorand extracts that top-level package on demand. - Tokens (
load_tokens):agent.json → channels.wechat.{bot_token, base_url}(default basehttps://ilinkai.weixin.qq.com); context tokens keyed by user id. Values are never printed. - Send:
ILinkClient(token, base)→await start()→send_image(user, path, ctok)/send_video/send_file(user, path, display_name, ctok)→await stop(). Success = response dict carriesmessage_id. Report themessage_idand kind to the user and ask them to confirm in WeChat.
Notes & edge cases
- This sends a real WeChat message — invoke only when the user asked for this delivery or confirmed it.
- Context tokens rotate when the user messages the bot; if sends start failing with token errors, have the user ping the bot once and retry.
- After a QwenPaw Desktop update the cache key (exe size+mtime) changes and
the cache rebuilds automatically;
--refreshforces a rebuild. - Image messages are compressed by WeChat; for lossless originals send with
mode: "file"instead. - The running backend keeps its own getupdates poll — this script only calls
getuploadurl/upload/sendmessage, so it does not steal inbound messages.
Scan to join WeChat group