diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 1fcac73d5..105eabf93 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -97,7 +97,7 @@ jobs: - name: Download models.dev catalog id: models_catalog run: | - temp_file=$(mktemp app/agent/llm/models.json.XXXXXX) + temp_file=$(mktemp app/agent/llm/resources/models.json.XXXXXX) trap 'rm -f "$temp_file"' EXIT curl --fail --show-error --silent --location --retry 3 \ --connect-timeout 10 --max-time 120 \ @@ -105,9 +105,9 @@ jobs: jq -e 'type == "object"' "$temp_file" >/dev/null # Git keeps only a small placeholder; the beta image receives the current catalog. chmod 644 "$temp_file" - mv "$temp_file" app/agent/llm/models.json - echo "digest=sha256:$(sha256sum app/agent/llm/models.json | awk '{print $1}')" >> "$GITHUB_OUTPUT" - echo "Downloaded models.dev catalog ($(wc -c < app/agent/llm/models.json) bytes)" + mv "$temp_file" app/agent/llm/resources/models.json + echo "digest=sha256:$(sha256sum app/agent/llm/resources/models.json | awk '{print $1}')" >> "$GITHUB_OUTPUT" + echo "Downloaded models.dev catalog ($(wc -c < app/agent/llm/resources/models.json) bytes)" - name: Docker Meta id: meta diff --git a/.github/workflows/build-v3.yml b/.github/workflows/build-v3.yml index df39c44ef..91775aab2 100644 --- a/.github/workflows/build-v3.yml +++ b/.github/workflows/build-v3.yml @@ -105,7 +105,7 @@ jobs: - name: Download models.dev catalog id: models_catalog run: | - temp_file=$(mktemp app/agent/llm/models.json.XXXXXX) + temp_file=$(mktemp app/agent/llm/resources/models.json.XXXXXX) trap 'rm -f "$temp_file"' EXIT curl --fail --show-error --silent --location --retry 3 \ --connect-timeout 10 --max-time 120 \ @@ -113,9 +113,9 @@ jobs: jq -e 'type == "object"' "$temp_file" >/dev/null # Git keeps only a small placeholder; the release image receives the current catalog. chmod 644 "$temp_file" - mv "$temp_file" app/agent/llm/models.json - echo "digest=sha256:$(sha256sum app/agent/llm/models.json | awk '{print $1}')" >> "$GITHUB_OUTPUT" - echo "Downloaded models.dev catalog ($(wc -c < app/agent/llm/models.json) bytes)" + mv "$temp_file" app/agent/llm/resources/models.json + echo "digest=sha256:$(sha256sum app/agent/llm/resources/models.json | awk '{print $1}')" >> "$GITHUB_OUTPUT" + echo "Downloaded models.dev catalog ($(wc -c < app/agent/llm/resources/models.json) bytes)" - name: Create Release Snapshot id: release_snapshot diff --git a/app/agent/llm/discovery.py b/app/agent/llm/discovery.py index f65847ac5..f65435667 100644 --- a/app/agent/llm/discovery.py +++ b/app/agent/llm/discovery.py @@ -50,7 +50,7 @@ class _ProviderDiscovery: _MODELS_DEV_URL = "https://models.dev/api.json" - _MODELS_DEV_BUNDLED_PATH = Path(__file__).with_name("models.json") + _MODELS_DEV_BUNDLED_PATH = Path(__file__).with_name("resources") / "models.json" _MODELS_DEV_CACHE_TTL = 7 * 24 * 60 * 60 diff --git a/app/agent/llm/models.json b/app/agent/llm/resources/models.json similarity index 100% rename from app/agent/llm/models.json rename to app/agent/llm/resources/models.json diff --git a/app/agent/policy/api_mcp_schema.json b/app/agent/policy/resources/api_mcp_schema.json similarity index 100% rename from app/agent/policy/api_mcp_schema.json rename to app/agent/policy/resources/api_mcp_schema.json diff --git a/app/agent/tools/impl/api.py b/app/agent/tools/impl/api.py index 37061dcef..8c3cabfa3 100644 --- a/app/agent/tools/impl/api.py +++ b/app/agent/tools/impl/api.py @@ -19,7 +19,12 @@ from app.schemas.types import NotificationChannel @lru_cache(maxsize=1) def _load_api_mcp_input_schema() -> dict[str, Any]: """读取由业务 OpenAPI 生成并经漂移测试锁定的外部 MCP schema。""" - schema_path = Path(__file__).resolve().parents[2] / "policy" / "api_mcp_schema.json" + schema_path = ( + Path(__file__).resolve().parents[2] + / "policy" + / "resources" + / "api_mcp_schema.json" + ) payload = json.loads(schema_path.read_text(encoding="utf-8")) if not isinstance(payload, dict) or not isinstance(payload.get("oneOf"), list): raise RuntimeError("moviepilot_api MCP schema 无效") diff --git a/docs/architecture/agent-tool-refactor-plan.md b/docs/architecture/agent-tool-refactor-plan.md index 6c1ef9245..3a045d28c 100644 --- a/docs/architecture/agent-tool-refactor-plan.md +++ b/docs/architecture/agent-tool-refactor-plan.md @@ -296,7 +296,7 @@ action,并使用 MoviePilot 已配置的具体服务实例访问其自身 API - Skill 文档生成器会对数据库说明与 ORM 元数据做双向缺项校验;新增、删除或改名表但未同步说明时生成直接失败,避免文档再次出现漏表或只有字段没有语义的问题 - 系统设置继续由 `config.system.get/update` 统一承载,不恢复旧配置工具;Skill 不再复制易漂移的 `Settings` / `SystemConfigKey` 全量清单,而是要求先动态发现 `definition`(声明类型、当前形状、敏感性、允许操作、列表匹配字段和持久化位置),再按精确键更新 - `Settings` 更新执行类型转换并持久化到 `app.env`;`SystemConfigKey` 通过配置服务写入数据库,保留插件 mutation 门禁、敏感值脱敏和配置变更事件 -- `app/agent/policy/api_mcp_schema.json` 明确为 OpenAPI + 固定 operation 合同生成的 MCP `tools/list` 制品,禁止手工维护;所有 operation、模型和字段必须具备具体英文说明,抽象占位文本或中文说明会由测试拒绝 +- `app/agent/policy/resources/api_mcp_schema.json` 明确为 OpenAPI + 固定 operation 合同生成的 MCP `tools/list` 制品,禁止手工维护;所有 operation、模型和字段必须具备具体英文说明,抽象占位文本或中文说明会由测试拒绝 - API 权限保持双层硬门禁:`moviepilot_api` 按 operation 的 `required_role` 在发起 HTTP 前拒绝非管理员,最终 FastAPI 端点继续用真实当前用户令牌执行 superuser/manage/user 级鉴权;下载器、媒体服务器、数据库外部工具整体为 admin-only - 异步边界复核:内置 Skill 脚本经 asyncio subprocess 执行;外部服务/数据库 MCP 工具把同步脚本放入 downloader、mediaserver、db 分域线程池;`moviepilot_api` 使用异步 HTTP,不阻塞 Agent event loop - 当前阶段进入最终验证:聚焦 Agent/Skill/MCP 回归、全量锁定测试、80% 固定覆盖率 CI、提交推送与远端终态确认 diff --git a/docs/mcp-api.md b/docs/mcp-api.md index 8cd74dd31..ac2382f6d 100644 --- a/docs/mcp-api.md +++ b/docs/mcp-api.md @@ -43,14 +43,14 @@ MCP 当前不会主动发送工具列表变更通知(`listChanged=false`)。 | MCP 工具 | 用途 | 参数合同来源 | | :--- | :--- | :--- | -| `moviepilot_api` | MoviePilot 产品业务 API:媒体、搜索、订阅、下载、整理、站点、存储、调度、工作流、插件、过滤规则和系统配置 | `skills/moviepilot-api/SKILL.md`;运行时 schema 为 `app/agent/policy/api_mcp_schema.json` | +| `moviepilot_api` | MoviePilot 产品业务 API:媒体、搜索、订阅、下载、整理、站点、存储、调度、工作流、插件、过滤规则和系统配置 | `skills/moviepilot-api/SKILL.md`;运行时 schema 为 `app/agent/policy/resources/api_mcp_schema.json` | | `downloader_operation` | qBittorrent、Transmission、rTorrent 原生任务、队列、文件、限速、标签和会话操作 | `skills/downloader-operation/SKILL.md` 与 `skills/downloader-operation/scripts/mp-downloader.py` 的 `ACTIONS` | | `mediaserver_operation` | Emby、Jellyfin、Plex、ZSpace、UGREEN、TrimeMedia、Navidrome 原生媒体库、搜索、播放、扫描和刷新操作 | `skills/mediaserver-operation/SKILL.md` 与 `skills/mediaserver-operation/scripts/mp-mediaserver.py` 的 `ACTIONS` | | `database_operation` | MoviePilot 配置数据库表清单、实时 schema、只读 SQL 和明确授权写入 | `skills/database-operation/SKILL.md` 与 `skills/database-operation/scripts/mp-db.py` 的 `ACTIONS` | 这四个工具都要求管理员级 MCP 集成身份;`tools/list` 的可见性不等于绕过业务权限或写操作确认。下载器和媒体服务器工具会在一次调用内自动选择默认/唯一实例;实例不明确时,错误结果会列出可复用的精确实例名。数据库工具不接受任意连接串或凭据,脚本从 MoviePilot 运行时配置读取数据库连接。 -`app/agent/policy/api_mcp_schema.json` 是 `moviepilot_api` 的生成制品,不是设置项或 API 参数的手工事实源。`scripts/generate_agent_api_mcp_schema.py` 从当前 FastAPI OpenAPI、固定 operation 路由和 Agent 专用英文参数说明生成该文件;运行时直接读取它响应外部 MCP `tools/list`,测试会校验生成结果没有漂移。修改 API、请求模型或 operation 后应重新生成并提交该文件,不应直接编辑 JSON。 +`app/agent/policy/resources/api_mcp_schema.json` 是 `moviepilot_api` 的生成制品,不是设置项或 API 参数的手工事实源。`scripts/generate_agent_api_mcp_schema.py` 从当前 FastAPI OpenAPI、固定 operation 路由和 Agent 专用英文参数说明生成该文件;运行时直接读取它响应外部 MCP `tools/list`,测试会校验生成结果没有漂移。修改 API、请求模型或 operation 后应重新生成并提交该文件,不应直接编辑 JSON。 当前完整 FastAPI OpenAPI 包含 375 个 HTTP 操作,其中 203 个稳定业务操作进入 `moviepilot_api`,使用 201 个固定路由模板:200 条 OpenAPI 路由直接匹配,另有 1 条只允许 diff --git a/scripts/generate_agent_api_mcp_schema.py b/scripts/generate_agent_api_mcp_schema.py index 9c12af105..8e29aeb08 100644 --- a/scripts/generate_agent_api_mcp_schema.py +++ b/scripts/generate_agent_api_mcp_schema.py @@ -17,7 +17,7 @@ from app.agent.policy.api import API_OPERATION_ROUTES, API_OPERATION_SPECS # no from app.agent.policy.mcp import build_api_mcp_input_schema # noqa: E402 from app.api.apiv1 import api_router # noqa: E402 -OUTPUT_PATH = PROJECT_ROOT / "app/agent/policy/api_mcp_schema.json" +OUTPUT_PATH = PROJECT_ROOT / "app/agent/policy/resources/api_mcp_schema.json" def generate_schema() -> dict: diff --git a/scripts/generate_agent_skill_docs.py b/scripts/generate_agent_skill_docs.py index 4cda2a124..661598241 100644 --- a/scripts/generate_agent_skill_docs.py +++ b/scripts/generate_agent_skill_docs.py @@ -161,7 +161,7 @@ DATABASE_TABLE_GUIDES: dict[str, tuple[str, str, str]] = { def _load_json_schema() -> dict[str, Any]: """Load the generated moviepilot_api MCP schema.""" - path = PROJECT_ROOT / "app/agent/policy/api_mcp_schema.json" + path = PROJECT_ROOT / "app/agent/policy/resources/api_mcp_schema.json" payload = json.loads(path.read_text(encoding="utf-8")) if not isinstance(payload, dict): raise ValueError("The API MCP schema must be a JSON object")