Refine user profile access and chat sync
This commit is contained in:
@@ -51,6 +51,13 @@ export interface RunEvent {
|
||||
session_id?: string
|
||||
/** Queue length from run.queued event */
|
||||
queue_length?: number
|
||||
/** User message broadcast to other windows already watching the same session. */
|
||||
message?: {
|
||||
id?: string | number
|
||||
role?: string
|
||||
content?: string
|
||||
timestamp?: number
|
||||
}
|
||||
}
|
||||
|
||||
// ============================
|
||||
@@ -83,8 +90,11 @@ const sessionEventHandlers = new Map<string, {
|
||||
onRunQueued?: (event: RunEvent) => void
|
||||
onApprovalRequested?: (event: RunEvent) => void
|
||||
onApprovalResolved?: (event: RunEvent) => void
|
||||
onPeerUserMessage?: (event: RunEvent) => void
|
||||
}>()
|
||||
|
||||
const peerUserMessageHandlers = new Set<(event: RunEvent) => void>()
|
||||
|
||||
/**
|
||||
* Global message.delta event handler
|
||||
* Distributes events to appropriate session based on session_id
|
||||
@@ -324,6 +334,20 @@ function globalApprovalResolvedHandler(event: RunEvent): void {
|
||||
}
|
||||
}
|
||||
|
||||
function globalPeerUserMessageHandler(event: RunEvent): void {
|
||||
const sid = event.session_id
|
||||
if (!sid) return
|
||||
|
||||
const handlers = sessionEventHandlers.get(sid)
|
||||
if (handlers?.onPeerUserMessage) {
|
||||
handlers.onPeerUserMessage(event)
|
||||
}
|
||||
|
||||
for (const handler of peerUserMessageHandlers) {
|
||||
handler(event)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register event handlers for a session
|
||||
* @param sessionId - Session ID
|
||||
@@ -351,6 +375,7 @@ export function registerSessionHandlers(
|
||||
onRunQueued?: (event: RunEvent) => void
|
||||
onApprovalRequested?: (event: RunEvent) => void
|
||||
onApprovalResolved?: (event: RunEvent) => void
|
||||
onPeerUserMessage?: (event: RunEvent) => void
|
||||
}
|
||||
): () => void {
|
||||
sessionEventHandlers.set(sessionId, handlers)
|
||||
@@ -369,6 +394,13 @@ export function unregisterSessionHandlers(sessionId: string): void {
|
||||
sessionEventHandlers.delete(sessionId)
|
||||
}
|
||||
|
||||
export function onPeerUserMessage(handler: (event: RunEvent) => void): () => void {
|
||||
peerUserMessageHandlers.add(handler)
|
||||
return () => {
|
||||
peerUserMessageHandlers.delete(handler)
|
||||
}
|
||||
}
|
||||
|
||||
export function respondToolApproval(
|
||||
sessionId: string,
|
||||
approvalId: string,
|
||||
@@ -441,6 +473,7 @@ export function connectChatRun(): Socket {
|
||||
chatRunSocket.on('run.queued', globalRunQueuedHandler)
|
||||
chatRunSocket.on('approval.requested', globalApprovalRequestedHandler)
|
||||
chatRunSocket.on('approval.resolved', globalApprovalResolvedHandler)
|
||||
chatRunSocket.on('run.peer_user_message', globalPeerUserMessageHandler)
|
||||
|
||||
// Compression events
|
||||
chatRunSocket.on('compression.started', globalCompressionStartedHandler)
|
||||
|
||||
@@ -47,7 +47,7 @@ export default {
|
||||
username: 'Benutzername',
|
||||
role: 'Rolle',
|
||||
statusLabel: 'Status',
|
||||
profiles: 'Profiles',
|
||||
profiles: 'Zugreifbare Profile',
|
||||
profilesPlaceholder: 'Zugreifbare Profile auswaehlen',
|
||||
allProfiles: 'Alle Profile',
|
||||
noProfiles: 'Keine Profile zugewiesen',
|
||||
@@ -679,7 +679,7 @@ jobTriggered: 'Job ausgelost',
|
||||
stopped: 'Gestoppt',
|
||||
restartGateway: 'Gateway neu starten',
|
||||
restartProfile: 'Profil neu starten',
|
||||
switchProfile: 'Profil wechseln',
|
||||
switchProfile: 'Frontend-Profil wechseln',
|
||||
gatewayRestarted: 'Gateway neu gestartet: {name}',
|
||||
gatewayRestartFailed: 'Gateway-Neustart fehlgeschlagen',
|
||||
profileRestarted: 'Profil neu gestartet: {name}',
|
||||
|
||||
@@ -47,7 +47,7 @@ export default {
|
||||
username: 'Username',
|
||||
role: 'Role',
|
||||
statusLabel: 'Status',
|
||||
profiles: 'Profiles',
|
||||
profiles: 'Accessible Profiles',
|
||||
profilesPlaceholder: 'Select accessible profiles',
|
||||
allProfiles: 'All profiles',
|
||||
noProfiles: 'No profiles assigned',
|
||||
@@ -782,7 +782,7 @@ export default {
|
||||
stopped: 'Stopped',
|
||||
restartGateway: 'Restart Gateway',
|
||||
restartProfile: 'Restart Profile',
|
||||
switchProfile: 'Switch Profile',
|
||||
switchProfile: 'Switch Frontend Profile',
|
||||
gatewayRestarted: 'Gateway restarted: {name}',
|
||||
gatewayRestartFailed: 'Failed to restart gateway',
|
||||
profileRestarted: 'Profile restarted: {name}',
|
||||
|
||||
@@ -41,16 +41,16 @@ export default {
|
||||
|
||||
users: {
|
||||
title: 'Gestion de cuentas',
|
||||
description: 'Crea usuarios, asigna roles y controla que Profile pueden usar los administradores normales.',
|
||||
description: 'Crea usuarios, asigna roles y controla que perfiles pueden usar los administradores normales.',
|
||||
create: 'Crear usuario',
|
||||
edit: 'Editar usuario',
|
||||
username: 'Nombre de usuario',
|
||||
role: 'Rol',
|
||||
statusLabel: 'Estado',
|
||||
profiles: 'Profiles',
|
||||
profilesPlaceholder: 'Selecciona Profile accesibles',
|
||||
allProfiles: 'Todos los Profile',
|
||||
noProfiles: 'Sin Profile asignados',
|
||||
profiles: 'Perfiles accesibles',
|
||||
profilesPlaceholder: 'Selecciona perfiles accesibles',
|
||||
allProfiles: 'Todos los perfiles',
|
||||
noProfiles: 'Sin perfiles asignados',
|
||||
lastLogin: 'Ultimo inicio',
|
||||
newPasswordOptional: 'Nueva contrasena (dejar vacio para conservar)',
|
||||
loadFailed: 'No se pudieron cargar los usuarios',
|
||||
@@ -679,7 +679,7 @@ jobTriggered: 'Job ejecutado',
|
||||
stopped: 'Detenido',
|
||||
restartGateway: 'Reiniciar gateway',
|
||||
restartProfile: 'Reiniciar perfil',
|
||||
switchProfile: 'Cambiar perfil',
|
||||
switchProfile: 'Cambiar perfil frontend',
|
||||
gatewayRestarted: 'Gateway reiniciado: {name}',
|
||||
gatewayRestartFailed: 'No se pudo reiniciar el gateway',
|
||||
profileRestarted: 'Perfil reiniciado: {name}',
|
||||
|
||||
@@ -41,16 +41,16 @@ export default {
|
||||
|
||||
users: {
|
||||
title: 'Gestion des comptes',
|
||||
description: 'Creez des utilisateurs, attribuez des roles et controlez les Profile accessibles aux administrateurs standard.',
|
||||
description: 'Creez des utilisateurs, attribuez des roles et controlez les profils accessibles aux administrateurs standard.',
|
||||
create: 'Creer un utilisateur',
|
||||
edit: 'Modifier l utilisateur',
|
||||
username: 'Nom d utilisateur',
|
||||
role: 'Role',
|
||||
statusLabel: 'Statut',
|
||||
profiles: 'Profiles',
|
||||
profilesPlaceholder: 'Selectionner les Profile accessibles',
|
||||
allProfiles: 'Tous les Profile',
|
||||
noProfiles: 'Aucun Profile assigne',
|
||||
profiles: 'Profils accessibles',
|
||||
profilesPlaceholder: 'Selectionner les profils accessibles',
|
||||
allProfiles: 'Tous les profils',
|
||||
noProfiles: 'Aucun profil assigne',
|
||||
lastLogin: 'Derniere connexion',
|
||||
newPasswordOptional: 'Nouveau mot de passe (laisser vide pour conserver)',
|
||||
loadFailed: 'Echec du chargement des utilisateurs',
|
||||
@@ -679,7 +679,7 @@ jobTriggered: 'Job declenche',
|
||||
stopped: 'Arrêté',
|
||||
restartGateway: 'Redémarrer le gateway',
|
||||
restartProfile: 'Redémarrer le profil',
|
||||
switchProfile: 'Changer de profil',
|
||||
switchProfile: 'Changer le profil frontend',
|
||||
gatewayRestarted: 'Gateway redémarré : {name}',
|
||||
gatewayRestartFailed: 'Échec du redémarrage du gateway',
|
||||
profileRestarted: 'Profil redémarré : {name}',
|
||||
|
||||
@@ -41,16 +41,16 @@ export default {
|
||||
|
||||
users: {
|
||||
title: 'アカウント管理',
|
||||
description: 'ユーザーを作成し、ロールを割り当て、通常管理者がアクセスできる Profile を制御します。',
|
||||
description: 'ユーザーを作成し、ロールを割り当て、通常管理者がアクセスできるプロファイルを制御します。',
|
||||
create: 'ユーザー作成',
|
||||
edit: 'ユーザー編集',
|
||||
username: 'ユーザー名',
|
||||
role: 'ロール',
|
||||
statusLabel: 'ステータス',
|
||||
profiles: 'Profiles',
|
||||
profilesPlaceholder: 'アクセス可能な Profile を選択',
|
||||
allProfiles: 'すべての Profile',
|
||||
noProfiles: 'Profile 未割り当て',
|
||||
profiles: 'アクセス可能なプロファイル',
|
||||
profilesPlaceholder: 'アクセス可能なプロファイルを選択',
|
||||
allProfiles: 'すべてのプロファイル',
|
||||
noProfiles: 'プロファイル未割り当て',
|
||||
lastLogin: '最終ログイン',
|
||||
newPasswordOptional: '新しいパスワード(空欄なら変更なし)',
|
||||
loadFailed: 'ユーザー一覧の読み込みに失敗しました',
|
||||
@@ -679,7 +679,7 @@ export default {
|
||||
stopped: '停止中',
|
||||
restartGateway: 'Gateway を再起動',
|
||||
restartProfile: 'プロファイルを再起動',
|
||||
switchProfile: 'プロファイルを切り替え',
|
||||
switchProfile: 'フロントエンドプロファイルを切り替え',
|
||||
gatewayRestarted: 'Gateway を再起動しました: {name}',
|
||||
gatewayRestartFailed: 'Gateway の再起動に失敗しました',
|
||||
profileRestarted: 'プロファイルを再起動しました: {name}',
|
||||
|
||||
@@ -41,16 +41,16 @@ export default {
|
||||
|
||||
users: {
|
||||
title: '계정 관리',
|
||||
description: '사용자를 만들고 역할을 할당하며 일반 관리자가 접근할 수 있는 Profile 을 제어합니다.',
|
||||
description: '사용자를 만들고 역할을 할당하며 일반 관리자가 접근할 수 있는 프로필을 제어합니다.',
|
||||
create: '사용자 만들기',
|
||||
edit: '사용자 편집',
|
||||
username: '사용자 이름',
|
||||
role: '역할',
|
||||
statusLabel: '상태',
|
||||
profiles: 'Profiles',
|
||||
profilesPlaceholder: '접근 가능한 Profile 선택',
|
||||
allProfiles: '모든 Profile',
|
||||
noProfiles: '할당된 Profile 없음',
|
||||
profiles: '접근 가능한 프로필',
|
||||
profilesPlaceholder: '접근 가능한 프로필 선택',
|
||||
allProfiles: '모든 프로필',
|
||||
noProfiles: '할당된 프로필 없음',
|
||||
lastLogin: '마지막 로그인',
|
||||
newPasswordOptional: '새 비밀번호 (비워두면 유지)',
|
||||
loadFailed: '사용자 목록을 불러오지 못했습니다',
|
||||
@@ -679,7 +679,7 @@ export default {
|
||||
stopped: '중지됨',
|
||||
restartGateway: 'Gateway 재시작',
|
||||
restartProfile: '프로필 재시작',
|
||||
switchProfile: '프로필 전환',
|
||||
switchProfile: '프론트엔드 프로필 전환',
|
||||
gatewayRestarted: 'Gateway가 재시작되었습니다: {name}',
|
||||
gatewayRestartFailed: 'Gateway 재시작 실패',
|
||||
profileRestarted: '프로필이 재시작되었습니다: {name}',
|
||||
|
||||
@@ -41,16 +41,16 @@ export default {
|
||||
|
||||
users: {
|
||||
title: 'Gerenciamento de contas',
|
||||
description: 'Crie usuarios, atribua funcoes e controle quais Profile administradores comuns podem acessar.',
|
||||
description: 'Crie usuarios, atribua funcoes e controle quais perfis administradores comuns podem acessar.',
|
||||
create: 'Criar usuario',
|
||||
edit: 'Editar usuario',
|
||||
username: 'Nome de usuario',
|
||||
role: 'Funcao',
|
||||
statusLabel: 'Status',
|
||||
profiles: 'Profiles',
|
||||
profilesPlaceholder: 'Selecione Profile acessiveis',
|
||||
allProfiles: 'Todos os Profile',
|
||||
noProfiles: 'Nenhum Profile atribuido',
|
||||
profiles: 'Perfis acessiveis',
|
||||
profilesPlaceholder: 'Selecione perfis acessiveis',
|
||||
allProfiles: 'Todos os perfis',
|
||||
noProfiles: 'Nenhum perfil atribuido',
|
||||
lastLogin: 'Ultimo login',
|
||||
newPasswordOptional: 'Nova senha (deixe em branco para manter)',
|
||||
loadFailed: 'Falha ao carregar usuarios',
|
||||
@@ -679,7 +679,7 @@ jobTriggered: 'Job acionado',
|
||||
stopped: 'Parado',
|
||||
restartGateway: 'Reiniciar gateway',
|
||||
restartProfile: 'Reiniciar perfil',
|
||||
switchProfile: 'Trocar perfil',
|
||||
switchProfile: 'Trocar perfil frontend',
|
||||
gatewayRestarted: 'Gateway reiniciado: {name}',
|
||||
gatewayRestartFailed: 'Falha ao reiniciar gateway',
|
||||
profileRestarted: 'Perfil reiniciado: {name}',
|
||||
|
||||
@@ -41,16 +41,16 @@ export default {
|
||||
|
||||
users: {
|
||||
title: '帳號管理',
|
||||
description: '建立使用者、分配角色,並控制一般管理員可存取的 Profile。',
|
||||
description: '建立使用者、分配角色,並控制一般管理員可存取的設定檔。',
|
||||
create: '建立使用者',
|
||||
edit: '編輯使用者',
|
||||
username: '使用者名稱',
|
||||
role: '角色',
|
||||
statusLabel: '狀態',
|
||||
profiles: 'Profiles',
|
||||
profilesPlaceholder: '選擇可存取的 Profile',
|
||||
allProfiles: '全部 Profile',
|
||||
noProfiles: '未關聯 Profile',
|
||||
profiles: '可存取設定檔',
|
||||
profilesPlaceholder: '選擇可存取的設定檔',
|
||||
allProfiles: '全部設定檔',
|
||||
noProfiles: '未關聯設定檔',
|
||||
lastLogin: '最後登入',
|
||||
newPasswordOptional: '新密碼(留空不修改)',
|
||||
loadFailed: '使用者列表載入失敗',
|
||||
@@ -774,7 +774,7 @@ export default {
|
||||
stopped: '已停止',
|
||||
restartGateway: '重啟閘道',
|
||||
restartProfile: '重啟設定檔',
|
||||
switchProfile: '切換前端 Profile',
|
||||
switchProfile: '切換前端設定檔',
|
||||
gatewayRestarted: '閘道已重啟:{name}',
|
||||
gatewayRestartFailed: '重啟閘道失敗',
|
||||
profileRestarted: '設定檔已重啟:{name}',
|
||||
|
||||
@@ -41,16 +41,16 @@ export default {
|
||||
|
||||
users: {
|
||||
title: '账户管理',
|
||||
description: '创建用户、分配角色,并控制普通管理员可访问的 Profile。',
|
||||
description: '创建用户、分配角色,并控制普通管理员可访问的配置。',
|
||||
create: '创建用户',
|
||||
edit: '编辑用户',
|
||||
username: '用户名',
|
||||
role: '角色',
|
||||
statusLabel: '状态',
|
||||
profiles: 'Profiles',
|
||||
profilesPlaceholder: '选择可访问的 Profile',
|
||||
allProfiles: '全部 Profile',
|
||||
noProfiles: '未关联 Profile',
|
||||
profiles: '可访问配置',
|
||||
profilesPlaceholder: '选择可访问的配置',
|
||||
allProfiles: '全部配置',
|
||||
noProfiles: '未关联配置',
|
||||
lastLogin: '最后登录',
|
||||
newPasswordOptional: '新密码(留空不修改)',
|
||||
loadFailed: '用户列表加载失败',
|
||||
@@ -774,7 +774,7 @@ export default {
|
||||
stopped: '已停止',
|
||||
restartGateway: '重启网关',
|
||||
restartProfile: '重启配置',
|
||||
switchProfile: '切换配置',
|
||||
switchProfile: '切换前端配置',
|
||||
gatewayRestarted: '网关已重启:{name}',
|
||||
gatewayRestartFailed: '重启网关失败',
|
||||
profileRestarted: '配置已重启:{name}',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { startRunViaSocket, resumeSession, registerSessionHandlers, unregisterSessionHandlers, getChatRunSocket, respondToolApproval, type RunEvent, type ContentBlock as ContentBlockImport } from '@/api/hermes/chat'
|
||||
import { startRunViaSocket, resumeSession, registerSessionHandlers, unregisterSessionHandlers, getChatRunSocket, respondToolApproval, onPeerUserMessage, type RunEvent, type ContentBlock as ContentBlockImport } from '@/api/hermes/chat'
|
||||
import { deleteSession as deleteSessionApi, fetchSession, fetchSessions, setSessionModel, type HermesMessage, type SessionSummary } from '@/api/hermes/sessions'
|
||||
import { getApiKey } from '@/api/client'
|
||||
import { defineStore } from 'pinia'
|
||||
@@ -1451,11 +1451,11 @@ export const useChatStore = defineStore('chat', () => {
|
||||
* Emits 'resume' to join the session room on the server,
|
||||
* then sets up event listeners to receive ongoing events.
|
||||
*/
|
||||
function resumeServerWorkingRun(sid: string) {
|
||||
function resumeServerWorkingRun(sid: string, force = false) {
|
||||
// Don't register duplicate listeners if already streaming
|
||||
if (streamStates.value.has(sid)) return
|
||||
// Only set up listeners if the server reported an active run during resume.
|
||||
if (!serverWorking.value.has(sid)) return
|
||||
if (!force && !serverWorking.value.has(sid)) return
|
||||
|
||||
let closed = false
|
||||
let runProducedAssistantText = false
|
||||
@@ -1872,6 +1872,39 @@ export const useChatStore = defineStore('chat', () => {
|
||||
})
|
||||
}
|
||||
|
||||
function handlePeerUserMessage(evt: RunEvent) {
|
||||
const sid = evt.session_id
|
||||
if (!sid || activeSessionId.value !== sid || !activeSession.value) return
|
||||
|
||||
const peer = evt.message
|
||||
const content = typeof peer?.content === 'string' ? peer.content : ''
|
||||
if (!content.trim()) return
|
||||
|
||||
const messageId = peer?.id != null ? String(peer.id) : ''
|
||||
const msgs = getSessionMsgs(sid)
|
||||
if (messageId && msgs.some(msg => msg.id === messageId)) {
|
||||
serverWorking.value.add(sid)
|
||||
resumeServerWorkingRun(sid, true)
|
||||
return
|
||||
}
|
||||
|
||||
const timestamp = typeof peer?.timestamp === 'number' && Number.isFinite(peer.timestamp)
|
||||
? Math.round(peer.timestamp * 1000)
|
||||
: Date.now()
|
||||
|
||||
addMessage(sid, {
|
||||
id: messageId || uid(),
|
||||
role: 'user',
|
||||
content,
|
||||
timestamp,
|
||||
})
|
||||
updateSessionTitle(sid)
|
||||
serverWorking.value.add(sid)
|
||||
resumeServerWorkingRun(sid, true)
|
||||
}
|
||||
|
||||
onPeerUserMessage(handlePeerUserMessage)
|
||||
|
||||
function stopStreaming() {
|
||||
const sid = activeSessionId.value
|
||||
if (!sid) return
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createHmac, timingSafeEqual } from 'crypto'
|
||||
import { getToken } from '../services/auth'
|
||||
import {
|
||||
findUserById,
|
||||
listUserProfiles,
|
||||
touchUserLogin,
|
||||
userCanAccessProfile,
|
||||
type UserRecord,
|
||||
@@ -13,6 +14,7 @@ export interface AuthenticatedUser {
|
||||
id: number
|
||||
username: string
|
||||
role: UserRole
|
||||
profiles?: string[]
|
||||
}
|
||||
|
||||
export interface RequestProfile {
|
||||
@@ -110,11 +112,15 @@ export async function issueUserJwt(user: Pick<UserRecord, 'id' | 'username' | 'r
|
||||
}
|
||||
|
||||
export function toAuthenticatedUser(user: Pick<UserRecord, 'id' | 'username' | 'role'>): AuthenticatedUser {
|
||||
return {
|
||||
const authenticated: AuthenticatedUser = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
}
|
||||
if (user.role !== 'super_admin') {
|
||||
authenticated.profiles = listUserProfiles(user.id).map(profile => profile.profile_name)
|
||||
}
|
||||
return authenticated
|
||||
}
|
||||
|
||||
export async function authenticateUserToken(token: string): Promise<AuthenticatedUser | null> {
|
||||
|
||||
@@ -196,7 +196,11 @@ groupChatRoutes.get('/api/hermes/group-chat/rooms', async (ctx) => {
|
||||
return
|
||||
}
|
||||
|
||||
const rooms = chatServer.getStorage().getAllRooms()
|
||||
const user = ctx.state.user
|
||||
const storage = chatServer.getStorage()
|
||||
const rooms = !user || user.role === 'super_admin'
|
||||
? storage.getAllRooms()
|
||||
: storage.getRoomsForProfiles(user.profiles || [])
|
||||
ctx.body = { rooms }
|
||||
})
|
||||
|
||||
|
||||
@@ -72,6 +72,17 @@ interface RoomAgent {
|
||||
invited: number
|
||||
}
|
||||
|
||||
interface RoomInfo {
|
||||
id: string
|
||||
name: string
|
||||
inviteCode: string | null
|
||||
triggerTokens: number
|
||||
maxHistoryTokens: number
|
||||
tailMessageCount: number
|
||||
totalTokens: number
|
||||
sessionSeed: string
|
||||
}
|
||||
|
||||
interface Member {
|
||||
id: string
|
||||
userId: string
|
||||
@@ -278,18 +289,31 @@ class ChatStorage {
|
||||
|
||||
// ─── Rooms ────────────────────────────────────────────────
|
||||
|
||||
getRoom(roomId: string): { id: string; name: string; inviteCode: string | null; triggerTokens: number; maxHistoryTokens: number; tailMessageCount: number; totalTokens: number; sessionSeed: string } | undefined {
|
||||
getRoom(roomId: string): RoomInfo | undefined {
|
||||
return this.db()?.prepare('SELECT id, name, inviteCode, triggerTokens, maxHistoryTokens, tailMessageCount, totalTokens, sessionSeed FROM gc_rooms WHERE id = ?').get(roomId) as any
|
||||
}
|
||||
|
||||
getRoomByInviteCode(code: string): { id: string; name: string; inviteCode: string | null; triggerTokens: number; maxHistoryTokens: number; tailMessageCount: number; totalTokens: number; sessionSeed: string } | undefined {
|
||||
getRoomByInviteCode(code: string): RoomInfo | undefined {
|
||||
return this.db()?.prepare('SELECT id, name, inviteCode, triggerTokens, maxHistoryTokens, tailMessageCount, totalTokens, sessionSeed FROM gc_rooms WHERE inviteCode = ?').get(code) as any
|
||||
}
|
||||
|
||||
getAllRooms(): { id: string; name: string; inviteCode: string | null; triggerTokens: number; maxHistoryTokens: number; tailMessageCount: number; totalTokens: number; sessionSeed: string }[] {
|
||||
getAllRooms(): RoomInfo[] {
|
||||
return (this.db()?.prepare('SELECT id, name, inviteCode, triggerTokens, maxHistoryTokens, tailMessageCount, totalTokens, sessionSeed FROM gc_rooms ORDER BY id').all() || []) as any[]
|
||||
}
|
||||
|
||||
getRoomsForProfiles(profiles: string[]): RoomInfo[] {
|
||||
const uniqueProfiles = [...new Set(profiles.map(profile => profile.trim()).filter(Boolean))]
|
||||
if (!uniqueProfiles.length) return []
|
||||
const placeholders = uniqueProfiles.map(() => '?').join(', ')
|
||||
return (this.db()?.prepare(
|
||||
`SELECT DISTINCT r.id, r.name, r.inviteCode, r.triggerTokens, r.maxHistoryTokens, r.tailMessageCount, r.totalTokens, r.sessionSeed
|
||||
FROM gc_rooms r
|
||||
INNER JOIN gc_room_agents a ON a.roomId = r.id
|
||||
WHERE a.profile IN (${placeholders})
|
||||
ORDER BY r.id`
|
||||
).all(...uniqueProfiles) || []) as any[]
|
||||
}
|
||||
|
||||
saveRoom(id: string, name: string, inviteCode?: string, config?: { triggerTokens?: number; maxHistoryTokens?: number; tailMessageCount?: number }): void {
|
||||
this.db()?.prepare(
|
||||
'INSERT OR IGNORE INTO gc_rooms (id, name, inviteCode, triggerTokens, maxHistoryTokens, tailMessageCount) VALUES (?, ?, ?, ?, ?, ?)'
|
||||
|
||||
@@ -109,6 +109,7 @@ export async function handleApiRun(
|
||||
state.source = 'api_server'
|
||||
state.activeRunMarker = runMarker
|
||||
|
||||
let peerUserMessage: { id?: number; role: 'user'; content: string; timestamp: number } | null = null
|
||||
if (!skipUserMessage) {
|
||||
const inputStr = contentBlocksToString(input)
|
||||
state.messages.push({
|
||||
@@ -126,12 +127,13 @@ export async function handleApiRun(
|
||||
createSession({ id: session_id, profile, source: 'api_server', model, provider, title: preview })
|
||||
}
|
||||
|
||||
addMessage({
|
||||
const messageId = addMessage({
|
||||
session_id,
|
||||
role: 'user',
|
||||
content: inputStr,
|
||||
timestamp: now,
|
||||
})
|
||||
peerUserMessage = { id: messageId, role: 'user', content: inputStr, timestamp: now }
|
||||
} else {
|
||||
const inputStr = contentBlocksToString(input)
|
||||
state.messages.push({
|
||||
@@ -147,15 +149,23 @@ export async function handleApiRun(
|
||||
const preview = previewText.replace(/[\r\n]/g, ' ').substring(0, 100)
|
||||
createSession({ id: session_id, profile, source: 'api_server', model, provider, title: preview })
|
||||
}
|
||||
addMessage({
|
||||
const messageId = addMessage({
|
||||
session_id,
|
||||
role: 'user',
|
||||
content: inputStr,
|
||||
timestamp: now,
|
||||
})
|
||||
peerUserMessage = { id: messageId, role: 'user', content: inputStr, timestamp: now }
|
||||
}
|
||||
|
||||
socket.join(`session:${session_id}`)
|
||||
if (peerUserMessage) {
|
||||
socket.to(`session:${session_id}`).emit('run.peer_user_message', {
|
||||
event: 'run.peer_user_message',
|
||||
session_id,
|
||||
message: peerUserMessage,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const emit = (event: string, payload: any) => {
|
||||
|
||||
@@ -173,7 +173,7 @@ export async function handleBridgeRun(
|
||||
const preview = previewText.replace(/[\r\n]/g, ' ').substring(0, 100)
|
||||
createSession({ id: session_id, profile, source: 'cli', model: resolvedModel, provider: resolvedProvider, title: preview })
|
||||
}
|
||||
addMessage({
|
||||
const messageId = addMessage({
|
||||
session_id,
|
||||
role: 'user',
|
||||
content: inputStr,
|
||||
@@ -181,6 +181,16 @@ export async function handleBridgeRun(
|
||||
})
|
||||
|
||||
socket.join(`session:${session_id}`)
|
||||
socket.to(`session:${session_id}`).emit('run.peer_user_message', {
|
||||
event: 'run.peer_user_message',
|
||||
session_id,
|
||||
message: {
|
||||
id: messageId,
|
||||
role: 'user',
|
||||
content: inputStr,
|
||||
timestamp: now,
|
||||
},
|
||||
})
|
||||
const emit = (event: string, payload: any) => {
|
||||
const tagged = { ...payload, session_id }
|
||||
nsp.to(`session:${session_id}`).emit(event, tagged)
|
||||
|
||||
@@ -224,6 +224,52 @@ describe('Group Chat member/agent identity sync', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('filters room list to rooms containing one of the regular admin profiles', async () => {
|
||||
const allRooms = [
|
||||
{ id: 'room-default', name: 'Default', inviteCode: null },
|
||||
{ id: 'room-private', name: 'Private', inviteCode: null },
|
||||
]
|
||||
const visibleRooms = [allRooms[0]]
|
||||
const storage = {
|
||||
getAllRooms: vi.fn(() => allRooms),
|
||||
getRoomsForProfiles: vi.fn(() => visibleRooms),
|
||||
}
|
||||
setGroupChatServer({ getStorage: () => storage } as any)
|
||||
|
||||
const handler = routeHandler('/api/hermes/group-chat/rooms', 'GET')
|
||||
const ctx: any = {
|
||||
state: { user: { id: 2, username: 'ops', role: 'admin', profiles: ['default', 'research'] } },
|
||||
status: 200,
|
||||
body: undefined,
|
||||
}
|
||||
await handler(ctx, async () => {})
|
||||
|
||||
expect(storage.getRoomsForProfiles).toHaveBeenCalledWith(['default', 'research'])
|
||||
expect(storage.getAllRooms).not.toHaveBeenCalled()
|
||||
expect(ctx.body).toEqual({ rooms: visibleRooms })
|
||||
})
|
||||
|
||||
it('keeps room list unrestricted for super admins', async () => {
|
||||
const rooms = [{ id: 'room-1', name: 'All', inviteCode: null }]
|
||||
const storage = {
|
||||
getAllRooms: vi.fn(() => rooms),
|
||||
getRoomsForProfiles: vi.fn(() => []),
|
||||
}
|
||||
setGroupChatServer({ getStorage: () => storage } as any)
|
||||
|
||||
const handler = routeHandler('/api/hermes/group-chat/rooms', 'GET')
|
||||
const ctx: any = {
|
||||
state: { user: { id: 1, username: 'admin', role: 'super_admin' } },
|
||||
status: 200,
|
||||
body: undefined,
|
||||
}
|
||||
await handler(ctx, async () => {})
|
||||
|
||||
expect(storage.getAllRooms).toHaveBeenCalledOnce()
|
||||
expect(storage.getRoomsForProfiles).not.toHaveBeenCalled()
|
||||
expect(ctx.body).toEqual({ rooms })
|
||||
})
|
||||
|
||||
it('routes @mentions only from user messages, not agent replies', () => {
|
||||
const server = Object.create(GroupChatServer.prototype) as any
|
||||
const emit = vi.fn()
|
||||
|
||||
Reference in New Issue
Block a user