mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-12 15:08:36 +00:00
Merge pull request #736 from Jasonzhu1207/main
fix:修复ai见解误发送xml原文给ai的问题,并增加debug日志
This commit is contained in:
30
.github/workflows/release.yml
vendored
30
.github/workflows/release.yml
vendored
@@ -31,28 +31,12 @@ jobs:
|
|||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
- name: Ensure mac key helpers are executable
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
for file in \
|
|
||||||
resources/key/macos/universal/xkey_helper \
|
|
||||||
resources/key/macos/universal/image_scan_helper \
|
|
||||||
resources/key/macos/universal/xkey_helper_macos \
|
|
||||||
resources/key/macos/universal/libwx_key.dylib
|
|
||||||
do
|
|
||||||
if [ -f "$file" ]; then
|
|
||||||
chmod +x "$file"
|
|
||||||
ls -l "$file"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Sync version with tag
|
- name: Sync version with tag
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
VERSION=${GITHUB_REF_NAME#v}
|
VERSION=${GITHUB_REF_NAME#v}
|
||||||
echo "Syncing package.json version to $VERSION"
|
echo "Syncing package.json version to $VERSION"
|
||||||
npm version $VERSION --no-git-tag-version --allow-same-version
|
node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.version='$VERSION';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\n')"
|
||||||
|
|
||||||
- name: Build Frontend & Type Check
|
- name: Build Frontend & Type Check
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -114,7 +98,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
VERSION=${GITHUB_REF_NAME#v}
|
VERSION=${GITHUB_REF_NAME#v}
|
||||||
echo "Syncing package.json version to $VERSION"
|
echo "Syncing package.json version to $VERSION"
|
||||||
npm version $VERSION --no-git-tag-version --allow-same-version
|
node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.version='$VERSION';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\n')"
|
||||||
|
|
||||||
- name: Build Frontend & Type Check
|
- name: Build Frontend & Type Check
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -136,7 +120,7 @@ jobs:
|
|||||||
TAG=${GITHUB_REF_NAME}
|
TAG=${GITHUB_REF_NAME}
|
||||||
REPO=${{ github.repository }}
|
REPO=${{ github.repository }}
|
||||||
MINIMUM_VERSION="4.1.7"
|
MINIMUM_VERSION="4.1.7"
|
||||||
gh release download "$TAG" --repo "$REPO" --pattern "latest-linux.yml" --output "/tmp/latest-linux.yml" 2>/dev/null
|
gh release download "$TAG" --repo "$REPO" --pattern "latest-linux.yml" --output "/tmp/latest-linux.yml" 2>/dev/null || true
|
||||||
if [ -f /tmp/latest-linux.yml ] && ! grep -q 'minimumVersion' /tmp/latest-linux.yml; then
|
if [ -f /tmp/latest-linux.yml ] && ! grep -q 'minimumVersion' /tmp/latest-linux.yml; then
|
||||||
echo "minimumVersion: $MINIMUM_VERSION" >> /tmp/latest-linux.yml
|
echo "minimumVersion: $MINIMUM_VERSION" >> /tmp/latest-linux.yml
|
||||||
gh release upload "$TAG" --repo "$REPO" /tmp/latest-linux.yml --clobber
|
gh release upload "$TAG" --repo "$REPO" /tmp/latest-linux.yml --clobber
|
||||||
@@ -165,7 +149,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
VERSION=${GITHUB_REF_NAME#v}
|
VERSION=${GITHUB_REF_NAME#v}
|
||||||
echo "Syncing package.json version to $VERSION"
|
echo "Syncing package.json version to $VERSION"
|
||||||
npm version $VERSION --no-git-tag-version --allow-same-version
|
node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.version='$VERSION';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\n')"
|
||||||
|
|
||||||
- name: Build Frontend & Type Check
|
- name: Build Frontend & Type Check
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -187,7 +171,7 @@ jobs:
|
|||||||
TAG=${GITHUB_REF_NAME}
|
TAG=${GITHUB_REF_NAME}
|
||||||
REPO=${{ github.repository }}
|
REPO=${{ github.repository }}
|
||||||
MINIMUM_VERSION="4.1.7"
|
MINIMUM_VERSION="4.1.7"
|
||||||
gh release download "$TAG" --repo "$REPO" --pattern "latest.yml" --output "/tmp/latest.yml" 2>/dev/null
|
gh release download "$TAG" --repo "$REPO" --pattern "latest.yml" --output "/tmp/latest.yml" 2>/dev/null || true
|
||||||
if [ -f /tmp/latest.yml ] && ! grep -q 'minimumVersion' /tmp/latest.yml; then
|
if [ -f /tmp/latest.yml ] && ! grep -q 'minimumVersion' /tmp/latest.yml; then
|
||||||
echo "minimumVersion: $MINIMUM_VERSION" >> /tmp/latest.yml
|
echo "minimumVersion: $MINIMUM_VERSION" >> /tmp/latest.yml
|
||||||
gh release upload "$TAG" --repo "$REPO" /tmp/latest.yml --clobber
|
gh release upload "$TAG" --repo "$REPO" /tmp/latest.yml --clobber
|
||||||
@@ -216,7 +200,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
VERSION=${GITHUB_REF_NAME#v}
|
VERSION=${GITHUB_REF_NAME#v}
|
||||||
echo "Syncing package.json version to $VERSION"
|
echo "Syncing package.json version to $VERSION"
|
||||||
npm version $VERSION --no-git-tag-version --allow-same-version
|
node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.version='$VERSION';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\n')"
|
||||||
|
|
||||||
- name: Build Frontend & Type Check
|
- name: Build Frontend & Type Check
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -238,7 +222,7 @@ jobs:
|
|||||||
TAG=${GITHUB_REF_NAME}
|
TAG=${GITHUB_REF_NAME}
|
||||||
REPO=${{ github.repository }}
|
REPO=${{ github.repository }}
|
||||||
MINIMUM_VERSION="4.1.7"
|
MINIMUM_VERSION="4.1.7"
|
||||||
gh release download "$TAG" --repo "$REPO" --pattern "latest-arm64.yml" --output "/tmp/latest-arm64.yml" 2>/dev/null
|
gh release download "$TAG" --repo "$REPO" --pattern "latest-arm64.yml" --output "/tmp/latest-arm64.yml" 2>/dev/null || true
|
||||||
if [ -f /tmp/latest-arm64.yml ] && ! grep -q 'minimumVersion' /tmp/latest-arm64.yml; then
|
if [ -f /tmp/latest-arm64.yml ] && ! grep -q 'minimumVersion' /tmp/latest-arm64.yml; then
|
||||||
echo "minimumVersion: $MINIMUM_VERSION" >> /tmp/latest-arm64.yml
|
echo "minimumVersion: $MINIMUM_VERSION" >> /tmp/latest-arm64.yml
|
||||||
gh release upload "$TAG" --repo "$REPO" /tmp/latest-arm64.yml --clobber
|
gh release upload "$TAG" --repo "$REPO" /tmp/latest-arm64.yml --clobber
|
||||||
|
|||||||
@@ -102,6 +102,8 @@ interface ConfigSchema {
|
|||||||
// AI 足迹
|
// AI 足迹
|
||||||
aiFootprintEnabled: boolean
|
aiFootprintEnabled: boolean
|
||||||
aiFootprintSystemPrompt: string
|
aiFootprintSystemPrompt: string
|
||||||
|
/** 是否将 AI 见解调试日志输出到桌面 */
|
||||||
|
aiInsightDebugLogEnabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// 需要 safeStorage 加密的字段(普通模式)
|
// 需要 safeStorage 加密的字段(普通模式)
|
||||||
@@ -204,7 +206,8 @@ export class ConfigService {
|
|||||||
aiInsightTelegramToken: '',
|
aiInsightTelegramToken: '',
|
||||||
aiInsightTelegramChatIds: '',
|
aiInsightTelegramChatIds: '',
|
||||||
aiFootprintEnabled: false,
|
aiFootprintEnabled: false,
|
||||||
aiFootprintSystemPrompt: ''
|
aiFootprintSystemPrompt: '',
|
||||||
|
aiInsightDebugLogEnabled: false
|
||||||
}
|
}
|
||||||
|
|
||||||
const storeOptions: any = {
|
const storeOptions: any = {
|
||||||
|
|||||||
@@ -15,8 +15,10 @@
|
|||||||
|
|
||||||
import https from 'https'
|
import https from 'https'
|
||||||
import http from 'http'
|
import http from 'http'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
import { URL } from 'url'
|
import { URL } from 'url'
|
||||||
import { Notification } from 'electron'
|
import { app, Notification } from 'electron'
|
||||||
import { ConfigService } from './config'
|
import { ConfigService } from './config'
|
||||||
import { chatService, ChatSession, Message } from './chatService'
|
import { chatService, ChatSession, Message } from './chatService'
|
||||||
|
|
||||||
@@ -33,6 +35,8 @@ const SILENCE_SCAN_INITIAL_DELAY_MS = 3 * 60 * 1000
|
|||||||
|
|
||||||
/** 单次 API 请求超时(毫秒) */
|
/** 单次 API 请求超时(毫秒) */
|
||||||
const API_TIMEOUT_MS = 45_000
|
const API_TIMEOUT_MS = 45_000
|
||||||
|
const API_MAX_TOKENS = 200
|
||||||
|
const API_TEMPERATURE = 0.7
|
||||||
|
|
||||||
/** 沉默天数阈值默认值 */
|
/** 沉默天数阈值默认值 */
|
||||||
const DEFAULT_SILENCE_DAYS = 3
|
const DEFAULT_SILENCE_DAYS = 3
|
||||||
@@ -62,15 +66,74 @@ interface SharedAiModelConfig {
|
|||||||
|
|
||||||
// ─── 日志 ─────────────────────────────────────────────────────────────────────
|
// ─── 日志 ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type InsightLogLevel = 'INFO' | 'WARN' | 'ERROR'
|
||||||
|
|
||||||
|
let debugLogWriteQueue: Promise<void> = Promise.resolve()
|
||||||
|
|
||||||
|
function formatDebugTimestamp(date: Date = new Date()): string {
|
||||||
|
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}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInsightDebugLogFilePath(date: Date = new Date()): string {
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
return path.join(app.getPath('desktop'), `weflow-ai-insight-debug-${year}-${month}-${day}.log`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInsightDebugLogEnabled(): boolean {
|
||||||
|
try {
|
||||||
|
return ConfigService.getInstance().get('aiInsightDebugLogEnabled') === true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendInsightDebugText(text: string): void {
|
||||||
|
if (!isInsightDebugLogEnabled()) return
|
||||||
|
|
||||||
|
let logFilePath = ''
|
||||||
|
try {
|
||||||
|
logFilePath = getInsightDebugLogFilePath()
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLogWriteQueue = debugLogWriteQueue
|
||||||
|
.then(() => fs.promises.appendFile(logFilePath, text, 'utf8'))
|
||||||
|
.catch(() => undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
function insightDebugLine(level: InsightLogLevel, message: string): void {
|
||||||
|
appendInsightDebugText(`[${formatDebugTimestamp()}] [${level}] ${message}\n`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function insightDebugSection(level: InsightLogLevel, title: string, payload: unknown): void {
|
||||||
|
const content = typeof payload === 'string'
|
||||||
|
? payload
|
||||||
|
: JSON.stringify(payload, null, 2)
|
||||||
|
|
||||||
|
appendInsightDebugText(
|
||||||
|
`\n========== [${formatDebugTimestamp()}] [${level}] ${title} ==========\n${content}\n========== END ==========\n`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 仅输出到 console,不落盘到文件。
|
* 仅输出到 console,不落盘到文件。
|
||||||
*/
|
*/
|
||||||
function insightLog(level: 'INFO' | 'WARN' | 'ERROR', message: string): void {
|
function insightLog(level: InsightLogLevel, message: string): void {
|
||||||
if (level === 'ERROR' || level === 'WARN') {
|
if (level === 'ERROR' || level === 'WARN') {
|
||||||
console.warn(`[InsightService] ${message}`)
|
console.warn(`[InsightService] ${message}`)
|
||||||
} else {
|
} else {
|
||||||
console.log(`[InsightService] ${message}`)
|
console.log(`[InsightService] ${message}`)
|
||||||
}
|
}
|
||||||
|
insightDebugLine(level, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 工具函数 ─────────────────────────────────────────────────────────────────
|
// ─── 工具函数 ─────────────────────────────────────────────────────────────────
|
||||||
@@ -127,8 +190,8 @@ function callApi(
|
|||||||
const body = JSON.stringify({
|
const body = JSON.stringify({
|
||||||
model,
|
model,
|
||||||
messages,
|
messages,
|
||||||
max_tokens: 200,
|
max_tokens: API_MAX_TOKENS,
|
||||||
temperature: 0.7,
|
temperature: API_TEMPERATURE,
|
||||||
stream: false
|
stream: false
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -336,15 +399,35 @@ class InsightService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions')
|
||||||
|
const requestMessages = [{ role: 'user', content: '请回复"连接成功"四个字。' }]
|
||||||
|
insightDebugSection(
|
||||||
|
'INFO',
|
||||||
|
'AI 测试连接请求',
|
||||||
|
[
|
||||||
|
`Endpoint: ${endpoint}`,
|
||||||
|
`Model: ${model}`,
|
||||||
|
'',
|
||||||
|
'用户提示词:',
|
||||||
|
requestMessages[0].content
|
||||||
|
].join('\n')
|
||||||
|
)
|
||||||
|
|
||||||
const result = await callApi(
|
const result = await callApi(
|
||||||
apiBaseUrl,
|
apiBaseUrl,
|
||||||
apiKey,
|
apiKey,
|
||||||
model,
|
model,
|
||||||
[{ role: 'user', content: '请回复"连接成功"四个字。' }],
|
requestMessages,
|
||||||
15_000
|
15_000
|
||||||
)
|
)
|
||||||
|
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 测试连接失败',
|
||||||
|
`错误信息:${(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}` }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -522,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')}`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断某个会话是否允许触发见解。
|
* 判断某个会话是否允许触发见解。
|
||||||
* 若白名单未启用,则所有私聊会话均允许;
|
* 若白名单未启用,则所有私聊会话均允许;
|
||||||
@@ -817,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 ? '已配置' : '未配置'}`)
|
||||||
|
|
||||||
@@ -837,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}`)
|
||||||
@@ -868,48 +1045,71 @@ ${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 = [
|
||||||
|
{ role: 'system', content: systemPrompt },
|
||||||
|
{ role: 'user', content: userPrompt }
|
||||||
|
]
|
||||||
|
|
||||||
insightLog('INFO', `准备调用 API: ${endpoint},模型: ${model}`)
|
insightLog('INFO', `准备调用 API: ${endpoint},模型: ${model}`)
|
||||||
|
insightDebugSection(
|
||||||
|
'INFO',
|
||||||
|
`AI 请求 ${resolvedDisplayName} (${sessionId})`,
|
||||||
|
[
|
||||||
|
`接口地址:${endpoint}`,
|
||||||
|
`模型:${model}`,
|
||||||
|
`触发原因:${triggerReason}`,
|
||||||
|
`上下文开关:${allowContext ? '开启' : '关闭'}`,
|
||||||
|
`上下文条数:${contextCount}`,
|
||||||
|
'',
|
||||||
|
'系统提示词:',
|
||||||
|
systemPrompt,
|
||||||
|
'',
|
||||||
|
'用户提示词:',
|
||||||
|
userPrompt
|
||||||
|
].join('\n')
|
||||||
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await callApi(
|
const result = await callApi(
|
||||||
apiBaseUrl,
|
apiBaseUrl,
|
||||||
apiKey,
|
apiKey,
|
||||||
model,
|
model,
|
||||||
[
|
requestMessages
|
||||||
{ role: 'system', content: systemPrompt },
|
|
||||||
{ role: 'user', content: userPrompt }
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
insightLog('INFO', `API 返回原文: ${result.slice(0, 150)}`)
|
insightLog('INFO', `API 返回原文: ${result.slice(0, 150)}`)
|
||||||
|
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()) {
|
||||||
@@ -937,9 +1137,14 @@ ${topMentionText}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
insightLog('INFO', `已为 ${displayName} 推送见解`)
|
insightLog('INFO', `已为 ${resolvedDisplayName} 推送见解`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
insightLog('ERROR', `API 调用失败 (${displayName}): ${(e as Error).message}`)
|
insightDebugSection(
|
||||||
|
'ERROR',
|
||||||
|
`AI 请求失败 ${resolvedDisplayName} (${sessionId})`,
|
||||||
|
`错误信息:${(e as Error).message}\n\n堆栈:\n${(e as Error).stack || '[无堆栈]'}`
|
||||||
|
)
|
||||||
|
insightLog('ERROR', `API 调用失败 (${resolvedDisplayName}): ${(e as Error).message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
32
package.json
32
package.json
@@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/hicccc77/WeFlow"
|
"url": "https://github.com/Jasonzhu1207/WeFlow"
|
||||||
},
|
},
|
||||||
"//": "二改不应改变此处的作者与应用信息",
|
"//": "二改不应改变此处的作者与应用信息",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -23,7 +23,6 @@
|
|||||||
"electron:build": "npm run build"
|
"electron:build": "npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vscode/sudo-prompt": "^9.3.2",
|
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"echarts-for-react": "^3.0.2",
|
"echarts-for-react": "^3.0.2",
|
||||||
"electron-store": "^11.0.2",
|
"electron-store": "^11.0.2",
|
||||||
@@ -42,8 +41,9 @@
|
|||||||
"react-router-dom": "^7.14.0",
|
"react-router-dom": "^7.14.0",
|
||||||
"react-virtuoso": "^4.18.1",
|
"react-virtuoso": "^4.18.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"sherpa-onnx-node": "^1.12.35",
|
"sherpa-onnx-node": "^1.10.38",
|
||||||
"silk-wasm": "^3.7.1",
|
"silk-wasm": "^3.7.1",
|
||||||
|
"sudo-prompt": "^9.2.1",
|
||||||
"wechat-emojis": "^1.0.2",
|
"wechat-emojis": "^1.0.2",
|
||||||
"zustand": "^5.0.2"
|
"zustand": "^5.0.2"
|
||||||
},
|
},
|
||||||
@@ -54,11 +54,11 @@
|
|||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"electron": "^41.1.1",
|
"electron": "^41.1.1",
|
||||||
"electron-builder": "^26.8.1",
|
"electron-builder": "^26.8.1",
|
||||||
"sass": "^1.99.0",
|
"sass": "^1.98.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"typescript": "^6.0.2",
|
"typescript": "^6.0.2",
|
||||||
"vite": "^7.3.2",
|
"vite": "^7.0.0",
|
||||||
"vite-plugin-electron": "^0.29.1",
|
"vite-plugin-electron": "^0.28.8",
|
||||||
"vite-plugin-electron-renderer": "^0.14.6"
|
"vite-plugin-electron-renderer": "^0.14.6"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
@@ -70,16 +70,14 @@
|
|||||||
"lodash": ">=4.17.21",
|
"lodash": ">=4.17.21",
|
||||||
"brace-expansion": ">=1.1.11",
|
"brace-expansion": ">=1.1.11",
|
||||||
"picomatch": ">=2.3.1",
|
"picomatch": ">=2.3.1",
|
||||||
"ajv": ">=8.18.0",
|
"ajv": ">=8.18.0"
|
||||||
"ajv-keywords@3>ajv": "^6.12.6",
|
|
||||||
"@develar/schema-utils>ajv": "^6.12.6"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.WeFlow.app",
|
"appId": "com.WeFlow.app",
|
||||||
"publish": {
|
"publish": {
|
||||||
"provider": "github",
|
"provider": "github",
|
||||||
"owner": "hicccc77",
|
"owner": "Jasonzhu1207",
|
||||||
"repo": "WeFlow",
|
"repo": "WeFlow",
|
||||||
"releaseType": "release"
|
"releaseType": "release"
|
||||||
},
|
},
|
||||||
@@ -98,7 +96,7 @@
|
|||||||
"gatekeeperAssess": false,
|
"gatekeeperAssess": false,
|
||||||
"entitlements": "electron/entitlements.mac.plist",
|
"entitlements": "electron/entitlements.mac.plist",
|
||||||
"entitlementsInherit": "electron/entitlements.mac.plist",
|
"entitlementsInherit": "electron/entitlements.mac.plist",
|
||||||
"icon": "resources/icons/macos/icon.icns"
|
"icon": "resources/icon.icns"
|
||||||
},
|
},
|
||||||
"win": {
|
"win": {
|
||||||
"target": [
|
"target": [
|
||||||
@@ -107,19 +105,19 @@
|
|||||||
"icon": "public/icon.ico",
|
"icon": "public/icon.ico",
|
||||||
"extraFiles": [
|
"extraFiles": [
|
||||||
{
|
{
|
||||||
"from": "resources/runtime/win32/msvcp140.dll",
|
"from": "resources/msvcp140.dll",
|
||||||
"to": "."
|
"to": "."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "resources/runtime/win32/msvcp140_1.dll",
|
"from": "resources/msvcp140_1.dll",
|
||||||
"to": "."
|
"to": "."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "resources/runtime/win32/vcruntime140.dll",
|
"from": "resources/vcruntime140.dll",
|
||||||
"to": "."
|
"to": "."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "resources/runtime/win32/vcruntime140_1.dll",
|
"from": "resources/vcruntime140_1.dll",
|
||||||
"to": "."
|
"to": "."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -135,7 +133,7 @@
|
|||||||
"synopsis": "WeFlow for Linux",
|
"synopsis": "WeFlow for Linux",
|
||||||
"extraFiles": [
|
"extraFiles": [
|
||||||
{
|
{
|
||||||
"from": "resources/installer/linux/install.sh",
|
"from": "resources/linux/install.sh",
|
||||||
"to": "install.sh"
|
"to": "install.sh"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -190,7 +188,7 @@
|
|||||||
"node_modules/sherpa-onnx-*/**/*",
|
"node_modules/sherpa-onnx-*/**/*",
|
||||||
"node_modules/ffmpeg-static/**/*"
|
"node_modules/ffmpeg-static/**/*"
|
||||||
],
|
],
|
||||||
"icon": "resources/icons/macos/icon.icns"
|
"icon": "resources/icon.icns"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.4",
|
||||||
|
|||||||
@@ -286,6 +286,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
const [aiInsightTelegramChatIds, setAiInsightTelegramChatIds] = useState('')
|
const [aiInsightTelegramChatIds, setAiInsightTelegramChatIds] = useState('')
|
||||||
const [aiFootprintEnabled, setAiFootprintEnabled] = useState(false)
|
const [aiFootprintEnabled, setAiFootprintEnabled] = useState(false)
|
||||||
const [aiFootprintSystemPrompt, setAiFootprintSystemPrompt] = useState('')
|
const [aiFootprintSystemPrompt, setAiFootprintSystemPrompt] = useState('')
|
||||||
|
const [aiInsightDebugLogEnabled, setAiInsightDebugLogEnabled] = useState(false)
|
||||||
|
|
||||||
// 检查 Hello 可用性
|
// 检查 Hello 可用性
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -528,6 +529,8 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
const savedAiInsightTelegramChatIds = await configService.getAiInsightTelegramChatIds()
|
const savedAiInsightTelegramChatIds = await configService.getAiInsightTelegramChatIds()
|
||||||
const savedAiFootprintEnabled = await configService.getAiFootprintEnabled()
|
const savedAiFootprintEnabled = await configService.getAiFootprintEnabled()
|
||||||
const savedAiFootprintSystemPrompt = await configService.getAiFootprintSystemPrompt()
|
const savedAiFootprintSystemPrompt = await configService.getAiFootprintSystemPrompt()
|
||||||
|
const savedAiInsightDebugLogEnabled = await configService.getAiInsightDebugLogEnabled()
|
||||||
|
|
||||||
setAiInsightEnabled(savedAiInsightEnabled)
|
setAiInsightEnabled(savedAiInsightEnabled)
|
||||||
setAiModelApiBaseUrl(savedAiModelApiBaseUrl)
|
setAiModelApiBaseUrl(savedAiModelApiBaseUrl)
|
||||||
setAiModelApiKey(savedAiModelApiKey)
|
setAiModelApiKey(savedAiModelApiKey)
|
||||||
@@ -545,6 +548,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
setAiInsightTelegramChatIds(savedAiInsightTelegramChatIds)
|
setAiInsightTelegramChatIds(savedAiInsightTelegramChatIds)
|
||||||
setAiFootprintEnabled(savedAiFootprintEnabled)
|
setAiFootprintEnabled(savedAiFootprintEnabled)
|
||||||
setAiFootprintSystemPrompt(savedAiFootprintSystemPrompt)
|
setAiFootprintSystemPrompt(savedAiFootprintSystemPrompt)
|
||||||
|
setAiInsightDebugLogEnabled(savedAiInsightDebugLogEnabled)
|
||||||
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('加载配置失败:', e)
|
console.error('加载配置失败:', e)
|
||||||
@@ -2722,7 +2726,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
setIsTestingInsight(true)
|
setIsTestingInsight(true)
|
||||||
setInsightTestResult(null)
|
setInsightTestResult(null)
|
||||||
try {
|
try {
|
||||||
const result = await (window.electronAPI as any).insight.testConnection()
|
const result = await window.electronAPI.insight.testConnection()
|
||||||
setInsightTestResult(result)
|
setInsightTestResult(result)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setInsightTestResult({ success: false, message: `调用失败:${e?.message || String(e)}` })
|
setInsightTestResult({ success: false, message: `调用失败:${e?.message || String(e)}` })
|
||||||
@@ -2883,7 +2887,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
setIsTriggeringInsightTest(true)
|
setIsTriggeringInsightTest(true)
|
||||||
setInsightTriggerResult(null)
|
setInsightTriggerResult(null)
|
||||||
try {
|
try {
|
||||||
const result = await (window.electronAPI as any).insight.triggerTest()
|
const result = await window.electronAPI.insight.triggerTest()
|
||||||
setInsightTriggerResult(result)
|
setInsightTriggerResult(result)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setInsightTriggerResult({ success: false, message: `调用失败:${e?.message || String(e)}` })
|
setInsightTriggerResult({ success: false, message: `调用失败:${e?.message || String(e)}` })
|
||||||
@@ -3340,6 +3344,32 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="divider" />
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label>调试日志导出</label>
|
||||||
|
<span className="form-hint">
|
||||||
|
开启后,AI 见解链路会额外把完整调试日志写到桌面上的 <code>weflow-ai-insight-debug-YYYY-MM-DD.log</code>。
|
||||||
|
其中会包含发送给 AI 的完整提示词原文、近期对话上下文原文和模型输出原文,但不会记录 API Key。
|
||||||
|
</span>
|
||||||
|
<div className="log-toggle-line">
|
||||||
|
<span className="log-status">{aiInsightDebugLogEnabled ? '已开启' : '已关闭'}</span>
|
||||||
|
<label className="switch">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={aiInsightDebugLogEnabled}
|
||||||
|
onChange={async (e) => {
|
||||||
|
const val = e.target.checked
|
||||||
|
setAiInsightDebugLogEnabled(val)
|
||||||
|
await configService.setAiInsightDebugLogEnabled(val)
|
||||||
|
showMessage(val ? '已开启 AI 见解调试日志,后续日志将写入桌面' : '已关闭 AI 见解调试日志', true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="switch-slider" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,8 @@ export const CONFIG_KEYS = {
|
|||||||
|
|
||||||
// AI 足迹
|
// AI 足迹
|
||||||
AI_FOOTPRINT_ENABLED: 'aiFootprintEnabled',
|
AI_FOOTPRINT_ENABLED: 'aiFootprintEnabled',
|
||||||
AI_FOOTPRINT_SYSTEM_PROMPT: 'aiFootprintSystemPrompt'
|
AI_FOOTPRINT_SYSTEM_PROMPT: 'aiFootprintSystemPrompt',
|
||||||
|
AI_INSIGHT_DEBUG_LOG_ENABLED: 'aiInsightDebugLogEnabled'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export interface WxidConfig {
|
export interface WxidConfig {
|
||||||
@@ -1803,3 +1804,12 @@ export async function getAiFootprintSystemPrompt(): Promise<string> {
|
|||||||
export async function setAiFootprintSystemPrompt(prompt: string): Promise<void> {
|
export async function setAiFootprintSystemPrompt(prompt: string): Promise<void> {
|
||||||
await config.set(CONFIG_KEYS.AI_FOOTPRINT_SYSTEM_PROMPT, prompt)
|
await config.set(CONFIG_KEYS.AI_FOOTPRINT_SYSTEM_PROMPT, prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAiInsightDebugLogEnabled(): Promise<boolean> {
|
||||||
|
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_DEBUG_LOG_ENABLED)
|
||||||
|
return value === true
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setAiInsightDebugLogEnabled(enabled: boolean): Promise<void> {
|
||||||
|
await config.set(CONFIG_KEYS.AI_INSIGHT_DEBUG_LOG_ENABLED, enabled)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user