diff --git a/.gitignore b/.gitignore index 7fdc56522ff9d8dae6814047c94e207696a685c5..04c4535fb4dbfc4cdd321314ec9d27a1103c777c 100755 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ coverage .gitee/ .npmrc +release \ No newline at end of file diff --git a/electron/main/index.ts b/electron/main/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..2babc5b1d0adc7521942c9bdf0b10ca3f6953c27 --- /dev/null +++ b/electron/main/index.ts @@ -0,0 +1,20 @@ +import { app, ipcMain } from 'electron' +import { createDefaultWindow } from './window' + +app.whenReady().then(() => { + createDefaultWindow() + // const defaultWindow = createDefaultWindow() + + // 监听渲染进程崩溃或被杀死,重新运行程序 + // defaultWindow.webContents.on('render-process-gone', () => { + // app.relaunch() + // app.exit(0) + // }) +}) + +app.on('window-all-closed', () => { + ipcMain.removeAllListeners() + if (process.platform !== 'darwin') { + app.quit() + } +}) diff --git a/electron/main/window/create.ts b/electron/main/window/create.ts new file mode 100644 index 0000000000000000000000000000000000000000..1d47863dcd9f988f1e6b1a2fe9474c69b8b9f9cf --- /dev/null +++ b/electron/main/window/create.ts @@ -0,0 +1,32 @@ +import path from 'node:path' +import { BrowserWindow, app } from 'electron' +import { options as allWindow } from './options' + +export function createWindow(options: Electron.BrowserWindowConstructorOptions, hash: string): BrowserWindow { + const win = new BrowserWindow({ + ...options, + webPreferences: { + devTools: true, + preload: path.join(__dirname, '../preload/index.js'), + }, + }) + + if (app.isPackaged) { + win.loadFile(path.join(__dirname, `../index.html`)) + } + else { + win.webContents.openDevTools() + win.loadURL(`http://localhost:${process.env.PORT}`) + } + + return win +} + +let defaultWindow: BrowserWindow | null = null +export function createDefaultWindow(): BrowserWindow { + if (defaultWindow) return defaultWindow + + defaultWindow = createWindow(allWindow.defaultWin.window, allWindow.defaultWin.hash) + + return defaultWindow +} diff --git a/electron/main/window/index.ts b/electron/main/window/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..fd953c62341ff8612431a4b0218dfb40c6dff5b9 --- /dev/null +++ b/electron/main/window/index.ts @@ -0,0 +1 @@ +export { createWindow, createDefaultWindow } from './create' diff --git a/electron/main/window/options.ts b/electron/main/window/options.ts new file mode 100644 index 0000000000000000000000000000000000000000..06e9bfb7654c28c2f1b2f7d86c80c36d9af5218f --- /dev/null +++ b/electron/main/window/options.ts @@ -0,0 +1,23 @@ +export interface allWindowType { + [propName: string]: { + window: Electron.BrowserWindowConstructorOptions + hash: string + } +} + +export const options: allWindowType = { + defaultWin: { + window: { + width: 800, + height: 600, + resizable: true, + show: true, + alwaysOnTop: false, + useContentSize: true, + frame: true, + backgroundColor: '#ffffff', + icon: 'dist/favicon.ico', + }, + hash: 'defaultWin', + }, +} diff --git a/electron/preload/index.ts b/electron/preload/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/installer.nsh b/installer.nsh new file mode 100644 index 0000000000000000000000000000000000000000..750916ed00da1353814776bab4c94ee72aff3b42 --- /dev/null +++ b/installer.nsh @@ -0,0 +1,21 @@ +# Custom NSIS script + +# 预初始化时,根据机器位数决定默认安装路径 + +!include "x64.nsh" +!include "LogicLib.nsh" + +Var INSTALL_PATH + +!macro preInit + ${If} ${RunningX64} + SetRegView 64 + StrCpy $INSTALL_PATH "C:\Program Files\eulercopilot" + ${Else} + SetRegView 32 + StrCpy $INSTALL_PATH "C:\Program Files (x86)\eulercopilot" + ${EndIf} + + WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "$INSTALL_PATH" + WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$INSTALL_PATH" +!macroend diff --git a/package.json b/package.json index 6fdca229b841011f2fd950283fb912dcf398376c..32e5cdedf6d257373c8d386df514dd52b4fbbbe0 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,12 @@ { "name": "euler-copilot-web", "version": "1.0.0", + "description": "Openeuler intelligent question and answer assistant", + "author": { + "name": "Openeuler" + }, "private": true, - "type": "module", + "main": "./dist/main/index.js", "scripts": { "dev": "vite", "dev:micro": "vite --mode micro", @@ -11,15 +15,69 @@ "preview": "vite preview", "lint": "eslint . --fix", "format": "prettier --write src/", - "type-check": "tsc --noEmit" + "type-check": "tsc --noEmit", + "dev:desktop": "concurrently -n=R,P,M -c=green,yellow,blue \"pnpm run dev:render\" \"pnpm run dev:preload\" \"pnpm run dev:main\"", + "dev:render": "vite", + "dev:preload": "node -r ts-node/register scripts/build-preload --env=development --watch", + "dev:main": "node -r ts-node/register scripts/build-main --env=development --watch", + "build:render": "vite build --mode electron-production", + "build:preload": "node -r ts-node/register scripts/build-preload --env=production", + "build:main": "node -r ts-node/register scripts/build-main --env=production", + "build:desktop": "rimraf dist && pnpm run build:render && pnpm run build:preload && pnpm run build:main", + "package:win32": "pnpm run build:desktop && electron-builder --win --ia32", + "package:win64": "pnpm run build:desktop && electron-builder --win --x64", + "package:mac": "pnpm run build:desktop && electron-builder --mac", + "package:linux": "pnpm run build:desktop && electron-builder --linux" + }, + "build": { + "appId": "com.openeuler.eulercopilot", + "productName": "eulercopilot", + "asar": true, + "copyright": "", + "directories": { + "output": "release/eulercopilot-${version}" + }, + "win": { + "icon": "dist/app_favicon.ico", + "target": [ + { + "target": "nsis" + } + ], + "artifactName": "${productName}_${version}.${ext}" + }, + "mac": { + "artifactName": "${productName}_${version}.${ext}", + "target": [ + "dmg" + ] + }, + "linux": { + "artifactName": "${productName}_${version}.${ext}", + "target": [ + "deb", + "rpm", + "AppImage" + ] + }, + "nsis": { + "oneClick": false, + "allowElevation": true, + "allowToChangeInstallationDirectory": true, + "installerIcon": "dist/app_favicon.ico", + "uninstallerIcon": "dist/app_favicon.ico", + "installerHeaderIcon": "dist/app_favicon.ico", + "createDesktopShortcut": true, + "createStartMenuShortcut": true, + "shortcutName": "eulercopilot", + "deleteAppDataOnUninstall": true, + "include": "./installer.nsh" + } }, "engines": { "node": ">= 18.18.2" }, "dependencies": { - "@babel/core": "^7.26.0", - "@babel/plugin-transform-runtime": "^7.25.9", - "@babel/preset-env": "^7.26.0", "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-yaml": "^6.1.2", "@codemirror/theme-one-dark": "^6.1.2", @@ -27,7 +85,6 @@ "@computing/opendesign2": "file:lib\\opendesign2-2.0.23.tgz", "@dagrejs/dagre": "1.1.2", "axios": "1.7.9", - "babel-loader": "^9.2.1", "codemirror": "^6.0.1", "dayjs": "1.11.9", "echarts": "^5.5.1", @@ -36,9 +93,6 @@ "js-yaml": "^4.1.0", "marked": "4.3", "pinia": "2.1.6", - "sass": "1.62.0", - "typescript": "4.9.5", - "vite": "5.4.11", "vue": "3.3.4", "vue-codemirror": "^6.1.1", "vue-echarts": "^7.0.3", @@ -50,7 +104,17 @@ "xterm-addon-fit": "0.6.0" }, "devDependencies": { + "@babel/core": "^7.26.0", + "@babel/plugin-transform-runtime": "^7.25.9", + "@babel/preset-env": "^7.26.0", "@eslint/js": "9.16.0", + "@rollup/plugin-alias": "^5.1.1", + "@rollup/plugin-commonjs": "^28.0.3", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-replace": "^6.0.2", + "@rollup/plugin-typescript": "^12.1.2", + "@types/minimist": "^1.2.5", "@types/node": "18.19.67", "@typescript-eslint/eslint-plugin": "8.17.0", "@typescript-eslint/parser": "8.17.0", @@ -61,13 +125,29 @@ "@vue-flow/minimap": "^1.5.0", "@vue-flow/node-resizer": "^1.4.0", "@vue-flow/node-toolbar": "^1.1.0", + "babel-loader": "^9.2.1", + "chalk": "^5.4.1", + "concurrently": "^9.1.2", + "dotenv": "^16.4.7", + "electron": "35.1.0", + "electron-builder": "^26.0.12", "eslint": "9.16.0", "eslint-plugin-vue": "9.32.0", "globals": "15.13.0", + "minimist": "^1.2.8", "mitt": "^3.0.1", + "ora": "^8.2.0", "prettier": "3.4.2", + "rimraf": "^6.0.1", + "rollup": "^4.37.0", + "rollup-plugin-copy": "^3.5.0", + "sass": "1.62.0", + "ts-node": "^10.9.2", + "tslib": "^2.8.1", + "typescript": "4.9.5", "typescript-eslint": "8.17.0", "uuid": "^11.0.5", + "vite": "5.4.11", "vite-plugin-qiankun": "1.0.15" } } diff --git a/public/app_favicon.ico b/public/app_favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..71f3ce0b8e3f2596c44e5fa3f6364b58ee335f58 Binary files /dev/null and b/public/app_favicon.ico differ diff --git a/scripts/build-main.ts b/scripts/build-main.ts new file mode 100644 index 0000000000000000000000000000000000000000..651d92e0a35ecea2290362a6185ef66c428f77b8 --- /dev/null +++ b/scripts/build-main.ts @@ -0,0 +1,59 @@ +import path from 'node:path' +import chalk from 'chalk' +import ora from 'ora' +import minimist from 'minimist' +import electron from 'electron' +import { rollup, watch } from 'rollup' +import { getEnv, waitOn } from './utils' +import options from './rollup.config' +import { spawn } from 'node:child_process' +import { main } from '../package.json' + +import type { OutputOptions } from 'rollup' +import type { ChildProcess } from 'child_process' + +const TAG = '[build-main.ts]' + +const env = getEnv() +const argv = minimist(process.argv.slice(2)) +const opt = options({ proc: 'main', env: argv.env }) +const spinner = ora(`${TAG} Electron main build...`) + +;(async () => { + if (argv.watch) { + // Wait on vite server launched + await waitOn({ port: env.PORT as string }) + + const watcher = watch(opt) + let child: ChildProcess + watcher.on('change', (filename) => { + const log = chalk.green(`change -- ${filename}`) + console.log(TAG, log) + }) + watcher.on('event', (ev) => { + if (ev.code === 'END') { + if (child) child.kill() + child = spawn(electron as unknown as string, [path.join(__dirname, `../${main}`)], { + stdio: 'inherit', + }) + } + else if (ev.code === 'ERROR') { + console.log(ev.error) + } + }) + } + else { + spinner.start() + try { + const build = await rollup(opt) + await build.write(opt.output as OutputOptions) + spinner.succeed() + process.exit() + } + catch (error) { + console.log(`\n${TAG} ${chalk.red('构建报错')}\n`, error, '\n') + spinner.fail() + process.exit(1) + } + } +})() diff --git a/scripts/build-preload.ts b/scripts/build-preload.ts new file mode 100644 index 0000000000000000000000000000000000000000..6b3765628ef9acc5a93771679053dea0ccdb3ed4 --- /dev/null +++ b/scripts/build-preload.ts @@ -0,0 +1,39 @@ +import type { OutputOptions } from 'rollup' +import { watch, rollup } from 'rollup' +import minimist from 'minimist' +import chalk from 'chalk' +import ora from 'ora' +import options from './rollup.config' + +const argv = minimist(process.argv.slice(2)) +const opt = options({ proc: 'preload', env: argv.env }) +const TAG = '[build-preload.ts]' +const spinner = ora(`${TAG} Electron preload build...`) + +;(async () => { + if (argv.watch) { + const watcher = watch(opt) + watcher.on('change', (filename) => { + const log = chalk.yellow(`change -- ${filename}`) + console.log(TAG, log) + + /** + * @todo Hot reload render process !!! + */ + }) + } + else { + spinner.start() + try { + const build = await rollup(opt) + await build.write(opt.output as OutputOptions) + spinner.succeed() + process.exit() + } + catch (error) { + console.log(`\n${TAG} ${chalk.red('构建报错')}\n`, error, '\n') + spinner.fail() + process.exit(1) + } + } +})() diff --git a/scripts/rollup.config.ts b/scripts/rollup.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..813e953bbc65d6fe1f56d7ea5a6568dbd0275eec --- /dev/null +++ b/scripts/rollup.config.ts @@ -0,0 +1,68 @@ +import path from 'path' +import type { RollupOptions } from 'rollup' +import copy from 'rollup-plugin-copy' +import nodeResolve from '@rollup/plugin-node-resolve' +import typescript from '@rollup/plugin-typescript' +import commonjs from '@rollup/plugin-commonjs' +import replace from '@rollup/plugin-replace' +import alias from '@rollup/plugin-alias' +import json from '@rollup/plugin-json' +import { builtins, getEnv } from './utils' + +export interface ConfigOptions { + env?: typeof process.env.NODE_ENV + proc: 'main' | 'render' | 'preload' +} + +const compilationInclude = ['electron/**/*.ts'] + +export default function (opts: ConfigOptions) { + const sourcemap = opts.proc === 'render' + const options: RollupOptions = { + input: path.join(__dirname, `../electron/${opts.proc}/index.ts`), + output: { + dir: path.join(__dirname, `../dist/${opts.proc}`), + format: 'cjs', + sourcemap, + }, + plugins: [ + nodeResolve({ + extensions: ['.ts', '.js', 'json'], + }), + commonjs({ + include: compilationInclude + }), + json(), + typescript({ + sourceMap: sourcemap, + noEmitOnError: true, + include: compilationInclude + }), + alias({ + entries: { + '@': path.join(__dirname, '../src'), + }, + }), + copy({ + // 复制 favicon.ico 到指定目录 + targets: [{ src: 'app_favicon.ico', dest: 'dist' }], + }), + replace({ + ...Object.entries({ ...getEnv(), NODE_ENV: opts.env }).reduce( + (acc, [k, v]) => Object.assign(acc, { [`process.env.${k}`]: JSON.stringify(v) }), + {}, + ), + preventAssignment: true, + }), + ], + external: [...builtins(), 'electron'], + onwarn: (warning) => { + // https://github.com/rollup/rollup/issues/1089#issuecomment-365395213 + if (warning.code !== 'CIRCULAR_DEPENDENCY') { + console.error(`(!) ${warning.message}`) + } + }, + } + + return options +} diff --git a/scripts/utils.ts b/scripts/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..e07752cba4e76e3c7ec5f53d564e2fe250a28e0e --- /dev/null +++ b/scripts/utils.ts @@ -0,0 +1,43 @@ +import fs from 'node:fs' +import path from 'node:path' +import chalk from 'chalk' +import { get } from 'http' +import { builtinModules } from 'module' +import { parse as parseEnv } from 'dotenv' + +export function getEnv(): Record { + try { + if (getEnv.env) { + return getEnv.env + } + const env = parseEnv(fs.readFileSync(path.join(process.cwd(), '.env'))) + return (getEnv.env = env) + } + catch { + return {} + } +} +getEnv.env = undefined as Record | undefined + +/** node.js builtins module */ +export const builtins = () => + builtinModules.filter(x => !/^_|^(internal|v8|node-inspect)\/|\//.test(x)) + +// 轮询监听vite启动 +export function waitOn(arg0: { port: string | number, interval?: number }) { + return new Promise((resolve) => { + const { port, interval = 149 } = arg0 + const url = `http://localhost:${port}` + let counter = 0 + const timer: NodeJS.Timer = setInterval(() => { + get(url, (res) => { + clearInterval(timer as unknown as number) + console.log('[waitOn]', chalk.green(`"${url}" are already responsive.`)) + resolve(res.statusCode) + }).on('error', (error) => { + console.log(error) + console.log('[waitOn]', `counter: ${counter++}`) + }) + }, interval) + }) +} diff --git a/src/apis/server.ts b/src/apis/server.ts index e185043e8c7cbb93390bb2b920c89cf6661ebd1d..64d34b33b5dd22beb56e7f0d5a773c49e49007cd 100644 --- a/src/apis/server.ts +++ b/src/apis/server.ts @@ -34,8 +34,11 @@ export interface IAnyObj { export type Fn = (data: FcResponse) => unknown; + +const baseURL = import.meta.env.MODE === 'electron-production' ? import.meta.env.VITE_BASE_PROXY_URL : './'; // 创建 axios 实例 export const server = axios.create({ + baseURL, // API 请求的默认前缀 timeout: 60 * 1000, // 请求超时时间 }); diff --git a/tsconfig.json b/tsconfig.json index c88a583d107939ae02bca96e43d60a771fd7eef7..5ad27fa3964dcfed9b7de013c7bdfe4680891420 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,7 +33,18 @@ }, "lib": ["esnext", "dom", "dom.iterable", "scripthost"] }, - "types": [ "vite/client" ], - "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "vite.config.ts"], + "ts-node": { + "compilerOptions": { + "module": "CommonJS" + } + }, + "types": ["vite/client"], + "include": [ + "env.d.ts", + "src/**/*", + "src/**/*.vue", + "vite.config.ts", + "electron/**/*" + ], "exclude": ["node_modules", "dist"] } diff --git a/vite.config.ts b/vite.config.ts index 1229411d4dcf007750fac8bb709e8e68f8245c62..179aadd2b4a2d4141fa5e5bb1e98bbaa0f3da556 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -22,7 +22,7 @@ export default ({ mode }): UserConfigExport => { VITE_BASE_URL } = env - const baseUrl = mode === 'production' ? VITE_BASE_URL : '/' + const baseUrl = mode === 'micro' ? VITE_BASE_URL : './' return defineConfig({ base: baseUrl, resolve: { @@ -67,45 +67,12 @@ export default ({ mode }): UserConfigExport => { server: { host: "localhost", hmr: true, - open: true, port: 3000, origin: 'http://localhost:3000', headers: { "Access-Control-Allow-Origin": "*", }, proxy: { - "/plugin": { - target: env.VITE_BASE_PROXY_URL, - changeOrigin: true, - ws: false, - secure: false, - }, - "/rag": { - target: env.VITE_BASE_PROXY_URL, - secure: false, - changeOrigin: true, - ws: false, - rewrite: (path: string) => path.replace(/^\/rag/, ""), - }, - "/stream": { - target: env.VITE_BASE_PROXY_URL, - secure: false, - changeOrigin: true, - ws: false, - rewrite: (path: string) => path.replace(/^\/stream/, ""), - }, - "/qabot": { - target: env.VITE_QABOT_URL, - changeOrigin: true, - ws: false, - rewrite: (path: string) => path.replace(/^\/qabot/, ""), - }, - "/test": { - target: env.VITE_BASE_PROXY_URL, - changeOrigin: true, - ws: false, - rewrite: (path: string) => path.replace(/^\/test/, ""), - }, "/api": { target: env.VITE_BASE_PROXY_URL, changeOrigin: true,