generated from tailored/router-db-template
base module
This commit is contained in:
1
assistant-module/src/assistant-config.ts
Normal file
1
assistant-module/src/assistant-config.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './config/index.ts';
|
||||
1
assistant-module/src/assistant-process.ts
Normal file
1
assistant-module/src/assistant-process.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './process/index.ts';
|
||||
1
assistant-module/src/assistant-proxy.ts
Normal file
1
assistant-module/src/assistant-proxy.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './proxy/index.ts';
|
||||
111
assistant-module/src/config/index.ts
Normal file
111
assistant-module/src/config/index.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import path from 'path';
|
||||
import { homedir } from 'os';
|
||||
import fs from 'fs';
|
||||
import { checkFileExists, createDir } from '../file/index.ts';
|
||||
import { ProxyInfo } from '../proxy/proxy.ts';
|
||||
|
||||
export const kevisualUrl = 'https://kevisual.xiongxiao.me';
|
||||
const configDir = createDir(path.join(homedir(), '.config/envision'));
|
||||
export const configPath = path.join(configDir, 'assistant-config.json');
|
||||
export const appConfigPath = path.join(configDir, 'assistant-app-config.json');
|
||||
export const appDir = createDir(path.join(configDir, 'assistant-app/frontend'));
|
||||
export const appPidPath = path.join(configDir, 'assistant-app.pid');
|
||||
export const LocalElectronAppUrl = 'https://assistant.app/user/tiptap/';
|
||||
|
||||
type AssistantConfig = {
|
||||
pageApi?: string; // https://kevisual.silkyai.cn
|
||||
loadURL?: string; // https://assistant.app/user/tiptap/
|
||||
proxy?: { user: string; key: string; path: string }[];
|
||||
apiProxyList?: ProxyInfo[];
|
||||
};
|
||||
let assistantConfig: AssistantConfig;
|
||||
export const getConfig = () => {
|
||||
try {
|
||||
if (!checkFileExists(configPath)) {
|
||||
fs.writeFileSync(configPath, JSON.stringify({ proxy: [] }, null, 2));
|
||||
return {
|
||||
loadURL: LocalElectronAppUrl,
|
||||
pageApi: '',
|
||||
proxy: [],
|
||||
};
|
||||
}
|
||||
assistantConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
return assistantConfig;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
loadURL: LocalElectronAppUrl,
|
||||
pageApi: '',
|
||||
proxy: [],
|
||||
};
|
||||
}
|
||||
};
|
||||
export const getCacheAssistantConfig = () => {
|
||||
if (assistantConfig) {
|
||||
return assistantConfig;
|
||||
}
|
||||
return getConfig();
|
||||
};
|
||||
|
||||
export const setConfig = (config?: AssistantConfig) => {
|
||||
if (!config) {
|
||||
return assistantConfig;
|
||||
}
|
||||
assistantConfig = config;
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||
return assistantConfig;
|
||||
};
|
||||
type AppConfig = {
|
||||
list: any[];
|
||||
};
|
||||
/**
|
||||
* 应用配置
|
||||
* @returns
|
||||
*/
|
||||
export const getAppConfig = (): AppConfig => {
|
||||
if (!checkFileExists(appConfigPath)) {
|
||||
return {
|
||||
list: [],
|
||||
};
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(appConfigPath, 'utf8'));
|
||||
};
|
||||
|
||||
export const setAppConfig = (config: AppConfig) => {
|
||||
fs.writeFileSync(appConfigPath, JSON.stringify(config, null, 2));
|
||||
return config;
|
||||
};
|
||||
|
||||
export const addAppConfig = (app: any) => {
|
||||
const config = getAppConfig();
|
||||
const assistantConfig = getCacheAssistantConfig();
|
||||
const _apps = config.list;
|
||||
const _proxy = assistantConfig.proxy || [];
|
||||
const { user, key } = app;
|
||||
const newProxyInfo = {
|
||||
user,
|
||||
key,
|
||||
path: `/${user}/${key}`,
|
||||
};
|
||||
const _proxyIndex = _proxy.findIndex((_proxy: any) => _proxy.path === newProxyInfo.path);
|
||||
if (_proxyIndex !== -1) {
|
||||
_proxy[_proxyIndex] = newProxyInfo;
|
||||
} else {
|
||||
_proxy.push(newProxyInfo);
|
||||
}
|
||||
|
||||
const _app = _apps.findIndex((_app: any) => _app.id === app.id);
|
||||
if (_app !== -1) {
|
||||
_apps[_app] = app;
|
||||
} else {
|
||||
_apps.push(app);
|
||||
}
|
||||
setAppConfig({ ...config, list: _apps });
|
||||
setConfig({ ...assistantConfig, proxy: _proxy });
|
||||
return config;
|
||||
};
|
||||
|
||||
export const getAppList = () => {
|
||||
const config = getAppConfig();
|
||||
return config.list || [];
|
||||
};
|
||||
20
assistant-module/src/file/index.ts
Normal file
20
assistant-module/src/file/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import fs from 'fs';
|
||||
|
||||
export const checkFileExists = (filePath: string, checkIsFile = false) => {
|
||||
try {
|
||||
fs.accessSync(filePath);
|
||||
if (checkIsFile) {
|
||||
return fs.statSync(filePath).isFile();
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const createDir = (dirPath: string) => {
|
||||
if (!checkFileExists(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
return dirPath;
|
||||
};
|
||||
2
assistant-module/src/index.ts
Normal file
2
assistant-module/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './install/index.ts';
|
||||
export * from './config/index.ts';
|
||||
127
assistant-module/src/install/index.ts
Normal file
127
assistant-module/src/install/index.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
type DownloadTask = {
|
||||
downloadPath: string;
|
||||
downloadUrl: string;
|
||||
user: string;
|
||||
key: string;
|
||||
version: string;
|
||||
};
|
||||
export type Package = {
|
||||
id: string;
|
||||
name?: string;
|
||||
version?: string;
|
||||
description?: string;
|
||||
title?: string;
|
||||
user?: string;
|
||||
key?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
type InstallAppOpts = {
|
||||
appDir?: string;
|
||||
kevisualUrl?: string;
|
||||
/**
|
||||
* 是否是客户端, 下载到 assistant-config的下面
|
||||
*/
|
||||
};
|
||||
export const installApp = async (app: Package, opts: InstallAppOpts = {}) => {
|
||||
// const _app = demoData;
|
||||
const { appDir = '', kevisualUrl = 'https://kevisual.cn' } = opts;
|
||||
const _app = app;
|
||||
try {
|
||||
let files = _app.data.files || [];
|
||||
const version = _app.version;
|
||||
const user = _app.user;
|
||||
const key = _app.key;
|
||||
|
||||
const downFiles = files.map((file: any) => {
|
||||
const noVersionPath = file.path.replace(`/${version}`, '');
|
||||
return {
|
||||
...file,
|
||||
downloadPath: path.join(appDir, noVersionPath),
|
||||
downloadUrl: `${kevisualUrl}/${noVersionPath}`,
|
||||
};
|
||||
});
|
||||
const downloadTasks: DownloadTask[] = downFiles as any;
|
||||
for (const file of downloadTasks) {
|
||||
const downloadPath = file.downloadPath;
|
||||
const downloadUrl = file.downloadUrl;
|
||||
const dir = path.dirname(downloadPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
const res = await fetch(downloadUrl);
|
||||
const blob = await res.blob();
|
||||
fs.writeFileSync(downloadPath, Buffer.from(await blob.arrayBuffer()));
|
||||
}
|
||||
let indexHtml = files.find((file: any) => file.name === 'index.html');
|
||||
if (!indexHtml) {
|
||||
files.push({
|
||||
name: 'index.html',
|
||||
path: `${user}/${key}/index.html`,
|
||||
});
|
||||
fs.writeFileSync(path.join(appDir, `${user}/${key}/index.html`), JSON.stringify(app, null, 2));
|
||||
}
|
||||
_app.data.files = files;
|
||||
return {
|
||||
code: 200,
|
||||
data: _app,
|
||||
message: 'Install app success',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
code: 500,
|
||||
message: 'Install app failed',
|
||||
};
|
||||
}
|
||||
};
|
||||
export const checkAppDir = (appDir: string) => {
|
||||
const files = fs.readdirSync(appDir);
|
||||
if (files.length === 0) {
|
||||
fs.rmSync(appDir, { recursive: true });
|
||||
}
|
||||
};
|
||||
export const checkFileExists = (path: string) => {
|
||||
try {
|
||||
fs.accessSync(path);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
type UninstallAppOpts = {
|
||||
appDir?: string;
|
||||
};
|
||||
export const uninstallApp = async (app: Partial<Package>, opts: UninstallAppOpts = {}) => {
|
||||
const { appDir = '' } = opts;
|
||||
try {
|
||||
const { user, key } = app;
|
||||
const keyDir = path.join(appDir, user, key);
|
||||
const parentDir = path.join(appDir, user);
|
||||
if (!checkFileExists(appDir) || !checkFileExists(keyDir)) {
|
||||
return {
|
||||
code: 200,
|
||||
message: 'uninstall app success',
|
||||
};
|
||||
}
|
||||
try {
|
||||
// 删除appDir和文件
|
||||
fs.rmSync(keyDir, { recursive: true });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
checkAppDir(parentDir);
|
||||
return {
|
||||
code: 200,
|
||||
message: 'Uninstall app success',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
code: 500,
|
||||
message: 'Uninstall app failed',
|
||||
};
|
||||
}
|
||||
};
|
||||
70
assistant-module/src/process/index.ts
Normal file
70
assistant-module/src/process/index.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { ChildProcess, fork } from 'child_process';
|
||||
|
||||
export const runProcess = (appPath: string) => {
|
||||
const process = fork(appPath);
|
||||
process.on('exit', (code) => {
|
||||
console.log(`Process exited with code ${code}`);
|
||||
});
|
||||
|
||||
process.on('message', (message) => {
|
||||
console.log('Message from child:', message);
|
||||
});
|
||||
|
||||
// Example of sending a message to the child process
|
||||
// process.send({ hello: 'world' });
|
||||
};
|
||||
class BaseProcess {
|
||||
private process: ChildProcess;
|
||||
status: 'running' | 'stopped' | 'error' = 'stopped';
|
||||
appPath: string;
|
||||
constructor(appPath: string) {
|
||||
this.appPath = appPath;
|
||||
// this.createProcess(appPath);
|
||||
}
|
||||
createProcess(appPath: string = this.appPath) {
|
||||
if (this.process) {
|
||||
this.process.kill();
|
||||
}
|
||||
this.appPath = appPath;
|
||||
this.process = fork(appPath);
|
||||
return this;
|
||||
}
|
||||
kill(signal?: NodeJS.Signals | number) {
|
||||
if (this.process) {
|
||||
this.process.kill(signal);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
public send(message: any) {
|
||||
this.process.send(message);
|
||||
}
|
||||
|
||||
public on(event: string, callback: (message: any) => void) {
|
||||
this.process.on(event, callback);
|
||||
}
|
||||
|
||||
public onExit(callback: (code: number) => void) {
|
||||
this.process.on('exit', callback);
|
||||
}
|
||||
|
||||
public onError(callback: (error: Error) => void) {
|
||||
this.process.on('error', callback);
|
||||
}
|
||||
|
||||
public onMessage(callback: (message: any) => void) {
|
||||
this.process.on('message', callback);
|
||||
}
|
||||
|
||||
public onClose(callback: () => void) {
|
||||
this.process.on('close', callback);
|
||||
}
|
||||
|
||||
public onDisconnect(callback: () => void) {
|
||||
this.process.on('disconnect', callback);
|
||||
}
|
||||
}
|
||||
export class AssistantProcess extends BaseProcess {
|
||||
constructor(appPath: string) {
|
||||
super(appPath);
|
||||
}
|
||||
}
|
||||
82
assistant-module/src/proxy/api-proxy.ts
Normal file
82
assistant-module/src/proxy/api-proxy.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
|
||||
import { ProxyInfo } from './proxy.ts';
|
||||
export const defaultApiProxy = [
|
||||
{
|
||||
path: '/api/router',
|
||||
target: 'https://kevisual.xiongxiao.me',
|
||||
},
|
||||
{
|
||||
path: '/v1',
|
||||
target: 'https://kevisual.xiongxiao.me',
|
||||
},
|
||||
];
|
||||
/**
|
||||
* 创建api代理
|
||||
* @param api
|
||||
* @param paths ['/api/router', '/v1' ]
|
||||
* @returns
|
||||
*/
|
||||
export const createApiProxy = (api: string, paths: string[] = ['/api/router', '/v1']) => {
|
||||
const pathList = paths.map((item) => {
|
||||
return {
|
||||
path: item,
|
||||
target: new URL(api).origin,
|
||||
};
|
||||
});
|
||||
return pathList;
|
||||
};
|
||||
|
||||
export const apiProxy = (req: http.IncomingMessage, res: http.ServerResponse, proxyApi: ProxyInfo) => {
|
||||
const _u = new URL(req.url, `${proxyApi.target}`);
|
||||
console.log('proxyApi', req.url, _u.href);
|
||||
// 设置代理请求的目标 URL 和请求头
|
||||
let header: any = {};
|
||||
if (req.headers?.['Authorization'] && !req.headers?.['authorization']) {
|
||||
header.authorization = req.headers['Authorization'];
|
||||
}
|
||||
// 提取req的headers中的非HOST的header
|
||||
const headers = Object.keys(req.headers).filter((item) => item && item.toLowerCase() !== 'host');
|
||||
headers.forEach((item) => {
|
||||
if (item.toLowerCase() === 'origin') {
|
||||
header.origin = new URL(proxyApi.target).origin;
|
||||
return;
|
||||
}
|
||||
if (item.toLowerCase() === 'referer') {
|
||||
header.referer = new URL(req.url, proxyApi.target).href;
|
||||
return;
|
||||
}
|
||||
header[item] = req.headers[item];
|
||||
});
|
||||
const options = {
|
||||
host: _u.hostname,
|
||||
path: req.url,
|
||||
method: req.method,
|
||||
headers: {
|
||||
...header,
|
||||
},
|
||||
};
|
||||
console.log('options', JSON.stringify(options, null, 2));
|
||||
if (_u.port) {
|
||||
// @ts-ignore
|
||||
options.port = _u.port;
|
||||
}
|
||||
const httpProxy = _u.protocol === 'https:' ? https : http;
|
||||
// 创建代理请求
|
||||
const proxyReq = httpProxy.request(options, (proxyRes) => {
|
||||
// 将代理服务器的响应头和状态码返回给客户端
|
||||
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
||||
// 将代理响应流写入客户端响应
|
||||
proxyRes.pipe(res, { end: true });
|
||||
});
|
||||
// 处理代理请求的错误事件
|
||||
proxyReq.on('error', (err) => {
|
||||
console.error(`Proxy request error: ${err.message}`);
|
||||
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
||||
res.write(`Proxy request error: ${err.message}`);
|
||||
});
|
||||
// 处理 POST 请求的请求体(传递数据到目标服务器),end:true 表示当请求体结束时,关闭请求
|
||||
req.pipe(proxyReq, { end: true });
|
||||
return;
|
||||
};
|
||||
47
assistant-module/src/proxy/file-proxy.ts
Normal file
47
assistant-module/src/proxy/file-proxy.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import http from 'http';
|
||||
import send from 'send';
|
||||
import fs from 'fs';
|
||||
import { fileIsExist } from '@kevisual/use-config';
|
||||
import path from 'path';
|
||||
import { ProxyInfo } from './proxy.ts';
|
||||
|
||||
export const fileProxy = (req: http.IncomingMessage, res: http.ServerResponse, proxyApi: ProxyInfo) => {
|
||||
// url开头的文件
|
||||
const url = new URL(req.url, 'http://localhost');
|
||||
let pathname = url.pathname.slice(1);
|
||||
const { indexPath = '', target = '', rootPath = process.cwd() } = proxyApi;
|
||||
try {
|
||||
if (pathname.endsWith('/')) {
|
||||
pathname = pathname + 'index.html';
|
||||
}
|
||||
// 检测文件是否存在,如果文件不存在,则返回404
|
||||
let filePath = path.join(rootPath, target, pathname);
|
||||
let exist = fileIsExist(filePath);
|
||||
if (!exist) {
|
||||
filePath = path.join(rootPath, target, '/' + indexPath);
|
||||
exist = fileIsExist(filePath);
|
||||
}
|
||||
console.log('filePath', filePath, exist);
|
||||
|
||||
if (!exist) {
|
||||
res.statusCode = 404;
|
||||
res.end('Not Found File');
|
||||
return;
|
||||
}
|
||||
const ext = path.extname(filePath);
|
||||
let maxAge = 24 * 60 * 60 * 1000; // 24小时
|
||||
if (ext === '.html') {
|
||||
maxAge = 0;
|
||||
}
|
||||
let sendFilePath = filePath.replace(rootPath + '/', '');
|
||||
const file = send(req, sendFilePath, {
|
||||
root: rootPath,
|
||||
maxAge,
|
||||
});
|
||||
file.pipe(res);
|
||||
} catch (error) {
|
||||
res.statusCode = 404;
|
||||
res.end('Error:Not Found File');
|
||||
return;
|
||||
}
|
||||
};
|
||||
5
assistant-module/src/proxy/index.ts
Normal file
5
assistant-module/src/proxy/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './proxy.ts';
|
||||
export * from './file-proxy.ts';
|
||||
export { default as send } from 'send';
|
||||
export * from './api-proxy.ts';
|
||||
export * from './wx-proxy.ts';
|
||||
35
assistant-module/src/proxy/proxy.ts
Normal file
35
assistant-module/src/proxy/proxy.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export type ProxyInfo = {
|
||||
path?: string;
|
||||
target?: string;
|
||||
type?: 'static' | 'dynamic' | 'minio';
|
||||
/**
|
||||
* 首要文件,比如index.html, 设置了首要文件,如果文件不存在,则访问首要文件
|
||||
*/
|
||||
indexPath?: string;
|
||||
/**
|
||||
* 根路径, 默认是process.cwd()
|
||||
*/
|
||||
rootPath?: string;
|
||||
};
|
||||
export type ApiList = {
|
||||
path: string;
|
||||
/**
|
||||
* url或者相对路径
|
||||
*/
|
||||
target: string;
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
type?: 'static' | 'dynamic' | 'minio';
|
||||
}[];
|
||||
|
||||
/**
|
||||
|
||||
[
|
||||
{
|
||||
path: '/api/v1/user',
|
||||
target: 'http://localhost:3000/api/v1/user',
|
||||
type: 'dynamic',
|
||||
},
|
||||
]
|
||||
*/
|
||||
48
assistant-module/src/proxy/wx-proxy.ts
Normal file
48
assistant-module/src/proxy/wx-proxy.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Server } from 'http';
|
||||
import WebSocket from 'ws';
|
||||
/**
|
||||
* websocket代理
|
||||
* apiList: [{ path: '/api/router', target: 'https://kevisual.xiongxiao.me' }]
|
||||
* @param server
|
||||
* @param config
|
||||
*/
|
||||
export const wsProxy = (server: Server, config: { apiList: any[] }) => {
|
||||
console.log('Upgrade initialization started');
|
||||
|
||||
server.on('upgrade', (req, socket, head) => {
|
||||
const proxyApiList = config?.apiList || [];
|
||||
const proxyApi = proxyApiList.find((item) => req.url.startsWith(item.path));
|
||||
|
||||
if (proxyApi) {
|
||||
const _u = new URL(req.url, `${proxyApi.target}`);
|
||||
const isHttps = _u.protocol === 'https:';
|
||||
const wsProtocol = isHttps ? 'wss' : 'ws';
|
||||
const wsUrl = `${wsProtocol}://${_u.hostname}${_u.pathname}`;
|
||||
|
||||
const proxySocket = new WebSocket(wsUrl, {
|
||||
headers: req.headers,
|
||||
});
|
||||
|
||||
proxySocket.on('open', () => {
|
||||
socket.on('data', (data) => {
|
||||
proxySocket.send(data);
|
||||
});
|
||||
|
||||
proxySocket.on('message', (message) => {
|
||||
socket.write(message);
|
||||
});
|
||||
});
|
||||
|
||||
proxySocket.on('error', (err) => {
|
||||
console.error(`WebSocket proxy error: ${err.message}`);
|
||||
socket.end();
|
||||
});
|
||||
|
||||
socket.on('error', () => {
|
||||
proxySocket.close();
|
||||
});
|
||||
} else {
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user