crush-level-web/src/stores/stream-chat.ts

222 lines
5.9 KiB
TypeScript
Raw Normal View History

2025-12-11 11:31:56 +00:00
'use client';
import { Channel, StreamChat } from 'stream-chat';
import { create } from 'zustand';
2025-12-17 10:13:47 +00:00
import { getUserToken, createChannel } from '@/services/editor';
import { parseSSEStream, parseData } from '@/utils/streamParser';
type Message = {
key: string;
role: string;
content: string;
};
2025-12-11 11:31:56 +00:00
interface StreamChatStore {
2025-12-17 10:13:47 +00:00
client: StreamChat | null;
user: {
userId: string;
userName: string;
};
// 连接 StreamChat 客户端
2025-12-11 11:31:56 +00:00
connect: (user: any) => Promise<void>;
2025-12-17 10:13:47 +00:00
// 频道
2025-12-11 11:31:56 +00:00
channels: Channel[];
currentChannel: Channel | null;
2025-12-17 10:13:47 +00:00
// 创建某个角色的聊天频道, 返回channelId
createChannel: (characterId: string) => Promise<string | false>;
2025-12-11 11:31:56 +00:00
switchToChannel: (id: string) => Promise<void>;
queryChannels: (filter: any) => Promise<void>;
deleteChannel: (id: string) => Promise<void>;
clearChannels: () => Promise<void>;
2025-12-17 10:13:47 +00:00
getCurrentCharacter: () => any | null;
// 消息列表
messages: Message[];
setMessages: (messages: Message[]) => void;
// 发送消息
sendMessage: (content: string) => Promise<void>;
// 清除通知
2025-12-11 11:31:56 +00:00
clearNotifications: () => Promise<void>;
}
export const useStreamChatStore = create<StreamChatStore>((set, get) => ({
2025-12-17 10:13:47 +00:00
client: null,
user: {
userId: '',
userName: '',
},
2025-12-11 11:31:56 +00:00
channels: [],
2025-12-17 10:13:47 +00:00
messages: [],
setMessages: (messages: any[]) => set({ messages }),
2025-12-11 11:31:56 +00:00
currentChannel: null,
2025-12-17 10:13:47 +00:00
// 获取当前聊天频道中的角色id
getCurrentCharacter() {
const { currentChannel, user } = get();
return (
Object.values(currentChannel?.state?.members || {})?.find((i) => i.user?.id !== user?.userId)
?.user?.id || null
);
},
// 创建某个角色的聊天频道
async createChannel(characterId: string) {
const { user, client } = get();
const { switchToChannel, queryChannels } = get();
if (!client) {
return false;
}
const { data } = await createChannel({
userId: user.userId,
userName: user.userName,
characterId,
});
if (!data?.channelId) {
return false;
}
await queryChannels({});
switchToChannel(data.channelId);
return data.channelId;
},
2025-12-11 11:31:56 +00:00
async connect(user) {
2025-12-17 10:13:47 +00:00
const { client } = get();
set({ user });
2025-12-15 11:31:18 +00:00
if (client) return;
2025-12-11 11:31:56 +00:00
const { data } = await getUserToken(user);
2025-12-17 10:13:47 +00:00
const streamClient = new StreamChat(process.env.NEXT_PUBLIC_STREAM_CHAT_API_KEY || '');
const res = await streamClient.connectUser(
2025-12-11 11:31:56 +00:00
{
id: user.userId,
name: user.userName,
},
data
);
2025-12-17 10:13:47 +00:00
set({ client: streamClient });
2025-12-11 11:31:56 +00:00
},
2025-12-17 10:13:47 +00:00
2025-12-11 11:31:56 +00:00
async switchToChannel(id: string) {
2025-12-17 10:13:47 +00:00
const { client, user } = get();
const channel = client!.channel('messaging', id);
const result = await channel.query({
messages: { limit: 100 },
});
const messages = result.messages.map((i) => ({
key: i.id,
role: i.user?.id === user.userId ? 'user' : 'assistant',
content: i.text!,
}));
set({ currentChannel: channel, messages });
2025-12-11 11:31:56 +00:00
},
2025-12-17 10:13:47 +00:00
async queryChannels() {
const { user, client } = get();
2025-12-11 11:31:56 +00:00
if (!client) {
console.error('StreamChat client is not connected');
return;
}
try {
2025-12-17 10:13:47 +00:00
const channels = await client.queryChannels(
{
members: {
$in: [user.userId],
},
},
{
last_message_at: -1,
},
{
message_limit: 1, // 返回最新的1条消息
}
);
2025-12-11 11:31:56 +00:00
set({ channels });
} catch (error) {
console.error('Failed to query channels:', error);
}
},
2025-12-17 10:13:47 +00:00
2025-12-11 11:31:56 +00:00
async deleteChannel(id: string) {
const { channels, currentChannel, queryChannels } = get();
const channel = channels.find((ch) => ch.id === id);
if (!channel) {
console.warn(`Channel with id ${id} not found`);
return;
}
try {
await channel.delete();
await queryChannels({});
2025-12-17 10:13:47 +00:00
if (currentChannel?.id === id) {
set({ currentChannel: null });
}
2025-12-11 11:31:56 +00:00
} catch (error) {
console.error(`Failed to delete channel ${id}:`, error);
}
},
2025-12-17 10:13:47 +00:00
2025-12-11 11:31:56 +00:00
async clearChannels() {
const { channels } = get();
try {
// 停止监听所有频道
for (const channel of channels) {
try {
await channel.stopWatching();
} catch (error) {
console.warn(`Failed to stop watching channel ${channel.id}:`, error);
}
}
// 清空频道列表和当前频道
set({
channels: [],
currentChannel: null,
});
} catch (error) {
console.error('Failed to clear channels:', error);
}
},
async clearNotifications() {},
2025-12-17 10:13:47 +00:00
// 发送消息
sendMessage: async (content: any) => {
const { user, currentChannel, getCurrentCharacter, setMessages, messages } = get();
// 过滤出用户和助手的消息
const filteredMessages = messages.filter((i) => i.role === 'user' || i.role === 'assistant');
let finalMessages = [
...filteredMessages,
{ key: user.userId, role: 'user', content: content },
{ key: 'assistant', role: 'assistant', content: '' },
];
setMessages(finalMessages);
// 发送消息到服务器
const response = await fetch(
`${process.env.NEXT_PUBLIC_CHAT_API_URL}/chat-api/chat/testPrompt`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId: user.userId,
channelId: currentChannel?.id || '',
message: content,
promptTemplateId: 'default',
characterId: getCurrentCharacter()?.id,
modelName: 'gpt-3.5-turbo',
}),
}
);
// 处理服务器返回的 SSE 流
await parseSSEStream(response, (event: string, data: string) => {
if (event === 'chat-message') {
const d = parseData(data);
const lastMsg = finalMessages[finalMessages.length - 1];
if (lastMsg.role === 'assistant') {
lastMsg.content = d.content || '';
}
setMessages([...finalMessages]);
}
});
},
2025-12-11 11:31:56 +00:00
}));