import * as tcb from '@cloudbase/node-sdk' import { AITools } from './aitools' import { CHAT_RECORD_DATA_MODEL } from './consts' import { BOT_TYPE_TEXT, TRIGGER_SRC_TCB } from './consts' import { SSESender } from './sse-sender' import { BotCoreSseSender, ChatRecord, ChatRecordDataModel, PickRequired, TcbContext } from './types' import { genRecordId, parseBotId, parseBotTag } from './utils' export interface IBot { /** * sendMessage - POST /v1/aibot/bots/:botId/send-message * * 与 Agent 问答对话接口实现,该接口无返回值,接口内需要通过 this.sseSender 发送 Server-Sent Events 给客户端 * * 微信基础库 wx.cloud.extend.AI.bot.sendMessage API对应接口 * https://developers.weixin.qq.com/miniprogram/dev/wxcloudservice/wxcloud/reference-sdk-api/extend/ai.html#AI-bot-sendMessage * * @param input - 发送消息的输入 * @link */ sendMessage(input: SendMessageInput): Promise wxSendMessage(input: WxSendMessageInput): Promise /** * getChatRecords - GET /v1/aibot/bots/:botId/records * * 查询 Agent 历史对话信息接口实现,返回历史对话记录。 * * 注意:需在 sendMessage 接口中实现记录对话信息,并在此接口查询对话信息并中返回 * * 微信基础库 wx.cloud.extend.AI.bot.getChatRecords API对应接口 * https://developers.weixin.qq.com/miniprogram/dev/wxcloudservice/wxcloud/reference-sdk-api/extend/ai.html#AI-bot-getChatRecords * * @link */ getChatRecords?(input: GetChatRecordInput): Promise /** * getRecommendQuestions - POST /v1/aibot/bots/:botId/recommend-questions * * 获取推荐问题接口实现,返回推荐问题列表 * * 微信基础库 wx.cloud.extend.AI.bot.getRecommendQuestions API对应接口 * https://developers.weixin.qq.com/miniprogram/dev/wxcloudservice/wxcloud/reference-sdk-api/extend/ai.html#AI-bot-getRecommendQuestions * * @link */ getRecommendQuestions?(input: GetRecommendQuestionsInput): Promise /** * sendFeedback - POST /v1/aibot/bots/:botId/feedback * * 发送用户反馈接口实现,返回发送用户反馈结果 * * 微信基础库 wx.cloud.extend.AI.bot.sendFeedback API对应接口 * https://developers.weixin.qq.com/miniprogram/dev/wxcloudservice/wxcloud/reference-sdk-api/extend/ai.html#AI-bot-sendFeedback * * @link */ sendFeedback?(input: SendFeedbackInput): Promise /** * getFeedback - GET /v1/aibot/bots/:botId/feedback * * 获取用户反馈接口实现,返回用户反馈列表 * * 微信基础库 wx.cloud.extend.AI.bot.getFeedBack API对应接口 * https://developers.weixin.qq.com/miniprogram/dev/wxcloudservice/wxcloud/reference-sdk-api/extend/ai.html#AI-bot-getFeedBack * * @link */ getFeedback?(input: GetFeedbackInput): Promise /** * getBotInfo - GET /v1/aibot/bots/:botId * * 获取 Agent 配置实现,返回 Agent 信息 * * 微信基础库 wx.cloud.extend.AI.bot.get API对应接口 * https://developers.weixin.qq.com/miniprogram/dev/wxcloudservice/wxcloud/reference-sdk-api/extend/ai.html#AI-bot-get * * @link */ getBotInfo?(): Promise /** * speechToText - POST /v1/aibot/bots/:botId/speechToText * * 语音转文字 * * @link */ speechToText?(input: SpeechToTextInput): Promise /** * text-to-speech - POST /v1/aibot/bots/:botId/text-to-speech * * 文字转语音 * * @link */ textToSpeech?(input: TextToSpeechInput): Promise /** * text-to-speech - GET /v1/aibot/bots/:botId/text-to-speech * * 获取文字转语音结果 * * @link */ getTextToSpeechResult?(input: GetTextToSpeechResultInput): Promise /** * conversation - POST /v1/aibot/bots/:botId/conversation * * 创建会话 * */ createConversation?(): Promise /** * conversation - GET /v1/aibot/bots/:botId/conversation?limit=10&offset=0 * * 查询会话 * */ getConversation?(input: GetConversationInput): Promise /** * conversation - PATCH /v1/aibot/bots/:botId/conversation/:conversation * * 更新会话 * */ updateConversation?(input: UpdateConversationInput): Promise /** * conversation - DELETE /v1/aibot/bots/:botId/conversation/:conversation * * 删除会话 * */ deleteConversation?(input: DeleteConversationInput): Promise } /** * BotCore - 空白基类,实现了基本的初始化逻辑,配合 IBot 使用 * * ```ts * import { BotCore, IBot } from '@cloudbase/aiagent-framework' * class Bot extends BotBase implements IBot { * 实现 IBot 定义的方法 * } * ``` */ export class BotCore { /** * botTag - botTag */ get botTag() { const url = this.context!.httpContext!.url return parseBotTag(url) } /** * botId - botId */ get botId() { const url = this.context!.httpContext!.url return parseBotId(url) } readonly tools: AITools /** * sseSender - 用于发送 Server-Sent Events */ readonly sseSender: BotCoreSseSender readonly chatRecordDataModelKey: string get chatRecord(): ChatRecordDataModel { return this.tcb.models[this.chatRecordDataModelKey] } private _tcb: tcb.CloudBase | null = null // lazy,不需要 node sdk 的 Agent 本地开发时可以不用配 envId get tcb(): tcb.CloudBase { if (this._tcb) return this._tcb const envId = this.context.extendedContext?.envId if (!envId) throw new Error('Invalid envId') const secretId = this.context.extendedContext?.tmpSecret?.secretId || process.env.TENCENTCLOUD_SECRETID // 本地开发从环境变量读 const secretKey = this.context.extendedContext?.tmpSecret?.secretKey || process.env.TENCENTCLOUD_SECRETKEY // 本地开发从环境变量读 const token = this.context.extendedContext?.tmpSecret?.token this._tcb = tcb.init({ env: envId, secretId, secretKey, sessionToken: token }) return this._tcb } /** * context - TCB 函数的上下文 */ constructor( readonly context: TcbContext, option?: { chatRecordDataModelKey?: string } ) { const { chatRecordDataModelKey = CHAT_RECORD_DATA_MODEL } = option || {} this.chatRecordDataModelKey = chatRecordDataModelKey this.sseSender = new SSESender(context) this.tools = new AITools(context) } /** * 创建一个用户聊天记录 */ createUserRecord({ record }: { record: PickRequired }): ChatRecord { return Object.assign( { role: 'user', record_id: genRecordId(), sender: this.context.extendedContext?.userId, type: BOT_TYPE_TEXT, origin_msg: '{}', trigger_src: TRIGGER_SRC_TCB, bot_id: this.botId, recommend_questions: [], conversation: this.context.extendedContext?.userId, trace_id: this.context.ctxId, async_reply: '', image: '' }, record ) } /** * 创建一个 Agent 聊天记录 */ createBotRecord({ record }: { record: PickRequired }): ChatRecord { return Object.assign( { role: 'assistant', sender: this.context.extendedContext?.userId, type: BOT_TYPE_TEXT, content: '', origin_msg: '{}', trigger_src: TRIGGER_SRC_TCB, bot_id: this.botId, recommend_questions: [], conversation: this.context.extendedContext?.userId, trace_id: this.context.ctxId, async_reply: '', image: '', need_async_reply: false }, record ) } /** * 创建一对 用户 - Agent 的聊天记录,并存到数据模型中。 * 返回值中提供更新 Agent 聊天记录的方法。 */ async createRecordPair({ userContent }: { userContent: string }) { const botRecordId = genRecordId() const userRecord = this.createUserRecord({ record: { content: userContent, reply: botRecordId } }) const botRecord = this.createBotRecord({ record: { record_id: botRecordId } }) await this.chatRecord.create({ data: userRecord }) await this.chatRecord.create({ data: botRecord }) return { userRecord, botRecord, updateBotRecord: async (data: PickRequired) => this.chatRecord.update({ data, filter: { where: { record_id: { $eq: botRecordId } } } }) } } /** * `IBot#getChatRecords` 的默认实现。 * 从数据模型中获取聊天记录。 */ async getChatRecords({ pageNumber, pageSize, sort }: GetChatRecordInput): Promise { const res = await this.chatRecord.list({ filter: { where: { $and: [ { conversation: { $eq: this.context.extendedContext?.userId } }, { bot_id: { $eq: this.botId } } ] } }, getCount: true, select: { $master: true }, orderBy: [ { createdAt: sort as 'desc' | 'asc' } ], pageSize, pageNumber }) const ret: GetChatRecordOutput = { recordList: res.data.records.map(x => ({ ...x, botId: x.bot_id, recordId: x.record_id, role: x.role, content: x.content, conversation: x.conversation, type: x.type, triggerSrc: x.trigger_src, replyTo: x.reply_to, reply: x.reply, image: x.image, createTime: ( x as { createdAt: string } ).createdAt })), total: res.data.total! } return ret } /** * 获取聊天记录,并加以整理,保证: * - 列表中,最旧的聊天记录在第一位,最新的在最后一位 * - 聊天记录以用户的消息开头 * - 用户的消息与 Agent 的消息交替出现 * - 默认情况下保证以 Agent 的消息结尾 */ async getHistoryMessages(option?: { size?: number; removeLastUser?: boolean }) { const { size = 20, removeLastUser = true } = option || {} const { recordList } = await this.getChatRecords({ pageNumber: 1, pageSize: size, sort: 'desc' }) // 保证最旧的聊天记录在第一位,最新的在最后一位 recordList.reverse() const ret = recordList .filter(x => x.content) .reduce< Array< { role: 'user' | 'assistant' content: string } & GetChatRecordOutput['recordList'][number] > >((acc, cur) => { const isUser = acc.length % 2 === 0 isUser && cur.role === 'user' && acc.push( cur as unknown as { role: 'user' content: string } ) !isUser && cur.role === 'assistant' && acc.push( cur as unknown as { role: 'assistant' content: string } ) return acc }, []) if (removeLastUser && ret.length > 0 && ret[ret.length - 1].role === 'user') { ret.pop() } return ret } } export interface ChatHistoryItem { role: string | 'user' | 'assistant' content: string } export interface SendMessageInput { msg: string history?: Array searchEnable?: boolean files?: string[] conversationId?: string } export interface SendMessageOutputChunk { created?: number record_id?: string model?: string version?: string role?: string content?: string finish_reason?: string } export interface WeChatCommonInput { toUserName: string fromUserName: string createTime: number msgType: string msgId: string } export interface WeChatTextInput extends WeChatCommonInput { content: string } export interface WeChatVoiceInput extends WeChatCommonInput { mediaId: string format: string } export interface WeChatWorkCommonInput { msgId: string openKfId: string externalUserId: string sendTime: number origin: number msgType: string } export interface WeChatWorkTextInput extends WeChatWorkCommonInput { text: { content: string } } export interface WeChatWorkVoiceInput extends WeChatWorkCommonInput { voice: { mediaId: string } } export interface WxSendMessageInput { triggerSrc?: string wxVerify?: boolean callbackData: WeChatTextInput | WeChatVoiceInput | WeChatWorkTextInput | WeChatWorkVoiceInput } // 微信接口,使用大驼峰返回 export interface WeChatTextOutput { ToUserName?: string FromUserName?: string CreateTime?: number MsgType?: string Content?: string } export interface WeChatEmptyOutput {} export interface SendFeedbackInput { recordId: string type: string comment: string rating: number tags: string[] input: string aiAnswer: string } export interface SendFeedbackOutput { status: 'success' } export interface GetFeedbackInput { type: string sender: string senderFilter: string minRating: number maxRating: number from: number to: number pageSize: number pageNumber: number } export interface Feedback { type: string botId: string sender: string comment: string rating: number tags: string[] input: string aiAnswer: string createTime: string } export interface GetFeedbackOutput { feedbackList: Array total: number } export interface GetChatRecordInput { sort: string pageSize: number pageNumber: number conversationId?: string } export interface GetChatRecordOutput { recordList: Array<{ botId?: string recordId?: string role?: string content?: string conversation?: string type?: string image?: string triggerSrc?: string reply?: string replyTo?: string createTime?: string fileInfos?: Array<{ cloudId?: string, fileName?: string, bytes?:number, type?:string, }> }> total: number } export interface GetRecommendQuestionsInput { name?: string introduction?: string agentSetting?: string msg?: string history?: Array } export interface GetRecommendQuestionsOutputChunk extends SendMessageOutputChunk {} export interface GetBotInfoOutput { botId?: string name?: string model?: string modelValue?: string agentSetting?: string introduction?: string welcomeMessage?: string avatar?: string background?: string isNeedRecommend?: boolean knowledgeBase?: string[] databaseModel?: string[] initQuestions?: string[] type?: string tags?: string[] searchEnable?: boolean searchFileEnable?: boolean mcpServerList?: Array<{ tools: Array<{ name?: string }> url?: string name?: string }> voiceSettings?: { enable?: boolean inputType?: string outputType?: number } updateTime?: number multiConversationEnable?: boolean } export interface SpeechToTextInput { engSerViceType: string voiceFormat: string url: string isPreview?: boolean } export interface SpeechToTextOutput { Result: string } export interface TextToSpeechInput { text: string voiceType: number isPreview?: boolean } export interface TextToSpeechOutput { TaskId: string } export interface GetTextToSpeechResultInput { taskId?: string isPreview?: string } export interface GetTextToSpeechResultOutput { TaskId: string Status: number StatusStr: string ResultUrl: string } export interface UploadFileInput { fileList?: Array<{ fileId?: string fileName?: string type?: string }> } export interface CreateConversationOutput { conversationId: string title: string } export interface GetConversationInput { isDefault?: boolean conversationId?: string offset?: number limit?: number } export interface GetConversationOutput { data: Array<{ conversationId: string title: string createTime: string updateTime: string }> total: number } export interface UpdateConversationInput { conversationId?: string title: string } export interface UpdateConversationOutput { count: number } export interface DeleteConversationInput { conversationId: string } export interface DeleteConversationOutput { count: number }