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:
2026-02-04 13:20:12 +08:00
parent 6212194f95
commit 5d6bd4f429
21 changed files with 363 additions and 256 deletions

View File

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

View File

@@ -48,10 +48,8 @@ export type ProxyInfo = {
},
lightcode?: {
id?: string;
/**
* 是否检测远程服务更新
*/
check?: boolean;
sync?: 'remote' | 'local' | 'both';
rootPath?: string;
}
};

View File

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

View File

@@ -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 : '未知错误'}`

View File

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

View File

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