rednote-cli 0.1.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.
Files changed (81) hide show
  1. rednote_cli/__init__.py +5 -0
  2. rednote_cli/_runtime/__init__.py +0 -0
  3. rednote_cli/_runtime/common/__init__.py +0 -0
  4. rednote_cli/_runtime/common/app_utils.py +77 -0
  5. rednote_cli/_runtime/common/config.py +83 -0
  6. rednote_cli/_runtime/common/enums.py +17 -0
  7. rednote_cli/_runtime/common/errors.py +22 -0
  8. rednote_cli/_runtime/core/__init__.py +0 -0
  9. rednote_cli/_runtime/core/account_manager.py +349 -0
  10. rednote_cli/_runtime/core/browser/__init__.py +0 -0
  11. rednote_cli/_runtime/core/browser/manager.py +247 -0
  12. rednote_cli/_runtime/core/database/__init__.py +0 -0
  13. rednote_cli/_runtime/core/database/manager.py +334 -0
  14. rednote_cli/_runtime/platforms/__init__.py +0 -0
  15. rednote_cli/_runtime/platforms/base.py +62 -0
  16. rednote_cli/_runtime/platforms/factory.py +55 -0
  17. rednote_cli/_runtime/platforms/publishing/__init__.py +12 -0
  18. rednote_cli/_runtime/platforms/publishing/media.py +275 -0
  19. rednote_cli/_runtime/platforms/publishing/models.py +59 -0
  20. rednote_cli/_runtime/platforms/publishing/validator.py +124 -0
  21. rednote_cli/_runtime/services/__init__.py +1 -0
  22. rednote_cli/_runtime/services/scraper_service.py +235 -0
  23. rednote_cli/adapters/__init__.py +1 -0
  24. rednote_cli/adapters/output/__init__.py +1 -0
  25. rednote_cli/adapters/output/event_stream.py +29 -0
  26. rednote_cli/adapters/output/formatter_json.py +23 -0
  27. rednote_cli/adapters/output/formatter_table.py +39 -0
  28. rednote_cli/adapters/output/writer.py +17 -0
  29. rednote_cli/adapters/persistence/__init__.py +1 -0
  30. rednote_cli/adapters/persistence/file_account_repo.py +51 -0
  31. rednote_cli/adapters/platform/__init__.py +1 -0
  32. rednote_cli/adapters/platform/rednote/__init__.py +1 -0
  33. rednote_cli/adapters/platform/rednote/extractor.py +65 -0
  34. rednote_cli/adapters/platform/rednote/publisher.py +26 -0
  35. rednote_cli/adapters/platform/rednote/runtime_extractor.py +818 -0
  36. rednote_cli/adapters/platform/rednote/runtime_publisher.py +373 -0
  37. rednote_cli/adapters/platform/rednote/runtime_registration.py +20 -0
  38. rednote_cli/application/__init__.py +1 -0
  39. rednote_cli/application/dto/__init__.py +1 -0
  40. rednote_cli/application/dto/input_models.py +121 -0
  41. rednote_cli/application/dto/output_models.py +78 -0
  42. rednote_cli/application/use_cases/__init__.py +1 -0
  43. rednote_cli/application/use_cases/account_list.py +9 -0
  44. rednote_cli/application/use_cases/account_mutation.py +22 -0
  45. rednote_cli/application/use_cases/auth_login.py +64 -0
  46. rednote_cli/application/use_cases/auth_status.py +96 -0
  47. rednote_cli/application/use_cases/doctor.py +49 -0
  48. rednote_cli/application/use_cases/init_runtime.py +20 -0
  49. rednote_cli/application/use_cases/note_get.py +22 -0
  50. rednote_cli/application/use_cases/note_search.py +26 -0
  51. rednote_cli/application/use_cases/publish_note.py +25 -0
  52. rednote_cli/application/use_cases/user_get.py +18 -0
  53. rednote_cli/application/use_cases/user_search.py +8 -0
  54. rednote_cli/application/use_cases/user_self.py +8 -0
  55. rednote_cli/cli/__init__.py +1 -0
  56. rednote_cli/cli/__main__.py +5 -0
  57. rednote_cli/cli/commands/__init__.py +1 -0
  58. rednote_cli/cli/commands/account.py +204 -0
  59. rednote_cli/cli/commands/doctor.py +20 -0
  60. rednote_cli/cli/commands/init.py +20 -0
  61. rednote_cli/cli/commands/note.py +101 -0
  62. rednote_cli/cli/commands/publish.py +147 -0
  63. rednote_cli/cli/commands/search.py +185 -0
  64. rednote_cli/cli/commands/user.py +113 -0
  65. rednote_cli/cli/main.py +163 -0
  66. rednote_cli/cli/options.py +13 -0
  67. rednote_cli/cli/runtime.py +142 -0
  68. rednote_cli/cli/utils.py +74 -0
  69. rednote_cli/domain/__init__.py +1 -0
  70. rednote_cli/domain/errors.py +50 -0
  71. rednote_cli/domain/note_search_filters.py +155 -0
  72. rednote_cli/infra/__init__.py +1 -0
  73. rednote_cli/infra/exit_codes.py +30 -0
  74. rednote_cli/infra/logger.py +11 -0
  75. rednote_cli/infra/paths.py +31 -0
  76. rednote_cli/infra/platforms.py +4 -0
  77. rednote_cli-0.1.0.dist-info/METADATA +81 -0
  78. rednote_cli-0.1.0.dist-info/RECORD +81 -0
  79. rednote_cli-0.1.0.dist-info/WHEEL +5 -0
  80. rednote_cli-0.1.0.dist-info/entry_points.txt +2 -0
  81. rednote_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable
4
+
5
+ from rednote_cli._runtime.common.enums import Platform
6
+ from rednote_cli._runtime.core.account_manager import AccountManager
7
+ from rednote_cli.domain.errors import CliTimeoutError, InternalError, InvalidArgsError
8
+ from rednote_cli.infra.platforms import REDNOTE_PLATFORM
9
+
10
+
11
+ async def execute_auth_login(
12
+ *,
13
+ mode: str = "local",
14
+ login_timeout_seconds: int = 240,
15
+ screenshot_path: str = "",
16
+ account_uid: str = "",
17
+ progress_callback: Callable[[dict], None] | None = None,
18
+ ) -> dict:
19
+ normalized_mode = mode.strip().lower()
20
+ if normalized_mode not in {"local", "remote"}:
21
+ raise InvalidArgsError("`--mode` 仅支持 local 或 remote")
22
+
23
+ platform_enum = Platform(REDNOTE_PLATFORM)
24
+ result = await AccountManager.login_and_add_account(
25
+ platform_enum,
26
+ mode=normalized_mode,
27
+ timeout_seconds=login_timeout_seconds,
28
+ screenshot_path=screenshot_path,
29
+ account_uid=account_uid,
30
+ progress_callback=progress_callback,
31
+ )
32
+ if result.get("success"):
33
+ return {"platform": platform_enum.value, **result}
34
+
35
+ reason = result.get("reason")
36
+ message = result.get("message") or "登录失败"
37
+ if reason == "login_timeout":
38
+ raise CliTimeoutError(
39
+ message=message,
40
+ details={
41
+ "mode": normalized_mode,
42
+ "account_uid": account_uid or None,
43
+ "timeout_seconds": login_timeout_seconds,
44
+ "screenshot_path": result.get("screenshot_path"),
45
+ },
46
+ )
47
+ if reason in {"account_not_found", "account_mismatch"}:
48
+ raise InvalidArgsError(
49
+ message=message,
50
+ details={
51
+ "mode": normalized_mode,
52
+ "account_uid": account_uid or None,
53
+ "reason": reason,
54
+ },
55
+ )
56
+ raise InternalError(
57
+ message=message,
58
+ details={
59
+ "mode": normalized_mode,
60
+ "account_uid": account_uid or None,
61
+ "reason": reason,
62
+ "screenshot_path": result.get("screenshot_path"),
63
+ },
64
+ )
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+
5
+ from loguru import logger
6
+
7
+ from rednote_cli._runtime.common.enums import Platform
8
+ from rednote_cli._runtime.core.account_manager import AccountManager
9
+ from rednote_cli._runtime.core.browser.manager import browser_manager, stealth_async
10
+ from rednote_cli._runtime.core.database.manager import DatabaseManager
11
+ from rednote_cli._runtime.platforms.factory import PlatformFactory
12
+ from rednote_cli.application.use_cases.account_list import execute_account_list
13
+ from rednote_cli.domain.errors import InvalidArgsError
14
+ from rednote_cli.infra.platforms import REDNOTE_PLATFORM
15
+
16
+
17
+ async def _refresh_account_status(account: dict, *, platform_enum: Platform) -> None:
18
+ user_no = str(account.get("user_no") or "").strip()
19
+ if not user_no:
20
+ return
21
+
22
+ is_logged_in = False
23
+ nickname = account.get("nickname")
24
+ refreshed_state = account.get("storage_state")
25
+ check_time = int(time.time())
26
+ profile = AccountManager.get_login_profile(platform_enum)
27
+
28
+ try:
29
+ async with browser_manager.get_context(
30
+ storage_state=account.get("storage_state"),
31
+ account_key=user_no,
32
+ ) as context:
33
+ page = await context.new_page()
34
+ await stealth_async(page)
35
+ extractor = PlatformFactory.get_extractor(platform_enum, page)
36
+
37
+ await page.goto(profile.login_url, wait_until="domcontentloaded")
38
+ await page.wait_for_timeout(500)
39
+ has_logged_in_selector = await page.locator(profile.login_selector).count() > 0
40
+ is_logged_in = has_logged_in_selector
41
+
42
+ if has_logged_in_selector:
43
+ try:
44
+ info = await extractor.get_self_info()
45
+ if isinstance(info, dict):
46
+ info_nickname = str(info.get(profile.nickname_field) or "").strip()
47
+ if info_nickname:
48
+ nickname = info_nickname
49
+ self_uid = str(info.get(profile.account_id_field) or "").strip()
50
+ if self_uid and self_uid != user_no:
51
+ # Cookie 已漂移到其他账号,当前账号视为失效。
52
+ is_logged_in = False
53
+ elif profile.guest_field in info:
54
+ is_logged_in = not bool(info.get(profile.guest_field))
55
+ except Exception as exc:
56
+ logger.warning(f"Self info check failed for account {user_no}: {exc}")
57
+
58
+ refreshed_state = await context.storage_state()
59
+ except Exception as exc:
60
+ logger.warning(f"Account status probe failed for account {user_no}: {exc}")
61
+
62
+ DatabaseManager.upsert_account(
63
+ {
64
+ "platform": account.get("platform"),
65
+ "user_no": user_no,
66
+ "nickname": nickname,
67
+ "is_logged_in": bool(is_logged_in),
68
+ "check_time": check_time,
69
+ "usage_time": account.get("usage_time"),
70
+ "storage_state": refreshed_state,
71
+ }
72
+ )
73
+
74
+
75
+ def _load_platform_accounts() -> list[dict]:
76
+ accounts = DatabaseManager.get_all_accounts(only_active=False)
77
+ return [acc for acc in accounts if acc.get("platform") == REDNOTE_PLATFORM]
78
+
79
+
80
+ async def execute_auth_status(account_uid: str | None = None) -> list[dict]:
81
+ platform_enum = Platform(REDNOTE_PLATFORM)
82
+ platform_accounts = _load_platform_accounts()
83
+
84
+ if account_uid:
85
+ target = str(account_uid).strip()
86
+ account = next((acc for acc in platform_accounts if str(acc.get("user_no") or "").strip() == target), None)
87
+ if not account:
88
+ raise InvalidArgsError(f"账号不存在: {target}")
89
+ accounts_to_probe = [account]
90
+ else:
91
+ accounts_to_probe = [acc for acc in platform_accounts if bool(acc.get("is_logged_in"))]
92
+
93
+ for account in accounts_to_probe:
94
+ await _refresh_account_status(account, platform_enum=platform_enum)
95
+
96
+ return execute_account_list(only_active=False)
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ import platform
5
+ import shutil
6
+
7
+ from rednote_cli._runtime.common.app_utils import get_system_chrome_path
8
+ from rednote_cli._runtime.core.database.manager import init_db
9
+ from rednote_cli.infra.paths import APP_HOME, LOG_DIR, OUTPUT_DIR, configure_legacy_data_paths, ensure_runtime_dirs
10
+
11
+
12
+
13
+ def execute_doctor() -> dict:
14
+ ensure_runtime_dirs()
15
+ configure_legacy_data_paths()
16
+ init_db()
17
+ checks = {
18
+ "python": {
19
+ "ok": True,
20
+ "version": platform.python_version(),
21
+ },
22
+ "playwright": {
23
+ "ok": importlib.util.find_spec("playwright") is not None,
24
+ "installed": importlib.util.find_spec("playwright") is not None,
25
+ },
26
+ "chrome": {
27
+ "ok": bool(get_system_chrome_path()),
28
+ "path": get_system_chrome_path() or "",
29
+ },
30
+ "uv": {
31
+ "ok": shutil.which("uv") is not None,
32
+ "path": shutil.which("uv") or "",
33
+ },
34
+ "runtime_dirs": {
35
+ "ok": True,
36
+ "app_home": str(APP_HOME),
37
+ "log_dir": str(LOG_DIR),
38
+ "output_dir": str(OUTPUT_DIR),
39
+ },
40
+ "storage": {
41
+ "ok": True,
42
+ "mode": "file_primary",
43
+ "accounts_dir": str(APP_HOME / "accounts"),
44
+ "settings_file": str(APP_HOME / "settings.json"),
45
+ "task_schedules_file": str(APP_HOME / "task_schedules.json"),
46
+ },
47
+ }
48
+ checks["overall_ok"] = all(item.get("ok", False) for item in checks.values() if isinstance(item, dict))
49
+ return checks
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from rednote_cli._runtime.core.database.manager import init_db
4
+ from rednote_cli.infra.paths import APP_HOME, LOG_DIR, OUTPUT_DIR, configure_legacy_data_paths, ensure_runtime_dirs
5
+
6
+
7
+ def execute_runtime_init() -> dict:
8
+ ensure_runtime_dirs()
9
+ configure_legacy_data_paths()
10
+ init_db()
11
+ return {
12
+ "app_home": str(APP_HOME),
13
+ "log_dir": str(LOG_DIR),
14
+ "output_dir": str(OUTPUT_DIR),
15
+ "accounts_dir": str(APP_HOME / "accounts"),
16
+ "settings_file": str(APP_HOME / "settings.json"),
17
+ "task_schedules_file": str(APP_HOME / "task_schedules.json"),
18
+ "storage_mode": "file_primary",
19
+ "store_initialized": True,
20
+ }
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from rednote_cli.adapters.platform.rednote.extractor import RednoteExtractorAdapter
4
+
5
+
6
+ async def execute_note_get(
7
+ note_id: str,
8
+ xsec_token: str | None = None,
9
+ xsec_source: str | None = "pc_feed",
10
+ comment_size: int = 10,
11
+ sub_comment_size: int = 5,
12
+ account_uid: str | None = None,
13
+ ) -> dict:
14
+ adapter = RednoteExtractorAdapter()
15
+ return await adapter.get_note_info(
16
+ note_id=note_id,
17
+ xsec_token=xsec_token,
18
+ xsec_source=xsec_source,
19
+ comment_size=comment_size,
20
+ sub_comment_size=sub_comment_size,
21
+ account_uid=account_uid,
22
+ )
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from rednote_cli.adapters.platform.rednote.extractor import RednoteExtractorAdapter
4
+
5
+
6
+ async def execute_note_search(
7
+ keyword: str,
8
+ size: int = 20,
9
+ sort_by: str | None = None,
10
+ note_type: str | None = None,
11
+ publish_time: str | None = None,
12
+ search_scope: str | None = None,
13
+ location: str | None = None,
14
+ account_uid: str | None = None,
15
+ ) -> list[dict]:
16
+ adapter = RednoteExtractorAdapter()
17
+ return await adapter.search_notes(
18
+ keyword=keyword,
19
+ size=size,
20
+ sort_by=sort_by,
21
+ note_type=note_type,
22
+ publish_time=publish_time,
23
+ search_scope=search_scope,
24
+ location=location,
25
+ account_uid=account_uid,
26
+ )
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from rednote_cli.adapters.platform.rednote.publisher import RednotePublisherAdapter
4
+
5
+
6
+ async def execute_publish_note(
7
+ *,
8
+ target: str,
9
+ image_list: list[str],
10
+ title: str = "",
11
+ content: str = "",
12
+ tags: list[str] | None = None,
13
+ schedule_at: str | None = None,
14
+ account_uid: str | None = None,
15
+ ) -> dict:
16
+ adapter = RednotePublisherAdapter()
17
+ return await adapter.publish_note(
18
+ target=target,
19
+ image_list=image_list,
20
+ title=title,
21
+ content=content,
22
+ tags=tags,
23
+ schedule_at=schedule_at,
24
+ account_uid=account_uid,
25
+ )
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from rednote_cli.adapters.platform.rednote.extractor import RednoteExtractorAdapter
4
+
5
+
6
+ async def execute_user_get(
7
+ user_id: str,
8
+ xsec_token: str | None = None,
9
+ xsec_source: str | None = "pc_feed",
10
+ account_uid: str | None = None,
11
+ ) -> dict:
12
+ adapter = RednoteExtractorAdapter()
13
+ return await adapter.get_user_info(
14
+ user_id=user_id,
15
+ xsec_token=xsec_token,
16
+ xsec_source=xsec_source,
17
+ account_uid=account_uid,
18
+ )
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from rednote_cli.adapters.platform.rednote.extractor import RednoteExtractorAdapter
4
+
5
+
6
+ async def execute_user_search(keyword: str, size: int = 20, account_uid: str | None = None) -> list[dict]:
7
+ adapter = RednoteExtractorAdapter()
8
+ return await adapter.search_users(keyword=keyword, size=size, account_uid=account_uid)
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from rednote_cli.adapters.platform.rednote.extractor import RednoteExtractorAdapter
4
+
5
+
6
+ async def execute_user_self(account_uid: str | None = None) -> dict:
7
+ adapter = RednoteExtractorAdapter()
8
+ return await adapter.get_self_info(account_uid=account_uid)
@@ -0,0 +1 @@
1
+ """CLI entry package."""
@@ -0,0 +1,5 @@
1
+ from rednote_cli.cli.main import run
2
+
3
+
4
+ if __name__ == "__main__":
5
+ run()
@@ -0,0 +1 @@
1
+ """CLI commands package."""
@@ -0,0 +1,204 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import replace
4
+ from uuid import uuid4
5
+
6
+ import typer
7
+
8
+ from rednote_cli.adapters.output.event_stream import emit_event
9
+ from rednote_cli.application.dto.input_models import AccountListInput
10
+ from rednote_cli.application.use_cases.auth_login import execute_auth_login
11
+ from rednote_cli.application.use_cases.auth_status import execute_auth_status
12
+ from rednote_cli.application.use_cases.account_list import execute_account_list
13
+ from rednote_cli.application.use_cases.account_mutation import (
14
+ execute_account_activate,
15
+ execute_account_deactivate,
16
+ execute_account_delete,
17
+ )
18
+ from rednote_cli.cli.options import CliContext
19
+ from rednote_cli.cli.runtime import run_async_command, run_sync_command
20
+ from rednote_cli.cli.utils import all_option_params_are_default
21
+ from rednote_cli.domain.errors import InvalidArgsError
22
+
23
+ app = typer.Typer(help="账号管理(登录/状态/激活/停用/删除)", no_args_is_help=True)
24
+
25
+
26
+ @app.command("login")
27
+ def account_login(
28
+ ctx: typer.Context,
29
+ account: str | None = typer.Option(
30
+ None,
31
+ "--account",
32
+ help="账号唯一标识(user_no)",
33
+ ),
34
+ mode: str = typer.Option(
35
+ "local",
36
+ "--mode",
37
+ help="登录模式:local(本机扫码)| remote(远程截图扫码)",
38
+ ),
39
+ login_timeout_seconds: int = typer.Option(
40
+ 240,
41
+ "--login-timeout-seconds",
42
+ min=30,
43
+ help="扫码登录超时(秒)",
44
+ ),
45
+ screenshot_path: str = typer.Option(
46
+ "",
47
+ "--screenshot-path",
48
+ help="remote 模式屏幕截图路径,可传目录或文件路径,默认为 .rednote_cli/outputs/login_viewport.png",
49
+ ),
50
+ ):
51
+ """启动登录流程并写入持久登录态。"""
52
+ cli_ctx: CliContext = ctx.obj
53
+ effective_trace_id = cli_ctx.trace_id or uuid4().hex
54
+
55
+ def _progress_callback(event_payload: dict) -> None:
56
+ event_name = str(event_payload.get("event") or "").strip()
57
+ if not event_name:
58
+ return
59
+ emit_event(
60
+ command="account.login",
61
+ event=event_name,
62
+ trace_id=effective_trace_id,
63
+ data={k: v for k, v in event_payload.items() if k != "event"},
64
+ )
65
+
66
+ async def _run():
67
+ return await execute_auth_login(
68
+ account_uid=(account or "").strip(),
69
+ mode=mode,
70
+ login_timeout_seconds=login_timeout_seconds,
71
+ screenshot_path=screenshot_path,
72
+ progress_callback=_progress_callback,
73
+ )
74
+
75
+ effective_ctx = replace(
76
+ cli_ctx,
77
+ timeout=max(cli_ctx.timeout, login_timeout_seconds + 30),
78
+ trace_id=effective_trace_id,
79
+ )
80
+ run_async_command(
81
+ ctx=effective_ctx,
82
+ command="account.login",
83
+ func=_run,
84
+ )
85
+
86
+
87
+ @app.command("status")
88
+ def account_status(
89
+ ctx: typer.Context,
90
+ account: str | None = typer.Option(None, "--account", help="账号唯一标识;为空时检查所有有效账号"),
91
+ ):
92
+ """实时检查并刷新账号登录状态。"""
93
+ cli_ctx: CliContext = ctx.obj
94
+
95
+ async def _run() -> list[dict]:
96
+ return await execute_auth_status(account_uid=account)
97
+
98
+ run_async_command(
99
+ ctx=cli_ctx,
100
+ command="account.status",
101
+ func=_run,
102
+ account_uid=account,
103
+ )
104
+
105
+
106
+ @app.command("list")
107
+ def account_list(
108
+ ctx: typer.Context,
109
+ only_active: str = typer.Option(
110
+ "true",
111
+ "--only-active",
112
+ help="仅显示当前可用账号,需显式传入 true|false(默认 true)",
113
+ ),
114
+ ):
115
+ """列出账号池。"""
116
+ cli_ctx: CliContext = ctx.obj
117
+
118
+ def _run() -> list[dict]:
119
+ validated = AccountListInput(only_active=only_active.strip())
120
+ return execute_account_list(only_active=validated.only_active)
121
+
122
+ run_sync_command(
123
+ ctx=cli_ctx,
124
+ command="account.list",
125
+ func=_run,
126
+ )
127
+
128
+
129
+ @app.command("activate")
130
+ def account_activate(
131
+ ctx: typer.Context,
132
+ account: str | None = typer.Option(None, "--account", help="账号唯一标识(user_no)"),
133
+ ):
134
+ """将账号标记为可用。"""
135
+ if all_option_params_are_default(ctx=ctx):
136
+ typer.echo(ctx.get_help())
137
+ raise typer.Exit(code=0)
138
+
139
+ cli_ctx: CliContext = ctx.obj
140
+ account_uid = (account or "").strip()
141
+
142
+ def _run() -> dict:
143
+ if not account_uid:
144
+ raise InvalidArgsError("`--account` 为必填参数")
145
+ return execute_account_activate(account_uid=account_uid)
146
+
147
+ run_sync_command(
148
+ ctx=cli_ctx,
149
+ command="account.activate",
150
+ func=_run,
151
+ account_uid=account_uid,
152
+ )
153
+
154
+
155
+ @app.command("deactivate")
156
+ def account_deactivate(
157
+ ctx: typer.Context,
158
+ account: str | None = typer.Option(None, "--account", help="账号唯一标识(user_no)"),
159
+ ):
160
+ """将账号标记为不可用。"""
161
+ if all_option_params_are_default(ctx=ctx):
162
+ typer.echo(ctx.get_help())
163
+ raise typer.Exit(code=0)
164
+
165
+ cli_ctx: CliContext = ctx.obj
166
+ account_uid = (account or "").strip()
167
+
168
+ def _run() -> dict:
169
+ if not account_uid:
170
+ raise InvalidArgsError("`--account` 为必填参数")
171
+ return execute_account_deactivate(account_uid=account_uid)
172
+
173
+ run_sync_command(
174
+ ctx=cli_ctx,
175
+ command="account.deactivate",
176
+ func=_run,
177
+ account_uid=account_uid,
178
+ )
179
+
180
+
181
+ @app.command("delete")
182
+ def account_delete(
183
+ ctx: typer.Context,
184
+ account: str | None = typer.Option(None, "--account", help="账号唯一标识(user_no)"),
185
+ ):
186
+ """删除账号记录(不会清理业务输出文件)。"""
187
+ if all_option_params_are_default(ctx=ctx):
188
+ typer.echo(ctx.get_help())
189
+ raise typer.Exit(code=0)
190
+
191
+ cli_ctx: CliContext = ctx.obj
192
+ account_uid = (account or "").strip()
193
+
194
+ def _run() -> dict:
195
+ if not account_uid:
196
+ raise InvalidArgsError("`--account` 为必填参数")
197
+ return execute_account_delete(account_uid=account_uid)
198
+
199
+ run_sync_command(
200
+ ctx=cli_ctx,
201
+ command="account.delete",
202
+ func=_run,
203
+ account_uid=account_uid,
204
+ )
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from rednote_cli.application.use_cases.doctor import execute_doctor
6
+ from rednote_cli.cli.options import CliContext
7
+ from rednote_cli.cli.runtime import run_sync_command
8
+
9
+ app = typer.Typer(help="环境诊断与依赖自检", no_args_is_help=True)
10
+
11
+
12
+ @app.command("run")
13
+ def doctor_run(ctx: typer.Context):
14
+ """
15
+ 执行自检并输出可机读诊断结果。
16
+
17
+ 推荐:`rednote-cli doctor run --format json`。
18
+ """
19
+ cli_ctx: CliContext = ctx.obj
20
+ run_sync_command(ctx=cli_ctx, command="doctor.run", func=execute_doctor)
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from rednote_cli.application.use_cases.init_runtime import execute_runtime_init
6
+ from rednote_cli.cli.options import CliContext
7
+ from rednote_cli.cli.runtime import run_sync_command
8
+
9
+ app = typer.Typer(help="初始化运行环境与本地存储", no_args_is_help=True)
10
+
11
+
12
+ @app.command("runtime")
13
+ def init_runtime(ctx: typer.Context):
14
+ """
15
+ 初始化运行时目录与存储文件。
16
+
17
+ 返回字段包括 `app_home`、`accounts_dir`、`settings_file`、`task_schedules_file`。
18
+ """
19
+ cli_ctx: CliContext = ctx.obj
20
+ run_sync_command(ctx=cli_ctx, command="init.runtime", func=execute_runtime_init)