md2wx-cli 0.2.0__py3-none-any.whl

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.
md2wx/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """md2wx — Markdown to WeChat HTML CLI tool."""
2
+
3
+ __version__ = "0.2.0"
md2wx/cli.py ADDED
@@ -0,0 +1,299 @@
1
+ """md2wx CLI 入口点 —— Markdown to WeChat HTML 排版与生产力套件."""
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import List, Optional
7
+
8
+ from . import __version__
9
+ from .clipboard import copy_html_to_clipboard
10
+ from .config import get_wechat_credentials
11
+ from .converter import WeChatConverter, make_paste_safe, preview_html
12
+ from .fetcher import fetch_and_convert_article
13
+ from .gallery import launch_gallery
14
+ from .publisher import publish_to_wechat
15
+ from .theme import Theme, list_themes, load_theme
16
+ from .theme_extractor import extract_from_dir, extract_from_file, extract_from_url, save_theme_yaml
17
+ from .validator import print_diagnostic_report, validate_wechat_article
18
+
19
+
20
+ def _load_effective_theme(theme_name: str, theme_file: Optional[str]) -> Theme:
21
+ if theme_file:
22
+ import yaml
23
+
24
+ path = Path(theme_file)
25
+ if not path.exists():
26
+ raise FileNotFoundError(f"未找到外部主题文件: {theme_file}")
27
+ data = yaml.safe_load(path.read_text(encoding="utf-8-sig"))
28
+ if not isinstance(data, dict):
29
+ raise ValueError(f"外部主题 YAML 格式无效 (必须为字典映射): {theme_file}")
30
+ theme = Theme(
31
+ name=data.get("name", path.stem),
32
+ description=data.get("description", ""),
33
+ base_css=data.get("base_css", ""),
34
+ colors=data.get("colors", {}) if isinstance(data.get("colors"), dict) else {},
35
+ _raw_data=data,
36
+ )
37
+ return theme
38
+ return load_theme(theme_name)
39
+
40
+
41
+ def cmd_convert(args: argparse.Namespace) -> int:
42
+ in_path = None
43
+ if args.input == "-" or (not args.input and not sys.stdin.isatty()):
44
+ input_text = sys.stdin.read()
45
+ input_filename = "document"
46
+ elif args.input:
47
+ in_path = Path(args.input)
48
+ if not in_path.exists():
49
+ print(f"错误: 输入文件未找到: {args.input}", file=sys.stderr)
50
+ return 1
51
+ input_text = in_path.read_text(encoding="utf-8-sig")
52
+ input_filename = in_path.stem
53
+ else:
54
+ print("错误: 请指定输入的 Markdown 文件或使用子命令 (运行 'md2wx --help' 查看帮助)", file=sys.stderr)
55
+ return 1
56
+
57
+ try:
58
+ theme = _load_effective_theme(args.theme, args.theme_file)
59
+ except Exception as e:
60
+ print(f"加载主题失败: {e}", file=sys.stderr)
61
+ return 1
62
+
63
+ converter = WeChatConverter(theme=theme)
64
+ base_dir = in_path.parent if in_path else None
65
+ result = converter.convert(input_text, base_dir=base_dir)
66
+ body_html = result.html
67
+
68
+ if not getattr(args, "no_paste_safe", False):
69
+ body_html = make_paste_safe(body_html)
70
+
71
+ final_output = preview_html(body_html, theme) if getattr(args, "full_page", False) else body_html
72
+
73
+ out_file = args.output
74
+ if out_file:
75
+ out_path = Path(out_file)
76
+ out_path.parent.mkdir(parents=True, exist_ok=True)
77
+ out_path.write_text(final_output, encoding="utf-8")
78
+ print(f"已生成文件: {out_path}", file=sys.stderr)
79
+
80
+ if getattr(args, "clipboard", False):
81
+ plain = input_text
82
+ ok = copy_html_to_clipboard(body_html, plain)
83
+ if ok:
84
+ print("已成功复制富文本 HTML 到系统剪贴板!可直接去微信公众号后台 Ctrl+V 粘贴。", file=sys.stderr)
85
+ else:
86
+ print("写入剪贴板失败,请使用 -o 保存为文件后手动复制。", file=sys.stderr)
87
+
88
+ if getattr(args, "publish", False):
89
+ creds = get_wechat_credentials(
90
+ cli_appid=getattr(args, "appid", None),
91
+ cli_secret=getattr(args, "secret", None),
92
+ cli_author=getattr(args, "author", None),
93
+ )
94
+ if not creds.appid or not creds.secret:
95
+ print(
96
+ "错误: 发布至微信草稿箱需要提供公众号凭据 (AppID 与 AppSecret)。\n"
97
+ "请通过命令行参数 --appid/--secret,或配置环境变量 WECHAT_APPID 与 WECHAT_SECRET,"
98
+ "或在 ~/.md2wx/config.yaml 中配置。",
99
+ file=sys.stderr,
100
+ )
101
+ return 1
102
+
103
+ title = getattr(args, "title", "") or result.title or input_filename
104
+ digest = getattr(args, "digest", "") or result.digest
105
+ cover = getattr(args, "cover", None)
106
+
107
+ print(f"正在发布到微信公众号草稿箱: 《{title}》...")
108
+ try:
109
+ media_id = publish_to_wechat(
110
+ html=body_html,
111
+ title=title,
112
+ digest=digest,
113
+ images=result.images,
114
+ appid=creds.appid,
115
+ secret=creds.secret,
116
+ cover=cover,
117
+ author=creds.author,
118
+ base_dir=base_dir,
119
+ )
120
+ print(f"[OK] 成功发布至微信公众号草稿箱!media_id: {media_id}")
121
+ print("可登录 https://mp.weixin.qq.com 后台在「草稿箱」中查看并群发。")
122
+ return 0
123
+ except Exception as e:
124
+ print(f"[ERROR] 发布失败: {e}", file=sys.stderr)
125
+ return 1
126
+
127
+ if not out_file and not getattr(args, "clipboard", False) and not getattr(args, "publish", False):
128
+ print(final_output)
129
+
130
+ return 0
131
+
132
+
133
+ def cmd_gallery(argv: List[str]) -> int:
134
+ parser = argparse.ArgumentParser(prog="md2wx gallery", description="生成 19 套主题的 Shadcn 交互式画廊并在浏览器中打开")
135
+ parser.add_argument("input", help="输入的 Markdown 文件路径")
136
+ parser.add_argument("-o", "--output", help="输出的画廊 HTML 路径(可选)")
137
+ args = parser.parse_args(argv)
138
+
139
+ p = Path(args.input)
140
+ if not p.exists():
141
+ print(f"错误: 输入文件未找到: {args.input}", file=sys.stderr)
142
+ return 1
143
+
144
+ out_p = Path(args.output) if args.output else None
145
+ target = launch_gallery(p, output_html=out_p)
146
+ print(f"[OK] 已成功生成主题画廊并已在默认浏览器中打开: {target}")
147
+ return 0
148
+
149
+
150
+ def cmd_learn_theme(argv: List[str]) -> int:
151
+ parser = argparse.ArgumentParser(
152
+ prog="md2wx learn-theme",
153
+ description="从微信在线文章或本地设计目录 (如 test_html2json) 逆向学习并生成新主题",
154
+ )
155
+ group = parser.add_mutually_exclusive_group(required=True)
156
+ group.add_argument("--url", help="微信公众号文章在线 URL")
157
+ group.add_argument("--dir", help="本地设计快照目录(包含 DESIGN.md 或 HTML 资源)")
158
+ group.add_argument("--file", help="单个本地 HTML 或 DESIGN.md 文件路径")
159
+ parser.add_argument("-n", "--name", required=True, help="要生成的主题名称(如 my-brand)")
160
+ parser.add_argument("-o", "--output", help="保存主题的目标文件路径(默认存入全局 ~/.md2wx/themes/)")
161
+ args = parser.parse_args(argv)
162
+
163
+ try:
164
+ if args.url:
165
+ yaml_str = extract_from_url(args.url, theme_name=args.name)
166
+ elif args.dir:
167
+ yaml_str = extract_from_dir(Path(args.dir), theme_name=args.name)
168
+ elif args.file:
169
+ yaml_str = extract_from_file(Path(args.file), theme_name=args.name)
170
+ else:
171
+ print("错误: 请指定 --url, --dir 或 --file", file=sys.stderr)
172
+ return 1
173
+
174
+ out_path = Path(args.output) if args.output else None
175
+ target_path = save_theme_yaml(yaml_str, args.name, target_path=out_path)
176
+ print(f"[OK] 成功提取并生成新主题: {args.name}")
177
+ print(f"[PATH] 已保存至: {target_path}")
178
+ print(f"[TIP] 立即使用: md2wx article.md -t {args.name} -c")
179
+ return 0
180
+ except Exception as e:
181
+ print(f"提取主题失败: {e}", file=sys.stderr)
182
+ return 1
183
+
184
+
185
+ def cmd_fetch(argv: List[str]) -> int:
186
+ parser = argparse.ArgumentParser(prog="md2wx fetch", description="抓取微信公众号文章并逆向提取为本地 Markdown与图片")
187
+ parser.add_argument("url", help="微信推文 URL")
188
+ parser.add_argument("-o", "--output", help="输出的 Markdown 文件路径(默认按标题生成)")
189
+ args = parser.parse_args(argv)
190
+
191
+ try:
192
+ out_p = Path(args.output) if args.output else None
193
+ target_md, _ = fetch_and_convert_article(args.url, output_file=out_p)
194
+ print(f"[OK] 微信推文已成功抓取并转为 Markdown: {target_md}")
195
+ return 0
196
+ except Exception as e:
197
+ print(f"抓取文章失败: {e}", file=sys.stderr)
198
+ return 1
199
+
200
+
201
+ def cmd_check(argv: List[str]) -> int:
202
+ parser = argparse.ArgumentParser(prog="md2wx check", description="对 Markdown 或 HTML 进行微信排版兼容性与合规体检")
203
+ parser.add_argument("file", help="要检查的 Markdown 或 HTML 文件路径")
204
+ parser.add_argument("-t", "--theme", default="default", help="排版主题名称 (默认: default)")
205
+ args = parser.parse_args(argv)
206
+
207
+ p = Path(args.file)
208
+ if not p.exists():
209
+ print(f"错误: 文件未找到: {args.file}", file=sys.stderr)
210
+ return 1
211
+
212
+ diagnostics = validate_wechat_article(p, theme_name=args.theme)
213
+ print_diagnostic_report(p, diagnostics)
214
+ return 1 if any(d.level == "ERROR" for d in diagnostics) else 0
215
+
216
+
217
+ def cmd_themes_list(argv: List[str]) -> int:
218
+ names = list_themes()
219
+ print(f"已安装的排版主题列表 (共 {len(names)} 个):")
220
+ for n in names:
221
+ try:
222
+ t = load_theme(n)
223
+ print(f" - {n:<20} {t.description}")
224
+ except Exception:
225
+ print(f" - {n:<20}")
226
+ return 0
227
+
228
+
229
+ def build_parser() -> argparse.ArgumentParser:
230
+ epilog_text = """常用功能与子命令:
231
+ md2wx article.md -c # 转换并一键复制富文本到剪贴板
232
+ md2wx article.md -t impeccable -p # 原生直传公众号草稿箱(自动换取微信 CDN 图床)
233
+ md2wx gallery article.md # 生成 19 套主题的 Shadcn 交互式对比画廊
234
+ md2wx learn-theme --dir test_html2json -n my # 从本地设计快照提取新主题
235
+ md2wx learn-theme --url <url> -n tech # 从微信在线推文提取新主题
236
+ md2wx fetch <url> -o my-article.md # 抓取微信推文转为 Markdown 与本地图片
237
+ md2wx check article.md # 对文章执行微信排版兼容性诊断体检
238
+ md2wx themes # 查看全部已安装主题
239
+ """
240
+ parser = argparse.ArgumentParser(
241
+ prog="md2wx",
242
+ description="Markdown to WeChat HTML CLI tool (支持画廊对比、主题提取、公众号草稿直传、推文抓取、排版体检)",
243
+ epilog=epilog_text,
244
+ formatter_class=argparse.RawDescriptionHelpFormatter,
245
+ )
246
+ parser.add_argument("-v", "-V", "--version", action="version", version=f"md2wx {__version__}", help="显示版本号并退出")
247
+ parser.add_argument("input", nargs="?", help="输入的 Markdown 文件路径(传入 - 或省略时支持管道标准输入)")
248
+ parser.add_argument("-o", "--output", help="输出的 HTML 文件路径")
249
+ parser.add_argument("-t", "--theme", default="default", help="排版主题名称 (默认: default)")
250
+ parser.add_argument("--theme-file", help="外部自定义 YAML 主题文件路径")
251
+ parser.add_argument("-c", "--clipboard", action="store_true", help="转换后直接将富文本 HTML 写入系统剪贴板")
252
+ parser.add_argument("--full-page", action="store_true", help="生成包含完整 <html> 结构的独立预览页面")
253
+ parser.add_argument("-p", "--publish", action="store_true", help="原生直传并发布至微信公众号草稿箱(需 AppID/Secret)")
254
+ parser.add_argument("--appid", help="微信公众号 AppID")
255
+ parser.add_argument("--secret", help="微信公众号 AppSecret")
256
+ parser.add_argument("--cover", help="图文封面图片路径")
257
+ parser.add_argument("--title", help="文章标题(覆盖 Markdown 一级标题)")
258
+ parser.add_argument("--author", help="文章作者名称")
259
+ parser.add_argument("--digest", help="文章摘要简介")
260
+ parser.add_argument("--no-paste-safe", action="store_true", help="关闭 span[leaf] 粘贴防护")
261
+ return parser
262
+
263
+
264
+ def main(argv: Optional[List[str]] = None) -> int:
265
+ if argv is None:
266
+ argv = sys.argv[1:]
267
+
268
+ if not argv:
269
+ parser = build_parser()
270
+ parser.print_help()
271
+ return 0
272
+
273
+ cmd = argv[0].lower()
274
+
275
+ if cmd in ("gallery", "preview-all"):
276
+ return cmd_gallery(argv[1:])
277
+ elif cmd in ("learn-theme", "extract-theme"):
278
+ return cmd_learn_theme(argv[1:])
279
+ elif cmd == "fetch":
280
+ return cmd_fetch(argv[1:])
281
+ elif cmd in ("check", "validate", "doctor"):
282
+ return cmd_check(argv[1:])
283
+ elif cmd in ("themes", "list-themes"):
284
+ return cmd_themes_list(argv[1:])
285
+ elif cmd in ("version", "--version", "-v", "-V"):
286
+ print(f"md2wx {__version__}")
287
+ return 0
288
+ elif cmd in ("help", "--help", "-h"):
289
+ parser = build_parser()
290
+ parser.print_help()
291
+ return 0
292
+
293
+ parser = build_parser()
294
+ args = parser.parse_args(argv)
295
+ return cmd_convert(args)
296
+
297
+
298
+ if __name__ == "__main__":
299
+ sys.exit(main())
md2wx/clipboard.py ADDED
@@ -0,0 +1,169 @@
1
+ """跨平台剪贴板处理模块 —— 支持富文本 HTML 与纯文本写入。"""
2
+
3
+ import os
4
+ import platform
5
+ import subprocess
6
+ import sys
7
+ from typing import Optional
8
+
9
+
10
+ def _format_windows_cf_html(html_fragment: str) -> bytes:
11
+ """构建 Windows 标准的 CF_HTML 数据包格式。"""
12
+ header_template = (
13
+ "Version:0.9\r\n"
14
+ "StartHTML:{start_html:08d}\r\n"
15
+ "EndHTML:{end_html:08d}\r\n"
16
+ "StartFragment:{start_fragment:08d}\r\n"
17
+ "EndFragment:{end_fragment:08d}\r\n"
18
+ )
19
+
20
+ prefix = (
21
+ '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\r\n'
22
+ '<HTML><HEAD><META http-equiv="content-type" content="text/html; charset=utf-8"></HEAD><BODY>\r\n'
23
+ "<!--StartFragment-->"
24
+ )
25
+ suffix = "<!--EndFragment--></BODY></HTML>"
26
+
27
+ dummy_header = header_template.format(start_html=0, end_html=0, start_fragment=0, end_fragment=0)
28
+ header_len = len(dummy_header.encode("utf-8"))
29
+ prefix_len = len(prefix.encode("utf-8"))
30
+ body_len = len(html_fragment.encode("utf-8"))
31
+ suffix_len = len(suffix.encode("utf-8"))
32
+
33
+ start_html = header_len
34
+ start_fragment = header_len + prefix_len
35
+ end_fragment = start_fragment + body_len
36
+ end_html = end_fragment + suffix_len
37
+
38
+ header = header_template.format(
39
+ start_html=start_html,
40
+ end_html=end_html,
41
+ start_fragment=start_fragment,
42
+ end_fragment=end_fragment,
43
+ )
44
+ return (header + prefix + html_fragment + suffix).encode("utf-8")
45
+
46
+
47
+ def _copy_windows(html_str: str, plain_text: str) -> bool:
48
+ """Windows 原生 Win32 API 写入 CF_HTML 与 CF_UNICODETEXT。"""
49
+ try:
50
+ import ctypes
51
+ from ctypes import wintypes
52
+
53
+ user32 = ctypes.windll.user32
54
+ kernel32 = ctypes.windll.kernel32
55
+
56
+ # 必须显式声明 64 位指针类型,避免 64 位内存地址被截断为 32 位 int
57
+ kernel32.GlobalAlloc.restype = wintypes.HGLOBAL
58
+ kernel32.GlobalAlloc.argtypes = [wintypes.UINT, ctypes.c_size_t]
59
+
60
+ kernel32.GlobalLock.restype = wintypes.LPVOID
61
+ kernel32.GlobalLock.argtypes = [wintypes.HGLOBAL]
62
+
63
+ kernel32.GlobalUnlock.restype = wintypes.BOOL
64
+ kernel32.GlobalUnlock.argtypes = [wintypes.HGLOBAL]
65
+
66
+ user32.RegisterClipboardFormatW.restype = wintypes.UINT
67
+ user32.RegisterClipboardFormatW.argtypes = [wintypes.LPCWSTR]
68
+
69
+ user32.OpenClipboard.restype = wintypes.BOOL
70
+ user32.OpenClipboard.argtypes = [wintypes.HWND]
71
+
72
+ user32.EmptyClipboard.restype = wintypes.BOOL
73
+ user32.EmptyClipboard.argtypes = []
74
+
75
+ user32.SetClipboardData.restype = wintypes.HANDLE
76
+ user32.SetClipboardData.argtypes = [wintypes.UINT, wintypes.HANDLE]
77
+
78
+ user32.CloseClipboard.restype = wintypes.BOOL
79
+ user32.CloseClipboard.argtypes = []
80
+
81
+ GMEM_MOVEABLE = 0x0002
82
+ CF_UNICODETEXT = 13
83
+
84
+ cf_html = user32.RegisterClipboardFormatW("HTML Format")
85
+ if not cf_html:
86
+ return False
87
+
88
+ if not user32.OpenClipboard(None):
89
+ return False
90
+
91
+ try:
92
+ user32.EmptyClipboard()
93
+
94
+ # 1. 写入 CF_HTML
95
+ cf_html_bytes = _format_windows_cf_html(html_str) + b"\x00"
96
+ h_html = kernel32.GlobalAlloc(GMEM_MOVEABLE, len(cf_html_bytes))
97
+ if h_html:
98
+ p_html = kernel32.GlobalLock(h_html)
99
+ if p_html:
100
+ ctypes.memmove(p_html, cf_html_bytes, len(cf_html_bytes))
101
+ kernel32.GlobalUnlock(h_html)
102
+ user32.SetClipboardData(cf_html, h_html)
103
+
104
+ # 2. 写入 CF_UNICODETEXT (备用纯文本)
105
+ text_utf16 = (plain_text + "\x00").encode("utf-16le")
106
+ h_text = kernel32.GlobalAlloc(GMEM_MOVEABLE, len(text_utf16))
107
+ if h_text:
108
+ p_text = kernel32.GlobalLock(h_text)
109
+ if p_text:
110
+ ctypes.memmove(p_text, text_utf16, len(text_utf16))
111
+ kernel32.GlobalUnlock(h_text)
112
+ user32.SetClipboardData(CF_UNICODETEXT, h_text)
113
+
114
+ return True
115
+ finally:
116
+ user32.CloseClipboard()
117
+ except Exception:
118
+ # PowerShell Set-Clipboard 作为纯文本后备方案
119
+ try:
120
+ subprocess.run(
121
+ ["powershell", "-NoProfile", "-Command", "Set-Clipboard", "-Value", plain_text],
122
+ check=True,
123
+ capture_output=True,
124
+ )
125
+ return True
126
+ except Exception:
127
+ return False
128
+
129
+
130
+ def _copy_macos(html_str: str, plain_text: str) -> bool:
131
+ try:
132
+ script = f'set the clipboard to "{html_str.replace("\"", "\\\"")}"'
133
+ proc = subprocess.run(["osascript", "-e", script], capture_output=True)
134
+ return proc.returncode == 0
135
+ except Exception:
136
+ try:
137
+ proc = subprocess.run(["pbcopy"], input=plain_text.encode("utf-8"), capture_output=True)
138
+ return proc.returncode == 0
139
+ except Exception:
140
+ return False
141
+
142
+
143
+ def _copy_linux(html_str: str, plain_text: str) -> bool:
144
+ try:
145
+ proc = subprocess.run(["xclip", "-selection", "clipboard", "-t", "text/html"], input=html_str.encode("utf-8"), capture_output=True)
146
+ if proc.returncode == 0:
147
+ return True
148
+ except Exception:
149
+ pass
150
+ try:
151
+ proc = subprocess.run(["wl-copy", "--type", "text/html"], input=html_str.encode("utf-8"), capture_output=True)
152
+ return proc.returncode == 0
153
+ except Exception:
154
+ return False
155
+
156
+
157
+ def copy_html_to_clipboard(html_str: str, plain_text: Optional[str] = None) -> bool:
158
+ """将 HTML 富文本写入系统剪贴板。"""
159
+ if plain_text is None:
160
+ import re
161
+ plain_text = re.sub(r"<[^>]+>", "", html_str).strip()
162
+
163
+ system = platform.system()
164
+ if system == "Windows":
165
+ return _copy_windows(html_str, plain_text)
166
+ elif system == "Darwin":
167
+ return _copy_macos(html_str, plain_text)
168
+ else:
169
+ return _copy_linux(html_str, plain_text)
md2wx/config.py ADDED
@@ -0,0 +1,83 @@
1
+ """md2wx 配置与微信凭据管理模块。"""
2
+
3
+ import os
4
+ import platform
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import yaml
10
+
11
+
12
+ @dataclass
13
+ class WeChatConfig:
14
+ appid: str = ""
15
+ secret: str = ""
16
+ author: str = ""
17
+
18
+
19
+ def _get_registry_env(key: str) -> str:
20
+ """在 Windows 下尝试从用户注册表 HKCU\\Environment 动态读取环境变量。"""
21
+ if platform.system() != "Windows":
22
+ return ""
23
+ try:
24
+ import winreg
25
+ with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Environment", 0, winreg.KEY_READ) as k:
26
+ val, _ = winreg.QueryValueEx(k, key)
27
+ return str(val).strip()
28
+ except Exception:
29
+ return ""
30
+
31
+
32
+ def get_config_dir() -> Path:
33
+ p = Path.home() / ".md2wx"
34
+ p.mkdir(parents=True, exist_ok=True)
35
+ return p
36
+
37
+
38
+ def load_config() -> dict:
39
+ cfg_file = get_config_dir() / "config.yaml"
40
+ if cfg_file.exists():
41
+ try:
42
+ data = yaml.safe_load(cfg_file.read_text(encoding="utf-8"))
43
+ return data if isinstance(data, dict) else {}
44
+ except Exception:
45
+ return {}
46
+ return {}
47
+
48
+
49
+ def get_wechat_credentials(
50
+ cli_appid: Optional[str] = None,
51
+ cli_secret: Optional[str] = None,
52
+ cli_author: Optional[str] = None,
53
+ ) -> WeChatConfig:
54
+ """按优先级解析微信公众号凭据:CLI 参数 > 环境变量/注册表 > ~/.md2wx/config.yaml。"""
55
+ cfg = load_config()
56
+ wechat_cfg = cfg.get("wechat", {}) if isinstance(cfg.get("wechat"), dict) else {}
57
+
58
+ appid = (
59
+ cli_appid
60
+ or os.getenv("WECHAT_APPID")
61
+ or os.getenv("WECHAT_APP_ID")
62
+ or _get_registry_env("WECHAT_APPID")
63
+ or _get_registry_env("WECHAT_APP_ID")
64
+ or wechat_cfg.get("appid", "")
65
+ )
66
+
67
+ secret = (
68
+ cli_secret
69
+ or os.getenv("WECHAT_SECRET")
70
+ or os.getenv("WECHAT_APP_SECRET")
71
+ or _get_registry_env("WECHAT_SECRET")
72
+ or _get_registry_env("WECHAT_APP_SECRET")
73
+ or wechat_cfg.get("secret", "")
74
+ )
75
+
76
+ author = (
77
+ cli_author
78
+ or os.getenv("WECHAT_AUTHOR")
79
+ or _get_registry_env("WECHAT_AUTHOR")
80
+ or wechat_cfg.get("author", "")
81
+ )
82
+
83
+ return WeChatConfig(appid=appid.strip(), secret=secret.strip(), author=author.strip())