Refactor client routes and add IP fetching functionality
- Moved client route definitions to separate files for better organization. - Added new route to fetch client IP addresses, supporting both IPv4 and IPv6. - Implemented system information retrieval in the client routes. - Updated package dependencies to their latest versions. - Adjusted call route to prevent overwriting existing routes.
This commit is contained in:
@@ -12,6 +12,7 @@ import { initApi } from '@kevisual/api/proxy'
|
||||
import { Query } from '@kevisual/query';
|
||||
import { initLightCode } from '@/module/light-code/index.ts';
|
||||
import { ModuleResolver } from './assistant-app-resolve.ts';
|
||||
import z from 'zod';
|
||||
export class AssistantApp extends Manager {
|
||||
config: AssistantConfig;
|
||||
pagesPath: string;
|
||||
@@ -149,7 +150,9 @@ export class AssistantApp extends Manager {
|
||||
routerProxy.push({
|
||||
type: 'lightcode',
|
||||
lightcode: {
|
||||
check: true,
|
||||
id: 'main',
|
||||
sync: 'remote',
|
||||
rootPath: path.join(this.config.configPath.appsDir, 'light-code', 'code'),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -162,9 +165,22 @@ export class AssistantApp extends Manager {
|
||||
continue;
|
||||
}
|
||||
if (proxyInfo.type === 'lightcode') {
|
||||
const schema = z.object({
|
||||
rootPath: z.string().describe('light-code 代码存放路径'),
|
||||
sync: z.enum(['remote', 'local', 'both']).describe('同步方式,remote: 仅从远程拉取,local: 仅上传本地代码,both: 双向同步').default('remote'),
|
||||
});
|
||||
const parseRes = schema.safeParse(proxyInfo.lightcode);
|
||||
if (!parseRes.success) {
|
||||
console.warn('lightcode 配置错误', parseRes.error);
|
||||
continue;
|
||||
}
|
||||
const lightcodeConfig = parseRes.data;
|
||||
|
||||
initLightCode({
|
||||
router: this.mainApp,
|
||||
config: this.config
|
||||
config: this.config,
|
||||
sync: lightcodeConfig.sync,
|
||||
rootPath: lightcodeConfig.rootPath,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -48,10 +48,8 @@ export type ProxyInfo = {
|
||||
},
|
||||
lightcode?: {
|
||||
id?: string;
|
||||
/**
|
||||
* 是否检测远程服务更新
|
||||
*/
|
||||
check?: boolean;
|
||||
sync?: 'remote' | 'local' | 'both';
|
||||
rootPath?: string;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { App, QueryRouterServer } from '@kevisual/router';
|
||||
import { AssistantInit } from '../../services/init/index.ts';
|
||||
import path from 'node:path';
|
||||
import fs, { write } from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import fs from 'node:fs';
|
||||
import glob from 'fast-glob';
|
||||
import { runCode } from './run.ts';
|
||||
const codeDemoId = '0e700dc8-90dd-41b7-91dd-336ea51de3d2'
|
||||
@@ -35,44 +34,47 @@ const writeCodeDemo = async (appDir: string) => {
|
||||
}
|
||||
// writeCodeDemo(path.join(os.homedir(), 'kevisual', 'assistant-app', 'apps'));
|
||||
|
||||
type opts = {
|
||||
type Opts = {
|
||||
router: QueryRouterServer | App
|
||||
config: AssistantConfig | AssistantInit
|
||||
sync?: boolean
|
||||
sync?: 'remote' | 'local' | 'both'
|
||||
rootPath?: string
|
||||
}
|
||||
type LightCodeFile = {
|
||||
id?: string, code?: string, hash?: string, filepath: string
|
||||
}
|
||||
export const initLightCode = async (opts: opts) => {
|
||||
export const initLightCode = async (opts: Opts) => {
|
||||
// 注册 light-code 路由
|
||||
console.log('初始化 light-code 路由');
|
||||
const config = opts.config as AssistantInit;
|
||||
const app = opts.router;
|
||||
const token = config.getConfig()?.token || '';
|
||||
const query = config.query;
|
||||
const sync = opts.sync ?? true;
|
||||
const sync = opts.sync ?? 'remote';
|
||||
if (!config || !app) {
|
||||
console.error('initLightCode 缺少必要参数, config 或 app');
|
||||
return;
|
||||
}
|
||||
const appDir = config.configPath.appsDir;
|
||||
const lightcodeDir = path.join(appDir, 'light-code', 'code');
|
||||
const lightcodeDir = opts.rootPath;
|
||||
if (!fs.existsSync(lightcodeDir)) {
|
||||
fs.mkdirSync(lightcodeDir, { recursive: true });
|
||||
}
|
||||
let diffList: LightCodeFile[] = [];
|
||||
|
||||
const codeFiles = glob.sync(['**/*.ts', '**/*.js'], {
|
||||
cwd: lightcodeDir,
|
||||
onlyFiles: true,
|
||||
}).map(file => {
|
||||
return {
|
||||
filepath: path.join(lightcodeDir, file),
|
||||
// hash: getHash(path.join(lightcodeDir, file))
|
||||
}
|
||||
});
|
||||
const findGlob = (opts: { cwd: string }) => {
|
||||
return glob.sync(['**/*.ts', '**/*.js'], {
|
||||
cwd: opts.cwd,
|
||||
onlyFiles: true,
|
||||
}).map(file => {
|
||||
return {
|
||||
filepath: path.join(opts.cwd, file),
|
||||
// hash: getHash(path.join(lightcodeDir, file))
|
||||
}
|
||||
});
|
||||
}
|
||||
const codeFiles = findGlob({ cwd: lightcodeDir });
|
||||
|
||||
if (sync) {
|
||||
if (sync === 'remote' || sync === 'both') {
|
||||
const queryRes = await query.post({
|
||||
path: 'light-code',
|
||||
key: 'list',
|
||||
@@ -100,13 +102,6 @@ export const initLightCode = async (opts: opts) => {
|
||||
fs.writeFileSync(item.filepath, item.code, 'utf-8');
|
||||
// console.log(`新增 light-code 文件: ${item.filepath}`);
|
||||
}
|
||||
|
||||
// 执行删除
|
||||
for (const filepath of toDelete) {
|
||||
fs.unlinkSync(filepath.filepath);
|
||||
// console.log(`删除 light-code 文件: ${filepath.filepath}`);
|
||||
}
|
||||
|
||||
// 执行更新
|
||||
for (const item of toUpdate) {
|
||||
fs.writeFileSync(item.filepath, item.code, 'utf-8');
|
||||
@@ -117,23 +112,38 @@ export const initLightCode = async (opts: opts) => {
|
||||
// filepath: d.filepath,
|
||||
// hash: d.hash
|
||||
// }));
|
||||
|
||||
if (sync === 'remote') {
|
||||
// 执行删除
|
||||
for (const filepath of toDelete) {
|
||||
// console.log(`删除 light-code 文件: ${filepath.filepath}`);
|
||||
const parentDir = path.dirname(filepath.filepath);
|
||||
// console.log('parentDir', parentDir, lightcodeDir);
|
||||
if (parentDir === lightcodeDir) {
|
||||
fs.unlinkSync(filepath.filepath);
|
||||
}
|
||||
}
|
||||
}
|
||||
diffList = findGlob({ cwd: lightcodeDir });
|
||||
} else {
|
||||
console.error('light-code 同步失败', queryRes.message);
|
||||
diffList = codeFiles;
|
||||
}
|
||||
} else {
|
||||
} else if (sync === 'local') {
|
||||
diffList = codeFiles;
|
||||
}
|
||||
|
||||
|
||||
for (const file of diffList) {
|
||||
const tsPath = file.filepath;
|
||||
const runRes = await runCode(tsPath, { path: 'router', key: 'list' }, { timeout: 10000 });
|
||||
const runRes = await runCode(tsPath, { message: { path: 'router', key: 'list' } }, { timeout: 10000 });
|
||||
// console.log('light-code 运行结果', file.filepath, runRes);
|
||||
if (runRes.success) {
|
||||
const res = runRes.data;
|
||||
if (res.code === 200) {
|
||||
const list = res.data?.list || [];
|
||||
for (const routerItem of list) {
|
||||
// console.log('注册 light-code 路由项:', routerItem.id, routerItem.path);
|
||||
if (routerItem.path?.includes('auth') || routerItem.path?.includes('router') || routerItem.path?.includes('call')) {
|
||||
continue;
|
||||
}
|
||||
@@ -144,6 +154,10 @@ export const initLightCode = async (opts: opts) => {
|
||||
} else {
|
||||
metadata.tags = ['light-code'];
|
||||
}
|
||||
metadata.source = 'light-code';
|
||||
metadata['light-code'] = {
|
||||
id: file.id
|
||||
}
|
||||
app.route({
|
||||
id: routerItem.id,
|
||||
path: `${routerItem.id}__${routerItem.path}`,
|
||||
@@ -153,8 +167,13 @@ export const initLightCode = async (opts: opts) => {
|
||||
middleware: ['auth'],
|
||||
}).define(async (ctx) => {
|
||||
const tokenUser = ctx.state?.tokenUser || {};
|
||||
const query = { ...ctx.query, tokenUser }
|
||||
const runRes2 = await runCode(tsPath, query, { timeout: 30000 });
|
||||
const query = { ...ctx.query }
|
||||
const runRes2 = await runCode(tsPath, {
|
||||
message: query,
|
||||
context: {
|
||||
state: { tokenUser, user: tokenUser },
|
||||
}
|
||||
}, { timeout: 30000 });
|
||||
if (runRes2.success) {
|
||||
const res2 = runRes2.data;
|
||||
if (res2.code === 200) {
|
||||
@@ -166,11 +185,9 @@ export const initLightCode = async (opts: opts) => {
|
||||
ctx.throw(runRes2.error || 'Lightcode 路由执行失败');
|
||||
}
|
||||
}).addTo(app, {
|
||||
override: false,
|
||||
// @ts-ignore
|
||||
overwrite: false
|
||||
});// 不允许覆盖已存在的路由
|
||||
|
||||
// console.log(`light-code 路由注册成功: [${routerItem.path}] ${routerItem.id} 来自文件: ${file.filepath}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -178,4 +195,19 @@ export const initLightCode = async (opts: opts) => {
|
||||
}
|
||||
}
|
||||
console.log(`light-code 路由注册成功`, `注册${diffList.length}个路由`);
|
||||
}
|
||||
|
||||
export const clearLightCodeRoutes = (opts: Pick<Opts, 'router'>) => {
|
||||
const app = opts.router;
|
||||
if (!app) {
|
||||
console.error('clearLightCodeRoutes 缺少必要参数, app');
|
||||
return;
|
||||
}
|
||||
const routes = app.getList();
|
||||
for (const route of routes) {
|
||||
if (route.metadata?.source === 'light-code') {
|
||||
// console.log(`删除 light-code 路由: ${route.path} ${route.id}`);
|
||||
app.removeById(route.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { fork } from 'node:child_process'
|
||||
import fs from 'fs';
|
||||
|
||||
import fs from 'node:fs';
|
||||
import { ListenProcessParams, ListenProcessResponse } from '@kevisual/router';
|
||||
export const fileExists = (path: string): boolean => {
|
||||
try {
|
||||
fs.accessSync(path, fs.constants.F_OK);
|
||||
@@ -10,30 +10,12 @@ export const fileExists = (path: string): boolean => {
|
||||
}
|
||||
}
|
||||
|
||||
export type RunCodeParams = {
|
||||
path?: string;
|
||||
key?: string;
|
||||
payload?: string;
|
||||
[key: string]: any
|
||||
}
|
||||
export type RunCodeParams = ListenProcessParams
|
||||
type RunCodeOptions = {
|
||||
timeout?: number; // 超时时间,单位毫秒
|
||||
[key: string]: any
|
||||
}
|
||||
type RunCode = {
|
||||
// 调用进程的功能
|
||||
success?: boolean
|
||||
data?: {
|
||||
// 调用router的结果
|
||||
code?: number
|
||||
data?: any
|
||||
message?: string
|
||||
[key: string]: any
|
||||
};
|
||||
error?: any
|
||||
timestamp?: string
|
||||
output?: string
|
||||
}
|
||||
type RunCode = ListenProcessResponse & { output?: string }
|
||||
export const runCode = async (tsPath: string, params: RunCodeParams = {}, opts?: RunCodeOptions): Promise<RunCode> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (fileExists(tsPath) === false) {
|
||||
@@ -81,7 +63,7 @@ export const runCode = async (tsPath: string, params: RunCodeParams = {}, opts?:
|
||||
silent: true, // 启用 stdio 重定向
|
||||
env: {
|
||||
...process.env,
|
||||
BUN_CHILD_PROCESS: 'true' // 标记为子进程
|
||||
KEVISUAL_CHILD_PROCESS: 'true' // 标记为子进程
|
||||
}
|
||||
})
|
||||
// 监听来自子进程的消息
|
||||
@@ -150,6 +132,7 @@ export const runCode = async (tsPath: string, params: RunCodeParams = {}, opts?:
|
||||
// 向子进程发送消息
|
||||
|
||||
} catch (error) {
|
||||
console.error('启动子进程失败:', error)
|
||||
resolveOnce({
|
||||
success: false,
|
||||
error: `启动子进程失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { WSSManager } from './wss.ts';
|
||||
import { App, Route } from '@kevisual/router'
|
||||
import { WebSocketReq } from '@kevisual/router'
|
||||
import { WebSocketReq, ListenProcessParams } from '@kevisual/router'
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
const letter = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
const customId = customAlphabet(letter, 16);
|
||||
/**
|
||||
* 实时注册的代码模块
|
||||
*
|
||||
* 别人通过 WebSocket 连接到此模块,发送路由列表和请求数据
|
||||
*/
|
||||
export class LiveCode {
|
||||
wssManager: WSSManager;
|
||||
app: App;
|
||||
emitter: EventEmitter;
|
||||
constructor(app: App) {
|
||||
this.wssManager = new WSSManager({ heartbeatInterval: 5000 });
|
||||
this.wssManager = new WSSManager({ heartbeatInterval: 6 * 5000 });
|
||||
this.app = app;
|
||||
this.emitter = new EventEmitter();
|
||||
console.log('[LiveCode] 模块已初始化');
|
||||
@@ -46,9 +50,9 @@ export class LiveCode {
|
||||
return this.wssManager.getConnection(id)
|
||||
}
|
||||
async init(id: string): Promise<{ code: number, message?: string, data?: any }> {
|
||||
return this.sendData({ path: 'router', key: 'list', }, id);
|
||||
return this.sendData({ message: { path: 'router', key: 'list', } }, id);
|
||||
}
|
||||
sendData(data: any, id: string): Promise<{ code: number, message?: string, data?: any }> {
|
||||
sendData(data: ListenProcessParams, id: string): Promise<{ code: number, message?: string, data?: any }> {
|
||||
const reqId = customId()
|
||||
const wss = this.getWss(id);
|
||||
if (!wss) {
|
||||
@@ -102,6 +106,7 @@ export class LiveCode {
|
||||
description: route.description,
|
||||
metadata: {
|
||||
...route.metadata,
|
||||
source: 'livecode',
|
||||
liveCodeId: wid
|
||||
},
|
||||
middleware: ['auth'],
|
||||
@@ -109,9 +114,15 @@ export class LiveCode {
|
||||
const { token, cookie, ...rest } = ctx.query;
|
||||
const tokenUser = ctx.state.tokernUser;
|
||||
const res = await this.sendData({
|
||||
id: route.id,
|
||||
tokenUser,
|
||||
payload: rest,
|
||||
message: {
|
||||
id: route.id,
|
||||
payload: rest,
|
||||
},
|
||||
context: {
|
||||
state: {
|
||||
tokenUser
|
||||
}
|
||||
}
|
||||
}, wid);
|
||||
// console.log('路由响应数据:', res);
|
||||
ctx.forward(res)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { WebSocketReq } from '@kevisual/router'
|
||||
import { logger } from "../logger.ts";
|
||||
type ConnectionInfo = {
|
||||
id: string;
|
||||
wsReq: WebSocketReq;
|
||||
@@ -59,7 +60,7 @@ export class WSSManager {
|
||||
const ws = connection.wsReq.ws;
|
||||
ws.send(JSON.stringify({ type: 'heartbeat', timestamp: new Date().toISOString() }));
|
||||
connection.lastHeartbeat = new Date();
|
||||
console.log(`[LiveCode] 发送心跳给连接 ${connection.id}`);
|
||||
logger.debug(`[LiveCode] 发送心跳给连接 ${connection.id}`);
|
||||
}, this.heartbeatInterval);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user