mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-31 12:24:39 +08:00
feat(plugin): add avatar to agent pet declarations
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
InfinityPacer
parent
25aaf3a32b
commit
e97f64e777
@@ -19,7 +19,7 @@ from app.schemas.plugin import PluginDashboard
|
||||
_AGENT_PET_KEY_PATTERN = re.compile(r"^[a-z0-9_-]{1,32}$")
|
||||
_AGENT_PET_MODES = frozenset({"stage", "renderer"})
|
||||
_AGENT_PET_BUBBLES = frozenset({"host", "self"})
|
||||
# 预览图允许原样透传的绝对地址前缀,其余一律视为相对联邦产物目录的路径
|
||||
# 预览图与头像允许原样透传的绝对地址前缀,其余一律视为相对联邦产物目录的路径
|
||||
_AGENT_PET_ABSOLUTE_PREVIEW_PREFIXES = ("http://", "https://", "data:")
|
||||
|
||||
|
||||
@@ -383,11 +383,12 @@ class PluginProjection:
|
||||
)
|
||||
continue
|
||||
seen.add(item["key"])
|
||||
preview = item.pop("preview")
|
||||
item["preview_url"] = (
|
||||
self._agent_pet_preview_url(plugin_id, plugin, dist_path, preview)
|
||||
if preview else None
|
||||
)
|
||||
for field in ("preview", "avatar"):
|
||||
image = item.pop(field)
|
||||
item[f"{field}_url"] = (
|
||||
self._agent_pet_image_url(plugin_id, plugin, dist_path, image)
|
||||
if image else None
|
||||
)
|
||||
items.append({**base, **item})
|
||||
return items
|
||||
|
||||
@@ -414,7 +415,8 @@ class PluginProjection:
|
||||
api_version = raw.get("api_version", 1)
|
||||
if isinstance(api_version, bool) or not isinstance(api_version, int) or api_version < 1:
|
||||
raise _InvalidAgentPet(f"{key} 的 api_version 必须是正整数:{api_version!r}")
|
||||
preview = PluginProjection._agent_pet_preview(key, raw.get("preview"))
|
||||
preview = PluginProjection._agent_pet_image(key, "preview", raw.get("preview"))
|
||||
avatar = PluginProjection._agent_pet_image(key, "avatar", raw.get("avatar"))
|
||||
bubbles = None
|
||||
random_actions = None
|
||||
if mode == "stage":
|
||||
@@ -433,6 +435,7 @@ class PluginProjection:
|
||||
"component": component.strip().removeprefix("./"),
|
||||
"api_version": api_version,
|
||||
"preview": preview,
|
||||
"avatar": avatar,
|
||||
"bubbles": bubbles,
|
||||
"random_actions": random_actions,
|
||||
}
|
||||
@@ -449,12 +452,12 @@ class PluginProjection:
|
||||
return list(dict.fromkeys(value))
|
||||
|
||||
@staticmethod
|
||||
def _agent_pet_preview(key: str, value: Any) -> Optional[str]:
|
||||
"""校验预览图声明:绝对地址仅允许 http(s) 与 data,相对路径不得越出联邦产物目录。"""
|
||||
def _agent_pet_image(key: str, field: str, value: Any) -> Optional[str]:
|
||||
"""校验预览图或头像声明:绝对地址仅允许 http(s) 与 data,相对路径不得越出联邦产物目录。"""
|
||||
if value is None or value == "":
|
||||
return None
|
||||
if not isinstance(value, str):
|
||||
raise _InvalidAgentPet(f"{key} 的 preview 必须是字符串")
|
||||
raise _InvalidAgentPet(f"{key} 的 {field} 必须是字符串")
|
||||
preview = value.strip()
|
||||
if preview.lower().startswith(_AGENT_PET_ABSOLUTE_PREVIEW_PREFIXES):
|
||||
return preview
|
||||
@@ -464,17 +467,17 @@ class PluginProjection:
|
||||
or "\\" in relative
|
||||
or any(segment in ("", ".", "..") for segment in relative.split("/"))
|
||||
):
|
||||
raise _InvalidAgentPet(f"{key} 的 preview 路径无效:{value!r}")
|
||||
raise _InvalidAgentPet(f"{key} 的 {field} 路径无效:{value!r}")
|
||||
return relative
|
||||
|
||||
def _agent_pet_preview_url(
|
||||
def _agent_pet_image_url(
|
||||
self,
|
||||
plugin_id: str,
|
||||
plugin: Any,
|
||||
dist_path: str,
|
||||
preview: str,
|
||||
) -> str:
|
||||
"""把已校验的预览图解析为可访问 URL。
|
||||
"""把已校验的预览图或头像解析为可访问 URL。
|
||||
|
||||
绝对地址原样透传;相对路径解析到 remoteEntry 所在目录,与 remoteEntry 共用
|
||||
插件静态文件接口、实例 ID 与版本缓存键,分身由静态文件接口映射回源插件目录。
|
||||
|
||||
@@ -567,6 +567,10 @@ class PluginAgentPet(BaseModel): # type: ignore[misc]
|
||||
default=None,
|
||||
description="预览图 URL:相对路径已解析为与 remoteEntry 同源的插件静态文件地址,或原样的 http(s)/data URL",
|
||||
)
|
||||
avatar_url: Optional[str] = Field(
|
||||
default=None,
|
||||
description="方形头像 URL,用于 Agent 面板头部与消息头像;解析规则同 preview_url,未声明为空",
|
||||
)
|
||||
bubbles: Optional[Literal["host", "self"]] = Field(
|
||||
default=None,
|
||||
description="气泡归属,仅 stage 模式:host(宿主按锚点绘制)/ self(插件自绘);renderer 模式为空",
|
||||
|
||||
@@ -214,6 +214,7 @@ class _PluginBase(metaclass=ABCMeta):
|
||||
- component:联邦暴露名,默认 ``AgentPet``(即 ``./AgentPet``)
|
||||
- api_version:形象契约版本,默认 1;前端忽略不认识的版本
|
||||
- preview:预览图,相对插件联邦产物目录的路径,或 ``http(s)://``、``data:`` URL
|
||||
- avatar:方形头像,用于 Agent 面板头部与消息头像,路径规则同 preview
|
||||
- bubbles:仅 stage 模式,``host``(插件上报锚点、宿主画气泡,默认)或 ``self``
|
||||
- random_actions:仅 renderer 模式,宿主随机动作的候选名单
|
||||
|
||||
@@ -224,7 +225,8 @@ class _PluginBase(metaclass=ABCMeta):
|
||||
"key": "chibi",
|
||||
"name": "Q 版看板娘",
|
||||
"mode": "stage",
|
||||
"preview": "preview.png"
|
||||
"preview": "preview.png",
|
||||
"avatar": "avatar.png"
|
||||
}]
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -69,11 +69,17 @@ def _projection(plugins, log=None):
|
||||
|
||||
|
||||
def test_agent_pets_projects_defaults_and_preview_url_next_to_remote_entry():
|
||||
"""合法声明补齐默认值,相对预览图解析到与 remoteEntry 相同的静态目录并带版本。"""
|
||||
"""合法声明补齐默认值,相对预览图与头像解析到与 remoteEntry 相同的静态目录并带版本,缺省为空。"""
|
||||
plugin = _Plugin(
|
||||
plugin_version="1.0.0",
|
||||
get_agent_pets=lambda: [
|
||||
{"key": "chibi", "name": "看板娘", "mode": "stage", "preview": "./img/preview.png"},
|
||||
{
|
||||
"key": "chibi",
|
||||
"name": "看板娘",
|
||||
"mode": "stage",
|
||||
"preview": "./img/preview.png",
|
||||
"avatar": "img/avatar.png",
|
||||
},
|
||||
{
|
||||
"key": "sprite_1",
|
||||
"name": " 精灵 ",
|
||||
@@ -81,6 +87,7 @@ def test_agent_pets_projects_defaults_and_preview_url_next_to_remote_entry():
|
||||
"component": "./SpritePet",
|
||||
"api_version": 2,
|
||||
"preview": "https://example.com/p.png",
|
||||
"avatar": "data:image/png;base64,AAAA",
|
||||
"random_actions": ["wave", "jump", "wave"],
|
||||
},
|
||||
{"key": "plain", "name": "无预览"},
|
||||
@@ -104,6 +111,7 @@ def test_agent_pets_projects_defaults_and_preview_url_next_to_remote_entry():
|
||||
"component": "AgentPet",
|
||||
"api_version": 1,
|
||||
"preview_url": "/plugin/file/petdemo/dist/assets/img/preview.png?v=1.0.0",
|
||||
"avatar_url": "/plugin/file/petdemo/dist/assets/img/avatar.png?v=1.0.0",
|
||||
"bubbles": "host",
|
||||
"random_actions": None,
|
||||
},
|
||||
@@ -118,6 +126,7 @@ def test_agent_pets_projects_defaults_and_preview_url_next_to_remote_entry():
|
||||
"component": "SpritePet",
|
||||
"api_version": 2,
|
||||
"preview_url": "https://example.com/p.png",
|
||||
"avatar_url": "data:image/png;base64,AAAA",
|
||||
"bubbles": None,
|
||||
"random_actions": ["wave", "jump"],
|
||||
},
|
||||
@@ -132,6 +141,7 @@ def test_agent_pets_projects_defaults_and_preview_url_next_to_remote_entry():
|
||||
"component": "AgentPet",
|
||||
"api_version": 1,
|
||||
"preview_url": None,
|
||||
"avatar_url": None,
|
||||
"bubbles": None,
|
||||
"random_actions": None,
|
||||
},
|
||||
@@ -168,6 +178,10 @@ def test_agent_pets_skips_disabled_non_vue_and_unimplemented_plugins():
|
||||
{"key": "trav", "name": "越界", "preview": "../secret.png"},
|
||||
{"key": "abs", "name": "根路径", "preview": "/etc/passwd"},
|
||||
{"key": "js", "name": "脚本", "preview": "javascript:alert(1)"},
|
||||
{"key": "avatar_trav", "name": "头像越界", "avatar": "img/../../secret.png"},
|
||||
{"key": "avatar_abs", "name": "头像根路径", "avatar": "/etc/passwd"},
|
||||
{"key": "avatar_ftp", "name": "头像协议", "avatar": "ftp://example.com/a.png"},
|
||||
{"key": "avatar_type", "name": "头像类型", "avatar": 1},
|
||||
{"key": "acts", "name": "动作", "random_actions": "wave"},
|
||||
"not-a-dict",
|
||||
],
|
||||
@@ -208,7 +222,9 @@ def test_agent_pets_instance_keeps_instance_id_and_source_identity():
|
||||
"""插件分身沿用实例 ID 作为 plugin_id 与预览 URL 路径,source_plugin_id 指向源插件。"""
|
||||
plugin = _Plugin(
|
||||
plugin_source_id="PetDemo",
|
||||
get_agent_pets=lambda: [{"key": "pet", "name": "形象", "preview": "preview.png"}],
|
||||
get_agent_pets=lambda: [
|
||||
{"key": "pet", "name": "形象", "preview": "preview.png", "avatar": "avatar.png"}
|
||||
],
|
||||
)
|
||||
|
||||
pet = _projection({"PetDemoWork": plugin}).agent_pets()[0]
|
||||
@@ -216,6 +232,7 @@ def test_agent_pets_instance_keeps_instance_id_and_source_identity():
|
||||
assert pet["plugin_id"] == "PetDemoWork"
|
||||
assert pet["source_plugin_id"] == "PetDemo"
|
||||
assert pet["preview_url"] == "/plugin/file/petdemowork/dist/assets/preview.png"
|
||||
assert pet["avatar_url"] == "/plugin/file/petdemowork/dist/assets/avatar.png"
|
||||
|
||||
|
||||
def test_agent_pets_isolates_hook_failure_and_non_list_result():
|
||||
|
||||
Reference in New Issue
Block a user