generated from tailored/app-template
	feat: center change
This commit is contained in:
		@@ -10,7 +10,8 @@
 | 
			
		||||
    "type": "system-app"
 | 
			
		||||
  },
 | 
			
		||||
  "files": [
 | 
			
		||||
    "dist"
 | 
			
		||||
    "dist",
 | 
			
		||||
    "types"
 | 
			
		||||
  ],
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "watch": "rollup -c rollup.config.mjs -w",
 | 
			
		||||
@@ -29,7 +30,8 @@
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@kevisual/cache": "^0.0.2",
 | 
			
		||||
    "@kevisual/permission": "^0.0.1",
 | 
			
		||||
    "@kevisual/router": "0.0.10"
 | 
			
		||||
    "@kevisual/router": "0.0.10",
 | 
			
		||||
    "pino-pretty": "^13.0.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@kevisual/code-center-module": "0.0.18",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										74
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										74
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@@ -17,6 +17,9 @@ importers:
 | 
			
		||||
      '@kevisual/router':
 | 
			
		||||
        specifier: 0.0.10
 | 
			
		||||
        version: 0.0.10
 | 
			
		||||
      pino-pretty:
 | 
			
		||||
        specifier: ^13.0.0
 | 
			
		||||
        version: 13.0.0
 | 
			
		||||
    devDependencies:
 | 
			
		||||
      '@kevisual/code-center-module':
 | 
			
		||||
        specifier: 0.0.18
 | 
			
		||||
@@ -1197,6 +1200,9 @@ packages:
 | 
			
		||||
  colorette@1.4.0:
 | 
			
		||||
    resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==}
 | 
			
		||||
 | 
			
		||||
  colorette@2.0.20:
 | 
			
		||||
    resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
 | 
			
		||||
 | 
			
		||||
  combined-stream@1.0.8:
 | 
			
		||||
    resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
 | 
			
		||||
    engines: {node: '>= 0.8'}
 | 
			
		||||
@@ -1291,6 +1297,9 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
 | 
			
		||||
    engines: {node: '>= 0.4'}
 | 
			
		||||
 | 
			
		||||
  dateformat@4.6.3:
 | 
			
		||||
    resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
 | 
			
		||||
 | 
			
		||||
  dayjs@1.11.13:
 | 
			
		||||
    resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
 | 
			
		||||
 | 
			
		||||
@@ -1400,6 +1409,9 @@ packages:
 | 
			
		||||
  emoji-regex@9.2.2:
 | 
			
		||||
    resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
 | 
			
		||||
 | 
			
		||||
  end-of-stream@1.4.4:
 | 
			
		||||
    resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
 | 
			
		||||
 | 
			
		||||
  engine.io-parser@5.2.3:
 | 
			
		||||
    resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==}
 | 
			
		||||
    engines: {node: '>=10.0.0'}
 | 
			
		||||
@@ -1523,6 +1535,9 @@ packages:
 | 
			
		||||
  fast-content-type-parse@2.0.1:
 | 
			
		||||
    resolution: {integrity: sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==}
 | 
			
		||||
 | 
			
		||||
  fast-copy@3.0.2:
 | 
			
		||||
    resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==}
 | 
			
		||||
 | 
			
		||||
  fast-deep-equal@3.1.3:
 | 
			
		||||
    resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
 | 
			
		||||
 | 
			
		||||
@@ -1537,6 +1552,9 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==}
 | 
			
		||||
    engines: {node: '>=6'}
 | 
			
		||||
 | 
			
		||||
  fast-safe-stringify@2.1.1:
 | 
			
		||||
    resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
 | 
			
		||||
 | 
			
		||||
  fast-uri@3.0.6:
 | 
			
		||||
    resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==}
 | 
			
		||||
 | 
			
		||||
@@ -1736,6 +1754,9 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
 | 
			
		||||
    engines: {node: '>= 0.4'}
 | 
			
		||||
 | 
			
		||||
  help-me@5.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
 | 
			
		||||
 | 
			
		||||
  hexoid@2.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==}
 | 
			
		||||
    engines: {node: '>=8'}
 | 
			
		||||
@@ -2326,6 +2347,10 @@ packages:
 | 
			
		||||
  pino-abstract-transport@2.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==}
 | 
			
		||||
 | 
			
		||||
  pino-pretty@13.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==}
 | 
			
		||||
    hasBin: true
 | 
			
		||||
 | 
			
		||||
  pino-std-serializers@7.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==}
 | 
			
		||||
 | 
			
		||||
@@ -2433,6 +2458,9 @@ packages:
 | 
			
		||||
  pstree.remy@1.1.8:
 | 
			
		||||
    resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
 | 
			
		||||
 | 
			
		||||
  pump@3.0.2:
 | 
			
		||||
    resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
 | 
			
		||||
 | 
			
		||||
  punycode@2.3.1:
 | 
			
		||||
    resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
 | 
			
		||||
    engines: {node: '>=6'}
 | 
			
		||||
@@ -2579,6 +2607,9 @@ packages:
 | 
			
		||||
  sax@1.4.1:
 | 
			
		||||
    resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
 | 
			
		||||
 | 
			
		||||
  secure-json-parse@2.7.0:
 | 
			
		||||
    resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
 | 
			
		||||
 | 
			
		||||
  selfsigned@2.4.1:
 | 
			
		||||
    resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
@@ -2789,6 +2820,10 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==}
 | 
			
		||||
    engines: {node: '>=18'}
 | 
			
		||||
 | 
			
		||||
  strip-json-comments@3.1.1:
 | 
			
		||||
    resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
 | 
			
		||||
    engines: {node: '>=8'}
 | 
			
		||||
 | 
			
		||||
  stubborn-fs@1.2.5:
 | 
			
		||||
    resolution: {integrity: sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==}
 | 
			
		||||
 | 
			
		||||
@@ -4244,6 +4279,8 @@ snapshots:
 | 
			
		||||
 | 
			
		||||
  colorette@1.4.0: {}
 | 
			
		||||
 | 
			
		||||
  colorette@2.0.20: {}
 | 
			
		||||
 | 
			
		||||
  combined-stream@1.0.8:
 | 
			
		||||
    dependencies:
 | 
			
		||||
      delayed-stream: 1.0.0
 | 
			
		||||
@@ -4337,6 +4374,8 @@ snapshots:
 | 
			
		||||
      es-errors: 1.3.0
 | 
			
		||||
      is-data-view: 1.0.2
 | 
			
		||||
 | 
			
		||||
  dateformat@4.6.3: {}
 | 
			
		||||
 | 
			
		||||
  dayjs@1.11.13: {}
 | 
			
		||||
 | 
			
		||||
  dayjs@1.8.36: {}
 | 
			
		||||
@@ -4443,6 +4482,10 @@ snapshots:
 | 
			
		||||
 | 
			
		||||
  emoji-regex@9.2.2: {}
 | 
			
		||||
 | 
			
		||||
  end-of-stream@1.4.4:
 | 
			
		||||
    dependencies:
 | 
			
		||||
      once: 1.4.0
 | 
			
		||||
 | 
			
		||||
  engine.io-parser@5.2.3: {}
 | 
			
		||||
 | 
			
		||||
  engine.io@6.6.4:
 | 
			
		||||
@@ -4651,6 +4694,8 @@ snapshots:
 | 
			
		||||
 | 
			
		||||
  fast-content-type-parse@2.0.1: {}
 | 
			
		||||
 | 
			
		||||
  fast-copy@3.0.2: {}
 | 
			
		||||
 | 
			
		||||
  fast-deep-equal@3.1.3: {}
 | 
			
		||||
 | 
			
		||||
  fast-glob@3.3.3:
 | 
			
		||||
@@ -4665,6 +4710,8 @@ snapshots:
 | 
			
		||||
 | 
			
		||||
  fast-redact@3.5.0: {}
 | 
			
		||||
 | 
			
		||||
  fast-safe-stringify@2.1.1: {}
 | 
			
		||||
 | 
			
		||||
  fast-uri@3.0.6: {}
 | 
			
		||||
 | 
			
		||||
  fastq@1.19.1:
 | 
			
		||||
@@ -4885,6 +4932,8 @@ snapshots:
 | 
			
		||||
    dependencies:
 | 
			
		||||
      function-bind: 1.1.2
 | 
			
		||||
 | 
			
		||||
  help-me@5.0.0: {}
 | 
			
		||||
 | 
			
		||||
  hexoid@2.0.0: {}
 | 
			
		||||
 | 
			
		||||
  http-proxy-agent@7.0.2:
 | 
			
		||||
@@ -5514,6 +5563,22 @@ snapshots:
 | 
			
		||||
    dependencies:
 | 
			
		||||
      split2: 4.2.0
 | 
			
		||||
 | 
			
		||||
  pino-pretty@13.0.0:
 | 
			
		||||
    dependencies:
 | 
			
		||||
      colorette: 2.0.20
 | 
			
		||||
      dateformat: 4.6.3
 | 
			
		||||
      fast-copy: 3.0.2
 | 
			
		||||
      fast-safe-stringify: 2.1.1
 | 
			
		||||
      help-me: 5.0.0
 | 
			
		||||
      joycon: 3.1.1
 | 
			
		||||
      minimist: 1.2.8
 | 
			
		||||
      on-exit-leak-free: 2.1.2
 | 
			
		||||
      pino-abstract-transport: 2.0.0
 | 
			
		||||
      pump: 3.0.2
 | 
			
		||||
      secure-json-parse: 2.7.0
 | 
			
		||||
      sonic-boom: 4.2.0
 | 
			
		||||
      strip-json-comments: 3.1.1
 | 
			
		||||
 | 
			
		||||
  pino-std-serializers@7.0.0: {}
 | 
			
		||||
 | 
			
		||||
  pino@9.6.0:
 | 
			
		||||
@@ -5664,6 +5729,11 @@ snapshots:
 | 
			
		||||
 | 
			
		||||
  pstree.remy@1.1.8: {}
 | 
			
		||||
 | 
			
		||||
  pump@3.0.2:
 | 
			
		||||
    dependencies:
 | 
			
		||||
      end-of-stream: 1.4.4
 | 
			
		||||
      once: 1.4.0
 | 
			
		||||
 | 
			
		||||
  punycode@2.3.1: {}
 | 
			
		||||
 | 
			
		||||
  queue-microtask@1.2.3: {}
 | 
			
		||||
@@ -5845,6 +5915,8 @@ snapshots:
 | 
			
		||||
 | 
			
		||||
  sax@1.4.1: {}
 | 
			
		||||
 | 
			
		||||
  secure-json-parse@2.7.0: {}
 | 
			
		||||
 | 
			
		||||
  selfsigned@2.4.1:
 | 
			
		||||
    dependencies:
 | 
			
		||||
      '@types/node-forge': 1.3.11
 | 
			
		||||
@@ -6082,6 +6154,8 @@ snapshots:
 | 
			
		||||
 | 
			
		||||
  strip-final-newline@4.0.0: {}
 | 
			
		||||
 | 
			
		||||
  strip-json-comments@3.1.1: {}
 | 
			
		||||
 | 
			
		||||
  stubborn-fs@1.2.5: {}
 | 
			
		||||
 | 
			
		||||
  sucrase@3.35.0:
 | 
			
		||||
 
 | 
			
		||||
@@ -71,6 +71,9 @@ const config = {
 | 
			
		||||
    'sequelize', // 数据库 orm
 | 
			
		||||
    'ioredis', // redis
 | 
			
		||||
    'pg', // pg
 | 
			
		||||
    'pino', // pino
 | 
			
		||||
    'pino-pretty', // pino-pretty
 | 
			
		||||
    
 | 
			
		||||
  ],
 | 
			
		||||
};
 | 
			
		||||
export default config;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										37
									
								
								src/logger/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/logger/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
import { pino } from 'pino';
 | 
			
		||||
import { useConfig } from '@kevisual/use-config/env';
 | 
			
		||||
 | 
			
		||||
const config = useConfig();
 | 
			
		||||
 | 
			
		||||
export const logger = pino({
 | 
			
		||||
  level: config.LOG_LEVEL || 'info',
 | 
			
		||||
  transport: {
 | 
			
		||||
    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 logWarning = (message: string, data?: any) => logger.warn({ data }, message);
 | 
			
		||||
export const logInfo = (message: string, data?: any) => logger.info({ data }, message);
 | 
			
		||||
export const logDebug = (message: string, data?: any) => logger.debug({ data }, message);
 | 
			
		||||
 | 
			
		||||
export const log = {
 | 
			
		||||
  error: logError,
 | 
			
		||||
  warn: logWarning,
 | 
			
		||||
  info: logInfo,
 | 
			
		||||
  debug: logDebug,
 | 
			
		||||
};
 | 
			
		||||
@@ -7,6 +7,7 @@ export type OllamaOptions = BaseChatOptions;
 | 
			
		||||
 */
 | 
			
		||||
export class Custom extends BaseChat {
 | 
			
		||||
  constructor(options: OllamaOptions) {
 | 
			
		||||
    super(options);
 | 
			
		||||
    const baseURL = options.baseURL || 'https://api.deepseek.com/v1/';
 | 
			
		||||
    super({ ...(options as BaseChatOptions), baseURL: baseURL });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import { BaseChat, BaseChatOptions } from '../core/chat.ts';
 | 
			
		||||
export type DeepSeekOptions = Partial<BaseChatOptions>;
 | 
			
		||||
export class DeepSeek extends BaseChat {
 | 
			
		||||
  constructor(options: DeepSeekOptions) {
 | 
			
		||||
    super({ baseURL: 'https://api.deepseek.com/v1/', ...options } as any);
 | 
			
		||||
    const baseURL = options.baseURL || 'https://api.deepseek.com/v1/';
 | 
			
		||||
    super({ ...(options as BaseChatOptions), baseURL: baseURL });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import { BaseChat, BaseChatOptions } from '../core/chat.ts';
 | 
			
		||||
export type ModelScopeOptions = Partial<BaseChatOptions>;
 | 
			
		||||
export class ModelScope extends BaseChat {
 | 
			
		||||
  constructor(options: ModelScopeOptions) {
 | 
			
		||||
    super({ baseURL: 'https://api-inference.modelscope.cn/v1/', ...options } as any);
 | 
			
		||||
    const baseURL = options.baseURL || 'https://api-inference.modelscope.cn/v1/';
 | 
			
		||||
    super({ ...options, baseURL: baseURL } as any);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,8 @@ type OllamaModel = {
 | 
			
		||||
};
 | 
			
		||||
export class Ollama extends BaseChat {
 | 
			
		||||
  constructor(options: OllamaOptions) {
 | 
			
		||||
    super({ baseURL: 'http://localhost:11434/v1', ...(options as BaseChatOptions) });
 | 
			
		||||
    const baseURL = options.baseURL || 'http://localhost:11434/v1';
 | 
			
		||||
    super({ ...(options as BaseChatOptions), baseURL: baseURL });
 | 
			
		||||
  }
 | 
			
		||||
  async chat(messages: ChatMessage[], options?: ChatMessageOptions) {
 | 
			
		||||
    const res = await super.chat(messages, options);
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,8 @@ type SiliconFlowUsageResponse = {
 | 
			
		||||
};
 | 
			
		||||
export class SiliconFlow extends BaseChat {
 | 
			
		||||
  constructor(options: SiliconFlowOptions) {
 | 
			
		||||
    super({ baseURL: 'https://api.siliconflow.com/v1', ...(options as BaseChatOptions) });
 | 
			
		||||
    const baseURL = options.baseURL || 'https://api.siliconflow.com/v1';
 | 
			
		||||
    super({ ...(options as BaseChatOptions), baseURL: baseURL });
 | 
			
		||||
  }
 | 
			
		||||
  async getUsageInfo(): Promise<SiliconFlowUsageResponse> {
 | 
			
		||||
    return this.openai.get('/user/info');
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import { BaseChat, BaseChatOptions } from '../core/chat.ts';
 | 
			
		||||
export type VolcesOptions = Partial<BaseChatOptions>;
 | 
			
		||||
export class Volces extends BaseChat {
 | 
			
		||||
  constructor(options: VolcesOptions) {
 | 
			
		||||
    super({ baseURL: 'https://ark.cn-beijing.volces.com/api/v3/', ...options } as any);
 | 
			
		||||
    const baseURL = options.baseURL || 'https://ark.cn-beijing.volces.com/api/v3/';
 | 
			
		||||
    super({ ...(options as BaseChatOptions), baseURL: baseURL });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
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 ChatMessageComplete = OpenAI.Chat.Completions.ChatCompletion;
 | 
			
		||||
export type ChatMessageStream = OpenAI.Chat.Completions.ChatCompletion;
 | 
			
		||||
 
 | 
			
		||||
@@ -40,6 +40,8 @@ export class ProviderManager {
 | 
			
		||||
    if (!Provider) {
 | 
			
		||||
      throw new Error(`Provider ${provider} not found`);
 | 
			
		||||
    }
 | 
			
		||||
    console.log('pm', 'Provider', ProviderMap[provider]);
 | 
			
		||||
 | 
			
		||||
    this.provider = new Provider({
 | 
			
		||||
      model,
 | 
			
		||||
      apiKey,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										52
									
								
								src/provider/utils/ai-config-type.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/provider/utils/ai-config-type.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
import type { Permission } from '@kevisual/permission';
 | 
			
		||||
 | 
			
		||||
export type AIModel = {
 | 
			
		||||
  /**
 | 
			
		||||
   * 提供商
 | 
			
		||||
   */
 | 
			
		||||
  provider: string;
 | 
			
		||||
  /**
 | 
			
		||||
   * 模型名称
 | 
			
		||||
   */
 | 
			
		||||
  model: string;
 | 
			
		||||
  /**
 | 
			
		||||
   * 模型组
 | 
			
		||||
   */
 | 
			
		||||
  group: string;
 | 
			
		||||
  /**
 | 
			
		||||
   * 每日请求频率限制
 | 
			
		||||
   */
 | 
			
		||||
  dayLimit?: number;
 | 
			
		||||
  /**
 | 
			
		||||
   * 总的token限制
 | 
			
		||||
   */
 | 
			
		||||
  tokenLimit?: number;
 | 
			
		||||
};
 | 
			
		||||
export type SecretKey = {
 | 
			
		||||
  /**
 | 
			
		||||
   * 组
 | 
			
		||||
   */
 | 
			
		||||
  group: string;
 | 
			
		||||
  /**
 | 
			
		||||
   * API密钥
 | 
			
		||||
   */
 | 
			
		||||
  apiKey: string;
 | 
			
		||||
  /**
 | 
			
		||||
   * 解密密钥
 | 
			
		||||
   */
 | 
			
		||||
  decryptKey?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type AIConfig = {
 | 
			
		||||
  title?: string;
 | 
			
		||||
  description?: string;
 | 
			
		||||
  models: AIModel[];
 | 
			
		||||
  secretKeys: SecretKey[];
 | 
			
		||||
  permission?: Permission;
 | 
			
		||||
  filter?: {
 | 
			
		||||
    objectKey: string;
 | 
			
		||||
    type: 'array' | 'object';
 | 
			
		||||
    operate: 'removeAttribute' | 'remove';
 | 
			
		||||
    attribute: string[];
 | 
			
		||||
  }[];
 | 
			
		||||
};
 | 
			
		||||
@@ -88,7 +88,11 @@ export class AIConfigParser {
 | 
			
		||||
  constructor(config: AIConfig) {
 | 
			
		||||
    this.config = config;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 获取模型配置
 | 
			
		||||
   * @param opts
 | 
			
		||||
   * @returns
 | 
			
		||||
   */
 | 
			
		||||
  getProvider(opts: GetProviderOpts): ProviderResult {
 | 
			
		||||
    const { model, group, decryptKey } = opts;
 | 
			
		||||
    const modelConfig = this.config.models.find((m) => m.model === model && m.group === group);
 | 
			
		||||
@@ -117,16 +121,17 @@ export class AIConfigParser {
 | 
			
		||||
    this.result = mergeConfig;
 | 
			
		||||
    return mergeConfig;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getSecretKey({
 | 
			
		||||
    getCache,
 | 
			
		||||
    setCache,
 | 
			
		||||
    providerResult,
 | 
			
		||||
  }: {
 | 
			
		||||
  /**
 | 
			
		||||
   * 获取解密密钥
 | 
			
		||||
   * @param opts
 | 
			
		||||
   * @returns
 | 
			
		||||
   */
 | 
			
		||||
  async getSecretKey(opts?: {
 | 
			
		||||
    getCache?: (key: string) => Promise<string>;
 | 
			
		||||
    setCache?: (key: string, value: string) => Promise<void>;
 | 
			
		||||
    providerResult?: ProviderResult;
 | 
			
		||||
  }) {
 | 
			
		||||
    const { getCache, setCache, providerResult } = opts || {};
 | 
			
		||||
    const { apiKey, decryptKey, group = '', model } = providerResult || this.result;
 | 
			
		||||
    const cacheKey = `${group}--${model}`;
 | 
			
		||||
    if (!decryptKey) {
 | 
			
		||||
@@ -144,11 +149,38 @@ export class AIConfigParser {
 | 
			
		||||
    }
 | 
			
		||||
    return secretKey;
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * 加密
 | 
			
		||||
   * @param plainText
 | 
			
		||||
   * @param secretKey
 | 
			
		||||
   * @returns
 | 
			
		||||
   */
 | 
			
		||||
  encrypt(plainText: string, secretKey: string) {
 | 
			
		||||
    return encryptAES(plainText, secretKey);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 解密
 | 
			
		||||
   * @param cipherText
 | 
			
		||||
   * @param secretKey
 | 
			
		||||
   * @returns
 | 
			
		||||
   */
 | 
			
		||||
  decrypt(cipherText: string, secretKey: string) {
 | 
			
		||||
    return decryptAES(cipherText, secretKey);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /**
 | 
			
		||||
   * 获取模型配置
 | 
			
		||||
   * @returns
 | 
			
		||||
   */
 | 
			
		||||
  getSelectOpts() {
 | 
			
		||||
    const { models, secretKeys = [] } = this.config;
 | 
			
		||||
 | 
			
		||||
    return models.map((model) => {
 | 
			
		||||
      const selectOpts = secretKeys.find((m) => m.group === model.group);
 | 
			
		||||
      return {
 | 
			
		||||
        ...model,
 | 
			
		||||
        ...selectOpts,
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										43
									
								
								src/routes/ai-chat/cache.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/routes/ai-chat/cache.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
import { app } from '@/app.ts';
 | 
			
		||||
import { ChatConfigServices } from './services/chat-config-srevices.ts';
 | 
			
		||||
import { log } from '@/logger/index.ts';
 | 
			
		||||
import { ChatServices } from './services/chat-services.ts';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 清除缓存
 | 
			
		||||
 */
 | 
			
		||||
// https://localhost:4000/api/router?path=ai&key=clear-cache
 | 
			
		||||
app
 | 
			
		||||
  .route({
 | 
			
		||||
    path: 'ai',
 | 
			
		||||
    key: 'clear-cache',
 | 
			
		||||
    description: '清除缓存',
 | 
			
		||||
    middleware: ['auth'],
 | 
			
		||||
  })
 | 
			
		||||
  .define(async (ctx) => {
 | 
			
		||||
    const tokenUser = ctx.state.tokenUser;
 | 
			
		||||
    const username = tokenUser.username;
 | 
			
		||||
    const services = new ChatConfigServices(username, username);
 | 
			
		||||
    await services.clearCache();
 | 
			
		||||
    log.info('清除缓存成功', { username });
 | 
			
		||||
    ctx.body = 'success';
 | 
			
		||||
  })
 | 
			
		||||
  .addTo(app);
 | 
			
		||||
 | 
			
		||||
app
 | 
			
		||||
  .route({
 | 
			
		||||
    path: 'ai',
 | 
			
		||||
    key: 'clear-chat-limit',
 | 
			
		||||
    description: '清除chat使用情况',
 | 
			
		||||
    middleware: ['auth'],
 | 
			
		||||
  })
 | 
			
		||||
  .define(async (ctx) => {
 | 
			
		||||
    const tokenUser = ctx.state.tokenUser;
 | 
			
		||||
    const username = tokenUser.username;
 | 
			
		||||
    const cache = await ChatServices.clearChatLimit(username);
 | 
			
		||||
    log.debug('清除chat使用情况成功', { username, cache });
 | 
			
		||||
    ctx.body = {
 | 
			
		||||
      cache,
 | 
			
		||||
    };
 | 
			
		||||
  })
 | 
			
		||||
  .addTo(app);
 | 
			
		||||
@@ -3,6 +3,8 @@ import { ChatServices } from './services/chat-services.ts';
 | 
			
		||||
import { ChatConfigServices } from './services/chat-config-srevices.ts';
 | 
			
		||||
import { AiChatHistoryModel } from './models/ai-chat-history.ts';
 | 
			
		||||
import { UserPermission } from '@kevisual/permission';
 | 
			
		||||
import { AIConfigParser } from '@/provider/utils/parse-config.ts';
 | 
			
		||||
import { log } from '@/logger/index.ts';
 | 
			
		||||
app
 | 
			
		||||
  .route({
 | 
			
		||||
    path: 'ai',
 | 
			
		||||
@@ -11,10 +13,13 @@ app
 | 
			
		||||
  })
 | 
			
		||||
  .define(async (ctx) => {
 | 
			
		||||
    const data = ctx.query.data || {};
 | 
			
		||||
    const { id, messages = [], options = {}, title, hook, getFull = false } = data;
 | 
			
		||||
    let { username, model, group } = ctx.query;
 | 
			
		||||
    const { id, messages = [], title, type } = data;
 | 
			
		||||
    const hook = data.data?.hook;
 | 
			
		||||
 | 
			
		||||
    let { username, model, group, getFull = false } = ctx.query;
 | 
			
		||||
    const tokenUser = ctx.state.tokenUser || {};
 | 
			
		||||
    const tokenUsername = tokenUser.username;
 | 
			
		||||
    const options = ctx.query.options || {};
 | 
			
		||||
    let aiChatHistory: AiChatHistoryModel;
 | 
			
		||||
    if (id) {
 | 
			
		||||
      aiChatHistory = await AiChatHistoryModel.findByPk(id);
 | 
			
		||||
@@ -58,46 +63,65 @@ app
 | 
			
		||||
    if (pickMessages.length === 0) {
 | 
			
		||||
      ctx.throw(400, 'chat messages is empty');
 | 
			
		||||
    }
 | 
			
		||||
    const res = await chatServices.chat(pickMessages, options);
 | 
			
		||||
    if (!aiChatHistory) {
 | 
			
		||||
      aiChatHistory = await AiChatHistoryModel.create({
 | 
			
		||||
        username,
 | 
			
		||||
        model,
 | 
			
		||||
        group,
 | 
			
		||||
        title,
 | 
			
		||||
        type: type || 'keep',
 | 
			
		||||
      });
 | 
			
		||||
      if (!title) {
 | 
			
		||||
        // TODO: 创建标题
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const message = res.choices[0].message;
 | 
			
		||||
    const newMessage = await chatServices.createNewMessage([...messages, message]);
 | 
			
		||||
 | 
			
		||||
    const usage = chatServices.chatProvider.getChatUsage();
 | 
			
		||||
    await chatServices.updateChatLimit(usage.total_tokens);
 | 
			
		||||
    await chatConfigServices.updateUserChatLimit(tokenUsername, usage.total_tokens);
 | 
			
		||||
 | 
			
		||||
    const needUpdateData: any = {
 | 
			
		||||
      messages: newMessage,
 | 
			
		||||
      prompt_tokens: aiChatHistory.prompt_tokens + usage.prompt_tokens,
 | 
			
		||||
      completion_tokens: aiChatHistory.completion_tokens + usage.completion_tokens,
 | 
			
		||||
      total_tokens: aiChatHistory.total_tokens + usage.total_tokens,
 | 
			
		||||
      version: aiChatHistory.version + 1,
 | 
			
		||||
      model: model,
 | 
			
		||||
      group: group,
 | 
			
		||||
      username: username,
 | 
			
		||||
    };
 | 
			
		||||
    if (hook) {
 | 
			
		||||
      needUpdateData.data = {
 | 
			
		||||
        ...aiChatHistory.data,
 | 
			
		||||
        hook,
 | 
			
		||||
      };
 | 
			
		||||
    let message;
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await chatServices.chat(pickMessages, options);
 | 
			
		||||
      message = res.choices[0].message;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      log.error('chat error', {
 | 
			
		||||
        errorMessage: error.message,
 | 
			
		||||
      });
 | 
			
		||||
      ctx.throw(500, error.message);
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const newMessage = await chatServices.createNewMessage([...messages, message]);
 | 
			
		||||
 | 
			
		||||
      const usage = chatServices.chatProvider.getChatUsage();
 | 
			
		||||
      await chatServices.updateChatLimit(usage.total_tokens);
 | 
			
		||||
      await chatConfigServices.updateUserChatLimit(tokenUsername, usage.total_tokens);
 | 
			
		||||
 | 
			
		||||
      const needUpdateData: any = {
 | 
			
		||||
        messages: newMessage,
 | 
			
		||||
        prompt_tokens: aiChatHistory.prompt_tokens + usage.prompt_tokens,
 | 
			
		||||
        completion_tokens: aiChatHistory.completion_tokens + usage.completion_tokens,
 | 
			
		||||
        total_tokens: aiChatHistory.total_tokens + usage.total_tokens,
 | 
			
		||||
        version: aiChatHistory.version + 1,
 | 
			
		||||
        model: model,
 | 
			
		||||
        group: group,
 | 
			
		||||
        username: username,
 | 
			
		||||
      };
 | 
			
		||||
      if (hook) {
 | 
			
		||||
        needUpdateData.data = {
 | 
			
		||||
          ...aiChatHistory.data,
 | 
			
		||||
          hook,
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
      if (type) {
 | 
			
		||||
        needUpdateData.type = type;
 | 
			
		||||
      }
 | 
			
		||||
      await AiChatHistoryModel.update(needUpdateData, { where: { id: aiChatHistory.id } });
 | 
			
		||||
      ctx.body = {
 | 
			
		||||
        message: newMessage[newMessage.length - 1],
 | 
			
		||||
        updatedAt: aiChatHistory.updatedAt,
 | 
			
		||||
        version: aiChatHistory.version,
 | 
			
		||||
        aiChatHistory: getFull || !id ? aiChatHistory : undefined,
 | 
			
		||||
      };
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('create new message error', error);
 | 
			
		||||
      ctx.throw(500, error.message);
 | 
			
		||||
    }
 | 
			
		||||
    await AiChatHistoryModel.update(needUpdateData, { where: { id: aiChatHistory.id } });
 | 
			
		||||
    ctx.body = {
 | 
			
		||||
      message: newMessage[newMessage.length - 1],
 | 
			
		||||
      aiChatHistory: getFull || !id ? aiChatHistory : undefined,
 | 
			
		||||
    };
 | 
			
		||||
  })
 | 
			
		||||
  .addTo(app);
 | 
			
		||||
 | 
			
		||||
@@ -136,50 +160,69 @@ app
 | 
			
		||||
    path: 'ai',
 | 
			
		||||
    key: 'get-model-list',
 | 
			
		||||
    middleware: ['auth'],
 | 
			
		||||
    description: '获取模型列表',
 | 
			
		||||
    isDebug: true,
 | 
			
		||||
  })
 | 
			
		||||
  .define(async (ctx) => {
 | 
			
		||||
    const username = ctx.query.username || 'root';
 | 
			
		||||
    const tokenUser = ctx.state.tokenUser;
 | 
			
		||||
    const usernames = ctx.query.data?.usernames || [];
 | 
			
		||||
    const keepSecret = ctx.query.keepSecret || false;
 | 
			
		||||
    const tokenUsername = tokenUser.username;
 | 
			
		||||
    const isSameUser = username === tokenUser.username;
 | 
			
		||||
    const configArray: any[] = [];
 | 
			
		||||
    const services = new ChatConfigServices(username, tokenUser.username);
 | 
			
		||||
    const res = await services.getChatConfig(true, ctx.query.token);
 | 
			
		||||
    configArray.push({
 | 
			
		||||
      username,
 | 
			
		||||
      config: res,
 | 
			
		||||
    });
 | 
			
		||||
    if (!isSameUser) {
 | 
			
		||||
      const selfServices = new ChatConfigServices(tokenUser.username, tokenUser.username);
 | 
			
		||||
      const selfRes = await selfServices.getChatConfig(true, ctx.query.token);
 | 
			
		||||
      configArray.push({
 | 
			
		||||
        username: tokenUser.username,
 | 
			
		||||
        self: true,
 | 
			
		||||
        config: selfRes,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    for (const username of usernames) {
 | 
			
		||||
    try {
 | 
			
		||||
      const services = new ChatConfigServices(username, tokenUser.username);
 | 
			
		||||
      const res = await services.getChatConfig(true, ctx.query.token);
 | 
			
		||||
      const aiConfig = services.aiConfig;
 | 
			
		||||
      const permission = new UserPermission({ permission: aiConfig.permission, owner: username });
 | 
			
		||||
      const checkPermission = permission.checkPermissionSuccess({ username: tokenUsername, password: '-----------------' });
 | 
			
		||||
      if (!checkPermission.success) {
 | 
			
		||||
        // ctx.throw(403, `[${username}] ${checkPermission.message}`);
 | 
			
		||||
      const res = await services.getChatConfig(services.isOwner && keepSecret, ctx.query.token);
 | 
			
		||||
      const selectOpts = await services.getSelectOpts(res);
 | 
			
		||||
      configArray.push({
 | 
			
		||||
        username,
 | 
			
		||||
        config: res,
 | 
			
		||||
        selectOpts,
 | 
			
		||||
        self: isSameUser,
 | 
			
		||||
      });
 | 
			
		||||
      if (!isSameUser) {
 | 
			
		||||
        const selfServices = new ChatConfigServices(tokenUser.username, tokenUser.username);
 | 
			
		||||
        const selfRes = await selfServices.getChatConfig(services.isOwner && keepSecret, ctx.query.token);
 | 
			
		||||
        const selfSelectOpts = await selfServices.getSelectOpts(selfRes);
 | 
			
		||||
        configArray.push({
 | 
			
		||||
          username,
 | 
			
		||||
          config: null,
 | 
			
		||||
          error: checkPermission.message,
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        configArray.push({
 | 
			
		||||
          username,
 | 
			
		||||
          config: res,
 | 
			
		||||
          username: tokenUser.username,
 | 
			
		||||
          self: true,
 | 
			
		||||
          config: selfRes,
 | 
			
		||||
          selectOpts: selfSelectOpts,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      for (const username of usernames) {
 | 
			
		||||
        const services = new ChatConfigServices(username, tokenUser.username);
 | 
			
		||||
        const res = await services.getChatConfig(services.isOwner && keepSecret, ctx.query.token);
 | 
			
		||||
        const aiConfig = services.aiConfig;
 | 
			
		||||
        const permission = new UserPermission({ permission: aiConfig.permission, owner: username });
 | 
			
		||||
        const checkPermission = permission.checkPermissionSuccess({ username: tokenUsername, password: '-----------------' });
 | 
			
		||||
        if (!checkPermission.success) {
 | 
			
		||||
          configArray.push({
 | 
			
		||||
            username,
 | 
			
		||||
            config: null,
 | 
			
		||||
            error: checkPermission.message,
 | 
			
		||||
          });
 | 
			
		||||
        } else {
 | 
			
		||||
          const selectOpts = await services.getSelectOpts(res);
 | 
			
		||||
          configArray.push({
 | 
			
		||||
            username,
 | 
			
		||||
            config: res,
 | 
			
		||||
            selectOpts,
 | 
			
		||||
            self: username === tokenUser.username,
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      ctx.body = { list: configArray };
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      log.error('get model list error', {
 | 
			
		||||
        username,
 | 
			
		||||
        errorMessage: error.message,
 | 
			
		||||
        errorStack: error.stack,
 | 
			
		||||
      });
 | 
			
		||||
      ctx.throw(500, error.message);
 | 
			
		||||
    }
 | 
			
		||||
    ctx.body = configArray;
 | 
			
		||||
  })
 | 
			
		||||
  .addTo(app);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,9 +10,11 @@ app
 | 
			
		||||
  })
 | 
			
		||||
  .define(async (ctx) => {
 | 
			
		||||
    const tokenUser = ctx.state.tokenUser;
 | 
			
		||||
    const type = ctx.query.type || 'keep';
 | 
			
		||||
    const aiChatList = await AiChatHistoryModel.findAll({
 | 
			
		||||
      where: {
 | 
			
		||||
        uid: tokenUser.id,
 | 
			
		||||
        type,
 | 
			
		||||
      },
 | 
			
		||||
      order: [['updatedAt', 'DESC']],
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,7 @@ export class AiChatHistoryModel extends Model {
 | 
			
		||||
  declare completion_tokens: number;
 | 
			
		||||
 | 
			
		||||
  declare version: number;
 | 
			
		||||
  declare type: string;
 | 
			
		||||
 | 
			
		||||
  declare createdAt: Date;
 | 
			
		||||
  declare updatedAt: Date;
 | 
			
		||||
@@ -87,6 +88,11 @@ AiChatHistoryModel.init(
 | 
			
		||||
      type: DataTypes.JSONB,
 | 
			
		||||
      defaultValue: {},
 | 
			
		||||
    },
 | 
			
		||||
    type: {
 | 
			
		||||
      type: DataTypes.STRING,
 | 
			
		||||
      allowNull: false,
 | 
			
		||||
      defaultValue: 'keep', // keep 保留  temp 临时
 | 
			
		||||
    },
 | 
			
		||||
    version: {
 | 
			
		||||
      type: DataTypes.INTEGER,
 | 
			
		||||
      defaultValue: 0,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,8 @@
 | 
			
		||||
import type { AIConfig } from '@/provider/utils/parse-config.ts';
 | 
			
		||||
import { AIConfigParser, type AIConfig } from '@/provider/utils/parse-config.ts';
 | 
			
		||||
import { redis } from '@/modules/db.ts';
 | 
			
		||||
import { CustomError } from '@kevisual/router';
 | 
			
		||||
import { queryConfig } from '@/modules/query.ts';
 | 
			
		||||
import { log } from '@/logger/index.ts';
 | 
			
		||||
export class ChatConfigServices {
 | 
			
		||||
  cachePrefix = 'ai:chat:config';
 | 
			
		||||
  // 使用谁的模型
 | 
			
		||||
@@ -9,7 +10,7 @@ export class ChatConfigServices {
 | 
			
		||||
  // 使用者
 | 
			
		||||
  username: string;
 | 
			
		||||
  aiConfig?: AIConfig;
 | 
			
		||||
 | 
			
		||||
  isOwner: boolean;
 | 
			
		||||
  /**
 | 
			
		||||
   * username 是使用的模型的用户名,使用谁的模型
 | 
			
		||||
   * @param username
 | 
			
		||||
@@ -17,16 +18,17 @@ export class ChatConfigServices {
 | 
			
		||||
  constructor(owner: string, username: string, token?: string) {
 | 
			
		||||
    this.owner = owner;
 | 
			
		||||
    this.username = username;
 | 
			
		||||
    this.isOwner = owner === username;
 | 
			
		||||
  }
 | 
			
		||||
  getKey() {
 | 
			
		||||
    return `${this.cachePrefix}:${this.owner}`;
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * 获取chat配置
 | 
			
		||||
   * @param needClearSecret 是否需要清除secret 默认false
 | 
			
		||||
   * @param keepSecret 是否需要清除secret 默认 不清除 为true
 | 
			
		||||
   * @returns
 | 
			
		||||
   */
 | 
			
		||||
  async getChatConfig(needClearSecret = false, token?: string) {
 | 
			
		||||
  async getChatConfig(keepSecret = true, token?: string) {
 | 
			
		||||
    const key = this.getKey();
 | 
			
		||||
    const cache = await redis.get(key);
 | 
			
		||||
    let modelConfig = null;
 | 
			
		||||
@@ -35,7 +37,9 @@ export class ChatConfigServices {
 | 
			
		||||
    }
 | 
			
		||||
    if (!modelConfig) {
 | 
			
		||||
      if (this.owner !== this.username) {
 | 
			
		||||
        throw new CustomError(`the owner [${this.owner}] config, [${this.username}] not permission to init config, only owner can init config, place connect owner`);
 | 
			
		||||
        throw new CustomError(
 | 
			
		||||
          `the owner [${this.owner}] config, [${this.username}] not permission to init config, only owner can init config, place connect owner`,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        const res = await queryConfig.getConfigByKey('ai.json', { token });
 | 
			
		||||
        if (res.code === 200 && res.data?.data) {
 | 
			
		||||
@@ -53,14 +57,26 @@ export class ChatConfigServices {
 | 
			
		||||
      await redis.set(key, JSON.stringify(modelConfig), 'EX', cacheTime);
 | 
			
		||||
    }
 | 
			
		||||
    this.aiConfig = modelConfig;
 | 
			
		||||
    if (needClearSecret) {
 | 
			
		||||
    if (!keepSecret) {
 | 
			
		||||
      modelConfig = this.filterApiKey(modelConfig);
 | 
			
		||||
    }
 | 
			
		||||
    return modelConfig;
 | 
			
		||||
  }
 | 
			
		||||
  async clearCache() {
 | 
			
		||||
    const key = this.getKey();
 | 
			
		||||
    await redis.set(key, JSON.stringify({}), 'EX', 1);
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * 获取模型配置
 | 
			
		||||
   * @returns
 | 
			
		||||
   */
 | 
			
		||||
  async getSelectOpts(config?: AIConfig) {
 | 
			
		||||
    const aiConfigParser = new AIConfigParser(config || this.aiConfig);
 | 
			
		||||
    return aiConfigParser.getSelectOpts();
 | 
			
		||||
  }
 | 
			
		||||
  async filterApiKey(chatConfig: AIConfig) {
 | 
			
		||||
    // 过滤掉secret中的所有apiKey,移除掉并返回chatConfig
 | 
			
		||||
    const { secretKeys, ...rest } = chatConfig;
 | 
			
		||||
    const { secretKeys = [], ...rest } = chatConfig;
 | 
			
		||||
    return {
 | 
			
		||||
      ...rest,
 | 
			
		||||
      secretKeys: secretKeys.map((item) => {
 | 
			
		||||
@@ -128,4 +144,9 @@ export class ChatConfigServices {
 | 
			
		||||
      await redis.set(userCacheKey, JSON.stringify({ token }), 'EX', 60 * 60 * 24 * 30); // 30天
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  async clearChatLimit() {
 | 
			
		||||
    if (this.owner !== 'root') return;
 | 
			
		||||
    // const userCacheKey = `${this.cachePrefix}:root:chat-limit:${this.username}`;
 | 
			
		||||
    // await redis.del(userCacheKey);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ import { pick } from 'lodash-es';
 | 
			
		||||
import { ChastHistoryMessage } from '../models/ai-chat-history.ts';
 | 
			
		||||
import { nanoid } from '@/utils/uuid.ts';
 | 
			
		||||
import dayjs from 'dayjs';
 | 
			
		||||
import { log } from '@/logger/index.ts';
 | 
			
		||||
 | 
			
		||||
export type ChatServicesConfig = {
 | 
			
		||||
  owner: string;
 | 
			
		||||
@@ -78,22 +79,49 @@ export class ChatServices {
 | 
			
		||||
    const owner = this.owner;
 | 
			
		||||
    return `${this.cachePrefix}${owner}:${key}`;
 | 
			
		||||
  }
 | 
			
		||||
  static chatLimitKey(owner: string, key = 'chat-limit') {
 | 
			
		||||
    return `ai-chat:model:${owner}:${key}`;
 | 
			
		||||
  }
 | 
			
		||||
  static async clearChatLimit(owner: string) {
 | 
			
		||||
    const key = ChatServices.chatLimitKey(owner);
 | 
			
		||||
    const cache = await redis.get(key);
 | 
			
		||||
    if (cache) {
 | 
			
		||||
      await redis.expire(key, 2);
 | 
			
		||||
    }
 | 
			
		||||
    return cache;
 | 
			
		||||
  }
 | 
			
		||||
  async getConfig(username: string) {
 | 
			
		||||
    const services = new ChatConfigServices(this.owner, username);
 | 
			
		||||
    return services.getChatConfig();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async chat(messages: ChatMessage[], options?: ChatMessageOptions) {
 | 
			
		||||
  async chat(messages: ChatMessage[], options?: ChatMessageOptions, customOptions?: { clearThink?: boolean }) {
 | 
			
		||||
    const { model, provider, apiKey, baseURL } = this.modelConfig;
 | 
			
		||||
    const providerManager = await ProviderManager.createProvider({
 | 
			
		||||
      provider: provider,
 | 
			
		||||
      model: model,
 | 
			
		||||
      apiKey: apiKey,
 | 
			
		||||
      baseURL: baseURL,
 | 
			
		||||
    });
 | 
			
		||||
    this.chatProvider = providerManager;
 | 
			
		||||
    const result = await providerManager.chat(messages, options);
 | 
			
		||||
    return result;
 | 
			
		||||
    try {
 | 
			
		||||
      const providerManager = await ProviderManager.createProvider({
 | 
			
		||||
        provider: provider,
 | 
			
		||||
        model: model,
 | 
			
		||||
        apiKey: apiKey,
 | 
			
		||||
        baseURL: baseURL,
 | 
			
		||||
      });
 | 
			
		||||
      this.chatProvider = providerManager;
 | 
			
		||||
      const result = await providerManager.chat(messages, options);
 | 
			
		||||
      const { clearThink = true } = customOptions || {};
 | 
			
		||||
      if (clearThink) {
 | 
			
		||||
        result.choices[0].message.content = result.choices[0].message.content.replace(/<think>[\s\S]*?<\/think>/g, '');
 | 
			
		||||
      }
 | 
			
		||||
      return result;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      log.error('chat error', {
 | 
			
		||||
        errorMessage: error.message,
 | 
			
		||||
        errorStack: error.stack,
 | 
			
		||||
        provider,
 | 
			
		||||
        model,
 | 
			
		||||
        apiKey,
 | 
			
		||||
        baseURL,
 | 
			
		||||
      });
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  async createTitle(messages: ChastHistoryMessage[]) {
 | 
			
		||||
    return nanoid();
 | 
			
		||||
@@ -135,21 +163,31 @@ export class ChatServices {
 | 
			
		||||
    const { modelConfig } = this;
 | 
			
		||||
    const { tokenLimit, dayLimit, group, model } = modelConfig;
 | 
			
		||||
    const key = this.wrapperKey(`chat-limit`);
 | 
			
		||||
    const cache = await redis.get(key);
 | 
			
		||||
    if (cache) {
 | 
			
		||||
      const cacheData = JSON.parse(cache);
 | 
			
		||||
      const today = dayjs().format('YYYY-MM-DD');
 | 
			
		||||
      const current = cacheData.find((item) => item.group === group && item.model === model);
 | 
			
		||||
      const day = current[today] || 0;
 | 
			
		||||
      const token = current.token || 0;
 | 
			
		||||
      if (tokenLimit && token >= tokenLimit) {
 | 
			
		||||
        throw new CustomError(400, 'token limit exceeded');
 | 
			
		||||
      }
 | 
			
		||||
      if (dayLimit && day >= dayLimit) {
 | 
			
		||||
        throw new CustomError(400, 'day limit exceeded');
 | 
			
		||||
    try {
 | 
			
		||||
      const cache = await redis.get(key);
 | 
			
		||||
      if (cache) {
 | 
			
		||||
        const cacheData = JSON.parse(cache);
 | 
			
		||||
        const today = dayjs().format('YYYY-MM-DD');
 | 
			
		||||
        log.debug('checkCanChat', { cacheData });
 | 
			
		||||
        let current = cacheData.find((item) => item.group === group && item.model === model);
 | 
			
		||||
        if (current) {
 | 
			
		||||
          const day = current[today] || 0;
 | 
			
		||||
          const token = current.token || 0;
 | 
			
		||||
          if (tokenLimit && token >= tokenLimit) {
 | 
			
		||||
            throw new CustomError(400, 'token limit exceeded');
 | 
			
		||||
          }
 | 
			
		||||
          if (dayLimit && day >= dayLimit) {
 | 
			
		||||
            throw new CustomError(400, 'day limit exceeded');
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return true;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('checkCanChat error', error);
 | 
			
		||||
      // 如果获取失败,则设置一个空的缓存,2秒后删除
 | 
			
		||||
      await redis.set(key, '', 'EX', 2); // 2秒
 | 
			
		||||
      throw new CustomError(500, 'checkCanChat error, please try again later');
 | 
			
		||||
    }
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * 获取模型的使用情况
 | 
			
		||||
@@ -184,19 +222,27 @@ export class ChatServices {
 | 
			
		||||
    const key = this.wrapperKey(`chat-limit`);
 | 
			
		||||
    const cache = await redis.get(key);
 | 
			
		||||
    const today = dayjs().format('YYYY-MM-DD');
 | 
			
		||||
    if (cache) {
 | 
			
		||||
      const cacheData = JSON.parse(cache);
 | 
			
		||||
      const current = cacheData.find((item) => item.group === group && item.model === model);
 | 
			
		||||
      if (current) {
 | 
			
		||||
        const day = current[today] || 0;
 | 
			
		||||
        current[today] = day + 1;
 | 
			
		||||
        current.token = current.token + token;
 | 
			
		||||
    try {
 | 
			
		||||
      if (cache) {
 | 
			
		||||
        const cacheData = JSON.parse(cache);
 | 
			
		||||
        const current = cacheData.find((item) => item.group === group && item.model === model);
 | 
			
		||||
        if (current) {
 | 
			
		||||
          const day = current[today] || 0;
 | 
			
		||||
          current[today] = day + 1;
 | 
			
		||||
          current.token = current.token + token;
 | 
			
		||||
        } else {
 | 
			
		||||
          cacheData.push({ group, model, token: token, [today]: 1 });
 | 
			
		||||
        }
 | 
			
		||||
        await redis.set(key, JSON.stringify(cacheData), 'EX', 60 * 60 * 24 * 30); // 30天
 | 
			
		||||
      } else {
 | 
			
		||||
        cacheData.push({ group, model, token: token, [today]: 1 });
 | 
			
		||||
        const cacheData = { group, model, token: token, [today]: 1 };
 | 
			
		||||
        await redis.set(key, JSON.stringify([cacheData]), 'EX', 60 * 60 * 24 * 30); // 30天
 | 
			
		||||
      }
 | 
			
		||||
      await redis.set(key, JSON.stringify(cacheData), 'EX', 60 * 60 * 24 * 30); // 30天
 | 
			
		||||
    } else {
 | 
			
		||||
      await redis.set(key, JSON.stringify({ group, model, token: token, [today]: 1 }), 'EX', 60 * 60 * 24 * 30); // 30天
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('updateChatLimit error', error);
 | 
			
		||||
      // 如果更新失败,则设置一个空的缓存,2秒后删除
 | 
			
		||||
      await redis.set(key, '', 'EX', 2); // 2秒
 | 
			
		||||
      throw new CustomError(500, 'updateChatLimit error, please try again later');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,2 +1,3 @@
 | 
			
		||||
import './ai-chat/index.ts';
 | 
			
		||||
import './ai-chat/list.ts';
 | 
			
		||||
import './ai-chat/cache.ts';
 | 
			
		||||
							
								
								
									
										26
									
								
								src/test/model-scope/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/test/model-scope/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
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();
 | 
			
		||||
							
								
								
									
										6
									
								
								src/test/provider/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/test/provider/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
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);
 | 
			
		||||
		Reference in New Issue
	
	Block a user