187 lines
5.6 KiB
TypeScript
187 lines
5.6 KiB
TypeScript
import { hostApiFetch } from '@/lib/host-api';
|
|
import type { AgentSessionData } from '../../shared/agent-session';
|
|
|
|
export type AgentProfileGender = 'male' | 'female' | 'other' | null;
|
|
|
|
export type AgentProfileRead = {
|
|
display_name: string | null;
|
|
age: number | null;
|
|
gender: AgentProfileGender;
|
|
avatar_url?: string | null;
|
|
share_age_with_agents: boolean;
|
|
share_gender_with_agents: boolean;
|
|
analysis_enabled: boolean;
|
|
completed: boolean;
|
|
version: number;
|
|
updated_at: string | null;
|
|
};
|
|
|
|
export type AgentProfileUpdate = {
|
|
display_name: string;
|
|
age: number | null;
|
|
gender: AgentProfileGender;
|
|
share_age_with_agents: boolean;
|
|
share_gender_with_agents: boolean;
|
|
analysis_enabled: boolean;
|
|
version: number;
|
|
session_data?: AgentSessionData;
|
|
};
|
|
|
|
type AgentProfileEnvelope = {
|
|
success?: boolean;
|
|
status?: number;
|
|
code?: string;
|
|
profile?: AgentProfileRead;
|
|
error?: string;
|
|
detail?: unknown;
|
|
};
|
|
|
|
export type AgentAvatarUpload = {
|
|
fileName: string;
|
|
mimeType: string;
|
|
dataBase64: string;
|
|
};
|
|
|
|
type AgentAvatarEnvelope = {
|
|
success?: boolean;
|
|
status?: number;
|
|
code?: string;
|
|
avatar_url?: string | null;
|
|
profile?: Pick<AgentProfileRead, 'avatar_url'>;
|
|
error?: string;
|
|
detail?: unknown;
|
|
};
|
|
|
|
export class AgentProfileApiError extends Error {
|
|
readonly status: number;
|
|
readonly detail: unknown;
|
|
readonly code: string | null;
|
|
|
|
constructor(status: number, message: string, detail?: unknown, code?: string | null) {
|
|
super(message);
|
|
this.name = 'AgentProfileApiError';
|
|
this.status = status;
|
|
this.detail = detail;
|
|
this.code = code?.trim() || readDetailCode(detail);
|
|
}
|
|
}
|
|
|
|
export function getAgentProfileApiErrorMessage(error: AgentProfileApiError): string {
|
|
if (error.status === 401) return '登录已过期,请重新登录后再同步个人资料。';
|
|
if (error.code === 'avatar_unsupported_type') return '头像仅支持 PNG、JPEG 或 WebP 图片。';
|
|
if (error.code === 'avatar_file_too_large') return '头像文件超过服务端大小限制。';
|
|
if (error.code === 'avatar_invalid_file') return '头像文件无效或无法读取。';
|
|
if (error.code === 'avatar_storage_unavailable') return '头像存储服务暂时不可用,请稍后重试。';
|
|
if (error.status === 503) {
|
|
return readDetailMessage(error.detail)
|
|
? error.message
|
|
: '个人资料服务暂时不可用,请稍后重试。';
|
|
}
|
|
return error.message;
|
|
}
|
|
|
|
function readDetailCode(detail: unknown): string | null {
|
|
if (!detail || typeof detail !== 'object' || Array.isArray(detail)) return null;
|
|
const value = (detail as Record<string, unknown>).code;
|
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
}
|
|
|
|
function readDetailMessage(detail: unknown): string | null {
|
|
if (typeof detail === 'string' && detail.trim()) return detail.trim();
|
|
if (!detail || typeof detail !== 'object' || Array.isArray(detail)) return null;
|
|
|
|
const record = detail as Record<string, unknown>;
|
|
for (const field of ['message', 'msg', 'error_description', 'error']) {
|
|
const value = record[field];
|
|
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function requestAgentProfile(
|
|
accessToken: string,
|
|
init: RequestInit = {},
|
|
): Promise<AgentProfileRead> {
|
|
const response = await hostApiFetch<AgentProfileEnvelope>('/api/works/user/agent-profile', {
|
|
...init,
|
|
headers: {
|
|
'X-NianCode-Access-Token': accessToken,
|
|
...init.headers,
|
|
},
|
|
});
|
|
|
|
if (!response?.success || !response.profile) {
|
|
const status = typeof response?.status === 'number' ? response.status : 502;
|
|
const detailMessage = readDetailMessage(response?.detail);
|
|
const code = response?.code?.trim() || readDetailCode(response?.detail);
|
|
throw new AgentProfileApiError(
|
|
status,
|
|
response?.error || detailMessage || `Agent Profile request failed (${status})`,
|
|
response?.detail,
|
|
code,
|
|
);
|
|
}
|
|
|
|
return response.profile;
|
|
}
|
|
|
|
export function getAgentProfile(accessToken: string): Promise<AgentProfileRead> {
|
|
return requestAgentProfile(accessToken);
|
|
}
|
|
|
|
export function putAgentProfile(
|
|
accessToken: string,
|
|
update: AgentProfileUpdate,
|
|
): Promise<AgentProfileRead> {
|
|
return requestAgentProfile(accessToken, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(update),
|
|
});
|
|
}
|
|
|
|
function readAvatarUrl(response: AgentAvatarEnvelope): string | null {
|
|
const value = response.avatar_url ?? response.profile?.avatar_url;
|
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
}
|
|
|
|
async function requestAgentAvatar(
|
|
accessToken: string,
|
|
init: RequestInit = {},
|
|
): Promise<string | null> {
|
|
const response = await hostApiFetch<AgentAvatarEnvelope>('/api/works/user/avatar', {
|
|
...init,
|
|
headers: {
|
|
'X-NianCode-Access-Token': accessToken,
|
|
...init.headers,
|
|
},
|
|
});
|
|
|
|
if (!response?.success) {
|
|
const status = typeof response?.status === 'number' ? response.status : 502;
|
|
const detailMessage = readDetailMessage(response?.detail);
|
|
const code = response?.code?.trim() || readDetailCode(response?.detail);
|
|
throw new AgentProfileApiError(
|
|
status,
|
|
response?.error || detailMessage || `Agent Avatar request failed (${status})`,
|
|
response?.detail,
|
|
code,
|
|
);
|
|
}
|
|
|
|
return readAvatarUrl(response);
|
|
}
|
|
|
|
export function uploadAgentAvatar(
|
|
accessToken: string,
|
|
upload: AgentAvatarUpload,
|
|
): Promise<string | null> {
|
|
return requestAgentAvatar(accessToken, {
|
|
method: 'POST',
|
|
body: JSON.stringify(upload),
|
|
});
|
|
}
|
|
|
|
export function deleteAgentAvatar(accessToken: string): Promise<string | null> {
|
|
return requestAgentAvatar(accessToken, { method: 'DELETE' });
|
|
}
|