Compare commits

..

6 Commits

7 changed files with 195 additions and 75 deletions

2
.npmrc
View File

@@ -1,2 +1,2 @@
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN} /npm.cnb.cool/kevisual/registry/-/packages/:_authToken=${CNB_API_KEY}
//registry.npmjs.org/:_authToken=${NPM_TOKEN} //registry.npmjs.org/:_authToken=${NPM_TOKEN}

View File

@@ -55,7 +55,7 @@ app
| 方法 | 参数 | 说明 | | 方法 | 参数 | 说明 |
| ----------------------------------- | ----------------------------------------- | -------------------------------------------- | | ----------------------------------- | ----------------------------------------- | -------------------------------------------- |
| `ctx.call(msg, ctx?)` | `{ path, key?, payload?, ... } \| { id }` | 调用其他路由,返回完整 context | | `ctx.call(msg, ctx?)` | `{ path, key?, payload?, ... } \| { rid }` | 调用其他路由,返回完整 context |
| `ctx.run(msg, ctx?)` | `{ path, key?, payload? }` | 调用其他路由,返回 `{ code, data, message }` | | `ctx.run(msg, ctx?)` | `{ path, key?, payload? }` | 调用其他路由,返回 `{ code, data, message }` |
| `ctx.forward(res)` | `{ code, data?, message? }` | 设置响应结果 | | `ctx.forward(res)` | `{ code, data?, message? }` | 设置响应结果 |
| `ctx.throw(code?, message?, tips?)` | - | 抛出自定义错误 | | `ctx.throw(code?, message?, tips?)` | - | 抛出自定义错误 |
@@ -64,13 +64,13 @@ app
```ts ```ts
import { App } from '@kevisual/router'; import { App } from '@kevisual/router';
import z from 'zod'; import { z } from 'zod';
const app = new App(); const app = new App();
app.listen(4002); app.listen(4002);
// 基本路由 // 基本路由
app app
.route({ path: 'user', key: 'info', id: 'user-info' }) .route({ path: 'user', key: 'info', rid: 'user-info' })
.define(async (ctx) => { .define(async (ctx) => {
// ctx.query 包含请求参数 // ctx.query 包含请求参数
const { id } = ctx.query; const { id } = ctx.query;
@@ -129,7 +129,7 @@ const app = new App();
// 定义中间件 // 定义中间件
app app
.route({ .route({
id: 'auth-example', rid: 'auth-example',
description: '权限校验中间件', description: '权限校验中间件',
}) })
.define(async (ctx) => { .define(async (ctx) => {
@@ -142,7 +142,7 @@ app
}) })
.addTo(app); .addTo(app);
// 使用中间件(通过 id 引用) // 使用中间件(通过 rid 引用)
app app
.route({ path: 'admin', key: 'panel', middleware: ['auth-example'] }) .route({ path: 'admin', key: 'panel', middleware: ['auth-example'] })
.define(async (ctx) => { .define(async (ctx) => {
@@ -164,17 +164,17 @@ app
path: 'dog', path: 'dog',
key: 'info', key: 'info',
description: '获取狗的信息', description: '获取狗的信息',
metedata: { metadata: {
args: { args: {
owner: z.string().describe('狗主人姓名'), name: z.string().describe('狗姓名'),
age: z.number().describe('狗的年龄'), age: z.number().describe('狗的年龄'),
}, },
}, },
}) })
.define(async (ctx) => { .define(async (ctx) => {
const { owner, age } = ctx.query; const { name, age } = ctx.query;
ctx.body = { ctx.body = {
content: `这是一只${age}岁的狗,主人是${owner}`, content: `这是一只${age}岁的狗,名字是${name}`,
}; };
}) })
.addTo(app); .addTo(app);

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package", "$schema": "https://json.schemastore.org/package",
"name": "@kevisual/router", "name": "@kevisual/router",
"version": "0.1.2", "version": "0.2.2",
"description": "", "description": "",
"type": "module", "type": "module",
"main": "./dist/router.js", "main": "./dist/router.js",
@@ -27,11 +27,11 @@
"@kevisual/dts": "^0.0.4", "@kevisual/dts": "^0.0.4",
"@kevisual/js-filter": "^0.0.6", "@kevisual/js-filter": "^0.0.6",
"@kevisual/local-proxy": "^0.0.8", "@kevisual/local-proxy": "^0.0.8",
"@kevisual/query": "^0.0.53", "@kevisual/query": "^0.0.55",
"@kevisual/remote-app": "^0.0.7", "@kevisual/remote-app": "^0.0.7",
"@kevisual/use-config": "^1.0.30", "@kevisual/use-config": "^1.0.30",
"@opencode-ai/plugin": "^1.2.26", "@opencode-ai/plugin": "^1.3.0",
"@types/bun": "^1.3.10", "@types/bun": "^1.3.11",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/node": "^25.5.0", "@types/node": "^25.5.0",
"@types/send": "^1.2.1", "@types/send": "^1.2.1",
@@ -43,7 +43,7 @@
"eventemitter3": "^5.0.4", "eventemitter3": "^5.0.4",
"fast-glob": "^3.3.3", "fast-glob": "^3.3.3",
"hono": "^4.12.8", "hono": "^4.12.8",
"nanoid": "^5.1.6", "nanoid": "^5.1.7",
"path-to-regexp": "^8.3.0", "path-to-regexp": "^8.3.0",
"send": "^1.2.1", "send": "^1.2.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",

View File

@@ -1,6 +1,7 @@
import { Command, program } from 'commander'; import { Command, program } from 'commander';
import { App, QueryRouterServer } from './app.ts'; import { App } from './app.ts';
import { RemoteApp } from '@kevisual/remote-app' import { RemoteApp } from '@kevisual/remote-app'
import z from 'zod';
export const groupByPath = (routes: App['routes']) => { export const groupByPath = (routes: App['routes']) => {
return routes.reduce((acc, route) => { return routes.reduce((acc, route) => {
const path = route.path || 'default'; const path = route.path || 'default';
@@ -124,9 +125,10 @@ export const parse = async (opts: {
token?: string, token?: string,
username?: string, username?: string,
id?: string, id?: string,
} },
exitOnEnd?: boolean,
}) => { }) => {
const { description, parse = true, version } = opts; const { description, parse = true, version, exitOnEnd = true } = opts;
const app = opts.app as App; const app = opts.app as App;
const _program = opts.program || program; const _program = opts.program || program;
_program.description(description || 'Router 命令行工具'); _program.description(description || 'Router 命令行工具');
@@ -134,30 +136,14 @@ export const parse = async (opts: {
_program.version(version); _program.version(version);
} }
app.createRouteList(); app.createRouteList();
app.route({
path: 'cli',
key: 'list'
}).define(async () => {
const routes = app.routes.map(route => {
return {
path: route.path,
key: route.key,
description: route?.metadata?.summary || route.description || '',
};
});
// 输出为表格格式
const table = routes.map(route => {
return `${route.path} ${route.key} - ${route.description}`;
}).join('\n');
console.log(table);
}).addTo(app, { overwrite: false })
createCliList(app);
createCommand({ app: app as App, program: _program }); createCommand({ app: app as App, program: _program });
if (opts.remote) { if (opts.remote) {
const { token, username, id } = opts.remote; const { token, username, id } = opts.remote;
const remoteApp = new RemoteApp({ const remoteApp = new RemoteApp({
app,
token, token,
username, username,
id, id,
@@ -167,9 +153,104 @@ export const parse = async (opts: {
remoteApp.listenProxy(); remoteApp.listenProxy();
console.log('已连接到远程应用,正在监听命令...'); console.log('已连接到远程应用,正在监听命令...');
} }
return return;
} }
if (parse) { if (parse) {
_program.parse(process.argv); await _program.parseAsync(process.argv);
if (exitOnEnd) {
process.exit(0);
} }
} }
}
const createCliList = (app: App) => {
app.route({
path: 'cli',
key: 'list',
description: '列出所有可用的命令',
metadata: {
summary: '列出所有可用的命令',
args: {
q: z.string().optional().describe('查询关键词,支持模糊匹配命令'),
path: z.string().optional().describe('按路径前缀过滤,如 user、admin'),
tags: z.string().optional().describe('按标签过滤,多个标签用逗号分隔'),
sort: z.enum(['key', 'path', 'name']).optional().describe('排序方式'),
limit: z.number().optional().describe('限制返回数量'),
offset: z.number().optional().describe('偏移量,用于分页'),
format: z.enum(['table', 'simple', 'json']).optional().describe('输出格式'),
}
}
}).define(async (ctx) => {
const { q, path: pathFilter, tags, sort, limit, offset, format } = ctx.query as any;
let routes = app.routes.map(route => {
return {
path: route.path,
key: route.key,
description: route?.metadata?.summary || route.description || '',
tags: route?.metadata?.tags || [],
};
});
// 路径过滤
if (pathFilter) {
routes = routes.filter(route => route.path.startsWith(pathFilter));
}
// 标签过滤
if (tags) {
const tagList = tags.split(',').map((t: string) => t.trim().toLowerCase()).filter(Boolean);
if (tagList.length > 0) {
routes = routes.filter(route => {
const routeTags = Array.isArray(route.tags) ? route.tags.map((t: unknown) => String(t).toLowerCase()) : [];
return tagList.some((tag: string) => routeTags.includes(tag));
});
}
}
// 关键词过滤
if (q) {
const keyword = q.toLowerCase();
routes = routes.filter(route => {
return route.path.toLowerCase().includes(keyword) ||
route.key.toLowerCase().includes(keyword) ||
route.description.toLowerCase().includes(keyword);
});
}
// 排序
if (sort) {
routes.sort((a, b) => {
if (sort === 'path') return a.path.localeCompare(b.path);
if (sort === 'key') return a.key.localeCompare(b.key);
return a.key.localeCompare(b.key); // name 默认为 key
});
}
// 分页
const total = routes.length;
const start = offset || 0;
const end = limit ? start + limit : undefined;
routes = routes.slice(start, end);
// 输出
const outputFormat = format || 'table';
if (outputFormat === 'json') {
console.log(JSON.stringify({ total, offset: start, limit, routes }, null, 2));
return;
}
if (outputFormat === 'simple') {
routes.forEach(route => {
console.log(`${route.path} ${route.key}`);
});
return;
}
// table 格式
const table = routes.map(route => {
return `${route.path} ${route.key} - ${route.description}`;
}).join('\n');
console.log(table);
}).addTo(app, { overwrite: false })
}

View File

@@ -60,11 +60,11 @@ export const createRouterAgentPluginFn = (opts?: {
addCallFn(router as App) addCallFn(router as App)
} }
if (router) { if (router) {
(router as any).route({ path: 'auth', key: '', id: 'auth', description: '认证' }).define(async (ctx) => { }).addTo(router as App, { (router as any).route({ path: 'auth', key: '', rid: 'auth', description: '认证' }).define(async (ctx) => { }).addTo(router as App, {
overwrite: false overwrite: false
}); });
(router as any).route({ path: 'auth-admin', key: '', id: 'auth-admin', description: '认证' }).define(async (ctx) => { }).addTo(router as App, { (router as any).route({ path: 'auth-admin', key: '', rid: 'auth-admin', description: '认证' }).define(async (ctx) => { }).addTo(router as App, {
overwrite: false overwrite: false
}) })
} }

View File

@@ -78,18 +78,18 @@ export type RouteContext<T = { code?: number }, U extends SimpleObject = {}, S =
export type SimpleObject = Record<string, any>; export type SimpleObject = Record<string, any>;
export type Run<T extends SimpleObject = {}> = (ctx: Required<RouteContext<T>>) => Promise<typeof ctx | null | void>; export type Run<T extends SimpleObject = {}> = (ctx: Required<RouteContext<T>>) => Promise<typeof ctx | null | void>;
export type RunMessage = { path?: string; key?: string; id?: string; payload?: any; }; export type RunMessage = { path?: string; key?: string; id?: string; payload?: any; };
export type NextRoute = Pick<Route, 'id' | 'path' | 'key'>; export type NextRoute = Pick<Route, 'rid' | 'path' | 'key'>;
export type RouteMiddleware = export type RouteMiddleware =
| { | {
path?: string; path?: string;
key?: string; key?: string;
id?: string; rid?: string;
} }
| string; | string;
export type RouteOpts<U = {}, T = SimpleObject> = { export type RouteOpts<U = {}, T = SimpleObject> = {
path?: string; path?: string;
key?: string; key?: string;
id?: string; rid?: string;
run?: Run<U>; run?: Run<U>;
nextRoute?: NextRoute; // route to run after this route nextRoute?: NextRoute; // route to run after this route
description?: string; description?: string;
@@ -99,7 +99,7 @@ export type RouteOpts<U = {}, T = SimpleObject> = {
isDebug?: boolean; isDebug?: boolean;
}; };
export type DefineRouteOpts = Omit<RouteOpts, 'idUsePath' | 'nextRoute'>; export type DefineRouteOpts = Omit<RouteOpts, 'idUsePath' | 'nextRoute'>;
const pickValue = ['path', 'key', 'id', 'description', 'type', 'middleware', 'metadata'] as const; const pickValue = ['path', 'key', 'rid', 'description', 'type', 'middleware', 'metadata'] as const;
export type Skill<T = SimpleObject> = { export type Skill<T = SimpleObject> = {
@@ -143,7 +143,7 @@ export class Route<M extends SimpleObject = SimpleObject, U extends SimpleObject
* 二级路径 * 二级路径
*/ */
key?: string; key?: string;
id?: string; rid?: string;
run?: Run<BuildRouteContext<M, U>>; run?: Run<BuildRouteContext<M, U>>;
nextRoute?: NextRoute; // route to run after this route nextRoute?: NextRoute; // route to run after this route
description?: string; description?: string;
@@ -164,7 +164,7 @@ export class Route<M extends SimpleObject = SimpleObject, U extends SimpleObject
this.key = key; this.key = key;
const pathKey = `${path}$$${key}`; const pathKey = `${path}$$${key}`;
if (opts) { if (opts) {
this.id = opts.id || hashIdMd5Sync(pathKey); this.rid = opts.rid || hashIdMd5Sync(pathKey);
this.run = opts.run as Run<BuildRouteContext<M, U>>; this.run = opts.run as Run<BuildRouteContext<M, U>>;
this.nextRoute = opts.nextRoute; this.nextRoute = opts.nextRoute;
this.description = opts.description; this.description = opts.description;
@@ -176,8 +176,8 @@ export class Route<M extends SimpleObject = SimpleObject, U extends SimpleObject
} else { } else {
this.middleware = []; this.middleware = [];
} }
if (!this.id) { if (!this.rid) {
this.id = hashIdMd5Sync(pathKey); this.rid = hashIdMd5Sync(pathKey);
} }
this.isDebug = opts?.isDebug ?? false; this.isDebug = opts?.isDebug ?? false;
} }
@@ -313,7 +313,7 @@ export class QueryRouter<T extends SimpleObject = SimpleObject> implements throw
* @param uniqueId * @param uniqueId
*/ */
removeById(uniqueId: string) { removeById(uniqueId: string) {
this.routes = this.routes.filter((r) => r.id !== uniqueId); this.routes = this.routes.filter((r) => r.rid !== uniqueId);
} }
/** /**
* 执行route * 执行route
@@ -327,7 +327,7 @@ export class QueryRouter<T extends SimpleObject = SimpleObject> implements throw
const maxNextRoute = this.maxNextRoute; const maxNextRoute = this.maxNextRoute;
ctx = (ctx || {}) as RouteContext<T>; ctx = (ctx || {}) as RouteContext<T>;
ctx.currentPath = path; ctx.currentPath = path;
ctx.currentId = route?.id; ctx.currentId = route?.rid;
ctx.currentKey = key; ctx.currentKey = key;
ctx.currentRoute = route; ctx.currentRoute = route;
ctx.index = (ctx.index || 0) + 1; ctx.index = (ctx.index || 0) + 1;
@@ -354,11 +354,11 @@ export class QueryRouter<T extends SimpleObject = SimpleObject> implements throw
let route: Route | undefined; let route: Route | undefined;
const isString = typeof item === 'string'; const isString = typeof item === 'string';
if (isString) { if (isString) {
route = this.routes.find((r) => r.id === item); route = this.routes.find((r) => r.rid === item);
} else { } else {
route = this.routes.find((r) => { route = this.routes.find((r) => {
if (item.id) { if (item.rid) {
return r.id === item.id; return r.rid === item.rid;
} else { } else {
// key 可以是空,所以可以不严格验证 // key 可以是空,所以可以不严格验证
return r.path === item.path && r.key == item.key; return r.path === item.path && r.key == item.key;
@@ -408,8 +408,8 @@ export class QueryRouter<T extends SimpleObject = SimpleObject> implements throw
ctx.message = e.message; ctx.message = e.message;
ctx.body = null; ctx.body = null;
} else { } else {
console.error(`[router error] fn:${route.path}-${route.key}:${route.id}`); console.error(`[router error] fn:${route.path}-${route.key}:${route.rid}`);
console.error(`[router error] middleware:${middleware.path}-${middleware.key}:${middleware.id}`); console.error(`[router error] middleware:${middleware.path}-${middleware.key}:${middleware.rid}`);
console.error(e) console.error(e)
ctx.code = 500; ctx.code = 500;
ctx.message = 'Internal Server Error'; ctx.message = 'Internal Server Error';
@@ -434,11 +434,11 @@ export class QueryRouter<T extends SimpleObject = SimpleObject> implements throw
console.error('=====debug====:', e); console.error('=====debug====:', e);
console.error('=====debug====:[path:key]:', `${route.path}-${route.key}`); console.error('=====debug====:[path:key]:', `${route.path}-${route.key}`);
} }
if (e instanceof CustomError) { if (e instanceof CustomError || e?.code) {
ctx.code = e.code; ctx.code = e.code;
ctx.message = e.message; ctx.message = e.message;
} else { } else {
console.error(`[router error] fn:${route.path}-${route.key}:${route.id}`); console.error(`[router error] fn:${route.path}-${route.key}:${route.rid}`);
console.error(`[router error] error`, e); console.error(`[router error] error`, e);
ctx.code = 500; ctx.code = 500;
ctx.message = 'Internal Server Error'; ctx.message = 'Internal Server Error';
@@ -455,8 +455,8 @@ export class QueryRouter<T extends SimpleObject = SimpleObject> implements throw
if (route.nextRoute.path || route.nextRoute.key) { if (route.nextRoute.path || route.nextRoute.key) {
path = route.nextRoute.path; path = route.nextRoute.path;
key = route.nextRoute.key; key = route.nextRoute.key;
} else if (route.nextRoute.id) { } else if (route.nextRoute.rid) {
const nextRoute = this.routes.find((r) => r.id === route.nextRoute.id); const nextRoute = this.routes.find((r) => r.rid === route.nextRoute.rid);
if (nextRoute) { if (nextRoute) {
path = nextRoute.path; path = nextRoute.path;
key = nextRoute.key; key = nextRoute.key;
@@ -529,14 +529,14 @@ export class QueryRouter<T extends SimpleObject = SimpleObject> implements throw
* @param ctx * @param ctx
* @returns * @returns
*/ */
async call(message: { id?: string; path?: string; key?: string; payload?: any }, ctx?: RouteContext<T> & { [key: string]: any }) { async call(message: { rid?: string; path?: string; key?: string; payload?: any }, ctx?: RouteContext<T> & { [key: string]: any }) {
let path = message.path; let path = message.path;
let key = message.key; let key = message.key;
// 优先 path + key // 优先 path + key
if (path) { if (path) {
return await this.parse({ ...message, path, key }, { ...this.context, ...ctx }); return await this.parse({ ...message, path, key }, { ...this.context, ...ctx });
} else if (message.id) { } else if (message.rid) {
const route = this.routes.find((r) => r.id === message.id); const route = this.routes.find((r) => r.rid === message.rid);
if (route) { if (route) {
path = route.path; path = route.path;
key = route.key; key = route.key;
@@ -630,11 +630,11 @@ export class QueryRouter<T extends SimpleObject = SimpleObject> implements throw
hasRoute(path: string, key: string = '') { hasRoute(path: string, key: string = '') {
return this.routes.find((r) => r.path === path && r.key === key); return this.routes.find((r) => r.path === path && r.key === key);
} }
findRoute(opts?: { path?: string; key?: string; id?: string }) { findRoute(opts?: { path?: string; key?: string; rid?: string }) {
const { path, key, id } = opts || {}; const { path, key, rid } = opts || {};
return this.routes.find((r) => { return this.routes.find((r) => {
if (id) { if (rid) {
return r.id === id; return r.rid === rid;
} }
if (path) { if (path) {
if (key !== undefined) { if (key !== undefined) {
@@ -655,14 +655,14 @@ export class QueryRouter<T extends SimpleObject = SimpleObject> implements throw
const tokenUser = ctx.state as unknown as { tokenUser?: any }; const tokenUser = ctx.state as unknown as { tokenUser?: any };
let isUser = !!tokenUser; let isUser = !!tokenUser;
const list = this.getList(opts?.filter).filter((item) => { const list = this.getList(opts?.filter).filter((item) => {
if (item.id === 'auth' || item.id === 'auth-can' || item.id === 'check-auth-admin' || item.id === 'auth-admin') { if (item.rid === 'auth' || item.rid === 'auth-can' || item.rid === 'check-auth-admin' || item.rid === 'auth-admin') {
return false; return false;
} }
return true; return true;
}); });
ctx.body = { ctx.body = {
list: list.map((item) => { list: list.map((item) => {
const route = pick(item, ['id', 'path', 'key', 'description', 'middleware', 'metadata'] as const); const route = pick(item, ['rid', 'path', 'key', 'description', 'middleware', 'metadata'] as const);
return toJSONSchemaRoute(route); return toJSONSchemaRoute(route);
}), }),
isUser isUser
@@ -766,7 +766,7 @@ export class QueryRouterServer<C extends SimpleObject = SimpleObject> extends Qu
* @param param0 * @param param0
* @returns * @returns
*/ */
async run(msg: { id?: string; path?: string; key?: string; payload?: any, token?: string, data?: any }, ctx?: Partial<RouteContext<C>>) { async run(msg: { rid?: string; path?: string; key?: string; payload?: any, token?: string, data?: any }, ctx?: Partial<RouteContext<C>>) {
const handle = this.handle; const handle = this.handle;
if (handle) { if (handle) {
return handle(msg, ctx); return handle(msg, ctx);
@@ -774,13 +774,52 @@ export class QueryRouterServer<C extends SimpleObject = SimpleObject> extends Qu
return super.run(msg, ctx as RouteContext<C>); return super.run(msg, ctx as RouteContext<C>);
} }
async runAction<T extends { id?: string; path?: string; key?: string; metadata?: { args?: any } } = {}>( async runAction<T extends { rid?: string; path?: string; key?: string; metadata?: { args?: any } } = {}>(
api: T, api: T,
payload: RunActionPayload<T>, payload: RunActionPayload<T>,
ctx?: RouteContext<C> ctx?: RouteContext<C>
) { ) {
const { path, key, id } = api as any; const { path, key, rid } = api as any;
return this.run({ path, key, id, payload }, ctx); return this.run({ path, key, rid, payload }, ctx);
}
/**
* 创建认证相关的中间件,默认是 auth, auth-admin, auth-can 三个中间件
* @param fun 认证函数,接收 RouteContext 和认证类型
*/
async createAuth(fun: (ctx: RouteContext<C>, type?: 'auth' | 'auth-admin' | 'auth-can') => any) {
this.route({
path: 'auth',
key: 'auth',
rid: 'auth',
description: 'token验证',
}).define(async (ctx) => {
if (fun) {
await fun(ctx, 'auth');
}
}).addTo(this, { overwrite: false });
this.route({
path: 'auth-admin',
key: 'auth-admin',
rid: 'auth-admin',
description: 'admin token验证',
middleware: ['auth']
}).define(async (ctx) => {
if (fun) {
await fun(ctx, 'auth-admin');
}
}).addTo(this, { overwrite: false });
this.route({
path: 'auth-can',
key: 'auth-can',
rid: 'auth-can',
description: '权限验证'
}).define(async (ctx) => {
if (fun) {
await fun(ctx, 'auth-can');
}
}).addTo(this, { overwrite: false });
} }
} }

View File

@@ -53,8 +53,8 @@ class Chain {
this.object.key = key; this.object.key = key;
return this; return this;
} }
setId(key: string) { setRid(key: string) {
this.object.id = key; this.object.rid = key;
return this; return this;
} }
setRun<U extends SimpleObject = {}>(run: Run<U>) { setRun<U extends SimpleObject = {}>(run: Run<U>) {