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

@@ -1,8 +1,19 @@
import { App } from '@kevisual/router';
import { useContextKey } from '@kevisual/use-config/context';
import { httpsConfig } from './modules/config.ts';
const init = () => {
return new App();
const app = new App({
serverOptions: {
path: '/client/router',
httpType: 'https',
httpsCert: httpsConfig.cert.toString(),
httpsKey: httpsConfig.key.toString(),
},
});
return app;
};
export const app = useContextKey('app', init);

View File

@@ -1,16 +0,0 @@
import { app } from './app.ts';
import { useConfig } from '@kevisual/use-config';
app
.route({
path: 'demo',
key: 'demo',
})
.define(async (ctx) => {
ctx.body = '123';
})
.addTo(app);
const config = useConfig();
console.log('run demo: http://localhost:' + config.port + '/api/router?path=demo&key=demo');

View File

@@ -1,8 +1,18 @@
import { useConfig } from '@kevisual/use-config';
import { app } from './index.ts';
import { proxyRoute } from './proxy-route/index.ts';
const config = useConfig();
app
.route({
path: 'demo',
})
.define(async (ctx) => {
ctx.body = 'hello world';
})
.addTo(app);
app.listen(config.port, () => {
console.log(`server is running at http://localhost:${config.port}`);
console.log('httpsConfig', `https://localhost:51015/client/router?path=demo`);
app.listen(51015, () => {
console.log('Router App is running on https://localhost:51015');
});
app.server.on(proxyRoute);

View File

@@ -1,4 +1,4 @@
import { app } from './app.ts';
import './demo-route.ts';
import './route/index.ts';
export { app };

9
src/modules/config.ts Normal file
View File

@@ -0,0 +1,9 @@
import fs from 'fs';
import path from 'path';
const pemDir = path.join(process.cwd(), 'pem');
export const httpsConfig = {
key: fs.readFileSync(path.join(pemDir, 'https-key.pem')),
cert: fs.readFileSync(path.join(pemDir, 'https-cert.pem')),
};

108
src/modules/config/index.ts Normal file
View File

@@ -0,0 +1,108 @@
import path from 'path';
import { homedir } from 'os';
import fs from 'fs';
import { checkFileExists, createDir } from '../file/index.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 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 }[];
};
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
src/modules/file/index.ts Normal file
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;
};

156
src/modules/install.ts Normal file
View File

@@ -0,0 +1,156 @@
import path from 'path';
import fs from 'fs';
import { appDir, kevisualUrl, addAppConfig, getAppConfig, setAppConfig, getCacheAssistantConfig, setConfig } from './config/index.ts';
export const demoData = {
id: '471ee96f-d7d8-4da1-b84f-4a34f4732f16',
title: 'tiptap',
description: '',
data: {
files: [
{
name: 'README.md',
path: 'user/tiptap/0.0.1/README.md',
},
{
name: 'app.css',
path: 'user/tiptap/0.0.1/app.css',
},
{
name: 'app.js',
path: 'user/tiptap/0.0.1/app.js',
},
{
name: 'create-BxEwtceK.js',
path: 'user/tiptap/0.0.1/create-BxEwtceK.js',
},
{
name: 'index.CrTXFMOJ.js',
path: 'user/tiptap/0.0.1/index.CrTXFMOJ.js',
},
{
name: 'index.html',
path: 'user/tiptap/0.0.1/index.html',
},
],
},
version: '0.0.1',
domain: '',
appType: '',
key: 'tiptap',
type: '',
uid: '2bebe6a0-3c64-4a64-89f9-cc47fd082a07',
pid: null,
proxy: false,
user: 'user',
status: 'running',
createdAt: '2024-12-14T15:39:30.684Z',
updatedAt: '2024-12-14T15:39:55.714Z',
deletedAt: null,
};
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;
};
export const installApp = async (app: Package) => {
// const _app = demoData;
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;
addAppConfig(_app);
return {
code: 200,
data: _app,
message: 'Install app success',
};
} catch (error) {
console.error(error);
return {
code: 500,
message: 'Install app failed',
};
}
};
export const uninstallApp = async (app: Package) => {
try {
const { user, key } = app;
const appConfig = getAppConfig();
const index = appConfig.list.findIndex((item: any) => item.user === user && item.key === key);
if (index !== -1) {
appConfig.list.splice(index, 1);
setAppConfig(appConfig);
// 删除appDir和文件
fs.rmSync(path.join(appDir, user, key), { recursive: true });
// 删除proxy
const proxyConfig = getCacheAssistantConfig();
const proxyIndex = proxyConfig.proxy.findIndex((item: any) => item.user === user && item.key === key);
if (proxyIndex !== -1) {
proxyConfig.proxy.splice(proxyIndex, 1);
setConfig(proxyConfig);
}
}
return {
code: 200,
message: 'Uninstall app success',
};
} catch (error) {
console.error(error);
return {
code: 500,
message: 'Uninstall app failed',
};
}
};
export const getInstallList = async () => {
const appConfig = getAppConfig();
return appConfig.list;
};

61
src/proxy-route/index.ts Normal file
View File

@@ -0,0 +1,61 @@
import { fileProxy, apiProxy, createApiProxy } from '@kevisual/assistant-module/proxy';
import { getCacheAssistantConfig, appDir } from '@kevisual/assistant-module';
import http from 'http';
// https://localhost:51015/user/tiptap/
export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResponse) => {
const assistantConfig = getCacheAssistantConfig();
// const { apiList } = assistantConfig;
const url = new URL(req.url, 'http://localhost');
const pathname = url.pathname;
if (pathname.startsWith('/favicon.ico')) {
res.statusCode = 404;
res.end('Not Found Favicon');
return;
}
if (pathname.startsWith('/client')) {
console.log('handle by router');
return;
}
const apiProxyList = assistantConfig?.apiProxyList || [];
const defaultApiProxy = createApiProxy(assistantConfig?.pageApi || 'https://kevisual.xiongxiao.me');
const apiBackendProxy = [...apiProxyList, ...defaultApiProxy].find((item) => pathname.startsWith(item.path));
if (apiBackendProxy) {
console.log('apiBackendProxy', apiBackendProxy);
return apiProxy(req, res, {
path: apiBackendProxy.path,
target: apiBackendProxy.target,
});
}
// client, api, v1, serve 开头的拦截
const proxyApiList = assistantConfig?.proxy || [];
const proxyApi = proxyApiList.find((item) => pathname.startsWith(item.path));
if (proxyApi) {
console.log('proxyApi', proxyApi, pathname);
const { user, key } = proxyApi;
return fileProxy(req, res, {
path: proxyApi.path,
rootPath: appDir,
indexPath: `${user}/${key}/index.html`,
});
}
const localProxyProxy = localProxyProxyList.find((item) => pathname.startsWith(item.path));
if (localProxyProxy) {
return fileProxy(req, res, {
path: localProxyProxy.path,
rootPath: process.cwd(),
indexPath: localProxyProxy.indexPath,
});
}
console.log('handle by router 404');
res.statusCode = 404;
res.end('Not Found Proxy');
};
const localProxyProxyList = [
{
user: 'root',
key: 'assistant-base-app',
path: '/root/assistant-base-app',
indexPath: 'root/assistant-base-app/index.html',
},
];

View File

@@ -0,0 +1,42 @@
import net from 'net';
import { App } from '@kevisual/router';
export const wsProxy = (app: App, config: { apiList: any[] }) => {
console.log('Upgrade initialization started');
app.server.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 options = {
hostname: _u.hostname,
port: Number(_u.port) || 80,
path: _u.pathname,
headers: req.headers,
};
const proxySocket = net.connect(options.port, options.hostname, () => {
proxySocket.write(
`GET ${options.path} HTTP/1.1\r\n` +
`Host: ${options.hostname}\r\n` +
`Connection: Upgrade\r\n` +
`Upgrade: websocket\r\n` +
`Sec-WebSocket-Key: ${req.headers['sec-websocket-key']}\r\n` +
`Sec-WebSocket-Version: ${req.headers['sec-websocket-version']}\r\n` +
`\r\n`,
);
proxySocket.pipe(socket);
socket.pipe(proxySocket);
});
proxySocket.on('error', (err) => {
console.error(`WebSocket proxy error: ${err.message}`);
socket.end();
});
} else {
socket.end();
}
});
};

10
src/route/client/check.ts Normal file
View File

@@ -0,0 +1,10 @@
import { app } from '@/app.ts';
app
.route({
path: 'check',
})
.define(async (ctx) => {
ctx.body = 'ok';
})
.addTo(app);

25
src/route/config/index.ts Normal file
View File

@@ -0,0 +1,25 @@
import { app } from '@/app.ts';
import { getCacheAssistantConfig, setConfig } from '@/modules/config/index.ts';
app
.route({
path: 'config',
description: '获取配置',
})
.define(async (ctx) => {
ctx.body = getCacheAssistantConfig();
})
.addTo(app);
app
.route({
path: 'config',
key: 'set',
description: '设置配置',
})
.define(async (ctx) => {
const { data } = ctx.query;
ctx.body = setConfig(data);
})
.addTo(app);

3
src/route/index.ts Normal file
View File

@@ -0,0 +1,3 @@
import './shop-install/index.ts';
import './client/check.ts';
import './config/index.ts';

View File

@@ -0,0 +1,40 @@
import { app } from '@/app.ts';
import { getInstallList, installApp, uninstallApp } from '@/modules/install.ts';
app
.route({
path: 'shop',
key: 'list-installed',
})
.define(async (ctx) => {
// https://localhost:51015/client/router?path=shop&key=list-installed
const list = await getInstallList();
ctx.body = list;
})
.addTo(app);
app
.route({
path: 'shop',
key: 'install',
})
.define(async (ctx) => {
// https://localhost:51015/client/router?path=shop&key=install
const { pkg } = ctx.query.data;
const res = await installApp(pkg);
ctx.body = res;
})
.addTo(app);
app
.route({
path: 'shop',
key: 'uninstall',
})
.define(async (ctx) => {
// https://localhost:51015/client/router?path=shop&key=uninstall
const { pkg } = ctx.query.data;
const res = await uninstallApp(pkg);
ctx.body = res;
})
.addTo(app);

View File

@@ -0,0 +1,8 @@
import { getCacheAssistantConfig, appConfigPath, appDir } from '@kevisual/assistant-module';
import fs from 'fs';
const assistantConfig = getCacheAssistantConfig();
console.log(assistantConfig);
console.log('appConfigPath', appConfigPath);
console.log('appDir', appDir);