sophhub 0.4.23 → 0.4.25

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sophhub",
3
- "version": "0.4.23",
3
+ "version": "0.4.25",
4
4
  "description": "SophHub CLI - Manage and download AI Agent skills and agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,12 +1,19 @@
1
1
  {
2
2
  "name": "agent-install",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "types": [
5
5
  "store"
6
6
  ],
7
7
  "displayName": "Agent安装",
8
8
  "description": "通用 Agent 安装与升级。",
9
9
  "changelog": [
10
+ {
11
+ "changes": [
12
+ "新增 install_agent_skills.py:Agent 安装/升级时重新下载 auto_install skill 并直接替换 workspace/skills 目录"
13
+ ],
14
+ "date": "2026-05-19",
15
+ "version": "0.1.6"
16
+ },
10
17
  {
11
18
  "changes": [
12
19
  "update_openclaw 根据 .config.json 的 auto_generate_image_description 同步自动生成图片描述开关(默认开启)"
@@ -30,5 +37,5 @@
30
37
  }
31
38
  ],
32
39
  "createdAt": "2026-04-21",
33
- "updatedAt": "2026-05-15"
40
+ "updatedAt": "2026-05-19"
34
41
  }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-install
3
3
  description: 安装或升级通用 OpenClaw Agent(含按 .config.json 自动下载 skill)。Use when the user asks to install an agent, upgrade an agent, download agent config, back up an existing agent, update openclaw.json, or install skills listed with auto_install.
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  ---
6
6
 
7
7
  # Agent Install
@@ -191,17 +191,14 @@ uv run {baseDir}/scripts/update_openclaw.py \
191
191
 
192
192
  在 Step 3 成功、`openclaw.json` 已写入后执行。
193
193
 
194
- **Skill**:读 workspace 内 `.config.json` 的 `skills`,对 **`auto_install: true`** 的条目:
194
+ **Skill 重新安装**:读 workspace 内 `.config.json` 的 `skills`,对 **`auto_install: true`** 的条目**依次**下载并**直接替换** `{workspace}/skills/{skill_name}` 目录(先删旧目录再写入新包;本地对该 skill 的改动会被覆盖):
195
195
 
196
196
  ```bash
197
- mkdir -p "{workspace}/skills"
198
- ```
199
-
200
- ```bash
201
- npx -y sophhub@latest download {skill_name} -o "{workspace}/skills"
197
+ uv run {baseDir}/scripts/install_agent_skills.py \
198
+ --workspace "{workspace}"
202
199
  ```
203
200
 
204
- `{workspace}` 为本 Agent 工作目录;多条**依次**下载。必要时加 `--type builtin` / `--type store`。
201
+ `{workspace}` 为本 Agent 工作目录。新装与升级/重置均须执行本步。
205
202
 
206
203
  **输出模版**
207
204
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-install"
3
- version = "0.1.5"
3
+ version = "0.1.6"
4
4
  description = "通用 Agent 安装与升级"
5
5
  requires-python = ">=3.10"
6
6
  dependencies = []
@@ -185,6 +185,30 @@ def normalize_skills(skills: Any) -> list[str]:
185
185
  return list(dict.fromkeys(result))
186
186
 
187
187
 
188
+ def get_auto_install_skills(agent_def: dict[str, Any]) -> list[dict[str, Any]]:
189
+ """返回 .config.json 中 auto_install: true 的 skill 条目(保序、去重)。"""
190
+ skills = agent_def.get("skills")
191
+ if not isinstance(skills, list):
192
+ return []
193
+
194
+ result: list[dict[str, Any]] = []
195
+ seen: set[str] = set()
196
+ for item in skills:
197
+ if not isinstance(item, dict):
198
+ continue
199
+ if item.get("auto_install") is not True:
200
+ continue
201
+ name = item.get("name")
202
+ if not isinstance(name, str) or not name.strip():
203
+ continue
204
+ skill_name = name.strip()
205
+ if skill_name in seen:
206
+ continue
207
+ seen.add(skill_name)
208
+ result.append(item)
209
+ return result
210
+
211
+
188
212
  def normalize_dependencies(dependencies: Any) -> list[str]:
189
213
  if not isinstance(dependencies, list):
190
214
  return []
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ from pathlib import Path
11
+
12
+ from common import get_auto_install_skills, load_agent_definition, load_json
13
+
14
+
15
+ def run_sophhub_download(skill_name: str, output_dir: Path, *, timeout: int = 300) -> subprocess.CompletedProcess[str]:
16
+ return subprocess.run(
17
+ ["npx", "-y", "sophhub@latest", "download", skill_name, "-o", str(output_dir)],
18
+ capture_output=True,
19
+ text=True,
20
+ check=False,
21
+ timeout=timeout,
22
+ )
23
+
24
+
25
+ def replace_skill_directory(source_dir: Path, target_dir: Path) -> None:
26
+ """删除目标 skill 目录并用新下载内容完整替换。"""
27
+ if target_dir.exists():
28
+ shutil.rmtree(target_dir)
29
+ shutil.copytree(source_dir, target_dir)
30
+
31
+
32
+ def install_single_skill(skill_name: str, skills_dir: Path) -> dict[str, object]:
33
+ target_dir = skills_dir / skill_name
34
+
35
+ with tempfile.TemporaryDirectory(prefix=f"agent-install-skill-{skill_name}-") as tmp:
36
+ tmp_path = Path(tmp)
37
+ proc = run_sophhub_download(skill_name, tmp_path)
38
+
39
+ downloaded_dir = tmp_path / skill_name
40
+ if proc.returncode != 0:
41
+ return {
42
+ "name": skill_name,
43
+ "ok": False,
44
+ "reason": "download_failed",
45
+ "returncode": proc.returncode,
46
+ "stdout": proc.stdout.strip(),
47
+ "stderr": proc.stderr.strip(),
48
+ }
49
+
50
+ if not downloaded_dir.is_dir():
51
+ return {
52
+ "name": skill_name,
53
+ "ok": False,
54
+ "reason": "download_missing_directory",
55
+ "expected_path": str(downloaded_dir),
56
+ "stdout": proc.stdout.strip(),
57
+ "stderr": proc.stderr.strip(),
58
+ }
59
+
60
+ skills_dir.mkdir(parents=True, exist_ok=True)
61
+ had_existing = target_dir.exists()
62
+ replace_skill_directory(downloaded_dir, target_dir)
63
+
64
+ return {
65
+ "name": skill_name,
66
+ "ok": True,
67
+ "target_dir": str(target_dir),
68
+ "replaced": had_existing,
69
+ }
70
+
71
+
72
+ def load_agent_def_for_workspace(
73
+ workspace: Path,
74
+ *,
75
+ agent_id: str | None = None,
76
+ source_path: Path | None = None,
77
+ ) -> dict[str, object]:
78
+ config_path = workspace / ".config.json"
79
+ if config_path.is_file():
80
+ return load_json(config_path)
81
+ if agent_id:
82
+ return load_agent_definition(agent_id, source_path)
83
+ raise ValueError("workspace 下无 .config.json,请提供 --agent-id")
84
+
85
+
86
+ def install_agent_skills(
87
+ workspace: Path,
88
+ *,
89
+ agent_id: str | None = None,
90
+ source_path: Path | None = None,
91
+ ) -> dict[str, object]:
92
+ agent_def = load_agent_def_for_workspace(workspace, agent_id=agent_id, source_path=source_path)
93
+ auto_install = get_auto_install_skills(agent_def)
94
+ skills_dir = workspace / "skills"
95
+
96
+ if not auto_install:
97
+ return {
98
+ "ok": True,
99
+ "workspace": str(workspace),
100
+ "skills_dir": str(skills_dir),
101
+ "installed": [],
102
+ "failed": [],
103
+ "details": [],
104
+ "message": "无 auto_install skill,跳过。",
105
+ }
106
+
107
+ results: list[dict[str, object]] = []
108
+ for item in auto_install:
109
+ skill_name = str(item["name"])
110
+ results.append(install_single_skill(skill_name, skills_dir))
111
+
112
+ failed = [item for item in results if not item.get("ok")]
113
+ installed = [str(item["name"]) for item in results if item.get("ok")]
114
+
115
+ return {
116
+ "ok": not failed,
117
+ "workspace": str(workspace),
118
+ "skills_dir": str(skills_dir),
119
+ "installed": installed,
120
+ "failed": [
121
+ {
122
+ "name": str(item["name"]),
123
+ "reason": item.get("reason"),
124
+ "stderr": item.get("stderr"),
125
+ }
126
+ for item in failed
127
+ ],
128
+ "details": results,
129
+ "message": (
130
+ f"已重新安装 {len(installed)} 个 skill(直接替换目录)。"
131
+ if not failed
132
+ else f"{len(failed)} 个 skill 安装失败,已成功 {len(installed)} 个。"
133
+ ),
134
+ }
135
+
136
+
137
+ def main() -> int:
138
+ parser = argparse.ArgumentParser(description="安装或重新安装 Agent 的 auto_install skills(直接替换目录)")
139
+ parser.add_argument("--workspace", required=True, help="Agent workspace 路径")
140
+ parser.add_argument("--agent-id", help="Agent ID(workspace 尚无 .config.json 时使用)")
141
+ parser.add_argument("--source-path", help="Agent 下载目录")
142
+ args = parser.parse_args()
143
+
144
+ try:
145
+ result = install_agent_skills(
146
+ Path(args.workspace).expanduser().resolve(),
147
+ agent_id=args.agent_id,
148
+ source_path=Path(args.source_path).expanduser().resolve() if args.source_path else None,
149
+ )
150
+ except (ValueError, FileNotFoundError) as exc:
151
+ payload = {"ok": False, "message": str(exc)}
152
+ json.dump(payload, sys.stdout, indent=2, ensure_ascii=False)
153
+ sys.stdout.write("\n")
154
+ return 1
155
+
156
+ json.dump(result, sys.stdout, indent=2, ensure_ascii=False)
157
+ sys.stdout.write("\n")
158
+ return 0 if result.get("ok") else 1
159
+
160
+
161
+ if __name__ == "__main__":
162
+ raise SystemExit(main())
@@ -1,12 +1,19 @@
1
1
  {
2
2
  "name": "image-classify",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "types": [
5
5
  "store"
6
6
  ],
7
7
  "displayName": "照片分类器",
8
- "description": "通过人脸识别对照片进行分类、搜索和管理",
8
+ "description": "照片分类器:注册人脸、按人搜索/一键分类相册。当用户说用照片分类器查找或搜索某人、对文件夹分类、添加/删除照片分类器用户时使用。",
9
9
  "changelog": [
10
+ {
11
+ "version": "1.0.5",
12
+ "date": "2026-05-19",
13
+ "changes": [
14
+ "配置与注册照片迁至与 images 同级的 .image-classify/ 目录;阈值改为脚本常量;首次运行从 .references/ 或 skill 内 references/ 自动迁移并删除旧目录"
15
+ ]
16
+ },
10
17
  {
11
18
  "version": "1.0.4",
12
19
  "date": "2026-05-18",
@@ -45,5 +52,5 @@
45
52
  }
46
53
  ],
47
54
  "createdAt": "2026-04-10",
48
- "updatedAt": "2026-05-18"
55
+ "updatedAt": "2026-05-19"
49
56
  }
@@ -17,13 +17,14 @@ description: 照片分类器。通过人脸识别对照片进行分类、搜索
17
17
 
18
18
  - Python 3.10+、uv
19
19
  - 依赖:`requests`, `opencv-python`, `numpy`, `sophnet_tools`(项目内置)
20
- - 配置文件:`{baseDir}/references/config.json`
20
+ - 配置文件:`{dataRoot}/.image-classify/config.json`(与 `images` 同级;首次运行可从 `.references/` 或 `{baseDir}/references/` 迁移)
21
21
  - 若使用 **DM 自动推送**(注册时 `--friend-id`、一键 `classify` 后推送结果),需本机存在有效 JWT:`/home/node/.openclaw/jwt.json`(与 `send_dm_message.py` 一致)
22
22
 
23
23
  ## 核心概念
24
24
 
25
25
  - **`{baseDir}`**:本 skill 根目录,即 `skills/image-classify`,调用时替换为实际绝对路径。
26
- - **配置文件**:`{baseDir}/references/config.json`,存储所有注册用户的名称、照片路径、人脸 embedding 向量,以及可选 **DM 绑定**:`friendId`(好友 userId)、`friendLabel`(可选展示名)。旧字段 `xia_you_hao` / `xia_you_label` 仍可被脚本读取。
26
+ - **`{dataRoot}`**:与 `images` 同级的工作区根目录(由脚本自动解析,本仓库内通常为 `skills/`)。
27
+ - **配置文件**:`{dataRoot}/.image-classify/config.json`,存储注册用户、照片路径(位于 `.image-classify/`)、人脸 embedding、搜索结果,以及可选 **DM 绑定**(`friendId` / `friendLabel`;旧字段 `xia_you_hao` / `xia_you_label` 仍兼容)。
27
28
  - **脚本入口**:`{baseDir}/scripts/face_search.py`,通过 `uv run` 以子命令方式调用。
28
29
  - **所有命令输出均为 JSON 格式**,方便解析结果。
29
30
 
@@ -33,7 +34,7 @@ description: 照片分类器。通过人脸识别对照片进行分类、搜索
33
34
  uv run {baseDir}/scripts/face_search.py [-c CONFIG_PATH] <command> [args...]
34
35
  ```
35
36
 
36
- `-c` / `--config` 可选,指定配置文件路径,默认为 `references/config.json`(相对于执行目录)。建议始终传入绝对路径 `{baseDir}/references/config.json`。
37
+ `-c` / `--config` 可选;省略时自动使用 `{dataRoot}/.image-classify/config.json`。
37
38
 
38
39
  ## 可用命令一览
39
40
 
@@ -63,12 +64,12 @@ uv run {baseDir}/scripts/face_search.py [-c CONFIG_PATH] <command> [args...]
63
64
 
64
65
  ### 未指定搜索目录时(`search` / `quick-search` / `classify` 共用)
65
66
 
66
- 待搜索根目录由脚本内 `get_images_path()` 解析(通常为仓库上级 `images`)。其下按日期命名的子目录(`YYYYMMDD`)为可选搜索范围。
67
+ 待搜索根目录为 `{dataRoot}/images`(与 `.image-classify` 同级,由脚本自动解析)。其下按日期命名的子目录(`YYYYMMDD`)为可选搜索范围。
67
68
 
68
69
  若用户**未说明**搜索目录(或只说「默认目录」「最近上传的」等),**不要猜测路径**,先执行:
69
70
 
70
71
  ```bash
71
- uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json list-date-folders
72
+ uv run {baseDir}/scripts/face_search.py list-date-folders
72
73
  ```
73
74
 
74
75
  输出 JSON 字段说明:
@@ -96,20 +97,20 @@ uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json list
96
97
 
97
98
  **流程**:
98
99
 
99
- 1. 获取用户提供的**用户名**、**照片路径**,以及是否绑定 **DM 接收方**(均可选)。用户常以自然语言写出类似:**虾友:xxx,ID:23725**(虾友即 DM 对象;**23725** 即 `friendId`)。解析规则:**xxx** → `--friend-label`;**23725** → `--friend-id`(正整数,与 `send_dm_message.py` 的 `--user-id` / 接口 `userId` 一致)。可只填 `friendId`、不填展示名。
100
+ 1. 获取用户提供的**用户名**、**照片路径**,以及是否绑定 **DM 接收方**(均可选)。用户常以自然语言写出类似:**虾友:xxx,ID:23725**。解析规则:**xxx** → `--friend-label`;**23725** → `--friend-id`(正整数)。可只填 `friendId`、不填展示名。
100
101
  2. 检查用户名是否已存在:
101
102
  ```bash
102
- uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json check "张三"
103
+ uv run {baseDir}/scripts/face_search.py check "张三"
103
104
  ```
104
105
  输出:`{"exists": true/false, "name": "张三"}`
105
106
 
106
107
  3. **如果不存在** → 注册新用户(无虾友号):
107
108
  ```bash
108
- uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json add "张三" "/path/to/photo.jpg"
109
+ uv run {baseDir}/scripts/face_search.py add "张三" "/path/to/photo.jpg"
109
110
  ```
110
111
  若用户提供了 `friendId`,追加 `--friend-id <正整数>`;若有展示名,追加 `--friend-label "xxx"`(注意转义或引号,避免 shell 拆词):
111
112
  ```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
+ uv run {baseDir}/scripts/face_search.py add "张三" "/path/to/photo.jpg" --friend-label "小李" --friend-id 23725
113
114
  ```
114
115
  输出:`{"success": true/false, "message": "...", "name": "张三", "image_url": "https://...", "friendId": 23725, "friendLabel": "小李"}`(`friendId` / `friendLabel` 仅在实际传入时出现)
115
116
  - `success: true` → 提示:`🎉✨ **注册成功** · xxx`,并使用返回的 `image_url` 展示照片:`![image](image_url)`;若返回含 `friendId` / `friendLabel`,可顺带一句已绑定 DM(不必冗长)
@@ -124,19 +125,19 @@ uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json add
124
125
  ```
125
126
  - **更新照片**:
126
127
  ```bash
127
- uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json replace "张三" "/path/to/new.jpg"
128
+ uv run {baseDir}/scripts/face_search.py replace "张三" "/path/to/new.jpg"
128
129
  ```
129
130
  输出包含 `image_url` 字段。
130
131
  - 成功:`✅🔄 **照片已更新** · xxx`,使用 `image_url` 展示照片:`![image](image_url)`
131
132
  - 失败:`❌🙈 照片中未发现有效的人脸信息,更新失败!`
132
133
  - **重新命名**:
133
134
  ```bash
134
- uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json rename "张三" "李三"
135
+ uv run {baseDir}/scripts/face_search.py rename "张三" "李三"
135
136
  ```
136
137
  - 提示:`✏️📛 **用户名已更新** · xxx → yyy`
137
138
  - **保留多张**:
138
139
  ```bash
139
- uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json append "张三" "/path/to/extra.jpg"
140
+ uv run {baseDir}/scripts/face_search.py append "张三" "/path/to/extra.jpg"
140
141
  ```
141
142
  输出包含 `image_url` 字段。
142
143
  - 成功:`✅➕ **新照片已追加** · xxx`,使用 `image_url` 展示照片:`![image](image_url)`
@@ -152,7 +153,7 @@ uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json add
152
153
  2. 检查用户名是否存在(`check` 命令),不存在则提示:`❌🔍 **未找到该用户** · 请确认姓名后重试`
153
154
  3. 使用**子会话**执行搜索(可能耗时较长):
154
155
  ```bash
155
- uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json search "张三" "<dir>"
156
+ uv run {baseDir}/scripts/face_search.py search "张三" "<dir>"
156
157
  ```
157
158
  (`<dir>` 为用户指定路径,或从 `list-date-folders` 选中的 `path`。)**命令返回后立刻关闭并删除该子会话。**
158
159
  输出:
@@ -178,7 +179,7 @@ uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json sear
178
179
  1. 获取用户提供的**照片路径**和**搜索目录**。若**未指定搜索目录**,先按「未指定搜索目录时」列出最近 3 个日期文件夹供选择(支持「更多」),得到 `<dir>` 后再继续。
179
180
  2. 使用**子会话**执行搜索(先提取人脸 embedding,再匹配,可能耗时较长):
180
181
  ```bash
181
- uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json quick-search "/path/to/face.jpg" "<dir>"
182
+ uv run {baseDir}/scripts/face_search.py quick-search "/path/to/face.jpg" "<dir>"
182
183
  ```
183
184
  **命令返回后立刻关闭并删除该子会话。**
184
185
  输出:
@@ -205,7 +206,7 @@ uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json quic
205
206
  1. 从用户表述中提取**搜索目录**。若**未指定搜索目录**,先按「未指定搜索目录时」列出最近 3 个日期文件夹供选择(支持「更多」),得到 `<dir>` 后再继续。
206
207
  2. 使用**子会话**执行分类(可能耗时较长):
207
208
  ```bash
208
- uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json classify "<dir>"
209
+ uv run {baseDir}/scripts/face_search.py classify "<dir>"
209
210
  ```
210
211
  **命令返回后立刻关闭并删除该子会话。**
211
212
  输出(节选):
@@ -260,7 +261,7 @@ uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json clas
260
261
  3. 提示确认:`⚠️🗑️ **确认删除用户 xxx?** · 此操作不可撤销`
261
262
  - 用户确认 →
262
263
  ```bash
263
- uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json delete "张三"
264
+ uv run {baseDir}/scripts/face_search.py delete "张三"
264
265
  ```
265
266
  提示:`🗑️✨ **用户已删除** · xxx`
266
267
  - 用户取消 → 提示:`↩️👋 **已取消删除**`
@@ -273,20 +274,20 @@ uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json clas
273
274
 
274
275
  - **单用户 `search`**(指定 `--name`):
275
276
  ```bash
276
- uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json pack --name "张三" --timeout 120
277
+ uv run {baseDir}/scripts/face_search.py pack --name "张三" --timeout 120
277
278
  ```
278
279
  输出(节选):`{"success": true, "url": "https://...", "zip_path": "/tmp/.../search_results.zip", "dm": {...}, "dm_lines": [...]}`(有 `friendId` 时含 `dm` / `dm_lines`)
279
280
  成功提示:`📦🔗 **下载链接已就绪** ✨` → `<url>` → 若有 `dm_lines`,再展示「下载链接私信状态」块。
280
281
 
281
282
  - **`quick-search`(直接搜索)**:结果存于 `quick_search_result`,执行 **不带 `--name`** 的 `pack`(与仅含直接搜索结果时一致):
282
283
  ```bash
283
- uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json pack --timeout 120
284
+ uv run {baseDir}/scripts/face_search.py pack --timeout 120
284
285
  ```
285
286
  成功提示:`📦🔗 **下载链接已就绪** ✨` → `<url>`(通常**无**虾友私信,因无注册用户上下文)
286
287
 
287
288
  - **一键 `classify`**(不指定 `--name`,**每位用户单独打包**):
288
289
  ```bash
289
- uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json pack --timeout 120
290
+ uv run {baseDir}/scripts/face_search.py pack --timeout 120
290
291
  ```
291
292
  输出示例(节选):
292
293
  ```json
@@ -319,7 +320,7 @@ uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json pack
319
320
  1. 获取用户提供的**图片路径**(支持单张或多张)
320
321
  2. 直接执行:
321
322
  ```bash
322
- uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json upload-image "/path/to/a.jpg" "/path/to/b.jpg"
323
+ uv run {baseDir}/scripts/face_search.py upload-image "/path/to/a.jpg" "/path/to/b.jpg"
323
324
  ```
324
325
  3. 单张时输出:`{"success": true/false, "folder_path": "/abs/path/to/images/YYYYMMDD", "file_path": "/abs/path/to/images/YYYYMMDD/a.jpg", "failed": []}`
325
326
  4. 多张时输出:`{"success": true/false, "folder_path": "/abs/path/to/images/YYYYMMDD", "uploaded": [...], "failed": [...]}`
@@ -328,41 +329,40 @@ uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json uplo
328
329
  ## Example Workflow
329
330
 
330
331
  - 用户:「把这张照片添加为张三」
331
- 1. `uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json check "张三"`
332
- 2. 不存在 → `uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json add "张三" "/path/to/photo.jpg"`
332
+ 1. `uv run {baseDir}/scripts/face_search.py check "张三"`
333
+ 2. 不存在 → `uv run {baseDir}/scripts/face_search.py add "张三" "/path/to/photo.jpg"`
333
334
  3. 提示:`🎉✨ **注册成功** · 张三`
334
335
 
335
336
  - 用户:「用照片分类器在 /home/photos 下查找张三」
336
- 1. `uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json check "张三"` → 存在
337
- 2. 子会话执行 `uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json search "张三" "/home/photos"`
337
+ 1. `uv run {baseDir}/scripts/face_search.py check "张三"` → 存在
338
+ 2. 子会话执行 `uv run {baseDir}/scripts/face_search.py search "张三" "/home/photos"`
338
339
  3. 展示结果表格 → `pack --name "张三" --timeout 120` 生成下载链接(见 §6)→ 若有 `dm_lines`,展示下载链接私信状态
339
340
 
340
341
  - 用户:「用这张照片在 /home/photos 里找找有没有类似的」
341
- 1. 子会话执行 `uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json quick-search "/tmp/face.jpg" "/home/photos"`
342
+ 1. 子会话执行 `uv run {baseDir}/scripts/face_search.py quick-search "/tmp/face.jpg" "/home/photos"`
342
343
  2. 成功 → 展示表格 → `pack --timeout 120`(不带 `--name`);失败 → 提示未检测到有效人脸
343
344
 
344
345
  - 用户:「用照片分类器对 /home/album 进行分类」
345
- 1. 子会话执行 `uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json classify "/home/album"`
346
+ 1. 子会话执行 `uv run {baseDir}/scripts/face_search.py classify "/home/album"`
346
347
  2. 展示各用户结果(及分类摘要 `dm_lines` 若有)→ `pack --timeout 120`(多用户各一链接,见 §6)→ 展示链接及 **`pack` 返回的 `dm_lines`**(下载链接私信状态)
347
348
 
348
349
  - 用户:「删除照片分类器中李四的信息」
349
- 1. `uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json check "李四"` → 存在
350
- 2. 提示确认 → 用户确认 → `uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json delete "李四"`
350
+ 1. `uv run {baseDir}/scripts/face_search.py check "李四"` → 存在
351
+ 2. 提示确认 → 用户确认 → `uv run {baseDir}/scripts/face_search.py delete "李四"`
351
352
  3. 提示:`🗑️✨ **用户已删除** · 李四`
352
353
 
353
354
  - 用户:「把这些图片放到待搜索目录」
354
- 1. `uv run {baseDir}/scripts/face_search.py -c {baseDir}/references/config.json upload-image "/tmp/a.jpg" "/tmp/b.jpg"`
355
+ 1. `uv run {baseDir}/scripts/face_search.py upload-image "/tmp/a.jpg" "/tmp/b.jpg"`
355
356
  2. 全部成功后提示:`✅📤 **已放入待搜索目录** → <folder_path> ✨`
356
357
 
357
358
  ## Notes
358
359
 
359
- - 建议始终通过 `-c` 传入 `{baseDir}/references/config.json` 的绝对路径,避免相对路径导致找不到配置文件。
360
+ - 配置与注册照片位于 `{dataRoot}/.image-classify/`;一般无需传 `-c`。首次运行若尚无 `.image-classify/config.json`,会按顺序从 `{dataRoot}/.references/` `{baseDir}/references/` 迁移并删除旧目录。
360
361
  - `search`、`quick-search` 和 `classify` 可能耗时较长(取决于搜索目录中的图片数量),应在**子会话**中执行;**结束后立即关闭并删除该子会话**,再将结果展示与 `pack` 放在**主会话**中完成。
361
362
  - `images/` 目录用于存放待搜索图片;`upload-image` 内部会创建当天目录并串行逐张处理图片。
362
363
  - 搜索过程中会在图片所在目录下创建 `.embedding` 隐藏目录缓存 embedding 结果,后续搜索同一目录会自动跳过已处理的图片。
363
364
  - `pack` 与 `copy` 均依赖 `config.json` 中保存的搜索结果(`search_result` 或 `quick_search_result` 等),须先执行 `search`、`classify` 或 `quick-search`。默认流程只需 **`pack`**;`copy` 为可选,由用户另行提出时再执行。
364
- - **`classify`** 会向 `friendId` 发**分类结果摘要**;**`pack`** 会向 `friendId` 发**打包下载链接**(两次私信、用途不同);若只需其一,可后续再改脚本或配置(当前实现为两者都发)。
365
- - `pack` 的压缩文件存放在系统临时目录中,上传完成后可忽略清理。上传超时默认 120 秒,文件特别大时可通过 `--timeout` 增大。
366
- - `config.json` 中的 `query_threshold`(默认 0.5)控制人脸检测置信度阈值,`search_similarity_threshold`(默认 0.3)控制搜索匹配的最低相似度。
365
+ - 已绑定 `friendId` 的用户:**`classify`** 会推送分类结果摘要;**`pack`** 会推送打包下载链接(两次私信、用途不同)。
366
+ - `pack` 的压缩文件在系统临时目录,上传完成后可忽略。默认上传超时 120 秒,大文件可通过 `--timeout` 增大。
367
367
  - `{baseDir}` 指本 skill 根目录(如 `skills/image-classify`),调用时替换为实际绝对路径。
368
368
  - `friendId`(虾友号)与可选 `friendLabel` 写在用户条目下;**注册时**通过 `add ... --friend-id` / `--friend-label` 写入。后续若需修改可编辑 `config.json`(本 skill 未单独提供改绑子命令)。旧键名仍兼容读取。
@@ -0,0 +1,11 @@
1
+ [project]
2
+ name = "image-classify"
3
+ version = "1.0.5"
4
+ description = "Photo classifier by face embedding similarity"
5
+ requires-python = ">=3.10"
6
+ dependencies = [
7
+ "opencv-python-headless>=4.8.0",
8
+ "numpy>=1.24.0",
9
+ "requests>=2.28.0",
10
+ "sophnet-tools>=0.0.1",
11
+ ]