feat: add agent avatar support (#1329) (thanks @dlauer)
This commit is contained in:
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const ROOT_PREFIX = "/";
|
||||
const AVATAR_PREFIX = "/avatar";
|
||||
|
||||
export type ControlUiRequestOptions = {
|
||||
basePath?: string;
|
||||
@@ -62,6 +63,10 @@ function contentTypeForExt(ext: string): string {
|
||||
case ".jpg":
|
||||
case ".jpeg":
|
||||
return "image/jpeg";
|
||||
case ".gif":
|
||||
return "image/gif";
|
||||
case ".webp":
|
||||
return "image/webp";
|
||||
case ".ico":
|
||||
return "image/x-icon";
|
||||
case ".txt":
|
||||
@@ -71,6 +76,83 @@ function contentTypeForExt(ext: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
export type ControlUiAvatarResolution =
|
||||
| { kind: "none"; reason: string }
|
||||
| { kind: "local"; filePath: string }
|
||||
| { kind: "remote"; url: string }
|
||||
| { kind: "data"; url: string };
|
||||
|
||||
type ControlUiAvatarMeta = {
|
||||
avatarUrl: string | null;
|
||||
};
|
||||
|
||||
function sendJson(res: ServerResponse, status: number, body: unknown) {
|
||||
res.statusCode = status;
|
||||
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
res.setHeader("Cache-Control", "no-cache");
|
||||
res.end(JSON.stringify(body));
|
||||
}
|
||||
|
||||
function buildAvatarUrl(basePath: string, agentId: string): string {
|
||||
return basePath ? `${basePath}${AVATAR_PREFIX}/${agentId}` : `${AVATAR_PREFIX}/${agentId}`;
|
||||
}
|
||||
|
||||
function isValidAgentId(agentId: string): boolean {
|
||||
return /^[a-z0-9][a-z0-9_-]{0,63}$/i.test(agentId);
|
||||
}
|
||||
|
||||
export function handleControlUiAvatarRequest(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
opts: { basePath?: string; resolveAvatar: (agentId: string) => ControlUiAvatarResolution },
|
||||
): boolean {
|
||||
const urlRaw = req.url;
|
||||
if (!urlRaw) return false;
|
||||
if (req.method !== "GET" && req.method !== "HEAD") return false;
|
||||
|
||||
const url = new URL(urlRaw, "http://localhost");
|
||||
const basePath = normalizeControlUiBasePath(opts.basePath);
|
||||
const pathname = url.pathname;
|
||||
const pathWithBase = basePath ? `${basePath}${AVATAR_PREFIX}/` : `${AVATAR_PREFIX}/`;
|
||||
if (!pathname.startsWith(pathWithBase)) return false;
|
||||
|
||||
const agentIdParts = pathname.slice(pathWithBase.length).split("/").filter(Boolean);
|
||||
const agentId = agentIdParts[0] ?? "";
|
||||
if (agentIdParts.length !== 1 || !agentId || !isValidAgentId(agentId)) {
|
||||
respondNotFound(res);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.searchParams.get("meta") === "1") {
|
||||
const resolved = opts.resolveAvatar(agentId);
|
||||
const avatarUrl =
|
||||
resolved.kind === "local"
|
||||
? buildAvatarUrl(basePath, agentId)
|
||||
: resolved.kind === "remote" || resolved.kind === "data"
|
||||
? resolved.url
|
||||
: null;
|
||||
sendJson(res, 200, { avatarUrl } satisfies ControlUiAvatarMeta);
|
||||
return true;
|
||||
}
|
||||
|
||||
const resolved = opts.resolveAvatar(agentId);
|
||||
if (resolved.kind !== "local") {
|
||||
respondNotFound(res);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (req.method === "HEAD") {
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", contentTypeForExt(path.extname(resolved.filePath).toLowerCase()));
|
||||
res.setHeader("Cache-Control", "no-cache");
|
||||
res.end();
|
||||
return true;
|
||||
}
|
||||
|
||||
serveFile(res, resolved.filePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
function respondNotFound(res: ServerResponse) {
|
||||
res.statusCode = 404;
|
||||
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
||||
|
||||
@@ -11,7 +11,9 @@ import { handleA2uiHttpRequest } from "../canvas-host/a2ui.js";
|
||||
import type { CanvasHostHandler } from "../canvas-host/server.js";
|
||||
import type { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { handleSlackHttpRequest } from "../slack/http/index.js";
|
||||
import { handleControlUiHttpRequest } from "./control-ui.js";
|
||||
import { resolveAgentAvatar } from "../agents/identity-avatar.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { handleControlUiAvatarRequest, handleControlUiHttpRequest } from "./control-ui.js";
|
||||
import {
|
||||
extractHookToken,
|
||||
getHookChannelError,
|
||||
@@ -244,6 +246,13 @@ export function createGatewayHttpServer(opts: {
|
||||
if (await canvasHost.handleHttpRequest(req, res)) return;
|
||||
}
|
||||
if (controlUiEnabled) {
|
||||
if (
|
||||
handleControlUiAvatarRequest(req, res, {
|
||||
basePath: controlUiBasePath,
|
||||
resolveAvatar: (agentId) => resolveAgentAvatar(loadConfig(), agentId),
|
||||
})
|
||||
)
|
||||
return;
|
||||
if (
|
||||
handleControlUiHttpRequest(req, res, {
|
||||
basePath: controlUiBasePath,
|
||||
|
||||
Reference in New Issue
Block a user