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 +1 -1
- package/skills/agent-install/skill.json +9 -2
- package/skills/agent-install/src/SKILL.md +5 -8
- package/skills/agent-install/src/pyproject.toml +1 -1
- package/skills/agent-install/src/scripts/common.py +24 -0
- package/skills/agent-install/src/scripts/install_agent_skills.py +162 -0
- package/skills/image-classify/skill.json +10 -3
- package/skills/image-classify/src/SKILL.md +33 -33
- package/skills/image-classify/src/pyproject.toml +11 -0
- package/skills/image-classify/src/scripts/face_search.py +271 -58
- package/skills/online-bug-report/skill.json +55 -0
- package/skills/online-bug-report/src/SKILL.md +131 -0
- package/skills/online-bug-report/src/pyproject.toml +5 -0
- package/skills/online-bug-report/src/references/config.example.json +11 -0
- package/skills/online-bug-report/src/scripts/report_bug.py +631 -0
- package/skills/online-bug-report/src/secrets/bug-report.json +6 -0
- package/skills/image-classify/src/references/config.json +0 -4
package/package.json
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-install",
|
|
3
|
-
"version": "0.1.
|
|
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-
|
|
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.
|
|
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
|
|
194
|
+
**Skill 重新安装**:读 workspace 内 `.config.json` 的 `skills`,对 **`auto_install: true`** 的条目**依次**下载并**直接替换** `{workspace}/skills/{skill_name}` 目录(先删旧目录再写入新包;本地对该 skill 的改动会被覆盖):
|
|
195
195
|
|
|
196
196
|
```bash
|
|
197
|
-
|
|
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
|
|
201
|
+
`{workspace}` 为本 Agent 工作目录。新装与升级/重置均须执行本步。
|
|
205
202
|
|
|
206
203
|
**输出模版**
|
|
207
204
|
|
|
@@ -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.
|
|
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-
|
|
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
|
-
- 配置文件:`{
|
|
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
|
-
-
|
|
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`
|
|
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
|
-
|
|
67
|
+
待搜索根目录为 `{dataRoot}/images`(与 `.image-classify` 同级,由脚本自动解析)。其下按日期命名的子目录(`YYYYMMDD`)为可选搜索范围。
|
|
67
68
|
|
|
68
69
|
若用户**未说明**搜索目录(或只说「默认目录」「最近上传的」等),**不要猜测路径**,先执行:
|
|
69
70
|
|
|
70
71
|
```bash
|
|
71
|
-
uv run {baseDir}/scripts/face_search.py
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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` 展示照片:``;若返回含 `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
|
|
128
|
+
uv run {baseDir}/scripts/face_search.py replace "张三" "/path/to/new.jpg"
|
|
128
129
|
```
|
|
129
130
|
输出包含 `image_url` 字段。
|
|
130
131
|
- 成功:`✅🔄 **照片已更新** · xxx`,使用 `image_url` 展示照片:``
|
|
131
132
|
- 失败:`❌🙈 照片中未发现有效的人脸信息,更新失败!`
|
|
132
133
|
- **重新命名**:
|
|
133
134
|
```bash
|
|
134
|
-
uv run {baseDir}/scripts/face_search.py
|
|
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
|
|
140
|
+
uv run {baseDir}/scripts/face_search.py append "张三" "/path/to/extra.jpg"
|
|
140
141
|
```
|
|
141
142
|
输出包含 `image_url` 字段。
|
|
142
143
|
- 成功:`✅➕ **新照片已追加** · xxx`,使用 `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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
332
|
-
2. 不存在 → `uv run {baseDir}/scripts/face_search.py
|
|
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
|
|
337
|
-
2. 子会话执行 `uv run {baseDir}/scripts/face_search.py
|
|
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
|
|
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
|
|
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
|
|
350
|
-
2. 提示确认 → 用户确认 → `uv run {baseDir}/scripts/face_search.py
|
|
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
|
|
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
|
-
-
|
|
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
|
-
-
|
|
365
|
-
- `pack`
|
|
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
|
+
]
|