From 8a841d65dae772ccfc13ec717b15549af43d73e5 Mon Sep 17 00:00:00 2001 From: Neko Ayaka Date: Sat, 27 Dec 2025 17:48:41 +0800 Subject: [PATCH] feat(server-sdk,stage-ui,server-shared): more server channel hooks --- packages/server-sdk/src/client.ts | 8 +++- .../src/types/websocket/events.ts | 27 +++++++++--- packages/stage-ui/src/stores/chat.ts | 44 ++++++++++++++----- .../src/stores/mods/api/context-bridge.ts | 29 +++++++++++- 4 files changed, 90 insertions(+), 18 deletions(-) diff --git a/packages/server-sdk/src/client.ts b/packages/server-sdk/src/client.ts index 4412ccdf4..58821cbf5 100644 --- a/packages/server-sdk/src/client.ts +++ b/packages/server-sdk/src/client.ts @@ -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' diff --git a/packages/server-shared/src/types/websocket/events.ts b/packages/server-shared/src/types/websocket/events.ts index 5c4725695..a7ac76e0f 100644 --- a/packages/server-shared/src/types/websocket/events.ts +++ b/packages/server-shared/src/types/websocket/events.ts @@ -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 { 'error': { message: string } + 'module:authenticate': { token: string } @@ -93,11 +94,13 @@ export interface WebSocketEvents { 'module:configure': { config: C } + 'ui:configure': { moduleName: string moduleIndex?: number config: C | Record } + 'input:text': { text: string } & Partial> @@ -107,9 +110,21 @@ export interface WebSocketEvents { 'input:voice': { audio: ArrayBuffer } & Partial> + + 'output:gen-ai:chat:tool-call': { + toolCalls: ToolMessage[] + } & Partial> & Partial> 'output:gen-ai:chat:message': { - messages: Array - } & Partial> & Partial> + message: AssistantMessage + } & Partial> & Partial> + 'output:gen-ai:chat:complete': { + input: UserMessage + contexts: Record + composedMessage: Array + message: AssistantMessage + toolCalls: ToolMessage[] + } & Partial> & Partial> + 'context:update': ContextUpdate } diff --git a/packages/stage-ui/src/stores/chat.ts b/packages/stage-ui/src/stores/chat.ts index 15390689f..b03aaef5c 100644 --- a/packages/stage-ui/src/stores/chat.ts +++ b/packages/stage-ui/src/stores/chat.ts @@ -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 Promise>>([]) const onStreamEndHooks = ref Promise>>([]) const onAssistantResponseEndHooks = ref Promise>>([]) - const onAssistantMessageHooks = ref Promise>>([]) - const onChatTurnCompleteHooks = ref, composedMessage: Message[], output: StreamingAssistantMessage }) => Promise>>([]) + const onAssistantMessageHooks = ref Promise>>([]) + const onChatTurnCompleteHooks = ref + composedMessage: Message[] + output: StreamingAssistantMessage + outputText: string + toolCalls: ToolMessage[] + }) => Promise>>([]) function onBeforeMessageComposed(cb: (message: string) => Promise) { 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) { + function onAssistantMessage(cb: (message: StreamingAssistantMessage, messageText?: string) => Promise) { 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, composedMessage: Message[], output: StreamingAssistantMessage }) => Promise) { + function onChatTurnComplete(cb: (chat: { + input: ChatHistoryItem + contexts: Record + composedMessage: Message[] + output: StreamingAssistantMessage + outputText: string + toolCalls: ToolMessage[] + }) => Promise) { 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, composedMessage: Message[], output: StreamingAssistantMessage }) { + async function emitChatTurnCompleteHooks(chat: { + input: ChatHistoryItem + contexts: Record + 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, diff --git a/packages/stage-ui/src/stores/mods/api/context-bridge.ts b/packages/stage-ui/src/stores/mods/api/context-bridge.ts index 660339692..80180f402 100644 --- a/packages/stage-ui/src/stores/mods/api/context-bridge.ts +++ b/packages/stage-ui/src/stores/mods/api/context-bridge.ts @@ -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) => {