feat: implement AI insights service and settings tab

Add core insight service and IPC handlers; update config and settings page.

Co-authored-by: Jason <159670257+Jasonzhu1207@users.noreply.github.com>
This commit is contained in:
v0
2026-04-05 15:32:22 +00:00
parent 209b91bfef
commit 93a9df48f4
6 changed files with 837 additions and 6 deletions

View File

@@ -10,12 +10,13 @@ import {
Eye, EyeOff, FolderSearch, FolderOpen, Search, Copy,
RotateCcw, Trash2, Plug, Check, Sun, Moon, Monitor,
Palette, Database, HardDrive, Info, RefreshCw, ChevronDown, Download, Mic,
ShieldCheck, Fingerprint, Lock, KeyRound, Bell, Globe, BarChart2, X, UserRound
ShieldCheck, Fingerprint, Lock, KeyRound, Bell, Globe, BarChart2, X, UserRound,
Sparkles, Loader2, CheckCircle2, XCircle
} from 'lucide-react'
import { Avatar } from '../components/Avatar'
import './SettingsPage.scss'
type SettingsTab = 'appearance' | 'notification' | 'antiRevoke' | 'database' | 'models' | 'cache' | 'api' | 'updates' | 'security' | 'about' | 'analytics'
type SettingsTab = 'appearance' | 'notification' | 'antiRevoke' | 'database' | 'models' | 'cache' | 'api' | 'updates' | 'security' | 'about' | 'analytics' | 'insight'
const tabs: { id: SettingsTab; label: string; icon: React.ElementType }[] = [
{ id: 'appearance', label: '外观', icon: Palette },
@@ -26,6 +27,7 @@ const tabs: { id: SettingsTab; label: string; icon: React.ElementType }[] = [
{ id: 'cache', label: '缓存', icon: HardDrive },
{ id: 'api', label: 'API 服务', icon: Globe },
{ id: 'analytics', label: '分析', icon: BarChart2 },
{ id: 'insight', label: 'AI 见解', icon: Sparkles },
{ id: 'security', label: '安全', icon: ShieldCheck },
{ id: 'updates', label: '版本更新', icon: RefreshCw },
{ id: 'about', label: '关于', icon: Info }
@@ -213,6 +215,17 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
const isClearingCache = isClearingAnalyticsCache || isClearingImageCache || isClearingAllCache
// AI 见解 state
const [aiInsightEnabled, setAiInsightEnabled] = useState(false)
const [aiInsightApiBaseUrl, setAiInsightApiBaseUrl] = useState('')
const [aiInsightApiKey, setAiInsightApiKey] = useState('')
const [aiInsightApiModel, setAiInsightApiModel] = useState('gpt-4o-mini')
const [aiInsightSilenceDays, setAiInsightSilenceDays] = useState(3)
const [aiInsightAllowContext, setAiInsightAllowContext] = useState(false)
const [isTestingInsight, setIsTestingInsight] = useState(false)
const [insightTestResult, setInsightTestResult] = useState<{ success: boolean; message: string } | null>(null)
const [showInsightApiKey, setShowInsightApiKey] = useState(false)
const [isWayland, setIsWayland] = useState(false)
useEffect(() => {
const checkWaylandStatus = async () => {
@@ -438,6 +451,19 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
if (savedWhisperModelDir) setWhisperModelDir(savedWhisperModelDir)
// 加载 AI 见解配置
const savedAiInsightEnabled = await configService.getAiInsightEnabled()
const savedAiInsightApiBaseUrl = await configService.getAiInsightApiBaseUrl()
const savedAiInsightApiKey = await configService.getAiInsightApiKey()
const savedAiInsightApiModel = await configService.getAiInsightApiModel()
const savedAiInsightSilenceDays = await configService.getAiInsightSilenceDays()
const savedAiInsightAllowContext = await configService.getAiInsightAllowContext()
setAiInsightEnabled(savedAiInsightEnabled)
setAiInsightApiBaseUrl(savedAiInsightApiBaseUrl)
setAiInsightApiKey(savedAiInsightApiKey)
setAiInsightApiModel(savedAiInsightApiModel)
setAiInsightSilenceDays(savedAiInsightSilenceDays)
setAiInsightAllowContext(savedAiInsightAllowContext)
} catch (e: any) {
console.error('加载配置失败:', e)
@@ -579,7 +605,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
showMessage(`已切换到${channelLabel}更新渠道,正在检查更新`, true)
await handleCheckUpdate()
} catch (e: any) {
showMessage(`切换更新渠道败: ${e}`, false)
showMessage(`切换更新渠道<EFBFBD><EFBFBD>败: ${e}`, false)
}
}
@@ -2451,6 +2477,222 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
showMessage(enabled ? '已开启主动推送' : '已关闭主动推送', true)
}
const handleTestInsightConnection = async () => {
setIsTestingInsight(true)
setInsightTestResult(null)
try {
const result = await (window.electronAPI as any).insight.testConnection()
setInsightTestResult(result)
} catch (e: any) {
setInsightTestResult({ success: false, message: `调用失败:${e?.message || String(e)}` })
} finally {
setIsTestingInsight(false)
}
}
const renderInsightTab = () => (
<div className="tab-content">
{/* 总开关 */}
<div className="form-group">
<label>AI </label>
<span className="form-hint">
AI
</span>
<div className="log-toggle-line">
<span className="log-status">{aiInsightEnabled ? '已开启' : '已关闭'}</span>
<label className="switch">
<input
type="checkbox"
checked={aiInsightEnabled}
onChange={async (e) => {
const val = e.target.checked
setAiInsightEnabled(val)
await configService.setAiInsightEnabled(val)
showMessage(val ? 'AI 见解已开启' : 'AI 见解已关闭', true)
}}
/>
<span className="switch-slider" />
</label>
</div>
</div>
<div className="divider" />
{/* API 配置 */}
<div className="form-group">
<label>API </label>
<span className="form-hint">
OpenAI <strong>Base URL</strong><strong></strong>
<code>/chat/completions</code>
<br />
<code>https://api.ohmygpt.com/v1</code> 或 <code>https://api.openai.com/v1</code>
</span>
<input
type="text"
className="field-input"
value={aiInsightApiBaseUrl}
placeholder="https://api.ohmygpt.com/v1"
onChange={(e) => {
const val = e.target.value
setAiInsightApiBaseUrl(val)
scheduleConfigSave('aiInsightApiBaseUrl', () => configService.setAiInsightApiBaseUrl(val))
}}
style={{ fontFamily: 'monospace' }}
/>
</div>
<div className="form-group">
<label>API Key</label>
<span className="form-hint">
API Key
</span>
<div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
<input
type={showInsightApiKey ? 'text' : 'password'}
className="field-input"
value={aiInsightApiKey}
placeholder="sk-..."
onChange={(e) => {
const val = e.target.value
setAiInsightApiKey(val)
scheduleConfigSave('aiInsightApiKey', () => configService.setAiInsightApiKey(val))
}}
style={{ flex: 1, fontFamily: 'monospace' }}
/>
<button
className="btn btn-secondary"
onClick={() => setShowInsightApiKey(!showInsightApiKey)}
title={showInsightApiKey ? '隐藏' : '显示'}
>
{showInsightApiKey ? <EyeOff size={14} /> : <Eye size={14} />}
</button>
{aiInsightApiKey && (
<button
className="btn btn-danger"
onClick={async () => {
setAiInsightApiKey('')
await configService.setAiInsightApiKey('')
}}
title="清除 Key"
>
<Trash2 size={14} />
</button>
)}
</div>
</div>
<div className="form-group">
<label></label>
<span className="form-hint">
API 使
<br />
<code>gpt-4o-mini</code><code>gpt-4o</code><code>deepseek-chat</code><code>claude-3-5-haiku-20241022</code>
</span>
<input
type="text"
className="field-input"
value={aiInsightApiModel}
placeholder="gpt-4o-mini"
onChange={(e) => {
const val = e.target.value.trim() || 'gpt-4o-mini'
setAiInsightApiModel(val)
scheduleConfigSave('aiInsightApiModel', () => configService.setAiInsightApiModel(val))
}}
style={{ width: 260, fontFamily: 'monospace' }}
/>
</div>
{/* 测试连接 */}
<div className="form-group">
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap' }}>
<button
className="btn btn-secondary"
onClick={handleTestInsightConnection}
disabled={isTestingInsight || !aiInsightApiBaseUrl || !aiInsightApiKey}
>
{isTestingInsight ? (
<><Loader2 size={14} style={{ marginRight: 4, animation: 'spin 1s linear infinite' }} /> ...</>
) : (
<> API </>
)}
</button>
{insightTestResult && (
<span style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, color: insightTestResult.success ? 'var(--color-success, #22c55e)' : 'var(--color-danger, #ef4444)' }}>
{insightTestResult.success ? <CheckCircle2 size={14} /> : <XCircle size={14} />}
{insightTestResult.message}
</span>
)}
</div>
</div>
<div className="divider" />
{/* 行为配置 */}
<div className="form-group">
<label></label>
<span className="form-hint">
AI 4
</span>
<input
type="number"
className="field-input"
value={aiInsightSilenceDays}
min={1}
max={365}
onChange={(e) => {
const val = Math.max(1, parseInt(e.target.value, 10) || 3)
setAiInsightSilenceDays(val)
scheduleConfigSave('aiInsightSilenceDays', () => configService.setAiInsightSilenceDays(val))
}}
style={{ width: 100 }}
/>
</div>
<div className="form-group">
<label></label>
<span className="form-hint">
AI 40 AI使
<br />
<strong></strong>AI "与某人沉默了 N 天"
<br />
<strong></strong> API
</span>
<div className="log-toggle-line">
<span className="log-status">{aiInsightAllowContext ? '已授权' : '未授权'}</span>
<label className="switch">
<input
type="checkbox"
checked={aiInsightAllowContext}
onChange={async (e) => {
const val = e.target.checked
setAiInsightAllowContext(val)
await configService.setAiInsightAllowContext(val)
}}
/>
<span className="switch-slider" />
</label>
</div>
</div>
<div className="divider" />
{/* 工作原理说明 */}
<div className="form-group">
<label></label>
<div className="api-docs">
<div className="api-item">
<p className="api-desc" style={{ lineHeight: 1.7 }}>
<strong></strong> 500ms <br />
<strong></strong> 4 <br />
<strong></strong> AI AI <br />
<strong></strong> API WeFlow
</p>
</div>
</div>
</div>
</div>
)
const renderApiTab = () => (
<div className="tab-content">
<div className="form-group">
@@ -3135,6 +3377,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
{activeTab === 'models' && renderModelsTab()}
{activeTab === 'cache' && renderCacheTab()}
{activeTab === 'api' && renderApiTab()}
{activeTab === 'insight' && renderInsightTab()}
{activeTab === 'updates' && renderUpdatesTab()}
{activeTab === 'analytics' && renderAnalyticsTab()}
{activeTab === 'security' && renderSecurityTab()}

View File

@@ -79,7 +79,15 @@ export const CONFIG_KEYS = {
// 数据收集
ANALYTICS_CONSENT: 'analyticsConsent',
ANALYTICS_DENY_COUNT: 'analyticsDenyCount'
ANALYTICS_DENY_COUNT: 'analyticsDenyCount',
// AI 见解
AI_INSIGHT_ENABLED: 'aiInsightEnabled',
AI_INSIGHT_API_BASE_URL: 'aiInsightApiBaseUrl',
AI_INSIGHT_API_KEY: 'aiInsightApiKey',
AI_INSIGHT_API_MODEL: 'aiInsightApiModel',
AI_INSIGHT_SILENCE_DAYS: 'aiInsightSilenceDays',
AI_INSIGHT_ALLOW_CONTEXT: 'aiInsightAllowContext'
} as const
export interface WxidConfig {
@@ -1551,3 +1559,59 @@ export async function getHttpApiHost(): Promise<string> {
export async function setHttpApiHost(host: string): Promise<void> {
await config.set(CONFIG_KEYS.HTTP_API_HOST, host)
}
// ─── AI 见解 ──────────────────────────────────────────────────────────────────
export async function getAiInsightEnabled(): Promise<boolean> {
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_ENABLED)
return value === true
}
export async function setAiInsightEnabled(enabled: boolean): Promise<void> {
await config.set(CONFIG_KEYS.AI_INSIGHT_ENABLED, enabled)
}
export async function getAiInsightApiBaseUrl(): Promise<string> {
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_API_BASE_URL)
return typeof value === 'string' ? value : ''
}
export async function setAiInsightApiBaseUrl(url: string): Promise<void> {
await config.set(CONFIG_KEYS.AI_INSIGHT_API_BASE_URL, url)
}
export async function getAiInsightApiKey(): Promise<string> {
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_API_KEY)
return typeof value === 'string' ? value : ''
}
export async function setAiInsightApiKey(key: string): Promise<void> {
await config.set(CONFIG_KEYS.AI_INSIGHT_API_KEY, key)
}
export async function getAiInsightApiModel(): Promise<string> {
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_API_MODEL)
return typeof value === 'string' && value.trim() ? value.trim() : 'gpt-4o-mini'
}
export async function setAiInsightApiModel(model: string): Promise<void> {
await config.set(CONFIG_KEYS.AI_INSIGHT_API_MODEL, model)
}
export async function getAiInsightSilenceDays(): Promise<number> {
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_SILENCE_DAYS)
return typeof value === 'number' && value > 0 ? value : 3
}
export async function setAiInsightSilenceDays(days: number): Promise<void> {
await config.set(CONFIG_KEYS.AI_INSIGHT_SILENCE_DAYS, days)
}
export async function getAiInsightAllowContext(): Promise<boolean> {
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_ALLOW_CONTEXT)
return value === true
}
export async function setAiInsightAllowContext(allow: boolean): Promise<void> {
await config.set(CONFIG_KEYS.AI_INSIGHT_ALLOW_CONTEXT, allow)
}