Skip to content

chore: update version to 7.12.14#766

Merged
idranme merged 6 commits into
mainfrom
dev
May 15, 2026
Merged

chore: update version to 7.12.14#766
idranme merged 6 commits into
mainfrom
dev

Conversation

@idranme
Copy link
Copy Markdown
Collaborator

@idranme idranme commented May 15, 2026

Summary by Sourcery

重构媒体、文件和好友处理逻辑,改用新的基于 PMHQ OIDB 的 API 和简化的数据模型,并更新 OneBot、Milky、Satori 与 WebUI 中的集成,同时提升项目版本号。

New Features:

  • 新增基于 OIDB 的图片 OCR、好友请求、好友列表、置顶(pins)与联系人推荐协议,以及对应的 PMHQ mixin 和 API。
  • 引入带缓存的高级好友与分组模型,并在各 API 中提供更丰富的好友查询工具。
  • 新增专用的群聊与 C2C 图片上传辅助方法,并统一用其构造富媒体消息。

Bug Fixes:

  • 确保图片、视频和语音消息的媒体 URL 基于 PMHQ 富媒体响应正确构建,而不是使用旧的内核调用。
  • 修复 ark(轻应用)元素的处理逻辑,在 JSON 解析前安全提取 app 字段。
  • 生成 NTV2 富媒体 highway 扩展时,通过初始化为一个零长度 buffer 条目,避免产生非法的空 SHA1 数组。

Enhancements:

  • 简化文件缓存 schema,以文件 UUID 作为键并保存 MD5,移除消息级字段,并相应更新所有调用方。
  • 将好友相关操作(列表、信息、检查、通过请求)统一构建在新的好友列表缓存和 PMHQ 好友 mixin 之上,替换旧的 buddy API。
  • 精简收发消息转换逻辑以及 OneBot/Satori 适配器,使其使用新的媒体 URL、好友和文件抽象。
  • 将直接扫描与删除缓存的逻辑替换为基于 TODO 的占位实现,用于后续完善缓存清理。
  • 向 PMHQ 添加 System mixin,并新增对应的 NT system API,用于获取置顶好友和群组。

Build:

  • 更新运行时依赖,将 @hono/node-server、fast-xml-parser、hono 和 vite 等升级到较新的补丁版本。

Chores:

  • 移除 NTQQFileCache API 及其在适配器与启动装配中的注入,作为新文件缓存设计的一部分。
  • 更新部分好友请求标志位以及 WebUI/适配器的处理逻辑,改为使用不带时间戳的纯 UID。
  • 将内部版本常量提升至 7.12.14。
Original summary in English

Summary by Sourcery

Refactor media, file, and friend handling to use new PMHQ OIDB-backed APIs and simplified data models, while updating integrations across OneBot, Milky, Satori, and WebUI and bumping the project version.

New Features:

  • Add OIDB-based image OCR, friend request, friend list, pins, and contact recommendation protocols and corresponding PMHQ mixins and APIs.
  • Introduce high-level friend and category models with caching and expose richer friend lookup utilities across APIs.
  • Add dedicated group and C2C image upload helpers and use them to construct rich media messages consistently.

Bug Fixes:

  • Ensure media URLs for images, videos, and voice messages are built correctly from PMHQ rich media responses instead of older kernel calls.
  • Fix handling of ark (light app) elements by safely extracting the app field before JSON parsing.
  • Prevent invalid empty SHA1 arrays when generating NTV2 rich media highway extensions by initializing with a zero-length buffer entry.

Enhancements:

  • Simplify file cache schema to be keyed by file UUID with MD5 and remove message-level fields, updating all callers accordingly.
  • Unify friend-related operations (list, info, checks, approvals) on top of the new friend list cache and PMHQ friend mixin, replacing legacy buddy APIs.
  • Streamline incoming/outgoing message transforms and OneBot/Satori adapters to use the new media URL, friend, and file abstractions.
  • Replace direct cache scanning and deletion logic with a stubbed TODO-based implementation for cache cleaning.
  • Add a System mixin to PMHQ and a corresponding NT system API for fetching pinned friends and groups.

Build:

  • Bump runtime dependencies including @hono/node-server, fast-xml-parser, hono, and vite to newer patch versions.

Chores:

  • Remove the NTQQFileCache API and its injection from adapters and startup wiring as part of the new file cache design.
  • Update various friend request flags and WebUI/adapter handling to use plain UIDs without embedded timestamps.
  • Bump internal version constant to 7.12.14.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented May 15, 2026

Reviewer's Guide

重构媒体和好友处理逻辑以使用基于 Oidb 的新 PMHQ 接口,简化文件缓存与媒体 URL 生成方式,更新所有 OneBot / Milky / Satori / Web UI 的集成以适配新抽象,并将项目版本提升至 7.12.14,同时进行少量依赖更新。

通过 Oidb/imageOcr 的新图片 OCR 流程时序图

sequenceDiagram
  actor Client
  participant OCRImage as OCRImage_action
  participant ntFileApi as NTQQFileApi
  participant PMHQ as PMHQ(MediaMixin)
  participant Oidb as OidbSvcTrpcTcp_0xe07_0

  Client->>OCRImage: _handle(image)
  alt image is http url
    OCRImage->>ntFileApi: ocrImage(imageUrl)
  else image is local/uri
    OCRImage->>OCRImage: uri2local(image)
    OCRImage->>ntFileApi: uploadC2CImage(selfInfo.uid, path)
    ntFileApi->>PMHQ: getC2CImageUploadInfo(peerUid, filePath)
    PMHQ->>Oidb: httpSendPB 0x11c5_100
    Oidb-->>PMHQ: NTV2RichMediaResp(upload)
    PMHQ-->>ntFileApi: msgInfo, compat, ext
    ntFileApi-->>OCRImage: msgInfo
    OCRImage->>ntFileApi: getImageUrl(pic.urlPath + ext.originalParam, md5HexStr)
  end

  ntFileApi->>PMHQ: imageOcr(imageUrl)
  PMHQ->>Oidb: httpSendPB 0xe07_0(ImageOcrReq)
  Oidb-->>PMHQ: ImageOcrResp
  PMHQ-->>ntFileApi: ImageOcrResp
  ntFileApi-->>OCRImage: ocrRspBody
  OCRImage-->>Client: texts, language
Loading

通过 fetchFriends 的新好友列表缓存流程时序图

sequenceDiagram
  actor Client
  participant Milky as GetFriendList_API
  participant ntFriendApi as NTQQFriendApi
  participant PMHQ as PMHQ(FriendMixin)
  participant Oidb as OidbSvcTrpcTcp_0xfd4_1

  Client->>Milky: get_friend_list(no_cache)
  Milky->>ntFriendApi: getFriendList(forceUpdate = no_cache)
  alt cache empty or forceUpdate
    ntFriendApi->>PMHQ: fetchFriends()
    PMHQ->>Oidb: httpSendPB 0xfd4_1(IncPullReq)
    Oidb-->>PMHQ: IncPullResp(friendList, category)
    PMHQ-->>ntFriendApi: IncPullResp
    ntFriendApi->>ntFriendApi: build friendsCache, categoriesCache
  else use cache
    ntFriendApi-->>Milky: friendsCache, categoriesCache
  end
  ntFriendApi-->>Milky: {friends, categories}
  Milky->>Milky: transformFriend(friend, categories.get(categoryId))
  Milky-->>Client: friends[] with category info
Loading

File-Level Changes

Change Details Files
将 NTQQ 文件/媒体 API 重构为使用结构化的 PMHQ 响应,新增专用图片上传辅助方法,并移除传统的缓存/下载逻辑。
  • getVideoUrl(fileUuid, isGroup) 替换 getVideoUrlByPacket,基于 pmhq.getGroup/PrivateVideoUrl 返回的 Media.NTV2RichMediaResp 构造 URL,同时相应更新 getPttUrl 以返回 URL 字符串。
  • 内联 getFileType 的使用,并移除 NTQQFileApi 中的 getFileTypedownloadMediagetImageSizedownloadFileForModelIdocrImage(path)uploadRMFileWithoutMsg 等辅助方法,从而简化服务接口。
  • 在 NTQQFileApi 中新增 uploadGroupImageuploadC2CImage,调用新的 pmhq.getGroupImageUploadInfo/getC2CImageUploadInfo 并执行 Highway TCP/HTTP 上传,返回下游使用的 msgInfo/compat
  • 调整 ntqqapi.entitiesSendElement.pic,在构造发出的图片元素时,直接使用 getFileType/getImageSize 而不是通过 ntFileApi 封装。
src/ntqqapi/api/file.ts
src/main/pmhq/mixins/media.ts
src/ntqqapi/entities.ts
引入基于 Oidb 的好友列表与好友请求处理逻辑,使用内存缓存和新的 Friend/Category 模型,替换 Milky、OneBot11、Satori 和 Web UI 中的旧版 buddy API。
  • 定义 FriendCategory 接口以及 Oidb 消息(IncPullReq/RespSetFriendRequestReqSetFilteredFriendRequestReqFetchPinsRespGetFriendRecommendContactArkReq/RespImageOcrReq/Resp)以支持新的好友列表、置顶、OCR 和推荐流程。
  • NTQQFriendApi 中实现 friendsCache/categoriesCache,提供 getFriendList(forceUpdate)getFriendInfoByUin/UidisFriendapprovalFriendRequestapprovalDoubtFriendRequestgetFriendRecommendContactArk,并废弃 handleFriendRequest/getBuddyList/getBuddyV2/isBuddy 相关方法。
  • 更新 Milky、OneBot11、Satori 和 Web UI 中的好友列表/信息 API、好友请求处理、快捷操作与仪表盘指标,使其使用新的好友缓存以及 approvalFriendRequest/approvalDoubtFriendRequest 接口,并简化为仅使用 friendUid 标识。
  • 更新实体转换器(OB11Entities.friend/friendsmilky.transformFriendsatori.decodeUser)以消费新的 Friend/Category 结构,同时支持数值型 uin,但保留现有对外响应格式。
src/ntqqapi/types/user.ts
src/ntqqapi/proto/oidb.ts
src/ntqqapi/api/friend.ts
src/main/pmhq/mixins/friend.ts
src/milky/api/friend.ts
src/milky/api/system.ts
src/milky/transform/entity.ts
src/satori/api/friend/list.ts
src/satori/api/friend/approve.ts
src/satori/utils.ts
src/onebot11/entities.ts
src/onebot11/action/user/GetFriendList.ts
src/onebot11/action/user/SetFriendAddRequest.ts
src/onebot11/helper/quickOperation.ts
src/onebot11/adapter.ts
src/webui/BE/routes/webqq/notifications.ts
src/webui/BE/routes/dashboard.ts
src/satori/event/user.ts
src/onebot11/action/llbot/user/GetFriendWithCategory.ts
src/onebot11/action/llbot/user/SetDoubtFriendsAddRequest.ts
统一文件缓存存储为简化的 schema,并将媒体访问方式从基于消息的下载切换为通过 uri2local 和新的 NTQQFileApi 方法进行 URL 级访问。
  • 用单一的 file 表替换 file_v2 表,使用 FileCache 类型(fileNamefileSizefileUuidmsgTimechatTypeelementTypemd5HexStr),并相应更新 Store.addFileCache/getFileCache*
  • 修改 Milky 和 OneBot11 中入站消息转换器的所有文件缓存写入逻辑,填充 md5HexStr,并在存储 schema 中去掉 msgId/elementId/peerUid
  • 重构 uri2localGetFile/GetRecord(及相关辅助方法),通过 ntFileApi.getImageUrl/getVideoUrl/getPttUrl 构造图片/视频/语音 URL,再通过 uri2local 下载,而不再使用 ntFileApi.downloadMedia
  • 调整音频语音的获取逻辑,改为使用 uri2local 获取本地路径后再调用 decodeSilk,从而移除对 ntFileApi.downloadMedia 的直接依赖。
src/common/types.ts
src/main/store.ts
src/milky/transform/message/incoming.ts
src/onebot11/transform/message/incoming.ts
src/common/utils/file.ts
src/onebot11/action/file/GetFile.ts
src/onebot11/action/file/GetRecord.ts
重新设计消息编解码及 OCR 中的图片/视频处理逻辑,改为依赖服务器提供的 MsgInfo 和新的 OCR 端点,而不是本地文件探测。
  • 更新 OneBot11 和 Milky 的入站转换器以及 Satori 解码器,使其调用 ntFileApi.getImageUrl(originImageUrl, md5HexStr)getVideoUrl(fileUuid, isGroup),并使用新的 PMHQ MediaMixin 行为。
  • 修改消息编码器(OneBot11 createMultiMessage、Milky 转发编码器、Satori ntToProto),统一使用 ntFileApi.uploadGroupImage/uploadC2CImage,并将返回的 MsgInfo 直接传给 packImage,而不是基于 RichMediaUploadCompleteNotify 在本地重建 Media.MsgInfo
  • 重写 OCRImage action,使其同时接受 HTTP URL 和非 HTTP 的图片标识;对于非 HTTP 输入,先作为 C2C 图片上传以获取 URL,再将该 URL 传给 ntFileApi.ocrImage(url),底层由新的 Oidb.ImageOcrReq/Resppmhq.imageOcr 支持;将 ImageOcrResp.textDetections 映射为预期的响应结构。
  • 简化 OB11 和 Milky 的转发消息解码逻辑,使用新的 getVideoUrl(fileUuid, isGroup) 辅助方法;并在 Milky 的 ark/light_app 解析中,先通过正则提取 app 字段,以避免在非转发应用场景下对整个内容执行 JSON.parse
src/milky/transform/message/incoming.ts
src/milky/transform/message/outgoing.ts
src/onebot11/helper/createMultiMessage.ts
src/satori/message.ts
src/satori/utils.ts
src/milky/api/message.ts
src/onebot11/helper/decodeMultiMessage.ts
src/onebot11/action/go-cqhttp/GetForwardMsg.ts
src/onebot11/action/go-cqhttp/GetGroupMsgHistory.ts
src/onebot11/action/go-cqhttp/OCRImage.ts
新增 PMHQ SystemMixin 和置顶消息(pins)支持,同时清理未使用的缓存 API,并提升版本与依赖。
  • 引入 PMHQ SystemMixin,提供 fetchPins,调用 OidbSvcTrpcTcp.0x12b3_0 并解析 Oidb.FetchPinsResp;将其接入 PMHQ 组合,并暴露 NTQQSystemApi.getPins 以委托给 pmhq.fetchPins
  • 更新 Milky GetPeerPins API,使其使用 ntSystemApi.getPins 以及新的好友/群组辅助方法(ntFriendApi.getFriendInfoByUidntGroupApi.getGroupDetailInfo),不再依赖基于 topTime 的本地过滤。
  • 移除 NTQQFileCacheApi 服务及其引用(包括 OneBot11/Satori 适配器和 CleanCache 中的使用),保留 CleanCache 作为仅用于删除 LLBot 临时文件的 TODO stub。
  • 新增 isHttpUrl 工具函数并用于 OCRImage;调整 NTV2RichMedia.generateExt 的默认 hash.fileSha1 为非空的 buffer 数组以满足类型要求。
  • 将核心库版本和内部导出版本从 7.12.13 提升至 7.12.14,并对 @hono/node-serverfast-xml-parserhonovite 进行小版本更新。
src/main/pmhq/mixins/system.ts
src/main/pmhq/index.ts
src/ntqqapi/api/system.ts
src/milky/api/system.ts
src/onebot11/action/system/CleanCache.ts
src/onebot11/adapter.ts
src/satori/adapter.ts
src/common/utils/misc.ts
src/ntqqapi/helper/ntv2RichMedia.ts
src/main/main.ts
src/version.ts
package.json

Tips and commands

Interacting with Sourcery

  • 触发新一次代码审查: 在 Pull Request 上评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论即可继续对话。
  • 从审查评论生成 GitHub Issue: 在审查评论下回复,要求 Sourcery 基于该评论创建 issue。你也可以直接回复 @sourcery-ai issue 来从该评论创建 issue。
  • 生成 Pull Request 标题: 在 PR 标题的任意位置写上 @sourcery-ai 即可随时生成标题。你也可以在 Pull Request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 Pull Request 总结: 在 PR 描述正文任意位置写上 @sourcery-ai summary,即可在该位置生成 PR 总结。你也可以在 Pull Request 中评论 @sourcery-ai summary 来随时(重新)生成总结。
  • 生成审查指南: 在 Pull Request 中评论 @sourcery-ai guide,即可随时(重新)生成审查者指南。
  • 一次性标记解决所有 Sourcery 评论: 在 Pull Request 中评论 @sourcery-ai resolve,会将所有 Sourcery 评论标记为已解决。如果你已经处理完所有评论,不想再看到它们时非常有用。
  • 取消所有 Sourcery 审查: 在 Pull Request 中评论 @sourcery-ai dismiss,会取消所有已有的 Sourcery 审查。特别适合同步开始一次全新的审查——别忘了再评论 @sourcery-ai review 来触发新审查!

Customizing Your Experience

访问你的 dashboard 以:

  • 启用或禁用审查功能,例如 Sourcery 自动生成的 Pull Request 总结、审查者指南等。
  • 更改审查语言。
  • 添加、删除或编辑自定义审查指令。
  • 调整其他审查相关设置。

Getting Help

Original review guide in English

Reviewer's Guide

Refactors media and friend handling to use new Oidb-based PMHQ endpoints, simplifies file caching and media URL generation, updates all OneBot/Milky/Satori/web UI integrations to the new abstractions, and bumps the project version to 7.12.14 with minor dependency updates.

Sequence diagram for new image OCR flow via Oidb/imageOcr

sequenceDiagram
  actor Client
  participant OCRImage as OCRImage_action
  participant ntFileApi as NTQQFileApi
  participant PMHQ as PMHQ(MediaMixin)
  participant Oidb as OidbSvcTrpcTcp_0xe07_0

  Client->>OCRImage: _handle(image)
  alt image is http url
    OCRImage->>ntFileApi: ocrImage(imageUrl)
  else image is local/uri
    OCRImage->>OCRImage: uri2local(image)
    OCRImage->>ntFileApi: uploadC2CImage(selfInfo.uid, path)
    ntFileApi->>PMHQ: getC2CImageUploadInfo(peerUid, filePath)
    PMHQ->>Oidb: httpSendPB 0x11c5_100
    Oidb-->>PMHQ: NTV2RichMediaResp(upload)
    PMHQ-->>ntFileApi: msgInfo, compat, ext
    ntFileApi-->>OCRImage: msgInfo
    OCRImage->>ntFileApi: getImageUrl(pic.urlPath + ext.originalParam, md5HexStr)
  end

  ntFileApi->>PMHQ: imageOcr(imageUrl)
  PMHQ->>Oidb: httpSendPB 0xe07_0(ImageOcrReq)
  Oidb-->>PMHQ: ImageOcrResp
  PMHQ-->>ntFileApi: ImageOcrResp
  ntFileApi-->>OCRImage: ocrRspBody
  OCRImage-->>Client: texts, language
Loading

Sequence diagram for new friend list caching via fetchFriends

sequenceDiagram
  actor Client
  participant Milky as GetFriendList_API
  participant ntFriendApi as NTQQFriendApi
  participant PMHQ as PMHQ(FriendMixin)
  participant Oidb as OidbSvcTrpcTcp_0xfd4_1

  Client->>Milky: get_friend_list(no_cache)
  Milky->>ntFriendApi: getFriendList(forceUpdate = no_cache)
  alt cache empty or forceUpdate
    ntFriendApi->>PMHQ: fetchFriends()
    PMHQ->>Oidb: httpSendPB 0xfd4_1(IncPullReq)
    Oidb-->>PMHQ: IncPullResp(friendList, category)
    PMHQ-->>ntFriendApi: IncPullResp
    ntFriendApi->>ntFriendApi: build friendsCache, categoriesCache
  else use cache
    ntFriendApi-->>Milky: friendsCache, categoriesCache
  end
  ntFriendApi-->>Milky: {friends, categories}
  Milky->>Milky: transformFriend(friend, categories.get(categoryId))
  Milky-->>Client: friends[] with category info
Loading

File-Level Changes

Change Details Files
Refactor NTQQ file/media APIs to use structured PMHQ responses, add dedicated image upload helpers, and remove legacy cache/download logic.
  • Replace getVideoUrlByPacket with getVideoUrl(fileUuid, isGroup) building URLs from Media.NTV2RichMediaResp returned by pmhq.getGroup/PrivateVideoUrl and similarly update getPttUrl to return a URL string.
  • Inline getFileType usage and remove NTQQFileApi helpers for getFileType, downloadMedia, getImageSize, downloadFileForModelId, ocrImage(path), and uploadRMFileWithoutMsg, simplifying the service surface.
  • Add uploadGroupImage and uploadC2CImage to NTQQFileApi that call new pmhq.getGroupImageUploadInfo/getC2CImageUploadInfo and perform Highway TCP/HTTP uploads, returning msgInfo/compat for downstream use.
  • Adjust ntqqapi.entities SendElement.pic to use getFileType/getImageSize directly instead of ntFileApi wrappers when preparing outgoing picture elements.
src/ntqqapi/api/file.ts
src/main/pmhq/mixins/media.ts
src/ntqqapi/entities.ts
Introduce Oidb-based friend list and request handling with in-memory caching and new Friend/Category models, replacing legacy buddy APIs across Milky, OneBot11, Satori, and web UI.
  • Define Friend and Category interfaces and Oidb messages (IncPullReq/Resp, SetFriendRequestReq, SetFilteredFriendRequestReq, FetchPinsResp, GetFriendRecommendContactArkReq/Resp, ImageOcrReq/Resp) to support new friend list, pins, OCR, and recommendation flows.
  • Implement NTQQFriendApi.friendsCache/categoriesCache with getFriendList(forceUpdate), getFriendInfoByUin/Uid, isFriend, approvalFriendRequest, approvalDoubtFriendRequest, and getFriendRecommendContactArk, deprecating handleFriendRequest/getBuddyList/getBuddyV2/isBuddy-related methods.
  • Update Milky, OneBot11, Satori, and web UI friend list/info APIs, friend request handlers, quick operations, and dashboard metrics to use the new friend cache and approvalFriendRequest/approvalDoubtFriendRequest APIs with simplified friendUid-only flags.
  • Update entity transformers (OB11Entities.friend/friends, milky.transformFriend, satori.decodeUser) to consume the new Friend/Category shapes and support numeric uins while preserving existing external response formats.
src/ntqqapi/types/user.ts
src/ntqqapi/proto/oidb.ts
src/ntqqapi/api/friend.ts
src/main/pmhq/mixins/friend.ts
src/milky/api/friend.ts
src/milky/api/system.ts
src/milky/transform/entity.ts
src/satori/api/friend/list.ts
src/satori/api/friend/approve.ts
src/satori/utils.ts
src/onebot11/entities.ts
src/onebot11/action/user/GetFriendList.ts
src/onebot11/action/user/SetFriendAddRequest.ts
src/onebot11/helper/quickOperation.ts
src/onebot11/adapter.ts
src/webui/BE/routes/webqq/notifications.ts
src/webui/BE/routes/dashboard.ts
src/satori/event/user.ts
src/onebot11/action/llbot/user/GetFriendWithCategory.ts
src/onebot11/action/llbot/user/SetDoubtFriendsAddRequest.ts
Unify file cache storage to a simplified schema and switch media access from message-based downloading to URL-based retrieval via uri2local and new NTQQFileApi methods.
  • Replace the file_v2 table with a single file table using the FileCache type (fileName, fileSize, fileUuid, msgTime, chatType, elementType, md5HexStr) and update Store.addFileCache/getFileCache* accordingly.
  • Change all file cache writers in incoming message transformers (Milky and OneBot11) to populate md5HexStr and drop msgId/elementId/peerUid from the stored schema.
  • Refactor uri2local and GetFile/GetRecord (and related helpers) to resolve cached files by building image/video/PTT URLs via ntFileApi.getImageUrl/getVideoUrl/getPttUrl and then downloading via uri2local instead of using ntFileApi.downloadMedia.
  • Adjust audio record retrieval to use uri2local plus decodeSilk on the local path, eliminating direct dependence on ntFileApi.downloadMedia.
src/common/types.ts
src/main/store.ts
src/milky/transform/message/incoming.ts
src/onebot11/transform/message/incoming.ts
src/common/utils/file.ts
src/onebot11/action/file/GetFile.ts
src/onebot11/action/file/GetRecord.ts
Rework image/video handling in message encoding/decoding and OCR to rely on server-provided MsgInfo and new OCR endpoints instead of local file introspection.
  • Update OneBot11 and Milky incoming transformers and Satori decoders to call ntFileApi.getImageUrl(originImageUrl, md5HexStr) and getVideoUrl(fileUuid, isGroup) using the new PMHQ MediaMixin behavior.
  • Change message encoders (OneBot11 createMultiMessage, Milky forward encoder, Satori ntToProto) to use ntFileApi.uploadGroupImage/uploadC2CImage, passing through returned MsgInfo to packImage instead of reconstructing Media.MsgInfo locally based on RichMediaUploadCompleteNotify.
  • Reimplement OCRImage action to accept either HTTP URLs or non-HTTP image identifiers; non-HTTP inputs are uploaded as C2C images to obtain a URL which is then passed to ntFileApi.ocrImage(url) backed by the new Oidb.ImageOcrReq/Resp and pmhq.imageOcr; map ImageOcrResp.textDetections into the expected response shape.
  • Simplify OB11 and Milky forward message decoding to use the new getVideoUrl(fileUuid, isGroup) helper, and adjust ark/light_app parsing in Milky to avoid JSON.parse for non-forward apps by extracting the app field via regex first.
src/milky/transform/message/incoming.ts
src/milky/transform/message/outgoing.ts
src/onebot11/helper/createMultiMessage.ts
src/satori/message.ts
src/satori/utils.ts
src/milky/api/message.ts
src/onebot11/helper/decodeMultiMessage.ts
src/onebot11/action/go-cqhttp/GetForwardMsg.ts
src/onebot11/action/go-cqhttp/GetGroupMsgHistory.ts
src/onebot11/action/go-cqhttp/OCRImage.ts
Add PMHQ SystemMixin and pins support while cleaning up unused cache APIs and bumping version/dependencies.
  • Introduce PMHQ SystemMixin with fetchPins that calls OidbSvcTrpcTcp.0x12b3_0 and parses Oidb.FetchPinsResp; wire it into PMHQ composition and expose NTQQSystemApi.getPins which delegates to pmhq.fetchPins.
  • Update Milky GetPeerPins API to use ntSystemApi.getPins and new friend/group helpers (ntFriendApi.getFriendInfoByUid and ntGroupApi.getGroupDetailInfo) instead of local topTime-based filtering.
  • Remove NTQQFileCacheApi service and its usage (including from OneBot11/Satori adapters and CleanCache), leaving CleanCache as a TODO stub for deleting LLBot temp files.
  • Add isHttpUrl utility and use it in OCRImage; adjust NTV2RichMedia.generateExt default hash.fileSha1 to a non-empty buffer array to satisfy type expectations.
  • Bump core library versions and internal version export from 7.12.13 to 7.12.14 along with minor updates to @hono/node-server, fast-xml-parser, hono, and vite.
src/main/pmhq/mixins/system.ts
src/main/pmhq/index.ts
src/ntqqapi/api/system.ts
src/milky/api/system.ts
src/onebot11/action/system/CleanCache.ts
src/onebot11/adapter.ts
src/satori/adapter.ts
src/common/utils/misc.ts
src/ntqqapi/helper/ntv2RichMedia.ts
src/main/main.ts
src/version.ts
package.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 15, 2026

Test Report

Job Status
unit-test ✅ success
e2e-test ✅ success

✅ All tests passed

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 3 个问题,并给出了一些整体反馈:

  • GetFriendWithCategory._handle 中,result.categories.values().map(...).toArray() 会在运行时失败,因为 Map.prototype.values() 返回的是不带 map/toArray 的迭代器;应当先转换为数组(例如 Array.from(result.categories.values()).map(...))。
  • 若干新增的 helper 在没有检查的情况下假定嵌套字段一定存在(例如 NTQQFriendApi.getFriendList 中的 friend.subBiz.get(1)!biz.data.get(20031)![2],以及新的媒体 URL 构造器中的 download!);建议为缺失 key 的情况添加更健壮的处理,以避免当上游 payload 发生变化时出现难以调试的运行时异常。
给 AI Agent 的提示
请根据这次代码评审的评论进行修改:

## Overall Comments
-`GetFriendWithCategory._handle` 中,`result.categories.values().map(...).toArray()` 会在运行时失败,因为 `Map.prototype.values()` 返回的是不带 `map`/`toArray` 的迭代器;应当先转换为数组(例如 `Array.from(result.categories.values()).map(...)`)。
- 若干新增的 helper 在没有检查的情况下假定嵌套字段一定存在(例如 `NTQQFriendApi.getFriendList` 中的 `friend.subBiz.get(1)!``biz.data.get(20031)![2]`,以及新的媒体 URL 构造器中的 `download!`);建议为缺失 key 的情况添加更健壮的处理,以避免当上游 payload 发生变化时出现难以调试的运行时异常。

## Individual Comments

### Comment 1
<location path="src/onebot11/action/llbot/user/GetFriendWithCategory.ts" line_range="19-28" />
<code_context>
-        onlineCount: item.onlineCount,
-        buddyList: item.buddyList!.map(buddy => {
-          return OB11Entities.friend(buddy)
+    const result = await this.ctx.ntFriendApi.getFriendList(true)
+    return result.categories.values().map(item => ({
+      categoryId: item.categoryId,
+      categorySortId: item.categorySortId,
+      categoryName: item.categoryName,
+      categoryMbCount: item.categoryMemberCount,
+      buddyList: result.friends
+        .filter(friend => friend.categoryId === item.categoryId)
+        .map(friend => {
+          return OB11Entities.friend(friend)
         })
-      }
-    })
+    })).toArray()
   }
 }
</code_context>
<issue_to_address>
**issue (bug_risk):** 使用 `result.categories.values().map(...)).toArray()` 会在运行时抛出异常,因为 `Map.prototype.values()` 返回的是不带 `map`/`toArray` 的迭代器。

由于 `result.categories` 是一个 `Map<number, Category>``result.categories.values()` 返回的是 `IterableIterator<Category>`,它不支持 `map``toArray`,因此在运行时会抛出 `TypeError`。在进行 `map` 之前需要先把迭代器转换为数组,例如:

```ts
const categories = Array.from(result.categories.values()).map(item => ({
  categoryId: item.categoryId,
  categorySortId: item.categorySortId,
  categoryName: item.categoryName,
  categoryMbCount: item.categoryMemberCount,
  buddyList: result.friends
    .filter(friend => friend.categoryId === item.categoryId)
    .map(friend => OB11Entities.friend(friend)),
}))
return categories
```

如果你在使用某个 helper 为可迭代对象添加了 `map`/`toArray`,请将 `result.categories.values()` 先交给该 helper 处理,而不是直接在迭代器上调用这些方法。
</issue_to_address>

### Comment 2
<location path="src/milky/transform/message/incoming.ts" line_range="210-219" />
<code_context>
-              json_payload: arkElement!.bytesData,
-            },
-          })
+        const match = arkElement!.bytesData.match(/"app"\s*:\s*"([^"]*)"/)
+        if (match?.[1]) {
+          if (match[1] === 'com.tencent.multimsg') {
+            const data = JSON.parse(arkElement!.bytesData)
+            segments.push({
+              type: 'forward',
+              data: {
+                forward_id: data.meta.detail.resid,
+                title: data.meta.detail.source,
+                preview: data.meta.detail.news.map((item: { text: string }) => item.text),
+                summary: data.meta.detail.summary,
+              },
+            })
+          } else {
+            segments.push({
+              type: 'light_app',
+              data: {
+                app_name: match[1],
+                json_payload: arkElement!.bytesData,
+              },
+            })
+          }
         }
         break
       }
</code_context>
<issue_to_address>
**issue (bug_risk):**`bytesData` 中不包含 `app` 字段时,该 Ark 元素现在会被静默丢弃,而不是以 light_app 的形式暴露出来。

在新的逻辑下,如果正则没有匹配到 `"app"` 字段,该元素会直接穿过这个 `case` 而不向 `segments` 中推入任何内容,导致这部分消息内容被丢弃。这相较于之前的行为是一种回归:之前任何非 `com.tencent.multimsg` 的 Ark payload 都会以带原始 `bytesData``light_app` 形式发出。

为了在避免不必要的 `JSON.parse` 的同时保持向后兼容,可以在 `match` 为 falsy 时回退到 `light_app````ts
const match = arkElement!.bytesData.match(/"app"\s*:\s*"([^"]*)"/)
if (!match) {
  segments.push({
    type: 'light_app',
    data: {
      app_name: '',
      json_payload: arkElement!.bytesData,
    },
  })
  break
}
// existing com.tencent.multimsg / other app handling...
```
</issue_to_address>

### Comment 3
<location path="src/ntqqapi/api/file.ts" line_range="506" />
<code_context>
-      order: 1,
-      lastRecord: _lastRecord,
-    }])
+  async uploadGroupImage(groupCode: string, filePath: string) {
+    const result = await this.ctx.pmhq.getGroupImageUploadInfo(groupCode, filePath)
+    const highwaySession = await this.ctx.pmhq.getHighwaySession()
</code_context>
<issue_to_address>
**issue (complexity):** 建议将 `uploadGroupImage``uploadC2CImage` 中共享的 Highway 上传逻辑提取到一个私有 helper 中,以避免重复代码并集中管理行为。

你可以通过抽取一个私有 helper,把公共的 Highway 上传流程合并,并将 `cmd` 和获取上传信息的方式作为参数传入,从而消除 `uploadGroupImage``uploadC2CImage` 之间的重复。

例如:

```ts
private async uploadImageViaHighway(
  filePath: string,
  cmd: number,
  getInfo: () => Promise<{
    ext: { uKey?: string; msgInfoBody: any[] };
    info: any;
    compat: any;
  }>
) {
  const result = await getInfo()
  const highwaySession = await this.ctx.pmhq.getHighwaySession()
  const maxBlockSize = 1024 * 1024

  if (result.ext.uKey) {
    const { index } = result.ext.msgInfoBody[0]
    const trans = {
      uin: selfInfo.uin,
      cmd,
      readable: createReadStream(filePath, { highWaterMark: maxBlockSize }),
      sum: Buffer.from(index.info.md5HexStr, 'hex'),
      size: index.info.fileSize,
      ticket: highwaySession.sigSession,
      ext: Media.NTV2RichMediaHighwayExt.encode(result.ext),
      server: highwaySession.highwayHostAndPorts[1][0].host,
      port: highwaySession.highwayHostAndPorts[1][0].port,
    }
    try {
      await new HighwayTcpSession(trans).upload()
    } catch {
      await new HighwayHttpSession(trans).upload()
    }
  }

  return {
    msgInfo: result.info,
    compat: result.compat,
  }
}
```

然后这两个 public 方法就可以变成很薄的一层封装,在保持所有行为不变的同时减少样板代码:

```ts
async uploadGroupImage(groupCode: string, filePath: string) {
  return this.uploadImageViaHighway(filePath, 1004, () =>
    this.ctx.pmhq.getGroupImageUploadInfo(groupCode, filePath)
  )
}

async uploadC2CImage(peerUid: string, filePath: string) {
  return this.uploadImageViaHighway(filePath, 1003, () =>
    this.ctx.pmhq.getC2CImageUploadInfo(peerUid, filePath)
  )
}
```

这样可以将 Highway 会话构造和上传逻辑集中起来,未来如果需要修改(例如错误处理、分块大小、会话选择等),会更容易且不易出错。
</issue_to_address>

Sourcery 对开源项目免费使用——如果你觉得我们的评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进后续评审。
Original comment in English

Hey - I've found 3 issues, and left some high level feedback:

  • In GetFriendWithCategory._handle, result.categories.values().map(...).toArray() will fail at runtime because Map.prototype.values() returns an iterator without map/toArray; convert to an array first (e.g. Array.from(result.categories.values()).map(...)).
  • Several new helpers assume presence of nested fields without checks (e.g. friend.subBiz.get(1)! and biz.data.get(20031)![2] in NTQQFriendApi.getFriendList, and download! in the new media URL builders); consider adding graceful handling for missing keys to avoid hard-to-debug runtime exceptions when the upstream payload changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `GetFriendWithCategory._handle`, `result.categories.values().map(...).toArray()` will fail at runtime because `Map.prototype.values()` returns an iterator without `map`/`toArray`; convert to an array first (e.g. `Array.from(result.categories.values()).map(...)`).
- Several new helpers assume presence of nested fields without checks (e.g. `friend.subBiz.get(1)!` and `biz.data.get(20031)![2]` in `NTQQFriendApi.getFriendList`, and `download!` in the new media URL builders); consider adding graceful handling for missing keys to avoid hard-to-debug runtime exceptions when the upstream payload changes.

## Individual Comments

### Comment 1
<location path="src/onebot11/action/llbot/user/GetFriendWithCategory.ts" line_range="19-28" />
<code_context>
-        onlineCount: item.onlineCount,
-        buddyList: item.buddyList!.map(buddy => {
-          return OB11Entities.friend(buddy)
+    const result = await this.ctx.ntFriendApi.getFriendList(true)
+    return result.categories.values().map(item => ({
+      categoryId: item.categoryId,
+      categorySortId: item.categorySortId,
+      categoryName: item.categoryName,
+      categoryMbCount: item.categoryMemberCount,
+      buddyList: result.friends
+        .filter(friend => friend.categoryId === item.categoryId)
+        .map(friend => {
+          return OB11Entities.friend(friend)
         })
-      }
-    })
+    })).toArray()
   }
 }
</code_context>
<issue_to_address>
**issue (bug_risk):** The use of `result.categories.values().map(...)).toArray()` will throw at runtime because `Map.prototype.values()` returns an iterator without `map`/`toArray`.

Because `result.categories` is a `Map<number, Category>`, `result.categories.values()` returns an `IterableIterator<Category>` that does not support `map` or `toArray`, so this will throw a `TypeError` at runtime. Convert the iterator to an array before mapping, e.g.

```ts
const categories = Array.from(result.categories.values()).map(item => ({
  categoryId: item.categoryId,
  categorySortId: item.categorySortId,
  categoryName: item.categoryName,
  categoryMbCount: item.categoryMemberCount,
  buddyList: result.friends
    .filter(friend => friend.categoryId === item.categoryId)
    .map(friend => OB11Entities.friend(friend)),
}))
return categories
```

If you’re using a helper that adds `map`/`toArray` to iterables, wrap `result.categories.values()` with that helper rather than calling these methods directly on the iterator.
</issue_to_address>

### Comment 2
<location path="src/milky/transform/message/incoming.ts" line_range="210-219" />
<code_context>
-              json_payload: arkElement!.bytesData,
-            },
-          })
+        const match = arkElement!.bytesData.match(/"app"\s*:\s*"([^"]*)"/)
+        if (match?.[1]) {
+          if (match[1] === 'com.tencent.multimsg') {
+            const data = JSON.parse(arkElement!.bytesData)
+            segments.push({
+              type: 'forward',
+              data: {
+                forward_id: data.meta.detail.resid,
+                title: data.meta.detail.source,
+                preview: data.meta.detail.news.map((item: { text: string }) => item.text),
+                summary: data.meta.detail.summary,
+              },
+            })
+          } else {
+            segments.push({
+              type: 'light_app',
+              data: {
+                app_name: match[1],
+                json_payload: arkElement!.bytesData,
+              },
+            })
+          }
         }
         break
       }
</code_context>
<issue_to_address>
**issue (bug_risk):** When `bytesData` does not contain an `app` field, the Ark element is now silently dropped instead of being exposed as a light_app.

With the new logic, if the regex doesn’t find an `"app"` field, the element falls through this `case` without pushing any segment, so that message content is dropped. That’s a regression from the previous behavior where any non-`com.tencent.multimsg` Ark payload was emitted as a `light_app` with the raw `bytesData`.

To preserve backward compatibility while still avoiding unnecessary `JSON.parse`, you can fall back to `light_app` when `match` is falsy:

```ts
const match = arkElement!.bytesData.match(/"app"\s*:\s*"([^"]*)"/)
if (!match) {
  segments.push({
    type: 'light_app',
    data: {
      app_name: '',
      json_payload: arkElement!.bytesData,
    },
  })
  break
}
// existing com.tencent.multimsg / other app handling...
```
</issue_to_address>

### Comment 3
<location path="src/ntqqapi/api/file.ts" line_range="506" />
<code_context>
-      order: 1,
-      lastRecord: _lastRecord,
-    }])
+  async uploadGroupImage(groupCode: string, filePath: string) {
+    const result = await this.ctx.pmhq.getGroupImageUploadInfo(groupCode, filePath)
+    const highwaySession = await this.ctx.pmhq.getHighwaySession()
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting the shared Highway upload logic from `uploadGroupImage` and `uploadC2CImage` into a single private helper to avoid duplicated code and centralize behavior.

You can remove the duplication between `uploadGroupImage` and `uploadC2CImage` by extracting the common Highway upload path into a private helper that takes only the varying pieces (`cmd` and how to get upload info).

For example:

```ts
private async uploadImageViaHighway(
  filePath: string,
  cmd: number,
  getInfo: () => Promise<{
    ext: { uKey?: string; msgInfoBody: any[] };
    info: any;
    compat: any;
  }>
) {
  const result = await getInfo()
  const highwaySession = await this.ctx.pmhq.getHighwaySession()
  const maxBlockSize = 1024 * 1024

  if (result.ext.uKey) {
    const { index } = result.ext.msgInfoBody[0]
    const trans = {
      uin: selfInfo.uin,
      cmd,
      readable: createReadStream(filePath, { highWaterMark: maxBlockSize }),
      sum: Buffer.from(index.info.md5HexStr, 'hex'),
      size: index.info.fileSize,
      ticket: highwaySession.sigSession,
      ext: Media.NTV2RichMediaHighwayExt.encode(result.ext),
      server: highwaySession.highwayHostAndPorts[1][0].host,
      port: highwaySession.highwayHostAndPorts[1][0].port,
    }
    try {
      await new HighwayTcpSession(trans).upload()
    } catch {
      await new HighwayHttpSession(trans).upload()
    }
  }

  return {
    msgInfo: result.info,
    compat: result.compat,
  }
}
```

Then both public methods become thin wrappers, keeping all behavior intact but reducing boilerplate:

```ts
async uploadGroupImage(groupCode: string, filePath: string) {
  return this.uploadImageViaHighway(filePath, 1004, () =>
    this.ctx.pmhq.getGroupImageUploadInfo(groupCode, filePath)
  )
}

async uploadC2CImage(peerUid: string, filePath: string) {
  return this.uploadImageViaHighway(filePath, 1003, () =>
    this.ctx.pmhq.getC2CImageUploadInfo(peerUid, filePath)
  )
}
```

This centralizes the Highway session construction and upload logic, making future changes (e.g. error handling, block size, session selection) easier and less error-prone.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +19 to +28
const result = await this.ctx.ntFriendApi.getFriendList(true)
return result.categories.values().map(item => ({
categoryId: item.categoryId,
categorySortId: item.categorySortId,
categoryName: item.categoryName,
categoryMbCount: item.categoryMemberCount,
buddyList: result.friends
.filter(friend => friend.categoryId === item.categoryId)
.map(friend => {
return OB11Entities.friend(friend)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): 使用 result.categories.values().map(...)).toArray() 会在运行时抛出异常,因为 Map.prototype.values() 返回的是不带 map/toArray 的迭代器。

由于 result.categories 是一个 Map<number, Category>result.categories.values() 返回的是 IterableIterator<Category>,它不支持 maptoArray,因此在运行时会抛出 TypeError。在进行 map 之前需要先把迭代器转换为数组,例如:

const categories = Array.from(result.categories.values()).map(item => ({
  categoryId: item.categoryId,
  categorySortId: item.categorySortId,
  categoryName: item.categoryName,
  categoryMbCount: item.categoryMemberCount,
  buddyList: result.friends
    .filter(friend => friend.categoryId === item.categoryId)
    .map(friend => OB11Entities.friend(friend)),
}))
return categories

如果你在使用某个 helper 为可迭代对象添加了 map/toArray,请将 result.categories.values() 先交给该 helper 处理,而不是直接在迭代器上调用这些方法。

Original comment in English

issue (bug_risk): The use of result.categories.values().map(...)).toArray() will throw at runtime because Map.prototype.values() returns an iterator without map/toArray.

Because result.categories is a Map<number, Category>, result.categories.values() returns an IterableIterator<Category> that does not support map or toArray, so this will throw a TypeError at runtime. Convert the iterator to an array before mapping, e.g.

const categories = Array.from(result.categories.values()).map(item => ({
  categoryId: item.categoryId,
  categorySortId: item.categorySortId,
  categoryName: item.categoryName,
  categoryMbCount: item.categoryMemberCount,
  buddyList: result.friends
    .filter(friend => friend.categoryId === item.categoryId)
    .map(friend => OB11Entities.friend(friend)),
}))
return categories

If you’re using a helper that adds map/toArray to iterables, wrap result.categories.values() with that helper rather than calling these methods directly on the iterator.

Comment on lines +210 to +219
const match = arkElement!.bytesData.match(/"app"\s*:\s*"([^"]*)"/)
if (match?.[1]) {
if (match[1] === 'com.tencent.multimsg') {
const data = JSON.parse(arkElement!.bytesData)
segments.push({
type: 'forward',
data: {
forward_id: data.meta.detail.resid,
title: data.meta.detail.source,
preview: data.meta.detail.news.map((item: { text: string }) => item.text),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk):bytesData 中不包含 app 字段时,该 Ark 元素现在会被静默丢弃,而不是以 light_app 的形式暴露出来。

在新的逻辑下,如果正则没有匹配到 "app" 字段,该元素会直接穿过这个 case 而不向 segments 中推入任何内容,导致这部分消息内容被丢弃。这相较于之前的行为是一种回归:之前任何非 com.tencent.multimsg 的 Ark payload 都会以带原始 bytesDatalight_app 形式发出。

为了在避免不必要的 JSON.parse 的同时保持向后兼容,可以在 match 为 falsy 时回退到 light_app

const match = arkElement!.bytesData.match(/"app"\s*:\s*"([^"]*)"/)
if (!match) {
  segments.push({
    type: 'light_app',
    data: {
      app_name: '',
      json_payload: arkElement!.bytesData,
    },
  })
  break
}
// existing com.tencent.multimsg / other app handling...
Original comment in English

issue (bug_risk): When bytesData does not contain an app field, the Ark element is now silently dropped instead of being exposed as a light_app.

With the new logic, if the regex doesn’t find an "app" field, the element falls through this case without pushing any segment, so that message content is dropped. That’s a regression from the previous behavior where any non-com.tencent.multimsg Ark payload was emitted as a light_app with the raw bytesData.

To preserve backward compatibility while still avoiding unnecessary JSON.parse, you can fall back to light_app when match is falsy:

const match = arkElement!.bytesData.match(/"app"\s*:\s*"([^"]*)"/)
if (!match) {
  segments.push({
    type: 'light_app',
    data: {
      app_name: '',
      json_payload: arkElement!.bytesData,
    },
  })
  break
}
// existing com.tencent.multimsg / other app handling...

Comment thread src/ntqqapi/api/file.ts
order: 1,
lastRecord: _lastRecord,
}])
async uploadGroupImage(groupCode: string, filePath: string) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): 建议将 uploadGroupImageuploadC2CImage 中共享的 Highway 上传逻辑提取到一个私有 helper 中,以避免重复代码并集中管理行为。

你可以通过抽取一个私有 helper,把公共的 Highway 上传流程合并,并将 cmd 和获取上传信息的方式作为参数传入,从而消除 uploadGroupImageuploadC2CImage 之间的重复。

例如:

private async uploadImageViaHighway(
  filePath: string,
  cmd: number,
  getInfo: () => Promise<{
    ext: { uKey?: string; msgInfoBody: any[] };
    info: any;
    compat: any;
  }>
) {
  const result = await getInfo()
  const highwaySession = await this.ctx.pmhq.getHighwaySession()
  const maxBlockSize = 1024 * 1024

  if (result.ext.uKey) {
    const { index } = result.ext.msgInfoBody[0]
    const trans = {
      uin: selfInfo.uin,
      cmd,
      readable: createReadStream(filePath, { highWaterMark: maxBlockSize }),
      sum: Buffer.from(index.info.md5HexStr, 'hex'),
      size: index.info.fileSize,
      ticket: highwaySession.sigSession,
      ext: Media.NTV2RichMediaHighwayExt.encode(result.ext),
      server: highwaySession.highwayHostAndPorts[1][0].host,
      port: highwaySession.highwayHostAndPorts[1][0].port,
    }
    try {
      await new HighwayTcpSession(trans).upload()
    } catch {
      await new HighwayHttpSession(trans).upload()
    }
  }

  return {
    msgInfo: result.info,
    compat: result.compat,
  }
}

然后这两个 public 方法就可以变成很薄的一层封装,在保持所有行为不变的同时减少样板代码:

async uploadGroupImage(groupCode: string, filePath: string) {
  return this.uploadImageViaHighway(filePath, 1004, () =>
    this.ctx.pmhq.getGroupImageUploadInfo(groupCode, filePath)
  )
}

async uploadC2CImage(peerUid: string, filePath: string) {
  return this.uploadImageViaHighway(filePath, 1003, () =>
    this.ctx.pmhq.getC2CImageUploadInfo(peerUid, filePath)
  )
}

这样可以将 Highway 会话构造和上传逻辑集中起来,未来如果需要修改(例如错误处理、分块大小、会话选择等),会更容易且不易出错。

Original comment in English

issue (complexity): Consider extracting the shared Highway upload logic from uploadGroupImage and uploadC2CImage into a single private helper to avoid duplicated code and centralize behavior.

You can remove the duplication between uploadGroupImage and uploadC2CImage by extracting the common Highway upload path into a private helper that takes only the varying pieces (cmd and how to get upload info).

For example:

private async uploadImageViaHighway(
  filePath: string,
  cmd: number,
  getInfo: () => Promise<{
    ext: { uKey?: string; msgInfoBody: any[] };
    info: any;
    compat: any;
  }>
) {
  const result = await getInfo()
  const highwaySession = await this.ctx.pmhq.getHighwaySession()
  const maxBlockSize = 1024 * 1024

  if (result.ext.uKey) {
    const { index } = result.ext.msgInfoBody[0]
    const trans = {
      uin: selfInfo.uin,
      cmd,
      readable: createReadStream(filePath, { highWaterMark: maxBlockSize }),
      sum: Buffer.from(index.info.md5HexStr, 'hex'),
      size: index.info.fileSize,
      ticket: highwaySession.sigSession,
      ext: Media.NTV2RichMediaHighwayExt.encode(result.ext),
      server: highwaySession.highwayHostAndPorts[1][0].host,
      port: highwaySession.highwayHostAndPorts[1][0].port,
    }
    try {
      await new HighwayTcpSession(trans).upload()
    } catch {
      await new HighwayHttpSession(trans).upload()
    }
  }

  return {
    msgInfo: result.info,
    compat: result.compat,
  }
}

Then both public methods become thin wrappers, keeping all behavior intact but reducing boilerplate:

async uploadGroupImage(groupCode: string, filePath: string) {
  return this.uploadImageViaHighway(filePath, 1004, () =>
    this.ctx.pmhq.getGroupImageUploadInfo(groupCode, filePath)
  )
}

async uploadC2CImage(peerUid: string, filePath: string) {
  return this.uploadImageViaHighway(filePath, 1003, () =>
    this.ctx.pmhq.getC2CImageUploadInfo(peerUid, filePath)
  )
}

This centralizes the Highway session construction and upload logic, making future changes (e.g. error handling, block size, session selection) easier and less error-prone.

@idranme idranme merged commit 1798b8c into main May 15, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant