mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-11 15:08:34 +00:00
一些更新
This commit is contained in:
@@ -1635,6 +1635,22 @@ function registerIpcHandlers() {
|
|||||||
return insightService.triggerTest()
|
return insightService.triggerTest()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('insight:generateFootprintInsight', async (_, payload: {
|
||||||
|
rangeLabel: string
|
||||||
|
summary: {
|
||||||
|
private_inbound_people?: number
|
||||||
|
private_replied_people?: number
|
||||||
|
private_outbound_people?: number
|
||||||
|
private_reply_rate?: number
|
||||||
|
mention_count?: number
|
||||||
|
mention_group_count?: number
|
||||||
|
}
|
||||||
|
privateSegments?: Array<{ displayName?: string; session_id?: string; incoming_count?: number; outgoing_count?: number; message_count?: number; replied?: boolean }>
|
||||||
|
mentionGroups?: Array<{ displayName?: string; session_id?: string; count?: number }>
|
||||||
|
}) => {
|
||||||
|
return insightService.generateFootprintInsight(payload)
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('config:clear', async () => {
|
ipcMain.handle('config:clear', async () => {
|
||||||
if (isLaunchAtStartupSupported() && getSystemLaunchAtStartup()) {
|
if (isLaunchAtStartupSupported() && getSystemLaunchAtStartup()) {
|
||||||
const result = setSystemLaunchAtStartup(false)
|
const result = setSystemLaunchAtStartup(false)
|
||||||
|
|||||||
@@ -526,6 +526,19 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
insight: {
|
insight: {
|
||||||
testConnection: () => ipcRenderer.invoke('insight:testConnection'),
|
testConnection: () => ipcRenderer.invoke('insight:testConnection'),
|
||||||
getTodayStats: () => ipcRenderer.invoke('insight:getTodayStats'),
|
getTodayStats: () => ipcRenderer.invoke('insight:getTodayStats'),
|
||||||
triggerTest: () => ipcRenderer.invoke('insight:triggerTest')
|
triggerTest: () => ipcRenderer.invoke('insight:triggerTest'),
|
||||||
|
generateFootprintInsight: (payload: {
|
||||||
|
rangeLabel: string
|
||||||
|
summary: {
|
||||||
|
private_inbound_people?: number
|
||||||
|
private_replied_people?: number
|
||||||
|
private_outbound_people?: number
|
||||||
|
private_reply_rate?: number
|
||||||
|
mention_count?: number
|
||||||
|
mention_group_count?: number
|
||||||
|
}
|
||||||
|
privateSegments?: Array<{ displayName?: string; session_id?: string; incoming_count?: number; outgoing_count?: number; message_count?: number; replied?: boolean }>
|
||||||
|
mentionGroups?: Array<{ displayName?: string; session_id?: string; count?: number }>
|
||||||
|
}) => ipcRenderer.invoke('insight:generateFootprintInsight', payload)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ interface ConfigSchema {
|
|||||||
exportWriteLayout: 'A' | 'B' | 'C'
|
exportWriteLayout: 'A' | 'B' | 'C'
|
||||||
|
|
||||||
// AI 见解
|
// AI 见解
|
||||||
|
aiModelApiBaseUrl: string
|
||||||
|
aiModelApiKey: string
|
||||||
|
aiModelApiModel: string
|
||||||
aiInsightEnabled: boolean
|
aiInsightEnabled: boolean
|
||||||
aiInsightApiBaseUrl: string
|
aiInsightApiBaseUrl: string
|
||||||
aiInsightApiKey: string
|
aiInsightApiKey: string
|
||||||
@@ -93,10 +96,21 @@ interface ConfigSchema {
|
|||||||
aiInsightTelegramToken: string
|
aiInsightTelegramToken: string
|
||||||
/** Telegram 接收 Chat ID,逗号分隔,支持多个 */
|
/** Telegram 接收 Chat ID,逗号分隔,支持多个 */
|
||||||
aiInsightTelegramChatIds: string
|
aiInsightTelegramChatIds: string
|
||||||
|
|
||||||
|
// AI 足迹
|
||||||
|
aiFootprintEnabled: boolean
|
||||||
|
aiFootprintSystemPrompt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 需要 safeStorage 加密的字段(普通模式)
|
// 需要 safeStorage 加密的字段(普通模式)
|
||||||
const ENCRYPTED_STRING_KEYS: Set<string> = new Set(['decryptKey', 'imageAesKey', 'authPassword', 'httpApiToken', 'aiInsightApiKey'])
|
const ENCRYPTED_STRING_KEYS: Set<string> = new Set([
|
||||||
|
'decryptKey',
|
||||||
|
'imageAesKey',
|
||||||
|
'authPassword',
|
||||||
|
'httpApiToken',
|
||||||
|
'aiModelApiKey',
|
||||||
|
'aiInsightApiKey'
|
||||||
|
])
|
||||||
const ENCRYPTED_BOOL_KEYS: Set<string> = new Set(['authEnabled', 'authUseHello'])
|
const ENCRYPTED_BOOL_KEYS: Set<string> = new Set(['authEnabled', 'authUseHello'])
|
||||||
const ENCRYPTED_NUMBER_KEYS: Set<string> = new Set(['imageXorKey'])
|
const ENCRYPTED_NUMBER_KEYS: Set<string> = new Set(['imageXorKey'])
|
||||||
|
|
||||||
@@ -167,6 +181,9 @@ export class ConfigService {
|
|||||||
quoteLayout: 'quote-top',
|
quoteLayout: 'quote-top',
|
||||||
wordCloudExcludeWords: [],
|
wordCloudExcludeWords: [],
|
||||||
exportWriteLayout: 'A',
|
exportWriteLayout: 'A',
|
||||||
|
aiModelApiBaseUrl: '',
|
||||||
|
aiModelApiKey: '',
|
||||||
|
aiModelApiModel: 'gpt-4o-mini',
|
||||||
aiInsightEnabled: false,
|
aiInsightEnabled: false,
|
||||||
aiInsightApiBaseUrl: '',
|
aiInsightApiBaseUrl: '',
|
||||||
aiInsightApiKey: '',
|
aiInsightApiKey: '',
|
||||||
@@ -181,7 +198,9 @@ export class ConfigService {
|
|||||||
aiInsightSystemPrompt: '',
|
aiInsightSystemPrompt: '',
|
||||||
aiInsightTelegramEnabled: false,
|
aiInsightTelegramEnabled: false,
|
||||||
aiInsightTelegramToken: '',
|
aiInsightTelegramToken: '',
|
||||||
aiInsightTelegramChatIds: ''
|
aiInsightTelegramChatIds: '',
|
||||||
|
aiFootprintEnabled: false,
|
||||||
|
aiFootprintSystemPrompt: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const storeOptions: any = {
|
const storeOptions: any = {
|
||||||
@@ -213,6 +232,7 @@ export class ConfigService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.migrateAuthFields()
|
this.migrateAuthFields()
|
||||||
|
this.migrateAiConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
// === 状态查询 ===
|
// === 状态查询 ===
|
||||||
@@ -717,6 +737,26 @@ export class ConfigService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private migrateAiConfig(): void {
|
||||||
|
const sharedBaseUrl = String(this.get('aiModelApiBaseUrl') || '').trim()
|
||||||
|
const sharedApiKey = String(this.get('aiModelApiKey') || '').trim()
|
||||||
|
const sharedModel = String(this.get('aiModelApiModel') || '').trim()
|
||||||
|
|
||||||
|
const legacyBaseUrl = String(this.get('aiInsightApiBaseUrl') || '').trim()
|
||||||
|
const legacyApiKey = String(this.get('aiInsightApiKey') || '').trim()
|
||||||
|
const legacyModel = String(this.get('aiInsightApiModel') || '').trim()
|
||||||
|
|
||||||
|
if (!sharedBaseUrl && legacyBaseUrl) {
|
||||||
|
this.set('aiModelApiBaseUrl', legacyBaseUrl)
|
||||||
|
}
|
||||||
|
if (!sharedApiKey && legacyApiKey) {
|
||||||
|
this.set('aiModelApiKey', legacyApiKey)
|
||||||
|
}
|
||||||
|
if (!sharedModel && legacyModel) {
|
||||||
|
this.set('aiModelApiModel', legacyModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// === 验证 ===
|
// === 验证 ===
|
||||||
|
|
||||||
verifyAuthEnabled(): boolean {
|
verifyAuthEnabled(): boolean {
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ class ExportRecordService {
|
|||||||
|
|
||||||
private resolveFilePath(): string {
|
private resolveFilePath(): string {
|
||||||
if (this.filePath) return this.filePath
|
if (this.filePath) return this.filePath
|
||||||
const userDataPath = app.getPath('userData')
|
const workerUserDataPath = String(process.env.WEFLOW_USER_DATA_PATH || process.env.WEFLOW_CONFIG_CWD || '').trim()
|
||||||
|
const userDataPath = workerUserDataPath || app?.getPath?.('userData') || process.cwd()
|
||||||
fs.mkdirSync(userDataPath, { recursive: true })
|
fs.mkdirSync(userDataPath, { recursive: true })
|
||||||
this.filePath = path.join(userDataPath, 'weflow-export-records.json')
|
this.filePath = path.join(userDataPath, 'weflow-export-records.json')
|
||||||
return this.filePath
|
return this.filePath
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ const DEFAULT_SILENCE_DAYS = 3
|
|||||||
const INSIGHT_CONFIG_KEYS = new Set([
|
const INSIGHT_CONFIG_KEYS = new Set([
|
||||||
'aiInsightEnabled',
|
'aiInsightEnabled',
|
||||||
'aiInsightScanIntervalHours',
|
'aiInsightScanIntervalHours',
|
||||||
|
'aiModelApiBaseUrl',
|
||||||
|
'aiModelApiKey',
|
||||||
|
'aiModelApiModel',
|
||||||
'dbPath',
|
'dbPath',
|
||||||
'decryptKey',
|
'decryptKey',
|
||||||
'myWxid'
|
'myWxid'
|
||||||
@@ -51,6 +54,12 @@ interface TodayTriggerRecord {
|
|||||||
timestamps: number[]
|
timestamps: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SharedAiModelConfig {
|
||||||
|
apiBaseUrl: string
|
||||||
|
apiKey: string
|
||||||
|
model: string
|
||||||
|
}
|
||||||
|
|
||||||
// ─── 日志 ─────────────────────────────────────────────────────────────────────
|
// ─── 日志 ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -320,9 +329,7 @@ class InsightService {
|
|||||||
* 供设置页"测试连接"按钮调用。
|
* 供设置页"测试连接"按钮调用。
|
||||||
*/
|
*/
|
||||||
async testConnection(): Promise<{ success: boolean; message: string }> {
|
async testConnection(): Promise<{ success: boolean; message: string }> {
|
||||||
const apiBaseUrl = this.config.get('aiInsightApiBaseUrl') as string
|
const { apiBaseUrl, apiKey, model } = this.getSharedAiModelConfig()
|
||||||
const apiKey = this.config.get('aiInsightApiKey') as string
|
|
||||||
const model = (this.config.get('aiInsightApiModel') as string) || 'gpt-4o-mini'
|
|
||||||
|
|
||||||
if (!apiBaseUrl || !apiKey) {
|
if (!apiBaseUrl || !apiKey) {
|
||||||
return { success: false, message: '请先填写 API 地址和 API Key' }
|
return { success: false, message: '请先填写 API 地址和 API Key' }
|
||||||
@@ -348,8 +355,7 @@ class InsightService {
|
|||||||
*/
|
*/
|
||||||
async triggerTest(): Promise<{ success: boolean; message: string }> {
|
async triggerTest(): Promise<{ success: boolean; message: string }> {
|
||||||
insightLog('INFO', '手动触发测试见解...')
|
insightLog('INFO', '手动触发测试见解...')
|
||||||
const apiBaseUrl = this.config.get('aiInsightApiBaseUrl') as string
|
const { apiBaseUrl, apiKey } = this.getSharedAiModelConfig()
|
||||||
const apiKey = this.config.get('aiInsightApiKey') as string
|
|
||||||
if (!apiBaseUrl || !apiKey) {
|
if (!apiBaseUrl || !apiKey) {
|
||||||
return { success: false, message: '请先填写 API 地址和 Key' }
|
return { success: false, message: '请先填写 API 地址和 Key' }
|
||||||
}
|
}
|
||||||
@@ -398,12 +404,124 @@ class InsightService {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generateFootprintInsight(params: {
|
||||||
|
rangeLabel: string
|
||||||
|
summary: {
|
||||||
|
private_inbound_people?: number
|
||||||
|
private_replied_people?: number
|
||||||
|
private_outbound_people?: number
|
||||||
|
private_reply_rate?: number
|
||||||
|
mention_count?: number
|
||||||
|
mention_group_count?: number
|
||||||
|
}
|
||||||
|
privateSegments?: Array<{ displayName?: string; session_id?: string; incoming_count?: number; outgoing_count?: number; message_count?: number; replied?: boolean }>
|
||||||
|
mentionGroups?: Array<{ displayName?: string; session_id?: string; count?: number }>
|
||||||
|
}): Promise<{ success: boolean; message: string; insight?: string }> {
|
||||||
|
const enabled = this.config.get('aiFootprintEnabled') === true
|
||||||
|
if (!enabled) {
|
||||||
|
return { success: false, message: '请先在设置中开启「AI 足迹总结」' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const { apiBaseUrl, apiKey, model } = this.getSharedAiModelConfig()
|
||||||
|
if (!apiBaseUrl || !apiKey) {
|
||||||
|
return { success: false, message: '请先填写通用 AI 模型配置(API 地址和 Key)' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = params?.summary || {}
|
||||||
|
const rangeLabel = String(params?.rangeLabel || '').trim() || '当前范围'
|
||||||
|
const privateSegments = Array.isArray(params?.privateSegments) ? params.privateSegments.slice(0, 6) : []
|
||||||
|
const mentionGroups = Array.isArray(params?.mentionGroups) ? params.mentionGroups.slice(0, 6) : []
|
||||||
|
|
||||||
|
const topPrivateText = privateSegments.length > 0
|
||||||
|
? privateSegments
|
||||||
|
.map((item, idx) => {
|
||||||
|
const name = String(item.displayName || item.session_id || `联系人${idx + 1}`).trim()
|
||||||
|
const inbound = Number(item.incoming_count) || 0
|
||||||
|
const outbound = Number(item.outgoing_count) || 0
|
||||||
|
const total = Math.max(Number(item.message_count) || 0, inbound + outbound)
|
||||||
|
return `${idx + 1}. ${name}(收${inbound}/发${outbound}/总${total}${item.replied ? '/已回复' : ''})`
|
||||||
|
})
|
||||||
|
.join('\n')
|
||||||
|
: '无'
|
||||||
|
|
||||||
|
const topMentionText = mentionGroups.length > 0
|
||||||
|
? mentionGroups
|
||||||
|
.map((item, idx) => {
|
||||||
|
const name = String(item.displayName || item.session_id || `群聊${idx + 1}`).trim()
|
||||||
|
const count = Number(item.count) || 0
|
||||||
|
return `${idx + 1}. ${name}(@我 ${count} 次)`
|
||||||
|
})
|
||||||
|
.join('\n')
|
||||||
|
: '无'
|
||||||
|
|
||||||
|
const defaultSystemPrompt = `你是用户的聊天足迹教练,负责基于统计数据给出一段简明复盘。
|
||||||
|
要求:
|
||||||
|
1. 输出 2-3 句,总长度不超过 180 字。
|
||||||
|
2. 必须包含:总体观察 + 一个可执行建议。
|
||||||
|
3. 语气务实,不夸张,不使用 Markdown。`
|
||||||
|
const customPrompt = String(this.config.get('aiFootprintSystemPrompt') || '').trim()
|
||||||
|
const systemPrompt = customPrompt || defaultSystemPrompt
|
||||||
|
|
||||||
|
const userPrompt = `统计范围:${rangeLabel}
|
||||||
|
有聊天的人数:${Number(summary.private_inbound_people) || 0}
|
||||||
|
我有回复的人数:${Number(summary.private_outbound_people) || 0}
|
||||||
|
回复率:${(((Number(summary.private_reply_rate) || 0) * 100)).toFixed(1)}%
|
||||||
|
@我次数:${Number(summary.mention_count) || 0}
|
||||||
|
涉及群聊:${Number(summary.mention_group_count) || 0}
|
||||||
|
|
||||||
|
私聊重点:
|
||||||
|
${topPrivateText}
|
||||||
|
|
||||||
|
群聊@我重点:
|
||||||
|
${topMentionText}
|
||||||
|
|
||||||
|
请给出足迹复盘(2-3句,含建议):`
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await callApi(
|
||||||
|
apiBaseUrl,
|
||||||
|
apiKey,
|
||||||
|
model,
|
||||||
|
[
|
||||||
|
{ role: 'system', content: systemPrompt },
|
||||||
|
{ role: 'user', content: userPrompt }
|
||||||
|
],
|
||||||
|
25_000
|
||||||
|
)
|
||||||
|
const insight = result.trim().slice(0, 400)
|
||||||
|
if (!insight) return { success: false, message: '模型返回为空' }
|
||||||
|
return { success: true, message: '生成成功', insight }
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, message: `生成失败:${(error as Error).message}` }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── 私有方法 ────────────────────────────────────────────────────────────────
|
// ── 私有方法 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private isEnabled(): boolean {
|
private isEnabled(): boolean {
|
||||||
return this.config.get('aiInsightEnabled') === true
|
return this.config.get('aiInsightEnabled') === true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getSharedAiModelConfig(): SharedAiModelConfig {
|
||||||
|
const apiBaseUrl = String(
|
||||||
|
this.config.get('aiModelApiBaseUrl')
|
||||||
|
|| this.config.get('aiInsightApiBaseUrl')
|
||||||
|
|| ''
|
||||||
|
).trim()
|
||||||
|
const apiKey = String(
|
||||||
|
this.config.get('aiModelApiKey')
|
||||||
|
|| this.config.get('aiInsightApiKey')
|
||||||
|
|| ''
|
||||||
|
).trim()
|
||||||
|
const model = String(
|
||||||
|
this.config.get('aiModelApiModel')
|
||||||
|
|| this.config.get('aiInsightApiModel')
|
||||||
|
|| 'gpt-4o-mini'
|
||||||
|
).trim() || 'gpt-4o-mini'
|
||||||
|
|
||||||
|
return { apiBaseUrl, apiKey, model }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断某个会话是否允许触发见解。
|
* 判断某个会话是否允许触发见解。
|
||||||
* 若白名单未启用,则所有私聊会话均允许;
|
* 若白名单未启用,则所有私聊会话均允许;
|
||||||
@@ -696,9 +814,7 @@ class InsightService {
|
|||||||
if (!sessionId) return
|
if (!sessionId) return
|
||||||
if (!this.isEnabled()) return
|
if (!this.isEnabled()) return
|
||||||
|
|
||||||
const apiBaseUrl = this.config.get('aiInsightApiBaseUrl') as string
|
const { apiBaseUrl, apiKey, model } = this.getSharedAiModelConfig()
|
||||||
const apiKey = this.config.get('aiInsightApiKey') as string
|
|
||||||
const model = (this.config.get('aiInsightApiModel') as string) || 'gpt-4o-mini'
|
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -258,6 +258,42 @@
|
|||||||
display: none; /* Minimalistic, hide icon in KPI */
|
display: none; /* Minimalistic, hide icon in KPI */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footprint-ai-result {
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: color-mix(in srgb, var(--text-tertiary) 8%, transparent);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border-color) 40%, transparent);
|
||||||
|
|
||||||
|
.footprint-ai-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.footprint-ai-result-error {
|
||||||
|
border-color: color-mix(in srgb, #ef4444 50%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.kpi-label {
|
.kpi-label {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { AlertCircle, AtSign, CheckCircle2, Download, MessageCircle, RefreshCw, Search, Users } from 'lucide-react'
|
import { AlertCircle, AtSign, CheckCircle2, Download, Loader2, MessageCircle, RefreshCw, Search, Sparkles, Users } from 'lucide-react'
|
||||||
import DateRangePicker from '../components/DateRangePicker'
|
import DateRangePicker from '../components/DateRangePicker'
|
||||||
import './MyFootprintPage.scss'
|
import './MyFootprintPage.scss'
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@ type TimelineMode = 'all' | 'mention' | 'private'
|
|||||||
type TimelineTimeMode = 'clock' | 'month_day_clock' | 'full_date_clock'
|
type TimelineTimeMode = 'clock' | 'month_day_clock' | 'full_date_clock'
|
||||||
type PrivateDotVariant = 'both' | 'inbound_only' | 'outbound_only'
|
type PrivateDotVariant = 'both' | 'inbound_only' | 'outbound_only'
|
||||||
type ExportModalStatus = 'idle' | 'progress' | 'success' | 'error'
|
type ExportModalStatus = 'idle' | 'progress' | 'success' | 'error'
|
||||||
|
type FootprintAiStatus = 'idle' | 'loading' | 'success' | 'error'
|
||||||
|
|
||||||
interface MyFootprintSummary {
|
interface MyFootprintSummary {
|
||||||
private_inbound_people: number
|
private_inbound_people: number
|
||||||
@@ -336,6 +337,8 @@ function MyFootprintPage() {
|
|||||||
const [exportModalDescription, setExportModalDescription] = useState('')
|
const [exportModalDescription, setExportModalDescription] = useState('')
|
||||||
const [exportModalPath, setExportModalPath] = useState('')
|
const [exportModalPath, setExportModalPath] = useState('')
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [footprintAiStatus, setFootprintAiStatus] = useState<FootprintAiStatus>('idle')
|
||||||
|
const [footprintAiText, setFootprintAiText] = useState('')
|
||||||
const inflightRangeKeyRef = useRef<string | null>(null)
|
const inflightRangeKeyRef = useRef<string | null>(null)
|
||||||
|
|
||||||
const currentRange = useMemo(() => buildRange(preset, customStartDate, customEndDate), [preset, customStartDate, customEndDate])
|
const currentRange = useMemo(() => buildRange(preset, customStartDate, customEndDate), [preset, customStartDate, customEndDate])
|
||||||
@@ -638,6 +641,41 @@ function MyFootprintPage() {
|
|||||||
}
|
}
|
||||||
}, [currentRange.begin, currentRange.end, currentRange.label])
|
}, [currentRange.begin, currentRange.end, currentRange.label])
|
||||||
|
|
||||||
|
const handleGenerateAiSummary = useCallback(async () => {
|
||||||
|
setFootprintAiStatus('loading')
|
||||||
|
setFootprintAiText('')
|
||||||
|
try {
|
||||||
|
const privateSegments = (data.private_segments.length > 0 ? data.private_segments : data.private_sessions).slice(0, 12)
|
||||||
|
const result = await window.electronAPI.insight.generateFootprintInsight({
|
||||||
|
rangeLabel: currentRange.label,
|
||||||
|
summary: data.summary,
|
||||||
|
privateSegments: privateSegments.map((item: MyFootprintPrivateSegment | MyFootprintPrivateSession) => ({
|
||||||
|
session_id: item.session_id,
|
||||||
|
displayName: item.displayName,
|
||||||
|
incoming_count: item.incoming_count,
|
||||||
|
outgoing_count: item.outgoing_count,
|
||||||
|
message_count: 'message_count' in item ? item.message_count : item.incoming_count + item.outgoing_count,
|
||||||
|
replied: item.replied
|
||||||
|
})),
|
||||||
|
mentionGroups: data.mention_groups.slice(0, 12).map((item) => ({
|
||||||
|
session_id: item.session_id,
|
||||||
|
displayName: item.displayName,
|
||||||
|
count: item.count
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
if (!result.success || !result.insight) {
|
||||||
|
setFootprintAiStatus('error')
|
||||||
|
setFootprintAiText(result.message || '生成失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setFootprintAiStatus('success')
|
||||||
|
setFootprintAiText(result.insight)
|
||||||
|
} catch (generateError) {
|
||||||
|
setFootprintAiStatus('error')
|
||||||
|
setFootprintAiText(String(generateError))
|
||||||
|
}
|
||||||
|
}, [currentRange.label, data])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="my-footprint-page">
|
<div className="my-footprint-page">
|
||||||
<section className="footprint-header">
|
<section className="footprint-header">
|
||||||
@@ -690,6 +728,10 @@ function MyFootprintPage() {
|
|||||||
<RefreshCw size={15} className={loading ? 'spin' : ''} />
|
<RefreshCw size={15} className={loading ? 'spin' : ''} />
|
||||||
<span>刷新</span>
|
<span>刷新</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" className="action-btn" onClick={() => void handleGenerateAiSummary()} disabled={loading || footprintAiStatus === 'loading'}>
|
||||||
|
{footprintAiStatus === 'loading' ? <Loader2 size={15} className="spin" /> : <Sparkles size={15} />}
|
||||||
|
<span>{footprintAiStatus === 'loading' ? '生成中...' : 'AI 总结'}</span>
|
||||||
|
</button>
|
||||||
<button type="button" className="action-btn" onClick={() => void handleExport('csv')} disabled={exporting || loading}>
|
<button type="button" className="action-btn" onClick={() => void handleExport('csv')} disabled={exporting || loading}>
|
||||||
<Download size={15} />
|
<Download size={15} />
|
||||||
<span>导出 CSV</span>
|
<span>导出 CSV</span>
|
||||||
@@ -749,6 +791,16 @@ function MyFootprintPage() {
|
|||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{footprintAiStatus !== 'idle' && (
|
||||||
|
<section className={`footprint-ai-result footprint-ai-result-${footprintAiStatus}`}>
|
||||||
|
<div className="footprint-ai-head">
|
||||||
|
<strong>AI 足迹总结</strong>
|
||||||
|
<span>{currentRange.label}</span>
|
||||||
|
</div>
|
||||||
|
<p>{footprintAiText}</p>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
<section
|
<section
|
||||||
className={`footprint-timeline timeline-time-${timelineTimeMode}`}
|
className={`footprint-timeline timeline-time-${timelineTimeMode}`}
|
||||||
key={`${timelineMode}:${currentRange.begin}:${currentRange.end}`}
|
key={`${timelineMode}:${currentRange.begin}:${currentRange.end}`}
|
||||||
|
|||||||
@@ -177,6 +177,66 @@
|
|||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tab-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-group-trigger {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-group-arrow {
|
||||||
|
margin-left: auto;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
|
||||||
|
&.expanded {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-sublist {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-sublist-wrap {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 0fr;
|
||||||
|
opacity: 0;
|
||||||
|
transition: grid-template-rows 0.22s ease, opacity 0.18s ease;
|
||||||
|
|
||||||
|
&.expanded {
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-sublist {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-sub-btn {
|
||||||
|
padding-left: 24px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-sub-dot {
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: color-mix(in srgb, var(--text-tertiary) 70%, transparent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-body {
|
.settings-body {
|
||||||
@@ -199,6 +259,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ai-prompt-textarea {
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
.tab-content {
|
.tab-content {
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
|
|||||||
@@ -16,9 +16,23 @@ import {
|
|||||||
import { Avatar } from '../components/Avatar'
|
import { Avatar } from '../components/Avatar'
|
||||||
import './SettingsPage.scss'
|
import './SettingsPage.scss'
|
||||||
|
|
||||||
type SettingsTab = 'appearance' | 'notification' | 'antiRevoke' | 'database' | 'models' | 'cache' | 'api' | 'updates' | 'security' | 'about' | 'analytics' | 'insight'
|
type SettingsTab =
|
||||||
|
| 'appearance'
|
||||||
|
| 'notification'
|
||||||
|
| 'antiRevoke'
|
||||||
|
| 'database'
|
||||||
|
| 'models'
|
||||||
|
| 'cache'
|
||||||
|
| 'api'
|
||||||
|
| 'updates'
|
||||||
|
| 'security'
|
||||||
|
| 'about'
|
||||||
|
| 'analytics'
|
||||||
|
| 'aiCommon'
|
||||||
|
| 'insight'
|
||||||
|
| 'aiFootprint'
|
||||||
|
|
||||||
const tabs: { id: SettingsTab; label: string; icon: React.ElementType }[] = [
|
const tabs: { id: Exclude<SettingsTab, 'insight' | 'aiFootprint'>; label: string; icon: React.ElementType }[] = [
|
||||||
{ id: 'appearance', label: '外观', icon: Palette },
|
{ id: 'appearance', label: '外观', icon: Palette },
|
||||||
{ id: 'notification', label: '通知', icon: Bell },
|
{ id: 'notification', label: '通知', icon: Bell },
|
||||||
{ id: 'antiRevoke', label: '防撤回', icon: RotateCcw },
|
{ id: 'antiRevoke', label: '防撤回', icon: RotateCcw },
|
||||||
@@ -27,12 +41,17 @@ const tabs: { id: SettingsTab; label: string; icon: React.ElementType }[] = [
|
|||||||
{ id: 'cache', label: '缓存', icon: HardDrive },
|
{ id: 'cache', label: '缓存', icon: HardDrive },
|
||||||
{ id: 'api', label: 'API 服务', icon: Globe },
|
{ id: 'api', label: 'API 服务', icon: Globe },
|
||||||
{ id: 'analytics', label: '分析', icon: BarChart2 },
|
{ id: 'analytics', label: '分析', icon: BarChart2 },
|
||||||
{ id: 'insight', label: 'AI 见解', icon: Sparkles },
|
|
||||||
{ id: 'security', label: '安全', icon: ShieldCheck },
|
{ id: 'security', label: '安全', icon: ShieldCheck },
|
||||||
{ id: 'updates', label: '版本更新', icon: RefreshCw },
|
{ id: 'updates', label: '版本更新', icon: RefreshCw },
|
||||||
{ id: 'about', label: '关于', icon: Info }
|
{ id: 'about', label: '关于', icon: Info }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const aiTabs: Array<{ id: Extract<SettingsTab, 'aiCommon' | 'insight' | 'aiFootprint'>; label: string }> = [
|
||||||
|
{ id: 'aiCommon', label: 'AI 通用' },
|
||||||
|
{ id: 'insight', label: 'AI 见解' },
|
||||||
|
{ id: 'aiFootprint', label: 'AI 足迹' }
|
||||||
|
]
|
||||||
|
|
||||||
const isMac = navigator.userAgent.toLowerCase().includes('mac')
|
const isMac = navigator.userAgent.toLowerCase().includes('mac')
|
||||||
const isLinux = navigator.userAgent.toLowerCase().includes('linux')
|
const isLinux = navigator.userAgent.toLowerCase().includes('linux')
|
||||||
const isWindows = !isMac && !isLinux
|
const isWindows = !isMac && !isLinux
|
||||||
@@ -88,6 +107,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
const clearAnalyticsStoreCache = useAnalyticsStore((state) => state.clearCache)
|
const clearAnalyticsStoreCache = useAnalyticsStore((state) => state.clearCache)
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<SettingsTab>('appearance')
|
const [activeTab, setActiveTab] = useState<SettingsTab>('appearance')
|
||||||
|
const [aiGroupExpanded, setAiGroupExpanded] = useState(false)
|
||||||
const [decryptKey, setDecryptKey] = useState('')
|
const [decryptKey, setDecryptKey] = useState('')
|
||||||
const [imageXorKey, setImageXorKey] = useState('')
|
const [imageXorKey, setImageXorKey] = useState('')
|
||||||
const [imageAesKey, setImageAesKey] = useState('')
|
const [imageAesKey, setImageAesKey] = useState('')
|
||||||
@@ -217,9 +237,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
|
|
||||||
// AI 见解 state
|
// AI 见解 state
|
||||||
const [aiInsightEnabled, setAiInsightEnabled] = useState(false)
|
const [aiInsightEnabled, setAiInsightEnabled] = useState(false)
|
||||||
const [aiInsightApiBaseUrl, setAiInsightApiBaseUrl] = useState('')
|
const [aiModelApiBaseUrl, setAiModelApiBaseUrl] = useState('')
|
||||||
const [aiInsightApiKey, setAiInsightApiKey] = useState('')
|
const [aiModelApiKey, setAiModelApiKey] = useState('')
|
||||||
const [aiInsightApiModel, setAiInsightApiModel] = useState('gpt-4o-mini')
|
const [aiModelApiModel, setAiModelApiModel] = useState('gpt-4o-mini')
|
||||||
const [aiInsightSilenceDays, setAiInsightSilenceDays] = useState(3)
|
const [aiInsightSilenceDays, setAiInsightSilenceDays] = useState(3)
|
||||||
const [aiInsightAllowContext, setAiInsightAllowContext] = useState(false)
|
const [aiInsightAllowContext, setAiInsightAllowContext] = useState(false)
|
||||||
const [isTestingInsight, setIsTestingInsight] = useState(false)
|
const [isTestingInsight, setIsTestingInsight] = useState(false)
|
||||||
@@ -237,6 +257,8 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
const [aiInsightTelegramEnabled, setAiInsightTelegramEnabled] = useState(false)
|
const [aiInsightTelegramEnabled, setAiInsightTelegramEnabled] = useState(false)
|
||||||
const [aiInsightTelegramToken, setAiInsightTelegramToken] = useState('')
|
const [aiInsightTelegramToken, setAiInsightTelegramToken] = useState('')
|
||||||
const [aiInsightTelegramChatIds, setAiInsightTelegramChatIds] = useState('')
|
const [aiInsightTelegramChatIds, setAiInsightTelegramChatIds] = useState('')
|
||||||
|
const [aiFootprintEnabled, setAiFootprintEnabled] = useState(false)
|
||||||
|
const [aiFootprintSystemPrompt, setAiFootprintSystemPrompt] = useState('')
|
||||||
|
|
||||||
// 检查 Hello 可用性
|
// 检查 Hello 可用性
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -276,6 +298,12 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
setActiveTab(initialTab)
|
setActiveTab(initialTab)
|
||||||
}, [location.state])
|
}, [location.state])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTab === 'aiCommon' || activeTab === 'insight' || activeTab === 'aiFootprint') {
|
||||||
|
setAiGroupExpanded(true)
|
||||||
|
}
|
||||||
|
}, [activeTab])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!onClose) return
|
if (!onClose) return
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
@@ -448,9 +476,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
|
|
||||||
// 加载 AI 见解配置
|
// 加载 AI 见解配置
|
||||||
const savedAiInsightEnabled = await configService.getAiInsightEnabled()
|
const savedAiInsightEnabled = await configService.getAiInsightEnabled()
|
||||||
const savedAiInsightApiBaseUrl = await configService.getAiInsightApiBaseUrl()
|
const savedAiModelApiBaseUrl = await configService.getAiModelApiBaseUrl()
|
||||||
const savedAiInsightApiKey = await configService.getAiInsightApiKey()
|
const savedAiModelApiKey = await configService.getAiModelApiKey()
|
||||||
const savedAiInsightApiModel = await configService.getAiInsightApiModel()
|
const savedAiModelApiModel = await configService.getAiModelApiModel()
|
||||||
const savedAiInsightSilenceDays = await configService.getAiInsightSilenceDays()
|
const savedAiInsightSilenceDays = await configService.getAiInsightSilenceDays()
|
||||||
const savedAiInsightAllowContext = await configService.getAiInsightAllowContext()
|
const savedAiInsightAllowContext = await configService.getAiInsightAllowContext()
|
||||||
const savedAiInsightWhitelistEnabled = await configService.getAiInsightWhitelistEnabled()
|
const savedAiInsightWhitelistEnabled = await configService.getAiInsightWhitelistEnabled()
|
||||||
@@ -462,10 +490,12 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
const savedAiInsightTelegramEnabled = await configService.getAiInsightTelegramEnabled()
|
const savedAiInsightTelegramEnabled = await configService.getAiInsightTelegramEnabled()
|
||||||
const savedAiInsightTelegramToken = await configService.getAiInsightTelegramToken()
|
const savedAiInsightTelegramToken = await configService.getAiInsightTelegramToken()
|
||||||
const savedAiInsightTelegramChatIds = await configService.getAiInsightTelegramChatIds()
|
const savedAiInsightTelegramChatIds = await configService.getAiInsightTelegramChatIds()
|
||||||
|
const savedAiFootprintEnabled = await configService.getAiFootprintEnabled()
|
||||||
|
const savedAiFootprintSystemPrompt = await configService.getAiFootprintSystemPrompt()
|
||||||
setAiInsightEnabled(savedAiInsightEnabled)
|
setAiInsightEnabled(savedAiInsightEnabled)
|
||||||
setAiInsightApiBaseUrl(savedAiInsightApiBaseUrl)
|
setAiModelApiBaseUrl(savedAiModelApiBaseUrl)
|
||||||
setAiInsightApiKey(savedAiInsightApiKey)
|
setAiModelApiKey(savedAiModelApiKey)
|
||||||
setAiInsightApiModel(savedAiInsightApiModel)
|
setAiModelApiModel(savedAiModelApiModel)
|
||||||
setAiInsightSilenceDays(savedAiInsightSilenceDays)
|
setAiInsightSilenceDays(savedAiInsightSilenceDays)
|
||||||
setAiInsightAllowContext(savedAiInsightAllowContext)
|
setAiInsightAllowContext(savedAiInsightAllowContext)
|
||||||
setAiInsightWhitelistEnabled(savedAiInsightWhitelistEnabled)
|
setAiInsightWhitelistEnabled(savedAiInsightWhitelistEnabled)
|
||||||
@@ -477,6 +507,8 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
setAiInsightTelegramEnabled(savedAiInsightTelegramEnabled)
|
setAiInsightTelegramEnabled(savedAiInsightTelegramEnabled)
|
||||||
setAiInsightTelegramToken(savedAiInsightTelegramToken)
|
setAiInsightTelegramToken(savedAiInsightTelegramToken)
|
||||||
setAiInsightTelegramChatIds(savedAiInsightTelegramChatIds)
|
setAiInsightTelegramChatIds(savedAiInsightTelegramChatIds)
|
||||||
|
setAiFootprintEnabled(savedAiFootprintEnabled)
|
||||||
|
setAiFootprintSystemPrompt(savedAiFootprintSystemPrompt)
|
||||||
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('加载配置失败:', e)
|
console.error('加载配置失败:', e)
|
||||||
@@ -2498,6 +2530,118 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderAiCommonTab = () => (
|
||||||
|
<div className="tab-content">
|
||||||
|
<div className="form-group">
|
||||||
|
<label>通用 API 地址</label>
|
||||||
|
<span className="form-hint">
|
||||||
|
这是「AI 见解」与「AI 足迹总结」共享的模型接入配置。填写 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={aiModelApiBaseUrl}
|
||||||
|
placeholder="https://api.ohmygpt.com/v1"
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value
|
||||||
|
setAiModelApiBaseUrl(val)
|
||||||
|
scheduleConfigSave('aiModelApiBaseUrl', () => configService.setAiModelApiBaseUrl(val))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</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={aiModelApiKey}
|
||||||
|
placeholder="sk-..."
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value
|
||||||
|
setAiModelApiKey(val)
|
||||||
|
scheduleConfigSave('aiModelApiKey', () => configService.setAiModelApiKey(val))
|
||||||
|
}}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => setShowInsightApiKey(!showInsightApiKey)}
|
||||||
|
title={showInsightApiKey ? '隐藏' : '显示'}
|
||||||
|
>
|
||||||
|
{showInsightApiKey ? <EyeOff size={14} /> : <Eye size={14} />}
|
||||||
|
</button>
|
||||||
|
{aiModelApiKey && (
|
||||||
|
<button
|
||||||
|
className="btn btn-danger"
|
||||||
|
onClick={async () => {
|
||||||
|
setAiModelApiKey('')
|
||||||
|
await configService.setAiModelApiKey('')
|
||||||
|
}}
|
||||||
|
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={aiModelApiModel}
|
||||||
|
placeholder="gpt-4o-mini"
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value.trim() || 'gpt-4o-mini'
|
||||||
|
setAiModelApiModel(val)
|
||||||
|
scheduleConfigSave('aiModelApiModel', () => configService.setAiModelApiModel(val))
|
||||||
|
}}
|
||||||
|
style={{ width: 260 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label>连接测试</label>
|
||||||
|
<span className="form-hint">
|
||||||
|
测试通用模型连接,见解与足迹都会使用这套配置。
|
||||||
|
</span>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap', marginTop: '10px' }}>
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={handleTestInsightConnection}
|
||||||
|
disabled={isTestingInsight || !aiModelApiBaseUrl || !aiModelApiKey}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
)
|
||||||
|
|
||||||
const renderInsightTab = () => (
|
const renderInsightTab = () => (
|
||||||
<div className="tab-content">
|
<div className="tab-content">
|
||||||
{/* 总开关 */}
|
{/* 总开关 */}
|
||||||
@@ -2526,119 +2670,12 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
|
|
||||||
<div className="divider" />
|
<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 className="form-group">
|
||||||
<label>调试工具</label>
|
<label>调试工具</label>
|
||||||
<span className="form-hint">
|
<span className="form-hint">
|
||||||
先用"测试 API 连接"确认 Key 和 URL 填写正确,再用"立即触发测试见解"验证完整链路(数据库→API→弹窗)。触发后请留意右下角通知弹窗。
|
该功能依赖「AI 通用」里的模型配置。用于验证完整链路(数据库→API→弹窗)。
|
||||||
</span>
|
</span>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginTop: '10px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap', marginTop: '10px' }}>
|
||||||
{/* 测试 API 连接 */}
|
|
||||||
<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 style={{ display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap' }}>
|
|
||||||
<button
|
<button
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -2653,7 +2690,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
setIsTriggeringInsightTest(false)
|
setIsTriggeringInsightTest(false)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={isTriggeringInsightTest || !aiInsightEnabled || !aiInsightApiBaseUrl || !aiInsightApiKey}
|
disabled={isTriggeringInsightTest || !aiInsightEnabled || !aiModelApiBaseUrl || !aiModelApiKey}
|
||||||
title={!aiInsightEnabled ? '请先开启 AI 见解总开关' : ''}
|
title={!aiInsightEnabled ? '请先开启 AI 见解总开关' : ''}
|
||||||
>
|
>
|
||||||
{isTriggeringInsightTest ? (
|
{isTriggeringInsightTest ? (
|
||||||
@@ -2670,7 +2707,6 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="divider" />
|
<div className="divider" />
|
||||||
|
|
||||||
@@ -2824,9 +2860,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
当前显示内置默认提示词,可直接编辑修改。修改后立即生效,无需重启。可变的统计信息(触发次数、对话内容)会自动附加在用户消息里,无需在此填写。
|
当前显示内置默认提示词,可直接编辑修改。修改后立即生效,无需重启。可变的统计信息(触发次数、对话内容)会自动附加在用户消息里,无需在此填写。
|
||||||
</span>
|
</span>
|
||||||
<textarea
|
<textarea
|
||||||
className="field-input"
|
className="field-input ai-prompt-textarea"
|
||||||
rows={8}
|
rows={8}
|
||||||
style={{ width: '100%', resize: 'vertical', fontFamily: 'monospace', fontSize: 12 }}
|
style={{ width: '100%', resize: 'vertical' }}
|
||||||
value={displayValue}
|
value={displayValue}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = e.target.value
|
const val = e.target.value
|
||||||
@@ -3106,6 +3142,74 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const renderAiFootprintTab = () => (
|
||||||
|
<div className="tab-content">
|
||||||
|
{(() => {
|
||||||
|
const DEFAULT_FOOTPRINT_PROMPT = `你是用户的聊天足迹教练,负责基于统计数据给出一段简明复盘。
|
||||||
|
要求:
|
||||||
|
1. 输出 2-3 句,总长度不超过 180 字。
|
||||||
|
2. 必须包含:总体观察 + 一个可执行建议。
|
||||||
|
3. 语气务实,不夸张,不使用 Markdown。`
|
||||||
|
const displayValue = aiFootprintSystemPrompt || DEFAULT_FOOTPRINT_PROMPT
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>AI 足迹总结</label>
|
||||||
|
<span className="form-hint">
|
||||||
|
开启后,可在「我的微信足迹」页面一键生成当前范围的 AI 复盘总结。
|
||||||
|
</span>
|
||||||
|
<div className="log-toggle-line">
|
||||||
|
<span className="log-status">{aiFootprintEnabled ? '已开启' : '已关闭'}</span>
|
||||||
|
<label className="switch">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={aiFootprintEnabled}
|
||||||
|
onChange={async (e) => {
|
||||||
|
const val = e.target.checked
|
||||||
|
setAiFootprintEnabled(val)
|
||||||
|
await configService.setAiFootprintEnabled(val)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="switch-slider" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 }}>
|
||||||
|
<label style={{ marginBottom: 0 }}>足迹总结提示词</label>
|
||||||
|
<button
|
||||||
|
className="button-secondary"
|
||||||
|
style={{ fontSize: 12, padding: '3px 10px' }}
|
||||||
|
onClick={async () => {
|
||||||
|
setAiFootprintSystemPrompt('')
|
||||||
|
await configService.setAiFootprintSystemPrompt('')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
恢复默认
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span className="form-hint">
|
||||||
|
足迹模块专用的小配置。留空时使用内置默认提示词。
|
||||||
|
</span>
|
||||||
|
<textarea
|
||||||
|
className="field-input ai-prompt-textarea"
|
||||||
|
rows={6}
|
||||||
|
style={{ width: '100%', resize: 'vertical' }}
|
||||||
|
value={displayValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value
|
||||||
|
setAiFootprintSystemPrompt(val)
|
||||||
|
scheduleConfigSave('aiFootprintSystemPrompt', () => configService.setAiFootprintSystemPrompt(val))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
const renderApiTab = () => (
|
const renderApiTab = () => (
|
||||||
<div className="tab-content">
|
<div className="tab-content">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
@@ -3780,6 +3884,33 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
<span>{tab.label}</span>
|
<span>{tab.label}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<div className={`tab-group ${aiGroupExpanded ? 'expanded' : ''}`}>
|
||||||
|
<button
|
||||||
|
className={`tab-btn tab-group-trigger ${(activeTab === 'aiCommon' || activeTab === 'insight' || activeTab === 'aiFootprint') ? 'active' : ''}`}
|
||||||
|
onClick={() => setAiGroupExpanded((prev) => !prev)}
|
||||||
|
aria-expanded={aiGroupExpanded}
|
||||||
|
>
|
||||||
|
<Sparkles size={16} />
|
||||||
|
<span>AI 相关</span>
|
||||||
|
<ChevronDown size={14} className={`tab-group-arrow ${aiGroupExpanded ? 'expanded' : ''}`} />
|
||||||
|
</button>
|
||||||
|
<div className={`tab-sublist-wrap ${aiGroupExpanded ? 'expanded' : 'collapsed'}`}>
|
||||||
|
<div className="tab-sublist">
|
||||||
|
{aiTabs.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
className={`tab-btn tab-sub-btn ${activeTab === tab.id ? 'active' : ''}`}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
tabIndex={aiGroupExpanded ? 0 : -1}
|
||||||
|
>
|
||||||
|
<span className="tab-sub-dot" />
|
||||||
|
<span>{tab.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="settings-body">
|
<div className="settings-body">
|
||||||
@@ -3790,7 +3921,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
{activeTab === 'models' && renderModelsTab()}
|
{activeTab === 'models' && renderModelsTab()}
|
||||||
{activeTab === 'cache' && renderCacheTab()}
|
{activeTab === 'cache' && renderCacheTab()}
|
||||||
{activeTab === 'api' && renderApiTab()}
|
{activeTab === 'api' && renderApiTab()}
|
||||||
|
{activeTab === 'aiCommon' && renderAiCommonTab()}
|
||||||
{activeTab === 'insight' && renderInsightTab()}
|
{activeTab === 'insight' && renderInsightTab()}
|
||||||
|
{activeTab === 'aiFootprint' && renderAiFootprintTab()}
|
||||||
{activeTab === 'updates' && renderUpdatesTab()}
|
{activeTab === 'updates' && renderUpdatesTab()}
|
||||||
{activeTab === 'analytics' && renderAnalyticsTab()}
|
{activeTab === 'analytics' && renderAnalyticsTab()}
|
||||||
{activeTab === 'security' && renderSecurityTab()}
|
{activeTab === 'security' && renderSecurityTab()}
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ export const CONFIG_KEYS = {
|
|||||||
ANALYTICS_DENY_COUNT: 'analyticsDenyCount',
|
ANALYTICS_DENY_COUNT: 'analyticsDenyCount',
|
||||||
|
|
||||||
// AI 见解
|
// AI 见解
|
||||||
|
AI_MODEL_API_BASE_URL: 'aiModelApiBaseUrl',
|
||||||
|
AI_MODEL_API_KEY: 'aiModelApiKey',
|
||||||
|
AI_MODEL_API_MODEL: 'aiModelApiModel',
|
||||||
AI_INSIGHT_ENABLED: 'aiInsightEnabled',
|
AI_INSIGHT_ENABLED: 'aiInsightEnabled',
|
||||||
AI_INSIGHT_API_BASE_URL: 'aiInsightApiBaseUrl',
|
AI_INSIGHT_API_BASE_URL: 'aiInsightApiBaseUrl',
|
||||||
AI_INSIGHT_API_KEY: 'aiInsightApiKey',
|
AI_INSIGHT_API_KEY: 'aiInsightApiKey',
|
||||||
@@ -97,7 +100,11 @@ export const CONFIG_KEYS = {
|
|||||||
AI_INSIGHT_SYSTEM_PROMPT: 'aiInsightSystemPrompt',
|
AI_INSIGHT_SYSTEM_PROMPT: 'aiInsightSystemPrompt',
|
||||||
AI_INSIGHT_TELEGRAM_ENABLED: 'aiInsightTelegramEnabled',
|
AI_INSIGHT_TELEGRAM_ENABLED: 'aiInsightTelegramEnabled',
|
||||||
AI_INSIGHT_TELEGRAM_TOKEN: 'aiInsightTelegramToken',
|
AI_INSIGHT_TELEGRAM_TOKEN: 'aiInsightTelegramToken',
|
||||||
AI_INSIGHT_TELEGRAM_CHAT_IDS: 'aiInsightTelegramChatIds'
|
AI_INSIGHT_TELEGRAM_CHAT_IDS: 'aiInsightTelegramChatIds',
|
||||||
|
|
||||||
|
// AI 足迹
|
||||||
|
AI_FOOTPRINT_ENABLED: 'aiFootprintEnabled',
|
||||||
|
AI_FOOTPRINT_SYSTEM_PROMPT: 'aiFootprintSystemPrompt'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export interface WxidConfig {
|
export interface WxidConfig {
|
||||||
@@ -1586,6 +1593,39 @@ export async function setHttpApiHost(host: string): Promise<void> {
|
|||||||
|
|
||||||
// ─── AI 见解 ──────────────────────────────────────────────────────────────────
|
// ─── AI 见解 ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getAiModelApiBaseUrl(): Promise<string> {
|
||||||
|
const value = await config.get(CONFIG_KEYS.AI_MODEL_API_BASE_URL)
|
||||||
|
if (typeof value === 'string' && value.trim()) return value
|
||||||
|
const legacy = await config.get(CONFIG_KEYS.AI_INSIGHT_API_BASE_URL)
|
||||||
|
return typeof legacy === 'string' ? legacy : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setAiModelApiBaseUrl(url: string): Promise<void> {
|
||||||
|
await config.set(CONFIG_KEYS.AI_MODEL_API_BASE_URL, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAiModelApiKey(): Promise<string> {
|
||||||
|
const value = await config.get(CONFIG_KEYS.AI_MODEL_API_KEY)
|
||||||
|
if (typeof value === 'string' && value.trim()) return value
|
||||||
|
const legacy = await config.get(CONFIG_KEYS.AI_INSIGHT_API_KEY)
|
||||||
|
return typeof legacy === 'string' ? legacy : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setAiModelApiKey(key: string): Promise<void> {
|
||||||
|
await config.set(CONFIG_KEYS.AI_MODEL_API_KEY, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAiModelApiModel(): Promise<string> {
|
||||||
|
const value = await config.get(CONFIG_KEYS.AI_MODEL_API_MODEL)
|
||||||
|
if (typeof value === 'string' && value.trim()) return value.trim()
|
||||||
|
const legacy = await config.get(CONFIG_KEYS.AI_INSIGHT_API_MODEL)
|
||||||
|
return typeof legacy === 'string' && legacy.trim() ? legacy.trim() : 'gpt-4o-mini'
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setAiModelApiModel(model: string): Promise<void> {
|
||||||
|
await config.set(CONFIG_KEYS.AI_MODEL_API_MODEL, model)
|
||||||
|
}
|
||||||
|
|
||||||
export async function getAiInsightEnabled(): Promise<boolean> {
|
export async function getAiInsightEnabled(): Promise<boolean> {
|
||||||
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_ENABLED)
|
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_ENABLED)
|
||||||
return value === true
|
return value === true
|
||||||
@@ -1596,30 +1636,30 @@ export async function setAiInsightEnabled(enabled: boolean): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getAiInsightApiBaseUrl(): Promise<string> {
|
export async function getAiInsightApiBaseUrl(): Promise<string> {
|
||||||
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_API_BASE_URL)
|
return getAiModelApiBaseUrl()
|
||||||
return typeof value === 'string' ? value : ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setAiInsightApiBaseUrl(url: string): Promise<void> {
|
export async function setAiInsightApiBaseUrl(url: string): Promise<void> {
|
||||||
await config.set(CONFIG_KEYS.AI_INSIGHT_API_BASE_URL, url)
|
await config.set(CONFIG_KEYS.AI_INSIGHT_API_BASE_URL, url)
|
||||||
|
await setAiModelApiBaseUrl(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAiInsightApiKey(): Promise<string> {
|
export async function getAiInsightApiKey(): Promise<string> {
|
||||||
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_API_KEY)
|
return getAiModelApiKey()
|
||||||
return typeof value === 'string' ? value : ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setAiInsightApiKey(key: string): Promise<void> {
|
export async function setAiInsightApiKey(key: string): Promise<void> {
|
||||||
await config.set(CONFIG_KEYS.AI_INSIGHT_API_KEY, key)
|
await config.set(CONFIG_KEYS.AI_INSIGHT_API_KEY, key)
|
||||||
|
await setAiModelApiKey(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAiInsightApiModel(): Promise<string> {
|
export async function getAiInsightApiModel(): Promise<string> {
|
||||||
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_API_MODEL)
|
return getAiModelApiModel()
|
||||||
return typeof value === 'string' && value.trim() ? value.trim() : 'gpt-4o-mini'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setAiInsightApiModel(model: string): Promise<void> {
|
export async function setAiInsightApiModel(model: string): Promise<void> {
|
||||||
await config.set(CONFIG_KEYS.AI_INSIGHT_API_MODEL, model)
|
await config.set(CONFIG_KEYS.AI_INSIGHT_API_MODEL, model)
|
||||||
|
await setAiModelApiModel(model)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAiInsightSilenceDays(): Promise<number> {
|
export async function getAiInsightSilenceDays(): Promise<number> {
|
||||||
@@ -1720,3 +1760,21 @@ export async function getAiInsightTelegramChatIds(): Promise<string> {
|
|||||||
export async function setAiInsightTelegramChatIds(chatIds: string): Promise<void> {
|
export async function setAiInsightTelegramChatIds(chatIds: string): Promise<void> {
|
||||||
await config.set(CONFIG_KEYS.AI_INSIGHT_TELEGRAM_CHAT_IDS, chatIds)
|
await config.set(CONFIG_KEYS.AI_INSIGHT_TELEGRAM_CHAT_IDS, chatIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAiFootprintEnabled(): Promise<boolean> {
|
||||||
|
const value = await config.get(CONFIG_KEYS.AI_FOOTPRINT_ENABLED)
|
||||||
|
return value === true
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setAiFootprintEnabled(enabled: boolean): Promise<void> {
|
||||||
|
await config.set(CONFIG_KEYS.AI_FOOTPRINT_ENABLED, enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAiFootprintSystemPrompt(): Promise<string> {
|
||||||
|
const value = await config.get(CONFIG_KEYS.AI_FOOTPRINT_SYSTEM_PROMPT)
|
||||||
|
return typeof value === 'string' ? value : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setAiFootprintSystemPrompt(prompt: string): Promise<void> {
|
||||||
|
await config.set(CONFIG_KEYS.AI_FOOTPRINT_SYSTEM_PROMPT, prompt)
|
||||||
|
}
|
||||||
|
|||||||
18
src/types/electron.d.ts
vendored
18
src/types/electron.d.ts
vendored
@@ -1075,6 +1075,24 @@ export interface ElectronAPI {
|
|||||||
stop: () => Promise<{ success: boolean }>
|
stop: () => Promise<{ success: boolean }>
|
||||||
status: () => Promise<{ running: boolean; port: number; mediaExportPath: string }>
|
status: () => Promise<{ running: boolean; port: number; mediaExportPath: string }>
|
||||||
}
|
}
|
||||||
|
insight: {
|
||||||
|
testConnection: () => Promise<{ success: boolean; message: string }>
|
||||||
|
getTodayStats: () => Promise<Array<{ sessionId: string; count: number; times: string[] }>>
|
||||||
|
triggerTest: () => Promise<{ success: boolean; message: string }>
|
||||||
|
generateFootprintInsight: (payload: {
|
||||||
|
rangeLabel: string
|
||||||
|
summary: {
|
||||||
|
private_inbound_people?: number
|
||||||
|
private_replied_people?: number
|
||||||
|
private_outbound_people?: number
|
||||||
|
private_reply_rate?: number
|
||||||
|
mention_count?: number
|
||||||
|
mention_group_count?: number
|
||||||
|
}
|
||||||
|
privateSegments?: Array<{ displayName?: string; session_id?: string; incoming_count?: number; outgoing_count?: number; message_count?: number; replied?: boolean }>
|
||||||
|
mentionGroups?: Array<{ displayName?: string; session_id?: string; count?: number }>
|
||||||
|
}) => Promise<{ success: boolean; message: string; insight?: string }>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExportOptions {
|
export interface ExportOptions {
|
||||||
|
|||||||
Reference in New Issue
Block a user