generated from tailored/router-db-template
base module
This commit is contained in:
13
src/app.ts
13
src/app.ts
@@ -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);
|
||||
|
||||
|
||||
|
||||
@@ -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');
|
||||
18
src/dev.ts
18
src/dev.ts
@@ -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);
|
||||
|
||||
@@ -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
9
src/modules/config.ts
Normal 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
108
src/modules/config/index.ts
Normal 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
20
src/modules/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;
|
||||
};
|
||||
156
src/modules/install.ts
Normal file
156
src/modules/install.ts
Normal 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
61
src/proxy-route/index.ts
Normal 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',
|
||||
},
|
||||
];
|
||||
42
src/proxy-route/ws-proxy.ts
Normal file
42
src/proxy-route/ws-proxy.ts
Normal 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
10
src/route/client/check.ts
Normal 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
25
src/route/config/index.ts
Normal 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
3
src/route/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import './shop-install/index.ts';
|
||||
import './client/check.ts';
|
||||
import './config/index.ts';
|
||||
40
src/route/shop-install/index.ts
Normal file
40
src/route/shop-install/index.ts
Normal 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);
|
||||
8
src/scripts/assistant-config.ts
Normal file
8
src/scripts/assistant-config.ts
Normal 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);
|
||||
Reference in New Issue
Block a user