@aiyiran/myclaw 1.0.236 → 1.0.238
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/assets/myclaw-artifacts.js +52 -5
- package/assets/myclaw-inject.js +3 -3
- package/index.js +173 -0
- package/package.json +1 -1
- package/server/sync_workspace.py +288 -0
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
panel.id = 'myclaw-artifacts-panel';
|
|
108
108
|
panel.style.cssText = [
|
|
109
109
|
'position: fixed',
|
|
110
|
-
'top:
|
|
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
|
|
208
|
+
return 'https://cdn.yiranlaoshi.com/' + claw + '/' + wsPrefix + '/.myclaw/__MY_ARTIFACTS__.json';
|
|
197
209
|
}
|
|
198
210
|
|
|
199
211
|
function fetchArtifacts(contentEl) {
|
|
200
|
-
|
|
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 =
|
|
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';
|
package/assets/myclaw-inject.js
CHANGED
|
@@ -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 \
|
|
855
|
-
{ label: "\uD83D\uDD04 \
|
|
856
|
-
{ label: "\uD83E\uDD1D \
|
|
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
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import argparse
|
|
4
|
+
from datetime import datetime, timezone, timedelta
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from watchdog.observers import Observer
|
|
8
|
+
from watchdog.events import FileSystemEventHandler
|
|
9
|
+
from qiniu import Auth, put_file_v2, CdnManager
|
|
10
|
+
|
|
11
|
+
claw = os.environ.get("CLAW_NAME", "claw")
|
|
12
|
+
|
|
13
|
+
BASE_URL = f"https://cdn.yiranlaoshi.com/{claw}"
|
|
14
|
+
|
|
15
|
+
QINIU_KEY = "T3tgxM7EMx1j4VESw4m4PIfFXoOvBo-wQEOQewXX"
|
|
16
|
+
QINIU_TOKEN = "PVZvlKVOjX2RqlV2ILMg-QwpNMssOlpVbaEzypz0"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
QINIU_BUCKET = "yiran1"
|
|
20
|
+
|
|
21
|
+
access_key = QINIU_KEY
|
|
22
|
+
secret_key = QINIU_TOKEN
|
|
23
|
+
bucket_name = QINIU_BUCKET
|
|
24
|
+
q = Auth(access_key, secret_key)
|
|
25
|
+
cdn_manager = CdnManager(q)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MyHandler(FileSystemEventHandler):
|
|
29
|
+
def _is_file_event(self, event):
|
|
30
|
+
return not getattr(event, "is_directory", False)
|
|
31
|
+
|
|
32
|
+
# def on_created(self, event):
|
|
33
|
+
# if not self._is_file_event(event):
|
|
34
|
+
# return
|
|
35
|
+
# print(f"🟢 新建: {event.src_path}")
|
|
36
|
+
# file_gen(event.src_path, "add")
|
|
37
|
+
|
|
38
|
+
def on_modified(self, event):
|
|
39
|
+
if not self._is_file_event(event):
|
|
40
|
+
return
|
|
41
|
+
print(f"🟡 修改: {event.src_path}")
|
|
42
|
+
file_gen(event.src_path, "add")
|
|
43
|
+
|
|
44
|
+
def on_deleted(self, event):
|
|
45
|
+
if not self._is_file_event(event):
|
|
46
|
+
return
|
|
47
|
+
print(f"🔴 删除: {event.src_path}")
|
|
48
|
+
file_gen(event.src_path, "delete")
|
|
49
|
+
|
|
50
|
+
def on_moved(self, event):
|
|
51
|
+
if not self._is_file_event(event):
|
|
52
|
+
return
|
|
53
|
+
print(f"🔵 移动: {event.src_path} -> {getattr(event, 'dest_path', '')}")
|
|
54
|
+
file_gen(event.src_path, "delete")
|
|
55
|
+
if getattr(event, 'dest_path', ''):
|
|
56
|
+
file_gen(event.dest_path, "add")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def now_iso():
|
|
60
|
+
# 生成 +08:00 时间格式
|
|
61
|
+
tz = timezone(timedelta(hours=8))
|
|
62
|
+
return datetime.now(tz).isoformat()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def gen_id():
|
|
66
|
+
return f"asset-{int(datetime.now().timestamp() * 1000)}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_type(file_path):
|
|
70
|
+
return file_path.split(".")[-1] if "." in file_path else "unknown"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def init_config(workspace_id, file_path, method="add"):
|
|
74
|
+
file = f"/root/.openclaw/{workspace_id}/.myclaw/__MY_ARTIFACTS__.json"
|
|
75
|
+
|
|
76
|
+
# 确保目录存在
|
|
77
|
+
os.makedirs(os.path.dirname(file), exist_ok=True)
|
|
78
|
+
|
|
79
|
+
now = now_iso()
|
|
80
|
+
|
|
81
|
+
# 如果文件不存在,先初始化(包含根级创建/更改时间)
|
|
82
|
+
if not os.path.exists(file):
|
|
83
|
+
data = {
|
|
84
|
+
"workspace_id": workspace_id,
|
|
85
|
+
"base_url": BASE_URL,
|
|
86
|
+
"assets": [],
|
|
87
|
+
"created_at": now,
|
|
88
|
+
"updated_at": now
|
|
89
|
+
}
|
|
90
|
+
else:
|
|
91
|
+
with open(file, "r", encoding="utf-8") as f:
|
|
92
|
+
try:
|
|
93
|
+
data = json.load(f)
|
|
94
|
+
except:
|
|
95
|
+
data = {
|
|
96
|
+
"workspace_id": workspace_id,
|
|
97
|
+
"assets": [],
|
|
98
|
+
"created_at": now,
|
|
99
|
+
"updated_at": now
|
|
100
|
+
}
|
|
101
|
+
# 确保根级时间字段存在;保留已有 created_at,更新 updated_at
|
|
102
|
+
if "created_at" not in data:
|
|
103
|
+
data["created_at"] = now
|
|
104
|
+
data["updated_at"] = now
|
|
105
|
+
|
|
106
|
+
if method == "delete":
|
|
107
|
+
# 删除逻辑:将匹配 path 的项过滤掉,并更新根级更新时间
|
|
108
|
+
data["assets"] = [asset for asset in data.get(
|
|
109
|
+
"assets", []) if asset.get("path") != file_path]
|
|
110
|
+
data["updated_at"] = now
|
|
111
|
+
else:
|
|
112
|
+
found = False
|
|
113
|
+
# 查找是否已存在该 path
|
|
114
|
+
for asset in data.get("assets", []):
|
|
115
|
+
if asset.get("path") == file_path:
|
|
116
|
+
# 保证已有 created_at,不被覆盖;更新 updated_at 和 type(以防扩展名变化)
|
|
117
|
+
asset.setdefault("created_at", now)
|
|
118
|
+
asset["updated_at"] = now
|
|
119
|
+
asset["type"] = get_type(file_path)
|
|
120
|
+
found = True
|
|
121
|
+
break
|
|
122
|
+
|
|
123
|
+
# 不存在则新增(asset 已包含创建/更改时间)
|
|
124
|
+
if not found:
|
|
125
|
+
new_asset = {
|
|
126
|
+
"id": gen_id(),
|
|
127
|
+
"type": get_type(file_path),
|
|
128
|
+
"path": file_path,
|
|
129
|
+
"created_at": now,
|
|
130
|
+
"updated_at": now
|
|
131
|
+
}
|
|
132
|
+
data["assets"].append(new_asset)
|
|
133
|
+
|
|
134
|
+
# 更新根级更新时间
|
|
135
|
+
data["updated_at"] = now
|
|
136
|
+
|
|
137
|
+
# 写回文件
|
|
138
|
+
with open(file, "w", encoding="utf-8") as f:
|
|
139
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def file_gen(path, method):
|
|
143
|
+
# 规范化路径,兼容 Windows 和 *nix
|
|
144
|
+
norm_path = os.path.normpath(path)
|
|
145
|
+
parts = norm_path.split(os.sep)
|
|
146
|
+
|
|
147
|
+
# 查找包含 workspace 标识的分段(例如 workspace 或 workspace-xxx)
|
|
148
|
+
space_idx = -1
|
|
149
|
+
for i, p in enumerate(parts):
|
|
150
|
+
if "workspace" in p:
|
|
151
|
+
space_idx = i
|
|
152
|
+
break
|
|
153
|
+
|
|
154
|
+
if space_idx == -1:
|
|
155
|
+
# 路径中不包含 workspace,不处理
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
space_id = parts[space_idx]
|
|
159
|
+
# 相对路径:workspace 后面的所有段,使用 '/' 作为存储格式(保证跨平台一致)
|
|
160
|
+
relative_parts = parts[space_idx + 1:]
|
|
161
|
+
if not relative_parts:
|
|
162
|
+
# 没有文件名,可能是目录事件,忽略
|
|
163
|
+
return
|
|
164
|
+
relative_path = "/".join(relative_parts)
|
|
165
|
+
|
|
166
|
+
# 上传 key 保留 workspace 段(用于 CDN 存储),但写入 JSON 时只保存相对路径
|
|
167
|
+
path_url = f"{space_id}/{relative_path}"
|
|
168
|
+
key = f"{claw}/{path_url}"
|
|
169
|
+
|
|
170
|
+
if method == "delete":
|
|
171
|
+
# 删除时不需要上传,只清理配置文件记录,传入的 file_path 不包含 space_id(relative_path)
|
|
172
|
+
init_config(space_id, relative_path, method="delete")
|
|
173
|
+
print(f"🗑️ 已删除配置记录: {relative_path}")
|
|
174
|
+
else:
|
|
175
|
+
# 添加或修改时进行上传
|
|
176
|
+
try:
|
|
177
|
+
token = q.upload_token(bucket_name, key, 3600)
|
|
178
|
+
# 直接使用 put_file_v2 上传文件路径,避免读取到空内容时导致 SDK 报错缺少 data 参数
|
|
179
|
+
ret, info = put_file_v2(token, key, path)
|
|
180
|
+
|
|
181
|
+
# 刷新 CDN
|
|
182
|
+
cdn_url = f"https://cdn.yiranlaoshi.com/{key}"
|
|
183
|
+
cdn_ret, cdn_info = cdn_manager.refresh_urls([cdn_url])
|
|
184
|
+
|
|
185
|
+
# 如果是 __MY_ARTIFACTS__.json 配置文件,只上传不写入(防止循环触发)
|
|
186
|
+
if "__MY_ARTIFACTS__.json" in path:
|
|
187
|
+
print(f"🚀 已上传配置文件: {relative_path}")
|
|
188
|
+
print(f"风 CDN刷新: {cdn_url} - {cdn_info.status_code if info else 'unknown'}")
|
|
189
|
+
else:
|
|
190
|
+
# 记录时使用不包含 workspace 的相对路径
|
|
191
|
+
init_config(space_id, relative_path, method="add")
|
|
192
|
+
print(f"🚀 已上传并记录: {relative_path}")
|
|
193
|
+
print(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 sync_all(workspace_id):
|
|
199
|
+
"""全量同步指定 workspace 下所有文件"""
|
|
200
|
+
workspace_path = f"/root/.openclaw/{workspace_id}"
|
|
201
|
+
|
|
202
|
+
if not os.path.exists(workspace_path):
|
|
203
|
+
print(f"[错误] workspace 不存在: {workspace_path}")
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
print(f"[全量同步] workspace: {workspace_id}")
|
|
207
|
+
print(f"[全量同步] 目录: {workspace_path}")
|
|
208
|
+
print("-" * 50)
|
|
209
|
+
|
|
210
|
+
file_count = 0
|
|
211
|
+
success_count = 0
|
|
212
|
+
|
|
213
|
+
for root, dirs, files in os.walk(workspace_path):
|
|
214
|
+
for filename in files:
|
|
215
|
+
# 排除配置文件自身(防止循环)
|
|
216
|
+
if filename == "__MY_ARTIFACTS__.json":
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
local_path = os.path.join(root, filename)
|
|
220
|
+
rel_path = os.path.relpath(local_path, workspace_path)
|
|
221
|
+
relative_path = rel_path.replace(os.sep, "/")
|
|
222
|
+
|
|
223
|
+
file_count += 1
|
|
224
|
+
|
|
225
|
+
# 上传到七牛云
|
|
226
|
+
path_url = f"{workspace_id}/{relative_path}"
|
|
227
|
+
key = f"{claw}/{path_url}"
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
token = q.upload_token(bucket_name, key, 3600)
|
|
231
|
+
ret, info = put_file_v2(token, key, local_path)
|
|
232
|
+
cdn_url = f"https://cdn.yiranlaoshi.com/{key}"
|
|
233
|
+
cdn_manager.refresh_urls([cdn_url])
|
|
234
|
+
print(f" ✅ {relative_path}")
|
|
235
|
+
success_count += 1
|
|
236
|
+
except Exception as e:
|
|
237
|
+
print(f" ❌ {relative_path}: {e}")
|
|
238
|
+
|
|
239
|
+
# 写入配置记录
|
|
240
|
+
init_config(workspace_id, relative_path, method="add")
|
|
241
|
+
|
|
242
|
+
print("-" * 50)
|
|
243
|
+
print(f"[完成] 共 {file_count} 个文件,成功 {success_count} 个")
|
|
244
|
+
|
|
245
|
+
# 最后上传配置文件
|
|
246
|
+
config_path = f"{workspace_path}/.myclaw/__MY_ARTIFACTS__.json"
|
|
247
|
+
if os.path.exists(config_path):
|
|
248
|
+
print(f"[配置] 上传配置文件...")
|
|
249
|
+
try:
|
|
250
|
+
key = f"{claw}/{workspace_id}/.myclaw/__MY_ARTIFACTS__.json"
|
|
251
|
+
token = q.upload_token(bucket_name, key, 3600)
|
|
252
|
+
ret, info = put_file_v2(token, key, config_path)
|
|
253
|
+
cdn_url = f"https://cdn.yiranlaoshi.com/{key}"
|
|
254
|
+
cdn_manager.refresh_urls([cdn_url])
|
|
255
|
+
print(f" ✅ __MY_ARTIFACTS__.json")
|
|
256
|
+
except Exception as e:
|
|
257
|
+
print(f" ❌ __MY_ARTIFACTS__.json: {e}")
|
|
258
|
+
|
|
259
|
+
return True
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
if __name__ == "__main__":
|
|
263
|
+
parser = argparse.ArgumentParser(description="文件同步服务")
|
|
264
|
+
parser.add_argument("--agent", help="启动前先全量同步指定 workspace")
|
|
265
|
+
args = parser.parse_args()
|
|
266
|
+
|
|
267
|
+
path = "/root/.openclaw"
|
|
268
|
+
|
|
269
|
+
# 如果指定了 --agent,先全量同步
|
|
270
|
+
if args.agent:
|
|
271
|
+
if not sync_all(args.agent):
|
|
272
|
+
sys.exit(1)
|
|
273
|
+
print("")
|
|
274
|
+
|
|
275
|
+
event_handler = MyHandler()
|
|
276
|
+
observer = Observer()
|
|
277
|
+
observer.schedule(event_handler, path, recursive=True)
|
|
278
|
+
|
|
279
|
+
observer.start()
|
|
280
|
+
print(f"开始监听目录: {path}")
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
while True:
|
|
284
|
+
time.sleep(1)
|
|
285
|
+
except KeyboardInterrupt:
|
|
286
|
+
observer.stop()
|
|
287
|
+
|
|
288
|
+
observer.join()
|