Compare commits

..

4 Commits

3 changed files with 146 additions and 27 deletions

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.1.6",
"description": "", "description": "",
"type": "module", "type": "module",
"main": "./dist/router.js", "main": "./dist/router.js",
@@ -30,7 +30,7 @@
"@kevisual/query": "^0.0.53", "@kevisual/query": "^0.0.53",
"@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.2.27",
"@types/bun": "^1.3.10", "@types/bun": "^1.3.10",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/node": "^25.5.0", "@types/node": "^25.5.0",
@@ -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,25 +136,8 @@ 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) {
@@ -167,9 +152,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

@@ -434,7 +434,7 @@ 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 {
@@ -782,6 +782,45 @@ export class QueryRouterServer<C extends SimpleObject = SimpleObject> extends Qu
const { path, key, id } = api as any; const { path, key, id } = api as any;
return this.run({ path, key, id, payload }, ctx); return this.run({ path, key, id, 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',
id: 'auth',
description: 'token验证',
}).define(async (ctx) => {
if (fun) {
await fun(ctx, 'auth');
}
}).addTo(this, { overwrite: false });
this.route({
path: 'auth-admin',
key: 'auth-admin',
id: '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',
id: 'auth-can',
description: '权限验证'
}).define(async (ctx) => {
if (fun) {
await fun(ctx, 'auth-can');
}
}).addTo(this, { overwrite: false });
}
} }