@amaster.ai/pi-lark 0.1.14 → 0.1.16
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 +4 -4
- package/skills/lark-apps/SKILL.md +1 -0
- package/skills/lark-apps/references/lark-apps-db.md +2 -0
- package/skills/lark-apps/references/lark-apps-env-pull.md +1 -1
- package/skills/lark-apps/references/lark-apps-env.md +1 -1
- package/skills/lark-apps/references/lark-apps-export.md +62 -0
- package/skills/lark-apps/references/lark-apps-local-dev.md +1 -1
- package/skills/lark-apps/references/lark-apps-observability.md +1 -1
- package/skills/lark-base/SKILL.md +6 -5
- package/skills/lark-base/references/lark-base-app.md +3 -3
- package/skills/lark-base/references/lark-base-dashboard-block-config.md +21 -3
- package/skills/lark-base/references/lark-base-dashboard.md +1 -1
- package/skills/lark-base/references/lark-base-data-query.md +1 -1
- package/skills/lark-base/references/lark-base-field-create.md +1 -1
- package/skills/lark-base/references/lark-base-field-update.md +1 -1
- package/skills/lark-base/references/lark-base-form-questions-create.md +1 -1
- package/skills/lark-base/references/lark-base-form-questions-update.md +1 -1
- package/skills/lark-base/references/lark-base-form-submit.md +1 -1
- package/skills/lark-base/references/lark-base-view-set-filter.md +1 -1
- package/skills/lark-base/references/lark-base-view.md +109 -0
- package/skills/lark-base/references/lark-base-workflow-schema.md +90 -12
- package/skills/lark-base/references/lark-base-workflow.md +99 -3
- package/skills/lark-calendar/SKILL.md +13 -8
- package/skills/lark-calendar/references/lark-calendar-meeting-relation.md +99 -0
- package/skills/lark-calendar/references/lark-calendar-meeting.md +1 -1
- package/skills/lark-calendar/references/lark-calendar-recurring.md +3 -1
- package/skills/lark-calendar/references/lark-calendar-transfer.md +1 -1
- package/skills/lark-doc/SKILL.md +1 -1
- package/skills/lark-doc/references/lark-doc-create-workflow.md +8 -10
- package/skills/lark-doc/references/lark-doc-media-download.md +1 -1
- package/skills/lark-doc/references/lark-doc-media-insert.md +1 -1
- package/skills/lark-doc/references/lark-doc-media-preview.md +1 -1
- package/skills/lark-doc/references/lark-doc-resource-cover.md +1 -1
- package/skills/lark-doc/references/lark-doc-script.md +11 -17
- package/skills/lark-drive/references/lark-drive-add-comment.md +1 -1
- package/skills/lark-drive/references/lark-drive-apply-permission.md +1 -1
- package/skills/lark-drive/references/lark-drive-copy.md +1 -1
- package/skills/lark-drive/references/lark-drive-cover.md +1 -1
- package/skills/lark-drive/references/lark-drive-create-folder.md +1 -1
- package/skills/lark-drive/references/lark-drive-create-shortcut.md +1 -1
- package/skills/lark-drive/references/lark-drive-delete.md +1 -1
- package/skills/lark-drive/references/lark-drive-download.md +1 -1
- package/skills/lark-drive/references/lark-drive-export-download.md +1 -1
- package/skills/lark-drive/references/lark-drive-export.md +1 -1
- package/skills/lark-drive/references/lark-drive-import.md +1 -1
- package/skills/lark-drive/references/lark-drive-inspect.md +2 -2
- package/skills/lark-drive/references/lark-drive-list-comments.md +1 -1
- package/skills/lark-drive/references/lark-drive-move.md +4 -4
- package/skills/lark-drive/references/lark-drive-permission-guide.md +1 -1
- package/skills/lark-drive/references/lark-drive-preview.md +1 -1
- package/skills/lark-drive/references/lark-drive-pull.md +1 -1
- package/skills/lark-drive/references/lark-drive-push.md +1 -1
- package/skills/lark-drive/references/lark-drive-search.md +1 -1
- package/skills/lark-drive/references/lark-drive-status.md +1 -1
- package/skills/lark-drive/references/lark-drive-task-result.md +4 -4
- package/skills/lark-drive/references/lark-drive-update-title.md +1 -1
- package/skills/lark-drive/references/lark-drive-upload.md +1 -1
- package/skills/lark-drive/references/lark-drive-version-delete.md +1 -1
- package/skills/lark-drive/references/lark-drive-version-get.md +1 -1
- package/skills/lark-drive/references/lark-drive-version-history.md +1 -1
- package/skills/lark-drive/references/lark-drive-version-revert.md +1 -1
- package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-execute.md +1 -1
- package/skills/lark-im/SKILL.md +7 -1
- package/skills/lark-im/references/lark-im-chat-create.md +1 -1
- package/skills/lark-im/references/lark-im-chat-list.md +1 -1
- package/skills/lark-im/references/lark-im-chat-members-list.md +1 -1
- package/skills/lark-im/references/lark-im-chat-messages-list.md +7 -2
- package/skills/lark-im/references/lark-im-chat-search.md +1 -1
- package/skills/lark-im/references/lark-im-chat-update.md +1 -1
- package/skills/lark-im/references/lark-im-feed-groups.md +1 -1
- package/skills/lark-im/references/lark-im-feed-shortcut-create.md +1 -1
- package/skills/lark-im/references/lark-im-feed-shortcut-list.md +1 -1
- package/skills/lark-im/references/lark-im-feed-shortcut-remove.md +1 -1
- package/skills/lark-im/references/lark-im-flag-cancel.md +1 -1
- package/skills/lark-im/references/lark-im-flag-create.md +1 -1
- package/skills/lark-im/references/lark-im-flag-list.md +1 -1
- package/skills/lark-im/references/lark-im-message-enrichment.md +1 -1
- package/skills/lark-im/references/lark-im-message-read-status.md +1 -1
- package/skills/lark-im/references/lark-im-messages-edit.md +1 -1
- package/skills/lark-im/references/lark-im-messages-mget.md +20 -3
- package/skills/lark-im/references/lark-im-messages-reply.md +1 -1
- package/skills/lark-im/references/lark-im-messages-resources-download.md +3 -1
- package/skills/lark-im/references/lark-im-messages-search.md +2 -2
- package/skills/lark-im/references/lark-im-messages-send.md +1 -1
- package/skills/lark-im/references/lark-im-reactions.md +1 -1
- package/skills/lark-im/references/lark-im-threads-messages-list.md +6 -2
- package/skills/lark-mail/SKILL.md +19 -8
- package/skills/lark-mail/references/lark-mail-draft-create.md +1 -1
- package/skills/lark-mail/references/lark-mail-draft-edit.md +1 -1
- package/skills/lark-mail/references/lark-mail-forward.md +1 -1
- package/skills/lark-mail/references/lark-mail-reply-all.md +1 -1
- package/skills/lark-mail/references/lark-mail-reply.md +1 -1
- package/skills/lark-mail/references/lark-mail-rules.md +87 -4
- package/skills/lark-mail/references/lark-mail-send.md +1 -1
- package/skills/lark-mail/references/lark-mail-thread-modify.md +73 -0
- package/skills/lark-mail/references/lark-mail-thread-trash.md +62 -0
- package/skills/lark-mail/references/lark-mail-triage.md +1 -1
- package/skills/lark-mail/references/lark-mail-watch.md +2 -2
- package/skills/lark-markdown/references/lark-markdown-create.md +1 -1
- package/skills/lark-markdown/references/lark-markdown-diff.md +1 -1
- package/skills/lark-markdown/references/lark-markdown-fetch.md +1 -1
- package/skills/lark-markdown/references/lark-markdown-overwrite.md +1 -1
- package/skills/lark-markdown/references/lark-markdown-patch.md +1 -1
- package/skills/lark-meeting/SKILL.md +2 -2
- package/skills/lark-meeting/references/lark-minutes-search.md +2 -2
- package/skills/lark-meeting/references/lark-vc-meeting-events.md +15 -15
- package/skills/lark-meeting/references/lark-vc-search.md +10 -7
- package/skills/lark-meeting/scenes/create-and-edit-minutes.md +4 -0
- package/skills/lark-meeting/scenes/live-meeting-attend.md +2 -2
- package/skills/lark-meeting/scenes/live-meeting-interact.md +1 -1
- package/skills/lark-meeting/scenes/query-meeting-and-artifacts.md +3 -3
- package/skills/lark-shared/references/lark-wiki-token-routing.md +7 -7
- package/skills/lark-sheets/SKILL.md +60 -173
- package/skills/lark-sheets/references/lark-sheets-batch-update.md +10 -10
- package/skills/lark-sheets/references/lark-sheets-chart.md +68 -34
- package/skills/lark-sheets/references/lark-sheets-conditional-format.md +4 -4
- package/skills/lark-sheets/references/lark-sheets-filter-view.md +1 -1
- package/skills/lark-sheets/references/lark-sheets-filter.md +2 -2
- package/skills/lark-sheets/references/lark-sheets-float-image.md +2 -2
- package/skills/lark-sheets/references/lark-sheets-formula-translation.md +90 -4
- package/skills/lark-sheets/references/lark-sheets-formula-verify.md +49 -13
- package/skills/lark-sheets/references/lark-sheets-pivot-table.md +13 -13
- package/skills/lark-sheets/references/lark-sheets-range-operations.md +12 -9
- package/skills/lark-sheets/references/lark-sheets-read-data.md +14 -12
- package/skills/lark-sheets/references/lark-sheets-search-replace.md +3 -3
- package/skills/lark-sheets/references/lark-sheets-sheet-structure.md +8 -4
- package/skills/lark-sheets/references/lark-sheets-sparkline.md +2 -2
- package/skills/lark-sheets/references/lark-sheets-styles-put.md +2 -2
- package/skills/lark-sheets/references/lark-sheets-visual-standards.md +22 -19
- package/skills/lark-sheets/references/lark-sheets-workbook.md +22 -7
- package/skills/lark-sheets/references/lark-sheets-write-cells.md +66 -57
- package/skills/lark-sheets/scripts/lark_chart_quality_check.py +1540 -0
- package/skills/lark-sheets/scripts/lark_chart_size_advisor.py +409 -0
- package/skills/lark-sheets/scripts/lark_chart_size_rules.py +292 -0
- package/skills/lark-sheets/scripts/lark_inspect_workbook.py +37 -9
- package/skills/lark-sheets/scripts/lark_sheet_read_cli.py +53 -0
- package/skills/lark-sheets/scripts/{sheets_df.py → lark_sheets_df.py} +1 -1
- package/skills/lark-slides/SKILL.md +11 -18
- package/skills/lark-slides/references/cli/lark-slides-add-slide.md +1 -1
- package/skills/lark-slides/references/cli/lark-slides-create.md +3 -3
- package/skills/lark-slides/references/cli/lark-slides-delete-slide.md +1 -1
- package/skills/lark-slides/references/cli/lark-slides-history.md +1 -8
- package/skills/lark-slides/references/cli/lark-slides-media-upload.md +6 -11
- package/skills/lark-slides/references/cli/lark-slides-replace-slide.md +15 -16
- package/skills/lark-slides/references/cli/lark-slides-update-slide.md +2 -2
- package/skills/lark-slides/references/cli/lark-slides-xml-presentation-slide-get.md +3 -108
- package/skills/lark-slides/references/cli/lark-slides-xml-presentation-slide-replace.md +6 -183
- package/skills/lark-slides/references/cli/lark-slides-xml-presentations-get.md +26 -143
- package/skills/lark-slides/references/lark-slides-xml-presentation-slide-get.md +3 -3
- package/skills/lark-slides/references/lark-slides-xml-presentation-slide-replace.md +2 -2
- package/skills/lark-slides/references/lark-slides-xml-presentations-get.md +2 -2
- package/skills/lark-slides/references/workflow/error-handling.md +3 -3
- package/skills/lark-slides/references/workflow/slides-editing.md +10 -11
- package/skills/lark-slides/references/xml/slides_xml_schema_definition.xml +333 -20
- package/skills/lark-task/references/lark-task-assign.md +1 -1
- package/skills/lark-task/references/lark-task-comment.md +1 -1
- package/skills/lark-task/references/lark-task-complete.md +1 -1
- package/skills/lark-task/references/lark-task-create.md +1 -1
- package/skills/lark-task/references/lark-task-followers.md +1 -1
- package/skills/lark-task/references/lark-task-get-my-tasks.md +1 -1
- package/skills/lark-task/references/lark-task-get-related-tasks.md +1 -1
- package/skills/lark-task/references/lark-task-reminder.md +1 -1
- package/skills/lark-task/references/lark-task-reopen.md +1 -1
- package/skills/lark-task/references/lark-task-search.md +1 -1
- package/skills/lark-task/references/lark-task-set-ancestor.md +1 -1
- package/skills/lark-task/references/lark-task-tasklist-create.md +1 -1
- package/skills/lark-task/references/lark-task-tasklist-members.md +1 -1
- package/skills/lark-task/references/lark-task-tasklist-search.md +1 -1
- package/skills/lark-task/references/lark-task-tasklist-task-add.md +1 -1
- package/skills/lark-task/references/lark-task-update.md +1 -1
- package/skills/lark-task/references/lark-task-upload-attachment.md +1 -1
- package/skills/lark-wiki/SKILL.md +1 -2
- package/skills/lark-wiki/references/lark-wiki-delete-space.md +1 -1
- package/skills/lark-wiki/references/lark-wiki-move-to-drive.md +1 -1
- package/skills/lark-wiki/references/lark-wiki-move.md +4 -3
- package/skills/lark-wiki/references/lark-wiki-node-create.md +4 -3
- package/skills/lark-wiki/references/lark-wiki-node-delete.md +8 -4
- package/skills/lark-wiki/references/lark-wiki-node-get.md +7 -4
- package/skills/lark-sheets/references/lark-sheets-legacy-command-migration.md +0 -152
- package/skills/lark-sheets/scripts/lark_chart_layout_check.py +0 -472
|
@@ -1,472 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
3
|
-
# SPDX-License-Identifier: MIT
|
|
4
|
-
"""Check whether Lark Sheet charts have obvious placement problems.
|
|
5
|
-
|
|
6
|
-
The single required argument is a spreadsheet URL or spreadsheet token. By
|
|
7
|
-
default every worksheet is checked; pass --worksheet-id to restrict the check
|
|
8
|
-
to one worksheet reference_id.
|
|
9
|
-
|
|
10
|
-
Exit codes:
|
|
11
|
-
0: check completed and no layout issue was found
|
|
12
|
-
1: the check could not be completed (CLI/read/response error)
|
|
13
|
-
2: check completed and at least one layout issue was found
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
|
-
import argparse
|
|
19
|
-
import json
|
|
20
|
-
from typing import Any
|
|
21
|
-
|
|
22
|
-
from lark_sheet_read_cli import (
|
|
23
|
-
LarkCliError,
|
|
24
|
-
emit_error,
|
|
25
|
-
envelope_data,
|
|
26
|
-
resolve_target_sheets,
|
|
27
|
-
run_sheets,
|
|
28
|
-
sheet_identifier,
|
|
29
|
-
sheet_title,
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
ACTION = "chart_layout_check"
|
|
33
|
-
DEFAULT_COLUMN_WIDTH = 105.0
|
|
34
|
-
DEFAULT_ROW_HEIGHT = 27.0
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def column_to_index(column: str) -> int:
|
|
38
|
-
value = 0
|
|
39
|
-
text = str(column).strip().upper()
|
|
40
|
-
if not text or not text.isalpha():
|
|
41
|
-
raise ValueError(f"Invalid column: {column!r}")
|
|
42
|
-
for char in text:
|
|
43
|
-
value = value * 26 + ord(char) - ord("A") + 1
|
|
44
|
-
return value - 1
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def index_to_column(index: int) -> str:
|
|
48
|
-
if index < 0:
|
|
49
|
-
raise ValueError(f"Invalid column index: {index}")
|
|
50
|
-
chars: list[str] = []
|
|
51
|
-
value = index + 1
|
|
52
|
-
while value:
|
|
53
|
-
value, remainder = divmod(value - 1, 26)
|
|
54
|
-
chars.append(chr(ord("A") + remainder))
|
|
55
|
-
return "".join(reversed(chars))
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def _span_bounds(span: str, *, columns: bool) -> tuple[int, int]:
|
|
59
|
-
start, separator, end = str(span).partition(":")
|
|
60
|
-
end = end if separator else start
|
|
61
|
-
if columns:
|
|
62
|
-
return column_to_index(start), column_to_index(end)
|
|
63
|
-
return int(start) - 1, int(end) - 1
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _size_edges(
|
|
67
|
-
groups: Any,
|
|
68
|
-
*,
|
|
69
|
-
count: int,
|
|
70
|
-
span_key: str,
|
|
71
|
-
size_key: str,
|
|
72
|
-
columns: bool,
|
|
73
|
-
default_size: float,
|
|
74
|
-
) -> tuple[list[float], bool]:
|
|
75
|
-
sizes: list[float | None] = [None] * count
|
|
76
|
-
if isinstance(groups, list):
|
|
77
|
-
for group in groups:
|
|
78
|
-
if not isinstance(group, dict) or group.get(span_key) is None:
|
|
79
|
-
continue
|
|
80
|
-
start, end = _span_bounds(str(group[span_key]), columns=columns)
|
|
81
|
-
size = float(group.get(size_key, default_size))
|
|
82
|
-
for index in range(max(0, start), min(count - 1, end) + 1):
|
|
83
|
-
sizes[index] = max(0.0, size)
|
|
84
|
-
|
|
85
|
-
used_default = any(size is None for size in sizes)
|
|
86
|
-
resolved = [default_size if size is None else size for size in sizes]
|
|
87
|
-
edges = [0.0]
|
|
88
|
-
for size in resolved:
|
|
89
|
-
edges.append(edges[-1] + size)
|
|
90
|
-
return edges, used_default
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def build_layout(
|
|
94
|
-
structure: dict[str, Any], row_count: int, column_count: int
|
|
95
|
-
) -> tuple[list[float], list[float], list[str]]:
|
|
96
|
-
row_groups = structure.get("row_heights")
|
|
97
|
-
column_groups = structure.get("col_widths", structure.get("column_widths"))
|
|
98
|
-
row_edges, row_defaulted = _size_edges(
|
|
99
|
-
row_groups,
|
|
100
|
-
count=row_count,
|
|
101
|
-
span_key="rows",
|
|
102
|
-
size_key="height",
|
|
103
|
-
columns=False,
|
|
104
|
-
default_size=DEFAULT_ROW_HEIGHT,
|
|
105
|
-
)
|
|
106
|
-
column_edges, column_defaulted = _size_edges(
|
|
107
|
-
column_groups,
|
|
108
|
-
count=column_count,
|
|
109
|
-
span_key="cols",
|
|
110
|
-
size_key="width",
|
|
111
|
-
columns=True,
|
|
112
|
-
default_size=DEFAULT_COLUMN_WIDTH,
|
|
113
|
-
)
|
|
114
|
-
warnings: list[str] = []
|
|
115
|
-
if row_defaulted:
|
|
116
|
-
warnings.append("部分行缺少高度信息,按 27 px 估算")
|
|
117
|
-
if column_defaulted:
|
|
118
|
-
warnings.append("部分列缺少宽度信息,按 105 px 估算")
|
|
119
|
-
return row_edges, column_edges, warnings
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def _first_dict(value: Any) -> dict[str, Any] | None:
|
|
123
|
-
if isinstance(value, dict):
|
|
124
|
-
return value
|
|
125
|
-
if isinstance(value, list):
|
|
126
|
-
return next((item for item in value if isinstance(item, dict)), None)
|
|
127
|
-
return None
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def extract_sheet_structure(data: dict[str, Any]) -> dict[str, Any]:
|
|
131
|
-
sheet = _first_dict(data.get("sheets")) or _first_dict(data.get("sheet"))
|
|
132
|
-
return sheet or data
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def extract_charts(data: dict[str, Any], sheet_id: str, title: str) -> list[dict[str, Any]]:
|
|
136
|
-
sheets = data.get("sheets")
|
|
137
|
-
if isinstance(sheets, list):
|
|
138
|
-
for sheet in sheets:
|
|
139
|
-
if not isinstance(sheet, dict):
|
|
140
|
-
continue
|
|
141
|
-
if sheet_identifier(sheet) == sheet_id or sheet_title(sheet) == title:
|
|
142
|
-
charts = sheet.get("charts")
|
|
143
|
-
return [chart for chart in charts if isinstance(chart, dict)] if isinstance(charts, list) else []
|
|
144
|
-
charts = data.get("charts")
|
|
145
|
-
return [chart for chart in charts if isinstance(chart, dict)] if isinstance(charts, list) else []
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def chart_rectangle(
|
|
149
|
-
chart: dict[str, Any], row_edges: list[float], column_edges: list[float]
|
|
150
|
-
) -> dict[str, Any]:
|
|
151
|
-
details = chart.get("details") if isinstance(chart.get("details"), dict) else chart
|
|
152
|
-
position = details.get("position") if isinstance(details.get("position"), dict) else {}
|
|
153
|
-
offset = details.get("offset") if isinstance(details.get("offset"), dict) else {}
|
|
154
|
-
size = details.get("size") if isinstance(details.get("size"), dict) else {}
|
|
155
|
-
|
|
156
|
-
row = int(position["row"])
|
|
157
|
-
column = column_to_index(str(position["col"]))
|
|
158
|
-
if row < 0 or column < 0 or row >= len(row_edges) - 1 or column >= len(column_edges) - 1:
|
|
159
|
-
raise ValueError(f"anchor outside sheet: {position!r}")
|
|
160
|
-
|
|
161
|
-
width = float(size["width"])
|
|
162
|
-
height = float(size["height"])
|
|
163
|
-
if width <= 0 or height <= 0:
|
|
164
|
-
raise ValueError(f"invalid chart size: {size!r}")
|
|
165
|
-
|
|
166
|
-
left = column_edges[column] + float(offset.get("col_offset", 0) or 0)
|
|
167
|
-
top = row_edges[row] + float(offset.get("row_offset", 0) or 0)
|
|
168
|
-
return {
|
|
169
|
-
"chart_id": str(chart.get("chart_id") or chart.get("id") or ""),
|
|
170
|
-
"anchor_cell": f"{index_to_column(column)}{row + 1}",
|
|
171
|
-
"left": left,
|
|
172
|
-
"top": top,
|
|
173
|
-
"right": left + width,
|
|
174
|
-
"bottom": top + height,
|
|
175
|
-
"width": width,
|
|
176
|
-
"height": height,
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
def intersection(first: dict[str, Any], second: dict[str, Any]) -> dict[str, float] | None:
|
|
181
|
-
left = max(float(first["left"]), float(second["left"]))
|
|
182
|
-
top = max(float(first["top"]), float(second["top"]))
|
|
183
|
-
right = min(float(first["right"]), float(second["right"]))
|
|
184
|
-
bottom = min(float(first["bottom"]), float(second["bottom"]))
|
|
185
|
-
if right <= left or bottom <= top:
|
|
186
|
-
return None
|
|
187
|
-
return {
|
|
188
|
-
"width": round(right - left, 2),
|
|
189
|
-
"height": round(bottom - top, 2),
|
|
190
|
-
"area": round((right - left) * (bottom - top), 2),
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
def chart_context(rectangle: dict[str, Any]) -> dict[str, Any]:
|
|
195
|
-
return {
|
|
196
|
-
"chart_id": rectangle["chart_id"],
|
|
197
|
-
"anchor_cell": rectangle["anchor_cell"],
|
|
198
|
-
"rectangle_px": {
|
|
199
|
-
"left": round(rectangle["left"], 2),
|
|
200
|
-
"top": round(rectangle["top"], 2),
|
|
201
|
-
"right": round(rectangle["right"], 2),
|
|
202
|
-
"bottom": round(rectangle["bottom"], 2),
|
|
203
|
-
"width": round(rectangle["width"], 2),
|
|
204
|
-
"height": round(rectangle["height"], 2),
|
|
205
|
-
},
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def _covered_indexes(edges: list[float], start: float, end: float) -> list[int]:
|
|
210
|
-
return [
|
|
211
|
-
index
|
|
212
|
-
for index in range(len(edges) - 1)
|
|
213
|
-
if edges[index + 1] > start and edges[index] < end
|
|
214
|
-
]
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def rectangle_cell_range(
|
|
218
|
-
rectangle: dict[str, Any], row_edges: list[float], column_edges: list[float]
|
|
219
|
-
) -> str | None:
|
|
220
|
-
rows = _covered_indexes(row_edges, max(0.0, rectangle["top"]), rectangle["bottom"])
|
|
221
|
-
columns = _covered_indexes(column_edges, max(0.0, rectangle["left"]), rectangle["right"])
|
|
222
|
-
if not rows or not columns:
|
|
223
|
-
return None
|
|
224
|
-
return f"{index_to_column(columns[0])}{rows[0] + 1}:{index_to_column(columns[-1])}{rows[-1] + 1}"
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def _has_content(cell: Any) -> bool:
|
|
228
|
-
if not isinstance(cell, dict):
|
|
229
|
-
return False
|
|
230
|
-
for key in ("value", "formula", "note"):
|
|
231
|
-
value = cell.get(key)
|
|
232
|
-
if value not in (None, ""):
|
|
233
|
-
return True
|
|
234
|
-
return bool(cell.get("rich_text") or cell.get("multiple_values"))
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
def non_empty_cells(data: dict[str, Any], sample_limit: int) -> tuple[int, list[str], bool]:
|
|
238
|
-
count = 0
|
|
239
|
-
samples: list[str] = []
|
|
240
|
-
truncated = bool(data.get("has_more"))
|
|
241
|
-
ranges = data.get("ranges")
|
|
242
|
-
if not isinstance(ranges, list):
|
|
243
|
-
return 0, [], truncated
|
|
244
|
-
for result_range in ranges:
|
|
245
|
-
if not isinstance(result_range, dict):
|
|
246
|
-
continue
|
|
247
|
-
truncated = truncated or bool(result_range.get("truncated"))
|
|
248
|
-
cells = result_range.get("cells")
|
|
249
|
-
rows = result_range.get("row_indices")
|
|
250
|
-
columns = result_range.get("col_indices")
|
|
251
|
-
if not isinstance(cells, list):
|
|
252
|
-
continue
|
|
253
|
-
for row_offset, row in enumerate(cells):
|
|
254
|
-
if not isinstance(row, list):
|
|
255
|
-
continue
|
|
256
|
-
row_number = rows[row_offset] if isinstance(rows, list) and row_offset < len(rows) else row_offset + 1
|
|
257
|
-
for column_offset, cell in enumerate(row):
|
|
258
|
-
if not _has_content(cell):
|
|
259
|
-
continue
|
|
260
|
-
count += 1
|
|
261
|
-
if len(samples) < sample_limit:
|
|
262
|
-
column = columns[column_offset] if isinstance(columns, list) and column_offset < len(columns) else index_to_column(column_offset)
|
|
263
|
-
samples.append(f"{column}{row_number}")
|
|
264
|
-
return count, samples, truncated
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
def _locator(target: str) -> dict[str, str]:
|
|
268
|
-
return {"url": target} if target.startswith(("http://", "https://")) else {"spreadsheet_token": target}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
def _sheet_counts(sheet: dict[str, Any]) -> tuple[int, int]:
|
|
272
|
-
row_count = int(sheet.get("row_count") or sheet.get("rowCount") or 0)
|
|
273
|
-
column_count = int(sheet.get("column_count") or sheet.get("columnCount") or 0)
|
|
274
|
-
if row_count <= 0 or column_count <= 0:
|
|
275
|
-
raise LarkCliError(f"Missing row_count/column_count for sheet {sheet_title(sheet)!r}")
|
|
276
|
-
return row_count, column_count
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
def check_sheet(
|
|
280
|
-
locator: dict[str, str], sheet: dict[str, Any], *, timeout: int, sample_limit: int
|
|
281
|
-
) -> dict[str, Any]:
|
|
282
|
-
sheet_id = sheet_identifier(sheet)
|
|
283
|
-
title = sheet_title(sheet)
|
|
284
|
-
row_count, column_count = _sheet_counts(sheet)
|
|
285
|
-
if not sheet_id:
|
|
286
|
-
raise LarkCliError(f"Missing sheet_id for sheet {title!r}")
|
|
287
|
-
|
|
288
|
-
structure_data = envelope_data(
|
|
289
|
-
run_sheets(
|
|
290
|
-
"+sheet-info",
|
|
291
|
-
**locator,
|
|
292
|
-
sheet_id=sheet_id,
|
|
293
|
-
flags={"include": "row_heights,col_widths"},
|
|
294
|
-
timeout=timeout,
|
|
295
|
-
)
|
|
296
|
-
)
|
|
297
|
-
row_edges, column_edges, warnings = build_layout(
|
|
298
|
-
extract_sheet_structure(structure_data), row_count, column_count
|
|
299
|
-
)
|
|
300
|
-
chart_data = envelope_data(
|
|
301
|
-
run_sheets("+chart-list", **locator, sheet_id=sheet_id, timeout=timeout)
|
|
302
|
-
)
|
|
303
|
-
charts = extract_charts(chart_data, sheet_id, title)
|
|
304
|
-
|
|
305
|
-
rectangles: list[dict[str, Any]] = []
|
|
306
|
-
unverifiable: list[dict[str, str]] = []
|
|
307
|
-
expected_chart_count = sheet.get("chart_count")
|
|
308
|
-
if expected_chart_count is not None and int(expected_chart_count) != len(charts):
|
|
309
|
-
unverifiable.append(
|
|
310
|
-
{
|
|
311
|
-
"chart_id": "",
|
|
312
|
-
"reason": (
|
|
313
|
-
f"chart-list returned {len(charts)} charts, "
|
|
314
|
-
f"but workbook-info reported {int(expected_chart_count)}"
|
|
315
|
-
),
|
|
316
|
-
}
|
|
317
|
-
)
|
|
318
|
-
for chart in charts:
|
|
319
|
-
chart_id = str(chart.get("chart_id") or chart.get("id") or "")
|
|
320
|
-
if not chart_id:
|
|
321
|
-
unverifiable.append({"chart_id": "", "reason": "chart is missing chart_id"})
|
|
322
|
-
continue
|
|
323
|
-
try:
|
|
324
|
-
rectangles.append(chart_rectangle(chart, row_edges, column_edges))
|
|
325
|
-
except (KeyError, TypeError, ValueError) as exc:
|
|
326
|
-
unverifiable.append({"chart_id": chart_id, "reason": str(exc)})
|
|
327
|
-
|
|
328
|
-
overlaps: list[dict[str, Any]] = []
|
|
329
|
-
for index, first in enumerate(rectangles):
|
|
330
|
-
for second in rectangles[index + 1 :]:
|
|
331
|
-
overlap = intersection(first, second)
|
|
332
|
-
if overlap:
|
|
333
|
-
overlaps.append(
|
|
334
|
-
{
|
|
335
|
-
"chart_ids": [first["chart_id"], second["chart_id"]],
|
|
336
|
-
"charts": [chart_context(first), chart_context(second)],
|
|
337
|
-
"intersection": overlap,
|
|
338
|
-
}
|
|
339
|
-
)
|
|
340
|
-
|
|
341
|
-
sheet_width = column_edges[-1]
|
|
342
|
-
sheet_height = row_edges[-1]
|
|
343
|
-
out_of_bounds: list[dict[str, Any]] = []
|
|
344
|
-
content_overlaps: list[dict[str, Any]] = []
|
|
345
|
-
for rectangle in rectangles:
|
|
346
|
-
overflow = {
|
|
347
|
-
"left": round(max(0.0, -rectangle["left"]), 2),
|
|
348
|
-
"top": round(max(0.0, -rectangle["top"]), 2),
|
|
349
|
-
"right": round(max(0.0, rectangle["right"] - sheet_width), 2),
|
|
350
|
-
"bottom": round(max(0.0, rectangle["bottom"] - sheet_height), 2),
|
|
351
|
-
}
|
|
352
|
-
if any(overflow.values()):
|
|
353
|
-
out_of_bounds.append({**chart_context(rectangle), "overflow_px": overflow})
|
|
354
|
-
|
|
355
|
-
covered_range = rectangle_cell_range(rectangle, row_edges, column_edges)
|
|
356
|
-
if not covered_range:
|
|
357
|
-
continue
|
|
358
|
-
cells_data = envelope_data(
|
|
359
|
-
run_sheets(
|
|
360
|
-
"+cells-get",
|
|
361
|
-
**locator,
|
|
362
|
-
sheet_id=sheet_id,
|
|
363
|
-
flags={"range": covered_range, "include": "value,formula,comment"},
|
|
364
|
-
timeout=timeout,
|
|
365
|
-
)
|
|
366
|
-
)
|
|
367
|
-
count, samples, truncated = non_empty_cells(cells_data, sample_limit)
|
|
368
|
-
if truncated:
|
|
369
|
-
unverifiable.append(
|
|
370
|
-
{"chart_id": rectangle["chart_id"], "reason": f"cells-get truncated for {covered_range}"}
|
|
371
|
-
)
|
|
372
|
-
if count:
|
|
373
|
-
content_overlaps.append(
|
|
374
|
-
{
|
|
375
|
-
**chart_context(rectangle),
|
|
376
|
-
"covered_range": covered_range,
|
|
377
|
-
"non_empty_cell_count": count,
|
|
378
|
-
"sample_cells": samples,
|
|
379
|
-
}
|
|
380
|
-
)
|
|
381
|
-
|
|
382
|
-
issue_count = len(overlaps) + len(out_of_bounds) + len(content_overlaps)
|
|
383
|
-
return {
|
|
384
|
-
"sheet_id": sheet_id,
|
|
385
|
-
"sheet_name": title,
|
|
386
|
-
"chart_count": len(charts),
|
|
387
|
-
"sheet_size_px": {"width": round(sheet_width, 2), "height": round(sheet_height, 2)},
|
|
388
|
-
"chart_overlaps": overlaps,
|
|
389
|
-
"cell_content_overlaps": content_overlaps,
|
|
390
|
-
"out_of_visible_range": out_of_bounds,
|
|
391
|
-
"unverifiable_charts": unverifiable,
|
|
392
|
-
"issue_count": issue_count,
|
|
393
|
-
"unverifiable_count": len(unverifiable),
|
|
394
|
-
"warnings": warnings,
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
def parse_args() -> argparse.Namespace:
|
|
399
|
-
parser = argparse.ArgumentParser(
|
|
400
|
-
description="Check chart overlap, covered cell content, and worksheet boundary overflow."
|
|
401
|
-
)
|
|
402
|
-
parser.add_argument("sheet_id", help="Spreadsheet URL or spreadsheet token")
|
|
403
|
-
parser.add_argument("--worksheet-id", help="Only check this worksheet reference_id")
|
|
404
|
-
parser.add_argument("--timeout", type=int, default=60)
|
|
405
|
-
parser.add_argument("--sample-limit", type=int, default=10)
|
|
406
|
-
return parser.parse_args()
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
def success_envelope(results: list[dict[str, Any]]) -> dict[str, Any]:
|
|
410
|
-
issue_count = sum(result["issue_count"] for result in results)
|
|
411
|
-
unverifiable_count = sum(result["unverifiable_count"] for result in results)
|
|
412
|
-
warnings = [
|
|
413
|
-
f"{result['sheet_name'] or result['sheet_id']}: {warning}"
|
|
414
|
-
for result in results
|
|
415
|
-
for warning in result["warnings"]
|
|
416
|
-
]
|
|
417
|
-
return {
|
|
418
|
-
"ok": True,
|
|
419
|
-
"engine": "lark",
|
|
420
|
-
"action": ACTION,
|
|
421
|
-
"data": {
|
|
422
|
-
"passed": issue_count == 0 and unverifiable_count == 0,
|
|
423
|
-
"scope_note": "out_of_visible_range checks worksheet drawable bounds, not a device-specific browser viewport",
|
|
424
|
-
"summary": {
|
|
425
|
-
"worksheet_count": len(results),
|
|
426
|
-
"chart_count": sum(result["chart_count"] for result in results),
|
|
427
|
-
"issue_count": issue_count,
|
|
428
|
-
"unverifiable_count": unverifiable_count,
|
|
429
|
-
},
|
|
430
|
-
"sheets": results,
|
|
431
|
-
},
|
|
432
|
-
"warnings": warnings,
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
def report_exit_code(report: dict[str, Any]) -> int:
|
|
437
|
-
if report["data"]["passed"]:
|
|
438
|
-
return 0
|
|
439
|
-
if report["data"]["summary"]["issue_count"] > 0:
|
|
440
|
-
return 2
|
|
441
|
-
return 1
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
def main() -> None:
|
|
445
|
-
args = parse_args()
|
|
446
|
-
locator = _locator(args.sheet_id)
|
|
447
|
-
try:
|
|
448
|
-
workbook_data = envelope_data(
|
|
449
|
-
run_sheets("+workbook-info", **locator, timeout=args.timeout)
|
|
450
|
-
)
|
|
451
|
-
sheets = resolve_target_sheets(workbook_data, sheet_id=args.worksheet_id)
|
|
452
|
-
if not args.worksheet_id:
|
|
453
|
-
sheets = [sheet for sheet in sheets if not bool(sheet.get("is_hidden"))]
|
|
454
|
-
if not sheets:
|
|
455
|
-
raise LarkCliError("No visible worksheet matched")
|
|
456
|
-
results = [
|
|
457
|
-
check_sheet(locator, sheet, timeout=args.timeout, sample_limit=args.sample_limit)
|
|
458
|
-
for sheet in sheets
|
|
459
|
-
]
|
|
460
|
-
except (LarkCliError, KeyError, TypeError, ValueError) as exc:
|
|
461
|
-
emit_error(ACTION, str(exc))
|
|
462
|
-
raise SystemExit(1) from exc
|
|
463
|
-
|
|
464
|
-
report = success_envelope(results)
|
|
465
|
-
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
466
|
-
exit_code = report_exit_code(report)
|
|
467
|
-
if exit_code:
|
|
468
|
-
raise SystemExit(exit_code)
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
if __name__ == "__main__":
|
|
472
|
-
main()
|