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
|
#!/usr/bin/env node
|
||||||
import { runParser } from '../dist/app.mjs';
|
import { runParser } from '../dist/envision.mjs';
|
||||||
|
|
||||||
runParser(process.argv);
|
runParser(process.argv);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
// https://bun.sh/docs/bundler
|
// 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 --
|
// bun run src/index.ts --
|
||||||
await Bun.build({
|
await Bun.build({
|
||||||
target: 'node',
|
target: 'node',
|
||||||
@ -8,7 +9,7 @@ await Bun.build({
|
|||||||
entrypoints: ['./src/index.ts'],
|
entrypoints: ['./src/index.ts'],
|
||||||
outdir: './dist',
|
outdir: './dist',
|
||||||
naming: {
|
naming: {
|
||||||
entry: 'app.mjs',
|
entry: 'envision.mjs',
|
||||||
},
|
},
|
||||||
|
|
||||||
define: {
|
define: {
|
||||||
|
19
package.json
19
package.json
@ -9,23 +9,23 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"key": "envision-cli",
|
"key": "envision-cli",
|
||||||
"entry": "dist/app.mjs",
|
"entry": "dist/app.mjs",
|
||||||
"type": "pm2-system-app",
|
"type": "pm2-system-app"
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"envision": "bin/envision.js",
|
"envision": "bin/envision.js",
|
||||||
"ev": "bin/envision.js",
|
"ev": "bin/envision.js",
|
||||||
"kv": "bin/envision.js"
|
"assistant": "bin/assistant.js",
|
||||||
|
"asst": "bin/assistant.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
"bin"
|
"bin",
|
||||||
|
"bun.config.mjs"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run src/run.ts ",
|
"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": [
|
"keywords": [
|
||||||
"kevisual",
|
"kevisual",
|
||||||
@ -49,7 +49,10 @@
|
|||||||
"ignore": "^7.0.3",
|
"ignore": "^7.0.3",
|
||||||
"inquirer": "^12.5.2",
|
"inquirer": "^12.5.2",
|
||||||
"jsonwebtoken": "^9.0.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": {
|
"engines": {
|
||||||
"node": ">=22.0.0"
|
"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:
|
packages:
|
||||||
- 'submodules/*'
|
- '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,
|
"noImplicitAny": false,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"allowJs": true,
|
|
||||||
"newLine": "LF",
|
"newLine": "LF",
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"typeRoots": [
|
"typeRoots": [
|
||||||
@ -18,21 +17,16 @@
|
|||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
"src/*"
|
"src/*"
|
||||||
],
|
],
|
||||||
},
|
}
|
||||||
"resolveJsonModule": true
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"typings.d.ts",
|
|
||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
"./bun.config.mjs"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules",
|
|
||||||
"dist",
|
|
||||||
],
|
],
|
||||||
|
"exclude": [],
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user