@amaster.ai/pi-lark 0.1.7 → 0.1.8
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 +14 -6
- package/skills/lark-base/references/lark-base-dashboard-block-get-data.md +17 -1
- package/skills/lark-base/references/lark-base-dashboard.md +17 -4
- 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 +5 -2
- package/skills/lark-calendar/SKILL.md +1 -1
- package/skills/lark-doc/SKILL.md +26 -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 +16 -15
- package/skills/lark-doc/references/lark-doc-md.md +5 -1
- package/skills/lark-doc/references/lark-doc-media-download.md +2 -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-apply-permission.md +1 -1
- package/skills/lark-drive/references/lark-drive-copy.md +87 -0
- package/skills/lark-drive/references/lark-drive-download.md +2 -1
- package/skills/lark-drive/references/lark-drive-export.md +3 -0
- package/skills/lark-drive/references/lark-drive-task-result.md +3 -0
- package/skills/lark-drive/references/lark-drive-update-title.md +78 -0
- package/skills/lark-event/SKILL.md +7 -4
- package/skills/lark-event/references/lark-event-vc.md +8 -2
- package/skills/lark-im/SKILL.md +8 -8
- package/skills/lark-im/references/lark-im-chat-list.md +9 -2
- package/skills/lark-im/references/lark-im-chat-members-list.md +7 -4
- package/skills/lark-im/references/lark-im-chat-messages-list.md +10 -3
- package/skills/lark-im/references/lark-im-chat-search.md +9 -2
- package/skills/lark-im/references/lark-im-feed-group-list-item.md +2 -2
- package/skills/lark-im/references/lark-im-feed-group-list.md +2 -2
- package/skills/lark-im/references/lark-im-feed-shortcut-list.md +1 -1
- package/skills/lark-im/references/lark-im-flag-list.md +2 -2
- 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 +4 -5
- package/skills/lark-im/references/lark-im-threads-messages-list.md +8 -4
- package/skills/lark-mail/references/lark-mail-triage.md +19 -4
- package/skills/lark-minutes/SKILL.md +1 -1
- package/skills/lark-minutes/references/lark-minutes-search.md +6 -7
- package/skills/lark-shared/SKILL.md +3 -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 +27 -44
- package/skills/lark-slides/references/lark-slides-add-slide.md +92 -0
- package/skills/lark-slides/references/lark-slides-create.md +77 -65
- package/skills/lark-slides/references/lark-slides-delete-slide.md +65 -0
- package/skills/lark-slides/references/lark-slides-edit-workflows.md +6 -7
- package/skills/lark-slides/references/lark-slides-media-upload.md +3 -25
- package/skills/lark-slides/references/lark-slides-replace-slide.md +22 -1
- package/skills/lark-slides/references/lark-slides-screenshot.md +31 -13
- package/skills/lark-slides/references/lark-slides-update-slide.md +146 -0
- package/skills/lark-slides/references/lark-slides-xml-presentations-get.md +31 -8
- package/skills/lark-slides/references/slides_chart_demo.xml +1 -2
- package/skills/lark-slides/references/slides_xml_schema_definition.xml +48 -4
- package/skills/lark-slides/references/troubleshooting.md +7 -8
- package/skills/lark-slides/references/validation-checklist.md +4 -4
- package/skills/lark-slides/references/xml-schema-quick-ref.md +23 -11
- package/skills/lark-slides/scripts/sxsd_validator.py +154 -10
- package/skills/lark-slides/scripts/xml_text_overlap_lint.py +360 -76
- package/skills/lark-slides/scripts/xml_text_overlap_lint_test.py +1138 -214
- 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-wiki/SKILL.md +5 -3
- package/skills/lark-wiki/references/lark-wiki-delete-space.md +6 -3
- 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 -95
- package/skills/lark-slides/references/lark-slides-xml-presentation-slide-create.md +0 -219
- package/skills/lark-slides/references/lark-slides-xml-presentation-slide-delete.md +0 -126
|
@@ -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,12 +79,15 @@ 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
|
-
| 编辑单个标题、文本块、图片或局部元素 |
|
|
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
|
+
| 给已有 PPT 追加或插入页面 | 一次一页,`--slide` 支持 `@file` 绕开 shell 转义 | `slides +add-slide`、`lark-slides-add-slide.md` |
|
|
87
|
+
| 删除页面 | 按 `slide_id` 单页删除,删前先回读确认 | `slides +delete-slide`、`lark-slides-delete-slide.md` |
|
|
85
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` |
|
|
86
89
|
| 查看或回滚历史版本 | 先用 `+history-list` 找 `history_version_id`,再 `+history-revert`,必要时 `+history-revert-status` 轮询 | [`lark-slides-history.md`](references/lark-slides-history.md) |
|
|
87
|
-
| 获取幻灯片页面截图 | 用
|
|
90
|
+
| 获取幻灯片页面截图 | 按页码用 `--slide-number`,按 ID 用 `--slide-id`;单张用 `--output`,批量或全量用 `--output-dir`,每批最多 10 页串行执行;截图目录复用同一任务的 deck/task 标识,后续读取返回的实际路径 | `slides +screenshot`、`lark-slides-screenshot.md` |
|
|
88
91
|
| 上传或使用图片 | 先上传为 `file_token`,禁止直接写 http(s) 外链 | `slides +media-upload`、`lark-slides-media-upload.md`,或 `+create --slides` 的 XML 里写 `<img src="@./path">` 占位符 |
|
|
89
92
|
| 绘制图表 | 原生图表(柱状、条形、折线、面积、饼(环)、雷达、组合图)用 `<chart>`,其他(漏斗图、金字塔图、象限图、矩阵图等)用 `<shape>` + `<line>` 模拟 | `xml-schema-quick-ref.md`、`slides_chart_demo.xml` |
|
|
90
93
|
| 绘制表格 | 优先用 `rect` 和 `text` 模拟,其他用 `<table>` | `xml-schema-quick-ref.md` |
|
|
@@ -103,13 +106,13 @@ metadata:
|
|
|
103
106
|
|
|
104
107
|
**CRITICAL — 新建演示文稿或大幅改写页面时,规划 `asset_need` MUST 遵循 [asset-planning.md](references/asset-planning.md):只做元数据规划,必须有 `fallback_if_missing`,不得要求真实搜索、下载或上传素材。**
|
|
105
108
|
|
|
106
|
-
**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 才能调用接口。**
|
|
107
110
|
|
|
108
|
-
**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、越界、重叠、空白页和内容稀疏风险。**
|
|
109
112
|
|
|
110
113
|
**CRITICAL — 创建前自检或失败排障时,MUST 按 [troubleshooting.md](references/troubleshooting.md) 检查 XML 转义、结构、shell 截断、图片 token、3350001 和布局风险。**
|
|
111
114
|
|
|
112
|
-
**编辑已有幻灯片页面**:单个标题、文本块、图片或局部元素优先用 [`+replace-slide`](references/lark-slides-replace-slide.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)。
|
|
113
116
|
|
|
114
117
|
**用户要求使用模板**:按 [lark-slides-pptx-template-workflows.md](references/lark-slides-pptx-template-workflows.md) 处理。
|
|
115
118
|
|
|
@@ -145,9 +148,10 @@ lark-cli auth login --domain slides
|
|
|
145
148
|
|
|
146
149
|
调用相关命令前必须读取相关的文档以了解命令的使用方式:
|
|
147
150
|
|
|
148
|
-
- 创建:[`lark-slides-create.md`](references/lark-slides-create.md)、[`lark-slides-
|
|
151
|
+
- 创建:[`lark-slides-create.md`](references/lark-slides-create.md)、[`lark-slides-add-slide.md`](references/lark-slides-add-slide.md)(逐页添加 / 给已有 PPT 追加页面)
|
|
152
|
+
- 删除页面:[`lark-slides-delete-slide.md`](references/lark-slides-delete-slide.md)
|
|
149
153
|
- 阅读:[`lark-slides-xml-presentations-get.md`](references/lark-slides-xml-presentations-get.md)
|
|
150
|
-
- 编辑:[`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)
|
|
151
155
|
- 历史版本:[`lark-slides-history.md`](references/lark-slides-history.md)
|
|
152
156
|
- 截图:[`lark-slides-screenshot.md`](references/lark-slides-screenshot.md)
|
|
153
157
|
- 图片:[`lark-slides-media-upload.md`](references/lark-slides-media-upload.md)
|
|
@@ -208,7 +212,7 @@ Step 2: 生成大纲 → 写入 slide_plan.json
|
|
|
208
212
|
Step 3: 按 slide_plan.json 生成 XML → 创建
|
|
209
213
|
- 逐页消费 plan:key_message 定主结论,layout_type 定几何,visual_focus 定主视觉,text_density 定文本量
|
|
210
214
|
- 缺少真实素材时必须用 `fallback_if_missing` 生成替代图片,不要留空
|
|
211
|
-
- 读 lark-slides-create.md 定一步创建还是两步创建,并据此构造 `slides +create`;两步创建再读 lark-slides-
|
|
215
|
+
- 读 lark-slides-create.md 定一步创建还是两步创建,并据此构造 `slides +create`;两步创建再读 lark-slides-add-slide.md 用 `+add-slide` 逐页添加
|
|
212
216
|
- 图片按 lark-slides-media-upload.md 处理;复杂 XML、转义和 3350001 排查按 troubleshooting.md 执行
|
|
213
217
|
|
|
214
218
|
Step 4: 审查 & 交付
|
|
@@ -217,31 +221,6 @@ Step 4: 审查 & 交付
|
|
|
217
221
|
- 没问题 → 交付:使用 NotifyHuman 工具交付 PPT 链接
|
|
218
222
|
```
|
|
219
223
|
|
|
220
|
-
### jq 命令模板(编辑已有 PPT 时使用)
|
|
221
|
-
|
|
222
|
-
以下 jq 模板适用于向已有演示文稿追加页面的场景,可以避免手动转义双引号:
|
|
223
|
-
|
|
224
|
-
```bash
|
|
225
|
-
# 追加到末尾
|
|
226
|
-
lark-cli slides xml_presentation.slide create \
|
|
227
|
-
--as user \
|
|
228
|
-
--params '{"xml_presentation_id":"YOUR_ID"}' \
|
|
229
|
-
--data "$(jq -n --arg content '<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
230
|
-
<style><fill><fillColor color="BACKGROUND_COLOR"/></fill></style>
|
|
231
|
-
<data>
|
|
232
|
-
在这里放置 shape、line、table、chart 等元素
|
|
233
|
-
</data>
|
|
234
|
-
</slide>' '{slide:{content:$content}}')"
|
|
235
|
-
|
|
236
|
-
# 插到指定页之前:before_slide_id 必须在 --data body 里,与 slide 同级
|
|
237
|
-
# ⚠️ 不要把 before_slide_id 写进 --params —— CLI 会当未知 query 参数静默下发,服务端忽略,新页跑到末尾
|
|
238
|
-
lark-cli slides xml_presentation.slide create \
|
|
239
|
-
--as user \
|
|
240
|
-
--params '{"xml_presentation_id":"YOUR_ID"}' \
|
|
241
|
-
--data "$(jq -n --arg content '<slide ...>...</slide>' --arg before 'TARGET_SLIDE_ID' \
|
|
242
|
-
'{slide:{content:$content}, before_slide_id:$before}')"
|
|
243
|
-
```
|
|
244
|
-
|
|
245
224
|
> 渐变色必须使用 `rgba()` 格式并带百分比停靠点,如 `linear-gradient(135deg,rgba(15,23,42,1) 0%,rgba(56,97,140,1) 100%)`。使用 `rgb()` 或省略停靠点会导致服务端回退为白色。
|
|
246
225
|
|
|
247
226
|
### 大纲模板
|
|
@@ -268,19 +247,21 @@ N. 结尾页:[结尾文案]
|
|
|
268
247
|
| URL 格式 | 示例 | Token 类型 | 处理方式 |
|
|
269
248
|
|----------|------|-----------|----------|
|
|
270
249
|
| `/slides/` | `https://example.larkoffice.com/slides/xxxxxxxxxxxxx` | `xml_presentation_id` | URL 路径中的 token 直接作为 `xml_presentation_id` 使用 |
|
|
271
|
-
| `/wiki/` | `https://
|
|
250
|
+
| `/wiki/` | `https://xxx.feishu.cn/wiki/wikcn_EXAMPLE_NODE_TOKEN_123456` | `wiki_token` | ⚠️ **不能直接使用**,需要先查询获取真实的 `obj_token` |
|
|
272
251
|
|
|
273
|
-
>
|
|
252
|
+
> 带 `--presentation` 的 slides shortcut 都会自动解析以上两种 URL;直接调用原生 API 时仍需手动解析 wiki 链接。
|
|
274
253
|
|
|
275
254
|
### Wiki 链接特殊处理(关键!)
|
|
276
255
|
|
|
277
|
-
知识库链接(`/wiki/TOKEN`)不能直接当 `xml_presentation_id`。直接调用原生 API
|
|
256
|
+
知识库链接(`/wiki/TOKEN`)不能直接当 `xml_presentation_id`。直接调用原生 API 前,先用 Wiki shortcut 查询节点,确认 `data.obj_type == "slides"`,再用 `data.obj_token` 作为真实 presentation ID。
|
|
278
257
|
|
|
279
258
|
```bash
|
|
280
|
-
lark-cli wiki
|
|
259
|
+
lark-cli wiki +node-get --node-token 'https://xxx.feishu.cn/wiki/wikcn_EXAMPLE_NODE_TOKEN_123456' --as user --format json
|
|
281
260
|
```
|
|
282
261
|
|
|
283
|
-
|
|
262
|
+
节点解析必须与后续 Slides 操作使用相同身份;下游明确使用 `--as bot` 时,这里也改为 `--as bot`。
|
|
263
|
+
|
|
264
|
+
带 `--presentation` 的 slides shortcut 都会自动解析 `/wiki/` URL 并校验 `obj_type`;手动调用 `xml_presentations.*` / `xml_presentation.slide.*` 时才需要自己做这一步。
|
|
284
265
|
|
|
285
266
|
### 资源关系
|
|
286
267
|
|
|
@@ -303,11 +284,13 @@ Shortcut 是对常用操作的高级封装(`lark-cli slides +<verb> [flags]`
|
|
|
303
284
|
| Shortcut | 说明 |
|
|
304
285
|
|----------|------|
|
|
305
286
|
| [`+create`](references/lark-slides-create.md) | 创建 PPT,可选一步添加页面 |
|
|
287
|
+
| [`+add-slide`](references/lark-slides-add-slide.md) | 向已有演示文稿追加或插入**一页**(`--before-slide-id` 控制位置),XML 支持 `@file` / stdin,`<img src="@./path">` 占位符自动上传 |
|
|
288
|
+
| [`+delete-slide`](references/lark-slides-delete-slide.md) | 按 `slide_id` 删除**一页** |
|
|
306
289
|
| [`+xml-get`](references/lark-slides-xml-presentations-get.md) | 读取全文 XML,用 `--presentation` 指定演示文稿的 `xml_presentation_id`,用 `--output` 把 XML 存到本地文件(必须是 CWD 内的相对路径,如 `.lark-slides/plan/<deck>/readback.xml`) |
|
|
307
|
-
| [`+screenshot`](references/lark-slides-screenshot.md) |
|
|
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` |
|
|
308
291
|
| [`+media-upload`](references/lark-slides-media-upload.md) | 上传本地图片到指定演示文稿,返回 `file_token`(用作 `<img src="...">`),最大 20 MB |
|
|
309
292
|
| [`+replace-slide`](references/lark-slides-replace-slide.md) | 对已有幻灯片页面进行块级替换/插入(`block_replace` / `block_insert`),自动注入 id 和 `<content/>`,不改变页序 |
|
|
310
|
-
| [`+
|
|
293
|
+
| [`+update-slide`](references/lark-slides-update-slide.md) | 把一整页 XML 交给已有页面,页面变成 `--content` 描述的样子;能一次改样式/插入/删除/备注/背景,`slide_id` 和页序不变。**没写进 `--content` 的元素会被删除** |
|
|
311
294
|
|
|
312
295
|
没有 Shortcut 覆盖时使用原生 API。高频资源:`slides +xml-get` 读取全文;`xml_presentation.slide.create/delete/get/replace` 管理单页。
|
|
313
296
|
|
|
@@ -323,10 +306,10 @@ lark-cli slides <resource> <method> [flags] # 调用 API
|
|
|
323
306
|
1. **先规划再写 XML**:新建演示文稿或大幅改写页面时,必须先写入 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`;模板、风格和大纲只能作为规划输入,不能绕过规划层
|
|
324
307
|
2. **创建流程**:新建演示文稿用 `slides +create`,一步创建还是两步创建按 [`lark-slides-create.md`](references/lark-slides-create.md) 判断
|
|
325
308
|
3. **`<slide>` 直接子元素只有 `<style>`、`<data>`、`<note>`**:文本和图形必须放在 `<data>` 内
|
|
326
|
-
4. **文本通过 `<content>` 表达**:必须用 `<content><p>...</p></content>`,不能把文字直接写在 shape
|
|
309
|
+
4. **文本通过 `<content>` 表达**:必须用 `<content><p>...</p></content>`,不能把文字直接写在 shape 内;注意 `<content>` 只是 XML 元素,不是 `--parts` 的字段名——part 里装 XML 的字段,`block_replace` 是 `replacement`,`block_insert` 是 `insertion`
|
|
327
310
|
5. **保存关键 ID**:后续操作需要 `xml_presentation_id`、`slide_id`、`revision_id`
|
|
328
|
-
6.
|
|
329
|
-
7. **编辑已有页面优先原链接更新**:修改单个 shape/img 用 `+replace-slide`(`block_replace` / `block_insert
|
|
311
|
+
6. **删除谨慎**:删除不可逆,删前先回读确认 `slide_id`
|
|
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`
|
|
330
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 不支持分片上传)。
|
|
331
314
|
|
|
332
315
|
> **注意**:如果 md 内容与 `slides_xml_schema_definition.xml` 或 `lark-cli schema slides.<resource>.<method>` 输出不一致,以后两者为准。
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# slides +add-slide(向已有演示文稿追加/插入单页)
|
|
2
|
+
|
|
3
|
+
向已有演示文稿添加**一页**。这是两步创建流程的第二步:先 `+create` 建空壳,再逐页 `+add-slide`;也用于给已有 PPT 追加新页。
|
|
4
|
+
|
|
5
|
+
`--presentation` 接受 token / `/slides/` URL / `/wiki/` URL(wiki 自动解析),`--slide` 直接收 XML(支持 `@file` 和 stdin,复杂 XML 走文件可绕开 shell 转义),`<img src="@./local.png">` 占位符自动上传并替换成 `file_token`。
|
|
6
|
+
|
|
7
|
+
**CRITICAL — 提交前必须先跑版式 lint**:把待提交的 `<slide>` XML 存成本地文件,运行 [`scripts/xml_text_overlap_lint.py`](../scripts/xml_text_overlap_lint.py),`summary.error_count` 必须为 0。
|
|
8
|
+
|
|
9
|
+
## 命令
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# 追加到末尾(XML 直接作为参数)
|
|
13
|
+
lark-cli slides +add-slide --as user \
|
|
14
|
+
--presentation "$PID" \
|
|
15
|
+
--slide '<slide xmlns="https://www.larkoffice.com/sml/2.0"><data></data></slide>'
|
|
16
|
+
|
|
17
|
+
# XML 从文件读(推荐:避免 shell 转义和长参数截断)
|
|
18
|
+
lark-cli slides +add-slide --as user \
|
|
19
|
+
--presentation "$PID" \
|
|
20
|
+
--slide @page3.xml
|
|
21
|
+
|
|
22
|
+
# XML 从 stdin 读
|
|
23
|
+
cat page3.xml | lark-cli slides +add-slide --as user --presentation "$PID" --slide -
|
|
24
|
+
|
|
25
|
+
# 插到某页之前
|
|
26
|
+
lark-cli slides +add-slide --as user \
|
|
27
|
+
--presentation "$PID" \
|
|
28
|
+
--slide @cover.xml \
|
|
29
|
+
--before-slide-id "$SID"
|
|
30
|
+
|
|
31
|
+
# wiki 链接(CLI 自动 wiki.spaces.get_node 解析,并校验 obj_type=slides)
|
|
32
|
+
lark-cli slides +add-slide --as user \
|
|
33
|
+
--presentation "https://xxx.feishu.cn/wiki/wikcnXXXXXX" \
|
|
34
|
+
--slide @page3.xml
|
|
35
|
+
|
|
36
|
+
# 预览请求,不实际写入
|
|
37
|
+
lark-cli slides +add-slide --presentation "$PID" --slide @page3.xml --dry-run
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 参数
|
|
41
|
+
|
|
42
|
+
| 参数 | 必需 | 说明 |
|
|
43
|
+
|------|------|------|
|
|
44
|
+
| `--presentation` | 是 | `xml_presentation_id`、`/slides/` URL 或 `/wiki/` URL |
|
|
45
|
+
| `--slide` | 是 | 一个完整的 `<slide>...</slide>` 文档;支持字面量、`@file`、stdin `-` |
|
|
46
|
+
| `--before-slide-id` | 否 | 插到该 `slide_id` 之前;**不传就是追加到末尾** |
|
|
47
|
+
| `--revision-id` | 否 | 演示文稿版本号,默认 `-1`(最新);传具体版本号做乐观锁 |
|
|
48
|
+
| `--dry-run` | 否 | 打印将要发起的请求(含图片上传步骤),不写入 |
|
|
49
|
+
|
|
50
|
+
`@file` 路径**必须在 CWD 内**(如 `@./plan/page3.xml`);绝对路径和 `../` 会被拒绝并报 `unsafe file path`。
|
|
51
|
+
|
|
52
|
+
## 本地图片:`@路径` 占位符
|
|
53
|
+
|
|
54
|
+
XML 里写 `<img src="@./chart.png" .../>`,CLI 会:先把每个不重复的本地文件上传到这份演示文稿(`parent_type=slide_file`),再把 `src` 替换成返回的 `file_token`,最后才提交页面。
|
|
55
|
+
|
|
56
|
+
占位符路径按**执行命令时的 CWD** 解析,跟 `--slide @file` 所在目录无关;`@./assets/x.png` 找的是 `$PWD/assets/x.png`。
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
lark-cli slides +add-slide --as user \
|
|
60
|
+
--presentation "$PID" \
|
|
61
|
+
--slide '<slide xmlns="https://www.larkoffice.com/sml/2.0"><data><img src="@./chart.png" topLeftX="100" topLeftY="100" width="320" height="180"/></data></slide>'
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- 文件不存在、不是普通文件、超过 20 MB,都在**调用任何接口之前**报错,不会留下半成品。
|
|
65
|
+
- 去重只在**单次调用内**生效:多页共用同一张图时,逐页循环会把它每页重传一次。这种图先用 [`+media-upload`](lark-slides-media-upload.md) 传一次,把 `file_token` 写进各页的 `src`。
|
|
66
|
+
|
|
67
|
+
## 成功输出
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"xml_presentation_id": "slides_example_presentation_id",
|
|
72
|
+
"slide_id": "slide_example_id",
|
|
73
|
+
"revision_id": 42,
|
|
74
|
+
"before_slide_id": "slide_example_target_id",
|
|
75
|
+
"images_uploaded": 1,
|
|
76
|
+
"issues": "[issue=unsupported_attr tag=<strong> attr=style]"
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
| 字段 | 说明 |
|
|
81
|
+
|------|------|
|
|
82
|
+
| `slide_id` | 新创建页面的唯一标识 |
|
|
83
|
+
| `issues` | 字符串,**只在服务端丢弃过内容时才出现**:页面创建成功,但括号里列出的标签/属性没写进去。出现就必须 `+screenshot` 复核,别当纯警告忽略;干净提交时这个字段不返回 |
|
|
84
|
+
|
|
85
|
+
## 常见错误
|
|
86
|
+
|
|
87
|
+
| 现象 | 原因 | 解决 |
|
|
88
|
+
|------|------|------|
|
|
89
|
+
| `--slide is not a single complete <slide> document` | 传了 `<presentation>` 整份 XML,或多个 `<slide>` 拼在一起 | 一次只传一页,根元素必须是 `<slide>` |
|
|
90
|
+
| `--slide cannot be empty` | `@file` 指向空文件,或 stdin 没内容 | 检查文件内容 |
|
|
91
|
+
| 3350001 | XML 结构/转义有问题;**或 `--before-slide-id` 不是有效 `slide_id`** | 优先改用 `--slide @file` 绕开 shell 转义;插页失败先 `+xml-get` 回读确认 `slide_id`;再按 [troubleshooting.md](troubleshooting.md) 排查 |
|
|
92
|
+
| 1061004 / 403 | 当前身份对这份 PPT 没有编辑权限 | 检查是否拥有 `slides:presentation:update` 或 `slides:presentation:write_only` scope;wiki 链接另需 `wiki:node:read`,`@` 占位符另需 `docs:document.media:upload`;`--as bot` 还要求该 bot 对目标 PPT 有编辑权限 |
|