@lark-apaas/coding-steering 0.1.32-beta.0 → 0.1.32-dev.4f80f68
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/package.json +6 -6
- package/steering/design-html/skills/charts/SKILL.md +4 -0
- package/steering/design-html/skills/pptx-style-extract/SKILL.md +58 -22
- package/steering/design-html/skills/pptx-style-extract/font-fallback.yaml +3 -3
- package/steering/design-html/skills/pptx-style-extract/scripts/census.py +18 -12
- package/steering/design-html/skills/pptx-style-extract/scripts/check_v2.py +153 -8
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +1768 -241
- package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +325 -22
- package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +1 -1
- package/steering/design-html/skills/pptx-style-extract/scripts/package.py +379 -156
- package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +6 -3
- package/steering/design-html/skills/pptx-style-extract/scripts/query.py +4 -9
- package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +16 -10
- package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +57 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_color_contract.py +60 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +62 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +378 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +98 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_rounded_contract.py +112 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +168 -0
- package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +27 -15
- package/steering/design-html/skills/preflight/scripts/probe.sh +0 -0
- package/steering/nestjs-react-fullstack/skills/app-init-feasibility-guide/SKILL.md +1 -0
- package/steering/nestjs-react-fullstack/skills/authn-guide/SKILL.md +6 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +5 -5
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +1 -1
- package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +37 -113
- package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +13 -2
- package/steering/nestjs-react-fullstack/skills/code-fix/SKILL.md +7 -7
- package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +149 -24
- package/steering/nestjs-react-fullstack/skills/connections-sdk/SKILL.md +202 -0
- package/steering/nestjs-react-fullstack/skills/nestjs-cache/SKILL.md +255 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +158 -543
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +15 -1
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +30 -14
- package/steering/nestjs-react-fullstack/skills/raw-sql-boundary-audit/SKILL.md +63 -0
- package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +1 -1
- package/steering/nestjs-react-fullstack/skills_common/trigger-guide/SKILL.md +284 -12
- package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +4 -0
- package/steering/vite-react/skills/plugin-guide/SKILL.md +3 -1
- package/steering/vite-react/skills/react-three-fiber/SKILL.md +4 -0
- package/steering/nestjs-react-fullstack/skills/client-add-aily-web-chat/SKILL.md +0 -139
- package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +0 -269
- package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +0 -214
- package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +0 -163
- package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +0 -311
- package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +0 -190
- package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +0 -160
- package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +0 -257
- package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +0 -104
- package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +0 -199
- package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +0 -128
- package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +0 -207
- package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +0 -165
- package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +0 -91
- package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +0 -165
- package/steering/nestjs-react-fullstack/skills_common/trigger-guide/references/trigger-lifecycle.md +0 -301
package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md
CHANGED
|
@@ -68,7 +68,9 @@
|
|
|
68
68
|
```markdown
|
|
69
69
|
[Schema 摘录卡]
|
|
70
70
|
- pluginInstanceId / actionKey / outputMode
|
|
71
|
-
- input.required
|
|
71
|
+
- input.required: [字段名: 类型, ...]
|
|
72
|
+
- output.fields: [字段名: 类型, ...](每个字段必须在代码中被消费,未消费需注释说明原因)
|
|
73
|
+
- readme.constraints
|
|
72
74
|
- 调用侧决策: Client | Server
|
|
73
75
|
```
|
|
74
76
|
|
|
@@ -339,6 +341,18 @@ this.somePluginInstanceSideEffect(input).catch(error => {
|
|
|
339
341
|
|
|
340
342
|
---
|
|
341
343
|
|
|
344
|
+
### 调用错误分类与应对
|
|
345
|
+
|
|
346
|
+
| 错误类型 | 含义 | 应对策略 |
|
|
347
|
+
|----------|------|---------|
|
|
348
|
+
| `InputValidationError` | 入参不符合 schema | 修复参数后重试,不应出现在生产环境 |
|
|
349
|
+
| `RateLimitError` | 触发限流 | 指数退避重试(1s/2s/4s),最多 3 次 |
|
|
350
|
+
| `ExecutionError` | 插件执行失败 | 记录日志 + 降级方案(如规则计算)+ 通知用户 |
|
|
351
|
+
| `OutputValidationError` | 返回值不符合 schema | 记录异常返回 + 使用默认值或降级 |
|
|
352
|
+
| 网络超时 | 请求超时 | 重试 + 超时后降级 |
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
342
356
|
### outputMode 与调用侧选择
|
|
343
357
|
|
|
344
358
|
先通过 `get_plugin_ai_json(pluginInstanceId)` 获取 `actions[].outputMode`:
|
|
@@ -146,6 +146,20 @@
|
|
|
146
146
|
❌ searchRecords 拉全量 → find 找单条 → 必须用 getRecord
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
+
## 数据架构选择(应用读取多维表格数据时)
|
|
150
|
+
|
|
151
|
+
按**查询模式**选择架构:
|
|
152
|
+
|
|
153
|
+
| 查询模式 | 架构选择 | 实现方式 |
|
|
154
|
+
|---------|---------|---------|
|
|
155
|
+
| 展示/编辑单条记录 | 纯插件 | `getRecord` / `batchUpdateRecords` |
|
|
156
|
+
| 列表分页浏览 | 纯插件 | `searchRecords` + `pageToken` 游标分页 |
|
|
157
|
+
| 统计聚合(计数/求和/平均) | **纯插件 + aggregateQuery** | 禁止 searchRecords 全量拉取后内存计算 |
|
|
158
|
+
| 统计 + 明细下钻 | 纯插件 | 聚合用 `aggregateQuery`,下钻用 `searchRecords` + filter |
|
|
159
|
+
| 排行榜 / TOP N | 纯插件 | `searchRecords` + sort + pageSize=N |
|
|
160
|
+
| 复杂排序/多表关联/全文搜索 | 插件同步 + 本地数据库 | 定时/Webhook 同步到 postgres,复杂查询走数据库 |
|
|
161
|
+
| 高频写入 + 读取 | 本地数据库为主 | 多维表格仅作展示/备份 |
|
|
162
|
+
|
|
149
163
|
## 数据分析类Action补充说明
|
|
150
164
|
|
|
151
165
|
`aggregateQuery` 用于分组聚合统计,支持 dimensions(分组维度)和 measures(聚合计算)。
|
|
@@ -301,23 +315,25 @@ const chartData =
|
|
|
301
315
|
|
|
302
316
|
## 推荐 UI 组件
|
|
303
317
|
|
|
304
|
-
| BizType | 展示
|
|
305
|
-
| ---------------------------- |
|
|
306
|
-
| `User` | `<UserDisplay
|
|
307
|
-
| `Url` | `<Hyperlink value={val} readOnly />`
|
|
308
|
-
| `SingleSelect`/`MultiSelect` | 文本/标签
|
|
309
|
-
| `DateTime` | `new Date(ts).toLocaleDateString()`
|
|
310
|
-
| `Checkbox` | `value ? '是' : '否'`
|
|
311
|
-
| `Currency` | `¥${value.toFixed(2)}`
|
|
312
|
-
| `Progress` | `${value}%` 或进度条
|
|
313
|
-
| `Rating` | `'★'.repeat(value) + '☆'.repeat(5-value)`
|
|
318
|
+
| BizType | 展示 | 录入 |
|
|
319
|
+
| ---------------------------- | -------------------------------------------------------------------- | ------------------------------------------- |
|
|
320
|
+
| `User` | `<UserDisplay value={ids.map(id => id.toString())} />` | `<UserSelect multiple />` |
|
|
321
|
+
| `Url` | `<Hyperlink value={val} readOnly />` | `<Hyperlink readOnly={false} />` |
|
|
322
|
+
| `SingleSelect`/`MultiSelect` | 文本/标签 | `<Select>` 选项来自 enumValues |
|
|
323
|
+
| `DateTime` | `new Date(ts).toLocaleDateString()` | `<Input type="date" />` |
|
|
324
|
+
| `Checkbox` | `value ? '是' : '否'` | `<Checkbox />` |
|
|
325
|
+
| `Currency` | `¥${value.toFixed(2)}` | `<Input type="number" />` |
|
|
326
|
+
| `Progress` | `${value}%` 或进度条 | `<Input type="number" min={0} max={100} />` |
|
|
327
|
+
| `Rating` | `'★'.repeat(value) + '☆'.repeat(5-value)` | `<Input type="number" min={0} max={5} />` |
|
|
328
|
+
|
|
329
|
+
BizType 为 `User` 的字段展示必须使用 business-ui `UserDisplay`;不要直接渲染、拼接或 `join` 用户 ID 文本。
|
|
314
330
|
|
|
315
331
|
### 组件代码示例
|
|
316
332
|
|
|
317
333
|
```tsx
|
|
318
|
-
//
|
|
334
|
+
// 人员展示(business-ui UserDisplay 使用 value;不要传 users)
|
|
319
335
|
<UserDisplay
|
|
320
|
-
|
|
336
|
+
value={record['人员字段']?.map(userId => userId.toString()) || []}
|
|
321
337
|
size="small"
|
|
322
338
|
showLabel={true}
|
|
323
339
|
/>
|
|
@@ -365,13 +381,13 @@ const chartData =
|
|
|
365
381
|
const columns = [
|
|
366
382
|
// 文本字段 - 注意读取格式是 { text: string }
|
|
367
383
|
{ title: '文本', dataIndex: ['record', '文本字段', 'text'], key: 'text' },
|
|
368
|
-
// 人员字段 - 使用 UserDisplay
|
|
384
|
+
// 人员字段 - 使用 business-ui UserDisplay 组件,传 value(用户 ID 字符串数组),不要传 users
|
|
369
385
|
{
|
|
370
386
|
title: '负责人',
|
|
371
387
|
key: 'user',
|
|
372
388
|
render: (_, record) => (
|
|
373
389
|
<UserDisplay
|
|
374
|
-
|
|
390
|
+
value={record.record['人员字段']?.map((id) => id.toString()) || []}
|
|
375
391
|
size="small"
|
|
376
392
|
/>
|
|
377
393
|
),
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: raw-sql-boundary-audit
|
|
3
|
+
description: >-
|
|
4
|
+
Use when editing or reviewing NestJS backend database code that contains Drizzle raw `sql` tagged templates,
|
|
5
|
+
db.execute(sql`...`), report/statistics/aggregation endpoints, filter (...), CASE WHEN, window functions,
|
|
6
|
+
or custom type / Date range parameters.
|
|
7
|
+
steering: true
|
|
8
|
+
steering-inclusion: always
|
|
9
|
+
steering-topic: raw_sql_boundary_audit
|
|
10
|
+
match-template-name: nestjs-react-fullstack
|
|
11
|
+
unavailable-agents:
|
|
12
|
+
- AppInit
|
|
13
|
+
- SpecDoc
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Drizzle raw SQL 参数边界审计
|
|
17
|
+
|
|
18
|
+
本 skill 只管一件事:改 NestJS 后端数据库代码时,raw `sql` 模板里的 `${...}` 参数必须是数据库 driver 可直接接收的标量。Drizzle helper 绑定 schema 列会按列 encoder 处理;raw `sql` 模板参数不会自动调用列的 `toDriver`。
|
|
19
|
+
|
|
20
|
+
## 何时必须执行审计
|
|
21
|
+
|
|
22
|
+
满足任一条件就执行:
|
|
23
|
+
|
|
24
|
+
- 正在修改 `server/**/*.service.ts` 或 repository 中的数据库查询;
|
|
25
|
+
- 文件里有 `sql\``、`db.execute(sql`、`filter (...)`、`CASE WHEN`、窗口函数或复杂 where;
|
|
26
|
+
- 需求涉及统计、报表、趋势、排行、周期范围、截止时间、创建/完成时间等后端汇总接口。
|
|
27
|
+
|
|
28
|
+
## 审计步骤(写代码前和提交前各做一次)
|
|
29
|
+
|
|
30
|
+
1. 先读 `server/database/schema.ts`,确认相关列类型和 custom type。
|
|
31
|
+
2. 枚举当前 service/repository 里的 raw SQL 模板;不要只看刚新增的查询。可用搜索辅助,但结论以读代码为准:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
rg -n 'sql`|db\.execute\(sql|filter \(|CASE WHEN|OVER \(' server
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
3. 对每个 raw SQL 模板逐个检查 `${...}` 参数:
|
|
38
|
+
- 已是 string/number/boolean、ISO string、普通 ID、`sql.join(...)` 中逐个参数化的标量:可以保留;
|
|
39
|
+
- `user_profile` 是特殊 custom type,不适用“普通 ID 标量即可”的泛化判断;raw SQL 遇到 `user_profile` 列必须按 coding-guide 的三侧写法处理:读 `(col).user_id`,写 `ROW(${userId})::user_profile`,过滤 `(col).user_id = ${userId}`;禁止裸列 SELECT、`col = ${userId}`、`col::text = ${userId}` 或 `col = ROW(...)::user_profile` 比较;
|
|
40
|
+
- 是 `Date`、date/timestamptz/custom type 的运行时对象,或变量来源不明但表示时间/自定义结构:先转 driver-safe 标量再传入 raw SQL;日期时间用 `.toISOString()`;
|
|
41
|
+
- 如果能用 `gte(col, Date)` / `lt(col, Date)` / `eq(col, value)` / `insert().values()` / `update().set()` 等绑定 schema 列的 helper 表达,就优先改回 helper。
|
|
42
|
+
4. 保留 helper 安全路径:不要把 `gte(col, Date)`、`lt(col, Date)` 这类绑定列的 Drizzle helper 当成坏边界。
|
|
43
|
+
5. 改完后调用受影响的真实 API,至少覆盖一个会触发该 raw SQL 的范围/筛选参数;HTTP 200 且后端无参数序列化错误才算完成。
|
|
44
|
+
|
|
45
|
+
## 写法示例
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
const rangeStart: Date = getRangeStart(range);
|
|
49
|
+
const rangeStartIso: string = rangeStart.toISOString();
|
|
50
|
+
|
|
51
|
+
// helper 绑定 schema 列,可以直接传 Date
|
|
52
|
+
where: gte(tasks.createdAt, rangeStart)
|
|
53
|
+
|
|
54
|
+
// raw SQL 参数先转 driver-safe 标量
|
|
55
|
+
total: sql<number>`
|
|
56
|
+
count(*) filter (where ${tasks.createdAt} >= ${rangeStartIso})
|
|
57
|
+
`
|
|
58
|
+
|
|
59
|
+
// 不要把 Date/custom type 运行时对象直接放进 raw SQL 模板参数
|
|
60
|
+
total: sql<number>`
|
|
61
|
+
count(*) filter (where ${tasks.createdAt} >= ${rangeStart})
|
|
62
|
+
`
|
|
63
|
+
```
|
package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md
CHANGED
|
@@ -11,7 +11,7 @@ match-template-name: nestjs-react-fullstack
|
|
|
11
11
|
在 NestJS 服务端代码中使用 `FileService` 进行文件上传、下载、删除和管理。
|
|
12
12
|
|
|
13
13
|
> **使用注意**: 仅用于服务端文件处理场景,非必要文件上传下载场景请使用前端 `client-builtins-file-storage-service` skill。
|
|
14
|
-
>
|
|
14
|
+
> **入口边界**:本 SDK 是**服务端代码**读写应用存储的入口;Agent 在对话 / 开发中自己上传或调试文件用 `miaoda file` CLI(见 `miaoda-file` skill)。二者与本 SDK 操作同一个应用存储桶,但 **`miaoda file` CLI 禁止写进服务端代码**。
|
|
15
15
|
|
|
16
16
|
## 使用场景
|
|
17
17
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: trigger-guide
|
|
3
|
-
description:
|
|
3
|
+
description: 自动化任务触发器配置与代码开发指南,支持 cron 定时触发器、record_change 数据变更触发器和 webhook 触发器,包含 @Automation/@BindTrigger 装饰器用法和 Crontab 表达式规范。Use when 需要:(1) 创建或配置自动化任务/定时任务,(2) 编写 automation 代码绑定触发器,或其他自动化任务相关开发
|
|
4
4
|
steering: true
|
|
5
5
|
steering-topic: trigger_guide
|
|
6
6
|
match-template-name: nestjs-react-fullstack
|
|
@@ -30,7 +30,7 @@ server
|
|
|
30
30
|
1. 每个模块只应该有一个存放自动化任务逻辑的文件,业务逻辑需要聚合到该文件中。
|
|
31
31
|
2. 如果该模块只有对应的自动化任务,无需编写 Controller
|
|
32
32
|
|
|
33
|
-
###
|
|
33
|
+
### 触发器类型与入参
|
|
34
34
|
|
|
35
35
|
触发器类型(`triggerType`)有三种:
|
|
36
36
|
|
|
@@ -38,15 +38,151 @@ server
|
|
|
38
38
|
- `cron`:定时触发器,**无入参**
|
|
39
39
|
- `webhook`:Webhook 触发器,**有入参**
|
|
40
40
|
|
|
41
|
-
|
|
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
|
+
}
|
|
42
55
|
|
|
43
|
-
|
|
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
|
+
}
|
|
44
70
|
|
|
45
|
-
|
|
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
|
+
```
|
|
46
87
|
|
|
47
|
-
###
|
|
88
|
+
### 指定值限制
|
|
89
|
+
1. Webhook 触发器不可以设置指定值,并且告知用户。
|
|
48
90
|
|
|
49
|
-
|
|
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
|
+
```
|
|
50
186
|
|
|
51
187
|
### 任务代码实现约束
|
|
52
188
|
|
|
@@ -62,12 +198,148 @@ server
|
|
|
62
198
|
|
|
63
199
|
### 技术实现路径参考
|
|
64
200
|
|
|
65
|
-
|
|
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
|
+
```
|
|
66
341
|
|
|
67
|
-
|
|
68
|
-
- **场景二:定时任务通知特定用户** —— 任务执行时无法获取用户上下文;在数据库预存目标用户 ID,执行时查询再调用飞书插件发送。
|
|
69
|
-
- **场景三:记录变更触发器防抖/去重** —— 利用数据库记录最近一次处理时间戳,对比 event 时间戳进行去重。
|
|
70
|
-
- **场景四:自定义定时任务触发时间** —— cron 创建后不可动态改;平台设固定高频定时器(如每 30 分钟),执行时读数据库配置判断是否命中。
|
|
342
|
+
> 注意:由于平台最小调度间隔为 30 分钟,用户可配置的时间精度也应限制为 30 分钟的整数倍(如 `09:00`、`09:30`),前端做好校验提示。
|
|
71
343
|
|
|
72
344
|
## Crontab 表达式规范
|
|
73
345
|
|
|
@@ -8,6 +8,8 @@ steering-topic: plugin_guide
|
|
|
8
8
|
match-template-name: nestjs-react-fullstack
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
{% raw %}
|
|
12
|
+
|
|
11
13
|
# Plugin 集成指南(本地开发)
|
|
12
14
|
|
|
13
15
|
AI 插件集成规范,使用 lark-cli 命令管理插件包与实例,通过 capabilityClient / CapabilityService 生成调用代码。
|
|
@@ -578,3 +580,5 @@ npx @lark-apaas/miaoda-cli plugin list --id <instance_id>
|
|
|
578
580
|
5. **禁止用 `npm install` 安装插件包** — 插件包和 npm 包是两套独立机制。
|
|
579
581
|
6. **禁止 Mock** — 必须走真实插件实例调用链路。
|
|
580
582
|
7. **formValue 禁止 Handlebars 控制语法** — 仅允许 `{{input.xxx}}`。
|
|
583
|
+
|
|
584
|
+
{% endraw %}
|
|
@@ -180,7 +180,7 @@ const structured = await (jsonExtractor as any).call('textToJson', { text: rawRe
|
|
|
180
180
|
- **Plugin(插件)**:底层承载单元,包含插件元信息与表单定义(form.schema)。模型侧只感知插件及其表单字段,不感知插件内部实现细节。
|
|
181
181
|
- **PluginInstance(插件实例配置)**:基于某个 Plugin 的表单做"业务封装",以 **单文件 JSON** 的形式存储(每个插件实例一个文件,语义化 id)。
|
|
182
182
|
- 通过 `paramsSchema` 暴露业务入参
|
|
183
|
-
- 通过 `formValue` 将业务入参映射到插件表单字段(可常量或引用 `{{input.xxx}}`)
|
|
183
|
+
- 通过 `formValue` 将业务入参映射到插件表单字段(可常量或引用 `{% raw %}{{input.xxx}}{% endraw %}`)
|
|
184
184
|
- **PluginInstanceAIJson(pluginInstance.ai.json)**:工程转化层产物,是 pluginInstance 的**运行时投影 / 调用合同(Runtime Spec)**。
|
|
185
185
|
- 包含插件定位信息、actions 入口列表、input/output schema、outputMode、readme 等
|
|
186
186
|
- Code Agent 在生成**调用代码**前,必须读取它作为权威依据(使用 `capabilityClient` 调用)
|
|
@@ -224,6 +224,7 @@ Plugin 的具体内容以JSON格式给出,例如:
|
|
|
224
224
|
|
|
225
225
|
PluginInstance 的配置以 JSON 形式输出,例如:
|
|
226
226
|
|
|
227
|
+
{% raw %}
|
|
227
228
|
```json
|
|
228
229
|
{
|
|
229
230
|
"id": "create_feishu_group", // 全局唯一语义化 ID
|
|
@@ -245,6 +246,7 @@ PluginInstance 的配置以 JSON 形式输出,例如:
|
|
|
245
246
|
}
|
|
246
247
|
}
|
|
247
248
|
```
|
|
249
|
+
{% endraw %}
|
|
248
250
|
|
|
249
251
|
**注意**paramsSchema 支持以下 4 种参数类型,需要按下面规定的格式进行填充:
|
|
250
252
|
|
|
@@ -5,6 +5,8 @@ steering: true
|
|
|
5
5
|
steering-topic: react_three_fiber
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
{% raw %}
|
|
9
|
+
|
|
8
10
|
# React Three Fiber (R3F) 编码指南
|
|
9
11
|
|
|
10
12
|
实现 3D 场景 / 3D 游戏 / 3D 数据可视化时, MUST 用 **react-three-fiber + drei** 声明式栈, 严禁用 React + CSS / SVG / `transform: rotateX` 伪 3D.
|
|
@@ -220,3 +222,5 @@ npm install react-error-boundary # 必装! Canvas 外包 ErrorBoundary
|
|
|
220
222
|
|
|
221
223
|
- `client-coding-guide` - vite-react 通用编码规范
|
|
222
224
|
- `component-conventions` - React 组件命名 / 文件结构
|
|
225
|
+
|
|
226
|
+
{% endraw %}
|