@eggjs/skills 0.0.0 → 4.1.2-beta.10
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/LICENSE +21 -0
- package/egg/SKILL.md +304 -0
- package/egg-controller/SKILL.md +105 -0
- package/egg-controller/references/ajv-validate.md +140 -0
- package/egg-controller/references/http-controller.md +464 -0
- package/egg-controller/references/mcp-controller.md +293 -0
- package/egg-controller/references/middleware.md +133 -0
- package/egg-controller/references/schedule.md +110 -0
- package/egg-core/SKILL.md +248 -0
- package/egg-core/references/aop.md +223 -0
- package/egg-core/references/background-task.md +121 -0
- package/egg-core/references/dynamic-inject.md +90 -0
- package/egg-core/references/eventbus.md +138 -0
- package/egg-core/references/inject.md +258 -0
- package/egg-core/references/module.md +202 -0
- package/egg-core/references/proto.md +181 -0
- package/egg-unittest/SKILL.md +149 -0
- package/egg-unittest/references/background-task-test.md +53 -0
- package/egg-unittest/references/eventbus-test.md +47 -0
- package/egg-unittest/references/http-test.md +145 -0
- package/egg-unittest/references/mock.md +108 -0
- package/egg-unittest/references/service-test.md +75 -0
- package/package.json +19 -2
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# MCPController 开发指南
|
|
2
|
+
|
|
3
|
+
## 常见错误
|
|
4
|
+
|
|
5
|
+
生成 MCPController 代码时,**必须**注意以下易错点:
|
|
6
|
+
|
|
7
|
+
| 错误写法 | 正确写法 | 说明 |
|
|
8
|
+
| -------------------------------- | ------------------------------------- | ---------------------------------------- |
|
|
9
|
+
| `from 'egg'` | `from '@eggjs/tegg'` | 所有 MCP 装饰器和类型来自 `@eggjs/tegg` |
|
|
10
|
+
| `import z from 'zod'` | `import { z } from '@eggjs/tegg/zod'` | 框架内置 zod,必须使用具名导入 |
|
|
11
|
+
| `z.object({ name: z.string() })` | `{ name: z.string() }` | Schema 使用普通对象,不要用 `z.object()` |
|
|
12
|
+
| `args: ToolArgs<MySchema>` | `args: ToolArgs<typeof MySchema>` | 类型参数必须用 `typeof` |
|
|
13
|
+
| `@MCPController` 不加括号 | `@MCPController()` | 装饰器必须带括号调用 |
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 文件约定
|
|
18
|
+
|
|
19
|
+
### 文件位置与命名
|
|
20
|
+
|
|
21
|
+
MCPController 放在 module 的 `controller/` 目录下,命名规则为 `{Name}MCPController.ts`:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
app/module-name/
|
|
25
|
+
├── controller/
|
|
26
|
+
│ ├── PackageMCPController.ts ← MCP 控制器
|
|
27
|
+
│ └── PackageHTTPController.ts ← 同模块可共存 HTTP 控制器
|
|
28
|
+
└── service/
|
|
29
|
+
└── PackageService.ts
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 插件配置
|
|
33
|
+
|
|
34
|
+
在 `config/plugin.ts` 中启用:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
plugin.mcpProxy = true;
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 路径配置
|
|
41
|
+
|
|
42
|
+
在 `config/config.default.ts` 中配置 MCP 路径(通常不需要修改,以下为默认值):
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { randomUUID } from 'node:crypto';
|
|
46
|
+
|
|
47
|
+
export default () => {
|
|
48
|
+
const config = {
|
|
49
|
+
mcp: {
|
|
50
|
+
sseInitPath: '/mcp/sse',
|
|
51
|
+
sseMessagePath: '/mcp/message',
|
|
52
|
+
streamPath: '/mcp/stream',
|
|
53
|
+
statelessStreamPath: '/mcp/stateless/stream',
|
|
54
|
+
sessionIdGenerator: randomUUID,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
return config;
|
|
58
|
+
};
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
当使用 `@MCPController({ name: 'myServer' })` 声明命名服务时,路径自动变为:
|
|
62
|
+
|
|
63
|
+
- `/mcp/myServer/sse`
|
|
64
|
+
- `/mcp/myServer/message`
|
|
65
|
+
- `/mcp/myServer/stream`
|
|
66
|
+
- `/mcp/myServer/stateless/stream`
|
|
67
|
+
|
|
68
|
+
### AccessLevel
|
|
69
|
+
|
|
70
|
+
`@MCPController` 装饰器内部已默认设置 AccessLevel(PUBLIC),不需要再手动声明。
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 场景决策树
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
用户需要什么?
|
|
78
|
+
|
|
79
|
+
├─ "让 AI 能查数据 / 执行操作"
|
|
80
|
+
│ └─ → @MCPTool + @Inject Service 处理业务
|
|
81
|
+
│
|
|
82
|
+
├─ "给 AI 一个提示词模板"
|
|
83
|
+
│ └─ → @MCPPrompt
|
|
84
|
+
│
|
|
85
|
+
├─ "让 AI 读取某类资源数据"
|
|
86
|
+
│ ├─ 资源地址固定 → @MCPResource({ uri: '...' })
|
|
87
|
+
│ └─ 资源地址动态 → @MCPResource({ template: [...] })
|
|
88
|
+
│
|
|
89
|
+
├─ "Tool 执行中要推送进度"
|
|
90
|
+
│ └─ → @MCPTool + @Extra() 获取 sendNotification(见下方 @Extra 章节)
|
|
91
|
+
│
|
|
92
|
+
└─ "Tool 中需要读取自定义请求头"
|
|
93
|
+
└─ → @MCPTool + @Extra() 获取 requestInfo.headers
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 端到端完整示例
|
|
99
|
+
|
|
100
|
+
以下展示一个完整的 MCP 功能从配置到测试的所有文件:
|
|
101
|
+
|
|
102
|
+
### 1. 插件配置 — `config/plugin.ts`
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
plugin.mcpProxy = true;
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 2. 控制器 — `app/npm/controller/PackageMCPController.ts`
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import {
|
|
112
|
+
MCPController,
|
|
113
|
+
MCPTool,
|
|
114
|
+
MCPToolResponse,
|
|
115
|
+
MCPPrompt,
|
|
116
|
+
MCPPromptResponse,
|
|
117
|
+
MCPResource,
|
|
118
|
+
MCPResourceResponse,
|
|
119
|
+
ToolArgs,
|
|
120
|
+
ToolArgsSchema,
|
|
121
|
+
PromptArgs,
|
|
122
|
+
PromptArgsSchema,
|
|
123
|
+
Inject,
|
|
124
|
+
} from '@eggjs/tegg';
|
|
125
|
+
import { z } from '@eggjs/tegg/zod';
|
|
126
|
+
|
|
127
|
+
import { PackageService } from '../service/PackageService.ts';
|
|
128
|
+
|
|
129
|
+
const SearchSchema = {
|
|
130
|
+
name: z.string({ description: 'npm package name' }),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const SummarySchema = {
|
|
134
|
+
name: z.string(),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
@MCPController()
|
|
138
|
+
export class PackageMCPController {
|
|
139
|
+
@Inject()
|
|
140
|
+
private readonly packageService: PackageService;
|
|
141
|
+
|
|
142
|
+
@MCPTool({ description: 'Search npm package info' })
|
|
143
|
+
async searchPackage(@ToolArgsSchema(SearchSchema) args: ToolArgs<typeof SearchSchema>): Promise<MCPToolResponse> {
|
|
144
|
+
const pkg = await this.packageService.findByName(args.name);
|
|
145
|
+
if (!pkg) {
|
|
146
|
+
return { content: [{ type: 'text', text: `Package ${args.name} not found` }] };
|
|
147
|
+
}
|
|
148
|
+
return { content: [{ type: 'text', text: JSON.stringify(pkg) }] };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@MCPPrompt({ description: 'Generate package summary' })
|
|
152
|
+
async summarize(@PromptArgsSchema(SummarySchema) args: PromptArgs<typeof SummarySchema>): Promise<MCPPromptResponse> {
|
|
153
|
+
return {
|
|
154
|
+
messages: [
|
|
155
|
+
{
|
|
156
|
+
role: 'user',
|
|
157
|
+
content: {
|
|
158
|
+
type: 'text',
|
|
159
|
+
text: `Summarize the npm package: ${args.name}`,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@MCPResource({
|
|
167
|
+
template: ['npm://{name}/{?version}', { list: undefined }],
|
|
168
|
+
})
|
|
169
|
+
async getPackageReadme(uri: URL): Promise<MCPResourceResponse> {
|
|
170
|
+
const name = uri.hostname;
|
|
171
|
+
const readme = await this.packageService.getReadme(name);
|
|
172
|
+
return { contents: [{ uri: uri.toString(), text: readme }] };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 3. Service — `app/npm/service/PackageService.ts`
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import { SingletonProto } from 'egg';
|
|
181
|
+
|
|
182
|
+
@SingletonProto()
|
|
183
|
+
export class PackageService {
|
|
184
|
+
async findByName(name: string) {
|
|
185
|
+
// 业务逻辑
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async getReadme(name: string): Promise<string> {
|
|
189
|
+
// 业务逻辑
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### 4. 单元测试 — `test/npm/controller/PackageMCPController.test.ts`
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import assert from 'node:assert';
|
|
198
|
+
import { app } from 'egg-mock/bootstrap';
|
|
199
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
200
|
+
|
|
201
|
+
describe('PackageMCPController', () => {
|
|
202
|
+
it('should search package via tool', async () => {
|
|
203
|
+
app.mockCsrf();
|
|
204
|
+
const client: Client = await app.mcpClient();
|
|
205
|
+
|
|
206
|
+
const tools = await client.listTools();
|
|
207
|
+
assert(tools.tools.some((t) => t.name === 'searchPackage'));
|
|
208
|
+
|
|
209
|
+
const res = await client.callTool({
|
|
210
|
+
name: 'searchPackage',
|
|
211
|
+
arguments: { name: 'egg' },
|
|
212
|
+
});
|
|
213
|
+
assert(res.content[0].type === 'text');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should get prompt', async () => {
|
|
217
|
+
app.mockCsrf();
|
|
218
|
+
const client: Client = await app.mcpClient();
|
|
219
|
+
|
|
220
|
+
const res = await client.getPrompt({
|
|
221
|
+
name: 'summarize',
|
|
222
|
+
arguments: { name: 'egg' },
|
|
223
|
+
});
|
|
224
|
+
assert(res.messages.length > 0);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should read resource', async () => {
|
|
228
|
+
app.mockCsrf();
|
|
229
|
+
const client: Client = await app.mcpClient();
|
|
230
|
+
|
|
231
|
+
const res = await client.readResource({
|
|
232
|
+
uri: 'npm://egg?version=4.0.0',
|
|
233
|
+
});
|
|
234
|
+
assert(res.contents.length > 0);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## @Extra() 的使用场景
|
|
242
|
+
|
|
243
|
+
`@Extra()` 装饰器注入 `ToolExtra` 对象,提供两个能力:
|
|
244
|
+
|
|
245
|
+
### 发送通知(长任务进度推送)
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
@MCPTool()
|
|
249
|
+
async longTask(
|
|
250
|
+
@ToolArgsSchema(Schema) args: ToolArgs<typeof Schema>,
|
|
251
|
+
@Extra() extra: ToolExtra,
|
|
252
|
+
): Promise<MCPToolResponse> {
|
|
253
|
+
const { sendNotification } = extra;
|
|
254
|
+
for (let i = 0; i < 10; i++) {
|
|
255
|
+
await sendNotification({
|
|
256
|
+
method: 'notifications/message',
|
|
257
|
+
params: { level: 'info', data: `Step ${i + 1}/10` },
|
|
258
|
+
});
|
|
259
|
+
// ... 执行步骤
|
|
260
|
+
}
|
|
261
|
+
return { content: [{ type: 'text', text: 'Done' }] };
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### 读取自定义请求头
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
@MCPTool()
|
|
269
|
+
async myTool(
|
|
270
|
+
@ToolArgsSchema(Schema) args: ToolArgs<typeof Schema>,
|
|
271
|
+
@Extra() extra: ToolExtra,
|
|
272
|
+
): Promise<MCPToolResponse> {
|
|
273
|
+
const headers = extra.requestInfo?.headers;
|
|
274
|
+
// 处理自定义 header
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## 装饰器参考
|
|
281
|
+
|
|
282
|
+
| 装饰器 | 用途 | 常用参数 | 返回类型 |
|
|
283
|
+
| --------------------- | ------------ | ------------------------------------------ | --------------------- |
|
|
284
|
+
| `@MCPController()` | 声明控制器 | `{ name?: string }` | - |
|
|
285
|
+
| `@MCPTool()` | 声明工具 | `{ name?: string, description?: string }` | `MCPToolResponse` |
|
|
286
|
+
| `@MCPPrompt()` | 声明提示词 | `{ name?: string, description?: string }` | `MCPPromptResponse` |
|
|
287
|
+
| `@MCPResource()` | 声明资源 | `{ uri: string }` 或 `{ template: [...] }` | `MCPResourceResponse` |
|
|
288
|
+
| `@ToolArgsSchema()` | Tool 参数 | Zod Schema 普通对象 | - |
|
|
289
|
+
| `@PromptArgsSchema()` | Prompt 参数 | Zod Schema 普通对象 | - |
|
|
290
|
+
| `@Extra()` | 额外上下文 | - | `ToolExtra` |
|
|
291
|
+
| `@Inject()` | 注入 Service | - | - |
|
|
292
|
+
|
|
293
|
+
**注意**:`@MCPController` 的 `version`、`timeout` 等参数通常不需要配置。
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Middleware 中间件指南
|
|
2
|
+
|
|
3
|
+
## 常见错误
|
|
4
|
+
|
|
5
|
+
| 错误写法 | 正确写法 | 说明 |
|
|
6
|
+
| ------------------------------------------ | ---------------------------------- | ----------------------------------------------- |
|
|
7
|
+
| `import { Middleware } from '@eggjs/tegg'` | `import { Middleware } from 'egg'` | Middleware 从 `egg` 导入 |
|
|
8
|
+
| `import { Advice } from 'egg'` | `import { Advice } from 'egg/aop'` | AOP 装饰器从 `egg/aop` 导入 |
|
|
9
|
+
| `@Middleware(funcMw, AdviceClass)` | 分开写两个 `@Middleware` | 同一个 `@Middleware()` 中不能混用函数式和 AOP |
|
|
10
|
+
| AOP Advice 中用实例属性存请求级状态 | 使用 `ctx.set()`/`ctx.get()` | Advice 默认 Singleton,实例属性会被并发请求共享 |
|
|
11
|
+
| 把中间件文件放在 `app/middleware/` | 放在模块目录下 | 函数式中间件放在模块中,通过 import 引用 |
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 两种中间件模式
|
|
16
|
+
|
|
17
|
+
Egg 的 `@Middleware` 装饰器支持两种中间件写法,根据传入参数类型自动识别:
|
|
18
|
+
|
|
19
|
+
- **AOP 写法(推荐)**:使用 `@Advice` 类,支持 `@Inject` 注入 Proto 依赖,拥有丰富的生命周期钩子
|
|
20
|
+
- **函数式写法(旧版兼容)**:标准 Koa 中间件函数,需要从 `ctx` 对象上手动获取依赖
|
|
21
|
+
|
|
22
|
+
新项目应优先使用 AOP 写法。函数式写法主要用于兼容旧的 egg 中间件或非常简单的场景。
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 实现中间件
|
|
27
|
+
|
|
28
|
+
### AOP 写法(推荐)
|
|
29
|
+
|
|
30
|
+
使用 `@Advice()` 装饰器定义类,实现 `IAdvice` 的 `around` 方法,写法与 Koa 中间件一致(`next` 调用目标方法)。Advice 本身是 Proto,支持 `@Inject` 注入依赖。`around` 中可以修改入参(`ctx.args`)和返回值:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// app/modules/foo/advice/LogAdvice.ts
|
|
34
|
+
import { AccessLevel, Inject, Logger } from 'egg';
|
|
35
|
+
import { Advice, IAdvice, AdviceContext } from 'egg/aop';
|
|
36
|
+
|
|
37
|
+
// 跨模块使用时需设置 accessLevel: AccessLevel.PUBLIC
|
|
38
|
+
@Advice({ accessLevel: AccessLevel.PUBLIC })
|
|
39
|
+
export class LogAdvice implements IAdvice {
|
|
40
|
+
@Inject()
|
|
41
|
+
logger: Logger;
|
|
42
|
+
|
|
43
|
+
async around(ctx: AdviceContext, next: () => Promise<any>): Promise<any> {
|
|
44
|
+
// 修改入参:ctx.args 对应控制器方法的参数列表
|
|
45
|
+
// ctx.args[0] = sanitize(ctx.args[0]);
|
|
46
|
+
|
|
47
|
+
const start = Date.now();
|
|
48
|
+
const result = await next();
|
|
49
|
+
this.logger.info('%s cost %dms', ctx.method, Date.now() - start);
|
|
50
|
+
|
|
51
|
+
// 修改返回值:直接返回新的值即可
|
|
52
|
+
return { success: true, data: result };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 函数式写法(旧版兼容)
|
|
58
|
+
|
|
59
|
+
标准 Koa 中间件函数,签名为 `(ctx: Context, next: Next) => Promise<void>`。旧版 egg 写法,无法使用 `@Inject`,需要从 `ctx` 对象上手动获取依赖:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// app/modules/foo/middleware/count.ts
|
|
63
|
+
import type { Context, Next } from 'egg';
|
|
64
|
+
|
|
65
|
+
export async function countMw(ctx: Context, next: Next): Promise<void> {
|
|
66
|
+
const start = Date.now();
|
|
67
|
+
await next();
|
|
68
|
+
ctx.set('X-Response-Time', `${Date.now() - start}ms`);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 应用中间件
|
|
75
|
+
|
|
76
|
+
通过 `@Middleware()` 装饰器将中间件应用到控制器,支持类级别和方法级别:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// app/modules/foo/FooController.ts
|
|
80
|
+
import { HTTPController, HTTPMethod, HTTPMethodEnum, Middleware } from 'egg';
|
|
81
|
+
import { LogAdvice } from '../common/advice/LogAdvice.ts';
|
|
82
|
+
import { countMw } from './middleware/count.ts';
|
|
83
|
+
|
|
84
|
+
@HTTPController({ path: '/api' })
|
|
85
|
+
@Middleware(LogAdvice) // 类级别:所有方法都会执行
|
|
86
|
+
export class FooController {
|
|
87
|
+
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/profile' })
|
|
88
|
+
@Middleware(countMw) // 方法级别:仅此方法执行
|
|
89
|
+
async getProfile() {
|
|
90
|
+
return { name: 'test' };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 执行顺序
|
|
98
|
+
|
|
99
|
+
遵循洋葱模型,类级别先执行,方法级别后执行:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
@Middleware(globalMw)
|
|
103
|
+
export class FooController {
|
|
104
|
+
// 进:globalMw → methodMw → hello()
|
|
105
|
+
// 出:hello() → methodMw → globalMw
|
|
106
|
+
@Middleware(methodMw)
|
|
107
|
+
async hello() {}
|
|
108
|
+
|
|
109
|
+
// 多个 @Middleware 从下往上执行(靠近方法的先注册)
|
|
110
|
+
// 进:globalMw → mw3 → mw2 → mw1 → multiple()
|
|
111
|
+
// 出:multiple() → mw1 → mw2 → mw3 → globalMw
|
|
112
|
+
@Middleware(mw1)
|
|
113
|
+
@Middleware(mw2)
|
|
114
|
+
@Middleware(mw3)
|
|
115
|
+
async multiple() {}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**若混用函数式和 AOP 中间件,所有函数式中间件(无论类级别还是方法级别)会先于所有 AOP 中间件执行。** 即函数式和 AOP 分属两个独立的执行阶段,函数式阶段在前,AOP 阶段在后:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
@Middleware(countMw) // 函数式 - 类级别
|
|
123
|
+
@Middleware(LogAdvice) // AOP - 类级别
|
|
124
|
+
export class FooController {
|
|
125
|
+
@Middleware(timeMw) // 函数式 - 方法级别
|
|
126
|
+
@Middleware(AuthAdvice) // AOP - 方法级别
|
|
127
|
+
async hello() {}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 实际执行顺序:
|
|
131
|
+
// countMw → timeMw → LogAdvice → AuthAdvice → hello()
|
|
132
|
+
// (先所有函数式,再所有 AOP;各阶段内类级别先于方法级别)
|
|
133
|
+
```
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# 定时任务开发指南
|
|
2
|
+
|
|
3
|
+
## 注意事项
|
|
4
|
+
|
|
5
|
+
- **不要将代码放在 `app/schedule` 目录下**,egg 默认会扫描该路径注册定时任务,会和装饰器方式冲突
|
|
6
|
+
- 定时任务类必须包含一个 `subscribe` 方法,框架调度时会调用该方法
|
|
7
|
+
- import 路径是 `egg/schedule`,不是 `egg`
|
|
8
|
+
|
|
9
|
+
## Step 1: 创建定时任务
|
|
10
|
+
|
|
11
|
+
使用 `@Schedule` 装饰器标识一个类为定时任务,支持 interval 和 cron 两种调度模式。
|
|
12
|
+
|
|
13
|
+
### interval 模式
|
|
14
|
+
|
|
15
|
+
按固定间隔执行。`interval` 支持毫秒数或 [ms](https://github.com/vercel/ms) 格式字符串(如 `'5s'`、`'1m'`)。
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// app/{moduleName}/schedule/Demo.ts
|
|
19
|
+
import { Inject, Logger } from 'egg';
|
|
20
|
+
import { IntervalParams, Schedule, ScheduleType } from 'egg/schedule';
|
|
21
|
+
|
|
22
|
+
@Schedule<IntervalParams>({
|
|
23
|
+
type: ScheduleType.WORKER,
|
|
24
|
+
scheduleData: {
|
|
25
|
+
interval: '5s',
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
export class DemoScheduler {
|
|
29
|
+
@Inject()
|
|
30
|
+
private logger: Logger;
|
|
31
|
+
|
|
32
|
+
async subscribe() {
|
|
33
|
+
this.logger.info('schedule called');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### cron 模式
|
|
39
|
+
|
|
40
|
+
按 cron 表达式执行,格式参考 [cron-parser](https://github.com/harrisiirak/cron-parser):
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
* * * * * *
|
|
44
|
+
┬ ┬ ┬ ┬ ┬ ┬
|
|
45
|
+
│ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun)
|
|
46
|
+
│ │ │ │ └───── month (1 - 12)
|
|
47
|
+
│ │ │ └────────── day of month (1 - 31)
|
|
48
|
+
│ │ └─────────────── hour (0 - 23)
|
|
49
|
+
│ └──────────────────── minute (0 - 59)
|
|
50
|
+
└───────────────────────── second (0 - 59, optional)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// app/{moduleName}/schedule/CronDemo.ts
|
|
55
|
+
import { Inject, Logger } from 'egg';
|
|
56
|
+
import { CronParams, Schedule, ScheduleType } from 'egg/schedule';
|
|
57
|
+
|
|
58
|
+
@Schedule<CronParams>({
|
|
59
|
+
type: ScheduleType.WORKER,
|
|
60
|
+
scheduleData: {
|
|
61
|
+
cron: '0 0 3 * * *', // 每日 3 点执行
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
export class CronScheduler {
|
|
65
|
+
@Inject()
|
|
66
|
+
private logger: Logger;
|
|
67
|
+
|
|
68
|
+
async subscribe() {
|
|
69
|
+
this.logger.info('schedule called');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Step 2: 选择工作模式
|
|
75
|
+
|
|
76
|
+
| 模式 | 说明 | 使用场景 |
|
|
77
|
+
| --------------------- | ---------------------------------------- | ------------------------------------ |
|
|
78
|
+
| `ScheduleType.WORKER` | 每台机器只有一个 worker 执行(随机选择) | 大多数场景,如数据同步、缓存刷新 |
|
|
79
|
+
| `ScheduleType.ALL` | 每台机器的所有 worker 都执行 | 需要每个 worker 都更新本地状态的场景 |
|
|
80
|
+
|
|
81
|
+
## Step 3: 配置运行参数
|
|
82
|
+
|
|
83
|
+
`@Schedule` 装饰器支持第二个参数,控制定时任务的运行行为:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
@Schedule<IntervalParams>(
|
|
87
|
+
{
|
|
88
|
+
type: ScheduleType.WORKER,
|
|
89
|
+
scheduleData: {
|
|
90
|
+
interval: '1m',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
immediate: true, // 应用启动后立即执行一次
|
|
95
|
+
// disable: true, // 禁用该定时任务
|
|
96
|
+
env: ['devserver', 'test'], // 仅在指定环境下启动
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
export class MyScheduler {
|
|
100
|
+
async subscribe() {
|
|
101
|
+
// ...
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
| 参数 | 类型 | 说明 |
|
|
107
|
+
| ----------- | -------- | ------------------------------- |
|
|
108
|
+
| `immediate` | boolean | 应用启动并 ready 后立即执行一次 |
|
|
109
|
+
| `disable` | boolean | 设为 true 时不启动该定时任务 |
|
|
110
|
+
| `env` | string[] | 仅在指定环境下启动 |
|