sophhub 0.2.1 → 0.2.3

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 (189) 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/flight-booking/skill.json +9 -2
  21. package/skills/flight-booking/src/scripts/flight_booking.py +2 -1
  22. package/skills/image-classify/skill.json +5 -5
  23. package/skills/image-classify/src/SKILL.md +60 -67
  24. package/skills/image-classify/src/scripts/face_search.py +400 -15
  25. package/skills/image-classify/src/scripts/send_dm_message.py +332 -0
  26. package/skills/md2pdf-converter/skill.json +20 -0
  27. package/skills/md2pdf-converter/src/SKILL.md +244 -0
  28. package/skills/md2pdf-converter/src/_meta.json +6 -0
  29. package/skills/md2pdf-converter/src/scripts/generate_emoji_mapping.py +74 -0
  30. package/skills/md2pdf-converter/src/scripts/md2pdf-local.sh +291 -0
  31. package/skills/sophnet-bot-client/skill.json +20 -0
  32. package/skills/sophnet-bot-client/src/SKILL.md +255 -0
  33. package/skills/sophnet-bot-client/src/pyproject.toml +13 -0
  34. package/skills/sophnet-bot-client/src/scripts/__init__.py +0 -0
  35. package/skills/sophnet-bot-client/src/scripts/bot_client_proxy.py +165 -0
  36. package/skills/sophnet-bot-client/src/scripts/bot_client_safe.sh +29 -0
  37. package/skills/sophnet-bot-client/src/scripts/bot_client_setup.py +502 -0
  38. package/skills/sophnet-bot-client/src/tests/__init__.py +0 -0
  39. package/skills/sophnet-bot-client/src/tests/test_bot_client_proxy.py +255 -0
  40. package/skills/sophnet-bot-client/src/tests/test_bot_client_setup.py +679 -0
  41. package/skills/sophnet-bot-client/src/uv.lock +8 -0
  42. package/skills/sophnet-docx/skill.json +20 -0
  43. package/skills/sophnet-docx/src/SKILL.md +463 -0
  44. package/skills/sophnet-docx/src/package-lock.json +208 -0
  45. package/skills/sophnet-docx/src/package.json +16 -0
  46. package/skills/sophnet-docx/src/pyproject.toml +11 -0
  47. package/skills/sophnet-docx/src/scripts/__init__.py +1 -0
  48. package/skills/sophnet-docx/src/scripts/accept_changes.py +135 -0
  49. package/skills/sophnet-docx/src/scripts/comment.py +318 -0
  50. package/skills/sophnet-docx/src/scripts/ensure_uv_env.sh +68 -0
  51. package/skills/sophnet-docx/src/scripts/office/helpers/__init__.py +0 -0
  52. package/skills/sophnet-docx/src/scripts/office/helpers/merge_runs.py +199 -0
  53. package/skills/sophnet-docx/src/scripts/office/helpers/simplify_redlines.py +197 -0
  54. package/skills/sophnet-docx/src/scripts/office/pack.py +159 -0
  55. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  56. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  57. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  58. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  59. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  60. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  61. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  62. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  63. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  64. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  65. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  66. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  67. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  68. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  69. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  70. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  71. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  72. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  73. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  74. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  75. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  76. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  77. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  78. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  79. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  80. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  81. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  82. package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  83. package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  84. package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  85. package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  86. package/skills/sophnet-docx/src/scripts/office/schemas/mce/mc.xsd +75 -0
  87. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  88. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  89. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  90. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  91. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  92. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  93. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  94. package/skills/sophnet-docx/src/scripts/office/soffice.py +183 -0
  95. package/skills/sophnet-docx/src/scripts/office/unpack.py +132 -0
  96. package/skills/sophnet-docx/src/scripts/office/validate.py +111 -0
  97. package/skills/sophnet-docx/src/scripts/office/validators/__init__.py +15 -0
  98. package/skills/sophnet-docx/src/scripts/office/validators/base.py +847 -0
  99. package/skills/sophnet-docx/src/scripts/office/validators/docx.py +446 -0
  100. package/skills/sophnet-docx/src/scripts/office/validators/pptx.py +275 -0
  101. package/skills/sophnet-docx/src/scripts/office/validators/redlining.py +247 -0
  102. package/skills/sophnet-docx/src/scripts/templates/comments.xml +3 -0
  103. package/skills/sophnet-docx/src/scripts/templates/commentsExtended.xml +3 -0
  104. package/skills/sophnet-docx/src/scripts/templates/commentsExtensible.xml +3 -0
  105. package/skills/sophnet-docx/src/scripts/templates/commentsIds.xml +3 -0
  106. package/skills/sophnet-docx/src/scripts/templates/people.xml +3 -0
  107. package/skills/sophnet-docx/src/scripts/upload_file.sh +96 -0
  108. package/skills/sophnet-docx/src/uv.lock +320 -0
  109. package/skills/sophnet-pdf/skill.json +20 -0
  110. package/skills/sophnet-pdf/src/SKILL.md +413 -0
  111. package/skills/sophnet-pdf/src/forms.md +297 -0
  112. package/skills/sophnet-pdf/src/pyproject.toml +14 -0
  113. package/skills/sophnet-pdf/src/reference.md +612 -0
  114. package/skills/sophnet-pdf/src/scripts/check_bounding_boxes.py +65 -0
  115. package/skills/sophnet-pdf/src/scripts/check_fillable_fields.py +11 -0
  116. package/skills/sophnet-pdf/src/scripts/convert_pdf_to_images.py +33 -0
  117. package/skills/sophnet-pdf/src/scripts/create_validation_image.py +37 -0
  118. package/skills/sophnet-pdf/src/scripts/enhance_tutorial.py +558 -0
  119. package/skills/sophnet-pdf/src/scripts/ensure_uv_env.sh +68 -0
  120. package/skills/sophnet-pdf/src/scripts/extract_form_field_info.py +122 -0
  121. package/skills/sophnet-pdf/src/scripts/extract_form_structure.py +115 -0
  122. package/skills/sophnet-pdf/src/scripts/extract_pdf_content.py +35 -0
  123. package/skills/sophnet-pdf/src/scripts/fill_fillable_fields.py +98 -0
  124. package/skills/sophnet-pdf/src/scripts/fill_pdf_form_with_annotations.py +107 -0
  125. package/skills/sophnet-pdf/src/scripts/upload_file.sh +88 -0
  126. package/skills/sophnet-pdf/src/uv.lock +537 -0
  127. package/skills/sophnet-xlsx/skill.json +20 -0
  128. package/skills/sophnet-xlsx/src/SKILL.md +399 -0
  129. package/skills/sophnet-xlsx/src/pyproject.toml +11 -0
  130. package/skills/sophnet-xlsx/src/scripts/ensure_uv_env.sh +68 -0
  131. package/skills/sophnet-xlsx/src/scripts/office/helpers/__init__.py +0 -0
  132. package/skills/sophnet-xlsx/src/scripts/office/helpers/merge_runs.py +199 -0
  133. package/skills/sophnet-xlsx/src/scripts/office/helpers/simplify_redlines.py +197 -0
  134. package/skills/sophnet-xlsx/src/scripts/office/pack.py +159 -0
  135. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  136. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  137. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  138. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  139. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  140. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  141. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  142. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  143. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  144. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  145. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  146. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  147. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  148. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  149. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  150. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  151. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  152. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  153. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  154. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  155. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  156. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  157. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  158. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  159. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  160. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  161. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  162. package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  163. package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  164. package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  165. package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  166. package/skills/sophnet-xlsx/src/scripts/office/schemas/mce/mc.xsd +75 -0
  167. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  168. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  169. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  170. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  171. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  172. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  173. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  174. package/skills/sophnet-xlsx/src/scripts/office/soffice.py +183 -0
  175. package/skills/sophnet-xlsx/src/scripts/office/unpack.py +132 -0
  176. package/skills/sophnet-xlsx/src/scripts/office/validate.py +111 -0
  177. package/skills/sophnet-xlsx/src/scripts/office/validators/__init__.py +15 -0
  178. package/skills/sophnet-xlsx/src/scripts/office/validators/base.py +847 -0
  179. package/skills/sophnet-xlsx/src/scripts/office/validators/docx.py +446 -0
  180. package/skills/sophnet-xlsx/src/scripts/office/validators/pptx.py +275 -0
  181. package/skills/sophnet-xlsx/src/scripts/office/validators/redlining.py +247 -0
  182. package/skills/sophnet-xlsx/src/scripts/recalc.py +184 -0
  183. package/skills/sophnet-xlsx/src/scripts/upload_file.sh +96 -0
  184. package/skills/sophnet-xlsx/src/uv.lock +319 -0
  185. package/skills/wechat-article-publisher/skill.json +20 -0
  186. package/skills/wechat-article-publisher/src/SKILL.md +60 -0
  187. package/skills/wechat-article-publisher/src/config.json +7 -0
  188. package/skills/wechat-article-publisher/src/pyproject.toml +12 -0
  189. package/skills/wechat-article-publisher/src/scripts/publish_wechat.py +825 -0
@@ -0,0 +1,181 @@
1
+ import json
2
+ import stat
3
+ import subprocess
4
+ import sys
5
+ import tempfile
6
+ import textwrap
7
+ import unittest
8
+ from pathlib import Path
9
+
10
+
11
+ SCRIPT = Path(__file__).resolve().parent / "openclaw_notes_crud.py"
12
+
13
+
14
+ def write_fake_cli(path: Path) -> None:
15
+ body = """#!/usr/bin/env python3
16
+ import json
17
+ import sys
18
+
19
+ args = sys.argv[1:]
20
+
21
+ def out(obj):
22
+ print(json.dumps(obj, ensure_ascii=False))
23
+
24
+ if args[:2] == ["docs", "+create"]:
25
+ out({"ok": True, "data": {"doc_id": "dox_new", "doc_url": "https://feishu.example/docx/dox_new"}})
26
+ elif args[:2] == ["docs", "+fetch"]:
27
+ out({"ok": True, "data": {"doc_id": "dox_read", "doc_url": "https://feishu.example/docx/dox_read", "markdown": "# read"}})
28
+ elif args[:2] == ["docs", "+update"]:
29
+ out({"ok": True, "data": {"doc_id": "dox_update", "doc_url": "https://feishu.example/docx/dox_update", "args": args}})
30
+ elif args[:2] == ["docs", "+search"]:
31
+ out({"ok": True, "data": {"items": [{"title": "note-1", "obj_token": "dox_1"}], "has_more": False}})
32
+ elif args[:2] == ["docs", "+media-insert"]:
33
+ out({"ok": True, "data": {"document_id": "dox_media", "file_token": "file_x", "args": args}})
34
+ elif args[:2] == ["docs", "+media-download"]:
35
+ out({"ok": True, "data": {"saved_path": "/tmp/downloaded.png", "content_type": "image/png", "args": args}})
36
+ elif args[:2] == ["drive", "+add-comment"]:
37
+ out({"ok": True, "data": {"comment_id": "cmt_1", "args": args}})
38
+ elif args[:2] == ["api", "DELETE"]:
39
+ out({"code": 0, "msg": "success", "data": {"deleted": True}})
40
+ else:
41
+ sys.stderr.write("unexpected args: " + " ".join(args) + "\\n")
42
+ sys.exit(2)
43
+ """
44
+ path.write_text(textwrap.dedent(body), encoding="utf-8")
45
+ path.chmod(path.stat().st_mode | stat.S_IEXEC)
46
+
47
+
48
+ class OpenClawNotesCrudTest(unittest.TestCase):
49
+ def run_cmd(self, args: list[str]) -> dict:
50
+ with tempfile.TemporaryDirectory() as tmpdir:
51
+ fake_cli = Path(tmpdir) / "fake_cli.py"
52
+ write_fake_cli(fake_cli)
53
+ cmd = [
54
+ sys.executable,
55
+ str(SCRIPT),
56
+ "--lark-cli-bin",
57
+ str(fake_cli),
58
+ ] + args
59
+ proc = subprocess.run(cmd, capture_output=True, text=True)
60
+ self.assertEqual(proc.returncode, 0, proc.stderr)
61
+ return json.loads(proc.stdout)
62
+
63
+ def test_create(self):
64
+ out = self.run_cmd(
65
+ ["--op", "create", "--title", "t", "--markdown", "# hello"]
66
+ )
67
+ self.assertTrue(out["ok"])
68
+ self.assertEqual(out["op"], "create")
69
+ self.assertEqual(out["data"]["doc_id"], "dox_new")
70
+
71
+ def test_read_update_search(self):
72
+ read_out = self.run_cmd(["--op", "read", "--doc", "dox_read"])
73
+ self.assertTrue(read_out["ok"])
74
+ self.assertEqual(read_out["data"]["doc_id"], "dox_read")
75
+
76
+ update_out = self.run_cmd(
77
+ [
78
+ "--op",
79
+ "update",
80
+ "--doc",
81
+ "dox_update",
82
+ "--mode",
83
+ "append",
84
+ "--markdown",
85
+ "new content",
86
+ ]
87
+ )
88
+ self.assertTrue(update_out["ok"])
89
+ self.assertEqual(update_out["data"]["doc_id"], "dox_update")
90
+
91
+ search_out = self.run_cmd(["--op", "search", "--query", "note"])
92
+ self.assertTrue(search_out["ok"])
93
+ self.assertEqual(search_out["data"]["items"][0]["obj_token"], "dox_1")
94
+
95
+ def test_media_comment_and_section_update(self):
96
+ media_insert_out = self.run_cmd(
97
+ [
98
+ "--op",
99
+ "media-insert",
100
+ "--doc",
101
+ "dox_doc",
102
+ "--file",
103
+ "/tmp/test.png",
104
+ "--media-type",
105
+ "image",
106
+ "--caption",
107
+ "demo",
108
+ ]
109
+ )
110
+ self.assertTrue(media_insert_out["ok"])
111
+ self.assertEqual(media_insert_out["data"]["file_token"], "file_x")
112
+
113
+ media_download_out = self.run_cmd(
114
+ [
115
+ "--op",
116
+ "media-download",
117
+ "--token",
118
+ "file_x",
119
+ "--output",
120
+ "/tmp/downloaded",
121
+ "--overwrite",
122
+ ]
123
+ )
124
+ self.assertTrue(media_download_out["ok"])
125
+ self.assertEqual(media_download_out["data"]["saved_path"], "/tmp/downloaded.png")
126
+
127
+ comment_out = self.run_cmd(
128
+ [
129
+ "--op",
130
+ "comment",
131
+ "--doc",
132
+ "dox_doc",
133
+ "--comment",
134
+ "请补充这段说明",
135
+ "--selection-with-ellipsis",
136
+ "旧内容...结束",
137
+ ]
138
+ )
139
+ self.assertTrue(comment_out["ok"])
140
+ args = comment_out["data"]["args"]
141
+ content = args[args.index("--content") + 1]
142
+ self.assertEqual(json.loads(content)[0]["text"], "请补充这段说明")
143
+
144
+ section_out = self.run_cmd(
145
+ [
146
+ "--op",
147
+ "section-update",
148
+ "--doc",
149
+ "dox_doc",
150
+ "--section-title",
151
+ "## 计划",
152
+ "--section-mode",
153
+ "replace",
154
+ "--markdown",
155
+ "新的章节内容",
156
+ ]
157
+ )
158
+ self.assertTrue(section_out["ok"])
159
+ section_args = section_out["data"]["args"]
160
+ self.assertIn("replace_range", section_args)
161
+ self.assertIn("## 计划", section_args)
162
+ markdown = section_args[section_args.index("--markdown") + 1]
163
+ self.assertTrue(markdown.startswith("## 计划\n\n"))
164
+
165
+ def test_delete(self):
166
+ out = self.run_cmd(
167
+ ["--op", "delete", "--doc", "https://x.feishu.cn/docx/dox_delete123"]
168
+ )
169
+ self.assertTrue(out["ok"])
170
+ self.assertEqual(out["data"]["deleted_doc_token"], "dox_delete123")
171
+
172
+ def test_delete_invalid_url(self):
173
+ out = self.run_cmd(
174
+ ["--op", "delete", "--doc", "https://x.feishu.cn/wiki/wiki_token"]
175
+ )
176
+ self.assertFalse(out["ok"])
177
+ self.assertEqual(out["error"]["code"], "validation")
178
+
179
+
180
+ if __name__ == "__main__":
181
+ unittest.main()
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env python3
2
+ """Cross-platform entrypoint for meeting minutes flow."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+ from urllib.parse import urlparse
12
+
13
+ from _resolve_lark_cli import resolve_bundled_cli
14
+
15
+
16
+ def parse_args() -> argparse.Namespace:
17
+ parser = argparse.ArgumentParser(
18
+ description="Run meeting-minutes flow from Feishu minutes URL or token."
19
+ )
20
+ parser.add_argument("minutes_url_or_token", help="minutes URL or minute_token")
21
+ parser.add_argument(
22
+ "--output-dir",
23
+ default=str(Path.home() / ".openclaw" / "workspace" / ".meeting-minutes"),
24
+ help="artifact output dir",
25
+ )
26
+ parser.add_argument("--job-id", help="optional custom job id")
27
+ return parser.parse_args()
28
+
29
+
30
+ def extract_token(raw: str) -> str:
31
+ if raw.startswith("http://") or raw.startswith("https://"):
32
+ parsed = urlparse(raw)
33
+ token = parsed.path.rstrip("/").split("/")[-1]
34
+ return token
35
+ return raw.strip()
36
+
37
+
38
+ def main() -> int:
39
+ args = parse_args()
40
+ token = extract_token(args.minutes_url_or_token)
41
+ if not token:
42
+ print(
43
+ json.dumps(
44
+ {"status": "failed", "error_message": "missing minute_token"},
45
+ ensure_ascii=False,
46
+ )
47
+ )
48
+ return 1
49
+
50
+ script_dir = Path(__file__).resolve().parent
51
+ base_dir = script_dir.parent
52
+ cli_bin = resolve_bundled_cli(base_dir)
53
+ runner = script_dir / "openclaw_meeting_minutes.py"
54
+ job_id = args.job_id or f"meeting-minutes-{token}"
55
+
56
+ proc = subprocess.run(
57
+ [
58
+ sys.executable,
59
+ str(runner),
60
+ "--lark-cli-bin",
61
+ str(cli_bin),
62
+ "--minute-token",
63
+ token,
64
+ "--job-id",
65
+ job_id,
66
+ "--output-dir",
67
+ args.output_dir,
68
+ ],
69
+ capture_output=True,
70
+ text=True,
71
+ )
72
+ if proc.stdout:
73
+ sys.stdout.write(proc.stdout)
74
+ if proc.stderr:
75
+ sys.stderr.write(proc.stderr)
76
+ return proc.returncode
77
+
78
+
79
+ if __name__ == "__main__":
80
+ raise SystemExit(main())
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
5
+ exec python3 "$SCRIPT_DIR/run_meeting_minutes.py" "$@"
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env python3
2
+ """Cross-platform entrypoint for note CRUD flow."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from _resolve_lark_cli import resolve_bundled_cli
11
+
12
+
13
+ def main() -> int:
14
+ script_dir = Path(__file__).resolve().parent
15
+ base_dir = script_dir.parent
16
+ cli_bin = resolve_bundled_cli(base_dir)
17
+ runner = script_dir / "openclaw_notes_crud.py"
18
+
19
+ proc = subprocess.run(
20
+ [sys.executable, str(runner), "--lark-cli-bin", str(cli_bin), *sys.argv[1:]],
21
+ capture_output=True,
22
+ text=True,
23
+ )
24
+ if proc.stdout:
25
+ sys.stdout.write(proc.stdout)
26
+ if proc.stderr:
27
+ sys.stderr.write(proc.stderr)
28
+ return proc.returncode
29
+
30
+
31
+ if __name__ == "__main__":
32
+ raise SystemExit(main())
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
5
+ exec python3 "$SCRIPT_DIR/run_note_crud.py" "$@"
@@ -1,12 +1,19 @@
1
1
  {
2
2
  "name": "flight-booking",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "types": [
5
5
  "builtin"
6
6
  ],
7
7
  "displayName": "国内机票预定",
8
8
  "description": "",
9
9
  "changelog": [
10
+ {
11
+ "changes": [
12
+ "修正票务网关基址为 https://www.sophnet.com/api,避免请求落到 /v1/api 导致 405 或非 JSON 响应"
13
+ ],
14
+ "date": "2026-04-16",
15
+ "version": "1.1.1"
16
+ },
10
17
  {
11
18
  "changes": [
12
19
  "初次提交"
@@ -16,5 +23,5 @@
16
23
  }
17
24
  ],
18
25
  "createdAt": "2026-04-09",
19
- "updatedAt": "2026-04-09"
26
+ "updatedAt": "2026-04-16"
20
27
  }
@@ -14,7 +14,7 @@ import requests
14
14
 
15
15
  # API 配置(新版网关,Bearer API_KEY 鉴权)
16
16
 
17
- GATEWAY_BASE_URL = "https://www.sophnet.com/v1/api"
17
+ GATEWAY_BASE_URL = "https://www.sophnet.com/api"
18
18
 
19
19
  TICKET_BASE = f"{GATEWAY_BASE_URL}/open-apis/ticket"
20
20
  def _resolve_api_key() -> str:
@@ -33,6 +33,7 @@ def _resolve_api_key() -> str:
33
33
  raise RuntimeError("未找到 SOPH_API_KEY:请设置环境变量 SOPH_API_KEY ")
34
34
 
35
35
  API_KEY = _resolve_api_key()
36
+
36
37
  # 乘机人信息从环境变量读取(用户需提前设置)
37
38
  ENV_PASSENGER_NAME = "PASSENGER_NAME"
38
39
  ENV_PASSENGER_MOBILE = "PASSENGER_MOBILE"
@@ -8,11 +8,11 @@
8
8
  "description": "通过人脸识别对照片进行分类、搜索和管理",
9
9
  "changelog": [
10
10
  {
11
+ "version": "1.0.3",
12
+ "date": "2026-04-15",
11
13
  "changes": [
12
- "脚本内 pathlib 全部改为 os.path(递归列举图片、复制与打包等路径处理);list_images_pathlib 重命名为 list_images_recursive"
13
- ],
14
- "date": "2026-04-10",
15
- "version": "1.0.3"
14
+ "照片分类器添加对自动发送虾友的支持"
15
+ ]
16
16
  },
17
17
  {
18
18
  "changes": [
@@ -38,5 +38,5 @@
38
38
  }
39
39
  ],
40
40
  "createdAt": "2026-04-10",
41
- "updatedAt": "2026-04-10"
41
+ "updatedAt": "2026-04-15"
42
42
  }
@@ -5,24 +5,25 @@ description: 照片分类器。通过人脸识别对照片进行分类、搜索
5
5
 
6
6
  # 📸 照片分类器
7
7
 
8
- 基于人脸 embedding 相似度的照片分类系统。支持注册用户人脸、按用户搜索照片、一键全量分类、删除用户;搜索/分类结束后**先自动生成下载链接**,再**询问是否复制到指定文件夹**。
8
+ 基于人脸 embedding 相似度的照片分类系统。支持注册用户人脸、按用户搜索照片、一键全量分类、删除用户;搜索/分类结束后**生成下载链接**(`pack`)即完成结果导出。
9
9
 
10
10
  ## Processing Mode
11
11
 
12
12
  **STRICT SERIAL PROCESSING ONLY** — 单次顺序执行,不拆分子任务、不并行。
13
13
 
14
- **子会话(耗时任务)**:`search`、`quick-search`、`classify` 在**独立子会话**中执行。**子命令结束(成功或失败)后,须立即关闭并删除该子会话**,不得保留空壳会话。后续步骤——向用户展示结果表格、执行 `pack`、询问是否 `copy`——一律在**当前主会话**中完成。
14
+ **子会话(耗时任务)**:`search`、`quick-search`、`classify` 在**独立子会话**中执行。**子命令结束(成功或失败)后,须立即关闭并删除该子会话**,不得保留空壳会话。后续步骤——向用户展示结果表格、执行 `pack`——一律在**当前主会话**中完成。
15
15
 
16
16
  ## Prerequisites
17
17
 
18
18
  - Python 3.10+、uv
19
19
  - 依赖:`requests`, `opencv-python`, `numpy`, `sophnet_tools`(项目内置)
20
20
  - 配置文件:`{baseDir}/references/config.json`
21
+ - 若使用 **DM 自动推送**(注册时 `--friend-id`、一键 `classify` 后推送结果),需本机存在有效 JWT:`/home/node/.openclaw/jwt.json`(与 `send_dm_message.py` 一致)
21
22
 
22
23
  ## 核心概念
23
24
 
24
25
  - **`{baseDir}`**:本 skill 根目录,即 `skills/image-classify`,调用时替换为实际绝对路径。
25
- - **配置文件**:`{baseDir}/references/config.json`,存储所有注册用户的名称、照片路径及人脸 embedding 向量。
26
+ - **配置文件**:`{baseDir}/references/config.json`,存储所有注册用户的名称、照片路径、人脸 embedding 向量,以及可选 **DM 绑定**:`friendId`(好友 userId)、`friendLabel`(可选展示名)。旧字段 `xia_you_hao` / `xia_you_label` 仍可被脚本读取。
26
27
  - **脚本入口**:`{baseDir}/scripts/face_search.py`,通过 `uv run` 以子命令方式调用。
27
28
  - **所有命令输出均为 JSON 格式**,方便解析结果。
28
29
 
@@ -39,7 +40,7 @@ uv run {baseDir}/scripts/face_search.py [-c CONFIG_PATH] <command> [args...]
39
40
  | 命令 | 用途 | 示例 |
40
41
  |------|------|------|
41
42
  | `check <name>` | 检查用户名是否已注册 | `check 张三` |
42
- | `add <name> <image>` | 注册新用户 | `add 张三 /path/photo.jpg` |
43
+ | `add <name> <image> [--friend-id ID] [--friend-label 名]` | 注册新用户;可选 `friendId` 与 `friendLabel`(括号内提示用) | `add 张三 /path/photo.jpg --friend-label 小李 --friend-id 23725` |
43
44
  | `replace <name> <image>` | 替换用户照片(删除旧照片) | `replace 张三 /path/new.jpg` |
44
45
  | `append <name> <image>` | 为已有用户追加一张照片 | `append 张三 /path/extra.jpg` |
45
46
  | `rename <old> <new>` | 修改用户名 | `rename 张三 李三` |
@@ -47,7 +48,7 @@ uv run {baseDir}/scripts/face_search.py [-c CONFIG_PATH] <command> [args...]
47
48
  | `search <name> <dir>` | 在目录中搜索某用户的相似人脸 | `search 张三 /home/photos` |
48
49
  | `quick-search <image> <dir>` | 直接用照片搜索相似人脸(不注册) | `quick-search /tmp/face.jpg /home/photos` |
49
50
  | `classify <dir>` | 一键分类:遍历所有用户搜索全部照片 | `classify /home/photos` |
50
- | `copy <target> [--name N]` | 将搜索结果复制到文件夹 | `copy /output --name 张三` |
51
+ | `copy <target> [--name N]` | (可选)将搜索结果复制到本地文件夹 | `copy /output --name 张三` |
51
52
  | `pack [--name N] [--timeout T]` | 压缩结果为 zip 并上传获取下载链接 | `pack --timeout 120` |
52
53
  | `upload <file> [--timeout T]` | 上传文件获取 URL | `upload /path/file.zip` |
53
54
  | `init-today-folder` | 创建并返回待搜索目录当天子目录 | `init-today-folder` |
@@ -95,19 +96,23 @@ uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json list
95
96
 
96
97
  **流程**:
97
98
 
98
- 1. 获取用户提供的**用户名**和**照片路径**
99
+ 1. 获取用户提供的**用户名**、**照片路径**,以及是否绑定 **DM 接收方**(均可选)。用户常以自然语言写出类似:**虾友:xxx,ID:23725**(虾友即 DM 对象;**23725** 即 `friendId`)。解析规则:**xxx** → `--friend-label`;**23725** → `--friend-id`(正整数,与 `send_dm_message.py` 的 `--user-id` / 接口 `userId` 一致)。可只填 `friendId`、不填展示名。
99
100
  2. 检查用户名是否已存在:
100
101
  ```bash
101
102
  uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json check "张三"
102
103
  ```
103
104
  输出:`{"exists": true/false, "name": "张三"}`
104
105
 
105
- 3. **如果不存在** → 注册新用户:
106
+ 3. **如果不存在** → 注册新用户(无虾友号):
106
107
  ```bash
107
108
  uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json add "张三" "/path/to/photo.jpg"
108
109
  ```
109
- 输出:`{"success": true/false, "message": "...", "name": "张三", "image_url": "https://..."}`
110
- - `success: true` → 提示:`🎉✨ **注册成功** · xxx`,并使用返回的 `image_url` 展示照片:`![image](image_url)`
110
+ 若用户提供了 `friendId`,追加 `--friend-id <正整数>`;若有展示名,追加 `--friend-label "xxx"`(注意转义或引号,避免 shell 拆词):
111
+ ```bash
112
+ uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json add "张三" "/path/to/photo.jpg" --friend-label "小李" --friend-id 23725
113
+ ```
114
+ 输出:`{"success": true/false, "message": "...", "name": "张三", "image_url": "https://...", "friendId": 23725, "friendLabel": "小李"}`(`friendId` / `friendLabel` 仅在实际传入时出现)
115
+ - `success: true` → 提示:`🎉✨ **注册成功** · xxx`,并使用返回的 `image_url` 展示照片:`![image](image_url)`;若返回含 `friendId` / `friendLabel`,可顺带一句已绑定 DM(不必冗长)
111
116
  - `success: false` → 提示:`❌🙈 照片中未发现有效的人脸信息,注册失败!`
112
117
 
113
118
  4. **如果已存在** → 提示用户选择操作:
@@ -162,7 +167,7 @@ uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json sear
162
167
  | 🖼️ 照片路径 | 📊 相似度 |
163
168
  |:----:|:----:|
164
169
  | /path/to/photo.jpg | 0.85 |
165
- 5. 展示结果后按「结果导出」流程处理:先 `pack` 生成下载链接,再询问是否复制到文件夹(见 §6
170
+ 5. 展示结果后按「结果导出」执行 `pack` 生成下载链接(见 §6);`pack` 返回后若含 `dm_lines`(向虾友推送**下载链接**的状态),须在链接展示之后**再**逐行展示(可与搜索结果本身区分开)。
166
171
 
167
172
  ### 3. 直接搜索(不注册)
168
173
 
@@ -189,7 +194,7 @@ uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json quic
189
194
  | 🖼️ 照片路径 | 📊 相似度 |
190
195
  |:----:|:----:|
191
196
  | /path/to/photo.jpg | 0.85 |
192
- 4. 展示结果后按「结果导出」流程处理:先 `pack` 生成下载链接,再询问是否复制到文件夹(见 §6
197
+ 4. 展示结果后按「结果导出」执行 `pack` 生成下载链接(见 §6);`pack` 返回后若含 `dm_lines`,须在链接展示之后**再**逐行展示。
193
198
 
194
199
  ### 4. 一键分类
195
200
 
@@ -203,10 +208,20 @@ uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json quic
203
208
  uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json classify "<dir>"
204
209
  ```
205
210
  **命令返回后立刻关闭并删除该子会话。**
206
- 输出:
211
+ 输出(节选):
207
212
  ```json
208
- {"user_count": 2, "users": {"张三": {"count": 3, "results": [...]}, "李四": {"count": 1, "results": [...]}}}
213
+ {
214
+ "user_count": 2,
215
+ "users": {"张三": {"count": 3, "results": [...]}, "李四": {"count": 1, "results": [...]}},
216
+ "dm": {"张三": {"success": true, "friendId": 23725, "friendLabel": "小李"}},
217
+ "dm_lines": ["✅ 💌 已向虾友「小李」发送成功", "📭 「李四」暂未绑定虾友,跳过私信"]
218
+ }
209
219
  ```
220
+ - `dm`:仅当配置里**至少有一位用户**登记了 `friendId` 时出现;脚本在分类结束后**自动调用** `{baseDir}/scripts/send_dm_message.py`(`--user-id` = `friendId`),将**该用户本轮分类结果摘要**发出。失败时含 `"success": false` 与 `detail`。
221
+ - `dm_lines`:**只要本轮参与分类的用户数 ≥1 即出现**(与是否有人绑定 `friendId` 无关)。为**最终提示专用**短句数组,顺序与本轮 `users` 中用户顺序一致。句式固定:
222
+ - 已绑定且发送成功:`✅ 💌 已向虾友「xxx」发送成功` — **xxx** 优先为配置中的 `friendLabel`,否则为 `friendId` 数字字符串。
223
+ - 已绑定且发送失败:`❌ 💢 虾友「xxx」发送失败:<原因>`(原因取自 `dm` 中对应用户的 `detail`)。
224
+ - 未绑定 `friendId`:`📭 「<本地用户名>」暂未绑定虾友,跳过私信`。
210
225
  - `user_count: 0` → 提示:`❌👥 **暂无注册用户** · 请先注册人脸后再分类`
211
226
  3. 将结果通知到当前会话,对每个用户分别展示:
212
227
  ```
@@ -225,7 +240,14 @@ uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json clas
225
240
  ```
226
241
  👤💤 **王五** · 本轮暂无匹配
227
242
  ```
228
- 4. 展示结果后按「结果导出」流程处理:先 `pack` 生成下载链接,再询问是否复制到文件夹(见 §6)
243
+ 4. **分类摘要 DM(必须在表格之后、`pack` 之前展示)**:若 `classify` 的 JSON 含 `dm_lines`(本轮**分类结果摘要**的私信状态),在分类表格**之后**单独加一块,**逐行原样**展示。示例:
244
+ ```
245
+ 📬 **虾友发送状态(分类摘要)**
246
+ ✅ 💌 已向虾友「小李」发送成功
247
+ 📭 「李四」暂未绑定虾友,跳过私信
248
+ ```
249
+ 无 `dm_lines` 时(仅 `user_count: 0` 等异常)跳过本块。
250
+ 5. 展示结果后按「结果导出」执行 `pack` 生成下载链接(见 §6)。**`pack` 成功后还会自动向对应 `friendId` 私信「打包下载链接」**;若 `pack` 的 JSON 另含 `dm_lines`(**下载链接**的私信状态),须在下载链接展示之后**再**逐行展示,可与上一步标题区分,例如「下载链接私信状态」。
229
251
 
230
252
  ### 5. 删除用户
231
253
 
@@ -245,74 +267,48 @@ uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json clas
245
267
 
246
268
  ### 6. 结果导出(搜索/分类完成后)
247
269
 
248
- 搜索或分类完成后,结果已保存在 `config.json` 中。展示匹配结果表格后,**不要先弹出菜单**,按下面顺序执行。
249
-
250
- #### 第一步:直接生成下载链接
270
+ 搜索或分类完成后,结果已保存在 `config.json` 中。展示匹配结果表格(分类场景下若已有「分类摘要」`dm_lines`,须先展示完毕)后,**不要先弹出与导出无关的菜单**,直接执行 **`pack`** 生成下载链接(压缩包可能较大,超时默认 120 秒)。
251
271
 
252
- 立即执行 `pack`(压缩包可能较大,超时默认 120 秒),将返回的链接展示给用户:
272
+ **`pack` 成功后,脚本会自动调用** `{baseDir}/scripts/send_dm_message.py`,向**已绑定 `friendId` 的注册用户**私信发送**对应用户的打包下载链接**;正文模板为:先提示「「用户名」的照片已为您送达」,再附 URL,并提醒 **24 小时内下载**、逾期链接可能失效。无 `friendId` 的用户条目会体现在返回的 `dm_lines` 里(`📭 「xxx」暂未绑定虾友,跳过私信`)。**仅 `quick-search` 结果、无注册用户名时**,无法绑定虾友,不会发链接私信。
253
273
 
254
274
  - **单用户 `search`**(指定 `--name`):
255
275
  ```bash
256
276
  uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json pack --name "张三" --timeout 120
257
277
  ```
258
- 输出:`{"success": true, "url": "https://...", "zip_path": "/tmp/.../search_results.zip"}`
259
- 成功提示:`📦🔗 **下载链接已就绪** ✨` → `<url>`
278
+ 输出(节选):`{"success": true, "url": "https://...", "zip_path": "/tmp/.../search_results.zip", "dm": {...}, "dm_lines": [...]}`(有 `friendId` 时含 `dm` / `dm_lines`)
279
+ 成功提示:`📦🔗 **下载链接已就绪** ✨` → `<url>` → 若有 `dm_lines`,再展示「下载链接私信状态」块。
260
280
 
261
281
  - **`quick-search`(直接搜索)**:结果存于 `quick_search_result`,执行 **不带 `--name`** 的 `pack`(与仅含直接搜索结果时一致):
262
282
  ```bash
263
283
  uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json pack --timeout 120
264
284
  ```
265
- 成功提示:`📦🔗 **下载链接已就绪** ✨` → `<url>`
285
+ 成功提示:`📦🔗 **下载链接已就绪** ✨` → `<url>`(通常**无**虾友私信,因无注册用户上下文)
266
286
 
267
287
  - **一键 `classify`**(不指定 `--name`,**每位用户单独打包**):
268
288
  ```bash
269
289
  uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json pack --timeout 120
270
290
  ```
271
- 输出示例:
291
+ 输出示例(节选):
272
292
  ```json
273
- {"success": true, "user_urls": {"张三": "https://...", "李四": "https://..."}, "errors": null}
293
+ {
294
+ "success": true,
295
+ "user_urls": {"张三": "https://...", "李四": "https://..."},
296
+ "errors": null,
297
+ "dm": {"张三": {"success": true, "friendId": 23725, "friendLabel": "小李", "url": "https://..."}},
298
+ "dm_lines": ["✅ 💌 已向虾友「小李」发送成功", "📭 「李四」暂未绑定虾友,跳过私信"]
299
+ }
274
300
  ```
275
- 成功提示:逐用户展示链接,例如:
301
+ 成功提示:先逐用户展示链接,例如:
276
302
  ```
277
303
  📦✨ **各用户独立下载包**
278
304
  ───────────────────────
279
305
  👤 张三 📎 <url>
280
306
  👤 李四 📎 <url>
281
307
  ```
282
- 部分失败:列出已成功链接,并对失败用户提示:`⚠️❌ **<用户名>** · 下载包生成失败:<原因>`。全部失败:`❌📦 下载链接全部失败`(仍进入第二步询问复制,便于用户落盘)。
308
+ 再展示 `dm_lines`(下载链接私信状态)。
309
+ 部分失败:列出已成功链接,并对失败用户提示:`⚠️❌ **<用户名>** · 下载包生成失败:<原因>`;`dm_lines` 中亦会有对应失败说明。全部失败:`❌📦 下载链接全部失败`。
283
310
 
284
- #### 第二步:再询问是否复制到指定文件夹
285
-
286
- 下载链接处理完毕后,询问用户:
287
-
288
- ```
289
- 💾📂 **要不要把匹配照片复制到本地?**
290
- ✅ 需要 → 💬 请发目标文件夹路径
291
- 👋 不用了 → 到这里结束 ✨
292
- ```
293
-
294
- 用户选择**需要**时,再问 `📂✏️ 请输入目标文件夹路径:` 并执行:
295
-
296
- - **单用户 `search`**(注册用户搜索结果):
297
- ```bash
298
- uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json copy "/output/folder" --name "张三"
299
- ```
300
-
301
- - **`quick-search`**(结果为列表、无注册用户名):**不带** `--name`:
302
- ```bash
303
- uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json copy "/output/folder"
304
- ```
305
-
306
- - **一键 `classify`**(不指定 `--name`,按用户名自动建子目录):
307
- ```bash
308
- uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json copy "/output/folder"
309
- ```
310
-
311
- 输出:`{"success": true, "copied": 5, "failed": 0, "target": "/output/folder"}`
312
- - 成功:`✅📁 **复制完成** · 共 N 张 → /path/to/target/`
313
- - 部分失败:`⚠️📋 **复制结束** · 成功 N 张 · 失败 M 张`
314
-
315
- 用户选择**不需要**则结束,无需再执行 `copy`。
311
+ `pack` 完成后,**结果导出即结束**。若用户另行要求把文件复制到本地目录,可再使用命令表中的 `copy`(非本 skill 默认流程)。
316
312
 
317
313
  ### 7. 上传图片到待搜索目录
318
314
 
@@ -339,25 +335,21 @@ uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json uplo
339
335
  - 用户:「用照片分类器在 /home/photos 下查找张三」
340
336
  1. `uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json check "张三"` → 存在
341
337
  2. 子会话执行 `uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json search "张三" "/home/photos"`
342
- 3. 展示结果表格 → **直接** `pack --name "张三" --timeout 120` 生成链接 再问是否 `copy`(见 §6)
338
+ 3. 展示结果表格 → `pack --name "张三" --timeout 120` 生成下载链接(见 §6)→ 若有 `dm_lines`,展示下载链接私信状态
343
339
 
344
340
  - 用户:「用这张照片在 /home/photos 里找找有没有类似的」
345
341
  1. 子会话执行 `uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json quick-search "/tmp/face.jpg" "/home/photos"`
346
- 2. 成功 → 展示表格 → **直接** `pack --timeout 120`(不带 `--name`)→ 再问是否 `copy`;失败 → 提示未检测到有效人脸
342
+ 2. 成功 → 展示表格 → `pack --timeout 120`(不带 `--name`);失败 → 提示未检测到有效人脸
347
343
 
348
344
  - 用户:「用照片分类器对 /home/album 进行分类」
349
345
  1. 子会话执行 `uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json classify "/home/album"`
350
- 2. 展示各用户结果 **直接** `pack --timeout 120`(多用户各一链接)→ 再问是否 `copy`
346
+ 2. 展示各用户结果(及分类摘要 `dm_lines` 若有)→ `pack --timeout 120`(多用户各一链接,见 §6)→ 展示链接及 **`pack` 返回的 `dm_lines`**(下载链接私信状态)
351
347
 
352
348
  - 用户:「删除照片分类器中李四的信息」
353
349
  1. `uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json check "李四"` → 存在
354
350
  2. 提示确认 → 用户确认 → `uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json delete "李四"`
355
351
  3. 提示:`🗑️✨ **用户已删除** · 李四`
356
352
 
357
- - 用户:在搜索/分类完成后表示「要复制到 /output」(§6 第二步)
358
- 1. `uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json copy "/output"`(按需加 `--name`,与 §6 一致)
359
- 2. 提示:`✅📁 **复制完成** · N 张 → /output/`
360
-
361
353
  - 用户:「把这些图片放到待搜索目录」
362
354
  1. `uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json upload-image "/tmp/a.jpg" "/tmp/b.jpg"`
363
355
  2. 全部成功后提示:`✅📤 **已放入待搜索目录** → <folder_path> ✨`
@@ -365,11 +357,12 @@ uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json uplo
365
357
  ## Notes
366
358
 
367
359
  - 建议始终通过 `-c` 传入 `{baseDir}/references/config.json` 的绝对路径,避免相对路径导致找不到配置文件。
368
- - `search`、`quick-search` 和 `classify` 可能耗时较长(取决于搜索目录中的图片数量),应在**子会话**中执行;**结束后立即关闭并删除该子会话**,再将结果与后续 `pack` / `copy` 交互放在**主会话**中完成。
360
+ - `search`、`quick-search` 和 `classify` 可能耗时较长(取决于搜索目录中的图片数量),应在**子会话**中执行;**结束后立即关闭并删除该子会话**,再将结果展示与 `pack` 放在**主会话**中完成。
369
361
  - `images/` 目录用于存放待搜索图片;`upload-image` 内部会创建当天目录并串行逐张处理图片。
370
362
  - 搜索过程中会在图片所在目录下创建 `.embedding` 隐藏目录缓存 embedding 结果,后续搜索同一目录会自动跳过已处理的图片。
371
- - `copy` `pack` 命令依赖 `config.json` 中保存的搜索结果(`search_result` 或 `quick_search_result` 字段),必须先执行 `search`、`classify` 或 `quick-search` 后才能使用。
372
- - 结果导出顺序:**先**执行 `pack` 生成下载链接,**再**询问用户是否需要 `copy`(见 §6)。
363
+ - `pack` `copy` 均依赖 `config.json` 中保存的搜索结果(`search_result` 或 `quick_search_result` 等),须先执行 `search`、`classify` 或 `quick-search`。默认流程只需 **`pack`**;`copy` 为可选,由用户另行提出时再执行。
364
+ - **`classify`** 会向 `friendId` 发**分类结果摘要**;**`pack`** 会向 `friendId` 发**打包下载链接**(两次私信、用途不同);若只需其一,可后续再改脚本或配置(当前实现为两者都发)。
373
365
  - `pack` 的压缩文件存放在系统临时目录中,上传完成后可忽略清理。上传超时默认 120 秒,文件特别大时可通过 `--timeout` 增大。
374
366
  - `config.json` 中的 `query_threshold`(默认 0.7)控制人脸检测置信度阈值,`search_similarity_threshold`(默认 0.3)控制搜索匹配的最低相似度。
375
367
  - `{baseDir}` 指本 skill 根目录(如 `skills/image-classify`),调用时替换为实际绝对路径。
368
+ - `friendId`(虾友号)与可选 `friendLabel` 写在用户条目下;**注册时**通过 `add ... --friend-id` / `--friend-label` 写入。后续若需修改可编辑 `config.json`(本 skill 未单独提供改绑子命令)。旧键名仍兼容读取。