generated from tailored/app-template
Compare commits
13 Commits
fe716b8ea3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d9385e3c4e | |||
| 0c885e9012 | |||
| 6ced3676ab | |||
| 8d2401ea30 | |||
| 2369417961 | |||
| 2338242018 | |||
| 4a2d3e8d32 | |||
| b07bcc5454 | |||
| abac483610 | |||
| 6376773e13 | |||
| 0804bb9f2b | |||
| e0df4e490c | |||
| ac207ff374 |
@@ -1,2 +0,0 @@
|
|||||||
config.json
|
|
||||||
.env*
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -19,3 +19,5 @@ logs
|
|||||||
config.json
|
config.json
|
||||||
|
|
||||||
pack-dist
|
pack-dist
|
||||||
|
|
||||||
|
videos/output*
|
||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -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
37
bun.config.mjs
Normal 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_*',
|
||||||
|
});
|
||||||
87
package.json
87
package.json
@@ -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
3110
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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;
|
|
||||||
@@ -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
6
src/modules/logger.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { Logger } from '@kevisual/logger';
|
||||||
|
|
||||||
|
export const logger = new Logger({
|
||||||
|
level: process?.env?.LOG_LEVEL || 'info',
|
||||||
|
showTime: true,
|
||||||
|
});
|
||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/provider/media/index.ts
Normal file
1
src/provider/media/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './video/siliconflow.ts';
|
||||||
37
src/provider/media/video/siliconflow.ts
Normal file
37
src/provider/media/video/siliconflow.ts
Normal 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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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是当前使用用户
|
||||||
|
|||||||
@@ -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
1
src/run.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
console.log('run commander')
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import { getChunks } from '../../provider/utils/chunk.ts';
|
|
||||||
|
|
||||||
const str = 'Hello world this is a test 你好沙盒 very big';
|
|
||||||
|
|
||||||
|
|
||||||
const str2 = `不能直接使用 tiktoken(OpenAI的分词器)来计算 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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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);
|
|
||||||
@@ -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();
|
|
||||||
Submodule submodules/query-config deleted from 53cd97454d
@@ -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/*"
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { defineConfig } from 'vite';
|
|
||||||
import basicSsl from '@vitejs/plugin-basic-ssl';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [basicSsl()],
|
|
||||||
server: {
|
|
||||||
port: 3000,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user