import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { AgentProfileApiError, getAgentProfile, putAgentProfile, type AgentProfileRead, type AgentProfileUpdate, } from '@/lib/agent-profile'; export type UserGender = 'male' | 'female' | 'other' | 'undisclosed'; export type UserProfileDraft = { displayName: string; age: number | null; gender: UserGender; }; export type AgentUserProfile = UserProfileDraft & { shareAgeWithAgents: boolean; shareGenderWithAgents: boolean; analysisEnabled: boolean; pendingSync: boolean; version: number; updatedAt: string | null; }; export type ProfileSyncStatus = 'idle' | 'loading' | 'ready' | 'saving' | 'error'; export type ProfileSyncState = { status: ProfileSyncStatus; error: string | null; }; type UserProfileState = { profilesByUserId: Record; syncStatesByUserId: Record; saveProfile: (userId: string, profile: UserProfileDraft) => void; syncProfile: (userId: string, accessToken: string) => Promise; saveProfileToServer: (userId: string, accessToken: string) => Promise; }; export const EMPTY_AGENT_USER_PROFILE: AgentUserProfile = { displayName: '', age: null, gender: 'undisclosed', shareAgeWithAgents: false, shareGenderWithAgents: false, analysisEnabled: true, pendingSync: false, version: 0, updatedAt: null, }; const inFlightSyncRequests = new Map>(); export function getProfileAccountKey(userId: string | null | undefined): string | null { const value = userId?.trim(); return value || null; } export function isUserProfileComplete(profile: AgentUserProfile | undefined): boolean { return Boolean(profile?.displayName.trim()); } function fromServerProfile(profile: AgentProfileRead): AgentUserProfile { return { displayName: profile.display_name ?? '', age: profile.age, gender: profile.gender ?? 'undisclosed', shareAgeWithAgents: profile.share_age_with_agents, shareGenderWithAgents: profile.share_gender_with_agents, analysisEnabled: profile.analysis_enabled, pendingSync: false, version: profile.version, updatedAt: profile.updated_at, }; } function toAgentProfileUpdate(profile: AgentUserProfile, version = profile.version): AgentProfileUpdate { const gender = profile.gender === 'undisclosed' ? null : profile.gender; return { display_name: profile.displayName.trim(), age: profile.age, gender, share_age_with_agents: profile.age !== null && (profile.shareAgeWithAgents ?? true), share_gender_with_agents: gender !== null && (profile.shareGenderWithAgents ?? true), analysis_enabled: profile.analysisEnabled ?? true, version, }; } function getErrorMessage(error: unknown): string { if (error instanceof AgentProfileApiError) { if (error.status === 401) return '登录已过期,请重新登录后再同步个人资料。'; if (error.status === 503) return '个人资料服务暂时不可用,请稍后重试。'; return error.message; } return error instanceof Error ? error.message : String(error); } function buildLocalProfile( existing: AgentUserProfile | undefined, draft: UserProfileDraft, ): AgentUserProfile { const age = draft.age; const gender = draft.gender; const hadAge = typeof existing?.age === 'number'; const hadGender = Boolean(existing && existing.gender !== 'undisclosed'); return { ...(existing ?? EMPTY_AGENT_USER_PROFILE), displayName: draft.displayName.trim(), age, gender, shareAgeWithAgents: age !== null ? (hadAge ? (existing?.shareAgeWithAgents ?? true) : true) : false, shareGenderWithAgents: gender !== 'undisclosed' ? (hadGender ? (existing?.shareGenderWithAgents ?? true) : true) : false, pendingSync: true, version: existing?.version ?? 0, updatedAt: existing?.updatedAt ?? null, }; } function profileSyncState(status: ProfileSyncStatus, error: string | null = null): ProfileSyncState { return { status, error }; } export function buildAgentUserContext(profile: AgentUserProfile | undefined): string | null { if (!isUserProfileComplete(profile)) return null; const lines = [ '## 当前用户资料', `当前与你协作的用户名字是「${profile!.displayName.trim()}」。`, `可以根据这份资料调整称呼、表达方式、解释深度和互动方式,但不要反复提及资料。`, ]; if (profile!.age !== null && (profile!.shareAgeWithAgents ?? true)) { lines.push(`用户年龄:${profile!.age} 岁。`); } if (profile!.gender !== 'undisclosed' && (profile!.shareGenderWithAgents ?? true)) { const genderLabel = profile!.gender === 'male' ? '男' : profile!.gender === 'female' ? '女' : '其他'; lines.push(`用户性别:${genderLabel}。`); } lines.push('不要根据年龄或性别推断用户的健康、能力、身份或其他敏感属性。'); return lines.join('\n'); } export const useUserProfileStore = create()( persist( (set, get) => ({ profilesByUserId: {}, syncStatesByUserId: {}, saveProfile: (userId, profile) => set((state) => ({ profilesByUserId: { ...state.profilesByUserId, [userId]: buildLocalProfile(state.profilesByUserId[userId], profile), }, syncStatesByUserId: { ...state.syncStatesByUserId, [userId]: profileSyncState('idle'), }, })), syncProfile: (userId, accessToken) => { const existingRequest = inFlightSyncRequests.get(userId); if (existingRequest) return existingRequest; const request = (async () => { set((state) => ({ syncStatesByUserId: { ...state.syncStatesByUserId, [userId]: profileSyncState('loading'), }, })); try { const server = await getAgentProfile(accessToken); const local = get().profilesByUserId[userId]; if (server.version > 0) { const profile = fromServerProfile(server); set((state) => ({ profilesByUserId: { ...state.profilesByUserId, [userId]: profile }, syncStatesByUserId: { ...state.syncStatesByUserId, [userId]: profileSyncState('ready'), }, })); return profile; } if (local && isUserProfileComplete(local)) { await putAgentProfile(accessToken, toAgentProfileUpdate(local, 0)); const confirmed = fromServerProfile(await getAgentProfile(accessToken)); set((state) => ({ profilesByUserId: { ...state.profilesByUserId, [userId]: confirmed }, syncStatesByUserId: { ...state.syncStatesByUserId, [userId]: profileSyncState('ready'), }, })); return confirmed; } const profile = fromServerProfile(server); set((state) => ({ profilesByUserId: { ...state.profilesByUserId, [userId]: profile }, syncStatesByUserId: { ...state.syncStatesByUserId, [userId]: profileSyncState('ready'), }, })); return profile; } catch (error) { set((state) => ({ syncStatesByUserId: { ...state.syncStatesByUserId, [userId]: profileSyncState('error', getErrorMessage(error)), }, })); throw error; } })(); inFlightSyncRequests.set(userId, request); void request.then( () => { if (inFlightSyncRequests.get(userId) === request) inFlightSyncRequests.delete(userId); }, () => { if (inFlightSyncRequests.get(userId) === request) inFlightSyncRequests.delete(userId); }, ); return request; }, saveProfileToServer: async (userId, accessToken) => { const local = get().profilesByUserId[userId]; if (!local) throw new Error('个人资料尚未加载'); set((state) => ({ syncStatesByUserId: { ...state.syncStatesByUserId, [userId]: profileSyncState('saving'), }, })); try { const saved = fromServerProfile(await putAgentProfile(accessToken, toAgentProfileUpdate(local))); set((state) => ({ profilesByUserId: { ...state.profilesByUserId, [userId]: saved }, syncStatesByUserId: { ...state.syncStatesByUserId, [userId]: profileSyncState('ready'), }, })); return saved; } catch (error) { if (error instanceof AgentProfileApiError && error.status === 409) { try { const latest = fromServerProfile(await getAgentProfile(accessToken)); set((state) => ({ profilesByUserId: { ...state.profilesByUserId, [userId]: latest }, syncStatesByUserId: { ...state.syncStatesByUserId, [userId]: profileSyncState('ready'), }, })); } catch (refreshError) { set((state) => ({ syncStatesByUserId: { ...state.syncStatesByUserId, [userId]: profileSyncState('error', getErrorMessage(refreshError)), }, })); } } else { set((state) => ({ syncStatesByUserId: { ...state.syncStatesByUserId, [userId]: profileSyncState('error', getErrorMessage(error)), }, })); } throw error; } }, }), { name: 'niancode-user-agent-profiles', partialize: (state) => ({ profilesByUserId: state.profilesByUserId }), }, ), );