feat: add experimental Weibo context to AI insights

This commit is contained in:
Jason
2026-04-12 20:53:10 +08:00
parent 9c5426159d
commit 1be03734a4
9 changed files with 833 additions and 11 deletions

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'
@@ -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,127 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
)}
</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 = () => (
<div className="tab-content">
{/* 总开关 */}
@@ -3032,6 +3172,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 = `你是用户的私人关系观察助手,名叫"见解"。你的任务是主动提供有价值的观察和建议。
@@ -3189,12 +3389,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 +3475,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 +3517,46 @@ 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>
) : (
<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 +3615,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
</label>
</div>
</div>
</div>
)
@@ -3934,6 +4180,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
</div>
</div>
</div>
</div>
)
@@ -4117,6 +4364,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
</label>
</div>
</div>
</div>
)
@@ -4311,7 +4559,76 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
</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