@lark-apaas/coding-steering 0.1.18-dev.6de99aa → 0.1.18-dev.7f786ca

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 (43) hide show
  1. package/README.md +19 -21
  2. package/package.json +1 -1
  3. package/steering/design-html/skills/animated-video/SKILL.md +2 -2
  4. package/steering/design-html/skills/charts/SKILL.md +52 -7
  5. package/steering/design-html/skills/{data-report → data-viz}/SKILL.md +65 -9
  6. package/steering/design-html/skills/frontend-design/SKILL.md +2 -2
  7. package/steering/design-html/skills/mini-game/SKILL.md +71 -0
  8. package/steering/design-html/skills/mini-game/references/three-js.md +54 -0
  9. package/steering/design-html/skills/pptx-style-extract/SKILL.md +148 -0
  10. package/steering/design-html/skills/pptx-style-extract/font-fallback.yaml +129 -0
  11. package/steering/design-html/skills/pptx-style-extract/scripts/census.py +961 -0
  12. package/steering/design-html/skills/pptx-style-extract/scripts/check_v2.py +1052 -0
  13. package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +2804 -0
  14. package/steering/design-html/skills/pptx-style-extract/scripts/export_consumer_md.py +75 -0
  15. package/steering/design-html/skills/pptx-style-extract/scripts/export_consumer_zip.py +175 -0
  16. package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +1068 -0
  17. package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +716 -0
  18. package/steering/design-html/skills/pptx-style-extract/scripts/package.py +1464 -0
  19. package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +464 -0
  20. package/steering/design-html/skills/pptx-style-extract/scripts/query.py +557 -0
  21. package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +685 -0
  22. package/steering/design-html/skills/pptx-style-extract/scripts/test_asset_judgment_package.py +161 -0
  23. package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +57 -0
  24. package/steering/design-html/skills/pptx-style-extract/scripts/test_color_contract.py +60 -0
  25. package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +63 -0
  26. package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +468 -0
  27. package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +503 -0
  28. package/steering/design-html/skills/pptx-style-extract/scripts/test_rounded_contract.py +112 -0
  29. package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +208 -0
  30. package/steering/design-html/skills/pptx-style-extract/scripts/verify_font.py +68 -0
  31. package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +205 -0
  32. package/steering/design-html/skills/preflight/SKILL.md +26 -131
  33. package/steering/design-html/skills/preflight/scripts/probe.sh +108 -0
  34. package/steering/design-html/skills/slide-deck/SKILL.md +160 -0
  35. package/steering/design-html/skills/slide-deck/scripts/check_local_references.py +179 -0
  36. package/steering/design-html/skills/{visual-exposure → visual-report}/SKILL.md +24 -2
  37. package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +5 -3
  38. package/steering/nestjs-react-fullstack/skills_common/trigger-guide/SKILL.md +180 -0
  39. package/steering/nestjs-react-fullstack/{skills/trigger-guide/SKILL.md → skills_common/trigger-guide/references/trigger-lifecycle.md} +11 -162
  40. package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +4 -0
  41. package/steering/vite-react/skills/plugin-guide/SKILL.md +3 -1
  42. package/steering/vite-react/skills/react-three-fiber/SKILL.md +4 -0
  43. package/steering/design-html/skills/make-a-deck/SKILL.md +0 -209
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import re
4
+ import sys
5
+ from html.parser import HTMLParser
6
+ from pathlib import Path
7
+ from urllib.parse import unquote, urlsplit
8
+
9
+
10
+ CSS_URL_PATTERN = re.compile(r"""url\(\s*(['"]?)(.*?)\1\s*\)""", re.IGNORECASE)
11
+ CSS_IMPORT_PATTERN = re.compile(
12
+ r"""@import\s+(?:url\(\s*)?(['"])(.*?)\1\s*\)?""",
13
+ re.IGNORECASE,
14
+ )
15
+ CSS_COMMENT_PATTERN = re.compile(r"/\*.*?\*/", re.DOTALL)
16
+ HREF_RESOURCE_TAGS = {"image", "link", "use"}
17
+ SRC_RESOURCE_TAGS = {
18
+ "audio",
19
+ "embed",
20
+ "iframe",
21
+ "img",
22
+ "input",
23
+ "script",
24
+ "source",
25
+ "track",
26
+ "video",
27
+ }
28
+
29
+
30
+ class ReferenceParser(HTMLParser):
31
+ def __init__(self) -> None:
32
+ super().__init__()
33
+ self.references: set[str] = set()
34
+ self.in_style = False
35
+
36
+ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
37
+ tag = tag.lower()
38
+ if tag == "style":
39
+ self.in_style = True
40
+ attributes = dict(attrs)
41
+ if tag in HREF_RESOURCE_TAGS and attributes.get("href"):
42
+ self.references.add(attributes["href"])
43
+ if tag in SRC_RESOURCE_TAGS and attributes.get("src"):
44
+ self.references.add(attributes["src"])
45
+ if tag == "object" and attributes.get("data"):
46
+ self.references.add(attributes["data"])
47
+ if tag == "video" and attributes.get("poster"):
48
+ self.references.add(attributes["poster"])
49
+
50
+ srcset = attributes.get("srcset")
51
+ if tag in {"img", "source"} and srcset:
52
+ self.references.update(
53
+ candidate.strip().split()[0]
54
+ for candidate in srcset.split(",")
55
+ if candidate.strip()
56
+ )
57
+
58
+ style = attributes.get("style")
59
+ if style:
60
+ self.references.update(css_references(style))
61
+
62
+ def handle_endtag(self, tag: str) -> None:
63
+ if tag.lower() == "style":
64
+ self.in_style = False
65
+
66
+ def handle_data(self, data: str) -> None:
67
+ if self.in_style:
68
+ self.references.update(css_references(data))
69
+
70
+
71
+ def css_references(content: str) -> set[str]:
72
+ content_without_comments = CSS_COMMENT_PATTERN.sub("", content)
73
+ references = {
74
+ match.group(2).strip() for match in CSS_URL_PATTERN.finditer(content_without_comments)
75
+ }
76
+ references.update(
77
+ match.group(2).strip() for match in CSS_IMPORT_PATTERN.finditer(content_without_comments)
78
+ )
79
+ return references
80
+
81
+
82
+ def resolve_local_reference(
83
+ raw_reference: str,
84
+ source: Path,
85
+ project_root: Path,
86
+ ) -> Path | None:
87
+ reference = raw_reference.strip()
88
+ parsed = urlsplit(reference)
89
+ if (
90
+ not reference
91
+ or reference.startswith(("#", "//"))
92
+ or (parsed.scheme and parsed.scheme.lower() != "file")
93
+ or parsed.netloc
94
+ ):
95
+ return None
96
+
97
+ path_text = unquote(parsed.path)
98
+ if not path_text:
99
+ return None
100
+
101
+ if parsed.scheme.lower() == "file":
102
+ return Path(path_text).resolve()
103
+ if path_text.startswith("/"):
104
+ return (project_root / path_text.lstrip("/")).resolve()
105
+ return (source.parent / path_text).resolve()
106
+
107
+
108
+ def collect_references(entry: Path, project_root: Path) -> list[tuple[Path, str, Path]]:
109
+ pending = [entry]
110
+ visited: set[Path] = set()
111
+ local_references: list[tuple[Path, str, Path]] = []
112
+
113
+ while pending:
114
+ source = pending.pop()
115
+ if source in visited or not source.is_file():
116
+ continue
117
+ visited.add(source)
118
+
119
+ content = source.read_text(encoding="utf-8")
120
+ if source.suffix.lower() == ".css":
121
+ references = css_references(content)
122
+ else:
123
+ parser = ReferenceParser()
124
+ parser.feed(content)
125
+ references = parser.references
126
+
127
+ for raw_reference in sorted(references):
128
+ target = resolve_local_reference(raw_reference, source, project_root)
129
+ if target is None:
130
+ continue
131
+ local_references.append((source, raw_reference, target))
132
+ if target.suffix.lower() == ".css" and target.is_file():
133
+ pending.append(target)
134
+
135
+ return local_references
136
+
137
+
138
+ def display_path(path: Path, project_root: Path) -> str:
139
+ try:
140
+ return path.relative_to(project_root).as_posix()
141
+ except ValueError:
142
+ return str(path)
143
+
144
+
145
+ def main() -> int:
146
+ if len(sys.argv) != 2:
147
+ print("usage: check_local_references.py <entry.html>", file=sys.stderr)
148
+ return 2
149
+
150
+ project_root = Path.cwd().resolve()
151
+ entry = (project_root / sys.argv[1]).resolve()
152
+ if not entry.is_file():
153
+ print(f"RESOURCE_CHECK: FAIL entryNotFound={sys.argv[1]}")
154
+ return 1
155
+
156
+ missing: list[tuple[Path, str, Path]] = []
157
+ for source, raw_reference, target in collect_references(entry, project_root):
158
+ try:
159
+ target.relative_to(project_root)
160
+ except ValueError:
161
+ missing.append((source, raw_reference, target))
162
+ continue
163
+ if not target.is_file():
164
+ missing.append((source, raw_reference, target))
165
+
166
+ if not missing:
167
+ print("RESOURCE_CHECK: PASS")
168
+ return 0
169
+
170
+ print(f"RESOURCE_CHECK: FAIL missingLocalReferences={len(missing)}")
171
+ for source, raw_reference, target in missing:
172
+ source_name = display_path(source, project_root)
173
+ target_name = display_path(target, project_root)
174
+ print(f"- {source_name}: {raw_reference} -> {target_name}")
175
+ return 1
176
+
177
+
178
+ if __name__ == "__main__":
179
+ raise SystemExit(main())
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: visual-exposure
2
+ name: visual-report
3
3
  description: 用于制作可视化报告、专题视觉页、信息图、视觉长图、概念可视化、产品能力曝光、方案亮点展示等内容型 HTML 视觉作品。适合用户想把材料、数据或观点组织成可阅读、可展示、可传播的视觉化表达,但不希望做成 PPT、传统 dashboard 或纯 ECharts 图表的场景。触发词:可视化报告, 视觉报告, 可视化曝光, 视觉化曝光, 信息图, 长图, infographic, 视觉表达, 概念可视化, 亮点展示, 能力曝光
4
4
  metadata:
5
5
  display-names:
@@ -45,6 +45,23 @@ metadata:
45
45
 
46
46
  不要为了“丰富”而乱放装饰。变化应该来自内容关系和阅读任务,而不是从组件清单里凑满页面。
47
47
 
48
+ ## 移动端适配
49
+
50
+ 可视化报告的产物(长页报告、专题页、信息图)经常在手机上被打开和转发。桌面端的多列版式、满版图文和精细间距到了 390px 宽度上会挤碎。写完桌面布局后,必须为窄屏补充响应式处理:
51
+
52
+ **页面基础**:HTML 必须包含 `<meta name="viewport" content="width=device-width, initial-scale=1">`。
53
+
54
+ **版式折叠**:
55
+
56
+ - **多列章节**(并排图文、对比矩阵、左右证据栏):移动端折叠为单列堆叠。用 `auto-fit + minmax(320px, 1fr)` 自动折叠,或 `@media (max-width: 768px)` 显式切换。
57
+ - **满版主视觉 / 封面**:桌面端的固定高度大图在移动端改为 `aspect-ratio` 或 `min-height` + `max-height` 约束,避免图片撑满整屏看不到内容。
58
+ - **数字/指标区**:横排的 KPI 或关键数字在移动端折叠为 2 列或纵向排列,每个数字块至少 160px 宽。
59
+ - **图表**:图表容器的窄屏处理由 charts skill 的「窄屏适配」规则覆盖。
60
+ - **宽表格 / 时间线 / 矩阵**:加 `overflow-x: auto` 容器让内容可横向滚动,不要压缩到不可读。
61
+ - **大字标题**:桌面端 48px+ 的展示字体在移动端用 `clamp()` 或 `@media` 缩到合理范围(如 `clamp(24px, 6vw, 48px)`),避免单词撑出视口。
62
+
63
+ **字号底线**:移动端正文不低于 14px,标注 / 图注不低于 12px。
64
+
48
65
  ## 视觉原则
49
66
 
50
67
  - 优先清楚,其次好看。读者应该先理解结构,再感受到风格。
@@ -54,6 +71,7 @@ metadata:
54
71
  - 风格跟随内容、受众和品牌:可以正式、温和、技术、编辑化、品牌化或实验感,但不要从某个样例场景继承固定颜色、固定目录或固定组件。
55
72
  - 每份报告应有一个可解释的签名元素。签名元素要从用户主题、材料质感和阅读任务中生成,而不是复用固定手法;它可以是任何能组织内容、建立记忆点并保持一致性的视觉规则。
56
73
  - 真实素材优先:用户给的截图、logo、图片、图标、数据片段要优先使用。没有素材时,用清楚的占位结构和可替换文案。
74
+ - 数据忠实度:页面中展示的每个数值必须可溯源到用户提供的数据或可验证的计算过程。源数据不含的派生指标(同比/环比、完成率等缺少基准数据的)不编造——用"—"占位或省略。确需补充示例数据时,必须用视觉标记(虚线边框、"示例数据"标签、灰色斜体)明确区分。
57
75
  - 允许少量动效,但只用于进入、强调或引导阅读,不做干扰理解的持续动画。
58
76
  - 可以包含数字、图表和表格,但它们服务于报告叙事;不要为了“可视化”而把所有内容都做成图。
59
77
  - 深色区域可以用于封面、结论、行动区或整篇报告的主视觉;只要它服务主题气质和阅读体验,而不是作为无依据的装饰。
@@ -77,4 +95,8 @@ metadata:
77
95
  - 文字密度可读,没有小字堆叠。
78
96
  - 图标、线条、颜色和卡片样式属于同一套视觉语言。
79
97
  - 明暗选择能解释为什么适合这个主题;无论浅色还是暗色,都保证长文、图表和表格可读。
80
- - 事实性内容没有编造;不确定内容用中性描述或占位说明。
98
+ - 事实性内容没有编造;不确定内容用中性描述或占位说明。
99
+ - 页面中每个数值可溯源到用户提供的数据;缺少基准数据的派生指标(同比/环比/完成率等)没有编造数值,而是用"—"占位或省略。
100
+ - HTML 包含 `<meta name="viewport" content="width=device-width, initial-scale=1">`。
101
+ - 多列版式在 390px 视口下折叠为单列且无横向滚动;宽表格 / 矩阵有 `overflow-x: auto` 包裹。
102
+ - 移动端字号达到底线(正文 ≥14px、图注 ≥12px),大标题没有撑出视口。
@@ -215,7 +215,7 @@ const structured = await capabilityClient
215
215
  - **Plugin(插件)**:底层承载单元,包含插件元信息与表单定义(form.schema)。模型侧只感知插件及其表单字段,不感知插件内部实现细节。
216
216
  - **PluginInstance(插件实例配置)**:基于某个 Plugin 的表单做"业务封装",以 **单文件 JSON** 的形式存储(每个插件实例一个文件,语义化 id)。
217
217
  - 通过 `paramsSchema` 暴露业务入参
218
- - 通过 `formValue` 将业务入参映射到插件表单字段(可常量或引用 `{{input.xxx}}`)
218
+ - 通过 `formValue` 将业务入参映射到插件表单字段(可常量或引用 `{% raw %}{{input.xxx}}{% endraw %}`)
219
219
  - **PluginInstanceAIJson(pluginInstance.ai.json)**:工程转化层产物,是 pluginInstance 的**运行时投影 / 调用合同(Runtime Spec)**。
220
220
  - 包含插件定位信息、actions 入口列表、input/output schema、outputMode、readme 等
221
221
  - Code Agent 在生成**调用代码**前,必须读取它作为权威依据(Server 侧用 `CapabilityService`,Client 侧用 `capabilityClient`)
@@ -258,6 +258,7 @@ Plugin 的具体内容以JSON格式给出,例如:
258
258
 
259
259
 
260
260
  PluginInstance 的配置以 JSON 形式输出,例如:
261
+ {% raw %}
261
262
  ```json
262
263
  {
263
264
  "id": "create_feishu_group", // 全局唯一语义化 ID
@@ -279,6 +280,7 @@ PluginInstance 的配置以 JSON 形式输出,例如:
279
280
  }
280
281
  }
281
282
  ```
283
+ {% endraw %}
282
284
 
283
285
  **注意**paramsSchema 支持以下 4 种参数类型,需要按下面规定的格式进行填充:
284
286
 
@@ -560,7 +562,7 @@ PluginInstanceAIJson 的配置以 JSON 形式输出,例如:
560
562
  | 未按 `outputSchema` 解析返回值,猜测返回结构 | 严格按 `get_plugin_ai_json` 返回的 `outputSchema` 读取字段,流式和非流式均适用 |
561
563
  | 未输出 Schema 摘录卡就直接写调用代码 | 先完成“编码前闸门”中的摘录卡,再开始编码 |
562
564
  | 改完未做真实调用冒烟就宣告完成 | 至少完成一次 unary/stream 真实调用验证,并附最小日志字段 |
563
- | formValue 中用 `["{{input.xxx}}"]` 包装已经是 `type: array` 的 paramsSchema 参数 | 当 paramsSchema 定义为 array 时,formValue 应透传 `"{{input.xxx}}"`,不要再包一层数组 |
565
+ | formValue 中用 `{% raw %}["{{input.xxx}}"]{% endraw %}` 包装已经是 `type: array` 的 paramsSchema 参数 | 当 paramsSchema 定义为 array 时,formValue 应透传 `{% raw %}"{{input.xxx}}"{% endraw %}`,不要再包一层数组 |
564
566
  | 通过 `getDataloom().capability` 或 `(dataloom as any).capability` 调用插件 | `capabilityClient` 是独立导入,不通过 dataloom 访问。dataloom 仅提供 storage 和 service |
565
567
  | Client 侧调用插件时,先通过 dataloom 上传文件拿 URL 再传给插件 | Client 侧可直接传 File/Blob 对象给 `capabilityClient`,SDK 自动处理上传。适用于所有文件类型字段(`format` 为 `file`/`picture`/`plugin-file-url`)。Server 侧仍需传 URL |
566
568
  | 前端调用插件后不保存结果到数据库,导致页面刷新后数据丢失 | 需要持久化时:优先在 Server 侧调用并直接落库(方案A);若在 Client 侧调用,必须通过已有 CRUD 接口立即保存结果(方案B) |
@@ -642,7 +644,7 @@ try {
642
644
  | 场景 | 正确做法 | 示例 |
643
645
  |------|---------|------|
644
646
  | 需求明确的**固定**接收人/配置 | 在 `plugin_instance CREATE` 的 `formValue` 中直接写死 | `formValue.receiverUserList: ["1854102143505690"]` |
645
- | **动态**接收人/配置(按角色/条件变化) | 从配置/平台 API/DB 获取,传入 `input` 参数 | `formValue.receiverUserList: "{{input.receiverIds}}"` |
647
+ | **动态**接收人/配置(按角色/条件变化) | 从配置/平台 API/DB 获取,传入 `input` 参数 | `{% raw %}formValue.receiverUserList: "{{input.receiverIds}}"{% endraw %}` |
646
648
 
647
649
  > **关键区分**:`formValue` 中配置固定值 ≠ 代码中硬编码。`formValue` 是插件实例的声明式配置,修改不需要改代码;而代码中硬编码的值散落在业务逻辑中,难以维护。
648
650
 
@@ -0,0 +1,180 @@
1
+ ---
2
+ name: trigger-guide
3
+ description: 自动化任务触发器代码开发指南,支持 cron 定时触发器、record_change 数据变更触发器和 webhook 触发器,包含 @Automation/@BindTrigger 装饰器用法、handler 入参解析和 Crontab 表达式规范。Use when 需要:(1) 为已创建的自动化任务/定时任务编写业务 handler,(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
+ 各触发器 handler 的入参类型定义(`TaskHandlerArgs`、`DataChangeEventInput`、`WebhookEvent`)见 [触发器入参类型与代码示例](references/trigger-lifecycle.md)。
42
+
43
+ ### 指定值限制
44
+
45
+ 1. Webhook 触发器不可以设置指定值,并且告知用户。
46
+
47
+ ### 代码绑定
48
+
49
+ 你需要根据触发器创建后确定的自动化任务名字(应用内唯一),编写并绑定到对应的方法上:`@BindTrigger('<任务名字>')` 中的名字必须与创建触发器时确定的名字逐字相同,不能用 trigger ID 或方法名代替。`@Automation()` 标记的类需注册为对应 `<module>.module.ts` 的 provider,且该 module 必须被 `server/app.module.ts` 直接或传递 import,否则装饰器不会生效。完整代码示例见 [触发器入参类型与代码示例](references/trigger-lifecycle.md)。
50
+
51
+ ### 任务代码实现约束
52
+
53
+ 1. 执行自动化任务时无法获取用户信息。依赖用户信息的场景,实现路径如下:
54
+ - 需要查询数据库中的特定数据,给用户发消息:数据库中需要存储用户 id,使用从数据库中查询到的用户 id 进行后续操作
55
+ - 需要调用飞书能力给用户发消息:飞书能力不应该接受用户信息作为参数,而是应该在飞书能力配置里要求用户自己预先指定
56
+
57
+ 2. 入参解析规范(仅 record_change 和 webhook 触发器):
58
+ - 有入参的触发器方法签名为 `async methodName(event: TaskHandlerArgs)`,`cron` 触发器无入参
59
+ - `content.input` 是 JSON 字符串,先用 `typeof input === 'string'` 检查类型,再用 `JSON.parse()` 解析,需添加 try-catch 错误处理
60
+ - `record_change`:根据操作类型获取数据:INSERT/UPDATE 使用 `after` 字段,DELETE 使用 `before` 字段
61
+ - `webhook`:从 `method`、`path`、`query`、`headers`、`body` 中按需取用;`body` 本身也是 JSON 字符串,需要时再次 `JSON.parse()` 解析;`query` 和 `headers` 的值均为 `string[]`
62
+
63
+ ### 技术实现路径参考
64
+
65
+ 以下常见需求的推荐实现路径,帮助你在平台能力限制下找到合理的技术方案;完整代码见 [触发器入参类型与代码示例](references/trigger-lifecycle.md)。
66
+
67
+ - **场景一:管理页面控制定时任务启停** —— 平台侧不支持通过 API 动态启停触发器;定时触发器始终保持开启,在任务执行时查询数据库中的开关状态决定是否执行。
68
+ - **场景二:定时任务通知特定用户** —— 任务执行时无法获取用户上下文;在数据库预存目标用户 ID,执行时查询再调用飞书插件发送。
69
+ - **场景三:记录变更触发器防抖/去重** —— 利用数据库记录最近一次处理时间戳,对比 event 时间戳进行去重。
70
+ - **场景四:自定义定时任务触发时间** —— cron 创建后不可动态改;平台设固定高频定时器(如每 30 分钟),执行时读数据库配置判断是否命中。
71
+
72
+ ## Crontab 表达式规范
73
+
74
+ ### 基本结构
75
+
76
+ Crontab 表达式由 5 个字段组成:`<minute> <hour> <day> <month> <week>`
77
+
78
+ ### 字段说明
79
+
80
+ 1. **minute(分钟)**:0-59 的整数
81
+ 2. **hour(小时)**:0-23 的整数
82
+ 3. **day(日期)**:1-31 的整数,或大写字母 `L` 表示月份的最后一天
83
+ 4. **month(月份)**:1-12 的整数
84
+ 5. **week(星期)**:0-6 的整数,其中 0 表示星期天
85
+
86
+ ### 特殊字符
87
+
88
+ - **星号 `*`**:表示所有可能的值(每)
89
+ - 例:`* * * * *` 表示每分钟
90
+ - **逗号 `,`**:表示列表范围
91
+ - 例:`1,2,3 * * * *` 表示每小时的第 1、2、3 分钟
92
+ - **中杠 `-`**:表示数值范围
93
+ - 例:`1-10 * * * *` 表示每小时的第 1 到 10 分钟
94
+ - **正斜线 `/`**:表示间隔频率
95
+ - 例:`0 10-18/2 * * *` 表示每天 10 点到 18 点,每隔 2 小时执行
96
+
97
+ ## 输出要求
98
+
99
+ 1. 必须以 JSON 格式输出
100
+ 2. JSON 包含两个字段:
101
+ - `expression`:Crontab 表达式字符串
102
+ - `explanation`:中文说明,简要描述执行时间
103
+ 3. 如果用户描述不清晰,请询问具体细节
104
+
105
+ ## 示例
106
+
107
+ **用户输入**:每天早上 8 点执行
108
+
109
+ **输出**:
110
+
111
+ ```json
112
+ {
113
+ "expression": "0 8 * * *",
114
+ "explanation": "每天早上 8:00 执行"
115
+ }
116
+ ```
117
+
118
+ **用户输入**:每周一到周五的上午 9 点和下午 6 点执行
119
+
120
+ **输出**:
121
+
122
+ ```json
123
+ {
124
+ "expression": "0 9,18 * * 1-5",
125
+ "explanation": "每周一至周五的 9:00 和 18:00 执行"
126
+ }
127
+ ```
128
+
129
+ **用户输入**:每隔 30 分钟执行一次
130
+
131
+ **输出**:
132
+
133
+ ```json
134
+ {
135
+ "expression": "*/30 * * * *",
136
+ "explanation": "每隔 30 分钟执行一次"
137
+ }
138
+ ```
139
+
140
+ **用户输入**:每月最后一天的晚上 11 点执行
141
+
142
+ **输出**:
143
+
144
+ ```json
145
+ {
146
+ "expression": "0 23 L * *",
147
+ "explanation": "每月最后一天的 23:00 执行"
148
+ }
149
+ ```
150
+
151
+ **用户输入**:每个工作日的每小时第 15 和 45 分钟执行
152
+
153
+ **输出**:
154
+
155
+ ```json
156
+ {
157
+ "expression": "15,45 * * * 1-5",
158
+ "explanation": "每周一至周五,每小时的第 15 和 45 分钟执行"
159
+ }
160
+ ```
161
+
162
+ **用户输入**:每天上午 10 点到下午 6 点,每隔 2 小时执行
163
+
164
+ **输出**:
165
+
166
+ ```json
167
+ {
168
+ "expression": "0 10-18/2 * * *",
169
+ "explanation": "每天 10:00、12:00、14:00、16:00、18:00 执行"
170
+ }
171
+ ```
172
+
173
+ ## 注意事项
174
+
175
+ - 星期字段:0 和 7 都可以表示星期天(但本规范使用 0)
176
+ - 时间采用 24 小时制
177
+ - 月份和星期都从较小的数字开始计数
178
+ - 确保生成的表达式符合实际日历逻辑
179
+ - 由于技术限制,最小间隔为 30 分钟,如用户要求有误请直接拒绝用户并给出原因
180
+ - 输出必须是有效的 JSON 格式
@@ -1,36 +1,8 @@
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
- ---
1
+ # 触发器入参类型与代码示例
8
2
 
9
- ## 自动化任务配置与代码编写指引
3
+ reference 承载 nestjs-react-fullstack 触发器 handler 的入参类型定义、完整代码示例与常见实现场景。先读主 [trigger-guide](../SKILL.md) 了解目录结构、绑定约束与配置要求。
10
4
 
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
- ### 触发器类型与入参
5
+ ## 触发器类型与入参
34
6
 
35
7
  触发器类型(`triggerType`)有三种:
36
8
 
@@ -85,12 +57,11 @@ interface WebhookEvent {
85
57
  }
86
58
  ```
87
59
 
88
- ### 指定值限制
89
- 1. Webhook 触发器不可以设置指定值,并且告知用户。
60
+ `DataChangeEventInput.type` 只定义 `INSERT`、`UPDATE`、`DELETE`,不包含 `UPSERT`。
90
61
 
91
- ### 代码示例
62
+ ## 代码示例
92
63
 
93
- 你需要根据 `automation_trigger_manager` 工具返回的自动化任务名字,编写并绑定到对应的方法上。具体代码示例如下:
64
+ 根据触发器创建后确定的任务名字(应用内唯一),编写并绑定到对应的方法上。使用模板已有的 `@lark-apaas/fullstack-nestjs-core` 聚合入口导入 `Automation` / `BindTrigger`,不要求项目再感知底层 trigger 包。具体代码示例如下:
94
65
 
95
66
  ```typescript
96
67
  // 文件名:demo.automation.ts
@@ -184,23 +155,11 @@ export class DemoAutomationTasksService {
184
155
  }
185
156
  ```
186
157
 
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
- ### 技术实现路径参考
158
+ ## 技术实现路径参考
200
159
 
201
160
  以下是一些常见需求的推荐实现路径,帮助你在平台能力限制下找到合理的技术方案。
202
161
 
203
- #### 场景一:用户需要管理页面控制定时任务的启停
162
+ ### 场景一:用户需要管理页面控制定时任务的启停
204
163
 
205
164
  平台侧不支持通过 API 动态启停触发器。推荐方案:**平台定时触发器始终保持开启,在任务执行时查询数据库中的开关状态,决定是否真正执行业务逻辑。**
206
165
 
@@ -233,7 +192,7 @@ export class ReportAutomationService {
233
192
  }
234
193
  ```
235
194
 
236
- #### 场景二:定时任务需要将结果通知给特定用户
195
+ ### 场景二:定时任务需要将结果通知给特定用户
237
196
 
238
197
  自动化任务执行时无法获取当前用户上下文。推荐方案:**在数据库中预存需要通知的用户 ID,任务执行时从数据库查询目标用户,再调用飞书插件发送通知。**
239
198
 
@@ -267,7 +226,7 @@ export class NotifyAutomationService {
267
226
  }
268
227
  ```
269
228
 
270
- #### 场景三:记录变更触发器需要做防抖/去重
229
+ ### 场景三:记录变更触发器需要做防抖/去重
271
230
 
272
231
  高频数据变更场景下,同一条记录可能短时间内触发多次。推荐方案:**利用数据库记录最近一次处理时间戳,对比 event 时间戳进行去重。**
273
232
 
@@ -294,7 +253,7 @@ async handleOrderChange(event: TaskHandlerArgs) {
294
253
  }
295
254
  ```
296
255
 
297
- #### 场景四:用户需要自定义定时任务的触发时间
256
+ ### 场景四:用户需要自定义定时任务的触发时间
298
257
 
299
258
  平台侧的 cron 表达式在触发器创建后无法由用户动态修改。推荐方案:**平台设置一个固定的高频定时器(如每 30 分钟执行一次),在任务执行时从数据库读取用户配置的触发时间,判断当前是否命中再决定是否执行。**
300
259
 
@@ -340,113 +299,3 @@ export class ScheduleAutomationService {
340
299
  ```
341
300
 
342
301
  > 注意:由于平台最小调度间隔为 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 格式