xqc-pymagicpush 0.0.1__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xiaoqiangclub
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.3
2
+ Name: xqc-pymagicpush
3
+ Version: 0.0.1
4
+ Summary: 🎉 微信公众号:XiaoqiangClub 自用模块。
5
+ Keywords: magicpush,push,notification,feishu,dingtalk,telegram,bark,wecom
6
+ Author: Xiaoqiang
7
+ Author-email: xiaoqiangclub@hotmail.com
8
+ Requires-Python: >=3.10
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Communications
17
+ Requires-Dist: python-dotenv (>=1.0.0)
18
+ Requires-Dist: requests (>=2.28.0)
19
+ Project-URL: Homepage, https://xiaoqiangclub.github.io/
20
+ Project-URL: Repository, https://github.com/xiaoqiangclub/pymagicpush
@@ -0,0 +1,32 @@
1
+ [tool.poetry]
2
+ name = "xqc-pymagicpush"
3
+ version = "0.0.1"
4
+ description = "🎉 微信公众号:XiaoqiangClub 自用模块。"
5
+ authors = ["Xiaoqiang <xiaoqiangclub@hotmail.com>"]
6
+ homepage = "https://xiaoqiangclub.github.io/"
7
+ repository = "https://github.com/xiaoqiangclub/pymagicpush"
8
+ keywords = ["magicpush", "push", "notification", "feishu", "dingtalk", "telegram", "bark", "wecom"]
9
+ classifiers = [
10
+ "Programming Language :: Python :: 3",
11
+ "Programming Language :: Python :: 3.10",
12
+ "Programming Language :: Python :: 3.11",
13
+ "Programming Language :: Python :: 3.12",
14
+ "Programming Language :: Python :: 3.13",
15
+ "Operating System :: OS Independent",
16
+ "Intended Audience :: Developers",
17
+ "Topic :: Communications",
18
+ ]
19
+ packages = [{ include = "pymagicpush", from = "src" }]
20
+ include = [{ path = "src/pymagicpush/skill/SKILL.md", format = ["sdist", "wheel"] }]
21
+
22
+ [tool.poetry.dependencies]
23
+ python = ">=3.10"
24
+ requests = ">=2.28.0"
25
+ python-dotenv = ">=1.0.0"
26
+
27
+ [tool.poetry.scripts]
28
+ pymagicpush = "pymagicpush.cli:main"
29
+
30
+ [build-system]
31
+ requires = ["poetry-core>=1.0.0"]
32
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,27 @@
1
+ """pymagicpush —— MagicPush 消息推送服务的 Python 客户端。
2
+
3
+ 快速示例::
4
+
5
+ from pymagicpush import MagicPushClient
6
+ client = MagicPushClient() # 自动读取 .env / 环境变量
7
+ client.send(content="Hello from pymagicpush!", title="测试")
8
+ """
9
+ from .config import Config
10
+ from .client import MagicPushClient
11
+ from .exceptions import (
12
+ MagicPushError,
13
+ MagicPushAuthError,
14
+ MagicPushAPIError,
15
+ MagicPushConfigError,
16
+ )
17
+
18
+ __version__ = "0.1.0"
19
+ __all__ = [
20
+ "MagicPushClient",
21
+ "Config",
22
+ "MagicPushError",
23
+ "MagicPushAuthError",
24
+ "MagicPushAPIError",
25
+ "MagicPushConfigError",
26
+ "__version__",
27
+ ]
@@ -0,0 +1,452 @@
1
+ """pymagicpush 命令行工具。
2
+
3
+ 提供消息推送、渠道/接口管理、推送记录、管理员、配置查看、AI Skill 安装等子命令。
4
+ 运行 `pymagicpush --help` 查看完整帮助;`pymagicpush <子命令> --help` 查看子命令详情。
5
+ """
6
+ import argparse
7
+ import json
8
+ import os
9
+ import shutil
10
+ import sys
11
+ from typing import Any, Optional
12
+
13
+ from . import __version__
14
+ from .client import MagicPushClient
15
+ from .config import Config
16
+ from .exceptions import MagicPushError
17
+
18
+ # .env 模板(内联,确保 wheel 安装也能在 skill 目录生成配置模板)
19
+ _ENV_EXAMPLE_TEMPLATE = """# ============================================================
20
+ # pymagicpush 配置示例
21
+ #
22
+ # ⚠️ 前置:本模块只是客户端,使用前必须先部署 MagicPush 服务端。
23
+ # 部署教程:https://xiaoqiangclub.blog.csdn.net/article/details/160310725
24
+ # 部署完成后,把下方服务地址、推送令牌、登录账号填入即可。
25
+ #
26
+ # 使用方式:
27
+ # 1. 复制本文件为 .env:cp .env.example .env
28
+ # 2. 取消下方对应行的注释(删去行首 #)
29
+ # 3. 把示例值替换为你的真实值
30
+ # .env 已被 .gitignore 忽略,不会提交。
31
+ # 所有配置项均可通过同名环境变量覆盖(环境变量优先级高于 .env)。
32
+ # ============================================================
33
+
34
+ # -------------------- 服务地址 --------------------
35
+ # MagicPush 服务地址(部署后的后端地址,含端口,不要带 /api 后缀)
36
+ # 优先作为「本地地址」使用,客户端初始化时会先尝试连通本地地址。
37
+ # MAGICPUSH_BASE_URL=http://localhost:3000
38
+
39
+ # 远程访问地址(可选,域名或公网 IP)
40
+ # 配置后客户端会在本地不可达时自动切换到远程地址,保证外网场景仍可推送。
41
+ # 留空或保持注释则仅使用上面的 MAGICPUSH_BASE_URL。
42
+ # MAGICPUSH_BASE_URL_REMOTE=https://your-domain.com
43
+
44
+ # -------------------- 推送令牌 --------------------
45
+ # 默认推送令牌(接口令牌,用于 /api/push 推送,无需登录)
46
+ # 推送时未显式指定令牌即使用此值。
47
+ # MAGICPUSH_TOKEN=your_push_token
48
+
49
+ # 带描述的推送令牌列表(可选,多接口场景按描述选用)
50
+ # 一个 MagicPush 服务可创建多个接口,每个接口对应独立令牌;
51
+ # 格式为「令牌:描述」,多对用逗号分隔——AI 会读取描述理解每个令牌的用途。
52
+ # 调用 push(token_desc="关键字") 时按描述模糊匹配取令牌。
53
+ # 两种写法任选其一:
54
+ # 简易格式:令牌1:描述1,令牌2:描述2
55
+ # JSON 格式:[{"token":"令牌1","desc":"描述1"},{"token":"令牌2","desc":"描述2"}]
56
+ # 示例(按用途/渠道描述,便于 AI 选用):
57
+ # MAGICPUSH_TOKENS=abc123:飞书频道推送,def456:告警通知专用,ghi789:每日报告
58
+ # MAGICPUSH_TOKENS=
59
+
60
+ # -------------------- 登录账号 --------------------
61
+ # 登录账号(用于调用需要 JWT 认证的管理类接口,如渠道/接口管理)
62
+ # MAGICPUSH_EMAIL=your_email@example.com
63
+ # MAGICPUSH_PASSWORD=your_password
64
+
65
+ # 已有的 JWT 令牌(可选,提供后将跳过登录;access 过期会自动用 refresh 续期)
66
+ # MAGICPUSH_ACCESS_TOKEN=
67
+ # MAGICPUSH_REFRESH_TOKEN=
68
+
69
+ # -------------------- 请求行为 --------------------
70
+ # 请求超时时间(秒)
71
+ # MAGICPUSH_TIMEOUT=30
72
+
73
+ # 网络失败重试次数(不含首次请求,默认 2 次)
74
+ # 仅对网络层错误(连接超时、SSL 抖动等)生效;业务错误(如 401/4xx)不重试。
75
+ # 设为 0 表示不重试,仅尝试一次。
76
+ # MAGICPUSH_MAX_RETRIES=2
77
+ """
78
+
79
+
80
+ def _print(obj: Any) -> None:
81
+ """以缩进 JSON 输出结果,非 JSON 则原样打印。"""
82
+ if isinstance(obj, (dict, list)):
83
+ print(json.dumps(obj, ensure_ascii=False, indent=2, default=str))
84
+ else:
85
+ print(obj)
86
+
87
+
88
+ def _build_client(args: argparse.Namespace) -> MagicPushClient:
89
+ """根据命令行参数构造客户端。"""
90
+ return MagicPushClient(
91
+ base_url=getattr(args, "base_url", None),
92
+ token=getattr(args, "token", None),
93
+ email=getattr(args, "email", None),
94
+ password=getattr(args, "password", None),
95
+ env_path=getattr(args, "env", None),
96
+ )
97
+
98
+
99
+ def _add_conn_args(p: argparse.ArgumentParser) -> None:
100
+ """为子命令添加连接相关公共参数。"""
101
+ p.add_argument("--base-url", help="MagicPush 服务地址(不含 /api),覆盖 .env/环境变量")
102
+ p.add_argument("--token", help="推送令牌,覆盖 .env/环境变量")
103
+ p.add_argument("--token-desc", help="令牌描述关键字,从 MAGICPUSH_TOKENS 中按描述模糊匹配取令牌推送(与 --token 互斥)")
104
+ p.add_argument("--email", help="登录邮箱,覆盖 .env/环境变量")
105
+ p.add_argument("--password", help="登录密码,覆盖 .env/环境变量")
106
+ p.add_argument("--env", help=".env 文件路径")
107
+
108
+
109
+ # ----------------------------------------------------------------------
110
+ # 子命令实现
111
+ # ----------------------------------------------------------------------
112
+ def cmd_health(args: argparse.Namespace) -> int:
113
+ """健康检查。"""
114
+ _print(_build_client(args).health())
115
+ return 0
116
+
117
+
118
+ def cmd_version(args: argparse.Namespace) -> int:
119
+ """获取服务版本。"""
120
+ _print(_build_client(args).version())
121
+ return 0
122
+
123
+
124
+ def _parse_extra_data(raw: Optional[str]) -> Optional[dict]:
125
+ """解析 --extra-data 参数(JSON 字符串),为空返回 None。"""
126
+ if not raw:
127
+ return None
128
+ try:
129
+ return json.loads(raw)
130
+ except json.JSONDecodeError as e:
131
+ raise SystemExit(f"[错误] --extra-data 不是合法 JSON:{e}")
132
+
133
+
134
+ def cmd_push(args: argparse.Namespace) -> int:
135
+ """推送消息。"""
136
+ client = _build_client(args)
137
+ extra_data = _parse_extra_data(args.extra_data)
138
+ # 按渠道名定向推送(需登录),优先于 --channel-id
139
+ if args.channel_name:
140
+ _print(client.push_by_channel_name(
141
+ name=args.channel_name, title=args.title, content=args.content,
142
+ type=args.type, url=args.url, extra_data=extra_data,
143
+ ))
144
+ return 0
145
+ result = client.push(
146
+ token=args.token, token_desc=args.token_desc,
147
+ title=args.title, content=args.content, type=args.type, url=args.url, method=args.method,
148
+ extra_data=extra_data, channel_id=args.channel_id,
149
+ )
150
+ _print(result)
151
+ return 0
152
+
153
+
154
+ def cmd_send(args: argparse.Namespace) -> int:
155
+ """快捷推送(使用配置中的 token)。"""
156
+ client = _build_client(args)
157
+ extra_data = _parse_extra_data(args.extra_data)
158
+ # 按渠道名定向推送(需登录),优先于 --channel-id
159
+ if args.channel_name:
160
+ _print(client.push_by_channel_name(
161
+ name=args.channel_name, title=args.title, content=args.content,
162
+ type=args.type, url=args.url, extra_data=extra_data,
163
+ ))
164
+ return 0
165
+ _print(client.send(
166
+ content=args.content, title=args.title, type=args.type, url=args.url,
167
+ extra_data=extra_data, channel_id=args.channel_id, token_desc=args.token_desc,
168
+ ))
169
+ return 0
170
+
171
+
172
+ def cmd_channels(args: argparse.Namespace) -> int:
173
+ """渠道管理。"""
174
+ client = _build_client(args)
175
+ action = args.action
176
+ if action == "list":
177
+ _print(client.list_channels())
178
+ elif action == "types":
179
+ _print(client.get_channel_types())
180
+ elif action == "get":
181
+ _print(client.get_channel(args.id))
182
+ elif action == "create":
183
+ cfg = json.loads(args.config) if args.config else {}
184
+ _print(client.create_channel(args.type, args.name, cfg, args.description))
185
+ elif action == "delete":
186
+ _print(client.delete_channel(args.id))
187
+ elif action == "test":
188
+ _print(client.test_channel(args.id))
189
+ return 0
190
+
191
+
192
+ def cmd_endpoints(args: argparse.Namespace) -> int:
193
+ """接口管理。"""
194
+ client = _build_client(args)
195
+ action = args.action
196
+ if action == "list":
197
+ _print(client.list_endpoints(args.page, args.pageSize))
198
+ elif action == "get":
199
+ _print(client.get_endpoint(args.id))
200
+ elif action == "create":
201
+ data = json.loads(args.data) if args.data else {}
202
+ _print(client.create_endpoint(data))
203
+ elif action == "delete":
204
+ _print(client.delete_endpoint(args.id))
205
+ elif action == "regenerate-token":
206
+ _print(client.regenerate_endpoint_token(args.id))
207
+ elif action == "channels":
208
+ _print(client.get_endpoint_channels(args.id))
209
+ elif action == "bind":
210
+ ids = json.loads(args.channel_ids)
211
+ _print(client.update_endpoint_channels(args.id, ids))
212
+ return 0
213
+
214
+
215
+ def cmd_logs(args: argparse.Namespace) -> int:
216
+ """推送记录。"""
217
+ client = _build_client(args)
218
+ action = args.action
219
+ if action == "list":
220
+ _print(client.list_logs(args.page, args.pageSize, args.channelType, args.status, args.keyword, args.startDate, args.endDate))
221
+ elif action == "stats":
222
+ _print(client.get_log_stats())
223
+ elif action == "get":
224
+ _print(client.get_log(args.id))
225
+ elif action == "clear":
226
+ _print(client.clear_logs())
227
+ return 0
228
+
229
+
230
+ def cmd_auth(args: argparse.Namespace) -> int:
231
+ """认证。"""
232
+ client = _build_client(args)
233
+ if args.action == "login":
234
+ _print(client.login())
235
+ elif args.action == "refresh":
236
+ _print(client.refresh_access_token())
237
+ elif args.action == "me":
238
+ _print(client.get_me())
239
+ return 0
240
+
241
+
242
+ def cmd_admin(args: argparse.Namespace) -> int:
243
+ """管理员接口。"""
244
+ client = _build_client(args)
245
+ if args.action == "users":
246
+ _print(client.admin_list_users(args.page, args.pageSize, args.keyword))
247
+ elif args.action == "settings":
248
+ _print(client.admin_get_settings())
249
+ elif args.action == "rate-limits":
250
+ _print(client.admin_get_rate_limits())
251
+ return 0
252
+
253
+
254
+ def cmd_config(args: argparse.Namespace) -> int:
255
+ """查看当前配置(脱敏)。"""
256
+ cfg = Config.from_env(args.env)
257
+ if args.action == "show":
258
+ _print(cfg.to_dict())
259
+ elif args.action == "init":
260
+ example = os.path.join(os.path.dirname(__file__), "..", "..", ".env.example")
261
+ example = os.path.abspath(example)
262
+ target = args.target or ".env"
263
+ if not os.path.exists(example):
264
+ print(f"未找到 .env.example 模板:{example}", file=sys.stderr)
265
+ return 1
266
+ shutil.copyfile(example, target)
267
+ print(f"已生成 {target},请填写真实配置后使用。")
268
+ return 0
269
+
270
+
271
+ def cmd_install_skill(args: argparse.Namespace) -> int:
272
+ """安装 AI 调用 Skill 到本地技能目录(固定目录名 magicpush-skill)。
273
+
274
+ 默认安装到当前工作目录下的 ./magicpush-skill;通过 --target 指定时安装到 <target>/magicpush-skill。
275
+ """
276
+ src = os.path.join(os.path.dirname(__file__), "skill", "SKILL.md")
277
+ if not os.path.exists(src):
278
+ print(f"未找到内置 SKILL.md:{src}", file=sys.stderr)
279
+ return 1
280
+
281
+ # 固定目录名 magicpush-skill;默认安装到当前项目根目录下,--target 指定父目录
282
+ base_dir = args.target if args.target else os.getcwd()
283
+ target_dir = os.path.join(base_dir, "magicpush-skill")
284
+
285
+ os.makedirs(target_dir, exist_ok=True)
286
+ dst = os.path.join(target_dir, "SKILL.md")
287
+ shutil.copyfile(src, dst)
288
+ print(f"AI Skill 安装成功:{dst}")
289
+ # 在该目录放置 .env 模板,便于全局 skill 安装时配置凭证
290
+ env_dst = os.path.join(target_dir, ".env.example")
291
+ with open(env_dst, "w", encoding="utf-8") as f:
292
+ f.write(_ENV_EXAMPLE_TEMPLATE)
293
+ # 若目录下尚无 .env,提示用户填写
294
+ if not os.path.exists(os.path.join(target_dir, ".env")):
295
+ print(f"请复制 {env_dst} 为 .env 并填写服务地址、推送令牌、登录账号后使用。")
296
+ print("重启 TRAE 会话或在技能面板刷新后即可使用名为 `magicpush` 的技能。")
297
+ return 0
298
+
299
+
300
+ def cmd_selfcheck(args: argparse.Namespace) -> int:
301
+ """本地自检(不发真实请求)。"""
302
+ from .client import demo
303
+ demo()
304
+ return 0
305
+
306
+
307
+ # ----------------------------------------------------------------------
308
+ # 解析器构建
309
+ # ----------------------------------------------------------------------
310
+ def build_parser() -> argparse.ArgumentParser:
311
+ """构建 CLI 解析器。"""
312
+ parser = argparse.ArgumentParser(
313
+ prog="pymagicpush",
314
+ description="MagicPush 消息推送服务 Python 客户端命令行工具。",
315
+ formatter_class=argparse.RawDescriptionHelpFormatter,
316
+ epilog=(
317
+ "示例:\n"
318
+ " pymagicpush push --content '你好' --title '测试'\n"
319
+ " pymagicpush channels types\n"
320
+ " pymagicpush endpoints list\n"
321
+ " pymagicpush logs stats\n"
322
+ " pymagicpush install-skill\n"
323
+ ),
324
+ )
325
+ parser.add_argument("-V", "--version", action="version", version=f"pymagicpush {__version__}")
326
+ sub = parser.add_subparsers(dest="command", metavar="<子命令>")
327
+
328
+ # health
329
+ p = sub.add_parser("health", help="健康检查")
330
+ _add_conn_args(p)
331
+ p.set_defaults(func=cmd_health)
332
+
333
+ # version
334
+ p = sub.add_parser("version", help="获取服务版本")
335
+ _add_conn_args(p)
336
+ p.set_defaults(func=cmd_version)
337
+
338
+ # push
339
+ p = sub.add_parser("push", help="通过推送令牌发送消息")
340
+ _add_conn_args(p)
341
+ p.add_argument("--content", help="消息正文(发送特有类型 --extra-data 时可省略)")
342
+ p.add_argument("--title", help="消息标题")
343
+ p.add_argument("--type", default="text", choices=["text", "markdown", "html"], help="消息类型(默认 text)")
344
+ p.add_argument("--url", help="跳转链接")
345
+ p.add_argument("--method", default="header", choices=["header", "url", "get"], help="推送方式(默认 header 最安全)")
346
+ p.add_argument("--extra-data", help="渠道特有消息类型参数(JSON 字符串),如飞书富文本、企业微信模板卡片、Telegram 图片")
347
+ p.add_argument("--channel-id", help="定向推送的渠道 ID(用于特有类型定向推送)")
348
+ p.add_argument("--channel-name", help="按渠道名称/类型关键字定向推送(需登录,如「小爱音响」「飞书」),优先于 --channel-id")
349
+ p.set_defaults(func=cmd_push)
350
+
351
+ # send(快捷)
352
+ p = sub.add_parser("send", help="快捷推送(使用配置中的 token)")
353
+ _add_conn_args(p)
354
+ p.add_argument("--content", help="消息正文(发送特有类型 --extra-data 时可省略)")
355
+ p.add_argument("--title", help="消息标题")
356
+ p.add_argument("--type", default="text", choices=["text", "markdown", "html"], help="消息类型")
357
+ p.add_argument("--url", help="跳转链接")
358
+ p.add_argument("--extra-data", help="渠道特有消息类型参数(JSON 字符串)")
359
+ p.add_argument("--channel-id", help="定向推送的渠道 ID(用于特有类型定向推送)")
360
+ p.add_argument("--channel-name", help="按渠道名称/类型关键字定向推送(需登录,如「小爱音响」「飞书」),优先于 --channel-id")
361
+ p.set_defaults(func=cmd_send)
362
+
363
+ # channels
364
+ p = sub.add_parser("channels", help="渠道管理")
365
+ _add_conn_args(p)
366
+ p.add_argument("action", choices=["list", "types", "get", "create", "delete", "test"], help="操作")
367
+ p.add_argument("--id", help="渠道 ID(get/delete/test)")
368
+ p.add_argument("--type", help="渠道类型标识(create)")
369
+ p.add_argument("--name", help="渠道名称(create)")
370
+ p.add_argument("--config", help="渠道配置 JSON 字符串(create)")
371
+ p.add_argument("--description", help="渠道描述(create)")
372
+ p.set_defaults(func=cmd_channels)
373
+
374
+ # endpoints
375
+ p = sub.add_parser("endpoints", help="接口管理")
376
+ _add_conn_args(p)
377
+ p.add_argument("action", choices=["list", "get", "create", "delete", "regenerate-token", "channels", "bind"], help="操作")
378
+ p.add_argument("--id", help="接口 ID")
379
+ p.add_argument("--page", type=int, help="页码")
380
+ p.add_argument("--pageSize", type=int, help="每页数量")
381
+ p.add_argument("--data", help="接口数据 JSON 字符串(create)")
382
+ p.add_argument("--channel-ids", help="渠道 ID 列表 JSON 字符串(bind)")
383
+ p.set_defaults(func=cmd_endpoints)
384
+
385
+ # logs
386
+ p = sub.add_parser("logs", help="推送记录")
387
+ _add_conn_args(p)
388
+ p.add_argument("action", choices=["list", "stats", "get", "clear"], help="操作")
389
+ p.add_argument("--id", help="记录 ID(get)")
390
+ p.add_argument("--page", type=int, help="页码")
391
+ p.add_argument("--pageSize", type=int, help="每页数量")
392
+ p.add_argument("--channelType", help="按渠道类型筛选")
393
+ p.add_argument("--status", help="按状态筛选(success/failed/skipped_dnd)")
394
+ p.add_argument("--keyword", help="关键词搜索")
395
+ p.add_argument("--startDate", help="开始日期(YYYY-MM-DD)")
396
+ p.add_argument("--endDate", help="结束日期(YYYY-MM-DD)")
397
+ p.set_defaults(func=cmd_logs)
398
+
399
+ # auth
400
+ p = sub.add_parser("auth", help="认证与当前用户")
401
+ _add_conn_args(p)
402
+ p.add_argument("action", choices=["login", "refresh", "me"], help="操作")
403
+ p.set_defaults(func=cmd_auth)
404
+
405
+ # admin
406
+ p = sub.add_parser("admin", help="管理员接口(需 Admin 角色)")
407
+ _add_conn_args(p)
408
+ p.add_argument("action", choices=["users", "settings", "rate-limits"], help="操作")
409
+ p.add_argument("--page", type=int, help="页码")
410
+ p.add_argument("--pageSize", type=int, help="每页数量")
411
+ p.add_argument("--keyword", help="搜索关键词")
412
+ p.set_defaults(func=cmd_admin)
413
+
414
+ # config
415
+ p = sub.add_parser("config", help="查看当前配置(脱敏)/ 生成 .env")
416
+ p.add_argument("action", choices=["show", "init"], help="操作")
417
+ p.add_argument("--env", help=".env 文件路径(show)")
418
+ p.add_argument("--target", help="生成目标路径(init,默认 .env)")
419
+ p.set_defaults(func=cmd_config)
420
+
421
+ # install-skill
422
+ p = sub.add_parser("install-skill", help="安装 AI 调用 Skill 到本地技能目录")
423
+ p.add_argument("--target", help="目标父目录(默认当前工作目录,skill 安装到 <target>/magicpush-skill)")
424
+ p.set_defaults(func=cmd_install_skill)
425
+
426
+ # selfcheck
427
+ p = sub.add_parser("selfcheck", help="本地自检(不发真实请求)")
428
+ p.set_defaults(func=cmd_selfcheck)
429
+
430
+ return parser
431
+
432
+
433
+ def main(argv: Optional[list] = None) -> int:
434
+ """CLI 入口。无子命令时直接显示帮助信息。"""
435
+ parser = build_parser()
436
+ args = parser.parse_args(argv)
437
+ # 无子命令:直接打印帮助并退出(exit 0)
438
+ if not getattr(args, "command", None):
439
+ parser.print_help()
440
+ return 0
441
+ try:
442
+ return args.func(args)
443
+ except MagicPushError as e:
444
+ print(f"[错误] {e}", file=sys.stderr)
445
+ return 1
446
+ except KeyboardInterrupt:
447
+ print("\n已中断", file=sys.stderr)
448
+ return 130
449
+
450
+
451
+ if __name__ == "__main__":
452
+ sys.exit(main())