import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { SshService } from './ssh.service'; interface AvatarCacheItem { expiresAt: number; contentType: string; data: Buffer; } @Injectable() export class AvatarService { private readonly logger = new Logger(AvatarService.name); private readonly cache = new Map(); private readonly ttlMs = 5 * 60 * 1000; constructor( private readonly ssh: SshService, private readonly config: ConfigService, ) {} async getAvatar(sister: { name: string; ip: string; user: string }) { const cached = this.cache.get(sister.name); if (cached && cached.expiresAt > Date.now()) { return cached; } const sshKeyPath = this.config.get('SSH_KEY_PATH'); if (!sshKeyPath) { return this.getFallback(sister.name); } const files = [ { path: '~/.hermes/avatar.png', type: 'image/png' }, { path: '~/.hermes/avatar.jpg', type: 'image/jpeg' }, { path: '~/.hermes/avatar.jpeg', type: 'image/jpeg' }, { path: '~/.hermes/avatar.webp', type: 'image/webp' }, { path: '~/.openclaw/avatar.png', type: 'image/png' }, { path: '~/.openclaw/avatar.jpg', type: 'image/jpeg' }, { path: '~/.openclaw/avatar.jpeg', type: 'image/jpeg' }, { path: '~/.openclaw/avatar.webp', type: 'image/webp' }, ]; for (const file of files) { try { const result = await this.ssh.executeCommand( sister.ip, sister.user, sshKeyPath, `if [ -f ${file.path} ]; then base64 -w0 ${file.path}; fi`, ); if (result.stdout.trim()) { const item = { contentType: file.type, data: Buffer.from(result.stdout.trim(), 'base64'), expiresAt: Date.now() + this.ttlMs, }; this.cache.set(sister.name, item); return item; } } catch { this.logger.warn(`Avatar fetch failed for ${sister.name}`); // 실패 시 오래된 캐시 삭제 (다음 요청에서 재시도) this.cache.delete(sister.name); } } return this.getFallback(sister.name); } private getFallback(name: string) { const initial = name.slice(0, 1).toUpperCase(); const svg = `${initial}`; return { contentType: 'image/svg+xml', data: Buffer.from(svg), expiresAt: Date.now() + this.ttlMs, }; } }