完善客户端模块与工作区能力
This commit is contained in:
@@ -42,6 +42,14 @@ type SpeechTranscriptionInput = {
|
||||
|
||||
type ImageGenerationInput = Record<string, unknown>;
|
||||
type AgentProfileUpdateInput = Record<string, unknown>;
|
||||
type AgentAvatarUploadInput = {
|
||||
fileName?: unknown;
|
||||
mimeType?: unknown;
|
||||
dataBase64?: unknown;
|
||||
};
|
||||
|
||||
const MAX_AGENT_AVATAR_BYTES = 4 * 1024 * 1024;
|
||||
const AGENT_AVATAR_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp']);
|
||||
|
||||
function readRequiredString(value: unknown, field: string): string {
|
||||
if (typeof value !== 'string' || !value.trim()) {
|
||||
@@ -93,7 +101,21 @@ function getErrorMessage(payload: unknown, fallback: string): string {
|
||||
for (const field of ['msg', 'message', 'error_description', 'error', 'detail']) {
|
||||
const value = record[field];
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
return value;
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// FastAPI and similar services may put the actual message inside a
|
||||
// structured `detail` object. Preserve it so the renderer can tell a
|
||||
// service outage from a missing migration or another actionable error.
|
||||
const detail = record.detail;
|
||||
if (detail && typeof detail === 'object' && !Array.isArray(detail)) {
|
||||
const detailRecord = detail as Record<string, unknown>;
|
||||
for (const field of ['msg', 'message', 'error_description', 'error']) {
|
||||
const value = detailRecord[field];
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,6 +125,18 @@ function getErrorMessage(payload: unknown, fallback: string): string {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function getPayloadErrorCode(payload: unknown): string | undefined {
|
||||
if (!isRecord(payload)) return undefined;
|
||||
const detail = isRecord(payload.detail) ? payload.detail : undefined;
|
||||
return readOptionalString(payload.code) ?? (detail ? readOptionalString(detail.code) : undefined);
|
||||
}
|
||||
|
||||
function stripReadOnlyAgentProfileFields(body: AgentProfileUpdateInput): AgentProfileUpdateInput {
|
||||
const writableFields = { ...body };
|
||||
delete writableFields.avatar_url;
|
||||
return writableFields;
|
||||
}
|
||||
|
||||
async function sendUpstreamError(
|
||||
res: ServerResponse,
|
||||
response: Response,
|
||||
@@ -574,7 +608,7 @@ async function handleAgentProfile(
|
||||
): Promise<void> {
|
||||
const accessToken = readRequiredHeader(req, 'x-niancode-access-token');
|
||||
const body = req.method === 'PUT'
|
||||
? await parseJsonBody<AgentProfileUpdateInput>(req)
|
||||
? stripReadOnlyAgentProfileFields(await parseJsonBody<AgentProfileUpdateInput>(req))
|
||||
: undefined;
|
||||
const response = await proxyAwareFetch(createWorksUrl('/api/user/agent-profile').toString(), {
|
||||
method: req.method,
|
||||
@@ -590,13 +624,22 @@ async function handleAgentProfile(
|
||||
const detail = payload && typeof payload === 'object' && !Array.isArray(payload)
|
||||
? (payload as Record<string, unknown>).detail
|
||||
: undefined;
|
||||
const code = getPayloadErrorCode(payload);
|
||||
const error = getErrorMessage(payload, `Agent Profile request failed (${response.status})`);
|
||||
logger.warn('[works] Agent Profile upstream request failed', {
|
||||
method: req.method,
|
||||
status: response.status,
|
||||
...(code === undefined ? {} : { code }),
|
||||
error,
|
||||
});
|
||||
// Keep the local Host API response successful so the renderer can inspect
|
||||
// the upstream status and conflict detail instead of losing it in the
|
||||
// generic Host API error parser.
|
||||
sendJson(res, 200, {
|
||||
success: false,
|
||||
status: response.status,
|
||||
error: getErrorMessage(payload, `Agent Profile request failed (${response.status})`),
|
||||
error,
|
||||
...(code === undefined ? {} : { code }),
|
||||
...(detail === undefined ? {} : { detail }),
|
||||
});
|
||||
return;
|
||||
@@ -605,6 +648,98 @@ async function handleAgentProfile(
|
||||
sendJson(res, response.status, { success: true, profile: payload });
|
||||
}
|
||||
|
||||
function createAgentAvatarUploadForm(body: AgentAvatarUploadInput): FormData {
|
||||
const fileName = (readOptionalString(body.fileName) ?? 'avatar.webp')
|
||||
.replace(/[\\/]/g, '_')
|
||||
.slice(0, 160);
|
||||
const mimeType = readRequiredString(body.mimeType, 'mimeType').toLowerCase();
|
||||
if (!AGENT_AVATAR_MIME_TYPES.has(mimeType)) {
|
||||
throw new Error('Unsupported avatar image type');
|
||||
}
|
||||
|
||||
const dataBase64 = readRequiredString(body.dataBase64, 'dataBase64');
|
||||
if (!/^[A-Za-z0-9+/]*={0,2}$/u.test(dataBase64) || dataBase64.length % 4 === 1) {
|
||||
throw new Error('Invalid avatar image data');
|
||||
}
|
||||
const bytes = Buffer.from(dataBase64, 'base64');
|
||||
if (bytes.length === 0) throw new Error('Avatar image data is empty');
|
||||
if (bytes.length > MAX_AGENT_AVATAR_BYTES) {
|
||||
throw new Error('Avatar image is too large');
|
||||
}
|
||||
|
||||
const form = new FormData();
|
||||
form.set('file', new Blob([new Uint8Array(bytes)], { type: mimeType }), fileName);
|
||||
return form;
|
||||
}
|
||||
|
||||
function normalizeAgentAvatarPayload(payload: unknown): {
|
||||
success: true;
|
||||
avatar_url: string | null;
|
||||
} {
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
||||
return { success: true, avatar_url: null };
|
||||
}
|
||||
|
||||
const record = payload as Record<string, unknown>;
|
||||
const profile = record.profile && typeof record.profile === 'object' && !Array.isArray(record.profile)
|
||||
? record.profile as Record<string, unknown>
|
||||
: undefined;
|
||||
const data = record.data && typeof record.data === 'object' && !Array.isArray(record.data)
|
||||
? record.data as Record<string, unknown>
|
||||
: undefined;
|
||||
const value = record.avatar_url ?? profile?.avatar_url ?? data?.avatar_url;
|
||||
return {
|
||||
success: true,
|
||||
avatar_url: typeof value === 'string' && value.trim() ? value.trim() : null,
|
||||
};
|
||||
}
|
||||
|
||||
async function handleAgentAvatar(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
): Promise<void> {
|
||||
const accessToken = readRequiredHeader(req, 'x-niancode-access-token');
|
||||
const form = req.method === 'POST'
|
||||
? createAgentAvatarUploadForm(await parseJsonBody<AgentAvatarUploadInput>(req))
|
||||
: undefined;
|
||||
const response = await proxyAwareFetch(createWorksUrl('/api/user/avatar').toString(), {
|
||||
method: req.method,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
...(form ? { body: form } : {}),
|
||||
});
|
||||
const payload = await readResponsePayload(response);
|
||||
|
||||
if (!response.ok) {
|
||||
const detail = payload && typeof payload === 'object' && !Array.isArray(payload)
|
||||
? (payload as Record<string, unknown>).detail
|
||||
: undefined;
|
||||
const code = getPayloadErrorCode(payload);
|
||||
const error = getErrorMessage(payload, `Agent Avatar request failed (${response.status})`);
|
||||
logger.warn('[works] Agent Avatar upstream request failed', {
|
||||
method: req.method,
|
||||
status: response.status,
|
||||
...(code === undefined ? {} : { code }),
|
||||
error,
|
||||
});
|
||||
sendJson(res, 200, {
|
||||
success: false,
|
||||
status: response.status,
|
||||
error,
|
||||
...(code === undefined ? {} : { code }),
|
||||
...(detail === undefined ? {} : { detail }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
sendJson(
|
||||
res,
|
||||
response.status === 204 ? 200 : response.status,
|
||||
normalizeAgentAvatarPayload(payload),
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSubmitImageGeneration(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
@@ -1054,6 +1189,11 @@ export async function handleWorksRoutes(
|
||||
}
|
||||
|
||||
try {
|
||||
if (url.pathname === '/api/works/user/avatar' && (req.method === 'POST' || req.method === 'DELETE')) {
|
||||
await handleAgentAvatar(req, res);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/works/user/agent-profile' && (req.method === 'GET' || req.method === 'PUT')) {
|
||||
await handleAgentProfile(req, res);
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user