@aiyiran/myclaw 1.0.241 → 1.0.243
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 +139 -49
- package/index.js +75 -5
- package/package.json +1 -1
- package/patches/patch.js +20 -22
- package/server/sync_workspace.py +138 -0
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
var panelVisible = false;
|
|
31
31
|
var cachedData = null;
|
|
32
32
|
var pollTimer = null;
|
|
33
|
+
var cachedConfig = null; // { claw, base_url }
|
|
34
|
+
var envInfo = null; // { remote: bool, clawName: string|null }
|
|
35
|
+
var MYCLAW_API_PORT = 18800;
|
|
36
|
+
var MYCLAW_API_BASE = 'http://127.0.0.1:' + MYCLAW_API_PORT;
|
|
33
37
|
|
|
34
38
|
// ═══ 工具:从 URL 解析 agent 名称 ═══
|
|
35
39
|
function getAgentName() {
|
|
@@ -47,18 +51,82 @@
|
|
|
47
51
|
return '';
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
// ═══
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
// ═══ 环境检测 ═══
|
|
55
|
+
// 判断当前是远程服务器还是本地环境
|
|
56
|
+
// 远程:https://claw.yiranlaoshi.com → { remote: true, clawName: 'clawdev' }
|
|
57
|
+
// 远程:https://claw3.kekouen.cn → { remote: true, clawName: 'claw3' }
|
|
58
|
+
// 本地:http://127.0.0.1:18789 → { remote: false, clawName: null }
|
|
59
|
+
function detectEnvironment() {
|
|
60
|
+
var hostname = window.location.hostname;
|
|
61
|
+
|
|
62
|
+
// claw.yiranlaoshi.com → 特殊映射:clawdev
|
|
63
|
+
if (hostname === 'claw.yiranlaoshi.com') {
|
|
64
|
+
return { remote: true, clawName: 'clawdev' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// *.kekouen.cn → 子域名即 clawName
|
|
68
|
+
if (hostname.endsWith('.kekouen.cn')) {
|
|
69
|
+
var clawName = hostname.split('.')[0];
|
|
70
|
+
return { remote: true, clawName: clawName };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 本地或无法判定
|
|
74
|
+
return { remote: false, clawName: null };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ═══ 初始化配置 ═══
|
|
78
|
+
// 远程环境直接用 hostname 中的 clawName
|
|
79
|
+
// 本地环境从 sync_workspace.py 的 HTTP API 获取
|
|
80
|
+
function initConfig() {
|
|
81
|
+
envInfo = detectEnvironment();
|
|
82
|
+
|
|
83
|
+
// 解析 agentName 和 workspaceName(与环境无关,纯粹从 URL 参数获取)
|
|
84
|
+
var agent = getAgentName() || 'main';
|
|
85
|
+
var workspace = agent === 'main' ? 'workspace' : 'workspace-' + agent;
|
|
86
|
+
|
|
87
|
+
if (envInfo.remote) {
|
|
88
|
+
// 远程:直接拿到 clawName,不需要请求
|
|
89
|
+
cachedConfig = {
|
|
90
|
+
claw: envInfo.clawName,
|
|
91
|
+
base_url: 'https://cdn.yiranlaoshi.com/' + envInfo.clawName,
|
|
92
|
+
agentName: agent,
|
|
93
|
+
workspaceName: workspace,
|
|
94
|
+
};
|
|
95
|
+
console.log('[myclaw-artifacts] ✅ 远程环境:', envInfo.clawName, '| agent:', agent, '| workspace:', workspace);
|
|
96
|
+
return Promise.resolve(cachedConfig);
|
|
55
97
|
}
|
|
56
|
-
|
|
57
|
-
|
|
98
|
+
|
|
99
|
+
// 本地:从 API 获取 claw 配置
|
|
100
|
+
return fetch(MYCLAW_API_BASE + '/api/config')
|
|
101
|
+
.then(function (res) {
|
|
102
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
103
|
+
return res.json();
|
|
104
|
+
})
|
|
105
|
+
.then(function (data) {
|
|
106
|
+
data.agentName = agent;
|
|
107
|
+
data.workspaceName = workspace;
|
|
108
|
+
cachedConfig = data;
|
|
109
|
+
console.log('[myclaw-artifacts] ✅ 本地环境 | claw:', data.claw, '| agent:', agent, '| workspace:', workspace);
|
|
110
|
+
return data;
|
|
111
|
+
})
|
|
112
|
+
.catch(function (err) {
|
|
113
|
+
// API 不可用时仍保留 agent/workspace 信息
|
|
114
|
+
cachedConfig = { claw: null, base_url: null, agentName: agent, workspaceName: workspace };
|
|
115
|
+
console.warn('[myclaw-artifacts] ⚠ 本地 API 不可用:', err.message);
|
|
116
|
+
return cachedConfig;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ═══ 构建预览 URL ═══
|
|
121
|
+
// 统一走 CDN,clawName 和 workspaceName 由 cachedConfig 提供
|
|
122
|
+
function buildPreviewUrl(data, assetPath) {
|
|
123
|
+
var clawName = cachedConfig ? cachedConfig.claw : null;
|
|
124
|
+
var wsPrefix = cachedConfig ? cachedConfig.workspaceName : null;
|
|
125
|
+
if (!clawName || !wsPrefix) {
|
|
126
|
+
console.error('[myclaw-artifacts] ❌ 配置未就绪,无法构建预览链接');
|
|
58
127
|
return null;
|
|
59
128
|
}
|
|
60
|
-
|
|
61
|
-
return 'https://cdn.yiranlaoshi.com/' + claw + '/' + wsName + '/' + assetPath;
|
|
129
|
+
return 'https://cdn.yiranlaoshi.com/' + clawName + '/' + wsPrefix + '/' + assetPath;
|
|
62
130
|
}
|
|
63
131
|
|
|
64
132
|
// ═══ 创建按钮 ═══
|
|
@@ -197,32 +265,45 @@
|
|
|
197
265
|
}
|
|
198
266
|
|
|
199
267
|
// ═══ 请求数据 ═══
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
function getArtifactsUrl() {
|
|
203
|
-
var claw = window.location.hostname.split('.')[0];
|
|
204
|
-
// 如果 hostname 不含 claw,从 localStorage 读取
|
|
205
|
-
if (!claw.includes('claw')) {
|
|
206
|
-
claw = localStorage.getItem(CLAW_STORAGE_KEY);
|
|
207
|
-
if (!claw) {
|
|
208
|
-
console.log('[myclaw-artifacts] ❌ 未配置 claw 名称');
|
|
209
|
-
console.log('[myclaw-artifacts] 请在控制台执行: localStorage.setItem("myclaw-claw-name", "你的claw名称")');
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
var agentName = getAgentName() || 'main';
|
|
214
|
-
var wsPrefix = agentName === 'main' ? 'workspace' : 'workspace-' + agentName;
|
|
215
|
-
return 'https://cdn.yiranlaoshi.com/' + claw + '/' + wsPrefix + '/.myclaw/__MY_ARTIFACTS__.json';
|
|
268
|
+
function getWorkspaceId() {
|
|
269
|
+
return cachedConfig ? cachedConfig.workspaceName : 'workspace';
|
|
216
270
|
}
|
|
217
271
|
|
|
218
|
-
function
|
|
219
|
-
|
|
220
|
-
if (!url) return;
|
|
221
|
-
fetch(url)
|
|
272
|
+
function fetchArtifactsFromLocalAPI(wsPrefix) {
|
|
273
|
+
return fetch(MYCLAW_API_BASE + '/api/artifacts?workspace=' + encodeURIComponent(wsPrefix))
|
|
222
274
|
.then(function (res) {
|
|
223
275
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
224
276
|
return res.json();
|
|
225
|
-
})
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function fetchArtifactsFromCDN(wsPrefix) {
|
|
281
|
+
var clawName = cachedConfig ? cachedConfig.claw : null;
|
|
282
|
+
if (!clawName) return Promise.reject(new Error('no claw name'));
|
|
283
|
+
var url = 'https://cdn.yiranlaoshi.com/' + clawName + '/' + wsPrefix + '/.myclaw/__MY_ARTIFACTS__.json?t=' + Date.now();
|
|
284
|
+
return fetch(url).then(function (res) {
|
|
285
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
286
|
+
return res.text();
|
|
287
|
+
}).then(function (text) {
|
|
288
|
+
if (text.trim().indexOf('<') === 0) throw new Error('Not JSON');
|
|
289
|
+
return JSON.parse(text);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function fetchArtifacts(contentEl) {
|
|
294
|
+
var wsPrefix = getWorkspaceId();
|
|
295
|
+
var fetcher;
|
|
296
|
+
|
|
297
|
+
if (envInfo && envInfo.remote) {
|
|
298
|
+
// 远程环境 → 走 CDN
|
|
299
|
+
fetcher = fetchArtifactsFromCDN(wsPrefix);
|
|
300
|
+
} else {
|
|
301
|
+
// 本地环境 → 优先本地 API,失败降级 CDN
|
|
302
|
+
fetcher = fetchArtifactsFromLocalAPI(wsPrefix)
|
|
303
|
+
.catch(function () { return fetchArtifactsFromCDN(wsPrefix); });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
fetcher
|
|
226
307
|
.then(function (data) {
|
|
227
308
|
cachedData = data;
|
|
228
309
|
if (!contentEl) return;
|
|
@@ -232,10 +313,9 @@
|
|
|
232
313
|
}
|
|
233
314
|
renderArtifactsList(contentEl, data);
|
|
234
315
|
})
|
|
235
|
-
.catch(function (
|
|
236
|
-
console.error('[myclaw-artifacts] 加载失败:', err);
|
|
316
|
+
.catch(function () {
|
|
237
317
|
if (contentEl) {
|
|
238
|
-
contentEl.innerHTML = '<div style="text-align:center;padding:32px;color:#
|
|
318
|
+
contentEl.innerHTML = '<div style="text-align:center;padding:32px;color:#888;">暂无作品</div>';
|
|
239
319
|
}
|
|
240
320
|
});
|
|
241
321
|
}
|
|
@@ -630,15 +710,12 @@
|
|
|
630
710
|
titleInput.style.borderColor = '#ff4444';
|
|
631
711
|
return;
|
|
632
712
|
}
|
|
633
|
-
var agentName = getAgentName() || 'main';
|
|
634
|
-
var wsField = agentName === 'main' ? 'workspace' : 'workspace-' + agentName;
|
|
635
|
-
var clawVal = window.location.hostname.split('.')[0];
|
|
636
713
|
var payload = {
|
|
637
714
|
title: titleVal,
|
|
638
|
-
workspace:
|
|
715
|
+
workspace: cachedConfig ? cachedConfig.workspaceName : 'workspace',
|
|
639
716
|
cover_path: coverSelect.value || '',
|
|
640
717
|
entry_path: entrySelect.value || '',
|
|
641
|
-
claw:
|
|
718
|
+
claw: cachedConfig ? cachedConfig.claw : '',
|
|
642
719
|
};
|
|
643
720
|
submitBtn.disabled = true;
|
|
644
721
|
submitBtn.textContent = '\u53D1\u5E03\u4E2D...';
|
|
@@ -846,9 +923,9 @@
|
|
|
846
923
|
qrSection.style.cssText = 'display:flex;flex-direction:column;align-items:center;gap:6px;padding-top:4px;';
|
|
847
924
|
|
|
848
925
|
var qrCanvas = document.createElement('canvas');
|
|
849
|
-
qrCanvas.style.cssText = 'width:140px;height:140px;border-radius:6px;
|
|
926
|
+
qrCanvas.style.cssText = 'width:140px;height:140px;border-radius:6px;';
|
|
850
927
|
try {
|
|
851
|
-
generateQR(qrCanvas, data.permanent_url,
|
|
928
|
+
generateQR(qrCanvas, data.permanent_url, 280);
|
|
852
929
|
} catch (e) {
|
|
853
930
|
console.warn('[myclaw-artifacts] QR generate error:', e);
|
|
854
931
|
qrCanvas.style.display = 'none';
|
|
@@ -870,11 +947,11 @@
|
|
|
870
947
|
footer.style.cssText = 'padding: 0 24px 20px;text-align:center;display:flex;gap:10px;justify-content:center;';
|
|
871
948
|
|
|
872
949
|
var showcaseBtn = document.createElement('a');
|
|
873
|
-
var
|
|
874
|
-
var
|
|
875
|
-
showcaseBtn.href = 'https://www.yiranlaoshi.com/showcase?workspace=' +
|
|
950
|
+
var cfgAgent = cachedConfig ? cachedConfig.agentName : 'main';
|
|
951
|
+
var cfgWs = cachedConfig ? cachedConfig.workspaceName : 'workspace';
|
|
952
|
+
showcaseBtn.href = 'https://www.yiranlaoshi.com/showcase?workspace=' + cfgWs;
|
|
876
953
|
showcaseBtn.target = '_blank';
|
|
877
|
-
showcaseBtn.textContent = '\uD83D\uDC41 \u67E5\u770B' +
|
|
954
|
+
showcaseBtn.textContent = '\uD83D\uDC41 \u67E5\u770B' + cfgAgent + '\u9879\u76EE\u96C6';
|
|
878
955
|
showcaseBtn.style.cssText = [
|
|
879
956
|
'padding: 10px 20px',
|
|
880
957
|
'background: #a78bfa',
|
|
@@ -1265,17 +1342,27 @@
|
|
|
1265
1342
|
if (!qr) throw new Error("QR code too large");
|
|
1266
1343
|
|
|
1267
1344
|
var moduleCount = qr.getModuleCount();
|
|
1268
|
-
|
|
1345
|
+
// quiet zone: QR 规范要求至少 4 模块的留白
|
|
1346
|
+
var quietZone = 4;
|
|
1347
|
+
var totalModules = moduleCount + quietZone * 2;
|
|
1348
|
+
var cellSize = size / totalModules;
|
|
1269
1349
|
canvas.width = size;
|
|
1270
1350
|
canvas.height = size;
|
|
1271
1351
|
var ctx = canvas.getContext('2d');
|
|
1352
|
+
// 白色背景(含 quiet zone)
|
|
1272
1353
|
ctx.fillStyle = '#ffffff';
|
|
1273
1354
|
ctx.fillRect(0, 0, size, size);
|
|
1274
|
-
|
|
1355
|
+
// 纯黑前景,确保扫码对比度
|
|
1356
|
+
ctx.fillStyle = '#000000';
|
|
1275
1357
|
for (var row = 0; row < moduleCount; row++) {
|
|
1276
1358
|
for (var col = 0; col < moduleCount; col++) {
|
|
1277
1359
|
if (qr.isDark(row, col)) {
|
|
1278
|
-
ctx.fillRect(
|
|
1360
|
+
ctx.fillRect(
|
|
1361
|
+
Math.round((col + quietZone) * cellSize),
|
|
1362
|
+
Math.round((row + quietZone) * cellSize),
|
|
1363
|
+
Math.ceil(cellSize),
|
|
1364
|
+
Math.ceil(cellSize)
|
|
1365
|
+
);
|
|
1279
1366
|
}
|
|
1280
1367
|
}
|
|
1281
1368
|
}
|
|
@@ -1312,8 +1399,11 @@
|
|
|
1312
1399
|
function init() {
|
|
1313
1400
|
injectStyles();
|
|
1314
1401
|
createArtifactsButton();
|
|
1315
|
-
|
|
1316
|
-
|
|
1402
|
+
// 检测环境 → 获取配置 → 启动轮询
|
|
1403
|
+
initConfig().then(function () {
|
|
1404
|
+
startPolling();
|
|
1405
|
+
console.log('[myclaw-artifacts] ✅ 初始化完成 (' + (envInfo.remote ? '远程: ' + envInfo.clawName : '本地') + ')');
|
|
1406
|
+
});
|
|
1317
1407
|
}
|
|
1318
1408
|
|
|
1319
1409
|
if (document.readyState === 'loading') {
|
package/index.js
CHANGED
|
@@ -1172,7 +1172,53 @@ function padRight(str, len) {
|
|
|
1172
1172
|
// 交互式菜单(上下键选择)
|
|
1173
1173
|
// ============================================================================
|
|
1174
1174
|
|
|
1175
|
+
// ============================================================================
|
|
1176
|
+
// 机器选择配置
|
|
1177
|
+
// ============================================================================
|
|
1178
|
+
const MACHINE_CONFIG = [
|
|
1179
|
+
{ name: 'kendy', claw: 'claw1', desc: 'kendy 的机器' },
|
|
1180
|
+
{ name: 'Ethan', claw: 'claw2', desc: 'Ethan 的机器' },
|
|
1181
|
+
{ name: 'HENRY', claw: 'claw3', desc: 'HENRY 的机器' },
|
|
1182
|
+
{ name: 'Mo靖宇', claw: 'claw4', desc: 'Mo靖宇 的机器' },
|
|
1183
|
+
{ name: '高兴', claw: 'claw5', desc: '高兴 的机器' },
|
|
1184
|
+
{ name: '伊伊', claw: 'claw6', desc: '伊伊 的机器' },
|
|
1185
|
+
{ name: '雨熙', claw: 'claw7', desc: '雨熙 的机器' },
|
|
1186
|
+
{ name: '绍博', claw: 'claw8', desc: '绍博 的机器' },
|
|
1187
|
+
];
|
|
1188
|
+
|
|
1189
|
+
function showMachineMenu() {
|
|
1190
|
+
console.log('');
|
|
1191
|
+
console.log(' ' + colors.blue + '机器选择' + colors.nc);
|
|
1192
|
+
console.log('----------------------------------------');
|
|
1193
|
+
console.log('');
|
|
1194
|
+
MACHINE_CONFIG.forEach((m, i) => {
|
|
1195
|
+
console.log(' ' + colors.cyan + (i + 1) + '.' + colors.nc + ' ' + m.name + ' (' + colors.dim + m.claw + colors.nc + ')');
|
|
1196
|
+
console.log(' ' + colors.dim + m.desc + colors.nc);
|
|
1197
|
+
console.log('');
|
|
1198
|
+
});
|
|
1199
|
+
console.log(' ' + colors.cyan + '0.' + colors.nc + ' 返回');
|
|
1200
|
+
console.log('');
|
|
1201
|
+
|
|
1202
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1203
|
+
rl.question('请选择机器 [0-' + MACHINE_CONFIG.length + ']: ', function (answer) {
|
|
1204
|
+
rl.close();
|
|
1205
|
+
const choice = parseInt(answer.trim());
|
|
1206
|
+
if (choice === 0) return;
|
|
1207
|
+
if (choice >= 1 && choice <= MACHINE_CONFIG.length) {
|
|
1208
|
+
const selected = MACHINE_CONFIG[choice - 1];
|
|
1209
|
+
console.log('');
|
|
1210
|
+
console.log(colors.green + ' → 已选择: ' + selected.name + ' (' + selected.claw + ')' + colors.nc);
|
|
1211
|
+
console.log(' ' + colors.dim + '→ 正在用 Chrome 打开...' + colors.nc);
|
|
1212
|
+
const { execSync } = require('child_process');
|
|
1213
|
+
execSync('open -a "Google Chrome" "https://' + selected.claw + '.kekouen.cn?token=aiyiran"', { stdio: 'ignore' });
|
|
1214
|
+
} else {
|
|
1215
|
+
console.log('[' + colors.red + '错误' + colors.nc + '] 无效选择');
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1175
1220
|
const MENU_ITEMS = [
|
|
1221
|
+
{ key: 'machine', label: '💻机器', cmd: 'mc machine', desc: '选择不同的机器', action: showMachineMenu },
|
|
1176
1222
|
{ key: 'start', label: '🦞启动🦞', cmd: 'mc start', desc: '把你的 AI 助手叫醒,让它开始工作', action: () => { const start = require('./start'); start.run(); } },
|
|
1177
1223
|
{ key: 'restart', label: '重启', cmd: 'mc restart', desc: 'AI 助手卡住了?让它重新启动一下', action: runRestart },
|
|
1178
1224
|
{ key: 'new', label: '😊新伙伴', cmd: 'mc new', desc: '创建一个新的 AI 助手,给它取个名字', action: runNew },
|
|
@@ -1484,7 +1530,7 @@ function runSync(workspaceName) {
|
|
|
1484
1530
|
}
|
|
1485
1531
|
|
|
1486
1532
|
async function runServer(name) {
|
|
1487
|
-
const { spawn } = require('child_process');
|
|
1533
|
+
const { spawn, execSync: execSyncLocal } = require('child_process');
|
|
1488
1534
|
const fs = require('fs');
|
|
1489
1535
|
|
|
1490
1536
|
// 用户目录下的服务目录
|
|
@@ -1492,6 +1538,19 @@ async function runServer(name) {
|
|
|
1492
1538
|
const targetPyPath = path.join(targetDir, 'sync_workspace.py');
|
|
1493
1539
|
const targetConfigPath = path.join(targetDir, 'config.json');
|
|
1494
1540
|
const sourcePyPath = path.join(__dirname, 'server', 'sync_workspace.py');
|
|
1541
|
+
const pidFile = path.join(os.homedir(), '.openclaw', '.myclaw-sync.pid');
|
|
1542
|
+
|
|
1543
|
+
// 0. 杀死旧的 sync_workspace 进程(通过 PID 文件)
|
|
1544
|
+
if (fs.existsSync(pidFile)) {
|
|
1545
|
+
try {
|
|
1546
|
+
const oldPid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
1547
|
+
if (oldPid && !isNaN(oldPid)) {
|
|
1548
|
+
try { process.kill(oldPid, 'SIGTERM'); } catch (e) { /* 不存在则忽略 */ }
|
|
1549
|
+
console.log('[Server] 已终止旧进程 PID=' + oldPid);
|
|
1550
|
+
}
|
|
1551
|
+
} catch (e) { /* 读取失败忽略 */ }
|
|
1552
|
+
try { fs.unlinkSync(pidFile); } catch (e) { }
|
|
1553
|
+
}
|
|
1495
1554
|
|
|
1496
1555
|
// 1. 创建目标目录
|
|
1497
1556
|
if (!fs.existsSync(targetDir)) {
|
|
@@ -1499,7 +1558,7 @@ async function runServer(name) {
|
|
|
1499
1558
|
console.log('[Server] 创建目录: ' + targetDir);
|
|
1500
1559
|
}
|
|
1501
1560
|
|
|
1502
|
-
// 2. 覆盖 py
|
|
1561
|
+
// 2. 覆盖 py 文件(确保最新)
|
|
1503
1562
|
fs.copyFileSync(sourcePyPath, targetPyPath);
|
|
1504
1563
|
console.log('[Server] 同步脚本: ' + targetPyPath);
|
|
1505
1564
|
|
|
@@ -1550,14 +1609,25 @@ async function runServer(name) {
|
|
|
1550
1609
|
console.error('[' + colors.red + '错误' + colors.nc + '] 启动失败: ' + err.message);
|
|
1551
1610
|
});
|
|
1552
1611
|
|
|
1553
|
-
child.on('exit', (code) => {
|
|
1612
|
+
child.on('exit', (code, signal) => {
|
|
1613
|
+
// 被 SIGTERM 杀死(下一次 mc server 启动时)→ 不重启
|
|
1614
|
+
if (signal === 'SIGTERM') {
|
|
1615
|
+
console.log('[Server] 服务已被外部终止,不再重启');
|
|
1616
|
+
process.exit(0);
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1554
1619
|
if (code !== null && code !== 0) {
|
|
1555
1620
|
console.log('[' + colors.yellow + '警告' + colors.nc + '] 服务异常退出,代码: ' + code + ',3秒后重启...');
|
|
1621
|
+
setTimeout(startProcess, 3000);
|
|
1556
1622
|
} else {
|
|
1557
|
-
console.log('[Server]
|
|
1623
|
+
console.log('[Server] 服务已正常停止');
|
|
1558
1624
|
}
|
|
1559
|
-
setTimeout(startProcess, 3000);
|
|
1560
1625
|
});
|
|
1626
|
+
|
|
1627
|
+
// 记录子进程 PID 到文件
|
|
1628
|
+
if (child.pid) {
|
|
1629
|
+
fs.writeFileSync(pidFile, String(child.pid), 'utf-8');
|
|
1630
|
+
}
|
|
1561
1631
|
}
|
|
1562
1632
|
|
|
1563
1633
|
startProcess();
|
package/package.json
CHANGED
package/patches/patch.js
CHANGED
|
@@ -232,9 +232,9 @@ function patch() {
|
|
|
232
232
|
|
|
233
233
|
// 检查是否需要 patch(分别检测每项,避免部分已 patch 导致整体跳过)
|
|
234
234
|
const needsMicrophonePatch = content.includes('microphone=()');
|
|
235
|
-
const needsFrameSrc = !content.includes('"frame-src');
|
|
236
|
-
const needsConnectSrc = content.includes(
|
|
237
|
-
const needsFrameAncestors = content.includes(
|
|
235
|
+
const needsFrameSrc = !content.includes('"frame-src *');
|
|
236
|
+
const needsConnectSrc = !content.includes("connect-src *");
|
|
237
|
+
const needsFrameAncestors = !content.includes("frame-ancestors *");
|
|
238
238
|
const needsAnyCspPatch = needsFrameSrc || needsConnectSrc || needsFrameAncestors;
|
|
239
239
|
|
|
240
240
|
if (needsMicrophonePatch || needsAnyCspPatch) {
|
|
@@ -253,31 +253,29 @@ function patch() {
|
|
|
253
253
|
console.log('[myclaw-patch] ✅ 已修复 Permissions-Policy (microphone): ' + f);
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
// Patch 2: CSP -
|
|
256
|
+
// Patch 2: CSP - 全部放宽到最宽松模式
|
|
257
257
|
const cspPatches = [];
|
|
258
|
-
//
|
|
258
|
+
// frame-src → 允许所有
|
|
259
259
|
if (needsFrameSrc) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
260
|
+
if (content.includes('"frame-src')) {
|
|
261
|
+
content = content.replace(/\"frame-src[^"]*\"/g, '"frame-src *"');
|
|
262
|
+
} else {
|
|
263
|
+
content = content.replace(
|
|
264
|
+
'"default-src \'self\'"',
|
|
265
|
+
'"default-src \'self\'",\n\t\t"frame-src *"'
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
cspPatches.push('frame-src *');
|
|
265
269
|
}
|
|
266
|
-
//
|
|
270
|
+
// connect-src → 允许所有(含 http://127.0.0.1 本地 API)
|
|
267
271
|
if (needsConnectSrc) {
|
|
268
|
-
content = content.replace(
|
|
269
|
-
|
|
270
|
-
'"connect-src \'self\' https: ws: wss: https://cdn.yiranlaoshi.com"'
|
|
271
|
-
);
|
|
272
|
-
cspPatches.push('connect-src');
|
|
272
|
+
content = content.replace(/\"connect-src[^"]*\"/g, '"connect-src *"');
|
|
273
|
+
cspPatches.push('connect-src *');
|
|
273
274
|
}
|
|
274
|
-
//
|
|
275
|
+
// frame-ancestors → 允许所有
|
|
275
276
|
if (needsFrameAncestors) {
|
|
276
|
-
content = content.replace(
|
|
277
|
-
|
|
278
|
-
'"frame-ancestors \'self\' https:"'
|
|
279
|
-
);
|
|
280
|
-
cspPatches.push('frame-ancestors');
|
|
277
|
+
content = content.replace(/\"frame-ancestors[^"]*\"/g, '"frame-ancestors *"');
|
|
278
|
+
cspPatches.push('frame-ancestors *');
|
|
281
279
|
}
|
|
282
280
|
if (cspPatches.length > 0) {
|
|
283
281
|
console.log('[myclaw-patch] ✅ 已修复 CSP (' + cspPatches.join(', ') + '): ' + f);
|
package/server/sync_workspace.py
CHANGED
|
@@ -5,6 +5,9 @@ import platform
|
|
|
5
5
|
from datetime import datetime, timezone, timedelta
|
|
6
6
|
import json
|
|
7
7
|
import time
|
|
8
|
+
import threading
|
|
9
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
10
|
+
from urllib.parse import urlparse, parse_qs
|
|
8
11
|
from watchdog.observers import Observer
|
|
9
12
|
from watchdog.events import FileSystemEventHandler
|
|
10
13
|
from qiniu import Auth, put_file_v2, CdnManager
|
|
@@ -33,9 +36,22 @@ cdn_manager = CdnManager(q)
|
|
|
33
36
|
|
|
34
37
|
|
|
35
38
|
class MyHandler(FileSystemEventHandler):
|
|
39
|
+
# 需要忽略的文件模式
|
|
40
|
+
IGNORE_PATTERNS = {'.myclaw-sync.pid', '.DS_Store'}
|
|
41
|
+
|
|
36
42
|
def _is_file_event(self, event):
|
|
37
43
|
return not getattr(event, "is_directory", False)
|
|
38
44
|
|
|
45
|
+
def _should_ignore(self, filepath):
|
|
46
|
+
"""忽略 PID 文件、隐藏文件等非业务文件"""
|
|
47
|
+
basename = os.path.basename(filepath)
|
|
48
|
+
if basename in self.IGNORE_PATTERNS:
|
|
49
|
+
return True
|
|
50
|
+
# 忽略 .openclaw 根目录下的隐藏文件(非 workspace 目录内的)
|
|
51
|
+
if basename.startswith('.') and 'workspace' not in filepath:
|
|
52
|
+
return True
|
|
53
|
+
return False
|
|
54
|
+
|
|
39
55
|
# def on_created(self, event):
|
|
40
56
|
# if not self._is_file_event(event):
|
|
41
57
|
# return
|
|
@@ -45,18 +61,24 @@ class MyHandler(FileSystemEventHandler):
|
|
|
45
61
|
def on_modified(self, event):
|
|
46
62
|
if not self._is_file_event(event):
|
|
47
63
|
return
|
|
64
|
+
if self._should_ignore(event.src_path):
|
|
65
|
+
return
|
|
48
66
|
print(f"🟡 修改: {event.src_path}")
|
|
49
67
|
file_gen(event.src_path, "add")
|
|
50
68
|
|
|
51
69
|
def on_deleted(self, event):
|
|
52
70
|
if not self._is_file_event(event):
|
|
53
71
|
return
|
|
72
|
+
if self._should_ignore(event.src_path):
|
|
73
|
+
return
|
|
54
74
|
print(f"🔴 删除: {event.src_path}")
|
|
55
75
|
file_gen(event.src_path, "delete")
|
|
56
76
|
|
|
57
77
|
def on_moved(self, event):
|
|
58
78
|
if not self._is_file_event(event):
|
|
59
79
|
return
|
|
80
|
+
if self._should_ignore(event.src_path):
|
|
81
|
+
return
|
|
60
82
|
print(f"🔵 移动: {event.src_path} -> {getattr(event, 'dest_path', '')}")
|
|
61
83
|
file_gen(event.src_path, "delete")
|
|
62
84
|
if getattr(event, 'dest_path', ''):
|
|
@@ -268,11 +290,124 @@ def sync_all(workspace_id):
|
|
|
268
290
|
return True
|
|
269
291
|
|
|
270
292
|
|
|
293
|
+
# ═══════════════════════════════════════════════════════════════
|
|
294
|
+
# 内嵌 HTTP API 服务
|
|
295
|
+
# ═══════════════════════════════════════════════════════════════
|
|
296
|
+
|
|
297
|
+
API_PORT = int(os.environ.get("MYCLAW_API_PORT", "18800"))
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class MyclawAPIHandler(BaseHTTPRequestHandler):
|
|
301
|
+
"""轻量 HTTP API,供前端获取作品列表和配置"""
|
|
302
|
+
|
|
303
|
+
def _cors_headers(self):
|
|
304
|
+
self.send_header('Access-Control-Allow-Origin', '*')
|
|
305
|
+
self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS')
|
|
306
|
+
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
|
307
|
+
self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
|
|
308
|
+
|
|
309
|
+
def do_OPTIONS(self):
|
|
310
|
+
self.send_response(200)
|
|
311
|
+
self._cors_headers()
|
|
312
|
+
self.end_headers()
|
|
313
|
+
|
|
314
|
+
def do_GET(self):
|
|
315
|
+
parsed = urlparse(self.path)
|
|
316
|
+
path = parsed.path.rstrip('/')
|
|
317
|
+
params = parse_qs(parsed.query)
|
|
318
|
+
|
|
319
|
+
if path == '/api/config':
|
|
320
|
+
return self._handle_config()
|
|
321
|
+
elif path == '/api/artifacts':
|
|
322
|
+
return self._handle_artifacts(params)
|
|
323
|
+
else:
|
|
324
|
+
self.send_response(404)
|
|
325
|
+
self._cors_headers()
|
|
326
|
+
self.send_header('Content-Type', 'application/json; charset=utf-8')
|
|
327
|
+
self.end_headers()
|
|
328
|
+
self.wfile.write(json.dumps({"error": "not found"}).encode('utf-8'))
|
|
329
|
+
|
|
330
|
+
def _handle_config(self):
|
|
331
|
+
"""GET /api/config → 返回 claw 名称和 CDN base_url"""
|
|
332
|
+
data = {
|
|
333
|
+
"claw": claw,
|
|
334
|
+
"base_url": BASE_URL,
|
|
335
|
+
}
|
|
336
|
+
self.send_response(200)
|
|
337
|
+
self._cors_headers()
|
|
338
|
+
self.send_header('Content-Type', 'application/json; charset=utf-8')
|
|
339
|
+
self.end_headers()
|
|
340
|
+
self.wfile.write(json.dumps(data, ensure_ascii=False).encode('utf-8'))
|
|
341
|
+
|
|
342
|
+
def _handle_artifacts(self, params):
|
|
343
|
+
"""GET /api/artifacts?workspace=xxx → 直接从磁盘读取 __MY_ARTIFACTS__.json"""
|
|
344
|
+
workspace = params.get('workspace', [''])[0]
|
|
345
|
+
if not workspace:
|
|
346
|
+
self.send_response(400)
|
|
347
|
+
self._cors_headers()
|
|
348
|
+
self.send_header('Content-Type', 'application/json; charset=utf-8')
|
|
349
|
+
self.end_headers()
|
|
350
|
+
self.wfile.write(json.dumps({"error": "missing workspace param"}).encode('utf-8'))
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
base_path = get_openclaw_path()
|
|
354
|
+
json_path = os.path.join(base_path, workspace, '.myclaw', '__MY_ARTIFACTS__.json')
|
|
355
|
+
|
|
356
|
+
if not os.path.exists(json_path):
|
|
357
|
+
# 文件不存在,返回空结构
|
|
358
|
+
data = {
|
|
359
|
+
"workspace_id": workspace,
|
|
360
|
+
"base_url": BASE_URL,
|
|
361
|
+
"assets": [],
|
|
362
|
+
}
|
|
363
|
+
self.send_response(200)
|
|
364
|
+
self._cors_headers()
|
|
365
|
+
self.send_header('Content-Type', 'application/json; charset=utf-8')
|
|
366
|
+
self.end_headers()
|
|
367
|
+
self.wfile.write(json.dumps(data, ensure_ascii=False).encode('utf-8'))
|
|
368
|
+
return
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
with open(json_path, 'r', encoding='utf-8') as f:
|
|
372
|
+
content = f.read()
|
|
373
|
+
self.send_response(200)
|
|
374
|
+
self._cors_headers()
|
|
375
|
+
self.send_header('Content-Type', 'application/json; charset=utf-8')
|
|
376
|
+
self.end_headers()
|
|
377
|
+
self.wfile.write(content.encode('utf-8'))
|
|
378
|
+
except Exception as e:
|
|
379
|
+
self.send_response(500)
|
|
380
|
+
self._cors_headers()
|
|
381
|
+
self.send_header('Content-Type', 'application/json; charset=utf-8')
|
|
382
|
+
self.end_headers()
|
|
383
|
+
self.wfile.write(json.dumps({"error": str(e)}).encode('utf-8'))
|
|
384
|
+
|
|
385
|
+
def log_message(self, format, *args):
|
|
386
|
+
# 静默日志,避免轮询刷屏
|
|
387
|
+
pass
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def start_api_server():
|
|
391
|
+
"""在后台线程启动 HTTP API 服务"""
|
|
392
|
+
class ReusableHTTPServer(HTTPServer):
|
|
393
|
+
allow_reuse_address = True
|
|
394
|
+
|
|
395
|
+
server = ReusableHTTPServer(('0.0.0.0', API_PORT), MyclawAPIHandler)
|
|
396
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
397
|
+
thread.start()
|
|
398
|
+
print(f"[myclaw-api] ✅ HTTP API 服务已启动: http://127.0.0.1:{API_PORT}")
|
|
399
|
+
print(f"[myclaw-api] GET /api/config → claw 配置")
|
|
400
|
+
print(f"[myclaw-api] GET /api/artifacts → 作品列表")
|
|
401
|
+
|
|
402
|
+
|
|
271
403
|
if __name__ == "__main__":
|
|
272
404
|
parser = argparse.ArgumentParser(description="文件同步服务")
|
|
273
405
|
parser.add_argument("--agent", help="启动前先全量同步指定 workspace")
|
|
406
|
+
parser.add_argument("--port", type=int, default=API_PORT, help="API 服务端口 (默认 18800)")
|
|
274
407
|
args = parser.parse_args()
|
|
275
408
|
|
|
409
|
+
API_PORT = args.port
|
|
410
|
+
|
|
276
411
|
base_path = get_openclaw_path()
|
|
277
412
|
path = base_path
|
|
278
413
|
|
|
@@ -282,6 +417,9 @@ if __name__ == "__main__":
|
|
|
282
417
|
sys.exit(1)
|
|
283
418
|
print("")
|
|
284
419
|
|
|
420
|
+
# 启动 HTTP API 服务
|
|
421
|
+
start_api_server()
|
|
422
|
+
|
|
285
423
|
event_handler = MyHandler()
|
|
286
424
|
observer = Observer()
|
|
287
425
|
observer.schedule(event_handler, path, recursive=True)
|