From 980a8135fa44a2dc1bcdfdc9fd170f68b0d0f373 Mon Sep 17 00:00:00 2001 From: iven Date: Thu, 23 Apr 2026 17:54:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(suggest):=20=E6=96=B0=E5=A2=9E=20fetchSugg?= =?UTF-8?q?estionContext=20=E8=81=9A=E5=90=88=E5=87=BD=E6=95=B0=20+=20?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 4 路并行拉取智能上下文:用户画像、痛点、经验、技能匹配 - 500ms 超时保护 + 静默降级(失败不阻断建议生成) - Tauri 不可用时直接返回空上下文 --- desktop/src/lib/suggestion-context.ts | 131 ++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 desktop/src/lib/suggestion-context.ts diff --git a/desktop/src/lib/suggestion-context.ts b/desktop/src/lib/suggestion-context.ts new file mode 100644 index 0000000..0df355f --- /dev/null +++ b/desktop/src/lib/suggestion-context.ts @@ -0,0 +1,131 @@ +/** + * Suggestion context enrichment — fetches intelligence data for personalized suggestions. + * All fetches are optional; failures silently degrade to empty context. + */ + +import { invoke } from '@tauri-apps/api/core'; +import { createLogger } from './logger'; + +const log = createLogger('SuggestionContext'); + +const CONTEXT_FETCH_TIMEOUT = 500; + +/** Pain point from butler intelligence layer. */ +interface PainPoint { + summary: string; + category: string; + confidence: number; + status: string; + occurrence_count: number; +} + +/** Brief experience from the experience store. */ +interface ExperienceBrief { + pain_pattern: string; + solution_summary: string; + reuse_count: number; +} + +/** Pipeline/skill match candidate. */ +interface PipelineCandidateInfo { + id: string; + display_name: string; + description: string; + category: string | null; + match_reason: string | null; +} + +/** Route intent response (only NoMatch variant has suggestions). */ +interface RouteResultResponse { + type: 'Matched' | 'Ambiguous' | 'NoMatch' | 'NeedMoreInfo'; + suggestions?: PipelineCandidateInfo[]; +} + +/** Aggregated suggestion context from all intelligence sources. */ +export interface SuggestionContext { + userProfile: string; + painPoints: string; + experiences: string; + skillMatch: string; +} + +function isTauriAvailable(): boolean { + return typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window; +} + +function withTimeout(promise: Promise, ms: number): Promise { + return Promise.race([ + promise, + new Promise(resolve => setTimeout(() => resolve(null), ms)), + ]); +} + +async function fetchUserProfile(agentId: string): Promise { + const profile = await invoke('identity_get_file', { + agentId, + file: 'userprofile', + }); + if (!profile || profile.trim().length === 0) return ''; + const text = profile.trim(); + return text.length > 200 ? text.slice(0, 200) : text; +} + +async function fetchPainPoints(agentId: string): Promise { + const points = await invoke('butler_list_pain_points', { agentId }); + if (!Array.isArray(points) || points.length === 0) return ''; + + const active = points + .filter(p => p.confidence >= 0.5 && p.status !== 'Solved' && p.status !== 'Dismissed') + .sort((a, b) => b.confidence - a.confidence) + .slice(0, 3); + + if (active.length === 0) return ''; + return active + .map((p, i) => `${i + 1}. [${p.category}] ${p.summary}(出现${p.occurrence_count}次)`) + .join('\n'); +} + +async function fetchExperiences(agentId: string, query: string): Promise { + const experiences = await invoke('experience_find_relevant', { + agentId, + query, + }); + if (!Array.isArray(experiences) || experiences.length === 0) return ''; + + return experiences.slice(0, 2) + .map(e => `上次解决"${e.pain_pattern}"的方法:${e.solution_summary}(已复用${e.reuse_count}次)`) + .join('\n'); +} + +async function fetchSkillMatch(userInput: string): Promise { + const result = await invoke('route_intent', { userInput }); + const suggestions = result?.suggestions; + if (!Array.isArray(suggestions) || suggestions.length === 0) return ''; + + const best = suggestions[0]; + return `你可能需要:${best.display_name} — ${best.description}`; +} + +const EMPTY_CONTEXT: SuggestionContext = { userProfile: '', painPoints: '', experiences: '', skillMatch: '' }; + +/** + * Fetch all intelligence context in parallel for suggestion enrichment. + * Returns empty strings for any source that fails — never throws. + */ +export async function fetchSuggestionContext( + agentId: string, + lastUserMessage: string, +): Promise { + if (!isTauriAvailable()) { + return EMPTY_CONTEXT; + } + + const [userProfile, painPoints, experiences, skillMatch] = await Promise.all([ + withTimeout(fetchUserProfile(agentId).catch(e => { log.warn('User profile fetch failed:', e); return ''; }), CONTEXT_FETCH_TIMEOUT), + withTimeout(fetchPainPoints(agentId).catch(e => { log.warn('Pain points fetch failed:', e); return ''; }), CONTEXT_FETCH_TIMEOUT), + withTimeout(fetchExperiences(agentId, lastUserMessage).catch(e => { log.warn('Experiences fetch failed:', e); return ''; }), CONTEXT_FETCH_TIMEOUT), + withTimeout(fetchSkillMatch(lastUserMessage).catch(e => { log.warn('Skill match fetch failed:', e); return ''; }), CONTEXT_FETCH_TIMEOUT), + ]); + + return { userProfile: userProfile ?? '', painPoints: painPoints ?? '', experiences: experiences ?? '', skillMatch: skillMatch ?? '' }; +}