codebee 0.1.97 → 0.1.99
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 +12 -2
- package/README.md +5 -3
- package/app/core/builtin_agent.py +36 -37
- package/app/core/jobs.py +8 -1
- package/app/core/manager.py +109 -11
- package/app/core/modelhub.py +21 -5
- package/app/core/pipeline.py +29 -14
- package/app/core/publish/auto.py +91 -20
- package/app/core/publish/fanqie.py +30 -0
- package/app/core/publish/flows-fanqie-calibrated.json +81 -0
- package/app/core/publish/flows-qimao-calibrated.json +58 -0
- package/app/core/publish/ledger.py +24 -1
- package/app/core/publish/manager.py +36 -9
- package/app/core/publish/qimao.py +17 -0
- package/app/core/quality_gate.py +7 -0
- package/app/core/runner.py +13 -4
- package/app/main.py +5 -2
- package/app/ui/app.js +86 -14
- package/app/ui/board.css +5 -1
- package/app/ui/board.js +16 -0
- package/app/ui/i18n.js +26 -1
- package/app/ui/index.html +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,19 @@ CodeBee 的用户可感知变更记录。发布新版时:最上面加一节,
|
|
|
4
4
|
`<!-- relnotes:start -->…<!-- relnotes:end -->` 段(那段会被 `npm view` 的
|
|
5
5
|
README 元数据带回,供老版本在「发现新版本」时展示新版更新内容)。
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## v0.1.99(2026-10-09)
|
|
8
8
|
|
|
9
|
-
-
|
|
9
|
+
- **直连对话「本轮失败 NameError」修复**:内置智能体(CodeBee 直连)每轮起跑数秒即失败,根因是沙箱加固提交把 `sandbox=task.get("sandbox")` 误引入无 task 上下文的内置步骤函数——实参求值即 NameError,Mac/Windows 全平台直连对话不可用。现改为显式传参,任务沙箱策略照常透传生效(回归测试锁定形参与全链)
|
|
10
|
+
- **命令执行沙箱闸策略感知**:沙箱加固曾把内置智能体的 run_command 在 Windows/macOS 一律拒跑(「未接入文件系统隔离后端」)——无后端平台上命令工具全死。现与外部 CLI 闸同口径:默认宽策略照常全信任直跑(2026-09-22 拍板档位),进程内无法兑现的收窄仍拒(禁网需 OS 后端;环境变量白名单在子进程侧实际滤不掉,宁拒不假装);Linux bubblewrap 全策略强制不变
|
|
11
|
+
- **大屏 KPI 长数值不再裁字**:万位级数值(如「2246.7万」)在 KPI 卡被挤到第二行、遭 overflow 裁切只剩半截;现 KPI 数字禁止换行,并按内容长度三档自动降字号,长数值单行完整显示
|
|
12
|
+
- **经验库设置页文案对齐**:注入范围说明「小说类任务」改「同类任务」,与知识注入按任务类型全量覆盖的实现口径一致
|
|
13
|
+
|
|
14
|
+
## v0.1.98(2026-10-09)
|
|
15
|
+
|
|
16
|
+
- **连载起草误拒修复(沙箱闸策略感知)**:0.1.97 起任务创建强制落默认执行策略,沙箱闸把非 codex 外部 CLI(claude-code/opencode/kimi-code 等)一律拒跑——89—96 批连载起草整批被拒死、连载停摆。现改为策略感知:默认策略(工作目录白名单+允许网络+无禁用)与外部 CLI 天然权限一致,放行;仅当任务带收窄策略(禁网/禁工具/环境变量白名单/目录白名单不含工作目录)时仍拒绝(fail-closed)。codex 分支语义原样保留
|
|
17
|
+
- **蜂巢「3D 场景」重做为真 3D 蜜蜂办公室(WebGL)**:此前的 3D 只是把平面卡片整体斜切(CSS 透视),现换成零依赖 WebGL2 引擎渲染的蜜蜂办公室——每个流水线阶段是一排工位,每个步骤是一只坐在桌前对着显示器的低多边形蜜蜂:运行中的屏幕亮蓝脉动+蜜蜂打字颤翅、完成绿屏、失败红屏+趴桌、排队黑屏打盹;蜂王戴皇冠坐讲台、始终面向当前在干活的工位,巡逻蜂平时绕场巡查、有格子跑起来就飞去悬停督工。左键拖动旋转/滚轮缩放/右键升降/双击空白复位,空闲 8 秒缓慢自转(尊重系统减弱动效设置);悬停工位出气泡卡(角色/模型/实时输出/思考行),点按直接开日志抽屉;换肤与明暗主题自动跟随;WebGL 不可用或上下文丢失自动回落 2D 列表并记忆视图偏好
|
|
18
|
+
- **uv 缺失自动补装**:管理页 uv 系安装命令遇到 uv 缺失,从「人话指引」升级为自动补装(Windows 先 winget 后 pip、Mac 先 brew 后官方脚本,全部免 sudo 非交互),补装成功直接接管安装;uv 全家桶命令挂国内镜像兜底
|
|
19
|
+
- **uv 系安装链 Mac 三防**:PATH 快照落点兜底(~/.local/bin 等)+ uv 缺失人话短路 + winget 头识别放宽
|
|
10
20
|
|
|
11
21
|
## v0.1.97(2026-10-09)
|
|
12
22
|
|
package/README.md
CHANGED
|
@@ -25,10 +25,12 @@ Trae Agent、OpenClaw……),提供
|
|
|
25
25
|
---
|
|
26
26
|
|
|
27
27
|
<!-- relnotes:start -->
|
|
28
|
-
### 最新版更新内容(v0.1.
|
|
28
|
+
### 最新版更新内容(v0.1.99)
|
|
29
29
|
|
|
30
|
-
-
|
|
31
|
-
-
|
|
30
|
+
- 修复直连对话每轮起跑即「本轮失败 NameError」:Mac/Windows 全平台直连对话恢复可用
|
|
31
|
+
- 内置智能体命令执行沙箱闸策略感知:默认策略照常直跑,仅收窄策略仍拒绝
|
|
32
|
+
- 大屏 KPI 长数值不再裁字:禁换行 + 按内容长度三档降字号
|
|
33
|
+
- 经验库设置页注入范围文案与全类型实现口径对齐
|
|
32
34
|
<!-- relnotes:end -->
|
|
33
35
|
|
|
34
36
|
|
|
@@ -98,15 +98,15 @@ def resolve(provider_id="", model="", difficulty="default"):
|
|
|
98
98
|
if selected not in enabled_names:
|
|
99
99
|
return None
|
|
100
100
|
if not selected:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
p.get("model") or "").strip()
|
|
104
|
-
if not selected:
|
|
101
|
+
# 默认/难度模型键位与 models[] 启停开关不联动:键位指向已停用
|
|
102
|
+
# 模型时对账启用名单回落,不冒充可用(停用的模型不准用)
|
|
105
103
|
try:
|
|
106
|
-
|
|
104
|
+
selected = modelhub._usable_default_model(
|
|
105
|
+
p, (p.get("model_hard") if difficulty == "hard" else
|
|
106
|
+
p.get("model_easy") if difficulty == "easy" else
|
|
107
|
+
p.get("model")) or "")
|
|
107
108
|
except Exception:
|
|
108
|
-
|
|
109
|
-
selected = names[0]["name"] if names else ""
|
|
109
|
+
selected = (p.get("model") or "").strip()
|
|
110
110
|
if selected:
|
|
111
111
|
return {"prov": p, "model": selected,
|
|
112
112
|
"provider_id": p.get("id") or "",
|
|
@@ -125,15 +125,13 @@ def resolve(provider_id="", model="", difficulty="default"):
|
|
|
125
125
|
for p in modelhub.providers():
|
|
126
126
|
if not p.get("enabled", True) or not p.get("api_key"):
|
|
127
127
|
continue
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
names = []
|
|
136
|
-
m = names[0]["name"] if names else ""
|
|
128
|
+
try:
|
|
129
|
+
m = modelhub._usable_default_model(
|
|
130
|
+
p, (p.get("model_hard") if difficulty == "hard" else
|
|
131
|
+
p.get("model_easy") if difficulty == "easy" else
|
|
132
|
+
p.get("model")) or "")
|
|
133
|
+
except Exception:
|
|
134
|
+
m = (p.get("model") or "").strip()
|
|
137
135
|
if m:
|
|
138
136
|
return {"prov": p, "model": m, "provider_id": p.get("id") or "",
|
|
139
137
|
"provider_name": p.get("name") or p.get("id") or ""}
|
|
@@ -422,7 +420,11 @@ def _tool_run_command(workdir, args, cancel_event=None, deadline=None, sandbox=N
|
|
|
422
420
|
→ cmd /c 或 /bin/sh -c),仓库唯一的 shell 落点,不另起炉灶。全信任档
|
|
423
421
|
(用户拍板 2026-09-22):无白名单无确认闸,与 codex/claude 全权沙箱同档。
|
|
424
422
|
提权靠模型自己包 Start-Process -Verb RunAs——UAC 必须人点,绕不过也不该绕。
|
|
425
|
-
输出按字符头尾截断(解码已在 run_process 完成,无残缺多字节问题)。
|
|
423
|
+
输出按字符头尾截断(解码已在 run_process 完成,无残缺多字节问题)。
|
|
424
|
+
沙箱闸策略感知(与外部 CLI 闸 b267107 同口径):Linux 有 bwrap 全策略
|
|
425
|
+
强制;Win/Mac 无 OS 后端,进程内无法兑现的收窄仍拒(禁网、env 白名单
|
|
426
|
+
——run_process 的 env 是注入语义滤不掉),默认宽策略保持全信任档直跑,
|
|
427
|
+
目录闸由 path_allowed 兑现。"""
|
|
426
428
|
cmdline = str(args.get("command") or "").strip()
|
|
427
429
|
if not cmdline:
|
|
428
430
|
return "(命令为空,未执行)"
|
|
@@ -432,15 +434,20 @@ def _tool_run_command(workdir, args, cancel_event=None, deadline=None, sandbox=N
|
|
|
432
434
|
cwd = os.path.abspath(workdir or ".")
|
|
433
435
|
if not policy.path_allowed(cwd, sandbox):
|
|
434
436
|
return "(沙箱拒绝:工作目录不在允许范围内,命令未执行)"
|
|
435
|
-
#
|
|
436
|
-
#
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
#
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
437
|
+
# Enforce the declared boundary at the process boundary where an OS
|
|
438
|
+
# backend exists (bubblewrap on Linux). Windows/macOS have no equivalent:
|
|
439
|
+
# stay policy-aware like the external-CLI gate (b267107) — the narrowings
|
|
440
|
+
# this in-process runner cannot honor fail closed, while a default wide
|
|
441
|
+
# policy keeps the user-approved full-trust tier (2026-09-22) instead of
|
|
442
|
+
# refusing every command.
|
|
443
|
+
has_bwrap = os.name == "posix" and bool(shutil.which("bwrap"))
|
|
444
|
+
if sandbox.get("network") is False and not has_bwrap:
|
|
445
|
+
return "(沙箱拒绝:当前系统没有可用的网络隔离后端,命令未执行)"
|
|
446
|
+
if sandbox.get("env_allowlist"):
|
|
447
|
+
# run_process 的 env 是注入语义(合并进完整父环境,供凭据注入),
|
|
448
|
+
# 不是白名单替换——环境收窄在子进程侧无法兑现,宁拒不假装
|
|
449
|
+
return "(沙箱拒绝:该任务带环境变量白名单,命令执行无法兑现该策略,未执行)"
|
|
450
|
+
if has_bwrap:
|
|
444
451
|
argv = ["bwrap", "--die-with-parent", "--new-session", "--tmpfs", "/"]
|
|
445
452
|
# Mount only the runtime needed by ordinary commands. Do not expose the
|
|
446
453
|
# entire host root read-only: read access can still leak user secrets.
|
|
@@ -461,18 +468,10 @@ def _tool_run_command(workdir, args, cancel_event=None, deadline=None, sandbox=N
|
|
|
461
468
|
argv.extend(["--dir", root_path])
|
|
462
469
|
argv.extend(["--bind", root_path, root_path])
|
|
463
470
|
argv.extend(["--chdir", cwd, "--", "/bin/sh", "-lc", cmdline])
|
|
464
|
-
elif os.name == "posix" and sys.platform == "darwin":
|
|
465
|
-
return "(沙箱拒绝:当前 macOS 执行器尚未接入文件系统隔离后端,命令未执行)"
|
|
466
|
-
elif os.name != "posix":
|
|
467
|
-
return "(沙箱拒绝:当前 Windows 执行器尚未接入文件系统隔离后端,命令未执行)"
|
|
468
471
|
else:
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
list(sandbox.get("env_allowlist") or [])
|
|
473
|
-
+ ["PATH", "SYSTEMROOT", "WINDIR", "TEMP", "TMP", "COMSPEC", "HOME", "USERPROFILE"]))
|
|
474
|
-
r = runner.run_process(argv=argv, cwd=cwd,
|
|
475
|
-
env=policy.filter_env(os.environ, env_policy),
|
|
472
|
+
argv = None # 全信任档:runner 按平台自包 cmd /c 或 /bin/sh -c
|
|
473
|
+
r = runner.run_process(argv=argv, shell_cmd=cmdline if argv is None else None,
|
|
474
|
+
cwd=cwd, env=None,
|
|
476
475
|
timeout=min(timeout, CMD_MAX_TIMEOUT),
|
|
477
476
|
deadline=deadline, cancel_event=cancel_event)
|
|
478
477
|
out = runner.clean_cli_text(
|
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/modelhub.py
CHANGED
|
@@ -2177,6 +2177,20 @@ def _model_bindable(prov, model):
|
|
|
2177
2177
|
return True
|
|
2178
2178
|
|
|
2179
2179
|
|
|
2180
|
+
def _usable_default_model(prov, model=""):
|
|
2181
|
+
"""默认模型键位(model / model_easy / model_hard / 编排者配置)对账启用名单。
|
|
2182
|
+
|
|
2183
|
+
默认/难度模型是自由文本,与 models[] 的启停开关互不联动——键位指向已
|
|
2184
|
+
停用/已删除的模型时不能冒充可用:回落该厂商启用名单首个;全停用返回
|
|
2185
|
+
空串(调用方按「无默认模型」处理)。空键位本身合法(用网关默认),
|
|
2186
|
+
也返回空。"""
|
|
2187
|
+
model = (model or "").strip()
|
|
2188
|
+
if model and _model_bindable(prov, model):
|
|
2189
|
+
return model
|
|
2190
|
+
names = [m.get("name") for m in _enabled_models(prov) if m.get("name")]
|
|
2191
|
+
return names[0] if names else ""
|
|
2192
|
+
|
|
2193
|
+
|
|
2180
2194
|
def _model_image_in(prov, model):
|
|
2181
2195
|
"""模型是否声明支持图片输入:models[] 条目 image_in;缺省/查不到=False(纯文本)。"""
|
|
2182
2196
|
for m in ((prov or {}).get("models") or []):
|
|
@@ -2591,7 +2605,7 @@ def resolve_binding(agent_kind_or_id, difficulty="default"):
|
|
|
2591
2605
|
if len(entries) >= MAX_CHAIN_ATTEMPTS:
|
|
2592
2606
|
break
|
|
2593
2607
|
entries.append(_chain_entry_env(
|
|
2594
|
-
prov, model or prov.get("model")
|
|
2608
|
+
prov, model or _usable_default_model(prov, prov.get("model")),
|
|
2595
2609
|
target=agent_kind_or_id, endpoint=ep, key=kk["key"],
|
|
2596
2610
|
key_id=kk.get("id") or "", provider_id=pid))
|
|
2597
2611
|
if len(entries) >= MAX_CHAIN_ATTEMPTS:
|
|
@@ -2662,6 +2676,8 @@ def resolve_binding(agent_kind_or_id, difficulty="default"):
|
|
|
2662
2676
|
model = prov.get("model_" + tier) or "" if (routing and tier) else ""
|
|
2663
2677
|
if not model:
|
|
2664
2678
|
model = prov.get("model") or ""
|
|
2679
|
+
if model and not _model_bindable(prov, model):
|
|
2680
|
+
model = "" # 默认/难度模型键位指向停用或已删模型:不冒充可用,回落启用名单
|
|
2665
2681
|
if not model and names:
|
|
2666
2682
|
model = names[0] if (not routing or difficulty != "easy") else names[-1]
|
|
2667
2683
|
if not _evaluation_is_usable(pid, model):
|
|
@@ -2724,7 +2740,9 @@ def recommend_binding(agent_kind_or_id, difficulty="default", task_type="", role
|
|
|
2724
2740
|
names = [m.get("name") for m in _enabled_models(prov)
|
|
2725
2741
|
if isinstance(m, dict) and m.get("name")]
|
|
2726
2742
|
if not names and prov.get("model"):
|
|
2727
|
-
|
|
2743
|
+
dflt = str(prov.get("model"))
|
|
2744
|
+
if _model_bindable(prov, dflt): # 默认模型被停用时不再充作唯一候选
|
|
2745
|
+
names = [dflt]
|
|
2728
2746
|
# 候选阶段不能先截前三个:便宜档往往排在供应商列表后面,easy 任务
|
|
2729
2747
|
# 需要看到完整启用列表后再按成本与档位评分。最终调用链仍受上限约束。
|
|
2730
2748
|
for model in names:
|
|
@@ -3618,9 +3636,7 @@ def resolve_orchestrator():
|
|
|
3618
3636
|
if not prov or not prov.get("enabled", True) or not prov.get("api_key"):
|
|
3619
3637
|
return None
|
|
3620
3638
|
model = (cfg.get("model") or "").strip() or prov.get("model") or ""
|
|
3621
|
-
|
|
3622
|
-
names = _enabled_models(prov)
|
|
3623
|
-
model = names[0]["name"] if names else ""
|
|
3639
|
+
model = _usable_default_model(prov, model) # 配置的模型被停用时回落启用名单,不冒充可用
|
|
3624
3640
|
if not model:
|
|
3625
3641
|
return None
|
|
3626
3642
|
if not _evaluation_is_usable(pid, model):
|
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
|
|
|
@@ -615,14 +628,16 @@ def _create_task_from_builtin(payload):
|
|
|
615
628
|
|
|
616
629
|
|
|
617
630
|
def _run_builtin_step(run_id, role, bi, prompt, workdir, ev, note="", images=None,
|
|
618
|
-
followups=False):
|
|
631
|
+
followups=False, sandbox=None):
|
|
619
632
|
"""内置智能体步骤:直连模型 API + 工具循环(builtin_agent),不经 CLI 进程。
|
|
620
633
|
|
|
621
634
|
与 _run_step 对齐的三件事:暂停/取消闸门、运行中指令 drain 注入、重复调用
|
|
622
635
|
守门;结果同样经 _finish_step_result 落步骤(output=干净回答)并入用量台账。
|
|
623
636
|
日志只有「迭代/工具」摘要行——对话视图吃 output,日志抽屉看工具轨迹。
|
|
624
637
|
followups=True 时从回答末尾解析「建议追问」块(直连对话专用协议):
|
|
625
|
-
剥离出结构化列表落步骤记录,正文保持干净。
|
|
638
|
+
剥离出结构化列表落步骤记录,正文保持干净。
|
|
639
|
+
sandbox:任务沙箱策略由调用方透传(本函数无 task 上下文),builtin_agent
|
|
640
|
+
内部 normalize 后在工具执行期强制。"""
|
|
626
641
|
_wait_gate(run_id, ev)
|
|
627
642
|
deadline = _ensure_budget(run_id)
|
|
628
643
|
step, log_abs = store.add_step(run_id, role, "builtin", "CodeBee", note=note,
|
|
@@ -676,7 +691,7 @@ def _run_builtin_step(run_id, role, bi, prompt, workdir, ev, note="", images=Non
|
|
|
676
691
|
on_reason=_on_reason, on_stream=_on_stream,
|
|
677
692
|
on_activity=_on_activity,
|
|
678
693
|
task_creator=_create_task_from_builtin,
|
|
679
|
-
sandbox=
|
|
694
|
+
sandbox=sandbox, checkpoint_run_id=run_id)
|
|
680
695
|
if followups and res.get("ok"):
|
|
681
696
|
clean, fups = _parse_followups(res.get("text") or "")
|
|
682
697
|
if fups:
|
|
@@ -2321,7 +2336,7 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
2321
2336
|
if bi is not None:
|
|
2322
2337
|
res = _run_builtin_step(run_id, "direct" if first else "chat", bi, prompt,
|
|
2323
2338
|
step_wd, ev=ev, note=note, images=images,
|
|
2324
|
-
followups=True)
|
|
2339
|
+
followups=True, sandbox=task.get("sandbox"))
|
|
2325
2340
|
else:
|
|
2326
2341
|
res = _run_step(run_id, "direct" if first else "chat", impl, prompt, step_wd,
|
|
2327
2342
|
readonly=False, ev=ev, note=note,
|