mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-03-20 03:57:30 +08:00
feat: 更新下载工具和搜索结果工具的描述,添加可选展示过滤选项参数,优化SKILL.md
This commit is contained in:
@@ -22,7 +22,7 @@ class AddDownloadInput(BaseModel):
|
|||||||
explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context")
|
explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context")
|
||||||
torrent_url: str = Field(
|
torrent_url: str = Field(
|
||||||
...,
|
...,
|
||||||
description="torrent_url in hash:id format (can be obtained from search_torrents tool)"
|
description="torrent_url in hash:id format (obtainable from get_search_results tool per-item results)"
|
||||||
)
|
)
|
||||||
downloader: Optional[str] = Field(None,
|
downloader: Optional[str] = Field(None,
|
||||||
description="Name of the downloader to use (optional, uses default if not specified)")
|
description="Name of the downloader to use (optional, uses default if not specified)")
|
||||||
@@ -34,7 +34,7 @@ class AddDownloadInput(BaseModel):
|
|||||||
|
|
||||||
class AddDownloadTool(MoviePilotTool):
|
class AddDownloadTool(MoviePilotTool):
|
||||||
name: str = "add_download"
|
name: str = "add_download"
|
||||||
description: str = "Add torrent download task to the configured downloader (qBittorrent, Transmission, etc.) using torrent_url reference from search_torrents results."
|
description: str = "Add torrent download task to the configured downloader (qBittorrent, Transmission, etc.) using torrent_url reference from get_search_results results."
|
||||||
args_schema: Type[BaseModel] = AddDownloadInput
|
args_schema: Type[BaseModel] = AddDownloadInput
|
||||||
|
|
||||||
def get_tool_message(self, **kwargs) -> Optional[str]:
|
def get_tool_message(self, **kwargs) -> Optional[str]:
|
||||||
@@ -104,7 +104,7 @@ class AddDownloadTool(MoviePilotTool):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if not torrent_url or not self._is_torrent_ref(torrent_url):
|
if not torrent_url or not self._is_torrent_ref(torrent_url):
|
||||||
return "错误:torrent_url 必须是 search_torrents 返回的 hash:id 引用,请重新搜索后选择。"
|
return "错误:torrent_url 必须是 get_search_results 返回的 hash:id 引用,请先使用 search_torrents 搜索,再通过 get_search_results 筛选后选择。"
|
||||||
|
|
||||||
cached_context = self._resolve_cached_context(torrent_url)
|
cached_context = self._resolve_cached_context(torrent_url)
|
||||||
if not cached_context or not cached_context.torrent_info:
|
if not cached_context or not cached_context.torrent_info:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from app.chain.search import SearchChain
|
|||||||
from app.log import logger
|
from app.log import logger
|
||||||
from ._torrent_search_utils import (
|
from ._torrent_search_utils import (
|
||||||
TORRENT_RESULT_LIMIT,
|
TORRENT_RESULT_LIMIT,
|
||||||
|
build_filter_options,
|
||||||
filter_contexts,
|
filter_contexts,
|
||||||
simplify_search_result,
|
simplify_search_result,
|
||||||
)
|
)
|
||||||
@@ -27,6 +28,7 @@ class GetSearchResultsInput(BaseModel):
|
|||||||
resolution: Optional[List[str]] = Field(None, description="Resolution filters")
|
resolution: Optional[List[str]] = Field(None, description="Resolution filters")
|
||||||
release_group: Optional[List[str]] = Field(None, description="Release group filters")
|
release_group: Optional[List[str]] = Field(None, description="Release group filters")
|
||||||
title_pattern: Optional[str] = Field(None, description="Regular expression pattern to filter torrent titles (e.g., '4K|2160p|UHD', '1080p.*BluRay')")
|
title_pattern: Optional[str] = Field(None, description="Regular expression pattern to filter torrent titles (e.g., '4K|2160p|UHD', '1080p.*BluRay')")
|
||||||
|
show_filter_options: Optional[bool] = Field(False, description="Whether to return only optional filter options for re-checking available conditions")
|
||||||
|
|
||||||
class GetSearchResultsTool(MoviePilotTool):
|
class GetSearchResultsTool(MoviePilotTool):
|
||||||
name: str = "get_search_results"
|
name: str = "get_search_results"
|
||||||
@@ -40,9 +42,22 @@ class GetSearchResultsTool(MoviePilotTool):
|
|||||||
free_state: Optional[List[str]] = None, video_code: Optional[List[str]] = None,
|
free_state: Optional[List[str]] = None, video_code: Optional[List[str]] = None,
|
||||||
edition: Optional[List[str]] = None, resolution: Optional[List[str]] = None,
|
edition: Optional[List[str]] = None, resolution: Optional[List[str]] = None,
|
||||||
release_group: Optional[List[str]] = None, title_pattern: Optional[str] = None,
|
release_group: Optional[List[str]] = None, title_pattern: Optional[str] = None,
|
||||||
|
show_filter_options: bool = False,
|
||||||
**kwargs) -> str:
|
**kwargs) -> str:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"执行工具: {self.name}, 参数: site={site}, season={season}, free_state={free_state}, video_code={video_code}, edition={edition}, resolution={resolution}, release_group={release_group}, title_pattern={title_pattern}")
|
f"执行工具: {self.name}, 参数: site={site}, season={season}, free_state={free_state}, video_code={video_code}, edition={edition}, resolution={resolution}, release_group={release_group}, title_pattern={title_pattern}, show_filter_options={show_filter_options}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
items = await SearchChain().async_last_search_results() or []
|
||||||
|
if not items:
|
||||||
|
return "没有可用的搜索结果,请先使用 search_torrents 搜索"
|
||||||
|
|
||||||
|
if show_filter_options:
|
||||||
|
payload = {
|
||||||
|
"total_count": len(items),
|
||||||
|
"filter_options": build_filter_options(items),
|
||||||
|
}
|
||||||
|
return json.dumps(payload, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
regex_pattern = None
|
regex_pattern = None
|
||||||
if title_pattern:
|
if title_pattern:
|
||||||
@@ -52,11 +67,6 @@ class GetSearchResultsTool(MoviePilotTool):
|
|||||||
logger.warning(f"正则表达式编译失败: {title_pattern}, 错误: {e}")
|
logger.warning(f"正则表达式编译失败: {title_pattern}, 错误: {e}")
|
||||||
return f"正则表达式格式错误: {str(e)}"
|
return f"正则表达式格式错误: {str(e)}"
|
||||||
|
|
||||||
try:
|
|
||||||
items = await SearchChain().async_last_search_results() or []
|
|
||||||
if not items:
|
|
||||||
return "没有可用的搜索结果,请先使用 search_torrents 搜索"
|
|
||||||
|
|
||||||
filtered_items = filter_contexts(
|
filtered_items = filter_contexts(
|
||||||
items=items,
|
items=items,
|
||||||
site=site,
|
site=site,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class QuerySubscribesInput(BaseModel):
|
|||||||
description="Filter subscriptions by status: 'R' for enabled subscriptions, 'S' for paused ones, 'all' for all subscriptions")
|
description="Filter subscriptions by status: 'R' for enabled subscriptions, 'S' for paused ones, 'all' for all subscriptions")
|
||||||
media_type: Optional[str] = Field("all",
|
media_type: Optional[str] = Field("all",
|
||||||
description="Allowed values: movie, tv, all")
|
description="Allowed values: movie, tv, all")
|
||||||
|
tmdb_id: Optional[int] = Field(None, description="Filter by TMDB ID to check if a specific media is already subscribed")
|
||||||
|
|
||||||
|
|
||||||
class QuerySubscribesTool(MoviePilotTool):
|
class QuerySubscribesTool(MoviePilotTool):
|
||||||
@@ -42,19 +43,23 @@ class QuerySubscribesTool(MoviePilotTool):
|
|||||||
|
|
||||||
return " | ".join(parts) if len(parts) > 1 else parts[0]
|
return " | ".join(parts) if len(parts) > 1 else parts[0]
|
||||||
|
|
||||||
async def run(self, status: Optional[str] = "all", media_type: Optional[str] = "all", **kwargs) -> str:
|
async def run(self, status: Optional[str] = "all", media_type: Optional[str] = "all",
|
||||||
logger.info(f"执行工具: {self.name}, 参数: status={status}, media_type={media_type}")
|
tmdb_id: Optional[int] = None, **kwargs) -> str:
|
||||||
|
logger.info(f"执行工具: {self.name}, 参数: status={status}, media_type={media_type}, tmdb_id={tmdb_id}")
|
||||||
try:
|
try:
|
||||||
if media_type not in ["all", "movie", "tv"]:
|
if media_type not in ["all", "movie", "tv"]:
|
||||||
return f"错误:无效的媒体类型 '{media_type}',支持的类型:'movie', 'tv', 'all'"
|
return f"错误:无效的媒体类型 '{media_type}',支持的类型:'movie', 'tv', 'all'"
|
||||||
|
|
||||||
|
media_type_map = {"movie": "电影", "tv": "电视剧"}
|
||||||
subscribe_oper = SubscribeOper()
|
subscribe_oper = SubscribeOper()
|
||||||
subscribes = await subscribe_oper.async_list()
|
subscribes = await subscribe_oper.async_list()
|
||||||
filtered_subscribes = []
|
filtered_subscribes = []
|
||||||
for sub in subscribes:
|
for sub in subscribes:
|
||||||
if status != "all" and sub.state != status:
|
if status != "all" and sub.state != status:
|
||||||
continue
|
continue
|
||||||
if media_type != "all" and sub.type != media_type:
|
if media_type != "all" and sub.type != media_type_map.get(media_type):
|
||||||
|
continue
|
||||||
|
if tmdb_id is not None and sub.tmdbid != tmdb_id:
|
||||||
continue
|
continue
|
||||||
filtered_subscribes.append(sub)
|
filtered_subscribes.append(sub)
|
||||||
if filtered_subscribes:
|
if filtered_subscribes:
|
||||||
|
|||||||
@@ -131,13 +131,13 @@ class MoviePilotToolsManager:
|
|||||||
try:
|
try:
|
||||||
return int(value)
|
return int(value)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
logger.warning(f"无法将参数 {key}='{value}' 转换为整数,保持原值")
|
logger.warning(f"无法将参数 {key}='{value}' 转换为整数,返回 None")
|
||||||
return None
|
return None
|
||||||
if field_type == "number" and isinstance(value, str):
|
if field_type == "number" and isinstance(value, str):
|
||||||
try:
|
try:
|
||||||
return float(value)
|
return float(value)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
logger.warning(f"无法将参数 {key}='{value}' 转换为浮点数,保持原值")
|
logger.warning(f"无法将参数 {key}='{value}' 转换为浮点数,返回 None")
|
||||||
return None
|
return None
|
||||||
if field_type == "boolean":
|
if field_type == "boolean":
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ Always run `show <command>` before calling a command. Do not guess parameter nam
|
|||||||
- **Omitting `sites` uses the user's configured default sites**, not all available sites. Only call `query_sites` and pass `sites=` when the user explicitly asks for a specific site.
|
- **Omitting `sites` uses the user's configured default sites**, not all available sites. Only call `query_sites` and pass `sites=` when the user explicitly asks for a specific site.
|
||||||
- **TMDB season numbers don't always match fan-labeled seasons.** Anime and long-running shows often split one TMDB season into parts. Always validate with `query_media_detail` when the user mentions a specific season.
|
- **TMDB season numbers don't always match fan-labeled seasons.** Anime and long-running shows often split one TMDB season into parts. Always validate with `query_media_detail` when the user mentions a specific season.
|
||||||
- **`add_download` is irreversible without manual cleanup.** Always present torrent details and wait for explicit confirmation before calling it.
|
- **`add_download` is irreversible without manual cleanup.** Always present torrent details and wait for explicit confirmation before calling it.
|
||||||
|
- **`get_search_results` filter params are ANDed.** Combining multiple fields can silently exclude valid results. If results come back empty, drop the most restrictive filter and retry before reporting failure.
|
||||||
- **`volume_factor` and `freedate_diff` indicate promotional status.** `volume_factor` describes the discount type (e.g. `免费` = free download, `2X` = double upload only, `2X免费` = free download + double upload, `普通` = no discount). `freedate_diff` is the remaining free window (e.g. `2天3小时`); empty means no active promotion. Always include both fields when presenting results — they are critical for the user to pick the best-value torrent.
|
- **`volume_factor` and `freedate_diff` indicate promotional status.** `volume_factor` describes the discount type (e.g. `免费` = free download, `2X` = double upload only, `2X免费` = free download + double upload, `普通` = no discount). `freedate_diff` is the remaining free window (e.g. `2天3小时`); empty means no active promotion. Always include both fields when presenting results — they are critical for the user to pick the best-value torrent.
|
||||||
|
|
||||||
## Common Workflows
|
## Common Workflows
|
||||||
@@ -56,15 +57,27 @@ node scripts/mp-cli.js query_sites
|
|||||||
node scripts/mp-cli.js search_torrents tmdb_id=791373 media_type="movie" # use user's default sites
|
node scripts/mp-cli.js search_torrents tmdb_id=791373 media_type="movie" # use user's default sites
|
||||||
node scripts/mp-cli.js search_torrents tmdb_id=791373 media_type="movie" sites='1,3' # override with specific sites
|
node scripts/mp-cli.js search_torrents tmdb_id=791373 media_type="movie" sites='1,3' # override with specific sites
|
||||||
|
|
||||||
# 3. Present available filter options to the user and ask for their preferences
|
# 3. Present ALL available filter_options to the user and ask which ones to apply
|
||||||
# e.g. "Available resolutions: 1080p, 2160p. Release groups: CMCT, PTer. Which do you prefer?"
|
# Show every field and its values — do not pre-select or omit any
|
||||||
|
# e.g. "分辨率: 1080p, 2160p;字幕组: CMCT, PTer;请问需要筛选哪些条件?"
|
||||||
|
|
||||||
# 4. Filter cached results using the user's selected preferences
|
# 4. Filter cached results based on user preferences and your own judgment
|
||||||
node scripts/mp-cli.js get_search_results resolution='2160p'
|
# Filter params are ANDed — if results come back empty, drop the most restrictive field and retry
|
||||||
|
node scripts/mp-cli.js get_search_results resolution='1080p'
|
||||||
|
|
||||||
|
# [Optional] Re-check available filter options from cached results (same shape as search_torrents; returns filter options only)
|
||||||
|
node scripts/mp-cli.js get_search_results show_filter_options=true
|
||||||
|
|
||||||
# 5. Present ALL filtered results as a numbered list — do not pre-select or discard any
|
# 5. Present ALL filtered results as a numbered list — do not pre-select or discard any
|
||||||
# Show for each: index, title, size, seeders, resolution, release group, volume_factor, freedate_diff
|
# Show for each: index, title, size, seeders, resolution, release group, volume_factor, freedate_diff
|
||||||
# Let the user pick by number; only then call add_download
|
# Let the user pick by number; only then proceed to step 6
|
||||||
|
|
||||||
|
# 6. After user confirms selection, check library and subscriptions before downloading
|
||||||
|
node scripts/mp-cli.js query_library_exists tmdb_id=123456 media_type="movie"
|
||||||
|
node scripts/mp-cli.js query_subscribes tmdb_id=123456
|
||||||
|
# If already in library or subscribed, warn the user and ask for confirmation to proceed
|
||||||
|
|
||||||
|
# 7. Add download
|
||||||
node scripts/mp-cli.js add_download torrent_url="..."
|
node scripts/mp-cli.js add_download torrent_url="..."
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -113,7 +126,7 @@ Use `air_date` to find a block of recently-aired episodes that likely correspond
|
|||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
| Error | Resolution |
|
| Error | Resolution |
|
||||||
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| No search results | Retry with an alternative title (e.g. English title). If still empty, ask the user to confirm the title or provide the TMDB ID directly. |
|
| No search results | Retry with an alternative title (e.g. English title). If still empty, ask the user to confirm the title or provide the TMDB ID directly. |
|
||||||
| Download failure | Check downloader status with `query_downloaders`; advise the user to verify storage or downloader health. If these are normal, mention it could be a network error and suggest retrying later. |
|
| Download failure | Run `query_downloaders` to check downloader health, then `query_download_tasks` to check if the task already exists (duplicate tasks are rejected). If both are normal, report findings to the user, suggest checking storage space, and mention it may be a network error — suggest retrying later. |
|
||||||
| Missing configuration | Ask the user for the backend host and API key. Once provided, run `node scripts/mp-cli.js -h <HOST> -k <KEY>` (no command) to save the config persistently — subsequent commands will use it automatically. |
|
| Missing configuration | Ask the user for the backend host and API key. Once provided, run `node scripts/mp-cli.js -h <HOST> -k <KEY>` (no command) to save the config persistently — subsequent commands will use it automatically. |
|
||||||
|
|||||||
Reference in New Issue
Block a user