codebee 0.1.54 → 0.1.55
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 +7 -0
- package/README.md +3 -4
- package/app/core/builtin_agent.py +67 -26
- package/app/core/jobs.py +21 -2
- package/app/core/knowledge.py +21 -4
- package/app/core/modelhub.py +33 -4
- package/app/core/pipeline.py +143 -61
- package/app/core/planner.py +27 -13
- package/app/core/runner.py +280 -78
- package/app/core/store.py +64 -27
- package/app/core/zentao.py +26 -6
- package/app/ui/app.js +23 -13
- package/app/ui/board.css +360 -262
- package/app/ui/board.html +119 -41
- package/app/ui/board.js +60 -14
- package/app/ui/i18n.js +2698 -2659
- package/app/ui/index.html +2 -0
- package/package.json +1 -1
package/app/core/store.py
CHANGED
|
@@ -28,7 +28,11 @@ _RUNS = {}
|
|
|
28
28
|
# 长跑也不会无限增长(finish_step 里调 clear_stream_state)。
|
|
29
29
|
_STREAM_LOCK = threading.Lock()
|
|
30
30
|
_STREAM_TS = {}
|
|
31
|
-
LIVE_TEXT_MAX = 20000 # 单步骤实时思考/正文的保留上限(与 builtin_agent 同口径)
|
|
31
|
+
LIVE_TEXT_MAX = 20000 # 单步骤实时思考/正文的保留上限(与 builtin_agent 同口径)
|
|
32
|
+
|
|
33
|
+
DEFAULT_TASK_TIMEOUT_S = 3600
|
|
34
|
+
MIN_TASK_TIMEOUT_S = 1
|
|
35
|
+
MAX_TASK_TIMEOUT_S = 7 * 24 * 3600
|
|
32
36
|
|
|
33
37
|
# ---------------------------------------------------------------- 状态版本(SSE 事件驱动)
|
|
34
38
|
# 任何落盘写都算状态变化:SSE 连接等版本号变化才构建/推送全量状态,
|
|
@@ -57,12 +61,38 @@ def wait_state_change(last_ver, timeout):
|
|
|
57
61
|
return _STATE_VER
|
|
58
62
|
|
|
59
63
|
|
|
60
|
-
def _new_id(prefix):
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
def _new_id(prefix):
|
|
65
|
+
stamp = time.strftime("%Y%m%d-%H%M%S")
|
|
66
|
+
# 10k random suffixes are not unique under a burst of task/run creation.
|
|
67
|
+
# Reserve against both indexes and disk so a collision cannot silently
|
|
68
|
+
# overwrite a queued run and make one of several submissions disappear.
|
|
69
|
+
with LOCK:
|
|
70
|
+
for _ in range(100):
|
|
71
|
+
candidate = "%s-%s-%04d" % (prefix, stamp, secrets.randbelow(10000))
|
|
72
|
+
if candidate in _TASKS or candidate in _RUNS:
|
|
73
|
+
continue
|
|
74
|
+
if (paths.TASKS_DIR / (candidate + ".json")).exists():
|
|
75
|
+
continue
|
|
76
|
+
if (paths.RUNS_DIR / candidate).exists():
|
|
77
|
+
continue
|
|
78
|
+
return candidate
|
|
79
|
+
raise RuntimeError("无法生成唯一记录 ID")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _safe_name(s):
|
|
83
|
+
return re.sub(r"[^0-9A-Za-z_-]+", "-", str(s))[:40].strip("-") or "x"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _task_timeout_s(value=None):
|
|
87
|
+
"""Resolve one bounded wall-clock budget for a task."""
|
|
88
|
+
raw = value
|
|
89
|
+
if raw in (None, ""):
|
|
90
|
+
raw = os.environ.get("TUTTI_TASK_TIMEOUT_S", DEFAULT_TASK_TIMEOUT_S)
|
|
91
|
+
try:
|
|
92
|
+
seconds = int(float(raw))
|
|
93
|
+
except (TypeError, ValueError):
|
|
94
|
+
seconds = DEFAULT_TASK_TIMEOUT_S
|
|
95
|
+
return max(MIN_TASK_TIMEOUT_S, min(MAX_TASK_TIMEOUT_S, seconds))
|
|
66
96
|
|
|
67
97
|
|
|
68
98
|
# ---------------------------------------------------------------- 任务
|
|
@@ -128,7 +158,8 @@ def create_task(payload):
|
|
|
128
158
|
"status": "created",
|
|
129
159
|
}
|
|
130
160
|
thinking = str(payload.get("thinking") or "standard").strip().lower()
|
|
131
|
-
task["thinking"] = thinking if thinking in ("auto", "low", "standard", "high") else "auto"
|
|
161
|
+
task["thinking"] = thinking if thinking in ("auto", "low", "standard", "high") else "auto"
|
|
162
|
+
task["timeout_s"] = _task_timeout_s(payload.get("timeout_s"))
|
|
132
163
|
if flow["engine"] == "direct":
|
|
133
164
|
task["direct_provider_id"] = _text(payload.get("direct_provider_id"),
|
|
134
165
|
"direct_provider_id")[:80]
|
|
@@ -515,7 +546,7 @@ def load_all():
|
|
|
515
546
|
_save_json(paths.TASKS_DIR / (t["id"] + ".json"), t)
|
|
516
547
|
continue
|
|
517
548
|
latest = max(runs, key=lambda r: r["id"])
|
|
518
|
-
if latest.get("status") in ("done", "failed", "cancelled"):
|
|
549
|
+
if latest.get("status") in ("done", "failed", "cancelled", "timeout"):
|
|
519
550
|
t["status"] = latest["status"]
|
|
520
551
|
_save_json(paths.TASKS_DIR / (t["id"] + ".json"), t)
|
|
521
552
|
|
|
@@ -531,7 +562,7 @@ def set_run_estimator(estimator):
|
|
|
531
562
|
_RUN_ESTIMATOR = estimator if callable(estimator) else None
|
|
532
563
|
|
|
533
564
|
|
|
534
|
-
def create_run(kind, title, task_id=None, entry_id=None, op=None):
|
|
565
|
+
def create_run(kind, title, task_id=None, entry_id=None, op=None):
|
|
535
566
|
run = {
|
|
536
567
|
"id": _new_id("r" if kind == "orchestration" else "m"),
|
|
537
568
|
"kind": kind, # orchestration | mgmt
|
|
@@ -539,12 +570,14 @@ def create_run(kind, title, task_id=None, entry_id=None, op=None):
|
|
|
539
570
|
"task_id": task_id, "entry_id": entry_id, "op": op,
|
|
540
571
|
"status": "queued", "steps": [], "messages": [],
|
|
541
572
|
"created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
542
|
-
"started_at": None, "ended_at": None,
|
|
573
|
+
"started_at": None, "ended_at": None,
|
|
574
|
+
"deadline_at": None,
|
|
543
575
|
"cost_usd": 0.0, "tokens": 0, "error": "",
|
|
544
576
|
"verdict": None, "summary": "",
|
|
545
577
|
}
|
|
546
|
-
if kind == "orchestration" and task_id:
|
|
547
|
-
task = get_task(task_id) or {}
|
|
578
|
+
if kind == "orchestration" and task_id:
|
|
579
|
+
task = get_task(task_id) or {}
|
|
580
|
+
run["deadline_at"] = time.time() + _task_timeout_s(task.get("timeout_s"))
|
|
548
581
|
try:
|
|
549
582
|
eta = _RUN_ESTIMATOR(
|
|
550
583
|
task_type=task.get("type") or "", mode=task.get("mode") or "auto",
|
|
@@ -729,25 +762,27 @@ def update_run(run_id, expected_status=None, **fields):
|
|
|
729
762
|
# 这套在落终态瞬间即时收口——打磨组长被取消/异常打断时 UI 不闪
|
|
730
763
|
# 假「工作中」)。run 终态后不可能还有步骤在真跑。
|
|
731
764
|
st = fields.get("status")
|
|
732
|
-
if st in ("done", "failed", "cancelled"):
|
|
733
|
-
end_s = time.strftime("%H:%M:%S")
|
|
734
|
-
for s in run["steps"]:
|
|
735
|
-
if s.get("status") in ("queued", "running"):
|
|
736
|
-
s["status"] = "cancelled"
|
|
737
|
-
s["ended_at"] = s.get("ended_at") or end_s
|
|
738
|
-
if not s.get("summary"):
|
|
739
|
-
s["summary"] = "
|
|
765
|
+
if st in ("done", "failed", "cancelled", "timeout"):
|
|
766
|
+
end_s = time.strftime("%H:%M:%S")
|
|
767
|
+
for s in run["steps"]:
|
|
768
|
+
if s.get("status") in ("queued", "running"):
|
|
769
|
+
s["status"] = "timeout" if st == "timeout" else "cancelled"
|
|
770
|
+
s["ended_at"] = s.get("ended_at") or end_s
|
|
771
|
+
if not s.get("summary"):
|
|
772
|
+
s["summary"] = ("步骤未正常收尾(任务总时限已到)"
|
|
773
|
+
if st == "timeout" else
|
|
774
|
+
"步骤未正常收尾(终态自动恢复)")
|
|
740
775
|
_save_json(paths.RUNS_DIR / run_id / "run.json", run)
|
|
741
776
|
# 状态回填:起跑与结束都同步任务状态。只回填终态的话,run 在跑、
|
|
742
777
|
# 任务永远显示「排队中」(2026-09-18 实案:「# 重写·续」run 已 running
|
|
743
778
|
# 写了一小时,任务卡 queued,用户以为卡死连点重试)。
|
|
744
|
-
if st in ("running", "done", "failed", "cancelled"):
|
|
779
|
+
if st in ("running", "done", "failed", "cancelled", "timeout"):
|
|
745
780
|
tid = run.get("task_id")
|
|
746
781
|
task = _TASKS.get(tid) if tid else None
|
|
747
782
|
if task:
|
|
748
783
|
task["status"] = st
|
|
749
784
|
_save_json(paths.TASKS_DIR / (tid + ".json"), task)
|
|
750
|
-
if st in ("done", "failed", "cancelled"):
|
|
785
|
+
if st in ("done", "failed", "cancelled", "timeout"):
|
|
751
786
|
# run_end 钩子(2026-09-22):终态即触发,后台跑副作用型脚本
|
|
752
787
|
# (回写知识库等);失败/超时静默,绝不拖慢收尾路径
|
|
753
788
|
try:
|
|
@@ -838,15 +873,17 @@ def recover_orphaned_runs():
|
|
|
838
873
|
step_now = time.strftime("%H:%M:%S")
|
|
839
874
|
with LOCK:
|
|
840
875
|
for r in _RUNS.values():
|
|
841
|
-
if r.get("status") not in ("done", "failed", "cancelled"):
|
|
842
|
-
continue
|
|
876
|
+
if r.get("status") not in ("done", "failed", "cancelled", "timeout"):
|
|
877
|
+
continue
|
|
843
878
|
changed = False
|
|
844
879
|
for s in r.get("steps") or []:
|
|
845
880
|
if s.get("status") in ("queued", "running"):
|
|
846
|
-
s["status"] = "cancelled"
|
|
881
|
+
s["status"] = "timeout" if r.get("status") == "timeout" else "cancelled"
|
|
847
882
|
s["ended_at"] = s.get("ended_at") or step_now
|
|
848
883
|
if not s.get("summary"):
|
|
849
|
-
s["summary"] = "
|
|
884
|
+
s["summary"] = ("步骤未正常收尾(任务总时限已到)"
|
|
885
|
+
if r.get("status") == "timeout" else
|
|
886
|
+
"步骤未正常收尾(取消/中断自动恢复)")
|
|
850
887
|
changed = True
|
|
851
888
|
if changed:
|
|
852
889
|
_save_json(paths.RUNS_DIR / r["id"] / "run.json", r)
|
package/app/core/zentao.py
CHANGED
|
@@ -66,6 +66,7 @@ from . import jobs, paths, settings, store, tlsctx
|
|
|
66
66
|
log = logging.getLogger(__name__)
|
|
67
67
|
|
|
68
68
|
_LOCK = threading.RLock()
|
|
69
|
+
_SCAN_LOCK = threading.Lock() # 手动/定时扫描单飞,避免重复拉取和重复认领
|
|
69
70
|
_FILE = paths.DATA_DIR / "zentao.json"
|
|
70
71
|
_STATE = {
|
|
71
72
|
"config": {}, # 持久配置(_CFG_DEFAULTS)
|
|
@@ -868,12 +869,20 @@ def _claimable(bug, profile):
|
|
|
868
869
|
return True
|
|
869
870
|
|
|
870
871
|
|
|
871
|
-
def list_bugs(cfg, product_id):
|
|
872
|
-
"""
|
|
872
|
+
def list_bugs(cfg, product_id, profile=None):
|
|
873
|
+
"""拉一个产品下的激活 bug(分页,总量封顶 MAX_BUGS)。
|
|
874
|
+
|
|
875
|
+
REST v1 支持时把状态/指派人过滤下推给禅道,减少无关 bug 的分页传输;
|
|
876
|
+
本地的 _claimable 仍保留,兼容老接口或禅道忽略未知查询参数的情况。
|
|
877
|
+
"""
|
|
873
878
|
out = []
|
|
874
879
|
for page in range(1, 6):
|
|
875
|
-
|
|
876
|
-
|
|
880
|
+
query = {"page": page, "limit": PAGE_LIMIT, "status": "active"}
|
|
881
|
+
assigned = str((profile or {}).get("assigned_to") or "").strip()
|
|
882
|
+
if assigned:
|
|
883
|
+
query["assignedTo"] = assigned
|
|
884
|
+
d = _call("GET", "/products/%s/bugs?%s" % (
|
|
885
|
+
product_id, urllib.parse.urlencode(query)), cfg=cfg)
|
|
877
886
|
bugs = d.get("bugs") if isinstance(d, dict) else None
|
|
878
887
|
if not isinstance(bugs, list):
|
|
879
888
|
raise ZenError("禅道 bug 列表响应形状不对(预期 bugs 数组)")
|
|
@@ -1038,7 +1047,7 @@ def test_connection(base_url=None, account=None, password=None):
|
|
|
1038
1047
|
try:
|
|
1039
1048
|
profiles = _profiles(c)
|
|
1040
1049
|
if profiles:
|
|
1041
|
-
bugs = list_bugs(probe, profiles[0]["product"])
|
|
1050
|
+
bugs = list_bugs(probe, profiles[0]["product"], profiles[0])
|
|
1042
1051
|
return True, "连接成功(%s),产品 %s 可访问(当前 %d 条 bug 在列表里)" % (
|
|
1043
1052
|
tag, profiles[0]["product"], len(bugs))
|
|
1044
1053
|
except ZenError as e:
|
|
@@ -1712,7 +1721,7 @@ def _scan(cfg):
|
|
|
1712
1721
|
if not profiles:
|
|
1713
1722
|
raise ZenError("未配置产品档案——请先在设置页添加产品并配置仓库")
|
|
1714
1723
|
for profile in profiles:
|
|
1715
|
-
bugs = list_bugs(cfg, profile["product"])
|
|
1724
|
+
bugs = list_bugs(cfg, profile["product"], profile)
|
|
1716
1725
|
by_id = {str(b.get("id") or ""): b for b in bugs}
|
|
1717
1726
|
with _LOCK:
|
|
1718
1727
|
seen = set(_STATE["claims"].keys())
|
|
@@ -1735,6 +1744,17 @@ def _scan(cfg):
|
|
|
1735
1744
|
|
|
1736
1745
|
|
|
1737
1746
|
def _poll(force=False):
|
|
1747
|
+
"""执行一次扫描;同一进程内只允许一个扫描实例。"""
|
|
1748
|
+
if not _SCAN_LOCK.acquire(blocking=False):
|
|
1749
|
+
return {"ok": False, "claimed": 0, "reconciled": False,
|
|
1750
|
+
"error": "扫描正在进行,请稍候", "skipped": "in_progress"}
|
|
1751
|
+
try:
|
|
1752
|
+
return _poll_unlocked(force=force)
|
|
1753
|
+
finally:
|
|
1754
|
+
_SCAN_LOCK.release()
|
|
1755
|
+
|
|
1756
|
+
|
|
1757
|
+
def _poll_unlocked(force=False):
|
|
1738
1758
|
"""一次完整轮询:对账回写 + (到点/强制时)扫描认领。异常不外抛。"""
|
|
1739
1759
|
_ensure_loaded()
|
|
1740
1760
|
with _LOCK:
|
package/app/ui/app.js
CHANGED
|
@@ -6377,18 +6377,18 @@ function logLiveBadge(live) {
|
|
|
6377
6377
|
}
|
|
6378
6378
|
}
|
|
6379
6379
|
|
|
6380
|
-
/* 复制抽屉日志全文:报错 JSON 动辄几十行还带横向滚动,手动圈选很折磨 */
|
|
6381
|
-
window.rdLogCopy = function () {
|
|
6382
|
-
const pre = $("rd-log-text");
|
|
6383
|
-
const text = ((pre && pre.textContent) || "").trim();
|
|
6384
|
-
const idle = [t("(等待输出…)"), t("(无输出)")];
|
|
6385
|
-
if (!text || idle.includes(text)) return;
|
|
6386
|
-
const done = () => toast(t("已复制"));
|
|
6387
|
-
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
6388
|
-
navigator.clipboard.writeText(text).then(done, () => fallbackCopy(text, done));
|
|
6389
|
-
} else fallbackCopy(text, done);
|
|
6390
|
-
};
|
|
6391
|
-
|
|
6380
|
+
/* 复制抽屉日志全文:报错 JSON 动辄几十行还带横向滚动,手动圈选很折磨 */
|
|
6381
|
+
window.rdLogCopy = function () {
|
|
6382
|
+
const pre = $("rd-log-text");
|
|
6383
|
+
const text = ((pre && pre.textContent) || "").trim();
|
|
6384
|
+
const idle = [t("(等待输出…)"), t("(无输出)")];
|
|
6385
|
+
if (!text || idle.includes(text)) return;
|
|
6386
|
+
const done = () => toast(t("已复制"));
|
|
6387
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
6388
|
+
navigator.clipboard.writeText(text).then(done, () => fallbackCopy(text, done));
|
|
6389
|
+
} else fallbackCopy(text, done);
|
|
6390
|
+
};
|
|
6391
|
+
|
|
6392
6392
|
/* 抽屉标题:按 (runId, 日志路径) 反查步骤角色与执行者——不知道在看谁的日志,
|
|
6393
6393
|
* 抽屉就成了无名黑框 */
|
|
6394
6394
|
function rdLogStepLabel(runId, rel) {
|
|
@@ -9240,11 +9240,15 @@ function autoTplPace(tp) {
|
|
|
9240
9240
|
});
|
|
9241
9241
|
}
|
|
9242
9242
|
|
|
9243
|
-
/* last_status → 徽章:started=正常灰、error=红「拉起失败」、missed
|
|
9243
|
+
/* last_status → 徽章:started=正常灰、error=红「拉起失败」、missed=黄「已错过」;
|
|
9244
|
+
* done/failed/cancelled=运行终态(2026-09-23 回写接线后会出现,失败必须可见) */
|
|
9244
9245
|
function autoStatusTag(tsk) {
|
|
9245
9246
|
if (tsk.last_status === "error") return '<span class="tag auto-tag-err">' + t("拉起失败") + "</span>";
|
|
9246
9247
|
if (tsk.last_status === "missed") return '<span class="tag auto-tag-miss">' + t("已错过") + "</span>";
|
|
9247
9248
|
if (tsk.last_status === "started" || tsk.last_status === "queued") return '<span class="tag">' + t("正常") + "</span>";
|
|
9249
|
+
if (tsk.last_status === "failed") return '<span class="tag auto-tag-err">' + t("上次失败") + "</span>";
|
|
9250
|
+
if (tsk.last_status === "cancelled") return '<span class="tag auto-tag-miss">' + t("已取消") + "</span>";
|
|
9251
|
+
if (tsk.last_status === "done") return '<span class="tag">' + t("已完成") + "</span>";
|
|
9248
9252
|
return "";
|
|
9249
9253
|
}
|
|
9250
9254
|
|
|
@@ -11327,6 +11331,12 @@ function pickLang(lang) {
|
|
|
11327
11331
|
window.toggleLangMenu = toggleLangMenu;
|
|
11328
11332
|
window.pickLang = pickLang;
|
|
11329
11333
|
|
|
11334
|
+
/* 顶栏大屏入口:新标签页打开任务驾驶舱;同源共享 orch.token,board 页免二次输令牌 */
|
|
11335
|
+
function openBoard() {
|
|
11336
|
+
window.open("/board.html", "_blank", "noopener");
|
|
11337
|
+
}
|
|
11338
|
+
window.openBoard = openBoard;
|
|
11339
|
+
|
|
11330
11340
|
function setLangBtn(lang) {
|
|
11331
11341
|
if (getLang() === lang) return;
|
|
11332
11342
|
setLang(lang);
|