Compare commits

..

7 Commits

Author SHA1 Message Date
87068cd626 update 2025-10-24 21:15:02 +08:00
ac32ff9d4a udpate 2025-10-24 03:04:06 +08:00
cc74dc6803 update add no path and random path 2025-10-22 17:05:20 +08:00
24166f9899 feat: add Mini 2025-10-15 21:13:37 +08:00
10506503eb update: add listen process 2025-10-15 20:48:20 +08:00
2483205a22 update 2025-10-14 19:17:00 +08:00
cd96b53f6e recorver code 2025-10-14 19:15:18 +08:00
20 changed files with 1041 additions and 31 deletions

20
auto.ts Normal file
View File

@@ -0,0 +1,20 @@
import { loadTS, getMatchFiles } from './src/auto/load-ts.ts';
import { listenSocket } from './src/auto/listen-sock.ts';
import { Route, QueryRouter, QueryRouterServer } from './src/route.ts';
export { Route, QueryRouter, QueryRouterServer };
export const App = QueryRouterServer;
export { createSchema } from './src/validator/index.ts';
export type { Rule } from './src/validator/rule.ts';
export type { Schema } from 'zod';
export type { RouteContext, RouteOpts } from './src/route.ts';
export type { Run } from './src/route.ts';
export { CustomError } from './src/result/error.ts';
export { listenSocket, loadTS, getMatchFiles };
export { autoCall } from './src/auto/call-sock.ts';

28
demo/simple/src/nopath.ts Normal file
View File

@@ -0,0 +1,28 @@
import { Route, App } from '@kevisual/router/src/index.ts';
const app = new App();
app.route({
description: 'sdf'
}).define(async (ctx) => {
ctx.body = 'this is no path fns';
return ctx;
}).addTo(app);
let id = ''
console.log('routes', app.router.routes.map(item => {
id = item.id;
return {
path: item.path,
key: item.key,
id: item.id,
description: item.description
}
}))
app.call({id: id}).then(res => {
console.log('id', id);
console.log('res', res);
})

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package",
"name": "@kevisual/router",
"version": "0.0.27",
"version": "0.0.31",
"description": "",
"type": "module",
"main": "./dist/router.js",
@@ -24,18 +24,18 @@
"@kevisual/local-proxy": "^0.0.6",
"@kevisual/query": "^0.0.29",
"@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-commonjs": "^28.0.6",
"@rollup/plugin-commonjs": "28.0.8",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-typescript": "^12.1.4",
"@rollup/plugin-typescript": "^12.3.0",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.7.2",
"@types/node": "^24.9.1",
"@types/send": "^1.2.0",
"@types/ws": "^8.18.1",
"@types/xml2js": "^0.4.14",
"cookie": "^1.0.2",
"lodash-es": "^4.17.21",
"nanoid": "^5.1.6",
"rollup": "^4.52.4",
"rollup": "^4.52.5",
"rollup-plugin-dts": "^6.2.3",
"ts-loader": "^9.5.4",
"ts-node": "^10.9.2",

View File

@@ -5,7 +5,7 @@ import { CustomError } from './result/error.ts';
import { handleServer } from './server/handle-server.ts';
import { IncomingMessage, ServerResponse } from 'http';
type RouterHandle = (msg: { path: string; [key: string]: any }) => { code: string; data?: any; message?: string; [key: string]: any };
type RouterHandle = (msg: { path: string;[key: string]: any }) => { code: string; data?: any; message?: string;[key: string]: any };
type AppOptions<T = {}> = {
router?: QueryRouter;
server?: Server;
@@ -82,7 +82,20 @@ export class App<T = {}, U = AppReqRes> {
}
return new Route(path, key, opts);
}
async call(message: { path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
prompt(description: string): Route<Required<RouteContext>>;
prompt(description: Function): Route<Required<RouteContext>>;
prompt(...args: any[]) {
const [desc] = args;
let description = ''
if (typeof desc === 'string') {
description = desc;
} else if (typeof desc === 'function') {
description = desc() || ''; // 如果是Promise需要addTo App之前就要获取应有的函数了。
}
return new Route('', '', { description });
}
async call(message: { id?: string, path?: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
const router = this.router;
return await router.call(message, ctx);
}

164
src/auto/call-sock.ts Normal file
View File

@@ -0,0 +1,164 @@
import { createConnection } from 'node:net';
type QueryData = {
path?: string;
key?: string;
payload?: any;
[key: string]: any;
};
type CallSockOptions = {
socketPath?: string;
timeout?: number;
method?: 'GET' | 'POST';
};
export const callSock = async (data: QueryData, options: CallSockOptions = {}): Promise<any> => {
const { socketPath = './app.sock', timeout = 10000, method = 'POST' } = options;
return new Promise((resolve, reject) => {
const client = createConnection(socketPath);
let responseData = '';
let timer: NodeJS.Timeout;
// 设置超时
if (timeout > 0) {
timer = setTimeout(() => {
client.destroy();
reject(new Error(`Socket call timeout after ${timeout}ms`));
}, timeout);
}
client.on('connect', () => {
try {
let request: string;
if (method === 'GET') {
// GET 请求:参数放在 URL 中
const searchParams = new URLSearchParams();
Object.entries(data).forEach(([key, value]) => {
if (key === 'payload' && typeof value === 'object') {
searchParams.append(key, JSON.stringify(value));
} else {
searchParams.append(key, String(value));
}
});
const queryString = searchParams.toString();
const url = queryString ? `/?${queryString}` : '/';
request = [`GET ${url} HTTP/1.1`, 'Host: localhost', 'Connection: close', '', ''].join('\r\n');
} else {
// POST 请求:数据放在 body 中
const body = JSON.stringify(data);
const contentLength = Buffer.byteLength(body, 'utf8');
request = [
'POST / HTTP/1.1',
'Host: localhost',
'Content-Type: application/json',
`Content-Length: ${contentLength}`,
'Connection: close',
'',
body,
].join('\r\n');
}
client.write(request);
} catch (error) {
if (timer) clearTimeout(timer);
client.destroy();
reject(error);
}
});
client.on('data', (chunk) => {
responseData += chunk.toString();
// 检查是否收到完整的HTTP响应
if (responseData.includes('\r\n\r\n')) {
const [headerSection] = responseData.split('\r\n\r\n');
const contentLengthMatch = headerSection.match(/content-length:\s*(\d+)/i);
if (contentLengthMatch) {
const expectedLength = parseInt(contentLengthMatch[1]);
const bodyStart = responseData.indexOf('\r\n\r\n') + 4;
const currentBodyLength = Buffer.byteLength(responseData.slice(bodyStart), 'utf8');
// 如果收到了完整的响应,主动关闭连接
if (currentBodyLength >= expectedLength) {
client.end();
}
} else if (responseData.includes('\r\n0\r\n\r\n')) {
// 检查 chunked 编码结束标记
client.end();
}
}
});
client.on('end', () => {
if (timer) clearTimeout(timer);
try {
// 解析 HTTP 响应
const response = parseHttpResponse(responseData);
if (response.statusCode >= 400) {
reject(new Error(`HTTP ${response.statusCode}: ${response.body}`));
return;
}
// 尝试解析 JSON 响应
try {
const result = JSON.parse(response.body);
resolve(result);
} catch {
// 如果不是 JSON直接返回文本
resolve(response.body);
}
} catch (error) {
reject(error);
}
});
client.on('error', (error) => {
if (timer) clearTimeout(timer);
reject(error);
});
client.on('timeout', () => {
if (timer) clearTimeout(timer);
client.destroy();
reject(new Error('Socket connection timeout'));
});
});
};
// 解析 HTTP 响应的辅助函数
function parseHttpResponse(responseData: string) {
const [headerSection, ...bodyParts] = responseData.split('\r\n\r\n');
const body = bodyParts.join('\r\n\r\n');
const lines = headerSection.split('\r\n');
const statusLine = lines[0];
const statusMatch = statusLine.match(/HTTP\/\d\.\d (\d+)/);
const statusCode = statusMatch ? parseInt(statusMatch[1]) : 200;
const headers: Record<string, string> = {};
for (let i = 1; i < lines.length; i++) {
const [key, ...valueParts] = lines[i].split(':');
if (key && valueParts.length > 0) {
headers[key.trim().toLowerCase()] = valueParts.join(':').trim();
}
}
return {
statusCode,
headers,
body: body || '',
};
}
export const autoCall = (data: QueryData, options?: Omit<CallSockOptions, 'method'>) => {
return callSock(data, { ...options, method: 'POST' });
};

274
src/auto/listen-sock.ts Normal file
View File

@@ -0,0 +1,274 @@
import type { IncomingMessage } from 'http';
import { QueryRouterServer } from '../route.ts';
import { getRuntime } from './runtime.ts';
import { runFirstCheck } from './listen/run-check.ts';
import { cleanup } from './listen/cleanup.ts';
import { ServerTimer } from './listen/server-time.ts';
type ListenSocketOptions = {
/**
* Unix socket path, defaults to './app.sock'
*/
path?: string;
app?: QueryRouterServer;
/**
* Unix socket path, defaults to './app.pid'
*/
pidPath?: string;
/**
* Timeout for the server, defaults to 15 minutes.
* If the server is not responsive for this duration, it will be terminated
*/
timeout?: number;
};
const server = async (req, app: QueryRouterServer) => {
const runtime = getRuntime();
let data;
if (!runtime.isNode) {
data = await getRequestParams(req);
} else {
data = await parseBody(req);
}
// @ts-ignore
const serverTimer = app.serverTimer;
if (serverTimer) {
serverTimer?.run?.();
}
const result = await app.queryRoute(data as any);
const response = new Response(JSON.stringify(result));
response.headers.set('Content-Type', 'application/json');
return response;
};
export const closeListenSocket = () => {
console.log('Closing listen socket');
process.emit('SIGINT');
};
export const serverTimer = new ServerTimer();
export const listenSocket = async (options?: ListenSocketOptions) => {
const path = options?.path || './app.sock';
const pidPath = options?.pidPath || './app.pid';
const timeout = options?.timeout || 24 * 60 * 60 * 1000; // 24 hours
const runtime = getRuntime();
serverTimer.timeout = timeout;
serverTimer.startTimer();
serverTimer.onTimeout = closeListenSocket;
let app = options?.app || globalThis.context?.app;
if (!app) {
app = new QueryRouterServer();
}
app.serverTimer = serverTimer;
await runFirstCheck(path, pidPath);
let close = async () => {};
cleanup({ path, close });
if (runtime.isDeno) {
// 检查 Deno 版本是否支持 Unix domain socket
try {
// @ts-ignore
const listener = Deno.listen({
transport: 'unix',
path: path,
});
// 处理连接
(async () => {
for await (const conn of listener) {
(async () => {
// @ts-ignore
const httpConn = Deno.serveHttp(conn);
for await (const requestEvent of httpConn) {
try {
const response = await server(requestEvent.request, app);
await requestEvent.respondWith(response);
} catch (error) {
await requestEvent.respondWith(new Response('Internal Server Error', { status: 500 }));
}
}
})();
}
})();
close = async () => {
listener.close();
};
return listener;
} catch (error) {
// 如果 Unix socket 不支持,回退到 HTTP 服务器
console.warn('Unix socket not supported in this Deno environment, falling back to HTTP server');
// @ts-ignore
const listener = Deno.listen({ port: 0 }); // 使用随机端口
// @ts-ignore
console.log(`Deno server listening on port ${listener.addr.port}`);
(async () => {
for await (const conn of listener) {
(async () => {
// @ts-ignore
const httpConn = Deno.serveHttp(conn);
for await (const requestEvent of httpConn) {
try {
const response = await server(requestEvent.request, app);
await requestEvent.respondWith(response);
} catch (error) {
await requestEvent.respondWith(new Response('Internal Server Error', { status: 500 }));
}
}
})();
}
})();
return listener;
}
}
if (runtime.isBun) {
// @ts-ignore
const bunServer = Bun.serve({
unix: path,
fetch(req) {
return server(req, app);
},
});
close = async () => {
await bunServer.stop();
};
return bunServer;
}
// Node.js 环境
const http = await import('http');
const httpServer = http.createServer(async (req, res) => {
try {
const response = await server(req, app);
// 设置响应头
response.headers.forEach((value, key) => {
res.setHeader(key, value);
});
// 设置状态码
res.statusCode = response.status;
// 读取响应体并写入
const body = await response.text();
res.end(body);
} catch (error) {
console.error('Error handling request:', error);
res.statusCode = 500;
res.end('Internal Server Error');
}
});
httpServer.listen(path);
close = async () => {
httpServer.close();
};
return httpServer;
};
export const getRequestParams = async (req: Request) => {
let urlParams: Record<string, any> = {};
let bodyParams: Record<string, any> = {};
// 获取URL参数
const url = new URL(req.url);
for (const [key, value] of url.searchParams.entries()) {
// 尝试解析JSON payload
if (key === 'payload') {
try {
urlParams[key] = JSON.parse(value);
} catch {
urlParams[key] = value;
}
} else {
urlParams[key] = value;
}
}
// 获取body参数
if (req.method.toLowerCase() === 'post' && req.body) {
const contentType = req.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
try {
bodyParams = await req.json();
} catch {
// 如果解析失败,保持空对象
}
} else if (contentType.includes('application/x-www-form-urlencoded')) {
const formData = await req.text();
const params = new URLSearchParams(formData);
for (const [key, value] of params.entries()) {
bodyParams[key] = value;
}
} else if (contentType.includes('multipart/form-data')) {
try {
const formData = await req.formData();
for (const [key, value] of formData.entries()) {
// @ts-ignore
bodyParams[key] = value instanceof File ? value : value.toString();
}
} catch {
// 如果解析失败,保持空对象
}
}
}
// body参数优先合并数据
return {
...urlParams,
...bodyParams,
};
};
export const parseBody = async <T = Record<string, any>>(req: IncomingMessage) => {
return new Promise<T>((resolve, reject) => {
const arr: any[] = [];
req.on('data', (chunk) => {
arr.push(chunk);
});
req.on('end', () => {
try {
const body = Buffer.concat(arr).toString();
// 获取 Content-Type 头信息
const contentType = req.headers['content-type'] || '';
// 处理 application/json
if (contentType.includes('application/json')) {
resolve(JSON.parse(body) as T);
return;
}
// 处理 application/x-www-form-urlencoded
if (contentType.includes('application/x-www-form-urlencoded')) {
const formData = new URLSearchParams(body);
const result: Record<string, any> = {};
formData.forEach((value, key) => {
// 尝试将值解析为 JSON如果失败则保留原始字符串
try {
result[key] = JSON.parse(value);
} catch {
result[key] = value;
}
});
resolve(result as T);
return;
}
// 默认尝试 JSON 解析
try {
resolve(JSON.parse(body) as T);
} catch {
resolve({} as T);
}
} catch (e) {
resolve({} as T);
}
});
});
};

102
src/auto/listen/cleanup.ts Normal file
View File

@@ -0,0 +1,102 @@
import { getRuntime } from '../runtime.ts';
let isClean = false;
export const deleteFileDetached = async (path: string, pidPath: string = './app.pid') => {
const runtime = getRuntime();
if (runtime.isDeno) {
// Deno 实现 - 启动后不等待结果
const process = new Deno.Command('sh', {
args: ['-c', `rm -f "${path}" & rm -f "${pidPath}"`],
stdout: 'null',
stderr: 'null',
});
process.spawn(); // 不等待结果
console.log(`[DEBUG] Fire-and-forget delete initiated for ${path}`);
return;
}
const { spawn } = await import('node:child_process');
const child = spawn('sh', ['-c', `rm -f "${path}" & rm -f "${pidPath}"`], {
detached: true,
stdio: 'ignore',
});
child.unref(); // 完全分离
console.log(`[DEBUG] Fire-and-forget delete initiated for ${path}`);
};
type CleanupOptions = {
path: string;
close?: () => Promise<void>;
pidPath?: string;
};
export const cleanup = async ({ path, close = async () => {}, pidPath = './app.pid' }: CleanupOptions) => {
const runtime = getRuntime();
// 检查文件是否存在并删除
const cleanupFile = async () => {
if (isClean) return;
isClean = true;
if (runtime.isDeno) {
await deleteFileDetached(path, pidPath);
}
await close();
if (!runtime.isDeno) {
await deleteFileDetached(path, pidPath);
}
};
// 根据运行时环境注册不同的退出监听器
if (runtime.isDeno) {
// Deno 环境
const handleSignal = () => {
cleanupFile();
Deno.exit(0);
};
try {
Deno.addSignalListener('SIGINT', handleSignal);
Deno.addSignalListener('SIGTERM', handleSignal);
} catch (error) {
console.warn('[DEBUG] Failed to add signal listeners:', error);
}
// 对于 beforeunload 和 unload使用异步清理
const handleUnload = () => {
cleanupFile();
};
globalThis.addEventListener('beforeunload', handleUnload);
globalThis.addEventListener('unload', handleUnload);
} else if (runtime.isNode || runtime.isBun) {
// Node.js 和 Bun 环境
import('process').then(({ default: process }) => {
// 信号处理使用同步清理,然后退出
const signalHandler = async (signal: string) => {
await cleanupFile();
process.exit(0);
};
process.on('SIGINT', () => signalHandler('SIGINT'));
process.on('SIGTERM', () => signalHandler('SIGTERM'));
process.on('SIGUSR1', () => signalHandler('SIGUSR1'));
process.on('SIGUSR2', () => signalHandler('SIGUSR2'));
process.on('exit', async () => {
await cleanupFile();
});
process.on('uncaughtException', async (error) => {
console.error('Uncaught Exception:', error);
await cleanupFile();
process.exit(1);
});
process.on('unhandledRejection', async (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
await cleanupFile();
});
});
}
// 返回手动清理函数,以便需要时主动调用
return cleanupFile;
};

View File

@@ -0,0 +1,51 @@
import { getRuntime } from '../runtime.ts';
export const getPid = async () => {
const runtime = getRuntime();
let pid = 0;
if (runtime.isDeno) {
// @ts-ignore
pid = Deno.pid;
} else {
pid = process.pid;
}
return pid;
};
export const writeAppid = async (pidPath = './app.pid') => {
const fs = await import('node:fs');
const pid = await getPid();
fs.writeFileSync(pidPath, pid + '');
};
export const getPidFromFileAndStop = async () => {
const fs = await import('node:fs');
if (fs.existsSync('./app.pid')) {
const pid = parseInt(fs.readFileSync('./app.pid', 'utf-8'), 10);
if (!isNaN(pid)) {
if (pid === 0) {
return;
}
try {
process.kill(pid);
console.log(`Stopped process with PID ${pid}`);
} catch (error) {
console.error(`Failed to stop process with PID ${pid}:`);
}
}
}
};
export const runFirstCheck = async (path: string, pidPath: string) => {
await getPidFromFileAndStop();
await writeAppid(pidPath);
try {
const fs = await import('node:fs');
if (fs.existsSync(path)) {
fs.unlinkSync(path);
console.log(`Socket file ${path} cleaned up during first check`);
}
} catch (error) {
console.error(`Failed to clean up socket file ${path} during first check:`, error);
}
};

View File

@@ -0,0 +1,33 @@
export class ServerTimer {
updatedAt: number;
timer: any;
timeout: number;
onTimeout: any;
interval = 10 * 1000;
constructor(opts?: { timeout?: number }) {
this.timeout = opts?.timeout || 15 * 60 * 1000;
this.run();
}
startTimer() {
const that = this;
if (this.timer) {
clearInterval(this.timer);
}
this.timer = setInterval(() => {
const updatedAt = Date.now();
const timeout = that.timeout;
const onTimeout = that.onTimeout;
const isExpired = updatedAt - that.updatedAt > timeout;
if (isExpired) {
onTimeout?.();
clearInterval(that.timer);
that.timer = null;
}
}, that.interval);
}
run(): number {
this.updatedAt = Date.now();
return this.updatedAt;
}
}

38
src/auto/load-ts.ts Normal file
View File

@@ -0,0 +1,38 @@
import { getRuntime } from './runtime.ts';
import { glob } from './utils/glob.ts';
type GlobOptions = {
cwd?: string;
load?: (args?: any) => Promise<any>;
};
export const getMatchFiles = async (match: string = './*.ts', { cwd = process.cwd() }: GlobOptions = {}): Promise<string[]> => {
const runtime = getRuntime();
if (runtime.isNode) {
console.error(`Node.js is not supported`);
return [];
}
if (runtime.isDeno) {
// Deno 环境下
return await glob(match);
}
if (runtime.isBun) {
// Bun 环境下
// @ts-ignore
const { Glob } = await import('bun');
const path = await import('path');
// @ts-ignore
const glob = new Glob(match, { cwd, absolute: true, onlyFiles: true });
const files: string[] = [];
for await (const file of glob.scan('.')) {
files.push(path.join(cwd, file));
}
// @ts-ignore
return Array.from(files);
}
return [];
};
export const loadTS = async (match: string = './*.ts', { cwd = process.cwd(), load }: GlobOptions = {}): Promise<any[]> => {
const files = await getMatchFiles(match, { cwd });
return Promise.all(files.map((file) => (load ? load(file) : import(file))));
};

19
src/auto/runtime.ts Normal file
View File

@@ -0,0 +1,19 @@
type RuntimeEngine = 'node' | 'deno' | 'bun';
type Runtime = {
isNode?: boolean;
isDeno?: boolean;
isBun?: boolean;
engine: RuntimeEngine;
};
export const getRuntime = (): Runtime => {
// @ts-ignore
if (typeof Deno !== 'undefined') {
return { isDeno: true, engine: 'deno' };
}
// @ts-ignore
if (typeof Bun !== 'undefined') {
return { isBun: true, engine: 'bun' };
}
return { isNode: true, engine: 'node' };
};

83
src/auto/utils/glob.ts Normal file
View File

@@ -0,0 +1,83 @@
type GlobOptions = {
cwd?: string;
};
export const glob = async (match: string = './*.ts', { cwd = process.cwd() }: GlobOptions = {}) => {
const fs = await import('node:fs');
const path = await import('node:path');
// 将 glob 模式转换为正则表达式
const globToRegex = (pattern: string): RegExp => {
const escaped = pattern
.replace(/\./g, '\\.')
.replace(/\*\*/g, '__DOUBLE_STAR__') // 临时替换 **
.replace(/\*/g, '[^/]*') // * 匹配除 / 外的任意字符
.replace(/__DOUBLE_STAR__/g, '.*') // ** 匹配任意字符包括 /
.replace(/\?/g, '[^/]'); // ? 匹配除 / 外的单个字符
return new RegExp(`^${escaped}$`);
};
// 递归读取目录
const readDirRecursive = async (dir: string): Promise<string[]> => {
const files: string[] = [];
try {
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isFile()) {
files.push(fullPath);
} else if (entry.isDirectory()) {
// 递归搜索子目录
const subFiles = await readDirRecursive(fullPath);
files.push(...subFiles);
}
}
} catch (error) {
// 忽略无法访问的目录
}
return files;
};
// 解析模式是否包含递归搜索
const hasRecursive = match.includes('**');
try {
let allFiles: string[] = [];
if (hasRecursive) {
// 处理递归模式
const basePath = match.split('**')[0];
const startDir = path.resolve(cwd, basePath || '.');
allFiles = await readDirRecursive(startDir);
} else {
// 处理非递归模式
const dir = path.resolve(cwd, path.dirname(match));
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile()) {
allFiles.push(path.join(dir, entry.name));
}
}
}
// 创建相对于 cwd 的匹配模式
const normalizedMatch = path.resolve(cwd, match);
const regex = globToRegex(normalizedMatch);
// 过滤匹配的文件
const matchedFiles = allFiles.filter(file => {
const normalizedFile = path.resolve(file);
return regex.test(normalizedFile);
});
return matchedFiles;
} catch (error) {
console.error(`Error in glob pattern "${match}":`, error);
return [];
}
};

View File

@@ -1,4 +1,4 @@
export { Route, QueryRouter, QueryRouterServer } from './route.ts';
export { Route, QueryRouter, QueryRouterServer, Mini } from './route.ts';
export type { Rule, Schema } from './validator/index.ts';

40
src/chat.ts Normal file
View File

@@ -0,0 +1,40 @@
import { QueryRouter } from "./route.ts";
type RouterChatOptions = {
router?: QueryRouter;
}
export class RouterChat {
router: QueryRouter;
prompt: string = '';
constructor(opts?: RouterChatOptions) {
this.router = opts?.router || new QueryRouter();
}
prefix(wrapperFn?: (routes: any[]) => string) {
if (this.prompt) {
return this.prompt;
}
let _prompt = `你是一个调用函数工具的助手,当用户询问时,如果拥有工具,请返回 JSON 数据,数据的值的内容是 id 和 payload 。如果有参数,请放到 payload 当中。
下面是你可以使用的工具列表:
`;
if (!wrapperFn) {
_prompt += this.router.routes.map(r => `工具名称: ${r.id}\n描述: ${r.description}\n`).join('\n');
} else {
_prompt += wrapperFn(this.router.exportRoutes());
}
_prompt += `当你需要使用工具时,请严格按照以下格式返回:
{
"id": "工具名称",
"payload": {
// 参数列表
}
}
如果你不需要使用工具,直接返回用户想要的内容即可,不要返回任何多余的信息。`;
return _prompt;
}
chat() {
const prompt = this.prefix();
return prompt;
}
}

View File

@@ -1,4 +1,4 @@
export { Route, QueryRouter, QueryRouterServer } from './route.ts';
export { Route, QueryRouter, QueryRouterServer, Mini } from './route.ts';
export { Connect, QueryConnect } from './connect.ts';
export type { RouteContext, RouteOpts, RouteMiddleware } from './route.ts';
@@ -11,7 +11,9 @@ export { Server, handleServer } from './server/index.ts';
*/
export { CustomError } from './result/error.ts';
export { Rule, Schema, createSchema } from './validator/index.ts';
export { createSchema } from './validator/index.ts';
export type { Rule, Schema, } from './validator/index.ts';
export { App } from './app.ts';

View File

@@ -1,10 +1,11 @@
import { nanoid } from 'nanoid';
import { nanoid, random } from 'nanoid';
import { CustomError } from './result/error.ts';
import { Schema, Rule, createSchema } from './validator/index.ts';
import { pick } from './utils/pick.ts';
import { get } from 'lodash-es';
import { listenProcess } from './utils/listen-process.ts';
export type RouterContextT = { code?: number; [key: string]: any };
export type RouterContextT = { code?: number;[key: string]: any };
export type RouteContext<T = { code?: number }, S = any> = {
// run first
query?: { [key: string]: any };
@@ -47,7 +48,7 @@ export type RouteContext<T = { code?: number }, S = any> = {
error?: any;
/** 请求 route的返回结果包函ctx */
call?: (
message: { path: string; key?: string; payload?: any; [key: string]: any } | { id: string; apyload?: any; [key: string]: any },
message: { path: string; key?: string; payload?: any;[key: string]: any } | { id: string; apyload?: any;[key: string]: any },
ctx?: RouteContext & { [key: string]: any },
) => Promise<any>;
/** 请求 route的返回结果不包函ctx */
@@ -87,7 +88,7 @@ export type RouteOpts = {
* }
*/
validator?: { [key: string]: Rule };
schema?: { [key: string]: Schema<any> };
schema?: { [key: string]: any };
isVerify?: boolean;
verify?: (ctx?: RouteContext, dev?: boolean) => boolean;
verifyKey?: (key: string, ctx?: RouteContext, dev?: boolean) => boolean;
@@ -102,7 +103,7 @@ export type RouteOpts = {
isDebug?: boolean;
};
export type DefineRouteOpts = Omit<RouteOpts, 'idUsePath' | 'verify' | 'verifyKey' | 'nextRoute'>;
const pickValue = ['path', 'key', 'id', 'description', 'type', 'validator', 'middleware'] as const;
const pickValue = ['path', 'key', 'id', 'description', 'type', 'validator', 'middleware', 'metadata'] as const;
export type RouteInfo = Pick<Route, (typeof pickValue)[number]>;
export class Route<U = { [key: string]: any }> {
/**
@@ -122,7 +123,7 @@ export class Route<U = { [key: string]: any }> {
middleware?: RouteMiddleware[]; // middleware
type? = 'route';
private _validator?: { [key: string]: Rule };
schema?: { [key: string]: Schema<any> };
schema?: { [key: string]: any };
data?: any;
/**
* 是否需要验证
@@ -132,7 +133,10 @@ export class Route<U = { [key: string]: any }> {
* 是否开启debug开启后会打印错误信息
*/
isDebug?: boolean;
constructor(path: string, key: string = '', opts?: RouteOpts) {
constructor(path: string = '', key: string = '', opts?: RouteOpts) {
if (!path) {
path = nanoid(8)
}
path = path.trim();
key = key.trim();
this.path = path;
@@ -260,6 +264,17 @@ export class Route<U = { [key: string]: any }> {
this.validator = validator;
return this;
}
prompt(description: string): this;
prompt(description: Function): this;
prompt(...args: any[]) {
const [description] = args;
if (typeof description === 'string') {
this.description = description;
} else if (typeof description === 'function') {
this.description = description() || ''; // 如果是Promise需要addTo App之前就要获取应有的函数了。
}
return this;
}
define<T extends { [key: string]: any } = RouterContextT>(opts: DefineRouteOpts): this;
define<T extends { [key: string]: any } = RouterContextT>(fn: Run<T & U>): this;
define<T extends { [key: string]: any } = RouterContextT>(key: string, fn: Run<T & U>): this;
@@ -303,7 +318,28 @@ export class Route<U = { [key: string]: any }> {
}
return this;
}
addTo(router: QueryRouter | { add: (route: Route) => void; [key: string]: any }) {
update(opts: DefineRouteOpts, checkList?: string[]): this {
const keys = Object.keys(opts);
const defaultCheckList = ['path', 'key', 'run', 'nextRoute', 'description', 'metadata', 'middleware', 'type', 'validator', 'isVerify', 'isDebug'];
checkList = checkList || defaultCheckList;
for (let item of keys) {
if (!checkList.includes(item)) {
continue;
}
if (item === 'validator') {
this.validator = opts[item];
continue;
}
if (item === 'middleware') {
this.middleware = this.middleware.concat(opts[item]);
continue;
}
this[item] = opts[item];
}
return this;
}
addTo(router: QueryRouter | { add: (route: Route) => void;[key: string]: any }) {
router.add(this);
}
setData(data: any) {
@@ -582,8 +618,9 @@ export class QueryRouter {
} else {
return { code: 404, body: null, message: 'Not found route' };
}
return await this.parse({ ...message, path, key }, { ...this.context, ...ctx });
} else if (path) {
return await this.parse({ ...message, path }, { ...this.context, ...ctx });
return await this.parse({ ...message, path, key }, { ...this.context, ...ctx });
} else {
return { code: 404, body: null, message: 'Not found path' };
}
@@ -595,7 +632,7 @@ export class QueryRouter {
* @param ctx
* @returns
*/
async queryRoute(message: { path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
async queryRoute(message: { id?: string; path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
const res = await this.parse(message, { ...this.context, ...ctx });
return {
code: res.code,
@@ -620,9 +657,19 @@ export class QueryRouter {
* 获取handle函数, 这里会去执行parse函数
*/
getHandle<T = any>(router: QueryRouter, wrapperFn?: HandleFn<T>, ctx?: RouteContext) {
return async (msg: { path: string; key?: string; [key: string]: any }, handleContext?: RouteContext) => {
return async (msg: { id?: string; path?: string; key?: string;[key: string]: any }, handleContext?: RouteContext) => {
try {
const context = { ...ctx, ...handleContext };
if (msg.id) {
const route = router.routes.find((r) => r.id === msg.id);
if (route) {
msg.path = route.path;
msg.key = route.key;
} else {
return { code: 404, message: 'Not found route' };
}
}
// @ts-ignore
const res = await router.parse(msg, context);
if (wrapperFn) {
res.data = res.body;
@@ -655,6 +702,17 @@ export class QueryRouter {
hasRoute(path: string, key: string = '') {
return this.routes.find((r) => r.path === path && r.key === key);
}
/**
* 等待程序运行, 获取到message的数据,就执行
*
* emitter = process
* -- .exit
* -- .on
* -- .send
*/
wait(params?: { path?: string; key?: string; payload?: any }, opts?: { emitter?: any, timeout?: number }) {
return listenProcess({ app: this, params, ...opts });
}
}
type QueryRouterServerOpts = {
@@ -662,7 +720,7 @@ type QueryRouterServerOpts = {
context?: RouteContext;
};
interface HandleFn<T = any> {
(msg: { path: string; [key: string]: any }, ctx?: any): { code: string; data?: any; message?: string; [key: string]: any };
(msg: { path: string;[key: string]: any }, ctx?: any): { code: string; data?: any; message?: string;[key: string]: any };
(res: RouteContext<T>): any;
}
/**
@@ -709,6 +767,18 @@ export class QueryRouterServer extends QueryRouter {
}
return new Route(path, key, opts);
}
prompt(description: string): Route<Required<RouteContext>>;
prompt(description: Function): Route<Required<RouteContext>>;
prompt(...args: any[]) {
const [desc] = args;
let description = ''
if (typeof desc === 'string') {
description = desc;
} else if (typeof desc === 'function') {
description = desc() || ''; // 如果是Promise需要addTo App之前就要获取应有的函数了。
}
return new Route('', '', { description });
}
/**
* 等于queryRoute但是调用了handle
@@ -739,3 +809,6 @@ export class QueryRouterServer extends QueryRouter {
}
}
}
export const Mini = QueryRouterServer

17
src/test/chat.ts Normal file
View File

@@ -0,0 +1,17 @@
import { App } from '../app.ts'
import { RouterChat } from '@/chat.ts';
const app = new App();
app.prompt(`获取时间的工具`).define(async (ctx) => {
ctx.body = '123'
}).addTo(app);
app.prompt('获取天气的工具。\n参数是 city 为对应的城市').define(async (ctx) => {
ctx.body = '晴天'
}).addTo(app);
export const chat = new RouterChat({ router: app.router });
console.log(chat.chat());

View File

@@ -0,0 +1,50 @@
export type ListenProcessOptions = {
app?: any; // 传入的应用实例
emitter?: any; // 可选的事件发射器
params?: any; // 可选的参数
timeout?: number; // 可选的超时时间 (单位: 毫秒)
};
export const listenProcess = async ({ app, emitter, params, timeout = 10 * 60 * 60 * 1000 }: ListenProcessOptions) => {
const process = emitter || globalThis.process;
let isEnd = false;
const timer = setTimeout(() => {
if (isEnd) return;
isEnd = true;
process.send?.({ success: false, error: 'Timeout' });
process.exit?.(1);
}, timeout);
// 监听来自主进程的消息
const getParams = async (): Promise<any> => {
return new Promise((resolve) => {
process.on('message', (msg) => {
if (isEnd) return;
isEnd = true;
clearTimeout(timer);
resolve(msg)
})
})
}
try {
const { path = 'main', ...rest } = await getParams()
// 执行主要逻辑
const result = await app.queryRoute({ path, ...rest, ...params })
// 发送结果回主进程
const response = {
success: true,
data: result,
timestamp: new Date().toISOString()
}
process.send?.(response, (error) => {
process.exit?.(0)
})
} catch (error) {
process.send?.({
success: false,
error: error.message
})
process.exit?.(1)
}
}

View File

@@ -1,2 +1,6 @@
export type { Rule, Schema } from './rule.ts';
import { z } from 'zod';
export { schemaFormRule, createSchema, createSchemaList } from './rule.ts';
export type { Rule } from './rule.ts';
export type Schema = z.ZodType<any, any, any>;

View File

@@ -1,5 +1,4 @@
import { z, ZodError, Schema } from 'zod';
export { Schema };
import { z, ZodError } from 'zod';
type BaseRule = {
value?: any;
required?: boolean;
@@ -64,7 +63,7 @@ export const schemaFormRule = (rule: Rule): z.ZodType<any, any, any> => {
throw new Error(`Unknown rule type: ${(rule as any)?.type}`);
}
};
export const createSchema = (rule: Rule): Schema => {
export const createSchema = (rule: Rule): z.ZodType<any, any, any> => {
try {
rule.required = rule.required ?? false;
if (!rule.required) {