feat: 实现 RemoteApp 类以支持远程连接,添加初始化和重连逻辑;更新用户路由以支持获取用户信息
This commit is contained in:
@@ -5,9 +5,14 @@ import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import glob from 'fast-glob';
|
||||
import type { App } from '@kevisual/router';
|
||||
import { RemoteApp } from '@/module/remote-app/remote-app.ts';
|
||||
import { logger } from '@/module/logger.ts';
|
||||
export class AssistantApp extends Manager {
|
||||
config: AssistantConfig;
|
||||
pagesPath: string;
|
||||
remoteIsConnected = false;
|
||||
attemptedConnectTimes = 0;
|
||||
remoteApp: RemoteApp | null = null;
|
||||
constructor(config: AssistantConfig, mainApp?: App) {
|
||||
config.checkMounted();
|
||||
const appsPath = config?.configPath?.appsDir || path.join(process.cwd(), 'apps');
|
||||
@@ -65,4 +70,60 @@ export class AssistantApp extends Manager {
|
||||
});
|
||||
return pagesParse;
|
||||
}
|
||||
|
||||
async initRemoteApp() {
|
||||
const config = this.config.getConfig();
|
||||
const share = config?.share;
|
||||
if (share && share.enabled !== false) {
|
||||
const token = config?.token;
|
||||
const url = new URL(share.url || 'https://kevisual.cn/ws/proxy');
|
||||
const id = config?.app?.id;
|
||||
if (token && url && id) {
|
||||
const remoteApp = new RemoteApp({
|
||||
url: url.toString(),
|
||||
token,
|
||||
id,
|
||||
app: this.mainApp,
|
||||
});
|
||||
const isConnect = await remoteApp.isConnect();
|
||||
if (isConnect) {
|
||||
remoteApp.listenProxy();
|
||||
this.remoteIsConnected = true;
|
||||
remoteApp.emitter.once('close', () => {
|
||||
setTimeout(() => {
|
||||
if (remoteApp.isError) {
|
||||
console.error('远程应用发生错误,不重连');
|
||||
} else {
|
||||
this.reconnectRemoteApp();
|
||||
}
|
||||
}, 5 * 1000); // 第一次断开5秒后重连
|
||||
});
|
||||
logger.debug('链接到了远程应用服务器');
|
||||
} else {
|
||||
console.log('Not connected to remote app server');
|
||||
}
|
||||
this.remoteApp = remoteApp;
|
||||
} else {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
async reconnectRemoteApp() {
|
||||
console.log('重新连接到远程应用服务器...', this.attemptedConnectTimes);
|
||||
const remoteApp = this.remoteApp;;
|
||||
if (remoteApp) {
|
||||
remoteApp.init();
|
||||
this.attemptedConnectTimes += 1;
|
||||
const isConnect = await remoteApp.isConnect();
|
||||
if (isConnect) {
|
||||
remoteApp.listenProxy();
|
||||
this.attemptedConnectTimes = 0;
|
||||
console.log('重新连接到了远程应用服务器');
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.reconnectRemoteApp();
|
||||
}, 30 * 1000); // 30秒后重连
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
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 config = this.assistantConfig?.getConfig();
|
||||
const share = config?.share;
|
||||
const token = config?.token;
|
||||
const app = config?.app || {};
|
||||
const name = app.id;
|
||||
this.emitter = opts?.emitter || new EventEmitter();
|
||||
if (share) {
|
||||
const { url, enabled } = share;
|
||||
const _url = new URL(url);
|
||||
if (token) {
|
||||
_url.searchParams.set('token', token);
|
||||
}
|
||||
_url.searchParams.set('id', app.id);
|
||||
this.url = _url.toString();
|
||||
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) {
|
||||
console.log('Message from remote app:', this.name, data.toString());
|
||||
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
assistant/src/module/remote-app/.npmrc
Normal file
2
assistant/src/module/remote-app/.npmrc
Normal file
@@ -0,0 +1,2 @@
|
||||
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
|
||||
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
||||
20
assistant/src/module/remote-app/package.json
Normal file
20
assistant/src/module/remote-app/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@kevisual/remote-app",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "remote-app.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"files": [
|
||||
"remote-app.ts"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@10.26.0",
|
||||
"type": "module"
|
||||
}
|
||||
165
assistant/src/module/remote-app/remote-app.ts
Normal file
165
assistant/src/module/remote-app/remote-app.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import type { App } from '@kevisual/router';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
type RemoteAppOptions = {
|
||||
app?: App;
|
||||
url?: string;
|
||||
token?: string;
|
||||
emitter?: EventEmitter;
|
||||
id?: string;
|
||||
};
|
||||
export class RemoteApp {
|
||||
mainApp: App;
|
||||
url: string;
|
||||
id: string;
|
||||
emitter: EventEmitter;
|
||||
isConnected: boolean;
|
||||
ws: WebSocket;
|
||||
remoteIsConnected: boolean;
|
||||
isError: boolean = false;
|
||||
constructor(opts?: RemoteAppOptions) {
|
||||
this.mainApp = opts?.app;
|
||||
const token = opts.token;
|
||||
const url = opts.url;
|
||||
const id = opts.id;
|
||||
this.emitter = opts?.emitter || new EventEmitter();
|
||||
const _url = new URL(url);
|
||||
if (token) {
|
||||
_url.searchParams.set('token', token);
|
||||
}
|
||||
_url.searchParams.set('id', id);
|
||||
this.url = _url.toString();
|
||||
this.id = id;
|
||||
this.init();
|
||||
}
|
||||
async isConnect(): Promise<boolean> {
|
||||
const that = this;
|
||||
if (this.isConnected) {
|
||||
return true;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
resolve(false);
|
||||
that.emitter.off('open', listenOnce);
|
||||
}, 5000);
|
||||
const listenOnce = () => {
|
||||
clearTimeout(timeout);
|
||||
that.isConnected = true;
|
||||
that.remoteIsConnected = 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.id) {
|
||||
throw new Error('No id provided for remote app');
|
||||
}
|
||||
this.isError = false;
|
||||
const ws = new WebSocket(this.getWsURL(this.url));
|
||||
const that = this;
|
||||
ws.onopen = function () {
|
||||
that.isConnected = true;
|
||||
that.onOpen();
|
||||
};
|
||||
ws.onclose = function () {
|
||||
that.isConnected = false;
|
||||
that.onClose();
|
||||
}
|
||||
ws.onmessage = function (event) {
|
||||
that.onMessage(event.data);
|
||||
}
|
||||
ws.onerror = function (error) {
|
||||
that.onError(error);
|
||||
}
|
||||
this.ws = ws;
|
||||
}
|
||||
onOpen() {
|
||||
this.emitter.emit('open', this.id);
|
||||
}
|
||||
onClose() {
|
||||
console.log('远程应用关闭:', this.id);
|
||||
this.emitter.emit('close', this.id);
|
||||
this.isConnected = false;
|
||||
}
|
||||
onMessage(data: any) {
|
||||
this.emitter.emit('message', data);
|
||||
}
|
||||
onError(error: any) {
|
||||
console.error('远程应用错误:', this.id, error);
|
||||
this.isError = true;
|
||||
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) => {
|
||||
try {
|
||||
const data = event.toString();
|
||||
const body = JSON.parse(data);
|
||||
const message = body.data || {};
|
||||
if (body?.code === 401) {
|
||||
console.error('远程应用认证失败,请检查 token 是否正确');
|
||||
this.isError = true;
|
||||
return;
|
||||
}
|
||||
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,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('处理远程代理请求出错:', error);
|
||||
}
|
||||
};
|
||||
remoteApp.json({
|
||||
id: this.id,
|
||||
type: 'registryClient'
|
||||
});
|
||||
remoteApp.emitter.on('message', listenFn);
|
||||
const closeMessage = () => {
|
||||
remoteApp.emitter.off('message', listenFn);
|
||||
}
|
||||
remoteApp.emitter.once('close', () => {
|
||||
closeMessage();
|
||||
});
|
||||
return () => {
|
||||
closeMessage();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -67,3 +67,16 @@ app.route({
|
||||
|
||||
ctx.body = responseData.data;
|
||||
}).addTo(app);
|
||||
|
||||
app.route({
|
||||
path: 'user',
|
||||
key: 'me'
|
||||
}).define(async (ctx) => {
|
||||
const token = ctx.query.token;
|
||||
const res = await assistantConfig.query.post({
|
||||
path: 'user',
|
||||
key: 'me',
|
||||
token,
|
||||
});
|
||||
ctx.forward(res);
|
||||
}).addTo(app);
|
||||
@@ -45,6 +45,7 @@ export const runServer = async (port: number = 51015, listenPath = '127.0.0.1')
|
||||
manager.load({ runtime: 'client' }).then(() => {
|
||||
console.log('Assistant App Loaded');
|
||||
});
|
||||
manager.initRemoteApp()
|
||||
}, 1000);
|
||||
return {
|
||||
app,
|
||||
|
||||
@@ -1,22 +1,42 @@
|
||||
import { logger } from '@/module/logger.ts';
|
||||
import { assistantConfig, app } from '../app.ts';
|
||||
import { WebSocket } from 'ws';
|
||||
import '../routes/index.ts';
|
||||
import { RemoteApp } from '@/module/assistant/remote-app/remote-app.ts';
|
||||
import { RemoteApp } from '@/module/remote-app/remote-app.ts';
|
||||
const main = async () => {
|
||||
assistantConfig.checkMounted();
|
||||
const config = assistantConfig?.getConfig();
|
||||
const share = config?.share;
|
||||
if (share && share.enabled !== false) {
|
||||
const token = config?.token;
|
||||
const url = new URL(share.url || 'https://kevisual.cn/ws/proxy');
|
||||
const id = config?.app?.id;
|
||||
if (!id) {
|
||||
console.error('App ID is required for remote app connection.');
|
||||
return;
|
||||
}
|
||||
if (!token) {
|
||||
console.error('Token is required for remote app connection.');
|
||||
return;
|
||||
}
|
||||
const remoteApp = new RemoteApp({
|
||||
assistantConfig,
|
||||
url: url.toString(),
|
||||
token,
|
||||
id,
|
||||
app,
|
||||
});
|
||||
const connect = await remoteApp.isConnect();
|
||||
if (connect) {
|
||||
console.log('Connected to proxy server');
|
||||
remoteApp.listenProxy();
|
||||
remoteApp.emitter.on('message', (event) => {
|
||||
const _msg = event.toString();
|
||||
console.log('Received message from remote app:', _msg);
|
||||
});
|
||||
} else {
|
||||
console.log('Not connected to proxy server');
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// main();
|
||||
main();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user