@aiyiran/myclaw 1.0.242 → 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.
@@ -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
- // ═══ 构建预览 URL(CDN) ═══
51
- function buildPreviewUrl(data, assetPath) {
52
- var claw = window.location.hostname.split('.')[0];
53
- if (!claw.includes('claw')) {
54
- claw = localStorage.getItem(CLAW_STORAGE_KEY);
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 };
55
71
  }
56
- if (!claw) {
57
- console.error('[myclaw-artifacts] ❌ 未配置 claw 名称,无法构建预览链接');
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);
97
+ }
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
- var wsName = data.workspace_id || 'main';
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,20 +265,45 @@
197
265
  }
198
266
 
199
267
  // ═══ 请求数据 ═══
200
- function getArtifactsUrl() {
201
- var agentName = getAgentName() || 'main';
202
- var wsPrefix = agentName === 'main' ? 'workspace' : 'workspace-' + agentName;
203
- return window.location.origin + '/cmd/api/preview?path=' + wsPrefix + '/.myclaw/__MY_ARTIFACTS__.json';
268
+ function getWorkspaceId() {
269
+ return cachedConfig ? cachedConfig.workspaceName : 'workspace';
204
270
  }
205
271
 
206
- function fetchArtifacts(contentEl) {
207
- var url = getArtifactsUrl();
208
- if (!url) return;
209
- fetch(url)
272
+ function fetchArtifactsFromLocalAPI(wsPrefix) {
273
+ return fetch(MYCLAW_API_BASE + '/api/artifacts?workspace=' + encodeURIComponent(wsPrefix))
210
274
  .then(function (res) {
211
275
  if (!res.ok) throw new Error('HTTP ' + res.status);
212
276
  return res.json();
213
- })
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
214
307
  .then(function (data) {
215
308
  cachedData = data;
216
309
  if (!contentEl) return;
@@ -220,10 +313,9 @@
220
313
  }
221
314
  renderArtifactsList(contentEl, data);
222
315
  })
223
- .catch(function (err) {
224
- console.error('[myclaw-artifacts] 加载失败:', err);
316
+ .catch(function () {
225
317
  if (contentEl) {
226
- contentEl.innerHTML = '<div style="text-align:center;padding:32px;color:#ff6b6b;">加载失败,请稍后重试</div>';
318
+ contentEl.innerHTML = '<div style="text-align:center;padding:32px;color:#888;">暂无作品</div>';
227
319
  }
228
320
  });
229
321
  }
@@ -618,15 +710,12 @@
618
710
  titleInput.style.borderColor = '#ff4444';
619
711
  return;
620
712
  }
621
- var agentName = getAgentName() || 'main';
622
- var wsField = agentName === 'main' ? 'workspace' : 'workspace-' + agentName;
623
- var clawVal = window.location.hostname.split('.')[0];
624
713
  var payload = {
625
714
  title: titleVal,
626
- workspace: wsField,
715
+ workspace: cachedConfig ? cachedConfig.workspaceName : 'workspace',
627
716
  cover_path: coverSelect.value || '',
628
717
  entry_path: entrySelect.value || '',
629
- claw: clawVal,
718
+ claw: cachedConfig ? cachedConfig.claw : '',
630
719
  };
631
720
  submitBtn.disabled = true;
632
721
  submitBtn.textContent = '\u53D1\u5E03\u4E2D...';
@@ -834,9 +923,9 @@
834
923
  qrSection.style.cssText = 'display:flex;flex-direction:column;align-items:center;gap:6px;padding-top:4px;';
835
924
 
836
925
  var qrCanvas = document.createElement('canvas');
837
- qrCanvas.style.cssText = 'width:140px;height:140px;border-radius:6px;background:#fff;padding:8px;';
926
+ qrCanvas.style.cssText = 'width:140px;height:140px;border-radius:6px;';
838
927
  try {
839
- generateQR(qrCanvas, data.permanent_url, 140);
928
+ generateQR(qrCanvas, data.permanent_url, 280);
840
929
  } catch (e) {
841
930
  console.warn('[myclaw-artifacts] QR generate error:', e);
842
931
  qrCanvas.style.display = 'none';
@@ -858,11 +947,11 @@
858
947
  footer.style.cssText = 'padding: 0 24px 20px;text-align:center;display:flex;gap:10px;justify-content:center;';
859
948
 
860
949
  var showcaseBtn = document.createElement('a');
861
- var agentName = getAgentName() || 'main';
862
- var wsName = agentName === 'main' ? 'workspace' : 'workspace-' + agentName;
863
- showcaseBtn.href = 'https://www.yiranlaoshi.com/showcase?workspace=' + wsName;
950
+ var cfgAgent = cachedConfig ? cachedConfig.agentName : 'main';
951
+ var cfgWs = cachedConfig ? cachedConfig.workspaceName : 'workspace';
952
+ showcaseBtn.href = 'https://www.yiranlaoshi.com/showcase?workspace=' + cfgWs;
864
953
  showcaseBtn.target = '_blank';
865
- showcaseBtn.textContent = '\uD83D\uDC41 \u67E5\u770B' + agentName + '\u9879\u76EE\u96C6';
954
+ showcaseBtn.textContent = '\uD83D\uDC41 \u67E5\u770B' + cfgAgent + '\u9879\u76EE\u96C6';
866
955
  showcaseBtn.style.cssText = [
867
956
  'padding: 10px 20px',
868
957
  'background: #a78bfa',
@@ -1253,17 +1342,27 @@
1253
1342
  if (!qr) throw new Error("QR code too large");
1254
1343
 
1255
1344
  var moduleCount = qr.getModuleCount();
1256
- var cellSize = size / moduleCount;
1345
+ // quiet zone: QR 规范要求至少 4 模块的留白
1346
+ var quietZone = 4;
1347
+ var totalModules = moduleCount + quietZone * 2;
1348
+ var cellSize = size / totalModules;
1257
1349
  canvas.width = size;
1258
1350
  canvas.height = size;
1259
1351
  var ctx = canvas.getContext('2d');
1352
+ // 白色背景(含 quiet zone)
1260
1353
  ctx.fillStyle = '#ffffff';
1261
1354
  ctx.fillRect(0, 0, size, size);
1262
- ctx.fillStyle = '#1e1e2e';
1355
+ // 纯黑前景,确保扫码对比度
1356
+ ctx.fillStyle = '#000000';
1263
1357
  for (var row = 0; row < moduleCount; row++) {
1264
1358
  for (var col = 0; col < moduleCount; col++) {
1265
1359
  if (qr.isDark(row, col)) {
1266
- ctx.fillRect(Math.floor(col * cellSize), Math.floor(row * cellSize), Math.ceil(cellSize), Math.ceil(cellSize));
1360
+ ctx.fillRect(
1361
+ Math.round((col + quietZone) * cellSize),
1362
+ Math.round((row + quietZone) * cellSize),
1363
+ Math.ceil(cellSize),
1364
+ Math.ceil(cellSize)
1365
+ );
1267
1366
  }
1268
1367
  }
1269
1368
  }
@@ -1300,8 +1399,11 @@
1300
1399
  function init() {
1301
1400
  injectStyles();
1302
1401
  createArtifactsButton();
1303
- startPolling();
1304
- console.log('[myclaw-artifacts] 初始化完成');
1402
+ // 检测环境 → 获取配置 → 启动轮询
1403
+ initConfig().then(function () {
1404
+ startPolling();
1405
+ console.log('[myclaw-artifacts] ✅ 初始化完成 (' + (envInfo.remote ? '远程: ' + envInfo.clawName : '本地') + ')');
1406
+ });
1305
1407
  }
1306
1408
 
1307
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiyiran/myclaw",
3
- "version": "1.0.242",
3
+ "version": "1.0.243",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
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('"connect-src \'self\' ws: wss:"');
237
- const needsFrameAncestors = content.includes('"frame-ancestors \'none\'"');
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 - 在 buildControlUiCspHeader 的 CSP 数组中放宽限制
256
+ // Patch 2: CSP - 全部放宽到最宽松模式
257
257
  const cspPatches = [];
258
- // 添加 frame-src 指令(允许 iframe 加载外部 https 资源)
258
+ // frame-src 允许所有
259
259
  if (needsFrameSrc) {
260
- content = content.replace(
261
- '"default-src \'self\'"',
262
- '"default-src \'self\'",\n\t\t"frame-src \'self\' https:"'
263
- );
264
- cspPatches.push('frame-src');
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
- // 放开 connect-src,允许 fetch/XHR 到外部 https API(包括 CDN
270
+ // connect-src 允许所有(含 http://127.0.0.1 本地 API)
267
271
  if (needsConnectSrc) {
268
- content = content.replace(
269
- '"connect-src \'self\' ws: wss:"',
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
- // 放开 frame-ancestors,允许被外部 https 页面嵌入
275
+ // frame-ancestors 允许所有
275
276
  if (needsFrameAncestors) {
276
- content = content.replace(
277
- '"frame-ancestors \'none\'"',
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);
@@ -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)