@aiyiran/myclaw 1.1.68 → 1.1.70

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.
@@ -275,7 +275,7 @@
275
275
  var _scEnv = detectEnvironment();
276
276
  var _scClaw = _scEnv.remote ? _scEnv.clawName : (lastKnownClawName || '');
277
277
  var _scWs = getWorkspaceId();
278
- showcaseLink.href = 'https://www.yiranlaoshi.com/showcase?claw=' + _scClaw + '&workspace=' + _scWs;
278
+ showcaseLink.href = 'https://www.yiranlaoshi.com/showcase?claw=' + _scClaw + '&workspace=' + _scWs + '&clawName=' + _scClaw;
279
279
  showcaseLink.target = '_blank';
280
280
  showcaseLink.textContent = '我的项目';
281
281
  showcaseLink.style.cssText = 'cursor:pointer;padding:2px 10px;border-radius:3px;font-size:12px;background:rgba(255,255,255,0.08);transition:background 0.15s;color:#cdd6f4;text-decoration:none;';
@@ -2157,54 +2157,21 @@
2157
2157
  forkForm.appendChild(urlInput);
2158
2158
  forkForm.appendChild(errMsg);
2159
2159
 
2160
- // 确认按钮
2161
- var forkSubmitBtn = document.createElement('button');
2162
- forkSubmitBtn.textContent = '\u786E\u8BA4\u83B7\u53D6';
2163
- forkSubmitBtn.style.cssText = [
2164
- 'margin: 0 20px 20px',
2165
- 'padding: 10px',
2166
- 'background: #4a4a7a',
2167
- 'border: none',
2168
- 'border-radius: 4px',
2169
- 'color: #cdd6f4',
2170
- 'font-size: 13px',
2171
- 'font-family: monospace',
2172
- 'cursor: pointer',
2173
- 'transition: background 0.15s',
2174
- ].join(';');
2175
- forkSubmitBtn.onmouseenter = function () { if (!forkSubmitBtn.disabled) forkSubmitBtn.style.background = '#5a5a9a'; };
2176
- forkSubmitBtn.onmouseleave = function () { if (!forkSubmitBtn.disabled) forkSubmitBtn.style.background = '#4a4a7a'; };
2177
-
2178
- forkSubmitBtn.onclick = function () {
2179
- var url = urlInput.value.trim();
2180
-
2181
- // 校验 URL 完整性
2182
- var valid = false;
2183
- try {
2184
- var parsed = new URL(url);
2185
- valid = parsed.protocol === 'http:' || parsed.protocol === 'https:';
2186
- } catch (e) { valid = false; }
2187
-
2188
- if (!valid) {
2189
- urlInput.style.borderColor = '#ff4444';
2190
- errMsg.style.display = 'block';
2191
- errVisible = true;
2192
- return;
2193
- }
2194
- urlInput.style.borderColor = '#3d3d5c';
2195
- errMsg.style.display = 'none';
2196
- errVisible = false;
2197
-
2198
- forkSubmitBtn.disabled = true;
2199
- forkSubmitBtn.textContent = '\u23F3 \u83B7\u53D6\u4E2D...';
2200
- forkSubmitBtn.style.background = '#3a3a5a';
2201
- forkSubmitBtn.style.cursor = 'default';
2160
+ // 提交逻辑(供两个按钮共用)
2161
+ function doFork(url, targetAgent, btn) {
2162
+ btn.disabled = true;
2163
+ btn.textContent = '⏳ 获取中...';
2164
+ btn.style.background = '#3a3a5a';
2165
+ btn.style.cursor = 'default';
2202
2166
  urlInput.disabled = true;
2203
2167
 
2168
+ var body = { url: url };
2169
+ if (targetAgent) body.targetAgent = targetAgent;
2170
+
2204
2171
  fetch(MYCLAW_API_BASE + '/api/fork', {
2205
2172
  method: 'POST',
2206
2173
  headers: { 'Content-Type': 'application/json' },
2207
- body: JSON.stringify({ url: url }),
2174
+ body: JSON.stringify(body),
2208
2175
  })
2209
2176
  .then(function (res) {
2210
2177
  if (!res.ok) throw new Error('HTTP ' + res.status);
@@ -2212,7 +2179,6 @@
2212
2179
  })
2213
2180
  .then(function (data) {
2214
2181
  if (!data.job_id) throw new Error(data.error || 'no job_id');
2215
- // 轮询 job 状态
2216
2182
  var jobId = data.job_id;
2217
2183
  var timer = setInterval(function () {
2218
2184
  fetch(MYCLAW_API_BASE + '/api/fork/status?job_id=' + encodeURIComponent(jobId))
@@ -2221,8 +2187,8 @@
2221
2187
  if (job.status === 'running') return;
2222
2188
  clearInterval(timer);
2223
2189
  if (job.status === 'done') {
2224
- forkSubmitBtn.textContent = '\u2705 \u83B7\u53D6\u6210\u529F\uFF01';
2225
- forkSubmitBtn.style.background = '#10b981';
2190
+ btn.textContent = ' 获取成功!';
2191
+ btn.style.background = '#10b981';
2226
2192
  setTimeout(function () {
2227
2193
  closeForkModal();
2228
2194
  showForkDoneToast(job.workspace, job.files);
@@ -2233,18 +2199,90 @@
2233
2199
  })
2234
2200
  .catch(function (err) {
2235
2201
  clearInterval(timer);
2236
- onForkError(err, forkSubmitBtn, urlInput);
2202
+ onForkError(err, btn, urlInput);
2237
2203
  });
2238
2204
  }, 1500);
2239
2205
  })
2240
2206
  .catch(function (err) {
2241
- onForkError(err, forkSubmitBtn, urlInput);
2207
+ onForkError(err, btn, urlInput);
2242
2208
  });
2209
+ }
2210
+
2211
+ function validateAndFork(targetAgent, btn) {
2212
+ var url = urlInput.value.trim();
2213
+ var valid = false;
2214
+ try {
2215
+ var parsed = new URL(url);
2216
+ valid = parsed.protocol === 'http:' || parsed.protocol === 'https:';
2217
+ } catch (e) { valid = false; }
2218
+
2219
+ if (!valid) {
2220
+ urlInput.style.borderColor = '#ff4444';
2221
+ errMsg.style.display = 'block';
2222
+ errVisible = true;
2223
+ return;
2224
+ }
2225
+ urlInput.style.borderColor = '#3d3d5c';
2226
+ errMsg.style.display = 'none';
2227
+ errVisible = false;
2228
+ doFork(url, targetAgent, btn);
2229
+ }
2230
+
2231
+ // 按钮行
2232
+ var btnRow = document.createElement('div');
2233
+ btnRow.style.cssText = 'display:flex;gap:10px;margin:0 20px 20px;';
2234
+
2235
+ // 左侧主按钮:确认获取(新建空间)
2236
+ var forkSubmitBtn = document.createElement('button');
2237
+ forkSubmitBtn.textContent = '确认获取';
2238
+ forkSubmitBtn.style.cssText = [
2239
+ 'flex: 2',
2240
+ 'padding: 10px',
2241
+ 'background: #4a4a7a',
2242
+ 'border: none',
2243
+ 'border-radius: 4px',
2244
+ 'color: #cdd6f4',
2245
+ 'font-size: 13px',
2246
+ 'font-family: monospace',
2247
+ 'cursor: pointer',
2248
+ 'transition: background 0.15s',
2249
+ ].join(';');
2250
+ forkSubmitBtn.onmouseenter = function () { if (!forkSubmitBtn.disabled) forkSubmitBtn.style.background = '#5a5a9a'; };
2251
+ forkSubmitBtn.onmouseleave = function () { if (!forkSubmitBtn.disabled) forkSubmitBtn.style.background = '#4a4a7a'; };
2252
+ forkSubmitBtn.onclick = function () { validateAndFork(null, forkSubmitBtn); };
2253
+
2254
+ // 右侧次按钮:获取到当前空间
2255
+ var forkCurrentBtn = document.createElement('button');
2256
+ forkCurrentBtn.textContent = '获取到当前空间';
2257
+ forkCurrentBtn.style.cssText = [
2258
+ 'flex: 1',
2259
+ 'padding: 10px',
2260
+ 'background: #2d2d4a',
2261
+ 'border: 1px solid #4a4a7a',
2262
+ 'border-radius: 4px',
2263
+ 'color: #aaa',
2264
+ 'font-size: 12px',
2265
+ 'font-family: monospace',
2266
+ 'cursor: pointer',
2267
+ 'transition: background 0.15s',
2268
+ ].join(';');
2269
+ forkCurrentBtn.onmouseenter = function () { if (!forkCurrentBtn.disabled) forkCurrentBtn.style.background = '#3a3a5a'; };
2270
+ forkCurrentBtn.onmouseleave = function () { if (!forkCurrentBtn.disabled) forkCurrentBtn.style.background = '#2d2d4a'; };
2271
+ forkCurrentBtn.onclick = function () {
2272
+ var currentAgent = getAgentName();
2273
+ if (!currentAgent) {
2274
+ alert('当前页面未检测到 agent,请确认 URL 中包含 session 或 agent 参数');
2275
+ return;
2276
+ }
2277
+ validateAndFork(currentAgent, forkCurrentBtn);
2243
2278
  };
2244
2279
 
2280
+ btnRow.appendChild(forkSubmitBtn);
2281
+ btnRow.appendChild(forkCurrentBtn);
2282
+
2245
2283
  box.appendChild(forkHeader);
2246
2284
  box.appendChild(forkForm);
2247
- box.appendChild(forkSubmitBtn);
2285
+ box.appendChild(btnRow);
2248
2286
  overlay.appendChild(box);
2249
2287
  document.body.appendChild(overlay);
2250
2288
  urlInput.focus();
package/gateway.js CHANGED
@@ -74,10 +74,12 @@ function stop() {
74
74
  }
75
75
 
76
76
  // 2b. 杀所有 openclaw 相关进程(防止僵尸进程)
77
+ // 注意:用精确匹配,避免误杀路径中含 "openclaw" 的无关进程(如 Antigravity 等)
77
78
  try {
78
79
  if (!isWin) {
79
80
  const myPid = process.pid;
80
- const procs = execSync('pgrep -f "openclaw" 2>/dev/null || true', {
81
+ // pgrep -x 只匹配进程名本身;pgrep -f 的精确模式匹配 "openclaw gateway" 子命令
82
+ const procs = execSync('{ pgrep -x openclaw; pgrep -f "openclaw gateway"; } 2>/dev/null | sort -u || true', {
81
83
  encoding: 'utf8', stdio: 'pipe', timeout: 5000
82
84
  }).trim();
83
85
  if (procs) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiyiran/myclaw",
3
- "version": "1.1.68",
3
+ "version": "1.1.70",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -624,40 +624,57 @@ def _download_from_cdn(key, local_path):
624
624
  return False
625
625
 
626
626
 
627
- def _do_fork(job_id, src_clawname, src_workspace, src_version, entry_rel_path=None):
628
- """在后台线程中执行 fork"""
627
+ def _do_fork(job_id, src_clawname, src_workspace, src_version, entry_rel_path=None, target_agent=None, claw_name=None):
628
+ """在后台线程中执行 fork
629
+ - target_agent 为 None 或对应目录不存在:新建 workspace
630
+ - target_agent 存在:将文件下载到该 workspace 下的子文件夹
631
+ - claw_name 指定目标 claw,由调用方负责路由;未传则使用本机默认路径
632
+ """
629
633
  try:
630
634
  now = datetime.now()
631
635
  time_id = now.strftime('%d') + 'D' + now.strftime('%M') + 'M'
632
636
 
633
- # create_agent.js 会自动加 "workspace-" 前缀,所以这里只传去掉前缀的名字
634
- # 例:src_workspace="workspace-fangshunhe" → agent_name="fangshunhe-v1-fork-15D30M"
635
- # mc new 创建目录 → workspace-fangshunhe-v1-fork-15D30M ✓
636
637
  base_name = src_workspace
637
638
  if base_name.startswith("workspace-"):
638
639
  base_name = base_name[len("workspace-"):]
639
- agent_name = f"{base_name}-v{src_version}-fork-{time_id}"
640
- new_workspace = f"workspace-{agent_name}"
640
+ folder_name = f"{base_name}-v{src_version}-fork-{time_id}"
641
641
 
642
- print(f"[Fork] 开始: {src_workspace} v{src_version} → {new_workspace}")
643
- if entry_rel_path:
644
- print(f"[Fork] 入口文件: {entry_rel_path}")
642
+ base_path = get_openclaw_path()
645
643
 
646
- # mc new 创建 workspace(不带首条消息,等文件下载完再发)
647
- mc_result = subprocess.run(
648
- ["mc", "new", agent_name],
649
- capture_output=True, text=True
650
- )
651
- if mc_result.returncode != 0:
652
- _fork_jobs[job_id] = {
653
- "status": "failed",
654
- "error": f"mc new 失败: {mc_result.stderr.strip()}"
655
- }
656
- return
644
+ # 判断是否使用已有 workspace
645
+ use_existing = False
646
+ if target_agent:
647
+ target_workspace = f"workspace-{target_agent}" if not target_agent.startswith("workspace-") else target_agent
648
+ target_workspace_dir = os.path.join(base_path, target_workspace)
649
+ if os.path.isdir(target_workspace_dir):
650
+ use_existing = True
651
+
652
+ if use_existing:
653
+ # 下载到已有 workspace 的子文件夹中
654
+ agent_name = target_agent if not target_agent.startswith("workspace-") else target_agent[len("workspace-"):]
655
+ workspace_name = target_workspace
656
+ target_dir = os.path.join(target_workspace_dir, folder_name)
657
+ print(f"[Fork] 使用已有空间: {workspace_name}, 子文件夹: {folder_name}")
658
+ else:
659
+ # 新建 workspace
660
+ agent_name = folder_name
661
+ workspace_name = f"workspace-{agent_name}"
662
+ target_dir = os.path.join(base_path, workspace_name)
663
+ print(f"[Fork] 新建空间: {workspace_name}")
664
+
665
+ mc_result = subprocess.run(
666
+ ["mc", "new", agent_name],
667
+ capture_output=True, text=True
668
+ )
669
+ if mc_result.returncode != 0:
670
+ _fork_jobs[job_id] = {
671
+ "status": "failed",
672
+ "error": f"mc new 失败: {mc_result.stderr.strip()}"
673
+ }
674
+ return
657
675
 
658
- base_path = get_openclaw_path()
659
- # create_agent.js: workspaceDir = openclawDir + "/workspace-" + agentId
660
- target_dir = os.path.join(base_path, new_workspace)
676
+ if entry_rel_path:
677
+ print(f"[Fork] 入口文件: {entry_rel_path}")
661
678
 
662
679
  # 列出源文件
663
680
  prefix = f"{FORK_CDN_PREFIX}/{src_clawname}/{src_workspace}/{src_version}/"
@@ -676,24 +693,26 @@ def _do_fork(job_id, src_clawname, src_workspace, src_version, entry_rel_path=No
676
693
  rel_path = unquote(key[len(prefix):])
677
694
  if not rel_path:
678
695
  continue
696
+ # 已有空间模式:rel_path 前加子文件夹前缀
697
+ record_rel = os.path.join(folder_name, rel_path) if use_existing else rel_path
679
698
  local_path = os.path.join(target_dir, rel_path)
680
699
  if _download_from_cdn(key, local_path):
681
700
  success += 1
682
- downloaded_rels.append(rel_path)
683
- print(f" ✓ {rel_path}")
701
+ downloaded_rels.append(record_rel)
702
+ print(f" ✓ {record_rel}")
684
703
 
685
704
  print(f"[Fork] 完成: 成功 {success}/{len(files)} 个文件")
686
705
 
687
706
  # 写入 JSON 记录,入口文件最后写,确保其 updated_at 最新排第一
688
- entry_decoded = unquote(entry_rel_path) if entry_rel_path else None
707
+ entry_decoded = os.path.join(folder_name, unquote(entry_rel_path)) if (use_existing and entry_rel_path) else (unquote(entry_rel_path) if entry_rel_path else None)
689
708
  for rel in downloaded_rels:
690
709
  if rel != entry_decoded:
691
- init_config(new_workspace, rel, method="add")
710
+ init_config(workspace_name, rel, method="add")
692
711
  if entry_decoded and entry_decoded in downloaded_rels:
693
- init_config(new_workspace, entry_decoded, method="add")
712
+ init_config(workspace_name, entry_decoded, method="add")
694
713
  print(f"[Fork] 入口文件已置顶: {entry_decoded}")
695
714
 
696
- # 文件到位后再发分析消息,确保 AI 能读到 fork 的内容
715
+ # 文件到位后再发分析消息
697
716
  fork_message = (
698
717
  "你好!请你仔细阅读我 workspace 下的所有文件,"
699
718
  "简单介绍一下这个项目有哪些资源、大概是做什么用的,"
@@ -711,7 +730,8 @@ def _do_fork(job_id, src_clawname, src_workspace, src_version, entry_rel_path=No
711
730
 
712
731
  _fork_jobs[job_id] = {
713
732
  "status": "done",
714
- "workspace": new_workspace,
733
+ "workspace": workspace_name,
734
+ "folder": folder_name if use_existing else None,
715
735
  "files": success,
716
736
  "total": len(files)
717
737
  }
@@ -721,10 +741,12 @@ def _do_fork(job_id, src_clawname, src_workspace, src_version, entry_rel_path=No
721
741
  _fork_jobs[job_id] = {"status": "failed", "error": str(e)}
722
742
 
723
743
 
724
- def start_fork(remote_url):
744
+ def start_fork(remote_url, target_agent=None, claw_name=None):
725
745
  """
726
746
  解析 remote_url,生成 job_id,在后台线程执行 fork。
727
747
  URL 格式: https://cdn.yiranlaoshi.com/myclaw/show/{clawname}/{workspace}/{version}/...
748
+ target_agent: 可选,指定已有 workspace 的 agent 名(不含 workspace- 前缀亦可)
749
+ claw_name: 可选,指定目标 claw,由调用方负责路由到对应机器/目录
728
750
  """
729
751
  parsed = urlparse(remote_url)
730
752
  # path: /myclaw/show/{clawname}/{workspace}/{version}/...
@@ -747,7 +769,7 @@ def start_fork(remote_url):
747
769
 
748
770
  t = threading.Thread(
749
771
  target=_do_fork,
750
- args=(job_id, src_clawname, src_workspace, src_version, entry_rel_path),
772
+ args=(job_id, src_clawname, src_workspace, src_version, entry_rel_path, target_agent, claw_name),
751
773
  daemon=True
752
774
  )
753
775
  t.start()
@@ -1030,11 +1052,13 @@ class MyclawAPIHandler(BaseHTTPRequestHandler):
1030
1052
  self._send_json({"ok": False, "error": str(e)}, 500)
1031
1053
 
1032
1054
  def _handle_fork(self):
1033
- """POST /api/fork Body: {"url": "https://cdn.yiranlaoshi.com/myclaw/show/..."}"""
1055
+ """POST /api/fork Body: {"url": "...", "targetAgent": "myspace"(可选), "clawName": "alice"(可选)}"""
1034
1056
  try:
1035
1057
  length = int(self.headers.get('Content-Length', 0))
1036
1058
  body = json.loads(self.rfile.read(length).decode('utf-8')) if length else {}
1037
1059
  remote_url = body.get('url', '')
1060
+ target_agent = body.get('targetAgent', None) or None
1061
+ claw_name = body.get('clawName', None) or None
1038
1062
  except Exception:
1039
1063
  self._send_json({"error": "请求体解析失败"}, 400)
1040
1064
  return
@@ -1043,7 +1067,7 @@ class MyclawAPIHandler(BaseHTTPRequestHandler):
1043
1067
  self._send_json({"error": "缺少 url 参数"}, 400)
1044
1068
  return
1045
1069
 
1046
- job_id, err = start_fork(remote_url)
1070
+ job_id, err = start_fork(remote_url, target_agent, claw_name)
1047
1071
  if err:
1048
1072
  self._send_json({"error": err}, 400)
1049
1073
  return