Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/client/src/api/hermes/group-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface RoomInfo {
maxHistoryTokens?: number
tailMessageCount?: number
totalTokens?: number
defaultAgentId?: string | null
}

export interface RoomAgent {
Expand Down Expand Up @@ -226,3 +227,11 @@ export async function forceCompress(roomId: string): Promise<{ success: boolean;
method: 'POST',
})
}

export async function setDefaultAgent(roomId: string, agentId: string): Promise<{ success: boolean }> {
return request(`/api/hermes/group-chat/rooms/${roomId}/default-agent`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ agentId }),
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const isCompressing = ref(false)
const selectedProfile = ref<string | null>(null)
const agentName = ref('')
const agentDescription = ref('')
const setAsDefaultAgent = ref(false)
const cloneSourceRoomId = ref<string | null>(null)
const cloneRoomName = ref('')
const cloneInviteCode = ref('')
Expand Down Expand Up @@ -186,11 +187,13 @@ async function confirmAddAgent() {
profile: selectedProfile.value,
name: agentName.value.trim() || undefined,
description: agentDescription.value.trim() || undefined,
setDefaultAgent: setAsDefaultAgent.value,
})
showAddAgentModal.value = false
selectedProfile.value = null
agentName.value = ''
agentDescription.value = ''
setAsDefaultAgent.value = false
message.success(t('groupChat.agentAdded'))
} catch (err: any) {
if (err.message?.includes('already')) {
Expand Down Expand Up @@ -252,6 +255,16 @@ async function handleRemoveAgent(agentId: string) {
}
}

async function handleSetDefaultAgent(agentId: string) {
if (!store.currentRoomId) return
try {
await store.setDefaultAgent(store.currentRoomId, agentId)
message.success(t('groupChat.defaultAgentSet'))
} catch {
message.error(t('common.saveFailed'))
}
}

async function handleInterruptAgent(agentName: string) {
try {
await store.interruptAgent(agentName)
Expand Down Expand Up @@ -366,13 +379,16 @@ watch(() => store.sortedMessages.length, async () => {
</div>
</div>
<div class="agent-popover-title">{{ t('groupChat.agents') }} ({{ store.agents.length }})</div>
<div v-for="agent in store.agents" :key="agent.id" class="agent-popover-item">
<div v-for="agent in store.agents" :key="agent.id" class="agent-popover-item" @click="handleSetDefaultAgent(agent.id)">
<ProfileAvatar class="agent-avatar" :name="agentAvatarName(agent)" :avatar="profileAvatarFor(agent.profile)" :size="28" />
<div class="agent-popover-info">
<span class="agent-popover-name">{{ agent.name }}</span>
<span class="agent-popover-name">
{{ agent.name }}
<span v-if="store.rooms.find(r => r.id === store.currentRoomId)?.defaultAgentId === agent.id" class="default-badge">{{ t('groupChat.default') }}</span>
</span>
<span class="agent-popover-profile">{{ agent.profile }}</span>
</div>
<button class="agent-popover-remove" @click="handleRemoveAgent(agent.id)">
<button class="agent-popover-remove" @click.stop="handleRemoveAgent(agent.id)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
Expand Down Expand Up @@ -519,6 +535,12 @@ watch(() => store.sortedMessages.length, async () => {
:placeholder="t('groupChat.agentDescPlaceholder')"
/>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" v-model="setAsDefaultAgent" />
{{ t('groupChat.setAsDefaultAgent') }}
</label>
</div>
<div class="modal-actions">
<NSpace justify="end">
<NButton @click="showAddAgentModal = false">{{ t('common.cancel') }}</NButton>
Expand Down Expand Up @@ -1235,6 +1257,32 @@ export default defineComponent({ components: { CreateRoomForm } })
margin: 4px 0 0;
}

.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: $text-secondary;
cursor: pointer;

input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
}
}

.default-badge {
display: inline-block;
margin-left: 6px;
padding: 1px 6px;
font-size: 10px;
font-weight: 600;
color: var(--accent-primary);
background: rgba(var(--accent-primary-rgb), 0.1);
border-radius: 4px;
}

// ─── Connection Dot ──────────────────────────────────────

.connection-dot {
Expand Down
3 changes: 3 additions & 0 deletions packages/client/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1090,6 +1090,9 @@ export default {
compressionSaved: 'Compression config saved',
compressNow: 'Compress Now',
compressingInProgress: 'Compression in progress, please wait',
setAsDefaultAgent: 'Set as default agent',
defaultAgentSet: 'Default agent updated',
default: 'Default',
},

// Usage
Expand Down
3 changes: 3 additions & 0 deletions packages/client/src/i18n/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1092,6 +1092,9 @@ export default {
compressionSaved: '压缩配置已保存',
compressNow: '立即压缩',
compressingInProgress: '正在压缩中,请稍后',
setAsDefaultAgent: '设为默认智能体',
defaultAgentSet: '默认智能体已更新',
default: '默认',
},

// 用量统计
Expand Down
34 changes: 32 additions & 2 deletions packages/client/src/stores/hermes/group-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
cloneRoom as cloneRoomApi,
deleteRoom as deleteRoomApi,
clearRoomContext,
setDefaultAgent as setDefaultAgentApi,
} from '@/api/hermes/group-chat'

async function uploadGroupFiles(attachments: Attachment[]): Promise<{ name: string; path: string }[]> {
Expand Down Expand Up @@ -438,6 +439,13 @@ export const useGroupChatStore = defineStore('groupChat', () => {
messages.value = res.messages
agents.value = res.agents
members.value = res.members || []

const roomIdx = rooms.value.findIndex(r => r.id === res.room.id)
if (roomIdx >= 0) {
rooms.value[roomIdx] = { ...rooms.value[roomIdx], ...res.room }
} else {
rooms.value.push(res.room)
}
} catch (err: any) {
error.value = err.message
throw err
Expand All @@ -457,6 +465,10 @@ export const useGroupChatStore = defineStore('groupChat', () => {
if (!res?.error) {
members.value = res.members || []
if (res.agents) agents.value = res.agents
if (res.defaultAgentId !== undefined) {
const roomIdx = rooms.value.findIndex(r => r.id === roomId)
if (roomIdx >= 0) rooms.value[roomIdx].defaultAgentId = res.defaultAgentId
}

// Restore typing state from server
if (res.typingUsers) {
Expand Down Expand Up @@ -526,7 +538,7 @@ export const useGroupChatStore = defineStore('groupChat', () => {
}
}

async function createNewRoom(name: string, inviteCode: string, agentList?: { profile: string; name?: string; description?: string; invited?: boolean }[], compression?: { triggerTokens: number; maxHistoryTokens: number; tailMessageCount: number }) {
async function createNewRoom(name: string, inviteCode: string, agentList?: { profile: string; name?: string; description?: string; invited?: boolean; setDefaultAgent?: boolean }[], compression?: { triggerTokens: number; maxHistoryTokens: number; tailMessageCount: number }) {
try {
const res = await createRoom({
name,
Expand Down Expand Up @@ -605,10 +617,16 @@ export const useGroupChatStore = defineStore('groupChat', () => {
} catch { /* ignore */ }
}

async function addAgentToRoom(roomId: string, data: { profile: string; name?: string; description?: string; invited?: boolean }) {
async function addAgentToRoom(roomId: string, data: { profile: string; name?: string; description?: string; invited?: boolean; setDefaultAgent?: boolean }) {
try {
const res = await addAgent(roomId, data)
agents.value.push(res.agent)
if (data.setDefaultAgent) {
const idx = rooms.value.findIndex(r => r.id === roomId)
if (idx >= 0) {
rooms.value[idx] = { ...rooms.value[idx], defaultAgentId: res.agent.id }
}
}
return res.agent
} catch (err: any) {
error.value = err.message
Expand All @@ -627,6 +645,17 @@ export const useGroupChatStore = defineStore('groupChat', () => {
}
}

async function setDefaultAgent(roomId: string, agentId: string) {
try {
await setDefaultAgentApi(roomId, agentId)
const room = rooms.value.find(r => r.id === roomId)
if (room) room.defaultAgentId = agentId
} catch (err: any) {
error.value = err.message
throw err
}
}

// ─── Typing ────────────────────────────────────────────
let _typingTimer: ReturnType<typeof setTimeout> | null = null

Expand Down Expand Up @@ -717,6 +746,7 @@ export const useGroupChatStore = defineStore('groupChat', () => {
loadAgents,
addAgentToRoom,
removeAgentFromRoom,
setDefaultAgent,
}
})

Expand Down
1 change: 1 addition & 0 deletions packages/server/src/db/hermes/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export const GC_ROOMS_SCHEMA: Record<string, string> = {
tailMessageCount: 'INTEGER NOT NULL DEFAULT 10',
totalTokens: 'INTEGER NOT NULL DEFAULT 0',
sessionSeed: "TEXT NOT NULL DEFAULT '0'",
defaultAgentId: 'TEXT',
}

export const GC_MESSAGES_TABLE = 'gc_messages'
Expand Down
75 changes: 68 additions & 7 deletions packages/server/src/routes/hermes/group-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ groupChatRoutes.post('/api/hermes/group-chat/rooms', async (ctx) => {
const { name, inviteCode, agents, compression } = ctx.request.body as {
name?: string
inviteCode?: string
agents?: { profile: string; name?: string; description?: string; invited?: boolean }[]
agents?: { profile: string; name?: string; description?: string; invited?: boolean; setDefaultAgent?: boolean }[]
compression?: { triggerTokens?: number; maxHistoryTokens?: number; tailMessageCount?: number }
}
if (!name || !inviteCode) {
Expand All @@ -98,24 +98,33 @@ groupChatRoutes.post('/api/hermes/group-chat/rooms', async (ctx) => {
const storage = chatServer.getStorage()
storage.saveRoom(roomId, name, inviteCode, compression)

const addedAgents = []
let lastDefaultAgentId: string | null = null
const agentResults = []
for (const a of agents || []) {
const agentId = generateId()
try {
const agent = await connectAndPersistRoomAgent(chatServer, roomId, {
profile: a.profile,
name: a.name || a.profile,
description: a.description || '',
invited: a.invited,
})
}, agentId)
addedAgents.push(agent)
agentResults.push({ profile: a.profile, ok: true, agent })
if (a.setDefaultAgent) {
lastDefaultAgentId = agentId
}
} catch (err: any) {
console.error(`[GroupChat] Failed to connect agent ${a.profile} to room ${roomId}: ${sanitizeAgentConnectReason(err.message)}`)
agentResults.push({ ok: false, ...agentConnectFailureBody(a.profile, err) })
}
}

if (lastDefaultAgentId) {
storage.setDefaultAgent(roomId, lastDefaultAgentId)
chatServer.agentClients.setAgentShouldAnswer(roomId, lastDefaultAgentId)
}

const room = storage.getRoom(roomId)
ctx.body = { room, agents: addedAgents, agentResults }
})
Expand Down Expand Up @@ -145,16 +154,18 @@ groupChatRoutes.post('/api/hermes/group-chat/rooms/:roomId/clone', async (ctx) =
tailMessageCount: sourceRoom.tailMessageCount,
})

const addedAgents = []
const agentIdMap = new Map<string, string>()
const agentResults = []
for (const sourceAgent of storage.getRoomAgents(sourceRoom.id)) {
const agentId = generateId()
agentIdMap.set(sourceAgent.id, agentId)
try {
const agent = await connectAndPersistRoomAgent(chatServer, roomId, {
profile: sourceAgent.profile,
name: sourceAgent.name,
description: sourceAgent.description,
invited: sourceAgent.invited,
})
}, agentId)
addedAgents.push(agent)
agentResults.push({ profile: sourceAgent.profile, ok: true, agent })
} catch (err: any) {
Expand All @@ -163,6 +174,12 @@ groupChatRoutes.post('/api/hermes/group-chat/rooms/:roomId/clone', async (ctx) =
}
}

if (sourceRoom.defaultAgentId && agentIdMap.has(sourceRoom.defaultAgentId)) {
const newAgentId = agentIdMap.get(sourceRoom.defaultAgentId)!
storage.setDefaultAgent(roomId, newAgentId)
chatServer.agentClients.setAgentShouldAnswer(roomId, newAgentId)
}

const room = storage.getRoom(roomId)
ctx.body = { room, agents: addedAgents, agentResults }
})
Expand Down Expand Up @@ -245,7 +262,7 @@ groupChatRoutes.post('/api/hermes/group-chat/rooms/:roomId/agents', async (ctx)
return
}

const { profile, name, description, invited } = ctx.request.body as { profile?: string; name?: string; description?: string; invited?: boolean }
const { profile, name, description, invited, setDefaultAgent } = ctx.request.body as { profile?: string; name?: string; description?: string; invited?: boolean; setDefaultAgent?: boolean }
if (!profile) {
ctx.status = 400
ctx.body = { error: 'profile is required' }
Expand All @@ -265,13 +282,18 @@ groupChatRoutes.post('/api/hermes/group-chat/rooms/:roomId/agents', async (ctx)
return
}

const agentId = generateId()
try {
const agent = await connectAndPersistRoomAgent(chatServer, ctx.params.roomId, {
profile,
name: name || profile,
description: description || '',
invited,
})
}, agentId)
if (setDefaultAgent) {
chatServer.getStorage().setDefaultAgent(ctx.params.roomId, agentId)
chatServer.agentClients.setAgentShouldAnswer(ctx.params.roomId, agentId)
}
ctx.body = { agent }
} catch (err: any) {
console.error(`[GroupChat] Failed to connect agent ${profile} to room ${ctx.params.roomId}: ${sanitizeAgentConnectReason(err.message)}`)
Expand Down Expand Up @@ -310,6 +332,11 @@ groupChatRoutes.delete('/api/hermes/group-chat/rooms/:roomId/agents/:agentId', a
return
}

const room = storage.getRoom(roomId)
if (room?.defaultAgentId === requestedAgentId) {
storage.setDefaultAgent(roomId, null)
}

storage.removeRoomMembersForAgent(roomId, agent)
storage.removeRoomAgent(roomId, requestedAgentId)
chatServer.agentClients.removeAgentFromRoom(roomId, agent.agentId)
Expand All @@ -320,6 +347,40 @@ groupChatRoutes.delete('/api/hermes/group-chat/rooms/:roomId/agents/:agentId', a
}
})

// Set default agent for room
groupChatRoutes.put('/api/hermes/group-chat/rooms/:roomId/default-agent', async (ctx) => {
if (!chatServer) {
ctx.status = 503
ctx.body = { error: 'Group chat not initialized' }
return
}

const { agentId } = ctx.request.body as { agentId?: string }
if (!agentId) {
ctx.status = 400
ctx.body = { error: 'agentId is required' }
return
}

const room = chatServer.getStorage().getRoom(ctx.params.roomId)
if (!room) {
ctx.status = 404
ctx.body = { error: 'Room not found' }
return
}

const agents = chatServer.getStorage().getRoomAgents(ctx.params.roomId)
if (!agents.find(a => a.id === agentId)) {
ctx.status = 400
ctx.body = { error: 'Agent not in room' }
return
}

chatServer.getStorage().setDefaultAgent(ctx.params.roomId, agentId)
chatServer.agentClients.setAgentShouldAnswer(ctx.params.roomId, agentId)
ctx.body = { success: true }
})

// Delete room
groupChatRoutes.delete('/api/hermes/group-chat/rooms/:roomId', async (ctx) => {
if (!chatServer) {
Expand Down
Loading