@aiyiran/myclaw 1.1.20 → 1.1.21
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/.claude/settings.local.json +17 -1
- package/assets/myclaw-artifacts.js +142 -46
- package/index.js +36 -4
- package/package.json +1 -1
- package/server/artifacts_schema.py +32 -0
- package/server/fork.py +615 -0
- package/server/install-linux-guard.sh +88 -0
- package/server/myclaw-guard.sh +121 -0
- package/server/myclaw-sync.service +18 -0
- package/server/sync_workspace.py +398 -83
- package/skills/yiran-skill-media/scripts/registry.py +10 -14
package/server/fork.py
ADDED
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from datetime import datetime, timezone, timedelta
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
import threading
|
|
6
|
+
import re
|
|
7
|
+
import urllib.request
|
|
8
|
+
from watchdog.observers import Observer
|
|
9
|
+
from watchdog.events import FileSystemEventHandler
|
|
10
|
+
from qiniu import Auth, put_file_v2, CdnManager, BucketManager
|
|
11
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
12
|
+
from urllib.parse import urlparse, parse_qs
|
|
13
|
+
|
|
14
|
+
claw = "claw"
|
|
15
|
+
|
|
16
|
+
BASE_URL = f"https://cdn.yiranlaoshi.com/{claw}"
|
|
17
|
+
|
|
18
|
+
QINIU_KEY = "T3tgxM7EMx1j4VESw4m4PIfFXoOvBo-wQEOQewXX"
|
|
19
|
+
QINIU_TOKEN = "PVZvlKVOjX2RqlV2ILMg-QwpNMssOlpVbaEzypz0"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
QINIU_BUCKET = "yiran1"
|
|
23
|
+
|
|
24
|
+
access_key = QINIU_KEY
|
|
25
|
+
secret_key = QINIU_TOKEN
|
|
26
|
+
bucket_name = QINIU_BUCKET
|
|
27
|
+
q = Auth(access_key, secret_key)
|
|
28
|
+
cdn_manager = CdnManager(q)
|
|
29
|
+
bucket = BucketManager(q)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MyHandler(FileSystemEventHandler):
|
|
33
|
+
def _is_file_event(self, event):
|
|
34
|
+
return not getattr(event, "is_directory", False)
|
|
35
|
+
|
|
36
|
+
def on_modified(self, event):
|
|
37
|
+
if not self._is_file_event(event):
|
|
38
|
+
return
|
|
39
|
+
print(f"🟡 修改: {event.src_path}")
|
|
40
|
+
file_gen(event.src_path, "add")
|
|
41
|
+
|
|
42
|
+
def on_deleted(self, event):
|
|
43
|
+
if not self._is_file_event(event):
|
|
44
|
+
return
|
|
45
|
+
print(f"🔴 删除: {event.src_path}")
|
|
46
|
+
file_gen(event.src_path, "delete")
|
|
47
|
+
|
|
48
|
+
def on_moved(self, event):
|
|
49
|
+
if not self._is_file_event(event):
|
|
50
|
+
return
|
|
51
|
+
print(f"🔵 移动: {event.src_path} -> {getattr(event, 'dest_path', '')}")
|
|
52
|
+
file_gen(event.src_path, "delete")
|
|
53
|
+
if getattr(event, 'dest_path', ''):
|
|
54
|
+
file_gen(event.dest_path, "add")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def now_iso():
|
|
58
|
+
# 生成 +08:00 时间格式
|
|
59
|
+
tz = timezone(timedelta(hours=8))
|
|
60
|
+
return datetime.now(tz).isoformat()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def gen_id():
|
|
64
|
+
return f"asset-{int(datetime.now().timestamp() * 1000)}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_type(file_path):
|
|
68
|
+
return file_path.split(".")[-1] if "." in file_path else "unknown"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def init_config(workspace_id, file_path, method="add"):
|
|
72
|
+
file = f"/root/.openclaw/{workspace_id}/.myclaw/__MY_ARTIFACTS__.json"
|
|
73
|
+
|
|
74
|
+
# 确保目录存在
|
|
75
|
+
os.makedirs(os.path.dirname(file), exist_ok=True)
|
|
76
|
+
|
|
77
|
+
now = now_iso()
|
|
78
|
+
|
|
79
|
+
# 如果文件不存在,先初始化(包含根级创建/更改时间)
|
|
80
|
+
if not os.path.exists(file):
|
|
81
|
+
data = {
|
|
82
|
+
"workspace_id": workspace_id,
|
|
83
|
+
"base_url": BASE_URL,
|
|
84
|
+
"assets": [],
|
|
85
|
+
"created_at": now,
|
|
86
|
+
"updated_at": now
|
|
87
|
+
}
|
|
88
|
+
else:
|
|
89
|
+
with open(file, "r", encoding="utf-8") as f:
|
|
90
|
+
try:
|
|
91
|
+
data = json.load(f)
|
|
92
|
+
except BaseException:
|
|
93
|
+
data = {
|
|
94
|
+
"workspace_id": workspace_id,
|
|
95
|
+
"assets": [],
|
|
96
|
+
"created_at": now,
|
|
97
|
+
"updated_at": now
|
|
98
|
+
}
|
|
99
|
+
# 确保根级时间字段存在;保留已有 created_at,更新 updated_at
|
|
100
|
+
if "created_at" not in data:
|
|
101
|
+
data["created_at"] = now
|
|
102
|
+
data["updated_at"] = now
|
|
103
|
+
|
|
104
|
+
if method == "delete":
|
|
105
|
+
# 删除逻辑:将匹配 path 的项过滤掉,并更新根级更新时间
|
|
106
|
+
data["assets"] = [asset for asset in data.get(
|
|
107
|
+
"assets", []) if asset.get("path") != file_path]
|
|
108
|
+
data["updated_at"] = now
|
|
109
|
+
else:
|
|
110
|
+
found = False
|
|
111
|
+
# 查找是否已存在该 path
|
|
112
|
+
for asset in data.get("assets", []):
|
|
113
|
+
if asset.get("path") == file_path:
|
|
114
|
+
# 保证已有 created_at,不被覆盖;更新 updated_at 和 type(以防扩展名变化)
|
|
115
|
+
asset.setdefault("created_at", now)
|
|
116
|
+
asset["updated_at"] = now
|
|
117
|
+
asset["type"] = get_type(file_path)
|
|
118
|
+
found = True
|
|
119
|
+
break
|
|
120
|
+
|
|
121
|
+
# 不存在则新增(asset 已包含创建/更改时间)
|
|
122
|
+
if not found:
|
|
123
|
+
new_asset = {
|
|
124
|
+
"id": gen_id(),
|
|
125
|
+
"type": get_type(file_path),
|
|
126
|
+
"path": file_path,
|
|
127
|
+
"created_at": now,
|
|
128
|
+
"updated_at": now
|
|
129
|
+
}
|
|
130
|
+
data["assets"].append(new_asset)
|
|
131
|
+
|
|
132
|
+
# 更新根级更新时间
|
|
133
|
+
data["updated_at"] = now
|
|
134
|
+
|
|
135
|
+
# 写回文件
|
|
136
|
+
with open(file, "w", encoding="utf-8") as f:
|
|
137
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def file_gen(path, method, timestamp=None):
|
|
141
|
+
# 排除配置记录文件自身,防止循环触发
|
|
142
|
+
if "__MY_ARTIFACTS__.json" in path:
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
# 规范化路径,兼容 Windows 和 *nix
|
|
146
|
+
norm_path = os.path.normpath(path)
|
|
147
|
+
parts = norm_path.split(os.sep)
|
|
148
|
+
|
|
149
|
+
# 查找包含 workspace 标识的分段(支持 workspace 和 workspace-xxx 两种格式)
|
|
150
|
+
space_idx = -1
|
|
151
|
+
for i, p in enumerate(parts):
|
|
152
|
+
# 匹配精确的 "workspace" 或以 "workspace-" 开头的目录
|
|
153
|
+
if p == "workspace" or p.startswith("workspace-"):
|
|
154
|
+
space_idx = i
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
if space_idx == -1:
|
|
158
|
+
# 路径中不包含 workspace,不处理
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
space_id = parts[space_idx]
|
|
162
|
+
# 相对路径:workspace 后面的所有段,使用 '/' 作为存储格式(保证跨平台一致)
|
|
163
|
+
relative_parts = parts[space_idx + 1:]
|
|
164
|
+
if not relative_parts:
|
|
165
|
+
# 没有文件名,可能是目录事件,忽略
|
|
166
|
+
return
|
|
167
|
+
relative_path = "/".join(relative_parts)
|
|
168
|
+
|
|
169
|
+
# 上传 key 保留 workspace 段(用于 CDN 存储),包含本地目录的时间戳
|
|
170
|
+
path_url = f"{space_id}/{relative_path}"
|
|
171
|
+
# 使用 claw 前缀作为存储 key 的一部分,保证与 CDN 访问路径一致
|
|
172
|
+
key = f"{claw}/{path_url}"
|
|
173
|
+
|
|
174
|
+
if method == "delete":
|
|
175
|
+
# 删除时不需要上传,只清理配置文件记录,传入的 file_path 不包含 space_id(relative_path)
|
|
176
|
+
init_config(space_id, relative_path, method="delete")
|
|
177
|
+
print(f"🗑️ 已删除配置记录: {relative_path}")
|
|
178
|
+
else:
|
|
179
|
+
# 添加或修改时进行上传
|
|
180
|
+
try:
|
|
181
|
+
token = q.upload_token(bucket_name, key, 3600)
|
|
182
|
+
# 直接使用 put_file_v2 上传文件路径,避免读取到空内容时导致 SDK 报错缺少 data 参数
|
|
183
|
+
ret, info = put_file_v2(token, key, path)
|
|
184
|
+
|
|
185
|
+
# 刷新 CDN
|
|
186
|
+
cdn_url = f"https://cdn.yiranlaoshi.com/{key}"
|
|
187
|
+
cdn_ret, cdn_info = cdn_manager.refresh_urls([cdn_url])
|
|
188
|
+
|
|
189
|
+
# 记录时使用不包含 workspace 的相对路径
|
|
190
|
+
init_config(space_id, relative_path, method="add")
|
|
191
|
+
print(f"🚀 已上传并记录: {relative_path}")
|
|
192
|
+
print(
|
|
193
|
+
f"风 CDN刷新: {cdn_url} - {cdn_info.status_code if info else 'unknown'}")
|
|
194
|
+
except Exception as e:
|
|
195
|
+
print(f"❌ 上传/读取文件失败: {e}")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def list_files_from_qiniu(bucket_name, prefix, auth):
|
|
199
|
+
"""
|
|
200
|
+
从七牛云列出指定前缀的所有文件
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
bucket_name: 空间名称
|
|
204
|
+
prefix: 文件前缀
|
|
205
|
+
auth: 认证对象
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
list: 文件列表
|
|
209
|
+
"""
|
|
210
|
+
bucket = BucketManager(auth)
|
|
211
|
+
marker = None
|
|
212
|
+
files = []
|
|
213
|
+
|
|
214
|
+
while True:
|
|
215
|
+
ret, eof, info = bucket.list(
|
|
216
|
+
bucket_name, prefix=prefix, marker=marker, limit=1000)
|
|
217
|
+
if ret is not None:
|
|
218
|
+
for item in ret.get('items', []):
|
|
219
|
+
files.append(item)
|
|
220
|
+
marker = ret.get('marker')
|
|
221
|
+
if eof:
|
|
222
|
+
break
|
|
223
|
+
else:
|
|
224
|
+
break
|
|
225
|
+
|
|
226
|
+
return files
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def download_qiniu_file(bucket_manager, bucket_name, key, local_path):
|
|
230
|
+
"""
|
|
231
|
+
从七牛云下载单个文件
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
bucket_manager: BucketManager 对象
|
|
235
|
+
bucket_name: 空间名称
|
|
236
|
+
key: 文件 key
|
|
237
|
+
local_path: 本地路径
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
bool: 下载是否成功
|
|
241
|
+
"""
|
|
242
|
+
import os
|
|
243
|
+
|
|
244
|
+
# 生成私有空间的下载链接
|
|
245
|
+
base_url = f"http://cdn.yiranlaoshi.com/{key}"
|
|
246
|
+
|
|
247
|
+
# 尝试使用多种方法生成私有链接
|
|
248
|
+
private_url = None
|
|
249
|
+
try:
|
|
250
|
+
# 方法1: 使用 bucket_manager.private_url
|
|
251
|
+
if hasattr(bucket_manager, 'private_url'):
|
|
252
|
+
private_url = bucket_manager.private_url(base_url)
|
|
253
|
+
except Exception:
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
if private_url is None:
|
|
257
|
+
try:
|
|
258
|
+
# 方法2: 使用 q.private_url
|
|
259
|
+
private_url = q.private_url(base_url)
|
|
260
|
+
except Exception:
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
if private_url is None:
|
|
264
|
+
# 方法3: 直接使用公共 URL
|
|
265
|
+
private_url = base_url
|
|
266
|
+
|
|
267
|
+
print(f" download URL: {private_url}")
|
|
268
|
+
|
|
269
|
+
# 确保目录存在
|
|
270
|
+
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
# 下载文件
|
|
274
|
+
urllib.request.urlretrieve(private_url, local_path)
|
|
275
|
+
return True
|
|
276
|
+
except Exception as e:
|
|
277
|
+
print(f"下载文件失败 {key}: {e}")
|
|
278
|
+
# 尝试使用公共 URL
|
|
279
|
+
public_url = base_url
|
|
280
|
+
try:
|
|
281
|
+
urllib.request.urlretrieve(public_url, local_path)
|
|
282
|
+
return True
|
|
283
|
+
except Exception as e2:
|
|
284
|
+
print(f" 公共 URL 也失败: {e2}")
|
|
285
|
+
return False
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def extract_cdn_prefix_from_url(remote_url):
|
|
289
|
+
"""
|
|
290
|
+
从 URL 中提取 CDN 存储路径前缀
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
remote_url: 远程 URL,例如 https://cdn.yiranlaoshi.com/myclaw/show/claw/workspace-huangxicheng/2/parkour.html
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
str: CDN 存储路径前缀,例如 "myclaw/show/claw/"
|
|
297
|
+
"""
|
|
298
|
+
# 从 URL 中提取路径部分
|
|
299
|
+
match = re.search(r'https?://[^/]+(/[^?]*)/', remote_url)
|
|
300
|
+
if match:
|
|
301
|
+
url_path = match.group(1)
|
|
302
|
+
# 移除末尾的 /
|
|
303
|
+
url_path = url_path.rstrip('/')
|
|
304
|
+
# 找到 workspace- 开始的部分
|
|
305
|
+
workspace_match = re.search(r'(.*)/workspace-', url_path)
|
|
306
|
+
if workspace_match:
|
|
307
|
+
return workspace_match.group(1) + '/'
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def sync_workspace_from_cdn(remote_url, timestamp=None):
|
|
312
|
+
"""
|
|
313
|
+
从 CDN 同步 workspace 目录到本地
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
remote_url: 远程 URL,例如 https://cdn.yiranlaoshi.com/myclaw/show/claw/workspace-huangxicheng/2/parkour.html
|
|
317
|
+
timestamp: 时间戳,如果不提供则自动生成
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
dict: 包含成功状态和相关信息
|
|
321
|
+
"""
|
|
322
|
+
try:
|
|
323
|
+
# 解析远程 URL 以获取路径和 workspace 名称
|
|
324
|
+
parsed_url = urlparse(remote_url)
|
|
325
|
+
# 例如: myclaw/show/claw/workspace-huangxicheng/2/parkour.html
|
|
326
|
+
url_path = parsed_url.path.lstrip('/')
|
|
327
|
+
|
|
328
|
+
# 提取 workspace 名称 (匹配 workspace- 开的部分)
|
|
329
|
+
match = re.search(r'(workspace-[^/]+)', url_path)
|
|
330
|
+
if not match:
|
|
331
|
+
return {
|
|
332
|
+
"success": False,
|
|
333
|
+
"error": "无法从 URL 中提取 workspace 名称"
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
workspace_name = match.group(1)
|
|
337
|
+
print(f"检测到 workspace 名称: {workspace_name}")
|
|
338
|
+
|
|
339
|
+
# 提取 workspace 前面的所有前缀 (例如 myclaw/show/claw/)
|
|
340
|
+
workspace_idx_in_path = url_path.find(workspace_name)
|
|
341
|
+
remote_prefix = url_path[:workspace_idx_in_path]
|
|
342
|
+
print(f"检测到远程前缀: {remote_prefix}")
|
|
343
|
+
|
|
344
|
+
# 生成时间戳(如果未提供)
|
|
345
|
+
if timestamp is None:
|
|
346
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
347
|
+
|
|
348
|
+
# 新的本地目录名
|
|
349
|
+
local_dir_name = f"{workspace_name}_{timestamp}"
|
|
350
|
+
print(f"新目录名: {local_dir_name}")
|
|
351
|
+
|
|
352
|
+
# 目标路径
|
|
353
|
+
target_dir = f"/root/.openclaw/{local_dir_name}"
|
|
354
|
+
print(f"目标路径: {target_dir}")
|
|
355
|
+
|
|
356
|
+
# 七牛云存储的查询前缀
|
|
357
|
+
source_prefix = f"{remote_prefix}{workspace_name}/"
|
|
358
|
+
print(f"七牛云存储查询前缀: {source_prefix}")
|
|
359
|
+
|
|
360
|
+
# 尝试的前缀列表
|
|
361
|
+
prefixes_to_try = [source_prefix]
|
|
362
|
+
|
|
363
|
+
print(f"源目录前缀: {source_prefix}")
|
|
364
|
+
print(f"Bucket 名称: {QINIU_BUCKET}")
|
|
365
|
+
for prefix in prefixes_to_try:
|
|
366
|
+
print(f"尝试使用前缀: {prefix}")
|
|
367
|
+
|
|
368
|
+
# 执行 myclaw new 命令
|
|
369
|
+
print(f"执行 myclaw new {local_dir_name}...")
|
|
370
|
+
import subprocess
|
|
371
|
+
result = subprocess.run(
|
|
372
|
+
["myclaw", "new", local_dir_name],
|
|
373
|
+
cwd="/root/.openclaw",
|
|
374
|
+
capture_output=True,
|
|
375
|
+
text=True
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
if result.returncode != 0:
|
|
379
|
+
return {
|
|
380
|
+
"success": False,
|
|
381
|
+
"error": f"myclaw new 命令执行失败: {result.stderr}",
|
|
382
|
+
"stdout": result.stdout
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
print(f"myclaw new 执行成功: {result.stdout}")
|
|
386
|
+
|
|
387
|
+
# 列出源目录的所有文件(尝试多个前缀)
|
|
388
|
+
print(f"开始从七牛云列出文件...")
|
|
389
|
+
files = []
|
|
390
|
+
for prefix in prefixes_to_try:
|
|
391
|
+
print(f"尝试前缀: {prefix}")
|
|
392
|
+
prefix_files = list_files_from_qiniu(QINIU_BUCKET, prefix, q)
|
|
393
|
+
print(f" 找到 {len(prefix_files)} 个文件")
|
|
394
|
+
if prefix_files:
|
|
395
|
+
files.extend(prefix_files)
|
|
396
|
+
print(f" 使用前缀 {prefix} 找到文件,停止尝试其他前缀")
|
|
397
|
+
break
|
|
398
|
+
|
|
399
|
+
print(f"总共找到 {len(files)} 个文件")
|
|
400
|
+
|
|
401
|
+
if not files:
|
|
402
|
+
return {
|
|
403
|
+
"success": False,
|
|
404
|
+
"error": "未找到任何文件"
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
# 下载所有文件
|
|
408
|
+
success_count = 0
|
|
409
|
+
fail_count = 0
|
|
410
|
+
for item in files:
|
|
411
|
+
key = item.get('key', '')
|
|
412
|
+
if key.startswith(source_prefix):
|
|
413
|
+
# 提取相对于 workspace 的路径,例如 "2/parkour.html"
|
|
414
|
+
file_name = key[len(source_prefix):]
|
|
415
|
+
|
|
416
|
+
# 移除第一级目录(如版本号 "2/")
|
|
417
|
+
# 用户要求:被复制之后不需要版本号目录
|
|
418
|
+
parts = file_name.split('/')
|
|
419
|
+
if len(parts) > 1:
|
|
420
|
+
file_name = '/'.join(parts[1:])
|
|
421
|
+
else:
|
|
422
|
+
# 不应该发生,因为我们按前缀列出的
|
|
423
|
+
continue
|
|
424
|
+
|
|
425
|
+
local_file_path = os.path.join(target_dir, file_name)
|
|
426
|
+
|
|
427
|
+
if download_qiniu_file(bucket, QINIU_BUCKET, key, local_file_path):
|
|
428
|
+
success_count += 1
|
|
429
|
+
print(f" ✓ 下载: {file_name}")
|
|
430
|
+
else:
|
|
431
|
+
fail_count += 1
|
|
432
|
+
|
|
433
|
+
print(f"下载完成: 成功 {success_count} 个, 失败 {fail_count} 个")
|
|
434
|
+
|
|
435
|
+
if success_count == 0:
|
|
436
|
+
return {
|
|
437
|
+
"success": False,
|
|
438
|
+
"error": "没有文件下载成功"
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
# 构建访问 URL (使用带时间戳的本地目录名作为 CDN 路径)
|
|
442
|
+
access_url = f"https://cdn.yiranlaoshi.com/{claw}/{local_dir_name}/"
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
"success": True,
|
|
446
|
+
"message": "同步成功",
|
|
447
|
+
"workspace_name": workspace_name,
|
|
448
|
+
"local_dir": local_dir_name,
|
|
449
|
+
"local_path": target_dir,
|
|
450
|
+
"access_url": access_url,
|
|
451
|
+
"download_count": success_count
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
except Exception as e:
|
|
455
|
+
return {
|
|
456
|
+
"success": False,
|
|
457
|
+
"error": str(e)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class FlashPolicyHandler(BaseHTTPRequestHandler):
|
|
462
|
+
"""Flash跨域策略请求处理器"""
|
|
463
|
+
|
|
464
|
+
def log_message(self, format, *args):
|
|
465
|
+
"""简化日志输出"""
|
|
466
|
+
print(f"[FlashPolicy] {args[0]}")
|
|
467
|
+
|
|
468
|
+
def do_GET(self):
|
|
469
|
+
"""处理GET请求"""
|
|
470
|
+
if self.path == '/' or self.path == '':
|
|
471
|
+
# 返回简单欢迎信息
|
|
472
|
+
self.send_response(200)
|
|
473
|
+
self.send_header('Content-type', 'text/html; charset=utf-8')
|
|
474
|
+
self.end_headers()
|
|
475
|
+
response = {
|
|
476
|
+
"status": "ok",
|
|
477
|
+
"message": "Flash同步服务运行中",
|
|
478
|
+
"endpoints": {
|
|
479
|
+
"/sync": "同步远程workspace到本地"
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
self.wfile.write(json.dumps(response, ensure_ascii=False).encode())
|
|
483
|
+
elif self.path.startswith('/sync'):
|
|
484
|
+
# 处理同步请求
|
|
485
|
+
self.handle_sync()
|
|
486
|
+
else:
|
|
487
|
+
self.send_response(404)
|
|
488
|
+
self.send_header('Content-type', 'text/plain; charset=utf-8')
|
|
489
|
+
self.end_headers()
|
|
490
|
+
self.wfile.write(b"Not Found")
|
|
491
|
+
|
|
492
|
+
def do_POST(self):
|
|
493
|
+
"""处理POST请求"""
|
|
494
|
+
if self.path == '/sync':
|
|
495
|
+
self.handle_sync()
|
|
496
|
+
else:
|
|
497
|
+
self.send_response(404)
|
|
498
|
+
self.send_header('Content-type', 'text/plain; charset=utf-8')
|
|
499
|
+
self.end_headers()
|
|
500
|
+
self.wfile.write(b"Not Found")
|
|
501
|
+
|
|
502
|
+
def handle_sync(self):
|
|
503
|
+
"""处理同步请求"""
|
|
504
|
+
try:
|
|
505
|
+
content_length = int(self.headers.get('Content-Length', 0))
|
|
506
|
+
|
|
507
|
+
if content_length > 0:
|
|
508
|
+
# 从JSON body中读取参数
|
|
509
|
+
post_data = self.rfile.read(content_length).decode('utf-8')
|
|
510
|
+
data = json.loads(post_data)
|
|
511
|
+
remote_url = data.get('url', '')
|
|
512
|
+
else:
|
|
513
|
+
# 从URL查询参数中读取
|
|
514
|
+
parsed = urlparse(self.path)
|
|
515
|
+
params = parse_qs(parsed.query)
|
|
516
|
+
remote_url = params.get('url', [''])[0]
|
|
517
|
+
|
|
518
|
+
if not remote_url:
|
|
519
|
+
self.send_response(400)
|
|
520
|
+
self.send_header(
|
|
521
|
+
'Content-type', 'application/json; charset=utf-8')
|
|
522
|
+
self.end_headers()
|
|
523
|
+
response = {
|
|
524
|
+
"success": False,
|
|
525
|
+
"error": "缺少必要的参数: url"
|
|
526
|
+
}
|
|
527
|
+
self.wfile.write(json.dumps(
|
|
528
|
+
response, ensure_ascii=False).encode())
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
print(f"收到同步请求: {remote_url}")
|
|
532
|
+
|
|
533
|
+
# 执行同步
|
|
534
|
+
result = sync_workspace_from_cdn(remote_url)
|
|
535
|
+
|
|
536
|
+
if result["success"]:
|
|
537
|
+
self.send_response(200)
|
|
538
|
+
else:
|
|
539
|
+
self.send_response(500)
|
|
540
|
+
|
|
541
|
+
self.send_header('Content-type', 'application/json; charset=utf-8')
|
|
542
|
+
self.end_headers()
|
|
543
|
+
self.wfile.write(json.dumps(result, ensure_ascii=False).encode())
|
|
544
|
+
|
|
545
|
+
except Exception as e:
|
|
546
|
+
self.send_response(500)
|
|
547
|
+
self.send_header('Content-type', 'application/json; charset=utf-8')
|
|
548
|
+
self.end_headers()
|
|
549
|
+
response = {
|
|
550
|
+
"success": False,
|
|
551
|
+
"error": str(e)
|
|
552
|
+
}
|
|
553
|
+
self.wfile.write(json.dumps(response, ensure_ascii=False).encode())
|
|
554
|
+
|
|
555
|
+
def do_OPTIONS(self):
|
|
556
|
+
"""处理OPTIONS请求(Flash预检)"""
|
|
557
|
+
self.send_response(200)
|
|
558
|
+
self.send_header('Access-Control-Allow-Origin', '*')
|
|
559
|
+
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
|
560
|
+
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
|
561
|
+
self.end_headers()
|
|
562
|
+
|
|
563
|
+
def do_HEAD(self):
|
|
564
|
+
"""处理HEAD请求"""
|
|
565
|
+
self.send_response(200)
|
|
566
|
+
self.send_header('Content-type', 'text/html; charset=utf-8')
|
|
567
|
+
self.end_headers()
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def run_http_server(port=8080):
|
|
571
|
+
"""启动HTTP服务器"""
|
|
572
|
+
server_address = ('', port)
|
|
573
|
+
httpd = HTTPServer(server_address, FlashPolicyHandler)
|
|
574
|
+
print(f"HTTP服务器启动,监听端口: {port}")
|
|
575
|
+
print(f"同步接口示例:")
|
|
576
|
+
print(
|
|
577
|
+
f" GET: http://localhost:{port}/sync?url=https://cdn.yiranlaoshi.com/myclaw/show/claw/workspace-huangxicheng/2/parkour.html")
|
|
578
|
+
print(f" POST: http://localhost:{port}/sync")
|
|
579
|
+
print(f" Body: url=https://cdn.yiranlaoshi.com/myclaw/show/claw/workspace-huangxicheng/2/parkour.html")
|
|
580
|
+
httpd.serve_forever()
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
if __name__ == "__main__":
|
|
584
|
+
path = "/root/.openclaw" # 你要监听的目录
|
|
585
|
+
port = 8080 # HTTP服务端口
|
|
586
|
+
|
|
587
|
+
# 启动文件监听器
|
|
588
|
+
event_handler = MyHandler()
|
|
589
|
+
observer = Observer()
|
|
590
|
+
observer.schedule(event_handler, path, recursive=True)
|
|
591
|
+
observer.start()
|
|
592
|
+
print(f"✅ 文件监听器启动,监听目录: {path}")
|
|
593
|
+
|
|
594
|
+
# 启动HTTP服务器(在单独线程中)
|
|
595
|
+
http_thread = threading.Thread(
|
|
596
|
+
target=run_http_server,
|
|
597
|
+
args=(port,),
|
|
598
|
+
daemon=True
|
|
599
|
+
)
|
|
600
|
+
http_thread.start()
|
|
601
|
+
print(f"✅ HTTP服务器启动,监听端口: {port}")
|
|
602
|
+
print(f"📝 同步接口示例:")
|
|
603
|
+
print(
|
|
604
|
+
f" GET: http://localhost:{port}/sync?url=https://cdn.yiranlaoshi.com/myclaw/show/claw/workspace-huangxicheng/2/parkour.html")
|
|
605
|
+
print(f" POST: http://localhost:{port}/sync")
|
|
606
|
+
print(f" Body: url=https://cdn.yiranlaoshi.com/myclaw/show/claw/workspace-huangxicheng/2/parkour.html")
|
|
607
|
+
|
|
608
|
+
# 保持主进程运行
|
|
609
|
+
try:
|
|
610
|
+
while True:
|
|
611
|
+
time.sleep(1)
|
|
612
|
+
except KeyboardInterrupt:
|
|
613
|
+
observer.stop()
|
|
614
|
+
|
|
615
|
+
observer.join()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# install-linux-guard.sh - 在 Linux 上安装 myclaw-sync systemd 守卫
|
|
3
|
+
# 前提:已经手动运行过 mc server <name> 完成注册(config.json 已存在)
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
GREEN='\033[0;32m'
|
|
8
|
+
YELLOW='\033[1;33m'
|
|
9
|
+
RED='\033[0;31m'
|
|
10
|
+
NC='\033[0m'
|
|
11
|
+
|
|
12
|
+
log_ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
|
13
|
+
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
|
14
|
+
log_err() { echo -e "${RED}[ERR]${NC} $*"; }
|
|
15
|
+
log_info() { echo -e " $*"; }
|
|
16
|
+
|
|
17
|
+
echo ""
|
|
18
|
+
echo "=== MyClaw Linux 守卫安装 ==="
|
|
19
|
+
echo ""
|
|
20
|
+
|
|
21
|
+
# ── 1. 检查 config.json 是否存在(必须先注册)─────────────────────────────
|
|
22
|
+
CONFIG_FILE="$HOME/.openclaw/myclaw/server/config.json"
|
|
23
|
+
if [ ! -f "$CONFIG_FILE" ]; then
|
|
24
|
+
log_err "config.json 不存在: $CONFIG_FILE"
|
|
25
|
+
log_info "请先手动运行一次注册:"
|
|
26
|
+
log_info " mc server <你的名字>"
|
|
27
|
+
log_info "注册成功后再运行本脚本。"
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
CLAW_NAME=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE')).get('claw','?'))" 2>/dev/null || echo "unknown")
|
|
32
|
+
log_ok "检测到注册名: $CLAW_NAME"
|
|
33
|
+
|
|
34
|
+
# ── 2. 复制 guard 脚本到用户目录 ─────────────────────────────────────────
|
|
35
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
36
|
+
GUARD_DEST="$HOME/.openclaw/myclaw/server/myclaw-guard.sh"
|
|
37
|
+
|
|
38
|
+
cp "$SCRIPT_DIR/myclaw-guard.sh" "$GUARD_DEST"
|
|
39
|
+
chmod +x "$GUARD_DEST"
|
|
40
|
+
log_ok "guard 脚本: $GUARD_DEST"
|
|
41
|
+
|
|
42
|
+
# ── 3. 安装 systemd user service ─────────────────────────────────────────
|
|
43
|
+
SERVICE_DIR="$HOME/.config/systemd/user"
|
|
44
|
+
mkdir -p "$SERVICE_DIR"
|
|
45
|
+
|
|
46
|
+
SERVICE_DEST="$SERVICE_DIR/myclaw-sync.service"
|
|
47
|
+
cp "$SCRIPT_DIR/myclaw-sync.service" "$SERVICE_DEST"
|
|
48
|
+
log_ok "service 文件: $SERVICE_DEST"
|
|
49
|
+
|
|
50
|
+
# ── 4. 重载 systemd 并启用 ───────────────────────────────────────────────
|
|
51
|
+
systemctl --user daemon-reload
|
|
52
|
+
log_ok "systemd daemon-reload 完成"
|
|
53
|
+
|
|
54
|
+
systemctl --user enable myclaw-sync
|
|
55
|
+
log_ok "开机自启: 已启用"
|
|
56
|
+
|
|
57
|
+
# ── 5. 开启 lingering(SSH 断开后 user service 继续跑)────────────────────
|
|
58
|
+
if command -v loginctl &>/dev/null; then
|
|
59
|
+
loginctl enable-linger "$USER" 2>/dev/null && log_ok "loginctl linger: 已启用(断开 SSH 不影响服务)" \
|
|
60
|
+
|| log_warn "loginctl linger 失败,可能需要 root 权限: sudo loginctl enable-linger $USER"
|
|
61
|
+
else
|
|
62
|
+
log_warn "loginctl 不可用,请手动确认 SSH 断开后服务能否持续"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# ── 6. 启动服务 ──────────────────────────────────────────────────────────
|
|
66
|
+
systemctl --user restart myclaw-sync
|
|
67
|
+
sleep 2
|
|
68
|
+
|
|
69
|
+
STATUS=$(systemctl --user is-active myclaw-sync 2>/dev/null || echo "unknown")
|
|
70
|
+
if [ "$STATUS" = "active" ]; then
|
|
71
|
+
log_ok "服务状态: $STATUS"
|
|
72
|
+
else
|
|
73
|
+
log_warn "服务状态: $STATUS(等几秒后重试,或查看日志)"
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# ── 7. 打印日志路径和常用命令 ─────────────────────────────────────────────
|
|
77
|
+
echo ""
|
|
78
|
+
echo "=== 安装完成 ==="
|
|
79
|
+
echo ""
|
|
80
|
+
echo "日志文件: $HOME/.openclaw/logs/myclaw-guard.log"
|
|
81
|
+
echo ""
|
|
82
|
+
echo "常用命令:"
|
|
83
|
+
echo " 查看状态 systemctl --user status myclaw-sync"
|
|
84
|
+
echo " 查看日志 tail -f ~/.openclaw/logs/myclaw-guard.log"
|
|
85
|
+
echo " 重启服务 systemctl --user restart myclaw-sync"
|
|
86
|
+
echo " 停止服务 systemctl --user stop myclaw-sync"
|
|
87
|
+
echo " 卸载守卫 systemctl --user disable --now myclaw-sync"
|
|
88
|
+
echo ""
|