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 { decrypt, encrypt } from './utils/aes'
|
||||||
import { getFilesDir } from './utils/file'
|
import { getFilesDir } from './utils/file'
|
||||||
import { compress, decompress } from './utils/zip'
|
import { compress, decompress } from './utils/zip'
|
||||||
|
import NodeAppService from './services/NodeAppService'
|
||||||
|
|
||||||
const fileManager = new FileStorage()
|
const fileManager = new FileStorage()
|
||||||
const backupManager = new BackupManager()
|
const backupManager = new BackupManager()
|
||||||
@@ -257,6 +258,17 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
|
|
||||||
ipcMain.handle('mcp:cleanup', async () => mcpService.cleanup())
|
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:is-binary-exist', (_, name: string) => isBinaryExists(name))
|
||||||
ipcMain.handle('app:get-binary-path', (_, name: string) => getBinaryPath(name))
|
ipcMain.handle('app:get-binary-path', (_, name: string) => getBinaryPath(name))
|
||||||
ipcMain.handle('app:install-uv-binary', () => runInstallScript('install-uv.js'))
|
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:get-token', CopilotService.getToken)
|
||||||
ipcMain.handle('copilot:logout', CopilotService.logout)
|
ipcMain.handle('copilot:logout', CopilotService.logout)
|
||||||
ipcMain.handle('copilot:get-user', CopilotService.getUser)
|
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 '.'
|
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) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
const installScriptPath = path.join(getResourcePath(), 'scripts', scriptPath)
|
const installScriptPath = path.join(getResourcePath(), 'scripts', scriptPath)
|
||||||
log.info(`Running script at: ${installScriptPath}`)
|
log.info(`Running script at: ${installScriptPath}`)
|
||||||
|
|
||||||
const nodeProcess = spawn(process.execPath, [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) => {
|
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>
|
logout: () => Promise<void>
|
||||||
getUser: (token: string) => Promise<{ login: string; avatar: string }>
|
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>
|
isBinaryExist: (name: string) => Promise<boolean>
|
||||||
getBinaryPath: (name: string) => Promise<string>
|
getBinaryPath: (name: string) => Promise<string>
|
||||||
installUVBinary: () => Promise<void>
|
installUVBinary: () => Promise<void>
|
||||||
installBunBinary: () => Promise<void>
|
installBunBinary: () => Promise<void>
|
||||||
|
run: (command: string) => Promise<string>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,38 @@ const api = {
|
|||||||
restoreFromWebdav: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:restoreFromWebdav', webdavConfig),
|
restoreFromWebdav: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:restoreFromWebdav', webdavConfig),
|
||||||
listWebdavFiles: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:listWebdavFiles', 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: {
|
file: {
|
||||||
select: (options?: OpenDialogOptions) => ipcRenderer.invoke('file:select', options),
|
select: (options?: OpenDialogOptions) => ipcRenderer.invoke('file:select', options),
|
||||||
upload: (filePath: string) => ipcRenderer.invoke('file:upload', filePath),
|
upload: (filePath: string) => ipcRenderer.invoke('file:upload', filePath),
|
||||||
@@ -124,7 +156,7 @@ const api = {
|
|||||||
cleanup: () => ipcRenderer.invoke('mcp:cleanup')
|
cleanup: () => ipcRenderer.invoke('mcp:cleanup')
|
||||||
},
|
},
|
||||||
shell: {
|
shell: {
|
||||||
openExternal: shell.openExternal
|
openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url)
|
||||||
},
|
},
|
||||||
copilot: {
|
copilot: {
|
||||||
getAuthMessage: (headers?: Record<string, string>) => ipcRenderer.invoke('copilot:get-auth-message', headers),
|
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),
|
isBinaryExist: (name: string) => ipcRenderer.invoke('app:is-binary-exist', name),
|
||||||
getBinaryPath: (name: string) => ipcRenderer.invoke('app:get-binary-path', name),
|
getBinaryPath: (name: string) => ipcRenderer.invoke('app:get-binary-path', name),
|
||||||
installUVBinary: () => ipcRenderer.invoke('app:install-uv-binary'),
|
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
|
// 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 FilesPage from './pages/files/FilesPage'
|
||||||
import HomePage from './pages/home/HomePage'
|
import HomePage from './pages/home/HomePage'
|
||||||
import KnowledgePage from './pages/knowledge/KnowledgePage'
|
import KnowledgePage from './pages/knowledge/KnowledgePage'
|
||||||
|
import NodeAppsPage from './pages/nodeapps/NodeAppsPage'
|
||||||
import PaintingsPage from './pages/paintings/PaintingsPage'
|
import PaintingsPage from './pages/paintings/PaintingsPage'
|
||||||
import SettingsPage from './pages/settings/SettingsPage'
|
import SettingsPage from './pages/settings/SettingsPage'
|
||||||
import TranslatePage from './pages/translate/TranslatePage'
|
import TranslatePage from './pages/translate/TranslatePage'
|
||||||
@@ -41,6 +42,7 @@ function App(): JSX.Element {
|
|||||||
<Route path="/files" element={<FilesPage />} />
|
<Route path="/files" element={<FilesPage />} />
|
||||||
<Route path="/knowledge" element={<KnowledgePage />} />
|
<Route path="/knowledge" element={<KnowledgePage />} />
|
||||||
<Route path="/apps" element={<AppsPage />} />
|
<Route path="/apps" element={<AppsPage />} />
|
||||||
|
<Route path="/nodeapps" element={<NodeAppsPage />} />
|
||||||
<Route path="/settings/*" element={<SettingsPage />} />
|
<Route path="/settings/*" element={<SettingsPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ const MainMenus: FC = () => {
|
|||||||
paintings: <PictureOutlined style={{ fontSize: 16 }} />,
|
paintings: <PictureOutlined style={{ fontSize: 16 }} />,
|
||||||
translate: <TranslationOutlined />,
|
translate: <TranslationOutlined />,
|
||||||
minapp: <i className="iconfont icon-appstore" />,
|
minapp: <i className="iconfont icon-appstore" />,
|
||||||
|
nodeapps: <i className="iconfont icon-code" />,
|
||||||
knowledge: <FileSearchOutlined />,
|
knowledge: <FileSearchOutlined />,
|
||||||
files: <FolderOutlined />
|
files: <FolderOutlined />
|
||||||
}
|
}
|
||||||
@@ -140,6 +141,7 @@ const MainMenus: FC = () => {
|
|||||||
paintings: '/paintings',
|
paintings: '/paintings',
|
||||||
translate: '/translate',
|
translate: '/translate',
|
||||||
minapp: '/apps',
|
minapp: '/apps',
|
||||||
|
nodeapps: '/nodeapps',
|
||||||
knowledge: '/knowledge',
|
knowledge: '/knowledge',
|
||||||
files: '/files'
|
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
|
keyv: KeyvStorage
|
||||||
mermaid: any
|
mermaid: any
|
||||||
store: 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": {
|
"tools": {
|
||||||
"invoking": "Invoking",
|
"invoking": "Invoking",
|
||||||
"completed": "Completed"
|
"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": {
|
"minapp": {
|
||||||
"sidebar.add.title": "Add to sidebar",
|
"add": "Add",
|
||||||
"sidebar.remove.title": "Remove from sidebar",
|
"apps.tab.search": "Search apps",
|
||||||
"title": "MinApp"
|
"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": {
|
"miniwindow": {
|
||||||
"clipboard": {
|
"clipboard": {
|
||||||
@@ -1115,6 +1124,74 @@
|
|||||||
"title": "Tavily"
|
"title": "Tavily"
|
||||||
},
|
},
|
||||||
"title": "Web Search"
|
"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": {
|
"translate": {
|
||||||
@@ -1152,6 +1229,146 @@
|
|||||||
"quit": "Quit",
|
"quit": "Quit",
|
||||||
"show_window": "Show Window",
|
"show_window": "Show Window",
|
||||||
"visualization": "Visualization"
|
"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": "Tavily"
|
||||||
},
|
},
|
||||||
"title": "网络搜索"
|
"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": {
|
"translate": {
|
||||||
@@ -1152,6 +1199,32 @@
|
|||||||
"quit": "退出",
|
"quit": "退出",
|
||||||
"show_window": "显示窗口",
|
"show_window": "显示窗口",
|
||||||
"visualization": "可视化"
|
"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 messagesReducer from './messages'
|
||||||
import migrate from './migrate'
|
import migrate from './migrate'
|
||||||
import minapps from './minapps'
|
import minapps from './minapps'
|
||||||
|
import nodeapps from './nodeapps'
|
||||||
import paintings from './paintings'
|
import paintings from './paintings'
|
||||||
import runtime from './runtime'
|
import runtime from './runtime'
|
||||||
import settings from './settings'
|
import settings from './settings'
|
||||||
@@ -33,6 +34,7 @@ const rootReducer = combineReducers({
|
|||||||
websearch,
|
websearch,
|
||||||
mcp,
|
mcp,
|
||||||
copilot,
|
copilot,
|
||||||
|
nodeapps,
|
||||||
messages: messagesReducer
|
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 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[] = [
|
export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [
|
||||||
'assistants',
|
'assistants',
|
||||||
@@ -12,6 +12,7 @@ export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [
|
|||||||
'paintings',
|
'paintings',
|
||||||
'translate',
|
'translate',
|
||||||
'minapp',
|
'minapp',
|
||||||
|
'nodeapps',
|
||||||
'knowledge',
|
'knowledge',
|
||||||
'files'
|
'files'
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -164,6 +164,21 @@ export type MinAppType = {
|
|||||||
style?: React.CSSProperties
|
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 {
|
export interface FileType {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
Reference in New Issue
Block a user