sophhub 0.2.1 → 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.
- package/package.json +1 -1
- package/skills/compact-context/skill.json +20 -0
- package/skills/compact-context/src/SKILL.md +133 -0
- package/skills/compact-context/src/scripts/check.sh +381 -0
- package/skills/compact-context/src/scripts/set-keep-recent.mjs +1337 -0
- package/skills/compact-context/src/scripts/setup.sh +96 -0
- package/skills/feishu-notes-assistant-universal/skill.json +20 -0
- package/skills/feishu-notes-assistant-universal/src/README.md +55 -0
- package/skills/feishu-notes-assistant-universal/src/SKILL.md +159 -0
- package/skills/feishu-notes-assistant-universal/src/bin/linux-amd64/lark-cli-openclaw +0 -0
- package/skills/feishu-notes-assistant-universal/src/bin/linux-arm64/lark-cli-openclaw +0 -0
- package/skills/feishu-notes-assistant-universal/src/scripts/_resolve_lark_cli.py +58 -0
- package/skills/feishu-notes-assistant-universal/src/scripts/openclaw_meeting_minutes.py +462 -0
- package/skills/feishu-notes-assistant-universal/src/scripts/openclaw_notes_crud.py +547 -0
- package/skills/feishu-notes-assistant-universal/src/scripts/openclaw_notes_crud_test.py +181 -0
- package/skills/feishu-notes-assistant-universal/src/scripts/run_meeting_minutes.py +80 -0
- package/skills/feishu-notes-assistant-universal/src/scripts/run_meeting_minutes.sh +5 -0
- package/skills/feishu-notes-assistant-universal/src/scripts/run_note_crud.py +32 -0
- package/skills/feishu-notes-assistant-universal/src/scripts/run_note_crud.sh +5 -0
- package/skills/image-classify/skill.json +5 -5
- package/skills/image-classify/src/SKILL.md +60 -67
- package/skills/image-classify/src/scripts/face_search.py +400 -15
- package/skills/image-classify/src/scripts/send_dm_message.py +332 -0
- package/skills/md2pdf-converter/skill.json +20 -0
- package/skills/md2pdf-converter/src/SKILL.md +244 -0
- package/skills/md2pdf-converter/src/_meta.json +6 -0
- package/skills/md2pdf-converter/src/scripts/generate_emoji_mapping.py +74 -0
- package/skills/md2pdf-converter/src/scripts/md2pdf-local.sh +291 -0
- package/skills/sophnet-bot-client/skill.json +20 -0
- package/skills/sophnet-bot-client/src/SKILL.md +255 -0
- package/skills/sophnet-bot-client/src/pyproject.toml +13 -0
- package/skills/sophnet-bot-client/src/scripts/__init__.py +0 -0
- package/skills/sophnet-bot-client/src/scripts/bot_client_proxy.py +165 -0
- package/skills/sophnet-bot-client/src/scripts/bot_client_safe.sh +29 -0
- package/skills/sophnet-bot-client/src/scripts/bot_client_setup.py +502 -0
- package/skills/sophnet-bot-client/src/tests/__init__.py +0 -0
- package/skills/sophnet-bot-client/src/tests/test_bot_client_proxy.py +255 -0
- package/skills/sophnet-bot-client/src/tests/test_bot_client_setup.py +679 -0
- package/skills/sophnet-bot-client/src/uv.lock +8 -0
- package/skills/sophnet-docx/skill.json +20 -0
- package/skills/sophnet-docx/src/SKILL.md +463 -0
- package/skills/sophnet-docx/src/package-lock.json +208 -0
- package/skills/sophnet-docx/src/package.json +16 -0
- package/skills/sophnet-docx/src/pyproject.toml +11 -0
- package/skills/sophnet-docx/src/scripts/__init__.py +1 -0
- package/skills/sophnet-docx/src/scripts/accept_changes.py +135 -0
- package/skills/sophnet-docx/src/scripts/comment.py +318 -0
- package/skills/sophnet-docx/src/scripts/ensure_uv_env.sh +68 -0
- package/skills/sophnet-docx/src/scripts/office/helpers/__init__.py +0 -0
- package/skills/sophnet-docx/src/scripts/office/helpers/merge_runs.py +199 -0
- package/skills/sophnet-docx/src/scripts/office/helpers/simplify_redlines.py +197 -0
- package/skills/sophnet-docx/src/scripts/office/pack.py +159 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/mce/mc.xsd +75 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/skills/sophnet-docx/src/scripts/office/soffice.py +183 -0
- package/skills/sophnet-docx/src/scripts/office/unpack.py +132 -0
- package/skills/sophnet-docx/src/scripts/office/validate.py +111 -0
- package/skills/sophnet-docx/src/scripts/office/validators/__init__.py +15 -0
- package/skills/sophnet-docx/src/scripts/office/validators/base.py +847 -0
- package/skills/sophnet-docx/src/scripts/office/validators/docx.py +446 -0
- package/skills/sophnet-docx/src/scripts/office/validators/pptx.py +275 -0
- package/skills/sophnet-docx/src/scripts/office/validators/redlining.py +247 -0
- package/skills/sophnet-docx/src/scripts/templates/comments.xml +3 -0
- package/skills/sophnet-docx/src/scripts/templates/commentsExtended.xml +3 -0
- package/skills/sophnet-docx/src/scripts/templates/commentsExtensible.xml +3 -0
- package/skills/sophnet-docx/src/scripts/templates/commentsIds.xml +3 -0
- package/skills/sophnet-docx/src/scripts/templates/people.xml +3 -0
- package/skills/sophnet-docx/src/scripts/upload_file.sh +96 -0
- package/skills/sophnet-docx/src/uv.lock +320 -0
- package/skills/sophnet-pdf/skill.json +20 -0
- package/skills/sophnet-pdf/src/SKILL.md +413 -0
- package/skills/sophnet-pdf/src/forms.md +297 -0
- package/skills/sophnet-pdf/src/pyproject.toml +14 -0
- package/skills/sophnet-pdf/src/reference.md +612 -0
- package/skills/sophnet-pdf/src/scripts/check_bounding_boxes.py +65 -0
- package/skills/sophnet-pdf/src/scripts/check_fillable_fields.py +11 -0
- package/skills/sophnet-pdf/src/scripts/convert_pdf_to_images.py +33 -0
- package/skills/sophnet-pdf/src/scripts/create_validation_image.py +37 -0
- package/skills/sophnet-pdf/src/scripts/enhance_tutorial.py +558 -0
- package/skills/sophnet-pdf/src/scripts/ensure_uv_env.sh +68 -0
- package/skills/sophnet-pdf/src/scripts/extract_form_field_info.py +122 -0
- package/skills/sophnet-pdf/src/scripts/extract_form_structure.py +115 -0
- package/skills/sophnet-pdf/src/scripts/extract_pdf_content.py +35 -0
- package/skills/sophnet-pdf/src/scripts/fill_fillable_fields.py +98 -0
- package/skills/sophnet-pdf/src/scripts/fill_pdf_form_with_annotations.py +107 -0
- package/skills/sophnet-pdf/src/scripts/upload_file.sh +88 -0
- package/skills/sophnet-pdf/src/uv.lock +537 -0
- package/skills/sophnet-xlsx/skill.json +20 -0
- package/skills/sophnet-xlsx/src/SKILL.md +399 -0
- package/skills/sophnet-xlsx/src/pyproject.toml +11 -0
- package/skills/sophnet-xlsx/src/scripts/ensure_uv_env.sh +68 -0
- package/skills/sophnet-xlsx/src/scripts/office/helpers/__init__.py +0 -0
- package/skills/sophnet-xlsx/src/scripts/office/helpers/merge_runs.py +199 -0
- package/skills/sophnet-xlsx/src/scripts/office/helpers/simplify_redlines.py +197 -0
- package/skills/sophnet-xlsx/src/scripts/office/pack.py +159 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/mce/mc.xsd +75 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/skills/sophnet-xlsx/src/scripts/office/soffice.py +183 -0
- package/skills/sophnet-xlsx/src/scripts/office/unpack.py +132 -0
- package/skills/sophnet-xlsx/src/scripts/office/validate.py +111 -0
- package/skills/sophnet-xlsx/src/scripts/office/validators/__init__.py +15 -0
- package/skills/sophnet-xlsx/src/scripts/office/validators/base.py +847 -0
- package/skills/sophnet-xlsx/src/scripts/office/validators/docx.py +446 -0
- package/skills/sophnet-xlsx/src/scripts/office/validators/pptx.py +275 -0
- package/skills/sophnet-xlsx/src/scripts/office/validators/redlining.py +247 -0
- package/skills/sophnet-xlsx/src/scripts/recalc.py +184 -0
- package/skills/sophnet-xlsx/src/scripts/upload_file.sh +96 -0
- package/skills/sophnet-xlsx/src/uv.lock +319 -0
- package/skills/wechat-article-publisher/skill.json +20 -0
- package/skills/wechat-article-publisher/src/SKILL.md +60 -0
- package/skills/wechat-article-publisher/src/config.json +7 -0
- package/skills/wechat-article-publisher/src/pyproject.toml +12 -0
- 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(
|
|
293
|
-
|
|
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
|
-
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
if
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
-
|
|
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
|
-
|
|
819
|
-
|
|
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
|
-
|
|
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
|
|