@lark-apaas/coding-steering 0.1.32-beta.0 → 0.1.32-dev.1b5c337

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.
Files changed (65) hide show
  1. package/package.json +6 -6
  2. package/steering/design-html/skills/charts/SKILL.md +4 -0
  3. package/steering/design-html/skills/pptx-style-extract/SKILL.md +71 -26
  4. package/steering/design-html/skills/pptx-style-extract/font-fallback.yaml +3 -3
  5. package/steering/design-html/skills/pptx-style-extract/scripts/census.py +26 -14
  6. package/steering/design-html/skills/pptx-style-extract/scripts/check_v2.py +153 -8
  7. package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +2831 -293
  8. package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +544 -22
  9. package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +19 -2
  10. package/steering/design-html/skills/pptx-style-extract/scripts/package.py +991 -165
  11. package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +22 -3
  12. package/steering/design-html/skills/pptx-style-extract/scripts/query.py +4 -9
  13. package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +20 -12
  14. package/steering/design-html/skills/pptx-style-extract/scripts/test_asset_judgment_package.py +556 -0
  15. package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +364 -0
  16. package/steering/design-html/skills/pptx-style-extract/scripts/test_color_contract.py +60 -0
  17. package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +76 -0
  18. package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +528 -0
  19. package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +1814 -0
  20. package/steering/design-html/skills/pptx-style-extract/scripts/test_logo_scope.py +600 -0
  21. package/steering/design-html/skills/pptx-style-extract/scripts/test_rounded_contract.py +112 -0
  22. package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +315 -0
  23. package/steering/design-html/skills/pptx-style-extract/scripts/verify_layout_assets.py +421 -0
  24. package/steering/design-html/skills/pptx-style-extract/scripts/verify_logo_scope.py +12 -0
  25. package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +27 -15
  26. package/steering/design-html/skills/preflight/scripts/probe.sh +0 -0
  27. package/steering/nestjs-react-fullstack/skills/app-init-feasibility-guide/SKILL.md +1 -0
  28. package/steering/nestjs-react-fullstack/skills/authn-guide/SKILL.md +6 -0
  29. package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +5 -5
  30. package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +1 -1
  31. package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +37 -113
  32. package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +13 -2
  33. package/steering/nestjs-react-fullstack/skills/code-fix/SKILL.md +7 -7
  34. package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +149 -24
  35. package/steering/nestjs-react-fullstack/skills/connections-sdk/SKILL.md +202 -0
  36. package/steering/nestjs-react-fullstack/skills/nestjs-cache/SKILL.md +265 -0
  37. package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +158 -543
  38. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +15 -1
  39. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +30 -14
  40. package/steering/nestjs-react-fullstack/skills/raw-sql-boundary-audit/SKILL.md +63 -0
  41. package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +1 -1
  42. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/SKILL.md +77 -0
  43. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/debugging.md +76 -0
  44. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/mcp-apps.md +182 -0
  45. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/tool-authoring.md +197 -0
  46. package/steering/nestjs-react-fullstack/skills_common/trigger-guide/SKILL.md +284 -12
  47. package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +4 -0
  48. package/steering/vite-react/skills/plugin-guide/SKILL.md +3 -1
  49. package/steering/vite-react/skills/react-three-fiber/SKILL.md +4 -0
  50. package/steering/nestjs-react-fullstack/skills/client-add-aily-web-chat/SKILL.md +0 -139
  51. package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +0 -269
  52. package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +0 -214
  53. package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +0 -163
  54. package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +0 -311
  55. package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +0 -190
  56. package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +0 -160
  57. package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +0 -257
  58. package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +0 -104
  59. package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +0 -199
  60. package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +0 -128
  61. package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +0 -207
  62. package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +0 -165
  63. package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +0 -91
  64. package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +0 -165
  65. package/steering/nestjs-react-fullstack/skills_common/trigger-guide/references/trigger-lifecycle.md +0 -301
@@ -0,0 +1,197 @@
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/SKILL.md`(下节)。
155
+ 5. `npx miaoda-mcp validate` 通过,`.spark/mcp/manifest.json` 一起提交。
156
+ 6. 进 `debugging.md` 的调试回路。
157
+
158
+ ## 应用 Skill:`server/mcp/SKILL.md`
159
+
160
+ 给消费方 Agent 的使用说明,SDK 自动注册为资源 `skill://app/SKILL.md`(`text/markdown`),每次请求重新读文件,改完不用重启;上限 1 MiB,必须是工程内的普通文件。
161
+
162
+ 必含:`name` / `description` frontmatter、适用场景、实际工具名与关键参数、调用顺序、需要用户确认的边界、身份与权限要求、每个错误 code 的处理、界面 URI(如有)。只写已实现的能力;不写凭证、用户数据、内部实现。
163
+
164
+ ```markdown
165
+ ---
166
+ name: orders
167
+ description: 查询当前用户可见的订单,并在用户明确要求后取消待支付订单。
168
+ ---
169
+ # 订单能力
170
+ - 需要已登录用户身份;只返回当前用户可见的订单。
171
+ - 找订单:order_search,keyword 模糊匹配客户名;nextCursor 非空时用 cursor 翻页。
172
+ - 取消订单:只有用户明确要求取消、且 status 为 pending 时调用 order_cancel;orderId 取自 order_search 的 items[].id。
173
+ - ORDER_NOT_FOUND:请用户核对订单 ID。ORDER_NOT_CANCELLABLE:告知当前状态,不重试。
174
+ - 支持界面的宿主会用 ui://order/list 展示查询结果。
175
+ ```
176
+
177
+ ## 校验与清单
178
+
179
+ - `npx miaoda-mcp validate`:用工程自身 TypeScript 加载 `server/mcp` 源码(不启动应用),检查工具名格式、重名、`ui://` 前缀、`ui.resourceUri` 引用,写入 `.spark/mcp/manifest.json`。
180
+ - `npx miaoda-mcp validate --check`:只比对不写入,CI 用。
181
+ - 开发态启动应用也会写清单;生产不写。删光工具、资源与应用 Skill 后再跑一次 `validate` 清理旧清单。
182
+ - 启动期同样的校验失败会阻断启动(「MCP 定义校验失败」),一个方法不能同时标 `@McpTool()` 与 `@McpUiResource()`。
183
+
184
+ ## 模块配置(一般不动)
185
+
186
+ ```typescript
187
+ PlatformModule.forRoot({
188
+ mcp: {
189
+ serverName: 'my-app', // 默认 SUDA_APP_ID,再缺省为 miaoda-app
190
+ serverVersion: '1.0.0',
191
+ instructions: '…', // initialize 响应里的说明
192
+ },
193
+ });
194
+ // 关闭端点:PlatformModule.forRoot({ mcp: false })
195
+ ```
196
+
197
+ `requireUser` 保持默认 `true`。零工具、零资源、无应用 Skill 时端点返回 404,这是正常状态不是故障。
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: trigger-guide
3
- description: 自动化任务触发器代码开发指南,支持 cron 定时触发器、record_change 数据变更触发器和 webhook 触发器,包含 @Automation/@BindTrigger 装饰器用法、handler 入参解析和 Crontab 表达式规范。Use when 需要:(1) 为已创建的自动化任务/定时任务编写业务 handler,(2) 编写 automation 代码绑定触发器,或其他自动化任务相关开发
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
- 各触发器 handler 的入参类型定义(`TaskHandlerArgs`、`DataChangeEventInput`、`WebhookEvent`)见 [触发器入参类型与代码示例](references/trigger-lifecycle.md)。
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
- 1. Webhook 触发器不可以设置指定值,并且告知用户。
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
- 你需要根据触发器创建后确定的自动化任务名字(应用内唯一),编写并绑定到对应的方法上:`@BindTrigger('<任务名字>')` 中的名字必须与创建触发器时确定的名字逐字相同,不能用 trigger ID 或方法名代替。`@Automation()` 标记的类需注册为对应 `<module>.module.ts` 的 provider,且该 module 必须被 `server/app.module.ts` 直接或传递 import,否则装饰器不会生效。完整代码示例见 [触发器入参类型与代码示例](references/trigger-lifecycle.md)。
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
- 以下常见需求的推荐实现路径,帮助你在平台能力限制下找到合理的技术方案;完整代码见 [触发器入参类型与代码示例](references/trigger-lifecycle.md)。
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
- - **场景一:管理页面控制定时任务启停** —— 平台侧不支持通过 API 动态启停触发器;定时触发器始终保持开启,在任务执行时查询数据库中的开关状态决定是否执行。
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 %}