@lark-apaas/coding-steering 0.1.32-beta.0 → 0.1.32-dev.05acdd1
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 +71 -26
- package/steering/design-html/skills/pptx-style-extract/font-fallback.yaml +3 -3
- package/steering/design-html/skills/pptx-style-extract/scripts/census.py +26 -14
- 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 +2831 -293
- package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +544 -22
- package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +19 -2
- package/steering/design-html/skills/pptx-style-extract/scripts/package.py +991 -165
- package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +22 -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 +20 -12
- package/steering/design-html/skills/pptx-style-extract/scripts/test_asset_judgment_package.py +556 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +364 -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 +76 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +528 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +1814 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_logo_scope.py +600 -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 +315 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_layout_assets.py +421 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_logo_scope.py +12 -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 +265 -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/mcp-guide/SKILL.md +77 -0
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/debugging.md +81 -0
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/mcp-apps.md +205 -0
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/tool-authoring.md +206 -0
- 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
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# 工具类写法
|
|
2
|
+
|
|
3
|
+
API 形状以 `node_modules/@lark-apaas/nestjs-mcp/dist/index.d.ts` 为准。下面是当前版本的写法与约束;字段名对不上时以 d.ts 为准。
|
|
4
|
+
|
|
5
|
+
## 骨架
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// server/mcp/tools/order.tools.ts
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import {
|
|
11
|
+
McpTools,
|
|
12
|
+
McpTool,
|
|
13
|
+
McpToolError,
|
|
14
|
+
type Infer,
|
|
15
|
+
type McpContext,
|
|
16
|
+
type McpToolResult,
|
|
17
|
+
} from '@lark-apaas/fullstack-nestjs-core';
|
|
18
|
+
import { OrderService } from '@server/modules/order/order.service';
|
|
19
|
+
|
|
20
|
+
// schema 提成常量:方法签名用 Infer<typeof …> 引用,清单导出也从这里取
|
|
21
|
+
export const SearchOrdersInput = {
|
|
22
|
+
keyword: z.string().optional().describe('客户名关键字,模糊匹配'),
|
|
23
|
+
limit: z.number().int().min(1).max(50).default(20).describe('单页条数'),
|
|
24
|
+
cursor: z.string().optional().describe('上一页返回的 nextCursor;首页不传'),
|
|
25
|
+
};
|
|
26
|
+
export const SearchOrdersOutput = {
|
|
27
|
+
items: z.array(
|
|
28
|
+
z.object({
|
|
29
|
+
id: z.string(),
|
|
30
|
+
customer: z.string(),
|
|
31
|
+
amount: z.number().describe('金额,单位元'),
|
|
32
|
+
status: z.enum(['pending', 'paid', 'cancelled']),
|
|
33
|
+
}),
|
|
34
|
+
),
|
|
35
|
+
nextCursor: z.string().nullable().describe('还有下一页时非空,原样传回 cursor'),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const CancelOrderInput = { orderId: z.string().describe('订单 ID,来自 order_search 的 items[].id') };
|
|
39
|
+
export const CancelOrderOutput = { orderId: z.string(), status: z.literal('cancelled') };
|
|
40
|
+
|
|
41
|
+
@McpTools({ prefix: 'order_' })
|
|
42
|
+
export class OrderMcpTools {
|
|
43
|
+
constructor(private readonly orders: OrderService) {}
|
|
44
|
+
|
|
45
|
+
@McpTool({
|
|
46
|
+
title: '查询订单',
|
|
47
|
+
description:
|
|
48
|
+
'按客户名关键字分页查询当前用户可见的订单。用户想找订单、看订单列表时调用;一页最多 50 条,超出用 cursor 翻页。',
|
|
49
|
+
inputSchema: SearchOrdersInput,
|
|
50
|
+
outputSchema: SearchOrdersOutput,
|
|
51
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
52
|
+
})
|
|
53
|
+
async search(
|
|
54
|
+
input: Infer<typeof SearchOrdersInput>,
|
|
55
|
+
ctx: McpContext,
|
|
56
|
+
): Promise<McpToolResult<typeof SearchOrdersOutput>> {
|
|
57
|
+
const userId = ctx.user.userId!; // requireUser 默认开启,执行到这里必有值;取一次往下传
|
|
58
|
+
const page = await this.orders.search({ ...input, userId });
|
|
59
|
+
return {
|
|
60
|
+
structuredContent: page,
|
|
61
|
+
content: [{ type: 'text', text: `找到 ${page.items.length} 条订单` }],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@McpTool({
|
|
66
|
+
title: '取消订单',
|
|
67
|
+
description:
|
|
68
|
+
'取消一个待支付订单。仅在用户明确要求取消、且订单状态为 pending 时调用;订单不存在返回 ORDER_NOT_FOUND,状态不允许返回 ORDER_NOT_CANCELLABLE。重复取消同一订单返回同一结果。',
|
|
69
|
+
inputSchema: CancelOrderInput,
|
|
70
|
+
outputSchema: CancelOrderOutput,
|
|
71
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
72
|
+
})
|
|
73
|
+
async cancel(
|
|
74
|
+
input: Infer<typeof CancelOrderInput>,
|
|
75
|
+
ctx: McpContext,
|
|
76
|
+
): Promise<McpToolResult<typeof CancelOrderOutput>> {
|
|
77
|
+
const userId = ctx.user.userId!;
|
|
78
|
+
const order = await this.orders.findVisibleTo(input.orderId, userId);
|
|
79
|
+
if (!order) throw new McpToolError('订单不存在', { code: 'ORDER_NOT_FOUND' });
|
|
80
|
+
if (order.status === 'cancelled') return { structuredContent: { orderId: order.id, status: 'cancelled' } };
|
|
81
|
+
if (order.status !== 'pending') {
|
|
82
|
+
throw new McpToolError(`订单状态为 ${order.status},不能取消`, { code: 'ORDER_NOT_CANCELLABLE' });
|
|
83
|
+
}
|
|
84
|
+
await this.orders.cancel(order.id, userId);
|
|
85
|
+
return { structuredContent: { orderId: order.id, status: 'cancelled' } };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// server/modules/order/order.module.ts —— 工具类和它依赖的 Service 注册在同一个模块
|
|
92
|
+
import { Module } from '@nestjs/common';
|
|
93
|
+
// OrderController / OrderService / OrderMcpTools 的 import 略
|
|
94
|
+
|
|
95
|
+
@Module({
|
|
96
|
+
controllers: [OrderController],
|
|
97
|
+
providers: [OrderService, OrderMcpTools],
|
|
98
|
+
})
|
|
99
|
+
export class OrderModule {}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`@McpTools()` 自带 `@Injectable()`。SDK 启动时用 DiscoveryService 扫所有 provider;类没进任何模块的 `providers` 就扫不到它,启动日志不会有任何提示,工具静默缺席——启动日志「已注册 N 个 MCP 工具:…」列出的名字才是真相,对不上就是没注册。工具类及其依赖必须是单例:request / transient 作用域会在启动时抛「MCP 工具必须使用单例及单例依赖」,整个应用起不来。
|
|
103
|
+
|
|
104
|
+
## 装饰器选项
|
|
105
|
+
|
|
106
|
+
`@McpTool(options)`:
|
|
107
|
+
|
|
108
|
+
| 字段 | 说明 |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `name?` | 默认取方法名;最终工具名 = `prefix + name`,需匹配 `^[A-Za-z0-9_.-]{1,128}$`,全应用唯一 |
|
|
111
|
+
| `title?` | 面向人的展示名,面板里显示 |
|
|
112
|
+
| `description` | 必填,面向模型 |
|
|
113
|
+
| `inputSchema?` / `outputSchema?` | zod:原始形状 `{ a: z.string() }` 或 `z.object({...})` 都行;顶层必须是对象 |
|
|
114
|
+
| `annotations?` | `readOnlyHint` / `destructiveHint` / `idempotentHint` / `openWorldHint`,四项都写 |
|
|
115
|
+
| `ui?` | `{ resourceUri: 'ui://…', visibility?: ['model' \| 'app'] }`,见 `mcp-apps.md` |
|
|
116
|
+
| `meta?` | 额外 `_meta`,与 `ui` 生成的字段合并 |
|
|
117
|
+
|
|
118
|
+
`@McpTools({ prefix? })`:给类下所有工具名加前缀,如 `order_`。
|
|
119
|
+
|
|
120
|
+
## 方法签名与返回值
|
|
121
|
+
|
|
122
|
+
- 固定 `(input, ctx)`。无入参的工具 `input` 为 `Record<string, never>`,仍要写第二个参数 `ctx`。
|
|
123
|
+
- 声明了 `outputSchema`:返回 `{ structuredContent }`,类型由 schema 推导;`content` 可选,SDK 会自动补一份 JSON 文本给不支持结构化输出的客户端。加一句业务摘要的 `content` 让文本客户端也能读。
|
|
124
|
+
- 未声明 `outputSchema`:返回 `{ content: [{ type: 'text', text }] }`;`content` 还支持 `image` / `audio` / `resource_link` 块。
|
|
125
|
+
- `isError: true` 由 SDK 在捕获 `McpToolError` 时设置,不需要手动返回。
|
|
126
|
+
|
|
127
|
+
## McpContext
|
|
128
|
+
|
|
129
|
+
| 字段 | 用途 |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `user` | 与 REST 里的 `req.userContext` 同源:`userId` / `tenantId` / `roles` / `env`(`preview` \| `runtime`)等,字段全部可选(`userId?: string`)。默认 `requireUser: true`,缺 `userId` 时 SDK 直接返回 `MCP_USER_REQUIRED`、方法不执行,所以方法体开头 `const userId = ctx.user.userId!;` 取一次往下传;不要为迁就类型去改 Service 签名 |
|
|
132
|
+
| `request` | 原始 HTTP 请求,读 header、logid |
|
|
133
|
+
| `signal` | 客户端取消时触发,长任务把它传给下游 |
|
|
134
|
+
| `requestId` | MCP 请求 id |
|
|
135
|
+
| `extra` | SDK 的 `RequestHandlerExtra`(`sessionId` / `requestId` / `authInfo` 等)。它有 `sendNotification`,但端点是无状态 JSON 模式,`notifications/progress` 发不到客户端——长任务别靠进度通知续命,拆成「发起 + 查状态」两个工具 |
|
|
136
|
+
|
|
137
|
+
## 错误处理
|
|
138
|
+
|
|
139
|
+
- 可预期的业务失败抛 `new McpToolError(message, { code?, data? })`:SDK 转成 `isError: true`,`message` / `code` / `data` 原样进 Agent 上下文。`code` 用 `SCREAMING_SNAKE`,并在 `description` 里预告,Agent 才知道怎么应对。
|
|
140
|
+
- 其他异常:SDK 记日志、返回通用失败提示,Agent 看不到原因。调试时看到「通用失败提示」就去读 Trace-ID 日志。
|
|
141
|
+
- 入参校验交给 zod,方法体不重复判空;zod 失败由 SDK 返回,方法不执行。
|
|
142
|
+
|
|
143
|
+
## description 怎么写
|
|
144
|
+
|
|
145
|
+
消费方 Agent 只靠 `description` 与字段 `.describe()` 决定调不调、怎么填。写全五件事:用途、何时调用(含前置条件)、参数从哪来(「来自 order_search 的 items[].id」)、返回什么、失败会返回哪些 code。写操作再加一句幂等语义(「重复取消返回同一结果」)。
|
|
146
|
+
|
|
147
|
+
字段描述写单位、格式、取值来源;枚举用 `z.enum`;入参不用 `z.any()` / `z.unknown()`。
|
|
148
|
+
|
|
149
|
+
## 开通(首次)
|
|
150
|
+
|
|
151
|
+
1. 前置检查通过(见 SKILL.md)。
|
|
152
|
+
2. 建 `server/mcp/tools/<域>.tools.ts`,至少一个只读工具复用现有 Service。
|
|
153
|
+
3. 把工具类加进所属业务模块的 `providers`。
|
|
154
|
+
4. 写 `server/mcp/skills/<name>/SKILL.md`(下节)。
|
|
155
|
+
5. `npx miaoda-mcp validate` 通过,`.spark/mcp/manifest.json` 一起提交。
|
|
156
|
+
6. 进 `debugging.md` 的调试回路。
|
|
157
|
+
|
|
158
|
+
## 应用 Skill:`server/mcp/skills/<name>/SKILL.md`
|
|
159
|
+
|
|
160
|
+
给消费方 Agent 的使用说明。一个业务流程一个目录,目录里只认固定文件名 `SKILL.md`。SDK 把每份注册为同名 MCP Prompt:`prompts/list` 给 `name` / `description`,`prompts/get` 返回去掉 frontmatter 的正文(一条 `role: user` 文本消息)。它不是 Resource、没有 `skill://` URI,`resources/list` 里看不到它是正常的。每次请求重新读文件,改完不用重启。
|
|
161
|
+
|
|
162
|
+
frontmatter 规则。SDK 每次请求、`validate` 都会校验,不合法时整个 MCP 请求 500、`validate` 失败,不会静默跳过这一份:
|
|
163
|
+
|
|
164
|
+
- `name` 必填且等于目录名:1–64 位小写字母、数字与单个连字符,不能首尾连字符、不能连续 `--`。
|
|
165
|
+
- `description` 必填非空,不超过 1024 字符;消费方只靠它决定要不要取这份 Prompt,写清适用场景。
|
|
166
|
+
- 全应用不重名;最多 100 份,单文件不超过 1 MiB,合计不超过 8 MiB;必须是工程内普通文件,不接受符号链接。
|
|
167
|
+
- 只有 `SKILL.md` 会被读取和交付,同目录的图片、附件不会。
|
|
168
|
+
|
|
169
|
+
正文必含:适用场景、实际工具名与关键参数、调用顺序、需要用户确认的边界、身份与权限要求、每个错误 code 的处理、界面 URI(如有)。只写已实现的能力;不写凭证、用户数据、内部实现。
|
|
170
|
+
|
|
171
|
+
示例 `server/mcp/skills/orders/SKILL.md`。`---` 必须是文件第一行,前面不能有任何字符,注释也不行:
|
|
172
|
+
|
|
173
|
+
```markdown
|
|
174
|
+
---
|
|
175
|
+
name: orders
|
|
176
|
+
description: 查询当前用户可见的订单,并在用户明确要求后取消待支付订单。
|
|
177
|
+
---
|
|
178
|
+
# 订单能力
|
|
179
|
+
- 需要已登录用户身份;只返回当前用户可见的订单。
|
|
180
|
+
- 找订单:order_search,keyword 模糊匹配客户名;nextCursor 非空时用 cursor 翻页。
|
|
181
|
+
- 取消订单:只有用户明确要求取消、且 status 为 pending 时调用 order_cancel;orderId 取自 order_search 的 items[].id。
|
|
182
|
+
- ORDER_NOT_FOUND:请用户核对订单 ID。ORDER_NOT_CANCELLABLE:告知当前状态,不重试。
|
|
183
|
+
- 支持界面的宿主会用 ui://order/list 展示查询结果。
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## 校验与清单
|
|
187
|
+
|
|
188
|
+
- `npx miaoda-mcp validate`:用工程自身 TypeScript 加载 `server/mcp` 下的工具源码(不启动应用,跳过 `ui/` 与 `skills/`),检查工具名格式、重名、`ui://` 前缀、`ui.resourceUri` 引用、每份应用 Skill 的 frontmatter,写入 `.spark/mcp/manifest.json`(`tools` / `resources` / `prompts` / `skills` 与源码定位)。
|
|
189
|
+
- `npx miaoda-mcp validate --check`:只比对不写入,CI 用。
|
|
190
|
+
- 开发态启动应用也会写清单;生产不写。删光工具、资源与应用 Skill 后再跑一次 `validate` 清理旧清单。
|
|
191
|
+
- 启动期同样的校验失败会阻断启动(「MCP 定义校验失败」),一个方法不能同时标 `@McpTool()` 与 `@McpUiResource()`。
|
|
192
|
+
|
|
193
|
+
## 模块配置(一般不动)
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
PlatformModule.forRoot({
|
|
197
|
+
mcp: {
|
|
198
|
+
serverName: 'my-app', // 默认 SUDA_APP_ID,再缺省为 miaoda-app
|
|
199
|
+
serverVersion: '1.0.0',
|
|
200
|
+
instructions: '…', // initialize 响应里的说明
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
// 关闭端点:PlatformModule.forRoot({ mcp: false })
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`requireUser` 保持默认 `true`。零工具、零资源、无应用 Skill 时端点返回 404,这是正常状态不是故障。
|
|
@@ -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 %}
|