This commit is contained in:
cc
2026-04-15 23:57:40 +08:00
9 changed files with 1155 additions and 50 deletions

View File

@@ -1,4 +1,4 @@
import './preload-env'
import './preload-env'
import { app, BrowserWindow, ipcMain, nativeTheme, session, Tray, Menu, nativeImage } from 'electron'
import { Worker } from 'worker_threads'
import { randomUUID } from 'crypto'
@@ -31,6 +31,7 @@ import { destroyNotificationWindow, registerNotificationHandlers, showNotificati
import { httpService } from './services/httpService'
import { messagePushService } from './services/messagePushService'
import { insightService } from './services/insightService'
import { normalizeWeiboCookieInput, weiboService } from './services/social/weiboService'
import { bizService } from './services/bizService'
// 配置自动更新
@@ -1660,6 +1661,32 @@ function registerIpcHandlers() {
return insightService.generateFootprintInsight(payload)
})
ipcMain.handle('social:saveWeiboCookie', async (_, rawInput: string) => {
try {
if (!configService) {
return { success: false, error: 'Config service is not initialized' }
}
const normalized = normalizeWeiboCookieInput(rawInput)
configService.set('aiInsightWeiboCookie' as any, normalized as any)
weiboService.clearCache()
return { success: true, normalized, hasCookie: Boolean(normalized) }
} catch (error) {
return { success: false, error: (error as Error).message || 'Failed to save Weibo cookie' }
}
})
ipcMain.handle('social:validateWeiboUid', async (_, uid: string) => {
try {
if (!configService) {
return { success: false, error: 'Config service is not initialized' }
}
const cookie = String(configService.get('aiInsightWeiboCookie' as any) || '')
return await weiboService.validateUid(uid, cookie)
} catch (error) {
return { success: false, error: (error as Error).message || 'Failed to validate Weibo UID' }
}
})
ipcMain.handle('config:clear', async () => {
if (isLaunchAtStartupSupported() && getSystemLaunchAtStartup()) {
const result = setSystemLaunchAtStartup(false)
@@ -3866,3 +3893,7 @@ app.on('window-all-closed', () => {
app.quit()
}
})

View File

@@ -1,4 +1,4 @@
import { contextBridge, ipcRenderer } from 'electron'
import { contextBridge, ipcRenderer } from 'electron'
// 暴露给渲染进程的 API
contextBridge.exposeInMainWorld('electronAPI', {
@@ -557,5 +557,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
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)
},
social: {
saveWeiboCookie: (rawInput: string) => ipcRenderer.invoke('social:saveWeiboCookie', rawInput),
validateWeiboUid: (uid: string) => ipcRenderer.invoke('social:validateWeiboUid', uid)
}
})

View File

@@ -1,4 +1,4 @@
import { join } from 'path'
import { join } from 'path'
import { app, safeStorage } from 'electron'
import crypto from 'crypto'
import Store from 'electron-store'
@@ -82,6 +82,7 @@ interface ConfigSchema {
aiInsightApiModel: string
aiInsightSilenceDays: number
aiInsightAllowContext: boolean
aiInsightAllowSocialContext: boolean
aiInsightWhitelistEnabled: boolean
aiInsightWhitelist: string[]
/** 活跃分析冷却时间分钟0 表示无冷却 */
@@ -113,7 +114,8 @@ const ENCRYPTED_STRING_KEYS: Set<string> = new Set([
'authPassword',
'httpApiToken',
'aiModelApiKey',
'aiInsightApiKey'
'aiInsightApiKey',
'aiInsightWeiboCookie'
])
const ENCRYPTED_BOOL_KEYS: Set<string> = new Set(['authEnabled', 'authUseHello'])
const ENCRYPTED_NUMBER_KEYS: Set<string> = new Set(['imageXorKey'])
@@ -196,15 +198,19 @@ export class ConfigService {
aiInsightApiModel: 'gpt-4o-mini',
aiInsightSilenceDays: 3,
aiInsightAllowContext: false,
aiInsightAllowSocialContext: false,
aiInsightWhitelistEnabled: false,
aiInsightWhitelist: [],
aiInsightCooldownMinutes: 120,
aiInsightScanIntervalHours: 4,
aiInsightContextCount: 40,
aiInsightSocialContextCount: 3,
aiInsightSystemPrompt: '',
aiInsightTelegramEnabled: false,
aiInsightTelegramToken: '',
aiInsightTelegramChatIds: '',
aiInsightWeiboCookie: '',
aiInsightWeiboBindings: {},
aiFootprintEnabled: false,
aiFootprintSystemPrompt: '',
aiInsightDebugLogEnabled: false
@@ -825,3 +831,4 @@ export class ConfigService {
this.unlockPassword = null
}
}

View File

@@ -1,4 +1,4 @@
/**
/**
* insightService.ts
*
* AI 见解后台服务:
@@ -21,6 +21,7 @@ import { URL } from 'url'
import { app, Notification } from 'electron'
import { ConfigService } from './config'
import { chatService, ChatSession, Message } from './chatService'
import { weiboService } from './social/weiboService'
// ─── 常量 ────────────────────────────────────────────────────────────────────
@@ -46,6 +47,10 @@ const INSIGHT_CONFIG_KEYS = new Set([
'aiModelApiBaseUrl',
'aiModelApiKey',
'aiModelApiModel',
'aiInsightAllowSocialContext',
'aiInsightSocialContextCount',
'aiInsightWeiboCookie',
'aiInsightWeiboBindings',
'dbPath',
'decryptKey',
'myWxid'
@@ -318,6 +323,10 @@ class InsightService {
if (!INSIGHT_CONFIG_KEYS.has(normalizedKey)) return
// 数据库相关配置变更后,丢弃缓存并强制下次重连
if (normalizedKey === 'aiInsightAllowSocialContext' || normalizedKey === 'aiInsightSocialContextCount' || normalizedKey === 'aiInsightWeiboCookie' || normalizedKey === 'aiInsightWeiboBindings') {
weiboService.clearCache()
}
if (normalizedKey === 'dbPath' || normalizedKey === 'decryptKey' || normalizedKey === 'myWxid') {
this.clearRuntimeCache()
}
@@ -350,6 +359,7 @@ class InsightService {
this.lastSeenTimestamp.clear()
this.todayTriggers.clear()
this.todayDate = getStartOfDay()
weiboService.clearCache()
}
private clearTimers(): void {
@@ -786,6 +796,50 @@ ${topMentionText}
return total
}
private formatWeiboTimestamp(raw: string): string {
const parsed = Date.parse(String(raw || ''))
if (!Number.isFinite(parsed)) {
return String(raw || '').trim()
}
return new Date(parsed).toLocaleString('zh-CN')
}
private async getSocialContextSection(sessionId: string): Promise<string> {
const allowSocialContext = this.config.get('aiInsightAllowSocialContext') === true
if (!allowSocialContext) return ''
const rawCookie = String(this.config.get('aiInsightWeiboCookie') || '').trim()
const hasCookie = rawCookie.length > 0
const bindings =
(this.config.get('aiInsightWeiboBindings') as Record<string, { uid?: string; screenName?: string }> | undefined) || {}
const binding = bindings[sessionId]
const uid = String(binding?.uid || '').trim()
if (!uid) return ''
const socialCountRaw = Number(this.config.get('aiInsightSocialContextCount') || 3)
const socialCount = Math.max(1, Math.min(5, Math.floor(socialCountRaw) || 3))
try {
const posts = await weiboService.fetchRecentPosts(uid, rawCookie, socialCount)
if (posts.length === 0) return ''
const lines = posts.map((post) => {
const time = this.formatWeiboTimestamp(post.createdAt)
const text = post.text.length > 180 ? `${post.text.slice(0, 180)}...` : post.text
return `[微博 ${time}] ${text}`
})
insightLog('INFO', `已加载 ${lines.length} 条微博公开内容 (uid=${uid})`)
const riskHint = hasCookie
? ''
: '\n提示未配置微博 Cookie使用移动端公开接口抓取可能因平台风控导致获取失败或内容较少。'
return `近期公开社交平台内容(来源:微博,最近 ${lines.length} 条):\n${lines.join('\n')}${riskHint}`
} catch (error) {
insightLog('WARN', `拉取微博公开内容失败 (uid=${uid}): ${(error as Error).message}`)
return ''
}
}
// ── 沉默联系人扫描 ──────────────────────────────────────────────────────────
private scheduleSilenceScan(): void {
@@ -1028,6 +1082,8 @@ ${topMentionText}
}
}
const socialContextSection = await this.getSocialContextSection(sessionId)
// ── 默认 system prompt稳定内容有利于 provider 端 prompt cache 命中)────
const DEFAULT_SYSTEM_PROMPT = `你是用户的私人关系观察助手,名叫"见解"。你的任务是主动提供有价值的观察和建议。
@@ -1060,6 +1116,7 @@ ${topMentionText}
`时间统计:${todayStatsDesc}`,
`全局统计:${globalStatsDesc}`,
contextSection,
socialContextSection,
'请给出你的见解≤80字'
].filter(Boolean).join('\n\n')
@@ -1190,3 +1247,5 @@ ${topMentionText}
}
export const insightService = new InsightService()

View File

@@ -0,0 +1,367 @@
import https from 'https'
import { createHash } from 'crypto'
import { URL } from 'url'
const WEIBO_TIMEOUT_MS = 10_000
const WEIBO_MAX_POSTS = 5
const WEIBO_CACHE_TTL_MS = 30 * 60 * 1000
const WEIBO_USER_AGENT =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36'
const WEIBO_MOBILE_USER_AGENT =
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1'
interface BrowserCookieEntry {
domain?: string
name?: string
value?: string
}
interface WeiboUserInfo {
id?: number | string
screen_name?: string
}
interface WeiboWaterFallItem {
id?: number | string
idstr?: string
mblogid?: string
created_at?: string
text_raw?: string
isLongText?: boolean
user?: WeiboUserInfo
retweeted_status?: WeiboWaterFallItem
}
interface WeiboWaterFallResponse {
ok?: number
data?: {
list?: WeiboWaterFallItem[]
next_cursor?: string
}
}
interface WeiboStatusShowResponse {
id?: number | string
idstr?: string
mblogid?: string
created_at?: string
text_raw?: string
user?: WeiboUserInfo
retweeted_status?: WeiboWaterFallItem
}
interface MWeiboCard {
mblog?: WeiboWaterFallItem
card_group?: MWeiboCard[]
}
interface MWeiboContainerResponse {
ok?: number
data?: {
cards?: MWeiboCard[]
}
}
export interface WeiboRecentPost {
id: string
createdAt: string
url: string
text: string
screenName?: string
}
interface CachedRecentPosts {
expiresAt: number
posts: WeiboRecentPost[]
}
function requestJson<T>(url: string, options: { cookie?: string; referer?: string; userAgent?: string }): Promise<T> {
return new Promise((resolve, reject) => {
let urlObj: URL
try {
urlObj = new URL(url)
} catch {
reject(new Error(`无效的微博请求地址:${url}`))
return
}
const headers: Record<string, string> = {
Accept: 'application/json, text/plain, */*',
Referer: options.referer || 'https://weibo.com',
'User-Agent': options.userAgent || WEIBO_USER_AGENT,
'X-Requested-With': 'XMLHttpRequest'
}
if (options.cookie) {
headers.Cookie = options.cookie
}
const req = https.request(
{
hostname: urlObj.hostname,
port: urlObj.port || 443,
path: urlObj.pathname + urlObj.search,
method: 'GET',
headers
},
(res) => {
let raw = ''
res.setEncoding('utf8')
res.on('data', (chunk) => {
raw += chunk
})
res.on('end', () => {
const statusCode = res.statusCode || 0
if (statusCode < 200 || statusCode >= 300) {
reject(new Error(`微博接口返回异常状态码 ${statusCode}`))
return
}
try {
resolve(JSON.parse(raw) as T)
} catch {
reject(new Error('微博接口返回了非 JSON 响应'))
}
})
}
)
req.setTimeout(WEIBO_TIMEOUT_MS, () => {
req.destroy()
reject(new Error('微博请求超时'))
})
req.on('error', reject)
req.end()
})
}
function normalizeCookieArray(entries: BrowserCookieEntry[]): string {
const picked = new Map<string, string>()
for (const entry of entries) {
const name = String(entry?.name || '').trim()
const value = String(entry?.value || '').trim()
const domain = String(entry?.domain || '').trim().toLowerCase()
if (!name || !value) continue
if (domain && !domain.includes('weibo.com') && !domain.includes('weibo.cn')) continue
picked.set(name, value)
}
return Array.from(picked.entries())
.map(([name, value]) => `${name}=${value}`)
.join('; ')
}
export function normalizeWeiboCookieInput(rawInput: string): string {
const trimmed = String(rawInput || '').trim()
if (!trimmed) return ''
try {
const parsed = JSON.parse(trimmed) as unknown
if (Array.isArray(parsed)) {
const normalized = normalizeCookieArray(parsed as BrowserCookieEntry[])
if (normalized) return normalized
throw new Error('Cookie JSON 中未找到可用的微博 Cookie 项')
}
} catch (error) {
if (!(error instanceof SyntaxError)) {
throw error
}
}
return trimmed.replace(/^Cookie:\s*/i, '').trim()
}
function normalizeWeiboUid(input: string): string {
const trimmed = String(input || '').trim()
const directMatch = trimmed.match(/^\d{5,}$/)
if (directMatch) return directMatch[0]
const linkMatch = trimmed.match(/(?:weibo\.com|m\.weibo\.cn)\/u\/(\d{5,})/i)
if (linkMatch) return linkMatch[1]
throw new Error('请输入有效的微博 UID纯数字')
}
function sanitizeWeiboText(text: string): string {
return String(text || '')
.replace(/\u200b|\u200c|\u200d|\ufeff/g, '')
.replace(/https?:\/\/t\.cn\/[A-Za-z0-9]+/g, ' ')
.replace(/ +/g, ' ')
.replace(/\n{3,}/g, '\n\n')
.trim()
}
function mergeRetweetText(item: Pick<WeiboWaterFallItem, 'text_raw' | 'retweeted_status'>): string {
const baseText = sanitizeWeiboText(item.text_raw || '')
const retweetText = sanitizeWeiboText(item.retweeted_status?.text_raw || '')
if (!retweetText) return baseText
if (!baseText || baseText === '转发微博') return `转发:${retweetText}`
return `${baseText}\n\n转发内容${retweetText}`
}
function buildCacheKey(uid: string, count: number, cookie: string): string {
const cookieHash = createHash('sha1').update(cookie).digest('hex')
return `${uid}:${count}:${cookieHash}`
}
class WeiboService {
private recentPostsCache = new Map<string, CachedRecentPosts>()
clearCache(): void {
this.recentPostsCache.clear()
}
async validateUid(
uidInput: string,
cookieInput: string
): Promise<{ success: boolean; uid?: string; screenName?: string; error?: string }> {
try {
const uid = normalizeWeiboUid(uidInput)
const cookie = normalizeWeiboCookieInput(cookieInput)
if (!cookie) {
return { success: true, uid }
}
const timeline = await this.fetchTimeline(uid, cookie)
const firstItem = timeline.data?.list?.[0]
if (!firstItem) {
return { success: false, error: '该微博账号暂无可读取的近期公开内容,或当前 Cookie 已失效' }
}
return {
success: true,
uid,
screenName: firstItem.user?.screen_name
}
} catch (error) {
return {
success: false,
error: (error as Error).message || '微博 UID 校验失败'
}
}
}
async fetchRecentPosts(
uidInput: string,
cookieInput: string,
requestedCount: number
): Promise<WeiboRecentPost[]> {
const uid = normalizeWeiboUid(uidInput)
const cookie = normalizeWeiboCookieInput(cookieInput)
const hasCookie = Boolean(cookie)
const count = Math.max(1, Math.min(WEIBO_MAX_POSTS, Math.floor(Number(requestedCount) || 0)))
const cacheKey = buildCacheKey(uid, count, hasCookie ? cookie : '__no_cookie_mobile__')
const cached = this.recentPostsCache.get(cacheKey)
const now = Date.now()
if (cached && cached.expiresAt > now) {
return cached.posts
}
const rawItems = hasCookie
? (await this.fetchTimeline(uid, cookie)).data?.list || []
: await this.fetchMobileTimeline(uid)
const posts: WeiboRecentPost[] = []
for (const item of rawItems) {
if (posts.length >= count) break
const id = String(item.idstr || item.id || '').trim()
if (!id) continue
let text = mergeRetweetText(item)
if (item.isLongText && hasCookie) {
try {
const detail = await this.fetchDetail(id, cookie)
text = mergeRetweetText(detail)
} catch {
// 长文补抓失败时回退到列表摘要
}
}
text = sanitizeWeiboText(text)
if (!text) continue
posts.push({
id,
createdAt: String(item.created_at || ''),
url: `https://m.weibo.cn/detail/${id}`,
text,
screenName: item.user?.screen_name
})
}
this.recentPostsCache.set(cacheKey, {
expiresAt: now + WEIBO_CACHE_TTL_MS,
posts
})
return posts
}
private fetchTimeline(uid: string, cookie: string): Promise<WeiboWaterFallResponse> {
return requestJson<WeiboWaterFallResponse>(
`https://weibo.com/ajax/profile/getWaterFallContent?uid=${encodeURIComponent(uid)}`,
{
cookie,
referer: `https://weibo.com/u/${encodeURIComponent(uid)}`
}
).then((response) => {
if (response.ok !== 1 || !Array.isArray(response.data?.list)) {
throw new Error('微博时间线获取失败,请检查 Cookie 是否仍然有效')
}
return response
})
}
private fetchMobileTimeline(uid: string): Promise<WeiboWaterFallItem[]> {
const containerid = `107603${uid}`
return requestJson<MWeiboContainerResponse>(
`https://m.weibo.cn/api/container/getIndex?type=uid&value=${encodeURIComponent(uid)}&containerid=${encodeURIComponent(containerid)}`,
{
referer: `https://m.weibo.cn/u/${encodeURIComponent(uid)}`,
userAgent: WEIBO_MOBILE_USER_AGENT
}
).then((response) => {
if (response.ok !== 1 || !Array.isArray(response.data?.cards)) {
throw new Error('微博时间线获取失败,请稍后重试')
}
const rows: WeiboWaterFallItem[] = []
for (const card of response.data.cards) {
if (card?.mblog) rows.push(card.mblog)
if (Array.isArray(card?.card_group)) {
for (const subCard of card.card_group) {
if (subCard?.mblog) rows.push(subCard.mblog)
}
}
}
if (rows.length === 0) {
throw new Error('该微博账号暂无可读取的近期公开内容')
}
return rows
})
}
private fetchDetail(id: string, cookie: string): Promise<WeiboStatusShowResponse> {
return requestJson<WeiboStatusShowResponse>(
`https://weibo.com/ajax/statuses/show?id=${encodeURIComponent(id)}&isGetLongText=true`,
{
cookie,
referer: `https://weibo.com/detail/${encodeURIComponent(id)}`
}
).then((response) => {
if (!response || (!response.id && !response.idstr)) {
throw new Error('微博详情获取失败')
}
return response
})
}
}
export const weiboService = new WeiboService()

View File

@@ -1,4 +1,4 @@
.settings-modal-overlay {
.settings-modal-overlay {
position: fixed;
top: 0;
left: 0;
@@ -1849,6 +1849,20 @@
animation: fadeIn 0.2s ease;
}
.social-cookie-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.52);
display: flex;
align-items: center;
justify-content: center;
z-index: 2300;
animation: fadeIn 0.2s ease;
}
// API 警告弹窗
.api-warning-modal {
width: 420px;
@@ -1918,6 +1932,85 @@
}
}
.settings-inline-modal {
width: min(560px, calc(100vw - 40px));
max-height: min(720px, calc(100vh - 40px));
display: flex;
flex-direction: column;
background: var(--bg-primary);
border-radius: 16px;
overflow: hidden;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);
animation: slideUp 0.25s ease;
.modal-header {
display: flex;
align-items: center;
gap: 10px;
padding: 20px 24px;
border-bottom: 1px solid var(--border-color);
svg {
color: var(--primary);
}
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
}
.modal-body {
flex: 1;
padding: 20px 24px;
overflow-y: auto;
.warning-text {
margin: 0 0 16px;
font-size: 14px;
color: var(--text-secondary);
line-height: 1.6;
}
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 16px 24px;
border-top: 1px solid var(--border-color);
background: var(--bg-secondary);
}
}
.social-cookie-textarea {
width: 100%;
min-height: 220px;
resize: vertical;
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 12px 14px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 12px;
line-height: 1.6;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;
margin-top: 10px;
}
.social-inline-error {
margin-top: 10px;
border-radius: 10px;
padding: 10px 12px;
font-size: 12px;
line-height: 1.5;
color: color-mix(in srgb, var(--danger) 72%, var(--text-primary) 28%);
background: color-mix(in srgb, var(--danger) 8%, var(--bg-secondary));
border: 1px solid color-mix(in srgb, var(--danger) 24%, var(--border-color));
}
@keyframes fadeIn {
from {
opacity: 0;
@@ -3506,6 +3599,103 @@
overflow: hidden;
}
&.insight-social-tab {
.anti-revoke-list-header {
grid-template-columns: minmax(0, 1fr) minmax(300px, 420px) auto;
.insight-social-column-title {
color: var(--text-tertiary);
}
}
.anti-revoke-row {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(300px, 420px) auto;
align-items: center;
gap: 14px;
}
.anti-revoke-row-main {
min-width: 0;
}
.insight-social-binding-cell {
min-width: 0;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px 10px;
align-items: center;
}
.insight-social-binding-input-wrap {
min-width: 0;
display: flex;
align-items: center;
gap: 8px;
}
.binding-platform-chip {
flex-shrink: 0;
border-radius: 999px;
padding: 2px 8px;
font-size: 11px;
color: var(--text-secondary);
border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent);
background: color-mix(in srgb, var(--bg-secondary) 88%, var(--bg-primary) 12%);
}
.insight-social-binding-input {
width: 100%;
min-width: 0;
height: 30px;
border-radius: 8px;
border: 1px solid var(--border-color);
background: color-mix(in srgb, var(--bg-primary) 92%, var(--bg-secondary) 8%);
color: var(--text-primary);
font-size: 12px;
padding: 0 10px;
outline: none;
transition: border-color 0.18s ease, box-shadow 0.18s ease;
&:focus {
border-color: color-mix(in srgb, var(--primary) 55%, var(--border-color));
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 16%, transparent);
}
}
.insight-social-binding-actions {
display: inline-flex;
align-items: center;
justify-self: flex-end;
gap: 8px;
}
.insight-social-binding-feedback {
grid-column: 1 / span 2;
min-height: 18px;
}
.binding-feedback {
font-size: 12px;
line-height: 1.4;
color: var(--text-secondary);
&.error {
color: color-mix(in srgb, var(--danger) 72%, var(--text-primary) 28%);
}
&.muted {
color: var(--text-tertiary);
}
}
.anti-revoke-row-status {
justify-self: flex-end;
align-items: flex-end;
max-width: none;
}
}
@media (max-width: 980px) {
.anti-revoke-hero {
flex-direction: column;
@@ -3541,5 +3731,36 @@
justify-content: space-between;
max-width: none;
}
&.insight-social-tab {
.anti-revoke-list-header {
grid-template-columns: minmax(0, 1fr) auto;
.insight-social-column-title {
display: none;
}
}
.anti-revoke-row {
display: flex;
align-items: flex-start;
flex-direction: column;
}
.insight-social-binding-cell,
.anti-revoke-row-status {
width: 100%;
}
.insight-social-binding-cell {
grid-template-columns: 1fr;
}
.insight-social-binding-feedback {
grid-column: 1;
}
}
}
}

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from 'react'
import { useState, useEffect, useRef } from 'react'
import { useLocation } from 'react-router-dom'
import { useAppStore } from '../stores/appStore'
import { useChatStore } from '../stores/chatStore'
@@ -48,7 +48,7 @@ const tabs: { id: Exclude<SettingsTab, 'insight' | 'aiFootprint'>; label: string
]
const aiTabs: Array<{ id: Extract<SettingsTab, 'aiCommon' | 'insight' | 'aiFootprint'>; label: string }> = [
{ id: 'aiCommon', label: 'AI 通用' },
{ id: 'aiCommon', label: '基础配置' },
{ id: 'insight', label: 'AI 见解' },
{ id: 'aiFootprint', label: 'AI 足迹' }
]
@@ -284,6 +284,17 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
const [aiInsightTelegramEnabled, setAiInsightTelegramEnabled] = useState(false)
const [aiInsightTelegramToken, setAiInsightTelegramToken] = useState('')
const [aiInsightTelegramChatIds, setAiInsightTelegramChatIds] = useState('')
const [aiInsightAllowSocialContext, setAiInsightAllowSocialContext] = useState(false)
const [aiInsightSocialContextCount, setAiInsightSocialContextCount] = useState(3)
const [aiInsightWeiboCookie, setAiInsightWeiboCookie] = useState('')
const [aiInsightWeiboBindings, setAiInsightWeiboBindings] = useState<Record<string, configService.AiInsightWeiboBinding>>({})
const [showWeiboCookieModal, setShowWeiboCookieModal] = useState(false)
const [weiboCookieDraft, setWeiboCookieDraft] = useState('')
const [weiboCookieError, setWeiboCookieError] = useState('')
const [isSavingWeiboCookie, setIsSavingWeiboCookie] = useState(false)
const [weiboBindingDrafts, setWeiboBindingDrafts] = useState<Record<string, string>>({})
const [weiboBindingErrors, setWeiboBindingErrors] = useState<Record<string, string>>({})
const [weiboBindingLoadingSessionId, setWeiboBindingLoadingSessionId] = useState<string | null>(null)
const [aiFootprintEnabled, setAiFootprintEnabled] = useState(false)
const [aiFootprintSystemPrompt, setAiFootprintSystemPrompt] = useState('')
const [aiInsightDebugLogEnabled, setAiInsightDebugLogEnabled] = useState(false)
@@ -527,6 +538,10 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
const savedAiInsightTelegramEnabled = await configService.getAiInsightTelegramEnabled()
const savedAiInsightTelegramToken = await configService.getAiInsightTelegramToken()
const savedAiInsightTelegramChatIds = await configService.getAiInsightTelegramChatIds()
const savedAiInsightAllowSocialContext = await configService.getAiInsightAllowSocialContext()
const savedAiInsightSocialContextCount = await configService.getAiInsightSocialContextCount()
const savedAiInsightWeiboCookie = await configService.getAiInsightWeiboCookie()
const savedAiInsightWeiboBindings = await configService.getAiInsightWeiboBindings()
const savedAiFootprintEnabled = await configService.getAiFootprintEnabled()
const savedAiFootprintSystemPrompt = await configService.getAiFootprintSystemPrompt()
const savedAiInsightDebugLogEnabled = await configService.getAiInsightDebugLogEnabled()
@@ -546,6 +561,10 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
setAiInsightTelegramEnabled(savedAiInsightTelegramEnabled)
setAiInsightTelegramToken(savedAiInsightTelegramToken)
setAiInsightTelegramChatIds(savedAiInsightTelegramChatIds)
setAiInsightAllowSocialContext(savedAiInsightAllowSocialContext)
setAiInsightSocialContextCount(savedAiInsightSocialContextCount)
setAiInsightWeiboCookie(savedAiInsightWeiboCookie)
setAiInsightWeiboBindings(savedAiInsightWeiboBindings)
setAiFootprintEnabled(savedAiFootprintEnabled)
setAiFootprintSystemPrompt(savedAiFootprintSystemPrompt)
setAiInsightDebugLogEnabled(savedAiInsightDebugLogEnabled)
@@ -1684,6 +1703,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
</div>
</div>
</div>
</div>
)
@@ -2331,6 +2351,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
</button>
</div>
</div>
</div>
)
const resolvedWhisperModelPath = whisperModelDir || whisperModelStatus?.modelPath || ''
@@ -2438,6 +2459,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
</label>
</div>
</div>
</div>
)
@@ -2844,9 +2866,145 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
)}
</div>
</div>
</div>
)
const withAsyncTimeout = async <T,>(task: Promise<T>, timeoutMs: number, timeoutMessage: string): Promise<T> => {
let timeoutHandle: ReturnType<typeof setTimeout> | null = null
try {
return await Promise.race([
task,
new Promise<T>((_, reject) => {
timeoutHandle = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs)
})
])
} finally {
if (timeoutHandle) clearTimeout(timeoutHandle)
}
}
const hasWeiboCookieConfigured = aiInsightWeiboCookie.trim().length > 0
const openWeiboCookieModal = () => {
setWeiboCookieDraft(aiInsightWeiboCookie)
setWeiboCookieError('')
setShowWeiboCookieModal(true)
}
const persistWeiboCookieDraft = async (draftOverride?: string): Promise<boolean> => {
const draftToSave = draftOverride ?? weiboCookieDraft
if (draftToSave === aiInsightWeiboCookie) return true
setIsSavingWeiboCookie(true)
setWeiboCookieError('')
try {
const result = await withAsyncTimeout(
window.electronAPI.social.saveWeiboCookie(draftToSave),
10000,
'保存微博 Cookie 超时,请稍后重试'
)
if (!result.success) {
setWeiboCookieError(result.error || '微博 Cookie 保存失败')
return false
}
const normalized = result.normalized || ''
setAiInsightWeiboCookie(normalized)
setWeiboCookieDraft(normalized)
showMessage(result.hasCookie ? '微博 Cookie 已保存' : '微博 Cookie 已清空', true)
return true
} catch (e: any) {
setWeiboCookieError(e?.message || String(e))
return false
} finally {
setIsSavingWeiboCookie(false)
}
}
const handleCloseWeiboCookieModal = async (discard = false) => {
if (discard) {
setShowWeiboCookieModal(false)
setWeiboCookieDraft(aiInsightWeiboCookie)
setWeiboCookieError('')
return
}
const ok = await persistWeiboCookieDraft()
if (!ok) return
setShowWeiboCookieModal(false)
setWeiboCookieError('')
}
const getWeiboBindingDraftValue = (sessionId: string): string => {
const draft = weiboBindingDrafts[sessionId]
if (draft !== undefined) return draft
return aiInsightWeiboBindings[sessionId]?.uid || ''
}
const updateWeiboBindingDraft = (sessionId: string, value: string) => {
setWeiboBindingDrafts((prev) => ({
...prev,
[sessionId]: value
}))
setWeiboBindingErrors((prev) => {
if (!prev[sessionId]) return prev
const next = { ...prev }
delete next[sessionId]
return next
})
}
const handleSaveWeiboBinding = async (sessionId: string, displayName: string) => {
const draftUid = getWeiboBindingDraftValue(sessionId)
setWeiboBindingLoadingSessionId(sessionId)
setWeiboBindingErrors((prev) => {
if (!prev[sessionId]) return prev
const next = { ...prev }
delete next[sessionId]
return next
})
try {
const result = await withAsyncTimeout(
window.electronAPI.social.validateWeiboUid(draftUid),
12000,
'微博 UID 校验超时,请稍后重试'
)
if (!result.success || !result.uid) {
setWeiboBindingErrors((prev) => ({ ...prev, [sessionId]: result.error || '微博 UID 校验失败' }))
return
}
const nextBindings: Record<string, configService.AiInsightWeiboBinding> = {
...aiInsightWeiboBindings,
[sessionId]: {
uid: result.uid,
screenName: result.screenName,
updatedAt: Date.now()
}
}
setAiInsightWeiboBindings(nextBindings)
await configService.setAiInsightWeiboBindings(nextBindings)
setWeiboBindingDrafts((prev) => ({ ...prev, [sessionId]: result.uid! }))
showMessage(`已为「${displayName}」绑定微博 UID`, true)
} catch (e: any) {
setWeiboBindingErrors((prev) => ({ ...prev, [sessionId]: e?.message || String(e) }))
} finally {
setWeiboBindingLoadingSessionId(null)
}
}
const handleClearWeiboBinding = async (sessionId: string, silent = false) => {
const nextBindings = { ...aiInsightWeiboBindings }
delete nextBindings[sessionId]
setAiInsightWeiboBindings(nextBindings)
setWeiboBindingDrafts((prev) => ({ ...prev, [sessionId]: '' }))
setWeiboBindingErrors((prev) => {
if (!prev[sessionId]) return prev
const next = { ...prev }
delete next[sessionId]
return next
})
await configService.setAiInsightWeiboBindings(nextBindings)
if (!silent) showMessage('已清除微博绑定', true)
}
const renderInsightTab = () => (
<div className="tab-content">
{/* 总开关 */}
@@ -2878,7 +3036,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
<div className="form-group">
<label></label>
<span className="form-hint">
AI API
API
</span>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap', marginTop: '10px' }}>
<button
@@ -3032,6 +3190,66 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
<div className="divider" />
<div className="form-group">
<label></label>
<span className="form-hint">
UID
</span>
<div className="log-toggle-line">
<span className="log-status">{aiInsightAllowSocialContext ? '已开启' : '已关闭'}</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
<span style={{ fontSize: 12, color: hasWeiboCookieConfigured ? 'var(--color-success, #22c55e)' : 'var(--text-tertiary)' }}>
{hasWeiboCookieConfigured ? '微博 Cookie 已配置' : '微博 Cookie 未配置'}
</span>
<button className="btn btn-secondary btn-sm" type="button" onClick={openWeiboCookieModal}>
{hasWeiboCookieConfigured ? '编辑微博 Cookie' : '填写微博 Cookie'}
</button>
<label className="switch">
<input
type="checkbox"
checked={aiInsightAllowSocialContext}
onChange={async (e) => {
const val = e.target.checked
setAiInsightAllowSocialContext(val)
await configService.setAiInsightAllowSocialContext(val)
}}
/>
<span className="switch-slider" />
</label>
</div>
</div>
{!hasWeiboCookieConfigured && (
<span className="form-hint" style={{ marginTop: 8, display: 'block' }}>
Cookie
</span>
)}
</div>
{aiInsightAllowSocialContext && (
<div className="form-group">
<label></label>
<span className="form-hint">
<br />
<strong> 5</strong>
</span>
<input
type="number"
className="field-input"
value={aiInsightSocialContextCount}
min={1}
max={5}
onChange={(e) => {
const val = Math.max(1, Math.min(5, parseInt(e.target.value, 10) || 3))
setAiInsightSocialContextCount(val)
scheduleConfigSave('aiInsightSocialContextCount', () => configService.setAiInsightSocialContextCount(val))
}}
style={{ width: 100 }}
/>
</div>
)}
<div className="divider" />
{/* 自定义 System Prompt */}
{(() => {
const DEFAULT_SYSTEM_PROMPT = `你是用户的私人关系观察助手,名叫"见解"。你的任务是主动提供有价值的观察和建议。
@@ -3050,8 +3268,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 }}>
<label style={{ marginBottom: 0 }}> AI </label>
<button
className="button-secondary"
style={{ fontSize: 12, padding: '3px 10px' }}
className="btn btn-secondary btn-sm"
onClick={async () => {
// 恢复默认清空自定义值UI 回到显示默认内容的状态
setAiInsightSystemPrompt('')
@@ -3189,12 +3406,12 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
}
return (
<div className="anti-revoke-tab">
<div className="anti-revoke-tab insight-social-tab">
<div className="anti-revoke-hero">
<div className="anti-revoke-hero-main">
<h3></h3>
<p>
AI
AI UID
</p>
</div>
<div className="anti-revoke-metrics">
@@ -3275,10 +3492,15 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
<>
<div className="anti-revoke-list-header">
<span>{filteredSessions.length}</span>
<span className="insight-social-column-title"></span>
<span></span>
</div>
{filteredSessions.map((session) => {
const isSelected = aiInsightWhitelist.has(session.username)
const weiboBinding = aiInsightWeiboBindings[session.username]
const weiboDraftValue = getWeiboBindingDraftValue(session.username)
const isBindingLoading = weiboBindingLoadingSessionId === session.username
const weiboBindingError = weiboBindingErrors[session.username]
return (
<div
key={session.username}
@@ -3312,6 +3534,48 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
<span className="name">{session.displayName || session.username}</span>
</div>
</label>
<div className="insight-social-binding-cell">
<div className="insight-social-binding-input-wrap">
<span className="binding-platform-chip"></span>
<input
type="text"
className="insight-social-binding-input"
value={weiboDraftValue}
placeholder="填写数字 UID"
onChange={(e) => updateWeiboBindingDraft(session.username, e.target.value)}
/>
</div>
<div className="insight-social-binding-actions">
<button
type="button"
className="btn btn-secondary btn-sm"
onClick={() => void handleSaveWeiboBinding(session.username, session.displayName || session.username)}
disabled={isBindingLoading || !weiboDraftValue.trim()}
>
{isBindingLoading ? '绑定中...' : (weiboBinding ? '更新' : '绑定')}
</button>
{weiboBinding && (
<button
type="button"
className="btn btn-secondary btn-sm"
onClick={() => void handleClearWeiboBinding(session.username)}
>
</button>
)}
</div>
<div className="insight-social-binding-feedback">
{weiboBindingError ? (
<span className="binding-feedback error">{weiboBindingError}</span>
) : weiboBinding?.screenName ? (
<span className="binding-feedback">@{weiboBinding.screenName}</span>
) : weiboBinding?.uid ? (
<span className="binding-feedback"> UID{weiboBinding.uid}</span>
) : (
<span className="binding-feedback muted"> UID</span>
)}
</div>
</div>
<div className="anti-revoke-row-status">
<span className={`status-badge ${isSelected ? 'installed' : 'not-installed'}`}>
<i className="status-dot" aria-hidden="true" />
@@ -3370,6 +3634,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
</label>
</div>
</div>
</div>
)
@@ -3410,8 +3675,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
<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' }}
className="btn btn-secondary btn-sm"
onClick={async () => {
setAiFootprintSystemPrompt('')
await configService.setAiFootprintSystemPrompt('')
@@ -3934,6 +4198,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
</div>
</div>
</div>
</div>
)
@@ -4117,6 +4382,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
</label>
</div>
</div>
</div>
)
@@ -4197,6 +4463,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
}
return (
<>
<div className={`settings-modal-overlay ${isClosing ? 'closing' : ''}`} onClick={handleClose}>
<div className={`settings-page ${isClosing ? 'closing' : ''}`} onClick={(event) => event.stopPropagation()}>
{message && <div className={`message-toast ${message.success ? 'success' : 'error'}`}>{message.text}</div>}
@@ -4253,43 +4520,51 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
<div className="settings-layout">
<div className="settings-tabs" role="tablist" aria-label="设置项">
{tabs.map(tab => (
<button
key={tab.id}
className={`tab-btn ${activeTab === tab.id ? 'active' : ''}`}
onClick={() => setActiveTab(tab.id)}
>
<tab.icon size={16} />
<span>{tab.label}</span>
</button>
))}
{tabs.flatMap((tab) => {
const row: React.ReactNode[] = [
<button
key={tab.id}
className={`tab-btn ${activeTab === tab.id ? 'active' : ''}`}
onClick={() => setActiveTab(tab.id)}
>
<tab.icon size={16} />
<span>{tab.label}</span>
</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) => (
if (tab.id === 'analytics') {
row.push(
<div key="ai-settings-group" className={`tab-group ${aiGroupExpanded ? 'expanded' : ''}`}>
<button
key={tab.id}
className={`tab-btn tab-sub-btn ${activeTab === tab.id ? 'active' : ''}`}
onClick={() => setActiveTab(tab.id)}
tabIndex={aiGroupExpanded ? 0 : -1}
className={`tab-btn tab-group-trigger ${(activeTab === 'aiCommon' || activeTab === 'insight' || activeTab === 'aiFootprint') ? 'active' : ''}`}
onClick={() => setAiGroupExpanded((prev) => !prev)}
aria-expanded={aiGroupExpanded}
>
<span className="tab-sub-dot" />
<span>{tab.label}</span>
<Sparkles size={16} />
<span>AI </span>
<ChevronDown size={14} className={`tab-group-arrow ${aiGroupExpanded ? 'expanded' : ''}`} />
</button>
))}
</div>
</div>
</div>
<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>
)
}
return row
})}
</div>
<div className="settings-body">
@@ -4311,7 +4586,77 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
</div>
</div>
</div>
{showWeiboCookieModal && (
<div
className="social-cookie-modal-overlay"
onClick={(e) => {
e.stopPropagation()
void handleCloseWeiboCookieModal()
}}
>
<div className="settings-inline-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<Globe size={20} />
<h3> Cookie</h3>
</div>
<div className="modal-body">
<p className="warning-text">
Cookie JSON <code>name=value</code>
</p>
<textarea
className="social-cookie-textarea"
value={weiboCookieDraft}
placeholder="粘贴微博 Cookie关闭弹层时自动保存"
onChange={(e) => {
setWeiboCookieDraft(e.target.value)
setWeiboCookieError('')
}}
/>
{weiboCookieError && (
<div className="social-inline-error">{weiboCookieError}</div>
)}
</div>
<div className="modal-footer">
<button className="btn btn-secondary" onClick={() => void handleCloseWeiboCookieModal(true)}>
</button>
<button
className="btn btn-secondary"
onClick={async () => {
setWeiboCookieDraft('')
const ok = await persistWeiboCookieDraft('')
if (ok) setShowWeiboCookieModal(false)
}}
disabled={isSavingWeiboCookie || !aiInsightWeiboCookie}
>
</button>
<button className="btn btn-primary" onClick={() => { void handleCloseWeiboCookieModal() }} disabled={isSavingWeiboCookie}>
{isSavingWeiboCookie ? '保存中...' : '关闭并保存'}
</button>
</div>
</div>
</div>
)}
</>
)
}
export default SettingsPage

View File

@@ -1,4 +1,4 @@
// 配置服务 - 封装 Electron Store
// 配置服务 - 封装 Electron Store
import { config } from './ipc'
import type { ExportDefaultDateRangeConfig } from '../utils/exportDateRange'
import type { ExportAutomationTask } from '../types/exportAutomation'
@@ -95,15 +95,19 @@ export const CONFIG_KEYS = {
AI_INSIGHT_API_MODEL: 'aiInsightApiModel',
AI_INSIGHT_SILENCE_DAYS: 'aiInsightSilenceDays',
AI_INSIGHT_ALLOW_CONTEXT: 'aiInsightAllowContext',
AI_INSIGHT_ALLOW_SOCIAL_CONTEXT: 'aiInsightAllowSocialContext',
AI_INSIGHT_WHITELIST_ENABLED: 'aiInsightWhitelistEnabled',
AI_INSIGHT_WHITELIST: 'aiInsightWhitelist',
AI_INSIGHT_COOLDOWN_MINUTES: 'aiInsightCooldownMinutes',
AI_INSIGHT_SCAN_INTERVAL_HOURS: 'aiInsightScanIntervalHours',
AI_INSIGHT_CONTEXT_COUNT: 'aiInsightContextCount',
AI_INSIGHT_SOCIAL_CONTEXT_COUNT: 'aiInsightSocialContextCount',
AI_INSIGHT_SYSTEM_PROMPT: 'aiInsightSystemPrompt',
AI_INSIGHT_TELEGRAM_ENABLED: 'aiInsightTelegramEnabled',
AI_INSIGHT_TELEGRAM_TOKEN: 'aiInsightTelegramToken',
AI_INSIGHT_TELEGRAM_CHAT_IDS: 'aiInsightTelegramChatIds',
AI_INSIGHT_WEIBO_COOKIE: 'aiInsightWeiboCookie',
AI_INSIGHT_WEIBO_BINDINGS: 'aiInsightWeiboBindings',
// AI 足迹
AI_FOOTPRINT_ENABLED: 'aiFootprintEnabled',
@@ -118,6 +122,12 @@ export interface WxidConfig {
updatedAt?: number
}
export interface AiInsightWeiboBinding {
uid: string
screenName?: string
updatedAt: number
}
export interface ExportDefaultMediaConfig {
images: boolean
videos: boolean
@@ -1882,6 +1892,15 @@ export async function setAiInsightAllowContext(allow: boolean): Promise<void> {
await config.set(CONFIG_KEYS.AI_INSIGHT_ALLOW_CONTEXT, allow)
}
export async function getAiInsightAllowSocialContext(): Promise<boolean> {
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_ALLOW_SOCIAL_CONTEXT)
return value === true
}
export async function setAiInsightAllowSocialContext(allow: boolean): Promise<void> {
await config.set(CONFIG_KEYS.AI_INSIGHT_ALLOW_SOCIAL_CONTEXT, allow)
}
export async function getAiInsightWhitelistEnabled(): Promise<boolean> {
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_WHITELIST_ENABLED)
return value === true
@@ -1927,6 +1946,15 @@ export async function setAiInsightContextCount(count: number): Promise<void> {
await config.set(CONFIG_KEYS.AI_INSIGHT_CONTEXT_COUNT, count)
}
export async function getAiInsightSocialContextCount(): Promise<number> {
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_SOCIAL_CONTEXT_COUNT)
return typeof value === 'number' && value > 0 ? value : 3
}
export async function setAiInsightSocialContextCount(count: number): Promise<void> {
await config.set(CONFIG_KEYS.AI_INSIGHT_SOCIAL_CONTEXT_COUNT, count)
}
export async function getAiInsightSystemPrompt(): Promise<string> {
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_SYSTEM_PROMPT)
return typeof value === 'string' ? value : ''
@@ -1963,6 +1991,25 @@ export async function setAiInsightTelegramChatIds(chatIds: string): Promise<void
await config.set(CONFIG_KEYS.AI_INSIGHT_TELEGRAM_CHAT_IDS, chatIds)
}
export async function getAiInsightWeiboCookie(): Promise<string> {
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_WEIBO_COOKIE)
return typeof value === 'string' ? value : ''
}
export async function setAiInsightWeiboCookie(cookieValue: string): Promise<void> {
await config.set(CONFIG_KEYS.AI_INSIGHT_WEIBO_COOKIE, cookieValue)
}
export async function getAiInsightWeiboBindings(): Promise<Record<string, AiInsightWeiboBinding>> {
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_WEIBO_BINDINGS)
if (!value || typeof value !== 'object') return {}
return value as Record<string, AiInsightWeiboBinding>
}
export async function setAiInsightWeiboBindings(bindings: Record<string, AiInsightWeiboBinding>): Promise<void> {
await config.set(CONFIG_KEYS.AI_INSIGHT_WEIBO_BINDINGS, bindings)
}
export async function getAiFootprintEnabled(): Promise<boolean> {
const value = await config.get(CONFIG_KEYS.AI_FOOTPRINT_ENABLED)
return value === true
@@ -1989,3 +2036,4 @@ export async function getAiInsightDebugLogEnabled(): Promise<boolean> {
export async function setAiInsightDebugLogEnabled(enabled: boolean): Promise<void> {
await config.set(CONFIG_KEYS.AI_INSIGHT_DEBUG_LOG_ENABLED, enabled)
}

View File

@@ -1,4 +1,4 @@
import type { ChatSession, Message, Contact, ContactInfo, ChatRecordItem } from './models'
import type { ChatSession, Message, Contact, ContactInfo, ChatRecordItem } from './models'
export interface SessionChatWindowOpenOptions {
source?: 'chat' | 'export'
@@ -7,6 +7,20 @@ export interface SessionChatWindowOpenOptions {
initialContactType?: ContactInfo['type']
}
export interface SocialValidateWeiboUidResult {
success: boolean
uid?: string
screenName?: string
error?: string
}
export interface SocialSaveWeiboCookieResult {
success: boolean
normalized?: string
hasCookie?: boolean
error?: string
}
export interface ElectronAPI {
window: {
minimize: () => void
@@ -1091,6 +1105,10 @@ export interface ElectronAPI {
stop: () => Promise<{ success: boolean }>
status: () => Promise<{ running: boolean; port: number; mediaExportPath: string }>
}
social: {
saveWeiboCookie: (rawInput: string) => Promise<SocialSaveWeiboCookieResult>
validateWeiboUid: (uid: string) => Promise<SocialValidateWeiboUidResult>
}
insight: {
testConnection: () => Promise<{ success: boolean; message: string }>
getTodayStats: () => Promise<Array<{ sessionId: string; count: number; times: string[] }>>
@@ -1191,3 +1209,6 @@ declare global {
}
export { }