Compare commits
21 Commits
e7dca513f3
...
e1a0e11052
| Author | SHA1 | Date | |
|---|---|---|---|
| e1a0e11052 | |||
| 058e6843b1 | |||
| bf6b7ad709 | |||
| 0866f01b1e | |||
| e4e1e9abb9 | |||
| f72f7d6cf1 | |||
| 40a8825ea2 | |||
| b2f718c492 | |||
| 074775985e | |||
| a53e8c0bc3 | |||
| b4f2615afa | |||
| 170954ae7c | |||
| 15db8515d6 | |||
| 08696dedd8 | |||
| 48de44587a | |||
| 7f7ea79689 | |||
| b081a03399 | |||
| fed87eb3a1 | |||
|
|
a5429f055a | ||
| 7e34564516 | |||
|
|
605061a60e |
2
.cnb.yml
2
.cnb.yml
@@ -17,7 +17,7 @@ $:
|
||||
- vscode
|
||||
- docker
|
||||
imports: !reference [.common_env, imports]
|
||||
stages: !reference [.dev_tempalte, stages]
|
||||
stages: !reference [.dev_template, stages]
|
||||
|
||||
.common_sync_to_gitea: &common_sync_to_gitea
|
||||
- <<: *common_env
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { app, createSkill, tool } from '../app.ts';
|
||||
import * as docs from '../gen/index.ts'
|
||||
import * as pkgs from '../../package.json' assert { type: 'json' };
|
||||
import z from 'zod';
|
||||
app.route({
|
||||
path: 'router-skill',
|
||||
key: 'create-route',
|
||||
@@ -32,14 +33,47 @@ app.route({
|
||||
}
|
||||
}).addTo(app);
|
||||
|
||||
// 调用router应用 path router-skill key version
|
||||
// 获取最新router版本号
|
||||
app.route({
|
||||
path: 'router-skill',
|
||||
key: 'version',
|
||||
description: '获取路由技能版本',
|
||||
description: '获取最新router版本号',
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
skill: 'router-skill-version',
|
||||
title: '获取最新router版本号',
|
||||
summary: '获取最新router版本号',
|
||||
args: {}
|
||||
})
|
||||
},
|
||||
}).define(async (ctx) => {
|
||||
ctx.body = {
|
||||
content: pkgs.version || 'unknown'
|
||||
}
|
||||
}).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)
|
||||
@@ -1,24 +1,15 @@
|
||||
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);
|
||||
import { buildWithBun } from '@kevisual/code-builder';
|
||||
|
||||
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
|
||||
});
|
||||
await buildWithBun({ naming: 'app', entry: 'agent/main.ts', dts: true });
|
||||
|
||||
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
284
bun.lock
Normal 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
38
demo/bun/src/index.ts
Normal 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 });
|
||||
}
|
||||
});
|
||||
@@ -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 } });
|
||||
app.listen(4002);
|
||||
const route01 = new Route('demo', '01');
|
||||
@@ -7,14 +9,6 @@ route01.run = async (ctx) => {
|
||||
ctx.body = '01';
|
||||
return ctx;
|
||||
};
|
||||
app.use(
|
||||
'demo',
|
||||
async (ctx) => {
|
||||
ctx.body = '01';
|
||||
return ctx;
|
||||
},
|
||||
{ key: '01' },
|
||||
);
|
||||
|
||||
const route02 = new Route('demo', '02');
|
||||
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=01`);
|
||||
|
||||
app.server.on({
|
||||
id: 'abc',
|
||||
path: '/ws',
|
||||
io: true,
|
||||
fun: async ({ data }, { end }) => {
|
||||
console.log('Custom middleware for /ws');
|
||||
console.log('Data received:', data);
|
||||
end({ message: 'Hello from /ws middleware' });
|
||||
app.route({
|
||||
path: 'demo',
|
||||
key: '03',
|
||||
metadata: {
|
||||
info: 'This is route 03',
|
||||
args: {
|
||||
test: tool.schema.string().describe('defaultTest'),
|
||||
}
|
||||
},
|
||||
}).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
2461
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
78
package.json
78
package.json
@@ -1,15 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package",
|
||||
"name": "@kevisual/router",
|
||||
"version": "0.0.62",
|
||||
"version": "0.0.80",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"main": "./dist/router.js",
|
||||
"types": "./dist/router.d.ts",
|
||||
"scripts": {
|
||||
"build": "npm run clean && rollup -c",
|
||||
"postbuild": "bun run bun.config.ts",
|
||||
"watch": "rollup -c -w",
|
||||
"build": "npm run clean && bun bun.config.ts",
|
||||
"watch": "bun bun.config.ts --watch",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"files": [
|
||||
@@ -22,84 +21,51 @@
|
||||
"keywords": [],
|
||||
"author": "abearxiong",
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@10.28.1",
|
||||
"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/local-proxy": "^0.0.8",
|
||||
"@kevisual/query": "^0.0.35",
|
||||
"@kevisual/use-config": "^1.0.28",
|
||||
"@opencode-ai/plugin": "^1.1.27",
|
||||
"@rollup/plugin-alias": "^6.0.0",
|
||||
"@rollup/plugin-commonjs": "29.0.0",
|
||||
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||
"@rollup/plugin-typescript": "^12.3.0",
|
||||
"@types/bun": "^1.3.6",
|
||||
"@types/node": "^25.0.9",
|
||||
"@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",
|
||||
"rollup": "^4.55.2",
|
||||
"rollup-plugin-dts": "^6.3.0",
|
||||
"send": "^1.2.1",
|
||||
"ts-loader": "^9.5.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.8.1",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"ws": "npm:@kevisual/ws",
|
||||
"xml2js": "^0.6.2",
|
||||
"zod": "^4.3.5"
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/abearxiong/kevisual-router.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"hono": "^4.11.4"
|
||||
"es-toolkit": "^1.44.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/router.js",
|
||||
"require": "./dist/router.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"
|
||||
},
|
||||
".": "./dist/router.js",
|
||||
"./browser": "./dist/router-browser.js",
|
||||
"./simple": "./dist/router-simple.js",
|
||||
"./opencode": "./dist/opencode.js",
|
||||
"./skill": "./dist/app.js",
|
||||
"./define": {
|
||||
"import": "./dist/router-define.js",
|
||||
"require": "./dist/router-define.js",
|
||||
"types": "./dist/router-define.d.ts"
|
||||
},
|
||||
"./mod.ts": {
|
||||
"import": "./mod.ts",
|
||||
"require": "./mod.ts",
|
||||
"types": "./mod.d.ts"
|
||||
},
|
||||
"./src/*": {
|
||||
"import": "./src/*",
|
||||
"require": "./src/*"
|
||||
},
|
||||
"./modules/*": {
|
||||
"import": "./src/modules/*",
|
||||
"require": "./src/modules/*"
|
||||
}
|
||||
"./define": "./dist/router-define.js",
|
||||
"./ws": "./dist/ws.js",
|
||||
"./mod.ts": "./mod.ts",
|
||||
"./src/*": "./src/*",
|
||||
"./modules/*": "./src/modules/*"
|
||||
}
|
||||
}
|
||||
2116
pnpm-lock.yaml
generated
2116
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +0,0 @@
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
packages:
|
||||
- 'demo/**/*'
|
||||
151
rollup.config.js
151
rollup.config.js
@@ -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()],
|
||||
},
|
||||
];
|
||||
10
src/app.ts
10
src/app.ts
@@ -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 { HandleCtx } from './server/server-base.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 { isBun } from './utils/is-engine.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 AppOptions<T = {}> = {
|
||||
@@ -48,7 +48,7 @@ export class App<U = {}> extends QueryRouter {
|
||||
if (opts?.appId) {
|
||||
this.appId = opts.appId;
|
||||
} else {
|
||||
this.appId = nanoid(16);
|
||||
this.appId = randomId(16, 'rand-');
|
||||
}
|
||||
router.appId = this.appId;
|
||||
}
|
||||
@@ -64,8 +64,8 @@ export class App<U = {}> extends QueryRouter {
|
||||
// @ts-ignore
|
||||
this.server.listen(...args);
|
||||
}
|
||||
addRoute(route: Route) {
|
||||
super.add(route);
|
||||
addRoute(route: Route, opts?: AddOpts) {
|
||||
super.add(route, opts);
|
||||
}
|
||||
|
||||
Route = Route;
|
||||
|
||||
@@ -17,9 +17,11 @@ export const getMatchFiles = async (match: string = './*.ts', { cwd = process.cw
|
||||
}
|
||||
if (runtime.isBun) {
|
||||
// Bun 环境下
|
||||
const bunPkgs = 'bun';
|
||||
const pathPkgs = 'node:path';
|
||||
// @ts-ignore
|
||||
const { Glob } = await import('bun');
|
||||
const path = await import('path');
|
||||
const { Glob } = await import(/*---*/bunPkgs);
|
||||
const path = await import(/*---*/pathPkgs);
|
||||
// @ts-ignore
|
||||
const glob = new Glob(match, { cwd, absolute: true, onlyFiles: true });
|
||||
const files: string[] = [];
|
||||
@@ -32,7 +34,7 @@ export const getMatchFiles = async (match: string = './*.ts', { cwd = process.cw
|
||||
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 });
|
||||
return Promise.all(files.map((file) => (load ? load(file) : import(file))));
|
||||
return Promise.all(files.map((file) => (load ? load(file) : import(/*---*/file))));
|
||||
};
|
||||
|
||||
@@ -4,15 +4,15 @@ export type { Rule, Schema, } 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 { createSkill, tool } from './route.ts';
|
||||
export { createSkill, tool, fromJSONSchema, toJSONSchema } from './route.ts';
|
||||
|
||||
export { CustomError } from './result/error.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 ---
|
||||
@@ -4,17 +4,17 @@ export type { Rule, Schema, } 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 { createSkill, tool } from './route.ts';
|
||||
export { createSkill, tool, fromJSONSchema, toJSONSchema } from './route.ts';
|
||||
|
||||
export { CustomError } from './result/error.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 ---
|
||||
|
||||
export { ServerNode, handleServer } from './server/index.ts';
|
||||
@@ -34,4 +34,4 @@ export type {
|
||||
OnListener,
|
||||
} from './server/server-type.ts';
|
||||
|
||||
export { loadTS } from './auto/load-ts.ts';
|
||||
// export { loadTS } from './auto/load-ts.ts';
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
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 Plugin } from "@opencode-ai/plugin"
|
||||
import { PluginInput, type Plugin, Hooks } from "@opencode-ai/plugin"
|
||||
|
||||
import { filter } from '@kevisual/js-filter';
|
||||
|
||||
export const addCallFn = (app: App) => {
|
||||
app.route({
|
||||
path: 'call',
|
||||
@@ -14,8 +15,12 @@ export const addCallFn = (app: App) => {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
skill: 'call-app',
|
||||
title: '调用app应用',
|
||||
summary: '调用router的应用, 参数path, key, payload',
|
||||
title: '调用app应用,非技能模块',
|
||||
summary: `调用router的应用(非技能模块),适用于需要直接调用应用而不是技能的场景
|
||||
条件1: 参数path(string), key(string), payload(object)
|
||||
条件2: 当前的应用调用的模式不是技能
|
||||
|
||||
`,
|
||||
args: {
|
||||
path: tool.schema.string().describe('应用路径,例如 cnb'),
|
||||
key: tool.schema.string().optional().describe('应用key,例如 list-repos'),
|
||||
@@ -34,12 +39,16 @@ export const addCallFn = (app: App) => {
|
||||
ctx.forward(res);
|
||||
}).addTo(app)
|
||||
}
|
||||
|
||||
export const createRouterAgentPluginFn = (opts?: {
|
||||
router?: App | QueryRouterServer,
|
||||
//** 过滤比如,WHERE metadata.tags includes 'opencode' */
|
||||
query?: string
|
||||
query?: string,
|
||||
hooks?: (plugin: PluginInput) => Promise<Hooks>
|
||||
}) => {
|
||||
new Promise(resolve => setTimeout(resolve, 100)) // 等待路由加载
|
||||
let router = opts?.router
|
||||
|
||||
if (!router) {
|
||||
const app = useContextKey<App>('app')
|
||||
router = app
|
||||
@@ -56,21 +65,28 @@ export const createRouterAgentPluginFn = (opts?: {
|
||||
const _routes = filter(router.routes, opts?.query || '')
|
||||
const routes = _routes.filter(r => {
|
||||
const metadata = r.metadata as Skill
|
||||
if (metadata && metadata.skill && metadata.summary) {
|
||||
return true
|
||||
}
|
||||
if (metadata && metadata.tags && metadata.tags.includes('opencode')) {
|
||||
return !!metadata.skill
|
||||
}
|
||||
return false
|
||||
})
|
||||
// opencode run "查看系统信息"
|
||||
const AgentPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
|
||||
});
|
||||
// opencode run "使用技能查看系统信息"
|
||||
const AgentPlugin: Plugin = async (pluginInput) => {
|
||||
useContextKey<PluginInput>('plugin-input', () => pluginInput, true)
|
||||
const hooks = opts?.hooks ? await opts.hooks(pluginInput) : {}
|
||||
return {
|
||||
...hooks,
|
||||
'tool': {
|
||||
...routes.reduce((acc, route) => {
|
||||
const metadata = route.metadata as Skill
|
||||
let args = extractArgs(metadata?.args)
|
||||
acc[metadata.skill!] = {
|
||||
name: metadata.title || metadata.skill,
|
||||
description: metadata.summary || '',
|
||||
args: metadata.args || {},
|
||||
args: args,
|
||||
async execute(args: Record<string, any>) {
|
||||
const res = await router.run({
|
||||
path: route.path,
|
||||
@@ -96,13 +112,31 @@ export const createRouterAgentPluginFn = (opts?: {
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, any>)
|
||||
}, {} as Record<string, any>),
|
||||
...hooks?.tool
|
||||
},
|
||||
'tool.execute.before': async (opts) => {
|
||||
// console.log('CnbPlugin: tool.execute.before', opts.tool);
|
||||
// delete toolSkills['cnb-login-verify']
|
||||
}
|
||||
// 'tool.execute.before': async (opts) => {
|
||||
// // console.log('CnbPlugin: tool.execute.before', opts.tool);
|
||||
// // delete toolSkills['cnb-login-verify']
|
||||
// },
|
||||
}
|
||||
}
|
||||
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 || {}
|
||||
}
|
||||
127
src/route.ts
127
src/route.ts
@@ -1,9 +1,9 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import { CustomError } from './result/error.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 { 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 RouteContext<T = { code?: number }, S = any> = {
|
||||
@@ -14,6 +14,7 @@ export type RouteContext<T = { code?: number }, S = any> = {
|
||||
appId?: string;
|
||||
// run first
|
||||
query?: { [key: string]: any };
|
||||
args?: { [key: string]: any };
|
||||
// response body
|
||||
/** return body */
|
||||
body?: number | string | Object;
|
||||
@@ -61,7 +62,7 @@ export type RouteContext<T = { code?: number }, S = any> = {
|
||||
} & T;
|
||||
export type SimpleObject = Record<string, any>;
|
||||
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 RouteMiddleware =
|
||||
| {
|
||||
@@ -98,6 +99,7 @@ export type Skill<T = SimpleObject> = {
|
||||
skill: string;
|
||||
title: string;
|
||||
summary?: string;
|
||||
tags?: string[];
|
||||
args?: {
|
||||
[key: string]: any
|
||||
};
|
||||
@@ -107,6 +109,12 @@ export const tool = {
|
||||
}
|
||||
/** */
|
||||
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 {
|
||||
args: {},
|
||||
...skill
|
||||
@@ -114,6 +122,7 @@ export const createSkill = <T = SimpleObject>(skill: Skill<T>): Skill<T> => {
|
||||
}
|
||||
|
||||
export type RouteInfo = Pick<Route, (typeof pickValue)[number]>;
|
||||
|
||||
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;
|
||||
middleware?: RouteMiddleware[]; // middleware
|
||||
type? = 'route';
|
||||
data?: any;
|
||||
/**
|
||||
* 是否开启debug,开启后会打印错误信息
|
||||
*/
|
||||
isDebug?: boolean;
|
||||
constructor(path: string = '', key: string = '', opts?: RouteOpts) {
|
||||
if (!path) {
|
||||
path = nanoid(8)
|
||||
path = randomId(8, 'rand-');
|
||||
}
|
||||
path = path.trim();
|
||||
key = key.trim();
|
||||
this.path = path;
|
||||
this.key = key;
|
||||
if (opts) {
|
||||
this.id = opts.id || nanoid();
|
||||
this.id = opts.id || randomId(12, 'rand-');
|
||||
if (!opts.id && opts.idUsePath) {
|
||||
const delimiter = opts.delimiter ?? '$#$';
|
||||
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;
|
||||
} else {
|
||||
this.middleware = [];
|
||||
this.id = nanoid();
|
||||
this.id = randomId(12, 'rand-');
|
||||
}
|
||||
this.isDebug = opts?.isDebug ?? false;
|
||||
}
|
||||
@@ -215,10 +223,10 @@ export class Route<U = { [key: string]: any }, T extends SimpleObject = SimpleOb
|
||||
return this;
|
||||
}
|
||||
|
||||
update(opts: DefineRouteOpts, checkList?: string[]): this {
|
||||
update(opts: DefineRouteOpts, onlyUpdateList?: string[]): this {
|
||||
const keys = Object.keys(opts);
|
||||
const defaultCheckList = ['path', 'key', 'run', 'nextRoute', 'description', 'metadata', 'middleware', 'type', 'isDebug'];
|
||||
checkList = checkList || defaultCheckList;
|
||||
const checkList = onlyUpdateList || defaultCheckList;
|
||||
for (let item of keys) {
|
||||
if (!checkList.includes(item)) {
|
||||
continue;
|
||||
@@ -231,12 +239,8 @@ export class Route<U = { [key: string]: any }, T extends SimpleObject = SimpleOb
|
||||
}
|
||||
return this;
|
||||
}
|
||||
addTo(router: QueryRouter | { add: (route: Route) => void;[key: string]: any }) {
|
||||
router.add(this);
|
||||
}
|
||||
setData(data: any) {
|
||||
this.data = data;
|
||||
return this;
|
||||
addTo(router: QueryRouter | { add: (route: Route) => void;[key: string]: any }, opts?: AddOpts) {
|
||||
router.add(this, opts);
|
||||
}
|
||||
throw(code?: number | string, message?: string, tips?: string): void;
|
||||
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 {
|
||||
appId: string = '';
|
||||
routes: Route[];
|
||||
@@ -252,11 +271,20 @@ export class QueryRouter {
|
||||
constructor() {
|
||||
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);
|
||||
|
||||
if (has !== -1) {
|
||||
// remove the old route
|
||||
if (!overwrite) {
|
||||
return;
|
||||
}
|
||||
// 如果存在,且overwrite为true,则覆盖
|
||||
this.routes.splice(has, 1);
|
||||
}
|
||||
this.routes.push(route);
|
||||
@@ -272,8 +300,8 @@ export class QueryRouter {
|
||||
* remove route by id
|
||||
* @param uniqueId
|
||||
*/
|
||||
removeById(unique: string) {
|
||||
this.routes = this.routes.filter((r) => r.id !== unique);
|
||||
removeById(uniqueId: string) {
|
||||
this.routes = this.routes.filter((r) => r.id !== uniqueId);
|
||||
}
|
||||
/**
|
||||
* 执行route
|
||||
@@ -361,15 +389,15 @@ export class QueryRouter {
|
||||
console.error('=====debug====:middlerware error');
|
||||
console.error('=====debug====:', e);
|
||||
console.error('=====debug====:[path:key]:', `${route.path}-${route.key}`);
|
||||
console.error('=====debug====:', e.message);
|
||||
}
|
||||
if (e instanceof CustomError || e?.code) {
|
||||
ctx.code = e.code;
|
||||
ctx.message = e.message;
|
||||
ctx.body = null;
|
||||
} else {
|
||||
console.error(`fn:${route.path}-${route.key}:${route.id}`);
|
||||
console.error(`middleware:${middleware.path}-${middleware.key}:${middleware.id}`);
|
||||
console.error(`[router error] fn:${route.path}-${route.key}:${route.id}`);
|
||||
console.error(`[router error] middleware:${middleware.path}-${middleware.key}:${middleware.id}`);
|
||||
console.error(e)
|
||||
ctx.code = 500;
|
||||
ctx.message = 'Internal Server Error';
|
||||
ctx.body = null;
|
||||
@@ -388,14 +416,16 @@ export class QueryRouter {
|
||||
await route.run(ctx as Required<RouteContext>);
|
||||
} catch (e) {
|
||||
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) {
|
||||
ctx.code = e.code;
|
||||
ctx.message = e.message;
|
||||
} else {
|
||||
console.error(`[error]fn:${route.path}-${route.key}:${route.id}`);
|
||||
console.error('error', e.message);
|
||||
console.error(`[router error] fn:${route.path}-${route.key}:${route.id}`);
|
||||
console.error(`[router error] error`, e);
|
||||
ctx.code = 500;
|
||||
ctx.message = 'Internal Server Error';
|
||||
}
|
||||
@@ -425,6 +455,7 @@ export class QueryRouter {
|
||||
return ctx;
|
||||
}
|
||||
ctx.query = { ...ctx.query, ...ctx.nextQuery };
|
||||
ctx.args = ctx.query;
|
||||
ctx.nextQuery = {};
|
||||
return await this.runRoute(path, key, ctx);
|
||||
}
|
||||
@@ -452,6 +483,7 @@ export class QueryRouter {
|
||||
const { path, key = '', payload = {}, ...query } = message;
|
||||
ctx = ctx || {};
|
||||
ctx.query = { ...ctx.query, ...query, ...payload };
|
||||
ctx.args = ctx.query;
|
||||
ctx.state = { ...ctx?.state };
|
||||
ctx.throw = this.throw;
|
||||
ctx.app = this;
|
||||
@@ -542,7 +574,8 @@ export class QueryRouter {
|
||||
}
|
||||
getList(filter?: (route: Route) => boolean): RouteInfo[] {
|
||||
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;
|
||||
});
|
||||
}
|
||||
createRouteList(force: boolean = false, filter?: (route: Route) => boolean) {
|
||||
createRouteList(opts?: { force?: boolean, filter?: (route: Route) => boolean, middleware?: string[] }) {
|
||||
const hasListRoute = this.hasRoute('router', 'list');
|
||||
if (!hasListRoute || force) {
|
||||
if (!hasListRoute || opts?.force) {
|
||||
const listRoute = new Route('router', 'list', {
|
||||
description: '列出当前应用下的所有的路由信息',
|
||||
middleware: opts?.middleware || [],
|
||||
run: async (ctx: RouteContext) => {
|
||||
const list = this.getList(filter);
|
||||
ctx.body = { list };
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
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);
|
||||
@@ -620,19 +667,22 @@ export class QueryRouter {
|
||||
* -- .on
|
||||
* -- .send
|
||||
*/
|
||||
wait(params?: { path?: string; key?: string; payload?: any }, opts?: {
|
||||
emitter?: any,
|
||||
wait(params?: { message: RunMessage }, opts?: {
|
||||
mockProcess?: MockProcess,
|
||||
timeout?: number,
|
||||
getList?: boolean
|
||||
force?: boolean
|
||||
filter?: (route: Route) => boolean
|
||||
routeListMiddleware?: string[]
|
||||
}) {
|
||||
const getList = opts?.getList ?? true;
|
||||
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 });
|
||||
}
|
||||
toJSONSchema = toJSONSchema;
|
||||
fromJSONSchema = fromJSONSchema;
|
||||
}
|
||||
|
||||
type QueryRouterServerOpts = {
|
||||
@@ -658,14 +708,14 @@ export class QueryRouterServer extends QueryRouter {
|
||||
if (opts?.appId) {
|
||||
this.appId = opts.appId;
|
||||
} else {
|
||||
this.appId = nanoid(16);
|
||||
this.appId = randomId(16);
|
||||
}
|
||||
}
|
||||
setHandle(wrapperFn?: HandleFn, ctx?: RouteContext) {
|
||||
this.handle = this.getHandle(this, wrapperFn, ctx);
|
||||
}
|
||||
addRoute(route: Route) {
|
||||
this.add(route);
|
||||
addRoute(route: Route, opts?: AddOpts) {
|
||||
this.add(route, opts);
|
||||
}
|
||||
Route = Route;
|
||||
route(opts: RouteOpts): Route<Required<RouteContext>>;
|
||||
@@ -717,3 +767,4 @@ export class QueryRouterServer extends QueryRouter {
|
||||
|
||||
|
||||
export class Mini extends QueryRouterServer { }
|
||||
|
||||
|
||||
@@ -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';
|
||||
// export type RouteObject<T extends readonly string[]> = {
|
||||
// [K in T[number]]: RouteOpts;
|
||||
|
||||
170
src/server/reconnect-ws.ts
Normal file
170
src/server/reconnect-ws.ts
Normal 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, // 指数退避倍数
|
||||
// });
|
||||
@@ -50,7 +50,7 @@ export class BunServer extends ServerBase implements ServerType {
|
||||
port,
|
||||
hostname,
|
||||
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 clientInfo = server.requestIP(request); // 返回 { address: string, port: number } 或 null
|
||||
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
|
||||
return new Promise(async (resolve) => {
|
||||
const reqListener: { event: string; listener: Function }[] = [];
|
||||
const req: RouterReq = {
|
||||
url: url.pathname + url.search,
|
||||
method: request.method,
|
||||
@@ -81,12 +82,29 @@ export class BunServer extends ServerBase implements ServerType {
|
||||
remoteAddress: request?.remoteAddress || request?.ip || clientInfo?.address || '',
|
||||
remotePort: clientInfo?.port || 0,
|
||||
},
|
||||
// @ts-ignore
|
||||
on: (event: string, listener: Function) => {
|
||||
reqListener.push({ event, listener });
|
||||
},
|
||||
bun: {
|
||||
request, // 原始请求对象
|
||||
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 = {
|
||||
statusCode: 200,
|
||||
@@ -143,7 +161,7 @@ export class BunServer extends ServerBase implements ServerType {
|
||||
if (callback) callback();
|
||||
return true;
|
||||
},
|
||||
pipe(stream: any) {
|
||||
pipe(stream: ReadableStream | NodeJS.ReadableStream) {
|
||||
this.writableEnded = true;
|
||||
|
||||
// 如果是 ReadableStream,直接使用
|
||||
@@ -164,6 +182,7 @@ export class BunServer extends ServerBase implements ServerType {
|
||||
controller.enqueue(chunk);
|
||||
});
|
||||
stream.on('end', () => {
|
||||
onClose();
|
||||
controller.close();
|
||||
});
|
||||
stream.on('error', (err: any) => {
|
||||
@@ -171,9 +190,9 @@ export class BunServer extends ServerBase implements ServerType {
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
if (stream.destroy) {
|
||||
stream.destroy();
|
||||
}
|
||||
// 只有NODE流才有destroy方法
|
||||
// @ts-ignore
|
||||
stream?.destroy?.();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -104,6 +104,12 @@ export type RouterReq<T = {}> = {
|
||||
};
|
||||
body?: string;
|
||||
cookies?: Record<string, string>;
|
||||
bun?: {
|
||||
request: Bun.BunRequest;
|
||||
server: Bun.Server<{}>;
|
||||
resolve: (response: Response) => void;
|
||||
}
|
||||
on: (event: 'close', listener: Function) => void;
|
||||
} & T;
|
||||
|
||||
export type RouterRes<T = {}> = {
|
||||
@@ -116,6 +122,6 @@ export type RouterRes<T = {}> = {
|
||||
setHeader: (name: string, value: string | string[]) => void;
|
||||
cookie: (name: string, value: string, options?: any) => void;
|
||||
write: (chunk: any) => void;
|
||||
pipe: (stream: any) => void;
|
||||
pipe: (stream: ReadableStream) => void;
|
||||
end: (data?: any) => void;
|
||||
} & T;
|
||||
@@ -1,25 +1,7 @@
|
||||
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
|
||||
}
|
||||
import { ListenProcessParams, ListenProcessResponse } from '@/utils/listen-process.ts';
|
||||
export type RunCodeParams = ListenProcessParams
|
||||
export type RunCode = ListenProcessResponse
|
||||
export const runCode = async (tsPath: string, params: RunCodeParams = {}): Promise<RunCode> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 使用 Bun 的 fork 模式启动子进程
|
||||
@@ -52,8 +34,11 @@ 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'
|
||||
message: {
|
||||
path: 'router',
|
||||
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
14
src/test/schema.ts
Normal 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);
|
||||
@@ -1,7 +1,6 @@
|
||||
import { App } from "../app.ts";
|
||||
|
||||
const app = new App({
|
||||
io: true
|
||||
});
|
||||
|
||||
app
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { EventEmitter } from "eventemitter3";
|
||||
import { QueryRouterServer } from "../route.ts"
|
||||
import { QueryRouterServer, RouterContextT, RunMessage } from "../route.ts"
|
||||
import { merge } from 'es-toolkit'
|
||||
export class MockProcess {
|
||||
emitter?: EventEmitter
|
||||
process?: NodeJS.Process;
|
||||
@@ -37,13 +38,31 @@ export class MockProcess {
|
||||
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 = {
|
||||
app?: QueryRouterServer; // 传入的应用实例
|
||||
mockProcess?: MockProcess; // 可选的事件发射器
|
||||
params?: any; // 可选的参数
|
||||
params?: ListenProcessParams; // 可选的参数
|
||||
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();
|
||||
let isEnd = false;
|
||||
const timer = setTimeout(() => {
|
||||
@@ -57,11 +76,11 @@ export const listenProcess = async ({ app, mockProcess, params, timeout = 10 * 6
|
||||
// 监听来自主进程的消息
|
||||
const getParams = async (): Promise<any> => {
|
||||
return new Promise((resolve) => {
|
||||
process.on((msg) => {
|
||||
process.on((params) => {
|
||||
if (isEnd) return;
|
||||
isEnd = true;
|
||||
clearTimeout(timer);
|
||||
resolve(msg)
|
||||
resolve(params || {})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -70,11 +89,11 @@ export const listenProcess = async ({ app, mockProcess, params, timeout = 10 * 6
|
||||
/**
|
||||
* 如果不提供path,默认是main
|
||||
*/
|
||||
const {
|
||||
payload = {},
|
||||
...rest
|
||||
} = await getParams()
|
||||
const msg = { ...params, ...rest, payload: { ...params?.payload, ...payload } }
|
||||
const _params = await getParams()
|
||||
const mergeParams = merge(params, _params)
|
||||
|
||||
const msg = mergeParams?.message || {};
|
||||
const ctx: RouterContextT = mergeParams?.context || {}
|
||||
/**
|
||||
* 如果没有提供path和id,默认取第一个路由, 而且路由path不是router的
|
||||
*/
|
||||
@@ -83,7 +102,7 @@ export const listenProcess = async ({ app, mockProcess, params, timeout = 10 * 6
|
||||
msg.id = route?.id
|
||||
}
|
||||
// 执行主要逻辑
|
||||
const result = await app.run(msg)
|
||||
const result = await app.run(msg, ctx);
|
||||
// 发送结果回主进程
|
||||
const response = {
|
||||
success: true,
|
||||
@@ -95,6 +114,7 @@ export const listenProcess = async ({ app, mockProcess, params, timeout = 10 * 6
|
||||
process.exit?.(0)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error in listenProcess:', error);
|
||||
process.send?.({
|
||||
success: false,
|
||||
error: error.message
|
||||
|
||||
8
src/utils/random.ts
Normal file
8
src/utils/random.ts
Normal 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
100
src/validator/schema.ts
Normal 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
65
src/ws.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user