Conversation
…nfirmation prompt
Reviewer’s Guide通过设置对话框引入 WebUI 登出流程;改进 VIP 标记处理和用户信息获取;优化群聊 @ 提及显示名;调整 WebUI 开发服务器和 CI 安装行为;微调消息时间过滤逻辑,并将应用版本升级到 7.12.10。 从设置对话框进行 WebUI 登出交互的时序图sequenceDiagram
actor User
participant SettingsDialog
participant App
participant Browser
User->>SettingsDialog: Click logout button
SettingsDialog->>SettingsDialog: setShowLogoutConfirm(true)
SettingsDialog-->>User: Show confirmation modal
User->>SettingsDialog: Click Confirm logout
SettingsDialog->>App: onLogout()
App->>Browser: deleteCookie(webui_token)
App->>Browser: window.location.reload()
Browser-->>User: Reloaded page without WebUI session
优化群聊 @ 提及显示名解析的时序图sequenceDiagram
participant Sender
participant MessageBuilder as MessageBuilder_createSendElements
participant NTQQUserApi as NTQQUserApi
participant NTQQGroupApi as NTQQGroupApi
Sender->>MessageBuilder: Provide segment type mention with user_id
MessageBuilder->>NTQQUserApi: getUidByUin(memberUin, peerUid)
NTQQUserApi-->>MessageBuilder: memberUid
MessageBuilder->>NTQQGroupApi: getGroupMember(peerUid, memberUid)
NTQQGroupApi-->>MessageBuilder: memberInfo(cardName, nick)
MessageBuilder->>MessageBuilder: Build displayName = "@" + (cardName or nick)
MessageBuilder-->>Sender: SendElement.at(..., displayName)
在 UserMixin 中提取 VIP 信息的时序图sequenceDiagram
participant UserMixin as UserMixin_getUser
participant PMHQ as PMHQBase
participant Misc as Misc_UserInfoBusiness
UserMixin->>PMHQ: fetch raw user info
PMHQ-->>UserMixin: info(body.properties)
UserMixin->>UserMixin: bytes[107] ?
alt has business bytes
UserMixin->>Misc: decode(bytes[107])
Misc-->>UserMixin: business
UserMixin->>UserMixin: vipInfo = business.body.lists.find(type == 1)
UserMixin->>UserMixin: isVip = !!vipInfo
UserMixin->>UserMixin: isYearsVip = !!vipInfo.isYear
UserMixin->>UserMixin: vipLevel = vipInfo.level or 0
else no business bytes
UserMixin->>UserMixin: isVip = false
UserMixin->>UserMixin: isYearsVip = false
UserMixin->>UserMixin: vipLevel = 0
end
UserMixin-->>PMHQ: Return normalized user data
更新后的 SettingsDialog props 与 NTQQ 用户 API 的类图classDiagram
class SettingsDialogProps {
+boolean visible
+function onClose()
+function onLogout() optional
}
class SettingsDialog {
+SettingsDialogProps props
-boolean showLogoutConfirm
+render()
}
class App {
-boolean showSettingsDialog
+render()
+handleLogout()
}
SettingsDialogProps <.. SettingsDialog : uses
App o-- SettingsDialog : renders
class NTQQUserApi {
+getUidByUin(uin string, peerUid string) Promise~string~
+fetchUserDetailInfo(uid string) Promise
+getUserSimpleInfo(uid string, force boolean) Promise~SimpleInfo~
+getCoreAndBaseInfo(uids string[]) Promise
}
class UserMixinResult {
+string uin
+string nick
+string longNick
+number age
+number gender
+string location
+string avatarUrl
+string qid
+string qidName
+string sign
+string status
+boolean isVip
+boolean isYearsVip
+number vipLevel
+string[] labels
+string school
+string remark
}
class UserMixin {
+getUser(uid string) Promise~UserMixinResult~
}
UserMixinResult <.. UserMixin : returns
NTQQUserApi <.. UserMixin : uses
文件级变更
提示与命令与 Sourcery 交互
自定义你的体验访问你的 控制台 以:
获取帮助Original review guide in EnglishReviewer's GuideIntroduces a WebUI logout flow from the settings dialog, improves VIP flag handling and user info fetching, refines @-mention display names, adjusts WebUI dev server and CI install behavior, tweaks message time filtering, and bumps the app version to 7.12.10. Sequence diagram for WebUI logout interaction from settings dialogsequenceDiagram
actor User
participant SettingsDialog
participant App
participant Browser
User->>SettingsDialog: Click logout button
SettingsDialog->>SettingsDialog: setShowLogoutConfirm(true)
SettingsDialog-->>User: Show confirmation modal
User->>SettingsDialog: Click Confirm logout
SettingsDialog->>App: onLogout()
App->>Browser: deleteCookie(webui_token)
App->>Browser: window.location.reload()
Browser-->>User: Reloaded page without WebUI session
Sequence diagram for improved group @-mention display resolutionsequenceDiagram
participant Sender
participant MessageBuilder as MessageBuilder_createSendElements
participant NTQQUserApi as NTQQUserApi
participant NTQQGroupApi as NTQQGroupApi
Sender->>MessageBuilder: Provide segment type mention with user_id
MessageBuilder->>NTQQUserApi: getUidByUin(memberUin, peerUid)
NTQQUserApi-->>MessageBuilder: memberUid
MessageBuilder->>NTQQGroupApi: getGroupMember(peerUid, memberUid)
NTQQGroupApi-->>MessageBuilder: memberInfo(cardName, nick)
MessageBuilder->>MessageBuilder: Build displayName = "@" + (cardName or nick)
MessageBuilder-->>Sender: SendElement.at(..., displayName)
Sequence diagram for VIP info extraction in UserMixinsequenceDiagram
participant UserMixin as UserMixin_getUser
participant PMHQ as PMHQBase
participant Misc as Misc_UserInfoBusiness
UserMixin->>PMHQ: fetch raw user info
PMHQ-->>UserMixin: info(body.properties)
UserMixin->>UserMixin: bytes[107] ?
alt has business bytes
UserMixin->>Misc: decode(bytes[107])
Misc-->>UserMixin: business
UserMixin->>UserMixin: vipInfo = business.body.lists.find(type == 1)
UserMixin->>UserMixin: isVip = !!vipInfo
UserMixin->>UserMixin: isYearsVip = !!vipInfo.isYear
UserMixin->>UserMixin: vipLevel = vipInfo.level or 0
else no business bytes
UserMixin->>UserMixin: isVip = false
UserMixin->>UserMixin: isYearsVip = false
UserMixin->>UserMixin: vipLevel = 0
end
UserMixin-->>PMHQ: Return normalized user data
Class diagram for updated SettingsDialog props and NTQQ user APIsclassDiagram
class SettingsDialogProps {
+boolean visible
+function onClose()
+function onLogout() optional
}
class SettingsDialog {
+SettingsDialogProps props
-boolean showLogoutConfirm
+render()
}
class App {
-boolean showSettingsDialog
+render()
+handleLogout()
}
SettingsDialogProps <.. SettingsDialog : uses
App o-- SettingsDialog : renders
class NTQQUserApi {
+getUidByUin(uin string, peerUid string) Promise~string~
+fetchUserDetailInfo(uid string) Promise
+getUserSimpleInfo(uid string, force boolean) Promise~SimpleInfo~
+getCoreAndBaseInfo(uids string[]) Promise
}
class UserMixinResult {
+string uin
+string nick
+string longNick
+number age
+number gender
+string location
+string avatarUrl
+string qid
+string qidName
+string sign
+string status
+boolean isVip
+boolean isYearsVip
+number vipLevel
+string[] labels
+string school
+string remark
}
class UserMixin {
+getUser(uid string) Promise~UserMixinResult~
}
UserMixinResult <.. UserMixin : returns
NTQQUserApi <.. UserMixin : uses
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - 我发现了 5 个问题,并给出了一些整体反馈:
- 在
SettingsDialog中,onLogout是可选的,但登出 UI 始终显示,并且确认按钮在点击时无条件调用它;建议在未提供onLogout时隐藏登出按钮,或者在调用时加上保护,以避免无效操作 / 令人困惑的交互体验。 - 在
transformOutgoingMessage、createSendElements和MessageEncoder中构建 @ 提及时新增的getGroupMember调用假定请求一定成功且cardName/nick一定非空;建议增加失败或数据缺失时的处理,以避免运行时错误,并在群成员信息不可用时仍能保持消息发送功能正常。
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `SettingsDialog`, `onLogout` is optional but the logout UI is always shown and the confirm button calls it unconditionally; consider hiding the logout button when `onLogout` is not provided or guarding the call to avoid a no-op/confusing UX.
- The new `getGroupMember` calls when building @-mentions in `transformOutgoingMessage`, `createSendElements`, and `MessageEncoder` assume a successful response and non-empty `cardName`/`nick`; consider handling failures or missing data to avoid runtime errors and preserve message sending when group member info is unavailable.
## Individual Comments
### Comment 1
<location path="src/webui/FE/components/common/SettingsDialog.tsx" line_range="15-18" />
<code_context>
+const SettingsDialog: React.FC<SettingsDialogProps> = ({ visible, onClose, onLogout }) => {
const { mode, setMode } = useThemeStore()
const { autoHideSidebarInWebQQ, setAutoHideSidebarInWebQQ, showWebQQFullscreenButton, setShowWebQQFullscreenButton } = useSettingsStore()
+ const [showLogoutConfirm, setShowLogoutConfirm] = React.useState(false)
if (!visible) return null
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard optional `onLogout` and consider hiding the logout entry when it is not provided.
Since `onLogout` is optional but the UI always renders a logout button and calls it unconditionally, omitting the prop leaves the confirm dialog with a non-functional confirm button. Consider either only rendering the logout button/dialog when `onLogout` is defined, or guarding the handler (e.g. `onClick={() => { onLogout?.(); setShowLogoutConfirm(false); }}`) so the dialog still closes if no callback is provided.
</issue_to_address>
### Comment 2
<location path="src/ntqqapi/api/user.ts" line_range="76" />
<code_context>
return (await this.ctx.pmhq.invoke('nodeIKernelUixConvertService/getUin', [[uid]])).uinInfo.get(uid)
},
async () => {
- return (await this.fetchUserDetailInfo(uid)).detail.get(uid)?.uin
+ return (await this.getUserSimpleInfo(uid)).uin
},
]
</code_context>
<issue_to_address>
**issue:** New `getUidByUin` fallback is stricter and may throw when user info is missing.
The previous fallback used `fetchUserDetailInfo(uid)).detail.get(uid)?.uin`, which safely returned `undefined` when the user detail was missing. `getUserSimpleInfo(uid)` instead does `data.get(uid)!`, which will throw if the user is not in the map (e.g., cache miss or API quirk). If callers expect an optional result, this changes behavior from "undefined" to "exception". Please either make `getUserSimpleInfo` handle a missing entry or access the map safely here and keep returning `undefined` instead of throwing.
</issue_to_address>
### Comment 3
<location path="src/ntqqapi/api/msg.ts" line_range="293" />
<code_context>
chatInfo: peer,
filterMsgType: [],
filterSendersUid,
- filterMsgToTime: filterMsgTime,
+ filterMsgToTime: String(filterMsgTime + 1), // 获取到的消息时间可能比 replyMsgTime 多一毫秒
filterMsgFromTime: filterMsgTime,
isReverseOrder: true,
</code_context>
<issue_to_address>
**issue (bug_risk):** Inconsistent typing between `filterMsgToTime` and `filterMsgFromTime` may be problematic.
`filterMsgToTime` is now passed as `String(filterMsgTime + 1)` while `filterMsgFromTime` is still a number. If the API expects these fields to share a type, this mismatch could lead to subtle issues. Consider normalizing both fields, e.g. `String(filterMsgTime)` and `String(filterMsgTime + 1)`, or both as numbers, based on the PMHQ/NT API contract.
</issue_to_address>
### Comment 4
<location path="src/onebot11/helper/createMessage.ts" line_range="67-68" />
<code_context>
}
else if (peer.chatType === ChatType.Group) {
const uid = await ctx.ntUserApi.getUidByUin(atQQ, peer.peerUid)
- let display = ''
+ const info = await ctx.ntGroupApi.getGroupMember(peer.peerUid, uid)
+ let display = `@${info.cardName || info.nick}`
if (segment.data.name) {
</code_context>
<issue_to_address>
**suggestion (performance):** Avoid unnecessary `getGroupMember` call when a custom mention name is provided.
`getGroupMember` is always called even though its result is discarded when `segment.data.name` is set. Consider branching on `segment.data.name` first, and only calling `getGroupMember` in the `else` path so the API is only used when no custom name is provided.
</issue_to_address>
### Comment 5
<location path="src/satori/message.ts" line_range="385-386" />
<code_context>
this.elements.push(SendElement.at('', '', NT.AtType.All, '@全体成员'))
} else {
const uid = await this.ctx.ntUserApi.getUidByUin(attrs.id, this.peer.peerUid)
- const display = attrs.name ? '@' + attrs.name : ''
+ const info = await this.ctx.ntGroupApi.getGroupMember(this.peer.peerUid, uid)
+ const display = attrs.name ? '@' + attrs.name : `@${info.cardName || info.nick}`
this.elements.push(SendElement.at(attrs.id, uid, NT.AtType.One, display))
</code_context>
<issue_to_address>
**suggestion (performance):** Similar to OneBot path, `getGroupMember` is done even when `attrs.name` is provided.
Here, as in `createMessage.ts`, `getGroupMember` is always called even though `attrs.name` may already provide the display text. To avoid unnecessary network calls and handle failures more robustly, only fetch the group member when `attrs.name` is absent, and guard the `getGroupMember` result with try/catch or a null-check in case the member cannot be resolved.
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的 Review。
Original comment in English
Hey - I've found 5 issues, and left some high level feedback:
- In
SettingsDialog,onLogoutis optional but the logout UI is always shown and the confirm button calls it unconditionally; consider hiding the logout button whenonLogoutis not provided or guarding the call to avoid a no-op/confusing UX. - The new
getGroupMembercalls when building @-mentions intransformOutgoingMessage,createSendElements, andMessageEncoderassume a successful response and non-emptycardName/nick; consider handling failures or missing data to avoid runtime errors and preserve message sending when group member info is unavailable.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `SettingsDialog`, `onLogout` is optional but the logout UI is always shown and the confirm button calls it unconditionally; consider hiding the logout button when `onLogout` is not provided or guarding the call to avoid a no-op/confusing UX.
- The new `getGroupMember` calls when building @-mentions in `transformOutgoingMessage`, `createSendElements`, and `MessageEncoder` assume a successful response and non-empty `cardName`/`nick`; consider handling failures or missing data to avoid runtime errors and preserve message sending when group member info is unavailable.
## Individual Comments
### Comment 1
<location path="src/webui/FE/components/common/SettingsDialog.tsx" line_range="15-18" />
<code_context>
+const SettingsDialog: React.FC<SettingsDialogProps> = ({ visible, onClose, onLogout }) => {
const { mode, setMode } = useThemeStore()
const { autoHideSidebarInWebQQ, setAutoHideSidebarInWebQQ, showWebQQFullscreenButton, setShowWebQQFullscreenButton } = useSettingsStore()
+ const [showLogoutConfirm, setShowLogoutConfirm] = React.useState(false)
if (!visible) return null
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard optional `onLogout` and consider hiding the logout entry when it is not provided.
Since `onLogout` is optional but the UI always renders a logout button and calls it unconditionally, omitting the prop leaves the confirm dialog with a non-functional confirm button. Consider either only rendering the logout button/dialog when `onLogout` is defined, or guarding the handler (e.g. `onClick={() => { onLogout?.(); setShowLogoutConfirm(false); }}`) so the dialog still closes if no callback is provided.
</issue_to_address>
### Comment 2
<location path="src/ntqqapi/api/user.ts" line_range="76" />
<code_context>
return (await this.ctx.pmhq.invoke('nodeIKernelUixConvertService/getUin', [[uid]])).uinInfo.get(uid)
},
async () => {
- return (await this.fetchUserDetailInfo(uid)).detail.get(uid)?.uin
+ return (await this.getUserSimpleInfo(uid)).uin
},
]
</code_context>
<issue_to_address>
**issue:** New `getUidByUin` fallback is stricter and may throw when user info is missing.
The previous fallback used `fetchUserDetailInfo(uid)).detail.get(uid)?.uin`, which safely returned `undefined` when the user detail was missing. `getUserSimpleInfo(uid)` instead does `data.get(uid)!`, which will throw if the user is not in the map (e.g., cache miss or API quirk). If callers expect an optional result, this changes behavior from "undefined" to "exception". Please either make `getUserSimpleInfo` handle a missing entry or access the map safely here and keep returning `undefined` instead of throwing.
</issue_to_address>
### Comment 3
<location path="src/ntqqapi/api/msg.ts" line_range="293" />
<code_context>
chatInfo: peer,
filterMsgType: [],
filterSendersUid,
- filterMsgToTime: filterMsgTime,
+ filterMsgToTime: String(filterMsgTime + 1), // 获取到的消息时间可能比 replyMsgTime 多一毫秒
filterMsgFromTime: filterMsgTime,
isReverseOrder: true,
</code_context>
<issue_to_address>
**issue (bug_risk):** Inconsistent typing between `filterMsgToTime` and `filterMsgFromTime` may be problematic.
`filterMsgToTime` is now passed as `String(filterMsgTime + 1)` while `filterMsgFromTime` is still a number. If the API expects these fields to share a type, this mismatch could lead to subtle issues. Consider normalizing both fields, e.g. `String(filterMsgTime)` and `String(filterMsgTime + 1)`, or both as numbers, based on the PMHQ/NT API contract.
</issue_to_address>
### Comment 4
<location path="src/onebot11/helper/createMessage.ts" line_range="67-68" />
<code_context>
}
else if (peer.chatType === ChatType.Group) {
const uid = await ctx.ntUserApi.getUidByUin(atQQ, peer.peerUid)
- let display = ''
+ const info = await ctx.ntGroupApi.getGroupMember(peer.peerUid, uid)
+ let display = `@${info.cardName || info.nick}`
if (segment.data.name) {
</code_context>
<issue_to_address>
**suggestion (performance):** Avoid unnecessary `getGroupMember` call when a custom mention name is provided.
`getGroupMember` is always called even though its result is discarded when `segment.data.name` is set. Consider branching on `segment.data.name` first, and only calling `getGroupMember` in the `else` path so the API is only used when no custom name is provided.
</issue_to_address>
### Comment 5
<location path="src/satori/message.ts" line_range="385-386" />
<code_context>
this.elements.push(SendElement.at('', '', NT.AtType.All, '@全体成员'))
} else {
const uid = await this.ctx.ntUserApi.getUidByUin(attrs.id, this.peer.peerUid)
- const display = attrs.name ? '@' + attrs.name : ''
+ const info = await this.ctx.ntGroupApi.getGroupMember(this.peer.peerUid, uid)
+ const display = attrs.name ? '@' + attrs.name : `@${info.cardName || info.nick}`
this.elements.push(SendElement.at(attrs.id, uid, NT.AtType.One, display))
</code_context>
<issue_to_address>
**suggestion (performance):** Similar to OneBot path, `getGroupMember` is done even when `attrs.name` is provided.
Here, as in `createMessage.ts`, `getGroupMember` is always called even though `attrs.name` may already provide the display text. To avoid unnecessary network calls and handle failures more robustly, only fetch the group member when `attrs.name` is absent, and guard the `getGroupMember` result with try/catch or a null-check in case the member cannot be resolved.
</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 [showLogoutConfirm, setShowLogoutConfirm] = React.useState(false) | ||
|
|
||
| if (!visible) return null | ||
|
|
There was a problem hiding this comment.
issue (bug_risk): 需要保护可选的 onLogout,并在未提供该回调时考虑隐藏登出入口。
由于 onLogout 是可选的,但 UI 始终渲染登出按钮且在点击时无条件调用它,如果不传这个 prop,则确认对话框中的确认按钮会变成无效操作。建议要么只在 onLogout 已定义时渲染登出按钮 / 对话框,要么在处理函数中加保护(例如 onClick={() => { onLogout?.(); setShowLogoutConfirm(false); }}),这样即使没有回调也能正常关闭对话框。
Original comment in English
issue (bug_risk): Guard optional onLogout and consider hiding the logout entry when it is not provided.
Since onLogout is optional but the UI always renders a logout button and calls it unconditionally, omitting the prop leaves the confirm dialog with a non-functional confirm button. Consider either only rendering the logout button/dialog when onLogout is defined, or guarding the handler (e.g. onClick={() => { onLogout?.(); setShowLogoutConfirm(false); }}) so the dialog still closes if no callback is provided.
| return (await this.ctx.pmhq.invoke('nodeIKernelUixConvertService/getUin', [[uid]])).uinInfo.get(uid) | ||
| }, | ||
| async () => { | ||
| return (await this.fetchUserDetailInfo(uid)).detail.get(uid)?.uin |
There was a problem hiding this comment.
issue: 新的 getUidByUin 兜底逻辑更严格,在用户信息缺失时可能抛异常。
之前的兜底逻辑使用的是 fetchUserDetailInfo(uid)).detail.get(uid)?.uin,当用户详情缺失时会安全地返回 undefined。而 getUserSimpleInfo(uid) 内部使用的是 data.get(uid)!,如果该用户不在 map 中(例如缓存未命中或 API 行为异常),就会抛出异常。如果调用方预期拿到的是一个可选结果,那么这相当于从“返回 undefined”变成了“抛异常”。请考虑让 getUserSimpleInfo 在条目缺失时安全处理,或者在此处以安全方式访问 map,保持返回 undefined 而不是抛出异常。
Original comment in English
issue: New getUidByUin fallback is stricter and may throw when user info is missing.
The previous fallback used fetchUserDetailInfo(uid)).detail.get(uid)?.uin, which safely returned undefined when the user detail was missing. getUserSimpleInfo(uid) instead does data.get(uid)!, which will throw if the user is not in the map (e.g., cache miss or API quirk). If callers expect an optional result, this changes behavior from "undefined" to "exception". Please either make getUserSimpleInfo handle a missing entry or access the map safely here and keep returning undefined instead of throwing.
| chatInfo: peer, | ||
| filterMsgType: [], | ||
| filterSendersUid, | ||
| filterMsgToTime: filterMsgTime, |
There was a problem hiding this comment.
issue (bug_risk): filterMsgToTime 与 filterMsgFromTime 之间类型不一致,可能带来问题。
现在 filterMsgToTime 传的是 String(filterMsgTime + 1),而 filterMsgFromTime 仍然是一个数字。如果 API 预期这两个字段的类型一致,这种不匹配可能会导致一些隐蔽的问题。建议根据 PMHQ/NT API 的约定统一这两个字段的类型,例如都传字符串(String(filterMsgTime) 和 String(filterMsgTime + 1)),或者都传数字。
Original comment in English
issue (bug_risk): Inconsistent typing between filterMsgToTime and filterMsgFromTime may be problematic.
filterMsgToTime is now passed as String(filterMsgTime + 1) while filterMsgFromTime is still a number. If the API expects these fields to share a type, this mismatch could lead to subtle issues. Consider normalizing both fields, e.g. String(filterMsgTime) and String(filterMsgTime + 1), or both as numbers, based on the PMHQ/NT API contract.
Test Report
✅ All tests passed |
Summary by Sourcery
为 WebUI 增加注销支持,改进消息 @ 提及和回复处理,优化用户信息获取方式,并更新工具配置和版本。
New Features:
Bug Fixes:
Enhancements:
Build:
Chores:
Original summary in English
Summary by Sourcery
Add WebUI logout support, improve message mention and reply handling, refine user info retrieval, and update tooling configuration and version.
New Features:
Bug Fixes:
Enhancements:
Build:
Chores: