feat: 实现 RemoteApp 类以支持远程连接,添加初始化和重连逻辑;更新用户路由以支持获取用户信息
This commit is contained in:
@@ -5,9 +5,14 @@ import path from 'node:path';
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import glob from 'fast-glob';
|
import glob from 'fast-glob';
|
||||||
import type { App } from '@kevisual/router';
|
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 {
|
export class AssistantApp extends Manager {
|
||||||
config: AssistantConfig;
|
config: AssistantConfig;
|
||||||
pagesPath: string;
|
pagesPath: string;
|
||||||
|
remoteIsConnected = false;
|
||||||
|
attemptedConnectTimes = 0;
|
||||||
|
remoteApp: RemoteApp | null = null;
|
||||||
constructor(config: AssistantConfig, mainApp?: App) {
|
constructor(config: AssistantConfig, mainApp?: App) {
|
||||||
config.checkMounted();
|
config.checkMounted();
|
||||||
const appsPath = config?.configPath?.appsDir || path.join(process.cwd(), 'apps');
|
const appsPath = config?.configPath?.appsDir || path.join(process.cwd(), 'apps');
|
||||||
@@ -65,4 +70,60 @@ export class AssistantApp extends Manager {
|
|||||||
});
|
});
|
||||||
return pagesParse;
|
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;
|
ctx.body = responseData.data;
|
||||||
}).addTo(app);
|
}).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(() => {
|
manager.load({ runtime: 'client' }).then(() => {
|
||||||
console.log('Assistant App Loaded');
|
console.log('Assistant App Loaded');
|
||||||
});
|
});
|
||||||
|
manager.initRemoteApp()
|
||||||
}, 1000);
|
}, 1000);
|
||||||
return {
|
return {
|
||||||
app,
|
app,
|
||||||
|
|||||||
@@ -1,22 +1,42 @@
|
|||||||
import { logger } from '@/module/logger.ts';
|
|
||||||
import { assistantConfig, app } from '../app.ts';
|
import { assistantConfig, app } from '../app.ts';
|
||||||
import { WebSocket } from 'ws';
|
|
||||||
import '../routes/index.ts';
|
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 () => {
|
const main = async () => {
|
||||||
assistantConfig.checkMounted();
|
assistantConfig.checkMounted();
|
||||||
const remoteApp = new RemoteApp({
|
const config = assistantConfig?.getConfig();
|
||||||
assistantConfig,
|
const share = config?.share;
|
||||||
app,
|
if (share && share.enabled !== false) {
|
||||||
});
|
const token = config?.token;
|
||||||
const connect = await remoteApp.isConnect();
|
const url = new URL(share.url || 'https://kevisual.cn/ws/proxy');
|
||||||
if (connect) {
|
const id = config?.app?.id;
|
||||||
console.log('Connected to proxy server');
|
if (!id) {
|
||||||
remoteApp.listenProxy();
|
console.error('App ID is required for remote app connection.');
|
||||||
} else {
|
return;
|
||||||
console.log('Not connected to proxy server');
|
}
|
||||||
|
if (!token) {
|
||||||
|
console.error('Token is required for remote app connection.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const remoteApp = new RemoteApp({
|
||||||
|
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