mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 11:37:06 +08:00
feat: 内置微信连接器并完善 Agent 查询能力
This commit is contained in:
@@ -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}>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user