mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
bot tag and profile images yay urls only though...
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
# main idea
|
||||
- add a way for a spectator bot to show up as a bot with the bot tag in a message
|
||||
- just like chat: {bot: true} basically
|
||||
- having this would make it show as a bot in the web UI and discord
|
||||
- add a system for custom profile images that can be defined per chat message
|
||||
- image shows as circle in chat row, like discord icons, or as the webhook icon
|
||||
- only accept image URLs
|
||||
- chat: {profileImage: "https://image.com/image.png"}
|
||||
- add setting in server config for the overseer's image URL
|
||||
- only do this with overseer control service, not the old llm commentary service
|
||||
- make bot: true messages show up as "name [BOT]" or something in discord instead of like now "name - No rover"
|
||||
- make spectators show as "name [SPECTATOR]" in discord, again instead of the current
|
||||
@@ -20,6 +20,7 @@ overseerControl:
|
||||
name: "The Overseer"
|
||||
model: "qwen2.5:7b-instruct"
|
||||
ollamaServer: "http://127.0.0.1:11434"
|
||||
profileImageUrl: "https://example.com/overseer.png"
|
||||
gateIntervalMs: 2000
|
||||
heartbeatMs: 30000
|
||||
media:
|
||||
|
||||
@@ -15,6 +15,8 @@ Rules:
|
||||
- Respect safety limits, lock policies, cooldowns, and blocked tools.
|
||||
- Do not invent tools.
|
||||
- Ask a question only if a required action parameter is missing.
|
||||
- Do not make up names for yourself.
|
||||
- Do not narrarate your actions. Speak in first person.
|
||||
|
||||
Chat style:
|
||||
- Always use English.
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,8 +11,8 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Roomba Rover" />
|
||||
<title>Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-Dj4tGQk7.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-tJ18YrEo.css">
|
||||
<script type="module" crossorigin src="/assets/index-CZZyVOuh.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CR1Y7EEc.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -26,6 +26,19 @@ function isPrivateClosedRoverId(roverId) {
|
||||
return roverManager.canReplayRoverId(roverId) !== true;
|
||||
}
|
||||
|
||||
function normalizeProfileImageUrl(value) {
|
||||
const raw = String(value || '').trim();
|
||||
if (!raw) return null;
|
||||
try {
|
||||
const parsed = new URL(raw);
|
||||
const protocol = String(parsed.protocol || '').toLowerCase();
|
||||
if (protocol !== 'http:' && protocol !== 'https:') return null;
|
||||
return parsed.toString();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isChargingFromSensors(sensors = {}) {
|
||||
const label = String(sensors?.chargingState?.label || '').toLowerCase();
|
||||
if (label === 'waiting' || label === 'full charging' || label === 'trickle charging') return true;
|
||||
@@ -103,10 +116,10 @@ function buildMessage(socket, text, meta = {}) {
|
||||
discordUserId: meta.discordUserId || null,
|
||||
discordUserName: meta.discordUserName || null,
|
||||
discordUserAvatarUrl: meta.discordUserAvatarUrl || null,
|
||||
profileImage: normalizeProfileImageUrl(meta.profileImage),
|
||||
roverCtx: meta.roverCtx || null,
|
||||
text,
|
||||
tts: meta.tts || null,
|
||||
system: Boolean(meta.system),
|
||||
bot: Boolean(meta.bot),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ function createHandlers({ sendSystemMessage }) {
|
||||
cb({ success: true });
|
||||
}
|
||||
|
||||
function sendExternalMessage({ text, nickname = 'Discord', role = 'admin', roverId = null, discordGuildId = null, discordGuildName = null, discordGuildIconUrl = null, discordChannelId = null, discordUserId = null, discordUserName = null, discordUserAvatarUrl = null }) {
|
||||
function sendExternalMessage({ text, nickname = 'Discord', role = 'admin', roverId = null, discordGuildId = null, discordGuildName = null, discordGuildIconUrl = null, discordChannelId = null, discordUserId = null, discordUserName = null, discordUserAvatarUrl = null, bot = false, profileImage = null }) {
|
||||
const normalized = normalizeUserText(text);
|
||||
const clean = normalized.trim();
|
||||
if (!clean || clean.length > 400) throw new Error('Message invalid');
|
||||
@@ -66,6 +66,8 @@ function createHandlers({ sendSystemMessage }) {
|
||||
discordUserId,
|
||||
discordUserName,
|
||||
discordUserAvatarUrl,
|
||||
bot,
|
||||
profileImage,
|
||||
});
|
||||
|
||||
logger.info('External chat message', { roverId, nickname });
|
||||
|
||||
@@ -17,8 +17,8 @@ function sendSystemMessage(text, options = {}) {
|
||||
nickname: String(options.nickname || 'The Overseer'),
|
||||
role: 'user',
|
||||
fromDiscord: false,
|
||||
system: true,
|
||||
bot: true,
|
||||
bot: options.bot !== false,
|
||||
profileImage: options.profileImage || null,
|
||||
});
|
||||
broadcastMessage(message);
|
||||
return message;
|
||||
|
||||
@@ -49,7 +49,7 @@ function buildAccessNoticeText(mode, reasonText) {
|
||||
}
|
||||
|
||||
function shouldSendAccessNotice(message) {
|
||||
if (!message?.text || message.system) return false;
|
||||
if (!message?.text || message.bot) return false;
|
||||
const mode = getMode();
|
||||
if (mode !== MODES.ADMIN && mode !== MODES.LOCKDOWN) return false;
|
||||
if (!ACCESS_KEYWORD_RE.test(message.text)) return false;
|
||||
|
||||
@@ -28,7 +28,7 @@ function pushHistory(message) {
|
||||
function getRecentMessages(limit = 20, options = {}) {
|
||||
const safeLimit = Number.isFinite(limit) ? Math.max(0, Math.floor(limit)) : 20;
|
||||
const includeSystem = options?.includeSystem !== false;
|
||||
const source = includeSystem ? history : history.filter((entry) => !entry?.system);
|
||||
const source = includeSystem ? history : history.filter((entry) => !entry?.bot);
|
||||
return source.slice(-safeLimit);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ function createChatBridgeHandlers(deps) {
|
||||
|
||||
const text = payload.text?.length > 1900 ? `${payload.text.slice(0, 1897)}...` : payload.text;
|
||||
const username = formatWebhookUsername(payload);
|
||||
const avatarURL = payload.fromDiscord ? payload.discordUserAvatarUrl || null : client.user?.displayAvatarURL?.({ extension: 'png', size: 128 }) || null;
|
||||
const avatarURL = payload.profileImage || (payload.fromDiscord ? payload.discordUserAvatarUrl || null : client.user?.displayAvatarURL?.({ extension: 'png', size: 128 }) || null);
|
||||
const typingId = getTypingId(payload);
|
||||
|
||||
guildConfigs.forEach((entry) => {
|
||||
|
||||
@@ -17,15 +17,15 @@ function formatDuration(ms) {
|
||||
|
||||
function formatWebhookUsername(payload) {
|
||||
const name = payload.nickname || payload.socketId?.slice(0, 6) || 'unknown';
|
||||
const botTag = payload.bot ? ' [BOT]' : '';
|
||||
const spectatorTag = payload.role === 'spectator' ? ' [SPECTATOR]' : '';
|
||||
const adminTag = payload.role === 'admin' || payload.role === 'lockdown' || payload.role === 'lockdown-admin' ? ' [Rover Admin]' : '';
|
||||
if (payload.fromDiscord) {
|
||||
const origin = payload.discordGuildName ? ` (From: ${payload.discordGuildName})` : '';
|
||||
const adminTag = payload.role === 'admin' || payload.role === 'lockdown' || payload.role === 'lockdown-admin' ? ' [Rover Admin]' : '';
|
||||
return `${name}${origin}${adminTag}`;
|
||||
return `${name}${origin}${botTag}${spectatorTag}${adminTag}`;
|
||||
}
|
||||
const roverText = payload.roverId ? `Rover: ${payload.roverId}` : `No rover`;
|
||||
const roleText = payload.role === 'admin' || payload.role === 'lockdown' || payload.role === 'lockdown-admin' ? 'Admin' : null;
|
||||
const suffix = [roverText, roleText].filter(Boolean).join(' · ');
|
||||
return suffix ? `${name} · ${suffix}` : name;
|
||||
const roverTag = payload.roverId ? ` [${payload.roverId}]` : '';
|
||||
return `${name}${botTag}${spectatorTag}${adminTag}${roverTag}`;
|
||||
}
|
||||
|
||||
function getTypingId(payload = {}) {
|
||||
|
||||
@@ -223,7 +223,7 @@ function createRunner(deps) {
|
||||
updatePhase('decision_post', { lastGeneratedText: text });
|
||||
const recentBotMessages = getRecentMessages(120, { includeSystem: true })
|
||||
.filter((entry) => Number(entry?.ts) >= runtime.contextResetAt)
|
||||
.filter((entry) => entry?.system)
|
||||
.filter((entry) => entry?.bot)
|
||||
.slice(-Math.max(3, maxBotMessages));
|
||||
const duplicateKey = normalizeDuplicateKey(text);
|
||||
const duplicate = recentBotMessages.some(
|
||||
|
||||
@@ -371,7 +371,7 @@ function createSnapshotEngine(deps) {
|
||||
return roverManager.canReplayRoverId(roverId);
|
||||
});
|
||||
const chatRecent = allRecentMessages
|
||||
.filter((entry) => !entry?.system)
|
||||
.filter((entry) => !entry?.bot)
|
||||
.slice(-MAX_CHAT_MESSAGES)
|
||||
.map((entry) => ({
|
||||
nickname: entry.nickname || entry.socketId?.slice(0, 6) || 'unknown',
|
||||
@@ -380,7 +380,7 @@ function createSnapshotEngine(deps) {
|
||||
|
||||
const botRecentWindow = allRecentMessages
|
||||
.filter((entry) => Number(entry?.ts) >= contextResetAt)
|
||||
.filter((entry) => entry?.system);
|
||||
.filter((entry) => entry?.bot);
|
||||
const lastBotMessage = botRecentWindow.length ? botRecentWindow[botRecentWindow.length - 1] : null;
|
||||
const botRecent30m = botRecentWindow.filter(
|
||||
(entry) => nowMs - Number(entry?.ts || 0) <= SELF_TALK_WINDOW_MS,
|
||||
@@ -408,7 +408,7 @@ function createSnapshotEngine(deps) {
|
||||
summary: entry.summary || '',
|
||||
};
|
||||
}
|
||||
if (entry?.system) {
|
||||
if (entry?.bot) {
|
||||
return {
|
||||
type: 'bot',
|
||||
nickname: entry.nickname || 'Rover Bot',
|
||||
|
||||
@@ -49,7 +49,7 @@ function buildConversation({ recentMessages, name }) {
|
||||
(recentMessages || []).forEach((entry) => {
|
||||
const text = String(entry?.text || '').trim();
|
||||
if (!text) return;
|
||||
const isAssistant = Boolean(entry?.bot || entry?.system);
|
||||
const isAssistant = Boolean(entry?.bot);
|
||||
const nickname = String(entry?.nickname || (isAssistant ? name || 'Overseer' : 'user')).trim();
|
||||
if (isAssistant) {
|
||||
messages.push({ role: 'assistant', content: text });
|
||||
|
||||
@@ -39,6 +39,7 @@ const ollamaUrl = String(overseerConfig.ollamaUrl || overseerConfig.ollamaServer
|
||||
const gateIntervalMs = normalizeMs(Number(overseerConfig.gateIntervalMs), DEFAULT_GATE_INTERVAL_MS);
|
||||
const heartbeatMs = normalizeMs(Number(overseerConfig.heartbeatMs), DEFAULT_HEARTBEAT_MS);
|
||||
const alwaysRunModel = Boolean(overseerConfig.alwaysRunModel);
|
||||
const profileImageUrl = String(overseerConfig.profileImageUrl || '').trim() || null;
|
||||
const ollamaClient = ollamaUrl ? new Ollama({ host: ollamaUrl }) : null;
|
||||
|
||||
const runtime = {
|
||||
@@ -280,7 +281,7 @@ async function runDecision(triggerReason) {
|
||||
|
||||
if (!observeOnly) {
|
||||
if ((decision === 'CHAT' || decision === 'ACTION+CHAT') && chatDraft) {
|
||||
sendSystemMessage(chatDraft, { nickname: name });
|
||||
sendSystemMessage(chatDraft, { nickname: name, bot: true, profileImage: profileImageUrl });
|
||||
actionResults.push({ kind: 'chat', ok: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ module.exports = {
|
||||
async execute({ args = {}, sendSystemMessage, name }) {
|
||||
const text = String(args?.text || '').trim();
|
||||
if (!text) throw new Error('chat_say requires args.text');
|
||||
sendSystemMessage(text, { nickname: name });
|
||||
sendSystemMessage(text, { nickname: name, bot: true });
|
||||
return { ok: true };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -62,7 +62,7 @@ export function buildLlmConversationRowsFromMessages(modelMessages, rawOutput) {
|
||||
nickname,
|
||||
text: content,
|
||||
role: 'spectator',
|
||||
system: role === 'system',
|
||||
bot: role === 'system',
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -75,7 +75,7 @@ export function buildLlmConversationRowsFromMessages(modelMessages, rawOutput) {
|
||||
nickname: 'LLM Output',
|
||||
text: raw.trim() ? raw : '<empty>',
|
||||
role: 'spectator',
|
||||
system: true,
|
||||
bot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ function roleColors(role) {
|
||||
}
|
||||
}
|
||||
|
||||
function isBotSystemMessage(message) {
|
||||
return Boolean(message?.system);
|
||||
function isBotMessage(message) {
|
||||
return Boolean(message?.bot);
|
||||
}
|
||||
|
||||
function formatTime(ts) {
|
||||
@@ -65,24 +65,39 @@ function DiscordAvatar({ guildIconUrl, userAvatarUrl, label }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ProfileAvatar({ imageUrl, label }) {
|
||||
if (!imageUrl) return null;
|
||||
return (
|
||||
<span className="flex h-4 w-4 overflow-hidden rounded-full border border-slate-700/80" title={label}>
|
||||
<span
|
||||
className="h-full w-full bg-cover bg-center bg-no-repeat"
|
||||
style={{ backgroundImage: `url(${imageUrl})` }}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChatIdentity({ message }) {
|
||||
const discordLabel = message.fromDiscord
|
||||
? `${message.discordGuildName || 'Discord'} · ${displayName(message)}`
|
||||
: null;
|
||||
const isBot = isBotSystemMessage(message);
|
||||
const isBot = isBotMessage(message);
|
||||
const nameClass = isBot ? 'text-emerald-300' : roleColors(message.role);
|
||||
return (
|
||||
<>
|
||||
{message.fromDiscord ? (
|
||||
<>
|
||||
<FaDiscord className="h-3.5 w-3.5 text-indigo-200" />
|
||||
<DiscordAvatar
|
||||
guildIconUrl={message.discordGuildIconUrl}
|
||||
userAvatarUrl={message.discordUserAvatarUrl}
|
||||
label={discordLabel}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<ProfileAvatar imageUrl={message.profileImage} label={discordLabel || displayName(message)} />
|
||||
{!message.profileImage ? (
|
||||
<DiscordAvatar
|
||||
guildIconUrl={message.discordGuildIconUrl}
|
||||
userAvatarUrl={message.discordUserAvatarUrl}
|
||||
label={discordLabel}
|
||||
/>
|
||||
) : null}
|
||||
<span className={`font-semibold text-[0.85rem] ${nameClass}`}>
|
||||
{displayName(message)}
|
||||
</span>
|
||||
@@ -104,7 +119,7 @@ export function ChatIdentity({ message }) {
|
||||
}
|
||||
|
||||
function chatRowClass(message) {
|
||||
if (isBotSystemMessage(message)) {
|
||||
if (isBotMessage(message)) {
|
||||
return 'surface-muted relative flex flex-wrap items-start gap-0.5 border border-emerald-500/40 bg-emerald-900/15 text-sm';
|
||||
}
|
||||
const isAdmin =
|
||||
@@ -119,7 +134,7 @@ function chatRowClass(message) {
|
||||
}
|
||||
|
||||
export default function ChatMessageRow({ message }) {
|
||||
const isBot = isBotSystemMessage(message);
|
||||
const isBot = isBotMessage(message);
|
||||
return (
|
||||
<div className={chatRowClass(message)}>
|
||||
<ChatIdentity message={message} />
|
||||
|
||||
@@ -379,16 +379,8 @@ export default function RoverMediaPlayer({
|
||||
const handleStatus = (nextStatus, info) => {
|
||||
if (!active) return;
|
||||
logAudio('audio/status', { nextStatus, info: info || null });
|
||||
setAudioDetail(info || (nextStatus === 'connected' ? 'connected' : null));
|
||||
setAudioStatus((prev) => {
|
||||
if (nextStatus === 'connected' && (prev === 'playing' || prev === 'connecting')) {
|
||||
return prev;
|
||||
}
|
||||
if (nextStatus === 'new') {
|
||||
return prev;
|
||||
}
|
||||
return nextStatus;
|
||||
});
|
||||
setAudioStatus(nextStatus);
|
||||
setAudioDetail(info || null);
|
||||
if (['error', 'failed'].includes(nextStatus)) {
|
||||
scheduleAudioRestart();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user