@lark-apaas/coding-steering 0.1.18-dev.ec88bb5 → 0.1.18-dev.fb5e270
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 +1 -1
- package/steering/design-html/skills/charts/SKILL.md +4 -0
- package/steering/design-html/skills/pptx-style-extract/SKILL.md +62 -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 +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 +2123 -264
- package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +325 -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 +504 -160
- 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_asset_judgment_package.py +161 -0
- 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 +63 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +468 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +503 -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 +208 -0
- package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +28 -16
- package/steering/design-html/skills/slide-deck/SKILL.md +15 -20
- package/steering/design-html/skills/slide-deck/scripts/check_local_references.py +179 -0
- package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +10 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +5 -3
- 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
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Regression tests for inherited layout text and model-decided text roles."""
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
import unittest
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
10
|
+
|
|
11
|
+
from draft import draft_layouts, emit_layouts, inherited_text_shapes
|
|
12
|
+
from package import build_layouts_md, split_top_blocks
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def text_shape(part, layer, shape_id, box, text, placeholder):
|
|
16
|
+
return {
|
|
17
|
+
'part': part,
|
|
18
|
+
'layer': layer,
|
|
19
|
+
'id': shape_id,
|
|
20
|
+
'kind': 'sp',
|
|
21
|
+
'name': 'Text Placeholder',
|
|
22
|
+
'ph': placeholder,
|
|
23
|
+
'box': box,
|
|
24
|
+
'text': {
|
|
25
|
+
'bodyPr': {},
|
|
26
|
+
'lstStyle': {
|
|
27
|
+
'lvl1pPr': {
|
|
28
|
+
'sz_px': 48,
|
|
29
|
+
'weight': 600,
|
|
30
|
+
'color': {'resolved': '#C41230'},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
'paragraphs': [
|
|
34
|
+
{
|
|
35
|
+
'runs': [{'text': text}],
|
|
36
|
+
},
|
|
37
|
+
] if text else [],
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TextRoleContractTest(unittest.TestCase):
|
|
43
|
+
def test_empty_non_placeholder_layout_shape_is_not_a_text_slot(self):
|
|
44
|
+
layout_part = 'ppt/slideLayouts/slideLayout2.xml'
|
|
45
|
+
decorative_shape = text_shape(
|
|
46
|
+
layout_part,
|
|
47
|
+
'layout',
|
|
48
|
+
'9',
|
|
49
|
+
{'x': 0, 'y': 0, 'w': 1920, 'h': 24},
|
|
50
|
+
'',
|
|
51
|
+
None,
|
|
52
|
+
)
|
|
53
|
+
decorative_shape['name'] = 'Decorative bar'
|
|
54
|
+
|
|
55
|
+
self.assertEqual(inherited_text_shapes([decorative_shape], []), [])
|
|
56
|
+
|
|
57
|
+
def test_empty_slide_inherits_text_slot_and_css_from_its_layout(self):
|
|
58
|
+
layout_part = 'ppt/slideLayouts/slideLayout2.xml'
|
|
59
|
+
slide_part = 'ppt/slides/slide1.xml'
|
|
60
|
+
shapes = [
|
|
61
|
+
text_shape(
|
|
62
|
+
layout_part,
|
|
63
|
+
'layout',
|
|
64
|
+
'10',
|
|
65
|
+
{'x': 120, 'y': 80, 'w': 840, 'h': 120},
|
|
66
|
+
'Example heading',
|
|
67
|
+
{'type': 'body', 'idx': '10'},
|
|
68
|
+
),
|
|
69
|
+
text_shape(
|
|
70
|
+
slide_part,
|
|
71
|
+
'slide',
|
|
72
|
+
'2',
|
|
73
|
+
None,
|
|
74
|
+
'',
|
|
75
|
+
{'type': 'body', 'idx': '10'},
|
|
76
|
+
),
|
|
77
|
+
]
|
|
78
|
+
data = {
|
|
79
|
+
'canvas': {'px': [1920, 1080]},
|
|
80
|
+
'form_hint': {'form': 0},
|
|
81
|
+
'slides': [
|
|
82
|
+
{
|
|
83
|
+
'part': slide_part,
|
|
84
|
+
'layout': layout_part,
|
|
85
|
+
'background': None,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
'background_composites': {},
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
92
|
+
os.makedirs(os.path.join(output_dir, 'ref'))
|
|
93
|
+
with open(
|
|
94
|
+
os.path.join(output_dir, 'ref', 'shapes.json'),
|
|
95
|
+
'w',
|
|
96
|
+
encoding='utf-8',
|
|
97
|
+
) as stream:
|
|
98
|
+
json.dump({'shapes': shapes}, stream)
|
|
99
|
+
archetypes, _, _ = draft_layouts(data, output_dir)
|
|
100
|
+
|
|
101
|
+
self.assertEqual(len(archetypes), 1)
|
|
102
|
+
self.assertEqual(len(archetypes[0]['slots']), 1)
|
|
103
|
+
slot = archetypes[0]['slots'][0]
|
|
104
|
+
self.assertEqual(slot['box'], [120, 80, 840, 120])
|
|
105
|
+
self.assertEqual(slot['txt'], 'Example heading')
|
|
106
|
+
self.assertEqual(slot['role'], 'body')
|
|
107
|
+
self.assertEqual(slot['type'], 'body')
|
|
108
|
+
self.assertTrue(slot['_needs_role'])
|
|
109
|
+
self.assertIn('font-size: 48px', slot['css'])
|
|
110
|
+
self.assertIn('font-weight: 600', slot['css'])
|
|
111
|
+
self.assertIn('color: #C41230', slot['css'])
|
|
112
|
+
|
|
113
|
+
def test_text_role_judgement_changes_semantics_without_dropping_slot(self):
|
|
114
|
+
archetype = {
|
|
115
|
+
'name': 'layout-1',
|
|
116
|
+
'zh': None,
|
|
117
|
+
'role': 'content',
|
|
118
|
+
'bg': None,
|
|
119
|
+
'slots': [
|
|
120
|
+
{
|
|
121
|
+
'role': 'body',
|
|
122
|
+
'type': 'body',
|
|
123
|
+
'box': [120, 80, 840, 120],
|
|
124
|
+
'sz': 48,
|
|
125
|
+
'txt': 'Example heading',
|
|
126
|
+
'css': 'font-size: 48px; color: #C41230',
|
|
127
|
+
'_needs_role': True,
|
|
128
|
+
'_source_layer': 'layout',
|
|
129
|
+
'_placeholder': 'body/10',
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
'decor': [],
|
|
133
|
+
'pages': [1],
|
|
134
|
+
'rep': 1,
|
|
135
|
+
'pic_n': 0,
|
|
136
|
+
'confidence': 'low',
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
140
|
+
emit_layouts([archetype], output_dir)
|
|
141
|
+
path = os.path.join(output_dir, 'layouts.yaml')
|
|
142
|
+
with open(path, encoding='utf-8') as stream:
|
|
143
|
+
draft = stream.read()
|
|
144
|
+
|
|
145
|
+
self.assertIn('text_roles:', draft)
|
|
146
|
+
self.assertIn(
|
|
147
|
+
'layout-1-text-1: TODO文本角色',
|
|
148
|
+
draft,
|
|
149
|
+
)
|
|
150
|
+
self.assertEqual(draft.count('box: [120, 80, 840, 120]'), 1)
|
|
151
|
+
|
|
152
|
+
decided = draft.replace(
|
|
153
|
+
'layout-1-text-1: TODO文本角色',
|
|
154
|
+
'layout-1-text-1: title',
|
|
155
|
+
)
|
|
156
|
+
layouts_md = build_layouts_md(split_top_blocks(decided), (1920, 1080))
|
|
157
|
+
|
|
158
|
+
self.assertNotIn('text_roles:', layouts_md)
|
|
159
|
+
self.assertEqual(layouts_md.count('box: [120, 80, 840, 120]'), 1)
|
|
160
|
+
self.assertIn(
|
|
161
|
+
'role: title, box: [120, 80, 840, 120], type: title',
|
|
162
|
+
layouts_md,
|
|
163
|
+
)
|
|
164
|
+
self.assertIn('css: "font-size: 48px; color: #C41230"', layouts_md)
|
|
165
|
+
|
|
166
|
+
def test_template_layout_shape_without_ph_key_does_not_crash(self):
|
|
167
|
+
# form=3 模板里,版式层可能有「带 box、带文字、但没有 ph 键」的普通形状
|
|
168
|
+
# (非占位符的文本/装饰)。layouts_from_template 的入口筛选是
|
|
169
|
+
# `s.get('ph') or shape_text(s)`——有文字就放进来,随后按 ph 判类型时若用
|
|
170
|
+
# s['ph'] 直接下标就会 KeyError: 'ph',整份抽取在草案阶段崩掉(EXTRACT_PARTIAL)。
|
|
171
|
+
layout_part = 'ppt/slideLayouts/slideLayout1.xml'
|
|
172
|
+
shape = {
|
|
173
|
+
'part': layout_part,
|
|
174
|
+
'layer': 'layout',
|
|
175
|
+
'id': '7',
|
|
176
|
+
'kind': 'sp',
|
|
177
|
+
'name': '页脚文字',
|
|
178
|
+
# 关键:没有 'ph' 键
|
|
179
|
+
'box': {'x': 100, 'y': 980, 'w': 800, 'h': 60},
|
|
180
|
+
'text': {
|
|
181
|
+
'bodyPr': {},
|
|
182
|
+
'lstStyle': {'lvl1pPr': {'sz_px': 20}},
|
|
183
|
+
'paragraphs': [{'runs': [{'text': '内部资料'}]}],
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
data = {
|
|
187
|
+
'canvas': {'px': [1920, 1080]},
|
|
188
|
+
'form_hint': {'form': 3},
|
|
189
|
+
'layouts': [{'part': layout_part}],
|
|
190
|
+
'slides': [{'part': 'ppt/slides/slide1.xml', 'layout': layout_part,
|
|
191
|
+
'background': None}],
|
|
192
|
+
'background_composites': {},
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
196
|
+
os.makedirs(os.path.join(output_dir, 'ref'))
|
|
197
|
+
with open(os.path.join(output_dir, 'ref', 'shapes.json'), 'w',
|
|
198
|
+
encoding='utf-8') as stream:
|
|
199
|
+
json.dump({'shapes': [shape]}, stream)
|
|
200
|
+
# 修复前这里抛 KeyError: 'ph'(draft.py 用 s['ph'] 直接下标),
|
|
201
|
+
# 抽取在草案阶段崩掉、退成 EXTRACT_PARTIAL。修复后应正常返回。
|
|
202
|
+
archetypes, pages, leftover = draft_layouts(data, output_dir)
|
|
203
|
+
|
|
204
|
+
self.assertIsInstance(archetypes, list)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
if __name__ == '__main__':
|
|
208
|
+
unittest.main()
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
- 消费方直接读 design.md,靠目录约定找 sidecar 与资产;frontmatter `path` 是唯一文件引用点。
|
|
18
18
|
- manifest(`manifest.json`,识别靠内部 `schemaVersion` 字面量不靠文件名)服务存储、索引、校验和迁移;消费模型不需要读它。
|
|
19
|
-
- `ref/`
|
|
19
|
+
- `ref/` 只放 `audit.yaml`(数值出处的人工复核记录,几 KB)。频次原表、聚类原始数据、`extract.json`、重建图、logo 候选图**留在抽取工作目录,不进交付包**——原设计是「下发时链路剥离」,但链路上没有环节真的做剥离,审计材料会连带进消费上下文并占掉包体的大头。
|
|
20
20
|
|
|
21
21
|
## 1. design.md frontmatter 新增键
|
|
22
22
|
|
|
@@ -53,10 +53,10 @@ canvas: 1920x1080 # layouts.md 首键;px = round(EMU / sldSz_cx * 1920)
|
|
|
53
53
|
themes: [dark, light]
|
|
54
54
|
default-theme: dark # 双主题包必填(V2-13)
|
|
55
55
|
colors: # v1 单层扁平,主题进 token 名
|
|
56
|
-
dark-surface: "#
|
|
57
|
-
dark-on-surface: "#
|
|
58
|
-
light-surface: "#
|
|
59
|
-
primary: "#
|
|
56
|
+
dark-surface: "#RRGGBB"
|
|
57
|
+
dark-on-surface: "#RRGGBB"
|
|
58
|
+
light-surface: "#RRGGBB"
|
|
59
|
+
primary: "#RRGGBB" # 共用色不加前缀
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
- token 前缀约定(`dark-X`/`light-X` = 主题专属,无前缀 = 共用)在 `## Usage` 里向消费者写一句;主题机制溯源(clrMap 反转等)落 `ref/audit.yaml`。
|
|
@@ -65,7 +65,7 @@ colors: # v1 单层扁平,主题进 token 名
|
|
|
65
65
|
|
|
66
66
|
### 1.3 排除色
|
|
67
67
|
|
|
68
|
-
- **排除色进 `## Hard Rules`
|
|
68
|
+
- **排除色进 `## Hard Rules` 带证据计数**,正向给替代(如「`<hex>` 为编辑器参考线色(出现 <N> 次),非设计色」)。频次原表与 color-confidence 证据进 `ref/`。
|
|
69
69
|
- 排除色断言必须以**解析后频次**为准,`styleRef` 主题兜底引用(不渲染)与真实设计用色分开。
|
|
70
70
|
|
|
71
71
|
## 2. `assets` 段
|
|
@@ -84,7 +84,7 @@ assets:
|
|
|
84
84
|
kind: background
|
|
85
85
|
role: cover # cover | content | section | closing | accent
|
|
86
86
|
theme: dark
|
|
87
|
-
recipe: "linear-gradient(
|
|
87
|
+
recipe: "linear-gradient(<angle>, <color> 0%, <color> 100%)" # 可选:CSS 重绘配方
|
|
88
88
|
bg-content-solid: # 纯色背景:无 path/url,引 colors token
|
|
89
89
|
kind: background
|
|
90
90
|
role: content
|
|
@@ -99,17 +99,18 @@ assets:
|
|
|
99
99
|
- 方案甲·包内(抽取产物默认形态):大图保留原图 + 压缩图(`<name>@full.<ext>` / `<name>.<ext>`,`path` 指压缩图、`full` 指原图),消费侧优先用压缩图;压缩图 >500KB WARN、包内总量 >20MB FAIL。
|
|
100
100
|
- 方案乙·平台云盘(入库后目标形态):条目用 `url`,消费时按云盘图片处理参数取压缩版;包内不落二进制,体积约束不适用。入库时由后端把 `path`/`full` 重写为 `url`(重写版仍须过 V2-1/V2-12)。
|
|
101
101
|
- `url` 必须 http(s) 持久地址,禁 24h TTL 签名 URL。
|
|
102
|
-
-
|
|
102
|
+
- 图片按用途三分:整幅替换底图的满屏主视觉(含封面艺术图)属**背景族**,照收;服务于具体内容的图表/截图/产品说明图是**内容图,不进包**;既非 logo、又不服务内容的纹理/装饰插画/色块/几何点缀是**装饰图,标 `texture`**。`logo` 特指这份 deck 自己的品牌标志——logo 墙里的第三方 logo 算内容图。被遮挡/无用资产不进包;抽不出不编造(记 `gaps`,logo 候选图存 `ref/logo-candidates/`)。
|
|
103
103
|
|
|
104
104
|
## 2.5 `## Usage` 章节(正文必产,紧随 Overview)
|
|
105
105
|
|
|
106
106
|
design.md 是消费模型的操作文档,不是抽取记录。`## Usage` 承载三件事,全部**可执行**(具体文件、具体坐标、具体顺序):
|
|
107
107
|
|
|
108
|
-
1.
|
|
108
|
+
1. **消费步骤**:① 画布取 `layouts.md` 的 `canvas`;② 从页型清单选 archetype,按其 flow / slots / decor / background 原样落版;③ `design.md` frontmatter 的 colors / typography / spacing / rounded / components 作为全局 token,局部 CSS 优先;④ 包内资产复制到项目相对目录后引用,字体使用完整 fallback 栈且不在运行时安装;⑤ 双主题包写明默认主题与 token 前缀切换法。
|
|
109
109
|
2. **资产用法表**:每个资产一行——id、文件路径、用在哪类页、怎么摆(logo 给坐标,背景给首选序——如「封面首选 cover-art,无主视觉需求用 cover-dark」)。
|
|
110
|
-
3.
|
|
110
|
+
3. **强调色族纪律**:强调色族以 `colors` 段和 layout slot CSS 为主。必要时可使用其他颜色,但新增颜色须与模板整体的色相、明度和饱和度关系协调,且不能形成与模板主色竞争的第二强调色。中性色、低彩度辅助色或局部语义色可表达正负、风险、警告、状态、图表序列,但须保持辅助层级;新色不得通过高饱和、高对比、大面积、跨页重复,或用于标题、关键数字、图表主序列、卡片底色、渐变来获得主视觉权重。
|
|
111
|
+
4. **交付检查**:逐页确认色板、字体、版式、背景、资产和 Hard Rules 均来自本包,并检查资源加载、内容溢出与画幅裁切。
|
|
111
112
|
|
|
112
|
-
**Hard Rules 必须包含对应的正向硬规则**(有资产的包):每页放 logo(位置+文件);封面底图必用 cover 资产;版式从 layouts.md
|
|
113
|
+
**Hard Rules 必须包含对应的正向硬规则**(有资产的包):每页放 logo(位置+文件);封面底图必用 cover 资产;版式从 layouts.md 取;以 colors / layout slot CSS 为强调色基准,新增颜色与整体色板协调并保持辅助层级。禁止句只用于无法正向表达的红线,且同句给替代。
|
|
113
114
|
|
|
114
115
|
## 3. `layouts` 段(默认 sidecar)
|
|
115
116
|
|
|
@@ -123,13 +124,21 @@ layouts:
|
|
|
123
124
|
themes: [dark, light] # 深浅孪生合并
|
|
124
125
|
background: {dark: bg-cover-dark, light: bg-cover-light}
|
|
125
126
|
slots:
|
|
126
|
-
- {role: title, box: [
|
|
127
|
-
- {role: logo, box: [
|
|
127
|
+
- {role: title, box: [<x>, <y>, <w>, <h>], type: title, css: "<CSS 声明串>"}
|
|
128
|
+
- {role: logo, box: [<x>, <y>, <w>, <h>], asset: {dark: logo-on-dark, light: logo-on-light}}
|
|
129
|
+
decor:
|
|
130
|
+
- {box: [<x>, <y>, <w>, <h>], geom: ellipse, css: "<CSS 声明串>"}
|
|
128
131
|
confidence: high
|
|
129
132
|
```
|
|
130
133
|
|
|
131
134
|
- **`background` 三形态**:`<asset-id>` / `{<theme>: <asset-id>}` / `{color: <colors-token>}`(`color` 是保留键,主题名禁止叫 color)。`asset` 两形态:`<asset-id>` / `{<theme>: <asset-id>}`。
|
|
132
135
|
- **背景安全扩展**:有真实背景图的 archetype 建议写 `text_safe: [x,y,w,h]`、`avoid: [{box: [x,y,w,h], reason: "..."}]`、`pairing_rule: "..."`。这些是消费约束,不参与封闭枚举;用于避免标题、正文、图表、卡片、表格、时间线及其容器外接矩形覆盖背景视觉主体、强光斑或深色透明区;透明容器也不能跨进禁放区。
|
|
136
|
+
- **流式页型**:内容长度会变化的内容页可用 `flow.regions` 表达纵向区带。`stack` 表达单列顺序,`grid` 表达并列列组,`free` 中的 item 必须带 `box`,用于 logo、页码、页眉和页脚等固定锚点。并列卡片可在 `grid.items` 中使用一层 `{role: group, css, gap, items}`:group 的 `css` 是卡片容器样式,内部 `items` 按顺序排布;不继续嵌套 group。区带可带自己的 `margin: [左, 右]`,覆盖 `flow` 整块的 `margin`(居中卡片组和贴左标题横向范围本就不同);不带则继承整块 `margin`。纵向位置与留白由消费模型结合实际内容决定,不把样张的 `y` 坐标当作流式硬约束。
|
|
137
|
+
- **`decor`(可选)**:这一页无文字的图形骨架——图标托底的圆、卡片、分隔线。每条 `{box, geom, css}`:`box` 定位,`css` 是可直接写进 style 的声明串,`geom` 取源形状的 prst(`ellipse` 另加 `border-radius: 50%`)。圆角以每条 `css` 为准,没有 `border-radius` 就按 `0`;不得因 `geom: roundRect` 自行补圆角,因为 OOXML 的 roundRect 可以有零圆角调节点。层级在背景之上、`slots` 之下;带 `asset` 的槽落在 decor 之上是版式本意,不算重叠。
|
|
138
|
+
- **slot 样式契约**:`box` 只承载 `[x,y,w,h]` 几何;可渲染属性统一放进 `css`,并可直接写入 HTML `style`。PPTX `bodyPr.insets_px` 转成 `box-sizing: border-box; padding: ...`,字号/字重/颜色/水平与垂直对齐/行高/字距/旋转分别转成标准 CSS。禁止在 slot 中输出 `size` / `weight` / `color` / `align` / `valign` / `insets_px` 等旧字段。
|
|
139
|
+
- **文本角色判断**:脚本把实例页及其引用版式中的现有文本槽、几何和 CSS 完整写入草案;`text_roles` 只供模型把这些槽判断为 `title | subtitle | header | footer | body`,不控制槽位去留。判断不清时用 `body`,不归纳模板中不存在的标题、页眉或页脚。
|
|
140
|
+
- **标题结构**:存在合适的模板页型时,沿用其标题层级和局部 CSS,只渲染该页型已有的文字槽;背景中已经可见的固定标题不重复创建文本,该页型没有副标题槽时不新增副标题。没有合适参考时由模型按模板整体视觉判断。
|
|
141
|
+
- **圆角作用域**:`rounded` 只允许表达全档共同的单一圆角档位;零圆角与非零圆角混用、或存在多个非零档位时不输出该全局 token。此时每个 `role: container` / `decor` 的 `css` 是唯一事实源,逐项原样消费,不得归并或推断。
|
|
133
142
|
- **`type` 封闭枚举**:`title | subtitle | body | pic | table | chart | media | slide-number | footer`。大数字/序号走 `type: title`,语义由 `role`(如 `big-number`)承担。
|
|
134
143
|
- **`slots.*.role` 开放不校验**(语义槽位):优先复用已知词表(OOXML ST_SlideLayoutType / Slidev 20 布局 / Google PredefinedLayout,如 big-number、caption、main-point),确无对应再自造。
|
|
135
144
|
- archetype ≤15(内联降级形态 ≤11);深浅孪生合并为一条;版式溯源/母版取舍进 `ref/`。
|
|
@@ -139,14 +148,14 @@ layouts:
|
|
|
139
148
|
|
|
140
149
|
```yaml
|
|
141
150
|
safe-area: # 开放命名 map,可多套边距体系
|
|
142
|
-
content: {top:
|
|
143
|
-
editorial: {left:
|
|
151
|
+
content: {top: <px>, right: <px>, bottom: <px>, left: <px>, applies-to: [content, quote]}
|
|
152
|
+
editorial: {left: <px>, right: <px>, applies-to: [cover, section, closing]}
|
|
144
153
|
confidence: medium
|
|
145
154
|
```
|
|
146
155
|
|
|
147
156
|
冲突裁决:`slots.box` 是实例真值,`safe-area` 是归纳框架,**以 slots.box 为准**。
|
|
148
157
|
|
|
149
|
-
## 5. check_v2 校验(
|
|
158
|
+
## 5. check_v2 校验(19 行:V2-1..V2-16 + V2-R5/R6/R7)
|
|
150
159
|
|
|
151
160
|
check_v1 全部规则原样生效。扫描范围 = 包目录,V2-1/V2-2 跨 design.md + layouts.md 求并集。
|
|
152
161
|
|
|
@@ -165,6 +174,9 @@ check_v1 全部规则原样生效。扫描范围 = 包目录,V2-1/V2-2 跨 des
|
|
|
165
174
|
| V2-11 | 键名命中 YAML 1.1 布尔字面量 | FAIL |
|
|
166
175
|
| V2-12 | `path`/`url`/`color` 恰好存在一个;`color` 仅 background 允许;`full` 仅可伴随 `path` | FAIL |
|
|
167
176
|
| V2-13 | 多主题(`themes` 长度 >1)缺 `default-theme`(只看 design.md frontmatter——冲突以 design.md 为准) | WARN |
|
|
177
|
+
| V2-14 | 版式的 `flow` 与 `slots` 互斥(同时出现 = FAIL);`flow.regions[].kind` 在 `grid`/`stack`/`free` 内,`grid` 必带 `cols` | FAIL |
|
|
178
|
+
| V2-15 | 同段字段自洽:`backgrounds.*` 的 `text_safe` 不得与任一 `avoid` 相交;两者形态须为 `[x, y, w, h]` 四个数且 w/h 为正 | FAIL |
|
|
179
|
+
| V2-16 | slot/flow item 的渲染样式只通过 `css` 承载;出现 `size/weight/color/align/valign/insets_px` 旧键 | FAIL |
|
|
168
180
|
| V2-R5 | sidecar frontmatter 重复 design.md 已有顶层键(`layouts` 载荷键与 `canvas` 除外——canvas 的家就在 sidecar) | WARN |
|
|
169
181
|
| V2-R6 | assets 条目出现审计字段(boxes/aspect/mark/confidence)或 design.md 顶层出现 canvas/canvas-source/theme-mechanism/color-confidence——应移 `ref/audit.yaml` / layouts.md | WARN |
|
|
170
182
|
| V2-R7 | 有 layouts sidecar 指针但正文未出现 `layouts.md` 字样(弱指针,消费者到不了版式数据) | WARN |
|
|
@@ -17,14 +17,6 @@ metadata:
|
|
|
17
17
|
|
|
18
18
|
每张幻灯片既是版式设计的练习,也是文案写作的练习。动手前先写大纲;好的大纲本身就是一次讲故事和叙事结构的练习。
|
|
19
19
|
|
|
20
|
-
## PPTX/POTX 模板附件前置步骤
|
|
21
|
-
|
|
22
|
-
如果用户上传了 `.pptx` 或 `.potx`,并要求制作/生成/改做一个 PPT、演示文稿、slides、deck,先判断附件是否是模板或视觉参考。
|
|
23
|
-
|
|
24
|
-
- 命中“按这个模板 / 照附件风格 / 参考这个 PPT / 保持同款视觉 / 基于这个模板”等意图时,先调用 `pptx-style-extract` skill,读取产出的 `design.md`、`layouts.md` 和 `assets/`,再开始写 deck。
|
|
25
|
-
- 只总结、翻译、提取内容、审阅已有 PPTX 时,不需要抽取风格。
|
|
26
|
-
- 不要用 `SummarizeAttachmentOrFile` 的 Markdown/文本摘要替代风格抽取;文本摘要不包含母版、色板、字体、版式坐标和素材角色。
|
|
27
|
-
|
|
28
20
|
## 动手前先问
|
|
29
21
|
|
|
30
22
|
- 如果用户没有说明想要的视觉风格,也没有提供 design system,就用提问工具(ask_user_question)**主动询问**。绝不要直接给出一个通用设计!
|
|
@@ -45,18 +37,6 @@ metadata:
|
|
|
45
37
|
|
|
46
38
|
deck-stage 组件会对每个 slotted 子元素做绝对定位——**绝不**在幻灯片 `<section>` 元素上自行设置 position/inset/width/height。
|
|
47
39
|
|
|
48
|
-
### 使用 PPTX 风格包
|
|
49
|
-
|
|
50
|
-
如果前一步产出了 `pptx-style-extract` 风格包,必须把它当作本 deck 的设计系统:
|
|
51
|
-
|
|
52
|
-
- 先读 `design.md` 的 Usage / Hard Rules / colors / typography / components / assets / safe-area。
|
|
53
|
-
- 再读 `layouts.md`,用其中的 layout archetype 和 slots 坐标生成页面;坐标单位已经是 px@1920,不需要 EMU 或 pt 换算。
|
|
54
|
-
- 消费并落实 colors / typography / spacing / rounded / safe-area 等样式 token;可以用 CSS variables、类名或内联样式承载,但最终页面必须看得出这些 token 被系统性使用,而不是另起一套视觉系统。
|
|
55
|
-
- 背景和版式必须成对消费:选择某个 layout archetype 时,同时采用它声明的 `background`、`text_safe`、`avoid` / `pairing_rule`。标题、正文、图表、表格、卡片、时间线及其容器外接矩形不得进入背景禁放区;如果内容与背景主体冲突,换页型、拆页或缩小内容区域,不要只换背景色或把容器铺到禁放区。
|
|
56
|
-
- 把包内 `assets/` 复制到项目内相对目录并引用复制后的路径;最终 HTML 禁止引用 `/tmp` 或本机绝对路径。
|
|
57
|
-
- 字体使用 design.md 的完整 font stack 和 fallback,不现场安装字体。
|
|
58
|
-
- 自检时除了常规 deck preflight,还要确认色板、字体、版式坐标、资产和 Hard Rules 均来自该风格包。
|
|
59
|
-
|
|
60
40
|
### 把幻灯片内容写成静态 HTML,而不是 React
|
|
61
41
|
|
|
62
42
|
幻灯片内容应写成静态 HTML,而非 React 或脚本生成的 DOM。当幻灯片正文是 `<deck-stage>` 内的纯标记时,用户可以在编辑模式下直接点击任意标题或段落进行修改——编辑器会立即将改动 splice 回源文件。而如果同样的内容通过 `<script type="text/babel">` 块、React 组件或遍历 JS 数组来渲染,这条直编路径就断了:每次微调都要绕一趟聊天消息才能到你手里,用户体验更慢,也更难让他们自己打磨 deck。因此,凡是静态页面能表达的——文本、布局、背景、图片——都直接在 HTML 里写字面元素并用 CSS 设置样式。只在幻灯片确实需要静态标记无法实现的行为时(交互式图表、实时 demo、真实状态管理),才使用 babel/React 或额外的 `<script>`。同样的渲染结果,静态 HTML 版本**始终优先于**动态版本,因为静态版本可被直接编辑。Tweaks 面板(`tweaks-panel.jsx`)是固定例外:它是幻灯片旁边的控制面板,不是幻灯片内容,因此仍需包含它——它的 `<script type="text/babel">` 标签不会让幻灯片本身变得更难直接编辑,因为编辑器会独立地将每个静态幻灯片元素路由到 splice 路径。
|
|
@@ -144,6 +124,21 @@ deck-stage 组件会对每个 slotted 子元素做绝对定位——**绝不**
|
|
|
144
124
|
5. **把这套 token 当成每页的内容预算**:在上述数值下,一页正文区大约容纳 14 行正文、或 6 个两行 bullet——在 scratchpad 排内容时就按预算裁剪,而不是写完再看塞不塞得下。装不下的处置顺序是**拆页 > 删内容 > 换更省空间的版式**;缩小字号是最后手段,且绝不越过 24px 下限——靠缩字塞进去的页,只是把溢出换成了后排看不清。反过来,内容远少于预算的页按「视觉平衡」的出路增密或合并,而不是放大字号去撑面积。
|
|
145
125
|
6. 构建幻灯片,牢记每张幻灯片既是设计练习也是文案练习。在版式、文字内容和语调方面给予每张幻灯片应有的关注。遵循上述原则,确保每张幻灯片能独立成立;一个只看这一页的人,应当无需其他上下文就能理解其高层含义。
|
|
146
126
|
|
|
127
|
+
## 提交前资源完整性门禁
|
|
128
|
+
|
|
129
|
+
最终一次写入 HTML/CSS 后、调用 `run_commit` 前,必须从项目根目录运行:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
python3 <本skill目录>/scripts/check_local_references.py index.html
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
把 `<本skill目录>` 换成本 skill 的实际所在目录。脚本从最终 `index.html` 出发,递归检查 HTML/CSS 引用的每个项目内脚本、样式、图片、字体等文件是否真实存在;外部 URL 不做离线探测。
|
|
136
|
+
|
|
137
|
+
- 只有输出 `RESOURCE_CHECK: PASS` 才能提交。
|
|
138
|
+
- 输出 `RESOURCE_CHECK: FAIL` 时,先重新复制或修正列出的项目内文件,再原样重跑;不得删除引用来掩盖仍在使用的资源。
|
|
139
|
+
- 该检查针对最终工作区状态。较早执行过复制命令或检查,不能证明最终提交完整。
|
|
140
|
+
- `run_commit` 的静态检查跳过参数不能替代本门禁,也不能用来绕过失败;提交时必须包含检查通过所依赖的全部项目内文件。
|
|
141
|
+
|
|
147
142
|
## 验证要点
|
|
148
143
|
|
|
149
144
|
审阅时,用幻灯片构图规则——而非网页布局直觉——来检查版面。底部留白是不是缺陷,用「留白 ≠ 空洞」的归属判据:内容自身完整、下方是无边框的整块呼吸空间,这是正确的幻灯片构图——不要出于网页直觉把 `flex-start` 改成 `center`;空白被元素边界圈占的,是被动空洞,按「视觉平衡」的出路修。
|
|
@@ -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())
|
|
@@ -395,6 +395,8 @@ async createArticle(@Req() req: Request, @Body() dto: CreateArticleDto) {
|
|
|
395
395
|
| 语音识别、音频转文字、STT | AI语音转文字 |
|
|
396
396
|
| 插件实例、PluginInstance、Capability | 通用插件调用 |
|
|
397
397
|
|
|
398
|
+
> ⚠️ **语义检索 / 相似推荐场景例外**:若需求是对**应用自有数据**做语义检索、向量检索、相似推荐、相关推荐、按内容找相似,**不要**用上表的「AI搜索总结 / AI文本转JSON / AI智能生文」等通用插件替代,必须召回 `/semantic-search`。上表的 AI 搜索/文本插件用于公网搜索、文本生成、结构化抽取,**不具备对自有数据库的向量检索能力**。
|
|
399
|
+
|
|
398
400
|
### 关键区分:多维表格 vs 数据库表
|
|
399
401
|
|
|
400
402
|
| 场景 | 判断 | 操作 |
|
|
@@ -497,6 +499,14 @@ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBac
|
|
|
497
499
|
|
|
498
500
|
禁止未调用 Skill 直接编写表单/图表/表格代码。
|
|
499
501
|
|
|
502
|
+
### 检索 Skill 召回规则(强制执行)
|
|
503
|
+
|
|
504
|
+
| 场景 | Skill | 说明 |
|
|
505
|
+
|------|-------|------|
|
|
506
|
+
| 语义检索、向量检索、相似推荐、相关推荐、相似内容、按内容找相似、猜你喜欢 | `/semantic-search` | 对应用自有数据库的数据做向量检索 / 相似召回 |
|
|
507
|
+
|
|
508
|
+
禁止未调用 Skill 直接用「AI搜索总结 / AI文本转JSON / AI智能生文」等通用插件实现对自有数据的语义检索。
|
|
509
|
+
|
|
500
510
|
### 组件使用规范
|
|
501
511
|
|
|
502
512
|
- 优先使用 `client/src/components` 下已有组件(Card/Button/Badge 等)
|
|
@@ -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
|
|
|
@@ -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 %}
|