From 8a3f1078f6931452e41a7c80980c1b6b8f21530f Mon Sep 17 00:00:00 2001 From: Jason <159670257+Jasonzhu1207@users.noreply.github.com> Date: Fri, 10 Apr 2026 22:56:41 +0800 Subject: [PATCH 1/4] Add files via upload --- .github/workflows/release.yml | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 44cf1bb..cc26d08 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,28 +31,12 @@ jobs: - name: Install Dependencies 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 shell: bash run: | VERSION=${GITHUB_REF_NAME#v} 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 shell: bash @@ -109,7 +93,7 @@ jobs: run: | VERSION=${GITHUB_REF_NAME#v} 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 shell: bash @@ -131,7 +115,7 @@ jobs: TAG=${GITHUB_REF_NAME} REPO=${{ github.repository }} 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 echo "minimumVersion: $MINIMUM_VERSION" >> /tmp/latest-linux.yml gh release upload "$TAG" --repo "$REPO" /tmp/latest-linux.yml --clobber @@ -160,7 +144,7 @@ jobs: run: | VERSION=${GITHUB_REF_NAME#v} 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 shell: bash @@ -182,7 +166,7 @@ jobs: TAG=${GITHUB_REF_NAME} REPO=${{ github.repository }} 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 echo "minimumVersion: $MINIMUM_VERSION" >> /tmp/latest.yml gh release upload "$TAG" --repo "$REPO" /tmp/latest.yml --clobber @@ -211,7 +195,7 @@ jobs: run: | VERSION=${GITHUB_REF_NAME#v} 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 shell: bash @@ -233,7 +217,7 @@ jobs: TAG=${GITHUB_REF_NAME} REPO=${{ github.repository }} 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 echo "minimumVersion: $MINIMUM_VERSION" >> /tmp/latest-arm64.yml gh release upload "$TAG" --repo "$REPO" /tmp/latest-arm64.yml --clobber From a26d5620ca2df0aec6b60cf1a31f189f8410f311 Mon Sep 17 00:00:00 2001 From: Jason <159670257+Jasonzhu1207@users.noreply.github.com> Date: Fri, 10 Apr 2026 22:57:15 +0800 Subject: [PATCH 2/4] Add files via upload --- package.json | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 0f05abe..2aac96c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/hicccc77/WeFlow" + "url": "https://github.com/Jasonzhu1207/WeFlow" }, "//": "二改不应改变此处的作者与应用信息", "scripts": { @@ -23,7 +23,6 @@ "electron:build": "npm run build" }, "dependencies": { - "@vscode/sudo-prompt": "^9.3.2", "echarts": "^6.0.0", "echarts-for-react": "^3.0.2", "electron-store": "^11.0.2", @@ -42,8 +41,9 @@ "react-router-dom": "^7.14.0", "react-virtuoso": "^4.18.1", "remark-gfm": "^4.0.1", - "sherpa-onnx-node": "^1.12.35", + "sherpa-onnx-node": "^1.10.38", "silk-wasm": "^3.7.1", + "sudo-prompt": "^9.2.1", "wechat-emojis": "^1.0.2", "zustand": "^5.0.2" }, @@ -54,11 +54,11 @@ "@vitejs/plugin-react": "^4.3.4", "electron": "^41.1.1", "electron-builder": "^26.8.1", - "sass": "^1.99.0", + "sass": "^1.98.0", "sharp": "^0.34.5", "typescript": "^6.0.2", - "vite": "^7.3.2", - "vite-plugin-electron": "^0.29.1", + "vite": "^7.0.0", + "vite-plugin-electron": "^0.28.8", "vite-plugin-electron-renderer": "^0.14.6" }, "pnpm": { @@ -70,16 +70,14 @@ "lodash": ">=4.17.21", "brace-expansion": ">=1.1.11", "picomatch": ">=2.3.1", - "ajv": ">=8.18.0", - "ajv-keywords@3>ajv": "^6.12.6", - "@develar/schema-utils>ajv": "^6.12.6" + "ajv": ">=8.18.0" } }, "build": { "appId": "com.WeFlow.app", "publish": { "provider": "github", - "owner": "hicccc77", + "owner": "Jasonzhu1207", "repo": "WeFlow", "releaseType": "release" }, @@ -98,7 +96,7 @@ "gatekeeperAssess": false, "entitlements": "electron/entitlements.mac.plist", "entitlementsInherit": "electron/entitlements.mac.plist", - "icon": "resources/icons/macos/icon.icns" + "icon": "resources/icon.icns" }, "win": { "target": [ @@ -107,19 +105,19 @@ "icon": "public/icon.ico", "extraFiles": [ { - "from": "resources/runtime/win32/msvcp140.dll", + "from": "resources/msvcp140.dll", "to": "." }, { - "from": "resources/runtime/win32/msvcp140_1.dll", + "from": "resources/msvcp140_1.dll", "to": "." }, { - "from": "resources/runtime/win32/vcruntime140.dll", + "from": "resources/vcruntime140.dll", "to": "." }, { - "from": "resources/runtime/win32/vcruntime140_1.dll", + "from": "resources/vcruntime140_1.dll", "to": "." } ] @@ -135,7 +133,7 @@ "synopsis": "WeFlow for Linux", "extraFiles": [ { - "from": "resources/installer/linux/install.sh", + "from": "resources/linux/install.sh", "to": "install.sh" } ] @@ -190,7 +188,7 @@ "node_modules/sherpa-onnx-*/**/*", "node_modules/ffmpeg-static/**/*" ], - "icon": "resources/icons/macos/icon.icns" + "icon": "resources/icon.icns" }, "overrides": { "picomatch": "^4.0.4", From a734cedac119a8b5454c1884871707e0353bea9d Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 12 Apr 2026 15:45:43 +0800 Subject: [PATCH 3/4] feat: add AI insight debug log export toggle --- electron/services/config.ts | 5 +- electron/services/insightService.ts | 127 ++++++++++++++++++++++++++-- src/pages/SettingsPage.tsx | 92 +++++++++++++------- src/services/config.ts | 12 ++- 4 files changed, 194 insertions(+), 42 deletions(-) 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) +} From f3bb548626692c2833786941688ed04c7c0c9d71 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 12 Apr 2026 16:24:29 +0800 Subject: [PATCH 4/4] fix: clean AI insight prompt and debug log formatting --- electron/services/insightService.ts | 218 ++++++++++++++++++++-------- 1 file changed, 157 insertions(+), 61 deletions(-) diff --git a/electron/services/insightService.ts b/electron/services/insightService.ts index ff91a32..911af51 100644 --- a/electron/services/insightService.ts +++ b/electron/services/insightService.ts @@ -401,17 +401,17 @@ 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 - } - }) + insightDebugSection( + 'INFO', + 'AI 测试连接请求', + [ + `Endpoint: ${endpoint}`, + `Model: ${model}`, + '', + '用户提示词:', + requestMessages[0].content + ].join('\n') + ) const result = await callApi( apiBaseUrl, @@ -423,10 +423,11 @@ class InsightService { 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 - }) + insightDebugSection( + 'ERROR', + 'AI 测试连接失败', + `错误信息:${(e as Error).message}\n\n堆栈:\n${(e as Error).stack || '[无堆栈]'}` + ) return { success: false, message: `连接失败:${(e as Error).message}` } } } @@ -604,6 +605,105 @@ ${topMentionText} 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| 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 { + 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 allowContext = this.config.get('aiInsightAllowContext') as boolean 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 ? '已配置' : '未配置'}`) @@ -919,14 +1020,8 @@ ${topMentionText} const msgsResult = await chatService.getLatestMessages(sessionId, contextCount) if (msgsResult.success && msgsResult.messages && msgsResult.messages.length > 0) { const messages: Message[] = msgsResult.messages - const msgLines = messages.map((m) => { - const sender = m.isSend === 1 ? '我' : (displayName || sessionId) - 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} 条上下文消息`) + contextSection = this.buildInsightContextSection(messages, resolvedDisplayName) + insightLog('INFO', `已加载 ${messages.length} 条上下文消息`) } } catch (e) { insightLog('WARN', `拉取上下文失败: ${(e as Error).message}`) @@ -950,20 +1045,23 @@ ${topMentionText} // 这样 provider 端(Anthropic/OpenAI)能最大化命中 prompt cache,降低费用 const triggerDesc = triggerReason === 'silence' - ? `你已经 ${silentDays} 天没有和「${displayName}」聊天了。` - : `你最近和「${displayName}」有新的聊天动态。` + ? `你已经 ${silentDays} 天没有和「${resolvedDisplayName}」聊天了。` + : `你最近和「${resolvedDisplayName}」有新的聊天动态。` const todayStatsDesc = sessionTriggerTimes.length > 1 - ? `今天你已经针对「${displayName}」收到过 ${sessionTriggerTimes.length - 1} 条见解(时间:${sessionTriggerTimes.slice(0, -1).join('、')}),请适当克制。` - : `今天你还没有针对「${displayName}」发出过见解。` + ? `今天你已经针对「${resolvedDisplayName}」收到过 ${sessionTriggerTimes.length - 1} 条见解(时间:${sessionTriggerTimes.slice(0, -1).join('、')}),请适当克制。` + : `今天你还没有针对「${resolvedDisplayName}」发出过见解。` const globalStatsDesc = `今天全部联系人合计已触发 ${totalTodayTriggers} 条见解。` - const userPrompt = `触发原因:${triggerDesc} -时间统计:${todayStatsDesc} ${globalStatsDesc}${contextSection} - -请给出你的见解(≤80字):` + const userPrompt = [ + `触发原因:${triggerDesc}`, + `时间统计:${todayStatsDesc}`, + `全局统计:${globalStatsDesc}`, + contextSection, + '请给出你的见解(≤80字):' + ].filter(Boolean).join('\n\n') const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions') const requestMessages = [ @@ -972,23 +1070,23 @@ ${topMentionText} ] 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 - } - }) + insightDebugSection( + 'INFO', + `AI 请求 ${resolvedDisplayName} (${sessionId})`, + [ + `接口地址:${endpoint}`, + `模型:${model}`, + `触发原因:${triggerReason}`, + `上下文开关:${allowContext ? '开启' : '关闭'}`, + `上下文条数:${contextCount}`, + '', + '系统提示词:', + systemPrompt, + '', + '用户提示词:', + userPrompt + ].join('\n') + ) try { const result = await callApi( @@ -999,19 +1097,19 @@ ${topMentionText} ) 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')) { - insightLog('INFO', `模型选择跳过 ${displayName}`) + insightLog('INFO', `模型选择跳过 ${resolvedDisplayName}`) return } if (!this.isEnabled()) return const insight = result.slice(0, 120) - const notifTitle = `见解 · ${displayName}` + const notifTitle = `见解 · ${resolvedDisplayName}` - insightLog('INFO', `推送通知 → ${displayName}: ${insight}`) + insightLog('INFO', `推送通知 → ${resolvedDisplayName}: ${insight}`) // 渠道一:Electron 原生系统通知 if (Notification.isSupported()) { @@ -1039,16 +1137,14 @@ ${topMentionText} } } - insightLog('INFO', `已为 ${displayName} 推送见解`) + insightLog('INFO', `已为 ${resolvedDisplayName} 推送见解`) } 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}`) + insightDebugSection( + 'ERROR', + `AI 请求失败 ${resolvedDisplayName} (${sessionId})`, + `错误信息:${(e as Error).message}\n\n堆栈:\n${(e as Error).stack || '[无堆栈]'}` + ) + insightLog('ERROR', `API 调用失败 (${resolvedDisplayName}): ${(e as Error).message}`) } }