codebee 0.1.97 → 0.1.98
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 +6 -1
- package/README.md +4 -3
- package/app/core/jobs.py +8 -1
- package/app/core/manager.py +109 -11
- package/app/core/pipeline.py +23 -10
- package/app/core/runner.py +13 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,12 @@ README 元数据带回,供老版本在「发现新版本」时展示新版更
|
|
|
6
6
|
|
|
7
7
|
## 未发布
|
|
8
8
|
|
|
9
|
-
-
|
|
9
|
+
## v0.1.98(2026-10-09)
|
|
10
|
+
|
|
11
|
+
- **连载起草误拒修复(沙箱闸策略感知)**:0.1.97 起任务创建强制落默认执行策略,沙箱闸把非 codex 外部 CLI(claude-code/opencode/kimi-code 等)一律拒跑——89—96 批连载起草整批被拒死、连载停摆。现改为策略感知:默认策略(工作目录白名单+允许网络+无禁用)与外部 CLI 天然权限一致,放行;仅当任务带收窄策略(禁网/禁工具/环境变量白名单/目录白名单不含工作目录)时仍拒绝(fail-closed)。codex 分支语义原样保留
|
|
12
|
+
- **蜂巢「3D 场景」重做为真 3D 蜜蜂办公室(WebGL)**:此前的 3D 只是把平面卡片整体斜切(CSS 透视),现换成零依赖 WebGL2 引擎渲染的蜜蜂办公室——每个流水线阶段是一排工位,每个步骤是一只坐在桌前对着显示器的低多边形蜜蜂:运行中的屏幕亮蓝脉动+蜜蜂打字颤翅、完成绿屏、失败红屏+趴桌、排队黑屏打盹;蜂王戴皇冠坐讲台、始终面向当前在干活的工位,巡逻蜂平时绕场巡查、有格子跑起来就飞去悬停督工。左键拖动旋转/滚轮缩放/右键升降/双击空白复位,空闲 8 秒缓慢自转(尊重系统减弱动效设置);悬停工位出气泡卡(角色/模型/实时输出/思考行),点按直接开日志抽屉;换肤与明暗主题自动跟随;WebGL 不可用或上下文丢失自动回落 2D 列表并记忆视图偏好
|
|
13
|
+
- **uv 缺失自动补装**:管理页 uv 系安装命令遇到 uv 缺失,从「人话指引」升级为自动补装(Windows 先 winget 后 pip、Mac 先 brew 后官方脚本,全部免 sudo 非交互),补装成功直接接管安装;uv 全家桶命令挂国内镜像兜底
|
|
14
|
+
- **uv 系安装链 Mac 三防**:PATH 快照落点兜底(~/.local/bin 等)+ uv 缺失人话短路 + winget 头识别放宽
|
|
10
15
|
|
|
11
16
|
## v0.1.97(2026-10-09)
|
|
12
17
|
|
package/README.md
CHANGED
|
@@ -25,10 +25,11 @@ Trae Agent、OpenClaw……),提供
|
|
|
25
25
|
---
|
|
26
26
|
|
|
27
27
|
<!-- relnotes:start -->
|
|
28
|
-
### 最新版更新内容(v0.1.
|
|
28
|
+
### 最新版更新内容(v0.1.98)
|
|
29
29
|
|
|
30
|
-
-
|
|
31
|
-
-
|
|
30
|
+
- 修复连载/常规任务被沙箱闸误拒:非 codex 外部 CLI 默认策略放行,仅收窄策略仍拒绝(89—96 批连载起草误拒实案)
|
|
31
|
+
- 蜂巢 3D 场景重做为真 3D 蜜蜂办公室(WebGL):运行打字颤翅、失败趴桌、蜂王督工,悬停看实时输出
|
|
32
|
+
- uv 缺失自动补装(winget/pip/brew/curl 免 sudo),uv 系安装链 Mac PATH 兜底
|
|
32
33
|
<!-- relnotes:end -->
|
|
33
34
|
|
|
34
35
|
|
package/app/core/jobs.py
CHANGED
|
@@ -1013,9 +1013,15 @@ def _do_mgmt(job, ev):
|
|
|
1013
1013
|
# 修复智能体面对文件锁只会给出 taskkill 全杀 node 之类白名单必拒的危险
|
|
1014
1014
|
# 命令,白白烧一轮 300s 诊断(2026-09-18 dsh 同版本重装 EBUSY 案)
|
|
1015
1015
|
lock_hit = (not ok and op in ("install", "upgrade") and _file_lock_error(res))
|
|
1016
|
+
# uv 缺失同跳 AI 修复:修复链就算装上 uv,复检目标 CLI 仍未装,结论还是
|
|
1017
|
+
# 失败,白烧一轮;人话指路「先装 uv 再点安装」一步到位(2026-10-09 Mac
|
|
1018
|
+
# aider/trae 双装失败实案)
|
|
1019
|
+
uv_missing = bool(res.get("uv_missing"))
|
|
1016
1020
|
if lock_hit:
|
|
1017
1021
|
summary = ("失败:安装文件被占用(可能有同名程序在运行,或杀毒软件正在扫描),"
|
|
1018
1022
|
"请关闭占用该文件的程序后重试。完整输出见日志。")
|
|
1023
|
+
elif uv_missing:
|
|
1024
|
+
summary = "失败:" + str(res.get("error") or "")[:300]
|
|
1019
1025
|
else:
|
|
1020
1026
|
# note 优先:良性收口(如 winget「已装无可升级」复检在装)给用户
|
|
1021
1027
|
# 看原因,别让「完成」两个字孤零零不带解释
|
|
@@ -1026,7 +1032,7 @@ def _do_mgmt(job, ev):
|
|
|
1026
1032
|
summary=summary,
|
|
1027
1033
|
exit_code=res.get("exit_code"))
|
|
1028
1034
|
# AI 修复只针对安装类失败;卸载失败多为权限/程序占用,留给用户看日志处理
|
|
1029
|
-
if not ok and op in ("install", "upgrade") and not lock_hit:
|
|
1035
|
+
if not ok and op in ("install", "upgrade") and not lock_hit and not uv_missing:
|
|
1030
1036
|
ok = _ai_repair(run_id, entry, ev, entry.get(op), log_abs)
|
|
1031
1037
|
elif op == "smoke":
|
|
1032
1038
|
from . import runner as _r
|
|
@@ -1129,6 +1135,7 @@ def _ai_repair(run_id, entry, ev, failed_cmd, orig_log):
|
|
|
1129
1135
|
"npm: %s" % (shutil.which("npm") or "缺失"),
|
|
1130
1136
|
"pnpm: %s" % (shutil.which("pnpm") or "缺失"),
|
|
1131
1137
|
"python: %s" % (shutil.which("python3" if not is_win else "python") or "缺失"),
|
|
1138
|
+
"uv: %s" % (shutil.which("uv") or "缺失"),
|
|
1132
1139
|
]
|
|
1133
1140
|
if not is_win:
|
|
1134
1141
|
env_lines.append("brew: %s" % (shutil.which("brew") or "缺失"))
|
package/app/core/manager.py
CHANGED
|
@@ -91,19 +91,21 @@ def _safe_config_path(raw):
|
|
|
91
91
|
# ---------------------------------------------------------------- 检测
|
|
92
92
|
|
|
93
93
|
def _which_cli(cli):
|
|
94
|
-
"""PATH 找 CLI
|
|
95
|
-
|
|
96
|
-
winget 装的 CLI(如 Claude Code
|
|
97
|
-
%LOCALAPPDATA%\\Microsoft\\WinGet\\Packages\\<PkgId>_
|
|
98
|
-
|
|
99
|
-
落空 → 卡片「未安装」假象,点安装又被 winget
|
|
100
|
-
|
|
101
|
-
<cli>.exe
|
|
94
|
+
"""PATH 找 CLI;落空再按平台兜底磁盘已知安装位。
|
|
95
|
+
|
|
96
|
+
Windows 兜 winget 便携包两处标准落点:winget 装的 CLI(如 Claude Code)
|
|
97
|
+
把 exe 放进 %LOCALAPPDATA%\\Microsoft\\WinGet\\Packages\\<PkgId>_*\\,并把
|
|
98
|
+
该目录写进注册表用户 PATH——注册表更新后已在跑的进程(本服务)不继承,
|
|
99
|
+
shutil.which 落空 → 卡片「未安装」假象,点安装又被 winget「已装无可升级」
|
|
100
|
+
非零退出拒掉(2026-09-30 实案)。Links 是便携包符号链接标准目录,Packages
|
|
101
|
+
按 <cli>.exe 通配兜另一形态。POSIX 兜 runner 同款三落点(~/.local/bin 与
|
|
102
|
+
brew bin,Mac 服务吃不到 shell rc 里的 PATH 时同样假「未安装」)。
|
|
103
|
+
两条都只兜 which 落空的场景,零额外开销。"""
|
|
102
104
|
path = shutil.which(cli)
|
|
103
105
|
if path:
|
|
104
106
|
return path
|
|
105
107
|
if os.name != "nt":
|
|
106
|
-
return
|
|
108
|
+
return runner._fallback_probe_cli(cli)
|
|
107
109
|
local = os.environ.get("LOCALAPPDATA")
|
|
108
110
|
if not local:
|
|
109
111
|
return None
|
|
@@ -1974,7 +1976,10 @@ def _winget_already_ok(cmd, op, res):
|
|
|
1974
1976
|
还要复检 installed 才按成功收口。"""
|
|
1975
1977
|
if res.get("ok") or op not in ("install", "upgrade"):
|
|
1976
1978
|
return False
|
|
1977
|
-
|
|
1979
|
+
# 首 token 可能已被 _resolve_mgmt_tool 换成绝对路径(PATH 快照落空兜底),
|
|
1980
|
+
# 所以按 basename 认 winget,别用 startswith
|
|
1981
|
+
head = str(cmd or "").strip().split()[0] if str(cmd or "").strip() else ""
|
|
1982
|
+
if os.path.basename(head).lower().rstrip(".exe") != "winget":
|
|
1978
1983
|
return False
|
|
1979
1984
|
code = res.get("exit_code")
|
|
1980
1985
|
try:
|
|
@@ -1986,6 +1991,85 @@ def _winget_already_ok(cmd, op, res):
|
|
|
1986
1991
|
return any(m in out for m in _WINGET_NOOP_MARKERS)
|
|
1987
1992
|
|
|
1988
1993
|
|
|
1994
|
+
def _uv_install_hint():
|
|
1995
|
+
if sys.platform == "darwin":
|
|
1996
|
+
return "brew install uv(或官网 curl -LsSf https://astral.sh/uv/install.sh | sh)"
|
|
1997
|
+
return "winget install astral-sh.uv,或 pip install uv"
|
|
1998
|
+
|
|
1999
|
+
|
|
2000
|
+
def _uv_mirror_env():
|
|
2001
|
+
"""uv 系命令的镜像兜底 env(用户没自己配才补默认,绝不覆盖)。
|
|
2002
|
+
|
|
2003
|
+
uv tool install --python 3.12 会现场下载 CPython 独立构建(GitHub 直连,
|
|
2004
|
+
国内常断流),包体走 PyPI——npmmirror/清华这对镜像是 2026-09 本机 uv
|
|
2005
|
+
实装验证过的组合。"""
|
|
2006
|
+
env = {}
|
|
2007
|
+
if not os.environ.get("UV_PYTHON_INSTALL_MIRROR"):
|
|
2008
|
+
env["UV_PYTHON_INSTALL_MIRROR"] = (
|
|
2009
|
+
"https://registry.npmmirror.com/-/binary/python-build-standalone")
|
|
2010
|
+
if not os.environ.get("UV_DEFAULT_INDEX"):
|
|
2011
|
+
env["UV_DEFAULT_INDEX"] = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
2012
|
+
return env
|
|
2013
|
+
|
|
2014
|
+
|
|
2015
|
+
def _cmd_head_is_uv(cmd):
|
|
2016
|
+
stripped = str(cmd or "").strip()
|
|
2017
|
+
if not stripped:
|
|
2018
|
+
return False
|
|
2019
|
+
return os.path.basename(stripped.split()[0]).lower() in ("uv", "uv.exe")
|
|
2020
|
+
|
|
2021
|
+
|
|
2022
|
+
def _bootstrap_uv(cancel_event=None, log_path=None):
|
|
2023
|
+
"""uv 不在本机时自动补装一个——安装按钮「点了就该装上」的契约兜底。
|
|
2024
|
+
|
|
2025
|
+
按平台选渠道,全部免 sudo 非交互:POSIX 先 brew(在才用)后官方 curl
|
|
2026
|
+
脚本(落 ~/.local/bin,探测表可寻址);Windows 先 winget 后 pip。装完
|
|
2027
|
+
不指望 PATH 快照自愈——调用方重跑 _resolve_mgmt_tool 用绝对路径接管。
|
|
2028
|
+
返回 {ok, detail};失败 detail 列出试过的渠道与结局,供人话收口。"""
|
|
2029
|
+
candidates = []
|
|
2030
|
+
if sys.platform == "win32":
|
|
2031
|
+
if shutil.which("winget"):
|
|
2032
|
+
candidates.append(("winget", "winget install -e --id astral-sh.uv --silent"))
|
|
2033
|
+
if shutil.which("py"):
|
|
2034
|
+
candidates.append(("pip(py)", "py -3 -m pip install uv"))
|
|
2035
|
+
elif shutil.which("python"):
|
|
2036
|
+
candidates.append(("pip", "python -m pip install uv"))
|
|
2037
|
+
else:
|
|
2038
|
+
if shutil.which("brew"):
|
|
2039
|
+
candidates.append(("brew", "brew install uv"))
|
|
2040
|
+
if shutil.which("curl"):
|
|
2041
|
+
candidates.append(("curl", "curl -LsSf https://astral.sh/uv/install.sh | sh"))
|
|
2042
|
+
attempts = []
|
|
2043
|
+
for label, cmd in candidates:
|
|
2044
|
+
res = runner.run_process(shell_cmd=cmd, cwd=str(paths.ROOT),
|
|
2045
|
+
timeout=900, cancel_event=cancel_event,
|
|
2046
|
+
log_path=log_path)
|
|
2047
|
+
if res["ok"]:
|
|
2048
|
+
return {"ok": True, "detail": "已自动补装 uv(%s)" % label}
|
|
2049
|
+
attempts.append("%s 退出码 %s" % (label, res.get("exit_code")))
|
|
2050
|
+
return {"ok": False,
|
|
2051
|
+
"detail": "、".join(attempts) or "本机没有 brew/curl/winget/pip 任一渠道"}
|
|
2052
|
+
|
|
2053
|
+
|
|
2054
|
+
def _resolve_mgmt_tool(cmd, op):
|
|
2055
|
+
"""install/upgrade 命令首 token 的工具本体解析。
|
|
2056
|
+
|
|
2057
|
+
服务 PATH 是启动快照,本机后装的 uv/npm 不在快照里时 shell 报
|
|
2058
|
+
command not found 退出码 127,零信息量。磁盘已知落点能探到就把首 token
|
|
2059
|
+
原地换成绝对路径(装完即用,不必重启服务);探不到且是 uv 系安装命令
|
|
2060
|
+
则返回 uv_missing=True——本机连 uv 都没有,包无从装起,人话报错并跳过
|
|
2061
|
+
AI 修复(修复链装上 uv 后复检目标 CLI 仍未装,白烧一轮诊断)。
|
|
2062
|
+
返回 (替换后的命令, uv_missing)。"""
|
|
2063
|
+
stripped = str(cmd or "").strip()
|
|
2064
|
+
first = stripped.split()[0] if stripped else ""
|
|
2065
|
+
if not first or os.path.isabs(first) or shutil.which(first):
|
|
2066
|
+
return cmd, False
|
|
2067
|
+
found = runner._fallback_probe_cli(first)
|
|
2068
|
+
if found:
|
|
2069
|
+
return found + stripped[len(first):], False
|
|
2070
|
+
return cmd, (first == "uv" and op in ("install", "upgrade"))
|
|
2071
|
+
|
|
2072
|
+
|
|
1989
2073
|
def run_mgmt_command(entry, op, cancel_event=None, log_path=None):
|
|
1990
2074
|
"""执行 install/upgrade/uninstall 命令(由任务执行器异步运行,日志实时落盘)。"""
|
|
1991
2075
|
if op == "uninstall":
|
|
@@ -2000,8 +2084,22 @@ def run_mgmt_command(entry, op, cancel_event=None, log_path=None):
|
|
|
2000
2084
|
return {"ok": False,
|
|
2001
2085
|
"error": "未配置 %s 命令:请在 data/catalog.json 的 \"%s\" 里补充,或用官方渠道安装"
|
|
2002
2086
|
% (op, entry["id"])}
|
|
2087
|
+
cmd, uv_missing = _resolve_mgmt_tool(cmd, op)
|
|
2088
|
+
if uv_missing:
|
|
2089
|
+
# 「提供安装就该能装上」:uv 不在本机先自动补装,装 uv 不该是用户
|
|
2090
|
+
# 操心的事;真补不上才退回人话指引(2026-10-09 Mac 双装失败实案)
|
|
2091
|
+
boot = _bootstrap_uv(cancel_event=cancel_event, log_path=log_path)
|
|
2092
|
+
if boot["ok"]:
|
|
2093
|
+
cmd, uv_missing = _resolve_mgmt_tool(cmd, op)
|
|
2094
|
+
if uv_missing:
|
|
2095
|
+
return {"ok": False, "uv_missing": True,
|
|
2096
|
+
"error": "自动安装 uv 未成功(%s)。请手动安装:%s;"
|
|
2097
|
+
"装完重启 CodeBee,再点一次安装。"
|
|
2098
|
+
% (boot["detail"], _uv_install_hint())}
|
|
2099
|
+
# uv 全家桶命令挂镜像兜底:Python 独立构建/包索引国内直连常断(只补缺不覆盖)
|
|
2003
2100
|
res = runner.run_process(shell_cmd=cmd, cwd=str(paths.ROOT),
|
|
2004
|
-
timeout=1800, cancel_event=cancel_event, log_path=log_path
|
|
2101
|
+
timeout=1800, cancel_event=cancel_event, log_path=log_path,
|
|
2102
|
+
env=_uv_mirror_env() if _cmd_head_is_uv(cmd) else None)
|
|
2005
2103
|
winget_noop = _winget_already_ok(cmd, op, res)
|
|
2006
2104
|
detect_all(force=True)
|
|
2007
2105
|
with _LOCK:
|
package/app/core/pipeline.py
CHANGED
|
@@ -574,25 +574,38 @@ def _external_sandbox_block_reason(agent, workdir, sandbox):
|
|
|
574
574
|
"""Reject task policies that this CLI launcher cannot enforce.
|
|
575
575
|
|
|
576
576
|
Codex is currently the only supported external runner with an OS-backed
|
|
577
|
-
workspace write sandbox. Other CLI integrations
|
|
578
|
-
|
|
577
|
+
workspace write sandbox. Other CLI integrations run with their natural
|
|
578
|
+
permission set (workdir read/write, network allowed): they are rejected
|
|
579
|
+
only when the task policy is *narrower* than that — fail closed for
|
|
580
|
+
narrowed policies, not for defaults(实案:create_task 改为强制落默认
|
|
581
|
+
策略后,本闸把 89—96 批的连载起草整批拒成「没有经验证的 OS 文件隔
|
|
582
|
+
离」;默认策略与外部 CLI 的天然权限一致,不属于收窄).
|
|
579
583
|
"""
|
|
580
584
|
if (agent or {}).get("mode") == "mock":
|
|
581
585
|
return ""
|
|
582
586
|
from . import policy
|
|
583
|
-
if (agent or {}).get("kind") != "codex":
|
|
584
|
-
return "沙箱拒绝:该外部 CLI 没有经验证的 OS 文件隔离;请改用内置 Agent"
|
|
585
587
|
root = os.path.realpath(workdir or ".")
|
|
586
588
|
normalized = policy.normalize_sandbox(sandbox, root)
|
|
587
|
-
|
|
589
|
+
if (agent or {}).get("kind") == "codex":
|
|
590
|
+
if normalized.get("network") is False:
|
|
591
|
+
return "沙箱拒绝:该 CLI 后端未接入网络隔离,拒绝运行网络受限任务"
|
|
592
|
+
if [os.path.realpath(x) for x in normalized.get("allowed_roots") or []] != [root]:
|
|
593
|
+
return "沙箱拒绝:该 CLI 后端未接入允许目录隔离,拒绝运行目录受限任务"
|
|
594
|
+
if normalized.get("disabled_tools"):
|
|
595
|
+
return "权限拒绝:该 CLI 后端无法执行 CodeBee 工具禁用策略,拒绝运行"
|
|
596
|
+
if normalized.get("env_allowlist"):
|
|
597
|
+
return "沙箱拒绝:该 CLI 后端未接入环境变量白名单,拒绝运行受限任务"
|
|
598
|
+
return ""
|
|
599
|
+
# 非 codex 外部 CLI:默认策略放行,收窄策略拒绝
|
|
588
600
|
if normalized.get("network") is False:
|
|
589
|
-
return "
|
|
590
|
-
if roots != [root]:
|
|
591
|
-
return "沙箱拒绝:该 CLI 后端未接入允许目录隔离,拒绝运行目录受限任务"
|
|
601
|
+
return "沙箱拒绝:该任务要求禁网,外部 CLI 无法执行网络受限任务"
|
|
592
602
|
if normalized.get("disabled_tools"):
|
|
593
|
-
return "
|
|
603
|
+
return "权限拒绝:该任务带工具禁用策略,外部 CLI 无法执行"
|
|
594
604
|
if normalized.get("env_allowlist"):
|
|
595
|
-
return "
|
|
605
|
+
return "沙箱拒绝:该任务带环境变量白名单,外部 CLI 无法执行受限任务"
|
|
606
|
+
roots = [os.path.realpath(x) for x in normalized.get("allowed_roots") or []]
|
|
607
|
+
if roots and root not in roots:
|
|
608
|
+
return "沙箱拒绝:该任务目录白名单不含工作目录,外部 CLI 无法保证隔离范围"
|
|
596
609
|
return ""
|
|
597
610
|
|
|
598
611
|
|
package/app/core/runner.py
CHANGED
|
@@ -73,18 +73,27 @@ _CLI_EXTS = (".exe", ".cmd", ".bat")
|
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
def _fallback_probe_cli(name):
|
|
76
|
-
"""which() 落空时按常见安装位兜底找 CLI
|
|
76
|
+
"""which() 落空时按常见安装位兜底找 CLI,返回全路径或 None。
|
|
77
77
|
|
|
78
78
|
服务进程的 PATH 是启动那一刻的快照:CLI 在服务起跑之后才安装/迁移(如
|
|
79
79
|
2026-09-30 claude 转 winget 原生安装)时,快照里没有新目录,which 解析
|
|
80
80
|
不到、裸名直传 Popen 直接 FileNotFoundError,直到重启服务才自愈。这里
|
|
81
81
|
只探测磁盘已知落点,不改 PATH、不缓存(装没装是低频事件,探几次不亏)。
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return None
|
|
82
|
+
Mac 同病:curl 脚本装的 uv/aider 落 ~/.local/bin,GUI 启动的服务常吃不到
|
|
83
|
+
这条 PATH;brew 在 Apple Silicon 落 /opt/homebrew/bin。"""
|
|
85
84
|
name = str(name)
|
|
86
85
|
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", name):
|
|
87
86
|
return None # 带斜杠/参数形态的不是裸命令名,交给原路径逻辑
|
|
87
|
+
if os.name != "nt":
|
|
88
|
+
for d in (os.path.join(os.path.expanduser("~"), ".local", "bin"),
|
|
89
|
+
"/opt/homebrew/bin", "/usr/local/bin"):
|
|
90
|
+
p = os.path.join(d, name)
|
|
91
|
+
try:
|
|
92
|
+
if os.path.isfile(p) and os.access(p, os.X_OK):
|
|
93
|
+
return p
|
|
94
|
+
except OSError:
|
|
95
|
+
pass
|
|
96
|
+
return None
|
|
88
97
|
local = os.environ.get("LOCALAPPDATA") or ""
|
|
89
98
|
home = os.path.expanduser("~")
|
|
90
99
|
appdata = os.environ.get("APPDATA") or os.path.join(home, "AppData", "Roaming")
|
package/package.json
CHANGED