add: 添加多平台的支持

This commit is contained in:
lrhh123
2024-06-07 23:58:15 +08:00
parent 2984cd8f70
commit 30a1b25a9c
96 changed files with 7054 additions and 1263 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
PY_HOSTNAME=localhost
PY_PORT=9999
VAR=1234
BKEXE_PATH=./backend/__main__.exe
BKEXE_PATH=./backend/__main__.exe
PKG_VERSION=0.0.1
+4
View File
@@ -30,6 +30,10 @@ module.exports = {
'max-classes-per-file': 'off',
'no-continue': 'off',
'no-plusplus': 'off',
'no-underscore-dangle': 'off',
camelcase: 'off',
'no-use-before-define': 'off',
'no-dupe-class-members': 'off',
},
parserOptions: {
ecmaVersion: 2022,
+1 -2
View File
@@ -17,7 +17,7 @@ import { BroadcastService } from './services/broadcastService';
import { SystemService } from './services/systemService';
import { ConfigService } from './services/configService';
import { PlatformConfigController } from './controllers/platformConfigController';
import { AutoReplyController } from './controllers/autoReplyController';
import { AutoReplyController } from './controllers/keywordReplyController';
const sessionController = new SessionController();
const configController = new ConfigController();
@@ -93,7 +93,6 @@ class BKServer {
console.log('Client connected registerHandlers');
this.messageService.registerHandlers(socket);
this.sessionService.registerHandlers(socket);
this.configService.registerHandlers(socket);
this.broadcastService.registerHandlers(socket);
socket.on('disconnect', () => {
+71
View File
@@ -0,0 +1,71 @@
export const ALL_PLATFORMS = [
{
id: 'bilibili',
name: 'bilibili',
},
{
id: 'douyin',
name: '抖音',
},
{
id: 'douyin_mp',
name: '抖音企业号',
},
{
id: 'win_jinmai',
name: '京卖',
},
{
id: 'jinritemai',
name: '抖店',
},
{
id: 'win_qianniu',
name: '千牛',
},
{
id: 'win_wechat',
name: '微信',
},
{
id: 'win_wecom',
name: '企微(Bate版)',
},
{
id: 'weibo',
name: '微博私信',
},
{
id: 'xiaohongshu',
name: '小红书评论',
},
{
id: 'xiaohongshu_pro',
name: '小红书私信',
},
{
id: 'zhihu',
name: '知乎',
},
];
// # 固定会传递的上下文参数
// CTX_APP_NAME = "app_name"
// CTX_APP_ID = "app_id"
// CTX_INSTANCE_ID = "instance_id"
// CTX_USERNAME = "username"
// CTX_PLATFORM = "platform"
// CTX_HAS_NEW_MESSAGE = "has_new_message"
// CTX_HAS_GROUP_MESSAGE = "has_group_message"
// # 千牛平台特有的上下文参数
// CTX_CURRENT_GOODS = "CTX_CURRENT_GOODS" # 当前商品
// CTX_CURRENT_GOODS_ID = "CTX_CURRENT_GOODS_ID" # 当前商品 ID
// CTX_MEMBER_TAG = "CTX_MEMBER_TAG" # 会员标签
// CTX_FAN_TAG = "CTX_FAN_TAG" # 粉丝标签
// CTX_NEW_CUSTOMER_TAG = "CTX_NEW_CUSTOMER_TAG" # 新客标签
export const CTX_APP_NAME = 'app_name';
export const CTX_APP_ID = 'app_id';
export const CTX_INSTANCE_ID = 'instance_id';
@@ -1,44 +1,37 @@
import { Config } from '../entities/config';
import { Plugin } from '../entities/plugin';
export class ConfigController {
async getConfig(): Promise<Config> {
try {
const config = await Config.findByPk(1);
if (!config) {
throw new Error('Config not found');
}
return config;
} catch (error) {
const newConfig = await Config.create({
extract_phone: true,
extract_product: true,
save_path: '',
reply_speed: 0,
reply_random_speed: 0,
default_reply: '',
wait_humans_time: 60,
context_count: 1,
gpt_base_url: 'https://api.openai.com/v1',
gpt_key: 'your-key',
gpt_model: 'gpt-3.5-turbo',
gpt_temperature: 0.7,
gpt_top_p: 0.9,
stream: true,
use_lazy: false,
lazy_key: 'your-key',
});
return newConfig;
}
async getByAppId(appId: string): Promise<Config | null> {
const config = await Config.findOne({
where: { platform_id: appId },
});
return config;
}
async updateConfig(id: number, configData: Partial<Config>): Promise<Config> {
const config = await Config.findByPk(id);
async getByInstanceId(instanceId: string): Promise<Config | null> {
const config = await Config.findOne({
where: { instance_id: instanceId },
});
return config;
}
async getGlobalConfig(): Promise<Config> {
let config = await Config.findOne({
where: { global: true },
});
if (!config) {
throw new Error('Config not found');
config = await Config.create({
global: true,
});
}
const data = await config.update(configData);
return data;
return config;
}
async getPluginConfig(pluginId: number): Promise<Plugin | null> {
const plugin = await Plugin.findByPk(pluginId);
return plugin;
}
}
@@ -1,27 +0,0 @@
import { GlobalParam } from '../entities/globalParam';
export class GlobalParamController {
async create(globalParamData: any) {
return GlobalParam.create(globalParamData);
}
async update(id: number, globalParamData: any) {
const globalParam = await GlobalParam.findByPk(id);
if (!globalParam) {
throw new Error('GlobalParam not found');
}
return globalParam.update(globalParamData);
}
async delete(id: number) {
const globalParam = await GlobalParam.findByPk(id);
if (!globalParam) {
throw new Error('GlobalParam not found');
}
return globalParam.destroy();
}
async list() {
return GlobalParam.findAll();
}
}
@@ -1,17 +1,17 @@
import ExcelJS from 'exceljs';
import fs from 'fs';
import { Op } from 'sequelize';
import { AutoReply } from '../entities/autoReply';
import { Keyword } from '../entities/keyword';
import { ALL_PLATFORMS } from '../constants';
import { getTempPath } from '../../utils';
export class AutoReplyController {
async create(autoReplyData: any) {
return AutoReply.create(autoReplyData);
return Keyword.create(autoReplyData);
}
async update(id: number, autoReplyData: any) {
const autoReply = await AutoReply.findByPk(id);
const autoReply = await Keyword.findByPk(id);
if (!autoReply) {
throw new Error('AutoReply not found');
}
@@ -19,7 +19,7 @@ export class AutoReplyController {
}
async delete(id: number) {
const autoReply = await AutoReply.findByPk(id);
const autoReply = await Keyword.findByPk(id);
if (!autoReply) {
throw new Error('AutoReply not found');
}
@@ -68,22 +68,22 @@ export class AutoReplyController {
}
});
const originalAutoReplies = await AutoReply.findAll();
const originalAutoReplies = await Keyword.findAll();
try {
// 先删除所有数据
await AutoReply.destroy({ where: {} });
await AutoReply.bulkCreate(autoReplies);
await Keyword.destroy({ where: {} });
await Keyword.bulkCreate(autoReplies);
} catch (error) {
// 如果插入失败,回滚数据
// @ts-ignore
await AutoReply.bulkCreate(originalAutoReplies);
await Keyword.bulkCreate(originalAutoReplies);
throw error;
}
}
async exportExcel() {
const autoReplies = await AutoReply.findAll();
const autoReplies = await Keyword.findAll();
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('自动回复');
@@ -144,13 +144,13 @@ export class AutoReplyController {
}
async getKeywords(platformId: string) {
const autoReplies = await AutoReply.findAll({
const autoReplies = await Keyword.findAll({
where: {
platform_id: platformId,
},
});
const globalKeywords = await AutoReply.findAll({
const globalKeywords = await Keyword.findAll({
where: {
platform_id: {
[Op.or]: [null, ''],
@@ -172,7 +172,7 @@ export class AutoReplyController {
}) {
try {
const { rows: autoReplies, count: total } =
await AutoReply.findAndCountAll({
await Keyword.findAndCountAll({
where: platformId
? {
platform_id: platformId,
@@ -1,68 +0,0 @@
import { PlatformConfig } from '../entities/platformConfig';
export class PlatformConfigController {
async updateByPlatformId(
platformId: string,
settings: any,
): Promise<PlatformConfig> {
const platformConfig = await PlatformConfig.findOne({
where: { platform_id: platformId },
});
const active = settings && settings.active ? settings.active : false;
if (platformConfig) {
// @ts-ignore
return this.update(platformConfig.id, { settings, active });
}
return this.create({
platform_id: platformId,
settings,
active,
});
}
async getByPlatformId(platformId: string) {
let data = await PlatformConfig.findOne({
where: { platform_id: platformId },
});
if (!data) {
// 创建一个对象
data = await PlatformConfig.create({
platform_id: platformId,
active: false,
settings: {},
});
}
return data;
}
async create(platformConfigData: any): Promise<PlatformConfig> {
const platformConfig = await PlatformConfig.create(platformConfigData);
return platformConfig;
}
async update(id: number, platformConfigData: Partial<PlatformConfig>) {
const platformConfig = await PlatformConfig.findOne({
where: { id },
});
if (!platformConfig) {
return;
}
await platformConfig.update(platformConfigData);
}
async delete(id: number): Promise<void> {
const platformConfig = await PlatformConfig.findOne({
where: { id },
});
if (!platformConfig) {
return;
}
await platformConfig.destroy();
}
}
@@ -17,9 +17,22 @@ export class SessionCreate {
}
export class SessionController {
async create(sessionData: SessionCreate) {
// @ts-ignore
return Session.create(sessionData);
async createSession(
platformId: string,
platformName: string,
username?: string,
goodsName?: string,
goodsAvatar?: string,
): Promise<Session> {
return Session.create({
platform_id: platformId,
platform: platformName,
username: username || '',
last_active: new Date(),
goods_name: goodsName || '',
goods_avatar: goodsAvatar || '',
created_at: new Date(),
});
}
async update(id: number, sessionData: Session) {
+51 -65
View File
@@ -4,6 +4,20 @@ import { DataTypes, Model, Sequelize } from 'sequelize';
export class Config extends Model {
declare id: number;
declare global: boolean;
declare active: boolean;
declare platform: string;
declare platform_id: string;
declare instance_id: string;
declare use_plugin: boolean;
declare plugin_id: number;
declare extract_phone: boolean;
declare extract_product: boolean;
@@ -24,48 +38,7 @@ export class Config extends Model {
declare gpt_key: string;
declare use_dify: boolean;
declare gpt_model: string;
declare gpt_temperature: number;
declare gpt_top_p: number;
declare stream: boolean;
}
export async function checkAndAddFields(sequelize: Sequelize) {
const tableDescription = await Config.describe();
// @ts-ignore
if (!tableDescription.reply_random_speed) {
await sequelize
.getQueryInterface()
.addColumn('Config', 'reply_random_speed', {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
});
}
// @ts-ignore
if (!tableDescription.context_count) {
await sequelize.getQueryInterface().addColumn('Config', 'context_count', {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
});
}
// @ts-ignore
if (!tableDescription.default_reply) {
await sequelize.getQueryInterface().addColumn('Config', 'default_reply', {
type: DataTypes.STRING,
allowNull: true,
defaultValue: '',
});
}
declare llm_type: string;
}
export function initConfig(sequelize: Sequelize) {
@@ -76,6 +49,37 @@ export function initConfig(sequelize: Sequelize) {
autoIncrement: true,
primaryKey: true,
},
global: {
type: DataTypes.BOOLEAN,
defaultValue: false,
allowNull: true,
},
active: {
type: DataTypes.BOOLEAN,
defaultValue: false,
allowNull: true,
},
platform: {
type: DataTypes.STRING(255),
allowNull: true,
},
platform_id: {
type: DataTypes.STRING(255),
allowNull: true,
},
instance_id: {
type: DataTypes.STRING(255),
allowNull: true,
},
use_plugin: {
type: DataTypes.BOOLEAN,
defaultValue: false,
allowNull: true,
},
plugin_id: {
type: DataTypes.INTEGER,
allowNull: true,
},
extract_phone: {
type: DataTypes.BOOLEAN,
defaultValue: false,
@@ -110,43 +114,25 @@ export function initConfig(sequelize: Sequelize) {
type: DataTypes.FLOAT,
allowNull: true,
},
gpt_base_url: {
base_url: {
type: DataTypes.STRING,
allowNull: true,
},
gpt_key: {
key: {
type: DataTypes.STRING,
allowNull: true,
},
use_dify: {
type: DataTypes.BOOLEAN,
defaultValue: false,
allowNull: true,
},
gpt_model: {
llm_type: {
type: DataTypes.STRING,
allowNull: true,
},
gpt_temperature: {
type: DataTypes.FLOAT,
allowNull: true,
},
gpt_top_p: {
type: DataTypes.FLOAT,
allowNull: true,
},
stream: {
type: DataTypes.BOOLEAN,
allowNull: true,
defaultValue: 'chatgpt',
},
},
{
sequelize,
modelName: 'Config',
tableName: 'config',
tableName: 'n_config',
timestamps: false,
},
);
checkAndAddFields(sequelize);
}
-36
View File
@@ -1,36 +0,0 @@
import { DataTypes, Model, Sequelize } from 'sequelize';
// Extend the Model class with the attributes interface
export class GlobalParam extends Model {
declare id: number;
declare key: string;
declare value: string;
}
export function initGlobalParam(sequelize: Sequelize) {
GlobalParam.init(
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
key: {
type: DataTypes.STRING(255),
allowNull: false,
},
value: {
type: DataTypes.TEXT,
allowNull: false,
},
},
{
sequelize,
modelName: 'GlobalParam',
tableName: 'global_params',
timestamps: false,
},
);
}
@@ -1,7 +1,7 @@
import { DataTypes, Model, Sequelize } from 'sequelize';
// Extend the Model class with the attributes interface
export class AutoReply extends Model {
export class Keyword extends Model {
declare id: number; // Note that the `null assertion` `!` is required in strict mode.
declare keyword: string;
@@ -13,8 +13,8 @@ export class AutoReply extends Model {
declare platform_id: string;
}
export function initAutoReply(sequelize: Sequelize) {
AutoReply.init(
export function initKeyword(sequelize: Sequelize) {
Keyword.init(
{
id: {
type: DataTypes.INTEGER,
@@ -40,8 +40,8 @@ export function initAutoReply(sequelize: Sequelize) {
},
{
sequelize,
modelName: 'AutoReply',
tableName: 'auto_reply',
modelName: 'Keyword',
tableName: 'keyword',
timestamps: false,
},
);
+9 -9
View File
@@ -10,7 +10,7 @@ export class Message extends Model {
declare content: string;
declare unique: string;
declare sender: string;
declare msg_type: string;
@@ -31,19 +31,19 @@ export function initMessage(sequelize: Sequelize) {
},
role: {
type: DataTypes.STRING(100),
allowNull: false,
allowNull: true,
},
sender: {
type: DataTypes.STRING(100),
allowNull: true,
},
content: {
type: DataTypes.TEXT,
allowNull: false,
},
unique: {
type: DataTypes.STRING(255),
allowNull: false,
allowNull: true,
},
msg_type: {
type: DataTypes.STRING(55),
allowNull: false,
allowNull: true,
},
created_at: {
type: DataTypes.DATE,
@@ -53,7 +53,7 @@ export function initMessage(sequelize: Sequelize) {
{
sequelize,
modelName: 'Message',
tableName: 'messages',
tableName: 'n_messages',
timestamps: false,
},
);
@@ -1,42 +0,0 @@
import { DataTypes, Model, Sequelize } from 'sequelize';
// Extend the Model class with the attributes interface
export class PlatformConfig extends Model {
declare id: number;
declare platform_id: string;
declare settings: any;
declare active: boolean;
}
export function initPlatformConfig(sequelize: Sequelize) {
PlatformConfig.init(
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
platform_id: {
type: DataTypes.STRING(255),
allowNull: false,
},
settings: {
type: DataTypes.JSON,
allowNull: false,
},
active: {
type: DataTypes.BOOLEAN,
defaultValue: false,
},
},
{
sequelize,
modelName: 'PlatformConfig',
tableName: 'platform_cfg', // 创建一个新的表,用于存储 JSON 格式的配置
timestamps: false,
},
);
}
+65
View File
@@ -0,0 +1,65 @@
import { DataTypes, Model, Sequelize } from 'sequelize';
export class Plugin extends Model {
declare id: number;
declare name: string;
declare enabled: boolean;
declare code: string;
declare platform: string;
declare platform_id: string;
declare instance_id: string; // 可能是作用于单个实例的插件
declare created_at: Date;
}
export function initPlugin(sequelize: Sequelize) {
Plugin.init(
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
name: {
type: DataTypes.STRING(255),
allowNull: true,
},
enabled: {
type: DataTypes.BOOLEAN,
allowNull: true,
},
code: {
type: DataTypes.TEXT,
allowNull: true,
},
platform: {
type: DataTypes.STRING(255),
allowNull: true,
},
platform_id: {
type: DataTypes.STRING(255),
allowNull: true,
},
instance_id: {
type: DataTypes.STRING(255),
allowNull: true,
},
created_at: {
type: DataTypes.DATE,
allowNull: true,
},
},
{
sequelize,
modelName: 'Plugin',
tableName: 'plugins',
timestamps: false,
},
);
}
+5 -21
View File
@@ -3,17 +3,13 @@ import { DataTypes, Model, Sequelize } from 'sequelize';
export class Session extends Model {
declare id: number;
declare username: string;
declare last_active: Date;
declare platform: string;
declare platform_id: string;
declare goods_name: string;
declare instance_id: string; // 可能是作用于单个实例的插件
declare goods_avatar: string;
declare context: string;
declare created_at: Date;
}
@@ -26,14 +22,6 @@ export function initSession(sequelize: Sequelize) {
autoIncrement: true,
primaryKey: true,
},
username: {
type: DataTypes.STRING(255),
allowNull: false,
},
last_active: {
type: DataTypes.DATE,
allowNull: true,
},
platform: {
type: DataTypes.STRING(255),
allowNull: true,
@@ -42,12 +30,8 @@ export function initSession(sequelize: Sequelize) {
type: DataTypes.STRING(255),
allowNull: true,
},
goods_name: {
type: DataTypes.STRING(255),
allowNull: true,
},
goods_avatar: {
type: DataTypes.STRING(255),
context: {
type: DataTypes.JSON,
allowNull: true,
},
created_at: {
@@ -58,7 +42,7 @@ export function initSession(sequelize: Sequelize) {
{
sequelize,
modelName: 'Session',
tableName: 'sessions',
tableName: 'n_sessions',
timestamps: false,
},
);
+6 -8
View File
@@ -6,11 +6,10 @@ import * as path from 'path';
import sqlite from 'sqlite3';
import { Sequelize } from 'sequelize';
import { initConfig } from './entities/config';
import { initPlatformConfig } from './entities/platformConfig';
import { initSession } from './entities/session';
import { initMessage } from './entities/message';
import { AutoReply, initAutoReply } from './entities/autoReply';
import { initGlobalParam } from './entities/globalParam';
import { initPlugin } from './entities/plugin';
import { Keyword, initKeyword } from './entities/keyword';
// Get user's documents directory path
const DOCUMENTS_DIR = path.join(os.homedir(), 'Documents');
@@ -35,15 +34,14 @@ const sequelize = new Sequelize({
// 初始化模型
initConfig(sequelize);
initPlatformConfig(sequelize);
initSession(sequelize);
initMessage(sequelize);
initAutoReply(sequelize);
initGlobalParam(sequelize);
initKeyword(sequelize);
initPlugin(sequelize);
// 异步初始化和数据填充函数
async function initDb(): Promise<void> {
const count = await AutoReply.count();
const count = await Keyword.count();
if (count === 0) {
const replies = [
{
@@ -153,7 +151,7 @@ async function initDb(): Promise<void> {
},
];
await AutoReply.bulkCreate(replies);
await Keyword.bulkCreate(replies);
}
}
@@ -1,25 +0,0 @@
import socketIo from 'socket.io';
import { BrowserWindow } from 'electron';
export class BroadcastService {
private mainWindow: BrowserWindow;
constructor(mainWindow: BrowserWindow) {
this.mainWindow = mainWindow;
}
public registerHandlers(socket: socketIo.Socket): void {
socket.on('broadcastService-sendBroadcast', (msg: any, callback) => {
const { event_id: eventId, message } = msg;
this.receiveBroadcast(msg);
callback({
event_id: eventId,
event_type: message,
});
});
}
public receiveBroadcast(msg: any): void {
this.mainWindow.webContents.send('broadcast', msg);
}
}
+14 -52
View File
@@ -1,67 +1,29 @@
import socketIo from 'socket.io';
import { ConfigController } from '../controllers/configController';
import { PlatformConfigController } from '../controllers/platformConfigController';
import { Config } from '../entities/config';
import { CTX_APP_ID, CTX_INSTANCE_ID } from '../constants';
export class ConfigService {
private configController: ConfigController;
private platformConfigController: PlatformConfigController;
constructor(
configController: ConfigController,
platformConfigController: PlatformConfigController,
) {
constructor(configController: ConfigController) {
this.configController = configController;
this.platformConfigController = platformConfigController;
}
public registerHandlers(socket: socketIo.Socket): void {
socket.on('configService-getConfig', async (arg, callback) => {
try {
const config = await this.getConfig();
callback(config);
} catch (error) {
console.error('Failed to get config', error);
callback(null);
}
});
}
public async get(ctx: any): Promise<Config> {
const appId = ctx.get(CTX_APP_ID);
const instanceId = ctx.get(CTX_INSTANCE_ID);
public async getConfigByPlatformId(platformId: string): Promise<Config> {
const platformConfig =
await this.platformConfigController.getByPlatformId(platformId);
const { settings } = platformConfig;
const settingsVal =
typeof settings === 'string' ? JSON.parse(settings) : settings;
if (!settingsVal || !settingsVal.active) {
return this.configController.getConfig();
const instanceConfig =
await this.configController.getByInstanceId(instanceId);
if (instanceConfig) {
return instanceConfig;
}
const config = await this.configController.getConfig();
const config = await this.configController.getByAppId(appId);
if (config) {
return config;
}
return {
id: config.id,
extract_phone: config.extract_phone,
extract_product: config.extract_product,
save_path: config.save_path,
reply_speed: config.reply_speed,
reply_random_speed: config.reply_random_speed,
wait_humans_time: config.wait_humans_time,
context_count: settingsVal.contextCount ?? config.context_count,
gpt_base_url: settingsVal.proxyAddress ?? config.gpt_base_url,
gpt_key: settingsVal.apiKey ?? config.gpt_key,
use_dify: settingsVal.useDify ?? config.use_dify,
gpt_model: settingsVal.model ?? config.gpt_model,
default_reply: settingsVal.defaultReply ?? config.default_reply,
gpt_temperature: config.gpt_temperature,
gpt_top_p: config.gpt_top_p,
stream: config.stream,
} as any;
}
public async getConfig(): Promise<Config> {
return this.configController.getConfig();
return this.configController.getGlobalConfig();
}
}
@@ -0,0 +1,80 @@
import socketIo from 'socket.io';
import { BrowserWindow } from 'electron';
import { Platform, StrategyServiceStatusEnum } from '../types';
import { emitAndWait } from '../../utils';
export class DispatchService {
private mainWindow: BrowserWindow;
private io: socketIo.Server;
constructor(mainWindow: BrowserWindow, io: socketIo.Server) {
this.mainWindow = mainWindow;
this.io = io;
}
public registerHandlers(socket: socketIo.Socket): void {
socket.on('messageService-broadcast', (msg: any, callback) => {
const { event_id: eventId, message } = msg;
this.receiveBroadcast(msg);
callback({
event_id: eventId,
event_type: message,
});
});
socket.on('messageService-getMessages', async (data, callback) => {
const { ctx, messages } = data;
});
}
public receiveBroadcast(msg: any): void {
this.mainWindow.webContents.send('broadcast', msg);
}
public async checkHealth(): Promise<boolean> {
try {
return await this.io.timeout(5000).emitWithAck('systemService-health');
} catch (error) {
console.error('Failed to check health', error);
return false;
}
}
public async updateStatus(status: StrategyServiceStatusEnum): Promise<any> {
try {
return await emitAndWait(this.io, 'strategyService-updateStatus', {
status,
});
} catch (error) {
console.error('Failed to update status', error);
return null;
}
}
public async getAllPlatforms(): Promise<Platform[]> {
const maxRetries = 10;
let attempt = 0;
while (attempt < maxRetries) {
try {
// eslint-disable-next-line no-await-in-loop
const data = await emitAndWait<Platform[]>(
this.io,
'strategyService-getAppsInfo',
);
return data;
} catch (error) {
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => setTimeout(resolve, 1000));
attempt++;
console.error(`Attempt ${attempt} failed to update strategies`, error);
if (attempt >= maxRetries) {
return [];
}
}
}
return [];
}
}
File diff suppressed because it is too large Load Diff
+126
View File
@@ -0,0 +1,126 @@
import * as vm from 'vm';
// 静态导入预加载模块
import * as fs from 'fs';
import * as path from 'path';
import axios from 'axios';
import { ConfigController } from '../controllers/configController';
import { MessageDTO, ReplyDTO } from '../types';
interface PreloadedModules {
[key: string]: any;
}
// 创建一个包含所有预加载模块的对象
const preloadedModules: PreloadedModules = {
fs,
path,
axios,
};
class PluginSystem {
private configController: ConfigController;
constructor(configController: ConfigController) {
this.configController = configController;
}
/**
* 执行插件函数
* @param plugin_id 插件的ID
* @param ctx 上下文信息
* @param messages 消息数组
* @returns 插件函数的执行结果
*/
async executePlugin(
plugin_id: number,
ctx: any,
messages: MessageDTO[],
): Promise<ReplyDTO> {
// 从配置控制器中获取插件配置
const plugin = await this.configController.getPluginConfig(plugin_id);
if (!plugin) {
throw new Error('Plugin not found');
}
try {
// 创建沙盒环境
const sandbox = {
module: {} as any,
exports: {},
require: (module: string) => {
// 只允许加载预加载的模块
if (preloadedModules[module]) {
return preloadedModules[module];
}
throw new Error(`Module ${module} is not available`);
},
};
// =================== 插件代码示例 ===================
// module.exports = function(ctx, messages) {
// do something...
// return {
// content: 'Hello, world!',
// type: 'TEXT',
// };
// };
// =================== HTTP请求示例 ===================
// module.exports = async function(ctx, messages) {
// const response = await axios.get('https://api.example.com');
// return {
// content: response.data,
// type: 'TEXT',
// };
// };
// =================== 文件读取示例 ===================
// module.exports = function(ctx, messages) {
// const content = fs.readFileSync('example.txt', 'utf8');
// return {
// content,
// type: 'TEXT',
// };
// };
// ===================================================
// 插件代码包装模板,确保插件函数存在并导出
const pluginCode = `
const pluginFunction = ${plugin.code};
if (typeof pluginFunction !== 'function') {
throw new Error('Plugin function is not defined or not a function');
}
module.exports = pluginFunction;
`;
// 创建并运行沙盒上下文
vm.createContext(sandbox);
vm.runInContext(pluginCode, sandbox);
// 检查导出的是否为函数
if (typeof sandbox.module.exports !== 'function') {
throw new Error('Plugin does not export a function');
}
// 执行插件函数并传递 ctx 和 messages
const data = sandbox.module.exports(ctx, messages);
// data 的返回类型应该是 ReplyDTO,这里做个数据校验
if (
data &&
typeof data === 'object' &&
'content' in data &&
'type' in data
) {
return data as ReplyDTO;
}
throw new Error('Plugin function did not return a valid response');
} catch (error) {
// 捕获并返回错误信息
console.error('Plugin execution error:', error);
throw error;
}
}
}
export default PluginSystem;
-101
View File
@@ -1,101 +0,0 @@
import socketIo from 'socket.io';
import { SessionController } from '../controllers/sessionController';
import { Session } from '../entities/session';
import { StrategyService } from './strategyService';
export class SessionService {
private sessionController: SessionController;
private strategyService: StrategyService;
constructor(
sessionController: SessionController,
strategyService: StrategyService,
) {
this.sessionController = sessionController;
this.strategyService = strategyService;
}
public registerHandlers(socket: socketIo.Socket): void {
socket.on('sessionService-getSession', async (data: any, callback) => {
const { platformId, username, goodsName, goodsAvatar } = data;
const ptf = await this.strategyService.getPlatformInfoById(platformId);
const startTime = new Date().getTime();
try {
if (!username) {
// 直接新建一个 session
const session = await this.createSession(
platformId,
ptf?.name || '',
goodsName,
goodsAvatar,
);
callback(session);
return;
}
const session = await this.getSessionOrCreate(
platformId,
username,
goodsName,
goodsAvatar,
);
callback(session);
} catch (error) {
console.error('Failed to get session', error);
callback(null);
} finally {
console.log('getSession time:', new Date().getTime() - startTime, 'ms');
}
});
}
public async createSession(
platformId: string,
platformName: string,
goodsName?: string,
goodsAvatar?: string,
): Promise<Session> {
return this.sessionController.create({
platform_id: platformId,
platform: platformName,
username: '',
last_active: new Date(),
goods_name: goodsName || '',
goods_avatar: goodsAvatar || '',
created_at: new Date(),
});
}
public async getSessionOrCreate(
platformId: string,
username: string,
goodsName?: string,
goodsAvatar?: string,
): Promise<Session> {
const sessions = await this.sessionController.search(platformId, username);
if (sessions.length > 0) {
const session = sessions[0];
session.last_active = new Date();
if (goodsName && goodsAvatar) {
session.goods_name = goodsName;
session.goods_avatar = goodsAvatar;
}
return this.sessionController.update(session.id, session);
}
const ptf = await this.strategyService.getPlatformInfoById(platformId);
return this.sessionController.create({
platform_id: platformId,
platform: ptf?.name || '',
username,
last_active: new Date(),
goods_name: goodsName || '',
goods_avatar: goodsAvatar || '',
created_at: new Date(),
});
}
}
@@ -1,82 +0,0 @@
import socketIo from 'socket.io';
import { Platform, StrategyServiceStatusEnum } from '../types';
export class StrategyService {
private io: socketIo.Server;
constructor(io: socketIo.Server) {
this.io = io;
}
async emitAndWait<T>(
event: string,
data?: any,
timeout: number = 5000,
): Promise<T> {
let response;
if (data === undefined) {
response = await this.io.timeout(timeout).emitWithAck(event);
} else {
response = await this.io.timeout(timeout).emitWithAck(event, data);
}
// 判断是否是 Array
if (Array.isArray(response) && response.length === 1) {
if (typeof response[0] === 'undefined') {
return {} as T;
}
if (response[0] === 'string') {
return {} as T;
}
// 如果 response[0] 也是数组,那么直接返回
if (Array.isArray(response[0])) {
return response[0] as T;
}
return JSON.parse(response[0]);
}
// 判断是否是字符串
if (typeof response === 'string') {
return JSON.parse(response);
}
return response;
}
async updateStatus(status: StrategyServiceStatusEnum): Promise<any> {
try {
return await this.emitAndWait('strategyService-updateStatus', { status });
} catch (error) {
console.error('Failed to update status', error);
return null;
}
}
public async getAllPlatforms(): Promise<Platform[]> {
const maxRetries = 10;
let attempt = 0;
while (attempt < maxRetries) {
try {
// eslint-disable-next-line no-await-in-loop
const data = await this.emitAndWait<Platform[]>(
'strategyService-getAppsInfo',
);
return data;
} catch (error) {
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => setTimeout(resolve, 1000));
attempt++;
console.error(`Attempt ${attempt} failed to update strategies`, error);
if (attempt >= maxRetries) {
return [];
}
}
}
return [];
}
}
@@ -1,18 +0,0 @@
import socketIo from 'socket.io';
export class SystemService {
private io: socketIo.Server;
constructor(io: socketIo.Server) {
this.io = io;
}
async checkHealth(): Promise<boolean> {
try {
return await this.io.timeout(5000).emitWithAck('systemService-health');
} catch (error) {
console.error('Failed to check health', error);
return false;
}
}
}
+6 -14
View File
@@ -12,27 +12,19 @@ export enum PlatformTypeEnum {
ME_MEDIA = 'ME_MEDIA',
}
export type RoleType = 'assistant' | 'user' | 'system';
export type MessageType =
| 'text'
| 'image'
| 'video'
| 'file'
| 'mention'
| 'goods';
export type RoleType = 'SELF' | 'OTHER' | 'SYSTEM';
export type MessageType = 'TEXT' | 'IMAGE' | 'VIDEO' | 'FILE';
export interface MessageDTO {
session_id: number;
platform_id: string;
unique: string;
content: string; // If it's an image, this is the URL of the image
sender: string;
content: string;
role: RoleType; // assistant, user
msg_type: MessageType;
type: MessageType;
}
export interface ReplyDTO {
content: string;
msg_type: MessageType;
type: MessageType;
}
export interface Platform {
+2
View File
@@ -0,0 +1,2 @@
fork from https://github.com/zhengxs2018/ai
+212
View File
@@ -0,0 +1,212 @@
import axios from 'axios';
import OpenAI from 'openai';
import { Stream } from 'openai/streaming';
import { APIResource } from '../../resource';
export class Completions extends APIResource {
create(
body: ChatCompletionCreateParamsNonStreaming,
options?: OpenAI.RequestOptions,
): Promise<OpenAI.ChatCompletion>;
create(
body: ChatCompletionCreateParamsStreaming,
options?: OpenAI.RequestOptions,
): Promise<Stream<OpenAI.ChatCompletionChunk>>;
async create(
params: ChatCompletionCreateParams,
options?: OpenAI.RequestOptions,
): Promise<OpenAI.ChatCompletion | Stream<OpenAI.ChatCompletionChunk>> {
const { stream } = params;
const body = this.buildCreateParams(params);
const path = '/chat-messages';
const response: Response = await this._client.post(path, {
...options,
body: body as unknown as Record<string, unknown>,
stream: false,
__binaryResponse: true,
});
if (stream) {
const controller = new AbortController();
options?.signal?.addEventListener('abort', () => {
controller.abort();
});
return this.afterSSEResponse(response, controller);
}
// if ('stream' in params && params.stream) {
// // Streaming request
// const stream = new Stream<OpenAI.ChatCompletionChunk>();
// try {
// const resp = await axios.post(
// `${gpt_base_url}/chat-messages`,
// {
// inputs: {},
// query: rest.query,
// response_mode: 'streaming',
// user: 'apiuser',
// auto_generate_name: false,
// },
// {
// headers: {
// 'Content-Type': 'application/json',
// Authorization: `Bearer ${gpt_key}`,
// },
// responseType: 'stream',
// },
// );
// let buffer = '';
// resp.data.on('data', (chunk: any) => {
// buffer += chunk.toString();
// const lines = buffer.split('\n');
// for (let i = 0; i < lines.length - 1; i++) {
// const line = lines[i].trim();
// if (line === '') continue;
// let chunkObj;
// try {
// const cleanedLine = line.replace(/^data: /, '').trim();
// if (cleanedLine.startsWith('{') && cleanedLine.endsWith('}')) {
// chunkObj = JSON.parse(cleanedLine);
// } else {
// continue;
// }
// } catch (error) {
// console.error('Error parsing JSON:', error);
// continue;
// }
// if (
// chunkObj.event === 'message' ||
// chunkObj.event === 'agent_message'
// ) {
// stream.push(chunkObj);
// }
// }
// buffer = lines[lines.length - 1];
// });
// resp.data.on('end', () => {
// stream.push(null);
// });
// resp.data.on('error', (error: Error) => {
// stream.emit('error', error);
// });
// } catch (error) {
// stream.emit('error', error);
// }
// return stream;
// }
// // Non-streaming request
// const response = await axios.post(
// `${gpt_base_url}/chat-messages`,
// {
// inputs: {},
// query: rest.query,
// response_mode: 'non-streaming',
// user: 'apiuser',
// auto_generate_name: false,
// },
// {
// headers: {
// 'Content-Type': 'application/json',
// Authorization: `Bearer ${gpt_key}`,
// },
// },
// );
return response.data as OpenAI.ChatCompletion;
}
protected buildCreateParams(params: ChatCompletionCreateParams) {
const { model, top_k, ...rest } = params;
return {
...rest,
model,
top_k,
response_mode: 'streaming',
};
}
protected afterSSEResponse(
response: Response,
controller: AbortController,
): Stream<OpenAI.ChatCompletionChunk> {
const stream = Stream.fromSSEResponse<DifyChat.GenerateContentResponse>(
response,
controller,
);
// response.body.on('data', (chunk: any) => {
// const chunkObj = JSON.parse(chunk.toString());
// if (chunkObj.event === 'message' || chunkObj.event === 'agent_message') {
// stream.push(chunkObj);
// }
// });
// response.body.on('end', () => {
// stream.push(null);
// });
// response.body.on('error', (error: Error) => {
// stream.emit('error', error);
// });
// return stream;
const toChoices = (chunk: DifyChat.GenerateContentResponse) => {};
}
}
export type ChatCompletionCreateParamsNonStreaming =
Chat.ChatCompletionCreateParamsNonStreaming;
export type ChatCompletionCreateParamsStreaming =
Chat.ChatCompletionCreateParamsStreaming;
export type ChatCompletionCreateParams = Chat.ChatCompletionCreateParams;
export namespace Chat {
// eslint-disable-next-line @typescript-eslint/no-shadow
export type ChatModel = (string & NonNullable<unknown>) | 'gemini-pro';
// 支持的有点问题
// | 'gemini-pro-vision';
// eslint-disable-next-line @typescript-eslint/no-shadow
export interface ChatCompletionCreateParamsNonStreaming
extends OpenAI.ChatCompletionCreateParamsNonStreaming {
model: ChatModel;
top_k?: number;
}
// eslint-disable-next-line @typescript-eslint/no-shadow
export interface ChatCompletionCreateParamsStreaming
extends OpenAI.ChatCompletionCreateParamsStreaming {
model: ChatModel;
top_k?: number | null;
}
// eslint-disable-next-line @typescript-eslint/no-shadow
export type ChatCompletionCreateParams =
| ChatCompletionCreateParamsNonStreaming
| ChatCompletionCreateParamsStreaming;
}
export namespace DifyChat {
export interface GenerateContentResponse {
event: 'message' | 'agent_message';
answer: string;
}
}
+7
View File
@@ -0,0 +1,7 @@
export {
type ChatModel,
type ChatCompletionCreateParams,
type ChatCompletionCreateParamsNonStreaming,
type ChatCompletionCreateParamsStreaming,
Completions,
} from './completions';
+64
View File
@@ -0,0 +1,64 @@
import type { Agent } from 'node:http';
import {
APIClient,
type DefaultQuery,
type Fetch,
type FinalRequestOptions,
type Headers,
} from 'openai/core';
import * as API from './resources';
export interface DifyAIOptions {
baseURL?: string;
apiKey?: string;
timeout?: number | undefined;
httpAgent?: Agent;
fetch?: Fetch | undefined;
defaultHeaders?: Headers;
defaultQuery?: DefaultQuery;
}
export class DifyAI extends APIClient {
protected apiKey: string;
private _options: DifyAIOptions;
constructor(options: DifyAIOptions = {}) {
const {
apiKey = process.env.DIFY_API_KEY || '',
baseURL = 'https://api.dify.ai/v1/',
timeout = 30000,
httpAgent = undefined,
...rest
} = options;
super({
baseURL,
timeout,
fetch,
httpAgent,
...rest,
});
this._options = options;
this.apiKey = apiKey;
}
chat = new API.Chat(this);
protected override defaultHeaders(opts: FinalRequestOptions): Headers {
return {
...super.defaultHeaders(opts),
...this._options.defaultHeaders,
};
}
protected override defaultQuery(): DefaultQuery | undefined {
return {
...this._options.defaultQuery,
key: this.apiKey,
};
}
}
+1
View File
@@ -0,0 +1 @@
export * from './chat/index';
+99
View File
@@ -0,0 +1,99 @@
import type { Agent } from 'node:http';
import {
APIClient,
type DefaultQuery,
type Fetch,
type FinalRequestOptions,
type Headers,
} from 'openai/core';
import * as API from './resources';
export interface ErnieAIOptions {
baseURL?: string;
apiKey?: string;
timeout?: number | undefined;
httpAgent?: Agent;
fetch?: Fetch | undefined;
/**
* Default headers to include with every request to the API.
*
* These can be removed in individual requests by explicitly setting the
* header to `undefined` or `null` in request options.
*/
defaultHeaders?: Headers;
/**
* Default query parameters to include with every request to the API.
*
* These can be removed in individual requests by explicitly setting the
* param to `undefined` in request options.
*/
defaultQuery?: DefaultQuery;
}
export class ErnieAI extends APIClient {
protected apiKey: string;
private _options: ErnieAIOptions;
constructor(options: ErnieAIOptions = {}) {
const {
apiKey = process.env.EB_API_KEY || '',
baseURL = 'https://aistudio.baidu.com/llm/lmapi/v1',
timeout = 30000,
httpAgent = undefined,
...rest
} = options;
super({
baseURL,
timeout,
fetch,
httpAgent,
...rest,
});
this._options = options;
this.apiKey = apiKey;
}
chat = new API.Chat(this);
embeddings = new API.Embeddings(this);
protected override authHeaders() {
return {
Authorization: `token ${this.apiKey}`,
};
}
protected override defaultHeaders(opts: FinalRequestOptions): Headers {
return {
...super.defaultHeaders(opts),
...this._options.defaultHeaders,
};
}
protected override defaultQuery(): DefaultQuery | undefined {
return this._options.defaultQuery;
}
}
// eslint-disable-next-line no-redeclare
export namespace ErnieAI {
export type Chat = API.Chat;
export type ChatModel = API.ChatModel;
export type ChatCompletionCreateParams = API.ChatCompletionCreateParams;
export type ChatCompletionCreateParamsNonStreaming =
API.ChatCompletionCreateParamsNonStreaming;
export type ChatCompletionCreateParamsStreaming =
API.ChatCompletionCreateParamsStreaming;
export type EmbeddingCreateParams = API.EmbeddingCreateParams;
}
export default ErnieAI;
@@ -0,0 +1,6 @@
import { APIResource } from '../../../resource';
import { Completions } from './completions';
export class Chat extends APIResource {
completions = new Completions(this._client);
}
@@ -0,0 +1,417 @@
import OpenAI, { APIError, OpenAIError } from 'openai';
import { Stream } from 'openai/streaming';
import { APIResource } from '../../../resource';
import { ensureArray } from '../../../util';
export class Completions extends APIResource {
protected endpoints: Record<ChatModel, string> = {
'ernie-bot': '/chat/completions',
'ernie-bot-turbo': '/chat/eb-instant',
'ernie-bot-4': '/chat/completions_pro',
'ernie-bot-8k': '/chat/ernie_bot_8k',
};
/**
* Creates a model response for the given chat conversation.
*
* 文心一言 由于分发在不同的平台,所以有不同的文档
* 百度云的响应和 OpenAI 的比较类似,但授权没有 AI Studio 方便
* 之前 AI Studio 的文档是有文档的,但现在不知道去哪了
* 参考:
* - https://cloud.baidu.com/doc/WENXINWORKSHOP/s/jlil56u11
* - https://github.com/PaddlePaddle/ERNIE-Bot-SDK/blob/develop/erniebot/backends/aistudio.py
*/
create(
body: ChatCompletionCreateParamsNonStreaming,
options?: OpenAI.RequestOptions,
): Promise<OpenAI.ChatCompletion>;
create(
body: ChatCompletionCreateParamsStreaming,
options?: OpenAI.RequestOptions,
): Promise<Stream<OpenAI.ChatCompletionChunk>>;
async create(
params: ChatCompletionCreateParams,
options?: OpenAI.RequestOptions,
) {
const { model = 'ernie-bot', ...body } =
Completions.buildCreateParams(params);
const endpoint = this.endpoints[model];
if (!endpoint) {
throw new OpenAIError(`Invalid model: ${model}`);
}
const { stream } = body;
const headers = {
...options?.headers,
// Note: 如果是 stream 的话,需要设置 Accept 为 text/event-stream
Accept: stream ? 'text/event-stream' : 'application/json',
};
const response: Response = await this._client.post(endpoint, {
...options,
body,
headers,
// 文心一言的响应内容被包裹了一层,需要解构并转换为 OpenAI 的格式
// 设置 __binaryResponse 为 true 是为了让 client 返回原始的 response
stream: false,
__binaryResponse: true,
});
if (stream) {
const controller = new AbortController();
options?.signal?.addEventListener('abort', () => {
controller.abort();
});
return Completions.fromOpenAIStream(
model,
Stream.fromSSEResponse(response, controller),
controller,
);
}
return Completions.fromResponse(model, await response.json());
}
static buildCreateParams(
params: ChatCompletionCreateParams,
): ChatCompletions.ChatCompletionCreateParams {
const { messages = [], presence_penalty, user, stop, ...rest } = params;
const head = messages[0];
// 文心一言的 system 是独立字段
// 1)长度限制1024个字符
// 2)如果使用functions参数,不支持设定人设system
const system = head && head.role === 'system' ? head.content : undefined;
// 移除 system 角色的消息
if (system) {
messages.splice(0, 1);
}
const data: ChatCompletions.ChatCompletionCreateParams = {
...rest,
messages,
};
if (system) {
data.system = system;
}
if (user) {
data.user_id = user;
}
if (presence_penalty) {
data.penalty_score = presence_penalty;
}
if (stop) {
data.stop = ensureArray(stop);
}
return data;
}
static fromResponse(
model: string,
data: ChatCompletions.APIResponse,
): OpenAI.ChatCompletion {
Completions.assert(data);
const { result } = data;
const choice: OpenAI.ChatCompletion.Choice = {
index: 0,
message: {
role: 'assistant',
content: result.result,
},
logprobs: null,
finish_reason: 'stop',
};
// TODO 需要确认 is_truncated 是否和 is_end 互斥
// TODO 需要确认 functions 是否响应式不一样
if (result.is_end) {
choice.finish_reason = 'stop';
} else if (result.is_truncated) {
choice.finish_reason = 'length';
} else if (result.need_clear_history) {
choice.finish_reason = 'content_filter';
}
return {
id: result.id,
model,
choices: [choice],
created: parseInt(result.created, 10),
object: 'chat.completion',
usage: result.usage,
};
}
static fromOpenAIStream(
model: string,
stream: Stream<ChatCompletions.APIResponse>,
controller: AbortController,
): Stream<OpenAI.ChatCompletionChunk> {
async function* iterator(): AsyncIterator<
OpenAI.ChatCompletionChunk,
any,
undefined
> {
// eslint-disable-next-line no-restricted-syntax
for await (const chunk of stream) {
Completions.assert(chunk);
// TODO 某些情况下,文心一言的 result 只有 id,需要排查情况
const data = chunk.result;
const choice: OpenAI.ChatCompletionChunk.Choice = {
index: 0,
delta: {
role: 'assistant',
content: data.result || '',
},
finish_reason: null,
};
// TODO 需要确认 is_truncated 是否和 is_end 互斥
// TODO 需要确认 functions 是否响应式不一样
if (data.is_end) {
choice.finish_reason = 'stop';
} else if (data.is_truncated) {
choice.finish_reason = 'length';
} else if (data.need_clear_history) {
choice.finish_reason = 'content_filter';
}
yield {
id: data.id,
model,
choices: [choice],
object: 'chat.completion.chunk',
created: parseInt(data.created, 10),
// openai-node 上 已经有讨论添加 usage 的问题
// 文心一言是有提供的,这里主要是为了向前兼容
// @ts-ignore
usage: data.usage,
};
}
}
return new Stream(iterator, controller);
}
/**
* 构建错误
*
* @param code -
* @param message -
* @returns 错误
*/
static makeAPIError(code: number, message: string) {
const error = { code, message };
switch (code) {
case 2:
return APIError.generate(500, error, message, {});
case 6: // permission error
case 111: // token expired
return APIError.generate(403, error, message, {});
case 17:
case 18:
case 19:
case 40407:
return APIError.generate(429, error, message, {});
case 110: // invalid token
case 40401: // invalid token
return APIError.generate(401, error, message, {});
case 336003: // invalid parameter
return APIError.generate(400, error, message, {});
case 336100: // try again
return APIError.generate(500, error, message, {});
default:
return APIError.generate(undefined, error, message, {});
}
}
/**
* 如果 code 不为 0,抛出 APIError
*
* @param code -
* @param message -
*/
static assert(resp: ChatCompletions.APIResponse) {
if (resp.errorCode === 0) return;
throw Completions.makeAPIError(resp.errorCode, resp.errorMsg);
}
}
export interface ChatCompletionCreateParamsNonStreaming
extends Pick<
OpenAI.ChatCompletionCreateParamsNonStreaming,
| 'messages'
| 'functions'
| 'temperature'
| 'top_p'
| 'presence_penalty'
| 'stream'
| 'stop'
| 'user'
> {
model: ChatModel;
disable_search?: boolean | null;
enable_citation?: boolean | null;
}
export interface ChatCompletionCreateParamsStreaming
extends Pick<
OpenAI.ChatCompletionCreateParamsStreaming,
| 'messages'
| 'functions'
| 'temperature'
| 'top_p'
| 'presence_penalty'
| 'stream'
| 'stop'
| 'user'
> {
model: ChatModel;
disable_search?: boolean | null;
enable_citation?: boolean | null;
}
export type ChatCompletionCreateParams =
| ChatCompletionCreateParamsNonStreaming
| ChatCompletionCreateParamsStreaming;
export type ChatModel =
| 'ernie-bot'
| 'ernie-bot-turbo'
| 'ernie-bot-4'
| 'ernie-bot-8k';
export namespace ChatCompletions {
// eslint-disable-next-line @typescript-eslint/no-shadow
export interface ChatCompletionCreateParams {
/**
* 模型名称
*/
model: ChatModel;
/**
* 是否强制关闭实时搜索功能,默认 false,表示不关闭
*
* @defaultValue false
*/
disable_search?: boolean | null;
/**
* 是否开启上角标返回,说明:
* (1)开启后,有概率触发搜索溯源信息search_infosearch_info内容见响应参数介绍
* (2)默认false,不开启
*
* @defaultValue false
*/
enable_citation?: boolean | null;
/**
* 模型人设,主要用于人设设定,例如,你是xxx公司制作的AI助手,说明:
* (1)长度限制1024个字符
* 2)如果使用 functions 参数,不支持设定人设 system
*
* @remarks OpenAI 是通过 messages 的 role 来区分的
*/
system?: string | null;
/**
* 聊天上下文信息
*
* @remarks 不支持 system 角色
*/
messages: OpenAI.ChatCompletionCreateParams['messages'];
/**
* 一个可触发函数的描述列表
*/
functions?: OpenAI.ChatCompletionCreateParams['functions'];
/**
* 内容随机性
*
* 说明:
* (1)较高的数值会使输出更加随机,而较低的数值会使其更加集中和确定
* 2)默认0.8,范围 (0, 1.0],不能为0
* (3)建议该参数和 top_p 只设置1个
* 4)建议 top_p 和 temperature 不要同时更改
*/
temperature?: number | null;
/**
* 生成文本的多样性
*
* 说明:
* (1)影响输出文本的多样性,取值越大,生成文本的多样性越强
* 2)默认0.8,取值范围 [0, 1.0]
* 3)建议该参数和 temperature 只设置1个
* 4)建议 top_p 和 temperature 不要同时更改
*/
top_p?: number | null;
/**
*
* 通过对已生成的token增加惩罚,减少重复生成的现象。说明:
* (1)值越大表示惩罚越大
* (2)默认1.0,取值范围:[1.0, 2.0]
*
* @remarks 在 OpenAI 中,参数名为 presence_penalty
*/
penalty_score?: number | null;
/**
* 是否以流式接口的形式返回数据,默认 false
*/
stream?: boolean | null;
/**
* 生成停止标识,当模型生成结果以stop中某个元素结尾时,停止文本生成。说明:
* (1)每个元素长度不超过20字符
* 2)最多4个元素
*/
stop?: string | string[] | undefined;
/**
* 表示最终用户的唯一标识符,可以监视和检测滥用行为,防止接口恶意调用
*
* @remarks OpenAI 中是通过 user 区分
*/
user_id?: string | undefined;
}
export type ChatCompletion = {
id: string;
result: string;
created: string;
is_end: boolean;
is_truncated: boolean;
need_clear_history: boolean;
usage: OpenAI.CompletionUsage;
};
export type APIResponse = {
errorCode: number;
errorMsg: string;
result: ChatCompletion;
};
}
@@ -0,0 +1,8 @@
export { Chat } from './chat';
export {
type ChatModel,
type ChatCompletionCreateParams,
type ChatCompletionCreateParamsNonStreaming,
type ChatCompletionCreateParamsStreaming,
Completions,
} from './completions';
@@ -0,0 +1,95 @@
import OpenAI, { APIError, OpenAIError } from 'openai';
import { type RequestOptions } from 'openai/core';
import { APIResource } from '../../resource';
export class Embeddings extends APIResource {
protected endpoints: Record<EmbeddingModel, string> = {
'ernie-text-embedding': '/embeddings/embedding-v1',
};
/**
* Creates an embedding vector representing the input text.
*
* See https://cloud.baidu.com/doc/WENXINWORKSHOP/s/alj562vvu
*/
async create(
params: EmbeddingCreateParams,
options?: RequestOptions,
): Promise<OpenAI.CreateEmbeddingResponse> {
const { model, user, input } = params;
const endpoint = this.endpoints[model];
if (!endpoint) {
throw new OpenAIError(`Invalid model: ${model}`);
}
const body = {
input,
user_id: user,
};
const response: Response = await this._client.post(endpoint, {
body,
...options,
__binaryResponse: true,
});
return Embeddings.fromResponse(model, await response.json());
}
static fromResponse(
model: EmbeddingModel,
data: CreateEmbeddingResponse,
): OpenAI.CreateEmbeddingResponse {
Embeddings.assert(data);
const { result } = data;
return {
data: result.data,
model,
object: 'list',
usage: result.usage,
};
}
/**
* 如果 code 不为 0,抛出 APIError
*
* @param code -
* @param message -
*/
static assert(resp: CreateEmbeddingResponse) {
if (resp.errorCode === 0) return;
const error = { code: resp.errorCode, message: resp.errorMsg };
throw APIError.generate(undefined, error, undefined, undefined);
}
}
export type EmbeddingModel = 'ernie-text-embedding';
export interface EmbeddingCreateParams {
/**
* 输入文本
*/
input: string | Array<string> | Array<number> | Array<Array<number>>;
/**
* 模型
*/
model: EmbeddingModel;
/**
* 用户 ID
*/
user?: string;
}
type CreateEmbeddingResponse = {
errorCode: number;
errorMsg: string;
result: OpenAI.CreateEmbeddingResponse;
};
@@ -0,0 +1,3 @@
export * from './chat/index';
export { Embeddings, type EmbeddingCreateParams } from './embeddings';
+46
View File
@@ -0,0 +1,46 @@
import { APIError } from 'openai';
/**
* 构建错误
*
* @param code -
* @param message -
* @returns 错误
*/
export function makeAPIError(code: number, message: string) {
const error = { code, message };
switch (code) {
case 2:
return APIError.generate(500, error, message, {});
case 6: // permission error
case 111: // token expired
return APIError.generate(403, error, message, {});
case 17:
case 18:
case 19:
case 40407:
return APIError.generate(429, error, message, {});
case 110: // invalid token
case 40401: // invalid token
return APIError.generate(401, error, message, {});
case 336003: // invalid parameter
return APIError.generate(400, error, message, {});
case 336100: // try again
return APIError.generate(500, error, message, {});
default:
return APIError.generate(undefined, error, message, {});
}
}
/**
* 如果 code 不为 0,抛出 APIError
*
* @param code -
* @param message -
*/
export function assertNonZero(code: number, message: string) {
if (code === 0) return;
throw makeAPIError(code, message);
}
+81
View File
@@ -0,0 +1,81 @@
import type { Agent } from 'node:http';
import {
APIClient,
type DefaultQuery,
type Fetch,
type FinalRequestOptions,
type Headers,
} from 'openai/core';
import * as API from './resources';
const BASE_URL = 'https://generativelanguage.googleapis.com/v1';
export interface GeminiAIOptions {
baseURL?: string;
apiKey?: string;
timeout?: number | undefined;
httpAgent?: Agent;
fetch?: Fetch | undefined;
defaultHeaders?: Headers;
defaultQuery?: DefaultQuery;
}
export class GeminiAI extends APIClient {
apiKey: string;
private _options: GeminiAIOptions;
constructor(options: GeminiAIOptions = {}) {
const {
apiKey = process.env.GEMINI_API_KEY || '',
baseURL = process.env.GEMINI_BASE_URL || BASE_URL,
timeout = 30000,
httpAgent = undefined,
...rest
} = options;
super({
baseURL,
timeout,
fetch,
httpAgent,
...rest,
});
this._options = options;
this.apiKey = apiKey;
}
chat = new API.Chat(this);
models = new API.Models(this);
protected override defaultHeaders(opts: FinalRequestOptions): Headers {
return {
...super.defaultHeaders(opts),
...this._options.defaultHeaders,
};
}
protected override defaultQuery(): DefaultQuery | undefined {
return {
...this._options.defaultQuery,
key: this.apiKey,
};
}
}
// eslint-disable-next-line no-redeclare
export namespace GeminiAI {
export type ChatModel = API.ChatModel;
export type ChatCompletionCreateParams = API.ChatCompletionCreateParams;
export type ChatCompletionCreateParamsStreaming =
API.ChatCompletionCreateParamsStreaming;
export type ChatCompletionCreateParamsNonStreaming =
API.ChatCompletionCreateParamsNonStreaming;
}
export default GeminiAI;
+9
View File
@@ -0,0 +1,9 @@
import type { GeminiAI } from './index';
export class APIResource {
protected _client: GeminiAI;
constructor(client: GeminiAI) {
this._client = client;
}
}
@@ -0,0 +1,6 @@
import { Completions } from './completions';
import { APIResource } from '../../resource';
export class Chat extends APIResource {
completions = new Completions(this._client);
}
@@ -0,0 +1,347 @@
import { randomUUID } from 'crypto';
import OpenAI from 'openai';
import { Stream } from 'openai/streaming';
import { ensureArray } from '../../../util';
import { APIResource } from '../../resource';
export class Completions extends APIResource {
/**
* Creates a model response for the given chat conversation.
*/
create(
body: ChatCompletionCreateParamsNonStreaming,
options?: OpenAI.RequestOptions,
): Promise<OpenAI.ChatCompletion>;
create(
body: ChatCompletionCreateParamsStreaming,
options?: OpenAI.RequestOptions,
): Promise<Stream<OpenAI.ChatCompletionChunk>>;
async create(
params: ChatCompletionCreateParams,
options?: OpenAI.RequestOptions,
): Promise<OpenAI.ChatCompletion | Stream<OpenAI.ChatCompletionChunk>> {
const { stream, model } = params;
const body = this.buildCreateParams(params);
const path = `/models/${model}:generateContent`;
const response: Response = await this._client.post(path, {
...options,
query: stream ? { alt: 'sse' } : {},
body: body as unknown as Record<string, unknown>,
stream: false,
__binaryResponse: true,
});
if (stream) {
const controller = new AbortController();
options?.signal?.addEventListener('abort', () => {
controller.abort();
});
return this.afterSSEResponse(model, response, controller);
}
return this.afterResponse(model, response);
}
protected buildCreateParams(params: ChatCompletionCreateParams) {
const {
messages = [],
max_tokens,
top_p,
top_k,
stop,
temperature,
} = params;
function formatContentParts(
content: string | OpenAI.ChatCompletionContentPart[],
) {
const parts: GeminiChat.Part[] = [];
if (typeof content === 'string') {
parts.push({ text: content });
return parts;
}
// eslint-disable-next-line no-restricted-syntax
for (const part of content) {
if (part.type === 'text') {
parts.push({ text: part.text });
} else {
// TODO: Handle images
// parts.push({
// inline_data: {
// "mime_type": "image/jpeg",
// "data": "'$(base64 -w0 image.jpg)'"
// }
// });
}
}
return parts;
}
function formatRole(role: string): 'user' | 'model' {
return role === 'user' ? 'user' : 'model';
}
const generationConfig: GeminiChat.GenerationConfig = {};
const data: GeminiChat.GenerateContentRequest = {
contents: messages.map((item) => {
return {
role: formatRole(item.role),
parts: formatContentParts(item.content!),
};
}),
generationConfig,
};
if (temperature != null) {
generationConfig.temperature = temperature;
}
if (top_k != null) {
generationConfig.topK = top_k;
}
if (top_p != null) {
generationConfig.topP = top_p;
}
if (stop != null) {
generationConfig.stopSequences = ensureArray(stop);
}
if (max_tokens != null) {
generationConfig.maxOutputTokens = max_tokens;
}
return data;
}
protected async afterResponse(
model: string,
response: Response,
): Promise<OpenAI.ChatCompletion> {
const data: GeminiChat.GenerateContentResponse = await response.json();
const choices: OpenAI.ChatCompletion.Choice[] = data.candidates!.map(
(item) => {
const [part] = item.content.parts;
const choice: OpenAI.ChatCompletion.Choice = {
index: item.index,
message: {
role: 'assistant',
content: part.text!,
},
logprobs: null,
finish_reason: 'stop',
};
switch (item.finishReason) {
case 'MAX_TOKENS':
choice.finish_reason = 'length';
break;
case 'SAFETY':
case 'RECITATION':
choice.finish_reason = 'content_filter';
break;
default:
choice.finish_reason = 'stop';
}
return choice;
},
);
return {
id: randomUUID(),
model,
choices,
object: 'chat.completion',
created: Date.now() / 10,
// TODO 需要支持 usage
usage: {
completion_tokens: 0,
prompt_tokens: 0,
total_tokens: 0,
},
};
}
protected afterSSEResponse(
model: string,
response: Response,
controller: AbortController,
): Stream<OpenAI.ChatCompletionChunk> {
const stream = Stream.fromSSEResponse<GeminiChat.GenerateContentResponse>(
response,
controller,
);
const toChoices = (data: GeminiChat.GenerateContentResponse) => {
return data.candidates!.map((item) => {
const [part] = item.content.parts;
const choice: OpenAI.ChatCompletionChunk.Choice = {
index: item.index,
delta: {
role: 'assistant',
content: part.text || '',
},
finish_reason: null,
};
switch (item.finishReason) {
case 'MAX_TOKENS':
choice.finish_reason = 'length';
break;
case 'SAFETY':
case 'RECITATION':
choice.finish_reason = 'content_filter';
break;
default:
choice.finish_reason = 'stop';
}
return choice;
});
};
async function* iterator(): AsyncIterator<
OpenAI.ChatCompletionChunk,
any,
undefined
> {
// eslint-disable-next-line no-restricted-syntax
for await (const chunk of stream) {
yield {
id: randomUUID(),
model,
choices: toChoices(chunk),
object: 'chat.completion.chunk',
created: Date.now() / 10,
};
}
}
return new Stream(iterator, controller);
}
}
export type ChatCompletionCreateParamsNonStreaming =
Chat.ChatCompletionCreateParamsNonStreaming;
export type ChatCompletionCreateParamsStreaming =
Chat.ChatCompletionCreateParamsStreaming;
export type ChatCompletionCreateParams = Chat.ChatCompletionCreateParams;
export type ChatModel = Chat.ChatModel;
export namespace Chat {
// eslint-disable-next-line @typescript-eslint/no-shadow
export type ChatModel = (string & NonNullable<unknown>) | 'gemini-pro';
// 支持的有点问题
// | 'gemini-pro-vision';
// eslint-disable-next-line @typescript-eslint/no-shadow
export interface ChatCompletionCreateParamsNonStreaming
extends OpenAI.ChatCompletionCreateParamsNonStreaming {
model: ChatModel;
top_k?: number;
}
// eslint-disable-next-line @typescript-eslint/no-shadow
export interface ChatCompletionCreateParamsStreaming
extends OpenAI.ChatCompletionCreateParamsStreaming {
model: ChatModel;
top_k?: number | null;
}
// eslint-disable-next-line @typescript-eslint/no-shadow
export type ChatCompletionCreateParams =
| ChatCompletionCreateParamsNonStreaming
| ChatCompletionCreateParamsStreaming;
}
namespace GeminiChat {
export interface GenerationConfig {
candidateCount?: number;
stopSequences?: string[];
maxOutputTokens?: number;
temperature?: number;
topP?: number;
topK?: number;
}
export interface GenerateContentCandidate {
index: number;
content: Content;
finishReason?:
| 'FINISH_REASON_UNSPECIFIED'
| 'STOP'
| 'MAX_TOKENS'
| 'SAFETY'
| 'RECITATION'
| 'OTHER';
finishMessage?: string;
citationMetadata?: CitationMetadata;
}
export interface GenerateContentResponse {
candidates?: GenerateContentCandidate[];
// promptFeedback?: PromptFeedback;
}
export interface CitationMetadata {
citationSources: CitationSource[];
}
export interface CitationSource {
startIndex?: number;
endIndex?: number;
uri?: string;
license?: string;
}
export interface InputContent {
parts: string | Array<string | Part>;
role: string;
}
export interface Content extends InputContent {
parts: Part[];
}
export type Part = TextPart | InlineDataPart;
export interface TextPart {
text: string;
inlineData?: never;
}
export interface InlineDataPart {
text?: never;
inlineData: GeminiContentBlob;
}
export interface GeminiContentBlob {
mimeType: string;
data: string;
}
export interface BaseParams {
generationConfig?: GenerationConfig;
}
export interface GenerateContentRequest extends BaseParams {
contents: Content[];
}
}
@@ -0,0 +1,8 @@
export { Chat } from './chat';
export {
type ChatModel,
type ChatCompletionCreateParams,
type ChatCompletionCreateParamsNonStreaming,
type ChatCompletionCreateParamsStreaming,
Completions,
} from './completions';
@@ -0,0 +1,3 @@
export * from './chat/index';
export { Models, type Model, ModelsPage } from './models';
@@ -0,0 +1,85 @@
import OpenAI from 'openai';
import {
type FinalRequestOptions,
type PagePromise,
type RequestOptions,
} from 'openai/core';
import { Page } from 'openai/pagination';
import { type GeminiAI } from '../../index';
import { APIResource } from '../resource';
// TODO 输出原始对象
export class Models extends APIResource {
/**
* Retrieves a model instance, providing basic information about the model such as
* the owner and permissioning.
*/
async retrieve(model: string, options?: RequestOptions): Promise<Model> {
const item: GeminiModel = await this._client.get(
`/models/${model}`,
options,
);
return {
id: item.name,
created: 0,
object: 'model',
owned_by: 'google',
};
}
/**
* Lists the currently available models, and provides basic information about each
* one such as the owner and availability.
*/
list(options?: RequestOptions): PagePromise<ModelsPage, Model> {
return this._client.getAPIList('/models', ModelsPage, options);
}
}
export class ModelsPage extends Page<Model> {
constructor(
client: GeminiAI,
response: Response,
body: GeminiPageResponse,
options: FinalRequestOptions,
) {
const data: Model[] = body.models.map((item) => {
return {
id: item.name,
created: 0,
object: 'model',
owned_by: 'google',
};
});
super(client, response, { data, object: 'list' }, options);
}
}
interface GeminiModel {
name: string;
version: string;
displayName: string;
description: string;
inputTokenLimit: string;
outputTokenLimit: string;
supportedGenerationMethods: string[];
}
interface GeminiPageResponse {
models: GeminiModel[];
}
/**
* Describes an OpenAI model offering that can be used with the API.
*/
export type Model = OpenAI.Models.Model;
// eslint-disable-next-line no-redeclare
export namespace Models {
export import Model = OpenAI.Models.Model;
// eslint-disable-next-line @typescript-eslint/no-shadow
export import ModelsPage = OpenAI.Models.ModelsPage;
}
+125
View File
@@ -0,0 +1,125 @@
import { createHmac } from 'node:crypto';
import type { Agent } from 'node:http';
import {
APIClient,
type DefaultQuery,
type Fetch,
type FinalRequestOptions,
type Headers,
} from 'openai/core';
import * as API from './resources';
export interface HunYuanAIOptions {
baseURL?: string;
appId?: string;
secretId?: string;
secretKey?: string;
timeout?: number | undefined;
httpAgent?: Agent;
fetch?: Fetch | undefined;
/**
* Default headers to include with every request to the API.
*
* These can be removed in individual requests by explicitly setting the
* header to `undefined` or `null` in request options.
*/
defaultHeaders?: Headers;
/**
* Default query parameters to include with every request to the API.
*
* These can be removed in individual requests by explicitly setting the
* param to `undefined` in request options.
*/
defaultQuery?: DefaultQuery;
}
export class HunYuanAI extends APIClient {
appId: number;
secretId: string;
secretKey: string;
private _options: HunYuanAIOptions;
constructor(options: HunYuanAIOptions = {}) {
const {
appId = process.env.HUNYUAN_APP_ID || '',
secretId = process.env.HUNYUAN_SECRET_ID || '',
secretKey = process.env.HUNYUAN_SECRET_KEY || '',
baseURL = 'https://hunyuan.cloud.tencent.com/hyllm/v1',
timeout = 30000,
httpAgent = undefined,
...rest
} = options;
super({
baseURL,
timeout,
fetch,
httpAgent,
...rest,
});
this._options = options;
this.appId = parseInt(appId, 10);
this.secretKey = secretKey;
this.secretId = secretId;
}
chat = new API.Chat(this);
protected override defaultHeaders(opts: FinalRequestOptions): Headers {
return {
...super.defaultHeaders(opts),
...this._options.defaultHeaders,
};
}
protected override defaultQuery(): DefaultQuery | undefined {
return this._options.defaultQuery;
}
generateAuthorization(path: string, data: Record<string, any>) {
const rawSessionKey = this.buildURL(path, {}).replace('https://', '');
const rawSignature: string[] = [];
Object.keys(data)
.sort()
.forEach((key) => {
const value = data[key];
if (value == null) return;
if (typeof value === 'object') {
rawSignature.push(`${key}=${JSON.stringify(value)}`);
} else {
rawSignature.push(`${key}=${value}`);
}
});
return this.hash(`${rawSessionKey}?${rawSignature.join('&')}`);
}
protected hash(data: string) {
const hash = createHmac('sha1', this.secretKey);
return hash.update(Buffer.from(data, 'utf8')).digest('base64');
}
}
// eslint-disable-next-line no-redeclare
export namespace HunYuanAI {
export type ChatModel = API.ChatModel;
export type ChatCompletionCreateParams = API.ChatCompletionCreateParams;
export type ChatCompletionCreateParamsStreaming =
API.ChatCompletionCreateParamsStreaming;
export type ChatCompletionCreateParamsNonStreaming =
API.ChatCompletionCreateParamsNonStreaming;
}
export default HunYuanAI;
+9
View File
@@ -0,0 +1,9 @@
import type { HunYuanAI } from './index';
export class APIResource {
protected _client: HunYuanAI;
constructor(client: HunYuanAI) {
this._client = client;
}
}
@@ -0,0 +1,6 @@
import { APIResource } from '../../resource';
import { Completions } from './completions';
export class Chat extends APIResource {
completions = new Completions(this._client);
}
@@ -0,0 +1,250 @@
import { createHmac } from 'node:crypto';
import OpenAI, { APIError } from 'openai';
import { Stream } from 'openai/streaming';
import { APIResource } from '../../resource';
export class Completions extends APIResource {
/**
* Creates a model response for the given chat conversation.
*
* See https://cloud.tencent.com/document/product/1729/97732
*/
create(
body: ChatCompletionCreateParamsNonStreaming,
options?: OpenAI.RequestOptions,
): Promise<OpenAI.ChatCompletion>;
create(
body: ChatCompletionCreateParamsStreaming,
options?: OpenAI.RequestOptions,
): Promise<Stream<OpenAI.ChatCompletionChunk>>;
async create(
params: ChatCompletionCreateParams,
options?: OpenAI.RequestOptions,
): Promise<OpenAI.ChatCompletion | Stream<OpenAI.ChatCompletionChunk>> {
const client = this._client;
const { model, messages, temperature = 0.8, top_p, stream } = params;
const timestamp = Math.floor(Date.now() / 1000);
const body: ChatCompletions.ChatCompletionCreateParams = {
app_id: client.appId,
secret_id: client.secretId,
timestamp,
expired: timestamp + 7200,
temperature,
top_p,
stream: stream ? 1 : 0,
messages,
};
const path = '/chat/completions';
const signature = client.generateAuthorization(path, body);
const response: Response = await this._client.post(path, {
...options,
body,
headers: {
...options?.headers,
Authorization: signature,
},
stream: false,
__binaryResponse: true,
});
if (params.stream) {
const controller = new AbortController();
options?.signal?.addEventListener('abort', () => {
controller.abort();
});
return Completions.fromSSEResponse(
model,
Stream.fromSSEResponse(response, controller),
controller,
);
}
return Completions.fromResponse(model, await response.json());
}
static fromSSEResponse(
model: string,
stream: Stream<ChatCompletions.ChatCompletion>,
controller: AbortController,
): Stream<OpenAI.ChatCompletionChunk> {
async function* iterator(): AsyncIterator<
OpenAI.ChatCompletionChunk,
any,
undefined
> {
// eslint-disable-next-line no-restricted-syntax
for await (const chunk of stream) {
if (chunk.error) {
throw new APIError(undefined, chunk.error, undefined, undefined);
}
const message = chunk.choices[0];
const choice: OpenAI.ChatCompletionChunk.Choice = {
index: 0,
delta: {
role: 'assistant',
content: message.delta.content || '',
},
finish_reason: null,
};
yield {
id: chunk.id,
model,
choices: [choice],
object: 'chat.completion.chunk',
created: parseInt(chunk.created, 10),
};
}
}
return new Stream(iterator, controller);
}
static fromResponse(
model: string,
data: ChatCompletions.ChatCompletion,
): OpenAI.ChatCompletion {
if (data.error) {
throw new APIError(undefined, data.error, undefined, undefined);
}
const message = data.choices[0];
const choice: OpenAI.ChatCompletion.Choice = {
index: 0,
message: {
role: 'assistant',
content: message.messages.content,
},
logprobs: null,
finish_reason: message.finish_reason,
};
return {
id: data.id,
model,
choices: [choice],
created: parseInt(data.created, 10),
object: 'chat.completion',
usage: data.usage,
};
}
protected hash(data: string) {
const hash = createHmac('sha1', this._client.secretKey);
return hash.update(Buffer.from(data, 'utf8')).digest('base64');
}
}
export interface ChatCompletionCreateParamsNonStreaming
extends OpenAI.ChatCompletionCreateParamsNonStreaming {
model: ChatModel;
}
export interface ChatCompletionCreateParamsStreaming
extends OpenAI.ChatCompletionCreateParamsStreaming {
model: ChatModel;
}
export type ChatCompletionCreateParams =
| ChatCompletionCreateParamsNonStreaming
| ChatCompletionCreateParamsStreaming;
export type ChatModel = 'hunyuan';
export namespace ChatCompletions {
// eslint-disable-next-line @typescript-eslint/no-shadow
export interface ChatCompletionCreateParams {
/**
* 腾讯云账号的 APPID
*/
app_id: number;
/**
* API 密钥
*/
secret_id: string;
/**
* 当前 UNIX 时间戳,单位为秒,可记录发起 API 请求的时间。
*/
timestamp: number;
/**
* 签名的有效期,是一个符合 UNIX Epoch 时间戳规范的数值,单位为秒;Expired 必须与 Timestamp 的差值小于90天
*/
expired: number;
/**
* 请求 ID,用于问题排查
*/
query_id?: string;
/**
* 内容随机性
*/
temperature?: number | null;
/**
* 生成结果的多样性
*/
top_p?: number | null;
/**
* 是否返回流式结果
*
* 0:同步,1:流式 (默认,协议:SSE)
*
* 同步请求超时:60s,如果内容较长建议使用流式
*/
stream?: number | null;
/**
* 会话内容, 按对话时间序排列,长度最多为40
* 最大支持16k tokens上下文
*/
messages: OpenAI.ChatCompletionMessageParam[];
}
export type CompletionChoicesDelta = {
content: string;
};
export type CompletionChoice = {
finish_reason: 'stop';
/**
* 内容,同步模式返回内容,流模式为 null
*/
messages: OpenAI.ChatCompletionMessage;
/**
* 内容,流模式返回内容,同步模式为 null
*/
delta: CompletionChoicesDelta;
};
export interface ChatCompletion {
choices: CompletionChoice[];
created: string;
note: string;
id: string;
usage: OpenAI.CompletionUsage;
error?: {
message: string;
code: number;
};
}
}
@@ -0,0 +1,8 @@
export { Chat } from './chat';
export {
type ChatModel,
type ChatCompletionCreateParams,
type ChatCompletionCreateParamsNonStreaming,
type ChatCompletionCreateParamsStreaming,
Completions,
} from './completions';
@@ -0,0 +1 @@
export * from './chat/index';
+51
View File
@@ -0,0 +1,51 @@
import OpenAI from 'openai';
import ErnieAI, { ErnieAIOptions } from './ernie';
import GeminiAI, { GeminiAIOptions } from './gemini';
import HunYuanAI, { HunYuanAIOptions } from './hunyuan';
import MinimaxAI, { MinimaxAIOptions } from './minimax';
import QWenAI, { QWenAIOptions } from './qwen';
import SparkAI, { SparkAIOptions } from './spark';
import VYroAI, { VYroAIOptions } from './vyro';
export {
ErnieAI,
type ErnieAIOptions,
GeminiAI,
type GeminiAIOptions,
HunYuanAI,
type HunYuanAIOptions,
MinimaxAI,
type MinimaxAIOptions,
OpenAI,
QWenAI,
type QWenAIOptions,
SparkAI,
type SparkAIOptions,
VYroAI,
type VYroAIOptions,
};
export {
OpenAIError,
APIError,
APIConnectionError,
APIConnectionTimeoutError,
APIUserAbortError,
NotFoundError,
ConflictError,
RateLimitError,
BadRequestError,
AuthenticationError,
InternalServerError,
PermissionDeniedError,
UnprocessableEntityError,
} from 'openai';
export * from './resource';
export * from './streaming';
export * from './util';
export default {
version: process.env.PKG_VERSION,
};
+19
View File
@@ -0,0 +1,19 @@
import { APIError } from 'openai';
export type MinimaxAPIResponse = {
base_resp: {
status_code: number;
status_msg: string;
};
};
export function assertStatusCode(data: MinimaxAPIResponse) {
if (data.base_resp.status_code === 0) return;
const error = {
code: data.base_resp.status_code,
message: data.base_resp.status_msg,
};
throw new APIError(undefined, error, undefined, undefined);
}
+98
View File
@@ -0,0 +1,98 @@
import type { Agent } from 'node:http';
import {
APIClient,
type DefaultQuery,
type Fetch,
type FinalRequestOptions,
type Headers,
} from 'openai/core';
import * as API from './resources';
export interface MinimaxAIOptions {
baseURL?: string;
orgId?: string;
apiKey?: string;
timeout?: number | undefined;
httpAgent?: Agent;
fetch?: Fetch | undefined;
defaultHeaders?: Headers;
defaultQuery?: DefaultQuery;
}
export class MinimaxAI extends APIClient {
protected orgId: string;
protected apiKey: string;
private _options: MinimaxAIOptions;
constructor(options: MinimaxAIOptions = {}) {
const {
orgId = process.env.MINIMAX_API_ORG || '',
apiKey = process.env.MINIMAX_API_KEY || '',
baseURL = 'https://api.minimax.chat/v1',
timeout = 30000,
httpAgent = undefined,
...rest
} = options;
super({
baseURL,
timeout,
fetch,
httpAgent,
...rest,
});
this._options = options;
this.apiKey = apiKey;
this.orgId = orgId;
}
audio = new API.Audio(this);
chat = new API.Chat(this);
embeddings = new API.Embeddings(this);
protected authHeaders(): Headers {
return {
Authorization: `Bearer ${this.apiKey}`,
};
}
protected override defaultHeaders(opts: FinalRequestOptions): Headers {
return {
...super.defaultHeaders(opts),
...this._options.defaultHeaders,
};
}
protected override defaultQuery(): DefaultQuery | undefined {
return {
GroupId: this.orgId,
...this._options.defaultQuery,
};
}
}
// eslint-disable-next-line no-redeclare
export namespace MinimaxAI {
export type Chat = API.Chat;
export type ChatModel = API.ChatModel;
export type ChatCompletionCreateParams = API.ChatCompletionCreateParams;
export type ChatCompletionCreateParamsNonStreaming =
API.ChatCompletionCreateParamsNonStreaming;
export type ChatCompletionCreateParamsStreaming =
API.ChatCompletionCreateParamsStreaming;
export type Embeddings = API.Embeddings;
export type EmbeddingCreateParams = API.EmbeddingCreateParams;
export type Audio = API.Audio;
}
export default MinimaxAI;
@@ -0,0 +1,7 @@
// File generated from our OpenAPI spec by Stainless.
import { APIResource } from '../../../resource';
import { Speech } from './speech';
export class Audio extends APIResource {
speech = new Speech(this._client);
}
@@ -0,0 +1,2 @@
export { Audio } from './audio';
export { type SpeechCreateParams, type SpeechModel, Speech } from './speech';
@@ -0,0 +1,234 @@
import { OpenAIError } from 'openai';
import { type RequestOptions } from 'openai/core';
import { APIResource } from '../../../resource';
import { assertStatusCode, type MinimaxAPIResponse } from '../../error';
export class Speech extends APIResource {
protected resources: Record<
SpeechModel,
{
model: string;
endpoint: string;
resposne_type: 'json' | 'binary' | 'stream';
}
> = {
'speech-01': {
model: 'speech-01',
endpoint: '/text_to_speech',
resposne_type: 'binary',
},
'speech-01-pro': {
model: 'speech-01',
endpoint: '/t2a_pro',
resposne_type: 'json',
},
// Note: 返回的是 SSE 流数据
// 'speech-01-stream': {
// model: 'speech-01',
// endpoint: '/tts/stream',
// resposne_type: 'stream',
// },
};
/**
* Generates audio from the input text.
*
* See https://api.minimax.chat/document/guides/T2A-model/tts
*/
create(
params: Speech.SpeechCreateParams,
options?: RequestOptions,
): Promise<Response>;
create(
params: Speech.SpeechCreateParams,
options: RequestOptions & {
__binaryResponse: false;
},
): Promise<Speech.AudioCreateResponse>;
async create(
params: Speech.SpeechCreateParams,
options?: RequestOptions,
): Promise<Response | Speech.AudioCreateResponse> {
const { input, voice, ...rest } = params;
const resource = this.resources[params.model];
if (!resource) {
throw new OpenAIError(`Invalid model: ${params.model}`);
}
const body: Record<string, any> = {
...rest,
text: input,
model: resource.model,
};
if (voice) {
body.voice_id = voice;
}
const response: Response = await this._client.post(resource.endpoint, {
...options,
body,
__binaryResponse: true,
});
// Note: pro 模型返回 json
if (
options?.__binaryResponse ||
resource.resposne_type === 'binary' ||
resource.resposne_type === 'stream'
)
return response;
return response.json().then((data: Speech.AudioCreateResponse) => {
assertStatusCode(data);
return fetch(data.audio_file);
});
}
}
export type SpeechModel = Speech.SpeechModel;
export type SpeechCreateParams = Speech.SpeechCreateParams;
// eslint-disable-next-line no-redeclare
export namespace Speech {
// eslint-disable-next-line @typescript-eslint/no-shadow
export type SpeechModel =
| (string & NonNullable<unknown>)
| 'speech-01'
| 'speech-01-pro';
// eslint-disable-next-line @typescript-eslint/no-shadow
export interface SpeechCreateParams {
/**
* One of the available [TTS models](https://api.minimax.chat/document/guides/T2A-model/tts)
*/
model: SpeechModel;
/**
* The text to generate audio for.
*/
input: string;
/**
* The voice to use when generating the audio.
*
* - 青涩青年音色(male-qn-qingse)
* - 精英青年音色(male-qn-jingying)
* - 霸道青年音色(male-qn-badao)
* - 青年大学生音色(male-qn-daxuesheng)
* - 少女音色(female-shaonv)
* - 御姐音色(female-yujie)
* - 成熟女性音色(female-chengshu)
* - 甜美女性音色(female-tianmei)
* - 男性主持人(presenter_male)
* - 女性主持人(presenter_female)
* - 男性有声书1(audiobook_male_1)
* - 男性有声书2(audiobook_male_2)
* - 女性有声书1(audiobook_female_1)
* - 女性有声书2(audiobook_female_2)
* - 青涩青年音色-beta(male-qn-qingse-jingpin)
* - 精英青年音色-beta(male-qn-jingying-jingpin)
* - 霸道青年音色-beta(male-qn-badao-jingpin)
* - 青年大学生音色-beta(male-qn-daxuesheng-jingpin)
* - 少女音色-beta(female-shaonv-jingpin)
* - 御姐音色-beta(female-yujie-jingpin)
* - 成熟女性音色-beta(female-chengshu-jingpin)
* - 甜美女性音色-beta(female-tianmei-jingpin)
*/
voice:
| (string & NonNullable<unknown>)
| 'male-qn-qingse'
| 'male-qn-jingying'
| 'male-qn-badao'
| 'male-qn-daxuesheng'
| 'female-shaonv'
| 'female-yujie'
| 'female-chengshu'
| 'female-tianmei'
| 'presenter_male'
| 'presenter_female'
| 'audiobook_male_1'
| 'audiobook_male_2'
| 'audiobook_female_1'
| 'audiobook_female_2'
| 'male-qn-qingse-jingpin'
| 'male-qn-jingying-jingpin'
| 'male-qn-badao-jingpin'
| 'male-qn-daxuesheng-jingpin'
| 'female-shaonv-jingpin'
| 'female-yujie-jingpin'
| 'female-chengshu-jingpin'
| 'female-tianmei-jingpin';
/**
* The speed of the generated audio.
*
* Range: 0.5 - 2.0
*
* @defaultValue 1.0
*/
speed?: number;
/**
* The vol of the generated audio.
*
*
* Range: 0~1
*
* @defaultValue 1.0
*/
vol?: number;
/**
* The pitch of the generated audio.
*
* Range: 0~1
*
* @defaultValue 0
*/
pitch?: number;
/**
* 生成声音的采样率。t2a_pro 可用
*
* Range: [16000, 24000]
*
* @defaultValue 24000
*/
audio_sample_rate?: number;
/**
* 生成声音的比特率. t2a_pro 可用
*
* Range: [32000, 64000128000]
*
* @defaultValue 128000
*/
bitrate?: number;
/**
* The format to audio in. Supported formats are `mp3`, `opus`, `aac`, and `flac`.
*/
response_format?: 'mp3' | 'opus' | 'aac' | 'flac';
}
export interface AudioCreateResponse extends MinimaxAPIResponse {
audio_file: string;
subtitle_file: string;
trace_id: string;
extra_info: {
audio_length: number;
audio_sample_rate: number;
audio_size: number;
bitrate: number;
word_count: number;
invisible_character_ratio: number;
};
}
}
@@ -0,0 +1,6 @@
import { APIResource } from '../../../resource';
import { Completions } from './completions';
export class Chat extends APIResource {
completions = new Completions(this._client);
}
@@ -0,0 +1,505 @@
import OpenAI, { APIError, OpenAIError } from 'openai';
import { Stream } from 'openai/streaming';
import { APIResource } from '../../../resource';
import { iterMessages, SSEDecoder } from '../../../streaming';
import { assertStatusCode } from '../../error';
export class Completions extends APIResource {
protected resources: Record<
ChatModel,
{
model: ChatModel;
endpoint: string;
}
> = {
'abab5-chat': {
model: 'abab5-chat',
endpoint: '/text/chatcompletion',
},
'abab5.5-chat': {
model: 'abab5.5-chat',
endpoint: '/text/chatcompletion',
},
'abab5.5-chat-pro': {
model: 'abab5.5-chat',
endpoint: '/text/chatcompletion_pro',
},
};
protected system =
'MM智能助理是一款由MiniMax自研的,没有调用其他产品的接口的大型语言模型。MiniMax是一家中国科技公司,一直致力于进行大模型相关的研究。';
/**
* Creates a model response for the given chat conversation.
*
* See https://api.minimax.chat/document/guides/chat-model/chat/api
*/
create(
body: ChatCompletionCreateParamsNonStreaming,
options?: OpenAI.RequestOptions,
): Promise<OpenAI.ChatCompletion>;
create(
body: ChatCompletionCreateParamsStreaming,
options?: OpenAI.RequestOptions,
): Promise<Stream<OpenAI.ChatCompletionChunk>>;
async create(
params: ChatCompletionCreateParams,
options?: OpenAI.RequestOptions,
) {
const resource = this.resources[params.model];
if (!resource) {
throw new OpenAIError(`Invalid model: ${params.model}`);
}
const body = this.buildCreateParams(params);
const response: Response = await this._client.post(resource.endpoint, {
...options,
body: { ...body, model: resource.model },
stream: false,
__binaryResponse: true,
});
if (body.stream) {
const controller = new AbortController();
options?.signal?.addEventListener('abort', () => {
controller.abort();
});
return Completions.fromSSEResponse(params.model, response, controller);
}
return Completions.fromResponse(params.model, await response.json());
}
protected buildCreateParams(
params: ChatCompletionCreateParams,
): ChatCompletions.ChatCompletionCreateParams {
const { model, messages = [], max_tokens, ...rest } = params;
const data: ChatCompletions.ChatCompletionCreateParams = {
model,
messages: [],
...rest,
};
if (max_tokens) {
data.tokens_to_generate = max_tokens;
}
const head = messages[0];
// minimax 的 system 是独立字段
const system = head && head.role === 'system' ? head.content : null;
// 移除 system 角色的消息
if (system) {
messages.splice(0, 1);
}
if (model === 'abab5.5-chat-pro') {
data.bot_setting = [
{
bot_name: 'MM智能助理',
content: system || this.system,
},
];
data.reply_constraints = {
sender_type: 'BOT',
sender_name: 'MM智能助理',
};
} else {
data.role_meta = {
bot_name: 'MM智能助理',
user_name: '用户',
};
data.prompt = system || this.system;
}
data.messages = messages.map((item) => {
switch (item.role) {
case 'assistant':
return {
sender_type: 'BOT',
text: item.content as string,
};
default: {
const message: ChatCompletions.ChatMessage = {
sender_type: 'USER',
text: item.content as string,
};
if (model === 'abab5.5-chat-pro') {
message.sender_name = '用户';
}
return message;
}
}
});
if (params.stream) {
data.use_standard_sse = true;
}
return data;
}
static fromResponse(
model: ChatModel,
data: ChatCompletions.ChatCompletion,
): OpenAI.ChatCompletion {
assertStatusCode(data);
return {
id: data.id,
model: data.model,
choices: data.choices.map((choice, index) => {
const { finish_reason } = choice;
if (model === 'abab5.5-chat-pro') {
return {
index,
message: {
role: 'assistant',
content: choice.messages[0].text,
},
logprobs: null,
finish_reason,
};
}
return {
index,
message: {
role: 'assistant',
content: choice.text,
},
logprobs: null,
finish_reason,
};
}),
created: data.created,
object: 'chat.completion',
usage: data.usage,
};
}
static fromSSEResponse(
model: ChatModel,
response: Response,
controller: AbortController,
): Stream<OpenAI.ChatCompletionChunk> {
let consumed = false;
const decoder = new SSEDecoder();
function transform(
data: ChatCompletions.ChatCompletionChunk,
): OpenAI.ChatCompletionChunk {
return {
id: data.request_id,
model,
choices: data.choices.map((choice, index) => {
const { finish_reason = null } = choice;
if (model === 'abab5.5-chat-pro') {
const content = choice.messages[0].text;
return {
index,
delta: {
role: 'assistant',
content: finish_reason === 'stop' ? '' : content,
},
finish_reason,
};
}
return {
index,
delta: {
role: 'assistant',
content: choice.delta,
},
finish_reason,
};
}),
object: 'chat.completion.chunk',
created: data.created,
};
}
async function* iterator(): AsyncIterator<
OpenAI.ChatCompletionChunk,
any,
undefined
> {
if (consumed) {
throw new Error(
'Cannot iterate over a consumed stream, use `.tee()` to split the stream.',
);
}
consumed = true;
let done = false;
try {
// eslint-disable-next-line no-restricted-syntax
for await (const sse of iterMessages(response, decoder, controller)) {
if (done) continue;
if (sse.data.startsWith('[DONE]')) {
done = true;
continue;
}
if (sse.event === null) {
let data;
try {
data = JSON.parse(sse.data);
} catch (e) {
console.error(`Could not parse message into JSON:`, sse.data);
console.error(`From chunk:`, sse.raw);
throw e;
}
if (data && data.code) {
throw new APIError(undefined, data, undefined, undefined);
}
yield transform(data);
}
}
done = true;
} catch (e) {
// If the user calls `stream.controller.abort()`, we should exit without throwing.
if (e instanceof Error && e.name === 'AbortError') return;
throw e;
} finally {
// If the user `break`s, abort the ongoing request.
if (!done) controller.abort();
}
}
return new Stream(iterator, controller);
}
}
export interface ChatCompletionCreateParamsNonStreaming
extends OpenAI.ChatCompletionCreateParamsNonStreaming {
model: ChatModel;
}
export interface ChatCompletionCreateParamsStreaming
extends OpenAI.ChatCompletionCreateParamsStreaming {
model: ChatModel;
}
export type ChatCompletionCreateParams =
| ChatCompletionCreateParamsNonStreaming
| ChatCompletionCreateParamsStreaming;
export type ChatModel = 'abab5-chat' | 'abab5.5-chat' | 'abab5.5-chat-pro';
export namespace ChatCompletions {
export type ChatMessage = {
sender_type: 'USER' | 'BOT' | 'FUNCTION';
sender_name?: string;
text: string;
};
// eslint-disable-next-line @typescript-eslint/no-shadow
export interface ChatCompletionCreateParams {
/**
* 模型名称
*/
model: ChatModel;
/**
* 对话背景、人物或功能设定
*
* 和 bot_setting 互斥
*/
prompt?: string | null;
/**
* 对话 meta 信息
*
* 和 bot_setting 互斥
*/
role_meta?: {
/**
* 用户代称
*/
user_name: string;
/**
* AI 代称
*/
bot_name: string;
};
/**
* pro 模式下,可以设置 bot 的名称和内容
*
* 和 prompt 互斥
*/
bot_setting?: {
bot_name: string;
content: string;
}[];
/**
* pro 模式下,设置模型回复要求
*/
reply_constraints?: {
sender_type: string;
sender_name: string;
};
/**
* 对话内容
*/
messages: ChatMessage[];
/**
* 如果为 true,则表明设置当前请求为续写模式,回复内容为传入 messages 的最后一句话的续写;
*
* 此时最后一句发送者不限制 USER,也可以为 BOT。
*/
continue_last_message?: boolean | null;
/**
* 内容随机性
*/
temperature?: number | null;
/**
* 生成文本的多样性
*/
top_p?: number | null;
/**
* 最大生成token数,需要注意的是,这个参数并不会影响模型本身的生成效果,
*
* 而是仅仅通过以截断超出的 token 的方式来实现功能需要保证输入上文的 token 个数和这个值加一起小于 6144 或者 16384,否则请求会失败
*/
tokens_to_generate?: number | null;
/**
* 对输出中易涉及隐私问题的文本信息进行脱敏,
*
* 目前包括但不限于邮箱、域名、链接、证件号、家庭住址等,默认 false,即开启脱敏
*/
skip_info_mask?: boolean | null;
/**
* 对输出中易涉及隐私问题的文本信息进行打码,
*
* 目前包括但不限于邮箱、域名、链接、证件号、家庭住址等,默认true,即开启打码
*/
mask_sensitive_info?: boolean | null;
/**
* 生成多少个结果;不设置默认为1,最大不超过4。
*
* 由于 beam_width 生成多个结果,会消耗更多 token。
*/
beam_width?: number | null;
/**
* 是否以流式接口的形式返回数据,默认 false
*/
stream?: boolean | null;
/**
* 是否使用标准 SSE 格式,设置为 true 时,
* 流式返回的结果将以两个换行为分隔符。
*
* 只有在 stream=true 时,此参数才会生效。
*/
use_standard_sse?: boolean | null;
}
export type ChatCompletionChoice = {
index?: number;
text: string;
messages: {
sender_type: 'BOT';
sender_name: string;
text: string;
}[];
finish_reason:
| 'stop'
| 'length'
| 'tool_calls'
| 'content_filter'
| 'function_call';
};
export interface ChatCompletion {
id: string;
created: number;
model: ChatModel;
reply: string;
choices: ChatCompletionChoice[];
usage: {
/**
* Number of tokens in the generated completion.
*/
completion_tokens: number;
/**
* Number of tokens in the prompt.
*/
prompt_tokens: number;
/**
* Total number of tokens used in the request (prompt + completion).
*/
total_tokens: number;
};
input_sensitive: boolean;
output_sensitive: boolean;
base_resp: {
status_code: number;
status_msg: string;
};
}
export type ChatCompletionChunkChoice = {
index: number;
delta: string;
messages: {
sender_type: 'BOT';
sender_name: string;
text: string;
}[];
finish_reason:
| 'stop'
| 'length'
| 'content_filter'
| 'function_call'
| null;
};
export interface ChatCompletionChunk {
request_id: string;
created: number;
model: ChatModel;
reply: string;
choices: ChatCompletionChunkChoice[];
usage: {
total_tokens: number;
};
input_sensitive: false;
output_sensitive: false;
base_resp: {
status_code: number;
status_msg: string;
};
}
}
@@ -0,0 +1,8 @@
export { Chat } from './chat';
export {
type ChatModel,
type ChatCompletionCreateParams,
type ChatCompletionCreateParamsNonStreaming,
type ChatCompletionCreateParamsStreaming,
Completions,
} from './completions';
@@ -0,0 +1,70 @@
import OpenAI from 'openai';
import { type RequestOptions } from 'openai/core';
import { APIResource } from '../../resource';
import { assertStatusCode } from '../error';
export class Embeddings extends APIResource {
/**
* Creates an embedding vector representing the input text.
*
* See https://api.minimax.chat/document/guides/Embeddings
*/
async create(
params: EmbeddingCreateParams,
options?: RequestOptions,
): Promise<OpenAI.CreateEmbeddingResponse> {
const { model, input, type = 'query' } = params;
const response: Response = await this._client.post('/embeddings', {
body: {
model,
texts: input,
type,
},
...options,
__binaryResponse: true,
});
const data: CreateEmbeddingResponse = await response.json();
assertStatusCode(data);
return {
data: data.vectors.map((embedding, index) => {
return {
embedding,
index,
object: 'embedding',
};
}),
model,
object: 'list',
usage: {
prompt_tokens: data.total_tokens,
total_tokens: data.total_tokens,
},
};
}
}
export interface EmbeddingCreateParams extends OpenAI.EmbeddingCreateParams {
/**
* 模型
*/
model: 'embo-01';
/**
* 首先通过db生成目标内容的向量并存储到向量数据库中,之后通过query生成检索文本的向量。
*/
type?: 'db' | 'query';
}
type CreateEmbeddingResponse = {
vectors: number[][];
total_tokens: number;
base_resp: {
status_code: number;
status_msg: string;
};
};
@@ -0,0 +1,4 @@
export * from './chat/index';
export * from './audio/audio';
export { Embeddings, type EmbeddingCreateParams } from './embeddings';
@@ -0,0 +1,2 @@
export * from './resolvers';
export * from './types';
@@ -0,0 +1,262 @@
import OpenAI, { APIError } from 'openai';
import { _iterSSEMessages, Stream } from 'openai/streaming';
import { DashscopeChat, OpenAIChatCompatibility } from '../types';
import { isMultiModal, toCompletionUsage } from './completions';
export function fromChatCompletionMessages(
messages: OpenAI.ChatCompletionMessageParam[],
): OpenAI.ChatCompletionMessageParam[] {
return messages.map((message) => {
if (Array.isArray(message.content)) {
message.content.forEach((part) => {
if (part.type === 'image_url') {
// @ts-expect-error
part.image = part.image_url.url;
// @ts-expect-error
delete part.image_url;
}
// @ts-expect-error
delete part.type;
});
} else {
message.content = [
// @ts-expect-error
{ text: message.content! },
];
}
return message;
});
}
export function fromChatCompletionTextMessages(
messages: OpenAI.ChatCompletionMessageParam[],
): OpenAI.ChatCompletionMessageParam[] {
return messages.map((message) => {
if (Array.isArray(message.content)) {
const part = message.content.find(
(c) => c.type === 'text',
) as OpenAI.ChatCompletionContentPartText;
return {
role: message.role,
content: part.text,
} as OpenAI.ChatCompletionMessageParam;
}
return message;
});
}
export function fromChatCompletionCreateParams(
params: OpenAIChatCompatibility.ChatCompletionCreateParams,
): DashscopeChat.ChatCompletionCreateParams {
const {
model,
messages,
raw,
response_format,
stream_options = {},
...parameters
} = params;
const result: DashscopeChat.ChatCompletionCreateParams = {
model,
input: {
messages: [],
},
parameters,
};
if (raw === true) {
result.input.messages = messages;
} else if (isMultiModal(model)) {
result.input.messages = fromChatCompletionMessages(messages);
} else {
result.input.messages = fromChatCompletionTextMessages(messages);
}
if (params.tools) {
result.parameters!.result_format = 'message';
} else {
if (response_format && response_format.type) {
result.parameters!.result_format = response_format.type;
}
if (params.stream) {
const incremental_output = stream_options?.incremental_output ?? true;
result.parameters!.incremental_output = incremental_output;
}
}
return result;
}
export function toChatCompletionFinishReason(
reason?: DashscopeChat.ResponseFinish | null,
stream?: boolean,
) {
if (reason === 'null' || !reason) {
return (stream ? null : 'stop') as 'stop';
}
return reason;
}
export function toChatCompletion(
params: DashscopeChat.ChatCompletionCreateParams,
response: DashscopeChat.ChatCompletion,
): OpenAI.ChatCompletion {
const { model } = params;
const { output, usage } = response;
const choice: OpenAI.ChatCompletion.Choice = {
index: 0,
message: {
role: 'assistant',
content: '',
},
logprobs: null,
finish_reason: 'stop',
};
// Note: `params.parameters.result_format=message`
if (output.choices) {
const { message, finish_reason } = output.choices[0];
choice.message = {
role: message.role,
content: message.content,
};
if (finish_reason === 'tool_calls') {
choice.finish_reason = 'tool_calls';
choice.message.tool_calls = message.tool_calls;
} else {
choice.finish_reason = toChatCompletionFinishReason(finish_reason, true);
}
} else {
choice.message.content = output.text;
choice.finish_reason = toChatCompletionFinishReason(output.finish_reason);
}
return {
id: response.request_id,
model,
choices: [choice],
created: Math.floor(Date.now() / 1000),
object: 'chat.completion',
usage: toCompletionUsage(usage),
};
}
function toCompletionChunk(
params: DashscopeChat.ChatCompletionCreateParams,
chunk: DashscopeChat.ChatCompletion,
): OpenAI.ChatCompletionChunk {
const { output } = chunk;
const choice: OpenAI.ChatCompletionChunk.Choice = {
index: 0,
delta: {
role: 'assistant',
content: '',
},
finish_reason: null,
};
// Note: work in `params.parameters.result_format=message`
if (output.choices) {
const { message, finish_reason } = output.choices[0];
choice.delta = {
role: message.role,
content: message.content,
};
if (finish_reason === 'tool_calls') {
choice.finish_reason = 'tool_calls';
choice.delta.tool_calls =
message.tool_calls as OpenAI.ChatCompletionChunk.Choice.Delta.ToolCall[];
} else {
choice.finish_reason = toChatCompletionFinishReason(finish_reason, true);
}
} else {
choice.delta.content = output.text;
choice.finish_reason = toChatCompletionFinishReason(
output.finish_reason,
true,
);
}
return {
id: chunk.request_id,
model: params.model,
choices: [choice],
object: 'chat.completion.chunk',
created: Math.floor(Date.now() / 1000),
};
}
export function toChatCompletionStream(
params: DashscopeChat.ChatCompletionCreateParams,
response: Response,
controller: AbortController,
): Stream<OpenAI.ChatCompletionChunk> {
let consumed = false;
async function* iterator(): AsyncIterator<
OpenAI.ChatCompletionChunk,
any,
undefined
> {
if (consumed) {
throw new Error(
'Cannot iterate over a consumed stream, use `.tee()` to split the stream.',
);
}
consumed = true;
let done = false;
try {
// eslint-disable-next-line no-restricted-syntax
for await (const sse of _iterSSEMessages(response, controller)) {
if (done) continue;
if (sse.data.startsWith('[DONE]')) {
done = true;
continue;
}
if (sse.event === 'result') {
let message;
try {
message = JSON.parse(sse.data);
} catch (e) {
console.error(`Could not parse message into JSON:`, sse.data);
console.error(`From chunk:`, sse.raw);
throw e;
}
if (message && message.code) {
throw new APIError(undefined, message, undefined, undefined);
}
yield toCompletionChunk(params, message);
}
}
done = true;
} catch (e) {
// If the user calls `stream.controller.abort()`, we should exit without throwing.
if (e instanceof Error && e.name === 'AbortError') return;
throw e;
} finally {
// If the user `break`s, abort the ongoing request.
if (!done) controller.abort();
}
}
return new Stream(iterator, controller);
}
@@ -0,0 +1,152 @@
import OpenAI, { APIError } from 'openai';
import { _iterSSEMessages, Stream } from 'openai/streaming';
import type {
DashscopeCompletions,
OpenAICompletionsCompatibility,
} from '../types';
export function isMultiModal(model: string): boolean {
return model.startsWith('qwen-vl');
}
export function getCompletionCreateEndpoint(model: string) {
return isMultiModal(model)
? '/services/aigc/multimodal-generation/generation'
: '/services/aigc/text-generation/generation';
}
export function fromCompletionCreateParams(
params: OpenAICompletionsCompatibility.CompletionCreateParams,
): DashscopeCompletions.CompletionCreateParams {
const { model, prompt, response_format, stream_options, ...parameters } =
params;
const result: DashscopeCompletions.CompletionCreateParams = {
model,
input: { prompt },
parameters,
};
if (response_format && response_format.type) {
result.parameters!.result_format = response_format.type;
}
if (params.stream) {
const { incremental_output } = stream_options || {};
result.parameters!.incremental_output = incremental_output ?? true;
}
return result;
}
export function toCompletionFinishReason(
reason?: DashscopeCompletions.ResponseFinish | null,
stream?: boolean,
) {
if (reason === 'null' || !reason) {
return (stream ? null : 'stop') as 'stop';
}
return reason;
}
export function toCompletionUsage(
usage: DashscopeCompletions.CompletionUsage,
): OpenAI.CompletionUsage {
// hack: 部分模型不存在 total tokens?
// 如:llama2-7b-chat-v2
const {
output_tokens,
input_tokens,
total_tokens = output_tokens + input_tokens,
} = usage;
return {
completion_tokens: output_tokens,
prompt_tokens: input_tokens,
total_tokens,
};
}
export function toCompletion(
params: DashscopeCompletions.CompletionCreateParams,
response: DashscopeCompletions.Completion,
stream?: boolean,
): OpenAI.Completion {
const { model } = params;
const { output, usage } = response;
const choice: OpenAI.CompletionChoice = {
index: 0,
text: output.text,
logprobs: null,
finish_reason: toCompletionFinishReason(output.finish_reason, stream),
};
return {
id: response.request_id,
model,
choices: [choice],
created: Math.floor(Date.now() / 1000),
object: 'text_completion',
usage: toCompletionUsage(usage),
};
}
export function toCompletionStream(
params: DashscopeCompletions.CompletionCreateParams,
response: Response,
controller: AbortController,
): Stream<OpenAI.Completion> {
let consumed = false;
async function* iterator(): AsyncIterator<OpenAI.Completion, any, undefined> {
if (consumed) {
throw new Error(
'Cannot iterate over a consumed stream, use `.tee()` to split the stream.',
);
}
consumed = true;
let done = false;
try {
// eslint-disable-next-line no-restricted-syntax
for await (const sse of _iterSSEMessages(response, controller)) {
if (done) continue;
if (sse.data.startsWith('[DONE]')) {
done = true;
continue;
}
if (sse.event === 'result') {
let message;
try {
message = JSON.parse(sse.data);
} catch (e) {
console.error(`Could not parse message into JSON:`, sse.data);
console.error(`From chunk:`, sse.raw);
throw e;
}
if (message && message.code) {
throw new APIError(undefined, message, undefined, undefined);
}
yield toCompletion(params, message, true);
}
}
done = true;
} catch (e) {
// If the user calls `stream.controller.abort()`, we should exit without throwing.
if (e instanceof Error && e.name === 'AbortError') return;
throw e;
} finally {
// If the user `break`s, abort the ongoing request.
if (!done) controller.abort();
}
}
return new Stream(iterator, controller);
}
@@ -0,0 +1,38 @@
import { OpenAI } from 'openai';
import { DashscopeEmbeddings, OpenAIEmbeddingsCompatibility } from '../types';
export function fromEmbeddingCreatePrams(
params: OpenAIEmbeddingsCompatibility.EmbeddingCreateParams,
): DashscopeEmbeddings.EmbeddingCreateParams {
return {
model: params.model,
input: {
texts: params.input,
},
parameters: {
text_type: params.type || 'query',
},
};
}
export function toEmbedding(
params: OpenAIEmbeddingsCompatibility.EmbeddingCreateParams,
response: DashscopeEmbeddings.CreateEmbeddingResponse,
): OpenAI.CreateEmbeddingResponse {
const { output, usage } = response;
return {
object: 'list',
model: params.model,
data: output.embeddings.map(({ text_index, embedding }) => ({
index: text_index,
embedding,
object: 'embedding',
})),
usage: {
prompt_tokens: usage.total_tokens,
total_tokens: usage.total_tokens,
},
};
}
@@ -0,0 +1,2 @@
export * from './chat';
export * from './completions';
@@ -0,0 +1,58 @@
import type OpenAI from 'openai';
import type { DashscopeCompletions } from './completions';
export namespace DashscopeChat {
/**
* https://help.aliyun.com/zh/dashscope/developer-reference/model-square
*/
export type ChatModel = DashscopeCompletions.CompletionModel;
export type ResponseFinish =
| 'stop'
| 'length'
| 'tool_calls'
| 'content_filter'
| 'function_call'
| 'null';
export interface ChatCompletionParametersParam
extends DashscopeCompletions.CompletionParametersParam {
/**
* 指定可供模型调用的工具列表
*
* 当输入多个工具时,模型会选择其中一个生成结果。
*
* 警告:
*
* - tools 暂时无法和 incremental_output 参数同时使用
* - 使用 tools 时需要同时指定 result_format 为 message
*/
tools?: OpenAI.ChatCompletionTool[];
}
export interface ChatCompletionCreateParams {
model: ({} & string) | ChatModel;
input: {
messages: OpenAI.ChatCompletionMessageParam[];
};
parameters?: ChatCompletionParametersParam;
}
export namespace ChatCompletion {
export interface Output {
text: string;
finish_reason?: ResponseFinish;
choices: OpenAI.ChatCompletion.Choice[];
}
}
/**
* 详见 [输入参数配置](https://help.aliyun.com/zh/dashscope/developer-reference/api-details)
*/
export interface ChatCompletion {
request_id: string;
usage: DashscopeCompletions.CompletionUsage;
output: ChatCompletion.Output;
}
}
@@ -0,0 +1,170 @@
export namespace DashscopeCompletions {
/**
* https://help.aliyun.com/zh/dashscope/developer-reference/model-square
*/
export type CompletionModel =
// 通义千问
| 'qwen-long'
| 'qwen-turbo'
| 'qwen-plus'
| 'qwen-max'
| 'qwen-max-0428'
| 'qwen-max-0403'
| 'qwen-max-0107'
| 'qwen-max-1201'
| 'qwen-max-longcontext'
// 通义千问开源系列
| 'qwen-7b-chat'
| 'qwen-14b-chat'
| 'qwen-72b-chat'
// 多模型
| 'qwen-vl-v1'
| 'qwen-vl-chat-v1'
| 'qwen-vl-plus'
// LLAMA2;
| 'llama2-7b-chat-v2'
| 'llama2-13b-chat-v2'
// 百川
| 'baichuan-7b-v1'
| 'baichuan2-13b-chat-v1'
| 'baichuan2-7b-chat-v1'
// ChatGLM
| 'chatglm3-6b'
| 'chatglm-6b-v2';
export type ResponseFinish = 'stop' | 'length' | 'null';
/**
* - text 旧版本的 text
* - message 兼容 openai 的 message
*
* @defaultValue "text"
*/
export type ResponseFormat = 'text' | 'message';
export type CompletionParametersParam = {
/**
* 启用流式输出
*
* 默认每次输出为当前生成的整个序列,最后一次输出为最终全部生成结果
*
* 通过 {@link CompletionParametersParam.incremental_output incremental_output} 参数关闭。
*/
stream?: boolean | null;
/**
* 启用增量输出
*
* 启用 {@link ChatCompletionCreateParamsBase.stream stream} 参数时,每次输出是否每次都包含前面输出的内容。
*
* Warning: Function call 信息暂时不支持增量输出,开启时需要注意。
*
* @defaultValue false
*/
incremental_output?: boolean | null;
/**
* 生成结果的格式
*
* @defaultValue "text"
*/
result_format?: ResponseFormat;
/**
* 生成时,随机数的种子,用于控制模型生成的随机性。
*
* 如果使用相同的种子,每次运行生成的结果都将相同;
* 当需要复现模型的生成结果时,可以使用相同的种子。
* seed参数支持无符号64位整数类型。
*
* @defaultValue 1234
*/
seed?: number | null;
/**
* 用于限制模型生成token的数量,max_tokens设置的是生成上限,并不表示一定会生成这么多的token数量。最大值和默认值均为1500
*
* @defaultValue 1500
*/
max_tokens?: number | null;
/**
* 生成文本的多样性
*
* @defaultValue 0.8
*/
top_p?: number | null;
/**
* 生成时,采样候选集的大小。
*
* 例如,
* 取值为50时,仅将单次生成中得分最高的50个token组成随机采样的候选集。
* 取值越大,生成的随机性越高;取值越小,生成的确定性越高。
*
* 注意:如果top_k参数为空或者top_k的值大于100,表示不启用top_k策略,此时仅有top_p策略生效,默认是空。
*
* @defaultValue 80
*/
top_k?: number | null;
/**
* 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。默认为1.1。
*/
repetition_penalty?: number | null;
/**
* 用户控制模型生成时整个序列中的重复度。
*
* 提高时可以降低模型生成的重复度,取值范围[-2.0, 2.0]。
*/
presence_penalty?: number | null;
/**
* 内容随机性
*
* @defaultValue 1.0
*/
temperature?: number | null;
/**
* 生成停止标识符
*/
stop?: string | string[] | null;
/**
* 生成时,是否参考搜索的结果。
*
* 注意:打开搜索并不意味着一定会使用搜索结果;
* 如果打开搜索,模型会将搜索结果作为prompt,进而“自行判断”是否生成结合搜索结果的文本,默认为false
*/
enable_search?: boolean | null;
};
export interface CompletionCreateParams {
model: ({} & string) | CompletionModel;
input: {
prompt: string;
};
parameters?: DashscopeCompletions.CompletionParametersParam;
}
export interface CompletionUsage {
output_tokens: number;
input_tokens: number;
total_tokens: number;
}
export namespace Completion {
export interface Output {
text: string;
finish_reason: DashscopeCompletions.ResponseFinish;
}
}
export interface Completion {
request_id: string;
usage: CompletionUsage;
output: Completion.Output;
}
}
@@ -0,0 +1,47 @@
export namespace DashscopeEmbeddings {
export type EmbeddingModel =
| 'text-embedding-v1'
| 'text-embedding-async-v1'
| 'text-embedding-v2'
| 'text-embedding-async-v2';
export interface EmbeddingCreateParams {
/**
* 模型
*/
model: ({} & string) | EmbeddingModel;
input: {
/**
* 输入文本
*/
texts: string | Array<string> | Array<number> | Array<Array<number>>;
};
parameters: {
/**
* 文本转换为向量后可以应用于检索、聚类、分类等下游任务,对检索这类非对称任务为了达到更好的检索效果
* 建议区分查询文本(query)和底库文本(document)类型,
* 聚类、分类等对称任务可以不用特殊指定,采用系统默认值"document"即可
*
* @defaultValue 'query'
*/
text_type?: 'query' | 'document';
};
}
export type Embedding = {
text_index: number;
embedding: number[];
};
export type CreateEmbeddingResponse = {
request_id: string;
code: string;
message: string;
output: {
embeddings: Embedding[];
};
usage: {
total_tokens: number;
};
};
}
@@ -0,0 +1,4 @@
export * from './chat';
export * from './completions';
export * from './embeddings';
export * from './openai';
@@ -0,0 +1,191 @@
import type OpenAI from 'openai';
import type { DashscopeChat } from './chat';
import type { DashscopeCompletions } from './completions';
import { DashscopeEmbeddings } from './embeddings';
export namespace OpenAICompletionsCompatibility {
export type CompletionModel = DashscopeCompletions.CompletionModel;
export interface StreamOptions {
/**
* 启用增量输出
*
* 在启用流输出参数后,是否每次输出是否每次都包含前面输出的内容。
*
* @defaultValue true
*/
incremental_output?: boolean | null;
}
export interface CompletionCreateParamsBase
extends Pick<
DashscopeCompletions.CompletionParametersParam,
| 'enable_search'
| 'temperature'
| 'presence_penalty'
| 'repetition_penalty'
| 'top_k'
| 'top_p'
| 'seed'
| 'stop'
| 'max_tokens'
| 'stream'
> {
/**
* 生成模型
*
* 内置的 {@link CompletionModel} 是经过测试的,但你可以通过 [模型列表](https://help.aliyun.com/zh/dashscope/developer-reference/model-square) 测试其他支持的模型。
*/
model: ({} & string) | CompletionModel;
/**
* 用户输入的指令,用于指导模型生成回复
*/
prompt: string;
/**
* 流输出额外参数
*/
stream_options?: StreamOptions | null;
/**
* 响应格式
*/
response_format?: {
type?: 'text';
};
}
export interface CompletionCreateParamsNonStreaming
extends CompletionCreateParamsBase {
/**
* 启用流式输出
*
* 默认每次输出为当前生成的整个序列,最后一次输出为最终全部生成结果
* 可以使用 {@link StreamOptions stream_options} 参数关闭。
*/
stream?: false | null;
}
export interface CompletionCreateParamsStreaming
extends CompletionCreateParamsBase {
/**
* 启用流式输出
*
* 默认每次输出为当前生成的整个序列,最后一次输出为最终全部生成结果
* 可以使用 {@link StreamOptions stream_options} 参数关闭。
*/
stream: true;
}
export type CompletionCreateParams =
| CompletionCreateParamsNonStreaming
| CompletionCreateParamsStreaming;
}
export namespace OpenAIChatCompatibility {
export type ChatModel = DashscopeChat.ChatModel;
export interface ChatCompletionCreateParamsBase
extends Pick<
DashscopeCompletions.CompletionParametersParam,
| 'enable_search'
| 'temperature'
| 'presence_penalty'
| 'repetition_penalty'
| 'top_k'
| 'top_p'
| 'seed'
| 'stop'
| 'max_tokens'
| 'stream'
> {
/**
* 聊天模型
*
* 内置的 {@link ChatModel} 是经过测试的,但你可以通过 [模型列表](https://help.aliyun.com/zh/dashscope/developer-reference/model-square) 测试其他支持的模型。
*/
model: ({} & string) | ChatModel;
/**
* 聊天上下文信息
*/
messages: OpenAI.ChatCompletionMessageParam[];
/**
* 指定可供模型调用的工具列表
*
* 当输入多个工具时,模型会选择其中一个生成结果。
*/
tools?: OpenAI.ChatCompletionTool[];
/**
* SDK 内部有特殊的多模型消息适配机制
*
* 设置为 true 可以直接采用外部传递的消息格式
*/
raw?: boolean | null;
/**
* 流输出额外参数
*/
stream_options?: OpenAICompletionsCompatibility.StreamOptions | null;
/**
* 响应格式
*/
response_format?: {
type?: 'text';
};
}
export interface ChatCompletionCreateParamsNonStreaming
extends ChatCompletionCreateParamsBase {
/**
* 启用流式输出
*
* 默认每次输出为当前生成的整个序列,最后一次输出为最终全部生成结果
* 可以使用 {@link ChatCompletionCreateParamsBase.stream_options stream_options} 参数关闭。
*/
stream?: false | null;
}
export interface ChatCompletionCreateParamsStreaming
extends ChatCompletionCreateParamsBase {
/**
* 启用流式输出
*
* 默认每次输出为当前生成的整个序列,最后一次输出为最终全部生成结果
* 可以使用 {@link ChatCompletionCreateParamsBase.stream_options stream_options} 参数关闭。
*/
stream: true;
}
export type ChatCompletionCreateParams =
| ChatCompletionCreateParamsNonStreaming
| ChatCompletionCreateParamsStreaming;
}
export namespace OpenAIEmbeddingsCompatibility {
export interface EmbeddingCreateParams {
/**
* 模型
*/
model: ({} & string) | DashscopeEmbeddings.EmbeddingModel;
/**
* 输入文本
*/
input: string | Array<string> | Array<number> | Array<Array<number>>;
/**
* 文本转换为向量后可以应用于检索、聚类、分类等下游任务,对检索这类非对称任务为了达到更好的检索效果
* 建议区分查询文本(query)和底库文本(document)类型,
* 聚类、分类等对称任务可以不用特殊指定,采用系统默认值"document"即可
*
* @defaultValue 'query'
*/
type?: 'query' | 'document';
}
}
+129
View File
@@ -0,0 +1,129 @@
import type { Agent } from 'node:http';
import { APIError } from 'openai';
import {
APIClient,
type DefaultQuery,
type Fetch,
type FinalRequestOptions,
type Headers,
} from 'openai/core';
import * as API from './resources';
export interface QWenAIOptions {
baseURL?: string;
apiKey?: string;
timeout?: number | undefined;
httpAgent?: Agent;
fetch?: Fetch | undefined;
/**
* Default headers to include with every request to the API.
*
* These can be removed in individual requests by explicitly setting the
* header to `undefined` or `null` in request options.
*/
defaultHeaders?: Headers;
/**
* Default query parameters to include with every request to the API.
*
* These can be removed in individual requests by explicitly setting the
* param to `undefined` in request options.
*/
defaultQuery?: DefaultQuery;
}
/**
* 基于阿里云 [DashScope 灵积模型服务](https://help.aliyun.com/zh/dashscope/product-overview/product-introduction) 的接口封装
*
* @deprecated 请重点关注阿里云的 [OpenAI接口兼容](https://help.aliyun.com/zh/dashscope/developer-reference/compatibility-of-openai-with-dashscope/) 计划。
*/
export class QWenAI extends APIClient {
protected apiKey: string;
private _options: QWenAIOptions;
constructor(options: QWenAIOptions = {}) {
const {
apiKey = process.env.QWEN_API_KEY || '',
baseURL = 'https://dashscope.aliyuncs.com/api/v1/',
timeout = 30000,
httpAgent = undefined,
...rest
} = options;
super({
baseURL,
timeout,
fetch,
httpAgent,
...rest,
});
this._options = options;
this.apiKey = apiKey;
}
chat = new API.Chat(this);
completions = new API.Completions(this);
embeddings = new API.Embeddings(this);
images = new API.Images(this);
protected override authHeaders() {
return {
Authorization: `Bearer ${this.apiKey}`,
};
}
protected override defaultHeaders(opts: FinalRequestOptions): Headers {
return {
...super.defaultHeaders(opts),
...this._options.defaultHeaders,
};
}
protected override defaultQuery(): DefaultQuery | undefined {
return this._options.defaultQuery;
}
protected override makeStatusError(
status: number | undefined,
error: Record<string, any> | undefined,
message: string | undefined,
headers: Headers | undefined,
) {
return APIError.generate(status, { error }, message, headers);
}
}
// eslint-disable-next-line no-redeclare
export namespace QWenAI {
export import Chat = API.Chat;
export import ChatModel = API.ChatModel;
export import ChatCompletionCreateParams = API.ChatCompletionCreateParams;
export import ChatCompletionCreateParamsNonStreaming = API.ChatCompletionCreateParamsNonStreaming;
export import ChatCompletionCreateParamsStreaming = API.ChatCompletionCreateParamsStreaming;
export import Completions = API.Completions;
export import CompletionModel = API.CompletionModel;
export type CompletionCreateParams = API.CompletionCreateParams;
export type CompletionCreateParamsStreaming =
API.CompletionCreateParamsStreaming;
export type CompletionCreateParamsNonStreaming =
API.CompletionCreateParamsNonStreaming;
export import Embeddings = API.Embeddings;
export type EmbeddingModel = API.EmbeddingModel;
export type EmbeddingCreateParams = API.EmbeddingCreateParams;
export import Images = API.Images;
export type ImageModel = API.ImageModel;
export type ImageGenerateParams = API.ImageGenerateParams;
}
export default QWenAI;
@@ -0,0 +1,15 @@
import { APIResource } from '../../../resource';
import { OpenAIChatCompatibility } from '../../dashscope';
import { Completions } from './completions';
export class Chat extends APIResource {
completions = new Completions(this._client);
}
export type ChatModel = OpenAIChatCompatibility.ChatModel;
export type ChatCompletionCreateParams =
OpenAIChatCompatibility.ChatCompletionCreateParams;
export type ChatCompletionCreateParamsNonStreaming =
OpenAIChatCompatibility.ChatCompletionCreateParams;
export type ChatCompletionCreateParamsStreaming =
OpenAIChatCompatibility.ChatCompletionCreateParamsStreaming;
@@ -0,0 +1,67 @@
import OpenAI from 'openai';
import { type Headers } from 'openai/core';
import { Stream } from 'openai/streaming';
import { APIResource } from '../../../resource';
import {
fromChatCompletionCreateParams,
getCompletionCreateEndpoint,
type OpenAIChatCompatibility,
toChatCompletion,
toChatCompletionStream,
} from '../../dashscope';
export class Completions extends APIResource {
/**
* Creates a model response for the given chat conversation.
*
* See https://help.aliyun.com/zh/dashscope/developer-reference/api-details
*/
create(
body: OpenAIChatCompatibility.ChatCompletionCreateParamsNonStreaming,
options?: OpenAI.RequestOptions,
): Promise<OpenAI.ChatCompletion>;
create(
body: OpenAIChatCompatibility.ChatCompletionCreateParamsStreaming,
options?: OpenAI.RequestOptions,
): Promise<Stream<OpenAI.ChatCompletionChunk>>;
async create(
body: OpenAIChatCompatibility.ChatCompletionCreateParams,
options?: OpenAI.RequestOptions,
) {
const headers: Headers = {
...options?.headers,
};
if (body.stream) {
headers.Accept = 'text/event-stream';
}
const path = getCompletionCreateEndpoint(body.model);
const params = fromChatCompletionCreateParams(body);
const response: Response = await this._client.post(path, {
...options,
body: params,
headers,
// 通义千问的响应内容被包裹了一层,需要解构并转换为 OpenAI 的格式
// 设置 __binaryResponse 为 true 是为了让 client 返回原始的 response
stream: false,
__binaryResponse: true,
});
if (body.stream) {
const controller = new AbortController();
options?.signal?.addEventListener('abort', () => {
controller.abort();
});
return toChatCompletionStream(params, response, controller);
}
return toChatCompletion(params, await response.json());
}
}
@@ -0,0 +1 @@
export * from './chat';
@@ -0,0 +1,78 @@
import OpenAI from 'openai';
import { type Headers } from 'openai/core';
import { Stream } from 'openai/streaming';
import { APIResource } from '../../resource';
import {
fromCompletionCreateParams,
getCompletionCreateEndpoint,
type OpenAICompletionsCompatibility,
toCompletion,
toCompletionStream,
} from '../dashscope';
export class Completions extends APIResource {
/**
* Creates a completion for the provided prompt and parameters.
*/
create(
body: OpenAICompletionsCompatibility.CompletionCreateParamsNonStreaming,
options?: OpenAI.RequestOptions,
): Promise<OpenAI.Completion>;
create(
body: OpenAICompletionsCompatibility.CompletionCreateParamsStreaming,
options?: OpenAI.RequestOptions,
): Promise<Stream<OpenAI.Completion>>;
create(
body: OpenAICompletionsCompatibility.CompletionCreateParamsBase,
options?: OpenAI.RequestOptions,
): Promise<Stream<OpenAI.Completion> | OpenAI.Completion>;
async create(
body: OpenAICompletionsCompatibility.CompletionCreateParams,
options?: OpenAI.RequestOptions,
): Promise<OpenAI.Completion | Stream<OpenAI.Completion>> {
const headers: Headers = {
...options?.headers,
};
if (body.stream) {
headers.Accept = 'text/event-stream';
}
const path = getCompletionCreateEndpoint(body.model);
const params = fromCompletionCreateParams(body);
const response: Response = await this._client.post(path, {
...options,
body: params,
headers,
// 通义千问的响应内容被包裹了一层,需要解构并转换为 OpenAI 的格式
// 设置 __binaryResponse 为 true 是为了让 client 返回原始的 response
stream: false,
__binaryResponse: true,
});
if (body.stream) {
const controller = new AbortController();
options?.signal?.addEventListener('abort', () => {
controller.abort();
});
return toCompletionStream(params, response, controller);
}
return toCompletion(params, await response.json());
}
}
export type CompletionModel = OpenAICompletionsCompatibility.CompletionModel;
export type CompletionCreateParams =
OpenAICompletionsCompatibility.CompletionCreateParams;
export type CompletionCreateParamsStreaming =
OpenAICompletionsCompatibility.CompletionCreateParamsStreaming;
export type CompletionCreateParamsNonStreaming =
OpenAICompletionsCompatibility.CompletionCreateParamsNonStreaming;
@@ -0,0 +1,46 @@
import OpenAI from 'openai';
import { type RequestOptions } from 'openai/core';
import { APIResource } from '../../resource';
import { DashscopeEmbeddings } from '../dashscope';
import {
fromEmbeddingCreatePrams,
toEmbedding,
} from '../dashscope/resolvers/embeddings';
export class Embeddings extends APIResource {
/**
* Creates an embedding vector representing the input text.
*
* See https://help.aliyun.com/zh/dashscope/developer-reference/generic-text-vector
*/
async create(
params: OpenAI.EmbeddingCreateParams,
options?: RequestOptions,
): Promise<OpenAI.CreateEmbeddingResponse> {
const body = fromEmbeddingCreatePrams(params);
const response: Response = await this._client.post(
'/services/embeddings/text-embedding/text-embedding',
{
...options,
body,
__binaryResponse: true,
},
);
return toEmbedding(params, await response.json());
}
}
export type EmbeddingModel = Embeddings.EmbeddingModel;
export type EmbeddingCreateParams = Embeddings.EmbeddingCreateParams;
// eslint-disable-next-line no-redeclare
export namespace Embeddings {
// eslint-disable-next-line @typescript-eslint/no-shadow
export type EmbeddingModel = DashscopeEmbeddings.EmbeddingModel;
// eslint-disable-next-line @typescript-eslint/no-shadow
export type EmbeddingCreateParams = DashscopeEmbeddings.EmbeddingCreateParams;
}
+315
View File
@@ -0,0 +1,315 @@
import OpenAI, { OpenAIError } from 'openai';
import { type RequestOptions } from 'openai/core';
import { APIResource } from '../../resource';
export class Images extends APIResource {
/**
* Creates an image given a prompt.
*/
async generate(
params: ImageGenerateParams,
options: RequestOptions = {},
): Promise<OpenAI.ImagesResponse> {
const client = this._client;
const { headers, ...config } = options;
const { model = 'wanx-v1', prompt, n = 1, cfg, ...rest } = params;
const taskId = await client
.post<any, Response>('/services/aigc/text2image/image-synthesis', {
...config,
headers: { 'X-DashScope-Async': 'enable', ...headers },
body: {
model,
input: {
prompt,
},
parameters: {
...rest,
scale: cfg,
n,
},
},
__binaryResponse: true,
})
.then<ImageCreateTaskResponse>((res) => res.json())
.then((res) => res.output.task_id);
return this.waitTask(taskId, options).then((images) => {
return {
created: Date.now() / 1000,
data: images,
};
});
}
protected async waitTask(
taskId: string,
options?: RequestOptions,
): Promise<ImageTask.Image[]> {
const response = await this._client
.get<any, Response>(`/tasks/${taskId}`, {
...options,
__binaryResponse: true,
})
// eslint-disable-next-line @typescript-eslint/no-shadow
.then<ImageTaskQueryResponse>((response) => response.json());
const { task_status, message } = response.output;
if (task_status === 'PENDING' || task_status === 'RUNNING') {
return new Promise<ImageTask.Image[]>((resolve) => {
setTimeout(() => resolve(this.waitTask(taskId, options)), 5000);
});
}
if (task_status === 'SUCCEEDED') {
return response.output.results.filter(
(result) => 'url' in result,
) as ImageTask.Image[];
}
if (task_status === 'FAILED') {
throw new OpenAIError(message);
}
throw new OpenAIError('Unknown task status');
}
}
type ImageCreateTaskResponse = {
request_id: string;
output: {
task_id: string;
task_status: ImageTask.Status;
code: string;
message: string;
};
};
type ImageTaskQueryResponse =
| ImageTaskPendingResponse
| ImageTaskRunningResponse
| ImageTaskFinishedResponse
| ImageTaskFailedResponse
| ImageTaskUnknownResponse;
type ImageTaskPendingResponse = {
request_id: string;
output: {
task_id: string;
task_status: 'PENDING';
task_metrics: ImageTask.Metrics;
submit_time: string;
scheduled_time: string;
code: string;
message: string;
};
};
type ImageTaskRunningResponse = {
request_id: string;
output: {
task_id: string;
task_status: 'RUNNING';
task_metrics: ImageTask.Metrics;
submit_time: string;
scheduled_time: string;
code: string;
message: string;
};
};
type ImageTaskFinishedResponse = {
request_id: string;
output: {
task_id: string;
task_status: 'SUCCEEDED';
task_metrics: ImageTask.Metrics;
results: (ImageTask.Image | ImageTask.FailedError)[];
submit_time: string;
scheduled_time: string;
end_time: string;
code: string;
message: string;
};
usage: {
image_count: number;
};
};
type ImageTaskFailedResponse = {
request_id: string;
code: string;
message: string;
output: {
task_status: 'FAILED';
task_metrics: ImageTask.Metrics;
submit_time: string;
scheduled_time: string;
code: string;
message: string;
};
};
type ImageTaskUnknownResponse = {
request_id: string;
output: {
task_status: 'UNKNOWN';
task_metrics: ImageTask.Metrics;
code: string;
message: string;
};
};
namespace ImageTask {
export type Image = {
url: string;
};
export type FailedError = {
code: string;
message: string;
};
export type Status =
| 'PENDING'
| 'RUNNING'
| 'SUCCEEDED'
| 'FAILED'
| 'UNKNOWN';
export type Metrics = {
TOTAL: number;
SUCCEEDED: number;
FAILED: number;
};
}
export type ImageModel = Images.ImageModel;
export type ImageGenerateParams = Images.ImageGenerateParams;
// eslint-disable-next-line no-redeclare
export namespace Images {
// eslint-disable-next-line @typescript-eslint/no-shadow
export type ImageModel =
| (string & NonNullable<unknown>)
// 通义万相
| 'wanx-v1'
// Stable Diffusion
| 'stable-diffusion-v1.5'
| 'stable-diffusion-xl';
// eslint-disable-next-line @typescript-eslint/no-shadow
export interface ImageGenerateParams {
/**
* The model to use for image generation.
*
* @defaultValue wanx-v1
*/
model?: ImageModel | null;
/**
* A prompt is the text input that guides the AI in generating visual content.
* It defines the textual description or concept for the image you wish to generate.
* Think of it as the creative vision you want the AI to bring to life.
* Crafting clear and creative prompts is crucial for achieving the desired results with Imagine's API.
* For example, A serene forest with a river under the moonlight, can be a prompt.
*/
prompt: string;
/**
* The negative_prompt parameter empowers you to provide additional
* guidance to the AI by specifying what you don't want in the image.
* It helps refine the creative direction, ensuring that the generated
* content aligns with your intentions.
*/
negative_prompt?: string | null;
/**
* The size of the generated images.
*
* @defaultValue 1024*1024
*/
size?: (string & NonNullable<unknown>) | '1024*1024' | null;
/**
* The style of the generated images.
*
* - \<photography\> 摄影
* - \<portrait\> 人像写真
* - \<3d cartoon\> 3D卡通
* - \<anime\> 动画
* - \<oil painting\> 油画
* - \<watercolor\>水彩
* - \<sketch\> 素描
* - \<chinese painting\> 中国画
* - \<flat illustration\> 扁平插画
* - \<auto\> 默认
*
* 仅 wanx-v1 模型支持
*
* @defaultValue <auto>
*/
style?:
| '<photography>'
| '<portrait>'
| '<3d cartoon>'
| '<anime>'
| '<oil painting>'
| '<watercolor>'
| '<sketch>'
| '<chinese painting>'
| '<flat illustration>'
| '<auto>'
| null;
/**
* The number of images to generate. Must be between 1 and 4.
*
* @defaultValue 1
*/
n?: number | null;
/**
* The steps parameter defines the number of operations or iterations that the
* generator will perform during image creation. It can impact the complexity
* and detail of the generated image.
*
* Range: 30-50
*
* 仅 StableDiffusion 模型支持
*
* @defaultValue 40
*/
steps?: number | null;
/**
* The cfg parameter acts as a creative control knob.
* You can adjust it to fine-tune the level of artistic innovation in the image.
* Lower values encourage faithful execution of the prompt,
* while higher values introduce more creative and imaginative variations.
*
* Range: 1 - 15
*
* @defaultValue 10
*/
cfg?: number | null;
/**
* The seed parameter serves as the initial value for the random number generator.
* By setting a specific seed value, you can ensure that the AI generates the same
* image or outcome each time you use that exact seed.
*
* range: 1-Infinity
*/
seed?: number | null;
/**
* The format in which the generated images are returned.
*/
response_format?: 'url' | null;
}
}
@@ -0,0 +1,8 @@
export * from './chat/index';
export * from './completions';
export { Images, type ImageModel, type ImageGenerateParams } from './images';
export {
Embeddings,
type EmbeddingModel,
type EmbeddingCreateParams,
} from './embeddings';
+9
View File
@@ -0,0 +1,9 @@
import { APIClient } from 'openai/core';
export class APIResource<Client extends APIClient = APIClient> {
protected _client: Client;
constructor(client: Client) {
this._client = client;
}
}
+149
View File
@@ -0,0 +1,149 @@
import { createHmac } from 'node:crypto';
import {
APIClient,
type DefaultQuery,
type Fetch,
type Headers,
} from 'openai/core';
import * as API from './resources';
export interface SparkAIOptions {
baseURL?: string;
appId?: string;
apiKey?: string;
apiSecret?: string;
timeout?: number | undefined;
httpAgent?: unknown;
fetch?: Fetch | undefined;
/**
* Default headers to include with every request to the API.
*
* These can be removed in individual requests by explicitly setting the
* header to `undefined` or `null` in request options.
*/
defaultHeaders?: Headers;
/**
* Default query parameters to include with every request to the API.
*
* These can be removed in individual requests by explicitly setting the
* param to `undefined` in request options.
*/
defaultQuery?: DefaultQuery;
}
export class SparkAI extends APIClient {
appId: string;
protected apiKey: string;
protected apiSecret: string;
private _options: SparkAIOptions;
constructor(options: SparkAIOptions = {}) {
const {
appId = process.env.SPARK_APP_ID || '',
apiKey = process.env.SPARK_API_KEY || '',
apiSecret = process.env.SPARK_API_SECRET || '',
baseURL = 'https://spark-api.xf-yun.com',
timeout = 30000,
httpAgent = undefined,
...rest
} = options;
super({
baseURL,
timeout,
fetch,
httpAgent,
...rest,
});
this._options = options;
this.appId = appId;
this.apiKey = apiKey;
this.apiSecret = apiSecret;
}
chat = new API.Chat(this);
images = new API.Images(this);
protected override defaultQuery(): DefaultQuery | undefined {
return this._options.defaultQuery;
}
/**
* @param url - 需要签名的 URL
* @param method - HTTP method
* @returns 签名后的 URL
*/
generateAuthorizationURL(url: string | URL, method: string = 'GET'): string {
const target = new URL(url, this.baseURL);
const date = new Date().toUTCString();
const authorization = this.generateAuthorization({
method,
path: target.pathname,
host: target.host,
date,
});
target.searchParams.set('authorization', authorization);
target.searchParams.set('host', target.host);
target.searchParams.set('date', date);
return target.toString();
}
/**
* 生成鉴权信息
*
* See https://www.xfyun.cn/doc/spark/general_url_authentication.html
*/
generateAuthorization({
method,
host,
path,
date,
}: {
method: string;
host: string;
path: string;
date: string;
}) {
// 生成签名原文
const rawSignature = `host: ${host}\ndate: ${date}\n${method} ${path} HTTP/1.1`;
// 生成签名,需要转为 base64 编码
const signature = this.hash(rawSignature);
return btoa(
`api_key="${this.apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signature}"`,
);
}
protected hash(data: string) {
const sha256Hmac = createHmac('sha256', this.apiSecret);
sha256Hmac.update(data);
return sha256Hmac.digest('base64');
}
}
// eslint-disable-next-line no-redeclare
export namespace SparkAI {
export type Chat = API.Chat;
export type ChatModel = API.ChatModel;
export type ChatCompletionCreateParams = API.ChatCompletionCreateParams;
export type ChatCompletionCreateParamsNonStreaming =
API.ChatCompletionCreateParamsNonStreaming;
export type ChatCompletionCreateParamsStreaming =
API.ChatCompletionCreateParamsStreaming;
}
export default SparkAI;
+9
View File
@@ -0,0 +1,9 @@
import type { SparkAI } from './index';
export class APIResource {
protected _client: SparkAI;
constructor(client: SparkAI) {
this._client = client;
}
}
@@ -0,0 +1,6 @@
import { APIResource } from '../../resource';
import { Completions } from './completions';
export class Chat extends APIResource {
completions = new Completions(this._client);
}
@@ -0,0 +1,277 @@
import OpenAI, { APIError } from 'openai';
import { RequestOptions } from 'openai/core';
import { Stream } from 'openai/streaming';
import { APIResource } from '../../resource';
export class Completions extends APIResource {
protected resources: Record<
ChatModel,
{
domain: string;
url: string;
}
> = {
'spark-1.5': {
domain: 'general',
url: 'wss://spark-api.xf-yun.com/v1.1/chat',
},
'spark-2': {
domain: 'generalv2',
url: 'wss://spark-api.xf-yun.com/v2.1/chat',
},
'spark-3': {
domain: 'generalv3',
url: 'wss://spark-api.xf-yun.com/v3.1/chat',
},
};
/**
* Creates a model response for the given chat conversation.
*
* See https://help.aliyun.com/zh/dashscope/developer-reference/api-details
*/
create(
body: ChatCompletionCreateParamsNonStreaming,
options?: RequestOptions,
): Promise<OpenAI.ChatCompletion>;
create(
body: ChatCompletionCreateParamsStreaming,
options?: RequestOptions,
): Promise<Stream<OpenAI.ChatCompletionChunk>>;
async create(
params: ChatCompletionCreateParams,
options?: RequestOptions,
): Promise<Stream<OpenAI.ChatCompletionChunk> | OpenAI.ChatCompletion> {
const { model, messages, functions, user, ...rest } = params;
const resource = this.resources[model];
const url = this._client.generateAuthorizationURL(resource.url, 'GET');
const body: ChatCompletions.ChatCompletionParameters = {
header: {
app_id: this._client.appId,
},
parameter: {
chat: {
...rest,
domain: resource.domain,
},
},
payload: {
message: {
text: messages,
},
},
};
if (functions) {
body.payload.functions = { text: functions };
}
if (user) {
body.header.uid = user;
}
const controller = new AbortController();
if (options?.signal) {
options.signal.addEventListener('abort', () => {
controller.abort();
});
}
const ws: WebSocket = new WebSocket(url);
ws.onopen = () => {
ws.send(JSON.stringify(body));
};
if (params.stream) {
const readableStream = new ReadableStream({
pull(ctrl) {
const encoder = new TextEncoder();
ws.onmessage = (event) => {
const data: ChatCompletions.ChatCompletionResponse = JSON.parse(
event.data,
);
const { header, payload } = data;
if (header.code !== 0) {
ctrl.error(
new APIError(undefined, data.header, undefined, undefined),
);
return;
}
const choices = payload.choices.text;
const [message] = choices;
const choice: OpenAI.ChatCompletionChunk.Choice = {
index: 0,
delta: {
role: message.role,
content: message.content,
},
finish_reason: null,
};
if (header.status === 2) {
choice.finish_reason = 'stop';
}
if (message.function_call) {
choice.delta.function_call = message.function_call;
}
const completion: OpenAI.ChatCompletionChunk = {
id: header.sid,
model,
choices: [choice],
object: 'chat.completion.chunk',
created: Date.now() / 1000,
};
ctrl.enqueue(encoder.encode(`${JSON.stringify(completion)}\n`));
};
ws.onerror = (error) => {
ctrl.error(error);
};
},
cancel() {
ws.close();
},
});
controller.signal.addEventListener('abort', () => {
ws.close();
});
return Stream.fromReadableStream(readableStream, controller);
}
return new Promise((resolve, reject) => {
ws.onmessage = (event) => {
const data: ChatCompletions.ChatCompletionResponse = JSON.parse(
event.data,
);
const { header, payload } = data;
// 2 代表完成
if (header.status !== 2) return;
const usage = payload.usage.text;
const choices = payload.choices.text;
const [message] = choices;
const choice: OpenAI.ChatCompletion.Choice = {
index: 0,
message: {
role: 'assistant',
content: message.content,
},
logprobs: null,
finish_reason: 'stop',
};
const completion: OpenAI.ChatCompletion = {
id: header.sid,
object: 'chat.completion',
created: Date.now() / 1000,
model,
choices: [choice],
usage: {
completion_tokens: usage.completion_tokens,
total_tokens: usage.total_tokens,
prompt_tokens: usage.prompt_tokens,
},
};
resolve(completion);
};
ws.onerror = (error) => reject(error);
});
}
}
export interface ChatCompletionCreateParamsNonStreaming
extends OpenAI.ChatCompletionCreateParamsNonStreaming {
model: ChatModel;
top_k?: number | null;
chat_id?: string | null;
}
export interface ChatCompletionCreateParamsStreaming
extends OpenAI.ChatCompletionCreateParamsStreaming {
model: ChatModel;
top_k?: number | null;
chat_id?: string | null;
}
export type ChatCompletionCreateParams =
| ChatCompletionCreateParamsNonStreaming
| ChatCompletionCreateParamsStreaming;
export type ChatModel = 'spark-1.5' | 'spark-2' | 'spark-3';
export namespace ChatCompletions {
export type ChatCompletionParameters = {
header: {
app_id: string;
uid?: string;
};
parameter: {
chat: {
domain: string;
temperature?: number | null;
max_tokens?: number | null;
top_k?: number | null;
chat_id?: string | null;
};
};
payload: {
message: {
text: OpenAI.ChatCompletionMessageParam[];
};
functions?: {
text: OpenAI.ChatCompletionCreateParams.Function[];
};
};
};
export type ChatCompletionResponse = {
header: {
code: number;
message: string;
sid: string;
status: number;
};
payload: {
choices: {
status: number;
seq: number;
text: OpenAI.ChatCompletionMessage[];
};
usage: {
text: {
question_tokens: number;
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
};
};
};
}
@@ -0,0 +1,8 @@
export { Chat } from './chat';
export {
type ChatModel,
type ChatCompletionCreateParams,
type ChatCompletionCreateParamsNonStreaming,
type ChatCompletionCreateParamsStreaming,
Completions,
} from './completions';
+117
View File
@@ -0,0 +1,117 @@
import OpenAI, { APIError } from 'openai';
import { type RequestOptions } from 'openai/core';
import { APIResource } from '../resource';
// TODO: 没有权限,暂未测试
export class Images extends APIResource {
/**
* See https://www.xfyun.cn/doc/spark/ImageGeneration.html
*/
async generate(
params: OpenAI.ImageGenerateParams,
options?: RequestOptions,
): Promise<OpenAI.ImagesResponse> {
const { prompt, user } = params;
const body: ImagesAPI.ImageGenerateParams = {
header: {
app_id: this._client.appId,
uid: user,
},
parameter: {
chat: {
max_tokens: 4096,
domain: 'general',
temperature: 0.5,
},
},
payload: {
message: {
text: [{ role: 'user', content: prompt }],
},
},
};
const url = this._client.generateAuthorizationURL(
'https://spark-api.cn-huabei-1.xf-yun.com/v2.1/tti',
'POST',
);
const response: Response = await this._client.post(url, {
...options,
body,
__binaryResponse: true,
});
const resp: ImagesAPI.ImageGenerateResponse = await response.json();
if (resp.header.code > 0) {
throw new APIError(undefined, resp.header, undefined, undefined);
}
return {
created: Date.now() / 1000,
data: [
{
// base64 encoded image
url: resp.payload.choices.text[0].content,
},
],
};
}
}
namespace ImagesAPI {
export type ImageGenerateMessageParam = {
role: 'user';
content: string;
};
export type ImageGenerateParams = {
header: {
/**
* 应用ID
*/
app_id: string;
/**
* 用户唯一标识
*/
uid?: string;
};
parameter: {
chat: {
max_tokens: number;
domain: string;
temperature: number;
};
};
payload: {
message: {
text: ImageGenerateMessageParam[];
};
};
};
type ImageGenerateAssistantMessage = {
index: 0;
role: 'assistant';
content: string;
};
export type ImageGenerateResponse = {
header: {
code: number;
message: string;
sid: string;
status: number;
};
payload: {
choices: {
status: number;
seq: number;
text: ImageGenerateAssistantMessage[];
};
};
};
}
@@ -0,0 +1,3 @@
export * from './chat/index';
export { Images } from './images';
+262
View File
@@ -0,0 +1,262 @@
import { OpenAIError } from 'openai';
export type Bytes =
| string
| ArrayBuffer
| Uint8Array
| Buffer
| null
| undefined;
export type ServerSentEvent = {
event: string | null;
data: string;
raw: string[];
};
export async function* iterMessages(
response: Response,
decoder: SSEDecoder,
controller: AbortController,
): AsyncGenerator<ServerSentEvent, void, unknown> {
if (!response.body) {
controller.abort();
throw new OpenAIError(`Attempted to iterate over a response with no body`);
}
const lineDecoder = new LineDecoder();
const iter = readableStreamAsyncIterable<Bytes>(response.body);
// eslint-disable-next-line no-restricted-syntax
for await (const chunk of iter) {
// eslint-disable-next-line no-restricted-syntax
for (const line of lineDecoder.decode(chunk)) {
const sse = decoder.decode(line);
if (sse) yield sse;
}
}
// eslint-disable-next-line no-restricted-syntax
for (const line of lineDecoder.flush()) {
const sse = decoder.decode(line);
if (sse) yield sse;
}
}
export class SSEDecoder {
private data: string[];
private event: string | null;
private chunks: string[];
constructor() {
this.event = null;
this.data = [];
this.chunks = [];
}
decode(line: string) {
if (line.endsWith('\r')) {
// eslint-disable-next-line no-param-reassign
line = line.substring(0, line.length - 1);
}
if (!line) {
// empty line and we didn't previously encounter any messages
if (!this.event && !this.data.length) return null;
const sse: ServerSentEvent = {
event: this.event,
data: this.data.join('\n'),
raw: this.chunks,
};
this.event = null;
this.data = [];
this.chunks = [];
return sse;
}
this.chunks.push(line);
if (line.startsWith(':')) {
return null;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, prefer-const
let [fieldname, _, value] = partition(line, ':');
if (value.startsWith(' ')) {
value = value.substring(1);
}
if (fieldname === 'event') {
this.event = value;
} else if (fieldname === 'data') {
this.data.push(value);
}
return null;
}
}
/**
* A re-implementation of httpx's `LineDecoder` in Python that handles incrementally
* reading lines from text.
*
* https://github.com/encode/httpx/blob/920333ea98118e9cf617f246905d7b202510941c/httpx/_decoders.py#L258
*/
export class LineDecoder {
// prettier-ignore
static NEWLINE_CHARS = new Set(['\n', '\r', '\x0b', '\x0c', '\x1c', '\x1d', '\x1e', '\x85', '\u2028', '\u2029']);
// eslint-disable-next-line no-control-regex
static NEWLINE_REGEXP = /\r\n|[\n\r\x0b\x0c\x1c\x1d\x1e\x85\u2028\u2029]/g;
buffer: string[];
trailingCR: boolean;
textDecoder: any; // TextDecoder found in browsers; not typed to avoid pulling in either "dom" or "node" types.
constructor() {
this.buffer = [];
this.trailingCR = false;
}
decode(chunk: Bytes): string[] {
let text = this.decodeText(chunk);
if (this.trailingCR) {
text = `\r${text}`;
this.trailingCR = false;
}
if (text.endsWith('\r')) {
this.trailingCR = true;
text = text.slice(0, -1);
}
if (!text) {
return [];
}
const trailingNewline = LineDecoder.NEWLINE_CHARS.has(
text[text.length - 1] || '',
);
let lines = text.split(LineDecoder.NEWLINE_REGEXP);
if (lines.length === 1 && !trailingNewline) {
this.buffer.push(lines[0]!);
return [];
}
if (this.buffer.length > 0) {
lines = [this.buffer.join('') + lines[0], ...lines.slice(1)];
this.buffer = [];
}
if (!trailingNewline) {
this.buffer = [lines.pop() || ''];
}
return lines;
}
decodeText(bytes: Bytes): string {
if (bytes == null) return '';
if (typeof bytes === 'string') return bytes;
// Node:
if (typeof Buffer !== 'undefined') {
if (bytes instanceof Buffer) {
return bytes.toString();
}
if (bytes instanceof Uint8Array) {
return Buffer.from(bytes).toString();
}
throw new OpenAIError(
`Unexpected: received non-Uint8Array (${bytes.constructor.name}) stream chunk in an environment with a global "Buffer" defined, which this library assumes to be Node. Please report this error.`,
);
}
// Browser
if (typeof TextDecoder !== 'undefined') {
if (bytes instanceof Uint8Array || bytes instanceof ArrayBuffer) {
this.textDecoder ??= new TextDecoder('utf8');
return this.textDecoder.decode(bytes);
}
throw new OpenAIError(
`Unexpected: received non-Uint8Array/ArrayBuffer (${
(bytes as any).constructor.name
}) in a web platform. Please report this error.`,
);
}
throw new OpenAIError(
`Unexpected: neither Buffer nor TextDecoder are available as globals. Please report this error.`,
);
}
flush(): string[] {
if (!this.buffer.length && !this.trailingCR) {
return [];
}
const lines = [this.buffer.join('')];
this.buffer = [];
this.trailingCR = false;
return lines;
}
}
function partition(str: string, delimiter: string): [string, string, string] {
const index = str.indexOf(delimiter);
if (index !== -1) {
return [
str.substring(0, index),
delimiter,
str.substring(index + delimiter.length),
];
}
return [str, '', ''];
}
/**
* Most browsers don't yet have async iterable support for ReadableStream,
* and Node has a very different way of reading bytes from its "ReadableStream".
*
* This polyfill was pulled from https://github.com/MattiasBuelens/web-streams-polyfill/pull/122#issuecomment-1627354490
*/
export function readableStreamAsyncIterable<T>(
stream: any,
): AsyncIterableIterator<T> {
if (stream[Symbol.asyncIterator]) return stream;
const reader = stream.getReader();
return {
async next() {
try {
const result = await reader.read();
if (result?.done) reader.releaseLock(); // release lock when stream becomes closed
return result;
} catch (e) {
reader.releaseLock(); // release lock when stream becomes errored
throw e;
}
},
async return() {
const cancelPromise = reader.cancel();
reader.releaseLock();
await cancelPromise;
return { done: true, value: undefined };
},
[Symbol.asyncIterator]() {
return this;
},
};
}
+12
View File
@@ -0,0 +1,12 @@
export const castToError = (err: any): Error => {
if (err instanceof Error) return err;
return new Error(err);
};
export function ensureArray<T>(value: T | T[]): T[] {
return Array.isArray(value) ? value : [value];
}
export function random(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
+80
View File
@@ -0,0 +1,80 @@
import type { Agent } from 'node:http';
import {
APIClient,
type DefaultQuery,
type Fetch,
type Headers,
} from 'openai/core';
import * as API from './resources';
export interface VYroAIOptions {
baseURL?: string;
apiKey?: string;
timeout?: number | undefined;
httpAgent?: Agent;
apiType?: (string & NonNullable<unknown>) | 'api';
fetch?: Fetch | undefined;
defaultHeaders?: Headers;
defaultQuery?: DefaultQuery;
}
export class VYroAI extends APIClient {
public apiType: (string & NonNullable<unknown>) | 'api';
protected apiKey: string;
private _options: VYroAIOptions;
constructor(options: VYroAIOptions = {}) {
const {
apiKey = process.env.VYRO_API_KEY || '',
apiType = process.env.VYRO_API_TYPE || 'api',
baseURL = 'https://api.vyro.ai/v1',
timeout = 30000,
httpAgent = undefined,
...rest
} = options;
super({
baseURL,
timeout,
fetch,
httpAgent,
...rest,
});
this._options = options;
this.apiKey = apiKey;
this.apiType = apiType;
}
images = new API.Images(this);
protected override authHeaders() {
return {
Authorization: `Bearer ${this.apiKey}`,
};
}
protected override defaultHeaders(): Headers {
return {
...this.authHeaders(),
...this._options.defaultHeaders,
};
}
protected override defaultQuery(): DefaultQuery | undefined {
return this._options.defaultQuery;
}
}
// eslint-disable-next-line no-redeclare
export namespace VYroAI {
export type Images = API.Images;
export type ImageGenerateParams = API.ImageGenerateParams;
}
export default VYroAI;
+9
View File
@@ -0,0 +1,9 @@
import type { VYroAI } from './index';
export class APIResource {
protected _client: VYroAI;
constructor(client: VYroAI) {
this._client = client;
}
}
+610
View File
@@ -0,0 +1,610 @@
import { ReadableStream } from 'node:stream/web';
import { type RequestOptions, type Uploadable } from 'openai/core';
import { toFile } from 'openai/uploads';
import { random } from '../../util';
import { APIResource } from '../resource';
export class Images extends APIResource {
protected models: Record<ImageModel, number> = {
'imagine-v5': 33,
'anime-v5': 34,
'imagine-v4.1': 32,
'imagine-v4': 31,
'imagine-v3': 30,
'imagine-v1': 28,
realistic: 29,
anime: 21,
portrait: 26,
'sdxl-1.0': 122,
};
/**
* Creates a variation of a given image.
*/
async createVariation(
params: ImageCreateVariationParams,
options?: RequestOptions,
): Promise<ImagesResponse> {
const client = this._client;
const formData = new FormData();
const { model, style = this.models[model ?? 'realistic'] } = params;
// @ts-expect-error
formData.append('image', await toFile(params.image));
formData.append('style_id', (style || 29).toString());
formData.append('prompt', params.prompt);
formData.append('negative_prompt', params.negative_prompt || '');
formData.append('strength', (params.strength || 0).toString());
formData.append('steps', (params.steps || 30).toString());
formData.append('cfg', (params.cfg || 7.5).toString());
formData.append('seed', (params.seed || random(1, 1000000)).toString());
const response: Response = await client.post(
`/imagine/${client.apiType}/generations/variations`,
{
...options,
body: {
body: formData,
[Symbol.toStringTag]: 'MultipartBody',
},
__binaryResponse: true,
},
);
return {
data: [
{
binary: response.body as unknown as ReadableStream,
},
],
created: Math.floor(Date.now() / 1000),
};
}
/**
* Experience the magic of Imagine's Image Remix feature, designed to breathe new life into your existing images.
*/
async edit(
params: ImageEditParams,
options?: RequestOptions,
): Promise<ImagesResponse> {
const client = this._client;
const formData = new FormData();
const { model, style = this.models[model ?? 'realistic'] } = params;
// @ts-expect-error
formData.append('image', await toFile(params.image));
formData.append('style_id', (style || 29).toString());
formData.append('prompt', params.prompt);
formData.append('negative_prompt', params.negative_prompt || '');
formData.append('strength', (params.strength || 0).toString());
formData.append('control', params.control || 'openpose');
formData.append('steps', (params.steps || 30).toString());
formData.append('cfg', (params.cfg || 7.5).toString());
formData.append('seed', (params.seed || random(1, 1000000)).toString());
const response: Response = await client.post(
`/imagine/${client.apiType}/edits/remix`,
{
...options,
body: {
body: formData,
[Symbol.toStringTag]: 'MultipartBody',
},
__binaryResponse: true,
},
);
return {
data: [
{
binary: response.body as unknown as ReadableStream,
},
],
created: Math.floor(Date.now() / 1000),
};
}
/**
* Creates an image given a prompt.
*/
async generate(
params: ImageGenerateParams,
options?: RequestOptions,
): Promise<ImagesResponse> {
const client = this._client;
const formData = new FormData();
const { model, style = this.models[model ?? 'imagine-v4'] } = params;
formData.append('style_id', (style || 30).toString());
formData.append('prompt', params.prompt);
formData.append('negative_prompt', params.negative_prompt || '');
formData.append('aspect_ratio', params.aspect_ratio || '1:1');
formData.append('steps', (params.steps || 30).toString());
formData.append('cfg', (params.cfg || 7.5).toString());
formData.append('seed', (params.seed || random(1, 1000000)).toString());
formData.append('high_res_results', params.quality === 'hd' ? '1' : '0');
const response: Response = await client.post(
`/imagine/${client.apiType}/generations`,
{
...options,
body: {
body: formData,
[Symbol.toStringTag]: 'MultipartBody',
},
__binaryResponse: true,
},
);
return {
created: Math.floor(Date.now() / 1000),
data: [
{
binary: response.body as unknown as ReadableStream,
},
],
};
}
/**
* The image upscale feature provides a better image to the user by increasing its resolution.
*/
async upscale(
params: ImageUpscaleParams,
options?: RequestOptions,
): Promise<ImagesResponse> {
const client = this._client;
const formData = new FormData();
// @ts-expect-error
formData.append('image', await toFile(params.image));
const response: Response = await client.post(
`/imagine/${client.apiType}/upscale`,
{
...options,
body: {
body: formData,
[Symbol.toStringTag]: 'MultipartBody',
},
__binaryResponse: true,
},
);
return {
created: Math.floor(Date.now() / 1000),
data: [
{
binary: response.body as unknown as ReadableStream,
},
],
};
}
/**
* Inpaint is an advanced feature of the Text-to-Image Stable Diffusion pipeline.
* It allows users to remove unwanted objects or elements from an image by intelligently filling in the missing areas.
*/
async restoration(
params: ImageRestorationParams,
options?: RequestOptions,
): Promise<ImagesResponse> {
const client = this._client;
const formData = new FormData();
// @ts-expect-error
formData.append('image', await toFile(params.image));
// @ts-expect-error
formData.append('mask', await toFile(params.mask));
formData.append('style_id', '1');
formData.append('prompt', params.prompt);
formData.append('neg_prompt', params.negative_prompt || '');
formData.append('inpaint_strength', (params.strength || 0).toString());
formData.append('cfg', (params.cfg || 7.5).toString());
const response: Response = await client.post(
`/imagine/${client.apiType}/generations/variations`,
{
...options,
body: {
body: formData,
[Symbol.toStringTag]: 'MultipartBody',
},
__binaryResponse: true,
},
);
return {
data: [
{
binary: response.body as unknown as ReadableStream,
},
],
created: Math.floor(Date.now() / 1000),
};
}
}
export type ImageModel =
| 'imagine-v5'
| 'anime-v5'
| 'imagine-v4.1'
| 'imagine-v4'
| 'imagine-v3'
| 'imagine-v1'
| 'realistic'
| 'anime'
| 'portrait'
| 'sdxl-1.0';
export interface ImageRestorationParams {
/**
* The image to use as the basis for the variation(s). Must be a valid PNG file,
* less than 4MB, and square.
*/
image: Uploadable;
/**
* The mask indicating the areas to be inpainted.
*/
mask: Uploadable;
/**
* The text guides the image generation.
*/
prompt: string;
/**
* The model to use for image generation.
*/
model?: 'vyro-inpaint' | null;
/**
* The negative_prompt parameter empowers you to provide additional
* guidance to the AI by specifying what you don't want in the image.
* It helps refine the creative direction, ensuring that the generated
* content aligns with your intentions.
*/
negative_prompt?: string | null;
/**
* Specifies the model to be used. Currently supports only 1 for realism.
*
* @defaultValue 1
*/
style?: 1 | null;
/**
* Weightage to be given to text
*
* Range: 3 - 15
*
* @defaultValue 7.5
*/
cfg?: number | null;
/**
* Weightage given to initial image. Greater this parameter more the output will be close to starting image and far from prompt.
*
* Range: 0 - 1
*
* @defaultValue 0.5
*/
strength?: number | null;
/**
* 目前仅支持 binary 格式
*/
response_format?: 'binary' | null;
}
export interface ImageCreateVariationParams {
/**
* The image to use as the basis for the variation(s). Must be a valid PNG file,
* less than 4MB, and square.
*/
image: Uploadable;
/**
* The text guides the image generation.
*/
prompt: string;
/**
* The model to use for image generation.
*/
model?: ImageModel | null;
/**
* The negative_prompt parameter empowers you to provide additional
* guidance to the AI by specifying what you don't want in the image.
* It helps refine the creative direction, ensuring that the generated
* content aligns with your intentions.
*/
negative_prompt?: string | null;
/**
* The style_id parameter is like choosing an artistic palette for your image.
* By selecting a style id, you guide the AI in crafting the image with a particular visual aesthetic.
* Style IDs range from 1 to N, each representing a unique artistic style.
*
* @defaultValue 30
*/
style?: number | null;
/**
* The steps parameter defines the number of operations or iterations that the
* generator will perform during image creation. It can impact the complexity
* and detail of the generated image.
*
* Range: 30-50
*
* @defaultValue 30
*/
steps?: number | null;
/**
* The cfg parameter acts as a creative control knob.
* You can adjust it to fine-tune the level of artistic innovation in the image.
* Lower values encourage faithful execution of the prompt,
* while higher values introduce more creative and imaginative variations.
*
* Range: 3 - 15
*
* @defaultValue 7.5
*/
cfg?: number | null;
/**
* The seed parameter serves as the initial value for the random number generator.
* By setting a specific seed value, you can ensure that the AI generates the same
* image or outcome each time you use that exact seed.
*
* range: 1-Infinity
*/
seed?: number | null;
/**
* Influences the impact of the control image on output.
*
* Range: 0 - 1
*
* @defaultValue 0
*/
strength?: number | null;
/**
* 目前仅支持 binary 格式
*/
response_format?: 'binary' | null;
}
export interface ImageEditParams {
/**
* The image to use as the basis for the variation(s). Must be a valid PNG file,
* less than 4MB, and square.
*/
image: Uploadable;
/**
* The text guides the image generation.
*/
prompt: string;
/**
* The model to use for image generation.
*/
model?: ImageModel | null;
/**
* The negative_prompt parameter empowers you to provide additional
* guidance to the AI by specifying what you don't want in the image.
* It helps refine the creative direction, ensuring that the generated
* content aligns with your intentions.
*/
negative_prompt?: string | null;
/**
* The style_id parameter is like choosing an artistic palette for your image.
* By selecting a style id, you guide the AI in crafting the image with a particular visual aesthetic.
* Style IDs range from 1 to N, each representing a unique artistic style.
*
* @defaultValue 29
*/
style?: number | null;
/**
* The steps parameter defines the number of operations or iterations that the
* generator will perform during image creation. It can impact the complexity
* and detail of the generated image.
*
* Range: 30-50
*
* @defaultValue 30
*/
steps?: number | null;
/**
* The cfg parameter acts as a creative control knob.
* You can adjust it to fine-tune the level of artistic innovation in the image.
* Lower values encourage faithful execution of the prompt,
* while higher values introduce more creative and imaginative variations.
*
* Range: 3 - 15
*
* @defaultValue 7.5
*/
cfg?: number | null;
/**
* The seed parameter serves as the initial value for the random number generator.
* By setting a specific seed value, you can ensure that the AI generates the same
* image or outcome each time you use that exact seed.
*
* range: 1-Infinity
*/
seed?: number | null;
/**
* Influences the impact of the control image on output.
*
* Range: 0 - 1
*
* @defaultValue 0
*/
strength?: number | null;
/**
* The method/control used to guide image generation.
*
* @defaultValue openpose
*/
control?: 'openpose' | 'scribble' | 'canny' | 'lineart' | 'depth' | null;
/**
* 目前仅支持 binary 格式
*/
response_format?: 'binary' | null;
}
export interface ImageGenerateParams {
/**
* A prompt is the text input that guides the AI in generating visual content.
* It defines the textual description or concept for the image you wish to generate.
* Think of it as the creative vision you want the AI to bring to life.
* Crafting clear and creative prompts is crucial for achieving the desired results with Imagine's API.
* For example, A serene forest with a river under the moonlight, can be a prompt.
*/
prompt: string;
/**
* The model to use for image generation.
*/
model?: ImageModel | null;
/**
* The negative_prompt parameter empowers you to provide additional
* guidance to the AI by specifying what you don't want in the image.
* It helps refine the creative direction, ensuring that the generated
* content aligns with your intentions.
*/
negative_prompt?: string | null;
/**
* The aspect_ratio parameter allows you to specify the proportions and dimensions of the generated image.
* You can set it to different ratios like 1:1 for square images, 16:9 for widescreen, or 3:4 for vertical formats,
* shaping the visual composition to your liking.
*
* @defaultValue 1:1
*/
aspect_ratio?: '1:1' | '3:2' | '4:3' | '3:4' | '16:9' | '9:16' | null;
/**
* The quality parameter is a flag that, when set to hd,
* requests high-resolution results from the AI.
*
* @defaultValue standard
*/
quality?: 'standard' | 'hd';
/**
* The style_id parameter is like choosing an artistic palette for your image.
* By selecting a style id, you guide the AI in crafting the image with a particular visual aesthetic.
* Style IDs range from 1 to N, each representing a unique artistic style.
*
* @defaultValue 30
*/
style?: number | null;
/**
* The steps parameter defines the number of operations or iterations that the
* generator will perform during image creation. It can impact the complexity
* and detail of the generated image.
*
* Range: 30-50
*
* @defaultValue 30
*/
steps?: number | null;
/**
* The cfg parameter acts as a creative control knob.
* You can adjust it to fine-tune the level of artistic innovation in the image.
* Lower values encourage faithful execution of the prompt,
* while higher values introduce more creative and imaginative variations.
*
* Range: 3 - 15
*
* @defaultValue 7.5
*/
cfg?: number | null;
/**
* The seed parameter serves as the initial value for the random number generator.
* By setting a specific seed value, you can ensure that the AI generates the same
* image or outcome each time you use that exact seed.
*
* range: 1-Infinity
*/
seed?: number | null;
/**
* 目前仅支持 binary 格式
*/
response_format?: 'binary' | null;
}
export interface ImageUpscaleParams {
/**
* The image to use as the basis for the variation(s). Must be a valid PNG file,
* less than 4MB, and square.
*/
image: Uploadable;
}
export interface Image {
/**
* The binary of the generated image.
*/
binary?: ReadableStream;
/**
* The base64-encoded JSON of the generated image, if `response_format` is
* `b64_json`.
*/
b64_json?: string;
/**
* The prompt that was used to generate the image, if there was any revision to the
* prompt.
*/
revised_prompt?: string;
/**
* The URL of the generated image, if `response_format` is `url` (default).
*/
url?: string;
}
export interface ImagesResponse {
/**
* When the request was made.
*/
created: number;
/**
* The generated images.
*/
data: Image[];
}
@@ -0,0 +1 @@
export { Images, type ImageGenerateParams } from './images';
+40
View File
@@ -1,6 +1,7 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import socketIo from 'socket.io';
export const getTempPath = () => {
const tempDir = os.tmpdir();
@@ -10,3 +11,42 @@ export const getTempPath = () => {
}
return logDir;
};
export async function emitAndWait<T>(
io: socketIo.Server,
event: string,
data?: any,
timeout: number = 5000,
): Promise<any> {
let response;
if (data === undefined) {
response = await io.timeout(timeout).emitWithAck(event);
} else {
response = await io.timeout(timeout).emitWithAck(event, data);
}
// 判断是否是 Array
if (Array.isArray(response) && response.length === 1) {
if (typeof response[0] === 'undefined') {
return {} as T;
}
if (response[0] === 'string') {
return {} as T;
}
// 如果 response[0] 也是数组,那么直接返回
if (Array.isArray(response[0])) {
return response[0] as T;
}
return JSON.parse(response[0]);
}
// 判断是否是字符串
if (typeof response === 'string') {
return JSON.parse(response);
}
return response;
}
+95
View File
@@ -0,0 +1,95 @@
/**
* 范围匹配
* @param ptt 范围查询关键词 e.g. 'hello [and] world'
* @param msg 消息
* @returns
*/
export function rangeMatch(ptt: string, msg: string): boolean {
if (ptt.includes('[and]')) {
const keywords = ptt.split('[and]');
return keywords.every((keyword) => matchKeyword(keyword.trim(), msg));
}
return matchKeyword(ptt, msg);
}
/**
* 匹配关键词
* @param ptt 匹配模式
* @param msg 消息
* @returns
*/
export function matchKeyword(ptt: string, msg: string): boolean {
let pattern = ptt.trim();
// 如果模式只是一个星号,它应该匹配任何消息。
if (pattern === '*') {
return true;
}
// 合并连续的 '*' 字符为一个 '*'
pattern = pattern.replace(/\*+/g, '*');
// 如果模式不包含 '*',则直接比较是否相等
if (!pattern.includes('*')) {
return pattern === msg;
}
const parts = pattern.split('*');
let lastIndex = 0;
// eslint-disable-next-line no-restricted-syntax
for (const part of parts) {
// 跳过空字符串(它们来自模式开始、结束或连续 '*'
if (part === '') continue;
const index = msg.indexOf(part, lastIndex);
// 如果找不到部分或部分不是按顺序出现,则匹配失败
if (index === -1 || index < lastIndex) {
return false;
}
lastIndex = index + part.length;
}
// 确保消息的剩余部分可以被模式尾部的 '*' 匹配
return parts[parts.length - 1] === '' || lastIndex <= msg.length;
}
/**
* 替换文本中的 token
* @param text 原始文本
* @param replacements 替换文本
* @returns
*/
export function tokenReplace(
text: string,
replacements: Record<string, string>,
): string {
return text.replace(/\{(\w+)\}/g, (match, key) => {
return replacements[key] || match;
});
}
/**
* 替换特殊 token
* @param text 原始文本
* @returns
*/
export function specialTokenReplace(text: string): string {
// eslint-disable-next-line no-useless-escape
return text.replace(/\[\~\]/g, () => {
const randomChoices = [
...'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
'🌸',
'😊',
'🌷',
'🌹',
'💖',
'🪷',
'💐',
'🌺',
'🌼',
'🌻',
];
return randomChoices[Math.floor(Math.random() * randomChoices.length)];
});
}