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
8 changes: 8 additions & 0 deletions .agents/skills/framework-usage/references/bot-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()` |
Expand Down
4 changes: 4 additions & 0 deletions ncatbot/adapter/mock/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
MessageData,
MessageHistory,
OcrResult,
PttTextResult,
SendMessageResult,
StrangerInfo,
VersionInfo,
Expand Down Expand Up @@ -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)

Expand Down
13 changes: 13 additions & 0 deletions ncatbot/adapter/napcat/api/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
MessageData,
MessageHistory,
ForwardMessageData,
PttTextResult,
RecentContact,
StrangerInfo,
VersionInfo,
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions ncatbot/api/qq/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
MessageHistory,
ForwardMessageData,
OcrResult,
PttTextResult,
RecentContact,
SendMessageResult,
StrangerInfo,
Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions ncatbot/api/qq/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
LoginInfo,
MessageData,
OcrResult,
PttTextResult,
RecentContact,
StrangerInfo,
VersionInfo,
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions ncatbot/types/napcat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
EmojiLikeUser,
OcrResult,
OcrText,
PttTextResult,
RecentContact,
RecentContactMessage,
VersionInfo,
Expand Down Expand Up @@ -103,6 +104,7 @@
"EmojiLikesUser",
"OcrText",
"OcrResult",
"PttTextResult",
"RecentContact",
"RecentContactMessage",
]
9 changes: 9 additions & 0 deletions ncatbot/types/napcat/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,15 @@ class OcrResult(NapCatModel):
texts: Optional[List[OcrText]] = None


class PttTextResult(NapCatModel):
"""语音转文字结果

对应: ``fetch_ptt_text``
"""

text: str = ""


class RecentContactMessage(NapCatModel):
"""最近联系人中的最新消息(内嵌结构)"""

Expand Down
1 change: 1 addition & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
11 changes: 11 additions & 0 deletions tests/unit/adapter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 34 additions & 3 deletions tests/unit/adapter/test_napcat_query_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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)

Expand All @@ -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 == ""
Loading