Compare commits
20 Commits
5dcc678ad4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e7dca513f3 | |||
| 0f8986b491 | |||
| 911f03b4bd | |||
| 8a1aff4c50 | |||
| 3284a06ad5 | |||
| 7dcf53fb4f | |||
| 61add1aad1 | |||
| 2363617404 | |||
| f4372ae55f | |||
|
|
9b11ea5138 | ||
| 999397611c | |||
| 730f4c6eb9 | |||
|
|
123d02f452 | ||
|
|
309446c864 | ||
| 2c57435a81 | |||
| cd785ddb51 | |||
| a8da85e0bd | |||
| 9f53c9e18c | |||
| 78af49906e | |||
| 03f24318d2 |
40
.cnb.yml
Normal file
40
.cnb.yml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# .cnb.yml
|
||||||
|
include:
|
||||||
|
- https://cnb.cool/kevisual/cnb/-/blob/main/.cnb/template.yml
|
||||||
|
|
||||||
|
.common_env: &common_env
|
||||||
|
env:
|
||||||
|
TO_REPO: kevisual/router
|
||||||
|
TO_URL: git.xiongxiao.me
|
||||||
|
imports:
|
||||||
|
- https://cnb.cool/kevisual/env/-/blob/main/.env.development
|
||||||
|
|
||||||
|
$:
|
||||||
|
vscode:
|
||||||
|
- docker:
|
||||||
|
image: docker.cnb.cool/kevisual/dev-env:latest
|
||||||
|
services:
|
||||||
|
- vscode
|
||||||
|
- docker
|
||||||
|
imports: !reference [.common_env, imports]
|
||||||
|
stages: !reference [.dev_tempalte, stages]
|
||||||
|
|
||||||
|
.common_sync_to_gitea: &common_sync_to_gitea
|
||||||
|
- <<: *common_env
|
||||||
|
services: !reference [.common_sync_to_gitea_template, services]
|
||||||
|
stages: !reference [.common_sync_to_gitea_template, stages]
|
||||||
|
|
||||||
|
.common_sync_from_gitea: &common_sync_from_gitea
|
||||||
|
- <<: *common_env
|
||||||
|
services: !reference [.common_sync_from_gitea_template, services]
|
||||||
|
stages: !reference [.common_sync_from_gitea_template, stages]
|
||||||
|
|
||||||
|
main:
|
||||||
|
web_trigger_sync_to_gitea:
|
||||||
|
- <<: *common_sync_to_gitea
|
||||||
|
web_trigger_sync_from_gitea:
|
||||||
|
- <<: *common_sync_from_gitea
|
||||||
|
api_trigger_sync_to_gitea:
|
||||||
|
- <<: *common_sync_to_gitea
|
||||||
|
api_trigger_sync_from_gitea:
|
||||||
|
- <<: *common_sync_from_gitea
|
||||||
11
.cnb/web_trigger.yml
Normal file
11
.cnb/web_trigger.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# .cnb/web_trigger.yml
|
||||||
|
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
|
||||||
38
.github/workflows/publish.yml
vendored
38
.github/workflows/publish.yml
vendored
@@ -1,38 +0,0 @@
|
|||||||
name: Publish to npm
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*.*.*' # 当推送带有版本号的 tag 时触发,例如 v1.0.0
|
|
||||||
workflow_dispatch: # 添加手动触发器
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
# Step 1: Clone current Git repository
|
|
||||||
- name: Checkout this repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
# Step 3: Setup Node.js and install dependencies
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: '20.6'
|
|
||||||
registry-url: 'https://registry.npmjs.org/'
|
|
||||||
cache: 'npm' # 启用 npm 缓存,提高安装速度
|
|
||||||
- name: Configure npm authentication
|
|
||||||
run: npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm install --no-save
|
|
||||||
- name: Build project
|
|
||||||
run: npm run build
|
|
||||||
# Step 6: 发布到 npm
|
|
||||||
- name: Publish package
|
|
||||||
run: npm publish
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
# Step 7: 发布成功后,更新版本标签
|
|
||||||
# - name: Create Git tag
|
|
||||||
# run: |
|
|
||||||
# TAG="v$(node -p -e "require('./package.json').version")"
|
|
||||||
# git tag $TAG
|
|
||||||
# git push origin $TAG
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,3 +6,5 @@ dist
|
|||||||
|
|
||||||
https-cert.pem
|
https-cert.pem
|
||||||
https-key.pem
|
https-key.pem
|
||||||
|
|
||||||
|
.pnpm-store
|
||||||
3
.opencode/plugin/agent.ts
Normal file
3
.opencode/plugin/agent.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { routerAgentPlugin } from "../../agent/main.ts";
|
||||||
|
|
||||||
|
export { routerAgentPlugin };
|
||||||
25
AGENTS.md
Normal file
25
AGENTS.md
Normal 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
15
STATIC.md
Normal 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
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);
|
||||||
19
auto.ts
19
auto.ts
@@ -1,19 +0,0 @@
|
|||||||
import { loadTS, getMatchFiles } from './src/auto/load-ts.ts';
|
|
||||||
import { listenSocket } from './src/auto/listen-sock.ts';
|
|
||||||
import { Route, QueryRouter, QueryRouterServer } from './src/route.ts';
|
|
||||||
|
|
||||||
export { Route, QueryRouter, QueryRouterServer };
|
|
||||||
|
|
||||||
export const App = QueryRouterServer;
|
|
||||||
|
|
||||||
export { createSchema } from './src/validator/index.ts';
|
|
||||||
export type { Rule } from './src/validator/rule.ts';
|
|
||||||
export type { RouteContext, RouteOpts } from './src/route.ts';
|
|
||||||
|
|
||||||
export type { Run } from './src/route.ts';
|
|
||||||
|
|
||||||
export { CustomError } from './src/result/error.ts';
|
|
||||||
|
|
||||||
export { listenSocket, loadTS, getMatchFiles };
|
|
||||||
|
|
||||||
export { autoCall } from './src/auto/call-sock.ts';
|
|
||||||
24
bun.config.ts
Normal file
24
bun.config.ts
Normal 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
|
||||||
@@ -1,20 +1,14 @@
|
|||||||
import { Route, App } from '@kevisual/router';
|
import { Route, App } from '@kevisual/router';
|
||||||
|
|
||||||
const app = new App();
|
const app = new App({ appId: 'abc' });
|
||||||
app.listen(4003);
|
app.listen(4003);
|
||||||
const route01 = new Route('demo', '01');
|
const route01 = new Route('demo', '00');
|
||||||
route01.run = async (ctx) => {
|
route01.run = async (ctx) => {
|
||||||
ctx.body = '01';
|
ctx.body = '00';
|
||||||
|
console.log('appId', ctx.app.appId, ctx);
|
||||||
return ctx;
|
return ctx;
|
||||||
};
|
};
|
||||||
app.use(
|
app.addRoute(route01);
|
||||||
'demo',
|
|
||||||
async (ctx) => {
|
|
||||||
ctx.body = '01';
|
|
||||||
return ctx;
|
|
||||||
},
|
|
||||||
{ key: '01' },
|
|
||||||
);
|
|
||||||
|
|
||||||
const route02 = new Route('demo', '02');
|
const route02 = new Route('demo', '02');
|
||||||
route02.run = async (ctx) => {
|
route02.run = async (ctx) => {
|
||||||
@@ -25,3 +19,10 @@ app.addRoute(route02);
|
|||||||
|
|
||||||
console.log(`http://localhost:4003/api/router?path=demo&key=02`);
|
console.log(`http://localhost:4003/api/router?path=demo&key=02`);
|
||||||
console.log(`http://localhost:4003/api/router?path=demo&key=01`);
|
console.log(`http://localhost:4003/api/router?path=demo&key=01`);
|
||||||
|
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
await wait(1000);
|
||||||
|
const a = await app.run({
|
||||||
|
path: 'demo',
|
||||||
|
key: '00',
|
||||||
|
})
|
||||||
|
console.log('a', a);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { QueryRouter, Route, Server } from '@kevisual/router';
|
import { QueryRouter, Route, } from '@kevisual/router';
|
||||||
|
// Server
|
||||||
|
|
||||||
const router = new QueryRouter();
|
const router = new QueryRouter();
|
||||||
|
|
||||||
@@ -9,14 +10,14 @@ route01.run = async (ctx) => {
|
|||||||
};
|
};
|
||||||
router.add(route01);
|
router.add(route01);
|
||||||
|
|
||||||
const server = new Server({
|
// const server = new Server({
|
||||||
handle: async (msg) => {
|
// handle: async (msg) => {
|
||||||
const res = await router.parse(msg);
|
// const res = await router.parse(msg);
|
||||||
const { code, body, message } = res;
|
// const { code, body, message } = res;
|
||||||
// console.log('response', res);
|
// // console.log('response', res);
|
||||||
return { code, data: body, message };
|
// return { code, data: body, message };
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
// server.setHandle(async (msg) => {
|
// server.setHandle(async (msg) => {
|
||||||
// const res = await router.parse(msg);
|
// const res = await router.parse(msg);
|
||||||
@@ -25,7 +26,7 @@ const server = new Server({
|
|||||||
// return { code, data: body, message };
|
// return { code, data: body, message };
|
||||||
// });
|
// });
|
||||||
|
|
||||||
server.listen(3000);
|
// server.listen(3000);
|
||||||
|
|
||||||
const route02 = new Route('demo', '02');
|
const route02 = new Route('demo', '02');
|
||||||
route02.run = async (ctx) => {
|
route02.run = async (ctx) => {
|
||||||
|
|||||||
15
docs/examples/base.md
Normal file
15
docs/examples/base.md
Normal 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);
|
||||||
|
|
||||||
|
```
|
||||||
45
package.json
45
package.json
@@ -1,60 +1,66 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package",
|
"$schema": "https://json.schemastore.org/package",
|
||||||
"name": "@kevisual/router",
|
"name": "@kevisual/router",
|
||||||
"version": "0.0.52",
|
"version": "0.0.62",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/router.js",
|
"main": "./dist/router.js",
|
||||||
"types": "./dist/router.d.ts",
|
"types": "./dist/router.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run clean && rollup -c",
|
"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",
|
"watch": "rollup -c -w",
|
||||||
"clean": "rm -rf dist"
|
"clean": "rm -rf dist"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
"src",
|
"src",
|
||||||
|
"agent",
|
||||||
"auto.ts",
|
"auto.ts",
|
||||||
"mod.ts"
|
"mod.ts"
|
||||||
],
|
],
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "abearxiong",
|
"author": "abearxiong",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"packageManager": "pnpm@10.26.2",
|
"packageManager": "pnpm@10.28.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@kevisual/context": "^0.0.4",
|
||||||
|
"@kevisual/js-filter": "^0.0.5",
|
||||||
"@kevisual/local-proxy": "^0.0.8",
|
"@kevisual/local-proxy": "^0.0.8",
|
||||||
"@kevisual/query": "^0.0.33",
|
"@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-alias": "^6.0.0",
|
||||||
"@rollup/plugin-commonjs": "29.0.0",
|
"@rollup/plugin-commonjs": "29.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^16.0.3",
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||||
"@rollup/plugin-typescript": "^12.3.0",
|
"@rollup/plugin-typescript": "^12.3.0",
|
||||||
"@types/bun": "^1.3.5",
|
"@types/bun": "^1.3.6",
|
||||||
"@types/node": "^25.0.3",
|
"@types/node": "^25.0.9",
|
||||||
"@types/send": "^1.2.1",
|
"@types/send": "^1.2.1",
|
||||||
"@types/xml2js": "^0.4.14",
|
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
|
"@types/xml2js": "^0.4.14",
|
||||||
|
"eventemitter3": "^5.0.4",
|
||||||
|
"fast-glob": "^3.3.3",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"rollup": "^4.54.0",
|
"path-to-regexp": "^8.3.0",
|
||||||
|
"rollup": "^4.55.2",
|
||||||
"rollup-plugin-dts": "^6.3.0",
|
"rollup-plugin-dts": "^6.3.0",
|
||||||
|
"send": "^1.2.1",
|
||||||
"ts-loader": "^9.5.4",
|
"ts-loader": "^9.5.4",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
|
"ws": "npm:@kevisual/ws",
|
||||||
"xml2js": "^0.6.2",
|
"xml2js": "^0.6.2",
|
||||||
"zod": "^4.2.1",
|
"zod": "^4.3.5"
|
||||||
"eventemitter3": "^5.0.1",
|
|
||||||
"ws": "npm:@kevisual/ws"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/abearxiong/kevisual-router.git"
|
"url": "git+https://github.com/abearxiong/kevisual-router.git"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"path-to-regexp": "^8.3.0",
|
"hono": "^4.11.4"
|
||||||
"selfsigned": "^5.4.0",
|
|
||||||
"send": "^1.2.1"
|
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
@@ -70,16 +76,13 @@
|
|||||||
"require": "./dist/router-browser.js",
|
"require": "./dist/router-browser.js",
|
||||||
"types": "./dist/router-browser.d.ts"
|
"types": "./dist/router-browser.d.ts"
|
||||||
},
|
},
|
||||||
"./sign": {
|
|
||||||
"import": "./dist/router-sign.js",
|
|
||||||
"require": "./dist/router-sign.js",
|
|
||||||
"types": "./dist/router-sign.d.ts"
|
|
||||||
},
|
|
||||||
"./simple": {
|
"./simple": {
|
||||||
"import": "./dist/router-simple.js",
|
"import": "./dist/router-simple.js",
|
||||||
"require": "./dist/router-simple.js",
|
"require": "./dist/router-simple.js",
|
||||||
"types": "./dist/router-simple.d.ts"
|
"types": "./dist/router-simple.d.ts"
|
||||||
},
|
},
|
||||||
|
"./opencode": "./dist/opencode.js",
|
||||||
|
"./skill": "./dist/app.js",
|
||||||
"./define": {
|
"./define": {
|
||||||
"import": "./dist/router-define.js",
|
"import": "./dist/router-define.js",
|
||||||
"require": "./dist/router-define.js",
|
"require": "./dist/router-define.js",
|
||||||
@@ -93,6 +96,10 @@
|
|||||||
"./src/*": {
|
"./src/*": {
|
||||||
"import": "./src/*",
|
"import": "./src/*",
|
||||||
"require": "./src/*"
|
"require": "./src/*"
|
||||||
|
},
|
||||||
|
"./modules/*": {
|
||||||
|
"import": "./src/modules/*",
|
||||||
|
"require": "./src/modules/*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
683
pnpm-lock.yaml
generated
683
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
172
readme.md
172
readme.md
@@ -1,5 +1,9 @@
|
|||||||
# router
|
# router
|
||||||
|
|
||||||
|
一个轻量级的路由框架,支持链式调用、中间件、嵌套路由等功能。
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { App } from '@kevisual/router';
|
import { App } from '@kevisual/router';
|
||||||
|
|
||||||
@@ -7,30 +11,178 @@ const app = new App();
|
|||||||
app.listen(4002);
|
app.listen(4002);
|
||||||
|
|
||||||
app
|
app
|
||||||
.route({path:'demo', key: '02})
|
.route({ path: 'demo', key: '02' })
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
ctx.body = '02';
|
ctx.body = '02';
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
|
|
||||||
app
|
app
|
||||||
.route('demo', '03')
|
.route({ path: 'demo', key: '03' })
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
ctx.body = '03';
|
ctx.body = '03';
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.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';
|
import { App } from '@kevisual/router';
|
||||||
|
|
||||||
const app = new App();
|
const app = new App();
|
||||||
app.listen(4002);
|
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**,错误信息中会包含找不到的中间件列表。
|
||||||
|
|||||||
@@ -81,29 +81,6 @@ export default [
|
|||||||
},
|
},
|
||||||
plugins: [dts()],
|
plugins: [dts()],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
input: 'src/sign.ts',
|
|
||||||
output: {
|
|
||||||
file: 'dist/router-sign.js',
|
|
||||||
format: 'es',
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
createAlias(),
|
|
||||||
resolve({
|
|
||||||
browser: false,
|
|
||||||
}),
|
|
||||||
commonjs(),
|
|
||||||
typescript(),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'src/sign.ts',
|
|
||||||
output: {
|
|
||||||
file: 'dist/router-sign.d.ts',
|
|
||||||
format: 'es',
|
|
||||||
},
|
|
||||||
plugins: [dts()],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
input: 'src/router-define.ts',
|
input: 'src/router-define.ts',
|
||||||
output: {
|
output: {
|
||||||
@@ -149,4 +126,26 @@ export default [
|
|||||||
},
|
},
|
||||||
plugins: [dts()],
|
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()],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
49
src/app.ts
49
src/app.ts
@@ -2,11 +2,11 @@ import { QueryRouter, Route, RouteContext, RouteOpts } from './route.ts';
|
|||||||
import { ServerNode, ServerNodeOpts } from './server/server.ts';
|
import { ServerNode, ServerNodeOpts } from './server/server.ts';
|
||||||
import { HandleCtx } from './server/server-base.ts';
|
import { HandleCtx } from './server/server-base.ts';
|
||||||
import { ServerType } from './server/server-type.ts';
|
import { ServerType } from './server/server-type.ts';
|
||||||
import { CustomError } from './result/error.ts';
|
|
||||||
import { handleServer } from './server/handle-server.ts';
|
import { handleServer } from './server/handle-server.ts';
|
||||||
import { IncomingMessage, ServerResponse } from 'http';
|
import { IncomingMessage, ServerResponse } from 'http';
|
||||||
import { isBun } from './utils/is-engine.ts';
|
import { isBun } from './utils/is-engine.ts';
|
||||||
import { BunServer } from './server/server-bun.ts';
|
import { BunServer } from './server/server-bun.ts';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
type RouterHandle = (msg: { path: string;[key: string]: any }) => { code: string; data?: any; message?: string;[key: string]: any };
|
type RouterHandle = (msg: { path: string;[key: string]: any }) => { code: string; data?: any; message?: string;[key: string]: any };
|
||||||
type AppOptions<T = {}> = {
|
type AppOptions<T = {}> = {
|
||||||
@@ -16,6 +16,7 @@ type AppOptions<T = {}> = {
|
|||||||
routerHandle?: RouterHandle;
|
routerHandle?: RouterHandle;
|
||||||
routerContext?: RouteContext<T>;
|
routerContext?: RouteContext<T>;
|
||||||
serverOptions?: ServerNodeOpts;
|
serverOptions?: ServerNodeOpts;
|
||||||
|
appId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AppRouteContext<T = {}> = HandleCtx & RouteContext<T> & { app: App<T> };
|
export type AppRouteContext<T = {}> = HandleCtx & RouteContext<T> & { app: App<T> };
|
||||||
@@ -24,11 +25,13 @@ export type AppRouteContext<T = {}> = HandleCtx & RouteContext<T> & { app: App<T
|
|||||||
* 封装了 Router 和 Server 的 App 模块,处理http的请求和响应,内置了 Cookie 和 Token 和 res 的处理
|
* 封装了 Router 和 Server 的 App 模块,处理http的请求和响应,内置了 Cookie 和 Token 和 res 的处理
|
||||||
* U - Route Context的扩展类型
|
* U - Route Context的扩展类型
|
||||||
*/
|
*/
|
||||||
export class App<U = {}> {
|
export class App<U = {}> extends QueryRouter {
|
||||||
|
declare appId: string;
|
||||||
router: QueryRouter;
|
router: QueryRouter;
|
||||||
server: ServerType;
|
server: ServerType;
|
||||||
constructor(opts?: AppOptions<U>) {
|
constructor(opts?: AppOptions<U>) {
|
||||||
const router = opts?.router || new QueryRouter();
|
super();
|
||||||
|
const router = this;
|
||||||
let server = opts?.server;
|
let server = opts?.server;
|
||||||
if (!server) {
|
if (!server) {
|
||||||
const serverOptions = opts?.serverOptions || {};
|
const serverOptions = opts?.serverOptions || {};
|
||||||
@@ -42,6 +45,12 @@ export class App<U = {}> {
|
|||||||
router.setContext({ needSerialize: true, ...opts?.routerContext });
|
router.setContext({ needSerialize: true, ...opts?.routerContext });
|
||||||
this.router = router;
|
this.router = router;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
|
if (opts?.appId) {
|
||||||
|
this.appId = opts.appId;
|
||||||
|
} else {
|
||||||
|
this.appId = nanoid(16);
|
||||||
|
}
|
||||||
|
router.appId = this.appId;
|
||||||
}
|
}
|
||||||
listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void): void;
|
listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void): void;
|
||||||
listen(port: number, hostname?: string, listeningListener?: () => void): void;
|
listen(port: number, hostname?: string, listeningListener?: () => void): void;
|
||||||
@@ -55,15 +64,9 @@ export class App<U = {}> {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.server.listen(...args);
|
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) {
|
addRoute(route: Route) {
|
||||||
this.router.add(route);
|
super.add(route);
|
||||||
}
|
}
|
||||||
add = this.addRoute;
|
|
||||||
|
|
||||||
Route = Route;
|
Route = Route;
|
||||||
route(opts: RouteOpts<AppRouteContext<U>>): Route<AppRouteContext<U>>;
|
route(opts: RouteOpts<AppRouteContext<U>>): Route<AppRouteContext<U>>;
|
||||||
@@ -100,30 +103,10 @@ export class App<U = {}> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async call(message: { id?: string, path?: string; key?: string; payload?: any }, ctx?: AppRouteContext<U> & { [key: string]: any }) {
|
async call(message: { id?: string, path?: string; key?: string; payload?: any }, ctx?: AppRouteContext<U> & { [key: string]: any }) {
|
||||||
const router = this.router;
|
return await super.call(message, ctx);
|
||||||
return await router.call(message, ctx);
|
|
||||||
}
|
}
|
||||||
/**
|
async run(msg: { id?: string, path?: string; key?: string; payload?: any }, ctx?: Partial<AppRouteContext<U>> & { [key: string]: any }) {
|
||||||
* @deprecated
|
return await super.run(msg, ctx);
|
||||||
*/
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
static handleRequest(req: IncomingMessage, res: ServerResponse) {
|
static handleRequest(req: IncomingMessage, res: ServerResponse) {
|
||||||
return handleServer(req, res);
|
return handleServer(req, res);
|
||||||
|
|||||||
19
src/auto/index.ts
Normal file
19
src/auto/index.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { loadTS, getMatchFiles } from './load-ts.ts';
|
||||||
|
import { listenSocket } from './listen-sock.ts';
|
||||||
|
import { Route, QueryRouter, QueryRouterServer } from '../route.ts';
|
||||||
|
|
||||||
|
export { Route, QueryRouter, QueryRouterServer };
|
||||||
|
|
||||||
|
export const App = QueryRouterServer;
|
||||||
|
|
||||||
|
export { createSchema } from './../index.ts';
|
||||||
|
export type { Rule } from '../validator/rule.ts';
|
||||||
|
export type { RouteContext, RouteOpts } from '../route.ts';
|
||||||
|
|
||||||
|
export type { Run } from '../route.ts';
|
||||||
|
|
||||||
|
export { CustomError } from '../result/error.ts';
|
||||||
|
|
||||||
|
export { listenSocket, loadTS, getMatchFiles };
|
||||||
|
|
||||||
|
export { autoCall } from './call-sock.ts';
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
export { Route, QueryRouter, QueryRouterServer, Mini } from './route.ts';
|
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 { 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 { CustomError } from './result/error.ts';
|
||||||
|
|
||||||
export * from './server/parse-body.ts';
|
|
||||||
|
|
||||||
export * from './router-define.ts';
|
export * from './router-define.ts';
|
||||||
|
|
||||||
|
export { MockProcess } from './utils/listen-process.ts'
|
||||||
|
// --- 以上同步更新至 browser.ts ---
|
||||||
40
src/chat.ts
40
src/chat.ts
@@ -1,40 +0,0 @@
|
|||||||
import { QueryRouter } from "./route.ts";
|
|
||||||
|
|
||||||
type RouterChatOptions = {
|
|
||||||
router?: QueryRouter;
|
|
||||||
}
|
|
||||||
export class RouterChat {
|
|
||||||
router: QueryRouter;
|
|
||||||
prompt: string = '';
|
|
||||||
constructor(opts?: RouterChatOptions) {
|
|
||||||
this.router = opts?.router || new QueryRouter();
|
|
||||||
}
|
|
||||||
prefix(wrapperFn?: (routes: any[]) => string) {
|
|
||||||
if (this.prompt) {
|
|
||||||
return this.prompt;
|
|
||||||
}
|
|
||||||
let _prompt = `你是一个调用函数工具的助手,当用户询问时,如果拥有工具,请返回 JSON 数据,数据的值的内容是 id 和 payload 。如果有参数,请放到 payload 当中。
|
|
||||||
|
|
||||||
下面是你可以使用的工具列表:
|
|
||||||
|
|
||||||
`;
|
|
||||||
if (!wrapperFn) {
|
|
||||||
_prompt += this.router.routes.map(r => `工具名称: ${r.id}\n描述: ${r.description}\n`).join('\n');
|
|
||||||
} else {
|
|
||||||
_prompt += wrapperFn(this.router.exportRoutes());
|
|
||||||
}
|
|
||||||
_prompt += `当你需要使用工具时,请严格按照以下格式返回:
|
|
||||||
{
|
|
||||||
"id": "工具名称",
|
|
||||||
"payload": {
|
|
||||||
// 参数列表
|
|
||||||
}
|
|
||||||
}
|
|
||||||
如果你不需要使用工具,直接返回用户想要的内容即可,不要返回任何多余的信息。`;
|
|
||||||
return _prompt;
|
|
||||||
}
|
|
||||||
chat() {
|
|
||||||
const prompt = this.prefix();
|
|
||||||
return prompt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
33
src/index.ts
33
src/index.ts
@@ -1,26 +1,27 @@
|
|||||||
export { Route, QueryRouter, QueryRouterServer, Mini } from './route.ts';
|
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 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 * from './router-define.ts';
|
||||||
|
|
||||||
|
export { MockProcess } from './utils/listen-process.ts'
|
||||||
|
// --- 以上同步更新至 browser.ts ---
|
||||||
|
|
||||||
export {
|
export { ServerNode, handleServer } from './server/index.ts';
|
||||||
|
|
||||||
|
export { App } from './app.ts';
|
||||||
|
|
||||||
|
export type {
|
||||||
RouterReq,
|
RouterReq,
|
||||||
RouterRes,
|
RouterRes,
|
||||||
OnWebSocketFn,
|
OnWebSocketFn,
|
||||||
@@ -32,3 +33,5 @@ export {
|
|||||||
HttpListenerFun,
|
HttpListenerFun,
|
||||||
OnListener,
|
OnListener,
|
||||||
} from './server/server-type.ts';
|
} from './server/server-type.ts';
|
||||||
|
|
||||||
|
export { loadTS } from './auto/load-ts.ts';
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
// TODO: Implement IOApp
|
|
||||||
export class IOApp {
|
|
||||||
constructor() {
|
|
||||||
console.log('IoApp');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
57
src/modules/chat.ts
Normal file
57
src/modules/chat.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { QueryRouter } from "../route.ts";
|
||||||
|
import { filter } from '@kevisual/js-filter'
|
||||||
|
type RouterChatOptions = {
|
||||||
|
router?: QueryRouter;
|
||||||
|
}
|
||||||
|
export class RouterChat {
|
||||||
|
router: QueryRouter;
|
||||||
|
prompt: string = '';
|
||||||
|
constructor(opts?: RouterChatOptions) {
|
||||||
|
this.router = opts?.router || new QueryRouter();
|
||||||
|
}
|
||||||
|
prefix(opts?: { query?: string }) {
|
||||||
|
if (this.prompt) {
|
||||||
|
return this.prompt;
|
||||||
|
}
|
||||||
|
let _routes = this.router.routes;
|
||||||
|
if (opts?.query) {
|
||||||
|
_routes = filter(this.router.routes, opts.query);
|
||||||
|
}
|
||||||
|
const toolsList = _routes.map((r, index) =>
|
||||||
|
`${index + 1}. 工具名称: ${r.id}\n 描述: ${r.description}`
|
||||||
|
).join('\n\n');
|
||||||
|
const _prompt = `你是一个 AI 助手,你可以使用以下工具来帮助用户完成任务:
|
||||||
|
|
||||||
|
${toolsList}
|
||||||
|
|
||||||
|
## 回复规则
|
||||||
|
1. 如果用户的请求可以使用上述工具完成,请返回 JSON 格式数据
|
||||||
|
2. 如果没有合适的工具,请直接分析并回答用户问题
|
||||||
|
|
||||||
|
## JSON 数据格式
|
||||||
|
\`\`\`json
|
||||||
|
{
|
||||||
|
"id": "工具的id",
|
||||||
|
"payload": {
|
||||||
|
// 工具所需的参数(如果需要)
|
||||||
|
// 例如: "id": "xxx", "name": "xxx"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
注意:
|
||||||
|
- payload 中包含工具执行所需的所有参数
|
||||||
|
- 如果工具不需要参数,payload 可以为空对象 {}
|
||||||
|
- 确保返回的 id 与上述工具列表中的工具名称完全匹配`
|
||||||
|
|
||||||
|
this.prompt = _prompt;
|
||||||
|
return _prompt;
|
||||||
|
}
|
||||||
|
recreate() {
|
||||||
|
this.prompt = '';
|
||||||
|
}
|
||||||
|
getChatPrompt() {
|
||||||
|
const prompt = this.prefix();
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { RouteContext } from './route.ts';
|
import { RouteContext } from '../route.ts';
|
||||||
|
|
||||||
export class Connect {
|
export class Connect {
|
||||||
path: string;
|
path: string;
|
||||||
108
src/opencode.ts
Normal file
108
src/opencode.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -9,10 +9,10 @@ export class CustomError extends Error {
|
|||||||
this.name = 'CustomError';
|
this.name = 'CustomError';
|
||||||
if (typeof code === 'number') {
|
if (typeof code === 'number') {
|
||||||
this.code = code;
|
this.code = code;
|
||||||
this.message = message;
|
this.message = message!;
|
||||||
} else {
|
} else {
|
||||||
this.code = 500;
|
this.code = 500;
|
||||||
this.message = code;
|
this.message = code!;
|
||||||
}
|
}
|
||||||
this.tips = tips;
|
this.tips = tips;
|
||||||
// 这一步可不写,默认会保存堆栈追踪信息到自定义错误构造函数之前,
|
// 这一步可不写,默认会保存堆栈追踪信息到自定义错误构造函数之前,
|
||||||
|
|||||||
67
src/route.ts
67
src/route.ts
@@ -2,9 +2,16 @@ import { nanoid } from 'nanoid';
|
|||||||
import { CustomError } from './result/error.ts';
|
import { CustomError } from './result/error.ts';
|
||||||
import { pick } from './utils/pick.ts';
|
import { pick } from './utils/pick.ts';
|
||||||
import { listenProcess } from './utils/listen-process.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 RouterContextT = { code?: number;[key: string]: any };
|
||||||
export type RouteContext<T = { code?: number }, S = any> = {
|
export type RouteContext<T = { code?: number }, S = any> = {
|
||||||
|
/**
|
||||||
|
* 本地自己调用的时候使用,可以标识为当前自调用,那么 auth 就不许重复的校验
|
||||||
|
* 或者不需要登录的,直接调用
|
||||||
|
*/
|
||||||
|
appId?: string;
|
||||||
// run first
|
// run first
|
||||||
query?: { [key: string]: any };
|
query?: { [key: string]: any };
|
||||||
// response body
|
// response body
|
||||||
@@ -53,7 +60,7 @@ export type RouteContext<T = { code?: number }, S = any> = {
|
|||||||
needSerialize?: boolean;
|
needSerialize?: boolean;
|
||||||
} & T;
|
} & T;
|
||||||
export type SimpleObject = Record<string, any>;
|
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 NextRoute = Pick<Route, 'id' | 'path' | 'key'>;
|
||||||
export type RouteMiddleware =
|
export type RouteMiddleware =
|
||||||
@@ -85,6 +92,27 @@ export type RouteOpts<U = {}, T = SimpleObject> = {
|
|||||||
};
|
};
|
||||||
export type DefineRouteOpts = Omit<RouteOpts, 'idUsePath' | 'nextRoute'>;
|
export type DefineRouteOpts = Omit<RouteOpts, 'idUsePath' | 'nextRoute'>;
|
||||||
const pickValue = ['path', 'key', 'id', 'description', 'type', 'middleware', 'metadata'] as const;
|
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 type RouteInfo = Pick<Route, (typeof pickValue)[number]>;
|
||||||
export class Route<U = { [key: string]: any }, T extends SimpleObject = SimpleObject> {
|
export class Route<U = { [key: string]: any }, T extends SimpleObject = SimpleObject> {
|
||||||
/**
|
/**
|
||||||
@@ -217,6 +245,7 @@ export class Route<U = { [key: string]: any }, T extends SimpleObject = SimpleOb
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class QueryRouter {
|
export class QueryRouter {
|
||||||
|
appId: string = '';
|
||||||
routes: Route[];
|
routes: Route[];
|
||||||
maxNextRoute = 40;
|
maxNextRoute = 40;
|
||||||
context?: RouteContext = {}; // default context for call
|
context?: RouteContext = {}; // default context for call
|
||||||
@@ -326,7 +355,7 @@ export class QueryRouter {
|
|||||||
const middleware = routeMiddleware[i];
|
const middleware = routeMiddleware[i];
|
||||||
if (middleware) {
|
if (middleware) {
|
||||||
try {
|
try {
|
||||||
await middleware.run(ctx);
|
await middleware.run(ctx as Required<RouteContext>);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (route?.isDebug) {
|
if (route?.isDebug) {
|
||||||
console.error('=====debug====:middlerware error');
|
console.error('=====debug====:middlerware error');
|
||||||
@@ -356,7 +385,7 @@ export class QueryRouter {
|
|||||||
if (route) {
|
if (route) {
|
||||||
if (route.run) {
|
if (route.run) {
|
||||||
try {
|
try {
|
||||||
await route.run(ctx);
|
await route.run(ctx as Required<RouteContext>);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (route?.isDebug) {
|
if (route?.isDebug) {
|
||||||
console.error('=====debug====:', 'router run error:', e.message);
|
console.error('=====debug====:', 'router run error:', e.message);
|
||||||
@@ -555,6 +584,21 @@ export class QueryRouter {
|
|||||||
hasRoute(path: string, key: string = '') {
|
hasRoute(path: string, key: string = '') {
|
||||||
return this.routes.find((r) => r.path === path && r.key === key);
|
return this.routes.find((r) => r.path === path && r.key === key);
|
||||||
}
|
}
|
||||||
|
findRoute(opts?: { path?: string; key?: string; id?: string }) {
|
||||||
|
const { path, key, id } = opts || {};
|
||||||
|
return this.routes.find((r) => {
|
||||||
|
if (id) {
|
||||||
|
return r.id === id;
|
||||||
|
}
|
||||||
|
if (path) {
|
||||||
|
if (key !== undefined) {
|
||||||
|
return r.path === path && r.key === key;
|
||||||
|
}
|
||||||
|
return r.path === path;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
createRouteList(force: boolean = false, filter?: (route: Route) => boolean) {
|
createRouteList(force: boolean = false, filter?: (route: Route) => boolean) {
|
||||||
const hasListRoute = this.hasRoute('router', 'list');
|
const hasListRoute = this.hasRoute('router', 'list');
|
||||||
if (!hasListRoute || force) {
|
if (!hasListRoute || force) {
|
||||||
@@ -570,7 +614,7 @@ export class QueryRouter {
|
|||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 等待程序运行, 获取到message的数据,就执行
|
* 等待程序运行, 获取到message的数据,就执行
|
||||||
*
|
* params 是预设参数
|
||||||
* emitter = process
|
* emitter = process
|
||||||
* -- .exit
|
* -- .exit
|
||||||
* -- .on
|
* -- .on
|
||||||
@@ -587,13 +631,14 @@ export class QueryRouter {
|
|||||||
if (getList) {
|
if (getList) {
|
||||||
this.createRouteList(opts?.force ?? false, opts?.filter);
|
this.createRouteList(opts?.force ?? false, opts?.filter);
|
||||||
}
|
}
|
||||||
return listenProcess({ app: this, params, ...opts });
|
return listenProcess({ app: this as any, params, ...opts });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type QueryRouterServerOpts = {
|
type QueryRouterServerOpts = {
|
||||||
handleFn?: HandleFn;
|
handleFn?: HandleFn;
|
||||||
context?: RouteContext;
|
context?: RouteContext;
|
||||||
|
appId?: string;
|
||||||
};
|
};
|
||||||
interface HandleFn<T = any> {
|
interface HandleFn<T = any> {
|
||||||
(msg: { path: string;[key: string]: any }, ctx?: any): { code: string; data?: any; message?: string;[key: string]: any };
|
(msg: { path: string;[key: string]: any }, ctx?: any): { code: string; data?: any; message?: string;[key: string]: any };
|
||||||
@@ -604,24 +649,24 @@ interface HandleFn<T = any> {
|
|||||||
* @description 移除server相关的功能,只保留router相关的功能,和http.createServer不相关,独立
|
* @description 移除server相关的功能,只保留router相关的功能,和http.createServer不相关,独立
|
||||||
*/
|
*/
|
||||||
export class QueryRouterServer extends QueryRouter {
|
export class QueryRouterServer extends QueryRouter {
|
||||||
|
declare appId: string;
|
||||||
handle: any;
|
handle: any;
|
||||||
constructor(opts?: QueryRouterServerOpts) {
|
constructor(opts?: QueryRouterServerOpts) {
|
||||||
super();
|
super();
|
||||||
this.handle = this.getHandle(this, opts?.handleFn, opts?.context);
|
this.handle = this.getHandle(this, opts?.handleFn, opts?.context);
|
||||||
this.setContext({ needSerialize: false, ...opts?.context });
|
this.setContext({ needSerialize: false, ...opts?.context });
|
||||||
|
if (opts?.appId) {
|
||||||
|
this.appId = opts.appId;
|
||||||
|
} else {
|
||||||
|
this.appId = nanoid(16);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setHandle(wrapperFn?: HandleFn, ctx?: RouteContext) {
|
setHandle(wrapperFn?: HandleFn, ctx?: RouteContext) {
|
||||||
this.handle = this.getHandle(this, wrapperFn, ctx);
|
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) {
|
addRoute(route: Route) {
|
||||||
this.add(route);
|
this.add(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
Route = Route;
|
Route = Route;
|
||||||
route(opts: RouteOpts): Route<Required<RouteContext>>;
|
route(opts: RouteOpts): Route<Required<RouteContext>>;
|
||||||
route(path: string, key?: string): Route<Required<RouteContext>>;
|
route(path: string, key?: string): Route<Required<RouteContext>>;
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
import { parseXml } from './server/parse-xml.ts';
|
|
||||||
|
|
||||||
export { parseXml };
|
|
||||||
@@ -2,6 +2,8 @@ import { pathToRegexp, Key } from 'path-to-regexp';
|
|||||||
import type { IncomingMessage, ServerResponse, Server } from 'node:http';
|
import type { IncomingMessage, ServerResponse, Server } from 'node:http';
|
||||||
import { parseBody, parseSearch, parseSearchValue } from './server/parse-body.ts';
|
import { parseBody, parseSearch, parseSearchValue } from './server/parse-body.ts';
|
||||||
import { ListenOptions } from 'node:net';
|
import { ListenOptions } from 'node:net';
|
||||||
|
// import { Hono } from 'hono'
|
||||||
|
// const app = new Hono()
|
||||||
|
|
||||||
type Req = IncomingMessage & { params?: Record<string, string> };
|
type Req = IncomingMessage & { params?: Record<string, string> };
|
||||||
type SimpleObject = {
|
type SimpleObject = {
|
||||||
|
|||||||
59
src/sign.ts
59
src/sign.ts
@@ -1,59 +0,0 @@
|
|||||||
import { generate } from 'selfsigned';
|
|
||||||
|
|
||||||
export type Attributes = {
|
|
||||||
name: string;
|
|
||||||
value: string;
|
|
||||||
};
|
|
||||||
export type AltNames = {
|
|
||||||
type: number;
|
|
||||||
value?: string;
|
|
||||||
ip?: string;
|
|
||||||
};
|
|
||||||
export const createCert = (attrs: Attributes[] = [], altNames: AltNames[] = []) => {
|
|
||||||
let attributes = [
|
|
||||||
{ name: 'countryName', value: 'CN' }, // 国家代码
|
|
||||||
{ name: 'stateOrProvinceName', value: 'ZheJiang' }, // 州名
|
|
||||||
{ name: 'localityName', value: 'HangZhou' }, // 城市名
|
|
||||||
{ name: 'organizationName', value: 'kevisual' }, // 组织名
|
|
||||||
{ name: 'organizationalUnitName', value: 'kevisual' }, // 组织单位
|
|
||||||
...attrs,
|
|
||||||
];
|
|
||||||
// attribute 根据name去重复, 后面的覆盖前面的
|
|
||||||
attributes = Object.values(
|
|
||||||
attributes.reduce(
|
|
||||||
(acc, attr) => ({
|
|
||||||
...acc,
|
|
||||||
[attr.name]: attr,
|
|
||||||
}),
|
|
||||||
{} as Record<string, Attributes>,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
days: 365, // 证书有效期(天)
|
|
||||||
extensions: [
|
|
||||||
{
|
|
||||||
name: 'subjectAltName',
|
|
||||||
altNames: [
|
|
||||||
{ type: 2, value: '*' }, // DNS 名称
|
|
||||||
{ type: 2, value: 'localhost' }, // DNS
|
|
||||||
{
|
|
||||||
type: 2,
|
|
||||||
value: '[::1]',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 7,
|
|
||||||
ip: 'fe80::1',
|
|
||||||
},
|
|
||||||
{ type: 7, ip: '127.0.0.1' }, // IP 地址
|
|
||||||
...altNames,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const pems = generate(attributes, options);
|
|
||||||
return {
|
|
||||||
key: pems.private,
|
|
||||||
cert: pems.cert,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
const http = require('http');
|
|
||||||
const fs = require('fs').promises;
|
|
||||||
const path = require('path');
|
|
||||||
const fetch = require('node-fetch'); // 如果使用 Node.js 18 以上版本,可以改用内置 fetch
|
|
||||||
const url = require('url');
|
|
||||||
|
|
||||||
// 配置远端静态文件服务器和本地缓存目录
|
|
||||||
const remoteServer = 'https://example.com/static'; // 远端服务器的 URL
|
|
||||||
const cacheDir = path.join(__dirname, 'cache'); // 本地缓存目录
|
|
||||||
const PORT = process.env.PORT || 3000;
|
|
||||||
|
|
||||||
// 确保本地缓存目录存在
|
|
||||||
fs.mkdir(cacheDir, { recursive: true }).catch(console.error);
|
|
||||||
|
|
||||||
// 获取文件的 content-type
|
|
||||||
function getContentType(filePath) {
|
|
||||||
const extname = path.extname(filePath);
|
|
||||||
const contentType = {
|
|
||||||
'.html': 'text/html',
|
|
||||||
'.js': 'text/javascript',
|
|
||||||
'.css': 'text/css',
|
|
||||||
'.json': 'application/json',
|
|
||||||
'.png': 'image/png',
|
|
||||||
'.jpg': 'image/jpg',
|
|
||||||
'.gif': 'image/gif',
|
|
||||||
'.svg': 'image/svg+xml',
|
|
||||||
'.wav': 'audio/wav',
|
|
||||||
'.mp4': 'video/mp4'
|
|
||||||
};
|
|
||||||
return contentType[extname] || 'application/octet-stream';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理请求文件
|
|
||||||
async function serveFile(filePath, remoteUrl, res) {
|
|
||||||
try {
|
|
||||||
// 检查文件是否存在于本地缓存中
|
|
||||||
const fileContent = await fs.readFile(filePath);
|
|
||||||
res.writeHead(200, { 'Content-Type': getContentType(filePath) });
|
|
||||||
res.end(fileContent, 'utf-8');
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code === 'ENOENT') {
|
|
||||||
// 本地缓存中不存在,向远端服务器请求文件
|
|
||||||
try {
|
|
||||||
const response = await fetch(remoteUrl);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
// 远端请求成功,获取文件内容
|
|
||||||
const data = await response.buffer();
|
|
||||||
|
|
||||||
// 将文件缓存到本地
|
|
||||||
await fs.writeFile(filePath, data);
|
|
||||||
|
|
||||||
// 返回文件内容
|
|
||||||
res.writeHead(200, { 'Content-Type': getContentType(filePath) });
|
|
||||||
res.end(data, 'utf-8');
|
|
||||||
} else {
|
|
||||||
// 远端文件未找到或错误,返回 404
|
|
||||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
||||||
res.end(`Error 404: File not found at ${remoteUrl}`);
|
|
||||||
}
|
|
||||||
} catch (fetchErr) {
|
|
||||||
// 处理请求错误
|
|
||||||
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
||||||
res.end(`Server Error: Unable to fetch ${remoteUrl}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 其他文件系统错误
|
|
||||||
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
||||||
res.end(`Server Error: ${err.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建 HTTP 服务器
|
|
||||||
http.createServer(async (req, res) => {
|
|
||||||
let reqPath = req.url;
|
|
||||||
|
|
||||||
// 如果路径是根路径 `/`,将其设置为 `index.html`
|
|
||||||
if (reqPath === '/') reqPath = '/index.html';
|
|
||||||
|
|
||||||
// 构建本地缓存路径和远端 URL
|
|
||||||
const localFilePath = path.join(cacheDir, reqPath); // 本地文件路径
|
|
||||||
const remoteFileUrl = url.resolve(remoteServer, reqPath); // 远端文件 URL
|
|
||||||
|
|
||||||
// 根据请求路径处理文件或返回 index.html(单页面应用处理)
|
|
||||||
await serveFile(localFilePath, remoteFileUrl, res);
|
|
||||||
|
|
||||||
// 单页面应用的路由处理
|
|
||||||
if (res.headersSent) return; // 如果响应已发送,不再处理
|
|
||||||
|
|
||||||
// 如果未匹配到任何文件,返回 index.html
|
|
||||||
const indexFilePath = path.join(cacheDir, 'index.html');
|
|
||||||
const indexRemoteUrl = url.resolve(remoteServer, '/index.html');
|
|
||||||
await serveFile(indexFilePath, indexRemoteUrl, res);
|
|
||||||
}).listen(PORT, () => {
|
|
||||||
console.log(`Server running at http://localhost:${PORT}`);
|
|
||||||
});
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { App } from '../app.ts'
|
import { App } from '../app.ts'
|
||||||
import { RouterChat } from '@/chat.ts';
|
import { RouterChat } from '@/modules/chat.ts';
|
||||||
|
|
||||||
const app = new App();
|
const app = new App();
|
||||||
|
|
||||||
|
|||||||
16
src/test/mini.ts
Normal file
16
src/test/mini.ts
Normal 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
59
src/test/run-mini.ts
Normal 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)
|
||||||
@@ -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 = {
|
export type ListenProcessOptions = {
|
||||||
app?: any; // 传入的应用实例
|
app?: QueryRouterServer; // 传入的应用实例
|
||||||
emitter?: any; // 可选的事件发射器
|
mockProcess?: MockProcess; // 可选的事件发射器
|
||||||
params?: any; // 可选的参数
|
params?: any; // 可选的参数
|
||||||
timeout?: number; // 可选的超时时间 (单位: 毫秒)
|
timeout?: number; // 可选的超时时间 (单位: 毫秒) 默认 10 分钟
|
||||||
};
|
};
|
||||||
export const listenProcess = async ({ app, emitter, params, timeout = 10 * 60 * 60 * 1000 }: ListenProcessOptions) => {
|
export const listenProcess = async ({ app, mockProcess, params, timeout = 10 * 60 * 60 * 1000 }: ListenProcessOptions) => {
|
||||||
const process = emitter || globalThis.process;
|
const process = mockProcess || new MockProcess();
|
||||||
let isEnd = false;
|
let isEnd = false;
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
if (isEnd) return;
|
if (isEnd) return;
|
||||||
isEnd = true;
|
isEnd = true;
|
||||||
process.send?.({ success: false, error: 'Timeout' });
|
process.send?.({ success: false, error: 'Timeout' }, () => {
|
||||||
process.exit?.(1);
|
process.exit?.(1);
|
||||||
|
});
|
||||||
}, timeout);
|
}, timeout);
|
||||||
|
|
||||||
// 监听来自主进程的消息
|
// 监听来自主进程的消息
|
||||||
const getParams = async (): Promise<any> => {
|
const getParams = async (): Promise<any> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
process.on('message', (msg) => {
|
process.on((msg) => {
|
||||||
if (isEnd) return;
|
if (isEnd) return;
|
||||||
isEnd = true;
|
isEnd = true;
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
@@ -27,9 +67,23 @@ export const listenProcess = async ({ app, emitter, params, timeout = 10 * 60 *
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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 = {
|
const response = {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -37,14 +91,15 @@ export const listenProcess = async ({ app, emitter, params, timeout = 10 * 60 *
|
|||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
}
|
}
|
||||||
|
|
||||||
process.send?.(response, (error) => {
|
process.send?.(response, () => {
|
||||||
process.exit?.(0)
|
process.exit?.(0)
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
process.send?.({
|
process.send?.({
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message
|
error: error.message
|
||||||
})
|
}, () => {
|
||||||
process.exit?.(1)
|
process.exit?.(1)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user