codebee 0.1.66 → 0.1.68
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 +543 -520
- package/LICENSE +21 -21
- package/README.md +11 -7
- package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +88 -0
- package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +126 -0
- package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +210 -0
- package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
- package/app/.playwright-cli/run-detail-after.png +0 -0
- package/app/core/acceptance.py +100 -0
- package/app/core/aiflavor.py +197 -197
- package/app/core/artifacts.py +148 -148
- package/app/core/assets.py +110 -0
- package/app/core/attachments.py +623 -532
- package/app/core/automation.py +758 -758
- package/app/core/backup.py +470 -470
- package/app/core/beekeeper.py +183 -183
- package/app/core/board.py +209 -209
- package/app/core/bookmeta.py +930 -796
- package/app/core/bookmeta_catalog.py +180 -180
- package/app/core/branching.py +137 -137
- package/app/core/builtin_agent.py +1234 -1234
- package/app/core/capability.py +144 -144
- package/app/core/catalog.py +42 -0
- package/app/core/chaptersafe.py +32 -32
- package/app/core/cleanup.py +303 -303
- package/app/core/compaction.py +201 -201
- package/app/core/covergen.py +356 -356
- package/app/core/defectretro.py +43 -0
- package/app/core/diagnostics.py +114 -114
- package/app/core/dispatch.py +173 -173
- package/app/core/dispatch_log.py +128 -113
- package/app/core/env_scrub.py +85 -85
- package/app/core/error_codes.py +23 -23
- package/app/core/errorlog.py +137 -137
- package/app/core/evaluation.py +174 -174
- package/app/core/failure_notify.py +290 -290
- package/app/core/flows.py +48 -2
- package/app/core/goal_service.py +167 -167
- package/app/core/health.py +374 -374
- package/app/core/history.py +38 -38
- package/app/core/hooks.py +162 -162
- package/app/core/jobs.py +46 -26
- package/app/core/knowledge.py +57 -3
- package/app/core/manager.py +2122 -2115
- package/app/core/market.py +362 -362
- package/app/core/market_remote.py +124 -26
- package/app/core/mocks.py +64 -64
- package/app/core/modelhub.py +51 -3
- package/app/core/notify.py +106 -106
- package/app/core/operations.py +228 -0
- package/app/core/paihang.py +138 -109
- package/app/core/paths.py +2 -2
- package/app/core/pipeline.py +221 -12
- package/app/core/planner.py +6 -1
- package/app/core/portguard.py +111 -111
- package/app/core/preview.py +293 -293
- package/app/core/publish/__init__.py +7 -7
- package/app/core/publish/auto.py +367 -367
- package/app/core/publish/browser.py +539 -533
- package/app/core/publish/fanqie.py +159 -128
- package/app/core/publish/flow.py +440 -432
- package/app/core/publish/flows-fanqie-calibrated.json +332 -332
- package/app/core/publish/flows-qimao-calibrated.json +7 -3
- package/app/core/publish/ledger.py +203 -198
- package/app/core/publish/manager.py +692 -623
- package/app/core/publish/qimao.py +141 -141
- package/app/core/publish/ws.py +139 -139
- package/app/core/redact.py +38 -38
- package/app/core/repeat_guard.py +129 -129
- package/app/core/revisions.py +51 -0
- package/app/core/router.py +398 -398
- package/app/core/runner.py +2067 -2050
- package/app/core/selfupdate.py +384 -384
- package/app/core/session_log.py +161 -161
- package/app/core/sessions.py +312 -312
- package/app/core/settings.py +149 -149
- package/app/core/settings_schema.py +262 -262
- package/app/core/share_page.py +114 -114
- package/app/core/skill_scan.py +122 -114
- package/app/core/skillpacks/fanqie-novel.md +80 -80
- package/app/core/skillpacks/market/character-bible.md +66 -66
- package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
- package/app/core/skillpacks/market/git-workflow.md +57 -57
- package/app/core/skillpacks/market/release-notes.md +72 -72
- package/app/core/skillpacks/market/weekly-report.md +71 -71
- package/app/core/skillpacks/market/worldview-consistency.md +70 -70
- package/app/core/skills.py +109 -2
- package/app/core/step_runner.py +132 -132
- package/app/core/store.py +278 -41
- package/app/core/task_compile.py +199 -199
- package/app/core/telemetry.py +292 -292
- package/app/core/tlsctx.py +64 -64
- package/app/core/token_meter.py +147 -147
- package/app/core/tracing.py +144 -0
- package/app/core/upstream.py +41 -41
- package/app/core/usage.py +862 -858
- package/app/core/volumes.py +389 -355
- package/app/core/wxdigest.py +858 -858
- package/app/core/zentao.py +2465 -2447
- package/app/main.py +2981 -2838
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +14399 -13994
- package/app/ui/artdiff.js +74 -0
- package/app/ui/board.html +162 -162
- package/app/ui/board.js +305 -305
- package/app/ui/i18n.js +2770 -2716
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +6 -0
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +96 -24
- package/bin/tutti.js +147 -147
- package/package.json +46 -46
- package/tools/wx_reader.py +261 -261
package/app/core/board.py
CHANGED
|
@@ -1,209 +1,209 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""任务驾驶舱(/board 大屏)的轻量聚合数据源。
|
|
3
|
-
|
|
4
|
-
大屏 4 秒轮询:不能拉 /api/state 全量(MB 级),也不能每次全量扫 usage
|
|
5
|
-
台账(summary 全历史读入),所以这里只取大屏要看的字段,并把 usage 聚合
|
|
6
|
-
缓存 8 秒。只读 store/usage/jobs/health,不落新盘。
|
|
7
|
-
"""
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import time
|
|
11
|
-
|
|
12
|
-
_CACHE = {"ts": 0.0, "data": None}
|
|
13
|
-
_TTL = 8.0
|
|
14
|
-
|
|
15
|
-
_TAIL_MAX = 150 # 跑马灯尾行截断
|
|
16
|
-
_TAIL_LINES = 6 # 实时流窗口行数
|
|
17
|
-
_TAIL_LINE_CHARS = 56 # 实时流单行截断(卡内 11px 等宽不换行的安全宽度)
|
|
18
|
-
_RECENT_LIMIT = 10 # 最近完成/失败条数
|
|
19
|
-
_RUN_WINDOW = 300 # 今日计数扫描的运行窗口(id 字典序=时间序)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def payload():
|
|
23
|
-
now = time.time()
|
|
24
|
-
if _CACHE["data"] is not None and now - _CACHE["ts"] < _TTL:
|
|
25
|
-
return _CACHE["data"]
|
|
26
|
-
data = _build()
|
|
27
|
-
_CACHE["ts"] = now
|
|
28
|
-
_CACHE["data"] = data
|
|
29
|
-
return data
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def _tail(step):
|
|
33
|
-
"""步骤实时流(正文优先,其次思考)的最后一行,给大屏跑马灯。"""
|
|
34
|
-
for key in ("stream", "thinking"):
|
|
35
|
-
v = step.get(key)
|
|
36
|
-
if isinstance(v, str) and v.strip():
|
|
37
|
-
lines = [ln.strip() for ln in v.strip().splitlines() if ln.strip()]
|
|
38
|
-
if lines:
|
|
39
|
-
return lines[-1][:_TAIL_MAX]
|
|
40
|
-
return ""
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def _live_block(step):
|
|
44
|
-
"""步骤实时流窗口:尾部多行(正文优先,其次思考),供大屏看它在想什么。
|
|
45
|
-
|
|
46
|
-
返回 (来源标签 code, 文本);标签是 code(output/thinking),由前端 i18n
|
|
47
|
-
翻译——大屏要做多语言,后端不下发中文。都没有返回 ("", "")。
|
|
48
|
-
"""
|
|
49
|
-
for key, code in (("stream", "output"), ("thinking", "thinking")):
|
|
50
|
-
v = step.get(key)
|
|
51
|
-
if not (isinstance(v, str) and v.strip()):
|
|
52
|
-
continue
|
|
53
|
-
lines = [ln.strip()[:_TAIL_LINE_CHARS] for ln in v.strip().splitlines()]
|
|
54
|
-
lines = [ln for ln in lines if ln]
|
|
55
|
-
if lines:
|
|
56
|
-
return code, "\n".join(lines[-_TAIL_LINES:])
|
|
57
|
-
return "", ""
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def _model_provider_map():
|
|
61
|
-
"""模型名 → 厂商名(enabled 优先,同模型多厂商取 priority 最靠前的)。"""
|
|
62
|
-
try:
|
|
63
|
-
from . import modelhub
|
|
64
|
-
rows = modelhub.models_view()
|
|
65
|
-
except Exception:
|
|
66
|
-
return {}
|
|
67
|
-
best = {}
|
|
68
|
-
for r in rows:
|
|
69
|
-
name = r.get("name")
|
|
70
|
-
if not name or not r.get("enabled", False):
|
|
71
|
-
continue
|
|
72
|
-
pri = r.get("priority", 0)
|
|
73
|
-
cur = best.get(name)
|
|
74
|
-
if cur is None or pri < cur[0]:
|
|
75
|
-
best[name] = (pri, r.get("provider_name") or r.get("provider_id") or "")
|
|
76
|
-
return {k: v[1] for k, v in best.items()}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def _parse_ts(s):
|
|
80
|
-
try:
|
|
81
|
-
return time.mktime(time.strptime(str(s), "%Y-%m-%d %H:%M:%S"))
|
|
82
|
-
except (ValueError, TypeError):
|
|
83
|
-
return 0.0
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def _duration_s(run):
|
|
87
|
-
"""运行耗时(秒):ended-started,缺 started 用 created 兜底。"""
|
|
88
|
-
end = _parse_ts(run.get("ended_at"))
|
|
89
|
-
start = _parse_ts(run.get("started_at")) or _parse_ts(run.get("created_at"))
|
|
90
|
-
if end > 0 and start > 0 and end >= start:
|
|
91
|
-
return int(end - start)
|
|
92
|
-
return None
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def _step_view(steps, prov_map):
|
|
96
|
-
"""进度概要:完成数 + 当前运行中步骤(含模型/厂商/实时流)。"""
|
|
97
|
-
done = sum(1 for s in steps if s.get("status") == "done")
|
|
98
|
-
cur = next((s for s in steps if s.get("status") == "running"), None)
|
|
99
|
-
cur_v = None
|
|
100
|
-
if cur:
|
|
101
|
-
live_label, live_text = _live_block(cur)
|
|
102
|
-
model = str(cur.get("model") or "")[:60]
|
|
103
|
-
cur_v = {"n": cur.get("n"), "role": cur.get("role") or "",
|
|
104
|
-
"agent": cur.get("agent_label") or cur.get("agent") or "",
|
|
105
|
-
"summary": (cur.get("summary") or "")[:80],
|
|
106
|
-
"tail": _tail(cur),
|
|
107
|
-
"model": model, "provider": prov_map.get(model, "") if model else "",
|
|
108
|
-
"live_label": live_label, "live_text": live_text}
|
|
109
|
-
return done, len(steps), cur_v
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def _build():
|
|
113
|
-
from . import flows, health, jobs, store, usage
|
|
114
|
-
|
|
115
|
-
flow_map = {}
|
|
116
|
-
try:
|
|
117
|
-
for f in flows.list_flows():
|
|
118
|
-
flow_map[f.get("id")] = f
|
|
119
|
-
except Exception:
|
|
120
|
-
pass
|
|
121
|
-
|
|
122
|
-
prov_map = _model_provider_map()
|
|
123
|
-
today = time.strftime("%Y-%m-%d")
|
|
124
|
-
tasks = store.list_tasks(200, archived=False)
|
|
125
|
-
latest = store.latest_run_by_task()
|
|
126
|
-
|
|
127
|
-
running, queued = [], []
|
|
128
|
-
for t in tasks:
|
|
129
|
-
tid = t.get("id")
|
|
130
|
-
run = latest.get(tid)
|
|
131
|
-
status = t.get("status")
|
|
132
|
-
if status == "running" and run and run.get("status") == "running":
|
|
133
|
-
done, total, cur = _step_view(run.get("steps") or [], prov_map)
|
|
134
|
-
running.append({
|
|
135
|
-
"task_id": tid, "run_id": run.get("id"),
|
|
136
|
-
"title": t.get("title") or "", "goal": (t.get("goal") or "")[:120],
|
|
137
|
-
"type": t.get("type") or "",
|
|
138
|
-
"type_name": (flow_map.get(t.get("type")) or {}).get("name") or (t.get("type") or ""),
|
|
139
|
-
"status": status,
|
|
140
|
-
"started_at": run.get("started_at") or run.get("created_at") or "",
|
|
141
|
-
"steps_done": done, "steps_total": total,
|
|
142
|
-
"current": cur,
|
|
143
|
-
"run_tokens": run.get("tokens") or 0,
|
|
144
|
-
"run_cost_usd": round(float(run.get("cost_usd") or 0.0), 4),
|
|
145
|
-
"estimate_s": run.get("estimated_duration_s"),
|
|
146
|
-
})
|
|
147
|
-
elif status == "queued":
|
|
148
|
-
queued.append({
|
|
149
|
-
"task_id": tid, "run_id": (run or {}).get("id"),
|
|
150
|
-
"title": t.get("title") or "",
|
|
151
|
-
"type_name": (flow_map.get(t.get("type")) or {}).get("name") or (t.get("type") or ""),
|
|
152
|
-
"created_at": (run or {}).get("created_at") or "",
|
|
153
|
-
})
|
|
154
|
-
running.sort(key=lambda c: _parse_ts(c["started_at"]))
|
|
155
|
-
queued.sort(key=lambda c: _parse_ts(c["created_at"]))
|
|
156
|
-
|
|
157
|
-
runs = store.list_runs(_RUN_WINDOW)
|
|
158
|
-
today_done = today_failed = 0
|
|
159
|
-
recent = []
|
|
160
|
-
for r in runs:
|
|
161
|
-
st = r.get("status")
|
|
162
|
-
ended = str(r.get("ended_at") or "")
|
|
163
|
-
if st in ("done", "failed", "cancelled") and ended.startswith(today):
|
|
164
|
-
if st == "done":
|
|
165
|
-
today_done += 1
|
|
166
|
-
elif st == "failed":
|
|
167
|
-
today_failed += 1
|
|
168
|
-
if len(recent) < _RECENT_LIMIT and st in ("done", "failed", "cancelled") and ended:
|
|
169
|
-
recent.append({
|
|
170
|
-
"run_id": r.get("id"), "task_id": r.get("task_id"),
|
|
171
|
-
"title": r.get("title") or "", "status": st,
|
|
172
|
-
"ended_at": ended, "duration_s": _duration_s(r),
|
|
173
|
-
"error": (r.get("error") or "")[:80],
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
trend, models, usage_today = [], [], {"tokens": 0, "calls": 0, "cost_usd": 0.0}
|
|
177
|
-
try:
|
|
178
|
-
s = usage.summary(days=7)
|
|
179
|
-
trend = [{"day": d.get("day"), "tokens": d.get("tokens") or 0}
|
|
180
|
-
for d in (s.get("by_day") or [])]
|
|
181
|
-
for row in (s.get("by_model") or [])[:5]:
|
|
182
|
-
if not row.get("tokens"):
|
|
183
|
-
continue
|
|
184
|
-
models.append({"model": row.get("key") or "?",
|
|
185
|
-
"tokens": row.get("tokens") or 0,
|
|
186
|
-
"calls": row.get("calls") or 0})
|
|
187
|
-
for d in s.get("by_day") or []:
|
|
188
|
-
if str(d.get("day")) == today:
|
|
189
|
-
usage_today = {"tokens": d.get("tokens") or 0,
|
|
190
|
-
"calls": d.get("calls") or 0,
|
|
191
|
-
"cost_usd": round(float(d.get("cost_usd") or 0.0), 3)}
|
|
192
|
-
break
|
|
193
|
-
except Exception:
|
|
194
|
-
pass
|
|
195
|
-
|
|
196
|
-
return {
|
|
197
|
-
"now": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
198
|
-
"today": today,
|
|
199
|
-
"running": running,
|
|
200
|
-
"queued": queued,
|
|
201
|
-
"today_done": today_done,
|
|
202
|
-
"today_failed": today_failed,
|
|
203
|
-
"recent": recent,
|
|
204
|
-
"trend": trend,
|
|
205
|
-
"models": models,
|
|
206
|
-
"usage_today": usage_today,
|
|
207
|
-
"pool": jobs.workers_info(),
|
|
208
|
-
"health": health.snapshot().get("providers") or [],
|
|
209
|
-
}
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""任务驾驶舱(/board 大屏)的轻量聚合数据源。
|
|
3
|
+
|
|
4
|
+
大屏 4 秒轮询:不能拉 /api/state 全量(MB 级),也不能每次全量扫 usage
|
|
5
|
+
台账(summary 全历史读入),所以这里只取大屏要看的字段,并把 usage 聚合
|
|
6
|
+
缓存 8 秒。只读 store/usage/jobs/health,不落新盘。
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
_CACHE = {"ts": 0.0, "data": None}
|
|
13
|
+
_TTL = 8.0
|
|
14
|
+
|
|
15
|
+
_TAIL_MAX = 150 # 跑马灯尾行截断
|
|
16
|
+
_TAIL_LINES = 6 # 实时流窗口行数
|
|
17
|
+
_TAIL_LINE_CHARS = 56 # 实时流单行截断(卡内 11px 等宽不换行的安全宽度)
|
|
18
|
+
_RECENT_LIMIT = 10 # 最近完成/失败条数
|
|
19
|
+
_RUN_WINDOW = 300 # 今日计数扫描的运行窗口(id 字典序=时间序)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def payload():
|
|
23
|
+
now = time.time()
|
|
24
|
+
if _CACHE["data"] is not None and now - _CACHE["ts"] < _TTL:
|
|
25
|
+
return _CACHE["data"]
|
|
26
|
+
data = _build()
|
|
27
|
+
_CACHE["ts"] = now
|
|
28
|
+
_CACHE["data"] = data
|
|
29
|
+
return data
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _tail(step):
|
|
33
|
+
"""步骤实时流(正文优先,其次思考)的最后一行,给大屏跑马灯。"""
|
|
34
|
+
for key in ("stream", "thinking"):
|
|
35
|
+
v = step.get(key)
|
|
36
|
+
if isinstance(v, str) and v.strip():
|
|
37
|
+
lines = [ln.strip() for ln in v.strip().splitlines() if ln.strip()]
|
|
38
|
+
if lines:
|
|
39
|
+
return lines[-1][:_TAIL_MAX]
|
|
40
|
+
return ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _live_block(step):
|
|
44
|
+
"""步骤实时流窗口:尾部多行(正文优先,其次思考),供大屏看它在想什么。
|
|
45
|
+
|
|
46
|
+
返回 (来源标签 code, 文本);标签是 code(output/thinking),由前端 i18n
|
|
47
|
+
翻译——大屏要做多语言,后端不下发中文。都没有返回 ("", "")。
|
|
48
|
+
"""
|
|
49
|
+
for key, code in (("stream", "output"), ("thinking", "thinking")):
|
|
50
|
+
v = step.get(key)
|
|
51
|
+
if not (isinstance(v, str) and v.strip()):
|
|
52
|
+
continue
|
|
53
|
+
lines = [ln.strip()[:_TAIL_LINE_CHARS] for ln in v.strip().splitlines()]
|
|
54
|
+
lines = [ln for ln in lines if ln]
|
|
55
|
+
if lines:
|
|
56
|
+
return code, "\n".join(lines[-_TAIL_LINES:])
|
|
57
|
+
return "", ""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _model_provider_map():
|
|
61
|
+
"""模型名 → 厂商名(enabled 优先,同模型多厂商取 priority 最靠前的)。"""
|
|
62
|
+
try:
|
|
63
|
+
from . import modelhub
|
|
64
|
+
rows = modelhub.models_view()
|
|
65
|
+
except Exception:
|
|
66
|
+
return {}
|
|
67
|
+
best = {}
|
|
68
|
+
for r in rows:
|
|
69
|
+
name = r.get("name")
|
|
70
|
+
if not name or not r.get("enabled", False):
|
|
71
|
+
continue
|
|
72
|
+
pri = r.get("priority", 0)
|
|
73
|
+
cur = best.get(name)
|
|
74
|
+
if cur is None or pri < cur[0]:
|
|
75
|
+
best[name] = (pri, r.get("provider_name") or r.get("provider_id") or "")
|
|
76
|
+
return {k: v[1] for k, v in best.items()}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _parse_ts(s):
|
|
80
|
+
try:
|
|
81
|
+
return time.mktime(time.strptime(str(s), "%Y-%m-%d %H:%M:%S"))
|
|
82
|
+
except (ValueError, TypeError):
|
|
83
|
+
return 0.0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _duration_s(run):
|
|
87
|
+
"""运行耗时(秒):ended-started,缺 started 用 created 兜底。"""
|
|
88
|
+
end = _parse_ts(run.get("ended_at"))
|
|
89
|
+
start = _parse_ts(run.get("started_at")) or _parse_ts(run.get("created_at"))
|
|
90
|
+
if end > 0 and start > 0 and end >= start:
|
|
91
|
+
return int(end - start)
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _step_view(steps, prov_map):
|
|
96
|
+
"""进度概要:完成数 + 当前运行中步骤(含模型/厂商/实时流)。"""
|
|
97
|
+
done = sum(1 for s in steps if s.get("status") == "done")
|
|
98
|
+
cur = next((s for s in steps if s.get("status") == "running"), None)
|
|
99
|
+
cur_v = None
|
|
100
|
+
if cur:
|
|
101
|
+
live_label, live_text = _live_block(cur)
|
|
102
|
+
model = str(cur.get("model") or "")[:60]
|
|
103
|
+
cur_v = {"n": cur.get("n"), "role": cur.get("role") or "",
|
|
104
|
+
"agent": cur.get("agent_label") or cur.get("agent") or "",
|
|
105
|
+
"summary": (cur.get("summary") or "")[:80],
|
|
106
|
+
"tail": _tail(cur),
|
|
107
|
+
"model": model, "provider": prov_map.get(model, "") if model else "",
|
|
108
|
+
"live_label": live_label, "live_text": live_text}
|
|
109
|
+
return done, len(steps), cur_v
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _build():
|
|
113
|
+
from . import flows, health, jobs, store, usage
|
|
114
|
+
|
|
115
|
+
flow_map = {}
|
|
116
|
+
try:
|
|
117
|
+
for f in flows.list_flows():
|
|
118
|
+
flow_map[f.get("id")] = f
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
prov_map = _model_provider_map()
|
|
123
|
+
today = time.strftime("%Y-%m-%d")
|
|
124
|
+
tasks = store.list_tasks(200, archived=False)
|
|
125
|
+
latest = store.latest_run_by_task()
|
|
126
|
+
|
|
127
|
+
running, queued = [], []
|
|
128
|
+
for t in tasks:
|
|
129
|
+
tid = t.get("id")
|
|
130
|
+
run = latest.get(tid)
|
|
131
|
+
status = t.get("status")
|
|
132
|
+
if status == "running" and run and run.get("status") == "running":
|
|
133
|
+
done, total, cur = _step_view(run.get("steps") or [], prov_map)
|
|
134
|
+
running.append({
|
|
135
|
+
"task_id": tid, "run_id": run.get("id"),
|
|
136
|
+
"title": t.get("title") or "", "goal": (t.get("goal") or "")[:120],
|
|
137
|
+
"type": t.get("type") or "",
|
|
138
|
+
"type_name": (flow_map.get(t.get("type")) or {}).get("name") or (t.get("type") or ""),
|
|
139
|
+
"status": status,
|
|
140
|
+
"started_at": run.get("started_at") or run.get("created_at") or "",
|
|
141
|
+
"steps_done": done, "steps_total": total,
|
|
142
|
+
"current": cur,
|
|
143
|
+
"run_tokens": run.get("tokens") or 0,
|
|
144
|
+
"run_cost_usd": round(float(run.get("cost_usd") or 0.0), 4),
|
|
145
|
+
"estimate_s": run.get("estimated_duration_s"),
|
|
146
|
+
})
|
|
147
|
+
elif status == "queued":
|
|
148
|
+
queued.append({
|
|
149
|
+
"task_id": tid, "run_id": (run or {}).get("id"),
|
|
150
|
+
"title": t.get("title") or "",
|
|
151
|
+
"type_name": (flow_map.get(t.get("type")) or {}).get("name") or (t.get("type") or ""),
|
|
152
|
+
"created_at": (run or {}).get("created_at") or "",
|
|
153
|
+
})
|
|
154
|
+
running.sort(key=lambda c: _parse_ts(c["started_at"]))
|
|
155
|
+
queued.sort(key=lambda c: _parse_ts(c["created_at"]))
|
|
156
|
+
|
|
157
|
+
runs = store.list_runs(_RUN_WINDOW)
|
|
158
|
+
today_done = today_failed = 0
|
|
159
|
+
recent = []
|
|
160
|
+
for r in runs:
|
|
161
|
+
st = r.get("status")
|
|
162
|
+
ended = str(r.get("ended_at") or "")
|
|
163
|
+
if st in ("done", "failed", "cancelled") and ended.startswith(today):
|
|
164
|
+
if st == "done":
|
|
165
|
+
today_done += 1
|
|
166
|
+
elif st == "failed":
|
|
167
|
+
today_failed += 1
|
|
168
|
+
if len(recent) < _RECENT_LIMIT and st in ("done", "failed", "cancelled") and ended:
|
|
169
|
+
recent.append({
|
|
170
|
+
"run_id": r.get("id"), "task_id": r.get("task_id"),
|
|
171
|
+
"title": r.get("title") or "", "status": st,
|
|
172
|
+
"ended_at": ended, "duration_s": _duration_s(r),
|
|
173
|
+
"error": (r.get("error") or "")[:80],
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
trend, models, usage_today = [], [], {"tokens": 0, "calls": 0, "cost_usd": 0.0}
|
|
177
|
+
try:
|
|
178
|
+
s = usage.summary(days=7)
|
|
179
|
+
trend = [{"day": d.get("day"), "tokens": d.get("tokens") or 0}
|
|
180
|
+
for d in (s.get("by_day") or [])]
|
|
181
|
+
for row in (s.get("by_model") or [])[:5]:
|
|
182
|
+
if not row.get("tokens"):
|
|
183
|
+
continue
|
|
184
|
+
models.append({"model": row.get("key") or "?",
|
|
185
|
+
"tokens": row.get("tokens") or 0,
|
|
186
|
+
"calls": row.get("calls") or 0})
|
|
187
|
+
for d in s.get("by_day") or []:
|
|
188
|
+
if str(d.get("day")) == today:
|
|
189
|
+
usage_today = {"tokens": d.get("tokens") or 0,
|
|
190
|
+
"calls": d.get("calls") or 0,
|
|
191
|
+
"cost_usd": round(float(d.get("cost_usd") or 0.0), 3)}
|
|
192
|
+
break
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
"now": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
198
|
+
"today": today,
|
|
199
|
+
"running": running,
|
|
200
|
+
"queued": queued,
|
|
201
|
+
"today_done": today_done,
|
|
202
|
+
"today_failed": today_failed,
|
|
203
|
+
"recent": recent,
|
|
204
|
+
"trend": trend,
|
|
205
|
+
"models": models,
|
|
206
|
+
"usage_today": usage_today,
|
|
207
|
+
"pool": jobs.workers_info(),
|
|
208
|
+
"health": health.snapshot().get("providers") or [],
|
|
209
|
+
}
|