init
This commit is contained in:
parent
9eb4d06939
commit
bcc12209e0
3
assistant/.gitignore
vendored
3
assistant/.gitignore
vendored
@ -6,3 +6,6 @@ dist
|
||||
pack-dist
|
||||
|
||||
assistant-app
|
||||
|
||||
.env*
|
||||
!.env*example
|
4
assistant/bin/assistant-server.js
Normal file
4
assistant/bin/assistant-server.js
Normal file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
import { runParser } from '../dist/assistant-server.mjs';
|
||||
|
||||
runParser(process.argv);
|
4
assistant/bin/assistant.js
Normal file
4
assistant/bin/assistant.js
Normal file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
import { runParser } from '../dist/assistant.mjs';
|
||||
|
||||
runParser(process.argv);
|
@ -17,3 +17,17 @@ await Bun.build({
|
||||
},
|
||||
env: 'ENVISION_*',
|
||||
});
|
||||
|
||||
await Bun.build({
|
||||
target: 'node',
|
||||
format: 'esm',
|
||||
entrypoints: ['./src/server.ts'],
|
||||
outdir: './dist',
|
||||
naming: {
|
||||
entry: 'assistan-server.mjs',
|
||||
},
|
||||
define: {
|
||||
ENVISION_VERSION: JSON.stringify(pkg.version),
|
||||
},
|
||||
env: 'ENVISION_*',
|
||||
});
|
||||
|
@ -10,7 +10,7 @@
|
||||
],
|
||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@10.7.0",
|
||||
"packageManager": "pnpm@10.9.0",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist",
|
||||
@ -19,24 +19,36 @@
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "bun run src/run.ts ",
|
||||
"dev:serve": "bun --watch src/serve.ts",
|
||||
"dev:server": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 bun --watch src/server.ts",
|
||||
"build": "rimraf dist && bun run bun.config.mjs"
|
||||
},
|
||||
"bin": {
|
||||
"ev-assistant": "bin/assistant.js",
|
||||
"ev-asst": "bin/assistant.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kevisual/load": "^0.0.6",
|
||||
"@kevisual/local-app-manager": "^0.1.16",
|
||||
"@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/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.15.2",
|
||||
"@types/send": "^0.17.4",
|
||||
"@types/ws": "^8.18.1",
|
||||
"chalk": "^5.4.1",
|
||||
"commander": "^13.1.0",
|
||||
"inquirer": "^12.5.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"inquirer": "^12.6.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nanoid": "^5.1.5",
|
||||
"pino": "^9.6.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"send": "^1.2.0",
|
||||
"supports-color": "^10.0.0",
|
||||
"ws": "npm:@kevisual/ws",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -44,5 +56,11 @@
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"dayjs": "^1.11.13"
|
||||
},
|
||||
"overrides": {
|
||||
"ws": "npm:@kevisual/ws"
|
||||
}
|
||||
}
|
@ -1,13 +1,7 @@
|
||||
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,
|
||||
});
|
||||
import { assistantConfig } from '@/config.ts';
|
||||
export { assistantConfig };
|
||||
const httpsPem = new HttpsPem(assistantConfig);
|
||||
export const app = new App({
|
||||
serverOptions: {
|
||||
|
68
assistant/src/command/app-manager/index.ts
Normal file
68
assistant/src/command/app-manager/index.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { AssistantApp } from '@/module/assistant/index.ts';
|
||||
import { program, Command, assistantConfig } from '@/program.ts';
|
||||
|
||||
const appManagerCommand = new Command('app-manager').alias('am').description('Manage Assistant Apps 管理本地的应用模块');
|
||||
program.addCommand(appManagerCommand);
|
||||
|
||||
appManagerCommand
|
||||
.command('list')
|
||||
.description('List all installed apps')
|
||||
.action(async () => {
|
||||
const manager = new AssistantApp(assistantConfig);
|
||||
await manager.loadConfig();
|
||||
const showInfos = manager.getAllAppShowInfo();
|
||||
console.log('Installed Apps:', showInfos);
|
||||
});
|
||||
|
||||
appManagerCommand
|
||||
.command('detect')
|
||||
.description('Detect all installed apps')
|
||||
.action(async () => {
|
||||
const manager = new AssistantApp(assistantConfig);
|
||||
await manager.loadConfig();
|
||||
const showInfos = await manager.detectApp();
|
||||
if (showInfos === true) {
|
||||
const showInfos = manager.getAllAppShowInfo();
|
||||
console.log('Installed Apps:', showInfos);
|
||||
} else {
|
||||
console.log('Install New Apps:', showInfos);
|
||||
}
|
||||
});
|
||||
|
||||
appManagerCommand
|
||||
.command('start')
|
||||
.argument('<app-key-name>', '应用的 key 名称')
|
||||
.action(async (appKey: string) => {
|
||||
const manager = new AssistantApp(assistantConfig);
|
||||
await manager.loadConfig();
|
||||
manager.start(appKey);
|
||||
console.log('Start App:', appKey);
|
||||
});
|
||||
|
||||
appManagerCommand
|
||||
.command('stop')
|
||||
.argument('<app-key-name>', '应用的 key 名称')
|
||||
.action(async (appKey: string) => {
|
||||
const manager = new AssistantApp(assistantConfig);
|
||||
try {
|
||||
await manager.loadConfig();
|
||||
await manager.stop(appKey);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
console.log('Stop App:', appKey);
|
||||
});
|
||||
|
||||
appManagerCommand
|
||||
.command('restart')
|
||||
.argument('<app-key-name>', '应用的 key 名称')
|
||||
.action(async (appKey: string) => {
|
||||
const manager = new AssistantApp(assistantConfig);
|
||||
try {
|
||||
await manager.loadConfig();
|
||||
await manager.restart(appKey);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
console.log('Restart App:', appKey);
|
||||
});
|
7
assistant/src/config.ts
Normal file
7
assistant/src/config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { AssistantConfig } from '@/module/assistant/index.ts';
|
||||
|
||||
export const configDir = AssistantConfig.detectConfigDir();
|
||||
export const assistantConfig = new AssistantConfig({
|
||||
configDir,
|
||||
init: true,
|
||||
});
|
@ -1,5 +1,6 @@
|
||||
import { program, runProgram } from '@/program.ts';
|
||||
import './command/init/index.ts';
|
||||
import './command/app-manager/index.ts';
|
||||
|
||||
/**
|
||||
* 通过命令行解析器解析参数
|
||||
|
@ -17,11 +17,12 @@ const configDir = createDir(path.join(homedir(), '.config/envision/assistant-app
|
||||
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 pageConfigPath = path.join(configDir, 'assistant-page-config.json');
|
||||
const pageDir = createDir(path.join(configDir, 'page'));
|
||||
const appsDir = createDir(path.join(configDir, 'apps'));
|
||||
const appsConfigPath = path.join(appsDir, 'assistant-apps-config.json');
|
||||
const appPidPath = path.join(configDir, 'assistant-app.pid');
|
||||
const envConfigPath = path.join(configDir, '.env');
|
||||
return {
|
||||
/**
|
||||
* 助手配置文件路径
|
||||
@ -34,30 +35,34 @@ export const initConfig = (configRootPath: string) => {
|
||||
/**
|
||||
* 服务目录, 后端服务目录
|
||||
*/
|
||||
serviceDir,
|
||||
appsDir,
|
||||
/**
|
||||
* 服务配置文件路径 assistant-service-config.json
|
||||
*/
|
||||
serviceConfigPath,
|
||||
appsConfigPath,
|
||||
/**
|
||||
* 应用目录, 前端应用目录
|
||||
*/
|
||||
appDir,
|
||||
pageDir,
|
||||
/**
|
||||
* 应用配置文件路径, assistant-app-config.json
|
||||
* 应用配置文件路径, assistant-page-config.json
|
||||
*/
|
||||
appConfigPath,
|
||||
pageConfigPath,
|
||||
/**
|
||||
* 应用进程pid文件路径
|
||||
*/
|
||||
appPidPath,
|
||||
/**
|
||||
* 环境变量配置文件路径
|
||||
*/
|
||||
envConfigPath,
|
||||
};
|
||||
};
|
||||
export type ReturnInitConfigType = ReturnType<typeof initConfig>;
|
||||
|
||||
type AssistantConfigData = {
|
||||
pageApi?: string; // https://kevisual.silkyai.cn
|
||||
proxy?: { user: string; key: string; path: string }[];
|
||||
proxy?: ProxyInfo[];
|
||||
apiProxyList?: ProxyInfo[];
|
||||
description?: string;
|
||||
};
|
||||
@ -120,25 +125,25 @@ export class AssistantConfig {
|
||||
* 应用配置
|
||||
* @returns
|
||||
*/
|
||||
getAppConfig(): AppConfig {
|
||||
const { appConfigPath } = this.configPath;
|
||||
if (!checkFileExists(appConfigPath)) {
|
||||
getPageConfig(): AppConfig {
|
||||
const { pageConfigPath } = this.configPath;
|
||||
if (!checkFileExists(pageConfigPath)) {
|
||||
return {
|
||||
list: [],
|
||||
};
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(appConfigPath, 'utf8'));
|
||||
return JSON.parse(fs.readFileSync(pageConfigPath, 'utf8'));
|
||||
}
|
||||
setAppConfig(config?: AppConfig) {
|
||||
const _config = this.getAppConfig();
|
||||
const _config = this.getPageConfig();
|
||||
const _saveConfig = { ..._config, ...config };
|
||||
const { appConfigPath } = this.configPath;
|
||||
const { pageConfigPath } = this.configPath;
|
||||
|
||||
fs.writeFileSync(appConfigPath, JSON.stringify(_saveConfig, null, 2));
|
||||
fs.writeFileSync(pageConfigPath, JSON.stringify(_saveConfig, null, 2));
|
||||
return _saveConfig;
|
||||
}
|
||||
assAppConfig(app: any) {
|
||||
const config = this.getAppConfig();
|
||||
const config = this.getPageConfig();
|
||||
const assistantConfig = this.getCacheAssistantConfig();
|
||||
const _apps = config.list;
|
||||
const _proxy = assistantConfig.proxy || [];
|
||||
@ -166,7 +171,36 @@ export class AssistantConfig {
|
||||
return config;
|
||||
}
|
||||
getAppList() {
|
||||
return this.getAppConfig().list;
|
||||
return this.getPageConfig().list;
|
||||
}
|
||||
/**
|
||||
* process.env.ASSISTANT_CONFIG_DIR || process.cwd()
|
||||
* configDir是助手配置文件目录,文件内部包函
|
||||
* assistant-config.json 配置文件
|
||||
* assistant-page-config.json 应用配置文件
|
||||
* assistant-app.pid 应用进程pid文件
|
||||
* .env 环境变量配置文件
|
||||
* apps: 服务目录
|
||||
* page: 应用目录
|
||||
* pem: 证书目录
|
||||
* @param configDir
|
||||
*/
|
||||
static detectConfigDir(configDir?: string) {
|
||||
const checkConfigDir = path.resolve(configDir || process.env.ASSISTANT_CONFIG_DIR || process.cwd());
|
||||
const configPath = path.join(checkConfigDir, 'assistant-app');
|
||||
if (checkFileExists(configPath)) {
|
||||
return path.join(checkConfigDir);
|
||||
}
|
||||
const lastConfigPath = path.join(checkConfigDir, '..', 'assistant-app');
|
||||
if (checkFileExists(lastConfigPath)) {
|
||||
return path.join(checkConfigDir, '..');
|
||||
}
|
||||
const lastConfigPath2 = path.join(checkConfigDir, '../..', 'assistant-app');
|
||||
if (checkFileExists(lastConfigPath2)) {
|
||||
return path.join(checkConfigDir, '../..');
|
||||
}
|
||||
// 如果没有找到助手配置文件目录,则返回当前目录, 执行默认创建助手配置文件目录
|
||||
return checkConfigDir;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,8 @@ import fs from 'node:fs';
|
||||
import { AssistantConfig } from '../config/index.ts';
|
||||
import { checkFileExists } from '../file/index.ts';
|
||||
import { chalk } from '@/module/chalk.ts';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
type Attributes = {
|
||||
name: string;
|
||||
value: string;
|
||||
@ -36,24 +38,91 @@ export class HttpsPem {
|
||||
const pemPath = {
|
||||
key: path.join(pemDir, 'https-private-key.pem'),
|
||||
cert: path.join(pemDir, 'https-cert.pem'),
|
||||
config: path.join(pemDir, 'https-config.json'),
|
||||
};
|
||||
const writeCreate = (opts: { key: string; cert: string; data: { createTime: number; expireTime: number } }) => {
|
||||
fs.writeFileSync(pemPath.key, opts.key);
|
||||
fs.writeFileSync(pemPath.cert, opts.cert);
|
||||
fs.writeFileSync(pemPath.config, JSON.stringify(opts.data, null, 2));
|
||||
};
|
||||
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('证书创建成功'))
|
||||
const { key, cert, data } = this.createCert();
|
||||
writeCreate({ key, cert, data });
|
||||
console.log(chalk.green('证书创建成功,浏览器需要导入当前证书'));
|
||||
return {
|
||||
key,
|
||||
cert,
|
||||
};
|
||||
}
|
||||
|
||||
if (!checkFileExists(pemPath.config)) {
|
||||
const data = this.createExpireData();
|
||||
fs.writeFileSync(pemPath.config, JSON.stringify(data, null, 2));
|
||||
}
|
||||
const key = fs.readFileSync(pemPath.key, 'utf-8');
|
||||
const cert = fs.readFileSync(pemPath.cert, 'utf-8');
|
||||
const config = fs.readFileSync(pemPath.config, 'utf-8');
|
||||
let expireTime = 0;
|
||||
try {
|
||||
const data = JSON.parse(config);
|
||||
expireTime = data.expireTime;
|
||||
if (typeof expireTime !== 'number') {
|
||||
throw new Error('expireTime is not a number');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(chalk.red('证书配置文件损坏,重新生成证书'));
|
||||
}
|
||||
const now = new Date().getTime();
|
||||
if (now > expireTime) {
|
||||
this.removeCert();
|
||||
const { key, cert, data } = this.createCert();
|
||||
writeCreate({ key, cert, data });
|
||||
console.log(chalk.green('证书更新成功, 浏览器需要重新导入当前证书'));
|
||||
return {
|
||||
key,
|
||||
cert,
|
||||
};
|
||||
}
|
||||
return {
|
||||
key,
|
||||
cert,
|
||||
};
|
||||
}
|
||||
createExpireData() {
|
||||
const expireTime = new Date().getTime() + 365 * 24 * 60 * 60 * 1000;
|
||||
const expireDate = dayjs(expireTime).format('YYYY-MM-DD HH:mm:ss');
|
||||
return {
|
||||
description: '手动导入证书到浏览器, https-cert.pem文件, 具体使用教程访问 https://kevisual.cn/root/pem-docs/',
|
||||
createTime: new Date().getTime(),
|
||||
expireDate,
|
||||
expireTime,
|
||||
};
|
||||
}
|
||||
/*
|
||||
* 重新生成证书
|
||||
*/
|
||||
removeCert() {
|
||||
const pemDir = this.getPemDir();
|
||||
const pemPath = {
|
||||
key: path.join(pemDir, 'https-private-key.pem'),
|
||||
cert: path.join(pemDir, 'https-cert.pem'),
|
||||
};
|
||||
const oldPath = {
|
||||
key: path.join(pemDir, 'https-private-key.pem.bak'),
|
||||
cert: path.join(pemDir, 'https-cert.pem.bak'),
|
||||
};
|
||||
if (checkFileExists(pemPath.key)) {
|
||||
fs.renameSync(pemPath.key, oldPath.key);
|
||||
}
|
||||
if (checkFileExists(pemPath.cert)) {
|
||||
fs.renameSync(pemPath.cert, oldPath.cert);
|
||||
}
|
||||
}
|
||||
/*
|
||||
* 创建证书
|
||||
* @param attrs 证书属性
|
||||
* @param altNames 证书备用名称
|
||||
*/
|
||||
createCert(attrs?: Attributes[], altNames?: AltNames[]) {
|
||||
const attributes = attrs || [];
|
||||
const altNamesList = altNames || [];
|
||||
@ -71,9 +140,13 @@ export class HttpsPem {
|
||||
],
|
||||
altNamesList,
|
||||
);
|
||||
const data = this.createExpireData();
|
||||
return {
|
||||
key,
|
||||
cert,
|
||||
data: {
|
||||
...data,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -5,3 +5,5 @@ export * from './file/index.ts';
|
||||
export * from './process/index.ts';
|
||||
|
||||
export * from './proxy/index.ts';
|
||||
|
||||
export * from './local-app-manager/index.ts';
|
@ -0,0 +1,18 @@
|
||||
import { Manager } from '@kevisual/local-app-manager/manager';
|
||||
import { AssistantConfig } from '@/module/assistant/index.ts';
|
||||
import path from 'node:path';
|
||||
|
||||
export class AssistantApp extends Manager {
|
||||
config: AssistantConfig;
|
||||
|
||||
constructor(config: AssistantConfig) {
|
||||
const appsPath = config.configPath?.appsDir || path.join(process.cwd(), 'apps');
|
||||
const appsConfigPath = config.configPath?.appsConfigPath;
|
||||
const configFimename = path.basename(appsConfigPath || '');
|
||||
super({
|
||||
appsPath,
|
||||
configFilename: configFimename,
|
||||
});
|
||||
this.config = config;
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export { Manager } from '@kevisual/local-app-manager/manager';
|
||||
export { Pm2Manager } from '@kevisual/local-app-manager/pm2';
|
||||
export { AssistantApp } from './assistant-app.ts';
|
@ -4,6 +4,7 @@ import fs from 'node:fs';
|
||||
import path from 'path';
|
||||
import { ProxyInfo } from './proxy.ts';
|
||||
import { checkFileExists } from '../file/index.ts';
|
||||
import { log } from '@/module/logger.ts';
|
||||
|
||||
export const fileProxy = (req: http.IncomingMessage, res: http.ServerResponse, proxyApi: ProxyInfo) => {
|
||||
// url开头的文件
|
||||
@ -11,6 +12,9 @@ export const fileProxy = (req: http.IncomingMessage, res: http.ServerResponse, p
|
||||
const [user, key, _info] = url.pathname.split('/');
|
||||
const pathname = url.pathname.slice(1);
|
||||
const { indexPath = '', target = '', rootPath = process.cwd() } = proxyApi;
|
||||
if (!indexPath) {
|
||||
return res.end('Not Found indexPath');
|
||||
}
|
||||
try {
|
||||
// 检测文件是否存在,如果文件不存在,则返回404
|
||||
let filePath = '';
|
||||
@ -23,7 +27,7 @@ export const fileProxy = (req: http.IncomingMessage, res: http.ServerResponse, p
|
||||
filePath = path.join(rootPath, target, indexPath);
|
||||
exist = checkFileExists(filePath, true);
|
||||
}
|
||||
console.log('filePath', filePath, exist);
|
||||
log.debug('filePath', { filePath, exist });
|
||||
|
||||
if (!exist) {
|
||||
res.statusCode = 404;
|
||||
|
@ -28,10 +28,13 @@ export const createApiProxy = (api: string, paths: string[] = ['/api', '/v1']) =
|
||||
return pathList;
|
||||
};
|
||||
|
||||
export const apiProxy = (req: http.IncomingMessage, res: http.ServerResponse, proxyApi: ProxyInfo) => {
|
||||
export const httpProxy = (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 });
|
||||
// console.log('proxyApi', { url: req.url, target: _u.href });
|
||||
if (proxyApi.pathname) {
|
||||
_u.pathname = _u.pathname.replace(proxyApi.path, proxyApi.pathname);
|
||||
}
|
||||
// 设置代理请求的目标 URL 和请求头
|
||||
let header: any = {};
|
||||
if (req.headers?.['Authorization'] && !req.headers?.['authorization']) {
|
||||
@ -41,6 +44,9 @@ export const apiProxy = (req: http.IncomingMessage, res: http.ServerResponse, pr
|
||||
// 处理大小写不一致的cookie
|
||||
header.cookie = req.headers['cookie'] || req.headers['Cookie'];
|
||||
}
|
||||
// console.log('host', req.headers.host, proxyApi, _u.href, req.headers.authorization);
|
||||
const origin = req.headers?.origin || req.headers?.referer || 'http://localhost';
|
||||
const cookieHost = new URL(origin).host.split(':')[0];
|
||||
// 提取req的headers中的非HOST的header
|
||||
const headers = Object.keys(req.headers).filter((item) => item && item.toLowerCase() !== 'host');
|
||||
headers.forEach((item) => {
|
||||
@ -54,11 +60,12 @@ export const apiProxy = (req: http.IncomingMessage, res: http.ServerResponse, pr
|
||||
}
|
||||
header[item] = req.headers[item];
|
||||
});
|
||||
const options: http.RequestOptions = {
|
||||
const options: http.RequestOptions | https.RequestOptions = {
|
||||
host: _u.hostname,
|
||||
path: req.url,
|
||||
method: req.method,
|
||||
headers: {
|
||||
['kevisual-origin']: 'assistant',
|
||||
...header,
|
||||
},
|
||||
};
|
||||
@ -67,12 +74,20 @@ export const apiProxy = (req: http.IncomingMessage, res: http.ServerResponse, pr
|
||||
// @ts-ignore
|
||||
options.port = _u.port;
|
||||
}
|
||||
const isHttps = _u.protocol === 'https:';
|
||||
const httpProxy = _u.protocol === 'https:' ? https : http;
|
||||
console.log('httpProxy', { isHttps, target, url: _u.href });
|
||||
if (isHttps) {
|
||||
// @ts-ignore
|
||||
options.rejectUnauthorized = false; // 忽略证书错误
|
||||
}
|
||||
|
||||
// 创建代理请求
|
||||
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'));
|
||||
proxyRes.headers['set-cookie'] = proxyRes.headers['set-cookie'].map((cookie) => rewriteCookieDomain(cookie, cookieHost));
|
||||
console.log('rewritten set-cookie:', proxyRes.headers['set-cookie']);
|
||||
}
|
||||
// 将代理服务器的响应头和状态码返回给客户端
|
||||
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
@ -1,5 +1,5 @@
|
||||
export * from './proxy.ts';
|
||||
export * from './file-proxy.ts';
|
||||
export { default as send } from 'send';
|
||||
export * from './api-proxy.ts';
|
||||
export * from './http-proxy.ts';
|
||||
export * from './ws-proxy.ts';
|
@ -1,4 +1,7 @@
|
||||
export type ProxyInfo = {
|
||||
/**
|
||||
* 代理路径, 比如/root/center, 匹配的路径
|
||||
*/
|
||||
path?: string;
|
||||
/**
|
||||
* 目标地址
|
||||
@ -7,21 +10,28 @@ export type ProxyInfo = {
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
type?: 'static' | 'dynamic' | 'minio';
|
||||
type?: 'file' | 'dynamic' | 'minio' | 'http';
|
||||
/**
|
||||
* 是否使用websocket
|
||||
* 目标的 pathname, 默认为请求的url.pathname, 设置了pathname,则会使用pathname作为请求的url.pathname
|
||||
* @default undefined
|
||||
* @example /api/v1/user
|
||||
*/
|
||||
pathname?: string;
|
||||
/**
|
||||
* 是否使用websocket, http
|
||||
* @default false
|
||||
*/
|
||||
ws?: boolean;
|
||||
/**
|
||||
* 首要文件,比如index.html, 设置了首要文件,如果文件不存在,则访问首要文件
|
||||
* 首要文件,比如index.html, type为fileProxy代理有用 设置了首要文件,如果文件不存在,则访问首要文件
|
||||
*/
|
||||
indexPath?: string;
|
||||
/**
|
||||
* 根路径, 默认是process.cwd()
|
||||
* 根路径, 默认是process.cwd(), type为fileProxy代理有用,必须为绝对路径
|
||||
*/
|
||||
rootPath?: string;
|
||||
};
|
||||
|
||||
export type ApiList = {
|
||||
path: string;
|
||||
/**
|
||||
|
@ -1,49 +1,103 @@
|
||||
import { Server } from 'http';
|
||||
import WebSocket from 'ws';
|
||||
import type { Server as HttpsServer } from 'node:https';
|
||||
import type { Server as HttpServer } from 'node:http';
|
||||
import type { Http2Server } from 'node:http2';
|
||||
import { WebSocket } from 'ws';
|
||||
import { ProxyInfo } from './proxy.ts';
|
||||
import { WebSocketServer } from 'ws';
|
||||
|
||||
export const wss = new WebSocketServer({
|
||||
noServer: true,
|
||||
});
|
||||
|
||||
type WssAppOptions = {
|
||||
wss: WebSocketServer;
|
||||
apiList: ProxyInfo[];
|
||||
};
|
||||
class WssApp {
|
||||
wss: WebSocketServer;
|
||||
apiList: ProxyInfo[];
|
||||
constructor(opts: WssAppOptions) {
|
||||
this.wss = opts.wss;
|
||||
this.apiList = opts.apiList;
|
||||
}
|
||||
upgrade(request: any, socket: any, head: any) {
|
||||
const req = request as any;
|
||||
const wss = this.wss;
|
||||
const url = new URL(req.url, 'http://localhost');
|
||||
const id = url.searchParams.get('id');
|
||||
console.log('upgrade', request.url, id);
|
||||
wss.handleUpgrade(req, socket, head, (ws) => {
|
||||
wss.emit('connection', ws, req);
|
||||
});
|
||||
}
|
||||
async handleConnection(ws: WebSocket, req: any) {
|
||||
console.log('connected', req.url);
|
||||
const that = this;
|
||||
const proxyApiList: ProxyInfo[] = that.apiList || [];
|
||||
const proxyApi = proxyApiList.find((item) => req.url.startsWith(item.path));
|
||||
|
||||
if (proxyApi && proxyApi.ws) {
|
||||
const _u = new URL(req.url, `${proxyApi.target}`);
|
||||
if (proxyApi.pathname) {
|
||||
_u.pathname = _u.pathname.replace(proxyApi.path, proxyApi.pathname);
|
||||
}
|
||||
const isHttps = _u.protocol === 'https:';
|
||||
const wsProtocol = isHttps ? 'wss' : 'ws';
|
||||
const wsUrl = `${wsProtocol}://${_u.host}${_u.pathname}`;
|
||||
console.log('WebSocket proxy URL', { wsUrl });
|
||||
const proxySocket = new WebSocket(wsUrl, {
|
||||
// headers: req.headers,
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
proxySocket.on('open', () => {
|
||||
ws.on('message', (message) => {
|
||||
console.log('WebSocket client message', message);
|
||||
proxySocket.send(message);
|
||||
});
|
||||
proxySocket.on('message', (message) => {
|
||||
// console.log('WebSocket proxy message', message);
|
||||
ws.send(message.toString());
|
||||
});
|
||||
});
|
||||
|
||||
proxySocket.on('error', (err) => {
|
||||
console.error(`WebSocket proxy error: ${err.message}`);
|
||||
});
|
||||
proxySocket.on('close', () => {
|
||||
console.log('WebSocket proxy closed');
|
||||
});
|
||||
ws.on('error', (err) => {
|
||||
console.error('WebSocket client error', err);
|
||||
proxySocket?.close?.();
|
||||
});
|
||||
ws.on('close', () => {
|
||||
console.log('WebSocket client closed');
|
||||
proxySocket?.close?.();
|
||||
});
|
||||
} else {
|
||||
ws.send(JSON.stringify({ type: 'error', message: 'not found' }));
|
||||
ws.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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,
|
||||
export const wsProxy = (server: HttpServer | HttpsServer | Http2Server, config: { apiList: ProxyInfo[] }) => {
|
||||
// console.log('Upgrade initialization started');
|
||||
const wssApp = new WssApp({
|
||||
wss: wss,
|
||||
apiList: config.apiList,
|
||||
});
|
||||
|
||||
proxySocket.on('open', () => {
|
||||
socket.on('data', (data) => {
|
||||
proxySocket.send(data);
|
||||
wss.on('connection', async (ws, req) => {
|
||||
// console.log('WebSocket connection established');
|
||||
await wssApp.handleConnection(ws, req);
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
// 处理升级请求
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
wssApp.upgrade(request, socket, head);
|
||||
});
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { program, Command } from 'commander';
|
||||
import { assistantConfig } from './config.ts';
|
||||
import fs from 'fs';
|
||||
// 将多个子命令加入主程序中
|
||||
let version = '0.0.1';
|
||||
@ -15,7 +16,7 @@ const ls = new Command('ls').description('List files in the current directory').
|
||||
});
|
||||
program.addCommand(ls);
|
||||
|
||||
export { program, Command };
|
||||
export { program, Command, assistantConfig };
|
||||
|
||||
/**
|
||||
* 在命令行中运行程序
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { app } from './app.ts';
|
||||
import { proxyRoute } from './services/proxy/proxy-page-index.ts';
|
||||
import { proxyRoute, proxyWs } from './services/proxy/proxy-page-index.ts';
|
||||
|
||||
app.listen(51015, () => {
|
||||
console.log('Server is running on http://localhost:51015');
|
||||
});
|
||||
|
||||
app.server.on(proxyRoute);
|
||||
|
||||
proxyWs();
|
@ -1,3 +1,4 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { checkFileExists, AssistantConfig } from '@/module/assistant/index.ts';
|
||||
import { chalk } from '@/module/chalk.ts';
|
||||
@ -40,5 +41,10 @@ export class AssistantInit extends AssistantConfig {
|
||||
});
|
||||
console.log(chalk.green('助手配置文件创建成功'));
|
||||
}
|
||||
const env = this.configPath?.envConfigPath;
|
||||
if (!checkFileExists(env, true)) {
|
||||
fs.writeFileSync(env, '# 环境配置文件\n');
|
||||
console.log(chalk.green('助手环境配置文件创建成功'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,8 +43,7 @@ export class LocalProxy {
|
||||
}
|
||||
}
|
||||
init() {
|
||||
const frontAppDir = this.assistantConfig.configPath?.appDir;
|
||||
console.log('frontAppDir', frontAppDir);
|
||||
const frontAppDir = this.assistantConfig.configPath?.pageDir;
|
||||
if (frontAppDir) {
|
||||
const userList = fs.readdirSync(frontAppDir);
|
||||
const localProxyProxyList: ProxyType[] = [];
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { fileProxy, apiProxy, createApiProxy } from '@/module/assistant/index.ts';
|
||||
import { fileProxy, httpProxy, createApiProxy, wsProxy } from '@/module/assistant/index.ts';
|
||||
import http from 'http';
|
||||
import { LocalProxy } from './local-proxy.ts';
|
||||
import { assistantConfig } from '@/app.ts';
|
||||
import { assistantConfig, app } 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 appDir = assistantConfig.configPath?.pageDir;
|
||||
const url = new URL(req.url, 'http://localhost');
|
||||
const pathname = url.pathname;
|
||||
if (pathname.startsWith('/favicon.ico')) {
|
||||
@ -17,7 +17,7 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp
|
||||
return;
|
||||
}
|
||||
if (pathname.startsWith('/client')) {
|
||||
console.log('handle by router');
|
||||
console.debug('handle by router');
|
||||
return;
|
||||
}
|
||||
// client, api, v1, serve 开头的拦截
|
||||
@ -25,8 +25,8 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp
|
||||
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, {
|
||||
log.debug('apiBackendProxy', { apiBackendProxy, url: req.url });
|
||||
return httpProxy(req, res, {
|
||||
path: apiBackendProxy.path,
|
||||
target: apiBackendProxy.target,
|
||||
});
|
||||
@ -45,13 +45,19 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp
|
||||
}
|
||||
const proxyApiList = _assistantConfig?.proxy || [];
|
||||
const proxyApi = proxyApiList.find((item) => pathname.startsWith(item.path));
|
||||
if (proxyApi) {
|
||||
log.log('proxyApi', { proxyApi, pathname });
|
||||
const { user, key } = proxyApi;
|
||||
if (proxyApi && proxyApi.type === 'file') {
|
||||
log.debug('proxyApi', { proxyApi, pathname });
|
||||
const _indexPath = proxyApi.indexPath || `${_user}/${_app}/index.html`;
|
||||
const _rootPath = proxyApi.rootPath;
|
||||
if (!_rootPath) {
|
||||
log.error('Not Found rootPath', { proxyApi, pathname });
|
||||
return res.end(`Not Found [${proxyApi.path}] rootPath`);
|
||||
}
|
||||
return fileProxy(req, res, {
|
||||
path: proxyApi.path, // 代理路径, 比如/root/center
|
||||
rootPath: appDir, // 根路径
|
||||
indexPath: `${user}/${key}/index.html`, // 首页路径
|
||||
rootPath: proxyApi.rootPath,
|
||||
...proxyApi,
|
||||
indexPath: _indexPath, // 首页路径
|
||||
});
|
||||
}
|
||||
const localProxyProxyList = localProxy.getLocalProxyList();
|
||||
@ -60,26 +66,31 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp
|
||||
log.log('localProxyProxy', { localProxyProxy, url: req.url });
|
||||
return fileProxy(req, res, {
|
||||
path: localProxyProxy.path,
|
||||
rootPath: assistantConfig.configPath?.appDir,
|
||||
rootPath: 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, {
|
||||
return httpProxy(req, res, {
|
||||
path: centerProxy.path,
|
||||
target: centerProxy.target,
|
||||
type: 'static',
|
||||
type: 'http',
|
||||
});
|
||||
}
|
||||
log.debug('handle by router 404', req.url);
|
||||
|
||||
res.statusCode = 404;
|
||||
res.end('Not Found Proxy');
|
||||
// console.log('getCacheAssistantConfig().pageApi', getCacheAssistantConfig().pageApi);
|
||||
// return apiProxy(req, res, {
|
||||
// path: url.pathname,
|
||||
// target: getCacheAssistantConfig().pageApi,
|
||||
// });
|
||||
};
|
||||
|
||||
export const proxyWs = () => {
|
||||
const apiProxyList = assistantConfig.getCacheAssistantConfig()?.apiProxyList || [];
|
||||
const proxy = assistantConfig.getCacheAssistantConfig()?.proxy || [];
|
||||
const proxyApi = [...apiProxyList, ...proxy].filter((item) => item.ws);
|
||||
log.debug('proxyApi ', proxyApi);
|
||||
wsProxy(app.server.server, {
|
||||
apiList: proxyApi,
|
||||
});
|
||||
};
|
||||
|
27
assistant/src/test/ws.ts
Normal file
27
assistant/src/test/ws.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { WebSocket } from 'ws';
|
||||
|
||||
export const main = () => {
|
||||
// https://192.168.31.39:16000/tts
|
||||
const url = 'https://192.168.31.39:16000/tts';
|
||||
// const wss
|
||||
const ws = new WebSocket('wss://192.168.31.39:16000/tts', {
|
||||
rejectUnauthorized: false, // 禁用证书验证
|
||||
});
|
||||
|
||||
ws.on('open', () => {
|
||||
console.log('WebSocket connection opened');
|
||||
});
|
||||
|
||||
ws.on('message', (data) => {
|
||||
console.log(`Received message: ${data}`);
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
console.log('WebSocket test connection closed');
|
||||
});
|
||||
|
||||
ws.on('error', (error) => {
|
||||
console.error(`WebSocket error: ${error}`);
|
||||
});
|
||||
};
|
||||
main();
|
14
assistant/tasks/silkyai/talkshow.ts
Normal file
14
assistant/tasks/silkyai/talkshow.ts
Normal file
@ -0,0 +1,14 @@
|
||||
const task1 = {
|
||||
description: '下载前端应用 root/center 应用',
|
||||
command: 'ev app download -i root/center -o assistant-app/page',
|
||||
};
|
||||
|
||||
const task2 = {
|
||||
description: '下载前端应用 root/talkshow-admin 应用',
|
||||
command: 'ev app download -i root/talkshow-admin -o assistant-app/page',
|
||||
};
|
||||
|
||||
const task3 = {
|
||||
description: '安装后端应用 root/talkshow-code-center 应用',
|
||||
command: 'ev app download -i root/talkshow-code-center -t app -o assistant-app/apps/talkshow-code-center',
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
import { runParser } from '../dist/assistant.mjs';
|
||||
|
||||
runParser(process.argv);
|
1224
pnpm-lock.yaml
generated
1224
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user