feat(insight): add moments context gating and prompt integration

This commit is contained in:
Jason
2026-04-28 00:14:05 +08:00
parent 608f74a3f9
commit 55a7ce7b66
5 changed files with 293 additions and 5 deletions

View File

@@ -85,7 +85,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
@@ -205,6 +211,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: [],

View File

@@ -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',
@@ -853,6 +857,61 @@ ${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 ''
@@ -1136,6 +1195,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 命中)────
@@ -1170,6 +1230,7 @@ ${topMentionText}
`时间统计:${todayStatsDesc}`, `时间统计:${todayStatsDesc}`,
`全局统计:${globalStatsDesc}`, `全局统计:${globalStatsDesc}`,
contextSection, contextSection,
momentsContextSection,
socialContextSection, socialContextSection,
'请给出你的见解≤80字' '请给出你的见解≤80字'
].filter(Boolean).join('\n\n') ].filter(Boolean).join('\n\n')

View File

@@ -3617,7 +3617,12 @@
&.insight-social-tab { &.insight-social-tab {
.anti-revoke-list-header { .anti-revoke-list-header {
grid-template-columns: minmax(0, 1fr) minmax(300px, 420px) auto; grid-template-columns: minmax(0, 1fr) 86px minmax(240px, 340px) auto;
.insight-moments-column-title {
color: var(--text-tertiary);
text-align: center;
}
.insight-social-column-title { .insight-social-column-title {
color: var(--text-tertiary); color: var(--text-tertiary);
@@ -3626,7 +3631,7 @@
.anti-revoke-row { .anti-revoke-row {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) minmax(300px, 420px) auto; grid-template-columns: minmax(0, 1fr) 86px minmax(240px, 340px) auto;
align-items: center; align-items: center;
gap: 14px; gap: 14px;
} }
@@ -3635,6 +3640,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 +3719,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 +3729,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%);
@@ -3752,6 +3818,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 +3830,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;
} }

View File

@@ -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)
@@ -3319,6 +3346,53 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
<div className="divider" /> <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>
{aiInsightAllowMomentsContext && (
<div className="form-group">
<label></label>
<span className="form-hint">
XML
</span>
<input
type="number"
className="field-input"
value={aiInsightMomentsContextCount}
min={1}
max={20}
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 className="divider" />
<div className="form-group"> <div className="form-group">
<label></label> <label></label>
<span className="form-hint"> <span className="form-hint">
@@ -3652,11 +3726,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></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 +3772,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>

View File

@@ -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