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
+31
View File
@@ -0,0 +1,31 @@
import { app } from 'electron'
import path from 'path'
import {
chooseUserDataRoot,
getUserDataRoots,
LEGACY_USER_DATA_NAME
} from './app-data-paths'
// This module must remain the first main-process import. Static imports in
// settings/cache services can otherwise resolve Electron paths before the
// legacy runtime identity and selected userData are installed.
app.setName(process.platform === 'win32' ? 'WeFlow' : LEGACY_USER_DATA_NAME)
const isolatedUserData = process.env['WXE_USER_DATA']
const roots = getUserDataRoots(app.getPath('appData'))
const selectedUserData = chooseUserDataRoot({
...roots,
isolated: isolatedUserData
})
app.setPath('userData', selectedUserData)
app.setPath('sessionData', selectedUserData)
// Logs are intentionally independent from userData. New TraceMemo logs go to
// the new visible directory while historical WechatExplorer logs remain in
// place and are never moved or renamed.
if (process.platform === 'darwin') {
app.setPath('logs', path.join(app.getPath('home'), 'Library', 'Logs', 'TraceMemo'))
}
export { roots, selectedUserData }
+104
View File
@@ -0,0 +1,104 @@
import fs from 'fs'
import path from 'path'
export const LEGACY_USER_DATA_NAME = 'WechatExplorer'
export const CURRENT_USER_DATA_NAME = 'tracememo'
export interface UserDataRoots {
legacy: string
current: string
}
export interface UserDataSelectionInput extends UserDataRoots {
isolated?: string
}
function isNonEmptyFile(filePath: string): boolean {
try {
const stat = fs.statSync(filePath)
return stat.isFile() && stat.size > 0
} catch {
return false
}
}
function hasPersistentEntries(directoryPath: string): boolean {
try {
return fs.readdirSync(directoryPath, { withFileTypes: true }).some((entry) => {
if (entry.name === '.DS_Store') return false
if (entry.name === 'LOCK' || entry.name === 'LOG' || entry.name === 'LOG.old') return false
return entry.isFile() || entry.isDirectory()
})
} catch {
return false
}
}
function hasDatabaseKey(directoryPath: string): boolean {
try {
return fs.readdirSync(directoryPath, { withFileTypes: true }).some((entry) => {
return entry.isFile() && entry.name.endsWith('.bin') && isNonEmptyFile(path.join(directoryPath, entry.name))
})
} catch {
return false
}
}
function hasKnowledgeDatabase(root: string): boolean {
const knowledgeRoot = path.join(root, 'knowledge')
try {
return fs.readdirSync(knowledgeRoot, { withFileTypes: true }).some((entry) => {
if (!entry.isDirectory()) return false
return isNonEmptyFile(path.join(knowledgeRoot, entry.name, 'knowledge.sqlite'))
})
} catch {
return false
}
}
/**
* Runtime-only Chromium files are deliberately excluded. A directory is a
* valid data root only when it contains at least one user-owned marker.
*/
export function hasValidUserAssets(root: string): boolean {
const markers = [
'settings.json',
'ai-providers.json',
'ai-provider-keys.bin',
'local-api-token.bin',
'wechat-db-key.bin',
'wechat-image-keys.bin',
'image-insights.json',
'wechat-share-service.bin'
]
if (markers.some((marker) => isNonEmptyFile(path.join(root, marker)))) return true
if (hasKnowledgeDatabase(root)) return true
if (hasDatabaseKey(path.join(root, 'database-keys'))) return true
if (hasPersistentEntries(path.join(root, 'reports'))) return true
if (hasPersistentEntries(path.join(root, 'recall-archive'))) return true
if (hasPersistentEntries(path.join(root, 'digital-twin'))) return true
if (hasPersistentEntries(path.join(root, 'group-exit-monitor'))) return true
if (hasPersistentEntries(path.join(root, 'Local Storage', 'leveldb'))) return true
return false
}
export function getUserDataRoots(appDataPath: string): UserDataRoots {
return {
legacy: path.join(appDataPath, LEGACY_USER_DATA_NAME),
current: path.join(appDataPath, CURRENT_USER_DATA_NAME)
}
}
/**
* Select exactly one root. This intentionally does not copy, merge, delete or
* modify either directory. Legacy wins when both roots contain user assets so
* a v2.1.9 upgrade remains deterministic and lossless.
*/
export function chooseUserDataRoot(input: UserDataSelectionInput): string {
const isolated = input.isolated?.trim()
if (isolated) return path.resolve(isolated)
if (hasValidUserAssets(input.legacy)) return input.legacy
if (hasValidUserAssets(input.current)) return input.current
return input.current
}
+1 -1
View File
@@ -34,7 +34,7 @@ export class AppLogger {
}
get logPath(): string {
return path.join(this.logDir, 'wechatexplorer.log')
return path.join(this.logDir, 'tracememo.log')
}
private rotateIfNeeded(): void {
+15 -3
View File
@@ -44,6 +44,18 @@ const exportStamp = (): string => {
const pad = (value: number): string => String(value).padStart(2, '0')
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}_${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`
}
const defaultExportRoot = (): string => join(app.getPath('documents'), 'TraceMemo', '导出')
const legacyExportRoot = (): string => join(app.getPath('documents'), 'WechatExplorer', '导出')
const resolveDefaultExportRoot = async (outputFolder?: string): Promise<string> => {
if (!outputFolder) return defaultExportRoot()
try {
await fs.access(join(legacyExportRoot(), outputFolder))
// Continue incremental exports in the legacy folder when it already exists.
return legacyExportRoot()
} catch {
return defaultExportRoot()
}
}
const imageKeys = new ImageKeyConfigService()
export interface HtmlExportConversation {
@@ -908,13 +920,13 @@ async function runSingleExport(
total: messages.length,
percent: request.format === 'html' ? 18 : 20
})
const root = options.outputRoot || join(app.getPath('documents'), 'WechatExplorer', '导出')
await fs.mkdir(root, { recursive: true })
const ext = request.format === 'markdown' ? 'md' : request.format
const outputFolder =
request.format === 'html'
? options.outputFolderName || safeFilePart(request.outputName)
: `${safeFilePart(request.outputName)}_${exportStamp()}`
const root = options.outputRoot || (await resolveDefaultExportRoot(outputFolder))
await fs.mkdir(root, { recursive: true })
const outputDir = join(root, outputFolder)
const outputPath =
request.format === 'html'
@@ -1506,8 +1518,8 @@ async function runAllExport(
throw new Error('导出聊天不能重复')
}
const exportRoot = join(app.getPath('documents'), 'WechatExplorer', '导出')
const outputFolder = safeFilePart(request.outputName)
const exportRoot = await resolveDefaultExportRoot(outputFolder)
outputDir = join(exportRoot, outputFolder)
const folderNames = conversationFolderNames(targets)
let lastProgressAt = 0
+1 -1
View File
@@ -75,7 +75,7 @@ const embedAvatar = async (source: string | undefined, name: string): Promise<st
if (/^https?:\/\//i.test(source)) {
const response = await fetch(source, {
headers: {
'User-Agent': 'Mozilla/5.0 WechatExplorer',
'User-Agent': 'Mozilla/5.0 TraceMemo',
Referer: 'https://weixin.qq.com/'
},
signal: AbortSignal.timeout(8000)
+9 -9
View File
@@ -167,7 +167,7 @@ const routes: Record<string, RouteHandler> = {
sendJson(res, 200, {
ok: true,
ready: isReady(),
service: 'WechatExplorer Reader',
service: 'TraceMemo Reader',
version: '1.0.0',
timestamp: new Date().toISOString()
})
@@ -186,7 +186,7 @@ const routes: Record<string, RouteHandler> = {
},
'/api/v1/contact': ({ res, url }) => {
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化')
if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
const filter = url.searchParams.get('filter') || undefined
const type = url.searchParams.get('type') || undefined
let contacts = listContacts(filter)
@@ -197,7 +197,7 @@ const routes: Record<string, RouteHandler> = {
},
'/api/v1/chatroom': ({ res, url }) => {
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化')
if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
const keyword = url.searchParams.get('keyword') || ''
let groups = listContacts().filter((c) => c.type === 'group')
if (keyword) {
@@ -212,14 +212,14 @@ const routes: Record<string, RouteHandler> = {
},
'/api/v1/recent_chat': ({ res, url }) => {
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化')
if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
const limit = parseNumeric(url.searchParams.get('limit'), 50)
const items = listRecentChat(limit)
sendJson(res, 200, { count: items.length, items })
},
'/api/v1/chatlog': ({ res, url }) => {
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化')
if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
const talker = url.searchParams.get('talker')
if (!talker) return sendError(res, 400, '缺少必要参数 talker')
@@ -257,7 +257,7 @@ const routes: Record<string, RouteHandler> = {
},
'/api/v1/group_snapshot': ({ res, url }) => {
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化')
if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
const md5 = url.searchParams.get('md5')
if (!md5) return sendError(res, 400, '缺少必要参数 md5')
const snapshot = getGroupSnapshot(md5)
@@ -266,7 +266,7 @@ const routes: Record<string, RouteHandler> = {
},
'/api/v1/resolve': ({ res, url }) => {
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化')
if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
const q = url.searchParams.get('q')
if (!q) return sendError(res, 400, '缺少必要参数 q')
const contact = resolveMd5(q)
@@ -276,7 +276,7 @@ const routes: Record<string, RouteHandler> = {
'/api/v1/report': async ({ req, res, body }) => {
if (req.method !== 'POST') return sendError(res, 405, '需要 POST 请求')
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化')
if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
if (typeof body !== 'string' || !body.trim()) {
return sendError(res, 400, '请求体为空,需 POST GroupReportExportRequest JSON')
}
@@ -300,7 +300,7 @@ const routes: Record<string, RouteHandler> = {
'/api/v1/agent/group-report': async ({ req, res, body }) => {
if (req.method !== 'POST') return sendError(res, 405, '需要 POST 请求')
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化')
if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
let request: { group?: string; range?: 'today' | 'yesterday' | '7days' }
try {
request = JSON.parse(typeof body === 'string' ? body : '{}')
+2 -2
View File
@@ -562,7 +562,7 @@ export class ImageDecryptService {
}
/**
* 根据 md5 查找图片文件 (WechatExplorer 风格)
* 根据 md5 查找图片文件
*/
findImageFile(md5?: string, imageDatName?: string, options?: ImageFindOptions): string | null {
const allowThumbnail = options?.allowThumbnail !== false
@@ -648,7 +648,7 @@ export class ImageDecryptService {
}
}
// 尝试 WechatExplorer 的目录结构: msg/attach/{hash}/{YYYY-MM}/Img/
// 尝试 TraceMemo 兼容的微信目录结构: msg/attach/{hash}/{YYYY-MM}/Img/
if (!existsSync(attachDir)) {
imageDecryptLog('[ImageDecrypt] attach dir not found:', attachDir)
return rememberPath(
+6 -16
View File
@@ -1,3 +1,4 @@
import './app-data-bootstrap'
import './preload-env'
import {
app,
@@ -272,17 +273,6 @@ protocol.registerSchemesAsPrivileged([
}
])
// WCDB's Windows runtime checks the host application name during wcdb_init.
// Mirroring WeFlow's name unblocks the -1006 init failure on Windows.
app.setName(
process.platform === 'win32'
? 'WeFlow'
: process.env['WXE_USER_DATA']
? 'WechatExplorer Dev'
: 'WechatExplorer'
)
const isolatedUserData = process.env['WXE_USER_DATA']
if (isolatedUserData) app.setPath('userData', isolatedUserData)
let dbInitInFlight: Promise<{ success: boolean; monitoring?: boolean; error?: string }> | null =
null
let appShutdownRequested = false
@@ -435,7 +425,7 @@ function createWindow(): void {
void dialog
.showMessageBox(mainWindow, {
type: 'question',
title: '关闭 WechatExplorer',
title: '关闭 TraceMemo',
message: '请选择关闭方式',
detail: '你可以将窗口隐藏到系统托盘,或退出整个应用进程。',
buttons: ['最小化到系统托盘', '关闭进程', '取消'],
@@ -516,11 +506,11 @@ app.whenReady().then(async () => {
return new Response('Media unavailable', { status: 500 })
}
})
console.log(`WechatExplorer main build: ${BUILD_MARK}`)
console.log(`TraceMemo main build: ${BUILD_MARK}`)
appLogger.write({
level: 'info',
scope: 'lifecycle',
message: 'WechatExplorer 启动',
message: 'TraceMemo 启动',
details: { build: BUILD_MARK, platform: process.platform, version: app.getVersion() }
})
process.on('uncaughtException', (error) => {
@@ -1695,7 +1685,7 @@ function buildTrayMenu(): Menu {
},
{ type: 'separator' },
{
label: '退出 WechatExplorer',
label: '退出 TraceMemo',
click: () => {
tray?.destroy()
tray = null
@@ -1714,7 +1704,7 @@ function setupTray(): void {
? nativeImage.createEmpty()
: image.resize({ width: traySize, height: traySize, quality: 'best' })
tray = new Tray(trayImage)
tray.setToolTip('WechatExplorer')
tray.setToolTip('TraceMemo')
// macOS may show a Tray context menu on a primary click when it is set
// directly on the Tray. Keep the menu for an explicit secondary click so
// the primary click only restores the main window.
+1 -1
View File
@@ -33,5 +33,5 @@ try {
process.env.WEFLOW_PROJECT_NAME = process.env.WEFLOW_PROJECT_NAME || 'WeFlow'
prependPath(dllDirs.filter((dir) => fs.existsSync(dir)))
} catch (error) {
console.error('[WechatExplorer] failed to enforce local DLL priority:', error)
console.error('[TraceMemo] failed to enforce local DLL priority:', error)
}
+5 -5
View File
@@ -455,7 +455,7 @@ class AgentHubService {
private async replyRecentChats(inbound: InboundMessage, limit: number): Promise<void> {
if (!isReady()) {
await this.sendConnector(inbound, 'WechatExplorer 本地数据库尚未连接,请连接后再试。')
await this.sendConnector(inbound, 'TraceMemo 本地数据库尚未连接,请连接后再试。')
return
}
const items = listRecentChat(limit)
@@ -474,7 +474,7 @@ class AgentHubService {
const result = await agentAIProvider.chat([
{
role: 'system',
content: `你是 WechatExplorer 微信机器人的意图理解器。只能输出一行 JSON,不要 Markdown。
content: `你是 TraceMemo 微信机器人的意图理解器。只能输出一行 JSON,不要 Markdown。
支持的工具:
1. recent:查看最近会话,参数 limit 为 1-20。
2. contact:查看我与某个联系人的最近聊天,参数 contact 和 limit。
@@ -544,7 +544,7 @@ class AgentHubService {
intent: ContactChatIntent
): Promise<void> {
if (!isReady()) {
await this.sendConnector(inbound, 'WechatExplorer 本地数据库尚未连接,请连接后再试。')
await this.sendConnector(inbound, 'TraceMemo 本地数据库尚未连接,请连接后再试。')
return
}
@@ -580,7 +580,7 @@ class AgentHubService {
): Promise<void> {
try {
if (!isReady()) {
await this.sendConnector(inbound, 'WechatExplorer 本地数据库尚未连接,请连接后再试。')
await this.sendConnector(inbound, 'TraceMemo 本地数据库尚未连接,请连接后再试。')
return
}
const contact = resolveMd5(intent.contact)
@@ -642,7 +642,7 @@ class AgentHubService {
): Promise<void> {
try {
if (!isReady()) {
await this.sendConnector(inbound, 'WechatExplorer 本地数据库尚未连接,请连接后再试。')
await this.sendConnector(inbound, 'TraceMemo 本地数据库尚未连接,请连接后再试。')
return
}
const group = this.resolveGroup(intent.group)
+1 -1
View File
@@ -80,7 +80,7 @@ const agentSystemPrompt = (
question: string,
scopeLabel: string,
rangeLabel: string
): string => `你是 WechatExplorer 的受控本地聊天搜索代理,只负责决定下一步检索,不回答用户问题。
): string => `你是 TraceMemo 的受控本地聊天搜索代理,只负责决定下一步检索,不回答用户问题。
用户问题:${question}
允许范围:${scopeLabel};时间范围:${rangeLabel}
@@ -28,6 +28,7 @@ export async function inspectImageDecryptionStatus(
const imageDirectoryFound = hasImageDirectory(accountRoot)
const stickerCacheFound =
fs.existsSync(path.join(accountRoot, 'cache')) ||
fs.existsSync(path.join(os.homedir(), 'Documents', 'TraceMemo', 'Emojis')) ||
fs.existsSync(path.join(os.homedir(), 'Documents', 'WechatExplorer', 'Emojis'))
const dbConnected = chat.isReady()
const [wechatRunning, decoder] = await Promise.all([
@@ -287,7 +288,7 @@ export function buildImageTestDiagnosticLog(input: {
const rootIsDirectory = rootExists ? safeIsDirectory(root) : false
const resultCode = input.result.success ? 'SUCCESS' : input.result.code || 'UNKNOWN'
return [
'WechatExplorer 图片解析测试日志(已脱敏)',
'TraceMemo 图片解析测试日志(已脱敏)',
`时间:${new Date().toISOString()}`,
`应用版本:${safeAppVersion()}`,
`运行环境:${process.platform} ${process.arch}`,
+1 -1
View File
@@ -1,5 +1,5 @@
// src/main/services/image-insight-service.ts
// WechatExplorer AI 图片理解基础设施
// TraceMemo AI 图片理解基础设施
//
// 设计原则:
// 1. base64 不走 IPC,只在 main 内部流转(renderer 只看到 ImageInsight 结构化结果)
+1 -1
View File
@@ -90,7 +90,7 @@ export function resolveSkillResourceStatus(
available: false,
source,
githubUrl: GITHUB_URL,
error: `未找到 WechatExplorer Reader Skill 文件(已检查:${candidates.map((item) => item.path).join('')}`
error: `未找到 TraceMemo Reader Skill 文件(已检查:${candidates.map((item) => item.path).join('')}`
}
}
return {
+6 -3
View File
@@ -19,9 +19,12 @@ const downloadCache = new Map<string, Promise<StickerResult>>()
export class StickerService {
private readonly cacheDir: string
private readonly legacyCacheDir: string
constructor(private readonly wcdb4Client?: Wcdb4Client | null) {
this.cacheDir = path.join(os.homedir(), 'Documents', 'WechatExplorer', 'Emojis')
this.cacheDir = path.join(os.homedir(), 'Documents', 'TraceMemo', 'Emojis')
// Keep reading the former directory so existing sticker caches remain usable.
this.legacyCacheDir = path.join(os.homedir(), 'Documents', 'WechatExplorer', 'Emojis')
}
async resolveSticker(cdnUrl?: string, md5?: string): Promise<StickerResult> {
@@ -69,7 +72,7 @@ export class StickerService {
const extensions = ['.gif', '.png', '.webp', '.jpg', '.jpeg']
const cacheDirs = [
this.cacheDir,
path.join(os.homedir(), 'Documents', 'WechatExplorer', 'Emojis')
this.legacyCacheDir
]
for (const cacheDir of cacheDirs) {
for (const ext of extensions) {
@@ -128,7 +131,7 @@ export class StickerService {
url,
{
headers: {
'User-Agent': 'Mozilla/5.0 MicroMessenger WechatExplorer',
'User-Agent': 'Mozilla/5.0 MicroMessenger TraceMemo',
Referer: 'https://weixin.qq.com/'
}
},
+1
View File
@@ -261,6 +261,7 @@ export function resolveWindowsNativeAccountRoot(
.find((candidate) => candidate && isAsciiPath(candidate))
if (!publicRoot || !isAsciiPath(publicRoot)) return accountRoot
// Preserve the legacy ASCII bridge path so existing junctions remain reusable.
const bridgeRoot = path.join(publicRoot, 'WechatExplorer', 'path-bridges')
const bridgePath = path.join(
bridgeRoot,
+1 -1
View File
@@ -2,7 +2,7 @@
<html>
<head>
<meta charset="UTF-8" />
<title>WechatExplorer</title>
<title>TraceMemo(迹忆)</title>
<link rel="icon" type="image/svg+xml" href="/src/assets/brand-icon.svg" />
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
+2 -2
View File
@@ -805,7 +805,7 @@ function App(): React.ReactElement {
})
setStartupProgress({
title: '正在加载账号信息...',
subtitle: '即将进入 WechatExplorer',
subtitle: '即将进入 TraceMemo',
detail: '正在读取联系人和当前账号',
percent: 70
})
@@ -1813,7 +1813,7 @@ function App(): React.ReactElement {
? autoConnectSource === 'env'
? '检测到环境变量中的密钥'
: '使用上次安全保存的密钥'
: 'WechatExplorer')
: 'TraceMemo')
return (
<div className={`boot-splash ${appearanceSettings.showStartupProgress ? '' : 'is-quiet'}`}>
<div className="boot-splash-spinner" aria-hidden />
@@ -141,12 +141,12 @@ export function DatabaseConnectionPage({
return (
<main className="database-login-page">
<section className="database-login-brand" aria-label="WechatExplorer 产品说明">
<section className="database-login-brand" aria-label="TraceMemo(迹忆)产品说明">
<div className="database-login-brand-content">
<div className="database-login-logo" aria-hidden="true">
<LineIcon name="database" />
</div>
<h1>WechatExplorer</h1>
<h1>TraceMemo</h1>
<p className="database-login-tagline"> AI </p>
<p className="database-login-description">
使 AI
@@ -261,7 +261,7 @@ export function DatabaseConnectionPage({
'确认下方检测结果;没有找到目录时可以手动选择。',
'请退出当前微信账号,让微信停留在登录页面,然后点击“我已准备好”。',
'开始后请按页面提示完成系统授权。',
'正在准备连接组件,请不要关闭微信或 WechatExplorer。',
'正在准备连接组件,请不要关闭微信或 TraceMemo。',
'请回到微信完成登录,登录成功后再回来验证。',
'正在验证密钥和本地数据库,请稍候。'
][guideStep - 1]}
@@ -567,7 +567,7 @@ export function DatabaseConnectionPage({
<button type="button" onClick={onClearKey}>
</button>
<span>WechatExplorer</span>
<span>TraceMemo</span>
</div>
</div>
</section>
@@ -478,7 +478,7 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({
API Bearer Token Token API Center
Claude / Codex
<br />
:<code>docs/skill/wechatexplorer-reader/SKILL.md</code>
:<code>docs/skill/wechatexplorer-reader/SKILL.md</code>
</div>
</section>
@@ -45,7 +45,7 @@ export function ExportTaskCenter({
const copyTaskLog = async (task: ExportTaskRecord): Promise<void> => {
const log = [
'WechatExplorer 导出任务日志',
'TraceMemo 导出任务日志',
`时间:${new Date(task.createdAt).toLocaleString('zh-CN')}`,
`会话:${task.targetLabel}`,
`格式:${task.format.toUpperCase()}`,
@@ -431,13 +431,13 @@ export function ExportWorkspace({
const targetPath = exportAll
? format === 'html' && zip
? `文稿/WechatExplorer/导出/${outputName}.zip`
: `文稿/WechatExplorer/导出/${outputName}/`
? `文稿/TraceMemo/导出/${outputName}.zip`
: `文稿/TraceMemo/导出/${outputName}/`
: format === 'html'
? zip
? `文稿/WechatExplorer/导出/${outputName}.zip`
: `文稿/WechatExplorer/导出/${outputName}/`
: `文稿/WechatExplorer/导出/${outputName}.${format === 'markdown' ? 'md' : format}`
? `文稿/TraceMemo/导出/${outputName}.zip`
: `文稿/TraceMemo/导出/${outputName}/`
: `文稿/TraceMemo/导出/${outputName}.${format === 'markdown' ? 'md' : format}`
return (
<div className="export-workspace">
@@ -26,7 +26,7 @@ interface AppShellProps {
function BrandLogo(): React.ReactElement {
return (
<div className="app-brand" title="WechatExplorer" aria-label="WechatExplorer">
<div className="app-brand" title="TraceMemo(迹忆)" aria-label="TraceMemo(迹忆)">
<img src={brandIcon} alt="" aria-hidden="true" />
</div>
)
@@ -194,7 +194,7 @@ export function AiReportWorkspace({
<h3></h3>
<p></p>
<p></p>
<p>WechatExplorer </p>
<p>TraceMemo </p>
</section>
{generatedImage && (
@@ -1094,7 +1094,7 @@ export function AISearchWorkspace({
<div className="ai-search-workspace">
<header className="ai-search-header">
<div>
<span className="ai-search-kicker">WechatExplorer · LOCAL INTELLIGENCE</span>
<span className="ai-search-kicker">TraceMemo · LOCAL INTELLIGENCE</span>
<h1></h1>
<p></p>
</div>
@@ -95,7 +95,7 @@ export function AgentHubWorkspace(): React.ReactElement {
<div className="agent-hub-workspace">
<header className="agent-hub-header">
<div>
<div className="agent-hub-eyebrow">WechatExplorer</div>
<div className="agent-hub-eyebrow">TraceMemo</div>
<h1>Agent Hub</h1>
<p> AI </p>
</div>
@@ -193,7 +193,7 @@ export function AgentHubWorkspace(): React.ReactElement {
<aside className="agent-hub-card agent-hub-capability-card">
<span className="agent-hub-card-kicker"></span>
<h2></h2>
<p> Agent Hub WechatExplorer</p>
<p> Agent Hub TraceMemo</p>
<div className="agent-hub-example">
<span></span>
<strong> 5 </strong>
@@ -195,7 +195,7 @@ export function ApiRuntimePanel({
<h3></h3>
<p>
{localOnly
? '本地 API 默认监听 127.0.0.1。WechatExplorer 不会通过该接口自动把聊天内容发送到云端。外部 Agent 是否调用第三方模型,取决于其自身配置。'
? '本地 API 默认监听 127.0.0.1。TraceMemo 不会通过该接口自动把聊天内容发送到云端。外部 Agent 是否调用第三方模型,取决于其自身配置。'
: '当前服务并非仅本机访问。请确认局域网环境可信;API Token 不等同于公网安全防护。'}
</p>
</section>
@@ -37,13 +37,13 @@ export function ReaderSkillOverview({
<div className="api-workspace-heading">
<div>
<div className="api-title-line">
<h1>WechatExplorer Reader</h1>
<h1>TraceMemo Reader</h1>
<span className={`api-skill-status ${skill?.available ? 'ready' : 'error'}`}>
{skill?.available ? '已安装' : '文件不可用'}
</span>
<span className="api-version">{skill?.version || 'v1.0'}</span>
</div>
<p> HTTP API WechatExplorer </p>
<p> HTTP API TraceMemo </p>
</div>
<div className="api-header-actions">
<button type="button" onClick={onPreview} disabled={!skill?.available}>
@@ -79,9 +79,9 @@ export function ReaderSkillOverview({
<div className="api-introduction">
<h2></h2>
<p>
WechatExplorer Reader AI Agent
TraceMemo Reader AI Agent
WechatExplorer API
TraceMemo API
</p>
<div className="api-flow">
<span>AI Agent</span>
@@ -18,6 +18,10 @@ export function SkillDetails({
<dl>
<div>
<dt></dt>
<dd>TraceMemo Reader</dd>
</div>
<div>
<dt></dt>
<dd>wechatexplorer-reader</dd>
</div>
<div>
@@ -43,7 +43,7 @@ export function SkillInstallFlow({
<section className={service?.running && dbReady ? 'done' : 'active'}>
<b>1</b>
<div>
<h3> WechatExplorer </h3>
<h3> TraceMemo </h3>
<p>
API{service?.running ? '运行中' : '已停止'} · {address}
</p>
@@ -16,12 +16,12 @@ export function SkillPreviewDialog({
className="api-markdown-overlay"
role="dialog"
aria-modal="true"
aria-label="WechatExplorer Reader Skill 预览"
aria-label="TraceMemo Reader Skill 预览"
>
<div>
<header>
<div>
<strong>WechatExplorer Reader</strong>
<strong>TraceMemo Reader</strong>
<span>{version || 'v1.0'}</span>
</div>
<div>
@@ -18,7 +18,7 @@ export const REPORT_REQUEST_PRESET = JSON.stringify(
activeUsers: 0,
timeSpan: '00:00-23:59',
generatedAt: '2026-07-13 22:00',
recordNote: '本日报由 WechatExplorer 自动生成',
recordNote: '本日报由 TraceMemo 自动生成',
footerNote: '',
heroParticipants: [],
avatars: {},
@@ -37,7 +37,7 @@ export const AGENT_GROUP_REPORT_PRESET = JSON.stringify(
)
export const AGENT_SEND_PRESET = JSON.stringify(
{ to: '', text: 'WechatExplorer Agent Hub 发送测试' },
{ to: '', text: 'TraceMemo Agent Hub 发送测试' },
null,
2
)
@@ -7,13 +7,13 @@ function requestHost(host: string): string {
function opening(target: AgentInstallTarget): string {
switch (target) {
case 'codex':
return '请将本地目录中的 WechatExplorer Reader Skill 安装到当前 Codex 项目或用户 Skill 目录:'
return '请将本地目录中的 TraceMemo Reader Skill 安装到当前 Codex 项目或用户 Skill 目录:'
case 'claude-code':
return '请安装以下本地 WechatExplorer Reader Skill,并按照 SKILL.md 调用本地 HTTP API'
return '请安装以下本地 TraceMemo Reader Skill,并按照 SKILL.md 调用本地 HTTP API'
case 'openclaw':
return '请将以下本地目录作为 WechatExplorer Reader Skill 安装,并阅读其中的 SKILL.md'
return '请将以下本地目录作为 TraceMemo Reader Skill 安装,并阅读其中的 SKILL.md'
default:
return '请读取并安装以下 WechatExplorer Reader Skill'
return '请读取并安装以下 TraceMemo Reader Skill'
}
}
@@ -32,10 +32,10 @@ export function buildSkillInstallInstruction({
const sourceText =
source.type === 'local'
? `${opening(target)}\n\n${source.directoryPath}\n\n请先阅读该目录中的 SKILL.md,然后调用:`
: `请从以下地址安装 WechatExplorer Reader Skill\n\n${source.installUrl}\n\n阅读 SKILL.md 后,调用:`
return `${sourceText}\n\n${healthUrl}\n\n先调用公开的 health 接口验证服务。然后请用户在 WechatExplorer → API Center → API Token 中点击“复制 Token”,并把 Token 配置为 Agent 本机环境变量 WECHATEXPLORER_API_TOKEN。读取联系人、会话或聊天记录时,必须发送 Authorization: Bearer $WECHATEXPLORER_API_TOKEN。此服务是 Local HTTP API,不是 MCP Server。安装完成后告诉我验证结果。`
: `请从以下地址安装 TraceMemo Reader Skill\n\n${source.installUrl}\n\n阅读 SKILL.md 后,调用:`
return `${sourceText}\n\n${healthUrl}\n\n先调用公开的 health 接口验证服务。然后请用户在 TraceMemo → API Center → API Token 中点击“复制 Token”,并把 Token 配置为 Agent 本机环境变量 WECHATEXPLORER_API_TOKEN。读取联系人、会话或聊天记录时,必须发送 Authorization: Bearer $WECHATEXPLORER_API_TOKEN。此服务是 Local HTTP API,不是 MCP Server。安装完成后告诉我验证结果。`
}
export function buildSkillVerificationPrompt(): string {
return '请检查 WechatExplorer Reader 是否已连接,然后列出最近 5 个微信会话。'
return '请检查 TraceMemo Reader 是否已连接,然后列出最近 5 个微信会话。'
}
@@ -75,7 +75,7 @@ export function AIImageUnderstandingTest({
</label>
<p className="ai-vision-privacy">
AI WechatExplorer
AI TraceMemo
</p>
{state.error ? <p className="ai-vision-error">{state.error}</p> : null}
@@ -30,7 +30,7 @@ export function DatabaseKeyAutoDetect({
<div className="database-key-auto-heading">
<div>
<strong>Windows </strong>
<p>WechatExplorer </p>
<p>TraceMemo </p>
</div>
<button
type="button"
@@ -63,7 +63,7 @@ export function DatabaseKeyDangerZone({
>
<h2 id="database-key-confirm-title"></h2>
<p>
WechatExplorer
TraceMemo
</p>
<div>
@@ -99,7 +99,7 @@ export function DatabaseKeyDangerZone({
>
<h2 id="database-key-return-confirm-title"></h2>
<p>
WechatExplorer
TraceMemo
</p>
<div>
@@ -26,7 +26,7 @@ export function buildDatabaseKeyDiagnostics(
): string {
const validation = state.validation
return [
'WechatExplorer 数据库密钥诊断',
'TraceMemo 数据库密钥诊断',
`已保存: ${state.saved ? '是' : '否'}`,
`已验证: ${validation?.success ? '是' : '否'}`,
`密钥长度合法: ${isDatabaseKeyFormatValid(input) ? '是' : '否'}`,
@@ -54,13 +54,13 @@ export function AboutPage({ onNotice }: { onNotice: (message: string) => void })
<header className="settings-page-header">
<div>
<h1></h1>
<p>WechatExplorer </p>
<p>TraceMemo AI </p>
</div>
</header>
<div className="settings-page-scroll">
<div className="settings-page-content">
<section className="settings-card about-identity-card">
<div><span className="settings-card-kicker"></span><strong>WechatExplorer</strong><small>v{update.currentVersion}</small></div>
<div><span className="settings-card-kicker"></span><strong>TraceMemo</strong><small>v{update.currentVersion}</small></div>
<a href={REPOSITORY_URL} target="_blank" rel="noreferrer">GitHub </a>
</section>
@@ -76,7 +76,7 @@ export function DatabaseKeyPage({
<div>
<strong></strong>
<p>
WechatExplorer
TraceMemo
使
</p>
</div>
@@ -51,7 +51,7 @@ export function ImageDecryptionPage({
</svg>
<div>
<strong></strong>
<p>WechatExplorer </p>
<p>TraceMemo </p>
</div>
</section>
+1 -1
View File
@@ -658,7 +658,7 @@ export const buildGroupReportFacts = async (
mediaMessageCount: imageCount + voiceCount + stickerCount,
timeSpan,
generatedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
recordNote: `基于 WechatExplorer 已加载的 ${transcriptRows.length} 条记录`,
recordNote: `基于 TraceMemo 已加载的 ${transcriptRows.length} 条记录`,
footerNote: '基于已读取聊天记录生成;图片、表情等未解析内容默认只按类型与上下文参与日报。',
heroParticipants: topSpeakers.slice(0, 4).map((speaker) => speaker.name),
avatars,
+1 -1
View File
@@ -4,7 +4,7 @@ export function buildSafeDiagnosticSummary(
environment: Omit<DatabaseKeyEnvironment, 'diagnosticSummary'>
): string {
return [
`WechatExplorer: ${environment.appVersion}`,
`TraceMemo: ${environment.appVersion}`,
`操作系统: ${environment.osVersion}`,
`微信客户端: ${environment.wechatVersion}`,
`数据结构: ${environment.dataStructureVersion}`,
+1 -1
View File
@@ -1,6 +1,6 @@
// src/shared/image-insight.ts
// ImageInsight:微信图片的 AI 理解结果持久化数据结构
// 与 WechatExplorer 整体 AI 知识平台定位一致 — 图片理解结果可索引、可缓存、可复用。
// 与 TraceMemo 整体 AI 知识平台定位一致 — 图片理解结果可索引、可缓存、可复用。
export type ImageCategory =
| 'screenshot' // 截图
+1 -1
View File
@@ -1,6 +1,6 @@
export const WINDOWS_VC_RUNTIME_DOWNLOAD_URL = 'https://aka.ms/vc14/vc_redist.x64.exe'
export const WINDOWS_VC_RUNTIME_ERROR_MESSAGE = `当前 Windows 缺少 Microsoft Visual C++ 2015-2022 x64 运行库,无法加载微信数据库组件。请下载安装后重新启动 WechatExplorer${WINDOWS_VC_RUNTIME_DOWNLOAD_URL}`
export const WINDOWS_VC_RUNTIME_ERROR_MESSAGE = `当前 Windows 缺少 Microsoft Visual C++ 2015-2022 x64 运行库,无法加载微信数据库组件。请下载安装后重新启动 TraceMemo${WINDOWS_VC_RUNTIME_DOWNLOAD_URL}`
const VC_RUNTIME_LIBRARY_PATTERN =
/(?:vcruntime140(?:_1)?\.dll|msvcp140(?:_[12])?\.dll|concrt140\.dll|ucrtbase\.dll|api-ms-win-crt)/i