web-presentation-cli 0.1.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.
Files changed (31) hide show
  1. web_presentation_cli-0.1.0/.gitignore +29 -0
  2. web_presentation_cli-0.1.0/PKG-INFO +64 -0
  3. web_presentation_cli-0.1.0/README.md +50 -0
  4. web_presentation_cli-0.1.0/hatch_build.py +21 -0
  5. web_presentation_cli-0.1.0/pyproject.toml +46 -0
  6. web_presentation_cli-0.1.0/src/wp/__init__.py +3 -0
  7. web_presentation_cli-0.1.0/src/wp/cli.py +68 -0
  8. web_presentation_cli-0.1.0/src/wp/client.py +27 -0
  9. web_presentation_cli-0.1.0/src/wp/commands/__init__.py +1 -0
  10. web_presentation_cli-0.1.0/src/wp/commands/asset.py +218 -0
  11. web_presentation_cli-0.1.0/src/wp/commands/auth.py +98 -0
  12. web_presentation_cli-0.1.0/src/wp/commands/catalog.py +84 -0
  13. web_presentation_cli-0.1.0/src/wp/commands/common.py +182 -0
  14. web_presentation_cli-0.1.0/src/wp/commands/component.py +293 -0
  15. web_presentation_cli-0.1.0/src/wp/commands/doctor.py +84 -0
  16. web_presentation_cli-0.1.0/src/wp/commands/job.py +84 -0
  17. web_presentation_cli-0.1.0/src/wp/commands/page.py +314 -0
  18. web_presentation_cli-0.1.0/src/wp/commands/profile.py +71 -0
  19. web_presentation_cli-0.1.0/src/wp/commands/project.py +244 -0
  20. web_presentation_cli-0.1.0/src/wp/commands/screenshot.py +73 -0
  21. web_presentation_cli-0.1.0/src/wp/commands/style.py +155 -0
  22. web_presentation_cli-0.1.0/src/wp/commands/system.py +93 -0
  23. web_presentation_cli-0.1.0/src/wp/commands/theme.py +152 -0
  24. web_presentation_cli-0.1.0/src/wp/commands/workspace.py +89 -0
  25. web_presentation_cli-0.1.0/src/wp/config.py +91 -0
  26. web_presentation_cli-0.1.0/src/wp/formatter.py +66 -0
  27. web_presentation_cli-0.1.0/src/wp_api_client/__init__.py +5 -0
  28. web_presentation_cli-0.1.0/src/wp_api_client/client.py +431 -0
  29. web_presentation_cli-0.1.0/tests/test_capability_commands.py +121 -0
  30. web_presentation_cli-0.1.0/tests/test_cli_commands.py +300 -0
  31. web_presentation_cli-0.1.0/tests/test_screenshot_command.py +146 -0
@@ -0,0 +1,29 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+ venv/
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ .mypy_cache/
9
+ .coverage*
10
+ htmlcov/
11
+ dist/
12
+ build/
13
+ *.egg-info/
14
+
15
+ # Local configuration and secrets
16
+ .env
17
+ .env.*
18
+ !.env.example
19
+ !.env.*.example
20
+
21
+ # Agent and tool caches
22
+ .codex/
23
+ .tmp/
24
+
25
+ # Editors and OS
26
+ .vscode/
27
+ .idea/
28
+ .DS_Store
29
+ Thumbs.db
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.5
2
+ Name: web-presentation-cli
3
+ Version: 0.1.0
4
+ Summary: Web Presentation CLI - 面向 AI 演示文稿创作平台的命令行与 Agent 工具包
5
+ Project-URL: Homepage, https://github.com/LLMxPM/web-presentation-agent-kit
6
+ Project-URL: Repository, https://github.com/LLMxPM/web-presentation-agent-kit
7
+ Project-URL: Issues, https://github.com/LLMxPM/web-presentation-agent-kit/issues
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: click>=8.1.7
10
+ Requires-Dist: httpx>=0.27.0
11
+ Requires-Dist: pydantic>=2.7.0
12
+ Requires-Dist: rich>=13.7.0
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Web Presentation CLI (`wp`)
16
+
17
+ 面向 `web-presentation` AI 演示文稿创作平台的官方命令行与 Agent 工具包。
18
+
19
+ External API 路径、Scope、错误码、幂等和异步任务语义以主仓 [External Agent API v1 契约](https://github.com/LLMxPM/web-presentation/blob/main/docs/developer/reference/external-agent-api.md) 为准;CLI 命令和参数以本仓 `wp --help` 与 CLI 实施文档为准。
20
+
21
+ ## 安装与快速开始
22
+
23
+ ```bash
24
+ # 从 PyPI 安装公开发行版
25
+ uv tool install web-presentation-cli
26
+ # 或使用 pipx
27
+ pipx install web-presentation-cli
28
+
29
+ # 在 agent-kit 仓库根目录安装 CLI
30
+ uv pip install -e ./packages/cli
31
+
32
+ # 登录并绑定 PAT 令牌
33
+ wp login --token wp_pat_xxxxxxxx.yyyyyyyyyyyyyyyy
34
+ # 生产环境可指定 Backend 根地址(不要包含 /api/v1)
35
+ wp login --endpoint https://api.example.com --token wp_pat_xxxxxxxx.yyyyyyyyyyyyyyyy
36
+
37
+ # 检查环境与连通性
38
+ wp doctor
39
+
40
+ # 查看授权的工作空间并切换当前工作空间
41
+ wp workspace list
42
+ wp workspace use <workspace_id>
43
+
44
+ # 查看并切换默认 Profile(Profile 保存 Backend 地址、PAT 和默认工作空间)
45
+ wp profile list
46
+ wp profile use production
47
+
48
+ # 常用操作
49
+ wp system health
50
+ wp project list
51
+ wp project configuration get <project_id>
52
+ wp page list --project-id <project_id>
53
+ wp page dependencies <page_id>
54
+ wp component list --scope suggested --project-id <project_id>
55
+ wp asset content get <asset_id>
56
+ wp theme list
57
+ wp style list
58
+ wp --json guide list
59
+ wp job wait <job_id>
60
+ ```
61
+
62
+ 复杂写入参数使用 `--payload-file`、`--edits-file`、`--content-file`、`--route-file` 和 `--ids-file`。Build、产物下载、Agent 运行、图片能力、Restore 和 MCP 不属于当前 CLI。
63
+
64
+ 写入命令支持 `--idempotency-key <key>`;网络超时后需要重放同一业务请求时复用原 key,不要把同一个 key 用于不同请求。
@@ -0,0 +1,50 @@
1
+ # Web Presentation CLI (`wp`)
2
+
3
+ 面向 `web-presentation` AI 演示文稿创作平台的官方命令行与 Agent 工具包。
4
+
5
+ External API 路径、Scope、错误码、幂等和异步任务语义以主仓 [External Agent API v1 契约](https://github.com/LLMxPM/web-presentation/blob/main/docs/developer/reference/external-agent-api.md) 为准;CLI 命令和参数以本仓 `wp --help` 与 CLI 实施文档为准。
6
+
7
+ ## 安装与快速开始
8
+
9
+ ```bash
10
+ # 从 PyPI 安装公开发行版
11
+ uv tool install web-presentation-cli
12
+ # 或使用 pipx
13
+ pipx install web-presentation-cli
14
+
15
+ # 在 agent-kit 仓库根目录安装 CLI
16
+ uv pip install -e ./packages/cli
17
+
18
+ # 登录并绑定 PAT 令牌
19
+ wp login --token wp_pat_xxxxxxxx.yyyyyyyyyyyyyyyy
20
+ # 生产环境可指定 Backend 根地址(不要包含 /api/v1)
21
+ wp login --endpoint https://api.example.com --token wp_pat_xxxxxxxx.yyyyyyyyyyyyyyyy
22
+
23
+ # 检查环境与连通性
24
+ wp doctor
25
+
26
+ # 查看授权的工作空间并切换当前工作空间
27
+ wp workspace list
28
+ wp workspace use <workspace_id>
29
+
30
+ # 查看并切换默认 Profile(Profile 保存 Backend 地址、PAT 和默认工作空间)
31
+ wp profile list
32
+ wp profile use production
33
+
34
+ # 常用操作
35
+ wp system health
36
+ wp project list
37
+ wp project configuration get <project_id>
38
+ wp page list --project-id <project_id>
39
+ wp page dependencies <page_id>
40
+ wp component list --scope suggested --project-id <project_id>
41
+ wp asset content get <asset_id>
42
+ wp theme list
43
+ wp style list
44
+ wp --json guide list
45
+ wp job wait <job_id>
46
+ ```
47
+
48
+ 复杂写入参数使用 `--payload-file`、`--edits-file`、`--content-file`、`--route-file` 和 `--ids-file`。Build、产物下载、Agent 运行、图片能力、Restore 和 MCP 不属于当前 CLI。
49
+
50
+ 写入命令支持 `--idempotency-key <key>`;网络超时后需要重放同一业务请求时复用原 key,不要把同一个 key 用于不同请求。
@@ -0,0 +1,21 @@
1
+ """文件功能:为 CLI 的 editable 安装映射共享 API Client 源码。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
9
+
10
+
11
+ class CustomBuildHook(BuildHookInterface):
12
+ """在本地 editable 构建时,把同仓 API Client 映射到 CLI 环境。"""
13
+
14
+ def initialize(self, version: str, build_data: dict[str, Any]) -> None:
15
+ """为 editable wheel 注入跨目录 API Client,正式发行包沿用标准打包配置。"""
16
+
17
+ if version != "editable":
18
+ return
19
+
20
+ api_client_source = Path(self.root).parent / "api-client" / "src" / "wp_api_client"
21
+ build_data["force_include_editable"] = {str(api_client_source): "wp_api_client"}
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "web-presentation-cli"
7
+ version = "0.1.0"
8
+ description = "Web Presentation CLI - 面向 AI 演示文稿创作平台的命令行与 Agent 工具包"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ dependencies = [
12
+ "click>=8.1.7",
13
+ "httpx>=0.27.0",
14
+ "rich>=13.7.0",
15
+ "pydantic>=2.7.0",
16
+ ]
17
+
18
+ [project.urls]
19
+ Homepage = "https://github.com/LLMxPM/web-presentation-agent-kit"
20
+ Repository = "https://github.com/LLMxPM/web-presentation-agent-kit"
21
+ Issues = "https://github.com/LLMxPM/web-presentation-agent-kit/issues"
22
+
23
+ [dependency-groups]
24
+ dev = [
25
+ "pytest>=8.0.0",
26
+ ]
27
+
28
+ [project.scripts]
29
+ wp = "wp.cli:main"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src/wp"]
33
+
34
+ [tool.hatch.build.targets.sdist.force-include]
35
+ "../api-client/src/wp_api_client" = "src/wp_api_client"
36
+
37
+ [tool.hatch.build.targets.wheel.force-include]
38
+ "src/wp_api_client" = "wp_api_client"
39
+
40
+ [tool.hatch.build.targets.wheel.hooks.custom]
41
+ path = "hatch_build.py"
42
+
43
+
44
+ [tool.pytest.ini_options]
45
+ testpaths = ["tests"]
46
+ pythonpath = ["src"]
@@ -0,0 +1,3 @@
1
+ """Web Presentation 官方命令行工具包。"""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,68 @@
1
+ """文件功能:CLI 总入口,注册全局选项与全部资源子命令。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from wp import __version__
8
+ from wp.commands.asset import asset_group
9
+ from wp.commands.auth import login_cmd, logout_cmd, whoami_cmd
10
+ from wp.commands.component import component_group
11
+ from wp.commands.catalog import font_group, runtime_kit_group
12
+ from wp.commands.doctor import doctor_cmd
13
+ from wp.commands.job import job_group
14
+ from wp.commands.page import page_group
15
+ from wp.commands.profile import profile_group
16
+ from wp.commands.project import project_group
17
+ from wp.commands.style import style_group
18
+ from wp.commands.theme import theme_group
19
+ from wp.commands.workspace import workspace_group
20
+ from wp.commands.system import guide_group, standards_group, system_group
21
+
22
+
23
+ @click.group()
24
+ @click.version_option(version=__version__, prog_name="wp")
25
+ @click.option(
26
+ "--json",
27
+ "as_json",
28
+ is_flag=True,
29
+ help="让支持表格视图的命令输出 JSON;复杂响应默认也输出 JSON。",
30
+ )
31
+ @click.option("--profile", help="指定使用的 Profile 名称 (默认 default)")
32
+ @click.option("--workspace", "-w", "workspace_id", type=int, help="覆盖当前操作的目标工作空间 ID")
33
+ @click.pass_context
34
+ def main(ctx: click.Context, as_json: bool, profile: str | None, workspace_id: int | None) -> None:
35
+ """Web Presentation 官方命令行工具 (wp) - 面向 AI 演示文稿创作平台。"""
36
+
37
+ ctx.ensure_object(dict)
38
+ ctx.obj["as_json"] = as_json
39
+ ctx.obj["profile"] = profile
40
+ ctx.obj["workspace_id"] = workspace_id
41
+
42
+
43
+ # 注册认证与系统命令
44
+ main.add_command(login_cmd)
45
+ main.add_command(logout_cmd)
46
+ main.add_command(whoami_cmd)
47
+ main.add_command(doctor_cmd)
48
+ main.add_command(profile_group)
49
+ main.add_command(system_group)
50
+ main.add_command(standards_group)
51
+ main.add_command(guide_group)
52
+ main.add_command(runtime_kit_group)
53
+ main.add_command(font_group)
54
+
55
+
56
+ # 注册各实体资源命令组
57
+ main.add_command(workspace_group)
58
+ main.add_command(project_group)
59
+ main.add_command(page_group)
60
+ main.add_command(component_group)
61
+ main.add_command(asset_group)
62
+ main.add_command(theme_group)
63
+ main.add_command(style_group)
64
+ main.add_command(job_group)
65
+
66
+
67
+ if __name__ == "__main__":
68
+ main()
@@ -0,0 +1,27 @@
1
+ """文件功能:提供 CLI 对共享 External API v1 客户端的 Profile 适配。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from wp.config import ProfileConfig
6
+ from wp_api_client import ApiClient as SharedApiClient, ApiClientError
7
+
8
+
9
+ class ApiClient(SharedApiClient):
10
+ """把 CLI ProfileConfig 适配为共享 API Client。"""
11
+
12
+ def __init__(
13
+ self,
14
+ profile: ProfileConfig,
15
+ workspace_id: int | None = None,
16
+ idempotency_key: str | None = None,
17
+ ) -> None:
18
+ super().__init__(
19
+ endpoint=profile.endpoint,
20
+ token=profile.token,
21
+ workspace_id=workspace_id or profile.default_workspace_id,
22
+ user_agent="web-presentation-cli/0.1.0",
23
+ idempotency_key=idempotency_key,
24
+ )
25
+
26
+
27
+ __all__ = ["ApiClient", "ApiClientError"]
@@ -0,0 +1 @@
1
+ """CLI 子命令模块集合。"""
@@ -0,0 +1,218 @@
1
+ """文件功能:提供工作空间资源的查询、上传、文本内容、复制、更新与归档命令。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import click
8
+
9
+ from wp.client import ApiClientError
10
+ from wp.commands.common import (
11
+ confirm_archive,
12
+ get_client,
13
+ handle_api_error,
14
+ idempotency_key_option,
15
+ output_result,
16
+ read_json_file,
17
+ read_text_file,
18
+ require_array,
19
+ require_ids,
20
+ require_object,
21
+ )
22
+ from wp.formatter import print_table
23
+
24
+
25
+ ASSET_TYPES = ["image", "icon", "font", "video", "drawio", "mermaid", "chart", "formula"]
26
+
27
+
28
+ @click.group("asset")
29
+ def asset_group() -> None:
30
+ """工作空间静态资源管理。"""
31
+
32
+
33
+ @asset_group.command("list")
34
+ @click.option("--page", default=1, type=int)
35
+ @click.option("--page-size", default=50, type=int)
36
+ @click.option("--type", "asset_type", type=click.Choice(ASSET_TYPES))
37
+ @click.option("--keyword")
38
+ @click.pass_context
39
+ def list_assets_cmd(ctx: click.Context, page: int, page_size: int, asset_type: str | None, keyword: str | None) -> None:
40
+ """查询工作空间资源。"""
41
+
42
+ params = {"page": page, "page_size": page_size, "asset_type": asset_type, "keyword": keyword}
43
+ try:
44
+ result = get_client(ctx).get("/assets", params={key: value for key, value in params.items() if value is not None})
45
+ if ctx.obj.get("as_json"):
46
+ output_result(ctx, result)
47
+ return
48
+ rows = [[item.get("id"), item.get("name", "-"), item.get("asset_type", "-"), item.get("status", "-")] for item in result.get("items", [])]
49
+ print_table("资源列表", ["ID", "名称", "类型", "状态"], rows)
50
+ except ApiClientError as err:
51
+ handle_api_error("获取资源列表失败", err)
52
+
53
+
54
+ @asset_group.command("get")
55
+ @click.argument("asset_id", type=int)
56
+ @click.pass_context
57
+ def get_asset_cmd(ctx: click.Context, asset_id: int) -> None:
58
+ """获取资源详情。"""
59
+
60
+ try:
61
+ output_result(ctx, get_client(ctx).get(f"/assets/{asset_id}"))
62
+ except ApiClientError as err:
63
+ handle_api_error("获取资源详情失败", err)
64
+
65
+
66
+ @asset_group.command("upload")
67
+ @click.argument("file_path", type=click.Path(exists=True, dir_okay=False))
68
+ @click.option("--type", "asset_type", type=click.Choice(ASSET_TYPES), required=True)
69
+ @click.option("--name")
70
+ @click.option("--description")
71
+ @idempotency_key_option
72
+ @click.pass_context
73
+ def upload_asset_cmd(ctx: click.Context, file_path: str, asset_type: str, name: str | None, description: str | None) -> None:
74
+ """上传二进制或图片资源。"""
75
+
76
+ path = Path(file_path)
77
+ try:
78
+ with path.open("rb") as file_obj:
79
+ result = get_client(ctx).upload(
80
+ "/assets",
81
+ files={"file": (path.name, file_obj)},
82
+ data={"asset_type": asset_type, "name": name or path.stem, "description": description},
83
+ )
84
+ output_result(ctx, result)
85
+ except ApiClientError as err:
86
+ handle_api_error("上传资源失败", err)
87
+
88
+
89
+ @asset_group.command("create")
90
+ @click.option("--payload-file", type=click.Path(exists=True, dir_okay=False), required=True)
91
+ @click.option("--content-file", type=click.Path(exists=True, dir_okay=False), help="覆盖 JSON 中 content 的文本文件")
92
+ @idempotency_key_option
93
+ @click.pass_context
94
+ def create_asset_cmd(ctx: click.Context, payload_file: str, content_file: str | None) -> None:
95
+ """创建文本内容资源。"""
96
+
97
+ payload = require_object(read_json_file(payload_file, label="资源创建载荷"), label="资源创建载荷")
98
+ if content_file:
99
+ payload["content"] = read_text_file(content_file, label="资源内容")
100
+ try:
101
+ output_result(ctx, get_client(ctx).post("/assets/content", json_data=payload))
102
+ except ApiClientError as err:
103
+ handle_api_error("创建文本资源失败", err)
104
+
105
+
106
+ @asset_group.group("content")
107
+ def asset_content_group() -> None:
108
+ """可编辑文本资源内容。"""
109
+
110
+
111
+ @asset_content_group.command("get")
112
+ @click.argument("asset_id", type=int)
113
+ @click.pass_context
114
+ def get_asset_content_cmd(ctx: click.Context, asset_id: int) -> None:
115
+ """读取资源文本内容。"""
116
+
117
+ try:
118
+ output_result(ctx, get_client(ctx).get(f"/assets/{asset_id}/content"))
119
+ except ApiClientError as err:
120
+ handle_api_error("读取资源内容失败", err)
121
+
122
+
123
+ @asset_content_group.command("update")
124
+ @click.argument("asset_id", type=int)
125
+ @click.option("--content-file", type=click.Path(exists=True, dir_okay=False), required=True)
126
+ @click.option("--change-note")
127
+ @idempotency_key_option
128
+ @click.pass_context
129
+ def update_asset_content_cmd(ctx: click.Context, asset_id: int, content_file: str, change_note: str | None) -> None:
130
+ """写入资源文本内容。"""
131
+
132
+ try:
133
+ output_result(ctx, get_client(ctx).put(f"/assets/{asset_id}/content", json_data={"content": read_text_file(content_file, label="资源内容"), "change_note": change_note}))
134
+ except ApiClientError as err:
135
+ handle_api_error("更新资源内容失败", err)
136
+
137
+
138
+ @asset_content_group.command("preview")
139
+ @click.argument("asset_id", type=int)
140
+ @click.option("--content-file", type=click.Path(exists=True, dir_okay=False), required=True)
141
+ @click.pass_context
142
+ def preview_asset_content_cmd(ctx: click.Context, asset_id: int, content_file: str) -> None:
143
+ """预览资源文本内容差异。"""
144
+
145
+ try:
146
+ output_result(ctx, get_client(ctx).post(f"/assets/{asset_id}/content/preview", json_data={"content": read_text_file(content_file, label="资源内容")}, idempotent=False))
147
+ except ApiClientError as err:
148
+ handle_api_error("预览资源内容失败", err)
149
+
150
+
151
+ @asset_group.group("tags")
152
+ def asset_tags_group() -> None:
153
+ """资源标签。"""
154
+
155
+
156
+ @asset_tags_group.command("list")
157
+ @click.option("--type", "asset_type", type=click.Choice(ASSET_TYPES))
158
+ @click.pass_context
159
+ def list_asset_tags_cmd(ctx: click.Context, asset_type: str | None) -> None:
160
+ """列出工作空间资源标签。"""
161
+
162
+ try:
163
+ params = {"asset_type": asset_type} if asset_type else None
164
+ output_result(ctx, get_client(ctx).get("/assets/tags", params=params))
165
+ except ApiClientError as err:
166
+ handle_api_error("获取资源标签失败", err)
167
+
168
+
169
+ @asset_group.command("update")
170
+ @click.argument("asset_id", type=int)
171
+ @click.option("--payload-file", type=click.Path(exists=True, dir_okay=False), required=True)
172
+ @idempotency_key_option
173
+ @click.pass_context
174
+ def update_asset_cmd(ctx: click.Context, asset_id: int, payload_file: str) -> None:
175
+ """更新资源元数据。"""
176
+
177
+ try:
178
+ output_result(ctx, get_client(ctx).patch(f"/assets/{asset_id}", json_data=require_object(read_json_file(payload_file, label="资源更新载荷"), label="资源更新载荷")))
179
+ except ApiClientError as err:
180
+ handle_api_error("更新资源失败", err)
181
+
182
+
183
+ @asset_group.command("copy")
184
+ @click.argument("asset_id", type=int)
185
+ @click.option("--payload-file", type=click.Path(exists=True, dir_okay=False), required=True)
186
+ @idempotency_key_option
187
+ @click.pass_context
188
+ def copy_asset_cmd(ctx: click.Context, asset_id: int, payload_file: str) -> None:
189
+ """复制资源。"""
190
+
191
+ try:
192
+ output_result(ctx, get_client(ctx).post(f"/assets/{asset_id}/copy", json_data=require_object(read_json_file(payload_file, label="资源复制载荷"), label="资源复制载荷")))
193
+ except ApiClientError as err:
194
+ handle_api_error("复制资源失败", err)
195
+
196
+
197
+ @asset_group.command("archive")
198
+ @click.argument("asset_id", type=int, required=False)
199
+ @click.option("--ids-file", type=click.Path(exists=True, dir_okay=False))
200
+ @click.option("--yes", is_flag=True)
201
+ @idempotency_key_option
202
+ @click.pass_context
203
+ def archive_asset_cmd(ctx: click.Context, asset_id: int | None, ids_file: str | None, yes: bool) -> None:
204
+ """归档单个或一批资源。"""
205
+
206
+ try:
207
+ client = get_client(ctx)
208
+ if ids_file:
209
+ ids = require_ids(read_json_file(ids_file, label="归档 ID"))
210
+ confirm_archive(ids, yes=yes, label="资源")
211
+ output_result(ctx, client.post("/assets/batch-archive", json_data={"ids": ids}))
212
+ return
213
+ if asset_id is None:
214
+ raise click.UsageError("必须提供 asset_id 或 --ids-file。")
215
+ confirm_archive([asset_id], yes=yes, label="资源")
216
+ output_result(ctx, client.post(f"/assets/{asset_id}/archive"))
217
+ except ApiClientError as err:
218
+ handle_api_error("归档资源失败", err)
@@ -0,0 +1,98 @@
1
+ """文件功能:处理 CLI 用户登录、PAT 凭证持久化与身份检查。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from wp.client import ApiClient, ApiClientError
8
+ from wp.config import get_profile, load_config, save_config
9
+ from wp.formatter import print_error, print_json, print_success, print_table
10
+
11
+
12
+ @click.command("login")
13
+ @click.option("--token", "-t", required=True, prompt=True, hide_input=True, help="个人访问令牌 (PAT)")
14
+ @click.option("--endpoint", "-e", help="Backend 服务地址 (默认 http://127.0.0.1:8000)")
15
+ @click.pass_context
16
+ def login_cmd(ctx: click.Context, token: str, endpoint: str | None) -> None:
17
+ """登录并保存个人访问令牌 (PAT)。"""
18
+
19
+ cfg = load_config()
20
+ profile = get_profile(cfg, ctx.obj.get("profile"))
21
+ if endpoint:
22
+ profile.endpoint = endpoint
23
+ profile.token = token.strip()
24
+
25
+ client = ApiClient(profile)
26
+ try:
27
+ workspaces = client.get("/workspaces")
28
+ if workspaces:
29
+ profile.default_workspace_id = workspaces[0]["id"]
30
+ save_config(cfg)
31
+ print_success(f"登录成功!已连接到 {profile.endpoint},授权工作空间数: {len(workspaces)}")
32
+ except ApiClientError as err:
33
+ print_error(f"PAT 验证失败: {err.message}", code=err.code)
34
+ raise SystemExit(1)
35
+
36
+
37
+ @click.command("logout")
38
+ @click.pass_context
39
+ def logout_cmd(ctx: click.Context) -> None:
40
+ """清除当前 Profile 的访问令牌。"""
41
+
42
+ cfg = load_config()
43
+ profile = get_profile(cfg, ctx.obj.get("profile"))
44
+ profile.token = None
45
+ profile.default_workspace_id = None
46
+ save_config(cfg)
47
+ print_success("已安全清除本地保存的访问令牌。")
48
+
49
+
50
+ @click.command("whoami")
51
+ @click.pass_context
52
+ def whoami_cmd(ctx: click.Context) -> None:
53
+ """检查当前认证状态与权限能力。"""
54
+
55
+ cfg = load_config()
56
+ profile = get_profile(cfg, ctx.obj.get("profile"))
57
+ client = ApiClient(profile, workspace_id=ctx.obj.get("workspace_id"))
58
+
59
+ try:
60
+ identity = client.get("/auth/whoami")
61
+ workspaces = identity.get("workspaces", [])
62
+ if ctx.obj.get("as_json"):
63
+ print_json({"endpoint": profile.endpoint, **identity})
64
+ return
65
+
66
+ user = identity.get("user") or {}
67
+ print_success(
68
+ f"当前环境: [bold]{profile.endpoint}[/bold],"
69
+ f"用户: [bold]{user.get('display_name') or user.get('username') or '-'}[/bold]"
70
+ )
71
+ print_table(
72
+ title="当前身份",
73
+ columns=["字段", "值"],
74
+ rows=[
75
+ ["用户 ID", user.get("id")],
76
+ ["用户名", user.get("username")],
77
+ ["角色", user.get("role")],
78
+ ["状态", user.get("status")],
79
+ ],
80
+ )
81
+ token = identity.get("token") or {}
82
+ print_table(
83
+ title="当前 PAT",
84
+ columns=["字段", "值"],
85
+ rows=[
86
+ ["公开 ID", token.get("token_public_id")],
87
+ ["Scopes", ", ".join(token.get("scopes") or []) or "-"],
88
+ ["过期时间", token.get("expires_at") or "未设置"],
89
+ ],
90
+ )
91
+ print_table(
92
+ title="已授权工作空间",
93
+ columns=["ID", "空间名称", "成员角色"],
94
+ rows=[[w["id"], w["name"], w.get("role")] for w in workspaces],
95
+ )
96
+ except ApiClientError as err:
97
+ print_error(f"查询失败: {err.message}", code=err.code)
98
+ raise SystemExit(1)