"feat: 新增远程应用测试脚本,升级 pm2 依赖"
This commit is contained in:
@@ -77,6 +77,14 @@ export type AssistantConfigData = {
|
||||
path?: string;
|
||||
port?: number;
|
||||
};
|
||||
share?: {
|
||||
url: string;
|
||||
enabled?: boolean; // 是否启用远程应用
|
||||
name: string;
|
||||
};
|
||||
watch?: {
|
||||
enabled?: boolean;
|
||||
};
|
||||
/**
|
||||
* 首页
|
||||
*/
|
||||
|
||||
138
assistant/src/module/assistant/remote-app/remote-app.ts
Normal file
138
assistant/src/module/assistant/remote-app/remote-app.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
17
assistant/src/test/common.ts
Normal file
17
assistant/src/test/common.ts
Normal 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();
|
||||
20
assistant/src/test/remote-app.ts
Normal file
20
assistant/src/test/remote-app.ts
Normal 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();
|
||||
Reference in New Issue
Block a user