@lark-apaas/coding-steering 0.1.6-alpha.1 → 0.1.6-alpha.11
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/README.md +11 -2
- package/package.json +1 -1
- package/steering/design-stack/skills/.gitkeep +0 -0
- package/steering/nestjs-react-fullstack/skills/authn-guide/SKILL.md +122 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +174 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +621 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/management-page-spec.md +505 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/runtime-role-controller-spec.md +203 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-examples.md +90 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-types.md +216 -0
- package/steering/nestjs-react-fullstack/skills/client-add-aily-web-chat/SKILL.md +139 -0
- package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +405 -0
- package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +628 -0
- package/steering/nestjs-react-fullstack/skills/devops-guide/SKILL.md +119 -0
- package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +270 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +214 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +163 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +309 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +190 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +160 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +256 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +103 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +198 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +128 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +207 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +164 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +90 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +164 -0
- package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +267 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +582 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +357 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +513 -0
- package/steering/nestjs-react-fullstack/skills/react-hook-best-practices/SKILL.md +118 -0
- package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +177 -0
- package/steering/nestjs-react-fullstack/skills/trigger-guide/SKILL.md +452 -0
- package/steering/nestjs-react-fullstack/skills/user-identity/SKILL.md +300 -0
- package/steering/nestjs-react-fullstack/skills/user-management-best-practices/SKILL.md +142 -0
- package/steering/nestjs-react-fullstack/skills_local/code-fix/SKILL.md +253 -0
- package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +585 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: server-builtins-file-storage-service
|
|
3
|
+
description: "Use when server-side code needs to upload or download files programmatically, such as file format conversion, automated report generation, or background tasks that produce files. NOT for frontend file operations. 触发词:服务端文件上传, 服务端文件下载, FileService, nestjs file, 后端文件处理, 文件格式转换, 生成文件上传"
|
|
4
|
+
steering: true
|
|
5
|
+
steering-topic: server_builtins_file_storage_service
|
|
6
|
+
match-template-name: nestjs-react-fullstack
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# NestJS File Service SDK
|
|
10
|
+
|
|
11
|
+
在 NestJS 服务端代码中使用 `FileService` 进行文件上传、下载、删除和管理。
|
|
12
|
+
|
|
13
|
+
> **使用注意**: 仅用于服务端文件处理场景,非必要文件上传下载场景请使用前端 `client-builtins-file-storage-service` skill。
|
|
14
|
+
> CLI 调试场景请参考 `file-storage` skill。
|
|
15
|
+
|
|
16
|
+
## 使用场景
|
|
17
|
+
|
|
18
|
+
| 场景 | 说明 |
|
|
19
|
+
|------|------|
|
|
20
|
+
| 服务端文件处理 | 下载文件到本地处理后重新上传,如图片格式转换、docx 格式转换等 |
|
|
21
|
+
| 自动化任务生成文件 | 定时任务或事件触发后,根据数据库/插件内容生成文件并上传,如定时生成 PDF 报告、图片海报等 |
|
|
22
|
+
|
|
23
|
+
## Quick Reference
|
|
24
|
+
|
|
25
|
+
| 方法 | 说明 | 返回值 |
|
|
26
|
+
|------|------|--------|
|
|
27
|
+
| `upload(file, options?)` | 上传文件 | `FileMeta` |
|
|
28
|
+
| `download(path)` | 下载文件(≤50MB) | `FileDownloadBuilder` |
|
|
29
|
+
| `remove(filePaths)` | 删除文件 | `RemoveResponse` |
|
|
30
|
+
| `getFileMetadata(filePath)` | 获取文件元信息 | `FileMeta` |
|
|
31
|
+
|
|
32
|
+
## 注入 FileService
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { FileService } from '@lark-apaas/fullstack-nestjs-core';
|
|
36
|
+
|
|
37
|
+
@Injectable()
|
|
38
|
+
export class MyService {
|
|
39
|
+
constructor(private readonly fileService: FileService) {}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 公共类型
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
interface FileMeta {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
filePath: string;
|
|
50
|
+
metadata: { contentLength: string; mimeType: string };
|
|
51
|
+
downloadURL: string;
|
|
52
|
+
createdAt: string;
|
|
53
|
+
updatedAt: string;
|
|
54
|
+
bucketID: string;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 核心操作
|
|
59
|
+
|
|
60
|
+
### 1. 上传文件
|
|
61
|
+
|
|
62
|
+
**入参:**
|
|
63
|
+
|
|
64
|
+
| 参数 | 类型 | 必需 | 说明 |
|
|
65
|
+
|------|------|------|------|
|
|
66
|
+
| `file` | `FileBody` | 是 | 文件内容(支持 `Buffer`、`ReadableStream`、`ArrayBuffer`、`Blob`、`string` 等) |
|
|
67
|
+
| `options` | `UploadOptions` | 否 | 上传选项 |
|
|
68
|
+
|
|
69
|
+
**UploadOptions:**
|
|
70
|
+
|
|
71
|
+
| 字段 | 类型 | 必需 | 说明 |
|
|
72
|
+
|------|------|------|------|
|
|
73
|
+
| `fileName` | `string` | 否 | 文件名称 |
|
|
74
|
+
| `contentType` | `string` | 否 | MIME 类型 |
|
|
75
|
+
| `cacheControl` | `string \| number` | 否 | 缓存控制 |
|
|
76
|
+
| `upsert` | `boolean` | 否 | 是否覆盖已有文件 |
|
|
77
|
+
|
|
78
|
+
**返回值:** `FileMeta`
|
|
79
|
+
|
|
80
|
+
**示例:**
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// 基本上传
|
|
84
|
+
const result = await this.fileService.upload(buffer);
|
|
85
|
+
|
|
86
|
+
// 带选项上传
|
|
87
|
+
const result = await this.fileService.upload(fileBody, {
|
|
88
|
+
fileName: 'report.pdf',
|
|
89
|
+
contentType: 'application/pdf',
|
|
90
|
+
upsert: false,
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 2. 下载文件
|
|
95
|
+
|
|
96
|
+
**入参:**
|
|
97
|
+
|
|
98
|
+
| 参数 | 类型 | 必需 | 说明 |
|
|
99
|
+
|------|------|------|------|
|
|
100
|
+
| `path` | `string` | 是 | downloadURL 或文件存储路径 |
|
|
101
|
+
|
|
102
|
+
**返回值 `DownloadResult`:**
|
|
103
|
+
|
|
104
|
+
| 字段 | 类型 | 说明 |
|
|
105
|
+
|------|------|------|
|
|
106
|
+
| `content` | `Blob`(默认)/ `ReadableStream`(流式) | 文件内容 |
|
|
107
|
+
| `metadata` | `FileMeta` | 文件元信息 |
|
|
108
|
+
|
|
109
|
+
> **推荐始终使用 `.asStream()`**,避免大文件导致内存溢出。直接 `await download()` 有 50MB 限制。
|
|
110
|
+
|
|
111
|
+
**示例:**
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// 推荐:使用 downloadURL + 流式下载
|
|
115
|
+
const downloadURL = fileMeta.downloadURL; // 从上传返回值或数据库获取
|
|
116
|
+
const { content, metadata } = await this.fileService
|
|
117
|
+
.download(downloadURL)
|
|
118
|
+
.asStream();
|
|
119
|
+
|
|
120
|
+
// 也可以使用文件存储路径
|
|
121
|
+
const { content, metadata } = await this.fileService
|
|
122
|
+
.download('file.pdf')
|
|
123
|
+
.asStream();
|
|
124
|
+
|
|
125
|
+
// 不推荐:Blob 下载(限制 50MB,会将整个文件加载到内存)
|
|
126
|
+
const { content, metadata } = await this.fileService.download(downloadURL);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 3. 删除文件
|
|
130
|
+
|
|
131
|
+
**入参:**
|
|
132
|
+
|
|
133
|
+
| 参数 | 类型 | 必需 | 说明 |
|
|
134
|
+
|------|------|------|------|
|
|
135
|
+
| `filePaths` | `string[]` | 是 | downloadURL 或文件存储路径数组 |
|
|
136
|
+
|
|
137
|
+
**返回值:** `FileMeta[]` - 删除成功的文件元信息数组
|
|
138
|
+
|
|
139
|
+
**示例:**
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// 推荐:使用 downloadURL
|
|
143
|
+
const result = await this.fileService.remove([fileMeta.downloadURL]);
|
|
144
|
+
|
|
145
|
+
// 也可以使用文件存储路径
|
|
146
|
+
const result = await this.fileService.remove(['file1.pdf', 'file2.png']);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 4. 获取文件元信息
|
|
150
|
+
|
|
151
|
+
**入参:**
|
|
152
|
+
|
|
153
|
+
| 参数 | 类型 | 必需 | 说明 |
|
|
154
|
+
|------|------|------|------|
|
|
155
|
+
| `filePath` | `string` | 是 | downloadURL 或文件存储路径 |
|
|
156
|
+
|
|
157
|
+
**返回值:** `FileMeta | null` - 文件元信息或 `null` 表示文件不存在或删除失败
|
|
158
|
+
|
|
159
|
+
**示例:**
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// 推荐:使用 downloadURL
|
|
163
|
+
const meta = await this.fileService.getFileMetadata(fileMeta.downloadURL);
|
|
164
|
+
|
|
165
|
+
// 也可以使用文件存储路径
|
|
166
|
+
const meta = await this.fileService.getFileMetadata('file.pdf');
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Common Mistakes
|
|
170
|
+
|
|
171
|
+
| 错误 | 正确做法 |
|
|
172
|
+
|------|----------|
|
|
173
|
+
| 直接 `await download()` 导致内存溢出 | 始终使用 `.asStream()` 流式下载 |
|
|
174
|
+
| 忘记设置 `contentType` 导致浏览器无法预览 | 上传时明确指定 `contentType` |
|
|
175
|
+
| 直接 `new FileService()` 手动实例化 | 通过 NestJS DI 注入 `FileService` |
|
|
176
|
+
| 在异步回调中调用 `download()` 导致上下文丢失 | 在请求处理函数中立即调用,SDK 内部已处理上下文捕获 |
|
|
177
|
+
| 删除时传单个字符串 | `remove()` 参数为 `string[]` 数组 |
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trigger-guide
|
|
3
|
+
description: 自动化任务触发器配置与代码开发指南,支持 cron 定时触发器、record_change 数据变更触发器和 webhook 触发器,包含 @Automation/@BindTrigger 装饰器用法和 Crontab 表达式规范。Use when 需要:(1) 创建或配置自动化任务/定时任务,(2) 编写 automation 代码绑定触发器,或其他自动化任务相关开发
|
|
4
|
+
steering: true
|
|
5
|
+
steering-topic: trigger_guide
|
|
6
|
+
match-template-name: nestjs-react-fullstack
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 自动化任务配置与代码编写指引
|
|
10
|
+
|
|
11
|
+
### 自动化任务配置
|
|
12
|
+
|
|
13
|
+
1. 新建自动化任务触发器时无需 enable(激活),将任务创建好然后开发完代码即可。触发器随后交由用户主动操作、要求开始。
|
|
14
|
+
|
|
15
|
+
### 目录结构
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
server
|
|
19
|
+
└── modules
|
|
20
|
+
└── xxx
|
|
21
|
+
├── xxx.automation.ts
|
|
22
|
+
├── xxx.module.ts // 必须在 module 中注册自动化任务类,并且在 app.module.ts 中引用并注册该 module,否则代码将不会生效。
|
|
23
|
+
└── 其他文件(如有的话)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
文件命名规则:{模块名}.automation.ts
|
|
27
|
+
|
|
28
|
+
注意:
|
|
29
|
+
|
|
30
|
+
1. 每个模块只应该有一个存放自动化任务逻辑的文件,业务逻辑需要聚合到该文件中。
|
|
31
|
+
2. 如果该模块只有对应的自动化任务,无需编写 Controller
|
|
32
|
+
|
|
33
|
+
### 触发器类型与入参
|
|
34
|
+
|
|
35
|
+
触发器类型(`triggerType`)有三种:
|
|
36
|
+
|
|
37
|
+
- `record_change`:记录变更触发器,**有入参**
|
|
38
|
+
- `cron`:定时触发器,**无入参**
|
|
39
|
+
- `webhook`:Webhook 触发器,**有入参**
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// 有入参触发器的入参类型(triggerType = 'record_change' 时)
|
|
43
|
+
interface TaskHandlerArgs {
|
|
44
|
+
attributes: {
|
|
45
|
+
trigger: string;
|
|
46
|
+
triggerID?: string;
|
|
47
|
+
triggerType: 'record_change' | 'cron' | 'webhook';
|
|
48
|
+
instanceID: string;
|
|
49
|
+
startAt?: number;
|
|
50
|
+
};
|
|
51
|
+
content: {
|
|
52
|
+
input: string; // JSON 字符串,根据 triggerType 解析为对应类型
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// record_change:input 解析后的数据结构
|
|
57
|
+
interface DataChangeEventInput {
|
|
58
|
+
id: string;
|
|
59
|
+
tenant_id: number;
|
|
60
|
+
workspace: string;
|
|
61
|
+
branch: string;
|
|
62
|
+
app: string;
|
|
63
|
+
table: string;
|
|
64
|
+
type: 'INSERT' | 'UPDATE' | 'DELETE';
|
|
65
|
+
timestamp: number;
|
|
66
|
+
before?: Record<string, unknown>; // DELETE 时有值,其他情况可能为空
|
|
67
|
+
after?: Record<string, unknown>; // INSERT/UPDATE 时有值,其他情况可能为空
|
|
68
|
+
msg_id: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// webhook:input 解析后的数据结构
|
|
72
|
+
interface WebhookEvent {
|
|
73
|
+
method: 'GET' | 'POST';
|
|
74
|
+
url: string; // 完整 URL(含查询参数)
|
|
75
|
+
host: string; // 不含查询参数的 URL
|
|
76
|
+
path: string; // 路径部分
|
|
77
|
+
query: Record<string, string[]>; // URL 查询参数,值为字符串数组
|
|
78
|
+
headers: Record<string, string[]>; // 请求头,值为字符串数组
|
|
79
|
+
body: string; // 请求体,JSON 字符串
|
|
80
|
+
meta: {
|
|
81
|
+
timestamp: number; // 请求时间戳(秒)
|
|
82
|
+
traceID: string; // 追踪 ID
|
|
83
|
+
env: 'development' | 'online'; // 环境:开发/线上
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 指定值限制
|
|
89
|
+
1. Webhook 触发器不可以设置指定值,并且告知用户。
|
|
90
|
+
|
|
91
|
+
### 代码示例
|
|
92
|
+
|
|
93
|
+
你需要根据 `automation_trigger_manager` 工具返回的自动化任务名字,编写并绑定到对应的方法上。具体代码示例如下:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// 文件名:demo.automation.ts
|
|
97
|
+
import { Logger } from '@nestjs/common';
|
|
98
|
+
// 必须导入
|
|
99
|
+
import { Automation, BindTrigger } from '@lark-apaas/fullstack-nestjs-core';
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 示例自动化任务服务
|
|
103
|
+
* 使用 @Automation 装饰器标记 class,@BindTrigger 绑定具体 function
|
|
104
|
+
*/
|
|
105
|
+
@Automation()
|
|
106
|
+
export class DemoAutomationTasksService {
|
|
107
|
+
// 务必使用 logger 打印日志
|
|
108
|
+
private readonly logger = new Logger(DemoAutomationTasksService.name);
|
|
109
|
+
|
|
110
|
+
@BindTrigger('triggerName1')
|
|
111
|
+
// 任务对应具体的实现
|
|
112
|
+
async helloWorld() {
|
|
113
|
+
this.logger.log('执行 Hello World 任务');
|
|
114
|
+
// do logic
|
|
115
|
+
// return logic result or throw Error
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@BindTrigger('triggerName2')
|
|
119
|
+
// 任务对应具体的实现
|
|
120
|
+
async sendNotification() {
|
|
121
|
+
this.logger.log('开始发送通知');
|
|
122
|
+
// do logic
|
|
123
|
+
this.logger.log('通知发送完成');
|
|
124
|
+
// return logic result or throw Error
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@BindTrigger('recordChangeTrigger')
|
|
128
|
+
// 记录变更任务(triggerType = 'record_change')
|
|
129
|
+
async handleDataChange(event: TaskHandlerArgs) {
|
|
130
|
+
// 1. 校验并解析 input
|
|
131
|
+
const input = event.content.input;
|
|
132
|
+
if (typeof input !== 'string') {
|
|
133
|
+
this.logger.error('input 类型错误');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let eventData: DataChangeEventInput;
|
|
138
|
+
try {
|
|
139
|
+
eventData = JSON.parse(input);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
this.logger.error('JSON 解析失败', error);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 2. 根据操作类型获取数据:INSERT/UPDATE 用 after,DELETE 用 before
|
|
146
|
+
const record = eventData.after || eventData.before;
|
|
147
|
+
if (!record) {
|
|
148
|
+
this.logger.error('记录数据为空');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
this.logger.log(`处理 ${eventData.type} 事件,记录ID: ${record.id}`);
|
|
152
|
+
// do logic
|
|
153
|
+
// return logic result or throw Error
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@BindTrigger('webhookTrigger')
|
|
157
|
+
// Webhook 任务(triggerType = 'webhook')
|
|
158
|
+
async handleWebhook(event: TaskHandlerArgs) {
|
|
159
|
+
// 1. 校验并解析 input
|
|
160
|
+
const input = event.content.input;
|
|
161
|
+
if (typeof input !== 'string') {
|
|
162
|
+
this.logger.error('input 类型错误');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let webhookEvent: WebhookEvent;
|
|
167
|
+
try {
|
|
168
|
+
webhookEvent = JSON.parse(input);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
this.logger.error('JSON 解析失败', error);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 2. 获取请求信息
|
|
175
|
+
const { method, path, query, headers, body } = webhookEvent;
|
|
176
|
+
this.logger.log(`处理 Webhook 请求:${method} ${path}`);
|
|
177
|
+
|
|
178
|
+
// 3. 按需解析 body(body 本身也是 JSON 字符串)
|
|
179
|
+
// const bodyData = JSON.parse(body);
|
|
180
|
+
|
|
181
|
+
// do logic
|
|
182
|
+
// return logic result or throw Error
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 任务代码实现约束
|
|
188
|
+
|
|
189
|
+
1. 执行自动化任务时无法获取用户信息。依赖用户信息的场景,实现路径如下:
|
|
190
|
+
- 需要查询数据库中的特定数据,给用户发消息:数据库中需要存储用户 id,使用从数据库中查询到的用户 id 进行后续操作
|
|
191
|
+
- 需要调用飞书能力给用户发消息:飞书能力不应该接受用户信息作为参数,而是应该在飞书能力配置里要求用户自己预先指定
|
|
192
|
+
|
|
193
|
+
2. 入参解析规范(仅 record_change 和 webhook 触发器):
|
|
194
|
+
- 有入参的触发器方法签名为 `async methodName(event: TaskHandlerArgs)`,`cron` 触发器无入参
|
|
195
|
+
- `content.input` 是 JSON 字符串,先用 `typeof input === 'string'` 检查类型,再用 `JSON.parse()` 解析,需添加 try-catch 错误处理
|
|
196
|
+
- `record_change`:根据操作类型获取数据:INSERT/UPDATE 使用 `after` 字段,DELETE 使用 `before` 字段
|
|
197
|
+
- `webhook`:从 `method`、`path`、`query`、`headers`、`body` 中按需取用;`body` 本身也是 JSON 字符串,需要时再次 `JSON.parse()` 解析;`query` 和 `headers` 的值均为 `string[]`
|
|
198
|
+
|
|
199
|
+
### 技术实现路径参考
|
|
200
|
+
|
|
201
|
+
以下是一些常见需求的推荐实现路径,帮助你在平台能力限制下找到合理的技术方案。
|
|
202
|
+
|
|
203
|
+
#### 场景一:用户需要管理页面控制定时任务的启停
|
|
204
|
+
|
|
205
|
+
平台侧不支持通过 API 动态启停触发器。推荐方案:**平台定时触发器始终保持开启,在任务执行时查询数据库中的开关状态,决定是否真正执行业务逻辑。**
|
|
206
|
+
|
|
207
|
+
实现步骤:
|
|
208
|
+
|
|
209
|
+
1. 在数据库中建一张配置表(或复用已有配置表),存储任务开关状态
|
|
210
|
+
2. 前端管理页面提供开关操作,修改数据库中的状态
|
|
211
|
+
3. 定时任务触发时,先查询开关状态,关闭则直接跳过
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
@Automation()
|
|
215
|
+
export class ReportAutomationService {
|
|
216
|
+
private readonly logger = new Logger(ReportAutomationService.name);
|
|
217
|
+
|
|
218
|
+
constructor(private readonly configService: ConfigService) {}
|
|
219
|
+
|
|
220
|
+
@BindTrigger('dailyReportTrigger')
|
|
221
|
+
async generateDailyReport() {
|
|
222
|
+
// 1. 先查询任务开关状态
|
|
223
|
+
const config = await this.configService.getTaskConfig('dailyReport');
|
|
224
|
+
if (!config?.enabled) {
|
|
225
|
+
this.logger.log('每日报告任务已被管理员关闭,跳过执行');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 2. 开关开启,执行实际业务逻辑
|
|
230
|
+
this.logger.log('开始生成每日报告');
|
|
231
|
+
// do logic
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### 场景二:定时任务需要将结果通知给特定用户
|
|
237
|
+
|
|
238
|
+
自动化任务执行时无法获取当前用户上下文。推荐方案:**在数据库中预存需要通知的用户 ID,任务执行时从数据库查询目标用户,再调用飞书插件发送通知。**
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
@Automation()
|
|
242
|
+
export class NotifyAutomationService {
|
|
243
|
+
private readonly logger = new Logger(NotifyAutomationService.name);
|
|
244
|
+
|
|
245
|
+
constructor(
|
|
246
|
+
private readonly userConfigService: UserConfigService,
|
|
247
|
+
) {}
|
|
248
|
+
|
|
249
|
+
@BindTrigger('weeklyDigestTrigger')
|
|
250
|
+
async sendWeeklyDigest() {
|
|
251
|
+
// 1. 从数据库查询订阅了周报的用户列表
|
|
252
|
+
const subscribers = await this.userConfigService.getSubscribers('weeklyDigest');
|
|
253
|
+
if (!subscribers.length) {
|
|
254
|
+
this.logger.log('无订阅用户,跳过发送');
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 2. 生成周报内容
|
|
259
|
+
const reportContent = await this.buildWeeklyReport();
|
|
260
|
+
|
|
261
|
+
// 3. 逐个发送通知
|
|
262
|
+
for (const user of subscribers) {
|
|
263
|
+
// 调用插件发送飞书消息
|
|
264
|
+
this.logger.log(`已发送周报给用户: ${user.userId}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
#### 场景三:记录变更触发器需要做防抖/去重
|
|
271
|
+
|
|
272
|
+
高频数据变更场景下,同一条记录可能短时间内触发多次。推荐方案:**利用数据库记录最近一次处理时间戳,对比 event 时间戳进行去重。**
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
@BindTrigger('orderStatusChange')
|
|
276
|
+
async handleOrderChange(event: TaskHandlerArgs) {
|
|
277
|
+
const eventData: DataChangeEventInput = JSON.parse(event.content.input);
|
|
278
|
+
const record = eventData.after;
|
|
279
|
+
if (!record) return;
|
|
280
|
+
|
|
281
|
+
const orderId = record.id as string;
|
|
282
|
+
|
|
283
|
+
// 查询上次处理时间,跳过短时间内的重复事件
|
|
284
|
+
const lastProcessed = await this.orderService.getLastProcessedTime(orderId);
|
|
285
|
+
if (lastProcessed && eventData.timestamp - lastProcessed < 5000) {
|
|
286
|
+
this.logger.log(`订单 ${orderId} 短时间内重复触发,跳过`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 记录本次处理时间并执行业务逻辑
|
|
291
|
+
await this.orderService.updateLastProcessedTime(orderId, eventData.timestamp);
|
|
292
|
+
this.logger.log(`处理订单状态变更: ${orderId}`);
|
|
293
|
+
// do logic
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
#### 场景四:用户需要自定义定时任务的触发时间
|
|
298
|
+
|
|
299
|
+
平台侧的 cron 表达式在触发器创建后无法由用户动态修改。推荐方案:**平台设置一个固定的高频定时器(如每 30 分钟执行一次),在任务执行时从数据库读取用户配置的触发时间,判断当前是否命中再决定是否执行。**
|
|
300
|
+
|
|
301
|
+
实现步骤:
|
|
302
|
+
|
|
303
|
+
1. 平台侧创建一个每 30 分钟执行的 cron 触发器(最小粒度)
|
|
304
|
+
2. 数据库中存储用户配置的期望执行时间(如 `"09:00"`、`"每周一 14:00"` 等)
|
|
305
|
+
3. 前端管理页面提供时间配置界面,用户可随时修改
|
|
306
|
+
4. 每次触发时,读取配置并判断当前时间是否匹配,不匹配则跳过
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
@Automation()
|
|
310
|
+
export class ScheduleAutomationService {
|
|
311
|
+
private readonly logger = new Logger(ScheduleAutomationService.name);
|
|
312
|
+
|
|
313
|
+
constructor(private readonly scheduleConfigService: ScheduleConfigService) {}
|
|
314
|
+
|
|
315
|
+
@BindTrigger('fixedIntervalTrigger') // 平台侧固定每 30 分钟触发
|
|
316
|
+
async checkAndExecuteTasks() {
|
|
317
|
+
// 1. 查询所有用户配置的定时任务
|
|
318
|
+
const tasks = await this.scheduleConfigService.getAllActiveTasks();
|
|
319
|
+
|
|
320
|
+
const now = new Date();
|
|
321
|
+
for (const task of tasks) {
|
|
322
|
+
// 2. 判断当前时间是否命中用户配置的执行时间
|
|
323
|
+
if (!this.isTimeMatched(now, task.scheduledTime)) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// 3. 命中则执行对应业务逻辑
|
|
328
|
+
this.logger.log(`执行任务: ${task.name}, 配置时间: ${task.scheduledTime}`);
|
|
329
|
+
await this.executeTask(task);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private isTimeMatched(now: Date, scheduledTime: string): boolean {
|
|
334
|
+
// 将当前时间取到半小时精度,与用户配置的时间比较
|
|
335
|
+
// 例如 scheduledTime = "09:00",当前 08:46~09:15 之间的某次触发即命中
|
|
336
|
+
const [hour, minute] = scheduledTime.split(':').map(Number);
|
|
337
|
+
return now.getHours() === hour && now.getMinutes() === minute;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
> 注意:由于平台最小调度间隔为 30 分钟,用户可配置的时间精度也应限制为 30 分钟的整数倍(如 `09:00`、`09:30`),前端做好校验提示。
|
|
343
|
+
|
|
344
|
+
## Crontab 表达式规范
|
|
345
|
+
|
|
346
|
+
### 基本结构
|
|
347
|
+
|
|
348
|
+
Crontab 表达式由 5 个字段组成:`<minute> <hour> <day> <month> <week>`
|
|
349
|
+
|
|
350
|
+
### 字段说明
|
|
351
|
+
|
|
352
|
+
1. **minute(分钟)**:0-59 的整数
|
|
353
|
+
2. **hour(小时)**:0-23 的整数
|
|
354
|
+
3. **day(日期)**:1-31 的整数,或大写字母 `L` 表示月份的最后一天
|
|
355
|
+
4. **month(月份)**:1-12 的整数
|
|
356
|
+
5. **week(星期)**:0-6 的整数,其中 0 表示星期天
|
|
357
|
+
|
|
358
|
+
### 特殊字符
|
|
359
|
+
|
|
360
|
+
- **星号 `*`**:表示所有可能的值(每)
|
|
361
|
+
- 例:`* * * * *` 表示每分钟
|
|
362
|
+
- **逗号 `,`**:表示列表范围
|
|
363
|
+
- 例:`1,2,3 * * * *` 表示每小时的第 1、2、3 分钟
|
|
364
|
+
- **中杠 `-`**:表示数值范围
|
|
365
|
+
- 例:`1-10 * * * *` 表示每小时的第 1 到 10 分钟
|
|
366
|
+
- **正斜线 `/`**:表示间隔频率
|
|
367
|
+
- 例:`0 10-18/2 * * *` 表示每天 10 点到 18 点,每隔 2 小时执行
|
|
368
|
+
|
|
369
|
+
## 输出要求
|
|
370
|
+
|
|
371
|
+
1. 必须以 JSON 格式输出
|
|
372
|
+
2. JSON 包含两个字段:
|
|
373
|
+
- `expression`:Crontab 表达式字符串
|
|
374
|
+
- `explanation`:中文说明,简要描述执行时间
|
|
375
|
+
3. 如果用户描述不清晰,请询问具体细节
|
|
376
|
+
|
|
377
|
+
## 示例
|
|
378
|
+
|
|
379
|
+
**用户输入**:每天早上 8 点执行
|
|
380
|
+
|
|
381
|
+
**输出**:
|
|
382
|
+
|
|
383
|
+
```json
|
|
384
|
+
{
|
|
385
|
+
"expression": "0 8 * * *",
|
|
386
|
+
"explanation": "每天早上 8:00 执行"
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**用户输入**:每周一到周五的上午 9 点和下午 6 点执行
|
|
391
|
+
|
|
392
|
+
**输出**:
|
|
393
|
+
|
|
394
|
+
```json
|
|
395
|
+
{
|
|
396
|
+
"expression": "0 9,18 * * 1-5",
|
|
397
|
+
"explanation": "每周一至周五的 9:00 和 18:00 执行"
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**用户输入**:每隔 30 分钟执行一次
|
|
402
|
+
|
|
403
|
+
**输出**:
|
|
404
|
+
|
|
405
|
+
```json
|
|
406
|
+
{
|
|
407
|
+
"expression": "*/30 * * * *",
|
|
408
|
+
"explanation": "每隔 30 分钟执行一次"
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**用户输入**:每月最后一天的晚上 11 点执行
|
|
413
|
+
|
|
414
|
+
**输出**:
|
|
415
|
+
|
|
416
|
+
```json
|
|
417
|
+
{
|
|
418
|
+
"expression": "0 23 L * *",
|
|
419
|
+
"explanation": "每月最后一天的 23:00 执行"
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
**用户输入**:每个工作日的每小时第 15 和 45 分钟执行
|
|
424
|
+
|
|
425
|
+
**输出**:
|
|
426
|
+
|
|
427
|
+
```json
|
|
428
|
+
{
|
|
429
|
+
"expression": "15,45 * * * 1-5",
|
|
430
|
+
"explanation": "每周一至周五,每小时的第 15 和 45 分钟执行"
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**用户输入**:每天上午 10 点到下午 6 点,每隔 2 小时执行
|
|
435
|
+
|
|
436
|
+
**输出**:
|
|
437
|
+
|
|
438
|
+
```json
|
|
439
|
+
{
|
|
440
|
+
"expression": "0 10-18/2 * * *",
|
|
441
|
+
"explanation": "每天 10:00、12:00、14:00、16:00、18:00 执行"
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## 注意事项
|
|
446
|
+
|
|
447
|
+
- 星期字段:0 和 7 都可以表示星期天(但本规范使用 0)
|
|
448
|
+
- 时间采用 24 小时制
|
|
449
|
+
- 月份和星期都从较小的数字开始计数
|
|
450
|
+
- 确保生成的表达式符合实际日历逻辑
|
|
451
|
+
- 由于技术限制,最小间隔为 30 分钟,如用户要求有误请直接拒绝用户并给出原因
|
|
452
|
+
- 输出必须是有效的 JSON 格式
|