259 lines
9.3 KiB
TypeScript
259 lines
9.3 KiB
TypeScript
import type { Message } from '../types/types';
|
||
import { CozeAPI } from '@coze/api';
|
||
|
||
|
||
// 定义Coze事件数据接口
|
||
interface DeltaEventData {
|
||
content?: string;
|
||
delta?: { content?: string };
|
||
}
|
||
|
||
interface CompletedEventData {
|
||
conversation_id?: string;
|
||
usage?: { token_count?: number };
|
||
token_count?: number;
|
||
messages?: { content?: string }[];
|
||
}
|
||
|
||
// 错误事件数据接口 - 备用
|
||
// interface ErrorEventData {
|
||
// code?: string;
|
||
// message?: string;
|
||
// type?: string;
|
||
// }
|
||
|
||
class CozeService {
|
||
private apiKey: string | null = null;
|
||
private botId: string | null = null;
|
||
private userId: string = `user_${Date.now()}`; // 生成一个临时用户ID
|
||
private apiClient: CozeAPI | null = null;
|
||
private conversationId: string | null = null; // 存储对话ID,用于后续调用
|
||
|
||
constructor() {
|
||
// 初始化服务,实际使用时需要设置API密钥和Bot ID
|
||
}
|
||
|
||
/**
|
||
* 设置API密钥和Bot ID
|
||
* @param apiKey Coze API密钥
|
||
* @param botId Bot ID
|
||
*/
|
||
setCredentials(apiKey: string, botId: string): void {
|
||
this.apiKey = apiKey;
|
||
this.botId = botId;
|
||
|
||
// 初始化Coze API客户端 - 严格按照官方示例实现
|
||
this.apiClient = new CozeAPI({
|
||
token: apiKey,
|
||
baseURL: 'https://api.coze.cn'
|
||
});
|
||
|
||
console.log('Coze API client initialized with official SDK');
|
||
}
|
||
|
||
/**
|
||
* 发送消息给Coze agent
|
||
* 使用官方SDK的stream方法实现流式响应
|
||
* 严格按照api.ts示例实现,使用for await...of循环处理流式响应
|
||
* @param message 用户消息内容
|
||
* @param onStream 流式响应回调函数
|
||
* @returns Promise<Message> 完整的响应消息
|
||
*/
|
||
async sendMessage(
|
||
message: string,
|
||
onStream?: (partialText: string, isDone: boolean) => void
|
||
): Promise<Message> {
|
||
// 检查初始化状态
|
||
if (!this.isInitialized()) {
|
||
console.warn('Coze服务尚未初始化或凭据不完整。使用模拟回复。');
|
||
return this.getMockResponse(message);
|
||
}
|
||
|
||
try {
|
||
console.log('正在使用官方SDK发送消息到Coze服务...');
|
||
console.log('Bot ID:', this.botId);
|
||
console.log('User ID:', this.userId);
|
||
|
||
// 严格按照api.ts示例调用chat.stream方法
|
||
const res = await this.apiClient!.chat.stream({
|
||
bot_id: this.botId!,
|
||
user_id: this.userId,
|
||
conversation_id: this.conversationId || undefined, // 如果有保存的对话ID则使用
|
||
additional_messages: [
|
||
{
|
||
"content": message,
|
||
"content_type": "text",
|
||
"role": "user",
|
||
"type": "question"
|
||
}
|
||
] as any // eslint-disable-line @typescript-eslint/no-explicit-any,
|
||
});
|
||
|
||
let fullContent = '';
|
||
const messageId = Date.now().toString();
|
||
|
||
console.log('开始处理Coze流式响应...');
|
||
|
||
// 严格按照api.ts的实现方式,使用for await...of循环处理流式响应
|
||
for await (const event of res) {
|
||
try {
|
||
console.log('接收到Coze事件:', event.event || 'unknown');
|
||
|
||
// 1. 处理消息增量事件
|
||
if (event.event === 'conversation.message.delta') {
|
||
// 根据TypeScript类型定义,content可能直接在data上
|
||
const deltaData = event.data as DeltaEventData;
|
||
if (deltaData && deltaData.content) {
|
||
const deltaContent = deltaData.content;
|
||
fullContent += deltaContent;
|
||
console.log('增量内容:', fullContent.substring(0, 100) + '...');
|
||
|
||
// 通知流式更新
|
||
if (onStream) {
|
||
onStream(fullContent, false);
|
||
}
|
||
} else if (deltaData && deltaData.delta && deltaData.delta.content) {
|
||
// 兼容另一种可能的数据结构
|
||
const deltaContent = deltaData.delta.content;
|
||
fullContent += deltaContent;
|
||
console.log('增量内容(delta路径):', fullContent.substring(0, 100) + '...');
|
||
|
||
if (onStream) {
|
||
onStream(fullContent, false);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 2. 处理聊天完成事件
|
||
if (event.event === 'conversation.chat.completed') {
|
||
console.log(); // 输出换行
|
||
// 尝试从data中获取conversation_id和usage信息
|
||
const dataObj = event.data as CompletedEventData;
|
||
|
||
// 打印conversation_id并保存
|
||
if (dataObj && dataObj.conversation_id) {
|
||
console.log('Conversation ID:', dataObj.conversation_id);
|
||
this.conversationId = dataObj.conversation_id; // 保存对话ID供下次使用
|
||
}
|
||
|
||
// 打印token使用量
|
||
if (dataObj && dataObj.usage && dataObj.usage.token_count) {
|
||
console.log('token usage:', dataObj.usage.token_count);
|
||
} else if (dataObj && dataObj.token_count) {
|
||
console.log('token usage:', dataObj.token_count);
|
||
}
|
||
|
||
// 尝试从data中获取完整内容(如果有的话)
|
||
if (dataObj && dataObj.messages && dataObj.messages[0] && dataObj.messages[0].content) {
|
||
fullContent = dataObj.messages[0].content || '';
|
||
console.log('使用完成事件中的完整内容');
|
||
}
|
||
}
|
||
|
||
// 3. 处理错误事件
|
||
if (event.event === 'error') {
|
||
console.error('错误事件:', event.data);
|
||
throw new Error(`Coze服务错误: ${JSON.stringify(event.data)}`);
|
||
}
|
||
} catch (err) {
|
||
console.error('处理事件时出错:', err);
|
||
console.log('错误事件完整数据:', JSON.stringify(event, null, 2));
|
||
}
|
||
}
|
||
|
||
// 流式处理完成,检查是否有内容
|
||
console.log('Coze流式响应处理完成,总内容长度:', fullContent.length);
|
||
|
||
if (fullContent.trim()) {
|
||
const botMessage: Message = {
|
||
id: messageId,
|
||
content: fullContent,
|
||
sender: 'bot',
|
||
timestamp: new Date()
|
||
};
|
||
|
||
// 通知流式更新完成
|
||
if (onStream) {
|
||
onStream(fullContent, true);
|
||
}
|
||
|
||
return botMessage;
|
||
} else {
|
||
throw new Error('Coze服务返回空内容,请检查API配置和网络连接');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error sending message to Coze with official SDK:', error);
|
||
// 获取具体的错误信息
|
||
const errorMsg = error instanceof Error ? error.message : '未知错误';
|
||
|
||
// 即使出错也返回模拟回复
|
||
return this.getMockResponse(message, errorMsg);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取模拟回复,当Coze API调用失败时使用
|
||
*/
|
||
private getMockResponse(message: string, error?: string): Message {
|
||
let mockContent = '';
|
||
|
||
// 根据用户消息内容生成更有针对性的模拟回复
|
||
if (message.includes('你好') || message.includes('hello') || message.includes('hi')) {
|
||
mockContent = '你好!我是你的爆款文案策划师。为了给你提供更有针对性的方案,请告诉我:\n1. 你的行业/产品/服务\n2. 你的目标用户群体\n有了这些信息,我就能帮你打造更精准的文案了!';
|
||
} else if (message.includes('文案') || message.includes('内容') || message.includes('写作')) {
|
||
mockContent = '关于文案创作,我有一些专业建议:\n1. 明确目标受众和核心卖点\n2. 使用吸引人的标题和开头\n3. 突出产品/服务的独特价值\n4. 加入社会证明(案例、数据)\n5. 包含明确的行动号召\n需要针对特定行业的文案模板吗?';
|
||
} else if (message.includes('价格') || message.includes('费用') || message.includes('多少钱')) {
|
||
mockContent = '文案服务的价格因需求复杂度而异。我们提供:\n- 基础文案套餐:¥500-1000\n- 营销策划套餐:¥1000-3000\n- 品牌全案套餐:¥3000+\n欢迎告诉我你的具体需求,我可以为你提供更精准的报价。';
|
||
} else {
|
||
mockContent = '感谢你的提问!作为文案策划师,我可以帮你:\n- 撰写吸引人的营销文案\n- 设计爆款标题\n- 优化产品描述\n- 制定内容营销策略\n请告诉我你具体需要哪方面的帮助?';
|
||
}
|
||
|
||
// 如果有错误信息,可以选择性地在开头添加提示
|
||
if (error) {
|
||
console.log('使用模拟回复,原因:', error);
|
||
// 不直接显示错误,只显示友好提示
|
||
mockContent = '【提示:当前使用的是智能助手的推荐回复】\n\n' + mockContent;
|
||
}
|
||
|
||
return {
|
||
id: Date.now().toString(),
|
||
content: mockContent,
|
||
sender: 'bot',
|
||
timestamp: new Date()
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 重置对话状态
|
||
*/
|
||
resetConversation(): void {
|
||
this.conversationId = null; // 清除保存的对话ID
|
||
console.log('Conversation reset');
|
||
}
|
||
|
||
/**
|
||
* 检查客户端是否已初始化
|
||
*/
|
||
isInitialized(): boolean {
|
||
return !!this.apiClient && !!this.apiKey && !!this.botId;
|
||
}
|
||
|
||
/**
|
||
* 设置用户ID
|
||
*/
|
||
setUserId(userId: string): void {
|
||
this.userId = userId;
|
||
}
|
||
|
||
/**
|
||
* 获取用户ID
|
||
*/
|
||
getUserId(): string {
|
||
return this.userId;
|
||
}
|
||
}
|
||
|
||
// 导出单例实例
|
||
// 创建并导出单例实例
|
||
const cozeServiceInstance = new CozeService();
|
||
export default cozeServiceInstance; |