feat: 更新静态资源代理文档,优化路由和插件集成,提升代码可读性和功能性

This commit is contained in:
2026-01-21 01:44:58 +08:00
parent 61add1aad1
commit 7dcf53fb4f
16 changed files with 487 additions and 69 deletions

View File

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

View File

@@ -1,4 +1,5 @@
## 兼容服务器
## 兼容服务器静态资源代理
```ts
import { App } from '@kevisual/router';

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,20 +1,21 @@
{
"$schema": "https://json.schemastore.org/package",
"name": "@kevisual/router",
"version": "0.0.58",
"version": "0.0.60",
"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"
],
@@ -28,7 +29,7 @@
"@kevisual/local-proxy": "^0.0.8",
"@kevisual/query": "^0.0.35",
"@kevisual/use-config": "^1.0.28",
"@opencode-ai/plugin": "^1.1.26",
"@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",
@@ -39,6 +40,7 @@
"@types/ws": "^8.18.1",
"@types/xml2js": "^0.4.14",
"eventemitter3": "^5.0.4",
"fast-glob": "^3.3.3",
"nanoid": "^5.1.6",
"path-to-regexp": "^8.3.0",
"rollup": "^4.55.2",
@@ -80,6 +82,7 @@
"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",

110
pnpm-lock.yaml generated
View File

@@ -28,8 +28,8 @@ importers:
specifier: ^1.0.28
version: 1.0.28(dotenv@17.2.3)
'@opencode-ai/plugin':
specifier: ^1.1.26
version: 1.1.26
specifier: ^1.1.27
version: 1.1.27
'@rollup/plugin-alias':
specifier: ^6.0.0
version: 6.0.0(rollup@4.55.2)
@@ -60,6 +60,9 @@ importers:
eventemitter3:
specifier: ^5.0.4
version: 5.0.4
fast-glob:
specifier: ^3.3.3
version: 3.3.3
nanoid:
specifier: ^5.1.6
version: 5.1.6
@@ -329,11 +332,23 @@ packages:
resolution: {integrity: sha512-jlFxSlXUEz93cFW+UYT5BXv/rFVgiMQnIfqRYZ0gj1hSP8PMGRqMqUoHSLfKvfRRS4jseLSvTTeEKSQpZJtURg==}
engines: {node: '>=10.0.0'}
'@opencode-ai/plugin@1.1.26':
resolution: {integrity: sha512-GnhLZuw9NHDJwY5msUZnFIWiLc0SnoBXWZNfnWF5+hu0fcVLgul8gqB2VK4kUv+KOQlzCVMILYikSSQkMYp7RQ==}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
'@opencode-ai/sdk@1.1.26':
resolution: {integrity: sha512-5s+yxNJy7DpWwDq0L//F2sqKERz4640DmDLyrRlFXmckx5prnzFl3liEOOTySOL2WaxgVwjYw8OBiT15v2mAgA==}
'@nodelib/fs.stat@2.0.5':
resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
engines: {node: '>= 8'}
'@nodelib/fs.walk@1.2.8':
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@opencode-ai/plugin@1.1.27':
resolution: {integrity: sha512-EevLVaEhQ1jTLNRbQJj18tFZaVNJcZZcVqvZEbDSe17CfmVRv3FQNKRAjD/QHwb+Kym7sn+LAZxD7aYIPPelvQ==}
'@opencode-ai/sdk@1.1.27':
resolution: {integrity: sha512-ssRZpET3zUNdk1GuF6HwFkNHhCXSTG0lhuPmw9HjifTwv1EVrn8gz7jAuME2OCvUSBvRTesH6Lb0Xt78Qbhzww==}
'@rollup/plugin-alias@6.0.0':
resolution: {integrity: sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g==}
@@ -777,9 +792,16 @@ packages:
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-glob@3.3.3:
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
engines: {node: '>=8.6.0'}
fast-uri@3.1.0:
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
fastq@1.20.1:
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@@ -808,6 +830,10 @@ packages:
get-tsconfig@4.13.0:
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
glob-to-regexp@0.4.1:
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
@@ -837,6 +863,14 @@ packages:
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
engines: {node: '>= 0.4'}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-module@1.0.0:
resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==}
@@ -873,6 +907,10 @@ packages:
merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
@@ -928,6 +966,9 @@ packages:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
randombytes@2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
@@ -947,6 +988,10 @@ packages:
engines: {node: '>= 0.4'}
hasBin: true
reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
rollup-plugin-dts@6.3.0:
resolution: {integrity: sha512-d0UrqxYd8KyZ6i3M2Nx7WOMy708qsV/7fTHMHxCMCBOAe3V/U7OMPu5GkX8hC+cmkHhzGnfeYongl1IgiooddA==}
engines: {node: '>=16'}
@@ -959,6 +1004,9 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
@@ -1265,12 +1313,24 @@ snapshots:
'@kevisual/ws@8.0.0': {}
'@opencode-ai/plugin@1.1.26':
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@opencode-ai/sdk': 1.1.26
'@nodelib/fs.stat': 2.0.5
run-parallel: 1.2.0
'@nodelib/fs.stat@2.0.5': {}
'@nodelib/fs.walk@1.2.8':
dependencies:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.20.1
'@opencode-ai/plugin@1.1.27':
dependencies:
'@opencode-ai/sdk': 1.1.27
zod: 4.1.8
'@opencode-ai/sdk@1.1.26': {}
'@opencode-ai/sdk@1.1.27': {}
'@rollup/plugin-alias@6.0.0(rollup@4.55.2)':
optionalDependencies:
@@ -1670,8 +1730,20 @@ snapshots:
fast-deep-equal@3.1.3: {}
fast-glob@3.3.3:
dependencies:
'@nodelib/fs.stat': 2.0.5
'@nodelib/fs.walk': 1.2.8
glob-parent: 5.1.2
merge2: 1.4.1
micromatch: 4.0.8
fast-uri@3.1.0: {}
fastq@1.20.1:
dependencies:
reusify: 1.1.0
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
@@ -1691,6 +1763,10 @@ snapshots:
dependencies:
resolve-pkg-maps: 1.0.0
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
glob-to-regexp@0.4.1: {}
graceful-fs@4.2.11: {}
@@ -1717,6 +1793,12 @@ snapshots:
dependencies:
hasown: 2.0.2
is-extglob@2.1.1: {}
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
is-module@1.0.0: {}
is-number@7.0.0: {}
@@ -1748,6 +1830,8 @@ snapshots:
merge-stream@2.0.0: {}
merge2@1.4.1: {}
micromatch@4.0.8:
dependencies:
braces: 3.0.3
@@ -1787,6 +1871,8 @@ snapshots:
picomatch@4.0.3: {}
queue-microtask@1.2.3: {}
randombytes@2.1.0:
dependencies:
safe-buffer: 5.2.1
@@ -1803,6 +1889,8 @@ snapshots:
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
reusify@1.1.0: {}
rollup-plugin-dts@6.3.0(rollup@4.55.2)(typescript@5.9.3):
dependencies:
magic-string: 0.30.21
@@ -1842,6 +1930,10 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.55.2
fsevents: 2.3.3
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
safe-buffer@5.2.1: {}
sax@1.4.3: {}

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

@@ -4,7 +4,7 @@ import { type App } from './app.ts'
import { type Plugin } from "@opencode-ai/plugin"
import { filter } from '@kevisual/js-filter';
export const addCallFn = (app: QueryRouterServer) => {
export const addCallFn = (app: App) => {
app.route({
path: 'call',
key: '',
@@ -35,20 +35,23 @@ export const addCallFn = (app: QueryRouterServer) => {
}).addTo(app)
}
export const createRouterAgentPluginFn = (opts?: {
router?: QueryRouter,
router?: App | QueryRouterServer,
//** 过滤比如WHERE metadata.tags includes 'opencode' */
query?: string
}) => {
let router = opts?.router
if (!router) {
const app = useContextKey<App>('app')
router = app.router
router = app
}
if (!router) {
throw new Error('Router 参数缺失')
}
if (!router.hasRoute('call', '')) {
addCallFn(router as QueryRouterServer)
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 => {
@@ -88,6 +91,7 @@ export const createRouterAgentPluginFn = (opts?: {
}
return str;
}
console.error('调用出错', res);
return `Error: ${res?.message || '无法获取结果'}`;
}
}

View File

@@ -60,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 =
@@ -355,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');
@@ -385,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);
@@ -664,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>>;