feat: 内置微信连接器并完善 Agent 查询能力

This commit is contained in:
Wxw-Gu
2026-07-15 20:27:05 +08:00
parent 1e97953d67
commit eaea8d8435
26 changed files with 2340 additions and 38 deletions
+75 -1
View File
@@ -14,7 +14,7 @@ import type {
} from '../../shared/agent-hub'
import type { AppSettings } from './settings-store'
import { generateAgentGroupReport } from './agent-group-report-service'
import { isReady, listRecentChat } from './chat-service'
import { isReady, listMessages, listRecentChat, resolveMd5 } from './chat-service'
const execFileAsync = promisify(execFile)
const HEALTH_INTERVAL_MS = 5_000
@@ -36,6 +36,11 @@ interface GroupReportIntent {
range: 'today' | 'yesterday' | '7days'
}
interface ContactChatIntent {
contact: string
limit: number
}
function resolveBundledBinary(
resourceSegments: string[],
executable: string,
@@ -323,6 +328,13 @@ class AgentHubService {
return this.sendHubJson(response, 202, { status: 'generating' })
}
const contactChatIntent = this.matchContactChatIntent(text)
if (contactChatIntent) {
if (messageId) this.processedMessages.set(messageId, Date.now())
await this.replyContactChat(inbound, contactChatIntent)
return this.sendHubJson(response, 200, { status: 'ok' })
}
const limit = this.matchRecentChatIntent(text)
if (limit === null) {
this.addLog('agent-hub', 'info', '消息已忽略:没有匹配到支持的意图')
@@ -348,6 +360,50 @@ class AgentHubService {
this.sendHubJson(response, 200, { status: 'ok' })
}
private async replyContactChat(
inbound: InboundMessage,
intent: ContactChatIntent
): Promise<void> {
if (!isReady()) {
await this.sendConnector(inbound, 'WechatExplorer 本地数据库尚未连接,请连接后再试。')
return
}
const contact = resolveMd5(intent.contact)
if (!contact || contact.type !== 'user') {
this.addLog('agent-hub', 'info', `没有匹配到联系人:${intent.contact}`)
await this.sendConnector(inbound, `没有找到联系人“${intent.contact}”。`)
return
}
this.addLog(
'agent-hub',
'info',
`匹配联系人聊天查询:${contact.m_nsNickName}(最近 ${intent.limit} 条)`
)
const messages = listMessages(contact.md5, undefined, undefined, { limit: intent.limit })
const recent = messages.slice(-intent.limit)
const lines = recent.map((message) => {
const speaker = message.isSender ? '我' : contact.m_nsNickName
const content = this.describeChatMessage(message.content, message.type)
return `${speaker}${content}`
})
const reply = lines.length
? `我和${contact.m_nsNickName}最近聊了这些:\n${lines.join('\n')}`
: `暂时没有找到和${contact.m_nsNickName}的聊天记录。`
await this.sendConnector(inbound, reply)
this.addLog('agent-hub', 'info', `联系人聊天回复已发送(${recent.length} 条)`)
}
private describeChatMessage(content: string, type: string): string {
const normalized = String(content || '')
.replace(/\s+/g, ' ')
.trim()
if (normalized) return normalized.length > 100 ? `${normalized.slice(0, 100)}` : normalized
const label = String(type || '消息').replace(/^普通文本$/, '消息')
return `[${label}]`
}
private async generateAndSendReport(
inbound: InboundMessage,
intent: GroupReportIntent
@@ -394,6 +450,24 @@ class AgentHubService {
return Math.max(1, Math.min(20, limit))
}
private matchContactChatIntent(text: string): ContactChatIntent | null {
const normalized = text.replace(/\s+/g, '').replace(/[,。!??:]/g, '')
if (!normalized.includes('最近') || !/(聊|消息|会话)/.test(normalized)) return null
const patterns = [
/(?:看一下|看看|查一下|查询)?我和(.+?)最近(?:\d{1,2}条)?(?:聊了什么|聊什么|的聊天|的消息|聊天|消息)/,
/(?:看一下|看看|查一下|查询)?(?:我)?最近(?:\d{1,2}条)?和(.+?)(?:聊了什么|聊什么|的聊天|的消息|聊天|消息)/,
/(?:看一下|看看|查一下|查询)?和(.+?)最近(?:\d{1,2}条)?(?:聊了什么|聊什么|的聊天|的消息|聊天|消息)/
]
const contact = patterns
.map((pattern) => normalized.match(pattern)?.[1]?.trim())
.find((value): value is string => Boolean(value))
if (!contact) return null
const limit = Number(normalized.match(/最近(\d{1,2})条/)?.[1] || 10)
return { contact, limit: Math.max(1, Math.min(20, limit)) }
}
private matchGroupReportIntent(text: string): GroupReportIntent | null {
const normalized = text.trim()
if (!normalized.includes('群') || !/(总结|日报|报告)/.test(normalized)) return null
@@ -241,7 +241,11 @@ export function AgentHubWorkspace(): React.ReactElement {
<option value="agent-hub">Agent Hub</option>
<option value="wechat-connector"></option>
</select>
<button type="button" onClick={() => void copyLogs()} disabled={visibleLogs.length === 0}>
<button
type="button"
onClick={() => void copyLogs()}
disabled={visibleLogs.length === 0}
>
</button>
<button type="button" onClick={() => void clearLogs()}>
@@ -251,7 +255,9 @@ export function AgentHubWorkspace(): React.ReactElement {
</div>
<div className="agent-hub-log-body" ref={logBodyRef}>
{visibleLogs.length === 0 ? (
<div className="agent-hub-log-empty"></div>
<div className="agent-hub-log-empty">
</div>
) : (
visibleLogs.map((entry) => (
<div className={`agent-hub-log-line ${entry.level}`} key={entry.id}>
+34 -14
View File
@@ -10,12 +10,32 @@ import {
ReportVoiceHighlight,
ReportVoiceLeaderboardItem
} from '../../../shared/group-report'
import type {
ImageAnalysisRequest,
ImageAnalysisResponse,
ImageCandidate,
ImageCandidateQuery
} from '../../../shared/image-insight'
interface ReportImageReadResult {
success: boolean
data?: string
error?: string
}
declare const window: {
api: {
imageListCandidates: (...args: unknown[]) => Promise<any>
imageAnalyze: (...args: unknown[]) => Promise<any>
getImage: (...args: unknown[]) => Promise<any>
imageListCandidates: (query: ImageCandidateQuery) => Promise<{
success: boolean
candidates: ImageCandidate[]
error?: string
}>
imageAnalyze: (request: ImageAnalysisRequest) => Promise<ImageAnalysisResponse>
getImage: (
imageMd5?: string,
imageDatNameOrThumb?: string | boolean,
sessionId?: string
) => Promise<ReportImageReadResult>
}
}
@@ -337,17 +357,17 @@ const buildMediaSection = async (
? await Promise.all(
rawImageCandidates.map(async (item) => {
const result = await rendererApi.getImage(item.md5, item.datName, item.sessionId)
if (!result.success || !result.data?.startsWith('data:image/')) return null
return {
sender: item.sender,
time: item.time,
imageUrl: result.data,
note: item.note,
stats: item.stats,
inferenceLabel: '基于图片后的聊天上下文推断',
sourceMessageIds: item.sourceMessageIds,
replyCount: item.replyCount,
score: item.score
if (!result.success || !result.data?.startsWith('data:image/')) return null
return {
sender: item.sender,
time: item.time,
imageUrl: result.data,
note: item.note,
stats: item.stats,
inferenceLabel: '基于图片后的聊天上下文推断',
sourceMessageIds: item.sourceMessageIds,
replyCount: item.replyCount,
score: item.score
}
})
)