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
|
|
|
}));
|