Compare commits

..

16 Commits

Author SHA1 Message Date
e7dca513f3 feat: 添加路由描述和更新运行代码参数,优化测试用例 2026-01-26 04:20:56 +08:00
0f8986b491 update 2026-01-26 01:09:20 +08:00
911f03b4bd fix mini modules 2026-01-25 22:12:04 +08:00
8a1aff4c50 update 2026-01-25 21:19:28 +08:00
3284a06ad5 udpate 2026-01-21 19:02:30 +08:00
7dcf53fb4f feat: 更新静态资源代理文档,优化路由和插件集成,提升代码可读性和功能性 2026-01-21 01:44:58 +08:00
61add1aad1 udpate 2026-01-20 13:39:22 +08:00
2363617404 add call 2026-01-20 13:38:50 +08:00
f4372ae55f feat: add opencode plugin with router integration and TypeScript definitions
- Implemented `createRouterAgentPluginFn` in `src/opencode.ts` to create a plugin that interacts with the router and filters routes based on metadata tags.
- Added support for executing routes with error handling and response formatting.
- Updated `rollup.config.js` to include build configurations for `opencode.js` and `opencode.d.ts`.
2026-01-20 13:23:06 +08:00
xiongxiao
9b11ea5138 更新 .cnb.yml、package.json 和文档,添加新功能和示例代码 2026-01-19 04:52:14 +08:00
999397611c temp 2026-01-16 18:53:55 +08:00
730f4c6eb9 fix 2026-01-16 12:06:01 +08:00
xiongxiao
123d02f452 udpate 2026-01-15 23:44:40 +08:00
xiongxiao
309446c864 更新依赖项,添加 Connect 和 QueryConnect 类,重构导出结构 2026-01-15 21:21:35 +08:00
2c57435a81 update 2026-01-13 14:09:40 +08:00
cd785ddb51 add trigger 2026-01-13 14:08:38 +08:00
29 changed files with 1229 additions and 481 deletions

View File

@@ -17,10 +17,7 @@ $:
- vscode
- docker
imports: !reference [.common_env, imports]
# 开发环境启动后会执行的任务
# stages:
# - name: pnpm install
# script: pnpm install
stages: !reference [.dev_tempalte, stages]
.common_sync_to_gitea: &common_sync_to_gitea
- <<: *common_env
@@ -41,16 +38,3 @@ main:
- <<: *common_sync_to_gitea
api_trigger_sync_from_gitea:
- <<: *common_sync_from_gitea
branch:
# 如下按钮在分支名以 release 开头的分支详情页面显示
- reg: "^main"
buttons:
- name: 同步代码到gitea
desc: 同步代码到gitea
event: web_trigger_sync_to_gitea
- name: 同步gitea代码到当前仓库
desc: 同步gitea代码到当前仓库
event: web_trigger_sync_from_gitea

2
.gitignore vendored
View File

@@ -6,3 +6,5 @@ dist
https-cert.pem
https-key.pem
.pnpm-store

View File

@@ -0,0 +1,3 @@
import { routerAgentPlugin } from "../../agent/main.ts";
export { routerAgentPlugin };

25
AGENTS.md Normal file
View File

@@ -0,0 +1,25 @@
# @kevisual/router
## 是什么
`@kevisual/router` 是一个基于自定义的的轻量级路由框架,支持 TypeScript适用于构建 API 服务。
## 安装
```bash
npm install @kevisual/router
```
## 快速开始
```ts
import { App } from '@kevisual/router';
const app = new App();
app.listen(4002);
app.route({path:'demo', key: '01'})
.define(async (ctx) => {
ctx.body = '01';
})
.addTo(app);
```

15
STATIC.md Normal file
View File

@@ -0,0 +1,15 @@
## 兼容服务器静态资源代理
```ts
import { App } from '@kevisual/router';
const app = new App();
app.listen(4002);
import { proxyRoute, initProxy } from '@kevisual/local-proxy/proxy.ts';
initProxy({
pagesDir: './demo',
watch: true,
});
app.onServerRequest(proxyRoute);
```

5
agent/app.ts Normal file
View File

@@ -0,0 +1,5 @@
import { App } from '../src/app.ts';
import { useContextKey } from '@kevisual/context';
export const app = useContextKey<App>('app', () => new App());
export { createSkill, type Skill, tool } from '../src/app.ts';

42
agent/gen.ts Normal file
View File

@@ -0,0 +1,42 @@
import path from 'path';
import glob from 'fast-glob';
async function inlineMarkdownFiles() {
const files: { path: string; name: string }[] = [];
// 添加 readme.md
const readmePath = path.join(import.meta.dir, '..', 'readme.md');
files.push({ path: readmePath, name: 'readme' });
// 使用 fast-glob 动态获取 docs 目录下的 md 文件
const rootDir = path.join(import.meta.dir, '..', 'docs');
const mdFiles = await glob('**.md', { cwd: rootDir });
for (const filename of mdFiles) {
// 将路径转为变量名,如 examples/base -> examples_base
const name = filename.replace(/\.md$/, '').replace(/[^a-zA-Z0-9]/g, '_');
files.push({ path: path.join(rootDir, filename), name });
}
let generatedCode = '// Generated by build script\n';
for (const file of files) {
try {
const content = await Bun.file(file.path).text();
// 转义模板字符串中的特殊字符
const escapedContent = content
.replace(/\\/g, '\\\\')
.replace(/`/g, '\\`')
.replace(/\${/g, '\\${');
generatedCode += `export const ${file.name} = \`${escapedContent}\`;\n`;
} catch (error) {
console.warn(`Failed to read ${file.path}:`, error);
generatedCode += `export const ${file.name} = '';\n`;
}
}
// 写入生成的文件
await Bun.write(path.join(import.meta.dir, 'gen', 'index.ts'), generatedCode);
}
await inlineMarkdownFiles();

205
agent/gen/index.ts Normal file
View File

@@ -0,0 +1,205 @@
// Generated by build script
export const readme = `# router
一个轻量级的路由框架,支持链式调用、中间件、嵌套路由等功能。
## 快速开始
\`\`\`ts
import { App } from '@kevisual/router';
const app = new App();
app.listen(4002);
app
.route({ path: 'demo', key: '02' })
.define(async (ctx) => {
ctx.body = '02';
})
.addTo(app);
app
.route({ path: 'demo', key: '03' })
.define(async (ctx) => {
ctx.body = '03';
})
.addTo(app);
\`\`\`
## 核心概念
### RouteContext 属性说明
在 route handler 中,你可以通过 \`ctx\` 访问以下属性:
| 属性 | 类型 | 说明 |
|------|------|------|
| \`query\` | \`object\` | 请求参数,会自动合并 payload |
| \`body\` | \`number \\| string \\| Object\` | 响应内容 |
| \`code\` | \`number\` | 响应状态码,默认为 200 |
| \`message\` | \`string\` | 响应消息 |
| \`state\` | \`any\` | 状态数据,可在路由间传递 |
| \`appId\` | \`string\` | 应用标识 |
| \`currentPath\` | \`string\` | 当前路由路径 |
| \`currentKey\` | \`string\` | 当前路由 key |
| \`currentRoute\` | \`Route\` | 当前 Route 实例 |
| \`progress\` | \`[string, string][]\` | 路由执行路径记录 |
| \`nextQuery\` | \`object\` | 传递给下一个路由的参数 |
| \`end\` | \`boolean\` | 是否提前结束路由执行 |
| \`app\` | \`QueryRouter\` | 路由实例引用 |
| \`error\` | \`any\` | 错误信息 |
| \`index\` | \`number\` | 当前路由执行深度 |
| \`needSerialize\` | \`boolean\` | 是否需要序列化响应数据 |
### 上下文方法
| 方法 | 参数 | 说明 |
|------|------|------|
| \`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? }\` | 设置响应结果 |
| \`ctx.throw(code?, message?, tips?)\` | - | 抛出自定义错误 |
## 完整示例
\`\`\`ts
import { App } from '@kevisual/router';
const app = new App();
app.listen(4002);
// 基本路由
app
.route({ path: 'user', key: 'info' })
.define(async (ctx) => {
// ctx.query 包含请求参数
const { id } = ctx.query;
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' })
.define(async (ctx) => {
// 可以获取前一个路由设置的 state
const { orderId } = ctx.state;
ctx.body = { orderId, status: 'paid' };
})
.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' })
.define(async (ctx) => {
// 调用 user/info 路由
const userRes = await ctx.run({ path: 'user', key: 'info', payload: { id: 1 } });
// 调用 product/list 路由
const productRes = await ctx.run({ path: 'product', key: 'list' });
ctx.body = {
user: userRes.data,
products: productRes.data
};
})
.addTo(app);
// 使用 throw 抛出错误
app
.route({ path: 'admin', key: 'delete' })
.define(async (ctx) => {
const { id } = ctx.query;
if (!id) {
ctx.throw(400, '缺少参数', 'id is required');
}
ctx.body = { success: true };
})
.addTo(app);
\`\`\`
## 中间件
\`\`\`ts
import { App, Route } from '@kevisual/router';
const app = new App();
// 定义中间件
app.route({
id: 'auth-example',
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);
// 使用中间件(通过 id 引用)
app
.route({ path: 'admin', key: 'panel', middleware: ['auth-example'] })
.define(async (ctx) => {
// 可以访问中间件设置的 state
const { tokenUser } = ctx.state;
ctx.body = { tokenUser };
})
.addTo(app);
\`\`\`
## 注意事项
1. **path 和 key 的组合是路由的唯一标识**,同一个 path+key 只能添加一个路由,后添加的会覆盖之前的。
2. **ctx.call vs ctx.run**
- \`call\` 返回完整 context包含所有属性
- \`run\` 返回 \`{ code, data, message }\` 格式data 即 body
3. **ctx.throw 会自动结束执行**,抛出自定义错误。
4. **state 不会自动继承**,每个路由的 state 是独立的,除非显式传递或使用 nextRoute。
5. **payload 会自动合并到 query**,调用 \`ctx.run({ path, key, payload })\`payload 会合并到 query。
6. **nextQuery 用于传递给 nextRoute**,在当前路由中设置 \`ctx.nextQuery\`,会在执行 nextRoute 时合并到 query。
7. **避免 nextRoute 循环调用**,默认最大深度为 40 次,超过会返回 500 错误。
8. **needSerialize 默认为 true**,会自动对 body 进行 JSON 序列化和反序列化。
9. **progress 记录执行路径**,可用于调试和追踪路由调用链。
10. **中间件找不到会返回 404**,错误信息中会包含找不到的中间件列表。
`;
export const examples_base = `# 最基本的用法
\`\`\`ts
import { App } from '@kevisual/router';
const app = new App();
app.listen(4002);
app
.route({ path: 'demo', key: '02' })
.define(async (ctx) => {
ctx.body = '02';
})
.addTo(app);
\`\`\``;

6
agent/main.ts Normal file
View File

@@ -0,0 +1,6 @@
import { app } from './app.ts'
import { createRouterAgentPluginFn } from '../src/opencode.ts'
import './routes/index.ts'
// 工具列表
export const routerAgentPlugin = createRouterAgentPluginFn({ router: app });

14
agent/routes/index.ts Normal file
View File

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

View File

@@ -0,0 +1,45 @@
import { app, createSkill, tool } from '../app.ts';
import * as docs from '../gen/index.ts'
import * as pkgs from '../../package.json' assert { type: 'json' };
app.route({
path: 'router-skill',
key: 'create-route',
description: '创建路由技能',
middleware: ['auth'],
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'create-router-skill',
title: '创建路由技能',
summary: '创建一个新的路由技能,参数包括路径、键、描述、参数等',
args: {
question: tool.schema.string().describe('要实现的功能'),
}
})
},
}).define(async (ctx) => {
const { question } = ctx.query || {};
if (!question) {
ctx.throw('参数 question 不能为空');
}
let base = ''
base += `根据用户需要实现的功能生成一个route的代码${question}\n\n`;
base += `资料库:\n`
base += docs.readme + '\n\n';
ctx.body = {
body: base
}
}).addTo(app);
// 调用router应用 path router-skill key version
app.route({
path: 'router-skill',
key: 'version',
description: '获取路由技能版本',
middleware: ['auth'],
}).define(async (ctx) => {
ctx.body = {
content: pkgs.version || 'unknown'
}
}).addTo(app);

24
bun.config.ts Normal file
View File

@@ -0,0 +1,24 @@
import path from 'node:path';
import pkg from './package.json';
import fs from 'node:fs';
import { execSync } from 'node:child_process';
const w = (p: string) => path.resolve(import.meta.dir, p);
const external: string[] = ["bun"];
await Bun.build({
target: 'node',
format: 'esm',
entrypoints: [w('./agent/main.ts')],
outdir: w('./dist'),
naming: {
entry: 'app.js',
},
define: {},
external
});
const cmd = 'dts -i ./agent/main.ts -o /app.d.ts';
execSync(cmd, { stdio: 'inherit' });
// Copy package.json to dist

View File

@@ -10,15 +10,6 @@ route01.run = async (ctx) => {
};
app.addRoute(route01);
// 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';

15
docs/examples/base.md Normal file
View File

@@ -0,0 +1,15 @@
# 最基本的用法
```ts
import { App } from '@kevisual/router';
const app = new App();
app.listen(4002);
app
.route({ path: 'demo', key: '02' })
.define(async (ctx) => {
ctx.body = '02';
})
.addTo(app);
```

View File

@@ -1,43 +1,51 @@
{
"$schema": "https://json.schemastore.org/package",
"name": "@kevisual/router",
"version": "0.0.53",
"version": "0.0.62",
"description": "",
"type": "module",
"main": "./dist/router.js",
"types": "./dist/router.d.ts",
"scripts": {
"build": "npm run clean && rollup -c",
"build:app": "npm run build && rsync dist/*browser* ../deploy/dist",
"postbuild": "bun run bun.config.ts",
"watch": "rollup -c -w",
"clean": "rm -rf dist"
},
"files": [
"dist",
"src",
"agent",
"auto.ts",
"mod.ts"
],
"keywords": [],
"author": "abearxiong",
"license": "MIT",
"packageManager": "pnpm@10.28.0",
"packageManager": "pnpm@10.28.1",
"devDependencies": {
"@kevisual/context": "^0.0.4",
"@kevisual/js-filter": "^0.0.5",
"@kevisual/local-proxy": "^0.0.8",
"@kevisual/query": "^0.0.35",
"@kevisual/use-config": "^1.0.28",
"@opencode-ai/plugin": "^1.1.27",
"@rollup/plugin-alias": "^6.0.0",
"@rollup/plugin-commonjs": "29.0.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-typescript": "^12.3.0",
"@types/bun": "^1.3.5",
"@types/node": "^25.0.7",
"@types/bun": "^1.3.6",
"@types/node": "^25.0.9",
"@types/send": "^1.2.1",
"@types/ws": "^8.18.1",
"@types/xml2js": "^0.4.14",
"eventemitter3": "^5.0.1",
"eventemitter3": "^5.0.4",
"fast-glob": "^3.3.3",
"nanoid": "^5.1.6",
"rollup": "^4.55.1",
"path-to-regexp": "^8.3.0",
"rollup": "^4.55.2",
"rollup-plugin-dts": "^6.3.0",
"send": "^1.2.1",
"ts-loader": "^9.5.4",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
@@ -45,16 +53,15 @@
"typescript": "^5.9.3",
"ws": "npm:@kevisual/ws",
"xml2js": "^0.6.2",
"zod": "^4.3.5",
"@kevisual/js-filter": "^0.0.4",
"path-to-regexp": "^8.3.0",
"send": "^1.2.1"
"zod": "^4.3.5"
},
"repository": {
"type": "git",
"url": "git+https://github.com/abearxiong/kevisual-router.git"
},
"dependencies": {},
"dependencies": {
"hono": "^4.11.4"
},
"publishConfig": {
"access": "public"
},
@@ -74,6 +81,8 @@
"require": "./dist/router-simple.js",
"types": "./dist/router-simple.d.ts"
},
"./opencode": "./dist/opencode.js",
"./skill": "./dist/app.js",
"./define": {
"import": "./dist/router-define.js",
"require": "./dist/router-define.js",

658
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

172
readme.md
View File

@@ -1,5 +1,9 @@
# router
一个轻量级的路由框架,支持链式调用、中间件、嵌套路由等功能。
## 快速开始
```ts
import { App } from '@kevisual/router';
@@ -7,30 +11,178 @@ const app = new App();
app.listen(4002);
app
.route({path:'demo', key: '02})
.route({ path: 'demo', key: '02' })
.define(async (ctx) => {
ctx.body = '02';
})
.addTo(app);
app
.route('demo', '03')
.route({ path: 'demo', key: '03' })
.define(async (ctx) => {
ctx.body = '03';
})
.addTo(app);
```
## 兼容服务器
```
## 核心概念
### RouteContext 属性说明
在 route handler 中,你可以通过 `ctx` 访问以下属性:
| 属性 | 类型 | 说明 |
|------|------|------|
| `query` | `object` | 请求参数,会自动合并 payload |
| `body` | `number \| string \| Object` | 响应内容 |
| `code` | `number` | 响应状态码,默认为 200 |
| `message` | `string` | 响应消息 |
| `state` | `any` | 状态数据,可在路由间传递 |
| `appId` | `string` | 应用标识 |
| `currentPath` | `string` | 当前路由路径 |
| `currentKey` | `string` | 当前路由 key |
| `currentRoute` | `Route` | 当前 Route 实例 |
| `progress` | `[string, string][]` | 路由执行路径记录 |
| `nextQuery` | `object` | 传递给下一个路由的参数 |
| `end` | `boolean` | 是否提前结束路由执行 |
| `app` | `QueryRouter` | 路由实例引用 |
| `error` | `any` | 错误信息 |
| `index` | `number` | 当前路由执行深度 |
| `needSerialize` | `boolean` | 是否需要序列化响应数据 |
### 上下文方法
| 方法 | 参数 | 说明 |
|------|------|------|
| `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? }` | 设置响应结果 |
| `ctx.throw(code?, message?, tips?)` | - | 抛出自定义错误 |
## 完整示例
```ts
import { App } from '@kevisual/router';
const app = new App();
app.listen(4002);
import { proxyRoute, initProxy } from '@kevisual/local-proxy/proxy.ts';
initProxy({
pagesDir: './demo',
watch: true,
});
app.onServerRequest(proxyRoute);
// 基本路由
app
.route({ path: 'user', key: 'info' })
.define(async (ctx) => {
// ctx.query 包含请求参数
const { id } = ctx.query;
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' })
.define(async (ctx) => {
// 可以获取前一个路由设置的 state
const { orderId } = ctx.state;
ctx.body = { orderId, status: 'paid' };
})
.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' })
.define(async (ctx) => {
// 调用 user/info 路由
const userRes = await ctx.run({ path: 'user', key: 'info', payload: { id: 1 } });
// 调用 product/list 路由
const productRes = await ctx.run({ path: 'product', key: 'list' });
ctx.body = {
user: userRes.data,
products: productRes.data
};
})
.addTo(app);
// 使用 throw 抛出错误
app
.route({ path: 'admin', key: 'delete' })
.define(async (ctx) => {
const { id } = ctx.query;
if (!id) {
ctx.throw(400, '缺少参数', 'id is required');
}
ctx.body = { success: true };
})
.addTo(app);
```
## 中间件
```ts
import { App, Route } from '@kevisual/router';
const app = new App();
// 定义中间件
app.route({
id: 'auth-example',
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);
// 使用中间件(通过 id 引用)
app
.route({ path: 'admin', key: 'panel', middleware: ['auth-example'] })
.define(async (ctx) => {
// 可以访问中间件设置的 state
const { tokenUser } = ctx.state;
ctx.body = { tokenUser };
})
.addTo(app);
```
## 注意事项
1. **path 和 key 的组合是路由的唯一标识**,同一个 path+key 只能添加一个路由,后添加的会覆盖之前的。
2. **ctx.call vs ctx.run**
- `call` 返回完整 context包含所有属性
- `run` 返回 `{ code, data, message }` 格式data 即 body
3. **ctx.throw 会自动结束执行**,抛出自定义错误。
4. **state 不会自动继承**,每个路由的 state 是独立的,除非显式传递或使用 nextRoute。
5. **payload 会自动合并到 query**,调用 `ctx.run({ path, key, payload })`payload 会合并到 query。
6. **nextQuery 用于传递给 nextRoute**,在当前路由中设置 `ctx.nextQuery`,会在执行 nextRoute 时合并到 query。
7. **避免 nextRoute 循环调用**,默认最大深度为 40 次,超过会返回 500 错误。
8. **needSerialize 默认为 true**,会自动对 body 进行 JSON 序列化和反序列化。
9. **progress 记录执行路径**,可用于调试和追踪路由调用链。
10. **中间件找不到会返回 404**,错误信息中会包含找不到的中间件列表。

View File

@@ -126,4 +126,26 @@ export default [
},
plugins: [dts()],
},
{
input: 'src/opencode.ts',
output: {
file: 'dist/opencode.js',
format: 'es',
},
plugins: [
resolve({
browser: true,
}),
commonjs(),
typescript(),
],
},
{
input: 'src/opencode.ts',
output: {
file: 'dist/opencode.d.ts',
format: 'es',
},
plugins: [dts()],
},
];

View File

@@ -2,7 +2,6 @@ import { QueryRouter, 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';
import { CustomError } from './result/error.ts';
import { handleServer } from './server/handle-server.ts';
import { IncomingMessage, ServerResponse } from 'http';
import { isBun } from './utils/is-engine.ts';
@@ -26,12 +25,13 @@ export type AppRouteContext<T = {}> = HandleCtx & RouteContext<T> & { app: App<T
* 封装了 Router 和 Server 的 App 模块处理http的请求和响应内置了 Cookie 和 Token 和 res 的处理
* U - Route Context的扩展类型
*/
export class App<U = {}> {
appId: string;
export class App<U = {}> extends QueryRouter {
declare appId: string;
router: QueryRouter;
server: ServerType;
constructor(opts?: AppOptions<U>) {
const router = opts?.router || new QueryRouter();
super();
const router = this;
let server = opts?.server;
if (!server) {
const serverOptions = opts?.serverOptions || {};
@@ -64,15 +64,9 @@ export class App<U = {}> {
// @ts-ignore
this.server.listen(...args);
}
use(path: string, fn: (ctx: any) => any, opts?: RouteOpts) {
const route = new Route(path, '', opts);
route.run = fn;
this.router.add(route);
}
addRoute(route: Route) {
this.router.add(route);
super.add(route);
}
add = this.addRoute;
Route = Route;
route(opts: RouteOpts<AppRouteContext<U>>): Route<AppRouteContext<U>>;
@@ -109,30 +103,10 @@ export class App<U = {}> {
}
async call(message: { id?: string, path?: string; key?: string; payload?: any }, ctx?: AppRouteContext<U> & { [key: string]: any }) {
const router = this.router;
return await router.call(message, ctx);
return await super.call(message, ctx);
}
/**
* @deprecated
*/
async queryRoute(path: string, key?: string, payload?: any, ctx?: AppRouteContext<U> & { [key: string]: any }) {
return await this.router.queryRoute({ path, key, payload }, ctx);
}
async run(msg: { id?: string, path?: string; key?: string; payload?: any }, ctx?: AppRouteContext<U> & { [key: string]: any }) {
return await this.router.run(msg, ctx);
}
exportRoutes() {
return this.router.exportRoutes();
}
importRoutes(routes: any[]) {
this.router.importRoutes(routes);
}
importApp(app: App) {
this.importRoutes(app.exportRoutes());
}
throw(code?: number | string, message?: string, tips?: string): void;
throw(...args: any[]) {
throw new CustomError(...args);
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);

View File

@@ -1,15 +1,18 @@
export { Route, QueryRouter, QueryRouterServer, Mini } from './route.ts';
export type { Rule, Schema } from './validator/index.ts';
export type { Rule, Schema, } from './validator/index.ts';
export { createSchema } from './validator/index.ts';
export type { RouteContext, RouteOpts } from './route.ts';
export type { RouteContext, RouteOpts, RouteMiddleware } from './route.ts';
export type { Run } from './route.ts';
export type { Run, Skill } from './route.ts';
export { createSkill, tool } from './route.ts';
export { CustomError } from './result/error.ts';
export * from './server/parse-body.ts';
export * from './router-define.ts';
export { MockProcess } from './utils/listen-process.ts'
// --- 以上同步更新至 browser.ts ---

View File

@@ -1,24 +1,25 @@
export { Route, QueryRouter, QueryRouterServer, Mini } from './route.ts';
export { Connect, QueryConnect } from './connect.ts';
export type { RouteContext, RouteOpts, RouteMiddleware } from './route.ts';
export type { Run } from './route.ts';
export { ServerNode, handleServer } from './server/index.ts';
/**
* 自定义错误
*/
export { CustomError } from './result/error.ts';
export { createSchema } from './validator/index.ts';
export type { Rule, Schema, } from './validator/index.ts';
export { App } from './app.ts';
export { createSchema } from './validator/index.ts';
export type { RouteContext, RouteOpts, RouteMiddleware } from './route.ts';
export type { Run, Skill } from './route.ts';
export { createSkill, tool } from './route.ts';
export { CustomError } from './result/error.ts';
export * from './router-define.ts';
export { MockProcess } from './utils/listen-process.ts'
// --- 以上同步更新至 browser.ts ---
export { ServerNode, handleServer } from './server/index.ts';
export { App } from './app.ts';
export type {
RouterReq,

View File

@@ -1,5 +1,5 @@
import { nanoid } from 'nanoid';
import { RouteContext } from './route.ts';
import { RouteContext } from '../route.ts';
export class Connect {
path: string;

108
src/opencode.ts Normal file
View File

@@ -0,0 +1,108 @@
import { useContextKey } from '@kevisual/context'
import { createSkill, type QueryRouterServer, tool, type QueryRouter, type Skill } from './route.ts'
import { type App } from './app.ts'
import { type Plugin } from "@opencode-ai/plugin"
import { filter } from '@kevisual/js-filter';
export const addCallFn = (app: App) => {
app.route({
path: 'call',
key: '',
description: '调用',
middleware: ['auth'],
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'call-app',
title: '调用app应用',
summary: '调用router的应用, 参数path, key, payload',
args: {
path: tool.schema.string().describe('应用路径,例如 cnb'),
key: tool.schema.string().optional().describe('应用key例如 list-repos'),
payload: tool.schema.object({}).optional().describe('调用参数'),
}
})
},
}).define(async (ctx) => {
const { path, key = '' } = ctx.query;
if (!path) {
ctx.throw('路径path不能为空');
}
const res = await ctx.run({ path, key, payload: ctx.query.payload || {} }, {
...ctx
});
ctx.forward(res);
}).addTo(app)
}
export const createRouterAgentPluginFn = (opts?: {
router?: App | QueryRouterServer,
//** 过滤比如WHERE metadata.tags includes 'opencode' */
query?: string
}) => {
let router = opts?.router
if (!router) {
const app = useContextKey<App>('app')
router = app
}
if (!router) {
throw new Error('Router 参数缺失')
}
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)
}
const _routes = filter(router.routes, opts?.query || '')
const routes = _routes.filter(r => {
const metadata = r.metadata as Skill
if (metadata && metadata.tags && metadata.tags.includes('opencode')) {
return !!metadata.skill
}
return false
})
// opencode run "查看系统信息"
const AgentPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
return {
'tool': {
...routes.reduce((acc, route) => {
const metadata = route.metadata as Skill
acc[metadata.skill!] = {
name: metadata.title || metadata.skill,
description: metadata.summary || '',
args: metadata.args || {},
async execute(args: Record<string, any>) {
const res = await router.run({
path: route.path,
key: route.key,
payload: args
},
{ appId: router.appId! });
if (res.code === 200) {
if (res.data?.content) {
return res.data.content;
}
if (res.data?.final) {
return '调用程序成功';
}
const str = JSON.stringify(res.data || res, null, 2);
if (str.length > 10000) {
return str.slice(0, 10000) + '... (truncated)';
}
return str;
}
console.error('调用出错', res);
return `Error: ${res?.message || '无法获取结果'}`;
}
}
return acc;
}, {} as Record<string, any>)
},
'tool.execute.before': async (opts) => {
// console.log('CnbPlugin: tool.execute.before', opts.tool);
// delete toolSkills['cnb-login-verify']
}
}
}
return AgentPlugin
}

View File

@@ -9,10 +9,10 @@ export class CustomError extends Error {
this.name = 'CustomError';
if (typeof code === 'number') {
this.code = code;
this.message = message;
this.message = message!;
} else {
this.code = 500;
this.message = code;
this.message = code!;
}
this.tips = tips;
// 这一步可不写,默认会保存堆栈追踪信息到自定义错误构造函数之前,

View File

@@ -2,6 +2,8 @@ import { nanoid } from 'nanoid';
import { CustomError } from './result/error.ts';
import { pick } from './utils/pick.ts';
import { listenProcess } from './utils/listen-process.ts';
import { z } from 'zod';
import { filter } from '@kevisual/js-filter'
export type RouterContextT = { code?: number;[key: string]: any };
export type RouteContext<T = { code?: number }, S = any> = {
@@ -58,7 +60,7 @@ export type RouteContext<T = { code?: number }, S = any> = {
needSerialize?: boolean;
} & T;
export type SimpleObject = Record<string, any>;
export type Run<T extends SimpleObject = {}> = (ctx: RouteContext<T>) => Promise<typeof ctx | null | void>;
export type Run<T extends SimpleObject = {}> = (ctx: Required<RouteContext<T>>) => Promise<typeof ctx | null | void>;
export type NextRoute = Pick<Route, 'id' | 'path' | 'key'>;
export type RouteMiddleware =
@@ -90,6 +92,27 @@ export type RouteOpts<U = {}, T = SimpleObject> = {
};
export type DefineRouteOpts = Omit<RouteOpts, 'idUsePath' | 'nextRoute'>;
const pickValue = ['path', 'key', 'id', 'description', 'type', 'middleware', 'metadata'] as const;
export type Skill<T = SimpleObject> = {
skill: string;
title: string;
summary?: string;
args?: {
[key: string]: any
};
} & T
export const tool = {
schema: z
}
/** */
export const createSkill = <T = SimpleObject>(skill: Skill<T>): Skill<T> => {
return {
args: {},
...skill
};
}
export type RouteInfo = Pick<Route, (typeof pickValue)[number]>;
export class Route<U = { [key: string]: any }, T extends SimpleObject = SimpleObject> {
/**
@@ -332,7 +355,7 @@ export class QueryRouter {
const middleware = routeMiddleware[i];
if (middleware) {
try {
await middleware.run(ctx);
await middleware.run(ctx as Required<RouteContext>);
} catch (e) {
if (route?.isDebug) {
console.error('=====debug====:middlerware error');
@@ -362,7 +385,7 @@ export class QueryRouter {
if (route) {
if (route.run) {
try {
await route.run(ctx);
await route.run(ctx as Required<RouteContext>);
} catch (e) {
if (route?.isDebug) {
console.error('=====debug====:', 'router run error:', e.message);
@@ -591,7 +614,7 @@ export class QueryRouter {
}
/**
* 等待程序运行, 获取到message的数据,就执行
*
* params 是预设参数
* emitter = process
* -- .exit
* -- .on
@@ -608,7 +631,7 @@ export class QueryRouter {
if (getList) {
this.createRouteList(opts?.force ?? false, opts?.filter);
}
return listenProcess({ app: this, params, ...opts });
return listenProcess({ app: this as any, params, ...opts });
}
}
@@ -641,15 +664,9 @@ export class QueryRouterServer extends QueryRouter {
setHandle(wrapperFn?: HandleFn, ctx?: RouteContext) {
this.handle = this.getHandle(this, wrapperFn, ctx);
}
use(path: string, fn: (ctx: any) => any, opts?: RouteOpts) {
const route = new Route(path, '', opts);
route.run = fn;
this.add(route);
}
addRoute(route: Route) {
this.add(route);
}
Route = Route;
route(opts: RouteOpts): Route<Required<RouteContext>>;
route(path: string, key?: string): Route<Required<RouteContext>>;

View File

@@ -2,6 +2,8 @@ import { pathToRegexp, Key } from 'path-to-regexp';
import type { IncomingMessage, ServerResponse, Server } from 'node:http';
import { parseBody, parseSearch, parseSearchValue } from './server/parse-body.ts';
import { ListenOptions } from 'node:net';
// import { Hono } from 'hono'
// const app = new Hono()
type Req = IncomingMessage & { params?: Record<string, string> };
type SimpleObject = {

16
src/test/mini.ts Normal file
View File

@@ -0,0 +1,16 @@
import { Mini } from "../route.ts";
const app = new Mini();
app.route({
path: 'main',
id: 'abc',
description: '这是一个测试的 main 路由'
}).define(async (ctx) => {
ctx.body = {
a: '123'
}
}).addTo(app)
app.wait()

59
src/test/run-mini.ts Normal file
View File

@@ -0,0 +1,59 @@
import { fork } from 'child_process'
export type RunCodeParams = {
path?: string;
key?: string;
payload?: string;
[key: string]: any
}
type RunCode = {
// 调用进程的功能
success?: boolean
data?: {
// 调用router的结果
code?: number
data?: any
message?: string
[key: string]: any
};
error?: any
timestamp?: string
[key: string]: any
}
export const runCode = async (tsPath: string, params: RunCodeParams = {}): Promise<RunCode> => {
return new Promise((resolve, reject) => {
// 使用 Bun 的 fork 模式启动子进程
const child = fork(tsPath)
// 监听来自子进程的消息
child.on('message', (msg: RunCode) => {
resolve(msg)
})
// child.on('exit', (code, signal) => {
// console.log('子进程已退出,退出码:', code, '信号:', signal)
// })
// child.on('close', (code, signal) => {
// console.log('子进程已关闭,退出码:', code, '信号:', signal)
// })
child.on('error', (error) => {
resolve({
success: false, error: error?.message
})
})
// 向子进程发送消息
child.send(params)
});
}
import path from 'node:path'
const res = await runCode(path.join(process.cwd(), './src/test/mini.ts'), {
// path: 'main'
// id: 'abc'
path: 'router',
key: 'list'
})
console.log('res', res.data.data.list)

View File

@@ -1,23 +1,63 @@
import { EventEmitter } from "eventemitter3";
import { QueryRouterServer } from "../route.ts"
export class MockProcess {
emitter?: EventEmitter
process?: NodeJS.Process;
constructor(opts?: { emitter?: EventEmitter, isNode?: boolean }) {
this.emitter = opts?.emitter || new EventEmitter();
const isNode = opts?.isNode ?? true;
if (isNode) {
this.process = globalThis?.process;
}
}
send(data?: any, callback?: (err?: Error) => void) {
if (this.process) {
this.process?.send?.(data, (err?: Error) => {
callback(err)
})
}
this.emitter.emit('send', data)
}
exit(flag: number = 0) {
if (this.process) {
this.process?.exit?.(flag)
}
this.emitter.emit('exit', flag)
}
on(fn: (msg?: any) => any) {
if (this.process) {
this.process.on('message', fn)
}
this.emitter.on('message', fn)
}
desctroy() {
if (this.emitter) {
this.emitter = undefined;
}
this.process = undefined;
}
}
export type ListenProcessOptions = {
app?: any; // 传入的应用实例
emitter?: any; // 可选的事件发射器
app?: QueryRouterServer; // 传入的应用实例
mockProcess?: MockProcess; // 可选的事件发射器
params?: any; // 可选的参数
timeout?: number; // 可选的超时时间 (单位: 毫秒)
timeout?: number; // 可选的超时时间 (单位: 毫秒) 默认 10 分钟
};
export const listenProcess = async ({ app, emitter, params, timeout = 10 * 60 * 60 * 1000 }: ListenProcessOptions) => {
const process = emitter || globalThis.process;
export const listenProcess = async ({ app, mockProcess, params, timeout = 10 * 60 * 60 * 1000 }: ListenProcessOptions) => {
const process = mockProcess || new MockProcess();
let isEnd = false;
const timer = setTimeout(() => {
if (isEnd) return;
isEnd = true;
process.send?.({ success: false, error: 'Timeout' });
process.send?.({ success: false, error: 'Timeout' }, () => {
process.exit?.(1);
});
}, timeout);
// 监听来自主进程的消息
const getParams = async (): Promise<any> => {
return new Promise((resolve) => {
process.on('message', (msg) => {
process.on((msg) => {
if (isEnd) return;
isEnd = true;
clearTimeout(timer);
@@ -27,9 +67,23 @@ export const listenProcess = async ({ app, emitter, params, timeout = 10 * 60 *
}
try {
const { path = 'main', ...rest } = await getParams()
/**
* 如果不提供path默认是main
*/
const {
payload = {},
...rest
} = await getParams()
const msg = { ...params, ...rest, payload: { ...params?.payload, ...payload } }
/**
* 如果没有提供path和id默认取第一个路由, 而且路由path不是router的
*/
if (!msg.path && !msg.id) {
const route = app.routes.find(r => r.path !== 'router')
msg.id = route?.id
}
// 执行主要逻辑
const result = await app.queryRoute({ path, ...rest, ...params })
const result = await app.run(msg)
// 发送结果回主进程
const response = {
success: true,
@@ -37,14 +91,15 @@ export const listenProcess = async ({ app, emitter, params, timeout = 10 * 60 *
timestamp: new Date().toISOString()
}
process.send?.(response, (error) => {
process.send?.(response, () => {
process.exit?.(0)
})
} catch (error) {
process.send?.({
success: false,
error: error.message
})
}, () => {
process.exit?.(1)
})
}
}