Compare commits

..

18 Commits

Author SHA1 Message Date
b375e5ac23 chore: 更新版本号至 0.0.88 并修改类型导入以支持 QueryRouterServer 2026-03-07 01:46:01 +08:00
7345940f18 chore: 添加 commander 模块及相关命令行工具功能 2026-03-07 01:41:47 +08:00
a9cf1505ff chore: 更新版本号并添加 runAction 方法及其类型推断支持 2026-03-06 23:41:31 +08:00
9ed6e63d9e chore: 更新依赖项版本以修复兼容性问题 2026-03-05 01:02:07 +08:00
e58c99c285 ws 2026-03-05 01:01:13 +08:00
xiongxiao
2628eb3693 chore: 移除未使用的类型定义并更新 RouteOpts 类型以支持复合路由 2026-03-02 04:01:17 +08:00
46499bedc7 chore: 更新依赖项版本以保持兼容性 2026-03-01 01:10:16 +08:00
xiongxiao
ab61be4875 Add design documents for HTTP server, router, and WebSocket server
- Created `https-server.md` detailing the HTTP server design, including request normalization, routing, and built-in routes.
- Added `router.md` outlining the router system design, core components, and execution flow.
- Introduced `ws-server.md` for the WebSocket server design, covering connection handling, message protocols, and custom listener registration.
2026-02-28 14:40:20 +08:00
52b10f2f03 chore: 删除不再使用的文件并更新路由上下文以支持自定义字段 2026-02-24 01:01:43 +08:00
f8337a1216 temp 2026-02-23 23:47:59 +08:00
ad95dc0081 chore: 更新 QueryRouterServer 和 App 类以支持自定义 RouteContext 类型 2026-02-23 23:43:12 +08:00
9859c2f673 temp 2026-02-23 23:36:47 +08:00
0152d15824 chore: 在文档和代码中添加 currentId 属性,增强路由上下文信息 2026-02-23 23:11:37 +08:00
5c24e197e6 chore: 更新身份验证中间件为 'auth-admin',并简化身份验证路由定义 2026-02-21 01:04:35 +08:00
af7d809270 chore: 更新版本号至0.0.83,并在 CustomError 类中添加静态 throw 方法以增强错误处理 2026-02-21 00:26:21 +08:00
a8f409f900 chore: 更新版本号至0.0.82,并在 CustomError 类中增强构造函数以支持对象参数 2026-02-20 22:53:03 +08:00
132aa3a888 chore: 在 ServerBase 类中添加自定义错误处理逻辑 2026-02-20 22:34:48 +08:00
2e59e318bf Refactor code structure for improved readability and maintainability 2026-02-20 22:25:44 +08:00
34 changed files with 2698 additions and 435 deletions

View File

@@ -1,14 +1,20 @@
import { app } from '../app.ts'
import './route-create.ts'
if (!app.hasRoute('auth', '')) {
app.route({
app.route({
path: 'auth',
key: '',
id: 'auth',
description: '身份验证路由',
}).define(async (ctx) => {
}).define(async (ctx) => {
//
}).addTo(app);
}
}).addTo(app, { overwrite: false });
app.route({
path: 'auth-admin',
key: '',
id: 'auth-admin',
description: '管理员身份验证路由',
}).define(async (ctx) => {
//
}).addTo(app, { overwrite: false });

View File

@@ -6,7 +6,7 @@ app.route({
path: 'router-skill',
key: 'create-route',
description: '创建路由技能',
middleware: ['auth'],
middleware: ['auth-admin'],
metadata: {
tags: ['opencode'],
...createSkill({
@@ -38,7 +38,7 @@ app.route({
path: 'router-skill',
key: 'version',
description: '获取最新router版本号',
middleware: ['auth'],
middleware: ['auth-admin'],
metadata: {
tags: ['opencode'],
...createSkill({
@@ -59,7 +59,7 @@ app.route({
path: 'route-skill',
key: 'test',
description: '测试路由技能',
middleware: ['auth'],
middleware: ['auth-admin'],
metadata: {
tags: ['opencode'],
...createSkill({

View File

@@ -1,15 +1,18 @@
import { buildWithBun } from '@kevisual/code-builder';
await buildWithBun({ naming: 'app', entry: 'agent/main.ts', dts: true });
const external: any[] = []
await buildWithBun({ naming: 'app', entry: 'agent/main.ts', dts: true, external });
await buildWithBun({ naming: 'router', entry: 'src/index.ts', dts: true });
await buildWithBun({ naming: 'router', entry: 'src/index.ts', dts: true, external });
await buildWithBun({ naming: 'router-browser', entry: 'src/app-browser.ts', target: 'browser', dts: true });
await buildWithBun({ naming: 'router-browser', entry: 'src/app-browser.ts', target: 'browser', dts: true, external });
await buildWithBun({ naming: 'router-define', entry: 'src/router-define.ts', target: 'browser', dts: true });
await buildWithBun({ naming: 'router-define', entry: 'src/router-define.ts', target: 'browser', dts: true, external });
await buildWithBun({ naming: 'router-simple', entry: 'src/router-simple.ts', dts: true });
await buildWithBun({ naming: 'router-simple', entry: 'src/router-simple.ts', dts: true, external });
await buildWithBun({ naming: 'opencode', entry: 'src/opencode.ts', dts: true });
await buildWithBun({ naming: 'opencode', entry: 'src/opencode.ts', dts: true, external });
await buildWithBun({ naming: 'ws', entry: 'src/ws.ts', dts: true });
await buildWithBun({ naming: 'ws', entry: 'src/ws.ts', dts: true, external });
await buildWithBun({ naming: 'commander', entry: 'src/commander.ts', dts: true, external });

View File

@@ -9,21 +9,21 @@
},
"devDependencies": {
"@kevisual/code-builder": "^0.0.6",
"@kevisual/context": "^0.0.6",
"@kevisual/context": "^0.0.8",
"@kevisual/dts": "^0.0.4",
"@kevisual/js-filter": "^0.0.5",
"@kevisual/local-proxy": "^0.0.8",
"@kevisual/query": "^0.0.47",
"@kevisual/query": "^0.0.49",
"@kevisual/use-config": "^1.0.30",
"@opencode-ai/plugin": "^1.2.6",
"@opencode-ai/plugin": "^1.2.10",
"@types/bun": "^1.3.9",
"@types/node": "^25.2.3",
"@types/node": "^25.3.0",
"@types/send": "^1.2.1",
"@types/ws": "^8.18.1",
"@types/xml2js": "^0.4.14",
"eventemitter3": "^5.0.4",
"fast-glob": "^3.3.3",
"hono": "^4.11.9",
"hono": "^4.12.2",
"nanoid": "^5.1.6",
"path-to-regexp": "^8.3.0",
"send": "^1.2.1",
@@ -43,7 +43,7 @@
"@kevisual/code-builder": ["@kevisual/code-builder@0.0.6", "", { "bin": { "code-builder": "bin/code.js", "builder": "bin/code.js" } }, "sha512-0aqATB31/yw4k4s5/xKnfr4DKbUnx8e3Z3BmKbiXTrc+CqWiWTdlGe9bKI9dZ2Df+xNp6g11W4xM2NICNyyCCw=="],
"@kevisual/context": ["@kevisual/context@0.0.6", "", {}, "sha512-w7HBOuO3JH37n6xT6W3FD7ykqHTwtyxOQzTzfEcKDCbsvGB1wVreSxFm2bvoFnnFLuxT/5QMpKlnPrwvmcTGnw=="],
"@kevisual/context": ["@kevisual/context@0.0.8", "", {}, "sha512-DTJpyHI34NE76B7g6f+QlIqiCCyqI2qkBMQE736dzeRDGxOjnbe2iQY9W+Rt2PE6kmymM3qyOmSfNovyWyWrkA=="],
"@kevisual/dts": ["@kevisual/dts@0.0.4", "", { "dependencies": { "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-typescript": "^12.3.0", "rollup": "^4.57.1", "rollup-plugin-dts": "^6.3.0", "tslib": "^2.8.1" }, "bin": { "dts": "bin/dts.mjs" } }, "sha512-FVUaH/0nyhbHWpEVjFTGP54PLMm4Hf06aqWLdHOYHNPIgr1aK1C26kOH7iumklGFGk9w93IGxj8Zxe5fap5N2A=="],
@@ -53,7 +53,7 @@
"@kevisual/local-proxy": ["@kevisual/local-proxy@0.0.8", "", {}, "sha512-VX/P+6/Cc8ruqp34ag6gVX073BchUmf5VNZcTV/6MJtjrNE76G8V6TLpBE8bywLnrqyRtFLIspk4QlH8up9B5Q=="],
"@kevisual/query": ["@kevisual/query@0.0.47", "", {}, "sha512-ZR7WXeDDGUSzBtcGVU3J173sA0hCqrGTw5ybGbdNGlM0VyJV/XQIovCcSoZh1YpnciLRRqJvzXUgTnCkam+M3g=="],
"@kevisual/query": ["@kevisual/query@0.0.49", "", {}, "sha512-GrWW+QlBO5lkiqvb7PjOstNtpTQVSR74EHHWjm7YoL9UdT1wuPQXGUApZHmMBSh3NIWCf0AL2G1hPWZMC7YeOQ=="],
"@kevisual/use-config": ["@kevisual/use-config@1.0.30", "", { "dependencies": { "@kevisual/load": "^0.0.6" }, "peerDependencies": { "dotenv": "^17" } }, "sha512-kPdna0FW/X7D600aMdiZ5UTjbCo6d8d4jjauSc8RMmBwUU6WliFDSPUNKVpzm2BsDX5Nth1IXFPYMqH+wxqAmw=="],
@@ -63,9 +63,9 @@
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.2.6", "", { "dependencies": { "@opencode-ai/sdk": "1.2.6", "zod": "4.1.8" } }, "sha512-CJEp3k17yWsjyfivm3zQof8L42pdze3a7iTqMOyesHgJplSuLiBYAMndbBYMDuJkyAh0dHYjw8v10vVw7Kfl4Q=="],
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.2.15", "", { "dependencies": { "@opencode-ai/sdk": "1.2.15", "zod": "4.1.8" } }, "sha512-mh9S05W+CZZmo6q3uIEBubS66QVgiev7fRafX7vemrCfz+3pEIkSwipLjU/sxIewC9yLiDWLqS73DH/iEQzVDw=="],
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.2.6", "", {}, "sha512-dWMF8Aku4h7fh8sw5tQ2FtbqRLbIFT8FcsukpxTird49ax7oUXP+gzqxM/VdxHjfksQvzLBjLZyMdDStc5g7xA=="],
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.2.15", "", {}, "sha512-NUJNlyBCdZ4R0EBLjJziEQOp2XbRPJosaMcTcWSWO5XJPKGUpz0u8ql+5cR8K+v2RJ+hp2NobtNwpjEYfe6BRQ=="],
"@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@29.0.0", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" } }, "sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ=="],
@@ -129,7 +129,7 @@
"@types/estree": ["@types/estree@1.0.8", "", {}, ""],
"@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
"@types/node": ["@types/node@25.3.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q=="],
"@types/resolve": ["@types/resolve@1.20.2", "", {}, ""],
@@ -185,7 +185,7 @@
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, ""],
"hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="],
"hono": ["hono@4.12.3", "", {}, "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
@@ -261,7 +261,7 @@
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, ""],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"ws": ["@kevisual/ws@8.0.0", "", {}, "sha512-jlFxSlXUEz93cFW+UYT5BXv/rFVgiMQnIfqRYZ0gj1hSP8PMGRqMqUoHSLfKvfRRS4jseLSvTTeEKSQpZJtURg=="],
@@ -279,6 +279,16 @@
"@types/xml2js/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"bun-types/@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, ""],
"@types/send/@types/node/undici-types": ["undici-types@7.16.0", "", {}, ""],
"@types/ws/@types/node/undici-types": ["undici-types@7.16.0", "", {}, ""],
"@types/xml2js/@types/node/undici-types": ["undici-types@7.16.0", "", {}, ""],
"bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, ""],
}
}

View File

@@ -31,6 +31,7 @@ app.route({
},
}).define(async (ctx) => {
ctx.body = '03';
ctx.args.test
return ctx;
}).addTo(app);
// app.server.on({

View File

@@ -18,15 +18,6 @@ route01.run = async (ctx) => {
ctx.body = '01';
return ctx;
};
app.use(
'demo',
async (ctx) => {
ctx.body = '01';
return ctx;
},
{ key: '01' },
);
const route02 = new Route('demo', '02');
route02.run = async (ctx) => {
ctx.body = '02';

View File

@@ -1,6 +0,0 @@
import { createCert } from '@kevisual/router/sign';
import { writeFileSync } from 'fs';
const { key, cert } = createCert();
writeFileSync('https-key.pem', key);
writeFileSync('https-cert.pem', cert);

View File

@@ -20,7 +20,7 @@ router
.define(async (ctx) => {
ctx.body = 'Hello, world!';
// throw new CustomError('error');
throw new CustomError(5000, 'error');
ctx.throw(5000, 'error');
})
.addTo(router);

View File

@@ -1,19 +0,0 @@
import { createCert } from '@kevisual/router/src/sign.ts';
import fs from 'node:fs';
const cert = createCert();
fs.writeFileSync('pem/https-private-key.pem', cert.key);
fs.writeFileSync('pem/https-cert.pem', cert.cert);
fs.writeFileSync(
'pem/https-config.json',
JSON.stringify(
{
createTime: new Date().getTime(),
expireDate: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).getTime(),
expireTime: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(),
},
null,
2,
),
);

View File

@@ -16,10 +16,16 @@ const queryApp = new QueryRouterServer();
app
.route({
path: 'hello',
metadata: {
args: {
name: 'string',
},
}
})
.define(async (ctx) => {
// console.log('hello', ctx);
// console.log('hello', ctx.res);
ctx.query.name;
console.log('hello', ctx.query.cookies);
// ctx.res?.cookie?.('token', 'abc', {
// domain: '*', // 设置为顶级域名,允许跨子域共享

View File

@@ -1,14 +0,0 @@
import { createSchema } from '@kevisual/router';
const a = createSchema({
type: 'string',
minLength: 1,
maxLength: 10,
regex: '^[a-zA-Z0-9_]+$',
required: false,
});
console.log(a.safeParse('1234567890'));
console.log(a.safeParse('').error);
console.log(a.safeParse(undefined));
console.log(a.safeParse(null).error);

View File

@@ -1,35 +0,0 @@
import { SimpleRouter } from '@kevisual/router/src/router-simple.ts';
export const router = new SimpleRouter();
router.get('/', async (req, res) => {
console.log('get /');
});
router.post('/post', async (req, res) => {
console.log('post /post');
console.log('req body:', req, res);
res.end('post response');
});
router.get('/user/:id', async (req, res) => {
console.log('get /user/:id', req.params);
res.end(`user id is ${req.params.id}`);
});
router.post('/user/:id', async (req, res) => {
console.log('post /user/:id params', req.params);
const body = await router.getBody(req);
console.log('post body:', body);
res.end(`post user id is ${req.params.id}`);
});
router.post('/user/:id/a', async (req, res) => {
console.log('post /user/:id', req.params);
res.end(`post user id is ${req.params.id} a`);
});
// router.parse({ url: 'http://localhost:3000/', method: 'GET' } as any, {} as any);
// router.parse({ url: 'http://localhost:3000/post', method: 'POST' } as any, {} as any);
// router.parse({ url: 'http://localhost:3000/user/1/a', method: 'GET' } as any, {} as any);
// router.parse({ url: 'http://localhost:3000/user/1/a', method: 'POST' } as any, {} as any);

View File

@@ -1,56 +0,0 @@
import { App } from '@kevisual/router/src/app.ts';
import { router } from './a.ts';
export const app = new App();
app.server.on([{
fun: async (req, res) => {
console.log('Received request:', req.method, req.url);
const p = await router.parse(req, res);
if (p) {
console.log('Router parse result:', p);
}
}
}, {
id: 'abc',
path: '/ws',
io: true,
fun: async (data, end) => {
console.log('Custom middleware for /ws');
console.log('Data received:', data);
end({ message: 'Hello from /ws middleware' });
}
}]);
app.server.listen(3004, () => {
console.log('Server is running on http://localhost:3004');
// fetch('http://localhost:3004/', { method: 'GET' }).then(async (res) => {
// const text = await res.text();
// console.log('Response for GET /:', text);
// });
// fetch('http://localhost:3004/post', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({ message: 'Hello, server!' }),
// }).then(async (res) => {
// const text = await res.text();
// console.log('Response for POST /post:', text);
// });
// fetch('http://localhost:3004/user/123', { method: 'GET' }).then(async (res) => {
// const text = await res.text();
// console.log('Response for GET /user/123:', text);
// });
fetch('http://localhost:3004/user/456', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'User456' }),
}).then(async (res) => {
const text = await res.text();
console.log('Response for POST /user/456:', text);
});
});

View File

@@ -65,8 +65,6 @@ app.importRoutes(app1.exportRoutes());
app.importRoutes(app2.exportRoutes());
app.importApp(app3);
app.listen(4003, () => {
console.log(`http://localhost:4003/api/router?path=app1&key=02`);
console.log(`http://localhost:4003/api/router?path=app1&key=01`);

View File

@@ -26,41 +26,6 @@ qr.add(
description: 'get project detail2',
run: async (ctx: RouteContext) => {
ctx!.body = 'project detail2';
return ctx;
},
validator: {
id: {
type: 'number',
required: true,
message: 'id is required',
},
data: {
// @ts-ignore
type: 'object',
message: 'data query is error',
properties: {
name: {
type: 'string',
required: true,
message: 'name is required',
},
age: {
type: 'number',
required: true,
message: 'age is error',
},
friends: {
type: 'object',
properties: {
hair: {
type: 'string',
required: true,
message: 'hair is required',
},
},
},
},
},
},
}),
);
@@ -73,7 +38,7 @@ const main = async () => {
id: 4,
data: {
name: 'john',
age: 's'+13,
age: 's' + 13,
friends: {
hair: 'black',
messages: 'hello',

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package",
"name": "@kevisual/router",
"version": "0.0.80",
"version": "0.0.88",
"description": "",
"type": "module",
"main": "./dist/router.js",
@@ -23,21 +23,22 @@
"license": "MIT",
"devDependencies": {
"@kevisual/code-builder": "^0.0.6",
"@kevisual/context": "^0.0.6",
"@kevisual/context": "^0.0.8",
"@kevisual/dts": "^0.0.4",
"@kevisual/js-filter": "^0.0.5",
"@kevisual/local-proxy": "^0.0.8",
"@kevisual/query": "^0.0.47",
"@kevisual/query": "^0.0.53",
"@kevisual/use-config": "^1.0.30",
"@opencode-ai/plugin": "^1.2.6",
"@types/bun": "^1.3.9",
"@types/node": "^25.2.3",
"@opencode-ai/plugin": "^1.2.20",
"@types/bun": "^1.3.10",
"@types/node": "^25.3.5",
"@types/send": "^1.2.1",
"@types/ws": "^8.18.1",
"@types/xml2js": "^0.4.14",
"commander": "^14.0.3",
"eventemitter3": "^5.0.4",
"fast-glob": "^3.3.3",
"hono": "^4.11.9",
"hono": "^4.12.5",
"nanoid": "^5.1.6",
"path-to-regexp": "^8.3.0",
"send": "^1.2.1",
@@ -51,7 +52,7 @@
"url": "git+https://github.com/abearxiong/kevisual-router.git"
},
"dependencies": {
"es-toolkit": "^1.44.0"
"es-toolkit": "^1.45.1"
},
"publishConfig": {
"access": "public"
@@ -59,6 +60,7 @@
"exports": {
".": "./dist/router.js",
"./browser": "./dist/router-browser.js",
"./commander": "./dist/commander.js",
"./simple": "./dist/router-simple.js",
"./opencode": "./dist/opencode.js",
"./skill": "./dist/app.js",

1212
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -32,13 +32,14 @@ app
在 route handler 中,你可以通过 `ctx` 访问以下属性:
| 属性 | 类型 | 说明 |
|------|------|------|
| --------------- | ---------------------------- | ---------------------------- |
| `query` | `object` | 请求参数,会自动合并 payload |
| `body` | `number \| string \| Object` | 响应内容 |
| `code` | `number` | 响应状态码,默认为 200 |
| `message` | `string` | 响应消息 |
| `state` | `any` | 状态数据,可在路由间传递 |
| `appId` | `string` | 应用标识 |
| `currentId` | `string` | 当前路由ID |
| `currentPath` | `string` | 当前路由路径 |
| `currentKey` | `string` | 当前路由 key |
| `currentRoute` | `Route` | 当前 Route 实例 |
@@ -53,7 +54,7 @@ app
### 上下文方法
| 方法 | 参数 | 说明 |
|------|------|------|
| ----------------------------------- | ----------------------------------------- | -------------------------------------------- |
| `ctx.call(msg, ctx?)` | `{ path, key?, payload?, ... } \| { id }` | 调用其他路由,返回完整 context |
| `ctx.run(msg, ctx?)` | `{ path, key?, payload? }` | 调用其他路由,返回 `{ code, data, message }` |
| `ctx.forward(res)` | `{ code, data?, message? }` | 设置响应结果 |
@@ -63,31 +64,25 @@ app
```ts
import { App } from '@kevisual/router';
import z from 'zod';
const app = new App();
app.listen(4002);
// 基本路由
app
.route({ path: 'user', key: 'info' })
.route({ path: 'user', key: 'info', id: 'user-info' })
.define(async (ctx) => {
// ctx.query 包含请求参数
const { id } = ctx.query;
// 使用 state 在路由间传递数据
ctx.state.orderId = '12345';
ctx.body = { id, name: '张三' };
ctx.code = 200;
})
.addTo(app);
// 使用 state 在路由间传递数据
app
.route({ path: 'order', key: 'create' })
.define(async (ctx) => {
ctx.state = { orderId: '12345' };
})
.addTo(app);
app
.route({ path: 'order', key: 'pay' })
.route({ path: 'order', key: 'pay', middleware: ['user-info'] })
.define(async (ctx) => {
// 可以获取前一个路由设置的 state
const { orderId } = ctx.state;
@@ -95,14 +90,6 @@ app
})
.addTo(app);
// 链式调用
app
.route({ path: 'product', key: 'list' })
.define(async (ctx) => {
ctx.body = [{ id: 1, name: '商品A' }];
})
.addTo(app);
// 调用其他路由
app
.route({ path: 'dashboard', key: 'stats' })
@@ -114,7 +101,7 @@ app
ctx.body = {
user: userRes.data,
products: productRes.data
products: productRes.data,
};
})
.addTo(app);
@@ -140,17 +127,20 @@ import { App, Route } from '@kevisual/router';
const app = new App();
// 定义中间件
app.route({
app
.route({
id: 'auth-example',
description: '权限校验中间件'
}).define(async(ctx) => {
description: '权限校验中间件',
})
.define(async (ctx) => {
const token = ctx.query.token;
if (!token) {
ctx.throw(401, '未登录', '需要 token');
}
// 验证通过,设置用户信息到 state
ctx.state.tokenUser = { id: 1, name: '用户A' };
}).addTo(app);
})
.addTo(app);
// 使用中间件(通过 id 引用)
app
@@ -163,6 +153,33 @@ app
.addTo(app);
```
## 一个丰富的router示例
```ts
import { App } from '@kevisual/router';
const app = new App();
app
.router({
path: 'dog',
key: 'info',
description: '获取狗的信息',
metedata: {
args: {
owner: z.string().describe('狗主人姓名'),
age: z.number().describe('狗的年龄'),
},
},
})
.define(async (ctx) => {
const { owner, age } = ctx.query;
ctx.body = {
content: `这是一只${age}岁的狗,主人是${owner}`,
};
})
.addTo(app);
```
## 注意事项
1. **path 和 key 的组合是路由的唯一标识**,同一个 path+key 只能添加一个路由,后添加的会覆盖之前的。
@@ -173,16 +190,14 @@ app
3. **ctx.throw 会自动结束执行**,抛出自定义错误。
4. **state 不会自动继承**,每个路由的 state 是独立的,除非显式传递或使用 nextRoute
4. **payload 会自动合并到 query**,调用 `ctx.run({ path, key, payload })`payload 会合并到 query
5. **payload 会自动合并到 query**,调用 `ctx.run({ path, key, payload })`payload 会合并到 query。
5. **nextQuery 用于传递给 nextRoute**,在当前路由中设置 `ctx.nextQuery`,会在执行 nextRoute 时合并到 query。
6. **nextQuery 用于传递给 nextRoute**在当前路由中设置 `ctx.nextQuery`,会在执行 nextRoute 时合并到 query
6. **避免 nextRoute 循环调用**默认最大深度为 40 次,超过会返回 500 错误
7. **避免 nextRoute 循环调用**,默认最大深度为 40 次,超过会返回 500 错误
7. **needSerialize 默认为 true**,会自动对 body 进行 JSON 序列化和反序列化
8. **needSerialize 默认为 true**,会自动对 body 进行 JSON 序列化和反序列化
8. **progress 记录执行路径**,可用于调试和追踪路由调用链
9. **progress 记录执行路径**,可用于调试和追踪路由调用链。
10. **中间件找不到会返回 404**,错误信息中会包含找不到的中间件列表。
9. **中间件找不到会返回 404**,错误信息中会包含找不到的中间件列表。

View File

@@ -1,4 +1,4 @@
import { AddOpts, QueryRouter, Route, RouteContext, RouteOpts } from './route.ts';
import { AddOpts, QueryRouter, QueryRouterServer, Route, RouteContext, RouteOpts } from './route.ts';
import { ServerNode, ServerNodeOpts } from './server/server.ts';
import { HandleCtx } from './server/server-base.ts';
import { ServerType } from './server/server-type.ts';
@@ -10,7 +10,7 @@ import { randomId } from './utils/random.ts';
type RouterHandle = (msg: { path: string;[key: string]: any }) => { code: string; data?: any; message?: string;[key: string]: any };
type AppOptions<T = {}> = {
router?: QueryRouter;
router?: QueryRouterServer;
server?: ServerType;
/** handle msg 关联 */
routerHandle?: RouterHandle;
@@ -19,18 +19,19 @@ type AppOptions<T = {}> = {
appId?: string;
};
export type AppRouteContext<T = {}> = HandleCtx & RouteContext<T> & { app: App<T> };
export type AppRouteContext<T> = HandleCtx & RouteContext<T> & { app: App<T> };
/**
* 封装了 Router 和 Server 的 App 模块处理http的请求和响应内置了 Cookie 和 Token 和 res 的处理
* U - Route Context的扩展类型
*/
export class App<U = {}> extends QueryRouter {
export class App<U = {}> extends QueryRouterServer<AppRouteContext<U>> {
declare appId: string;
router: QueryRouter;
router: QueryRouterServer;
server: ServerType;
declare context: AppRouteContext<U>;
constructor(opts?: AppOptions<U>) {
super();
super({ initHandle: false, context: { needSerialize: true, ...opts?.routerContext } as any });
const router = this;
let server = opts?.server;
if (!server) {
@@ -42,7 +43,6 @@ export class App<U = {}> extends QueryRouter {
}
}
server.setHandle(router.getHandle(router, opts?.routerHandle, opts?.routerContext));
router.setContext({ needSerialize: true, ...opts?.routerContext });
this.router = router;
this.server = server;
if (opts?.appId) {
@@ -64,50 +64,7 @@ export class App<U = {}> extends QueryRouter {
// @ts-ignore
this.server.listen(...args);
}
addRoute(route: Route, opts?: AddOpts) {
super.add(route, opts);
}
Route = Route;
route(opts: RouteOpts<AppRouteContext<U>>): Route<AppRouteContext<U>>;
route(path: string, key?: string): Route<AppRouteContext<U>>;
route(path: string, opts?: RouteOpts<AppRouteContext<U>>): Route<AppRouteContext<U>>;
route(path: string, key?: string, opts?: RouteOpts<AppRouteContext<U>>): Route<AppRouteContext<U>>;
route(...args: any[]) {
const [path, key, opts] = args;
if (typeof path === 'object') {
return new Route(path.path, path.key, path);
}
if (typeof path === 'string') {
if (opts) {
return new Route(path, key, opts);
}
if (key && typeof key === 'object') {
return new Route(path, key?.key || '', key);
}
return new Route(path, key);
}
return new Route(path, key, opts);
}
prompt(description: string): Route<AppRouteContext<U>>
prompt(description: Function): Route<AppRouteContext<U>>
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?: AppRouteContext<U> & { [key: string]: any }) {
return await super.call(message, ctx);
}
async run(msg: { id?: string, path?: string; key?: string; payload?: any }, ctx?: Partial<AppRouteContext<U>> & { [key: string]: any }) {
return await super.run(msg, ctx);
}
static handleRequest(req: IncomingMessage, res: ServerResponse) {
return handleServer(req, res);
}

113
src/commander.ts Normal file
View File

@@ -0,0 +1,113 @@
import { program } from 'commander';
import { App, QueryRouterServer } from './app.ts';
export const groupByPath = (routes: App['routes']) => {
return routes.reduce((acc, route) => {
const path = route.path || 'default';
if (!acc[path]) {
acc[path] = [];
}
acc[path].push(route);
return acc;
}, {} as Record<string, typeof routes>);
}
export const parseArgs = (args: string) => {
try {
return JSON.parse(args);
} catch {
// 尝试解析 a=b b=c 格式
const result: Record<string, string> = {};
const pairs = args.match(/(\S+?)=(\S+)/g);
if (pairs && pairs.length > 0) {
for (const pair of pairs) {
const idx = pair.indexOf('=');
const key = pair.slice(0, idx);
const value = pair.slice(idx + 1);
result[key] = value;
}
return result;
}
throw new Error('Invalid arguments: expected JSON or key=value pairs (e.g. a=b c=d)');
}
}
export const parseDescription = (route: App['routes'][number]) => {
let desc = '';
if (route.metadata?.skill) {
desc += `\n\t=====${route.metadata.skill}=====\n`;
}
let hasSummary = false;
if (route.metadata?.summary) {
desc += `\t${route.metadata.summary}`;
hasSummary = true;
}
if (route.metadata?.args) {
const argsLines = Object.entries(route.metadata.args).map(([key, schema]: [string, any]) => {
const defType: string = schema?._def?.type ?? schema?.type ?? '';
const isOptional = defType === 'optional';
const innerType: string = isOptional
? (schema?._def?.innerType?.type ?? schema?._def?.innerType?._def?.type ?? '')
: defType;
const description: string =
schema?.description ??
schema?._def?.description ??
'';
const optionalMark = isOptional ? '?' : '';
const descPart = description ? ` ${description}` : '';
return `\t - ${key}${optionalMark}: ${innerType}${descPart}`;
});
desc += '\n\targs:\n' + argsLines.join('\n');
}
if (route.description && !hasSummary) {
desc += `\t - ${route.description}`;
}
return desc;
}
export const createCommand = (opts: { app: App, program: typeof program }) => {
const { app, program } = opts;
const routes = app.routes;
const groupRoutes = groupByPath(routes);
for (const path in groupRoutes) {
const routeList = groupRoutes[path];
const keys = routeList.map(route => route.key).filter(Boolean);
const subProgram = program.command(path).description(`路由《${path}${keys.length > 0 ? ': ' + keys.join(', ') : ''}`);
routeList.forEach(route => {
if (!route.key) return;
const description = parseDescription(route);
subProgram.command(route.key)
.description(description || '')
.option('--args <args>', 'JSON字符串参数传递给命令执行')
.action(async (options) => {
const output = (data: any) => {
if (typeof data === 'object') {
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
} else {
process.stdout.write(String(data) + '\n');
}
}
try {
const args = options.args ? parseArgs(options.args) : {};
// 这里可以添加实际的命令执行逻辑,例如调用对应的路由处理函数
const res = await app.run({ path, key: route.key, payload: args }, { appId: app.appId });
if (res.code === 200) {
output(res.data);
} else {
output(`Error: ${res.message}`);
}
} catch (error) {
output(`Execution error: ${error instanceof Error ? error.message : String(error)}`);
}
});
});
}
}
export const parse = (opts: { app: QueryRouterServer, description?: string, parse?: boolean }) => {
const { app, description, parse = true } = opts;
program.description(description || 'Router 命令行工具');
createCommand({ app: app as App, program });
if (parse) {
program.parse(process.argv);
}
}

View File

@@ -10,7 +10,7 @@ export const addCallFn = (app: App) => {
path: 'call',
key: '',
description: '调用',
middleware: ['auth'],
middleware: ['auth-admin'],
metadata: {
tags: ['opencode'],
...createSkill({
@@ -24,7 +24,7 @@ export const addCallFn = (app: App) => {
args: {
path: tool.schema.string().describe('应用路径,例如 cnb'),
key: tool.schema.string().optional().describe('应用key例如 list-repos'),
payload: tool.schema.object({}).optional().describe('调用参数'),
payload: tool.schema.object({}).optional().describe('调用参数, 为对象, 例如 { "query": "javascript" }'),
}
})
},
@@ -59,9 +59,16 @@ export const createRouterAgentPluginFn = (opts?: {
if (!router.hasRoute('call', '')) {
addCallFn(router as App)
}
if (!router.hasRoute('auth', '')) {
router.route({ path: 'auth', key: '', id: 'auth', description: '认证' }).define(async (ctx) => { }).addTo(router as App)
if (router) {
(router as any).route({ path: 'auth', key: '', id: 'auth', description: '认证' }).define(async (ctx) => { }).addTo(router as App, {
overwrite: false
});
(router as any).route({ path: 'auth-admin', key: '', id: 'auth-admin', description: '认证' }).define(async (ctx) => { }).addTo(router as App, {
overwrite: false
})
}
const _routes = filter(router.routes, opts?.query || '')
const routes = _routes.filter(r => {
const metadata = r.metadata as Skill
@@ -75,7 +82,7 @@ export const createRouterAgentPluginFn = (opts?: {
});
// opencode run "使用技能查看系统信息"
const AgentPlugin: Plugin = async (pluginInput) => {
useContextKey<PluginInput>('plugin-input', () => pluginInput, true)
useContextKey<PluginInput>('plugin-input', () => pluginInput, { isNew: true })
const hooks = opts?.hooks ? await opts.hooks(pluginInput) : {}
return {
...hooks,

View File

@@ -1,20 +1,25 @@
export type CustomErrorOptions = {
cause?: Error | string;
code?: number;
message?: string;
}
/** 自定义错误 */
export class CustomError extends Error {
code?: number;
data?: any;
message: string;
tips?: string;
constructor(code?: number | string, message?: string, tips?: string) {
super(message || String(code));
this.name = 'CustomError';
if (typeof code === 'number') {
this.code = code;
this.message = message!;
} else {
this.code = 500;
this.message = code!;
constructor(code?: number | string | CustomErrorOptions, opts?: CustomErrorOptions) {
if (typeof code === 'object' && code !== null) {
opts = code;
code = opts.code || 500;
}
this.tips = tips;
let message = opts?.message || String(code);
const cause = opts?.cause;
super(message, { cause });
this.name = 'RouterError';
let codeNum = opts?.code || (typeof code === 'number' ? code : undefined);
this.code = codeNum ?? 500;
this.message = message!;
// 这一步可不写,默认会保存堆栈追踪信息到自定义错误构造函数之前,
// 而如果写成 `Error.captureStackTrace(this)` 则自定义错误的构造函数也会被保存到堆栈追踪信息
Error.captureStackTrace(this, this.constructor);
@@ -31,8 +36,7 @@ export class CustomError extends Error {
return {
code: e?.code,
data: e?.data,
message: e?.message,
tips: e?.tips,
message: e?.message
};
}
/**
@@ -43,6 +47,22 @@ export class CustomError extends Error {
static isError(error: unknown): error is CustomError {
return error instanceof CustomError || (typeof error === 'object' && error !== null && 'code' in error);
}
static throw(code?: number | string, message?: string): void;
static throw(code?: number | string, opts?: CustomErrorOptions): void;
static throw(opts?: CustomErrorOptions): void;
static throw(...args: any[]) {
const [args0, args1] = args;
if (args0 && typeof args0 === 'object') {
throw new CustomError(args0);
}
if (args1 && typeof args1 === 'object') {
throw new CustomError(args0, args1);
} else if (args1) {
throw new CustomError(args0, { message: args1 });
}
// args1 不存在;
throw new CustomError(args0);
}
parse(e?: CustomError) {
if (e) {
return CustomError.parseError(e);
@@ -52,12 +72,17 @@ export class CustomError extends Error {
code: e?.code,
data: e?.data,
message: e?.message,
tips: e?.tips,
};
}
}
}
export interface throwError {
throw(code?: number | string, message?: string): void;
throw(code?: number | string, opts?: CustomErrorOptions): void;
throw(opts?: CustomErrorOptions): void;
}
/*
try {
//

View File

@@ -1,4 +1,4 @@
import { CustomError } from './result/error.ts';
import { CustomError, throwError, CustomErrorOptions } from './result/error.ts';
import { pick } from './utils/pick.ts';
import { listenProcess, MockProcess } from './utils/listen-process.ts';
import { z } from 'zod';
@@ -6,7 +6,16 @@ import { randomId } from './utils/random.ts';
import * as schema from './validator/schema.ts';
export type RouterContextT = { code?: number;[key: string]: any };
export type RouteContext<T = { code?: number }, S = any> = {
type BuildRouteContext<M, U> = M extends { args?: infer A }
? A extends z.ZodObject<any>
? RouteContext<{ args?: z.infer<A> }, U>
: A extends Record<string, z.ZodTypeAny>
? RouteContext<{ args?: { [K in keyof A]: z.infer<A[K]> } }, U>
: RouteContext<U>
: RouteContext<U>;
export type RouteContext<T = { code?: number }, U extends SimpleObject = {}, S = { [key: string]: any }> = {
/**
* 本地自己调用的时候使用,可以标识为当前自调用,那么 auth 就不许重复的校验
* 或者不需要登录的,直接调用
@@ -23,9 +32,15 @@ export type RouteContext<T = { code?: number }, S = any> = {
code?: number;
/** return msg */
message?: string;
// 传递状态
/**
* 传递状态
*/
state?: S;
// transfer data
/**
* 当前routerId
*/
currentId?: string;
/**
* 当前路径
*/
@@ -54,19 +69,19 @@ export type RouteContext<T = { code?: number }, S = any> = {
ctx?: RouteContext & { [key: string]: any },
) => Promise<any>;
/** 请求 route的返回结果解析了body为data就类同于 query.post获取的数据*/
run?: (message: { path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) => Promise<any>;
run?: (message: { path: string; key?: string; payload?: any }, ctx?: RouteContext) => Promise<any>;
index?: number;
throw?: (code?: number | string, message?: string, tips?: string) => void;
throw?: throwError['throw'];
/** 是否需要序列化, 使用JSON.stringify和JSON.parse */
needSerialize?: boolean;
} & T;
} & T & U;
export type SimpleObject = Record<string, any>;
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 NextRoute = Pick<Route, 'id' | 'path' | 'key'>;
export type RouteMiddleware =
| {
path: string;
path?: string;
key?: string;
id?: string;
}
@@ -80,7 +95,7 @@ export type RouteOpts<U = {}, T = SimpleObject> = {
description?: string;
metadata?: T;
middleware?: RouteMiddleware[]; // middleware
type?: 'route' | 'middleware';
type?: 'route' | 'middleware' | 'compound'; // compound表示这个 route 作为一个聚合体,没有实际的 run而是一个 router 的聚合列表
/**
* $#$ will be used to split path and key
*/
@@ -123,7 +138,11 @@ export const createSkill = <T = SimpleObject>(skill: Skill<T>): Skill<T> => {
export type RouteInfo = Pick<Route, (typeof pickValue)[number]>;
export class Route<U = { [key: string]: any }, T extends SimpleObject = SimpleObject> {
/**
* @M 是 route的 metadate的类型默认是 SimpleObject
* @U 是 RouteContext 里 state的类型
*/
export class Route<M extends SimpleObject = SimpleObject, U extends SimpleObject = SimpleObject> implements throwError {
/**
* 一级路径
*/
@@ -133,10 +152,10 @@ export class Route<U = { [key: string]: any }, T extends SimpleObject = SimpleOb
*/
key?: string;
id?: string;
run?: Run;
run?: Run<BuildRouteContext<M, U>>;
nextRoute?: NextRoute; // route to run after this route
description?: string;
metadata?: T;
metadata?: M;
middleware?: RouteMiddleware[]; // middleware
type? = 'route';
/**
@@ -154,13 +173,13 @@ export class Route<U = { [key: string]: any }, T extends SimpleObject = SimpleOb
if (opts) {
this.id = opts.id || randomId(12, 'rand-');
if (!opts.id && opts.idUsePath) {
const delimiter = opts.delimiter ?? '$#$';
const delimiter = opts.delimiter ?? '$$';
this.id = path + delimiter + key;
}
this.run = opts.run;
this.run = opts.run as Run<BuildRouteContext<M, U>>;
this.nextRoute = opts.nextRoute;
this.description = opts.description;
this.metadata = opts.metadata as T;
this.metadata = opts.metadata as M;
this.type = opts.type || 'route';
this.middleware = opts.middleware || [];
this.key = opts.key || key;
@@ -184,9 +203,9 @@ export class Route<U = { [key: string]: any }, T extends SimpleObject = SimpleOb
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;
define<T extends { [key: string]: any } = RouterContextT>(path: string, key: string, fn: Run<T & U>): this;
define<T extends { [key: string]: any } = RouterContextT>(fn: Run<T & BuildRouteContext<M, U>>): this;
define<T extends { [key: string]: any } = RouterContextT>(key: string, fn: Run<T & BuildRouteContext<M, U>>): this;
define<T extends { [key: string]: any } = RouterContextT>(path: string, key: string, fn: Run<T & BuildRouteContext<M, U>>): this;
define(...args: any[]) {
const [path, key, opts] = args;
// 全覆盖所以opts需要准确不能由idUsePath 需要check的变量
@@ -209,7 +228,7 @@ export class Route<U = { [key: string]: any }, T extends SimpleObject = SimpleOb
return this;
}
if (typeof path === 'function') {
this.run = path;
this.run = path as Run<BuildRouteContext<M, U>>;
return this;
}
if (typeof path === 'string' && typeof key === 'function') {
@@ -242,9 +261,8 @@ export class Route<U = { [key: string]: any }, T extends SimpleObject = SimpleOb
addTo(router: QueryRouter | { add: (route: Route) => void;[key: string]: any }, opts?: AddOpts) {
router.add(this, opts);
}
throw(code?: number | string, message?: string, tips?: string): void;
throw(...args: any[]) {
throw new CustomError(...args);
CustomError.throw(...args);
}
}
@@ -263,11 +281,11 @@ export const fromJSONSchema = schema.fromJSONSchema;
* @parmas overwrite 是否覆盖已存在的route默认true
*/
export type AddOpts = { overwrite?: boolean };
export class QueryRouter {
export class QueryRouter<T extends SimpleObject = SimpleObject> implements throwError {
appId: string = '';
routes: Route[];
maxNextRoute = 40;
context?: RouteContext = {}; // default context for call
context?: RouteContext<T> = {} as RouteContext<T>; // default context for call
constructor() {
this.routes = [];
}
@@ -310,11 +328,12 @@ export class QueryRouter {
* @param ctx
* @returns
*/
async runRoute(path: string, key: string, ctx?: RouteContext) {
async runRoute(path: string, key: string, ctx?: RouteContext<T>): Promise<RouteContext<T>> {
const route = this.routes.find((r) => r.path === path && r.key === key);
const maxNextRoute = this.maxNextRoute;
ctx = (ctx || {}) as RouteContext;
ctx = (ctx || {}) as RouteContext<T>;
ctx.currentPath = path;
ctx.currentId = route?.id;
ctx.currentKey = key;
ctx.currentRoute = route;
ctx.index = (ctx.index || 0) + 1;
@@ -328,7 +347,7 @@ export class QueryRouter {
ctx.code = 500;
ctx.message = 'Too many nextRoute';
ctx.body = null;
return;
return ctx;
}
// run middleware
if (route && route.middleware && route.middleware.length > 0) {
@@ -383,7 +402,7 @@ export class QueryRouter {
const middleware = routeMiddleware[i];
if (middleware) {
try {
await middleware.run(ctx as Required<RouteContext>);
await middleware.run(ctx as Required<RouteContext<T>>);
} catch (e) {
if (route?.isDebug) {
console.error('=====debug====:middlerware error');
@@ -405,6 +424,7 @@ export class QueryRouter {
return ctx;
}
if (ctx.end) {
return ctx;
}
}
}
@@ -413,7 +433,7 @@ export class QueryRouter {
if (route) {
if (route.run) {
try {
await route.run(ctx as Required<RouteContext>);
await route.run(ctx as Required<RouteContext<T>>);
} catch (e) {
if (route?.isDebug) {
console.error('=====debug====:route error');
@@ -468,7 +488,7 @@ export class QueryRouter {
}
}
// 如果没有找到route返回404这是因为出现了错误
return Promise.resolve({ code: 404, body: 'Not found' });
return Promise.resolve({ code: 404, body: 'Not found' } as RouteContext<T>);
}
/**
* 第一次执行
@@ -476,12 +496,12 @@ export class QueryRouter {
* @param ctx
* @returns
*/
async parse(message: { path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
async parse(message: { path: string; key?: string; payload?: any }, ctx?: RouteContext<T> & { [key: string]: any }) {
if (!message?.path) {
return Promise.resolve({ code: 404, body: null, message: 'Not found path' });
return Promise.resolve({ code: 404, body: null, message: 'Not found path' } as RouteContext<T>);
}
const { path, key = '', payload = {}, ...query } = message;
ctx = ctx || {};
ctx = ctx || {} as RouteContext<T>;
ctx.query = { ...ctx.query, ...query, ...payload };
ctx.args = ctx.query;
ctx.state = { ...ctx?.state };
@@ -515,7 +535,7 @@ export class QueryRouter {
* @param ctx
* @returns
*/
async call(message: { id?: string; path?: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
async call(message: { id?: string; path?: string; key?: string; payload?: any }, ctx?: RouteContext<T> & { [key: string]: any }) {
let path = message.path;
let key = message.key;
// 优先 path + key
@@ -556,7 +576,7 @@ export class QueryRouter {
* @param ctx
* @returns
*/
async run(message: { id?: string; path?: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
async run(message: { id?: string; path?: string; key?: string; payload?: any }, ctx?: RouteContext<T> & { [key: string]: any }) {
const res = await this.call(message, { ...this.context, ...ctx });
return {
code: res.code,
@@ -570,7 +590,7 @@ export class QueryRouter {
* @param ctx
*/
setContext(ctx: RouteContext) {
this.context = ctx;
this.context = ctx as RouteContext<T>;
}
getList(filter?: (route: Route) => boolean): RouteInfo[] {
return this.routes.filter(filter || (() => true)).map((r) => {
@@ -581,11 +601,11 @@ export class QueryRouter {
/**
* 获取handle函数, 这里会去执行parse函数
*/
getHandle<T = any>(router: QueryRouter, wrapperFn?: HandleFn<T>, ctx?: RouteContext) {
return async (msg: { id?: string; path?: string; key?: string;[key: string]: any }, handleContext?: RouteContext) => {
getHandle<T = any>(router: QueryRouter, wrapperFn?: HandleFn, ctx?: RouteContext) {
return async (msg: { id?: string; path?: string; key?: string;[key: string]: any }, handleContext?: RouteContext<T>) => {
try {
const context = { ...ctx, ...handleContext };
const res = await router.call(msg, context);
const res = await router.call(msg, context) as any;
if (wrapperFn) {
res.data = res.body;
return wrapperFn(res, context);
@@ -610,9 +630,8 @@ export class QueryRouter {
importRouter(router: QueryRouter) {
this.importRoutes(router.routes);
}
throw(code?: number | string, message?: string, tips?: string): void;
throw(...args: any[]) {
throw new CustomError(...args);
CustomError.throw(...args);
}
hasRoute(path: string, key: string = '') {
return this.routes.find((r) => r.path === path && r.key === key);
@@ -639,7 +658,7 @@ export class QueryRouter {
description: '列出当前应用下的所有的路由信息',
middleware: opts?.middleware || [],
run: async (ctx: RouteContext) => {
const tokenUser = ctx.state.tokenUser;
const tokenUser = ctx.state as unknown as { tokenUser?: any };
let isUser = !!tokenUser;
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') {
@@ -685,10 +704,11 @@ export class QueryRouter {
fromJSONSchema = fromJSONSchema;
}
type QueryRouterServerOpts = {
type QueryRouterServerOpts<C extends SimpleObject = SimpleObject> = {
handleFn?: HandleFn;
context?: RouteContext;
context?: RouteContext<C>;
appId?: string;
initHandle?: boolean;
};
interface HandleFn<T = any> {
(msg: { path: string;[key: string]: any }, ctx?: any): { code: string; data?: any; message?: string;[key: string]: any };
@@ -697,13 +717,18 @@ interface HandleFn<T = any> {
/**
* QueryRouterServer
* @description 移除server相关的功能只保留router相关的功能和http.createServer不相关独立
* @template C 自定义 RouteContext 类型
*/
export class QueryRouterServer extends QueryRouter {
export class QueryRouterServer<C extends SimpleObject = SimpleObject> extends QueryRouter<C> {
declare appId: string;
handle: any;
constructor(opts?: QueryRouterServerOpts) {
declare context: RouteContext<C>;
constructor(opts?: QueryRouterServerOpts<C>) {
super();
const initHandle = opts?.initHandle ?? true;
if (initHandle || opts?.handleFn) {
this.handle = this.getHandle(this, opts?.handleFn, opts?.context);
}
this.setContext({ needSerialize: false, ...opts?.context });
if (opts?.appId) {
this.appId = opts.appId;
@@ -718,37 +743,28 @@ export class QueryRouterServer extends QueryRouter {
this.add(route, opts);
}
Route = Route;
route(opts: RouteOpts): Route<Required<RouteContext>>;
route(path: string, key?: string): Route<Required<RouteContext>>;
route(path: string, opts?: RouteOpts): Route<Required<RouteContext>>;
route(path: string, key?: string, opts?: RouteOpts): Route<Required<RouteContext>>;
route(...args: any[]) {
route<M extends SimpleObject = SimpleObject>(opts: RouteOpts & { metadata?: M }): Route<M, Required<RouteContext<C>>>;
route<M extends SimpleObject = SimpleObject>(path: string, opts?: RouteOpts & { metadata?: M }): Route<M, Required<RouteContext<C>>>;
route<M extends SimpleObject = SimpleObject>(path: string, key?: string): Route<M, Required<RouteContext<C>>>;
route<M extends SimpleObject = SimpleObject>(path: string, key?: string, opts?: RouteOpts & { metadata?: M }): Route<M, Required<RouteContext<C>>>;
route<M extends SimpleObject = SimpleObject>(...args: any[]) {
const [path, key, opts] = args;
if (typeof path === 'object') {
return new Route(path.path, path.key, path);
return new Route<M, Required<RouteContext<C>>>(path.path, path.key, path);
}
if (typeof path === 'string') {
if (opts) {
return new Route(path, key, opts);
return new Route<M, Required<RouteContext<C>>>(path, key, opts);
}
if (key && typeof key === 'object') {
return new Route(path, key?.key || '', key);
return new Route<M, Required<RouteContext<C>>>(path, key?.key || '', key);
}
return new Route(path, key);
return new Route<M, Required<RouteContext<C>>>(path, key);
}
return new Route(path, key, opts);
return new Route<M, Required<RouteContext<C>>>(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 });
prompt(description: string) {
return new Route(undefined, undefined, { description });
}
/**
@@ -756,15 +772,51 @@ export class QueryRouterServer extends QueryRouter {
* @param param0
* @returns
*/
async run(msg: { id?: string; path?: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
async run(msg: { id?: string; path?: string; key?: string; payload?: any }, ctx?: Partial<RouteContext<C>>) {
const handle = this.handle;
if (handle) {
return handle(msg, ctx);
}
return super.run(msg, ctx);
return super.run(msg, ctx as RouteContext<C>);
}
async runAction<T extends { id?: string; path?: string; key?: string; metadata?: { args?: any } } = {}>(
api: T,
payload: RunActionPayload<T>,
ctx?: RouteContext<C>
) {
const { path, key, id } = api as any;
return this.run({ path, key, id, payload }, ctx);
}
}
export class Mini extends QueryRouterServer { }
/** JSON Schema 基本类型映射到 TypeScript 类型 */
type JsonSchemaTypeToTS<T> =
T extends { type: "string" } ? string :
T extends { type: "boolean" } ? boolean :
T extends { type: "number" } ? number :
T extends { type: "integer" } ? number :
T extends { type: "object" } ? object :
T extends { type: "array" } ? any[] :
any;
/** 将 args shapekey -> JSON Schema 类型)转换为 payload 类型,支持 optional: true 的字段为可选 */
type ArgsShapeToPayload<T> =
{ [K in keyof T as T[K] extends { optional: true } ? never : K]: JsonSchemaTypeToTS<T[K]> } &
{ [K in keyof T as T[K] extends { optional: true } ? K : never]?: JsonSchemaTypeToTS<T[K]> };
/** 处理两种 args 格式:完整 JSON Schema含 properties或简单 key->type 映射 */
type ArgsToPayload<T> =
T extends { type: "object"; properties: infer P }
? ArgsShapeToPayload<P>
: ArgsShapeToPayload<T>;
/** 从 API 定义中提取 metadata.args */
type ExtractArgs<T> =
T extends { metadata: { args: infer A } } ? A : {};
/** runAction 第二个参数的类型,根据第一个参数的 metadata.args 推断 */
export type RunActionPayload<T> = ArgsToPayload<ExtractArgs<T>>;

View File

@@ -4,6 +4,7 @@ import * as cookie from './cookie.ts';
import { ServerType, Listener, OnListener, ServerOpts, OnWebSocketOptions, OnWebSocketFn, WebSocketListenerFun, ListenerFun, HttpListenerFun, WS } from './server-type.ts';
import { parseIfJson } from '../utils/parse.ts';
import { EventEmitter } from 'eventemitter3';
import { CustomError } from '../result/error.ts';
type CookieFn = (name: string, value: string, options?: cookie.SerializeOptions, end?: boolean) => void;
export type HandleCtx = {
@@ -165,8 +166,13 @@ export class ServerBase implements ServerType {
res.end(JSON.stringify(end));
}
} catch (e) {
console.error(e);
res.setHeader('Content-Type', 'application/json; charset=utf-8');
if (CustomError.isError(e)) {
const parsedError = CustomError.parseError(e);
res.end(JSON.stringify(parsedError));
return;
}
console.error(e);
if (e.code && typeof e.code === 'number') {
res.end(resultError(e.message || `Router Server error`, e.code));
} else {
@@ -278,7 +284,7 @@ export class ServerBase implements ServerType {
* @param ws
*/
async onWsClose(ws: WS) {
const id = ws?.data?.id || '';
const id = ws?.wsId || '';
if (id) {
this.emitter.emit('close--' + id, { type: 'close', ws, id });
setTimeout(() => {
@@ -292,4 +298,7 @@ export class ServerBase implements ServerType {
if (this.showConnected)
ws.send(JSON.stringify({ type: 'connected' }));
}
createId() {
return Math.random().toString(36).substring(2, 15);
}
}

View File

@@ -4,7 +4,7 @@
* @tags bun, server, websocket, http
* @createdAt 2025-12-20
*/
import { ServerType, type ServerOpts, type Cors, RouterRes, RouterReq } from './server-type.ts';
import { ServerType, type ServerOpts, type Cors, RouterRes, RouterReq, WS } from './server-type.ts';
import { ServerBase } from './server-base.ts';
export class BunServer extends ServerBase implements ServerType {
@@ -264,10 +264,14 @@ export class BunServer extends ServerBase implements ServerType {
open: (ws: any) => {
this.sendConnected(ws);
},
message: async (ws: any, message: string | Buffer) => {
message: async (bunWs: any, message: string | Buffer) => {
const ws = bunWs as WS;
const pathname = ws.data.pathname || '';
const token = ws.data.token || '';
const id = ws.data.id || '';
if (!ws.wsId) {
ws.wsId = this.createId();
}
await this.onWebSocket({ ws, message, pathname, token, id });
},
close: (ws: any) => {

View File

@@ -49,16 +49,33 @@ export type OnWebSocketOptions<T = {}> = {
message: string | Buffer;
pathname: string,
token?: string,
/** data 的id提取出来 */
id?: string,
}
export type OnWebSocketFn = (options: OnWebSocketOptions) => Promise<void> | void;
export type WS<T = {}> = {
send: (data: any) => void;
close: (code?: number, reason?: string) => void;
/**
* ws 自己生成的一个id主要是为了区分不同的ws连接方便在onWebSocket中使用
*/
wsId?: string;
data?: {
/**
* ws连接时的url包含pathname和searchParams
*/
url: URL;
/**
* ws连接时的pathname
*/
pathname: string;
/**
* ws连接时的url中的token参数
*/
token?: string;
/**
* ws连接时的url中的id参数.
*/
id?: string;
/**
* 鉴权后的获取的信息

View File

@@ -56,6 +56,11 @@ export class WsServerBase {
token,
id,
}
// @ts-ignore
if (!ws.wsId) {
// @ts-ignore
ws.wsId = this.createId();
}
ws.on('message', async (message: string | Buffer) => {
await this.server.onWebSocket({ ws, message, pathname, token, id });
});
@@ -66,7 +71,9 @@ export class WsServerBase {
this.server.onWsClose(ws);
});
});
}
createId() {
return Math.random().toString(36).substring(2, 15);
}
}
// TODO: ws handle and path and routerContext

View File

@@ -1,13 +1,69 @@
import { App } from '../app.ts'
const app = new App<{ f: string }>();
import { App, AppRouteContext } from "@/app.ts";
import { QueryRouterServer, RouteContext } from "@/app.ts";
import z from "zod";
const route: RouteContext<{ customField: string }> = {} as any;
route.customField
const appRoute: AppRouteContext<{ customField: string }> = {} as any;
appRoute.customField
// 示例 1: 使用 App它会自动使用 AppRouteContext<U> 作为 ctx 类型
const app = new App<{
customField: string;
}>();
app.context.customField = "customValue"; // 可以在 app.context 中添加自定义字段,这些字段会在 ctx 中可用
app.route({
path: 't',
run: async (ctx) => {
// ctx.r
ctx.app;
path: 'test1',
metadata: {
args: {
name: z.string(),
}
},
}).define(async (ctx) => {
ctx.f = 'hello';
}).addTo(app);
// ctx.app 是 App 类型
const appName = ctx.app.appId;
// ctx.customField 来自自定义泛型参数
const customField: string | undefined = ctx.customField;
// ctx.req 和 ctx.res 来自 HandleCtx
const req = ctx.req;
const res = ctx.res;
// ctx.args 从 metadata.args 推断
const name: string = ctx.args.name;
const name2: string = ctx.query.name;
ctx.body = `Hello ${name}!`;
});
// 示例 2: 使用 QueryRouterServer它可以传递自定义的 Context 类型
const router = new QueryRouterServer<{
routerContextField: number;
}>();
router.context.routerContextField
router.route({
path: 'router-test',
metadata: {
args: {
value: z.number(),
}
},
}).define(async (ctx) => {
const value: number = ctx.args.value;
const field: number | undefined = ctx.routerContextField;
ctx.body = value;
});
// 示例 3: 不带泛型参数的 QueryRouterServer使用默认的 RouteContext
const defaultRouter = new QueryRouterServer();
defaultRouter.route({
path: 'default-test',
metadata: {
args: {
id: z.string(),
}
},
}).define(async (ctx) => {
const id: string = ctx.args.id;
ctx.body = id;
});
export { app, router, defaultRouter };

View File

@@ -14,4 +14,4 @@ app.prompt('获取天气的工具。\n参数是 city 为对应的城市').define
export const chat = new RouterChat({ router: app.router });
console.log(chat.chat());
console.log(chat.getChatPrompt());

15
src/test/route-ts.ts Normal file
View File

@@ -0,0 +1,15 @@
import { QueryRouterServer } from "@/route.ts";
import z from "zod";
const router = new QueryRouterServer()
router.route({
metadata: {
args: {
a: z.string(),
}
},
}).define(async (ctx) => {
const argA: string = ctx.args.a;
ctx.body = '1';
})

87
src/test/run-schema.ts Normal file
View File

@@ -0,0 +1,87 @@
import z from "zod";
import { App } from "../index.ts";
const app = new App();
const api = {
"app_domain_manager": {
/**
* 获取域名信息可以通过id或者domain进行查询
*
* @param data - Request parameters
* @param data.data - {object}
*/
"get": {
"path": "app_domain_manager",
"key": "get",
"description": "获取域名信息可以通过id或者domain进行查询",
"metadata": {
"args": {
"data": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": {
"type": "string"
},
"domain": {
"type": "string"
}
},
"additionalProperties": false,
"required": ["id",]
}
},
"viewItem": {
"api": {
"url": "/api/router"
},
"type": "api",
"title": "路由"
},
"url": "/api/router",
"source": "query-proxy-api"
}
},
"delete": {
"path": "app_domain_manager",
"key": "delete",
"description": "删除域名",
"metadata": {
"args": {
"domainId": {
"type": "string",
"optional": true
}
}
}
}
},
"user_manager": {
"getUser": {
"path": "user_manager",
"key": "getUser",
"description": "获取用户信息",
"metadata": {
"args": {
"userId": {
"type": "string"
},
"includeProfile": {
"type": "boolean"
}
}
}
}
}
} as const;
type API = typeof api;
// 类型推断生效payload 根据 metadata.args 自动推断
// get 的 args.data 是 type:"object",所以 payload 需要 { data: object }
app.runAction(api.app_domain_manager.get, { data: { id: "1" } })
// delete 的 args 是 { domainId: { type: "string" } },所以 payload 需要 { domainId: string }
app.runAction(api.app_domain_manager.delete, { domainId: "d1" })
// getUser 的 args 是 { userId: string, includeProfile: boolean }
app.runAction(api.user_manager.getUser, { userId: "u1", includeProfile: true })

View File

@@ -0,0 +1,452 @@
# HTTP Server 设计文档
## 概述
HTTP 服务器层负责接收外部 HTTP 请求,将其归一化后传递给 Router 层处理。所有请求统一入口为 `/api/router`
## 请求入口
```
POST /api/router?path=demo&key=01
GET /api/router?path=demo&key=01
```
| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| path | /api/router | 请求入口路径 |
## 请求参数归一化
HTTP 请求的参数来源于两个部分,最终合并为一个 Message 对象:
### 1. URL Query (searchParams)
```typescript
// GET /api/router?path=demo&key=01&token=xxx
{
path: "demo",
key: "01",
token: "xxx"
}
```
### 2. POST Body
```typescript
// POST /api/router
// Body: { "name": "test", "value": 123 }
{
name: "test",
value: 123
}
```
### 3. 合并规则
最终的 Message = Query + Body后者覆盖前者
```typescript
// GET /api/router?path=demo&key=01
// POST Body: { "key": "02", "extra": "data" }
{
path: "demo",
key: "02", // body 覆盖 query
extra: "data"
}
```
### 4. payload 参数
如果 query 或 body 中有 `payload` 字段且为 JSON 字符串,会自动解析为对象:
```typescript
// GET /api/router?path=demo&key=01&payload={"id":123}
{
path: "demo",
key: "01",
payload: { id: 123 } // 自动解析
}
```
### 5. 认证信息
| 来源 | 字段名 | 说明 |
|------|--------|------|
| Authorization header | token | Bearer token |
| Cookie | token | cookie 中的 token |
| Cookie | cookies | 完整 cookie 对象 |
## 路由匹配
### 方式一path + key
```bash
# 访问 path=demo, key=01 的路由
POST /api/router?path=demo&key=01
```
### 方式二id
```bash
# 直接通过路由 ID 访问
POST /api/router?id=abc123
```
## 内置路由
所有内置路由使用统一的访问方式:
| 路由 | 访问方式 | 说明 |
|------|----------|------|
| router.list | POST /api/router?path=router&key=list | 获取路由列表 |
### router.list
获取当前应用所有路由列表。
**请求:**
```bash
POST /api/router?path=router&key=list
```
**响应:**
```json
{
"code": 200,
"data": {
"list": [
{
"id": "router$#$list",
"path": "router",
"key": "list",
"description": "列出当前应用下的所有的路由信息",
"middleware": [],
"metadata": {}
}
],
"isUser": false
},
"message": "success"
}
```
## Go 设计
```go
package server
import (
"encoding/json"
"net/http"
)
// Message HTTP 请求归一化后的消息
type Message struct {
Path string // 路由 path
Key string // 路由 key
ID string // 路由 ID (优先于 path+key)
Token string // 认证 token
Payload map[string]interface{} // payload 参数
Query url.Values // 原始 query
Cookies map[string]string // cookie
// 其他参数
Extra map[string]interface{}
}
// HandleServer 解析 HTTP 请求
func HandleServer(req *http.Request) (*Message, error) {
method := req.Method
if method != "GET" && method != "POST" {
return nil, fmt.Errorf("method not allowed")
}
// 解析 query
query := req.URL.Query()
// 获取 token
token := req.Header.Get("Authorization")
if token != "" {
token = strings.TrimPrefix(token, "Bearer ")
}
if token == "" {
if cookie, err := req.Cookie("token"); err == nil {
token = cookie.Value
}
}
// 解析 body (POST)
var body map[string]interface{}
if method == "POST" {
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
body = make(map[string]interface{})
}
}
// 合并 query 和 body
msg := &Message{
Token: token,
Query: query,
Cookies: parseCookies(req.Cookies()),
}
// query 参数
if v := query.Get("path"); v != "" {
msg.Path = v
}
if v := query.Get("key"); v != "" {
msg.Key = v
}
if v := query.Get("id"); v != "" {
msg.ID = v
}
if v := query.Get("payload"); v != "" {
if err := json.Unmarshal([]byte(v), &msg.Payload); err == nil {
// payload 解析成功
}
}
// body 参数覆盖 query
for k, v := range body {
msg.Extra[k] = v
switch k {
case "path":
msg.Path = v.(string)
case "key":
msg.Key = v.(string)
case "id":
msg.ID = v.(string)
case "payload":
msg.Payload = v.(map[string]interface{})
}
}
return msg, nil
}
// RouterHandler 创建 HTTP 处理函数
func RouterHandler(router *QueryRouter) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
// CORS
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST")
w.Header().Set("Content-Type", "application/json")
if req.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
msg, err := HandleServer(req)
if err != nil {
json.NewEncoder(w).Encode(Result{Code: 400, Message: err.Error()})
return
}
// 调用 router.run
result, err := router.Run(*msg, nil)
if err != nil {
json.NewEncoder(w).Encode(Result{Code: 500, Message: err.Error()})
return
}
json.NewEncoder(w).Encode(result)
}
}
// Server HTTP 服务器
type Server struct {
Router *QueryRouter
Path string
Handlers []http.HandlerFunc
}
func (s *Server) Listen(addr string) error {
mux := http.NewServeMux()
// 自定义处理器
for _, h := range s.Handlers {
mux.HandleFunc(s.Path, h)
}
// 路由处理器
mux.HandleFunc(s.Path, RouterHandler(s.Router))
return http.ListenAndServe(addr, mux)
}
```
## Rust 设计
```rust
use actix_web::{web, App, HttpServer, HttpRequest, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
// Message HTTP 请求归一化后的消息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub path: Option<String>,
pub key: Option<String>,
pub id: Option<String>,
pub token: Option<String>,
pub payload: Option<HashMap<String, Value>>,
#[serde(flatten)]
pub extra: HashMap<String, Value>,
}
// Result 调用结果
#[derive(Debug, Serialize)]
pub struct Result {
pub code: i32,
pub data: Option<Value>,
pub message: Option<String>,
}
// 解析 HTTP 请求
pub async fn handle_server(req: HttpRequest, body: web::Bytes) -> Result<Message, String> {
let method = req.method().as_str();
if method != "GET" && method != "POST" {
return Err("method not allowed".to_string());
}
// 获取 query 参数
let query: web::Query<HashMap<String, String>> = web::Query::clone(&req);
// 获取 token
let mut token = None;
if let Some(auth) = req.headers().get("authorization") {
if let Ok(s) = auth.to_str() {
token = Some(s.trim_start_matches("Bearer ").to_string());
}
}
if token.is_none() {
if let Some(cookie) = req.cookie("token") {
token = Some(cookie.value().to_string());
}
}
// 解析 body (POST)
let mut body_map = HashMap::new();
if method == "POST" {
if let Ok(v) = serde_json::from_slice::<Value>(&body) {
if let Some(obj) = v.as_object() {
for (k, val) in obj {
body_map.insert(k.clone(), val.clone());
}
}
}
}
// 构建 Message
let mut msg = Message {
path: query.get("path").cloned(),
key: query.get("key").cloned(),
id: query.get("id").cloned(),
token,
payload: None,
extra: HashMap::new(),
};
// 处理 payload
if let Some(p) = query.get("payload") {
if let Ok(v) = serde_json::from_str::<Value>(p) {
msg.payload = v.as_object().map(|m| m.clone());
}
}
// body 覆盖 query
for (k, v) in body_map {
match k.as_str() {
"path" => msg.path = v.as_str().map(|s| s.to_string()),
"key" => msg.key = v.as_str().map(|s| s.to_string()),
"id" => msg.id = v.as_str().map(|s| s.to_string()),
"payload" => msg.payload = v.as_object().map(|m| m.clone()),
_ => msg.extra.insert(k, v),
}
}
Ok(msg)
}
// HTTP 处理函数
async fn router_handler(
req: HttpRequest,
body: web::Bytes,
router: web::Data<QueryRouter>,
) -> impl Responder {
let msg = match handle_server(req, body).await {
Ok(m) => m,
Err(e) => return HttpResponse::BadRequest().json(Result {
code: 400,
data: None,
message: Some(e),
}),
};
// 调用 router.run
match router.run(msg, None).await {
Ok(result) => HttpResponse::Ok().json(result),
Err(e) => HttpResponse::InternalServerError().json(Result {
code: 500,
data: None,
message: Some(e.to_string()),
}),
}
}
// Server HTTP 服务器
pub struct Server {
pub router: QueryRouter,
pub path: String,
}
impl Server {
pub async fn listen(self, addr: &str) -> std::io::Result<()> {
let router = web::Data::new(self.router);
HttpServer::new(move || {
App::new()
.app_data(router.clone())
.route(&self.path, web::post().to(router_handler))
.route(&self.path, web::get().to(router_handler))
})
.bind(addr)?
.run()
.await
}
}
```
## 请求示例
### 基础请求
```bash
# 访问 demo/01 路由
curl -X POST "http://localhost:4002/api/router?path=demo&key=01"
# 带 body
curl -X POST "http://localhost:4002/api/router?path=demo&key=01" \
-H "Content-Type: application/json" \
-d '{"name":"test","value":123}'
```
### 获取路由列表
```bash
curl -X POST "http://localhost:4002/api/router?path=router&key=list"
```
### 带认证
```bash
# 通过 Header
curl -X POST "http://localhost:4002/api/router?path=demo&key=01" \
-H "Authorization: Bearer your-token"
# 通过 Cookie
curl -X POST "http://localhost:4002/api/router?path=demo&key=01" \
-b "token=your-token"
```

378
system_design/router.md Normal file
View File

@@ -0,0 +1,378 @@
# Router 系统设计文档
## 概述
轻量级路由框架,支持链式路由、中间件模式、统一上下文。适用于构建 API 服务支持跨语言实现Go、Rust 等)。
## 核心组件
### Route
| 字段 | 类型 | 说明 |
|------|------|------|
| path | string | 一级路径 |
| key | string | 二级路径 |
| id | string | 唯一标识 |
| run | Handler | 业务处理函数 |
| nextRoute | NextRoute? | 下一个路由 |
| middleware | string[] | 中间件 ID 列表 |
| metadata | T | 元数据/参数 schema |
| type | string | 类型route / middleware |
| isDebug | bool | 是否开启调试 |
### NextRoute
| 字段 | 类型 | 说明 |
|------|------|------|
| id | string? | 路由 ID |
| path | string? | 一级路径 |
| key | string? | 二级路径 |
### RouteContext
| 字段 | 类型 | 说明 |
|------|------|------|
| appId | string? | 应用 ID |
| query | object | URL 参数和 payload 合并结果 |
| args | object | 同 query |
| body | any | 响应 body |
| code | number | 响应状态码 |
| message | string | 响应消息 |
| state | object | 状态传递 |
| currentId | string? | 当前路由 ID |
| currentPath | string? | 当前路径 |
| currentKey | string? | 当前 key |
| currentRoute | Route? | 当前路由对象 |
| progress | [string, string][] | 路由执行路径 |
| nextQuery | object | 传递给下一个路由的参数 |
| end | boolean | 是否提前结束 |
| app | QueryRouter? | 路由实例引用 |
| error | any | 错误信息 |
| call | function | 调用其他路由(返回完整上下文) |
| run | function | 调用其他路由(返回简化结果) |
| throw | function | 抛出错误 |
| needSerialize | boolean | 是否需要序列化 |
### QueryRouter
| 方法 | 说明 |
|------|------|
| add(route, opts?) | 添加路由 |
| remove(route) | 按 path/key 移除路由 |
| removeById(id) | 按 ID 移除路由 |
| runRoute(path, key, ctx) | 执行单个路由 |
| parse(message, ctx) | 入口解析,返回完整上下文 |
| call(message, ctx) | 调用路由,返回完整上下文 |
| run(message, ctx) | 调用路由,返回简化结果 {code, data, message} |
| getHandle() | 获取 HTTP 处理函数 |
| setContext(ctx) | 设置默认上下文 |
| getList(filter?) | 获取路由列表 |
| hasRoute(path, key) | 检查路由是否存在 |
| findRoute(opts) | 查找路由 |
| exportRoutes() | 导出所有路由 |
| importRoutes(routes) | 批量导入路由 |
| createRouteList(opts) | 创建内置的路由列表功能 |
### QueryRouterServer
继承 QueryRouter新增
| 字段 | 类型 | 说明 |
|------|------|------|
| appId | string | 应用 ID |
| handle | function | HTTP 处理函数 |
| 方法 | 说明 |
|------|------|
| setHandle(wrapperFn, ctx) | 设置处理函数 |
| route(path, key?, opts?) | 工厂方法创建路由 |
## Go 设计
```go
package router
// Route 路由单元
type Route struct {
Path string
Key string
ID string
Run func(ctx *RouteContext) (*RouteContext, error)
NextRoute *NextRoute
Middleware []string
Metadata map[string]interface{}
Type string
IsDebug bool
}
// NextRoute 下一个路由
type NextRoute struct {
ID string
Path string
Key string
}
// RouteContext 请求上下文
type RouteContext struct {
AppID string
Query map[string]interface{}
Args map[string]interface{}
Body interface{}
Code int
Message string
State map[string]interface{}
CurrentID string
CurrentPath string
CurrentKey string
CurrentRoute *Route
Progress [][2]string
NextQuery map[string]interface{}
End bool
App *QueryRouter
Error error
NeedSerialize bool
// Methods
Call func(msg interface{}, ctx *RouteContext) (*RouteContext, error)
Run func(msg interface{}, ctx *RouteContext) (interface{}, error)
Throw func(err interface{})
}
// Message 调用消息
type Message struct {
ID string
Path string
Key string
Payload map[string]interface{}
}
// Result 调用结果
type Result struct {
Code int
Data interface{}
Message string
}
// AddOpts 添加选项
type AddOpts struct {
Overwrite bool
}
// QueryRouter 路由管理器
type QueryRouter struct {
Routes []*Route
MaxNextRoute int
Context *RouteContext
}
func NewQueryRouter() *QueryRouter
func (r *QueryRouter) Add(route *Route, opts *AddOpts)
func (r *QueryRouter) Remove(path, key string)
func (r *QueryRouter) RemoveByID(id string)
func (r *QueryRouter) RunRoute(path, key string, ctx *RouteContext) (*RouteContext, error)
func (r *QueryRouter) Parse(msg Message, ctx *RouteContext) (*RouteContext, error)
func (r *QueryRouter) Call(msg Message, ctx *RouteContext) (*RouteContext, error)
func (r *QueryRouter) Run(msg Message, ctx *RouteContext) (Result, error)
func (r *QueryRouter) GetHandle() func(msg interface{}) Result
func (r *QueryRouter) SetContext(ctx *RouteContext)
func (r *QueryRouter) GetList() []Route
func (r *QueryRouter) HasRoute(path, key string) bool
func (r *QueryRouter) FindRoute(opts FindOpts) *Route
// QueryRouterServer 服务端
type QueryRouterServer struct {
QueryRouter
AppID string
Handle func(msg interface{}) Result
}
type ServerOpts struct {
HandleFn func(msg interface{}, ctx interface{}) Result
Context *RouteContext
AppID string
}
func NewQueryRouterServer(opts *ServerOpts) *QueryRouterServer
func (s *QueryRouterServer) SetHandle(wrapperFn func(msg interface{}, ctx interface{}) Result, ctx *RouteContext)
func (s *QueryRouterServer) Route(path string, key ...string) *Route
```
## Rust 设计
```rust
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
// Route 路由单元
pub struct Route<M = Value> {
pub path: String,
pub key: String,
pub id: String,
pub run: Option<Box<dyn Fn(RouteContext) -> Pin<Box<dyn Future<Output = Result<RouteContext>>>> + Send>>,
pub next_route: Option<NextRoute>,
pub middleware: Vec<String>,
pub metadata: M,
pub route_type: String,
pub is_debug: bool,
}
// NextRoute 下一个路由
#[derive(Clone)]
pub struct NextRoute {
pub id: Option<String>,
pub path: Option<String>,
pub key: Option<String>,
}
// RouteContext 请求上下文
#[derive(Clone)]
pub struct RouteContext {
pub app_id: Option<String>,
pub query: HashMap<String, Value>,
pub args: HashMap<String, Value>,
pub body: Option<Value>,
pub code: Option<i32>,
pub message: Option<String>,
pub state: HashMap<String, Value>,
pub current_id: Option<String>,
pub current_path: Option<String>,
pub current_key: Option<String>,
pub current_route: Option<Box<Route>>,
pub progress: Vec<(String, String)>,
pub next_query: HashMap<String, Value>,
pub end: bool,
pub app: Option<Box<QueryRouter>>,
pub error: Option<Value>,
pub need_serialize: bool,
}
// Message 调用消息
#[derive(Clone)]
pub struct Message {
pub id: Option<String>,
pub path: Option<String>,
pub key: Option<String>,
pub payload: HashMap<String, Value>,
}
// Result 调用结果
pub struct Result {
pub code: i32,
pub data: Option<Value>,
pub message: Option<String>,
}
// AddOpts 添加选项
pub struct AddOpts {
pub overwrite: bool,
}
// FindOpts 查找选项
pub struct FindOpts {
pub path: Option<String>,
pub key: Option<String>,
pub id: Option<String>,
}
// QueryRouter 路由管理器
pub struct QueryRouter {
pub routes: Vec<Route>,
pub max_next_route: usize,
pub context: RouteContext,
}
impl QueryRouter {
pub fn new() -> Self
pub fn add(&mut self, route: Route, opts: Option<AddOpts>)
pub fn remove(&mut self, path: &str, key: &str)
pub fn remove_by_id(&mut self, id: &str)
pub async fn run_route(&self, path: &str, key: &str, ctx: RouteContext) -> Result<RouteContext>
pub async fn parse(&self, msg: Message, ctx: Option<RouteContext>) -> Result<RouteContext>
pub async fn call(&self, msg: Message, ctx: Option<RouteContext>) -> Result<RouteContext>
pub async fn run(&self, msg: Message, ctx: Option<RouteContext>) -> Result<Result>
pub fn get_handle(&self) -> impl Fn(Message) -> Result + '_
pub fn set_context(&mut self, ctx: RouteContext)
pub fn get_list(&self) -> Vec<Route>
pub fn has_route(&self, path: &str, key: &str) -> bool
pub fn find_route(&self, opts: FindOpts) -> Option<&Route>
}
// ServerOpts 服务端选项
pub struct ServerOpts {
pub handle_fn: Option<Box<dyn Fn(Message, Option<RouteContext>) -> Result + Send>>,
pub context: Option<RouteContext>,
pub app_id: Option<String>,
}
// QueryRouterServer 服务端
pub struct QueryRouterServer {
pub router: QueryRouter,
pub app_id: String,
pub handle: Option<Box<dyn Fn(Message) -> Result + Send>>,
}
impl QueryRouterServer {
pub fn new(opts: Option<ServerOpts>) -> Self
pub fn set_handle(&mut self, wrapperFn: Box<dyn Fn(Message) -> Result + Send>)
pub fn route(&self, path: &str, key: Option<&str>) -> Route
}
```
## 执行流程
```
Message → parse() → runRoute() → [middleware] → run() → [nextRoute] → ...
RouteContext (层层传递)
```
1. `parse()` 接收消息初始化上下文query、args、state
2. `runRoute()` 查找路由,先执行 middleware再执行 run
3. middleware 执行出错立即返回错误
4. 如有 nextRoute递归执行下一个路由最多 40 层)
5. 返回最终 RouteContext
## 特性说明
- **双层路径**: path + key 构成唯一路由
- **链式路由**: nextRoute 支持路由链式执行
- **中间件**: 每个 Route 可挂载多个 middleware
- **统一上下文**: RouteContext 贯穿整个请求生命周期
## 内置路由
框架内置以下路由,通过 HTTP 访问时使用 `path``key` 参数:
| 路由 path | 路由 key | 说明 |
|-----------|----------|------|
| router | list | 获取当前应用所有路由列表 |
### router/list
获取当前应用所有路由列表。
**访问方式:** `POST /api/router?path=router&key=list`
**响应:**
```json
{
"code": 200,
"data": {
"list": [
{
"id": "router$#$list",
"path": "router",
"key": "list",
"description": "列出当前应用下的所有的路由信息",
"middleware": [],
"metadata": {}
}
],
"isUser": false
},
"message": "success"
}
```

View File

@@ -0,0 +1,5 @@
# WebSocket Server 设计文档
## 概述
WebSocket 服务器支持实时双向通信,可与 HTTP 服务器共享同一端口。所有 WebSocket 连接统一入口为 `/api/router`,通过 `type` 参数区分业务类型。