mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-08 07:21:19 +08:00
feat: 重写agent hub连接器,新增对话记录和微信输入状态
- 新增 Agent Hub 对话记录面板,按会话回看机器人与微信用户的完整收发内容 - 新增微信原生正在输入状态,长任务维持 typing,异常路径强制收尾
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
import { mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { AgentHubConversationStore } from '../../src/main/services/agent-hub-conversation-store'
|
||||
|
||||
describe('AgentHubConversationStore', () => {
|
||||
const roots: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
while (roots.length) rmSync(roots.pop()!, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
function createStore(options: { maxMessages?: number; maxConversations?: number } = {}): {
|
||||
store: AgentHubConversationStore
|
||||
filePath: string
|
||||
} {
|
||||
const root = mkdtempSync(join(tmpdir(), 'tracememo-conv-'))
|
||||
roots.push(root)
|
||||
const filePath = join(root, 'agent-hub', 'conversations.json')
|
||||
let sequence = 0
|
||||
const store = new AgentHubConversationStore({
|
||||
filePath: () => filePath,
|
||||
createId: () => `m-${(sequence += 1)}`,
|
||||
now: () => 1_700_000_000_000,
|
||||
...(options.maxMessages !== undefined
|
||||
? { maxMessagesPerConversation: options.maxMessages }
|
||||
: {}),
|
||||
...(options.maxConversations !== undefined
|
||||
? { maxConversations: options.maxConversations }
|
||||
: {})
|
||||
})
|
||||
return { store, filePath }
|
||||
}
|
||||
|
||||
it('记录一次收发并给出会话摘要', () => {
|
||||
const { store, filePath } = createStore()
|
||||
|
||||
store.append({ userId: 'user-a', direction: 'in', kind: 'text', text: '你好' })
|
||||
const result = store.append({
|
||||
userId: 'user-a',
|
||||
direction: 'out',
|
||||
kind: 'text',
|
||||
text: '这是回答',
|
||||
status: 'sent'
|
||||
})
|
||||
|
||||
expect(result?.summary).toMatchObject({
|
||||
userId: 'user-a',
|
||||
messageCount: 2,
|
||||
lastPreview: '这是回答',
|
||||
lastDirection: 'out'
|
||||
})
|
||||
expect(result?.message).toMatchObject({ direction: 'out', status: 'sent', text: '这是回答' })
|
||||
|
||||
const conversation = store.get('user-a')
|
||||
expect(conversation?.messages.map((item) => item.direction)).toEqual(['in', 'out'])
|
||||
expect(statSync(filePath).mode & 0o777).toBe(0o600)
|
||||
})
|
||||
|
||||
it('保留发送失败的记录与错误码,便于在界面上对账', () => {
|
||||
const { store } = createStore()
|
||||
store.append({
|
||||
userId: 'user-a',
|
||||
direction: 'out',
|
||||
kind: 'text',
|
||||
text: '发不出去的内容',
|
||||
status: 'failed',
|
||||
errorCode: 'STALE_TOKEN'
|
||||
})
|
||||
|
||||
const message = store.get('user-a')?.messages[0]
|
||||
expect(message).toMatchObject({ status: 'failed', errorCode: 'STALE_TOKEN' })
|
||||
})
|
||||
|
||||
it('媒体消息只保留类型占位,不写入路径细节以外的内容', () => {
|
||||
const { store } = createStore()
|
||||
const result = store.append({
|
||||
userId: 'user-a',
|
||||
direction: 'out',
|
||||
kind: 'image',
|
||||
text: '群日报.png',
|
||||
status: 'sent'
|
||||
})
|
||||
|
||||
expect(result?.summary.lastPreview).toBe('群日报.png')
|
||||
// 空文本的媒体消息在列表里退化为中文占位。
|
||||
store.append({ userId: 'user-a', direction: 'in', kind: 'image', text: '' })
|
||||
expect(store.listSummaries()[0].lastPreview).toBe('[图片]')
|
||||
})
|
||||
|
||||
it('单个会话超出上限时丢弃最旧的消息', () => {
|
||||
const { store } = createStore({ maxMessages: 3 })
|
||||
for (let index = 0; index < 5; index += 1) {
|
||||
store.append({ userId: 'user-a', direction: 'in', kind: 'text', text: `第 ${index} 条` })
|
||||
}
|
||||
|
||||
const messages = store.get('user-a')?.messages ?? []
|
||||
expect(messages).toHaveLength(3)
|
||||
expect(messages.map((item) => item.text)).toEqual(['第 2 条', '第 3 条', '第 4 条'])
|
||||
})
|
||||
|
||||
it('会话数超出上限时只保留最近活跃的', () => {
|
||||
const { store } = createStore({ maxConversations: 2 })
|
||||
store.append({
|
||||
userId: 'old',
|
||||
direction: 'in',
|
||||
kind: 'text',
|
||||
text: '旧会话',
|
||||
createdAt: 1_000
|
||||
})
|
||||
store.append({
|
||||
userId: 'mid',
|
||||
direction: 'in',
|
||||
kind: 'text',
|
||||
text: '中间会话',
|
||||
createdAt: 2_000
|
||||
})
|
||||
store.append({
|
||||
userId: 'new',
|
||||
direction: 'in',
|
||||
kind: 'text',
|
||||
text: '新会话',
|
||||
createdAt: 3_000
|
||||
})
|
||||
|
||||
const summaries = store.listSummaries()
|
||||
expect(summaries.map((item) => item.userId)).toEqual(['new', 'mid'])
|
||||
expect(store.get('old')).toBeNull()
|
||||
})
|
||||
|
||||
it('会话列表按最近活跃排序,并带上条数', () => {
|
||||
const { store } = createStore()
|
||||
store.append({ userId: 'a', direction: 'in', kind: 'text', text: '1', createdAt: 1_000 })
|
||||
store.append({ userId: 'b', direction: 'in', kind: 'text', text: '2', createdAt: 2_000 })
|
||||
store.append({ userId: 'a', direction: 'in', kind: 'text', text: '3', createdAt: 3_000 })
|
||||
|
||||
expect(store.listSummaries().map((item) => [item.userId, item.messageCount])).toEqual([
|
||||
['a', 2],
|
||||
['b', 1]
|
||||
])
|
||||
})
|
||||
|
||||
it('get 返回副本,外部改动不会污染缓存', () => {
|
||||
const { store } = createStore()
|
||||
store.append({ userId: 'a', direction: 'in', kind: 'text', text: '原始' })
|
||||
|
||||
const first = store.get('a')
|
||||
first!.messages[0].text = '被改过'
|
||||
expect(store.get('a')?.messages[0].text).toBe('原始')
|
||||
})
|
||||
|
||||
it('清空后文件与内存都为空', () => {
|
||||
const { store, filePath } = createStore()
|
||||
store.append({ userId: 'a', direction: 'in', kind: 'text', text: 'x' })
|
||||
|
||||
store.clear()
|
||||
|
||||
expect(store.listSummaries()).toEqual([])
|
||||
expect(store.get('a')).toBeNull()
|
||||
expect(JSON.parse(readFileSync(filePath, 'utf8'))).toEqual({ version: 1, conversations: [] })
|
||||
})
|
||||
|
||||
it('忽略空会话标识,不产生垃圾条目', () => {
|
||||
const { store } = createStore()
|
||||
expect(store.append({ userId: ' ', direction: 'in', kind: 'text', text: 'x' })).toBeNull()
|
||||
expect(store.listSummaries()).toEqual([])
|
||||
})
|
||||
|
||||
it('文件损坏时按空库处理,不抛错', () => {
|
||||
const { store, filePath } = createStore()
|
||||
store.append({ userId: 'a', direction: 'in', kind: 'text', text: 'x' })
|
||||
rmSync(filePath)
|
||||
// 重新实例化,读一个不存在的文件
|
||||
const fresh = new AgentHubConversationStore({ filePath: () => filePath })
|
||||
expect(fresh.listSummaries()).toEqual([])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user