From 4828fe52d7a25badfd77438401dad01730f24e74 Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Sun, 16 Aug 2026 00:43:48 +0800 Subject: [PATCH 1/3] refactor: remove meme plugin v2 shell --- .github/workflows/plugin-api-v3.yml | 2 +- README.md | 4 +- plugin.py | 71 ------ tests/test_plugin.py | 373 +++++++--------------------- 4 files changed, 90 insertions(+), 360 deletions(-) diff --git a/.github/workflows/plugin-api-v3.yml b/.github/workflows/plugin-api-v3.yml index 5f8f753..e2a40c6 100644 --- a/.github/workflows/plugin-api-v3.yml +++ b/.github/workflows/plugin-api-v3.yml @@ -49,7 +49,7 @@ jobs: cache-dependency-path: .akashic-core/requirements.txt - name: Install pinned Core dependencies run: python -m pip install -r .akashic-core/requirements.txt pytest pytest-asyncio - - name: Compare v2 and v3 Meme receipts + - name: Verify Meme v3 behavior env: AKASHIC_AGENT_ROOT: .akashic-core AKASHIC_CITATION_ROOT: .citation diff --git a/README.md b/README.md index fe20f82..a1661a7 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ | `dashboard_module = "dashboard.py"` | 声明 v3 Dashboard | | `workspace_roots = ("memes",)` | 取得 Core 分配的表情包资产根 | -插件通过模块命名导出声明静态贡献,通过 `apply(ctx, config)` 自行构建 `MemeCatalog`/`MemeDecorator` 并注册接入点。`citation.protocol` 是硬依赖:Citation 先剥离 cited metadata 并保留 meme tag,Meme 再完成媒体装饰,Citation cleanup 最后清除残留协议标签。Core 只分配生命周期、依赖顺序与 workspace root;Meme 的目录结构、随机选图和 Dashboard 仍由插件拥有。旧 `MemePlugin` 暂时保留,只用于迁移期差分验证。 +插件通过模块命名导出声明静态贡献,通过 `apply(ctx, config)` 自行构建 `MemeCatalog`/`MemeDecorator` 并注册接入点。`citation.protocol` 是硬依赖:Citation 先剥离 cited metadata 并保留 meme tag,Meme 再完成媒体装饰,Citation cleanup 最后清除残留协议标签。Core 只分配生命周期、依赖顺序与 workspace root;Meme 的目录结构、随机选图和 Dashboard 仍由插件拥有。 --- @@ -24,7 +24,7 @@ 从工作区路径(`workspace/memes/`)加载 `manifest.json`,构建 `MemeCatalog` 和 `MemeDecorator` 实例。`MemeCatalog` 按需检测 manifest 的 mtime,变动时自动热重载,不需要重启。 -### 2. 注入 catalog(MemePromptModule) +### 2. 注入 catalog 每轮推理前,调用 `catalog.build_prompt_block()` 把启用的表情包类别(名称、描述、别名)拼成文本块,追加到系统 prompt 底部,告知 LLM 可以在回复中嵌入 `` 标签。如果 catalog 为空则跳过注入。 diff --git a/plugin.py b/plugin.py index 7f6e1f6..fb579f1 100644 --- a/plugin.py +++ b/plugin.py @@ -1,8 +1,6 @@ from __future__ import annotations import re -from pathlib import Path -from typing import Any, cast from agent.lifecycle.composition import ( AFTER_REASONING_PREPROCESS_EVENT, @@ -10,11 +8,9 @@ ) from agent.lifecycle.types import AfterReasoningCtx, PromptRenderCtx from agent.plugin_composition import Context, ServiceKey -from agent.plugins import Plugin, on_after_reasoning from agent.prompting import PromptSectionRender from .runtime import MemeCatalog, MemeDecorator -_CTX_SLOT = "prompt:ctx" _MEME_RE = re.compile( r"(?(?!`)", re.IGNORECASE, @@ -44,22 +40,6 @@ def decorate_meme_ctx(ctx: AfterReasoningCtx, decorator: MemeDecorator) -> None: ctx.meme_tag = decorated.tag -class MemePromptModule: - slot = "meme.prompt" - requires = ("prompt_render.emit", "citation.prompt", _CTX_SLOT) - produces = (_CTX_SLOT,) - - def __init__(self, plugin: "MemePlugin") -> None: - self._plugin = plugin - - async def run(self, frame: Any) -> Any: - ctx = frame.slots.get(_CTX_SLOT) - if not isinstance(ctx, PromptRenderCtx): - return frame - append_meme_prompt(ctx, self._plugin.catalog) - return frame - - api_version = 3 name = "meme" version = "1.0.0" @@ -88,51 +68,6 @@ def answer_listener(answer: AfterReasoningCtx) -> None: _ = await ctx.on(AFTER_REASONING_PREPROCESS_EVENT, answer_listener) -class MemePlugin(Plugin): - api_version = 2 - - @classmethod - def dashboard_module(cls) -> str | None: - return "dashboard.py" - - name = "meme" - version = "1.0.0" - - @classmethod - def skill_roots(cls) -> tuple[str, ...]: - return ("skills",) - - _catalog: Any = None - _decorator: Any = None - - async def prepare(self) -> None: - memes_dir = ( - _workspace(self.context.plugin_dir, self.context.workspace) / "memes" - ) - self._catalog = MemeCatalog(memes_dir) - self._decorator = MemeDecorator(self._catalog) - - def prompt_render_modules(self) -> list[object]: - return [MemePromptModule(self)] - - @on_after_reasoning() - async def decorate_meme(self, ctx: AfterReasoningCtx) -> AfterReasoningCtx: - decorate_meme_ctx(ctx, self.decorator) - return ctx - - @property - def catalog(self) -> Any: - if self._catalog is None: - raise RuntimeError("meme 插件尚未初始化") - return self._catalog - - @property - def decorator(self) -> Any: - if self._decorator is None: - raise RuntimeError("meme 插件尚未初始化") - return self._decorator - - def _extract_meme_tag(response: str) -> tuple[str, str | None]: match = _MEME_RE.search(response) if match is None: @@ -141,9 +76,3 @@ def _extract_meme_tag(response: str) -> tuple[str, str | None]: cleaned = re.sub(r"[ \t]+\n", "\n", cleaned) cleaned = re.sub(r" {2,}", " ", cleaned) return cleaned.strip(), match.group(1).lower() - - -def _workspace(plugin_dir: Path, configured: Path | None) -> Path: - if configured is not None: - return configured - return cast(Path, plugin_dir.parent.parent) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 16c36d1..1da9cd6 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -1,14 +1,13 @@ from __future__ import annotations -import json import importlib import importlib.util +import json import os -import shutil -from datetime import datetime, timezone -from types import SimpleNamespace from pathlib import Path +import shutil import sys +from datetime import datetime, timezone import pytest from fastapi import FastAPI @@ -28,10 +27,8 @@ PluginRuntime, ) from agent.plugins.composable import ComposablePlugin -from agent.plugins.context import PluginContext, PluginKVStore from agent.plugins.dashboard_host import DashboardBinding, PluginDashboardHost from agent.plugins.manager import PluginManager -from agent.plugins.scope import PluginScope, ScopedEventBus from bus.event_bus import EventBus from runtime import MemeCatalog, MemeDecorator @@ -52,10 +49,9 @@ def _load_meme_plugin_module(): _meme_plugin_module = _load_meme_plugin_module() -MemePlugin = _meme_plugin_module.MemePlugin -MemePromptModule = _meme_plugin_module.MemePromptModule CITATION_PROTOCOL_SERVICE = _meme_plugin_module.CITATION_PROTOCOL_SERVICE apply = _meme_plugin_module.apply +decorate_meme_ctx = _meme_plugin_module.decorate_meme_ctx inject = _meme_plugin_module.inject @@ -85,27 +81,39 @@ def _write_meme_workspace(workspace: Path) -> Path: return image -async def _make_plugin(tmp_path: Path) -> MemePlugin: - plugin_dir = tmp_path / "plugin" - plugin_dir.mkdir(parents=True) - scope = PluginScope("meme") - plugin = MemePlugin() - plugin.context = PluginContext( - event_bus=ScopedEventBus(EventBus(), scope), - tool_registry=None, - plugin_id="meme", - plugin_dir=plugin_dir, - data_dir=tmp_path, - kv_store=PluginKVStore(plugin_dir / ".kv.json"), - workspace=tmp_path, - scope=scope, +def _prompt_ctx() -> PromptRenderCtx: + return PromptRenderCtx( + session_key="webui:1", + channel="webui", + chat_id="1", + content="你好", + media=None, + timestamp=datetime.now(timezone.utc), + history=[], + skill_names=[], + retrieved_memory_block="", + disabled_sections=set(), + turn_injection_prompt="", + ) + + +def _answer_ctx(reply: str) -> AfterReasoningCtx: + return AfterReasoningCtx( + session_key="webui:1", + channel="webui", + chat_id="1", + tools_used=(), + thinking=None, + response_metadata=ResponseMetadata(raw_text=reply), + streamed=False, + tool_chain=(), + context_retry={}, + reply=reply, ) - await plugin.prepare() - return plugin def test_catalog_builds_prompt_block(tmp_path: Path) -> None: - _write_meme_workspace(tmp_path) + _ = _write_meme_workspace(tmp_path) block = MemeCatalog(tmp_path / "memes").build_prompt_block() assert block is not None assert "" in block @@ -122,120 +130,55 @@ def test_decorator_picks_image_for_tag(tmp_path: Path) -> None: assert result.media == [str(image)] -@pytest.mark.asyncio -async def test_meme_prompt_module_injects_bottom_section(tmp_path: Path) -> None: - _write_meme_workspace(tmp_path) - plugin = await _make_plugin(tmp_path) - module = plugin.prompt_render_modules()[0] - assert isinstance(module, MemePromptModule) - ctx = PromptRenderCtx( - session_key="telegram:1", - channel="telegram", - chat_id="1", - content="你好", - media=None, - timestamp=datetime.now(timezone.utc), - history=[], - skill_names=[], - retrieved_memory_block="", - disabled_sections=set(), - turn_injection_prompt="", - ) - frame = SimpleNamespace(slots={"prompt:ctx": ctx}) - await module.run(frame) - assert ctx.system_sections_bottom[0].name == "memes" - - -@pytest.mark.asyncio -async def test_meme_plugin_decorates_after_reasoning(tmp_path: Path) -> None: +def test_decorate_meme_ctx_updates_answer_metadata(tmp_path: Path) -> None: image = _write_meme_workspace(tmp_path) - plugin = await _make_plugin(tmp_path) - ctx = AfterReasoningCtx( - session_key="telegram:1", - channel="telegram", - chat_id="1", - tools_used=(), - thinking=None, - response_metadata=ResponseMetadata(raw_text="好的 "), - streamed=False, - tool_chain=(), - context_retry={}, - reply="好的 ", - ) - out = await plugin.decorate_meme(ctx) - assert out.reply == "好的" - assert out.media == [str(image)] - assert out.meme_tag == "shy" + ctx = _answer_ctx("好的 ") + decorate_meme_ctx(ctx, MemeDecorator(MemeCatalog(tmp_path / "memes"))) + assert ctx.reply == "好的" + assert ctx.media == [str(image)] + assert ctx.meme_tag == "shy" -@pytest.mark.asyncio -async def test_meme_plugin_accepts_inline_tag(tmp_path: Path) -> None: +def test_decorate_meme_ctx_accepts_inline_tag(tmp_path: Path) -> None: image = _write_meme_workspace(tmp_path) - plugin = await _make_plugin(tmp_path) - ctx = AfterReasoningCtx( - session_key="telegram:1", - channel="telegram", - chat_id="1", - tools_used=(), - thinking=None, - response_metadata=ResponseMetadata(raw_text="快了 \n\n马上到了"), - streamed=False, - tool_chain=(), - context_retry={}, - reply="快了 \n\n马上到了", - ) - out = await plugin.decorate_meme(ctx) - assert out.reply == "快了\n\n马上到了" - assert out.media == [str(image)] - assert out.meme_tag == "shy" + ctx = _answer_ctx("快了 \n\n马上到了") + decorate_meme_ctx(ctx, MemeDecorator(MemeCatalog(tmp_path / "memes"))) + assert ctx.reply == "快了\n\n马上到了" + assert ctx.media == [str(image)] + assert ctx.meme_tag == "shy" -@pytest.mark.asyncio -async def test_meme_plugin_ignores_code_tag(tmp_path: Path) -> None: - _write_meme_workspace(tmp_path) - plugin = await _make_plugin(tmp_path) - ctx = AfterReasoningCtx( - session_key="telegram:1", - channel="telegram", - chat_id="1", - tools_used=(), - thinking=None, - response_metadata=ResponseMetadata( - raw_text="应该是 ``。\n\n<æm>shy" - ), - streamed=False, - tool_chain=(), - context_retry={}, - reply="应该是 ``。\n\n<æm>shy", - ) - out = await plugin.decorate_meme(ctx) - assert out.reply == "应该是 ``。\n\n<æm>shy" - assert out.media == [] - assert out.meme_tag is None +def test_decorate_meme_ctx_ignores_code_tag(tmp_path: Path) -> None: + _ = _write_meme_workspace(tmp_path) + ctx = _answer_ctx("应该是 ``。\n\n<æm>shy") + decorate_meme_ctx(ctx, MemeDecorator(MemeCatalog(tmp_path / "memes"))) + assert ctx.reply == "应该是 ``。\n\n<æm>shy" + assert ctx.media == [] + assert ctx.meme_tag is None @pytest.mark.asyncio -async def test_v3_named_exports_match_legacy_behavior(tmp_path: Path) -> None: +async def test_v3_named_exports_run_complete_lifecycle_behavior( + tmp_path: Path, +) -> None: image = _write_meme_workspace(tmp_path) - legacy = await _make_plugin(tmp_path) composable = ComposablePlugin.from_module(_meme_plugin_module) assert composable.skill_roots == ("skills",) assert composable.dashboard_module == "dashboard.py" assert composable.workspace_roots == ("memes",) - root = CompositionRoot("meme-parity") + root = CompositionRoot("meme-v3") _ = await root.context.provide(CITATION_PROTOCOL_SERVICE, object()) async def mount(ctx: Context) -> None: await apply(ctx, object()) - plugin_dir = Path(__file__).parents[1] _ = await root.mount( mount, name="meme", inject=inject, runtime=PluginRuntime( plugin_id="meme", - plugin_dir=plugin_dir, + plugin_dir=Path(__file__).parents[1], data_dir=tmp_path / "plugin-data", workspace=tmp_path, config=object(), @@ -247,68 +190,16 @@ async def mount(ctx: Context) -> None: assert receipt.writes == () assert receipt.external_effects == () - legacy_prompt = PromptRenderCtx( - session_key="telegram:1", - channel="telegram", - chat_id="1", - content="你好", - media=None, - timestamp=datetime.now(timezone.utc), - history=[], - skill_names=[], - retrieved_memory_block="", - disabled_sections=set(), - turn_injection_prompt="", - ) - await legacy.prompt_render_modules()[0].run( - SimpleNamespace(slots={"prompt:ctx": legacy_prompt}) - ) - v3_prompt = PromptRenderCtx( - session_key="telegram:1", - channel="telegram", - chat_id="1", - content="你好", - media=None, - timestamp=legacy_prompt.timestamp, - history=[], - skill_names=[], - retrieved_memory_block="", - disabled_sections=set(), - turn_injection_prompt="", - ) - _ = await root.context.serial(PROMPT_RENDER_EVENT, v3_prompt) - assert v3_prompt.system_sections_bottom == legacy_prompt.system_sections_bottom + prompt = _prompt_ctx() + _ = await root.context.serial(PROMPT_RENDER_EVENT, prompt) + assert [section.name for section in prompt.system_sections_bottom] == ["memes"] - legacy_answer = AfterReasoningCtx( - session_key="telegram:1", - channel="telegram", - chat_id="1", - tools_used=(), - thinking=None, - response_metadata=ResponseMetadata(raw_text="好的 "), - streamed=False, - tool_chain=(), - context_retry={}, - reply="好的 ", - ) - await legacy.decorate_meme(legacy_answer) - v3_answer = AfterReasoningCtx( - session_key="telegram:1", - channel="telegram", - chat_id="1", - tools_used=(), - thinking=None, - response_metadata=ResponseMetadata(raw_text="好的 "), - streamed=False, - tool_chain=(), - context_retry={}, - reply="好的 ", - ) - _ = await root.context.serial(AFTER_REASONING_PREPROCESS_EVENT, v3_answer) + answer = _answer_ctx("好的 ") + _ = await root.context.serial(AFTER_REASONING_PREPROCESS_EVENT, answer) + assert answer.reply == "好的" + assert answer.media == [str(image)] + assert answer.meme_tag == "shy" - assert v3_answer.reply == legacy_answer.reply == "好的" - assert v3_answer.media == legacy_answer.media == [str(image)] - assert v3_answer.meme_tag == legacy_answer.meme_tag == "shy" await root.dispose() assert root.receipt().effects == () assert root.topology_view().listeners == () @@ -358,32 +249,9 @@ async def mount(ctx: Context) -> None: workspace_roots=("memes",), ), ) - prompt = PromptRenderCtx( - session_key="webui:1", - channel="webui", - chat_id="1", - content="你好", - media=None, - timestamp=datetime.now(timezone.utc), - history=[], - skill_names=[], - retrieved_memory_block="", - disabled_sections=set(), - turn_injection_prompt="", - ) + prompt = _prompt_ctx() _ = await root.context.serial(PROMPT_RENDER_EVENT, prompt) - answer = AfterReasoningCtx( - session_key="webui:1", - channel="webui", - chat_id="1", - tools_used=(), - thinking=None, - response_metadata=ResponseMetadata(raw_text="好的 "), - streamed=False, - tool_chain=(), - context_retry={}, - reply="好的 ", - ) + answer = _answer_ctx("好的 ") _ = await root.context.serial(AFTER_REASONING_PREPROCESS_EVENT, answer) dashboard_module = importlib.import_module("test_meme_plugin.dashboard") @@ -424,7 +292,7 @@ async def mount(ctx: Context) -> None: async def test_v3_plugin_loads_package_and_dashboard_through_real_manager( tmp_path: Path, ) -> None: - _write_meme_workspace(tmp_path / "workspace") + _ = _write_meme_workspace(tmp_path / "workspace") plugin_home = tmp_path / "plugins" citation_dir = plugin_home / "citation" citation_dir.mkdir(parents=True) @@ -465,6 +333,7 @@ async def test_v3_plugin_loads_package_and_dashboard_through_real_manager( assert generation.instance.workspace_roots == ("memes",) assert snapshot.plugin_skill_index is not None assert "meme-manage" in snapshot.plugin_skill_index.records + dashboard = PluginDashboardHost( workspace=workspace, memory_admin=object(), @@ -484,6 +353,7 @@ async def test_v3_plugin_loads_package_and_dashboard_through_real_manager( if route.path == "/api/dashboard/meme/categories" )() assert categories["categories"][0]["tag"] == "shy" + root = snapshot.composition_root assert root is not None await manager.terminate_all() @@ -492,63 +362,16 @@ async def test_v3_plugin_loads_package_and_dashboard_through_real_manager( @pytest.mark.asyncio -async def test_citation_meme_cross_repository_parity(tmp_path: Path) -> None: +async def test_citation_meme_cross_repository_v3_behavior(tmp_path: Path) -> None: raw_citation_root = os.environ.get("AKASHIC_CITATION_ROOT", "").strip() if not raw_citation_root: raise RuntimeError( "AKASHIC_CITATION_ROOT 必须指向 exact-commit Citation checkout" ) citation_root = Path(raw_citation_root) - citation_spec = importlib.util.spec_from_file_location( - "test_citation_plugin", - citation_root / "plugin.py", - ) - if citation_spec is None or citation_spec.loader is None: - raise ImportError(str(citation_root / "plugin.py")) - citation_module = importlib.util.module_from_spec(citation_spec) - sys.modules[citation_spec.name] = citation_module - citation_spec.loader.exec_module(citation_module) workspace = tmp_path / "workspace" image = _write_meme_workspace(workspace) - legacy_meme = await _make_plugin(workspace) - legacy_prompt = PromptRenderCtx( - session_key="telegram:1", - channel="telegram", - chat_id="1", - content="你好", - media=None, - timestamp=datetime.now(timezone.utc), - history=[], - skill_names=[], - retrieved_memory_block="", - disabled_sections=set(), - turn_injection_prompt="", - ) - await citation_module.CitationPromptModule().run( - SimpleNamespace(slots={"prompt:ctx": legacy_prompt}) - ) - await legacy_meme.prompt_render_modules()[0].run( - SimpleNamespace(slots={"prompt:ctx": legacy_prompt}) - ) - reply = "答复正文\n§cited:[mem_1]§ " - legacy_answer = AfterReasoningCtx( - session_key="telegram:1", - channel="telegram", - chat_id="1", - tools_used=(), - thinking=None, - response_metadata=ResponseMetadata(raw_text=reply), - streamed=False, - tool_chain=(), - context_retry={}, - reply=reply, - ) - legacy_frame = SimpleNamespace(slots={"reasoning:ctx": legacy_answer}) - await citation_module.CitationAfterReasoningModule().run(legacy_frame) - await legacy_meme.decorate_meme(legacy_answer) - await citation_module.ProtocolTagCleanupModule().run(legacy_frame) - plugin_home = tmp_path / "plugins" _ = shutil.copytree( citation_root, @@ -571,49 +394,27 @@ async def test_citation_meme_cross_repository_parity(tmp_path: Path) -> None: snapshot = manager.current_snapshot assert snapshot is not None and snapshot.composition_root is not None - v3_prompt = PromptRenderCtx( - session_key="telegram:1", - channel="telegram", - chat_id="1", - content="你好", - media=None, - timestamp=legacy_prompt.timestamp, - history=[], - skill_names=[], - retrieved_memory_block="", - disabled_sections=set(), - turn_injection_prompt="", - ) - _ = await snapshot.composition_root.context.serial( - PROMPT_RENDER_EVENT, - v3_prompt, - ) - v3_answer = AfterReasoningCtx( - session_key="telegram:1", - channel="telegram", - chat_id="1", - tools_used=(), - thinking=None, - response_metadata=ResponseMetadata(raw_text=reply), - streamed=False, - tool_chain=(), - context_retry={}, - reply=reply, - ) + prompt = _prompt_ctx() + _ = await snapshot.composition_root.context.serial(PROMPT_RENDER_EVENT, prompt) + answer = _answer_ctx("答复正文\n§cited:[mem_1]§ ") _ = await snapshot.composition_root.context.serial( AFTER_REASONING_PREPROCESS_EVENT, - v3_answer, + answer, ) _ = await snapshot.composition_root.context.serial( AFTER_REASONING_CLEANUP_EVENT, - v3_answer, + answer, ) - assert v3_prompt.system_sections_bottom == legacy_prompt.system_sections_bottom - assert v3_answer.reply == legacy_answer.reply == "答复正文" - assert v3_answer.persist_assistant_metadata["cited_memory_ids"] == ( - legacy_frame.slots["persist:assistant:cited_memory_ids"] - ) - assert v3_answer.media == legacy_answer.media == [str(image)] - assert v3_answer.meme_tag == legacy_answer.meme_tag == "shy" + assert [section.name for section in prompt.system_sections_bottom] == [ + "citation_protocol", + "memes", + ] + assert answer.reply == "答复正文" + assert answer.persist_assistant_metadata["cited_memory_ids"] == ["mem_1"] + assert answer.media == [str(image)] + assert answer.meme_tag == "shy" + root = snapshot.composition_root await manager.terminate_all() + assert root.receipt().effects == () + assert root.topology_view().listeners == () From cbb02f2edf3903c7a73a6223eb3e4a41a8cc1836 Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Sun, 16 Aug 2026 00:47:28 +0800 Subject: [PATCH 2/3] =?UTF-8?q?test:=20=E5=9B=BA=E5=AE=9A=E7=BA=AF=20v3=20?= =?UTF-8?q?Citation=20=E7=BB=84=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/plugin-api-v3.yml | 2 +- tests/test_plugin.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/plugin-api-v3.yml b/.github/workflows/plugin-api-v3.yml index e2a40c6..eabcabf 100644 --- a/.github/workflows/plugin-api-v3.yml +++ b/.github/workflows/plugin-api-v3.yml @@ -40,7 +40,7 @@ jobs: - uses: actions/checkout@v4 with: repository: akashic-plugins/citation - ref: a9abeb31c25458b8e799dc6aae25d3e83b912c83 + ref: b82453cd1eae71da8d25eb31aada30b01c659b54 path: .citation - uses: actions/setup-python@v5 with: diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 1da9cd6..6641d4d 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -48,6 +48,21 @@ def _load_meme_plugin_module(): return module +def _load_exact_citation_module(citation_root: Path): + path = citation_root / "plugin.py" + spec = importlib.util.spec_from_file_location( + "test_exact_citation_plugin", + path, + submodule_search_locations=[str(path.parent)], + ) + if spec is None or spec.loader is None: + raise ImportError(str(path)) + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + _meme_plugin_module = _load_meme_plugin_module() CITATION_PROTOCOL_SERVICE = _meme_plugin_module.CITATION_PROTOCOL_SERVICE apply = _meme_plugin_module.apply @@ -369,6 +384,8 @@ async def test_citation_meme_cross_repository_v3_behavior(tmp_path: Path) -> Non "AKASHIC_CITATION_ROOT 必须指向 exact-commit Citation checkout" ) citation_root = Path(raw_citation_root) + citation_module = _load_exact_citation_module(citation_root) + assert not hasattr(citation_module, "CitationPlugin") workspace = tmp_path / "workspace" image = _write_meme_workspace(workspace) From 6761c87560faf08a2fc312fcca973bde92400e07 Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Tue, 18 Aug 2026 00:29:37 +0800 Subject: [PATCH 3/3] =?UTF-8?q?refactor(plugin):=20=E6=94=B6=E5=8F=A3=20me?= =?UTF-8?q?me=20=E7=BA=AF=20v3=20manifest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- akashic.plugin.toml | 5 +++++ tests/test_plugin.py | 15 ++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 akashic.plugin.toml diff --git a/akashic.plugin.toml b/akashic.plugin.toml new file mode 100644 index 0000000..7f4da6f --- /dev/null +++ b/akashic.plugin.toml @@ -0,0 +1,5 @@ +schema_version = 1 +name = "meme" +version = "1.0.0" +api_version = 3 +entrypoint = "plugin.py" diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 6641d4d..76fd844 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -29,6 +29,7 @@ from agent.plugins.composable import ComposablePlugin from agent.plugins.dashboard_host import DashboardBinding, PluginDashboardHost from agent.plugins.manager import PluginManager +from agent.plugins.static_manifest import load_static_plugin_manifest from bus.event_bus import EventBus from runtime import MemeCatalog, MemeDecorator @@ -96,6 +97,17 @@ def _write_meme_workspace(workspace: Path) -> Path: return image +def test_static_manifest_matches_v3_module() -> None: + manifest = load_static_plugin_manifest( + Path(_meme_plugin_module.__file__ or "").resolve().parent + ) + + assert manifest.name == _meme_plugin_module.name == "meme" + assert manifest.version == _meme_plugin_module.version == "1.0.0" + assert manifest.api_version == _meme_plugin_module.api_version == 3 + assert manifest.entrypoint == "plugin.py" + + def _prompt_ctx() -> PromptRenderCtx: return PromptRenderCtx( session_key="webui:1", @@ -350,9 +362,6 @@ async def test_v3_plugin_loads_package_and_dashboard_through_real_manager( assert "meme-manage" in snapshot.plugin_skill_index.records dashboard = PluginDashboardHost( - workspace=workspace, - memory_admin=object(), - memory_store=object(), core_routes=(), ) dashboard.prepare_snapshot(snapshot)