mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-22 15:09:04 +00:00
Merge pull request #22 from Jasonzhu1207/feature/ai-insight-weibo-inline
feat: add experimental Weibo context to AI insights
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import './preload-env'
|
import './preload-env'
|
||||||
import { app, BrowserWindow, ipcMain, nativeTheme, session, Tray, Menu, nativeImage } from 'electron'
|
import { app, BrowserWindow, ipcMain, nativeTheme, session, Tray, Menu, nativeImage } from 'electron'
|
||||||
import { Worker } from 'worker_threads'
|
import { Worker } from 'worker_threads'
|
||||||
import { randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
@@ -31,6 +31,7 @@ import { destroyNotificationWindow, registerNotificationHandlers, showNotificati
|
|||||||
import { httpService } from './services/httpService'
|
import { httpService } from './services/httpService'
|
||||||
import { messagePushService } from './services/messagePushService'
|
import { messagePushService } from './services/messagePushService'
|
||||||
import { insightService } from './services/insightService'
|
import { insightService } from './services/insightService'
|
||||||
|
import { normalizeWeiboCookieInput, weiboService } from './services/social/weiboService'
|
||||||
import { bizService } from './services/bizService'
|
import { bizService } from './services/bizService'
|
||||||
|
|
||||||
// 配置自动更新
|
// 配置自动更新
|
||||||
@@ -1651,6 +1652,32 @@ function registerIpcHandlers() {
|
|||||||
return insightService.generateFootprintInsight(payload)
|
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 () => {
|
ipcMain.handle('config:clear', async () => {
|
||||||
if (isLaunchAtStartupSupported() && getSystemLaunchAtStartup()) {
|
if (isLaunchAtStartupSupported() && getSystemLaunchAtStartup()) {
|
||||||
const result = setSystemLaunchAtStartup(false)
|
const result = setSystemLaunchAtStartup(false)
|
||||||
@@ -3734,3 +3761,7 @@ app.on('window-all-closed', () => {
|
|||||||
app.quit()
|
app.quit()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { contextBridge, ipcRenderer } from 'electron'
|
import { contextBridge, ipcRenderer } from 'electron'
|
||||||
|
|
||||||
// 暴露给渲染进程的 API
|
// 暴露给渲染进程的 API
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
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 }>
|
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 }>
|
mentionGroups?: Array<{ displayName?: string; session_id?: string; count?: number }>
|
||||||
}) => ipcRenderer.invoke('insight:generateFootprintInsight', payload)
|
}) => ipcRenderer.invoke('insight:generateFootprintInsight', payload)
|
||||||
|
},
|
||||||
|
|
||||||
|
social: {
|
||||||
|
saveWeiboCookie: (rawInput: string) => ipcRenderer.invoke('social:saveWeiboCookie', rawInput),
|
||||||
|
validateWeiboUid: (uid: string) => ipcRenderer.invoke('social:validateWeiboUid', uid)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { app, safeStorage } from 'electron'
|
import { app, safeStorage } from 'electron'
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
import Store from 'electron-store'
|
import Store from 'electron-store'
|
||||||
@@ -82,6 +82,7 @@ interface ConfigSchema {
|
|||||||
aiInsightApiModel: string
|
aiInsightApiModel: string
|
||||||
aiInsightSilenceDays: number
|
aiInsightSilenceDays: number
|
||||||
aiInsightAllowContext: boolean
|
aiInsightAllowContext: boolean
|
||||||
|
aiInsightAllowSocialContext: boolean
|
||||||
aiInsightWhitelistEnabled: boolean
|
aiInsightWhitelistEnabled: boolean
|
||||||
aiInsightWhitelist: string[]
|
aiInsightWhitelist: string[]
|
||||||
/** 活跃分析冷却时间(分钟),0 表示无冷却 */
|
/** 活跃分析冷却时间(分钟),0 表示无冷却 */
|
||||||
@@ -113,7 +114,8 @@ const ENCRYPTED_STRING_KEYS: Set<string> = new Set([
|
|||||||
'authPassword',
|
'authPassword',
|
||||||
'httpApiToken',
|
'httpApiToken',
|
||||||
'aiModelApiKey',
|
'aiModelApiKey',
|
||||||
'aiInsightApiKey'
|
'aiInsightApiKey',
|
||||||
|
'aiInsightWeiboCookie'
|
||||||
])
|
])
|
||||||
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'])
|
||||||
@@ -196,15 +198,19 @@ export class ConfigService {
|
|||||||
aiInsightApiModel: 'gpt-4o-mini',
|
aiInsightApiModel: 'gpt-4o-mini',
|
||||||
aiInsightSilenceDays: 3,
|
aiInsightSilenceDays: 3,
|
||||||
aiInsightAllowContext: false,
|
aiInsightAllowContext: false,
|
||||||
|
aiInsightAllowSocialContext: false,
|
||||||
aiInsightWhitelistEnabled: false,
|
aiInsightWhitelistEnabled: false,
|
||||||
aiInsightWhitelist: [],
|
aiInsightWhitelist: [],
|
||||||
aiInsightCooldownMinutes: 120,
|
aiInsightCooldownMinutes: 120,
|
||||||
aiInsightScanIntervalHours: 4,
|
aiInsightScanIntervalHours: 4,
|
||||||
aiInsightContextCount: 40,
|
aiInsightContextCount: 40,
|
||||||
|
aiInsightSocialContextCount: 3,
|
||||||
aiInsightSystemPrompt: '',
|
aiInsightSystemPrompt: '',
|
||||||
aiInsightTelegramEnabled: false,
|
aiInsightTelegramEnabled: false,
|
||||||
aiInsightTelegramToken: '',
|
aiInsightTelegramToken: '',
|
||||||
aiInsightTelegramChatIds: '',
|
aiInsightTelegramChatIds: '',
|
||||||
|
aiInsightWeiboCookie: '',
|
||||||
|
aiInsightWeiboBindings: {},
|
||||||
aiFootprintEnabled: false,
|
aiFootprintEnabled: false,
|
||||||
aiFootprintSystemPrompt: '',
|
aiFootprintSystemPrompt: '',
|
||||||
aiInsightDebugLogEnabled: false
|
aiInsightDebugLogEnabled: false
|
||||||
@@ -825,3 +831,4 @@ export class ConfigService {
|
|||||||
this.unlockPassword = null
|
this.unlockPassword = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/**
|
/**
|
||||||
* insightService.ts
|
* insightService.ts
|
||||||
*
|
*
|
||||||
* AI 见解后台服务:
|
* AI 见解后台服务:
|
||||||
@@ -21,6 +21,7 @@ import { URL } from 'url'
|
|||||||
import { app, Notification } from 'electron'
|
import { app, Notification } from 'electron'
|
||||||
import { ConfigService } from './config'
|
import { ConfigService } from './config'
|
||||||
import { chatService, ChatSession, Message } from './chatService'
|
import { chatService, ChatSession, Message } from './chatService'
|
||||||
|
import { weiboService } from './social/weiboService'
|
||||||
|
|
||||||
// ─── 常量 ────────────────────────────────────────────────────────────────────
|
// ─── 常量 ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -46,6 +47,10 @@ const INSIGHT_CONFIG_KEYS = new Set([
|
|||||||
'aiModelApiBaseUrl',
|
'aiModelApiBaseUrl',
|
||||||
'aiModelApiKey',
|
'aiModelApiKey',
|
||||||
'aiModelApiModel',
|
'aiModelApiModel',
|
||||||
|
'aiInsightAllowSocialContext',
|
||||||
|
'aiInsightSocialContextCount',
|
||||||
|
'aiInsightWeiboCookie',
|
||||||
|
'aiInsightWeiboBindings',
|
||||||
'dbPath',
|
'dbPath',
|
||||||
'decryptKey',
|
'decryptKey',
|
||||||
'myWxid'
|
'myWxid'
|
||||||
@@ -318,6 +323,10 @@ class InsightService {
|
|||||||
if (!INSIGHT_CONFIG_KEYS.has(normalizedKey)) return
|
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') {
|
if (normalizedKey === 'dbPath' || normalizedKey === 'decryptKey' || normalizedKey === 'myWxid') {
|
||||||
this.clearRuntimeCache()
|
this.clearRuntimeCache()
|
||||||
}
|
}
|
||||||
@@ -350,6 +359,7 @@ class InsightService {
|
|||||||
this.lastSeenTimestamp.clear()
|
this.lastSeenTimestamp.clear()
|
||||||
this.todayTriggers.clear()
|
this.todayTriggers.clear()
|
||||||
this.todayDate = getStartOfDay()
|
this.todayDate = getStartOfDay()
|
||||||
|
weiboService.clearCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearTimers(): void {
|
private clearTimers(): void {
|
||||||
@@ -1028,6 +1038,8 @@ ${topMentionText}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const socialContextSection = await this.getSocialContextSection(sessionId)
|
||||||
|
|
||||||
// ── 默认 system prompt(稳定内容,有利于 provider 端 prompt cache 命中)────
|
// ── 默认 system prompt(稳定内容,有利于 provider 端 prompt cache 命中)────
|
||||||
const DEFAULT_SYSTEM_PROMPT = `你是用户的私人关系观察助手,名叫"见解"。你的任务是主动提供有价值的观察和建议。
|
const DEFAULT_SYSTEM_PROMPT = `你是用户的私人关系观察助手,名叫"见解"。你的任务是主动提供有价值的观察和建议。
|
||||||
|
|
||||||
@@ -1190,3 +1202,5 @@ ${topMentionText}
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const insightService = new InsightService()
|
export const insightService = new InsightService()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
273
electron/services/social/weiboService.ts
Normal file
273
electron/services/social/weiboService.ts
Normal file
@@ -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<T>(url: string, options: { cookie: string; referer?: string }): Promise<T> {
|
||||||
|
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<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}=).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 转发:
|
||||||
|
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<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: 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<WeiboRecentPost[]> {
|
||||||
|
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<WeiboWaterFallResponse> {
|
||||||
|
return requestJson<WeiboWaterFallResponse>(
|
||||||
|
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<WeiboStatusShowResponse> {
|
||||||
|
return requestJson<WeiboStatusShowResponse>(
|
||||||
|
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()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
.settings-modal-overlay {
|
.settings-modal-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 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 {
|
@keyframes fadeIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -3541,5 +3615,36 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
max-width: none;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { useAppStore } from '../stores/appStore'
|
import { useAppStore } from '../stores/appStore'
|
||||||
import { useChatStore } from '../stores/chatStore'
|
import { useChatStore } from '../stores/chatStore'
|
||||||
@@ -284,6 +284,17 @@ 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 [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 [aiFootprintEnabled, setAiFootprintEnabled] = useState(false)
|
||||||
const [aiFootprintSystemPrompt, setAiFootprintSystemPrompt] = useState('')
|
const [aiFootprintSystemPrompt, setAiFootprintSystemPrompt] = useState('')
|
||||||
const [aiInsightDebugLogEnabled, setAiInsightDebugLogEnabled] = useState(false)
|
const [aiInsightDebugLogEnabled, setAiInsightDebugLogEnabled] = useState(false)
|
||||||
@@ -527,6 +538,10 @@ 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 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 savedAiFootprintEnabled = await configService.getAiFootprintEnabled()
|
||||||
const savedAiFootprintSystemPrompt = await configService.getAiFootprintSystemPrompt()
|
const savedAiFootprintSystemPrompt = await configService.getAiFootprintSystemPrompt()
|
||||||
const savedAiInsightDebugLogEnabled = await configService.getAiInsightDebugLogEnabled()
|
const savedAiInsightDebugLogEnabled = await configService.getAiInsightDebugLogEnabled()
|
||||||
@@ -546,6 +561,10 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
setAiInsightTelegramEnabled(savedAiInsightTelegramEnabled)
|
setAiInsightTelegramEnabled(savedAiInsightTelegramEnabled)
|
||||||
setAiInsightTelegramToken(savedAiInsightTelegramToken)
|
setAiInsightTelegramToken(savedAiInsightTelegramToken)
|
||||||
setAiInsightTelegramChatIds(savedAiInsightTelegramChatIds)
|
setAiInsightTelegramChatIds(savedAiInsightTelegramChatIds)
|
||||||
|
setAiInsightAllowSocialContext(savedAiInsightAllowSocialContext)
|
||||||
|
setAiInsightSocialContextCount(savedAiInsightSocialContextCount)
|
||||||
|
setAiInsightWeiboCookie(savedAiInsightWeiboCookie)
|
||||||
|
setAiInsightWeiboBindings(savedAiInsightWeiboBindings)
|
||||||
setAiFootprintEnabled(savedAiFootprintEnabled)
|
setAiFootprintEnabled(savedAiFootprintEnabled)
|
||||||
setAiFootprintSystemPrompt(savedAiFootprintSystemPrompt)
|
setAiFootprintSystemPrompt(savedAiFootprintSystemPrompt)
|
||||||
setAiInsightDebugLogEnabled(savedAiInsightDebugLogEnabled)
|
setAiInsightDebugLogEnabled(savedAiInsightDebugLogEnabled)
|
||||||
@@ -1684,6 +1703,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2331,6 +2351,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
const resolvedWhisperModelPath = whisperModelDir || whisperModelStatus?.modelPath || ''
|
const resolvedWhisperModelPath = whisperModelDir || whisperModelStatus?.modelPath || ''
|
||||||
@@ -2438,6 +2459,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2844,9 +2866,127 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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 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<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 = () => (
|
const renderInsightTab = () => (
|
||||||
<div className="tab-content">
|
<div className="tab-content">
|
||||||
{/* 总开关 */}
|
{/* 总开关 */}
|
||||||
@@ -3032,6 +3172,66 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
|
|
||||||
<div className="divider" />
|
<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 */}
|
{/* 自定义 System Prompt */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const DEFAULT_SYSTEM_PROMPT = `你是用户的私人关系观察助手,名叫"见解"。你的任务是主动提供有价值的观察和建议。
|
const DEFAULT_SYSTEM_PROMPT = `你是用户的私人关系观察助手,名叫"见解"。你的任务是主动提供有价值的观察和建议。
|
||||||
@@ -3189,12 +3389,12 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="anti-revoke-tab">
|
<div className="anti-revoke-tab insight-social-tab">
|
||||||
<div className="anti-revoke-hero">
|
<div className="anti-revoke-hero">
|
||||||
<div className="anti-revoke-hero-main">
|
<div className="anti-revoke-hero-main">
|
||||||
<h3>对话白名单</h3>
|
<h3>对话白名单</h3>
|
||||||
<p>
|
<p>
|
||||||
开启后,AI 见解仅对勾选的私聊对话生效,未勾选的对话将被完全忽略。关闭时对所有私聊均生效。
|
开启后,AI 见解仅对勾选的私聊对话生效,未勾选的对话将被完全忽略。关闭时对所有私聊均生效。中间可填写微博 UID。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="anti-revoke-metrics">
|
<div className="anti-revoke-metrics">
|
||||||
@@ -3275,10 +3475,15 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
<>
|
<>
|
||||||
<div className="anti-revoke-list-header">
|
<div className="anti-revoke-list-header">
|
||||||
<span>对话({filteredSessions.length})</span>
|
<span>对话({filteredSessions.length})</span>
|
||||||
|
<span className="insight-social-column-title">社交平台(微博)</span>
|
||||||
<span>状态</span>
|
<span>状态</span>
|
||||||
</div>
|
</div>
|
||||||
{filteredSessions.map((session) => {
|
{filteredSessions.map((session) => {
|
||||||
const isSelected = aiInsightWhitelist.has(session.username)
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
key={session.username}
|
key={session.username}
|
||||||
@@ -3312,6 +3517,46 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
<span className="name">{session.displayName || session.username}</span>
|
<span className="name">{session.displayName || session.username}</span>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</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>
|
||||||
|
) : (
|
||||||
|
<span className="binding-feedback muted">仅支持手动填写数字 UID</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="anti-revoke-row-status">
|
<div className="anti-revoke-row-status">
|
||||||
<span className={`status-badge ${isSelected ? 'installed' : 'not-installed'}`}>
|
<span className={`status-badge ${isSelected ? 'installed' : 'not-installed'}`}>
|
||||||
<i className="status-dot" aria-hidden="true" />
|
<i className="status-dot" aria-hidden="true" />
|
||||||
@@ -3370,6 +3615,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3934,6 +4180,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -4117,6 +4364,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -4311,7 +4559,76 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showWeiboCookieModal && (
|
||||||
|
<div
|
||||||
|
className="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
|
export default SettingsPage
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// 配置服务 - 封装 Electron Store
|
// 配置服务 - 封装 Electron Store
|
||||||
import { config } from './ipc'
|
import { config } from './ipc'
|
||||||
import type { ExportDefaultDateRangeConfig } from '../utils/exportDateRange'
|
import type { ExportDefaultDateRangeConfig } from '../utils/exportDateRange'
|
||||||
|
|
||||||
@@ -94,15 +94,19 @@ export const CONFIG_KEYS = {
|
|||||||
AI_INSIGHT_API_MODEL: 'aiInsightApiModel',
|
AI_INSIGHT_API_MODEL: 'aiInsightApiModel',
|
||||||
AI_INSIGHT_SILENCE_DAYS: 'aiInsightSilenceDays',
|
AI_INSIGHT_SILENCE_DAYS: 'aiInsightSilenceDays',
|
||||||
AI_INSIGHT_ALLOW_CONTEXT: 'aiInsightAllowContext',
|
AI_INSIGHT_ALLOW_CONTEXT: 'aiInsightAllowContext',
|
||||||
|
AI_INSIGHT_ALLOW_SOCIAL_CONTEXT: 'aiInsightAllowSocialContext',
|
||||||
AI_INSIGHT_WHITELIST_ENABLED: 'aiInsightWhitelistEnabled',
|
AI_INSIGHT_WHITELIST_ENABLED: 'aiInsightWhitelistEnabled',
|
||||||
AI_INSIGHT_WHITELIST: 'aiInsightWhitelist',
|
AI_INSIGHT_WHITELIST: 'aiInsightWhitelist',
|
||||||
AI_INSIGHT_COOLDOWN_MINUTES: 'aiInsightCooldownMinutes',
|
AI_INSIGHT_COOLDOWN_MINUTES: 'aiInsightCooldownMinutes',
|
||||||
AI_INSIGHT_SCAN_INTERVAL_HOURS: 'aiInsightScanIntervalHours',
|
AI_INSIGHT_SCAN_INTERVAL_HOURS: 'aiInsightScanIntervalHours',
|
||||||
AI_INSIGHT_CONTEXT_COUNT: 'aiInsightContextCount',
|
AI_INSIGHT_CONTEXT_COUNT: 'aiInsightContextCount',
|
||||||
|
AI_INSIGHT_SOCIAL_CONTEXT_COUNT: 'aiInsightSocialContextCount',
|
||||||
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_INSIGHT_WEIBO_COOKIE: 'aiInsightWeiboCookie',
|
||||||
|
AI_INSIGHT_WEIBO_BINDINGS: 'aiInsightWeiboBindings',
|
||||||
|
|
||||||
// AI 足迹
|
// AI 足迹
|
||||||
AI_FOOTPRINT_ENABLED: 'aiFootprintEnabled',
|
AI_FOOTPRINT_ENABLED: 'aiFootprintEnabled',
|
||||||
@@ -117,6 +121,12 @@ export interface WxidConfig {
|
|||||||
updatedAt?: number
|
updatedAt?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AiInsightWeiboBinding {
|
||||||
|
uid: string
|
||||||
|
screenName?: string
|
||||||
|
updatedAt: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExportDefaultMediaConfig {
|
export interface ExportDefaultMediaConfig {
|
||||||
images: boolean
|
images: boolean
|
||||||
videos: boolean
|
videos: boolean
|
||||||
@@ -1706,6 +1716,15 @@ export async function setAiInsightAllowContext(allow: boolean): Promise<void> {
|
|||||||
await config.set(CONFIG_KEYS.AI_INSIGHT_ALLOW_CONTEXT, allow)
|
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> {
|
export async function getAiInsightWhitelistEnabled(): Promise<boolean> {
|
||||||
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_WHITELIST_ENABLED)
|
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_WHITELIST_ENABLED)
|
||||||
return value === true
|
return value === true
|
||||||
@@ -1751,6 +1770,15 @@ export async function setAiInsightContextCount(count: number): Promise<void> {
|
|||||||
await config.set(CONFIG_KEYS.AI_INSIGHT_CONTEXT_COUNT, count)
|
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> {
|
export async function getAiInsightSystemPrompt(): Promise<string> {
|
||||||
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_SYSTEM_PROMPT)
|
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_SYSTEM_PROMPT)
|
||||||
return typeof value === 'string' ? value : ''
|
return typeof value === 'string' ? value : ''
|
||||||
@@ -1787,6 +1815,25 @@ 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 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> {
|
export async function getAiFootprintEnabled(): Promise<boolean> {
|
||||||
const value = await config.get(CONFIG_KEYS.AI_FOOTPRINT_ENABLED)
|
const value = await config.get(CONFIG_KEYS.AI_FOOTPRINT_ENABLED)
|
||||||
return value === true
|
return value === true
|
||||||
@@ -1813,3 +1860,4 @@ export async function getAiInsightDebugLogEnabled(): Promise<boolean> {
|
|||||||
export async function setAiInsightDebugLogEnabled(enabled: boolean): Promise<void> {
|
export async function setAiInsightDebugLogEnabled(enabled: boolean): Promise<void> {
|
||||||
await config.set(CONFIG_KEYS.AI_INSIGHT_DEBUG_LOG_ENABLED, enabled)
|
await config.set(CONFIG_KEYS.AI_INSIGHT_DEBUG_LOG_ENABLED, enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
23
src/types/electron.d.ts
vendored
23
src/types/electron.d.ts
vendored
@@ -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 {
|
export interface SessionChatWindowOpenOptions {
|
||||||
source?: 'chat' | 'export'
|
source?: 'chat' | 'export'
|
||||||
@@ -7,6 +7,20 @@ export interface SessionChatWindowOpenOptions {
|
|||||||
initialContactType?: ContactInfo['type']
|
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 {
|
export interface ElectronAPI {
|
||||||
window: {
|
window: {
|
||||||
minimize: () => void
|
minimize: () => void
|
||||||
@@ -1075,6 +1089,10 @@ 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 }>
|
||||||
}
|
}
|
||||||
|
social: {
|
||||||
|
saveWeiboCookie: (rawInput: string) => Promise<SocialSaveWeiboCookieResult>
|
||||||
|
validateWeiboUid: (uid: string) => Promise<SocialValidateWeiboUidResult>
|
||||||
|
}
|
||||||
insight: {
|
insight: {
|
||||||
testConnection: () => Promise<{ success: boolean; message: string }>
|
testConnection: () => Promise<{ success: boolean; message: string }>
|
||||||
getTodayStats: () => Promise<Array<{ sessionId: string; count: number; times: string[] }>>
|
getTodayStats: () => Promise<Array<{ sessionId: string; count: number; times: string[] }>>
|
||||||
@@ -1176,3 +1194,6 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { }
|
export { }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user