feat(agents): 复用 Yuxi 头像上传与展示

This commit is contained in:
brother7 committed 2026-10-12 13:48:38 +08:00
1 parent 0ca744fb9b
commit 235af24d6c
16 files changed
+381 -32

No files matched your search

+2
View File
@@ -41,6 +41,8 @@ export async function handleCloudAgentsRoutes(req: IncomingMessage, res: ServerR
data = await cloudAgents.rememberRecent(await parseJsonBody(req));
} else if (path === '/recovery/pending' && req.method === 'POST') {
data = await cloudAgents.resolvePending(await parseJsonBody(req));
} else if (path === '/avatars/pick' && req.method === 'POST') {
data = await cloudAgents.uploadAvatar();
} else if (path === '/attachments/pick' && req.method === 'POST') {
data = await cloudAgents.upload();
} else if (path === '/knowledge/pick' && req.method === 'POST') {
+3 -3
View File
@@ -8,9 +8,9 @@ const prompt = ['request_id', 'thread_id', 'query', 'expected_revision', 'attach
const request = ['request_id', 'thread_id', 'run_id', 'status', 'version'];
const schedule = ['name', 'prompt', 'cron_expression', 'timezone', 'enabled', 'result_notification'];
const job = ['id', 'agent_slug', ...schedule, 'next_run_at', 'runs'];
const entry = ['slug', 'name', 'purpose', 'published_version', 'is_creator', 'payer'];
const entry = ['slug', 'name', 'purpose', 'icon', 'published_version', 'is_creator', 'payer'];
const budget = ['agent_slug', 'unit', 'timezone', 'request_limit_points', 'daily_limit_points', 'daily_committed_points', 'resets_at'];
const draft = ['slug', 'name', 'purpose', 'system_prompt', 'draft_revision', 'updated_at', 'configuration', 'published_version', 'enabled', 'archived'];
const draft = ['slug', 'name', 'purpose', 'icon', 'system_prompt', 'draft_revision', 'updated_at', 'configuration', 'published_version', 'enabled', 'archived'];
const mcpInput = ['name', 'description', 'transport', 'url', 'headers'];
const mcpOutput = ['slug', 'name', 'description', 'transport', 'url', 'enabled', 'has_credentials'];
const child = ['name', 'purpose', 'system_prompt'];
@@ -228,7 +228,7 @@ const operations: Record<keyof CloudAgentOperations, OperationSpec> = {
importKnowledgeAttachment: { method: 'POST', path: '/agents/:slug/knowledge/:kb_id/import-attachment', body: ['operation_id', 'thread_id', 'attachment_id'], output: ['file_id', 'name', 'size', 'status', 'error', 'chunk_count', 'available', 'replaces_file_id', 'processing_task'] },
archiveAgent: { method: 'PUT', path: '/agents/:slug/archive', body: ['archived'], output: draft },
archiveThread: { method: 'PUT', path: '/threads/:thread_id/archive', body: ['archived'], output: ['thread_id', 'archived'] },
version: { method: 'GET', path: '/agents/:slug/versions/:version', output: ['version', 'created_at', 'name', 'purpose', 'system_prompt', 'configuration'] },
version: { method: 'GET', path: '/agents/:slug/versions/:version', output: ['version', 'created_at', 'name', 'purpose', 'icon', 'system_prompt', 'configuration'] },
restoreVersion: { method: 'POST', path: '/agents/:slug/versions/:version/restore', body: ['expected_revision'], output: draft },
scheduleContext: { method: 'GET', path: '/agents/:slug/schedule-context', output: ['version', 'enabled', 'configuration', 'result_destination', 'payer'] },
knowledge: { method: 'GET', path: '/agents/:slug/knowledge', output: ['databases', 'models'] },
+39 -8
View File
@@ -43,6 +43,8 @@ const MESSAGES: Record<string, string> = {
knowledge_processing: '知识库有文档正在处理,请完成后再维护',
agent_archived: '请先从归档恢复智能体,再启用',
attachment_too_large: '附件最大支持 5 MB',
avatar_too_large: '头像图片不能超过 5 MB',
avatar_invalid: '请选择有效的 PNG、JPEG、WebP 或 GIF 图片(最大 5 MB)',
download_failed: '文件保存失败,请重试',
channel_not_found: '微信渠道不存在或已解绑',
channel_operation_conflict: '微信渠道正在处理其他操作,请稍后查看状态',
@@ -104,6 +106,7 @@ function projectDraft(value: unknown): CloudAgentDraft {
try { return {
slug: slug(textField(item.slug, 80)),
name: textField(item.name, 100),
icon: item.icon == null ? null : textField(item.icon, 255, false),
purpose: textField(item.purpose, 2000),
system_prompt: textField(item.system_prompt, 32000, false),
draft_revision: revision(item.draft_revision),
@@ -183,11 +186,11 @@ export class CloudAgentsModule {
|| (body.next_cursor !== null && (typeof body.next_cursor !== 'string' || !/^\d{1,16}$/.test(body.next_cursor)))) {
throw new CloudAgentsError(502, 'cloud_service_unavailable');
}
return { agents: body.agents.map(projectDraft), next_cursor: body.next_cursor as string | null };
return { agents: body.agents.map(item => this.withAvatar(projectDraft(item))), next_cursor: body.next_cursor as string | null };
}
async get(agentSlug: string): Promise<CloudAgentDraft> {
return projectDraft(await this.request(`/api/makelore/agents/${slug(agentSlug)}`, 'GET'));
return this.withAvatar(projectDraft(await this.request(`/api/makelore/agents/${slug(agentSlug)}`, 'GET')));
}
async create(value: unknown): Promise<CloudAgentDraft> {
@@ -200,8 +203,9 @@ export class CloudAgentsModule {
operation_id: operationId,
name: textField(input.name, 100).trim(),
purpose: textField(input.purpose, 2000).trim(),
...(input.icon === undefined ? {} : { icon: input.icon === null ? null : textField(input.icon, 255, false) }),
};
return this.withRecovery('createAgent', { ...body }, () => this.request('/api/makelore/agents', 'POST', body).then(projectDraft));
return this.withRecovery('createAgent', { ...body }, () => this.request('/api/makelore/agents', 'POST', body).then(result => this.withAvatar(projectDraft(result))));
}
private async withRecovery<T>(operation: string, input: Record<string, unknown>, execute: () => Promise<T>): Promise<T> {
@@ -294,9 +298,10 @@ export class CloudAgentsModule {
name: textField(input.name, 100).trim(),
purpose: textField(input.purpose, 2000).trim(),
system_prompt: textField(input.system_prompt, 32000, false),
...(input.icon === undefined ? {} : { icon: input.icon === null ? null : textField(input.icon, 255, false) }),
...(input.configuration === undefined ? {} : { configuration: configuration(input.configuration) }),
};
return projectDraft(await this.request(`/api/makelore/agents/${slug(agentSlug)}/draft`, 'PATCH', body));
return this.withAvatar(projectDraft(await this.request(`/api/makelore/agents/${slug(agentSlug)}/draft`, 'PATCH', body)));
}
private async pollActivity(): Promise<void> {
@@ -353,6 +358,11 @@ export class CloudAgentsModule {
} else result = await invoke();
try {
const projected = plan.project(result);
if (plan.operation === 'received') {
const collection = record(projected);
return { ...collection, agents: Array.isArray(collection.agents) ? collection.agents.map(item => this.withAvatar(record(item))) : [] };
}
if (['entry', 'version', 'archiveAgent', 'restoreVersion'].includes(plan.operation)) return this.withAvatar(record(projected));
if (record(value).operation === 'access') {
// The address is public; both exchanged tokens stay in Main.
const binding = getWorksSquareAccountBinding();
@@ -369,6 +379,24 @@ export class CloudAgentsModule {
}
}
private withAvatar<T extends { icon?: unknown }>(value: T): T & { icon_url: string | null } {
let iconUrl: string | null = null;
if (typeof value.icon === 'string' && value.icon) {
try {
const url = new URL(value.icon, this.cached?.apiBaseUrl);
if (['https:', 'http:'].includes(url.protocol) && !url.username && !url.password) iconUrl = url.href;
} catch { /* Invalid legacy icons use the default avatar. */ }
}
return { ...value, icon_url: iconUrl };
}
async uploadAvatar(): Promise<{ icon: string; icon_url: string | null } | null> {
const body = await this.pickUpload(true);
if (!body) return null;
const result = record(await this.request('/api/makelore/avatars', 'POST', body));
return this.withAvatar({ icon: textField(result.icon, 255) });
}
async upload(): Promise<CloudUpload | null> {
const body = await this.pickUpload();
if (!body) return null;
@@ -402,20 +430,23 @@ export class CloudAgentsModule {
return { draft_id: result.draft_id, items: result.items };
}
private async pickUpload(): Promise<FormData | null> {
private async pickUpload(avatar = false): Promise<FormData | null> {
const binding = getWorksSquareAccountBinding();
if (!binding) throw new CloudAgentsError(401, 'session_expired');
const { dialog } = await import('electron');
const { readFile, stat } = await import('node:fs/promises');
const { basename } = await import('node:path');
const picked = await dialog.showOpenDialog({ title: '添加智能体附件(最大 5 MB)', properties: ['openFile'] });
const picked = await dialog.showOpenDialog({
title: avatar ? '选择智能体头像(最大 5 MB)' : '添加智能体附件(最大 5 MB)', properties: ['openFile'],
...(avatar ? { filters: [{ name: '头像图片', extensions: ['png', 'jpg', 'jpeg', 'webp', 'gif'] }] } : {}),
});
this.requireCurrent(binding);
if (picked.canceled || !picked.filePaths[0]) return null;
const path = picked.filePaths[0];
if ((await stat(path)).size > 5 * 1024 * 1024) throw new CloudAgentsError(422, 'attachment_too_large');
if ((await stat(path)).size > 5 * 1024 * 1024) throw new CloudAgentsError(422, avatar ? 'avatar_too_large' : 'attachment_too_large');
const bytes = await readFile(path);
this.requireCurrent(binding);
if (bytes.length > 5 * 1024 * 1024) throw new CloudAgentsError(422, 'attachment_too_large');
if (bytes.length > 5 * 1024 * 1024) throw new CloudAgentsError(422, avatar ? 'avatar_too_large' : 'attachment_too_large');
const body = new FormData();
body.append('file', new Blob([new Uint8Array(bytes)]), basename(path));
return body;