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,165 @@
1
+ """Permission-aware Bot API proxy.
2
+
3
+ Checks agent permissions before sending messages through the Bot API.
4
+ Credentials are stored in admin_dir (invisible to sub-agents).
5
+
6
+ Usage:
7
+ python scripts/bot_client_proxy.py --agent-id <id> send --message "..."
8
+ """
9
+
10
+ import argparse
11
+ import json
12
+ import os
13
+ import subprocess
14
+ import sys
15
+
16
+ def _find_manager_scripts() -> str:
17
+ """Locate sophnet-customer-agent-manager/scripts at runtime."""
18
+ skill_name = "sophnet-customer-agent-manager"
19
+ subdir = "scripts"
20
+ candidates = [
21
+ os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", skill_name, subdir),
22
+ os.path.join(os.path.expanduser("~"), ".openclaw", "skills", skill_name, subdir),
23
+ os.path.join("/app", "skills", skill_name, subdir),
24
+ ]
25
+ for path in candidates:
26
+ resolved = os.path.realpath(path)
27
+ if os.path.isdir(resolved):
28
+ return resolved
29
+ return os.path.realpath(candidates[0])
30
+
31
+
32
+ sys.path.insert(0, _find_manager_scripts())
33
+ from permission_store import _DEFAULT_ADMIN_DIR, get_permission
34
+
35
+ _BOT_CREDENTIALS_FILENAME = "bot_credentials.json"
36
+
37
+
38
+ def _output(data: dict) -> None:
39
+ print(json.dumps(data, ensure_ascii=False, indent=2))
40
+ sys.exit(0 if data.get("status") == "ok" else 1)
41
+
42
+
43
+ def _load_credentials(admin_dir: str, agent_id: str) -> dict | None:
44
+ path = os.path.join(admin_dir, agent_id, _BOT_CREDENTIALS_FILENAME)
45
+ if not os.path.isfile(path):
46
+ return None
47
+ with open(path, "r", encoding="utf-8") as f:
48
+ return json.load(f)
49
+
50
+
51
+ def send_message(
52
+ agent_id: str,
53
+ message: str,
54
+ admin_dir: str = _DEFAULT_ADMIN_DIR,
55
+ ) -> dict:
56
+ """Send a message through Bot API with permission checking."""
57
+ permission = get_permission(admin_dir, agent_id)
58
+ if permission is None:
59
+ return {
60
+ "status": "error",
61
+ "action": "bot_client_send",
62
+ "message": f"未找到 agent '{agent_id}' 的权限记录",
63
+ "fix": "请先使用 agent_manager_cli.py create 创建该 agent",
64
+ }
65
+
66
+ credentials = _load_credentials(admin_dir, agent_id)
67
+ if credentials is None:
68
+ return {
69
+ "status": "error",
70
+ "action": "bot_client_send",
71
+ "message": f"未找到 agent '{agent_id}' 的 Bot API 凭据",
72
+ "fix": "请先执行 bot_client_setup.py setup --mode proxy",
73
+ }
74
+
75
+ url = credentials["url"]
76
+ secret = credentials["secret"]
77
+ remote_agent_id = credentials["remote_agent_id"]
78
+
79
+ # Use agent_id as a stable chatId so the server always resolves to the
80
+ # same sessionKey. Previously we saved the server's sessionKey and sent
81
+ # it back as chatId, but sessionKey is a composite key that already
82
+ # contains chatId — feeding it back caused infinite key growth and a new
83
+ # session on every request.
84
+ payload = {
85
+ "agentId": remote_agent_id,
86
+ "senderId": agent_id,
87
+ "text": message,
88
+ "chatId": agent_id,
89
+ "senderPermissions": {
90
+ "operations": permission.get("operations", {}),
91
+ "scope": permission.get("scope", {}),
92
+ },
93
+ }
94
+
95
+ try:
96
+ result = subprocess.run(
97
+ [
98
+ "curl", "-s", "-X", "POST", url,
99
+ "-H", "Content-Type: application/json",
100
+ "-H", f"Authorization: Bearer {secret}",
101
+ "-d", json.dumps(payload, ensure_ascii=False),
102
+ ],
103
+ capture_output=True,
104
+ text=True,
105
+ timeout=120,
106
+ )
107
+ except subprocess.TimeoutExpired:
108
+ return {
109
+ "status": "error",
110
+ "action": "bot_client_send",
111
+ "message": "Bot API 请求超时 (120s)",
112
+ }
113
+
114
+ if result.returncode != 0:
115
+ return {
116
+ "status": "error",
117
+ "action": "bot_client_send",
118
+ "message": f"curl 请求失败 (exit {result.returncode})",
119
+ "stderr": result.stderr,
120
+ }
121
+
122
+ try:
123
+ response = json.loads(result.stdout)
124
+ except json.JSONDecodeError:
125
+ return {
126
+ "status": "error",
127
+ "action": "bot_client_send",
128
+ "message": "Bot API 返回了无法解析的响应",
129
+ "raw": result.stdout[:500],
130
+ }
131
+
132
+ return {
133
+ "status": "ok",
134
+ "action": "bot_client_send",
135
+ "reply": response.get("reply", ""),
136
+ "sessionKey": response.get("sessionKey", ""),
137
+ }
138
+
139
+
140
+ def main():
141
+ parser = argparse.ArgumentParser(description="Permission-aware Bot API proxy")
142
+ parser.add_argument("--agent-id", required=True, help="Agent identifier")
143
+ parser.add_argument(
144
+ "--admin-dir", default=_DEFAULT_ADMIN_DIR,
145
+ help="Admin directory (permissions + credentials)",
146
+ )
147
+
148
+ subparsers = parser.add_subparsers(dest="command", required=True)
149
+
150
+ send_parser = subparsers.add_parser("send", help="Send message via Bot API")
151
+ send_parser.add_argument("--message", required=True, help="Message to send")
152
+
153
+ args = parser.parse_args()
154
+
155
+ if args.command == "send":
156
+ result = send_message(
157
+ agent_id=args.agent_id,
158
+ message=args.message,
159
+ admin_dir=args.admin_dir,
160
+ )
161
+ _output(result)
162
+
163
+
164
+ if __name__ == "__main__":
165
+ main()
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bash
2
+ # Permission-aware wrapper for bot_client_proxy.py.
3
+ # Only allows Bot API proxy commands, no arbitrary shell/python execution.
4
+ # Designed to be exec-allowlisted alongside customer_proxy_safe.sh.
5
+
6
+ set -euo pipefail
7
+
8
+ PROXY_DIR="$(cd "$(dirname "$0")/.." && pwd)"
9
+
10
+ if [[ $# -lt 2 ]]; then
11
+ echo '{"status":"error","message":"Usage: bot_client_safe.sh --agent-id <id> send --message <msg>"}'
12
+ exit 1
13
+ fi
14
+
15
+ if [[ "$1" != "--agent-id" ]]; then
16
+ echo '{"status":"error","message":"First argument must be --agent-id"}'
17
+ exit 1
18
+ fi
19
+
20
+ AGENT_ID="$2"
21
+ shift 2
22
+
23
+ if [[ -z "${AGENT_ID}" || "${AGENT_ID}" == *";"* || "${AGENT_ID}" == *"|"* || "${AGENT_ID}" == *"&"* ]]; then
24
+ echo '{"status":"error","message":"Invalid agent-id"}'
25
+ exit 1
26
+ fi
27
+
28
+ cd "${PROXY_DIR}" && exec uv run --project . python scripts/bot_client_proxy.py \
29
+ --agent-id "${AGENT_ID}" "$@"
@@ -0,0 +1,502 @@
1
+ """Bot API client setup — inject/remove remote collaboration section in sub-agent AGENTS.md.
2
+
3
+ Supports two modes:
4
+ - raw: injects curl templates directly (legacy, no permission enforcement)
5
+ - proxy: injects bot_client_safe.sh reference with programmatic permission checks
6
+
7
+ Usage:
8
+ python scripts/bot_client_setup.py setup \
9
+ --agent-id vip-reader \
10
+ --url https://remote:3000/bot-api/chat \
11
+ --secret abc123 \
12
+ --remote-agent-id main \
13
+ --mode proxy
14
+
15
+ python scripts/bot_client_setup.py remove --agent-id vip-reader
16
+ """
17
+
18
+ import argparse
19
+ import json
20
+ import os
21
+ import stat
22
+ import sys
23
+
24
+ _DEFAULT_WORKSPACE_ROOT = os.path.join(
25
+ os.path.expanduser("~"), ".openclaw", "workspace-customer-agents"
26
+ )
27
+
28
+ _DEFAULT_ADMIN_DIR = os.path.join(
29
+ os.path.expanduser("~"), ".config", "sophnet-customer-admin"
30
+ )
31
+
32
+ _DEFAULT_EXEC_APPROVALS = os.path.join(
33
+ os.path.expanduser("~"), ".openclaw", "exec-approvals.json"
34
+ )
35
+
36
+ def _ensure_executable(path: str) -> None:
37
+ """Ensure a file has execute permission."""
38
+ try:
39
+ current = os.stat(path).st_mode
40
+ if not (current & stat.S_IXUSR):
41
+ os.chmod(path, current | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
42
+ except OSError:
43
+ pass
44
+
45
+
46
+ def _find_bot_client_safe_sh() -> str:
47
+ """Locate bot_client_safe.sh at runtime.
48
+
49
+ Search order:
50
+ 1. Alongside this script (source/development)
51
+ 2. clawhub runtime install path
52
+ 3. Docker pre-built path
53
+ """
54
+ skill_name = "sophnet-bot-client"
55
+ filename = "bot_client_safe.sh"
56
+ candidates = [
57
+ os.path.join(os.path.dirname(os.path.abspath(__file__)), filename),
58
+ os.path.join(os.path.expanduser("~"), ".openclaw", "skills", skill_name, "scripts", filename),
59
+ os.path.join("/app", "skills", skill_name, "scripts", filename),
60
+ ]
61
+ for path in candidates:
62
+ if os.path.isfile(path):
63
+ resolved = os.path.realpath(path)
64
+ _ensure_executable(resolved)
65
+ return resolved
66
+ return os.path.realpath(candidates[0])
67
+
68
+
69
+ _BOT_CLIENT_SAFE_SH = _find_bot_client_safe_sh()
70
+
71
+ SECTION_MARKER_START = "## 远程协作 — Bot API Client"
72
+ SECTION_MARKER_END = "<!-- END 远程协作 -->"
73
+
74
+ CREDENTIALS_FILENAME = "bot_credentials.json"
75
+ SESSION_FILENAME = "bot_session.txt"
76
+
77
+
78
+ def _workspace_path(agent_id: str, workspace_root: str) -> str:
79
+ return os.path.join(workspace_root, agent_id)
80
+
81
+
82
+ def _agents_md_path(agent_id: str, workspace_root: str) -> str:
83
+ return os.path.join(_workspace_path(agent_id, workspace_root), "AGENTS.md")
84
+
85
+
86
+ def _data_dir(agent_id: str, workspace_root: str) -> str:
87
+ return os.path.join(_workspace_path(agent_id, workspace_root), "data")
88
+
89
+
90
+ def generate_section(
91
+ url: str,
92
+ remote_agent_id: str,
93
+ agent_id: str,
94
+ credentials_path: str,
95
+ ) -> str:
96
+ """Generate the remote collaboration section for AGENTS.md (raw mode)."""
97
+ return f"""{SECTION_MARKER_START}
98
+
99
+ 你可以通过 Bot API 与远程系统对话。
100
+
101
+ ### 连接信息
102
+
103
+ - **凭据文件**: `{credentials_path}`
104
+ - **远端 Agent**: `{remote_agent_id}`
105
+
106
+ ### 发送消息
107
+
108
+ 使用以下命令发送消息并自动管理会话:
109
+
110
+ ```bash
111
+ # 读取凭据
112
+ BOT_URL=$(cat {credentials_path} | python3 -c "import sys,json; print(json.load(sys.stdin)['url'])")
113
+ BOT_SECRET=$(cat {credentials_path} | python3 -c "import sys,json; print(json.load(sys.stdin)['secret'])")
114
+
115
+ # 发送请求(chatId 使用固定的 agent_id 以保持会话稳定)
116
+ RESPONSE=$(curl -s -X POST "$BOT_URL" \\
117
+ -H "Content-Type: application/json" \\
118
+ -H "Authorization: Bearer $BOT_SECRET" \\
119
+ -d "$(python3 -c "
120
+ import json, sys
121
+ print(json.dumps({{
122
+ 'agentId': '{remote_agent_id}',
123
+ 'senderId': '{agent_id}',
124
+ 'text': sys.argv[1],
125
+ 'chatId': '{agent_id}'
126
+ }}))
127
+ " "<你要发送的消息>")")
128
+
129
+ # 提取回复
130
+ echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('reply','(无回复)'))"
131
+ ```
132
+
133
+ ### 会话管理
134
+
135
+ 1. **多轮对话**:chatId 固定为你的 agent_id,服务端自动维护同一会话
136
+ 2. **无需手动管理**:每次请求自动进入同一会话上下文
137
+
138
+ ### 错误处理
139
+
140
+ - 如果 curl 返回非 JSON(网络错误/超时),检查 URL 是否可达
141
+ - 如果收到 401/403,说明 secret 已过期,联系管理员更新凭据文件
142
+ - 如果 `reply` 字段为空,检查 `RESPONSE` 中的完整 JSON 以获取错误信息
143
+
144
+ ### 权限约束
145
+
146
+ **通过 Bot API 发送的请求必须遵守你本地相同的权限限制。**
147
+
148
+ - 如果你本地被禁止执行某项操作(如 delete、update),则**禁止**通过 Bot API 请求远端 agent 代为执行
149
+ - Bot API 仅用于在你的权限范围内与远端系统协作,不得用于绕过自身权限
150
+ - 违反此约束等同于直接执行被禁止的操作
151
+
152
+ ### 使用场景
153
+
154
+ 当本地命令无法满足需求,或用户明确要求通过远程系统执行时,在你的权限范围内使用上述命令发送自然语言请求。
155
+
156
+ {SECTION_MARKER_END}"""
157
+
158
+
159
+ def generate_proxy_section(
160
+ remote_agent_id: str,
161
+ agent_id: str,
162
+ wrapper_path: str,
163
+ ) -> str:
164
+ """Generate the remote collaboration section for AGENTS.md (proxy mode).
165
+
166
+ Unlike raw mode, this does NOT expose curl/python3 commands or credentials.
167
+ All communication goes through the permission-checking proxy.
168
+ """
169
+ return f"""{SECTION_MARKER_START}
170
+
171
+ 你可以通过 Bot API 与远程系统对话。
172
+
173
+ ### 连接信息
174
+
175
+ - **远端 Agent**: `{remote_agent_id}`
176
+ - 凭据和会话由系统自动管理
177
+
178
+ ### 发送消息
179
+
180
+ 使用以下命令发送消息:
181
+
182
+ ```bash
183
+ {wrapper_path} \\
184
+ --agent-id "{agent_id}" send --message "<你要发送的消息>"
185
+ ```
186
+
187
+ ### 错误处理
188
+
189
+ - 如果返回 `权限记录` 相关错误,说明 agent 未正确注册,联系管理员
190
+ - 如果返回 `凭据` 相关错误,说明 Bot API 连接未配置,联系管理员
191
+ - 如果返回 `curl 请求失败`,检查远端服务是否可用
192
+
193
+ ### 权限约束
194
+
195
+ **你的权限由系统强制执行,无法绕过。** 超出权限范围的操作会被代理自动拒绝。
196
+
197
+ - **禁止**使用 curl、python3 或任何其他方式直接访问 Bot API
198
+ - **禁止**直接读取凭据文件
199
+ - 所有远程通信**必须**通过上述命令
200
+
201
+ {SECTION_MARKER_END}"""
202
+
203
+
204
+ def _find_section(content: str) -> tuple[int, int] | None:
205
+ """Find the start and end positions of the remote collaboration section.
206
+
207
+ Returns (start, end) character indices, or None if not found.
208
+ """
209
+ start = content.find(SECTION_MARKER_START)
210
+ if start == -1:
211
+ return None
212
+ end = content.find(SECTION_MARKER_END, start)
213
+ if end == -1:
214
+ return None
215
+ end += len(SECTION_MARKER_END)
216
+ return (start, end)
217
+
218
+
219
+ def _make_writable(path: str) -> int | None:
220
+ """If file is read-only, make it writable. Returns original mode or None."""
221
+ if not os.path.exists(path):
222
+ return None
223
+ current_mode = os.stat(path).st_mode
224
+ if not (current_mode & stat.S_IWUSR):
225
+ os.chmod(path, current_mode | stat.S_IWUSR)
226
+ return current_mode
227
+ return None
228
+
229
+
230
+ def _protect_file(path: str) -> None:
231
+ """Set file to read-only (444)."""
232
+ os.chmod(path, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)
233
+
234
+
235
+ def _add_to_exec_allowlist(
236
+ agent_id: str,
237
+ pattern: str,
238
+ exec_approvals_path: str = _DEFAULT_EXEC_APPROVALS,
239
+ ) -> None:
240
+ """Add an entry to the agent's exec allowlist (idempotent)."""
241
+ data: dict = {"version": 1, "agents": {}}
242
+ if os.path.exists(exec_approvals_path):
243
+ with open(exec_approvals_path, "r", encoding="utf-8") as f:
244
+ data = json.load(f)
245
+
246
+ agents = data.setdefault("agents", {})
247
+ agent_entry = agents.get(agent_id)
248
+ if agent_entry is None:
249
+ agent_entry = {"security": "allowlist", "ask": "off", "allowlist": []}
250
+ agents[agent_id] = agent_entry
251
+ allowlist = agent_entry.setdefault("allowlist", [])
252
+
253
+ abs_pattern = os.path.abspath(pattern)
254
+ for entry in allowlist:
255
+ if entry.get("pattern") == abs_pattern:
256
+ return
257
+
258
+ allowlist.append({"pattern": abs_pattern})
259
+
260
+ with open(exec_approvals_path, "w", encoding="utf-8") as f:
261
+ json.dump(data, f, indent=2, ensure_ascii=False)
262
+
263
+
264
+ def setup(
265
+ agent_id: str,
266
+ url: str,
267
+ secret: str,
268
+ remote_agent_id: str,
269
+ workspace_root: str = _DEFAULT_WORKSPACE_ROOT,
270
+ mode: str = "raw",
271
+ admin_dir: str = _DEFAULT_ADMIN_DIR,
272
+ exec_approvals_path: str = _DEFAULT_EXEC_APPROVALS,
273
+ ) -> dict:
274
+ """Set up bot client for a sub-agent.
275
+
276
+ mode="raw": credentials in workspace/data, raw curl in AGENTS.md
277
+ mode="proxy": credentials in admin_dir, safe proxy in AGENTS.md + exec allowlist
278
+ """
279
+ workspace = _workspace_path(agent_id, workspace_root)
280
+ if not os.path.isdir(workspace):
281
+ return {
282
+ "status": "error",
283
+ "action": "setup_bot_client",
284
+ "message": f"Workspace not found: {workspace}",
285
+ "fix": "请先使用 customer-agent create 创建子代理",
286
+ }
287
+
288
+ agents_md = _agents_md_path(agent_id, workspace_root)
289
+ if not os.path.isfile(agents_md):
290
+ return {
291
+ "status": "error",
292
+ "action": "setup_bot_client",
293
+ "message": f"AGENTS.md not found: {agents_md}",
294
+ "fix": "Workspace 目录不完整,请重新创建子代理",
295
+ }
296
+
297
+ credentials_data = {
298
+ "url": url,
299
+ "secret": secret,
300
+ "remote_agent_id": remote_agent_id,
301
+ }
302
+
303
+ if mode == "proxy":
304
+ # Credentials go to admin_dir/<agent_id>/ (invisible to sub-agent)
305
+ creds_dir = os.path.join(admin_dir, agent_id)
306
+ os.makedirs(creds_dir, exist_ok=True)
307
+ credentials_abs = os.path.join(creds_dir, CREDENTIALS_FILENAME)
308
+ with open(credentials_abs, "w", encoding="utf-8") as f:
309
+ json.dump(credentials_data, f, indent=2, ensure_ascii=False)
310
+
311
+ wrapper_path = os.path.abspath(_BOT_CLIENT_SAFE_SH)
312
+ section = generate_proxy_section(remote_agent_id, agent_id, wrapper_path)
313
+ else:
314
+ # Legacy: credentials in workspace/data/
315
+ data_dir = _data_dir(agent_id, workspace_root)
316
+ if not os.path.exists(data_dir):
317
+ os.makedirs(data_dir, exist_ok=True)
318
+ credentials_path = os.path.join("data", CREDENTIALS_FILENAME)
319
+ credentials_abs = os.path.join(workspace, credentials_path)
320
+ with open(credentials_abs, "w", encoding="utf-8") as f:
321
+ json.dump(credentials_data, f, indent=2, ensure_ascii=False)
322
+
323
+ section = generate_section(url, remote_agent_id, agent_id, credentials_path)
324
+
325
+ # Read existing AGENTS.md (handle read-only files)
326
+ original_mode = _make_writable(agents_md)
327
+ try:
328
+ with open(agents_md, "r", encoding="utf-8") as f:
329
+ content = f.read()
330
+
331
+ bounds = _find_section(content)
332
+ if bounds is not None:
333
+ start, end = bounds
334
+ new_content = content[:start].rstrip("\n") + "\n\n" + section + content[end:]
335
+ else:
336
+ new_content = content.rstrip("\n") + "\n\n" + section + "\n"
337
+
338
+ with open(agents_md, "w", encoding="utf-8") as f:
339
+ f.write(new_content)
340
+ finally:
341
+ if mode == "proxy":
342
+ _protect_file(agents_md)
343
+ elif original_mode is not None:
344
+ os.chmod(agents_md, original_mode)
345
+
346
+ # In proxy mode, add bot_client_safe.sh to exec allowlist
347
+ if mode == "proxy":
348
+ _add_to_exec_allowlist(
349
+ agent_id, _BOT_CLIENT_SAFE_SH, exec_approvals_path,
350
+ )
351
+
352
+ result = {
353
+ "status": "ok",
354
+ "action": "setup_bot_client",
355
+ "agent_id": agent_id,
356
+ "url": url,
357
+ "remote_agent_id": remote_agent_id,
358
+ "mode": mode,
359
+ "workspace": workspace,
360
+ "credentials_file": credentials_abs,
361
+ }
362
+ if mode == "proxy":
363
+ result["exec_allowlist_updated"] = True
364
+ return result
365
+
366
+
367
+ def remove(
368
+ agent_id: str,
369
+ workspace_root: str = _DEFAULT_WORKSPACE_ROOT,
370
+ admin_dir: str = _DEFAULT_ADMIN_DIR,
371
+ ) -> dict:
372
+ """Remove bot client configuration from a sub-agent.
373
+
374
+ 1. Remove remote collaboration section from AGENTS.md
375
+ 2. Delete credentials and session files from both workspace/data and admin_dir
376
+ """
377
+ workspace = _workspace_path(agent_id, workspace_root)
378
+ if not os.path.isdir(workspace):
379
+ return {
380
+ "status": "error",
381
+ "action": "remove_bot_client",
382
+ "message": f"Workspace not found: {workspace}",
383
+ }
384
+
385
+ agents_md = _agents_md_path(agent_id, workspace_root)
386
+ removed_section = False
387
+
388
+ if os.path.isfile(agents_md):
389
+ original_mode = _make_writable(agents_md)
390
+ try:
391
+ with open(agents_md, "r", encoding="utf-8") as f:
392
+ content = f.read()
393
+
394
+ bounds = _find_section(content)
395
+ if bounds is not None:
396
+ start, end = bounds
397
+ new_content = content[:start].rstrip("\n") + content[end:].lstrip("\n")
398
+ if new_content and not new_content.endswith("\n"):
399
+ new_content += "\n"
400
+ with open(agents_md, "w", encoding="utf-8") as f:
401
+ f.write(new_content)
402
+ removed_section = True
403
+ finally:
404
+ if original_mode is not None:
405
+ os.chmod(agents_md, original_mode)
406
+
407
+ # Clean up data files from workspace/data (raw mode)
408
+ removed_files = []
409
+ for filename in (CREDENTIALS_FILENAME, SESSION_FILENAME):
410
+ filepath = os.path.join(workspace, "data", filename)
411
+ if os.path.isfile(filepath):
412
+ os.remove(filepath)
413
+ removed_files.append(filename)
414
+
415
+ # Clean up data files from admin_dir (proxy mode)
416
+ admin_agent_dir = os.path.join(admin_dir, agent_id)
417
+ for filename in (CREDENTIALS_FILENAME, SESSION_FILENAME):
418
+ filepath = os.path.join(admin_agent_dir, filename)
419
+ if os.path.isfile(filepath):
420
+ os.remove(filepath)
421
+ if filename not in removed_files:
422
+ removed_files.append(filename)
423
+
424
+ return {
425
+ "status": "ok",
426
+ "action": "remove_bot_client",
427
+ "agent_id": agent_id,
428
+ "removed_section": removed_section,
429
+ "removed_files": removed_files,
430
+ "workspace": workspace,
431
+ }
432
+
433
+
434
+ def main() -> None:
435
+ parser = argparse.ArgumentParser(
436
+ description="Bot API client setup for sub-agents"
437
+ )
438
+ subparsers = parser.add_subparsers(dest="command", required=True)
439
+
440
+ # setup command
441
+ setup_parser = subparsers.add_parser("setup", help="Configure remote connection")
442
+ setup_parser.add_argument("--agent-id", required=True, help="Sub-agent ID")
443
+ setup_parser.add_argument("--url", required=True, help="Bot API URL")
444
+ setup_parser.add_argument("--secret", required=True, help="API secret/token")
445
+ setup_parser.add_argument(
446
+ "--remote-agent-id", required=True,
447
+ help="Remote target agent ID (should be the calling agent's own ID for permission inheritance)"
448
+ )
449
+ setup_parser.add_argument(
450
+ "--mode", choices=["raw", "proxy"], default="proxy",
451
+ help="proxy (default): inject safe proxy with permission checks; raw (legacy): inject curl templates",
452
+ )
453
+ setup_parser.add_argument(
454
+ "--workspace-root", default=_DEFAULT_WORKSPACE_ROOT, help=argparse.SUPPRESS
455
+ )
456
+ setup_parser.add_argument(
457
+ "--admin-dir", default=_DEFAULT_ADMIN_DIR, help=argparse.SUPPRESS
458
+ )
459
+ setup_parser.add_argument(
460
+ "--exec-approvals-path", default=_DEFAULT_EXEC_APPROVALS,
461
+ help=argparse.SUPPRESS,
462
+ )
463
+
464
+ # remove command
465
+ remove_parser = subparsers.add_parser("remove", help="Remove remote connection")
466
+ remove_parser.add_argument("--agent-id", required=True, help="Sub-agent ID")
467
+ remove_parser.add_argument(
468
+ "--workspace-root", default=_DEFAULT_WORKSPACE_ROOT, help=argparse.SUPPRESS
469
+ )
470
+ remove_parser.add_argument(
471
+ "--admin-dir", default=_DEFAULT_ADMIN_DIR, help=argparse.SUPPRESS
472
+ )
473
+
474
+ args = parser.parse_args()
475
+
476
+ if args.command == "setup":
477
+ result = setup(
478
+ agent_id=args.agent_id,
479
+ url=args.url,
480
+ secret=args.secret,
481
+ remote_agent_id=args.remote_agent_id,
482
+ workspace_root=args.workspace_root,
483
+ mode=args.mode,
484
+ admin_dir=args.admin_dir,
485
+ exec_approvals_path=args.exec_approvals_path,
486
+ )
487
+ elif args.command == "remove":
488
+ result = remove(
489
+ agent_id=args.agent_id,
490
+ workspace_root=args.workspace_root,
491
+ admin_dir=args.admin_dir,
492
+ )
493
+ else:
494
+ parser.print_help()
495
+ sys.exit(1)
496
+
497
+ print(json.dumps(result, indent=2, ensure_ascii=False))
498
+ sys.exit(0 if result["status"] == "ok" else 1)
499
+
500
+
501
+ if __name__ == "__main__":
502
+ main()