feat(server-sdk,stage-ui,server-shared): more server channel hooks

This commit is contained in:
Neko Ayaka
2025-12-27 17:48:41 +08:00
parent a6aae910d0
commit 8a841d65da
4 changed files with 90 additions and 18 deletions
+7 -1
View File
@@ -1,4 +1,10 @@
import type { WebSocketBaseEvent, WebSocketEvent, WebSocketEventOptionalSource, WebSocketEvents, WebSocketEventSource } from '@proj-airi/server-shared/types'
import type {
WebSocketBaseEvent,
WebSocketEvent,
WebSocketEventOptionalSource,
WebSocketEvents,
WebSocketEventSource,
} from '@proj-airi/server-shared/types'
import WebSocket from 'crossws/websocket'
@@ -1,4 +1,4 @@
import type { AssistantMessage, ToolMessage } from '@xsai/shared-chat'
import type { AssistantMessage, Message, ToolMessage, UserMessage } from '@xsai/shared-chat'
export interface DiscordGuildMember {
nickname: string
@@ -19,13 +19,13 @@ export enum WebSocketEventSource {
}
interface InputSource {
'stage-web': string
'stage-tamagotchi': string
'stage-web': boolean
'stage-tamagotchi': boolean
'discord': Discord
}
interface OutputSource {
'gen-ai-model-chat': string
'gen-ai:chat': string
}
export enum ContextUpdateStrategy {
@@ -80,6 +80,7 @@ export interface WebSocketEvents<C = undefined> {
'error': {
message: string
}
'module:authenticate': {
token: string
}
@@ -93,11 +94,13 @@ export interface WebSocketEvents<C = undefined> {
'module:configure': {
config: C
}
'ui:configure': {
moduleName: string
moduleIndex?: number
config: C | Record<string, unknown>
}
'input:text': {
text: string
} & Partial<WithInputSource<'stage-web' | 'stage-tamagotchi' | 'discord'>>
@@ -107,9 +110,21 @@ export interface WebSocketEvents<C = undefined> {
'input:voice': {
audio: ArrayBuffer
} & Partial<WithInputSource<'stage-web' | 'stage-tamagotchi' | 'discord'>>
'output:gen-ai:chat:tool-call': {
toolCalls: ToolMessage[]
} & Partial<WithInputSource<'stage-web' | 'stage-tamagotchi' | 'discord'>> & Partial<WithOutputSource<'gen-ai:chat'>>
'output:gen-ai:chat:message': {
messages: Array<AssistantMessage | ToolMessage>
} & Partial<WithInputSource<'stage-web' | 'stage-tamagotchi' | 'discord'>> & Partial<WithOutputSource<'gen-ai-model-chat'>>
message: AssistantMessage
} & Partial<WithInputSource<'stage-web' | 'stage-tamagotchi' | 'discord'>> & Partial<WithOutputSource<'gen-ai:chat'>>
'output:gen-ai:chat:complete': {
input: UserMessage
contexts: Record<string, ContextUpdate[]>
composedMessage: Array<Message>
message: AssistantMessage
toolCalls: ToolMessage[]
} & Partial<WithInputSource<'stage-web' | 'stage-tamagotchi' | 'discord'>> & Partial<WithOutputSource<'gen-ai:chat'>>
'context:update': ContextUpdate
}
+34 -10
View File
@@ -1,5 +1,5 @@
import type { ChatProvider } from '@xsai-ext/shared-providers'
import type { CommonContentPart, Message, SystemMessage } from '@xsai/shared-chat'
import type { CommonContentPart, Message, SystemMessage, ToolMessage } from '@xsai/shared-chat'
import type { StreamEvent, StreamOptions } from '../stores/llm'
import type { ChatAssistantMessage, ChatHistoryItem, ChatSlices, ContextMessage, StreamingAssistantMessage } from '../types/chat'
@@ -98,8 +98,15 @@ export const useChatStore = defineStore('chat', () => {
const onTokenSpecialHooks = ref<Array<(special: string) => Promise<void>>>([])
const onStreamEndHooks = ref<Array<() => Promise<void>>>([])
const onAssistantResponseEndHooks = ref<Array<(message: string) => Promise<void>>>([])
const onAssistantMessageHooks = ref<Array<(message: StreamingAssistantMessage) => Promise<void>>>([])
const onChatTurnCompleteHooks = ref<Array<(chat: { input: ChatHistoryItem, contexts: Record<string, ContextMessage[]>, composedMessage: Message[], output: StreamingAssistantMessage }) => Promise<void>>>([])
const onAssistantMessageHooks = ref<Array<(message: StreamingAssistantMessage, messageText?: string) => Promise<void>>>([])
const onChatTurnCompleteHooks = ref<Array<(chat: {
input: ChatHistoryItem
contexts: Record<string, ContextMessage[]>
composedMessage: Message[]
output: StreamingAssistantMessage
outputText: string
toolCalls: ToolMessage[]
}) => Promise<void>>>([])
function onBeforeMessageComposed(cb: (message: string) => Promise<void>) {
onBeforeMessageComposedHooks.value.push(cb)
@@ -141,12 +148,19 @@ export const useChatStore = defineStore('chat', () => {
return () => onAssistantResponseEndHooks.value = onAssistantResponseEndHooks.value.filter(hook => hook !== cb) // return remove listener callback
}
function onAssistantMessage(cb: (message: StreamingAssistantMessage) => Promise<void>) {
function onAssistantMessage(cb: (message: StreamingAssistantMessage, messageText?: string) => Promise<void>) {
onAssistantMessageHooks.value.push(cb)
return () => onAssistantMessageHooks.value = onAssistantMessageHooks.value.filter(hook => hook !== cb) // return remove listener callback
}
function onChatTurnComplete(cb: (chat: { input: ChatHistoryItem, contexts: Record<string, ContextMessage[]>, composedMessage: Message[], output: StreamingAssistantMessage }) => Promise<void>) {
function onChatTurnComplete(cb: (chat: {
input: ChatHistoryItem
contexts: Record<string, ContextMessage[]>
composedMessage: Message[]
output: StreamingAssistantMessage
outputText: string
toolCalls: ToolMessage[]
}) => Promise<void>) {
onChatTurnCompleteHooks.value.push(cb)
return () => onChatTurnCompleteHooks.value = onChatTurnCompleteHooks.value.filter(hook => hook !== cb) // return remove listener callback
}
@@ -204,12 +218,19 @@ export const useChatStore = defineStore('chat', () => {
await hook(message)
}
async function emitAssistantMessageHooks(message: StreamingAssistantMessage) {
async function emitAssistantMessageHooks(message: StreamingAssistantMessage, messageText?: string) {
for (const hook of onAssistantMessageHooks.value)
await hook(message)
await hook(message, messageText)
}
async function emitChatTurnCompleteHooks(chat: { input: ChatHistoryItem, contexts: Record<string, ContextMessage[]>, composedMessage: Message[], output: StreamingAssistantMessage }) {
async function emitChatTurnCompleteHooks(chat: {
input: ChatHistoryItem
contexts: Record<string, ContextMessage[]>
composedMessage: Message[]
output: StreamingAssistantMessage
outputText: string
toolCalls: ToolMessage[]
}) {
for (const hook of onChatTurnCompleteHooks.value)
await hook(chat)
}
@@ -528,12 +549,14 @@ export const useChatStore = defineStore('chat', () => {
await emitAssistantResponseEndHooks(fullText)
await emitAfterSendHooks(sendingMessage)
await emitAssistantMessageHooks({ ...streamingMessage.value })
await emitAssistantMessageHooks({ ...streamingMessage.value }, fullText)
await emitChatTurnCompleteHooks({
input: { role: 'user', content: sendingMessage, createdAt: sendingCreatedAt },
contexts: { ...activeContexts.value },
composedMessage: newMessages as Message[],
output: { ...streamingMessage.value },
outputText: fullText,
toolCalls: sessionMessagesForSend.filter(msg => msg.role === 'tool') as ToolMessage[],
})
// Reset the streaming message for the next turn
@@ -576,12 +599,13 @@ export const useChatStore = defineStore('chat', () => {
send,
setActiveSession,
ingestContextMessage,
cleanupMessages,
getAllSessions,
replaceSessions,
resetAllSessions,
ingestContextMessage,
clearHooks,
emitBeforeMessageComposedHooks,
@@ -1,5 +1,8 @@
import type { UserMessage } from '@xsai/shared-chat'
import type { ChatStreamEvent, ContextMessage } from '../../../types/chat'
import { isStageTamagotchi, isStageWeb } from '@proj-airi/stage-shared'
import { useBroadcastChannel } from '@vueuse/core'
import { Mutex } from 'es-toolkit'
import { defineStore } from 'pinia'
@@ -87,8 +90,32 @@ export const useContextBridgeStore = defineStore('mods:api:context-bridge', () =
}),
)
disposeHookFns.value.push(chatStore.onAssistantMessage(async (message, messageText) => {
serverChannelStore.send({
type: 'output:gen-ai:chat:message',
data: {
message,
'stage-web': isStageWeb(),
'stage-tamagotchi': isStageTamagotchi(),
'gen-ai:chat': messageText || '',
},
})
}))
disposeHookFns.value.push(chatStore.onChatTurnComplete(async (chat) => {
serverChannelStore.send({ type: 'output:gen-ai:chat:message', data: { messages: [chat.output] } })
serverChannelStore.send({
type: 'output:gen-ai:chat:complete',
data: {
'input': chat.input as UserMessage,
'composedMessage': chat.composedMessage,
'contexts': chat.contexts,
'message': chat.output,
'toolCalls': [],
'stage-web': isStageWeb(),
'stage-tamagotchi': isStageTamagotchi(),
'gen-ai:chat': chat.outputText,
},
})
}))
const { stop: stopIncomingStreamWatch } = watch(incomingStreamEvent, async (event) => {