From 1be03734a4d9b5dd17af50ea020506269008ef25 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 12 Apr 2026 20:53:10 +0800 Subject: [PATCH 1/7] feat: add experimental Weibo context to AI insights --- electron/main.ts | 33 ++- electron/preload.ts | 8 +- electron/services/config.ts | 11 +- electron/services/insightService.ts | 16 +- electron/services/social/weiboService.ts | 273 +++++++++++++++++++ src/pages/SettingsPage.scss | 107 +++++++- src/pages/SettingsPage.tsx | 323 ++++++++++++++++++++++- src/services/config.ts | 50 +++- src/types/electron.d.ts | 23 +- 9 files changed, 833 insertions(+), 11 deletions(-) create mode 100644 electron/services/social/weiboService.ts diff --git a/electron/main.ts b/electron/main.ts index f6a873a..23cbc58 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -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' // 配置自动更新 @@ -1651,6 +1652,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) @@ -3734,3 +3761,7 @@ app.on('window-all-closed', () => { app.quit() } }) + + + + diff --git a/electron/preload.ts b/electron/preload.ts index 838a305..9de862a 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -1,4 +1,4 @@ -import { contextBridge, ipcRenderer } from 'electron' +import { contextBridge, ipcRenderer } from 'electron' // 暴露给渲染进程的 API contextBridge.exposeInMainWorld('electronAPI', { @@ -540,5 +540,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) } }) + diff --git a/electron/services/config.ts b/electron/services/config.ts index 5a6b868..fa1c5fd 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -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 = new Set([ 'authPassword', 'httpApiToken', 'aiModelApiKey', - 'aiInsightApiKey' + 'aiInsightApiKey', + 'aiInsightWeiboCookie' ]) const ENCRYPTED_BOOL_KEYS: Set = new Set(['authEnabled', 'authUseHello']) const ENCRYPTED_NUMBER_KEYS: Set = 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 } } + diff --git a/electron/services/insightService.ts b/electron/services/insightService.ts index 911af51..fb3ab2b 100644 --- a/electron/services/insightService.ts +++ b/electron/services/insightService.ts @@ -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 { @@ -1028,6 +1038,8 @@ ${topMentionText} } } + const socialContextSection = await this.getSocialContextSection(sessionId) + // ── 默认 system prompt(稳定内容,有利于 provider 端 prompt cache 命中)──── const DEFAULT_SYSTEM_PROMPT = `你是用户的私人关系观察助手,名叫"见解"。你的任务是主动提供有价值的观察和建议。 @@ -1190,3 +1202,5 @@ ${topMentionText} } export const insightService = new InsightService() + + diff --git a/electron/services/social/weiboService.ts b/electron/services/social/weiboService.ts new file mode 100644 index 0000000..4b04d70 --- /dev/null +++ b/electron/services/social/weiboService.ts @@ -0,0 +1,273 @@ +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' + +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 +} + +export interface WeiboRecentPost { + id: string + createdAt: string + url: string + text: string + screenName?: string +} + +interface CachedRecentPosts { + expiresAt: number + posts: WeiboRecentPost[] +} + +function requestJson(url: string, options: { cookie: string; referer?: string }): Promise { + return new Promise((resolve, reject) => { + let urlObj: URL + try { + urlObj = new URL(url) + } catch { + reject(new Error(无效的微博请求地址:)) + return + } + + const req = https.request({ + hostname: urlObj.hostname, + port: urlObj.port || 443, + path: urlObj.pathname + urlObj.search, + method: 'GET', + headers: { + Accept: 'application/json, text/plain, */*', + Referer: options.referer || 'https://weibo.com', + 'User-Agent': WEIBO_USER_AGENT, + 'X-Requested-With': 'XMLHttpRequest', + Cookie: options.cookie + } + }, (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(微博接口返回异常状态码 )) + 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() + 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}=).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): string { + const baseText = sanitizeWeiboText(item.text_raw || '') + const retweetText = sanitizeWeiboText(item.retweeted_status?.text_raw || '') + if (!retweetText) return baseText + if (!baseText || baseText === '转发微博') return 转发: + return ${baseText}\n\n转发内容: +} + +function buildCacheKey(uid: string, count: number, cookie: string): string { + const cookieHash = createHash('sha1').update(cookie).digest('hex') + return ${uid}:: +} + +class WeiboService { + private recentPostsCache = new Map() + + 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: false, error: '请先填写有效的微博 Cookie' } + + const timeline = await this.fetchTimeline(uid, cookie) + const firstItem = timeline.data?.list?.[0] + if (!firstItem) { + return { success: false, error: '该微博账号暂无可读取的近期公开内容,或当前 Cookie 已失效' } + } + const screenName = firstItem.user?.screen_name + return { success: true, uid, screenName } + } catch (error) { + return { success: false, error: (error as Error).message || '微博 UID 校验失败' } + } + } + + async fetchRecentPosts(uidInput: string, cookieInput: string, requestedCount: number): Promise { + const uid = normalizeWeiboUid(uidInput) + const cookie = normalizeWeiboCookieInput(cookieInput) + if (!cookie) return [] + + const count = Math.max(1, Math.min(WEIBO_MAX_POSTS, Math.floor(Number(requestedCount) || 0))) + const cacheKey = buildCacheKey(uid, count, cookie) + const cached = this.recentPostsCache.get(cacheKey) + const now = Date.now() + if (cached && cached.expiresAt > now) return cached.posts + + const timeline = await this.fetchTimeline(uid, cookie) + const rawItems = Array.isArray(timeline.data?.list) ? timeline.data.list : [] + 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) { + 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/, + 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 { + return requestJson( + https://weibo.com/ajax/profile/getWaterFallContent?uid=, + { cookie, referer: https://weibo.com/u/ } + ).then((response) => { + if (response.ok !== 1 || !Array.isArray(response.data?.list)) { + throw new Error('微博时间线获取失败,请检查 Cookie 是否仍然有效') + } + return response + }) + } + + private fetchDetail(id: string, cookie: string): Promise { + return requestJson( + https://weibo.com/ajax/statuses/show?id=&isGetLongText=true, + { cookie, referer: https://weibo.com/detail/ } + ).then((response) => { + if (!response || (!response.id && !response.idstr)) { + throw new Error('微博详情获取失败') + } + return response + }) + } +} + +export const weiboService = new WeiboService() diff --git a/src/pages/SettingsPage.scss b/src/pages/SettingsPage.scss index ac35a22..fe20d9f 100644 --- a/src/pages/SettingsPage.scss +++ b/src/pages/SettingsPage.scss @@ -1,4 +1,4 @@ -.settings-modal-overlay { +.settings-modal-overlay { position: fixed; top: 0; left: 0; @@ -1918,6 +1918,80 @@ } } +.settings-inline-modal { + width: min(560px, calc(100vw - 40px)); + 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 { + padding: 20px 24px; + + .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; @@ -3541,5 +3615,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; + } + } } } + + diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index b62f101..6a23857 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -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' @@ -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>({}) + const [showWeiboCookieModal, setShowWeiboCookieModal] = useState(false) + const [weiboCookieDraft, setWeiboCookieDraft] = useState('') + const [weiboCookieError, setWeiboCookieError] = useState('') + const [isSavingWeiboCookie, setIsSavingWeiboCookie] = useState(false) + const [weiboBindingDrafts, setWeiboBindingDrafts] = useState>({}) + const [weiboBindingErrors, setWeiboBindingErrors] = useState>({}) + const [weiboBindingLoadingSessionId, setWeiboBindingLoadingSessionId] = useState(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 = {}) { + ) @@ -2331,6 +2351,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { + ) const resolvedWhisperModelPath = whisperModelDir || whisperModelStatus?.modelPath || '' @@ -2438,6 +2459,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { + ) @@ -2844,9 +2866,127 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { )} + ) + const hasWeiboCookieConfigured = aiInsightWeiboCookie.trim().length > 0 + + const openWeiboCookieModal = () => { + setWeiboCookieDraft(aiInsightWeiboCookie) + setWeiboCookieError('') + setShowWeiboCookieModal(true) + } + + const persistWeiboCookieDraft = async (draftOverride?: string): Promise => { + const draftToSave = draftOverride ?? weiboCookieDraft + if (draftToSave === aiInsightWeiboCookie) return true + setIsSavingWeiboCookie(true) + setWeiboCookieError('') + try { + const result = await window.electronAPI.social.saveWeiboCookie(draftToSave) + 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) => { + if (!hasWeiboCookieConfigured) { + setWeiboBindingErrors((prev) => ({ ...prev, [sessionId]: '请先填写微博 Cookie,再进行 UID 绑定' })) + return + } + 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 window.electronAPI.social.validateWeiboUid(draftUid) + if (!result.success || !result.uid) { + setWeiboBindingErrors((prev) => ({ ...prev, [sessionId]: result.error || '微博 UID 校验失败' })) + return + } + + const nextBindings: Record = { + ...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 = () => (
{/* 总开关 */} @@ -3032,6 +3172,66 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
+
+ + + 当前仅支持微博,且仅对已手动绑定微博 UID 的联系人生效。为了控制资源占用和平台风控,程序只会在触发见解时按需抓取近期公开内容,不会做后台持续扫描。 + +
+ {aiInsightAllowSocialContext ? '已开启' : '已关闭'} +
+ + {hasWeiboCookieConfigured ? '微博 Cookie 已配置' : '微博 Cookie 未配置'} + + + +
+
+ {!hasWeiboCookieConfigured && ( + + 未配置微博 Cookie 时,开启后也不会发送社交平台内容。 + + )} +
+ + {aiInsightAllowSocialContext && ( +
+ + + 当前仅支持微博最近发帖。 +
+ 不建议超过 5,避免触发平台风控。 +
+ { + const val = Math.max(1, Math.min(5, parseInt(e.target.value, 10) || 3)) + setAiInsightSocialContextCount(val) + scheduleConfigSave('aiInsightSocialContextCount', () => configService.setAiInsightSocialContextCount(val)) + }} + style={{ width: 100 }} + /> +
+ )} + +
{/* 自定义 System Prompt */} {(() => { const DEFAULT_SYSTEM_PROMPT = `你是用户的私人关系观察助手,名叫"见解"。你的任务是主动提供有价值的观察和建议。 @@ -3189,12 +3389,12 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { } return ( -
+

对话白名单

- 开启后,AI 见解仅对勾选的私聊对话生效,未勾选的对话将被完全忽略。关闭时对所有私聊均生效。 + 开启后,AI 见解仅对勾选的私聊对话生效,未勾选的对话将被完全忽略。关闭时对所有私聊均生效。中间可填写微博 UID。

@@ -3275,10 +3475,15 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { <>
对话({filteredSessions.length}) + 社交平台(微博) 状态
{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 (
{session.displayName || session.username}
+
+
+ 微博 + updateWeiboBindingDraft(session.username, e.target.value)} + /> +
+
+ + {weiboBinding && ( + + )} +
+
+ {weiboBindingError ? ( + {weiboBindingError} + ) : weiboBinding?.screenName ? ( + @{weiboBinding.screenName} + ) : ( + 仅支持手动填写数字 UID + )} +
+