This commit is contained in:
abearxiong 2024-12-04 20:10:54 +08:00
commit 8b136d1a5a
20 changed files with 2292 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
node_modules
dist
coverage
.DS_Store
.env
/logs

17
ecosystem.config.cjs Normal file
View File

@ -0,0 +1,17 @@
module.exports = {
apps: [
{
name: 'dev-app',
script: 'tsx ./src/index.ts',
log_date_format: 'YYYY-MM-DD HH:mm Z',
output: './logs/dev-app.log', // 所有日志输出到这里
error: './logs/dev-app.log', // 错误日志也写到同一个文件
log_date_format: 'YYYY-MM-DD HH:mm:ss',
watch: false,
env: {
PM2_COLOR: 'true', // 开启彩色日志
},
merge_logs: true, // 合并日志流
},
],
};

30
package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "dev-app",
"version": "0.0.1",
"description": "",
"main": "index.js",
"app": {
"type": "micro-app"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me>",
"license": "MIT",
"type": "module",
"packageManager": "pnpm@9.8.0+sha512.8e4c3550fb500e808dbc30bb0ce4dd1eb614e30b1c55245f211591ec2cdf9c611cabd34e1364b42f564bd54b3945ed0f49d61d1bbf2ec9bd74b866fcdc723276",
"devDependencies": {
"@kevisual/router": "0.0.6-alpha-2",
"@kevisual/types": "^0.0.1",
"@kevisual/use-config": "^1.0.4",
"@types/node": "^22.10.1",
"typescript": "^5.7.2"
},
"dependencies": {
"esbuild": "^0.24.0",
"nanoid": "^5.0.9",
"sequelize": "^6.37.5",
"sqlite3": "^5.1.7"
}
}

1554
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

5
src/app.ts Normal file
View File

@ -0,0 +1,5 @@
import { App } from '@kevisual/router';
export const app = new App();
app.listen(9998)

15
src/child/run.ts Normal file
View File

@ -0,0 +1,15 @@
import { spawn, spawnSync } from 'child_process';
import path from 'path';
const devPath = '/Users/xion/on/on-ai/packages/parsex-extensions';
export const runDev = () => {
const a = spawnSync('vite build --watch', {
stdio: 'inherit',
shell: true,
cwd: path.resolve(devPath),
env: process.env,
});
console.log('a===', a);
};
runDev();

11
src/index.ts Normal file
View File

@ -0,0 +1,11 @@
import { app } from './app.ts';
import './route/ssh/list.ts';
app
.call({
path: 'ssh',
key: 'list',
})
.then((res) => {
console.log(res);
});

42
src/lib/build/a.ts Normal file
View File

@ -0,0 +1,42 @@
import { build } from 'esbuild';
import { writeFileSync } from 'fs';
import { resolve } from 'path';
export async function transformToMJS(inputCode) {
// 1. 使用 esbuild 转换代码
const result = await build({
stdin: {
contents: inputCode,
resolveDir: process.cwd(), // 当前目录为基准路径,用于解析文件路径
loader: 'ts',
},
format: 'esm', // 输出为 ESM 格式
outfile: 'output.mjs', // 输出文件路径
write: false, // 不直接写入文件
});
// 2. 将结果保存为 .mjs 文件
const outputPath = resolve('./output.mjs');
writeFileSync(outputPath, result.outputFiles[0].text);
console.log(`转换完成,文件已保存到: ${outputPath}`);
return outputPath;
}
// 示例字符串代码
const inputCode = `
import path from 'path';
import {nanoid} from 'nanoid';
console.log(path.resolve(), nanoid(6));
`;
const runCode = (filePath: string) => {
const mjs = import(filePath);
//
};
transformToMJS(inputCode)
.then((outputPath) => {
console.log(`生成的 MJS 文件路径: ${outputPath}`);
runCode(outputPath);
})
.catch(console.error);

3
src/lib/ssh.ts Normal file
View File

@ -0,0 +1,3 @@
import { SSHManager } from './ssh/manager.ts';
export const sshManager = new SSHManager();

46
src/lib/ssh/manager.ts Normal file
View File

@ -0,0 +1,46 @@
import { SSHServer, SSHValue } from './ssh.ts';
export class SSHManager {
servers: SSHServer[] = [];
constructor() {}
addServer(server: SSHServer) {
this.servers.push(server);
}
getServers() {
return this.servers.map((server) => {
return {
name: server.name,
isRunning: server.isRunning,
values: server.values,
};
});
}
createServer(remote: string, values: SSHValue[]) {
if (!remote) {
console.error('remote 不能为空');
return;
}
const has = this.servers.find((server) => server.name === remote);
let flag = false;
if (has && has.isRunning) {
flag = true;
has.stop();
}
const server = new SSHServer({ name: remote, values });
if (flag) {
setTimeout(() => {
server.start(); // 重新启动
}, 1000);
} else {
server.start();
}
this.addServer(server);
return server;
}
stopServer(remote: string) {
const server = this.servers.find((server) => server.name === remote);
if (server) {
server.stop();
}
}
}

103
src/lib/ssh/ssh.ts Normal file
View File

@ -0,0 +1,103 @@
// ssh -L 8080:localhost:80 -L 9090:localhost:90 user@remote-server
import { ChildProcess, spawn } from 'child_process';
type Options = {
name: string;
values: SSHValue[];
};
export type SSHValue = {
localPort: number;
remoteHost: string; // localhost
remotePort: number;
remote?: string; //sky config配置
status?: 'active' | 'inactive';
};
export const demos: SSHValue[] = [
{
localPort: 3000,
remoteHost: 'localhost',
remotePort: 3000,
remote: 'diana',
},
{
localPort: 5244,
remoteHost: 'localhost',
remotePort: 5244,
remote: 'diana',
},
];
export class SSHServer {
name = 'diana';
values: SSHValue[] = [];
childProcess: ChildProcess | null = null;
isRunning = false;
constructor(opts?: Options) {
this.name = opts?.name || this.name;
this.values = opts?.values || demos;
}
start(options?: Options) {
try {
const remote = options?.name || this.name;
const demos = options?.values || this.values;
const _port = demos.filter((item) => item.status === 'active');
if (_port.length === 0) {
this.isRunning = false;
return;
}
const params = _port
.map((item) => {
return ['-L', `${item.localPort}:${item.remoteHost}:${item.remotePort}`];
})
.flat();
const childProcess = spawn(
'ssh',
[
'-N', // 不执行远程命令
// '-f', // 后台运行
'-o',
'ServerAliveInterval=60', // 每60秒发送一次心跳包
'-o',
'ServerAliveCountMax=3', // 尝试3次心跳失败后断开连接
'-T',
...params,
remote,
],
{
// stdio: 'ignore',
stdio: 'inherit',
killSignal: 'SIGINT',
},
);
this.childProcess = childProcess;
childProcess.stdout?.on('data', (data) => {
console.log('childProcess', data.toString());
});
childProcess.on('error', (err: any) => {
if (err.code === 'EADDRINUSE') {
console.error('端口被占用:', err);
return;
}
console.error('Failed to start SSH process:', err);
});
childProcess.on('exit', (code, signal) => {
console.log('SSH process exited:', code, signal);
this.isRunning = false;
});
this.isRunning = true;
console.log('当前ssh -L 服务启动', this.name);
// 监听的端口是
// console.log('监听的端口是:', _port.map((item) => `[${item.localPort},${item.remotePort}]`).join(','));
console.log('监听的端口是:', _port.map((item) => item.localPort).join(','));
} catch (error) {
console.error('启动失败:', error);
}
}
stop() {
if (this.childProcess) {
console.log('正在停止 ssh -L 服务:', this.name);
this.childProcess.kill(); // 发送默认信号 SIGTERM
this.childProcess = undefined;
} else {
console.log('没有正在运行的 ssh -L 服务.');
}
}
}

47
src/modules/get-config.ts Normal file
View File

@ -0,0 +1,47 @@
import os from 'os';
import path from 'path';
import fs from 'fs';
export const envisionPath = path.join(os.homedir(), '.config', 'envision');
const configPath = path.join(os.homedir(), '.config', 'envision', 'config.json');
export const pidFilePath = path.join(envisionPath, 'app.pid');
export const dbPath = path.join(envisionPath, 'db.sqlite');
const envisionPidDir = path.join(envisionPath);
export const getPidList = () => {
const files = fs.readdirSync(envisionPidDir);
const pidFiles = files.filter((file) => file.endsWith('.pid'));
return pidFiles.map((file) => {
const pid = fs.readFileSync(path.join(envisionPidDir, file), 'utf-8');
return { pid, file: path.join(envisionPidDir, file) };
});
};
export const writeVitePid = (pid: number) => {
fs.writeFileSync(path.join(envisionPath, `vite-${pid}.pid`), pid.toString());
};
export const checkFileExists = (filePath: string) => {
try {
fs.accessSync(filePath, fs.constants.F_OK);
return true;
} catch (error) {
return false;
}
};
export const getConfig = () => {
if (!checkFileExists(envisionPath)) {
fs.mkdirSync(envisionPath, { recursive: true });
}
if (checkFileExists(configPath)) {
const config = fs.readFileSync(configPath, 'utf-8');
try {
return JSON.parse(config);
} catch (e) {
return {};
}
}
return {};
};
export const writeConfig = (config: Record<string, any>) => {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
};

16
src/modules/sequelize.ts Normal file
View File

@ -0,0 +1,16 @@
import { Sequelize } from 'sequelize';
import { dbPath } from './get-config.ts';
// connect to db
export const sequelize = new Sequelize({
dialect: 'sqlite',
storage: dbPath,
// logging: false,
});
sequelize
.authenticate({ logging: false })
.then(() => {})
.catch((err) => {
console.error('Unable to connect to the database:', err);
});

60
src/route/dev/model.ts Normal file
View File

@ -0,0 +1,60 @@
import { DataTypes, Model, Op } from 'sequelize';
import { sequelize } from '@/modules/sequelize.ts';
type DevData = {
cwd?: string; // 当前工作目录
command: string; // 启动命令
type: 'node' | 'shell' | 'script'; // 启动类型
env?: Record<string, string>; // 环境变量
keepalive?: boolean; // 是否保持运行, 让用户知道是不是保持运行的
code?: string; // 代码
status?: 'active' | 'inactive'; // 状态
path?: string; // esbuild 打包路径,用于 node 启动
};
export type Dev = Partial<InstanceType<typeof DevModel>>;
/**
* @description
*/
export class DevModel extends Model {
declare id: string;
declare title: string;
declare description: string;
declare data: DevData;
}
DevModel.init(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
},
title: {
type: DataTypes.TEXT,
allowNull: false,
defaultValue: '',
},
description: {
type: DataTypes.TEXT,
allowNull: true,
},
data: {
type: DataTypes.JSON,
allowNull: false,
defaultValue: {},
},
},
{
sequelize,
paranoid: true,
modelName: 'local_dev',
freezeTableName: true, // 禁用表名复数化
},
);
DevModel.sync({
alter: true,
logging: false,
}).catch((e) => {
console.error(e);
});

59
src/route/ssh/config.ts Normal file
View File

@ -0,0 +1,59 @@
import { DataTypes, Model, Op } from 'sequelize';
import { sequelize } from '@/modules/sequelize.ts';
type SSHModelData = {
localPort: number;
remoteHost: string; // localhost
remotePort: number;
status?: 'active' | 'inactive';
};
export type SSH = Partial<InstanceType<typeof SSHModel>>;
export class SSHModel extends Model {
declare id: string;
// declare title: string;
declare description: string;
declare configs: SSHModelData[];
declare remote: string;
}
SSHModel.init(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
},
// title: {
// type: DataTypes.TEXT,
// allowNull: false,
// defaultValue: '',
// },
description: {
type: DataTypes.TEXT,
allowNull: true,
},
configs: {
type: DataTypes.JSON,
allowNull: false,
defaultValue: [],
},
remote: {
type: DataTypes.TEXT,
allowNull: false,
defaultValue: '',
},
},
{
sequelize,
paranoid: true,
modelName: 'local_ssh',
freezeTableName: true, // 禁用表名复数化
},
);
SSHModel.sync({
alter: true,
logging: false,
}).catch((e) => {
console.error(e);
});

79
src/route/ssh/list.ts Normal file
View File

@ -0,0 +1,79 @@
import { app } from '@/app.ts';
import { SSHModel } from './model.ts';
import { sshManager } from '@/lib/ssh.ts';
import { exec } from 'child_process';
const init = async () => {
const sshList = await SSHModel.findAll();
sshList.forEach((ssh) => {
sshManager.createServer(ssh.remote, ssh.configs);
});
};
setTimeout(init, 1000);
app
.route({
path: 'ssh',
key: 'list',
})
.define(async (ctx) => {
const sshList = await SSHModel.findAll();
ctx.body = sshList;
})
.addTo(app);
app
.route({
path: 'ssh',
key: 'status',
})
.define(async (ctx) => {
ctx.body = sshManager.getServers();
})
.addTo(app);
app
.route({
path: 'ssh',
key: 'update',
})
.define(async (ctx) => {
const data = ctx.query.data;
const isExec = ctx.query.exec ?? true;
const { description, configs, remote } = data;
if (!remote) {
ctx.throw(400, 'remote 不能为空');
}
let ssh = await SSHModel.findOne({ where: { remote } });
if (!ssh) {
ssh = await SSHModel.create({ description, configs, remote });
}
await ssh.update({ description, configs }, { where: { remote } });
//
if (isExec) {
sshManager.createServer(remote, configs);
}
ctx.body = ssh;
})
.addTo(app);
app
.route({
path: 'ssh',
key: 'closePort',
})
.define(async (ctx) => {
const port = ctx.query.port;
if (!port) {
ctx.throw(400, 'port 不能为空');
}
exec(`lsof -i:${port} | grep LISTEN | awk '{print $2}' | xargs kill -9`, (killErr, stdout, stderr) => {
if (killErr) {
console.error(`Failed to kill process on port ${port}:`, killErr.message);
} else {
console.log(`Port ${port} is now free. Restarting process...`);
}
});
ctx.body = 'ok';
})
.addTo(app);

59
src/route/ssh/model.ts Normal file
View File

@ -0,0 +1,59 @@
import { DataTypes, Model, Op } from 'sequelize';
import { sequelize } from '@/modules/sequelize.ts';
type SSHModelData = {
localPort: number;
remoteHost: string; // localhost
remotePort: number;
status?: 'active' | 'inactive';
};
export type SSH = Partial<InstanceType<typeof SSHModel>>;
export class SSHModel extends Model {
declare id: string;
// declare title: string;
declare description: string;
declare configs: SSHModelData[];
declare remote: string;
}
SSHModel.init(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
},
// title: {
// type: DataTypes.TEXT,
// allowNull: false,
// defaultValue: '',
// },
description: {
type: DataTypes.TEXT,
allowNull: true,
},
configs: {
type: DataTypes.JSON,
allowNull: false,
defaultValue: [],
},
remote: {
type: DataTypes.TEXT,
allowNull: false,
defaultValue: '',
},
},
{
sequelize,
paranoid: true,
modelName: 'local_ssh',
freezeTableName: true, // 禁用表名复数化
},
);
SSHModel.sync({
alter: true,
logging: false,
}).catch((e) => {
console.error(e);
});

72
src/scripts/add-nisar.ts Normal file
View File

@ -0,0 +1,72 @@
import { app } from '../app.ts';
import '../index.ts';
const sleep = (time: number) => new Promise((resolve) => setTimeout(resolve, time));
const main = async () => {
await sleep(3000);
// console.log('add nisar')
app.call({
path: 'ssh',
key: 'update',
payload: {
exec: true,
data: {
remote: 'nisar',
description: 'diana的项目ssl l关联',
configs: [
{
localPort: 3000,
remoteHost: 'localhost',
remotePort: 3000,
description: 'openweb ui',
status: 'active',
},
{
localPort: 5244,
remoteHost: 'localhost',
remotePort: 5244,
description: 'alist',
status: 'inactive',
},
{
localPort: 3003,
remoteHost: 'localhost',
remotePort: 3003,
description: 'onai api',
status: 'inactive',
},
{
localPort: 3002,
remoteHost: 'localhost',
remotePort: 3004,
description: 'codeflow ui',
status: 'inactive',
},
{
localPort: 8000,
remoteHost: 'localhost',
remotePort: 8000,
description: 'parsex python api',
status: 'inactive',
},
{
localPort: 9092,
remoteHost: 'localhost',
remotePort: 9092,
description: 'kafka',
status: 'inactive',
},
{
localPort: 9200,
remoteHost: 'localhost',
remotePort: 9200,
description: 'elasticsearch',
status: 'inactive',
},
],
},
},
});
};
main();

33
tsconfig.json Normal file
View File

@ -0,0 +1,33 @@
{
"compilerOptions": {
"module": "nodenext",
"target": "esnext",
"noImplicitAny": false,
"outDir": "./dist",
"sourceMap": false,
"allowJs": true,
"newLine": "LF",
"baseUrl": "./",
"typeRoots": [
"node_modules/@types",
],
"declaration": true,
"noEmit": false,
"allowImportingTsExtensions": true,
"emitDeclarationOnly": true,
"moduleResolution": "NodeNext",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"paths": {
"@/*": [
"src/*"
],
}
},
"include": [
"typings.d.ts",
"src/**/*.ts",
"test/**/*.ts",
],
}

View File

@ -0,0 +1,33 @@
{
"compilerOptions": {
"module": "nodenext",
"target": "esnext",
"noImplicitAny": false,
"outDir": "./dist",
"sourceMap": false,
"allowJs": true,
"newLine": "LF",
"baseUrl": "./",
"typeRoots": [
"node_modules/@types",
"node_modules/@kevisual/types"
],
"declaration": true,
"noEmit": false,
"allowImportingTsExtensions": true,
"emitDeclarationOnly": true,
"moduleResolution": "NodeNext",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"paths": {
"@/*": [
"src/*"
]
}
},
"include": [
"typings.d.ts",
"src/**/*.ts"
]
}