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:
jxxghp
2026-10-18 14:24:49 +08:00
parent 9edb92620e
commit 2d73cbc43e
48 changed files with 14 additions and 16635 deletions

View File

@@ -1,7 +1,7 @@
"""
为宿主源码维护 mypy 存量债务例外清单,并保证新增文件、新增内容清零。
门禁口径(与 docs/code-quality.md 保持一致):
门禁口径(本脚本即为口径来源):
1. 例外:基线中登记的文件是“当前未完成”的存量债务,按文件 + 错误码记录上限。
2. 新增文件:基线未登记的文件必须零诊断,出现任何错误即失败。

View File

@@ -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())