@amaster.ai/pi-lark 0.1.2-beta.52 → 0.1.2-beta.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/skills/lark-apps/SKILL.md +39 -6
- package/skills/lark-apps/references/lark-apps-cloud-dev.md +5 -4
- package/skills/lark-apps/references/lark-apps-create.md +6 -3
- package/skills/lark-apps/references/lark-apps-get.md +1 -1
- package/skills/lark-apps/references/lark-apps-list.md +1 -1
- package/skills/lark-apps/references/lark-apps-local-dev.md +27 -1
- package/skills/lark-apps/references/lark-apps-release-create.md +1 -1
- package/skills/lark-base/SKILL.md +4 -3
- package/skills/lark-base/references/lark-base-data-query-guide.md +8 -0
- package/skills/lark-base/references/lark-base-field-create.md +19 -8
- package/skills/lark-base/references/lark-base-field-json.md +3 -2
- package/skills/lark-doc/SKILL.md +25 -61
- package/skills/lark-doc/references/genres/business-analysis.md +30 -0
- package/skills/lark-doc/references/genres/data-report.md +32 -0
- package/skills/lark-doc/references/genres/email.md +38 -0
- package/skills/lark-doc/references/genres/execution-plan.md +27 -0
- package/skills/lark-doc/references/genres/formal-doc.md +37 -0
- package/skills/lark-doc/references/genres/meeting-minutes.md +24 -0
- package/skills/lark-doc/references/genres/memo-brief.md +25 -0
- package/skills/lark-doc/references/genres/official-redhead.md +73 -0
- package/skills/lark-doc/references/genres/prd.md +26 -0
- package/skills/lark-doc/references/genres/proposal.md +24 -0
- package/skills/lark-doc/references/genres/research-report.md +32 -0
- package/skills/lark-doc/references/genres/retrospective.md +25 -0
- package/skills/lark-doc/references/genres/route-consumer.md +37 -0
- package/skills/lark-doc/references/genres/route-creative.md +36 -0
- package/skills/lark-doc/references/genres/route-knowledge.md +39 -0
- package/skills/lark-doc/references/genres/route-marketing.md +40 -0
- package/skills/lark-doc/references/genres/route-media.md +36 -0
- package/skills/lark-doc/references/genres/route-opinion.md +38 -0
- package/skills/lark-doc/references/genres/route-personal-brand.md +36 -0
- package/skills/lark-doc/references/genres/route-platform.md +9 -0
- package/skills/lark-doc/references/genres/route-report.md +10 -0
- package/skills/lark-doc/references/genres/route-workplace.md +17 -0
- package/skills/lark-doc/references/genres/sop-tutorial.md +41 -0
- package/skills/lark-doc/references/genres/technical-doc.md +39 -0
- package/skills/lark-doc/references/genres/wechat.md +39 -0
- package/skills/lark-doc/references/genres/weekly-report.md +24 -0
- package/skills/lark-doc/references/genres/white-paper.md +32 -0
- package/skills/lark-doc/references/genres/xiaohongshu.md +38 -0
- package/skills/lark-doc/references/lark-doc-create-workflow.md +121 -0
- package/skills/lark-doc/references/lark-doc-create.md +22 -48
- package/skills/lark-doc/references/lark-doc-fetch.md +75 -92
- package/skills/lark-doc/references/lark-doc-history.md +3 -1
- package/skills/lark-doc/references/lark-doc-md.md +5 -1
- package/skills/lark-doc/references/lark-doc-script.md +76 -0
- package/skills/lark-doc/references/lark-doc-update.md +70 -222
- package/skills/lark-doc/references/lark-doc-whiteboard.md +5 -9
- package/skills/lark-doc/references/lark-doc-xml-extended-blocks.md +17 -12
- package/skills/lark-doc/references/lark-doc-xml.md +38 -167
- package/skills/lark-drive/SKILL.md +7 -5
- package/skills/lark-drive/references/lark-drive-copy.md +87 -0
- package/skills/lark-drive/references/lark-drive-update-title.md +78 -0
- package/skills/lark-im/SKILL.md +3 -3
- package/skills/lark-im/references/lark-im-message-enrichment.md +1 -1
- package/skills/lark-im/references/lark-im-messages-resources-download.md +19 -25
- package/skills/lark-im/references/lark-im-messages-search.md +1 -3
- package/skills/lark-sheets/SKILL.md +83 -82
- package/skills/lark-sheets/references/lark-sheets-batch-update.md +13 -58
- package/skills/lark-sheets/references/lark-sheets-chart.md +2 -1
- package/skills/lark-sheets/references/lark-sheets-conditional-format.md +1 -1
- package/skills/lark-sheets/references/lark-sheets-range-operations.md +5 -5
- package/skills/lark-sheets/references/lark-sheets-read-data.md +80 -6
- package/skills/lark-sheets/references/lark-sheets-sheet-structure.md +21 -10
- package/skills/lark-sheets/references/lark-sheets-styles-put.md +93 -0
- package/skills/lark-sheets/references/lark-sheets-visual-standards.md +2 -2
- package/skills/lark-sheets/references/lark-sheets-workbook.md +4 -3
- package/skills/lark-sheets/references/lark-sheets-write-cells.md +40 -12
- package/skills/lark-sheets/scripts/lark_detect_subtables.py +593 -0
- package/skills/lark-sheets/scripts/lark_inspect_workbook.py +188 -0
- package/skills/lark-sheets/scripts/lark_profile_table.py +614 -0
- package/skills/lark-sheets/scripts/lark_sheet_range.py +176 -0
- package/skills/lark-sheets/scripts/lark_sheet_read_cli.py +184 -0
- package/skills/lark-sheets/scripts/sheets_df.py +21 -3
- package/skills/lark-slides/SKILL.md +11 -13
- package/skills/lark-slides/references/lark-slides-create.md +70 -39
- package/skills/lark-slides/references/lark-slides-edit-workflows.md +4 -7
- package/skills/lark-slides/references/lark-slides-update-slide.md +146 -0
- package/skills/lark-slides/references/lark-slides-xml-presentations-get.md +26 -3
- package/skills/lark-slides/references/slides_chart_demo.xml +0 -1
- package/skills/lark-slides/references/troubleshooting.md +6 -6
- package/skills/lark-slides/references/validation-checklist.md +1 -1
- package/skills/lark-slides/references/xml-schema-quick-ref.md +0 -2
- package/skills/lark-whiteboard/SKILL.md +15 -8
- package/skills/lark-whiteboard/references/lark-whiteboard-export.md +4 -3
- package/skills/lark-whiteboard/references/lark-whiteboard-update.md +4 -4
- package/skills/lark-whiteboard/references/lark-whiteboard-workflow.md +19 -17
- package/skills/lark-whiteboard/routes/dsl.md +8 -2
- package/skills/lark-whiteboard/routes/mermaid.md +1 -1
- package/skills/lark-whiteboard/routes/svg-edit.md +5 -2
- package/skills/lark-whiteboard/routes/svg.md +3 -1
- package/skills/lark-whiteboard/scenes/mention.md +71 -0
- package/skills/lark-doc/references/lark-doc-word-stat.md +0 -93
- package/skills/lark-doc/references/style/lark-doc-create-workflow.md +0 -47
- package/skills/lark-doc/references/style/lark-doc-style.md +0 -68
- package/skills/lark-doc/references/style/lark-doc-update-workflow.md +0 -48
- package/skills/lark-doc/scripts/doc_word_stat.py +0 -1243
- package/skills/lark-slides/references/lark-slides-replace-pages.md +0 -97
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Range and coordinate helpers for Lark Sheet scripts."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
CELL_RE = re.compile(r"^\$?([A-Za-z]+)\$?([1-9][0-9]*)$")
|
|
11
|
+
ROW_RE = re.compile(r"^\$?([1-9][0-9]*)$")
|
|
12
|
+
COLUMN_RE = re.compile(r"^\$?([A-Za-z]+)$")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class RangeBounds:
|
|
17
|
+
start_row: int
|
|
18
|
+
start_col: int
|
|
19
|
+
end_row: int
|
|
20
|
+
end_col: int
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def row_count(self) -> int:
|
|
24
|
+
return self.end_row - self.start_row + 1
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def col_count(self) -> int:
|
|
28
|
+
return self.end_col - self.start_col + 1
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def col_to_index(col: str) -> int:
|
|
32
|
+
value = 0
|
|
33
|
+
for char in col.strip().upper():
|
|
34
|
+
if not ("A" <= char <= "Z"):
|
|
35
|
+
raise ValueError(f"Invalid column: {col}")
|
|
36
|
+
value = value * 26 + (ord(char) - ord("A") + 1)
|
|
37
|
+
if value <= 0:
|
|
38
|
+
raise ValueError(f"Invalid column: {col}")
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def index_to_col(index: int) -> str:
|
|
43
|
+
if index < 1:
|
|
44
|
+
raise ValueError(f"Column index must be >= 1: {index}")
|
|
45
|
+
chars = []
|
|
46
|
+
n = index
|
|
47
|
+
while n:
|
|
48
|
+
n, rem = divmod(n - 1, 26)
|
|
49
|
+
chars.append(chr(ord("A") + rem))
|
|
50
|
+
return "".join(reversed(chars))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def parse_cell(cell_ref: str) -> tuple[int, int]:
|
|
54
|
+
match = CELL_RE.match(cell_ref.strip())
|
|
55
|
+
if not match:
|
|
56
|
+
raise ValueError(f"Invalid cell reference: {cell_ref}")
|
|
57
|
+
col, row = match.groups()
|
|
58
|
+
return int(row), col_to_index(col)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _parse_endpoint(endpoint: str) -> tuple[str, int, int] | tuple[str, int]:
|
|
62
|
+
cell = CELL_RE.match(endpoint)
|
|
63
|
+
if cell:
|
|
64
|
+
col, row = cell.groups()
|
|
65
|
+
return "cell", int(row), col_to_index(col)
|
|
66
|
+
row = ROW_RE.match(endpoint)
|
|
67
|
+
if row:
|
|
68
|
+
return "row", int(row.group(1))
|
|
69
|
+
column = COLUMN_RE.match(endpoint)
|
|
70
|
+
if column:
|
|
71
|
+
return "column", col_to_index(column.group(1))
|
|
72
|
+
raise ValueError(f"Invalid A1 range endpoint: {endpoint}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_range(
|
|
76
|
+
range_ref: str,
|
|
77
|
+
*,
|
|
78
|
+
max_row: int | None = None,
|
|
79
|
+
max_col: int | None = None,
|
|
80
|
+
) -> RangeBounds:
|
|
81
|
+
"""Parse the A1 range forms accepted by ``+csv-get``.
|
|
82
|
+
|
|
83
|
+
Open-ended forms need the caller's actual returned grid dimensions. This
|
|
84
|
+
keeps generated ranges finite without guessing a spreadsheet-wide limit.
|
|
85
|
+
"""
|
|
86
|
+
ref = range_ref.strip()
|
|
87
|
+
if "!" in ref:
|
|
88
|
+
_, ref = ref.rsplit("!", 1)
|
|
89
|
+
if not ref:
|
|
90
|
+
raise ValueError(f"Invalid A1 range: {range_ref}")
|
|
91
|
+
parts = ref.split(":")
|
|
92
|
+
if len(parts) > 2:
|
|
93
|
+
raise ValueError(f"Invalid A1 range: {range_ref}")
|
|
94
|
+
start = _parse_endpoint(parts[0])
|
|
95
|
+
end = _parse_endpoint(parts[-1])
|
|
96
|
+
|
|
97
|
+
if len(parts) == 1:
|
|
98
|
+
if start[0] != "cell":
|
|
99
|
+
raise ValueError(f"A1 range must include a cell or ':' separator: {range_ref}")
|
|
100
|
+
_, row, col = start
|
|
101
|
+
return RangeBounds(row, col, row, col)
|
|
102
|
+
|
|
103
|
+
if start[0] == end[0] == "cell":
|
|
104
|
+
_, start_row, start_col = start
|
|
105
|
+
_, end_row, end_col = end
|
|
106
|
+
elif start[0] == end[0] == "row":
|
|
107
|
+
_, start_row = start
|
|
108
|
+
_, end_row = end
|
|
109
|
+
start_col = 1
|
|
110
|
+
if max_col is None:
|
|
111
|
+
raise ValueError(f"Range needs a maximum column: {range_ref}")
|
|
112
|
+
end_col = max_col
|
|
113
|
+
elif start[0] == end[0] == "column":
|
|
114
|
+
_, start_col = start
|
|
115
|
+
_, end_col = end
|
|
116
|
+
start_row = 1
|
|
117
|
+
if max_row is None:
|
|
118
|
+
raise ValueError(f"Range needs a maximum row: {range_ref}")
|
|
119
|
+
end_row = max_row
|
|
120
|
+
elif start[0] == "cell" and end[0] == "column":
|
|
121
|
+
_, start_row, start_col = start
|
|
122
|
+
_, end_col = end
|
|
123
|
+
if max_row is None:
|
|
124
|
+
raise ValueError(f"Range needs a maximum row: {range_ref}")
|
|
125
|
+
end_row = max(start_row, max_row)
|
|
126
|
+
elif start[0] == "cell" and end[0] == "row":
|
|
127
|
+
_, start_row, start_col = start
|
|
128
|
+
_, end_row = end
|
|
129
|
+
if max_col is None:
|
|
130
|
+
raise ValueError(f"Range needs a maximum column: {range_ref}")
|
|
131
|
+
end_col = max(start_col, max_col)
|
|
132
|
+
else:
|
|
133
|
+
raise ValueError(f"Invalid A1 range: {range_ref}")
|
|
134
|
+
return RangeBounds(min(start_row, end_row), min(start_col, end_col), max(start_row, end_row), max(start_col, end_col))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def format_cell(row: int, col: int) -> str:
|
|
138
|
+
if row < 1:
|
|
139
|
+
raise ValueError(f"Row must be >= 1: {row}")
|
|
140
|
+
return f"{index_to_col(col)}{row}"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def format_range(start_row: int, start_col: int, end_row: int, end_col: int) -> str:
|
|
144
|
+
bounds = RangeBounds(
|
|
145
|
+
min(start_row, end_row),
|
|
146
|
+
min(start_col, end_col),
|
|
147
|
+
max(start_row, end_row),
|
|
148
|
+
max(start_col, end_col),
|
|
149
|
+
)
|
|
150
|
+
start = format_cell(bounds.start_row, bounds.start_col)
|
|
151
|
+
end = format_cell(bounds.end_row, bounds.end_col)
|
|
152
|
+
return start if start == end else f"{start}:{end}"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def iter_cells(bounds: RangeBounds):
|
|
156
|
+
for row in range(bounds.start_row, bounds.end_row + 1):
|
|
157
|
+
for col in range(bounds.start_col, bounds.end_col + 1):
|
|
158
|
+
yield row, col
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def ranges_intersect(a: RangeBounds, b: RangeBounds) -> bool:
|
|
162
|
+
return not (
|
|
163
|
+
a.end_row < b.start_row
|
|
164
|
+
or b.end_row < a.start_row
|
|
165
|
+
or a.end_col < b.start_col
|
|
166
|
+
or b.end_col < a.start_col
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def range_union(a: RangeBounds, b: RangeBounds) -> RangeBounds:
|
|
171
|
+
return RangeBounds(
|
|
172
|
+
min(a.start_row, b.start_row),
|
|
173
|
+
min(a.start_col, b.start_col),
|
|
174
|
+
max(a.end_row, b.end_row),
|
|
175
|
+
max(a.end_col, b.end_col),
|
|
176
|
+
)
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Read-only Lark Sheet subset wrapper for the helper scripts."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LarkCliError(RuntimeError):
|
|
14
|
+
def __init__(self, message: str, *, cmd: list[str] | None = None):
|
|
15
|
+
super().__init__(message)
|
|
16
|
+
self.cmd = cmd or []
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def add_spreadsheet_args(
|
|
20
|
+
parser,
|
|
21
|
+
*,
|
|
22
|
+
require_sheet: bool = False,
|
|
23
|
+
allow_sheet: bool = True,
|
|
24
|
+
) -> None:
|
|
25
|
+
spreadsheet = parser.add_mutually_exclusive_group(required=True)
|
|
26
|
+
spreadsheet.add_argument("--url")
|
|
27
|
+
spreadsheet.add_argument("--spreadsheet-token")
|
|
28
|
+
if allow_sheet:
|
|
29
|
+
sheet = parser.add_mutually_exclusive_group(required=require_sheet)
|
|
30
|
+
sheet.add_argument("--sheet-id")
|
|
31
|
+
sheet.add_argument("--sheet-name")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _append_flag(cmd: list[str], name: str, value: Any) -> None:
|
|
35
|
+
flag = f"--{name.replace('_', '-')}"
|
|
36
|
+
if value is None:
|
|
37
|
+
return
|
|
38
|
+
if isinstance(value, bool):
|
|
39
|
+
cmd.append(flag if value else f"{flag}=false")
|
|
40
|
+
return
|
|
41
|
+
cmd.extend([flag, str(value)])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def run_sheets(
|
|
45
|
+
shortcut: str,
|
|
46
|
+
*,
|
|
47
|
+
url: str | None = None,
|
|
48
|
+
spreadsheet_token: str | None = None,
|
|
49
|
+
sheet_id: str | None = None,
|
|
50
|
+
sheet_name: str | None = None,
|
|
51
|
+
flags: dict[str, Any] | None = None,
|
|
52
|
+
timeout: int = 60,
|
|
53
|
+
) -> dict[str, Any]:
|
|
54
|
+
if bool(url) == bool(spreadsheet_token):
|
|
55
|
+
raise LarkCliError("Pass exactly one of --url or --spreadsheet-token")
|
|
56
|
+
if sheet_id and sheet_name:
|
|
57
|
+
raise LarkCliError("Pass only one of --sheet-id or --sheet-name")
|
|
58
|
+
|
|
59
|
+
cmd = ["lark-cli", "sheets", shortcut]
|
|
60
|
+
_append_flag(cmd, "url", url)
|
|
61
|
+
_append_flag(cmd, "spreadsheet_token", spreadsheet_token)
|
|
62
|
+
_append_flag(cmd, "sheet_id", sheet_id)
|
|
63
|
+
_append_flag(cmd, "sheet_name", sheet_name)
|
|
64
|
+
for key, value in (flags or {}).items():
|
|
65
|
+
_append_flag(cmd, key, value)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
completed = subprocess.run(
|
|
69
|
+
cmd,
|
|
70
|
+
capture_output=True,
|
|
71
|
+
text=True,
|
|
72
|
+
timeout=timeout,
|
|
73
|
+
check=False,
|
|
74
|
+
)
|
|
75
|
+
except FileNotFoundError as exc:
|
|
76
|
+
raise LarkCliError("lark-cli not found", cmd=cmd) from exc
|
|
77
|
+
except subprocess.TimeoutExpired as exc:
|
|
78
|
+
raise LarkCliError(f"lark-cli timed out after {timeout}s", cmd=cmd) from exc
|
|
79
|
+
|
|
80
|
+
if completed.returncode != 0:
|
|
81
|
+
detail = (completed.stderr or completed.stdout or "").strip()
|
|
82
|
+
raise LarkCliError(detail or f"lark-cli exited with {completed.returncode}", cmd=cmd)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
envelope = json.loads(completed.stdout)
|
|
86
|
+
except json.JSONDecodeError as exc:
|
|
87
|
+
snippet = completed.stdout[:500].replace("\n", "\\n")
|
|
88
|
+
raise LarkCliError(f"lark-cli stdout was not JSON: {snippet}", cmd=cmd) from exc
|
|
89
|
+
|
|
90
|
+
if isinstance(envelope, dict) and envelope.get("ok") is False:
|
|
91
|
+
raise LarkCliError(json.dumps(envelope, ensure_ascii=False), cmd=cmd)
|
|
92
|
+
if not isinstance(envelope, dict):
|
|
93
|
+
raise LarkCliError("lark-cli returned a non-object JSON payload", cmd=cmd)
|
|
94
|
+
return envelope
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def envelope_data(envelope: dict[str, Any]) -> dict[str, Any]:
|
|
98
|
+
data = envelope.get("data")
|
|
99
|
+
return data if isinstance(data, dict) else envelope
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def emit_success(action: str, data: dict[str, Any], warnings: list[str] | None = None) -> None:
|
|
103
|
+
print(
|
|
104
|
+
json.dumps(
|
|
105
|
+
{
|
|
106
|
+
"ok": True,
|
|
107
|
+
"engine": "lark",
|
|
108
|
+
"action": action,
|
|
109
|
+
"data": data,
|
|
110
|
+
"warnings": warnings or [],
|
|
111
|
+
},
|
|
112
|
+
ensure_ascii=False,
|
|
113
|
+
indent=2,
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def emit_error(action: str, message: str, warnings: list[str] | None = None) -> None:
|
|
119
|
+
print(
|
|
120
|
+
json.dumps(
|
|
121
|
+
{
|
|
122
|
+
"ok": False,
|
|
123
|
+
"engine": "lark",
|
|
124
|
+
"action": action,
|
|
125
|
+
"error": message,
|
|
126
|
+
"warnings": warnings or [],
|
|
127
|
+
},
|
|
128
|
+
ensure_ascii=False,
|
|
129
|
+
indent=2,
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
sys.exit(1)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def sheet_title(sheet: dict[str, Any]) -> str:
|
|
136
|
+
return str(sheet.get("title") or sheet.get("sheet_name") or sheet.get("name") or "")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def sheet_identifier(sheet: dict[str, Any]) -> str:
|
|
140
|
+
return str(sheet.get("sheet_id") or sheet.get("id") or "")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def sheet_locator(sheet: dict[str, Any]) -> dict[str, str]:
|
|
144
|
+
sid = sheet_identifier(sheet)
|
|
145
|
+
if sid:
|
|
146
|
+
return {"sheet_id": sid}
|
|
147
|
+
title = sheet_title(sheet)
|
|
148
|
+
if title:
|
|
149
|
+
return {"sheet_name": title}
|
|
150
|
+
return {}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def extract_sheets(workbook_data: dict[str, Any]) -> list[dict[str, Any]]:
|
|
154
|
+
sheets = workbook_data.get("sheets")
|
|
155
|
+
if isinstance(sheets, list):
|
|
156
|
+
return [sheet for sheet in sheets if isinstance(sheet, dict)]
|
|
157
|
+
workbook = workbook_data.get("workbook")
|
|
158
|
+
if isinstance(workbook, dict) and isinstance(workbook.get("sheets"), list):
|
|
159
|
+
return [sheet for sheet in workbook["sheets"] if isinstance(sheet, dict)]
|
|
160
|
+
return []
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def resolve_target_sheets(
|
|
164
|
+
workbook_data: dict[str, Any],
|
|
165
|
+
*,
|
|
166
|
+
sheet_id: str | None = None,
|
|
167
|
+
sheet_name: str | None = None,
|
|
168
|
+
require_one: bool = False,
|
|
169
|
+
) -> list[dict[str, Any]]:
|
|
170
|
+
sheets = extract_sheets(workbook_data)
|
|
171
|
+
if sheet_id:
|
|
172
|
+
matches = [sheet for sheet in sheets if sheet_identifier(sheet) == sheet_id]
|
|
173
|
+
elif sheet_name:
|
|
174
|
+
matches = [sheet for sheet in sheets if sheet_title(sheet) == sheet_name]
|
|
175
|
+
else:
|
|
176
|
+
matches = sheets
|
|
177
|
+
|
|
178
|
+
if require_one:
|
|
179
|
+
if len(matches) == 1:
|
|
180
|
+
return matches
|
|
181
|
+
if not matches:
|
|
182
|
+
raise LarkCliError("No matching sheet found")
|
|
183
|
+
raise LarkCliError("Multiple sheets matched; pass --sheet-id or --sheet-name")
|
|
184
|
+
return matches
|
|
@@ -19,11 +19,29 @@ import pandas as pd
|
|
|
19
19
|
|
|
20
20
|
def df_to_sheet(df, name, formats=None):
|
|
21
21
|
"""Pack one DataFrame into one entry of a `+table-put --sheets` payload."""
|
|
22
|
+
packed = json.loads(df.to_json(orient="split", date_format="iso"))
|
|
23
|
+
# The protocol requires string column names. pandas keeps integer labels
|
|
24
|
+
# (e.g. the default RangeIndex columns 0/1/2) as JSON numbers, while the
|
|
25
|
+
# dtypes dict keys get stringified during JSON serialization — the CLI
|
|
26
|
+
# then rejects `columns` ("cannot unmarshal number into … type string")
|
|
27
|
+
# and the dtype lookup would miss anyway. Stringify every key once, and
|
|
28
|
+
# refuse to continue when that conversion silently merges two columns.
|
|
29
|
+
normalized_labels = [str(c) for c in df.columns]
|
|
30
|
+
columns = [str(c) for c in packed["columns"]]
|
|
31
|
+
if normalized_labels != columns:
|
|
32
|
+
columns = normalized_labels
|
|
33
|
+
if len(set(columns)) != len(columns):
|
|
34
|
+
raise ValueError(
|
|
35
|
+
"column labels collide after str() conversion; "
|
|
36
|
+
"rename the DataFrame columns before packing"
|
|
37
|
+
)
|
|
38
|
+
packed["columns"] = columns
|
|
39
|
+
dtype_values = list(df.dtypes)
|
|
22
40
|
return {
|
|
23
41
|
"name": name,
|
|
24
|
-
**
|
|
25
|
-
"dtypes":
|
|
26
|
-
**({"formats": formats} if formats else {}),
|
|
42
|
+
**packed,
|
|
43
|
+
"dtypes": {key: str(dtype) for key, dtype in zip(columns, dtype_values)},
|
|
44
|
+
**({"formats": {str(k): v for k, v in formats.items()}} if formats else {}),
|
|
27
45
|
}
|
|
28
46
|
|
|
29
47
|
|
|
@@ -79,10 +79,10 @@ metadata:
|
|
|
79
79
|
|
|
80
80
|
| 用户需求 | 优先动作 | 关键文档 / 命令 |
|
|
81
81
|
|----------|----------|-----------------|
|
|
82
|
-
| 新建 PPT | 先规划 `slide_plan.json
|
|
82
|
+
| 新建 PPT | 先规划 `slide_plan.json`,再按页数选择一步或两步创建 | `planning-layer.md`、`visual-planning.md`、`asset-planning.md`、`lark-slides-create.md`、`slides +create`、`slides +add-slide`、`lark-slides-add-slide.md`(两步创建逐页添加) |
|
|
83
83
|
| 用户要求使用模板,或提供 PPTX 文件要求修改、美化 | 将模板导入为 Slides 再编辑 | `lark-slides-pptx-template-workflows.md` |
|
|
84
|
-
| 编辑单个标题、文本块、图片或局部元素 |
|
|
85
|
-
|
|
|
84
|
+
| 编辑单个标题、文本块、图片或局部元素 | 块级替换/插入,**只动点名的 block,同页其他元素不受影响**;不改页序 | `slides +replace-slide`、`lark-slides-replace-slide.md` |
|
|
85
|
+
| 一页改动很多(批量字体/配色)、要改页面背景、要删掉若干元素 | 整页覆盖,`slide_id` 和页序不变;带原 `id` 写回的元素保留 id,不带 `id` 的会作为新元素插入并拿到新 id;**代价是没写进 `--content` 的元素会被删除,所以改个别元素不要用它** | `slides +update-slide`、`lark-slides-update-slide.md` |
|
|
86
86
|
| 给已有 PPT 追加或插入页面 | 一次一页,`--slide` 支持 `@file` 绕开 shell 转义 | `slides +add-slide`、`lark-slides-add-slide.md` |
|
|
87
87
|
| 删除页面 | 按 `slide_id` 单页删除,删前先回读确认 | `slides +delete-slide`、`lark-slides-delete-slide.md` |
|
|
88
88
|
| 读取或分析已有 PPT | 解析 slides/wiki token,用 shortcut 回读全文 XML 或读取单页 XML,保存 `xml_presentation_id`、`slide_id`、`revision_id` | `slides +xml-get`、`xml_presentation.slide.get`、`lark-slides-xml-presentations-get.md` |
|
|
@@ -106,15 +106,13 @@ metadata:
|
|
|
106
106
|
|
|
107
107
|
**CRITICAL — 新建演示文稿或大幅改写页面时,规划 `asset_need` MUST 遵循 [asset-planning.md](references/asset-planning.md):只做元数据规划,必须有 `fallback_if_missing`,不得要求真实搜索、下载或上传素材。**
|
|
108
108
|
|
|
109
|
-
**CRITICAL — 将完整 `<slide>` XML 提交给 `slides +create
|
|
109
|
+
**CRITICAL — 将完整 `<slide>` XML 提交给 `slides +create`、`slides +add-slide` 或 `slides +update-slide` 之前,MUST 先把待提交 XML 保存到本地文件并运行唯一版式准出入口 [`scripts/xml_text_overlap_lint.py`](scripts/xml_text_overlap_lint.py);`summary.error_count` 必须为 0 才能调用接口。**
|
|
110
110
|
|
|
111
|
-
**CRITICAL —
|
|
111
|
+
**CRITICAL — 创建、大幅改写或每次通过 `slides +update-slide` 整页写回后,MUST 按 [validation-checklist.md](references/validation-checklist.md) 做显式验证:回读全文 XML、核对页数和关键元素,并使用 [`scripts/xml_text_overlap_lint.py`](scripts/xml_text_overlap_lint.py) 统一检查 XML、越界、重叠、空白页和内容稀疏风险。**
|
|
112
112
|
|
|
113
113
|
**CRITICAL — 创建前自检或失败排障时,MUST 按 [troubleshooting.md](references/troubleshooting.md) 检查 XML 转义、结构、shell 截断、图片 token、3350001 和布局风险。**
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
**编辑已有幻灯片页面**:单个标题、文本块、图片或局部元素优先用 [`+replace-slide`](references/lark-slides-replace-slide.md)(块级替换/插入,不动页序);一页改动很多、要改背景或删除若干元素,以及一页或多页的整页重建,优先用 [`+replace-pages`](references/lark-slides-replace-pages.md) 在原 presentation 内按页重建,避免 `slides +create` 生成新链接。选择 action 和完整读-改-写流程见 [`lark-slides-edit-workflows.md`](references/lark-slides-edit-workflows.md)。
|
|
115
|
+
**编辑已有幻灯片页面**:单个标题、文本块、图片或局部元素优先用 [`+replace-slide`](references/lark-slides-replace-slide.md)(块级替换/插入,不动页序);一页里改动很多(例如批量换字体)、要改背景、或要删掉若干元素时用 [`+update-slide`](references/lark-slides-update-slide.md) 整页覆盖(`slide_id` 和页序不变,但没写进 `--content` 的元素会被删除);**多页大改就对每一页各跑一次 `+update-slide`**。选择 action 和完整读-改-写流程见 [`lark-slides-edit-workflows.md`](references/lark-slides-edit-workflows.md)。
|
|
118
116
|
|
|
119
117
|
**用户要求使用模板**:按 [lark-slides-pptx-template-workflows.md](references/lark-slides-pptx-template-workflows.md) 处理。
|
|
120
118
|
|
|
@@ -153,7 +151,7 @@ lark-cli auth login --domain slides
|
|
|
153
151
|
- 创建:[`lark-slides-create.md`](references/lark-slides-create.md)、[`lark-slides-add-slide.md`](references/lark-slides-add-slide.md)(逐页添加 / 给已有 PPT 追加页面)
|
|
154
152
|
- 删除页面:[`lark-slides-delete-slide.md`](references/lark-slides-delete-slide.md)
|
|
155
153
|
- 阅读:[`lark-slides-xml-presentations-get.md`](references/lark-slides-xml-presentations-get.md)
|
|
156
|
-
- 编辑:[`lark-slides-edit-workflows.md`](references/lark-slides-edit-workflows.md)、[`lark-slides-replace-slide.md`](references/lark-slides-replace-slide.md)、[`lark-slides-
|
|
154
|
+
- 编辑:[`lark-slides-edit-workflows.md`](references/lark-slides-edit-workflows.md)、[`lark-slides-replace-slide.md`](references/lark-slides-replace-slide.md)、[`lark-slides-update-slide.md`](references/lark-slides-update-slide.md)
|
|
157
155
|
- 历史版本:[`lark-slides-history.md`](references/lark-slides-history.md)
|
|
158
156
|
- 截图:[`lark-slides-screenshot.md`](references/lark-slides-screenshot.md)
|
|
159
157
|
- 图片:[`lark-slides-media-upload.md`](references/lark-slides-media-upload.md)
|
|
@@ -249,7 +247,7 @@ N. 结尾页:[结尾文案]
|
|
|
249
247
|
| URL 格式 | 示例 | Token 类型 | 处理方式 |
|
|
250
248
|
|----------|------|-----------|----------|
|
|
251
249
|
| `/slides/` | `https://example.larkoffice.com/slides/xxxxxxxxxxxxx` | `xml_presentation_id` | URL 路径中的 token 直接作为 `xml_presentation_id` 使用 |
|
|
252
|
-
| `/wiki/` | `https://
|
|
250
|
+
| `/wiki/` | `https://xxx.feishu.cn/wiki/wikcn_EXAMPLE_NODE_TOKEN_123456` | `wiki_token` | ⚠️ **不能直接使用**,需要先查询获取真实的 `obj_token` |
|
|
253
251
|
|
|
254
252
|
> 带 `--presentation` 的 slides shortcut 都会自动解析以上两种 URL;直接调用原生 API 时仍需手动解析 wiki 链接。
|
|
255
253
|
|
|
@@ -258,7 +256,7 @@ N. 结尾页:[结尾文案]
|
|
|
258
256
|
知识库链接(`/wiki/TOKEN`)不能直接当 `xml_presentation_id`。直接调用原生 API 前,先用 Wiki shortcut 查询节点,确认 `data.obj_type == "slides"`,再用 `data.obj_token` 作为真实 presentation ID。
|
|
259
257
|
|
|
260
258
|
```bash
|
|
261
|
-
lark-cli wiki +node-get --node-token '
|
|
259
|
+
lark-cli wiki +node-get --node-token 'https://xxx.feishu.cn/wiki/wikcn_EXAMPLE_NODE_TOKEN_123456' --as user --format json
|
|
262
260
|
```
|
|
263
261
|
|
|
264
262
|
节点解析必须与后续 Slides 操作使用相同身份;下游明确使用 `--as bot` 时,这里也改为 `--as bot`。
|
|
@@ -292,7 +290,7 @@ Shortcut 是对常用操作的高级封装(`lark-cli slides +<verb> [flags]`
|
|
|
292
290
|
| [`+screenshot`](references/lark-slides-screenshot.md) | 把幻灯片页面截图保存为本地图片;用 `--slide-number` 指定页码(从 1 开始,多页重复传入)或用 `--slide-id` 指定页面;单张用 `--output .lark-slides/screenshots/<deck-or-task-id>/page-01`,批量用 `--output-dir .lark-slides/screenshots/<deck-or-task-id>`(一次最多 10 页);后续必须读取返回的 `output` / `screenshots[].path` |
|
|
293
291
|
| [`+media-upload`](references/lark-slides-media-upload.md) | 上传本地图片到指定演示文稿,返回 `file_token`(用作 `<img src="...">`),最大 20 MB |
|
|
294
292
|
| [`+replace-slide`](references/lark-slides-replace-slide.md) | 对已有幻灯片页面进行块级替换/插入(`block_replace` / `block_insert`),自动注入 id 和 `<content/>`,不改变页序 |
|
|
295
|
-
| [`+
|
|
293
|
+
| [`+update-slide`](references/lark-slides-update-slide.md) | 把一整页 XML 交给已有页面,页面变成 `--content` 描述的样子;能一次改样式/插入/删除/备注/背景,`slide_id` 和页序不变。**没写进 `--content` 的元素会被删除** |
|
|
296
294
|
|
|
297
295
|
没有 Shortcut 覆盖时使用原生 API。高频资源:`slides +xml-get` 读取全文;`xml_presentation.slide.create/delete/get/replace` 管理单页。
|
|
298
296
|
|
|
@@ -311,7 +309,7 @@ lark-cli slides <resource> <method> [flags] # 调用 API
|
|
|
311
309
|
4. **文本通过 `<content>` 表达**:必须用 `<content><p>...</p></content>`,不能把文字直接写在 shape 内;注意 `<content>` 只是 XML 元素,不是 `--parts` 的字段名——part 里装 XML 的字段,`block_replace` 是 `replacement`,`block_insert` 是 `insertion`
|
|
312
310
|
5. **保存关键 ID**:后续操作需要 `xml_presentation_id`、`slide_id`、`revision_id`
|
|
313
311
|
6. **删除谨慎**:删除不可逆,删前先回读确认 `slide_id`
|
|
314
|
-
7. **编辑已有页面优先原链接更新**:修改单个 shape/img 用 `+replace-slide`(`block_replace` / `block_insert
|
|
312
|
+
7. **编辑已有页面优先原链接更新**:修改单个 shape/img 用 `+replace-slide`(`block_replace` / `block_insert`),不要整页重建;一页改动很多或要改背景用 `+update-slide` 整页覆盖(保 `slide_id` 和页序),多页整页重建就对每页各跑一次 `+update-slide`,不要用 `slides +create` 新建整份 PPT;追加/插入单页用 `+add-slide`、删除单页用 `+delete-slide`,只有这些 shortcut 未覆盖的参数才手动调 `slide.create` / `slide.delete`
|
|
315
313
|
8. **`<img src>` 只能用上传到飞书 drive 的 `file_token`,禁止使用 http(s) 外链 URL**:飞书 slides 渲染端不会代理外链图片,外链 src 在 PPT 里通常不显示或显示破图。流程必须是「先把图存到本地 → 用 `slides +media-upload` 上传,或在 `+create --slides` 的 XML 里写 `<img src="@./path">` 占位符自动上传 → 拿 `file_token` 写进 `<img src>`」。如果用户给了网图链接,先 `curl`/下载到 CWD 内再走上传流程,不要直接把外链 URL 塞进 `src`。**图片最大 20 MB**(slides upload API 不支持分片上传)。
|
|
316
314
|
|
|
317
315
|
> **注意**:如果 md 内容与 `slides_xml_schema_definition.xml` 或 `lark-cli schema slides.<resource>.<method>` 输出不一致,以后两者为准。
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
|
|
12
12
|
| 场景 | 推荐方式 |
|
|
13
13
|
|------|----------|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
14
|
+
| 不超过 10 页 | 每页存一个 XML 文件,`slides +create --slide @page-01.xml --slide @page-02.xml ...` 一步创建 |
|
|
15
|
+
| 超过 10 页 | **两步创建**:先 `slides +create` 创建空白 PPT,再用 [`+add-slide`](lark-slides-add-slide.md) 逐页添加 |
|
|
16
16
|
| 已有 PPT 继续追加或插入页面 | 使用 [`+add-slide`](lark-slides-add-slide.md),必要时配合 `--before-slide-id` |
|
|
17
17
|
|
|
18
|
-
> [!WARNING]
|
|
19
|
-
> `--slides '[...]'` 的风险点主要在 shell 参数传递,而不是单纯页数。即使只有 1 页,只要 XML 足够复杂,也建议使用两步创建法。
|
|
20
18
|
> [!IMPORTANT]
|
|
21
|
-
> `slides +create
|
|
19
|
+
> `slides +create` 带页面时底层会逐页创建,不是原子操作。中途失败时先记录 `xml_presentation_id`,回读确认当前状态,再继续修复或追加。
|
|
20
|
+
|
|
21
|
+
**CRITICAL — 提交前必须先跑版式 lint**:把待提交的 `<slide>` XML 存成本地文件,运行 [`scripts/xml_text_overlap_lint.py`](../scripts/xml_text_overlap_lint.py),`summary.error_count` 必须为 0。
|
|
22
22
|
|
|
23
23
|
## 命令
|
|
24
24
|
|
|
@@ -26,31 +26,22 @@
|
|
|
26
26
|
# 创建空白 PPT
|
|
27
27
|
lark-cli slides +create --title "项目汇报"
|
|
28
28
|
|
|
29
|
-
# 创建 PPT +
|
|
30
|
-
lark-cli slides +create --title "项目汇报"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
# 创建 PPT + 添加页面:每页一个 XML 文件,重复 --slide,顺序即页序
|
|
30
|
+
lark-cli slides +create --as user --title "项目汇报" \
|
|
31
|
+
--slide @.lark-slides/plan/project/slide-01.xml \
|
|
32
|
+
--slide @.lark-slides/plan/project/slide-02.xml
|
|
33
|
+
|
|
34
|
+
# 已有组装好的 JSON 数组:从文件或 stdin 读
|
|
35
|
+
lark-cli slides +create --as user --title "项目汇报" --slides @./deck.json
|
|
36
|
+
cat deck.json | lark-cli slides +create --as user --title "项目汇报" --slides -
|
|
34
37
|
|
|
35
38
|
# 以应用身份创建(自动授权当前用户)
|
|
36
39
|
lark-cli slides +create --title "项目汇报" --as bot
|
|
37
40
|
|
|
38
41
|
# 预览(不执行)
|
|
39
|
-
lark-cli slides +create --title "项目汇报" --
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
用 `--slides` 一步创建时,按页保存 XML,再用 `jq --rawfile` 组装参数,不要手写转义:
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
lark-cli slides +create --as user --title "项目汇报" \
|
|
46
|
-
--slides "$(jq -n \
|
|
47
|
-
--rawfile s1 .lark-slides/plan/project/slide-01.xml \
|
|
48
|
-
--rawfile s2 .lark-slides/plan/project/slide-02.xml \
|
|
49
|
-
'[$s1, $s2]')"
|
|
42
|
+
lark-cli slides +create --title "项目汇报" --slide @./slide-01.xml --dry-run
|
|
50
43
|
```
|
|
51
44
|
|
|
52
|
-
`--rawfile` 会把文件内容作为字符串读入 JSON,自动处理 XML 中的引号和换行;不要手动拼接带大量转义符的 JSON 字符串。
|
|
53
|
-
|
|
54
45
|
## 返回值
|
|
55
46
|
|
|
56
47
|
工具成功执行后,返回一个 JSON 对象,包含以下字段:
|
|
@@ -59,15 +50,15 @@ lark-cli slides +create --as user --title "项目汇报" \
|
|
|
59
50
|
- **`title`**(string):演示文稿标题
|
|
60
51
|
- **`url`**(string,可选):演示文稿的在线链接,如有返回则务必展示给用户(需要 drive 相关权限;若获取失败则不返回此字段)
|
|
61
52
|
- **`revision_id`**(integer):演示文稿版本号
|
|
62
|
-
- **`slide_ids`**(string[]
|
|
63
|
-
- **`slides_added`**(integer
|
|
64
|
-
- **`images_uploaded`**(integer
|
|
53
|
+
- **`slide_ids`**(string[],可选):带页面创建时返回,成功添加的页面 ID 列表
|
|
54
|
+
- **`slides_added`**(integer,可选):带页面创建时返回,成功添加的页面数量
|
|
55
|
+
- **`images_uploaded`**(integer,可选):页面 XML 中含 `@<本地路径>` 占位符时返回,已上传的去重后图片数量
|
|
65
56
|
- **`permission_grant`**(object,可选):仅 `--as bot` 时返回,说明是否已自动为当前 CLI 用户授予可管理权限
|
|
66
57
|
|
|
67
58
|
> [!IMPORTANT]
|
|
68
|
-
>
|
|
59
|
+
> 不带页面参数时,`slides +create` 只创建空白演示文稿。创建后用 [`+add-slide`](lark-slides-add-slide.md) 逐页添加 slide 内容。
|
|
69
60
|
>
|
|
70
|
-
>
|
|
61
|
+
> 带了页面时,CLI 先创建空白演示文稿,再逐页调用 slide 创建接口添加页面。如果某一页添加失败,CLI 会停止并报错,已创建的演示文稿和已添加的页面会保留。
|
|
71
62
|
>
|
|
72
63
|
> 如果演示文稿是**以应用身份(bot)创建**的,如 `lark-cli slides +create --as bot`,CLI 会**尝试为当前 CLI 用户自动授予该演示文稿的 `full_access`(可管理权限)**。
|
|
73
64
|
>
|
|
@@ -83,27 +74,67 @@ lark-cli slides +create --as user --title "项目汇报" \
|
|
|
83
74
|
| 参数 | 必填 | 说明 |
|
|
84
75
|
|------|------|------|
|
|
85
76
|
| `--title` | 否 | 演示文稿标题(不传则默认 "Untitled") |
|
|
86
|
-
| `--
|
|
77
|
+
| `--slide` | 否 | 一页 `<slide>` XML,或 `@路径`;可重复,最多 10 次。格式见[页面输入形式](#页面输入形式) |
|
|
78
|
+
| `--slides` | 否 | 页面 XML 的 JSON 字符串数组,最多 10 个;支持 `@文件` 和 `-`(stdin)。格式见[页面输入形式](#页面输入形式) |
|
|
79
|
+
|
|
80
|
+
10 页是 CLI 的上限,服务端每次只接收一页。超过 10 页时先用 `+create` 创建空白 PPT,再用 [`+add-slide`](lark-slides-add-slide.md) 逐页添加。
|
|
81
|
+
|
|
82
|
+
两种形式的每一页都会在发请求前校验成「单个完整的 `<slide>` 文档」。不合格的页在创建演示文稿之前报错并指出页序号,不会留下空壳演示文稿。
|
|
83
|
+
|
|
84
|
+
## 页面输入形式
|
|
85
|
+
|
|
86
|
+
页面内容有 `--slide` 和 `--slides` 两种传法,二选一,同时传会报错。
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
两种形式的 `@路径` 都必须是 CWD 内的相对路径(如 `./slide-01.xml`);绝对路径和 `../` 会被拒(报 `invalid file path`)。XML 写在别的目录时,先 `cd` 过去或把文件拷进 CWD 再执行。
|
|
89
|
+
|
|
90
|
+
### `--slide`:一页一个文件
|
|
91
|
+
|
|
92
|
+
可重复,重复次数即页数,出现顺序即页序。值是一页完整的 `<slide>` XML,或读取该 XML 的 `@路径`。
|
|
93
|
+
|
|
94
|
+
文件内容就是这一页 XML 本身,外面没有引号或方括号:
|
|
95
|
+
|
|
96
|
+
```xml
|
|
97
|
+
<slide xmlns="https://www.larkoffice.com/sml/2.0">
|
|
98
|
+
<data>…第1页…</data>
|
|
99
|
+
</slide>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
文件内容不需要转义:引号、换行、中文原样写。
|
|
103
|
+
|
|
104
|
+
### `--slides`:一个 JSON 数组
|
|
105
|
+
|
|
106
|
+
值是 JSON 字符串数组,每个元素是一整页 XML,支持 `@文件` 和 `-`(stdin)。
|
|
107
|
+
|
|
108
|
+
文件内容是一个 JSON 文档,XML 以 JSON 字符串出现,其中的 `"` 写作 `\"`,换行写作 `\n`:
|
|
89
109
|
|
|
90
110
|
```json
|
|
91
111
|
[
|
|
92
|
-
"<slide xmlns=\"https://www.larkoffice.com/sml/2.0\"
|
|
93
|
-
"<slide xmlns=\"https://www.larkoffice.com/sml/2.0\"
|
|
112
|
+
"<slide xmlns=\"https://www.larkoffice.com/sml/2.0\"><data>…第1页…</data></slide>",
|
|
113
|
+
"<slide xmlns=\"https://www.larkoffice.com/sml/2.0\"><data>…第2页…</data></slide>"
|
|
94
114
|
]
|
|
95
115
|
```
|
|
96
116
|
|
|
97
|
-
|
|
117
|
+
数组元素是页面 XML 原文。包装成 API 所需的 `{"slide": {"content": …}}` 并逐页调用由 CLI 完成。
|
|
118
|
+
|
|
119
|
+
> [!WARNING]
|
|
120
|
+
> `--slides '[...]'` 的风险点主要在 shell 参数传递,而不是单纯页数。即使只有 1 页,只要 XML 足够复杂,也建议改用 `--slide @page-01.xml` 逐页传文件。
|
|
98
121
|
|
|
99
|
-
|
|
122
|
+
## 本地图片:`@<path>` 占位符
|
|
100
123
|
|
|
101
124
|
`<img>` 元素的 `src` 属性如果以 `@` 开头,CLI 会把它当作本地文件路径,自动上传到当前演示文稿,并把占位符替换为返回的 `file_token`。
|
|
102
125
|
|
|
126
|
+
`slide-01.xml`:
|
|
127
|
+
|
|
128
|
+
```xml
|
|
129
|
+
<slide xmlns="https://www.larkoffice.com/sml/2.0">
|
|
130
|
+
<data>
|
|
131
|
+
<img src="@./assets/chart.png" topLeftX="100" topLeftY="100" width="320" height="180"/>
|
|
132
|
+
</data>
|
|
133
|
+
</slide>
|
|
134
|
+
```
|
|
135
|
+
|
|
103
136
|
```bash
|
|
104
|
-
lark-cli slides +create --as user --title "图测试" --
|
|
105
|
-
"<slide xmlns=\"https://www.larkoffice.com/sml/2.0\"><data><img src=\"@./assets/chart.png\" topLeftX=\"100\" topLeftY=\"100\" width=\"320\" height=\"180\"/></data></slide>"
|
|
106
|
-
]'
|
|
137
|
+
lark-cli slides +create --as user --title "图测试" --slide @./slide-01.xml
|
|
107
138
|
```
|
|
108
139
|
|
|
109
140
|
行为:
|
|
@@ -120,11 +151,11 @@ lark-cli slides +create --as user --title "图测试" --slides '[
|
|
|
120
151
|
|
|
121
152
|
## 创建后续步骤
|
|
122
153
|
|
|
123
|
-
|
|
154
|
+
创建空白 PPT 时,`slides +create` 返回的 `xml_presentation_id` 用于后续操作:
|
|
124
155
|
|
|
125
156
|
```bash
|
|
126
157
|
# 第 1 步:创建空白 PPT
|
|
127
|
-
PRES_ID=$(lark-cli slides +create --title "项目汇报"
|
|
158
|
+
PRES_ID=$(lark-cli slides +create --title "项目汇报" --jq '.data.xml_presentation_id')
|
|
128
159
|
|
|
129
160
|
# 第 2 步:逐页添加(--slide 支持 @file,复杂 XML 优先走文件)
|
|
130
161
|
lark-cli slides +add-slide --as user \
|