mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 03:27:00 +08:00
Merge pull request #15 from Michael-py001/develop
fix: 修复日报生成时群成员名称显示错误 docs: 添加本地启动排障文档 chore: 添加electron下载源配置
This commit is contained in:
@@ -1 +1,2 @@
|
||||
shamefully-hoist=true
|
||||
electron_mirror=https://npmmirror.com/mirrors/electron/
|
||||
|
||||
@@ -55,6 +55,7 @@ WechatExplorer 有两种不同的接入方式。微信机器人是普通用户
|
||||
|
||||
- [macOS 数据访问说明](./platform/macos.md)
|
||||
- [开发、测试与构建](./development/overview.md)
|
||||
- [本地启动排障](./development/local-startup-troubleshooting.md)
|
||||
- [v2.1.9 API 鉴权迁移说明](./agent/release-notes-v2.1.9.md)
|
||||
|
||||
当前工作区版本:**2.1.9**。文档只描述当前代码已经实现的能力;版本兼容性、AI Provider 行为和媒体读取结果可能随系统、微信客户端和服务商变化。
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# 本地启动排障
|
||||
|
||||
本文面向运行源码开发环境的贡献者。常规启动顺序和测试入口请先阅读[开发、测试与构建](./overview.md)。
|
||||
|
||||
## 启动成功的判断标准
|
||||
|
||||
执行 `pnpm dev` 后,以下状态同时满足,说明本地开发环境已经可用:
|
||||
|
||||
- 控制台显示连接器已生成,例如 `resources/connectors/wechat/win32-x64/wechat-connector.exe`;
|
||||
- Electron 窗口已打开,或 `http://localhost:5173/` 返回 HTTP `200`;
|
||||
- 控制台显示 Local HTTP API 正在监听 `http://127.0.0.1:6131`。
|
||||
|
||||
`6131` 是应用提供给本机集成使用的 API 端口,不是 Vite 的页面端口。
|
||||
|
||||
## Go 命令找不到
|
||||
|
||||
如果 `pnpm dev` 在构建微信连接器时出现 `spawnSync go ENOENT`,先执行:
|
||||
|
||||
```bash
|
||||
go version
|
||||
```
|
||||
|
||||
命令不可用表示当前终端的 `PATH` 没有找到 Go。Windows 默认安装位置是 `C:\Program Files\Go\bin`。确认 Go 已安装并把该目录加入系统 `PATH` 后,关闭并重新打开终端或 IDE,再重新执行 `go version` 和 `pnpm dev`。
|
||||
|
||||
如果 Go 刚完成安装,已经打开的终端不会自动继承新的环境变量;重开终端是必要步骤。不要绕过连接器构建直接启动 `electron-vite dev`,否则 Agent Hub 的微信连接器不会生成。
|
||||
|
||||
## Electron 二进制缺失或下载失败
|
||||
|
||||
`electron-vite dev` 报 `Electron uninstall`,或 Electron 安装器报 `fetch failed`,通常表示 `node_modules/electron/dist` 中的 Electron 二进制缺失或下载未完成。这不是应用业务代码的启动错误。
|
||||
|
||||
项目的 [`.npmrc`](../../.npmrc) 已设置:
|
||||
|
||||
```ini
|
||||
electron_mirror=https://npmmirror.com/mirrors/electron/
|
||||
```
|
||||
|
||||
pnpm 会把该值传给 Electron 安装器,令其从镜像下载与 `package.json` 锁定版本匹配的二进制文件,避免默认 GitHub 下载源在受限网络中不可访问。
|
||||
|
||||
依赖安装被中断或 Electron 目录不完整时,删除不完整的 `node_modules` 后重新安装:
|
||||
|
||||
```bash
|
||||
pnpm install --frozen-lockfile
|
||||
```
|
||||
|
||||
单次安装需要使用其他镜像时,可以临时覆盖项目默认值。PowerShell 示例:
|
||||
|
||||
```powershell
|
||||
$env:ELECTRON_MIRROR = 'https://your-electron-mirror.example/'
|
||||
pnpm install --frozen-lockfile
|
||||
```
|
||||
|
||||
该环境变量只影响当前终端,不会改写仓库中的 `.npmrc`。镜像地址必须保留末尾的 `/`,并提供与 Electron 版本对应的目录结构。
|
||||
|
||||
## 页面地址无法通过 IPv4 访问
|
||||
|
||||
Vite 在某些 Windows 环境中只监听 IPv6 本机回环地址 `::1`。这时直接访问 `http://127.0.0.1:5173/` 可能失败,但 `http://localhost:5173/` 仍然正常,Electron 也会使用后者加载页面。
|
||||
|
||||
排查时优先访问 `http://localhost:5173/`;需要显式验证 IPv6 时,使用 `http://[::1]:5173/`。不要因为 IPv4 回环地址不可用就判断 Electron 或 Vite 启动失败。
|
||||
|
||||
## 仍无法启动时
|
||||
|
||||
保留首次错误的完整输出,并同时记录操作系统、Node.js、pnpm 和 Go 版本,以及 `pnpm install --frozen-lockfile` 与 `pnpm dev` 的执行结果。不要提交数据库密钥、AI API Key、微信数据路径或聊天内容。
|
||||
@@ -18,6 +18,8 @@ pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
本地依赖安装、Go 环境和 Electron 二进制下载异常,请查看[本地启动排障](./local-startup-troubleshooting.md)。
|
||||
|
||||
常用检查:
|
||||
|
||||
```bash
|
||||
|
||||
+179
-148
@@ -58,6 +58,17 @@ export interface Wcdb4GroupMember {
|
||||
m_nsHeadImgUrl: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 联系人表中独立保存的成员名称字段。
|
||||
*
|
||||
* 群成员接口的 nickname 在部分微信数据版本中会被通讯录备注覆盖,因此不能用它
|
||||
* 推断微信昵称或备注。
|
||||
*/
|
||||
type Wcdb4ContactMemberNames = {
|
||||
wechatNickname: string
|
||||
remark: string
|
||||
}
|
||||
|
||||
export interface Wcdb4ImageHardlink {
|
||||
file_name?: string
|
||||
full_path?: string
|
||||
@@ -1684,76 +1695,16 @@ export class Wcdb4Client {
|
||||
const rows = this.callJson<Record<string, unknown>[]>((handle, outJson) =>
|
||||
this.wcdbGetGroupMembers!(handle, chatroomId, outJson)
|
||||
)
|
||||
|
||||
const members = (Array.isArray(rows) ? rows : []).map((row) => {
|
||||
const username = this.pickString(row, [
|
||||
'username',
|
||||
'userName',
|
||||
'user_name',
|
||||
'member_username',
|
||||
'm_nsUsrName'
|
||||
])
|
||||
const wechatNickname = this.pickString(row, [
|
||||
'nickname',
|
||||
'nickName',
|
||||
'wechatNickname',
|
||||
'wechat_nickname',
|
||||
'm_nsNickName'
|
||||
])
|
||||
const remark = this.pickString(row, [
|
||||
'remark',
|
||||
'remarkName',
|
||||
'remark_name',
|
||||
'contactRemark',
|
||||
'contact_remark'
|
||||
])
|
||||
const rowGroupNickname = this.pickString(row, [
|
||||
'groupNickname',
|
||||
'group_nickname',
|
||||
'displayName',
|
||||
'display_name'
|
||||
])
|
||||
const memberNickname = this.pickString(row, ['name'])
|
||||
const avatar = this.pickString(row, [
|
||||
'avatarUrl',
|
||||
'avatar_url',
|
||||
'headImgUrl',
|
||||
'm_nsHeadImgUrl'
|
||||
])
|
||||
|
||||
if (username) {
|
||||
if (avatar) this.avatarCache.set(username, avatar)
|
||||
}
|
||||
|
||||
return {
|
||||
m_nsUsrName: username,
|
||||
nickname: wechatNickname || memberNickname || remark || rowGroupNickname,
|
||||
groupNickname: groupNicknames.get(username) || rowGroupNickname,
|
||||
wechatNickname: wechatNickname || memberNickname,
|
||||
remark,
|
||||
m_nsHeadImgUrl: avatar
|
||||
}
|
||||
})
|
||||
|
||||
const missingDisplayNames = members
|
||||
.filter((member) => !member.nickname)
|
||||
.map((member) => member.m_nsUsrName)
|
||||
.filter(Boolean)
|
||||
this.hydrateDisplayNames(missingDisplayNames)
|
||||
const memberRows = Array.isArray(rows) ? rows : []
|
||||
const contactNames = this.readContactMemberNames(this.groupMemberUsernames(memberRows))
|
||||
const members = this.normalizeGroupMembers(memberRows, groupNicknames, contactNames)
|
||||
this.hydrateAvatarUrls(
|
||||
members
|
||||
.filter((member) => !member.m_nsHeadImgUrl)
|
||||
.map((member) => member.m_nsUsrName)
|
||||
.filter(Boolean)
|
||||
)
|
||||
return members.map((member) => ({
|
||||
...member,
|
||||
nickname:
|
||||
member.nickname || this.displayNameCache.get(member.m_nsUsrName) || member.m_nsUsrName,
|
||||
wechatNickname:
|
||||
member.wechatNickname || this.displayNameCache.get(member.m_nsUsrName) || '',
|
||||
m_nsHeadImgUrl: member.m_nsHeadImgUrl || this.avatarCache.get(member.m_nsUsrName) || ''
|
||||
}))
|
||||
return this.withCachedGroupMemberAvatars(members)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
@@ -1827,76 +1778,98 @@ export class Wcdb4Client {
|
||||
this.wcdbGetGroupMembers as unknown as KoffiAsyncFunction,
|
||||
chatroomId
|
||||
)
|
||||
const members = (Array.isArray(rows) ? rows : []).map((row) => {
|
||||
const username = this.pickString(row, [
|
||||
const memberRows = Array.isArray(rows) ? rows : []
|
||||
const contactNames = await this.readContactMemberNamesAsync(
|
||||
this.groupMemberUsernames(memberRows)
|
||||
)
|
||||
const members = this.normalizeGroupMembers(memberRows, groupNicknames, contactNames)
|
||||
const missingAvatars = members
|
||||
.filter((member) => !member.m_nsHeadImgUrl)
|
||||
.map((member) => member.m_nsUsrName)
|
||||
.filter(Boolean)
|
||||
await this.hydrateAvatarUrlsAsync(missingAvatars)
|
||||
return this.withCachedGroupMemberAvatars(members)
|
||||
} catch (error) {
|
||||
console.warn(`[WCDB4] async group members failed chatroom=${chatroomId}:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将群成员接口与联系人表组装为语义明确的成员快照。
|
||||
*
|
||||
* 群昵称只接受群聊数据;微信昵称与通讯录备注只接受 contact 表。即使群成员
|
||||
* 接口返回了 nickname,也不能作为名称字段的回退,以免把通讯录备注误展示为微信昵称。
|
||||
*/
|
||||
private normalizeGroupMembers(
|
||||
rows: Record<string, unknown>[],
|
||||
groupNicknames: Map<string, string>,
|
||||
contactNames: Map<string, Wcdb4ContactMemberNames>
|
||||
): Wcdb4GroupMember[] {
|
||||
return rows.map((row) => {
|
||||
const username = this.pickString(row, [
|
||||
'username',
|
||||
'userName',
|
||||
'user_name',
|
||||
'member_username',
|
||||
'm_nsUsrName'
|
||||
])
|
||||
const rowGroupNickname = this.pickString(row, [
|
||||
'groupNickname',
|
||||
'group_nickname',
|
||||
'displayName',
|
||||
'display_name'
|
||||
])
|
||||
const groupNickname = groupNicknames.get(username) || rowGroupNickname
|
||||
const contact = contactNames.get(username)
|
||||
const wechatNickname = contact?.wechatNickname || ''
|
||||
const remark = contact?.remark || ''
|
||||
const avatar = this.pickString(row, [
|
||||
'avatarUrl',
|
||||
'avatar_url',
|
||||
'headImgUrl',
|
||||
'm_nsHeadImgUrl'
|
||||
])
|
||||
|
||||
if (username && avatar) this.avatarCache.set(username, avatar)
|
||||
|
||||
return {
|
||||
m_nsUsrName: username,
|
||||
// nickname 是旧调用方使用的通用显示字段,保持安全的名称优先级。
|
||||
nickname: wechatNickname || groupNickname || username,
|
||||
groupNickname,
|
||||
wechatNickname,
|
||||
remark,
|
||||
m_nsHeadImgUrl: avatar
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 从原始群成员行提取联系人查询键,避免把空值传进 SQL IN 条件。
|
||||
*/
|
||||
private groupMemberUsernames(rows: Record<string, unknown>[]): string[] {
|
||||
return rows
|
||||
.map((row) =>
|
||||
this.pickString(row, [
|
||||
'username',
|
||||
'userName',
|
||||
'user_name',
|
||||
'member_username',
|
||||
'm_nsUsrName'
|
||||
])
|
||||
const wechatNickname = this.pickString(row, [
|
||||
'nickname',
|
||||
'nickName',
|
||||
'wechatNickname',
|
||||
'wechat_nickname',
|
||||
'm_nsNickName'
|
||||
])
|
||||
const remark = this.pickString(row, [
|
||||
'remark',
|
||||
'remarkName',
|
||||
'remark_name',
|
||||
'contactRemark',
|
||||
'contact_remark'
|
||||
])
|
||||
const rowGroupNickname = this.pickString(row, [
|
||||
'groupNickname',
|
||||
'group_nickname',
|
||||
'displayName',
|
||||
'display_name'
|
||||
])
|
||||
const memberNickname = this.pickString(row, ['name'])
|
||||
const avatar = this.pickString(row, [
|
||||
'avatarUrl',
|
||||
'avatar_url',
|
||||
'headImgUrl',
|
||||
'm_nsHeadImgUrl'
|
||||
])
|
||||
if (username && avatar) this.avatarCache.set(username, avatar)
|
||||
return {
|
||||
m_nsUsrName: username,
|
||||
nickname: wechatNickname || memberNickname || remark || rowGroupNickname,
|
||||
groupNickname: groupNicknames.get(username) || rowGroupNickname,
|
||||
wechatNickname: wechatNickname || memberNickname,
|
||||
remark,
|
||||
m_nsHeadImgUrl: avatar
|
||||
}
|
||||
})
|
||||
)
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
const missingNames = members
|
||||
.filter((member) => !member.nickname)
|
||||
.map((member) => member.m_nsUsrName)
|
||||
.filter(Boolean)
|
||||
const missingAvatars = members
|
||||
.filter((member) => !member.m_nsHeadImgUrl)
|
||||
.map((member) => member.m_nsUsrName)
|
||||
.filter(Boolean)
|
||||
await Promise.all([
|
||||
this.hydrateDisplayNamesAsync(missingNames),
|
||||
this.hydrateAvatarUrlsAsync(missingAvatars)
|
||||
])
|
||||
return members.map((member) => ({
|
||||
...member,
|
||||
nickname:
|
||||
member.nickname || this.displayNameCache.get(member.m_nsUsrName) || member.m_nsUsrName,
|
||||
wechatNickname:
|
||||
member.wechatNickname || this.displayNameCache.get(member.m_nsUsrName) || '',
|
||||
m_nsHeadImgUrl: member.m_nsHeadImgUrl || this.avatarCache.get(member.m_nsUsrName) || ''
|
||||
}))
|
||||
} catch (error) {
|
||||
console.warn(`[WCDB4] async group members failed chatroom=${chatroomId}:`, error)
|
||||
return []
|
||||
}
|
||||
/**
|
||||
* 为已标准化的成员补充头像缓存,不参与名称字段的回退。
|
||||
*/
|
||||
private withCachedGroupMemberAvatars(members: Wcdb4GroupMember[]): Wcdb4GroupMember[] {
|
||||
return members.map((member) => ({
|
||||
...member,
|
||||
m_nsHeadImgUrl: member.m_nsHeadImgUrl || this.avatarCache.get(member.m_nsUsrName) || ''
|
||||
}))
|
||||
}
|
||||
|
||||
getGroupNicknames(chatroomId: string): Map<string, string> {
|
||||
@@ -2667,27 +2640,6 @@ export class Wcdb4Client {
|
||||
return false
|
||||
}
|
||||
|
||||
private hydrateDisplayNames(usernames: string[]): void {
|
||||
if (!this.wcdbGetDisplayNames) return
|
||||
const missing = this.uniq(usernames).filter((username) => !this.displayNameCache.has(username))
|
||||
if (missing.length === 0) return
|
||||
|
||||
try {
|
||||
const rows = this.callJson<Record<string, string> | Record<string, unknown>[]>(
|
||||
(handle, outJson) => this.wcdbGetDisplayNames!(handle, JSON.stringify(missing), outJson)
|
||||
)
|
||||
this.readStringMap(rows, [
|
||||
'nickname',
|
||||
'displayName',
|
||||
'display_name',
|
||||
'remark',
|
||||
'name'
|
||||
]).forEach((name, username) => this.displayNameCache.set(username, name))
|
||||
} catch {
|
||||
// Names are optional; usernames are still enough to load chats.
|
||||
}
|
||||
}
|
||||
|
||||
private async hydrateDisplayNamesAsync(usernames: string[]): Promise<void> {
|
||||
if (!this.wcdbGetDisplayNames) return
|
||||
const missing = this.uniq(usernames).filter((username) => !this.displayNameCache.has(username))
|
||||
@@ -2793,6 +2745,85 @@ export class Wcdb4Client {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 contact 表读取群成员的微信昵称与通讯录备注。
|
||||
*
|
||||
* 查询失败时返回空映射:群成员快照仍可用,并由调用方回退到群昵称或 wxid;
|
||||
* 不得使用群成员接口的 nickname,因为该字段可能实际保存的是通讯录备注。
|
||||
*/
|
||||
private readContactMemberNames(usernames: string[]): Map<string, Wcdb4ContactMemberNames> {
|
||||
const result = new Map<string, Wcdb4ContactMemberNames>()
|
||||
if (!this.wcdbExecQuery || usernames.length === 0) return result
|
||||
|
||||
const inList = this.uniq(usernames)
|
||||
.map((username) => `'${username.replace(/'/g, "''")}'`)
|
||||
.join(',')
|
||||
if (!inList) return result
|
||||
|
||||
try {
|
||||
const rows = this.callJson<Record<string, unknown>[]>((handle, outJson) =>
|
||||
this.wcdbExecQuery!(
|
||||
handle,
|
||||
'contact',
|
||||
'',
|
||||
`SELECT username, nick_name, remark FROM contact WHERE username IN (${inList})`,
|
||||
outJson
|
||||
)
|
||||
)
|
||||
return this.contactMemberNamesFromRows(rows)
|
||||
} catch {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步读取 contact 表的成员名称字段,语义与同步路径保持一致。
|
||||
*/
|
||||
private async readContactMemberNamesAsync(
|
||||
usernames: string[]
|
||||
): Promise<Map<string, Wcdb4ContactMemberNames>> {
|
||||
const result = new Map<string, Wcdb4ContactMemberNames>()
|
||||
if (!this.wcdbExecQuery || usernames.length === 0) return result
|
||||
|
||||
const inList = this.uniq(usernames)
|
||||
.map((username) => `'${username.replace(/'/g, "''")}'`)
|
||||
.join(',')
|
||||
if (!inList) return result
|
||||
|
||||
try {
|
||||
const rows = await this.callJsonAsync<Record<string, unknown>[]>(
|
||||
this.wcdbExecQuery as unknown as KoffiAsyncFunction,
|
||||
'contact',
|
||||
'',
|
||||
`SELECT username, nick_name, remark FROM contact WHERE username IN (${inList})`
|
||||
)
|
||||
return this.contactMemberNamesFromRows(rows)
|
||||
} catch {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 contact 查询行转换为按 wxid 索引的名称映射。
|
||||
*/
|
||||
private contactMemberNamesFromRows(
|
||||
rows: Record<string, unknown>[]
|
||||
): Map<string, Wcdb4ContactMemberNames> {
|
||||
const result = new Map<string, Wcdb4ContactMemberNames>()
|
||||
if (!Array.isArray(rows)) return result
|
||||
|
||||
for (const row of rows) {
|
||||
const username = this.pickString(row, ['username', 'user_name', 'userName', 'm_nsUsrName'])
|
||||
if (!username) continue
|
||||
result.set(username, {
|
||||
wechatNickname: this.pickString(row, ['nick_name', 'nickName', 'm_nsNickName']),
|
||||
remark: this.pickString(row, ['remark', 'remark_name', 'remarkName'])
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private readContactAvatarUrls(usernames: string[]): Map<string, string> {
|
||||
const result = new Map<string, string>()
|
||||
if (!this.wcdbExecQuery || usernames.length === 0) return result
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { Wcdb4Client } from '../../src/main/wcdb4-client'
|
||||
|
||||
const groupMemberRows = [
|
||||
{
|
||||
username: 'wxid-member',
|
||||
// 某些微信数据版本会在这个字段返回通讯录备注,不能作为微信昵称使用。
|
||||
nickname: '被污染的接口备注',
|
||||
groupNickname: '行内群昵称',
|
||||
avatarUrl: 'https://example.com/member.jpg'
|
||||
}
|
||||
]
|
||||
|
||||
const contactRows = [
|
||||
{
|
||||
username: 'wxid-member',
|
||||
nick_name: '真实微信昵称',
|
||||
remark: '真实通讯录备注'
|
||||
}
|
||||
]
|
||||
|
||||
function expectedMember(overrides: Partial<Record<string, string>> = {}) {
|
||||
return {
|
||||
m_nsUsrName: 'wxid-member',
|
||||
nickname: '真实微信昵称',
|
||||
groupNickname: '真实群昵称',
|
||||
wechatNickname: '真实微信昵称',
|
||||
remark: '真实通讯录备注',
|
||||
m_nsHeadImgUrl: 'https://example.com/member.jpg',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('WCDB group member names', () => {
|
||||
it('uses contact nick_name and remark instead of the ambiguous synchronous member nickname', () => {
|
||||
const getGroupMembers = vi.fn(() => 0)
|
||||
const executeQuery = vi.fn(() => 0)
|
||||
const callJson = vi.fn((call: (handle: number, output: [null]) => number) => {
|
||||
const queryCount = executeQuery.mock.calls.length
|
||||
call(1, [null])
|
||||
return executeQuery.mock.calls.length > queryCount ? contactRows : groupMemberRows
|
||||
})
|
||||
const client = Object.assign(Object.create(Wcdb4Client.prototype), {
|
||||
wcdbGetGroupMembers: getGroupMembers,
|
||||
wcdbExecQuery: executeQuery,
|
||||
callJson,
|
||||
getGroupNicknames: vi.fn(() => new Map([['wxid-member', '真实群昵称']])),
|
||||
avatarCache: new Map<string, string>(),
|
||||
wcdbGetAvatarUrls: null
|
||||
}) as Wcdb4Client
|
||||
|
||||
expect(client.getGroupMembers('fixture@chatroom')).toEqual([expectedMember()])
|
||||
expect(executeQuery).toHaveBeenCalledWith(
|
||||
1,
|
||||
'contact',
|
||||
'',
|
||||
expect.stringContaining('SELECT username, nick_name, remark FROM contact'),
|
||||
expect.any(Array)
|
||||
)
|
||||
})
|
||||
|
||||
it('uses the same independent contact fields in the asynchronous snapshot path', async () => {
|
||||
const getGroupMembers = vi.fn()
|
||||
const executeQuery = vi.fn()
|
||||
const callJsonAsync = vi.fn(async (fn: unknown) => {
|
||||
if (fn === getGroupMembers) return groupMemberRows
|
||||
if (fn === executeQuery) return contactRows
|
||||
throw new Error('unexpected native query')
|
||||
})
|
||||
const client = Object.assign(Object.create(Wcdb4Client.prototype), {
|
||||
wcdbGetGroupMembers: getGroupMembers,
|
||||
wcdbExecQuery: executeQuery,
|
||||
callJsonAsync,
|
||||
getGroupNicknamesAsync: vi.fn(async () => new Map([['wxid-member', '真实群昵称']])),
|
||||
avatarCache: new Map<string, string>(),
|
||||
wcdbGetAvatarUrls: null
|
||||
}) as Wcdb4Client
|
||||
|
||||
await expect(client.getGroupMembersAsync('fixture@chatroom')).resolves.toEqual([
|
||||
expectedMember()
|
||||
])
|
||||
expect(callJsonAsync).toHaveBeenLastCalledWith(
|
||||
executeQuery,
|
||||
'contact',
|
||||
'',
|
||||
expect.stringContaining('SELECT username, nick_name, remark FROM contact')
|
||||
)
|
||||
})
|
||||
|
||||
it('falls back to the group nickname without leaking an ambiguous member nickname', () => {
|
||||
const getGroupMembers = vi.fn(() => 0)
|
||||
const executeQuery = vi.fn(() => 0)
|
||||
const callJson = vi.fn((call: (handle: number, output: [null]) => number) => {
|
||||
const queryCount = executeQuery.mock.calls.length
|
||||
call(1, [null])
|
||||
if (executeQuery.mock.calls.length > queryCount)
|
||||
throw new Error('contact database unavailable')
|
||||
return groupMemberRows
|
||||
})
|
||||
const client = Object.assign(Object.create(Wcdb4Client.prototype), {
|
||||
wcdbGetGroupMembers: getGroupMembers,
|
||||
wcdbExecQuery: executeQuery,
|
||||
callJson,
|
||||
getGroupNicknames: vi.fn(() => new Map([['wxid-member', '真实群昵称']])),
|
||||
avatarCache: new Map<string, string>(),
|
||||
wcdbGetAvatarUrls: null
|
||||
}) as Wcdb4Client
|
||||
|
||||
expect(client.getGroupMembers('fixture@chatroom')).toEqual([
|
||||
expectedMember({ nickname: '真实群昵称', wechatNickname: '', remark: '' })
|
||||
])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user