mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-12 15:08:36 +00:00
Merge pull request #21 from Jasonzhu1207/feature/ai-insight-debug-log-cleanup
fix: clean AI insight prompt and debug log formatting
This commit is contained in:
@@ -401,17 +401,17 @@ class InsightService {
|
|||||||
try {
|
try {
|
||||||
const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions')
|
const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions')
|
||||||
const requestMessages = [{ role: 'user', content: '请回复"连接成功"四个字。' }]
|
const requestMessages = [{ role: 'user', content: '请回复"连接成功"四个字。' }]
|
||||||
insightDebugSection('INFO', 'AI 测试连接请求', {
|
insightDebugSection(
|
||||||
endpoint,
|
'INFO',
|
||||||
model,
|
'AI 测试连接请求',
|
||||||
request: {
|
[
|
||||||
model,
|
`Endpoint: ${endpoint}`,
|
||||||
messages: requestMessages,
|
`Model: ${model}`,
|
||||||
max_tokens: API_MAX_TOKENS,
|
'',
|
||||||
temperature: API_TEMPERATURE,
|
'用户提示词:',
|
||||||
stream: false
|
requestMessages[0].content
|
||||||
}
|
].join('\n')
|
||||||
})
|
)
|
||||||
|
|
||||||
const result = await callApi(
|
const result = await callApi(
|
||||||
apiBaseUrl,
|
apiBaseUrl,
|
||||||
@@ -423,10 +423,11 @@ class InsightService {
|
|||||||
insightDebugSection('INFO', 'AI 测试连接输出原文', result)
|
insightDebugSection('INFO', 'AI 测试连接输出原文', result)
|
||||||
return { success: true, message: `连接成功,模型回复:${result.slice(0, 50)}` }
|
return { success: true, message: `连接成功,模型回复:${result.slice(0, 50)}` }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
insightDebugSection('ERROR', 'AI 测试连接失败', {
|
insightDebugSection(
|
||||||
error: (e as Error).message,
|
'ERROR',
|
||||||
stack: (e as Error).stack ?? null
|
'AI 测试连接失败',
|
||||||
})
|
`错误信息:${(e as Error).message}\n\n堆栈:\n${(e as Error).stack || '[无堆栈]'}`
|
||||||
|
)
|
||||||
return { success: false, message: `连接失败:${(e as Error).message}` }
|
return { success: false, message: `连接失败:${(e as Error).message}` }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -604,6 +605,105 @@ ${topMentionText}
|
|||||||
return { apiBaseUrl, apiKey, model }
|
return { apiBaseUrl, apiKey, model }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private looksLikeWxid(text: string): boolean {
|
||||||
|
const normalized = String(text || '').trim()
|
||||||
|
if (!normalized) return false
|
||||||
|
return /^wxid_[a-z0-9]+$/i.test(normalized)
|
||||||
|
|| /^[a-z0-9_]+@chatroom$/i.test(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
private looksLikeXmlPayload(text: string): boolean {
|
||||||
|
const normalized = String(text || '').trim()
|
||||||
|
if (!normalized) return false
|
||||||
|
return /^(<\?xml|<msg\b|<appmsg\b|<img\b|<emoji\b|<voip\b|<sysmsg\b|<\?xml|<msg\b|<appmsg\b)/i.test(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeInsightText(text: string): string {
|
||||||
|
return String(text || '')
|
||||||
|
.replace(/\r\n/g, '\n')
|
||||||
|
.replace(/\u0000/g, '')
|
||||||
|
.replace(/\n{3,}/g, '\n\n')
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatInsightMessageTimestamp(createTime: number): string {
|
||||||
|
const ms = createTime > 1_000_000_000_000 ? createTime : createTime * 1000
|
||||||
|
const date = new Date(ms)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
|
}
|
||||||
|
|
||||||
|
private async resolveInsightSessionDisplayName(sessionId: string, fallbackDisplayName: string): Promise<string> {
|
||||||
|
const fallback = String(fallbackDisplayName || '').trim()
|
||||||
|
if (fallback && !this.looksLikeWxid(fallback)) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sessions = await this.getSessionsCached()
|
||||||
|
const matched = sessions.find((session) => String(session.username || '').trim() === sessionId)
|
||||||
|
const cachedDisplayName = String(matched?.displayName || '').trim()
|
||||||
|
if (cachedDisplayName && !this.looksLikeWxid(cachedDisplayName)) {
|
||||||
|
return cachedDisplayName
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore display name lookup failures
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const contact = await chatService.getContactAvatar(sessionId)
|
||||||
|
const contactDisplayName = String(contact?.displayName || '').trim()
|
||||||
|
if (contactDisplayName && !this.looksLikeWxid(contactDisplayName)) {
|
||||||
|
return contactDisplayName
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore display name lookup failures
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback || sessionId
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatInsightMessageContent(message: Message): string {
|
||||||
|
const parsedContent = this.normalizeInsightText(String(message.parsedContent || ''))
|
||||||
|
const quotedPreview = this.normalizeInsightText(String(message.quotedContent || ''))
|
||||||
|
const quotedSender = this.normalizeInsightText(String(message.quotedSender || ''))
|
||||||
|
|
||||||
|
if (quotedPreview) {
|
||||||
|
const cleanQuotedSender = quotedSender && !this.looksLikeWxid(quotedSender) ? quotedSender : ''
|
||||||
|
const quoteLabel = cleanQuotedSender ? `${cleanQuotedSender}:${quotedPreview}` : quotedPreview
|
||||||
|
const replyText = parsedContent && parsedContent !== '[引用消息]' ? parsedContent : ''
|
||||||
|
return replyText ? `${replyText}[引用 ${quoteLabel}]` : `[引用 ${quoteLabel}]`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedContent) {
|
||||||
|
return parsedContent
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawContent = this.normalizeInsightText(String(message.rawContent || ''))
|
||||||
|
if (rawContent && !this.looksLikeXmlPayload(rawContent)) {
|
||||||
|
return rawContent
|
||||||
|
}
|
||||||
|
|
||||||
|
return '[其他消息]'
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildInsightContextSection(messages: Message[], peerDisplayName: string): string {
|
||||||
|
if (!messages.length) return ''
|
||||||
|
|
||||||
|
const lines = messages.map((message) => {
|
||||||
|
const senderName = message.isSend === 1 ? '我' : peerDisplayName
|
||||||
|
const content = this.formatInsightMessageContent(message)
|
||||||
|
return `${this.formatInsightMessageTimestamp(message.createTime)} '${senderName}'\n${content}`
|
||||||
|
})
|
||||||
|
|
||||||
|
return `近期聊天记录(最近 ${lines.length} 条):\n\n${lines.join('\n\n')}`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断某个会话是否允许触发见解。
|
* 判断某个会话是否允许触发见解。
|
||||||
* 若白名单未启用,则所有私聊会话均允许;
|
* 若白名单未启用,则所有私聊会话均允许;
|
||||||
@@ -899,6 +999,7 @@ ${topMentionText}
|
|||||||
const { apiBaseUrl, apiKey, model } = this.getSharedAiModelConfig()
|
const { apiBaseUrl, apiKey, model } = this.getSharedAiModelConfig()
|
||||||
const allowContext = this.config.get('aiInsightAllowContext') as boolean
|
const allowContext = this.config.get('aiInsightAllowContext') as boolean
|
||||||
const contextCount = (this.config.get('aiInsightContextCount') as number) || 40
|
const contextCount = (this.config.get('aiInsightContextCount') as number) || 40
|
||||||
|
const resolvedDisplayName = await this.resolveInsightSessionDisplayName(sessionId, displayName)
|
||||||
|
|
||||||
insightLog('INFO', `generateInsightForSession: sessionId=${sessionId}, reason=${triggerReason}, contextCount=${contextCount}, api=${apiBaseUrl ? '已配置' : '未配置'}`)
|
insightLog('INFO', `generateInsightForSession: sessionId=${sessionId}, reason=${triggerReason}, contextCount=${contextCount}, api=${apiBaseUrl ? '已配置' : '未配置'}`)
|
||||||
|
|
||||||
@@ -919,14 +1020,8 @@ ${topMentionText}
|
|||||||
const msgsResult = await chatService.getLatestMessages(sessionId, contextCount)
|
const msgsResult = await chatService.getLatestMessages(sessionId, contextCount)
|
||||||
if (msgsResult.success && msgsResult.messages && msgsResult.messages.length > 0) {
|
if (msgsResult.success && msgsResult.messages && msgsResult.messages.length > 0) {
|
||||||
const messages: Message[] = msgsResult.messages
|
const messages: Message[] = msgsResult.messages
|
||||||
const msgLines = messages.map((m) => {
|
contextSection = this.buildInsightContextSection(messages, resolvedDisplayName)
|
||||||
const sender = m.isSend === 1 ? '我' : (displayName || sessionId)
|
insightLog('INFO', `已加载 ${messages.length} 条上下文消息`)
|
||||||
const content = m.rawContent || m.parsedContent || '[非文字消息]'
|
|
||||||
const time = new Date(Number(m.createTime) * 1000).toLocaleString('zh-CN')
|
|
||||||
return `[${time}] ${sender}:${content}`
|
|
||||||
})
|
|
||||||
contextSection = `\n\n近期对话记录(最近 ${msgLines.length} 条):\n${msgLines.join('\n')}`
|
|
||||||
insightLog('INFO', `已加载 ${msgLines.length} 条上下文消息`)
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
insightLog('WARN', `拉取上下文失败: ${(e as Error).message}`)
|
insightLog('WARN', `拉取上下文失败: ${(e as Error).message}`)
|
||||||
@@ -950,20 +1045,23 @@ ${topMentionText}
|
|||||||
// 这样 provider 端(Anthropic/OpenAI)能最大化命中 prompt cache,降低费用
|
// 这样 provider 端(Anthropic/OpenAI)能最大化命中 prompt cache,降低费用
|
||||||
const triggerDesc =
|
const triggerDesc =
|
||||||
triggerReason === 'silence'
|
triggerReason === 'silence'
|
||||||
? `你已经 ${silentDays} 天没有和「${displayName}」聊天了。`
|
? `你已经 ${silentDays} 天没有和「${resolvedDisplayName}」聊天了。`
|
||||||
: `你最近和「${displayName}」有新的聊天动态。`
|
: `你最近和「${resolvedDisplayName}」有新的聊天动态。`
|
||||||
|
|
||||||
const todayStatsDesc =
|
const todayStatsDesc =
|
||||||
sessionTriggerTimes.length > 1
|
sessionTriggerTimes.length > 1
|
||||||
? `今天你已经针对「${displayName}」收到过 ${sessionTriggerTimes.length - 1} 条见解(时间:${sessionTriggerTimes.slice(0, -1).join('、')}),请适当克制。`
|
? `今天你已经针对「${resolvedDisplayName}」收到过 ${sessionTriggerTimes.length - 1} 条见解(时间:${sessionTriggerTimes.slice(0, -1).join('、')}),请适当克制。`
|
||||||
: `今天你还没有针对「${displayName}」发出过见解。`
|
: `今天你还没有针对「${resolvedDisplayName}」发出过见解。`
|
||||||
|
|
||||||
const globalStatsDesc = `今天全部联系人合计已触发 ${totalTodayTriggers} 条见解。`
|
const globalStatsDesc = `今天全部联系人合计已触发 ${totalTodayTriggers} 条见解。`
|
||||||
|
|
||||||
const userPrompt = `触发原因:${triggerDesc}
|
const userPrompt = [
|
||||||
时间统计:${todayStatsDesc} ${globalStatsDesc}${contextSection}
|
`触发原因:${triggerDesc}`,
|
||||||
|
`时间统计:${todayStatsDesc}`,
|
||||||
请给出你的见解(≤80字):`
|
`全局统计:${globalStatsDesc}`,
|
||||||
|
contextSection,
|
||||||
|
'请给出你的见解(≤80字):'
|
||||||
|
].filter(Boolean).join('\n\n')
|
||||||
|
|
||||||
const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions')
|
const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions')
|
||||||
const requestMessages = [
|
const requestMessages = [
|
||||||
@@ -972,23 +1070,23 @@ ${topMentionText}
|
|||||||
]
|
]
|
||||||
|
|
||||||
insightLog('INFO', `准备调用 API: ${endpoint},模型: ${model}`)
|
insightLog('INFO', `准备调用 API: ${endpoint},模型: ${model}`)
|
||||||
insightDebugSection('INFO', `AI 请求 ${displayName} (${sessionId})`, {
|
insightDebugSection(
|
||||||
sessionId,
|
'INFO',
|
||||||
displayName,
|
`AI 请求 ${resolvedDisplayName} (${sessionId})`,
|
||||||
triggerReason,
|
[
|
||||||
silentDays: silentDays ?? null,
|
`接口地址:${endpoint}`,
|
||||||
endpoint,
|
`模型:${model}`,
|
||||||
model,
|
`触发原因:${triggerReason}`,
|
||||||
allowContext,
|
`上下文开关:${allowContext ? '开启' : '关闭'}`,
|
||||||
contextCount,
|
`上下文条数:${contextCount}`,
|
||||||
request: {
|
'',
|
||||||
model,
|
'系统提示词:',
|
||||||
messages: requestMessages,
|
systemPrompt,
|
||||||
max_tokens: API_MAX_TOKENS,
|
'',
|
||||||
temperature: API_TEMPERATURE,
|
'用户提示词:',
|
||||||
stream: false
|
userPrompt
|
||||||
}
|
].join('\n')
|
||||||
})
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await callApi(
|
const result = await callApi(
|
||||||
@@ -999,19 +1097,19 @@ ${topMentionText}
|
|||||||
)
|
)
|
||||||
|
|
||||||
insightLog('INFO', `API 返回原文: ${result.slice(0, 150)}`)
|
insightLog('INFO', `API 返回原文: ${result.slice(0, 150)}`)
|
||||||
insightDebugSection('INFO', `AI 输出原文 ${displayName} (${sessionId})`, result)
|
insightDebugSection('INFO', `AI 输出原文 ${resolvedDisplayName} (${sessionId})`, result)
|
||||||
|
|
||||||
// 模型主动选择跳过
|
// 模型主动选择跳过
|
||||||
if (result.trim().toUpperCase() === 'SKIP' || result.trim().startsWith('SKIP')) {
|
if (result.trim().toUpperCase() === 'SKIP' || result.trim().startsWith('SKIP')) {
|
||||||
insightLog('INFO', `模型选择跳过 ${displayName}`)
|
insightLog('INFO', `模型选择跳过 ${resolvedDisplayName}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!this.isEnabled()) return
|
if (!this.isEnabled()) return
|
||||||
|
|
||||||
const insight = result.slice(0, 120)
|
const insight = result.slice(0, 120)
|
||||||
const notifTitle = `见解 · ${displayName}`
|
const notifTitle = `见解 · ${resolvedDisplayName}`
|
||||||
|
|
||||||
insightLog('INFO', `推送通知 → ${displayName}: ${insight}`)
|
insightLog('INFO', `推送通知 → ${resolvedDisplayName}: ${insight}`)
|
||||||
|
|
||||||
// 渠道一:Electron 原生系统通知
|
// 渠道一:Electron 原生系统通知
|
||||||
if (Notification.isSupported()) {
|
if (Notification.isSupported()) {
|
||||||
@@ -1039,16 +1137,14 @@ ${topMentionText}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
insightLog('INFO', `已为 ${displayName} 推送见解`)
|
insightLog('INFO', `已为 ${resolvedDisplayName} 推送见解`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
insightDebugSection('ERROR', `AI 请求失败 ${displayName} (${sessionId})`, {
|
insightDebugSection(
|
||||||
sessionId,
|
'ERROR',
|
||||||
displayName,
|
`AI 请求失败 ${resolvedDisplayName} (${sessionId})`,
|
||||||
triggerReason,
|
`错误信息:${(e as Error).message}\n\n堆栈:\n${(e as Error).stack || '[无堆栈]'}`
|
||||||
error: (e as Error).message,
|
)
|
||||||
stack: (e as Error).stack ?? null
|
insightLog('ERROR', `API 调用失败 (${resolvedDisplayName}): ${(e as Error).message}`)
|
||||||
})
|
|
||||||
insightLog('ERROR', `API 调用失败 (${displayName}): ${(e as Error).message}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user