web-presentation-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.
- web_presentation_cli-0.1.0.dist-info/METADATA +64 -0
- web_presentation_cli-0.1.0.dist-info/RECORD +27 -0
- web_presentation_cli-0.1.0.dist-info/WHEEL +4 -0
- web_presentation_cli-0.1.0.dist-info/entry_points.txt +2 -0
- wp/__init__.py +3 -0
- wp/cli.py +68 -0
- wp/client.py +27 -0
- wp/commands/__init__.py +1 -0
- wp/commands/asset.py +218 -0
- wp/commands/auth.py +98 -0
- wp/commands/catalog.py +84 -0
- wp/commands/common.py +182 -0
- wp/commands/component.py +293 -0
- wp/commands/doctor.py +84 -0
- wp/commands/job.py +84 -0
- wp/commands/page.py +314 -0
- wp/commands/profile.py +71 -0
- wp/commands/project.py +244 -0
- wp/commands/screenshot.py +73 -0
- wp/commands/style.py +155 -0
- wp/commands/system.py +93 -0
- wp/commands/theme.py +152 -0
- wp/commands/workspace.py +89 -0
- wp/config.py +91 -0
- wp/formatter.py +66 -0
- wp_api_client/__init__.py +5 -0
- wp_api_client/client.py +431 -0
wp/commands/page.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""文件功能:处理页面管理(列表、详情、源码查看、异步创建与归档)。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from wp.client import ApiClient, ApiClientError
|
|
8
|
+
from wp.commands.screenshot import screenshot_cmd
|
|
9
|
+
from wp.config import get_profile, load_config
|
|
10
|
+
from wp.formatter import print_code, print_error, print_json, print_success, print_table
|
|
11
|
+
from wp.commands.common import (
|
|
12
|
+
confirm_archive,
|
|
13
|
+
get_client,
|
|
14
|
+
handle_api_error,
|
|
15
|
+
idempotency_key_option,
|
|
16
|
+
output_result,
|
|
17
|
+
read_json_file,
|
|
18
|
+
read_text_file,
|
|
19
|
+
require_array,
|
|
20
|
+
require_ids,
|
|
21
|
+
require_object,
|
|
22
|
+
require_success_job,
|
|
23
|
+
resolve_wait_job,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
_PAGE_EDITS_FILE_HELP = (
|
|
27
|
+
"页面编辑操作 JSON 数组;每项的 type 只能是 replace_exact、insert_after 或 rewrite_file。"
|
|
28
|
+
" replace_exact 使用 old_text、new_text;insert_after 使用 anchor_text、new_text;"
|
|
29
|
+
"rewrite_file 使用 content。"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@click.group("page")
|
|
35
|
+
def page_group() -> None:
|
|
36
|
+
"""页面管理与 Mutation 任务。"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@page_group.command("list")
|
|
40
|
+
@click.option("--project-id", "-p", required=True, type=int, help="项目 ID")
|
|
41
|
+
@click.option("--page", default=1, type=int, help="页码")
|
|
42
|
+
@click.option("--page-size", default=50, type=int, help="每页数量")
|
|
43
|
+
@click.pass_context
|
|
44
|
+
def list_pages_cmd(ctx: click.Context, project_id: int, page: int, page_size: int) -> None:
|
|
45
|
+
"""查询指定项目下的页面列表。"""
|
|
46
|
+
|
|
47
|
+
cfg = load_config()
|
|
48
|
+
profile = get_profile(cfg, ctx.obj.get("profile"))
|
|
49
|
+
client = ApiClient(profile, workspace_id=ctx.obj.get("workspace_id"))
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
res = client.get(
|
|
53
|
+
f"/projects/{project_id}/pages",
|
|
54
|
+
params={"page": page, "page_size": page_size, "status": "active"},
|
|
55
|
+
)
|
|
56
|
+
if ctx.obj.get("as_json"):
|
|
57
|
+
print_json(res)
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
items = res.get("items", [])
|
|
61
|
+
rows = [[p["id"], p.get("code", "-"), p.get("title", "-"), f"v{p.get('current_version_no', 1)}", p.get("status", "-")] for p in items]
|
|
62
|
+
print_table(f"项目 (ID: {project_id}) 页面列表", ["ID", "编码", "标题", "版本", "状态"], rows)
|
|
63
|
+
except ApiClientError as err:
|
|
64
|
+
print_error(f"获取页面列表失败: {err.message}", code=err.code)
|
|
65
|
+
raise SystemExit(1)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@page_group.command("get")
|
|
69
|
+
@click.argument("page_id", type=int)
|
|
70
|
+
@click.pass_context
|
|
71
|
+
def get_page_cmd(ctx: click.Context, page_id: int) -> None:
|
|
72
|
+
"""获取指定页面的元数据详情。"""
|
|
73
|
+
|
|
74
|
+
cfg = load_config()
|
|
75
|
+
profile = get_profile(cfg, ctx.obj.get("profile"))
|
|
76
|
+
client = ApiClient(profile, workspace_id=ctx.obj.get("workspace_id"))
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
page = client.get(f"/pages/{page_id}")
|
|
80
|
+
if ctx.obj.get("as_json"):
|
|
81
|
+
print_json(page)
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
rows = [
|
|
85
|
+
["ID", str(page.get("id"))],
|
|
86
|
+
["编码", str(page.get("code"))],
|
|
87
|
+
["标题", str(page.get("title"))],
|
|
88
|
+
["所属项目 ID", str(page.get("project_id"))],
|
|
89
|
+
["当前版本", f"v{page.get('current_version_no')}"],
|
|
90
|
+
["状态", str(page.get("status"))],
|
|
91
|
+
["创建时间", str(page.get("created_at"))],
|
|
92
|
+
]
|
|
93
|
+
print_table(f"页面 (ID: {page_id}) 详情", ["属性", "值"], rows)
|
|
94
|
+
except ApiClientError as err:
|
|
95
|
+
print_error(f"获取页面失败: {err.message}", code=err.code)
|
|
96
|
+
raise SystemExit(1)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@page_group.command("source")
|
|
100
|
+
@click.argument("page_id", type=int)
|
|
101
|
+
@click.pass_context
|
|
102
|
+
def get_page_source_cmd(ctx: click.Context, page_id: int) -> None:
|
|
103
|
+
"""查看指定页面的当前 Vue SFC 源码。"""
|
|
104
|
+
|
|
105
|
+
cfg = load_config()
|
|
106
|
+
profile = get_profile(cfg, ctx.obj.get("profile"))
|
|
107
|
+
client = ApiClient(profile, workspace_id=ctx.obj.get("workspace_id"))
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
data = client.get(f"/pages/{page_id}/source")
|
|
111
|
+
if ctx.obj.get("as_json"):
|
|
112
|
+
print_json(data)
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
code = data.get("source_code", "")
|
|
116
|
+
print_code(code, lexer="vue", title=f"页面 ID {page_id} 源码 (v{data.get('version_no')})")
|
|
117
|
+
except ApiClientError as err:
|
|
118
|
+
print_error(f"读取页面源码失败: {err.message}", code=err.code)
|
|
119
|
+
raise SystemExit(1)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@page_group.command("update")
|
|
123
|
+
@click.argument("page_id", type=int)
|
|
124
|
+
@click.option("--payload-file", type=click.Path(exists=True, dir_okay=False), required=True, help="页面更新 JSON 请求体")
|
|
125
|
+
@idempotency_key_option
|
|
126
|
+
@click.pass_context
|
|
127
|
+
def update_page_cmd(ctx: click.Context, page_id: int, payload_file: str) -> None:
|
|
128
|
+
"""更新页面轻量元数据;源码和结构字段必须走 edit。"""
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
payload = require_object(read_json_file(payload_file, label="页面更新载荷"), label="页面更新载荷")
|
|
132
|
+
output_result(ctx, get_client(ctx).patch(f"/pages/{page_id}", json_data=payload))
|
|
133
|
+
except ApiClientError as err:
|
|
134
|
+
handle_api_error("更新页面失败", err)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@page_group.command("create")
|
|
138
|
+
@click.option("--project-id", "-p", type=int, help="所属项目 ID")
|
|
139
|
+
@click.option("--name", "-n", help="页面标题")
|
|
140
|
+
@click.option("--file", "-f", "file_path", type=click.Path(exists=True), help="Vue 源码文件路径")
|
|
141
|
+
@click.option("--description", "-d", help="页面描述")
|
|
142
|
+
@click.option("--payload-file", type=click.Path(exists=True, dir_okay=False), help="完整页面创建 JSON 请求体")
|
|
143
|
+
@click.option("--wait/--no-wait", default=True, help="是否等待后台 Worker 编译与诊断完成 (默认等待)")
|
|
144
|
+
@idempotency_key_option
|
|
145
|
+
@click.pass_context
|
|
146
|
+
def create_page_cmd(
|
|
147
|
+
ctx: click.Context,
|
|
148
|
+
project_id: int,
|
|
149
|
+
name: str,
|
|
150
|
+
file_path: str | None,
|
|
151
|
+
description: str | None,
|
|
152
|
+
payload_file: str | None,
|
|
153
|
+
wait: bool,
|
|
154
|
+
) -> None:
|
|
155
|
+
"""通过异步 Mutation 任务创建页面(带 AST 扫描与 Chromium 慢诊断)。"""
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
if payload_file:
|
|
159
|
+
payload = require_object(read_json_file(payload_file, label="页面创建载荷"), label="页面创建载荷")
|
|
160
|
+
else:
|
|
161
|
+
if project_id is None or not name or not file_path:
|
|
162
|
+
raise click.UsageError("未使用 --payload-file 时必须提供 --project-id、--name 和 --file。")
|
|
163
|
+
payload = {
|
|
164
|
+
"project_id": project_id,
|
|
165
|
+
"name": name,
|
|
166
|
+
"source_code": read_text_file(file_path, label="页面源码"),
|
|
167
|
+
"description": description,
|
|
168
|
+
}
|
|
169
|
+
client = get_client(ctx)
|
|
170
|
+
result = resolve_wait_job(client, client.create_page(payload), wait=wait, timeout=120.0)
|
|
171
|
+
if wait:
|
|
172
|
+
require_success_job(result, ctx=ctx)
|
|
173
|
+
output_result(ctx, result)
|
|
174
|
+
except ApiClientError as err:
|
|
175
|
+
handle_api_error("提交页面任务失败", err)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@page_group.command("archive")
|
|
179
|
+
@click.argument("page_id", type=int, required=False)
|
|
180
|
+
@click.option("--ids-file", type=click.Path(exists=True, dir_okay=False), help="批量归档 ID JSON 数组")
|
|
181
|
+
@click.option("--yes", "-y", is_flag=True, help="跳过确认直接归档")
|
|
182
|
+
@idempotency_key_option
|
|
183
|
+
@click.pass_context
|
|
184
|
+
def archive_page_cmd(ctx: click.Context, page_id: int | None, ids_file: str | None, yes: bool) -> None:
|
|
185
|
+
"""归档页面。"""
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
client = get_client(ctx)
|
|
189
|
+
if ids_file:
|
|
190
|
+
ids = require_ids(read_json_file(ids_file, label="归档 ID"))
|
|
191
|
+
confirm_archive(ids, yes=yes, label="页面")
|
|
192
|
+
output_result(ctx, client.post("/pages/batch-archive", json_data={"ids": ids}))
|
|
193
|
+
return
|
|
194
|
+
if page_id is None:
|
|
195
|
+
raise click.UsageError("必须提供 page_id 或 --ids-file。")
|
|
196
|
+
confirm_archive([page_id], yes=yes, label="页面")
|
|
197
|
+
output_result(ctx, client.post(f"/pages/{page_id}/archive"))
|
|
198
|
+
except ApiClientError as err:
|
|
199
|
+
handle_api_error("归档页面失败", err)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@page_group.command("copy")
|
|
203
|
+
@click.argument("page_id", type=int)
|
|
204
|
+
@click.option("--payload-file", type=click.Path(exists=True, dir_okay=False), required=True, help="页面复制 JSON 请求体")
|
|
205
|
+
@idempotency_key_option
|
|
206
|
+
@click.pass_context
|
|
207
|
+
def copy_page_cmd(ctx: click.Context, page_id: int, payload_file: str) -> None:
|
|
208
|
+
"""复制页面到目标项目。"""
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
payload = require_object(read_json_file(payload_file, label="页面复制载荷"), label="页面复制载荷")
|
|
212
|
+
output_result(ctx, get_client(ctx).copy_page(page_id, payload))
|
|
213
|
+
except ApiClientError as err:
|
|
214
|
+
handle_api_error("复制页面失败", err)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@page_group.command("edit")
|
|
218
|
+
@click.argument("page_id", type=int)
|
|
219
|
+
@click.option("--edits-file", type=click.Path(exists=True, dir_okay=False), required=True, help=_PAGE_EDITS_FILE_HELP)
|
|
220
|
+
@click.option("--base-version-no", type=int, required=True)
|
|
221
|
+
@click.option("--wait/--no-wait", default=True)
|
|
222
|
+
@click.option("--timeout", type=float, default=120.0, show_default=True)
|
|
223
|
+
@idempotency_key_option
|
|
224
|
+
@click.pass_context
|
|
225
|
+
def edit_page_cmd(ctx: click.Context, page_id: int, edits_file: str, base_version_no: int, wait: bool, timeout: float) -> None:
|
|
226
|
+
"""提交页面结构化编辑任务。"""
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
edits = require_array(read_json_file(edits_file, label="页面编辑操作"), label="页面编辑操作")
|
|
230
|
+
payload = {"page_id": page_id, "base_version_no": base_version_no, "edits": edits}
|
|
231
|
+
client = get_client(ctx)
|
|
232
|
+
job = client.edit_page(page_id, payload)
|
|
233
|
+
result = resolve_wait_job(client, job, wait=wait, timeout=timeout)
|
|
234
|
+
if wait:
|
|
235
|
+
require_success_job(result, ctx=ctx)
|
|
236
|
+
output_result(ctx, result)
|
|
237
|
+
except ApiClientError as err:
|
|
238
|
+
handle_api_error("提交页面编辑失败", err)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@page_group.group("version")
|
|
242
|
+
def page_version_group() -> None:
|
|
243
|
+
"""页面历史版本。"""
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@page_version_group.command("list")
|
|
247
|
+
@click.argument("page_id", type=int)
|
|
248
|
+
@click.pass_context
|
|
249
|
+
def list_page_versions_cmd(ctx: click.Context, page_id: int) -> None:
|
|
250
|
+
"""列出页面版本。"""
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
output_result(ctx, get_client(ctx).get(f"/pages/{page_id}/versions"))
|
|
254
|
+
except ApiClientError as err:
|
|
255
|
+
handle_api_error("获取页面版本失败", err)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@page_version_group.command("get")
|
|
259
|
+
@click.argument("page_id", type=int)
|
|
260
|
+
@click.argument("version_no", type=int)
|
|
261
|
+
@click.pass_context
|
|
262
|
+
def get_page_version_cmd(ctx: click.Context, page_id: int, version_no: int) -> None:
|
|
263
|
+
"""获取页面指定版本。"""
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
output_result(ctx, get_client(ctx).get(f"/pages/{page_id}/versions/{version_no}"))
|
|
267
|
+
except ApiClientError as err:
|
|
268
|
+
handle_api_error("获取页面版本内容失败", err)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@page_group.command("dependencies")
|
|
272
|
+
@click.argument("page_id", type=int)
|
|
273
|
+
@click.pass_context
|
|
274
|
+
def page_dependencies_cmd(ctx: click.Context, page_id: int) -> None:
|
|
275
|
+
"""获取页面当前版本依赖。"""
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
output_result(ctx, get_client(ctx).get(f"/pages/{page_id}/dependencies"))
|
|
279
|
+
except ApiClientError as err:
|
|
280
|
+
handle_api_error("获取页面依赖失败", err)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@page_group.command("validate")
|
|
284
|
+
@click.argument("page_id", type=int)
|
|
285
|
+
@click.option(
|
|
286
|
+
"--mode",
|
|
287
|
+
type=click.Choice(["current", "content", "edits"]),
|
|
288
|
+
default="current",
|
|
289
|
+
show_default=True,
|
|
290
|
+
help="校验模式:current 校验当前源码;content 校验完整候选源码;edits 校验结构化编辑后的候选源码。",
|
|
291
|
+
)
|
|
292
|
+
@click.option("--source-file", type=click.Path(exists=True, dir_okay=False), help="content 模式使用的完整候选源码文件")
|
|
293
|
+
@click.option("--edits-file", type=click.Path(exists=True, dir_okay=False), help=_PAGE_EDITS_FILE_HELP)
|
|
294
|
+
@click.option("--detail", is_flag=True, help="返回更详细的校验诊断")
|
|
295
|
+
@click.pass_context
|
|
296
|
+
def validate_page_cmd(ctx: click.Context, page_id: int, mode: str, source_file: str | None, edits_file: str | None, detail: bool) -> None:
|
|
297
|
+
"""校验页面当前或候选源码。"""
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
payload: dict[str, object] = {"entity_type": "page", "entity_id": page_id, "mode": mode, "detail": detail}
|
|
301
|
+
if mode == "content":
|
|
302
|
+
if not source_file:
|
|
303
|
+
raise click.UsageError("content 模式必须提供 --source-file。")
|
|
304
|
+
payload["source_code"] = read_text_file(source_file, label="页面源码")
|
|
305
|
+
if mode == "edits":
|
|
306
|
+
if not edits_file:
|
|
307
|
+
raise click.UsageError("edits 模式必须提供 --edits-file。")
|
|
308
|
+
payload["edits"] = require_array(read_json_file(edits_file, label="页面编辑操作"), label="页面编辑操作")
|
|
309
|
+
output_result(ctx, get_client(ctx).validate_entity(payload))
|
|
310
|
+
except ApiClientError as err:
|
|
311
|
+
handle_api_error("页面校验失败", err)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
page_group.add_command(screenshot_cmd)
|
wp/commands/profile.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""文件功能:处理 CLI Profile 列表查看与默认 Profile 切换。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from wp.config import get_profile, load_config, save_config
|
|
8
|
+
from wp.formatter import print_error, print_json, print_success, print_table
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group("profile")
|
|
12
|
+
def profile_group() -> None:
|
|
13
|
+
"""管理 Backend 地址、PAT 和默认工作空间所在的 Profile。"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@profile_group.command("list")
|
|
17
|
+
@click.pass_context
|
|
18
|
+
def list_profiles_cmd(ctx: click.Context) -> None:
|
|
19
|
+
"""列出本地 Profile,不显示 PAT。"""
|
|
20
|
+
|
|
21
|
+
cfg = load_config()
|
|
22
|
+
profiles = {
|
|
23
|
+
name: {
|
|
24
|
+
"endpoint": profile.endpoint,
|
|
25
|
+
"default_workspace_id": profile.default_workspace_id,
|
|
26
|
+
"has_token": bool(profile.token),
|
|
27
|
+
}
|
|
28
|
+
for name, profile in sorted(cfg.profiles.items())
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if ctx.obj.get("as_json"):
|
|
32
|
+
print_json({"current_profile": cfg.current_profile, "profiles": profiles})
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
rows = []
|
|
36
|
+
for name, profile in sorted(cfg.profiles.items()):
|
|
37
|
+
marker = "*" if name == cfg.current_profile else ""
|
|
38
|
+
rows.append([
|
|
39
|
+
f"{marker} {name}".strip(),
|
|
40
|
+
profile.endpoint,
|
|
41
|
+
profile.default_workspace_id or "-",
|
|
42
|
+
"已配置" if profile.token else "未配置",
|
|
43
|
+
])
|
|
44
|
+
print_table(
|
|
45
|
+
"本地 Profile (* 当前默认)",
|
|
46
|
+
["名称", "Backend 地址", "默认工作空间", "PAT"],
|
|
47
|
+
rows,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@profile_group.command("use")
|
|
52
|
+
@click.argument("profile_name")
|
|
53
|
+
def use_profile_cmd(profile_name: str) -> None:
|
|
54
|
+
"""将指定 Profile 设为后续命令使用的默认 Profile。"""
|
|
55
|
+
|
|
56
|
+
cfg = load_config()
|
|
57
|
+
if profile_name not in cfg.profiles:
|
|
58
|
+
print_error(
|
|
59
|
+
f"Profile 不存在: {profile_name}。请先使用 `wp --profile {profile_name} login ...` 创建。",
|
|
60
|
+
code="PROFILE_NOT_FOUND",
|
|
61
|
+
)
|
|
62
|
+
raise SystemExit(1)
|
|
63
|
+
|
|
64
|
+
cfg.current_profile = profile_name
|
|
65
|
+
profile = get_profile(cfg, profile_name)
|
|
66
|
+
save_config(cfg)
|
|
67
|
+
workspace = profile.default_workspace_id or "未设置"
|
|
68
|
+
print_success(
|
|
69
|
+
f"已切换默认 Profile: [bold]{profile_name}[/bold] "
|
|
70
|
+
f"(Backend: {profile.endpoint}, 工作空间: {workspace})"
|
|
71
|
+
)
|
wp/commands/project.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""文件功能:处理项目管理(列表、详情、创建、更新、归档)。"""
|
|
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
|
|
9
|
+
from wp.formatter import print_error, print_json, print_success, print_table
|
|
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
|
+
require_ids,
|
|
18
|
+
require_object,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@click.group("project")
|
|
23
|
+
def project_group() -> None:
|
|
24
|
+
"""项目管理操作。"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@project_group.command("list")
|
|
28
|
+
@click.option("--page", "-p", default=1, type=int, help="页码")
|
|
29
|
+
@click.option("--page-size", "-s", default=20, type=int, help="每页数量")
|
|
30
|
+
@click.option("--keyword", "-k", help="搜索关键字")
|
|
31
|
+
@click.pass_context
|
|
32
|
+
def list_projects_cmd(ctx: click.Context, page: int, page_size: int, keyword: str | None) -> None:
|
|
33
|
+
"""查询工作空间内的项目列表。"""
|
|
34
|
+
|
|
35
|
+
cfg = load_config()
|
|
36
|
+
profile = get_profile(cfg, ctx.obj.get("profile"))
|
|
37
|
+
client = ApiClient(profile, workspace_id=ctx.obj.get("workspace_id"))
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
params = {"page": page, "page_size": page_size, "status": "active"}
|
|
41
|
+
if keyword:
|
|
42
|
+
params["keyword"] = keyword
|
|
43
|
+
res = client.get("/projects", params=params)
|
|
44
|
+
if ctx.obj.get("as_json"):
|
|
45
|
+
print_json(res)
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
items = res.get("items", [])
|
|
49
|
+
rows = [[p["id"], p["name"], p.get("status", "-"), p.get("created_at", "-")[:19]] for p in items]
|
|
50
|
+
print_table(f"项目列表 (共 {res.get('total', len(items))} 项)", ["ID", "项目名称", "状态", "创建时间"], rows)
|
|
51
|
+
except ApiClientError as err:
|
|
52
|
+
print_error(f"获取项目列表失败: {err.message}", code=err.code)
|
|
53
|
+
raise SystemExit(1)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@project_group.command("get")
|
|
57
|
+
@click.argument("project_id", type=int)
|
|
58
|
+
@click.pass_context
|
|
59
|
+
def get_project_cmd(ctx: click.Context, project_id: int) -> None:
|
|
60
|
+
"""获取单个项目详情。"""
|
|
61
|
+
|
|
62
|
+
cfg = load_config()
|
|
63
|
+
profile = get_profile(cfg, ctx.obj.get("profile"))
|
|
64
|
+
client = ApiClient(profile, workspace_id=ctx.obj.get("workspace_id"))
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
project = client.get(f"/projects/{project_id}")
|
|
68
|
+
if ctx.obj.get("as_json"):
|
|
69
|
+
print_json(project)
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
print_success(f"项目详情: [bold]{project.get('name')}[/bold] (ID: {project_id})")
|
|
73
|
+
rows = [
|
|
74
|
+
["ID", str(project.get("id"))],
|
|
75
|
+
["名称", str(project.get("name"))],
|
|
76
|
+
["描述", str(project.get("description") or "-")],
|
|
77
|
+
["工作空间 ID", str(project.get("workspace_id"))],
|
|
78
|
+
["主题 ID", str(project.get("theme_id") or "-")],
|
|
79
|
+
["样式 ID", str(project.get("style_id") or "-")],
|
|
80
|
+
["状态", str(project.get("status"))],
|
|
81
|
+
["创建时间", str(project.get("created_at"))],
|
|
82
|
+
]
|
|
83
|
+
print_table("基本属性", ["字段", "值"], rows)
|
|
84
|
+
except ApiClientError as err:
|
|
85
|
+
print_error(f"获取项目详情失败: {err.message}", code=err.code)
|
|
86
|
+
raise SystemExit(1)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@project_group.command("create")
|
|
90
|
+
@click.option("--name", "-n", help="项目名称")
|
|
91
|
+
@click.option("--description", "-d", help="项目描述")
|
|
92
|
+
@click.option("--payload-file", type=click.Path(exists=True, dir_okay=False), help="完整项目 JSON 请求体")
|
|
93
|
+
@idempotency_key_option
|
|
94
|
+
@click.pass_context
|
|
95
|
+
def create_project_cmd(ctx: click.Context, name: str | None, description: str | None, payload_file: str | None) -> None:
|
|
96
|
+
"""创建新项目。"""
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
if payload_file:
|
|
100
|
+
payload = require_object(read_json_file(payload_file, label="项目载荷"), label="项目载荷")
|
|
101
|
+
else:
|
|
102
|
+
if not name:
|
|
103
|
+
raise click.UsageError("未使用 --payload-file 时必须提供 --name。")
|
|
104
|
+
payload = {"name": name, "description": description}
|
|
105
|
+
output_result(ctx, get_client(ctx).post("/projects", json_data=payload))
|
|
106
|
+
except ApiClientError as err:
|
|
107
|
+
handle_api_error("创建项目失败", err)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@project_group.command("update")
|
|
111
|
+
@click.argument("project_id", type=int)
|
|
112
|
+
@click.option("--payload-file", type=click.Path(exists=True, dir_okay=False), required=True, help="项目更新 JSON 请求体")
|
|
113
|
+
@idempotency_key_option
|
|
114
|
+
@click.pass_context
|
|
115
|
+
def update_project_cmd(ctx: click.Context, project_id: int, payload_file: str) -> None:
|
|
116
|
+
"""更新项目元数据或基础配置。"""
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
payload = require_object(read_json_file(payload_file, label="项目更新载荷"), label="项目更新载荷")
|
|
120
|
+
output_result(ctx, get_client(ctx).patch(f"/projects/{project_id}", json_data=payload))
|
|
121
|
+
except ApiClientError as err:
|
|
122
|
+
handle_api_error("更新项目失败", err)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@project_group.group("configuration")
|
|
126
|
+
def project_configuration_group() -> None:
|
|
127
|
+
"""项目结构化展示配置。"""
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@project_configuration_group.command("get")
|
|
131
|
+
@click.argument("project_id", type=int)
|
|
132
|
+
@click.pass_context
|
|
133
|
+
def get_project_configuration_cmd(ctx: click.Context, project_id: int) -> None:
|
|
134
|
+
"""获取项目配置。"""
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
output_result(ctx, get_client(ctx).get(f"/projects/{project_id}/configuration"))
|
|
138
|
+
except ApiClientError as err:
|
|
139
|
+
handle_api_error("获取项目配置失败", err)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@project_configuration_group.command("update")
|
|
143
|
+
@click.argument("project_id", type=int)
|
|
144
|
+
@click.option("--payload-file", type=click.Path(exists=True, dir_okay=False), required=True, help="项目配置 JSON 请求体")
|
|
145
|
+
@idempotency_key_option
|
|
146
|
+
@click.pass_context
|
|
147
|
+
def update_project_configuration_cmd(ctx: click.Context, project_id: int, payload_file: str) -> None:
|
|
148
|
+
"""更新项目展示配置。"""
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
payload = require_object(read_json_file(payload_file, label="项目配置载荷"), label="项目配置载荷")
|
|
152
|
+
output_result(ctx, get_client(ctx).put(f"/projects/{project_id}/configuration", json_data=payload))
|
|
153
|
+
except ApiClientError as err:
|
|
154
|
+
handle_api_error("更新项目配置失败", err)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@project_group.group("route")
|
|
158
|
+
def project_route_group() -> None:
|
|
159
|
+
"""项目路由树。"""
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@project_route_group.command("get")
|
|
163
|
+
@click.argument("project_id", type=int)
|
|
164
|
+
@click.pass_context
|
|
165
|
+
def get_project_route_cmd(ctx: click.Context, project_id: int) -> None:
|
|
166
|
+
"""获取项目路由树。"""
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
output_result(ctx, get_client(ctx).get(f"/projects/{project_id}/route-tree"))
|
|
170
|
+
except ApiClientError as err:
|
|
171
|
+
handle_api_error("获取项目路由树失败", err)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@project_route_group.command("replace")
|
|
175
|
+
@click.argument("project_id", type=int)
|
|
176
|
+
@click.option("--route-file", type=click.Path(exists=True, dir_okay=False), required=True, help="完整路由树 JSON 文件")
|
|
177
|
+
@idempotency_key_option
|
|
178
|
+
@click.pass_context
|
|
179
|
+
def replace_project_route_cmd(ctx: click.Context, project_id: int, route_file: str) -> None:
|
|
180
|
+
"""整体替换项目路由树。"""
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
payload = require_object(read_json_file(route_file, label="路由树"), label="路由树")
|
|
184
|
+
output_result(ctx, get_client(ctx).put(f"/projects/{project_id}/route-tree", json_data=payload))
|
|
185
|
+
except ApiClientError as err:
|
|
186
|
+
handle_api_error("替换项目路由树失败", err)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@project_group.command("apply-style")
|
|
190
|
+
@click.argument("project_id", type=int)
|
|
191
|
+
@click.option("--style-id", type=int, required=True)
|
|
192
|
+
@idempotency_key_option
|
|
193
|
+
@click.pass_context
|
|
194
|
+
def apply_project_style_cmd(ctx: click.Context, project_id: int, style_id: int) -> None:
|
|
195
|
+
"""将样式方案应用到项目。"""
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
output_result(ctx, get_client(ctx).post(f"/projects/{project_id}/apply-style", json_data={"style_id": style_id}))
|
|
199
|
+
except ApiClientError as err:
|
|
200
|
+
handle_api_error("应用项目样式失败", err)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@project_group.group("build-assets")
|
|
204
|
+
def project_build_assets_group() -> None:
|
|
205
|
+
"""项目构建额外资源配置,不启动构建。"""
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@project_build_assets_group.command("update")
|
|
209
|
+
@click.argument("project_id", type=int)
|
|
210
|
+
@click.option("--payload-file", type=click.Path(exists=True, dir_okay=False), required=True)
|
|
211
|
+
@idempotency_key_option
|
|
212
|
+
@click.pass_context
|
|
213
|
+
def update_project_build_assets_cmd(ctx: click.Context, project_id: int, payload_file: str) -> None:
|
|
214
|
+
"""更新项目构建额外资源配置。"""
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
payload = require_object(read_json_file(payload_file, label="构建资源配置"), label="构建资源配置")
|
|
218
|
+
output_result(ctx, get_client(ctx).put(f"/projects/{project_id}/build-assets", json_data=payload))
|
|
219
|
+
except ApiClientError as err:
|
|
220
|
+
handle_api_error("更新项目构建资源配置失败", err)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@project_group.command("archive")
|
|
224
|
+
@click.argument("project_id", type=int, required=False)
|
|
225
|
+
@click.option("--ids-file", type=click.Path(exists=True, dir_okay=False), help="批量归档 ID JSON 数组")
|
|
226
|
+
@click.option("--yes", "-y", is_flag=True, help="跳过确认直接归档")
|
|
227
|
+
@idempotency_key_option
|
|
228
|
+
@click.pass_context
|
|
229
|
+
def archive_project_cmd(ctx: click.Context, project_id: int | None, ids_file: str | None, yes: bool) -> None:
|
|
230
|
+
"""归档项目。"""
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
client = get_client(ctx)
|
|
234
|
+
if ids_file:
|
|
235
|
+
ids = require_ids(read_json_file(ids_file, label="归档 ID"))
|
|
236
|
+
confirm_archive(ids, yes=yes, label="项目")
|
|
237
|
+
output_result(ctx, client.post("/projects/batch-archive", json_data={"ids": ids}))
|
|
238
|
+
return
|
|
239
|
+
if project_id is None:
|
|
240
|
+
raise click.UsageError("必须提供 project_id 或 --ids-file。")
|
|
241
|
+
confirm_archive([project_id], yes=yes, label="项目")
|
|
242
|
+
output_result(ctx, client.post(f"/projects/{project_id}/archive"))
|
|
243
|
+
except ApiClientError as err:
|
|
244
|
+
handle_api_error("归档项目失败", err)
|