完善客户端模块与工作区能力
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { hostApiFetch } from '@/lib/host-api';
|
||||
import type { AgentSessionData } from '../../shared/agent-session';
|
||||
|
||||
export type AgentProfileGender = 'male' | 'female' | 'other' | null;
|
||||
|
||||
@@ -6,6 +7,7 @@ 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;
|
||||
@@ -22,34 +24,74 @@ export type AgentProfileUpdate = {
|
||||
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) {
|
||||
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', 'error', 'code']) {
|
||||
for (const field of ['message', 'msg', 'error_description', 'error']) {
|
||||
const value = record[field];
|
||||
if (typeof value === 'string' && value.trim()) return value.trim();
|
||||
}
|
||||
@@ -71,10 +113,12 @@ async function requestAgentProfile(
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -94,3 +138,49 @@ export function putAgentProfile(
|
||||
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' });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user