feat: extend Control UI assistant identity

This commit is contained in:
Peter Steinberger
2026-01-22 06:47:37 +00:00
parent 3125637ad6
commit 8544df36b8
24 changed files with 340 additions and 104 deletions

View File

@@ -3,84 +3,16 @@ import type { IncomingMessage, ServerResponse } from "node:http";
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { ClawdbotConfig } from "../config/config.js";
import { DEFAULT_ASSISTANT_IDENTITY, resolveAssistantIdentity } from "./assistant-identity.js";
const ROOT_PREFIX = "/";
const AVATAR_PREFIX = "/avatar";
// === Assistant Identity Resolution ===
const DEFAULT_ASSISTANT_IDENTITY = {
name: "Assistant",
avatar: "A",
};
interface AssistantIdentity {
name: string;
avatar: string;
}
interface AssistantConfig {
name?: string;
avatar?: string;
}
function parseIdentityMd(content: string): Partial<AssistantIdentity> {
const result: Partial<AssistantIdentity> = {};
const nameMatch = content.match(/^-\s*Name:\s*(.+)$/im);
if (nameMatch?.[1]) {
const name = nameMatch[1].trim();
if (name && name.length <= 50) {
result.name = name;
}
}
const emojiMatch = content.match(/^-\s*Emoji:\s*(.+)$/im);
if (emojiMatch?.[1]) {
const emoji = emojiMatch[1].trim();
if (emoji) {
result.avatar = emoji;
}
}
return result;
}
function resolveAssistantIdentity(opts?: {
configAssistant?: AssistantConfig;
workspaceDir?: string;
}): AssistantIdentity {
const { configAssistant, workspaceDir } = opts ?? {};
let name = DEFAULT_ASSISTANT_IDENTITY.name;
let avatar = DEFAULT_ASSISTANT_IDENTITY.avatar;
// Try IDENTITY.md from workspace
if (workspaceDir) {
try {
const identityPath = path.join(workspaceDir, "IDENTITY.md");
if (fs.existsSync(identityPath)) {
const identityMd = parseIdentityMd(fs.readFileSync(identityPath, "utf8"));
if (identityMd.name) name = identityMd.name;
if (identityMd.avatar) avatar = identityMd.avatar;
}
} catch {
// Ignore errors reading IDENTITY.md
}
}
// Config overrides IDENTITY.md
if (configAssistant?.name?.trim()) name = configAssistant.name.trim();
if (configAssistant?.avatar?.trim()) avatar = configAssistant.avatar.trim();
return { name, avatar };
}
// === End Assistant Identity ===
export type ControlUiRequestOptions = {
basePath?: string;
config?: {
ui?: {
assistant?: AssistantConfig;
};
};
workspaceDir?: string;
config?: ClawdbotConfig;
agentId?: string;
};
export function normalizeControlUiBasePath(basePath?: string): string {
@@ -252,8 +184,12 @@ function injectControlUiConfig(html: string, opts: ControlUiInjectionOpts): stri
const script =
`<script>` +
`window.__CLAWDBOT_CONTROL_UI_BASE_PATH__=${JSON.stringify(basePath)};` +
`window.__CLAWDBOT_ASSISTANT_NAME__=${JSON.stringify(assistantName ?? "Assistant")};` +
`window.__CLAWDBOT_ASSISTANT_AVATAR__=${JSON.stringify(assistantAvatar ?? "A")};` +
`window.__CLAWDBOT_ASSISTANT_NAME__=${JSON.stringify(
assistantName ?? DEFAULT_ASSISTANT_IDENTITY.name,
)};` +
`window.__CLAWDBOT_ASSISTANT_AVATAR__=${JSON.stringify(
assistantAvatar ?? DEFAULT_ASSISTANT_IDENTITY.avatar,
)};` +
`</script>`;
// Check if already injected
if (html.includes("__CLAWDBOT_ASSISTANT_NAME__")) return html;
@@ -266,16 +202,15 @@ function injectControlUiConfig(html: string, opts: ControlUiInjectionOpts): stri
interface ServeIndexHtmlOpts {
basePath: string;
config?: ControlUiRequestOptions["config"];
workspaceDir?: string;
config?: ClawdbotConfig;
agentId?: string;
}
function serveIndexHtml(res: ServerResponse, indexPath: string, opts: ServeIndexHtmlOpts) {
const { basePath, config, workspaceDir } = opts;
const identity = resolveAssistantIdentity({
configAssistant: config?.ui?.assistant,
workspaceDir,
});
const { basePath, config, agentId } = opts;
const identity = config
? resolveAssistantIdentity({ cfg: config, agentId })
: DEFAULT_ASSISTANT_IDENTITY;
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.setHeader("Cache-Control", "no-cache");
const raw = fs.readFileSync(indexPath, "utf8");
@@ -364,7 +299,11 @@ export function handleControlUiHttpRequest(
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
if (path.basename(filePath) === "index.html") {
serveIndexHtml(res, filePath, { basePath, config: opts?.config, workspaceDir: opts?.workspaceDir });
serveIndexHtml(res, filePath, {
basePath,
config: opts?.config,
agentId: opts?.agentId,
});
return true;
}
serveFile(res, filePath);
@@ -374,7 +313,11 @@ export function handleControlUiHttpRequest(
// SPA fallback (client-side router): serve index.html for unknown paths.
const indexPath = path.join(root, "index.html");
if (fs.existsSync(indexPath)) {
serveIndexHtml(res, indexPath, { basePath, config: opts?.config, workspaceDir: opts?.workspaceDir });
serveIndexHtml(res, indexPath, {
basePath,
config: opts?.config,
agentId: opts?.agentId,
});
return true;
}