mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-01 04:47:03 +08:00
docs: drop process records and internal implementation docs
Keep only install, usage, development-workflow and architecture-boundary documents; remove the one-off refactor plans, audits and ledgers that had already served their purpose. - delete docs/refactor plus the agent API surface audit generator and its drift-only tests; the fixed route registry remains the source of truth - delete internal implementation notes (agent, music, notification, transfer, subscription activity, docker startup, V3t runtime governance) - delete site adapter capture/release shims and the ADR records; external links point at MoviePilot-Site-Collector instead - keep docs/architecture-overview.md and gate its dependency and event figures against the generated fixtures - drop doc-only assertions from the architecture baseline test and retarget remaining references at code and rule documents
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
为宿主源码维护 mypy 存量债务例外清单,并保证新增文件、新增内容清零。
|
||||
|
||||
门禁口径(与 docs/code-quality.md 保持一致):
|
||||
门禁口径(本脚本即为口径来源):
|
||||
|
||||
1. 例外:基线中登记的文件是“当前未完成”的存量债务,按文件 + 错误码记录上限。
|
||||
2. 新增文件:基线未登记的文件必须零诊断,出现任何错误即失败。
|
||||
|
||||
@@ -1,455 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
生成全部 OpenAPI 操作与 Agent 接入范围的对应清单。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
# 独立脚本先加入项目根目录,再加载宿主模块。
|
||||
from app.agent.policy.api import API_OPERATION_ROUTES, API_OPERATION_SPECS # noqa: E402
|
||||
|
||||
# 独立脚本先加入项目根目录,再加载宿主模块。
|
||||
from app.api.apiv1 import api_router # noqa: E402
|
||||
|
||||
JSON_OUTPUT = PROJECT_ROOT / "docs/refactor/agent-api-surface-audit.json"
|
||||
MARKDOWN_OUTPUT = PROJECT_ROOT / "docs/refactor/agent-api-surface-audit.md"
|
||||
HTTP_METHODS = frozenset({"GET", "POST", "PUT", "PATCH", "DELETE"})
|
||||
|
||||
TRANSPORT_TAGS = frozenset(
|
||||
{
|
||||
"agent",
|
||||
"anthropic",
|
||||
"auth",
|
||||
"llm",
|
||||
"login",
|
||||
"mcp",
|
||||
"message",
|
||||
"mfa",
|
||||
"notification",
|
||||
"openai",
|
||||
"user",
|
||||
"webhook",
|
||||
}
|
||||
)
|
||||
CONSOLIDATED_TAGS = frozenset(
|
||||
{
|
||||
"anilist",
|
||||
"bangumi",
|
||||
"discover",
|
||||
"douban",
|
||||
"recommend",
|
||||
"tmdb",
|
||||
}
|
||||
)
|
||||
PROVIDER_PATH_PREFIXES = (
|
||||
"/api/v1/download/",
|
||||
"/api/v1/mediaserver/",
|
||||
)
|
||||
CONSOLIDATED_ROUTE_OWNERS: dict[tuple[str, str], str] = {
|
||||
("POST", "/api/v1/download/"): "download.add",
|
||||
("GET", "/api/v1/plugin/installed"): "plugin.installed",
|
||||
("GET", "/api/v1/plugin/source/{plugin_id}/options"): "plugin.source.options",
|
||||
("GET", "/api/v1/plugin/{plugin_id}"): "plugin.config.get",
|
||||
("GET", "/api/v1/search/last"): "search.results",
|
||||
("GET", "/api/v1/search/media/{media_id}/stream"): "search.torrents",
|
||||
("GET", "/api/v1/search/subtitle/media/{media_id}/stream"): "subtitle.search.media",
|
||||
("GET", "/api/v1/search/subtitle/title/stream"): "subtitle.search.title",
|
||||
("GET", "/api/v1/search/title/stream"): "search.title",
|
||||
("GET", "/api/v1/site/"): "site.list",
|
||||
("GET", "/api/v1/site/cookie/{site_id}"): "site.cookie.update",
|
||||
("GET", "/api/v1/site/domain/{site_url}"): "site.list",
|
||||
("GET", "/api/v1/site/{site_id}"): "site.list",
|
||||
("POST", "/api/v1/storage/list"): "storage.list",
|
||||
("GET", "/api/v1/subscribe/list"): "subscription.list",
|
||||
("GET", "/api/v1/system/env"): "config.system.get",
|
||||
("POST", "/api/v1/system/env"): "config.system.update",
|
||||
("GET", "/api/v1/system/global"): "config.system.get",
|
||||
("GET", "/api/v1/system/setting/{key}"): "config.system.get",
|
||||
("POST", "/api/v1/system/setting/{key}"): "config.system.update",
|
||||
("GET", "/api/v1/transfer/now"): "scheduler.run",
|
||||
("GET", "/api/v1/workflow/"): "workflow.list",
|
||||
("GET", "/api/v1/media/category"): "media.classification.policy.get",
|
||||
("GET", "/api/v1/media/category/config"): "media.classification.policy.get",
|
||||
}
|
||||
STREAM_OR_BINARY_PATHS = frozenset(
|
||||
{
|
||||
"/api/v1/plugin/file/{plugin_id}/{filepath}",
|
||||
"/api/v1/site/icon/{site_id}",
|
||||
"/api/v1/storage/download",
|
||||
"/api/v1/storage/image",
|
||||
"/api/v1/system/cache/image",
|
||||
"/api/v1/system/img/{proxy}",
|
||||
"/api/v1/subscribe/{subscription_id}/logs",
|
||||
"/api/v1/subscribe/{subscription_id}/logs/download",
|
||||
"/api/v1/system/logging",
|
||||
"/api/v1/system/logging/download/{name}",
|
||||
"/api/v1/system/message",
|
||||
"/api/v1/system/notification-image",
|
||||
"/api/v1/system/progress/{process_type}",
|
||||
}
|
||||
)
|
||||
UI_PRESENTATION_PATHS = frozenset(
|
||||
{
|
||||
"/api/v1/plugin/agent_pets",
|
||||
"/api/v1/plugin/dashboard/meta",
|
||||
"/api/v1/plugin/dashboard/{plugin_id}",
|
||||
"/api/v1/plugin/dashboard/{plugin_id}/{key}",
|
||||
"/api/v1/plugin/page/{plugin_id}",
|
||||
"/api/v1/plugin/runtime/{plugin_id}/data/summary",
|
||||
"/api/v1/plugin/sidebar_nav",
|
||||
"/api/v1/rule/custom/reorder",
|
||||
"/api/v1/rule/groups/reorder",
|
||||
"/api/v1/storage/catalog",
|
||||
"/api/v1/storage/options",
|
||||
"/api/v1/transfer/queue/page",
|
||||
}
|
||||
)
|
||||
EXPLICIT_TRANSPORT_PATHS = frozenset(
|
||||
{
|
||||
"/api/v1/plugin/remotes",
|
||||
"/api/v1/subscribe/seerr",
|
||||
"/api/v1/system/ping",
|
||||
}
|
||||
)
|
||||
GITHUB_AUTH_PATH_PREFIXES = (
|
||||
"/api/v1/github/auth/",
|
||||
"/api/v1/login/github-auth/",
|
||||
)
|
||||
SUBSCRIPTION_EXECUTION_UI_PREFIX = "/api/v1/subscribe/execution/"
|
||||
MUSIC_LIBRARY_STATUS_UI_PATH = "/api/v1/music/library/status"
|
||||
|
||||
|
||||
def _gateway_routes() -> dict[tuple[str, str], list[str]]:
|
||||
"""
|
||||
收集确切的 HTTP 路由及使用该路由的所有稳定网关操作。
|
||||
"""
|
||||
results: dict[tuple[str, str], list[str]] = defaultdict(list)
|
||||
for operation_id, route in API_OPERATION_ROUTES.items():
|
||||
results[(route.method.upper(), route.path)].append(operation_id)
|
||||
return {key: sorted(values) for key, values in results.items()}
|
||||
|
||||
|
||||
def _classify(
|
||||
*,
|
||||
method: str,
|
||||
path: str,
|
||||
tags: list[str],
|
||||
gateway_routes: dict[tuple[str, str], list[str]],
|
||||
) -> tuple[str, str, str, list[str]]:
|
||||
"""
|
||||
把 OpenAPI 操作归入经过审查的 Agent 或直接管理端契约。
|
||||
"""
|
||||
operations = gateway_routes.get((method, path), [])
|
||||
if operations:
|
||||
return (
|
||||
"gateway",
|
||||
"moviepilot-api",
|
||||
"Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.",
|
||||
operations,
|
||||
)
|
||||
primary_tag = tags[0] if tags else "untagged"
|
||||
if primary_tag in TRANSPORT_TAGS:
|
||||
return (
|
||||
"transport_or_identity",
|
||||
"host-runtime",
|
||||
"Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.",
|
||||
[],
|
||||
)
|
||||
consolidated_owner = CONSOLIDATED_ROUTE_OWNERS.get((method, path))
|
||||
if consolidated_owner:
|
||||
return (
|
||||
"consolidated",
|
||||
"moviepilot-api",
|
||||
f"This compatibility, broader-response, or UI route is represented by the safer stable operation {consolidated_owner}.",
|
||||
[consolidated_owner],
|
||||
)
|
||||
if path in STREAM_OR_BINARY_PATHS:
|
||||
return (
|
||||
"stream_or_binary",
|
||||
"host-transport",
|
||||
"Streaming, image, archive, or file response consumed by a direct client; the structured JSON Agent gateway does not proxy binary or unbounded streams.",
|
||||
[],
|
||||
)
|
||||
if path in UI_PRESENTATION_PATHS:
|
||||
return (
|
||||
"ui_presentation",
|
||||
"host-ui",
|
||||
"Plugin-rendered page, dashboard, or navigation metadata owned by the frontend presentation contract rather than an Agent business action.",
|
||||
[],
|
||||
)
|
||||
if method == "POST" and path == "/api/v1/history/transfer/{history_id}/cleanup-resolved":
|
||||
return (
|
||||
"ui_presentation",
|
||||
"host-ui",
|
||||
"Manual confirmation of downloader cleanup is owned by the authenticated history management workflow; it is not an Agent action that verifies external cleanup.",
|
||||
[],
|
||||
)
|
||||
if path == "/api/v1/history/transfer/{history_id}/discard-corrupt":
|
||||
return (
|
||||
"ui_presentation",
|
||||
"host-ui",
|
||||
"Discarding corrupt transfer state is owned by the authenticated management recovery workflow; it is not a stable Agent gateway operation.",
|
||||
[],
|
||||
)
|
||||
if path.startswith(SUBSCRIPTION_EXECUTION_UI_PREFIX) or path in {
|
||||
"/api/v1/subscribe/{subscription_id}/activity",
|
||||
"/api/v1/subscribe/{subscription_id}/activity/{event_id}",
|
||||
}:
|
||||
return (
|
||||
"ui_presentation",
|
||||
"host-ui",
|
||||
"Subscription activity details, background execution status and cancellation are owned by the authenticated frontend workflow; they are not yet a stable Agent gateway contract.",
|
||||
[],
|
||||
)
|
||||
if method == "POST" and path == MUSIC_LIBRARY_STATUS_UI_PATH:
|
||||
return (
|
||||
"ui_presentation",
|
||||
"host-ui",
|
||||
"Batch music-library presence is a bounded projection for the authenticated artist resource matrix; it is not a standalone Agent business action.",
|
||||
[],
|
||||
)
|
||||
if path in EXPLICIT_TRANSPORT_PATHS:
|
||||
return (
|
||||
"transport_or_identity",
|
||||
"host-runtime",
|
||||
"Health, bootstrap, federation, or external webhook transport endpoint; it is not recursively callable as an Agent business action.",
|
||||
[],
|
||||
)
|
||||
if method == "GET" and path == "/api/v1/github/community":
|
||||
return (
|
||||
"ui_presentation",
|
||||
"host-ui",
|
||||
"Community medals are a presentation of the administrator-configured GitHub identity, not an Agent business operation.",
|
||||
[],
|
||||
)
|
||||
if path.startswith(GITHUB_AUTH_PATH_PREFIXES):
|
||||
return (
|
||||
"transport_or_identity",
|
||||
"host-runtime",
|
||||
"GitHub Token authorization and first-run credential setup are host identity/configuration flows; they are not Agent business actions.",
|
||||
[],
|
||||
)
|
||||
if path.startswith(PROVIDER_PATH_PREFIXES):
|
||||
return (
|
||||
"provider-skill",
|
||||
"downloader-operation" if "/download/" in path else "mediaserver-operation",
|
||||
"Low-level provider behavior is exposed by the self-describing provider Skill; high-level MoviePilot operations remain in moviepilot-api.",
|
||||
[],
|
||||
)
|
||||
if primary_tag in CONSOLIDATED_TAGS:
|
||||
return (
|
||||
"consolidated",
|
||||
"moviepilot-api",
|
||||
"Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.",
|
||||
[],
|
||||
)
|
||||
if path.endswith("2") or "/schedule2" in path or "/recognize2" in path or "/recognize_file2" in path:
|
||||
return (
|
||||
"alternate-auth-duplicate",
|
||||
"moviepilot-api",
|
||||
"API-token compatibility duplicate; the Agent uses the corresponding bearer-authenticated operation with its persisted user identity.",
|
||||
[],
|
||||
)
|
||||
raise ValueError(f"Unclassified OpenAPI operation: {method} {path}")
|
||||
|
||||
|
||||
def generate_audit() -> dict[str, Any]:
|
||||
"""
|
||||
为每个 v1 OpenAPI HTTP 操作生成顺序稳定的清单条目。
|
||||
"""
|
||||
app = FastAPI()
|
||||
app.include_router(api_router, prefix="/api/v1")
|
||||
openapi = app.openapi()
|
||||
gateway_routes = _gateway_routes()
|
||||
entries = []
|
||||
for path, path_item in sorted(openapi.get("paths", {}).items()):
|
||||
for raw_method, operation in sorted(path_item.items()):
|
||||
method = raw_method.upper()
|
||||
if method not in HTTP_METHODS or not isinstance(operation, dict):
|
||||
continue
|
||||
tags = [str(tag) for tag in operation.get("tags") or []]
|
||||
disposition, owner, reason, operation_ids = _classify(
|
||||
method=method,
|
||||
path=path,
|
||||
tags=tags,
|
||||
gateway_routes=gateway_routes,
|
||||
)
|
||||
entries.append(
|
||||
{
|
||||
"method": method,
|
||||
"path": path,
|
||||
"tags": tags,
|
||||
"summary": str(operation.get("summary") or ""),
|
||||
"disposition": disposition,
|
||||
"owner": owner,
|
||||
"operation_ids": operation_ids,
|
||||
"reason": reason,
|
||||
}
|
||||
)
|
||||
counts = Counter(entry["disposition"] for entry in entries)
|
||||
matched_gateway_routes = {
|
||||
(entry["method"], entry["path"]) for entry in entries if entry["disposition"] == "gateway"
|
||||
}
|
||||
dynamic_gateway_routes = [
|
||||
{
|
||||
"method": method,
|
||||
"path": path,
|
||||
"operation_ids": operation_ids,
|
||||
"reason": (
|
||||
"The executor validates and expands this bounded source placeholder to one of "
|
||||
"tmdb, douban, bangumi, or anilist before calling the corresponding concrete OpenAPI route."
|
||||
),
|
||||
}
|
||||
for (method, path), operation_ids in sorted(gateway_routes.items())
|
||||
if (method, path) not in matched_gateway_routes
|
||||
]
|
||||
return {
|
||||
"openapi_operation_count": len(entries),
|
||||
"gateway_operation_count": len(API_OPERATION_ROUTES),
|
||||
"gateway_http_route_count": len(gateway_routes),
|
||||
"matched_gateway_http_route_count": len(matched_gateway_routes),
|
||||
"dynamic_gateway_routes": dynamic_gateway_routes,
|
||||
"disposition_counts": dict(sorted(counts.items())),
|
||||
"operations": entries,
|
||||
"gateway_identity_contracts": [
|
||||
{
|
||||
"operation_id": spec.operation_id,
|
||||
"method": API_OPERATION_ROUTES[spec.operation_id].method,
|
||||
"path": API_OPERATION_ROUTES[spec.operation_id].path,
|
||||
"identity_scope": spec.identity_scope.value,
|
||||
"required_role": spec.required_role.value,
|
||||
"effect": spec.effect.value,
|
||||
"result_sensitivity": spec.result_sensitivity.value,
|
||||
"confirmation": spec.confirmation.value,
|
||||
}
|
||||
for spec in sorted(API_OPERATION_SPECS, key=lambda item: item.operation_id)
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def render_markdown(audit: dict[str, Any]) -> str:
|
||||
"""
|
||||
将完整清单渲染为可审阅的架构说明文档。
|
||||
"""
|
||||
lines = [
|
||||
"# MoviePilot Agent API Surface Audit",
|
||||
"",
|
||||
"> Generated from the v1 FastAPI OpenAPI document and the fixed Agent API registry.",
|
||||
"> Do not edit route rows manually; run `scripts/generate_agent_api_surface_audit.py`.",
|
||||
"",
|
||||
"## Result",
|
||||
"",
|
||||
f"- OpenAPI HTTP operations: **{audit['openapi_operation_count']}**",
|
||||
f"- Stable `moviepilot_api` operations: **{audit['gateway_operation_count']}**",
|
||||
f"- Exact HTTP routes used by the gateway: **{audit['gateway_http_route_count']}**",
|
||||
f"- OpenAPI routes matched directly by the gateway: **{audit['matched_gateway_http_route_count']}**",
|
||||
f"- Bounded dynamic gateway routes: **{len(audit['dynamic_gateway_routes'])}**",
|
||||
"- Every gateway operation has a generated English oneOf input contract in MCP `tools/list` and `skills/moviepilot-api/SKILL.md`.",
|
||||
"- Every non-gateway OpenAPI operation is listed below with an explicit ownership boundary; it is not silently callable through arbitrary URL/method input.",
|
||||
"",
|
||||
"## Dispositions",
|
||||
"",
|
||||
"| disposition | count | meaning |",
|
||||
"| :--- | ---: | :--- |",
|
||||
]
|
||||
meanings = {
|
||||
"gateway": "Approved structured MoviePilot Agent operation.",
|
||||
"provider-skill": "Low-level downloader or media-server capability owned by a provider Skill.",
|
||||
"consolidated": "Source/UI route represented by a stable aggregate Agent operation.",
|
||||
"alternate-auth-duplicate": "API-token compatibility duplicate of a bearer-authenticated capability.",
|
||||
"transport_or_identity": "Authentication, protocol, callback, account, or conversation transport boundary.",
|
||||
"stream_or_binary": "Streaming or binary response owned by a direct client transport.",
|
||||
"ui_presentation": "Frontend or plugin-rendered presentation contract.",
|
||||
}
|
||||
for disposition, count in audit["disposition_counts"].items():
|
||||
lines.append(f"| `{disposition}` | {count} | {meanings[disposition]} |")
|
||||
lines.extend([
|
||||
"",
|
||||
"## Gateway Identity Audit",
|
||||
"",
|
||||
"All gateway operations are audited below. New operations default to bound_user; safe_read alone never grants public access.",
|
||||
"public: audited external public metadata only; the host-authenticated executor cannot call other operations.",
|
||||
"bound_user: an active account bound to the stable channel sender ID, or the authenticated Web user ID; endpoint ownership and management permissions still apply.",
|
||||
"admin: administrator authority, preserving existing verified channel/API integration semantics; this never substitutes for ownership on bound_user operations.",
|
||||
"Title recognition retains bound_user because its Context exposes locally configured applied identification rules. File recognition, source availability, rules, search results, media libraries, storage and public UI settings remain instance data.",
|
||||
"Plugin ratings include this installation's own rating and require the same administrator role as their REST dependencies. Community share/popularity/statistics and global plugin installation statistics are public reads; follow lists and rating submissions are not.",
|
||||
"",
|
||||
"| operation | method | path | identity scope | role | effect | result sensitivity | confirmation |",
|
||||
"| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |",
|
||||
])
|
||||
for item in audit["gateway_identity_contracts"]:
|
||||
lines.append(
|
||||
f"| `{item['operation_id']}` | `{item['method']}` | `{item['path']}` | "
|
||||
f"`{item['identity_scope']}` | `{item['required_role']}` | `{item['effect']}` | "
|
||||
f"`{item['result_sensitivity']}` | `{item['confirmation']}` |"
|
||||
)
|
||||
if audit["dynamic_gateway_routes"]:
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Bounded Dynamic Routes",
|
||||
"",
|
||||
"| method | route template | operations | constraint |",
|
||||
"| :--- | :--- | :--- | :--- |",
|
||||
]
|
||||
)
|
||||
for item in audit["dynamic_gateway_routes"]:
|
||||
lines.append(
|
||||
f"| `{item['method']}` | `{item['path']}` | {', '.join(item['operation_ids'])} | {item['reason']} |"
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Complete Route Inventory",
|
||||
"",
|
||||
"| method | path | tags | disposition | owner / operation | summary |",
|
||||
"| :--- | :--- | :--- | :--- | :--- | :--- |",
|
||||
]
|
||||
)
|
||||
for item in audit["operations"]:
|
||||
tags = ", ".join(item["tags"]) or "-"
|
||||
owner = ", ".join(item["operation_ids"]) or item["owner"]
|
||||
summary = item["summary"].replace("|", "\\|")
|
||||
lines.append(
|
||||
f"| `{item['method']}` | `{item['path']}` | {tags} | `{item['disposition']}` | {owner} | {summary} |"
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Exposure Rule",
|
||||
"",
|
||||
"Every structured JSON business endpoint is either a stable gateway operation, a provider Skill capability, or an explicitly consolidated compatibility route. Authentication, webhook, stream, binary, and UI-presentation endpoints remain owned by their direct transport or frontend consumer and must not be made recursively callable by the Agent.",
|
||||
"",
|
||||
]
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""
|
||||
写入确定性排序的 JSON 与 Markdown 清单产物。
|
||||
"""
|
||||
audit = generate_audit()
|
||||
JSON_OUTPUT.write_text(
|
||||
json.dumps(audit, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
MARKDOWN_OUTPUT.write_text(render_markdown(audit), encoding="utf-8")
|
||||
print(f"generated {JSON_OUTPUT.relative_to(PROJECT_ROOT)} and {MARKDOWN_OUTPUT.relative_to(PROJECT_ROOT)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user