base module

This commit is contained in:
2025-03-10 10:21:14 +08:00
parent 479eaccf57
commit b966ea68f2
40 changed files with 1594 additions and 73 deletions

View File

@@ -0,0 +1 @@
export * from './config/index.ts';

View File

@@ -0,0 +1 @@
export * from './process/index.ts';

View File

@@ -0,0 +1 @@
export * from './proxy/index.ts';

View 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 || [];
};

View 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;
};

View File

@@ -0,0 +1,2 @@
export * from './install/index.ts';
export * from './config/index.ts';

View 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',
};
}
};

View 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);
}
}

View 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;
};

View 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;
}
};

View 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';

View 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',
},
]
*/

View 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();
}
});
};