codebee 0.1.49 → 0.1.51

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/app/main.py CHANGED
@@ -20,7 +20,17 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
20
20
  from pathlib import Path
21
21
  from urllib.parse import parse_qs, unquote, urlparse
22
22
 
23
- from core import automation, catalog, flows, jobs, manager, market, market_remote, registry, remote, settings, store
23
+ # 测试主目录隔离(TUTTI_ISOLATE_HOME=1):必须先于 core.* 导入生效——
24
+ # modelhub/manager 在 import 期就绑定 ~ 路径。launch/运行前防线会直写
25
+ # ~/.claude 等真实 CLI 配置(2026-09-22 a.test 毒配置案),测试服务一律带上
26
+ # 本开关,绝不落真实用户目录。
27
+ if os.environ.get("TUTTI_ISOLATE_HOME", "").strip() == "1":
28
+ import tempfile as _tempfile
29
+ _fake_home = _tempfile.mkdtemp(prefix="tutti-home-")
30
+ os.environ["USERPROFILE"] = _fake_home
31
+ os.environ["HOME"] = _fake_home
32
+
33
+ from core import automation, catalog, flows, jobs, manager, market, market_remote, preview, registry, remote, settings, store
24
34
  from core import paths
25
35
  from core import health
26
36
  import pick_dialog
@@ -54,6 +64,10 @@ def _utf8_bytes(data):
54
64
  PORT = 8765 # main() 启动时更新;/api/connect 组装扫码地址用
55
65
  _BOOT_TS = time.time() # 服务进程启动时间戳:/api/pet_state 下发,蜜蜂据此识别「服务换人了」并让位
56
66
 
67
+ # SSE 空闲心跳间隔(秒):长 CLI 步骤期间状态零变化,注释行心跳保持中间层
68
+ # 不掐空闲流、半死连接尽早显形(EventSource 不派发注释事件,纯链路层探活)
69
+ SSE_PING_S = 15.0
70
+
57
71
  # 写接口统一限制 JSON 请求体,避免误传文件或异常客户端把 worker 线程和
58
72
  # 内存拖垮。16 MiB 足够覆盖任务上下文、故事圣经和附件清单(附件本体走
59
73
  # 独立上传接口)。
@@ -196,6 +210,10 @@ class Handler(BaseHTTPRequestHandler):
196
210
  # 静态页不设防(无敏感信息):远程裸地址打开时由前端令牌门引导输入
197
211
  if path in ("/", "/index.html"):
198
212
  return self._static("index.html")
213
+ # 运行预览:工作目录里网页成品的只读挂载,供 iframe/新窗口直接打开。
214
+ # 令牌走路径段(子资源继承不了查询串),鉴权复用 remote.request_authed。
215
+ if path == preview.BASE or path.startswith(preview.BASE + "/"):
216
+ return self._api_preview(path)
199
217
  if path.startswith("/api/"):
200
218
  if not self._authed():
201
219
  return self._json(401, {"error": "需要访问令牌(启动 CodeBee 时控制台会显示)"})
@@ -390,6 +408,10 @@ class Handler(BaseHTTPRequestHandler):
390
408
  files = []
391
409
  return self._json(200, {"workdir": wd, "files": files,
392
410
  "task_id": run.get("task_id") or ""})
411
+ m = re.match(r"^/api/runs/([^/]+)/preview$", path)
412
+ if m:
413
+ # 网页成品预览:入口 HTML + 同目录代码文件(前端渲染「预览」页签)
414
+ return self._json(200, preview.list_app(m.group(1), remote.token()))
393
415
  m = re.match(r"^/api/tasks/([^/]+)/side$", path)
394
416
  if m:
395
417
  # 任务检查器(右缘停靠列)专用:轻量聚合、可轮询,不带 diff 文本
@@ -1892,6 +1914,7 @@ class Handler(BaseHTTPRequestHandler):
1892
1914
  body = s.get("output") or ""
1893
1915
  if not body:
1894
1916
  body = s.get("summary") or ""
1917
+ running = (s.get("status") or "") == "running"
1895
1918
  items.append({
1896
1919
  "kind": "agent",
1897
1920
  "at": s.get("ended_at") or s.get("started_at") or "",
@@ -1904,6 +1927,13 @@ class Handler(BaseHTTPRequestHandler):
1904
1927
  "run": run_id_of_step,
1905
1928
  "log": s.get("log") or "",
1906
1929
  "followups": s.get("followups") or [],
1930
+ # 思考过程(2026-09-22 用户诉求):内置智能体流式抓的思维链随步骤
1931
+ # 落库,收尾保留;运行中另有 stream(正文实时预览)与 activity
1932
+ # (工具活动行)——前端边跑边打印,不再只有三点打字动画。
1933
+ "thinking": s.get("thinking") or "",
1934
+ "stream": (s.get("stream") or "") if running else "",
1935
+ "activity": (s.get("activity") or []) if running else [],
1936
+ "live": s.get("live") or 0,
1907
1937
  })
1908
1938
 
1909
1939
  for r in runs:
@@ -2058,30 +2088,49 @@ class Handler(BaseHTTPRequestHandler):
2058
2088
  def _api_events(self):
2059
2089
  """SSE 事件驱动:等 store 状态版本变化才构建/推送,空闲连接几乎零开销
2060
2090
  (此前每连接每 0.8s 盲构建全量 payload,多端并发会把 detect 的慢 IO
2061
- 放大成服务假死)。每 ~2s 醒一次顺带检查控制权变化。"""
2091
+ 放大成服务假死)。每 ~2s 醒一次;空闲超过 SSE_PING_S 发注释行心跳。"""
2062
2092
  self.send_response(200)
2063
2093
  self.send_header("Content-Type", "text/event-stream; charset=utf-8")
2064
2094
  self.send_header("Cache-Control", "no-store")
2065
2095
  self.send_header("X-Accel-Buffering", "no") # 防 nginx/隧道缓冲 SSE
2066
2096
  self.end_headers()
2097
+ try:
2098
+ # 半死连接(对端消失/网络黑洞)的写会无限挂起钉死线程;给足量
2099
+ # 超时让它必然显形成异常 → 线程退出 → 客户端断流重连自愈
2100
+ self.connection.settimeout(300)
2101
+ except Exception:
2102
+ pass
2067
2103
  cid = self._client_id()
2068
2104
  ver = store.state_version()
2069
2105
  sent_ctrl = None
2070
- n = 0
2106
+ last_send = 0.0
2071
2107
  try:
2108
+ # 连接即推一次当前全量状态:客户端重连(服务重启/网络闪断)后
2109
+ # 不必等下一次状态变化才知道真实进度。长 CLI 步骤一跑十来分钟
2110
+ # 期间一次 bump 都没有,等推送就是等个寂寞(2026-09-22 详情
2111
+ # 「完成」侧栏「在跑」实案)。此前靠 sent_ctrl=None 首轮必不等
2112
+ # 的巧合工作,这里改成显式行为。
2113
+ payload = json.dumps(_state_payload(cid, ver), ensure_ascii=False)
2114
+ self.wfile.write(("data: " + payload + "\n\n").encode("utf-8"))
2115
+ self.wfile.flush()
2116
+ sent_ctrl = remote.control_view(cid)
2117
+ last_send = time.time()
2072
2118
  while True:
2073
2119
  ver = store.wait_state_change(ver, 2.0)
2074
2120
  ctrl = remote.control_view(cid)
2075
2121
  if ver == store.state_version() and ctrl == sent_ctrl:
2122
+ # 空闲心跳:真发出去的字节让中间层不掐空闲流,也让半死
2123
+ # 连接在下一次写时尽早报错(注释行,EventSource 不派发事件)
2124
+ if time.time() - last_send >= SSE_PING_S:
2125
+ self.wfile.write(b": ping\n\n")
2126
+ self.wfile.flush()
2127
+ last_send = time.time()
2076
2128
  continue # 超时醒来且无变化
2077
2129
  payload = json.dumps(_state_payload(cid, ver), ensure_ascii=False)
2078
2130
  self.wfile.write(("data: " + payload + "\n\n").encode("utf-8"))
2079
2131
  self.wfile.flush()
2080
2132
  sent_ctrl = ctrl
2081
- n += 1
2082
- if n % 20 == 0: # ~40s 一次注释行:探活兼防中间层断开空闲连接
2083
- self.wfile.write(b": ping\n\n")
2084
- self.wfile.flush()
2133
+ last_send = time.time()
2085
2134
  except Exception:
2086
2135
  pass # 客户端断开是常态,线程随进程退出
2087
2136
 
@@ -2269,6 +2318,45 @@ class Handler(BaseHTTPRequestHandler):
2269
2318
  suffix = p.suffix.lower()
2270
2319
  self._send(200, p.read_bytes(), MIME.get(suffix, "application/octet-stream"))
2271
2320
 
2321
+ # ------------------------------------------------------------ 运行预览
2322
+ def _api_preview(self, path):
2323
+ """GET /preview/<run_id>/<令牌>/<相对路径>:工作目录网页成品的只读挂载。
2324
+
2325
+ 令牌放路径段而不是查询串:iframe 里的 style.css / script.js 是浏览器按
2326
+ 文档 URL 相对解析的,查询串不会被子资源继承(令牌会掉)。
2327
+ """
2328
+ rest = path[len(preview.BASE):].lstrip("/")
2329
+ parts = rest.split("/")
2330
+ if len(parts) < 2:
2331
+ return self._json(404, {"error": "not found"})
2332
+ run_id, tok = unquote(parts[0]), unquote(parts[1])
2333
+ rel = unquote("/".join(parts[2:]))
2334
+ # 令牌直接交给 remote.request_authed:与 /api/* 同一把尺子(本机豁免、
2335
+ # 代理下带转发头的 loopback 算远程),不另写一套鉴权
2336
+ ip, fw = self._forwarded_ip()
2337
+ if not remote.request_authed(ip, fw, tok, self.headers.get("X-CodeBee-Token") or ""):
2338
+ return self._json(401, {"error": "需要访问令牌(启动 CodeBee 时控制台会显示)"})
2339
+ if not rel or rel.endswith("/"):
2340
+ # 目录地址(/<令牌>/ 或 /<令牌>/web/):回落到该目录的入口页
2341
+ app = preview.list_app(run_id, tok)
2342
+ if not app.get("ok"):
2343
+ return self._json(404, {"error": "没有可预览的页面"})
2344
+ rel = app["entry"]
2345
+ if not self._preview_dir_ok(path, rel):
2346
+ return self._json(404, {"error": "not found"})
2347
+ data, ctype, err = preview.serve(run_id, rel, tok)
2348
+ if err:
2349
+ return self._json(404, {"error": err})
2350
+ return self._send(200, data, ctype)
2351
+
2352
+ def _preview_dir_ok(self, path, entry):
2353
+ """目录请求只允许回落到该目录自己的入口(/x/ 只解析 x/index.html),
2354
+ 免得 /sub/ 悄悄把根入口当子目录入口发出去。"""
2355
+ want = urlparse(path).path[len(preview.BASE):].strip("/").split("/")[2:]
2356
+ want = "/".join(unquote(p) for p in want if p)
2357
+ edir = entry.rsplit("/", 1)[0] if "/" in entry else ""
2358
+ return want == edir
2359
+
2272
2360
 
2273
2361
  def _state_payload(client_id="", ver=None):
2274
2362
  agents = registry.effective_agents(catalog.load(), manager.detect_all())