mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-28 23:16:44 +00:00
Merge pull request #868 from Jasonzhu1207/feature/insight-moments-context
feat(insight): add moments context gating and prompt integration & streamline insight prompts & and optimize UI animations in the Insights section
This commit is contained in:
@@ -86,7 +86,13 @@ interface ConfigSchema {
|
|||||||
aiInsightApiModel: string
|
aiInsightApiModel: string
|
||||||
aiInsightSilenceDays: number
|
aiInsightSilenceDays: number
|
||||||
aiInsightAllowContext: boolean
|
aiInsightAllowContext: boolean
|
||||||
|
aiInsightAllowMomentsContext: boolean
|
||||||
|
aiInsightMomentsContextCount: number
|
||||||
|
aiInsightMomentsBindings: Record<string, { enabled: boolean; updatedAt: number }>
|
||||||
aiInsightAllowSocialContext: boolean
|
aiInsightAllowSocialContext: boolean
|
||||||
|
aiInsightSocialContextCount: number
|
||||||
|
aiInsightWeiboCookie: string
|
||||||
|
aiInsightWeiboBindings: Record<string, { uid: string; screenName?: string; updatedAt: number }>
|
||||||
aiInsightFilterMode: 'whitelist' | 'blacklist'
|
aiInsightFilterMode: 'whitelist' | 'blacklist'
|
||||||
aiInsightFilterList: string[]
|
aiInsightFilterList: string[]
|
||||||
aiInsightWhitelistEnabled: boolean
|
aiInsightWhitelistEnabled: boolean
|
||||||
@@ -268,6 +274,9 @@ export class ConfigService {
|
|||||||
aiInsightApiModel: 'gpt-4o-mini',
|
aiInsightApiModel: 'gpt-4o-mini',
|
||||||
aiInsightSilenceDays: 3,
|
aiInsightSilenceDays: 3,
|
||||||
aiInsightAllowContext: false,
|
aiInsightAllowContext: false,
|
||||||
|
aiInsightAllowMomentsContext: false,
|
||||||
|
aiInsightMomentsContextCount: 5,
|
||||||
|
aiInsightMomentsBindings: {},
|
||||||
aiInsightAllowSocialContext: false,
|
aiInsightAllowSocialContext: false,
|
||||||
aiInsightFilterMode: 'whitelist',
|
aiInsightFilterMode: 'whitelist',
|
||||||
aiInsightFilterList: [],
|
aiInsightFilterList: [],
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* 设计原则:
|
* 设计原则:
|
||||||
* - 不引入任何额外 npm 依赖,使用 Node 原生 https 模块调用 OpenAI 兼容 API
|
* - 不引入任何额外 npm 依赖,使用 Node 原生 https 模块调用 OpenAI 兼容 API
|
||||||
* - 所有失败静默处理,不影响主流程
|
* - 所有失败静默处理,不影响主流程
|
||||||
* - 当日触发记录(sessionId + 时间列表)随 prompt 一起发送,让模型自行判断是否克制
|
* - 触发频率、冷却与名单过滤均在本地完成,不把调度统计塞进模型 prompt
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import https from 'https'
|
import https from 'https'
|
||||||
@@ -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 { snsService } from './snsService'
|
||||||
import { weiboService } from './social/weiboService'
|
import { weiboService } from './social/weiboService'
|
||||||
|
|
||||||
// ─── 常量 ────────────────────────────────────────────────────────────────────
|
// ─── 常量 ────────────────────────────────────────────────────────────────────
|
||||||
@@ -52,6 +53,9 @@ const INSIGHT_CONFIG_KEYS = new Set([
|
|||||||
'aiModelApiMaxTokens',
|
'aiModelApiMaxTokens',
|
||||||
'aiInsightFilterMode',
|
'aiInsightFilterMode',
|
||||||
'aiInsightFilterList',
|
'aiInsightFilterList',
|
||||||
|
'aiInsightAllowMomentsContext',
|
||||||
|
'aiInsightMomentsContextCount',
|
||||||
|
'aiInsightMomentsBindings',
|
||||||
'aiInsightAllowSocialContext',
|
'aiInsightAllowSocialContext',
|
||||||
'aiInsightSocialContextCount',
|
'aiInsightSocialContextCount',
|
||||||
'aiInsightWeiboCookie',
|
'aiInsightWeiboCookie',
|
||||||
@@ -445,7 +449,7 @@ class InsightService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions')
|
const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions')
|
||||||
const requestMessages = [{ role: 'user', content: appendPromptCurrentTime('请回复"连接成功"四个字。') }]
|
const requestMessages = [{ role: 'user', content: '请回复"连接成功"四个字。' }]
|
||||||
insightDebugSection(
|
insightDebugSection(
|
||||||
'INFO',
|
'INFO',
|
||||||
'AI 测试连接请求',
|
'AI 测试连接请求',
|
||||||
@@ -823,26 +827,13 @@ ${topMentionText}
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 记录触发并返回该会话今日所有触发时间(用于组装 prompt)。
|
* 记录成功推送的见解,用于设置页展示今日触发统计。
|
||||||
*/
|
*/
|
||||||
private recordTrigger(sessionId: string): string[] {
|
private recordTrigger(sessionId: string): void {
|
||||||
this.resetIfNewDay()
|
this.resetIfNewDay()
|
||||||
const existing = this.todayTriggers.get(sessionId) ?? { timestamps: [] }
|
const existing = this.todayTriggers.get(sessionId) ?? { timestamps: [] }
|
||||||
existing.timestamps.push(Date.now())
|
existing.timestamps.push(Date.now())
|
||||||
this.todayTriggers.set(sessionId, existing)
|
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 {
|
private formatWeiboTimestamp(raw: string): string {
|
||||||
@@ -853,12 +844,66 @@ ${topMentionText}
|
|||||||
return new Date(parsed).toLocaleString('zh-CN')
|
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<string> {
|
||||||
|
const allowMomentsContext = this.config.get('aiInsightAllowMomentsContext') === true
|
||||||
|
if (!allowMomentsContext) return ''
|
||||||
|
|
||||||
|
const bindings =
|
||||||
|
(this.config.get('aiInsightMomentsBindings') as Record<string, { enabled?: boolean }> | 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<string> {
|
private async getSocialContextSection(sessionId: string): Promise<string> {
|
||||||
const allowSocialContext = this.config.get('aiInsightAllowSocialContext') === true
|
const allowSocialContext = this.config.get('aiInsightAllowSocialContext') === true
|
||||||
if (!allowSocialContext) return ''
|
if (!allowSocialContext) return ''
|
||||||
|
|
||||||
const rawCookie = String(this.config.get('aiInsightWeiboCookie') || '').trim()
|
const rawCookie = String(this.config.get('aiInsightWeiboCookie') || '').trim()
|
||||||
const hasCookie = rawCookie.length > 0
|
|
||||||
|
|
||||||
const bindings =
|
const bindings =
|
||||||
(this.config.get('aiInsightWeiboBindings') as Record<string, { uid?: string; screenName?: string }> | undefined) || {}
|
(this.config.get('aiInsightWeiboBindings') as Record<string, { uid?: string; screenName?: string }> | undefined) || {}
|
||||||
@@ -879,10 +924,7 @@ ${topMentionText}
|
|||||||
return `[微博 ${time}] ${text}`
|
return `[微博 ${time}] ${text}`
|
||||||
})
|
})
|
||||||
insightLog('INFO', `已加载 ${lines.length} 条微博公开内容 (uid=${uid})`)
|
insightLog('INFO', `已加载 ${lines.length} 条微博公开内容 (uid=${uid})`)
|
||||||
const riskHint = hasCookie
|
return `近期公开社交平台内容(来源:微博,最近 ${lines.length} 条):\n${lines.join('\n')}`
|
||||||
? ''
|
|
||||||
: '\n提示:未配置微博 Cookie,使用移动端公开接口抓取,可能因平台风控导致获取失败或内容较少。'
|
|
||||||
return `近期公开社交平台内容(来源:微博,最近 ${lines.length} 条):\n${lines.join('\n')}${riskHint}`
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
insightLog('WARN', `拉取微博公开内容失败 (uid=${uid}): ${(error as Error).message}`)
|
insightLog('WARN', `拉取微博公开内容失败 (uid=${uid}): ${(error as Error).message}`)
|
||||||
return ''
|
return ''
|
||||||
@@ -1118,10 +1160,6 @@ ${topMentionText}
|
|||||||
|
|
||||||
// ── 构建 prompt ────────────────────────────────────────────────────────────
|
// ── 构建 prompt ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// 今日触发统计(让模型具备时间与克制感)
|
|
||||||
const sessionTriggerTimes = this.recordTrigger(sessionId)
|
|
||||||
const totalTodayTriggers = this.getTodayTotalTriggerCount()
|
|
||||||
|
|
||||||
let contextSection = ''
|
let contextSection = ''
|
||||||
if (allowContext) {
|
if (allowContext) {
|
||||||
try {
|
try {
|
||||||
@@ -1136,6 +1174,7 @@ ${topMentionText}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const momentsContextSection = await this.getMomentsContextSection(sessionId)
|
||||||
const socialContextSection = await this.getSocialContextSection(sessionId)
|
const socialContextSection = await this.getSocialContextSection(sessionId)
|
||||||
|
|
||||||
// ── 默认 system prompt(稳定内容,有利于 provider 端 prompt cache 命中)────
|
// ── 默认 system prompt(稳定内容,有利于 provider 端 prompt cache 命中)────
|
||||||
@@ -1151,25 +1190,12 @@ ${topMentionText}
|
|||||||
const customPrompt = (this.config.get('aiInsightSystemPrompt') as string) || ''
|
const customPrompt = (this.config.get('aiInsightSystemPrompt') as string) || ''
|
||||||
const systemPrompt = customPrompt.trim() || DEFAULT_SYSTEM_PROMPT
|
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 = [
|
const userPromptBase = [
|
||||||
`触发原因:${triggerDesc}`,
|
triggerReason === 'silence' && silentDays
|
||||||
`时间统计:${todayStatsDesc}`,
|
? `已 ${silentDays} 天未联系「${resolvedDisplayName}」。`
|
||||||
`全局统计:${globalStatsDesc}`,
|
: '',
|
||||||
contextSection,
|
contextSection,
|
||||||
|
momentsContextSection,
|
||||||
socialContextSection,
|
socialContextSection,
|
||||||
'请给出你的见解(≤80字):'
|
'请给出你的见解(≤80字):'
|
||||||
].filter(Boolean).join('\n\n')
|
].filter(Boolean).join('\n\n')
|
||||||
@@ -1189,7 +1215,7 @@ ${topMentionText}
|
|||||||
`接口地址:${endpoint}`,
|
`接口地址:${endpoint}`,
|
||||||
`模型:${model}`,
|
`模型:${model}`,
|
||||||
`Max Tokens:${maxTokens}`,
|
`Max Tokens:${maxTokens}`,
|
||||||
`触发原因:${triggerReason}`,
|
`触发类型:${triggerReason}`,
|
||||||
`上下文开关:${allowContext ? '开启' : '关闭'}`,
|
`上下文开关:${allowContext ? '开启' : '关闭'}`,
|
||||||
`上下文条数:${contextCount}`,
|
`上下文条数:${contextCount}`,
|
||||||
'',
|
'',
|
||||||
@@ -1253,6 +1279,7 @@ ${topMentionText}
|
|||||||
}
|
}
|
||||||
|
|
||||||
insightLog('INFO', `已为 ${resolvedDisplayName} 推送见解`)
|
insightLog('INFO', `已为 ${resolvedDisplayName} 推送见解`)
|
||||||
|
this.recordTrigger(sessionId)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
insightDebugSection(
|
insightDebugSection(
|
||||||
'ERROR',
|
'ERROR',
|
||||||
|
|||||||
@@ -915,6 +915,31 @@
|
|||||||
color: var(--text-secondary);
|
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 */
|
/* Premium Switch Style */
|
||||||
.switch {
|
.switch {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -3616,17 +3641,35 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.insight-social-tab {
|
&.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 {
|
.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 {
|
.insight-social-column-title {
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.anti-revoke-status-column-title {
|
||||||
|
justify-self: end;
|
||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.anti-revoke-row {
|
.anti-revoke-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(300px, 420px) auto;
|
grid-template-columns: var(--insight-social-list-grid);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
@@ -3635,6 +3678,67 @@
|
|||||||
min-width: 0;
|
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 {
|
.insight-social-binding-cell {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -3653,7 +3757,7 @@
|
|||||||
.binding-platform-chip {
|
.binding-platform-chip {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 2px 8px;
|
padding: 2px 7px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent);
|
border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent);
|
||||||
@@ -3663,7 +3767,7 @@
|
|||||||
.insight-social-binding-input {
|
.insight-social-binding-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 30px;
|
height: 28px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
background: color-mix(in srgb, var(--bg-primary) 92%, var(--bg-secondary) 8%);
|
background: color-mix(in srgb, var(--bg-primary) 92%, var(--bg-secondary) 8%);
|
||||||
@@ -3706,9 +3810,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.anti-revoke-row-status {
|
.anti-revoke-row-status {
|
||||||
justify-self: flex-end;
|
justify-self: end;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3752,6 +3857,7 @@
|
|||||||
.anti-revoke-list-header {
|
.anti-revoke-list-header {
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
|
||||||
|
.insight-moments-column-title,
|
||||||
.insight-social-column-title {
|
.insight-social-column-title {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -3763,11 +3869,16 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.insight-moments-cell,
|
||||||
.insight-social-binding-cell,
|
.insight-social-binding-cell,
|
||||||
.anti-revoke-row-status {
|
.anti-revoke-row-status {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.insight-moments-cell {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.insight-social-binding-cell {
|
.insight-social-binding-cell {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -284,6 +284,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
const [aiModelApiMaxTokens, setAiModelApiMaxTokens] = useState(200)
|
const [aiModelApiMaxTokens, setAiModelApiMaxTokens] = useState(200)
|
||||||
const [aiInsightSilenceDays, setAiInsightSilenceDays] = useState(3)
|
const [aiInsightSilenceDays, setAiInsightSilenceDays] = useState(3)
|
||||||
const [aiInsightAllowContext, setAiInsightAllowContext] = useState(false)
|
const [aiInsightAllowContext, setAiInsightAllowContext] = useState(false)
|
||||||
|
const [aiInsightAllowMomentsContext, setAiInsightAllowMomentsContext] = useState(false)
|
||||||
|
const [aiInsightMomentsContextCount, setAiInsightMomentsContextCount] = useState(5)
|
||||||
|
const [aiInsightMomentsBindings, setAiInsightMomentsBindings] = useState<Record<string, configService.AiInsightMomentsBinding>>({})
|
||||||
const [isTestingInsight, setIsTestingInsight] = useState(false)
|
const [isTestingInsight, setIsTestingInsight] = useState(false)
|
||||||
const [insightTestResult, setInsightTestResult] = useState<{ success: boolean; message: string } | null>(null)
|
const [insightTestResult, setInsightTestResult] = useState<{ success: boolean; message: string } | null>(null)
|
||||||
const [showInsightApiKey, setShowInsightApiKey] = useState(false)
|
const [showInsightApiKey, setShowInsightApiKey] = useState(false)
|
||||||
@@ -549,6 +552,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
const savedAiModelApiMaxTokens = await configService.getAiModelApiMaxTokens()
|
const savedAiModelApiMaxTokens = await configService.getAiModelApiMaxTokens()
|
||||||
const savedAiInsightSilenceDays = await configService.getAiInsightSilenceDays()
|
const savedAiInsightSilenceDays = await configService.getAiInsightSilenceDays()
|
||||||
const savedAiInsightAllowContext = await configService.getAiInsightAllowContext()
|
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 savedAiInsightFilterMode = await configService.getAiInsightFilterMode()
|
||||||
const savedAiInsightFilterList = await configService.getAiInsightFilterList()
|
const savedAiInsightFilterList = await configService.getAiInsightFilterList()
|
||||||
const savedAiInsightCooldownMinutes = await configService.getAiInsightCooldownMinutes()
|
const savedAiInsightCooldownMinutes = await configService.getAiInsightCooldownMinutes()
|
||||||
@@ -573,6 +579,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
setAiModelApiMaxTokens(savedAiModelApiMaxTokens)
|
setAiModelApiMaxTokens(savedAiModelApiMaxTokens)
|
||||||
setAiInsightSilenceDays(savedAiInsightSilenceDays)
|
setAiInsightSilenceDays(savedAiInsightSilenceDays)
|
||||||
setAiInsightAllowContext(savedAiInsightAllowContext)
|
setAiInsightAllowContext(savedAiInsightAllowContext)
|
||||||
|
setAiInsightAllowMomentsContext(savedAiInsightAllowMomentsContext)
|
||||||
|
setAiInsightMomentsContextCount(savedAiInsightMomentsContextCount)
|
||||||
|
setAiInsightMomentsBindings(savedAiInsightMomentsBindings)
|
||||||
setAiInsightFilterMode(savedAiInsightFilterMode)
|
setAiInsightFilterMode(savedAiInsightFilterMode)
|
||||||
setAiInsightFilterList(new Set(savedAiInsightFilterList))
|
setAiInsightFilterList(new Set(savedAiInsightFilterList))
|
||||||
setAiInsightCooldownMinutes(savedAiInsightCooldownMinutes)
|
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 handleSaveWeiboBinding = async (sessionId: string, displayName: string) => {
|
||||||
const draftUid = getWeiboBindingDraftValue(sessionId)
|
const draftUid = getWeiboBindingDraftValue(sessionId)
|
||||||
setWeiboBindingLoadingSessionId(sessionId)
|
setWeiboBindingLoadingSessionId(sessionId)
|
||||||
@@ -3274,7 +3301,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
<span className="form-hint">
|
<span className="form-hint">
|
||||||
开启后,触发见解时会将该联系人最近 N 条聊天记录发送给 AI,分析质量显著提升。
|
开启后,触发见解时会将该联系人最近 N 条聊天记录发送给 AI,分析质量显著提升。
|
||||||
<br />
|
<br />
|
||||||
<strong>关闭时</strong>:AI 仅知道统计摘要(沉默天数等),输出质量较低。
|
<strong>关闭时</strong>:不会发送聊天原文,输出质量较低。
|
||||||
<br />
|
<br />
|
||||||
<strong>开启时</strong>:聊天文本内容(不含图片、语音)会通过你配置的 API 发送给模型提供商。请确认你信任该服务商。
|
<strong>开启时</strong>:聊天文本内容(不含图片、语音)会通过你配置的 API 发送给模型提供商。请确认你信任该服务商。
|
||||||
</span>
|
</span>
|
||||||
@@ -3295,7 +3322,8 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{aiInsightAllowContext && (
|
<div className={`insight-collapsible-setting ${aiInsightAllowContext ? 'expanded' : 'collapsed'}`} aria-hidden={!aiInsightAllowContext}>
|
||||||
|
<div className="insight-collapsible-setting-inner">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>发送近期对话条数</label>
|
<label>发送近期对话条数</label>
|
||||||
<span className="form-hint">
|
<span className="form-hint">
|
||||||
@@ -3307,6 +3335,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
value={aiInsightContextCount}
|
value={aiInsightContextCount}
|
||||||
min={1}
|
min={1}
|
||||||
max={200}
|
max={200}
|
||||||
|
disabled={!aiInsightAllowContext}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = Math.max(1, Math.min(200, parseInt(e.target.value, 10) || 40))
|
const val = Math.max(1, Math.min(200, parseInt(e.target.value, 10) || 40))
|
||||||
setAiInsightContextCount(val)
|
setAiInsightContextCount(val)
|
||||||
@@ -3315,7 +3344,57 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
style={{ width: 100 }}
|
style={{ width: 100 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="divider" />
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label>允许发送近期朋友圈内容用于分析(实验性)</label>
|
||||||
|
<span className="form-hint">
|
||||||
|
开启后,可在下方列表为私聊联系人单独允许朋友圈补充分析。程序只会在触发见解时按需读取,不会做后台持续扫描。
|
||||||
|
</span>
|
||||||
|
<div className="log-toggle-line">
|
||||||
|
<span className="log-status">{aiInsightAllowMomentsContext ? '已开启' : '已关闭'}</span>
|
||||||
|
<label className="switch">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={aiInsightAllowMomentsContext}
|
||||||
|
onChange={async (e) => {
|
||||||
|
const val = e.target.checked
|
||||||
|
setAiInsightAllowMomentsContext(val)
|
||||||
|
await configService.setAiInsightAllowMomentsContext(val)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="switch-slider" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`insight-collapsible-setting ${aiInsightAllowMomentsContext ? 'expanded' : 'collapsed'}`} aria-hidden={!aiInsightAllowMomentsContext}>
|
||||||
|
<div className="insight-collapsible-setting-inner">
|
||||||
|
<div className="form-group">
|
||||||
|
<label>发送近期朋友圈条数</label>
|
||||||
|
<span className="form-hint">
|
||||||
|
发送给 AI 的朋友圈最大条数。条数越多上下文越充分,token 消耗也越多。
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="field-input"
|
||||||
|
value={aiInsightMomentsContextCount}
|
||||||
|
min={1}
|
||||||
|
max={20}
|
||||||
|
disabled={!aiInsightAllowMomentsContext}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = Math.max(1, Math.min(20, parseInt(e.target.value, 10) || 5))
|
||||||
|
setAiInsightMomentsContextCount(val)
|
||||||
|
scheduleConfigSave('aiInsightMomentsContextCount', () => configService.setAiInsightMomentsContextCount(val))
|
||||||
|
}}
|
||||||
|
style={{ width: 100 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="divider" />
|
<div className="divider" />
|
||||||
|
|
||||||
@@ -3354,7 +3433,8 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{aiInsightAllowSocialContext && (
|
<div className={`insight-collapsible-setting ${aiInsightAllowSocialContext ? 'expanded' : 'collapsed'}`} aria-hidden={!aiInsightAllowSocialContext}>
|
||||||
|
<div className="insight-collapsible-setting-inner">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>发送近期社交平台内容条数</label>
|
<label>发送近期社交平台内容条数</label>
|
||||||
<span className="form-hint">
|
<span className="form-hint">
|
||||||
@@ -3368,6 +3448,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
value={aiInsightSocialContextCount}
|
value={aiInsightSocialContextCount}
|
||||||
min={1}
|
min={1}
|
||||||
max={5}
|
max={5}
|
||||||
|
disabled={!aiInsightAllowSocialContext}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = Math.max(1, Math.min(5, parseInt(e.target.value, 10) || 3))
|
const val = Math.max(1, Math.min(5, parseInt(e.target.value, 10) || 3))
|
||||||
setAiInsightSocialContextCount(val)
|
setAiInsightSocialContextCount(val)
|
||||||
@@ -3376,7 +3457,8 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
style={{ width: 100 }}
|
style={{ width: 100 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="divider" />
|
<div className="divider" />
|
||||||
{/* 自定义 System Prompt */}
|
{/* 自定义 System Prompt */}
|
||||||
@@ -3652,11 +3734,14 @@ 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-moments-column-title">朋友圈</span>
|
||||||
<span className="insight-social-column-title">社交平台(微博)</span>
|
<span className="insight-social-column-title">社交平台(微博)</span>
|
||||||
<span>状态</span>
|
<span className="anti-revoke-status-column-title">状态</span>
|
||||||
</div>
|
</div>
|
||||||
{filteredSessions.map((session) => {
|
{filteredSessions.map((session) => {
|
||||||
const isSelected = aiInsightFilterList.has(session.username)
|
const isSelected = aiInsightFilterList.has(session.username)
|
||||||
|
const isPrivateSession = session.type === 'private'
|
||||||
|
const isMomentsEnabled = isMomentsEnabledForSession(session.username)
|
||||||
const weiboBinding = aiInsightWeiboBindings[session.username]
|
const weiboBinding = aiInsightWeiboBindings[session.username]
|
||||||
const weiboDraftValue = getWeiboBindingDraftValue(session.username)
|
const weiboDraftValue = getWeiboBindingDraftValue(session.username)
|
||||||
const isBindingLoading = weiboBindingLoadingSessionId === session.username
|
const isBindingLoading = weiboBindingLoadingSessionId === session.username
|
||||||
@@ -3695,8 +3780,24 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
<span className="desc">{getSessionFilterTypeLabel(session.type)}</span>
|
<span className="desc">{getSessionFilterTypeLabel(session.type)}</span>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
<div className="insight-moments-cell">
|
||||||
|
{isPrivateSession ? (
|
||||||
|
<label className="insight-moments-toggle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isMomentsEnabled}
|
||||||
|
onChange={(e) => { void handleToggleMomentsBinding(session.username, e.target.checked) }}
|
||||||
|
/>
|
||||||
|
<span className="check-indicator" aria-hidden="true">
|
||||||
|
<Check size={12} />
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
) : (
|
||||||
|
<span className="binding-feedback muted">-</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="insight-social-binding-cell">
|
<div className="insight-social-binding-cell">
|
||||||
{session.type === 'private' ? (
|
{isPrivateSession ? (
|
||||||
<>
|
<>
|
||||||
<div className="insight-social-binding-input-wrap">
|
<div className="insight-social-binding-input-wrap">
|
||||||
<span className="binding-platform-chip">微博</span>
|
<span className="binding-platform-chip">微博</span>
|
||||||
@@ -3771,9 +3872,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
<div className="api-docs">
|
<div className="api-docs">
|
||||||
<div className="api-item">
|
<div className="api-item">
|
||||||
<p className="api-desc" style={{ lineHeight: 1.7 }}>
|
<p className="api-desc" style={{ lineHeight: 1.7 }}>
|
||||||
<strong>触发方式一:活跃会话分析</strong> — 每当微信数据库变化(即你收到新消息)时,经过 500ms 防抖后,对符合黑白名单规则的活跃会话进行分析。<br />
|
<strong>触发方式一:活跃会话分析</strong> — 每当微信数据库变化(即你收到新消息)时,经过约 2 秒防抖后,对符合黑白名单规则的活跃会话进行分析。<br />
|
||||||
<strong>触发方式二:沉默扫描</strong> — 每 4 小时独立扫描一次,对超过阈值天数无消息的联系人发出提醒。<br />
|
<strong>触发方式二:沉默扫描</strong> — 每 4 小时独立扫描一次,对超过阈值天数无消息的联系人发出提醒。<br />
|
||||||
<strong>时间观念</strong> — 每次调用时,AI 会收到今天已向该联系人和全局发出过多少次见解,由 AI 自行决定是否需要克制。<br />
|
<strong>频率控制</strong> — 冷却期、沉默间隔、黑白名单均在本地判断,不额外发送给模型。<br />
|
||||||
<strong>隐私</strong> — 所有分析请求均直接从你的电脑发往你填写的 API 地址,不经过任何 WeFlow 服务器。
|
<strong>隐私</strong> — 所有分析请求均直接从你的电脑发往你填写的 API 地址,不经过任何 WeFlow 服务器。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ 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_MOMENTS_CONTEXT: 'aiInsightAllowMomentsContext',
|
||||||
|
AI_INSIGHT_MOMENTS_CONTEXT_COUNT: 'aiInsightMomentsContextCount',
|
||||||
|
AI_INSIGHT_MOMENTS_BINDINGS: 'aiInsightMomentsBindings',
|
||||||
AI_INSIGHT_ALLOW_SOCIAL_CONTEXT: 'aiInsightAllowSocialContext',
|
AI_INSIGHT_ALLOW_SOCIAL_CONTEXT: 'aiInsightAllowSocialContext',
|
||||||
AI_INSIGHT_FILTER_MODE: 'aiInsightFilterMode',
|
AI_INSIGHT_FILTER_MODE: 'aiInsightFilterMode',
|
||||||
AI_INSIGHT_FILTER_LIST: 'aiInsightFilterList',
|
AI_INSIGHT_FILTER_LIST: 'aiInsightFilterList',
|
||||||
@@ -132,6 +135,11 @@ export interface AiInsightWeiboBinding {
|
|||||||
updatedAt: number
|
updatedAt: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AiInsightMomentsBinding {
|
||||||
|
enabled: boolean
|
||||||
|
updatedAt: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExportDefaultMediaConfig {
|
export interface ExportDefaultMediaConfig {
|
||||||
images: boolean
|
images: boolean
|
||||||
videos: boolean
|
videos: boolean
|
||||||
@@ -1922,6 +1930,24 @@ 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 getAiInsightAllowMomentsContext(): Promise<boolean> {
|
||||||
|
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_ALLOW_MOMENTS_CONTEXT)
|
||||||
|
return value === true
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setAiInsightAllowMomentsContext(allow: boolean): Promise<void> {
|
||||||
|
await config.set(CONFIG_KEYS.AI_INSIGHT_ALLOW_MOMENTS_CONTEXT, allow)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAiInsightMomentsContextCount(): Promise<number> {
|
||||||
|
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<void> {
|
||||||
|
await config.set(CONFIG_KEYS.AI_INSIGHT_MOMENTS_CONTEXT_COUNT, count)
|
||||||
|
}
|
||||||
|
|
||||||
export async function getAiInsightAllowSocialContext(): Promise<boolean> {
|
export async function getAiInsightAllowSocialContext(): Promise<boolean> {
|
||||||
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_ALLOW_SOCIAL_CONTEXT)
|
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_ALLOW_SOCIAL_CONTEXT)
|
||||||
return value === true
|
return value === true
|
||||||
@@ -2067,6 +2093,33 @@ export async function setAiInsightWeiboBindings(bindings: Record<string, AiInsig
|
|||||||
await config.set(CONFIG_KEYS.AI_INSIGHT_WEIBO_BINDINGS, bindings)
|
await config.set(CONFIG_KEYS.AI_INSIGHT_WEIBO_BINDINGS, bindings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeAiInsightMomentsBindings = (value: unknown): Record<string, AiInsightMomentsBinding> => {
|
||||||
|
if (!value || typeof value !== 'object') return {}
|
||||||
|
const result: Record<string, AiInsightMomentsBinding> = {}
|
||||||
|
for (const [sessionIdRaw, bindingRaw] of Object.entries(value as Record<string, unknown>)) {
|
||||||
|
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<Record<string, AiInsightMomentsBinding>> {
|
||||||
|
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_MOMENTS_BINDINGS)
|
||||||
|
return normalizeAiInsightMomentsBindings(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setAiInsightMomentsBindings(bindings: Record<string, AiInsightMomentsBinding>): Promise<void> {
|
||||||
|
await config.set(CONFIG_KEYS.AI_INSIGHT_MOMENTS_BINDINGS, normalizeAiInsightMomentsBindings(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
|
||||||
|
|||||||
Reference in New Issue
Block a user