generated from tailored/router-db-template
base module
This commit is contained in:
2
assistant-module/.npmrc
Normal file
2
assistant-module/.npmrc
Normal file
@@ -0,0 +1,2 @@
|
||||
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
|
||||
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
||||
49
assistant-module/package.json
Normal file
49
assistant-module/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@kevisual/assistant-module",
|
||||
"version": "0.0.3",
|
||||
"description": "assistant module",
|
||||
"main": "dist/assistant-module.mjs",
|
||||
"types": "dist/assistant-module.d.ts",
|
||||
"scripts": {
|
||||
"dev": "rollup -c -w",
|
||||
"build": "npm run clean && rollup -c",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "abearxiong <xiongxiao@xiongxiao.me>",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/send": "^0.17.4",
|
||||
"@types/ws": "^8.18.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/assistant-module.mjs",
|
||||
"types": "./dist/assistant-module.d.ts"
|
||||
},
|
||||
"./proxy": {
|
||||
"import": "./dist/assistant-proxy.mjs",
|
||||
"types": "./dist/assistant-proxy.d.ts"
|
||||
},
|
||||
"./assistant-config": {
|
||||
"import": "./dist/assistant-config.mjs",
|
||||
"types": "./dist/assistant-config.d.ts"
|
||||
},
|
||||
"./assistant-process": {
|
||||
"import": "./dist/assistant-process.mjs",
|
||||
"types": "./dist/assistant-process.d.ts"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"send": "^1.1.0",
|
||||
"ws": "^8.18.1"
|
||||
}
|
||||
}
|
||||
29
assistant-module/pnpm-lock.yaml
generated
Normal file
29
assistant-module/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,29 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.13.10
|
||||
version: 22.13.10
|
||||
|
||||
packages:
|
||||
|
||||
'@types/node@22.13.10':
|
||||
resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==}
|
||||
|
||||
undici-types@6.20.0:
|
||||
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@types/node@22.13.10':
|
||||
dependencies:
|
||||
undici-types: 6.20.0
|
||||
|
||||
undici-types@6.20.0: {}
|
||||
172
assistant-module/rollup.config.mjs
Normal file
172
assistant-module/rollup.config.mjs
Normal file
@@ -0,0 +1,172 @@
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import json from '@rollup/plugin-json';
|
||||
import path from 'path';
|
||||
import esbuild from 'rollup-plugin-esbuild';
|
||||
import alias from '@rollup/plugin-alias';
|
||||
import replace from '@rollup/plugin-replace';
|
||||
import dts from 'rollup-plugin-dts';
|
||||
// @ts-ignore
|
||||
import pkgs from './package.json' with {type: 'json'};
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const input = './src/index.ts';
|
||||
/**
|
||||
* @type {import('rollup').RollupOptions}
|
||||
*/
|
||||
const config = {
|
||||
input,
|
||||
output: {
|
||||
dir: './dist',
|
||||
entryFileNames: 'assistant-module.mjs',
|
||||
chunkFileNames: '[name]-[hash].mjs',
|
||||
format: 'esm',
|
||||
},
|
||||
plugins: [
|
||||
replace({
|
||||
preventAssignment: true, // 防止意外赋值
|
||||
DEV_SERVER: JSON.stringify(isDev), // 替换 process.env.NODE_ENV
|
||||
VERSION: JSON.stringify(pkgs.version),
|
||||
}),
|
||||
alias({
|
||||
// only esbuild needs to be configured
|
||||
entries: [
|
||||
{ find: '@', replacement: path.resolve('src') }, // 配置 @ 为 src 目录
|
||||
{ find: 'http', replacement: 'node:http' },
|
||||
{ find: 'https', replacement: 'node:https' },
|
||||
{ find: 'fs', replacement: 'node:fs' },
|
||||
{ find: 'path', replacement: 'node:path' },
|
||||
{ find: 'crypto', replacement: 'node:crypto' },
|
||||
{ find: 'zlib', replacement: 'node:zlib' },
|
||||
{ find: 'stream', replacement: 'node:stream' },
|
||||
{ find: 'net', replacement: 'node:net' },
|
||||
{ find: 'tty', replacement: 'node:tty' },
|
||||
{ find: 'tls', replacement: 'node:tls' },
|
||||
{ find: 'buffer', replacement: 'node:buffer' },
|
||||
{ find: 'timers', replacement: 'node:timers' },
|
||||
// { find: 'string_decoder', replacement: 'node:string_decoder' },
|
||||
{ find: 'dns', replacement: 'node:dns' },
|
||||
{ find: 'domain', replacement: 'node:domain' },
|
||||
{ find: 'os', replacement: 'node:os' },
|
||||
{ find: 'events', replacement: 'node:events' },
|
||||
{ find: 'url', replacement: 'node:url' },
|
||||
{ find: 'assert', replacement: 'node:assert' },
|
||||
{ find: 'util', replacement: 'node:util' },
|
||||
],
|
||||
}),
|
||||
resolve({
|
||||
preferBuiltins: true, // 强制优先使用内置模块
|
||||
}),
|
||||
commonjs(),
|
||||
esbuild({
|
||||
target: 'node22', //
|
||||
minify: false, // 启用代码压缩
|
||||
tsconfig: 'tsconfig.json',
|
||||
}),
|
||||
json(),
|
||||
],
|
||||
external: [
|
||||
/@kevisual\/router(\/.*)?/, //, // 路由
|
||||
/@kevisual\/use-config(\/.*)?/, //
|
||||
],
|
||||
};
|
||||
const dtsConfig = [{
|
||||
input,
|
||||
output: {
|
||||
file: 'dist/assistant-module.d.ts',
|
||||
format: 'esm',
|
||||
},
|
||||
plugins: [dts()],
|
||||
}];
|
||||
|
||||
const moduleConfig = {
|
||||
input: './src/assistant-proxy.ts',
|
||||
output: {
|
||||
file: 'dist/assistant-proxy.mjs',
|
||||
format: 'esm',
|
||||
},
|
||||
plugins: [
|
||||
alias({
|
||||
entries: [{ find: '@', replacement: path.resolve('src') }],
|
||||
}),
|
||||
resolve({
|
||||
preferBuiltins: true, // 强制优先使用内置模块
|
||||
}),
|
||||
commonjs(),
|
||||
esbuild({
|
||||
target: 'node22', //
|
||||
}),
|
||||
json(),
|
||||
],
|
||||
|
||||
};
|
||||
const moduleDtsConfig = [{
|
||||
input: './src/assistant-proxy.ts',
|
||||
output: {
|
||||
file: 'dist/assistant-proxy.d.ts',
|
||||
format: 'esm',
|
||||
},
|
||||
plugins: [dts()],
|
||||
}];
|
||||
|
||||
const assistantConfigConfig = {
|
||||
input: './src/assistant-config.ts',
|
||||
output: {
|
||||
file: 'dist/assistant-config.mjs',
|
||||
format: 'esm',
|
||||
},
|
||||
plugins: [
|
||||
alias({
|
||||
entries: [{ find: '@', replacement: path.resolve('src') }],
|
||||
}),
|
||||
resolve({
|
||||
preferBuiltins: true, // 强制优先使用内置模块
|
||||
}),
|
||||
commonjs(),
|
||||
esbuild({
|
||||
target: 'node22', //
|
||||
}),
|
||||
json(),
|
||||
],
|
||||
};
|
||||
const assistantConfigDtsConfig = [{
|
||||
input: './src/assistant-config.ts',
|
||||
output: {
|
||||
file: 'dist/assistant-config.d.ts',
|
||||
format: 'esm',
|
||||
},
|
||||
plugins: [dts()],
|
||||
}];
|
||||
|
||||
const assistantProcessConfig = {
|
||||
input: './src/assistant-process.ts',
|
||||
output: {
|
||||
file: 'dist/assistant-process.mjs',
|
||||
format: 'esm',
|
||||
},
|
||||
plugins: [
|
||||
alias({
|
||||
entries: [{ find: '@', replacement: path.resolve('src') }],
|
||||
}),
|
||||
resolve({
|
||||
preferBuiltins: true, // 强制优先使用内置模块
|
||||
}),
|
||||
commonjs(),
|
||||
esbuild({
|
||||
target: 'node22', //
|
||||
}),
|
||||
json(),
|
||||
],
|
||||
};
|
||||
const assistantProcessDtsConfig = [{
|
||||
input: './src/assistant-process.ts',
|
||||
output: {
|
||||
file: 'dist/assistant-process.d.ts',
|
||||
format: 'esm',
|
||||
},
|
||||
plugins: [dts()],
|
||||
}];
|
||||
|
||||
|
||||
|
||||
export default [config, ...dtsConfig, moduleConfig, ...moduleDtsConfig, assistantConfigConfig, ...assistantConfigDtsConfig, assistantProcessConfig, ...assistantProcessDtsConfig];
|
||||
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();
|
||||
}
|
||||
});
|
||||
};
|
||||
33
assistant-module/tsconfig.json
Normal file
33
assistant-module/tsconfig.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "nodenext",
|
||||
"target": "esnext",
|
||||
"noImplicitAny": false,
|
||||
"outDir": "./dist",
|
||||
"sourceMap": false,
|
||||
"allowJs": true,
|
||||
"newLine": "LF",
|
||||
"baseUrl": "./",
|
||||
"typeRoots": [
|
||||
"node_modules/@types",
|
||||
"node_modules/@kevisual/types"
|
||||
],
|
||||
"declaration": true,
|
||||
"noEmit": false,
|
||||
"allowImportingTsExtensions": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"moduleResolution": "NodeNext",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"esModuleInterop": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
],
|
||||
"exclude": [],
|
||||
}
|
||||
Reference in New Issue
Block a user