forked from AxTps/Starlight_Lancher
feat:移除了弹窗,服务器添加sls
This commit is contained in:
342
.agents/skills/lark-slides/SKILL.md
Normal file
342
.agents/skills/lark-slides/SKILL.md
Normal file
@ -0,0 +1,342 @@
|
||||
---
|
||||
name: lark-slides
|
||||
version: 1.0.0
|
||||
description: "飞书幻灯片:创建和编辑幻灯片。创建演示文稿、读取幻灯片内容、管理幻灯片页面(创建、删除、读取、局部替换)。当用户需要创建或编辑幻灯片、读取或修改单个页面时使用。当用户给出 doubao.com 的 /slides/ URL/token 时,也应直接使用本 skill,不要因为域名不是飞书而回退到 WebFetch;路由依据是 URL 路径模式和 token,而不是域名。不负责:云文档内容编辑(走 lark-doc)、云文档里的独立画板对象(走 lark-whiteboard)、上传或下载普通文件(走 lark-drive)。"
|
||||
metadata:
|
||||
requires:
|
||||
bins: ["lark-cli"]
|
||||
cliHelp: "lark-cli slides --help"
|
||||
---
|
||||
|
||||
# slides (v1)
|
||||
|
||||
> 本技能文档较长,务必使用 Read 工具阅读两次,必须阅读完整全文。
|
||||
|
||||
## 权威经验
|
||||
|
||||
**权威经验是全局硬约束和高频易错点,必须牢记并严格遵守。**
|
||||
|
||||
- 你有充足的时间完成这个 PPT,质量永远比速度重要。
|
||||
- PPT 的尺寸是 960x540,必须严格确保主体内容在页面边界内。
|
||||
- !!!禁止交付无图产物!!! 必须使用大量图片增强视觉效果!!! 禁止重复使用同一张图!!!
|
||||
- 封面页的主视觉必须是 `<img>`(来自生图工具或搜图工具),不要使用 `<shape>` 或 `<icon>` 拼出封面视觉。
|
||||
- 禁止用 `<shape>` 和 `<line>` 拟形具体物项,必须使用生图工具生成的 `<img>`。
|
||||
- 禁止在 `headline` 或 `title` 下方放置用于分隔或装饰的 `rect` 或 `<line>`。
|
||||
- 禁止在任何页面内部使用无意义的装饰线条或色块条带,页面任何一边都不要使用贴边窄条。
|
||||
- 生图工具的指令参数必须以“不要出现任何文字和颜色色号”结尾,避免生成的图片上出现干扰文字。
|
||||
- 禁止使用 emoji 图标,任何位置都不能出现。
|
||||
- 字号必须显式设置 `<content>` 的 `fontSize` 属性,不要依赖 `textType` 的默认字号兜底,这些兜底值明显偏大。
|
||||
- 大数字、字号大或字数多的 `<content>` 必须设置 `wrap="true" autoFit="normal-auto-fit"` 属性自动换行和缩排,避免文字溢出。
|
||||
- 文字颜色必须用 `<content>` 的 `color` 属性而不是 `fontColor` 属性。
|
||||
- 文字行间距必须设置 `<content>` 的 `lineSpacing="multiple:xx"` 或 `lineSpacing="fixed:xx"` 而不是 `lineSpacing="xx"`。
|
||||
- 图片必须用 `<img>` 而不是 `<image>`。
|
||||
- IconPark 图标必须填充颜色(设置 `<fill><fillColor color="rgba(R,G,B,A)"/></fill>`)并和背景有足够对比。
|
||||
- 绘制图表时原生图表(柱状、条形、折线、面积、饼(环)、雷达、组合图)用 `<chart>`,其他(漏斗图、金字塔图、象限图、矩阵图等)用 `<shape>` + `<line>` 模拟。
|
||||
- 隐藏 `<chart>` 的图例只能通过不写或删除 `<chartLegend>` 实现,`<chartLegend>` 不支持 `position="none"`。
|
||||
- 表格优先用 `rect` 和 `text` 模拟,其他用 `<table>`,没有 `<shape type="table">`。
|
||||
- 必须设置 `<table>` 的 `width` 和 `height` 固定表格大小,同时设置需要保留列宽或行高的 `<col>` 的 `width` 和 `<tr>` 的 `height`,其余自动分配。
|
||||
- `<td>` 直接子元素只有 `<fill>`(背景)、`<content>`(文字)和边框配置(一般不用),不能嵌套 `<shape>`、`<img>`、`<icon>`。
|
||||
- `<shape type="rect">` 只是形状不是容器,`<icon>`、`<img>`、`<shape type="text">` 和其他 `<shape>` 必须与它平级靠坐标叠放。
|
||||
- 填充渐变颜色必须用 `<fill><fillColor color="linear-gradient(135deg, rgba(R,G,B,A) 0%, rgba(R,G,B,A) 100%)"/></fill>`。
|
||||
- 编辑页面前必须阅读 [`lark-slides-edit-workflows.md`](references/lark-slides-edit-workflows.md)。
|
||||
- 绘制图表前必须阅读 [`slides_chart_demo.xml`](references/slides_chart_demo.xml)。
|
||||
- 当用户要求无损复述历史上下文时,必须无损复述以上权威经验、必读的技能文档(需要重新阅读)和易错元素语法(尤其是 `<table>` 和 `<chart>`)。
|
||||
|
||||
## 豆包设计原则
|
||||
|
||||
适用范围:
|
||||
|
||||
- 普通内容页的设计必须以豆包设计原则为最高准则,除非用户要求使用模板或直接提供设计方案。
|
||||
- 不适用于 `title-cover`、`section-divider`、`conclusion`、`quote-highlight` 和 `big-number`。
|
||||
|
||||
核心要求:
|
||||
|
||||
- 必须采用信息密度极高的图文卡片布局,追求充实饱满、图文丰富、可逐行细读的版面,宁可密而满,不要空而疏。
|
||||
- **!!!信息密度极高!!! 图多!!! 卡多!!! 字多!!!**
|
||||
|
||||
排版布局:
|
||||
|
||||
- 卡片布局:卡片按多行网格铺满页面,版面对称、均衡、不留白。网格数、图文比例按内容变化,避免每页雷同。使用更多卡片做细分承载,避免在单张卡片里堆砌大量文字(例如 8 张 50 字卡片优于 2 张 200 字卡片),多个要点必须拆分为多张子卡片。
|
||||
- 卡片样式:方角卡片 + 半透明填充 + 无边框 + 卡片贴边窄条(可选);所有卡片必须使用相同的配色方案(少量需强调的卡片除外),禁止同页出现彩虹卡片(卡片颜色超过 3 种)。
|
||||
- 卡片结构:视觉锚点(关键词、编号或 IconPark 图标)+ 标题 + 内容(包括文字、图片、图表、子卡片)。
|
||||
- 文字卡片:多数页面必须满足 6-8 张文字卡片、200-400 文字数量,字数不足时必须扩写成长句或段落,文字卡片不要留白,必须充实饱满。文字卡片不是短标签,而是“标题 + 完整说明”,像浓缩的分析文稿。文字内容不得不用列表、分栏、关键词或短句时,必须保证层次清晰,更建议拆分为多张子卡片。
|
||||
- 图片卡片:多数页面必须满足 1-3 张图片卡片,缺少图片时必须用生图工具补充配图,图片卡片与文字卡片组成网格,确保图文丰富。
|
||||
- 图表卡片:数据信息不要在文字卡片中罗列,必须在图表卡片中可视化(包括表格、图表、时间线、流程图等),图表卡片与其他卡片组成网格,展现数据驱动。
|
||||
- 间距要求:所有边距都要左右对称,页面和内部内容的边距至少 40px(内容不要贴边),卡片和内部文字的边距至少 5px(文字不要贴边),卡片之间保持 20-40px 的间距。
|
||||
- 文字对齐:正文默认左对齐,只在封面、结尾或大号数字场景中使用居中;表格里的文字左对齐、数字右对齐、仅关键词或短句时居中对齐。
|
||||
|
||||
视觉风格:
|
||||
|
||||
- 美学:干净、明亮、清爽但信息饱满;靠卡片和对齐网格在高密度下维持秩序感;同排卡片文字数量应相近以保持观感整齐。
|
||||
- 字体:全篇以无衬线体(思源黑体)为主,封面或关键强调可少量使用衬线体。
|
||||
- 字号:标题 28-36pt、正文 12-14pt、注释 10-12pt,常规关键指标 16-32pt、核心指标用 36-52pt 数字,下面配 10-14pt 标签与简短解读,需要容纳更多文字时允许使用更小的字号。
|
||||
- 图标:内嵌 IconPark 图标(可用关键词或编号替代)作为视觉锚点,让高密度文字也有图形节奏,而不是成片纯文字块。
|
||||
- 配色:克制颜色数量,确保所有页面都只使用同样的 1 个背景色(偏好浅米白)、1 个主色、1 个强调色和 1 个辅助色;偏好莫兰迪配色,禁止彩虹配色(比如蓝配橙)。
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| 用户需求 | 优先动作 | 关键文档 / 命令 |
|
||||
|----------|----------|-----------------|
|
||||
| 新建 PPT | 先规划 `slide_plan.json`,再按复杂度选择一步或两步创建 | `planning-layer.md`、`visual-planning.md`、`asset-planning.md`、`slides +create` |
|
||||
| 用户要求使用模板 | 将模板导入为 Slides 再编辑 | `lark-slides-pptx-template-workflows.md` |
|
||||
| 编辑单个标题、文本块、图片或局部元素 | 优先块级替换/插入,不改页序 | `slides +replace-slide`、`lark-slides-replace-slide.md` |
|
||||
| 读取或分析已有 PPT | 解析 slides/wiki token,用 shortcut 回读全文 XML 或读取单页 XML,保存 `xml_presentation_id`、`slide_id`、`revision_id` | `slides +xml-get`、`xml_presentation.slide.get`、`lark-slides-xml-presentations-get.md` |
|
||||
| 查看或回滚历史版本 | 先用 `+history-list` 找 `history_version_id`,再 `+history-revert`,必要时 `+history-revert-status` 轮询 | [`lark-slides-history.md`](references/lark-slides-history.md) |
|
||||
| 获取幻灯片页面截图 | 用 `slide_id` 或页号指定页面,一次不超过 10 页 | `slides +screenshot`、`lark-slides-screenshot.md` |
|
||||
| 上传或使用图片 | 先上传为 `file_token`,禁止直接写 http(s) 外链 | `slides +media-upload`、`lark-slides-media-upload.md`,或 `+create --slides` 的 `@./path` 占位符 |
|
||||
| 绘制图表 | 原生图表(柱状、条形、折线、面积、饼(环)、雷达、组合图)用 `<chart>`,其他(漏斗图、金字塔图、象限图、矩阵图等)用 `<shape>` + `<line>` 模拟 | `xml-schema-quick-ref.md`、`slides_chart_demo.xml` |
|
||||
| 绘制表格 | 优先用 `rect` 和 `text` 模拟,其他用 `<table>` | `xml-schema-quick-ref.md` |
|
||||
| 使用图标 | 禁止盲猜 iconType,必须先检索 IconPark,再写 `<icon iconType="...">`,图标必须填充颜色并和背景有足够对比,禁止使用 emoji 图标 | `iconpark_tool.py search → resolve`、`iconpark.md` |
|
||||
| 创建失败、空白页、3350001、布局异常 | 先回读状态,再按排障清单修复,不假设原操作原子成功 | `troubleshooting.md`、`validation-checklist.md` |
|
||||
|
||||
**CRITICAL — 开始前 MUST 先用 Read 工具读取 [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md),认证、权限和全局参数均以 lark-shared 为准。**
|
||||
|
||||
**CRITICAL — 查看或回滚历史版本前,MUST 先读取 [`lark-slides-history.md`](references/lark-slides-history.md)。回滚接口只接受 `history_version_id`,不要把 `revision_id` 直接传给 `+history-revert`。**
|
||||
|
||||
**CRITICAL — 生成任何 XML 之前,MUST 先用 Read 工具读取 [xml-schema-quick-ref.md](references/xml-schema-quick-ref.md),禁止凭记忆猜测 XML 结构。**
|
||||
|
||||
**CRITICAL — 新建演示文稿或大幅改写页面时,MUST 先生成 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`,再生成 XML。先创建对应目录,规划层规则和中间产物生命周期见 [planning-layer.md](references/planning-layer.md)。仅替换一个标题、插入一个块等小型已有页编辑可豁免。**
|
||||
|
||||
**CRITICAL — 新建演示文稿或大幅改写页面时,生成 XML 前 MUST 读取 [visual-planning.md](references/visual-planning.md),确保 `layout_type`、`visual_focus`、`text_density` 实际改变页面几何、主视觉和文本量。**
|
||||
|
||||
**CRITICAL — 新建演示文稿或大幅改写页面时,规划 `asset_need` MUST 遵循 [asset-planning.md](references/asset-planning.md):只做元数据规划,必须有 `fallback_if_missing`,不得要求真实搜索、下载或上传素材。**
|
||||
|
||||
**CRITICAL — 将完整 `<slide>` XML 提交给 `slides +create --slides`、`xml_presentation.slide create` 或 `slides +replace-pages` 之前,MUST 先把待提交 XML 保存到本地文件并运行唯一版式准出入口 [`scripts/xml_text_overlap_lint.py`](scripts/xml_text_overlap_lint.py);`summary.error_count` 必须为 0 才能调用接口,`summary.warning_count > 0` 时必须先做对应页面的截图复核。**
|
||||
|
||||
**CRITICAL — 创建或大幅改写后,MUST 按 [validation-checklist.md](references/validation-checklist.md) 做显式验证:回读全文 XML、核对页数和关键元素,并使用 [`scripts/xml_text_overlap_lint.py`](scripts/xml_text_overlap_lint.py) 统一检查 XML、越界、重叠、空白页和内容稀疏风险。**
|
||||
|
||||
**CRITICAL — 创建前自检或失败排障时,MUST 按 [troubleshooting.md](references/troubleshooting.md) 检查 XML 转义、结构、shell 截断、图片 token、3350001 和布局风险。**
|
||||
|
||||
**编辑已有幻灯片页面**:单个标题、文本块、图片或局部元素优先用 [`+replace-slide`](references/lark-slides-replace-slide.md)(块级替换/插入,不动页序);已有 Slides 的多页大改优先用 [`+replace-pages`](references/lark-slides-replace-pages.md) 在原 presentation 内批量重建页面,避免 `slides +create` 生成新链接。选择 action 和完整读-改-写流程见 [`lark-slides-edit-workflows.md`](references/lark-slides-edit-workflows.md)。
|
||||
|
||||
**用户要求使用模板**:按 [lark-slides-pptx-template-workflows.md](references/lark-slides-pptx-template-workflows.md) 处理。
|
||||
|
||||
## 身份选择
|
||||
|
||||
飞书幻灯片通常是用户自己的内容资源。**默认应优先显式使用 `--as user`(用户身份)执行 slides 相关操作**,始终显式指定身份。
|
||||
|
||||
- **`--as user`(推荐)**:以当前登录用户身份创建、读取、管理演示文稿。执行前先完成用户授权:
|
||||
|
||||
```bash
|
||||
lark-cli auth login --domain slides
|
||||
```
|
||||
|
||||
- **`--as bot`**:仅在用户明确要求以应用身份操作,或需要让 bot 持有/创建资源时使用。使用 bot 身份时,要额外确认 bot 是否真的有目标演示文稿的访问权限。
|
||||
|
||||
**执行规则**:
|
||||
|
||||
1. 创建、读取、增删 slide、按用户给出的链接继续编辑已有 PPT,默认都先用 `--as user`。
|
||||
2. 如果出现权限不足,先检查当前是否误用了 bot 身份;不要默认回退到 bot。
|
||||
3. 只有在用户明确要求"用应用身份 / bot 身份操作",或当前工作流就是 bot 创建资源后再做协作授权时,才切换到 `--as bot`。
|
||||
|
||||
## 执行前必做
|
||||
|
||||
> **重要**:`references/slides_xml_schema_definition.xml` 是此 skill 唯一正确的 XML 协议来源;其他 md 仅是对它和 CLI schema 的摘要。
|
||||
|
||||
高频只读:
|
||||
|
||||
- [xml-schema-quick-ref.md](references/xml-schema-quick-ref.md)
|
||||
- [planning-layer.md](references/planning-layer.md)(新建 / 大幅改写)
|
||||
- [visual-planning.md](references/visual-planning.md)(新建 / 大幅改写)
|
||||
- [asset-planning.md](references/asset-planning.md)(新建 / 大幅改写)
|
||||
- [validation-checklist.md](references/validation-checklist.md)(创建 / 大幅改写后)
|
||||
|
||||
按需再读:
|
||||
|
||||
- 创建:[`lark-slides-create.md`](references/lark-slides-create.md)
|
||||
- 阅读:[`lark-slides-xml-presentations-get.md`](references/lark-slides-xml-presentations-get.md)
|
||||
- 编辑:[`lark-slides-edit-workflows.md`](references/lark-slides-edit-workflows.md)、[`lark-slides-replace-slide.md`](references/lark-slides-replace-slide.md)、[`lark-slides-replace-pages.md`](references/lark-slides-replace-pages.md)
|
||||
- 历史版本:[`lark-slides-history.md`](references/lark-slides-history.md)
|
||||
- 截图:[`lark-slides-screenshot.md`](references/lark-slides-screenshot.md)
|
||||
- 图片:[`lark-slides-media-upload.md`](references/lark-slides-media-upload.md)
|
||||
- 图表:[`slides_chart_demo.xml`](references/slides_chart_demo.xml)
|
||||
- 图标:[`iconpark.md`](references/iconpark.md)、[`scripts/iconpark_tool.py`](scripts/iconpark_tool.py)
|
||||
- 排障:[`troubleshooting.md`](references/troubleshooting.md)
|
||||
- 完整协议:[`slides_xml_schema_definition.xml`](references/slides_xml_schema_definition.xml)
|
||||
|
||||
|
||||
## Workflow
|
||||
|
||||
### Design Ideas
|
||||
|
||||
不要生成无设计感的幻灯片。纯白背景 + 标题 + bullets 只能作为极简临时稿,不能作为正式交付。
|
||||
|
||||
开始写 XML 前,先在 `slide_plan.json` 里确定 deck 级视觉策略:
|
||||
|
||||
- **主题化配色**:配色必须服务本次主题、行业和受众,不要默认蓝色商务风。如果把同一套颜色换到另一个完全不同主题仍然成立,说明配色不够具体。
|
||||
- **主次比例**:选择 1 个主色承担约 60-70% 视觉权重,1 个辅助色承担结构和分区,1 个强调色只用于关键数字、结论或行动点。不要让所有颜色权重相同。
|
||||
- **背景一致性**:先确定全 deck 的背景策略,默认保持同一明暗基调和底色体系;无论深浅,都要保证内容和背景对比充足。
|
||||
- **统一 motif**:选择一个可复用视觉母题贯穿全文,例如编号节点、卡片处理方式、半出血图片区域、标题、页脚。不要每页换一套装饰语言。
|
||||
|
||||
每页至少要有一个视觉元素:图片、图标、图表、表格、流程、对比结构或大号数字。文本框本身不算主视觉。
|
||||
|
||||
常见页面形态:
|
||||
|
||||
- **双栏结构**:左文右图或左图右文,视觉区域占 35-45% 宽度。
|
||||
- **图标行**:图标在色块或圆形底中,右侧是短标题和一句解释。
|
||||
- **网格**:适合能力、模块、风险、行动项,每格内容保持同等层级。
|
||||
- **半出血视觉**:图片占据左/右半屏,文字覆盖或贴边排布。
|
||||
- **大数字卡片**:核心指标用大数字,下面配标签与简短解读。
|
||||
- **对比列**:before/after、方案 A/B、问题/解法用左右并列,标题和基线严格对齐。
|
||||
- **时间线/流程图**:步骤用节点和箭头表达,流程方向必须一眼可见。
|
||||
|
||||
常见错误必须避免:
|
||||
|
||||
- 不要所有页面复用同一种标题 + 三 bullets 版式。
|
||||
- 不要用低对比文字或低对比图标,例如浅灰字压在浅色背景上。
|
||||
- 不要让装饰线穿过文字,或让页脚、来源、编号挤压主体内容。
|
||||
- 不要把素材缺失表现为空白图片框;必须按 `fallback_if_missing` 生成替代图片。
|
||||
- 不要在任何位置使用 emoji 图标。
|
||||
|
||||
|
||||
### 创建方式选择
|
||||
|
||||
| 场景 | 推荐方式 |
|
||||
|------|----------|
|
||||
| 简单 XML(1-3 页、结构简单、几乎无复杂中文和特殊字符) | `slides +create --slides '[...]'` 一步创建 |
|
||||
| 复杂 XML(多页、含中文、大段文本、复杂布局、嵌套引号、特殊字符较多) | **两步创建**:先 `slides +create` 创建空白 PPT,再用 `xml_presentation.slide create` 逐页添加 |
|
||||
| 已有 PPT 继续追加或插入页面 | 使用 `xml_presentation.slide create`,必要时配合 `before_slide_id` |
|
||||
|
||||
> [!WARNING]
|
||||
> `--slides '[...]'` 的风险点主要在 shell 参数传递,而不是单纯页数。即使只有 1 页,只要 XML 足够复杂,也建议使用两步创建法。
|
||||
|
||||
> [!IMPORTANT]
|
||||
> `slides +create --slides` 底层会逐页创建,不是原子操作。中途失败时先记录 `xml_presentation_id`,回读确认当前状态,再继续修复或追加。
|
||||
|
||||
### 生成流程
|
||||
|
||||
```text
|
||||
Step 1: 需求分析 & 读取知识
|
||||
- 分析主题、受众、页数、风格;
|
||||
- 若用户要求使用模板,按 lark-slides-pptx-template-workflows.md 处理
|
||||
- 读取 xml-schema-quick-ref.md;新建 / 大幅改写时还要读取 planning-layer.md、visual-planning.md、asset-planning.md
|
||||
- 涉及图表读取 slides_chart_demo.xml
|
||||
|
||||
Step 2: 生成大纲 → 写入 slide_plan.json
|
||||
- 生成结构化大纲
|
||||
- 新建 / 大幅改写必须先创建目录并写入 `slide_plan.json`
|
||||
- plan 字段、路径命名和 `asset_need` 结构按 planning-layer.md / asset-planning.md 执行
|
||||
|
||||
Step 3: 按 slide_plan.json 生成 XML → 创建
|
||||
- 逐页消费 plan:key_message 定主结论,layout_type 定几何,visual_focus 定主视觉,text_density 定文本量
|
||||
- 缺少真实素材时必须用 `fallback_if_missing` 生成替代图片,不要留空
|
||||
- 创建方式按“创建方式选择”判断;图片、复杂 XML、转义和 3350001 排查按 lark-slides-create.md、media-upload.md、troubleshooting.md 执行
|
||||
|
||||
Step 4: 审查 & 交付
|
||||
- 创建完成后,必须用 `slides +xml-get` 读取全文 XML,并按 validation-checklist.md 做显式验证记录,包括 XML 文本重叠检查
|
||||
- 失败或部分成功按 troubleshooting.md 处理;局部问题优先用 `+replace-slide` 修正
|
||||
- 没问题 → 交付:使用 NotifyHuman 工具交付 PPT 链接
|
||||
```
|
||||
|
||||
### jq 命令模板(编辑已有 PPT 时使用)
|
||||
|
||||
新建 PPT 推荐用 `+create --slides`。以下 jq 模板适用于向已有演示文稿追加页面的场景,可以避免手动转义双引号:
|
||||
|
||||
```bash
|
||||
# 追加到末尾
|
||||
lark-cli slides xml_presentation.slide create \
|
||||
--as user \
|
||||
--params '{"xml_presentation_id":"YOUR_ID"}' \
|
||||
--data "$(jq -n --arg content '<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
||||
<style><fill><fillColor color="BACKGROUND_COLOR"/></fill></style>
|
||||
<data>
|
||||
在这里放置 shape、line、table、chart 等元素
|
||||
</data>
|
||||
</slide>' '{slide:{content:$content}}')"
|
||||
|
||||
# 插到指定页之前:before_slide_id 必须在 --data body 里,与 slide 同级
|
||||
# ⚠️ 不要把 before_slide_id 写进 --params —— CLI 会当未知 query 参数静默下发,服务端忽略,新页跑到末尾
|
||||
lark-cli slides xml_presentation.slide create \
|
||||
--as user \
|
||||
--params '{"xml_presentation_id":"YOUR_ID"}' \
|
||||
--data "$(jq -n --arg content '<slide ...>...</slide>' --arg before 'TARGET_SLIDE_ID' \
|
||||
'{slide:{content:$content}, before_slide_id:$before}')"
|
||||
```
|
||||
|
||||
> 渐变色必须使用 `rgba()` 格式并带百分比停靠点,如 `linear-gradient(135deg,rgba(15,23,42,1) 0%,rgba(56,97,140,1) 100%)`。使用 `rgb()` 或省略停靠点会导致服务端回退为白色。
|
||||
|
||||
### 大纲模板
|
||||
|
||||
生成大纲时使用以下格式:
|
||||
|
||||
```text
|
||||
[PPT 标题] — [定位描述],面向 [目标受众]
|
||||
|
||||
页面结构(N 页):
|
||||
1. 封面页:[标题文案]
|
||||
2. [页面主题]:[要点1]、[要点2]、[要点3]
|
||||
3. [页面主题]:[要点描述]
|
||||
...
|
||||
N. 结尾页:[结尾文案]
|
||||
|
||||
风格:[配色方案],[排版风格]
|
||||
```
|
||||
|
||||
## 核心概念
|
||||
|
||||
### URL 格式与 Token
|
||||
|
||||
| URL 格式 | 示例 | Token 类型 | 处理方式 |
|
||||
|----------|------|-----------|----------|
|
||||
| `/slides/` | `https://example.larkoffice.com/slides/xxxxxxxxxxxxx` | `xml_presentation_id` | URL 路径中的 token 直接作为 `xml_presentation_id` 使用 |
|
||||
| `/wiki/` | `https://example.larkoffice.com/wiki/wikcnxxxxxxxxx` | `wiki_token` | ⚠️ **不能直接使用**,需要先查询获取真实的 `obj_token` |
|
||||
|
||||
> `+replace-slide` 和 `+media-upload` shortcut 会自动解析以上两种 URL;直接调用原生 API 时仍需手动解析 wiki 链接。
|
||||
|
||||
### Wiki 链接特殊处理(关键!)
|
||||
|
||||
知识库链接(`/wiki/TOKEN`)不能直接当 `xml_presentation_id`。直接调用原生 API 前,先查询 wiki 节点,确认 `node.obj_type == "slides"`,再用 `node.obj_token` 作为真实 presentation ID。
|
||||
|
||||
```bash
|
||||
lark-cli wiki spaces get_node --as user --params '{"token":"wiki_token"}'
|
||||
```
|
||||
|
||||
Shortcut `+replace-slide` 和 `+media-upload` 会自动解析 `/wiki/` URL;手动调用 `xml_presentations.*` / `xml_presentation.slide.*` 时才需要自己做这一步。
|
||||
|
||||
### 资源关系
|
||||
|
||||
```text
|
||||
Wiki Space (知识空间)
|
||||
└── Wiki Node (知识库节点, obj_type: slides)
|
||||
└── obj_token → xml_presentation_id
|
||||
|
||||
Slides (演示文稿)
|
||||
├── xml_presentation_id (演示文稿唯一标识)
|
||||
├── revision_id (版本号)
|
||||
└── Slide (幻灯片页面)
|
||||
└── slide_id (页面唯一标识)
|
||||
```
|
||||
|
||||
## Shortcuts 与 API
|
||||
|
||||
Shortcut 是对常用操作的高级封装(`lark-cli slides +<verb> [flags]`)。有 Shortcut 的操作优先使用。
|
||||
|
||||
| Shortcut | 说明 |
|
||||
|----------|------|
|
||||
| [`+create`](references/lark-slides-create.md) | 创建 PPT(可选 `--slides` 一步添加页面,支持 `<img src="@./local.png">` 占位符自动上传) |
|
||||
| [`+xml-get`](references/lark-slides-xml-presentations-get.md) | 读取全文 XML 并保存到本地文件,避免终端输出被截断 |
|
||||
| [`+media-upload`](references/lark-slides-media-upload.md) | 上传本地图片到指定演示文稿,返回 `file_token`(用作 `<img src="...">`),最大 20 MB |
|
||||
| [`+replace-slide`](references/lark-slides-replace-slide.md) | 对已有幻灯片页面进行块级替换/插入(`block_replace` / `block_insert`),自动注入 id 和 `<content/>`,不改变页序 |
|
||||
| [`+replace-pages`](references/lark-slides-replace-pages.md) | 在原演示文稿内批量重建多个页面:先创建新页到旧页前,再删除旧页;适合已有 Slides 的多页大改,不新建链接 |
|
||||
|
||||
没有 Shortcut 覆盖时使用原生 API。高频资源:`slides +xml-get` 读取全文;`xml_presentation.slide.create/delete/get/replace` 管理单页。
|
||||
|
||||
```bash
|
||||
lark-cli schema slides.<resource>.<method> # 调用 API 前必须先查看参数结构
|
||||
lark-cli slides <resource> <method> [flags] # 调用 API
|
||||
```
|
||||
|
||||
> **重要**:使用原生 API 时,必须先运行 `schema` 查看 `--data` / `--params` 参数结构,不要猜测字段格式。
|
||||
|
||||
## 核心规则
|
||||
|
||||
1. **先规划再写 XML**:新建演示文稿或大幅改写页面时,必须先写入 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`;模板、风格和大纲只能作为规划输入,不能绕过规划层
|
||||
2. **创建流程**:简单短 XML(1-3 页、结构简单、特殊字符少)可用 `slides +create --slides '[...]'` 一步创建;复杂内容、含图片/中文大段文本/嵌套引号/较多特殊字符,或超过 10 页时,默认先 `slides +create` 创建空白 PPT,再用 `xml_presentation.slide.create` 逐页添加
|
||||
3. **`<slide>` 直接子元素只有 `<style>`、`<data>`、`<note>`**:文本和图形必须放在 `<data>` 内
|
||||
4. **文本通过 `<content>` 表达**:必须用 `<content><p>...</p></content>`,不能把文字直接写在 shape 内
|
||||
5. **保存关键 ID**:后续操作需要 `xml_presentation_id`、`slide_id`、`revision_id`
|
||||
6. **删除谨慎**:删除操作不可逆,且至少保留一页幻灯片
|
||||
7. **编辑已有页面优先原链接更新**:修改单个 shape/img 用 `+replace-slide`(`block_replace` / `block_insert`),不要整页重建;已有 Slides 的多页整页重建用 `+replace-pages`,不要用 `slides +create` 新建整份 PPT;只有没有 shortcut 覆盖的特殊单页整页操作才手动 `slide.create` + `slide.delete`
|
||||
8. **`<img src>` 只能用上传到飞书 drive 的 `file_token`,禁止使用 http(s) 外链 URL**:飞书 slides 渲染端不会代理外链图片,外链 src 在 PPT 里通常不显示或显示破图。流程必须是「先把图存到本地 → 用 `slides +media-upload` 上传或 `+create --slides` 的 `@./path` 占位符自动上传 → 拿 `file_token` 写进 `<img src>`」。如果用户给了网图链接,先 `curl`/下载到 CWD 内再走上传流程,不要直接把外链 URL 塞进 `src`。**图片最大 20 MB**(slides upload API 不支持分片上传)。
|
||||
|
||||
> **注意**:如果 md 内容与 `slides_xml_schema_definition.xml` 或 `lark-cli schema slides.<resource>.<method>` 输出不一致,以后两者为准。
|
||||
137
.agents/skills/lark-slides/references/asset-planning.md
Normal file
137
.agents/skills/lark-slides/references/asset-planning.md
Normal file
@ -0,0 +1,137 @@
|
||||
# Asset Planning
|
||||
|
||||
新建演示文稿或大幅改写页面时,在写入 `slide_plan.json` 前后都可以参考本文件。目标是让 agent 主动识别有价值的图、图标、图表、流程图、时序图、架构图、装饰图案、截图或示意图需求,同时保持 deck 在没有真实素材时也能完整执行。
|
||||
|
||||
本文件只定义轻量资产规划。不要把它理解成素材采集流程。
|
||||
|
||||
## Core Rules
|
||||
|
||||
- `asset_need` is metadata only. It can guide page design.
|
||||
- Every planned asset must include a fallback visual plan. The fallback can use native charts, tables, placeholder regions, or XML shapes, text, and arrows as appropriate.
|
||||
- Asset needs must serve the page's `key_message` and `visual_focus`. Do not add decorative assets that do not clarify the page.
|
||||
- Prefer a few high-value asset plans over one asset on every page. For a 6-page technical or business deck, plan assets on at least 3 pages when the content allows.
|
||||
- If a real local asset already exists or the user provides one, it can be used through the normal media-upload workflow. Still keep `fallback_if_missing` in the plan.
|
||||
- Do not leave blank image boxes in final XML. If the asset is missing, render the fallback visual.
|
||||
|
||||
## JSON Shape
|
||||
|
||||
Use an object for one planned asset, or an array when a page genuinely needs multiple assets. Keep each item compact.
|
||||
|
||||
```json
|
||||
{
|
||||
"asset_type": "architecture_diagram",
|
||||
"purpose": "Show how API gateway, planner, XML generator, and Slides API interact.",
|
||||
"suggested_query": "agent native slides runtime architecture diagram",
|
||||
"fallback_if_missing": "Draw grouped boxes connected by arrows with short labels."
|
||||
}
|
||||
```
|
||||
|
||||
For a page without a meaningful asset need, use:
|
||||
|
||||
```json
|
||||
{
|
||||
"asset_type": "none",
|
||||
"purpose": "No external or simulated asset needed; the page is text-led.",
|
||||
"suggested_query": "",
|
||||
"fallback_if_missing": "Use typography, spacing, and simple accent shapes only."
|
||||
}
|
||||
```
|
||||
|
||||
## Supported Asset Types
|
||||
|
||||
- `paper_figure`: figure from a paper or technical article.
|
||||
- `architecture_diagram`: system components, data flow, dependency map, or model structure.
|
||||
- `icon`: small semantic symbol for a concept, step, role, or status.
|
||||
- `logo`: brand, product, team, or customer mark.
|
||||
- `chart`: column, bar, line, area, radar, pie, doughnut/ring, or combo data visual. Note: `<chart>` does not support funnel or scatter.
|
||||
- `infographic`: composed visual explanation, usually combining labels, numbers, and simple shapes.
|
||||
- `screenshot`: product UI, terminal output, workflow state, or page capture.
|
||||
- `flow_diagram`: process, sequence, decision tree, or mechanism diagram.
|
||||
- `none`: explicitly no asset needed.
|
||||
|
||||
Do not invent new asset types unless the user asks for a special visual format. If a need is close to these types, choose the closest one and explain the detail in `purpose`.
|
||||
|
||||
## Planning Guidance
|
||||
|
||||
Match asset type to slide role:
|
||||
|
||||
- `architecture-diagram` layout usually pairs with `architecture_diagram` or `flow_diagram`.
|
||||
- `process-flow` layout usually pairs with `flow_diagram`, `icon`, or `infographic`.
|
||||
- `comparison` layout often works with `icon`, `chart`, or `infographic`.
|
||||
- `timeline` layout often works with `icon`, `chart`, or shape-based milestone markers.
|
||||
- `big-number` layout often works with `chart` or `infographic`, but only if it supports the metric.
|
||||
- `image-left-text-right` and `image-right-text-left` can use `screenshot`, `paper_figure`, `logo`, or `infographic`; if missing, use a large placeholder diagram or stylized panel.
|
||||
|
||||
`suggested_query` is only a future lookup hint. Write it as a short phrase a human or later workflow could search, but do not execute the search unless the user separately requests real assets.
|
||||
|
||||
For `asset_type: "chart"`:
|
||||
|
||||
- If the visual is a supported standard data chart — column, bar, line, area, radar, pie, doughnut/ring, or combo — `fallback_if_missing` must still render as a native `<chart>`.
|
||||
- Do not imitate supported standard data visuals with manual drawing primitives.
|
||||
- Choose the data source explicitly:
|
||||
- `user_provided`: when the user provides concrete values, tables, CSV, or metric lists, use those values and do not replace them with mock data.
|
||||
- `mock_placeholder`: when the user asks for a placeholder, template, example, or chart position to replace later, use mock data in a native `<chart>`.
|
||||
- `mock_required_by_intent`: when the user does not provide concrete values but asks for data expression, charts, trends, comparisons, or distributions, use mock data in a native `<chart>`.
|
||||
- Mock data must be labeled as `模拟数据,仅占位,待替换真实数据` or equivalent. Do not present mock values as facts.
|
||||
- Manual drawing fallbacks are allowed only for unsupported chart types such as scatter, funnel, waterfall-like custom visuals, or decorative non-data visuals.
|
||||
|
||||
`fallback_if_missing` must be concrete enough to turn into XML, for example:
|
||||
|
||||
- "Draw a simplified attention matrix with 5 token labels, semi-transparent cells, and arrows to output token."
|
||||
- "Use three grouped boxes with arrows from client to gateway to service; add small protocol labels."
|
||||
- "Render a native `<chart>` using the user-provided series."
|
||||
- "Render a native `<chart>` with mock placeholder values and label it as `模拟数据,仅占位,待替换真实数据`."
|
||||
- "Use a bordered placeholder panel with product area labels, not an empty image."
|
||||
|
||||
Weak fallbacks to avoid:
|
||||
|
||||
- "Use a placeholder."
|
||||
- "Find another image."
|
||||
- "Leave blank if unavailable."
|
||||
- "Use generic decoration."
|
||||
|
||||
## Examples
|
||||
|
||||
Transformer Self-Attention page:
|
||||
|
||||
```json
|
||||
{
|
||||
"asset_type": "paper_figure",
|
||||
"purpose": "Explain token-to-token attention and why each output token mixes context.",
|
||||
"suggested_query": "Transformer self attention attention matrix diagram",
|
||||
"fallback_if_missing": "Draw a simplified attention matrix with token labels, colored weights, and arrows from input tokens to one highlighted output token."
|
||||
}
|
||||
```
|
||||
|
||||
System architecture page:
|
||||
|
||||
```json
|
||||
{
|
||||
"asset_type": "architecture_diagram",
|
||||
"purpose": "Show the runtime path from user prompt to plan, XML generation, Slides API creation, and fetch verification.",
|
||||
"suggested_query": "slides generation runtime architecture planner XML API verification",
|
||||
"fallback_if_missing": "Draw four grouped boxes connected left-to-right with arrows; put verification as a return arrow from Slides API to agent."
|
||||
}
|
||||
```
|
||||
|
||||
Business comparison page:
|
||||
|
||||
```json
|
||||
{
|
||||
"asset_type": "infographic",
|
||||
"purpose": "Make before/after differences scannable without dense bullet lists.",
|
||||
"suggested_query": "before after product workflow comparison infographic",
|
||||
"fallback_if_missing": "Use two side-by-side panels with matching icon circles and three parallel rows of concise labels."
|
||||
}
|
||||
```
|
||||
|
||||
## Plan To XML Contract
|
||||
|
||||
When generating XML:
|
||||
|
||||
1. If an asset exists and the workflow supports it, place it in the planned visual region.
|
||||
2. If no asset exists, immediately render `fallback_if_missing` with the planned generated close-enough image. Supported standard data visuals still use native `<chart>`; other fallbacks may use the image generation tool to create an approximate image.
|
||||
3. Size the fallback to satisfy `visual_focus`; it should be a real page element, not a tiny decoration.
|
||||
4. Keep text-density limits. Do not compensate for missing assets by adding long bullet text.
|
||||
5. After creation, fetch the presentation and verify asset pages are not blank and that each planned fallback is visible when no real asset was used.
|
||||
6. If the image generation tool is unavailable or fails, degrade to an XML-native fallback instead of leaving a blank: native `<chart>` for data, otherwise a simple in-card shape/text placeholder sized to fill `visual_focus`.
|
||||
41901
.agents/skills/lark-slides/references/iconpark-index.json
Normal file
41901
.agents/skills/lark-slides/references/iconpark-index.json
Normal file
File diff suppressed because it is too large
Load Diff
46
.agents/skills/lark-slides/references/iconpark.md
Normal file
46
.agents/skills/lark-slides/references/iconpark.md
Normal file
@ -0,0 +1,46 @@
|
||||
# IconPark 图标
|
||||
|
||||
IconPark 图标通过 `<icon>` 写入 slides XML,`iconType` 必须来自本 skill 的离线索引,避免凭记忆拼路径。
|
||||
|
||||
## 机器优先流程
|
||||
|
||||
```bash
|
||||
python3 skills/lark-slides/scripts/iconpark_tool.py search --query "增长趋势" --limit 8
|
||||
python3 skills/lark-slides/scripts/iconpark_tool.py resolve --name chart-line
|
||||
python3 skills/lark-slides/scripts/iconpark_tool.py list-categories
|
||||
```
|
||||
|
||||
`search` 返回 JSON 数组,每项包含 `iconType`、`category`、`name`、`tags`、`score`。直接把选中的 `iconType` 写入 XML,并为图标指定可见颜色:
|
||||
|
||||
```xml
|
||||
<icon iconType="iconpark/Charts/chart-line.svg" topLeftX="80" topLeftY="120" width="32" height="32">
|
||||
<fill>
|
||||
<fillColor color="rgba(37, 99, 235, 1)"/>
|
||||
</fill>
|
||||
</icon>
|
||||
```
|
||||
|
||||
## 使用规则
|
||||
|
||||
- 默认先检索:语义图标需求必须先用 `iconpark_tool.py search --limit 8` 或 `--limit 10`,让 agent 从候选里结合版面语义二次判断;不要阅读全文索引,也不要编造不存在的 `iconType`。
|
||||
- 图标用于概念提示、步骤、状态、指标、角色和导航;不要用无关装饰图标填充版面。
|
||||
- 常用尺寸:行内状态图标 16-24px,卡片标题图标 28-40px,主视觉图标 56-96px。
|
||||
- 图标必须填充颜色并和背景有足够对比;深色背景优先放在浅色圆形/方形底上,或使用 `rgba(255, 255, 255, 1)` 作为图标填充色。
|
||||
- 查不到合适图标时,从高频示例里选择替代图标(随机选择,不要千篇一律),不留空图标位。
|
||||
|
||||
## 高频示例
|
||||
|
||||
| 语义 | iconType |
|
||||
|---|---|
|
||||
| 设置/配置 | `iconpark/Base/setting.svg` |
|
||||
| 目标 | `iconpark/Base/aiming.svg` |
|
||||
| 增长趋势 | `iconpark/Charts/positive-dynamics.svg` |
|
||||
| 折线趋势 | `iconpark/Charts/chart-line.svg` |
|
||||
| 占比 | `iconpark/Charts/chart-proportion.svg` |
|
||||
| 数据看板 | `iconpark/Charts/data-screen.svg` |
|
||||
| 成功 | `iconpark/Character/check-one.svg` |
|
||||
| 失败/风险 | `iconpark/Character/close-one.svg` |
|
||||
| 团队/用户 | `iconpark/Peoples/peoples.svg` |
|
||||
| 安全防护 | `iconpark/Safe/protect.svg` |
|
||||
| 全球/市场 | `iconpark/Travel/world.svg` |
|
||||
| 邮件/联系 | `iconpark/Office/envelope-one.svg` |
|
||||
155
.agents/skills/lark-slides/references/lark-slides-create.md
Normal file
155
.agents/skills/lark-slides/references/lark-slides-create.md
Normal file
@ -0,0 +1,155 @@
|
||||
|
||||
# slides +create(创建飞书幻灯片)
|
||||
|
||||
创建一个新的飞书幻灯片演示文稿,可选一步添加页面内容。
|
||||
|
||||
- 禁止:从完整 <presentation> XML 解析/拆分/重序列化生成提交 payload。
|
||||
- 推荐:提交源直接就是单页 <slide> XML;+create --slides 只接受已经人工/程序直接生成的 slide 数组,不接受由
|
||||
presentation 动态拆出来的数组。
|
||||
|
||||
- 最稳:复杂 deck 默认空 deck + 单页 slide create,每次只提交一个 <slide>。
|
||||
|
||||
- 注意:复杂 XML 不适合直接塞命令行,中文、引号、特殊字符较多时,直接拼接 --slides 容易发生 shell 转义或截断。建议将每页 XML 保存为独立文件,使用 `jq --rawfile` 组装 JSON 数组,避免手动处理 XML 引号和换行。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 创建空白 PPT
|
||||
lark-cli slides +create --title "项目汇报"
|
||||
|
||||
# 创建 PPT + 添加 slide 页面
|
||||
lark-cli slides +create --title "项目汇报" --slides '[
|
||||
"<slide xmlns=\"http://www.larkoffice.com/sml/2.0\"><data><shape type=\"text\" topLeftX=\"80\" topLeftY=\"80\" width=\"800\" height=\"120\"><content textType=\"title\"><p>封面</p></content></shape></data></slide>",
|
||||
"<slide xmlns=\"http://www.larkoffice.com/sml/2.0\"><data><shape type=\"text\" topLeftX=\"80\" topLeftY=\"80\" width=\"800\" height=\"120\"><content textType=\"title\"><p>第二页</p></content></shape></data></slide>"
|
||||
]'
|
||||
|
||||
# 以应用身份创建(自动授权当前用户)
|
||||
lark-cli slides +create --title "项目汇报" --as bot
|
||||
|
||||
# 预览(不执行)
|
||||
lark-cli slides +create --title "项目汇报" --slides '[...]' --dry-run
|
||||
```
|
||||
|
||||
复杂内容建议按页保存 XML,再用 `jq --rawfile` 组装 `--slides` 参数:
|
||||
|
||||
```bash
|
||||
lark-cli slides +create --as user --title "项目汇报" \
|
||||
--slides "$(jq -n \
|
||||
--rawfile s1 .lark-slides/plan/project/slide-01.xml \
|
||||
--rawfile s2 .lark-slides/plan/project/slide-02.xml \
|
||||
'[$s1, $s2]')"
|
||||
```
|
||||
|
||||
`--rawfile` 会把文件内容作为字符串读入 JSON,自动处理 XML 中的引号和换行;不要手动拼接带大量转义符的 JSON 字符串。
|
||||
|
||||
## 返回值
|
||||
|
||||
工具成功执行后,返回一个 JSON 对象,包含以下字段:
|
||||
|
||||
- **`xml_presentation_id`**(string):演示文稿的唯一标识符,后续添加页面时需要此 ID
|
||||
- **`title`**(string):演示文稿标题
|
||||
- **`url`**(string,可选):演示文稿的在线链接,如有返回则务必展示给用户(需要 drive 相关权限;若获取失败则不返回此字段)
|
||||
- **`revision_id`**(integer):演示文稿版本号
|
||||
- **`slide_ids`**(string[],可选):仅传 `--slides` 时返回,成功添加的页面 ID 列表
|
||||
- **`slides_added`**(integer,可选):仅传 `--slides` 时返回,成功添加的页面数量
|
||||
- **`images_uploaded`**(integer,可选):仅 `--slides` 中含 `@<本地路径>` 占位符时返回,已上传的去重后图片数量
|
||||
- **`permission_grant`**(object,可选):仅 `--as bot` 时返回,说明是否已自动为当前 CLI 用户授予可管理权限
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 不传 `--slides` 时,`slides +create` 只创建空白演示文稿。创建后需要使用 `xml_presentation.slide create` 逐页添加 slide 内容。
|
||||
>
|
||||
> 传了 `--slides` 时,CLI 先创建空白演示文稿,再逐页调用 `xml_presentation.slide create` 添加页面。如果某一页添加失败,CLI 会停止并报错,已创建的演示文稿和已添加的页面会保留。
|
||||
>
|
||||
> 如果演示文稿是**以应用身份(bot)创建**的,如 `lark-cli slides +create --as bot`,CLI 会**尝试为当前 CLI 用户自动授予该演示文稿的 `full_access`(可管理权限)**。
|
||||
>
|
||||
> 以应用身份创建时,结果里会额外返回 `permission_grant` 字段,明确说明授权结果:
|
||||
> - `status = granted`:当前 CLI 用户已获得该演示文稿的可管理权限
|
||||
> - `status = skipped`:本地没有可用的当前用户 `open_id`,因此不会自动授权
|
||||
> - `status = failed`:演示文稿已创建成功,但自动授权用户失败
|
||||
>
|
||||
> **不要擅自执行 owner 转移。** 如果用户需要把 owner 转给自己,必须单独确认。
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--title` | 否 | 演示文稿标题(不传则默认 "Untitled") |
|
||||
| `--slides` | 否 | slide 内容 JSON 数组,每个元素是一个 `<slide>` XML 字符串(最多 10 个;超过 10 页请先用 `+create` 创建空白 PPT,再用 `xml_presentation.slide create` 逐页添加) |
|
||||
|
||||
## `--slides` 参数格式
|
||||
|
||||
```json
|
||||
[
|
||||
"<slide xmlns=\"http://www.larkoffice.com/sml/2.0\">...第1页XML...</slide>",
|
||||
"<slide xmlns=\"http://www.larkoffice.com/sml/2.0\">...第2页XML...</slide>"
|
||||
]
|
||||
```
|
||||
|
||||
JSON string 数组,每个元素是一页 slide 的完整 XML。CLI 内部负责包装成 API 所需的 `{"slide": {"content": "..."}}` 格式并逐页调用。
|
||||
|
||||
### 本地图片:`@<path>` 占位符
|
||||
|
||||
`<img>` 元素的 `src` 属性如果以 `@` 开头,CLI 会把它当作本地文件路径,自动上传到当前演示文稿,并把占位符替换为返回的 `file_token`。
|
||||
|
||||
```bash
|
||||
lark-cli slides +create --as user --title "图测试" --slides '[
|
||||
"<slide xmlns=\"http://www.larkoffice.com/sml/2.0\"><data><img src=\"@./assets/chart.png\" topLeftX=\"100\" topLeftY=\"100\" width=\"320\" height=\"180\"/></data></slide>"
|
||||
]'
|
||||
```
|
||||
|
||||
行为:
|
||||
|
||||
- 路径相对于**当前工作目录**(CWD)解析;**必须是 CWD 内的相对路径**(如 `./pic.png`、`./assets/x.png`)
|
||||
- 同一份图被多次引用时**只上传一次**(按路径去重)
|
||||
- `src` 不以 `@` 开头的会原样保留,但**只允许写 `slides +media-upload` 拿到的 `file_token`**;**禁止写 http(s) 外链 URL**:飞书 slides 渲染端不会代理外链图片,外链 src 通常显示破图。要用网图必须先下载到 CWD 内、再走上传流程
|
||||
- 单张图片最大 20 MB(slides upload API 不支持分片上传)
|
||||
- 校验阶段就会检查所有占位符文件存在及大小;缺文件或超限直接报错,不会创建空白 PPT 占位
|
||||
- 创空白 PPT → 上传所有图 → 替换 token → 逐页创建 slide,按这个顺序执行
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **路径必须在 CWD 内**:`@/abs/path/x.png` 或 `@../up/x.png` 这种会被 CLI 拒绝(报 `unsafe file path`)。如果素材在别的目录,先 `cd` 过去再执行。
|
||||
|
||||
### 给已有 PPT 加带图新页
|
||||
|
||||
`+create --slides` 只在新建 PPT 时使用 `@` 占位符。给已有 PPT 加带图新页要分两步(CLI 没封装这个组合):
|
||||
|
||||
```bash
|
||||
# 1) 上传图片
|
||||
TOKEN=$(lark-cli slides +media-upload --as user \
|
||||
--file ./pic.png --presentation $PRES_ID | jq -r .data.file_token)
|
||||
|
||||
# 2) 用返回的 file_token 创建带图新页
|
||||
lark-cli slides xml_presentation.slide create --as user \
|
||||
--params "{\"xml_presentation_id\":\"$PRES_ID\"}" \
|
||||
--data "{\"slide\":{\"content\":\"<slide xmlns=\\\"http://www.larkoffice.com/sml/2.0\\\"><data><img src=\\\"$TOKEN\\\" topLeftX=\\\"100\\\" topLeftY=\\\"100\\\" width=\\\"200\\\" height=\\\"200\\\"/></data></slide>\"}}"
|
||||
```
|
||||
|
||||
## 创建后续步骤
|
||||
|
||||
如果没有使用 `--slides`,`slides +create` 返回的 `xml_presentation_id` 用于后续操作:
|
||||
|
||||
```bash
|
||||
# 第 1 步:创建空白 PPT
|
||||
PRES_ID=$(lark-cli slides +create --title "项目汇报" | jq -r '.data.xml_presentation_id')
|
||||
|
||||
# 第 2 步:添加页面(使用返回的 xml_presentation_id)
|
||||
lark-cli slides xml_presentation.slide create --as user \
|
||||
--params "{\"xml_presentation_id\":\"$PRES_ID\"}" \
|
||||
--data '{
|
||||
"slide": {
|
||||
"content": "<slide xmlns=\"http://www.larkoffice.com/sml/2.0\">...</slide>"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## 常见错误
|
||||
|
||||
| 错误码 | 含义 | 解决方案 |
|
||||
|--------|------|----------|
|
||||
| 400 | 参数错误 | 检查参数格式是否正确 |
|
||||
| 403 | 权限不足 | 检查是否拥有 `slides:presentation:create` 和 `slides:presentation:write_only` scope |
|
||||
|
||||
## 相关命令
|
||||
|
||||
- [xml_presentation.slide create](lark-slides-xml-presentation-slide-create.md) — 添加幻灯片页面
|
||||
- [slides +xml-get](lark-slides-xml-presentations-get.md) — 读取 PPT 内容并保存到本地文件
|
||||
@ -0,0 +1,144 @@
|
||||
# 编辑已有 PPT:读-改-写闭环
|
||||
|
||||
局部编辑走 **shortcut [`+replace-slide`](lark-slides-replace-slide.md)**(块级替换 / 插入),配合 `xml_presentation.slide.get` 读原页拿 `block_id`。已有 Slides 的多页整页重建走 **[`+replace-pages`](lark-slides-replace-pages.md)**,保持原 presentation 链接不变。
|
||||
|
||||
> 生成 XML 前**必读** [xml-schema-quick-ref.md](xml-schema-quick-ref.md)。
|
||||
|
||||
## 决策树:block_replace vs block_insert
|
||||
|
||||
| 需求 | 推荐 action | 理由 |
|
||||
|------|------------|------|
|
||||
| 已知某块的 `block_id`,要换这块内容(改标题、换图、挪坐标) | `block_replace` | 精准替换,原子性好;`replacement` 根 `id` 由 CLI 自动注入为 `block_id` |
|
||||
| 只加 1~N 个元素、不动现有布局 | `block_insert` | 新增不覆盖,可选 `insert_before_block_id` 指定位置 |
|
||||
| 一次动多个元素(如:换标题 + 加图) | 单次 `--parts` 里拼多条 | 整批作为原子事务,任一失败整批不生效;`block_replace` 和 `block_insert` 可混用 |
|
||||
| 多页版式重建、整页坐标重排 | `+replace-pages` | 原 presentation 内批量 create-before/delete-old,不生成新 Slides 链接 |
|
||||
|
||||
> **没有字段级 patch**:即便只想改一个 `shape` 的 `topLeftX`,也得把整个块的新 XML 写出来用 `block_replace`。这不是"微调",是块级重写。
|
||||
|
||||
## 最小读-改-写闭环
|
||||
|
||||
```bash
|
||||
PID="xml_presentation_id_here"
|
||||
SID="slide_id_here"
|
||||
|
||||
# 1. 读原页,从 XML 里挑出要改的块的 3 位 short id(如 bUn / bab)
|
||||
lark-cli slides xml_presentation.slide get --as user \
|
||||
--params "{\"xml_presentation_id\":\"$PID\",\"slide_id\":\"$SID\"}"
|
||||
|
||||
# 2. 用 +replace-slide 直接改那个块(不需要搬原 XML)
|
||||
lark-cli slides +replace-slide --as user \
|
||||
--presentation "$PID" --slide-id "$SID" \
|
||||
--parts '[{"action":"block_replace","block_id":"bUn","replacement":"<shape type=\"text\" topLeftX=\"80\" topLeftY=\"80\" width=\"800\" height=\"120\"><content textType=\"title\"><p>新标题</p></content></shape>"}]'
|
||||
```
|
||||
|
||||
`slide_id` / 页序不会变。`block_replace` 的 `replacement` 根元素 `id` 会自动注入为 `block_id`,用户手写 XML 时不需要自己加。
|
||||
|
||||
## `revision_id` 参数
|
||||
|
||||
`--revision-id` 默认 `-1`,表示基于当前最新版执行。传具体版本号时,服务端以该版本为 base 应用变更:
|
||||
|
||||
```bash
|
||||
# 读时拿当前 revision_id
|
||||
REV=$(lark-cli slides xml_presentation.slide get --as user \
|
||||
--params "{\"xml_presentation_id\":\"$PID\",\"slide_id\":\"$SID\"}" \
|
||||
| jq '.data.revision_id')
|
||||
|
||||
# 写时传该版本号,服务端以此为 base
|
||||
lark-cli slides +replace-slide --as user \
|
||||
--presentation "$PID" --slide-id "$SID" --revision-id "$REV" \
|
||||
--parts '[{"action":"block_replace","block_id":"bUn","replacement":"<shape type=\"rect\" topLeftX=\"100\" topLeftY=\"100\" width=\"200\" height=\"100\"/>"}]'
|
||||
```
|
||||
|
||||
注意:传不存在的版本号(超过当前 revision)会返回 3350002 not found;不确定时用 `-1` 即可。
|
||||
|
||||
## `--tid` 事务锁
|
||||
|
||||
跨请求的并发事务 ID,多人协作长事务才用得上。**单人单次调用留空**即可。
|
||||
|
||||
## 两种 action 详解
|
||||
|
||||
### block_replace — 整块替换
|
||||
|
||||
适合"已知块 ID,要换这块整体内容"的场景。`replacement` 根元素的 `id="<block_id>"` 由 CLI 自动注入(用户手写的 XML 如果没带 `id` 直接省略即可;如果带了错的会被覆盖为正确值)。
|
||||
|
||||
```bash
|
||||
lark-cli slides +replace-slide --as user \
|
||||
--presentation "$PID" --slide-id "$SID" \
|
||||
--parts '[{"action":"block_replace","block_id":"bab","replacement":"<shape type=\"text\" topLeftX=\"80\" topLeftY=\"80\" width=\"800\" height=\"120\"><content textType=\"title\"><p>新标题</p></content></shape>"}]'
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `action` | 是 | 固定为 `block_replace` |
|
||||
| `block_id` | 是 | 目标块的 3 位 short element ID(从 `slide.get` 返回的 XML 里读)|
|
||||
| `replacement` | 是 | 新 XML 片段;根元素 `id` 会被 CLI 自动注入为 `block_id` |
|
||||
|
||||
### block_insert — 整块插入
|
||||
|
||||
适合"只想加一个元素,不动现有元素"的场景(典型:给已有页加图)。
|
||||
|
||||
```bash
|
||||
lark-cli slides +replace-slide --as user \
|
||||
--presentation "$PID" --slide-id "$SID" \
|
||||
--parts "$(jq -n --arg token "$FILE_TOKEN" \
|
||||
'[{action:"block_insert",insertion:("<img src=\""+$token+"\" topLeftX=\"500\" topLeftY=\"100\" width=\"200\" height=\"150\"/>"),insert_before_block_id:"baa"}]')"
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `action` | 是 | 固定为 `block_insert` |
|
||||
| `insertion` | 是 | 要插入的完整 XML 片段 |
|
||||
| `insert_before_block_id` | 否 | 插到这个块之前;省略(不提供此字段)则追加到页面末尾 |
|
||||
|
||||
> **`<img>` 必须用 `file_token`**,不能用外链 URL——先 `slides +media-upload --file ./pic.png --presentation $PID` 拿 token。
|
||||
|
||||
### 批量 parts
|
||||
|
||||
一次 `--parts` 最多 200 条,按数组顺序串行执行。`block_replace` 和 `block_insert` 可以在同一批次混用。举例:一次性把标题块替换、然后在末尾追加一个装饰图。
|
||||
|
||||
```bash
|
||||
lark-cli slides +replace-slide --as user \
|
||||
--presentation "$PID" --slide-id "$SID" \
|
||||
--parts '[
|
||||
{"action":"block_replace","block_id":"bab","replacement":"<shape type=\"text\" topLeftX=\"80\" topLeftY=\"80\" width=\"800\" height=\"120\"><content textType=\"title\"><p>新标题</p></content></shape>"},
|
||||
{"action":"block_insert","insertion":"<img src=\"<file_token>\" topLeftX=\"700\" topLeftY=\"400\" width=\"180\" height=\"100\"/>"}
|
||||
]'
|
||||
```
|
||||
|
||||
整批作为原子事务:任一条失败整批不生效。失败时后端通常返回 3350001;若响应中带 `failed_part_index` / `failed_reason` 字段,shortcut 会原样透传。
|
||||
|
||||
## 大 --parts 用 jq 或 stdin 组装
|
||||
|
||||
`--parts` 支持 `@file`(读文件)和 `-`(stdin)作为值来源,适合批量 XML 场景:
|
||||
|
||||
```bash
|
||||
# 从文件读
|
||||
lark-cli slides +replace-slide --as user --presentation "$PID" --slide-id "$SID" \
|
||||
--parts @parts.json
|
||||
|
||||
# 从 stdin 读
|
||||
cat parts.json | lark-cli slides +replace-slide --as user --presentation "$PID" --slide-id "$SID" \
|
||||
--parts -
|
||||
```
|
||||
|
||||
## 错误排查
|
||||
|
||||
| 现象 | 原因 | 对策 |
|
||||
|------|------|------|
|
||||
| 3350001,hint 含 "block_id not found" | `parts[i].block_id` 在当前页不存在 | 重新 `slide.get` 拿最新 XML,按里面的 short ID 再填 |
|
||||
| 3350002 not found | `--revision-id` 传了不存在的版本号 | 用 `-1` 或实际存在的 `revision_id` |
|
||||
| `<img>` 不显示 / 显示破图 | `src` 写了外链 URL | 换成通过 `+media-upload` 拿到的 `file_token` |
|
||||
| 3350001(block_replace 返回) | 正常情况下 CLI 已自动注入 `id` 和 `<content/>`;如果仍报错,确认 `block_id` 在当前页存在(重新 `slide.get`),检查 XML 结构是否合法;坐标是否超出 960×540 范围 | — |
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [lark-slides-replace-slide.md](lark-slides-replace-slide.md) — +replace-slide shortcut 参数详情
|
||||
- [lark-slides-replace-pages.md](lark-slides-replace-pages.md) — 多页整页重建 shortcut
|
||||
- [lark-slides-xml-presentation-slide-get.md](lark-slides-xml-presentation-slide-get.md) — slide.get 参考(拿 `block_id` / `revision_id`)
|
||||
- [lark-slides-xml-presentation-slide-replace.md](lark-slides-xml-presentation-slide-replace.md) — 底层 replace API 参考(一般直接用 shortcut 即可)
|
||||
- [lark-slides-media-upload.md](lark-slides-media-upload.md) — 上传图片拿 file_token
|
||||
- [xml-schema-quick-ref.md](xml-schema-quick-ref.md) — XML 元素和属性速查
|
||||
132
.agents/skills/lark-slides/references/lark-slides-history.md
Normal file
132
.agents/skills/lark-slides/references/lark-slides-history.md
Normal file
@ -0,0 +1,132 @@
|
||||
# slides history(历史版本与回滚)
|
||||
|
||||
用于查看 Slides XML presentation 历史版本、按 `history_version_id` 回滚,以及查询回滚任务状态。
|
||||
|
||||
`entries[].edit_time` 是 UTC RFC3339 时间字符串(例如 `2026-06-22T12:24:45Z`)。按时间匹配时先将其解析为时间值,再比较先后关系或时间差。
|
||||
|
||||
## 安全流程
|
||||
|
||||
1. 先用分页接口 `+history-list` 找到目标版本的 `history_version_id`。
|
||||
2. 如果用户指定的是 `revision_id`,不要假设它唯一,也不要把 `revision_id` 直接传给 `+history-revert`。先拉一页并在 `entries[]` 中筛选 `revision_id` 相同的候选;如果未匹配到且 `has_more=true`,继续用 `page_token` 翻页;如果已匹配到候选,最多额外再拉一页补齐可能跨页的相邻候选。最终优先根据用户目标时间与 `edit_time` 的接近程度选择最合适的一条,取同一条的 `history_version_id`;如果没有目标时间,或多个候选无法可靠区分,再向用户展示候选版本(`history_version_id`、`revision_id`、`edit_time`、`name/description`)并确认后回滚。
|
||||
3. 如果用户指定的是某一时刻但没有指定 `revision_id`,按 `entries[].edit_time` 匹配;优先选择不晚于目标时刻的最近一条历史记录,无法明确匹配时先向用户确认候选版本。
|
||||
4. 使用 `+history-revert` 发起回滚。接口会立即返回 `task_id`,回滚任务在服务端异步执行。
|
||||
5. 如果返回 `status: running`,保存 `task_id`,按照返回的 `poll_after_ms` 等待后调用 `+history-revert-status`。任务创建成功后,不得因为状态查询失败而重新发起回滚。
|
||||
6. 状态变为 `done`、`partial_failed` 或 `failed` 后停止轮询;达到整体轮询上限时也停止轮询,并向用户返回 `task_id` 和当前状态。
|
||||
7. 回滚完成后,用 `slides +xml-get` 或 `slides xml_presentations get` 读取演示文稿确认内容。
|
||||
|
||||
## 按 revision_id 或时间点回滚
|
||||
|
||||
当用户说“回滚到 revision_id=42”“恢复到昨天下午 3 点的版本”这类需求时,流程是:
|
||||
|
||||
1. 执行 `slides +history-list --presentation <presentation>` 获取第一页历史记录;`+history-list` 是分页接口,只有 `has_more=true` 且还需要更多候选时才继续传 `--page-token` 翻页。
|
||||
2. 如果用户给出 `revision_id`:先筛选当前页中 `entries[].revision_id == 用户给出的 revision_id`。如果未命中且 `has_more=true`,继续拉下一页;如果已经命中候选,最多额外再拉一页,补齐同一个 `revision_id` 可能跨页出现的相邻 `history_version_id`。若用户同时给出目标时间,在候选里选择 `edit_time` 与目标时间最接近的一条;若未给目标时间但候选只有一条,可直接使用;若多个候选无法可靠区分,不要自行取第一条,向用户展示候选并确认。
|
||||
3. 如果用户只给出时间:用 `entries[].edit_time` 匹配,选择目标时刻之前最近的一条;如果用户表达的是“最接近某时刻”,则选择绝对时间差最小的一条。
|
||||
4. 从最终匹配条目读取 `history_version_id`。`history_version_id` 对应服务端 `minor_history.version`,这是回滚接口需要的 ID。
|
||||
5. 执行 `slides +history-revert --presentation <presentation> --history-version-id <history_version_id>`。
|
||||
|
||||
候选确认时使用类似格式:
|
||||
|
||||
```text
|
||||
同一个 revision_id 命中多个历史版本,请确认要回滚哪一条:
|
||||
- history_version_id=11 revision_id=42 edit_time=2026-06-22T12:24:45Z name=...
|
||||
- history_version_id=12 revision_id=42 edit_time=2026-06-22T12:25:14Z name=...
|
||||
```
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 列出历史版本
|
||||
lark-cli slides +history-list --presentation "<slides_url_or_token>" --page-size 20
|
||||
|
||||
# 翻页
|
||||
lark-cli slides +history-list --presentation "<slides_url_or_token>" --page-size 20 --page-token "<page_token>"
|
||||
|
||||
# 发起回滚任务,立即返回 task_id
|
||||
lark-cli slides +history-revert --presentation "<slides_url_or_token>" --history-version-id 42
|
||||
|
||||
# 查询回滚任务状态
|
||||
lark-cli slides +history-revert-status --presentation "<slides_url_or_token>" --task-id "<task_id>"
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 命令 | 参数 | 必填 | 说明 |
|
||||
|-|-|-|-|
|
||||
| `+history-list` | `--presentation` | 是 | `xml_presentation_id`、Slides URL,或可解析为 Slides 的 wiki URL |
|
||||
| `+history-list` | `--page-size` | 否 | 返回条数,范围 `1-20`,默认 `20` |
|
||||
| `+history-list` | `--page-token` | 否 | 上一页返回的 `page_token` |
|
||||
| `+history-revert` | `--presentation` | 是 | 同一个演示文稿 |
|
||||
| `+history-revert` | `--history-version-id` | 是 | `+history-list` 返回的 `history_version_id`,必须大于 0 |
|
||||
| `+history-revert-status` | `--presentation` | 是 | 同一个演示文稿 |
|
||||
| `+history-revert-status` | `--task-id` | 是 | `+history-revert` 返回的 `task_id` |
|
||||
|
||||
## 异步轮询策略
|
||||
|
||||
1. `+history-revert` 返回 `task_id` 后,认为回滚任务已经成功创建。
|
||||
2. 如果 `status` 不是 `running`,不再调用状态接口。
|
||||
3. 如果 `status` 是 `running`,等待响应中的 `poll_after_ms` 后调用 `+history-revert-status`;`poll_after_ms` 缺失、为 `0` 或非法时,默认等待 10 秒。
|
||||
4. 状态查询返回 `running` 时继续轮询;返回 `done`、`partial_failed` 或 `failed` 时停止。
|
||||
5. 除非用户另有要求,默认最多轮询 5 分钟。达到上限后停止轮询,向用户说明任务仍在运行并返回 `task_id`,不得将其描述为回滚失败。
|
||||
6. 状态查询出现临时错误时,按相同间隔最多连续重试 3 次;只重试 `+history-revert-status`,不得重新调用 `+history-revert`。
|
||||
7. `done` 后读取当前演示文稿内容进行验证。
|
||||
8. `partial_failed` 或 `failed` 时展示 `failed_block_tokens`;除非用户明确确认,不得自动再次发起回滚。
|
||||
|
||||
## 返回值要点
|
||||
|
||||
`+history-list` 返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"revision_id": 42,
|
||||
"history_version_id": "11",
|
||||
"edit_time": "2026-06-22T12:24:45Z",
|
||||
"type": 1,
|
||||
"name": "版本名",
|
||||
"description": "版本说明",
|
||||
"editor_ids": ["ou_xxx"]
|
||||
}
|
||||
],
|
||||
"has_more": true,
|
||||
"page_token": "page_token"
|
||||
}
|
||||
```
|
||||
|
||||
`+history-revert` 返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task_xxx",
|
||||
"status": "running",
|
||||
"history_version_id": "11",
|
||||
"poll_after_ms": 10000
|
||||
}
|
||||
```
|
||||
|
||||
`+history-revert-status` 返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "partial_failed",
|
||||
"history_version_id": "11",
|
||||
"failed_block_tokens": ["blk_xxx"]
|
||||
}
|
||||
```
|
||||
|
||||
`status` 可能是 `running`、`done`、`partial_failed`、`failed`。当状态是 `partial_failed` 或 `failed` 时,优先检查 `failed_block_tokens`。
|
||||
|
||||
## 回滚后验证
|
||||
|
||||
回滚成功后必须读取一次当前内容确认:
|
||||
|
||||
```bash
|
||||
lark-cli slides +xml-get --presentation "<slides_url_or_token>" --output ./presentation.xml
|
||||
```
|
||||
|
||||
如果只需要快速检查返回结构,也可以走 raw OpenAPI:
|
||||
|
||||
```bash
|
||||
lark-cli api get "/open-apis/slides_ai/v1/xml_presentations/<xml_presentation_id>" \
|
||||
--params '{"revision_id":-1}'
|
||||
```
|
||||
@ -0,0 +1,126 @@
|
||||
|
||||
# slides +media-upload(上传本地图片到飞书幻灯片)
|
||||
|
||||
把本地图片上传到指定演示文稿的 drive 媒体库,返回 `file_token`。**返回的 token 作为 `<img src="...">` 的值塞进 slide XML 即可显示图片。**
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 直接传 xml_presentation_id
|
||||
lark-cli slides +media-upload --as user \
|
||||
--file ./pic.png \
|
||||
--presentation slidesXXXXXXXXXXXXXXXXXXXXXX
|
||||
|
||||
# 传 slides URL 也行
|
||||
lark-cli slides +media-upload --as user \
|
||||
--file ./chart.png \
|
||||
--presentation "https://xxx.feishu.cn/slides/slidesXXXXXXXXXXXXXXXXXXXXXX"
|
||||
|
||||
# 传 wiki URL(CLI 自动 wiki.spaces.get_node 解析为真实 token,校验 obj_type=slides)
|
||||
lark-cli slides +media-upload --as user \
|
||||
--file ./pic.png \
|
||||
--presentation "https://xxx.feishu.cn/wiki/wikcnXXXXXX"
|
||||
|
||||
# 预览(不实际上传)
|
||||
lark-cli slides +media-upload --file ./pic.png --presentation $PRES_ID --dry-run
|
||||
```
|
||||
|
||||
## 返回值
|
||||
|
||||
```json
|
||||
{
|
||||
"file_token": "boxcnXXXXXXXXXXXXXXXXXXXXXX",
|
||||
"file_name": "pic.png",
|
||||
"size": 12345,
|
||||
"presentation_id": "slidesXXXXXXXXXXXXXXXXXXXXXX"
|
||||
}
|
||||
```
|
||||
|
||||
- **`file_token`**:把它写进 `<img src="...">`
|
||||
- **`file_name` / `size`**:上传文件元信息
|
||||
- **`presentation_id`**:解析后的真实 `xml_presentation_id`(wiki URL 解析后会变化)
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--file` | 是 | 本地图片路径,**必须是 CWD 内的相对路径**(如 `./pic.png`)。**最大 20 MB**(slides upload API 不支持分片上传) |
|
||||
| `--presentation` | 是 | `xml_presentation_id`、`/slides/<token>` URL,或 `/wiki/<token>` URL |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **路径必须在 CWD 内**:`--file /abs/path/x.png` 或 `--file ../up/x.png` 会被 CLI 拒绝(报 `unsafe file path`)。如果素材在别的目录,先 `cd` 过去再执行。
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 给已有 PPT 加带图新页
|
||||
|
||||
```bash
|
||||
# 1) 上传图片
|
||||
TOKEN=$(lark-cli slides +media-upload --as user \
|
||||
--file ./pic.png \
|
||||
--presentation $PRES_ID | jq -r .data.file_token)
|
||||
|
||||
# 2) 用 file_token 创建带图新页
|
||||
lark-cli slides xml_presentation.slide create --as user \
|
||||
--params "{\"xml_presentation_id\":\"$PRES_ID\"}" \
|
||||
--data "{\"slide\":{\"content\":\"<slide xmlns=\\\"http://www.larkoffice.com/sml/2.0\\\"><data><img src=\\\"$TOKEN\\\" topLeftX=\\\"100\\\" topLeftY=\\\"100\\\" width=\\\"320\\\" height=\\\"180\\\"/></data></slide>\"}}"
|
||||
```
|
||||
|
||||
### 新建带图 PPT(推荐用 `+create --slides` 的 `@` 占位符,一步到位)
|
||||
|
||||
```bash
|
||||
# 不需要单独 +media-upload,写 src="@<本地路径>" 即可
|
||||
lark-cli slides +create --as user --title "图测试" --slides '[
|
||||
"<slide xmlns=\"http://www.larkoffice.com/sml/2.0\"><data><img src=\"@./pic.png\" topLeftX=\"100\" topLeftY=\"100\" width=\"320\" height=\"180\"/></data></slide>"
|
||||
]'
|
||||
```
|
||||
|
||||
详见 [+create 文档](lark-slides-create.md#本地图片path-占位符)。
|
||||
|
||||
### 给已有 PPT 的已有页加图
|
||||
|
||||
拿到 `file_token` 后走 [`+replace-slide`](lark-slides-replace-slide.md) 的 `block_insert`,不用搬原 XML、不改 `slide_id`、不打乱页序:
|
||||
|
||||
```bash
|
||||
PRES_ID=xxx
|
||||
SID=yyy # 要加图的那一页
|
||||
|
||||
# 1) 上传图片拿 file_token
|
||||
TOKEN=$(lark-cli slides +media-upload --as user \
|
||||
--file ./pic.png --presentation $PRES_ID | jq -r '.data.file_token')
|
||||
|
||||
# 2) block_insert 到页末(或用 insert_before_block_id 指定插入位置)
|
||||
lark-cli slides +replace-slide --as user \
|
||||
--presentation "$PRES_ID" --slide-id "$SID" \
|
||||
--parts "$(jq -n --arg token "$TOKEN" \
|
||||
'[{action:"block_insert",insertion:("<img src=\""+$token+"\" topLeftX=\"500\" topLeftY=\"100\" width=\"200\" height=\"150\"/>")}]')"
|
||||
```
|
||||
|
||||
注意事项:
|
||||
|
||||
1. **`<img>` 坐标避开现有元素** —— 先读现有元素 bbox 挑空白区;空间不够就先用 `block_replace` 挪动/缩小现有元素后再放图
|
||||
2. **`<img>` 的 `width:height` 对齐原图比例** —— 比例不一致会被裁剪,参见 [xml-schema-quick-ref.md](xml-schema-quick-ref.md) `<img>` 说明
|
||||
|
||||
## 工作原理
|
||||
|
||||
`+media-upload` 内部调用 `POST /open-apis/drive/v1/medias/upload_all`(单次上传,最大 20 MB),固定使用:
|
||||
|
||||
- `parent_type=slide_file`(slides 后端唯一接受的取值,已实测验证)
|
||||
- `parent_node=<xml_presentation_id>`
|
||||
|
||||
**不要尝试用 `slides_image`、`slide_image` 等 parent_type**——后端会返回 1061001 / 1061002 错误。这是 slides 的特殊约定。
|
||||
|
||||
## 常见错误
|
||||
|
||||
| 错误码 | 含义 | 解决方案 |
|
||||
|--------|------|----------|
|
||||
| 1061002 | params error / 不支持的 parent_type | 不要用原生 API 自己拼 parent_type;用 `+media-upload` 即可 |
|
||||
| 1061004 | forbidden:当前身份对该演示文稿无编辑权限 | 确认当前身份(user 或 bot)对目标 PPT 有编辑权限。bot 模式常见原因:PPT 不是该 bot 创建的——可用 `+create --as bot` 新建,或以 user 身份给 bot 授权 `lark-cli drive permission.members create --as user ...` |
|
||||
| 1061044 | parent node not exist | `--presentation` 给的 token 不对,或不是 slides 类型 |
|
||||
| 403 | 权限不足 | 检查 `docs:document.media:upload` scope;wiki URL 还需要 `wiki:node:read` |
|
||||
|
||||
## 相关命令
|
||||
|
||||
- [+create](lark-slides-create.md) — 新建 PPT(支持 `@` 占位符自动上传图片)
|
||||
- [+replace-slide](lark-slides-replace-slide.md) — 给已有页加图 / 换图(`block_insert` / `block_replace`)
|
||||
- [xml_presentation.slide create](lark-slides-xml-presentation-slide-create.md) — 创建 slide 页面(拿到 file_token 后塞进 XML)
|
||||
@ -0,0 +1,85 @@
|
||||
# PPT Template Rewrite Principles
|
||||
|
||||
核心原则:模板不是风格参考,而是必须沿用的编辑底稿。
|
||||
|
||||
## Import First
|
||||
|
||||
如果用户提供的模板是 PPTX 格式,先把模板导入成 Lark Slides。后续写入目标是导入后的 Slides,不是新建一个脱离模板的 deck,也不是先在本地重画 PPTX 再导入。
|
||||
|
||||
直接使用以下命令,不需要先加载 `lark-drive` Skill:
|
||||
|
||||
```bash
|
||||
lark-cli drive +import --as user --file "<template.pptx>" --type slides --json
|
||||
```
|
||||
|
||||
可选参数:用 `--name "<title>"` 指定导入后的 Slides 标题;用 `--folder-token <FOLDER_TOKEN>` 指定目标文件夹。若返回 `ready=false` / `timed_out=true`,直接执行返回值里的 `next_command`;等价形式是:
|
||||
|
||||
```bash
|
||||
lark-cli drive +task_result --scenario import --ticket <TICKET>
|
||||
```
|
||||
|
||||
## Read Before Editing
|
||||
|
||||
导入后必须阅读 Slides 内容,理解每页的真实版式、字体、层级、图片、图表、shape、表格和文本容器。阅读结果是后续编辑的事实来源。
|
||||
|
||||
阅读页面时至少判断:
|
||||
|
||||
- 该页原本承担的角色,例如封面、章节页、目录、流程、对比、数据、总结。
|
||||
- 该页的主要版式结构,例如图文关系、箭头、时间线、节点、表格、图表、左右对照、背景图或产品图。
|
||||
- 哪些文本框、shape 标签、表格单元格或图表标签承载内容。
|
||||
- 原页面的字体、字号、颜色、对齐、层级和留白关系。
|
||||
|
||||
## Edit The Imported Slides Directly
|
||||
|
||||
理解页面后,直接在导入后的 Slides 上编辑。允许的操作包括:
|
||||
|
||||
- 填写、替换、凝练或删除文字。
|
||||
- 替换或补充图片。
|
||||
- 更新图表、表格、数字标签或节点标签里的内容。
|
||||
- 按需复制、删除或重排模板页。
|
||||
- 在源页面没有合适承载位置时,做局部、小范围新增元素。
|
||||
|
||||
新增元素只能补足内容缺口,不能成为新的主版式。页面主体仍应由模板原有版式承载。
|
||||
|
||||
## Preserve Design
|
||||
|
||||
编辑必须严格沿用原版式和字体,只改内容,不做设计。
|
||||
|
||||
默认保留:
|
||||
|
||||
- 页面布局、视觉层级、留白和对齐关系。
|
||||
- 原字体、字号体系、颜色、文本框位置和 shape 顺序。
|
||||
- 背景图、图片、logo、图表、表格、装饰形状、线条、图标和页面结构。
|
||||
- 模板中不同页型之间的差异。
|
||||
|
||||
不要把模板页改造成统一的通用卡片、空白板式布局、标题栏、三栏、2x2 卡片或大面积遮罩。不要把模板当作背景图后另起一套设计系统。
|
||||
|
||||
## Content Only
|
||||
|
||||
内容必须优先进入原页面已有的文本框、shape 标签、节点、表格单元格、图表标签或注释容器。
|
||||
|
||||
如果原容器空间不足,优先:
|
||||
|
||||
- 凝练文字。
|
||||
- 降低字号但保持原字体体系。
|
||||
- 拆分到页面已有的邻近容器。
|
||||
- 使用模板已有的注释、标签或补充说明区域。
|
||||
- 复制同页或同模板中的原生容器样式做局部补充。
|
||||
|
||||
不要为了容纳长文案而重画页面主体结构。不要用新增大卡片遮住原图表、箭头、图片、背景或关键 shape。
|
||||
|
||||
## Readback And Tune
|
||||
|
||||
完成编辑后必须回读结果,并逐页微调。
|
||||
|
||||
回读时重点检查:
|
||||
|
||||
- 文字是否溢出、截断、压线或超出容器。
|
||||
- 文本是否遮挡图片、图表、shape、箭头、节点或其他文字。
|
||||
- shape 顺序是否导致内容被覆盖或遮住。
|
||||
- 新内容是否仍然落在模板原有版式中,而不是覆盖模板结构。
|
||||
- 字体、字号、颜色、对齐和层级是否仍贴近原页。
|
||||
|
||||
发现文字溢出时,优先凝练文字或缩减字号。发现遮挡时,调整 shape 顺序、局部位置或复用原有空白区域解决。只有在这些方法都不能满足内容表达时,才做局部新增或删除。
|
||||
|
||||
完成标准是“原模板的版式、字体和视觉结构仍清晰存在,内容已经被准确替换,并且回读后没有溢出和遮挡”。
|
||||
@ -0,0 +1,95 @@
|
||||
# slides +replace-pages(多页整页重建)
|
||||
|
||||
批量替换已有演示文稿里的多个页面,保持原 `xml_presentation_id` 和原 Slides 链接不变。适合多页版式大改、坐标重排、整页视觉重建;单个文本框、图片或 shape 的局部编辑仍优先用 [`+replace-slide`](lark-slides-replace-slide.md)。
|
||||
|
||||
> 重要:这是多步编排,不是后端原子事务。CLI 对每页执行“先创建新页到旧页前,再删除旧页”;创建失败时旧页会保留。删除失败时可能出现新旧页同时存在,需要按返回结果继续处理。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
lark-cli slides +replace-pages \
|
||||
--as user \
|
||||
--presentation <slides_url_or_xml_presentation_id> \
|
||||
--pages @pages.json
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必需 | 说明 |
|
||||
|------|------|------|
|
||||
| `--presentation` | 是 | `xml_presentation_id`、`/slides/` URL 或 `/wiki/` URL |
|
||||
| `--pages` | 是 | JSON 数组,每项包含 `slide_id` 和 `content`;支持 literal、`@file`、stdin `-` |
|
||||
| `--dry-run` | 否 | 基于 `slide_id` 输入输出替换计划,不执行 create/delete |
|
||||
| `--continue-on-error` | 否 | 默认失败即停;开启后继续处理后续页,并在结果中标记失败项 |
|
||||
| `--validate-only` | 否 | 只校验输入并生成替换计划,不执行 Slides get/create/delete |
|
||||
|
||||
## pages.json
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"slide_id": "slide_short_id_1",
|
||||
"content": "<slide xmlns=\"http://www.larkoffice.com/sml/2.0\"><data></data></slide>"
|
||||
},
|
||||
{
|
||||
"slide_id": "slide_short_id_2",
|
||||
"content": "<slide xmlns=\"http://www.larkoffice.com/sml/2.0\"><data></data></slide>"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- 每项必须提供 `slide_id`;不支持 `slide_number`。
|
||||
- `content` 必须是完整 `<slide>...</slide>` XML。
|
||||
- 同一批次不能重复 `slide_id`。
|
||||
- CLI 不会回读整份 presentation;如果 `slide_id` 已失效,create/delete 阶段会返回对应错误。
|
||||
|
||||
## Dry Run
|
||||
|
||||
```bash
|
||||
lark-cli slides +replace-pages --as user \
|
||||
--presentation "$PID" \
|
||||
--pages @pages.json \
|
||||
--dry-run
|
||||
```
|
||||
|
||||
输出包含 `xml_presentation_id`、`pages_count`、`plan`,以及每页的 `old_slide_id`、`insert_before_slide_id` 和动作 `create_before_then_delete_old`。Dry-run 只基于输入的 `slide_id` 构造计划,不会调用 `xml_presentations.get`,也不会执行 create/delete。
|
||||
|
||||
## 成功输出
|
||||
|
||||
```json
|
||||
{
|
||||
"xml_presentation_id": "xxx",
|
||||
"pages_count": 2,
|
||||
"status": "completed",
|
||||
"summary": {
|
||||
"replaced": 2,
|
||||
"failed": 0,
|
||||
"total": 2
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"old_slide_id": "old3",
|
||||
"new_slide_id": "new3",
|
||||
"status": "replaced"
|
||||
}
|
||||
],
|
||||
"revision_id": 123
|
||||
}
|
||||
```
|
||||
|
||||
如果使用 `--continue-on-error` 且任一页面失败,CLI 会继续处理后续页,但最终以 partial failure 非零退出;stdout 仍保留完整 `results`,顶层 `ok` 为 `false`,`status` 为 `partial_failure`。
|
||||
|
||||
`status` 可能为:
|
||||
|
||||
- `replaced`:新页创建成功,旧页删除成功。
|
||||
- `create_failed`:新页创建失败,旧页保留。
|
||||
- `delete_failed`:新页已创建,但旧页删除失败。
|
||||
|
||||
## 使用建议
|
||||
|
||||
1. 大幅改写前先 `slides +xml-get` 保存当前 XML,并记录要替换页面的 `slide_id`。
|
||||
2. 生成只含 `slide_id` 的 `pages.json` 后先跑 `--dry-run` 或 `--validate-only`。
|
||||
3. 默认不要开 `--continue-on-error`,除非能接受部分页面已替换。
|
||||
4. 替换后再回读全文 XML 并截图检查,确认页序、视觉和文本没有破损。
|
||||
@ -0,0 +1,237 @@
|
||||
# slides +replace-slide(块级替换 / 插入)
|
||||
|
||||
对指定 slide 做块级替换或插入。编辑已有 PPT 的主路径——`slide_id` 不变、页序不动、只影响被指定的块。
|
||||
|
||||
相比直接调 `xml_presentation.slide.replace`,这个 shortcut 的四个额外价值:
|
||||
|
||||
1. `--presentation` 接受 `xml_presentation_id` / `/slides/` URL / `/wiki/` URL(wiki 自动解析);
|
||||
2. `block_replace` 的 `replacement` 根元素 `id="<block_id>"` 由 CLI 自动注入——底层 API 的硬约束(不注入返回 3350001);直接调原生 API 需自己加,用 Shortcut 则自动注入;
|
||||
3. `<shape>` 元素缺少 `<content/>` 子元素时由 CLI 自动注入——SML 2.0 schema 要求每个 `<shape>` 必须有 `<content/>` 子元素,缺失同样触发 3350001;自闭合的 `<shape .../>` 也会被自动展开为 `<shape ...><content/></shape>`;
|
||||
4. 3350001 错误时提供上下文感知的 hint,帮助 AI agent 和用户快速定位原因。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# block_insert:在页末追加一个新元素
|
||||
lark-cli slides +replace-slide --as user \
|
||||
--presentation slidesXXXXXXXXXXXXXXXXXXXXXX \
|
||||
--slide-id pfG \
|
||||
--parts '[{"action":"block_insert","insertion":"<shape type=\"rect\" topLeftX=\"500\" topLeftY=\"100\" width=\"200\" height=\"100\"/>"}]'
|
||||
|
||||
# block_replace:已知某块 id,整块替换(replacement 根 id 自动注入为 bUn)
|
||||
lark-cli slides +replace-slide --as user \
|
||||
--presentation slidesXXXXXXXXXXXXXXXXXXXXXX \
|
||||
--slide-id pfG \
|
||||
--parts '[{"action":"block_replace","block_id":"bUn","replacement":"<shape type=\"text\" topLeftX=\"80\" topLeftY=\"80\" width=\"800\" height=\"120\"><content textType=\"title\"><p>新标题</p></content></shape>"}]'
|
||||
|
||||
# 大 --parts 走文件或 stdin(auto-gen 命令不支持 @file,但 shortcut 支持)
|
||||
lark-cli slides +replace-slide --as user \
|
||||
--presentation $PID --slide-id $SID --parts @parts.json
|
||||
cat parts.json | lark-cli slides +replace-slide --as user \
|
||||
--presentation $PID --slide-id $SID --parts -
|
||||
|
||||
# wiki URL 直接传(CLI 自动 get_node → 拿真实 xml_presentation_id)
|
||||
lark-cli slides +replace-slide --as user \
|
||||
--presentation "https://xxx.feishu.cn/wiki/wikcnXXXXXX" --slide-id pfG \
|
||||
--parts '[{"action":"block_insert","insertion":"<shape type=\"rect\" width=\"100\" height=\"100\"/>"}]'
|
||||
|
||||
# 预览(不实际调用)
|
||||
lark-cli slides +replace-slide --as user \
|
||||
--presentation $PID --slide-id $SID --parts "$PARTS" --dry-run
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--presentation` | 是 | `xml_presentation_id`、`/slides/<token>` URL,或 `/wiki/<token>` URL |
|
||||
| `--slide-id` | 是 | 页面 ID(`xml_presentation.slide.get` / `slides +xml-get` 都能拿到) |
|
||||
| `--parts` | 是 | JSON 数组(`[{...}, ...]`),单次最多 200 条。支持 `@<file>` 和 `-`(stdin)读取 |
|
||||
| `--revision-id` | 否 | 基础版本号;默认 `-1` 表示基于最新版执行;传具体版本号时,服务端以该版本为 base 执行;**传不存在的版本号(超过当前 revision)返回 3350002** |
|
||||
| `--tid` | 否 | 并发事务 ID;多人协作长事务才用,单次单人调用留空 |
|
||||
|
||||
## parts 元素结构
|
||||
|
||||
> **限制**:最多 200 条;`block_replace` 和 `block_insert` 可以在同一批次混用。**其他 action(含 `str_replace`)CLI 会直接报错拒绝**。
|
||||
|
||||
每条 part 按 `action` 取不同字段:
|
||||
|
||||
### action = `block_replace`
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `action` | 是 | `"block_replace"` |
|
||||
| `block_id` | 是 | 目标块的 3 位 short element ID(从 `slide.get` 返回 XML 里读) |
|
||||
| `replacement` | 是 | 新 XML 片段;**根元素 `id` 会被 CLI 自动注入为 `block_id`**,用户不用自己加(如果已经加了且不一致会被覆盖为正确值) |
|
||||
|
||||
### action = `block_insert`
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `action` | 是 | `"block_insert"` |
|
||||
| `insertion` | 是 | 要插入的 XML 片段 |
|
||||
| `insert_before_block_id` | 否 | 插到这个块之前;省略(不提供此字段)则追加到页末 |
|
||||
|
||||
## 合法根元素速查
|
||||
|
||||
`block_replace.replacement` 和 `block_insert.insertion` 必须以 SML 2.0 定义的合法元素为根。完整权威定义看 [`slides_xml_schema_definition.xml`](slides_xml_schema_definition.xml);这里只列能作为**根**的类型 + 每种类型的最小可工作片段。
|
||||
|
||||
| 元素 | 用途 | 关键点 |
|
||||
|---|---|---|
|
||||
| `<shape>` | 矩形/椭圆/三角/文本框等所有形状 | `type` 必填;`<content/>` 缺失时 CLI 会自动注入 |
|
||||
| `<line>` | 直线 | 需 `startX/startY/endX/endY` |
|
||||
| `<polyline>` | 折线 | `points` 读回时被服务端规整丢弃(几何已入库) |
|
||||
| `<img>` | 图片 | `src` 必须是 [`+media-upload`](lark-slides-media-upload.md) 返回的 `file_token`,不能是 URL |
|
||||
| `<icon>` | 图标 | `iconType` 取自 iconpark 资源;语义图标先用 `scripts/iconpark_tool.py search` 检索 |
|
||||
| `<table>` | 表格 | 整表替换会**重建内部 td id**,旧 td block_id 立即失效 |
|
||||
| `<td>` | 单元格局部替换 | 只能 `block_replace`,不能 `block_insert`;`block_id` 必须是最新 `slide.get` 拿到的 td id |
|
||||
| `<chart>` | 图表(line/bar/column/pie/area/radar/combo) | 必须嵌 `<chartPlotArea>` + `<chartData>` + `<dim1>/<dim2>/<chartField>` |
|
||||
|
||||
**不可作为根元素**:
|
||||
|
||||
- `<video>` / `<audio>` —— SML 2.0 没有这两个原生元素;`<undefined type="video|audio">` 是**导出时**的占位符(服务端遇到不支持的类型时用它代替),**不能写入**。尝试 insert/replace 都会返回 3350001。
|
||||
|
||||
### 最小 XML 片段(JSON 嵌入时记得把 `"` 转义成 `\"`)
|
||||
|
||||
`<shape>`(文本框;`type` 还可选 `rect`/`ellipse`/`triangle`/`custom` 等):
|
||||
```xml
|
||||
<shape type="text" topLeftX="80" topLeftY="80" width="800" height="120">
|
||||
<content textType="title"><p>标题</p></content>
|
||||
</shape>
|
||||
```
|
||||
|
||||
`<img>`:
|
||||
```xml
|
||||
<img src="{file_token}" topLeftX="600" topLeftY="20" width="80" height="80"/>
|
||||
```
|
||||
|
||||
`<polyline>`:
|
||||
```xml
|
||||
<polyline topLeftX="10" topLeftY="10" width="100" height="50" points="0,0 50,50 100,0"/>
|
||||
```
|
||||
|
||||
`<table>`(2×2):
|
||||
```xml
|
||||
<table topLeftX="30" topLeftY="80">
|
||||
<colgroup><col span="2" width="110"/></colgroup>
|
||||
<tr><td><content><p>A</p></content></td><td><content><p>B</p></content></td></tr>
|
||||
<tr><td><content><p>C</p></content></td><td><content><p>D</p></content></td></tr>
|
||||
</table>
|
||||
```
|
||||
|
||||
`<td>`(`block_replace` 单元格;`block_id` 必须是最新 `slide.get` 拿到的 td id):
|
||||
```xml
|
||||
<td><content><p>新内容</p></content></td>
|
||||
```
|
||||
|
||||
`<chart>`(`type` 改成 `bar`/`column`/`pie`/`area`/`radar`/`combo` 切换图型):
|
||||
```xml
|
||||
<chart topLeftX="30" topLeftY="300" width="300" height="200">
|
||||
<chartPlotArea><chartPlot type="line"/></chartPlotArea>
|
||||
<chartData>
|
||||
<dim1><chartField name="x" valueType="string">Q1,Q2,Q3,Q4</chartField></dim1>
|
||||
<dim2><chartField name="Sales" valueType="number">10,20,15,30</chartField></dim2>
|
||||
</chartData>
|
||||
</chart>
|
||||
```
|
||||
|
||||
## 返回值
|
||||
|
||||
```json
|
||||
{
|
||||
"xml_presentation_id": "slidesXXXXXXXXXXXXXXXXXXXXXX",
|
||||
"slide_id": "pfG",
|
||||
"parts_count": 1,
|
||||
"revision_id": 102
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `xml_presentation_id` | 解析后的真实 token(wiki URL 解析后会变化) |
|
||||
| `slide_id` | 与入参一致 |
|
||||
| `parts_count` | 本次提交的 parts 条数 |
|
||||
| `revision_id` | 成功后的新版本号,下次做乐观锁时用 |
|
||||
| `failed_part_index` | 有部分失败时存在,指向第几条 part 失败 |
|
||||
| `failed_reason` | 失败原因文字描述 |
|
||||
|
||||
整批作为原子事务:任一 part 失败则整批不生效,服务端通过 `failed_part_index` / `failed_reason` 告诉你是哪条;按此定位修正后重发。
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 给已有页加图(典型场景)
|
||||
|
||||
```bash
|
||||
PID=xxx
|
||||
SID=yyy
|
||||
|
||||
# 1) 上传图片
|
||||
TOKEN=$(lark-cli slides +media-upload --as user \
|
||||
--file ./pic.png --presentation "$PID" | jq -r '.data.file_token')
|
||||
|
||||
# 2) block_insert 到页末
|
||||
lark-cli slides +replace-slide --as user \
|
||||
--presentation "$PID" --slide-id "$SID" \
|
||||
--parts "$(jq -n --arg token "$TOKEN" \
|
||||
'[{action:"block_insert",insertion:("<img src=\""+$token+"\" topLeftX=\"500\" topLeftY=\"100\" width=\"200\" height=\"150\"/>")}]')"
|
||||
```
|
||||
|
||||
### 改标题(block_replace)
|
||||
|
||||
```bash
|
||||
# 先拿原页 XML,从里面找到标题块的 3 位 short id(如 bUn)
|
||||
lark-cli slides xml_presentation.slide get --as user \
|
||||
--params "{\"xml_presentation_id\":\"$PID\",\"slide_id\":\"$SID\"}"
|
||||
|
||||
# block_replace 换掉整个标题块(id 自动注入)
|
||||
lark-cli slides +replace-slide --as user \
|
||||
--presentation "$PID" --slide-id "$SID" \
|
||||
--parts '[{"action":"block_replace","block_id":"bUn","replacement":"<shape type=\"text\" topLeftX=\"80\" topLeftY=\"80\" width=\"800\" height=\"120\"><content textType=\"title\"><p>新标题</p></content></shape>"}]'
|
||||
```
|
||||
|
||||
### 批量:一次换标题 + 追加装饰图
|
||||
|
||||
`block_replace` 和 `block_insert` 可以在同一个 `--parts` 里混用,整批原子执行。
|
||||
|
||||
```bash
|
||||
lark-cli slides +replace-slide --as user \
|
||||
--presentation "$PID" --slide-id "$SID" \
|
||||
--parts '[
|
||||
{"action":"block_replace","block_id":"bab","replacement":"<shape type=\"text\" topLeftX=\"80\" topLeftY=\"80\" width=\"800\" height=\"120\"><content textType=\"title\"><p>新标题</p></content></shape>"},
|
||||
{"action":"block_insert","insertion":"<img src=\"<file_token>\" topLeftX=\"700\" topLeftY=\"400\" width=\"180\" height=\"100\"/>"}
|
||||
]'
|
||||
```
|
||||
|
||||
### 乐观锁
|
||||
|
||||
```bash
|
||||
# 读时记录 revision_id
|
||||
REV=$(lark-cli slides xml_presentation.slide get --as user \
|
||||
--params "{\"xml_presentation_id\":\"$PID\",\"slide_id\":\"$SID\"}" \
|
||||
| jq '.data.revision_id')
|
||||
|
||||
# 写时传 --revision-id;传不存在的版本号(超过当前 revision)返回 3350002
|
||||
lark-cli slides +replace-slide --as user \
|
||||
--presentation "$PID" --slide-id "$SID" --revision-id "$REV" \
|
||||
--parts "$PARTS"
|
||||
```
|
||||
|
||||
## 常见错误
|
||||
|
||||
| 现象 | 原因 | 对策 |
|
||||
|------|------|------|
|
||||
| 3350001 + hint "block_id not found" | `parts[i].block_id` 在当前页不存在 | 重新 `slide.get` 拿最新 XML,按里面的 short ID 再填 |
|
||||
| 3350002 not found | `--revision-id` 传了不存在的版本号(超过当前 revision) | 用 `-1` 或用 `slide.get` 拿到的有效 `revision_id` |
|
||||
| `--parts[i] action "str_replace" is not supported` | CLI 不暴露 `str_replace` | 把替换需求改写成 `block_replace` / `block_insert` |
|
||||
| `--parts contains N items, exceeds maximum of 200` | 一次提交 parts 太多 | 拆多次调用 |
|
||||
| `--parts[i] (block_replace) requires non-empty block_id` / `replacement` | 字段缺失 | 按 parts 元素结构补齐 |
|
||||
| `<img>` 不显示 / 显示破图 | `src` 写了外链 URL | 换成通过 [`+media-upload`](lark-slides-media-upload.md) 拿到的 `file_token` |
|
||||
| 3350001 | `replacement` 不是合法单根 XML 片段,或 `block_id` 不存在 | CLI 已自动注入 `id` 和 `<content/>`;如果仍报错,重新 `slide.get` 拿最新 XML 确认 `block_id` 存在;检查 XML 结构是否合法;坐标是否超出 960×540 |
|
||||
| 403 | 权限不足 | 需要 `slides:presentation:update` 或 `slides:presentation:write_only`;wiki URL 还需要 `wiki:node:read` |
|
||||
|
||||
## 相关命令
|
||||
|
||||
- [xml_presentation.slide get](lark-slides-xml-presentation-slide-get.md) — 读原页拿 `block_id` / `revision_id`
|
||||
- [xml_presentation.slide replace](lark-slides-xml-presentation-slide-replace.md) — 底层 replace API 参考
|
||||
- [+media-upload](lark-slides-media-upload.md) — 上传图片拿 `file_token`
|
||||
- [lark-slides-edit-workflows.md](lark-slides-edit-workflows.md) — 读-改-写闭环 + 决策树
|
||||
@ -0,0 +1,97 @@
|
||||
# slides +screenshot
|
||||
|
||||
## 用途
|
||||
|
||||
获取幻灯片页面截图并保存为本地图片文件。默认用于已存在 PPT 页面截图;传入 `--content` 时用于直接渲染单个 `<slide>` XML 片段预览。本 shortcut 会在 CLI 进程内解码并写入文件,stdout 只返回文件路径、大小、页面 ID 等元信息,避免把图片 Base64 输出给模型。
|
||||
|
||||
截图失败则降级到 XML 读回、结构 lint等非截图检查路径。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
lark-cli slides +screenshot --as user \
|
||||
--presentation '<xml_presentation_id 或 slides/wiki URL>' \
|
||||
--slide-number 1
|
||||
```
|
||||
|
||||
渲染本地 XML 内容:
|
||||
|
||||
```bash
|
||||
lark-cli slides +screenshot --as user \
|
||||
--content @slide.xml
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必需 | 说明 |
|
||||
|------|------|------|
|
||||
| `--presentation` | list 模式必需 | `xml_presentation_id`、`/slides/` URL,或解析后为 slides 的 `/wiki/` URL。传 `--content` 时不能使用 |
|
||||
| `--slide-id` | list 模式至少提供 `--slide-id` / `--slide-number` 之一 | 页面 short ID;多页截图时重复传入,或用逗号分隔一次传多个(如 `--slide-id slide_1,slide_2`);一次最多 10 页(`--slide-id` + `--slide-number` 合计小于等于 10) |
|
||||
| `--slide-number` | list 模式至少提供 `--slide-id` / `--slide-number` 之一 | 页面页号;多页截图时重复传入,或用逗号分隔一次传多个(如 `--slide-number 1,2,3`);一次最多 10 页(`--slide-id` + `--slide-number` 合计小于等于 10) |
|
||||
| `--content` | render 模式必需 | 要直接渲染的 `<slide>` XML 片段;支持直接传值、`@file`、`-` stdin。传入后不能同时传 `--slide-id` / `--slide-number` |
|
||||
| `--output-dir` | 否 | 输出目录,默认 `.lark-slides/screenshots`;必须是当前目录内的相对路径 |
|
||||
| `--output-name` | 否 | render 模式的输出文件名 stem;未指定时优先用返回的 `slide_id`,否则用 `rendered-slide`。若目标文件已存在,会自动追加递增后缀避免覆盖 |
|
||||
|
||||
## 示例
|
||||
|
||||
### 单页截图
|
||||
|
||||
```bash
|
||||
lark-cli slides +screenshot --as user \
|
||||
--presentation slides_example_presentation_id \
|
||||
--slide-number 1
|
||||
```
|
||||
|
||||
### 多页截图
|
||||
|
||||
一次不要超过 10 页;如需更多页面,分批调用。可以重复传参,也可以用逗号分隔一次传多个:
|
||||
|
||||
```bash
|
||||
lark-cli slides +screenshot --as user \
|
||||
--presentation slides_example_presentation_id \
|
||||
--slide-number 1 \
|
||||
--slide-number 2 \
|
||||
--output-dir .lark-slides/screenshots/demo
|
||||
```
|
||||
|
||||
### 渲染 XML 预览
|
||||
|
||||
```bash
|
||||
lark-cli slides +screenshot --as user \
|
||||
--content @.lark-slides/out/demo/slide.xml \
|
||||
--output-name preview
|
||||
```
|
||||
|
||||
## 返回值
|
||||
|
||||
返回 JSON 不包含 Base64 图片内容:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"identity": "user",
|
||||
"data": {
|
||||
"xml_presentation_id": "slides_example_presentation_id",
|
||||
"output_dir": ".lark-slides/screenshots",
|
||||
"screenshots": [
|
||||
{
|
||||
"slide_id": "slide_example_id",
|
||||
"slide_number": 1,
|
||||
"format": "png",
|
||||
"path": "/abs/path/.lark-slides/screenshots/slides_example_presentation_id_p001_slide_example_id.png",
|
||||
"size": 12345
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 优先使用 `slides +screenshot` 保存本地图片,不要把图片 Base64 打到 stdout。
|
||||
2. 已存在 PPT 页面截图时,不传 `--content`,用 `--presentation` + `--slide-id` 或 `--slide-number`。
|
||||
3. 本地 XML 预览时,传 `--content @file` 或 `--content -`,内容应为单个 `<slide>` XML 片段;此时不要传 `--presentation` / `--slide-id` / `--slide-number`。
|
||||
4. `slide_id` 是页面 short ID,页码请用 `--slide-number`。
|
||||
5. list 模式一次最多传 10 页(`--slide-id` + `--slide-number` 合计小于等于 10);更多页面请分批截图。
|
||||
6. list 模式默认文件名包含 presentation ID、页码和/或 slide ID;文件已存在时自动追加 `_2`、`_3` 等后缀,避免覆盖旧截图。
|
||||
7. 截图来自服务端渲染结果,适合创建/替换后验证页面是否为空白、破图或布局明显异常。
|
||||
@ -0,0 +1,219 @@
|
||||
# lark-slides xml_presentation.slide create
|
||||
|
||||
## 用途
|
||||
|
||||
在指定的 XML 演示文稿中创建新的幻灯片页面,通常用于给 `slides +create` 创建出的空白 PPT 逐页补充内容。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
lark-cli slides xml_presentation.slide create --as user --params '<json_params>' --data '<json_data>'
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `--params` | JSON string | 是 | 路径参数与查询参数 |
|
||||
| `--data` | JSON string | 是 | 请求体,包含新页面内容 |
|
||||
|
||||
### params JSON 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"xml_presentation_id": "slides_example_presentation_id",
|
||||
"revision_id": -1,
|
||||
"tid": "idMock"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `xml_presentation_id` | string | 是 | 目标演示文稿的唯一标识符 |
|
||||
| `revision_id` | integer | 否 | 演示文稿版本号,`-1` 表示最新版本 |
|
||||
| `tid` | string | 否 | 锁的事务 ID |
|
||||
|
||||
### data JSON 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"slide": {
|
||||
"slide_id": "slide_example_id",
|
||||
"content": "<slide xmlns=\"http://www.larkoffice.com/sml/2.0\">...</slide>"
|
||||
},
|
||||
"before_slide_id": "slide_before_target"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `slide.slide_id` | string | 否 | 幻灯片页面 short ID |
|
||||
| `slide.content` | string | 否 | 新幻灯片的 XML 内容 |
|
||||
| `before_slide_id` | string | 否 | 插入到指定页面之前 |
|
||||
|
||||
## slide XML 结构
|
||||
|
||||
`slide.content` 是一个完整的 `<slide>` 元素,遵循 SML 2.0 Schema:
|
||||
|
||||
```xml
|
||||
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
||||
<data>
|
||||
<shape type="text" topLeftX="80" topLeftY="80" width="800" height="120">
|
||||
<content textType="title">
|
||||
<p>标题</p>
|
||||
</content>
|
||||
</shape>
|
||||
</data>
|
||||
</slide>
|
||||
```
|
||||
|
||||
详细格式请参考 [xml-schema-quick-ref.md](xml-schema-quick-ref.md)。
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 在末尾添加幻灯片
|
||||
|
||||
```bash
|
||||
lark-cli slides xml_presentation.slide create --as user --params '{
|
||||
"xml_presentation_id": "slides_example_presentation_id"
|
||||
}' --data '{
|
||||
"slide": {
|
||||
"content": "<slide xmlns=\"http://www.larkoffice.com/sml/2.0\"><data><shape type=\"text\" topLeftX=\"80\" topLeftY=\"80\" width=\"800\" height=\"120\"><content textType=\"title\"><p>新页面标题</p></content></shape><shape type=\"text\" topLeftX=\"80\" topLeftY=\"200\" width=\"800\" height=\"180\"><content textType=\"body\"><p>内容文本</p></content></shape></data></slide>"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### 在指定页面前插入幻灯片
|
||||
|
||||
```bash
|
||||
lark-cli slides xml_presentation.slide create --as user --params '{
|
||||
"xml_presentation_id": "slides_example_presentation_id"
|
||||
}' --data '{
|
||||
"slide": {
|
||||
"content": "<slide xmlns=\"http://www.larkoffice.com/sml/2.0\"><data><shape type=\"text\" topLeftX=\"80\" topLeftY=\"80\" width=\"800\" height=\"120\"><content textType=\"title\"><p>插入的标题页</p></content></shape></data></slide>"
|
||||
},
|
||||
"before_slide_id": "slide_before_target"
|
||||
}'
|
||||
```
|
||||
|
||||
### 带图形元素的幻灯片
|
||||
|
||||
```bash
|
||||
lark-cli slides xml_presentation.slide create --as user --params '{
|
||||
"xml_presentation_id": "slides_example_presentation_id"
|
||||
}' --data '{
|
||||
"slide": {
|
||||
"content": "<slide xmlns=\"http://www.larkoffice.com/sml/2.0\"><data><shape type=\"text\" topLeftX=\"80\" topLeftY=\"80\" width=\"520\" height=\"120\"><content textType=\"title\"><p>数据展示</p></content></shape><shape type=\"rect\" topLeftX=\"700\" topLeftY=\"100\" width=\"200\" height=\"150\"><fill><fillColor color=\"rgb(100, 149, 237)\"/></fill></shape></data></slide>"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### 从文件读取 XML
|
||||
|
||||
```bash
|
||||
# 先创建 slide.xml 文件
|
||||
cat > slide.xml << 'EOF'
|
||||
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
||||
<data>
|
||||
<shape type="text" topLeftX="80" topLeftY="80" width="800" height="120">
|
||||
<content textType="title">
|
||||
<p>从文件加载</p>
|
||||
</content>
|
||||
</shape>
|
||||
<shape type="text" topLeftX="80" topLeftY="200" width="800" height="180">
|
||||
<content textType="body">
|
||||
<p>这是从文件读取的幻灯片内容</p>
|
||||
</content>
|
||||
</shape>
|
||||
</data>
|
||||
</slide>
|
||||
EOF
|
||||
|
||||
# 然后创建幻灯片
|
||||
lark-cli slides xml_presentation.slide create --as user \
|
||||
--params '{"xml_presentation_id":"slides_example_presentation_id"}' \
|
||||
--data "$(jq -n --arg content "$(cat slide.xml)" '{slide:{content:$content}}')"
|
||||
```
|
||||
|
||||
## 返回值
|
||||
|
||||
成功时返回创建的幻灯片信息:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"slide_id": "slide_example_id",
|
||||
"revision_id": 100
|
||||
},
|
||||
"msg": "success"
|
||||
}
|
||||
```
|
||||
|
||||
### 返回字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data.slide_id` | string | 新幻灯片的唯一标识 |
|
||||
| `data.revision_id` | integer | 演示文稿最新版本号 |
|
||||
|
||||
## slide 元素可用子元素
|
||||
|
||||
| 元素 | 说明 |
|
||||
|------|------|
|
||||
| `<style>` | 页面样式(背景填充) |
|
||||
| `<data>` | 图形元素容器(shape、img、table、chart 等) |
|
||||
| `<note>` | 演讲者备注 |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **本地图片必须先上传**:`xml_presentation.slide.create` 不识别 `@./local.png` 占位符(那是 `+create --slides` 的语法糖)。直接调本接口添加带图新页时,必须先用 [`slides +media-upload`](lark-slides-media-upload.md) 拿到 `file_token`,再写进 `<img src="<file_token>">`。
|
||||
>
|
||||
> 如果是从零开始建带图 PPT,**强烈建议改用 [`slides +create --slides '[...]'`](lark-slides-create.md#本地图片path-占位符)** 一步搞定(自动上传 + 替换 token)。
|
||||
|
||||
## 常见错误
|
||||
|
||||
| 错误码 | 含义 | 解决方案 |
|
||||
|--------|------|----------|
|
||||
| 404 | 演示文稿不存在 | 检查 `xml_presentation_id` 是否正确 |
|
||||
| 400 | XML 格式错误 | 检查 `slide.content` 是否是完整 `<slide>` 元素 |
|
||||
| 400 | 请求体结构错误 | 检查是否按 `slide.content` 和 `before_slide_id` 包装 |
|
||||
| 403 | 权限不足 | 检查是否拥有 `slides:presentation:update` 或 `slides:presentation:write_only` scope |
|
||||
| 3350001 | XML 非 well-formed 或服务端参数校验失败 | 优先检查未转义字符:文本 `Q&A -> Q&A`,文本 `<` / `>` 写成 `<` / `>`,属性 URL `a=1&b=2 -> a=1&b=2` |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **执行前必做**: 使用 `lark-cli schema slides.xml_presentation.slide.create` 查看最新的参数结构
|
||||
2. **slide.content 格式**: 必须是完整的 `<slide>` 元素,不是整个 presentation
|
||||
3. **命名空间建议**: 协议标准写法应带 `xmlns`,例如 `<slide xmlns="http://www.larkoffice.com/sml/2.0">`;当前服务端实现可能兼容不带 `xmlns` 的输入,但不作为协议保证
|
||||
4. **fill / border 写法**: 颜色填充使用 `<fill><fillColor color="..."/></fill>`,边框常用 `<border color="..." width="2"/>`
|
||||
5. **插入位置**: 通过 `before_slide_id` 指定插入目标,而不是用 `position`
|
||||
6. **JSON 转义**: 如果直接内联 XML,需要正确转义双引号
|
||||
7. **建议**: 先使用 `slides +xml-get` 获取现有结构,再添加新页面
|
||||
|
||||
## 批量添加建议
|
||||
|
||||
如果需要添加多张幻灯片,建议先明确每一页的 `before_slide_id`,或直接按最终顺序逐页追加:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
PRESENTATION_ID="slides_example_presentation_id"
|
||||
|
||||
declare -a slides=(
|
||||
'<slide xmlns="http://www.larkoffice.com/sml/2.0"><data><shape type="text" topLeftX="80" topLeftY="80" width="800" height="120"><content textType="title"><p>页面 1</p></content></shape></data></slide>'
|
||||
'<slide xmlns="http://www.larkoffice.com/sml/2.0"><data><shape type="text" topLeftX="80" topLeftY="80" width="800" height="120"><content textType="title"><p>页面 2</p></content></shape></data></slide>'
|
||||
'<slide xmlns="http://www.larkoffice.com/sml/2.0"><data><shape type="text" topLeftX="80" topLeftY="80" width="800" height="120"><content textType="title"><p>页面 3</p></content></shape></data></slide>'
|
||||
)
|
||||
|
||||
for slide_xml in "${slides[@]}"; do
|
||||
payload=$(jq -n --arg content "$slide_xml" '{slide:{content:$content}}')
|
||||
lark-cli slides xml_presentation.slide create --as user --params "{\"xml_presentation_id\":\"$PRESENTATION_ID\"}" --data "$payload"
|
||||
done
|
||||
```
|
||||
|
||||
## 相关命令
|
||||
|
||||
- [slides +create](lark-slides-create.md) - 创建空白 PPT
|
||||
- [slides +xml-get](lark-slides-xml-presentations-get.md) - 读取 PPT 内容并保存到本地文件
|
||||
- [xml_presentation.slide delete](lark-slides-xml-presentation-slide-delete.md) - 删除幻灯片页面
|
||||
- [xml-schema-quick-ref.md](xml-schema-quick-ref.md) - XML Schema 快速参考
|
||||
@ -0,0 +1,126 @@
|
||||
# lark-slides xml_presentation.slide delete
|
||||
|
||||
## 用途
|
||||
|
||||
删除指定 XML 演示文稿中的幻灯片页面。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
lark-cli slides xml_presentation.slide delete --as user --params '<json_params>'
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `--params` | JSON string | 是 | 路径参数与查询参数 |
|
||||
|
||||
### params JSON 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"xml_presentation_id": "slides_example_presentation_id",
|
||||
"slide_id": "slide_example_id",
|
||||
"revision_id": -1,
|
||||
"tid": "idMock"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `xml_presentation_id` | string | 是 | 演示文稿的唯一标识符 |
|
||||
| `slide_id` | string | 是 | 要删除的幻灯片唯一标识符 |
|
||||
| `revision_id` | integer | 否 | 演示文稿版本号,`-1` 表示最新版本 |
|
||||
| `tid` | string | 否 | 锁的事务 ID |
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 删除指定幻灯片
|
||||
|
||||
```bash
|
||||
lark-cli slides xml_presentation.slide delete --as user --params '{
|
||||
"xml_presentation_id": "slides_example_presentation_id",
|
||||
"slide_id": "slide_example_id"
|
||||
}'
|
||||
```
|
||||
|
||||
### 结合查询删除(使用 jq)
|
||||
|
||||
```bash
|
||||
# 先读取 XML 内容,确认待删除页面
|
||||
lark-cli slides +xml-get --as user \
|
||||
--presentation "slides_example_presentation_id" \
|
||||
--output .lark-slides/plan/slides_example_presentation_id/readback.xml \
|
||||
--json
|
||||
|
||||
# 然后按已知 slide_id 删除
|
||||
lark-cli slides xml_presentation.slide delete --as user --params '{"xml_presentation_id":"slides_example_presentation_id","slide_id":"slide_example_id"}'
|
||||
```
|
||||
|
||||
## 返回值
|
||||
|
||||
成功时返回删除确认信息:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"revision_id": 100
|
||||
},
|
||||
"msg": "success"
|
||||
}
|
||||
```
|
||||
|
||||
### 返回字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data.revision_id` | integer | 删除后的最新版本号 |
|
||||
|
||||
## 常见错误
|
||||
|
||||
| 错误码 | 含义 | 解决方案 |
|
||||
|--------|------|----------|
|
||||
| 404 | 演示文稿不存在 | 检查 `xml_presentation_id` 是否正确 |
|
||||
| 404 | 幻灯片不存在 | 检查 `slide_id` 是否正确,或该幻灯片已被删除 |
|
||||
| 400 | 无法删除唯一幻灯片 | 演示文稿至少保留一页幻灯片 |
|
||||
| 403 | 权限不足 | 检查是否拥有 `slides:presentation:update` 或 `slides:presentation:write_only` scope |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **执行前必做**: 使用 `lark-cli schema slides.xml_presentation.slide.delete` 查看最新的参数结构
|
||||
2. **删除不可逆**: 删除操作无法撤销,请确保已备份重要内容
|
||||
3. **至少保留一页**: 演示文稿必须至少保留一页幻灯片,删除最后一页会报错
|
||||
4. **版本控制**: 如果依赖版本号并发控制,删除前先确认 `revision_id`
|
||||
5. **获取 slide_id**: 创建幻灯片时请保存返回值;仅靠 `get` 返回的 XML 无法直接推导服务端 short ID
|
||||
|
||||
## 如何获取 slide_id
|
||||
|
||||
### 方法 1: 创建时保存
|
||||
|
||||
```bash
|
||||
lark-cli slides xml_presentation.slide create --as user --params '{"xml_presentation_id":"slides_example_presentation_id"}' --data '{
|
||||
"slide": {
|
||||
"content": "<slide xmlns=\"http://www.larkoffice.com/sml/2.0\"><data><shape type=\"text\" topLeftX=\"80\" topLeftY=\"80\" width=\"800\" height=\"120\"><content textType=\"title\"><p>新页面</p></content></shape></data></slide>"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
返回结果中的 `slide_id` 就是后续删除所需的值。
|
||||
|
||||
## 批量删除建议
|
||||
|
||||
如果需要删除多张幻灯片,建议先整理好待删 `slide_id` 列表,再逐个删除:
|
||||
|
||||
```bash
|
||||
for slide_id in sld_a sld_b sld_c; do
|
||||
lark-cli slides xml_presentation.slide delete --as user --params "{\"xml_presentation_id\":\"slides_example_presentation_id\",\"slide_id\":\"$slide_id\"}"
|
||||
done
|
||||
```
|
||||
|
||||
## 相关命令
|
||||
|
||||
- [slides +create](lark-slides-create.md) - 创建空白 PPT
|
||||
- [slides +xml-get](lark-slides-xml-presentations-get.md) - 读取 PPT 内容并保存到本地文件
|
||||
- [xml_presentation.slide create](lark-slides-xml-presentation-slide-create.md) - 添加幻灯片页面
|
||||
@ -0,0 +1,110 @@
|
||||
# lark-slides xml_presentation.slide get
|
||||
|
||||
## 用途
|
||||
|
||||
按 `slide_id` 拉取指定演示文稿单页的 XML 内容(可指定历史版本)。常用于"读-改-写"编辑闭环的第一步。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
lark-cli slides xml_presentation.slide get --as user --params '<json_params>'
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `--params` | JSON string | 是 | 路径参数与查询参数 |
|
||||
|
||||
### params JSON 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"xml_presentation_id": "slides_example_presentation_id",
|
||||
"slide_id": "slide_example_id",
|
||||
"revision_id": -1
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `xml_presentation_id` | string | 是 | 目标演示文稿唯一标识 |
|
||||
| `slide_id` | string | 是 | 目标页面唯一标识 |
|
||||
| `revision_id` | integer | 否 | 版本号,`-1` 表示最新版(默认)|
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 读最新版本
|
||||
|
||||
```bash
|
||||
lark-cli slides xml_presentation.slide get --as user --params '{
|
||||
"xml_presentation_id": "slides_example_presentation_id",
|
||||
"slide_id": "slide_example_id"
|
||||
}'
|
||||
```
|
||||
|
||||
### 只提取 XML 内容
|
||||
|
||||
```bash
|
||||
lark-cli slides xml_presentation.slide get --as user \
|
||||
--params '{"xml_presentation_id":"slides_example_presentation_id","slide_id":"slide_example_id"}' \
|
||||
| jq -r '.data.slide.content'
|
||||
```
|
||||
|
||||
### 读指定历史版本
|
||||
|
||||
```bash
|
||||
lark-cli slides xml_presentation.slide get --as user --params '{
|
||||
"xml_presentation_id": "slides_example_presentation_id",
|
||||
"slide_id": "slide_example_id",
|
||||
"revision_id": 42
|
||||
}'
|
||||
```
|
||||
|
||||
## 返回值
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"identity": "user",
|
||||
"data": {
|
||||
"slide": {
|
||||
"slide_id": "slide_example_id",
|
||||
"content": "<slide id=\"slide_example_id\"><style/><data>...</data></slide>"
|
||||
},
|
||||
"revision_id": 100
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data.slide.slide_id` | string | 页面唯一标识 |
|
||||
| `data.slide.content` | string | 页面完整 XML(`<slide>` 根节点,不含 xmlns)|
|
||||
| `data.revision_id` | integer | 此次读到的版本号,可用于后续 replace 的乐观锁 |
|
||||
|
||||
## 常见错误
|
||||
|
||||
| 错误码 | 含义 | 解决方案 |
|
||||
|--------|------|----------|
|
||||
| 404 | 演示文稿或页面不存在 | 检查 `xml_presentation_id` / `slide_id` |
|
||||
| 403 | 权限不足 | 需要 `slides:presentation:read` scope,并对该 PPT 有访问权限 |
|
||||
| 400 | `revision_id` 不存在 | 传了无效版本号,用 `-1` 或真实存在的版本号 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **执行前必做**:`lark-cli schema slides.xml_presentation.slide.get` 查看最新参数结构
|
||||
2. **block_id 提取**:返回 XML 里每个顶层块(shape、img、table、chart 等)的 `id` 属性即为 `block_id`,通常是 3 字符短码,例如 `<shape id="bUn" ...>`。用以下命令列出当前页所有 block_id:
|
||||
|
||||
```bash
|
||||
lark-cli slides xml_presentation.slide get --as user \
|
||||
--params "{\"xml_presentation_id\":\"$PID\",\"slide_id\":\"$SID\"}" \
|
||||
| jq -r '.data.slide.content' | grep -oE 'id="[^"]+"' | sed 's/id="//;s/"//'
|
||||
```
|
||||
|
||||
## 相关命令
|
||||
|
||||
- [slides +replace-slide](lark-slides-replace-slide.md) — 块级替换 shortcut(推荐)
|
||||
- [xml_presentation.slide replace](lark-slides-xml-presentation-slide-replace.md) — 底层 replace API 参考
|
||||
- [slides +xml-get](lark-slides-xml-presentations-get.md) — 读整个 PPT 并保存到本地文件
|
||||
- [lark-slides-edit-workflows.md](lark-slides-edit-workflows.md) — 读-改-写闭环
|
||||
@ -0,0 +1,188 @@
|
||||
# lark-slides xml_presentation.slide replace
|
||||
|
||||
## 用途
|
||||
|
||||
对单页做**块级局部替换**:不覆盖整页,按 patch 列表做 `block_replace`(整块替换)或 `block_insert`(整块插入)。适合"只想加 / 换一个元素、不动其他元素"的场景。
|
||||
|
||||
> **推荐**:优先使用 [`+replace-slide`](lark-slides-replace-slide.md) Shortcut——它会自动注入 `id` 和 `<content/>`,直接调本 API 需自己处理这两个约束(见注意事项 5、6)。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
lark-cli slides xml_presentation.slide replace --as user --params '<json_params>' --data '<json_data>'
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `--params` | JSON string | 是 | 路径参数与查询参数 |
|
||||
| `--data` | JSON string | 是 | patch 列表 |
|
||||
|
||||
### params JSON 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"xml_presentation_id": "slides_example_presentation_id",
|
||||
"slide_id": "slide_example_id",
|
||||
"revision_id": -1,
|
||||
"tid": "idMock"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `xml_presentation_id` | string | 是 | 演示文稿唯一标识 |
|
||||
| `slide_id` | string | 是 | 页面唯一标识 |
|
||||
| `revision_id` | integer | 否 | 默认 `-1`(以最新版为基准);传具体版本号做乐观锁 |
|
||||
| `tid` | string | 否 | 事务 ID,一般留空 |
|
||||
|
||||
### data JSON 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"parts": [
|
||||
{ "action": "block_replace", "block_id": "bab", "replacement": "<shape .../>" },
|
||||
{ "action": "block_insert", "insertion": "<img .../>", "insert_before_block_id": "baa" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `parts` | array | 是 | patch 列表,长度 1~200,顺序执行 |
|
||||
|
||||
### parts[] 字段(按 action 不同)
|
||||
|
||||
本期 CLI 文档化两种 action:
|
||||
|
||||
#### action = "block_replace" — 整块替换
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `action` | 是 | 固定 `block_replace` |
|
||||
| `block_id` | 是 | 目标块的 3 位 short element ID(从 `slide.get` 返回的 XML 里读到) |
|
||||
| `replacement` | 是 | 新 XML 片段,替换整个目标块 |
|
||||
|
||||
#### action = "block_insert" — 整块插入
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `action` | 是 | 固定 `block_insert` |
|
||||
| `insertion` | 是 | 要插入的完整 XML 片段 |
|
||||
| `insert_before_block_id` | 否 | 插到这个块之前;省略则追加到页面末尾 |
|
||||
|
||||
## 使用示例
|
||||
|
||||
### block_replace:换一个 shape 的整体内容
|
||||
|
||||
```bash
|
||||
lark-cli slides xml_presentation.slide replace --as user --params '{
|
||||
"xml_presentation_id": "slides_example_presentation_id",
|
||||
"slide_id": "slide_example_id"
|
||||
}' --data '{
|
||||
"parts": [
|
||||
{
|
||||
"action": "block_replace",
|
||||
"block_id": "bab",
|
||||
"replacement": "<shape type=\"text\" topLeftX=\"80\" topLeftY=\"80\" width=\"800\" height=\"120\"><content textType=\"title\"><p>新标题</p></content></shape>"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
### block_insert:在已有页上加一张图
|
||||
|
||||
```bash
|
||||
# 先拿 file_token
|
||||
TOKEN=$(lark-cli slides +media-upload --file ./pic.png --presentation "$PID" --as user | jq -r '.data.file_token')
|
||||
|
||||
lark-cli slides xml_presentation.slide replace --as user --params "{
|
||||
\"xml_presentation_id\": \"$PID\",
|
||||
\"slide_id\": \"$SID\"
|
||||
}" --data "$(jq -n --arg token "$TOKEN" '{
|
||||
parts: [
|
||||
{
|
||||
action: "block_insert",
|
||||
insertion: ("<img src=\"" + $token + "\" topLeftX=\"500\" topLeftY=\"100\" width=\"200\" height=\"150\"/>")
|
||||
}
|
||||
]
|
||||
}')"
|
||||
```
|
||||
|
||||
### 多条 parts 原子执行
|
||||
|
||||
```bash
|
||||
lark-cli slides xml_presentation.slide replace --as user --params '{
|
||||
"xml_presentation_id": "slides_example_presentation_id",
|
||||
"slide_id": "slide_example_id"
|
||||
}' --data '{
|
||||
"parts": [
|
||||
{"action":"block_replace","block_id":"bab","replacement":"<shape type=\"text\" topLeftX=\"80\" topLeftY=\"80\" width=\"800\" height=\"120\"><content textType=\"title\"><p>新标题</p></content></shape>"},
|
||||
{"action":"block_insert","insertion":"<img src=\"<file_token>\" topLeftX=\"700\" topLeftY=\"400\" width=\"180\" height=\"100\"/>"}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
## 返回值
|
||||
|
||||
### 成功
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"identity": "user",
|
||||
"data": {
|
||||
"revision_id": 105
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 失败(任一 part 失败,整批不生效)
|
||||
|
||||
失败时命令以非零退出码结束,stderr 返回类型化错误信封(`error.code`(如 3350001)/ `error.message` / `error.hint`),stdout 不会打印后端原始响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"identity": "user",
|
||||
"error": {
|
||||
"type": "api",
|
||||
"subtype": "...",
|
||||
"code": 3350001,
|
||||
"message": "...",
|
||||
"hint": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data.revision_id` | integer | 成功时返回更新后最新版本号 |
|
||||
|
||||
## 常见错误
|
||||
|
||||
| 错误码 | 含义 | 解决方案 |
|
||||
|--------|------|----------|
|
||||
| 3350001 | `block_id` 在当前页不存在,或 XML 格式 / 结构错误 | 重新 `slide.get` 拿最新 XML,确认 `block_id` 存在;检查 `replacement` / `insertion` 是否合法 XML |
|
||||
| 400 | `parts` 长度超过 200 | 拆多次调用 |
|
||||
| 3350002 | `revision_id` 不存在(超过当前版本号) | 用 `-1` 或实际存在的 `revision_id` |
|
||||
| 400 | XML 格式错误 | `replacement` / `insertion` 必须为合法的 XML 片段,标签闭合 + 属性引号 |
|
||||
| 403 | 权限不足 | 需要 `slides:presentation:update` 或 `slides:presentation:write_only` |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **parts 原子事务**:任一条失败整批回滚,不会出现"前几条成功、后几条失败"的中间态。
|
||||
2. **block_id 的获取**:`slide.get` 返回的 XML 里每个块(shape、img、table、chart 等)会带 3 位 short element ID,用这个值填 `block_id` / `insert_before_block_id`。
|
||||
3. **`<img>` 必须用 file_token**:不能用外链 URL——先 [`slides +media-upload`](lark-slides-media-upload.md) 拿 token。
|
||||
4. **不能字段级 patch**:要改一个块的某个属性(比如只改 `topLeftX`),得写整块新 XML 走 `block_replace`;API 不支持"只改一个字段"。
|
||||
5. **`block_replace` 要求 `replacement` 根元素带 `id="<block_id>"`**:底层 API 的硬约束,缺失会返回 3350001。推荐走 shortcut [`+replace-slide`](lark-slides-replace-slide.md)——它会自动把 `id` 注入到 `replacement` 根元素上,用户写 XML 时不用自己加。
|
||||
6. **`<shape>` 必须有 `<content/>` 子元素**:SML 2.0 schema 要求,缺失同样触发 3350001。shortcut [`+replace-slide`](lark-slides-replace-slide.md) 会自动注入 `<content/>`,直接调底层 API 需要自己加。
|
||||
7. **执行前必做**:`lark-cli schema slides.xml_presentation.slide.replace` 查看最新参数结构。
|
||||
|
||||
## 相关命令
|
||||
|
||||
- [slides +replace-slide](lark-slides-replace-slide.md) — 块级替换 shortcut(推荐,自动注入 id)
|
||||
- [xml_presentation.slide get](lark-slides-xml-presentation-slide-get.md) — 读原页拿 block short ID
|
||||
- [slides +media-upload](lark-slides-media-upload.md) — 上传图片拿 file_token
|
||||
- [lark-slides-edit-workflows.md](lark-slides-edit-workflows.md) — 读-改-写闭环 + 决策树
|
||||
@ -0,0 +1,134 @@
|
||||
# lark-slides xml_presentations get
|
||||
|
||||
## 用途
|
||||
|
||||
读取飞书幻灯片(PPT)演示文稿的完整 XML 内容信息。
|
||||
|
||||
## Shortcut
|
||||
|
||||
使用 `slides +xml-get` shortcut,可以把 XML 保存到本地文件,避免终端输出被截断。
|
||||
|
||||
|
||||
```bash
|
||||
lark-cli slides +xml-get --as user \
|
||||
--presentation "slides_example_presentation_id" \
|
||||
--output .lark-slides/plan/slides_example_presentation_id/readback.xml \
|
||||
--json
|
||||
```
|
||||
|
||||
### 参数说明
|
||||
|
||||
| 参数 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `--presentation` | string | 是 | 演示文稿的唯一标识符 |
|
||||
| `--revision-id` | integer | 否 | 版本号,`-1` 表示最新版本 |
|
||||
| `--output` | string | 是 | 本地文件,必须使用相对路径 |
|
||||
| `--remove-attr-id` | flag | 否 | 移除 XML id 属性后读取 |
|
||||
| `--json` | flag | 是 | 必须按照 json 格式输出 |
|
||||
|
||||
|
||||
### 基础示例
|
||||
|
||||
```bash
|
||||
lark-cli slides +xml-get --as user \
|
||||
--presentation "slides_example_presentation_id" \
|
||||
--output .lark-slides/plan/slides_example_presentation_id/readback.xml \
|
||||
--json
|
||||
```
|
||||
|
||||
### 指定版本读取
|
||||
|
||||
```bash
|
||||
lark-cli slides +xml-get --as user \
|
||||
--presentation "slides_example_presentation_id" \
|
||||
--revision-id 10 \
|
||||
--output .lark-slides/plan/slides_example_presentation_id/readback-r10.xml \
|
||||
--json
|
||||
```
|
||||
|
||||
### 移除 XML id 属性后读取
|
||||
|
||||
```bash
|
||||
lark-cli slides +xml-get --as user \
|
||||
--presentation "slides_example_presentation_id" \
|
||||
--remove-attr-id \
|
||||
--output .lark-slides/plan/slides_example_presentation_id/readback-no-id.xml \
|
||||
--json
|
||||
```
|
||||
|
||||
|
||||
## 底层原生命令形态
|
||||
|
||||
```bash
|
||||
lark-cli slides xml_presentations get --as user --params '<json_params>'
|
||||
```
|
||||
|
||||
### 参数说明
|
||||
|
||||
| 参数 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `--params` | JSON string | 是 | 路径参数与查询参数,结构以 schema 为准 |
|
||||
|
||||
### params JSON 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"xml_presentation_id": "slides_example_presentation_id",
|
||||
"revision_id": -1
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `xml_presentation_id` | string | 是 | 演示文稿的唯一标识符 |
|
||||
| `revision_id` | integer | 否 | 版本号,`-1` 表示最新版本 |
|
||||
|
||||
|
||||
### 返回值
|
||||
|
||||
成功时返回演示文稿的完整信息:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"identity": "user",
|
||||
"data": {
|
||||
"xml_presentation": {
|
||||
"presentation_id": "slides_example_presentation_id",
|
||||
"revision_id": 1,
|
||||
"content": "<presentation xmlns=\"http://www.larkoffice.com/sml/2.0\" height=\"540\" width=\"960\">...</presentation>"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 返回字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data.xml_presentation.presentation_id` | string | 演示文稿唯一标识 |
|
||||
| `data.xml_presentation.revision_id` | integer | 版本号 |
|
||||
| `data.xml_presentation.content` | string | XML 格式的完整内容 |
|
||||
|
||||
### 常见错误
|
||||
|
||||
| 错误码 | 含义 | 解决方案 |
|
||||
|--------|------|----------|
|
||||
| 404 | 演示文稿不存在 | 检查 `xml_presentation_id` 是否正确 |
|
||||
| 403 | 权限不足 | 检查是否拥有 `slides:presentation:read` scope,或是否有访问权限 |
|
||||
| 400 | 参数格式错误 | 确保 `--params` 是合法的 JSON 字符串 |
|
||||
|
||||
|
||||
### 注意事项
|
||||
|
||||
1. lark-slides 工作流默认使用 `slides +xml-get`;只有必须直接调底层 API 时,才使用
|
||||
2. 直接调用底层 API 前,使用 `lark-cli schema slides.xml_presentations.get` 查看最新的参数结构
|
||||
3. 返回的 XML 在 `data.xml_presentation.content` 字段中
|
||||
4. 如果只需要部分信息,可以使用 `jq` 等工具过滤返回结果
|
||||
5. 不要在普通工作流中把完整 XML 打到终端;用 `slides +xml-get --output` 保存文件
|
||||
|
||||
## 相关命令
|
||||
|
||||
- [slides +create](lark-slides-create.md) - 创建空白 PPT
|
||||
- [xml_presentation.slide create](lark-slides-xml-presentation-slide-create.md) - 添加幻灯片页面
|
||||
- [xml_presentation.slide delete](lark-slides-xml-presentation-slide-delete.md) - 删除幻灯片页面
|
||||
247
.agents/skills/lark-slides/references/planning-layer.md
Normal file
247
.agents/skills/lark-slides/references/planning-layer.md
Normal file
@ -0,0 +1,247 @@
|
||||
# Planning Layer
|
||||
|
||||
新建演示文稿或大幅改写页面时,必须先写 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`,再生成 XML。这个文件是 deck 的设计中间层,用来把叙事、页面角色、布局、视觉重点和文字密度固定下来,避免从用户提示直接跳到 XML。
|
||||
|
||||
小型已有页编辑可豁免,例如只替换一个标题、改一个数字、插入一个块、上传并插入一张图。只要任务会重排多页、生成新 deck、替换整页结构,仍然需要规划层。
|
||||
|
||||
## Required Flow
|
||||
|
||||
1. 理解用户需求,必要时澄清主题、受众、页数、风格。
|
||||
2. 选择唯一 plan 目录:`.lark-slides/plan/<deck-or-task-id>/`。
|
||||
3. 先创建目录:`mkdir -p .lark-slides/plan/<deck-or-task-id>`。
|
||||
4. 写入 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`。
|
||||
5. 读取 `xml-schema-quick-ref.md`、`visual-planning.md` 和 `asset-planning.md`。
|
||||
6. 按 plan、visual planning 和 asset planning 规则逐页生成 XML,把 `layout_type`、`visual_focus`、`text_density` 转成具体页面几何和文本量约束,并把缺失素材转成可执行兜底视觉。
|
||||
7. 创建 PPT 后用 `slides +xml-get` 回读,核对页面数量、关键元素和 plan 到 XML 的对应关系,空白 PPT 中没有 slide 元素。
|
||||
|
||||
|
||||
## Plan Path
|
||||
|
||||
Use a separate plan directory per deck or task so multiple presentations in the same workspace cannot overwrite each other.
|
||||
|
||||
Recommended IDs:
|
||||
|
||||
- New deck before creation: title slug plus date/time, such as `q3-review-20260507-1805`.
|
||||
- Existing PPT rewrite: the `xml_presentation_id`.
|
||||
- Ambiguous or untitled task: short task slug plus date/time.
|
||||
|
||||
Rules:
|
||||
|
||||
- Do not reuse `.lark-slides/plan/slide_plan.json` as a shared path.
|
||||
- Create the directory before writing the file.
|
||||
- Reuse the same plan path for XML generation and post-create verification for that deck.
|
||||
|
||||
## Artifact Lifecycle
|
||||
|
||||
`.lark-slides/` is local agent state. It supports recovery, iteration, and later edits, but it should not be treated as source code or committed by default.
|
||||
|
||||
Keep:
|
||||
|
||||
- `.lark-slides/plan/<deck-or-task-id>/slide_plan.json` after successful creation or major rewrite. The plan is the editable design state for the deck.
|
||||
- A small manifest when useful for follow-up work, such as `xml_presentation_id`, slide IDs, `revision_id`, plan path, and verification status.
|
||||
|
||||
Clean or avoid keeping:
|
||||
|
||||
- Transient XML payloads after successful creation and verification. Prefer `/tmp` for throwaway XML, or delete generated XML files after success.
|
||||
- Stale XML drafts that no longer match the current presentation state.
|
||||
|
||||
Exception:
|
||||
|
||||
- If creation fails or partially succeeds, keep the relevant XML/debug payloads until recovery is complete. Record `xml_presentation_id` first, then fetch current state before retrying.
|
||||
|
||||
## JSON Shape
|
||||
|
||||
```json
|
||||
{
|
||||
"presentation_goal": "Explain the proposal and secure approval for the next phase.",
|
||||
"audience": "Product and engineering leaders who know the domain but need a concise decision narrative.",
|
||||
"theme_style": "Clean business style, light background, restrained blue accent, strong visual hierarchy.",
|
||||
"visual_system": {
|
||||
"background_strategy": "Content pages use one light base; cover and closing may use a related dark treatment with the same accent system.",
|
||||
"motif": "Consistent card style and numbered anchors.",
|
||||
"color_roles": {
|
||||
"primary": "Used for the dominant structural motif and about 60-70% of visual weight.",
|
||||
"secondary": "Used for grouped regions, comparison panels, or supporting categories.",
|
||||
"accent": "Used only for key numbers, conclusions, or focus markers."
|
||||
}
|
||||
},
|
||||
"typography_constraints": {
|
||||
"title_max_lines": 2,
|
||||
"body_max_lines_per_box": 2,
|
||||
"footer_max_lines": 1,
|
||||
"long_text_handling": "Shorten, split into multiple boxes, or move detail to speaker notes instead of shrinking into a tight box."
|
||||
},
|
||||
"verification_plan": {
|
||||
"check_background_consistency": true,
|
||||
"check_text_fit": true,
|
||||
"check_visual_focus": true,
|
||||
"check_asset_rendering": true
|
||||
},
|
||||
"slides": [
|
||||
{
|
||||
"page": 1,
|
||||
"title": "Proposal Title",
|
||||
"key_message": "The initiative is ready for a focused pilot.",
|
||||
"layout_type": "title-cover",
|
||||
"visual_focus": "Large title area with one concise supporting statement.",
|
||||
"asset_need": {
|
||||
"asset_type": "logo",
|
||||
"purpose": "Signal product or team identity on the opening page.",
|
||||
"suggested_query": "product logo",
|
||||
"fallback_if_missing": "Create a close-enough image with the image generation tool instead of a real logo."
|
||||
},
|
||||
"text_density": "low",
|
||||
"speaker_intent": "Frame the decision and establish the deck's point of view."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Required Fields
|
||||
|
||||
Top-level fields:
|
||||
|
||||
- `presentation_goal`: what the whole deck is trying to achieve.
|
||||
- `audience`: target readers or listeners and their assumed background.
|
||||
- `theme_style`: visual tone, palette direction, and professional style.
|
||||
- `visual_system`: deck-level visual rules that must stay stable across pages, including background strategy, recurring motif, and color roles.
|
||||
- `typography_constraints`: deck-level limits for line count, text box density, and how to handle long text before XML generation.
|
||||
- `verification_plan`: explicit checks to perform after creation or major edits; include background consistency, text fit, visual focus, and asset rendering when relevant.
|
||||
- `slides`: ordered page plans.
|
||||
|
||||
Each slide must include:
|
||||
|
||||
- `page`: 1-based page number.
|
||||
- `title`: slide title.
|
||||
- `key_message`: the one idea this page must land.
|
||||
- `layout_type`: planned page structure.
|
||||
- `visual_focus`: dominant visual object or region.
|
||||
- `asset_need`: planning-only structured asset metadata; no search, download, or upload required. Follow `asset-planning.md`.
|
||||
- `text_density`: `low`, `medium`, or `high`.
|
||||
- `speaker_intent`: why the speaker needs this page and how it advances the story.
|
||||
|
||||
Optional slide fields:
|
||||
|
||||
- `chart_contract`: required when the page plan includes a standard data chart that `<chart>` supports. Use this shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"chart_contract": {
|
||||
"required": true,
|
||||
"render_as": "native_chart",
|
||||
"chart_type": "line",
|
||||
"data_source": "mock_placeholder",
|
||||
"data_series_required": true,
|
||||
"placeholder_label_required": true,
|
||||
"manual_shape_fallback_allowed": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When `chart_contract.required == true`, XML generation must produce a `<chart>` element on that slide. A shape, line, or polyline approximation does not satisfy the plan.
|
||||
|
||||
`data_source` must be one of:
|
||||
|
||||
- `user_provided`: the user supplied concrete values, tables, CSV, or metric lists; use them and do not replace them with mock data.
|
||||
- `mock_placeholder`: the user asked for a placeholder, template, example, or later-replaceable chart position; use mock data in native `<chart>`.
|
||||
- `mock_required_by_intent`: the user did not provide concrete values but asked for data expression, charts, trends, comparisons, or distributions; use mock data in native `<chart>`.
|
||||
|
||||
`data_series_required` means the generated XML must include `<chartData>`. It does not require user-provided real-world values. When real values are unavailable but chart expression is part of the user's intent, write mock or placeholder values into native `<chart>` and label them clearly instead of switching to manual drawing primitives or metric blocks.
|
||||
|
||||
## Layout Vocabulary
|
||||
|
||||
Use one of these `layout_type` values unless the user explicitly needs a custom structure:
|
||||
|
||||
- `title-cover`
|
||||
- `section-divider`
|
||||
- `two-column`
|
||||
- `image-left-text-right`
|
||||
- `image-right-text-left`
|
||||
- `big-number`
|
||||
- `timeline`
|
||||
- `comparison`
|
||||
- `architecture-diagram`
|
||||
- `process-flow`
|
||||
- `quote-highlight`
|
||||
- `conclusion`
|
||||
|
||||
The value must affect XML geometry, not just appear as a label. For example, `timeline` should create a horizontal or vertical sequence, `comparison` should create distinct side-by-side regions, and `big-number` should reserve dominant space for a large metric.
|
||||
|
||||
## Text Density Rules
|
||||
|
||||
- `low`: title plus 1 short statement, or 1-3 very short labels.
|
||||
- `medium`: title plus 2-4 concise bullets or labeled regions.
|
||||
- `high`: allowed only when the user needs detail; use tables, columns, or grouped regions instead of a long bullet list.
|
||||
|
||||
Do not let all pages become title + bullet slides. For decks of 4 or more pages, aim for at least 4 different `layout_type` values when the content allows it.
|
||||
|
||||
Text density must be realistic for the planned geometry. If a page needs long titles, bilingual labels, paper figure captions, legal disclaimers, or dense technical wording, record how the text will be shortened, split, or moved to speaker notes. Do not rely on small font sizes or tight boxes to make text fit.
|
||||
|
||||
## Visual System Planning
|
||||
|
||||
Before generating XML, define a visual system that can survive the whole deck:
|
||||
|
||||
- `background_strategy`: specify the default background for normal content pages, and which page roles may intentionally differ. Do not let pages drift through near-identical but inconsistent background colors.
|
||||
- `motif`: choose one reusable structural device, such as numbered node, card treatment, half-bleed image zone, headline, or footer. The motif should appear consistently enough that pages feel related.
|
||||
- `color_roles`: assign primary, secondary, and accent roles. The same color must not mean unrelated things across pages.
|
||||
- `cover_content_relationship`: if the cover uses a different dark or image-led treatment, state how it connects to content pages through shared colors, motifs, or geometry.
|
||||
- `closing_relationship`: if the closing page mirrors the cover, state that explicitly so it looks intentional rather than like a new theme.
|
||||
|
||||
These are planning constraints, not decoration notes. They must affect coordinates, background fills, shape styles, and text placement in generated XML.
|
||||
|
||||
## Iterative Deck State
|
||||
|
||||
When continuing an existing deck, update the same plan path rather than creating a new disconnected plan. Keep the plan aligned with what has actually been created.
|
||||
|
||||
Recommended optional fields for long-running work:
|
||||
|
||||
- `deck_status`: current slide count, target slide count if known, and last verified revision or timestamp.
|
||||
- `created_slides`: page number, slide id when known, and the page role.
|
||||
- `assets_used`: source, local path when applicable, uploaded token when known, and which page uses it.
|
||||
- `open_issues`: known layout, text fit, asset, or consistency risks that still need correction.
|
||||
|
||||
Do not hard-code a page number just because a previous deck used that pattern. Plan by page role and evidence need, such as "method overview pages should use a figure when the source has a readable figure" instead of binding screenshots, charts, or diagrams to a fixed page index. The plan should describe decision rules, not a rigid template sequence.
|
||||
|
||||
## Asset Planning
|
||||
|
||||
`asset_need` is metadata. It can describe a desired figure, diagram, chart, icon, logo, screenshot, or fallback visual.
|
||||
|
||||
Use an object for one planned asset, an array for multiple real needs, or `asset_type: "none"` when no asset is useful. Each planned asset must include:
|
||||
|
||||
- `asset_type`: one of `paper_figure`, `architecture_diagram`, `icon`, `logo`, `chart`, `infographic`, `screenshot`, `flow_diagram`, or `none`.
|
||||
- `purpose`: why this asset helps the page's key message.
|
||||
- `suggested_query`: short future lookup hint only; do not execute it unless separately requested.
|
||||
- `fallback_if_missing`: a plan to create a close-enough image with the image generation tool, or a native `<chart>` for data.
|
||||
- `chart_contract`: when `asset_type` is `chart` and the visual is a supported standard data chart, set this optional slide-level field so generation is locked to native `<chart>`.
|
||||
|
||||
For detailed rules and examples, read `asset-planning.md`.
|
||||
|
||||
Good examples:
|
||||
|
||||
- `{"asset_type":"architecture_diagram","purpose":"Explain component relationships.","suggested_query":"service architecture diagram","fallback_if_missing":"Render the component diagram with <shape> + <line>."}`
|
||||
- `{"asset_type":"logo","purpose":"Identify the customer context.","suggested_query":"customer logo","fallback_if_missing":"Create a close-enough image with the image generation tool instead of a real logo."}`
|
||||
- `{"asset_type":"chart","purpose":"Show adoption trend.","suggested_query":"monthly adoption trend chart","fallback_if_missing":"Render a native `<chart>` using the provided series when available; otherwise render a native `<chart>` with mock placeholder values and label it as 模拟数据,仅占位,待替换真实数据."}`
|
||||
|
||||
## XML Generation Contract
|
||||
|
||||
Before writing each slide XML, map the plan fields to concrete decisions:
|
||||
|
||||
- `key_message` determines the headline, dominant claim, or main takeaway.
|
||||
- `layout_type` determines the coordinate structure and element types. Use `visual-planning.md` for concrete layout rules.
|
||||
- `visual_focus` determines the largest visual region or emphasized object.
|
||||
- `text_density` caps visible text volume.
|
||||
- `asset_need` informs placeholder diagrams, icons, charts, screenshots, or fallback visuals only. Missing real assets must use `fallback_if_missing`, not blank regions.
|
||||
- `chart_contract` locks supported standard data charts to native `<chart>` output. Manual approximations are allowed only when the planned chart type is unsupported by `<chart>` or when the visual is explicitly non-data/decorative.
|
||||
|
||||
After creating the PPT, fetch the presentation and verify:
|
||||
|
||||
- Page count matches the plan.
|
||||
- Every page has the planned title and key message represented.
|
||||
- At least several pages have visibly different XML layout structures.
|
||||
- Planned `visual_focus` appears as a dominant visual region or object.
|
||||
- Asset planning is proportional to the deck topic and length: technical, research, product, and analytical decks should include meaningful planned visuals where they clarify the story, and each planned asset has a visible fallback if no real asset was used.
|
||||
- `text_density` is reflected in the amount of visible text.
|
||||
- Pages are not crowded, and any planned `timeline`, `comparison`, or `architecture-diagram` page uses its matching visual structure.
|
||||
- The actual backgrounds match `visual_system.background_strategy`; any dark, image-led, or emphasis page has an intentional relationship to the rest of the deck.
|
||||
- Text boxes respect `typography_constraints`; long labels, captions, footer text, and conclusion bars are not squeezed into boxes that are too short for the intended line count.
|
||||
- If real assets are used, the final XML contains renderable asset tokens or supported local placeholders for creation, not http URLs, stale local paths, or blank image boxes.
|
||||
1416
.agents/skills/lark-slides/references/slides_chart_demo.xml
Normal file
1416
.agents/skills/lark-slides/references/slides_chart_demo.xml
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
63
.agents/skills/lark-slides/references/troubleshooting.md
Normal file
63
.agents/skills/lark-slides/references/troubleshooting.md
Normal file
@ -0,0 +1,63 @@
|
||||
# Troubleshooting
|
||||
|
||||
本文件覆盖 lark-slides 的通用创建前自检、XML 排障和常见失败处理。命令专属问题优先看对应 reference,例如 `+replace-slide`、`+media-upload`、`xml_presentation.slide.create`。
|
||||
|
||||
## XML Preflight
|
||||
|
||||
在真正创建或替换前,至少检查:
|
||||
|
||||
- 特殊字符已转义:正文和标题里的 `&`、`<`、`>` 不能裸写;属性值里的裸 `&` 也必须写成 `&`。
|
||||
- 属性引号安全:XML 属性、shell 引号、JSON 字符串包装之间没有互相打断。
|
||||
- 结构合法:`<slide>` 下只放 `<style>`、`<data>`、`<note>`,文本都在 `<content>` 内。
|
||||
- 图片路径正确:`<img src="@...">` 只在 `+create --slides` 的支持链路中使用;直接调用 `xml_presentation.slide.create` 必须先拿到 `file_token`。
|
||||
|
||||
## Failure Order
|
||||
|
||||
遇到 `invalid param`、某一页创建失败、页面空白或布局错乱时,按顺序处理:
|
||||
|
||||
1. 记录 `xml_presentation_id`,不要假设失败代表什么都没创建。
|
||||
2. 用 `slides +xml-get` 回读,确认是否已有部分页面写入。
|
||||
3. 检查失败页是否含未转义字符:`Q&A -> Q&A`,文本 `<` / `>` 写成 `<` / `>`,属性 URL `a=1&b=2 -> a=1&b=2`。
|
||||
4. 检查标签闭合、属性引号、`<content>` 结构,以及 `<slide>` 直接子元素。
|
||||
5. 页面空白、溢出、重叠或越界时,按 [validation-checklist.md](validation-checklist.md) 运行 `xml_text_overlap_lint.py`;先修复所有 `error`,再对 `warning` 指向的页面和元素做截图复核。
|
||||
6. 如果使用 `--slides '[...]'`,怀疑 shell 截断时直接切到两步创建:先 `slides +create`,再用 `xml_presentation.slide.create` 逐页添加。
|
||||
7. 局部问题用 `+replace-slide` 块级修正;整页结构要改时再用 `slide.delete` 旧页 + `slide.create` 新页。
|
||||
|
||||
## Symptom Fixes
|
||||
|
||||
| 看到的问题 | 处理方式 |
|
||||
|-----------|----------|
|
||||
| 文字被截断 / 看不全 | 增大 shape 的 `width` 或 `height`,或减少文本量 |
|
||||
| 元素重叠 | 调整 `topLeftX` / `topLeftY`,拉开间距 |
|
||||
| 页面大面积空白 | 回读确认内容是否写入;若内容存在,再缩小间距或增加主体元素 |
|
||||
| 文字和背景色太接近 | 深色背景用浅色文字,浅色背景用深色文字 |
|
||||
| 表格列宽不合理 | 调整 `colgroup` 中 `col` 的 `width` 值 |
|
||||
| 图表没有显示 | 检查 `chartPlotArea` 和 `chartData` 是否都包含,`dim1` / `dim2` 数据数量是否匹配 |
|
||||
| 图片被裁掉一部分 | `<img>` 的 `width` / `height` 是裁剪后尺寸;要整图显示就让 `width:height` 对齐原图比例 |
|
||||
| 图片不显示 / `<img src>` 仍是 `@path` | `@` 占位符只在 `+create --slides` 中替换;直接调 `xml_presentation.slide.create` 必须先用 `+media-upload` 拿 `file_token` |
|
||||
| 新插入的 `<img>` 挡住原有元素 | `slide.get` 读原页,对照已有块坐标挑空白位置;空间不够就在同一批 `--parts` 里先移动/缩小现有块再插图 |
|
||||
| 渐变背景变成白色 | 渐变必须用 `rgba()` 格式 + 百分比停靠点,如 `linear-gradient(135deg,rgba(30,60,114,1) 0%,rgba(59,130,246,1) 100%)` |
|
||||
| 整体风格不统一 | 封面页和结尾页用同一背景,内容页保持一致的配色和字号体系 |
|
||||
|
||||
## Common Errors
|
||||
|
||||
| 错误码 / 信号 | 含义 | 解决方案 |
|
||||
|--------------|------|----------|
|
||||
| 400 XML 格式错误 | XML 语法错误 | 检查标签闭合、属性引号、特殊字符转义 |
|
||||
| 400 请求包装错误 | `--data` 未按 schema 包装 | 检查是否传入 `xml_presentation.content` 或 `slide.content` |
|
||||
| 创建成功但页面空白 / 内容缺失 / 布局错乱 | 常见于 `--slides '[...]'` 的 shell 转义或长参数传递问题 | 改用两步创建,并在创建后立即读取 XML 验证 |
|
||||
| 403 权限不足 | scope 或文档权限不匹配 | 确认 scope 和文档权限;无权限时根据错误响应引导用户解决 |
|
||||
| 404 演示文稿不存在 | `xml_presentation_id` 不正确或无权限 | 检查 token;wiki URL 需先解析真实 `obj_token` |
|
||||
| 404 幻灯片不存在 | `slide_id` 不正确 | 重新读取 presentation 或 slide,确认最新 ID |
|
||||
| 400 无法删除唯一幻灯片 | 演示文稿至少保留一页 | 先创建新页,再删除旧页 |
|
||||
| 1061002 媒体上传 params error | slides 媒体上传参数不符合约定 | 用 `slides +media-upload`,不要手拼原生 `medias/upload_all`;slides 唯一可用 `parent_type` 是 `slide_file` |
|
||||
| 1061004 forbidden | 当前用户对演示文稿无编辑权限 | 确认当前用户对目标 PPT 有编辑权限 |
|
||||
| 3350001 | XML 非 well-formed、XML 结构不符合服务端要求,或 replace 片段问题 | 优先检查未转义字符;replace 场景再看 `block_id` 和 `<content/>` |
|
||||
| 3350002 | `revision_id` 大于当前版本 | 用 `-1` 取当前版本,或重新用 `slides +xml-get` 取最新 `revision_id` |
|
||||
| validation: unsafe file path | `--file` 给了绝对路径或上层路径 | `--file` 必须是 CWD 内相对路径;先 `cd` 到素材目录再执行 |
|
||||
|
||||
## Command-Specific References
|
||||
|
||||
- 图片上传、`@path` 占位符、`file_token`:见 [lark-slides-media-upload.md](lark-slides-media-upload.md) 和 [lark-slides-create.md](lark-slides-create.md)。
|
||||
- 块级替换、`block_id`、3350001 replace 细节:见 [lark-slides-replace-slide.md](lark-slides-replace-slide.md)。
|
||||
- 原生 `slide.create` 包装、`before_slide_id` 和 jq 模板:见 [lark-slides-xml-presentation-slide-create.md](lark-slides-xml-presentation-slide-create.md)。
|
||||
156
.agents/skills/lark-slides/references/validation-checklist.md
Normal file
156
.agents/skills/lark-slides/references/validation-checklist.md
Normal file
@ -0,0 +1,156 @@
|
||||
# Validation Checklist
|
||||
|
||||
创建或大幅改写演示文稿后,必须做一次显式验证。目标是发现空白页、XML 损坏、内容截断、明显溢出、弱视觉层级和未验证输出。
|
||||
|
||||
小型已有页编辑也要做对应范围的验证:至少读取被改页面或全文 XML,确认目标元素已更新且未破坏周边结构。
|
||||
|
||||
## Required Flow
|
||||
|
||||
1. 记录创建或编辑返回的 `xml_presentation_id`,以及已知的 `slide_id` / `revision_id`。
|
||||
2. 用 `slides +xml-get` 回读全文 XML 到本地文件。
|
||||
3. 检查实际页数是否符合计划或用户要求。
|
||||
4. 检查每页 `<data>` 内是否有预期主要元素。
|
||||
5. 检查没有明显空白页、破损页、缺失标题或缺失主视觉。
|
||||
6. 检查页面不是全部退化为标题加 bullet list。
|
||||
7. 检查视觉层级:标题、主视觉、支撑信息三者可区分。
|
||||
8. 检查明显溢出和布局风险:重叠、越界、底部拥挤、长文本框。
|
||||
9. 在最终回复中给出简短验证记录。
|
||||
|
||||
回读命令:
|
||||
|
||||
```bash
|
||||
lark-cli slides +xml-get --as user \
|
||||
--presentation "YOUR_ID" \
|
||||
--output .lark-slides/plan/<deck-or-task-id>/readback.xml \
|
||||
--json
|
||||
```
|
||||
|
||||
## Automated XML Layout Lint
|
||||
|
||||
`slides +xml-get` 保存 XML 后,只运行统一版式准出入口。先取得当前已加载 `lark-slides/SKILL.md` 的父目录,记为 `<lark-slides-skill-dir>`;不要猜测全局安装路径。
|
||||
|
||||
```bash
|
||||
python3 "<lark-slides-skill-dir>/scripts/xml_text_overlap_lint.py" --input <presentation.xml>
|
||||
```
|
||||
|
||||
它一次检查 XML/SXSD 合法性、元素越界、文本重叠、空白页、文本高度风险、整页内容稀疏和大卡片内容覆盖率。大卡片自身 `<content>` 的估算文本面积与卡片内平级元素一起参与覆盖率并集计算。
|
||||
|
||||
准出规则:
|
||||
|
||||
- `summary.error_count > 0` 或 `summary.release_ready == false`:阻断创建、替换或交付,必须先修复。
|
||||
- `summary.warning_count > 0`:静态检查不直接阻断,但 `summary.screenshot_review_required == true`,必须复核对应页面截图。
|
||||
- `slides[].status` 为 `blocked`、`needs_screenshot_review` 或 `passed`,可直接决定逐页后续动作。
|
||||
- CLI 在存在 `error` 时退出码为 1;只有 `warning` 时仍输出 JSON 并退出 0,供截图复核链路继续执行。
|
||||
|
||||
每条 `error` / `warning` 都包含:
|
||||
|
||||
- `element_ids`:相关 XML 元素 ID;
|
||||
- `rule`:规则 ID、名称、阈值和比较关系;
|
||||
- `measurement`:越界量、交叠面积、覆盖率等实测值;
|
||||
- `related_objects`:相关对象的类型与坐标框;
|
||||
- `target`、`message`、`hint`:页码、语义说明和处理建议。
|
||||
|
||||
当 `sparse_container_content.measurement.content_coverage_ratio < rule.threshold` 时,需要结合同页截图判断留白是否有意设计;不要仅凭 warning 自动扩充内容。
|
||||
|
||||
常见 code 的处理方向:
|
||||
|
||||
| code | 含义 | 处理方式 |
|
||||
|------|------|----------|
|
||||
| `xml_not_well_formed` | XML 语法错误或文本未转义 | 修复标签闭合、属性引号、`&` / `<` / `>` 转义 |
|
||||
| `sml_prefixed_tag` | SML 元素使用了命名空间前缀,如 `<ns0:slide>` 或 `<sml:shape>` | 使用 `<slide xmlns="http://www.larkoffice.com/sml/2.0">` 的默认命名空间,或使用无前缀标签 |
|
||||
| `sxsd_unsupported_tag` | 使用了 SXSD 不支持的标签 | 按 lint `hint` 替换为受支持标签;常见如 `textbox -> <shape type="text">`、`image -> <img>` |
|
||||
| `sxsd_unsupported_attr` | 支持的标签上使用了不支持的属性 | 按 lint `hint` 改为支持的属性;常见如 `x -> topLeftX`、`fontColor -> color` |
|
||||
| `iconpark_unsupported_icon_type` | `<icon>` 使用了 `iconpark-index.json` 中不存在的 `iconType` | 按 lint `hint` 改为名单内的 `iconType`,或先用 `scripts/iconpark_tool.py` 搜索 |
|
||||
| `icon_missing_fill_color` | 视觉规范要求 `<icon>` 设置 `<fill><fillColor color="..."/></fill>`,避免图标不可见 | 给 `<icon>` 添加显式非透明填充色,例如 `rgba(37, 99, 235, 1)` |
|
||||
| `icon_transparent_fill_color` | `<icon>` 的 `fillColor` 是透明色,不满足视觉可见性要求 | 改成与背景有足够对比的非透明颜色 |
|
||||
| `bbox_overlap` | 文本元素的估算绘制区域明显重叠 | 拉开文本坐标、缩小文本框/字号,或改成明确的分栏/分组结构 |
|
||||
| `*_out_of_canvas` | 元素边界超出页面画布 | 根据 `measurement.overflow` 移回画布或缩小尺寸 |
|
||||
| `blank_slide` | 页面没有画布内可见内容 | 补充主体内容;仅有空背景或空形状不能准出 |
|
||||
| `sparse_container_content` | 大卡片内容覆盖率低于阈值 | 按元素 ID 定位卡片,结合截图判断是否补充或放大内容 |
|
||||
| `sparse_slide_content` | 全页有效内容覆盖率偏低 | 复核截图,确认是否为有意留白 |
|
||||
|
||||
## Screenshot QA
|
||||
|
||||
获取页面截图后,必须做视觉验收;不要只凭 XML 回读或静态 lint 结论声称截图验收通过。验收时假设页面存在问题,主动寻找并报告所有风险,包括轻微问题。
|
||||
|
||||
```text
|
||||
请逐页目视检查这些幻灯片截图。先假设存在问题,并尽量找出它们。
|
||||
|
||||
重点检查:
|
||||
- 元素重叠:文字与形状、图片或图表互相遮挡,线条穿过文字,卡片或标签堆叠。
|
||||
- 文本溢出或被裁切:靠近页面边缘、文本框边界或卡片边界处被截断。
|
||||
- 装饰元素位置错误:分割线、强调线或标签底板按单行文字布置,但标题或正文换行后压住文字或距离异常。
|
||||
- 来源标注、页脚或页码与上方内容碰撞。
|
||||
- 元素距离过近:相邻元素间距明显不足,卡片或分区几乎贴在一起;按 960x540 画布估算,小于约 15 px 的间隔通常要标记。
|
||||
- 间距不均:局部留白过大,另一处过于拥挤。
|
||||
- 页面边距不足:主体内容贴近幻灯片边缘;按 960x540 画布估算,小于约 30 px 的外边距通常要标记。
|
||||
- 列、卡片、图标或同类元素没有稳定对齐。
|
||||
- 图片或图表渲染异常:空白、变形、低清、关键内容不可读或预期图形缺失。
|
||||
- 文本对比度不足,例如浅灰文字放在米色或浅色背景上。
|
||||
- 图标对比度不足,例如深色图标放在深色背景上,且没有浅色圆形或底板承托。
|
||||
- 文本框过窄,导致不必要的频繁换行。
|
||||
- 残留占位符、模板默认文字或未替换内容。
|
||||
|
||||
对每一页分别列出发现的问题或可疑区域,即使只是轻微问题也要记录。
|
||||
|
||||
报告所有发现的问题,包括轻微问题。
|
||||
```
|
||||
|
||||
必须根据问题严重度决定是否修复:空白页、破图、文字遮挡、明显裁切、低对比不可读、占位符残留等必须先修复再交付;轻微间距或对齐问题如果不修复,最终验证记录要说明已知风险。
|
||||
|
||||
## Page Count And Structure
|
||||
|
||||
- 实际页数必须等于用户要求或 `slide_plan.json` 的页数。
|
||||
- 如果创建过程部分失败,先记录已创建的 `xml_presentation_id`,再回读确认哪些页已写入。
|
||||
- 每页都应包含 `<data>`,且 `<data>` 内至少有一个非背景主体元素。
|
||||
- 封面、章节页、总结页可以文字较少,但不能只有空背景。
|
||||
- 技术解释页、对比页、流程页、架构页必须有匹配的结构元素,例如分组框、连线、时间轴、表格或图形化区域。
|
||||
|
||||
## Expected Elements
|
||||
|
||||
按 `slide_plan.json` 和用户要求逐页核对:
|
||||
|
||||
- 标题或主结论存在,并能对应 `key_message`。
|
||||
- `layout_type` 对应的主要结构已生成。
|
||||
- `visual_focus` 是页面中最醒目或最大的信息区域之一。
|
||||
- `text_density` 影响了文本量,没有用长 bullet 框替代规划。
|
||||
- `asset_need` 有真实素材时已放入正确区域;没有真实素材时,`fallback_if_missing` 已用 XML 形状、线条、标签、表格或图表兜底。
|
||||
|
||||
如果用户指定了关键页,例如“架构解释”“Self-Attention 机制解释”“对比或演进视角”“总结页”,最终验证记录必须逐项说明这些页已存在。
|
||||
|
||||
## Blank Or Broken Page Signals
|
||||
|
||||
把下面情况视为需要修复后再交付:
|
||||
|
||||
- `<data/>` 为空,或只有背景、装饰线、空 `<content/>`。
|
||||
- 关键文本没有出现在回读 XML 中。
|
||||
- 图片仍是 `@./path`,或 `<img src>` 是 http(s) 外链。
|
||||
- 页面依赖的图片区域为空,且没有 fallback visual。
|
||||
- 返回 XML 缺页、页序明显错误,或某页内容被 shell 截断。
|
||||
- 大量形状坐标完全相同,导致主体内容重叠。
|
||||
- 渐变背景回退成空白或白底,导致文字不可读。
|
||||
|
||||
## Layout And Overflow Risk
|
||||
|
||||
优先修复这些明显风险:
|
||||
|
||||
- 正文或标签框高度不足,文本很可能被截断。
|
||||
- 多个主体元素在同一区域重叠,而不是有意叠加背景。
|
||||
- 重要内容越过画布边界,或贴近底部超过 `y=500`。
|
||||
- 高密度页使用单个长 bullet list,没有分栏、表格或分组。
|
||||
- 标题、主视觉、正文的字号和颜色差异太弱,视觉层级不清。
|
||||
- 所有内容页都是同一套标题加 bullets 坐标。
|
||||
|
||||
## Verification Record
|
||||
|
||||
最终回复必须包含简短验证记录,建议格式:
|
||||
|
||||
```text
|
||||
验证记录:
|
||||
- 回读:已执行 slides +xml-get,实际页数 N / 预期 N。
|
||||
- 关键页:架构解释 / Self-Attention / 对比或演进 / 总结页均存在。
|
||||
- 结构:检查了主要 shape/img/table/chart 元素,无明显空白页或破损页。
|
||||
- 布局:检查了标题层级、主视觉、重叠/越界/文本溢出风险。
|
||||
```
|
||||
|
||||
不要声称完成了人工视觉验收,除非确实打开或获取了可视化结果。仅从 XML 静态检查得出的结论,应表述为“静态检查未发现明显问题”。
|
||||
258
.agents/skills/lark-slides/references/visual-planning.md
Normal file
258
.agents/skills/lark-slides/references/visual-planning.md
Normal file
@ -0,0 +1,258 @@
|
||||
# Visual Planning
|
||||
|
||||
新建演示文稿或大幅改写页面时,在 `slide_plan.json` 完成后、生成 XML 前读取本文件。目标是让 `layout_type`、`visual_focus`、`text_density` 变成实际页面几何,而不是只写在 plan 里。
|
||||
|
||||
默认画布按 `960 x 540` 规划。模板 XML 可以覆盖具体坐标,但不能覆盖这些原则:页面要有主视觉区域、文本要受密度约束、不同 `layout_type` 必须产生明显不同的坐标结构。
|
||||
|
||||
## Core Rules
|
||||
|
||||
- `layout_type` must change geometry: element positions, region sizes, alignment, and visual rhythm must differ across page types.
|
||||
- `visual_focus` determines the largest or highest-contrast region. It can be an image, diagram, metric, quote, or table.
|
||||
- `text_density` caps visible text:
|
||||
- `low`: title plus one short statement, or 1-3 labels.
|
||||
- `medium`: title plus 2-4 concise bullets or labeled regions.
|
||||
- `high`: use a table, columns, grouped labels, or annotations. Do not use one long bullet box.
|
||||
- Do not create a deck where every content page is title plus bullets. For 4 or more pages, use at least 4 different layout structures when the content allows.
|
||||
- Keep safe outer margins around `40` px on standard content pages, and fill the content area densely with a card grid rather than leaving large empty space. Only go full-bleed for an intentional image or cover treatment.
|
||||
- Reserve vertical space for titles. A typical content title area is `y=36..90`; main content should usually start at `y>=110`.
|
||||
- Avoid crowding the bottom edge. Keep non-background content above `y=500` unless it is a footer.
|
||||
- Keep backgrounds consistent with the deck's `visual_system.background_strategy`. Normal content pages should use the same base background unless there is a clear page-role reason to change.
|
||||
- Treat text fit as a layout constraint, not a cleanup step. If a text box is too small for the intended line count, shorten the text, split it, or allocate more space before creating XML.
|
||||
- Do not use `<shape>` to build pictorial visuals like mock photos or fake objects. Use the image generation tool instead.
|
||||
- Do not place a `rect` or `line` for dividing or decorative purposes directly under a `headline` or `title`.
|
||||
- Do not use section bands, horizontal bars, vertical bars, or page-edge strips.
|
||||
|
||||
## Background And Motif Consistency
|
||||
|
||||
Decks can vary page backgrounds, but variation must be intentional and legible:
|
||||
|
||||
- Pick one default background for ordinary content pages and reuse it exactly. Avoid near-identical drift such as several slightly different off-white values unless it encodes a clear section change.
|
||||
- Cover, section divider, emphasis, and conclusion pages may use a dark, image-led, or high-contrast background. They must still share the deck's primary color, motif, typography, or geometry.
|
||||
- If a cover uses a split composition, make the split visible in the background or layout. For example, reserve a darker text region and a related but distinct visual region instead of placing all elements on one flat field.
|
||||
- Reuse a small number of visual devices: card radius, node style, icon container, or footer treatment. Do not introduce a new decorative language on each page.
|
||||
- Insert background and motif shapes before content elements so they do not cover text, images, or diagrams.
|
||||
|
||||
## Text Fit Guardrails
|
||||
|
||||
Use these as conservative minimums on a 960 x 540 canvas. Increase height when using bold text, Chinese text, mixed Chinese/English, or line spacing above default.
|
||||
|
||||
| Text use | Typical font size | Minimum height |
|
||||
|----------|-------------------|----------------|
|
||||
| Caption, 1 line | 10-12 | 18 |
|
||||
| Caption, 2 lines | 10-12 | 30 |
|
||||
| Body, 1 line | 12-14 | 24 |
|
||||
| Body, 2 lines | 12-14 | 40 |
|
||||
| Body, 2 lines, bold | 12-14 | 48 |
|
||||
| Headline, 1 line | 20-28 | 42 |
|
||||
| Title, 2 lines | 28-36 | 110 |
|
||||
|
||||
Additional rules:
|
||||
|
||||
- Do not put long Chinese sentences or long English phrases into `height=18` or `height=22` boxes. Those heights are for short labels only.
|
||||
- Footer/source text should usually be one short line. If it needs more, make it a real caption block above the footer area.
|
||||
- Bottom conclusion bars should be at least `40` px tall for one emphasized line and at least `54` px tall for two lines.
|
||||
- Diagram labels should be short enough to fit the shape. Prefer two short lines over one cramped long line.
|
||||
- When a text block has more than one `<p>`, size the box for multiple lines explicitly. Do not assume the renderer will auto-expand.
|
||||
- If a line contains mixed Chinese and English, budget more width than either language alone; mixed text wraps less predictably.
|
||||
|
||||
## Layout Types
|
||||
|
||||
### `title-cover`
|
||||
|
||||
Purpose: introduce the deck's point of view.
|
||||
|
||||
Geometry:
|
||||
- Use one dominant title block, usually `x=70..120`, `y=150..250`, `width=700..820`.
|
||||
- Add one subtitle or context line, not a bullet list.
|
||||
- Visual focus MUST be an `<img>`: a full-bleed background image or a large **full-height** side image (searched by the image search tool or generated by the image generation tool). Do NOT compose the cover visual from `<shape>` or `<icon>`.
|
||||
- If the cover has a large full-height side image, use a split layout: keep the title and subtitle in the text region on the opposite side, and reserve a separate visual region so the image does not overlap the title. Crop (size and place) the side image so it stays within its visual region and does not extend into the text region.
|
||||
- For split covers, make the background reinforce the composition, such as a darker text side and a related visual panel. Avoid one flat field where title and diagram compete for attention.
|
||||
- Keep source metadata to one short line where possible. If it wraps, shorten author lists or move details to notes.
|
||||
- The main title should be controlled, normally one or two lines. Do not let it occupy both the text region and the visual region.
|
||||
- Do not add a vertical accent bar, side rail, or decorative line/strip.
|
||||
|
||||
Text:
|
||||
- `low` only unless the user explicitly asks for detail.
|
||||
|
||||
### `section-divider`
|
||||
|
||||
Purpose: reset rhythm and mark a new chapter.
|
||||
|
||||
Geometry:
|
||||
- Use a large section number, chapter label, or single centered claim.
|
||||
- Keep the page sparse. A divider is not a content page.
|
||||
- Visual focus can be one oversized number.
|
||||
|
||||
Text:
|
||||
- Title plus one phrase. No bullets.
|
||||
|
||||
### `two-column`
|
||||
|
||||
Purpose: compare two related ideas or pair explanation with evidence.
|
||||
|
||||
Geometry:
|
||||
- Split main region into two balanced columns, for example left `x=60,width=400`, right `x=500,width=400`.
|
||||
- Each column needs its own heading or visual anchor.
|
||||
- Do not place one full-width bullet box under a normal title; that is not a two-column layout.
|
||||
|
||||
Text:
|
||||
- `medium`: 2-3 short items per column.
|
||||
- `high`: use grouped rows or mini table structure inside columns.
|
||||
|
||||
### `image-left-text-right`
|
||||
|
||||
Purpose: let a visual establish context, with text explaining implication.
|
||||
|
||||
Geometry:
|
||||
- Left visual region should occupy roughly `35-45%` of slide width, often full height or tall crop.
|
||||
- Right text region starts around `x=420` and should have a strong headline plus short support.
|
||||
- If no image is available, use the image generation tool to create an approximate image that matches `asset_need`.
|
||||
- For dense screenshots, paper figures, or product captures with small labels, allocate a larger visual region when possible: often `50-65%` of slide width or at least `320` px height.
|
||||
- Place screenshots in a deliberate frame or panel, and leave enough margin so axes, captions, and edge labels are not cropped by the slide boundary.
|
||||
|
||||
Text:
|
||||
- Keep right-side text short. Avoid more than 4 bullets.
|
||||
- For screenshot explanation pages, prefer 2-3 interpretation cards or callouts instead of a paragraph block.
|
||||
|
||||
### `image-right-text-left`
|
||||
|
||||
Purpose: lead with a message, then reinforce it with a visual.
|
||||
|
||||
Geometry:
|
||||
- Left text region starts around `x=60..90`, width `400..460`.
|
||||
- Right visual region occupies roughly `35-45%` of slide width.
|
||||
- Align the image with the main text block, not only with the title.
|
||||
- For dense screenshots, paper figures, or product captures with small labels, increase the visual region and reduce text. A readable image is more valuable than a fully populated text column.
|
||||
|
||||
Text:
|
||||
- Use one main claim and 2-3 supporting points.
|
||||
- Keep callouts parallel and short. If a callout needs more than two lines, split it into a separate note or a new slide.
|
||||
|
||||
### `big-number`
|
||||
|
||||
Purpose: make one metric or fact memorable.
|
||||
|
||||
Geometry:
|
||||
- Reserve the largest object for the metric: font size often `40-52`.
|
||||
- MUST Set `wrap="true" autoFit="normal-auto-fit"` on the metric's `<content>` so an oversized number shrinks to fit its box.
|
||||
- Pair the number with one explanation and optional 2-3 small supporting labels.
|
||||
- Do not bury the number in a bullet list or small card.
|
||||
|
||||
Text:
|
||||
- `low` or `medium`. If detail is needed, add small annotations around the metric.
|
||||
- Supporting labels must not compete with the number. Use compact labels, legends, or mini-cards rather than long explanatory bars.
|
||||
|
||||
### `timeline`
|
||||
|
||||
Purpose: show sequence, roadmap, history, or phases.
|
||||
|
||||
Geometry:
|
||||
- Create a horizontal or vertical spine with 3-6 milestones.
|
||||
- Each milestone should have a dot/card/date label connected by a line or arrow.
|
||||
- Title is separate from the sequence. The sequence is the visual focus.
|
||||
|
||||
Text:
|
||||
- Each milestone gets a short label and optional one-line explanation.
|
||||
- Do not use paragraph-length milestone descriptions.
|
||||
|
||||
### `comparison`
|
||||
|
||||
Purpose: make a choice, before/after, old/new, or option tradeoff clear.
|
||||
|
||||
Geometry:
|
||||
- Use two or three distinct panels, columns, or a table-like structure.
|
||||
- Headings must be visually aligned so differences are easy to scan.
|
||||
- Use color, border, icon, or label treatment to highlight the preferred option or key difference.
|
||||
|
||||
Text:
|
||||
- Use parallel wording across columns.
|
||||
- Avoid uneven long bullet lists that destroy comparability.
|
||||
|
||||
### `architecture-diagram`
|
||||
|
||||
Purpose: explain components, dependencies, or system flow.
|
||||
|
||||
Implementation: use `<shape>` + `<line>`.
|
||||
|
||||
Geometry:
|
||||
- Main visual area should be a diagram, not prose.
|
||||
- Use grouped boxes, lanes, arrows or lines, and short labels.
|
||||
- Keep diagram labels concise. Put explanation in notes or a small side caption if needed.
|
||||
|
||||
Text:
|
||||
- Prefer labels of 1-5 words.
|
||||
- Use no more than one short explanatory text block.
|
||||
- If a node label needs two lines, size the node and the text box for two lines. Do not let labels overlap connectors.
|
||||
|
||||
### `process-flow`
|
||||
|
||||
Purpose: show operational steps, workflow, or cause-effect path.
|
||||
|
||||
Implementation: use `<shape>` + `<line>`.
|
||||
|
||||
Geometry:
|
||||
- Use numbered steps connected by arrows or lines.
|
||||
- 3-5 steps is ideal for one slide. If there are more, group them into phases.
|
||||
- The flow direction must be visually obvious.
|
||||
|
||||
Text:
|
||||
- Each step gets a verb-led label and one short descriptor at most.
|
||||
- Step labels should be parallel in length and grammar. If one step needs a long explanation, move the explanation to a side note or speaker notes.
|
||||
|
||||
### `quote-highlight`
|
||||
|
||||
Purpose: emphasize a customer voice, principle, thesis, or decision statement.
|
||||
|
||||
Geometry:
|
||||
- Quote or claim is the dominant text object.
|
||||
- Use large type, generous whitespace, and optional attribution or context badge.
|
||||
- Do not combine a quote-highlight page with a normal bullet section.
|
||||
|
||||
Text:
|
||||
- One quote or statement, plus optional attribution. No bullets.
|
||||
|
||||
### `conclusion`
|
||||
|
||||
Purpose: close with decision, recommendation, or next action.
|
||||
|
||||
Geometry:
|
||||
- Use one dominant closing statement or call to action.
|
||||
- Visual focus should be the recommendation or action, not decorative filler.
|
||||
- When using a full-bleed background image, add a semi-transparent scrim between the image and the text so the text stays legible; verify contrast.
|
||||
|
||||
Text:
|
||||
- Keep the final page easy to remember. Avoid recap overload.
|
||||
- Conclusion pages may mirror the cover background.
|
||||
|
||||
## Screenshot And Paper Figure Pages
|
||||
|
||||
When a page uses a real screenshot, chart, paper figure, or product capture:
|
||||
|
||||
- Choose screenshot placement based on page role, not a fixed slide number. Method overview, evidence, comparison, and failure-analysis pages are common candidates; title, agenda, and conclusion pages usually are not.
|
||||
- Use the real asset only when it is readable at slide size. If the figure is too dense, crop to the relevant region, create a zoomed detail, or regenerate the core message with the image generation tool.
|
||||
- A screenshot should normally be the visual focus. Do not shrink it into a decorative thumbnail while surrounding it with dense text.
|
||||
- Pair the image with a small number of interpretive annotations that tell the audience what to notice.
|
||||
- Always include a short source caption when using external or paper-derived visuals.
|
||||
- Verify the final XML contains a supported image token or creation-time local placeholder, not an unsupported external URL.
|
||||
|
||||
## Plan To XML Checklist
|
||||
|
||||
Before creating XML for each page, answer these checks:
|
||||
|
||||
1. Which region is the visual focus, and is it the largest or most prominent object?
|
||||
2. Does the XML geometry match the `layout_type` description above?
|
||||
3. Does `text_density` limit the number of paragraphs, bullets, labels, and text boxes?
|
||||
4. Would this page still be recognizable if the `layout_type` label were removed from the plan?
|
||||
5. Across the deck, do multiple pages use genuinely different structures?
|
||||
6. Does the background follow the planned deck strategy, and are any deviations intentional?
|
||||
7. Are all text boxes large enough for their intended font size and line count?
|
||||
8. If the page uses a screenshot or paper figure, is it large enough to read and accompanied by concise interpretation?
|
||||
|
||||
After fetching the created presentation, verify:
|
||||
|
||||
- Use `timeline`, `comparison`, and `architecture-diagram` only when the content calls for them; do not force irrelevant page types.
|
||||
- Any planned `timeline`, `comparison`, or `architecture-diagram` page uses the matching sequence, side-by-side comparison, or component-and-connection structure.
|
||||
- Pages are not crowded and do not rely on long bullet boxes.
|
||||
- Main claim, supporting detail, and visual focus have clear hierarchy.
|
||||
- Static XML inspection should include text-fit risk: very short text boxes containing long text, multi-paragraph boxes with insufficient height, footer text that may wrap, and labels placed directly over connectors.
|
||||
- Background and motif consistency should be checked across pages, not only within one slide.
|
||||
485
.agents/skills/lark-slides/references/xml-schema-quick-ref.md
Normal file
485
.agents/skills/lark-slides/references/xml-schema-quick-ref.md
Normal file
@ -0,0 +1,485 @@
|
||||
# XML Schema 快速参考
|
||||
|
||||
本文档是 [slides_xml_schema_definition.xml](slides_xml_schema_definition.xml) 的精简版摘要,并合并了常用 XML 格式写法;如果两者不一致,以 XSD 原文为准。
|
||||
|
||||
## 最重要的规则
|
||||
|
||||
1. 协议标准写法应使用 `<presentation xmlns="http://www.larkoffice.com/sml/2.0">`;当前服务端实现可能兼容不带 `xmlns` 的输入,但不作为协议保证
|
||||
2. `<presentation>` 直接子元素只有 `<title>`、`<theme>`、`<slide>`
|
||||
3. `<slide>` 直接子元素只有 `<style>`、`<data>`、`<note>`
|
||||
4. 页面中的文本通常通过 `<content>` 表达,而不是把 `<title>`、`<body>` 直接挂在 `<slide>` 下
|
||||
|
||||
## 最小可用示例
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
||||
<slide>
|
||||
<data>
|
||||
<shape type="text" topLeftX="80" topLeftY="80" width="800" height="120">
|
||||
<content autoFit="normal-auto-fit" wrap="true" textType="title">
|
||||
<p>标题</p>
|
||||
</content>
|
||||
</shape>
|
||||
</data>
|
||||
</slide>
|
||||
</presentation>
|
||||
```
|
||||
|
||||
## presentation 根元素
|
||||
|
||||
| 属性 | 必需 | 说明 |
|
||||
|------|------|------|
|
||||
| `width` | 是 | 演示文稿宽度,正整数,标准 16:9 页面建议使用 `960` |
|
||||
| `height` | 是 | 演示文稿高度,正整数,标准 16:9 页面建议使用 `540` |
|
||||
| `id` | 否 | 演示文稿标识 |
|
||||
|
||||
**子元素:** `<title>?`, `<theme>?`, `<slide>+`
|
||||
|
||||
`<slide>` 至少 1 页,最多 100 页。
|
||||
|
||||
## theme 与文本类型
|
||||
|
||||
`<theme>` 当前包含两部分:
|
||||
|
||||
- `<background>`:演示文稿级背景填充
|
||||
- `<textStyles>`:主题文本样式集合
|
||||
|
||||
`<textStyles>` 下可选子元素包括 `<title>`、`<headline>`、`<sub-headline>`、`<body>`、`<caption>`。这些元素定义的是主题默认样式,不是页面结构。
|
||||
|
||||
常用属性:
|
||||
|
||||
| 属性 | 说明 |
|
||||
|------|------|
|
||||
| `fontFamily` | 字体 |
|
||||
| `fontSize` | 字号 |
|
||||
| `fontColor` | 字体颜色 |
|
||||
|
||||
XSD 中的 `title`、`headline`、`sub-headline`、`body`、`caption` 主要出现在:
|
||||
|
||||
- `<theme><textStyles>...</textStyles></theme>` 中,作为主题文本样式
|
||||
- `<content textType="...">` 中,作为内容的文本类型
|
||||
|
||||
`textStyles` 的 schema 默认值如下:
|
||||
|
||||
| textType | 默认字号 |
|
||||
|----------|----------|
|
||||
| `title` | 54 |
|
||||
| `headline` | 38 |
|
||||
| `sub-headline` | 32 |
|
||||
| `body` | 16 |
|
||||
| `caption` | 12 |
|
||||
|
||||
默认字号是省略 `fontSize` 时的兜底字号,不是推荐值。字号必须显式设置 `<content>` 的 `fontSize` 属性,不要依赖 `textType` 的默认字号兜底,这些兜底值明显偏大。
|
||||
|
||||
## slide 元素
|
||||
|
||||
| 属性 | 必需 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | 否 | 幻灯片标识 |
|
||||
|
||||
**子元素:**
|
||||
|
||||
- `<style>?` - 页面样式,目前可放 `<fill>`
|
||||
- `<data>?` - 页面元素容器,可放 `shape`、`line`、`polyline`、`img`、`table`、`icon`、`chart`、`undefined`
|
||||
- `<note>?` - 演讲者备注,内部可放 `<content>`
|
||||
|
||||
这意味着 `<title>`、`<headline>`、`<body>`、`<caption>` 不能直接放在 `<slide>` 下。
|
||||
|
||||
## content 内容模型
|
||||
|
||||
`<content>` 可出现在 `shape`、`table/td`、`note` 中,常用属性包括:
|
||||
|
||||
| 属性 | 说明 |
|
||||
|------|------|
|
||||
| `textType` | `title` / `headline` / `sub-headline` / `body` / `caption` |
|
||||
| `verticalAlign` | 垂直对齐 |
|
||||
| `textAlign` | 文本对齐方式 |
|
||||
| `lineSpacing` | 行间距,schema 默认 `multiple:1.5` |
|
||||
| `fontSize` | 字号 |
|
||||
| `fontFamily` | 字体 |
|
||||
| `color` | 字体颜色 |
|
||||
| `bold` / `italic` / `underline` / `strikethrough` | 内容级样式 |
|
||||
| `wrap` | 是否自动换行 |
|
||||
| `autoFit` | 是否自动缩排 |
|
||||
|
||||
注意事项:
|
||||
|
||||
- 字号必须显式设置 `<content>` 的 `fontSize` 属性,不要依赖 `textType` 的默认字号兜底,这些兜底值明显偏大。
|
||||
- 大数字、字号大或字数多的 `<content>` 必须设置 `wrap="true" autoFit="normal-auto-fit"` 属性自动换行和缩排,避免文字溢出。
|
||||
- 文字颜色必须用 `<content>` 的 `color` 属性而不是 `fontColor` 属性。
|
||||
- 文字行间距必须设置 `<content>` 的 `lineSpacing="multiple:xx"` 或 `lineSpacing="fixed:xx"` 而不是 `lineSpacing="xx"`。
|
||||
|
||||
`<content>` 直接子元素只有:
|
||||
|
||||
- `<p>`
|
||||
- `<ul>`
|
||||
- `<ol>`
|
||||
|
||||
### p 段落与内联标签
|
||||
|
||||
`<p>` 是段落元素,可混排纯文本和内联标签:
|
||||
|
||||
- `<br/>`
|
||||
- `<strong>`
|
||||
- `<em>`
|
||||
- `<u>`
|
||||
- `<span>`
|
||||
- `<del>`
|
||||
- `<a>`
|
||||
- `<shadow>`
|
||||
- `<outline>`
|
||||
- `<formula>`
|
||||
|
||||
公式写法:
|
||||
|
||||
```xml
|
||||
<p>公式:<formula><latex><![CDATA[ E = mc^2 ]]></latex></formula></p>
|
||||
```
|
||||
|
||||
`<formula>` 是内联元素;当前只支持一个 `<latex>` 子元素。LaTeX 内容必须放在 `CDATA` 中,且 `CDATA` 内不要写 XML 转义;宏只使用服务端支持范围内的写法,优先用基础运算符、`\frac`、`\sqrt`、`matrix`。
|
||||
|
||||
示例:
|
||||
|
||||
```xml
|
||||
<content autoFit="normal-auto-fit" textType="body" textAlign="left">
|
||||
<p>正文内容 <strong>加粗</strong> <em>斜体</em> <a href="https://example.com">链接</a></p>
|
||||
<ul>
|
||||
<li><p>列表项 1</p></li>
|
||||
<li><p>列表项 2</p></li>
|
||||
</ul>
|
||||
</content>
|
||||
```
|
||||
|
||||
## data 常用元素
|
||||
|
||||
所有页面元素都放在 `<data>` 中。
|
||||
|
||||
### shape
|
||||
|
||||
`shape` 可表示普通形状,也可表示文本框。文本框推荐使用 `type="text"`。
|
||||
|
||||
```xml
|
||||
<shape type="text" topLeftX="80" topLeftY="80" width="800" height="120">
|
||||
<content textType="title">
|
||||
<p>主标题</p>
|
||||
</content>
|
||||
</shape>
|
||||
```
|
||||
|
||||
```xml
|
||||
<shape type="rect" topLeftX="120" topLeftY="120" width="240" height="120">
|
||||
<fill>
|
||||
<fillColor color="rgb(100, 149, 237)"/>
|
||||
</fill>
|
||||
<border color="rgb(0, 0, 0)" width="2"/>
|
||||
</shape>
|
||||
```
|
||||
`<shape type="rect">` 只是形状不是容器,`<icon>`、`<img>`、`<shape type="text">` 和其他 `<shape>` 必须与它平级靠坐标叠放。
|
||||
|
||||
|
||||
| 属性 | 必需 | 说明 |
|
||||
|------|------|------|
|
||||
| `type` | 是 | 形状类型,`text` 表示文本框 |
|
||||
| `topLeftX` | 是 | 左上角 X 坐标 |
|
||||
| `topLeftY` | 是 | 左上角 Y 坐标 |
|
||||
| `width` | 是 | 宽度 |
|
||||
| `height` | 是 | 高度 |
|
||||
| `rotation` | 否 | 旋转角度 |
|
||||
| `flipX` / `flipY` | 否 | 翻转 |
|
||||
| `alpha` | 否 | 透明度 |
|
||||
|
||||
可选子元素:
|
||||
|
||||
- `<fill>`
|
||||
- `<border>`
|
||||
- `<reflection>`
|
||||
- `<shadow>`
|
||||
- `<content>`
|
||||
|
||||
`type` 常用取值:`text`(文本框)、`rect`、`round-rect`(圆角矩形)、`ellipse`(椭圆/圆)、`triangle`、`diamond`、`parallelogram`、`trapezoid`、`custom`(配合 `path` 属性写 SVG 路径串)。箭头、星形、标注气泡、`chevron`、`flow-chart-*` 等更多形状见 XSD `ShapeType` 枚举。
|
||||
|
||||
其它可选属性:
|
||||
|
||||
- `presetHandlers`:控制点,用于圆角等。例如 `<shape type="rect" presetHandlers="60">` = 圆角半径 60px 的圆角矩形;多个控制点用逗号分隔。
|
||||
- `path`:仅 `type="custom"` 时使用,SVG 路径串。
|
||||
|
||||
### line
|
||||
|
||||
```xml
|
||||
<line startX="120" startY="120" endX="420" endY="120">
|
||||
<border color="rgb(43, 47, 54)" width="2"/>
|
||||
</line>
|
||||
```
|
||||
|
||||
`line` 使用的是 `startX` / `startY` / `endX` / `endY`,不是 `x1` / `y1` / `x2` / `y2`。
|
||||
|
||||
### polyline
|
||||
|
||||
折线 / 曲线连接线,用外接矩形定位(`topLeftX` / `topLeftY` / `width` / `height`),不是端点坐标;`<border>` 必填(无 border 不可见)。`type` 默认 `bent-connector2`(可选 `bent-connector2-5` 折线 / `curved-connector2-5` 曲线)。
|
||||
|
||||
```xml
|
||||
<polyline topLeftX="120" topLeftY="120" width="200" height="100">
|
||||
<border color="rgb(43, 47, 54)" width="2"/>
|
||||
</polyline>
|
||||
```
|
||||
|
||||
### img
|
||||
|
||||
```xml
|
||||
<img src="file_token_or_url" topLeftX="80" topLeftY="120" width="320" height="180"/>
|
||||
```
|
||||
|
||||
`img` 使用 `topLeftX` / `topLeftY`,不是 `x` / `y`。
|
||||
|
||||
`src` 只支持:`slides +media-upload` 返回的 `file_token`,或 `@<本地路径>` 占位符(仅 `+create --slides` 自动上传并替换)。**禁止使用 http(s) 外链 URL**——飞书 slides 渲染端不会代理外链图,外链 src 在 PPT 里通常不显示。本地图片详见 [lark-slides-create.md](lark-slides-create.md#本地图片path-占位符) / [lark-slides-media-upload.md](lark-slides-media-upload.md)。
|
||||
|
||||
本地图片的两种姿势:
|
||||
|
||||
- 新建带图 PPT:`+create --slides` 里直接写 `src="@./pic.png"`,CLI 在创空白 PPT 后、加 slides 前自动上传并替换 token
|
||||
- 给已有 PPT 加带图新页:先 `slides +media-upload --file ./pic.png --presentation $PID` 拿 token,再用 token 写进 `xml_presentation.slide create` 的 XML
|
||||
|
||||
> **注意**:`width`/`height` 是**裁剪后**的显示尺寸。比例和原图不一致时会自动裁剪(无法靠属性关闭),想避免裁剪就让 `width:height` 对齐原图比例。
|
||||
|
||||
### icon
|
||||
|
||||
```xml
|
||||
<icon iconType="iconpark/Charts/chart-line.svg" topLeftX="80" topLeftY="120" width="32" height="32">
|
||||
<fill>
|
||||
<fillColor color="rgba(37, 99, 235, 1)"/>
|
||||
</fill>
|
||||
</icon>
|
||||
```
|
||||
|
||||
图标必须填充颜色并和背景有足够对比。
|
||||
|
||||
禁止盲猜 iconType,必须先检索 IconPark,再写 `<icon iconType="...">`。检索方式和更多规则见 [iconpark.md](iconpark.md)。
|
||||
|
||||
|
||||
### table
|
||||
|
||||
表格结构为:
|
||||
|
||||
- `<table>` 直接子元素只有 `<colgroup>` 和 `<tr>`,`width` 和 `height` 分别表示表格的目标总宽度和总高度。
|
||||
- `<colgroup>` 直接子元素只有 `<col width="...">`,width 定义列宽,默认 110。
|
||||
- `<tr height="...">` 直接子元素只有 `<td>`,height 定义行高,默认 37。
|
||||
- `<td>` 直接子元素只有 `<fill>`(背景)、`<content>`(文字)和边框配置(一般不用),不能嵌套 `<shape>`、`<img>`、`<icon>`。
|
||||
- 合并单元格:`<td>` 上用 `colspan`(跨列,默认 1)和 `rowspan`(跨行,默认 1);被合并覆盖的单元格不再写对应 `<td>`。
|
||||
|
||||
表头默认的白底白字视觉效果极差,必须设置背景和文字颜色,需在首行每个 `<td>` 上加 `<fill>`(配合 `bold` 与对比文字色)与正文行区分。
|
||||
|
||||
表格里的文字默认是居中对齐,可以设置 `textAlign` 调整对齐方式。
|
||||
|
||||
表格宽高设置:
|
||||
|
||||
- 已设置的列宽和行高优先保留,未设置的列宽、行高会使用表格的目标总宽度、总高度分配剩余空间
|
||||
- **必须设置 `<table>` 的 `width` 和 `height` 固定表格大小,同时设置需要保留列宽或行高的 `<col>` 的 `width` 和 `<tr>` 的 `height`,其余自动分配。**
|
||||
|
||||
不同字号的行高参考:
|
||||
|
||||
| `fontSize` | 内容行数 | 紧凑 `height` | 适中 `height` | 宽松 `height` |
|
||||
|------|------|------|------|------|
|
||||
| 10 | 单行 | 16 | 20 | 24 |
|
||||
| 12 | 单行 | 20 | 24 | 28 |
|
||||
| 10 | 双行 | 32 | 36 | 42 |
|
||||
| 12 | 双行 | 36 | 42 | 48 |
|
||||
|
||||
示例:
|
||||
|
||||
```xml
|
||||
<table topLeftX="80" topLeftY="140" width="520" height="52">
|
||||
<colgroup>
|
||||
<col width="160"/>
|
||||
<col width="120"/>
|
||||
<col />
|
||||
</colgroup>
|
||||
<tr height="28">
|
||||
<td>
|
||||
<fill><fillColor color="rgba(30,60,114,1)"/></fill>
|
||||
<content textType="body" fontSize="12" bold="true" color="rgba(255,255,255,1)" textAlign="center"><p>项目</p></content>
|
||||
</td>
|
||||
<td>
|
||||
<fill><fillColor color="rgba(30,60,114,1)"/></fill>
|
||||
<content textType="body" fontSize="12" bold="true" color="rgba(255,255,255,1)" textAlign="right"><p>营收</p></content>
|
||||
</td>
|
||||
<td>
|
||||
<fill><fillColor color="rgba(30,60,114,1)"/></fill>
|
||||
<content textType="body" fontSize="12" bold="true" color="rgba(255,255,255,1)" textAlign="left"><p>备注说明</p></content>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><content textType="body" fontSize="10" textAlign="center"><p>线上业务</p></content></td>
|
||||
<td><content textType="body" fontSize="10" textAlign="right"><p>195</p></content></td>
|
||||
<td><content textType="body" fontSize="10" textAlign="left"><p>同比增长 8%,主要来自新客</p></content></td>
|
||||
</tr>
|
||||
</table>
|
||||
```
|
||||
|
||||
### chart
|
||||
|
||||
图表语法十分复杂,必须阅读 [slides_chart_demo.xml](slides_chart_demo.xml),直接照抄其中的柱状、条形、折线、面积、饼(环)、雷达、组合图。
|
||||
|
||||
`<chart>` 直接子元素必须有 `<chartPlotArea>`(绘图区)和 `<chartData>`(数据);`<chartTitle>`、`<chartSubTitle>`、`<chartStyle>`、`<chartLegend>`、`<chartTooltip>` 可选,如果想不展示标题、副标题、图例或悬浮提示,省略相应元素标签即可。
|
||||
|
||||
`<chartStyle>` 常用子元素:
|
||||
|
||||
- `<chartBackground>`:`color` 省略时由渲染端决定默认背景;需要完全透明请显式写 `color="rgba(0, 0, 0, 0)"`
|
||||
- `<chartBorder>`:无边框可写 `width="0"`,或直接不写 `<chartBorder>` 元素
|
||||
|
||||
#### 图表渐变 `<fillGradient>` / `<strokeGradient>`
|
||||
|
||||
图表支持渐变填充/描边,`<fillGradient>` 用于面积、柱子、数据点、扇区填充,`<strokeGradient>` 用于线条、数据点边框、柱子边框。渐变只能挂在系列级或单元素级,不要挂在 `<chartPlot>` 全局层。
|
||||
|
||||
可挂载位置:
|
||||
|
||||
- 系列级:`<chartBars>` / `<chartPoints>` 支持 `<fillGradient>` 与 `<strokeGradient>`;`<chartLine>` 只支持 `<strokeGradient>`;`<chartArea>` / `<chartSectors>` 只支持 `<fillGradient>`
|
||||
- 单元素级:`<chartBar index="...">` / `<chartPoint index="...">` / `<chartSector index="...">` 只支持 `<fillGradient>`
|
||||
- 全局级:`<chartPlot>` 下的 `<chartLines>` / `<chartAreas>` / `<chartBars>` / `<chartPoints>` 不支持渐变
|
||||
|
||||
结构要点:`type` 必填,可为 `linear` 或 `radial`;`linear` 用 `x0` / `y0` / `x1` / `y1`,`radial` 用 `r0` / `r1`;`<stops>` 至少包含 2 个 `<stop>`,`offset` 与 `opacity` 取值均为 `[0, 1]`。
|
||||
|
||||
```xml
|
||||
<chartSeries index="1">
|
||||
<chartBars>
|
||||
<fillGradient type="linear" x0="0" y0="0" x1="0" y1="1">
|
||||
<stops>
|
||||
<stop offset="0" color="rgb(28, 71, 120)"/>
|
||||
<stop offset="1" color="rgb(28, 71, 120)" opacity="0.3"/>
|
||||
</stops>
|
||||
</fillGradient>
|
||||
</chartBars>
|
||||
</chartSeries>
|
||||
```
|
||||
|
||||
隐藏 `<chart>` 的图例只能通过不写或删除 `<chartLegend>` 实现,`<chartLegend>` 不支持 `position="none"`。
|
||||
|
||||
详细用法见 [slides_xml_schema_definition.xml](slides_xml_schema_definition.xml)。
|
||||
|
||||
## 颜色与样式
|
||||
|
||||
### fill
|
||||
|
||||
```xml
|
||||
<fill>
|
||||
<fillColor color="rgb(255, 0, 0)"/>
|
||||
</fill>
|
||||
```
|
||||
|
||||
### border
|
||||
|
||||
```xml
|
||||
<border color="rgb(43, 47, 54)" width="2" dashArray="solid"/>
|
||||
```
|
||||
|
||||
### 颜色格式
|
||||
|
||||
```xml
|
||||
<fillColor color="rgb(255, 0, 0)"/>
|
||||
<fillColor color="rgba(255, 0, 0, 0.5)"/>
|
||||
<fillColor color="linear-gradient(90deg, rgb(255,0,0) 0%, rgb(0,0,255) 100%)"/>
|
||||
<fillColor color="radial-gradient(circle at 50% 50%, rgb(255,0,0) 0%, rgb(0,0,255) 100%)"/>
|
||||
```
|
||||
|
||||
> **注意**:渐变色必须使用 `rgba()` 格式并带百分比停靠点,例如 `linear-gradient(135deg,rgba(30,60,114,1) 0%,rgba(59,130,246,1) 100%)`。使用 `rgb()` 或省略停靠点会导致服务端将其回退为白色。此规则对页面背景和 shape fill 均适用。
|
||||
|
||||
### 页面背景
|
||||
|
||||
```xml
|
||||
<!-- 纯色背景 -->
|
||||
<slide>
|
||||
<style>
|
||||
<fill>
|
||||
<fillColor color="rgb(245, 245, 245)"/>
|
||||
</fill>
|
||||
</style>
|
||||
</slide>
|
||||
|
||||
<!-- 渐变背景(必须用 rgba + 百分比停靠点) -->
|
||||
<slide>
|
||||
<style>
|
||||
<fill>
|
||||
<fillColor color="linear-gradient(135deg,rgba(30,60,114,1) 0%,rgba(59,130,246,1) 100%)"/>
|
||||
</fill>
|
||||
</style>
|
||||
</slide>
|
||||
```
|
||||
|
||||
## 备注示例
|
||||
|
||||
```xml
|
||||
<note>
|
||||
<content autoFit="normal-auto-fit" textType="body">
|
||||
<p>这是演讲者备注。</p>
|
||||
</content>
|
||||
</note>
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
||||
<title>季度报告</title>
|
||||
<theme>
|
||||
<textStyles>
|
||||
<title fontFamily="思源黑体" fontSize="54" fontColor="rgba(0, 0, 0, 1)"/>
|
||||
<body fontFamily="思源黑体" fontSize="18" fontColor="rgba(43, 47, 54, 1)"/>
|
||||
</textStyles>
|
||||
</theme>
|
||||
<slide>
|
||||
<style>
|
||||
<fill>
|
||||
<fillColor color="rgb(245, 245, 245)"/>
|
||||
</fill>
|
||||
</style>
|
||||
<data>
|
||||
<shape type="text" topLeftX="80" topLeftY="72" width="760" height="100">
|
||||
<content textType="title">
|
||||
<p>2024 年第一季度报告</p>
|
||||
</content>
|
||||
</shape>
|
||||
<shape type="text" topLeftX="80" topLeftY="200" width="520" height="180">
|
||||
<content textType="body">
|
||||
<p>核心指标</p>
|
||||
<ul>
|
||||
<li><p>用户增长:+25%</p></li>
|
||||
<li><p>收入增长:+30%</p></li>
|
||||
<li><p>市场份额:15%</p></li>
|
||||
</ul>
|
||||
</content>
|
||||
</shape>
|
||||
<shape type="rect" topLeftX="660" topLeftY="180" width="180" height="140">
|
||||
<fill>
|
||||
<fillColor color="rgba(100, 149, 237, 0.25)"/>
|
||||
</fill>
|
||||
<border color="rgb(100, 149, 237)" width="2"/>
|
||||
</shape>
|
||||
</data>
|
||||
<note>
|
||||
<content textType="body">
|
||||
<p>讲到增长率时补充样本范围。</p>
|
||||
</content>
|
||||
</note>
|
||||
</slide>
|
||||
</presentation>
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. 始终带上命名空间 `xmlns="http://www.larkoffice.com/sml/2.0"`
|
||||
2. 用 `shape type="text"` + `content` 表达页面文本
|
||||
3. 用 `topLeftX` / `topLeftY`、`startX` / `startY` 等 schema 中定义的属性名
|
||||
4. 优先使用 `rgb` / `rgba` 颜色格式;渐变必须使用 `rgba()` 且带百分比停靠点
|
||||
5. 特殊字符按 XML 规则转义
|
||||
6. 标准 16:9 页面建议使用 `width="960"` 和 `height="540"`
|
||||
|
||||
## 详细参考
|
||||
|
||||
- [slides_xml_schema_definition.xml](slides_xml_schema_definition.xml)
|
||||
- [slides_chart_demo.xml](slides_chart_demo.xml)
|
||||
|
||||
## Schema 版本信息
|
||||
|
||||
- **版本**: 2.0.0
|
||||
- **命名空间**: http://www.larkoffice.com/sml/2.0
|
||||
- **发布日期**: 2025-11-03
|
||||
362
.agents/skills/lark-slides/scripts/iconpark_tool.py
Normal file
362
.agents/skills/lark-slides/scripts/iconpark_tool.py
Normal file
@ -0,0 +1,362 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
SKILL_ROOT = Path(__file__).resolve().parent.parent
|
||||
REFERENCES_DIR = SKILL_ROOT / "references"
|
||||
DEFAULT_INDEX_PATH = REFERENCES_DIR / "iconpark-index.json"
|
||||
DEFAULT_LIMIT = 8
|
||||
CURATED_ICON_BOOSTS = {
|
||||
"设置": {"iconpark/Base/setting.svg"},
|
||||
"配置": {"iconpark/Base/setting.svg", "iconpark/Base/config.svg"},
|
||||
"目标": {"iconpark/Base/aiming.svg", "iconpark/Sports/target-one.svg"},
|
||||
"增长": {"iconpark/Charts/positive-dynamics.svg"},
|
||||
"趋势": {"iconpark/Charts/chart-line.svg", "iconpark/Charts/positive-dynamics.svg"},
|
||||
"占比": {"iconpark/Charts/chart-proportion.svg"},
|
||||
"数据": {"iconpark/Charts/data-screen.svg"},
|
||||
"看板": {"iconpark/Charts/data-screen.svg"},
|
||||
"成功": {"iconpark/Character/check-one.svg"},
|
||||
"完成": {"iconpark/Character/check-one.svg"},
|
||||
"失败": {"iconpark/Character/close-one.svg"},
|
||||
"风险": {"iconpark/Character/close-one.svg"},
|
||||
"团队": {"iconpark/Peoples/peoples.svg"},
|
||||
"用户": {"iconpark/Peoples/peoples.svg", "iconpark/Peoples/user.svg"},
|
||||
"安全": {"iconpark/Safe/protect.svg"},
|
||||
"防护": {"iconpark/Safe/protect.svg"},
|
||||
"全球": {"iconpark/Travel/world.svg"},
|
||||
"市场": {"iconpark/Travel/world.svg"},
|
||||
"邮件": {"iconpark/Office/envelope-one.svg"},
|
||||
"联系": {"iconpark/Office/envelope-one.svg"},
|
||||
"会议": {"iconpark/Office/schedule.svg"},
|
||||
"日程": {"iconpark/Office/schedule.svg"},
|
||||
"飞书": {"iconpark/Brand/bydesign.svg"},
|
||||
}
|
||||
CURATED_BOOST_SCORE = 40
|
||||
|
||||
|
||||
class IconParkToolError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
raise IconParkToolError(message)
|
||||
|
||||
|
||||
def normalize_whitespace(value: str) -> str:
|
||||
return re.sub(r"\s+", " ", value).strip()
|
||||
|
||||
|
||||
def normalize_token(value: str) -> str:
|
||||
return normalize_whitespace(value.lower().replace("_", "-"))
|
||||
|
||||
|
||||
def append_unique(target: list[str], token: str) -> None:
|
||||
normalized = normalize_token(token)
|
||||
if normalized and normalized not in target:
|
||||
target.append(normalized)
|
||||
|
||||
|
||||
def tokenize_query(value: str) -> list[str]:
|
||||
normalized = normalize_token(value)
|
||||
if not normalized:
|
||||
return []
|
||||
|
||||
tokens: list[str] = []
|
||||
for item in re.split(r"[\s,/|,。;;::()()【】\[\]《》<>]+", normalized):
|
||||
append_unique(tokens, item)
|
||||
|
||||
for phrase in re.findall(r"[\u3400-\u9fff]+", normalized):
|
||||
if len(phrase) < 2:
|
||||
continue
|
||||
max_size = min(6, len(phrase))
|
||||
for size in range(max_size, 1, -1):
|
||||
for start in range(0, len(phrase) - size + 1):
|
||||
append_unique(tokens, phrase[start : start + size])
|
||||
|
||||
synonym_tokens = {
|
||||
"目标": ["aim", "target", "goal"],
|
||||
"聚焦": ["focus", "target"],
|
||||
"增长": ["growth", "trend", "positive"],
|
||||
"趋势": ["trend", "chart", "line"],
|
||||
"数据": ["data", "analytics", "chart"],
|
||||
"指标": ["metric", "data"],
|
||||
"看板": ["dashboard", "screen", "data"],
|
||||
"成功": ["success", "check", "done"],
|
||||
"完成": ["done", "success", "check"],
|
||||
"失败": ["fail", "close", "risk"],
|
||||
"风险": ["risk", "fail", "protect"],
|
||||
"安全": ["safe", "security", "protect"],
|
||||
"配置": ["config", "setting", "system"],
|
||||
"设置": ["setting", "config"],
|
||||
"团队": ["team", "people", "users"],
|
||||
"用户": ["user", "people"],
|
||||
"全球": ["global", "world", "earth"],
|
||||
"市场": ["market", "world", "business"],
|
||||
"邮件": ["mail", "message"],
|
||||
"mail": ["message", "envelope", "envelope-one"],
|
||||
"计划": ["plan", "schedule"],
|
||||
"时间": ["time", "schedule"],
|
||||
"学习": ["learning", "education", "book"],
|
||||
"培训": ["training", "education"],
|
||||
"自动化": ["automation", "ai"],
|
||||
"ai": ["ai", "automation", "magic"],
|
||||
}
|
||||
for token in list(tokens):
|
||||
for keyword, aliases in synonym_tokens.items():
|
||||
if is_ascii_token(keyword):
|
||||
matches = token == keyword
|
||||
else:
|
||||
matches = keyword in token
|
||||
if matches:
|
||||
for alias in aliases:
|
||||
append_unique(tokens, alias)
|
||||
|
||||
return tokens
|
||||
|
||||
|
||||
def is_ascii_token(value: str) -> bool:
|
||||
return bool(re.fullmatch(r"[a-z0-9-]+", value))
|
||||
|
||||
|
||||
def allows_substring_match(value: str) -> bool:
|
||||
return not is_ascii_token(value) or len(value) >= 3
|
||||
|
||||
|
||||
def field_tokens(*values: str) -> set[str]:
|
||||
tokens: set[str] = set()
|
||||
for value in values:
|
||||
normalized = normalize_token(value)
|
||||
if not normalized:
|
||||
continue
|
||||
tokens.add(normalized)
|
||||
for part in re.split(r"[-\s]+", normalized):
|
||||
if part:
|
||||
tokens.add(part)
|
||||
return tokens
|
||||
|
||||
|
||||
def load_index(path: str | Path = DEFAULT_INDEX_PATH) -> dict[str, Any]:
|
||||
index_path = Path(path)
|
||||
if not index_path.exists():
|
||||
fail(f"iconpark index not found: {index_path}")
|
||||
try:
|
||||
index_data = json.loads(index_path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError as error:
|
||||
fail(f"invalid iconpark index JSON: {error}")
|
||||
if not isinstance(index_data.get("icons"), list):
|
||||
fail("iconpark index must contain an icons array")
|
||||
return index_data
|
||||
|
||||
|
||||
def icon_search_text(entry: dict[str, Any]) -> str:
|
||||
parts = [
|
||||
entry.get("iconType", ""),
|
||||
entry.get("category", ""),
|
||||
entry.get("name", ""),
|
||||
" ".join(entry.get("tags") or []),
|
||||
]
|
||||
return normalize_token(" ".join(parts))
|
||||
|
||||
|
||||
def score_icon(entry: dict[str, Any], query: str, tokens: list[str]) -> int:
|
||||
raw_icon_type = entry.get("iconType", "")
|
||||
icon_type = normalize_token(raw_icon_type)
|
||||
category = normalize_token(entry.get("category", ""))
|
||||
name = normalize_token(entry.get("name", ""))
|
||||
tags = [normalize_token(tag) for tag in entry.get("tags") or []]
|
||||
name_tokens = field_tokens(name)
|
||||
category_tokens = field_tokens(category)
|
||||
tag_tokens = field_tokens(*tags)
|
||||
icon_type_tokens = field_tokens(icon_type)
|
||||
search_text = icon_search_text(entry)
|
||||
normalized_query = normalize_token(query)
|
||||
|
||||
score = 0
|
||||
boosted_keywords: set[str] = set()
|
||||
if normalized_query:
|
||||
if normalized_query == icon_type or normalized_query == name:
|
||||
score += 200
|
||||
elif normalized_query in tag_tokens:
|
||||
score += 120
|
||||
elif normalized_query in icon_type_tokens:
|
||||
score += 60
|
||||
elif allows_substring_match(normalized_query) and normalized_query in search_text:
|
||||
score += 30
|
||||
|
||||
for token in tokens:
|
||||
for keyword, boosted_icon_types in CURATED_ICON_BOOSTS.items():
|
||||
if keyword in boosted_keywords:
|
||||
continue
|
||||
if keyword in token and raw_icon_type in boosted_icon_types:
|
||||
score += CURATED_BOOST_SCORE
|
||||
boosted_keywords.add(keyword)
|
||||
if token == name:
|
||||
score += 80
|
||||
elif token in name_tokens:
|
||||
score += 55
|
||||
elif allows_substring_match(token) and token in name:
|
||||
score += 45
|
||||
if token == category:
|
||||
score += 35
|
||||
elif token in category_tokens:
|
||||
score += 25
|
||||
elif allows_substring_match(token) and token in category:
|
||||
score += 15
|
||||
for tag in tags:
|
||||
if token == tag:
|
||||
score += 60
|
||||
elif token in field_tokens(tag):
|
||||
score += 45
|
||||
elif allows_substring_match(token) and token in tag:
|
||||
score += 20
|
||||
if token in icon_type_tokens:
|
||||
score += 20
|
||||
elif allows_substring_match(token) and token in icon_type:
|
||||
score += 15
|
||||
|
||||
return score
|
||||
|
||||
|
||||
def parse_limit(value: Any) -> int:
|
||||
if value is None or value is False:
|
||||
return DEFAULT_LIMIT
|
||||
if value is True:
|
||||
fail("limit requires an integer value")
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
fail(f"limit must be an integer: {value}")
|
||||
|
||||
|
||||
def public_icon(entry: dict[str, Any], score: int | None = None) -> dict[str, Any]:
|
||||
result = {
|
||||
"iconType": entry["iconType"],
|
||||
"category": entry["category"],
|
||||
"name": entry["name"],
|
||||
"tags": entry.get("tags") or [],
|
||||
}
|
||||
if score is not None:
|
||||
result["score"] = score
|
||||
return result
|
||||
|
||||
|
||||
def search_icons(index_data: dict[str, Any], options: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
query = str(options.get("query") or "")
|
||||
if not normalize_whitespace(query):
|
||||
fail("query is required")
|
||||
limit = parse_limit(options.get("limit"))
|
||||
category_filter = normalize_token(str(options.get("category") or ""))
|
||||
tokens = tokenize_query(query)
|
||||
|
||||
ranked: list[dict[str, Any]] = []
|
||||
for entry in index_data["icons"]:
|
||||
if category_filter and normalize_token(entry.get("category", "")) != category_filter:
|
||||
continue
|
||||
score = score_icon(entry, query, tokens)
|
||||
if query and score == 0:
|
||||
continue
|
||||
ranked.append(public_icon(entry, score))
|
||||
|
||||
ranked.sort(key=lambda item: (-int(item["score"]), item["category"], item["name"]))
|
||||
return ranked[: max(limit, 0)]
|
||||
|
||||
|
||||
def resolve_icon(index_data: dict[str, Any], name_or_type: str | None) -> dict[str, Any]:
|
||||
if not name_or_type:
|
||||
fail("name is required")
|
||||
target = normalize_token(name_or_type)
|
||||
matches = []
|
||||
for entry in index_data["icons"]:
|
||||
candidates = {
|
||||
normalize_token(entry["iconType"]),
|
||||
normalize_token(entry["name"]),
|
||||
normalize_token(f'{entry["category"]}/{entry["name"]}.svg'),
|
||||
}
|
||||
if target in candidates:
|
||||
matches.append(entry)
|
||||
if not matches:
|
||||
fail(f"icon not found: {name_or_type}")
|
||||
if len(matches) > 1:
|
||||
names = ", ".join(entry["iconType"] for entry in matches)
|
||||
fail(f"ambiguous icon name: {name_or_type}; matches: {names}")
|
||||
return public_icon(matches[0])
|
||||
|
||||
|
||||
def list_categories(index_data: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
counts: dict[str, int] = {}
|
||||
for entry in index_data["icons"]:
|
||||
counts[entry["category"]] = counts.get(entry["category"], 0) + 1
|
||||
return [{"category": category, "count": counts[category]} for category in sorted(counts)]
|
||||
|
||||
|
||||
def parse_cli_args(argv: list[str]) -> tuple[str | None, dict[str, Any]]:
|
||||
if not argv:
|
||||
return None, {}
|
||||
command, *rest = argv
|
||||
options: dict[str, Any] = {}
|
||||
index = 0
|
||||
while index < len(rest):
|
||||
token = rest[index]
|
||||
if not token.startswith("--"):
|
||||
fail(f"unexpected argument: {token}")
|
||||
key = token[2:]
|
||||
next_token = rest[index + 1] if index + 1 < len(rest) else None
|
||||
if next_token is None or next_token.startswith("--"):
|
||||
options[key] = True
|
||||
index += 1
|
||||
continue
|
||||
options[key] = next_token
|
||||
index += 2
|
||||
return command, options
|
||||
|
||||
|
||||
def print_usage() -> None:
|
||||
usage = [
|
||||
"Usage:",
|
||||
" python3 iconpark_tool.py search --query <text> [--category <Category>] [--limit 8]",
|
||||
" python3 iconpark_tool.py resolve --name <name|iconType>",
|
||||
" python3 iconpark_tool.py list-categories",
|
||||
]
|
||||
print("\n".join(usage), file=sys.stderr)
|
||||
|
||||
|
||||
def write_json(value: Any) -> None:
|
||||
print(json.dumps(value, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
def run_cli(argv: list[str] | None = None) -> None:
|
||||
command, options = parse_cli_args(argv or sys.argv[1:])
|
||||
if not command or command in {"--help", "help"}:
|
||||
print_usage()
|
||||
raise SystemExit(0)
|
||||
|
||||
index_data = load_index()
|
||||
if command == "search":
|
||||
write_json(search_icons(index_data, options))
|
||||
return
|
||||
if command == "resolve":
|
||||
write_json(resolve_icon(index_data, options.get("name")))
|
||||
return
|
||||
if command == "list-categories":
|
||||
write_json(list_categories(index_data))
|
||||
return
|
||||
|
||||
print_usage()
|
||||
fail(f"unknown command: {command}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
run_cli()
|
||||
except IconParkToolError as error:
|
||||
print(f"iconpark-tool error: {error}", file=sys.stderr)
|
||||
raise SystemExit(1) from error
|
||||
177
.agents/skills/lark-slides/scripts/iconpark_tool_test.py
Normal file
177
.agents/skills/lark-slides/scripts/iconpark_tool_test.py
Normal file
@ -0,0 +1,177 @@
|
||||
# Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
# SPDX-License-Identifier: MIT
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import iconpark_tool
|
||||
|
||||
|
||||
SCRIPT_PATH = Path(__file__).resolve().with_name("iconpark_tool.py")
|
||||
|
||||
|
||||
class IconParkToolTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.index_data = iconpark_tool.load_index()
|
||||
|
||||
def test_search_icons_finds_growth_trend(self) -> None:
|
||||
results = iconpark_tool.search_icons(self.index_data, {"query": "增长趋势", "limit": 5})
|
||||
self.assertTrue(results)
|
||||
self.assertTrue(
|
||||
any(entry["iconType"] == "iconpark/Charts/positive-dynamics.svg" for entry in results)
|
||||
)
|
||||
|
||||
def test_search_icons_supports_english_query(self) -> None:
|
||||
results = iconpark_tool.search_icons(self.index_data, {"query": "security protect", "limit": 3})
|
||||
self.assertTrue(results)
|
||||
self.assertEqual(results[0]["iconType"], "iconpark/Safe/protect.svg")
|
||||
|
||||
def test_search_icons_supports_category_filter(self) -> None:
|
||||
results = iconpark_tool.search_icons(
|
||||
self.index_data,
|
||||
{"query": "data", "category": "Charts", "limit": 10},
|
||||
)
|
||||
self.assertTrue(results)
|
||||
self.assertTrue(all(entry["category"] == "Charts" for entry in results))
|
||||
|
||||
def test_search_icons_does_not_expand_ai_inside_words(self) -> None:
|
||||
mail_results = iconpark_tool.search_icons(self.index_data, {"query": "mail", "limit": 5})
|
||||
self.assertEqual(mail_results[0]["iconType"], "iconpark/Office/envelope-one.svg")
|
||||
self.assertNotEqual(mail_results[0]["iconType"], "iconpark/Others/magic.svg")
|
||||
|
||||
fail_results = iconpark_tool.search_icons(self.index_data, {"query": "fail", "limit": 5})
|
||||
self.assertNotEqual(fail_results[0]["iconType"], "iconpark/Others/magic.svg")
|
||||
|
||||
def test_search_icons_supports_template_icon_queries(self) -> None:
|
||||
cases = [
|
||||
("arrow", "iconpark/Arrows/arrow-right.svg"),
|
||||
("right", "iconpark/Arrows/right.svg"),
|
||||
("PPT", "iconpark/Music/ppt.svg"),
|
||||
("table", "iconpark/Office/table.svg"),
|
||||
("会议", "iconpark/Office/schedule.svg"),
|
||||
("飞书", "iconpark/Brand/bydesign.svg"),
|
||||
]
|
||||
for query, icon_type in cases:
|
||||
with self.subTest(query=query):
|
||||
results = iconpark_tool.search_icons(self.index_data, {"query": query, "limit": 10})
|
||||
self.assertTrue(
|
||||
any(entry["iconType"] == icon_type for entry in results),
|
||||
f"{icon_type} not found in {results}",
|
||||
)
|
||||
|
||||
def test_search_icons_defaults_to_wider_candidate_set(self) -> None:
|
||||
results = iconpark_tool.search_icons(self.index_data, {"query": "data"})
|
||||
self.assertEqual(len(results), 8)
|
||||
|
||||
def test_search_icons_boosts_common_slide_terms(self) -> None:
|
||||
results = iconpark_tool.search_icons(self.index_data, {"query": "会议", "limit": 3})
|
||||
self.assertTrue(
|
||||
any(entry["iconType"] == "iconpark/Office/schedule.svg" for entry in results),
|
||||
f"iconpark/Office/schedule.svg not found in {results}",
|
||||
)
|
||||
|
||||
def test_search_icons_keeps_high_value_top_results(self) -> None:
|
||||
cases = [
|
||||
("安全", "iconpark/Safe/protect.svg"),
|
||||
("邮件", "iconpark/Office/mail-open.svg"),
|
||||
("会议", "iconpark/Office/schedule.svg"),
|
||||
("增长趋势", "iconpark/Charts/chart-line.svg"),
|
||||
("飞书", "iconpark/Brand/bydesign.svg"),
|
||||
]
|
||||
for query, icon_type in cases:
|
||||
with self.subTest(query=query):
|
||||
results = iconpark_tool.search_icons(self.index_data, {"query": query, "limit": 3})
|
||||
self.assertTrue(results)
|
||||
self.assertEqual(results[0]["iconType"], icon_type)
|
||||
|
||||
def test_search_icons_requires_query(self) -> None:
|
||||
with self.assertRaises(iconpark_tool.IconParkToolError):
|
||||
iconpark_tool.search_icons(self.index_data, {"limit": 5})
|
||||
|
||||
def test_search_icons_rejects_invalid_limit(self) -> None:
|
||||
with self.assertRaises(iconpark_tool.IconParkToolError):
|
||||
iconpark_tool.search_icons(self.index_data, {"query": "data", "limit": "abc"})
|
||||
|
||||
def test_resolve_icon_accepts_name_and_icon_type(self) -> None:
|
||||
by_name = iconpark_tool.resolve_icon(self.index_data, "chart-line")
|
||||
by_type = iconpark_tool.resolve_icon(self.index_data, "iconpark/Charts/chart-line.svg")
|
||||
self.assertEqual(by_name["iconType"], "iconpark/Charts/chart-line.svg")
|
||||
self.assertEqual(by_name, by_type)
|
||||
|
||||
def test_resolve_icon_accepts_template_icon_type(self) -> None:
|
||||
result = iconpark_tool.resolve_icon(self.index_data, "iconpark/Arrows/arrow-right.svg")
|
||||
self.assertEqual(result["iconType"], "iconpark/Arrows/arrow-right.svg")
|
||||
|
||||
def test_resolve_icon_rejects_unknown_name(self) -> None:
|
||||
with self.assertRaises(iconpark_tool.IconParkToolError):
|
||||
iconpark_tool.resolve_icon(self.index_data, "not-a-real-icon")
|
||||
|
||||
def test_list_categories_counts_index(self) -> None:
|
||||
categories = iconpark_tool.list_categories(self.index_data)
|
||||
self.assertTrue(any(entry["category"] == "Charts" and entry["count"] > 0 for entry in categories))
|
||||
|
||||
|
||||
class IconParkToolCLITest(unittest.TestCase):
|
||||
def run_tool(self, *args: str) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
[sys.executable, str(SCRIPT_PATH), *args],
|
||||
capture_output=True,
|
||||
check=False,
|
||||
text=True,
|
||||
)
|
||||
|
||||
def test_cli_search_writes_json_to_stdout(self) -> None:
|
||||
result = self.run_tool("search", "--query", "增长趋势", "--limit", "5")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stderr, "")
|
||||
|
||||
output = json.loads(result.stdout)
|
||||
self.assertTrue(output)
|
||||
self.assertTrue(
|
||||
any(entry["iconType"] == "iconpark/Charts/positive-dynamics.svg" for entry in output)
|
||||
)
|
||||
|
||||
def test_cli_resolve_writes_json_to_stdout(self) -> None:
|
||||
result = self.run_tool("resolve", "--name", "chart-line")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stderr, "")
|
||||
|
||||
output = json.loads(result.stdout)
|
||||
self.assertEqual(output["iconType"], "iconpark/Charts/chart-line.svg")
|
||||
|
||||
def test_cli_list_categories_writes_json_to_stdout(self) -> None:
|
||||
result = self.run_tool("list-categories")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stderr, "")
|
||||
|
||||
output = json.loads(result.stdout)
|
||||
self.assertTrue(any(entry["category"] == "Charts" and entry["count"] > 0 for entry in output))
|
||||
|
||||
def test_cli_help_writes_usage_to_stderr(self) -> None:
|
||||
result = self.run_tool("--help")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stdout, "")
|
||||
self.assertIn("Usage:", result.stderr)
|
||||
self.assertIn("python3 iconpark_tool.py search", result.stderr)
|
||||
|
||||
def test_cli_invalid_argument_writes_error_to_stderr(self) -> None:
|
||||
result = self.run_tool("search", "增长趋势")
|
||||
self.assertEqual(result.returncode, 1)
|
||||
self.assertEqual(result.stdout, "")
|
||||
self.assertIn("iconpark-tool error: unexpected argument: 增长趋势", result.stderr)
|
||||
|
||||
def test_cli_unknown_command_writes_usage_and_error_to_stderr(self) -> None:
|
||||
result = self.run_tool("unknown")
|
||||
self.assertEqual(result.returncode, 1)
|
||||
self.assertEqual(result.stdout, "")
|
||||
self.assertIn("Usage:", result.stderr)
|
||||
self.assertIn("iconpark-tool error: unknown command: unknown", result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
2482
.agents/skills/lark-slides/scripts/xml_text_overlap_lint.py
Normal file
2482
.agents/skills/lark-slides/scripts/xml_text_overlap_lint.py
Normal file
File diff suppressed because it is too large
Load Diff
2578
.agents/skills/lark-slides/scripts/xml_text_overlap_lint_test.py
Normal file
2578
.agents/skills/lark-slides/scripts/xml_text_overlap_lint_test.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user