temp
This commit is contained in:
parent
6827945446
commit
9eb4d06939
8
assistant/.gitignore
vendored
Normal file
8
assistant/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
|
||||
dist
|
||||
|
||||
pack-dist
|
||||
|
||||
assistant-app
|
19
assistant/bun.config.mjs
Normal file
19
assistant/bun.config.mjs
Normal file
@ -0,0 +1,19 @@
|
||||
// @ts-check
|
||||
// https://bun.sh/docs/bundler
|
||||
// @ts-ignore
|
||||
import pkg from './package.json';
|
||||
// bun run src/index.ts --
|
||||
await Bun.build({
|
||||
target: 'node',
|
||||
format: 'esm',
|
||||
entrypoints: ['./src/index.ts'],
|
||||
outdir: './dist',
|
||||
naming: {
|
||||
entry: 'assistant.mjs',
|
||||
},
|
||||
|
||||
define: {
|
||||
ENVISION_VERSION: JSON.stringify(pkg.version),
|
||||
},
|
||||
env: 'ENVISION_*',
|
||||
});
|
48
assistant/package.json
Normal file
48
assistant/package.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "@kevisual/assistant-cli",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "dist/assistant.mjs",
|
||||
"keywords": [
|
||||
"kevisual",
|
||||
"cli",
|
||||
"assistant"
|
||||
],
|
||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@10.7.0",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist",
|
||||
"bin",
|
||||
"bun.config.mjs"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "bun run src/run.ts ",
|
||||
"dev:serve": "bun --watch src/serve.ts",
|
||||
"build": "rimraf dist && bun run bun.config.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kevisual/load": "^0.0.6",
|
||||
"@kevisual/query": "0.0.17",
|
||||
"@kevisual/query-login": "0.0.5",
|
||||
"@kevisual/router": "^0.0.13",
|
||||
"@kevisual/use-config": "^1.0.11",
|
||||
"@types/bun": "^1.2.10",
|
||||
"@types/node": "^22.14.1",
|
||||
"@types/send": "^0.17.4",
|
||||
"chalk": "^5.4.1",
|
||||
"commander": "^13.1.0",
|
||||
"inquirer": "^12.5.2",
|
||||
"pino": "^9.6.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"send": "^1.2.0",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
15
assistant/pem/https-cert.pem
Normal file
15
assistant/pem/https-cert.pem
Normal file
@ -0,0 +1,15 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICXTCCAcagAwIBAgIJUmN5oWFZdxK8MA0GCSqGSIb3DQEBBQUAMF8xCjAIBgNV
|
||||
BAMTASoxCzAJBgNVBAYTAkNOMREwDwYDVQQIEwhaaGVKaWFuZzERMA8GA1UEBxMI
|
||||
SGFuZ3pob3UxETAPBgNVBAoTCEVudmlzaW9uMQswCQYDVQQLEwJJVDAeFw0yNTA0
|
||||
MjQxODExMjZaFw0yNjA0MjQxODExMjZaMF8xCjAIBgNVBAMTASoxCzAJBgNVBAYT
|
||||
AkNOMREwDwYDVQQIEwhaaGVKaWFuZzERMA8GA1UEBxMISGFuZ3pob3UxETAPBgNV
|
||||
BAoTCEVudmlzaW9uMQswCQYDVQQLEwJJVDCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
|
||||
gYkCgYEAirpqS9Lwh5JNY7N303wphXCR/HZDgfw1HnP6b62WVJTHtU97hLKjrTXx
|
||||
zUYPEyySXLzFGjptKSjT3ZgulV1I9YBXg2gdDibxxxZUZHoJ8j0oh+MSxRv1fTzw
|
||||
+HEBErUJQJ4lHnf9nbi7Tf48XiNWqh9Lce3XvyDFQoRDASX5yeUCAwEAAaMhMB8w
|
||||
HQYDVR0RBBYwFIIBKoIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3DQEBBQUAA4GB
|
||||
AGCYapPzhY0zUVZxo6CsijdDQpuHe2G3cDs4bzpF2YHRGN3t8/cPwROt7FWCkzBt
|
||||
b7g/Tar+200fGspmLS95QisjiKo0fAKfaEE8CHXr2jlt8+omOz0tPg9LCZi2GtgI
|
||||
8EC+Vvvcd9UjzHmoPBZQF4qAvJ2IyOwBh6Vwyh8las+e
|
||||
-----END CERTIFICATE-----
|
15
assistant/pem/https-private-key.pem
Normal file
15
assistant/pem/https-private-key.pem
Normal file
@ -0,0 +1,15 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICWwIBAAKBgQCKumpL0vCHkk1js3fTfCmFcJH8dkOB/DUec/pvrZZUlMe1T3uE
|
||||
sqOtNfHNRg8TLJJcvMUaOm0pKNPdmC6VXUj1gFeDaB0OJvHHFlRkegnyPSiH4xLF
|
||||
G/V9PPD4cQEStQlAniUed/2duLtN/jxeI1aqH0tx7de/IMVChEMBJfnJ5QIDAQAB
|
||||
AoGAGmBUKoN6OQSPk0fBniOqz1S2ZP5lWncF8HrToF0sSnuZNvdcQEAoz5uElGdg
|
||||
IWClmV1IynJWY+9/zM+M99grMT6it3VHHVM3MQoTf1Am4Vy0qgKR6Y1TzE0XLrVW
|
||||
3e3ezDph3gG0EQsRxVbn/goCEfstuhJaFyxHvsQRtPY+Z1ECQQC8ffbjV8hb911o
|
||||
iUw67FquOL9AYrFfQfohkQZ1TrDv0VTCAYpB7e5ml4dBhUL1dQbFV7avZIufD5fl
|
||||
pxCKkAPNAkEAvGnQPByrKj6ggf2l1CzgjXzZ24wm3AkJutWkFjAcf5EFy0+MIBOi
|
||||
ejyrGcGi9eovXCLGLrgzaBeAHa4XNkX2eQJAEZE73VxlFA0t63xAWo2EthAb4whP
|
||||
t60SfuZhT7WR0AgWei5ikFp4iZ89v+GHqBDMHMBcCmS4jo6JfaHgbMmXUQJAAIDL
|
||||
1I1DC77VEOPLgJCKHPabYlGyfN3tT7loUcLZIKITgOJ6fk9vHKJy1oPE2qFAdR+G
|
||||
pfNJ99owNmQTncp8CQJAI3fp5VABViB3uha4cHmpRUvoGNWmmh9Ob6LypDsGtd8z
|
||||
8ah+4Ek1DvsQC4XDuwgwnQsCmEYfa2P1T/GIdqPadw==
|
||||
-----END RSA PRIVATE KEY-----
|
7
assistant/readme.md
Normal file
7
assistant/readme.md
Normal file
@ -0,0 +1,7 @@
|
||||
# assistant cli
|
||||
|
||||
## 初始化路径
|
||||
|
||||
## 启动服务
|
||||
|
||||
## 配置
|
19
assistant/src/app.ts
Normal file
19
assistant/src/app.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { App } from '@kevisual/router';
|
||||
import { AssistantConfig } from '@/module/assistant/index.ts';
|
||||
import { HttpsPem } from '@/module/assistant/https/sign.ts';
|
||||
import path from 'node:path';
|
||||
|
||||
export const configDir = path.resolve(process.env.assistantConfigDir || process.cwd());
|
||||
export const assistantConfig = new AssistantConfig({
|
||||
configDir,
|
||||
init: true,
|
||||
});
|
||||
const httpsPem = new HttpsPem(assistantConfig);
|
||||
export const app = new App({
|
||||
serverOptions: {
|
||||
path: '/client/router',
|
||||
httpType: 'https',
|
||||
httpsCert: httpsPem.cert,
|
||||
httpsKey: httpsPem.key,
|
||||
},
|
||||
});
|
24
assistant/src/command/init/index.ts
Normal file
24
assistant/src/command/init/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { program, Command } from '@/program.ts';
|
||||
import { AssistantInit } from '@/services/init/index.ts';
|
||||
import path from 'node:path';
|
||||
|
||||
type InitCommandOptions = {
|
||||
path?: string;
|
||||
};
|
||||
const Init = new Command('init')
|
||||
.description('初始化一个助手客户端,生成配置文件。')
|
||||
.option('-p --path <path>', '助手路径,默认为执行命令的目录,如果助手路径不存在则创建。')
|
||||
.action((opts: InitCommandOptions) => {
|
||||
// 如果path参数存在,检测path是否是相对路径,如果是相对路径,则转换为绝对路径
|
||||
if (opts.path && !opts.path.startsWith('/')) {
|
||||
opts.path = path.join(process.cwd(), opts.path);
|
||||
} else if (opts.path) {
|
||||
opts.path = path.resolve(opts.path);
|
||||
}
|
||||
const assistantInit = new AssistantInit({
|
||||
path: opts.path,
|
||||
});
|
||||
assistantInit.init();
|
||||
});
|
||||
|
||||
program.addCommand(Init);
|
20
assistant/src/index.ts
Normal file
20
assistant/src/index.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { program, runProgram } from '@/program.ts';
|
||||
import './command/init/index.ts';
|
||||
|
||||
/**
|
||||
* 通过命令行解析器解析参数
|
||||
* args[0] 是执行的命令, example: node
|
||||
* args[1] 是执行的脚本, example: index.ts
|
||||
* @param argv
|
||||
*/
|
||||
export const runParser = async (argv: string[]) => {
|
||||
// program.parse(process.argv);
|
||||
// console.log('argv', argv);
|
||||
try {
|
||||
program.parse(argv);
|
||||
} catch (error) {
|
||||
console.error('执行错误:', error.message);
|
||||
}
|
||||
};
|
||||
|
||||
export { runProgram };
|
175
assistant/src/module/assistant/config/index.ts
Normal file
175
assistant/src/module/assistant/config/index.ts
Normal file
@ -0,0 +1,175 @@
|
||||
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';
|
||||
|
||||
/**
|
||||
* 助手配置文件路径, 全局配置文件目录
|
||||
*/
|
||||
const configDir = createDir(path.join(homedir(), '.config/envision/assistant-app'));
|
||||
|
||||
/**
|
||||
* 助手配置文件初始化
|
||||
* @param configRootPath
|
||||
* @returns
|
||||
*/
|
||||
export const initConfig = (configRootPath: string) => {
|
||||
const configDir = createDir(path.join(configRootPath, 'assistant-app'));
|
||||
const configPath = path.join(configDir, 'assistant-config.json');
|
||||
const appConfigPath = path.join(configDir, 'assistant-app-config.json');
|
||||
const appDir = createDir(path.join(configDir, 'frontend'));
|
||||
const serviceDir = createDir(path.join(configDir, 'services'));
|
||||
const serviceConfigPath = path.join(serviceDir, 'assistant-service-config.json');
|
||||
const appPidPath = path.join(configDir, 'assistant-app.pid');
|
||||
return {
|
||||
/**
|
||||
* 助手配置文件路径
|
||||
*/
|
||||
configDir,
|
||||
/**
|
||||
* 助手配置文件路径, assistant-config.json
|
||||
*/
|
||||
configPath,
|
||||
/**
|
||||
* 服务目录, 后端服务目录
|
||||
*/
|
||||
serviceDir,
|
||||
/**
|
||||
* 服务配置文件路径 assistant-service-config.json
|
||||
*/
|
||||
serviceConfigPath,
|
||||
/**
|
||||
* 应用目录, 前端应用目录
|
||||
*/
|
||||
appDir,
|
||||
/**
|
||||
* 应用配置文件路径, assistant-app-config.json
|
||||
*/
|
||||
appConfigPath,
|
||||
/**
|
||||
* 应用进程pid文件路径
|
||||
*/
|
||||
appPidPath,
|
||||
};
|
||||
};
|
||||
export type ReturnInitConfigType = ReturnType<typeof initConfig>;
|
||||
|
||||
type AssistantConfigData = {
|
||||
pageApi?: string; // https://kevisual.silkyai.cn
|
||||
proxy?: { user: string; key: string; path: string }[];
|
||||
apiProxyList?: ProxyInfo[];
|
||||
description?: string;
|
||||
};
|
||||
let assistantConfig: AssistantConfigData;
|
||||
type AssistantConfigOptions = {
|
||||
configDir?: string;
|
||||
init?: boolean;
|
||||
};
|
||||
export class AssistantConfig {
|
||||
config: AssistantConfigData;
|
||||
configPath: ReturnInitConfigType;
|
||||
configDir: string;
|
||||
constructor(opts?: AssistantConfigOptions) {
|
||||
this.configDir = opts?.configDir || configDir;
|
||||
if (opts?.init) {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
init() {
|
||||
this.configPath = initConfig(this.configDir);
|
||||
}
|
||||
getConfig() {
|
||||
try {
|
||||
if (!checkFileExists(this.configPath.configPath)) {
|
||||
fs.writeFileSync(this.configPath.configPath, JSON.stringify({ proxy: [] }, null, 2));
|
||||
return {
|
||||
pageApi: '',
|
||||
proxy: [],
|
||||
};
|
||||
}
|
||||
assistantConfig = JSON.parse(fs.readFileSync(this.configPath.configPath, 'utf8'));
|
||||
return assistantConfig;
|
||||
} catch (error) {
|
||||
console.error('file read', error.message);
|
||||
return {
|
||||
pageApi: '',
|
||||
proxy: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
getCacheAssistantConfig() {
|
||||
if (this.config) {
|
||||
return this.config;
|
||||
}
|
||||
return this.getConfig();
|
||||
}
|
||||
/**
|
||||
* 设置 assistant-config.json 配置
|
||||
* @param config
|
||||
* @returns
|
||||
*/
|
||||
setConfig(config?: AssistantConfigData) {
|
||||
const myConfig = this.getCacheAssistantConfig();
|
||||
const newConfig = { ...myConfig, ...config };
|
||||
this.config = newConfig;
|
||||
fs.writeFileSync(this.configPath.configPath, JSON.stringify(newConfig, null, 2));
|
||||
return newConfig;
|
||||
}
|
||||
/**
|
||||
* 应用配置
|
||||
* @returns
|
||||
*/
|
||||
getAppConfig(): AppConfig {
|
||||
const { appConfigPath } = this.configPath;
|
||||
if (!checkFileExists(appConfigPath)) {
|
||||
return {
|
||||
list: [],
|
||||
};
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(appConfigPath, 'utf8'));
|
||||
}
|
||||
setAppConfig(config?: AppConfig) {
|
||||
const _config = this.getAppConfig();
|
||||
const _saveConfig = { ..._config, ...config };
|
||||
const { appConfigPath } = this.configPath;
|
||||
|
||||
fs.writeFileSync(appConfigPath, JSON.stringify(_saveConfig, null, 2));
|
||||
return _saveConfig;
|
||||
}
|
||||
assAppConfig(app: any) {
|
||||
const config = this.getAppConfig();
|
||||
const assistantConfig = this.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);
|
||||
}
|
||||
this.setAppConfig({ ...config, list: _apps });
|
||||
this.setConfig({ ...assistantConfig, proxy: _proxy });
|
||||
return config;
|
||||
}
|
||||
getAppList() {
|
||||
return this.getAppConfig().list;
|
||||
}
|
||||
}
|
||||
|
||||
type AppConfig = {
|
||||
list: any[];
|
||||
};
|
26
assistant/src/module/assistant/file/index.ts
Normal file
26
assistant/src/module/assistant/file/index.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import fs from 'node:fs';
|
||||
|
||||
/**
|
||||
* 检查文件是否存在
|
||||
* @param filePath 文件路径
|
||||
* @param checkIsFile 是否检查文件类型 default: false
|
||||
* @returns
|
||||
*/
|
||||
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;
|
||||
};
|
27
assistant/src/module/assistant/https/cookie-rewrite.ts
Normal file
27
assistant/src/module/assistant/https/cookie-rewrite.ts
Normal file
@ -0,0 +1,27 @@
|
||||
export function rewriteCookieDomain(cookie: string, domainRewrite: string | Record<string, string>) {
|
||||
if (!domainRewrite) return cookie;
|
||||
|
||||
// 解析 Cookie 的属性
|
||||
const parts = cookie.split(';').map((part) => part.trim());
|
||||
const nameValue = parts[0];
|
||||
const attributes = parts.slice(1);
|
||||
|
||||
// 查找并替换 Domain 属性
|
||||
const newAttributes = attributes.map((attr) => {
|
||||
if (attr.startsWith('Domain=')) {
|
||||
const originalDomain = attr.slice(7); // 去掉 "Domain="
|
||||
let newDomain = domainRewrite;
|
||||
|
||||
// 如果 domainRewrite 是对象,根据映射关系替换
|
||||
if (typeof domainRewrite === 'object') {
|
||||
newDomain = domainRewrite[originalDomain] || originalDomain;
|
||||
}
|
||||
|
||||
return `Domain=${newDomain}`;
|
||||
}
|
||||
return attr;
|
||||
});
|
||||
|
||||
// 重新组合 Cookie
|
||||
return [nameValue, ...newAttributes].join('; ');
|
||||
}
|
79
assistant/src/module/assistant/https/sign.ts
Normal file
79
assistant/src/module/assistant/https/sign.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { createCert } from '@kevisual/router/sign';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { AssistantConfig } from '../config/index.ts';
|
||||
import { checkFileExists } from '../file/index.ts';
|
||||
import { chalk } from '@/module/chalk.ts';
|
||||
type Attributes = {
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
type AltNames = {
|
||||
type: number;
|
||||
value?: string;
|
||||
ip?: string;
|
||||
};
|
||||
export class HttpsPem {
|
||||
assistantConfig: AssistantConfig;
|
||||
key: string;
|
||||
cert: string;
|
||||
constructor(assistantConfig: AssistantConfig) {
|
||||
this.assistantConfig = assistantConfig;
|
||||
const { key, cert } = this.getCert();
|
||||
this.key = key;
|
||||
this.cert = cert;
|
||||
}
|
||||
getPemDir() {
|
||||
const configDir = this.assistantConfig.configPath?.configDir || process.cwd();
|
||||
const pemDir = path.join(configDir, 'pem');
|
||||
if (!checkFileExists(pemDir)) {
|
||||
fs.mkdirSync(pemDir, { recursive: true });
|
||||
}
|
||||
return pemDir;
|
||||
}
|
||||
getCert() {
|
||||
const pemDir = this.getPemDir();
|
||||
const pemPath = {
|
||||
key: path.join(pemDir, 'https-private-key.pem'),
|
||||
cert: path.join(pemDir, 'https-cert.pem'),
|
||||
};
|
||||
if (!checkFileExists(pemPath.key) || !checkFileExists(pemPath.cert)) {
|
||||
const { key, cert } = this.createCert();
|
||||
fs.writeFileSync(pemPath.key, key);
|
||||
fs.writeFileSync(pemPath.cert, cert);
|
||||
console.log(chalk.green('证书创建成功'))
|
||||
return {
|
||||
key,
|
||||
cert,
|
||||
};
|
||||
}
|
||||
const key = fs.readFileSync(pemPath.key, 'utf-8');
|
||||
const cert = fs.readFileSync(pemPath.cert, 'utf-8');
|
||||
return {
|
||||
key,
|
||||
cert,
|
||||
};
|
||||
}
|
||||
createCert(attrs?: Attributes[], altNames?: AltNames[]) {
|
||||
const attributes = attrs || [];
|
||||
const altNamesList = altNames || [];
|
||||
const { key, cert } = createCert(
|
||||
[
|
||||
{
|
||||
name: 'commonName',
|
||||
value: 'localhost',
|
||||
},
|
||||
{
|
||||
name: 'organizationName',
|
||||
value: 'kevisual',
|
||||
},
|
||||
...attributes,
|
||||
],
|
||||
altNamesList,
|
||||
);
|
||||
return {
|
||||
key,
|
||||
cert,
|
||||
};
|
||||
}
|
||||
}
|
7
assistant/src/module/assistant/index.ts
Normal file
7
assistant/src/module/assistant/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export * from './install/index.ts';
|
||||
export * from './config/index.ts';
|
||||
export * from './file/index.ts';
|
||||
|
||||
export * from './process/index.ts';
|
||||
|
||||
export * from './proxy/index.ts';
|
121
assistant/src/module/assistant/install/index.ts
Normal file
121
assistant/src/module/assistant/install/index.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { checkFileExists } from '../file/index.ts';
|
||||
|
||||
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 });
|
||||
}
|
||||
};
|
||||
|
||||
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',
|
||||
};
|
||||
}
|
||||
};
|
82
assistant/src/module/assistant/process/index.ts
Normal file
82
assistant/src/module/assistant/process/index.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { ChildProcess, fork, ForkOptions } from 'child_process';
|
||||
class BaseProcess {
|
||||
private process: ChildProcess;
|
||||
status: 'running' | 'stopped' | 'error' = 'stopped';
|
||||
appPath: string;
|
||||
args: any[] = [];
|
||||
opts: ForkOptions = {};
|
||||
/*
|
||||
* 重启次数, 默认0, TODO, 错误检测,当重启次数超过一定次数,则认为进程已崩溃
|
||||
*/
|
||||
restartCount: number = 0;
|
||||
constructor(appPath?: string, args?: any[], opts?: ForkOptions) {
|
||||
this.appPath = appPath;
|
||||
this.args = args || [];
|
||||
this.opts = opts || {};
|
||||
// this.createProcess(appPath);
|
||||
}
|
||||
createProcess(appPath: string = this.appPath, args: any[] = [], opts: ForkOptions = {}) {
|
||||
if (this.process) {
|
||||
this.process.kill();
|
||||
}
|
||||
this.appPath = appPath || this.appPath;
|
||||
this.args = args || this.args;
|
||||
this.opts = {
|
||||
...this.opts,
|
||||
...opts,
|
||||
};
|
||||
|
||||
this.process = fork(appPath, args, {
|
||||
stdio: 'inherit',
|
||||
...this.opts,
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV_PARENT: 'fork',
|
||||
...this.opts?.env,
|
||||
},
|
||||
});
|
||||
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);
|
||||
}
|
||||
restart() {
|
||||
this.kill();
|
||||
this.createProcess();
|
||||
}
|
||||
}
|
||||
export class AssistantProcess extends BaseProcess {
|
||||
constructor(appPath: string) {
|
||||
super(appPath);
|
||||
}
|
||||
}
|
91
assistant/src/module/assistant/proxy/api-proxy.ts
Normal file
91
assistant/src/module/assistant/proxy/api-proxy.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import { rewriteCookieDomain } from '../https/cookie-rewrite.ts';
|
||||
import { ProxyInfo } from './proxy.ts';
|
||||
export const defaultApiProxy = [
|
||||
{
|
||||
path: '/api/router',
|
||||
target: 'https://kevisual.cn',
|
||||
},
|
||||
{
|
||||
path: '/v1',
|
||||
target: 'https://kevisual.cn',
|
||||
},
|
||||
];
|
||||
/**
|
||||
* 创建api代理
|
||||
* @param api
|
||||
* @param paths ['/api/router', '/v1' ]
|
||||
* @returns
|
||||
*/
|
||||
export const createApiProxy = (api: string, paths: string[] = ['/api', '/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 { target } = proxyApi;
|
||||
const _u = new URL(req.url, `${target}`);
|
||||
console.log('proxyApi', { url: req.url, target: _u.href });
|
||||
// 设置代理请求的目标 URL 和请求头
|
||||
let header: any = {};
|
||||
if (req.headers?.['Authorization'] && !req.headers?.['authorization']) {
|
||||
header.authorization = req.headers['Authorization'];
|
||||
}
|
||||
if (req.headers?.['cookie'] || req.headers?.['Cookie']) {
|
||||
// 处理大小写不一致的cookie
|
||||
header.cookie = req.headers['cookie'] || req.headers['Cookie'];
|
||||
}
|
||||
// 提取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(target).origin;
|
||||
return;
|
||||
}
|
||||
if (item.toLowerCase() === 'referer') {
|
||||
header.referer = new URL(req.url, target).href;
|
||||
return;
|
||||
}
|
||||
header[item] = req.headers[item];
|
||||
});
|
||||
const options: http.RequestOptions = {
|
||||
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) => {
|
||||
// Modify the 'set-cookie' headers using rewriteCookieDomain
|
||||
if (proxyRes.headers['set-cookie']) {
|
||||
proxyRes.headers['set-cookie'] = proxyRes.headers['set-cookie'].map((cookie) => rewriteCookieDomain(cookie, 'localhost'));
|
||||
}
|
||||
// 将代理服务器的响应头和状态码返回给客户端
|
||||
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;
|
||||
};
|
49
assistant/src/module/assistant/proxy/file-proxy.ts
Normal file
49
assistant/src/module/assistant/proxy/file-proxy.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import http from 'node:http';
|
||||
import send from 'send';
|
||||
import fs from 'node:fs';
|
||||
import path from 'path';
|
||||
import { ProxyInfo } from './proxy.ts';
|
||||
import { checkFileExists } from '../file/index.ts';
|
||||
|
||||
export const fileProxy = (req: http.IncomingMessage, res: http.ServerResponse, proxyApi: ProxyInfo) => {
|
||||
// url开头的文件
|
||||
const url = new URL(req.url, 'http://localhost');
|
||||
const [user, key, _info] = url.pathname.split('/');
|
||||
const pathname = url.pathname.slice(1);
|
||||
const { indexPath = '', target = '', rootPath = process.cwd() } = proxyApi;
|
||||
try {
|
||||
// 检测文件是否存在,如果文件不存在,则返回404
|
||||
let filePath = '';
|
||||
let exist = false;
|
||||
if (_info) {
|
||||
filePath = path.join(rootPath, target, pathname);
|
||||
exist = checkFileExists(filePath, true);
|
||||
}
|
||||
if (!exist) {
|
||||
filePath = path.join(rootPath, target, indexPath);
|
||||
exist = checkFileExists(filePath, true);
|
||||
}
|
||||
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 = path.relative(rootPath, filePath);
|
||||
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/src/module/assistant/proxy/index.ts
Normal file
5
assistant/src/module/assistant/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 './ws-proxy.ts';
|
50
assistant/src/module/assistant/proxy/proxy.ts
Normal file
50
assistant/src/module/assistant/proxy/proxy.ts
Normal file
@ -0,0 +1,50 @@
|
||||
export type ProxyInfo = {
|
||||
path?: string;
|
||||
/**
|
||||
* 目标地址
|
||||
*/
|
||||
target?: string;
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
type?: 'static' | 'dynamic' | 'minio';
|
||||
/**
|
||||
* 是否使用websocket
|
||||
* @default false
|
||||
*/
|
||||
ws?: boolean;
|
||||
/**
|
||||
* 首要文件,比如index.html, 设置了首要文件,如果文件不存在,则访问首要文件
|
||||
*/
|
||||
indexPath?: string;
|
||||
/**
|
||||
* 根路径, 默认是process.cwd()
|
||||
*/
|
||||
rootPath?: string;
|
||||
};
|
||||
export type ApiList = {
|
||||
path: string;
|
||||
/**
|
||||
* url或者相对路径
|
||||
*/
|
||||
target: string;
|
||||
/**
|
||||
* 目标地址
|
||||
*/
|
||||
ws?: boolean;
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
type?: 'static' | 'dynamic' | 'minio';
|
||||
}[];
|
||||
|
||||
/**
|
||||
|
||||
[
|
||||
{
|
||||
path: '/api/v1/user',
|
||||
target: 'http://localhost:3000/api/v1/user',
|
||||
type: 'dynamic',
|
||||
},
|
||||
]
|
||||
*/
|
49
assistant/src/module/assistant/proxy/ws-proxy.ts
Normal file
49
assistant/src/module/assistant/proxy/ws-proxy.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { Server } from 'http';
|
||||
import WebSocket from 'ws';
|
||||
import { ProxyInfo } from './proxy.ts';
|
||||
/**
|
||||
* websocket代理
|
||||
* apiList: [{ path: '/api/router', target: 'https://kevisual.xiongxiao.me' }]
|
||||
* @param server
|
||||
* @param config
|
||||
*/
|
||||
export const wsProxy = (server: Server, config: { apiList: ProxyInfo[] }) => {
|
||||
console.log('Upgrade initialization started');
|
||||
|
||||
server.on('upgrade', (req, socket, head) => {
|
||||
const proxyApiList: ProxyInfo[] = config?.apiList || [];
|
||||
const proxyApi = proxyApiList.find((item) => req.url.startsWith(item.path));
|
||||
|
||||
if (proxyApi && proxyApi.ws) {
|
||||
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();
|
||||
}
|
||||
});
|
||||
};
|
2
assistant/src/module/chalk.ts
Normal file
2
assistant/src/module/chalk.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import { Chalk } from 'chalk';
|
||||
export const chalk = new Chalk({ level: 3 });
|
42
assistant/src/module/logger.ts
Normal file
42
assistant/src/module/logger.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { pino } from 'pino';
|
||||
|
||||
export const logger = pino({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
transport: {
|
||||
target: 'pino-pretty',
|
||||
options: {
|
||||
colorize: true,
|
||||
translateTime: 'SYS:standard',
|
||||
ignore: 'pid,hostname',
|
||||
},
|
||||
},
|
||||
serializers: {
|
||||
error: pino.stdSerializers.err,
|
||||
req: pino.stdSerializers.req,
|
||||
res: pino.stdSerializers.res,
|
||||
},
|
||||
base: {
|
||||
app: 'assistant',
|
||||
env: process.env.NODE_ENV || 'production',
|
||||
},
|
||||
});
|
||||
export const console = {
|
||||
log: logger.info,
|
||||
error: logger.error,
|
||||
warn: logger.warn,
|
||||
info: logger.info,
|
||||
debug: logger.debug,
|
||||
};
|
||||
|
||||
export const logError = (message: string, data?: any) => logger.error({ data }, message);
|
||||
export const logWarning = (message: string, data?: any) => logger.warn({ data }, message);
|
||||
export const logInfo = (message: string, data?: any) => logger.info({ data }, message);
|
||||
export const logDebug = (message: string, data?: any) => logger.debug({ data }, message);
|
||||
|
||||
export const log = {
|
||||
log: logInfo,
|
||||
error: logError,
|
||||
warn: logWarning,
|
||||
info: logInfo,
|
||||
debug: logDebug,
|
||||
};
|
28
assistant/src/program.ts
Normal file
28
assistant/src/program.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { program, Command } from 'commander';
|
||||
import fs from 'fs';
|
||||
// 将多个子命令加入主程序中
|
||||
let version = '0.0.1';
|
||||
try {
|
||||
// @ts-ignore
|
||||
if (ENVISION_VERSION) version = ENVISION_VERSION;
|
||||
} catch (e) {}
|
||||
// @ts-ignore
|
||||
program.name('app').description('A CLI tool with envison').version(version);
|
||||
|
||||
const ls = new Command('ls').description('List files in the current directory').action(() => {
|
||||
console.log('List files');
|
||||
console.log(fs.readdirSync(process.cwd()));
|
||||
});
|
||||
program.addCommand(ls);
|
||||
|
||||
export { program, Command };
|
||||
|
||||
/**
|
||||
* 在命令行中运行程序
|
||||
* 当前命令参数去执行 其他的应用模块
|
||||
* @param args
|
||||
*/
|
||||
export const runProgram = (args: string[]) => {
|
||||
const [_app, _command] = process.argv;
|
||||
program.parse([_app, _command, ...args]);
|
||||
};
|
6
assistant/src/run.ts
Normal file
6
assistant/src/run.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { runParser } from './index.ts';
|
||||
|
||||
/**
|
||||
* test run parser
|
||||
*/
|
||||
runParser(process.argv);
|
8
assistant/src/serve.ts
Normal file
8
assistant/src/serve.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { app } from './app.ts';
|
||||
import { proxyRoute } from './services/proxy/proxy-page-index.ts';
|
||||
|
||||
app.listen(51015, () => {
|
||||
console.log('Server is running on http://localhost:51015');
|
||||
});
|
||||
|
||||
app.server.on(proxyRoute);
|
44
assistant/src/services/init/index.ts
Normal file
44
assistant/src/services/init/index.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import path from 'node:path';
|
||||
import { checkFileExists, AssistantConfig } from '@/module/assistant/index.ts';
|
||||
import { chalk } from '@/module/chalk.ts';
|
||||
export type AssistantInitOptions = {
|
||||
path?: string;
|
||||
};
|
||||
/**
|
||||
* 助手初始化类
|
||||
* @class AssistantInit
|
||||
*/
|
||||
export class AssistantInit extends AssistantConfig {
|
||||
constructor(opts?: AssistantInitOptions) {
|
||||
const configDir = opts?.path || process.cwd();
|
||||
super({
|
||||
configDir,
|
||||
});
|
||||
}
|
||||
|
||||
async init() {
|
||||
// 1. 检查助手路径是否存在
|
||||
if (!this.checkConfigPath()) {
|
||||
console.log(chalk.blue('助手路径不存在,正在创建...'));
|
||||
super.init();
|
||||
this.createAssistantConfig();
|
||||
} else {
|
||||
super.init();
|
||||
console.log(chalk.yellow('助手路径已存在'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
checkConfigPath() {
|
||||
const assistantPath = path.join(this.configDir, 'assistant-config.json');
|
||||
return checkFileExists(assistantPath);
|
||||
}
|
||||
createAssistantConfig() {
|
||||
const assistantPath = this.configPath?.configPath;
|
||||
if (!checkFileExists(assistantPath, true)) {
|
||||
this.setConfig({
|
||||
description: '助手配置文件',
|
||||
});
|
||||
console.log(chalk.green('助手配置文件创建成功'));
|
||||
}
|
||||
}
|
||||
}
|
84
assistant/src/services/proxy/local-proxy.ts
Normal file
84
assistant/src/services/proxy/local-proxy.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import fs from 'node:fs';
|
||||
import { AssistantConfig, checkFileExists } from '@/module/assistant/index.ts';
|
||||
import path from 'node:path';
|
||||
|
||||
export const localProxyProxyList = [
|
||||
{
|
||||
user: 'root',
|
||||
key: 'assistant-base-app',
|
||||
path: '/root/assistant-base-app',
|
||||
indexPath: 'root/assistant-base-app/index.html',
|
||||
},
|
||||
{
|
||||
user: 'root',
|
||||
key: 'talkshow-admin',
|
||||
path: '/root/talkshow-admin',
|
||||
indexPath: 'root/talkshow-admin/index.html',
|
||||
},
|
||||
{
|
||||
user: 'root',
|
||||
key: 'center',
|
||||
path: '/root/center',
|
||||
indexPath: 'root/center/index.html',
|
||||
},
|
||||
];
|
||||
|
||||
type ProxyType = {
|
||||
user: string;
|
||||
key: string;
|
||||
path: string;
|
||||
indexPath: string;
|
||||
absolutePath?: string;
|
||||
};
|
||||
export type LocalProxyOpts = {
|
||||
assistantConfig?: AssistantConfig; // 前端应用路径
|
||||
};
|
||||
export class LocalProxy {
|
||||
localProxyProxyList: ProxyType[] = [];
|
||||
assistantConfig?: AssistantConfig;
|
||||
constructor(opts?: LocalProxyOpts) {
|
||||
this.assistantConfig = opts?.assistantConfig;
|
||||
if (this.assistantConfig) {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
init() {
|
||||
const frontAppDir = this.assistantConfig.configPath?.appDir;
|
||||
console.log('frontAppDir', frontAppDir);
|
||||
if (frontAppDir) {
|
||||
const userList = fs.readdirSync(frontAppDir);
|
||||
const localProxyProxyList: ProxyType[] = [];
|
||||
userList.forEach((user) => {
|
||||
const userPath = path.join(frontAppDir, user);
|
||||
const stat = fs.statSync(userPath);
|
||||
if (stat.isDirectory()) {
|
||||
const appList = fs.readdirSync(userPath);
|
||||
appList.forEach((app) => {
|
||||
const appPath = path.join(userPath, app);
|
||||
const indexPath = path.join(appPath, 'index.html');
|
||||
if (!checkFileExists(indexPath, true)) {
|
||||
return;
|
||||
}
|
||||
// const appPath = `${appPath}/index.html`;
|
||||
if (checkFileExists(indexPath, true)) {
|
||||
localProxyProxyList.push({
|
||||
user: user,
|
||||
key: app,
|
||||
path: `/${user}/${app}`,
|
||||
indexPath: `${user}/${app}/index.html`,
|
||||
absolutePath: appPath,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
this.localProxyProxyList = localProxyProxyList;
|
||||
}
|
||||
}
|
||||
getLocalProxyList() {
|
||||
return this.localProxyProxyList;
|
||||
}
|
||||
reload() {
|
||||
// 重新加载本地代理列表
|
||||
}
|
||||
}
|
85
assistant/src/services/proxy/proxy-page-index.ts
Normal file
85
assistant/src/services/proxy/proxy-page-index.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { fileProxy, apiProxy, createApiProxy } from '@/module/assistant/index.ts';
|
||||
import http from 'http';
|
||||
import { LocalProxy } from './local-proxy.ts';
|
||||
import { assistantConfig } from '@/app.ts';
|
||||
import { log } from '@/module/logger.ts';
|
||||
const localProxy = new LocalProxy({
|
||||
assistantConfig,
|
||||
});
|
||||
export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
const _assistantConfig = assistantConfig.getCacheAssistantConfig();
|
||||
const appDir = assistantConfig.configPath?.appDir;
|
||||
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;
|
||||
}
|
||||
// client, api, v1, serve 开头的拦截
|
||||
const apiProxyList = _assistantConfig?.apiProxyList || [];
|
||||
const defaultApiProxy = createApiProxy(_assistantConfig?.pageApi || 'https://kevisual.cn');
|
||||
const apiBackendProxy = [...apiProxyList, ...defaultApiProxy].find((item) => pathname.startsWith(item.path));
|
||||
if (apiBackendProxy) {
|
||||
console.log('apiBackendProxy', apiBackendProxy, req.url);
|
||||
return apiProxy(req, res, {
|
||||
path: apiBackendProxy.path,
|
||||
target: apiBackendProxy.target,
|
||||
});
|
||||
}
|
||||
const urls = pathname.split('/');
|
||||
const [_, _user, _app] = urls;
|
||||
if (!_app) {
|
||||
res.statusCode = 404;
|
||||
res.end('Not Found Proxy');
|
||||
return;
|
||||
}
|
||||
if (_app && urls.length === 3) {
|
||||
// 重定向到
|
||||
res.writeHead(302, { Location: `${req.url}/` });
|
||||
return res.end();
|
||||
}
|
||||
const proxyApiList = _assistantConfig?.proxy || [];
|
||||
const proxyApi = proxyApiList.find((item) => pathname.startsWith(item.path));
|
||||
if (proxyApi) {
|
||||
log.log('proxyApi', { proxyApi, pathname });
|
||||
const { user, key } = proxyApi;
|
||||
return fileProxy(req, res, {
|
||||
path: proxyApi.path, // 代理路径, 比如/root/center
|
||||
rootPath: appDir, // 根路径
|
||||
indexPath: `${user}/${key}/index.html`, // 首页路径
|
||||
});
|
||||
}
|
||||
const localProxyProxyList = localProxy.getLocalProxyList();
|
||||
const localProxyProxy = localProxyProxyList.find((item) => pathname.startsWith(item.path));
|
||||
if (localProxyProxy) {
|
||||
log.log('localProxyProxy', { localProxyProxy, url: req.url });
|
||||
return fileProxy(req, res, {
|
||||
path: localProxyProxy.path,
|
||||
rootPath: assistantConfig.configPath?.appDir,
|
||||
indexPath: localProxyProxy.indexPath,
|
||||
});
|
||||
}
|
||||
console.log('handle by router 404', req.url);
|
||||
const creatCenterProxy = createApiProxy(_assistantConfig?.pageApi || 'https://kevisual.cn', ['/root']);
|
||||
const centerProxy = creatCenterProxy.find((item) => pathname.startsWith(item.path));
|
||||
if (centerProxy) {
|
||||
console.log('centerProxy', centerProxy, req.url);
|
||||
return apiProxy(req, res, {
|
||||
path: centerProxy.path,
|
||||
target: centerProxy.target,
|
||||
type: 'static',
|
||||
});
|
||||
}
|
||||
res.statusCode = 404;
|
||||
res.end('Not Found Proxy');
|
||||
// console.log('getCacheAssistantConfig().pageApi', getCacheAssistantConfig().pageApi);
|
||||
// return apiProxy(req, res, {
|
||||
// path: url.pathname,
|
||||
// target: getCacheAssistantConfig().pageApi,
|
||||
// });
|
||||
};
|
32
assistant/tsconfig.json
Normal file
32
assistant/tsconfig.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "nodenext",
|
||||
"target": "esnext",
|
||||
"noImplicitAny": false,
|
||||
"outDir": "./dist",
|
||||
"sourceMap": false,
|
||||
"newLine": "LF",
|
||||
"baseUrl": "./",
|
||||
"typeRoots": [
|
||||
"node_modules/@types",
|
||||
],
|
||||
"declaration": true,
|
||||
"noEmit": false,
|
||||
"allowImportingTsExtensions": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"moduleResolution": "NodeNext",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
],
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
],
|
||||
"exclude": [],
|
||||
}
|
0
bin/assistant.js
Normal file
0
bin/assistant.js
Normal file
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
import { runParser } from '../dist/app.mjs';
|
||||
import { runParser } from '../dist/envision.mjs';
|
||||
|
||||
runParser(process.argv);
|
||||
|
@ -1,6 +1,7 @@
|
||||
// @ts-check
|
||||
// https://bun.sh/docs/bundler
|
||||
import pkg from './package.json' assert { type: 'json' };
|
||||
// @ts-ignore
|
||||
import pkg from './package.json';
|
||||
// bun run src/index.ts --
|
||||
await Bun.build({
|
||||
target: 'node',
|
||||
@ -8,7 +9,7 @@ await Bun.build({
|
||||
entrypoints: ['./src/index.ts'],
|
||||
outdir: './dist',
|
||||
naming: {
|
||||
entry: 'app.mjs',
|
||||
entry: 'envision.mjs',
|
||||
},
|
||||
|
||||
define: {
|
||||
|
19
package.json
19
package.json
@ -9,23 +9,23 @@
|
||||
"app": {
|
||||
"key": "envision-cli",
|
||||
"entry": "dist/app.mjs",
|
||||
"type": "pm2-system-app",
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
"type": "pm2-system-app"
|
||||
},
|
||||
"bin": {
|
||||
"envision": "bin/envision.js",
|
||||
"ev": "bin/envision.js",
|
||||
"kv": "bin/envision.js"
|
||||
"assistant": "bin/assistant.js",
|
||||
"asst": "bin/assistant.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"bin"
|
||||
"bin",
|
||||
"bun.config.mjs"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "bun run src/run.ts ",
|
||||
"build": "rimraf dist && bun run bun.config.mjs"
|
||||
"build": "rimraf dist && bun run bun.config.mjs",
|
||||
"dts": "dts-bundle-generator --external-inlines=@types/jsonwebtoken src/index.ts -o dist/index.d.ts "
|
||||
},
|
||||
"keywords": [
|
||||
"kevisual",
|
||||
@ -49,7 +49,10 @@
|
||||
"ignore": "^7.0.3",
|
||||
"inquirer": "^12.5.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"tar": "^7.4.3"
|
||||
"rollup": "^4.40.0",
|
||||
"rollup-plugin-dts": "^6.2.1",
|
||||
"tar": "^7.4.3",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
|
665
pnpm-lock.yaml
generated
665
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,2 +1,4 @@
|
||||
packages:
|
||||
- 'submodules/*'
|
||||
- 'assistant'
|
||||
- '!submodules/assistant-center'
|
11
rollup.config.mjs
Normal file
11
rollup.config.mjs
Normal file
@ -0,0 +1,11 @@
|
||||
import dts from 'rollup-plugin-dts';
|
||||
|
||||
export default {
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: 'dist/index.d.ts',
|
||||
format: 'es',
|
||||
},
|
||||
plugins: [dts()],
|
||||
};
|
||||
|
@ -5,7 +5,6 @@
|
||||
"noImplicitAny": false,
|
||||
"outDir": "./dist",
|
||||
"sourceMap": false,
|
||||
"allowJs": true,
|
||||
"newLine": "LF",
|
||||
"baseUrl": "./",
|
||||
"typeRoots": [
|
||||
@ -18,21 +17,16 @@
|
||||
"moduleResolution": "NodeNext",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
],
|
||||
},
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"typings.d.ts",
|
||||
"src/**/*.ts",
|
||||
"./bun.config.mjs"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
],
|
||||
"exclude": [],
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user