Compare commits
3 Commits
main
...
feat/node-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3499cd449b | ||
|
|
a97c3d9695 | ||
|
|
9145e998c4 |
13
packages/artifacts/package-lock.json
generated
Normal file
13
packages/artifacts/package-lock.json
generated
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@cherry-studio/artifacts",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@cherry-studio/artifacts",
|
||||
"version": "0.1.0",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
}
|
||||
1358
packages/database/package-lock.json
generated
Normal file
1358
packages/database/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
314
resources/scripts/install-node.js
Normal file
314
resources/scripts/install-node.js
Normal file
@@ -0,0 +1,314 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const os = require('os')
|
||||
const https = require('https')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
// 配置
|
||||
const NODE_VERSION = process.env.NODE_VERSION || '18.18.0' // 默认版本
|
||||
const NODE_RELEASE_BASE_URL = 'https://nodejs.org/dist'
|
||||
|
||||
// 平台映射
|
||||
const NODE_PACKAGES = {
|
||||
'darwin-arm64': `node-v${NODE_VERSION}-darwin-arm64.tar.gz`,
|
||||
'darwin-x64': `node-v${NODE_VERSION}-darwin-x64.tar.gz`,
|
||||
'win32-x64': `node-v${NODE_VERSION}-win32-x64.zip`,
|
||||
'win32-ia32': `node-v${NODE_VERSION}-win32-x86.zip`,
|
||||
'linux-x64': `node-v${NODE_VERSION}-linux-x64.tar.gz`,
|
||||
'linux-arm64': `node-v${NODE_VERSION}-linux-arm64.tar.gz`,
|
||||
}
|
||||
|
||||
// 辅助函数 - 递归复制目录
|
||||
function copyFolderRecursiveSync(source, target) {
|
||||
// 检查目标目录是否存在,不存在则创建
|
||||
if (!fs.existsSync(target)) {
|
||||
fs.mkdirSync(target, { recursive: true });
|
||||
}
|
||||
|
||||
// 读取源目录中的所有文件和文件夹
|
||||
const files = fs.readdirSync(source);
|
||||
|
||||
// 循环处理每个文件/文件夹
|
||||
for (const file of files) {
|
||||
const sourcePath = path.join(source, file);
|
||||
const targetPath = path.join(target, file);
|
||||
|
||||
// 检查是文件还是文件夹
|
||||
if (fs.statSync(sourcePath).isDirectory()) {
|
||||
// 如果是文件夹,递归复制
|
||||
copyFolderRecursiveSync(sourcePath, targetPath);
|
||||
} else {
|
||||
// 如果是文件,直接复制
|
||||
fs.copyFileSync(sourcePath, targetPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 二进制文件存放目录
|
||||
const binariesDir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
|
||||
// 创建二进制文件存放目录
|
||||
async function createBinariesDir() {
|
||||
if (!fs.existsSync(binariesDir)) {
|
||||
console.log(`Creating binaries directory at ${binariesDir}`)
|
||||
fs.mkdirSync(binariesDir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前平台对应的包名
|
||||
function getPackageForPlatform() {
|
||||
const platform = os.platform()
|
||||
const arch = os.arch()
|
||||
const key = `${platform}-${arch}`
|
||||
|
||||
console.log(`Current platform: ${platform}, architecture: ${arch}`)
|
||||
|
||||
if (!NODE_PACKAGES[key]) {
|
||||
throw new Error(`Unsupported platform/architecture: ${key}`)
|
||||
}
|
||||
|
||||
return NODE_PACKAGES[key]
|
||||
}
|
||||
|
||||
// 下载 Node.js
|
||||
async function downloadNodeJs() {
|
||||
const packageName = getPackageForPlatform()
|
||||
const downloadUrl = `${NODE_RELEASE_BASE_URL}/v${NODE_VERSION}/${packageName}`
|
||||
const tempFilePath = path.join(os.tmpdir(), packageName)
|
||||
|
||||
console.log(`Downloading Node.js v${NODE_VERSION} from ${downloadUrl}`)
|
||||
console.log(`Temp file path: ${tempFilePath}`)
|
||||
|
||||
// 如果临时文件已存在,先删除
|
||||
if (fs.existsSync(tempFilePath)) {
|
||||
fs.unlinkSync(tempFilePath)
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = fs.createWriteStream(tempFilePath)
|
||||
|
||||
https.get(downloadUrl, (response) => {
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`Failed to download: ${response.statusCode} ${response.statusMessage}`))
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`Download started, status code: ${response.statusCode}`)
|
||||
|
||||
response.pipe(file)
|
||||
|
||||
file.on('finish', () => {
|
||||
file.close()
|
||||
console.log('Download completed')
|
||||
resolve(tempFilePath)
|
||||
})
|
||||
|
||||
file.on('error', (err) => {
|
||||
fs.unlinkSync(tempFilePath)
|
||||
reject(err)
|
||||
})
|
||||
}).on('error', (err) => {
|
||||
if (fs.existsSync(tempFilePath)) {
|
||||
fs.unlinkSync(tempFilePath)
|
||||
}
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 解压 Node.js 包
|
||||
async function extractNodeJs(filePath) {
|
||||
const platform = os.platform()
|
||||
const extractDir = path.join(os.tmpdir(), `node-v${NODE_VERSION}-extract`)
|
||||
|
||||
if (fs.existsSync(extractDir)) {
|
||||
console.log(`Removing existing extract directory: ${extractDir}`)
|
||||
fs.rmSync(extractDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
console.log(`Creating extract directory: ${extractDir}`)
|
||||
fs.mkdirSync(extractDir, { recursive: true })
|
||||
|
||||
console.log(`Extracting to ${extractDir}`)
|
||||
|
||||
if (platform === 'win32') {
|
||||
// Windows 使用内置的解压工具
|
||||
try {
|
||||
const AdmZip = require('adm-zip')
|
||||
console.log(`Using adm-zip to extract ${filePath}`)
|
||||
const zip = new AdmZip(filePath)
|
||||
zip.extractAllTo(extractDir, true)
|
||||
console.log(`Extraction completed using adm-zip`)
|
||||
} catch (error) {
|
||||
console.error(`Error using adm-zip: ${error}`)
|
||||
throw error
|
||||
}
|
||||
} else {
|
||||
// Linux/Mac 使用 tar
|
||||
try {
|
||||
console.log(`Using tar to extract ${filePath} to ${extractDir}`)
|
||||
execSync(`tar -xzf "${filePath}" -C "${extractDir}"`, { stdio: 'inherit' })
|
||||
console.log(`Extraction completed using tar`)
|
||||
} catch (error) {
|
||||
console.error(`Error using tar: ${error}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return extractDir
|
||||
}
|
||||
|
||||
// 安装 Node.js
|
||||
async function installNodeJs(extractDir) {
|
||||
const platform = os.platform()
|
||||
console.log(`Finding extracted Node.js directory in ${extractDir}`)
|
||||
|
||||
const items = fs.readdirSync(extractDir)
|
||||
console.log(`Found items in extract directory: ${items.join(', ')}`)
|
||||
|
||||
// 找到包含"node-v"的目录名
|
||||
const folderName = items.find(item => item.startsWith('node-v'))
|
||||
|
||||
if (!folderName) {
|
||||
throw new Error(`Could not find Node.js directory in ${extractDir}`)
|
||||
}
|
||||
|
||||
console.log(`Found Node.js directory: ${folderName}`)
|
||||
const nodeBinPath = path.join(extractDir, folderName, 'bin')
|
||||
|
||||
console.log(`Node.js bin path: ${nodeBinPath}`)
|
||||
|
||||
// 复制 node 和 npm
|
||||
if (platform === 'win32') {
|
||||
// Windows
|
||||
console.log('Installing Node.js binaries for Windows')
|
||||
fs.copyFileSync(
|
||||
path.join(extractDir, folderName, 'node.exe'),
|
||||
path.join(binariesDir, 'node.exe')
|
||||
)
|
||||
console.log(`Copied node.exe to ${path.join(binariesDir, 'node.exe')}`)
|
||||
|
||||
fs.copyFileSync(
|
||||
path.join(extractDir, folderName, 'npm.cmd'),
|
||||
path.join(binariesDir, 'npm.cmd')
|
||||
)
|
||||
console.log(`Copied npm.cmd to ${path.join(binariesDir, 'npm.cmd')}`)
|
||||
|
||||
fs.copyFileSync(
|
||||
path.join(extractDir, folderName, 'npx.cmd'),
|
||||
path.join(binariesDir, 'npx.cmd')
|
||||
)
|
||||
console.log(`Copied npx.cmd to ${path.join(binariesDir, 'npx.cmd')}`)
|
||||
} else {
|
||||
// Linux/Mac
|
||||
console.log('Installing Node.js binaries for Linux/Mac')
|
||||
fs.copyFileSync(
|
||||
path.join(nodeBinPath, 'node'),
|
||||
path.join(binariesDir, 'node')
|
||||
)
|
||||
console.log(`Copied node to ${path.join(binariesDir, 'node')}`)
|
||||
|
||||
// 创建npm脚本,指向正确路径
|
||||
const npmScript = `#!/usr/bin/env node
|
||||
require("./node_modules/npm/lib/cli.js")(process)`;
|
||||
fs.writeFileSync(path.join(binariesDir, 'npm'), npmScript);
|
||||
console.log(`Created npm script at ${path.join(binariesDir, 'npm')}`);
|
||||
|
||||
// 创建npx脚本,指向正确路径
|
||||
const npxScript = `#!/usr/bin/env node
|
||||
require("./node_modules/npm/bin/npx-cli.js")`;
|
||||
fs.writeFileSync(path.join(binariesDir, 'npx'), npxScript);
|
||||
console.log(`Created npx script at ${path.join(binariesDir, 'npx')}`);
|
||||
|
||||
// 设置执行权限
|
||||
execSync(`chmod +x "${path.join(binariesDir, 'node')}"`)
|
||||
execSync(`chmod +x "${path.join(binariesDir, 'npm')}"`)
|
||||
execSync(`chmod +x "${path.join(binariesDir, 'npx')}"`)
|
||||
console.log('Set executable permissions for Node.js binaries')
|
||||
}
|
||||
|
||||
// 复制 npm 相关文件和目录
|
||||
const npmDir = path.join(binariesDir, 'node_modules', 'npm')
|
||||
fs.mkdirSync(npmDir, { recursive: true })
|
||||
console.log(`Created npm directory at ${npmDir}`)
|
||||
|
||||
// 复制 npm 目录的内容
|
||||
const srcNpmDir = path.join(extractDir, folderName, 'lib', 'node_modules', 'npm')
|
||||
console.log(`Copying npm files from ${srcNpmDir} to ${npmDir}`)
|
||||
|
||||
const files = fs.readdirSync(srcNpmDir)
|
||||
|
||||
for (const file of files) {
|
||||
const srcPath = path.join(srcNpmDir, file)
|
||||
const destPath = path.join(npmDir, file)
|
||||
|
||||
if (fs.lstatSync(srcPath).isDirectory()) {
|
||||
// 使用自定义函数代替fs.cpSync,确保兼容性
|
||||
console.log(`Copying directory: ${file}`)
|
||||
copyFolderRecursiveSync(srcPath, destPath)
|
||||
} else {
|
||||
console.log(`Copying file: ${file}`)
|
||||
fs.copyFileSync(srcPath, destPath)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Node.js installation completed successfully')
|
||||
}
|
||||
|
||||
// 清理临时文件
|
||||
async function cleanup(filePath, extractDir) {
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
console.log(`Cleaning up temp file: ${filePath}`)
|
||||
fs.unlinkSync(filePath)
|
||||
}
|
||||
|
||||
if (fs.existsSync(extractDir)) {
|
||||
console.log(`Cleaning up extract directory: ${extractDir}`)
|
||||
fs.rmSync(extractDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
console.log('Cleaned up temporary files')
|
||||
} catch (error) {
|
||||
console.error('Error during cleanup:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 主安装函数
|
||||
async function install() {
|
||||
try {
|
||||
console.log(`Starting Node.js v${NODE_VERSION} installation...`)
|
||||
|
||||
await createBinariesDir()
|
||||
console.log('Binary directory created/verified')
|
||||
|
||||
const filePath = await downloadNodeJs()
|
||||
console.log(`Downloaded Node.js to ${filePath}`)
|
||||
|
||||
const extractDir = await extractNodeJs(filePath)
|
||||
console.log(`Extracted Node.js to ${extractDir}`)
|
||||
|
||||
await installNodeJs(extractDir)
|
||||
console.log('Installed Node.js binaries')
|
||||
|
||||
await cleanup(filePath, extractDir)
|
||||
console.log('Cleanup completed')
|
||||
|
||||
console.log(`Node.js v${NODE_VERSION} has been installed successfully at ${binariesDir}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Installation failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 执行安装
|
||||
install()
|
||||
.then(() => {
|
||||
console.log('Installation process completed successfully')
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Fatal error during installation:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
16
src/@types/index.d.ts
vendored
Normal file
16
src/@types/index.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface NodeAppType {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
description?: string
|
||||
author?: string
|
||||
homepage?: string
|
||||
repositoryUrl?: string
|
||||
port?: number
|
||||
installCommand?: string
|
||||
buildCommand?: string
|
||||
startCommand?: string
|
||||
isInstalled: boolean
|
||||
isRunning: boolean
|
||||
url?: string
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import { getResourcePath } from './utils'
|
||||
import { decrypt, encrypt } from './utils/aes'
|
||||
import { getFilesDir } from './utils/file'
|
||||
import { compress, decompress } from './utils/zip'
|
||||
import NodeAppService from './services/NodeAppService'
|
||||
|
||||
const fileManager = new FileStorage()
|
||||
const backupManager = new BackupManager()
|
||||
@@ -257,6 +258,17 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
|
||||
ipcMain.handle('mcp:cleanup', async () => mcpService.cleanup())
|
||||
|
||||
// Shell API
|
||||
ipcMain.handle('shell:openExternal', async (_, url: string) => {
|
||||
try {
|
||||
log.info(`Opening external URL: ${url}`)
|
||||
return await shell.openExternal(url)
|
||||
} catch (error) {
|
||||
log.error('Error opening external URL:', error)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('app:is-binary-exist', (_, name: string) => isBinaryExists(name))
|
||||
ipcMain.handle('app:get-binary-path', (_, name: string) => getBinaryPath(name))
|
||||
ipcMain.handle('app:install-uv-binary', () => runInstallScript('install-uv.js'))
|
||||
@@ -276,4 +288,53 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle('copilot:get-token', CopilotService.getToken)
|
||||
ipcMain.handle('copilot:logout', CopilotService.logout)
|
||||
ipcMain.handle('copilot:get-user', CopilotService.getUser)
|
||||
|
||||
// Node app management
|
||||
const nodeAppService = NodeAppService.getInstance()
|
||||
|
||||
ipcMain.handle('nodeapp:list', async () => await nodeAppService.getAllApps())
|
||||
|
||||
ipcMain.handle('nodeapp:add', async (_, app) => await nodeAppService.addApp(app))
|
||||
|
||||
ipcMain.handle('nodeapp:install', async (_, appId) => await nodeAppService.installApp(appId))
|
||||
|
||||
ipcMain.handle('nodeapp:update', async (_, appId) => await nodeAppService.updateApp(appId))
|
||||
|
||||
ipcMain.handle('nodeapp:start', async (_, appId) => await nodeAppService.startApp(appId))
|
||||
|
||||
ipcMain.handle('nodeapp:stop', async (_, appId) => await nodeAppService.stopApp(appId))
|
||||
|
||||
ipcMain.handle('nodeapp:uninstall', async (_, appId) => await nodeAppService.uninstallApp(appId))
|
||||
|
||||
ipcMain.handle('nodeapp:deploy-zip', async (_, zipPath, options) => await nodeAppService.deployFromZip(zipPath, options))
|
||||
|
||||
ipcMain.handle('nodeapp:deploy-git', async (_, repoUrl, options) => await nodeAppService.deployFromGit(repoUrl, options))
|
||||
|
||||
ipcMain.handle('nodeapp:check-node', async () => {
|
||||
const isNodeInstalled = await isBinaryExists('node')
|
||||
return isNodeInstalled
|
||||
})
|
||||
|
||||
ipcMain.handle('nodeapp:install-node', async () => {
|
||||
return await nodeAppService.installNodeJs()
|
||||
})
|
||||
|
||||
// Listen for changes in Node.js apps and notify renderer
|
||||
nodeAppService.on('apps-updated', (apps) => {
|
||||
mainWindow?.webContents.send('nodeapp:updated', apps)
|
||||
})
|
||||
|
||||
app.on('before-quit', () => nodeAppService.cleanup())
|
||||
|
||||
// 运行简单命令
|
||||
ipcMain.handle('app:run-command', async (_, command: string) => {
|
||||
try {
|
||||
const { execSync } = require('child_process')
|
||||
const result = execSync(command).toString()
|
||||
return result
|
||||
} catch (error) {
|
||||
log.error('Error running command:', error)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
1351
src/main/services/NodeAppService.ts
Normal file
1351
src/main/services/NodeAppService.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,13 +6,13 @@ import path from 'path'
|
||||
|
||||
import { getResourcePath } from '.'
|
||||
|
||||
export function runInstallScript(scriptPath: string): Promise<void> {
|
||||
export function runInstallScript(scriptPath: string, env?: NodeJS.ProcessEnv): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const installScriptPath = path.join(getResourcePath(), 'scripts', scriptPath)
|
||||
log.info(`Running script at: ${installScriptPath}`)
|
||||
|
||||
const nodeProcess = spawn(process.execPath, [installScriptPath], {
|
||||
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }
|
||||
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1', ...env }
|
||||
})
|
||||
|
||||
nodeProcess.stdout.on('data', (data) => {
|
||||
|
||||
27
src/preload/index.d.ts
vendored
27
src/preload/index.d.ts
vendored
@@ -163,10 +163,37 @@ declare global {
|
||||
logout: () => Promise<void>
|
||||
getUser: (token: string) => Promise<{ login: string; avatar: string }>
|
||||
}
|
||||
nodeapp: {
|
||||
list: () => Promise<any[]>
|
||||
add: (app: any) => Promise<any>
|
||||
install: (appId: string) => Promise<any | null>
|
||||
update: (appId: string) => Promise<any | null>
|
||||
start: (appId: string) => Promise<{ port: number; url: string } | null>
|
||||
stop: (appId: string) => Promise<boolean>
|
||||
uninstall: (appId: string) => Promise<boolean>
|
||||
deployZip: (zipPath: string, options?: {
|
||||
name?: string;
|
||||
port?: number;
|
||||
startCommand?: string;
|
||||
installCommand?: string;
|
||||
buildCommand?: string;
|
||||
}) => Promise<{ port: number; url: string } | null>
|
||||
deployGit: (repoUrl: string, options?: {
|
||||
name?: string;
|
||||
port?: number;
|
||||
startCommand?: string;
|
||||
installCommand?: string;
|
||||
buildCommand?: string;
|
||||
}) => Promise<{ port: number; url: string } | null>
|
||||
checkNode: () => Promise<boolean>
|
||||
installNode: () => Promise<boolean>
|
||||
onUpdated: (callback: (apps: any[]) => void) => () => void
|
||||
}
|
||||
isBinaryExist: (name: string) => Promise<boolean>
|
||||
getBinaryPath: (name: string) => Promise<string>
|
||||
installUVBinary: () => Promise<void>
|
||||
installBunBinary: () => Promise<void>
|
||||
run: (command: string) => Promise<string>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,38 @@ const api = {
|
||||
restoreFromWebdav: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:restoreFromWebdav', webdavConfig),
|
||||
listWebdavFiles: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:listWebdavFiles', webdavConfig)
|
||||
},
|
||||
nodeapp: {
|
||||
list: () => ipcRenderer.invoke('nodeapp:list'),
|
||||
add: (app: any) => ipcRenderer.invoke('nodeapp:add', app),
|
||||
install: (appId: string) => ipcRenderer.invoke('nodeapp:install', appId),
|
||||
update: (appId: string) => ipcRenderer.invoke('nodeapp:update', appId),
|
||||
start: (appId: string) => ipcRenderer.invoke('nodeapp:start', appId),
|
||||
stop: (appId: string) => ipcRenderer.invoke('nodeapp:stop', appId),
|
||||
uninstall: (appId: string) => ipcRenderer.invoke('nodeapp:uninstall', appId),
|
||||
deployZip: (zipPath: string, options?: {
|
||||
name?: string;
|
||||
port?: number;
|
||||
startCommand?: string;
|
||||
installCommand?: string;
|
||||
buildCommand?: string;
|
||||
}) => ipcRenderer.invoke('nodeapp:deploy-zip', zipPath, options),
|
||||
deployGit: (repoUrl: string, options?: {
|
||||
name?: string;
|
||||
port?: number;
|
||||
startCommand?: string;
|
||||
installCommand?: string;
|
||||
buildCommand?: string;
|
||||
}) => ipcRenderer.invoke('nodeapp:deploy-git', repoUrl, options),
|
||||
checkNode: () => ipcRenderer.invoke('nodeapp:check-node'),
|
||||
installNode: () => ipcRenderer.invoke('nodeapp:install-node'),
|
||||
onUpdated: (callback: (apps: any[]) => void) => {
|
||||
const eventListener = (_: any, apps: any[]) => callback(apps)
|
||||
ipcRenderer.on('nodeapp:updated', eventListener)
|
||||
return () => {
|
||||
ipcRenderer.removeListener('nodeapp:updated', eventListener)
|
||||
}
|
||||
}
|
||||
},
|
||||
file: {
|
||||
select: (options?: OpenDialogOptions) => ipcRenderer.invoke('file:select', options),
|
||||
upload: (filePath: string) => ipcRenderer.invoke('file:upload', filePath),
|
||||
@@ -124,7 +156,7 @@ const api = {
|
||||
cleanup: () => ipcRenderer.invoke('mcp:cleanup')
|
||||
},
|
||||
shell: {
|
||||
openExternal: shell.openExternal
|
||||
openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url)
|
||||
},
|
||||
copilot: {
|
||||
getAuthMessage: (headers?: Record<string, string>) => ipcRenderer.invoke('copilot:get-auth-message', headers),
|
||||
@@ -140,7 +172,8 @@ const api = {
|
||||
isBinaryExist: (name: string) => ipcRenderer.invoke('app:is-binary-exist', name),
|
||||
getBinaryPath: (name: string) => ipcRenderer.invoke('app:get-binary-path', name),
|
||||
installUVBinary: () => ipcRenderer.invoke('app:install-uv-binary'),
|
||||
installBunBinary: () => ipcRenderer.invoke('app:install-bun-binary')
|
||||
installBunBinary: () => ipcRenderer.invoke('app:install-bun-binary'),
|
||||
run: (command: string) => ipcRenderer.invoke('app:run-command', command)
|
||||
}
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
|
||||
@@ -17,6 +17,7 @@ import AppsPage from './pages/apps/AppsPage'
|
||||
import FilesPage from './pages/files/FilesPage'
|
||||
import HomePage from './pages/home/HomePage'
|
||||
import KnowledgePage from './pages/knowledge/KnowledgePage'
|
||||
import NodeAppsPage from './pages/nodeapps/NodeAppsPage'
|
||||
import PaintingsPage from './pages/paintings/PaintingsPage'
|
||||
import SettingsPage from './pages/settings/SettingsPage'
|
||||
import TranslatePage from './pages/translate/TranslatePage'
|
||||
@@ -41,6 +42,7 @@ function App(): JSX.Element {
|
||||
<Route path="/files" element={<FilesPage />} />
|
||||
<Route path="/knowledge" element={<KnowledgePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/nodeapps" element={<NodeAppsPage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
|
||||
@@ -130,6 +130,7 @@ const MainMenus: FC = () => {
|
||||
paintings: <PictureOutlined style={{ fontSize: 16 }} />,
|
||||
translate: <TranslationOutlined />,
|
||||
minapp: <i className="iconfont icon-appstore" />,
|
||||
nodeapps: <i className="iconfont icon-code" />,
|
||||
knowledge: <FileSearchOutlined />,
|
||||
files: <FolderOutlined />
|
||||
}
|
||||
@@ -140,6 +141,7 @@ const MainMenus: FC = () => {
|
||||
paintings: '/paintings',
|
||||
translate: '/translate',
|
||||
minapp: '/apps',
|
||||
nodeapps: '/nodeapps',
|
||||
knowledge: '/knowledge',
|
||||
files: '/files'
|
||||
}
|
||||
|
||||
2
src/renderer/src/env.d.ts
vendored
2
src/renderer/src/env.d.ts
vendored
@@ -20,5 +20,7 @@ declare global {
|
||||
keyv: KeyvStorage
|
||||
mermaid: any
|
||||
store: any
|
||||
backupToWebdav: (data: string, webdavConfig: WebDavConfig) => Promise<boolean>
|
||||
restoreFromWebdav: (webdavConfig: WebDavConfig) => Promise<{ data: string; success: boolean }>
|
||||
}
|
||||
}
|
||||
|
||||
88
src/renderer/src/hooks/useNodeApps.ts
Normal file
88
src/renderer/src/hooks/useNodeApps.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { NodeAppType } from '@renderer/types'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
export function useNodeApps() {
|
||||
const [apps, setApps] = useState<NodeAppType[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
// Load apps
|
||||
const loadApps = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const result = await window.api.nodeapp.list()
|
||||
setApps(result || [])
|
||||
} catch (error) {
|
||||
console.error('Error loading node apps:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Add app
|
||||
const addApp = useCallback(async (app: NodeAppType) => {
|
||||
const result = await window.api.nodeapp.add(app)
|
||||
await loadApps()
|
||||
return result
|
||||
}, [loadApps])
|
||||
|
||||
// Install app
|
||||
const installApp = useCallback(async (appId: string) => {
|
||||
const result = await window.api.nodeapp.install(appId)
|
||||
await loadApps()
|
||||
return result
|
||||
}, [loadApps])
|
||||
|
||||
// Update app
|
||||
const updateApp = useCallback(async (appId: string) => {
|
||||
const result = await window.api.nodeapp.update(appId)
|
||||
await loadApps()
|
||||
return result
|
||||
}, [loadApps])
|
||||
|
||||
// Start app
|
||||
const startApp = useCallback(async (appId: string) => {
|
||||
const result = await window.api.nodeapp.start(appId)
|
||||
await loadApps()
|
||||
return result
|
||||
}, [loadApps])
|
||||
|
||||
// Stop app
|
||||
const stopApp = useCallback(async (appId: string) => {
|
||||
const result = await window.api.nodeapp.stop(appId)
|
||||
await loadApps()
|
||||
return result
|
||||
}, [loadApps])
|
||||
|
||||
// Uninstall app
|
||||
const uninstallApp = useCallback(async (appId: string) => {
|
||||
const result = await window.api.nodeapp.uninstall(appId)
|
||||
await loadApps()
|
||||
return result
|
||||
}, [loadApps])
|
||||
|
||||
// Initialize
|
||||
useEffect(() => {
|
||||
loadApps()
|
||||
|
||||
// Subscribe to app updates
|
||||
const unsubscribe = window.api.nodeapp.onUpdated((updatedApps) => {
|
||||
setApps(updatedApps || [])
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubscribe()
|
||||
}
|
||||
}, [loadApps])
|
||||
|
||||
return {
|
||||
apps,
|
||||
loading,
|
||||
addApp,
|
||||
installApp,
|
||||
updateApp,
|
||||
startApp,
|
||||
stopApp,
|
||||
uninstallApp,
|
||||
refresh: loadApps
|
||||
}
|
||||
}
|
||||
@@ -488,12 +488,21 @@
|
||||
"tools": {
|
||||
"invoking": "Invoking",
|
||||
"completed": "Completed"
|
||||
}
|
||||
},
|
||||
"nextJsInfo": "Next.js Application Note",
|
||||
"nextJsDescription": "Next.js applications require a build step before they can be started. Make sure to check 'This is a Next.js application' or manually add 'npm run build' as the build command."
|
||||
},
|
||||
"minapp": {
|
||||
"sidebar.add.title": "Add to sidebar",
|
||||
"sidebar.remove.title": "Remove from sidebar",
|
||||
"title": "MinApp"
|
||||
"add": "Add",
|
||||
"apps.tab.search": "Search apps",
|
||||
"apps.tab.title": "Apps",
|
||||
"empty": "No mini apps",
|
||||
"find": "Find more",
|
||||
"more": "More",
|
||||
"settings.disabled_apps": "Disabled Apps",
|
||||
"sidebar.add.title": "Add to Sidebar",
|
||||
"sidebar.remove.title": "Remove from Sidebar",
|
||||
"title": "Web Apps"
|
||||
},
|
||||
"miniwindow": {
|
||||
"clipboard": {
|
||||
@@ -1115,6 +1124,74 @@
|
||||
"title": "Tavily"
|
||||
},
|
||||
"title": "Web Search"
|
||||
},
|
||||
"nodeRequired": "Node.js Required",
|
||||
"nodeSettings": {
|
||||
"title": "Node.js Environment Settings",
|
||||
"description": "Manage the built-in Node.js environment for Cherry Studio. You can select which version of Node.js to install for optimal compatibility.",
|
||||
"status": "Status",
|
||||
"checking": "Checking...",
|
||||
"installed": "Installed",
|
||||
"notInstalled": "Not Installed",
|
||||
"refresh": "Refresh",
|
||||
"version": "Node.js Version",
|
||||
"versionHelp": "Select the version of Node.js to install",
|
||||
"customVersion": "Custom Version",
|
||||
"customVersionHelp": "If you need a specific version, enter it here (e.g., 18.16.1)",
|
||||
"install": "Install Node.js",
|
||||
"reinstall": "Reinstall Node.js",
|
||||
"installSuccess": "Node.js v{{version}} installed successfully",
|
||||
"installFailed": "Failed to install Node.js"
|
||||
},
|
||||
"nodeSettingsTab": "Node.js Environment",
|
||||
"appsManagerTab": "Apps Manager",
|
||||
"packageDeployerTab": "Deploy Package",
|
||||
"packageDeployer": {
|
||||
"advancedOptions": "Advanced Options",
|
||||
"deploy": "Deploy",
|
||||
"deployFailed": "Failed to deploy package",
|
||||
"deploySuccess": "{{name}} has been successfully deployed on port {{port}}",
|
||||
"description": "Upload a ZIP file containing Node.js application code. The package will be automatically extracted and installed.",
|
||||
"fileSelectError": "Error selecting file",
|
||||
"installNode": "Install Node.js",
|
||||
"installNodePrompt": "Node.js is required to deploy applications. Would you like to install it now?",
|
||||
"namePlaceholder": "Enter a name for your deployed application",
|
||||
"nodeInstallFailed": "Failed to install Node.js",
|
||||
"nodeInstallSuccess": "Node.js installed successfully",
|
||||
"nodeNeeded": "Built-in Node.js is required to run applications.",
|
||||
"nodeNotAvailable": "Node.js is not available",
|
||||
"nodeRequired": "Node.js Required",
|
||||
"noFileSelected": "Please select a ZIP file to deploy",
|
||||
"open": "Open in Browser",
|
||||
"selectZip": "Click to select ZIP file",
|
||||
"title": "Deploy Code Package",
|
||||
"moduleTypeError": "Module Type Error",
|
||||
"esModuleError": "ES module syntax detected. Set \"type\": \"module\" in package.json or use .mjs extension.",
|
||||
"convertToCommonJS": "Convert to CommonJS syntax",
|
||||
"nextJsDetected": "Next.js Application Detected",
|
||||
"buildStepAdded": "Build step has been automatically added for Next.js application.",
|
||||
"nextJsInfo": "Next.js Application Note",
|
||||
"nextJsDescription": "Next.js applications require a build step before they can be started. Make sure to check 'This is a Next.js application' or manually add 'npm run build' as the build command.",
|
||||
"deployPackage": "Deploy Package",
|
||||
"deployFromZip": "From ZIP",
|
||||
"deployFromGit": "From Git",
|
||||
"selectZipFile": "Select ZIP File",
|
||||
"appName": "Application Name",
|
||||
"appNamePlaceholder": "My Application",
|
||||
"port": "Port",
|
||||
"portPlaceholder": "3000",
|
||||
"portTooltip": "The port on which your application will run. Leave empty for automatic port assignment.",
|
||||
"showAdvanced": "Show Advanced Options",
|
||||
"hideAdvanced": "Hide Advanced Options",
|
||||
"installCommand": "Install Command",
|
||||
"buildCommand": "Build Command",
|
||||
"startCommand": "Start Command",
|
||||
"isNextJs": "This is a Next.js application",
|
||||
"deploy": "Deploy",
|
||||
"repoUrl": "Git Repository URL",
|
||||
"repoUrlRequired": "Please enter a Git repository URL",
|
||||
"noRepoUrlProvided": "Please provide a Git repository URL",
|
||||
"packageRequired": "Please select a package file"
|
||||
}
|
||||
},
|
||||
"translate": {
|
||||
@@ -1152,6 +1229,146 @@
|
||||
"quit": "Quit",
|
||||
"show_window": "Show Window",
|
||||
"visualization": "Visualization"
|
||||
},
|
||||
"nodeapp": {
|
||||
"add": "Add App",
|
||||
"addNew": "Add New Node.js App",
|
||||
"addSuccess": "App added successfully",
|
||||
"author": "Author",
|
||||
"codeRunner": {
|
||||
"description": "Enter your Node.js code below and click 'Run' to execute it. Your code will be run in a temporary Node.js environment.",
|
||||
"emptyCode": "Please enter some code to run",
|
||||
"open": "Open in Browser",
|
||||
"output": "Output",
|
||||
"placeholder": "// Enter your Node.js code here\n// Example:\nconst http = require('http');\n\nconst server = http.createServer((req, res) => {\n res.writeHead(200, { 'Content-Type': 'text/html' });\n res.end('<h1>Hello from Cherry Studio!</h1>');\n});\n\nconst PORT = process.env.PORT || 3000;\nserver.listen(PORT, () => {\n console.log(`Server running on port ${PORT}`);\n});",
|
||||
"run": "Run Code",
|
||||
"success": "Code is running on port {{port}}",
|
||||
"title": "Code Runner"
|
||||
},
|
||||
"codeRunnerTab": "Code Runner",
|
||||
"empty": "No Node.js apps found",
|
||||
"featured": "Featured Apps",
|
||||
"form": {
|
||||
"author": "Author",
|
||||
"authorPlaceholder": "The author of the app",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "Brief description of the app's functionality",
|
||||
"homepage": "Homepage",
|
||||
"homepagePlaceholder": "Homepage URL for the application",
|
||||
"installCommand": "Install Command",
|
||||
"installCommandHelp": "Command to install dependencies (defaults to 'npm install')",
|
||||
"buildCommand": "Build Command",
|
||||
"buildCommandHelp": "Command to build the application before starting (e.g. 'npm run build')",
|
||||
"isNextJs": "This is a Next.js application",
|
||||
"nextJsHelp": "Apply Next.js-specific optimizations for deployment",
|
||||
"name": "App Name",
|
||||
"nameRequired": "App name is required",
|
||||
"namePlaceholder": "Name of your Node.js application",
|
||||
"port": "Port",
|
||||
"portHelp": "Port the app will run on (detected automatically if not specified)"
|
||||
},
|
||||
"install": "Install",
|
||||
"installSuccess": "{{name}} installed successfully",
|
||||
"installed": "Installed",
|
||||
"marketplaceTab": "Marketplace",
|
||||
"more": "More",
|
||||
"notInstalled": "Not Installed",
|
||||
"packageDeployer": {
|
||||
"advancedOptions": "Advanced Options",
|
||||
"deploy": "Deploy",
|
||||
"deployFailed": "Failed to deploy package",
|
||||
"deploySuccess": "{{name}} has been successfully deployed on port {{port}}",
|
||||
"description": "Upload a ZIP file containing Node.js application code. The package will be automatically extracted and installed.",
|
||||
"fileSelectError": "Error selecting file",
|
||||
"installNode": "Install Node.js",
|
||||
"installNodePrompt": "Node.js is required to deploy applications. Would you like to install it now?",
|
||||
"namePlaceholder": "Enter a name for your deployed application",
|
||||
"nodeInstallFailed": "Failed to install Node.js",
|
||||
"nodeInstallSuccess": "Node.js installed successfully",
|
||||
"nodeNeeded": "Built-in Node.js is required to run applications.",
|
||||
"nodeNotAvailable": "Node.js is not available",
|
||||
"nodeRequired": "Node.js Required",
|
||||
"noFileSelected": "Please select a ZIP file to deploy",
|
||||
"open": "Open in Browser",
|
||||
"selectZip": "Click to select ZIP file",
|
||||
"title": "Deploy Code Package",
|
||||
"moduleTypeError": "Module Type Error",
|
||||
"esModuleError": "ES module syntax detected. Set \"type\": \"module\" in package.json or use .mjs extension.",
|
||||
"convertToCommonJS": "Convert to CommonJS syntax",
|
||||
"nextJsDetected": "Next.js Application Detected",
|
||||
"buildStepAdded": "Build step has been automatically added for Next.js application.",
|
||||
"nextJsInfo": "Next.js Application Note",
|
||||
"nextJsDescription": "Next.js applications require a build step before they can be started. Make sure to check 'This is a Next.js application' or manually add 'npm run build' as the build command.",
|
||||
"deployPackage": "Deploy Package",
|
||||
"deployFromZip": "From ZIP",
|
||||
"deployFromGit": "From Git",
|
||||
"selectZipFile": "Select ZIP File",
|
||||
"appName": "Application Name",
|
||||
"appNamePlaceholder": "My Application",
|
||||
"port": "Port",
|
||||
"portPlaceholder": "3000",
|
||||
"portTooltip": "The port on which your application will run. Leave empty for automatic port assignment.",
|
||||
"showAdvanced": "Show Advanced Options",
|
||||
"hideAdvanced": "Hide Advanced Options",
|
||||
"installCommand": "Install Command",
|
||||
"buildCommand": "Build Command",
|
||||
"startCommand": "Start Command",
|
||||
"isNextJs": "This is a Next.js application",
|
||||
"deploy": "Deploy",
|
||||
"repoUrl": "Git Repository URL",
|
||||
"repoUrlRequired": "Please enter a Git repository URL",
|
||||
"noRepoUrlProvided": "Please provide a Git repository URL",
|
||||
"packageRequired": "Please select a package file"
|
||||
},
|
||||
"packageDeployerTab": "Deploy Package",
|
||||
"running": "Running",
|
||||
"start": "Start",
|
||||
"startSuccess": "{{name}} started on port {{port}}",
|
||||
"stop": "Stop",
|
||||
"stopSuccess": "{{name}} stopped successfully",
|
||||
"title": "Node.js Apps",
|
||||
"uninstall": "Uninstall",
|
||||
"uninstallSuccess": "{{name}} uninstalled successfully",
|
||||
"update": "Update",
|
||||
"updateSuccess": "{{name}} updated successfully",
|
||||
"version": "Version",
|
||||
"viewRepository": "View Repository"
|
||||
},
|
||||
"model": {
|
||||
"add_parameter": "Add Parameter",
|
||||
"all": "All",
|
||||
"custom_parameters": "Custom Parameters",
|
||||
"dimensions": "Dimensions {{dimensions}}",
|
||||
"edit": "Edit Model",
|
||||
"embedding": "Embedding",
|
||||
"embedding_model": "Embedding Model",
|
||||
"embedding_model_tooltip": "Add in Settings->Model Provider->Manage",
|
||||
"free": "Free",
|
||||
"no_matches": "No models available",
|
||||
"parameter_name": "Parameter Name",
|
||||
"parameter_type": {
|
||||
"boolean": "Boolean",
|
||||
"json": "JSON",
|
||||
"number": "Number",
|
||||
"string": "Text"
|
||||
},
|
||||
"pinned": "Pinned",
|
||||
"reasoning": "Reasoning",
|
||||
"search": "Search models...",
|
||||
"stream_output": "Stream output",
|
||||
"function_calling": "Function Calling",
|
||||
"type": {
|
||||
"embedding": "Embedding",
|
||||
"reasoning": "Reasoning",
|
||||
"select": "Select Model Types",
|
||||
"text": "Text",
|
||||
"vision": "Vision",
|
||||
"function_calling": "Function Calling"
|
||||
},
|
||||
"vision": "Vision",
|
||||
"websearch": "WebSearch",
|
||||
"rerank_model": "Reordering Model",
|
||||
"rerank_model_tooltip": "Click the Manage button in Settings -> Model Services to add."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1115,6 +1115,53 @@
|
||||
"title": "Tavily"
|
||||
},
|
||||
"title": "网络搜索"
|
||||
},
|
||||
"nodeRequired": "需要 Node.js",
|
||||
"nodeSettings": {
|
||||
"title": "Node.js 环境设置",
|
||||
"description": "管理 Cherry Studio 内置的 Node.js 环境。您可以选择要安装的 Node.js 版本,以确保最佳的兼容性。",
|
||||
"status": "状态",
|
||||
"checking": "检查中...",
|
||||
"installed": "已安装",
|
||||
"notInstalled": "未安装",
|
||||
"refresh": "刷新",
|
||||
"version": "Node.js 版本",
|
||||
"versionHelp": "选择要安装的 Node.js 版本",
|
||||
"customVersion": "自定义版本",
|
||||
"customVersionHelp": "如果您需要特定版本,请在此输入版本号(如 18.16.1)",
|
||||
"install": "安装 Node.js",
|
||||
"reinstall": "重新安装 Node.js",
|
||||
"installSuccess": "Node.js v{{version}} 安装成功",
|
||||
"installFailed": "Node.js 安装失败"
|
||||
},
|
||||
"nodeSettingsTab": "Node.js 环境",
|
||||
"appsManagerTab": "应用管理",
|
||||
"packageDeployerTab": "部署代码包",
|
||||
"packageDeployer": {
|
||||
"advancedOptions": "高级选项",
|
||||
"deploy": "部署",
|
||||
"deployFailed": "部署包失败",
|
||||
"deploySuccess": "{{name}} 已成功部署在端口 {{port}} 上",
|
||||
"description": "上传包含 Node.js 应用程序代码的 ZIP 文件。该包将被自动解压和安装。",
|
||||
"fileSelectError": "选择文件时出错",
|
||||
"installNode": "安装 Node.js",
|
||||
"installNodePrompt": "部署应用程序需要 Node.js。您要现在安装吗?",
|
||||
"namePlaceholder": "为您部署的应用输入名称",
|
||||
"nodeInstallFailed": "安装 Node.js 失败",
|
||||
"nodeInstallSuccess": "Node.js 安装成功",
|
||||
"nodeNeeded": "运行应用程序需要内置 Node.js。",
|
||||
"nodeNotAvailable": "Node.js 不可用",
|
||||
"noFileSelected": "请选择要部署的 ZIP 文件",
|
||||
"open": "在浏览器中打开",
|
||||
"selectZip": "点击选择 ZIP 文件",
|
||||
"title": "部署代码包",
|
||||
"moduleTypeError": "模块类型错误",
|
||||
"esModuleError": "发现 ES 模块语法。请在 package.json 中设置 \"type\": \"module\" 或使用 .mjs 扩展名。",
|
||||
"convertToCommonJS": "转换为 CommonJS 语法",
|
||||
"nextJsDetected": "检测到 Next.js 应用",
|
||||
"buildStepAdded": "已自动添加构建步骤:将在启动应用前执行 'npm run build'。",
|
||||
"nextJsInfo": "Next.js 应用注意事项",
|
||||
"nextJsDescription": "Next.js 应用需要先构建后才能启动。请确保勾选\"这是一个 Next.js 应用\"或手动添加\"npm run build\"作为构建命令。"
|
||||
}
|
||||
},
|
||||
"translate": {
|
||||
@@ -1152,6 +1199,32 @@
|
||||
"quit": "退出",
|
||||
"show_window": "显示窗口",
|
||||
"visualization": "可视化"
|
||||
},
|
||||
"nodeapp": {
|
||||
"add": "添加",
|
||||
"addApp": "添加应用",
|
||||
"appName": "应用名称",
|
||||
"appsManager": {
|
||||
"confirmDelete": "确定要删除此应用吗?",
|
||||
"confirmStop": "确定要停止此应用吗?",
|
||||
"description": "管理应用",
|
||||
"install": "安装",
|
||||
"noApps": "暂无应用,请添加新应用或从代码部署",
|
||||
"port": "端口",
|
||||
"repository": "仓库",
|
||||
"start": "启动",
|
||||
"status": "状态",
|
||||
"stop": "停止",
|
||||
"title": "应用管理",
|
||||
"uninstall": "卸载",
|
||||
"update": "更新",
|
||||
"updateProgress": "更新进度",
|
||||
"updateSuccess": "{{name}} 更新成功",
|
||||
"version": "版本",
|
||||
"viewRepository": "查看仓库"
|
||||
},
|
||||
"nextJsInfo": "Next.js 应用注意事项",
|
||||
"nextJsDescription": "Next.js 应用需要先构建后才能启动。请确保勾选\"这是一个 Next.js 应用\"或手动添加\"npm run build\"作为构建命令。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
301
src/renderer/src/pages/nodeapps/NodeApp.tsx
Normal file
301
src/renderer/src/pages/nodeapps/NodeApp.tsx
Normal file
@@ -0,0 +1,301 @@
|
||||
import { DownloadOutlined, GithubOutlined, LoadingOutlined, PlayCircleOutlined, ReloadOutlined, StopOutlined } from '@ant-design/icons'
|
||||
import { useNodeApps } from '@renderer/hooks/useNodeApps'
|
||||
import { NodeAppType } from '@renderer/types'
|
||||
import { Avatar, Button, Card, Dropdown, Menu, Space, Tag, Tooltip, Typography, notification } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const { Title, Paragraph, Text } = Typography
|
||||
|
||||
interface Props {
|
||||
app: NodeAppType
|
||||
}
|
||||
|
||||
const NodeApp: FC<Props> = ({ app }) => {
|
||||
const { t } = useTranslation()
|
||||
const { installApp, updateApp, startApp, stopApp, uninstallApp } = useNodeApps()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [actionType, setActionType] = useState<string>('')
|
||||
|
||||
// Handle installation
|
||||
const handleInstall = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setActionType('install')
|
||||
await installApp(app.id as string)
|
||||
notification.success({
|
||||
message: t('common.success'),
|
||||
description: t('nodeapp.installSuccess', { name: app.name })
|
||||
})
|
||||
} catch (err) {
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: err instanceof Error ? err.message : String(err)
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setActionType('')
|
||||
}
|
||||
}
|
||||
|
||||
// Handle update
|
||||
const handleUpdate = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setActionType('update')
|
||||
await updateApp(app.id as string)
|
||||
notification.success({
|
||||
message: t('common.success'),
|
||||
description: t('nodeapp.updateSuccess', { name: app.name })
|
||||
})
|
||||
} catch (err) {
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: err instanceof Error ? err.message : String(err)
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setActionType('')
|
||||
}
|
||||
}
|
||||
|
||||
// Handle start
|
||||
const handleStart = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setActionType('start')
|
||||
const result = await startApp(app.id as string)
|
||||
if (result) {
|
||||
notification.success({
|
||||
message: t('common.success'),
|
||||
description: t('nodeapp.startSuccess', { name: app.name, port: result.port })
|
||||
})
|
||||
window.api.openWebsite(result.url)
|
||||
}
|
||||
} catch (err) {
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: err instanceof Error ? err.message : String(err)
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setActionType('')
|
||||
}
|
||||
}
|
||||
|
||||
// Handle stop
|
||||
const handleStop = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setActionType('stop')
|
||||
await stopApp(app.id as string)
|
||||
notification.success({
|
||||
message: t('common.success'),
|
||||
description: t('nodeapp.stopSuccess', { name: app.name })
|
||||
})
|
||||
} catch (err) {
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: err instanceof Error ? err.message : String(err)
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setActionType('')
|
||||
}
|
||||
}
|
||||
|
||||
// Handle uninstall
|
||||
const handleUninstall = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setActionType('uninstall')
|
||||
await uninstallApp(app.id as string)
|
||||
notification.success({
|
||||
message: t('common.success'),
|
||||
description: t('nodeapp.uninstallSuccess', { name: app.name })
|
||||
})
|
||||
} catch (err) {
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: err instanceof Error ? err.message : String(err)
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setActionType('')
|
||||
}
|
||||
}
|
||||
|
||||
// Open GitHub repository
|
||||
const openRepository = () => {
|
||||
if (app.repositoryUrl) {
|
||||
window.api.openWebsite(app.repositoryUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// Open app homepage
|
||||
const openHomepage = () => {
|
||||
if (app.homepage) {
|
||||
window.api.openWebsite(app.homepage)
|
||||
}
|
||||
}
|
||||
|
||||
// Render app status tag
|
||||
const renderStatusTag = () => {
|
||||
if (app.isRunning) {
|
||||
return <Tag color="green">{t('nodeapp.running')}</Tag>
|
||||
}
|
||||
if (app.isInstalled) {
|
||||
return <Tag color="blue">{t('nodeapp.installed')}</Tag>
|
||||
}
|
||||
return <Tag color="default">{t('nodeapp.notInstalled')}</Tag>
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
hoverable
|
||||
style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
|
||||
cover={
|
||||
<CardCoverContainer>
|
||||
{app.logo ? (
|
||||
<img alt={app.name} src={app.logo} style={{ width: '100%', height: '140px', objectFit: 'cover' }} />
|
||||
) : (
|
||||
<AppLogo>
|
||||
<Avatar size={64} style={{ backgroundColor: '#1890ff' }}>
|
||||
{app.name.substring(0, 2).toUpperCase()}
|
||||
</Avatar>
|
||||
</AppLogo>
|
||||
)}
|
||||
{renderStatusTag()}
|
||||
</CardCoverContainer>
|
||||
}
|
||||
actions={[
|
||||
// Show different actions based on app status
|
||||
app.isInstalled ? (
|
||||
app.isRunning ? (
|
||||
<Tooltip title={t('nodeapp.stop')}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={loading && actionType === 'stop' ? <LoadingOutlined /> : <StopOutlined />}
|
||||
onClick={handleStop}
|
||||
loading={loading && actionType === 'stop'}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title={t('nodeapp.start')}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={loading && actionType === 'start' ? <LoadingOutlined /> : <PlayCircleOutlined />}
|
||||
onClick={handleStart}
|
||||
loading={loading && actionType === 'start'}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
) : (
|
||||
<Tooltip title={t('nodeapp.install')}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={loading && actionType === 'install' ? <LoadingOutlined /> : <DownloadOutlined />}
|
||||
onClick={handleInstall}
|
||||
loading={loading && actionType === 'install'}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Tooltip>
|
||||
),
|
||||
app.isInstalled && (
|
||||
<Tooltip title={t('nodeapp.update')}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={loading && actionType === 'update' ? <LoadingOutlined /> : <ReloadOutlined />}
|
||||
onClick={handleUpdate}
|
||||
loading={loading && actionType === 'update'}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Tooltip>
|
||||
),
|
||||
app.repositoryUrl && (
|
||||
<Tooltip title={t('nodeapp.viewRepository')}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<GithubOutlined />}
|
||||
onClick={openRepository}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
].filter(Boolean)}
|
||||
>
|
||||
<Card.Meta
|
||||
title={<Title level={5}>{app.name}</Title>}
|
||||
description={
|
||||
<div style={{ minHeight: '100px' }}>
|
||||
<Paragraph ellipsis={{ rows: 3 }}>{app.description}</Paragraph>
|
||||
|
||||
{app.author && (
|
||||
<Space style={{ marginTop: '8px' }}>
|
||||
<Text type="secondary">{t('nodeapp.author')}:</Text>
|
||||
<Text>{app.author}</Text>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
{app.version && (
|
||||
<Space style={{ marginTop: '4px' }}>
|
||||
<Text type="secondary">{t('nodeapp.version')}:</Text>
|
||||
<Text>{app.version}</Text>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
{app.isInstalled && (
|
||||
<div style={{ marginTop: '12px' }}>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'uninstall',
|
||||
danger: true,
|
||||
label: t('nodeapp.uninstall'),
|
||||
onClick: handleUninstall
|
||||
}
|
||||
]
|
||||
}}
|
||||
trigger={['click']}
|
||||
>
|
||||
<Button type="link" size="small" danger>
|
||||
{t('nodeapp.more')}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const CardCoverContainer = styled.div`
|
||||
position: relative;
|
||||
min-height: 140px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f0f2f5;
|
||||
|
||||
.ant-tag {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
`
|
||||
|
||||
const AppLogo = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
`
|
||||
|
||||
export default NodeApp
|
||||
118
src/renderer/src/pages/nodeapps/NodeAppForm.tsx
Normal file
118
src/renderer/src/pages/nodeapps/NodeAppForm.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { NodeAppType } from '@renderer/types'
|
||||
import { Button, Form, Input, Space } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
onSubmit: (values: NodeAppType) => void
|
||||
onCancel: () => void
|
||||
loading: boolean
|
||||
initialValues?: Partial<NodeAppType>
|
||||
}
|
||||
|
||||
const NodeAppForm: FC<Props> = ({ onSubmit, onCancel, loading, initialValues }) => {
|
||||
const { t } = useTranslation()
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const handleSubmit = (values: any) => {
|
||||
onSubmit({
|
||||
...values,
|
||||
type: 'node',
|
||||
isInstalled: false,
|
||||
isRunning: false
|
||||
} as NodeAppType)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
initialValues={initialValues}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('nodeapp.form.name')}
|
||||
rules={[{ required: true, message: t('nodeapp.form.nameRequired') }]}
|
||||
>
|
||||
<Input placeholder={t('nodeapp.form.namePlaceholder')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="repositoryUrl"
|
||||
label={t('nodeapp.form.repositoryUrl')}
|
||||
rules={[
|
||||
{ required: true, message: t('nodeapp.form.repositoryUrlRequired') },
|
||||
{
|
||||
pattern: /^https?:\/\/github\.com\/[\w-]+\/[\w.-]+\/?$/,
|
||||
message: t('nodeapp.form.repositoryUrlInvalid')
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('nodeapp.form.repositoryUrlPlaceholder')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label={t('nodeapp.form.description')}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
placeholder={t('nodeapp.form.descriptionPlaceholder')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="author"
|
||||
label={t('nodeapp.form.author')}
|
||||
>
|
||||
<Input placeholder={t('nodeapp.form.authorPlaceholder')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="homepage"
|
||||
label={t('nodeapp.form.homepage')}
|
||||
>
|
||||
<Input placeholder={t('nodeapp.form.homepagePlaceholder')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="installCommand"
|
||||
label={t('nodeapp.form.installCommand')}
|
||||
help={t('nodeapp.form.installCommandHelp')}
|
||||
>
|
||||
<Input placeholder="npm install" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="startCommand"
|
||||
label={t('nodeapp.form.startCommand')}
|
||||
help={t('nodeapp.form.startCommandHelp')}
|
||||
>
|
||||
<Input placeholder="npm start" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="port"
|
||||
label={t('nodeapp.form.port')}
|
||||
help={t('nodeapp.form.portHelp')}
|
||||
>
|
||||
<Input placeholder="3000" type="number" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Space style={{ float: 'right' }}>
|
||||
<Button onClick={onCancel}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
{t('common.submit')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
export default NodeAppForm
|
||||
171
src/renderer/src/pages/nodeapps/NodeAppsPage.tsx
Normal file
171
src/renderer/src/pages/nodeapps/NodeAppsPage.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import { PlusOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { Center } from '@renderer/components/Layout'
|
||||
import { useNodeApps } from '@renderer/hooks/useNodeApps'
|
||||
import { NodeAppType } from '@renderer/types'
|
||||
import { Button, Col, Empty, Input, Modal, Row, Spin, Tabs, Typography, notification } from 'antd'
|
||||
import { isEmpty } from 'lodash'
|
||||
import React, { FC, useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import NodeApp from './NodeApp'
|
||||
import NodeAppForm from './NodeAppForm'
|
||||
import PackageDeployer from './PackageDeployer'
|
||||
|
||||
const { Title } = Typography
|
||||
const { TabPane } = Tabs
|
||||
|
||||
const NodeAppsPage: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [search, setSearch] = useState('')
|
||||
const { apps, loading, addApp, refresh } = useNodeApps()
|
||||
const [isModalVisible, setIsModalVisible] = useState(false)
|
||||
const [formLoading, setFormLoading] = useState(false)
|
||||
|
||||
// Filter apps based on search
|
||||
const filteredApps = search
|
||||
? apps.filter(
|
||||
(app) =>
|
||||
app.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
app.description?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
app.author?.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
: apps
|
||||
|
||||
// Handle adding a new app
|
||||
const handleAddApp = useCallback(async (values: NodeAppType) => {
|
||||
try {
|
||||
setFormLoading(true)
|
||||
await addApp({
|
||||
...values,
|
||||
type: 'node'
|
||||
})
|
||||
setIsModalVisible(false)
|
||||
notification.success({
|
||||
message: t('common.success'),
|
||||
description: t('nodeapp.addSuccess', { name: values.name })
|
||||
})
|
||||
} catch (err) {
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: err instanceof Error ? err.message : String(err)
|
||||
})
|
||||
} finally {
|
||||
setFormLoading(false)
|
||||
}
|
||||
}, [addApp, t])
|
||||
|
||||
// Disable right-click menu in blank area
|
||||
const handleContextMenu = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
// Handle successful package deployment
|
||||
const handleDeployed = useCallback(() => {
|
||||
refresh()
|
||||
}, [refresh])
|
||||
|
||||
return (
|
||||
<Container onContextMenu={handleContextMenu}>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
|
||||
{t('nodeapp.title')}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
||||
<Input
|
||||
placeholder={t('common.search')}
|
||||
className="nodrag"
|
||||
style={{ width: '250px', height: 28 }}
|
||||
size="small"
|
||||
variant="filled"
|
||||
suffix={<SearchOutlined />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setIsModalVisible(true)}
|
||||
>
|
||||
{t('nodeapp.add')}
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ width: 80 }} />
|
||||
</NavbarCenter>
|
||||
</Navbar>
|
||||
|
||||
<ContentContainer id="content-container">
|
||||
<Tabs defaultActiveKey="apps" style={{ height: '100%' }}>
|
||||
<TabPane tab={t('nodeapp.marketplaceTab')} key="apps">
|
||||
{loading ? (
|
||||
<Center>
|
||||
<Spin size="large" />
|
||||
</Center>
|
||||
) : isEmpty(filteredApps) ? (
|
||||
<Center>
|
||||
<Empty
|
||||
description={
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<p>{t('nodeapp.empty')}</p>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setIsModalVisible(true)}
|
||||
>
|
||||
{t('nodeapp.add')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Center>
|
||||
) : (
|
||||
<div style={{ padding: '16px' }}>
|
||||
<Title level={5} style={{ marginBottom: '16px' }}>{t('nodeapp.featured')}</Title>
|
||||
<Row gutter={[16, 16]}>
|
||||
{filteredApps.map((app) => (
|
||||
<Col key={app.id} xs={24} sm={12} md={8} lg={6}>
|
||||
<NodeApp app={app} />
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
</TabPane>
|
||||
<TabPane tab={t('nodeapp.packageDeployerTab')} key="packageDeployer">
|
||||
<PackageDeployer onDeployed={handleDeployed} />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</ContentContainer>
|
||||
|
||||
<Modal
|
||||
title={t('nodeapp.addNew')}
|
||||
open={isModalVisible}
|
||||
onCancel={() => setIsModalVisible(false)}
|
||||
footer={null}
|
||||
width={600}
|
||||
>
|
||||
<NodeAppForm
|
||||
onSubmit={handleAddApp}
|
||||
onCancel={() => setIsModalVisible(false)}
|
||||
loading={formLoading}
|
||||
/>
|
||||
</Modal>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding-bottom: 20px;
|
||||
`
|
||||
|
||||
export default NodeAppsPage
|
||||
198
src/renderer/src/pages/nodeapps/NodeSettings.tsx
Normal file
198
src/renderer/src/pages/nodeapps/NodeSettings.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import React, { FC, useEffect, useState } from 'react'
|
||||
import { Button, Card, Form, Input, Select, Typography, notification, Space } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
import { LoadingOutlined, SyncOutlined } from '@ant-design/icons'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
const { Option } = Select
|
||||
|
||||
const NodeSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [form] = Form.useForm()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [checkingVersion, setCheckingVersion] = useState(false)
|
||||
const [nodeInstalled, setNodeInstalled] = useState(false)
|
||||
const [currentVersion, setCurrentVersion] = useState('')
|
||||
|
||||
// 默认提供的Node.js版本选项
|
||||
const nodeVersions = [
|
||||
{ value: '20.11.1', label: 'v20.11.1 (LTS)' },
|
||||
{ value: '18.18.0', label: 'v18.18.0 (LTS)' },
|
||||
{ value: '16.20.2', label: 'v16.20.2 (LTS)' },
|
||||
{ value: '14.21.3', label: 'v14.21.3 (LTS)' }
|
||||
]
|
||||
|
||||
// 检查Node.js是否已安装
|
||||
const checkNodeStatus = async () => {
|
||||
try {
|
||||
setCheckingVersion(true)
|
||||
const isNodeInstalled = await window.api.nodeapp.checkNode()
|
||||
setNodeInstalled(isNodeInstalled)
|
||||
|
||||
if (isNodeInstalled) {
|
||||
try {
|
||||
// 获取当前安装的Node.js版本
|
||||
// 使用ipc调用获取Node.js版本
|
||||
const versionFromConfig = await window.api.config.get('NODE_VERSION')
|
||||
if (versionFromConfig) {
|
||||
setCurrentVersion(versionFromConfig)
|
||||
} else {
|
||||
setCurrentVersion('Unknown')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting Node.js version:', error)
|
||||
setCurrentVersion('Unknown')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking Node.js status:', error)
|
||||
} finally {
|
||||
setCheckingVersion(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 组件加载时检查状态
|
||||
useEffect(() => {
|
||||
checkNodeStatus()
|
||||
}, [])
|
||||
|
||||
// 安装Node.js
|
||||
const handleInstall = async (values: any) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
// 设置环境变量来指定要安装的Node.js版本
|
||||
if (values.nodeVersion) {
|
||||
await window.api.config.set('NODE_VERSION', values.nodeVersion)
|
||||
}
|
||||
|
||||
const success = await window.api.nodeapp.installNode()
|
||||
|
||||
if (success) {
|
||||
notification.success({
|
||||
message: t('common.success'),
|
||||
description: t('nodeapp.nodeSettings.installSuccess', {
|
||||
version: values.nodeVersion
|
||||
})
|
||||
})
|
||||
|
||||
// 重新检查状态
|
||||
await checkNodeStatus()
|
||||
} else {
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: t('nodeapp.nodeSettings.installFailed')
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error installing Node.js:', error)
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Card title={<Title level={4}>{t('nodeapp.nodeSettings.title')}</Title>}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>{t('nodeapp.nodeSettings.description')}</Text>
|
||||
|
||||
<StatusSection>
|
||||
<Text strong>{t('nodeapp.nodeSettings.status')}: </Text>
|
||||
{checkingVersion ? (
|
||||
<Text type="secondary">
|
||||
<LoadingOutlined style={{ marginRight: 8 }} />
|
||||
{t('nodeapp.nodeSettings.checking')}
|
||||
</Text>
|
||||
) : nodeInstalled ? (
|
||||
<Text type="success">
|
||||
{t('nodeapp.nodeSettings.installed')}
|
||||
{currentVersion && ` (${currentVersion})`}
|
||||
</Text>
|
||||
) : (
|
||||
<Text type="warning">{t('nodeapp.nodeSettings.notInstalled')}</Text>
|
||||
)}
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<SyncOutlined />}
|
||||
onClick={checkNodeStatus}
|
||||
loading={checkingVersion}
|
||||
>
|
||||
{t('nodeapp.nodeSettings.refresh')}
|
||||
</Button>
|
||||
</StatusSection>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleInstall}
|
||||
initialValues={{
|
||||
nodeVersion: '18.18.0'
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="nodeVersion"
|
||||
label={t('nodeapp.nodeSettings.version')}
|
||||
help={t('nodeapp.nodeSettings.versionHelp')}
|
||||
>
|
||||
<Select>
|
||||
{nodeVersions.map((version) => (
|
||||
<Option key={version.value} value={version.value}>
|
||||
{version.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="customVersion"
|
||||
label={t('nodeapp.nodeSettings.customVersion')}
|
||||
help={t('nodeapp.nodeSettings.customVersionHelp')}
|
||||
>
|
||||
<Input
|
||||
placeholder="20.12.1"
|
||||
onChange={(e) => {
|
||||
if (e.target.value) {
|
||||
form.setFieldsValue({ nodeVersion: e.target.value })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
>
|
||||
{nodeInstalled ? t('nodeapp.nodeSettings.reinstall') : t('nodeapp.nodeSettings.install')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Space>
|
||||
</Card>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
margin: 16px;
|
||||
`
|
||||
|
||||
const StatusSection = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 16px 0;
|
||||
padding: 12px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
`
|
||||
|
||||
export default NodeSettings
|
||||
580
src/renderer/src/pages/nodeapps/PackageDeployer.tsx
Normal file
580
src/renderer/src/pages/nodeapps/PackageDeployer.tsx
Normal file
@@ -0,0 +1,580 @@
|
||||
import { CloudUploadOutlined, GithubOutlined, LoadingOutlined, SettingOutlined, InfoCircleOutlined, QuestionCircleOutlined } from '@ant-design/icons'
|
||||
import { FileType } from '@renderer/types'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Collapse,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Space,
|
||||
Spin,
|
||||
Tabs,
|
||||
Typography,
|
||||
Upload,
|
||||
notification,
|
||||
Popover,
|
||||
Select,
|
||||
Checkbox
|
||||
} from 'antd'
|
||||
import React, { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
const { Panel } = Collapse
|
||||
const { TabPane } = Tabs
|
||||
const { Option } = Select
|
||||
|
||||
interface Props {
|
||||
onDeployed?: (result: { port: number; url: string }) => void
|
||||
}
|
||||
|
||||
const PackageDeployer: FC<Props> = ({ onDeployed }) => {
|
||||
const { t } = useTranslation()
|
||||
const [form] = Form.useForm()
|
||||
const [gitForm] = Form.useForm()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [gitLoading, setGitLoading] = useState(false)
|
||||
const [file, setFile] = useState<FileType | null>(null)
|
||||
const [advancedVisible, setAdvancedVisible] = useState(false)
|
||||
const [gitAdvancedVisible, setGitAdvancedVisible] = useState(false)
|
||||
const [isNodeAvailable, setIsNodeAvailable] = useState<boolean | null>(null)
|
||||
const [isInstallingNode, setIsInstallingNode] = useState(false)
|
||||
const [uploadUrl, setUploadUrl] = useState('')
|
||||
const [activeTab, setActiveTab] = useState('zip')
|
||||
|
||||
// Check if Node.js is available
|
||||
useEffect(() => {
|
||||
const checkNodeAvailability = async () => {
|
||||
try {
|
||||
const isAvailable = await window.api.nodeapp.checkNode()
|
||||
setIsNodeAvailable(isAvailable)
|
||||
} catch (error) {
|
||||
console.error('Error checking Node.js availability:', error)
|
||||
setIsNodeAvailable(false)
|
||||
}
|
||||
}
|
||||
|
||||
checkNodeAvailability()
|
||||
}, [])
|
||||
|
||||
// Handle Node.js installation
|
||||
const handleInstallNode = async () => {
|
||||
try {
|
||||
setIsInstallingNode(true)
|
||||
const success = await window.api.nodeapp.installNode()
|
||||
|
||||
if (success) {
|
||||
setIsNodeAvailable(true)
|
||||
notification.success({
|
||||
message: t('common.success'),
|
||||
description: t('nodeapp.packageDeployer.nodeInstallSuccess')
|
||||
})
|
||||
} else {
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: t('nodeapp.packageDeployer.nodeInstallFailed')
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error installing Node.js:', error)
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
} finally {
|
||||
setIsInstallingNode(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileSelect = async () => {
|
||||
try {
|
||||
const files = await window.api.file.select({
|
||||
filters: [
|
||||
{ name: 'ZIP Files', extensions: ['zip'] }
|
||||
],
|
||||
properties: ['openFile']
|
||||
})
|
||||
|
||||
if (files && files.length > 0) {
|
||||
setFile(files[0])
|
||||
form.setFieldsValue({
|
||||
name: files[0].name.replace(/\.zip$/, ''),
|
||||
file: files[0]
|
||||
})
|
||||
setUploadUrl(files[0].path)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error selecting file:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeploy = async (values: any) => {
|
||||
// First check if Node.js is available
|
||||
if (isNodeAvailable === false) {
|
||||
Modal.confirm({
|
||||
title: t('nodeapp.packageDeployer.nodeRequired'),
|
||||
content: t('nodeapp.packageDeployer.installNodePrompt'),
|
||||
okText: t('nodeapp.packageDeployer.installNode'),
|
||||
cancelText: t('common.cancel'),
|
||||
onOk: handleInstallNode
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!file) {
|
||||
notification.warning({
|
||||
message: t('common.warning'),
|
||||
description: t('nodeapp.packageDeployer.noFileSelected')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
// 检测是否为Next.js应用,如果文件名包含next或文件是从next.js项目导出的
|
||||
const isNextJs = file.name.toLowerCase().includes('next') ||
|
||||
(values.isNextJs === true);
|
||||
|
||||
// 如果是Next.js应用,自动设置构建步骤
|
||||
if (isNextJs && !values.buildCommand) {
|
||||
values.buildCommand = 'npm run build';
|
||||
notification.info({
|
||||
message: t('nodeapp.packageDeployer.nextJsDetected'),
|
||||
description: t('nodeapp.packageDeployer.buildStepAdded'),
|
||||
duration: 5
|
||||
});
|
||||
}
|
||||
|
||||
// Display note about ES modules compatibility
|
||||
if (file.name.includes('react') || file.name.includes('next') || file.name.includes('vue')) {
|
||||
notification.info({
|
||||
message: t('nodeapp.packageDeployer.moduleTypeError'),
|
||||
description: t('nodeapp.packageDeployer.esModuleError'),
|
||||
duration: 8
|
||||
})
|
||||
}
|
||||
|
||||
// Deploy the ZIP package
|
||||
const result = await window.api.nodeapp.deployZip(file.path, {
|
||||
name: values.name,
|
||||
port: values.port ? parseInt(values.port) : undefined,
|
||||
startCommand: values.startCommand,
|
||||
installCommand: values.installCommand,
|
||||
buildCommand: values.buildCommand
|
||||
})
|
||||
|
||||
if (result) {
|
||||
notification.success({
|
||||
message: t('common.success'),
|
||||
description: t('nodeapp.packageDeployer.deploySuccess', {
|
||||
name: values.name,
|
||||
port: result.port
|
||||
}),
|
||||
btn: (
|
||||
<Button type="primary" size="small" onClick={() => window.api.openWebsite(result.url)}>
|
||||
{t('nodeapp.packageDeployer.open')}
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
|
||||
// Reset form and state
|
||||
form.resetFields()
|
||||
setFile(null)
|
||||
setAdvancedVisible(false)
|
||||
setUploadUrl('')
|
||||
|
||||
// Notify parent
|
||||
if (onDeployed) {
|
||||
onDeployed(result)
|
||||
}
|
||||
} else {
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: t('nodeapp.packageDeployer.deployFailed')
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deploying ZIP:', error)
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeployGit = async (values: any) => {
|
||||
// First check if Node.js is available
|
||||
if (isNodeAvailable === false) {
|
||||
Modal.confirm({
|
||||
title: t('nodeapp.packageDeployer.nodeRequired'),
|
||||
content: t('nodeapp.packageDeployer.installNodePrompt'),
|
||||
okText: t('nodeapp.packageDeployer.installNode'),
|
||||
cancelText: t('common.cancel'),
|
||||
onOk: handleInstallNode
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!values.repoUrl) {
|
||||
notification.warning({
|
||||
message: t('common.warning'),
|
||||
description: t('nodeapp.packageDeployer.noRepoUrlProvided')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setGitLoading(true)
|
||||
|
||||
// 检测是否为Next.js应用
|
||||
const isNextJs = values.repoUrl.toLowerCase().includes('next') ||
|
||||
(values.isNextJs === true);
|
||||
|
||||
// 如果是Next.js应用,自动设置构建步骤
|
||||
if (isNextJs && !values.buildCommand) {
|
||||
values.buildCommand = 'npm run build';
|
||||
notification.info({
|
||||
message: t('nodeapp.packageDeployer.nextJsDetected'),
|
||||
description: t('nodeapp.packageDeployer.buildStepAdded'),
|
||||
duration: 5
|
||||
});
|
||||
}
|
||||
|
||||
// Deploy from Git repository
|
||||
const result = await window.api.nodeapp.deployGit(values.repoUrl, {
|
||||
name: values.name,
|
||||
port: values.port ? parseInt(values.port) : undefined,
|
||||
startCommand: values.startCommand,
|
||||
installCommand: values.installCommand,
|
||||
buildCommand: values.buildCommand
|
||||
})
|
||||
|
||||
if (result) {
|
||||
notification.success({
|
||||
message: t('common.success'),
|
||||
description: t('nodeapp.packageDeployer.deploySuccess', {
|
||||
name: values.name || 'Git App',
|
||||
port: result.port
|
||||
}),
|
||||
btn: (
|
||||
<Button type="primary" size="small" onClick={() => window.api.openWebsite(result.url)}>
|
||||
{t('nodeapp.packageDeployer.open')}
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
|
||||
// Reset form and state
|
||||
gitForm.resetFields()
|
||||
setGitAdvancedVisible(false)
|
||||
|
||||
// Notify parent
|
||||
if (onDeployed) {
|
||||
onDeployed(result)
|
||||
}
|
||||
} else {
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: t('nodeapp.packageDeployer.deployFailed')
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deploying from Git:', error)
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
} finally {
|
||||
setGitLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Card title={t('nodeapp.packageDeployer.deployPackage')}>
|
||||
{isNodeAvailable === false && (
|
||||
<Alert
|
||||
type="warning"
|
||||
message={t('nodeapp.packageDeployer.nodeNotAvailable')}
|
||||
description={
|
||||
<Space>
|
||||
<Text>{t('nodeapp.packageDeployer.nodeNeeded')}</Text>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleInstallNode}
|
||||
loading={isInstallingNode}
|
||||
>
|
||||
{t('nodeapp.packageDeployer.installNode')}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||||
<TabPane
|
||||
tab={
|
||||
<span>
|
||||
<CloudUploadOutlined /> {t('nodeapp.packageDeployer.deployFromZip')}
|
||||
</span>
|
||||
}
|
||||
key="zip"
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleDeploy}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>{t('nodeapp.packageDeployer.description')}</Text>
|
||||
|
||||
<UploadContainer>
|
||||
{file ? (
|
||||
<FileInfo>
|
||||
<div>
|
||||
<CloudUploadOutlined style={{ fontSize: 24, marginRight: 8 }} />
|
||||
<Text strong>{file.name}</Text>
|
||||
</div>
|
||||
<Button size="small" onClick={() => {
|
||||
setFile(null);
|
||||
form.setFieldsValue({ file: null });
|
||||
setUploadUrl('');
|
||||
}}>
|
||||
{t('common.remove')}
|
||||
</Button>
|
||||
</FileInfo>
|
||||
) : (
|
||||
<UploadButton onClick={handleFileSelect}>
|
||||
<CloudUploadOutlined style={{ fontSize: 24, marginBottom: 8 }} />
|
||||
<div>{t('nodeapp.packageDeployer.selectZip')}</div>
|
||||
</UploadButton>
|
||||
)}
|
||||
</UploadContainer>
|
||||
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('nodeapp.form.name')}
|
||||
rules={[{ required: true, message: t('nodeapp.form.nameRequired') }]}
|
||||
>
|
||||
<Input placeholder={t('nodeapp.packageDeployer.namePlaceholder')} />
|
||||
</Form.Item>
|
||||
|
||||
<Collapse
|
||||
ghost
|
||||
activeKey={advancedVisible ? ['1'] : []}
|
||||
onChange={() => setAdvancedVisible(!advancedVisible)}
|
||||
>
|
||||
<Panel
|
||||
header={
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<SettingOutlined style={{ marginRight: 8 }} />
|
||||
{t('nodeapp.packageDeployer.advancedOptions')}
|
||||
</div>
|
||||
}
|
||||
key="1"
|
||||
>
|
||||
<Form.Item
|
||||
name="port"
|
||||
label={t('nodeapp.form.port')}
|
||||
help={t('nodeapp.form.portHelp')}
|
||||
>
|
||||
<Input placeholder="3000" type="number" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="installCommand"
|
||||
label={t('nodeapp.form.installCommand')}
|
||||
help={t('nodeapp.form.installCommandHelp')}
|
||||
>
|
||||
<Input placeholder="npm install" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="buildCommand"
|
||||
label={t('nodeapp.form.buildCommand')}
|
||||
help={t('nodeapp.form.buildCommandHelp')}
|
||||
>
|
||||
<Input placeholder="npm run build" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="startCommand"
|
||||
label={t('nodeapp.form.startCommand')}
|
||||
help={t('nodeapp.form.startCommandHelp')}
|
||||
>
|
||||
<Input placeholder="npm start" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="isNextJs"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
form.setFieldsValue({
|
||||
buildCommand: 'npm run build',
|
||||
startCommand: 'npm run start',
|
||||
installCommand: 'npm install --legacy-peer-deps'
|
||||
});
|
||||
}
|
||||
}}>{t('nodeapp.form.isNextJs')}</Checkbox>
|
||||
</Form.Item>
|
||||
|
||||
<Alert
|
||||
message={t('nodeapp.packageDeployer.nextJsInfo')}
|
||||
description={t('nodeapp.packageDeployer.nextJsDescription')}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: '16px' }}
|
||||
/>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
disabled={!file}
|
||||
loading={loading}
|
||||
icon={loading ? <Spin size="small" /> : <CloudUploadOutlined />}
|
||||
>
|
||||
{t('nodeapp.packageDeployer.deploy')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form>
|
||||
</TabPane>
|
||||
|
||||
<TabPane
|
||||
tab={
|
||||
<span>
|
||||
<GithubOutlined /> {t('nodeapp.packageDeployer.deployFromGit')}
|
||||
</span>
|
||||
}
|
||||
key="git"
|
||||
>
|
||||
<Form form={gitForm} layout="vertical" onFinish={handleDeployGit}>
|
||||
<Form.Item
|
||||
name="repoUrl"
|
||||
label={t('nodeapp.packageDeployer.repoUrl')}
|
||||
rules={[{ required: true, message: t('nodeapp.packageDeployer.repoUrlRequired') }]}
|
||||
>
|
||||
<Input placeholder="https://github.com/username/repo" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="name" label={t('nodeapp.form.name')}>
|
||||
<Input placeholder={t('nodeapp.packageDeployer.namePlaceholder')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="port"
|
||||
label={
|
||||
<span>
|
||||
{t('nodeapp.form.port')}
|
||||
<Popover
|
||||
content={t('nodeapp.form.portHelp')}
|
||||
title={t('common.tips')}
|
||||
>
|
||||
<QuestionCircleOutlined style={{ marginLeft: 8 }} />
|
||||
</Popover>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Input placeholder="3000" type="number" />
|
||||
</Form.Item>
|
||||
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => setGitAdvancedVisible(!gitAdvancedVisible)}
|
||||
style={{ paddingLeft: 0, marginBottom: 16 }}
|
||||
>
|
||||
{gitAdvancedVisible
|
||||
? t('nodeapp.packageDeployer.hideAdvanced')
|
||||
: t('nodeapp.packageDeployer.showAdvanced')}
|
||||
</Button>
|
||||
|
||||
{gitAdvancedVisible && (
|
||||
<Collapse ghost>
|
||||
<Panel header={t('nodeapp.packageDeployer.advancedOptions')} key="1">
|
||||
<Form.Item
|
||||
name="installCommand"
|
||||
label={t('nodeapp.form.installCommand')}
|
||||
help={t('nodeapp.form.installCommandHelp')}
|
||||
>
|
||||
<Input placeholder="npm install" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="buildCommand" label={t('nodeapp.form.buildCommand')}
|
||||
help={t('nodeapp.form.buildCommandHelp')}>
|
||||
<Input placeholder="npm run build" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="startCommand" label={t('nodeapp.form.startCommand')}
|
||||
help={t('nodeapp.form.startCommandHelp')}>
|
||||
<Input placeholder="npm start" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="isNextJs" valuePropName="checked" style={{ marginBottom: 0 }}>
|
||||
<Checkbox onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
gitForm.setFieldsValue({
|
||||
buildCommand: 'npm run build',
|
||||
startCommand: 'npm run start',
|
||||
installCommand: 'npm install --legacy-peer-deps'
|
||||
});
|
||||
}
|
||||
}}>{t('nodeapp.form.isNextJs')}</Checkbox>
|
||||
</Form.Item>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
)}
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={gitLoading}>
|
||||
{t('nodeapp.packageDeployer.deploy')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
margin: 16px;
|
||||
`
|
||||
|
||||
const UploadContainer = styled.div`
|
||||
margin-bottom: 16px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 4px;
|
||||
background-color: #fafafa;
|
||||
transition: border-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
}
|
||||
`
|
||||
|
||||
const UploadButton = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100px;
|
||||
cursor: pointer;
|
||||
padding: 16px;
|
||||
`
|
||||
|
||||
const FileInfo = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
`
|
||||
|
||||
export default PackageDeployer
|
||||
39
src/renderer/src/pages/nodeapps/index.tsx
Normal file
39
src/renderer/src/pages/nodeapps/index.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Tabs } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
import AppsManager from './AppsManager'
|
||||
import PackageDeployer from './PackageDeployer'
|
||||
import NodeSettings from './NodeSettings'
|
||||
|
||||
const NodeAppsPage: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [activeTab, setActiveTab] = useState('apps')
|
||||
|
||||
const handleTabChange = (key: string) => {
|
||||
setActiveTab(key)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Tabs activeKey={activeTab} onChange={handleTabChange}>
|
||||
<Tabs.TabPane tab={t('nodeapp.appsManagerTab')} key="apps">
|
||||
<AppsManager />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={t('nodeapp.packageDeployerTab')} key="deploy">
|
||||
<PackageDeployer />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={t('nodeapp.nodeSettingsTab')} key="settings">
|
||||
<NodeSettings />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
export default NodeAppsPage
|
||||
@@ -13,6 +13,7 @@ import mcp from './mcp'
|
||||
import messagesReducer from './messages'
|
||||
import migrate from './migrate'
|
||||
import minapps from './minapps'
|
||||
import nodeapps from './nodeapps'
|
||||
import paintings from './paintings'
|
||||
import runtime from './runtime'
|
||||
import settings from './settings'
|
||||
@@ -33,6 +34,7 @@ const rootReducer = combineReducers({
|
||||
websearch,
|
||||
mcp,
|
||||
copilot,
|
||||
nodeapps,
|
||||
messages: messagesReducer
|
||||
})
|
||||
|
||||
|
||||
71
src/renderer/src/store/nodeapps.ts
Normal file
71
src/renderer/src/store/nodeapps.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
import { NodeAppType } from '@renderer/types'
|
||||
|
||||
// Define featured Node.js apps - you can add more as needed
|
||||
export const FEATURED_NODE_APPS: NodeAppType[] = [
|
||||
{
|
||||
id: 'writing-helper',
|
||||
name: 'Writing Helper',
|
||||
url: 'http://localhost:3000',
|
||||
type: 'node',
|
||||
repositoryUrl: 'https://github.com/GeekyWizKid/writing-helper',
|
||||
description: 'AI writing assistant supporting multiple LLM APIs with rich style customization features.',
|
||||
author: 'GeekyWizKid',
|
||||
homepage: 'https://github.com/GeekyWizKid/writing-helper',
|
||||
installCommand: 'npm install',
|
||||
startCommand: 'npm run dev',
|
||||
isInstalled: false,
|
||||
isRunning: false
|
||||
}
|
||||
]
|
||||
|
||||
export interface NodeAppsState {
|
||||
apps: NodeAppType[]
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
const initialState: NodeAppsState = {
|
||||
apps: [...FEATURED_NODE_APPS],
|
||||
loading: false,
|
||||
error: null
|
||||
}
|
||||
|
||||
const nodeAppsSlice = createSlice({
|
||||
name: 'nodeApps',
|
||||
initialState,
|
||||
reducers: {
|
||||
setNodeApps: (state, action: PayloadAction<NodeAppType[]>) => {
|
||||
state.apps = action.payload
|
||||
},
|
||||
addNodeApp: (state, action: PayloadAction<NodeAppType>) => {
|
||||
state.apps.push(action.payload)
|
||||
},
|
||||
updateNodeApp: (state, action: PayloadAction<NodeAppType>) => {
|
||||
const index = state.apps.findIndex(app => app.id === action.payload.id)
|
||||
if (index !== -1) {
|
||||
state.apps[index] = action.payload
|
||||
}
|
||||
},
|
||||
removeNodeApp: (state, action: PayloadAction<string>) => {
|
||||
state.apps = state.apps.filter(app => app.id !== action.payload)
|
||||
},
|
||||
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.loading = action.payload
|
||||
},
|
||||
setError: (state, action: PayloadAction<string | null>) => {
|
||||
state.error = action.payload
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const {
|
||||
setNodeApps,
|
||||
addNodeApp,
|
||||
updateNodeApp,
|
||||
removeNodeApp,
|
||||
setLoading,
|
||||
setError
|
||||
} = nodeAppsSlice.actions
|
||||
|
||||
export default nodeAppsSlice.reducer
|
||||
@@ -4,7 +4,7 @@ import { CodeStyleVarious, LanguageVarious, ThemeMode, TranslateLanguageVarious
|
||||
|
||||
export type SendMessageShortcut = 'Enter' | 'Shift+Enter' | 'Ctrl+Enter' | 'Command+Enter'
|
||||
|
||||
export type SidebarIcon = 'assistants' | 'agents' | 'paintings' | 'translate' | 'minapp' | 'knowledge' | 'files'
|
||||
export type SidebarIcon = 'assistants' | 'agents' | 'paintings' | 'translate' | 'minapp' | 'nodeapps' | 'knowledge' | 'files'
|
||||
|
||||
export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [
|
||||
'assistants',
|
||||
@@ -12,6 +12,7 @@ export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [
|
||||
'paintings',
|
||||
'translate',
|
||||
'minapp',
|
||||
'nodeapps',
|
||||
'knowledge',
|
||||
'files'
|
||||
]
|
||||
|
||||
@@ -164,6 +164,21 @@ export type MinAppType = {
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
export type NodeAppType = MinAppType & {
|
||||
type: 'node'
|
||||
repositoryUrl?: string
|
||||
version?: string
|
||||
description?: string
|
||||
author?: string
|
||||
homepage?: string
|
||||
installCommand?: string
|
||||
buildCommand?: string
|
||||
startCommand?: string
|
||||
port?: number
|
||||
isInstalled?: boolean
|
||||
isRunning?: boolean
|
||||
}
|
||||
|
||||
export interface FileType {
|
||||
id: string
|
||||
name: string
|
||||
|
||||
Reference in New Issue
Block a user