mirror of
https://github.com/larksuite/cli.git
synced 2026-08-03 08:32:46 +08:00
Compare commits
4 Commits
v1.0.79
...
feat/slide
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2b77acfdb | ||
|
|
91edc443e8 | ||
|
|
358cd06838 | ||
|
|
b0b1ca4b5d |
@@ -141,7 +141,7 @@ lark-cli auth login --domain slides
|
||||
- [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)
|
||||
@@ -223,7 +223,7 @@ Step 3: 按 slide_plan.json 生成 XML → 创建
|
||||
- 创建方式按“创建方式选择”判断;图片、复杂 XML、转义和 3350001 排查按 lark-slides-create.md、media-upload.md、troubleshooting.md 执行
|
||||
|
||||
Step 4: 审查 & 交付
|
||||
- 创建完成后,必须用 `slides +xml-get` 读取全文 XML,并按 validation-checklist.md 做显式验证记录,包括 XML 文本重叠检查
|
||||
- 创建完成后,必须用 `slides +xml-get --presentation <xml_presentation_id>` 读取全文 XML,并按 validation-checklist.md 做显式验证记录,包括 XML 文本重叠检查
|
||||
- 失败或部分成功按 troubleshooting.md 处理;局部问题优先用 `+replace-slide` 修正
|
||||
- 没问题 → 交付:使用 NotifyHuman 工具交付 PPT 链接
|
||||
```
|
||||
@@ -314,7 +314,8 @@ Shortcut 是对常用操作的高级封装(`lark-cli slides +<verb> [flags]`
|
||||
| Shortcut | 说明 |
|
||||
|----------|------|
|
||||
| [`+create`](references/lark-slides-create.md) | 创建 PPT(可选 `--slides` 一步添加页面,支持 `<img src="@./local.png">` 占位符自动上传) |
|
||||
| [`+xml-get`](references/lark-slides-xml-presentations-get.md) | 读取全文 XML 并保存到本地文件,避免终端输出被截断 |
|
||||
| [`+xml-get`](references/lark-slides-xml-presentations-get.md) | 读取全文 XML,用 `--presentation` 指定演示文稿的 `xml_presentation_id`,用 `--output` 把 XML 存到本地文件(必须是 CWD 内的相对路径,如 `.lark-slides/plan/<deck>/readback.xml`) |
|
||||
| [`+screenshot`](references/lark-slides-screenshot.md) | 把幻灯片页面截图保存为本地图片,用 `--slide-number` 指定页号(从 1 开始,多页重复传入,一次最多 10 页),用 `--output-dir` 指定保存目录(必须是 CWD 内的相对路径,默认 `.lark-slides/screenshots`),失败时降级到 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 的多页大改,不新建链接 |
|
||||
|
||||
@@ -1076,6 +1076,7 @@
|
||||
<xs:element ref="sml:img"/>
|
||||
<xs:element ref="sml:table"/>
|
||||
<xs:element ref="sml:icon"/>
|
||||
<xs:element ref="sml:embed"/>
|
||||
<xs:element ref="sml:chart"/>
|
||||
<xs:element ref="sml:undefined"/>
|
||||
</xs:choice>
|
||||
@@ -1622,6 +1623,49 @@
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
<!-- 嵌入内容元素 -->
|
||||
<xs:element name="embed">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
嵌入内容容器, 外层承载 Slides 摆放和效果属性, 内层承载外部标准内容。
|
||||
当前内层内容为标准 SVG, 必须使用 http://www.w3.org/2000/svg 命名空间。
|
||||
后续可扩展承载 HTML 等其他外部内容。
|
||||
|
||||
必需属性:
|
||||
- topLeftX/topLeftY: 左上角坐标
|
||||
- width/height: 显示尺寸
|
||||
|
||||
可选属性:
|
||||
- id: 唯一标识符
|
||||
- rotation: 旋转角度[0, 360)
|
||||
- flipX/flipY: 水平/垂直翻转
|
||||
- alpha: 不透明度[0,1]
|
||||
|
||||
子元素:
|
||||
- svg: 标准 SVG 根元素, 仅描述嵌入内容, 不承载 Slides 布局属性
|
||||
- reflection: 倒影样式, 无reflection标签代表无倒影, 空reflection标签代表使用默认样式
|
||||
- shadow: 阴影样式, 无shadow标签代表无阴影, 空shadow标签代表使用默认样式
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:any namespace="http://www.w3.org/2000/svg" processContents="lax" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="reflection" type="sml:ReflectionType" minOccurs="0"/>
|
||||
<xs:element name="shadow" type="sml:ShadowType" minOccurs="0"/>
|
||||
</xs:sequence>
|
||||
|
||||
<xs:attribute name="id" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="topLeftX" type="sml:XType" use="required"/>
|
||||
<xs:attribute name="topLeftY" type="sml:YType" use="required"/>
|
||||
<xs:attribute name="width" type="sml:PositiveSize" use="required"/>
|
||||
<xs:attribute name="height" type="sml:PositiveSize" use="required"/>
|
||||
<xs:attribute name="rotation" type="sml:RotationType" use="optional" default="0"/>
|
||||
<xs:attribute name="flipX" type="xs:boolean" use="optional" default="false"/>
|
||||
<xs:attribute name="flipY" type="xs:boolean" use="optional" default="false"/>
|
||||
<xs:attribute name="alpha" type="sml:AlphaType" use="optional" default="1"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
<!-- 表格元素 -->
|
||||
<xs:element name="table">
|
||||
<xs:annotation>
|
||||
|
||||
@@ -81,7 +81,7 @@ XSD 中的 `title`、`headline`、`sub-headline`、`body`、`caption` 主要出
|
||||
**子元素:**
|
||||
|
||||
- `<style>?` - 页面样式,目前可放 `<fill>`
|
||||
- `<data>?` - 页面元素容器,可放 `shape`、`line`、`polyline`、`img`、`table`、`icon`、`chart`、`undefined`
|
||||
- `<data>?` - 页面元素容器,可放 `shape`、`line`、`polyline`、`img`、`table`、`icon`、`embed`、`chart`、`undefined`
|
||||
- `<note>?` - 演讲者备注,内部可放 `<content>`
|
||||
|
||||
这意味着 `<title>`、`<headline>`、`<body>`、`<caption>` 不能直接放在 `<slide>` 下。
|
||||
@@ -355,6 +355,20 @@ XSD 中的 `title`、`headline`、`sub-headline`、`body`、`caption` 主要出
|
||||
|
||||
详细用法见 [slides_xml_schema_definition.xml](slides_xml_schema_definition.xml)。
|
||||
|
||||
### embed
|
||||
|
||||
嵌入内容容器:外层 `<embed>` 承载 Slides 的摆放和效果属性(`topLeftX`/`topLeftY`/`width`/`height` 必填,`rotation`/`flipX`/`flipY`/`alpha` 可选),内层承载外部标准内容。当前内层内容为标准 SVG,`<svg>` 必须使用 `http://www.w3.org/2000/svg` 命名空间,且仅描述嵌入内容本身,不承载 Slides 布局属性。
|
||||
|
||||
```xml
|
||||
<embed topLeftX="80" topLeftY="120" width="200" height="120">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 120">
|
||||
<circle cx="100" cy="60" r="40" fill="rgba(37, 99, 235, 1)"/>
|
||||
</svg>
|
||||
</embed>
|
||||
```
|
||||
|
||||
`<embed>` 直接子元素为一个 `<svg>`(必需),以及可选的 `<reflection>`(倒影)与 `<shadow>`(阴影)。
|
||||
|
||||
## 颜色与样式
|
||||
|
||||
### fill
|
||||
|
||||
@@ -403,7 +403,7 @@ def build_sxsd_tag_hint(tag_name: str, supported_tags: set[str]) -> str:
|
||||
if alias:
|
||||
return f"Use {alias} instead of <{tag_name}>."
|
||||
if tag_name == "svg":
|
||||
return 'Inside <whiteboard>, write SVG as <svg xmlns="http://www.w3.org/2000/svg">...</svg>.'
|
||||
return 'Inside <embed> or <whiteboard>, write SVG as <svg xmlns="http://www.w3.org/2000/svg">...</svg>.'
|
||||
close_matches = get_close_matches(tag_name, sorted(supported_tags), n=3, cutoff=0.72)
|
||||
if close_matches:
|
||||
return "Unsupported SXSD tag. Did you mean " + ", ".join(f"<{match}>" for match in close_matches) + "?"
|
||||
@@ -424,7 +424,7 @@ def build_sxsd_attr_hint(tag_name: str, attr_name: str, allowed_attrs: set[str])
|
||||
|
||||
|
||||
def should_skip_sxsd_subtree(element: ET.Element, ancestors: list[str]) -> bool:
|
||||
return "whiteboard" in ancestors and xml_namespace(element.tag) == SVG_NS
|
||||
return ("whiteboard" in ancestors or "embed" in ancestors) and xml_namespace(element.tag) == SVG_NS
|
||||
|
||||
|
||||
def should_skip_sxsd_attribute(tag_name: str, attr_name: str) -> bool:
|
||||
@@ -702,7 +702,7 @@ def parse_presentation(xml: str) -> dict[str, Any]:
|
||||
def extract_elements(slide_xml: str) -> list[dict[str, Any]]:
|
||||
elements: list[dict[str, Any]] = []
|
||||
|
||||
for match in re.finditer(r"<(shape|img|table|chart|whiteboard)\b([^>]*)>", slide_xml):
|
||||
for match in re.finditer(r"<(shape|img|table|chart|whiteboard|embed)\b([^>]*)>", slide_xml):
|
||||
kind, attrs = match.group(1), match.group(2)
|
||||
is_self_closing = attrs.rstrip().endswith("/")
|
||||
content = ""
|
||||
@@ -1917,7 +1917,7 @@ def slide_content_visual_bbox(
|
||||
# a straight horizontal/vertical line has zero width or height in one axis; clipped_bbox
|
||||
# treats zero-area rects as invisible, so pad to its rendered stroke thickness instead.
|
||||
return clipped_bbox(line_stroke_bbox(element), slide_bbox)
|
||||
if element["kind"] in {"img", "chart", "table", "whiteboard", "icon", "polyline"}:
|
||||
if element["kind"] in {"img", "chart", "table", "whiteboard", "embed", "icon", "polyline"}:
|
||||
return clipped_bbox(element, slide_bbox)
|
||||
return None
|
||||
|
||||
@@ -1955,7 +1955,7 @@ def is_slide_content_present(
|
||||
|
||||
|
||||
def is_large_visual_child(element: dict[str, Any], container: dict[str, Any]) -> bool:
|
||||
if element["kind"] not in {"img", "chart", "table", "whiteboard"}:
|
||||
if element["kind"] not in {"img", "chart", "table", "whiteboard", "embed"}:
|
||||
return False
|
||||
if not is_visually_rendered(element):
|
||||
return False
|
||||
|
||||
@@ -265,6 +265,27 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
|
||||
self.assertEqual(result["summary"]["error_count"], 0)
|
||||
self.assertEqual(result["summary"]["warning_count"], 0)
|
||||
|
||||
def test_lint_xml_allows_svg_subtree_inside_embed(self) -> None:
|
||||
result = xml_text_overlap_lint.lint_xml(
|
||||
"""
|
||||
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
||||
<data>
|
||||
<embed topLeftX="80" topLeftY="120" width="240" height="140">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 140">
|
||||
<rect x="10" y="10" width="220" height="120" rx="12" fill="#EFF6FF"/>
|
||||
<circle cx="70" cy="70" r="34" fill="#2563EB"/>
|
||||
<text x="130" y="76" font-size="18" fill="#1E3A8A">SVG OK</text>
|
||||
</svg>
|
||||
</embed>
|
||||
</data>
|
||||
</slide>
|
||||
"""
|
||||
)
|
||||
codes = [issue["code"] for issue in result.get("issues", [])]
|
||||
self.assertNotIn("sxsd_unsupported_tag", codes)
|
||||
self.assertNotIn("sxsd_unsupported_attr", codes)
|
||||
self.assertEqual(result["summary"]["error_count"], 0)
|
||||
|
||||
def test_lint_xml_reports_sxsd_unsupported_tag_with_alias_hint(self) -> None:
|
||||
cases = [
|
||||
("textbox", '<textbox topLeftX="80" topLeftY="80" width="300" height="60">Text</textbox>', '<shape type="text">'),
|
||||
@@ -1356,6 +1377,9 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
|
||||
<table id="table" topLeftX="400" topLeftY="60" width="220" height="120"></table>
|
||||
<chart id="chart" topLeftX="640" topLeftY="60" width="220" height="120"/>
|
||||
<whiteboard id="wb" topLeftX="80" topLeftY="220" width="760" height="240"/>
|
||||
<embed id="emb" topLeftX="600" topLeftY="320" width="240" height="140">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 140"><rect x="0" y="0" width="240" height="140"/></svg>
|
||||
</embed>
|
||||
<shape id="missing-height" type="text" topLeftX="80" topLeftY="480" width="320">
|
||||
<content><p>Skipped</p></content>
|
||||
</shape>
|
||||
@@ -1363,9 +1387,9 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
|
||||
</slide>
|
||||
"""
|
||||
)
|
||||
self.assertEqual([element["id"] for element in elements], ["photo", "headline", "table", "chart", "wb"])
|
||||
self.assertEqual([element["kind"] for element in elements], ["img", "shape", "table", "chart", "whiteboard"])
|
||||
self.assertEqual([element["order"] for element in elements], [0, 1, 2, 3, 4])
|
||||
self.assertEqual([element["id"] for element in elements], ["photo", "headline", "table", "chart", "wb", "emb"])
|
||||
self.assertEqual([element["kind"] for element in elements], ["img", "shape", "table", "chart", "whiteboard", "embed"])
|
||||
self.assertEqual([element["order"] for element in elements], [0, 1, 2, 3, 4, 5])
|
||||
self.assertEqual(elements[1]["type"], "text")
|
||||
self.assertEqual(elements[1]["textType"], "headline")
|
||||
self.assertEqual(elements[1]["textAlign"], "center")
|
||||
@@ -2404,6 +2428,25 @@ class XmlTextOverlapLintDensityTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(result["slides"][0]["issues"], [])
|
||||
|
||||
def test_lint_xml_does_not_report_blank_slide_for_embed_only_content(self) -> None:
|
||||
result = xml_text_overlap_lint.lint_xml(
|
||||
"""
|
||||
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
||||
<data>
|
||||
<embed id="emb" topLeftX="280" topLeftY="130" width="400" height="280">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 280">
|
||||
<circle cx="200" cy="140" r="100" fill="#2563EB"/>
|
||||
</svg>
|
||||
</embed>
|
||||
</data>
|
||||
</slide>
|
||||
"""
|
||||
)
|
||||
|
||||
self.assertEqual(result["summary"]["error_count"], 0)
|
||||
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
|
||||
self.assertNotIn("blank_slide", codes)
|
||||
|
||||
def test_lint_xml_does_not_report_blank_slide_for_line_only_content(self) -> None:
|
||||
result = xml_text_overlap_lint.lint_xml(
|
||||
"""
|
||||
|
||||
@@ -111,18 +111,29 @@ func TestBase_RoleWorkflow(t *testing.T) {
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
getResult, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"base", "+role-get", "--base-token", baseToken, "--role-id", roleID},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
getResult.AssertExitCode(t, 0)
|
||||
getResult.AssertStdoutStatus(t, true)
|
||||
pollTimeout := 30 * time.Second
|
||||
pollCtx, pollCancel := context.WithTimeout(ctx, pollTimeout)
|
||||
defer pollCancel()
|
||||
|
||||
rolePayload := gjson.Get(getResult.Stdout, "data.data").String()
|
||||
require.NotEmpty(t, rolePayload, "stdout:\n%s", getResult.Stdout)
|
||||
require.True(t, gjson.Valid(rolePayload), "stdout:\n%s", getResult.Stdout)
|
||||
assert.Equal(t, updatedRoleName, gjson.Get(rolePayload, "role_name").String())
|
||||
err = clie2e.WaitForCondition(pollCtx, clie2e.WaitOptions{
|
||||
Timeout: pollTimeout,
|
||||
Interval: 3 * time.Second,
|
||||
}, func() (bool, error) {
|
||||
getResult, getErr := clie2e.RunCmd(pollCtx, clie2e.Request{
|
||||
Args: []string{"base", "+role-get", "--base-token", baseToken, "--role-id", roleID},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
if getErr != nil {
|
||||
return false, getErr
|
||||
}
|
||||
if getResult.ExitCode != 0 {
|
||||
return false, getResult.RunErr
|
||||
}
|
||||
|
||||
rolePayload := gjson.Get(getResult.Stdout, "data.data").String()
|
||||
return gjson.Valid(rolePayload) && gjson.Get(rolePayload, "role_name").String() == updatedRoleName, nil
|
||||
})
|
||||
require.NoError(t, err, "role name should converge to %q", updatedRoleName)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user