This commit is contained in:
熊潇 2025-04-26 03:15:11 +08:00
parent 9eb4d06939
commit bcc12209e0
28 changed files with 1708 additions and 126 deletions

View File

@ -6,3 +6,6 @@ dist
pack-dist
assistant-app
.env*
!.env*example

View File

@ -0,0 +1,4 @@
#!/usr/bin/env node
import { runParser } from '../dist/assistant-server.mjs';
runParser(process.argv);

View File

@ -0,0 +1,4 @@
#!/usr/bin/env node
import { runParser } from '../dist/assistant.mjs';
runParser(process.argv);

View File

@ -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_*',
});

View File

@ -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"
}
}

View File

@ -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: {

View 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
View 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,
});

View File

@ -1,5 +1,6 @@
import { program, runProgram } from '@/program.ts';
import './command/init/index.ts';
import './command/app-manager/index.ts';
/**
*

View File

@ -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;
}
}

View File

@ -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,
},
};
}
}

View File

@ -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';

View File

@ -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;
}
}

View File

@ -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';

View File

@ -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;

View File

@ -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);

View File

@ -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';

View File

@ -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;
/**

View File

@ -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,
});
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();
}
export const wsProxy = (server: HttpServer | HttpsServer | Http2Server, config: { apiList: ProxyInfo[] }) => {
// console.log('Upgrade initialization started');
const wssApp = new WssApp({
wss: wss,
apiList: config.apiList,
});
wss.on('connection', async (ws, req) => {
// console.log('WebSocket connection established');
await wssApp.handleConnection(ws, req);
});
// 处理升级请求
server.on('upgrade', (request, socket, head) => {
wssApp.upgrade(request, socket, head);
});
};

View File

@ -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 };
/**
*

View File

@ -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();

View File

@ -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('助手环境配置文件创建成功'));
}
}
}

View File

@ -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[] = [];

View File

@ -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
View 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();

View 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',
};

View File

@ -0,0 +1,4 @@
#!/usr/bin/env node
import { runParser } from '../dist/assistant.mjs';
runParser(process.argv);

1224
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff