Files
MoviePilot/skills/feedback-issue/scripts/prepare_feedback_issue.py
T

194 lines
6.5 KiB
Python

"""校验并生成 feedback-issue 提交前的预览与 payload 文件。"""
from __future__ import annotations
import argparse
from pathlib import Path
from typing import Any, Optional
from feedback_issue_common import (
ALLOWED_ENVIRONMENTS,
ALLOWED_ISSUE_TYPES,
FEEDBACK_REPO,
MAX_PREVIEW_LOGS_CHARS,
MAX_TITLE_CHARS,
build_issue_body,
check_content_quality,
format_doctor_summary,
format_log_selection,
load_diagnostics_logs,
normalize_target_repo,
read_json_file,
result_payload,
runtime_file,
sanitize_logs,
truncate,
validate_enum,
validate_target_repo_for_issue,
write_json_file,
)
REQUIRED_DRAFT_FIELDS = (
"title",
"version",
"environment",
"issue_type",
"description",
"original_user_request",
"diagnostics_file",
)
def normalize_draft(raw: dict[str, Any]) -> tuple[dict[str, Any], list[str]]:
"""规范化草稿字段并返回缺失字段列表。"""
draft = {key: str(raw.get(key) or "").strip() for key in REQUIRED_DRAFT_FIELDS}
missing = [key for key, value in draft.items() if not value]
draft["title"] = truncate(draft["title"], MAX_TITLE_CHARS, marker="...")
draft["target_repo"] = str(raw.get("target_repo") or FEEDBACK_REPO).strip()
return draft, missing
def validate_draft(draft: dict[str, Any], logs: str) -> Optional[str]:
"""校验草稿枚举和内容质量,返回错误信息或 None。"""
for value, allowed, field_name in (
(draft["environment"], ALLOWED_ENVIRONMENTS, "environment"),
(draft["issue_type"], ALLOWED_ISSUE_TYPES, "issue_type"),
):
error = validate_enum(value, allowed, field_name)
if error:
return error
repo_error = validate_target_repo_for_issue(draft["issue_type"], draft["target_repo"])
if repo_error:
return repo_error
return check_content_quality(
title=draft["title"],
description=draft["description"],
original_user_request=draft["original_user_request"],
logs=logs,
issue_type=draft["issue_type"],
)
def build_preview_text(draft: dict[str, Any], logs: str, diagnostics: dict[str, Any]) -> str:
"""构造给用户确认的 Markdown 预览文本。"""
preview_logs = sanitize_logs(logs, MAX_PREVIEW_LOGS_CHARS) or "会话中未捕获到相关后端日志。"
doctor_summary = format_doctor_summary(diagnostics.get("doctor"))
log_selection_summary = format_log_selection(diagnostics.get("log_selection"))
source_files = diagnostics.get("source_files") or []
sources = "\n".join(f"- {item}" for item in source_files) or "- 未命中具体日志文件"
return (
"请确认是否提交以下问题反馈:\n\n"
f"标题:{draft['title']}\n"
f"目标仓库:{draft['target_repo']}\n"
f"版本:{draft['version']}\n"
f"环境:{draft['environment']}\n"
f"类型:{draft['issue_type']}\n\n"
"诊断来源:\n"
f"{sources}\n\n"
"Doctor 摘要:\n"
f"```text\n{doctor_summary}\n```\n\n"
"日志筛选依据:\n"
f"```text\n{log_selection_summary}\n```\n\n"
"问题描述:\n"
f"{draft['description'].strip()}\n\n"
"日志预览(已脱敏):\n"
f"```bash\n{preview_logs}\n```\n\n"
"如内容无误,请回复「确认」;如需调整,请回复「修改:...」。"
)
def prepare_issue(draft_file: str | Path) -> dict[str, Any]:
"""读取草稿 JSON,校验后写出 payload 与 preview 文件。"""
raw = read_json_file(draft_file)
draft, missing = normalize_draft(raw)
if missing:
return {
"success": False,
"reason": "missing_fields",
"message": f"草稿缺少必填字段:{', '.join(missing)}",
}
try:
draft["target_repo"] = normalize_target_repo(draft["target_repo"])
except ValueError as err:
return {
"success": False,
"reason": "invalid_target_repo",
"message": str(err),
}
try:
logs, diagnostics = load_diagnostics_logs(draft["diagnostics_file"])
except Exception as err:
return {
"success": False,
"reason": "diagnostics_missing",
"message": f"无法读取诊断日志文件:{err}",
}
error = validate_draft(draft, logs)
if error:
return {
"success": False,
"reason": "invalid_draft",
"message": error,
}
payload = {
**draft,
"diagnostics_file": str(draft["diagnostics_file"]),
}
payload_file = runtime_file("payload", ".json")
preview_file = runtime_file("preview", ".md")
write_json_file(payload_file, payload)
preview_text = build_preview_text(draft, logs, diagnostics)
preview_file.write_text(preview_text, encoding="utf-8")
combined_logs = "\n\n".join(
part for part in (
f"### Doctor 摘要\n{format_doctor_summary(diagnostics.get('doctor'))}",
f"### 日志筛选依据\n{format_log_selection(diagnostics.get('log_selection'))}",
logs,
) if part
)
body_preview = build_issue_body(
version=draft["version"],
environment=draft["environment"],
issue_type=draft["issue_type"],
description=draft["description"],
logs=combined_logs,
target_repo=draft["target_repo"],
)
return {
"success": True,
"target_repo": draft["target_repo"],
"payload_file": str(payload_file),
"preview_file": str(preview_file),
"body_chars": len(body_preview),
"log_bytes": len(logs.encode("utf-8", errors="replace")),
"log_lines": len(logs.splitlines()) if logs else 0,
"message": (
"已生成 Issue 预览和提交 payload。请把 preview_file 内容完整展示给用户,"
"等待明确「确认」后再调用 submit_feedback_issue.py。"
),
}
def parse_args() -> argparse.Namespace:
"""解析命令行参数。"""
parser = argparse.ArgumentParser(description="生成 MoviePilot 反馈 Issue 预览")
parser.add_argument("--draft-file", required=True, help="包含 Issue 草稿字段的 JSON 文件")
return parser.parse_args()
def main() -> int:
"""脚本入口:校验草稿并输出 JSON 结果。"""
args = parse_args()
result = prepare_issue(args.draft_file)
print(result_payload(**result))
return 0 if result.get("success") else 2
if __name__ == "__main__":
raise SystemExit(main())