@aiyiran/myclaw 1.1.67 → 1.1.69

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiyiran/myclaw",
3
- "version": "1.1.67",
3
+ "version": "1.1.69",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -624,39 +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
- rand_id = str(random.randint(100, 999))
634
+ now = datetime.now()
635
+ time_id = now.strftime('%d') + 'D' + now.strftime('%M') + 'M'
631
636
 
632
- # create_agent.js 会自动加 "workspace-" 前缀,所以这里只传去掉前缀的名字
633
- # 例:src_workspace="workspace-fangshunhe" → agent_name="fangshunhe-v1-fork-475"
634
- # mc new 创建目录 → workspace-fangshunhe-v1-fork-475 ✓
635
637
  base_name = src_workspace
636
638
  if base_name.startswith("workspace-"):
637
639
  base_name = base_name[len("workspace-"):]
638
- agent_name = f"{base_name}-v{src_version}-fork-{rand_id}"
639
- new_workspace = f"workspace-{agent_name}"
640
+ folder_name = f"{base_name}-v{src_version}-fork-{time_id}"
640
641
 
641
- print(f"[Fork] 开始: {src_workspace} v{src_version} → {new_workspace}")
642
- if entry_rel_path:
643
- print(f"[Fork] 入口文件: {entry_rel_path}")
642
+ base_path = get_openclaw_path()
644
643
 
645
- # mc new 创建 workspace(不带首条消息,等文件下载完再发)
646
- mc_result = subprocess.run(
647
- ["mc", "new", agent_name],
648
- capture_output=True, text=True
649
- )
650
- if mc_result.returncode != 0:
651
- _fork_jobs[job_id] = {
652
- "status": "failed",
653
- "error": f"mc new 失败: {mc_result.stderr.strip()}"
654
- }
655
- 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
656
675
 
657
- base_path = get_openclaw_path()
658
- # create_agent.js: workspaceDir = openclawDir + "/workspace-" + agentId
659
- target_dir = os.path.join(base_path, new_workspace)
676
+ if entry_rel_path:
677
+ print(f"[Fork] 入口文件: {entry_rel_path}")
660
678
 
661
679
  # 列出源文件
662
680
  prefix = f"{FORK_CDN_PREFIX}/{src_clawname}/{src_workspace}/{src_version}/"
@@ -675,24 +693,26 @@ def _do_fork(job_id, src_clawname, src_workspace, src_version, entry_rel_path=No
675
693
  rel_path = unquote(key[len(prefix):])
676
694
  if not rel_path:
677
695
  continue
696
+ # 已有空间模式:rel_path 前加子文件夹前缀
697
+ record_rel = os.path.join(folder_name, rel_path) if use_existing else rel_path
678
698
  local_path = os.path.join(target_dir, rel_path)
679
699
  if _download_from_cdn(key, local_path):
680
700
  success += 1
681
- downloaded_rels.append(rel_path)
682
- print(f" ✓ {rel_path}")
701
+ downloaded_rels.append(record_rel)
702
+ print(f" ✓ {record_rel}")
683
703
 
684
704
  print(f"[Fork] 完成: 成功 {success}/{len(files)} 个文件")
685
705
 
686
706
  # 写入 JSON 记录,入口文件最后写,确保其 updated_at 最新排第一
687
- 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)
688
708
  for rel in downloaded_rels:
689
709
  if rel != entry_decoded:
690
- init_config(new_workspace, rel, method="add")
710
+ init_config(workspace_name, rel, method="add")
691
711
  if entry_decoded and entry_decoded in downloaded_rels:
692
- init_config(new_workspace, entry_decoded, method="add")
712
+ init_config(workspace_name, entry_decoded, method="add")
693
713
  print(f"[Fork] 入口文件已置顶: {entry_decoded}")
694
714
 
695
- # 文件到位后再发分析消息,确保 AI 能读到 fork 的内容
715
+ # 文件到位后再发分析消息
696
716
  fork_message = (
697
717
  "你好!请你仔细阅读我 workspace 下的所有文件,"
698
718
  "简单介绍一下这个项目有哪些资源、大概是做什么用的,"
@@ -710,7 +730,8 @@ def _do_fork(job_id, src_clawname, src_workspace, src_version, entry_rel_path=No
710
730
 
711
731
  _fork_jobs[job_id] = {
712
732
  "status": "done",
713
- "workspace": new_workspace,
733
+ "workspace": workspace_name,
734
+ "folder": folder_name if use_existing else None,
714
735
  "files": success,
715
736
  "total": len(files)
716
737
  }
@@ -720,10 +741,12 @@ def _do_fork(job_id, src_clawname, src_workspace, src_version, entry_rel_path=No
720
741
  _fork_jobs[job_id] = {"status": "failed", "error": str(e)}
721
742
 
722
743
 
723
- def start_fork(remote_url):
744
+ def start_fork(remote_url, target_agent=None, claw_name=None):
724
745
  """
725
746
  解析 remote_url,生成 job_id,在后台线程执行 fork。
726
747
  URL 格式: https://cdn.yiranlaoshi.com/myclaw/show/{clawname}/{workspace}/{version}/...
748
+ target_agent: 可选,指定已有 workspace 的 agent 名(不含 workspace- 前缀亦可)
749
+ claw_name: 可选,指定目标 claw,由调用方负责路由到对应机器/目录
727
750
  """
728
751
  parsed = urlparse(remote_url)
729
752
  # path: /myclaw/show/{clawname}/{workspace}/{version}/...
@@ -746,7 +769,7 @@ def start_fork(remote_url):
746
769
 
747
770
  t = threading.Thread(
748
771
  target=_do_fork,
749
- 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),
750
773
  daemon=True
751
774
  )
752
775
  t.start()
@@ -1029,11 +1052,13 @@ class MyclawAPIHandler(BaseHTTPRequestHandler):
1029
1052
  self._send_json({"ok": False, "error": str(e)}, 500)
1030
1053
 
1031
1054
  def _handle_fork(self):
1032
- """POST /api/fork Body: {"url": "https://cdn.yiranlaoshi.com/myclaw/show/..."}"""
1055
+ """POST /api/fork Body: {"url": "...", "targetAgent": "myspace"(可选), "clawName": "alice"(可选)}"""
1033
1056
  try:
1034
1057
  length = int(self.headers.get('Content-Length', 0))
1035
1058
  body = json.loads(self.rfile.read(length).decode('utf-8')) if length else {}
1036
1059
  remote_url = body.get('url', '')
1060
+ target_agent = body.get('targetAgent', None) or None
1061
+ claw_name = body.get('clawName', None) or None
1037
1062
  except Exception:
1038
1063
  self._send_json({"error": "请求体解析失败"}, 400)
1039
1064
  return
@@ -1042,7 +1067,7 @@ class MyclawAPIHandler(BaseHTTPRequestHandler):
1042
1067
  self._send_json({"error": "缺少 url 参数"}, 400)
1043
1068
  return
1044
1069
 
1045
- job_id, err = start_fork(remote_url)
1070
+ job_id, err = start_fork(remote_url, target_agent, claw_name)
1046
1071
  if err:
1047
1072
  self._send_json({"error": err}, 400)
1048
1073
  return