"feat: 新增远程应用测试脚本,升级 pm2 依赖"

This commit is contained in:
2025-05-22 13:19:57 +08:00
parent 1c2aa26dd0
commit 1e340ec2b3
8 changed files with 415 additions and 259 deletions

View File

@@ -77,6 +77,14 @@ export type AssistantConfigData = {
path?: string;
port?: number;
};
share?: {
url: string;
enabled?: boolean; // 是否启用远程应用
name: string;
};
watch?: {
enabled?: boolean;
};
/**
* 首页
*/

View File

@@ -0,0 +1,138 @@
import type { AssistantConfig } from '@/module/assistant/index.ts';
import { WebSocket } from 'ws';
import type { App } from '@kevisual/router';
import { EventEmitter } from 'eventemitter3';
import { logger } from '@/module/logger.ts';
type RemoteAppOptions = {
app?: App;
assistantConfig?: AssistantConfig;
emitter?: EventEmitter;
};
export class RemoteApp {
mainApp: App;
assistantConfig: AssistantConfig;
url: string;
name: string;
enabled: boolean;
emitter: EventEmitter;
isConnected: boolean;
ws: WebSocket;
constructor(opts?: RemoteAppOptions) {
this.mainApp = opts?.app;
this.assistantConfig = opts?.assistantConfig;
const share = this.assistantConfig?.getConfig()?.share;
this.emitter = opts?.emitter || new EventEmitter();
if (share) {
const { url, name, enabled } = share;
this.url = url;
this.name = name;
this.enabled = enabled ?? false;
if (this.enabled) {
this.init();
}
}
}
async isConnect(): Promise<boolean> {
const that = this;
if (this.isConnected) {
return true;
}
if (!this.enabled) {
return false;
}
return new Promise((resolve) => {
const timeout = setTimeout(() => {
resolve(false);
that.emitter.off('open', listenOnce);
}, 5000);
const listenOnce = () => {
clearTimeout(timeout);
that.isConnected = true;
resolve(true);
};
that.emitter.once('open', listenOnce);
});
}
getWsURL(url: string) {
const { protocol } = new URL(url);
const wsProtocol = protocol === 'https:' ? 'wss:' : 'ws:';
const wsURL = url.toString().replace(protocol, wsProtocol);
return wsURL;
}
async init() {
if (!this.url) {
throw new Error('No url provided for remote app');
}
if (!this.name) {
throw new Error('No name provided for remote app');
}
console.log('Connecting to remote app:', this.name, this.url, this.getWsURL(this.url));
const ws = new WebSocket(this.getWsURL(this.url), {
rejectUnauthorized: true,
});
const that = this;
ws.on('open', that.onOpen.bind(that));
ws.on('close', that.onClose.bind(that));
ws.on('message', that.onMessage.bind(that));
ws.on('error', that.onError.bind(that));
this.ws = ws;
}
onOpen() {
this.emitter.emit('open', this.name);
}
onClose() {
this.emitter.emit('close', this.name);
}
onMessage(data: any) {
this.emitter.emit('message', data);
}
onError(error: any) {
console.error('Error in remote app:', this.name, error);
this.emitter.emit('error', error);
}
on(event: 'open' | 'close' | 'message' | 'error', listener: (data: any) => void) {
this.emitter.on(event, listener);
return () => {
this.emitter.off(event, listener);
};
}
sendData(data: any) {}
json(data: any) {
this.ws.send(JSON.stringify(data));
}
listenProxy() {
const remoteApp = this;
const app = this.mainApp;
const listenFn = async (event: any) => {
const data = event.toString();
logger.debug('Received message:', data);
const body = JSON.parse(data);
const message = body.data || {};
if (body?.type !== 'proxy') return;
if (!body.id) {
remoteApp.json({
id: body.id,
data: {
code: 400,
message: 'id is required',
},
});
return;
}
const res = await app.call(message);
remoteApp.json({
id: body.id,
data: {
code: res.code,
data: res.body,
message: res.message,
},
});
};
remoteApp.emitter.on('message', listenFn);
return () => {
remoteApp.emitter.off('message', listenFn);
};
}
}

View File

@@ -2,6 +2,7 @@ import { Query } from '@kevisual/query';
import { app, assistantConfig } from '../app.ts';
import './config/index.ts';
import './shop-install/index.ts';
import os from 'node:os';
app
.route({
@@ -49,3 +50,31 @@ app
ctx.body = 'v1.0.0';
})
.addTo(app);
app
.route({
path: 'client',
key: 'time',
})
.define(async (ctx) => {
ctx.body = {
time: new Date().getTime(),
date: new Date().toLocaleDateString(),
};
})
.addTo(app);
app
.route({
path: 'client',
key: 'system',
})
.define(async (ctx) => {
const { platform, arch, release } = os;
ctx.body = {
platform: platform(),
arch: arch(),
release: release(),
};
})
.addTo(app);

View File

@@ -32,19 +32,28 @@ type ProxyType = {
};
export type LocalProxyOpts = {
assistantConfig?: AssistantConfig; // 前端应用路径
// watch?: boolean; // 是否监听文件变化
};
export class LocalProxy {
localProxyProxyList: ProxyType[] = [];
assistantConfig?: AssistantConfig;
watch?: boolean;
watching?: boolean;
initing?: boolean;
constructor(opts?: LocalProxyOpts) {
this.assistantConfig = opts?.assistantConfig;
if (this.assistantConfig) {
this.watch = !!this.assistantConfig.config?.watch.enabled;
this.init();
}
}
init() {
const frontAppDir = this.assistantConfig.configPath?.pagesDir;
if (frontAppDir) {
if (this.initing) {
return;
}
this.initing = true;
const userList = fs.readdirSync(frontAppDir);
const localProxyProxyList: ProxyType[] = [];
userList.forEach((user) => {
@@ -72,12 +81,45 @@ export class LocalProxy {
}
});
this.localProxyProxyList = localProxyProxyList;
this.initing = false;
}
}
onWatch() {
// 监听文件变化
const frontAppDir = this.assistantConfig.configPath?.pagesDir;
const that = this;
if (!this.watch && !frontAppDir) {
return;
}
if (this.watching) {
return;
}
that.watching = true;
let timer: NodeJS.Timeout;
const debounce = (fn: () => void, delay: number) => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn();
}, delay);
};
fs.watch(frontAppDir, { recursive: true }, (eventType, filename) => {
if (eventType === 'change') {
const filePath = path.join(frontAppDir, filename);
try {
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
debounce(that.init, 5000);
}
} catch (error) {}
}
});
}
getLocalProxyList() {
return this.localProxyProxyList;
}
reload() {
// 重新加载本地代理列表
this.init();
}
}

View File

@@ -0,0 +1,17 @@
// import { assistantConfig } from '../app.ts';
// import { RemoteApp } from '@/module/assistant/remote-app/remote-app.ts';
console.log('assistantConfig');
// console.log('assistantConfig', assistantConfig);
// const main = async () => {
// const app = new RemoteApp({
// assistantConfig,
// });
// const connect = await app.isConnect();
// if (connect) {
// console.log('Connected to assistant');
// } else {
// console.log('Not connected to assistant');
// }
// };
// main();

View File

@@ -0,0 +1,20 @@
import { logger } from '@/module/logger.ts';
import { assistantConfig, app } from '../app.ts';
import '../routes/index.ts';
import { RemoteApp } from '@/module/assistant/remote-app/remote-app.ts';
const main = async () => {
assistantConfig.checkMounted();
const remoteApp = new RemoteApp({
assistantConfig,
app,
});
const connect = await remoteApp.isConnect();
if (connect) {
console.log('Connected to proxy server');
remoteApp.listenProxy();
} else {
console.log('Not connected to proxy server');
}
};
main();