@lark-apaas/coding-steering 0.1.32-beta.0 → 0.1.32-dev.4f80f68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/steering/design-html/skills/charts/SKILL.md +4 -0
- package/steering/design-html/skills/pptx-style-extract/SKILL.md +58 -22
- 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 +1768 -241
- package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +325 -22
- package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +1 -1
- package/steering/design-html/skills/pptx-style-extract/scripts/package.py +379 -156
- 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_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 +62 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +378 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +98 -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 +168 -0
- package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +27 -15
- package/steering/design-html/skills/preflight/scripts/probe.sh +0 -0
- package/steering/nestjs-react-fullstack/skills/app-init-feasibility-guide/SKILL.md +1 -0
- package/steering/nestjs-react-fullstack/skills/authn-guide/SKILL.md +6 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +5 -5
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +1 -1
- package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +37 -113
- package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +13 -2
- package/steering/nestjs-react-fullstack/skills/code-fix/SKILL.md +7 -7
- package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +149 -24
- package/steering/nestjs-react-fullstack/skills/connections-sdk/SKILL.md +202 -0
- package/steering/nestjs-react-fullstack/skills/nestjs-cache/SKILL.md +255 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +158 -543
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +15 -1
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +30 -14
- package/steering/nestjs-react-fullstack/skills/raw-sql-boundary-audit/SKILL.md +63 -0
- package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +1 -1
- package/steering/nestjs-react-fullstack/skills_common/trigger-guide/SKILL.md +284 -12
- package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +4 -0
- package/steering/vite-react/skills/plugin-guide/SKILL.md +3 -1
- package/steering/vite-react/skills/react-three-fiber/SKILL.md +4 -0
- package/steering/nestjs-react-fullstack/skills/client-add-aily-web-chat/SKILL.md +0 -139
- package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +0 -269
- package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +0 -214
- package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +0 -163
- package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +0 -311
- package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +0 -190
- package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +0 -160
- package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +0 -257
- package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +0 -104
- package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +0 -199
- package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +0 -128
- package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +0 -207
- package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +0 -165
- package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +0 -91
- package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +0 -165
- package/steering/nestjs-react-fullstack/skills_common/trigger-guide/references/trigger-lifecycle.md +0 -301
package/package.json
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/coding-steering",
|
|
3
|
-
"version": "0.1.32-
|
|
3
|
+
"version": "0.1.32-dev.4f80f68",
|
|
4
4
|
"description": "Stack-specific steering content for miaoda-coding templates",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"steering"
|
|
8
8
|
],
|
|
9
|
-
"scripts": {
|
|
10
|
-
"lint:md": "markdownlint 'steering/**/*.md' --ignore 'steering/**/skills/**' --ignore 'steering/**/skills_common/**' --ignore 'steering/**/skills_local/**'"
|
|
11
|
-
},
|
|
12
9
|
"devDependencies": {
|
|
13
10
|
"markdownlint-cli": "^0.47.0"
|
|
14
11
|
},
|
|
@@ -20,5 +17,8 @@
|
|
|
20
17
|
"miaoda",
|
|
21
18
|
"coding-steering"
|
|
22
19
|
],
|
|
23
|
-
"license": "MIT"
|
|
24
|
-
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"lint:md": "markdownlint 'steering/**/*.md' --ignore 'steering/**/skills/**' --ignore 'steering/**/skills_common/**' --ignore 'steering/**/skills_local/**'"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -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 %}
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pptx-style-extract
|
|
3
|
-
description: 从 PPTX/POTX
|
|
3
|
+
description: 从 PPTX/POTX 附件抽取 deck 风格包 v2(design.md + layouts.md + assets/ + ref/)。**对话里出现 .pptx / .potx 附件就先跑本 skill**——默认它是要参考的模板,不需要用户明说「按这个风格」。只有用户点明了另一种意图(只要翻译、只要总结、只要提取文字、要改这份文件本身)才跳过。附件文本摘要不能替代抽取。
|
|
4
|
+
metadata:
|
|
5
|
+
display-names:
|
|
6
|
+
zh-CN: PPT 模板风格抽取
|
|
7
|
+
en-US: PPTX Style Extract
|
|
4
8
|
---
|
|
5
9
|
|
|
6
10
|
# pptx-style-extract(PPT 模板风格抽取)
|
|
7
11
|
|
|
8
|
-
一份 PPTX → 一个风格包:`manifest.json`(机器清单)+ `design.md`(消费入口)+ `layouts.md` + `assets/` + `ref
|
|
12
|
+
一份 PPTX → 一个风格包:`manifest.json`(机器清单)+ `design.md`(消费入口)+ `layouts.md` + `assets/` + `ref/`。
|
|
13
|
+
|
|
14
|
+
三步依次跑完才有这个包:**抽取**(脚本出草案)→ **判断**(你改草案里的 TODO——认图、命名、写气质)→ **打包**(脚本产出 `design.md` + `layouts.md`)。deck 的版式坐标、资产路径、logo 出现在哪些页型,全部从包里读。
|
|
9
15
|
|
|
10
16
|
脚本路径相对本 skill 根目录;不做环境检测,依赖缺失脚本自己降级并在输出里报。
|
|
11
17
|
|
|
12
18
|
## When To Use This Skill
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
对话里出现 `.pptx` / `.potx` 附件,**默认它是要参考的模板**——先跑本 skill 抽风格,再动手做 deck。用户上传一份 PPT 却不说要干什么时,绝大多数情况就是「照着这个做」;等他明说才抽,等于把默认值设反了。
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
只有用户点明了另一种意图才跳过:只要翻译、只要总结、只要提取文字、要修改这份文件本身。
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
`SummarizeAttachmentOrFile` 的文本摘要不能替代抽取——它丢掉主题色、字体、母版几何、版式坐标和可复用的视觉资产,正是做 deck 最需要的那些。
|
|
19
25
|
|
|
20
26
|
## Runtime Contract
|
|
21
27
|
|
|
@@ -23,8 +29,8 @@ If the user only asks to summarize, translate, extract text, or review an existi
|
|
|
23
29
|
- Pillow may be present and improves image hashing, palette extraction, contact sheets, and asset transcoding. If Pillow is missing or fails, continue with degraded extraction and record the degradation in outputs.
|
|
24
30
|
- Do not run `pip install`, `uv pip install`, `npm install`, or any other dependency installation in the user session.
|
|
25
31
|
- Run Python with bytecode disabled: `PYTHONDONTWRITEBYTECODE=1 python3 -B ...`.
|
|
26
|
-
- Default working directory:
|
|
27
|
-
-
|
|
32
|
+
- Default working directory: `tmp/pptx-style-extract/<unique-id>/`, **relative to the project root**. The sandbox file tools (`read_file` / `write` / `multi_edit`) only accept project-relative paths — an absolute `/tmp/...` workdir makes every draft edit fall back to hand-written shell scripts.
|
|
33
|
+
- Intermediates stay under the project's `tmp/`; they are scratch, not deliverables. Only the final deck assets copied by the deck authoring step enter the app source tree.
|
|
28
34
|
- `package.py` only runs v1 checks when `--check-v1 <path>` is passed or `DSM_V1_DIR` points to a sibling checker. It must not rely on developer-machine paths in the sandbox.
|
|
29
35
|
|
|
30
36
|
## Fast Path Contract
|
|
@@ -39,40 +45,69 @@ Generated packages must make the attachment-consumption path explicit in `design
|
|
|
39
45
|
|
|
40
46
|
## 1. 抽取
|
|
41
47
|
|
|
48
|
+
附件的真实路径通常已经在用户消息的 `<<< 附件信息 >>>` 块里给出(`附件已保存到 <path>`)——直接用那个路径。**只有**消息里没给路径时才列目录,用列出来的名字,不要按附件标题猜文件名:
|
|
49
|
+
|
|
42
50
|
```bash
|
|
43
|
-
|
|
44
|
-
PYTHONDONTWRITEBYTECODE=1 python3 -B scripts/extract.py <pptx_path> /tmp/pptx-style-extract/<unique-id>/stage1
|
|
51
|
+
ls -la .agent/<conversation_id>/attachments/
|
|
45
52
|
```
|
|
46
53
|
|
|
54
|
+
一条命令,写成单行并带 `2>&1`:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
mkdir -p tmp/pptx-style-extract/<unique-id> && PYTHONDONTWRITEBYTECODE=1 python3 -B scripts/extract.py "<真实路径>" tmp/pptx-style-extract/<unique-id>/stage1 2>&1
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
输出最后一行必是这两个哨兵之一,照它走,不要用 `ls` 去探查目录——那会白花好几轮还是查不出原因:
|
|
61
|
+
|
|
62
|
+
- `EXTRACT_OK <outdir>/l-out` — 跑完了,进第 2 步
|
|
63
|
+
- `EXTRACT_PARTIAL ...` — 普查产物齐全、只有草案挂了,按它给的命令单独重跑 `draft.py`,不要重跑整条抽取
|
|
64
|
+
- 两个都没有 — 输出被截断(退出码可能仍是 0),原样重跑一次
|
|
65
|
+
|
|
66
|
+
**目录里没有 `.pptx` / `.potx`**(只有 `skills/` 之类)说明这次上传没有落成沙箱本地文件,本 skill 无法执行:如实告诉用户模板文件在沙箱里取不到、请重新上传或反馈平台,然后停下。此时**不要**用 `summarize_attachment_or_file` 的文本摘要当风格来源,也不要用 `generate_image` 自造配图充当模板视觉——那样产出的 deck 与模板无关,却看起来像成功了。
|
|
67
|
+
|
|
47
68
|
一条命令出全部:`extract.json`(普查数值)、`media-out/`(候选图)、`ref/`(审计层)、`l-out/`(**四件判断单草案 + BRIEF.md + contact-sheet.png + layout-sheet.png**)。
|
|
48
69
|
|
|
49
70
|
非 OOXML / 损坏 / 加密 → 报告调用方,不产半成品。
|
|
50
71
|
|
|
51
72
|
## 2. 判断
|
|
52
73
|
|
|
53
|
-
1. 读 `<outdir>/l-out/BRIEF.md` ——
|
|
74
|
+
1. 读 `<outdir>/l-out/BRIEF.md` —— 事实、草案依据、待判断清单一页看完。清单是实时扫出来的,**只列真正还有 TODO 的文件**,没列到的那份不用打开。
|
|
54
75
|
2. 看 `<outdir>/l-out/contact-sheet.png` 与 `<outdir>/l-out/layout-sheet.png` —— 候选图和代表页型都已拼好,对应 BRIEF 的表。
|
|
55
|
-
3.
|
|
56
|
-
4.
|
|
76
|
+
3. 用 `multi_edit` 一次批量改掉四件草案里的每一处 `TODO`:风格命名与气质、页型角色、`text_roles` 里的文本角色、`layout_modes` 里的布局模式、以及下面第 4 条的禁放区。文本角色只按模板中实际存在的文字判断为 `title|subtitle|header|footer|body`;判断不清就填 `body`,不补模板里没有的标题、页眉或页脚。草案就在项目内相对路径下,直接编辑,**不要写脚本去改 YAML**。看过重建图后真有这套模板特有的硬规则(例如主色只许用在哪类元素)再往 `## Hard Rules` 补一条;没有就不补,别为凑数编规则。
|
|
77
|
+
4. `layouts.yaml` 顶部 `bg_rules:` 按**背景资产**分组(不是按页型),逐张背景填 `text_safe` / `avoid` / `pairing_rule`:看图标清视觉主体、强光斑、深色透明区,打包时自动并入用该背景的所有页型。标题、正文、关键数字、图表、卡片、时间线及其容器的外接矩形都不得压住背景主体,透明容器也不能跨进禁放区。
|
|
78
|
+
|
|
79
|
+
`layouts.yaml` 的 `layouts:` 段是普查数值——页型、坐标、字号、色值、对齐、以及各页型自带的 `role: logo` 资产槽全部直读,**一个字都不要动**。模板自己给版式起过名时 `names:` 已经填好,读一遍确认表意即可。
|
|
57
80
|
|
|
58
81
|
判断口径:
|
|
59
82
|
|
|
60
83
|
- **满屏图默认属背景族**——整幅替换底图的艺术图/摄影图就是背景(首页那张记 `role: cover`);「内容图不进包」只指内容区里的图表、截图、配图。
|
|
61
84
|
- **logo 宁缺勿错**——看图确认有品牌文字或标志图形才留;拿不准就把该条从 `manifest.yaml` 删掉,在 `gaps` 写一句,候选图留在 `media-out/`。
|
|
85
|
+
- **anchors 是事实不是结论**——草案里每条只报测到的数(覆盖率、计数、占比),没有「这套风格的特征是 X」这种断言。这一段在 design.md 里读起来像总纲,消费端会照它建全局样式,所以脚本不敢替你下结论:一条 1/8 覆盖率的元素被描述成「跨页不动」,消费端就会每页都摆它。看过重建图后,把真正是这套模板特征的那几条改写成设计要点,不是特征的直接删掉。
|
|
62
86
|
- **数值只改名不改值**——草案里的坐标/色值/字号来自普查;确需推导值(CJK 行高转译、投影尺度上抬)在 manifest 写 `derived:` 声明理由,机检认声明。
|
|
63
|
-
-
|
|
64
|
-
-
|
|
87
|
+
- **页型命名**——模板自带版式名时草案已填好;只有靠样张聚类的模板(`names` 里还是 TODO)才需要按 BRIEF 的 slot 原文起中文名。
|
|
88
|
+
- **页型角色**——`roles:` 段有 TODO 时,看 `layout-sheet.png` 上对应的代表页定 `cover|section|content|quote|closing|blank|custom`。草案只把客观事实摆在注释里(代表页页码、页数、文字块数、字号序列、图片数、有无满屏底图),不替你下结论:字号多大算章节页、文字块多少算密集页,每套模板的答案都不一样。
|
|
89
|
+
- **文本角色**——`text_roles:` 段有 TODO 时,结合重建图和该行给出的来源、占位符、样例文字、box、字号与 CSS 判断标题、副标题、页眉、页脚或正文。这里仅改变已有文本槽的语义;所有文本槽、图片、容器与装饰仍由 `layouts:` 保留。标题、页眉或页脚已在背景中可见时,在 Hard Rules 写明对应页型只渲染已有 slots,不重复叠加文字。
|
|
90
|
+
- **资产位置不要写成全局规则**——同一个 logo 常按页型换位换尺寸(封面一个位置一个尺寸、内容页另一个)。位置只存在于 `layouts.md` 各页型的 `slots`,正文里只说「按该页型的 slot 摆放,没有就不放」。
|
|
91
|
+
- **背景和版式要配对**——真实背景不是纯色底。看 `contact-sheet.png` 判断每张背景的视觉主体/禁放区,看 `layout-sheet.png` 判断文字与卡片落点;`background` 草案已按版式直读填好,你只需在 `bg_rules` 里逐张背景补 `text_safe`、`avoid`、`pairing_rule`,避免消费 Agent 把文字、图表、卡片、时间线、标题容器、正文容器或宽透明容器的外接矩形压到金字塔、人物、产品图、强光斑或深色区域上。
|
|
65
92
|
- 双主题只认 BRIEF 的 `themes`;单主题包不写 `theme` 字段。
|
|
66
93
|
|
|
94
|
+
判断单里的**结构事实**(每个页型上方的注释)是判「这页该用绝对坐标还是流式」的依据:栅格几列、垂直间距序列(突变处即区带边界)、样张里每个文字槽的实际字数、命中哪个容器样式配方。文件头另有容器样式配方清单,带出现次数与跨页数——跨页多的是共性风格,只在一处出现的多半不是。
|
|
95
|
+
|
|
96
|
+
**页型给 `flow` 还是 `slots`,由你定**:内容长度会变的内容页(列表、表格、卡片组)优先用 `flow`,区带依次排,高度由实际内容决定;构图固定的页(封面、章节页)用 `slots` 绝对坐标。在扁平 `layout_modes:` 段为每个候选页型填 `flow` 或 `slots`,打包时按它落一份。判据就在该页型上方的结构事实里:规整多列栅格、等距重复的间距、成对出现的长文本槽都指向 `flow`。`flow` 中并列卡片以 `grid` 表达,每张卡片的容器、标题和正文放在同一个一层 `role: group` 中;logo、页码、页眉和页脚等固定锚点留在带 `box` 的 `free` 区带。模型结合实际内容判断纵向间距与留白,不机械照搬样张的 `y` 坐标。
|
|
97
|
+
|
|
98
|
+
普查是有损的。判断不了时可以直接翻 `<outdir>/ref/source/` —— PPTX 解压后的原文都在那里(`ppt/slides/slideN.xml`、`ppt/slideLayouts/`、`ppt/theme/`)。它只在中间产物里,不进交付包。
|
|
99
|
+
|
|
67
100
|
要更多依据时用 `PYTHONDONTWRITEBYTECODE=1 python3 -B scripts/query.py <outdir> <子命令>`(`shapes` / `colors` / `fonts` / `text-scale` / `images` / `clusters` / `media` / `slides` / `layouts` / `recipes` / `grids` / `get <点路径>`)。读数一律走它,**不写解析脚本、不读 XML、不开浏览器**。
|
|
68
101
|
|
|
69
102
|
## 3. 打包
|
|
70
103
|
|
|
71
104
|
```bash
|
|
72
|
-
PYTHONDONTWRITEBYTECODE=1 python3 -B scripts/package.py <outdir> <outdir>/l-out <包输出目录>
|
|
105
|
+
PYTHONDONTWRITEBYTECODE=1 python3 -B scripts/package.py <outdir> <outdir>/l-out <包输出目录> --force
|
|
73
106
|
```
|
|
74
107
|
|
|
75
|
-
exit 0 即交付。FAIL 会指名道姓(TODO 未改 / 数值不可追溯 / 枚举误用 / 引用断链 /
|
|
108
|
+
exit 0 即交付。FAIL 会指名道姓(TODO 未改 / 数值不可追溯 / 枚举误用 / 引用断链 / 体量超标),改判断单后**用同一个包目录重跑**(`--force` 就是为回修准备的),不要去改产物;体量 FAIL 走降级链(审计出包 → sidecar 化 → archetype 收缩)。
|
|
109
|
+
|
|
110
|
+
跑之前先确认判断单里 `TODO` 已经全部改完(`grep -rn TODO <outdir>/l-out`)——`body.md` 最容易漏。改完直接跑 package.py,不用先 `ls` 确认目录。
|
|
76
111
|
|
|
77
112
|
## 完成判据
|
|
78
113
|
|
|
@@ -82,7 +117,7 @@ exit 0 即交付。FAIL 会指名道姓(TODO 未改 / 数值不可追溯 / 枚
|
|
|
82
117
|
- [ ] Agent Fast Path 写清 `assetRoot` / `assetPaths` 的不透明前缀协议,并禁止目录探索。
|
|
83
118
|
- [ ] 抽不出的都在 `gaps`,无编造
|
|
84
119
|
|
|
85
|
-
配套 `v2-format-spec.md`(包形态与 schema)、`font-fallback.yaml
|
|
120
|
+
配套 `v2-format-spec.md`(包形态与 schema)、`font-fallback.yaml`(商业字体降级表)只在你要偏离草案结构时才需要读——草案的字段结构与包一致,`package.py` 按它落盘。
|
|
86
121
|
|
|
87
122
|
## Consume The Style Pack In A Deck
|
|
88
123
|
|
|
@@ -93,12 +128,13 @@ Read `<pack_dir>/design.md` first, especially `## Usage`, `## Hard Rules`, color
|
|
|
93
128
|
When generating a deck:
|
|
94
129
|
|
|
95
130
|
1. Call `copy_starter_component` with `kind: "deck-stage.js"`.
|
|
96
|
-
2. Build `<deck-stage width="
|
|
131
|
+
2. Build `<deck-stage width="<canvas width>" height="<canvas height>">` using the `canvas` declared in `layouts.md` — source decks are not always 16:9, and a default-sized stage shifts every coordinate on the page. Each slide is one static `<section>`.
|
|
97
132
|
3. Inline CSS variables from `design.md` into the HTML `<style>` block using a `--ppt-*` prefix.
|
|
98
|
-
4.
|
|
99
|
-
5.
|
|
100
|
-
6.
|
|
101
|
-
7.
|
|
133
|
+
4. **位移动画用独立的 `translate` 属性**:`@keyframes fadeUp { from{opacity:0; translate:0 24px} to{opacity:1; translate:0 0} }`。`transform` 是单一属性,动画里碰它会覆盖掉元素原有的那条(`left:50%; transform:translateX(-50%)` 的居中就此丢失);`translate` / `rotate` / `scale` 各自独立,与已有 `transform` 叠加。
|
|
134
|
+
5. Map `layouts.md` slots to absolute-positioned elements inside each section: expand `box: [x,y,w,h]` mechanically to `left/top/width/height`, then apply the slot's `css` declaration string unchanged. `box` owns geometry; `css` owns all rendering style, including the template's text padding, typography, alignment, line height, letter spacing, and rotation. Do not reinterpret PPTX fields or replace slot CSS with your own type scale. A slot with `asset` is a fixed image element (logo, mark): place that asset at that box on that archetype only. Do not reflow any of this as generic web grids.
|
|
135
|
+
6. Copy `<pack_dir>/assets/` into the project, for example `assets/pptx-style/<pack-name>/`, and reference those copied files with relative URLs. The scratch `tmp/pptx-style-extract/...` path must never appear in the final HTML.
|
|
136
|
+
7. Use the font stacks and fallback rules from `design.md`; do not install fonts or dependencies at runtime.
|
|
137
|
+
8. Run the slide preflight checks: no resource failures, no section overflow, and sampled screenshots follow the package colors, typography, layouts, assets, and Hard Rules.
|
|
102
138
|
|
|
103
139
|
## Export Consumer Attachments
|
|
104
140
|
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
version: 2
|
|
12
12
|
|
|
13
|
-
# fallback 用到的 web
|
|
14
|
-
# verified: true = 该镜像已 curl
|
|
13
|
+
# fallback 用到的 web 字体探测状态(镜像 https://miaoda.feishu.cn/fonts/css2)。
|
|
14
|
+
# verified: true = 该镜像已 curl 验证返回 @font-face(2026-08-06)。
|
|
15
15
|
# 状态过期不影响正确性:L4 对 verified 为 false 或存疑的字体按 dsm-v1-spec 字体条款
|
|
16
|
-
# 重新 curl
|
|
16
|
+
# 重新 curl 验证后再用,加载不到降到下一档。
|
|
17
17
|
web-fonts:
|
|
18
18
|
"Noto Sans SC": {verified: true}
|
|
19
19
|
"Noto Serif SC": {verified: true}
|
|
@@ -11,11 +11,16 @@ from ooxml import (NS, alias_group, family_of, local, luminance, raw_token, read
|
|
|
11
11
|
# 1920px canvas; fullscreen detection has its own independent threshold.
|
|
12
12
|
EPS_PX = 0.005 * 1920 # 9.6 px
|
|
13
13
|
FULLSCREEN_MIN_PCT = 95.0 # 上不封顶(尺寸维度,兼容旧口径)
|
|
14
|
-
FULLSCREEN_COVERAGE = 0.95 #
|
|
15
|
-
REPEAT_MIN = 2
|
|
14
|
+
FULLSCREEN_COVERAGE = 0.95 # 画布覆盖率判据:溢出/略未贴边都算背景,偏移出画布的大图不算
|
|
15
|
+
REPEAT_MIN = 2 # 出现 <2 次谈不上「重复位」
|
|
16
16
|
|
|
17
|
+
# 下面三个跨模块共用,import 处不要再抄一份字面量
|
|
18
|
+
SMALL_IMG_W_PCT = 25.0 # 占画布宽小于此值算小图(图标/角标/logo),不是内容配图
|
|
19
|
+
LUM_MID = 0.5 # 深/浅分界,Rec.709 相对亮度
|
|
20
|
+
ASSET_WARN_SINGLE = 500 * 1024 # 单张压缩后体积的 WARN 线,对齐 v2-format-spec §5 V2-6
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
|
|
23
|
+
def canvas_coverage(box, cw, ch):
|
|
19
24
|
"""图片与画布交集面积 / 画布面积。溢出(bleed)交集封顶于画布,天然 ≤1。"""
|
|
20
25
|
if not box:
|
|
21
26
|
return 0.0
|
|
@@ -99,8 +104,10 @@ def image_census(shape_recs, bg_images, units):
|
|
|
99
104
|
'in_group': any(o.get('in_group') for o in c['occs']),
|
|
100
105
|
})
|
|
101
106
|
clusters.sort(key=lambda c: -c['count'])
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
cw, ch = float(units.w), float(units.h)
|
|
108
|
+
fs = [o for o in occs if canvas_coverage(o.get('box'), cw, ch) >= FULLSCREEN_COVERAGE]
|
|
109
|
+
fs_clusters = [c for c in clusters
|
|
110
|
+
if canvas_coverage(c.get('box'), cw, ch) >= FULLSCREEN_COVERAGE]
|
|
104
111
|
crop_sigs = {_crop_sig(o.get('crop')) for o in occs} - {''}
|
|
105
112
|
images.append({
|
|
106
113
|
'media': media,
|
|
@@ -157,11 +164,10 @@ def _crop_sig(crop):
|
|
|
157
164
|
# ------------------------------------------ S5b media content clustering (素材聚类)
|
|
158
165
|
# Two levels: sha256 byte identity, then perceptual identity. The perceptual level
|
|
159
166
|
# is a dHash *prefilter* followed by a pixel confirmation, because dHash alone does
|
|
160
|
-
# not separate
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
#
|
|
164
|
-
# thumbnail difference decides. Calibration table is in reports/agent-extract-round2.md.
|
|
167
|
+
# not separate them on its own: distinct images can land at hamming distance 0
|
|
168
|
+
# while identical ones land several bits apart, so no single hamming threshold
|
|
169
|
+
# splits the two populations. The hash therefore only narrows the candidate set
|
|
170
|
+
# and the thumbnail pixel difference makes the call.
|
|
165
171
|
DHASH_PREFILTER_MAX = 10 # hamming distance over the 64-bit dHash
|
|
166
172
|
PIXDIFF_MAX = 5.0 # mean per-channel |Δ| over the 64x64 thumbnail
|
|
167
173
|
THUMB_PX = 64
|
|
@@ -546,7 +552,7 @@ def font_census(shape_recs, txstyles_by_master, themes, scheme_by_part=None):
|
|
|
546
552
|
# A paragraph with no runs at all (only <a:endParaRPr/>) declares a
|
|
547
553
|
# default that renders no glyph, so it is counted under its own source
|
|
548
554
|
# and kept out of `rendered_n` — otherwise a face backing zero visible
|
|
549
|
-
# text can outrank the deck's actual typeface
|
|
555
|
+
# text can outrank the deck's actual typeface.
|
|
550
556
|
dr = p.get('defRPr') or {}
|
|
551
557
|
if dr:
|
|
552
558
|
covered = {slot for slot in ('latin', 'ea', 'cs')
|
|
@@ -875,7 +881,7 @@ def fit_grids(shape_recs):
|
|
|
875
881
|
"""逐页把同类形状拟合成栅格:列数/列起点/列步距 + 行数/行步距。
|
|
876
882
|
|
|
877
883
|
按**中心**分档而不是按左上角——一排 logo 尺寸各不相同却居中对齐于等宽格,
|
|
878
|
-
|
|
884
|
+
用左上角看不出列。
|
|
879
885
|
因此这里也不要求同尺寸,只要求同页同 kind 同层级。
|
|
880
886
|
"""
|
|
881
887
|
buckets = defaultdict(list)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""风格包 v2 机器门禁(镜像 ../v2-format-spec.md §5
|
|
2
|
+
"""风格包 v2 机器门禁(镜像 ../v2-format-spec.md §5:V2-1..V2-16 + V2-R5/R6/R7)。
|
|
3
3
|
|
|
4
4
|
用法: check_v2.py <包目录>
|
|
5
5
|
只做 v2 追加校验;check_v1 的规则不重复实现,调用方须先跑 check_v1.py。
|
|
@@ -36,7 +36,7 @@ V2_SECTIONS = ['themes', 'default-theme', 'theme-mechanism', 'color-confidence',
|
|
|
36
36
|
# —— V2-11 YAML 1.1 布尔字面量(PyYAML 会把这些键名解析成 True/False)——
|
|
37
37
|
BOOL_LITERALS = {'y', 'yes', 'n', 'no', 'true', 'false', 'on', 'off'}
|
|
38
38
|
|
|
39
|
-
# —— V2-6 体积上限(
|
|
39
|
+
# —— V2-6 体积上限(KB = 1024)——
|
|
40
40
|
ASSET_WARN_SINGLE = 500 * 1024 # 压缩图单张(受检对象 = 条目 path 指向的包内文件)
|
|
41
41
|
ASSET_MAX_TOTAL = 20 * 1024 * 1024 # 包内资产总量(assets/** ∪ 条目 path/full 并集)
|
|
42
42
|
|
|
@@ -305,7 +305,7 @@ class MdFile:
|
|
|
305
305
|
self.keys += [(k, n + offset) for k, n in sub.keys]
|
|
306
306
|
|
|
307
307
|
def section(self, key):
|
|
308
|
-
"""先取 frontmatter
|
|
308
|
+
"""先取 frontmatter,再取正文围栏块——存量风格包有把 layouts 写在正文围栏块里的。"""
|
|
309
309
|
if isinstance(self.data.get(key), (dict, list)):
|
|
310
310
|
return self.data[key]
|
|
311
311
|
for block in self.fenced:
|
|
@@ -632,7 +632,7 @@ def _pack_asset_files(pack):
|
|
|
632
632
|
|
|
633
633
|
|
|
634
634
|
def rule_v2_6(pack):
|
|
635
|
-
"""V2-6 压缩图单张 >500KB WARN;包内资产总量 >20MB FAIL
|
|
635
|
+
"""V2-6 压缩图单张 >500KB WARN;包内资产总量 >20MB FAIL"""
|
|
636
636
|
res = Result('V2-6', '压缩图单张 >500KB WARN;包内资产总量 >20MB FAIL')
|
|
637
637
|
# 500KB 只管压缩图 = 条目 path 指向的包内文件;
|
|
638
638
|
# full(原图)天然大,豁免单张 WARN 但计入总量;url 条目包内无文件,不适用。
|
|
@@ -871,9 +871,147 @@ def rule_layouts_pointer(pack):
|
|
|
871
871
|
return res
|
|
872
872
|
|
|
873
873
|
|
|
874
|
+
ENUM_REGION_KIND = ['grid', 'stack', 'free']
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def _box4(v):
|
|
878
|
+
"""[x, y, w, h] 四个数且 w/h 为正才算合法框,否则 None。
|
|
879
|
+
|
|
880
|
+
门禁只该产出 PASS/WARN/FAIL。框里混进字符串会让下面的比大小直接抛 TypeError,
|
|
881
|
+
脚本崩了比报 FAIL 更难查;静默跳过又会让畸形框冒充通过。
|
|
882
|
+
"""
|
|
883
|
+
if not (isinstance(v, list) and len(v) == 4):
|
|
884
|
+
return None
|
|
885
|
+
if any(isinstance(x, bool) or not isinstance(x, (int, float)) for x in v):
|
|
886
|
+
return None
|
|
887
|
+
return v if v[2] > 0 and v[3] > 0 else None
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def _box_overlap(a, b):
|
|
891
|
+
ax0, ay0, ax1, ay1 = a[0], a[1], a[0] + a[2], a[1] + a[3]
|
|
892
|
+
bx0, by0, bx1, by1 = b[0], b[1], b[0] + b[2], b[1] + b[3]
|
|
893
|
+
ox = min(ax1, bx1) - max(ax0, bx0)
|
|
894
|
+
oy = min(ay1, by1) - max(ay0, by0)
|
|
895
|
+
return (ox * oy) if ox > 0 and oy > 0 else 0
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
def rule_v2_15(pack):
|
|
899
|
+
"""V2-15 同一段里的字段不能互相矛盾(text_safe 不得与 avoid 相交)
|
|
900
|
+
|
|
901
|
+
包里两条规则打架时,消费端照哪条都是错——实测一张背景 text_safe 有 34% 压在
|
|
902
|
+
avoid 上,消费端按 text_safe 把标题放进了禁放区,正好压在背景主体上。
|
|
903
|
+
这类矛盾出在「不同来源的字段拼在一起」:text_safe 是脚本算的,avoid 是看图的人
|
|
904
|
+
填的,谁都没错,凑一起就错。
|
|
905
|
+
"""
|
|
906
|
+
res = Result('V2-15', '同段字段自洽(text_safe 不与 avoid 相交)')
|
|
907
|
+
checked = 0
|
|
908
|
+
for md in pack.files:
|
|
909
|
+
for bg, entry in (md.data.get('backgrounds') or {}).items():
|
|
910
|
+
if not isinstance(entry, dict):
|
|
911
|
+
continue
|
|
912
|
+
raw_ts = entry.get('text_safe')
|
|
913
|
+
avoids = entry.get('avoid') or []
|
|
914
|
+
ts = _box4(raw_ts)
|
|
915
|
+
if ts is None:
|
|
916
|
+
if raw_ts is not None:
|
|
917
|
+
res.fails.append(
|
|
918
|
+
f'backgrounds.{bg}.text_safe 不是合法的 [x, y, w, h]'
|
|
919
|
+
f'(四个数、w/h 为正):{raw_ts!r}')
|
|
920
|
+
continue
|
|
921
|
+
checked += 1
|
|
922
|
+
for i, av in enumerate(avoids):
|
|
923
|
+
raw_box = av.get('box') if isinstance(av, dict) else av
|
|
924
|
+
box = _box4(raw_box)
|
|
925
|
+
if box is None:
|
|
926
|
+
res.fails.append(
|
|
927
|
+
f'backgrounds.{bg}.avoid[{i}] 不是合法的 [x, y, w, h]'
|
|
928
|
+
f'(四个数、w/h 为正):{raw_box!r}')
|
|
929
|
+
continue
|
|
930
|
+
ov = _box_overlap(ts, box)
|
|
931
|
+
if ov:
|
|
932
|
+
res.fails.append(
|
|
933
|
+
f'backgrounds.{bg} 的 text_safe {ts} 与 avoid[{i}] {box} '
|
|
934
|
+
f'相交 {ov} px²(占安全区 {round(100.0 * ov / (ts[2] * ts[3]))}%)'
|
|
935
|
+
f'——消费端按哪条都会违反另一条')
|
|
936
|
+
res.notes.append('校验 %d 段背景规则' % checked if checked else '无 backgrounds 段,无适用对象')
|
|
937
|
+
return res
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
LEGACY_SLOT_STYLE_KEYS = frozenset(('size', 'weight', 'color', 'align', 'valign', 'insets_px'))
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def rule_v2_16(pack):
|
|
944
|
+
"""V2-16 slot 的渲染样式只通过 CSS 承载,禁止泄漏 PPTX/旧契约字段。"""
|
|
945
|
+
res = Result('V2-16', 'slot 渲染样式统一使用 css(box 只负责几何)')
|
|
946
|
+
checked = 0
|
|
947
|
+
for name, (layout, _md) in sorted(pack.layouts.items()):
|
|
948
|
+
if not isinstance(layout, dict):
|
|
949
|
+
continue
|
|
950
|
+
groups = [('slots', layout.get('slots') or [])]
|
|
951
|
+
flow = layout.get('flow')
|
|
952
|
+
if isinstance(flow, dict):
|
|
953
|
+
for i, region in enumerate(flow.get('regions') or []):
|
|
954
|
+
if isinstance(region, dict):
|
|
955
|
+
groups.append((f'flow.regions[{i}].items', region.get('items') or []))
|
|
956
|
+
for group, items in groups:
|
|
957
|
+
for i, item in enumerate(items):
|
|
958
|
+
if not isinstance(item, dict):
|
|
959
|
+
continue
|
|
960
|
+
checked += 1
|
|
961
|
+
legacy = sorted(LEGACY_SLOT_STYLE_KEYS & set(item))
|
|
962
|
+
if legacy:
|
|
963
|
+
res.fails.append(
|
|
964
|
+
f'layouts.{name}.{group}[{i}] 含旧样式键 {"/".join(legacy)};'
|
|
965
|
+
'保留 box/role/type/asset,把渲染属性转换为可直接写入 style 的 css')
|
|
966
|
+
res.notes.append(f'校验 {checked} 个 slot/flow item')
|
|
967
|
+
return res
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
def rule_v2_14(pack):
|
|
971
|
+
"""V2-14 flow 与 slots 二选一;flow 的区带类型合法、grid 必带 cols"""
|
|
972
|
+
res = Result('V2-14', 'flow 形态合法(与 slots 互斥,区带类型在枚举内)')
|
|
973
|
+
n_flow = 0
|
|
974
|
+
for name, (layout, _md) in sorted(pack.layouts.items()):
|
|
975
|
+
if True:
|
|
976
|
+
if not isinstance(layout, dict):
|
|
977
|
+
continue
|
|
978
|
+
flow = layout.get('flow')
|
|
979
|
+
if flow is None:
|
|
980
|
+
continue
|
|
981
|
+
n_flow += 1
|
|
982
|
+
if layout.get('slots'):
|
|
983
|
+
res.fails.append(f'layouts.{name} 同时有 flow 和 slots——'
|
|
984
|
+
f'两份坐标都在,消费端不知道该按哪份渲染')
|
|
985
|
+
if not isinstance(flow, dict):
|
|
986
|
+
res.fails.append(f'layouts.{name}.flow 不是映射')
|
|
987
|
+
continue
|
|
988
|
+
regions = flow.get('regions')
|
|
989
|
+
if not isinstance(regions, list) or not regions:
|
|
990
|
+
res.fails.append(f'layouts.{name}.flow 没有 regions')
|
|
991
|
+
continue
|
|
992
|
+
for i, r in enumerate(regions):
|
|
993
|
+
if not isinstance(r, dict):
|
|
994
|
+
continue
|
|
995
|
+
_check_enum(res, f'layouts.{name}.flow.regions[{i}].kind',
|
|
996
|
+
r.get('kind'), ENUM_REGION_KIND)
|
|
997
|
+
if r.get('kind') == 'grid' and not r.get('cols'):
|
|
998
|
+
res.fails.append(f'layouts.{name}.flow.regions[{i}] 是 grid 但没有 cols')
|
|
999
|
+
if r.get('kind') == 'free' and not layout.get('slots'):
|
|
1000
|
+
# free 区带要按坐标摆,但坐标在 slots 里——而 slots 与 flow 互斥,
|
|
1001
|
+
# 所以 free 的元素必须自带 box
|
|
1002
|
+
for j, it in enumerate(r.get('items') or []):
|
|
1003
|
+
if isinstance(it, dict) and not it.get('box'):
|
|
1004
|
+
res.fails.append(
|
|
1005
|
+
f'layouts.{name}.flow.regions[{i}].items[{j}] 在 free 区带里'
|
|
1006
|
+
f'但没有 box——free 要按坐标摆,坐标必须自带')
|
|
1007
|
+
res.notes.append('%d 个页型用 flow' % n_flow if n_flow else '无页型用 flow,无适用对象')
|
|
1008
|
+
return res
|
|
1009
|
+
|
|
1010
|
+
|
|
874
1011
|
RULES = [rule_v2_1, rule_v2_2, rule_v2_3, rule_v2_4, rule_v2_5, rule_v2_6,
|
|
875
1012
|
rule_v2_7, rule_v2_8, rule_v2_9, rule_v2_10, rule_v2_11, rule_v2_12,
|
|
876
|
-
rule_v2_13,
|
|
1013
|
+
rule_v2_13, rule_v2_14, rule_v2_15, rule_v2_16, rule_sidecar_dup,
|
|
1014
|
+
rule_audit_fields, rule_layouts_pointer]
|
|
877
1015
|
|
|
878
1016
|
|
|
879
1017
|
def main():
|
|
@@ -883,9 +1021,10 @@ def main():
|
|
|
883
1021
|
pack = Pack(sys.argv[1])
|
|
884
1022
|
print(f'包: {pack.root}')
|
|
885
1023
|
print('文件: ' + ', '.join(md.name for md in pack.files))
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1024
|
+
# 这个解析器只服务于机检取数,它读不懂不代表内容有问题——产物是逐字节写出去的,
|
|
1025
|
+
# 消费模型读多行 CSS 之类毫无障碍。所以这里既不静默跳过,也不拦住流水线:
|
|
1026
|
+
# 把原文交出来,由模型看一眼确认内容没丢。
|
|
1027
|
+
unread = [(md.name, lineno, text) for md in pack.files for lineno, text in md.anomalies]
|
|
889
1028
|
print('-' * 72)
|
|
890
1029
|
failed = 0
|
|
891
1030
|
for rule in RULES:
|
|
@@ -900,6 +1039,12 @@ def main():
|
|
|
900
1039
|
failed += len(res.fails)
|
|
901
1040
|
print('-' * 72)
|
|
902
1041
|
print(f'结论: {"FAIL" if failed else "PASS"}({failed} 条 FAIL)')
|
|
1042
|
+
if unread:
|
|
1043
|
+
print('-' * 72)
|
|
1044
|
+
print(f'待你确认({len(unread)} 行机检解析器没读懂,产物里内容仍在,'
|
|
1045
|
+
f'请对照原文确认没丢东西):')
|
|
1046
|
+
for name, lineno, text in unread:
|
|
1047
|
+
print(f' {name}:{lineno} {text.strip()[:100]}')
|
|
903
1048
|
sys.exit(1 if failed else 0)
|
|
904
1049
|
|
|
905
1050
|
|