wechat-clawbot-push 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: wechat-clawbot-push
3
+ Version: 2.0.0
4
+ Summary: Personal WeChat ClawBot proactive push bridge as a stdio MCP server, for WorkBuddy automations.
5
+ Author: Yero
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/example/wechat-clawbot-push
8
+ Keywords: mcp,wechat,clawbot,workbuddy,push,stdio
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+
15
+ # wechat-clawbot-push
16
+
17
+ 个人微信 ClawBot 定时主动推送桥 —— 一个符合 MCP(stdio) 协议的服务器。注册为
18
+ WorkBuddy 连接器后,云端 WB 自动化即可直接调用,把定时/触发任务的结果**主动推送到
19
+ 用户自己的个人微信**。无需企业微信,无需抓包。
20
+
21
+ ## 安装
22
+
23
+ ```bash
24
+ pip install wechat-clawbot-push
25
+ # 或本地源码安装
26
+ pip install .
27
+ ```
28
+
29
+ ## 作为 WorkBuddy 连接器使用
30
+
31
+ 在 `~/.workbuddy/mcp.json` 注册(command 指向你的 python,args 用 -m):
32
+
33
+ ```json
34
+ {
35
+ "mcpServers": {
36
+ "wechat-clawbot-push": {
37
+ "command": "python",
38
+ "args": ["-m", "wechat_clawbot_push", "--mcp"]
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ 或安装后直接用 console script:
45
+
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "wechat-clawbot-push": {
50
+ "command": "wechat-clawbot-push",
51
+ "args": ["--mcp"]
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## 暴露的工具
58
+
59
+ | 工具 | 作用 |
60
+ |---|---|
61
+ | `push_wechat_message(text[, auto_acquire])` | 主动推送一条文本到用户个人微信(发送前自动验证 token) |
62
+ | `acquire_token()` | 获取/刷新 context_token(长轮询,需手机给 bot 发一条消息) |
63
+ | `bridge_status()` | 返回当前 token 状态,供自动化推送前自检 |
64
+
65
+ ## 首次授权
66
+
67
+ 每个用户首次使用前需执行一次 `acquire_token`:退出 WB → 手机给 bot 发一条消息 →
68
+ 脚本捕获并缓存 context_token。之后 `push_wechat_message` 即可全自动推送。
69
+
70
+ ## 协议契约(已核对腾讯官方 iLink / OpenClaw)
71
+
72
+ - 收消息:`POST https://ilinkai.weixin.qq.com/ilink/bot/getupdates`
73
+ - 发消息:`POST https://ilinkai.weixin.qq.com/ilink/bot/sendmessage`
74
+ - 鉴权:`Authorization: Bearer {bot_id:secret}`(token 自动从 `~/.workbuddy/settings.json` 读取)
75
+ - 主动发须带回 `context_token`(随入站消息返回,持久化可复用)
76
+
77
+ ## 实现说明(stdio 铁律)
78
+
79
+ - stdout 仅输出 newline-delimited JSON-RPC 消息;所有日志走 stderr
80
+ - 二进制缓冲 + UTF-8 编解码,规避 Windows GBK 中文乱码
81
+ - 运行态(token/缓存)写入用户级目录 `~/.workbuddy/wechat-clawbot-push/`,与安装位置无关、多用户隔离
82
+
83
+ ## License
84
+
85
+ MIT
@@ -0,0 +1,71 @@
1
+ # wechat-clawbot-push
2
+
3
+ 个人微信 ClawBot 定时主动推送桥 —— 一个符合 MCP(stdio) 协议的服务器。注册为
4
+ WorkBuddy 连接器后,云端 WB 自动化即可直接调用,把定时/触发任务的结果**主动推送到
5
+ 用户自己的个人微信**。无需企业微信,无需抓包。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ pip install wechat-clawbot-push
11
+ # 或本地源码安装
12
+ pip install .
13
+ ```
14
+
15
+ ## 作为 WorkBuddy 连接器使用
16
+
17
+ 在 `~/.workbuddy/mcp.json` 注册(command 指向你的 python,args 用 -m):
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "wechat-clawbot-push": {
23
+ "command": "python",
24
+ "args": ["-m", "wechat_clawbot_push", "--mcp"]
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ 或安装后直接用 console script:
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "wechat-clawbot-push": {
36
+ "command": "wechat-clawbot-push",
37
+ "args": ["--mcp"]
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ ## 暴露的工具
44
+
45
+ | 工具 | 作用 |
46
+ |---|---|
47
+ | `push_wechat_message(text[, auto_acquire])` | 主动推送一条文本到用户个人微信(发送前自动验证 token) |
48
+ | `acquire_token()` | 获取/刷新 context_token(长轮询,需手机给 bot 发一条消息) |
49
+ | `bridge_status()` | 返回当前 token 状态,供自动化推送前自检 |
50
+
51
+ ## 首次授权
52
+
53
+ 每个用户首次使用前需执行一次 `acquire_token`:退出 WB → 手机给 bot 发一条消息 →
54
+ 脚本捕获并缓存 context_token。之后 `push_wechat_message` 即可全自动推送。
55
+
56
+ ## 协议契约(已核对腾讯官方 iLink / OpenClaw)
57
+
58
+ - 收消息:`POST https://ilinkai.weixin.qq.com/ilink/bot/getupdates`
59
+ - 发消息:`POST https://ilinkai.weixin.qq.com/ilink/bot/sendmessage`
60
+ - 鉴权:`Authorization: Bearer {bot_id:secret}`(token 自动从 `~/.workbuddy/settings.json` 读取)
61
+ - 主动发须带回 `context_token`(随入站消息返回,持久化可复用)
62
+
63
+ ## 实现说明(stdio 铁律)
64
+
65
+ - stdout 仅输出 newline-delimited JSON-RPC 消息;所有日志走 stderr
66
+ - 二进制缓冲 + UTF-8 编解码,规避 Windows GBK 中文乱码
67
+ - 运行态(token/缓存)写入用户级目录 `~/.workbuddy/wechat-clawbot-push/`,与安装位置无关、多用户隔离
68
+
69
+ ## License
70
+
71
+ MIT
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "wechat-clawbot-push"
7
+ version = "2.0.0"
8
+ description = "Personal WeChat ClawBot proactive push bridge as a stdio MCP server, for WorkBuddy automations."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Yero" }]
13
+ keywords = ["mcp", "wechat", "clawbot", "workbuddy", "push", "stdio"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+ dependencies = []
20
+
21
+ [project.urls]
22
+ Homepage = "https://github.com/example/wechat-clawbot-push"
23
+
24
+ [project.scripts]
25
+ wechat-clawbot-push = "wechat_clawbot_push.__main__:main"
26
+
27
+ [tool.setuptools]
28
+ packages = ["wechat_clawbot_push"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from .server import main
2
+
3
+ __all__ = ["main"]
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from .server import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,335 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 个人微信 ClawBot 定时主动推送桥 —— 纯 MCP 服务器版(合并 获取token + 主动推送)
5
+ =============================================================================
6
+ 一个符合 MCP(stdio) 协议的服务器。注册为 WorkBuddy 连接器后,云端 WB 自动化即可
7
+ 直接调用,无需再维护单独的命令行脚本。
8
+
9
+ 暴露的工具:
10
+ - push_wechat_message(text[, auto_acquire]): 主动推送一条文本到用户个人微信。
11
+ 发送前自动验证 context_token:已获取则直接推送;未获取则返回明确指引,
12
+ 提示先调用 acquire_token 并在手机给 bot 发一条消息完成绑定。
13
+ auto_acquire=true 时,token 缺失会尝试自动长轮询获取(需手机配合,会阻塞等待)。
14
+ - acquire_token(): 获取/刷新 context_token(长轮询,期间需用手机给 bot 发一条消息)。
15
+ - bridge_status(): 返回当前 token 状态,供自动化推送前自检。
16
+
17
+ 契约(已核对腾讯官方开源 OpenClaw / iLink 协议):
18
+ - 收消息(长轮询): POST https://ilinkai.weixin.qq.com/ilink/bot/getupdates
19
+ - 发消息(主动): POST https://ilinkai.weixin.qq.com/ilink/bot/sendmessage
20
+ - 鉴权: AuthorizationType: ilink_bot_token + Authorization: Bearer {bot_id:secret}
21
+ - 每次请求: X-WECHAT-UIN = base64(随机uint32) 防重放
22
+ - 主动发须带回 context_token(随入站消息返回,持久化可复用)
23
+
24
+ token 默认从 ~/.workbuddy/settings.json 的
25
+ claw.users.*.channels.weixinClawBot.botToken (= "bot_id:secret") 自动读取。
26
+ context_token / 用户微信id / 游标 缓存到用户级目录
27
+ ~/.workbuddy/wechat-clawbot-push/push_cache.json(与安装位置无关,多用户隔离)。
28
+
29
+ 用法:
30
+ python -m wechat_clawbot_push --mcp # 以 stdio MCP 服务器运行(WB 连接器,主用)
31
+ python -m wechat_clawbot_push --refresh # 本地手动获取 token(退出 WB 后,手机发消息)
32
+ python -m wechat_clawbot_push --test "x" # 本地手动推送一条(调试用)
33
+ wechat-clawbot-push --mcp # 安装 console script 后等价
34
+ """
35
+
36
+ import json
37
+ import os
38
+ import sys
39
+ import time
40
+ import base64
41
+ import argparse
42
+ import urllib.request
43
+ import urllib.error
44
+
45
+ BASE_URL = "https://ilinkai.weixin.qq.com"
46
+ CHANNEL_VERSION = "1.0.3"
47
+ SK_ROUTE_TAG = "1001"
48
+
49
+ # 运行态数据放用户级目录,绝不依赖安装位置(site-packages 不可写、且多用户共享会冲突)。
50
+ APP_DIR = os.path.join(os.path.expanduser("~"), ".workbuddy", "wechat-clawbot-push")
51
+ os.makedirs(APP_DIR, exist_ok=True)
52
+ CONFIG_PATH = os.path.join(APP_DIR, "config.json")
53
+ CACHE_PATH = os.path.join(APP_DIR, "push_cache.json")
54
+ SETTINGS_PATH = os.path.expanduser(r"~/.workbuddy/settings.json")
55
+
56
+
57
+ def load_config():
58
+ if os.path.exists(CONFIG_PATH):
59
+ try:
60
+ with open(CONFIG_PATH, encoding="utf-8") as f:
61
+ return json.load(f)
62
+ except Exception:
63
+ return {}
64
+ return {}
65
+
66
+
67
+ def load_cache():
68
+ if os.path.exists(CACHE_PATH):
69
+ try:
70
+ with open(CACHE_PATH, encoding="utf-8") as f:
71
+ return json.load(f)
72
+ except Exception:
73
+ return {}
74
+ return {}
75
+
76
+
77
+ def save_cache(data):
78
+ tmp = CACHE_PATH + ".tmp"
79
+ with open(tmp, "w", encoding="utf-8") as f:
80
+ json.dump(data, f, ensure_ascii=False, indent=2)
81
+ os.replace(tmp, CACHE_PATH)
82
+
83
+
84
+ def get_full_token_from_settings():
85
+ if not os.path.exists(SETTINGS_PATH):
86
+ raise RuntimeError("找不到 settings.json: " + SETTINGS_PATH)
87
+ with open(SETTINGS_PATH, encoding="utf-8") as f:
88
+ d = json.load(f)
89
+ users = d.get("claw", {}).get("users", {})
90
+ for uid, u in users.items():
91
+ ch = u.get("channels", {}).get("weixinClawBot", {})
92
+ bt = ch.get("botToken")
93
+ if bt and ":" in bt:
94
+ return bt
95
+ raise RuntimeError("settings.json 中未找到 weixinClawBot.botToken")
96
+
97
+
98
+ def resolve_token(config):
99
+ raw = (config.get("bot_token") or "AUTO").strip()
100
+ if not raw or raw.upper() == "AUTO":
101
+ return get_full_token_from_settings()
102
+ return raw
103
+
104
+
105
+ def make_uin_header():
106
+ u = int.from_bytes(os.urandom(4), "big")
107
+ return base64.b64encode(str(u).encode("ascii")).decode("ascii")
108
+
109
+
110
+ def ilink_post(path, body, secret):
111
+ url = BASE_URL + path
112
+ data = json.dumps(body).encode("utf-8")
113
+ req = urllib.request.Request(url, data=data, method="POST")
114
+ req.add_header("Content-Type", "application/json")
115
+ req.add_header("AuthorizationType", "ilink_bot_token")
116
+ req.add_header("Authorization", "Bearer " + secret)
117
+ req.add_header("X-WECHAT-UIN", make_uin_header())
118
+ req.add_header("SKRouteTag", SK_ROUTE_TAG)
119
+ try:
120
+ with urllib.request.urlopen(req, timeout=45) as resp:
121
+ return resp.status, json.loads(resp.read().decode("utf-8"))
122
+ except urllib.error.HTTPError as e:
123
+ try:
124
+ return e.code, json.loads(e.read().decode("utf-8"))
125
+ except Exception:
126
+ return e.code, {"ret": -1, "errmsg": str(e)}
127
+ except Exception as e:
128
+ return -1, {"ret": -1, "errmsg": str(e)}
129
+
130
+
131
+ def acquire_token_once(config, wait_seconds=35):
132
+ """长轮询一次获取 context_token + user_id。返回 (ok, detail)。"""
133
+ bearer = resolve_token(config)
134
+ cache = load_cache()
135
+ cursor = cache.get("get_updates_buf", "")
136
+ status, resp = ilink_post(
137
+ "/ilink/bot/getupdates",
138
+ {"get_updates_buf": cursor, "base_info": {"channel_version": CHANNEL_VERSION}},
139
+ bearer,
140
+ )
141
+ if "errcode" in resp:
142
+ return False, "getupdates 失败: errcode %s %s" % (resp.get("errcode"), resp.get("errmsg", ""))
143
+ if resp.get("get_updates_buf"):
144
+ cache["get_updates_buf"] = resp["get_updates_buf"]
145
+ msgs = resp.get("msgs") or []
146
+ if not msgs:
147
+ return False, "本轮无新消息(%d 秒内手机未给 bot 发消息)。token 未变化。" % wait_seconds
148
+ m0 = msgs[0]
149
+ cache["user_id"] = m0.get("from_user_id")
150
+ cache["context_token"] = m0.get("context_token")
151
+ save_cache(cache)
152
+ return True, "已获取并缓存 context_token / user_id: %s" % cache.get("user_id")
153
+
154
+
155
+ def do_send(text):
156
+ """用缓存的 context_token 主动发一条文本到用户微信。返回 (ok, code, detail)。"""
157
+ config = load_config()
158
+ bearer = resolve_token(config)
159
+ cache = load_cache()
160
+ user_id = config.get("user_id") or cache.get("user_id")
161
+ ctx = cache.get("context_token")
162
+ if not user_id or not ctx:
163
+ return False, "NO_TOKEN", "尚未获取 token:请先调用 acquire_token 工具,并在手机给 bot 发一条消息完成绑定,再执行推送。"
164
+ msg = {
165
+ "from_user_id": "",
166
+ "to_user_id": user_id,
167
+ "client_id": "push-" + os.urandom(8).hex(),
168
+ "message_type": 2,
169
+ "message_state": 2,
170
+ "context_token": ctx,
171
+ "item_list": [{"type": 1, "text_item": {"text": text}}],
172
+ }
173
+ body = {"msg": msg, "base_info": {"channel_version": CHANNEL_VERSION}}
174
+ status, resp = ilink_post("/ilink/bot/sendmessage", body, bearer)
175
+ if "errcode" in resp or resp.get("ret") == -2:
176
+ err = resp.get("errcode")
177
+ if err == -14:
178
+ return False, "TOKEN_EXPIRED", "token 已失效(errcode -14):请重新调用 acquire_token 获取。"
179
+ return False, "SEND_FAIL", "HTTP %s | errcode %s | %s | ret %s" % (status, err, resp.get("errmsg", ""), resp.get("ret"))
180
+ return True, "OK", "HTTP %s | 发送成功" % status
181
+
182
+
183
+ # ---- MCP 服务器(stdio)----
184
+ def run_mcp(config):
185
+ TOOLS = [
186
+ {
187
+ "name": "push_wechat_message",
188
+ "description": (
189
+ "向用户【个人微信】(ClawBot)主动推送一条文本消息。发送前自动验证 context_token:"
190
+ "已获取则直接推送;未获取或已失效则返回明确指引,提示先调用 acquire_token 并在手机给 bot 发消息。"
191
+ "供 WB 自动化在定时/触发任务完成后调用,把结果推送到用户微信。"
192
+ ),
193
+ "inputSchema": {
194
+ "type": "object",
195
+ "properties": {
196
+ "text": {"type": "string", "description": "要推送到微信的文本内容"},
197
+ "auto_acquire": {
198
+ "type": "boolean",
199
+ "description": "可选。token 缺失时是否自动尝试获取(会阻塞等待手机消息,默认 false)",
200
+ },
201
+ },
202
+ "required": ["text"],
203
+ },
204
+ },
205
+ {
206
+ "name": "acquire_token",
207
+ "description": (
208
+ "获取/刷新 context_token(长轮询,约 35 秒内需用手机给 bot 发一条消息以完成绑定)。"
209
+ "推送前若 bridge_status 显示未获取 token,应先调用本工具。"
210
+ ),
211
+ "inputSchema": {"type": "object", "properties": {}},
212
+ },
213
+ {
214
+ "name": "bridge_status",
215
+ "description": "返回推送桥当前状态:是否已缓存 context_token、目标用户微信id。供自动化推送前自检。",
216
+ "inputSchema": {"type": "object", "properties": {}},
217
+ },
218
+ ]
219
+
220
+ def send(obj):
221
+ # stdout 铁律:只能写 JSON-RPC 消息(newline-delimited),且必须 UTF-8 字节。
222
+ # 任何日志/print 都只能走 stderr,否则会破坏协议解析。
223
+ data = (json.dumps(obj, ensure_ascii=False) + "\n").encode("utf-8")
224
+ sys.stdout.buffer.write(data)
225
+ sys.stdout.buffer.flush()
226
+
227
+ def log(msg):
228
+ sys.stderr.write("[mcp] " + msg + "\n")
229
+ sys.stderr.flush()
230
+
231
+ log("推送桥 MCP 服务器启动(stdio)。")
232
+
233
+ # 二进制缓冲 + UTF-8 解码(Windows 默认 GBK 会把中文参数解成乱码 → 微信收到乱码)。
234
+ for raw_bytes in sys.stdin.buffer:
235
+ raw = raw_bytes.decode("utf-8").strip()
236
+ if not raw:
237
+ continue
238
+ try:
239
+ msg = json.loads(raw)
240
+ except Exception as e:
241
+ log("JSON 解析失败: " + str(e))
242
+ continue
243
+ method = msg.get("method")
244
+ mid = msg.get("id")
245
+ params = msg.get("params") or {}
246
+
247
+ if method == "initialize":
248
+ send({
249
+ "jsonrpc": "2.0",
250
+ "id": mid,
251
+ "result": {
252
+ "protocolVersion": params.get("protocolVersion", "2024-11-05"),
253
+ "capabilities": {"tools": {}},
254
+ "serverInfo": {"name": "wechat-clawbot-push", "version": "2.0.0"},
255
+ },
256
+ })
257
+ elif method == "notifications/initialized":
258
+ continue
259
+ elif method == "tools/list":
260
+ send({"jsonrpc": "2.0", "id": mid, "result": {"tools": TOOLS}})
261
+ elif method == "tools/call":
262
+ name = params.get("name")
263
+ arguments = params.get("arguments") or {}
264
+ if name == "push_wechat_message":
265
+ text = (arguments.get("text") or "").strip()
266
+ auto_acquire = bool(arguments.get("auto_acquire", False))
267
+ if not text:
268
+ send({"jsonrpc": "2.0", "id": mid, "result": {
269
+ "content": [{"type": "text", "text": "缺少 text 参数"}], "isError": True}})
270
+ continue
271
+ cache = load_cache()
272
+ if not (cache.get("context_token") and (config.get("user_id") or cache.get("user_id"))):
273
+ if auto_acquire:
274
+ ok, detail = acquire_token_once(config)
275
+ if not ok:
276
+ send({"jsonrpc": "2.0", "id": mid, "result": {"content": [{"type": "text", "text": "token 缺失且自动获取失败:" + detail + "。请改显式调用 acquire_token 并在手机给 bot 发消息。"}], "isError": True}})
277
+ continue
278
+ log("auto_acquire 成功,继续推送。")
279
+ else:
280
+ send({"jsonrpc": "2.0", "id": mid, "result": {"content": [{"type": "text", "text": "尚未获取 token:请先调用 acquire_token 工具,并在手机给 bot 发一条消息完成绑定,再执行推送。"}], "isError": True}})
281
+ continue
282
+ ok, code, detail = do_send(text)
283
+ if not ok and code == "TOKEN_EXPIRED":
284
+ send({"jsonrpc": "2.0", "id": mid, "result": {"content": [{"type": "text", "text": detail + " 请调用 acquire_token 重新获取后再推送。"}], "isError": True}})
285
+ continue
286
+ send({"jsonrpc": "2.0", "id": mid, "result": {"content": [{"type": "text", "text": detail}], "isError": not ok}})
287
+ elif name == "acquire_token":
288
+ ok, detail = acquire_token_once(config)
289
+ send({"jsonrpc": "2.0", "id": mid, "result": {"content": [{"type": "text", "text": detail}], "isError": not ok}})
290
+ elif name == "bridge_status":
291
+ cache = load_cache()
292
+ send({"jsonrpc": "2.0", "id": mid, "result": {"content": [{"type": "text", "text": json.dumps({
293
+ "has_context_token": bool(cache.get("context_token")),
294
+ "user_id": cache.get("user_id"),
295
+ "cache_path": CACHE_PATH,
296
+ }, ensure_ascii=False)}], "isError": False}})
297
+ else:
298
+ send({"jsonrpc": "2.0", "id": mid, "result": {"content": [{"type": "text", "text": "未知工具: " + str(name)}], "isError": True}})
299
+ else:
300
+ if mid is not None:
301
+ send({"jsonrpc": "2.0", "id": mid, "result": {}})
302
+
303
+
304
+ # ---- 本地 CLI 调试(保留,便于无 WB 时验证)----
305
+ def cmd_refresh(config):
306
+ ok, detail = acquire_token_once(config)
307
+ print(("HTTP 成功 | " if ok else "[FAIL] ") + detail)
308
+
309
+
310
+ def cmd_test(config, text):
311
+ ok, code, detail = do_send(text)
312
+ print(detail)
313
+ if ok:
314
+ print("[OK] 已主动推送。请检查手机微信是否收到。")
315
+
316
+
317
+ def main():
318
+ ap = argparse.ArgumentParser()
319
+ ap.add_argument("--mcp", action="store_true", help="以 stdio MCP 服务器模式运行(主用)")
320
+ ap.add_argument("--refresh", action="store_true", help="本地获取 token(需退出 WB 后手机发消息)")
321
+ ap.add_argument("--test", metavar="TEXT", help="本地手动推送一条(调试)")
322
+ args = ap.parse_args()
323
+ config = load_config()
324
+ if args.mcp:
325
+ run_mcp(config)
326
+ elif args.refresh:
327
+ cmd_refresh(config)
328
+ elif args.test:
329
+ cmd_test(config, args.test)
330
+ else:
331
+ ap.print_help()
332
+
333
+
334
+ if __name__ == "__main__":
335
+ main()
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: wechat-clawbot-push
3
+ Version: 2.0.0
4
+ Summary: Personal WeChat ClawBot proactive push bridge as a stdio MCP server, for WorkBuddy automations.
5
+ Author: Yero
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/example/wechat-clawbot-push
8
+ Keywords: mcp,wechat,clawbot,workbuddy,push,stdio
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+
15
+ # wechat-clawbot-push
16
+
17
+ 个人微信 ClawBot 定时主动推送桥 —— 一个符合 MCP(stdio) 协议的服务器。注册为
18
+ WorkBuddy 连接器后,云端 WB 自动化即可直接调用,把定时/触发任务的结果**主动推送到
19
+ 用户自己的个人微信**。无需企业微信,无需抓包。
20
+
21
+ ## 安装
22
+
23
+ ```bash
24
+ pip install wechat-clawbot-push
25
+ # 或本地源码安装
26
+ pip install .
27
+ ```
28
+
29
+ ## 作为 WorkBuddy 连接器使用
30
+
31
+ 在 `~/.workbuddy/mcp.json` 注册(command 指向你的 python,args 用 -m):
32
+
33
+ ```json
34
+ {
35
+ "mcpServers": {
36
+ "wechat-clawbot-push": {
37
+ "command": "python",
38
+ "args": ["-m", "wechat_clawbot_push", "--mcp"]
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ 或安装后直接用 console script:
45
+
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "wechat-clawbot-push": {
50
+ "command": "wechat-clawbot-push",
51
+ "args": ["--mcp"]
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## 暴露的工具
58
+
59
+ | 工具 | 作用 |
60
+ |---|---|
61
+ | `push_wechat_message(text[, auto_acquire])` | 主动推送一条文本到用户个人微信(发送前自动验证 token) |
62
+ | `acquire_token()` | 获取/刷新 context_token(长轮询,需手机给 bot 发一条消息) |
63
+ | `bridge_status()` | 返回当前 token 状态,供自动化推送前自检 |
64
+
65
+ ## 首次授权
66
+
67
+ 每个用户首次使用前需执行一次 `acquire_token`:退出 WB → 手机给 bot 发一条消息 →
68
+ 脚本捕获并缓存 context_token。之后 `push_wechat_message` 即可全自动推送。
69
+
70
+ ## 协议契约(已核对腾讯官方 iLink / OpenClaw)
71
+
72
+ - 收消息:`POST https://ilinkai.weixin.qq.com/ilink/bot/getupdates`
73
+ - 发消息:`POST https://ilinkai.weixin.qq.com/ilink/bot/sendmessage`
74
+ - 鉴权:`Authorization: Bearer {bot_id:secret}`(token 自动从 `~/.workbuddy/settings.json` 读取)
75
+ - 主动发须带回 `context_token`(随入站消息返回,持久化可复用)
76
+
77
+ ## 实现说明(stdio 铁律)
78
+
79
+ - stdout 仅输出 newline-delimited JSON-RPC 消息;所有日志走 stderr
80
+ - 二进制缓冲 + UTF-8 编解码,规避 Windows GBK 中文乱码
81
+ - 运行态(token/缓存)写入用户级目录 `~/.workbuddy/wechat-clawbot-push/`,与安装位置无关、多用户隔离
82
+
83
+ ## License
84
+
85
+ MIT
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ wechat_clawbot_push/__init__.py
4
+ wechat_clawbot_push/__main__.py
5
+ wechat_clawbot_push/server.py
6
+ wechat_clawbot_push.egg-info/PKG-INFO
7
+ wechat_clawbot_push.egg-info/SOURCES.txt
8
+ wechat_clawbot_push.egg-info/dependency_links.txt
9
+ wechat_clawbot_push.egg-info/entry_points.txt
10
+ wechat_clawbot_push.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ wechat-clawbot-push = wechat_clawbot_push.__main__:main
@@ -0,0 +1 @@
1
+ wechat_clawbot_push