mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-04 03:01:42 +08:00
422 lines
14 KiB
TypeScript
422 lines
14 KiB
TypeScript
import {
|
||
mkdirSync,
|
||
mkdtempSync,
|
||
readFileSync,
|
||
rmSync,
|
||
unlinkSync,
|
||
utimesSync,
|
||
writeFileSync
|
||
} from 'fs'
|
||
import { tmpdir } from 'os'
|
||
import { join, sep } from 'path'
|
||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||
|
||
vi.mock('electron', () => ({
|
||
app: {
|
||
getAppPath: () => '/fixture/app',
|
||
getPath: () => '/tmp/tracememo-test-user-data',
|
||
isPackaged: false
|
||
}
|
||
}))
|
||
|
||
import {
|
||
buildPersonalWechatVoiceDiagnostic,
|
||
buildWindowsWechatRequest,
|
||
buildPersonalWechatOneBotRequest,
|
||
findWechatImagePath,
|
||
findPersonalWechatRuntime,
|
||
normalizeWindowsWechatPort,
|
||
parseWindowsLoginStatus,
|
||
parseWindowsHookResponse,
|
||
parsePersonalWechatHookLog,
|
||
prepareWindowsImageFile,
|
||
WindowsHookHttpError
|
||
} from '../../src/main/services/personal-wechat-send-service'
|
||
|
||
const temporaryDirectories: string[] = []
|
||
|
||
function temporaryDirectory(): string {
|
||
const directory = mkdtempSync(join(tmpdir(), 'wechat-personal-runtime-'))
|
||
temporaryDirectories.push(directory)
|
||
return directory
|
||
}
|
||
|
||
afterEach(() => {
|
||
while (temporaryDirectories.length > 0) {
|
||
rmSync(temporaryDirectories.pop()!, { recursive: true, force: true })
|
||
}
|
||
})
|
||
|
||
describe('personal WeChat OneBot request', () => {
|
||
it('keeps voice diagnostics to the safe metadata allowlist', () => {
|
||
const diagnostic = buildPersonalWechatVoiceDiagnostic('request-1', 'completed', {
|
||
input_bytes: 10,
|
||
upload_result: '0',
|
||
error: undefined,
|
||
aesKey: 'secret',
|
||
cdnKey: 'secret',
|
||
token: 'secret',
|
||
runtime_log: 'private'
|
||
})
|
||
expect(diagnostic).toMatchObject({ request_id: 'request-1', upload_result: '0' })
|
||
expect(diagnostic).not.toHaveProperty('aesKey')
|
||
expect(diagnostic).not.toHaveProperty('cdnKey')
|
||
expect(diagnostic).not.toHaveProperty('token')
|
||
expect(diagnostic).not.toHaveProperty('runtime_log')
|
||
})
|
||
|
||
it('redacts secrets embedded in diagnostic errors', () => {
|
||
const diagnostic = buildPersonalWechatVoiceDiagnostic('request-2', 'failed', {
|
||
error:
|
||
'upload failed aesKey=secret-value Bearer bearer-secret token:token-value {"cdnKey":"json-secret"}'
|
||
})
|
||
expect(diagnostic.error).toContain('aesKey=[redacted]')
|
||
expect(diagnostic.error).toContain('Bearer [redacted]')
|
||
expect(diagnostic.error).toContain('token:[redacted]')
|
||
expect(diagnostic.error).not.toContain('secret-value')
|
||
expect(diagnostic.error).not.toContain('bearer-secret')
|
||
expect(diagnostic.error).not.toContain('token-value')
|
||
expect(diagnostic.error).not.toContain('json-secret')
|
||
})
|
||
|
||
it('builds a private text message request', () => {
|
||
expect(
|
||
buildPersonalWechatOneBotRequest({
|
||
to: ' wxid_fixture ',
|
||
type: 'text',
|
||
text: ' 测试发送 ',
|
||
isGroup: false
|
||
})
|
||
).toEqual({
|
||
endpoint: '/send_private_msg',
|
||
body: {
|
||
user_id: 'wxid_fixture',
|
||
message: [{ type: 'text', data: { text: '测试发送' } }]
|
||
}
|
||
})
|
||
})
|
||
|
||
it('uses the group endpoint for chatroom targets', () => {
|
||
expect(
|
||
buildPersonalWechatOneBotRequest({
|
||
to: 'fixture-room@chatroom',
|
||
type: 'text',
|
||
text: '群聊测试',
|
||
isGroup: false
|
||
})
|
||
).toEqual({
|
||
endpoint: '/send_group_msg',
|
||
body: {
|
||
group_id: 'fixture-room@chatroom',
|
||
message: [{ type: 'text', data: { text: '群聊测试' } }]
|
||
}
|
||
})
|
||
})
|
||
|
||
it('builds a base64 image request without mixing text content', () => {
|
||
expect(
|
||
buildPersonalWechatOneBotRequest(
|
||
{ to: 'fixture-room@chatroom', type: 'image', filePath: '/tmp/test.png', isGroup: false },
|
||
'aGVsbG8='
|
||
)
|
||
).toEqual({
|
||
endpoint: '/send_group_msg',
|
||
body: {
|
||
group_id: 'fixture-room@chatroom',
|
||
message: [{ type: 'image', data: { file: 'base64://aGVsbG8=' } }]
|
||
}
|
||
})
|
||
})
|
||
|
||
it('builds a base64 voice record request', () => {
|
||
expect(
|
||
buildPersonalWechatOneBotRequest(
|
||
{ to: 'filehelper', type: 'voice', filePath: '/tmp/test.silk', isGroup: false },
|
||
'dm9pY2U='
|
||
)
|
||
).toEqual({
|
||
endpoint: '/send_private_msg',
|
||
body: {
|
||
user_id: 'filehelper',
|
||
message: [{ type: 'record', data: { file: 'base64://dm9pY2U=' } }]
|
||
}
|
||
})
|
||
})
|
||
})
|
||
|
||
describe('Windows request', () => {
|
||
it('accepts only a valid configured local port', () => {
|
||
expect(normalizeWindowsWechatPort('')).toBeNull()
|
||
expect(normalizeWindowsWechatPort(' 4567 ')).toBe('4567')
|
||
expect(normalizeWindowsWechatPort('0')).toBeNull()
|
||
expect(normalizeWindowsWechatPort('65536')).toBeNull()
|
||
expect(normalizeWindowsWechatPort('port')).toBeNull()
|
||
})
|
||
|
||
it('requires ret=0 for message responses but accepts the status shape', () => {
|
||
expect(parseWindowsHookResponse('{"ret":0,"retmsg":"success"}', true)).toEqual({
|
||
ret: 0,
|
||
retmsg: 'success'
|
||
})
|
||
expect(parseWindowsHookResponse('{"IsLogin":0,"hWeixin":123}', false)).toEqual({
|
||
IsLogin: 0,
|
||
hWeixin: 123
|
||
})
|
||
expect(() => parseWindowsHookResponse('{"ret":1,"retmsg":"fail"}', true)).toThrow('fail')
|
||
expect(() => parseWindowsHookResponse('{"retmsg":"success"}', true)).toThrow('success')
|
||
expect(() => parseWindowsHookResponse('{}', true)).toThrow('ret=undefined')
|
||
expect(() => parseWindowsHookResponse('', true)).toThrow('空响应')
|
||
})
|
||
|
||
it('preserves the Windows Hook HTTP status and response body', () => {
|
||
const error = new WindowsHookHttpError(500, '{"ret":-9,"retmsg":"image failed"}')
|
||
expect(error.message).toBe('HTTP 500: {"ret":-9,"retmsg":"image failed"}')
|
||
expect(error.status).toBe(500)
|
||
expect(error.responseBody).toContain('image failed')
|
||
expect(new WindowsHookHttpError(500, '').message).toBe('HTTP 500')
|
||
})
|
||
|
||
it('allows Windows sending only for a strict logged-in status', () => {
|
||
expect(parseWindowsLoginStatus({ status: true })).toBe(true)
|
||
expect(parseWindowsLoginStatus({ status: false })).toBe(false)
|
||
expect(parseWindowsLoginStatus({ status: 1 })).toBe(false)
|
||
expect(parseWindowsLoginStatus({ status: 'true' })).toBe(false)
|
||
expect(parseWindowsLoginStatus({ IsLogin: 1, hWeixin: 123 })).toBe(false)
|
||
})
|
||
|
||
it('builds the unified text request', () => {
|
||
expect(
|
||
buildWindowsWechatRequest({
|
||
to: ' wxid_fixture ',
|
||
type: 'text',
|
||
text: ' 测试发送 ',
|
||
isGroup: false
|
||
})
|
||
).toEqual({
|
||
endpoint: '/SendMsg',
|
||
body: { toWxid: 'wxid_fixture', type: 'text', msg: '测试发送' }
|
||
})
|
||
})
|
||
|
||
it('builds the unified image request with a local path', () => {
|
||
expect(
|
||
buildWindowsWechatRequest({
|
||
to: 'room@chatroom',
|
||
type: 'image',
|
||
filePath: 'C:\\fixture\\image.png',
|
||
isGroup: true
|
||
})
|
||
).toEqual({
|
||
endpoint: '/SendMsg',
|
||
body: { toWxid: 'room@chatroom', type: 'image', msg: 'C:\\fixture\\image.png' }
|
||
})
|
||
})
|
||
|
||
it('builds the unified voice request with sender wxid and duration', () => {
|
||
expect(
|
||
buildWindowsWechatRequest(
|
||
{
|
||
to: 'wxid_fixture',
|
||
type: 'voice',
|
||
filePath: '/source/input.wav',
|
||
fromId: ' wxid_self ',
|
||
isGroup: false
|
||
},
|
||
{ filePath: 'C:\\Temp\\prepared.silk', durationMs: 1234 }
|
||
)
|
||
).toEqual({
|
||
endpoint: '/SendMsg',
|
||
body: {
|
||
toWxid: 'wxid_fixture',
|
||
type: 'voice',
|
||
msg: 'C:\\Temp\\prepared.silk',
|
||
fromWxid: 'wxid_self',
|
||
duration: 1234
|
||
}
|
||
})
|
||
})
|
||
|
||
it('copies adjacent non-ASCII image paths to ASCII temporary files', () => {
|
||
const root = temporaryDirectory()
|
||
const content = Buffer.from('fixture-image')
|
||
for (const fileName of ['测试图片_2026-08-31_经典.png', '测试图片_2026-08-31_经典版.png']) {
|
||
const source = join(root, fileName)
|
||
writeFileSync(source, content)
|
||
|
||
const prepared = prepareWindowsImageFile(source)
|
||
|
||
expect(prepared.temporary).toBe(true)
|
||
expect(prepared.filePath).not.toContain(fileName)
|
||
expect(prepared.filePath).toMatch(/^[\x20-\x7e]+$/)
|
||
expect(readFileSync(prepared.filePath)).toEqual(content)
|
||
unlinkSync(prepared.filePath)
|
||
}
|
||
})
|
||
|
||
it('keeps an ASCII image path unchanged', () => {
|
||
const root = temporaryDirectory()
|
||
const source = join(root, 'report.png')
|
||
writeFileSync(source, 'fixture-image')
|
||
|
||
expect(prepareWindowsImageFile(source)).toEqual({ filePath: source, temporary: false })
|
||
})
|
||
})
|
||
|
||
describe('personal WeChat runtime discovery', () => {
|
||
it('uses the newest current-month WeChat image directory', () => {
|
||
const root = temporaryDirectory()
|
||
const oldPath = join(root, 'account', 'temp', 'old', '2026-08', 'Img')
|
||
const latestPath = join(root, 'account', 'temp', 'latest', '2026-08', 'Img')
|
||
mkdirSync(oldPath, { recursive: true })
|
||
mkdirSync(latestPath, { recursive: true })
|
||
utimesSync(oldPath, new Date('2026-08-01'), new Date('2026-08-01'))
|
||
utimesSync(latestPath, new Date('2026-08-05'), new Date('2026-08-05'))
|
||
|
||
expect(findWechatImagePath(root, new Date('2026-08-06'))).toBe(`${latestPath}${sep}`)
|
||
})
|
||
|
||
it('supports the current ImageTemp layout after a WeChat relogin', () => {
|
||
const root = temporaryDirectory()
|
||
const imageTempPath = join(root, 'account', 'temp', 'ImageTemp', '2026-08')
|
||
mkdirSync(imageTempPath, { recursive: true })
|
||
expect(findWechatImagePath(root, new Date('2026-08-06'))).toBe(`${imageTempPath}${sep}`)
|
||
})
|
||
|
||
it('derives the current ImageTemp directory before the month folder exists', () => {
|
||
const root = temporaryDirectory()
|
||
const tempRoot = join(root, 'account', 'temp')
|
||
mkdirSync(tempRoot, { recursive: true })
|
||
|
||
expect(findWechatImagePath(root, new Date('2026-08-06'))).toBe(
|
||
`${join(tempRoot, 'ImageTemp', '2026-08')}${sep}`
|
||
)
|
||
})
|
||
|
||
it('finds the nested release archive layout', () => {
|
||
const root = temporaryDirectory()
|
||
mkdirSync(join(root, 'onebot'), { recursive: true })
|
||
mkdirSync(join(root, 'wechat_version'), { recursive: true })
|
||
writeFileSync(join(root, 'onebot', 'onebot'), 'fixture')
|
||
writeFileSync(join(root, 'onebot', 'script.js'), 'fixture')
|
||
|
||
expect(findPersonalWechatRuntime([root])).toEqual({
|
||
root,
|
||
executable: join(root, 'onebot', 'onebot'),
|
||
workingDirectory: join(root, 'onebot'),
|
||
configDirectory: join(root, 'wechat_version'),
|
||
logPath: join(root, 'onebot', 'log', 'macos.log')
|
||
})
|
||
})
|
||
|
||
it('finds a flat runtime layout and rejects incomplete directories', () => {
|
||
const incomplete = temporaryDirectory()
|
||
writeFileSync(join(incomplete, 'onebot'), 'fixture')
|
||
|
||
const root = temporaryDirectory()
|
||
mkdirSync(join(root, 'wechat_version'), { recursive: true })
|
||
writeFileSync(join(root, 'onebot'), 'fixture')
|
||
writeFileSync(join(root, 'script.js'), 'fixture')
|
||
|
||
expect(findPersonalWechatRuntime([incomplete, root])).toEqual({
|
||
root,
|
||
executable: join(root, 'onebot'),
|
||
workingDirectory: root,
|
||
configDirectory: join(root, 'wechat_version'),
|
||
logPath: join(root, 'log', 'macos.log')
|
||
})
|
||
})
|
||
})
|
||
|
||
describe('personal WeChat hook diagnostics', () => {
|
||
it('does not treat a listening service as hook-ready after req2buf scan failure', () => {
|
||
expect(
|
||
parsePersonalWechatHookLog(
|
||
[
|
||
JSON.stringify({ time: '2026-08-06T14:08:10+08:00', payload: '[+] HTTP 服务启动在' }),
|
||
JSON.stringify({
|
||
time: '2026-08-06T14:08:11+08:00',
|
||
err: "Error: [-] Cannot find 'req2buf' keyword in a range > 100MB"
|
||
})
|
||
].join('\n')
|
||
)
|
||
).toMatchObject({ readiness: 'failed' })
|
||
})
|
||
|
||
it('requires StartTask capture before reporting ready', () => {
|
||
expect(
|
||
parsePersonalWechatHookLog(
|
||
JSON.stringify({ payload: '[+] Dynamic Text Message Setup Complete.' })
|
||
)
|
||
).toMatchObject({ readiness: 'initializing', textHookInstalled: true })
|
||
expect(
|
||
parsePersonalWechatHookLog(
|
||
JSON.stringify({ payload: '[+] 捕获到 StartTask 调用,X0:0x1, Payload: 0x2' })
|
||
)
|
||
).toMatchObject({ readiness: 'ready', textHookReady: true })
|
||
})
|
||
|
||
it('resets stale hook results when a new WeChat process attach starts', () => {
|
||
expect(
|
||
parsePersonalWechatHookLog(
|
||
[
|
||
JSON.stringify({ payload: '[+] 捕获到 StartTask 调用,X0:0x1, Payload: 0x2' }),
|
||
JSON.stringify({ message: '使用指定的微信进程 PID', PID: 123 }),
|
||
JSON.stringify({ payload: '[+] Dynamic Text Message Setup Complete.' })
|
||
].join('\n')
|
||
)
|
||
).toMatchObject({
|
||
readiness: 'initializing',
|
||
boundWechatPid: 123,
|
||
textHookInstalled: true,
|
||
textHookReady: false
|
||
})
|
||
})
|
||
|
||
it('tracks base, image Hook and message listener diagnostics', () => {
|
||
expect(
|
||
parsePersonalWechatHookLog(
|
||
[
|
||
JSON.stringify({ message: '使用指定的微信进程 PID', PID: 4668 }),
|
||
JSON.stringify({ message: '成功 Attach 微信进程', PID: 4668 }),
|
||
JSON.stringify({ payload: '[+] Base address from range: 0x114ef8000' }),
|
||
JSON.stringify({ payload: '[+] Dynamic Text Message Setup Complete.' }),
|
||
JSON.stringify({ payload: '[+] 图片上传 Hook Setup Complete.' }),
|
||
JSON.stringify({ payload: '[+] 捕获到图片上传上下文,uploadGlobalX0:0x1' }),
|
||
JSON.stringify({ message: '发送数据' })
|
||
].join('\n')
|
||
)
|
||
).toMatchObject({
|
||
attached: true,
|
||
baseAddress: '0x114ef8000',
|
||
textHookInstalled: true,
|
||
imageHookInstalled: true,
|
||
imageHookReady: true,
|
||
messageListenerReady: true,
|
||
boundWechatPid: 4668
|
||
})
|
||
})
|
||
|
||
it('recognizes a successful legacy image send without the new Hook marker', () => {
|
||
expect(
|
||
parsePersonalWechatHookLog(
|
||
JSON.stringify({ type: 'send_image', result: '1', message: '发送图片任务执行结果' })
|
||
)
|
||
).toMatchObject({ imageHookInstalled: true, imageHookReady: true })
|
||
})
|
||
|
||
it('recognizes the real onebot two-record image result format', () => {
|
||
expect(
|
||
parsePersonalWechatHookLog(
|
||
[
|
||
JSON.stringify({ task_id: 536870915, type: 'send_image' }),
|
||
JSON.stringify({
|
||
result: '1',
|
||
task_id: 536870915,
|
||
message: '📩 发送图片任务执行结果'
|
||
})
|
||
].join('\n')
|
||
)
|
||
).toMatchObject({ imageHookInstalled: true, imageHookReady: true })
|
||
})
|
||
})
|