mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-01 04:47:03 +08:00
fix(transfer): make manual review notes optional
This commit is contained in:
@@ -867,9 +867,9 @@ class TransferExecutionCommand:
|
||||
reason: str,
|
||||
result: Optional[TransferStepResult] = None,
|
||||
) -> TransferManualReviewResult:
|
||||
"""提交人工判定;FAILED 在无 lease durable 结算落地前明确拒绝。"""
|
||||
if not all((task_id, operation_id, actor, reason)):
|
||||
raise ValueError("人工判定缺少任务、步骤、操作者或原因")
|
||||
"""提交人工判定,理由可留空;FAILED 仍须经 lease durable 结算。"""
|
||||
if not all((task_id, operation_id, actor)):
|
||||
raise ValueError("人工判定缺少任务、步骤或操作者")
|
||||
if decision is TransferManualReviewDecision.APPLIED and result is None:
|
||||
raise ValueError("人工判定已发生时必须提供结果证据")
|
||||
if decision is TransferManualReviewDecision.FAILED:
|
||||
|
||||
@@ -172,7 +172,7 @@ class TransferManualReviewRequest(BaseModel): # type: ignore[misc]
|
||||
decision: Literal["not_applied", "applied"] = Field(
|
||||
description="人工判定;不公开 failed,失败终态只能由 durable 结算写入",
|
||||
)
|
||||
reason: str = Field(min_length=1, max_length=2000, description="人工判定理由")
|
||||
reason: str = Field(default="", max_length=2000, description="人工判定理由,可选")
|
||||
result_payload: Optional[dict[str, JsonData]] = Field(
|
||||
default=None,
|
||||
description="判定为 applied 时必填的外部结果证据",
|
||||
|
||||
@@ -407,7 +407,7 @@ FastAPI 的 HTTP 异常和参数校验异常统一使用 `message`,不再返
|
||||
| POST | `/api/v1/transfer/manual` | 手动整理;请求体可用 `media_source` + `media_id` 指定本次识别与刮削数据源;`logids` 会先还原为同一个显式文件批次,多首音乐因而共享专辑识别上下文;音乐请求未传 `music_type` 时,目录按 `album`、文件按 `recording` 解释;可用最多三项的 `music_release_regions`(ISO 3166-1)和 `music_release_scripts`(ISO 15924)仅覆盖本次 MusicBrainz 发行版本排序,省略时继承系统设置;命中持久失败历史,且未指定媒体身份、未开启 `reorganize` 时,由调度器重试原计划(包括 `logid` 历史入口);显式重整先校验并放弃确定失败任务,再清理旧目标和记录;旧版失败历史仍清理后重试;`reorganize=true` 时清理命中的成功历史和非移动模式旧目标后重新整理 |
|
||||
| GET | `/api/v1/transfer/tasks/manual-reviews` | 管理员分页查询 durable 人工复核任务;`state` 仅允许 `manual_review`(默认)或已经人工判定、等待调度恢复的 `retry_wait`,支持 `page` 与 `page_size`。响应只公开任务、源文件、状态、步骤意图/证据/错误和复核修订号,不返回 lease 或 attempt 身份 |
|
||||
| GET | `/api/v1/transfer/tasks/{task_id}/manual-review` | 管理员查询单个 durable 人工复核任务详情;仅可读取 `manual_review` 或已经人工判定的 `retry_wait` 任务,其余状态按不存在处理 |
|
||||
| POST | `/api/v1/transfer/tasks/{task_id}/manual-review` | 管理员判定处于 `manual_review` 的 durable 整理步骤;请求包含 `operation_id`、`decision=not_applied|applied`、`reason`,`applied` 还必须提供 `result_payload`。`failed` 不属于公开决策,失败终态只能由持租约的 durable 结算写入;响应仅返回任务、操作、决策、后续状态和复核修订号 |
|
||||
| POST | `/api/v1/transfer/tasks/{task_id}/manual-review` | 管理员判定处于 `manual_review` 的 durable 整理步骤;请求包含 `operation_id`、`decision=not_applied|applied`,`reason` 选填,省略或空白时记为空字符串,最多 2000 字符;`applied` 还必须提供 `result_payload`。`failed` 不属于公开决策,失败终态只能由持租约的 durable 结算写入;响应仅返回任务、操作、决策、后续状态和复核修订号 |
|
||||
|
||||
#### 媒体自动分类
|
||||
|
||||
|
||||
@@ -83,8 +83,8 @@ def test_manual_review_endpoints_require_manage_permission(endpoint) -> None:
|
||||
"必须提供 result_payload",
|
||||
),
|
||||
(
|
||||
{"operation_id": "op-1", "decision": "not_applied", "reason": " "},
|
||||
"String should have at least 1 character",
|
||||
{"operation_id": "op-1", "decision": "not_applied", "reason": "x" * 2001},
|
||||
"String should have at most 2000 characters",
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -97,6 +97,18 @@ def test_manual_review_request_rejects_unsafe_decisions(
|
||||
TransferManualReviewRequest.model_validate(payload)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("notes", [{}, {"reason": ""}, {"reason": " "}])
|
||||
def test_manual_review_request_allows_optional_reason(notes: dict) -> None:
|
||||
"""省略、空白或空字符串说明均可提交,空白输入统一为空字符串。"""
|
||||
request = TransferManualReviewRequest.model_validate({
|
||||
"operation_id": "op-1",
|
||||
"decision": "not_applied",
|
||||
**notes,
|
||||
})
|
||||
|
||||
assert request.reason == ""
|
||||
|
||||
|
||||
def test_manual_review_applied_wraps_result_and_hides_internal_state(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
|
||||
@@ -187,14 +187,16 @@ def test_manual_review_list_is_database_paginated(review_store) -> None:
|
||||
("not_applied", None, "failed"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("reason", ["确认文件状态", ""])
|
||||
def test_unknown_manual_review_is_discoverable_and_resumes_via_api(
|
||||
monkeypatch,
|
||||
review_store,
|
||||
decision: str,
|
||||
result_payload: dict[str, bool] | None,
|
||||
expected_step_state: str,
|
||||
reason: str,
|
||||
) -> None:
|
||||
"""UNKNOWN 任务应可发现,人工判定后进入唯一 retry_wait 恢复路径。"""
|
||||
"""复核理由选填;判定后持久化审计身份并进入唯一 retry_wait 恢复路径。"""
|
||||
repository, operation_id = _put_in_manual_review(
|
||||
review_store,
|
||||
task_id=f"task-{decision}",
|
||||
@@ -243,7 +245,7 @@ def test_unknown_manual_review_is_discoverable_and_resumes_via_api(
|
||||
review=TransferManualReviewRequest(
|
||||
operation_id=operation_id,
|
||||
decision=decision,
|
||||
reason=f"reviewed-{decision}",
|
||||
reason=reason,
|
||||
result_payload=result_payload,
|
||||
),
|
||||
current_user=SimpleNamespace(name="admin"),
|
||||
@@ -282,3 +284,6 @@ def test_unknown_manual_review_is_discoverable_and_resumes_via_api(
|
||||
assert pending.lease_owner is None
|
||||
assert pending.lease_token is None
|
||||
assert pending.retry_generation == 1
|
||||
assert pending.reviewed_by == "admin"
|
||||
assert pending.review_decision == decision
|
||||
assert pending.review_reason == reason
|
||||
|
||||
Reference in New Issue
Block a user