crush-level-web/src/app/(main)/chat/[id]/stream-chat.ts

301 lines
7.8 KiB
TypeScript

'use client';
import { Channel, StreamChat } from 'stream-chat';
import { create } from 'zustand';
import {
getUserToken,
createChannel,
deleteChannel,
fetchUserChatSetting,
updateUserChatSetting,
} from '@/services/chat';
import { parseSSEStream, parseData } from '@/utils/streamParser';
import { protect } from '@/lib/protect';
type Message = {
key: string;
role: string;
content: string;
};
export type ChatSettingType = {
chatModel: string;
longText: 0 | 1;
maximumReplies: number;
background: string;
font: number;
voiceActor: string;
};
type UserType = {
userId: string;
userName: string;
};
interface StreamChatStore {
client: StreamChat | null;
user: UserType;
chatSetting: ChatSettingType;
// 连接 StreamChat 客户端
connect: (user: any) => Promise<void>;
// 频道
channels: Channel[];
currentChannel: Channel | null;
// 用户聊天设置管理
setChatSetting: (chatSetting: any) => void;
fetchUserChatSetting: () => Promise<void>;
updateUserChatSetting: () => Promise<void>;
// 创建某个角色的聊天频道, 返回channelId
createChannel: (
characterId: string
) => Promise<{ result: 'ok' | 'error'; channelId?: string; error?: 'need_auth' | string }>;
switchToChannel: (id: string) => Promise<void>;
queryChannels: (filter: any) => Promise<Channel[]>;
deleteChannel: (
id: string[]
) => Promise<{ result: string; newChannels?: Channel[]; error?: unknown }>;
getCurrentCharacter: () => any | null;
// 消息列表
messages: Message[];
setMessages: (messages: Message[]) => void;
// 发送消息
sendMessage: (content: string) => Promise<void>;
// 清除通知
clearNotifications: () => Promise<void>;
// 推出登录,清除状态
clearClient: () => void;
}
export const useStreamChatStore = create<StreamChatStore>((set, get) => ({
client: null,
user: {
userId: '',
userName: '',
},
chatSetting: {
chatModel: '',
longText: 0,
maximumReplies: 0,
background: '',
font: 16,
voiceActor: '',
},
channels: [],
messages: [],
setMessages: (messages: any[]) => set({ messages }),
currentChannel: null,
// 获取当前聊天频道中的角色id
getCurrentCharacter() {
const { currentChannel, user } = get();
return Object.values(currentChannel?.state?.members || {})?.find((i) => {
return i.user_id !== user?.userId;
});
},
// 创建某个角色的聊天频道
async createChannel(characterId: string) {
const { user, client } = get();
const { switchToChannel, queryChannels } = get();
if (!client) {
return { result: 'error', error: 'need_auth' };
}
const { data } = await protect(() =>
createChannel({
userId: user.userId,
userName: user.userName,
characterId,
})
);
if (!data?.channelId) {
return { result: 'error', error: 'create_channel_failed' };
}
await queryChannels({});
switchToChannel(data.channelId);
return { result: 'ok', channelId: data.channelId };
},
setChatSetting: (setting: any) => {
const { chatSetting } = get();
set({ chatSetting: { ...chatSetting, ...setting } });
},
async fetchUserChatSetting() {
const { user } = get();
const { data } = await fetchUserChatSetting({
userId: Number(user.userId),
});
if (data) {
set({ chatSetting: data });
}
},
async updateUserChatSetting() {
const { user, chatSetting, fetchUserChatSetting } = get();
await updateUserChatSetting({
...chatSetting,
userId: user.userId,
});
fetchUserChatSetting();
},
async connect(user) {
const { client, queryChannels, fetchUserChatSetting } = get();
set({ user });
if (client) {
return;
}
const { data } = await getUserToken(user);
const streamClient = new StreamChat(process.env.NEXT_PUBLIC_STREAM_CHAT_API_KEY || '');
await protect(() =>
streamClient.connectUser(
{
id: user.userId,
name: user.userName,
},
data
)
);
set({ client: streamClient });
await queryChannels({});
await fetchUserChatSetting();
},
async switchToChannel(id: string) {
const { client, user } = get();
const channel = client!.channel('messaging', id);
const result = await protect(() =>
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 });
},
async queryChannels() {
const { user, client } = get();
if (!client) {
console.error('StreamChat client is not connected');
return [];
}
let channels: Channel[] = [];
try {
channels = await protect(() =>
client.queryChannels(
{
members: {
$in: [user.userId],
},
},
{
last_message_at: -1,
},
{
message_limit: 1, // 返回最新的1条消息
}
)
);
set({ channels });
} catch (error) {
console.error('Failed to query channels:', error);
}
return channels;
},
async deleteChannel(ids: string[]) {
const { channels, currentChannel, client, queryChannels } = get();
const deleteChannels = channels.filter((ch) => ids.includes(ch.id!));
try {
await Promise.all(
deleteChannels.map((ch) => {
return client?.channel('messaging', ch.id)?.delete();
})
);
await deleteChannel(ids);
const newChannels = await queryChannels({});
if (currentChannel?.id && ids.includes(currentChannel.id)) {
set({ currentChannel: null });
}
return { result: 'ok', newChannels };
} catch (error) {
return { result: 'error', error: error };
}
},
async clearNotifications() {},
// 发送消息
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/ai/generateReply`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId: user.userId,
channelId: currentChannel?.id || '',
message: content,
characterId: getCurrentCharacter()?.user_id,
language: 'zh',
}),
}
);
// 处理服务器返回的 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 = lastMsg.content + d.text || '';
}
finalMessages[finalMessages.length - 1] = lastMsg;
setMessages([...finalMessages]);
}
});
},
// 推出登录,清除状态
clearClient: async () => {
const { client } = get();
await client?.disconnectUser();
set({
client: null,
user: { userId: '', userName: '' },
chatSetting: {
chatModel: '',
longText: 0,
maximumReplies: 0,
background: '',
font: 16,
voiceActor: '',
},
channels: [],
messages: [],
currentChannel: null,
});
},
}));