diff --git a/demo/erniebot/demo1.ts b/demo/erniebot/demo1.ts new file mode 100644 index 0000000..7def862 --- /dev/null +++ b/demo/erniebot/demo1.ts @@ -0,0 +1,31 @@ +import { ErnieBot } from '../../src/ErnieBot' + +const bot = new ErnieBot({ + apiKey: process.env.BAIDU_BCE_API_KEY as string, + secretKey: process.env.BAIDU_BCE_SK as string, +}) + +;(async () => { + await bot.sendStreamMessage({ + initialMessages: [ + { + role: 'user', + content: '介绍下你自己', + }, + { + role: 'assistant', + content: '我是小胖AI,你的个人助手', + }, + { + role: 'user', + content: '你叫什么?', + }, + ], + onProgress(t) { + process.stdout.write(t) + }, + onEnd(t) { + console.log('end', t) + }, + }) +})() diff --git a/demo/raw/ernie-bot.ts b/demo/raw/ernie-bot.ts new file mode 100644 index 0000000..d68d5b9 --- /dev/null +++ b/demo/raw/ernie-bot.ts @@ -0,0 +1,52 @@ +import axios from 'axios' + +async function getAccessToken(): Promise { + const url = + `https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${process.env.BAIDU_BCE_API_KEY}&client_secret=${process.env.BAIDU_BCE_SK}` + const response = await axios.post(url, '', { + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + }) + return response.data.access_token +} + +async function main() { + const url = + 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions?access_token=' + + (await getAccessToken()) + const payload = { + messages: [ + { + role: 'user', + content: '你是一个作家', + }, + { + role: 'assistant', + content: '你好呀', + }, + { + role: 'user', + content: '介绍下你自己', + }, + { + role: 'assistant', + content: '我是文心一言', + }, + ], + } + const response = await axios.post(url, payload, { + headers: { 'Content-Type': 'application/json' }, + }) + console.log(response.data) +} + +main() + +// { +// id: 'as-cmmvbiyuf6', +// object: 'chat.completion', +// created: 1690189247, +// result: '您好,我是文心一言,英文名是ERNIE Bot。我能够与人对话互动,回答问题,协助创作,高效便捷地帮助人们获取信息、知识和灵感。', +// is_truncated: false, +// need_clear_history: false, +// usage: { prompt_tokens: 7, completion_tokens: 49, total_tokens: 56 } +// } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1e98be6..714b231 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lxfriday/chatgpt", - "version": "1.3.0", + "version": "1.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@lxfriday/chatgpt", - "version": "1.3.0", + "version": "1.4.1", "license": "MIT", "dependencies": { "@dqbd/tiktoken": "^0.4.0", @@ -14,6 +14,7 @@ "chalk": "^5.2.0", "keyv": "^4.5.2", "lru-cache": "^7.18.1", + "node-fetch": "^3.3.2", "uuid": "^9.0.0" }, "devDependencies": { @@ -641,6 +642,14 @@ "node": ">= 8" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz", @@ -760,6 +769,18 @@ "reusify": "^1.0.4" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz", @@ -798,6 +819,17 @@ "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmmirror.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1115,6 +1147,27 @@ "thenify-all": "^1.0.0" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1561,6 +1614,14 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz", diff --git a/package.json b/package.json index 07e1dcf..fb8a280 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "chalk": "^5.2.0", "keyv": "^4.5.2", "lru-cache": "^7.18.1", + "node-fetch": "^3.3.2", "uuid": "^9.0.0" }, "engines": { diff --git a/src/ErnieBot.ts b/src/ErnieBot.ts new file mode 100644 index 0000000..3cb8809 --- /dev/null +++ b/src/ErnieBot.ts @@ -0,0 +1,115 @@ +import axios from 'axios' +import type { + IErnieBotAccessToken, + IErnieBotAccessTokenErr, + IErnieBotSendMessageOpts, + IErnieBotUserMessage, + IErnieBotAssistantMessage, + TErnieBotCommonMessage, + IErnieBotResponseErr, +} from './ErnieBotTypes' + +interface IErnieBotParams { + apiKey: string + secretKey: string + debug?: boolean +} + +export class ErnieBot { + #apiKey = '' + #model = '' + #secretKey = '' + #debug = false + constructor(opts: IErnieBotParams) { + const { apiKey, secretKey, debug = false } = opts + this.#apiKey = apiKey + this.#secretKey = secretKey + this.#debug = debug + } + private async getAccessToken(): Promise { + const url = `https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${ + this.#apiKey + }&client_secret=${this.#secretKey}` + + const response = await axios(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + validateStatus: (status) => true, + }) + + const data = response.data as IErnieBotAccessToken + if (!data.access_token) + throw { msg: 'access_token 获取失败', status: response.status } + return data.access_token + } + async sendStreamMessage(opts: IErnieBotSendMessageOpts) { + try { + const accessToken = await this.getAccessToken() + const apiUrl = `https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions?access_token=${accessToken}` + + const requestData = { + messages: opts.initialMessages, + stream: true, + } + + const headers = { + 'Content-Type': 'application/json', + } + + const axiosResponse = await axios.post(apiUrl, requestData, { + headers, + responseType: 'stream', + }) + let responseText = '' + const stream = axiosResponse.data // 请求被取消之后变成 undefined + const status = axiosResponse.status + if (status === 200) { + let hasErr = false + let err: IErnieBotResponseErr + stream.on('data', (buf: Buffer) => { + try { + const dataArr = buf.toString().split('\n') + let onDataPieceText = '' + for (const dataStr of dataArr) { + // split 之后的空行,或者结束通知 + if (dataStr[0] === '{') throw dataStr + if (dataStr.indexOf('data: ') !== 0) continue + const parsedData = JSON.parse(dataStr.slice(6)) // [data: ] + const pieceText = parsedData.result || '' + onDataPieceText += pieceText + } + opts.onProgress(onDataPieceText, buf.toString()) + responseText += onDataPieceText + } catch (e) { + const dataStr = buf.toString() + const parsedData = JSON.parse(dataStr) // [data: ] + hasErr = true + err = parsedData + } + }) + stream.on('end', async () => { + opts.onEnd({ + success: !hasErr, + err, + text: responseText, + }) + }) + } else { + opts.onEnd({ + success: false, + err: String(axiosResponse.data), + text: '', + }) + } + } catch (error: any) { + opts.onEnd({ + success: false, + err: String(error.message), + text: '', + }) + } + } +} \ No newline at end of file diff --git a/src/ErnieBotTypes.ts b/src/ErnieBotTypes.ts new file mode 100644 index 0000000..b16a3da --- /dev/null +++ b/src/ErnieBotTypes.ts @@ -0,0 +1,40 @@ +export interface IErnieBotAccessTokenErr { + error_description: string + error: string +} +export interface IErnieBotAccessToken { + refresh_token: string + expires_in: number + session_key: string + access_token: string + scope: string + session_secret: string +} +export interface IErnieBotUserMessage { + role: 'user' + content: string +} +export interface IErnieBotAssistantMessage { + role: 'assistant' + content: string +} +export type TErnieBotCommonMessage = + | IErnieBotUserMessage + | IErnieBotAssistantMessage + +export interface IErnieBotResponseErr { + error_code: number + error_msg: string + id: string +} +export interface IErnieBotOnEndOpts { + success: boolean + err?: IErnieBotResponseErr | string + text: string +} +export interface IErnieBotSendMessageOpts { + initialMessages: TErnieBotCommonMessage[] + onProgress: (t: string, raw: string) => void + onEnd: (opts: IErnieBotOnEndOpts) => void + model?: string +}