@lark-apaas/coding-steering 0.1.18-dev.5c16901 → 0.1.18-dev.61b3ece
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 +3 -3
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +37 -11
- package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +18 -1
- package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +1 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +90 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +29 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +40 -0
- package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +2 -2
- 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
package/package.json
CHANGED
|
@@ -7,6 +7,8 @@ metadata:
|
|
|
7
7
|
en-US: Charts
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
{% raw %}
|
|
11
|
+
|
|
10
12
|
# 图表
|
|
11
13
|
|
|
12
14
|
你是用 ECharts 呈现信息的数据叙事设计者。你的图表会出现在创意 HTML 产物中,例如仪表盘、幻灯片、设计探索。ECharts 是你的媒介,不是目标;你的工作是让数据故事一眼可读,而不是堆配置项。一个图表只表达一个主要信息。
|
|
@@ -202,3 +204,5 @@ Object.assign(window, { EChart });
|
|
|
202
204
|
| Line 连接未按时间排序的点 | Bar 或 Scatter |
|
|
203
205
|
| markPoint 重复(统计极值 = 业务事件) | 仅保留业务注释 |
|
|
204
206
|
| 用 Stacked Bar 表示 Gantt | 使用带 `renderItem` 的 `custom` series |
|
|
207
|
+
|
|
208
|
+
{% endraw %}
|
|
@@ -80,14 +80,14 @@ mkdir -p tmp/pptx-style-extract/<unique-id> && PYTHONDONTWRITEBYTECODE=1 python3
|
|
|
80
80
|
|
|
81
81
|
判断口径:
|
|
82
82
|
|
|
83
|
-
-
|
|
84
|
-
- **logo
|
|
83
|
+
- **图片按用途三分**——一张图先问它服务于什么:整幅替换底图的艺术图/摄影图属**背景族**(首页那张记 `role: cover`);服务于具体内容的图表、截图、产品说明图是**内容图,不进包**;剩下既不是 logo、也不服务于内容的(纹理、装饰插画、色块、几何点缀)都是**装饰图,标 `texture` 进 assets**。满屏图默认走背景,不必逐张看图确认。
|
|
84
|
+
- **logo 特指这份 deck 自己的品牌标志**——看图确认是本 PPT 的品牌文字或标志图形才留 `logo`;一页若是 logo 墙(客户/合作方 logo 罗列),那些是内容图、不当风格 logo。宁缺勿错:拿不准就把该条从 `manifest.yaml` 删掉,在 `gaps` 写一句,候选图留在 `media-out/`。
|
|
85
85
|
- **anchors 是事实不是结论**——草案里每条只报测到的数(覆盖率、计数、占比),没有「这套风格的特征是 X」这种断言。这一段在 design.md 里读起来像总纲,消费端会照它建全局样式,所以脚本不敢替你下结论:一条 1/8 覆盖率的元素被描述成「跨页不动」,消费端就会每页都摆它。看过重建图后,把真正是这套模板特征的那几条改写成设计要点,不是特征的直接删掉。
|
|
86
86
|
- **数值只改名不改值**——草案里的坐标/色值/字号来自普查;确需推导值(CJK 行高转译、投影尺度上抬)在 manifest 写 `derived:` 声明理由,机检认声明。
|
|
87
87
|
- **页型命名**——模板自带版式名时草案已填好;只有靠样张聚类的模板(`names` 里还是 TODO)才需要按 BRIEF 的 slot 原文起中文名。
|
|
88
88
|
- **页型角色**——`roles:` 段有 TODO 时,看 `layout-sheet.png` 上对应的代表页定 `cover|section|content|quote|closing|blank|custom`。草案只把客观事实摆在注释里(代表页页码、页数、文字块数、字号序列、图片数、有无满屏底图),不替你下结论:字号多大算章节页、文字块多少算密集页,每套模板的答案都不一样。
|
|
89
89
|
- **文本角色**——`text_roles:` 段有 TODO 时,结合重建图和该行给出的来源、占位符、样例文字、box、字号与 CSS 判断标题、副标题、页眉、页脚或正文。这里仅改变已有文本槽的语义;所有文本槽、图片、容器与装饰仍由 `layouts:` 保留。标题、页眉或页脚已在背景中可见时,在 Hard Rules 写明对应页型只渲染已有 slots,不重复叠加文字。
|
|
90
|
-
- **资产位置不要写成全局规则**——同一个 logo 常按页型换位换尺寸(封面一个位置一个尺寸、内容页另一个)。位置只存在于 `layouts.md` 各页型的 `slots`,正文里只说「按该页型的 slot
|
|
90
|
+
- **资产位置不要写成全局规则**——同一个 logo 常按页型换位换尺寸(封面一个位置一个尺寸、内容页另一个)。位置只存在于 `layouts.md` 各页型的 `slots`,正文里只说「按该页型的 slot 摆放,没有就不放」。一张图只在 `manifest` 标了 `kind` 还不够:生成侧只摆 `slots` 里带 `asset:` 引用它的那些,没有任何页型引用它就不会被放出来。要它出现,就在对应页型的 `slots` 加一条 `asset:` 引用。
|
|
91
91
|
- **背景和版式要配对**——真实背景不是纯色底。看 `contact-sheet.png` 判断每张背景的视觉主体/禁放区,看 `layout-sheet.png` 判断文字与卡片落点;`background` 草案已按版式直读填好,你只需在 `bg_rules` 里逐张背景补 `text_safe`、`avoid`、`pairing_rule`,避免消费 Agent 把文字、图表、卡片、时间线、标题容器、正文容器或宽透明容器的外接矩形压到金字塔、人物、产品图、强光斑或深色区域上。
|
|
92
92
|
- 双主题只认 BRIEF 的 `themes`;单主题包不写 `theme` 字段。
|
|
93
93
|
|
|
@@ -982,7 +982,11 @@ def slot_style(s):
|
|
|
982
982
|
css_number(insets.get('lIns', 0) or 0),
|
|
983
983
|
))
|
|
984
984
|
if ls.get('sz_px'):
|
|
985
|
-
|
|
985
|
+
# normAutofit 的 fontScale 是模板让大字装进小框的手段——不乘它,消费端拿到的是
|
|
986
|
+
# 未缩放字号,字比框高,渐变裁切会把溢出的底部切成透明。缺省 1.0(无 autofit / 无缩放)。
|
|
987
|
+
scale = body.get('font_scale')
|
|
988
|
+
raw = ls['sz_px'] * scale if scale else ls['sz_px']
|
|
989
|
+
size = round(raw)
|
|
986
990
|
css.append('font-size: %dpx' % size)
|
|
987
991
|
out['_font_size'] = size
|
|
988
992
|
weight = ls.get('weight') or (700 if ls.get('bold') else None)
|
|
@@ -1020,10 +1024,13 @@ def slot_style(s):
|
|
|
1020
1024
|
'l': 'left', 'ctr': 'center', 'r': 'right', 'just': 'justify',
|
|
1021
1025
|
}.get(align, align))
|
|
1022
1026
|
line_spacing = ls.get('lnSpc') or {}
|
|
1027
|
+
# normAutofit 的 lnSpcReduction 与 fontScale 同时把行距压缩,一起缩才装得进原框。
|
|
1028
|
+
reduction = body.get('ln_spc_reduction') or 0
|
|
1023
1029
|
if line_spacing.get('mult'):
|
|
1024
|
-
|
|
1030
|
+
mult = line_spacing['mult'] * 1.2 * (1 - reduction)
|
|
1031
|
+
css.append('line-height: %s' % css_number(mult))
|
|
1025
1032
|
elif line_spacing.get('px'):
|
|
1026
|
-
css.append('line-height: %spx' % css_number(line_spacing['px']))
|
|
1033
|
+
css.append('line-height: %spx' % css_number(line_spacing['px'] * (1 - reduction)))
|
|
1027
1034
|
anchor = body.get('anchor')
|
|
1028
1035
|
if anchor in ('ctr', 'b'):
|
|
1029
1036
|
css += ['display: flex', 'flex-direction: column',
|
|
@@ -1106,7 +1113,7 @@ def layouts_from_template(d, shapes, cW, cH):
|
|
|
1106
1113
|
phs.sort(key=lambda s: ((s['box'].get('y') or 0), (s['box'].get('x') or 0)))
|
|
1107
1114
|
slots, seen_kind = [], set()
|
|
1108
1115
|
for s in phs:
|
|
1109
|
-
t = PH_TO_TYPE.get((s
|
|
1116
|
+
t = PH_TO_TYPE.get((s.get('ph') or {}).get('type'), 'body')
|
|
1110
1117
|
if t in ('slide-number', 'footer') and not shape_text(s):
|
|
1111
1118
|
continue # 空 chrome 占位符不是实际元素
|
|
1112
1119
|
b = s['box']
|
|
@@ -1686,6 +1693,11 @@ def draft_flow(a, facts, canvas):
|
|
|
1686
1693
|
cur.append(items[i + 1])
|
|
1687
1694
|
regions.append(cur)
|
|
1688
1695
|
|
|
1696
|
+
# 整页左右边距 = 所有内容的横向外包络,作为各区带的缺省。
|
|
1697
|
+
lefts = [s['box'][0] for s in items]
|
|
1698
|
+
rights = [s['box'][0] + s['box'][2] for s in items]
|
|
1699
|
+
page_margin = [min(lefts), cW - max(rights)]
|
|
1700
|
+
|
|
1689
1701
|
out = []
|
|
1690
1702
|
for reg in regions:
|
|
1691
1703
|
if not reg:
|
|
@@ -1708,8 +1720,17 @@ def draft_flow(a, facts, canvas):
|
|
|
1708
1720
|
if len(rows) > 1:
|
|
1709
1721
|
row_gap = round(rows[1][0]['box'][1]
|
|
1710
1722
|
- (rows[0][0]['box'][1] + rows[0][0]['box'][3]))
|
|
1711
|
-
|
|
1712
|
-
|
|
1723
|
+
region = {'kind': 'grid', 'cols': cols, 'gap': [max(col_gap, 0), max(row_gap, 0)],
|
|
1724
|
+
'items': rows[0]}
|
|
1725
|
+
# 卡片组的横向范围常和整页不同(标题贴左、卡片居中)。整页边距是所有元素的
|
|
1726
|
+
# 外包络,直接套给居中卡片组会把它拉偏成左对齐。区带范围和整页明显不一致时,
|
|
1727
|
+
# 落这个区带自己的左右边距,消费端把网格放进它再填 1fr。按落盘的整数比较,
|
|
1728
|
+
# 亚像素噪声不触发多余的区带边距。
|
|
1729
|
+
reg_margin = [min(s['box'][0] for s in rows[0]),
|
|
1730
|
+
cW - max(s['box'][0] + s['box'][2] for s in rows[0])]
|
|
1731
|
+
if [int(reg_margin[0]), int(reg_margin[1])] != [int(page_margin[0]), int(page_margin[1])]:
|
|
1732
|
+
region['margin'] = reg_margin
|
|
1733
|
+
out.append(region)
|
|
1713
1734
|
elif len(rows) == len(reg):
|
|
1714
1735
|
# 每行一个元素 = 真的竖着排
|
|
1715
1736
|
inner = 0
|
|
@@ -1724,9 +1745,7 @@ def draft_flow(a, facts, canvas):
|
|
|
1724
1745
|
out.append({'kind': 'free', 'items': fixed})
|
|
1725
1746
|
if len(out) < 2:
|
|
1726
1747
|
return None
|
|
1727
|
-
|
|
1728
|
-
rights = [s['box'][0] + s['box'][2] for s in items]
|
|
1729
|
-
return {'top': items[0]['box'][1], 'margin': [min(lefts), cW - max(rights)],
|
|
1748
|
+
return {'top': items[0]['box'][1], 'margin': page_margin,
|
|
1730
1749
|
'gap': round(cut), 'regions': out}
|
|
1731
1750
|
|
|
1732
1751
|
|
|
@@ -2010,6 +2029,10 @@ def emit_layouts(archetypes, ldir, busy_hints=None, facts=None, recipes=None):
|
|
|
2010
2029
|
L.append(' - kind: grid')
|
|
2011
2030
|
L.append(' cols: %d' % r['cols'])
|
|
2012
2031
|
L.append(' gap: [%d, %d]' % tuple(r['gap']))
|
|
2032
|
+
if r.get('margin'):
|
|
2033
|
+
L.append(' margin: [%d, %d] # 本区带自己的左右边距,'
|
|
2034
|
+
'和整页 margin 不同(居中卡片组不跟标题的左边距)'
|
|
2035
|
+
% tuple(r['margin']))
|
|
2013
2036
|
elif r['kind'] == 'free':
|
|
2014
2037
|
L.append(' - kind: free # 推不出规整结构,按 slots 的坐标摆')
|
|
2015
2038
|
else:
|
|
@@ -2107,12 +2130,15 @@ def emit_body(d, tokens, fonts, roles, assets, archetypes, exceptions, cusage, l
|
|
|
2107
2130
|
% sidecar,
|
|
2108
2131
|
'3. **按页型给的形态落元素** —— 页型给 `flow` 就用流式,给 `slots` 就用绝对,'
|
|
2109
2132
|
'两者只会出现一个。'
|
|
2110
|
-
'**flow**:整块用一个纵向 flex 容器,`top` 是它的起始 y,`margin`
|
|
2133
|
+
'**flow**:整块用一个纵向 flex 容器,`top` 是它的起始 y,`margin` 是整块的左右边距,'
|
|
2111
2134
|
'`gap` 是区带之间的间距;`regions` 从上往下依次排,**每个区带的高度由它自己的'
|
|
2112
2135
|
'内容决定,不要写死高度**——上面的区带内容变多时,下面的自然被推下去,这正是'
|
|
2113
2136
|
'这套表达要解决的事。区带内部:`kind: grid` 用 `grid-template-columns: repeat(cols, 1fr)` '
|
|
2114
2137
|
'配 `gap: [行间距, 列间距]`;`kind: stack` 用纵向 flex 配 `gap`;`kind: free` '
|
|
2115
|
-
'按 item 自带的 `box`
|
|
2138
|
+
'按 item 自带的 `box` 绝对定位。区带自带 `margin: [左, 右]` 时用它的、'
|
|
2139
|
+
'覆盖整块的 `margin`(模板里居中的卡片组和贴左的标题横向范围本就不同);'
|
|
2140
|
+
'没带就用整块的 `margin`。`grid` 在自己这份左右边距里再 `repeat(cols, 1fr)`。'
|
|
2141
|
+
'`grid` 里的 `role: group` 是一张卡片:'
|
|
2116
2142
|
'group 的 `css` 用于外层容器,内部 `items` 按顺序纵向排布并使用 group 的 `gap`。'
|
|
2117
2143
|
'每个 `role: container` 的项是容器,把它的 `css` 逐项原样写进 style,内容放进去;'
|
|
2118
2144
|
'其中没有 `border-radius` 就按 `0`,不得自行补圆角。',
|
|
@@ -519,8 +519,25 @@ def read_txbody(tx, ctx, kind='txBody'):
|
|
|
519
519
|
ins[k] = ctx.units.px(bp.get(k))
|
|
520
520
|
if ins:
|
|
521
521
|
b['insets_px'] = ins
|
|
522
|
-
|
|
522
|
+
na = bp.find('a:normAutofit', NS)
|
|
523
|
+
if na is not None:
|
|
523
524
|
b['autofit'] = 'norm'
|
|
525
|
+
# normAutofit 用 fontScale / lnSpcReduction(单位 1/1000 %)把大字缩进小框——
|
|
526
|
+
# 章节大号数字就靠它让 160px 的字装进 144px 的框。只记 autofit 存在、丢掉
|
|
527
|
+
# fontScale,消费端就拿到未缩放字号 + 原始框高,字比框高,渐变裁切把溢出的
|
|
528
|
+
# 底部切成透明。缺省即 100%(无缩放)。
|
|
529
|
+
fs = na.get('fontScale')
|
|
530
|
+
if fs is not None:
|
|
531
|
+
try:
|
|
532
|
+
b['font_scale'] = round(int(fs) / 100000.0, 4)
|
|
533
|
+
except (TypeError, ValueError):
|
|
534
|
+
pass
|
|
535
|
+
lsr = na.get('lnSpcReduction')
|
|
536
|
+
if lsr is not None:
|
|
537
|
+
try:
|
|
538
|
+
b['ln_spc_reduction'] = round(int(lsr) / 100000.0, 4)
|
|
539
|
+
except (TypeError, ValueError):
|
|
540
|
+
pass
|
|
524
541
|
elif bp.find('a:spAutoFit', NS) is not None:
|
|
525
542
|
b['autofit'] = 'shape'
|
|
526
543
|
if b:
|
package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py
CHANGED
|
@@ -56,6 +56,7 @@ class DesignConsumerContractTest(unittest.TestCase):
|
|
|
56
56
|
self.assertIn('沿用该页型已有的标题层级与局部 `css`', body)
|
|
57
57
|
self.assertIn('背景中已经可见的固定标题不再创建文本', body)
|
|
58
58
|
self.assertIn('没有 `subtitle` 槽就不新增副标题', body)
|
|
59
|
+
self.assertIn('区带自带 `margin: [左, 右]` 时用它的', body)
|
|
59
60
|
|
|
60
61
|
|
|
61
62
|
if __name__ == '__main__':
|
|
@@ -324,6 +324,96 @@ layouts:
|
|
|
324
324
|
['group', 'group', 'group'],
|
|
325
325
|
)
|
|
326
326
|
|
|
327
|
+
def test_centered_card_grid_carries_its_own_symmetric_margin(self):
|
|
328
|
+
# 短标题贴左(x=120..900),三张卡片居中(左右各 360)。整页 margin 取「最左槽 +
|
|
329
|
+
# 最右槽」的外包络 = [120, 360],直接套给居中卡片组会把它拉偏成左对齐(左缝小、
|
|
330
|
+
# 右缝大)。卡片区带应带自己的对称 margin,还原居中。
|
|
331
|
+
archetype = {
|
|
332
|
+
'name': 'layout-1',
|
|
333
|
+
'zh': '标题加三列卡片',
|
|
334
|
+
'role': 'content',
|
|
335
|
+
'bg': None,
|
|
336
|
+
'slots': [
|
|
337
|
+
text_slot('title', [120, 73, 780, 68], '核心能力'),
|
|
338
|
+
],
|
|
339
|
+
'decor': [],
|
|
340
|
+
'pages': [2],
|
|
341
|
+
'rep': 2,
|
|
342
|
+
'pic_n': 0,
|
|
343
|
+
'confidence': 'high',
|
|
344
|
+
}
|
|
345
|
+
for left in (360, 760, 1160):
|
|
346
|
+
archetype['decor'].append({
|
|
347
|
+
'box': [left, 430, 400, 300],
|
|
348
|
+
'geom': 'rect',
|
|
349
|
+
'css': 'background: rgba(255,255,255,0.7)',
|
|
350
|
+
})
|
|
351
|
+
for top in (490, 570):
|
|
352
|
+
archetype['slots'].append(
|
|
353
|
+
text_slot('body', [left + 40, top, 320, 41], '卡片内容'),
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
flow = draft_flow(archetype, {}, (1920, 1080))
|
|
357
|
+
|
|
358
|
+
self.assertIsNotNone(flow)
|
|
359
|
+
self.assertEqual([region['kind'] for region in flow['regions']], ['stack', 'grid'])
|
|
360
|
+
self.assertEqual(flow['margin'], [120, 360])
|
|
361
|
+
grid = flow['regions'][1]
|
|
362
|
+
self.assertEqual(grid.get('margin'), [360, 360])
|
|
363
|
+
|
|
364
|
+
archetype['flow'] = flow
|
|
365
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
366
|
+
emit_layouts([archetype], output_dir)
|
|
367
|
+
with open(os.path.join(output_dir, 'layouts.yaml'), encoding='utf-8') as stream:
|
|
368
|
+
layouts_yaml = stream.read()
|
|
369
|
+
decided = layouts_yaml.replace('TODO布局模式', 'flow')
|
|
370
|
+
layouts_md = build_layouts_md(split_top_blocks(decided), (1920, 1080))
|
|
371
|
+
self.assertRegex(layouts_md, r'- kind: grid\n\s+cols: 3\n\s+gap: \[\d+, \d+\]\n\s+margin: \[360, 360\]')
|
|
372
|
+
|
|
373
|
+
def test_symmetric_card_grid_omits_region_margin(self):
|
|
374
|
+
# 卡片组横向范围已和整页一致(都对称),不该冒出多余的区带 margin——保证对已有
|
|
375
|
+
# 对称模板零回归、layouts.md 不膨胀。
|
|
376
|
+
archetype = {
|
|
377
|
+
'name': 'layout-1',
|
|
378
|
+
'zh': '标题加三列卡片',
|
|
379
|
+
'role': 'content',
|
|
380
|
+
'bg': None,
|
|
381
|
+
'slots': [
|
|
382
|
+
text_slot('title', [120, 73, 1680, 68], '核心能力'),
|
|
383
|
+
],
|
|
384
|
+
'decor': [],
|
|
385
|
+
'pages': [2],
|
|
386
|
+
'rep': 2,
|
|
387
|
+
'pic_n': 0,
|
|
388
|
+
'confidence': 'high',
|
|
389
|
+
}
|
|
390
|
+
for left in (120, 720, 1320):
|
|
391
|
+
archetype['decor'].append({
|
|
392
|
+
'box': [left, 430, 480, 300],
|
|
393
|
+
'geom': 'rect',
|
|
394
|
+
'css': 'background: rgba(255,255,255,0.7)',
|
|
395
|
+
})
|
|
396
|
+
for top in (490, 570):
|
|
397
|
+
archetype['slots'].append(
|
|
398
|
+
text_slot('body', [left + 40, top, 400, 41], '卡片内容'),
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
flow = draft_flow(archetype, {}, (1920, 1080))
|
|
402
|
+
|
|
403
|
+
self.assertIsNotNone(flow)
|
|
404
|
+
self.assertEqual(flow['margin'], [120, 120])
|
|
405
|
+
grid = flow['regions'][1]
|
|
406
|
+
self.assertEqual(grid['kind'], 'grid')
|
|
407
|
+
self.assertNotIn('margin', grid)
|
|
408
|
+
|
|
409
|
+
archetype['flow'] = flow
|
|
410
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
411
|
+
emit_layouts([archetype], output_dir)
|
|
412
|
+
with open(os.path.join(output_dir, 'layouts.yaml'), encoding='utf-8') as stream:
|
|
413
|
+
layouts_yaml = stream.read()
|
|
414
|
+
grid_block = layouts_yaml.split('- kind: grid', 1)[1].split('items:', 1)[0]
|
|
415
|
+
self.assertNotIn('margin:', grid_block)
|
|
416
|
+
|
|
327
417
|
def test_fixed_template_anchors_stay_in_a_positioned_free_region(self):
|
|
328
418
|
archetype = {
|
|
329
419
|
'name': 'layout-1',
|
|
@@ -93,6 +93,35 @@ class LayoutCssTest(unittest.TestCase):
|
|
|
93
93
|
self.assertEqual(len(result.fails), 1)
|
|
94
94
|
self.assertIn('旧样式键 align/size', result.fails[0])
|
|
95
95
|
|
|
96
|
+
def test_normautofit_fontscale_shrinks_emitted_font_size(self):
|
|
97
|
+
# 章节大号数字:160px 字号靠 normAutofit fontScale 0.9 装进 144px 的框。
|
|
98
|
+
# 不乘 fontScale,消费端拿到 160px,字比框高,渐变裁切把底部切成透明。
|
|
99
|
+
shape = {
|
|
100
|
+
'text': {
|
|
101
|
+
'bodyPr': {'anchor': 't', 'font_scale': 0.9, 'ln_spc_reduction': 0.1},
|
|
102
|
+
'lstStyle': {'lvl1pPr': {'sz_px': 160, 'lnSpc': {'mult': 1.0}}},
|
|
103
|
+
'paragraphs': [{'runs': [{'text': '01.'}]}],
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
style = slot_style(shape)
|
|
107
|
+
self.assertIn('font-size: 144px', style['css'])
|
|
108
|
+
self.assertNotIn('font-size: 160px', style['css'])
|
|
109
|
+
# lnSpcReduction 0.1 把 1.0*1.2 的行高压到 1.08
|
|
110
|
+
self.assertIn('line-height: 1.08', style['css'])
|
|
111
|
+
|
|
112
|
+
def test_missing_autofit_leaves_font_size_untouched(self):
|
|
113
|
+
# 无 normAutofit(或无 fontScale)时零影响:字号原样、行高不缩。
|
|
114
|
+
shape = {
|
|
115
|
+
'text': {
|
|
116
|
+
'bodyPr': {'anchor': 't'},
|
|
117
|
+
'lstStyle': {'lvl1pPr': {'sz_px': 160, 'lnSpc': {'mult': 1.0}}},
|
|
118
|
+
'paragraphs': [{'runs': [{'text': '01.'}]}],
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
style = slot_style(shape)
|
|
122
|
+
self.assertIn('font-size: 160px', style['css'])
|
|
123
|
+
self.assertIn('line-height: 1.2', style['css'])
|
|
124
|
+
|
|
96
125
|
|
|
97
126
|
if __name__ == '__main__':
|
|
98
127
|
unittest.main()
|
|
@@ -163,6 +163,46 @@ class TextRoleContractTest(unittest.TestCase):
|
|
|
163
163
|
)
|
|
164
164
|
self.assertIn('css: "font-size: 48px; color: #C41230"', layouts_md)
|
|
165
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
|
+
|
|
166
206
|
|
|
167
207
|
if __name__ == '__main__':
|
|
168
208
|
unittest.main()
|
|
@@ -99,7 +99,7 @@ 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
|
|
|
@@ -133,7 +133,7 @@ layouts:
|
|
|
133
133
|
|
|
134
134
|
- **`background` 三形态**:`<asset-id>` / `{<theme>: <asset-id>}` / `{color: <colors-token>}`(`color` 是保留键,主题名禁止叫 color)。`asset` 两形态:`<asset-id>` / `{<theme>: <asset-id>}`。
|
|
135
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
|
|
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
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
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
139
|
- **文本角色判断**:脚本把实例页及其引用版式中的现有文本槽、几何和 CSS 完整写入草案;`text_roles` 只供模型把这些槽判断为 `title | subtitle | header | footer | body`,不控制槽位去留。判断不清时用 `body`,不归纳模板中不存在的标题、页眉或页脚。
|
|
@@ -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 %}
|
|
@@ -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 %}
|