ChatECNU 0.2.0__tar.gz → 0.2.2__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.
Files changed (28) hide show
  1. chatecnu-0.2.2/MANIFEST.in +1 -0
  2. chatecnu-0.2.2/PKG-INFO +113 -0
  3. chatecnu-0.2.2/README.md +77 -0
  4. {chatecnu-0.2.0 → chatecnu-0.2.2}/pyproject.toml +4 -1
  5. chatecnu-0.2.2/src/ChatECNU.egg-info/PKG-INFO +113 -0
  6. {chatecnu-0.2.0 → chatecnu-0.2.2}/src/ChatECNU.egg-info/SOURCES.txt +2 -0
  7. {chatecnu-0.2.0 → chatecnu-0.2.2}/src/ChatECNU.egg-info/requires.txt +1 -1
  8. {chatecnu-0.2.0 → chatecnu-0.2.2}/src/chatecnu/__init__.py +1 -1
  9. chatecnu-0.2.2/src/chatecnu/bin/linux-x86_64/auth_client +0 -0
  10. {chatecnu-0.2.0 → chatecnu-0.2.2}/src/chatecnu/ecnu/cli.py +135 -11
  11. {chatecnu-0.2.0 → chatecnu-0.2.2}/src/chatecnu/network_auth.py +133 -9
  12. chatecnu-0.2.2/tests/test_cli.py +114 -0
  13. {chatecnu-0.2.0 → chatecnu-0.2.2}/tests/test_network_auth.py +76 -0
  14. {chatecnu-0.2.0 → chatecnu-0.2.2}/tests/test_version.py +2 -2
  15. chatecnu-0.2.0/PKG-INFO +0 -92
  16. chatecnu-0.2.0/README.md +0 -56
  17. chatecnu-0.2.0/src/ChatECNU.egg-info/PKG-INFO +0 -92
  18. chatecnu-0.2.0/tests/test_cli.py +0 -59
  19. {chatecnu-0.2.0 → chatecnu-0.2.2}/LICENSE +0 -0
  20. {chatecnu-0.2.0 → chatecnu-0.2.2}/setup.cfg +0 -0
  21. {chatecnu-0.2.0 → chatecnu-0.2.2}/src/ChatECNU.egg-info/dependency_links.txt +0 -0
  22. {chatecnu-0.2.0 → chatecnu-0.2.2}/src/ChatECNU.egg-info/entry_points.txt +0 -0
  23. {chatecnu-0.2.0 → chatecnu-0.2.2}/src/ChatECNU.egg-info/top_level.txt +0 -0
  24. {chatecnu-0.2.0 → chatecnu-0.2.2}/src/chatecnu/cli.py +0 -0
  25. {chatecnu-0.2.0 → chatecnu-0.2.2}/src/chatecnu/config.py +0 -0
  26. {chatecnu-0.2.0 → chatecnu-0.2.2}/src/chatecnu/ecnu/__init__.py +0 -0
  27. {chatecnu-0.2.0 → chatecnu-0.2.2}/src/chatecnu/ecnu/captcha.py +0 -0
  28. {chatecnu-0.2.0 → chatecnu-0.2.2}/src/chatecnu/ecnu/portal.py +0 -0
@@ -0,0 +1 @@
1
+ include src/chatecnu/bin/linux-x86_64/auth_client
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: ChatECNU
3
+ Version: 0.2.2
4
+ Summary: ChatArch ECNU campus portal automation CLI
5
+ Author-email: ChatArch <1073853456@qq.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ChatArch/ChatECNU
8
+ Project-URL: Repository, https://github.com/ChatArch/ChatECNU
9
+ Project-URL: Documentation, https://arch.gh.wzhecnu.cn/ChatECNU/
10
+ Keywords: chatecnu,chatarch,ecnu,campus,portal
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: click>=8.0
17
+ Requires-Dist: chatstyle<0.2.0,>=0.1.0
18
+ Requires-Dist: chatenv<0.3.0,>=0.2.4
19
+ Requires-Dist: ChatNet<0.3.0,>=0.2.0
20
+ Requires-Dist: requests>=2.0
21
+ Requires-Dist: pycryptodome>=3.0
22
+ Provides-Extra: captcha
23
+ Requires-Dist: ddddocr; extra == "captcha"
24
+ Requires-Dist: numpy; extra == "captcha"
25
+ Requires-Dist: pillow; extra == "captcha"
26
+ Provides-Extra: dev
27
+ Requires-Dist: build; extra == "dev"
28
+ Requires-Dist: pytest; extra == "dev"
29
+ Requires-Dist: twine; extra == "dev"
30
+ Provides-Extra: docs
31
+ Requires-Dist: mkdocs<2.0,>=1.6; extra == "docs"
32
+ Requires-Dist: mkdocs-material<9.7,>=9.5; extra == "docs"
33
+ Requires-Dist: mkdocs-static-i18n<2.0,>=1.2; extra == "docs"
34
+ Requires-Dist: mike<3.0,>=2.0; extra == "docs"
35
+ Dynamic: license-file
36
+
37
+ # ChatECNU
38
+
39
+ ChatECNU 是 ECNU 门户与校园网联网工具包。安装包名仍是 `ChatECNU`,日常命令主推 `ecnu`。
40
+
41
+ ## 文档入口
42
+
43
+ | 任务 | 链接 |
44
+ | --- | --- |
45
+ | 安装和最小验证 | https://arch.gh.wzhecnu.cn/ChatECNU/quickstart/ |
46
+ | 命令树 | https://arch.gh.wzhecnu.cn/ChatECNU/cli-tree/ |
47
+ | ChatEnv 变量 | https://arch.gh.wzhecnu.cn/ChatECNU/chatenv/ |
48
+ | ECNU 使用 | https://arch.gh.wzhecnu.cn/ChatECNU/ecnu/ |
49
+
50
+ ## 安装
51
+
52
+ ```bash
53
+ pip install ChatECNU
54
+ ```
55
+
56
+ 如需验证码识别能力:
57
+
58
+ ```bash
59
+ pip install "ChatECNU[captcha]"
60
+ ```
61
+
62
+ ## 命令结构
63
+
64
+ ChatECNU 的命令面可用 `ecnu --tree` 从当前 Click 注册表直接回读:
65
+
66
+ ```text
67
+ ecnu
68
+ ├── --help # Show this message and exit.
69
+ ├── --version # Show the installed ChatECNU version.
70
+ ├── --tree # Print this registered command tree and exit.
71
+ ├── --env ENV-PROFILE # ChatEnv 配置名。
72
+ ├── home # ECNU 门户。
73
+ │ ├── info [--json] # 门户首页摘要。
74
+ │ ├── login [--username USERNAME] [--password PASSWORD] [--sms-code SMS-CODE] [--json] [--interactive] # 登录门户。
75
+ │ ├── logout [--json] # 退出门户会话。
76
+ │ ├── status [--json] # 门户会话状态。
77
+ │ └── user [--json] # 门户用户信息。
78
+ ├── net # 校园网联网。
79
+ │ ├── check [--auth-client AUTH-CLIENT-PATH] [--setting-file SETTING-FILE] [--json] # 检查在线状态。
80
+ │ ├── ensure-login [--auth-client AUTH-CLIENT-PATH] [--setting-file SETTING-FILE] [--username USERNAME] [--password PASSWORD] [--allow-argv-password] [--json] [--interactive] # 离线时登录。
81
+ │ ├── login [--auth-client AUTH-CLIENT-PATH] [--setting-file SETTING-FILE] [--username USERNAME] [--password PASSWORD] [--allow-argv-password] [--json] [--interactive] # 登录。
82
+ │ └── logout [--auth-client AUTH-CLIENT-PATH] [--setting-file SETTING-FILE] [--username USERNAME] [--json] [--interactive] # 退出校园网。
83
+ └── visitor # 访客账号。
84
+ ├── create [--remark REMARK] [--dry-run] [--json] [--interactive] # 创建访客账号。
85
+ ├── default [--password1 PASSWORD1] [--password2 PASSWORD2] [--remark REMARK] [--json] [--interactive] # 维护默认访客账号。
86
+ ├── delete [--id VISITOR-ID] [--dry-run] [--json] [--interactive] # 删除访客账号。
87
+ ├── get [--id VISITOR-ID] [--account ACCOUNT] [--json] # 查询访客账号。
88
+ ├── list [--json] # 列出访客账号。
89
+ └── update [--id VISITOR-ID] [--remark REMARK] [--password PASSWORD] [--dry-run] [--json] [--interactive] # 更新访客备注和密码。
90
+ ```
91
+
92
+ 门户命令统一放在 `home` 下:
93
+
94
+ ```bash
95
+ ecnu home login -i
96
+ ecnu home info
97
+ ecnu home status
98
+ ecnu home user
99
+ ecnu home logout
100
+ ```
101
+
102
+ 校园网联网统一放在 `net` 下:
103
+
104
+ ```bash
105
+ ecnu net check --json
106
+ ecnu net login -I --allow-argv-password
107
+ ecnu net logout -I
108
+ ecnu net ensure-login -I --allow-argv-password
109
+ ```
110
+
111
+ ChatECNU 的 PyPI wheel/sdist 内置 Linux x86_64 `auth_client`,默认 `ecnu net ...` 会优先使用该内置二进制;如需覆盖,可用 `--auth-client` 或 `ECNU_AUTH_CLIENT` 指定路径。账号密码使用同一组 `ECNU_USERNAME` / `ECNU_PASSWORD`。`net check` 会显示当前 `online/account/username`;`net logout` 未提供用户名时会先从 `auth_client check` 的 `Username=...` 自动提取。
112
+
113
+ 敏感值(密码、Cookie、短信码、会话)不要打印或提交。
@@ -0,0 +1,77 @@
1
+ # ChatECNU
2
+
3
+ ChatECNU 是 ECNU 门户与校园网联网工具包。安装包名仍是 `ChatECNU`,日常命令主推 `ecnu`。
4
+
5
+ ## 文档入口
6
+
7
+ | 任务 | 链接 |
8
+ | --- | --- |
9
+ | 安装和最小验证 | https://arch.gh.wzhecnu.cn/ChatECNU/quickstart/ |
10
+ | 命令树 | https://arch.gh.wzhecnu.cn/ChatECNU/cli-tree/ |
11
+ | ChatEnv 变量 | https://arch.gh.wzhecnu.cn/ChatECNU/chatenv/ |
12
+ | ECNU 使用 | https://arch.gh.wzhecnu.cn/ChatECNU/ecnu/ |
13
+
14
+ ## 安装
15
+
16
+ ```bash
17
+ pip install ChatECNU
18
+ ```
19
+
20
+ 如需验证码识别能力:
21
+
22
+ ```bash
23
+ pip install "ChatECNU[captcha]"
24
+ ```
25
+
26
+ ## 命令结构
27
+
28
+ ChatECNU 的命令面可用 `ecnu --tree` 从当前 Click 注册表直接回读:
29
+
30
+ ```text
31
+ ecnu
32
+ ├── --help # Show this message and exit.
33
+ ├── --version # Show the installed ChatECNU version.
34
+ ├── --tree # Print this registered command tree and exit.
35
+ ├── --env ENV-PROFILE # ChatEnv 配置名。
36
+ ├── home # ECNU 门户。
37
+ │ ├── info [--json] # 门户首页摘要。
38
+ │ ├── login [--username USERNAME] [--password PASSWORD] [--sms-code SMS-CODE] [--json] [--interactive] # 登录门户。
39
+ │ ├── logout [--json] # 退出门户会话。
40
+ │ ├── status [--json] # 门户会话状态。
41
+ │ └── user [--json] # 门户用户信息。
42
+ ├── net # 校园网联网。
43
+ │ ├── check [--auth-client AUTH-CLIENT-PATH] [--setting-file SETTING-FILE] [--json] # 检查在线状态。
44
+ │ ├── ensure-login [--auth-client AUTH-CLIENT-PATH] [--setting-file SETTING-FILE] [--username USERNAME] [--password PASSWORD] [--allow-argv-password] [--json] [--interactive] # 离线时登录。
45
+ │ ├── login [--auth-client AUTH-CLIENT-PATH] [--setting-file SETTING-FILE] [--username USERNAME] [--password PASSWORD] [--allow-argv-password] [--json] [--interactive] # 登录。
46
+ │ └── logout [--auth-client AUTH-CLIENT-PATH] [--setting-file SETTING-FILE] [--username USERNAME] [--json] [--interactive] # 退出校园网。
47
+ └── visitor # 访客账号。
48
+ ├── create [--remark REMARK] [--dry-run] [--json] [--interactive] # 创建访客账号。
49
+ ├── default [--password1 PASSWORD1] [--password2 PASSWORD2] [--remark REMARK] [--json] [--interactive] # 维护默认访客账号。
50
+ ├── delete [--id VISITOR-ID] [--dry-run] [--json] [--interactive] # 删除访客账号。
51
+ ├── get [--id VISITOR-ID] [--account ACCOUNT] [--json] # 查询访客账号。
52
+ ├── list [--json] # 列出访客账号。
53
+ └── update [--id VISITOR-ID] [--remark REMARK] [--password PASSWORD] [--dry-run] [--json] [--interactive] # 更新访客备注和密码。
54
+ ```
55
+
56
+ 门户命令统一放在 `home` 下:
57
+
58
+ ```bash
59
+ ecnu home login -i
60
+ ecnu home info
61
+ ecnu home status
62
+ ecnu home user
63
+ ecnu home logout
64
+ ```
65
+
66
+ 校园网联网统一放在 `net` 下:
67
+
68
+ ```bash
69
+ ecnu net check --json
70
+ ecnu net login -I --allow-argv-password
71
+ ecnu net logout -I
72
+ ecnu net ensure-login -I --allow-argv-password
73
+ ```
74
+
75
+ ChatECNU 的 PyPI wheel/sdist 内置 Linux x86_64 `auth_client`,默认 `ecnu net ...` 会优先使用该内置二进制;如需覆盖,可用 `--auth-client` 或 `ECNU_AUTH_CLIENT` 指定路径。账号密码使用同一组 `ECNU_USERNAME` / `ECNU_PASSWORD`。`net check` 会显示当前 `online/account/username`;`net logout` 未提供用户名时会先从 `auth_client check` 的 `Username=...` 自动提取。
76
+
77
+ 敏感值(密码、Cookie、短信码、会话)不要打印或提交。
@@ -12,7 +12,7 @@ license = "MIT"
12
12
  dependencies = [
13
13
  "click>=8.0",
14
14
  "chatstyle>=0.1.0,<0.2.0",
15
- "chatenv>=0.2.0,<0.3.0",
15
+ "chatenv>=0.2.4,<0.3.0",
16
16
  "ChatNet>=0.2.0,<0.3.0",
17
17
  "requests>=2.0",
18
18
  "pycryptodome>=3.0",
@@ -54,3 +54,6 @@ where = ["src"]
54
54
 
55
55
  [tool.setuptools]
56
56
  include-package-data = true
57
+
58
+ [tool.setuptools.package-data]
59
+ chatecnu = ["bin/linux-x86_64/auth_client"]
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: ChatECNU
3
+ Version: 0.2.2
4
+ Summary: ChatArch ECNU campus portal automation CLI
5
+ Author-email: ChatArch <1073853456@qq.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ChatArch/ChatECNU
8
+ Project-URL: Repository, https://github.com/ChatArch/ChatECNU
9
+ Project-URL: Documentation, https://arch.gh.wzhecnu.cn/ChatECNU/
10
+ Keywords: chatecnu,chatarch,ecnu,campus,portal
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: click>=8.0
17
+ Requires-Dist: chatstyle<0.2.0,>=0.1.0
18
+ Requires-Dist: chatenv<0.3.0,>=0.2.4
19
+ Requires-Dist: ChatNet<0.3.0,>=0.2.0
20
+ Requires-Dist: requests>=2.0
21
+ Requires-Dist: pycryptodome>=3.0
22
+ Provides-Extra: captcha
23
+ Requires-Dist: ddddocr; extra == "captcha"
24
+ Requires-Dist: numpy; extra == "captcha"
25
+ Requires-Dist: pillow; extra == "captcha"
26
+ Provides-Extra: dev
27
+ Requires-Dist: build; extra == "dev"
28
+ Requires-Dist: pytest; extra == "dev"
29
+ Requires-Dist: twine; extra == "dev"
30
+ Provides-Extra: docs
31
+ Requires-Dist: mkdocs<2.0,>=1.6; extra == "docs"
32
+ Requires-Dist: mkdocs-material<9.7,>=9.5; extra == "docs"
33
+ Requires-Dist: mkdocs-static-i18n<2.0,>=1.2; extra == "docs"
34
+ Requires-Dist: mike<3.0,>=2.0; extra == "docs"
35
+ Dynamic: license-file
36
+
37
+ # ChatECNU
38
+
39
+ ChatECNU 是 ECNU 门户与校园网联网工具包。安装包名仍是 `ChatECNU`,日常命令主推 `ecnu`。
40
+
41
+ ## 文档入口
42
+
43
+ | 任务 | 链接 |
44
+ | --- | --- |
45
+ | 安装和最小验证 | https://arch.gh.wzhecnu.cn/ChatECNU/quickstart/ |
46
+ | 命令树 | https://arch.gh.wzhecnu.cn/ChatECNU/cli-tree/ |
47
+ | ChatEnv 变量 | https://arch.gh.wzhecnu.cn/ChatECNU/chatenv/ |
48
+ | ECNU 使用 | https://arch.gh.wzhecnu.cn/ChatECNU/ecnu/ |
49
+
50
+ ## 安装
51
+
52
+ ```bash
53
+ pip install ChatECNU
54
+ ```
55
+
56
+ 如需验证码识别能力:
57
+
58
+ ```bash
59
+ pip install "ChatECNU[captcha]"
60
+ ```
61
+
62
+ ## 命令结构
63
+
64
+ ChatECNU 的命令面可用 `ecnu --tree` 从当前 Click 注册表直接回读:
65
+
66
+ ```text
67
+ ecnu
68
+ ├── --help # Show this message and exit.
69
+ ├── --version # Show the installed ChatECNU version.
70
+ ├── --tree # Print this registered command tree and exit.
71
+ ├── --env ENV-PROFILE # ChatEnv 配置名。
72
+ ├── home # ECNU 门户。
73
+ │ ├── info [--json] # 门户首页摘要。
74
+ │ ├── login [--username USERNAME] [--password PASSWORD] [--sms-code SMS-CODE] [--json] [--interactive] # 登录门户。
75
+ │ ├── logout [--json] # 退出门户会话。
76
+ │ ├── status [--json] # 门户会话状态。
77
+ │ └── user [--json] # 门户用户信息。
78
+ ├── net # 校园网联网。
79
+ │ ├── check [--auth-client AUTH-CLIENT-PATH] [--setting-file SETTING-FILE] [--json] # 检查在线状态。
80
+ │ ├── ensure-login [--auth-client AUTH-CLIENT-PATH] [--setting-file SETTING-FILE] [--username USERNAME] [--password PASSWORD] [--allow-argv-password] [--json] [--interactive] # 离线时登录。
81
+ │ ├── login [--auth-client AUTH-CLIENT-PATH] [--setting-file SETTING-FILE] [--username USERNAME] [--password PASSWORD] [--allow-argv-password] [--json] [--interactive] # 登录。
82
+ │ └── logout [--auth-client AUTH-CLIENT-PATH] [--setting-file SETTING-FILE] [--username USERNAME] [--json] [--interactive] # 退出校园网。
83
+ └── visitor # 访客账号。
84
+ ├── create [--remark REMARK] [--dry-run] [--json] [--interactive] # 创建访客账号。
85
+ ├── default [--password1 PASSWORD1] [--password2 PASSWORD2] [--remark REMARK] [--json] [--interactive] # 维护默认访客账号。
86
+ ├── delete [--id VISITOR-ID] [--dry-run] [--json] [--interactive] # 删除访客账号。
87
+ ├── get [--id VISITOR-ID] [--account ACCOUNT] [--json] # 查询访客账号。
88
+ ├── list [--json] # 列出访客账号。
89
+ └── update [--id VISITOR-ID] [--remark REMARK] [--password PASSWORD] [--dry-run] [--json] [--interactive] # 更新访客备注和密码。
90
+ ```
91
+
92
+ 门户命令统一放在 `home` 下:
93
+
94
+ ```bash
95
+ ecnu home login -i
96
+ ecnu home info
97
+ ecnu home status
98
+ ecnu home user
99
+ ecnu home logout
100
+ ```
101
+
102
+ 校园网联网统一放在 `net` 下:
103
+
104
+ ```bash
105
+ ecnu net check --json
106
+ ecnu net login -I --allow-argv-password
107
+ ecnu net logout -I
108
+ ecnu net ensure-login -I --allow-argv-password
109
+ ```
110
+
111
+ ChatECNU 的 PyPI wheel/sdist 内置 Linux x86_64 `auth_client`,默认 `ecnu net ...` 会优先使用该内置二进制;如需覆盖,可用 `--auth-client` 或 `ECNU_AUTH_CLIENT` 指定路径。账号密码使用同一组 `ECNU_USERNAME` / `ECNU_PASSWORD`。`net check` 会显示当前 `online/account/username`;`net logout` 未提供用户名时会先从 `auth_client check` 的 `Username=...` 自动提取。
112
+
113
+ 敏感值(密码、Cookie、短信码、会话)不要打印或提交。
@@ -1,4 +1,5 @@
1
1
  LICENSE
2
+ MANIFEST.in
2
3
  README.md
3
4
  pyproject.toml
4
5
  src/ChatECNU.egg-info/PKG-INFO
@@ -11,6 +12,7 @@ src/chatecnu/__init__.py
11
12
  src/chatecnu/cli.py
12
13
  src/chatecnu/config.py
13
14
  src/chatecnu/network_auth.py
15
+ src/chatecnu/bin/linux-x86_64/auth_client
14
16
  src/chatecnu/ecnu/__init__.py
15
17
  src/chatecnu/ecnu/captcha.py
16
18
  src/chatecnu/ecnu/cli.py
@@ -1,6 +1,6 @@
1
1
  click>=8.0
2
2
  chatstyle<0.2.0,>=0.1.0
3
- chatenv<0.3.0,>=0.2.0
3
+ chatenv<0.3.0,>=0.2.4
4
4
  ChatNet<0.3.0,>=0.2.0
5
5
  requests>=2.0
6
6
  pycryptodome>=3.0
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.0"
5
+ __version__ = "0.2.2"
@@ -21,6 +21,7 @@ from chatecnu.network_auth import (
21
21
  NetworkAuthCredentials,
22
22
  NetworkAuthResult,
23
23
  redact_text,
24
+ resolve_auth_client_path,
24
25
  )
25
26
  from .portal import BASE_URL
26
27
 
@@ -80,8 +81,115 @@ VISITOR_DEFAULT_SCHEMA = CommandSchema(
80
81
  )
81
82
 
82
83
 
84
+ def _format_metavar(name: str) -> str:
85
+ return name.replace("_", "-").upper()
86
+
87
+
88
+ def _format_argument(param: click.Argument) -> str:
89
+ metavar = _format_metavar(param.name or "ARG")
90
+ if param.nargs == -1:
91
+ metavar = f"{metavar}..."
92
+ if not param.required:
93
+ return f"[{metavar}]"
94
+ return f"<{metavar}>"
95
+
96
+
97
+ def _format_option(param: click.Option) -> str:
98
+ visible_opts = [opt for opt in param.opts if opt.startswith("--")]
99
+ visible_opts.extend(opt for opt in param.secondary_opts if opt.startswith("--"))
100
+ flag = visible_opts[0] if visible_opts else (param.opts[0] if param.opts else param.name or "OPTION")
101
+ if param.is_flag:
102
+ return f"[{flag}]"
103
+ return f"[{flag} {_format_metavar(param.name or 'VALUE')}]"
104
+
105
+
106
+ def _command_signature(command: click.Command) -> str:
107
+ parts: list[str] = []
108
+ for param in command.params:
109
+ if getattr(param, "hidden", False):
110
+ continue
111
+ if isinstance(param, click.Argument):
112
+ parts.append(_format_argument(param))
113
+ elif isinstance(param, click.Option):
114
+ parts.append(_format_option(param))
115
+ return " ".join(parts)
116
+
117
+
118
+ def _command_summary(command: click.Command) -> str:
119
+ return (command.short_help or command.help or "").strip().splitlines()[0]
120
+
121
+
122
+ def _root_option_items(group: click.Group) -> list[tuple[str, str]]:
123
+ items = [
124
+ ("--help", "Show this message and exit."),
125
+ ("--version", "Show the installed ChatECNU version."),
126
+ ("--tree", "Print this registered command tree and exit."),
127
+ ]
128
+ for param in group.params:
129
+ if getattr(param, "hidden", False) or not isinstance(param, click.Option):
130
+ continue
131
+ if param.name in {"version", "tree"}:
132
+ continue
133
+ items.append((_format_option(param).strip("[]"), param.help or ""))
134
+ return items
135
+
136
+
137
+ def _group_items(group: click.Group) -> list[tuple[str, str | click.Command]]:
138
+ items: list[tuple[str, str | click.Command]] = []
139
+ if group is cli:
140
+ items.extend(_root_option_items(group))
141
+ for name in group.list_commands(click.Context(group)):
142
+ command = group.get_command(click.Context(group), name)
143
+ if command is None or command.hidden:
144
+ continue
145
+ items.append((name, command))
146
+ return items
147
+
148
+
149
+ def render_cli_tree(root: click.Group | None = None) -> str:
150
+ """Render the registered Click command tree for `ecnu --tree`."""
151
+ if root is None:
152
+ root = cli
153
+ lines = [root.name or "ecnu"]
154
+
155
+ def walk(items: list[tuple[str, str | click.Command]], prefix: str = "") -> None:
156
+ for index, (name, item) in enumerate(items):
157
+ last = index == len(items) - 1
158
+ branch = "└── " if last else "├── "
159
+ child_prefix = prefix + (" " if last else "│ ")
160
+ if isinstance(item, str):
161
+ summary = f" # {item}" if item else ""
162
+ lines.append(f"{prefix}{branch}{name}{summary}")
163
+ continue
164
+ signature = _command_signature(item)
165
+ label = f"{name} {signature}".strip()
166
+ lines.append(f"{prefix}{branch}{label} # {_command_summary(item)}")
167
+ if isinstance(item, click.Group):
168
+ walk(_group_items(item), child_prefix)
169
+
170
+ walk(_group_items(root))
171
+ return "\n".join(lines)
172
+
173
+
174
+ def _tree_callback(ctx: click.Context, param: click.Option, value: bool) -> None:
175
+ if not value or ctx.resilient_parsing:
176
+ return
177
+ if not isinstance(ctx.command, click.Group):
178
+ raise click.ClickException("--tree is only available on command groups")
179
+ click.echo(render_cli_tree(ctx.command))
180
+ ctx.exit()
181
+
182
+
83
183
  @click.group(name="ecnu")
84
184
  @click.version_option(__version__)
185
+ @click.option(
186
+ "--tree",
187
+ is_flag=True,
188
+ is_eager=True,
189
+ expose_value=False,
190
+ callback=_tree_callback,
191
+ help="Print the registered CLI command tree and exit.",
192
+ )
85
193
  @click.option(
86
194
  "--base-url",
87
195
  default=None,
@@ -436,16 +544,17 @@ def network_auth_logout(
436
544
  """退出校园网。"""
437
545
 
438
546
  prefer_loaded_chatenv = network_auth_prefers_loaded_chatenv(ctx)
439
- resolved_username = resolve_network_auth_username(
440
- username=username,
441
- interactive=interactive,
547
+ resolved_username = username or resolve_ecnu_config_value(
548
+ "ECNU_USERNAME",
549
+ ECNUConfig.ECNU_USERNAME,
442
550
  prefer_loaded_chatenv=prefer_loaded_chatenv,
443
551
  )
444
- result = make_network_auth_client(
552
+ client = make_network_auth_client(
445
553
  auth_client_path=auth_client_path,
446
554
  setting_file=setting_file,
447
555
  prefer_loaded_chatenv=prefer_loaded_chatenv,
448
- ).logout(resolved_username)
556
+ )
557
+ result = client.logout(resolved_username) if resolved_username else client.logout_current()
449
558
  emit_network_auth_result(result, json_output=json_output)
450
559
 
451
560
 
@@ -688,11 +797,12 @@ def make_network_auth_client(
688
797
  allow_argv_password: bool = False,
689
798
  prefer_loaded_chatenv: bool = False,
690
799
  ) -> NetworkAuthClient:
691
- resolved_auth_client = auth_client_path or resolve_ecnu_config_value(
800
+ configured_auth_client = auth_client_path or resolve_ecnu_config_value(
692
801
  "ECNU_AUTH_CLIENT",
693
802
  ECNUConfig.ECNU_AUTH_CLIENT,
694
803
  prefer_loaded_chatenv=prefer_loaded_chatenv,
695
- ) or "auth_client"
804
+ )
805
+ resolved_auth_client = resolve_auth_client_path(configured_auth_client)
696
806
  resolved_setting_file = setting_file or resolve_ecnu_config_value(
697
807
  "ECNU_AUTH_SETTING_FILE",
698
808
  ECNUConfig.ECNU_AUTH_SETTING_FILE,
@@ -795,10 +905,24 @@ def redact_network_auth_payload(payload: dict[str, object], *, secret_values: tu
795
905
 
796
906
  def format_network_auth_result(result: NetworkAuthResult) -> str:
797
907
  if result.action == "ensure-login" and result.skipped:
798
- return "Already online; skipped."
799
- if result.success:
800
- return "Login OK." if result.action in {"login", "ensure-login"} else "auth_client OK."
801
- return "auth_client failed."
908
+ base = "Already online; skipped."
909
+ elif result.action == "logout" and result.skipped:
910
+ base = "Already offline; skipped."
911
+ elif result.success:
912
+ base = "Login OK." if result.action in {"login", "ensure-login"} else "auth_client OK."
913
+ else:
914
+ return "auth_client failed."
915
+
916
+ details: list[str] = []
917
+ if result.online is not None:
918
+ details.append(f"Online: {str(result.online).lower()}")
919
+ if result.account:
920
+ details.append(f"Account: {result.account}")
921
+ if result.username and result.username != result.account:
922
+ details.append(f"Username: {result.username}")
923
+ elif result.username and result.action == "check":
924
+ details.append(f"Username: {result.username}")
925
+ return " ".join([base, *details]) if details else base
802
926
 
803
927
 
804
928
  def make_client(ctx: click.Context) -> Any: