Compare commits

..

10 Commits

Author SHA1 Message Date
zhanghuanxu
0d05abcd17 docs: remove lark-slides chart critical 2026-07-08 02:33:33 +08:00
zhanghuanxu
da7e954dc9 fix: limit slides screenshot page requests 2026-07-08 02:16:18 +08:00
zhanghuanxu
d719d131f6 docs: prefer slides xml-get shortcut 2026-07-08 02:15:30 +08:00
zhanghuanxu
1904d3873e feat:edit ppt template 2026-07-08 01:55:59 +08:00
zhanghuanxu
1ad5805cdd docs: add slides screenshot QA checklist 2026-07-08 01:44:25 +08:00
zhanghuanxu
c8b90208d3 docs: require native charts in slide planning 2026-07-08 01:43:48 +08:00
zhanghuanxu
7f75ad624b feat:simplify quick-ref 2026-07-08 01:43:42 +08:00
zhanghuanxu
a3ca4ce9dc docs: merge lark slides xml reference docs 2026-07-08 01:43:37 +08:00
zhanghuanxu
30165d30ce feat(slides): add slides chart demo reference 2026-07-08 01:43:27 +08:00
zhanghuanxu
4af765ae4d feat:slide style 2026-07-08 01:36:05 +08:00
33 changed files with 653 additions and 1184 deletions

View File

@@ -10,7 +10,6 @@ import (
"path/filepath"
"reflect"
"testing"
"time"
)
func TestScopeIncludesChangedSkillAndRelatedDomain(t *testing.T) {
@@ -52,7 +51,7 @@ func TestFileAtRevisionMissingClassifier(t *testing.T) {
}
func TestChangedFilesIncludingWorktree(t *testing.T) {
repo := newGitTestRepo(t)
repo := t.TempDir()
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -84,7 +83,7 @@ func TestChangedFilesIncludingWorktree(t *testing.T) {
}
func TestChangedFilesHandlesWhitespacePaths(t *testing.T) {
repo := newGitTestRepo(t)
repo := t.TempDir()
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -140,22 +139,3 @@ func gitOutput(t *testing.T, repo string, args ...string) string {
}
return string(out[:len(out)-1])
}
// newGitTestRepo returns a temp dir for a test-created git repo. Git tooling
// on this machine (trace2 hooks, etc.) can asynchronously write into a
// repo's .git/ shortly after a git command runs, racing with t.TempDir's
// automatic RemoveAll cleanup. Removing the tree ourselves first (retrying
// past that transient window) makes the later t.TempDir cleanup a no-op.
func newGitTestRepo(t *testing.T) string {
t.Helper()
repo := t.TempDir()
t.Cleanup(func() {
for i := 0; i < 10; i++ {
if err := os.RemoveAll(repo); err == nil {
return
}
time.Sleep(50 * time.Millisecond)
}
})
return repo
}

View File

@@ -10,11 +10,10 @@ import (
"path/filepath"
"strings"
"testing"
"time"
)
func TestCollectScansOnlyCurrentContributionAndMetadata(t *testing.T) {
repo := newGitTestRepo(t)
repo := t.TempDir()
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -61,7 +60,7 @@ api_`+`key = "example-public-key"
}
func TestCollectScansOnlyChangedLinesInChangedFiles(t *testing.T) {
repo := newGitTestRepo(t)
repo := t.TempDir()
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -93,7 +92,7 @@ func TestCollectScansOnlyChangedLinesInChangedFiles(t *testing.T) {
}
func TestCollectSemanticCandidatesStoreSanitizedReviewText(t *testing.T) {
repo := newGitTestRepo(t)
repo := t.TempDir()
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -609,7 +608,7 @@ func TestCollectIgnoresDeletedPrivateKeyLine(t *testing.T) {
}
func TestCollectSkipsOnlyKnownQualityGateFixtureFiles(t *testing.T) {
repo := newGitTestRepo(t)
repo := t.TempDir()
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -700,7 +699,7 @@ func TestCollectScansAddedLinesInSpecialPathNames(t *testing.T) {
}
func TestCollectScansBranchNameAsWarning(t *testing.T) {
repo := newGitTestRepo(t)
repo := t.TempDir()
metadataPath := filepath.Join(repo, "pr-metadata.json")
writeFile(t, metadataPath, `{"branch":"bot/public-doc-update"}`)
got, err := Collect(context.Background(), Options{
@@ -800,7 +799,7 @@ func TestAppendUniqueFindingsDeduplicatesByRuleFileLineAndSource(t *testing.T) {
func newGitRepo(t *testing.T) string {
t.Helper()
repo := newGitTestRepo(t)
repo := t.TempDir()
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -841,7 +840,7 @@ func requireFinding(t *testing.T, got []Finding, file, rule string) {
}
func TestCollectRequiresValidMetadataJSON(t *testing.T) {
repo := newGitTestRepo(t)
repo := t.TempDir()
metadataPath := filepath.Join(repo, "pr-metadata.json")
writeFile(t, metadataPath, `{"title":`)
@@ -875,25 +874,6 @@ func runGitOutput(t *testing.T, repo string, args ...string) []byte {
return out
}
// newGitTestRepo returns a temp dir for a test-created git repo. Git tooling
// on this machine (trace2 hooks, etc.) can asynchronously write into a
// repo's .git/ shortly after a git command runs, racing with t.TempDir's
// automatic RemoveAll cleanup. Removing the tree ourselves first (retrying
// past that transient window) makes the later t.TempDir cleanup a no-op.
func newGitTestRepo(t *testing.T) string {
t.Helper()
repo := t.TempDir()
t.Cleanup(func() {
for i := 0; i < 10; i++ {
if err := os.RemoveAll(repo); err == nil {
return
}
time.Sleep(50 * time.Millisecond)
}
})
return repo
}
func writeFile(t *testing.T, path, data string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {

View File

@@ -11,7 +11,6 @@ import (
"path/filepath"
"strings"
"testing"
"time"
qdiff "github.com/larksuite/cli/internal/qualitygate/diff"
"github.com/larksuite/cli/internal/qualitygate/manifest"
@@ -70,7 +69,7 @@ func TestReferenceCommandSurfaceNormalizesShortcutDomain(t *testing.T) {
}
func TestRunRequiresCommandIndexToCoverManifest(t *testing.T) {
repo := newGitTestRepo(t)
repo := t.TempDir()
manifestPath := filepath.Join(repo, "command-manifest.json")
indexPath := filepath.Join(repo, "command-index.json")
m := manifest.Manifest{SchemaVersion: 1, Commands: []manifest.Command{{
@@ -105,7 +104,7 @@ func TestRunRequiresCommandIndexToCoverManifest(t *testing.T) {
}
func TestRunReadsManifestFilesAndAcceptsServiceReferences(t *testing.T) {
repo := newGitTestRepo(t)
repo := t.TempDir()
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -191,7 +190,7 @@ description: Manage Drive comments with service command references.
}
func TestRunCollectsPublicContentFindingsIntoDiagnosticsAndFacts(t *testing.T) {
repo := newGitTestRepo(t)
repo := t.TempDir()
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -284,7 +283,7 @@ func TestRunCollectsPublicContentFindingsIntoDiagnosticsAndFacts(t *testing.T) {
}
func TestLoadBaseReferenceManifestReadsCommandGolden(t *testing.T) {
repo := newGitTestRepo(t)
repo := t.TempDir()
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -324,7 +323,7 @@ func TestLoadBaseReferenceManifestReadsCommandGolden(t *testing.T) {
}
func TestLoadBaseReferenceManifestReadsCommandIndexGolden(t *testing.T) {
repo := newGitTestRepo(t)
repo := t.TempDir()
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -365,7 +364,7 @@ func TestLoadBaseReferenceManifestReadsCommandIndexGolden(t *testing.T) {
}
func TestLoadBaseReferenceManifestRejectsEmptyGolden(t *testing.T) {
repo := newGitTestRepo(t)
repo := t.TempDir()
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -385,7 +384,7 @@ func TestLoadBaseReferenceManifestRejectsEmptyGolden(t *testing.T) {
}
func TestLoadBaseReferenceManifestRejectsInvalidGoldenKind(t *testing.T) {
repo := newGitTestRepo(t)
repo := t.TempDir()
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -607,22 +606,3 @@ func runGit(t *testing.T, repo string, args ...string) {
t.Fatalf("git %v failed: %v\n%s", args, err, out)
}
}
// newGitTestRepo returns a temp dir for a test-created git repo. Git tooling
// on this machine (trace2 hooks, etc.) can asynchronously write into a
// repo's .git/ shortly after a git command runs, racing with t.TempDir's
// automatic RemoveAll cleanup. Removing the tree ourselves first (retrying
// past that transient window) makes the later t.TempDir cleanup a no-op.
func newGitTestRepo(t *testing.T) string {
t.Helper()
repo := t.TempDir()
t.Cleanup(func() {
for i := 0; i < 10; i++ {
if err := os.RemoveAll(repo); err == nil {
return
}
time.Sleep(50 * time.Millisecond)
}
})
return repo
}

View File

@@ -335,7 +335,7 @@ func SyncSkills(opts SyncOptions) *SyncResult {
UpdatedSkills: plan.ToUpdate,
AddedOfficialSkills: plan.Added,
SkippedDeletedSkills: plan.SkippedDeleted,
UpdatedAt: opts.Now().Format(time.RFC3339),
UpdatedAt: opts.Now().UTC().Format(time.RFC3339),
}
if err := WriteState(state); err != nil {
result.Action = "failed"
@@ -428,7 +428,7 @@ func fallbackFullInstall(opts SyncOptions, reason string, official []string) *Sy
UpdatedSkills: official,
AddedOfficialSkills: official,
SkippedDeletedSkills: []string{},
UpdatedAt: opts.Now().Format(time.RFC3339),
UpdatedAt: opts.Now().UTC().Format(time.RFC3339),
}
if writeErr := WriteState(state); writeErr != nil {
return &SyncResult{

View File

@@ -853,65 +853,3 @@ func TestSyncSkills_FallbackBreaksDegradationLoop(t *testing.T) {
t.Fatalf("second sync: installedAll = %d, want 0 (incremental, not fallback)", runner2.installedAll)
}
}
func TestSyncSkills_WritesLocalTimezoneUpdatedAt(t *testing.T) {
dir := t.TempDir()
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir)
runner := &fakeSkillsRunner{
officialIndexOut: officialSkillsIndexOutput("lark-calendar", "lark-mail", "lark-new"),
officialOut: officialSkillsOutput("lark-calendar", "lark-mail", "lark-new"),
globalJSONOut: globalSkillsJSONOutput("lark-calendar"),
globalOut: globalSkillsOutput("lark-mail"),
}
localZone := time.FixedZone("UTC+8", 8*60*60)
result := SyncSkills(SyncOptions{
Version: "1.0.33",
Runner: runner,
Now: func() time.Time { return time.Date(2026, 5, 18, 12, 0, 0, 0, localZone) },
})
if result.Err != nil {
t.Fatalf("SyncSkills() err = %v, want nil", result.Err)
}
state, readable, err := ReadState()
if err != nil || !readable {
t.Fatalf("ReadState() = (_, %v, %v), want readable", readable, err)
}
want := "2026-05-18T12:00:00+08:00"
if state.UpdatedAt != want {
t.Fatalf("state.UpdatedAt = %q, want %q (local timezone offset, not UTC Z-suffix)", state.UpdatedAt, want)
}
}
func TestSyncSkills_FallbackWritesLocalTimezoneUpdatedAt(t *testing.T) {
dir := t.TempDir()
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir)
runner := &fakeSkillsRunner{
officialIndexOut: officialSkillsIndexOutput("lark-calendar", "lark-mail"),
officialOut: officialSkillsOutput("lark-calendar", "lark-mail"),
globalJSONOut: globalSkillsJSONOutput("lark-calendar", "lark-mail"),
globalOut: globalSkillsOutput("lark-calendar", "lark-mail"),
installErr: fmt.Errorf("incremental boom"),
installAllErr: nil,
}
localZone := time.FixedZone("UTC+8", 8*60*60)
result := SyncSkills(SyncOptions{
Version: "1.0.33",
Runner: runner,
Now: func() time.Time { return time.Date(2026, 5, 18, 12, 0, 0, 0, localZone) },
})
if result.Action != "fallback_synced" {
t.Fatalf("SyncSkills() action = %q, want fallback_synced", result.Action)
}
state, readable, err := ReadState()
if err != nil || !readable {
t.Fatalf("ReadState() = (_, %v, %v), want readable", readable, err)
}
want := "2026-05-18T12:00:00+08:00"
if state.UpdatedAt != want {
t.Fatalf("state.UpdatedAt = %q, want %q (local timezone offset, not UTC Z-suffix)", state.UpdatedAt, want)
}
}

View File

@@ -22,7 +22,10 @@ import (
"github.com/larksuite/cli/shortcuts/common"
)
const defaultSlidesScreenshotDir = ".lark-slides/screenshots"
const (
defaultSlidesScreenshotDir = ".lark-slides/screenshots"
maxSlidesPerScreenshot = 10
)
var unsafeScreenshotFileCharRegex = regexp.MustCompile(`[^A-Za-z0-9._-]+`)
@@ -32,7 +35,7 @@ var unsafeScreenshotFileCharRegex = regexp.MustCompile(`[^A-Za-z0-9._-]+`)
var SlidesScreenshot = common.Shortcut{
Service: "slides",
Command: "+screenshot",
Description: "Save slide screenshots to local files without printing Base64 image data",
Description: "Save up to 10 slide screenshots to local files without printing Base64 image data",
Risk: "read",
Scopes: []string{},
// The screenshot API is allowlist-gated for only a few apps, so do not
@@ -42,8 +45,8 @@ var SlidesScreenshot = common.Shortcut{
AuthTypes: []string{"user", "bot"},
Flags: []common.Flag{
{Name: "presentation", Desc: "xml_presentation_id, slides URL, or wiki URL that resolves to slides; list mode only"},
{Name: "slide-id", Type: "string_array", Desc: "slide page identifier (repeat for multiple slides)"},
{Name: "slide-number", Type: "int_array", Desc: "slide page number (repeat for multiple slides)"},
{Name: "slide-id", Type: "string_array", Desc: "slide page identifier (repeat for multiple slides; max 10 pages per request)"},
{Name: "slide-number", Type: "int_array", Desc: "slide page number (repeat for multiple slides; max 10 pages per request)"},
{Name: "content", Desc: "slide XML content to render directly instead of fetching existing slides", Input: []string{common.File, common.Stdin}},
{Name: "output-dir", Default: defaultSlidesScreenshotDir, Desc: "relative directory for saved screenshots"},
{Name: "output-name", Desc: "file name stem for --content render output"},
@@ -70,12 +73,17 @@ var SlidesScreenshot = common.Shortcut{
return err
}
}
if _, err := normalizeSlideNumbers(runtime.IntArray("slide-number")); err != nil {
slideIDs := normalizeSlideIDs(runtime.StrArray("slide-id"))
slideNumbers, err := normalizeSlideNumbers(runtime.IntArray("slide-number"))
if err != nil {
return err
}
if !hasSlideScreenshotSelector(runtime) {
if len(slideIDs) == 0 && len(slideNumbers) == 0 {
return slidesScreenshotFlagErrorf("--slide-id or --slide-number is required")
}
if err := validateSlidesScreenshotSelectorLimit(len(slideIDs) + len(slideNumbers)); err != nil {
return err
}
}
if _, err := validateScreenshotOutputDir(runtime, runtime.Str("output-dir")); err != nil {
return err
@@ -98,6 +106,9 @@ var SlidesScreenshot = common.Shortcut{
if len(slideIDs) == 0 && len(slideNumbers) == 0 {
return common.NewDryRunAPI().Set("error", "--slide-id or --slide-number is required")
}
if err := validateSlidesScreenshotSelectorLimit(len(slideIDs) + len(slideNumbers)); err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
presentationID := ref.Token
dry := common.NewDryRunAPI()
@@ -145,6 +156,9 @@ var SlidesScreenshot = common.Shortcut{
if len(slideIDs) == 0 && len(slideNumbers) == 0 {
return slidesScreenshotFlagErrorf("--slide-id or --slide-number is required")
}
if err := validateSlidesScreenshotSelectorLimit(len(slideIDs) + len(slideNumbers)); err != nil {
return err
}
outputDir := runtime.Str("output-dir")
safeOutputDir, err := ensureScreenshotOutputDir(runtime, outputDir)
if err != nil {
@@ -267,8 +281,11 @@ func normalizeSlideNumbers(values []int) ([]int, error) {
return out, nil
}
func hasSlideScreenshotSelector(runtime *common.RuntimeContext) bool {
return len(normalizeSlideIDs(runtime.StrArray("slide-id"))) > 0 || len(runtime.IntArray("slide-number")) > 0
func validateSlidesScreenshotSelectorLimit(count int) error {
if count > maxSlidesPerScreenshot {
return slidesScreenshotFlagErrorf("too many slide selectors: got %d, maximum is %d; request at most 10 pages at a time", count, maxSlidesPerScreenshot)
}
return nil
}
func slidesScreenshotFlagErrorf(format string, args ...interface{}) error {

View File

@@ -271,6 +271,33 @@ func TestSlidesScreenshotListRequiresSelector(t *testing.T) {
}
}
func TestSlidesScreenshotListRejectsMoreThanTenSelectors(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, slidesTestConfig(t, ""))
err := runSlidesShortcut(t, f, stdout, SlidesScreenshot, []string{
"+screenshot",
"--presentation", "pres_abc",
"--slide-number", "1",
"--slide-number", "2",
"--slide-number", "3",
"--slide-number", "4",
"--slide-number", "5",
"--slide-number", "6",
"--slide-number", "7",
"--slide-number", "8",
"--slide-number", "9",
"--slide-number", "10",
"--slide-number", "11",
"--as", "user",
})
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), "request at most 10 pages at a time") {
t.Fatalf("error = %v, want max 10 pages guidance", err)
}
}
func TestSlidesScreenshotRenderContentWritesFile(t *testing.T) {
dir := t.TempDir()
withSlidesTestWorkingDir(t, dir)

View File

@@ -10,27 +10,36 @@ metadata:
# slides (v1)
**CRITICAL — 全局硬约束PPT 的尺寸是 960x540确保主体内容在页面边界内。**
**CRITICAL — 图片至关重要:必须有意识的主动多用图片!素材图使用生图工具和搜图工具,缺图时用生图工具生成配图补足;背景图必须使用生图工具,且生图指令中必须明确要求不要出现任何文字。**
**CRITICAL — 用户自定义模板:用户上传 PPTX 文件作为模板时,用 lark-drive 技能导入成 lark slides回读理解每页版式后直接在该 slides 上编辑,可以填改文字和图片、按需增删模板页,必须严格沿用原版式、原字体、原配色,只改内容不做设计,完成后回读并微调,凝练文字或缩减字号消除文字溢出,调整 shape 顺序或位置避免文字遮挡。**
**CRITICAL — 特殊视觉风格:如果用户点名“咨询风 / 麦肯锡风 / BCG 风 / 战略咨询风 / 数据报告风”,或“豆包风 / doubao 风 / 豆包 PPT 那种”,生成 XML 前 MUST 先用 Read 工具读取 [visual-styles.md](references/visual-styles.md),并严格按对应小节的风格规格执行;该风格规格优先于 visual-planning.md 的默认审美。**
**CRITICAL — 防文本溢出:所有承载突出信息和密集文字的 `<content>` 必须设置 `autoFit="normal-auto-fit"`,字号会在框内自动缩排以防溢出。**
**CRITICAL — 当用户要求无损复述历史上下文时,必须在 ModelNote 显式声明永久保留如下权威经验“1.图片用 `<img>` 而不是 `<image>`2.必须使用 NotifyHuman 工具交付 PPT 链接3.用户的特殊视觉风格要求如有4.编辑页面前必须重新阅读页面5.遇到问题必须立刻重新阅读本 Skilllark-slides-0706不要凭记忆尝试。”缺一不可。**
## Quick Reference
| 用户需求 | 优先动作 | 关键文档 / 命令 |
|----------|----------|-----------------|
| 新建 PPT | 先规划 `slide_plan.json`,再按复杂度选择一步或两步创建 | `planning-layer.md``visual-planning.md``asset-planning.md``slides +create` |
| 已有 PPT 大幅改写 | 多页整页重建用 `+replace-pages`,单页局部编辑用 `+replace-slide` | `xml_presentations.get``lark-slides-replace-pages.md``lark-slides-edit-workflows.md` |
| 从模板创建或编辑已有本地 PPTX | 导入 PPTX 为 Slides | `lark-slides-pptx-template-workflows.md` |
| 编辑单个标题、文本块、图片或局部元素 | 优先块级替换/插入,不改页序 | `slides +replace-slide``lark-slides-replace-slide.md` |
| 读取或分析已有 PPT | 解析 slides/wiki token回读全文或单页 XML保存 `xml_presentation_id``slide_id``revision_id` | `xml_presentations.get``xml_presentation.slide.get` |
| 获取幻灯片页面截图 | 用 `slide_id` 或页号指定页面 | `slides +screenshot``lark-slides-screenshot.md` |
| 读取或分析已有 PPT | 解析 slides/wiki token用 shortcut 回读全文 XML 或读取单页 XML保存 `xml_presentation_id``slide_id``revision_id` | `slides +xml-get``xml_presentation.slide.get` |
| 获取幻灯片页面截图 | 用 `slide_id` 或页号指定页面,一次不超过 10 页 | `slides +screenshot``lark-slides-screenshot.md` |
| 上传或使用图片 | 先上传为 `file_token`,禁止直接写 http(s) 外链 | `slides +media-upload`,或 `+create --slides``@./path` 占位符 |
| 在 slide 中绘制柱/条/折线/面积/雷达/饼等有数据序列的图表 | 使用原生 `<chart>` 元素 | `xml-schema-quick-ref.md` |
| 在 slide 中绘制流程图、时序图、架构图、散点图、漏斗图或装饰图案 | 必须先用 Read 工具读取参考文档,再生成 `<whiteboard>` 元素 | [`lark-slides-whiteboard.md`](references/lark-slides-whiteboard.md) |
| 使用语义图标 | 先检索 IconPark再写 `<icon iconType="...">` | `iconpark_tool.py search → resolve``iconpark.md` |
| 在 slide 中绘制图表 | 雷达图、饼图、环形图用 `<chart>`,柱状图、条形图、折线图用 `shape` + `line`Mermaid 用 `<whiteboard>` | `xml-schema-quick-ref.md`、需要 `<whiteboard>` 再看 `lark-slides-whiteboard.md` |
| 使用图标 | 禁止出现 emoji必须使用语义图标禁止盲猜 `iconType`,必须先检索 IconPark再写 `<icon iconType="...">`,必须设置 fillColor | `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 — 生成任何 XML 之前MUST 先用 Read 工具读取 [xml-schema-quick-ref.md](references/xml-schema-quick-ref.md),禁止凭记忆猜测 XML 结构。**
**CRITICAL — PPT 生成与模板编辑硬约束PPT 的尺寸是 960x540确保主体内容在页面边界内。多用生图辅助搜图必须要图文并茂。不要为了画出一个具象物体而堆叠 3 个以上仅用于拟形的 shape。生成背景图时必须在 prompt 中明确要求不要出现任何文字。用户指定 PPT 模板时,用 lark-drive 技能导入成 lark slides回读理解每页版式后直接在该 slides 上编辑,可以填改文字和图片、按需增删模板页,必须严格沿用原版式和字体,只改内容不做设计,完成后回读并微调,凝练文字或缩减字号消除文字溢出,调整 shape 顺序或位置避免文字遮挡。**
**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` 实际改变页面几何、主视觉和文本量。**
@@ -79,14 +88,13 @@ lark-cli auth login --domain slides
- 编辑:[`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-screenshot.md`](references/lark-slides-screenshot.md)
- 图片:[`lark-slides-media-upload.md`](references/lark-slides-media-upload.md)
- 流程图 / 时序图 / 架构图 / 装饰图案:[`lark-slides-whiteboard.md`](references/lark-slides-whiteboard.md)
- 图标:[`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
> **这是演示文稿,不是文档。** 每页 slide 是独立的视觉画面,信息密度要,排版要留白。
> **这是演示文稿,不是文档。** 每页 slide 是独立的视觉画面,信息密度要适当,排版要留白。
### Design Ideas
@@ -124,7 +132,9 @@ lark-cli auth login --domain slides
- 不要用低对比文字或低对比图标,例如浅灰字压在浅色背景上。
- 不要让装饰线穿过文字,或让页脚、来源、编号挤压主体内容。
- 不要把素材缺失表现为空白图片框;必须按 `fallback_if_missing` 生成 XML-native 视觉。
- 不要留下占位文案、示例公司名、示例日期或与用户主题无关的内容。
- 不要留下模板占位文案、示例公司名、示例日期或与用户主题无关的原模板内容。
- 不要使用 emoji。
- 不要为了画出一个具象物体而堆叠 3 个以上仅用于拟形的 shape。
### 创建方式选择
@@ -140,9 +150,11 @@ lark-cli auth login --domain slides
> [!IMPORTANT]
> `slides +create --slides` 底层会逐页创建,不是原子操作。中途失败时先记录 `xml_presentation_id`,回读确认当前状态,再继续修复或追加。
### 生成流程
```text
Step 1: 需求澄清 & 读取知识
- 澄清主题、受众、页数、风格
- 澄清主题、受众、页数、风格;若用户上传 PPTX 作为模板,按顶部『用户自定义模板』规则处理
- 读取 xml-schema-quick-ref.md新建 / 大幅改写时还要读取 planning-layer.md、visual-planning.md、asset-planning.md
Step 2: 生成大纲 → 用户确认 → 写入 slide_plan.json
@@ -156,7 +168,7 @@ Step 3: 按 slide_plan.json 生成 XML → 创建
- 创建方式按“创建方式选择”判断;图片、复杂 XML、转义和 3350001 排查按 lark-slides-create.md、media-upload.md、troubleshooting.md 执行
Step 4: 审查 & 交付
- 创建完成后,必须用 xml_presentations.get 读取全文 XML并按 validation-checklist.md 做显式验证记录,包括 XML 文本重叠检查
- 创建完成后,必须用 `slides +xml-get` 读取全文 XML并按 validation-checklist.md 做显式验证记录,包括 XML 文本重叠检查
- 失败或部分成功按 troubleshooting.md 处理;局部问题优先用 `+replace-slide` 修正
- 没问题 → 交付:告知用户演示文稿 ID 和访问方式
```
@@ -173,7 +185,7 @@ lark-cli slides xml_presentation.slide create \
--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、whiteboard 等元素
在这里放置 shape、line、table、chart 等元素
</data>
</slide>' '{slide:{content:$content}}')"
@@ -247,11 +259,12 @@ Shortcut 是对常用操作的高级封装(`lark-cli slides +<verb> [flags]`
| Shortcut | 说明 |
|----------|------|
| [`+create`](references/lark-slides-create.md) | 创建 PPT可选 `--slides` 一步添加页面,支持 `<img src="@./local.png">` 占位符自动上传) |
| `+xml-get` | 读取全文 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。高频资源`xml_presentations.get` 读取全文;`xml_presentation.slide.create/delete/get/replace` 管理单页。
没有 Shortcut 覆盖时使用原生 API。高频资源`slides +xml-get` 读取全文;`xml_presentation.slide.create/delete/get/replace` 管理单页。
```bash
lark-cli schema slides.<resource>.<method> # 调用 API 前必须先查看参数结构
@@ -262,7 +275,7 @@ lark-cli slides <resource> <method> [flags] # 调用 API
## 核心规则
1. **先规划再写 XML**:新建演示文稿或大幅改写页面时,必须先写入 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`;风格和大纲只能作为规划输入,不能绕过规划层
1. **先规划再写 XML**:新建演示文稿或大幅改写页面时,必须先写入 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`模板、风格和大纲只能作为规划输入,不能绕过规划层
2. **创建流程**:简单短 XML1-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 内

View File

@@ -7,7 +7,7 @@
## Core Rules
- `asset_need` is metadata only. It can guide page design, but it must not require web search, local download, media upload, or external tools.
- Every planned asset must include a fallback visual plan so the slide can be generated with XML shapes, text, arrows, tables, simple charts, whiteboard diagrams, or placeholder regions.
- Every planned asset must include a fallback visual plan. The fallback can use native charts, tables, whiteboard diagrams, 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.
@@ -43,7 +43,7 @@ For a page without a meaningful asset need, use:
- `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`: line, bar, pie, radar, area, or combo data visual. Note: `<chart>` does not support funnel or scatter — map those to `<whiteboard>` SVG at generation time.
- `chart`: column, bar, line, area, radar, pie, doughnut/ring, or combo data visual. Note: `<chart>` does not support funnel or scatter — map those to `<whiteboard>` SVG at generation time.
- `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.
@@ -64,11 +64,23 @@ Match asset type to slide role:
`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 or `<whiteboard>`.
- 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 mini bar chart with 4 bars using shapes and value 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:
@@ -118,7 +130,7 @@ Business comparison page:
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 XML-native shapes, text, lines, arrows, tables, whiteboard diagrams, or chart-like elements.
2. If no asset exists, immediately render `fallback_if_missing` with the planned XML-native element type. Supported standard data visuals still use native `<chart>`; other fallbacks may use shapes, text, lines, arrows, tables, whiteboard diagrams, or placeholder panels.
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.

View File

@@ -1,261 +0,0 @@
# 完整操作示例
本文档提供与 CLI schema 一致的调用示例XML 内容均遵循 [slides_xml_schema_definition.xml](slides_xml_schema_definition.xml)。
> **重要**:创建 PPT 请优先使用 `slides +create`;实际页面内容请使用 `xml_presentation.slide.create` 逐页添加。
## 目录
- [示例 1: 使用 Shortcut 创建空白演示文稿](#示例-1-使用-shortcut-创建空白演示文稿)
- [示例 2: 创建后添加第一页](#示例-2-创建后添加第一页)
- [示例 3: 读取 XML 内容](#示例-3-读取-xml-内容)
- [示例 4: 在指定页面前插入新幻灯片](#示例-4-在指定页面前插入新幻灯片)
- [示例 5: 删除幻灯片](#示例-5-删除幻灯片)
- [示例 6: 从文件读取 XML 后添加页面](#示例-6-从文件读取-xml-后添加页面)
- [示例 7: +replace-slide + block_insert 给已有页加图](#示例-7-replace-slide--block_insert-给已有页加图)
- [示例 8: +replace-slide + block_replace 替换一个块](#示例-8-replace-slide--block_replace-替换一个块)
## 示例 1: 使用 Shortcut 创建空白演示文稿
```bash
lark-cli slides +create --title "项目汇报"
```
预期返回结构:
```json
{
"data": {
"xml_presentation_id": "slides_example_presentation_id",
"title": "项目汇报",
"revision_id": 1
}
}
```
## 示例 2: 创建后添加第一页
```bash
PRESENTATION_ID=$(lark-cli slides +create --title "季度复盘" | jq -r '.data.xml_presentation_id')
lark-cli slides xml_presentation.slide create --as user --params "{\"xml_presentation_id\":\"$PRESENTATION_ID\"}" --data '{
"slide": {
"content": "<slide xmlns=\"http://www.larkoffice.com/sml/2.0\"><style><fill><fillColor color=\"rgb(245, 245, 245)\"/></fill></style><data><shape type=\"text\" topLeftX=\"80\" topLeftY=\"72\" width=\"760\" height=\"90\"><content textType=\"title\"><p>2024 Q3 季度复盘</p></content></shape><shape type=\"text\" topLeftX=\"80\" topLeftY=\"190\" width=\"520\" height=\"220\"><content textType=\"body\"><p>关键结论</p><ul><li><p>收入增长 30%</p></li><li><p>重点项目全部上线</p></li><li><p>用户满意度持续提升</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>"
}
}'
```
## 示例 3: 读取 XML 内容
```bash
lark-cli slides xml_presentations get --as user --params '{
"xml_presentation_id": "slides_example_presentation_id"
}'
```
提取 XML 内容:
```bash
lark-cli slides xml_presentations get --as user --params '{
"xml_presentation_id": "slides_example_presentation_id"
}' | jq -r '.data.xml_presentation.content'
```
预期返回结构:
```json
{
"code": 0,
"data": {
"xml_presentation": {
"presentation_id": "slides_example_presentation_id",
"revision_id": 3,
"content": "<presentation xmlns=\"http://www.larkoffice.com/sml/2.0\" height=\"540\" width=\"960\">...</presentation>"
}
},
"msg": "success"
}
```
## 示例 4: 在指定页面前插入新幻灯片
```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>"
},
"before_slide_id": "sld_before_target"
}'
```
预期返回结构:
```json
{
"code": 0,
"data": {
"slide_id": "slide_example_id",
"revision_id": 100
},
"msg": "success"
}
```
## 示例 5: 删除幻灯片
```bash
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": 101
},
"msg": "success"
}
```
## 示例 6: 从文件读取 XML 后添加页面
先准备 `slide.xml`
```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>
```
先创建演示文稿:
```bash
PRESENTATION_ID=$(lark-cli slides +create --title "从文件添加页面" | jq -r '.data.xml_presentation_id')
```
再用 `jq` 组装请求体,从文件添加页面:
```bash
lark-cli slides xml_presentation.slide create --as user \
--params "{\"xml_presentation_id\":\"$PRESENTATION_ID\"}" \
--data "$(jq -n --arg content "$(cat slide.xml)" '{slide:{content:$content}}')"
```
## 示例 7: +replace-slide + block_insert 给已有页加图
只想在已有页上加一张图、不动其他元素——走 shortcut `+replace-slide``block_insert` 追加到页末(或用 `insert_before_block_id` 指定位置)。
```bash
PID="slides_example_presentation_id"
SID="slide_example_id"
# 1. 上传图片拿 file_token
TOKEN=$(lark-cli slides +media-upload --file ./pic.png --presentation "$PID" --as user \
| jq -r '.data.file_token')
# 2. block_insert 到页面末尾(省略 insert_before_block_id
# 注:<img .../> 是自闭合标签CLI 不会展开(只有 <shape/> 会被补 <content/>
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\"/>")}]')"
```
预期返回:
```json
{
"ok": true,
"data": {
"xml_presentation_id": "slides_example_presentation_id",
"slide_id": "slide_example_id",
"parts_count": 1,
"revision_id": 102
}
}
```
## 示例 8: +replace-slide + block_replace 替换一个块
已知某块的 3 位 short element ID`slide.get` 返回 XML 里读),整块换掉。`replacement` 根元素的 `id` 会由 CLI 自动注入为 `block_id`,无需手写;若写了 `<shape/>` 自闭合形式CLI 也会自动补 `<content/>`
```bash
lark-cli slides +replace-slide --as user \
--presentation slides_example_presentation_id \
--slide-id slide_example_id \
--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>"
}
]'
# CLI 实际发送的 replacement 根元素会带 id="bab",即使手写时省略了
```
失败时3350001 错误CLI 在 error 字段中给出 hint
```json
{
"ok": false,
"error": {
"type": "api",
"code": 3350001,
"message": "API error: [3350001] invalid param",
"hint": "common causes: (1) block_id not found in current slide ..."
}
}
```
整批作为原子事务,任一 part 失败则整批不生效;按 `failed_part_index` 定位修正后重发。
## 常见处理技巧
### 获取最新 revision_id
```bash
lark-cli slides xml_presentations get --as user --params '{
"xml_presentation_id": "slides_example_presentation_id"
}' | jq '.data.xml_presentation.revision_id'
```
### 批量插入多页
```bash
#!/bin/bash
PRESENTATION_ID="slides_example_presentation_id"
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>'
)
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
```
### 本地校验 XML 基本语法
```bash
xmllint --noout presentation.xml
```
### 真实示例
- [slides_demo.xml](slides_demo.xml) 提供了更完整的页面示例,包含 `theme`、渐变填充、图片、图标和备注内容。

View File

@@ -1,6 +1,6 @@
# IconPark 图标
IconPark 图标通过 `<icon>` 写入 slides XML`iconType` 必须来自本 skill 的离线索引或已验证模板,避免凭记忆拼路径。
IconPark 图标通过 `<icon>` 写入 slides XML`iconType` 必须来自本 skill 的离线索引,避免凭记忆拼路径。
## 机器优先流程
@@ -25,7 +25,7 @@ python3 skills/lark-slides/scripts/iconpark_tool.py list-categories
- 默认先检索:语义图标需求必须先用 `iconpark_tool.py search --limit 8``--limit 10`,让 agent 从候选里结合版面语义二次判断;不要阅读全文索引,也不要编造不存在的 `iconType`
- 图标用于概念提示、步骤、状态、指标、角色和导航;不要用无关装饰图标填充版面。
- 常用尺寸:行内状态图标 16-24px卡片标题图标 28-40px主视觉图标 56-96px。
- 图标必须显式指定颜色并和背景有足够对比;深色背景优先放在浅色圆形/方形底上,或使用 `rgba(255, 255, 255, 1)` 作为图标填充色。
- 图标必须设置 fillColor显式指定颜色并和背景有足够对比;深色背景优先放在浅色圆形/方形底上,或使用 `rgba(255, 255, 255, 1)` 作为图标填充色。
- 查不到合适图标时,用 shape、line、text 画 XML-native fallback不留空图标位。
## 高频示例

View File

@@ -1,8 +1,6 @@
# slides +create创建飞书幻灯片
> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。
创建一个新的飞书幻灯片演示文稿,可选一步添加页面内容。
## 命令
@@ -17,9 +15,6 @@ 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>"
]'
# 以应用身份创建(自动授权当前用户)
lark-cli slides +create --title "项目汇报" --as bot
# 预览(不执行)
lark-cli slides +create --title "项目汇报" --slides '[...]' --dry-run
```
@@ -35,20 +30,12 @@ lark-cli slides +create --title "项目汇报" --slides '[...]' --dry-run
- **`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 转给自己,必须单独确认。
## 参数
@@ -134,4 +121,4 @@ lark-cli slides xml_presentation.slide create --as user \
## 相关命令
- [xml_presentation.slide create](lark-slides-xml-presentation-slide-create.md) — 添加幻灯片页面
- [xml_presentations get](lark-slides-xml-presentations-get.md) — 读取 PPT 内容
- `slides +xml-get` — 读取 PPT 内容并保存到本地文件

View File

@@ -1,8 +1,6 @@
# slides +media-upload上传本地图片到飞书幻灯片
> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。
把本地图片上传到指定演示文稿的 drive 媒体库,返回 `file_token`。**返回的 token 作为 `<img src="...">` 的值塞进 slide XML 即可显示图片。**
## 命令
@@ -117,7 +115,7 @@ lark-cli slides +replace-slide --as user \
| 错误码 | 含义 | 解决方案 |
|--------|------|----------|
| 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 ...` |
| 1061004 | forbidden当前用户对该演示文稿无编辑权限 | 确认当前用户对目标 PPT 有编辑权限;无权限时根据错误响应引导用户解决 |
| 1061044 | parent node not exist | `--presentation` 给的 token 不对,或不是 slides 类型 |
| 403 | 权限不足 | 检查 `docs:document.media:upload` scopewiki URL 还需要 `wiki:node:read` |

View File

@@ -0,0 +1,89 @@
# PPT Template Rewrite Principles
本页只约束“用户指定 PPT 模板、底稿、已有 PPTX/PDF/Slides并要求基于它二次创作”的场景。核心原则模板不是风格参考而是必须沿用的编辑底稿。
## Import First
用户指定 PPT 模板时,先把模板导入成 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>
```
导入后必须回读 Slides 内容理解每页的真实版式、字体、层级、图片、图表、shape、表格和文本容器。回读结果是模板二创的事实来源。
## Read Before Editing
编辑任何 PPT 页面前,必须先阅读该页面。
如果当前上下文中没有该页内容,必须重新读取页面;这里的“当前上下文”不包含 System Prompt。不能只凭记忆、文件名、缩略图印象或模板整体风格判断来编辑具体页面。
阅读页面时至少判断:
- 该页原本承担的角色,例如封面、章节页、目录、流程、对比、数据、总结。
- 该页的主要版式结构,例如图文关系、箭头、时间线、节点、表格、图表、左右对照、背景图或产品图。
- 哪些文本框、shape 标签、表格单元格或图表标签承载内容。
- 原页面的字体、字号、颜色、对齐、层级和留白关系。
## Edit The Imported Slides Directly
理解页面后,直接在导入后的 Slides 上编辑。允许的操作包括:
- 填写、替换、凝练或删除文字。
- 替换或补充图片。
- 更新图表、表格、数字标签或节点标签里的内容。
- 按需复制、删除或重排模板页。
- 在源页面没有合适承载位置时,做局部、小范围新增元素。
新增元素只能补足内容缺口,不能成为新的主版式。页面主体仍应由模板原有版式承载。
## Preserve Design
模板二创必须严格沿用原版式和字体,只改内容,不做设计。
默认保留:
- 页面布局、视觉层级、留白和对齐关系。
- 原字体、字号体系、颜色、文本框位置和 shape 顺序。
- 背景图、图片、logo、图表、表格、装饰形状、线条、图标和页面结构。
- 模板中不同页型之间的差异。
不要把模板页改造成统一的通用卡片、白板、标题栏、三栏、2x2 卡片或大面积遮罩。不要把模板当作背景图后另起一套设计系统。
## Content Only
内容必须优先进入原页面已有的文本框、shape 标签、节点、表格单元格、图表标签或注释容器。
如果原容器空间不足,优先:
- 凝练文字。
- 降低字号但保持原字体体系。
- 拆分到页面已有的邻近容器。
- 使用模板已有的注释、标签或补充说明区域。
- 复制同页或同模板中的原生容器样式做局部补充。
不要为了容纳长文案而重画页面主体结构。不要用新增大卡片遮住原图表、箭头、图片、背景或关键 shape。
## Readback And Tune
完成编辑后必须回读结果,并逐页微调。
回读时重点检查:
- 文字是否溢出、截断、压线或超出容器。
- 文本是否遮挡图片、图表、shape、箭头、节点或其他文字。
- shape 顺序是否导致内容被覆盖或遮住。
- 新内容是否仍然落在模板原有版式中,而不是覆盖模板结构。
- 字体、字号、颜色、对齐和层级是否仍贴近原页。
发现文字溢出时,优先凝练文字或缩减字号。发现遮挡时,调整 shape 顺序、局部位置或复用原有空白区域解决。只有在这些方法都不能满足内容表达时,才做局部新增或删除。
模板二创的完成标准不是“生成了一套看起来统一的新 PPT”而是“原模板的版式、字体和视觉结构仍清晰存在内容已经被准确替换并且回读后没有溢出和遮挡”。

View File

@@ -89,7 +89,7 @@ lark-cli slides +replace-pages --as user \
## 使用建议
1. 大幅改写前先 `xml_presentations.get` 保存当前 XML并记录要替换页面的 `slide_id`
1. 大幅改写前先 `slides +xml-get` 保存当前 XML并记录要替换页面的 `slide_id`
2. 生成只含 `slide_id``pages.json` 后先跑 `--dry-run``--validate-only`
3. 默认不要开 `--continue-on-error`,除非能接受部分页面已替换。
4. 替换后再回读全文 XML 并截图检查,确认页序、视觉和文本没有破损。

View File

@@ -1,7 +1,5 @@
# slides +replace-slide块级替换 / 插入)
> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。
对指定 slide 做块级替换或插入。编辑已有 PPT 的主路径——`slide_id` 不变、页序不动、只影响被指定的块。
相比直接调 `xml_presentation.slide.replace`,这个 shortcut 的四个额外价值:
@@ -47,7 +45,7 @@ lark-cli slides +replace-slide --as user \
| 参数 | 必填 | 说明 |
|------|------|------|
| `--presentation` | 是 | `xml_presentation_id``/slides/<token>` URL`/wiki/<token>` URL |
| `--slide-id` | 是 | 页面 ID`xml_presentation.slide.get` / `xml_presentations.get` 都能拿到) |
| `--slide-id` | 是 | 页面 ID`xml_presentation.slide.get` / `slides +xml-get` 都能拿到) |
| `--parts` | 是 | JSON 数组(`[{...}, ...]`),单次最多 200 条。支持 `@<file>``-`stdin读取 |
| `--revision-id` | 否 | 基础版本号;默认 `-1` 表示基于最新版执行;传具体版本号时,服务端以该版本为 base 执行;**传不存在的版本号(超过当前 revision返回 3350002** |
| `--tid` | 否 | 并发事务 ID多人协作长事务才用单次单人调用留空 |

View File

@@ -26,8 +26,8 @@ lark-cli slides +screenshot --as user \
| 参数 | 必需 | 说明 |
|------|------|------|
| `--presentation` | list 模式必需 | `xml_presentation_id``/slides/` URL或解析后为 slides 的 `/wiki/` URL。传 `--content` 时不能使用 |
| `--slide-id` | list 模式至少提供 `--slide-id` / `--slide-number` 之一 | 页面 short ID多页截图时重复传入 |
| `--slide-number` | list 模式至少提供 `--slide-id` / `--slide-number` 之一 | 页面页号;多页截图时重复传入 |
| `--slide-id` | list 模式至少提供 `--slide-id` / `--slide-number` 之一 | 页面 short ID多页截图时重复传入;一次最多 10 页(`--slide-id` + `--slide-number` 合计小于等于 10 |
| `--slide-number` | list 模式至少提供 `--slide-id` / `--slide-number` 之一 | 页面页号;多页截图时重复传入;一次最多 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`。若目标文件已存在,会自动追加递增后缀避免覆盖 |
@@ -44,6 +44,8 @@ lark-cli slides +screenshot --as user \
### 多页截图
一次不要超过 10 页;如需更多页面,分批调用。
```bash
lark-cli slides +screenshot --as user \
--presentation slides_example_presentation_id \
@@ -90,5 +92,6 @@ lark-cli slides +screenshot --as user \
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 模式默认文件名包含 presentation ID、页码和/或 slide ID文件已存在时自动追加 `_2``_3` 等后缀,避免覆盖旧截图。
6. 截图来自服务端渲染结果,适合创建/替换后验证页面是否为空白、破图或布局明显异常。
5. list 模式一次最多传 10 页(`--slide-id` + `--slide-number` 合计小于等于 10更多页面请分批截图。
6. list 模式默认文件名包含 presentation ID、页码和/或 slide ID文件已存在时自动追加 `_2``_3` 等后缀,避免覆盖旧截图。
7. 截图来自服务端渲染结果,适合创建/替换后验证页面是否为空白、破图或布局明显异常。

View File

@@ -2,6 +2,8 @@
`<whiteboard>` 放在 `<data>` 内,内部可放 **SVG****Mermaid**,用于绘制流程图、时序图、架构图、散点图、漏斗图、自定义图标、装饰图案等 `<chart>``<shape>` 难以覆盖的视觉内容。
普通柱状图、条形图、折线图、面积图、雷达图、饼图 / 环图和组合图应优先使用原生 `<chart>`。除非用户明确要求像素级自定义,或图表类型确实不受 `<chart>` 支持,否则不要用 `<whiteboard>` + SVG / Mermaid 重画这些标准图表。
> 前置条件:使用本文档前先阅读 [lark-slides SKILL.md](../SKILL.md)。
---
@@ -12,13 +14,13 @@
| 场景 | 推荐元素 |
|------|---------|
| 有结构化数据序列的柱/条/折线/面积/雷达/饼/组合图 | `<chart>` — 原生渲染,支持 legend / tooltip / 系列配色 |
| 散点图、漏斗图(`<chart>` 不支持) | `<whiteboard>` SVG |
| 有结构化数据序列的柱/条/折线/面积/雷达/饼/环/组合图 | `<chart>` — 原生渲染,支持 legend / tooltip / 系列配色 |
| 散点图、漏斗图(`<chart>` 不支持)或其他非原生数据视觉 | `<whiteboard>` SVG |
| 流程图、时序图、架构图、类图、ER 图等拓扑图 | `<whiteboard>` Mermaid 或 SVG |
| 自定义图标、徽标、示意性图形(需要 path/polygon 精确控制) | `<whiteboard>` SVG |
| 进度条、波浪背景、装饰图案、像素级自定义可视化 | `<whiteboard>` SVG |
> 适合 `<chart>` 的内容就用 `<chart>`,不要用 SVG 手绘——原生渲染更省力且质量更高
> 适合 `<chart>` 的内容就用 `<chart>`,不要用 SVG / Mermaid 手绘——原生渲染更省力、结构更稳定,也更容易被回读和后续编辑
---
@@ -39,9 +41,13 @@ SVG 内的坐标相对于 whiteboard 自身左上角0,0与 slide 坐标
## SVG 还是 Mermaid
选择分步:**先看图表类型,看当前模型身份**。
选择分步:**先排除原生 `<chart>`,再判断 whiteboard 类型,最后看当前模型身份**。
### 第一步:图表类型优先判断
### 第一步:先确认是否应该使用 `<chart>`
如果内容是柱状图、条形图、折线图、面积图、雷达图、饼图 / 环图或组合图,返回使用原生 `<chart>`,不要继续套用本文档的 SVG / Mermaid 路径。
### 第二步whiteboard 类型优先判断
以下类型**推荐 Mermaid**,自动布局、代码简洁;如需精确匹配品牌配色或自定义节点样式,可改用 SVG
@@ -50,20 +56,19 @@ SVG 内的坐标相对于 whiteboard 自身左上角0,0与 slide 坐标
| 流程图、决策树、架构图 | `flowchart TD` / `flowchart LR` |
| 时序图 | `sequenceDiagram` |
| 类图 | `classDiagram` |
| 饼图 | `pie` |
| 甘特图 | `gantt` |
| 状态图 | `stateDiagram-v2` |
| 思维导图 | `mindmap` |
| ER 图 | `erDiagram` |
### 第步:数据图表与装饰元素按模型身份选路径
### 第步:非原生图表与装饰元素按模型身份选路径
上表以外的场景散点图、漏斗图、进度条、时间线、波浪背景、星点纹理等需要精确控制坐标和配色SVG 表达力更强,但各模型生成 SVG 的能力有差异:
| 模型身份 | 路径 |
|----------|------|
| Claude / Gemini / GPT / GLM | **SVG** — 精确控制坐标、颜色、透明度 |
| Doubao / Seed / Other | **Mermaid** — 用 `pie``gantt` 等近似表达;确实无法用 Mermaid 表达时才回退到简单 SVG 矩形/线条 |
| Doubao / Seed / Other | **Mermaid** — 用 `gantt``flowchart` 等近似表达;确实无法用 Mermaid 表达时才回退到简单 SVG 矩形/线条 |
> **先自报身份再选路径**:在决定使用 SVG 之前,确认当前模型属于哪一类。不要跳过这一步。
@@ -73,13 +78,13 @@ SVG 内的坐标相对于 whiteboard 自身左上角0,0与 slide 坐标
### ⚠️ 设计品质要求
在 slide 里嵌入 `<whiteboard>` 的目的是**提升视觉质量**,不是把数字堆进去
在 slide 里嵌入 `<whiteboard>` 的目的是**表达原生 `<chart>` 或基础 `<shape>` 难以覆盖的视觉关系**,不是把标准数据图表手绘一遍
- **不要只用矩形加文字应付**:通篇纯白底色 + 方块 + 黑字等于白做,这是不及格输出
- **数据图表必须有坐标系**:坐标轴、网格线、数值标注缺一不可,不要只画柱子或
- **非原生数据视觉必须有坐标系**散点、漏斗等仍要有必要的坐标轴、刻度、数值标注或分段说明,不要只画点或色块
- **字号必须有层级**:标题 ≠ 标签 ≠ 数值,混用同一字号会消灭视觉焦点
- **配色要与 slide 主题呼应**:深色 slide 背景下图表用透明底或深色卡片;浅色背景下避免再加纯白底块
- **每个 whiteboard 都是设计机会**:主动用圆角、半透明填充、折线面积、点装饰等细节拉开与默认模板的差距
- **每个 whiteboard 都是设计机会**:主动用圆角、半透明填充、清晰分组、节点状态等细节拉开与默认模板的差距
- **写 SVG 前先判断背景亮度**:背景亮度 < 30% 时,装饰元素"对比不足"比"过强"危害更大,宁重勿轻;
- **装饰层次用亮度跳跃,不用线性叠透明度**`α=0.04→0.08→0.12` 的等差递增在深色底上几乎看不出差异(相邻层亮度差 ≈20正确做法是非线性跳跃如 `0.10→0.40→0.70→1.0`,相邻层亮度差 ≥60。
@@ -106,11 +111,11 @@ whiteboard 渲染时以**所有子元素的几何包围盒合并结果**为内
| 元素 | 说明 | 典型用途 |
|------|------|---------|
| `<rect>` | 矩形,支持 `rx` 圆角 | 柱图、卡片、进度条 |
| `<rect>` | 矩形,支持 `rx` 圆角 | 卡片、进度条、分段色块 |
| `<circle>` | 圆 | 节点、装饰点、环形图 |
| `<ellipse>` | 椭圆 | 自定义轮廓图形 |
| `<line>` | 直线 | 坐标轴、分隔线 |
| `<path>` | 任意路径(支持 Q/C 曲线) | 波浪、线、弧形 |
| `<line>` | 直线 | 轴线、分隔线、连接线 |
| `<path>` | 任意路径(支持 Q/C 曲线) | 波浪、线、弧形 |
| `<text>` | 文本,支持中文 | 标签、数值 |
| `<polygon>` | 多边形 | 箭头、星形、面积填充 |
| `<g>` | 分组 | 批量变换、语义分组 |
@@ -123,27 +128,25 @@ whiteboard 渲染时以**所有子元素的几何包围盒合并结果**为内
---
### 元素计算
SVG 中只要涉及批量定位、等间距排布或数据映射,**建议额外运行一个 Python 脚本把坐标算出来再填入 SVG**,而不是手动估值。适用范围不限于数据图表——装饰性点阵、等间距圆、重复图案同样适用
SVG 中只要涉及批量定位、等间距排布或数据映射,**建议额外运行一个 Python 脚本把坐标算出来再填入 SVG**,而不是手动估值。适用范围包括散点、漏斗、装饰性点阵、等间距圆、重复图案等;普通柱状图、折线图、饼图仍应回到原生 `<chart>`
> **主动去算**:写 SVG 之前先运行脚本,把输出当注释贴在 `<svg>` 开头,再照着填坐标。估值几乎每次都需要反复调整,跳过这步反而更慢。
**数据图表(柱状图范式**
**散点图 / 装饰性点阵范式**
```python
W, H = 360, 260
origin_x, origin_y = 50, 216 # 左下角SVG Y 轴向下
cw, ch = 290, 184
data, y_max = [120, 160, 90], 200
bar_w = int(cw / len(data) * 0.62)
for i, v in enumerate(data):
cx = round(origin_x + (i + 0.5) * cw / len(data))
y = round(origin_y - v / y_max * ch)
print(f"bar-{i}: x={cx - bar_w//2} y={y} w={bar_w} h={round(origin_y - y)}")
points = [(12, 40), (28, 80), (45, 65)]
x_min, x_max, y_min, y_max = 0, 50, 0, 100
for i, (xv, yv) in enumerate(points):
x = round(origin_x + (xv - x_min) / (x_max - x_min) * cw)
y = round(origin_y - (yv - y_min) / (y_max - y_min) * ch)
print(f"point-{i}: cx={x} cy={y}")
```
折线图:`x = origin_x + i/(n-1)*cw``y = origin_y - (v-y_min)/(y_max-y_min)*ch`
**装饰性元素(等间距范式)**
```python
@@ -160,9 +163,9 @@ for i in range(n):
```python
# 每个元素登记 (x, y, w, h),含 stroke 外扩
elements = [
(10, 20, 80, 160), # bar-0
(107, 10, 80, 170), # bar-1
(204, 40, 80, 140), # bar-2
(10, 20, 80, 160), # item-0
(107, 10, 80, 170), # item-1
(204, 40, 80, 140), # item-2
(0, 0, 300, 1), # x-axis
]
@@ -261,7 +264,6 @@ print(f"whiteboard width={wb_w} height={wb_h}")
| 流程图 | `flowchart TD` / `flowchart LR` | 业务流程、决策树、工作流 |
| 时序图 | `sequenceDiagram` | 系统交互、API 调用链 |
| 甘特图 | `gantt` | 项目计划、里程碑 |
| 饼图 | `pie` | 占比数据 |
| 类图 | `classDiagram` | 对象关系、架构设计 |
| ER 图 | `erDiagram` | 数据库结构 |
| 状态图 | `stateDiagram-v2` | 状态机、生命周期 |
@@ -279,7 +281,6 @@ Mermaid 图表会自动撑满 whiteboard 区域。建议:
|---------|-----------|------------|
| 流程图5-8 节点) | 720-816 | 300-400 |
| 时序图3-5 参与者) | 720-816 | 320-420 |
| 饼图 | 500-600 | 300-360 |
| 甘特图 | 816 | 280-360 |
| 思维导图 | 816 | 380-480 |
@@ -307,7 +308,7 @@ Mermaid 语法包含 `[`、`>`、`-->`,不用 CDATA 直接写会破坏 XML 解
- [ ] 文字 `y` 坐标为 baseline 位置,最小值 ≥ font-size避免被裁切
**SVG 模式——视觉品质检查:**
- [ ] 坐标轴、网格线、数值标注齐全,没有"裸柱子"或"裸折线"
- [ ] 非原生数据视觉有必要的坐标轴、网格线、数值标注或分段说明,没有"裸点"或无解释色块
- [ ] 字号有层级:标题 > 数值 > 轴标签,非全部相同
- [ ] 单一数据系列用同一颜色,多系列用不同颜色且对比充足
- [ ] 轴标签与图表元素互不遮挡,留有足够空间

View File

@@ -67,7 +67,7 @@ lark-cli slides xml_presentation.slide create --as user --params '<json_params>'
</slide>
```
详细格式请参考 [xml-format-guide.md](xml-format-guide.md) 和 [xml-schema-quick-ref.md](xml-schema-quick-ref.md)。
详细格式请参考 [xml-schema-quick-ref.md](xml-schema-quick-ref.md)。
## 使用示例
@@ -188,7 +188,7 @@ lark-cli slides xml_presentation.slide create --as user \
4. **fill / border 写法**: 颜色填充使用 `<fill><fillColor color="..."/></fill>`,边框常用 `<border color="..." width="2"/>`
5. **插入位置**: 通过 `before_slide_id` 指定插入目标,而不是用 `position`
6. **JSON 转义**: 如果直接内联 XML需要正确转义双引号
7. **建议**: 先使用 `xml_presentations.get` 获取现有结构,再添加新页面
7. **建议**: 先使用 `slides +xml-get` 获取现有结构,再添加新页面
## 批量添加建议
@@ -214,7 +214,6 @@ done
## 相关命令
- [slides +create](lark-slides-create.md) - 创建空白 PPT
- [xml_presentations get](lark-slides-xml-presentations-get.md) - 读取 PPT 内容
- `slides +xml-get` - 读取 PPT 内容并保存到本地文件
- [xml_presentation.slide delete](lark-slides-xml-presentation-slide-delete.md) - 删除幻灯片页面
- [xml-format-guide.md](xml-format-guide.md) - XML 格式详细规范
- [xml-schema-quick-ref.md](xml-schema-quick-ref.md) - Schema 快速参考
- [xml-schema-quick-ref.md](xml-schema-quick-ref.md) - XML Schema 快速参考

View File

@@ -49,7 +49,10 @@ lark-cli slides xml_presentation.slide delete --as user --params '{
```bash
# 先读取 XML 内容,确认待删除页面
lark-cli slides xml_presentations get --as user --params '{"xml_presentation_id":"slides_example_presentation_id"}' | jq -r '.data.xml_presentation.content'
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"}'
@@ -119,5 +122,5 @@ done
## 相关命令
- [slides +create](lark-slides-create.md) - 创建空白 PPT
- [xml_presentations get](lark-slides-xml-presentations-get.md) - 读取 PPT 内容
- `slides +xml-get` - 读取 PPT 内容并保存到本地文件
- [xml_presentation.slide create](lark-slides-xml-presentation-slide-create.md) - 添加幻灯片页面

View File

@@ -106,5 +106,5 @@ lark-cli slides xml_presentation.slide get --as user --params '{
- [slides +replace-slide](lark-slides-replace-slide.md) — 块级替换 shortcut推荐
- [xml_presentation.slide replace](lark-slides-xml-presentation-slide-replace.md) — 底层 replace API 参考
- [xml_presentations get](lark-slides-xml-presentations-get.md) — 读整个 PPT
- `slides +xml-get` — 读整个 PPT 并保存到本地文件
- [lark-slides-edit-workflows.md](lark-slides-edit-workflows.md) — 读-改-写闭环

View File

@@ -4,7 +4,19 @@
读取飞书幻灯片PPT演示文稿的完整 XML 内容信息。
## 命令
> [!IMPORTANT]
> lark-slides 工作流需要读取全文 XML 时,优先使用 `slides +xml-get` shortcut把 XML 保存到本地文件,避免终端输出被截断。本页仅作为底层原生 API 参考。
推荐命令:
```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_presentations get --as user --params '<json_params>'
@@ -35,19 +47,30 @@ lark-cli slides xml_presentations get --as user --params '<json_params>'
### 基础示例
```bash
lark-cli slides xml_presentations get --as user --params '{"xml_presentation_id":"slides_example_presentation_id"}'
lark-cli slides +xml-get --as user \
--presentation "slides_example_presentation_id" \
--output .lark-slides/plan/slides_example_presentation_id/readback.xml \
--json
```
### 结合 jq 格式化输出
### 指定版本读取
```bash
lark-cli slides xml_presentations get --as user --params '{"xml_presentation_id":"slides_example_presentation_id"}' | jq -r '.data.xml_presentation.content'
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_presentations get --as user --params '{"xml_presentation_id":"slides_example_presentation_id"}' > presentation_data.json
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
```
## 返回值
@@ -86,10 +109,11 @@ lark-cli slides xml_presentations get --as user --params '{"xml_presentation_id"
## 注意事项
1. **执行前必做**: 使用 `lark-cli schema slides.xml_presentations.get` 查看最新的参数结构
2. 返回的 XML 在 `data.xml_presentation.content` 字段中
3. 如果只需要部分信息,可以使用 `jq` 等工具过滤返回结果
4. 建议将获取的 XML 保存为文件,便于后续编辑或备份
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` 保存文件
## 相关命令

View File

@@ -12,7 +12,9 @@
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 后用 `xml_presentations.get` 回读,核对页面数量、关键元素和 plan 到 XML 的对应关系。
7. 创建 PPT 后用 `slides +xml-get` 回读,核对页面数量、关键元素和 plan 到 XML 的对应关系。
用户上传的 PPTX 模板不能代替 plan它只影响 `theme_style`、页面流和局部布局骨架;即使基于上传模板改写,最终仍必须有 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`
## Plan Path
@@ -119,6 +121,34 @@ Each slide must include:
- `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, polyline, or whiteboard 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:
@@ -183,6 +213,7 @@ Use an object for one planned asset, an array for multiple real needs, or `asset
- `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`: concrete XML-native visual plan using shapes, labels, tables, whiteboard diagrams, or placeholder panels.
- `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`.
@@ -190,7 +221,7 @@ Good examples:
- `{"asset_type":"architecture_diagram","purpose":"Explain component relationships.","suggested_query":"service architecture diagram","fallback_if_missing":"Draw a component diagram with grouped boxes, connector arrows, and short labels."}`
- `{"asset_type":"logo","purpose":"Identify the customer context.","suggested_query":"customer logo","fallback_if_missing":"Use a text label in a small badge."}`
- `{"asset_type":"chart","purpose":"Show adoption trend.","suggested_query":"monthly adoption trend chart","fallback_if_missing":"Draw a simple trend line chart with axis labels and data points."}`
- `{"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
@@ -201,6 +232,7 @@ Before writing each slide XML, map the plan fields to concrete decisions:
- `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 shape-based 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:

File diff suppressed because one or more lines are too long

View File

@@ -1,226 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<presentation xmlns="http://www.larkoffice.com/sml/2.0" height="540" width="960">
<title>制造端智能升级</title>
<theme>
<textStyles>
<headline fontColor="rgb(255,255,255)" fontFamily="思源黑体" fontSize="36"/>
<sub-headline fontColor="rgb(229,231,235)" fontFamily="思源黑体" fontSize="20"/>
<body fontColor="rgb(229,231,235)" fontFamily="思源黑体" fontSize="16"/>
</textStyles>
</theme>
<slide>
<style>
<fill>
<fillColor color="linear-gradient(180deg, rgb(47, 79, 79) 0%, rgb(26, 26, 26) 100%)"/>
</fill>
</style>
<data>
<shape height="36" rotation="0" topLeftX="48" topLeftY="40" type="text" width="300">
<content>
<p>
<strong>
<span color="rgb(255, 215, 0)" fontFamily="黑体" fontSize="28">时代背景</span>
</strong>
</p>
</content>
</shape>
<shape height="200" rotation="0" topLeftX="48" topLeftY="85" type="rect" width="864">
<fill>
<fillColor color="rgba(0,0,0,0.2)"/>
</fill>
</shape>
<img alpha="0.4" alt="十月革命场景" height="180" rotation="0" src="https://example.com/images/scene-1.png" topLeftX="48" topLeftY="95" width="288">
<crop type="rect"/>
</img>
<img alpha="0.4" alt="列宁演讲油画" height="180" rotation="0" src="https://example.com/images/scene-2.png" topLeftX="336" topLeftY="95" width="288">
<crop type="rect"/>
</img>
<img alpha="0.4" alt="十月革命战斗场面" height="180" rotation="0" src="https://example.com/images/scene-3.png" topLeftX="624" topLeftY="95" width="288">
<crop type="rect"/>
</img>
<shape height="2" rotation="0" topLeftX="90" topLeftY="180" type="rect" width="780">
<fill>
<fillColor color="rgba(255, 215, 0, 0.3)"/>
</fill>
</shape>
<shape height="10" rotation="0" topLeftX="120" topLeftY="176" type="ellipse" width="10">
<fill>
<fillColor color="rgb(196, 30, 58)"/>
</fill>
<border color="rgb(255, 215, 0)" width="1"/>
</shape>
<shape height="24" rotation="0" topLeftX="60" topLeftY="140" type="text" width="200">
<content>
<p textAlign="center">
<strong>
<span color="rgb(255, 215, 0)" fontFamily="黑体" fontSize="20">1917</span>
</strong>
</p>
</content>
</shape>
<shape height="22" rotation="0" topLeftX="60" topLeftY="200" type="text" width="200">
<content>
<p textAlign="center">
<span color="rgb(230, 230, 230)" fontFamily="宋体" fontSize="16">十月革命</span>
</p>
</content>
</shape>
<shape height="40" rotation="0" topLeftX="60" topLeftY="225" type="text" width="200">
<content verticalAlign="top">
<p textAlign="center">
<span color="rgb(156, 163, 175)" fontSize="12">沙皇专制终结,苏维埃政权建立</span>
</p>
</content>
</shape>
<shape height="10" rotation="0" topLeftX="475" topLeftY="176" type="ellipse" width="10">
<fill>
<fillColor color="rgb(196, 30, 58)"/>
</fill>
<border color="rgb(255, 215, 0)" width="1"/>
</shape>
<shape height="24" rotation="0" topLeftX="380" topLeftY="140" type="text" width="200">
<content>
<p textAlign="center">
<strong>
<span color="rgb(255, 215, 0)" fontFamily="黑体" fontSize="20">1920s</span>
</strong>
</p>
</content>
</shape>
<shape height="22" rotation="0" topLeftX="380" topLeftY="200" type="text" width="200">
<content>
<p textAlign="center">
<span color="rgb(230, 230, 230)" fontFamily="宋体" fontSize="16">国内战争</span>
</p>
</content>
</shape>
<shape height="40" rotation="0" topLeftX="380" topLeftY="225" type="text" width="200">
<content verticalAlign="top">
<p textAlign="center">
<span color="rgb(156, 163, 175)" fontSize="12">革命与反革命的残酷斗争</span>
</p>
</content>
</shape>
<shape height="10" rotation="0" topLeftX="830" topLeftY="176" type="ellipse" width="10">
<fill>
<fillColor color="rgb(196, 30, 58)"/>
</fill>
<border color="rgb(255, 215, 0)" width="1"/>
</shape>
<shape height="24" rotation="0" topLeftX="740" topLeftY="140" type="text" width="200">
<content>
<p textAlign="center">
<strong>
<span color="rgb(255, 215, 0)" fontFamily="黑体" fontSize="20">1930s</span>
</strong>
</p>
</content>
</shape>
<shape height="22" rotation="0" topLeftX="740" topLeftY="200" type="text" width="200">
<content>
<p textAlign="center">
<span color="rgb(230, 230, 230)" fontFamily="宋体" fontSize="16">社会主义建设</span>
</p>
</content>
</shape>
<shape height="40" rotation="0" topLeftX="740" topLeftY="225" type="text" width="200">
<content verticalAlign="top">
<p textAlign="center">
<span color="rgb(156, 163, 175)" fontSize="12">新经济政策与工业化探索</span>
</p>
</content>
</shape>
<shape height="1" rotation="0" topLeftX="48" topLeftY="300" type="rect" width="864">
<fill>
<fillColor color="rgba(255, 215, 0, 0.2)"/>
</fill>
</shape>
<shape height="24" rotation="0" topLeftX="48" topLeftY="320" type="text" width="280">
<content>
<p>
<span color="rgb(230, 230, 230)" fontFamily="宋体" fontSize="18">作者:奥斯特洛夫斯基</span>
</p>
</content>
</shape>
<icon height="16" iconType="iconpark/Peoples/user.svg" topLeftX="52" topLeftY="360" width="16">
<fill>
<fillColor color="rgb(196, 30, 58)"/>
</fill>
</icon>
<shape height="20" topLeftX="76" topLeftY="358" type="text" width="260">
<content verticalAlign="middle">
<p>
<span color="rgb(209, 213, 219)" fontSize="13">工人家庭出身,投身革命浪潮</span>
</p>
</content>
</shape>
<icon height="16" iconType="iconpark/Sports/torch.svg" topLeftX="52" topLeftY="390" width="16">
<fill>
<fillColor color="rgb(196, 30, 58)"/>
</fill>
</icon>
<shape height="20" topLeftX="76" topLeftY="388" type="text" width="260">
<content verticalAlign="middle">
<p>
<span color="rgb(209, 213, 219)" fontSize="13">战场负伤致残,生命陷入黑暗</span>
</p>
</content>
</shape>
<icon height="16" iconType="iconpark/Health/first-aid-kit.svg" topLeftX="52" topLeftY="420" width="16">
<fill>
<fillColor color="rgb(196, 30, 58)"/>
</fill>
</icon>
<shape height="20" topLeftX="76" topLeftY="418" type="text" width="260">
<content verticalAlign="middle">
<p>
<span color="rgb(209, 213, 219)" fontSize="13">全身瘫痪、双目失明</span>
</p>
</content>
</shape>
<icon height="16" iconType="iconpark/Edit/edit.svg" topLeftX="52" topLeftY="450" width="16">
<fill>
<fillColor color="rgb(196, 30, 58)"/>
</fill>
</icon>
<shape height="20" topLeftX="76" topLeftY="448" type="text" width="260">
<content verticalAlign="middle">
<p>
<span color="rgb(209, 213, 219)" fontSize="13">以文学为武器,口述完成创作</span>
</p>
</content>
</shape>
<img alt="奥斯特洛夫斯基青年时期" height="213" rotation="0" src="https://example.com/images/ostrovsky.png" topLeftX="360" topLeftY="320" width="160">
<border color="rgba(255, 215, 0, 0.5)" width="2"/>
<crop type="rect"/>
</img>
<shape height="24" rotation="0" topLeftX="552" topLeftY="320" type="text" width="360">
<content>
<p>
<span color="rgb(230, 230, 230)" fontFamily="宋体" fontSize="18">创作动机</span>
</p>
</content>
</shape>
<shape height="16" rotation="0" topLeftX="552" topLeftY="350" type="rect" width="2">
<fill>
<fillColor color="rgb(196, 30, 58)"/>
</fill>
</shape>
<shape height="120" rotation="0" topLeftX="562" topLeftY="350" type="text" width="350">
<content lineSpacing="multiple:1.6" verticalAlign="top">
<p>
<span color="rgb(209, 213, 219)" fontSize="13">在双目失明、全身瘫痪的逆境中,奥斯特洛夫斯基以自身经历为蓝本,用顽强的意志口述完成了这部不朽巨著。他将文学创作视为生命的延续和战斗的武器,旨在通过保尔·柯察金的形象,向青年一代传递坚不可摧的革命信念和超越个人痛苦的崇高人生价值观。</span>
</p>
</content>
</shape>
</data>
<note>
<content>
<p>各位好,这一页将我们带回《钢铁是怎样炼成的》这部巨著诞生的波澜壮阔的时代。</p>
<p>上半部分展示了从1917年十月革命到1930年代苏联社会主义建设的宏大历史画卷。这是一个充满剧烈社会变革和残酷斗争的年代也是英雄主义和理想主义精神熊熊燃烧的年代。正是这样的背景孕育了小说的灵魂。</p>
<p>下半部分,我们聚焦于作者奥斯特洛夫斯基的个人经历。他的一生,本身就是一部比小说更震撼人心的传奇。从投身革命的青年,到因伤致残的战士,再到与命运抗争的文学巨匠。他的创作动机源于自身不屈的战斗精神,他希望用保尔的故事激励后人,在任何困境中都不要放弃理想,要将有限的生命投入到无限的为人类解放而斗争的事业中去。</p>
<p>通过了解这段历史和作者的生平,我们能更深刻地理解《钢铁是怎样炼成的》这部作品的伟大之处。</p>
</content>
</note>
</slide>
</presentation>

View File

@@ -3018,7 +3018,7 @@
子元素(mermaid 与 svg 二选一):
- mermaid: Mermaid 源码文本, 可使用 CDATA 包裹
适用场景: 流程图、时序图、思维导图、类图、甘特图、饼图等结构化图表
适用场景: 流程图、时序图、思维导图、类图、甘特图、ER 图、用户旅程等结构图
特点: 用简短的文本声明描述图表逻辑, 由渲染引擎自动布局, 无需手动计算坐标
示例: &lt;mermaid&gt;&lt;![CDATA[flowchart TD\n A[开始] --&gt; B[结束]]]&gt;&lt;/mermaid&gt;
- svg: SVG 内容

View File

@@ -16,7 +16,7 @@
遇到 `invalid param`、某一页创建失败、页面空白或布局错乱时,按顺序处理:
1. 记录 `xml_presentation_id`,不要假设失败代表什么都没创建。
2.`xml_presentations.get` 回读,确认是否已有部分页面写入。
2.`slides +xml-get` 回读,确认是否已有部分页面写入。
3. 检查失败页是否含未转义字符:`Q&A -> Q&amp;A`,文本 `<` / `>` 写成 `&lt;` / `&gt;`,属性 URL `a=1&b=2 -> a=1&amp;b=2`
4. 检查标签闭合、属性引号、`<content>` 结构,以及 `<slide>` 直接子元素。
5. 页面空白、溢出、重叠或越界时,按 [validation-checklist.md](validation-checklist.md) 运行 XML 文本重叠检查,并人工核对越界、截断、图文压盖等视觉风险;工具当前只会报告 `xml_not_well_formed` / `bbox_overlap`
@@ -46,14 +46,14 @@
| 400 XML 格式错误 | XML 语法错误 | 检查标签闭合、属性引号、特殊字符转义 |
| 400 请求包装错误 | `--data` 未按 schema 包装 | 检查是否传入 `xml_presentation.content``slide.content` |
| 创建成功但页面空白 / 内容缺失 / 布局错乱 | 常见于 `--slides '[...]'` 的 shell 转义或长参数传递问题 | 改用两步创建,并在创建后立即读取 XML 验证 |
| 403 权限不足 | 身份或 scope 不匹配 | 先检查是否误用了 bot 身份,再确认 scope 和文档权限 |
| 403 权限不足 | scope 或文档权限不匹配 | 确认 scope 和文档权限;无权限时根据错误响应引导用户解决 |
| 404 演示文稿不存在 | `xml_presentation_id` 不正确或无权限 | 检查 tokenwiki 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 | 当前身份对演示文稿无编辑权限 | 确认 user/bot 对目标 PPT 有编辑权限bot 常见于 PPT 非该 bot 创建 |
| 1061004 forbidden | 当前用户对演示文稿无编辑权限 | 确认当前用户对目标 PPT 有编辑权限 |
| 3350001 | XML 非 well-formed、XML 结构不符合服务端要求,或 replace 片段问题 | 优先检查未转义字符replace 场景再看 `block_id``<content/>` |
| 3350002 | `revision_id` 大于当前版本 | 用 `-1` 取当前版本,或重新 `xml_presentations.get` 取最新 `revision_id` |
| 3350002 | `revision_id` 大于当前版本 | 用 `-1` 取当前版本,或重新 `slides +xml-get` 取最新 `revision_id` |
| validation: unsafe file path | `--file` 给了绝对路径或上层路径 | `--file` 必须是 CWD 内相对路径;先 `cd` 到素材目录再执行 |
## Command-Specific References

View File

@@ -7,7 +7,7 @@
## Required Flow
1. 记录创建或编辑返回的 `xml_presentation_id`,以及已知的 `slide_id` / `revision_id`
2.`xml_presentations.get` 回读全文 XML。
2.`slides +xml-get` 回读全文 XML 到本地文件
3. 检查实际页数是否符合计划或用户要求。
4. 检查每页 `<data>` 内是否有预期主要元素。
5. 检查没有明显空白页、破损页、缺失标题或缺失主视觉。
@@ -19,13 +19,15 @@
回读命令:
```bash
lark-cli slides xml_presentations get --as user \
--params '{"xml_presentation_id":"YOUR_ID"}'
lark-cli slides +xml-get --as user \
--presentation "YOUR_ID" \
--output .lark-slides/plan/<deck-or-task-id>/readback.xml \
--json
```
## Automated XML Text Overlap Lint
回读 XML 保存到本地文件后,优先运行 XML 语法和文本重叠静态检查:
`slides +xml-get` 保存 XML 到本地文件后,优先运行 XML 语法和文本重叠静态检查:
```bash
python3 skills/lark-slides/scripts/xml_text_overlap_lint.py --input <presentation.xml>
@@ -44,6 +46,37 @@ python3 skills/lark-slides/scripts/xml_text_overlap_lint.py --input <presentatio
| `xml_not_well_formed` | XML 语法错误或文本未转义 | 修复标签闭合、属性引号、`&` / `<` / `>` 转义 |
| `bbox_overlap` | 文本元素的估算绘制区域明显重叠 | 拉开文本坐标、缩小文本框/字号,或改成明确的分栏/分组结构 |
## Screenshot QA
获取页面截图后,必须使用 subagent 做视觉验收;主 agent 不要只凭 XML 回读或静态 lint 结论声称截图验收通过。验收时假设页面存在问题,主动寻找并报告所有风险,包括轻微问题。
把截图路径、每页预期内容和以下中文提示交给 subagent
```text
请逐页目视检查这些幻灯片截图。先假设存在问题,并尽量找出它们。
重点检查:
- 元素重叠:文字与形状、图片或图表互相遮挡,线条穿过文字,卡片或标签堆叠。
- 文本溢出或被裁切:靠近页面边缘、文本框边界或卡片边界处被截断。
- 装饰元素位置错误:分割线、强调线或标签底板按单行文字布置,但标题或正文换行后压住文字或距离异常。
- 来源标注、页脚或页码与上方内容碰撞。
- 元素距离过近:相邻元素间距明显不足,卡片或分区几乎贴在一起;按 960x540 画布估算,小于约 25-30 px 的间隔通常要标记。
- 间距不均:局部留白过大,另一处过于拥挤。
- 页面边距不足:主体内容贴近幻灯片边缘;按 960x540 画布估算,小于约 45-50 px 的外边距通常要标记。
- 列、卡片、图标或同类元素没有稳定对齐。
- 图片、图表或 whiteboard 渲染异常:空白、变形、低清、关键内容不可读或预期图形缺失。
- 文本对比度不足,例如浅灰文字放在米色或浅色背景上。
- 图标对比度不足,例如深色图标放在深色背景上,且没有浅色圆形或底板承托。
- 文本框过窄,导致不必要的频繁换行。
- 残留占位符、模板默认文字或未替换内容。
对每一页分别列出发现的问题或可疑区域,即使只是轻微问题也要记录。
报告所有发现的问题,包括轻微问题。
```
subagent 返回后,主 agent 必须根据问题严重度决定是否修复:空白页、破图、文字遮挡、明显裁切、低对比不可读、占位符残留等必须先修复再交付;轻微间距或对齐问题如果不修复,最终验证记录要说明已知风险。
## Page Count And Structure
- 实际页数必须等于用户要求或 `slide_plan.json` 的页数。
@@ -101,7 +134,7 @@ python3 skills/lark-slides/scripts/xml_text_overlap_lint.py --input <presentatio
```text
验证记录:
- 回读:已执行 xml_presentations.get实际页数 N / 预期 N。
- 回读:已执行 slides +xml-get实际页数 N / 预期 N。
- 关键页:架构解释 / Self-Attention / 对比或演进 / 总结页均存在。
- 结构:检查了主要 shape/img/table/chart 元素,无明显空白页或破损页。
- 布局:检查了标题层级、主视觉、重叠/越界/文本溢出风险。

View File

@@ -2,7 +2,7 @@
新建演示文稿或大幅改写页面时,在 `slide_plan.json` 完成后、生成 XML 前读取本文件。目标是让 `layout_type``visual_focus``text_density` 变成实际页面几何,而不是只写在 plan 里。
默认画布按 `960 x 540` 规划。已有页面回读 XML 可以影响具体坐标,但不能覆盖这些原则:页面要有主视觉区域、文本要受密度约束、不同 `layout_type` 必须产生明显不同的坐标结构。
默认画布按 `960 x 540` 规划。模板 XML 可以覆盖具体坐标,但不能覆盖这些原则:页面要有主视觉区域、文本要受密度约束、不同 `layout_type` 必须产生明显不同的坐标结构。
## Core Rules
@@ -131,6 +131,7 @@ Purpose: make one metric or fact memorable.
Geometry:
- Reserve the largest object for the metric: font size often `64-110`, region at least `300 x 120`.
- Set `autoFit="normal-auto-fit"` on the metric's `<content>` so an oversized number shrinks to fit its box instead of overflowing.
- 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.
@@ -168,7 +169,7 @@ Text:
Purpose: explain components, dependencies, or system flow.
Implementation: prefer `<whiteboard>` (see `lark-slides-whiteboard.md`); use `<shape>` + `<line>` only as fallback.
Implementation: prefer Mermaid `<whiteboard>` (see `lark-slides-whiteboard.md`); use `<shape>` + `<line>` as fallback.
Geometry:
- Main visual area should be a diagram, not prose.
@@ -184,7 +185,7 @@ Text:
Purpose: show operational steps, workflow, or cause-effect path.
Implementation: prefer `<whiteboard>` (see `lark-slides-whiteboard.md`); use `<shape>` + `<line>` only as fallback.
Implementation: prefer Mermaid `<whiteboard>` (see `lark-slides-whiteboard.md`); use `<shape>` + `<line>` as fallback.
Geometry:
- Use numbered steps connected by arrows or lines.

View File

@@ -0,0 +1,46 @@
# 特殊视觉风格
当用户明确点名以下某种风格时,本文对应小节的规格**优先于** `visual-planning.md` 的默认审美设定;仍需遵守画布 960×540、留白、文字不溢出等硬约束。颜色一律用 `rgba(R,G,B,A)`,深浅背景都友好。
适用触发:用户说“咨询风 / 麦肯锡风 / BCG 风 / 战略咨询风 / 数据报告风”,或“豆包风 / doubao 风 / 豆包 PPT 那种”,或直接贴出属于这两类风格的参考图/链接。
---
## 咨询风McKinsey / BCG 战略咨询)
要求:一张专业、高密度的咨询演示幻灯片,设计风格需融合顶尖战略咨询公司(麦肯锡/BCG的风格与高端出版物的美学。
核心内容与布局:
- 丰富的数据可视化:幻灯片需填充复杂、精确的图表(堆叠柱状图、瀑布图或折线图)以及包含行和列的详细数据表。
- 结构化框架:包含战略图表或由细致、简洁线条构成的 2x2 矩阵。
- 高信息密度:布局精致且采用多栏设计,模仿真实的商业分析文稿,而非仅仅是一个空白的封面页。
视觉风格:
- 美学:科技极简主义,但信息承载量大。风格干净、锐利且具权威感。
- 字体:主标题使用衬线体(如 Times New Roman以营造高级财务报告的质感图表标签和数据数字使用简洁的无衬线体。
- 配色纯白背景。文字为清晰的黑色。图表和图形装饰使用深皇家蓝Deep Royal Blue和不同深浅的灰色来体现数据层级。
- 图形:表格使用极细的边框线,图表使用精确的矢量线条。
---
## 豆包风(豆包数据卡片)
要求:一张信息密度巨高的图文卡片化布局幻灯片,追求充实饱满、图文丰富、可逐行细读的版面。
核心内容与布局:
- **信息密度巨高(至关重要):除封面和分节页外,每个内容页都塞满多张图文卡片,宁可密而满,不要空而疏,不要留白。**
- 卡片化布局:方角卡片按多行网格铺满版面(网格数、图文比例按内容变化,避免每页雷同);每个卡片不是短标签,而是“标题 + 完整说明”,像浓缩的分析文稿。
- 卡片版式:卡片用窄条点缀(贴任意一边或不加),采用“锚点(语义图标或编号)+ 标题 + 细分隔线(可选)+ 正文”的结构。
- 文字卡片:内容页正文量级必须达到 400-600 中文字,分布在 4-12 个卡片,每个文字卡片都塞满文字内容,宁可密而满,不要空而疏。
- 图片卡片:内容页必须包含图片卡片,多数内容页目标 1-3 张,图片卡片与文字卡片组成网格,缺少图片卡片时必须用生图工具生成配图补足。
- 图标密集镶嵌:文字卡片、列表项和指标旁大量内嵌语义图标(部分放在圆形 / 方形底里)作为视觉锚点,让高密度文字也有图形节奏,而不是成片纯文字块。
- 数据驱动:数据页密集使用原生图表(饼图、环形图、柱状、折线、组合),图表常与卡片网格并排成半栏布局(如左图右卡或上表下卡)。
- 关键指标突出KPI 用比正文稍大的数字呈现,不要过大,下方配小字标签与简短解读。
- 防文本溢出:所有承载突出信息和密集文字的 `<content>` 必须设置 `autoFit="normal-auto-fit"`,字号会在框内自动缩排以防溢出。
视觉风格:
- 美学:干净、明亮、清爽但信息饱满;纯平无渐变,靠卡片、分隔线和对齐网格在高密度下维持秩序感。
- 字体:全篇以无衬线体(思源黑体)为主,封面或关键强调可少量使用衬线体。
- 字号:使用偏小的正文字号以容纳更多文字,提高信息密度。
- 配色控制配色数量避免色彩泛滥导致画面杂乱1 个主色1-2 个辅助色1 个强调色。

View File

@@ -1,369 +0,0 @@
# XML 格式指南
本文档基于 [slides_xml_schema_definition.xml](slides_xml_schema_definition.xml) 整理,说明飞书 Slides XML SchemaSML 2.0)的核心结构和常用写法。
## 基本结构
```xml
<?xml version="1.0" encoding="UTF-8"?>
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
<title>演示文稿标题</title>
<slide>
<style>
<fill>
<fillColor color="rgb(245, 245, 245)"/>
</fill>
</style>
<data>
<shape type="text" topLeftX="80" topLeftY="80" width="800" height="120">
<content textType="title">
<p>主标题</p>
</content>
</shape>
</data>
<note>
<content textType="body">
<p>这是演讲者备注。</p>
</content>
</note>
</slide>
</presentation>
```
## 根元素
### `<presentation>`
协议标准写法应带命名空间 `http://www.larkoffice.com/sml/2.0`;当前服务端实现可能兼容不带 `xmlns` 的输入,但不作为协议保证。
**属性:**
| 属性 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `width` | positiveInteger | 是 | 演示文稿宽度,如 `960` |
| `height` | positiveInteger | 是 | 演示文稿高度,如 `540` |
| `id` | string | 否 | 演示文稿标识 |
**子元素:**
| 元素 | 必需 | 说明 |
|------|------|------|
| `<title>` | 否 | 演示文稿标题 |
| `<theme>` | 否 | 全局主题 |
| `<slide>` | 是 | 幻灯片页面,至少 1 页,最多 100 页 |
## 主题
### `<theme>`
`<theme>` 当前包含两部分:
- `<background>`:演示文稿级背景填充
- `<textStyles>`:主题文本样式集合
`<textStyles>` 下可选子元素:
- `<title>`
- `<headline>`
- `<sub-headline>`
- `<body>`
- `<caption>`
这些元素定义的是主题默认样式,不是页面结构。常用属性:
| 属性 | 说明 |
|------|------|
| `fontFamily` | 字体 |
| `fontSize` | 字号 |
| `fontColor` | 字体颜色 |
## 幻灯片元素
### `<slide>`
单张幻灯片的结构比较严格。
**属性:**
| 属性 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `id` | string | 否 | 幻灯片标识 |
**直接子元素只有:**
| 元素 | 必需 | 说明 |
|------|------|------|
| `<style>` | 否 | 页面样式 |
| `<data>` | 否 | 页面元素容器 |
| `<note>` | 否 | 演讲者备注 |
这意味着 `<title>``<headline>``<body>``<caption>` 不能直接放在 `<slide>` 下。
## 文本内容模型
### `<content>`
实际页面文本通常通过 `<content>` 表达,常见位置有:
- `shape` 内部
- `table/td` 内部
- `note` 内部
**常用属性:**
| 属性 | 说明 |
|------|------|
| `textType` | `title` / `headline` / `sub-headline` / `body` / `caption` |
| `verticalAlign` | 垂直对齐 |
| `textAlign` | 水平对齐 |
| `lineSpacing` | 行间距 |
| `fontSize` | 字号 |
| `fontFamily` | 字体 |
| `color` | 字体颜色 |
| `bold` / `italic` / `underline` / `strikethrough` | 内容级样式 |
| `wrap` | 是否自动换行 |
**可包含的子元素:**
- `<p>`
- `<ul>`
- `<ol>`
### `<p>`
`<p>` 是段落元素,可混排纯文本和内联标签:
- `<br/>`
- `<strong>`
- `<em>`
- `<u>`
- `<span>`
- `<del>`
- `<a>`
- `<shadow>`
- `<outline>`
示例:
```xml
<content 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>` 中。
### `<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="700" topLeftY="120" width="180" height="120">
<fill>
<fillColor color="rgba(100, 149, 237, 0.25)"/>
</fill>
<border color="rgb(100, 149, 237)" width="2"/>
</shape>
```
**属性:**
| 属性 | 必需 | 说明 |
|------|------|------|
| `type` | 是 | 形状类型,`text` 表示文本框 |
| `topLeftX` | 是 | 左上角 X 坐标 |
| `topLeftY` | 是 | 左上角 Y 坐标 |
| `width` | 是 | 宽度 |
| `height` | 是 | 高度 |
| `rotation` | 否 | 旋转角度 |
| `flipX` / `flipY` | 否 | 翻转 |
| `alpha` | 否 | 透明度 |
**可选子元素:**
- `<fill>`
- `<border>`
- `<reflection>`
- `<shadow>`
- `<content>`
### `<line>`
```xml
<line startX="100" startY="200" endX="420" endY="200">
<border color="rgb(43, 47, 54)" width="2"/>
</line>
```
`line` 使用的是 `startX` / `startY` / `endX` / `endY`,不是 `x1` / `y1` / `x2` / `y2`
### `<img>`
```xml
<img src="file_token_or_url" topLeftX="100" topLeftY="220" width="320" height="180"/>
```
`img` 使用 `topLeftX` / `topLeftY`,不是 `x` / `y`
`src` 只接受两种值:
| `src` 形式 | 说明 |
|---|---|
| `file_token`(如 `boxcnXXXXXXXXXXXXXXXXXXXXXX` | 通过 `slides +media-upload` 上传后返回的 token |
| `@<本地路径>`(如 `@./assets/chart.png` | **仅在 `slides +create --slides` 中可用**CLI 会自动上传该文件并替换为 file_token |
> **禁止使用 http(s) 外链 URL**:飞书 slides 渲染端不会代理外链图片,`src="https://..."` 在 PPT 里通常显示破图。要用网图必须先 `curl`/下载到 CWD 内,再走上传流程拿 `file_token`。
本地图片的两种姿势:
- **新建带图 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
### `<icon>`
```xml
<icon iconType="iconpark/Base/setting.svg" topLeftX="440" topLeftY="220" width="32" height="32"/>
```
### `<table>`
表格结构为:
- `<table>`
- `<colgroup>` / `<tr>`
- `<tr>` 内为 `<td>`
- `<td>` 内可放 `<content>`
### `<chart>`
图表元素必须至少包含:
- `<chartPlotArea>`
- `<chartData>`
同时还可以包含:
- `<chartTitle>`
- `<chartSubTitle>`
- `<chartStyle>`
- `<chartLegend>`
- `<chartTooltip>`
如果要写图表 XML建议直接以 XSD 为准,不要自行发明更简化的 chart DSL。
## 样式元素
### `<fill>`
```xml
<fill>
<fillColor color="rgb(100, 149, 237)"/>
</fill>
```
### `<border>`
```xml
<border color="rgb(0, 0, 0)" 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%)"/>
```
## 演讲者备注
### `<note>`
```xml
<note>
<content 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` 颜色格式
5. 特殊字符按 XML 规则转义
6. 标准 16:9 页面建议使用 `width="960"``height="540"`
## 参考文档
- [xml-schema-quick-ref.md](xml-schema-quick-ref.md)
- [slides_xml_schema_definition.xml](slides_xml_schema_definition.xml)
- [examples.md](examples.md)
- [slides_demo.xml](slides_demo.xml)

View File

@@ -1,6 +1,6 @@
# XML Schema 快速参考
本文档是 [slides_xml_schema_definition.xml](slides_xml_schema_definition.xml) 的精简版摘要;如果两者不一致,以 XSD 原文为准。
本文档是 [slides_xml_schema_definition.xml](slides_xml_schema_definition.xml) 的精简版摘要,并合并了常用 XML 格式写法;如果两者不一致,以 XSD 原文为准。
## 最重要的规则
@@ -30,25 +30,31 @@
| 属性 | 必需 | 说明 |
|------|------|------|
| `width` | 是 | 演示文稿宽度,正整数 |
| `height` | 是 | 演示文稿高度,正整数 |
| `width` | 是 | 演示文稿宽度,正整数,标准 16:9 页面建议使用 `960` |
| `height` | 是 | 演示文稿高度,正整数,标准 16:9 页面建议使用 `540` |
| `id` | 否 | 演示文稿标识 |
**子元素:** `<title>?`, `<theme>?`, `<slide>+`
## slide 元素
| 属性 | 必需 | 说明 |
|------|------|------|
| `id` | 否 | 幻灯片标识 |
**子元素:**
- `<style>?` - 页面样式,目前可放 `<fill>`
- `<data>?` - 页面元素容器,可放 `shape``line``polyline``img``table``icon``chart``whiteboard``undefined`
- `<note>?` - 演讲者备注,内部可放 `<content>`
`<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>` 中,作为主题文本样式
@@ -64,6 +70,20 @@ XSD 中的 `title`、`headline`、`sub-headline`、`body`、`caption` 主要出
| `body` | 16 |
| `caption` | 12 |
## slide 元素
| 属性 | 必需 | 说明 |
|------|------|------|
| `id` | 否 | 幻灯片标识 |
**子元素:**
- `<style>?` - 页面样式,目前可放 `<fill>`
- `<data>?` - 页面元素容器,可放 `shape``line``polyline``img``table``icon``chart``whiteboard``undefined`
- `<note>?` - 演讲者备注,内部可放 `<content>`
这意味着 `<title>``<headline>``<body>``<caption>` 不能直接放在 `<slide>` 下。
## content 内容模型
`<content>` 可出现在 `shape``table/td``note` 中,常用属性包括:
@@ -71,12 +91,14 @@ XSD 中的 `title`、`headline`、`sub-headline`、`body`、`caption` 主要出
| 属性 | 说明 |
|------|------|
| `textType` | `title` / `headline` / `sub-headline` / `body` / `caption` |
| `verticalAlign` | 垂直对齐 |
| `textAlign` | 文本对齐方式 |
| `lineSpacing` | 行间距schema 默认 `multiple:1.5` |
| `fontSize` | 字号 |
| `fontFamily` | 字体 |
| `color` | 字体颜色 |
| `bold` / `italic` / `underline` / `strikethrough` | 文本样式 |
| `bold` / `italic` / `underline` / `strikethrough` | 内容级样式 |
| `wrap` | 是否自动换行 |
`<content>` 的子元素只能是:
@@ -84,7 +106,21 @@ XSD 中的 `title`、`headline`、`sub-headline`、`body`、`caption` 主要出
- `<ul>`
- `<ol>`
### content 示例
### p 段落与内联标签
`<p>` 是段落元素,可混排纯文本和内联标签:
- `<br/>`
- `<strong>`
- `<em>`
- `<u>`
- `<span>`
- `<del>`
- `<a>`
- `<shadow>`
- `<outline>`
示例:
```xml
<content textType="body" textAlign="left">
@@ -98,8 +134,20 @@ XSD 中的 `title`、`headline`、`sub-headline`、`body`、`caption` 主要出
## 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>
@@ -117,6 +165,16 @@ XSD 中的 `title`、`headline`、`sub-headline`、`body`、`caption` 主要出
| `width` | 是 | 宽度 |
| `height` | 是 | 高度 |
| `rotation` | 否 | 旋转角度 |
| `flipX` / `flipY` | 否 | 翻转 |
| `alpha` | 否 | 透明度 |
可选子元素:
- `<fill>`
- `<border>`
- `<reflection>`
- `<shadow>`
- `<content>`
### line
@@ -126,14 +184,23 @@ XSD 中的 `title`、`headline`、`sub-headline`、`body`、`caption` 主要出
</line>
```
`line` 使用的是 `startX` / `startY` / `endX` / `endY`,不是 `x1` / `y1` / `x2` / `y2`
### 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
@@ -144,10 +211,85 @@ XSD 中的 `title`、`headline`、`sub-headline`、`body`、`caption` 主要出
`iconType` 必须来自已验证的 IconPark 路径。需要语义图标时,先运行 `scripts/iconpark_tool.py search --query "<语义>"`,不要凭记忆拼路径。更多规则见 [iconpark.md](iconpark.md)。
### table
表格结构为:
- `<table>`
- `<colgroup>` / `<tr>`
- `<tr>` 内为 `<td>`
- `<td>` 内可放 `<content>`
### chart
图表元素必须至少包含:
- `<chartPlotArea>`
- `<chartData>`
同时还可以包含:
- `<chartTitle>`
- `<chartSubTitle>`
- `<chartStyle>`
- `<chartLegend>`
- `<chartTooltip>`
完整图表类型覆盖示例见 [slides_chart_demo.xml](slides_chart_demo.xml),其中包含柱状、条形、折线、面积、饼 / 环、雷达等原生 `<chart>` 示例,以及散点、气泡、漏斗、帕累托、瀑布等 `<whiteboard>` SVG 图表示例。
组合图示例:
```xml
<chart width="556" height="350" topLeftX="42" topLeftY="132">
<chartPlotArea>
<chartPlot type="combo">
<chartExtra/>
<chartSeriesList>
<chartSeries index="1" comboType="column"/>
<chartSeries index="2" comboType="line" yAxisPosition="right">
<chartTooltip format="0%"/>
</chartSeries>
</chartSeriesList>
</chartPlot>
<chartAxes>
<chartAxis type="x">
<chartLabel fontSize="10"/>
</chartAxis>
<chartAxis type="y" position="left">
<chartGridLine color="rgb(226, 232, 240)"/>
<chartLabel fontSize="10"/>
</chartAxis>
<chartAxis type="y" position="right">
<chartLabel fontSize="10" format="0%"/>
</chartAxis>
</chartAxes>
</chartPlotArea>
<chartLegend position="bottom" fontSize="11"/>
<chartData>
<dim1>
<chartField name="季度">24Q1,24Q2,24Q3,24Q4,25Q1,25Q2,25Q3,25Q4</chartField>
</dim1>
<dim2>
<chartField name="营收">180,195,210,245,220,238,258,296</chartField>
<chartField name="增速">0.08,0.12,0.15,0.18,0.22,0.22,0.23,0.21</chartField>
</dim2>
</chartData>
<chartTitle fontSize="12" color="rgba(15, 30, 58, 1)" bold="true">营收(亿美元, 左轴) · 同比增速(%, 右轴)</chartTitle>
<chartStyle>
<chartBackground color="rgba(0, 0, 0, 0)"/>
<chartBorder color="rgb(222, 224, 227)" width="0"/>
<chartColorTheme>
<color value="rgb(28, 71, 120)"/>
<color value="rgb(240, 129, 54)"/>
</chartColorTheme>
</chartStyle>
</chart>
```
### whiteboard
```xml
<!-- SVG 模式:数据图表、装饰元素 -->
<!-- SVG 模式:<chart> 不支持的图表或自定义视觉、装饰元素 -->
<whiteboard topLeftX="580" topLeftY="120" width="340" height="280">
<svg xmlns="http://www.w3.org/2000/svg">
<rect x="60" y="80" width="40" height="140" rx="3" fill="rgba(59,130,246,0.85)"/>
@@ -231,12 +373,69 @@ Mermaid 模式:内容用 `<![CDATA[...]]>` 包裹,避免 `[`、`>`、`-->`
</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)
- [xml-format-guide.md](xml-format-guide.md)
- [examples.md](examples.md)
- [slides_demo.xml](slides_demo.xml)
- [slides_chart_demo.xml](slides_chart_demo.xml)
## Schema 版本信息

View File

@@ -3,12 +3,16 @@
from __future__ import annotations
import unittest
from pathlib import Path
import xml_text_overlap_lint
TEMPLATES_DIR = Path(__file__).resolve().parents[1] / "assets" / "templates"
class XmlTextOverlapLintTest(unittest.TestCase):
def assertNoXmlTextOverlapLintIssues(self, result: dict, sample_name: str) -> None:
def assertNoXmlTextOverlapLintIssues(self, result: dict, template_path: Path) -> None:
issue_summaries = []
for slide in result.get("slides", []):
for issue in slide.get("issues", []):
@@ -21,64 +25,24 @@ class XmlTextOverlapLintTest(unittest.TestCase):
self.assertEqual(
result["summary"]["error_count"],
0,
f"{sample_name} has XML text overlap lint errors:\n" + "\n".join(issue_summaries),
f"{template_path.name} has XML text overlap lint errors:\n" + "\n".join(issue_summaries),
)
self.assertEqual(
result["summary"]["warning_count"],
0,
f"{sample_name} has XML text overlap lint warnings:\n" + "\n".join(issue_summaries),
f"{template_path.name} has XML text overlap lint warnings:\n" + "\n".join(issue_summaries),
)
def test_xml_text_overlap_lint_accepts_inline_fixture_xml_samples(self) -> None:
samples = {
"image-led-cover": """
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="http://www.larkoffice.com/sml/2.0">
<style><fill><fillColor color="rgb(15,23,42)"/></fill></style>
<data>
<img src="tok" topLeftX="560" topLeftY="0" width="400" height="540"/>
<shape type="text" topLeftX="64" topLeftY="150" width="420" height="70">
<content textType="title"><p><span fontSize="42">Quarterly Review</span></p></content>
</shape>
<shape type="text" topLeftX="64" topLeftY="235" width="420" height="36">
<content textType="sub-headline"><p><span fontSize="20">Focus, progress, and next steps</span></p></content>
</shape>
</data>
</slide>
</presentation>
""",
"content-grid": """
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="http://www.larkoffice.com/sml/2.0">
<data>
<shape type="text" topLeftX="60" topLeftY="44" width="620" height="46">
<content textType="title"><p><span fontSize="30">Execution Snapshot</span></p></content>
</shape>
<shape type="rect" topLeftX="60" topLeftY="126" width="250" height="150"/>
<shape type="text" topLeftX="84" topLeftY="152" width="200" height="36">
<content textType="headline"><p><span fontSize="22">Plan</span></p></content>
</shape>
<shape type="rect" topLeftX="355" topLeftY="126" width="250" height="150"/>
<shape type="text" topLeftX="379" topLeftY="152" width="200" height="36">
<content textType="headline"><p><span fontSize="22">Build</span></p></content>
</shape>
<shape type="rect" topLeftX="650" topLeftY="126" width="250" height="150"/>
<shape type="text" topLeftX="674" topLeftY="152" width="200" height="36">
<content textType="headline"><p><span fontSize="22">Launch</span></p></content>
</shape>
</data>
</slide>
</presentation>
""",
}
self.assertTrue(samples)
for sample_name, sample_xml in samples.items():
with self.subTest(sample=sample_name):
def test_xml_text_overlap_lint_accepts_all_template_xml_files(self) -> None:
template_paths = sorted(TEMPLATES_DIR.glob("*.xml"))
self.assertTrue(template_paths)
for template_path in template_paths:
with self.subTest(template=template_path.name):
result = xml_text_overlap_lint.lint_xml(
sample_xml,
sample_name,
template_path.read_text(encoding="utf-8"),
str(template_path),
)
self.assertNoXmlTextOverlapLintIssues(result, sample_name)
self.assertNoXmlTextOverlapLintIssues(result, template_path)
def test_lint_xml_reports_unescaped_ampersand_in_text(self) -> None:
result = xml_text_overlap_lint.lint_xml(