feat: 完成 TraceMemo v2.2.0 品牌升级并保留旧数据兼容

- 将用户可见品牌升级为 TraceMemo(迹忆)
- 增加最早期 userData/sessionData 兼容路径选择
- 保留 WechatExplorer runtime identity 以兼容 safeStorage
- 继续使用旧 Knowledge、Settings、API Token 和 Provider 配置
- 新日志写入 TraceMemo 目录并保留历史日志
- 保留旧 API、Skill、环境变量和导出目录兼容标识
- 更新相关文档、界面文案与自动化测试
This commit is contained in:
Wxw-Gu
2026-08-11 14:42:12 +08:00
parent 2354fd0766
commit 55c6fb8cd3
86 changed files with 481 additions and 459 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ function renderPage(
wechatVersion: '4.1.9.57',
dataStructureVersion: '微信 4.xWCDB',
dataDirectoryDetected: true,
diagnosticSummary: 'WechatExplorer: v2.1.6',
diagnosticSummary: 'TraceMemo: v2.1.6',
autoDetectSupported: true,
wechatRunning: true,
accountIdentified: false,
@@ -40,7 +40,7 @@ describe('ImageTestSection', () => {
fileFound: true,
decrypted: false,
readable: false,
diagnosticLog: 'WechatExplorer 图片解析测试日志(已脱敏)'
diagnosticLog: 'TraceMemo 图片解析测试日志(已脱敏)'
}
}}
batchTest={emptyBatchTest}
+1 -1
View File
@@ -9,7 +9,7 @@ test('APP-01 first launch renders a usable connection screen without uncaught er
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await expect(fixture.page.getByRole('heading', { name: 'WechatExplorer' })).toBeVisible()
await expect(fixture.page.getByRole('heading', { name: 'TraceMemo(迹忆)' })).toBeVisible()
await expect(fixture.page.getByRole('main')).not.toBeEmpty()
expect(pageErrors).toEqual([])
} finally {
+2 -2
View File
@@ -117,7 +117,7 @@ handle('key:getEnvironment', () => ({
wechatVersion: '4.1.9.57',
dataStructureVersion: settings.dbRoot === 'fixture-account' ? '微信 4.xWCDB' : '未检测到',
dataDirectoryDetected: settings.dbRoot === 'fixture-account',
diagnosticSummary: 'WechatExplorer: v2.1.6\n数据目录: 已检测到',
diagnosticSummary: 'TraceMemo: v2.1.6\n数据目录: 已检测到',
autoDetectSupported: true,
wechatRunning: true,
accountIdentified: connected,
@@ -371,7 +371,7 @@ handle('image:testConfig', () => ({
fileFound: true,
decrypted: true,
readable: true,
diagnosticLog: 'WechatExplorer 图片解析测试日志(已脱敏)\n测试结果:成功(SUCCESS'
diagnosticLog: 'TraceMemo 图片解析测试日志(已脱敏)\n测试结果:成功(SUCCESS'
}))
handle('image:clearConfig', () => ({ success: true }))
handle('image:getDecoderStatus', () => ({
+1 -1
View File
@@ -14,7 +14,7 @@ test('NAV-01 login page visual @visual', async () => {
const fixture = await launchTestApp({ mode: 'disconnected' })
try {
await fixture.page.setViewportSize(visualViewport)
await expect(fixture.page.getByRole('heading', { name: 'WechatExplorer' })).toBeVisible()
await expect(fixture.page.getByRole('heading', { name: 'TraceMemo(迹忆)' })).toBeVisible()
await expect(fixture.page).toHaveScreenshot('login-page.png', {
animations: 'disabled',
caret: 'hide'
+4 -4
View File
@@ -1138,7 +1138,7 @@ describe('media export flow', () => {
expect(result).toEqual({ success: false, error: '已取消' })
expect(state.exportReads).toEqual(['cancel-1'])
const outputDir = join(state.documents, 'WechatExplorer', '导出', 'cancelled-all-conversations')
const outputDir = join(state.documents, 'TraceMemo', '导出', 'cancelled-all-conversations')
expect(existsSync(join(outputDir, '联系人', '联系人一', 'index.html'))).toBe(true)
expect(existsSync(join(outputDir, '联系人', '联系人二', 'index.html'))).toBe(false)
const partialManifest = JSON.parse(readFileSync(join(outputDir, '导出清单.json'), 'utf8')) as {
@@ -1195,7 +1195,7 @@ describe('media export flow', () => {
expect(firstSize).toBeGreaterThan(0)
expect(readFileSync(second.outputPath!).subarray(0, 2).toString()).toBe('PK')
const entries = execFileSync('unzip', ['-Z1', second.outputPath!], { encoding: 'utf8' })
const htmlPath = join(state.documents, 'WechatExplorer', '导出', 'zip-fixture', 'index.html')
const htmlPath = join(state.documents, 'TraceMemo', '导出', 'zip-fixture', 'index.html')
const archive = readArchive(htmlPath)
expect(entries).toContain('zip-fixture/index.html')
expect(entries).toContain('zip-fixture/data/messages.js')
@@ -1209,7 +1209,7 @@ describe('media export flow', () => {
true
)
expect(
readdirSync(join(state.documents, 'WechatExplorer', '导出')).some((name) =>
readdirSync(join(state.documents, 'TraceMemo', '导出')).some((name) =>
name.startsWith('zip-fixture.zip.tmp-')
)
).toBe(false)
@@ -1250,7 +1250,7 @@ describe('media export flow', () => {
expect(cancelled).toEqual({ success: false, error: '已取消' })
expect(readFileSync(first.outputPath!)).toEqual(completeZip)
expect(
readdirSync(join(state.documents, 'WechatExplorer', '导出')).some((name) =>
readdirSync(join(state.documents, 'TraceMemo', '导出')).some((name) =>
name.startsWith('zip-cancel-fixture.zip.tmp-')
)
).toBe(false)
+1 -1
View File
@@ -95,7 +95,7 @@ describe('Local API authentication', () => {
const health = await fetch(`${baseUrl(handle)}/api/v1/health`)
expect(health.status).toBe(200)
const healthBody = await health.json()
expect(healthBody).toMatchObject({ ok: true, service: 'WechatExplorer Reader' })
expect(healthBody).toMatchObject({ ok: true, service: 'TraceMemo Reader' })
expect(JSON.stringify(healthBody)).not.toMatch(
/token|authorization|wxid|databasePath|provider/i
)
+93
View File
@@ -0,0 +1,93 @@
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
import os from 'os'
import path from 'path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { chooseUserDataRoot, getUserDataRoots, hasValidUserAssets } from '../../src/main/app-data-paths'
let root = ''
beforeEach(() => {
root = mkdtempSync(path.join(os.tmpdir(), 'tracememo-app-data-paths-'))
})
afterEach(() => {
rmSync(root, { recursive: true, force: true })
})
function fixtureRoots(): ReturnType<typeof getUserDataRoots> {
return getUserDataRoots(path.join(root, 'Application Support'))
}
function writeMarker(filePath: string): void {
mkdirSync(path.dirname(filePath), { recursive: true })
writeFileSync(filePath, 'fixture')
}
describe('app data compatibility root selection', () => {
it('chooses the new root for a clean install', () => {
const roots = fixtureRoots()
expect(hasValidUserAssets(roots.legacy)).toBe(false)
expect(hasValidUserAssets(roots.current)).toBe(false)
expect(chooseUserDataRoot(roots)).toBe(roots.current)
})
it('ignores runtime-only Chromium files', () => {
const roots = fixtureRoots()
mkdirSync(roots.current, { recursive: true })
writeMarker(path.join(roots.current, 'Local State'))
mkdirSync(path.join(roots.current, 'Cache'), { recursive: true })
expect(hasValidUserAssets(roots.current)).toBe(false)
expect(chooseUserDataRoot(roots)).toBe(roots.current)
})
it('prefers a legacy root containing user settings', () => {
const roots = fixtureRoots()
writeMarker(path.join(roots.legacy, 'settings.json'))
expect(chooseUserDataRoot(roots)).toBe(roots.legacy)
})
it('uses a current root when only it contains valid user assets', () => {
const roots = fixtureRoots()
writeMarker(path.join(roots.current, 'ai-providers.json'))
expect(chooseUserDataRoot(roots)).toBe(roots.current)
})
it('prefers legacy without modifying a valid current root', () => {
const roots = fixtureRoots()
writeMarker(path.join(roots.legacy, 'reports', '2026', '08', 'report.json'))
writeMarker(path.join(roots.current, 'local-api-token.bin'))
const currentToken = path.join(roots.current, 'local-api-token.bin')
expect(chooseUserDataRoot(roots)).toBe(roots.legacy)
expect(hasValidUserAssets(roots.current)).toBe(true)
expect(readFileSync(currentToken, 'utf8')).toBe('fixture')
})
it('recognizes Knowledge only when a non-empty knowledge.sqlite exists', () => {
const roots = fixtureRoots()
mkdirSync(path.join(roots.legacy, 'knowledge', 'account-hash'), { recursive: true })
expect(hasValidUserAssets(roots.legacy)).toBe(false)
writeMarker(path.join(roots.legacy, 'knowledge', 'account-hash', 'knowledge.sqlite'))
expect(hasValidUserAssets(roots.legacy)).toBe(true)
})
it('honors WXE_USER_DATA isolation before normal root selection', () => {
const roots = fixtureRoots()
writeMarker(path.join(roots.legacy, 'settings.json'))
const isolated = path.join(root, 'isolated-user-data')
expect(chooseUserDataRoot({ ...roots, isolated })).toBe(isolated)
})
it.each([
'ai-provider-keys.bin',
'local-api-token.bin',
'wechat-image-keys.bin',
path.join('database-keys', 'account.bin'),
path.join('recall-archive', 'entry.json'),
path.join('Local Storage', 'leveldb', '000001.ldb')
])('recognizes %s as a persistent user asset', (relativePath) => {
const roots = fixtureRoots()
writeMarker(path.join(roots.legacy, relativePath))
expect(hasValidUserAssets(roots.legacy)).toBe(true)
expect(chooseUserDataRoot(roots)).toBe(roots.legacy)
})
})
+1 -1
View File
@@ -17,7 +17,7 @@ describe('connection diagnostics', () => {
encryptionAvailable: true
})
expect(summary).toContain('WechatExplorer: v2.1.6')
expect(summary).toContain('TraceMemo: v2.1.6')
expect(summary).toContain('微信客户端: 4.1.9.57')
expect(summary).not.toContain('0123456789abcdef')
expect(summary).not.toContain('C:\\Users\\fixture\\xwechat_files\\wxid_secret')
+2 -2
View File
@@ -45,11 +45,11 @@ describe('production runtime packaging', () => {
expect(config).toContain('docs/skill/wechatexplorer-reader')
expect(config).toContain('to: skill/wechatexplorer-reader')
expect(() => validateReaderSkillRuntime(resources)).toThrow(
/Missing bundled WechatExplorer Reader Skill/
/Missing bundled TraceMemo Reader Skill/
)
mkdirSync(dirname(skillPath), { recursive: true })
writeFileSync(skillPath, '# WechatExplorer Reader\n')
writeFileSync(skillPath, '# TraceMemo Reader\n')
expect(validateReaderSkillRuntime(resources)).toBe(skillPath)
})
+1 -1
View File
@@ -27,7 +27,7 @@ function environment(root: string, packaged: boolean) {
function writeSkill(filePath: string): void {
mkdirSync(dirname(filePath), { recursive: true })
writeFileSync(filePath, '# WechatExplorer Reader\n', 'utf8')
writeFileSync(filePath, '# TraceMemo Reader\n', 'utf8')
}
describe('Reader Skill resource resolution', () => {