codebee 0.1.63 → 0.1.64
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 +8 -0
- package/README.md +5 -2
- package/app/core/artifacts.py +148 -0
- package/app/core/bookmeta.py +215 -19
- package/app/core/modelhub.py +76 -10
- package/app/core/pipeline.py +42 -3
- package/app/core/runner.py +8 -3
- package/app/core/store.py +13 -70
- package/app/core/zentao.py +386 -29
- package/app/main.py +8 -2
- package/package.json +1 -1
package/app/core/pipeline.py
CHANGED
|
@@ -1541,13 +1541,27 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1541
1541
|
_write_project_memory(task, workdir, _mem_lines)
|
|
1542
1542
|
except Exception:
|
|
1543
1543
|
pass
|
|
1544
|
+
# Stop gate(planning-with-files):收工时活计划若有未勾选项,摘要与 verdict
|
|
1545
|
+
# 如实点名——「计划没走完就报完成」属于静默降级(赛马/异常路径不回写勾选时
|
|
1546
|
+
# 此前无任何可见信号)。只保可见不改判:勾选是文件事实,质量判据仍是评审。
|
|
1547
|
+
try:
|
|
1548
|
+
gate_total, gate_left = plan_stopgate(workdir)
|
|
1549
|
+
except Exception:
|
|
1550
|
+
gate_total, gate_left = 0, []
|
|
1551
|
+
gate_note = ""
|
|
1552
|
+
if gate_left:
|
|
1553
|
+
verdict["plan_gate"] = "活计划 %d 项中 %d 项未勾选完成:%s" % (
|
|
1554
|
+
gate_total, len(gate_left),
|
|
1555
|
+
"、".join("#%d[%s]%s" % (n, mk, t) for n, mk, t in gate_left[:3]))
|
|
1556
|
+
gate_note = ";⚠ 活计划 %d/%d 项完成" % (gate_total - len(gate_left), gate_total)
|
|
1544
1557
|
store.update_run(run_id, expected_status="running", status="done", verdict=verdict,
|
|
1545
|
-
summary="代码任务%s(验证%s / 评审%s%s)" % (
|
|
1558
|
+
summary="代码任务%s(验证%s / 评审%s%s%s)" % (
|
|
1546
1559
|
"通过" if overall_pass else "未通过",
|
|
1547
1560
|
"通过" if verify_pass else "未通过",
|
|
1548
1561
|
(("通过" if review_json.get("pass") else "未通过")
|
|
1549
1562
|
if workflow["review_required"] else "按策略省略"),
|
|
1550
|
-
",%d 轮修复" % (len(repairs) - 1) if len(repairs) > 1 else ""
|
|
1563
|
+
",%d 轮修复" % (len(repairs) - 1) if len(repairs) > 1 else "",
|
|
1564
|
+
gate_note),
|
|
1551
1565
|
ended_at=_now())
|
|
1552
1566
|
|
|
1553
1567
|
|
|
@@ -4145,6 +4159,31 @@ def _write_task_plan(task, workdir, plan):
|
|
|
4145
4159
|
_PLAN_MARK = {"done": "x", "fail": "!", "run": ">"}
|
|
4146
4160
|
|
|
4147
4161
|
|
|
4162
|
+
def plan_stopgate(workdir):
|
|
4163
|
+
"""Stop gate(planning-with-files 第四件):收工前校验活计划勾选完整性。
|
|
4164
|
+
返回 (总项数, [(序号, 标记, 标题)]);计划文件缺失/异常 → (0, [])(无计划不拦)。
|
|
4165
|
+
未完成 = 勾选框不是 [x](含 [ ] 未跑、[!] 失败、[>] 中断在跑)。"""
|
|
4166
|
+
try:
|
|
4167
|
+
from pathlib import Path as _P
|
|
4168
|
+
import re as _re
|
|
4169
|
+
root = _P(workdir).resolve()
|
|
4170
|
+
target = (root / ".codebee" / "task_plan.md").resolve()
|
|
4171
|
+
if root not in target.parents or not target.is_file():
|
|
4172
|
+
return 0, []
|
|
4173
|
+
pat = _re.compile(r"^(\d+)\. \[([ x!>])\] ")
|
|
4174
|
+
total, unfinished = 0, []
|
|
4175
|
+
for ln in target.read_text("utf-8").splitlines():
|
|
4176
|
+
m = pat.match(ln)
|
|
4177
|
+
if not m:
|
|
4178
|
+
continue
|
|
4179
|
+
total += 1
|
|
4180
|
+
if m.group(2) != "x":
|
|
4181
|
+
unfinished.append((int(m.group(1)), m.group(2), ln[m.end():][:60]))
|
|
4182
|
+
return total, unfinished
|
|
4183
|
+
except Exception:
|
|
4184
|
+
return 0, []
|
|
4185
|
+
|
|
4186
|
+
|
|
4148
4187
|
def mark_task_plan(workdir, index, status):
|
|
4149
4188
|
"""活计划回写:把 .codebee/task_plan.md 第 index 项(1 基)的勾选框
|
|
4150
4189
|
翻成 [x](完成)/[!](失败)/[>](进行中)。planning-with-files 的断点
|
|
@@ -4211,7 +4250,7 @@ def execute_run(run_id):
|
|
|
4211
4250
|
if initial_status not in ("queued", "running"):
|
|
4212
4251
|
return
|
|
4213
4252
|
if store.update_run(run_id, expected_status=initial_status, status="running",
|
|
4214
|
-
started_at=_now()) is None:
|
|
4253
|
+
started_at=_now(), resume_enqueue_at="", warnings=[]) is None:
|
|
4215
4254
|
return
|
|
4216
4255
|
if task is None:
|
|
4217
4256
|
store.update_run(run_id, expected_status="running", status="failed",
|
package/app/core/runner.py
CHANGED
|
@@ -981,7 +981,7 @@ def _model_flag(kind, model):
|
|
|
981
981
|
return ["--model", model] # claude / opencode / aider
|
|
982
982
|
|
|
983
983
|
|
|
984
|
-
|
|
984
|
+
_TRANSIENT_DEFAULT = ("503", "502", "529", "429", "no available channel", "temporarily",
|
|
985
985
|
"unavailable", "overloaded", "rate limit", "timeout", "timed out",
|
|
986
986
|
"输出停滞",
|
|
987
987
|
# 2026-09-15 连载验收实测:网关故障形态远不止 HTTP 5xx——
|
|
@@ -1007,10 +1007,15 @@ def _model_flag(kind, model):
|
|
|
1007
1007
|
"enotfound", "getaddrinfo", "name or service not known",
|
|
1008
1008
|
"name resolution")
|
|
1009
1009
|
|
|
1010
|
+
# 保留兼容别名:旧 worker 热重载时可能短暂丢失模块级变量,判断函数不能因此
|
|
1011
|
+
# 把一次普通上游故障升级成 NameError。
|
|
1012
|
+
_TRANSIENT = _TRANSIENT_DEFAULT
|
|
1010
1013
|
|
|
1011
|
-
|
|
1014
|
+
|
|
1015
|
+
def _transient_error(err, _default_markers=_TRANSIENT_DEFAULT):
|
|
1012
1016
|
err = (err or "").lower()
|
|
1013
|
-
|
|
1017
|
+
markers = globals().get("_TRANSIENT") or _default_markers
|
|
1018
|
+
return any(k in err for k in markers)
|
|
1014
1019
|
|
|
1015
1020
|
|
|
1016
1021
|
# 欠费/配额/限流耗尽:换 KEY 与换厂商都该继续(同厂商另一账号往往还能用)。
|
package/app/core/store.py
CHANGED
|
@@ -18,7 +18,7 @@ import threading
|
|
|
18
18
|
import time
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
|
|
21
|
-
from . import paths, runner, volumes
|
|
21
|
+
from . import artifacts, paths, runner, volumes
|
|
22
22
|
|
|
23
23
|
LOCK = threading.RLock()
|
|
24
24
|
_TASKS = {}
|
|
@@ -746,7 +746,7 @@ def task_side(task_id):
|
|
|
746
746
|
total_steps = sum(len(r.get("steps") or []) for r in runs)
|
|
747
747
|
# 成品口径闸:任务一步都没跑出来过(如历次都在检出前失败)→ 工作目录里的
|
|
748
748
|
# 文件变动全是并行活动的噪音,不算这个任务的成品
|
|
749
|
-
wd, arts = run_artifacts(latest["id"], limit=50) if (latest and total_steps > 0) else ("", [])
|
|
749
|
+
wd, arts = run_artifacts(latest["id"], limit=50, cache=True) if (latest and total_steps > 0) else ("", [])
|
|
750
750
|
return {
|
|
751
751
|
"task": {k: task.get(k) for k in ("id", "title", "status", "workdir", "git_state",
|
|
752
752
|
"git_rev")},
|
|
@@ -966,41 +966,6 @@ def run_workdir(run_id):
|
|
|
966
966
|
return str(task.get("workdir") or "") if task else ""
|
|
967
967
|
|
|
968
968
|
|
|
969
|
-
_SKIP_DIRS = {".git", "node_modules", "__pycache__", ".venv", "venv", ".idea", ".vscode"}
|
|
970
|
-
|
|
971
|
-
# 构建产物/依赖缓存目录:里面的文件是工具链再生成的,不是任务成品
|
|
972
|
-
_BUILD_DIRS = {
|
|
973
|
-
"target", "build", "dist", "out", "bin", "obj", # 通用构建输出
|
|
974
|
-
"surefire", "failsafe-reports", "test-output", "reports", # 测试/报告输出
|
|
975
|
-
".next", ".nuxt", ".output", ".gradle", ".gradle-home", # 前端/Gradle
|
|
976
|
-
"__MACOSX",
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
# CLI 自身的历史/评审中间件不属于用户成果。它们通常落在工作目录根部,
|
|
980
|
-
# 只按隐藏目录过滤会漏掉 Aider 的 .aider.* 与流程生成的 *_review.json。
|
|
981
|
-
_PROCESS_ARTIFACT_NAMES = {
|
|
982
|
-
".aider.chat.history.md", ".aider.input.history", ".aider.input.history.md",
|
|
983
|
-
".aider.tags.cache.v4", ".aider.tags.cache.v3", ".aider.conf.yml",
|
|
984
|
-
}
|
|
985
|
-
_PROCESS_ARTIFACT_SUFFIXES = ("_review.json", ".review.json")
|
|
986
|
-
_PROCESS_ARTIFACT_PREFIXES = ("tutti_prompt_", "_tutti_prompt_")
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
def _is_process_artifact(rel_name):
|
|
990
|
-
"""判断工作目录文件是否为 CLI/评审过程产物,而非可交付成果。"""
|
|
991
|
-
name = str(rel_name or "").replace("\\", "/")
|
|
992
|
-
base = name.rsplit("/", 1)[-1].lower()
|
|
993
|
-
if base in _PROCESS_ARTIFACT_NAMES:
|
|
994
|
-
return True
|
|
995
|
-
if base.startswith(".") and base.startswith(".aider"):
|
|
996
|
-
return True
|
|
997
|
-
if base.endswith(_PROCESS_ARTIFACT_SUFFIXES):
|
|
998
|
-
return True
|
|
999
|
-
if base.startswith(_PROCESS_ARTIFACT_PREFIXES):
|
|
1000
|
-
return True
|
|
1001
|
-
return False
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
969
|
def task_first_start(task_id, fallback=""):
|
|
1005
970
|
"""该任务最早一次运行的开始时间(含回退:任务创建时间 → 指定回退值)。"""
|
|
1006
971
|
stamps = []
|
|
@@ -1019,13 +984,15 @@ def task_first_start(task_id, fallback=""):
|
|
|
1019
984
|
return best
|
|
1020
985
|
|
|
1021
986
|
|
|
1022
|
-
def run_artifacts(run_id, limit=200):
|
|
987
|
+
def run_artifacts(run_id, limit=200, cache=False):
|
|
1023
988
|
"""列一次运行的「成品文件」:工作目录里自该任务首次运行以来新产生/修改的文件。
|
|
1024
989
|
|
|
1025
990
|
断点续跑会拆成多条 run,只按本 run 过滤会漏掉早期章节;这里以「任务首跑」
|
|
1026
991
|
为起点。返回 (workdir, files);files 按 mtime 新→旧,name 为工作目录内
|
|
1027
|
-
相对路径,已跳过 .git / 隐藏目录 / node_modules / target
|
|
1028
|
-
|
|
992
|
+
相对路径,已跳过 .git / 隐藏目录 / node_modules / target 等构建产物目录。
|
|
993
|
+
cache=True 供 UI 轮询热路径(详情/侧栏/状态快照)复用短 TTL 缓存与在飞
|
|
994
|
+
合并;发布/预览/知识库等正确性敏感方保持默认,永远现扫。
|
|
995
|
+
剪枝遍历与过滤实现在 artifacts.py(几万文件目录从秒级降到百毫秒级)。
|
|
1029
996
|
"""
|
|
1030
997
|
wd = run_workdir(run_id)
|
|
1031
998
|
if not wd:
|
|
@@ -1040,36 +1007,12 @@ def run_artifacts(run_id, limit=200):
|
|
|
1040
1007
|
root = Path(wd)
|
|
1041
1008
|
if not root.is_dir():
|
|
1042
1009
|
return wd, []
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
continue
|
|
1050
|
-
if any(part in _BUILD_DIRS for part in p.parts[:-1]):
|
|
1051
|
-
continue
|
|
1052
|
-
# 隐藏目录一律是工具过程文件(.mimocode/.zcode/.claude/.codex…),
|
|
1053
|
-
# 不是成品;按前缀通排,免得每来一个新 agent CLI 就补一次白名单
|
|
1054
|
-
if any(part.startswith(".") for part in p.parts[:-1]):
|
|
1055
|
-
continue
|
|
1056
|
-
try:
|
|
1057
|
-
st = p.stat()
|
|
1058
|
-
except OSError:
|
|
1059
|
-
continue
|
|
1060
|
-
if st.st_mtime < t0:
|
|
1061
|
-
continue
|
|
1062
|
-
rel_name = str(p.relative_to(root)).replace("\\", "/")
|
|
1063
|
-
if _is_process_artifact(rel_name):
|
|
1064
|
-
continue
|
|
1065
|
-
files.append({"name": rel_name,
|
|
1066
|
-
"size": st.st_size, "mtime": int(st.st_mtime)})
|
|
1067
|
-
if len(files) >= 800: # 防超大目录拖垮接口;截断后再排序取最新
|
|
1068
|
-
break
|
|
1069
|
-
except OSError:
|
|
1070
|
-
pass
|
|
1071
|
-
files.sort(key=lambda f: -f["mtime"])
|
|
1072
|
-
return wd, files[:limit]
|
|
1010
|
+
running = False
|
|
1011
|
+
if cache:
|
|
1012
|
+
task = get_task(run.get("task_id") or "") if run.get("task_id") else None
|
|
1013
|
+
running = bool(task) and task.get("status") in ("queued", "running")
|
|
1014
|
+
return wd, artifacts.scan(str(root), t0, limit=limit,
|
|
1015
|
+
running=running, cache=cache)
|
|
1073
1016
|
|
|
1074
1017
|
|
|
1075
1018
|
def task_step_count(task_id):
|