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/CHANGELOG.md +31 -0
- package/README.md +4 -150
- package/app/core/aiflavor.py +14 -0
- package/app/core/builtin_agent.py +567 -74
- package/app/core/jobs.py +170 -42
- package/app/core/knowledge.py +432 -405
- package/app/core/manager.py +100 -10
- package/app/core/market.py +27 -1
- package/app/core/modelhub.py +53 -5
- package/app/core/pipeline.py +193 -77
- package/app/core/planner.py +14 -5
- package/app/core/preview.py +293 -0
- package/app/core/router.py +189 -104
- package/app/core/runner.py +370 -144
- package/app/core/settings.py +11 -4
- package/app/core/skills.py +36 -12
- package/app/core/store.py +74 -2
- package/app/core/zentao.py +189 -30
- package/app/main.py +95 -7
- package/app/ui/app.js +13256 -12876
- package/app/ui/i18n.js +97 -34
- package/app/ui/index.html +55 -12
- package/app/ui/style.css +217 -26
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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())
|