feat: 更新静态资源代理文档,优化路由和插件集成,提升代码可读性和功能性
This commit is contained in:
5
agent/app.ts
Normal file
5
agent/app.ts
Normal 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
42
agent/gen.ts
Normal 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
205
agent/gen/index.ts
Normal 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
6
agent/main.ts
Normal 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
14
agent/routes/index.ts
Normal 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);
|
||||
}
|
||||
|
||||
45
agent/routes/route-create.ts
Normal file
45
agent/routes/route-create.ts
Normal 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);
|
||||
Reference in New Issue
Block a user