Compare commits

...

13 Commits

Author SHA1 Message Date
d9385e3c4e remove submodules 2025-10-27 19:50:46 +08:00
0c885e9012 "chore: 升级 Rollup 版本并优化构建配置" 2025-05-26 02:58:40 +08:00
6ced3676ab 迁移内容 2025-05-25 14:01:14 +08:00
8d2401ea30 fix 2025-05-24 10:03:35 +08:00
2369417961 update for baseURL 2025-05-17 15:55:41 +08:00
2338242018 feat: add chatStream 2025-05-09 19:50:32 +08:00
4a2d3e8d32 test 2025-04-30 18:53:45 +08:00
b07bcc5454 test for functions tools 2025-04-30 16:06:35 +08:00
abac483610 "chore: bump version and update dependencies" 2025-04-26 23:02:35 +08:00
6376773e13 test 2025-04-26 03:34:29 +08:00
0804bb9f2b Merge branch 'main' of git.xiongxiao.me:kevisual/ai-center 2025-04-26 03:32:14 +08:00
e0df4e490c Merge branch 'main' of git.xiongxiao.me:kevisual/ai-center 2025-04-26 03:31:44 +08:00
ac207ff374 feat: add deploy for ai-provider 2025-04-26 03:28:55 +08:00
37 changed files with 870 additions and 3019 deletions

View File

@@ -1,2 +0,0 @@
config.json
.env*

2
.gitignore vendored
View File

@@ -19,3 +19,5 @@ logs
config.json config.json
pack-dist pack-dist
videos/output*

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "submodules/query-config"]
path = submodules/query-config
url = git@git.xiongxiao.me:kevisual/kevisual-query-config.git

37
bun.config.mjs Normal file
View File

@@ -0,0 +1,37 @@
// @ts-check
import { resolvePath } from '@kevisual/use-config/env';
import { execSync } from 'node:child_process';
const entry = 'src/index.ts';
const naming = 'app';
const external = ['sequelize', 'pg', 'sqlite3', 'ioredis', 'pm2'];
/**
* @type {import('bun').BuildConfig}
*/
await Bun.build({
target: 'node',
format: 'esm',
entrypoints: [resolvePath(entry, { meta: import.meta })],
outdir: resolvePath('./dist', { meta: import.meta }),
naming: {
entry: `${naming}.js`,
},
external,
env: 'KEVISUAL_*',
});
// const cmd = `dts -i src/index.ts -o app.d.ts`;
// const cmd = `dts -i ${entry} -o ${naming}.d.ts`;
// execSync(cmd, { stdio: 'inherit' });
await Bun.build({
target: 'node',
format: 'esm',
entrypoints: [resolvePath('./src/run.ts', { meta: import.meta })],
outdir: resolvePath('./dist', { meta: import.meta }),
naming: {
entry: `${'run'}.js`,
},
external,
env: 'KEVISUAL_*',
});

View File

@@ -1,83 +1,70 @@
{ {
"name": "@kevisual/ai-center-services", "name": "@kevisual/ai-center-services",
"version": "0.0.1", "version": "0.0.5",
"description": "", "description": "后面需要把ai-center的provider模块提取出去",
"main": "index.js", "main": "index.js",
"basename": "/root/ai-center-services", "basename": "/root/ai-center-services",
"app": { "app": {
"entry": "dist/app.mjs", "entry": "dist/app.js",
"key": "ai-center-services", "key": "ai-center-services",
"type": "system-app" "type": "system-app",
"runtime": [
"client",
"server"
]
}, },
"files": [ "files": [
"dist", "dist",
"types" "types"
], ],
"scripts": { "scripts": {
"watch": "rollup -c rollup.config.mjs -w", "build": "npm run clean && bun bun.config.mjs",
"build": "rollup -c rollup.config.mjs", "dev": "bun run --watch bun.config.mjs",
"dev": "cross-env NODE_ENV=development nodemon --delay 2.5 -e js,cjs,mjs --exec node dist/app.mjs",
"test": "tsx test/**/*.ts",
"dev:watch": "cross-env NODE_ENV=development concurrently -n \"Watch,Dev\" -c \"green,blue\" \"npm run watch\" \"sleep 1 && npm run dev\" ",
"clean": "rm -rf dist", "clean": "rm -rf dist",
"pub": "envision pack -p -u" "pub": "envision pack -p -u"
}, },
"keywords": [], "keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)", "author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT", "license": "MIT",
"packageManager": "pnpm@10.7.1", "packageManager": "pnpm@10.19.0",
"type": "module", "type": "module",
"dependencies": { "publishConfig": {
"@kevisual/cache": "^0.0.2", "registry": "https://registry.npmjs.org/",
"@kevisual/permission": "^0.0.1", "access": "public"
"@kevisual/router": "0.0.10",
"pino-pretty": "^13.0.0"
}, },
"devDependencies": { "devDependencies": {
"@kevisual/code-center-module": "0.0.18", "@kevisual/ai": "^0.0.11",
"@kevisual/code-center-module": "0.0.24",
"@kevisual/mark": "0.0.7", "@kevisual/mark": "0.0.7",
"@kevisual/query": "^0.0.15", "@kevisual/permission": "^0.0.3",
"@kevisual/query-config": "workspace:*", "@kevisual/query": "^0.0.29",
"@kevisual/types": "^0.0.6", "@kevisual/query-config": "^0.0.2",
"@kevisual/use-config": "^1.0.10", "@kevisual/router": "0.0.30",
"@rollup/plugin-alias": "^5.1.1", "@kevisual/types": "^0.0.10",
"@rollup/plugin-commonjs": "^28.0.3", "@kevisual/use-config": "^1.0.19",
"@rollup/plugin-json": "^6.1.0", "@types/bun": "^1.3.1",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-replace": "^6.0.2",
"@rollup/plugin-typescript": "^12.1.2",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/formidable": "^3.4.5", "@types/formidable": "^3.4.6",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^22.14.0", "@types/node": "^24.9.1",
"@vitejs/plugin-basic-ssl": "^2.0.0",
"concurrently": "^9.1.2",
"cookie": "^1.0.2", "cookie": "^1.0.2",
"cross-env": "^7.0.3", "cross-env": "^10.1.0",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.18",
"dotenv": "^16.4.7", "dotenv": "^17.2.3",
"formidable": "^3.5.2", "formidable": "^3.5.4",
"ioredis": "^5.6.0", "ioredis": "^5.8.2",
"jsrepo": "^1.45.3",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"nanoid": "^5.1.5", "nanoid": "^5.1.6",
"nodemon": "^3.1.9", "openai": "6.7.0",
"openai": "^4.91.1",
"pg": "^8.14.1",
"pg-hstore": "^2.3.4", "pg-hstore": "^2.3.4",
"pino": "^9.6.0", "pm2": "^6.0.13",
"pm2": "^6.0.5",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"rollup": "^4.39.0",
"rollup-plugin-copy": "^3.5.0",
"rollup-plugin-dts": "^6.2.1",
"rollup-plugin-esbuild": "^6.2.1",
"sequelize": "^6.37.7", "sequelize": "^6.37.7",
"tape": "^5.9.0", "tape": "^5.9.0",
"tiktoken": "^1.0.20", "tiktoken": "^1.0.22"
"tsx": "^4.19.3", },
"typescript": "^5.8.2", "dependencies": {
"vite": "^6.2.5" "@kevisual/logger": "^0.0.4"
} }
} }

3110
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,79 +0,0 @@
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import path from 'path';
import esbuild from 'rollup-plugin-esbuild';
import alias from '@rollup/plugin-alias';
import replace from '@rollup/plugin-replace';
import pkgs from './package.json' with {type: 'json'};
const isDev = process.env.NODE_ENV === 'development';
const input = isDev ? './src/dev.ts' : './src/index.ts';
/**
* @type {import('rollup').RollupOptions}
*/
const config = {
input,
output: {
dir: './dist',
entryFileNames: 'app.mjs',
chunkFileNames: '[name]-[hash].mjs',
format: 'esm',
},
plugins: [
replace({
preventAssignment: true, // 防止意外赋值
DEV_SERVER: JSON.stringify(isDev), // 替换 process.env.NODE_ENV
// VERSION: JSON.stringify(pkgs.version),
}),
alias({
// only esbuild needs to be configured
entries: [
{ find: '@', replacement: path.resolve('src') }, // 配置 @ 为 src 目录
{ find: 'http', replacement: 'node:http' },
{ find: 'https', replacement: 'node:https' },
{ find: 'fs', replacement: 'node:fs' },
{ find: 'path', replacement: 'node:path' },
{ find: 'crypto', replacement: 'node:crypto' },
{ find: 'zlib', replacement: 'node:zlib' },
{ find: 'stream', replacement: 'node:stream' },
{ find: 'net', replacement: 'node:net' },
{ find: 'tty', replacement: 'node:tty' },
{ find: 'tls', replacement: 'node:tls' },
{ find: 'buffer', replacement: 'node:buffer' },
{ find: 'timers', replacement: 'node:timers' },
// { find: 'string_decoder', replacement: 'node:string_decoder' },
{ find: 'dns', replacement: 'node:dns' },
{ find: 'domain', replacement: 'node:domain' },
{ find: 'os', replacement: 'node:os' },
{ find: 'events', replacement: 'node:events' },
{ find: 'url', replacement: 'node:url' },
{ find: 'assert', replacement: 'node:assert' },
{ find: 'util', replacement: 'node:util' },
],
}),
resolve({
preferBuiltins: true, // 强制优先使用内置模块
browser: false,
}),
commonjs(),
esbuild({
target: 'node22', //
minify: false, // 启用代码压缩
tsconfig: 'tsconfig.json',
}),
json(),
],
external: [
/@kevisual\/router(\/.*)?/, //, // 路由
/@kevisual\/use-config(\/.*)?/, //
'sequelize', // 数据库 orm
'ioredis', // redis
'pg', // pg
'pino', // pino
'pino-pretty', // pino-pretty
],
};
export default config;

View File

@@ -1,27 +1,10 @@
import { pino } from 'pino';
import { useConfig } from '@kevisual/use-config/env'; import { useConfig } from '@kevisual/use-config/env';
import { Logger } from '@kevisual/logger';
const config = useConfig(); const config = useConfig();
export const logger = pino({ export const logger = new Logger({
level: config.LOG_LEVEL || 'info', level: config.LOG_LEVEL || 'info',
transport: { showTime: true,
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname',
},
},
serializers: {
error: pino.stdSerializers.err,
req: pino.stdSerializers.req,
res: pino.stdSerializers.res,
},
base: {
app: 'ai-chat',
env: process.env.NODE_ENV || 'development',
},
}); });
export const logError = (message: string, data?: any) => logger.error({ data }, message); export const logError = (message: string, data?: any) => logger.error({ data }, message);

6
src/modules/logger.ts Normal file
View File

@@ -0,0 +1,6 @@
import { Logger } from '@kevisual/logger';
export const logger = new Logger({
level: process?.env?.LOG_LEVEL || 'info',
showTime: true,
});

View File

@@ -6,8 +6,9 @@ export type OllamaOptions = BaseChatOptions;
* 自定义模型 * 自定义模型
*/ */
export class Custom extends BaseChat { export class Custom extends BaseChat {
static BASE_URL = 'https://api.deepseek.com/v1/';
constructor(options: OllamaOptions) { constructor(options: OllamaOptions) {
const baseURL = options.baseURL || 'https://api.deepseek.com/v1/'; const baseURL = options.baseURL || Custom.BASE_URL;
super({ ...(options as BaseChatOptions), baseURL: baseURL }); super({ ...(options as BaseChatOptions), baseURL: baseURL });
} }
} }

View File

@@ -2,8 +2,9 @@ import { BaseChat, BaseChatOptions } from '../core/chat.ts';
export type DeepSeekOptions = Partial<BaseChatOptions>; export type DeepSeekOptions = Partial<BaseChatOptions>;
export class DeepSeek extends BaseChat { export class DeepSeek extends BaseChat {
static BASE_URL = 'https://api.deepseek.com/v1/';
constructor(options: DeepSeekOptions) { constructor(options: DeepSeekOptions) {
const baseURL = options.baseURL || 'https://api.deepseek.com/v1/'; const baseURL = options.baseURL || DeepSeek.BASE_URL;
super({ ...(options as BaseChatOptions), baseURL: baseURL }); super({ ...(options as BaseChatOptions), baseURL: baseURL });
} }
} }

View File

@@ -3,8 +3,9 @@ import { BaseChat, BaseChatOptions } from '../core/chat.ts';
export type ModelScopeOptions = Partial<BaseChatOptions>; export type ModelScopeOptions = Partial<BaseChatOptions>;
export class ModelScope extends BaseChat { export class ModelScope extends BaseChat {
static BASE_URL = 'https://api-inference.modelscope.cn/v1/';
constructor(options: ModelScopeOptions) { constructor(options: ModelScopeOptions) {
const baseURL = options.baseURL || 'https://api-inference.modelscope.cn/v1/'; const baseURL = options.baseURL || ModelScope.BASE_URL;
super({ ...options, baseURL: baseURL } as any); super({ ...options, baseURL: baseURL } as any);
} }
} }

View File

@@ -20,8 +20,9 @@ type OllamaModel = {
}; };
}; };
export class Ollama extends BaseChat { export class Ollama extends BaseChat {
static BASE_URL = 'http://localhost:11434/v1';
constructor(options: OllamaOptions) { constructor(options: OllamaOptions) {
const baseURL = options.baseURL || 'http://localhost:11434/v1'; const baseURL = options.baseURL || Ollama.BASE_URL;
super({ ...(options as BaseChatOptions), baseURL: baseURL }); super({ ...(options as BaseChatOptions), baseURL: baseURL });
} }
async chat(messages: ChatMessage[], options?: ChatMessageOptions) { async chat(messages: ChatMessage[], options?: ChatMessageOptions) {

View File

@@ -1,7 +1,7 @@
import { BaseChat, BaseChatOptions } from '../core/chat.ts'; import { BaseChat, BaseChatOptions } from '../core/chat.ts';
import { OpenAI } from 'openai'; import { OpenAI } from 'openai';
type SiliconFlowOptions = Partial<BaseChatOptions>; export type SiliconFlowOptions = Partial<BaseChatOptions>;
type SiliconFlowUsageData = { type SiliconFlowUsageData = {
id: string; id: string;
@@ -24,8 +24,9 @@ type SiliconFlowUsageResponse = {
data: SiliconFlowUsageData; data: SiliconFlowUsageData;
}; };
export class SiliconFlow extends BaseChat { export class SiliconFlow extends BaseChat {
static BASE_URL = 'https://api.siliconflow.cn/v1';
constructor(options: SiliconFlowOptions) { constructor(options: SiliconFlowOptions) {
const baseURL = options.baseURL || 'https://api.siliconflow.com/v1'; const baseURL = options.baseURL || SiliconFlow.BASE_URL;
super({ ...(options as BaseChatOptions), baseURL: baseURL }); super({ ...(options as BaseChatOptions), baseURL: baseURL });
} }
async getUsageInfo(): Promise<SiliconFlowUsageResponse> { async getUsageInfo(): Promise<SiliconFlowUsageResponse> {

View File

@@ -2,8 +2,9 @@ import { BaseChat, BaseChatOptions } from '../core/chat.ts';
export type VolcesOptions = Partial<BaseChatOptions>; export type VolcesOptions = Partial<BaseChatOptions>;
export class Volces extends BaseChat { export class Volces extends BaseChat {
static BASE_URL = 'https://ark.cn-beijing.volces.com/api/v3/';
constructor(options: VolcesOptions) { constructor(options: VolcesOptions) {
const baseURL = options.baseURL || 'https://ark.cn-beijing.volces.com/api/v3/'; const baseURL = options.baseURL || Volces.BASE_URL;
super({ ...(options as BaseChatOptions), baseURL: baseURL }); super({ ...(options as BaseChatOptions), baseURL: baseURL });
} }
} }

View File

@@ -1,5 +1,14 @@
import { OpenAI } from 'openai'; import { OpenAI } from 'openai';
import type { BaseChatInterface, ChatMessageComplete, ChatMessage, ChatMessageOptions, BaseChatUsageInterface } from './type.ts'; import type {
BaseChatInterface,
ChatMessageComplete,
ChatMessage,
ChatMessageOptions,
BaseChatUsageInterface,
ChatStream,
EmbeddingMessage,
EmbeddingMessageComplete,
} from './type.ts';
export type BaseChatOptions<T = Record<string, any>> = { export type BaseChatOptions<T = Record<string, any>> = {
/** /**
@@ -9,7 +18,7 @@ export type BaseChatOptions<T = Record<string, any>> = {
/** /**
* 默认模型 * 默认模型
*/ */
model: string; model?: string;
/** /**
* 默认apiKey * 默认apiKey
*/ */
@@ -87,7 +96,7 @@ export class BaseChat implements BaseChatInterface, BaseChatUsageInterface {
if (createParams.response_format) { if (createParams.response_format) {
throw new Error('response_format is not supported in stream mode'); throw new Error('response_format is not supported in stream mode');
} }
return this.openai.chat.completions.create(createParams) as any; return this.openai.chat.completions.create(createParams) as unknown as ChatStream;
} }
/** /**
@@ -107,4 +116,28 @@ export class BaseChat implements BaseChatInterface, BaseChatUsageInterface {
completion_tokens: this.completion_tokens, completion_tokens: this.completion_tokens,
}; };
} }
getHeaders(headers?: Record<string, string>) {
return {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
...headers,
};
}
/**
* 生成embedding 内部
* @param text
* @returns
*/
async generateEmbeddingCore(text: string | string[], options?: EmbeddingMessage): Promise<EmbeddingMessageComplete> {
const embeddingModel = options?.model || this.model;
const res = await this.openai.embeddings.create({
model: embeddingModel,
input: text,
encoding_format: 'float',
...options,
});
this.prompt_tokens += res.usage.prompt_tokens;
this.total_tokens += res.usage.total_tokens;
return res;
}
} }

View File

@@ -1,2 +1,27 @@
export * from './chat.ts'; import { ChatStream } from './type.ts';
export * from './type.ts';
// export type { BaseChat, BaseChatOptions } from './chat.ts';
export * from './chat.ts'
// export {
// ChatMessage,
// ChatMessageOptions, //
// ChatMessageComplete,
// ChatMessageStream,
// BaseChatInterface,
// BaseChatUsageInterface,
// ChatStream,
// EmbeddingMessage,
// EmbeddingMessageComplete,
// } from './type.ts';
export * from './type.ts'
/**
* for await (const chunk of chatStream) {
* console.log(chunk);
* }
* @param chatStream
*/
export const readStream = async (chatStream: ChatStream) => {
for await (const chunk of chatStream) {
console.log(chunk);
}
};

View File

@@ -1,10 +1,12 @@
import OpenAI from 'openai'; import OpenAI from 'openai';
export type ChatMessage = OpenAI.Chat.Completions.ChatCompletionMessageParam ; export type ChatMessage = OpenAI.Chat.Completions.ChatCompletionMessageParam;
export type ChatMessageOptions = Partial<OpenAI.Chat.Completions.ChatCompletionCreateParams>; export type ChatMessageOptions = Partial<OpenAI.Chat.Completions.ChatCompletionCreateParams>;
export type ChatMessageComplete = OpenAI.Chat.Completions.ChatCompletion; export type ChatMessageComplete = OpenAI.Chat.Completions.ChatCompletion;
export type ChatMessageStream = OpenAI.Chat.Completions.ChatCompletion; export type ChatMessageStream = OpenAI.Chat.Completions.ChatCompletion;
export type EmbeddingMessage = Partial<OpenAI.Embeddings.EmbeddingCreateParams>;
export type EmbeddingMessageComplete = OpenAI.Embeddings.CreateEmbeddingResponse;
export interface BaseChatInterface { export interface BaseChatInterface {
chat(messages: ChatMessage[], options?: ChatMessageOptions): Promise<ChatMessageComplete>; chat(messages: ChatMessage[], options?: ChatMessageOptions): Promise<ChatMessageComplete>;
} }
@@ -23,3 +25,5 @@ export interface BaseChatUsageInterface {
*/ */
completion_tokens: number; completion_tokens: number;
} }
export type ChatStream = AsyncGenerator<ChatMessageComplete, void, unknown>;

View File

@@ -40,13 +40,15 @@ export class ProviderManager {
if (!Provider) { if (!Provider) {
throw new Error(`Provider ${provider} not found`); throw new Error(`Provider ${provider} not found`);
} }
console.log('pm', 'Provider', ProviderMap[provider]); const providerConfig = {
this.provider = new Provider({
model, model,
apiKey, apiKey,
baseURL, baseURL,
}); };
if (!providerConfig.baseURL) {
delete providerConfig.baseURL;
}
this.provider = new Provider(providerConfig);
} }
static async createProvider(config: ProviderManagerConfig) { static async createProvider(config: ProviderManagerConfig) {
if (!config.baseURL) { if (!config.baseURL) {

View File

@@ -1,5 +1,5 @@
import { BaseChat, BaseChatOptions } from '../core/chat.ts'; import { BaseChat, BaseChatOptions } from '../core/chat.ts';
import { numTokensFromString } from '../utils/token.ts'; import { EmbeddingMessage } from '../core/type.ts';
export type KnowledgeOptions<T = Record<string, string>> = BaseChatOptions< export type KnowledgeOptions<T = Record<string, string>> = BaseChatOptions<
{ {
@@ -29,49 +29,28 @@ export class KnowledgeBase extends BaseChat {
this.total_tokens = 0; this.total_tokens = 0;
this.batchSize = options.batchSize || 4; this.batchSize = options.batchSize || 4;
} }
/**
* 生成embedding 内部
* @param text
* @returns
*/
async generateEmbeddingCore(text: string | string[]) {
const res = await this.openai.embeddings.create({
model: this.embeddingModel,
input: text,
encoding_format: 'float',
});
this.prompt_tokens += res.usage.prompt_tokens;
this.total_tokens += res.usage.total_tokens;
return res;
}
async generateEmbeddingBatchCore(text: string[]) {
const res = await this.openai.embeddings.create({
model: this.embeddingModel,
input: text,
encoding_format: 'float',
});
this.prompt_tokens += res.usage.prompt_tokens;
this.total_tokens += res.usage.total_tokens;
return res.data.map((item) => item.embedding);
}
/** /**
* 生成embedding * 生成embedding
* @param text * @param text
* @returns * @returns
*/ */
async generateEmbedding(text: string | string[]) { async generateEmbedding(text: string | string[]) {
if (Array.isArray(text)) { try {
// size token 不能超过 8192 const res = await this.generateEmbeddingCore(text, { model: this.embeddingModel });
const allSize = text.reduce((acc, item) => acc + numTokensFromString(item), 0); return { code: 200, data: res.data };
if (allSize > 8192) { } catch (error) {
throw new Error('text size 不能超过 8192'); const has413 = error?.message?.includes('413');
if (has413) {
return {
code: 413,
message: '请求过大,请分割文本',
};
} }
return {
code: error?.code || 500,
message: '生成embedding失败',
};
} }
const res = await this.generateEmbeddingCore(text);
if (Array.isArray(text)) {
return res.data.map((item) => item.embedding);
}
return [res.data[0].embedding];
} }
/** /**
* 批量生成embedding * 批量生成embedding
@@ -83,8 +62,10 @@ export class KnowledgeBase extends BaseChat {
const embeddings: number[][] = []; const embeddings: number[][] = [];
for (let i = 0; i < textArray.length; i += batchSize) { for (let i = 0; i < textArray.length; i += batchSize) {
const batch = textArray.slice(i, i + batchSize); const batch = textArray.slice(i, i + batchSize);
const res = await this.generateEmbeddingBatchCore(batch); const res = await this.generateEmbedding(batch);
embeddings.push(...res); if (res.code === 200) {
embeddings.push(...res.data.map((item) => item.embedding));
}
} }
return embeddings; return embeddings;
} }

View File

@@ -0,0 +1 @@
export * from './video/siliconflow.ts';

View File

@@ -0,0 +1,37 @@
import { SiliconFlow } from './../../chat-adapter/siliconflow.ts';
export class VideoSiliconFlow extends SiliconFlow {
constructor(opts: any) {
super(opts);
}
async uploadAudioVoice(audioBase64: string | Blob | File) {
const pathname = 'uploads/audio/voice';
const url = `${this.baseURL}/${pathname}`;
const headers = {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${this.apiKey}`,
};
const formData = new FormData();
// formData.append('audio', 'data:audio/mpeg;base64,aGVsbG93b3JsZA==');
// formData.append('audio', audioBase64);
formData.append('file', audioBase64);
formData.append('model', 'FunAudioLLM/CosyVoice2-0.5B');
formData.append('customName', 'test_name');
formData.append('text', '在一无所知中, 梦里的一天结束了,一个新的轮回便会开始');
const res = await fetch(url, {
method: 'POST',
headers,
body: formData,
}).then((res) => res.json());
console.log('uploadAudioVoice', res);
}
async audioSpeech() {
this.openai.audio.speech.create({
model: 'FunAudioLLM/CosyVoice2-0.5B',
voice: 'alloy',
input: '在一无所知中, 梦里的一天结束了,一个新的轮回便会开始',
response_format: 'mp3',
});
}
}

View File

@@ -1,6 +1,8 @@
import { Permission } from '@kevisual/permission'; import { Permission } from '@kevisual/permission';
import CryptoJS from 'crypto-js'; import AES from 'crypto-js/aes.js';
import Utf8 from 'crypto-js/enc-utf8.js';
const CryptoJS = { AES, enc: { Utf8 } };
// 加密函数 // 加密函数
export function encryptAES(plainText: string, secretKey: string) { export function encryptAES(plainText: string, secretKey: string) {
return CryptoJS.AES.encrypt(plainText, secretKey).toString(); return CryptoJS.AES.encrypt(plainText, secretKey).toString();
@@ -58,7 +60,7 @@ export type GetProviderOpts = {
export type ProviderResult = { export type ProviderResult = {
provider: string; provider: string;
model: string; model: string;
group: string; group?: string;
apiKey: string; apiKey: string;
dayLimit?: number; dayLimit?: number;
tokenLimit?: number; tokenLimit?: number;
@@ -72,7 +74,7 @@ export type ProviderResult = {
export type AIConfig = { export type AIConfig = {
title?: string; title?: string;
description?: string; description?: string;
models: AIModel[]; models?: AIModel[];
secretKeys: SecretKey[]; secretKeys: SecretKey[];
permission?: Permission; permission?: Permission;
filter?: { filter?: {
@@ -183,4 +185,22 @@ export class AIConfigParser {
}; };
}); });
} }
getConfig(keepSecret?: boolean, config?: AIConfig) {
const chatConfig = config ?? this.config;
if (keepSecret) {
return chatConfig;
}
// 过滤掉secret中的所有apiKey移除掉并返回chatConfig
const { secretKeys = [], ...rest } = chatConfig || {};
return {
...rest,
secretKeys: secretKeys.map((item) => {
return {
...item,
apiKey: undefined,
decryptKey: undefined,
};
}),
};
}
} }

View File

@@ -19,6 +19,7 @@ app
let { username, model, group, getFull = false } = ctx.query; let { username, model, group, getFull = false } = ctx.query;
const tokenUser = ctx.state.tokenUser || {}; const tokenUser = ctx.state.tokenUser || {};
const tokenUsername = tokenUser.username; const tokenUsername = tokenUser.username;
const token = ctx.query.token || ctx.state.token;
const options = ctx.query.options || {}; const options = ctx.query.options || {};
let aiChatHistory: AiChatHistoryModel; let aiChatHistory: AiChatHistoryModel;
if (id) { if (id) {
@@ -46,6 +47,7 @@ app
model, model,
group, group,
username: tokenUsername, username: tokenUsername,
token,
}); });
if (!isSelf && username !== 'root') { if (!isSelf && username !== 'root') {
const aiConfig = chatServices.aiConfig; const aiConfig = chatServices.aiConfig;

View File

@@ -2,7 +2,7 @@ import { AIConfigParser, type AIConfig } from '@/provider/utils/parse-config.ts'
import { redis } from '@/modules/db.ts'; import { redis } from '@/modules/db.ts';
import { CustomError } from '@kevisual/router'; import { CustomError } from '@kevisual/router';
import { queryConfig } from '@/modules/query.ts'; import { queryConfig } from '@/modules/query.ts';
import { log } from '@/logger/index.ts'; import { logger } from '@/modules/logger.ts';
export class ChatConfigServices { export class ChatConfigServices {
cachePrefix = 'ai:chat:config'; cachePrefix = 'ai:chat:config';
// 使用谁的模型 // 使用谁的模型
@@ -11,6 +11,7 @@ export class ChatConfigServices {
username: string; username: string;
aiConfig?: AIConfig; aiConfig?: AIConfig;
isOwner: boolean; isOwner: boolean;
token?: string;
/** /**
* username 是使用的模型的用户名,使用谁的模型 * username 是使用的模型的用户名,使用谁的模型
* @param username * @param username
@@ -19,6 +20,7 @@ export class ChatConfigServices {
this.owner = owner; this.owner = owner;
this.username = username; this.username = username;
this.isOwner = owner === username; this.isOwner = owner === username;
// this.token = token;
} }
getKey() { getKey() {
return `${this.cachePrefix}:${this.owner}`; return `${this.cachePrefix}:${this.owner}`;
@@ -28,13 +30,14 @@ export class ChatConfigServices {
* @param keepSecret 是否需要清除secret 默认 不清除 为true * @param keepSecret 是否需要清除secret 默认 不清除 为true
* @returns * @returns
*/ */
async getChatConfig(keepSecret = true, token?: string) { async getChatConfig(keepSecret = true, newToken?: string) {
const key = this.getKey(); const key = this.getKey();
const cache = await redis.get(key); const cache = await redis.get(key);
let modelConfig = null; let modelConfig = null;
if (cache) { if (cache) {
modelConfig = JSON.parse(cache); modelConfig = JSON.parse(cache);
} }
const token = newToken || this.token;
if (!modelConfig) { if (!modelConfig) {
if (this.owner !== this.username) { if (this.owner !== this.username) {
throw new CustomError( throw new CustomError(
@@ -45,6 +48,7 @@ export class ChatConfigServices {
if (res.code === 200 && res.data?.data) { if (res.code === 200 && res.data?.data) {
modelConfig = res.data.data; modelConfig = res.data.data;
} else { } else {
logger.error('获取ai.json配置失败', res, 'username', this.username);
throw new CustomError(400, 'get config failed'); throw new CustomError(400, 'get config failed');
} }
} }
@@ -57,10 +61,8 @@ export class ChatConfigServices {
await redis.set(key, JSON.stringify(modelConfig), 'EX', cacheTime); await redis.set(key, JSON.stringify(modelConfig), 'EX', cacheTime);
} }
this.aiConfig = modelConfig; this.aiConfig = modelConfig;
if (!keepSecret) { const aiConfigParser = new AIConfigParser(modelConfig);
modelConfig = this.filterApiKey(modelConfig); return aiConfigParser.getConfig(keepSecret);
}
return modelConfig;
} }
async clearCache() { async clearCache() {
const key = this.getKey(); const key = this.getKey();
@@ -74,20 +76,6 @@ export class ChatConfigServices {
const aiConfigParser = new AIConfigParser(config || this.aiConfig); const aiConfigParser = new AIConfigParser(config || this.aiConfig);
return aiConfigParser.getSelectOpts(); return aiConfigParser.getSelectOpts();
} }
async filterApiKey(chatConfig: AIConfig) {
// 过滤掉secret中的所有apiKey移除掉并返回chatConfig
const { secretKeys = [], ...rest } = chatConfig;
return {
...rest,
secretKeys: secretKeys.map((item) => {
return {
...item,
apiKey: undefined,
decryptKey: undefined,
};
}),
};
}
/** /**
* 获取和检测当前用户的额度, 当使用 root 账号的时候,才需要检测 * 获取和检测当前用户的额度, 当使用 root 账号的时候,才需要检测
* username是当前使用用户 * username是当前使用用户

View File

@@ -14,6 +14,7 @@ export type ChatServicesConfig = {
model: string; model: string;
group: string; group: string;
decryptKey?: string; decryptKey?: string;
token?: string;
}; };
export class ChatServices { export class ChatServices {
cachePrefix = 'ai-chat:model:'; cachePrefix = 'ai-chat:model:';
@@ -39,6 +40,7 @@ export class ChatServices {
modelConfig?: ProviderResult; modelConfig?: ProviderResult;
aiConfig?: AIConfig; aiConfig?: AIConfig;
chatProvider?: BaseChat; chatProvider?: BaseChat;
token?: string;
constructor(opts: ChatServicesConfig) { constructor(opts: ChatServicesConfig) {
this.owner = opts.owner; this.owner = opts.owner;
this.model = opts.model; this.model = opts.model;
@@ -91,7 +93,8 @@ export class ChatServices {
return cache; return cache;
} }
async getConfig(username: string) { async getConfig(username: string) {
const services = new ChatConfigServices(this.owner, username); const token = this.token;
const services = new ChatConfigServices(this.owner, username, token);
return services.getChatConfig(); return services.getChatConfig();
} }
@@ -145,7 +148,7 @@ export class ChatServices {
return item; return item;
}); });
} }
static async createServices(opts: Partial<ChatServicesConfig> & { username: string }) { static async createServices(opts: Partial<ChatServicesConfig> & { username: string; token?: string }) {
const owner = opts.owner || 'root'; const owner = opts.owner || 'root';
const model = opts.model || 'deepseek-chat'; const model = opts.model || 'deepseek-chat';
const group = opts.group || 'deepseek'; const group = opts.group || 'deepseek';

1
src/run.ts Normal file
View File

@@ -0,0 +1 @@
console.log('run commander')

View File

@@ -1,65 +0,0 @@
import { getChunks } from '../../provider/utils/chunk.ts';
const str = 'Hello world this is a test 你好沙盒 very big';
const str2 = `不能直接使用 tiktokenOpenAI的分词器来计算 Qwen 模型的 Token 数量因为两者的分词规则Tokenization和词表Vocabulary完全不同。
为什么不能混用?
词表不同
tiktoken 是 OpenAI 为 GPT 系列设计的(如 gpt-3.5-turbo, gpt-4其词表针对英语和代码优化。
Qwen 使用独立训练的 BPE 词表,对中文、多语言的支持更友好,分词粒度可能不同。
分词结果差异大
同一段文本tiktoken 和 Qwen 的分词结果可能完全不同。例如:
OpenAI (tiktoken): "你好" → ['你', '好']2 Tokens
Qwen: "你好" → ['你好']1 Token如果词表中包含该组合
性能问题
即使强制使用 tiktoken 计算 Qwen 的 Token结果也不准确可能导致
输入超出模型上下文限制(因统计偏差)。
API 计费或本地推理时出现意外错误。
正确方法:用 Qwen 的分词器
通过 Hugging Face transformers 加载 Qwen 的原生分词器:
python
复制
from transformers import AutoTokenizer
# 加载 Qwen 的分词器(以 Qwen-7B 为例)
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen-7B", trust_remote_code=True)
text = "你好Qwen模型"
tokens = tokenizer.tokenize(text) # 查看分词结果
token_count = len(tokenizer.encode(text, add_special_tokens=False))
print("分词结果:", tokens)
print("Token数量:", token_count)
常见问题
为什么需要 trust_remote_code=True
Qwen 的分词器是自定义实现的(非 Hugging Face 原生),此参数允许从模型仓库加载运行代码。
其他语言的 Token 计算?
Qwen 对非英语(如中文、日文)的分词效率较高,但仍需用其原生分词器统计。
与 tiktoken 的速度对比?
tiktoken 是纯 Python 实现速度较快Qwen 的分词器基于 Hugging Face可能稍慢但对齐模型需求。
总结
禁止混用tiktoken ≠ Qwen 分词器。
始终使用模型配套工具Qwen 需通过 transformers 加载其官方分词器。
中文场景特别注意Qwen 对中文的分词更高效,直接使用可避免偏差。
如果需要验证分词规则,可通过 tokenizer.vocab 查看词表内容(但注意词表通常较大)。`
const chunks = getChunks(str2);
console.log(chunks);

View File

@@ -1,9 +0,0 @@
import { encryptAES, decryptAES } from '../../provider/utils/parse-config.ts';
const plainx = process.env.API_KEY;
const decryptKey = process.env.DECRYPT_KEY;
const encrypt = encryptAES(plainx, decryptKey);
console.log('encrypt', encrypt);
const decrypt = decryptAES(encrypt, decryptKey);
console.log(decrypt);

View File

@@ -1,26 +0,0 @@
import { ModelScope } from '../../provider/chat-adapter/model-scope.ts';
import { logInfo } from '../../logger/index.ts';
import util from 'util';
import { config } from 'dotenv';
config();
const chat = new ModelScope({
apiKey: process.env.MODEL_SCOPE_API_KEY,
model: 'Qwen/Qwen2.5-Coder-32B-Instruct',
});
// chat.chat([{ role: 'user', content: 'Hello, world! 1 + 1 equals ?' }]);
const chatMessage = [{ role: 'user', content: 'Hello, world! 1 + 1 equals ?' }];
const main = async () => {
const res = await chat.test();
logInfo('test', res);
};
// main();
const mainChat = async () => {
const res = await chat.chat(chatMessage as any);
logInfo('chat', res);
};
mainChat();

View File

@@ -1,37 +0,0 @@
import { Knowledge } from '../provider/knowledge/knowledge.ts';
import fs from 'fs';
import dotenv from 'dotenv';
dotenv.config();
const knowledge = new Knowledge({
embeddingModel: 'bge-m3:latest',
baseURL: 'https://ollama.xiongxiao.me/v1',
model: 'qwq:latest',
apiKey: process.env.OLLAMA_API_KEY,
});
const main = async () => {
const res = await knowledge.generateEmbeddingCore('Hello world this is a test 你好沙盒 very big');
fs.writeFileSync('docs/embedding.json', JSON.stringify(res, null, 2));
console.log(res);
};
main();
const main2 = async () => {
const text1 = 'Hello, world! this is a test';
const text2 = 'Hello, world! this is a test 2';
const text3 = 'Hello, world! this is a test 3';
const text4 = 'Hello, world! this is a test 4';
const text5 = 'Hello, world! this is a test 5';
const text6 = 'Hello, world! this is a test 6';
const text7 = 'Hello, world! this is a test 7';
const text8 = 'Hello, world! this is a test 8';
const text9 = 'Hello, world! this is a test 9';
const text10 = 'Hello, world! this is a test 10';
const res = await knowledge.generateEmbeddingCore([text1, text2, text3, text4, text5, text6, text7, text8, text9, text10]);
fs.writeFileSync('docs/embedding2.json', JSON.stringify(res, null, 2));
console.log(res);
};
// main2();

View File

@@ -1,86 +0,0 @@
import { Ollama } from '../provider/chat-adapter/ollama.ts';
import util from 'util';
const chat = new Ollama({
baseURL: 'https://ollama.xiongxiao.me/v1',
apiKey: 'xiongxiao2233',
model: 'qwq:latest',
});
// chat.chat([{ role: 'user', content: 'Hello, world!' }]);
const main = async () => {
const res = await chat.test();
console.log(util.inspect(res, { depth: null, colors: true }));
};
// main();
const getJson = async () => {
const res = await chat.chat(
[
{ role: 'system', content: '把发送的数据返回给我对应的json只处理完发送的数据。如果发送了多个给我一个数组' },
// { role: 'user', content: '{"name":"John","age":30}' },
{ role: 'user', content: 'name: 张三' },
{ role: 'user', content: 'name: 李四, age: 18' },
],
{
response_format: {
type: 'json_schema',
json_schema: {
name: 'user',
description: '用户信息',
schema: {
type: 'object',
// properties: {
// name: { type: 'string' },
// // age: { type: 'number' },
// },
// // required: ['name', 'age'],
// required: ['name'],
properties: {
name: { type: 'string' },
age: { type: 'number' },
},
required: ['name', 'age'],
},
},
},
n: 10,
},
);
console.log(util.inspect(res, { depth: null, colors: true }));
};
// getJson();
const createChat1 = async () => {
const res = await chat.chat(
[
{ role: 'user', content: 'a=1, b=2, c=3' },
{ role: 'user', content: 'a+b+c=?' },
{ role: 'assistant', content: '给定的值为 \\( a = 1 \\), \\( b = 2 \\), \\( c = 3 \\)。\n' + '\n' + '因此,\\( a + b + c = 1 + 2 + 3 = 6 \\)。' },
{ role: 'user', content: 'a+b+c+4=?' },
],
{
model: 'qwen2.5:7b',
},
);
console.log(util.inspect(res, { depth: null, colors: true }));
};
// createChat1();
const getTags = async () => {
const res = await chat.listModels();
console.log(util.inspect(res, { depth: null, colors: true }));
};
// getTags();
const getRunModels = async () => {
const res = await chat.listRunModels();
console.log('current', new Date().toISOString());
console.log(util.inspect(res, { depth: null, colors: true }));
};
// getRunModels();

View File

@@ -1,6 +0,0 @@
import { ProviderManager } from '../../provider/index.ts';
const providerConfig = { provider: 'ModelScope', model: 'Qwen/Qwen2.5-Coder-32B-Instruct', apiKey: 'a4cc0e94-3633-4374-85a6-06f455e17bea' };
const provider = await ProviderManager.createProvider(providerConfig);
const result = await provider.chat([{ role: 'user', content: '你好' }]);
console.log(result);

View File

@@ -1,15 +0,0 @@
import { SiliconFlow } from '../../provider/chat-adapter/siliconflow.ts';
import dotenv from 'dotenv';
dotenv.config();
const siliconflow = new SiliconFlow({
apiKey: process.env.SILICONFLOW_API_KEY,
model: 'Qwen/Qwen2-7B-Instruct',
});
const main = async () => {
const usage = await siliconflow.getUsage();
console.log(usage);
};
main();

View File

@@ -1,25 +1,8 @@
{ {
"extends": "@kevisual/types/json/backend.json",
"compilerOptions": { "compilerOptions": {
"module": "nodenext",
"target": "esnext",
"noImplicitAny": false,
"outDir": "./dist",
"sourceMap": false,
"allowJs": true,
"newLine": "LF",
"baseUrl": "./", "baseUrl": "./",
"typeRoots": [
"node_modules/@types",
"node_modules/@kevisual/types"
],
"declaration": true,
"noEmit": false,
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"emitDeclarationOnly": true,
"moduleResolution": "NodeNext",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"paths": { "paths": {
"@/*": [ "@/*": [
"src/*" "src/*"

View File

@@ -1,9 +0,0 @@
import { defineConfig } from 'vite';
import basicSsl from '@vitejs/plugin-basic-ssl';
export default defineConfig({
plugins: [basicSsl()],
server: {
port: 3000,
},
});