codebee 0.1.53 → 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 +17 -0
- package/README.md +3 -4
- package/app/core/board.py +209 -0
- package/app/core/builtin_agent.py +67 -26
- package/app/core/chaptersafe.py +32 -0
- package/app/core/jobs.py +22 -3
- package/app/core/knowledge.py +21 -4
- package/app/core/modelhub.py +33 -4
- package/app/core/pipeline.py +238 -66
- package/app/core/planner.py +27 -13
- package/app/core/runner.py +356 -142
- package/app/core/share_page.py +4 -1
- package/app/core/store.py +68 -28
- package/app/core/zentao.py +26 -6
- package/app/main.py +4 -1
- package/app/ui/app.js +13416 -13324
- package/app/ui/board.css +390 -0
- package/app/ui/board.html +162 -0
- package/app/ui/board.js +305 -0
- package/app/ui/i18n.js +2698 -2635
- package/app/ui/index.html +4 -0
- package/app/ui/style.css +17 -0
- package/package.json +1 -1
package/app/core/share_page.py
CHANGED
|
@@ -87,12 +87,15 @@ def render_share_html(run, task, report_text):
|
|
|
87
87
|
|
|
88
88
|
steps_rows = ""
|
|
89
89
|
for s in run.get("steps") or []:
|
|
90
|
+
who = s.get("agent_label") or s.get("agent") or ""
|
|
91
|
+
if s.get("model"):
|
|
92
|
+
who += " · " + str(s["model"])
|
|
90
93
|
steps_rows += (
|
|
91
94
|
'<div class="step"><span class="n">%02d</span>'
|
|
92
95
|
'<span class="role">%s</span><span class="who">%s</span>'
|
|
93
96
|
'<span class="sum">%s</span><span class="st %s">%s</span></div>'
|
|
94
97
|
% (s.get("n") or 0, _esc(s.get("role") or ""),
|
|
95
|
-
_esc(
|
|
98
|
+
_esc(who),
|
|
96
99
|
_esc((s.get("summary") or "")[:220]), _esc(s.get("status") or ""),
|
|
97
100
|
_esc(s.get("status") or "")))
|
|
98
101
|
|
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)
|
|
@@ -1537,7 +1574,7 @@ def update_task_params(task_id, patch):
|
|
|
1537
1574
|
return True, ""
|
|
1538
1575
|
|
|
1539
1576
|
|
|
1540
|
-
def add_step(run_id, role, agent_id, agent_label, note=""):
|
|
1577
|
+
def add_step(run_id, role, agent_id, agent_label, note="", model=""):
|
|
1541
1578
|
with LOCK:
|
|
1542
1579
|
run = _RUNS.get(run_id)
|
|
1543
1580
|
if not run:
|
|
@@ -1546,6 +1583,9 @@ def add_step(run_id, role, agent_id, agent_label, note=""):
|
|
|
1546
1583
|
step = {
|
|
1547
1584
|
"n": n, "role": role, "agent": agent_id, "agent_label": agent_label,
|
|
1548
1585
|
"note": note,
|
|
1586
|
+
# 起跑即记解析到的模型:运行中蜂巢卡/步骤列表就能看到「在用哪个模型」,
|
|
1587
|
+
# 不必等 finish_step 用实际结果覆盖(换将成功后以实际为准)
|
|
1588
|
+
"model": str(model or "")[:80],
|
|
1549
1589
|
"status": "running", "started_at": time.strftime("%H:%M:%S"),
|
|
1550
1590
|
"ended_at": None, "duration_s": None, "exit_code": None,
|
|
1551
1591
|
"summary": "", "log": None, "cost_usd": 0.0, "tokens": 0,
|
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/main.py
CHANGED
|
@@ -30,7 +30,7 @@ if os.environ.get("TUTTI_ISOLATE_HOME", "").strip() == "1":
|
|
|
30
30
|
os.environ["USERPROFILE"] = _fake_home
|
|
31
31
|
os.environ["HOME"] = _fake_home
|
|
32
32
|
|
|
33
|
-
from core import automation, catalog, flows, jobs, manager, market, market_remote, preview, registry, remote, settings, store
|
|
33
|
+
from core import automation, board, catalog, flows, jobs, manager, market, market_remote, preview, registry, remote, settings, store
|
|
34
34
|
from core import paths
|
|
35
35
|
from core import health
|
|
36
36
|
import pick_dialog
|
|
@@ -235,6 +235,9 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
235
235
|
if path == "/api/health":
|
|
236
236
|
from core import health
|
|
237
237
|
return self._json(200, health.snapshot())
|
|
238
|
+
if path == "/api/board":
|
|
239
|
+
# 任务驾驶舱大屏的轻量聚合(KB 级;别让它去拉 /api/state 全量)
|
|
240
|
+
return self._json(200, board.payload())
|
|
238
241
|
if path == "/api/connect":
|
|
239
242
|
# 供设置页「手机连接」弹框生成二维码;远程打开需令牌,天然受保护
|
|
240
243
|
return self._json(200, {"urls": remote.build_connect_urls(PORT),
|