├── .editorconfig ├── .env ├── .erb ├── configs │ ├── .eslintrc │ ├── webpack.config.base.ts │ ├── webpack.config.eslint.ts │ ├── webpack.config.main.prod.ts │ ├── webpack.config.preload.dev.ts │ ├── webpack.config.renderer.dev.dll.ts │ ├── webpack.config.renderer.dev.ts │ ├── webpack.config.renderer.prod.ts │ └── webpack.paths.ts ├── img │ ├── erb-banner.svg │ ├── erb-logo.png │ └── palette-sponsor-banner.svg ├── mocks │ └── fileMock.js └── scripts │ ├── .eslintrc │ ├── check-build-exists.ts │ ├── check-native-dep.js │ ├── check-node-env.js │ ├── check-port-in-use.js │ ├── clean.js │ ├── delete-source-maps.js │ ├── electron-builder-debug.js │ ├── electron-rebuild.js │ ├── link-modules.ts │ └── notarize.js ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── README_EN.md ├── assets ├── assets.d.ts ├── base │ ├── default-platform-icon.png │ └── windows.png ├── fonts │ └── 庞门正道标题体.ttf ├── icon.ico ├── icon.png └── icons │ ├── 128x128.png │ ├── 48x48.png │ ├── 64x64.png │ └── 96x96.png ├── docs ├── contact.png ├── contact2.png ├── first_settings_1.png ├── first_settings_2.png ├── home_settings_1.png ├── home_settings_2.png ├── intro1.png ├── intro2.png ├── intro3.png ├── intro4.png ├── intro5.png ├── intro6.png ├── license_AGPL_3.0.svg ├── logo.png ├── other_settings_1.png ├── other_settings_2.png ├── reply_settings_1.png ├── reply_settings_2.png ├── 本地开发-d4eaf7.svg ├── 相关文档-7d09f1.svg └── 立即下载-d4eaf7.svg ├── electron-builder.yml ├── package.json ├── pnpm-lock.yaml ├── release └── app │ ├── package-lock.json │ ├── package.json │ └── pnpm-lock.yaml ├── removeLocales.js ├── src ├── __tests__ │ ├── App.test.tsx │ └── gptproxy.test.js ├── main │ ├── backend │ │ ├── backend.ts │ │ ├── constants │ │ │ └── index.ts │ │ ├── controllers │ │ │ ├── configController.ts │ │ │ ├── keywordReplyController.ts │ │ │ └── messageController.ts │ │ ├── entities │ │ │ ├── config.ts │ │ │ ├── instance.ts │ │ │ ├── keyword.ts │ │ │ ├── message.ts │ │ │ ├── plugin.ts │ │ │ ├── replace.ts │ │ │ ├── session.ts │ │ │ └── transfer.ts │ │ ├── errors │ │ │ └── errors.ts │ │ ├── ormconfig.ts │ │ ├── services │ │ │ ├── appService.ts │ │ │ ├── dispatchService.ts │ │ │ ├── loggerService.ts │ │ │ ├── messageService.ts │ │ │ └── pluginService.ts │ │ └── types │ │ │ └── index.ts │ ├── cron.ts │ ├── gptproxy │ │ ├── README.md │ │ ├── dify │ │ │ ├── chat │ │ │ │ ├── chat.ts │ │ │ │ ├── completions.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ └── resources.ts │ │ ├── ernie │ │ │ ├── index.ts │ │ │ ├── resources │ │ │ │ ├── chat │ │ │ │ │ ├── chat.ts │ │ │ │ │ ├── completions.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── embeddings.ts │ │ │ │ └── index.ts │ │ │ └── util.ts │ │ ├── gemini │ │ │ ├── index.ts │ │ │ ├── resource.ts │ │ │ └── resources │ │ │ │ ├── chat │ │ │ │ ├── chat.ts │ │ │ │ ├── completions.ts │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ └── models.ts │ │ ├── hunyuan │ │ │ ├── index.ts │ │ │ ├── resource.ts │ │ │ └── resources │ │ │ │ ├── chat │ │ │ │ ├── chat.ts │ │ │ │ ├── completions.ts │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── minimax │ │ │ ├── error.ts │ │ │ ├── index.ts │ │ │ └── resources │ │ │ │ ├── audio │ │ │ │ ├── audio.ts │ │ │ │ ├── index.ts │ │ │ │ └── speech.ts │ │ │ │ ├── chat │ │ │ │ ├── chat.ts │ │ │ │ ├── completions.ts │ │ │ │ └── index.ts │ │ │ │ ├── embeddings.ts │ │ │ │ └── index.ts │ │ ├── qwen │ │ │ ├── dashscope │ │ │ │ ├── index.ts │ │ │ │ ├── resolvers │ │ │ │ │ ├── chat.ts │ │ │ │ │ ├── completions.ts │ │ │ │ │ ├── embeddings.ts │ │ │ │ │ └── index.ts │ │ │ │ └── types │ │ │ │ │ ├── chat.ts │ │ │ │ │ ├── completions.ts │ │ │ │ │ ├── embeddings.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── openai.ts │ │ │ ├── index.ts │ │ │ └── resources │ │ │ │ ├── chat │ │ │ │ ├── chat.ts │ │ │ │ ├── completions.ts │ │ │ │ └── index.ts │ │ │ │ ├── completions.ts │ │ │ │ ├── embeddings.ts │ │ │ │ ├── images.ts │ │ │ │ └── index.ts │ │ ├── resource.ts │ │ ├── spark │ │ │ ├── index.ts │ │ │ ├── resource.ts │ │ │ └── resources │ │ │ │ ├── chat │ │ │ │ ├── chat.ts │ │ │ │ ├── completions.ts │ │ │ │ └── index.ts │ │ │ │ ├── images.ts │ │ │ │ └── index.ts │ │ ├── streaming.ts │ │ ├── util.ts │ │ └── vyro │ │ │ ├── index.ts │ │ │ ├── resource.ts │ │ │ └── resources │ │ │ ├── images.ts │ │ │ └── index.ts │ ├── ipcHandlers.ts │ ├── main.ts │ ├── menu.ts │ ├── preload.ts │ ├── system │ │ ├── backend.ts │ │ ├── chrome.ts │ │ ├── cron.ts │ │ └── logger.ts │ ├── util.ts │ ├── utils │ │ ├── index.ts │ │ └── strings.ts │ └── windows │ │ ├── dataview-main │ │ └── index.ts │ │ └── settings-main │ │ └── index.ts ├── renderer │ ├── common │ │ ├── App.css │ │ ├── components │ │ │ ├── Markdown │ │ │ │ ├── index.module.scss │ │ │ │ └── index.tsx │ │ │ ├── MyModal │ │ │ │ └── index.tsx │ │ │ ├── MyTextarea │ │ │ │ └── index.tsx │ │ │ └── PageContainer │ │ │ │ └── index.tsx │ │ ├── services │ │ │ ├── analytics │ │ │ │ ├── index.ts │ │ │ │ └── index_template.ts │ │ │ ├── common │ │ │ │ └── api │ │ │ │ │ └── request.ts │ │ │ ├── platform │ │ │ │ ├── constant.ts │ │ │ │ ├── controller.ts │ │ │ │ └── platform.d.ts │ │ │ └── system │ │ │ │ └── controller.ts │ │ ├── styles │ │ │ ├── colors.ts │ │ │ ├── foundations │ │ │ │ ├── IconButton.ts │ │ │ │ └── Table.ts │ │ │ └── theme.ts │ │ └── utils │ │ │ ├── constants │ │ │ └── index.ts │ │ │ └── plugins │ │ │ └── system │ │ │ ├── index.ts │ │ │ └── normal.ts │ ├── dataview-window │ │ ├── App.tsx │ │ ├── components │ │ │ ├── DisplayContextModal │ │ │ │ └── index.tsx │ │ │ ├── EditKeyword │ │ │ │ ├── GlobalSwitch.tsx │ │ │ │ ├── KeywordInput.tsx │ │ │ │ ├── KeywordList.tsx │ │ │ │ ├── PlatformSelector.tsx │ │ │ │ ├── ReplyInput.tsx │ │ │ │ ├── ReplyList.tsx │ │ │ │ └── index.tsx │ │ │ ├── EditReplaceKeyword │ │ │ │ ├── GlobalSwitch.tsx │ │ │ │ ├── KeywordInput.tsx │ │ │ │ ├── KeywordList.tsx │ │ │ │ ├── PlatformSelector.tsx │ │ │ │ ├── ReplaceInput.tsx │ │ │ │ ├── ReplaceList.tsx │ │ │ │ └── index.tsx │ │ │ ├── EditTransferKeyword │ │ │ │ ├── GlobalSwitch.tsx │ │ │ │ ├── KeywordInput.tsx │ │ │ │ ├── KeywordList.tsx │ │ │ │ ├── PlatformSelector.tsx │ │ │ │ └── index.tsx │ │ │ ├── MessageModal │ │ │ │ └── index.tsx │ │ │ ├── ReplaceKeyword │ │ │ │ └── index.tsx │ │ │ ├── ReplyKeyword │ │ │ │ └── index.tsx │ │ │ ├── SessionHistory │ │ │ │ ├── index.css │ │ │ │ └── index.tsx │ │ │ └── TransferKeyword │ │ │ │ └── index.tsx │ │ ├── index.ejs │ │ └── index.tsx │ ├── main-window │ │ ├── App.tsx │ │ ├── components │ │ │ ├── AppManager │ │ │ │ ├── AppCardComponent.tsx │ │ │ │ ├── AppListComponent.tsx │ │ │ │ ├── AppManagerContext.tsx │ │ │ │ ├── InstanceCardComponent.tsx │ │ │ │ ├── InstanceListComponent.tsx │ │ │ │ ├── SearchBarComponent.tsx │ │ │ │ └── index.tsx │ │ │ ├── ErrorBoundary │ │ │ │ └── index.tsx │ │ │ ├── Loader │ │ │ │ ├── index.tsx │ │ │ │ └── loader.css │ │ │ ├── LogBox │ │ │ │ └── index.tsx │ │ │ ├── MyTooltip │ │ │ │ └── index.tsx │ │ │ ├── Panels │ │ │ │ └── index.tsx │ │ │ ├── SystemCheck │ │ │ │ └── index.tsx │ │ │ ├── Updater │ │ │ │ └── index.tsx │ │ │ └── layout │ │ │ │ ├── Footer.tsx │ │ │ │ └── Navbar.tsx │ │ ├── hooks │ │ │ ├── useBroadcastContext.tsx │ │ │ └── useToast.ts │ │ ├── index.ejs │ │ ├── index.tsx │ │ ├── pages │ │ │ ├── FullScreenLoader │ │ │ │ └── index.tsx │ │ │ └── Home │ │ │ │ └── index.tsx │ │ └── stores │ │ │ ├── useGlobalStore.ts │ │ │ └── useSystemStore.ts │ ├── preload.d.ts │ └── settings-window │ │ ├── App.tsx │ │ ├── components │ │ ├── About │ │ │ └── index.tsx │ │ ├── EditKeyword │ │ │ ├── ReplyInput.tsx │ │ │ ├── ReplyList.tsx │ │ │ └── index.tsx │ │ ├── LLMSettings │ │ │ ├── ThirdParty.tsx │ │ │ └── index.tsx │ │ └── Settings │ │ │ ├── AccountSettings.tsx │ │ │ └── GeneralSettings.tsx │ │ ├── index.ejs │ │ ├── index.tsx │ │ ├── pages │ │ ├── Plugin │ │ │ ├── PluginCard.tsx │ │ │ └── index.tsx │ │ └── PluginEdit │ │ │ ├── PluginBasicInfo.tsx │ │ │ ├── PluginEditor.tsx │ │ │ ├── PluginTestPage.tsx │ │ │ └── index.tsx │ │ └── stores │ │ ├── useGlobalStore.ts │ │ └── useSystemStore.ts └── types │ ├── global.d.ts │ ├── node-cron.d.ts │ └── node-windows.d.ts ├── tests └── backend.http └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PY_HOSTNAME=localhost 2 | PY_PORT=9999 3 | VAR=1234 4 | BKEXE_PATH=./backend/__main__.exe 5 | PKG_VERSION=0.0.1 -------------------------------------------------------------------------------- /.erb/configs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.base.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base webpack config used across other specific configs 3 | */ 4 | import path from 'path'; 5 | import webpack from 'webpack'; 6 | import dotenv from 'dotenv'; // 导入dotenv-webpack插件 7 | import TsconfigPathsPlugins from 'tsconfig-paths-webpack-plugin'; 8 | import webpackPaths from './webpack.paths'; 9 | import { dependencies as externals } from '../../release/app/package.json'; 10 | 11 | const configuration: webpack.Configuration = { 12 | externals: [...Object.keys(externals || {})], 13 | 14 | stats: 'errors-only', 15 | 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.[jt]sx?$/, 20 | exclude: /node_modules/, 21 | use: { 22 | loader: 'ts-loader', 23 | options: { 24 | // Remove this line to enable type checking in webpack builds 25 | transpileOnly: true, 26 | compilerOptions: { 27 | module: 'esnext', 28 | }, 29 | }, 30 | }, 31 | }, 32 | ], 33 | }, 34 | 35 | output: { 36 | path: webpackPaths.srcPath, 37 | // https://github.com/webpack/webpack/issues/1114 38 | library: { 39 | type: 'commonjs2', 40 | }, 41 | }, 42 | 43 | /** 44 | * Determine the array of extensions that should be used to resolve modules. 45 | */ 46 | resolve: { 47 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 48 | modules: [webpackPaths.srcPath, 'node_modules'], 49 | // There is no need to add aliases here, the paths in tsconfig get mirrored 50 | plugins: [new TsconfigPathsPlugins()], 51 | }, 52 | 53 | plugins: [ 54 | new webpack.EnvironmentPlugin({ 55 | NODE_ENV: 'production', 56 | ...dotenv.config({ 57 | path: path.join(webpackPaths.rootPath, '.env'), 58 | }).parsed, 59 | }), 60 | ], 61 | }; 62 | 63 | export default configuration; 64 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.eslint.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/no-unresolved: off, import/no-self-import: off */ 2 | 3 | module.exports = require('./webpack.config.renderer.dev').default; 4 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.main.prod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config for production electron main process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import { merge } from 'webpack-merge'; 8 | import TerserPlugin from 'terser-webpack-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import baseConfig from './webpack.config.base'; 11 | import webpackPaths from './webpack.paths'; 12 | import checkNodeEnv from '../scripts/check-node-env'; 13 | import deleteSourceMaps from '../scripts/delete-source-maps'; 14 | 15 | checkNodeEnv('production'); 16 | deleteSourceMaps(); 17 | 18 | const configuration: webpack.Configuration = { 19 | devtool: 'source-map', 20 | 21 | mode: 'production', 22 | 23 | target: 'electron-main', 24 | 25 | entry: { 26 | main: path.join(webpackPaths.srcMainPath, 'main.ts'), 27 | preload: path.join(webpackPaths.srcMainPath, 'preload.ts'), 28 | }, 29 | 30 | output: { 31 | path: webpackPaths.distMainPath, 32 | filename: '[name].js', 33 | library: { 34 | type: 'umd', 35 | }, 36 | }, 37 | 38 | optimization: { 39 | minimizer: [ 40 | new TerserPlugin({ 41 | parallel: true, 42 | }), 43 | ], 44 | }, 45 | 46 | plugins: [ 47 | new BundleAnalyzerPlugin({ 48 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 49 | analyzerPort: 8888, 50 | }), 51 | 52 | /** 53 | * Create global constants which can be configured at compile time. 54 | * 55 | * Useful for allowing different behaviour between development builds and 56 | * release builds 57 | * 58 | * NODE_ENV should be production so that modules do not perform certain 59 | * development checks 60 | */ 61 | new webpack.EnvironmentPlugin({ 62 | NODE_ENV: 'production', 63 | DEBUG_PROD: false, 64 | START_MINIMIZED: false, 65 | }), 66 | 67 | new webpack.DefinePlugin({ 68 | 'process.type': '"browser"', 69 | }), 70 | ], 71 | 72 | /** 73 | * Disables webpack processing of __dirname and __filename. 74 | * If you run the bundle in node.js it falls back to these values of node.js. 75 | * https://github.com/webpack/webpack/issues/2010 76 | */ 77 | node: { 78 | __dirname: false, 79 | __filename: false, 80 | }, 81 | }; 82 | 83 | export default merge(baseConfig, configuration); 84 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.preload.dev.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import { merge } from 'webpack-merge'; 4 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 5 | import baseConfig from './webpack.config.base'; 6 | import webpackPaths from './webpack.paths'; 7 | import checkNodeEnv from '../scripts/check-node-env'; 8 | 9 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's 10 | // at the dev webpack config is not accidentally run in a production environment 11 | if (process.env.NODE_ENV === 'production') { 12 | checkNodeEnv('development'); 13 | } 14 | 15 | const configuration: webpack.Configuration = { 16 | devtool: 'inline-source-map', 17 | 18 | mode: 'development', 19 | 20 | target: 'electron-preload', 21 | 22 | entry: path.join(webpackPaths.srcMainPath, 'preload.ts'), 23 | 24 | output: { 25 | path: webpackPaths.dllPath, 26 | filename: 'preload.js', 27 | library: { 28 | type: 'umd', 29 | }, 30 | }, 31 | 32 | plugins: [ 33 | new BundleAnalyzerPlugin({ 34 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 35 | }), 36 | 37 | /** 38 | * Create global constants which can be configured at compile time. 39 | * 40 | * Useful for allowing different behaviour between development builds and 41 | * release builds 42 | * 43 | * NODE_ENV should be production so that modules do not perform certain 44 | * development checks 45 | * 46 | * By default, use 'development' as NODE_ENV. This can be overriden with 47 | * 'staging', for example, by changing the ENV variables in the npm scripts 48 | */ 49 | new webpack.EnvironmentPlugin({ 50 | NODE_ENV: 'development', 51 | }), 52 | 53 | new webpack.LoaderOptionsPlugin({ 54 | debug: true, 55 | }), 56 | ], 57 | 58 | /** 59 | * Disables webpack processing of __dirname and __filename. 60 | * If you run the bundle in node.js it falls back to these values of node.js. 61 | * https://github.com/webpack/webpack/issues/2010 62 | */ 63 | node: { 64 | __dirname: false, 65 | __filename: false, 66 | }, 67 | 68 | watch: true, 69 | }; 70 | 71 | export default merge(baseConfig, configuration); 72 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.renderer.dev.dll.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Builds the DLL for development electron renderer process 3 | */ 4 | 5 | import webpack from 'webpack'; 6 | import path from 'path'; 7 | import { merge } from 'webpack-merge'; 8 | import baseConfig from './webpack.config.base'; 9 | import webpackPaths from './webpack.paths'; 10 | import { dependencies } from '../../package.json'; 11 | import checkNodeEnv from '../scripts/check-node-env'; 12 | 13 | checkNodeEnv('development'); 14 | 15 | const dist = webpackPaths.dllPath; 16 | 17 | const configuration: webpack.Configuration = { 18 | context: webpackPaths.rootPath, 19 | 20 | devtool: 'eval', 21 | 22 | mode: 'development', 23 | 24 | target: 'electron-renderer', 25 | 26 | externals: ['fsevents', 'crypto-browserify'], 27 | 28 | /** 29 | * Use `module` from `webpack.config.renderer.dev.js` 30 | */ 31 | module: require('./webpack.config.renderer.dev').default.module, 32 | 33 | entry: { 34 | renderer: Object.keys(dependencies || {}), 35 | }, 36 | 37 | output: { 38 | path: dist, 39 | filename: '[name].dev.dll.js', 40 | library: { 41 | name: 'renderer', 42 | type: 'var', 43 | }, 44 | }, 45 | 46 | plugins: [ 47 | new webpack.DllPlugin({ 48 | path: path.join(dist, '[name].json'), 49 | name: '[name]', 50 | }), 51 | 52 | /** 53 | * Create global constants which can be configured at compile time. 54 | * 55 | * Useful for allowing different behaviour between development builds and 56 | * release builds 57 | * 58 | * NODE_ENV should be production so that modules do not perform certain 59 | * development checks 60 | */ 61 | new webpack.EnvironmentPlugin({ 62 | NODE_ENV: 'development', 63 | }), 64 | 65 | new webpack.LoaderOptionsPlugin({ 66 | debug: true, 67 | options: { 68 | context: webpackPaths.srcPath, 69 | output: { 70 | path: webpackPaths.dllPath, 71 | }, 72 | }, 73 | }), 74 | ], 75 | }; 76 | 77 | export default merge(baseConfig, configuration); 78 | -------------------------------------------------------------------------------- /.erb/configs/webpack.paths.ts: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const rootPath = path.join(__dirname, '../..'); 4 | 5 | const dllPath = path.join(__dirname, '../dll'); 6 | 7 | const srcPath = path.join(rootPath, 'src'); 8 | const srcMainPath = path.join(srcPath, 'main'); 9 | const srcRendererPath = path.join(srcPath, 'renderer/main-window'); 10 | const srcSettingsRendererPath = path.join(srcPath, 'renderer/settings-window'); 11 | const srcDataviewRendererPath = path.join(srcPath, 'renderer/dataview-window'); 12 | 13 | const releasePath = path.join(rootPath, 'release'); 14 | const appPath = path.join(releasePath, 'app'); 15 | const appPackagePath = path.join(appPath, 'package.json'); 16 | const appNodeModulesPath = path.join(appPath, 'node_modules'); 17 | const srcNodeModulesPath = path.join(srcPath, 'node_modules'); 18 | 19 | const distPath = path.join(appPath, 'dist'); 20 | const distMainPath = path.join(distPath, 'main'); 21 | const distRendererPath = path.join(distPath, 'renderer'); 22 | 23 | const buildPath = path.join(releasePath, 'build'); 24 | 25 | export default { 26 | rootPath, 27 | dllPath, 28 | srcPath, 29 | srcMainPath, 30 | srcRendererPath, 31 | srcSettingsRendererPath, 32 | srcDataviewRendererPath, 33 | releasePath, 34 | appPath, 35 | appPackagePath, 36 | appNodeModulesPath, 37 | srcNodeModulesPath, 38 | distPath, 39 | distMainPath, 40 | distRendererPath, 41 | buildPath, 42 | }; 43 | -------------------------------------------------------------------------------- /.erb/img/erb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/.erb/img/erb-logo.png -------------------------------------------------------------------------------- /.erb/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /.erb/scripts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off", 6 | "import/no-extraneous-dependencies": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.erb/scripts/check-build-exists.ts: -------------------------------------------------------------------------------- 1 | // Check if the renderer and main bundles are built 2 | import path from 'path'; 3 | import chalk from 'chalk'; 4 | import fs from 'fs'; 5 | import webpackPaths from '../configs/webpack.paths'; 6 | 7 | const mainPath = path.join(webpackPaths.distMainPath, 'main.js'); 8 | const rendererPath = path.join(webpackPaths.distRendererPath, 'renderer.js'); 9 | 10 | if (!fs.existsSync(mainPath)) { 11 | throw new Error( 12 | chalk.whiteBright.bgRed.bold( 13 | 'The main process is not built yet. Build it by running "npm run build:main"', 14 | ), 15 | ); 16 | } 17 | 18 | if (!fs.existsSync(rendererPath)) { 19 | throw new Error( 20 | chalk.whiteBright.bgRed.bold( 21 | 'The renderer process is not built yet. Build it by running "npm run build:renderer"', 22 | ), 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /.erb/scripts/check-native-dep.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import chalk from 'chalk'; 3 | import { execSync } from 'child_process'; 4 | import { dependencies } from '../../package.json'; 5 | 6 | if (dependencies) { 7 | const dependenciesKeys = Object.keys(dependencies); 8 | const nativeDeps = fs 9 | .readdirSync('node_modules') 10 | .filter((folder) => fs.existsSync(`node_modules/${folder}/binding.gyp`)); 11 | if (nativeDeps.length === 0) { 12 | process.exit(0); 13 | } 14 | try { 15 | // Find the reason for why the dependency is installed. If it is installed 16 | // because of a devDependency then that is okay. Warn when it is installed 17 | // because of a dependency 18 | const { dependencies: dependenciesObject } = JSON.parse( 19 | execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString(), 20 | ); 21 | const rootDependencies = Object.keys(dependenciesObject); 22 | const filteredRootDependencies = rootDependencies.filter((rootDependency) => 23 | dependenciesKeys.includes(rootDependency), 24 | ); 25 | if (filteredRootDependencies.length > 0) { 26 | const plural = filteredRootDependencies.length > 1; 27 | console.log(` 28 | ${chalk.whiteBright.bgYellow.bold( 29 | 'Webpack does not work with native dependencies.', 30 | )} 31 | ${chalk.bold(filteredRootDependencies.join(', '))} ${ 32 | plural ? 'are native dependencies' : 'is a native dependency' 33 | } and should be installed inside of the "./release/app" folder. 34 | First, uninstall the packages from "./package.json": 35 | ${chalk.whiteBright.bgGreen.bold('npm uninstall your-package')} 36 | ${chalk.bold( 37 | 'Then, instead of installing the package to the root "./package.json":', 38 | )} 39 | ${chalk.whiteBright.bgRed.bold('npm install your-package')} 40 | ${chalk.bold('Install the package to "./release/app/package.json"')} 41 | ${chalk.whiteBright.bgGreen.bold( 42 | 'cd ./release/app && npm install your-package', 43 | )} 44 | Read more about native dependencies at: 45 | ${chalk.bold( 46 | 'https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure', 47 | )} 48 | `); 49 | process.exit(1); 50 | } 51 | } catch (e) { 52 | console.log('Native dependencies could not be checked', e); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.erb/scripts/check-node-env.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export default function checkNodeEnv(expectedEnv) { 4 | if (!expectedEnv) { 5 | throw new Error('"expectedEnv" not set'); 6 | } 7 | 8 | if (process.env.NODE_ENV !== expectedEnv) { 9 | console.log( 10 | chalk.whiteBright.bgRed.bold( 11 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config`, 12 | ), 13 | ); 14 | process.exit(2); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.erb/scripts/check-port-in-use.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import detectPort from 'detect-port'; 3 | 4 | const port = process.env.PORT || '1212'; 5 | 6 | detectPort(port, (_err, availablePort) => { 7 | if (port !== String(availablePort)) { 8 | throw new Error( 9 | chalk.whiteBright.bgRed.bold( 10 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 npm start`, 11 | ), 12 | ); 13 | } else { 14 | process.exit(0); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /.erb/scripts/clean.js: -------------------------------------------------------------------------------- 1 | import { rimrafSync } from 'rimraf'; 2 | import fs from 'fs'; 3 | import webpackPaths from '../configs/webpack.paths'; 4 | 5 | const foldersToRemove = [ 6 | webpackPaths.distPath, 7 | webpackPaths.buildPath, 8 | webpackPaths.dllPath, 9 | ]; 10 | 11 | foldersToRemove.forEach((folder) => { 12 | if (fs.existsSync(folder)) rimrafSync(folder); 13 | }); 14 | -------------------------------------------------------------------------------- /.erb/scripts/delete-source-maps.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { rimrafSync } from 'rimraf'; 4 | import webpackPaths from '../configs/webpack.paths'; 5 | 6 | export default function deleteSourceMaps() { 7 | if (fs.existsSync(webpackPaths.distMainPath)) 8 | rimrafSync(path.join(webpackPaths.distMainPath, '*.js.map'), { 9 | glob: true, 10 | }); 11 | if (fs.existsSync(webpackPaths.distRendererPath)) 12 | rimrafSync(path.join(webpackPaths.distRendererPath, '*.js.map'), { 13 | glob: true, 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /.erb/scripts/electron-builder-debug.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import chalk from 'chalk'; 3 | 4 | try { 5 | execSync('electron-builder install-app-deps', { stdio: 'inherit' }); 6 | } catch (error) { 7 | // 错误处理: 输出错误信息和退出码 8 | console.error( 9 | chalk.red('Failed to install dependencies with electron-builder:'), 10 | ); 11 | console.error(chalk.red(`Error message: ${error.message}`)); // 错误消息 12 | if (error instanceof Error && error.stack) { 13 | console.error(chalk.red(`Stack trace: ${error.stack}`)); // 堆栈跟踪 14 | } 15 | console.error(chalk.red(`Exit code: ${error.status}`)); // 错误码 16 | process.exit(error.status || 1); // 使用捕获到的退出码作为进程的退出码,或者默认为 1 17 | } 18 | -------------------------------------------------------------------------------- /.erb/scripts/electron-rebuild.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import fs from 'fs'; 3 | import { dependencies } from '../../release/app/package.json'; 4 | import webpackPaths from '../configs/webpack.paths'; 5 | 6 | if ( 7 | Object.keys(dependencies || {}).length > 0 && 8 | fs.existsSync(webpackPaths.appNodeModulesPath) 9 | ) { 10 | const electronRebuildCmd = 11 | '../../node_modules/.bin/electron-rebuild --force --types prod,dev,optional --module-dir .'; 12 | const cmd = 13 | process.platform === 'win32' 14 | ? electronRebuildCmd.replace(/\//g, '\\') 15 | : electronRebuildCmd; 16 | execSync(cmd, { 17 | cwd: webpackPaths.appPath, 18 | stdio: 'inherit', 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /.erb/scripts/link-modules.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import webpackPaths from '../configs/webpack.paths'; 3 | 4 | const { srcNodeModulesPath } = webpackPaths; 5 | const { appNodeModulesPath } = webpackPaths; 6 | 7 | if (!fs.existsSync(srcNodeModulesPath) && fs.existsSync(appNodeModulesPath)) { 8 | fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, 'junction'); 9 | } 10 | -------------------------------------------------------------------------------- /.erb/scripts/notarize.js: -------------------------------------------------------------------------------- 1 | const { notarize } = require('@electron/notarize'); 2 | const { build } = require('../../package.json'); 3 | 4 | exports.default = async function notarizeMacos(context) { 5 | const { electronPlatformName, appOutDir } = context; 6 | if (electronPlatformName !== 'darwin') { 7 | return; 8 | } 9 | 10 | if (process.env.CI !== 'true') { 11 | console.warn('Skipping notarizing step. Packaging is not running in CI'); 12 | return; 13 | } 14 | 15 | if ( 16 | !('APPLE_ID' in process.env && 'APPLE_APP_SPECIFIC_PASSWORD' in process.env) 17 | ) { 18 | console.warn( 19 | 'Skipping notarizing step. APPLE_ID and APPLE_APP_SPECIFIC_PASSWORD env variables must be set', 20 | ); 21 | return; 22 | } 23 | 24 | const appName = context.packager.appInfo.productFilename; 25 | 26 | await notarize({ 27 | appBundleId: build.appId, 28 | appPath: `${appOutDir}/${appName}.app`, 29 | appleId: process.env.APPLE_ID, 30 | appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD, 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | .eslintcache 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | 18 | # OSX 19 | .DS_Store 20 | 21 | release/app/dist 22 | release/build 23 | .erb/dll 24 | 25 | .idea 26 | npm-debug.log.* 27 | *.css.d.ts 28 | *.sass.d.ts 29 | *.scss.d.ts 30 | 31 | # eslint ignores hidden directories by default: 32 | # https://github.com/eslint/eslint/issues/8429 33 | !.erb 34 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'erb', 3 | plugins: ['@typescript-eslint'], 4 | rules: { 5 | // A temporary hack related to IDE not resolving correct package.json 6 | 'import/no-extraneous-dependencies': 'off', 7 | 'react/react-in-jsx-scope': 'off', 8 | 'react/jsx-filename-extension': 'off', 9 | 'import/extensions': 'off', 10 | 'import/no-unresolved': 'off', 11 | 'import/no-import-module-exports': 'off', 12 | 'no-shadow': 'off', 13 | '@typescript-eslint/no-shadow': 'error', 14 | 'no-unused-vars': 'off', 15 | 'react/function-component-definition': 'off', 16 | 'react/jsx-curly-brace-presence': 'off', 17 | 'react/require-default-props': 'off', 18 | 'react/jsx-props-no-spreading': 'off', 19 | 'react/destructuring-assignment': 'off', 20 | 'import/no-named-as-default': 'off', 21 | 'prefer-promise-reject-errors': 'off', 22 | 'react/jsx-no-useless-fragment': 'off', 23 | 'no-promise-executor-return': 'off', 24 | 'import/prefer-default-export': 'off', 25 | 'promise/no-promise-in-callback': 'off', 26 | 'react/no-array-index-key': 'off', 27 | 'class-methods-use-this': 'off', 28 | '@typescript-eslint/no-unused-vars': 'error', 29 | 'no-console': 'off', 30 | 'max-classes-per-file': 'off', 31 | 'no-continue': 'off', 32 | 'no-plusplus': 'off', 33 | 'no-underscore-dangle': 'off', 34 | camelcase: 'off', 35 | 'no-use-before-define': 'off', 36 | 'no-dupe-class-members': 'off', 37 | }, 38 | parserOptions: { 39 | ecmaVersion: 2022, 40 | sourceType: 'module', 41 | }, 42 | settings: { 43 | 'import/resolver': { 44 | // See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below 45 | node: {}, 46 | webpack: { 47 | config: require.resolve('./.erb/configs/webpack.config.eslint.ts'), 48 | }, 49 | typescript: {}, 50 | }, 51 | 'import/parsers': { 52 | '@typescript-eslint/parser': ['.ts', '.tsx'], 53 | }, 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.exe binary 3 | *.png binary 4 | *.jpg binary 5 | *.jpeg binary 6 | *.ico binary 7 | *.icns binary 8 | *.eot binary 9 | *.otf binary 10 | *.ttf binary 11 | *.woff binary 12 | *.woff2 binary 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | .eslintcache 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | 18 | # OSX 19 | .DS_Store 20 | 21 | release/app/dist 22 | release/build 23 | .erb/dll 24 | 25 | .idea 26 | npm-debug.log.* 27 | *.css.d.ts 28 | *.sass.d.ts 29 | *.scss.d.ts 30 | assets/backend/* 31 | src/renderer/services/analytics/* 32 | 33 | 34 | open_ai.http 35 | 36 | tests/* -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "EditorConfig.EditorConfig"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Electron: Main", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeExecutable": "npm", 9 | "runtimeArgs": ["run", "start"], 10 | "env": { 11 | "MAIN_ARGS": "--inspect=5858 --remote-debugging-port=9223" 12 | } 13 | }, 14 | { 15 | "name": "Electron: Renderer", 16 | "type": "chrome", 17 | "request": "attach", 18 | "port": 9223, 19 | "webRoot": "${workspaceFolder}", 20 | "timeout": 15000 21 | } 22 | ], 23 | "compounds": [ 24 | { 25 | "name": "Electron: All", 26 | "configurations": ["Electron: Main", "Electron: Renderer"] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | ".eslintrc": "jsonc", 4 | ".prettierrc": "jsonc", 5 | ".eslintignore": "ignore" 6 | }, 7 | 8 | "eslint.validate": [ 9 | "javascript", 10 | "javascriptreact", 11 | "html", 12 | "typescriptreact" 13 | ], 14 | 15 | "javascript.validate.enable": false, 16 | "javascript.format.enable": false, 17 | "typescript.format.enable": false, 18 | 19 | "search.exclude": { 20 | ".git": true, 21 | ".eslintcache": true, 22 | ".erb/dll": true, 23 | "release/{build,app/dist}": true, 24 | "node_modules": true, 25 | "npm-debug.log.*": true, 26 | "test/**/__snapshots__": true, 27 | "package-lock.json": true, 28 | "*.{css,sass,scss}.d.ts": true 29 | }, 30 | "cSpell.words": [ 31 | "dify", 32 | "jinritemai" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /assets/assets.d.ts: -------------------------------------------------------------------------------- 1 | type Styles = Record; 2 | 3 | declare module '*.svg' { 4 | import React = require('react'); 5 | 6 | export const ReactComponent: React.FC>; 7 | 8 | const content: string; 9 | export default content; 10 | } 11 | 12 | declare module '*.png' { 13 | const content: string; 14 | export default content; 15 | } 16 | 17 | declare module '*.jpg' { 18 | const content: string; 19 | export default content; 20 | } 21 | 22 | declare module '*.scss' { 23 | const content: Styles; 24 | export default content; 25 | } 26 | 27 | declare module '*.sass' { 28 | const content: Styles; 29 | export default content; 30 | } 31 | 32 | declare module '*.css' { 33 | const content: Styles; 34 | export default content; 35 | } 36 | 37 | declare module '*.gif' { 38 | const content: Styles; 39 | export default content; 40 | } 41 | -------------------------------------------------------------------------------- /assets/base/default-platform-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/assets/base/default-platform-icon.png -------------------------------------------------------------------------------- /assets/base/windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/assets/base/windows.png -------------------------------------------------------------------------------- /assets/fonts/庞门正道标题体.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/assets/fonts/庞门正道标题体.ttf -------------------------------------------------------------------------------- /assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/assets/icon.ico -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/assets/icon.png -------------------------------------------------------------------------------- /assets/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/assets/icons/128x128.png -------------------------------------------------------------------------------- /assets/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/assets/icons/48x48.png -------------------------------------------------------------------------------- /assets/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/assets/icons/64x64.png -------------------------------------------------------------------------------- /assets/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/assets/icons/96x96.png -------------------------------------------------------------------------------- /docs/contact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/docs/contact.png -------------------------------------------------------------------------------- /docs/contact2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/docs/contact2.png -------------------------------------------------------------------------------- /docs/first_settings_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/docs/first_settings_1.png -------------------------------------------------------------------------------- /docs/first_settings_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/docs/first_settings_2.png -------------------------------------------------------------------------------- /docs/home_settings_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/docs/home_settings_1.png -------------------------------------------------------------------------------- /docs/home_settings_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/docs/home_settings_2.png -------------------------------------------------------------------------------- /docs/intro1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/docs/intro1.png -------------------------------------------------------------------------------- /docs/intro2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/docs/intro2.png -------------------------------------------------------------------------------- /docs/intro3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/docs/intro3.png -------------------------------------------------------------------------------- /docs/intro4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/docs/intro4.png -------------------------------------------------------------------------------- /docs/intro5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/docs/intro5.png -------------------------------------------------------------------------------- /docs/intro6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/docs/intro6.png -------------------------------------------------------------------------------- /docs/license_AGPL_3.0.svg: -------------------------------------------------------------------------------- 1 | License: Apache-2.0LicenseAGPL-3.0 -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/docs/logo.png -------------------------------------------------------------------------------- /docs/other_settings_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/docs/other_settings_1.png -------------------------------------------------------------------------------- /docs/other_settings_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/docs/other_settings_2.png -------------------------------------------------------------------------------- /docs/reply_settings_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/docs/reply_settings_1.png -------------------------------------------------------------------------------- /docs/reply_settings_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cs-lazy-tools/ChatGPT-On-CS/53125497c184dd4ad80727e5164d375480d8f1e8/docs/reply_settings_2.png -------------------------------------------------------------------------------- /docs/相关文档-7d09f1.svg: -------------------------------------------------------------------------------- 1 | 相关文档相关文档 -------------------------------------------------------------------------------- /docs/立即下载-d4eaf7.svg: -------------------------------------------------------------------------------- 1 | 立即下载立即下载 -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | appId: org.lrhh123.cocs 2 | productName: 懒人客服 3 | asar: true 4 | asarUnpack: "**\\*.{node,dll}" 5 | compression: maximum 6 | files: 7 | - dist 8 | - node_modules 9 | - package.json 10 | afterSign: ".erb/scripts/notarize.js" 11 | win: 12 | requestedExecutionLevel: requireAdministrator 13 | target: 14 | - nsis 15 | nsis: 16 | oneClick: false 17 | perMachine: true 18 | allowElevation: true 19 | allowToChangeInstallationDirectory: true 20 | createDesktopShortcut: true 21 | createStartMenuShortcut: true 22 | shortcutName: 懒人客服 23 | uninstallDisplayName: ChatGPT-On-CS 24 | deleteAppDataOnUninstall: true 25 | artifactName: "${productName} ${version}.${ext}" 26 | directories: 27 | app: release/app 28 | buildResources: assets 29 | output: release/build 30 | extraResources: 31 | - "./assets/**" 32 | afterPack: "./removeLocales.js" -------------------------------------------------------------------------------- /release/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-on-cs", 3 | "version": "1.4.5", 4 | "description": "多平台智能客服,允许使用 ChatGPT 作为客服机器人", 5 | "license": "AGPL-3.0", 6 | "author": { 7 | "name": "lrhh123", 8 | "email": "lrhh123@users.noreply.github.com", 9 | "url": "https://github.com/lrhh123" 10 | }, 11 | "main": "./dist/main/main.js", 12 | "scripts": { 13 | "rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js", 14 | "postinstall": "pnpm run rebuild && pnpm run link-modules", 15 | "link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts" 16 | }, 17 | "dependencies": { 18 | "sqlite3": "^5.1.6" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /removeLocales.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | module.exports = async function (params) { 5 | // Assuming 'locales' directory is directly inside the app output directory 6 | const localesDir = path.join(params.appOutDir, 'locales'); 7 | 8 | try { 9 | const files = await fs.promises.readdir(localesDir); 10 | for (const file of files) { 11 | if (!file.endsWith('zh-CN.pak')) { 12 | await fs.promises.unlink(path.join(localesDir, file)); 13 | } 14 | } 15 | } catch (err) { 16 | console.error('Error removing locales:', err); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/__tests__/App.test.tsx: -------------------------------------------------------------------------------- 1 | // import '@testing-library/jest-dom'; 2 | // import { render } from '@testing-library/react'; 3 | // import App from '../renderer/App'; 4 | // import { isVersionGreater } from '../renderer/services/system/controller'; 5 | 6 | // describe('App', () => { 7 | // it('should render', () => { 8 | // expect(render()).toBeTruthy(); 9 | // }); 10 | // }); 11 | 12 | // describe('Version Comparison Tests', () => { 13 | // test('Standard version comparison where online version is greater', () => { 14 | // expect(isVersionGreater('1.0.1', '1.0.0')).toBe(true); 15 | // }); 16 | 17 | // test('Standard version comparison where current version is greater', () => { 18 | // expect(isVersionGreater('1.0.0', '1.0.1')).toBe(false); 19 | // }); 20 | 21 | // test('Beta version comparison with higher beta number', () => { 22 | // expect(isVersionGreater('1.0.0-beta.2', '1.0.0-beta.1')).toBe(true); 23 | // }); 24 | 25 | // test('Beta version comparison with lower beta number', () => { 26 | // expect(isVersionGreater('1.0.0-beta.1', '1.0.0-beta.2')).toBe(false); 27 | // }); 28 | 29 | // test('Beta versus stable where stable should be greater', () => { 30 | // expect(isVersionGreater('1.0.0', '1.0.0-beta.1')).toBe(true); 31 | // }); 32 | 33 | // test('Stable versus beta where beta should be lesser', () => { 34 | // expect(isVersionGreater('1.0.0-beta.1', '1.0.0')).toBe(false); 35 | // }); 36 | 37 | // test('Identical versions should not be greater', () => { 38 | // expect(isVersionGreater('1.0.0', '1.0.0')).toBe(false); 39 | // }); 40 | 41 | // test('Beta suffix without number compared to numeric beta suffix', () => { 42 | // expect(isVersionGreater('1.0.0-beta', '1.0.0-beta.1')).toBe(false); 43 | // }); 44 | 45 | // test('Numeric beta suffix compared to beta suffix without number', () => { 46 | // expect(isVersionGreater('1.0.0-beta.1', '1.0.0-beta')).toBe(true); 47 | // }); 48 | 49 | // test('Beta version comparison with identical suffix and number', () => { 50 | // expect(isVersionGreater('1.0.0-beta.1', '1.0.0-beta.1')).toBe(false); 51 | // }); 52 | // }); 53 | -------------------------------------------------------------------------------- /src/__tests__/gptproxy.test.js: -------------------------------------------------------------------------------- 1 | import 'openai/shims/node'; 2 | import fetch from 'node-fetch'; 3 | 4 | global.fetch = fetch; 5 | 6 | // eslint-disable-next-line import/first 7 | import { DifyAI } from '../main/gptproxy'; 8 | 9 | process.env.DEBUG = 'true'; 10 | 11 | // '{"code": "invalid_param", "message": "Missing required parameter in the JSON body", "params": "inputs"} 12 | 13 | describe('DifyAI', () => { 14 | it('chat generation', async () => { 15 | const dify = new DifyAI({ 16 | apiKey: 'app-5uPNxwQ6Q8bU6FzO3WZ72tqV', 17 | baseURL: 'https://api.dify.ai/v1', 18 | }); 19 | const response = await dify.chat.completions.create({ 20 | model: 'gpt-3.5-turbo', 21 | stream: true, 22 | messages: [ 23 | // { role: 'user', content: 'Hello, how are you?' }, 24 | // { role: 'assistant', content: 'I am fine, thank you.' }, 25 | { role: 'user', content: 'Hi' }, 26 | ], 27 | }); 28 | expect(response).toBeDefined(); 29 | // 打印返回结果 30 | console.log(response); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/main/backend/constants/index.ts: -------------------------------------------------------------------------------- 1 | // 固定会传递的上下文参数 2 | export const CTX_APP_NAME = 'CTX_APP_NAME'; 3 | export const CTX_APP_ID = 'CTX_APP_ID'; 4 | export const CTX_INSTANCE_ID = 'CTX_INSTANCE_ID'; 5 | 6 | export const CTX_USERNAME = 'CTX_USERNAME'; // 当前操作的用户名 7 | export const CTX_PLATFORM = 'CTX_PLATFORM'; // 当前所在平台 8 | export const CTX_HAS_NEW_MESSAGE = 'CTX_HAS_NEW_MESSAGE'; // 是否有新消息 9 | export const CTX_HAS_GROUP_MESSAGE = 'CTX_HAS_GROUP_MESSAGE'; // 是否有群消息 10 | 11 | // 电商平台 12 | export const CTX_CURRENT_GOODS = 'CTX_CURRENT_GOODS'; // 当前商品 13 | export const CTX_CURRENT_GOODS_ID = 'CTX_CURRENT_GOODS_ID'; // 当前商品 ID 14 | export const CTX_MEMBER_TAG = 'CTX_MEMBER_TAG'; // 会员标签 15 | export const CTX_FAN_TAG = 'CTX_FAN_TAG'; // 粉丝标签 16 | export const CTX_NEW_CUSTOMER_TAG = 'CTX_NEW_CUSTOMER_TAG'; // 新客标签 17 | 18 | export const CTX_ORDER_STATUS = 'CTX_ORDER_STATUS'; // 订单状态 19 | export const CTX_ORDER_ID = 'CTX_ORDER_ID'; // 订单 ID 20 | export const CTX_ORDER_AMOUNT = 'CTX_ORDER_AMOUNT'; // PDD 平台特有 [订单金额] 21 | export const CTX_GOODS_SPEC = 'CTX_GOODS_SPEC'; // PDD 平台特有 [商品规格] 22 | export const CTX_LOGISTICS_STATUS = 'CTX_LOGISTICS_STATUS'; // 物流状态 23 | 24 | export const PluginDefaultRunCode = ` 25 | const cc = require('config_srv'); 26 | const rp = require('reply_srv'); 27 | 28 | /** 29 | * 插件主函数 30 | * @param {AppContext} ctx - 上下文信息 31 | * @param {Message[]} messages - 消息数组 32 | * @returns {Reply} 插件执行结果 33 | */ 34 | async function main(ctx, messages) { 35 | const cfg = await cc.get(ctx); 36 | return await rp.getReply(cfg, ctx, messages); 37 | }`; 38 | -------------------------------------------------------------------------------- /src/main/backend/entities/instance.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model, Sequelize } from 'sequelize'; 2 | 3 | export class Instance extends Model { 4 | declare id: number; // instance_id 5 | 6 | declare app_id: string; 7 | 8 | declare env_id: string; 9 | 10 | declare created_at: string; 11 | } 12 | 13 | export function initInstance(sequelize: Sequelize) { 14 | Instance.init( 15 | { 16 | id: { 17 | type: DataTypes.INTEGER, 18 | autoIncrement: true, 19 | primaryKey: true, 20 | }, 21 | app_id: { 22 | type: DataTypes.STRING(255), 23 | allowNull: false, 24 | }, 25 | env_id: { 26 | type: DataTypes.STRING(255), 27 | allowNull: true, 28 | }, 29 | created_at: { 30 | type: DataTypes.DATE, 31 | allowNull: false, 32 | }, 33 | }, 34 | { 35 | sequelize, 36 | modelName: 'Instance', 37 | tableName: 'instance', 38 | timestamps: false, 39 | }, 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/main/backend/entities/keyword.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model, Sequelize } from 'sequelize'; 2 | 3 | // Extend the Model class with the attributes interface 4 | export class Keyword extends Model { 5 | declare id: number; // Note that the `null assertion` `!` is required in strict mode. 6 | 7 | declare keyword: string; 8 | 9 | declare reply: string; 10 | 11 | declare mode: string; 12 | 13 | declare platform_id: string; 14 | 15 | declare fuzzy: boolean; 16 | 17 | declare has_regular: boolean; 18 | } 19 | 20 | export async function checkAndAddFields(sequelize: Sequelize) { 21 | const tableDescription = await Keyword.describe(); 22 | 23 | // @ts-ignore 24 | if (!tableDescription.fuzzy) { 25 | await sequelize.getQueryInterface().addColumn('keyword', 'fuzzy', { 26 | type: DataTypes.BOOLEAN, 27 | allowNull: true, 28 | defaultValue: true, 29 | }); 30 | } 31 | 32 | // @ts-ignore 33 | if (!tableDescription.has_regular) { 34 | await sequelize.getQueryInterface().addColumn('keyword', 'has_regular', { 35 | type: DataTypes.BOOLEAN, 36 | allowNull: true, 37 | defaultValue: false, 38 | }); 39 | } 40 | } 41 | 42 | export function initKeyword(sequelize: Sequelize) { 43 | Keyword.init( 44 | { 45 | id: { 46 | type: DataTypes.INTEGER, 47 | autoIncrement: true, 48 | primaryKey: true, 49 | }, 50 | keyword: { 51 | type: DataTypes.STRING(255), 52 | allowNull: false, 53 | }, 54 | reply: { 55 | type: DataTypes.TEXT, 56 | allowNull: false, 57 | }, 58 | mode: { 59 | type: DataTypes.STRING(55), 60 | allowNull: false, 61 | }, 62 | platform_id: { 63 | type: DataTypes.STRING(255), 64 | allowNull: true, 65 | }, 66 | fuzzy: { 67 | type: DataTypes.BOOLEAN, 68 | allowNull: true, 69 | defaultValue: true, 70 | }, 71 | has_regular: { 72 | type: DataTypes.BOOLEAN, 73 | allowNull: true, 74 | defaultValue: false, 75 | }, 76 | }, 77 | { 78 | sequelize, 79 | modelName: 'Keyword', 80 | tableName: 'keyword', 81 | timestamps: false, 82 | }, 83 | ); 84 | 85 | checkAndAddFields(sequelize); 86 | } 87 | -------------------------------------------------------------------------------- /src/main/backend/entities/message.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model, Sequelize } from 'sequelize'; 2 | 3 | // Extend the Model class with the attributes interface 4 | export class Message extends Model { 5 | declare id: number; 6 | 7 | declare session_id: number; 8 | 9 | declare role: string; 10 | 11 | declare content: string; 12 | 13 | declare sender: string; 14 | 15 | declare type: string; 16 | 17 | declare created_at: Date; 18 | } 19 | 20 | export function initMessage(sequelize: Sequelize) { 21 | Message.init( 22 | { 23 | id: { 24 | type: DataTypes.INTEGER, 25 | autoIncrement: true, 26 | primaryKey: true, 27 | }, 28 | session_id: { 29 | type: DataTypes.INTEGER, 30 | allowNull: true, 31 | }, 32 | role: { 33 | type: DataTypes.STRING(100), 34 | allowNull: true, 35 | }, 36 | sender: { 37 | type: DataTypes.STRING(100), 38 | allowNull: true, 39 | }, 40 | content: { 41 | type: DataTypes.TEXT, 42 | allowNull: true, 43 | }, 44 | type: { 45 | type: DataTypes.STRING(55), 46 | allowNull: true, 47 | }, 48 | created_at: { 49 | type: DataTypes.DATE, 50 | allowNull: true, 51 | }, 52 | }, 53 | { 54 | sequelize, 55 | modelName: 'Message', 56 | tableName: 'n_messages', 57 | timestamps: false, 58 | }, 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /src/main/backend/entities/replace.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model, Sequelize } from 'sequelize'; 2 | 3 | // Extend the Model class with the attributes interface 4 | export class ReplaceKeyword extends Model { 5 | declare id: number; // Note that the `null assertion` `!` is required in strict mode. 6 | 7 | declare keyword: string; 8 | 9 | declare app_id: string; 10 | 11 | declare replace: string; 12 | 13 | declare fuzzy: boolean; 14 | 15 | declare has_regular: boolean; 16 | } 17 | 18 | export function initReplace(sequelize: Sequelize) { 19 | ReplaceKeyword.init( 20 | { 21 | id: { 22 | type: DataTypes.INTEGER, 23 | autoIncrement: true, 24 | primaryKey: true, 25 | }, 26 | keyword: { 27 | type: DataTypes.STRING(255), 28 | allowNull: false, 29 | }, 30 | replace: { 31 | type: DataTypes.STRING(255), 32 | allowNull: false, 33 | }, 34 | has_regular: { 35 | type: DataTypes.BOOLEAN, 36 | allowNull: true, 37 | defaultValue: false, 38 | }, 39 | app_id: { 40 | type: DataTypes.STRING(255), 41 | allowNull: true, 42 | }, 43 | fuzzy: { 44 | type: DataTypes.BOOLEAN, 45 | allowNull: true, 46 | defaultValue: true, 47 | }, 48 | }, 49 | { 50 | sequelize, 51 | modelName: 'Replace', 52 | tableName: 'replace', 53 | timestamps: false, 54 | }, 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/main/backend/entities/session.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model, Sequelize } from 'sequelize'; 2 | 3 | export class Session extends Model { 4 | declare id: number; 5 | 6 | declare platform: string; 7 | 8 | declare platform_id: string; 9 | 10 | declare instance_id: string; // 可能是作用于单个实例的插件 11 | 12 | declare context: string; 13 | 14 | declare created_at: Date; 15 | } 16 | 17 | export function initSession(sequelize: Sequelize) { 18 | Session.init( 19 | { 20 | id: { 21 | type: DataTypes.INTEGER, 22 | autoIncrement: true, 23 | primaryKey: true, 24 | }, 25 | platform: { 26 | type: DataTypes.STRING(255), 27 | allowNull: true, 28 | }, 29 | platform_id: { 30 | type: DataTypes.STRING(255), 31 | allowNull: true, 32 | }, 33 | context: { 34 | type: DataTypes.JSON, 35 | allowNull: true, 36 | }, 37 | created_at: { 38 | type: DataTypes.DATE, 39 | allowNull: true, 40 | }, 41 | }, 42 | { 43 | sequelize, 44 | modelName: 'Session', 45 | tableName: 'n_sessions', 46 | timestamps: false, 47 | }, 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/main/backend/entities/transfer.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model, Sequelize } from 'sequelize'; 2 | 3 | // Extend the Model class with the attributes interface 4 | export class TransferKeyword extends Model { 5 | declare id: number; // Note that the `null assertion` `!` is required in strict mode. 6 | 7 | declare keyword: string; 8 | 9 | declare app_id: string; 10 | 11 | declare fuzzy: boolean; 12 | 13 | declare has_regular: boolean; 14 | } 15 | 16 | export function initTransfer(sequelize: Sequelize) { 17 | TransferKeyword.init( 18 | { 19 | id: { 20 | type: DataTypes.INTEGER, 21 | autoIncrement: true, 22 | primaryKey: true, 23 | }, 24 | keyword: { 25 | type: DataTypes.STRING(255), 26 | allowNull: false, 27 | }, 28 | has_regular: { 29 | type: DataTypes.BOOLEAN, 30 | allowNull: true, 31 | defaultValue: false, 32 | }, 33 | app_id: { 34 | type: DataTypes.STRING(255), 35 | allowNull: true, 36 | }, 37 | fuzzy: { 38 | type: DataTypes.BOOLEAN, 39 | allowNull: true, 40 | defaultValue: true, 41 | }, 42 | }, 43 | { 44 | sequelize, 45 | modelName: 'Transfer', 46 | tableName: 'transfer', 47 | timestamps: false, 48 | }, 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/main/backend/errors/errors.ts: -------------------------------------------------------------------------------- 1 | export class TimeoutError extends Error {} 2 | 3 | export class HumanTaskError extends Error {} 4 | -------------------------------------------------------------------------------- /src/main/backend/services/loggerService.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron'; 2 | 3 | export class LoggerService { 4 | constructor(private mainWindow: BrowserWindow) { 5 | this.mainWindow = mainWindow; 6 | } 7 | 8 | public log(msg: string) { 9 | console.log(msg); 10 | this.mainWindow.webContents.send('broadcast', { 11 | event: 'log_show', 12 | data: { 13 | time: new Date().toLocaleTimeString(), 14 | content: msg, 15 | }, 16 | }); 17 | } 18 | 19 | public error(msg: string) { 20 | console.error('[ERROR]', msg); 21 | this.mainWindow.webContents.send('broadcast', { 22 | event: 'log_show', 23 | data: { 24 | time: new Date().toLocaleTimeString(), 25 | content: `[ERROR] ${msg}`, 26 | }, 27 | }); 28 | } 29 | 30 | public info(msg: string) { 31 | console.info('[INFO]', msg); 32 | this.mainWindow.webContents.send('broadcast', { 33 | event: 'log_show', 34 | data: { 35 | time: new Date().toLocaleTimeString(), 36 | content: `[INFO] ${msg}`, 37 | }, 38 | }); 39 | } 40 | 41 | public warn(msg: string) { 42 | console.warn('[WARN]', msg); 43 | this.mainWindow.webContents.send('broadcast', { 44 | event: 'log_show', 45 | data: { 46 | time: new Date().toLocaleTimeString(), 47 | content: `[WARN] ${msg}`, 48 | }, 49 | }); 50 | } 51 | 52 | public success(msg: string) { 53 | console.log('[SUCCESS]', msg); 54 | this.mainWindow.webContents.send('broadcast', { 55 | event: 'log_show', 56 | data: { 57 | time: new Date().toLocaleTimeString(), 58 | content: `[SUCCESS] ${msg}`, 59 | }, 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/backend/types/index.ts: -------------------------------------------------------------------------------- 1 | export enum StrategyServiceStatusEnum { 2 | RUNNING = 'RUNNING', 3 | STOPPED = 'STOPPED', 4 | } 5 | 6 | export enum PlatformTypeEnum { 7 | HOT = 'HOT', 8 | E_COMMERCE = 'E_COMMERCE', 9 | RECRUIT = 'RECRUIT', 10 | LAW = 'LAW', 11 | OTHER = 'OTHER', 12 | ME_MEDIA = 'ME_MEDIA', 13 | } 14 | 15 | export type RoleType = 'SELF' | 'OTHER' | 'SYSTEM'; 16 | export type MessageType = 'TEXT' | 'IMAGE' | 'VIDEO' | 'FILE' | 'NO_REPLY'; 17 | 18 | export type Context = Map; 19 | 20 | export interface MessageDTO { 21 | sender: string; 22 | content: string; 23 | role: RoleType; // assistant, user 24 | type: MessageType; 25 | } 26 | 27 | export interface ReplyDTO { 28 | content: string; 29 | type: MessageType; 30 | } 31 | 32 | export interface Platform { 33 | id: string; 34 | name: string; 35 | type?: string; 36 | avatar?: string; 37 | desc?: string; 38 | env?: string; 39 | } 40 | 41 | export interface GenericConfig { 42 | appId: string; 43 | instanceId: string; 44 | extractPhone: boolean; 45 | extractProduct: boolean; 46 | savePath: string; 47 | replySpeed: number; 48 | replyRandomSpeed: number; 49 | contextCount: number; 50 | waitHumansTime: number; 51 | defaultReply: string; 52 | truncateWordCount: number; 53 | truncateWordKey: string; 54 | jinritemaiDefaultReplyMatch: string; 55 | } 56 | 57 | export interface LLMConfig { 58 | appId: string; 59 | instanceId: string; 60 | baseUrl: string; 61 | key: string; 62 | llmType: string; 63 | model: string; 64 | } 65 | 66 | export interface AccountConfig { 67 | activationCode: string; 68 | } 69 | 70 | export interface PluginConfig { 71 | appId: string; 72 | instanceId: string; 73 | usePlugin: boolean; 74 | pluginId: number; 75 | } 76 | 77 | export interface DriverConfig { 78 | hasPaused: boolean; 79 | hasKeywordMatch: boolean; 80 | hasUseGpt: boolean; 81 | hasMouseClose: boolean; 82 | hasEscClose: boolean; 83 | hasTransfer: boolean; 84 | hasReplace: boolean; 85 | } 86 | -------------------------------------------------------------------------------- /src/main/cron.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { BrowserWindow } from 'electron'; 3 | import { setCron } from './system/cron'; 4 | import type BackendServiceManager from './system/backend'; 5 | 6 | const setupCron = (mainWindow: BrowserWindow, bsm: BackendServiceManager) => { 7 | const baseURL = (url: string) => { 8 | return `http://127.0.0.1:${bsm.getPort()}/${url}`; 9 | }; 10 | 11 | // 每隔 5 秒执行一次,通知和渲染进程刷新配置 12 | setCron('*/5 * * * * *', () => { 13 | mainWindow.webContents.send('refresh-config'); 14 | }); 15 | 16 | // 每隔 5 秒执行一次检查后端服务是否健康 17 | setCron('*/5 * * * * *', async () => { 18 | if (!bsm) { 19 | console.error('BackendServiceManager not found'); 20 | return; 21 | } 22 | 23 | const { 24 | data: { data }, 25 | } = await axios.get(baseURL(`api/v1/base/health`)); 26 | mainWindow.webContents.send('check-health', data); 27 | }); 28 | 29 | // 每隔 5 秒同步一次 Backend 服务的状态 30 | setCron('*/20 * * * * *', async () => { 31 | if (!bsm) { 32 | console.error('BackendServiceManager not found'); 33 | return; 34 | } 35 | 36 | // 为了避免依赖麻烦,这里直接通过 axios 发送请求 37 | try { 38 | await axios.post(baseURL('api/v1/base/sync'), {}); 39 | } catch (error) { 40 | console.error('Error syncing backend service status:', error); 41 | } 42 | }); 43 | }; 44 | 45 | export default setupCron; 46 | -------------------------------------------------------------------------------- /src/main/gptproxy/README.md: -------------------------------------------------------------------------------- 1 | 2 | fork from https://github.com/zhengxs2018/ai -------------------------------------------------------------------------------- /src/main/gptproxy/dify/chat/chat.ts: -------------------------------------------------------------------------------- 1 | import { APIResource } from '../../resource'; 2 | import { Completions } from './completions'; 3 | 4 | export class Chat extends APIResource { 5 | completions = new Completions(this._client); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/gptproxy/dify/chat/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | type ChatModel, 3 | type ChatCompletionCreateParams, 4 | type ChatCompletionCreateParamsNonStreaming, 5 | type ChatCompletionCreateParamsStreaming, 6 | Completions, 7 | } from './completions'; 8 | export { Chat } from './chat'; 9 | -------------------------------------------------------------------------------- /src/main/gptproxy/dify/index.ts: -------------------------------------------------------------------------------- 1 | import type { Agent } from 'node:http'; 2 | 3 | import { 4 | APIClient, 5 | type DefaultQuery, 6 | type Fetch, 7 | type FinalRequestOptions, 8 | type Headers, 9 | } from 'openai/core'; 10 | import * as API from './resources'; 11 | 12 | export interface DifyAIOptions { 13 | baseURL?: string; 14 | apiKey?: string; 15 | timeout?: number | undefined; 16 | httpAgent?: Agent; 17 | fetch?: Fetch | undefined; 18 | defaultHeaders?: Headers; 19 | defaultQuery?: DefaultQuery; 20 | } 21 | 22 | export class DifyAI extends APIClient { 23 | protected apiKey: string; 24 | 25 | private _options: DifyAIOptions; 26 | 27 | constructor(options: DifyAIOptions = {}) { 28 | const { 29 | apiKey = process.env.DIFY_API_KEY || '', 30 | baseURL = 'https://api.dify.ai/v1/', 31 | timeout = 50000, 32 | httpAgent = undefined, 33 | ...rest 34 | } = options; 35 | 36 | super({ 37 | baseURL, 38 | timeout, 39 | fetch, 40 | httpAgent, 41 | ...rest, 42 | }); 43 | 44 | this._options = options; 45 | 46 | this.apiKey = apiKey; 47 | } 48 | 49 | completions = new API.Completions(this); 50 | 51 | chat = new API.Chat(this); 52 | 53 | protected override defaultHeaders(opts: FinalRequestOptions): Headers { 54 | return { 55 | ...super.defaultHeaders(opts), 56 | ...this._options.defaultHeaders, 57 | Authorization: `Bearer ${this.apiKey}`, 58 | 'Content-Type': 'application/json', 59 | }; 60 | } 61 | 62 | protected override defaultQuery(): DefaultQuery | undefined { 63 | return { 64 | ...this._options.defaultQuery, 65 | }; 66 | } 67 | } 68 | 69 | export default DifyAI; 70 | -------------------------------------------------------------------------------- /src/main/gptproxy/dify/resources.ts: -------------------------------------------------------------------------------- 1 | export * from './chat/index'; 2 | -------------------------------------------------------------------------------- /src/main/gptproxy/ernie/index.ts: -------------------------------------------------------------------------------- 1 | import type { Agent } from 'node:http'; 2 | 3 | import { 4 | APIClient, 5 | type DefaultQuery, 6 | type Fetch, 7 | type FinalRequestOptions, 8 | type Headers, 9 | } from 'openai/core'; 10 | 11 | import * as API from './resources'; 12 | 13 | export interface ErnieAIOptions { 14 | baseURL?: string; 15 | apiKey?: string; 16 | timeout?: number | undefined; 17 | httpAgent?: Agent; 18 | fetch?: Fetch | undefined; 19 | 20 | /** 21 | * Default headers to include with every request to the API. 22 | * 23 | * These can be removed in individual requests by explicitly setting the 24 | * header to `undefined` or `null` in request options. 25 | */ 26 | defaultHeaders?: Headers; 27 | 28 | /** 29 | * Default query parameters to include with every request to the API. 30 | * 31 | * These can be removed in individual requests by explicitly setting the 32 | * param to `undefined` in request options. 33 | */ 34 | defaultQuery?: DefaultQuery; 35 | } 36 | 37 | export class ErnieAI extends APIClient { 38 | protected apiKey: string; 39 | 40 | private _options: ErnieAIOptions; 41 | 42 | constructor(options: ErnieAIOptions = {}) { 43 | const { 44 | apiKey = process.env.EB_API_KEY || '', 45 | baseURL = 'https://aistudio.baidu.com/llm/lmapi/v1', 46 | timeout = 30000, 47 | httpAgent = undefined, 48 | ...rest 49 | } = options; 50 | 51 | super({ 52 | baseURL, 53 | timeout, 54 | fetch, 55 | httpAgent, 56 | ...rest, 57 | }); 58 | 59 | this._options = options; 60 | 61 | this.apiKey = apiKey; 62 | } 63 | 64 | chat = new API.Chat(this); 65 | 66 | embeddings = new API.Embeddings(this); 67 | 68 | protected override authHeaders() { 69 | return { 70 | Authorization: `token ${this.apiKey}`, 71 | }; 72 | } 73 | 74 | protected override defaultHeaders(opts: FinalRequestOptions): Headers { 75 | return { 76 | ...super.defaultHeaders(opts), 77 | ...this._options.defaultHeaders, 78 | }; 79 | } 80 | 81 | protected override defaultQuery(): DefaultQuery | undefined { 82 | return this._options.defaultQuery; 83 | } 84 | } 85 | 86 | // eslint-disable-next-line no-redeclare 87 | export namespace ErnieAI { 88 | export type Chat = API.Chat; 89 | export type ChatModel = API.ChatModel; 90 | export type ChatCompletionCreateParams = API.ChatCompletionCreateParams; 91 | export type ChatCompletionCreateParamsNonStreaming = 92 | API.ChatCompletionCreateParamsNonStreaming; 93 | export type ChatCompletionCreateParamsStreaming = 94 | API.ChatCompletionCreateParamsStreaming; 95 | 96 | export type EmbeddingCreateParams = API.EmbeddingCreateParams; 97 | } 98 | 99 | export default ErnieAI; 100 | -------------------------------------------------------------------------------- /src/main/gptproxy/ernie/resources/chat/chat.ts: -------------------------------------------------------------------------------- 1 | import { APIResource } from '../../../resource'; 2 | import { Completions } from './completions'; 3 | 4 | export class Chat extends APIResource { 5 | completions = new Completions(this._client); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/gptproxy/ernie/resources/chat/index.ts: -------------------------------------------------------------------------------- 1 | export { Chat } from './chat'; 2 | export { 3 | type ChatModel, 4 | type ChatCompletionCreateParams, 5 | type ChatCompletionCreateParamsNonStreaming, 6 | type ChatCompletionCreateParamsStreaming, 7 | Completions, 8 | } from './completions'; 9 | -------------------------------------------------------------------------------- /src/main/gptproxy/ernie/resources/embeddings.ts: -------------------------------------------------------------------------------- 1 | import OpenAI, { APIError, OpenAIError } from 'openai'; 2 | import { type RequestOptions } from 'openai/core'; 3 | 4 | import { APIResource } from '../../resource'; 5 | 6 | export class Embeddings extends APIResource { 7 | protected endpoints: Record = { 8 | 'ernie-text-embedding': '/embeddings/embedding-v1', 9 | }; 10 | 11 | /** 12 | * Creates an embedding vector representing the input text. 13 | * 14 | * See https://cloud.baidu.com/doc/WENXINWORKSHOP/s/alj562vvu 15 | */ 16 | async create( 17 | params: EmbeddingCreateParams, 18 | options?: RequestOptions, 19 | ): Promise { 20 | const { model, user, input } = params; 21 | const endpoint = this.endpoints[model]; 22 | 23 | if (!endpoint) { 24 | throw new OpenAIError(`Invalid model: ${model}`); 25 | } 26 | 27 | const body = { 28 | input, 29 | user_id: user, 30 | }; 31 | 32 | const response: Response = await this._client.post(endpoint, { 33 | body, 34 | ...options, 35 | __binaryResponse: true, 36 | }); 37 | 38 | return Embeddings.fromResponse(model, await response.json()); 39 | } 40 | 41 | static fromResponse( 42 | model: EmbeddingModel, 43 | data: CreateEmbeddingResponse, 44 | ): OpenAI.CreateEmbeddingResponse { 45 | Embeddings.assert(data); 46 | 47 | const { result } = data; 48 | 49 | return { 50 | data: result.data, 51 | model, 52 | object: 'list', 53 | usage: result.usage, 54 | }; 55 | } 56 | 57 | /** 58 | * 如果 code 不为 0,抛出 APIError 59 | * 60 | * @param code - 61 | * @param message - 62 | */ 63 | static assert(resp: CreateEmbeddingResponse) { 64 | if (resp.errorCode === 0) return; 65 | 66 | const error = { code: resp.errorCode, message: resp.errorMsg }; 67 | 68 | throw APIError.generate(undefined, error, undefined, undefined); 69 | } 70 | } 71 | 72 | export type EmbeddingModel = 'ernie-text-embedding'; 73 | 74 | export interface EmbeddingCreateParams { 75 | /** 76 | * 输入文本 77 | */ 78 | input: string | Array | Array | Array>; 79 | 80 | /** 81 | * 模型 82 | */ 83 | model: EmbeddingModel; 84 | 85 | /** 86 | * 用户 ID 87 | */ 88 | user?: string; 89 | } 90 | 91 | type CreateEmbeddingResponse = { 92 | errorCode: number; 93 | errorMsg: string; 94 | result: OpenAI.CreateEmbeddingResponse; 95 | }; 96 | -------------------------------------------------------------------------------- /src/main/gptproxy/ernie/resources/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chat/index'; 2 | 3 | export { Embeddings, type EmbeddingCreateParams } from './embeddings'; 4 | -------------------------------------------------------------------------------- /src/main/gptproxy/ernie/util.ts: -------------------------------------------------------------------------------- 1 | import { APIError } from 'openai'; 2 | 3 | /** 4 | * 构建错误 5 | * 6 | * @param code - 7 | * @param message - 8 | * @returns 错误 9 | */ 10 | export function makeAPIError(code: number, message: string) { 11 | const error = { code, message }; 12 | 13 | switch (code) { 14 | case 2: 15 | return APIError.generate(500, error, message, {}); 16 | case 6: // permission error 17 | case 111: // token expired 18 | return APIError.generate(403, error, message, {}); 19 | case 17: 20 | case 18: 21 | case 19: 22 | case 40407: 23 | return APIError.generate(429, error, message, {}); 24 | case 110: // invalid token 25 | case 40401: // invalid token 26 | return APIError.generate(401, error, message, {}); 27 | case 336003: // invalid parameter 28 | return APIError.generate(400, error, message, {}); 29 | case 336100: // try again 30 | return APIError.generate(500, error, message, {}); 31 | default: 32 | return APIError.generate(undefined, error, message, {}); 33 | } 34 | } 35 | 36 | /** 37 | * 如果 code 不为 0,抛出 APIError 38 | * 39 | * @param code - 40 | * @param message - 41 | */ 42 | export function assertNonZero(code: number, message: string) { 43 | if (code === 0) return; 44 | 45 | throw makeAPIError(code, message); 46 | } 47 | -------------------------------------------------------------------------------- /src/main/gptproxy/gemini/index.ts: -------------------------------------------------------------------------------- 1 | import type { Agent } from 'node:http'; 2 | 3 | import { 4 | APIClient, 5 | type DefaultQuery, 6 | type Fetch, 7 | type FinalRequestOptions, 8 | type Headers, 9 | } from 'openai/core'; 10 | 11 | import * as API from './resources'; 12 | 13 | const BASE_URL = 'https://generativelanguage.googleapis.com/v1'; 14 | 15 | export interface GeminiAIOptions { 16 | baseURL?: string; 17 | apiKey?: string; 18 | timeout?: number | undefined; 19 | httpAgent?: Agent; 20 | fetch?: Fetch | undefined; 21 | defaultHeaders?: Headers; 22 | defaultQuery?: DefaultQuery; 23 | } 24 | 25 | export class GeminiAI extends APIClient { 26 | apiKey: string; 27 | 28 | private _options: GeminiAIOptions; 29 | 30 | constructor(options: GeminiAIOptions = {}) { 31 | const { 32 | apiKey = process.env.GEMINI_API_KEY || '', 33 | baseURL = process.env.GEMINI_BASE_URL || BASE_URL, 34 | timeout = 30000, 35 | httpAgent = undefined, 36 | ...rest 37 | } = options; 38 | 39 | super({ 40 | baseURL, 41 | timeout, 42 | fetch, 43 | httpAgent, 44 | ...rest, 45 | }); 46 | 47 | this._options = options; 48 | 49 | this.apiKey = apiKey; 50 | } 51 | 52 | chat = new API.Chat(this); 53 | 54 | models = new API.Models(this); 55 | 56 | protected override defaultHeaders(opts: FinalRequestOptions): Headers { 57 | return { 58 | ...super.defaultHeaders(opts), 59 | ...this._options.defaultHeaders, 60 | }; 61 | } 62 | 63 | protected override defaultQuery(): DefaultQuery | undefined { 64 | return { 65 | ...this._options.defaultQuery, 66 | key: this.apiKey, 67 | }; 68 | } 69 | } 70 | 71 | // eslint-disable-next-line no-redeclare 72 | export namespace GeminiAI { 73 | export type ChatModel = API.ChatModel; 74 | export type ChatCompletionCreateParams = API.ChatCompletionCreateParams; 75 | export type ChatCompletionCreateParamsStreaming = 76 | API.ChatCompletionCreateParamsStreaming; 77 | export type ChatCompletionCreateParamsNonStreaming = 78 | API.ChatCompletionCreateParamsNonStreaming; 79 | } 80 | 81 | export default GeminiAI; 82 | -------------------------------------------------------------------------------- /src/main/gptproxy/gemini/resource.ts: -------------------------------------------------------------------------------- 1 | import type { GeminiAI } from './index'; 2 | 3 | export class APIResource { 4 | protected _client: GeminiAI; 5 | 6 | constructor(client: GeminiAI) { 7 | this._client = client; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/gptproxy/gemini/resources/chat/chat.ts: -------------------------------------------------------------------------------- 1 | import { Completions } from './completions'; 2 | import { APIResource } from '../../resource'; 3 | 4 | export class Chat extends APIResource { 5 | completions = new Completions(this._client); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/gptproxy/gemini/resources/chat/index.ts: -------------------------------------------------------------------------------- 1 | export { Chat } from './chat'; 2 | export { 3 | type ChatModel, 4 | type ChatCompletionCreateParams, 5 | type ChatCompletionCreateParamsNonStreaming, 6 | type ChatCompletionCreateParamsStreaming, 7 | Completions, 8 | } from './completions'; 9 | -------------------------------------------------------------------------------- /src/main/gptproxy/gemini/resources/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chat/index'; 2 | 3 | export { Models, type Model, ModelsPage } from './models'; 4 | -------------------------------------------------------------------------------- /src/main/gptproxy/gemini/resources/models.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | import { 3 | type FinalRequestOptions, 4 | type PagePromise, 5 | type RequestOptions, 6 | } from 'openai/core'; 7 | import { Page } from 'openai/pagination'; 8 | 9 | import { type GeminiAI } from '../../index'; 10 | import { APIResource } from '../resource'; 11 | 12 | // TODO 输出原始对象 13 | export class Models extends APIResource { 14 | /** 15 | * Retrieves a model instance, providing basic information about the model such as 16 | * the owner and permissioning. 17 | */ 18 | async retrieve(model: string, options?: RequestOptions): Promise { 19 | const item: GeminiModel = await this._client.get( 20 | `/models/${model}`, 21 | options, 22 | ); 23 | 24 | return { 25 | id: item.name, 26 | created: 0, 27 | object: 'model', 28 | owned_by: 'google', 29 | }; 30 | } 31 | 32 | /** 33 | * Lists the currently available models, and provides basic information about each 34 | * one such as the owner and availability. 35 | */ 36 | list(options?: RequestOptions): PagePromise { 37 | return this._client.getAPIList('/models', ModelsPage, options); 38 | } 39 | } 40 | 41 | export class ModelsPage extends Page { 42 | constructor( 43 | client: GeminiAI, 44 | response: Response, 45 | body: GeminiPageResponse, 46 | options: FinalRequestOptions, 47 | ) { 48 | const data: Model[] = body.models.map((item) => { 49 | return { 50 | id: item.name, 51 | created: 0, 52 | object: 'model', 53 | owned_by: 'google', 54 | }; 55 | }); 56 | 57 | // @ts-ignore 58 | super(client, response, { data, object: 'list' }, options); 59 | } 60 | } 61 | 62 | interface GeminiModel { 63 | name: string; 64 | version: string; 65 | displayName: string; 66 | description: string; 67 | inputTokenLimit: string; 68 | outputTokenLimit: string; 69 | supportedGenerationMethods: string[]; 70 | } 71 | 72 | interface GeminiPageResponse { 73 | models: GeminiModel[]; 74 | } 75 | 76 | /** 77 | * Describes an OpenAI model offering that can be used with the API. 78 | */ 79 | export type Model = OpenAI.Models.Model; 80 | 81 | // eslint-disable-next-line no-redeclare 82 | export namespace Models { 83 | export import Model = OpenAI.Models.Model; 84 | // eslint-disable-next-line @typescript-eslint/no-shadow 85 | export import ModelsPage = OpenAI.Models.ModelsPage; 86 | } 87 | -------------------------------------------------------------------------------- /src/main/gptproxy/hunyuan/resource.ts: -------------------------------------------------------------------------------- 1 | import type { HunYuanAI } from './index'; 2 | 3 | export class APIResource { 4 | protected _client: HunYuanAI; 5 | 6 | constructor(client: HunYuanAI) { 7 | this._client = client; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/gptproxy/hunyuan/resources/chat/chat.ts: -------------------------------------------------------------------------------- 1 | import { APIResource } from '../../resource'; 2 | import { Completions } from './completions'; 3 | 4 | export class Chat extends APIResource { 5 | completions = new Completions(this._client); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/gptproxy/hunyuan/resources/chat/index.ts: -------------------------------------------------------------------------------- 1 | export { Chat } from './chat'; 2 | export { 3 | type ChatModel, 4 | type ChatCompletionCreateParams, 5 | type ChatCompletionCreateParamsNonStreaming, 6 | type ChatCompletionCreateParamsStreaming, 7 | Completions, 8 | } from './completions'; 9 | -------------------------------------------------------------------------------- /src/main/gptproxy/hunyuan/resources/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chat/index'; 2 | -------------------------------------------------------------------------------- /src/main/gptproxy/index.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | 3 | import ErnieAI, { ErnieAIOptions } from './ernie'; 4 | import GeminiAI, { GeminiAIOptions } from './gemini'; 5 | import HunYuanAI, { HunYuanAIOptions } from './hunyuan'; 6 | import MinimaxAI, { MinimaxAIOptions } from './minimax'; 7 | import QWenAI, { QWenAIOptions } from './qwen'; 8 | import SparkAI, { SparkAIOptions } from './spark'; 9 | import VYroAI, { VYroAIOptions } from './vyro'; 10 | import DifyAI, { DifyAIOptions } from './dify'; 11 | 12 | export { 13 | ErnieAI, 14 | type ErnieAIOptions, 15 | GeminiAI, 16 | type GeminiAIOptions, 17 | HunYuanAI, 18 | type HunYuanAIOptions, 19 | MinimaxAI, 20 | type MinimaxAIOptions, 21 | OpenAI, 22 | QWenAI, 23 | type QWenAIOptions, 24 | SparkAI, 25 | type SparkAIOptions, 26 | VYroAI, 27 | type VYroAIOptions, 28 | DifyAI, 29 | type DifyAIOptions, 30 | }; 31 | 32 | export { 33 | OpenAIError, 34 | APIError, 35 | APIConnectionError, 36 | APIConnectionTimeoutError, 37 | APIUserAbortError, 38 | NotFoundError, 39 | ConflictError, 40 | RateLimitError, 41 | BadRequestError, 42 | AuthenticationError, 43 | InternalServerError, 44 | PermissionDeniedError, 45 | UnprocessableEntityError, 46 | } from 'openai'; 47 | 48 | export * from './resource'; 49 | export * from './streaming'; 50 | export * from './util'; 51 | 52 | export default { 53 | version: process.env.PKG_VERSION, 54 | }; 55 | -------------------------------------------------------------------------------- /src/main/gptproxy/minimax/error.ts: -------------------------------------------------------------------------------- 1 | import { APIError } from 'openai'; 2 | 3 | export type MinimaxAPIResponse = { 4 | base_resp: { 5 | status_code: number; 6 | status_msg: string; 7 | }; 8 | }; 9 | 10 | export function assertStatusCode(data: MinimaxAPIResponse) { 11 | if (data.base_resp.status_code === 0) return; 12 | 13 | const error = { 14 | code: data.base_resp.status_code, 15 | message: data.base_resp.status_msg, 16 | }; 17 | 18 | throw new APIError(undefined, error, undefined, undefined); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/gptproxy/minimax/index.ts: -------------------------------------------------------------------------------- 1 | import type { Agent } from 'node:http'; 2 | 3 | import { 4 | APIClient, 5 | type DefaultQuery, 6 | type Fetch, 7 | type FinalRequestOptions, 8 | type Headers, 9 | } from 'openai/core'; 10 | 11 | import * as API from './resources'; 12 | 13 | export interface MinimaxAIOptions { 14 | baseURL?: string; 15 | orgId?: string; 16 | apiKey?: string; 17 | timeout?: number | undefined; 18 | httpAgent?: Agent; 19 | fetch?: Fetch | undefined; 20 | defaultHeaders?: Headers; 21 | defaultQuery?: DefaultQuery; 22 | } 23 | 24 | export class MinimaxAI extends APIClient { 25 | protected orgId: string; 26 | 27 | protected apiKey: string; 28 | 29 | private _options: MinimaxAIOptions; 30 | 31 | constructor(options: MinimaxAIOptions = {}) { 32 | const { 33 | orgId = process.env.MINIMAX_API_ORG || '', 34 | apiKey = process.env.MINIMAX_API_KEY || '', 35 | baseURL = 'https://api.minimax.chat/v1', 36 | timeout = 30000, 37 | httpAgent = undefined, 38 | ...rest 39 | } = options; 40 | 41 | super({ 42 | baseURL, 43 | timeout, 44 | fetch, 45 | httpAgent, 46 | ...rest, 47 | }); 48 | 49 | this._options = options; 50 | 51 | this.apiKey = apiKey; 52 | this.orgId = orgId; 53 | } 54 | 55 | audio = new API.Audio(this); 56 | 57 | chat = new API.Chat(this); 58 | 59 | embeddings = new API.Embeddings(this); 60 | 61 | protected authHeaders(): Headers { 62 | return { 63 | Authorization: `Bearer ${this.apiKey}`, 64 | }; 65 | } 66 | 67 | protected override defaultHeaders(opts: FinalRequestOptions): Headers { 68 | return { 69 | ...super.defaultHeaders(opts), 70 | ...this._options.defaultHeaders, 71 | }; 72 | } 73 | 74 | protected override defaultQuery(): DefaultQuery | undefined { 75 | return { 76 | GroupId: this.orgId, 77 | ...this._options.defaultQuery, 78 | }; 79 | } 80 | } 81 | 82 | // eslint-disable-next-line no-redeclare 83 | export namespace MinimaxAI { 84 | export type Chat = API.Chat; 85 | export type ChatModel = API.ChatModel; 86 | export type ChatCompletionCreateParams = API.ChatCompletionCreateParams; 87 | export type ChatCompletionCreateParamsNonStreaming = 88 | API.ChatCompletionCreateParamsNonStreaming; 89 | export type ChatCompletionCreateParamsStreaming = 90 | API.ChatCompletionCreateParamsStreaming; 91 | 92 | export type Embeddings = API.Embeddings; 93 | export type EmbeddingCreateParams = API.EmbeddingCreateParams; 94 | 95 | export type Audio = API.Audio; 96 | } 97 | 98 | export default MinimaxAI; 99 | -------------------------------------------------------------------------------- /src/main/gptproxy/minimax/resources/audio/audio.ts: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. 2 | import { APIResource } from '../../../resource'; 3 | import { Speech } from './speech'; 4 | 5 | export class Audio extends APIResource { 6 | speech = new Speech(this._client); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/gptproxy/minimax/resources/audio/index.ts: -------------------------------------------------------------------------------- 1 | export { Audio } from './audio'; 2 | export { type SpeechCreateParams, type SpeechModel, Speech } from './speech'; 3 | -------------------------------------------------------------------------------- /src/main/gptproxy/minimax/resources/chat/chat.ts: -------------------------------------------------------------------------------- 1 | import { APIResource } from '../../../resource'; 2 | import { Completions } from './completions'; 3 | 4 | export class Chat extends APIResource { 5 | completions = new Completions(this._client); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/gptproxy/minimax/resources/chat/index.ts: -------------------------------------------------------------------------------- 1 | export { Chat } from './chat'; 2 | export { 3 | type ChatModel, 4 | type ChatCompletionCreateParams, 5 | type ChatCompletionCreateParamsNonStreaming, 6 | type ChatCompletionCreateParamsStreaming, 7 | Completions, 8 | } from './completions'; 9 | -------------------------------------------------------------------------------- /src/main/gptproxy/minimax/resources/embeddings.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | import { type RequestOptions } from 'openai/core'; 3 | 4 | import { APIResource } from '../../resource'; 5 | import { assertStatusCode } from '../error'; 6 | 7 | export class Embeddings extends APIResource { 8 | /** 9 | * Creates an embedding vector representing the input text. 10 | * 11 | * See https://api.minimax.chat/document/guides/Embeddings 12 | */ 13 | async create( 14 | params: EmbeddingCreateParams, 15 | options?: RequestOptions, 16 | ): Promise { 17 | const { model, input, type = 'query' } = params; 18 | 19 | const response: Response = await this._client.post('/embeddings', { 20 | body: { 21 | model, 22 | texts: input, 23 | type, 24 | }, 25 | ...options, 26 | __binaryResponse: true, 27 | }); 28 | 29 | const data: CreateEmbeddingResponse = await response.json(); 30 | 31 | assertStatusCode(data); 32 | 33 | return { 34 | data: data.vectors.map((embedding, index) => { 35 | return { 36 | embedding, 37 | index, 38 | object: 'embedding', 39 | }; 40 | }), 41 | model, 42 | object: 'list', 43 | usage: { 44 | prompt_tokens: data.total_tokens, 45 | total_tokens: data.total_tokens, 46 | }, 47 | }; 48 | } 49 | } 50 | 51 | export interface EmbeddingCreateParams extends OpenAI.EmbeddingCreateParams { 52 | /** 53 | * 模型 54 | */ 55 | model: 'embo-01'; 56 | 57 | /** 58 | * 首先通过db生成目标内容的向量并存储到向量数据库中,之后通过query生成检索文本的向量。 59 | */ 60 | type?: 'db' | 'query'; 61 | } 62 | 63 | type CreateEmbeddingResponse = { 64 | vectors: number[][]; 65 | total_tokens: number; 66 | base_resp: { 67 | status_code: number; 68 | status_msg: string; 69 | }; 70 | }; 71 | -------------------------------------------------------------------------------- /src/main/gptproxy/minimax/resources/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chat/index'; 2 | export * from './audio/audio'; 3 | 4 | export { Embeddings, type EmbeddingCreateParams } from './embeddings'; 5 | -------------------------------------------------------------------------------- /src/main/gptproxy/qwen/dashscope/index.ts: -------------------------------------------------------------------------------- 1 | export * from './resolvers'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /src/main/gptproxy/qwen/dashscope/resolvers/embeddings.ts: -------------------------------------------------------------------------------- 1 | import { OpenAI } from 'openai'; 2 | 3 | import { DashscopeEmbeddings, OpenAIEmbeddingsCompatibility } from '../types'; 4 | 5 | export function fromEmbeddingCreatePrams( 6 | params: OpenAIEmbeddingsCompatibility.EmbeddingCreateParams, 7 | ): DashscopeEmbeddings.EmbeddingCreateParams { 8 | return { 9 | model: params.model, 10 | input: { 11 | texts: params.input, 12 | }, 13 | parameters: { 14 | text_type: params.type || 'query', 15 | }, 16 | }; 17 | } 18 | 19 | export function toEmbedding( 20 | params: OpenAIEmbeddingsCompatibility.EmbeddingCreateParams, 21 | response: DashscopeEmbeddings.CreateEmbeddingResponse, 22 | ): OpenAI.CreateEmbeddingResponse { 23 | const { output, usage } = response; 24 | 25 | return { 26 | object: 'list', 27 | model: params.model, 28 | data: output.embeddings.map(({ text_index, embedding }) => ({ 29 | index: text_index, 30 | embedding, 31 | object: 'embedding', 32 | })), 33 | usage: { 34 | prompt_tokens: usage.total_tokens, 35 | total_tokens: usage.total_tokens, 36 | }, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/main/gptproxy/qwen/dashscope/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chat'; 2 | export * from './completions'; 3 | -------------------------------------------------------------------------------- /src/main/gptproxy/qwen/dashscope/types/chat.ts: -------------------------------------------------------------------------------- 1 | import type OpenAI from 'openai'; 2 | 3 | import type { DashscopeCompletions } from './completions'; 4 | 5 | export namespace DashscopeChat { 6 | /** 7 | * https://help.aliyun.com/zh/dashscope/developer-reference/model-square 8 | */ 9 | export type ChatModel = DashscopeCompletions.CompletionModel; 10 | 11 | export type ResponseFinish = 12 | | 'stop' 13 | | 'length' 14 | | 'tool_calls' 15 | | 'content_filter' 16 | | 'function_call' 17 | | 'null'; 18 | 19 | export interface ChatCompletionParametersParam 20 | extends DashscopeCompletions.CompletionParametersParam { 21 | /** 22 | * 指定可供模型调用的工具列表 23 | * 24 | * 当输入多个工具时,模型会选择其中一个生成结果。 25 | * 26 | * 警告: 27 | * 28 | * - tools 暂时无法和 incremental_output 参数同时使用 29 | * - 使用 tools 时需要同时指定 result_format 为 message 30 | */ 31 | tools?: OpenAI.ChatCompletionTool[]; 32 | } 33 | 34 | export interface ChatCompletionCreateParams { 35 | model: ({} & string) | ChatModel; 36 | input: { 37 | messages: OpenAI.ChatCompletionMessageParam[]; 38 | }; 39 | parameters?: ChatCompletionParametersParam; 40 | } 41 | 42 | export namespace ChatCompletion { 43 | export interface Output { 44 | text: string; 45 | finish_reason?: ResponseFinish; 46 | choices: OpenAI.ChatCompletion.Choice[]; 47 | } 48 | } 49 | 50 | /** 51 | * 详见 [输入参数配置](https://help.aliyun.com/zh/dashscope/developer-reference/api-details) 52 | */ 53 | export interface ChatCompletion { 54 | request_id: string; 55 | usage: DashscopeCompletions.CompletionUsage; 56 | output: ChatCompletion.Output; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/gptproxy/qwen/dashscope/types/embeddings.ts: -------------------------------------------------------------------------------- 1 | export namespace DashscopeEmbeddings { 2 | export type EmbeddingModel = 3 | | 'text-embedding-v1' 4 | | 'text-embedding-async-v1' 5 | | 'text-embedding-v2' 6 | | 'text-embedding-async-v2'; 7 | 8 | export interface EmbeddingCreateParams { 9 | /** 10 | * 模型 11 | */ 12 | model: ({} & string) | EmbeddingModel; 13 | input: { 14 | /** 15 | * 输入文本 16 | */ 17 | texts: string | Array | Array | Array>; 18 | }; 19 | parameters: { 20 | /** 21 | * 文本转换为向量后可以应用于检索、聚类、分类等下游任务,对检索这类非对称任务为了达到更好的检索效果 22 | * 建议区分查询文本(query)和底库文本(document)类型, 23 | * 聚类、分类等对称任务可以不用特殊指定,采用系统默认值"document"即可 24 | * 25 | * @defaultValue 'query' 26 | */ 27 | text_type?: 'query' | 'document'; 28 | }; 29 | } 30 | 31 | export type Embedding = { 32 | text_index: number; 33 | embedding: number[]; 34 | }; 35 | 36 | export type CreateEmbeddingResponse = { 37 | request_id: string; 38 | code: string; 39 | message: string; 40 | output: { 41 | embeddings: Embedding[]; 42 | }; 43 | usage: { 44 | total_tokens: number; 45 | }; 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/main/gptproxy/qwen/dashscope/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chat'; 2 | export * from './completions'; 3 | export * from './embeddings'; 4 | export * from './openai'; 5 | -------------------------------------------------------------------------------- /src/main/gptproxy/qwen/resources/chat/chat.ts: -------------------------------------------------------------------------------- 1 | import { APIResource } from '../../../resource'; 2 | import { OpenAIChatCompatibility } from '../../dashscope'; 3 | import { Completions } from './completions'; 4 | 5 | export class Chat extends APIResource { 6 | completions = new Completions(this._client); 7 | } 8 | 9 | export type ChatModel = OpenAIChatCompatibility.ChatModel; 10 | export type ChatCompletionCreateParams = 11 | OpenAIChatCompatibility.ChatCompletionCreateParams; 12 | export type ChatCompletionCreateParamsNonStreaming = 13 | OpenAIChatCompatibility.ChatCompletionCreateParams; 14 | export type ChatCompletionCreateParamsStreaming = 15 | OpenAIChatCompatibility.ChatCompletionCreateParamsStreaming; 16 | -------------------------------------------------------------------------------- /src/main/gptproxy/qwen/resources/chat/completions.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | import { type Headers } from 'openai/core'; 3 | import { Stream } from 'openai/streaming'; 4 | 5 | import { APIResource } from '../../../resource'; 6 | import { 7 | fromChatCompletionCreateParams, 8 | getCompletionCreateEndpoint, 9 | type OpenAIChatCompatibility, 10 | toChatCompletion, 11 | toChatCompletionStream, 12 | } from '../../dashscope'; 13 | 14 | export class Completions extends APIResource { 15 | /** 16 | * Creates a model response for the given chat conversation. 17 | * 18 | * See https://help.aliyun.com/zh/dashscope/developer-reference/api-details 19 | */ 20 | create( 21 | body: OpenAIChatCompatibility.ChatCompletionCreateParamsNonStreaming, 22 | options?: OpenAI.RequestOptions, 23 | ): Promise; 24 | 25 | create( 26 | body: OpenAIChatCompatibility.ChatCompletionCreateParamsStreaming, 27 | options?: OpenAI.RequestOptions, 28 | ): Promise>; 29 | 30 | async create( 31 | body: OpenAIChatCompatibility.ChatCompletionCreateParams, 32 | options?: OpenAI.RequestOptions, 33 | ) { 34 | const headers: Headers = { 35 | ...options?.headers, 36 | }; 37 | 38 | if (body.stream) { 39 | headers.Accept = 'text/event-stream'; 40 | } 41 | 42 | const path = getCompletionCreateEndpoint(body.model); 43 | const params = fromChatCompletionCreateParams(body); 44 | 45 | const response: Response = await this._client.post(path, { 46 | ...options, 47 | body: params, 48 | headers, 49 | // 通义千问的响应内容被包裹了一层,需要解构并转换为 OpenAI 的格式 50 | // 设置 __binaryResponse 为 true, 是为了让 client 返回原始的 response 51 | stream: false, 52 | __binaryResponse: true, 53 | }); 54 | 55 | if (body.stream) { 56 | const controller = new AbortController(); 57 | 58 | options?.signal?.addEventListener('abort', () => { 59 | controller.abort(); 60 | }); 61 | 62 | return toChatCompletionStream(params, response, controller); 63 | } 64 | 65 | return toChatCompletion(params, await response.json()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/gptproxy/qwen/resources/chat/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chat'; 2 | -------------------------------------------------------------------------------- /src/main/gptproxy/qwen/resources/completions.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | import { type Headers } from 'openai/core'; 3 | import { Stream } from 'openai/streaming'; 4 | 5 | import { APIResource } from '../../resource'; 6 | import { 7 | fromCompletionCreateParams, 8 | getCompletionCreateEndpoint, 9 | type OpenAICompletionsCompatibility, 10 | toCompletion, 11 | toCompletionStream, 12 | } from '../dashscope'; 13 | 14 | export class Completions extends APIResource { 15 | /** 16 | * Creates a completion for the provided prompt and parameters. 17 | */ 18 | create( 19 | body: OpenAICompletionsCompatibility.CompletionCreateParamsNonStreaming, 20 | options?: OpenAI.RequestOptions, 21 | ): Promise; 22 | 23 | create( 24 | body: OpenAICompletionsCompatibility.CompletionCreateParamsStreaming, 25 | options?: OpenAI.RequestOptions, 26 | ): Promise>; 27 | 28 | create( 29 | body: OpenAICompletionsCompatibility.CompletionCreateParamsBase, 30 | options?: OpenAI.RequestOptions, 31 | ): Promise | OpenAI.Completion>; 32 | 33 | async create( 34 | body: OpenAICompletionsCompatibility.CompletionCreateParams, 35 | options?: OpenAI.RequestOptions, 36 | ): Promise> { 37 | const headers: Headers = { 38 | ...options?.headers, 39 | }; 40 | 41 | if (body.stream) { 42 | headers.Accept = 'text/event-stream'; 43 | } 44 | 45 | const path = getCompletionCreateEndpoint(body.model); 46 | const params = fromCompletionCreateParams(body); 47 | 48 | const response: Response = await this._client.post(path, { 49 | ...options, 50 | body: params, 51 | headers, 52 | // 通义千问的响应内容被包裹了一层,需要解构并转换为 OpenAI 的格式 53 | // 设置 __binaryResponse 为 true, 是为了让 client 返回原始的 response 54 | stream: false, 55 | __binaryResponse: true, 56 | }); 57 | 58 | if (body.stream) { 59 | const controller = new AbortController(); 60 | 61 | options?.signal?.addEventListener('abort', () => { 62 | controller.abort(); 63 | }); 64 | 65 | return toCompletionStream(params, response, controller); 66 | } 67 | 68 | return toCompletion(params, await response.json()); 69 | } 70 | } 71 | 72 | export type CompletionModel = OpenAICompletionsCompatibility.CompletionModel; 73 | export type CompletionCreateParams = 74 | OpenAICompletionsCompatibility.CompletionCreateParams; 75 | export type CompletionCreateParamsStreaming = 76 | OpenAICompletionsCompatibility.CompletionCreateParamsStreaming; 77 | export type CompletionCreateParamsNonStreaming = 78 | OpenAICompletionsCompatibility.CompletionCreateParamsNonStreaming; 79 | -------------------------------------------------------------------------------- /src/main/gptproxy/qwen/resources/embeddings.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | import { type RequestOptions } from 'openai/core'; 3 | 4 | import { APIResource } from '../../resource'; 5 | import { DashscopeEmbeddings } from '../dashscope'; 6 | import { 7 | fromEmbeddingCreatePrams, 8 | toEmbedding, 9 | } from '../dashscope/resolvers/embeddings'; 10 | 11 | export class Embeddings extends APIResource { 12 | /** 13 | * Creates an embedding vector representing the input text. 14 | * 15 | * See https://help.aliyun.com/zh/dashscope/developer-reference/generic-text-vector 16 | */ 17 | async create( 18 | params: OpenAI.EmbeddingCreateParams, 19 | options?: RequestOptions, 20 | ): Promise { 21 | const body = fromEmbeddingCreatePrams(params); 22 | 23 | const response: Response = await this._client.post( 24 | '/services/embeddings/text-embedding/text-embedding', 25 | { 26 | ...options, 27 | body, 28 | __binaryResponse: true, 29 | }, 30 | ); 31 | 32 | return toEmbedding(params, await response.json()); 33 | } 34 | } 35 | 36 | export type EmbeddingModel = Embeddings.EmbeddingModel; 37 | 38 | export type EmbeddingCreateParams = Embeddings.EmbeddingCreateParams; 39 | 40 | // eslint-disable-next-line no-redeclare 41 | export namespace Embeddings { 42 | // eslint-disable-next-line @typescript-eslint/no-shadow 43 | export type EmbeddingModel = DashscopeEmbeddings.EmbeddingModel; 44 | // eslint-disable-next-line @typescript-eslint/no-shadow 45 | export type EmbeddingCreateParams = DashscopeEmbeddings.EmbeddingCreateParams; 46 | } 47 | -------------------------------------------------------------------------------- /src/main/gptproxy/qwen/resources/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chat/index'; 2 | export * from './completions'; 3 | export { Images, type ImageModel, type ImageGenerateParams } from './images'; 4 | export { 5 | Embeddings, 6 | type EmbeddingModel, 7 | type EmbeddingCreateParams, 8 | } from './embeddings'; 9 | -------------------------------------------------------------------------------- /src/main/gptproxy/resource.ts: -------------------------------------------------------------------------------- 1 | import { APIClient } from 'openai/core'; 2 | 3 | export class APIResource { 4 | protected _client: Client; 5 | 6 | constructor(client: Client) { 7 | this._client = client; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/gptproxy/spark/resource.ts: -------------------------------------------------------------------------------- 1 | import type { SparkAI } from './index'; 2 | 3 | export class APIResource { 4 | protected _client: SparkAI; 5 | 6 | constructor(client: SparkAI) { 7 | this._client = client; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/gptproxy/spark/resources/chat/chat.ts: -------------------------------------------------------------------------------- 1 | import { APIResource } from '../../resource'; 2 | import { Completions } from './completions'; 3 | 4 | export class Chat extends APIResource { 5 | completions = new Completions(this._client); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/gptproxy/spark/resources/chat/index.ts: -------------------------------------------------------------------------------- 1 | export { Chat } from './chat'; 2 | export { 3 | type ChatModel, 4 | type ChatCompletionCreateParams, 5 | type ChatCompletionCreateParamsNonStreaming, 6 | type ChatCompletionCreateParamsStreaming, 7 | Completions, 8 | } from './completions'; 9 | -------------------------------------------------------------------------------- /src/main/gptproxy/spark/resources/images.ts: -------------------------------------------------------------------------------- 1 | import OpenAI, { APIError } from 'openai'; 2 | import { type RequestOptions } from 'openai/core'; 3 | 4 | import { APIResource } from '../resource'; 5 | 6 | // TODO: 没有权限,暂未测试 7 | export class Images extends APIResource { 8 | /** 9 | * See https://www.xfyun.cn/doc/spark/ImageGeneration.html 10 | */ 11 | async generate( 12 | params: OpenAI.ImageGenerateParams, 13 | options?: RequestOptions, 14 | ): Promise { 15 | const { prompt, user } = params; 16 | 17 | const body: ImagesAPI.ImageGenerateParams = { 18 | header: { 19 | app_id: this._client.appId, 20 | uid: user, 21 | }, 22 | parameter: { 23 | chat: { 24 | max_tokens: 4096, 25 | domain: 'general', 26 | temperature: 0.5, 27 | }, 28 | }, 29 | payload: { 30 | message: { 31 | text: [{ role: 'user', content: prompt }], 32 | }, 33 | }, 34 | }; 35 | 36 | const url = this._client.generateAuthorizationURL( 37 | 'https://spark-api.cn-huabei-1.xf-yun.com/v2.1/tti', 38 | 'POST', 39 | ); 40 | 41 | const response: Response = await this._client.post(url, { 42 | ...options, 43 | body, 44 | __binaryResponse: true, 45 | }); 46 | 47 | const resp: ImagesAPI.ImageGenerateResponse = await response.json(); 48 | 49 | if (resp.header.code > 0) { 50 | throw new APIError(undefined, resp.header, undefined, undefined); 51 | } 52 | 53 | return { 54 | created: Date.now() / 1000, 55 | data: [ 56 | { 57 | // base64 encoded image 58 | url: resp.payload.choices.text[0].content, 59 | }, 60 | ], 61 | }; 62 | } 63 | } 64 | 65 | namespace ImagesAPI { 66 | export type ImageGenerateMessageParam = { 67 | role: 'user'; 68 | content: string; 69 | }; 70 | 71 | export type ImageGenerateParams = { 72 | header: { 73 | /** 74 | * 应用ID 75 | */ 76 | app_id: string; 77 | /** 78 | * 用户唯一标识 79 | */ 80 | uid?: string; 81 | }; 82 | parameter: { 83 | chat: { 84 | max_tokens: number; 85 | domain: string; 86 | temperature: number; 87 | }; 88 | }; 89 | payload: { 90 | message: { 91 | text: ImageGenerateMessageParam[]; 92 | }; 93 | }; 94 | }; 95 | 96 | type ImageGenerateAssistantMessage = { 97 | index: 0; 98 | role: 'assistant'; 99 | content: string; 100 | }; 101 | 102 | export type ImageGenerateResponse = { 103 | header: { 104 | code: number; 105 | message: string; 106 | sid: string; 107 | status: number; 108 | }; 109 | payload: { 110 | choices: { 111 | status: number; 112 | seq: number; 113 | text: ImageGenerateAssistantMessage[]; 114 | }; 115 | }; 116 | }; 117 | } 118 | -------------------------------------------------------------------------------- /src/main/gptproxy/spark/resources/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chat/index'; 2 | 3 | export { Images } from './images'; 4 | -------------------------------------------------------------------------------- /src/main/gptproxy/util.ts: -------------------------------------------------------------------------------- 1 | export const castToError = (err: any): Error => { 2 | if (err instanceof Error) return err; 3 | return new Error(err); 4 | }; 5 | 6 | export function ensureArray(value: T | T[]): T[] { 7 | return Array.isArray(value) ? value : [value]; 8 | } 9 | 10 | export function random(min: number, max: number): number { 11 | return Math.floor(Math.random() * (max - min + 1)) + min; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/gptproxy/vyro/index.ts: -------------------------------------------------------------------------------- 1 | import type { Agent } from 'node:http'; 2 | 3 | import { 4 | APIClient, 5 | type DefaultQuery, 6 | type Fetch, 7 | type Headers, 8 | } from 'openai/core'; 9 | 10 | import * as API from './resources'; 11 | 12 | export interface VYroAIOptions { 13 | baseURL?: string; 14 | apiKey?: string; 15 | timeout?: number | undefined; 16 | httpAgent?: Agent; 17 | apiType?: (string & NonNullable) | 'api'; 18 | fetch?: Fetch | undefined; 19 | defaultHeaders?: Headers; 20 | defaultQuery?: DefaultQuery; 21 | } 22 | 23 | export class VYroAI extends APIClient { 24 | public apiType: (string & NonNullable) | 'api'; 25 | 26 | protected apiKey: string; 27 | 28 | private _options: VYroAIOptions; 29 | 30 | constructor(options: VYroAIOptions = {}) { 31 | const { 32 | apiKey = process.env.VYRO_API_KEY || '', 33 | apiType = process.env.VYRO_API_TYPE || 'api', 34 | baseURL = 'https://api.vyro.ai/v1', 35 | timeout = 30000, 36 | httpAgent = undefined, 37 | ...rest 38 | } = options; 39 | 40 | super({ 41 | baseURL, 42 | timeout, 43 | fetch, 44 | httpAgent, 45 | ...rest, 46 | }); 47 | 48 | this._options = options; 49 | 50 | this.apiKey = apiKey; 51 | this.apiType = apiType; 52 | } 53 | 54 | images = new API.Images(this); 55 | 56 | protected override authHeaders() { 57 | return { 58 | Authorization: `Bearer ${this.apiKey}`, 59 | }; 60 | } 61 | 62 | protected override defaultHeaders(): Headers { 63 | return { 64 | ...this.authHeaders(), 65 | ...this._options.defaultHeaders, 66 | }; 67 | } 68 | 69 | protected override defaultQuery(): DefaultQuery | undefined { 70 | return this._options.defaultQuery; 71 | } 72 | } 73 | 74 | // eslint-disable-next-line no-redeclare 75 | export namespace VYroAI { 76 | export type Images = API.Images; 77 | export type ImageGenerateParams = API.ImageGenerateParams; 78 | } 79 | 80 | export default VYroAI; 81 | -------------------------------------------------------------------------------- /src/main/gptproxy/vyro/resource.ts: -------------------------------------------------------------------------------- 1 | import type { VYroAI } from './index'; 2 | 3 | export class APIResource { 4 | protected _client: VYroAI; 5 | 6 | constructor(client: VYroAI) { 7 | this._client = client; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/gptproxy/vyro/resources/index.ts: -------------------------------------------------------------------------------- 1 | export { Images, type ImageGenerateParams } from './images'; 2 | -------------------------------------------------------------------------------- /src/main/preload.ts: -------------------------------------------------------------------------------- 1 | // Disable no-unused-vars, broken for spread args 2 | /* eslint no-unused-vars: off */ 3 | import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; 4 | 5 | export type Channels = 6 | | 'ipc-example' 7 | | 'get-env' 8 | | 'get-port' 9 | | 'check-health' 10 | | 'electron-store-get' 11 | | 'electron-store-set' 12 | | 'electron-store-remove' 13 | | 'refresh-config' 14 | | 'open-directory' 15 | | 'open-logger-folder' 16 | | 'select-file' 17 | | 'selected-file' 18 | | 'select-directory' 19 | | 'selected-directory' 20 | | 'open-url' 21 | | 'notification' 22 | | 'get-browser-version' 23 | | 'broadcast' 24 | | 'open-settings-window' 25 | | 'open-dataview-window' 26 | | 'update-settings-params' 27 | | 'get-version'; 28 | 29 | const electronHandler = { 30 | ipcRenderer: { 31 | sendMessage(channel: Channels, ...args: unknown[]) { 32 | ipcRenderer.send(channel, ...args); 33 | }, 34 | get(channel: Channels) { 35 | return ipcRenderer.sendSync(channel); 36 | }, 37 | on(channel: Channels, func: (...args: unknown[]) => void) { 38 | const subscription = (_event: IpcRendererEvent, ...args: unknown[]) => 39 | func(...args); 40 | ipcRenderer.on(channel, subscription); 41 | 42 | return () => { 43 | ipcRenderer.removeListener(channel, subscription); 44 | }; 45 | }, 46 | // 这个once方法是特别的,因为它确保了事件处理函数只会被调用一次,然后自动移除。 47 | once(channel: Channels, func: (...args: unknown[]) => void) { 48 | ipcRenderer.once(channel, (_event, ...args) => func(...args)); 49 | }, 50 | remove(channel: Channels) { 51 | ipcRenderer.removeAllListeners(channel); 52 | }, 53 | removeListener(channel: Channels, func: (...args: unknown[]) => void) { 54 | ipcRenderer.removeListener(channel, func); 55 | }, 56 | }, 57 | store: { 58 | get(key: string) { 59 | return ipcRenderer.sendSync('electron-store-get', key); 60 | }, 61 | set(key: string, value: string) { 62 | ipcRenderer.send('electron-store-set', key, value); 63 | }, 64 | remove(key: string) { 65 | ipcRenderer.send('electron-store-remove', key); 66 | }, 67 | }, 68 | getEnv: (key: string) => { 69 | const v = ipcRenderer.sendSync('get-env', key); 70 | return v; 71 | }, 72 | getPort: () => { 73 | const v = ipcRenderer.sendSync('get-port'); 74 | return v; 75 | }, 76 | getArgs: () => process.argv, 77 | }; 78 | 79 | export type ElectronHandler = typeof electronHandler; 80 | 81 | // 把功能暴露给渲染进程 82 | contextBridge.exposeInMainWorld('electron', electronHandler); 83 | -------------------------------------------------------------------------------- /src/main/system/chrome.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process'; 2 | import { promisify } from 'util'; 3 | 4 | const execAsync = promisify(exec); 5 | 6 | async function readVersionFromCmd(cmd: string, pattern: RegExp) { 7 | try { 8 | const { stdout } = await execAsync(cmd, { shell: 'powershell.exe' }); 9 | const match = stdout.match(pattern); 10 | return match ? match[0] : null; 11 | } catch (error) { 12 | // 如果命令执行失败,这里会捕获到异常,但我们选择忽略它,因为可能是因为浏览器不存在 13 | return null; 14 | } 15 | } 16 | 17 | export async function getBrowserVersionFromOS(): Promise { 18 | const commands = [ 19 | `(Get-Item -Path "$env:PROGRAMFILES\\Google\\Chrome\\Application\\chrome.exe").VersionInfo.FileVersion`, 20 | `(Get-Item -Path "'$env:PROGRAMFILES (x86)'\\Google\\Chrome\\Application\\chrome.exe").VersionInfo.FileVersion`, 21 | `(Get-Item -Path "$env:LOCALAPPDATA\\Google\\Chrome\\Application\\chrome.exe").VersionInfo.FileVersion`, 22 | `(Get-ItemProperty -Path Registry::"HKCU\\SOFTWARE\\Google\\Chrome\\BLBeacon").version`, 23 | `(Get-ItemProperty -Path Registry::"HKLM\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Google Chrome").version`, 24 | ]; 25 | const pattern = /\d+\.\d+\.\d+/; 26 | 27 | const firstHitTemplate = `$tmp = {expression}; if ($tmp) {echo $tmp; Exit;};`; 28 | const script = `$ErrorActionPreference='silentlycontinue'; ${commands 29 | .map((e) => firstHitTemplate.replace('{expression}', e)) 30 | .join(' ')}`; 31 | 32 | const version = await readVersionFromCmd(`${script}`, pattern); 33 | return version; // 如果所有命令都失败了,返回 null 34 | } 35 | -------------------------------------------------------------------------------- /src/main/system/cron.ts: -------------------------------------------------------------------------------- 1 | import nodeCron from 'node-cron'; 2 | 3 | export const setCron = (time: string, cb: () => void) => { 4 | // second minute hour day month week 5 | return nodeCron.schedule(time, cb); 6 | }; 7 | -------------------------------------------------------------------------------- /src/main/system/logger.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { getTempPath } from '../utils'; 4 | 5 | const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB 6 | const MAX_LOG_FILES = 10; 7 | 8 | // 在临时文件夹中创建一个名为 chatgpt-on-cs 的目录用于存放日志 9 | const logDir = getTempPath(); 10 | 11 | // 定义日志文件的路径 12 | let logFilePath = path.join(logDir, 'renderer.log'); 13 | 14 | function rotateLogs() { 15 | const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); 16 | const newLogFilePath = path.join(logDir, `renderer-${timestamp}.log`); 17 | fs.renameSync(logFilePath, newLogFilePath); 18 | 19 | const logFiles = fs.readdirSync(logDir).sort(); 20 | while (logFiles.length > MAX_LOG_FILES) { 21 | const oldestLogFile = logFiles.shift(); 22 | 23 | // 如果没有最旧的日志文件,则退出循环 24 | if (!oldestLogFile) { 25 | break; 26 | } 27 | 28 | fs.unlinkSync(path.join(logDir, oldestLogFile)); 29 | } 30 | 31 | logFilePath = path.join(logDir, 'renderer.log'); 32 | } 33 | 34 | function logToFile(message: string) { 35 | const timestamp = new Date().toISOString(); 36 | const logMessage = `[${timestamp}] ${message}\n`; 37 | 38 | const logSize = fs.existsSync(logFilePath) 39 | ? fs.statSync(logFilePath).size 40 | : 0; 41 | if (logSize + logMessage.length > MAX_LOG_SIZE) { 42 | rotateLogs(); 43 | } 44 | 45 | fs.appendFileSync(logFilePath, logMessage); 46 | } 47 | 48 | // const isDebug = 49 | // process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true'; 50 | 51 | const originalConsoleLog = console.log; 52 | const originalConsoleError = console.error; 53 | const originalConsoleWarn = console.warn; 54 | 55 | console.log = (...args) => { 56 | originalConsoleLog(...args); 57 | // if (!isDebug) return; 58 | 59 | logToFile( 60 | `INFO: ${args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : a)).join(' ')}`, 61 | ); 62 | }; 63 | 64 | console.error = (...args) => { 65 | originalConsoleError(...args); 66 | logToFile( 67 | `ERROR: ${args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : a)).join(' ')}`, 68 | ); 69 | }; 70 | 71 | console.warn = (...args) => { 72 | originalConsoleWarn(...args); 73 | logToFile( 74 | `WARN: ${args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : a)).join(' ')}`, 75 | ); 76 | }; 77 | 78 | process.on('uncaughtException', (error) => { 79 | console.error('An error occurred in the main process:', error); 80 | console.error(error.stack); 81 | }); 82 | -------------------------------------------------------------------------------- /src/main/util.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/prefer-default-export: off */ 2 | import { URL } from 'url'; 3 | import path from 'path'; 4 | 5 | export function resolveHtmlPath(htmlFileName: string) { 6 | if (process.env.NODE_ENV === 'development') { 7 | const port = process.env.PORT || 1212; 8 | const url = new URL(`http://localhost:${port}`); 9 | url.pathname = htmlFileName; 10 | return url.href; 11 | } 12 | return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/utils/index.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import os from 'os'; 3 | import path from 'path'; 4 | import socketIo from 'socket.io'; 5 | 6 | export const getTempPath = () => { 7 | const tempDir = os.tmpdir(); 8 | const logDir = path.join(tempDir, 'chatgpt-on-cs'); 9 | if (!fs.existsSync(logDir)) { 10 | fs.mkdirSync(logDir); 11 | } 12 | return logDir; 13 | }; 14 | 15 | export async function emitAndWait( 16 | io: socketIo.Server, 17 | event: string, 18 | data?: any, 19 | timeout: number = 5000, 20 | ): Promise { 21 | let response; 22 | if (data === undefined) { 23 | response = await io.timeout(timeout).emitWithAck(event); 24 | } else { 25 | response = await io.timeout(timeout).emitWithAck(event, data); 26 | } 27 | 28 | // 判断是否是 Array 29 | if (Array.isArray(response) && response.length === 1) { 30 | if (typeof response[0] === 'undefined') { 31 | return {} as T; 32 | } 33 | 34 | // 如果 response[0] 也是数组,那么直接返回 35 | if (Array.isArray(response[0])) { 36 | return response[0] as T; 37 | } 38 | 39 | if (typeof response[0] === 'string') { 40 | const obj = JSON.parse(response[0]); 41 | if (obj.error) { 42 | throw new Error(obj.error); 43 | } 44 | 45 | return obj; 46 | } 47 | 48 | const obj = response[0]; 49 | // 检查返回的对象是否包含 error 50 | if (obj.error) { 51 | throw new Error(obj.error); 52 | } 53 | 54 | return obj; 55 | } 56 | 57 | // 判断是否是字符串 58 | if (typeof response === 'string') { 59 | const obj = JSON.parse(response); 60 | if (obj.error) { 61 | throw new Error(obj.error); 62 | } 63 | return obj; 64 | } 65 | 66 | return response; 67 | } 68 | -------------------------------------------------------------------------------- /src/main/windows/dataview-main/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { app, BrowserWindow, shell } from 'electron'; 3 | import { resolveHtmlPath } from '../../util'; 4 | 5 | let dataviewWindow: BrowserWindow | null = null; 6 | 7 | const isDebug = 8 | process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true'; 9 | 10 | export const createWindow = async (args: string[]) => { 11 | if (isDebug) { 12 | // await installExtensions(); 13 | } 14 | 15 | if (dataviewWindow) { 16 | dataviewWindow.focus(); 17 | dataviewWindow.webContents.send('update-dataview-params', args); 18 | return; 19 | } 20 | 21 | const RESOURCES_PATH = app.isPackaged 22 | ? path.join(process.resourcesPath, 'assets') 23 | : path.join(__dirname, '../../assets'); 24 | 25 | const getAssetPath = (...paths: string[]): string => { 26 | return path.join(RESOURCES_PATH, ...paths); 27 | }; 28 | 29 | dataviewWindow = new BrowserWindow({ 30 | show: false, 31 | width: 1024, 32 | height: 728, 33 | icon: getAssetPath('icon.png'), 34 | webPreferences: { 35 | preload: app.isPackaged 36 | ? path.join(__dirname, 'preload.js') 37 | : path.join(__dirname, '../../../../.erb/dll/preload.js'), 38 | additionalArguments: args, 39 | }, 40 | }); 41 | 42 | dataviewWindow.loadURL(resolveHtmlPath('dataview.html')); 43 | 44 | dataviewWindow.on('ready-to-show', () => { 45 | if (!dataviewWindow) { 46 | throw new Error('"dataviewWindow" is not defined'); 47 | } 48 | 49 | dataviewWindow.show(); 50 | }); 51 | 52 | dataviewWindow.on('closed', () => { 53 | dataviewWindow = null; 54 | }); 55 | 56 | // Open urls in the user's browser 57 | dataviewWindow.webContents.setWindowOpenHandler((edata) => { 58 | shell.openExternal(edata.url); 59 | return { action: 'deny' }; 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /src/main/windows/settings-main/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off, no-console: off, promise/always-return: off, import/prefer-default-export: off */ 2 | 3 | import path from 'path'; 4 | import { app, BrowserWindow, shell } from 'electron'; 5 | import { resolveHtmlPath } from '../../util'; 6 | 7 | let settingsWindow: BrowserWindow | null = null; 8 | 9 | const isDebug = 10 | process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true'; 11 | 12 | // const installExtensions = async () => { 13 | // const installer = require('electron-devtools-installer'); 14 | // const forceDownload = !!process.env.UPGRADE_EXTENSIONS; 15 | // const extensions = ['REACT_DEVELOPER_TOOLS']; 16 | 17 | // return installer 18 | // .default( 19 | // extensions.map((name) => installer[name]), 20 | // forceDownload, 21 | // ) 22 | // .catch(console.log); 23 | // }; 24 | 25 | export const createWindow = async (args: string[]) => { 26 | if (isDebug) { 27 | // await installExtensions(); 28 | } 29 | 30 | if (settingsWindow) { 31 | settingsWindow.focus(); 32 | settingsWindow.webContents.send('update-settings-params', args); 33 | return; 34 | } 35 | 36 | const RESOURCES_PATH = app.isPackaged 37 | ? path.join(process.resourcesPath, 'assets') 38 | : path.join(__dirname, '../../assets'); 39 | 40 | const getAssetPath = (...paths: string[]): string => { 41 | return path.join(RESOURCES_PATH, ...paths); 42 | }; 43 | 44 | settingsWindow = new BrowserWindow({ 45 | show: false, 46 | width: 1024, 47 | height: 728, 48 | icon: getAssetPath('icon.png'), 49 | webPreferences: { 50 | preload: app.isPackaged 51 | ? path.join(__dirname, 'preload.js') 52 | : path.join(__dirname, '../../../../.erb/dll/preload.js'), 53 | additionalArguments: args, 54 | }, 55 | }); 56 | 57 | settingsWindow.loadURL(resolveHtmlPath('settings.html')); 58 | 59 | settingsWindow.on('ready-to-show', () => { 60 | if (!settingsWindow) { 61 | throw new Error('"settingsWindow" is not defined'); 62 | } 63 | 64 | settingsWindow.show(); 65 | }); 66 | 67 | settingsWindow.on('closed', () => { 68 | settingsWindow = null; 69 | }); 70 | 71 | // Open urls in the user's browser 72 | settingsWindow.webContents.setWindowOpenHandler((edata) => { 73 | shell.openExternal(edata.url); 74 | return { action: 'deny' }; 75 | }); 76 | }; 77 | -------------------------------------------------------------------------------- /src/renderer/common/App.css: -------------------------------------------------------------------------------- 1 | /* 2 | * @NOTE: Prepend a `~` to css file paths that are in your node_modules 3 | * See https://github.com/webpack-contrib/sass-loader#imports 4 | */ 5 | html, 6 | body { 7 | height: 100%; 8 | overflow: auto; /* 或者使用 'scroll' 来强制显示滚动条 */ 9 | } 10 | 11 | body { 12 | height: 100vh; 13 | } 14 | 15 | @font-face { 16 | font-family: 'zhFont'; 17 | src: url('../../../assets/fonts/庞门正道标题体.ttf') format('truetype'); 18 | font-weight: normal; 19 | font-style: normal; 20 | } 21 | 22 | .font-zh { 23 | font-family: 'zhFont'; 24 | } 25 | 26 | .table-tiny th, 27 | .table-tiny td { 28 | padding-left: 4px; 29 | padding-right: 4px; 30 | } 31 | 32 | .table-tiny th:first-child, 33 | .table-tiny td:first-child { 34 | max-width: 60px; 35 | } 36 | 37 | * { 38 | scrollbar-width: thin; 39 | scrollbar-color: var(--chakra-colors-gray-300) transparent; 40 | } 41 | 42 | *::-webkit-scrollbar { 43 | width: 5px; 44 | } 45 | 46 | *::-webkit-scrollbar-track { 47 | background: transparent; 48 | } 49 | 50 | *::-webkit-scrollbar-thumb { 51 | background-color: var(--chakra-colors-gray-300); 52 | border-radius: 10px; 53 | border: none; 54 | } 55 | -------------------------------------------------------------------------------- /src/renderer/common/components/Markdown/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactMarkdown, { Components } from 'react-markdown'; 3 | import remarkGfm from 'remark-gfm'; 4 | import styles from './index.module.scss'; 5 | 6 | interface MarkdownProps { 7 | content: string; 8 | } 9 | 10 | const Markdown: React.FC = ({ content }) => { 11 | // 使用正确的类型来确保与预期的ReactMarkdown组件兼容 12 | const components: Components = { 13 | // @ts-ignore 14 | a: ({ node, ...props }) => ( // eslint-disable-line 15 | 16 | {props.children} 17 | 18 | ), 19 | }; 20 | 21 | return ( 22 | 27 | {content} 28 | 29 | ); 30 | }; 31 | 32 | export default React.memo(Markdown); 33 | -------------------------------------------------------------------------------- /src/renderer/common/components/MyModal/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Modal, 4 | ModalOverlay, 5 | ModalContent, 6 | ModalHeader, 7 | ModalCloseButton, 8 | ModalContentProps, 9 | Box, 10 | Image, 11 | } from '@chakra-ui/react'; 12 | 13 | export interface MyModalProps extends ModalContentProps { 14 | iconSrc?: string; 15 | title?: any; 16 | isCentered?: boolean; 17 | isOpen: boolean; 18 | onClose?: () => void; 19 | } 20 | 21 | const MyModal = ({ 22 | isOpen, 23 | onClose, 24 | iconSrc, 25 | title, 26 | children, 27 | isCentered, 28 | w = 'auto', 29 | maxW = ['90vw', '600px'], 30 | style, 31 | ...props 32 | }: MyModalProps) => { 33 | return ( 34 | onClose && onClose()} 37 | autoFocus={false} 38 | isCentered={isCentered} 39 | > 40 | 41 | 49 | {!title && onClose && } 50 | {!!title && ( 51 | 59 | {iconSrc && ( 60 | <> 61 | 68 | 69 | )} 70 | {title} 71 | 72 | {onClose && ( 73 | 74 | )} 75 | 76 | )} 77 | 78 | 86 | {children} 87 | 88 | 89 | 90 | ); 91 | }; 92 | 93 | export default MyModal; 94 | -------------------------------------------------------------------------------- /src/renderer/common/components/MyTextarea/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState, useEffect } from 'react'; 2 | import { Box, Textarea, TextareaProps, Text } from '@chakra-ui/react'; 3 | 4 | type Props = TextareaProps & { 5 | title?: string; 6 | }; 7 | 8 | const Editor = React.memo(function Editor({ 9 | textareaRef, 10 | maxLength, 11 | onChange, 12 | value, 13 | ...props 14 | }: Props & { 15 | textareaRef: React.RefObject; 16 | onOpenModal?: () => void; 17 | }) { 18 | // @ts-ignore 19 | const [currentLength, setCurrentLength] = useState(value?.length || 0); 20 | 21 | // 处理文本变化的函数 22 | const handleChange = (e: React.ChangeEvent) => { 23 | setCurrentLength(e.target.value.length); // 更新当前文本长度 24 | if (onChange) { 25 | onChange(e); // 如果有外部传入的onChange事件处理器,也调用它 26 | } 27 | }; 28 | 29 | // 处理初始值的情况 30 | useEffect(() => { 31 | if (value) { 32 | // @ts-ignore 33 | setCurrentLength(value.length); 34 | } 35 | }, [value]); 36 | 37 | return ( 38 | 39 |