2025-12-11 11:31:56 +00:00
|
|
|
'use client';
|
|
|
|
|
import { Channel, StreamChat } from 'stream-chat';
|
|
|
|
|
import { create } from 'zustand';
|
2025-12-18 10:14:12 +00:00
|
|
|
import {
|
|
|
|
|
getUserToken,
|
|
|
|
|
createChannel,
|
|
|
|
|
deleteChannel,
|
|
|
|
|
fetchUserChatSetting,
|
|
|
|
|
updateUserChatSetting,
|
|
|
|
|
} from '@/services/chat';
|
2025-12-17 10:13:47 +00:00
|
|
|
import { parseSSEStream, parseData } from '@/utils/streamParser';
|
2025-12-18 10:14:12 +00:00
|
|
|
import { protect } from '@/lib/protect';
|
2025-12-17 10:13:47 +00:00
|
|
|
|
|
|
|
|
type Message = {
|
|
|
|
|
key: string;
|
|
|
|
|
role: string;
|
|
|
|
|
content: string;
|
|
|
|
|
};
|
2025-12-11 11:31:56 +00:00
|
|
|
|
2025-12-18 10:14:12 +00:00
|
|
|
export type ChatSettingType = {
|
|
|
|
|
chatModel: string;
|
|
|
|
|
longText: 0 | 1;
|
|
|
|
|
maximumReplies: number;
|
|
|
|
|
background: string;
|
|
|
|
|
font: number;
|
|
|
|
|
voiceActor: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type UserType = {
|
|
|
|
|
userId: string;
|
|
|
|
|
userName: string;
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-11 11:31:56 +00:00
|
|
|
interface StreamChatStore {
|
2025-12-17 10:13:47 +00:00
|
|
|
client: StreamChat | null;
|
2025-12-18 10:14:12 +00:00
|
|
|
user: UserType;
|
|
|
|
|
chatSetting: ChatSettingType;
|
2025-12-17 10:13:47 +00:00
|
|
|
// 连接 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-18 10:14:12 +00:00
|
|
|
|
|
|
|
|
// 用户聊天设置管理
|
|
|
|
|
setChatSetting: (chatSetting: any) => void;
|
|
|
|
|
fetchUserChatSetting: () => Promise<void>;
|
|
|
|
|
updateUserChatSetting: () => Promise<void>;
|
|
|
|
|
|
2025-12-17 10:13:47 +00:00
|
|
|
// 创建某个角色的聊天频道, 返回channelId
|
2025-12-19 06:58:19 +00:00
|
|
|
createChannel: (
|
|
|
|
|
characterId: string
|
|
|
|
|
) => Promise<{ result: 'ok' | 'error'; channelId?: string; error?: 'need_auth' | string }>;
|
2025-12-11 11:31:56 +00:00
|
|
|
switchToChannel: (id: string) => Promise<void>;
|
2025-12-18 10:14:12 +00:00
|
|
|
queryChannels: (filter: any) => Promise<Channel[]>;
|
|
|
|
|
deleteChannel: (
|
|
|
|
|
id: string[]
|
|
|
|
|
) => Promise<{ result: string; newChannels?: Channel[]; error?: unknown }>;
|
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>;
|
2025-12-18 10:14:12 +00:00
|
|
|
|
|
|
|
|
// 推出登录,清除状态
|
|
|
|
|
clearClient: () => void;
|
2025-12-11 11:31:56 +00:00
|
|
|
}
|
|
|
|
|
export const useStreamChatStore = create<StreamChatStore>((set, get) => ({
|
2025-12-17 10:13:47 +00:00
|
|
|
client: null,
|
|
|
|
|
user: {
|
|
|
|
|
userId: '',
|
|
|
|
|
userName: '',
|
|
|
|
|
},
|
2025-12-18 10:14:12 +00:00
|
|
|
chatSetting: {
|
|
|
|
|
chatModel: '',
|
|
|
|
|
longText: 0,
|
|
|
|
|
maximumReplies: 0,
|
|
|
|
|
background: '',
|
|
|
|
|
font: 16,
|
|
|
|
|
voiceActor: '',
|
|
|
|
|
},
|
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();
|
2025-12-18 10:14:12 +00:00
|
|
|
return Object.values(currentChannel?.state?.members || {})?.find((i) => {
|
|
|
|
|
return i.user_id !== user?.userId;
|
|
|
|
|
});
|
2025-12-17 10:13:47 +00:00
|
|
|
},
|
|
|
|
|
// 创建某个角色的聊天频道
|
|
|
|
|
async createChannel(characterId: string) {
|
|
|
|
|
const { user, client } = get();
|
|
|
|
|
const { switchToChannel, queryChannels } = get();
|
|
|
|
|
if (!client) {
|
2025-12-19 06:58:19 +00:00
|
|
|
return { result: 'error', error: 'need_auth' };
|
2025-12-17 10:13:47 +00:00
|
|
|
}
|
2025-12-19 06:58:19 +00:00
|
|
|
const { data } = await protect(() =>
|
|
|
|
|
createChannel({
|
|
|
|
|
userId: user.userId,
|
|
|
|
|
userName: user.userName,
|
|
|
|
|
characterId,
|
|
|
|
|
})
|
|
|
|
|
);
|
2025-12-17 10:13:47 +00:00
|
|
|
if (!data?.channelId) {
|
2025-12-19 06:58:19 +00:00
|
|
|
return { result: 'error', error: 'create_channel_failed' };
|
2025-12-17 10:13:47 +00:00
|
|
|
}
|
|
|
|
|
await queryChannels({});
|
|
|
|
|
switchToChannel(data.channelId);
|
2025-12-19 06:58:19 +00:00
|
|
|
return { result: 'ok', channelId: data.channelId };
|
2025-12-17 10:13:47 +00:00
|
|
|
},
|
|
|
|
|
|
2025-12-18 10:14:12 +00:00
|
|
|
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();
|
|
|
|
|
},
|
|
|
|
|
|
2025-12-11 11:31:56 +00:00
|
|
|
async connect(user) {
|
2025-12-18 10:14:12 +00:00
|
|
|
const { client, queryChannels, fetchUserChatSetting } = get();
|
2025-12-17 10:13:47 +00:00
|
|
|
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 || '');
|
2025-12-18 10:14:12 +00:00
|
|
|
await protect(() =>
|
|
|
|
|
streamClient.connectUser(
|
|
|
|
|
{
|
|
|
|
|
id: user.userId,
|
|
|
|
|
name: user.userName,
|
|
|
|
|
},
|
|
|
|
|
data
|
|
|
|
|
)
|
2025-12-11 11:31:56 +00:00
|
|
|
);
|
2025-12-17 10:13:47 +00:00
|
|
|
set({ client: streamClient });
|
2025-12-18 10:14:12 +00:00
|
|
|
await queryChannels({});
|
|
|
|
|
await fetchUserChatSetting();
|
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);
|
2025-12-18 10:14:12 +00:00
|
|
|
const result = await protect(() =>
|
|
|
|
|
channel.query({
|
|
|
|
|
messages: { limit: 100 },
|
|
|
|
|
})
|
|
|
|
|
);
|
2025-12-17 10:13:47 +00:00
|
|
|
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');
|
2025-12-18 10:14:12 +00:00
|
|
|
return [];
|
2025-12-11 11:31:56 +00:00
|
|
|
}
|
2025-12-18 10:14:12 +00:00
|
|
|
let channels: Channel[] = [];
|
2025-12-11 11:31:56 +00:00
|
|
|
try {
|
2025-12-18 10:14:12 +00:00
|
|
|
channels = await protect(() =>
|
|
|
|
|
client.queryChannels(
|
|
|
|
|
{
|
|
|
|
|
members: {
|
|
|
|
|
$in: [user.userId],
|
|
|
|
|
},
|
2025-12-17 10:13:47 +00:00
|
|
|
},
|
2025-12-18 10:14:12 +00:00
|
|
|
{
|
|
|
|
|
last_message_at: -1,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
message_limit: 1, // 返回最新的1条消息
|
|
|
|
|
}
|
|
|
|
|
)
|
2025-12-17 10:13:47 +00:00
|
|
|
);
|
2025-12-11 11:31:56 +00:00
|
|
|
set({ channels });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to query channels:', error);
|
|
|
|
|
}
|
2025-12-18 10:14:12 +00:00
|
|
|
return channels;
|
2025-12-11 11:31:56 +00:00
|
|
|
},
|
2025-12-17 10:13:47 +00:00
|
|
|
|
2025-12-18 10:14:12 +00:00
|
|
|
async deleteChannel(ids: string[]) {
|
|
|
|
|
const { channels, currentChannel, client, queryChannels } = get();
|
|
|
|
|
const deleteChannels = channels.filter((ch) => ids.includes(ch.id!));
|
2025-12-11 11:31:56 +00:00
|
|
|
try {
|
2025-12-18 10:14:12 +00:00
|
|
|
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)) {
|
2025-12-17 10:13:47 +00:00
|
|
|
set({ currentChannel: null });
|
|
|
|
|
}
|
2025-12-18 10:14:12 +00:00
|
|
|
return { result: 'ok', newChannels };
|
2025-12-11 11:31:56 +00:00
|
|
|
} catch (error) {
|
2025-12-18 10:14:12 +00:00
|
|
|
return { result: 'error', error: error };
|
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 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(
|
2025-12-18 10:14:12 +00:00
|
|
|
`${process.env.NEXT_PUBLIC_CHAT_API_URL}/chat-api/chat/ai/generateReply`,
|
2025-12-17 10:13:47 +00:00
|
|
|
{
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
userId: user.userId,
|
|
|
|
|
channelId: currentChannel?.id || '',
|
|
|
|
|
message: content,
|
2025-12-18 10:14:12 +00:00
|
|
|
characterId: getCurrentCharacter()?.user_id,
|
|
|
|
|
language: 'zh',
|
2025-12-17 10:13:47 +00:00
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 处理服务器返回的 SSE 流
|
|
|
|
|
await parseSSEStream(response, (event: string, data: string) => {
|
|
|
|
|
if (event === 'chat-message') {
|
|
|
|
|
const d = parseData(data);
|
2025-12-18 10:14:12 +00:00
|
|
|
// 重新赋值最后一项,改变引用
|
|
|
|
|
const lastMsg = { ...finalMessages[finalMessages.length - 1] };
|
2025-12-17 10:13:47 +00:00
|
|
|
if (lastMsg.role === 'assistant') {
|
2025-12-18 10:14:12 +00:00
|
|
|
lastMsg.content = lastMsg.content + d.text || '';
|
2025-12-17 10:13:47 +00:00
|
|
|
}
|
2025-12-18 10:14:12 +00:00
|
|
|
|
|
|
|
|
finalMessages[finalMessages.length - 1] = lastMsg;
|
2025-12-17 10:13:47 +00:00
|
|
|
setMessages([...finalMessages]);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
2025-12-18 10:14:12 +00:00
|
|
|
|
|
|
|
|
// 推出登录,清除状态
|
|
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
},
|
2025-12-11 11:31:56 +00:00
|
|
|
}));
|