@amaster.ai/pi-lark 0.1.2-beta.48 → 0.1.2-beta.50

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.
Files changed (37) hide show
  1. package/package.json +2 -2
  2. package/skills/lark-apps/SKILL.md +1 -0
  3. package/skills/lark-apps/references/lark-apps-cache.md +61 -0
  4. package/skills/lark-base/SKILL.md +12 -5
  5. package/skills/lark-base/references/lark-base-data-query.md +11 -4
  6. package/skills/lark-base/references/lark-base-form-questions-create.md +6 -3
  7. package/skills/lark-base/references/lark-base-role-guide.md +11 -0
  8. package/skills/lark-base/references/role-config.md +31 -5
  9. package/skills/lark-calendar/SKILL.md +2 -2
  10. package/skills/lark-calendar/references/lark-calendar-recurring.md +1 -0
  11. package/skills/lark-contact/SKILL.md +1 -1
  12. package/skills/lark-drive/SKILL.md +16 -43
  13. package/skills/lark-drive/references/lark-drive-add-comment.md +2 -4
  14. package/skills/lark-drive/references/lark-drive-add-reply.md +47 -0
  15. package/skills/lark-drive/references/lark-drive-apply-permission.md +2 -2
  16. package/skills/lark-drive/references/lark-drive-batch-query-comments.md +46 -0
  17. package/skills/lark-drive/references/lark-drive-comment-content.md +50 -0
  18. package/skills/lark-drive/references/lark-drive-comment-location.md +7 -13
  19. package/skills/lark-drive/references/lark-drive-delete-reply.md +48 -0
  20. package/skills/lark-drive/references/lark-drive-download.md +5 -1
  21. package/skills/lark-drive/references/lark-drive-list-comments.md +25 -68
  22. package/skills/lark-drive/references/lark-drive-list-replies.md +54 -0
  23. package/skills/lark-drive/references/lark-drive-member-add.md +2 -2
  24. package/skills/lark-drive/references/lark-drive-member-list.md +2 -2
  25. package/skills/lark-drive/references/lark-drive-permission-get-setting.md +2 -2
  26. package/skills/lark-drive/references/lark-drive-preview.md +11 -1
  27. package/skills/lark-drive/references/lark-drive-react-reply.md +51 -0
  28. package/skills/lark-drive/references/lark-drive-reactions.md +27 -25
  29. package/skills/lark-drive/references/lark-drive-resolve-comment.md +45 -0
  30. package/skills/lark-drive/references/lark-drive-restore-comment.md +46 -0
  31. package/skills/lark-drive/references/lark-drive-update-reply.md +46 -0
  32. package/skills/lark-drive/references/lark-drive-workflow-permission-governance.md +1 -1
  33. package/skills/lark-slides/references/lark-slides-media-upload.md +1 -1
  34. package/skills/lark-slides/scripts/sxsd_validator.py +908 -0
  35. package/skills/lark-slides/scripts/xml_text_overlap_lint.py +209 -92
  36. package/skills/lark-slides/scripts/xml_text_overlap_lint_test.py +1079 -42
  37. package/skills/lark-drive/references/lark-drive-comments-guide.md +0 -80
@@ -5,6 +5,7 @@
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ import copy
8
9
  import json
9
10
  import math
10
11
  import re
@@ -16,6 +17,8 @@ from difflib import SequenceMatcher, get_close_matches
16
17
  from pathlib import Path
17
18
  from typing import Any
18
19
 
20
+ import sxsd_validator
21
+
19
22
 
20
23
  XS_NS = "{http://www.w3.org/2001/XMLSchema}"
21
24
  XML_NS = "{http://www.w3.org/XML/1998/namespace}"
@@ -44,9 +47,8 @@ ROUNDTRIP_SXSD_ATTRS = {
44
47
  ("chartData", "isStaticData"),
45
48
  }
46
49
  # Slides readback echoes each chartField's CSV text as per-value <chartParsedValues> children;
47
- # it's server-emitted, absent from the write schema, and appears on virtually every chart-bearing
48
- # deck, so treating it as an unsupported tag would block per-slide linting document-wide.
49
- ROUNDTRIP_SXSD_TAGS = {"chartParsedValues"}
50
+ # it is server-emitted and absent from the write schema, so it must not block page linting.
51
+ ROUNDTRIP_SXSD_TAGS = {("chartField", "chartParsedValues")}
50
52
  DEFAULT_TABLE_COLUMN_WIDTH = 110
51
53
  DEFAULT_TABLE_ROW_HEIGHT = 37
52
54
  DEFAULT_TEXT_LINE_SPACING_MULTIPLE = 1.5
@@ -310,77 +312,13 @@ def xml_namespace(tag: str) -> str | None:
310
312
  return tag.split("}", 1)[0] + "}" if tag.startswith("{") else None
311
313
 
312
314
 
313
- def strip_xsd_prefix(value: str | None) -> str | None:
314
- if value is None:
315
- return None
316
- return value.rsplit(":", 1)[-1]
317
-
318
-
319
- def iter_direct_xsd_children(element: ET.Element, local_name: str) -> list[ET.Element]:
320
- return [child for child in element if child.tag == f"{XS_NS}{local_name}"]
321
-
322
-
323
315
  def load_sxsd_tag_attributes() -> dict[str, set[str]]:
324
316
  global _SXSD_TAG_ATTRIBUTES_CACHE
325
317
  if _SXSD_TAG_ATTRIBUTES_CACHE is not None:
326
318
  return _SXSD_TAG_ATTRIBUTES_CACHE
327
319
 
328
- schema_root = ET.parse(SXSD_SCHEMA_PATH).getroot()
329
- named_complex_types = {
330
- complex_type.attrib["name"]: complex_type
331
- for complex_type in schema_root.findall(f"{XS_NS}complexType")
332
- if complex_type.attrib.get("name")
333
- }
334
- resolving: set[str] = set()
335
-
336
- def attributes_for_complex_type(complex_type: ET.Element) -> set[str]:
337
- attrs: set[str] = {
338
- attribute.attrib["name"]
339
- for attribute in iter_direct_xsd_children(complex_type, "attribute")
340
- if attribute.attrib.get("name")
341
- }
342
- for content_name in ("simpleContent", "complexContent"):
343
- for complex_content in iter_direct_xsd_children(complex_type, content_name):
344
- for extension in iter_direct_xsd_children(complex_content, "extension"):
345
- base_type = strip_xsd_prefix(extension.attrib.get("base"))
346
- if base_type:
347
- attrs.update(attributes_for_type(base_type))
348
- attrs.update(
349
- attribute.attrib["name"]
350
- for attribute in iter_direct_xsd_children(extension, "attribute")
351
- if attribute.attrib.get("name")
352
- )
353
- return attrs
354
-
355
- def attributes_for_type(type_name: str) -> set[str]:
356
- if type_name in resolving:
357
- return set()
358
- complex_type = named_complex_types.get(type_name)
359
- if complex_type is None:
360
- return set()
361
- resolving.add(type_name)
362
- try:
363
- return attributes_for_complex_type(complex_type)
364
- finally:
365
- resolving.remove(type_name)
366
-
367
- tag_attributes: dict[str, set[str]] = {}
368
- for element in schema_root.iter(f"{XS_NS}element"):
369
- tag_name = element.attrib.get("name")
370
- if not tag_name:
371
- continue
372
-
373
- attrs: set[str] = set()
374
- type_name = strip_xsd_prefix(element.attrib.get("type"))
375
- if type_name:
376
- attrs.update(attributes_for_type(type_name))
377
- for complex_type in iter_direct_xsd_children(element, "complexType"):
378
- attrs.update(attributes_for_complex_type(complex_type))
379
-
380
- tag_attributes.setdefault(tag_name, set()).update(attrs)
381
-
382
- _SXSD_TAG_ATTRIBUTES_CACHE = tag_attributes
383
- return tag_attributes
320
+ _SXSD_TAG_ATTRIBUTES_CACHE = sxsd_validator.load_tag_attributes(SXSD_SCHEMA_PATH)
321
+ return _SXSD_TAG_ATTRIBUTES_CACHE
384
322
 
385
323
 
386
324
  def load_iconpark_icon_types() -> set[str]:
@@ -417,13 +355,19 @@ def build_sxsd_tag_hint(tag_name: str, supported_tags: set[str]) -> str:
417
355
  return "Unsupported SXSD tag. Use only tags defined in slides_xml_schema_definition.xml."
418
356
 
419
357
 
420
- def build_sxsd_attr_hint(tag_name: str, attr_name: str, allowed_attrs: set[str]) -> str:
358
+ def suggest_sxsd_attrs(attr_name: str, allowed_attrs: set[str]) -> list[str]:
421
359
  alias = SXSD_ATTR_ALIASES.get(attr_name)
422
360
  if alias and alias in allowed_attrs:
423
- return f'Use "{alias}" on <{tag_name}> instead of "{attr_name}".'
424
- close_matches = get_close_matches(attr_name, sorted(allowed_attrs), n=3, cutoff=0.68)
425
- if close_matches:
426
- return "Unsupported SXSD attribute. Did you mean " + ", ".join(f'"{match}"' for match in close_matches) + "?"
361
+ return [alias]
362
+ return get_close_matches(attr_name, sorted(allowed_attrs), n=3, cutoff=0.68)
363
+
364
+
365
+ def build_sxsd_attr_hint(tag_name: str, attr_name: str, allowed_attrs: set[str]) -> str:
366
+ suggestions = suggest_sxsd_attrs(attr_name, allowed_attrs)
367
+ if suggestions:
368
+ if SXSD_ATTR_ALIASES.get(attr_name) == suggestions[0]:
369
+ return f'Use "{suggestions[0]}" on <{tag_name}> instead of "{attr_name}".'
370
+ return "Unsupported SXSD attribute. Did you mean " + ", ".join(f'"{match}"' for match in suggestions) + "?"
427
371
  allowed_summary = ", ".join(sorted(allowed_attrs)[:8])
428
372
  if len(allowed_attrs) > 8:
429
373
  allowed_summary += ", ..."
@@ -438,10 +382,33 @@ def should_skip_sxsd_attribute(tag_name: str, attr_name: str) -> bool:
438
382
  return attr_name in SERVER_FILLED_SXSD_ATTRS or (tag_name, attr_name) in ROUNDTRIP_SXSD_ATTRS
439
383
 
440
384
 
441
- def validate_sxsd_tag_attributes(root: ET.Element) -> list[dict[str, Any]]:
385
+ def should_skip_sxsd_tag(parent_name: str | None, tag_name: str) -> bool:
386
+ return (parent_name, tag_name) in ROUNDTRIP_SXSD_TAGS
387
+
388
+
389
+ def without_server_filled_sxsd_fields(root: ET.Element) -> ET.Element:
390
+ sanitized_root = copy.deepcopy(root)
391
+
392
+ def sanitize(element: ET.Element) -> None:
393
+ tag_name = xml_local_name(element.tag)
394
+ for raw_attr_name in list(element.attrib):
395
+ if should_skip_sxsd_attribute(tag_name, xml_local_name(raw_attr_name)):
396
+ del element.attrib[raw_attr_name]
397
+ for child in list(element):
398
+ if should_skip_sxsd_tag(tag_name, xml_local_name(child.tag)):
399
+ element.remove(child)
400
+ continue
401
+ sanitize(child)
402
+
403
+ sanitize(sanitized_root)
404
+ return sanitized_root
405
+
406
+
407
+ def validate_sxsd_document(xml: str, root: ET.Element) -> list[dict[str, Any]]:
442
408
  tag_attributes = load_sxsd_tag_attributes()
443
409
  supported_tags = set(tag_attributes)
444
410
  issues: list[dict[str, Any]] = []
411
+ suggested_attr_candidates: dict[tuple[str, str], list[set[str]]] = {}
445
412
 
446
413
  def visit(element: ET.Element, ancestors: list[str], path: str) -> None:
447
414
  if should_skip_sxsd_subtree(element, ancestors):
@@ -449,7 +416,8 @@ def validate_sxsd_tag_attributes(root: ET.Element) -> list[dict[str, Any]]:
449
416
 
450
417
  tag_name = xml_local_name(element.tag)
451
418
  current_path = f"{path}/{tag_name}" if path else tag_name
452
- if tag_name in ROUNDTRIP_SXSD_TAGS:
419
+ parent_name = ancestors[-1] if ancestors else None
420
+ if should_skip_sxsd_tag(parent_name, tag_name):
453
421
  return
454
422
  if tag_name not in supported_tags:
455
423
  issues.append(
@@ -473,6 +441,11 @@ def validate_sxsd_tag_attributes(root: ET.Element) -> list[dict[str, Any]]:
473
441
  continue
474
442
  if attr_name in allowed_attrs:
475
443
  continue
444
+ suggestions = suggest_sxsd_attrs(attr_name, allowed_attrs)
445
+ if suggestions:
446
+ suggested_attr_candidates.setdefault((current_path, tag_name), []).append(
447
+ set(suggestions)
448
+ )
476
449
  issues.append(
477
450
  {
478
451
  "level": "error",
@@ -489,6 +462,76 @@ def validate_sxsd_tag_attributes(root: ET.Element) -> list[dict[str, Any]]:
489
462
  visit(child, [*ancestors, tag_name], current_path)
490
463
 
491
464
  visit(root, [], "")
465
+ existing = {
466
+ (issue.get("code"), issue.get("path"), issue.get("tag"), issue.get("attr"))
467
+ for issue in issues
468
+ }
469
+ unsupported_tag_locations = {
470
+ (issue.get("path"), issue.get("tag"))
471
+ for issue in issues
472
+ if issue.get("code") == "sxsd_unsupported_tag"
473
+ }
474
+ schema_issues = _validate_sxsd_schema_constraints(xml, root)
475
+ missing_attrs_by_location: dict[tuple[str, str], set[str]] = {}
476
+ for schema_issue in schema_issues:
477
+ if schema_issue.get("code") != "sxsd_missing_required_attr":
478
+ continue
479
+ location = (schema_issue.get("path"), schema_issue.get("tag"))
480
+ missing_attrs_by_location.setdefault(location, set()).add(schema_issue.get("attr"))
481
+
482
+ suggested_attrs: set[tuple[str, str, str]] = set()
483
+ for location, candidate_groups in suggested_attr_candidates.items():
484
+ missing_attrs = missing_attrs_by_location.get(location, set())
485
+ for candidates in candidate_groups:
486
+ matching_missing_attrs = candidates & missing_attrs
487
+ if len(matching_missing_attrs) == 1:
488
+ suggested_attrs.add((*location, next(iter(matching_missing_attrs))))
489
+
490
+ for schema_issue in schema_issues:
491
+ if schema_issue.get("code") == "sxsd_unexpected_child" and (
492
+ schema_issue.get("path"),
493
+ schema_issue.get("tag"),
494
+ ) in unsupported_tag_locations:
495
+ continue
496
+ if schema_issue.get("code") == "sxsd_missing_required_attr" and (
497
+ schema_issue.get("path"),
498
+ schema_issue.get("tag"),
499
+ schema_issue.get("attr"),
500
+ ) in suggested_attrs:
501
+ continue
502
+ key = (
503
+ schema_issue.get("code"),
504
+ schema_issue.get("path"),
505
+ schema_issue.get("tag"),
506
+ schema_issue.get("attr"),
507
+ )
508
+ if key not in existing:
509
+ issues.append(schema_issue)
510
+ return issues
511
+
512
+
513
+ def _validate_sxsd_schema_constraints(xml: str, root: ET.Element) -> list[dict[str, Any]]:
514
+ issues: list[dict[str, Any]] = []
515
+ if re.match(r"^\s*<\?xml\b", xml):
516
+ issues.append(
517
+ {
518
+ "level": "error",
519
+ "code": "sxsd_unsupported_declaration",
520
+ "path": xml_local_name(root.tag),
521
+ "tag": xml_local_name(root.tag),
522
+ "expected": "SXSD document without an XML declaration",
523
+ "actual": "<?xml ...?>",
524
+ "message": "XML declarations are not supported by the Slides SXSD write format",
525
+ "hint": "Remove the <?xml ...?> declaration and keep the SXSD root element.",
526
+ }
527
+ )
528
+
529
+ issues.extend(
530
+ sxsd_validator.validate_sxsd(
531
+ without_server_filled_sxsd_fields(root),
532
+ SXSD_SCHEMA_PATH,
533
+ )
534
+ )
492
535
  return issues
493
536
 
494
537
 
@@ -692,18 +735,39 @@ def validate_xml_well_formed(xml: str) -> dict[str, Any] | None:
692
735
  return xml_error
693
736
 
694
737
 
695
- def parse_presentation(xml: str) -> dict[str, Any]:
696
- presentation_match = re.search(r"<presentation\b([^>]*)>", xml)
697
- if presentation_match:
698
- return {
699
- "width": int(float(extract_attribute(presentation_match.group(1), "width") or 960)),
700
- "height": int(float(extract_attribute(presentation_match.group(1), "height") or 540)),
701
- "slides": re.findall(r"<slide\b[\s\S]*?</slide>", xml),
738
+ def serialize_slide_for_layout(slide_root: ET.Element) -> str:
739
+ slide_copy = copy.deepcopy(slide_root)
740
+ for element in slide_copy.iter():
741
+ if not isinstance(element.tag, str):
742
+ continue
743
+ element.tag = xml_local_name(element.tag)
744
+ attributes = {
745
+ xml_local_name(attribute_name): value
746
+ for attribute_name, value in element.attrib.items()
702
747
  }
703
- slide_match = re.findall(r"<slide\b[\s\S]*?</slide>", xml)
704
- if slide_match:
705
- return {"width": 960, "height": 540, "slides": slide_match}
706
- fail("input must contain a <presentation> or <slide> root")
748
+ element.attrib.clear()
749
+ element.attrib.update(attributes)
750
+ return ET.tostring(slide_copy, encoding="unicode")
751
+
752
+
753
+ def parse_presentation(root: ET.Element) -> dict[str, Any]:
754
+ root_name = xml_local_name(root.tag)
755
+ if root_name == "slide":
756
+ slide_roots = [root]
757
+ width = 960
758
+ height = 540
759
+ elif root_name == "presentation":
760
+ slide_roots = [child for child in root if xml_local_name(child.tag) == "slide"]
761
+ width = int(float(root.attrib.get("width", 960)))
762
+ height = int(float(root.attrib.get("height", 540)))
763
+ else:
764
+ fail("input must contain a <presentation> or <slide> root")
765
+ return {
766
+ "width": width,
767
+ "height": height,
768
+ "slides": [serialize_slide_for_layout(slide_root) for slide_root in slide_roots],
769
+ "slide_roots": slide_roots,
770
+ }
707
771
 
708
772
 
709
773
  def extract_elements(slide_xml: str) -> list[dict[str, Any]]:
@@ -2417,6 +2481,21 @@ def slide_status(errors: list[dict[str, Any]], warnings: list[dict[str, Any]]) -
2417
2481
  return "passed"
2418
2482
 
2419
2483
 
2484
+ def is_slide_scoped_sxsd_issue(issue: dict[str, Any], root_name: str) -> bool:
2485
+ if issue.get("code") == "sxsd_unsupported_declaration":
2486
+ return False
2487
+ if root_name == "slide":
2488
+ return True
2489
+ path = issue.get("path")
2490
+ if not isinstance(path, str):
2491
+ return False
2492
+ if path.startswith("presentation/slide/"):
2493
+ return True
2494
+ return path == "presentation/slide" and (
2495
+ issue.get("attr") is not None or issue.get("code") == "sxsd_invalid_namespace"
2496
+ )
2497
+
2498
+
2420
2499
  def build_result(
2421
2500
  source_path: str | None,
2422
2501
  slide_size: dict[str, int | float],
@@ -2472,11 +2551,20 @@ def lint_xml(xml: str, source_path: str | None = None) -> dict[str, Any]:
2472
2551
  raise AssertionError("parse_xml_root must return a root or error")
2473
2552
 
2474
2553
  namespace_issues = validate_sml_tag_prefixes(xml)
2475
- sxsd_issues = validate_sxsd_tag_attributes(root)
2554
+ root_name = xml_local_name(root.tag)
2555
+ sxsd_issues = validate_sxsd_document(xml, root)
2476
2556
  iconpark_issues = validate_iconpark_icon_types(root)
2477
2557
  top_level_issues = [
2478
2558
  normalize_issue(issue, None, {})
2479
- for issue in [*namespace_issues, *sxsd_issues, *iconpark_issues]
2559
+ for issue in [
2560
+ *namespace_issues,
2561
+ *[
2562
+ issue
2563
+ for issue in sxsd_issues
2564
+ if not is_slide_scoped_sxsd_issue(issue, root_name)
2565
+ ],
2566
+ *iconpark_issues,
2567
+ ]
2480
2568
  ]
2481
2569
  if any(issue["level"] == "error" for issue in top_level_issues):
2482
2570
  return build_result(
@@ -2486,10 +2574,36 @@ def lint_xml(xml: str, source_path: str | None = None) -> dict[str, Any]:
2486
2574
  [],
2487
2575
  )
2488
2576
 
2489
- presentation = parse_presentation(xml)
2577
+ presentation = parse_presentation(root)
2578
+ slide_roots = presentation["slide_roots"]
2490
2579
  slides: list[dict[str, Any]] = []
2491
2580
  for index, slide_xml in enumerate(presentation["slides"]):
2492
2581
  slide_number = index + 1
2582
+ slide_root = slide_roots[index]
2583
+ slide_sxsd_issues = [
2584
+ normalize_issue(issue, slide_number, {})
2585
+ for issue in validate_sxsd_document(slide_xml, slide_root)
2586
+ ]
2587
+ slide_sxsd_errors = [
2588
+ issue for issue in slide_sxsd_issues if issue["level"] == "error"
2589
+ ]
2590
+ if slide_sxsd_errors:
2591
+ slide_sxsd_warnings = [
2592
+ issue for issue in slide_sxsd_issues if issue["level"] == "warning"
2593
+ ]
2594
+ slides.append(
2595
+ {
2596
+ "slide_number": slide_number,
2597
+ "status": slide_status(slide_sxsd_errors, slide_sxsd_warnings),
2598
+ "element_count": 0,
2599
+ "errors": slide_sxsd_errors,
2600
+ "warnings": slide_sxsd_warnings,
2601
+ "infos": [],
2602
+ "issues": slide_sxsd_issues,
2603
+ }
2604
+ )
2605
+ continue
2606
+
2493
2607
  geometry = lint_slide(
2494
2608
  slide_xml,
2495
2609
  slide_number,
@@ -2535,8 +2649,11 @@ def lint_xml(xml: str, source_path: str | None = None) -> dict[str, Any]:
2535
2649
  ),
2536
2650
  ]
2537
2651
  issues = [
2538
- normalize_issue(issue, slide_number, elements_by_id)
2539
- for issue in raw_issues
2652
+ *slide_sxsd_issues,
2653
+ *[
2654
+ normalize_issue(issue, slide_number, elements_by_id)
2655
+ for issue in raw_issues
2656
+ ],
2540
2657
  ]
2541
2658
  errors = [issue for issue in issues if issue["level"] == "error"]
2542
2659
  warnings = [issue for issue in issues if issue["level"] == "warning"]