sophhub 0.2.0 → 0.2.2

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 (187) hide show
  1. package/package.json +1 -1
  2. package/skills/compact-context/skill.json +20 -0
  3. package/skills/compact-context/src/SKILL.md +133 -0
  4. package/skills/compact-context/src/scripts/check.sh +381 -0
  5. package/skills/compact-context/src/scripts/set-keep-recent.mjs +1337 -0
  6. package/skills/compact-context/src/scripts/setup.sh +96 -0
  7. package/skills/feishu-notes-assistant-universal/skill.json +20 -0
  8. package/skills/feishu-notes-assistant-universal/src/README.md +55 -0
  9. package/skills/feishu-notes-assistant-universal/src/SKILL.md +159 -0
  10. package/skills/feishu-notes-assistant-universal/src/bin/linux-amd64/lark-cli-openclaw +0 -0
  11. package/skills/feishu-notes-assistant-universal/src/bin/linux-arm64/lark-cli-openclaw +0 -0
  12. package/skills/feishu-notes-assistant-universal/src/scripts/_resolve_lark_cli.py +58 -0
  13. package/skills/feishu-notes-assistant-universal/src/scripts/openclaw_meeting_minutes.py +462 -0
  14. package/skills/feishu-notes-assistant-universal/src/scripts/openclaw_notes_crud.py +547 -0
  15. package/skills/feishu-notes-assistant-universal/src/scripts/openclaw_notes_crud_test.py +181 -0
  16. package/skills/feishu-notes-assistant-universal/src/scripts/run_meeting_minutes.py +80 -0
  17. package/skills/feishu-notes-assistant-universal/src/scripts/run_meeting_minutes.sh +5 -0
  18. package/skills/feishu-notes-assistant-universal/src/scripts/run_note_crud.py +32 -0
  19. package/skills/feishu-notes-assistant-universal/src/scripts/run_note_crud.sh +5 -0
  20. package/skills/image-classify/skill.json +5 -5
  21. package/skills/image-classify/src/SKILL.md +60 -67
  22. package/skills/image-classify/src/scripts/face_search.py +400 -15
  23. package/skills/image-classify/src/scripts/send_dm_message.py +332 -0
  24. package/skills/md2pdf-converter/skill.json +20 -0
  25. package/skills/md2pdf-converter/src/SKILL.md +244 -0
  26. package/skills/md2pdf-converter/src/_meta.json +6 -0
  27. package/skills/md2pdf-converter/src/scripts/generate_emoji_mapping.py +74 -0
  28. package/skills/md2pdf-converter/src/scripts/md2pdf-local.sh +291 -0
  29. package/skills/sophnet-bot-client/skill.json +20 -0
  30. package/skills/sophnet-bot-client/src/SKILL.md +255 -0
  31. package/skills/sophnet-bot-client/src/pyproject.toml +13 -0
  32. package/skills/sophnet-bot-client/src/scripts/__init__.py +0 -0
  33. package/skills/sophnet-bot-client/src/scripts/bot_client_proxy.py +165 -0
  34. package/skills/sophnet-bot-client/src/scripts/bot_client_safe.sh +29 -0
  35. package/skills/sophnet-bot-client/src/scripts/bot_client_setup.py +502 -0
  36. package/skills/sophnet-bot-client/src/tests/__init__.py +0 -0
  37. package/skills/sophnet-bot-client/src/tests/test_bot_client_proxy.py +255 -0
  38. package/skills/sophnet-bot-client/src/tests/test_bot_client_setup.py +679 -0
  39. package/skills/sophnet-bot-client/src/uv.lock +8 -0
  40. package/skills/sophnet-docx/skill.json +20 -0
  41. package/skills/sophnet-docx/src/SKILL.md +463 -0
  42. package/skills/sophnet-docx/src/package-lock.json +208 -0
  43. package/skills/sophnet-docx/src/package.json +16 -0
  44. package/skills/sophnet-docx/src/pyproject.toml +11 -0
  45. package/skills/sophnet-docx/src/scripts/__init__.py +1 -0
  46. package/skills/sophnet-docx/src/scripts/accept_changes.py +135 -0
  47. package/skills/sophnet-docx/src/scripts/comment.py +318 -0
  48. package/skills/sophnet-docx/src/scripts/ensure_uv_env.sh +68 -0
  49. package/skills/sophnet-docx/src/scripts/office/helpers/__init__.py +0 -0
  50. package/skills/sophnet-docx/src/scripts/office/helpers/merge_runs.py +199 -0
  51. package/skills/sophnet-docx/src/scripts/office/helpers/simplify_redlines.py +197 -0
  52. package/skills/sophnet-docx/src/scripts/office/pack.py +159 -0
  53. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  54. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  55. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  56. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  57. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  58. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  59. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  60. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  61. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  62. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  63. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  64. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  65. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  66. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  67. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  68. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  69. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  70. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  71. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  72. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  73. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  74. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  75. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  76. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  77. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  78. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  79. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  80. package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  81. package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  82. package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  83. package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  84. package/skills/sophnet-docx/src/scripts/office/schemas/mce/mc.xsd +75 -0
  85. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  86. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  87. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  88. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  89. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  90. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  91. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  92. package/skills/sophnet-docx/src/scripts/office/soffice.py +183 -0
  93. package/skills/sophnet-docx/src/scripts/office/unpack.py +132 -0
  94. package/skills/sophnet-docx/src/scripts/office/validate.py +111 -0
  95. package/skills/sophnet-docx/src/scripts/office/validators/__init__.py +15 -0
  96. package/skills/sophnet-docx/src/scripts/office/validators/base.py +847 -0
  97. package/skills/sophnet-docx/src/scripts/office/validators/docx.py +446 -0
  98. package/skills/sophnet-docx/src/scripts/office/validators/pptx.py +275 -0
  99. package/skills/sophnet-docx/src/scripts/office/validators/redlining.py +247 -0
  100. package/skills/sophnet-docx/src/scripts/templates/comments.xml +3 -0
  101. package/skills/sophnet-docx/src/scripts/templates/commentsExtended.xml +3 -0
  102. package/skills/sophnet-docx/src/scripts/templates/commentsExtensible.xml +3 -0
  103. package/skills/sophnet-docx/src/scripts/templates/commentsIds.xml +3 -0
  104. package/skills/sophnet-docx/src/scripts/templates/people.xml +3 -0
  105. package/skills/sophnet-docx/src/scripts/upload_file.sh +96 -0
  106. package/skills/sophnet-docx/src/uv.lock +320 -0
  107. package/skills/sophnet-pdf/skill.json +20 -0
  108. package/skills/sophnet-pdf/src/SKILL.md +413 -0
  109. package/skills/sophnet-pdf/src/forms.md +297 -0
  110. package/skills/sophnet-pdf/src/pyproject.toml +14 -0
  111. package/skills/sophnet-pdf/src/reference.md +612 -0
  112. package/skills/sophnet-pdf/src/scripts/check_bounding_boxes.py +65 -0
  113. package/skills/sophnet-pdf/src/scripts/check_fillable_fields.py +11 -0
  114. package/skills/sophnet-pdf/src/scripts/convert_pdf_to_images.py +33 -0
  115. package/skills/sophnet-pdf/src/scripts/create_validation_image.py +37 -0
  116. package/skills/sophnet-pdf/src/scripts/enhance_tutorial.py +558 -0
  117. package/skills/sophnet-pdf/src/scripts/ensure_uv_env.sh +68 -0
  118. package/skills/sophnet-pdf/src/scripts/extract_form_field_info.py +122 -0
  119. package/skills/sophnet-pdf/src/scripts/extract_form_structure.py +115 -0
  120. package/skills/sophnet-pdf/src/scripts/extract_pdf_content.py +35 -0
  121. package/skills/sophnet-pdf/src/scripts/fill_fillable_fields.py +98 -0
  122. package/skills/sophnet-pdf/src/scripts/fill_pdf_form_with_annotations.py +107 -0
  123. package/skills/sophnet-pdf/src/scripts/upload_file.sh +88 -0
  124. package/skills/sophnet-pdf/src/uv.lock +537 -0
  125. package/skills/sophnet-xlsx/skill.json +20 -0
  126. package/skills/sophnet-xlsx/src/SKILL.md +399 -0
  127. package/skills/sophnet-xlsx/src/pyproject.toml +11 -0
  128. package/skills/sophnet-xlsx/src/scripts/ensure_uv_env.sh +68 -0
  129. package/skills/sophnet-xlsx/src/scripts/office/helpers/__init__.py +0 -0
  130. package/skills/sophnet-xlsx/src/scripts/office/helpers/merge_runs.py +199 -0
  131. package/skills/sophnet-xlsx/src/scripts/office/helpers/simplify_redlines.py +197 -0
  132. package/skills/sophnet-xlsx/src/scripts/office/pack.py +159 -0
  133. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  134. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  135. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  136. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  137. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  138. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  139. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  140. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  141. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  142. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  143. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  144. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  145. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  146. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  147. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  148. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  149. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  150. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  151. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  152. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  153. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  154. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  155. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  156. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  157. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  158. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  159. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  160. package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  161. package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  162. package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  163. package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  164. package/skills/sophnet-xlsx/src/scripts/office/schemas/mce/mc.xsd +75 -0
  165. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  166. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  167. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  168. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  169. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  170. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  171. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  172. package/skills/sophnet-xlsx/src/scripts/office/soffice.py +183 -0
  173. package/skills/sophnet-xlsx/src/scripts/office/unpack.py +132 -0
  174. package/skills/sophnet-xlsx/src/scripts/office/validate.py +111 -0
  175. package/skills/sophnet-xlsx/src/scripts/office/validators/__init__.py +15 -0
  176. package/skills/sophnet-xlsx/src/scripts/office/validators/base.py +847 -0
  177. package/skills/sophnet-xlsx/src/scripts/office/validators/docx.py +446 -0
  178. package/skills/sophnet-xlsx/src/scripts/office/validators/pptx.py +275 -0
  179. package/skills/sophnet-xlsx/src/scripts/office/validators/redlining.py +247 -0
  180. package/skills/sophnet-xlsx/src/scripts/recalc.py +184 -0
  181. package/skills/sophnet-xlsx/src/scripts/upload_file.sh +96 -0
  182. package/skills/sophnet-xlsx/src/uv.lock +319 -0
  183. package/skills/wechat-article-publisher/skill.json +20 -0
  184. package/skills/wechat-article-publisher/src/SKILL.md +60 -0
  185. package/skills/wechat-article-publisher/src/config.json +7 -0
  186. package/skills/wechat-article-publisher/src/pyproject.toml +12 -0
  187. package/skills/wechat-article-publisher/src/scripts/publish_wechat.py +825 -0
@@ -0,0 +1,332 @@
1
+ #!/usr/bin/env python3
2
+ """Send a DM by friend user_id: resolve DM room via /friend/dm, then send. JWT: /home/node/.openclaw/jwt.json."""
3
+
4
+ import argparse
5
+ import base64
6
+ import json
7
+ import mimetypes
8
+ import os
9
+ import sys
10
+ import time
11
+ import urllib.error
12
+ import urllib.parse
13
+ import urllib.request
14
+ import uuid
15
+ from typing import Dict, List, Optional, Tuple
16
+
17
+
18
+ DEFAULT_TIMEOUT = 30
19
+ DEFAULT_BASE_URL = "https://yagent.sophnet.com/api"
20
+ JWT_PATH = "/home/node/.openclaw/jwt.json"
21
+
22
+
23
+ class ApiError(RuntimeError):
24
+ pass
25
+
26
+
27
+ def configure_stdio() -> None:
28
+ for name in ("stdout", "stderr"):
29
+ stream = getattr(sys, name, None)
30
+ if stream is not None and hasattr(stream, "reconfigure"):
31
+ stream.reconfigure(encoding="utf-8", errors="replace")
32
+
33
+
34
+ def read_bearer_token(jwt_path: str) -> str:
35
+ try:
36
+ with open(jwt_path, "r", encoding="utf-8") as fp:
37
+ data = json.load(fp)
38
+ except OSError as exc:
39
+ raise ApiError(f"Cannot read JWT file {jwt_path}: {exc}") from exc
40
+ except json.JSONDecodeError as exc:
41
+ raise ApiError(f"Invalid JSON in JWT file {jwt_path}: {exc}") from exc
42
+ if not isinstance(data, dict):
43
+ raise ApiError("JWT file root must be a JSON object.")
44
+ token = data.get("web_jwt")
45
+ if not token or not isinstance(token, str):
46
+ raise ApiError("JWT file has no valid `web_jwt` field.")
47
+ token = token.strip()
48
+ if token.lower().startswith("bearer "):
49
+ token = token[7:].strip()
50
+ if not token:
51
+ raise ApiError("`web_jwt` is empty.")
52
+ return token
53
+
54
+
55
+ def jwt_exp_unix(token: str) -> Optional[int]:
56
+ """Return JWT `exp` (seconds since epoch) if present; None if unreadable."""
57
+ try:
58
+ parts = token.split(".")
59
+ if len(parts) != 3:
60
+ return None
61
+ payload_b64 = parts[1] + "=" * (-len(parts[1]) % 4)
62
+ raw = base64.urlsafe_b64decode(payload_b64.encode("ascii"))
63
+ payload = json.loads(raw)
64
+ exp = payload.get("exp")
65
+ if isinstance(exp, (int, float)):
66
+ return int(exp)
67
+ except (ValueError, UnicodeDecodeError, json.JSONDecodeError, IndexError):
68
+ pass
69
+ return None
70
+
71
+
72
+ def ensure_jwt_valid_for_use(token: str, jwt_path: str, clock_skew_sec: int = 60) -> None:
73
+ """Fail fast if JWT is already expired (common cause of HTTP 401 / 请先登录)."""
74
+ exp = jwt_exp_unix(token)
75
+ if exp is None:
76
+ return
77
+ now = int(time.time())
78
+ if now >= exp + clock_skew_sec:
79
+ raise ApiError(
80
+ f"JWT in {jwt_path} has expired (exp={exp}, now={now}). "
81
+ "Log in again in the app or refresh the token, then update the file."
82
+ )
83
+
84
+
85
+ def normalize_base_url(url: str) -> str:
86
+ url = url.strip()
87
+ if not url:
88
+ raise ApiError("Base URL cannot be empty.")
89
+ return url.rstrip("/")
90
+
91
+
92
+ def resolve_files(raw: Optional[List[str]]) -> List[str]:
93
+ if not raw:
94
+ return []
95
+ cwd = os.getcwd()
96
+ out: List[str] = []
97
+ for item in raw:
98
+ if not isinstance(item, str) or not item.strip():
99
+ raise ApiError("Each --file path must be non-empty.")
100
+ p = item if os.path.isabs(item) else os.path.join(cwd, item)
101
+ out.append(os.path.realpath(p))
102
+ return out
103
+
104
+
105
+ def make_headers(token: str, content_type: Optional[str] = None) -> Dict[str, str]:
106
+ h = {"Authorization": f"Bearer {token}", "Accept": "application/json"}
107
+ if content_type:
108
+ h["Content-Type"] = content_type
109
+ return h
110
+
111
+
112
+ def read_http_body(exc: urllib.error.HTTPError) -> str:
113
+ try:
114
+ return exc.read().decode("utf-8", errors="replace")
115
+ except Exception:
116
+ return str(exc)
117
+
118
+
119
+ def http_api_error_hint(http_code: int, body: str) -> str:
120
+ """Append short hints for known API error bodies (JWT, RBAC)."""
121
+ try:
122
+ err = json.loads(body)
123
+ except json.JSONDecodeError:
124
+ if http_code == 401:
125
+ return f" (check web_jwt in {JWT_PATH}.)"
126
+ return ""
127
+ api_st = err.get("status")
128
+ msg = str(err.get("message", ""))
129
+ if http_code == 401 and (api_st == 10025 or "登录" in msg):
130
+ return f" (renew web_jwt in {JWT_PATH}.)"
131
+ if api_st == 20002 or "权限不足" in msg:
132
+ return (
133
+ " (insufficient permission: this account cannot send in this room/channel; "
134
+ "ask an org admin to grant access or send from an allowed account.)"
135
+ )
136
+ return ""
137
+
138
+
139
+ def request_json(
140
+ method: str,
141
+ url: str,
142
+ token: str,
143
+ timeout: int,
144
+ payload: Optional[dict] = None,
145
+ ) -> dict:
146
+ body = None
147
+ headers = make_headers(token)
148
+ if payload is not None:
149
+ body = json.dumps(payload).encode("utf-8")
150
+ headers["Content-Type"] = "application/json"
151
+ req = urllib.request.Request(url=url, data=body, headers=headers, method=method)
152
+ try:
153
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
154
+ raw = resp.read().decode("utf-8")
155
+ except urllib.error.HTTPError as exc:
156
+ body = read_http_body(exc)
157
+ raise ApiError(f"HTTP {exc.code} {url}: {body}{http_api_error_hint(exc.code, body)}")
158
+ except urllib.error.URLError as exc:
159
+ raise ApiError(f"Request failed for {url}: {exc}")
160
+ try:
161
+ data = json.loads(raw)
162
+ except json.JSONDecodeError as exc:
163
+ raise ApiError(f"Invalid JSON from {url}: {exc}\n{raw}")
164
+ if data.get("status") != 0:
165
+ raise ApiError(
166
+ f"API error for {url}: status={data.get('status')} message={data.get('message')!r}"
167
+ )
168
+ return data
169
+
170
+
171
+ def multipart_file(file_name: str, file_bytes: bytes, content_type: str) -> Tuple[bytes, str]:
172
+ boundary = f"----oc{uuid.uuid4().hex}"
173
+ b = boundary.encode("ascii")
174
+ parts: List[bytes] = [
175
+ b"--" + b + b"\r\n",
176
+ (
177
+ f'Content-Disposition: form-data; name="file"; filename="{file_name}"\r\n'
178
+ ).encode("utf-8"),
179
+ f"Content-Type: {content_type}\r\n\r\n".encode("utf-8"),
180
+ file_bytes,
181
+ b"\r\n",
182
+ b"--" + b + b"--\r\n",
183
+ ]
184
+ return b"".join(parts), f"multipart/form-data; boundary={boundary}"
185
+
186
+
187
+ def upload_asset(base_url: str, token: str, rid: str, file_path: str, timeout: int) -> dict:
188
+ if not os.path.isfile(file_path):
189
+ raise ApiError(f"Attachment not found: {file_path}")
190
+ base_name = os.path.basename(file_path)
191
+ mime = mimetypes.guess_type(base_name)[0] or "application/octet-stream"
192
+ with open(file_path, "rb") as fp:
193
+ blob = fp.read()
194
+ body, ctype = multipart_file(base_name, blob, mime)
195
+ url = f"{base_url}/sys/openclaw/im/chat.uploadAsset?{urllib.parse.urlencode({'rid': rid})}"
196
+ req = urllib.request.Request(
197
+ url=url,
198
+ data=body,
199
+ headers=make_headers(token, content_type=ctype),
200
+ method="POST",
201
+ )
202
+ try:
203
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
204
+ raw = resp.read().decode("utf-8")
205
+ except urllib.error.HTTPError as exc:
206
+ b = read_http_body(exc)
207
+ raise ApiError(
208
+ f"HTTP {exc.code} while uploading {file_path}: {b}{http_api_error_hint(exc.code, b)}"
209
+ )
210
+ except urllib.error.URLError as exc:
211
+ raise ApiError(f"Upload failed for {file_path}: {exc}")
212
+ try:
213
+ data = json.loads(raw)
214
+ except json.JSONDecodeError as exc:
215
+ raise ApiError(f"Invalid upload response for {file_path}: {exc}\n{raw}")
216
+ if data.get("status") != 0:
217
+ raise ApiError(
218
+ f"Upload API error: status={data.get('status')} message={data.get('message')!r}"
219
+ )
220
+ asset = (data.get("result") or {}).get("asset") or {}
221
+ url_path = asset.get("urlPath")
222
+ if not url_path:
223
+ raise ApiError(f"Upload OK but missing result.asset.urlPath for {file_path}.")
224
+ return {
225
+ "name": base_name,
226
+ "type": mime,
227
+ "size": os.path.getsize(file_path),
228
+ "url": url_path,
229
+ }
230
+
231
+
232
+ def get_or_create_dm(base_url: str, token: str, friend_id: int, timeout: int) -> str:
233
+ url = f"{base_url}/sys/openclaw/friend/dm"
234
+ data = request_json("POST", url, token, timeout, payload={"friendId": friend_id})
235
+ result = data.get("result") or {}
236
+ channel = result.get("channel") or {}
237
+ rid = channel.get("_id")
238
+ if not rid:
239
+ raise ApiError("DM creation succeeded, but `result.channel._id` is missing.")
240
+ return str(rid)
241
+
242
+
243
+ def send_message(
244
+ base_url: str,
245
+ token: str,
246
+ rid: str,
247
+ text: str,
248
+ attachments: List[dict],
249
+ timeout: int,
250
+ ) -> dict:
251
+ url = f"{base_url}/sys/openclaw/im/chat.sendMessage"
252
+ payload: Dict[str, object] = {"rid": rid}
253
+ if text:
254
+ payload["msg"] = text
255
+ if attachments:
256
+ payload["attachments"] = attachments
257
+ if "msg" not in payload and "attachments" not in payload:
258
+ raise ApiError("Message text and attachments cannot both be empty.")
259
+ data = request_json("POST", url, token, timeout, payload=payload)
260
+ return (data.get("result") or {}).get("message") or {}
261
+
262
+
263
+ def main() -> int:
264
+ p = argparse.ArgumentParser(
265
+ description="Send DM to a user by user_id (resolves DM via /friend/dm). JWT: %s." % JWT_PATH
266
+ )
267
+ p.add_argument(
268
+ "--user-id",
269
+ type=int,
270
+ required=True,
271
+ metavar="UID",
272
+ help="Target friend userId; script calls /sys/openclaw/friend/dm to get room id.",
273
+ )
274
+ p.add_argument("-m", "--message", default="", help="Text (use --file if no text)")
275
+ p.add_argument("--file", action="append", dest="files", metavar="PATH", help="Attachment; repeat")
276
+ p.add_argument("--base-url", default=DEFAULT_BASE_URL, help="API base URL")
277
+ p.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT, help="HTTP timeout (seconds)")
278
+ p.add_argument(
279
+ "--allow-expired-jwt",
280
+ action="store_true",
281
+ help="Skip local JWT exp check (server will still reject invalid tokens).",
282
+ )
283
+ args = p.parse_args()
284
+ configure_stdio()
285
+
286
+ try:
287
+ if args.user_id <= 0:
288
+ raise ApiError("`--user-id` must be a positive integer.")
289
+ token = read_bearer_token(JWT_PATH)
290
+ if args.allow_expired_jwt:
291
+ exp = jwt_exp_unix(token)
292
+ if exp is not None and int(time.time()) >= exp + 60:
293
+ print(
294
+ "WARNING: JWT is expired; request will likely fail with 401. "
295
+ "Update web_jwt in %s after re-login." % JWT_PATH,
296
+ file=sys.stderr,
297
+ )
298
+ else:
299
+ ensure_jwt_valid_for_use(token, JWT_PATH)
300
+ base_url = normalize_base_url(args.base_url)
301
+ text = args.message or ""
302
+ files = resolve_files(args.files)
303
+ if not text.strip() and not files:
304
+ raise ApiError("Provide -m/--message and/or --file.")
305
+ if args.timeout <= 0:
306
+ raise ApiError("`--timeout` must be a positive integer.")
307
+
308
+ rid = get_or_create_dm(base_url, token, args.user_id, args.timeout)
309
+ attachments = [upload_asset(base_url, token, rid, fp, args.timeout) for fp in files]
310
+ msg = send_message(base_url, token, rid, text, attachments, args.timeout)
311
+ print(
312
+ json.dumps(
313
+ {
314
+ "targetUserId": args.user_id,
315
+ "rid": rid,
316
+ "baseUrl": base_url,
317
+ "attachmentCount": len(attachments),
318
+ "messageId": msg.get("_id"),
319
+ "message": msg,
320
+ },
321
+ ensure_ascii=False,
322
+ indent=2,
323
+ )
324
+ )
325
+ return 0
326
+ except ApiError as exc:
327
+ print(f"ERROR: {exc}", file=sys.stderr)
328
+ return 1
329
+
330
+
331
+ if __name__ == "__main__":
332
+ sys.exit(main())
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "md2pdf-converter",
3
+ "version": "2.0.0",
4
+ "types": [
5
+ "store"
6
+ ],
7
+ "displayName": "Markdown 转 PDF(离线)",
8
+ "description": "使用 Pandoc 与 WeasyPrint 将 Markdown 转为 PDF,支持中文与彩色表情;可离线使用(需先完成依赖与 Twemoji 缓存)。",
9
+ "changelog": [
10
+ {
11
+ "version": "2.0.0",
12
+ "date": "2026-04-14",
13
+ "changes": [
14
+ "初次提交"
15
+ ]
16
+ }
17
+ ],
18
+ "createdAt": "2026-04-14",
19
+ "updatedAt": "2026-04-14"
20
+ }
@@ -0,0 +1,244 @@
1
+ ---
2
+ name: md2pdf-converter
3
+ description: Offline Markdown to PDF converter with full Unicode support using Pandoc + WeasyPrint + local Twemoji cache. Use when user needs to convert Markdown documents to PDF with Chinese fonts and colorful emojis, or work offline after initial setup.
4
+ metadata:
5
+ {
6
+ "openclaw":
7
+ {
8
+ "emoji": "📄",
9
+ "requires": { "bins": ["pandoc", "python3", "wget"] },
10
+ "install":
11
+ [
12
+ {
13
+ "id": "apt-pandoc",
14
+ "kind": "apt",
15
+ "package": "pandoc",
16
+ "bins": ["pandoc"],
17
+ "label": "Install Pandoc (apt)",
18
+ },
19
+ {
20
+ "id": "apt-weasyprint",
21
+ "kind": "apt",
22
+ "package": "python3-weasyprint",
23
+ "bins": ["weasyprint"],
24
+ "label": "Install WeasyPrint (apt)",
25
+ },
26
+ {
27
+ "id": "apt-fonts",
28
+ "kind": "apt",
29
+ "package": "fonts-arphic-uming",
30
+ "bins": [],
31
+ "label": "Install Chinese fonts (apt)",
32
+ },
33
+ {
34
+ "id": "pip-weasyprint",
35
+ "kind": "pip",
36
+ "package": "weasyprint",
37
+ "bins": ["weasyprint"],
38
+ "label": "Install WeasyPrint (pip)",
39
+ },
40
+ ],
41
+ },
42
+ }
43
+ ---
44
+
45
+ # Markdown to PDF Converter (Complete Version)
46
+
47
+ ## Overview
48
+
49
+ Convert Markdown documents to professional PDFs with **FULL Unicode support**, Chinese fonts, and **colorful emojis** (3660 emojis including all variants). Uses Pandoc + WeasyPrint with a local Twemoji cache to work offline after first run.
50
+
51
+ ## Quick Start
52
+
53
+ Convert a Markdown file to PDF:
54
+
55
+ ```bash
56
+ bash scripts/md2pdf-local.sh input.md output.pdf
57
+ ```
58
+
59
+ **First run only:** Downloads ~150MB emoji resources (Twemoji 14.0.0) from GitHub. Subsequent runs work offline.
60
+
61
+ **Example:**
62
+
63
+ ```bash
64
+ bash scripts/md2pdf-local.sh report.md report.pdf
65
+ ```
66
+
67
+ ## Features
68
+
69
+ - ✅ **Full Unicode support** (Chinese, Japanese, Korean)
70
+ - ✅ **Complete emoji support** (Twemoji 14.0.0, 3660 colorful PNGs)
71
+ - ✅ **All emoji variants** (skin tones, hair styles, regional flags, etc.)
72
+ - ✅ **Offline operation** after initial setup
73
+ - ✅ **Professional PDF layout** with page numbers
74
+ - ✅ **Code highlighting**, tables, blockquotes
75
+ - ✅ **Accurate emoji mapping** via Python pre-generated lookup table
76
+
77
+ ## Technical Details
78
+
79
+ ### Dependencies
80
+
81
+ - **Pandoc** - Universal document converter
82
+ - **WeasyPrint** - CSS-to-PDF renderer
83
+ - **Python 3** - For emoji mapping generation
84
+ - **wget** - For emoji download (first run only)
85
+
86
+ ### How It Works
87
+
88
+ 1. **First run**: Downloads Twemoji 14.0.0 to `~/.cache/md2pdf/emojis/`
89
+ 2. **Python script**: Generates emoji → filename mapping table (`emoji_mapping.json`)
90
+ 3. **Pandoc**: Converts Markdown to HTML with a Lua filter that replaces emoji characters with local image references
91
+ 4. **WeasyPrint**: Renders HTML to PDF using:
92
+ - AR PL UMing CN for Chinese characters
93
+ - Local emoji images (PNG, 72x72px, colorful)
94
+ - Professional CSS styling
95
+
96
+ ### Emoji Cache Location
97
+
98
+ ```
99
+ ~/.cache/md2pdf/
100
+ ├── emojis/ # 3660 colorful PNG files
101
+ │ ├── 0023-fe0f-20e3.png
102
+ │ ├── 1f600.png
103
+ │ └── ...
104
+ └── emoji_mapping.json # Emoji to filename mapping
105
+ {
106
+ "🙀": "1f600.png",
107
+ "⌛": "0023-fe0f-20e3.png",
108
+ ...
109
+ }
110
+ ```
111
+
112
+ ### Emoji Mapping
113
+
114
+ The Python script `generate_emoji_mapping.py` scans all Twemoji files and creates a precise mapping from emoji characters to PNG filenames. This ensures accurate emoji replacement even for complex variants like skin tones and regional indicators.
115
+
116
+ ### Fonts
117
+
118
+ **Primary Chinese font**: AR PL UMing CN
119
+
120
+ **Fallback**: Noto Sans SC, Noto Sans CJK SC, Microsoft YaHei
121
+
122
+ **Monospace**: Menlo, Monaco
123
+
124
+ ## Version History
125
+
126
+ ### v2.0 (Current)
127
+ - ✅ Switched to **Twemoji 14.0.0** (complete version)
128
+ - ✅ **3660 colorful emojis** (including all variants)
129
+ - ✅ **Python pre-generated mapping** for accurate emoji replacement
130
+ - ✅ Fixed black-and-white emoji display issue
131
+ - ✅ Proper support for emoji variants (skin tones, hair styles, etc.)
132
+
133
+ ### v1.0 (Previous)
134
+ - Used emoji-datasource-google (~2000-3000 emojis)
135
+ - Simple hex-based filename matching (inaccurate for variants)
136
+ - Some emojis displayed as Unicode characters (black-and-white)
137
+
138
+ ## Troubleshooting
139
+
140
+ ### Font Issues
141
+
142
+ If Chinese characters display incorrectly, ensure AR PL UMing CN is installed:
143
+
144
+ ```bash
145
+ # Ubuntu/Debian
146
+ sudo apt-get install fonts-arphic-uming
147
+
148
+ # Check if installed
149
+ fc-list | grep "AR PL UMing"
150
+ ```
151
+
152
+ ### Emoji Not Showing
153
+
154
+ 1. Check if emoji cache exists: `ls ~/.cache/md2pdf/emojis/`
155
+ 2. Check if mapping exists: `ls ~/.cache/md2pdf/emoji_mapping.json`
156
+ 3. If missing, delete cache and re-run: `rm -rf ~/.cache/md2pdf`
157
+ 4. Verify emoji file exists: `ls ~/.cache/md2pdf/emojis/1f600.png`
158
+
159
+ ### Emoji Displaying as Black-and-White
160
+
161
+ This issue has been **FIXED** in v2.0. If you still see black-and-white emojis:
162
+
163
+ 1. Verify you're using the v2.0 script:
164
+ ```bash
165
+ grep "TWEMOJI_VERSION" scripts/md2pdf-local.sh
166
+ # Should show: TWEMOJI_VERSION="14.0.0"
167
+ ```
168
+
169
+ 2. Clear cache and regenerate:
170
+ ```bash
171
+ rm -rf ~/.cache/md2pdf
172
+ bash scripts/md2pdf-local.sh test.md test.pdf
173
+ ```
174
+
175
+ ### WeasyPrint Errors
176
+
177
+ Install missing dependencies:
178
+
179
+ ```bash
180
+ # Ubuntu/Debian
181
+ sudo apt-get install python3-weasyprint
182
+
183
+ # Or via pip
184
+ pip3 install weasyprint
185
+ ```
186
+
187
+ ### Python Script Errors
188
+
189
+ If `generate_emoji_mapping.py` fails:
190
+
191
+ ```bash
192
+ # Check Python version
193
+ python3 --version
194
+ # Should be Python 3.6+
195
+
196
+ # Check emoji cache
197
+ ls ~/.cache/md2pdf/emojis
198
+ ```
199
+
200
+ ## Resources
201
+
202
+ ### scripts/
203
+
204
+ **md2pdf-local.sh** - Main conversion script with automatic emoji caching and mapping
205
+
206
+ **generate_emoji_mapping.py** - Python script to generate emoji lookup table
207
+
208
+ **Usage**: Direct execution from any location (uses absolute paths):
209
+
210
+ ```bash
211
+ bash /path/to/skills/md2pdf-converter/scripts/md2pdf-local.sh input.md output.pdf
212
+ ```
213
+
214
+ **Key Features**:
215
+ - Automatic Twemoji download and caching
216
+ - Python pre-generated emoji mapping (accurate)
217
+ - Lua filter for emoji replacement
218
+ - CSS styling for professional output
219
+ - Temporary file cleanup (automatic)
220
+
221
+ ## Comparison: v1.0 vs v2.0
222
+
223
+ | Feature | v1.0 (Old) | v2.0 (New) |
224
+ |---------|----------------|---------------|
225
+ | Emoji Source | emoji-datasource-google | Twemoji 14.0.0 |
226
+ | Emoji Count | ~2000-3000 | 3660 |
227
+ | Color Display | ❌ Unstable | ✅ Stable |
228
+ | Variants Support | ❌ Incomplete | ✅ Complete |
229
+ | Mapping Accuracy | ⚠️ Low | ✅ High |
230
+ | Offline Support | ✅ After first run | ✅ After first run |
231
+ | First Run Size | ~68MB | ~150MB |
232
+
233
+ ## Performance
234
+
235
+ - **First run**: ~150MB download, 10-30 seconds (depending on network)
236
+ - **Subsequent runs**: Offline, seconds-level conversion
237
+ - **Memory usage**: ~150MB for emoji cache
238
+ - **PDF generation**: 1-5 seconds per page
239
+
240
+ ## Limitations
241
+
242
+ - Missing emojis (newer than Twemoji 14.0.0) will display as Unicode characters
243
+ - First run requires internet connection (for Twemoji download)
244
+ - Emoji cache size: ~150MB (3660 PNG files at 72x72px)
@@ -0,0 +1,6 @@
1
+ {
2
+ "ownerId": "kn738t1fd03w3rjwg0dwkxpvjx810m1v",
3
+ "slug": "md2pdf-converter",
4
+ "version": "2.0.0",
5
+ "publishedAt": 1771124542046
6
+ }
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ generate_emoji_mapping.py
4
+ 生成 Twemoji 到 Unicode 代码点的映射表
5
+ """
6
+
7
+ import os
8
+ import json
9
+
10
+ # Twemoji 目录
11
+ TWEMOJI_DIR = os.path.expanduser("~/.cache/md2pdf/emojis")
12
+
13
+ # 解析 Twemoji 文件名
14
+ # 格式: codepoint(-codepoint).png
15
+ def parse_twemoji_filename(filename):
16
+ """
17
+ 解析 Twemoji 文件名,返回 Unicode 代码点列表
18
+ 例如: "0023-fe0f-20e3.png" -> [0x23, 0xfe0f, 0x20e3] (⌛)
19
+ 例如: "1f600.png" -> [0x1f600] (🙀)
20
+ """
21
+ if not filename.endswith('.png'):
22
+ return None
23
+
24
+ basename = filename[:-4] # 移除 .png
25
+ parts = basename.split('-')
26
+
27
+ codepoints = []
28
+ for part in parts:
29
+ try:
30
+ codepoint = int(part, 16)
31
+ codepoints.append(codepoint)
32
+ except ValueError:
33
+ # 忽略无效部分(如fe0f这是选择器)
34
+ continue
35
+
36
+ return codepoints if codepoints else None
37
+
38
+ # 生成映射表
39
+ def generate_mapping():
40
+ """生成 emoji 到文件路径的映射表"""
41
+ mapping = {}
42
+
43
+ if not os.path.exists(TWEMOJI_DIR):
44
+ print(f"错误: Twemoji 目录不存在: {TWEMOJI_DIR}")
45
+ return None
46
+
47
+ print(f"正在扫描 Twemoji 目录: {TWEMOJI_DIR}")
48
+
49
+ # 扫描所有 PNG 文件
50
+ for filename in os.listdir(TWEMOJI_DIR):
51
+ if not filename.endswith('.png'):
52
+ continue
53
+
54
+ codepoints = parse_twemoji_filename(filename)
55
+ if codepoints:
56
+ # 将代码点转换为字符串
57
+ emoji_str = ''.join(chr(cp) for cp in codepoints)
58
+
59
+ # 存储映射
60
+ mapping[emoji_str] = filename
61
+
62
+ print(f"✅ 找到 {len(mapping)} 个 emoji")
63
+
64
+ # 保存映射表
65
+ mapping_file = os.path.join(os.path.dirname(TWEMOJI_DIR), "emoji_mapping.json")
66
+ with open(mapping_file, 'w', encoding='utf-8') as f:
67
+ json.dump(mapping, f, ensure_ascii=False, indent=2)
68
+
69
+ print(f"✅ 映射表已保存至: {mapping_file}")
70
+
71
+ return mapping
72
+
73
+ if __name__ == '__main__':
74
+ generate_mapping()