Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,11 @@ jobs:

- name: Install dependencies
run: |
export ELECTRON_SKIP_BINARY_DOWNLOAD=1
corepack enable
yarn install
yarn install --no-immutable
cd src/webui/FE
npm install

- name: Build
run: |
Expand Down
9 changes: 9 additions & 0 deletions doc/更新日志.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
V7.12.10
更新时间 2026-04-30

* 修复偶现获取不到引用的消息
* 修复发送的 at 消息可能显示为空白
* 修复 get_stranger_info API 获取的会员相关信息不正确
* 支持退出 WebUI 登录

=================
V7.12.9
更新时间 2026-04-29

Expand Down
2 changes: 1 addition & 1 deletion package-dist.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"name":"llonebot-dist","version":"7.12.9","type":"module","description":"","main":"llbot.js","author":"linyuchen","repository":{"type":"git","url":"https://github.com/LLOneBot/LuckyLilliaBot"}}
{"name":"llonebot-dist","version":"7.12.10","type":"module","description":"","main":"llbot.js","author":"linyuchen","repository":{"type":"git","url":"https://github.com/LLOneBot/LuckyLilliaBot"}}
7 changes: 4 additions & 3 deletions src/main/pmhq/mixins/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export function UserMixin<T extends new (...args: any[]) => PMHQBase>(Base: T) {
const numbers = Object.fromEntries(info.body.properties.numberProperties.map(p => [p.key, p.value]))
const bytes = Object.fromEntries(info.body.properties.bytesProperties.map(p => [p.key, p.value]))
const business = bytes[107] ? Misc.UserInfoBusiness.decode(bytes[107]) : undefined
const vipInfo = business?.body.lists.find((e) => e.type === 1)
return {
uin: info.body.uin,
nick: bytes[20002]?.toString() ?? '',
Expand All @@ -53,9 +54,9 @@ export function UserMixin<T extends new (...args: any[]) => PMHQBase>(Base: T) {
labels: bytes[104] ? Misc.UserInfoLabel.decode(bytes[104]).labels.map(e => e.content) : [],
school: bytes[20021]?.toString() ?? '',
remark: bytes[103]?.toString() ?? '',
isVip: !!business?.body.lists[0],
isYearsVip: !!business?.body.lists[0]?.isYear,
vipLevel: business?.body.lists[0]?.level ?? 0
isVip: !!vipInfo,
isYearsVip: !!vipInfo?.isYear,
vipLevel: vipInfo?.level ?? 0
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/milky/transform/message/outgoing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export async function transformOutgoingMessage(
} else if (segment.type === 'mention' && isGroup) {
const memberUin = segment.data.user_id.toString()
const memberUid = await ctx.ntUserApi.getUidByUin(memberUin, peerUid)
elements.push(SendElement.at(memberUin, memberUid, AtType.One, ''))
const info = await ctx.ntGroupApi.getGroupMember(peerUid, memberUid)
elements.push(SendElement.at(memberUin, memberUid, AtType.One, `@${info.cardName || info.nick}`))
} else if (segment.type === 'mention_all' && isGroup) {
elements.push(SendElement.at('', '', AtType.All, '@全体成员'))
} else if (segment.type === 'face') {
Expand Down
2 changes: 1 addition & 1 deletion src/ntqqapi/api/msg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ export class NTQQMsgApi extends Service {
chatInfo: peer,
filterMsgType: [],
filterSendersUid,
filterMsgToTime: filterMsgTime,
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): filterMsgToTimefilterMsgFromTime 之间类型不一致,可能带来问题。

现在 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.

filterMsgToTime: String(filterMsgTime + 1), // 获取到的消息时间可能比 replyMsgTime 多一毫秒
filterMsgFromTime: filterMsgTime,
isReverseOrder: true,
isIncludeCurrent: true,
Expand Down
6 changes: 4 additions & 2 deletions src/ntqqapi/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class NTQQUserApi extends Service {
return (await this.ctx.pmhq.invoke('nodeIKernelUixConvertService/getUin', [[uid]])).uinInfo.get(uid)
},
async () => {
return (await this.fetchUserDetailInfo(uid)).detail.get(uid)?.uin
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: 新的 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.

return (await this.getUserSimpleInfo(uid)).uin
},
]

Expand All @@ -91,7 +91,7 @@ export class NTQQUserApi extends Service {
return ''
}

// 这个会从服务器拉取,比较可靠
/** 始终会从服务器拉取 */
async fetchUserDetailInfo(uid: string) {
return await this.ctx.pmhq.invoke(
'nodeIKernelProfileService/fetchUserDetailInfo',
Expand Down Expand Up @@ -119,6 +119,7 @@ export class NTQQUserApi extends Service {
return result
}

/** 无缓存时会从服务器拉取 */
async getUserSimpleInfo(uid: string, force = true) {
const data = await this.ctx.pmhq.invoke<Map<string, SimpleInfo>>(
'nodeIKernelProfileService/getUserSimpleInfo',
Expand All @@ -134,6 +135,7 @@ export class NTQQUserApi extends Service {
return data.get(uid)!
}

/** 无缓存时会获取不到用户信息 */
async getCoreAndBaseInfo(uids: string[]) {
return await this.ctx.pmhq.invoke(
'nodeIKernelProfileService/getCoreAndBaseInfo',
Expand Down
1 change: 0 additions & 1 deletion src/ntqqapi/entities.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import ffmpeg from 'fluent-ffmpeg'
import faceConfig from './helper/face_config.json'
import pathLib from 'node:path'
import {
Expand Down
5 changes: 4 additions & 1 deletion src/onebot11/helper/createMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,12 @@ export async function createSendElements(
}
else if (peer.chatType === ChatType.Group) {
const uid = await ctx.ntUserApi.getUidByUin(atQQ, peer.peerUid)
let display = ''
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
let display
if (segment.data.name) {
display = `@${segment.data.name}`
} else {
const info = await ctx.ntGroupApi.getGroupMember(peer.peerUid, uid)
display = `@${info.cardName || info.nick}`
}
sendElements.push(SendElement.at(atQQ, uid, AtType.One, display))
}
Expand Down
8 changes: 7 additions & 1 deletion src/satori/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,13 @@ export class MessageEncoder {
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 : ''
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
let display
if (attrs.name) {
display = `@${attrs.name}`
} else {
const info = await this.ctx.ntGroupApi.getGroupMember(this.peer.peerUid, uid)
display = `@${info.cardName || info.nick}`
}
this.elements.push(SendElement.at(attrs.id, uid, NT.AtType.One, display))
}
} else if (type === 'a') {
Expand Down
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const version = '7.12.9'
export const version = '7.12.10'
5 changes: 5 additions & 0 deletions src/webui/FE/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { WebQQPage, WebQQFullscreen } from './components/WebQQ';
import { Config, ResConfig, EmailConfig } from './types';
import { apiFetch, setPasswordPromptHandler } from './utils/api';
import { deleteCookie } from './utils/cookie';
import { Save, Loader2, Eye, EyeOff, Plus, Trash2, Menu, Cpu, Milk, ExternalLink } from 'lucide-react';
import { defaultConfig } from '../../main/config/defaultConfig'
import { version } from '../../version'
Expand Down Expand Up @@ -936,6 +937,10 @@ function App() {
<SettingsDialog
visible={showSettingsDialog}
onClose={() => setShowSettingsDialog(false)}
onLogout={() => {
deleteCookie('webui_token')
window.location.reload()
}}
/>
</div>
);
Expand Down
42 changes: 39 additions & 3 deletions src/webui/FE/components/common/SettingsDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import React from 'react'
import { X, Sun, Moon, Monitor, Eye, EyeOff } from 'lucide-react'
import { X, Sun, Moon, Monitor, Eye, EyeOff, LogOut } from 'lucide-react'
import { useThemeStore } from '../../stores/themeStore'
import { useSettingsStore } from '../../stores/settingsStore'

interface SettingsDialogProps {
visible: boolean
onClose: () => void
onLogout?: () => void
}

const SettingsDialog: React.FC<SettingsDialogProps> = ({ visible, onClose }) => {
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

Comment on lines +15 to 18
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): 需要保护可选的 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.

Expand Down Expand Up @@ -111,7 +113,14 @@ const SettingsDialog: React.FC<SettingsDialogProps> = ({ visible, onClose }) =>
</div>

{/* Footer */}
<div className="flex justify-end gap-3 p-6 border-t border-theme-divider">
<div className="flex justify-between gap-3 p-6 border-t border-theme-divider">
<button
onClick={() => setShowLogoutConfirm(true)}
className="flex items-center gap-2 px-4 py-2.5 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-xl transition-colors font-medium"
>
<LogOut size={18} />
退出 WebUI
</button>
<button
onClick={onClose}
className="px-6 py-2.5 bg-theme-item hover:bg-theme-item-hover text-theme rounded-xl transition-colors font-medium"
Expand All @@ -120,6 +129,33 @@ const SettingsDialog: React.FC<SettingsDialogProps> = ({ visible, onClose }) =>
</button>
</div>
</div>

{/* Logout Confirm Dialog */}
{showLogoutConfirm && (
<div className="fixed inset-0 z-[10000] flex items-center justify-center">
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={() => setShowLogoutConfirm(false)} />
<div className="relative bg-theme-card rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
<h3 className="text-lg font-semibold text-theme mb-2">确认退出</h3>
<p className="text-sm text-theme-secondary mb-6">
确定要退出 WebUI 吗?这不会退出 QQ,仅清除 WebUI 登录状态。
</p>
<div className="flex justify-end gap-3">
<button
onClick={() => setShowLogoutConfirm(false)}
className="px-4 py-2 bg-theme-item hover:bg-theme-item-hover text-theme rounded-xl transition-colors font-medium"
>
取消
</button>
<button
onClick={onLogout}
className="px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-xl transition-colors font-medium"
>
确认退出
</button>
</div>
</div>
</div>
)}
</div>
)
}
Expand Down
3 changes: 2 additions & 1 deletion src/webui/FE/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export default defineConfig({
},
},
server: {
port: 5173,
host: '127.0.0.1',
port: 15173,
open: true,
proxy: {
'/api': {
Expand Down
Loading