Conversation
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
通过 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
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your Experience访问你的 dashboard 以:
Getting HelpOriginal review guide in EnglishReviewer's GuideRefactors 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/imageOcrsequenceDiagram
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
Sequence diagram for new friend list caching via fetchFriendssequenceDiagram
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
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Test Report
✅ All tests passed |
There was a problem hiding this comment.
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>帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进后续评审。
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 becauseMap.prototype.values()returns an iterator withoutmap/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)!andbiz.data.get(20031)![2]inNTQQFriendApi.getFriendList, anddownload!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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| 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) |
There was a problem hiding this comment.
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 之前需要先把迭代器转换为数组,例如:
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 categoriesIf 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.
| 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), |
There was a problem hiding this comment.
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:
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...| order: 1, | ||
| lastRecord: _lastRecord, | ||
| }]) | ||
| async uploadGroupImage(groupCode: string, filePath: string) { |
There was a problem hiding this comment.
issue (complexity): 建议将 uploadGroupImage 与 uploadC2CImage 中共享的 Highway 上传逻辑提取到一个私有 helper 中,以避免重复代码并集中管理行为。
你可以通过抽取一个私有 helper,把公共的 Highway 上传流程合并,并将 cmd 和获取上传信息的方式作为参数传入,从而消除 uploadGroupImage 与 uploadC2CImage 之间的重复。
例如:
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.
Summary by Sourcery
重构媒体、文件和好友处理逻辑,改用新的基于 PMHQ OIDB 的 API 和简化的数据模型,并更新 OneBot、Milky、Satori 与 WebUI 中的集成,同时提升项目版本号。
New Features:
Bug Fixes:
Enhancements:
Build:
Chores:
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:
Bug Fixes:
Enhancements:
Build:
Chores: