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
@@ -8,6 +8,7 @@ import sys
8
8
  import json
9
9
  import argparse
10
10
  import shutil
11
+ import subprocess
11
12
  import zipfile
12
13
  from datetime import datetime
13
14
  import cv2
@@ -254,6 +255,26 @@ def get_config(config_path='references/config.json'):
254
255
  "query_threshold": 0.7,
255
256
  "search_similarity_threshold": 0.3
256
257
  }
258
+
259
+
260
+ def _user_friend_id(user):
261
+ """用户条目中的好友 ID(虾友号)。新字段 `friendId`,兼容旧字段 `xia_you_hao`。"""
262
+ if not isinstance(user, dict):
263
+ return None
264
+ v = user.get("friendId")
265
+ if v is not None:
266
+ return v
267
+ return user.get("xia_you_hao")
268
+
269
+
270
+ def _user_friend_label(user):
271
+ """DM 展示名。新字段 `friendLabel`,兼容旧字段 `xia_you_label`。"""
272
+ if not isinstance(user, dict):
273
+ return None
274
+ v = user.get("friendLabel")
275
+ if v is not None:
276
+ return v
277
+ return user.get("xia_you_label")
257
278
 
258
279
  def name_in_config(name, config_path='references/config.json'):
259
280
  """检查名字是否在配置文件的用户列表中"""
@@ -289,8 +310,14 @@ def delete_user_config(ost_name, config_path='references/config.json'):
289
310
  with open(config_path, 'w', encoding='utf-8') as f:
290
311
  json.dump(basic_config, f, indent=4, ensure_ascii=False)
291
312
 
292
- def add_user_config(name, image_path, config_path='references/config.json'):
293
- """添加用户到config配置文件的用户列表中"""
313
+ def add_user_config(
314
+ name,
315
+ image_path,
316
+ config_path="references/config.json",
317
+ friend_id=None,
318
+ friend_label=None,
319
+ ):
320
+ """添加用户到 config。friendId 为可选好友 userId(正整数);friendLabel 为可选展示名。"""
294
321
  basic_config = get_config(config_path)
295
322
  user_list = basic_config.get("users", [])
296
323
  face_embedding = get_baseface_embedding(image_path, det_thr=basic_config.get("query_threshold", 0.7))
@@ -298,7 +325,12 @@ def add_user_config(name, image_path, config_path='references/config.json'):
298
325
  return False, "未检测到有效的人脸信息"
299
326
 
300
327
  abs_image_path = os.path.realpath(image_path)
301
- user_list.append({"name": name, "info":[{"file_path": abs_image_path, "embedding": face_embedding}]})
328
+ entry = {"name": name, "info": [{"file_path": abs_image_path, "embedding": face_embedding}]}
329
+ if friend_id is not None:
330
+ entry["friendId"] = int(friend_id)
331
+ if friend_label is not None and str(friend_label).strip():
332
+ entry["friendLabel"] = str(friend_label).strip()
333
+ user_list.append(entry)
302
334
  basic_config["users"] = user_list
303
335
  with open(config_path, 'w', encoding='utf-8') as f:
304
336
  json.dump(basic_config, f, indent=4, ensure_ascii=False)
@@ -563,6 +595,296 @@ def classify_all_users(search_path, config_path='references/config.json'):
563
595
  return all_results
564
596
 
565
597
 
598
+ def _format_classify_dm_message(name: str, items: list, max_lines: int = 30, max_chars: int = 3500) -> str:
599
+ """将单用户分类结果格式化为私信正文(控制长度)。"""
600
+ n = len(items)
601
+ lines = [f"【照片分类结果】👤 {name}", f"匹配共 {n} 张。"]
602
+ if n == 0:
603
+ lines.append("本轮目录中暂无匹配照片。")
604
+ return "\n".join(lines)
605
+ for i, item in enumerate(items[:max_lines]):
606
+ path = item.get("image_path", "")
607
+ sim = item.get("similarity", 0)
608
+ try:
609
+ sim_s = f"{float(sim):.4f}"
610
+ except (TypeError, ValueError):
611
+ sim_s = str(sim)
612
+ lines.append(f"- {path} · 相似度 {sim_s}")
613
+ if n > max_lines:
614
+ lines.append(f"… 另有 {n - max_lines} 张未列出,请在本机打包下载查看完整列表。")
615
+ text = "\n".join(lines)
616
+ if len(text) > max_chars:
617
+ text = text[: max_chars - 20] + "\n…(正文过长已截断)"
618
+ return text
619
+
620
+
621
+ def _format_pack_link_dm_message(name: str, url: str) -> str:
622
+ """pack 成功后发给 friendId 的私信正文(含下载链接)。"""
623
+ return (
624
+ f"✨「{name}」的照片已为您送达\n\n"
625
+ f"{url}\n\n"
626
+ f"请在 24 小时内下载保存;逾期链接可能失效,请及时转存。"
627
+ )
628
+
629
+
630
+ def _find_user_by_name(config_path: str, name: str):
631
+ if not name:
632
+ return None
633
+ for u in get_config(config_path).get("users", []):
634
+ if u.get("name") == name:
635
+ return u
636
+ return None
637
+
638
+
639
+ def _send_classify_dm(friend_id: int, message: str, timeout: int = 90) -> tuple:
640
+ """调用同目录 send_dm_message.py 发送私信(--user-id 即 friendId)。返回 (success: bool, detail: str)。"""
641
+ script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "send_dm_message.py")
642
+ if not os.path.isfile(script):
643
+ return False, f"未找到 DM 脚本: {script}"
644
+ try:
645
+ proc = subprocess.run(
646
+ [sys.executable, script, "--user-id", str(int(friend_id)), "-m", message],
647
+ capture_output=True,
648
+ text=True,
649
+ timeout=timeout,
650
+ )
651
+ if proc.returncode == 0:
652
+ return True, (proc.stdout or "").strip() or "ok"
653
+ err = (proc.stderr or proc.stdout or "").strip() or f"exit {proc.returncode}"
654
+ return False, err[:2000]
655
+ except subprocess.TimeoutExpired:
656
+ return False, "发送私信超时"
657
+ except Exception as e:
658
+ return False, str(e)
659
+
660
+
661
+ def notify_classify_dm(config_path: str, all_results: dict) -> dict:
662
+ """对配置了 friendId 的用户推送 DM。返回 {用户名: {success, friendId, friendLabel?, detail?}}。"""
663
+ basic_config = get_config(config_path)
664
+ out = {}
665
+ for user in basic_config.get("users", []):
666
+ xid = _user_friend_id(user)
667
+ name = user.get("name") or ""
668
+ if xid is None:
669
+ continue
670
+ try:
671
+ xid_int = int(xid)
672
+ except (TypeError, ValueError):
673
+ bad = {"success": False, "detail": f"无效的 friendId: {xid!r}"}
674
+ lab_bad = _user_friend_label(user)
675
+ if isinstance(lab_bad, str) and lab_bad.strip():
676
+ bad["friendLabel"] = lab_bad.strip()
677
+ out[name if name else f"invalid_friend_id:{xid}"] = bad
678
+ continue
679
+ if xid_int <= 0:
680
+ fail_e = {"success": False, "detail": "friendId 须为正整数", "friendId": xid_int}
681
+ lab0 = _user_friend_label(user)
682
+ if isinstance(lab0, str) and lab0.strip():
683
+ fail_e["friendLabel"] = lab0.strip()
684
+ out[name] = fail_e
685
+ continue
686
+ lab = _user_friend_label(user)
687
+ if isinstance(lab, str) and lab.strip():
688
+ label_s = lab.strip()
689
+ else:
690
+ label_s = None
691
+ items = all_results.get(name, [])
692
+ msg = _format_classify_dm_message(name, items)
693
+ ok, detail = _send_classify_dm(xid_int, msg)
694
+ entry = {"success": ok, "friendId": xid_int}
695
+ if label_s:
696
+ entry["friendLabel"] = label_s
697
+ if not ok:
698
+ entry["detail"] = detail
699
+ out[name] = entry
700
+ return out
701
+
702
+
703
+ def notify_pack_link_dm(config_path: str, name_to_url: dict) -> dict:
704
+ """pack 成功后,向配置了 friendId 的用户私信下载链接。name_to_url: {本地用户名: url}。"""
705
+ out = {}
706
+ for name, url in (name_to_url or {}).items():
707
+ if not name or name == "quick_search":
708
+ continue
709
+ if not url:
710
+ continue
711
+ user = _find_user_by_name(config_path, name)
712
+ if not user:
713
+ continue
714
+ xid = _user_friend_id(user)
715
+ if xid is None:
716
+ continue
717
+ try:
718
+ xid_int = int(xid)
719
+ except (TypeError, ValueError):
720
+ bad = {"success": False, "detail": f"无效的 friendId: {xid!r}", "url": url}
721
+ lab_bad = _user_friend_label(user)
722
+ if isinstance(lab_bad, str) and lab_bad.strip():
723
+ bad["friendLabel"] = lab_bad.strip()
724
+ out[name] = bad
725
+ continue
726
+ if xid_int <= 0:
727
+ fail_e = {"success": False, "detail": "friendId 须为正整数", "friendId": xid_int, "url": url}
728
+ lab0 = _user_friend_label(user)
729
+ if isinstance(lab0, str) and lab0.strip():
730
+ fail_e["friendLabel"] = lab0.strip()
731
+ out[name] = fail_e
732
+ continue
733
+ lab = _user_friend_label(user)
734
+ label_s = lab.strip() if isinstance(lab, str) and lab.strip() else None
735
+ msg = _format_pack_link_dm_message(name, url)
736
+ ok, detail = _send_classify_dm(xid_int, msg)
737
+ entry = {"success": ok, "friendId": xid_int, "url": url}
738
+ if label_s:
739
+ entry["friendLabel"] = label_s
740
+ if not ok:
741
+ entry["detail"] = detail
742
+ out[name] = entry
743
+ return out
744
+
745
+
746
+ def _fmt_dm_line_ok(disp: str) -> str:
747
+ """dm_lines:私信发送成功(展示虾友昵称或 friendId)。"""
748
+ return f"✅ 💌 已向虾友「{disp}」发送成功"
749
+
750
+
751
+ def _fmt_dm_line_fail(disp: str, detail: str) -> str:
752
+ """dm_lines:私信发送失败。"""
753
+ return f"❌ 💢 虾友「{disp}」发送失败:{detail}"
754
+
755
+
756
+ def _fmt_dm_line_no_friend(name: str) -> str:
757
+ """dm_lines:未配置 friendId。"""
758
+ return f"📭 「{name}」暂未绑定虾友,跳过私信"
759
+
760
+
761
+ def _fmt_pack_line_fail(name: str, err: str) -> str:
762
+ """dm_lines:pack 打包失败。"""
763
+ return f"📦❌ 「{name}」下载包生成失败:{err}"
764
+
765
+
766
+ def build_pack_dm_status_lines(
767
+ config_path: str,
768
+ results_dict: dict,
769
+ user_urls: dict,
770
+ errors: dict | None,
771
+ pack_dm_out: dict,
772
+ ) -> list:
773
+ """pack 后的虾友发送状态短句(与 classify 的 dm_lines 同一套 emoji 模板)。"""
774
+ lines = []
775
+ errors = errors or {}
776
+ pack_dm_out = pack_dm_out or {}
777
+ for name in results_dict.keys():
778
+ if name == "quick_search":
779
+ continue
780
+ if name in errors and name not in user_urls:
781
+ lines.append(_fmt_pack_line_fail(name, str(errors[name])))
782
+ continue
783
+ if name not in user_urls:
784
+ continue
785
+ u = _find_user_by_name(config_path, name) or {}
786
+ xid = _user_friend_id(u)
787
+ if xid is None:
788
+ lines.append(_fmt_dm_line_no_friend(name))
789
+ continue
790
+
791
+ def _disp(uu) -> str:
792
+ lab = _user_friend_label(uu)
793
+ if isinstance(lab, str) and lab.strip():
794
+ return lab.strip()
795
+ try:
796
+ return str(int(_user_friend_id(uu)))
797
+ except (TypeError, ValueError):
798
+ return str(_user_friend_id(uu))
799
+
800
+ try:
801
+ xid_int = int(xid)
802
+ except (TypeError, ValueError):
803
+ disp = _disp(u)
804
+ info = pack_dm_out.get(name)
805
+ if info and not info.get("success"):
806
+ det = (info.get("detail") or "未知错误").strip()
807
+ lines.append(_fmt_dm_line_fail(disp, det))
808
+ else:
809
+ lines.append(_fmt_dm_line_fail(disp, "friendId 无效"))
810
+ continue
811
+
812
+ if xid_int <= 0:
813
+ disp = _disp(u)
814
+ info = pack_dm_out.get(name)
815
+ if info and not info.get("success"):
816
+ det = (info.get("detail") or "未知错误").strip()
817
+ lines.append(_fmt_dm_line_fail(disp, det))
818
+ else:
819
+ lines.append(_fmt_dm_line_fail(disp, "friendId 须为正整数"))
820
+ continue
821
+
822
+ disp = _disp(u)
823
+ info = pack_dm_out.get(name)
824
+ if info and info.get("success"):
825
+ lines.append(_fmt_dm_line_ok(disp))
826
+ elif info:
827
+ det = (info.get("detail") or "未知错误").strip()
828
+ lines.append(_fmt_dm_line_fail(disp, det))
829
+ else:
830
+ lines.append(_fmt_dm_line_fail(disp, "未返回状态"))
831
+ return lines
832
+
833
+
834
+ def build_dm_status_lines(config_path: str, all_results: dict, dm_out: dict) -> list:
835
+ """按本轮参与分类的用户顺序,生成最终提示用短句(含 emoji,见 _fmt_dm_line_*)。"""
836
+ basic_config = get_config(config_path)
837
+ name_to_user = {u.get("name"): u for u in basic_config.get("users", []) if u.get("name")}
838
+ lines = []
839
+ for name in all_results.keys():
840
+ u = name_to_user.get(name, {})
841
+ xid = _user_friend_id(u)
842
+ if xid is None:
843
+ lines.append(_fmt_dm_line_no_friend(name))
844
+ continue
845
+
846
+ def _disp_for_xid(raw, fallback_int=None) -> str:
847
+ lab = _user_friend_label(u)
848
+ if isinstance(lab, str) and lab.strip():
849
+ return lab.strip()
850
+ if fallback_int is not None:
851
+ return str(fallback_int)
852
+ return str(raw)
853
+
854
+ try:
855
+ xid_int = int(xid)
856
+ except (TypeError, ValueError):
857
+ disp = _disp_for_xid(xid, None)
858
+ info = dm_out.get(name) if dm_out else None
859
+ if info and not info.get("success"):
860
+ det = (info.get("detail") or "未知错误").strip()
861
+ lines.append(_fmt_dm_line_fail(disp, det))
862
+ else:
863
+ lines.append(_fmt_dm_line_fail(disp, "friendId 无效"))
864
+ continue
865
+
866
+ if xid_int <= 0:
867
+ disp = _disp_for_xid(xid, xid_int)
868
+ info = dm_out.get(name) if dm_out else None
869
+ if info and not info.get("success"):
870
+ det = (info.get("detail") or "未知错误").strip()
871
+ lines.append(_fmt_dm_line_fail(disp, det))
872
+ else:
873
+ lines.append(_fmt_dm_line_fail(disp, "friendId 须为正整数"))
874
+ continue
875
+
876
+ disp = _disp_for_xid(xid, xid_int)
877
+ info = dm_out.get(name) if dm_out else None
878
+ if info and info.get("success"):
879
+ lines.append(_fmt_dm_line_ok(disp))
880
+ elif info:
881
+ det = (info.get("detail") or "未知错误").strip()
882
+ lines.append(_fmt_dm_line_fail(disp, det))
883
+ else:
884
+ lines.append(_fmt_dm_line_fail(disp, "未返回状态"))
885
+ return lines
886
+
887
+
566
888
  def copy_results_to_folder(results, target_folder, user_name=None):
567
889
  """将搜索结果中的照片复制到指定文件夹。
568
890
  如果指定了 user_name,会在 target_folder 下创建以用户名命名的子目录。
@@ -667,6 +989,21 @@ def main():
667
989
  sp = subparsers.add_parser("add", help="注册新用户")
668
990
  sp.add_argument("name", help="用户名")
669
991
  sp.add_argument("image", help="照片路径")
992
+ sp.add_argument(
993
+ "--friend-id",
994
+ type=int,
995
+ default=None,
996
+ dest="friend_id",
997
+ metavar="ID",
998
+ help="可选:好友 userId(friendId)。设置后,一键 classify 会将该用户分类结果自动 DM 到此账号",
999
+ )
1000
+ sp.add_argument(
1001
+ "--friend-label",
1002
+ default=None,
1003
+ dest="friend_label",
1004
+ metavar="NAME",
1005
+ help="可选:DM 展示名(与自然语言「虾友:xxx」对应,用于最终提示括号内文案)",
1006
+ )
670
1007
 
671
1008
  # replace — 替换用户照片
672
1009
  sp = subparsers.add_parser("replace", help="替换用户照片(删除旧照片)")
@@ -738,14 +1075,36 @@ def main():
738
1075
  _json_output({"exists": exists, "name": args.name})
739
1076
 
740
1077
  elif args.command == "add":
741
- ok, msg = add_user_config(args.name, args.image, cfg)
742
- result = {"success": ok, "message": msg, "name": args.name}
743
- if ok:
744
- try:
745
- result["image_url"] = convert_to_url(args.image, timeout=30)
746
- except Exception:
747
- result["image_url"] = None
748
- _json_output(result)
1078
+ friend_id = getattr(args, "friend_id", None)
1079
+ raw_label = getattr(args, "friend_label", None)
1080
+ friend_label = str(raw_label).strip() if raw_label is not None and str(raw_label).strip() else None
1081
+ if friend_id is not None and friend_id <= 0:
1082
+ _json_output(
1083
+ {
1084
+ "success": False,
1085
+ "message": "friendId 须为正整数",
1086
+ "name": args.name,
1087
+ }
1088
+ )
1089
+ else:
1090
+ ok, msg = add_user_config(
1091
+ args.name,
1092
+ args.image,
1093
+ cfg,
1094
+ friend_id=friend_id,
1095
+ friend_label=friend_label,
1096
+ )
1097
+ result = {"success": ok, "message": msg, "name": args.name}
1098
+ if friend_id is not None:
1099
+ result["friendId"] = friend_id
1100
+ if friend_label:
1101
+ result["friendLabel"] = friend_label
1102
+ if ok:
1103
+ try:
1104
+ result["image_url"] = convert_to_url(args.image, timeout=30)
1105
+ except Exception:
1106
+ result["image_url"] = None
1107
+ _json_output(result)
749
1108
 
750
1109
  elif args.command == "replace":
751
1110
  ok, msg = replace_user_embeding_config(args.name, args.image, cfg)
@@ -791,7 +1150,13 @@ def main():
791
1150
  summary = {}
792
1151
  for name, items in all_results.items():
793
1152
  summary[name] = {"count": len(items), "results": items}
794
- _json_output({"user_count": len(all_results), "users": summary})
1153
+ payload = {"user_count": len(all_results), "users": summary}
1154
+ dm_status = notify_classify_dm(cfg, all_results)
1155
+ if dm_status:
1156
+ payload["dm"] = dm_status
1157
+ if all_results:
1158
+ payload["dm_lines"] = build_dm_status_lines(cfg, all_results, dm_status or {})
1159
+ _json_output(payload)
795
1160
 
796
1161
  elif args.command == "copy":
797
1162
  results = _load_search_results(cfg, args.name)
@@ -815,12 +1180,32 @@ def main():
815
1180
  user_urls[user_name] = url
816
1181
  else:
817
1182
  errors[user_name] = info
818
- _json_output({"success": bool(user_urls), "user_urls": user_urls,
819
- "errors": errors if errors else None})
1183
+ pack_dm = notify_pack_link_dm(cfg, user_urls)
1184
+ dm_lines = build_pack_dm_status_lines(cfg, results, user_urls, errors, pack_dm)
1185
+ payload = {
1186
+ "success": bool(user_urls),
1187
+ "user_urls": user_urls,
1188
+ "errors": errors if errors else None,
1189
+ }
1190
+ if pack_dm:
1191
+ payload["dm"] = pack_dm
1192
+ if dm_lines:
1193
+ payload["dm_lines"] = dm_lines
1194
+ _json_output(payload)
820
1195
  else:
821
1196
  url, info = pack_results_to_url(results, args.archive_name, args.timeout)
822
1197
  if url:
823
- _json_output({"success": True, "url": url, "zip_path": info})
1198
+ payload = {"success": True, "url": url, "zip_path": info}
1199
+ if args.name:
1200
+ pack_dm = notify_pack_link_dm(cfg, {args.name: url})
1201
+ dm_lines = build_pack_dm_status_lines(
1202
+ cfg, {args.name: results}, {args.name: url}, None, pack_dm
1203
+ )
1204
+ if pack_dm:
1205
+ payload["dm"] = pack_dm
1206
+ if dm_lines:
1207
+ payload["dm_lines"] = dm_lines
1208
+ _json_output(payload)
824
1209
  else:
825
1210
  _json_output({"success": False, "message": info})
826
1211