imean-service-engine 2.0.0 → 2.0.2
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/index.d.mts +77 -52
- package/dist/index.d.ts +77 -52
- package/dist/index.js +2078 -1945
- package/dist/index.mjs +2076 -1944
- package/package.json +9 -2
- package/.vscode/settings.json +0 -8
- package/src/core/checker.ts +0 -33
- package/src/core/decorators.test.ts +0 -96
- package/src/core/decorators.ts +0 -68
- package/src/core/engine.test.ts +0 -218
- package/src/core/engine.ts +0 -635
- package/src/core/errors.ts +0 -28
- package/src/core/factory.test.ts +0 -73
- package/src/core/factory.ts +0 -92
- package/src/core/logger.ts +0 -65
- package/src/core/testing.ts +0 -73
- package/src/core/types.ts +0 -191
- package/src/index.ts +0 -49
- package/src/metadata/README.md +0 -422
- package/src/metadata/metadata.test.ts +0 -369
- package/src/metadata/metadata.ts +0 -512
- package/src/plugins/action/action-plugin.test.ts +0 -660
- package/src/plugins/action/decorator.ts +0 -14
- package/src/plugins/action/index.ts +0 -4
- package/src/plugins/action/plugin.ts +0 -349
- package/src/plugins/action/types.ts +0 -49
- package/src/plugins/action/utils.test.ts +0 -196
- package/src/plugins/action/utils.ts +0 -111
- package/src/plugins/cache/adapter.test.ts +0 -689
- package/src/plugins/cache/adapter.ts +0 -324
- package/src/plugins/cache/cache-plugin.test.ts +0 -269
- package/src/plugins/cache/decorator.ts +0 -26
- package/src/plugins/cache/index.ts +0 -20
- package/src/plugins/cache/plugin.ts +0 -299
- package/src/plugins/cache/types.ts +0 -69
- package/src/plugins/client-code/client-code-plugin.test.ts +0 -511
- package/src/plugins/client-code/format.ts +0 -9
- package/src/plugins/client-code/generator.test.ts +0 -52
- package/src/plugins/client-code/generator.ts +0 -263
- package/src/plugins/client-code/index.ts +0 -15
- package/src/plugins/client-code/plugin.ts +0 -158
- package/src/plugins/client-code/types.ts +0 -52
- package/src/plugins/client-code/utils.ts +0 -164
- package/src/plugins/graceful-shutdown/graceful-shutdown-plugin.test.ts +0 -401
- package/src/plugins/graceful-shutdown/index.ts +0 -3
- package/src/plugins/graceful-shutdown/plugin.ts +0 -279
- package/src/plugins/graceful-shutdown/types.ts +0 -17
- package/src/plugins/rate-limit/rate-limit-plugin.example.ts +0 -171
- package/src/plugins/route/components/Layout.tsx +0 -42
- package/src/plugins/route/components/ServiceStatusPage.tsx +0 -141
- package/src/plugins/route/decorator.ts +0 -50
- package/src/plugins/route/index.ts +0 -16
- package/src/plugins/route/plugin.ts +0 -218
- package/src/plugins/route/route-plugin.test.ts +0 -759
- package/src/plugins/route/types.ts +0 -72
- package/src/plugins/schedule/README.md +0 -309
- package/src/plugins/schedule/decorator.ts +0 -25
- package/src/plugins/schedule/index.ts +0 -12
- package/src/plugins/schedule/mock-etcd.ts +0 -145
- package/src/plugins/schedule/plugin.ts +0 -164
- package/src/plugins/schedule/schedule-plugin.test.ts +0 -312
- package/src/plugins/schedule/scheduler.ts +0 -164
- package/src/plugins/schedule/types.ts +0 -94
- package/src/plugins/schedule/utils.test.ts +0 -163
- package/src/plugins/schedule/utils.ts +0 -41
- package/tests/integration/client.test.ts +0 -203
- package/tests/integration/dev-service.ts +0 -301
- package/tests/integration/generated/client.ts +0 -123
- package/tests/integration/start-service.ts +0 -21
- package/tsconfig.json +0 -27
- package/tsup.config.ts +0 -16
- package/vitest.config.ts +0 -19
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RateLimitPlugin 示例实现
|
|
3
|
-
* 演示如何与 CachePlugin 配合工作
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
HandlerMetadata,
|
|
8
|
-
Microservice,
|
|
9
|
-
Plugin,
|
|
10
|
-
PluginModuleOptionsSchema,
|
|
11
|
-
} from "../../core/types";
|
|
12
|
-
|
|
13
|
-
export interface RateLimitOptions {
|
|
14
|
-
maxRequests?: number; // 最大请求数
|
|
15
|
-
windowMs?: number; // 时间窗口(毫秒)
|
|
16
|
-
keyGenerator?: (methodName: string, ...args: any[]) => string; // 限流键生成器
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface RateLimitModuleOptions {
|
|
20
|
-
rateLimitMaxRequests?: number; // 模块级默认最大请求数
|
|
21
|
-
rateLimitWindowMs?: number; // 模块级默认时间窗口
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* RateLimitPlugin - 限流插件
|
|
26
|
-
*
|
|
27
|
-
* 执行顺序说明:
|
|
28
|
-
* - 如果 RateLimitPlugin 在 CachePlugin 之前注册,限流在外层,缓存在内层
|
|
29
|
-
* 请求流程:限流检查 → 缓存检查 → 业务逻辑
|
|
30
|
-
* - 如果 RateLimitPlugin 在 CachePlugin 之后注册,缓存在外层,限流在内层
|
|
31
|
-
* 请求流程:缓存检查 → 限流检查 → 业务逻辑
|
|
32
|
-
*
|
|
33
|
-
* 推荐顺序:RateLimitPlugin → CachePlugin
|
|
34
|
-
* 这样可以避免缓存命中时仍然进行限流检查
|
|
35
|
-
*/
|
|
36
|
-
export class RateLimitPlugin implements Plugin<RateLimitModuleOptions> {
|
|
37
|
-
public readonly name = "rate-limit-plugin";
|
|
38
|
-
private requestCounts: Map<string, { count: number; resetAt: number }> =
|
|
39
|
-
new Map();
|
|
40
|
-
private defaultMaxRequests: number = 100;
|
|
41
|
-
private defaultWindowMs: number = 60000; // 1分钟
|
|
42
|
-
private engine: Microservice | null = null;
|
|
43
|
-
|
|
44
|
-
getModuleOptionsSchema(): PluginModuleOptionsSchema<RateLimitModuleOptions> {
|
|
45
|
-
return {
|
|
46
|
-
_type: {} as RateLimitModuleOptions,
|
|
47
|
-
validate: (options) => {
|
|
48
|
-
if (
|
|
49
|
-
options.rateLimitMaxRequests !== undefined &&
|
|
50
|
-
options.rateLimitMaxRequests <= 0
|
|
51
|
-
) {
|
|
52
|
-
return `rateLimitMaxRequests must be greater than 0`;
|
|
53
|
-
}
|
|
54
|
-
if (
|
|
55
|
-
options.rateLimitWindowMs !== undefined &&
|
|
56
|
-
options.rateLimitWindowMs <= 0
|
|
57
|
-
) {
|
|
58
|
-
return `rateLimitWindowMs must be greater than 0`;
|
|
59
|
-
}
|
|
60
|
-
return true;
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
onInit(engine: Microservice): void {
|
|
66
|
-
this.engine = engine;
|
|
67
|
-
console.log("[RateLimitPlugin] Initialized rate limit storage");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
onHandlerLoad(handlers: HandlerMetadata[]): void {
|
|
71
|
-
// 筛选出所有 type="rate-limit" 的 Handler 元数据
|
|
72
|
-
const rateLimitHandlers = handlers.filter(
|
|
73
|
-
(handler) => handler.type === "rate-limit"
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
console.log(
|
|
77
|
-
`[RateLimitPlugin] Found ${rateLimitHandlers.length} rate limit handlers`
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
// 遍历限流 Handler,包装方法
|
|
81
|
-
// 注意:使用原型上的当前方法(可能已被其他插件包装),而不是 handler.method
|
|
82
|
-
for (const handler of rateLimitHandlers) {
|
|
83
|
-
const methodName = handler.methodName;
|
|
84
|
-
const moduleClass = handler.module;
|
|
85
|
-
const rateLimitOptions = handler.options as RateLimitOptions;
|
|
86
|
-
|
|
87
|
-
// 获取模块元数据以读取配置
|
|
88
|
-
const moduleMetadata = this.engine
|
|
89
|
-
?.getModules()
|
|
90
|
-
.find((m) => m.clazz === moduleClass);
|
|
91
|
-
|
|
92
|
-
if (!moduleMetadata) {
|
|
93
|
-
console.warn(
|
|
94
|
-
`[RateLimitPlugin] Module metadata not found for ${moduleClass.name}`
|
|
95
|
-
);
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// 获取模块配置
|
|
100
|
-
const moduleOptions = moduleMetadata.options as RateLimitModuleOptions;
|
|
101
|
-
const maxRequests =
|
|
102
|
-
rateLimitOptions.maxRequests ??
|
|
103
|
-
moduleOptions?.rateLimitMaxRequests ??
|
|
104
|
-
this.defaultMaxRequests;
|
|
105
|
-
const windowMs =
|
|
106
|
-
rateLimitOptions.windowMs ??
|
|
107
|
-
moduleOptions?.rateLimitWindowMs ??
|
|
108
|
-
this.defaultWindowMs;
|
|
109
|
-
|
|
110
|
-
// 生成限流键的函数
|
|
111
|
-
const keyGenerator =
|
|
112
|
-
rateLimitOptions.keyGenerator ||
|
|
113
|
-
((methodName: string, ...args: any[]) =>
|
|
114
|
-
`${methodName}:${JSON.stringify(args)}`);
|
|
115
|
-
|
|
116
|
-
// 获取当前原型上的方法(可能已被其他插件包装)
|
|
117
|
-
const prototype = moduleClass.prototype;
|
|
118
|
-
const currentMethod = prototype[methodName];
|
|
119
|
-
|
|
120
|
-
// 包装当前方法(支持插件链式包装)
|
|
121
|
-
const rateLimitedMethod = async (...args: any[]) => {
|
|
122
|
-
// 生成限流键
|
|
123
|
-
const limitKey = keyGenerator(methodName, ...args);
|
|
124
|
-
const now = Date.now();
|
|
125
|
-
|
|
126
|
-
// 获取或初始化计数器
|
|
127
|
-
let counter = this.requestCounts.get(limitKey);
|
|
128
|
-
if (!counter || counter.resetAt <= now) {
|
|
129
|
-
counter = { count: 0, resetAt: now + windowMs };
|
|
130
|
-
this.requestCounts.set(limitKey, counter);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 检查限流
|
|
134
|
-
if (counter.count >= maxRequests) {
|
|
135
|
-
console.log(
|
|
136
|
-
`[RateLimitPlugin] Rate limit exceeded for ${limitKey} (${counter.count}/${maxRequests})`
|
|
137
|
-
);
|
|
138
|
-
throw new Error(
|
|
139
|
-
`Rate limit exceeded: ${counter.count} requests in ${windowMs}ms`
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// 增加计数
|
|
144
|
-
counter.count++;
|
|
145
|
-
console.log(
|
|
146
|
-
`[RateLimitPlugin] Request allowed for ${limitKey} (${counter.count}/${maxRequests})`
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
// 执行当前方法(可能是原始方法,也可能是被其他插件包装后的)
|
|
150
|
-
const result = await currentMethod.apply(
|
|
151
|
-
this.engine?.get(moduleClass),
|
|
152
|
-
args
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
return result;
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
// 替换原型上的方法
|
|
159
|
-
prototype[methodName] = rateLimitedMethod;
|
|
160
|
-
|
|
161
|
-
console.log(
|
|
162
|
-
`[RateLimitPlugin] Wrapped ${moduleClass.name}.${methodName} with rate limit (${maxRequests} requests per ${windowMs}ms)`
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
onDestroy(): void {
|
|
168
|
-
this.requestCounts.clear();
|
|
169
|
-
console.log("[RateLimitPlugin] Rate limit storage destroyed");
|
|
170
|
-
}
|
|
171
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { html } from "hono/html";
|
|
2
|
-
|
|
3
|
-
const DEFAULT_FAVICON = (
|
|
4
|
-
<link
|
|
5
|
-
rel="icon"
|
|
6
|
-
href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' width='100' height='100'><defs><linearGradient id='nodeGradient' x1='0%' y1='0%' x2='100%' y2='100%'><stop offset='0%' stop-color='%233498db'/><stop offset='100%' stop-color='%232980b9'/></linearGradient><linearGradient id='centerNodeGradient' x1='0%' y1='0%' x2='100%' y2='100%'><stop offset='0%' stop-color='%232ecc71'/><stop offset='100%' stop-color='%2327ae60'/></linearGradient></defs><circle cx='50' cy='50' r='45' fill='%23f5f7fa'/><path d='M30,30 L50,50' stroke='%23bdc3c7' stroke-width='2' stroke-linecap='round'/><path d='M70,30 L50,50' stroke='%23bdc3c7' stroke-width='2' stroke-linecap='round'/><path d='M30,70 L50,50' stroke='%23bdc3c7' stroke-width='2' stroke-linecap='round'/><path d='M70,70 L50,50' stroke='%23bdc3c7' stroke-width='2' stroke-linecap='round'/><polygon points='30,15 45,25 45,45 30,55 15,45 15,25' fill='url(%23nodeGradient)' stroke='%232980b9' stroke-width='1.5'/><polygon points='70,15 85,25 85,45 70,55 55,45 55,25' fill='url(%23nodeGradient)' stroke='%232980b9' stroke-width='1.5'/><polygon points='30,45 45,55 45,75 30,85 15,75 15,55' fill='url(%23nodeGradient)' stroke='%232980b9' stroke-width='1.5'/><polygon points='70,45 85,55 85,75 70,85 55,75 55,55' fill='url(%23nodeGradient)' stroke='%232980b9' stroke-width='1.5'/><polygon points='50,30 65,40 65,60 50,70 35,60 35,40' fill='url(%23centerNodeGradient)' stroke='%2327ae60' stroke-width='2'/><circle cx='30' cy='30' r='3' fill='%23ffffff'/><circle cx='70' cy='30' r='3' fill='%23ffffff'/><circle cx='30' cy='70' r='3' fill='%23ffffff'/><circle cx='70' cy='70' r='3' fill='%23ffffff'/><circle cx='50' cy='50' r='4' fill='%23ffffff'/></svg>"
|
|
7
|
-
type="image/svg+xml"
|
|
8
|
-
></link>
|
|
9
|
-
);
|
|
10
|
-
|
|
11
|
-
export const BaseLayout = (
|
|
12
|
-
props: { children?: any; title?: string; heads?: any } = {
|
|
13
|
-
title: "Microservice Template",
|
|
14
|
-
}
|
|
15
|
-
) =>
|
|
16
|
-
html`<!DOCTYPE html>
|
|
17
|
-
<html>
|
|
18
|
-
<head>
|
|
19
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
20
|
-
<title>${props.title}</title>
|
|
21
|
-
${props.heads}
|
|
22
|
-
</head>
|
|
23
|
-
<body>
|
|
24
|
-
${props.children}
|
|
25
|
-
</body>
|
|
26
|
-
</html>`;
|
|
27
|
-
|
|
28
|
-
export const HtmxLayout = (
|
|
29
|
-
props: { children?: any; title: string; favicon?: any } = {
|
|
30
|
-
title: "Microservice Template",
|
|
31
|
-
}
|
|
32
|
-
) =>
|
|
33
|
-
BaseLayout({
|
|
34
|
-
title: props.title,
|
|
35
|
-
heads: html`
|
|
36
|
-
<script src="https://unpkg.com/htmx.org@latest"></script>
|
|
37
|
-
<script src="https://unpkg.com/hyperscript.org@latest"></script>
|
|
38
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
39
|
-
${props.favicon || DEFAULT_FAVICON}
|
|
40
|
-
`,
|
|
41
|
-
children: props.children,
|
|
42
|
-
});
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { type ServiceInfo } from "../../../mod.ts";
|
|
2
|
-
|
|
3
|
-
interface InfoCardProps {
|
|
4
|
-
icon: string;
|
|
5
|
-
iconColor: string;
|
|
6
|
-
bgColor: string;
|
|
7
|
-
label: string;
|
|
8
|
-
value: string | any;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const InfoCard = ({
|
|
12
|
-
icon,
|
|
13
|
-
iconColor,
|
|
14
|
-
bgColor,
|
|
15
|
-
label,
|
|
16
|
-
value,
|
|
17
|
-
}: InfoCardProps) => (
|
|
18
|
-
<div className={`${bgColor} p-4 rounded-lg`}>
|
|
19
|
-
<div className="flex items-center mb-2">
|
|
20
|
-
<svg
|
|
21
|
-
className={`w-5 h-5 ${iconColor} mr-2`}
|
|
22
|
-
fill="none"
|
|
23
|
-
stroke="currentColor"
|
|
24
|
-
viewBox="0 0 24 24"
|
|
25
|
-
>
|
|
26
|
-
<path
|
|
27
|
-
strokeLinecap="round"
|
|
28
|
-
strokeLinejoin="round"
|
|
29
|
-
strokeWidth={2}
|
|
30
|
-
d={icon}
|
|
31
|
-
/>
|
|
32
|
-
</svg>
|
|
33
|
-
<span className="text-sm font-medium text-gray-600">{label}</span>
|
|
34
|
-
</div>
|
|
35
|
-
<p className={`text-xl font-semibold text-gray-900`}>{value}</p>
|
|
36
|
-
</div>
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
const getEnvironmentBadgeClass = (env: string): string => {
|
|
40
|
-
switch (env) {
|
|
41
|
-
case "prod":
|
|
42
|
-
return "bg-red-100 text-red-800";
|
|
43
|
-
case "stg":
|
|
44
|
-
return "bg-yellow-100 text-yellow-800";
|
|
45
|
-
case "dev":
|
|
46
|
-
default:
|
|
47
|
-
return "bg-blue-100 text-blue-800";
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export const ServiceInfoCards = ({
|
|
52
|
-
serviceInfo,
|
|
53
|
-
}: {
|
|
54
|
-
serviceInfo: ServiceInfo;
|
|
55
|
-
}) => {
|
|
56
|
-
const infoCards = [
|
|
57
|
-
{
|
|
58
|
-
icon: "M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2m-9 0h10m-10 0a2 2 0 00-2 2v14a2 2 0 002 2h10a2 2 0 002-2V6a2 2 0 00-2-2",
|
|
59
|
-
iconColor: "text-blue-600",
|
|
60
|
-
bgColor: "bg-blue-50",
|
|
61
|
-
label: "服务名称",
|
|
62
|
-
value: serviceInfo.name,
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
icon: "M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1",
|
|
66
|
-
iconColor: "text-orange-600",
|
|
67
|
-
bgColor: "bg-orange-50",
|
|
68
|
-
label: "服务路径",
|
|
69
|
-
value: serviceInfo.prefix || "/",
|
|
70
|
-
isMonospace: true,
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
icon: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z",
|
|
74
|
-
iconColor: "text-green-600",
|
|
75
|
-
bgColor: "bg-green-50",
|
|
76
|
-
label: "运行环境",
|
|
77
|
-
value: (
|
|
78
|
-
<span
|
|
79
|
-
className={`px-2 py-1 rounded-full text-sm ${getEnvironmentBadgeClass(serviceInfo.env ?? "dev")}`}
|
|
80
|
-
>
|
|
81
|
-
{serviceInfo.env ?? "dev"}
|
|
82
|
-
</span>
|
|
83
|
-
),
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
icon: "M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z",
|
|
87
|
-
iconColor: "text-purple-600",
|
|
88
|
-
bgColor: "bg-purple-50",
|
|
89
|
-
label: "版本号",
|
|
90
|
-
value: serviceInfo.version || "unknown",
|
|
91
|
-
},
|
|
92
|
-
];
|
|
93
|
-
|
|
94
|
-
return (
|
|
95
|
-
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
|
|
96
|
-
<h2 className="text-2xl font-semibold text-gray-800 mb-6 flex items-center">
|
|
97
|
-
<svg
|
|
98
|
-
className="w-6 h-6 mr-2 text-blue-600"
|
|
99
|
-
fill="none"
|
|
100
|
-
stroke="currentColor"
|
|
101
|
-
viewBox="0 0 24 24"
|
|
102
|
-
>
|
|
103
|
-
<path
|
|
104
|
-
strokeLinecap="round"
|
|
105
|
-
strokeLinejoin="round"
|
|
106
|
-
strokeWidth={2}
|
|
107
|
-
d="M13 10V3L4 14h7v7l9-11h-7z"
|
|
108
|
-
/>
|
|
109
|
-
</svg>
|
|
110
|
-
服务基本信息
|
|
111
|
-
</h2>
|
|
112
|
-
|
|
113
|
-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-6">
|
|
114
|
-
{infoCards.map((card, index) => (
|
|
115
|
-
<InfoCard key={index} {...card} />
|
|
116
|
-
))}
|
|
117
|
-
</div>
|
|
118
|
-
</div>
|
|
119
|
-
);
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
export const ServiceStatusPage = ({
|
|
123
|
-
serviceInfo,
|
|
124
|
-
}: {
|
|
125
|
-
serviceInfo: ServiceInfo;
|
|
126
|
-
}) => {
|
|
127
|
-
return (
|
|
128
|
-
<div className="min-h-screen bg-gray-50 py-8">
|
|
129
|
-
<div className="max-w-6xl mx-auto px-4">
|
|
130
|
-
<div className="mb-8">
|
|
131
|
-
<h1 className="text-4xl font-bold text-gray-900 mb-2">
|
|
132
|
-
Service Status
|
|
133
|
-
</h1>
|
|
134
|
-
</div>
|
|
135
|
-
<ServiceInfoCards serviceInfo={serviceInfo} />
|
|
136
|
-
</div>
|
|
137
|
-
</div>
|
|
138
|
-
);
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
export default ServiceStatusPage;
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { Handler } from "../../core/decorators";
|
|
2
|
-
import { RouteOptions } from "./types";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Route装饰器(Handler的语法糖封装)
|
|
6
|
-
* 固定type="route",简化HTTP路由标注
|
|
7
|
-
* 使用最新的 Stage 3 装饰器标准
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* ```typescript
|
|
11
|
-
* @Route({ method: "GET", path: "/users" })
|
|
12
|
-
* async getUsers(ctx: Context) {
|
|
13
|
-
* return ctx.json({ users: [] });
|
|
14
|
-
* }
|
|
15
|
-
*
|
|
16
|
-
* // 支持多个路径
|
|
17
|
-
* @Route({ path: ["/", "/home", "/dashboard"] })
|
|
18
|
-
* async homePage(ctx: Context) {
|
|
19
|
-
* return <HomePage />;
|
|
20
|
-
* }
|
|
21
|
-
* ```
|
|
22
|
-
*/
|
|
23
|
-
export function Route(options: RouteOptions) {
|
|
24
|
-
return Handler({
|
|
25
|
-
type: "route",
|
|
26
|
-
options,
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Page装饰器(Route的别名,用于向后兼容)
|
|
32
|
-
* 专门用于页面路由,默认使用 GET 方法
|
|
33
|
-
*
|
|
34
|
-
* @example
|
|
35
|
-
* ```typescript
|
|
36
|
-
* @Page({ path: ["/", "/home"] })
|
|
37
|
-
* async adminPage(ctx: Context) {
|
|
38
|
-
* return HtmxLayout({ title: "Admin", children: <AdminLayout /> });
|
|
39
|
-
* }
|
|
40
|
-
* ```
|
|
41
|
-
*/
|
|
42
|
-
export function Page(options: RouteOptions) {
|
|
43
|
-
// 如果没有指定 method,默认为 GET
|
|
44
|
-
const pageOptions: RouteOptions = {
|
|
45
|
-
method: options.method || "GET",
|
|
46
|
-
...options,
|
|
47
|
-
};
|
|
48
|
-
return Route(pageOptions);
|
|
49
|
-
}
|
|
50
|
-
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Route Plugin - 路由插件
|
|
3
|
-
*
|
|
4
|
-
* 提供HTTP路由功能,支持模块级前缀和中间件
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export { RoutePlugin } from "./plugin";
|
|
8
|
-
export { Route, Page } from "./decorator";
|
|
9
|
-
export type {
|
|
10
|
-
RouteModuleOptions,
|
|
11
|
-
RouteOptions,
|
|
12
|
-
RoutePluginOptions,
|
|
13
|
-
HTTPMethod,
|
|
14
|
-
} from "./types";
|
|
15
|
-
export { BaseLayout, HtmxLayout } from "./components/Layout";
|
|
16
|
-
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import { Context, Hono, MiddlewareHandler } from "hono";
|
|
2
|
-
import {
|
|
3
|
-
HandlerMetadata,
|
|
4
|
-
Microservice,
|
|
5
|
-
Plugin,
|
|
6
|
-
PluginModuleOptionsSchema,
|
|
7
|
-
PluginPriority,
|
|
8
|
-
} from "../../core/types";
|
|
9
|
-
import logger from "../../core/logger";
|
|
10
|
-
import { RouteModuleOptions, RouteOptions, RoutePluginOptions } from "./types";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* RoutePlugin - 核心路由插件
|
|
14
|
-
* 负责解析type="route"的Handler元数据,注册HTTP路由到Hono实例
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* ```typescript
|
|
18
|
-
* // 使用全局中间件(如鉴权)
|
|
19
|
-
* const authMiddleware = async (ctx, next) => {
|
|
20
|
-
* // 验证 token
|
|
21
|
-
* await next();
|
|
22
|
-
* };
|
|
23
|
-
*
|
|
24
|
-
* const routePlugin = new RoutePlugin({
|
|
25
|
-
* globalMiddlewares: [authMiddleware],
|
|
26
|
-
* });
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
|
-
export class RoutePlugin implements Plugin<RouteModuleOptions> {
|
|
30
|
-
public readonly name = "route-plugin";
|
|
31
|
-
public readonly priority = PluginPriority.ROUTE;
|
|
32
|
-
private engine!: Microservice;
|
|
33
|
-
private globalMiddlewares: MiddlewareHandler[];
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 构造函数
|
|
37
|
-
* @param options 插件配置选项
|
|
38
|
-
*/
|
|
39
|
-
constructor(options?: RoutePluginOptions) {
|
|
40
|
-
this.globalMiddlewares = options?.globalMiddlewares || [];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* 声明Module配置Schema(用于类型推导+运行时校验)
|
|
45
|
-
*/
|
|
46
|
-
getModuleOptionsSchema(): PluginModuleOptionsSchema<RouteModuleOptions> {
|
|
47
|
-
return {
|
|
48
|
-
_type: {} as RouteModuleOptions,
|
|
49
|
-
validate: (options) => {
|
|
50
|
-
if (
|
|
51
|
-
options.routePrefix !== undefined &&
|
|
52
|
-
typeof options.routePrefix !== "string"
|
|
53
|
-
) {
|
|
54
|
-
return "routePrefix must be a string";
|
|
55
|
-
}
|
|
56
|
-
if (options.routePrefix && !options.routePrefix.startsWith("/")) {
|
|
57
|
-
return `routePrefix must start with '/'`;
|
|
58
|
-
}
|
|
59
|
-
if (
|
|
60
|
-
options.routeMiddlewares !== undefined &&
|
|
61
|
-
!Array.isArray(options.routeMiddlewares)
|
|
62
|
-
) {
|
|
63
|
-
return "routeMiddlewares must be an array";
|
|
64
|
-
}
|
|
65
|
-
return true;
|
|
66
|
-
},
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* 引擎初始化钩子:获取Hono实例
|
|
72
|
-
*/
|
|
73
|
-
onInit(engine: Microservice): void {
|
|
74
|
-
this.engine = engine;
|
|
75
|
-
logger.info("RoutePlugin initialized");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Handler加载钩子:解析type="route"的Handler元数据,注册HTTP路由
|
|
80
|
-
*/
|
|
81
|
-
onHandlerLoad(handlers: HandlerMetadata[]): void {
|
|
82
|
-
// 筛选出所有type="route"的Handler元数据
|
|
83
|
-
const routeHandlers = handlers.filter(
|
|
84
|
-
(handler) => handler.type === "route"
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
logger.info(`Found ${routeHandlers.length} route handler(s)`);
|
|
88
|
-
|
|
89
|
-
for (const handler of routeHandlers) {
|
|
90
|
-
const routeOptions = handler.options as RouteOptions;
|
|
91
|
-
const methodName = handler.methodName;
|
|
92
|
-
const moduleClass = handler.module;
|
|
93
|
-
|
|
94
|
-
// 获取模块实例
|
|
95
|
-
const moduleInstance = this.engine.get(moduleClass);
|
|
96
|
-
if (!moduleInstance) {
|
|
97
|
-
logger.warn(
|
|
98
|
-
`Module instance not found for ${moduleClass.name}, skipping route registration`
|
|
99
|
-
);
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 获取模块配置
|
|
104
|
-
const moduleMetadata = this.engine
|
|
105
|
-
.getModules()
|
|
106
|
-
.find((m: any) => m.clazz === moduleClass);
|
|
107
|
-
const moduleOptions = (moduleMetadata?.options ||
|
|
108
|
-
{}) as RouteModuleOptions;
|
|
109
|
-
|
|
110
|
-
// 构建路由路径数组(支持单个路径或多个路径)
|
|
111
|
-
const routePrefix = moduleOptions.routePrefix || "";
|
|
112
|
-
const paths = Array.isArray(routeOptions.path)
|
|
113
|
-
? routeOptions.path
|
|
114
|
-
: [routeOptions.path];
|
|
115
|
-
const fullPaths = paths.map((p) => routePrefix + p);
|
|
116
|
-
|
|
117
|
-
// 构建路由处理器
|
|
118
|
-
const routeHandler = async (ctx: Context) => {
|
|
119
|
-
// 引擎保证此时原型上的方法已经被所有包装插件包装
|
|
120
|
-
// 直接调用方法名即可获取包装后的方法
|
|
121
|
-
const method = (moduleInstance as any)[methodName];
|
|
122
|
-
if (typeof method !== "function") {
|
|
123
|
-
return ctx.json({ error: "Handler method not found" }, 500);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
const result = await method.call(moduleInstance, ctx);
|
|
128
|
-
// Hono 会自动处理 JSX/HTML/Response 响应
|
|
129
|
-
// 如果返回的是普通对象,自动转换为 JSON
|
|
130
|
-
if (result instanceof Response) {
|
|
131
|
-
return result;
|
|
132
|
-
}
|
|
133
|
-
// 如果返回的是 JSX 或其他 Hono 支持的类型,直接返回
|
|
134
|
-
if (
|
|
135
|
-
typeof result === "string" ||
|
|
136
|
-
(typeof result === "object" && result !== null && "type" in result)
|
|
137
|
-
) {
|
|
138
|
-
return result;
|
|
139
|
-
}
|
|
140
|
-
// 否则作为 JSON 返回
|
|
141
|
-
return ctx.json(result);
|
|
142
|
-
} catch (error) {
|
|
143
|
-
logger.error(
|
|
144
|
-
`Error in route handler ${moduleClass.name}.${methodName}`,
|
|
145
|
-
error
|
|
146
|
-
);
|
|
147
|
-
return ctx.json(
|
|
148
|
-
{
|
|
149
|
-
error: "Internal server error",
|
|
150
|
-
message: error instanceof Error ? error.message : String(error),
|
|
151
|
-
},
|
|
152
|
-
500
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
// 获取 Hono 实例
|
|
158
|
-
const hono = this.engine.getHono();
|
|
159
|
-
|
|
160
|
-
// 注册路由(支持单个方法或多个方法,默认为 GET)
|
|
161
|
-
const methods = routeOptions.method
|
|
162
|
-
? Array.isArray(routeOptions.method)
|
|
163
|
-
? routeOptions.method
|
|
164
|
-
: [routeOptions.method]
|
|
165
|
-
: ["GET"];
|
|
166
|
-
|
|
167
|
-
// 为每个路径注册路由
|
|
168
|
-
for (const fullPath of fullPaths) {
|
|
169
|
-
let route = hono;
|
|
170
|
-
|
|
171
|
-
// 应用全局中间件(最先执行)
|
|
172
|
-
if (this.globalMiddlewares.length > 0) {
|
|
173
|
-
route = route.use(fullPath, ...this.globalMiddlewares);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// 应用模块级中间件
|
|
177
|
-
if (
|
|
178
|
-
moduleOptions.routeMiddlewares &&
|
|
179
|
-
moduleOptions.routeMiddlewares.length > 0
|
|
180
|
-
) {
|
|
181
|
-
route = route.use(fullPath, ...moduleOptions.routeMiddlewares);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// 应用路由级中间件(最后执行)
|
|
185
|
-
if (routeOptions.middlewares && routeOptions.middlewares.length > 0) {
|
|
186
|
-
route = route.use(fullPath, ...routeOptions.middlewares);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// 为每个 HTTP 方法注册路由
|
|
190
|
-
for (const method of methods) {
|
|
191
|
-
const methodLower = method.toLowerCase();
|
|
192
|
-
if (
|
|
193
|
-
methodLower === "get" ||
|
|
194
|
-
methodLower === "post" ||
|
|
195
|
-
methodLower === "put" ||
|
|
196
|
-
methodLower === "delete" ||
|
|
197
|
-
methodLower === "patch" ||
|
|
198
|
-
methodLower === "head" ||
|
|
199
|
-
methodLower === "options"
|
|
200
|
-
) {
|
|
201
|
-
(route as any)[methodLower](fullPath, routeHandler);
|
|
202
|
-
const description = routeOptions.description
|
|
203
|
-
? ` (${routeOptions.description})`
|
|
204
|
-
: "";
|
|
205
|
-
logger.info(
|
|
206
|
-
`Registered route: ${method.toUpperCase()} ${fullPath} -> ${moduleClass.name}.${methodName}${description}`
|
|
207
|
-
);
|
|
208
|
-
} else {
|
|
209
|
-
logger.warn(
|
|
210
|
-
`Unsupported HTTP method: ${method}, skipping route ${fullPath}`
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
}
|