diff --git a/.agents/skills/framework-usage/references/bot-api.md b/.agents/skills/framework-usage/references/bot-api.md index da667875..0eaa358f 100644 --- a/.agents/skills/framework-usage/references/bot-api.md +++ b/.agents/skills/framework-usage/references/bot-api.md @@ -183,6 +183,13 @@ file = await self.api.qq.query.get_file(file_id) emoji = await self.api.qq.query.fetch_emoji_like(message_id, emoji_id, emoji_type="1", count=20, cookie="") # -> EmojiLikeInfo likes = await self.api.qq.query.get_emoji_likes(message_id, emoji_id="", count=0) # -> EmojiLikesResult +# 语音转文字(NapCat 内置,原样返回 QQ 侧转写结果;QQ 侧未产出文本时抛 APIError) +try: + ptt = await self.api.qq.query.fetch_ptt_text(message_id) # -> PttTextResult + print(ptt.text) +except APIError: # 常见:retcode 1200 "获取语音转文字结果失败" + ... + # 系统信息 version = await self.api.qq.query.get_version_info() status = await self.api.qq.query.get_status() @@ -427,6 +434,7 @@ await event.reject(reason="理由") | `"get_file"` | `query.get_file()` | | `"fetch_emoji_like"` | `query.fetch_emoji_like()` | | `"get_emoji_likes"` | `query.get_emoji_likes()` | +| `"fetch_ptt_text"` | `query.fetch_ptt_text()` | | `"get_version_info"` | `query.get_version_info()` | | `"get_status"` | `query.get_status()` | | `"get_recent_contact"` | `query.get_recent_contact()` | diff --git a/ncatbot/adapter/mock/api.py b/ncatbot/adapter/mock/api.py index 7c1c7810..489496f2 100644 --- a/ncatbot/adapter/mock/api.py +++ b/ncatbot/adapter/mock/api.py @@ -30,6 +30,7 @@ MessageData, MessageHistory, OcrResult, + PttTextResult, SendMessageResult, StrangerInfo, VersionInfo, @@ -328,6 +329,9 @@ async def get_group_member_list( async def get_msg(self, message_id: Union[str, int]) -> MessageData: return self._record("get_msg", message_id=message_id) + async def fetch_ptt_text(self, message_id: Union[str, int]) -> PttTextResult: + return self._record("fetch_ptt_text", message_id=message_id) + async def get_forward_msg(self, message_id: Union[str, int]) -> ForwardMessageData: return self._record("get_forward_msg", message_id=message_id) diff --git a/ncatbot/adapter/napcat/api/query.py b/ncatbot/adapter/napcat/api/query.py index 43a3f2f8..98db077c 100644 --- a/ncatbot/adapter/napcat/api/query.py +++ b/ncatbot/adapter/napcat/api/query.py @@ -22,6 +22,7 @@ MessageData, MessageHistory, ForwardMessageData, + PttTextResult, RecentContact, StrangerInfo, VersionInfo, @@ -236,6 +237,18 @@ async def get_emoji_likes( data = await self._call_data("get_emoji_likes", params) or {} return EmojiLikesResult(**data) + async def fetch_ptt_text(self, message_id: Union[str, int]) -> PttTextResult: + """获取语音消息的转文字结果 + + NapCat 的 ``fetch_ptt_text`` 接受数字或字符串消息 ID, + 这里统一按字符串传递,避免超过 JS 安全整数范围的 ID 精度丢失。 + """ + data = ( + await self._call_data("fetch_ptt_text", {"message_id": str(message_id)}) + or {} + ) + return PttTextResult(**data) + # ---- 系统信息 ---- async def get_version_info(self) -> VersionInfo: diff --git a/ncatbot/api/qq/interface.py b/ncatbot/api/qq/interface.py index 149d9936..144cd9af 100644 --- a/ncatbot/api/qq/interface.py +++ b/ncatbot/api/qq/interface.py @@ -37,6 +37,7 @@ MessageHistory, ForwardMessageData, OcrResult, + PttTextResult, RecentContact, SendMessageResult, StrangerInfo, @@ -404,6 +405,9 @@ async def download_file( # ---- 消息扩展操作 ---- + @abstractmethod + async def fetch_ptt_text(self, message_id: Union[str, int]) -> PttTextResult: ... + @abstractmethod async def set_msg_emoji_like( self, message_id: Union[str, int], emoji_id: str, set: bool = True diff --git a/ncatbot/api/qq/query.py b/ncatbot/api/qq/query.py index d2ddcd64..1d5adf7b 100644 --- a/ncatbot/api/qq/query.py +++ b/ncatbot/api/qq/query.py @@ -26,6 +26,7 @@ LoginInfo, MessageData, OcrResult, + PttTextResult, RecentContact, StrangerInfo, VersionInfo, @@ -165,6 +166,21 @@ async def get_recent_contact(self, count: int = 10) -> List[RecentContact]: async def ocr_image(self, image: str) -> OcrResult: return await self._api.ocr_image(image) + async def fetch_ptt_text(self, message_id: Union[str, int]) -> PttTextResult: + """获取语音消息的转文字结果 + + Parameters + ---------- + message_id : str | int + 语音消息的消息 ID + + Returns + ------- + PttTextResult + 转写文本位于 ``text`` 字段 + """ + return await self._api.fetch_ptt_text(message_id) + # ---- 头像查询 ---- async def get_user_avatar( diff --git a/ncatbot/types/napcat/__init__.py b/ncatbot/types/napcat/__init__.py index 32a73eee..114638fa 100644 --- a/ncatbot/types/napcat/__init__.py +++ b/ncatbot/types/napcat/__init__.py @@ -50,6 +50,7 @@ EmojiLikeUser, OcrResult, OcrText, + PttTextResult, RecentContact, RecentContactMessage, VersionInfo, @@ -103,6 +104,7 @@ "EmojiLikesUser", "OcrText", "OcrResult", + "PttTextResult", "RecentContact", "RecentContactMessage", ] diff --git a/ncatbot/types/napcat/system.py b/ncatbot/types/napcat/system.py index 20d7ad31..2d8ecffa 100644 --- a/ncatbot/types/napcat/system.py +++ b/ncatbot/types/napcat/system.py @@ -92,6 +92,15 @@ class OcrResult(NapCatModel): texts: Optional[List[OcrText]] = None +class PttTextResult(NapCatModel): + """语音转文字结果 + + 对应: ``fetch_ptt_text`` + """ + + text: str = "" + + class RecentContactMessage(NapCatModel): """最近联系人中的最新消息(内嵌结构)""" diff --git a/tests/README.md b/tests/README.md index fbd99c4b..0edeec22 100644 --- a/tests/README.md +++ b/tests/README.md @@ -69,6 +69,7 @@ python tests/e2e/napcat/run.py | A | API Client | A-01 ~ A-02 | | AE | API Errors | AE-01 ~ AE-07 | | P | EventParser / NapCatEventParser | P-01 ~ P-07 | +| NQ | NapCat 查询 API 参数转换 | NQ-01 ~ NQ-04 | | AR | AdapterRegistry | AR-01 ~ AR-05 | | CF | Config Migration | CF-01 ~ CF-05 | | CE | Config 分层与运行时覆盖 | CE-01 ~ CE-05 | diff --git a/tests/unit/adapter/README.md b/tests/unit/adapter/README.md index ce4d9650..c1ccfd1d 100644 --- a/tests/unit/adapter/README.md +++ b/tests/unit/adapter/README.md @@ -18,6 +18,17 @@ | P-06 | message_sent 映射 | `message_sent` 映射到 `MESSAGE` + `message_type` | | P-07 | notify 子类型推导 | `notice_type=notify` 时使用 `sub_type` 推导 | +### NapCat QueryAPI (`test_napcat_query_api.py`) + +测试 `QueryAPIMixin` 的消息 ID 参数转换与语音转文字结果解析。 + +| 规范 ID | 说明 | 验证点 | +|---------|------|--------| +| NQ-01 | `get_forward_msg` 参数规范 | 使用 `message_id` 键传递 | +| NQ-02 | 长消息 ID 精度 | 超过 JS 安全整数范围的 ID 以字符串原样传递 | +| NQ-03 | `fetch_ptt_text` 参数与解析 | 以字符串 `message_id` 调用 `fetch_ptt_text`,解析 `text` | +| NQ-04 | `fetch_ptt_text` 空数据 | `data` 为 null 时返回空文本 `text` | + ## 运行方式 ```bash diff --git a/tests/unit/adapter/test_napcat_query_api.py b/tests/unit/adapter/test_napcat_query_api.py index 063f8eff..6f8bc386 100644 --- a/tests/unit/adapter/test_napcat_query_api.py +++ b/tests/unit/adapter/test_napcat_query_api.py @@ -3,6 +3,8 @@ 规范: NQ-01: get_forward_msg 使用 NapCat 规范的 message_id 参数 NQ-02: get_forward_msg 保留超过 JavaScript 安全整数范围的完整 ID + NQ-03: fetch_ptt_text 调用 NapCat 的 fetch_ptt_text 动作并解析 text + NQ-04: fetch_ptt_text 在 data 为 null 时返回空文本而不抛异常 """ from unittest.mock import AsyncMock @@ -15,8 +17,8 @@ class _FakeQueryMixin(QueryAPIMixin): """为测试提供底层 API 调用方法。""" - def __init__(self): - self._call_data = AsyncMock(return_value={"messages": []}) + def __init__(self, data=None): + self._call_data = AsyncMock(return_value=data) class TestNapCatQueryAPI: @@ -32,7 +34,7 @@ class TestNapCatQueryAPI: @pytest.mark.asyncio async def test_nq01_get_forward_msg_preserves_long_id(self, message_id): """NQ-01/02: 转发消息长 ID 始终通过字符串 message_id 原样传递。""" - api = _FakeQueryMixin() + api = _FakeQueryMixin(data={"messages": []}) result = await api.get_forward_msg(message_id) @@ -41,3 +43,32 @@ async def test_nq01_get_forward_msg_preserves_long_id(self, message_id): {"message_id": "7672696335120716277"}, ) assert result.messages == [] + + @pytest.mark.parametrize( + "message_id", + [ + "7701320072321480080", + 7701320072321480080, + ], + ) + @pytest.mark.asyncio + async def test_nq03_fetch_ptt_text_passes_string_id(self, message_id): + """NQ-03: 语音转文字按字符串 message_id 调用 fetch_ptt_text 并解析 text。""" + api = _FakeQueryMixin(data={"text": "你好世界"}) + + result = await api.fetch_ptt_text(message_id) + + api._call_data.assert_awaited_once_with( + "fetch_ptt_text", + {"message_id": "7701320072321480080"}, + ) + assert result.text == "你好世界" + + @pytest.mark.asyncio + async def test_nq04_fetch_ptt_text_null_data(self): + """NQ-04: fetch_ptt_text 返回 data=null 时得到空文本而不是异常。""" + api = _FakeQueryMixin(data=None) + + result = await api.fetch_ptt_text("7701320072321480080") + + assert result.text == ""