diff --git a/electron/services/config.ts b/electron/services/config.ts index 16ece13..9e38931 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -86,7 +86,13 @@ interface ConfigSchema { aiInsightApiModel: string aiInsightSilenceDays: number aiInsightAllowContext: boolean + aiInsightAllowMomentsContext: boolean + aiInsightMomentsContextCount: number + aiInsightMomentsBindings: Record aiInsightAllowSocialContext: boolean + aiInsightSocialContextCount: number + aiInsightWeiboCookie: string + aiInsightWeiboBindings: Record aiInsightFilterMode: 'whitelist' | 'blacklist' aiInsightFilterList: string[] aiInsightWhitelistEnabled: boolean @@ -268,6 +274,9 @@ export class ConfigService { aiInsightApiModel: 'gpt-4o-mini', aiInsightSilenceDays: 3, aiInsightAllowContext: false, + aiInsightAllowMomentsContext: false, + aiInsightMomentsContextCount: 5, + aiInsightMomentsBindings: {}, aiInsightAllowSocialContext: false, aiInsightFilterMode: 'whitelist', aiInsightFilterList: [], diff --git a/electron/services/insightService.ts b/electron/services/insightService.ts index 0566571..5554a29 100644 --- a/electron/services/insightService.ts +++ b/electron/services/insightService.ts @@ -10,7 +10,7 @@ * 设计原则: * - 不引入任何额外 npm 依赖,使用 Node 原生 https 模块调用 OpenAI 兼容 API * - 所有失败静默处理,不影响主流程 - * - 当日触发记录(sessionId + 时间列表)随 prompt 一起发送,让模型自行判断是否克制 + * - 触发频率、冷却与名单过滤均在本地完成,不把调度统计塞进模型 prompt */ import https from 'https' @@ -21,6 +21,7 @@ import { URL } from 'url' import { app, Notification } from 'electron' import { ConfigService } from './config' import { chatService, ChatSession, Message } from './chatService' +import { snsService } from './snsService' import { weiboService } from './social/weiboService' // ─── 常量 ──────────────────────────────────────────────────────────────────── @@ -52,6 +53,9 @@ const INSIGHT_CONFIG_KEYS = new Set([ 'aiModelApiMaxTokens', 'aiInsightFilterMode', 'aiInsightFilterList', + 'aiInsightAllowMomentsContext', + 'aiInsightMomentsContextCount', + 'aiInsightMomentsBindings', 'aiInsightAllowSocialContext', 'aiInsightSocialContextCount', 'aiInsightWeiboCookie', @@ -445,7 +449,7 @@ class InsightService { try { const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions') - const requestMessages = [{ role: 'user', content: appendPromptCurrentTime('请回复"连接成功"四个字。') }] + const requestMessages = [{ role: 'user', content: '请回复"连接成功"四个字。' }] insightDebugSection( 'INFO', 'AI 测试连接请求', @@ -823,26 +827,13 @@ ${topMentionText} } /** - * 记录触发并返回该会话今日所有触发时间(用于组装 prompt)。 + * 记录成功推送的见解,用于设置页展示今日触发统计。 */ - private recordTrigger(sessionId: string): string[] { + private recordTrigger(sessionId: string): void { this.resetIfNewDay() const existing = this.todayTriggers.get(sessionId) ?? { timestamps: [] } existing.timestamps.push(Date.now()) this.todayTriggers.set(sessionId, existing) - return existing.timestamps.map(formatTimestamp) - } - - /** - * 获取今日全局已触发次数(所有会话合计),用于 prompt 中告知模型全局上下文。 - */ - private getTodayTotalTriggerCount(): number { - this.resetIfNewDay() - let total = 0 - for (const record of this.todayTriggers.values()) { - total += record.timestamps.length - } - return total } private formatWeiboTimestamp(raw: string): string { @@ -853,12 +844,66 @@ ${topMentionText} return new Date(parsed).toLocaleString('zh-CN') } + private formatMomentsTimestamp(raw: unknown): string { + const numeric = Number(raw) + if (!Number.isFinite(numeric) || numeric <= 0) { + return '' + } + const ms = numeric > 1_000_000_000_000 ? numeric : numeric * 1000 + return new Date(ms).toLocaleString('zh-CN') + } + + private extractMomentReadableText(post: { contentDesc?: unknown; linkTitle?: unknown }): string { + const contentDesc = this.normalizeInsightText(String(post.contentDesc || '')).replace(/\s+/g, ' ').trim() + if (contentDesc) return contentDesc + + const linkTitle = this.normalizeInsightText(String(post.linkTitle || '')).replace(/\s+/g, ' ').trim() + if (linkTitle) return `[链接] ${linkTitle}` + + return '' + } + + private async getMomentsContextSection(sessionId: string): Promise { + const allowMomentsContext = this.config.get('aiInsightAllowMomentsContext') === true + if (!allowMomentsContext) return '' + + const bindings = + (this.config.get('aiInsightMomentsBindings') as Record | undefined) || {} + const isEnabledForSession = bindings[sessionId]?.enabled === true + if (!isEnabledForSession) return '' + + const countRaw = Number(this.config.get('aiInsightMomentsContextCount') || 5) + const momentsCount = Math.max(1, Math.min(20, Math.floor(countRaw) || 5)) + + try { + const result = await snsService.getTimeline(momentsCount, 0, [sessionId]) + const posts = result.success && Array.isArray(result.timeline) ? result.timeline : [] + if (posts.length === 0) return '' + + const lines = posts + .map((post) => { + const text = this.extractMomentReadableText(post as { contentDesc?: unknown; linkTitle?: unknown }) + if (!text) return '' + const shortText = text.length > 180 ? `${text.slice(0, 180)}...` : text + const time = this.formatMomentsTimestamp((post as { createTime?: unknown }).createTime) + return time ? `[朋友圈 ${time}] ${shortText}` : `[朋友圈] ${shortText}` + }) + .filter(Boolean) as string[] + + if (lines.length === 0) return '' + insightLog('INFO', `已加载 ${lines.length} 条朋友圈内容 (sessionId=${sessionId})`) + return `近期朋友圈内容(最近 ${lines.length} 条):\n${lines.join('\n')}` + } catch (error) { + insightLog('WARN', `拉取朋友圈内容失败 (sessionId=${sessionId}): ${(error as Error).message}`) + return '' + } + } + private async getSocialContextSection(sessionId: string): Promise { const allowSocialContext = this.config.get('aiInsightAllowSocialContext') === true if (!allowSocialContext) return '' const rawCookie = String(this.config.get('aiInsightWeiboCookie') || '').trim() - const hasCookie = rawCookie.length > 0 const bindings = (this.config.get('aiInsightWeiboBindings') as Record | undefined) || {} @@ -879,10 +924,7 @@ ${topMentionText} return `[微博 ${time}] ${text}` }) insightLog('INFO', `已加载 ${lines.length} 条微博公开内容 (uid=${uid})`) - const riskHint = hasCookie - ? '' - : '\n提示:未配置微博 Cookie,使用移动端公开接口抓取,可能因平台风控导致获取失败或内容较少。' - return `近期公开社交平台内容(来源:微博,最近 ${lines.length} 条):\n${lines.join('\n')}${riskHint}` + return `近期公开社交平台内容(来源:微博,最近 ${lines.length} 条):\n${lines.join('\n')}` } catch (error) { insightLog('WARN', `拉取微博公开内容失败 (uid=${uid}): ${(error as Error).message}`) return '' @@ -1118,10 +1160,6 @@ ${topMentionText} // ── 构建 prompt ──────────────────────────────────────────────────────────── - // 今日触发统计(让模型具备时间与克制感) - const sessionTriggerTimes = this.recordTrigger(sessionId) - const totalTodayTriggers = this.getTodayTotalTriggerCount() - let contextSection = '' if (allowContext) { try { @@ -1136,6 +1174,7 @@ ${topMentionText} } } + const momentsContextSection = await this.getMomentsContextSection(sessionId) const socialContextSection = await this.getSocialContextSection(sessionId) // ── 默认 system prompt(稳定内容,有利于 provider 端 prompt cache 命中)──── @@ -1151,25 +1190,12 @@ ${topMentionText} const customPrompt = (this.config.get('aiInsightSystemPrompt') as string) || '' const systemPrompt = customPrompt.trim() || DEFAULT_SYSTEM_PROMPT - // 可变的上下文统计信息放在 user message 里,保持 system prompt 稳定不变 - // 这样 provider 端(Anthropic/OpenAI)能最大化命中 prompt cache,降低费用 - const triggerDesc = - triggerReason === 'silence' - ? `你已经 ${silentDays} 天没有和「${resolvedDisplayName}」聊天了。` - : `你最近和「${resolvedDisplayName}」有新的聊天动态。` - - const todayStatsDesc = - sessionTriggerTimes.length > 1 - ? `今天你已经针对「${resolvedDisplayName}」收到过 ${sessionTriggerTimes.length - 1} 条见解(时间:${sessionTriggerTimes.slice(0, -1).join('、')}),请适当克制。` - : `今天你还没有针对「${resolvedDisplayName}」发出过见解。` - - const globalStatsDesc = `今天全部联系人合计已触发 ${totalTodayTriggers} 条见解。` - const userPromptBase = [ - `触发原因:${triggerDesc}`, - `时间统计:${todayStatsDesc}`, - `全局统计:${globalStatsDesc}`, + triggerReason === 'silence' && silentDays + ? `已 ${silentDays} 天未联系「${resolvedDisplayName}」。` + : '', contextSection, + momentsContextSection, socialContextSection, '请给出你的见解(≤80字):' ].filter(Boolean).join('\n\n') @@ -1189,7 +1215,7 @@ ${topMentionText} `接口地址:${endpoint}`, `模型:${model}`, `Max Tokens:${maxTokens}`, - `触发原因:${triggerReason}`, + `触发类型:${triggerReason}`, `上下文开关:${allowContext ? '开启' : '关闭'}`, `上下文条数:${contextCount}`, '', @@ -1253,6 +1279,7 @@ ${topMentionText} } insightLog('INFO', `已为 ${resolvedDisplayName} 推送见解`) + this.recordTrigger(sessionId) } catch (e) { insightDebugSection( 'ERROR', diff --git a/src/pages/SettingsPage.scss b/src/pages/SettingsPage.scss index 76188e1..ecf1d8d 100644 --- a/src/pages/SettingsPage.scss +++ b/src/pages/SettingsPage.scss @@ -915,6 +915,31 @@ color: var(--text-secondary); } +.insight-collapsible-setting { + max-height: 0; + opacity: 0; + overflow: hidden; + transform: translate3d(0, -4px, 0); + contain: layout paint; + will-change: max-height, opacity, transform; + transition: max-height 0.2s ease, opacity 0.18s ease, transform 0.2s ease; + + &.expanded { + max-height: 128px; + opacity: 1; + transform: translate3d(0, 0, 0); + } + + &.collapsed { + pointer-events: none; + } +} + +.insight-collapsible-setting-inner { + padding-top: 2px; + backface-visibility: hidden; +} + /* Premium Switch Style */ .switch { position: relative; @@ -3616,17 +3641,35 @@ } &.insight-social-tab { + --insight-moments-column-width: 76px; + --insight-social-column-width: minmax(220px, 300px); + --insight-status-column-width: 82px; + --insight-social-list-grid: minmax(0, 1fr) var(--insight-moments-column-width) var(--insight-social-column-width) var(--insight-status-column-width); + .anti-revoke-list-header { - grid-template-columns: minmax(0, 1fr) minmax(300px, 420px) auto; + grid-template-columns: var(--insight-social-list-grid); + gap: 14px; + + .insight-moments-column-title { + display: flex; + justify-content: center; + color: var(--text-tertiary); + } .insight-social-column-title { + min-width: 0; + color: var(--text-tertiary); + } + + .anti-revoke-status-column-title { + justify-self: end; color: var(--text-tertiary); } } .anti-revoke-row { display: grid; - grid-template-columns: minmax(0, 1fr) minmax(300px, 420px) auto; + grid-template-columns: var(--insight-social-list-grid); align-items: center; gap: 14px; } @@ -3635,6 +3678,67 @@ min-width: 0; } + .insight-moments-cell { + min-width: 0; + display: flex; + align-items: center; + justify-content: center; + min-height: 30px; + } + + .insight-moments-toggle { + position: relative; + width: 18px; + height: 18px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + + input[type='checkbox'] { + position: absolute; + inset: 0; + margin: 0; + opacity: 0; + cursor: pointer; + } + + .check-indicator { + width: 100%; + height: 100%; + border-radius: 6px; + border: 1px solid color-mix(in srgb, var(--border-color) 78%, var(--primary) 22%); + background: color-mix(in srgb, var(--bg-primary) 86%, var(--bg-secondary) 14%); + color: var(--on-primary, #fff); + display: inline-flex; + align-items: center; + justify-content: center; + transition: all 0.16s ease; + + svg { + opacity: 0; + transform: scale(0.75); + transition: opacity 0.16s ease, transform 0.16s ease; + } + } + + input[type='checkbox']:checked + .check-indicator { + background: var(--primary); + border-color: var(--primary); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent); + + svg { + opacity: 1; + transform: scale(1); + } + } + + input[type='checkbox']:focus-visible + .check-indicator { + outline: 2px solid color-mix(in srgb, var(--primary) 42%, transparent); + outline-offset: 1px; + } + } + .insight-social-binding-cell { min-width: 0; display: grid; @@ -3653,7 +3757,7 @@ .binding-platform-chip { flex-shrink: 0; border-radius: 999px; - padding: 2px 8px; + padding: 2px 7px; font-size: 11px; color: var(--text-secondary); border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent); @@ -3663,7 +3767,7 @@ .insight-social-binding-input { width: 100%; min-width: 0; - height: 30px; + height: 28px; border-radius: 8px; border: 1px solid var(--border-color); background: color-mix(in srgb, var(--bg-primary) 92%, var(--bg-secondary) 8%); @@ -3706,9 +3810,10 @@ } .anti-revoke-row-status { - justify-self: flex-end; + justify-self: end; align-items: flex-end; max-width: none; + min-width: 0; } } @@ -3752,6 +3857,7 @@ .anti-revoke-list-header { grid-template-columns: minmax(0, 1fr) auto; + .insight-moments-column-title, .insight-social-column-title { display: none; } @@ -3763,11 +3869,16 @@ flex-direction: column; } + .insight-moments-cell, .insight-social-binding-cell, .anti-revoke-row-status { width: 100%; } + .insight-moments-cell { + justify-content: flex-start; + } + .insight-social-binding-cell { grid-template-columns: 1fr; } diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 9025a73..0d508ed 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -284,6 +284,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const [aiModelApiMaxTokens, setAiModelApiMaxTokens] = useState(200) const [aiInsightSilenceDays, setAiInsightSilenceDays] = useState(3) const [aiInsightAllowContext, setAiInsightAllowContext] = useState(false) + const [aiInsightAllowMomentsContext, setAiInsightAllowMomentsContext] = useState(false) + const [aiInsightMomentsContextCount, setAiInsightMomentsContextCount] = useState(5) + const [aiInsightMomentsBindings, setAiInsightMomentsBindings] = useState>({}) const [isTestingInsight, setIsTestingInsight] = useState(false) const [insightTestResult, setInsightTestResult] = useState<{ success: boolean; message: string } | null>(null) const [showInsightApiKey, setShowInsightApiKey] = useState(false) @@ -549,6 +552,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const savedAiModelApiMaxTokens = await configService.getAiModelApiMaxTokens() const savedAiInsightSilenceDays = await configService.getAiInsightSilenceDays() const savedAiInsightAllowContext = await configService.getAiInsightAllowContext() + const savedAiInsightAllowMomentsContext = await configService.getAiInsightAllowMomentsContext() + const savedAiInsightMomentsContextCount = await configService.getAiInsightMomentsContextCount() + const savedAiInsightMomentsBindings = await configService.getAiInsightMomentsBindings() const savedAiInsightFilterMode = await configService.getAiInsightFilterMode() const savedAiInsightFilterList = await configService.getAiInsightFilterList() const savedAiInsightCooldownMinutes = await configService.getAiInsightCooldownMinutes() @@ -573,6 +579,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { setAiModelApiMaxTokens(savedAiModelApiMaxTokens) setAiInsightSilenceDays(savedAiInsightSilenceDays) setAiInsightAllowContext(savedAiInsightAllowContext) + setAiInsightAllowMomentsContext(savedAiInsightAllowMomentsContext) + setAiInsightMomentsContextCount(savedAiInsightMomentsContextCount) + setAiInsightMomentsBindings(savedAiInsightMomentsBindings) setAiInsightFilterMode(savedAiInsightFilterMode) setAiInsightFilterList(new Set(savedAiInsightFilterList)) setAiInsightCooldownMinutes(savedAiInsightCooldownMinutes) @@ -3081,6 +3090,24 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { }) } + const isMomentsEnabledForSession = (sessionId: string): boolean => { + return aiInsightMomentsBindings[sessionId]?.enabled === true + } + + const handleToggleMomentsBinding = async (sessionId: string, enabled: boolean) => { + const nextBindings = { ...aiInsightMomentsBindings } + if (enabled) { + nextBindings[sessionId] = { + enabled: true, + updatedAt: Date.now() + } + } else { + delete nextBindings[sessionId] + } + setAiInsightMomentsBindings(nextBindings) + await configService.setAiInsightMomentsBindings(nextBindings) + } + const handleSaveWeiboBinding = async (sessionId: string, displayName: string) => { const draftUid = getWeiboBindingDraftValue(sessionId) setWeiboBindingLoadingSessionId(sessionId) @@ -3274,7 +3301,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { 开启后,触发见解时会将该联系人最近 N 条聊天记录发送给 AI,分析质量显著提升。
- 关闭时:AI 仅知道统计摘要(沉默天数等),输出质量较低。 + 关闭时:不会发送聊天原文,输出质量较低。
开启时:聊天文本内容(不含图片、语音)会通过你配置的 API 发送给模型提供商。请确认你信任该服务商。
@@ -3295,27 +3322,79 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { - {aiInsightAllowContext && ( -
- - - 发送给 AI 的聊天记录最大条数。条数越多分析越准确,token 消耗也越多。 - - { - const val = Math.max(1, Math.min(200, parseInt(e.target.value, 10) || 40)) - setAiInsightContextCount(val) - scheduleConfigSave('aiInsightContextCount', () => configService.setAiInsightContextCount(val)) - }} - style={{ width: 100 }} - /> +
+
+
+ + + 发送给 AI 的聊天记录最大条数。条数越多分析越准确,token 消耗也越多。 + + { + const val = Math.max(1, Math.min(200, parseInt(e.target.value, 10) || 40)) + setAiInsightContextCount(val) + scheduleConfigSave('aiInsightContextCount', () => configService.setAiInsightContextCount(val)) + }} + style={{ width: 100 }} + /> +
- )} +
+ +
+ +
+ + + 开启后,可在下方列表为私聊联系人单独允许朋友圈补充分析。程序只会在触发见解时按需读取,不会做后台持续扫描。 + +
+ {aiInsightAllowMomentsContext ? '已开启' : '已关闭'} + +
+
+ +
+
+
+ + + 发送给 AI 的朋友圈最大条数。条数越多上下文越充分,token 消耗也越多。 + + { + const val = Math.max(1, Math.min(20, parseInt(e.target.value, 10) || 5)) + setAiInsightMomentsContextCount(val) + scheduleConfigSave('aiInsightMomentsContextCount', () => configService.setAiInsightMomentsContextCount(val)) + }} + style={{ width: 100 }} + /> +
+
+
@@ -3354,29 +3433,32 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { )}
- {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 }} - /> +
+
+
+ + + 当前仅支持微博最近发帖。 +
+ 不建议超过 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 */} @@ -3652,11 +3734,14 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { <>
对话({filteredSessions.length}) + 朋友圈 社交平台(微博) - 状态 + 状态
{filteredSessions.map((session) => { const isSelected = aiInsightFilterList.has(session.username) + const isPrivateSession = session.type === 'private' + const isMomentsEnabled = isMomentsEnabledForSession(session.username) const weiboBinding = aiInsightWeiboBindings[session.username] const weiboDraftValue = getWeiboBindingDraftValue(session.username) const isBindingLoading = weiboBindingLoadingSessionId === session.username @@ -3695,8 +3780,24 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { {getSessionFilterTypeLabel(session.type)}
+
+ {isPrivateSession ? ( + + ) : ( + - + )} +
- {session.type === 'private' ? ( + {isPrivateSession ? ( <>
微博 @@ -3771,9 +3872,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {

- 触发方式一:活跃会话分析 — 每当微信数据库变化(即你收到新消息)时,经过 500ms 防抖后,对符合黑白名单规则的活跃会话进行分析。
+ 触发方式一:活跃会话分析 — 每当微信数据库变化(即你收到新消息)时,经过约 2 秒防抖后,对符合黑白名单规则的活跃会话进行分析。
触发方式二:沉默扫描 — 每 4 小时独立扫描一次,对超过阈值天数无消息的联系人发出提醒。
- 时间观念 — 每次调用时,AI 会收到今天已向该联系人和全局发出过多少次见解,由 AI 自行决定是否需要克制。
+ 频率控制 — 冷却期、沉默间隔、黑白名单均在本地判断,不额外发送给模型。
隐私 — 所有分析请求均直接从你的电脑发往你填写的 API 地址,不经过任何 WeFlow 服务器。

diff --git a/src/services/config.ts b/src/services/config.ts index 3b0863a..d85e611 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -97,6 +97,9 @@ export const CONFIG_KEYS = { AI_INSIGHT_API_MODEL: 'aiInsightApiModel', AI_INSIGHT_SILENCE_DAYS: 'aiInsightSilenceDays', AI_INSIGHT_ALLOW_CONTEXT: 'aiInsightAllowContext', + AI_INSIGHT_ALLOW_MOMENTS_CONTEXT: 'aiInsightAllowMomentsContext', + AI_INSIGHT_MOMENTS_CONTEXT_COUNT: 'aiInsightMomentsContextCount', + AI_INSIGHT_MOMENTS_BINDINGS: 'aiInsightMomentsBindings', AI_INSIGHT_ALLOW_SOCIAL_CONTEXT: 'aiInsightAllowSocialContext', AI_INSIGHT_FILTER_MODE: 'aiInsightFilterMode', AI_INSIGHT_FILTER_LIST: 'aiInsightFilterList', @@ -132,6 +135,11 @@ export interface AiInsightWeiboBinding { updatedAt: number } +export interface AiInsightMomentsBinding { + enabled: boolean + updatedAt: number +} + export interface ExportDefaultMediaConfig { images: boolean videos: boolean @@ -1922,6 +1930,24 @@ export async function setAiInsightAllowContext(allow: boolean): Promise { await config.set(CONFIG_KEYS.AI_INSIGHT_ALLOW_CONTEXT, allow) } +export async function getAiInsightAllowMomentsContext(): Promise { + const value = await config.get(CONFIG_KEYS.AI_INSIGHT_ALLOW_MOMENTS_CONTEXT) + return value === true +} + +export async function setAiInsightAllowMomentsContext(allow: boolean): Promise { + await config.set(CONFIG_KEYS.AI_INSIGHT_ALLOW_MOMENTS_CONTEXT, allow) +} + +export async function getAiInsightMomentsContextCount(): Promise { + const value = await config.get(CONFIG_KEYS.AI_INSIGHT_MOMENTS_CONTEXT_COUNT) + return typeof value === 'number' && value > 0 ? value : 5 +} + +export async function setAiInsightMomentsContextCount(count: number): Promise { + await config.set(CONFIG_KEYS.AI_INSIGHT_MOMENTS_CONTEXT_COUNT, count) +} + export async function getAiInsightAllowSocialContext(): Promise { const value = await config.get(CONFIG_KEYS.AI_INSIGHT_ALLOW_SOCIAL_CONTEXT) return value === true @@ -2067,6 +2093,33 @@ export async function setAiInsightWeiboBindings(bindings: Record => { + if (!value || typeof value !== 'object') return {} + const result: Record = {} + for (const [sessionIdRaw, bindingRaw] of Object.entries(value as Record)) { + const sessionId = String(sessionIdRaw || '').trim() + if (!sessionId) continue + if (!bindingRaw || typeof bindingRaw !== 'object') continue + const bindingObj = bindingRaw as { enabled?: unknown; updatedAt?: unknown } + if (bindingObj.enabled !== true) continue + const updatedAtRaw = Number(bindingObj.updatedAt) + result[sessionId] = { + enabled: true, + updatedAt: Number.isFinite(updatedAtRaw) && updatedAtRaw > 0 ? Math.floor(updatedAtRaw) : Date.now() + } + } + return result +} + +export async function getAiInsightMomentsBindings(): Promise> { + const value = await config.get(CONFIG_KEYS.AI_INSIGHT_MOMENTS_BINDINGS) + return normalizeAiInsightMomentsBindings(value) +} + +export async function setAiInsightMomentsBindings(bindings: Record): Promise { + await config.set(CONFIG_KEYS.AI_INSIGHT_MOMENTS_BINDINGS, normalizeAiInsightMomentsBindings(bindings)) +} + export async function getAiFootprintEnabled(): Promise { const value = await config.get(CONFIG_KEYS.AI_FOOTPRINT_ENABLED) return value === true