@aiyiran/myclaw 1.0.237 → 1.0.239

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.
@@ -107,7 +107,7 @@
107
107
  panel.id = 'myclaw-artifacts-panel';
108
108
  panel.style.cssText = [
109
109
  'position: fixed',
110
- 'top: 30px',
110
+ 'top: 75px',
111
111
  'right: 0',
112
112
  'width: 380px',
113
113
  'max-height: calc(100vh - 550px)',
@@ -190,14 +190,28 @@
190
190
  }
191
191
 
192
192
  // ═══ 请求数据 ═══
193
+ var CLAW_STORAGE_KEY = 'myclaw-claw-name';
194
+
193
195
  function getArtifactsUrl() {
196
+ var claw = window.location.hostname.split('.')[0];
197
+ // 如果 hostname 不含 claw,从 localStorage 读取
198
+ if (!claw.includes('claw')) {
199
+ claw = localStorage.getItem(CLAW_STORAGE_KEY);
200
+ if (!claw) {
201
+ console.log('[myclaw-artifacts] ❌ 未配置 claw 名称');
202
+ console.log('[myclaw-artifacts] 请运行: myclaw set claw <你的claw名称>');
203
+ return null;
204
+ }
205
+ }
194
206
  var agentName = getAgentName() || 'main';
195
207
  var wsPrefix = agentName === 'main' ? 'workspace' : 'workspace-' + agentName;
196
- return window.location.origin + '/cmd/api/preview?path=' + wsPrefix + '/.myclaw/__MY_ARTIFACTS__.json';
208
+ return 'https://cdn.yiranlaoshi.com/' + claw + '/' + wsPrefix + '/.myclaw/__MY_ARTIFACTS__.json';
197
209
  }
198
210
 
199
211
  function fetchArtifacts(contentEl) {
200
- fetch(getArtifactsUrl())
212
+ var url = getArtifactsUrl();
213
+ if (!url) return;
214
+ fetch(url)
201
215
  .then(function (res) {
202
216
  if (!res.ok) throw new Error('HTTP ' + res.status);
203
217
  return res.json();
@@ -732,7 +746,16 @@
732
746
 
733
747
  // 封面
734
748
  if (data.cover_path) {
735
- var coverRow = createInfoRow('\uD83D\uDCF7 \u5C01\u9762', data.cover_path);
749
+ var coverRow = document.createElement('div');
750
+ coverRow.style.cssText = 'display:flex;flex-direction:column;gap:6px;font-family:monospace;';
751
+ var coverLbl = document.createElement('span');
752
+ coverLbl.style.cssText = 'color:#888;font-size:12px;';
753
+ coverLbl.textContent = '\uD83D\uDCF7 \u5C01\u9762';
754
+ coverRow.appendChild(coverLbl);
755
+ var coverImg = document.createElement('img');
756
+ coverImg.src = buildPreviewUrl(cachedData, data.cover_path);
757
+ coverImg.style.cssText = 'width:100%;max-height:120px;object-fit:cover;border-radius:4px;border:1px solid #3d3d5c;';
758
+ coverRow.appendChild(coverImg);
736
759
  info.appendChild(coverRow);
737
760
  }
738
761
 
@@ -837,7 +860,31 @@
837
860
 
838
861
  // 关闭按钮
839
862
  var footer = document.createElement('div');
840
- footer.style.cssText = 'padding: 0 24px 20px;text-align:center;';
863
+ footer.style.cssText = 'padding: 0 24px 20px;text-align:center;display:flex;gap:10px;justify-content:center;';
864
+
865
+ var showcaseBtn = document.createElement('a');
866
+ var agentName = getAgentName() || 'main';
867
+ var wsName = agentName === 'main' ? 'workspace' : 'workspace-' + agentName;
868
+ showcaseBtn.href = 'https://www.yiranlaoshi.com/showcase?workspace=' + wsName;
869
+ showcaseBtn.target = '_blank';
870
+ showcaseBtn.textContent = '\uD83D\uDC41 \u67E5\u770B' + agentName + '\u9879\u76EE\u96C6';
871
+ showcaseBtn.style.cssText = [
872
+ 'padding: 10px 20px',
873
+ 'background: #a78bfa',
874
+ 'border: none',
875
+ 'border-radius: 6px',
876
+ 'color: #fff',
877
+ 'font-size: 13px',
878
+ 'font-family: monospace',
879
+ 'font-weight: bold',
880
+ 'cursor: pointer',
881
+ 'text-decoration: none',
882
+ 'transition: background 0.15s',
883
+ 'display: inline-block',
884
+ ].join(';');
885
+ showcaseBtn.onmouseenter = function () { showcaseBtn.style.background = '#8b5cf6'; };
886
+ showcaseBtn.onmouseleave = function () { showcaseBtn.style.background = '#a78bfa'; };
887
+ footer.appendChild(showcaseBtn);
841
888
 
842
889
  var closeSuccessBtn = document.createElement('button');
843
890
  closeSuccessBtn.textContent = '\u5F00\u5FC3\uFF01';
@@ -851,9 +851,9 @@
851
851
  // ── 按钮列表 ──
852
852
  var btns = [
853
853
  { label: "\uD83D\uDCAC \u6DFB\u52A0\u5BF9\u8BDD", desc: "\u6253\u5F00\u5DF2\u6709\u4F19\u4F34\u7684\u5BF9\u8BDD\u7A97\u53E3", hasInput: true, inputTitle: "\u6DFB\u52A0\u5BF9\u8BDD", placeholder: "\u8F93\u5165\u4F19\u4F34\u540D\u79F0\uFF0C\u5982 kakaxi", hint: "\u8F93\u5165\u4F60\u7684\u4F19\u4F34\u7684\u540D\u79F0\uFF08\u82F1\u6587\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u8FDE\u5B57\u7B26\uFF09\uFF0C\u70B9\u51FB\u540E\u4F1A\u6253\u5F00\u5BF9\u8BDD\u7A97\u53E3", cmd: "mc tui {name}", color: "#10b981" },
854
- { label: "\uD83D\uDE80 \u516D\u53F7\u5347\u7EA7", desc: "\u5347\u7EA7 myclaw \u5230\u6700\u65B0\u7248\u672C", hasInput: false, cmd: "mc up", color: "#8b5cf6" },
855
- { label: "\uD83D\uDD04 \u4E8C\u53F7\u91CD\u542F", desc: "\u91CD\u542F\u670D\u52A1\uFF0C\u4FEE\u590D\u5927\u591A\u6570\u95EE\u9898", hasInput: false, cmd: "mc restart", color: "#ef4444" },
856
- { label: "\uD83E\uDD1D \u4E09\u53F7\u65B0\u4F19\u4F34", desc: "\u521B\u5EFA\u4E00\u4E2A\u65B0\u7684 AI \u4F19\u4F34", hasInput: true, inputTitle: "\u65B0\u5EFA\u4F19\u4F34", placeholder: "\u8F93\u5165\u65B0\u4F19\u4F34\u540D\u79F0\uFF0C\u5982 my-cat", hint: "\u7ED9\u4F60\u7684\u65B0 AI \u4F19\u4F34\u8D77\u4E2A\u540D\u5B57\uFF08\u82F1\u6587\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u8FDE\u5B57\u7B26\uFF09\uFF0C\u70B9\u51FB\u540E\u4F1A\u81EA\u52A8\u521B\u5EFA", cmd: "mc tui {name}", color: "#3b82f6" },
854
+ { label: "\uD83D\uDE80 \u5347\u7EA7", desc: "\u5347\u7EA7 myclaw \u5230\u6700\u65B0\u7248\u672C", hasInput: false, cmd: "mc up", color: "#8b5cf6" },
855
+ { label: "\uD83D\uDD04 \u91CD\u542F", desc: "\u91CD\u542F\u670D\u52A1\uFF0C\u4FEE\u590D\u5927\u591A\u6570\u95EE\u9898", hasInput: false, cmd: "mc restart", color: "#ef4444" },
856
+ { label: "\uD83E\uDD1D \u65B0\u4F19\u4F34", desc: "\u521B\u5EFA\u4E00\u4E2A\u65B0\u7684 AI \u4F19\u4F34", hasInput: true, inputTitle: "\u65B0\u5EFA\u4F19\u4F34", placeholder: "\u8F93\u5165\u65B0\u4F19\u4F34\u540D\u79F0\uFF0C\u5982 my-cat", hint: "\u7ED9\u4F60\u7684\u65B0 AI \u4F19\u4F34\u8D77\u4E2A\u540D\u5B57\uFF08\u82F1\u6587\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u8FDE\u5B57\u7B26\uFF09\uFF0C\u70B9\u51FB\u540E\u4F1A\u81EA\u52A8\u521B\u5EFA", cmd: "mc tui {name}", color: "#3b82f6" },
857
857
  ];
858
858
 
859
859
  btns.forEach(function (item) {
package/index.js CHANGED
@@ -1177,6 +1177,7 @@ const MENU_ITEMS = [
1177
1177
  { key: 'restart', label: '重启', cmd: 'mc restart', desc: 'AI 助手卡住了?让它重新启动一下', action: runRestart },
1178
1178
  { key: 'new', label: '😊新伙伴', cmd: 'mc new', desc: '创建一个新的 AI 助手,给它取个名字', action: runNew },
1179
1179
  { key: 'tui', label: '新对话', cmd: 'mc tui', desc: '唤起新对话上下文', action: () => runTui(true) },
1180
+ { key: 'server', label: '🚀服务端', cmd: 'mc server', desc: '启动后端文件同步服务(守护模式,自动重启)', action: () => runServer() },
1180
1181
  { key: 'status', label: '网址', cmd: 'mc status', desc: '获取控制台链接,复制到浏览器打开', action: runStatus },
1181
1182
  {
1182
1183
  key: 'update', label: '升级', cmd: 'mc up', desc: '让 MyClaw 工具升级到最新版本', action: () => {
@@ -1394,6 +1395,174 @@ function showInjectMenu() {
1394
1395
  });
1395
1396
  }
1396
1397
 
1398
+ // ============================================================================
1399
+ // Server 后端服务
1400
+ // ============================================================================
1401
+
1402
+ const rl = require('readline');
1403
+
1404
+ function generateRandomName() {
1405
+ const letters = 'abcdefghijklmnopqrstuvwxyz';
1406
+ let name = '';
1407
+ for (let i = 0; i < 3; i++) {
1408
+ name += letters[Math.floor(Math.random() * letters.length)];
1409
+ }
1410
+ for (let i = 0; i < 3; i++) {
1411
+ name += String(Math.floor(Math.random() * 5) + 5);
1412
+ }
1413
+ return name;
1414
+ }
1415
+
1416
+ async function ensureConfig(customName) {
1417
+ const fs = require('fs');
1418
+ const configPath = path.join(__dirname, 'server', 'config.json');
1419
+
1420
+ if (fs.existsSync(configPath)) {
1421
+ return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1422
+ }
1423
+
1424
+ const clawName = customName || generateRandomName();
1425
+ const config = {
1426
+ claw: clawName
1427
+ };
1428
+
1429
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
1430
+ console.log('[Server] 配置文件已创建: ' + configPath);
1431
+ console.log('');
1432
+ return config;
1433
+ }
1434
+
1435
+ function runSync(workspaceName) {
1436
+ const { spawn } = require('child_process');
1437
+ const fs = require('fs');
1438
+
1439
+ // 用户目录下的服务目录
1440
+ const targetDir = path.join(os.homedir(), '.openclaw', 'myclaw', 'server');
1441
+ const targetPyPath = path.join(targetDir, 'sync_workspace.py');
1442
+ const targetConfigPath = path.join(targetDir, 'config.json');
1443
+ const sourcePyPath = path.join(__dirname, 'server', 'sync_workspace.py');
1444
+
1445
+ // 确保目标目录存在
1446
+ if (!fs.existsSync(targetDir)) {
1447
+ fs.mkdirSync(targetDir, { recursive: true });
1448
+ }
1449
+
1450
+ // 同步 py 文件
1451
+ fs.copyFileSync(sourcePyPath, targetPyPath);
1452
+
1453
+ // 读取配置(如果不存在则创建)
1454
+ let config;
1455
+ if (fs.existsSync(targetConfigPath)) {
1456
+ config = JSON.parse(fs.readFileSync(targetConfigPath, 'utf-8'));
1457
+ } else {
1458
+ config = { claw: generateRandomName() };
1459
+ fs.writeFileSync(targetConfigPath, JSON.stringify(config, null, 2), 'utf-8');
1460
+ }
1461
+
1462
+ const agent = workspaceName ? `workspace-${workspaceName}` : 'workspace';
1463
+
1464
+ console.log('[Sync] 单次同步: ' + agent);
1465
+ console.log('[Sync] CLAW_NAME: ' + config.claw);
1466
+
1467
+ const child = spawn('python3', [targetPyPath, '--agent', agent], {
1468
+ stdio: 'inherit',
1469
+ shell: false,
1470
+ env: { ...process.env, CLAW_NAME: config.claw }
1471
+ });
1472
+
1473
+ child.on('error', (err) => {
1474
+ console.error('[' + colors.red + '错误' + colors.nc + '] 同步失败: ' + err.message);
1475
+ process.exit(1);
1476
+ });
1477
+
1478
+ child.on('exit', (code) => {
1479
+ if (code !== 0) {
1480
+ console.error('[' + colors.red + '错误' + colors.nc + '] 同步异常退出,代码: ' + code);
1481
+ process.exit(code);
1482
+ }
1483
+ });
1484
+ }
1485
+
1486
+ async function runServer(name) {
1487
+ const { spawn } = require('child_process');
1488
+ const fs = require('fs');
1489
+
1490
+ // 用户目录下的服务目录
1491
+ const targetDir = path.join(os.homedir(), '.openclaw', 'myclaw', 'server');
1492
+ const targetPyPath = path.join(targetDir, 'sync_workspace.py');
1493
+ const targetConfigPath = path.join(targetDir, 'config.json');
1494
+ const sourcePyPath = path.join(__dirname, 'server', 'sync_workspace.py');
1495
+
1496
+ // 1. 创建目标目录
1497
+ if (!fs.existsSync(targetDir)) {
1498
+ fs.mkdirSync(targetDir, { recursive: true });
1499
+ console.log('[Server] 创建目录: ' + targetDir);
1500
+ }
1501
+
1502
+ // 2. 覆盖 py 文件
1503
+ fs.copyFileSync(sourcePyPath, targetPyPath);
1504
+ console.log('[Server] 同步脚本: ' + targetPyPath);
1505
+
1506
+ // 3. 确保配置文件存在(如果已有就不动)
1507
+ let config;
1508
+ if (fs.existsSync(targetConfigPath)) {
1509
+ config = JSON.parse(fs.readFileSync(targetConfigPath, 'utf-8'));
1510
+ console.log('[Server] 已有配置: ' + targetConfigPath);
1511
+ } else {
1512
+ const clawName = name || generateRandomName();
1513
+ config = { claw: clawName };
1514
+ fs.writeFileSync(targetConfigPath, JSON.stringify(config, null, 2), 'utf-8');
1515
+ console.log('[Server] 创建配置: ' + targetConfigPath);
1516
+ }
1517
+
1518
+ console.log('[Server] CLAW_NAME: ' + config.claw);
1519
+ console.log('[Server] 守护模式: 已启用');
1520
+ console.log('');
1521
+
1522
+ // 4. 检查并安装 Python 依赖
1523
+ console.log('[Server] 检查依赖...');
1524
+ try {
1525
+ execSync('python3 -c "from watchdog.observers import Observer; from qiniu import Auth"', { stdio: 'pipe' });
1526
+ console.log('[Server] 依赖已满足');
1527
+ } catch (e) {
1528
+ console.log('[Server] 正在安装依赖 watchdog qiniu...');
1529
+ try {
1530
+ execSync('pip3 install watchdog qiniu --trusted-host mirrors.aliyun.com -i https://mirrors.aliyun.com/pypi/simple/', { stdio: 'inherit' });
1531
+ console.log('[Server] 依赖安装完成');
1532
+ } catch (err) {
1533
+ console.error('[' + colors.red + '错误' + colors.nc + '] 依赖安装失败,请检查网络');
1534
+ process.exit(1);
1535
+ }
1536
+ }
1537
+ console.log('');
1538
+
1539
+ let child;
1540
+
1541
+ function startProcess() {
1542
+ console.log('[Server] 启动服务...');
1543
+ child = spawn('python3', [targetPyPath], {
1544
+ stdio: 'inherit',
1545
+ shell: false,
1546
+ env: { ...process.env, CLAW_NAME: config.claw }
1547
+ });
1548
+
1549
+ child.on('error', (err) => {
1550
+ console.error('[' + colors.red + '错误' + colors.nc + '] 启动失败: ' + err.message);
1551
+ });
1552
+
1553
+ child.on('exit', (code) => {
1554
+ if (code !== null && code !== 0) {
1555
+ console.log('[' + colors.yellow + '警告' + colors.nc + '] 服务异常退出,代码: ' + code + ',3秒后重启...');
1556
+ } else {
1557
+ console.log('[Server] 服务已停止');
1558
+ }
1559
+ setTimeout(startProcess, 3000);
1560
+ });
1561
+ }
1562
+
1563
+ startProcess();
1564
+ }
1565
+
1397
1566
  function showHelp() {
1398
1567
  console.log('');
1399
1568
  console.log('MyClaw - 学生友好的 OpenClaw 工具');
@@ -1564,6 +1733,10 @@ if (!command) {
1564
1733
  console.log('🔄 正在重启 Gateway 使配置生效...');
1565
1734
  console.log('');
1566
1735
  runRestart();
1736
+ } else if (command === 'server') {
1737
+ runServer(args[1]); // args[1] 是可选的 name
1738
+ } else if (command === 'sync') {
1739
+ runSync(args[1]); // args[1] 是可选的 workspace 名称
1567
1740
  } else {
1568
1741
  console.error('[' + colors.red + '错误' + colors.nc + '] 未知命令: ' + command);
1569
1742
  showHelp();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiyiran/myclaw",
3
- "version": "1.0.237",
3
+ "version": "1.0.239",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,298 @@
1
+ import os
2
+ import sys
3
+ import argparse
4
+ import platform
5
+ from datetime import datetime, timezone, timedelta
6
+ import json
7
+ import time
8
+ from watchdog.observers import Observer
9
+ from watchdog.events import FileSystemEventHandler
10
+ from qiniu import Auth, put_file_v2, CdnManager
11
+
12
+ # 跨平台获取 openclaw 目录
13
+ def get_openclaw_path():
14
+ if platform.system() == 'Windows' or os.path.exists('/root/.openclaw'):
15
+ return '/root/.openclaw'
16
+ return os.path.expanduser('~/.openclaw')
17
+
18
+ claw = os.environ.get("CLAW_NAME", "claw")
19
+
20
+ BASE_URL = f"https://cdn.yiranlaoshi.com/{claw}"
21
+
22
+ QINIU_KEY = "T3tgxM7EMx1j4VESw4m4PIfFXoOvBo-wQEOQewXX"
23
+ QINIU_TOKEN = "PVZvlKVOjX2RqlV2ILMg-QwpNMssOlpVbaEzypz0"
24
+
25
+
26
+ QINIU_BUCKET = "yiran1"
27
+
28
+ access_key = QINIU_KEY
29
+ secret_key = QINIU_TOKEN
30
+ bucket_name = QINIU_BUCKET
31
+ q = Auth(access_key, secret_key)
32
+ cdn_manager = CdnManager(q)
33
+
34
+
35
+ class MyHandler(FileSystemEventHandler):
36
+ def _is_file_event(self, event):
37
+ return not getattr(event, "is_directory", False)
38
+
39
+ # def on_created(self, event):
40
+ # if not self._is_file_event(event):
41
+ # return
42
+ # print(f"🟢 新建: {event.src_path}")
43
+ # file_gen(event.src_path, "add")
44
+
45
+ def on_modified(self, event):
46
+ if not self._is_file_event(event):
47
+ return
48
+ print(f"🟡 修改: {event.src_path}")
49
+ file_gen(event.src_path, "add")
50
+
51
+ def on_deleted(self, event):
52
+ if not self._is_file_event(event):
53
+ return
54
+ print(f"🔴 删除: {event.src_path}")
55
+ file_gen(event.src_path, "delete")
56
+
57
+ def on_moved(self, event):
58
+ if not self._is_file_event(event):
59
+ return
60
+ print(f"🔵 移动: {event.src_path} -> {getattr(event, 'dest_path', '')}")
61
+ file_gen(event.src_path, "delete")
62
+ if getattr(event, 'dest_path', ''):
63
+ file_gen(event.dest_path, "add")
64
+
65
+
66
+ def now_iso():
67
+ # 生成 +08:00 时间格式
68
+ tz = timezone(timedelta(hours=8))
69
+ return datetime.now(tz).isoformat()
70
+
71
+
72
+ def gen_id():
73
+ return f"asset-{int(datetime.now().timestamp() * 1000)}"
74
+
75
+
76
+ def get_type(file_path):
77
+ return file_path.split(".")[-1] if "." in file_path else "unknown"
78
+
79
+
80
+ def init_config(workspace_id, file_path, method="add"):
81
+ base_path = get_openclaw_path()
82
+ file = f"{base_path}/{workspace_id}/.myclaw/__MY_ARTIFACTS__.json"
83
+
84
+ # 确保目录存在
85
+ os.makedirs(os.path.dirname(file), exist_ok=True)
86
+
87
+ now = now_iso()
88
+
89
+ # 如果文件不存在,先初始化(包含根级创建/更改时间)
90
+ if not os.path.exists(file):
91
+ data = {
92
+ "workspace_id": workspace_id,
93
+ "base_url": BASE_URL,
94
+ "assets": [],
95
+ "created_at": now,
96
+ "updated_at": now
97
+ }
98
+ else:
99
+ with open(file, "r", encoding="utf-8") as f:
100
+ try:
101
+ data = json.load(f)
102
+ except:
103
+ data = {
104
+ "workspace_id": workspace_id,
105
+ "assets": [],
106
+ "created_at": now,
107
+ "updated_at": now
108
+ }
109
+ # 确保根级时间字段存在;保留已有 created_at,更新 updated_at
110
+ if "created_at" not in data:
111
+ data["created_at"] = now
112
+ data["updated_at"] = now
113
+
114
+ if method == "delete":
115
+ # 删除逻辑:将匹配 path 的项过滤掉,并更新根级更新时间
116
+ data["assets"] = [asset for asset in data.get(
117
+ "assets", []) if asset.get("path") != file_path]
118
+ data["updated_at"] = now
119
+ else:
120
+ found = False
121
+ # 查找是否已存在该 path
122
+ for asset in data.get("assets", []):
123
+ if asset.get("path") == file_path:
124
+ # 保证已有 created_at,不被覆盖;更新 updated_at 和 type(以防扩展名变化)
125
+ asset.setdefault("created_at", now)
126
+ asset["updated_at"] = now
127
+ asset["type"] = get_type(file_path)
128
+ found = True
129
+ break
130
+
131
+ # 不存在则新增(asset 已包含创建/更改时间)
132
+ if not found:
133
+ new_asset = {
134
+ "id": gen_id(),
135
+ "type": get_type(file_path),
136
+ "path": file_path,
137
+ "created_at": now,
138
+ "updated_at": now
139
+ }
140
+ data["assets"].append(new_asset)
141
+
142
+ # 更新根级更新时间
143
+ data["updated_at"] = now
144
+
145
+ # 写回文件
146
+ with open(file, "w", encoding="utf-8") as f:
147
+ json.dump(data, f, ensure_ascii=False, indent=2)
148
+
149
+
150
+ def file_gen(path, method):
151
+ # 规范化路径,兼容 Windows 和 *nix
152
+ norm_path = os.path.normpath(path)
153
+ parts = norm_path.split(os.sep)
154
+
155
+ # 查找包含 workspace 标识的分段(例如 workspace 或 workspace-xxx)
156
+ space_idx = -1
157
+ for i, p in enumerate(parts):
158
+ if "workspace" in p:
159
+ space_idx = i
160
+ break
161
+
162
+ if space_idx == -1:
163
+ # 路径中不包含 workspace,不处理
164
+ return
165
+
166
+ space_id = parts[space_idx]
167
+ # 相对路径:workspace 后面的所有段,使用 '/' 作为存储格式(保证跨平台一致)
168
+ relative_parts = parts[space_idx + 1:]
169
+ if not relative_parts:
170
+ # 没有文件名,可能是目录事件,忽略
171
+ return
172
+ relative_path = "/".join(relative_parts)
173
+
174
+ # 上传 key 保留 workspace 段(用于 CDN 存储),但写入 JSON 时只保存相对路径
175
+ path_url = f"{space_id}/{relative_path}"
176
+ key = f"{claw}/{path_url}"
177
+
178
+ if method == "delete":
179
+ # 删除时不需要上传,只清理配置文件记录,传入的 file_path 不包含 space_id(relative_path)
180
+ init_config(space_id, relative_path, method="delete")
181
+ print(f"🗑️ 已删除配置记录: {relative_path}")
182
+ else:
183
+ # 添加或修改时进行上传
184
+ try:
185
+ token = q.upload_token(bucket_name, key, 3600)
186
+ # 直接使用 put_file_v2 上传文件路径,避免读取到空内容时导致 SDK 报错缺少 data 参数
187
+ ret, info = put_file_v2(token, key, path)
188
+
189
+ # 刷新 CDN
190
+ cdn_url = f"https://cdn.yiranlaoshi.com/{key}"
191
+ cdn_ret, cdn_info = cdn_manager.refresh_urls([cdn_url])
192
+
193
+ # 如果是 __MY_ARTIFACTS__.json 配置文件,只上传不写入(防止循环触发)
194
+ if "__MY_ARTIFACTS__.json" in path:
195
+ print(f"🚀 已上传配置文件: {relative_path}")
196
+ print(f"风 CDN刷新: {cdn_url} - {cdn_info.status_code if info else 'unknown'}")
197
+ else:
198
+ # 记录时使用不包含 workspace 的相对路径
199
+ init_config(space_id, relative_path, method="add")
200
+ print(f"🚀 已上传并记录: {relative_path}")
201
+ print(f"风 CDN刷新: {cdn_url} - {cdn_info.status_code if info else 'unknown'}")
202
+ except Exception as e:
203
+ print(f"❌ 上传/读取文件失败: {e}")
204
+
205
+
206
+ def sync_all(workspace_id):
207
+ """全量同步指定 workspace 下所有文件"""
208
+ base_path = get_openclaw_path()
209
+ workspace_path = f"{base_path}/{workspace_id}"
210
+
211
+ if not os.path.exists(workspace_path):
212
+ print(f"[错误] workspace 不存在: {workspace_path}")
213
+ return False
214
+
215
+ print(f"[全量同步] workspace: {workspace_id}")
216
+ print(f"[全量同步] 目录: {workspace_path}")
217
+ print("-" * 50)
218
+
219
+ file_count = 0
220
+ success_count = 0
221
+
222
+ for root, dirs, files in os.walk(workspace_path):
223
+ for filename in files:
224
+ # 排除配置文件自身(防止循环)
225
+ if filename == "__MY_ARTIFACTS__.json":
226
+ continue
227
+
228
+ local_path = os.path.join(root, filename)
229
+ rel_path = os.path.relpath(local_path, workspace_path)
230
+ relative_path = rel_path.replace(os.sep, "/")
231
+
232
+ file_count += 1
233
+
234
+ # 上传到七牛云
235
+ path_url = f"{workspace_id}/{relative_path}"
236
+ key = f"{claw}/{path_url}"
237
+
238
+ try:
239
+ token = q.upload_token(bucket_name, key, 3600)
240
+ ret, info = put_file_v2(token, key, local_path)
241
+ cdn_url = f"https://cdn.yiranlaoshi.com/{key}"
242
+ cdn_manager.refresh_urls([cdn_url])
243
+ print(f" ✅ {relative_path}")
244
+ success_count += 1
245
+ except Exception as e:
246
+ print(f" ❌ {relative_path}: {e}")
247
+
248
+ # 写入配置记录
249
+ init_config(workspace_id, relative_path, method="add")
250
+
251
+ print("-" * 50)
252
+ print(f"[完成] 共 {file_count} 个文件,成功 {success_count} 个")
253
+
254
+ # 最后上传配置文件
255
+ config_path = f"{workspace_path}/.myclaw/__MY_ARTIFACTS__.json"
256
+ if os.path.exists(config_path):
257
+ print(f"[配置] 上传配置文件...")
258
+ try:
259
+ key = f"{claw}/{workspace_id}/.myclaw/__MY_ARTIFACTS__.json"
260
+ token = q.upload_token(bucket_name, key, 3600)
261
+ ret, info = put_file_v2(token, key, config_path)
262
+ cdn_url = f"https://cdn.yiranlaoshi.com/{key}"
263
+ cdn_manager.refresh_urls([cdn_url])
264
+ print(f" ✅ __MY_ARTIFACTS__.json")
265
+ except Exception as e:
266
+ print(f" ❌ __MY_ARTIFACTS__.json: {e}")
267
+
268
+ return True
269
+
270
+
271
+ if __name__ == "__main__":
272
+ parser = argparse.ArgumentParser(description="文件同步服务")
273
+ parser.add_argument("--agent", help="启动前先全量同步指定 workspace")
274
+ args = parser.parse_args()
275
+
276
+ base_path = get_openclaw_path()
277
+ path = base_path
278
+
279
+ # 如果指定了 --agent,先全量同步
280
+ if args.agent:
281
+ if not sync_all(args.agent):
282
+ sys.exit(1)
283
+ print("")
284
+
285
+ event_handler = MyHandler()
286
+ observer = Observer()
287
+ observer.schedule(event_handler, path, recursive=True)
288
+
289
+ observer.start()
290
+ print(f"开始监听目录: {path}")
291
+
292
+ try:
293
+ while True:
294
+ time.sleep(1)
295
+ except KeyboardInterrupt:
296
+ observer.stop()
297
+
298
+ observer.join()