Compare commits

...

21 Commits

Author SHA1 Message Date
e1a0e11052 chore: 更新版本号至0.0.80,并在 route.ts 中重构 toJSONSchema 和 fromJSONSchema 函数以使用新的 schema.ts 模块 2026-02-18 13:28:49 +08:00
058e6843b1 chore: 更新版本号至0.0.79,并在 route.ts 中修改 toJSONSchema 函数以支持可选参数 2026-02-18 11:19:58 +08:00
bf6b7ad709 chore: 更新版本号至0.0.78,并在 fromJSONSchema 函数中将返回值设为可选 2026-02-18 10:51:32 +08:00
0866f01b1e chore: 更新版本号至0.0.77,升级 @kevisual/query 至 0.0.47,重构 JSON Schema 处理函数并添加测试 2026-02-18 10:31:11 +08:00
e4e1e9abb9 chore: 更新版本号至0.0.76,并在 route.ts 中添加 extractArgs 函数 2026-02-18 10:00:26 +08:00
f72f7d6cf1 fix: temp 2026-02-18 08:58:53 +08:00
40a8825ea2 refactor: remove pnpm workspace configuration and update opencode functionality
- Deleted pnpm-workspace.yaml file.
- Modified opencode.ts to enhance skill creation and execution:
  - Updated skill title and summary for clarity.
  - Introduced a delay for router loading.
  - Improved route filtering logic.
  - Added extractArgs function to handle argument extraction from z.object types.
- Updated route.ts to ensure 'opencode' tag is added to skills if not present and improved JSON schema handling for args.
2026-02-18 04:53:32 +08:00
b2f718c492 chore: 更新版本号至0.0.74,并在 QueryRouter 中添加 metadata 字段 2026-02-18 03:25:09 +08:00
074775985e chore: 更新版本号至0.0.73,调整依赖项版本 2026-02-18 03:16:26 +08:00
a53e8c0bc3 refactor: update router import path and add random ID utility
- Changed the import path for router types from '@kevisual/router' to './route.ts' in src/router-define.ts.
- Added a new lock file (bun.lock) to manage dependencies.
- Introduced a new utility function in src/utils/random.ts to generate random IDs using the nanoid library.
2026-02-17 20:35:16 +08:00
b4f2615afa temp 2026-02-17 17:24:44 +08:00
170954ae7c fix: error 2026-02-17 16:28:50 +08:00
15db8515d6 perf: 优化监听进程 2026-02-04 02:36:12 +08:00
08696dedd8 chore: update package version to 0.0.69; modify import statements in loadTS function 2026-02-03 01:12:23 +08:00
48de44587a feat: add JSON schema conversion methods and update route handling 2026-02-03 00:18:26 +08:00
7f7ea79689 feat: update package version and dependencies; add ReconnectingWebSocket for automatic reconnection 2026-02-02 21:22:49 +08:00
b081a03399 test bun stream sse and http-stream 2026-02-01 01:26:57 +08:00
fed87eb3a1 0.0.65 2026-01-31 18:23:52 +08:00
xiongxiao
a5429f055a fix 2026-01-31 18:23:09 +08:00
7e34564516 Refactor code structure for improved readability and maintainability 2026-01-30 22:44:14 +08:00
xiongxiao
605061a60e feat: enhance plugin integration with hooks and context management
- Updated the `createRouterAgentPluginFn` to accept an optional `hooks` parameter for better plugin customization.
- Introduced `usePluginInput` for accessing plugin input context.
- Refactored `AgentPlugin` to utilize hooks and context, improving flexibility and functionality.
- Cleaned up commented-out code for clarity.
2026-01-28 00:02:21 +08:00
28 changed files with 1033 additions and 4944 deletions

View File

@@ -17,7 +17,7 @@ $:
- vscode - vscode
- docker - docker
imports: !reference [.common_env, imports] imports: !reference [.common_env, imports]
stages: !reference [.dev_tempalte, stages] stages: !reference [.dev_template, stages]
.common_sync_to_gitea: &common_sync_to_gitea .common_sync_to_gitea: &common_sync_to_gitea
- <<: *common_env - <<: *common_env

View File

@@ -1,6 +1,7 @@
import { app, createSkill, tool } from '../app.ts'; import { app, createSkill, tool } from '../app.ts';
import * as docs from '../gen/index.ts' import * as docs from '../gen/index.ts'
import * as pkgs from '../../package.json' assert { type: 'json' }; import * as pkgs from '../../package.json' assert { type: 'json' };
import z from 'zod';
app.route({ app.route({
path: 'router-skill', path: 'router-skill',
key: 'create-route', key: 'create-route',
@@ -32,14 +33,47 @@ app.route({
} }
}).addTo(app); }).addTo(app);
// 调用router应用 path router-skill key version // 获取最新router版本号
app.route({ app.route({
path: 'router-skill', path: 'router-skill',
key: 'version', key: 'version',
description: '获取路由技能版本', description: '获取最新router版本',
middleware: ['auth'], middleware: ['auth'],
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'router-skill-version',
title: '获取最新router版本号',
summary: '获取最新router版本号',
args: {}
})
},
}).define(async (ctx) => { }).define(async (ctx) => {
ctx.body = { ctx.body = {
content: pkgs.version || 'unknown' content: pkgs.version || 'unknown'
} }
}).addTo(app); }).addTo(app);
// 执行技能test-route-skill,name为abearxiong
app.route({
path: 'route-skill',
key: 'test',
description: '测试路由技能',
middleware: ['auth'],
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'test-route-skill',
title: '测试路由技能',
summary: '测试路由技能是否正常工作',
args: z.object({
name: z.string().describe('名字'),
})
})
},
}).define(async (ctx) => {
const name = ctx.query.name || 'unknown';
ctx.body = {
content: '测试成功,你好 ' + name
}
}).addTo(app)

View File

@@ -1,24 +1,15 @@
import path from 'node:path'; import { buildWithBun } from '@kevisual/code-builder';
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 buildWithBun({ naming: 'app', entry: 'agent/main.ts', dts: true });
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'; await buildWithBun({ naming: 'router', entry: 'src/index.ts', dts: true });
execSync(cmd, { stdio: 'inherit' }); await buildWithBun({ naming: 'router-browser', entry: 'src/app-browser.ts', target: 'browser', dts: true });
// Copy package.json to dist await buildWithBun({ naming: 'router-define', entry: 'src/router-define.ts', target: 'browser', dts: true });
await buildWithBun({ naming: 'router-simple', entry: 'src/router-simple.ts', dts: true });
await buildWithBun({ naming: 'opencode', entry: 'src/opencode.ts', dts: true });
await buildWithBun({ naming: 'ws', entry: 'src/ws.ts', dts: true });

284
bun.lock Normal file
View File

@@ -0,0 +1,284 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "@kevisual/router",
"dependencies": {
"es-toolkit": "^1.44.0",
},
"devDependencies": {
"@kevisual/code-builder": "^0.0.6",
"@kevisual/context": "^0.0.6",
"@kevisual/dts": "^0.0.4",
"@kevisual/js-filter": "^0.0.5",
"@kevisual/local-proxy": "^0.0.8",
"@kevisual/query": "^0.0.47",
"@kevisual/use-config": "^1.0.30",
"@opencode-ai/plugin": "^1.2.6",
"@types/bun": "^1.3.9",
"@types/node": "^25.2.3",
"@types/send": "^1.2.1",
"@types/ws": "^8.18.1",
"@types/xml2js": "^0.4.14",
"eventemitter3": "^5.0.4",
"fast-glob": "^3.3.3",
"hono": "^4.11.9",
"nanoid": "^5.1.6",
"path-to-regexp": "^8.3.0",
"send": "^1.2.1",
"typescript": "^5.9.3",
"ws": "npm:@kevisual/ws",
"xml2js": "^0.6.2",
"zod": "^4.3.6",
},
},
},
"packages": {
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, ""],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, ""],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, ""],
"@kevisual/code-builder": ["@kevisual/code-builder@0.0.6", "", { "bin": { "code-builder": "bin/code.js", "builder": "bin/code.js" } }, "sha512-0aqATB31/yw4k4s5/xKnfr4DKbUnx8e3Z3BmKbiXTrc+CqWiWTdlGe9bKI9dZ2Df+xNp6g11W4xM2NICNyyCCw=="],
"@kevisual/context": ["@kevisual/context@0.0.6", "", {}, "sha512-w7HBOuO3JH37n6xT6W3FD7ykqHTwtyxOQzTzfEcKDCbsvGB1wVreSxFm2bvoFnnFLuxT/5QMpKlnPrwvmcTGnw=="],
"@kevisual/dts": ["@kevisual/dts@0.0.4", "", { "dependencies": { "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-typescript": "^12.3.0", "rollup": "^4.57.1", "rollup-plugin-dts": "^6.3.0", "tslib": "^2.8.1" }, "bin": { "dts": "bin/dts.mjs" } }, "sha512-FVUaH/0nyhbHWpEVjFTGP54PLMm4Hf06aqWLdHOYHNPIgr1aK1C26kOH7iumklGFGk9w93IGxj8Zxe5fap5N2A=="],
"@kevisual/js-filter": ["@kevisual/js-filter@0.0.5", "", {}, "sha512-+S+Sf3K/aP6XtZI2s7TgKOr35UuvUvtpJ9YDW30a+mY0/N8gRuzyKhieBzQN7Ykayzz70uoMavBXut2rUlLgzw=="],
"@kevisual/load": ["@kevisual/load@0.0.6", "", { "dependencies": { "eventemitter3": "^5.0.1" } }, "sha512-+3YTFehRcZ1haGel5DKYMUwmi5i6f2psyaPZlfkKU/cOXgkpwoG9/BEqPCnPjicKqqnksEpixVRkyHJ+5bjLVA=="],
"@kevisual/local-proxy": ["@kevisual/local-proxy@0.0.8", "", {}, "sha512-VX/P+6/Cc8ruqp34ag6gVX073BchUmf5VNZcTV/6MJtjrNE76G8V6TLpBE8bywLnrqyRtFLIspk4QlH8up9B5Q=="],
"@kevisual/query": ["@kevisual/query@0.0.47", "", {}, "sha512-ZR7WXeDDGUSzBtcGVU3J173sA0hCqrGTw5ybGbdNGlM0VyJV/XQIovCcSoZh1YpnciLRRqJvzXUgTnCkam+M3g=="],
"@kevisual/use-config": ["@kevisual/use-config@1.0.30", "", { "dependencies": { "@kevisual/load": "^0.0.6" }, "peerDependencies": { "dotenv": "^17" } }, "sha512-kPdna0FW/X7D600aMdiZ5UTjbCo6d8d4jjauSc8RMmBwUU6WliFDSPUNKVpzm2BsDX5Nth1IXFPYMqH+wxqAmw=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.2.6", "", { "dependencies": { "@opencode-ai/sdk": "1.2.6", "zod": "4.1.8" } }, "sha512-CJEp3k17yWsjyfivm3zQof8L42pdze3a7iTqMOyesHgJplSuLiBYAMndbBYMDuJkyAh0dHYjw8v10vVw7Kfl4Q=="],
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.2.6", "", {}, "sha512-dWMF8Aku4h7fh8sw5tQ2FtbqRLbIFT8FcsukpxTird49ax7oUXP+gzqxM/VdxHjfksQvzLBjLZyMdDStc5g7xA=="],
"@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@29.0.0", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" } }, "sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ=="],
"@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@16.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" } }, "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg=="],
"@rollup/plugin-typescript": ["@rollup/plugin-typescript@12.3.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.14.0||^3.0.0||^4.0.0", "tslib": "*", "typescript": ">=3.7.0" } }, "sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big=="],
"@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" } }, ""],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="],
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, ""],
"@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
"@types/resolve": ["@types/resolve@1.20.2", "", {}, ""],
"@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
"@types/xml2js": ["@types/xml2js@0.4.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, ""],
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
"commondir": ["commondir@1.0.1", "", {}, ""],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, ""],
"deepmerge": ["deepmerge@4.3.1", "", {}, ""],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
"ee-first": ["ee-first@1.1.1", "", {}, ""],
"encodeurl": ["encodeurl@2.0.0", "", {}, ""],
"es-toolkit": ["es-toolkit@1.44.0", "", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="],
"escape-html": ["escape-html@1.0.3", "", {}, ""],
"estree-walker": ["estree-walker@2.0.2", "", {}, ""],
"etag": ["etag@1.8.1", "", {}, ""],
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, ""],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, ""],
"fresh": ["fresh@2.0.0", "", {}, ""],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, ""],
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, ""],
"hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, ""],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-module": ["is-module@1.0.0", "", {}, ""],
"is-number": ["is-number@7.0.0", "", {}, ""],
"is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, ""],
"js-tokens": ["js-tokens@4.0.0", "", {}, ""],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, ""],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, ""],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"ms": ["ms@2.1.3", "", {}, ""],
"nanoid": ["nanoid@5.1.6", "", { "bin": "bin/nanoid.js" }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, ""],
"path-parse": ["path-parse@1.0.7", "", {}, ""],
"path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
"picocolors": ["picocolors@1.1.1", "", {}, ""],
"picomatch": ["picomatch@4.0.3", "", {}, ""],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"range-parser": ["range-parser@1.2.1", "", {}, ""],
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, ""],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="],
"rollup-plugin-dts": ["rollup-plugin-dts@6.3.0", "", { "dependencies": { "magic-string": "^0.30.21" }, "optionalDependencies": { "@babel/code-frame": "^7.27.1" }, "peerDependencies": { "rollup": "^3.29.4 || ^4", "typescript": "^4.5 || ^5.0" } }, "sha512-d0UrqxYd8KyZ6i3M2Nx7WOMy708qsV/7fTHMHxCMCBOAe3V/U7OMPu5GkX8hC+cmkHhzGnfeYongl1IgiooddA=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"sax": ["sax@1.4.3", "", {}, ""],
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"statuses": ["statuses@2.0.2", "", {}, ""],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, ""],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, ""],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, ""],
"ws": ["@kevisual/ws@8.0.0", "", {}, "sha512-jlFxSlXUEz93cFW+UYT5BXv/rFVgiMQnIfqRYZ0gj1hSP8PMGRqMqUoHSLfKvfRRS4jseLSvTTeEKSQpZJtURg=="],
"xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="],
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, ""],
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"@opencode-ai/plugin/zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
"@types/send/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@types/ws/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@types/xml2js/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, ""],
}
}

38
demo/bun/src/index.ts Normal file
View File

@@ -0,0 +1,38 @@
const server = Bun.serve({
port: 5002,
fetch(request: Bun.BunRequest, server) {
const url = new URL(request.url);
if (url.pathname === '/stream') {
// 直接使用 Bun 的原生 ReadableStream
const readable = new ReadableStream({
async start(controller) {
for (let i = 1; i <= 10; i++) {
// 检查客户端是否断开
if (request.signal.aborted) {
console.log('客户端已断开');
controller.close();
return;
}
controller.enqueue(`${new Date().toISOString()}${i} 批数据\n`);
await new Promise(r => setTimeout(r, 100)); // 模拟延迟
}
controller.close();
}
});
request.signal.addEventListener('abort', () => {
console.log('Request aborted by client');
});
return new Response(readable, {
status: 200,
headers: {
'Content-Type': 'text/plain',
'Transfer-Encoding': 'chunked'
}
});
}
return new Response('Not Found', { status: 404 });
}
});

View File

@@ -1,5 +1,7 @@
import { Route, App } from '@kevisual/router/src/app.ts'; import { Route, App, tool } from '@kevisual/router/src/app.ts';
import util from 'node:util';
import z from 'zod';
const showMore = (obj) => util.inspect(obj, { showHidden: false, depth: null, colors: true });
const app = new App({ serverOptions: { io: true } }); const app = new App({ serverOptions: { io: true } });
app.listen(4002); app.listen(4002);
const route01 = new Route('demo', '01'); const route01 = new Route('demo', '01');
@@ -7,14 +9,6 @@ route01.run = async (ctx) => {
ctx.body = '01'; ctx.body = '01';
return ctx; return ctx;
}; };
app.use(
'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) => {
@@ -26,13 +20,54 @@ app.addRoute(route02);
console.log(`http://localhost:4002/api/router?path=demo&key=02`); console.log(`http://localhost:4002/api/router?path=demo&key=02`);
console.log(`http://localhost:4002/api/router?path=demo&key=01`); console.log(`http://localhost:4002/api/router?path=demo&key=01`);
app.server.on({ app.route({
id: 'abc', path: 'demo',
path: '/ws', key: '03',
io: true, metadata: {
fun: async ({ data }, { end }) => { info: 'This is route 03',
console.log('Custom middleware for /ws'); args: {
console.log('Data received:', data); test: tool.schema.string().describe('defaultTest'),
end({ message: 'Hello from /ws middleware' });
} }
}) },
}).define(async (ctx) => {
ctx.body = '03';
return ctx;
}).addTo(app);
// app.server.on({
// id: 'abc',
// path: '/ws',
// io: true,
// func: async (req,res) => {
// console.log('Custom middleware for /ws');
// // console.log('Data received:', data);
// // end({ message: 'Hello from /ws middleware' });
// }
// })
await app.createRouteList()
const res = await app.run({ path: 'router', key: 'list' })
console.log('Route List:', showMore(res.data));
const list = res.data.list;
for (const item of list) {
const args = item.metadata?.args || {}
const keys = Object.keys(args)
if (keys.length > 0) {
// console.log(`Route ${item.key} has args:`, showMore(args));
// for (const k of keys) {
// const argSchema = args[k];
// const v = z.fromJSONSchema(argSchema)
// console.log(` Arg ${k}:`, v.description, v.toJSONSchema());
// }
// const v = z.fromJSONSchema(args) as z.ZodObject<any>;
// if (v instanceof z.ZodObject) {
// const testZod = v.shape['test'];
// console.log('testZod:', testZod.description);
// }
// console.log(`Route ${item.key} args schema:`, v.description, v.toJSONSchema());
// // console.log('v.', v.)
// const test = v.parse({ test: 'hello' })
// console.log('Parsed args:', test);
}
}

2461
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,14 @@
{ {
"$schema": "https://json.schemastore.org/package", "$schema": "https://json.schemastore.org/package",
"name": "@kevisual/router", "name": "@kevisual/router",
"version": "0.0.62", "version": "0.0.80",
"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 && bun bun.config.ts",
"postbuild": "bun run bun.config.ts", "watch": "bun bun.config.ts --watch",
"watch": "rollup -c -w",
"clean": "rm -rf dist" "clean": "rm -rf dist"
}, },
"files": [ "files": [
@@ -22,84 +21,51 @@
"keywords": [], "keywords": [],
"author": "abearxiong", "author": "abearxiong",
"license": "MIT", "license": "MIT",
"packageManager": "pnpm@10.28.1",
"devDependencies": { "devDependencies": {
"@kevisual/context": "^0.0.4", "@kevisual/code-builder": "^0.0.6",
"@kevisual/context": "^0.0.6",
"@kevisual/dts": "^0.0.4",
"@kevisual/js-filter": "^0.0.5", "@kevisual/js-filter": "^0.0.5",
"@kevisual/local-proxy": "^0.0.8", "@kevisual/local-proxy": "^0.0.8",
"@kevisual/query": "^0.0.35", "@kevisual/query": "^0.0.47",
"@kevisual/use-config": "^1.0.28", "@kevisual/use-config": "^1.0.30",
"@opencode-ai/plugin": "^1.1.27", "@opencode-ai/plugin": "^1.2.6",
"@rollup/plugin-alias": "^6.0.0", "@types/bun": "^1.3.9",
"@rollup/plugin-commonjs": "29.0.0", "@types/node": "^25.2.3",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-typescript": "^12.3.0",
"@types/bun": "^1.3.6",
"@types/node": "^25.0.9",
"@types/send": "^1.2.1", "@types/send": "^1.2.1",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"@types/xml2js": "^0.4.14", "@types/xml2js": "^0.4.14",
"eventemitter3": "^5.0.4", "eventemitter3": "^5.0.4",
"fast-glob": "^3.3.3", "fast-glob": "^3.3.3",
"hono": "^4.11.9",
"nanoid": "^5.1.6", "nanoid": "^5.1.6",
"path-to-regexp": "^8.3.0", "path-to-regexp": "^8.3.0",
"rollup": "^4.55.2",
"rollup-plugin-dts": "^6.3.0",
"send": "^1.2.1", "send": "^1.2.1",
"ts-loader": "^9.5.4",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"tsx": "^4.21.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"ws": "npm:@kevisual/ws", "ws": "npm:@kevisual/ws",
"xml2js": "^0.6.2", "xml2js": "^0.6.2",
"zod": "^4.3.5" "zod": "^4.3.6"
}, },
"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": {
"hono": "^4.11.4" "es-toolkit": "^1.44.0"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
"exports": { "exports": {
".": { ".": "./dist/router.js",
"import": "./dist/router.js", "./browser": "./dist/router-browser.js",
"require": "./dist/router.js", "./simple": "./dist/router-simple.js",
"types": "./dist/router.d.ts"
},
"./browser": {
"import": "./dist/router-browser.js",
"require": "./dist/router-browser.js",
"types": "./dist/router-browser.d.ts"
},
"./simple": {
"import": "./dist/router-simple.js",
"require": "./dist/router-simple.js",
"types": "./dist/router-simple.d.ts"
},
"./opencode": "./dist/opencode.js", "./opencode": "./dist/opencode.js",
"./skill": "./dist/app.js", "./skill": "./dist/app.js",
"./define": { "./define": "./dist/router-define.js",
"import": "./dist/router-define.js", "./ws": "./dist/ws.js",
"require": "./dist/router-define.js", "./mod.ts": "./mod.ts",
"types": "./dist/router-define.d.ts" "./src/*": "./src/*",
}, "./modules/*": "./src/modules/*"
"./mod.ts": {
"import": "./mod.ts",
"require": "./mod.ts",
"types": "./mod.d.ts"
},
"./src/*": {
"import": "./src/*",
"require": "./src/*"
},
"./modules/*": {
"import": "./src/modules/*",
"require": "./src/modules/*"
}
} }
} }

2116
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +0,0 @@
onlyBuiltDependencies:
- esbuild
packages:
- 'demo/**/*'

View File

@@ -1,151 +0,0 @@
// rollup.config.js
import typescript from '@rollup/plugin-typescript';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { dts } from 'rollup-plugin-dts';
import alias from '@rollup/plugin-alias';
const createAlias = () => {
return alias({
entries: [
{ find: 'http', replacement: 'node:http' },
{ find: 'https', replacement: 'node:https' },
{ find: 'fs', replacement: 'node:fs' },
{ find: 'path', replacement: 'node:path' },
{ find: 'crypto', replacement: 'node:crypto' },
{ find: 'zlib', replacement: 'node:zlib' },
{ find: 'stream', replacement: 'node:stream' },
{ find: 'net', replacement: 'node:net' },
{ find: 'tty', replacement: 'node:tty' },
{ find: 'tls', replacement: 'node:tls' },
{ find: 'buffer', replacement: 'node:buffer' },
{ find: 'timers', replacement: 'node:timers' },
// { find: 'string_decoder', replacement: 'node:string_decoder' },
{ find: 'dns', replacement: 'node:dns' },
{ find: 'domain', replacement: 'node:domain' },
{ find: 'os', replacement: 'node:os' },
{ find: 'events', replacement: 'node:events' },
{ find: 'url', replacement: 'node:url' },
{ find: 'assert', replacement: 'node:assert' },
{ find: 'util', replacement: 'node:util' },
],
});
};
/**
* @type {import('rollup').RollupOptions}
*/
export default [
{
input: 'src/index.ts', // TypeScript 入口文件
output: {
file: 'dist/router.js', // 输出文件
format: 'es', // 输出格式设置为 ES 模块
},
plugins: [
createAlias(),
resolve({
browser: false,
}), // 使用 @rollup/plugin-node-resolve 解析 node_modules 中的模块
commonjs(),
typescript(), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件
],
},
{
input: 'src/index.ts',
output: {
file: 'dist/router.d.ts',
format: 'es',
},
plugins: [dts()],
},
{
input: 'src/app-browser.ts',
output: {
file: 'dist/router-browser.js',
format: 'es',
},
plugins: [
resolve({
browser: true,
}),
commonjs(),
typescript(),
],
},
{
input: 'src/app-browser.ts',
output: {
file: 'dist/router-browser.d.ts',
format: 'es',
},
plugins: [dts()],
},
{
input: 'src/router-define.ts',
output: {
file: 'dist/router-define.js',
format: 'es',
},
plugins: [
resolve({
browser: true,
}),
commonjs(),
typescript(),
],
},
{
input: 'src/router-define.ts',
output: {
file: 'dist/router-define.d.ts',
format: 'es',
},
plugins: [dts()],
external: ['@kevisual/router'],
},
{
input: 'src/router-simple.ts',
output: {
file: 'dist/router-simple.js',
format: 'es',
},
plugins: [
resolve({
browser: false,
}),
commonjs(),
typescript(),
],
},
{
input: 'src/router-simple.ts',
output: {
file: 'dist/router-simple.d.ts',
format: 'es',
},
plugins: [dts()],
},
{
input: 'src/opencode.ts',
output: {
file: 'dist/opencode.js',
format: 'es',
},
plugins: [
resolve({
browser: true,
}),
commonjs(),
typescript(),
],
},
{
input: 'src/opencode.ts',
output: {
file: 'dist/opencode.d.ts',
format: 'es',
},
plugins: [dts()],
},
];

View File

@@ -1,4 +1,4 @@
import { QueryRouter, Route, RouteContext, RouteOpts } from './route.ts'; import { AddOpts, 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';
@@ -6,7 +6,7 @@ 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'; import { randomId } from './utils/random.ts';
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 = {}> = {
@@ -48,7 +48,7 @@ export class App<U = {}> extends QueryRouter {
if (opts?.appId) { if (opts?.appId) {
this.appId = opts.appId; this.appId = opts.appId;
} else { } else {
this.appId = nanoid(16); this.appId = randomId(16, 'rand-');
} }
router.appId = this.appId; router.appId = this.appId;
} }
@@ -64,8 +64,8 @@ export class App<U = {}> extends QueryRouter {
// @ts-ignore // @ts-ignore
this.server.listen(...args); this.server.listen(...args);
} }
addRoute(route: Route) { addRoute(route: Route, opts?: AddOpts) {
super.add(route); super.add(route, opts);
} }
Route = Route; Route = Route;

View File

@@ -17,9 +17,11 @@ export const getMatchFiles = async (match: string = './*.ts', { cwd = process.cw
} }
if (runtime.isBun) { if (runtime.isBun) {
// Bun 环境下 // Bun 环境下
const bunPkgs = 'bun';
const pathPkgs = 'node:path';
// @ts-ignore // @ts-ignore
const { Glob } = await import('bun'); const { Glob } = await import(/*---*/bunPkgs);
const path = await import('path'); const path = await import(/*---*/pathPkgs);
// @ts-ignore // @ts-ignore
const glob = new Glob(match, { cwd, absolute: true, onlyFiles: true }); const glob = new Glob(match, { cwd, absolute: true, onlyFiles: true });
const files: string[] = []; const files: string[] = [];
@@ -32,7 +34,7 @@ export const getMatchFiles = async (match: string = './*.ts', { cwd = process.cw
return []; return [];
}; };
export const loadTS = async (match: string = './*.ts', { cwd = process.cwd(), load }: GlobOptions = {}): Promise<any[]> => { export const loadTS = async (match: string = './*.ts', { cwd = process?.cwd?.(), load }: GlobOptions = {}): Promise<any[]> => {
const files = await getMatchFiles(match, { cwd }); const files = await getMatchFiles(match, { cwd });
return Promise.all(files.map((file) => (load ? load(file) : import(file)))); return Promise.all(files.map((file) => (load ? load(file) : import(/*---*/file))));
}; };

View File

@@ -4,15 +4,15 @@ export type { Rule, Schema, } from './validator/index.ts';
export { createSchema } from './validator/index.ts'; export { createSchema } from './validator/index.ts';
export type { RouteContext, RouteOpts, RouteMiddleware } from './route.ts'; export type { RouteContext, RouteOpts, RouteInfo, RouteMiddleware } from './route.ts';
export type { Run, Skill } from './route.ts'; export type { Run, Skill } from './route.ts';
export { createSkill, tool } from './route.ts'; export { createSkill, tool, fromJSONSchema, toJSONSchema } from './route.ts';
export { CustomError } from './result/error.ts'; export { CustomError } from './result/error.ts';
export * from './router-define.ts'; export * from './router-define.ts';
export { MockProcess } from './utils/listen-process.ts' export { MockProcess, type ListenProcessParams, type ListenProcessResponse } from './utils/listen-process.ts'
// --- 以上同步更新至 browser.ts --- // --- 以上同步更新至 browser.ts ---

View File

@@ -4,17 +4,17 @@ export type { Rule, Schema, } from './validator/index.ts';
export { createSchema } from './validator/index.ts'; export { createSchema } from './validator/index.ts';
export type { RouteContext, RouteOpts, RouteMiddleware } from './route.ts'; export type { RouteContext, RouteOpts, RouteInfo, RouteMiddleware } from './route.ts';
export type { Run, Skill } from './route.ts'; export type { Run, Skill } from './route.ts';
export { createSkill, tool } from './route.ts'; export { createSkill, tool, fromJSONSchema, toJSONSchema } from './route.ts';
export { CustomError } from './result/error.ts'; export { CustomError } from './result/error.ts';
export * from './router-define.ts'; export * from './router-define.ts';
export { MockProcess } from './utils/listen-process.ts' export { MockProcess, type ListenProcessParams, type ListenProcessResponse } from './utils/listen-process.ts'
// --- 以上同步更新至 browser.ts --- // --- 以上同步更新至 browser.ts ---
export { ServerNode, handleServer } from './server/index.ts'; export { ServerNode, handleServer } from './server/index.ts';
@@ -34,4 +34,4 @@ export type {
OnListener, OnListener,
} from './server/server-type.ts'; } from './server/server-type.ts';
export { loadTS } from './auto/load-ts.ts'; // export { loadTS } from './auto/load-ts.ts';

View File

@@ -1,9 +1,10 @@
import { useContextKey } from '@kevisual/context' import { useContextKey } from '@kevisual/context'
import { createSkill, type QueryRouterServer, tool, type QueryRouter, type Skill } from './route.ts' import { createSkill, type QueryRouterServer, tool, type Skill } from './route.ts'
import { type App } from './app.ts' import { type App } from './app.ts'
import { type Plugin } from "@opencode-ai/plugin" import { PluginInput, type Plugin, Hooks } from "@opencode-ai/plugin"
import { filter } from '@kevisual/js-filter'; import { filter } from '@kevisual/js-filter';
export const addCallFn = (app: App) => { export const addCallFn = (app: App) => {
app.route({ app.route({
path: 'call', path: 'call',
@@ -14,8 +15,12 @@ export const addCallFn = (app: App) => {
tags: ['opencode'], tags: ['opencode'],
...createSkill({ ...createSkill({
skill: 'call-app', skill: 'call-app',
title: '调用app应用', title: '调用app应用,非技能模块',
summary: '调用router的应用, 参数path, key, payload', summary: `调用router的应用(非技能模块),适用于需要直接调用应用而不是技能的场景
条件1: 参数path(string), key(string), payload(object)
条件2: 当前的应用调用的模式不是技能
`,
args: { args: {
path: tool.schema.string().describe('应用路径,例如 cnb'), path: tool.schema.string().describe('应用路径,例如 cnb'),
key: tool.schema.string().optional().describe('应用key例如 list-repos'), key: tool.schema.string().optional().describe('应用key例如 list-repos'),
@@ -34,12 +39,16 @@ export const addCallFn = (app: App) => {
ctx.forward(res); ctx.forward(res);
}).addTo(app) }).addTo(app)
} }
export const createRouterAgentPluginFn = (opts?: { export const createRouterAgentPluginFn = (opts?: {
router?: App | QueryRouterServer, router?: App | QueryRouterServer,
//** 过滤比如WHERE metadata.tags includes 'opencode' */ //** 过滤比如WHERE metadata.tags includes 'opencode' */
query?: string query?: string,
hooks?: (plugin: PluginInput) => Promise<Hooks>
}) => { }) => {
new Promise(resolve => setTimeout(resolve, 100)) // 等待路由加载
let router = opts?.router let router = opts?.router
if (!router) { if (!router) {
const app = useContextKey<App>('app') const app = useContextKey<App>('app')
router = app router = app
@@ -56,21 +65,28 @@ export const createRouterAgentPluginFn = (opts?: {
const _routes = filter(router.routes, opts?.query || '') const _routes = filter(router.routes, opts?.query || '')
const routes = _routes.filter(r => { const routes = _routes.filter(r => {
const metadata = r.metadata as Skill const metadata = r.metadata as Skill
if (metadata && metadata.skill && metadata.summary) {
return true
}
if (metadata && metadata.tags && metadata.tags.includes('opencode')) { if (metadata && metadata.tags && metadata.tags.includes('opencode')) {
return !!metadata.skill return !!metadata.skill
} }
return false return false
}) });
// opencode run "查看系统信息" // opencode run "使用技能查看系统信息"
const AgentPlugin: Plugin = async ({ project, client, $, directory, worktree }) => { const AgentPlugin: Plugin = async (pluginInput) => {
useContextKey<PluginInput>('plugin-input', () => pluginInput, true)
const hooks = opts?.hooks ? await opts.hooks(pluginInput) : {}
return { return {
...hooks,
'tool': { 'tool': {
...routes.reduce((acc, route) => { ...routes.reduce((acc, route) => {
const metadata = route.metadata as Skill const metadata = route.metadata as Skill
let args = extractArgs(metadata?.args)
acc[metadata.skill!] = { acc[metadata.skill!] = {
name: metadata.title || metadata.skill, name: metadata.title || metadata.skill,
description: metadata.summary || '', description: metadata.summary || '',
args: metadata.args || {}, args: args,
async execute(args: Record<string, any>) { async execute(args: Record<string, any>) {
const res = await router.run({ const res = await router.run({
path: route.path, path: route.path,
@@ -96,13 +112,31 @@ export const createRouterAgentPluginFn = (opts?: {
} }
} }
return acc; return acc;
}, {} as Record<string, any>) }, {} as Record<string, any>),
...hooks?.tool
}, },
'tool.execute.before': async (opts) => { // 'tool.execute.before': async (opts) => {
// console.log('CnbPlugin: tool.execute.before', opts.tool); // // console.log('CnbPlugin: tool.execute.before', opts.tool);
// delete toolSkills['cnb-login-verify'] // // delete toolSkills['cnb-login-verify']
} // },
} }
} }
return AgentPlugin return AgentPlugin
} }
export const usePluginInput = (): PluginInput => {
return useContextKey<PluginInput>('plugin-input')
}
/**
* 如果args是z.object类型拆分第一个Object的属性比如z.object({ name: z.string(), age: z.number() }),拆分成{name: z.string(), age: z.number()}
* 如果args是普通对象直接返回
* @param args
* @returns
*/
export const extractArgs = (args: any) => {
if (args && typeof args === 'object' && typeof args.shape === 'object') {
return args.shape
}
return args || {}
}

View File

@@ -1,9 +1,9 @@
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, MockProcess } from './utils/listen-process.ts';
import { z } from 'zod'; import { z } from 'zod';
import { filter } from '@kevisual/js-filter' import { randomId } from './utils/random.ts';
import * as schema from './validator/schema.ts';
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> = {
@@ -14,6 +14,7 @@ export type RouteContext<T = { code?: number }, S = any> = {
appId?: string; appId?: string;
// run first // run first
query?: { [key: string]: any }; query?: { [key: string]: any };
args?: { [key: string]: any };
// response body // response body
/** return body */ /** return body */
body?: number | string | Object; body?: number | string | Object;
@@ -61,7 +62,7 @@ export type RouteContext<T = { code?: number }, S = any> = {
} & T; } & T;
export type SimpleObject = Record<string, any>; export type SimpleObject = Record<string, any>;
export type Run<T extends SimpleObject = {}> = (ctx: Required<RouteContext<T>>) => Promise<typeof ctx | null | void>; export type Run<T extends SimpleObject = {}> = (ctx: Required<RouteContext<T>>) => Promise<typeof ctx | null | void>;
export type RunMessage = { path?: string; key?: string; id?: string; payload?: any; };
export type NextRoute = Pick<Route, 'id' | 'path' | 'key'>; export type NextRoute = Pick<Route, 'id' | 'path' | 'key'>;
export type RouteMiddleware = export type RouteMiddleware =
| { | {
@@ -98,6 +99,7 @@ export type Skill<T = SimpleObject> = {
skill: string; skill: string;
title: string; title: string;
summary?: string; summary?: string;
tags?: string[];
args?: { args?: {
[key: string]: any [key: string]: any
}; };
@@ -107,6 +109,12 @@ export const tool = {
} }
/** */ /** */
export const createSkill = <T = SimpleObject>(skill: Skill<T>): Skill<T> => { export const createSkill = <T = SimpleObject>(skill: Skill<T>): Skill<T> => {
if (skill.tags) {
const hasOpencode = skill.tags.includes('opencode');
if (!hasOpencode) {
skill.tags.push('opencode');
}
}
return { return {
args: {}, args: {},
...skill ...skill
@@ -114,6 +122,7 @@ export const createSkill = <T = SimpleObject>(skill: Skill<T>): Skill<T> => {
} }
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> {
/** /**
* 一级路径 * 一级路径
@@ -130,21 +139,20 @@ export class Route<U = { [key: string]: any }, T extends SimpleObject = SimpleOb
metadata?: T; metadata?: T;
middleware?: RouteMiddleware[]; // middleware middleware?: RouteMiddleware[]; // middleware
type? = 'route'; type? = 'route';
data?: any;
/** /**
* 是否开启debug开启后会打印错误信息 * 是否开启debug开启后会打印错误信息
*/ */
isDebug?: boolean; isDebug?: boolean;
constructor(path: string = '', key: string = '', opts?: RouteOpts) { constructor(path: string = '', key: string = '', opts?: RouteOpts) {
if (!path) { if (!path) {
path = nanoid(8) path = randomId(8, 'rand-');
} }
path = path.trim(); path = path.trim();
key = key.trim(); key = key.trim();
this.path = path; this.path = path;
this.key = key; this.key = key;
if (opts) { if (opts) {
this.id = opts.id || nanoid(); this.id = opts.id || randomId(12, 'rand-');
if (!opts.id && opts.idUsePath) { if (!opts.id && opts.idUsePath) {
const delimiter = opts.delimiter ?? '$#$'; const delimiter = opts.delimiter ?? '$#$';
this.id = path + delimiter + key; this.id = path + delimiter + key;
@@ -159,7 +167,7 @@ export class Route<U = { [key: string]: any }, T extends SimpleObject = SimpleOb
this.path = opts.path || path; this.path = opts.path || path;
} else { } else {
this.middleware = []; this.middleware = [];
this.id = nanoid(); this.id = randomId(12, 'rand-');
} }
this.isDebug = opts?.isDebug ?? false; this.isDebug = opts?.isDebug ?? false;
} }
@@ -215,10 +223,10 @@ export class Route<U = { [key: string]: any }, T extends SimpleObject = SimpleOb
return this; return this;
} }
update(opts: DefineRouteOpts, checkList?: string[]): this { update(opts: DefineRouteOpts, onlyUpdateList?: string[]): this {
const keys = Object.keys(opts); const keys = Object.keys(opts);
const defaultCheckList = ['path', 'key', 'run', 'nextRoute', 'description', 'metadata', 'middleware', 'type', 'isDebug']; const defaultCheckList = ['path', 'key', 'run', 'nextRoute', 'description', 'metadata', 'middleware', 'type', 'isDebug'];
checkList = checkList || defaultCheckList; const checkList = onlyUpdateList || defaultCheckList;
for (let item of keys) { for (let item of keys) {
if (!checkList.includes(item)) { if (!checkList.includes(item)) {
continue; continue;
@@ -231,12 +239,8 @@ export class Route<U = { [key: string]: any }, T extends SimpleObject = SimpleOb
} }
return this; return this;
} }
addTo(router: QueryRouter | { add: (route: Route) => void;[key: string]: any }) { addTo(router: QueryRouter | { add: (route: Route) => void;[key: string]: any }, opts?: AddOpts) {
router.add(this); router.add(this, opts);
}
setData(data: any) {
this.data = data;
return this;
} }
throw(code?: number | string, message?: string, tips?: string): void; throw(code?: number | string, message?: string, tips?: string): void;
throw(...args: any[]) { throw(...args: any[]) {
@@ -244,6 +248,21 @@ export class Route<U = { [key: string]: any }, T extends SimpleObject = SimpleOb
} }
} }
const toJSONSchemaRoute = (route: RouteInfo) => {
const pickValues = pick(route, pickValue as any);
if (pickValues?.metadata?.args) {
pickValues.metadata.args = toJSONSchema(pickValues?.metadata?.args, { mergeObject: false });
}
return pickValues;
}
export const toJSONSchema = schema.toJSONSchema;
export const fromJSONSchema = schema.fromJSONSchema;
/**
* @parmas overwrite 是否覆盖已存在的route默认true
*/
export type AddOpts = { overwrite?: boolean };
export class QueryRouter { export class QueryRouter {
appId: string = ''; appId: string = '';
routes: Route[]; routes: Route[];
@@ -252,11 +271,20 @@ export class QueryRouter {
constructor() { constructor() {
this.routes = []; this.routes = [];
} }
/**
add(route: Route) { * add route
* @param route
* @param opts
*/
add(route: Route, opts?: AddOpts) {
const overwrite = opts?.overwrite ?? true;
const has = this.routes.findIndex((r) => r.path === route.path && r.key === route.key); const has = this.routes.findIndex((r) => r.path === route.path && r.key === route.key);
if (has !== -1) { if (has !== -1) {
// remove the old route if (!overwrite) {
return;
}
// 如果存在且overwrite为true则覆盖
this.routes.splice(has, 1); this.routes.splice(has, 1);
} }
this.routes.push(route); this.routes.push(route);
@@ -272,8 +300,8 @@ export class QueryRouter {
* remove route by id * remove route by id
* @param uniqueId * @param uniqueId
*/ */
removeById(unique: string) { removeById(uniqueId: string) {
this.routes = this.routes.filter((r) => r.id !== unique); this.routes = this.routes.filter((r) => r.id !== uniqueId);
} }
/** /**
* 执行route * 执行route
@@ -361,15 +389,15 @@ export class QueryRouter {
console.error('=====debug====:middlerware error'); console.error('=====debug====:middlerware error');
console.error('=====debug====:', e); console.error('=====debug====:', e);
console.error('=====debug====:[path:key]:', `${route.path}-${route.key}`); console.error('=====debug====:[path:key]:', `${route.path}-${route.key}`);
console.error('=====debug====:', e.message);
} }
if (e instanceof CustomError || e?.code) { if (e instanceof CustomError || e?.code) {
ctx.code = e.code; ctx.code = e.code;
ctx.message = e.message; ctx.message = e.message;
ctx.body = null; ctx.body = null;
} else { } else {
console.error(`fn:${route.path}-${route.key}:${route.id}`); console.error(`[router error] fn:${route.path}-${route.key}:${route.id}`);
console.error(`middleware:${middleware.path}-${middleware.key}:${middleware.id}`); console.error(`[router error] middleware:${middleware.path}-${middleware.key}:${middleware.id}`);
console.error(e)
ctx.code = 500; ctx.code = 500;
ctx.message = 'Internal Server Error'; ctx.message = 'Internal Server Error';
ctx.body = null; ctx.body = null;
@@ -388,14 +416,16 @@ export class QueryRouter {
await route.run(ctx as Required<RouteContext>); 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====:route error');
console.error('=====debug====:', e);
console.error('=====debug====:[path:key]:', `${route.path}-${route.key}`);
} }
if (e instanceof CustomError) { if (e instanceof CustomError) {
ctx.code = e.code; ctx.code = e.code;
ctx.message = e.message; ctx.message = e.message;
} else { } else {
console.error(`[error]fn:${route.path}-${route.key}:${route.id}`); console.error(`[router error] fn:${route.path}-${route.key}:${route.id}`);
console.error('error', e.message); console.error(`[router error] error`, e);
ctx.code = 500; ctx.code = 500;
ctx.message = 'Internal Server Error'; ctx.message = 'Internal Server Error';
} }
@@ -425,6 +455,7 @@ export class QueryRouter {
return ctx; return ctx;
} }
ctx.query = { ...ctx.query, ...ctx.nextQuery }; ctx.query = { ...ctx.query, ...ctx.nextQuery };
ctx.args = ctx.query;
ctx.nextQuery = {}; ctx.nextQuery = {};
return await this.runRoute(path, key, ctx); return await this.runRoute(path, key, ctx);
} }
@@ -452,6 +483,7 @@ export class QueryRouter {
const { path, key = '', payload = {}, ...query } = message; const { path, key = '', payload = {}, ...query } = message;
ctx = ctx || {}; ctx = ctx || {};
ctx.query = { ...ctx.query, ...query, ...payload }; ctx.query = { ...ctx.query, ...query, ...payload };
ctx.args = ctx.query;
ctx.state = { ...ctx?.state }; ctx.state = { ...ctx?.state };
ctx.throw = this.throw; ctx.throw = this.throw;
ctx.app = this; ctx.app = this;
@@ -542,7 +574,8 @@ export class QueryRouter {
} }
getList(filter?: (route: Route) => boolean): RouteInfo[] { getList(filter?: (route: Route) => boolean): RouteInfo[] {
return this.routes.filter(filter || (() => true)).map((r) => { return this.routes.filter(filter || (() => true)).map((r) => {
return pick(r, pickValue as any); const pickValues = pick(r, pickValue as any);
return pickValues;
}); });
} }
/** /**
@@ -599,14 +632,28 @@ export class QueryRouter {
return false; return false;
}); });
} }
createRouteList(force: boolean = false, filter?: (route: Route) => boolean) { createRouteList(opts?: { force?: boolean, filter?: (route: Route) => boolean, middleware?: string[] }) {
const hasListRoute = this.hasRoute('router', 'list'); const hasListRoute = this.hasRoute('router', 'list');
if (!hasListRoute || force) { if (!hasListRoute || opts?.force) {
const listRoute = new Route('router', 'list', { const listRoute = new Route('router', 'list', {
description: '列出当前应用下的所有的路由信息', description: '列出当前应用下的所有的路由信息',
middleware: opts?.middleware || [],
run: async (ctx: RouteContext) => { run: async (ctx: RouteContext) => {
const list = this.getList(filter); const tokenUser = ctx.state.tokenUser;
ctx.body = { list }; let isUser = !!tokenUser;
const list = this.getList(opts?.filter).filter((item) => {
if (item.id === 'auth' || item.id === 'auth-can' || item.id === 'check-auth-admin' || item.id === 'auth-admin') {
return false;
}
return true;
});
ctx.body = {
list: list.map((item) => {
const route = pick(item, ['id', 'path', 'key', 'description', 'middleware', 'metadata'] as const);
return toJSONSchemaRoute(route);
}),
isUser
};
}, },
}); });
this.add(listRoute); this.add(listRoute);
@@ -620,19 +667,22 @@ export class QueryRouter {
* -- .on * -- .on
* -- .send * -- .send
*/ */
wait(params?: { path?: string; key?: string; payload?: any }, opts?: { wait(params?: { message: RunMessage }, opts?: {
emitter?: any, mockProcess?: MockProcess,
timeout?: number, timeout?: number,
getList?: boolean getList?: boolean
force?: boolean force?: boolean
filter?: (route: Route) => boolean filter?: (route: Route) => boolean
routeListMiddleware?: string[]
}) { }) {
const getList = opts?.getList ?? true; const getList = opts?.getList ?? true;
if (getList) { if (getList) {
this.createRouteList(opts?.force ?? false, opts?.filter); this.createRouteList({ force: opts?.force, filter: opts?.filter, middleware: opts?.routeListMiddleware });
} }
return listenProcess({ app: this as any, params, ...opts }); return listenProcess({ app: this as any, params, ...opts });
} }
toJSONSchema = toJSONSchema;
fromJSONSchema = fromJSONSchema;
} }
type QueryRouterServerOpts = { type QueryRouterServerOpts = {
@@ -658,14 +708,14 @@ export class QueryRouterServer extends QueryRouter {
if (opts?.appId) { if (opts?.appId) {
this.appId = opts.appId; this.appId = opts.appId;
} else { } else {
this.appId = nanoid(16); this.appId = randomId(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);
} }
addRoute(route: Route) { addRoute(route: Route, opts?: AddOpts) {
this.add(route); this.add(route, opts);
} }
Route = Route; Route = Route;
route(opts: RouteOpts): Route<Required<RouteContext>>; route(opts: RouteOpts): Route<Required<RouteContext>>;
@@ -717,3 +767,4 @@ export class QueryRouterServer extends QueryRouter {
export class Mini extends QueryRouterServer { } export class Mini extends QueryRouterServer { }

View File

@@ -1,4 +1,4 @@
import type { QueryRouterServer, RouteOpts, Run, RouteMiddleware } from '@kevisual/router'; import type { QueryRouterServer, RouteOpts, Run, RouteMiddleware } from './route.ts';
import type { DataOpts, Query, Result } from '@kevisual/query/query'; import type { DataOpts, Query, Result } from '@kevisual/query/query';
// export type RouteObject<T extends readonly string[]> = { // export type RouteObject<T extends readonly string[]> = {
// [K in T[number]]: RouteOpts; // [K in T[number]]: RouteOpts;

170
src/server/reconnect-ws.ts Normal file
View File

@@ -0,0 +1,170 @@
import WebSocket from 'ws';
export type ReconnectConfig = {
/**
* 重连配置选项, 最大重试次数,默认无限
*/
maxRetries?: number;
/**
* 重连配置选项, 重试延迟(ms)默认1000
*/
retryDelay?: number;
/**
* 重连配置选项, 最大延迟(ms)默认30000
*/
maxDelay?: number;
/**
* 重连配置选项, 退避倍数默认2
*/
backoffMultiplier?: number;
};
/**
* 一个支持自动重连的 WebSocket 客户端。
* 在连接断开时会根据配置进行重连尝试,支持指数退避。
*/
export class ReconnectingWebSocket {
private ws: WebSocket | null = null;
private url: string;
private config: Required<ReconnectConfig>;
private retryCount: number = 0;
private reconnectTimer: NodeJS.Timeout | null = null;
private isManualClose: boolean = false;
private messageHandlers: Array<(data: any) => void> = [];
private openHandlers: Array<() => void> = [];
private closeHandlers: Array<(code: number, reason: Buffer) => void> = [];
private errorHandlers: Array<(error: Error) => void> = [];
constructor(url: string, config: ReconnectConfig = {}) {
this.url = url;
this.config = {
maxRetries: config.maxRetries ?? Infinity,
retryDelay: config.retryDelay ?? 1000,
maxDelay: config.maxDelay ?? 30000,
backoffMultiplier: config.backoffMultiplier ?? 2,
};
}
log(...args: any[]): void {
console.log('[ReconnectingWebSocket]', ...args);
}
error(...args: any[]): void {
console.error('[ReconnectingWebSocket]', ...args);
}
connect(): void {
if (this.ws?.readyState === WebSocket.OPEN) {
return;
}
this.log(`正在连接到 ${this.url}...`);
this.ws = new WebSocket(this.url);
this.ws.on('open', () => {
this.log('WebSocket 连接已打开');
this.retryCount = 0;
this.openHandlers.forEach(handler => handler());
this.send({ type: 'heartbeat', timestamp: new Date().toISOString() });
});
this.ws.on('message', (data: any) => {
this.messageHandlers.forEach(handler => {
try {
const message = JSON.parse(data.toString());
handler(message);
} catch {
handler(data.toString());
}
});
});
this.ws.on('close', (code: number, reason: Buffer) => {
this.log(`WebSocket 连接已关闭: code=${code}, reason=${reason.toString()}`);
this.closeHandlers.forEach(handler => handler(code, reason));
if (!this.isManualClose) {
this.scheduleReconnect();
}
});
this.ws.on('error', (error: Error) => {
this.error('WebSocket 错误:', error.message);
this.errorHandlers.forEach(handler => handler(error));
});
}
private scheduleReconnect(): void {
if (this.reconnectTimer) {
return;
}
if (this.retryCount >= this.config.maxRetries) {
this.error(`已达到最大重试次数 (${this.config.maxRetries}),停止重连`);
return;
}
// 计算延迟(指数退避)
const delay = Math.min(
this.config.retryDelay * Math.pow(this.config.backoffMultiplier, this.retryCount),
this.config.maxDelay
);
this.retryCount++;
this.log(`将在 ${delay}ms 后进行第 ${this.retryCount} 次重连尝试...`);
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.connect();
}, delay);
}
send(data: any): boolean {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
return true;
}
this.log('WebSocket 未连接,无法发送消息');
return false;
}
onMessage(handler: (data: any) => void): void {
this.messageHandlers.push(handler);
}
onOpen(handler: () => void): void {
this.openHandlers.push(handler);
}
onClose(handler: (code: number, reason: Buffer) => void): void {
this.closeHandlers.push(handler);
}
onError(handler: (error: Error) => void): void {
this.errorHandlers.push(handler);
}
close(): void {
this.isManualClose = true;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
getReadyState(): number {
return this.ws?.readyState ?? WebSocket.CLOSED;
}
getRetryCount(): number {
return this.retryCount;
}
}
// const ws = new ReconnectingWebSocket('ws://localhost:51516/livecode/ws?id=test-live-app', {
// maxRetries: Infinity, // 无限重试
// retryDelay: 1000, // 初始重试延迟 1 秒
// maxDelay: 30000, // 最大延迟 30 秒
// backoffMultiplier: 2, // 指数退避倍数
// });

View File

@@ -50,7 +50,7 @@ export class BunServer extends ServerBase implements ServerType {
port, port,
hostname, hostname,
idleTimeout: 0, // 4 minutes idle timeout (max 255 seconds) idleTimeout: 0, // 4 minutes idle timeout (max 255 seconds)
fetch: async (request: Bun.BunRequest, server: any) => { fetch: async (request: Bun.BunRequest, server: Bun.Server<{}>) => {
const host = request.headers.get('host') || 'localhost'; const host = request.headers.get('host') || 'localhost';
const clientInfo = server.requestIP(request); // 返回 { address: string, port: number } 或 null const clientInfo = server.requestIP(request); // 返回 { address: string, port: number } 或 null
const url = new URL(request.url, `http://${host}`); const url = new URL(request.url, `http://${host}`);
@@ -72,6 +72,7 @@ export class BunServer extends ServerBase implements ServerType {
// 将 Bun 的 Request 转换为 Node.js 风格的 req/res // 将 Bun 的 Request 转换为 Node.js 风格的 req/res
return new Promise(async (resolve) => { return new Promise(async (resolve) => {
const reqListener: { event: string; listener: Function }[] = [];
const req: RouterReq = { const req: RouterReq = {
url: url.pathname + url.search, url: url.pathname + url.search,
method: request.method, method: request.method,
@@ -81,12 +82,29 @@ export class BunServer extends ServerBase implements ServerType {
remoteAddress: request?.remoteAddress || request?.ip || clientInfo?.address || '', remoteAddress: request?.remoteAddress || request?.ip || clientInfo?.address || '',
remotePort: clientInfo?.port || 0, remotePort: clientInfo?.port || 0,
}, },
// @ts-ignore on: (event: string, listener: Function) => {
reqListener.push({ event, listener });
},
bun: { bun: {
request, // 原始请求对象 request, // 原始请求对象
server, // 原始服务器对象 server, // 原始服务器对象
resolve
} }
}; };
const onClose = () => {
reqListener.forEach(item => {
if (item.event === 'close') {
item.listener();
}
});
reqListener.length = 0;
}
// 监听请求的取消事件
if (request.signal) {
request.signal.addEventListener('abort', () => {
onClose();
});
}
const res: RouterRes = { const res: RouterRes = {
statusCode: 200, statusCode: 200,
@@ -143,7 +161,7 @@ export class BunServer extends ServerBase implements ServerType {
if (callback) callback(); if (callback) callback();
return true; return true;
}, },
pipe(stream: any) { pipe(stream: ReadableStream | NodeJS.ReadableStream) {
this.writableEnded = true; this.writableEnded = true;
// 如果是 ReadableStream直接使用 // 如果是 ReadableStream直接使用
@@ -164,6 +182,7 @@ export class BunServer extends ServerBase implements ServerType {
controller.enqueue(chunk); controller.enqueue(chunk);
}); });
stream.on('end', () => { stream.on('end', () => {
onClose();
controller.close(); controller.close();
}); });
stream.on('error', (err: any) => { stream.on('error', (err: any) => {
@@ -171,9 +190,9 @@ export class BunServer extends ServerBase implements ServerType {
}); });
}, },
cancel() { cancel() {
if (stream.destroy) { // 只有NODE流才有destroy方法
stream.destroy(); // @ts-ignore
} stream?.destroy?.();
} }
}); });

View File

@@ -104,6 +104,12 @@ export type RouterReq<T = {}> = {
}; };
body?: string; body?: string;
cookies?: Record<string, string>; cookies?: Record<string, string>;
bun?: {
request: Bun.BunRequest;
server: Bun.Server<{}>;
resolve: (response: Response) => void;
}
on: (event: 'close', listener: Function) => void;
} & T; } & T;
export type RouterRes<T = {}> = { export type RouterRes<T = {}> = {
@@ -116,6 +122,6 @@ export type RouterRes<T = {}> = {
setHeader: (name: string, value: string | string[]) => void; setHeader: (name: string, value: string | string[]) => void;
cookie: (name: string, value: string, options?: any) => void; cookie: (name: string, value: string, options?: any) => void;
write: (chunk: any) => void; write: (chunk: any) => void;
pipe: (stream: any) => void; pipe: (stream: ReadableStream) => void;
end: (data?: any) => void; end: (data?: any) => void;
} & T; } & T;

View File

@@ -1,25 +1,7 @@
import { fork } from 'child_process' import { fork } from 'child_process'
import { ListenProcessParams, ListenProcessResponse } from '@/utils/listen-process.ts';
export type RunCodeParams = { export type RunCodeParams = ListenProcessParams
path?: string; export type RunCode = ListenProcessResponse
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> => { export const runCode = async (tsPath: string, params: RunCodeParams = {}): Promise<RunCode> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// 使用 Bun 的 fork 模式启动子进程 // 使用 Bun 的 fork 模式启动子进程
@@ -52,8 +34,11 @@ import path from 'node:path'
const res = await runCode(path.join(process.cwd(), './src/test/mini.ts'), { const res = await runCode(path.join(process.cwd(), './src/test/mini.ts'), {
// path: 'main' // path: 'main'
// id: 'abc' // id: 'abc'
message: {
path: 'router', path: 'router',
key: 'list' key: 'list'
}
}) })
console.log('res', res.data.data.list) console.log('success', res)
console.log('res', res.data?.data?.list)

14
src/test/schema.ts Normal file
View File

@@ -0,0 +1,14 @@
import { toJSONSchema, fromJSONSchema } from "@/route.ts";
import { z } from "zod";
const schema = z.object({
name: z.string(),
age: z.number(),
});
// console.log("schema", schema);
const jsonSchema = toJSONSchema(schema);
console.log("jsonSchema", jsonSchema);
const newSchema = fromJSONSchema<true>(jsonSchema, { mergeObject: true });
console.log("newSchema shape", Object.keys(newSchema.shape));
console.log('check', newSchema.safeParse({ name: "Alice", age: "30" })?.success);

View File

@@ -1,7 +1,6 @@
import { App } from "../app.ts"; import { App } from "../app.ts";
const app = new App({ const app = new App({
io: true
}); });
app app

View File

@@ -1,5 +1,6 @@
import { EventEmitter } from "eventemitter3"; import { EventEmitter } from "eventemitter3";
import { QueryRouterServer } from "../route.ts" import { QueryRouterServer, RouterContextT, RunMessage } from "../route.ts"
import { merge } from 'es-toolkit'
export class MockProcess { export class MockProcess {
emitter?: EventEmitter emitter?: EventEmitter
process?: NodeJS.Process; process?: NodeJS.Process;
@@ -37,13 +38,31 @@ export class MockProcess {
this.process = undefined; this.process = undefined;
} }
} }
export type ListenProcessParams = {
message?: RunMessage,
context?: any
}
export type ListenProcessResponse = {
// 调用进程的功能
success?: boolean
data?: {
// 调用router的结果
code?: number
data?: any
message?: string
[key: string]: any
};
error?: any
timestamp?: string
[key: string]: any
}
export type ListenProcessOptions = { export type ListenProcessOptions = {
app?: QueryRouterServer; // 传入的应用实例 app?: QueryRouterServer; // 传入的应用实例
mockProcess?: MockProcess; // 可选的事件发射器 mockProcess?: MockProcess; // 可选的事件发射器
params?: any; // 可选的参数 params?: ListenProcessParams; // 可选的参数
timeout?: number; // 可选的超时时间 (单位: 毫秒) 默认 10 分钟 timeout?: number; // 可选的超时时间 (单位: 毫秒) 默认 10 分钟
}; };
export const listenProcess = async ({ app, mockProcess, params, timeout = 10 * 60 * 60 * 1000 }: ListenProcessOptions) => { export const listenProcess = async ({ app, mockProcess, params = {}, timeout = 10 * 60 * 60 * 1000 }: ListenProcessOptions) => {
const process = mockProcess || new MockProcess(); const process = mockProcess || new MockProcess();
let isEnd = false; let isEnd = false;
const timer = setTimeout(() => { const timer = setTimeout(() => {
@@ -57,11 +76,11 @@ export const listenProcess = async ({ app, mockProcess, params, timeout = 10 * 6
// 监听来自主进程的消息 // 监听来自主进程的消息
const getParams = async (): Promise<any> => { const getParams = async (): Promise<any> => {
return new Promise((resolve) => { return new Promise((resolve) => {
process.on((msg) => { process.on((params) => {
if (isEnd) return; if (isEnd) return;
isEnd = true; isEnd = true;
clearTimeout(timer); clearTimeout(timer);
resolve(msg) resolve(params || {})
}) })
}) })
} }
@@ -70,11 +89,11 @@ export const listenProcess = async ({ app, mockProcess, params, timeout = 10 * 6
/** /**
* 如果不提供path默认是main * 如果不提供path默认是main
*/ */
const { const _params = await getParams()
payload = {}, const mergeParams = merge(params, _params)
...rest
} = await getParams() const msg = mergeParams?.message || {};
const msg = { ...params, ...rest, payload: { ...params?.payload, ...payload } } const ctx: RouterContextT = mergeParams?.context || {}
/** /**
* 如果没有提供path和id默认取第一个路由, 而且路由path不是router的 * 如果没有提供path和id默认取第一个路由, 而且路由path不是router的
*/ */
@@ -83,7 +102,7 @@ export const listenProcess = async ({ app, mockProcess, params, timeout = 10 * 6
msg.id = route?.id msg.id = route?.id
} }
// 执行主要逻辑 // 执行主要逻辑
const result = await app.run(msg) const result = await app.run(msg, ctx);
// 发送结果回主进程 // 发送结果回主进程
const response = { const response = {
success: true, success: true,
@@ -95,6 +114,7 @@ export const listenProcess = async ({ app, mockProcess, params, timeout = 10 * 6
process.exit?.(0) process.exit?.(0)
}) })
} catch (error) { } catch (error) {
console.error('Error in listenProcess:', error);
process.send?.({ process.send?.({
success: false, success: false,
error: error.message error: error.message

8
src/utils/random.ts Normal file
View File

@@ -0,0 +1,8 @@
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 16);
export const randomId = (length: number = 8, affix: string = '') => {
return affix + nanoid(length);
}
export { nanoid };

100
src/validator/schema.ts Normal file
View File

@@ -0,0 +1,100 @@
import { z } from "zod";
const extractArgs = (args: any) => {
if (args && typeof args === 'object' && typeof args.shape === 'object') {
return args.shape as z.ZodRawShape;
}
return args || {};
};
type ZodOverride = (opts: { jsonSchema: any; path: string[]; zodSchema: z.ZodTypeAny }) => void;
/**
* 剥离第一层schema转换为JSON Schema无论是skill还是其他的infer比纯粹的zod object schema更合适因为它可能包含其他的字段而不仅仅是schema
* @param args
* @returns
*/
export const toJSONSchema = (args: any, opts?: { mergeObject?: boolean, override?: ZodOverride }): { [key: string]: any } => {
const mergeObject = opts?.mergeObject ?? false;
if (!args) return {};
const _override = ({ jsonSchema, path, zodSchema }) => {
if (Array.isArray(path) && path.length > 0) {
return
}
const isOptional = (zodSchema as any).isOptional?.();
if (isOptional) {
// 添加自定义属性
jsonSchema.optional = true;
}
}
const isError = (keys: string[]) => {
const errorKeys: string[] = ["toJSONSchema", "def", "type", "parse"]
const hasErrorKeys = errorKeys.every(key => keys.includes(key));
return hasErrorKeys;
}
const override: any = opts?.override || _override;
if (mergeObject) {
if (typeof args === 'object' && typeof args.toJSONSchema === 'function') {
return args.toJSONSchema();
}
if (isError(Object.keys(args))) {
return {};
}
// 如果 mergeObject 为 true直接将整个对象转换为 JSON Schema
// 先检测是否是一个错误的 schema
const schema = z.object(args);
return schema.toJSONSchema();
}
// 如果 args 本身是一个 zod object schema先提取 shape
args = extractArgs(args);
let keys = Object.keys(args);
if (isError(keys)) {
console.error(`[toJSONSchema error]: 解析到的 schema 可能不正确包含了zod默认的value的schema. 请检查输入的 schema 是否正确。`);
args = {};
keys = [];
}
if (mergeObject) {
}
let newArgs: { [key: string]: any } = {};
for (let key of keys) {
const item = args[key] as z.ZodAny;
if (item && typeof item === 'object' && typeof item.toJSONSchema === 'function') {
newArgs[key] = item.toJSONSchema({ override });
} else {
newArgs[key] = args[key]; // 可能不是schema
}
}
return newArgs;
}
export const fromJSONSchema = <Merge extends boolean = false>(args: any = {}, opts?: { mergeObject?: boolean }) => {
let resultArgs: any = null;
const mergeObject = opts?.mergeObject ?? false;
if (args["$schema"] || (args.type === 'object' && args.properties && typeof args.properties === 'object')) {
// 可能是整个schema
const objectSchema = z.fromJSONSchema(args);
const extract = extractArgs(objectSchema);
const keys = Object.keys(extract);
const newArgs: { [key: string]: any } = {};
for (let key of keys) {
newArgs[key] = extract[key];
}
resultArgs = newArgs;
}
if (!resultArgs) {
const keys = Object.keys(args);
const newArgs: { [key: string]: any } = {};
for (let key of keys) {
const item = args[key];
// fromJSONSchema 可能会失败,所以先 optional等使用的时候再验证
newArgs[key] = z.fromJSONSchema(item)
if (item.optional) {
newArgs[key] = newArgs[key].optional();
}
}
resultArgs = newArgs;
}
if (mergeObject) {
resultArgs = z.object(resultArgs);
}
type ResultArgs = Merge extends true ? z.ZodObject<{ [key: string]: any }> : { [key: string]: z.ZodTypeAny };
return resultArgs as unknown as ResultArgs;
}

65
src/ws.ts Normal file
View File

@@ -0,0 +1,65 @@
import { ReconnectingWebSocket, ReconnectConfig } from "./server/reconnect-ws.ts";
export * from "./server/reconnect-ws.ts";
import type { App } from "./app.ts";
export const handleCallWsApp = async (ws: ReconnectingWebSocket, app: App, message: any) => {
return handleCallApp((data: any) => {
ws.send(data);
}, app, message);
}
export const handleCallApp = async (send: (data: any) => void, app: App, message: any) => {
if (message.type === 'router' && message.id) {
const data = message?.data;
if (!message.id) {
console.error('Message id is required for router type');
return;
}
if (!data) {
send({
type: 'router',
id: message.id,
data: { code: 500, message: 'No data received' }
});
return;
}
const { tokenUser, ...rest } = data || {};
const res = await app.run(rest, {
state: { tokenUser },
appId: app.appId,
});
send({
type: 'router',
id: message.id,
data: res
});
}
}
export class Ws {
wsClient: ReconnectingWebSocket;
app: App;
showLog: boolean = true;
constructor(opts?: ReconnectConfig & {
url: string;
app: App;
showLog?: boolean;
handleMessage?: (ws: ReconnectingWebSocket, app: App, message: any) => void;
}) {
const { url, app, showLog = true, handleMessage = handleCallWsApp, ...rest } = opts;
this.wsClient = new ReconnectingWebSocket(url, rest);
this.app = app;
this.showLog = showLog;
this.wsClient.connect();
const onMessage = async (data: any) => {
return handleMessage(this.wsClient, this.app, data);
}
this.wsClient.onMessage(onMessage);
}
send(data: any): boolean {
return this.wsClient.send(data);
}
log(...args: any[]): void {
if (this.showLog)
console.log('[Ws]', ...args);
}
}