Makelore 2.0 initial clean snapshot
This commit is contained in:
294
src/stores/user-profile.ts
Normal file
294
src/stores/user-profile.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
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<string, AgentUserProfile>;
|
||||
syncStatesByUserId: Record<string, ProfileSyncState>;
|
||||
saveProfile: (userId: string, profile: UserProfileDraft) => void;
|
||||
syncProfile: (userId: string, accessToken: string) => Promise<AgentUserProfile>;
|
||||
saveProfileToServer: (userId: string, accessToken: string) => Promise<AgentUserProfile>;
|
||||
};
|
||||
|
||||
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<string, Promise<AgentUserProfile>>();
|
||||
|
||||
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<UserProfileState>()(
|
||||
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 }),
|
||||
},
|
||||
),
|
||||
);
|
||||
Reference in New Issue
Block a user