feat: 更新助手配置,添加应用ID和URL,优化身份验证和代理逻辑
This commit is contained in:
@@ -68,46 +68,92 @@ export const initConfig = (configRootPath: string) => {
|
||||
export type ReturnInitConfigType = ReturnType<typeof initConfig>;
|
||||
|
||||
type AuthPermission = {
|
||||
type?: 'auth-proxy' | 'public' | 'private' | 'project';
|
||||
share?: 'public' | 'private' | 'protected';
|
||||
username?: string; // 用户名
|
||||
admin?: string[];
|
||||
};
|
||||
export type AssistantConfigData = {
|
||||
pageApi?: string; // https://kevisual.cn
|
||||
app?: {
|
||||
/**
|
||||
* 应用ID, 唯一标识,识别是那个设备
|
||||
*/
|
||||
id?: string;
|
||||
/**
|
||||
* 应用地址
|
||||
*/
|
||||
url?: string;
|
||||
}
|
||||
token?: string;
|
||||
registry?: string; // https://kevisual.cn
|
||||
/**
|
||||
* 前端代理,比如/root/home 转到https://kevisual.cn/root/home
|
||||
* path?: string;
|
||||
* target?: string;
|
||||
* pathname?: string;
|
||||
* 例子: { path: '/root/home', target: 'https://kevisual.cn', pathname: '/root/home' }
|
||||
*/
|
||||
proxy?: ProxyInfo[];
|
||||
apiProxyList?: ProxyInfo[];
|
||||
/**
|
||||
* API 代理配置, 比如,api开头的,v1开头的等等
|
||||
*/
|
||||
api?: {
|
||||
proxy?: ProxyInfo[];
|
||||
}
|
||||
description?: string;
|
||||
/**
|
||||
* 服务启动
|
||||
* 服务启动,
|
||||
* path是配置 127.0.0.1
|
||||
* port是配置端口号
|
||||
*/
|
||||
server?: {
|
||||
path?: string;
|
||||
port?: number;
|
||||
};
|
||||
/**
|
||||
* 被远程调用配置
|
||||
* url: 远程应用地址 https://kevisual.cn/ws/proxy
|
||||
* enabled: 是否启用远程应用
|
||||
*/
|
||||
share?: {
|
||||
url: string;
|
||||
enabled?: boolean; // 是否启用远程应用
|
||||
name: string;
|
||||
enabled?: boolean;
|
||||
};
|
||||
/**
|
||||
* 对pages目录文件监听
|
||||
*/
|
||||
watch?: {
|
||||
enabled?: boolean;
|
||||
};
|
||||
/**
|
||||
* 首页
|
||||
* 首页, 访问 `/` 自动会打开的首页地址
|
||||
* 例如: /root/home
|
||||
*/
|
||||
home?: string;
|
||||
/**
|
||||
* 启用本地AI代理
|
||||
*/
|
||||
ai?: {
|
||||
enabled?: boolean;
|
||||
provider?: string | 'DeepSeek' | 'SiliconFlow';
|
||||
provider?: string | 'DeepSeek' | 'Custom';
|
||||
apiKey?: string;
|
||||
model?: string;
|
||||
};
|
||||
/**
|
||||
* 自定义脚本, asst 启动时会执行这些脚本
|
||||
*/
|
||||
scripts?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
/**
|
||||
* 认证和权限配置
|
||||
* share: protected 需要认证代理访问(默认), public 公开访问, private 私有访问
|
||||
* share 是对外共享 pages 目录下的页面
|
||||
*/
|
||||
auth?: AuthPermission;
|
||||
/**
|
||||
* HTTPS 证书配置, 启用后,助手服务会启用 HTTPS 服务, 默认 HTTP
|
||||
* 理论上也不需要https,因为可以通过反向代理实现https
|
||||
*/
|
||||
https?: {
|
||||
type?: 'https' | 'http';
|
||||
keyPath?: string; // 证书私钥路径
|
||||
@@ -149,12 +195,14 @@ export class AssistantConfig {
|
||||
}
|
||||
}
|
||||
getConfigPath() { }
|
||||
getConfig() {
|
||||
getConfig(): AssistantConfigData {
|
||||
try {
|
||||
if (!checkFileExists(this.configPath.configPath)) {
|
||||
fs.writeFileSync(this.configPath.configPath, JSON.stringify({ proxy: [] }, null, 2));
|
||||
return {
|
||||
pageApi: '',
|
||||
app: {
|
||||
url: 'https://kevisual.cn',
|
||||
},
|
||||
proxy: [],
|
||||
};
|
||||
}
|
||||
@@ -163,7 +211,9 @@ export class AssistantConfig {
|
||||
} catch (error) {
|
||||
console.error('file read', error.message);
|
||||
return {
|
||||
pageApi: '',
|
||||
app: {
|
||||
url: 'https://kevisual.cn',
|
||||
},
|
||||
proxy: [],
|
||||
};
|
||||
}
|
||||
@@ -176,14 +226,19 @@ export class AssistantConfig {
|
||||
}
|
||||
getRegistry() {
|
||||
const config = this.getCacheAssistantConfig();
|
||||
return config?.registry || config?.pageApi;
|
||||
return config?.registry || config?.app?.url || 'https://kevisual.cn';
|
||||
}
|
||||
/**
|
||||
* 设置 assistant-config.json 配置
|
||||
* @param config
|
||||
* @returns
|
||||
*/
|
||||
setConfig(config?: AssistantConfigData) {
|
||||
setConfig(config?: AssistantConfigData, force?: boolean) {
|
||||
if (force) {
|
||||
this.config = config || {};
|
||||
fs.writeFileSync(this.configPath.configPath, JSON.stringify(this.config, null, 2));
|
||||
return this.config;
|
||||
}
|
||||
const myConfig = this.getCacheAssistantConfig();
|
||||
const newConfig = { ...myConfig, ...config };
|
||||
this.config = newConfig;
|
||||
|
||||
@@ -4,7 +4,7 @@ export type ProxyInfo = {
|
||||
*/
|
||||
path?: string;
|
||||
/**
|
||||
* 目标地址
|
||||
* 目标url地址,比如http://localhost:3000
|
||||
*/
|
||||
target?: string;
|
||||
/**
|
||||
@@ -23,10 +23,12 @@ export type ProxyInfo = {
|
||||
*/
|
||||
ws?: boolean;
|
||||
/**
|
||||
* 首要文件,比如index.html, type为fileProxy代理有用 设置了首要文件,如果文件不存在,则访问首要文件
|
||||
* type为file时有效
|
||||
* 索引文件,比如index.html, type为fileProxy代理有用 设置了索引文件,如果文件不存在,则访问索引文件
|
||||
*/
|
||||
indexPath?: string;
|
||||
/**
|
||||
* type为file时有效
|
||||
* 根路径, 默认是process.cwd(), type为fileProxy代理有用,必须为绝对路径
|
||||
*/
|
||||
rootPath?: string;
|
||||
|
||||
42
assistant/src/module/http-token.ts
Normal file
42
assistant/src/module/http-token.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import http from 'http';
|
||||
export const error = (msg: string, code = 500) => {
|
||||
return JSON.stringify({ code, message: msg });
|
||||
};
|
||||
const cookie = {
|
||||
parse: (cookieStr: string) => {
|
||||
const cookies: Record<string, string> = {};
|
||||
const cookiePairs = cookieStr.split(';');
|
||||
for (const pair of cookiePairs) {
|
||||
const [key, value] = pair.split('=').map((v) => v.trim());
|
||||
if (key && value) {
|
||||
cookies[key] = decodeURIComponent(value);
|
||||
}
|
||||
}
|
||||
return cookies;
|
||||
}
|
||||
}
|
||||
export const getToken = async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
let token = (req.headers?.['authorization'] as string) || (req.headers?.['Authorization'] as string) || '';
|
||||
const url = new URL(req.url || '', 'http://localhost');
|
||||
const resNoPermission = () => {
|
||||
res.statusCode = 401;
|
||||
res.end(error('Invalid authorization'));
|
||||
return { tokenUser: null, token: null };
|
||||
};
|
||||
if (!token) {
|
||||
token = url.searchParams.get('token') || '';
|
||||
}
|
||||
if (!token) {
|
||||
const parsedCookies = cookie.parse(req.headers.cookie || '');
|
||||
token = parsedCookies.token || '';
|
||||
}
|
||||
if (!token) {
|
||||
return resNoPermission();
|
||||
}
|
||||
if (token) {
|
||||
token = token.replace('Bearer ', '');
|
||||
}
|
||||
|
||||
return { token };
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ export class LocalProxy {
|
||||
initFromAssistantConfig(assistantConfig?: AssistantConfig) {
|
||||
if (!assistantConfig) return;
|
||||
this.pagesDir = assistantConfig.configPath?.pagesDir || '';
|
||||
this.watch = !!assistantConfig.getCacheAssistantConfig()?.watch.enabled;
|
||||
this.watch = assistantConfig.getCacheAssistantConfig?.()?.watch?.enabled ?? true;
|
||||
this.init();
|
||||
if (this.watch) {
|
||||
this.onWatch();
|
||||
@@ -112,14 +112,26 @@ export class LocalProxy {
|
||||
};
|
||||
fs.watch(frontAppDir, { recursive: true }, (eventType, filename) => {
|
||||
if (eventType === 'rename' || eventType === 'change') {
|
||||
// 过滤 node_modules 目录
|
||||
if (filename && filename.includes('node_modules')) {
|
||||
return;
|
||||
}
|
||||
// 只监听 js、html、css 文件
|
||||
const validExtensions = ['.js', '.html', '.css', '.json', '.png'];
|
||||
const hasValidExtension = validExtensions.some(ext => filename && filename.endsWith(ext));
|
||||
|
||||
if (!hasValidExtension) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = path.join(frontAppDir, filename);
|
||||
try {
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat.isDirectory() || filename.endsWith('.html')) {
|
||||
if (stat.isFile() || stat.isDirectory()) {
|
||||
// 重新加载
|
||||
debounce(that.init.bind(that), 5 * 1000);
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch (error) { }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ import pm2 from 'pm2';
|
||||
import { logger } from './logger.ts';
|
||||
|
||||
export async function reload() {
|
||||
if (process.env.PM2_HOME === undefined) {
|
||||
return;
|
||||
}
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
pm2.connect((err) => {
|
||||
if (err) {
|
||||
|
||||
Reference in New Issue
Block a user