diff --git a/electron/services/config.ts b/electron/services/config.ts index 250c93d..5a6b868 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -102,6 +102,8 @@ interface ConfigSchema { // AI 足迹 aiFootprintEnabled: boolean aiFootprintSystemPrompt: string + /** 是否将 AI 见解调试日志输出到桌面 */ + aiInsightDebugLogEnabled: boolean } // 需要 safeStorage 加密的字段(普通模式) @@ -204,7 +206,8 @@ export class ConfigService { aiInsightTelegramToken: '', aiInsightTelegramChatIds: '', aiFootprintEnabled: false, - aiFootprintSystemPrompt: '' + aiFootprintSystemPrompt: '', + aiInsightDebugLogEnabled: false } const storeOptions: any = { diff --git a/electron/services/insightService.ts b/electron/services/insightService.ts index 6890f7a..ff91a32 100644 --- a/electron/services/insightService.ts +++ b/electron/services/insightService.ts @@ -15,8 +15,10 @@ import https from 'https' import http from 'http' +import fs from 'fs' +import path from 'path' import { URL } from 'url' -import { Notification } from 'electron' +import { app, Notification } from 'electron' import { ConfigService } from './config' import { chatService, ChatSession, Message } from './chatService' @@ -33,6 +35,8 @@ const SILENCE_SCAN_INITIAL_DELAY_MS = 3 * 60 * 1000 /** 单次 API 请求超时(毫秒) */ const API_TIMEOUT_MS = 45_000 +const API_MAX_TOKENS = 200 +const API_TEMPERATURE = 0.7 /** 沉默天数阈值默认值 */ const DEFAULT_SILENCE_DAYS = 3 @@ -62,15 +66,74 @@ interface SharedAiModelConfig { // ─── 日志 ───────────────────────────────────────────────────────────────────── +type InsightLogLevel = 'INFO' | 'WARN' | 'ERROR' + +let debugLogWriteQueue: Promise = 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,不落盘到文件。 */ -function insightLog(level: 'INFO' | 'WARN' | 'ERROR', message: string): void { +function insightLog(level: InsightLogLevel, message: string): void { if (level === 'ERROR' || level === 'WARN') { console.warn(`[InsightService] ${message}`) } else { console.log(`[InsightService] ${message}`) } + insightDebugLine(level, message) } // ─── 工具函数 ───────────────────────────────────────────────────────────────── @@ -127,8 +190,8 @@ function callApi( const body = JSON.stringify({ model, messages, - max_tokens: 200, - temperature: 0.7, + max_tokens: API_MAX_TOKENS, + temperature: API_TEMPERATURE, stream: false }) @@ -336,15 +399,34 @@ class InsightService { } try { + const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions') + const requestMessages = [{ role: 'user', content: '请回复"连接成功"四个字。' }] + insightDebugSection('INFO', 'AI 测试连接请求', { + endpoint, + model, + request: { + model, + messages: requestMessages, + max_tokens: API_MAX_TOKENS, + temperature: API_TEMPERATURE, + stream: false + } + }) + const result = await callApi( apiBaseUrl, apiKey, model, - [{ role: 'user', content: '请回复"连接成功"四个字。' }], + requestMessages, 15_000 ) + insightDebugSection('INFO', 'AI 测试连接输出原文', result) return { success: true, message: `连接成功,模型回复:${result.slice(0, 50)}` } } catch (e) { + insightDebugSection('ERROR', 'AI 测试连接失败', { + error: (e as Error).message, + stack: (e as Error).stack ?? null + }) return { success: false, message: `连接失败:${(e as Error).message}` } } } @@ -884,20 +966,40 @@ ${topMentionText} 请给出你的见解(≤80字):` const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions') + const requestMessages = [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userPrompt } + ] + insightLog('INFO', `准备调用 API: ${endpoint},模型: ${model}`) + insightDebugSection('INFO', `AI 请求 ${displayName} (${sessionId})`, { + sessionId, + displayName, + triggerReason, + silentDays: silentDays ?? null, + endpoint, + model, + allowContext, + contextCount, + request: { + model, + messages: requestMessages, + max_tokens: API_MAX_TOKENS, + temperature: API_TEMPERATURE, + stream: false + } + }) try { const result = await callApi( apiBaseUrl, apiKey, model, - [ - { role: 'system', content: systemPrompt }, - { role: 'user', content: userPrompt } - ] + requestMessages ) insightLog('INFO', `API 返回原文: ${result.slice(0, 150)}`) + insightDebugSection('INFO', `AI 输出原文 ${displayName} (${sessionId})`, result) // 模型主动选择跳过 if (result.trim().toUpperCase() === 'SKIP' || result.trim().startsWith('SKIP')) { @@ -939,6 +1041,13 @@ ${topMentionText} insightLog('INFO', `已为 ${displayName} 推送见解`) } catch (e) { + insightDebugSection('ERROR', `AI 请求失败 ${displayName} (${sessionId})`, { + sessionId, + displayName, + triggerReason, + error: (e as Error).message, + stack: (e as Error).stack ?? null + }) insightLog('ERROR', `API 调用失败 (${displayName}): ${(e as Error).message}`) } } diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 48f0ae2..b62f101 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -286,6 +286,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const [aiInsightTelegramChatIds, setAiInsightTelegramChatIds] = useState('') const [aiFootprintEnabled, setAiFootprintEnabled] = useState(false) const [aiFootprintSystemPrompt, setAiFootprintSystemPrompt] = useState('') + const [aiInsightDebugLogEnabled, setAiInsightDebugLogEnabled] = useState(false) // 检查 Hello 可用性 useEffect(() => { @@ -516,35 +517,38 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const savedAiModelApiKey = await configService.getAiModelApiKey() const savedAiModelApiModel = await configService.getAiModelApiModel() const savedAiInsightSilenceDays = await configService.getAiInsightSilenceDays() - const savedAiInsightAllowContext = await configService.getAiInsightAllowContext() - const savedAiInsightWhitelistEnabled = await configService.getAiInsightWhitelistEnabled() - const savedAiInsightWhitelist = await configService.getAiInsightWhitelist() - const savedAiInsightCooldownMinutes = await configService.getAiInsightCooldownMinutes() - const savedAiInsightScanIntervalHours = await configService.getAiInsightScanIntervalHours() - const savedAiInsightContextCount = await configService.getAiInsightContextCount() - const savedAiInsightSystemPrompt = await configService.getAiInsightSystemPrompt() - const savedAiInsightTelegramEnabled = await configService.getAiInsightTelegramEnabled() - const savedAiInsightTelegramToken = await configService.getAiInsightTelegramToken() - const savedAiInsightTelegramChatIds = await configService.getAiInsightTelegramChatIds() - const savedAiFootprintEnabled = await configService.getAiFootprintEnabled() - const savedAiFootprintSystemPrompt = await configService.getAiFootprintSystemPrompt() - setAiInsightEnabled(savedAiInsightEnabled) - setAiModelApiBaseUrl(savedAiModelApiBaseUrl) - setAiModelApiKey(savedAiModelApiKey) - setAiModelApiModel(savedAiModelApiModel) - setAiInsightSilenceDays(savedAiInsightSilenceDays) - setAiInsightAllowContext(savedAiInsightAllowContext) - setAiInsightWhitelistEnabled(savedAiInsightWhitelistEnabled) - setAiInsightWhitelist(new Set(savedAiInsightWhitelist)) - setAiInsightCooldownMinutes(savedAiInsightCooldownMinutes) - setAiInsightScanIntervalHours(savedAiInsightScanIntervalHours) - setAiInsightContextCount(savedAiInsightContextCount) - setAiInsightSystemPrompt(savedAiInsightSystemPrompt) - setAiInsightTelegramEnabled(savedAiInsightTelegramEnabled) - setAiInsightTelegramToken(savedAiInsightTelegramToken) - setAiInsightTelegramChatIds(savedAiInsightTelegramChatIds) - setAiFootprintEnabled(savedAiFootprintEnabled) - setAiFootprintSystemPrompt(savedAiFootprintSystemPrompt) + const savedAiInsightAllowContext = await configService.getAiInsightAllowContext() + const savedAiInsightWhitelistEnabled = await configService.getAiInsightWhitelistEnabled() + const savedAiInsightWhitelist = await configService.getAiInsightWhitelist() + const savedAiInsightCooldownMinutes = await configService.getAiInsightCooldownMinutes() + const savedAiInsightScanIntervalHours = await configService.getAiInsightScanIntervalHours() + const savedAiInsightContextCount = await configService.getAiInsightContextCount() + const savedAiInsightSystemPrompt = await configService.getAiInsightSystemPrompt() + const savedAiInsightTelegramEnabled = await configService.getAiInsightTelegramEnabled() + const savedAiInsightTelegramToken = await configService.getAiInsightTelegramToken() + const savedAiInsightTelegramChatIds = await configService.getAiInsightTelegramChatIds() + const savedAiFootprintEnabled = await configService.getAiFootprintEnabled() + const savedAiFootprintSystemPrompt = await configService.getAiFootprintSystemPrompt() + const savedAiInsightDebugLogEnabled = await configService.getAiInsightDebugLogEnabled() + + setAiInsightEnabled(savedAiInsightEnabled) + setAiModelApiBaseUrl(savedAiModelApiBaseUrl) + setAiModelApiKey(savedAiModelApiKey) + setAiModelApiModel(savedAiModelApiModel) + setAiInsightSilenceDays(savedAiInsightSilenceDays) + setAiInsightAllowContext(savedAiInsightAllowContext) + setAiInsightWhitelistEnabled(savedAiInsightWhitelistEnabled) + setAiInsightWhitelist(new Set(savedAiInsightWhitelist)) + setAiInsightCooldownMinutes(savedAiInsightCooldownMinutes) + setAiInsightScanIntervalHours(savedAiInsightScanIntervalHours) + setAiInsightContextCount(savedAiInsightContextCount) + setAiInsightSystemPrompt(savedAiInsightSystemPrompt) + setAiInsightTelegramEnabled(savedAiInsightTelegramEnabled) + setAiInsightTelegramToken(savedAiInsightTelegramToken) + setAiInsightTelegramChatIds(savedAiInsightTelegramChatIds) + setAiFootprintEnabled(savedAiFootprintEnabled) + setAiFootprintSystemPrompt(savedAiFootprintSystemPrompt) + setAiInsightDebugLogEnabled(savedAiInsightDebugLogEnabled) } catch (e: any) { console.error('加载配置失败:', e) @@ -2722,7 +2726,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { setIsTestingInsight(true) setInsightTestResult(null) try { - const result = await (window.electronAPI as any).insight.testConnection() + const result = await window.electronAPI.insight.testConnection() setInsightTestResult(result) } catch (e: any) { setInsightTestResult({ success: false, message: `调用失败:${e?.message || String(e)}` }) @@ -2883,7 +2887,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { setIsTriggeringInsightTest(true) setInsightTriggerResult(null) try { - const result = await (window.electronAPI as any).insight.triggerTest() + const result = await window.electronAPI.insight.triggerTest() setInsightTriggerResult(result) } catch (e: any) { setInsightTriggerResult({ success: false, message: `调用失败:${e?.message || String(e)}` }) @@ -3340,6 +3344,32 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { + +
+ +
+ + + 开启后,AI 见解链路会额外把完整调试日志写到桌面上的 weflow-ai-insight-debug-YYYY-MM-DD.log。 + 其中会包含发送给 AI 的完整提示词原文、近期对话上下文原文和模型输出原文,但不会记录 API Key。 + +
+ {aiInsightDebugLogEnabled ? '已开启' : '已关闭'} + +
+
) diff --git a/src/services/config.ts b/src/services/config.ts index afbbee4..7081266 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -106,7 +106,8 @@ export const CONFIG_KEYS = { // AI 足迹 AI_FOOTPRINT_ENABLED: 'aiFootprintEnabled', - AI_FOOTPRINT_SYSTEM_PROMPT: 'aiFootprintSystemPrompt' + AI_FOOTPRINT_SYSTEM_PROMPT: 'aiFootprintSystemPrompt', + AI_INSIGHT_DEBUG_LOG_ENABLED: 'aiInsightDebugLogEnabled' } as const export interface WxidConfig { @@ -1803,3 +1804,12 @@ export async function getAiFootprintSystemPrompt(): Promise { export async function setAiFootprintSystemPrompt(prompt: string): Promise { await config.set(CONFIG_KEYS.AI_FOOTPRINT_SYSTEM_PROMPT, prompt) } + +export async function getAiInsightDebugLogEnabled(): Promise { + const value = await config.get(CONFIG_KEYS.AI_INSIGHT_DEBUG_LOG_ENABLED) + return value === true +} + +export async function setAiInsightDebugLogEnabled(enabled: boolean): Promise { + await config.set(CONFIG_KEYS.AI_INSIGHT_DEBUG_LOG_ENABLED, enabled) +}