@kevisual/router 0.0.55 → 0.0.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@kevisual/router",
4
- "version": "0.0.55",
4
+ "version": "0.0.57",
5
5
  "description": "",
6
6
  "type": "module",
7
7
  "main": "./dist/router.js",
@@ -21,10 +21,14 @@
21
21
  "keywords": [],
22
22
  "author": "abearxiong",
23
23
  "license": "MIT",
24
- "packageManager": "pnpm@10.28.0",
24
+ "packageManager": "pnpm@10.28.1",
25
25
  "devDependencies": {
26
+ "@kevisual/context": "^0.0.4",
27
+ "@kevisual/js-filter": "^0.0.5",
26
28
  "@kevisual/local-proxy": "^0.0.8",
27
29
  "@kevisual/query": "^0.0.35",
30
+ "@kevisual/use-config": "^1.0.28",
31
+ "@opencode-ai/plugin": "^1.1.26",
28
32
  "@rollup/plugin-alias": "^6.0.0",
29
33
  "@rollup/plugin-commonjs": "29.0.0",
30
34
  "@rollup/plugin-node-resolve": "^16.0.3",
@@ -34,10 +38,12 @@
34
38
  "@types/send": "^1.2.1",
35
39
  "@types/ws": "^8.18.1",
36
40
  "@types/xml2js": "^0.4.14",
37
- "eventemitter3": "^5.0.1",
41
+ "eventemitter3": "^5.0.4",
38
42
  "nanoid": "^5.1.6",
39
- "rollup": "^4.55.1",
43
+ "path-to-regexp": "^8.3.0",
44
+ "rollup": "^4.55.2",
40
45
  "rollup-plugin-dts": "^6.3.0",
46
+ "send": "^1.2.1",
41
47
  "ts-loader": "^9.5.4",
42
48
  "ts-node": "^10.9.2",
43
49
  "tslib": "^2.8.1",
@@ -45,16 +51,15 @@
45
51
  "typescript": "^5.9.3",
46
52
  "ws": "npm:@kevisual/ws",
47
53
  "xml2js": "^0.6.2",
48
- "zod": "^4.3.5",
49
- "@kevisual/js-filter": "^0.0.4",
50
- "path-to-regexp": "^8.3.0",
51
- "send": "^1.2.1"
54
+ "zod": "^4.3.5"
52
55
  },
53
56
  "repository": {
54
57
  "type": "git",
55
58
  "url": "git+https://github.com/abearxiong/kevisual-router.git"
56
59
  },
57
- "dependencies": {},
60
+ "dependencies": {
61
+ "hono": "^4.11.4"
62
+ },
58
63
  "publishConfig": {
59
64
  "access": "public"
60
65
  },
@@ -74,6 +79,7 @@
74
79
  "require": "./dist/router-simple.js",
75
80
  "types": "./dist/router-simple.d.ts"
76
81
  },
82
+ "./opencode": "./dist/opencode.js",
77
83
  "./define": {
78
84
  "import": "./dist/router-define.js",
79
85
  "require": "./dist/router-define.js",
package/readme.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # router
2
2
 
3
+ 一个轻量级的路由框架,支持链式调用、中间件、嵌套路由等功能。
4
+
5
+ ## 快速开始
6
+
3
7
  ```ts
4
8
  import { App } from '@kevisual/router';
5
9
 
@@ -7,30 +11,178 @@ const app = new App();
7
11
  app.listen(4002);
8
12
 
9
13
  app
10
- .route({path:'demo', key: '02})
14
+ .route({ path: 'demo', key: '02' })
11
15
  .define(async (ctx) => {
12
16
  ctx.body = '02';
13
17
  })
14
18
  .addTo(app);
15
19
 
16
20
  app
17
- .route('demo', '03')
21
+ .route({ path: 'demo', key: '03' })
18
22
  .define(async (ctx) => {
19
23
  ctx.body = '03';
20
24
  })
21
25
  .addTo(app);
22
26
  ```
23
- ## 兼容服务器
24
- ```
27
+
28
+ ## 核心概念
29
+
30
+ ### RouteContext 属性说明
31
+
32
+ 在 route handler 中,你可以通过 `ctx` 访问以下属性:
33
+
34
+ | 属性 | 类型 | 说明 |
35
+ |------|------|------|
36
+ | `query` | `object` | 请求参数,会自动合并 payload |
37
+ | `body` | `number \| string \| Object` | 响应内容 |
38
+ | `code` | `number` | 响应状态码,默认为 200 |
39
+ | `message` | `string` | 响应消息 |
40
+ | `state` | `any` | 状态数据,可在路由间传递 |
41
+ | `appId` | `string` | 应用标识 |
42
+ | `currentPath` | `string` | 当前路由路径 |
43
+ | `currentKey` | `string` | 当前路由 key |
44
+ | `currentRoute` | `Route` | 当前 Route 实例 |
45
+ | `progress` | `[string, string][]` | 路由执行路径记录 |
46
+ | `nextQuery` | `object` | 传递给下一个路由的参数 |
47
+ | `end` | `boolean` | 是否提前结束路由执行 |
48
+ | `app` | `QueryRouter` | 路由实例引用 |
49
+ | `error` | `any` | 错误信息 |
50
+ | `index` | `number` | 当前路由执行深度 |
51
+ | `needSerialize` | `boolean` | 是否需要序列化响应数据 |
52
+
53
+ ### 上下文方法
54
+
55
+ | 方法 | 参数 | 说明 |
56
+ |------|------|------|
57
+ | `ctx.call(msg, ctx?)` | `{ path, key?, payload?, ... } \| { id }` | 调用其他路由,返回完整 context |
58
+ | `ctx.run(msg, ctx?)` | `{ path, key?, payload? }` | 调用其他路由,返回 `{ code, data, message }` |
59
+ | `ctx.forward(res)` | `{ code, data?, message? }` | 设置响应结果 |
60
+ | `ctx.throw(code?, message?, tips?)` | - | 抛出自定义错误 |
61
+
62
+ ## 完整示例
63
+
64
+ ```ts
25
65
  import { App } from '@kevisual/router';
26
66
 
27
67
  const app = new App();
28
68
  app.listen(4002);
29
- import { proxyRoute, initProxy } from '@kevisual/local-proxy/proxy.ts';
30
- initProxy({
31
- pagesDir: './demo',
32
- watch: true,
33
- });
34
-
35
- app.onServerRequest(proxyRoute);
36
- ```
69
+
70
+ // 基本路由
71
+ app
72
+ .route({ path: 'user', key: 'info' })
73
+ .define(async (ctx) => {
74
+ // ctx.query 包含请求参数
75
+ const { id } = ctx.query;
76
+ ctx.body = { id, name: '张三' };
77
+ ctx.code = 200;
78
+ })
79
+ .addTo(app);
80
+
81
+ // 使用 state 在路由间传递数据
82
+ app
83
+ .route({ path: 'order', key: 'create' })
84
+ .define(async (ctx) => {
85
+ ctx.state = { orderId: '12345' };
86
+ })
87
+ .addTo(app);
88
+
89
+ app
90
+ .route({ path: 'order', key: 'pay' })
91
+ .define(async (ctx) => {
92
+ // 可以获取前一个路由设置的 state
93
+ const { orderId } = ctx.state;
94
+ ctx.body = { orderId, status: 'paid' };
95
+ })
96
+ .addTo(app);
97
+
98
+ // 链式调用
99
+ app
100
+ .route({ path: 'product', key: 'list' })
101
+ .define(async (ctx) => {
102
+ ctx.body = [{ id: 1, name: '商品A' }];
103
+ })
104
+ .addTo(app);
105
+
106
+ // 调用其他路由
107
+ app
108
+ .route({ path: 'dashboard', key: 'stats' })
109
+ .define(async (ctx) => {
110
+ // 调用 user/info 路由
111
+ const userRes = await ctx.run({ path: 'user', key: 'info', payload: { id: 1 } });
112
+ // 调用 product/list 路由
113
+ const productRes = await ctx.run({ path: 'product', key: 'list' });
114
+
115
+ ctx.body = {
116
+ user: userRes.data,
117
+ products: productRes.data
118
+ };
119
+ })
120
+ .addTo(app);
121
+
122
+ // 使用 throw 抛出错误
123
+ app
124
+ .route({ path: 'admin', key: 'delete' })
125
+ .define(async (ctx) => {
126
+ const { id } = ctx.query;
127
+ if (!id) {
128
+ ctx.throw(400, '缺少参数', 'id is required');
129
+ }
130
+ ctx.body = { success: true };
131
+ })
132
+ .addTo(app);
133
+ ```
134
+
135
+ ## 中间件
136
+
137
+ ```ts
138
+ import { App, Route } from '@kevisual/router';
139
+
140
+ const app = new App();
141
+
142
+ // 定义中间件
143
+ app.route({
144
+ id: 'auth-example',
145
+ description: '权限校验中间件'
146
+ }).define(async(ctx) => {
147
+ const token = ctx.query.token;
148
+ if (!token) {
149
+ ctx.throw(401, '未登录', '需要 token');
150
+ }
151
+ // 验证通过,设置用户信息到 state
152
+ ctx.state.tokenUser = { id: 1, name: '用户A' };
153
+ }).addTo(app);
154
+
155
+ // 使用中间件(通过 id 引用)
156
+ app
157
+ .route({ path: 'admin', key: 'panel', middleware: ['auth-example'] })
158
+ .define(async (ctx) => {
159
+ // 可以访问中间件设置的 state
160
+ const { tokenUser } = ctx.state;
161
+ ctx.body = { tokenUser };
162
+ })
163
+ .addTo(app);
164
+ ```
165
+
166
+ ## 注意事项
167
+
168
+ 1. **path 和 key 的组合是路由的唯一标识**,同一个 path+key 只能添加一个路由,后添加的会覆盖之前的。
169
+
170
+ 2. **ctx.call vs ctx.run**:
171
+ - `call` 返回完整 context,包含所有属性
172
+ - `run` 返回 `{ code, data, message }` 格式,data 即 body
173
+
174
+ 3. **ctx.throw 会自动结束执行**,抛出自定义错误。
175
+
176
+ 4. **state 不会自动继承**,每个路由的 state 是独立的,除非显式传递或使用 nextRoute。
177
+
178
+ 5. **payload 会自动合并到 query**,调用 `ctx.run({ path, key, payload })` 时,payload 会合并到 query。
179
+
180
+ 6. **nextQuery 用于传递给 nextRoute**,在当前路由中设置 `ctx.nextQuery`,会在执行 nextRoute 时合并到 query。
181
+
182
+ 7. **避免 nextRoute 循环调用**,默认最大深度为 40 次,超过会返回 500 错误。
183
+
184
+ 8. **needSerialize 默认为 true**,会自动对 body 进行 JSON 序列化和反序列化。
185
+
186
+ 9. **progress 记录执行路径**,可用于调试和追踪路由调用链。
187
+
188
+ 10. **中间件找不到会返回 404**,错误信息中会包含找不到的中间件列表。
package/src/browser.ts CHANGED
@@ -1,15 +1,16 @@
1
1
  export { Route, QueryRouter, QueryRouterServer, Mini } from './route.ts';
2
2
 
3
- export type { Rule, Schema } from './validator/index.ts';
3
+ export type { Rule, Schema, } from './validator/index.ts';
4
4
 
5
5
  export { createSchema } from './validator/index.ts';
6
6
 
7
- export type { RouteContext, RouteOpts } from './route.ts';
7
+ export type { RouteContext, RouteOpts, RouteMiddleware } from './route.ts';
8
8
 
9
9
  export type { Run, Skill } from './route.ts';
10
10
 
11
- export { createSkill } from './route.ts';
11
+ export { createSkill, tool } from './route.ts';
12
12
 
13
13
  export { CustomError } from './result/error.ts';
14
14
 
15
15
  export * from './router-define.ts';
16
+ // --- 以上同步更新至 browser.ts ---
package/src/index.ts CHANGED
@@ -8,11 +8,12 @@ export type { RouteContext, RouteOpts, RouteMiddleware } from './route.ts';
8
8
 
9
9
  export type { Run, Skill } from './route.ts';
10
10
 
11
- export { createSkill } from './route.ts';
11
+ export { createSkill, tool } from './route.ts';
12
12
 
13
13
  export { CustomError } from './result/error.ts';
14
14
 
15
15
  export * from './router-define.ts';
16
+ // --- 以上同步更新至 browser.ts ---
16
17
 
17
18
  export { ServerNode, handleServer } from './server/index.ts';
18
19
 
@@ -0,0 +1,72 @@
1
+ import { useContextKey } from '@kevisual/context'
2
+ import { type QueryRouter, type Skill } from './route.ts'
3
+ import { type App } from './app.ts'
4
+ import { type Plugin } from "@opencode-ai/plugin"
5
+
6
+ import { filter } from '@kevisual/js-filter';
7
+
8
+ export const createRouterAgentPluginFn = (opts?: {
9
+ router?: QueryRouter,
10
+ //** 过滤比如,WHERE metadata.tags includes 'opencode' */
11
+ query?: string
12
+ }) => {
13
+ let router = opts?.router
14
+ if (!router) {
15
+ const app = useContextKey<App>('app')
16
+ router = app.router
17
+ }
18
+ if (!router) {
19
+ throw new Error('Router 参数缺失')
20
+ }
21
+ const _routes = filter(router.routes, opts?.query || '')
22
+ const routes = _routes.filter(r => {
23
+ const metadata = r.metadata as Skill
24
+ if (metadata && metadata.tags && metadata.tags.includes('opencode')) {
25
+ return !!metadata.skill
26
+ }
27
+ return false
28
+ })
29
+ // opencode run "查看系统信息"
30
+ const AgentPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
31
+ return {
32
+ 'tool': {
33
+ ...routes.reduce((acc, route) => {
34
+ const metadata = route.metadata as Skill
35
+ acc[metadata.skill!] = {
36
+ name: metadata.title || metadata.skill,
37
+ description: metadata.summary || '',
38
+ args: metadata.args || {},
39
+ async execute(args: Record<string, any>) {
40
+ const res = await router.run({
41
+ path: route.path,
42
+ key: route.key,
43
+ payload: args
44
+ },
45
+ { appId: router.appId! });
46
+ if (res.code === 200) {
47
+ if (res.data?.content) {
48
+ return res.data.content;
49
+ }
50
+ if (res.data?.final) {
51
+ return '调用程序成功';
52
+ }
53
+ const str = JSON.stringify(res.data || res, null, 2);
54
+ if (str.length > 10000) {
55
+ return str.slice(0, 10000) + '... (truncated)';
56
+ }
57
+ return str;
58
+ }
59
+ return `Error: ${res?.message || '无法获取结果'}`;
60
+ }
61
+ }
62
+ return acc;
63
+ }, {} as Record<string, any>)
64
+ },
65
+ 'tool.execute.before': async (opts) => {
66
+ // console.log('CnbPlugin: tool.execute.before', opts.tool);
67
+ // delete toolSkills['cnb-login-verify']
68
+ }
69
+ }
70
+ }
71
+ return AgentPlugin
72
+ }
package/src/route.ts CHANGED
@@ -102,6 +102,9 @@ export type Skill<T = SimpleObject> = {
102
102
  [key: string]: any
103
103
  };
104
104
  } & T
105
+ export const tool = {
106
+ schema: z
107
+ }
105
108
  /** */
106
109
  export const createSkill = <T = SimpleObject>(skill: Skill<T>): Skill<T> => {
107
110
  return {
@@ -2,6 +2,8 @@ import { pathToRegexp, Key } from 'path-to-regexp';
2
2
  import type { IncomingMessage, ServerResponse, Server } from 'node:http';
3
3
  import { parseBody, parseSearch, parseSearchValue } from './server/parse-body.ts';
4
4
  import { ListenOptions } from 'node:net';
5
+ // import { Hono } from 'hono'
6
+ // const app = new Hono()
5
7
 
6
8
  type Req = IncomingMessage & { params?: Record<string, string> };
7
9
  type SimpleObject = {