feat(review): 重构项目级单方案验收协作

This commit is contained in:
yuzhe
2026-07-22 16:41:03 +08:00
parent 6091d61612
commit e7e268d4eb
45 changed files with 1830 additions and 812 deletions

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""通过 Delivery Desk API 创建作品或上传作品新版本"""
"""通过 Delivery Desk API 创建作品或提交新的验收轮次"""
from __future__ import annotations
@@ -37,11 +37,10 @@ def request_json(opener, url: str, *, method: str = "GET", data: bytes | None =
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="通过 API 创建测试作品或上传新版本")
parser = argparse.ArgumentParser(description="通过 API 创建测试作品或提交新的验收轮次")
parser.add_argument("--base-url", default=os.getenv("DELIVERY_DESK_BASE_URL", "http://127.0.0.1:3010"))
parser.add_argument("--project-id", type=int, default=None, help="不传时自动选择唯一可访问的项目")
parser.add_argument("--collection-id", type=int, default=None, help="不传时自动选择项目下唯一的作品交付集")
parser.add_argument("--work-id", type=int, default=None, help="传入后为该作品创建新版本")
parser.add_argument("--work-id", type=int, default=None, help="传入后为该作品提交新的验收轮次")
parser.add_argument("--external-id", default=None, help="调用方作品唯一标识,用于幂等创建和找回作品")
parser.add_argument("--api-key", default=os.getenv("DELIVERY_DESK_API_KEY"))
parser.add_argument("--username", default=os.getenv("DELIVERY_DESK_USERNAME", "operator"))
@@ -99,16 +98,6 @@ def main() -> int:
_, projects, _ = request_json(opener, f"{base_url}/api/projects", headers=auth_headers)
project = choose_item(projects, args.project_id, "项目")
project_id = int(project["id"])
_, collections, _ = request_json(
opener,
f"{base_url}/api/projects/{project_id}/collections",
headers=auth_headers,
)
collection = choose_item(collections, args.collection_id, "作品交付集")
collection_id = int(collection["id"])
if int(collection["project_id"]) != project_id:
raise ApiError(f"作品交付集 {collection_id} 不属于项目 {project_id}")
title = args.title or f"API 测试作品 {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
image_urls = args.image_urls or ["https://placehold.co/1200x800/png?text=Delivery+Desk+API+Test"]
upload_headers = {**auth_headers, "Content-Type": "application/json"}
@@ -120,28 +109,29 @@ def main() -> int:
}
if args.work_id is not None:
_, current, _ = request_json(opener, f"{base_url}/api/notes/{args.work_id}", headers=auth_headers)
if int(current["project"]["id"]) != project_id or int(current["collection"]["id"]) != collection_id:
raise ApiError(f"作品 {args.work_id} 不属于选定的项目和作品交付集")
_, current, _ = request_json(opener, f"{base_url}/api/works/{args.work_id}", headers=auth_headers)
if int(current["project"]["id"]) != project_id:
raise ApiError(f"作品 {args.work_id} 不属于选定的项目")
status, work, _ = request_json(
opener,
f"{base_url}/api/notes/{args.work_id}/versions",
f"{base_url}/api/works/{args.work_id}/rounds",
method="POST",
data=json.dumps(payload).encode("utf-8"),
headers=upload_headers,
)
if status != 201 or not work or int(work.get("id", 0)) != args.work_id:
raise ApiError("版本接口没有返回目标作品")
action = "version_created"
raise ApiError("验收轮次接口没有返回目标作品")
action = "round_created"
external_id = work.get("external_id")
else:
external_id = args.external_id or f"api-smoke-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
create_payload = {**payload, "collectionId": collection_id, "externalId": external_id}
create_payload = {**payload, "externalId": external_id}
body = json.dumps(create_payload).encode("utf-8")
status, work, _ = request_json(opener, f"{base_url}/api/notes", method="POST", data=body, headers=upload_headers)
if status not in (200, 201) or not work or int(work.get("collection_id", 0)) != collection_id:
raise ApiError("接口未返回属于目标作品交付集的作品")
_, repeated, _ = request_json(opener, f"{base_url}/api/notes", method="POST", data=body, headers=upload_headers)
works_url = f"{base_url}/api/projects/{project_id}/works"
status, work, _ = request_json(opener, works_url, method="POST", data=body, headers=upload_headers)
if status not in (200, 201) or not work or int(work.get("project_id", 0)) != project_id:
raise ApiError("接口未返回属于目标项目的作品")
_, repeated, _ = request_json(opener, works_url, method="POST", data=body, headers=upload_headers)
if int(repeated.get("id", 0)) != int(work["id"]) or not repeated.get("idempotent"):
raise ApiError("相同 externalId 的重复请求未通过幂等校验")
action = "work_created" if status == 201 else "existing_work_returned"
@@ -153,7 +143,6 @@ def main() -> int:
"action": action,
"group": project.get("group_name"),
"project": project.get("name"),
"collection": collection.get("name"),
"work_id": work.get("id"),
"external_id": external_id,
"version_number": work.get("version_number"),

View File

@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""获取指定作品验收轮次的全部批注与反馈。"""
from __future__ import annotations
import argparse
import getpass
import json
import os
import sys
from http.cookiejar import CookieJar
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import HTTPCookieProcessor, Request, build_opener
class ApiError(RuntimeError):
"""Delivery Desk API 请求失败。"""
def request_json(opener, url: str, *, method: str = "GET", data: bytes | None = None, headers: dict[str, str] | None = None):
request = Request(url, data=data, method=method, headers=headers or {})
try:
with opener.open(request, timeout=15) as response:
body = response.read().decode("utf-8")
return response.status, json.loads(body) if body else None
except HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
try:
parsed = json.loads(body)
detail = parsed.get("error", body) if isinstance(parsed, dict) else body
except json.JSONDecodeError:
detail = body
raise ApiError(f"{method} {url} 返回 {error.code}: {detail}") from error
except URLError as error:
raise ApiError(f"无法连接 {url}: {error.reason}") from error
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="获取作品指定验收轮次的全部批注")
parser.add_argument("--base-url", default=os.getenv("DELIVERY_DESK_BASE_URL", "http://127.0.0.1:3010"))
parser.add_argument("--project-name", default="光影内容计划", help="用于校验作品所属项目")
parser.add_argument("--work-id", type=int, default=13)
parser.add_argument("--round", type=int, default=2, dest="round_number")
parser.add_argument("--include-history", action="store_true", help="同时返回已关闭或已撤回的历史反馈")
parser.add_argument("--api-key", default=os.getenv("DELIVERY_DESK_API_KEY"))
parser.add_argument("--username", default=os.getenv("DELIVERY_DESK_USERNAME", "operator"))
parser.add_argument("--password", default=os.getenv("DELIVERY_DESK_PASSWORD"))
return parser.parse_args()
def authenticate(opener, base_url: str, args: argparse.Namespace) -> dict[str, str]:
if args.api_key:
return {"Authorization": f"Bearer {args.api_key}"}
password = args.password or getpass.getpass(f"请输入账号 {args.username} 的密码: ")
payload = json.dumps({"username": args.username, "password": password}).encode("utf-8")
status, _ = request_json(
opener,
f"{base_url}/api/auth/login",
method="POST",
data=payload,
headers={"Content-Type": "application/json"},
)
if status != 200:
raise ApiError("登录接口未返回成功状态")
return {}
def main() -> int:
args = parse_args()
if args.work_id < 1 or args.round_number < 1:
raise ApiError("作品 ID 和轮次必须是正整数")
base_url = args.base_url.rstrip("/")
opener = build_opener(HTTPCookieProcessor(CookieJar()))
auth_headers = authenticate(opener, base_url, args)
query = urlencode({"round": args.round_number, "include_history": str(args.include_history).lower()})
_, context = request_json(
opener,
f"{base_url}/api/works/{args.work_id}/optimization-context?{query}",
headers=auth_headers,
)
if not isinstance(context, dict):
raise ApiError("内容优化接口返回格式无效")
project = context.get("project")
actual_project_name = project.get("name") if isinstance(project, dict) else None
if actual_project_name != args.project_name:
raise ApiError(f"Work {args.work_id:03d} 属于项目“{actual_project_name}”,不是“{args.project_name}")
print(json.dumps(context, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (ApiError, ValueError) as error:
print(f"测试失败: {error}", file=sys.stderr)
raise SystemExit(1)