Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/plugin-api-v2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,22 @@ jobs:
env:
PYTHONPATH: .plugin-contracts
run: python -m akashic_plugin_contracts check plugin.py

host-channel-contract:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
repository: kachofugetsu09/akashic-agent
ref: 2bf05e320103bf63b67693d114fd2433fd10a0ea
path: .akashic-agent
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install channel contract dependencies
run: python -m pip install -r .akashic-agent/requirements.txt pytest pytest-asyncio
- name: Verify pinned host channel contract
env:
AKASHIC_AGENT_ROOT: ${{ github.workspace }}/.akashic-agent
run: python -m pytest -q tests
42 changes: 36 additions & 6 deletions channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,25 @@
import json
import logging
import time
from dataclasses import dataclass
from dataclasses import dataclass, replace
from collections.abc import Callable, Coroutine
from typing import TYPE_CHECKING, Any, cast

import httpx
import websockets

from agent.looping.interrupt import InterruptController
from bus.events import InboundMessage, OutboundMessage
from bus.events import (
ChannelMessage,
DeliveryReceipt,
InboundMessage,
OutboundMessage,
channel_message_from_outbound,
)
from bus.events_lifecycle import StreamDeltaReady, TurnStarted
from bus.queue import MessageBus
from infra.channels.contract import ChannelContext
from infra.channels.delivery import deliver_message_parts

if TYPE_CHECKING:
from .plugin import QQBotGroupConfigModel
Expand Down Expand Up @@ -102,8 +109,7 @@ async def start(self, ctx: ChannelContext) -> None:
self._events_bound = True
ctx.push_tool.register_channel(
self.name,
text=self.send_proactive,
stream_text=self.send_stream,
deliver=self._deliver_message,
)
self._stopped.clear()
self._task = asyncio.create_task(self._gateway_loop())
Expand Down Expand Up @@ -261,8 +267,12 @@ async def _on_response(self, msg: OutboundMessage) -> None:
else:
await self._delete_live_preview(session_key)
self._clear_live_session(session_key)
if content and not sent_as_stream:
await self.send(msg.chat_id, msg.content)
outbound = channel_message_from_outbound(msg)
if sent_as_stream:
outbound = replace(outbound, content="")
receipt = await self._deliver_message(outbound)
if not receipt.succeeded:
raise RuntimeError(receipt.detail or "QQBot 消息提交失败")

async def send_proactive(self, chat_id: str, message: str) -> None:
kind, _target = self._parse_chat_id(chat_id)
Expand Down Expand Up @@ -292,6 +302,26 @@ async def send_stream(self, chat_id: str, message: str) -> None:
logger.warning("[qqbot] 私聊流式发送失败,回退普通发送: %s", e)
await self.send(chat_id, message)

async def _deliver_message(self, message: ChannelMessage) -> DeliveryReceipt:
"""提交 QQBot 文本消息,并明确拒绝未支持的附件。"""

async def unsupported_file(
_chat_id: str,
_path: str,
_name: str | None,
) -> None:
raise RuntimeError("官方 QQBot 当前不支持发送文件")

async def unsupported_image(_chat_id: str, _path: str) -> None:
raise RuntimeError("官方 QQBot 当前不支持发送图片")

return await deliver_message_parts(
message,
send_text=self.send_proactive,
send_file=unsupported_file,
send_image=unsupported_image,
)

async def _send_stream_c2c(self, openid: str, msg_id: str, message: str) -> None:
token = await self._get_access_token()
msg_seq = self._next_msg_seq()
Expand Down
45 changes: 43 additions & 2 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@

import pytest

from agent.tools.message_push import MessagePushTool
from bus.events import (
AttachmentKind,
ChannelAttachment,
ChannelMessage,
DeliveryStatus,
)


def _load_plugin_module():
path = Path(__file__).parents[1] / "plugin.py"
Expand Down Expand Up @@ -131,22 +139,55 @@ async def gateway_loop() -> None:
channel._gateway_loop = gateway_loop
registry = SimpleNamespace(
on=lambda *_args: object(),
register_channel=lambda *_args, **_kwargs: object(),
subscribe_outbound=lambda *_args: object(),
)
push_tools = [MessagePushTool(), MessagePushTool()]
context = SimpleNamespace(
bus=registry,
event_bus=registry,
push_tool=registry,
push_tool=push_tools[0],
interrupt_controller=None,
)

await channel.start(context)
await asyncio.sleep(0)
await channel.stop()
context.push_tool = push_tools[1]
await channel.start(context)
await asyncio.sleep(0)
await channel.stop()

assert starts == 2
assert channel._task is None
assert all("qqbot" in tool._adapters for tool in push_tools)


@pytest.mark.asyncio
async def test_delivery_adapter_reports_unsupported_attachment() -> None:
plugin = QQBotPlugin()
plugin.context = type(
"Ctx",
(),
{"config": QQBotConfigModel(app_id="app", client_secret="secret")},
)()
channel = plugin.channels()[0]
sent: list[tuple[str, str]] = []

async def send(chat_id: str, content: str) -> None:
sent.append((chat_id, content))

channel.send_proactive = send
receipt = await channel._deliver_message(
ChannelMessage(
channel="qqbot",
chat_id="c2c:user",
content="正文",
attachments=(
ChannelAttachment(AttachmentKind.FILE, "/tmp/a.txt", "a.txt"),
),
)
)

assert receipt.status is DeliveryStatus.PARTIAL
assert receipt.detail == "官方 QQBot 当前不支持发送文件"
assert sent == [("c2c:user", "正文")]
Loading