@kevisual/router 0.0.66 → 0.0.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ws.js ADDED
@@ -0,0 +1,186 @@
1
+ import WebSocket from 'ws';
2
+
3
+ /**
4
+ * 一个支持自动重连的 WebSocket 客户端。
5
+ * 在连接断开时会根据配置进行重连尝试,支持指数退避。
6
+ */
7
+ class ReconnectingWebSocket {
8
+ ws = null;
9
+ url;
10
+ config;
11
+ retryCount = 0;
12
+ reconnectTimer = null;
13
+ isManualClose = false;
14
+ messageHandlers = [];
15
+ openHandlers = [];
16
+ closeHandlers = [];
17
+ errorHandlers = [];
18
+ constructor(url, config = {}) {
19
+ this.url = url;
20
+ this.config = {
21
+ maxRetries: config.maxRetries ?? Infinity,
22
+ retryDelay: config.retryDelay ?? 1000,
23
+ maxDelay: config.maxDelay ?? 30000,
24
+ backoffMultiplier: config.backoffMultiplier ?? 2,
25
+ };
26
+ }
27
+ log(...args) {
28
+ console.log('[ReconnectingWebSocket]', ...args);
29
+ }
30
+ error(...args) {
31
+ console.error('[ReconnectingWebSocket]', ...args);
32
+ }
33
+ connect() {
34
+ if (this.ws?.readyState === WebSocket.OPEN) {
35
+ return;
36
+ }
37
+ this.log(`正在连接到 ${this.url}...`);
38
+ this.ws = new WebSocket(this.url);
39
+ this.ws.on('open', () => {
40
+ this.log('WebSocket 连接已打开');
41
+ this.retryCount = 0;
42
+ this.openHandlers.forEach(handler => handler());
43
+ this.send({ type: 'heartbeat', timestamp: new Date().toISOString() });
44
+ });
45
+ this.ws.on('message', (data) => {
46
+ this.messageHandlers.forEach(handler => {
47
+ try {
48
+ const message = JSON.parse(data.toString());
49
+ handler(message);
50
+ }
51
+ catch {
52
+ handler(data.toString());
53
+ }
54
+ });
55
+ });
56
+ this.ws.on('close', (code, reason) => {
57
+ this.log(`WebSocket 连接已关闭: code=${code}, reason=${reason.toString()}`);
58
+ this.closeHandlers.forEach(handler => handler(code, reason));
59
+ if (!this.isManualClose) {
60
+ this.scheduleReconnect();
61
+ }
62
+ });
63
+ this.ws.on('error', (error) => {
64
+ this.error('WebSocket 错误:', error.message);
65
+ this.errorHandlers.forEach(handler => handler(error));
66
+ });
67
+ }
68
+ scheduleReconnect() {
69
+ if (this.reconnectTimer) {
70
+ return;
71
+ }
72
+ if (this.retryCount >= this.config.maxRetries) {
73
+ this.error(`已达到最大重试次数 (${this.config.maxRetries}),停止重连`);
74
+ return;
75
+ }
76
+ // 计算延迟(指数退避)
77
+ const delay = Math.min(this.config.retryDelay * Math.pow(this.config.backoffMultiplier, this.retryCount), this.config.maxDelay);
78
+ this.retryCount++;
79
+ this.log(`将在 ${delay}ms 后进行第 ${this.retryCount} 次重连尝试...`);
80
+ this.reconnectTimer = setTimeout(() => {
81
+ this.reconnectTimer = null;
82
+ this.connect();
83
+ }, delay);
84
+ }
85
+ send(data) {
86
+ if (this.ws?.readyState === WebSocket.OPEN) {
87
+ this.ws.send(JSON.stringify(data));
88
+ return true;
89
+ }
90
+ this.log('WebSocket 未连接,无法发送消息');
91
+ return false;
92
+ }
93
+ onMessage(handler) {
94
+ this.messageHandlers.push(handler);
95
+ }
96
+ onOpen(handler) {
97
+ this.openHandlers.push(handler);
98
+ }
99
+ onClose(handler) {
100
+ this.closeHandlers.push(handler);
101
+ }
102
+ onError(handler) {
103
+ this.errorHandlers.push(handler);
104
+ }
105
+ close() {
106
+ this.isManualClose = true;
107
+ if (this.reconnectTimer) {
108
+ clearTimeout(this.reconnectTimer);
109
+ this.reconnectTimer = null;
110
+ }
111
+ if (this.ws) {
112
+ this.ws.close();
113
+ this.ws = null;
114
+ }
115
+ }
116
+ getReadyState() {
117
+ return this.ws?.readyState ?? WebSocket.CLOSED;
118
+ }
119
+ getRetryCount() {
120
+ return this.retryCount;
121
+ }
122
+ }
123
+ // const ws = new ReconnectingWebSocket('ws://localhost:51516/livecode/ws?id=test-live-app', {
124
+ // maxRetries: Infinity, // 无限重试
125
+ // retryDelay: 1000, // 初始重试延迟 1 秒
126
+ // maxDelay: 30000, // 最大延迟 30 秒
127
+ // backoffMultiplier: 2, // 指数退避倍数
128
+ // });
129
+
130
+ const handleCallWsApp = async (ws, app, message) => {
131
+ return handleCallApp((data) => {
132
+ ws.send(data);
133
+ }, app, message);
134
+ };
135
+ const handleCallApp = async (send, app, message) => {
136
+ if (message.type === 'router' && message.id) {
137
+ const data = message?.data;
138
+ if (!message.id) {
139
+ console.error('Message id is required for router type');
140
+ return;
141
+ }
142
+ if (!data) {
143
+ send({
144
+ type: 'router',
145
+ id: message.id,
146
+ data: { code: 500, message: 'No data received' }
147
+ });
148
+ return;
149
+ }
150
+ const { tokenUser, ...rest } = data || {};
151
+ const res = await app.run(rest, {
152
+ state: { tokenUser },
153
+ appId: app.appId,
154
+ });
155
+ send({
156
+ type: 'router',
157
+ id: message.id,
158
+ data: res
159
+ });
160
+ }
161
+ };
162
+ class Ws {
163
+ wsClient;
164
+ app;
165
+ showLog = true;
166
+ constructor(opts) {
167
+ const { url, app, showLog = true, handleMessage = handleCallWsApp, ...rest } = opts;
168
+ this.wsClient = new ReconnectingWebSocket(url, rest);
169
+ this.app = app;
170
+ this.showLog = showLog;
171
+ this.wsClient.connect();
172
+ const onMessage = async (data) => {
173
+ return handleMessage(this.wsClient, this.app, data);
174
+ };
175
+ this.wsClient.onMessage(onMessage);
176
+ }
177
+ send(data) {
178
+ return this.wsClient.send(data);
179
+ }
180
+ log(...args) {
181
+ if (this.showLog)
182
+ console.log('[Ws]', ...args);
183
+ }
184
+ }
185
+
186
+ export { ReconnectingWebSocket, Ws, handleCallApp, handleCallWsApp };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@kevisual/router",
4
- "version": "0.0.66",
4
+ "version": "0.0.68",
5
5
  "description": "",
6
6
  "type": "module",
7
7
  "main": "./dist/router.js",
@@ -24,17 +24,18 @@
24
24
  "packageManager": "pnpm@10.28.2",
25
25
  "devDependencies": {
26
26
  "@kevisual/context": "^0.0.4",
27
+ "@kevisual/dts": "^0.0.3",
27
28
  "@kevisual/js-filter": "^0.0.5",
28
29
  "@kevisual/local-proxy": "^0.0.8",
29
- "@kevisual/query": "^0.0.38",
30
- "@kevisual/use-config": "^1.0.28",
31
- "@opencode-ai/plugin": "^1.1.47",
30
+ "@kevisual/query": "^0.0.39",
31
+ "@kevisual/use-config": "^1.0.30",
32
+ "@opencode-ai/plugin": "^1.1.48",
32
33
  "@rollup/plugin-alias": "^6.0.0",
33
34
  "@rollup/plugin-commonjs": "29.0.0",
34
35
  "@rollup/plugin-node-resolve": "^16.0.3",
35
36
  "@rollup/plugin-typescript": "^12.3.0",
36
37
  "@types/bun": "^1.3.8",
37
- "@types/node": "^25.1.0",
38
+ "@types/node": "^25.2.0",
38
39
  "@types/send": "^1.2.1",
39
40
  "@types/ws": "^8.18.1",
40
41
  "@types/xml2js": "^0.4.14",
@@ -52,16 +53,14 @@
52
53
  "typescript": "^5.9.3",
53
54
  "ws": "npm:@kevisual/ws",
54
55
  "xml2js": "^0.6.2",
55
- "zod": "^4.3.6"
56
+ "zod": "^4.3.6",
57
+ "hono": "^4.11.7"
56
58
  },
57
59
  "repository": {
58
60
  "type": "git",
59
61
  "url": "git+https://github.com/abearxiong/kevisual-router.git"
60
62
  },
61
- "dependencies": {
62
- "@kevisual/dts": "^0.0.3",
63
- "hono": "^4.11.7"
64
- },
63
+ "dependencies": {},
65
64
  "publishConfig": {
66
65
  "access": "public"
67
66
  },
@@ -88,6 +87,7 @@
88
87
  "require": "./dist/router-define.js",
89
88
  "types": "./dist/router-define.d.ts"
90
89
  },
90
+ "./ws": "./dist/ws.js",
91
91
  "./mod.ts": {
92
92
  "import": "./mod.ts",
93
93
  "require": "./mod.ts",
@@ -102,4 +102,4 @@
102
102
  "require": "./src/modules/*"
103
103
  }
104
104
  }
105
- }
105
+ }
package/src/browser.ts CHANGED
@@ -8,7 +8,7 @@ export type { RouteContext, RouteOpts, RouteMiddleware } from './route.ts';
8
8
 
9
9
  export type { Run, Skill } from './route.ts';
10
10
 
11
- export { createSkill, tool } from './route.ts';
11
+ export { createSkill, tool, fromJSONSchema, toJSONSchema } from './route.ts';
12
12
 
13
13
  export { CustomError } from './result/error.ts';
14
14
 
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@ export type { RouteContext, RouteOpts, RouteMiddleware } from './route.ts';
8
8
 
9
9
  export type { Run, Skill } from './route.ts';
10
10
 
11
- export { createSkill, tool } from './route.ts';
11
+ export { createSkill, tool, fromJSONSchema, toJSONSchema } from './route.ts';
12
12
 
13
13
  export { CustomError } from './result/error.ts';
14
14
 
package/src/route.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { nanoid } from 'nanoid';
2
2
  import { CustomError } from './result/error.ts';
3
3
  import { pick } from './utils/pick.ts';
4
- import { listenProcess } from './utils/listen-process.ts';
4
+ import { listenProcess, MockProcess } from './utils/listen-process.ts';
5
5
  import { z } from 'zod';
6
6
  import { filter } from '@kevisual/js-filter'
7
7
 
@@ -243,11 +243,42 @@ export class Route<U = { [key: string]: any }, T extends SimpleObject = SimpleOb
243
243
  throw new CustomError(...args);
244
244
  }
245
245
  }
246
+ export const toJSONSchema = (route: RouteInfo) => {
247
+ const pickValues = pick(route, pickValue as any);
248
+ if (pickValues?.metadata?.args) {
249
+ const args = pickValues.metadata.args;
250
+ const keys = Object.keys(args);
251
+ const newArgs: { [key: string]: any } = {};
252
+ for (let key of keys) {
253
+ const item = args[key] as z.ZodAny;
254
+ if (item && typeof item === 'object' && typeof item.toJSONSchema === 'function') {
255
+ newArgs[key] = item.toJSONSchema();
256
+ } else {
257
+ newArgs[key] = args[key]; // 可能不是schema
258
+ }
259
+ }
260
+ pickValues.metadata.args = newArgs;
261
+ }
262
+ return pickValues;
263
+ }
264
+
265
+ export const fromJSONSchema = (route: RouteInfo): {
266
+ [key: string]: z.ZodTypeAny
267
+ } => {
268
+ const args = route?.metadata?.args || {};
269
+ const keys = Object.keys(args);
270
+ const newArgs: { [key: string]: any } = {};
271
+ for (let key of keys) {
272
+ const item = args[key];
273
+ newArgs[key] = z.fromJSONSchema(item);
274
+ }
275
+ return newArgs;
276
+ }
246
277
 
247
278
  /**
248
- * @parmas override 是否覆盖已存在的route,默认true
279
+ * @parmas overwrite 是否覆盖已存在的route,默认true
249
280
  */
250
- export type AddOpts = { override?: boolean };
281
+ export type AddOpts = { overwrite?: boolean };
251
282
  export class QueryRouter {
252
283
  appId: string = '';
253
284
  routes: Route[];
@@ -262,14 +293,14 @@ export class QueryRouter {
262
293
  * @param opts
263
294
  */
264
295
  add(route: Route, opts?: AddOpts) {
265
- const override = opts?.override ?? true;
296
+ const overwrite = opts?.overwrite ?? true;
266
297
  const has = this.routes.findIndex((r) => r.path === route.path && r.key === route.key);
267
298
 
268
299
  if (has !== -1) {
269
- if (!override) {
300
+ if (!overwrite) {
270
301
  return;
271
302
  }
272
- // 如果存在,且override为true,则覆盖
303
+ // 如果存在,且overwrite为true,则覆盖
273
304
  this.routes.splice(has, 1);
274
305
  }
275
306
  this.routes.push(route);
@@ -555,7 +586,23 @@ export class QueryRouter {
555
586
  }
556
587
  getList(filter?: (route: Route) => boolean): RouteInfo[] {
557
588
  return this.routes.filter(filter || (() => true)).map((r) => {
558
- return pick(r, pickValue as any);
589
+ const pickValues = pick(r, pickValue as any);
590
+ if (pickValues?.metadata?.args) {
591
+ // const demoArgs = { k: tool.schema.string().describe('示例参数') };
592
+ const args = pickValues.metadata.args;
593
+ const keys = Object.keys(args);
594
+ const newArgs: { [key: string]: any } = {};
595
+ for (let key of keys) {
596
+ const item = args[key] as z.ZodAny;
597
+ if (item && typeof item === 'object' && typeof item.toJSONSchema === 'function') {
598
+ newArgs[key] = item.toJSONSchema();
599
+ } else {
600
+ newArgs[key] = args[key]; // 可能不是schema
601
+ }
602
+ }
603
+ pickValues.metadata.args = newArgs;
604
+ }
605
+ return pickValues;
559
606
  });
560
607
  }
561
608
  /**
@@ -634,7 +681,7 @@ export class QueryRouter {
634
681
  * -- .send
635
682
  */
636
683
  wait(params?: { path?: string; key?: string; payload?: any }, opts?: {
637
- emitter?: any,
684
+ mockProcess?: MockProcess,
638
685
  timeout?: number,
639
686
  getList?: boolean
640
687
  force?: boolean
@@ -646,6 +693,8 @@ export class QueryRouter {
646
693
  }
647
694
  return listenProcess({ app: this as any, params, ...opts });
648
695
  }
696
+ static toJSONSchema = toJSONSchema;
697
+ static fromJSONSchema = fromJSONSchema;
649
698
  }
650
699
 
651
700
  type QueryRouterServerOpts = {
@@ -729,4 +778,5 @@ export class QueryRouterServer extends QueryRouter {
729
778
  }
730
779
 
731
780
 
732
- export class Mini extends QueryRouterServer { }
781
+ export class Mini extends QueryRouterServer { }
782
+
@@ -0,0 +1,170 @@
1
+ import WebSocket from 'ws';
2
+
3
+ export type ReconnectConfig = {
4
+ /**
5
+ * 重连配置选项, 最大重试次数,默认无限
6
+ */
7
+ maxRetries?: number;
8
+ /**
9
+ * 重连配置选项, 重试延迟(ms),默认1000
10
+ */
11
+ retryDelay?: number;
12
+ /**
13
+ * 重连配置选项, 最大延迟(ms),默认30000
14
+ */
15
+ maxDelay?: number;
16
+ /**
17
+ * 重连配置选项, 退避倍数,默认2
18
+ */
19
+ backoffMultiplier?: number;
20
+ };
21
+
22
+ /**
23
+ * 一个支持自动重连的 WebSocket 客户端。
24
+ * 在连接断开时会根据配置进行重连尝试,支持指数退避。
25
+ */
26
+ export class ReconnectingWebSocket {
27
+ private ws: WebSocket | null = null;
28
+ private url: string;
29
+ private config: Required<ReconnectConfig>;
30
+ private retryCount: number = 0;
31
+ private reconnectTimer: NodeJS.Timeout | null = null;
32
+ private isManualClose: boolean = false;
33
+ private messageHandlers: Array<(data: any) => void> = [];
34
+ private openHandlers: Array<() => void> = [];
35
+ private closeHandlers: Array<(code: number, reason: Buffer) => void> = [];
36
+ private errorHandlers: Array<(error: Error) => void> = [];
37
+
38
+ constructor(url: string, config: ReconnectConfig = {}) {
39
+ this.url = url;
40
+ this.config = {
41
+ maxRetries: config.maxRetries ?? Infinity,
42
+ retryDelay: config.retryDelay ?? 1000,
43
+ maxDelay: config.maxDelay ?? 30000,
44
+ backoffMultiplier: config.backoffMultiplier ?? 2,
45
+ };
46
+ }
47
+ log(...args: any[]): void {
48
+ console.log('[ReconnectingWebSocket]', ...args);
49
+ }
50
+ error(...args: any[]): void {
51
+ console.error('[ReconnectingWebSocket]', ...args);
52
+ }
53
+ connect(): void {
54
+ if (this.ws?.readyState === WebSocket.OPEN) {
55
+ return;
56
+ }
57
+
58
+ this.log(`正在连接到 ${this.url}...`);
59
+ this.ws = new WebSocket(this.url);
60
+
61
+ this.ws.on('open', () => {
62
+ this.log('WebSocket 连接已打开');
63
+ this.retryCount = 0;
64
+ this.openHandlers.forEach(handler => handler());
65
+ this.send({ type: 'heartbeat', timestamp: new Date().toISOString() });
66
+ });
67
+
68
+ this.ws.on('message', (data: any) => {
69
+ this.messageHandlers.forEach(handler => {
70
+ try {
71
+ const message = JSON.parse(data.toString());
72
+ handler(message);
73
+ } catch {
74
+ handler(data.toString());
75
+ }
76
+ });
77
+ });
78
+
79
+ this.ws.on('close', (code: number, reason: Buffer) => {
80
+ this.log(`WebSocket 连接已关闭: code=${code}, reason=${reason.toString()}`);
81
+ this.closeHandlers.forEach(handler => handler(code, reason));
82
+
83
+ if (!this.isManualClose) {
84
+ this.scheduleReconnect();
85
+ }
86
+ });
87
+
88
+ this.ws.on('error', (error: Error) => {
89
+ this.error('WebSocket 错误:', error.message);
90
+ this.errorHandlers.forEach(handler => handler(error));
91
+ });
92
+ }
93
+
94
+ private scheduleReconnect(): void {
95
+ if (this.reconnectTimer) {
96
+ return;
97
+ }
98
+
99
+ if (this.retryCount >= this.config.maxRetries) {
100
+ this.error(`已达到最大重试次数 (${this.config.maxRetries}),停止重连`);
101
+ return;
102
+ }
103
+
104
+ // 计算延迟(指数退避)
105
+ const delay = Math.min(
106
+ this.config.retryDelay * Math.pow(this.config.backoffMultiplier, this.retryCount),
107
+ this.config.maxDelay
108
+ );
109
+
110
+ this.retryCount++;
111
+ this.log(`将在 ${delay}ms 后进行第 ${this.retryCount} 次重连尝试...`);
112
+
113
+ this.reconnectTimer = setTimeout(() => {
114
+ this.reconnectTimer = null;
115
+ this.connect();
116
+ }, delay);
117
+ }
118
+
119
+ send(data: any): boolean {
120
+ if (this.ws?.readyState === WebSocket.OPEN) {
121
+ this.ws.send(JSON.stringify(data));
122
+ return true;
123
+ }
124
+ this.log('WebSocket 未连接,无法发送消息');
125
+ return false;
126
+ }
127
+
128
+ onMessage(handler: (data: any) => void): void {
129
+ this.messageHandlers.push(handler);
130
+ }
131
+
132
+ onOpen(handler: () => void): void {
133
+ this.openHandlers.push(handler);
134
+ }
135
+
136
+ onClose(handler: (code: number, reason: Buffer) => void): void {
137
+ this.closeHandlers.push(handler);
138
+ }
139
+
140
+ onError(handler: (error: Error) => void): void {
141
+ this.errorHandlers.push(handler);
142
+ }
143
+
144
+ close(): void {
145
+ this.isManualClose = true;
146
+ if (this.reconnectTimer) {
147
+ clearTimeout(this.reconnectTimer);
148
+ this.reconnectTimer = null;
149
+ }
150
+ if (this.ws) {
151
+ this.ws.close();
152
+ this.ws = null;
153
+ }
154
+ }
155
+
156
+ getReadyState(): number {
157
+ return this.ws?.readyState ?? WebSocket.CLOSED;
158
+ }
159
+
160
+ getRetryCount(): number {
161
+ return this.retryCount;
162
+ }
163
+ }
164
+
165
+ // const ws = new ReconnectingWebSocket('ws://localhost:51516/livecode/ws?id=test-live-app', {
166
+ // maxRetries: Infinity, // 无限重试
167
+ // retryDelay: 1000, // 初始重试延迟 1 秒
168
+ // maxDelay: 30000, // 最大延迟 30 秒
169
+ // backoffMultiplier: 2, // 指数退避倍数
170
+ // });
package/src/ws.ts ADDED
@@ -0,0 +1,65 @@
1
+ import { ReconnectingWebSocket, ReconnectConfig } from "./server/reconnect-ws.ts";
2
+
3
+ export * from "./server/reconnect-ws.ts";
4
+ import type { App } from "./app.ts";
5
+
6
+ export const handleCallWsApp = async (ws: ReconnectingWebSocket, app: App, message: any) => {
7
+ return handleCallApp((data: any) => {
8
+ ws.send(data);
9
+ }, app, message);
10
+ }
11
+ export const handleCallApp = async (send: (data: any) => void, app: App, message: any) => {
12
+ if (message.type === 'router' && message.id) {
13
+ const data = message?.data;
14
+ if (!message.id) {
15
+ console.error('Message id is required for router type');
16
+ return;
17
+ }
18
+ if (!data) {
19
+ send({
20
+ type: 'router',
21
+ id: message.id,
22
+ data: { code: 500, message: 'No data received' }
23
+ });
24
+ return;
25
+ }
26
+ const { tokenUser, ...rest } = data || {};
27
+ const res = await app.run(rest, {
28
+ state: { tokenUser },
29
+ appId: app.appId,
30
+ });
31
+ send({
32
+ type: 'router',
33
+ id: message.id,
34
+ data: res
35
+ });
36
+ }
37
+ }
38
+ export class Ws {
39
+ wsClient: ReconnectingWebSocket;
40
+ app: App;
41
+ showLog: boolean = true;
42
+ constructor(opts?: ReconnectConfig & {
43
+ url: string;
44
+ app: App;
45
+ showLog?: boolean;
46
+ handleMessage?: (ws: ReconnectingWebSocket, app: App, message: any) => void;
47
+ }) {
48
+ const { url, app, showLog = true, handleMessage = handleCallWsApp, ...rest } = opts;
49
+ this.wsClient = new ReconnectingWebSocket(url, rest);
50
+ this.app = app;
51
+ this.showLog = showLog;
52
+ this.wsClient.connect();
53
+ const onMessage = async (data: any) => {
54
+ return handleMessage(this.wsClient, this.app, data);
55
+ }
56
+ this.wsClient.onMessage(onMessage);
57
+ }
58
+ send(data: any): boolean {
59
+ return this.wsClient.send(data);
60
+ }
61
+ log(...args: any[]): void {
62
+ if (this.showLog)
63
+ console.log('[Ws]', ...args);
64
+ }
65
+ }