@aiyiran/myclaw 1.0.201 → 1.0.202

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 (37) hide show
  1. package/index.js +113 -25
  2. package/{inject-clear-models.js → injects/inject-clear-models.js} +1 -1
  3. package/{inject-image.js → injects/inject-image.js} +1 -1
  4. package/{inject-minimax.js → injects/inject-minimax.js} +1 -1
  5. package/{inject-search.js → injects/inject-search.js} +1 -1
  6. package/{inject-token.js → injects/inject-token.js} +1 -1
  7. package/injects/inject-tooldeny.js +50 -0
  8. package/{inject-zai.js → injects/inject-zai.js} +1 -1
  9. package/package.json +1 -1
  10. package/{patch-manifest.json → patches/patch-manifest.json} +15 -5
  11. package/{patch-reset.js → patches/patch-reset.js} +1 -1
  12. package/{patch-skill.js → patches/patch-skill.js} +6 -1
  13. package/pull.js +1 -1
  14. package/skills/vapi-image-gen-1.0.1/README.md +92 -0
  15. package/skills/vapi-image-gen-1.0.1/SKILL.md +75 -0
  16. package/skills/vapi-image-gen-1.0.1/_meta.json +6 -0
  17. package/skills/vapi-image-gen-1.0.1/scripts/gen.py +259 -0
  18. package/skills/yiran-skill-media/SKILL.md +74 -0
  19. package/skills/yiran-skill-media/config.json +26 -0
  20. package/skills/yiran-skill-media/references/image-api.md +88 -0
  21. package/skills/yiran-skill-media/references/music-api.md +120 -0
  22. package/skills/yiran-skill-media/scripts/generate.py +165 -0
  23. package/skills/yiran-skill-media/scripts/generation_log.json +20 -0
  24. package/skills/yiran-skill-media/scripts/image.sh +43 -0
  25. package/skills/yiran-skill-media/scripts/music.sh +46 -0
  26. package/skills/yiran-skill-media/scripts/providers/__init__.py +15 -0
  27. package/skills/yiran-skill-media/scripts/providers/__pycache__/__init__.cpython-311.pyc +0 -0
  28. package/skills/yiran-skill-media/scripts/providers/__pycache__/minimax_image.cpython-311.pyc +0 -0
  29. package/skills/yiran-skill-media/scripts/providers/__pycache__/minimax_music.cpython-311.pyc +0 -0
  30. package/skills/yiran-skill-media/scripts/providers/__pycache__/vapi_image.cpython-311.pyc +0 -0
  31. package/skills/yiran-skill-media/scripts/providers/minimax_image.py +75 -0
  32. package/skills/yiran-skill-media/scripts/providers/minimax_music.py +61 -0
  33. package/skills/yiran-skill-media/scripts/providers/vapi_image.py +63 -0
  34. package/skills/yiran-skill-media/scripts/registry.py +133 -0
  35. /package/{inject-workspaceAndSoul.js → injects/inject-workspaceAndSoul.js} +0 -0
  36. /package/{patch-agent.js → patches/patch-agent.js} +0 -0
  37. /package/{patch.js → patches/patch.js} +0 -0
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 统一资源生成调度器
4
+ 用法:
5
+ python3 generate.py image "prompt" [--aspect-ratio 16:9] [--output path]
6
+ python3 generate.py music "prompt" [--lyrics "歌词"] [--instrumental] [--output path]
7
+ """
8
+ import argparse
9
+ import json
10
+ import os
11
+ import sys
12
+ import tempfile
13
+ import time
14
+ from datetime import datetime
15
+
16
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
17
+ CONFIG_PATH = os.path.join(SCRIPT_DIR, "..", "config.json")
18
+
19
+
20
+ def load_config():
21
+ with open(CONFIG_PATH, "r", encoding="utf-8") as f:
22
+ return json.load(f)
23
+
24
+
25
+ def get_output_dir():
26
+ """Resolve output directory. Fallback to /tmp/media/ if primary is unwritable."""
27
+ workspace = os.environ.get("WORKSPACE_NAME", "main")
28
+ openclaw_home = os.environ.get("OPENCLAW_HOME", os.path.join(os.path.expanduser("~"), ".openclaw"))
29
+
30
+ if workspace == "main":
31
+ base = os.path.join(openclaw_home, "workspace")
32
+ else:
33
+ base = os.path.join(openclaw_home, f"workspace-{workspace}")
34
+
35
+ out_dir = os.path.join(base, "media")
36
+ try:
37
+ os.makedirs(out_dir, exist_ok=True)
38
+ return out_dir
39
+ except OSError:
40
+ # Primary dir unwritable (e.g. /root on macOS) → fallback to tmp
41
+ fallback = os.path.join(tempfile.gettempdir(), "media")
42
+ os.makedirs(fallback, exist_ok=True)
43
+ print(f"[warn] {out_dir} not writable, using {fallback}", file=sys.stderr)
44
+ return fallback
45
+
46
+
47
+ def make_output_path(out_dir, resource_type, ext):
48
+ """Generate timestamped filename."""
49
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
50
+ return os.path.join(out_dir, f"{resource_type}_{ts}.{ext}")
51
+
52
+
53
+ def append_log(log_path, entry):
54
+ """Append a generation record. Never raises — logging failure is non-fatal."""
55
+ try:
56
+ if os.path.exists(log_path):
57
+ with open(log_path, "r", encoding="utf-8") as f:
58
+ log = json.load(f)
59
+ else:
60
+ log = []
61
+
62
+ log.append(entry)
63
+
64
+ with open(log_path, "w", encoding="utf-8") as f:
65
+ json.dump(log, f, ensure_ascii=False, indent=2)
66
+
67
+ print(f"[log] 已记录到 {log_path}", file=sys.stderr)
68
+ except Exception as e:
69
+ print(f"[warn] log write failed: {e}", file=sys.stderr)
70
+
71
+
72
+ def dispatch(resource_type, prompt, **kwargs):
73
+ """Route to primary provider, fallback on failure.
74
+
75
+ Always tries primary first, then fallback if configured.
76
+ Returns (files, used_provider_cfg).
77
+ """
78
+ cfg = load_config()
79
+ resource_cfg = cfg.get(resource_type)
80
+ if not resource_cfg:
81
+ raise ValueError(f"unknown resource type: {resource_type}")
82
+
83
+ providers = [resource_cfg["primary"]]
84
+ if resource_cfg.get("fallback"):
85
+ providers.append(resource_cfg["fallback"])
86
+
87
+ sys.path.insert(0, SCRIPT_DIR)
88
+ from providers import get_adapter
89
+
90
+ errors = []
91
+ for provider_cfg in providers:
92
+ name = provider_cfg["provider"]
93
+ try:
94
+ adapter = get_adapter(name)
95
+ files = adapter.generate(prompt, config=provider_cfg, **kwargs)
96
+ return files, provider_cfg
97
+ except Exception as e:
98
+ msg = f"[{name}] failed: {e}"
99
+ print(msg, file=sys.stderr)
100
+ errors.append(msg)
101
+
102
+ raise RuntimeError("all providers failed:\n" + "\n".join(errors))
103
+
104
+
105
+ def main():
106
+ parser = argparse.ArgumentParser(description="Unified media generation dispatcher")
107
+ parser.add_argument("type", choices=["image", "music"], help="Resource type")
108
+ parser.add_argument("prompt", help="Generation prompt")
109
+ parser.add_argument("--aspect-ratio", default="1:1", help="Image aspect ratio (image only)")
110
+ parser.add_argument("--lyrics", default=None, help="Lyrics text (music only)")
111
+ parser.add_argument("--instrumental", action="store_true", help="Instrumental mode (music only)")
112
+ parser.add_argument("--output", default=None, help="Output file path")
113
+ args = parser.parse_args()
114
+
115
+ out_dir = get_output_dir()
116
+
117
+ if args.type == "image":
118
+ output_path = args.output or make_output_path(out_dir, "image", "png")
119
+ kwargs = {
120
+ "out_dir": out_dir,
121
+ "output_path": output_path,
122
+ "aspect_ratio": args.aspect_ratio,
123
+ }
124
+ else:
125
+ output_path = args.output or make_output_path(out_dir, "music", "mp3")
126
+ kwargs = {
127
+ "out_dir": out_dir,
128
+ "output_path": output_path,
129
+ "lyrics": args.lyrics,
130
+ "instrumental": args.instrumental,
131
+ }
132
+
133
+ start_time = time.time()
134
+ start_dt = datetime.now()
135
+
136
+ try:
137
+ files, used_provider = dispatch(args.type, args.prompt, **kwargs)
138
+ duration = round(time.time() - start_time, 2)
139
+ end_dt = datetime.now()
140
+
141
+ # Log — non-fatal
142
+ log_entry = {
143
+ "id": start_dt.strftime("%Y%m%d_%H%M%S"),
144
+ "type": args.type,
145
+ "name": os.path.basename(files[0]) if files else None,
146
+ "files": files,
147
+ "prompt": args.prompt,
148
+ "params": {k: v for k, v in kwargs.items() if k != "out_dir"},
149
+ "provider": used_provider["provider"],
150
+ "model": used_provider["model"],
151
+ "started_at": start_dt.strftime("%Y-%m-%d %H:%M:%S"),
152
+ "finished_at": end_dt.strftime("%Y-%m-%d %H:%M:%S"),
153
+ "duration_seconds": duration,
154
+ }
155
+ log_path = os.path.join(SCRIPT_DIR, "generation_log.json")
156
+ append_log(log_path, log_entry)
157
+
158
+ print(json.dumps({"success": True, "files": files}))
159
+ except Exception as e:
160
+ print(json.dumps({"success": False, "error": str(e)}))
161
+ sys.exit(1)
162
+
163
+
164
+ if __name__ == "__main__":
165
+ main()
@@ -0,0 +1,20 @@
1
+ [
2
+ {
3
+ "id": "20260413_141112",
4
+ "type": "image",
5
+ "name": "image_20260413_141112.png",
6
+ "files": [
7
+ "/Users/yiran/.openclaw/workspace/media/image_20260413_141112.png"
8
+ ],
9
+ "prompt": "a cute cat sitting on a windowsill",
10
+ "params": {
11
+ "output_path": "/Users/yiran/.openclaw/workspace/media/image_20260413_141112.png",
12
+ "aspect_ratio": "1:1"
13
+ },
14
+ "provider": "vapi_image",
15
+ "model": "nano-banana-pro",
16
+ "started_at": "2026-04-13 14:11:12",
17
+ "finished_at": "2026-04-13 14:11:36",
18
+ "duration_seconds": 23.5
19
+ }
20
+ ]
@@ -0,0 +1,43 @@
1
+ #!/bin/bash
2
+ # 图片生成入口
3
+ # 用法: WORKSPACE_NAME=main ./image.sh "描述" [--aspect-ratio 16:9] [--registry]
4
+ set -euo pipefail
5
+
6
+ PROMPT="${1:-}"
7
+ shift 2>/dev/null || true
8
+
9
+ if [ -z "$PROMPT" ]; then
10
+ echo "用法: WORKSPACE_NAME=xxx ./image.sh \"描述\" [--aspect-ratio 16:9] [--registry]"
11
+ echo "示例: WORKSPACE_NAME=main ./image.sh \"a cute cat\" --aspect-ratio 16:9 --registry"
12
+ exit 1
13
+ fi
14
+
15
+ ASPECT_RATIO=""
16
+ REGISTRY=false
17
+
18
+ while [ $# -gt 0 ]; do
19
+ case "$1" in
20
+ --aspect-ratio) ASPECT_RATIO="$2"; shift 2 ;;
21
+ --registry) REGISTRY=true; shift ;;
22
+ *) shift ;;
23
+ esac
24
+ done
25
+
26
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
27
+
28
+ # Build args for generate.py
29
+ ARGS=()
30
+ ARGS+=("image")
31
+ ARGS+=("$PROMPT")
32
+ [ -n "$ASPECT_RATIO" ] && ARGS+=(--aspect-ratio "$ASPECT_RATIO")
33
+
34
+ RESULT=$(python3 "$SCRIPT_DIR/generate.py" "${ARGS[@]}")
35
+ echo "$RESULT"
36
+
37
+ # Parse output path and optionally register
38
+ if [ "$REGISTRY" = true ]; then
39
+ OUTPUT=$(echo "$RESULT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['files'][0])" 2>/dev/null || true)
40
+ if [ -n "$OUTPUT" ]; then
41
+ WORKSPACE_NAME="${WORKSPACE_NAME:-main}" AUTO_REGISTRY=true python3 "$SCRIPT_DIR/registry.py" "$OUTPUT"
42
+ fi
43
+ fi
@@ -0,0 +1,46 @@
1
+ #!/bin/bash
2
+ # 音乐生成入口
3
+ # 用法: WORKSPACE_NAME=main ./music.sh "描述" [--lyrics "歌词"] [--instrumental] [--registry]
4
+ set -euo pipefail
5
+
6
+ PROMPT="${1:-}"
7
+ shift 2>/dev/null || true
8
+
9
+ if [ -z "$PROMPT" ]; then
10
+ echo "用法: WORKSPACE_NAME=xxx ./music.sh \"描述\" [--lyrics \"歌词\"] [--instrumental] [--registry]"
11
+ echo "示例: WORKSPACE_NAME=main ./music.sh \"relaxing guitar\" --instrumental --registry"
12
+ exit 1
13
+ fi
14
+
15
+ LYRICS=""
16
+ INSTRUMENTAL=false
17
+ REGISTRY=false
18
+
19
+ while [ $# -gt 0 ]; do
20
+ case "$1" in
21
+ --lyrics) LYRICS="$2"; shift 2 ;;
22
+ --instrumental) INSTRUMENTAL=true; shift ;;
23
+ --registry) REGISTRY=true; shift ;;
24
+ *) shift ;;
25
+ esac
26
+ done
27
+
28
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
29
+
30
+ # Build args for generate.py
31
+ ARGS=()
32
+ ARGS+=("music")
33
+ ARGS+=("$PROMPT")
34
+ [ -n "$LYRICS" ] && ARGS+=(--lyrics "$LYRICS")
35
+ [ "$INSTRUMENTAL" = true ] && ARGS+=(--instrumental)
36
+
37
+ RESULT=$(python3 "$SCRIPT_DIR/generate.py" "${ARGS[@]}")
38
+ echo "$RESULT"
39
+
40
+ # Parse output path and optionally register
41
+ if [ "$REGISTRY" = true ]; then
42
+ OUTPUT=$(echo "$RESULT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['files'][0])" 2>/dev/null || true)
43
+ if [ -n "$OUTPUT" ]; then
44
+ WORKSPACE_NAME="${WORKSPACE_NAME:-main}" AUTO_REGISTRY=true python3 "$SCRIPT_DIR/registry.py" "$OUTPUT"
45
+ fi
46
+ fi
@@ -0,0 +1,15 @@
1
+ from .vapi_image import VAPIImageAdapter
2
+ from .minimax_image import MiniMaxImageAdapter
3
+ from .minimax_music import MiniMaxMusicAdapter
4
+
5
+ ADAPTERS = {
6
+ "vapi_image": VAPIImageAdapter(),
7
+ "minimax_image": MiniMaxImageAdapter(),
8
+ "minimax_music": MiniMaxMusicAdapter(),
9
+ }
10
+
11
+ def get_adapter(name: str):
12
+ adapter = ADAPTERS.get(name)
13
+ if not adapter:
14
+ raise ValueError(f"unknown provider: {name}")
15
+ return adapter
@@ -0,0 +1,75 @@
1
+ """MiniMax image adapter — POST /image_generation, base64 decode to save."""
2
+ import base64
3
+ import json
4
+ import os
5
+ import sys
6
+
7
+ import requests
8
+
9
+
10
+ class MiniMaxImageAdapter:
11
+ @staticmethod
12
+ def _ratio_to_size(ratio_str, max_dim=2048):
13
+ """Parse '16:9' style ratio and compute (width, height) capped at max_dim."""
14
+ parts = ratio_str.split(":")
15
+ if len(parts) != 2:
16
+ return 1024, 1024
17
+ try:
18
+ rw, rh = float(parts[0]), float(parts[1])
19
+ except ValueError:
20
+ return 1024, 1024
21
+ # Scale so the longer side = max_dim
22
+ scale = max_dim / max(rw, rh)
23
+ w = round(rw * scale)
24
+ h = round(rh * scale)
25
+ return w, h
26
+
27
+ def generate(self, prompt, config, **kwargs):
28
+ base_url = config["base_url"].rstrip("/")
29
+ api_key = config["api_key"]
30
+ model = config["model"]
31
+ out_dir = kwargs["out_dir"]
32
+
33
+ # 默认 1024x1024;传了 aspect_ratio 则按 2048 上限换算 width/height
34
+ aspect_ratio = kwargs.get("aspect_ratio")
35
+ if aspect_ratio and aspect_ratio != "1:1":
36
+ w, h = self._ratio_to_size(aspect_ratio, max_dim=2048)
37
+ size_params = {"width": w, "height": h}
38
+ else:
39
+ size_params = {"width": 1024, "height": 1024}
40
+
41
+ endpoint = f"{base_url}/image_generation"
42
+ headers = {
43
+ "Authorization": f"Bearer {api_key}",
44
+ "Content-Type": "application/json",
45
+ }
46
+ payload = {
47
+ "model": model,
48
+ "prompt": prompt,
49
+ "response_format": "base64",
50
+ "n": 1,
51
+ **size_params,
52
+ }
53
+
54
+ print(f"[minimax_image] generating with model={model} size={size_params}...", file=sys.stderr)
55
+ resp = requests.post(endpoint, headers=headers, json=payload, timeout=180)
56
+ data = resp.json()
57
+
58
+ if data.get("base_resp", {}).get("status_code") != 0:
59
+ raise RuntimeError(f"MiniMax error: {data.get('base_resp', {}).get('status_msg')}")
60
+
61
+ image_list = data.get("data", {}).get("image_base64", [])
62
+ if not image_list:
63
+ raise RuntimeError("no image data in response")
64
+
65
+ saved = []
66
+ for i, img_b64 in enumerate(image_list):
67
+ decoded = base64.b64decode(img_b64)
68
+ ext = "png" if decoded[:8].startswith(b"\x89PNG") else "jpg"
69
+ fname = kwargs.get("output_path") or os.path.join(out_dir, f"image_{i+1}.{ext}")
70
+ with open(fname, "wb") as f:
71
+ f.write(decoded)
72
+ saved.append(fname)
73
+ print(f"[minimax_image] saved: {fname}", file=sys.stderr)
74
+
75
+ return saved
@@ -0,0 +1,61 @@
1
+ """MiniMax music adapter — POST /music_generation, download audio to save."""
2
+ import json
3
+ import os
4
+ import sys
5
+
6
+ import requests
7
+
8
+
9
+ class MiniMaxMusicAdapter:
10
+ def generate(self, prompt, config, **kwargs):
11
+ base_url = config["base_url"].rstrip("/")
12
+ api_key = config["api_key"]
13
+ model = config["model"]
14
+ out_dir = kwargs["out_dir"]
15
+ lyrics = kwargs.get("lyrics")
16
+ is_instrumental = kwargs.get("instrumental", False)
17
+
18
+ endpoint = f"{base_url}/music_generation"
19
+ headers = {
20
+ "Authorization": f"Bearer {api_key}",
21
+ "Content-Type": "application/json",
22
+ }
23
+ payload = {
24
+ "model": model,
25
+ "prompt": prompt,
26
+ "is_instrumental": is_instrumental,
27
+ "output_format": "url",
28
+ "audio_setting": {
29
+ "sample_rate": 44100,
30
+ "bitrate": 256000,
31
+ "format": "mp3",
32
+ },
33
+ }
34
+ if lyrics:
35
+ payload["lyrics"] = lyrics
36
+
37
+ print(f"[minimax_music] generating with model={model}...", file=sys.stderr)
38
+ resp = requests.post(endpoint, headers=headers, json=payload, timeout=120)
39
+ data = resp.json()
40
+
41
+ if data.get("base_resp", {}).get("status_code") != 0:
42
+ raise RuntimeError(f"MiniMax error: {data.get('base_resp', {}).get('status_msg')}")
43
+
44
+ audio_data = data.get("data", {})
45
+ status = audio_data.get("status")
46
+ if status == 1:
47
+ raise RuntimeError("music still synthesizing (status=1), retry later")
48
+
49
+ fname = kwargs.get("output_path") or os.path.join(out_dir, "music.mp3")
50
+ audio_url = audio_data.get("audio")
51
+ if not audio_url:
52
+ raise RuntimeError("no audio URL in response")
53
+
54
+ r = requests.get(audio_url, timeout=60)
55
+ with open(fname, "wb") as f:
56
+ f.write(r.content)
57
+
58
+ duration_ms = data.get("extra_info", {}).get("music_duration", 0)
59
+ print(f"[minimax_music] saved: {fname} ({duration_ms/1000:.1f}s)", file=sys.stderr)
60
+
61
+ return [fname]
@@ -0,0 +1,63 @@
1
+ """VAPI image adapter — POST /images/generations, download URL to save."""
2
+ import base64
3
+ import json
4
+ import os
5
+ import sys
6
+
7
+ import requests
8
+
9
+
10
+ class VAPIImageAdapter:
11
+ def generate(self, prompt, config, **kwargs):
12
+ base_url = config["base_url"].rstrip("/")
13
+ api_key = config["api_key"]
14
+ model = config["model"]
15
+ aspect_ratio = kwargs.get("aspect_ratio", "")
16
+ out_dir = kwargs["out_dir"]
17
+
18
+ url = f"{base_url}/images/generations"
19
+ payload = {
20
+ "model": model,
21
+ "prompt": prompt,
22
+ "n": 1,
23
+ "response_format": "url",
24
+ }
25
+ if aspect_ratio:
26
+ payload["aspect_ratio"] = aspect_ratio
27
+
28
+ headers = {
29
+ "Authorization": f"Bearer {api_key}",
30
+ "Content-Type": "application/json",
31
+ }
32
+
33
+ print(f"[vapi_image] generating with model={model}...", file=sys.stderr)
34
+ resp = requests.post(url, headers=headers, json=payload, timeout=180)
35
+ if resp.status_code != 200:
36
+ raise RuntimeError(f"VAPI API error ({resp.status_code}): {resp.text}")
37
+
38
+ data = resp.json()
39
+ images = data.get("data", [])
40
+ if not images:
41
+ raise RuntimeError("no image data in response")
42
+
43
+ saved = []
44
+ for i, item in enumerate(images):
45
+ image_url = item.get("url")
46
+ b64 = item.get("b64_json")
47
+ fname = kwargs.get("output_path") or os.path.join(out_dir, f"image_{i+1}.png")
48
+
49
+ if b64:
50
+ with open(fname, "wb") as f:
51
+ f.write(base64.b64decode(b64))
52
+ elif image_url:
53
+ r = requests.get(image_url, timeout=120)
54
+ r.raise_for_status()
55
+ with open(fname, "wb") as f:
56
+ f.write(r.content)
57
+ else:
58
+ raise RuntimeError(f"no image data for item {i}")
59
+
60
+ saved.append(fname)
61
+ print(f"[vapi_image] saved: {fname}", file=sys.stderr)
62
+
63
+ return saved
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MiniMax 资源注册脚本
4
+ 生成文件后自动注册到 __MY_ARTIFACTS__.json
5
+
6
+ 用法:
7
+ AUTO_REGISTRY=true ./image.sh "描述"
8
+ AUTO_REGISTRY=false ./image.sh "描述" # 不注册
9
+
10
+ 环境变量:
11
+ WORKSPACE_NAME - workspace名称,main 或其他
12
+ AUTO_REGISTRY - true=注册到JSON, false=不注册
13
+ MINIMAX_API_KEY - API密钥(不需要,生成用)
14
+ """
15
+
16
+ import os
17
+ import json
18
+ import sys
19
+ from datetime import datetime, timezone, timedelta
20
+
21
+ WORKSPACE = os.environ.get("WORKSPACE_NAME", "main")
22
+ AUTO_REGISTRY = os.environ.get("AUTO_REGISTRY", "false").lower() == "true"
23
+
24
+ OPENCLAW_HOME = os.environ.get("OPENCLAW_HOME", os.path.join(os.path.expanduser("~"), ".openclaw"))
25
+
26
+ # 路径构建
27
+ if WORKSPACE == "main":
28
+ WORKSPACE_ROOT = os.path.join(OPENCLAW_HOME, "workspace")
29
+ else:
30
+ WORKSPACE_ROOT = os.path.join(OPENCLAW_HOME, f"workspace-{WORKSPACE}")
31
+
32
+ MYCLAW_DIR = os.path.join(WORKSPACE_ROOT, ".myclaw")
33
+ REGISTRY_FILE = os.path.join(MYCLAW_DIR, "__MY_ARTIFACTS__.json")
34
+
35
+
36
+ def load_registry():
37
+ """加载或初始化 registry"""
38
+ if os.path.exists(REGISTRY_FILE):
39
+ with open(REGISTRY_FILE, "r", encoding="utf-8") as f:
40
+ return json.load(f)
41
+ return {
42
+ "workspace_id": WORKSPACE,
43
+ "title": f"我的资源库",
44
+ "release_version": 1,
45
+ "created_at": datetime.now(timezone(timedelta(hours=8))).isoformat(),
46
+ "updated_at": datetime.now(timezone(timedelta(hours=8))).isoformat(),
47
+ "version": 1,
48
+ "base_url": "https://claw.kekouen.cn/cmd/api/preview?path=",
49
+ "preview_path": "index.html",
50
+ "assets": []
51
+ }
52
+
53
+
54
+ def save_registry(data):
55
+ """保存 registry"""
56
+ os.makedirs(MYCLAW_DIR, exist_ok=True)
57
+ data["updated_at"] = datetime.now(timezone(timedelta(hours=8))).isoformat()
58
+ with open(REGISTRY_FILE, "w", encoding="utf-8") as f:
59
+ json.dump(data, f, ensure_ascii=False, indent=2)
60
+
61
+
62
+ def guess_type(filename):
63
+ """根据扩展名判断资源类型"""
64
+ ext = os.path.splitext(filename)[1].lower()
65
+ type_map = {
66
+ ".png": "image", ".jpg": "image", ".jpeg": "image",
67
+ ".gif": "image", ".webp": "image", ".bmp": "image",
68
+ ".mp4": "video", ".mov": "video", ".avi": "video",
69
+ ".mp3": "audio", ".wav": "audio", ".flac": "audio",
70
+ ".html": "html", ".htm": "html",
71
+ ".pdf": "document", ".doc": "document", ".docx": "document",
72
+ }
73
+ return type_map.get(ext, "file")
74
+
75
+
76
+ def register_asset(filepath, name=None):
77
+ """
78
+ 注册一个文件到 JSON
79
+ filepath: 完整路径,如 /root/.openclaw/workspace/media/image_xxx.png
80
+ """
81
+ if not AUTO_REGISTRY:
82
+ print(f"[Registry] AUTO_REGISTRY=false,跳过注册")
83
+ return
84
+
85
+ filename = os.path.basename(filepath)
86
+ rel_path = os.path.relpath(filepath, WORKSPACE_ROOT)
87
+ asset_type = guess_type(filename)
88
+
89
+ # 生成 id
90
+ import hashlib
91
+ asset_id = hashlib.md5(filename.encode()).hexdigest()[:8]
92
+ asset_id = f"asset-{asset_id}"
93
+
94
+ # 生成 name
95
+ if not name:
96
+ name = os.path.splitext(filename)[0]
97
+
98
+ registry = load_registry()
99
+
100
+ # 检查是否已存在(同名文件)
101
+ for i, asset in enumerate(registry["assets"]):
102
+ if asset.get("path") == rel_path:
103
+ registry["assets"][i] = {
104
+ "id": asset_id,
105
+ "type": asset_type,
106
+ "name": name,
107
+ "path": rel_path
108
+ }
109
+ save_registry(registry)
110
+ print(f"[Registry] 已更新: {rel_path}")
111
+ return
112
+
113
+ # 新增
114
+ registry["assets"].append({
115
+ "id": asset_id,
116
+ "type": asset_type,
117
+ "name": name,
118
+ "path": rel_path
119
+ })
120
+ save_registry(registry)
121
+ print(f"[Registry] 已注册: {rel_path} ({asset_type})")
122
+
123
+
124
+ if __name__ == "__main__":
125
+ # CLI 模式
126
+ if len(sys.argv) < 2:
127
+ print(f"用法: AUTO_REGISTRY=true python3 registry.py <文件路径> [名称]")
128
+ print(f"环境: WORKSPACE_NAME={WORKSPACE}, AUTO_REGISTRY={AUTO_REGISTRY}")
129
+ sys.exit(1)
130
+
131
+ filepath = sys.argv[1]
132
+ name = sys.argv[2] if len(sys.argv) > 2 else None
133
+ register_asset(filepath, name)
File without changes
File without changes