codebee 0.1.19 → 0.1.21
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 +13 -0
- package/README.md +21 -14
- package/app/core/automation.py +6 -6
- package/app/core/capability.py +16 -2
- package/app/core/catalog.py +1 -1
- package/app/core/covergen.py +40 -4
- package/app/core/dispatch.py +141 -0
- package/app/core/flows.py +5 -0
- package/app/core/jobs.py +281 -166
- package/app/core/manager.py +30 -23
- package/app/core/market_remote.py +66 -22
- package/app/core/modelhub.py +145 -11
- package/app/core/pipeline.py +197 -35
- package/app/core/router.py +18 -7
- package/app/core/runner.py +2 -1
- package/app/core/selfupdate.py +24 -14
- package/app/core/settings.py +3 -3
- package/app/core/skills.py +43 -0
- package/app/core/store.py +20 -14
- package/app/core/zentao.py +26 -6
- package/app/main.py +20 -12
- package/app/pet.py +2 -2
- package/app/ui/app.js +87 -63
- package/app/ui/i18n.js +22 -3
- package/app/ui/index.html +11 -11
- package/app/ui/style.css +2 -0
- package/package.json +1 -1
package/app/core/settings.py
CHANGED
|
@@ -21,7 +21,7 @@ _FILE = paths.DATA_DIR / "settings.json"
|
|
|
21
21
|
# 运行时出现(空闲 90s 隐身)。
|
|
22
22
|
# cleanup_enabled / cleanup_retention_days:每日垃圾清理(core/cleanup.py)——
|
|
23
23
|
# 运行过程日志/发布截图/bak 残留等超期自动清理;retention 为保留天数。
|
|
24
|
-
DEFAULTS = {"max_concurrent_jobs":
|
|
24
|
+
DEFAULTS = {"max_concurrent_jobs": 12, "default_workdir": "", "hooks_token": "",
|
|
25
25
|
"telemetry_errors": True, "publish_daily_cap": 10,
|
|
26
26
|
"publish_fail_streak": 3, "notify_webhook": "", "notify_base_url": "",
|
|
27
27
|
"pet_enabled": True, "pet_mode": "always", "pet_skin": "plush",
|
|
@@ -29,8 +29,8 @@ DEFAULTS = {"max_concurrent_jobs": 6, "default_workdir": "", "hooks_token": "",
|
|
|
29
29
|
# 桌宠形象白名单(与 app/pet.py 的 SKINS 对齐;这里不 import pet 模块,避免
|
|
30
30
|
# core 反向依赖 app 根目录脚本)
|
|
31
31
|
PET_SKINS = ("plush", "robot")
|
|
32
|
-
#
|
|
33
|
-
# 同任务单飞守卫在 jobs
|
|
32
|
+
# 并发保护上限 12:任务有空位即直接启动,满载明确返回忙,不进入等待队列;
|
|
33
|
+
# 同任务单飞守卫在 jobs 层。默认取上限,对齐「默认不排队」的使用预期。
|
|
34
34
|
MIN_WORKERS, MAX_WORKERS = 1, 12
|
|
35
35
|
|
|
36
36
|
|
package/app/core/skills.py
CHANGED
|
@@ -394,9 +394,50 @@ def relevance_top(lessons, task, limit):
|
|
|
394
394
|
grams = _text_bigrams(x.get("title")) | _text_bigrams(x.get("content"))
|
|
395
395
|
return (-len(probe & grams), x.get("id") or "")
|
|
396
396
|
|
|
397
|
+
# 使用反馈闭环(pmb「量化记忆真实帮助」借鉴):被选中次数多的教训排前
|
|
398
|
+
hits = _load_hits()
|
|
399
|
+
def rank(x):
|
|
400
|
+
grams = _text_bigrams(x.get("title")) | _text_bigrams(x.get("content"))
|
|
401
|
+
overlap = -len(probe & grams)
|
|
402
|
+
lid = x.get("id") or ""
|
|
403
|
+
return (overlap, -hits.get(lid, 0), lid)
|
|
404
|
+
|
|
397
405
|
return sorted(lessons, key=rank)[:limit]
|
|
398
406
|
|
|
399
407
|
|
|
408
|
+
def _hits_path():
|
|
409
|
+
return paths.DATA_DIR / "skill_hits.json"
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _load_hits():
|
|
413
|
+
"""读取教训使用计数({lesson_id: 次数})。文件缺失/损坏返回空 dict。"""
|
|
414
|
+
try:
|
|
415
|
+
p = _hits_path()
|
|
416
|
+
if p.is_file():
|
|
417
|
+
import json
|
|
418
|
+
return json.loads(p.read_text(encoding="utf-8"))
|
|
419
|
+
except Exception:
|
|
420
|
+
pass
|
|
421
|
+
return {}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _bump_hits(ids):
|
|
425
|
+
"""注入后递增使用计数并持久化(fire-and-forget,失败静默)。"""
|
|
426
|
+
try:
|
|
427
|
+
import json
|
|
428
|
+
p = _hits_path()
|
|
429
|
+
hits = _load_hits()
|
|
430
|
+
for lid in ids:
|
|
431
|
+
if lid:
|
|
432
|
+
hits[lid] = hits.get(lid, 0) + 1
|
|
433
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
434
|
+
tmp = p.with_suffix(".tmp")
|
|
435
|
+
tmp.write_text(json.dumps(hits, ensure_ascii=False), encoding="utf-8")
|
|
436
|
+
tmp.replace(p)
|
|
437
|
+
except Exception:
|
|
438
|
+
pass
|
|
439
|
+
|
|
440
|
+
|
|
400
441
|
def block_for(task, scope_override=None, *, stable_order=False):
|
|
401
442
|
"""生成注入提示词的经验块。命中即计数。返回 (文本, 命中的 id 列表)。
|
|
402
443
|
|
|
@@ -440,6 +481,8 @@ def block_for(task, scope_override=None, *, stable_order=False):
|
|
|
440
481
|
text = "## 经验库(写作/工程规范 + 历史教训,必须遵守)\n\n" + "\n\n".join(parts)
|
|
441
482
|
if len(text) > MAX_INJECT_CHARS:
|
|
442
483
|
text = text[:MAX_INJECT_CHARS] + "\n…(已截断)"
|
|
484
|
+
if used:
|
|
485
|
+
_bump_hits(used) # 使用反馈闭环:被选中的教训递增计数,下次排序升权
|
|
443
486
|
if used:
|
|
444
487
|
bump_hits(used)
|
|
445
488
|
return text, used
|
package/app/core/store.py
CHANGED
|
@@ -448,11 +448,16 @@ def load_all():
|
|
|
448
448
|
# 错误串(`[91m[1mError:`、U+FFFD 乱码墙),读盘时统一洗一遍——
|
|
449
449
|
# 老运行不必等重跑才干净(2026-09-20「咋还有乱码」实测)
|
|
450
450
|
_sanitize_run_text(r)
|
|
451
|
-
#
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
451
|
+
# 直接执行线程不跨进程:running 与无截止时间的 queued 是上次
|
|
452
|
+
# 进程中断残骸。带 resume_enqueue_at 的 queued 是有意的自动续跑
|
|
453
|
+
# 退避,保留给 jobs.restore_deferred_resumes 重建 Timer。
|
|
454
|
+
interrupted = (r.get("status") == "running" or
|
|
455
|
+
(r.get("status") == "queued" and
|
|
456
|
+
not r.get("resume_enqueue_at")))
|
|
457
|
+
if interrupted:
|
|
458
|
+
r["status"] = "failed"
|
|
459
|
+
r["error"] = r.get("error") or "服务重启中断,可重试"
|
|
460
|
+
_save_json(p, r)
|
|
456
461
|
_RUNS[r["id"]] = r
|
|
457
462
|
except Exception:
|
|
458
463
|
pass
|
|
@@ -507,10 +512,11 @@ def get_run(run_id):
|
|
|
507
512
|
return _RUNS.get(run_id)
|
|
508
513
|
|
|
509
514
|
|
|
510
|
-
def list_runs(limit=60):
|
|
511
|
-
with LOCK:
|
|
512
|
-
ids = sorted(_RUNS.keys(), reverse=True)
|
|
513
|
-
|
|
515
|
+
def list_runs(limit=60):
|
|
516
|
+
with LOCK:
|
|
517
|
+
ids = sorted(_RUNS.keys(), reverse=True)
|
|
518
|
+
selected = ids if limit is None else ids[:limit]
|
|
519
|
+
return [_RUNS[i] for i in selected]
|
|
514
520
|
|
|
515
521
|
|
|
516
522
|
def latest_run_by_task():
|
|
@@ -635,17 +641,17 @@ def task_side(task_id):
|
|
|
635
641
|
}
|
|
636
642
|
|
|
637
643
|
|
|
638
|
-
def update_run(run_id, expected_status=None, **fields):
|
|
644
|
+
def update_run(run_id, expected_status=None, **fields):
|
|
639
645
|
"""更新运行字段。expected_status 非 None 时做 CAS(§2C):
|
|
640
646
|
当前状态不等于 expected_status 则拒绝写入并返回 None,
|
|
641
647
|
防止陈旧执行方(被取消的 worker、崩溃恢复前的旧线程)覆盖新状态
|
|
642
648
|
——防御模式「异步状态不是同步状态」。不传则保持原行为。"""
|
|
643
649
|
with LOCK:
|
|
644
650
|
run = _RUNS.get(run_id)
|
|
645
|
-
if not run:
|
|
646
|
-
return None
|
|
647
|
-
if expected_status is not None and run.get("status") != expected_status:
|
|
648
|
-
return None
|
|
651
|
+
if not run:
|
|
652
|
+
return None
|
|
653
|
+
if expected_status is not None and run.get("status") != expected_status:
|
|
654
|
+
return None
|
|
649
655
|
for k in ("error", "summary"):
|
|
650
656
|
if isinstance(fields.get(k), str):
|
|
651
657
|
# 错误串多是我们自己拼的 CLI 尾巴(含 ANSI/覆写/乱码墙):
|
package/app/core/zentao.py
CHANGED
|
@@ -1077,7 +1077,7 @@ def _goal_text(bug, side=None):
|
|
|
1077
1077
|
|
|
1078
1078
|
|
|
1079
1079
|
def _launch_fix(bug, profile, side, cfg):
|
|
1080
|
-
"""为一个 bug
|
|
1080
|
+
"""为一个 bug 的某一端建修复任务并立即启动。"""
|
|
1081
1081
|
bid = bug.get("id")
|
|
1082
1082
|
repo = _repo_of(profile, side)
|
|
1083
1083
|
wd = str(repo.get("workdir") or "").strip() or settings.default_workdir()
|
|
@@ -1088,11 +1088,31 @@ def _launch_fix(bug, profile, side, cfg):
|
|
|
1088
1088
|
payload["git_rev"] = str(repo["git_rev"]).strip()
|
|
1089
1089
|
if str(repo.get("verify_command") or "").strip():
|
|
1090
1090
|
payload["verify_command"] = str(repo["verify_command"]).strip()
|
|
1091
|
-
task =
|
|
1092
|
-
run =
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1091
|
+
task = None
|
|
1092
|
+
run = None
|
|
1093
|
+
try:
|
|
1094
|
+
task = store.create_task(payload)
|
|
1095
|
+
run = store.create_run("orchestration", task["title"], task_id=task["id"])
|
|
1096
|
+
store.update_task_status(task["id"], "queued")
|
|
1097
|
+
jobs.enqueue({"kind": "orchestration", "run_id": run["id"],
|
|
1098
|
+
"task_id": task["id"]})
|
|
1099
|
+
return task, run
|
|
1100
|
+
except Exception:
|
|
1101
|
+
log.exception("zentao: 修复任务启动失败 bug=%s side=%s task=%s run=%s",
|
|
1102
|
+
bid, side, (task or {}).get("id"), (run or {}).get("id"))
|
|
1103
|
+
if run:
|
|
1104
|
+
try:
|
|
1105
|
+
store.update_run(run["id"], status="failed",
|
|
1106
|
+
error="禅道修复任务启动失败,本次未排队,请稍后重试",
|
|
1107
|
+
ended_at=time.strftime("%Y-%m-%d %H:%M:%S"))
|
|
1108
|
+
except Exception:
|
|
1109
|
+
log.exception("zentao: 修复运行失败收口失败 run=%s", run.get("id"))
|
|
1110
|
+
if task:
|
|
1111
|
+
try:
|
|
1112
|
+
store.update_task_status(task["id"], "failed")
|
|
1113
|
+
except Exception:
|
|
1114
|
+
log.exception("zentao: 修复任务失败收口失败 task=%s", task.get("id"))
|
|
1115
|
+
raise
|
|
1096
1116
|
|
|
1097
1117
|
|
|
1098
1118
|
# ---------------------------------------------------------------- 回写文本
|
package/app/main.py
CHANGED
|
@@ -972,7 +972,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
972
972
|
m = re.match(r"^/api/catalog/([^/]+)/launch$", path)
|
|
973
973
|
if m:
|
|
974
974
|
# 一键打开(web 类起服务+开浏览器 / console 类新终端窗口)。
|
|
975
|
-
#
|
|
975
|
+
# 即时返回不走任务执行器;body 可传 {"open": false} 供测试免开浏览器
|
|
976
976
|
entry = catalog.by_id(m.group(1))
|
|
977
977
|
if not entry:
|
|
978
978
|
return self._json(404, {"error": "catalog 中无此条目"})
|
|
@@ -1713,9 +1713,13 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1713
1713
|
ok, err, new_run = store.retry_task(run["task_id"])
|
|
1714
1714
|
if ok:
|
|
1715
1715
|
store.update_run(new_run["id"], op="qa", qa_text=text)
|
|
1716
|
-
self._enqueue_run(
|
|
1717
|
-
|
|
1718
|
-
|
|
1716
|
+
started, start_err = self._enqueue_run(
|
|
1717
|
+
new_run["id"], run["task_id"],
|
|
1718
|
+
{"kind": "orchestration", "run_id": new_run["id"],
|
|
1719
|
+
"task_id": run["task_id"]})
|
|
1720
|
+
if not started:
|
|
1721
|
+
return self._json(503, {"error": start_err,
|
|
1722
|
+
"run_id": new_run["id"]})
|
|
1719
1723
|
return self._json(200, {"ok": True, "message": msg,
|
|
1720
1724
|
"qa_run": new_run["id"]})
|
|
1721
1725
|
return self._json(200, {"ok": True, "message": msg})
|
|
@@ -2100,15 +2104,17 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
2100
2104
|
return self._json(status, resp)
|
|
2101
2105
|
|
|
2102
2106
|
def _enqueue_run(self, run_id, task_id, job):
|
|
2103
|
-
"""
|
|
2107
|
+
"""立即启动失败时收口到 failed;系统默认没有等待队列。"""
|
|
2104
2108
|
try:
|
|
2105
2109
|
jobs.enqueue(job)
|
|
2106
2110
|
return True, ""
|
|
2107
|
-
except Exception:
|
|
2111
|
+
except Exception as exc:
|
|
2108
2112
|
# 不把异常文本(本机路径、命令行参数、供应商响应)返回给客户端;
|
|
2109
2113
|
# 详细堆栈只进服务端日志,run 记录也保留稳定的用户可读文案。
|
|
2110
2114
|
log.exception("任务入队失败 run=%s task=%s", run_id, task_id)
|
|
2111
|
-
|
|
2115
|
+
busy = isinstance(exc, jobs.JobsBusyError)
|
|
2116
|
+
err = ("当前运行任务已达并发保护上限,本次未排队,请稍后重试"
|
|
2117
|
+
if busy else "任务启动失败,请稍后重试")
|
|
2112
2118
|
try:
|
|
2113
2119
|
closed = store.update_run(run_id, status="failed", error=err,
|
|
2114
2120
|
ended_at=time.strftime("%Y-%m-%d %H:%M:%S"))
|
|
@@ -2161,8 +2167,7 @@ def _state_payload(client_id="", ver=None):
|
|
|
2161
2167
|
"control": remote.control_view(client_id),
|
|
2162
2168
|
# 供应商健康/告警(顶栏横幅数据源;有告警时 bump_state 会推给所有端)
|
|
2163
2169
|
"health": health.snapshot(),
|
|
2164
|
-
#
|
|
2165
|
-
# 是「并发满载在等」还是「job 蒸发没人管」(后者由看门狗 2 分钟自愈)
|
|
2170
|
+
# 直接执行观测:并发保护上限、运行数和可用位;queued 恒为 0。
|
|
2166
2171
|
"jobs": jobs.workers_info(),
|
|
2167
2172
|
}
|
|
2168
2173
|
|
|
@@ -2355,14 +2360,17 @@ def main():
|
|
|
2355
2360
|
telemetry.start_background() # 匿名错误回传+版本 ping(默认开可关;未配端点自动休眠,延迟 45s 不挡启动)
|
|
2356
2361
|
except Exception:
|
|
2357
2362
|
pass
|
|
2358
|
-
_step("
|
|
2363
|
+
_step("正在启动任务执行器…")
|
|
2359
2364
|
jobs.start_worker()
|
|
2365
|
+
n_wait = jobs.restore_deferred_resumes()
|
|
2366
|
+
if n_wait:
|
|
2367
|
+
print("[CodeBee] 已恢复 %d 个定时退避中的自动续跑任务" % n_wait)
|
|
2360
2368
|
n_resume = jobs.resume_interrupted() # 启动恢复:服务被杀中断的连载任务自动续跑
|
|
2361
2369
|
if n_resume:
|
|
2362
2370
|
print("[CodeBee] 已自动恢复 %d 个中断的连载任务(断点续跑)" % n_resume)
|
|
2363
|
-
n_rq = jobs.requeue_pending() #
|
|
2371
|
+
n_rq = jobs.requeue_pending() # 兼容旧版本遗留的无退避 queued 记录
|
|
2364
2372
|
if n_rq:
|
|
2365
|
-
print("[CodeBee]
|
|
2373
|
+
print("[CodeBee] 已直接启动 %d 个遗留运行" % n_rq)
|
|
2366
2374
|
_step("正在启动自动化调度…")
|
|
2367
2375
|
n_auto = automation.start() # 自动化:加载定时任务并拉起调度线程(错过的一次性任务不补跑)
|
|
2368
2376
|
if n_auto:
|
package/app/pet.py
CHANGED
|
@@ -117,7 +117,7 @@ def derive_state(prev_active_ids, tasks):
|
|
|
117
117
|
|
|
118
118
|
|
|
119
119
|
def tooltip_lines(snap, lang="zh"):
|
|
120
|
-
"""悬停清单:未读摘要 + 进行中 +
|
|
120
|
+
"""悬停清单:未读摘要 + 进行中 + 待启动/自动续跑,最多 6 行。
|
|
121
121
|
|
|
122
122
|
只列「还在跑 / 待跑」的任务——历史失败项会刷满清单(截图实测十几条
|
|
123
123
|
✘ 把面板撑成一堵墙),失败已由警示气泡点名,这里不再重复。
|
|
@@ -167,7 +167,7 @@ LANG = {
|
|
|
167
167
|
"all_clear": "蜂群闲着,都在打盹…",
|
|
168
168
|
"title": "蜂群动态",
|
|
169
169
|
"n_running": "%d 个进行中",
|
|
170
|
-
"queued": "
|
|
170
|
+
"queued": "正在启动",
|
|
171
171
|
"cheer": "🎉 %d 个任务完工!",
|
|
172
172
|
"alert": "⚠️ 「%s」出岔子了,点我看看",
|
|
173
173
|
"bye": "蜜蜂回巢啦",
|