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/store.py
CHANGED
|
@@ -18,7 +18,7 @@ import threading
|
|
|
18
18
|
import time
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
|
|
21
|
-
from . import artifacts, paths, runner, volumes
|
|
21
|
+
from . import artifacts, paths, runner, tracing, volumes
|
|
22
22
|
|
|
23
23
|
LOCK = threading.RLock()
|
|
24
24
|
_TASKS = {}
|
|
@@ -30,9 +30,12 @@ _STREAM_LOCK = threading.Lock()
|
|
|
30
30
|
_STREAM_TS = {}
|
|
31
31
|
LIVE_TEXT_MAX = 20000 # 单步骤实时思考/正文的保留上限(与 builtin_agent 同口径)
|
|
32
32
|
|
|
33
|
-
DEFAULT_TASK_TIMEOUT_S = 3600
|
|
34
|
-
MIN_TASK_TIMEOUT_S = 1
|
|
35
|
-
MAX_TASK_TIMEOUT_S = 7 * 24 * 3600
|
|
33
|
+
DEFAULT_TASK_TIMEOUT_S = 3600
|
|
34
|
+
MIN_TASK_TIMEOUT_S = 1
|
|
35
|
+
MAX_TASK_TIMEOUT_S = 7 * 24 * 3600
|
|
36
|
+
REVIEW_TASK_TIMEOUT_S = 7200
|
|
37
|
+
LONG_TASK_TIMEOUT_S = 14400
|
|
38
|
+
SERIAL_EXTRA_CHAPTER_S = 1800
|
|
36
39
|
|
|
37
40
|
# ---------------------------------------------------------------- 状态版本(SSE 事件驱动)
|
|
38
41
|
# 任何落盘写都算状态变化:SSE 连接等版本号变化才构建/推送全量状态,
|
|
@@ -83,7 +86,7 @@ def _safe_name(s):
|
|
|
83
86
|
return re.sub(r"[^0-9A-Za-z_-]+", "-", str(s))[:40].strip("-") or "x"
|
|
84
87
|
|
|
85
88
|
|
|
86
|
-
def _task_timeout_s(value=None):
|
|
89
|
+
def _task_timeout_s(value=None):
|
|
87
90
|
"""Resolve one bounded wall-clock budget for a task."""
|
|
88
91
|
raw = value
|
|
89
92
|
if raw in (None, ""):
|
|
@@ -92,7 +95,27 @@ def _task_timeout_s(value=None):
|
|
|
92
95
|
seconds = int(float(raw))
|
|
93
96
|
except (TypeError, ValueError):
|
|
94
97
|
seconds = DEFAULT_TASK_TIMEOUT_S
|
|
95
|
-
return max(MIN_TASK_TIMEOUT_S, min(MAX_TASK_TIMEOUT_S, seconds))
|
|
98
|
+
return max(MIN_TASK_TIMEOUT_S, min(MAX_TASK_TIMEOUT_S, seconds))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _default_timeout_s(task):
|
|
102
|
+
"""Choose a bounded default from the amount of work, not one hour for all."""
|
|
103
|
+
env_value = os.environ.get("TUTTI_TASK_TIMEOUT_S")
|
|
104
|
+
if env_value not in (None, ""):
|
|
105
|
+
return _task_timeout_s(env_value)
|
|
106
|
+
serial = task.get("serial")
|
|
107
|
+
if isinstance(serial, dict) and serial.get("chapters"):
|
|
108
|
+
try:
|
|
109
|
+
chapters = max(1, min(20, int(serial["chapters"])))
|
|
110
|
+
except (TypeError, ValueError):
|
|
111
|
+
chapters = 8
|
|
112
|
+
return min(MAX_TASK_TIMEOUT_S,
|
|
113
|
+
LONG_TASK_TIMEOUT_S + max(0, chapters - 8) * SERIAL_EXTRA_CHAPTER_S)
|
|
114
|
+
if task.get("engine") == "code":
|
|
115
|
+
return LONG_TASK_TIMEOUT_S
|
|
116
|
+
if task.get("engine") == "review":
|
|
117
|
+
return REVIEW_TASK_TIMEOUT_S
|
|
118
|
+
return DEFAULT_TASK_TIMEOUT_S
|
|
96
119
|
|
|
97
120
|
|
|
98
121
|
# ---------------------------------------------------------------- 任务
|
|
@@ -159,7 +182,19 @@ def create_task(payload):
|
|
|
159
182
|
}
|
|
160
183
|
thinking = str(payload.get("thinking") or "standard").strip().lower()
|
|
161
184
|
task["thinking"] = thinking if thinking in ("auto", "low", "standard", "high") else "auto"
|
|
162
|
-
|
|
185
|
+
# 流程修订出处(借鉴 WorkDSH ADR-0010):创建时钉住当时流程定义的指纹。
|
|
186
|
+
# 固化参数已保证「改流程不影响本任务」,digest 再补上可对账的一面——
|
|
187
|
+
# 续跑/重试时若流程已改,flows.flow_drift 能给出明确诊断而非静默换新。
|
|
188
|
+
flow_content_sha = flows_mod.flow_content_sha256(flow)
|
|
189
|
+
task["flow_snapshot"] = {"id": flow["id"], "name": (flow.get("name") or "")[:40],
|
|
190
|
+
"engine": flow["engine"],
|
|
191
|
+
"edited": bool(flow.get("edited")),
|
|
192
|
+
"digest": flow_content_sha[:16],
|
|
193
|
+
"revision_id": "flowrev-" + flow_content_sha[:16],
|
|
194
|
+
"content_sha256": flow_content_sha}
|
|
195
|
+
# 参数修订号(借鉴 WorkDSH 契约纪律 expectedRevision):写接口据此做过期
|
|
196
|
+
# 写入检测。老任务无此键按 1 处理;不带 expected_rev 的调用不受影响。
|
|
197
|
+
task["rev"] = 1
|
|
163
198
|
if flow["engine"] == "direct":
|
|
164
199
|
task["direct_provider_id"] = _text(payload.get("direct_provider_id"),
|
|
165
200
|
"direct_provider_id")[:80]
|
|
@@ -221,11 +256,11 @@ def create_task(payload):
|
|
|
221
256
|
if serial_value is None or (isinstance(serial_value, dict) and
|
|
222
257
|
serial_value.get("enabled") is False):
|
|
223
258
|
serial = None
|
|
224
|
-
elif isinstance(serial_value, dict):
|
|
225
|
-
serial = serial_value
|
|
226
|
-
else:
|
|
227
|
-
serial = flow.get("serial")
|
|
228
|
-
if isinstance(serial, dict) and serial.get("chapters"):
|
|
259
|
+
elif isinstance(serial_value, dict):
|
|
260
|
+
serial = serial_value
|
|
261
|
+
else:
|
|
262
|
+
serial = flow.get("serial")
|
|
263
|
+
if isinstance(serial, dict) and serial.get("chapters"):
|
|
229
264
|
try:
|
|
230
265
|
s = {
|
|
231
266
|
# 续写批次允许只续 1 章,下限放宽到 1(全新连载仍由前端约束 ≥2)
|
|
@@ -277,10 +312,13 @@ def create_task(payload):
|
|
|
277
312
|
if vol_spec:
|
|
278
313
|
s["volumes"] = vol_spec
|
|
279
314
|
vper = volumes.norm_per(serial.get("volume_chapters"))
|
|
280
|
-
if vper:
|
|
281
|
-
s["volume_chapters"] = vper
|
|
282
|
-
task["serial"] = s
|
|
283
|
-
|
|
315
|
+
if vper:
|
|
316
|
+
s["volume_chapters"] = vper
|
|
317
|
+
task["serial"] = s
|
|
318
|
+
raw_timeout = payload.get("timeout_s")
|
|
319
|
+
task["timeout_s"] = (_task_timeout_s(raw_timeout)
|
|
320
|
+
if raw_timeout not in (None, "") else _default_timeout_s(task))
|
|
321
|
+
critics = payload.get("critics")
|
|
284
322
|
if isinstance(critics, list) and critics:
|
|
285
323
|
task["critics"] = [str(c) for c in critics]
|
|
286
324
|
# 初始故事圣经必须在任务入队前落盘,保证首个章节步骤就能读到设定。
|
|
@@ -312,8 +350,20 @@ def create_task(payload):
|
|
|
312
350
|
att_ids = payload.get("attachments")
|
|
313
351
|
if isinstance(att_ids, list) and att_ids:
|
|
314
352
|
from . import attachments as att_mod
|
|
315
|
-
|
|
316
|
-
if
|
|
353
|
+
dict_items = [a for a in att_ids if isinstance(a, dict) and a.get("path")]
|
|
354
|
+
if dict_items:
|
|
355
|
+
root = Path(str(wd)).resolve()
|
|
356
|
+
items = []
|
|
357
|
+
for raw in dict_items[:att_mod.MAX_FILES]:
|
|
358
|
+
rel = att_mod.norm_rel(raw)
|
|
359
|
+
fp = (root / rel).resolve() if rel else root / "__invalid__"
|
|
360
|
+
if rel and root in fp.parents and fp.is_file():
|
|
361
|
+
item = dict(raw)
|
|
362
|
+
item["path"] = rel
|
|
363
|
+
items.append(item)
|
|
364
|
+
if not items:
|
|
365
|
+
raise ValueError("附件路径必须位于工作目录的 _attachments/ 内")
|
|
366
|
+
else: # 纯 id 形态 → 从待提交区移入工作目录
|
|
317
367
|
try:
|
|
318
368
|
items = att_mod.commit_to_workdir(
|
|
319
369
|
str(wd), [str(x) for x in att_ids][:att_mod.MAX_FILES])
|
|
@@ -321,6 +371,18 @@ def create_task(payload):
|
|
|
321
371
|
raise ValueError("附件落盘失败: %s" % e)
|
|
322
372
|
if items:
|
|
323
373
|
task["attachments"] = items
|
|
374
|
+
task["asset_refs"] = att_mod.asset_refs(str(wd), items)
|
|
375
|
+
try:
|
|
376
|
+
from . import assets
|
|
377
|
+
for ref in task["asset_refs"]:
|
|
378
|
+
saved = assets.register(str(wd), ref.get("path"),
|
|
379
|
+
owner_type="task", owner_id=task["id"],
|
|
380
|
+
name=ref.get("name"), mime=ref.get("mime"))
|
|
381
|
+
if saved:
|
|
382
|
+
ref.update({k: saved[k] for k in ("asset_id", "revision_id",
|
|
383
|
+
"content_sha256")})
|
|
384
|
+
except Exception:
|
|
385
|
+
pass
|
|
324
386
|
task["context"] = att_mod.merge_context(
|
|
325
387
|
task["context"], items, workdir=str(wd))
|
|
326
388
|
with LOCK:
|
|
@@ -569,12 +631,106 @@ def set_run_auditor(auditor):
|
|
|
569
631
|
_RUN_AUDITOR = auditor if callable(auditor) else None
|
|
570
632
|
|
|
571
633
|
|
|
634
|
+
def _execution_snapshot(task):
|
|
635
|
+
"""Capture the redacted execution inputs at run creation time.
|
|
636
|
+
|
|
637
|
+
This is intentionally a snapshot of identities and revisions, never API
|
|
638
|
+
keys or task bodies. Later binding/skill/knowledge changes therefore do
|
|
639
|
+
not rewrite the explanation of an existing run.
|
|
640
|
+
"""
|
|
641
|
+
task = task or {}
|
|
642
|
+
flow_snapshot = dict(task.get("flow_snapshot") or {})
|
|
643
|
+
flow_digest = str(flow_snapshot.get("digest") or "")
|
|
644
|
+
flow_content_sha = str(flow_snapshot.get("content_sha256") or "")
|
|
645
|
+
flow_revision_id = str(flow_snapshot.get("revision_id") or "")
|
|
646
|
+
if not flow_content_sha and len(flow_digest) == 64:
|
|
647
|
+
flow_content_sha = flow_digest
|
|
648
|
+
if not flow_revision_id and flow_digest:
|
|
649
|
+
flow_revision_id = "flowrev-" + flow_digest[:16]
|
|
650
|
+
|
|
651
|
+
def _redact_command(command):
|
|
652
|
+
value = str(command or "")[:300]
|
|
653
|
+
if not value:
|
|
654
|
+
return ""
|
|
655
|
+
pattern = re.compile(
|
|
656
|
+
r"(?i)(api[_-]?key|access[_-]?token|auth(?:orization)?|password|passwd|secret|token)"
|
|
657
|
+
r"(\s*[:=]\s*|\s+)([^\s]+)")
|
|
658
|
+
return pattern.sub(lambda m: m.group(1) + m.group(2) + "<redacted>", value)
|
|
659
|
+
|
|
660
|
+
snapshot = {
|
|
661
|
+
"captured_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
662
|
+
"task_id": task.get("id") or "",
|
|
663
|
+
"flow_revision": flow_digest,
|
|
664
|
+
"flow_revision_id": flow_revision_id,
|
|
665
|
+
"flow_content_sha256": flow_content_sha,
|
|
666
|
+
"flow_snapshot": flow_snapshot,
|
|
667
|
+
"workdir": str(task.get("workdir") or "")[:500],
|
|
668
|
+
"git_rev": str(task.get("git_rev") or "")[:160],
|
|
669
|
+
"verify_command": _redact_command(task.get("verify_command") or ""),
|
|
670
|
+
"quality_gate": {
|
|
671
|
+
"threshold": task.get("threshold"),
|
|
672
|
+
"rubric": list(task.get("rubric") or [])[:8],
|
|
673
|
+
"rounds": task.get("rounds"),
|
|
674
|
+
"best_of": task.get("best_of"),
|
|
675
|
+
},
|
|
676
|
+
}
|
|
677
|
+
try:
|
|
678
|
+
from . import knowledge, modelhub, skills
|
|
679
|
+
snapshot["skills"] = skills.revision_for_task(task)
|
|
680
|
+
snapshot["knowledge"] = knowledge.revision_for_task(task)
|
|
681
|
+
providers = {str(p.get("id") or ""): p for p in modelhub.providers()}
|
|
682
|
+
bindings = {}
|
|
683
|
+
for agent_id, binding in (modelhub.bindings() or {}).items():
|
|
684
|
+
chain = []
|
|
685
|
+
for item in (binding or {}).get("chain") or []:
|
|
686
|
+
pid = str(item.get("provider_id") or "")
|
|
687
|
+
prov = providers.get(pid) or {}
|
|
688
|
+
try:
|
|
689
|
+
key_ids = [str(k.get("id") or "") for k in modelhub._provider_keys(prov)
|
|
690
|
+
if k.get("id")]
|
|
691
|
+
except Exception:
|
|
692
|
+
key_ids = []
|
|
693
|
+
entry = {
|
|
694
|
+
"provider_id": pid,
|
|
695
|
+
"model": str(item.get("model") or "")[:160],
|
|
696
|
+
"key_ids": key_ids[:8],
|
|
697
|
+
}
|
|
698
|
+
if pid:
|
|
699
|
+
entry.update(modelhub.connection_identity(pid, key_ids[0] if key_ids else ""))
|
|
700
|
+
chain.append(entry)
|
|
701
|
+
bindings[str(agent_id)[:80]] = {
|
|
702
|
+
"difficulty_routing": bool((binding or {}).get("difficulty_routing")),
|
|
703
|
+
"chain": chain,
|
|
704
|
+
}
|
|
705
|
+
snapshot["agent_bindings"] = bindings
|
|
706
|
+
if task.get("direct_provider_id") or task.get("direct_model"):
|
|
707
|
+
direct = {
|
|
708
|
+
"provider_id": str(task.get("direct_provider_id") or "")[:80],
|
|
709
|
+
"model": str(task.get("direct_model") or "")[:160],
|
|
710
|
+
"agent": str(task.get("direct_agent") or "")[:80],
|
|
711
|
+
}
|
|
712
|
+
direct_pid = str(task.get("direct_provider_id") or "")
|
|
713
|
+
direct_prov = providers.get(direct_pid) or {}
|
|
714
|
+
direct_keys = modelhub._provider_keys(direct_prov)
|
|
715
|
+
if direct_pid:
|
|
716
|
+
direct.update(modelhub.connection_identity(
|
|
717
|
+
direct_pid, str((direct_keys[0] if direct_keys else {}).get("id") or "")))
|
|
718
|
+
snapshot["direct"] = direct
|
|
719
|
+
except Exception:
|
|
720
|
+
# A snapshot is diagnostic metadata. A broken optional catalog must
|
|
721
|
+
# never prevent the task from being created.
|
|
722
|
+
snapshot.setdefault("skills", {})
|
|
723
|
+
snapshot.setdefault("knowledge", {})
|
|
724
|
+
snapshot.setdefault("agent_bindings", {})
|
|
725
|
+
return snapshot
|
|
726
|
+
|
|
727
|
+
|
|
572
728
|
# run 终态集合:与 CAS 收口、状态回填共用同一份定义
|
|
573
729
|
TERMINAL_STATUSES = ("done", "failed", "cancelled", "timeout")
|
|
574
730
|
|
|
575
731
|
|
|
576
|
-
def create_run(kind, title, task_id=None, entry_id=None, op=None):
|
|
577
|
-
run = {
|
|
732
|
+
def create_run(kind, title, task_id=None, entry_id=None, op=None):
|
|
733
|
+
run = {
|
|
578
734
|
"id": _new_id("r" if kind == "orchestration" else "m"),
|
|
579
735
|
"kind": kind, # orchestration | mgmt
|
|
580
736
|
"title": title,
|
|
@@ -584,11 +740,20 @@ def create_run(kind, title, task_id=None, entry_id=None, op=None):
|
|
|
584
740
|
"started_at": None, "ended_at": None,
|
|
585
741
|
"deadline_at": None,
|
|
586
742
|
"cost_usd": 0.0, "tokens": 0, "error": "",
|
|
587
|
-
"verdict": None, "summary": "",
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
743
|
+
"verdict": None, "summary": "",
|
|
744
|
+
}
|
|
745
|
+
# Persist trace identity at run creation so replay remains stable across
|
|
746
|
+
# process restarts and legacy readers can still use the normal run shape.
|
|
747
|
+
run["trace_schema_version"] = tracing.SCHEMA_VERSION
|
|
748
|
+
run["trace_id"] = tracing.trace_id(run["id"])
|
|
749
|
+
run["root_span_id"] = tracing.root_span_id(run["id"])
|
|
750
|
+
if kind == "orchestration" and task_id:
|
|
751
|
+
task = get_task(task_id) or {}
|
|
752
|
+
run["timeout_budget_s"] = _task_timeout_s(task.get("timeout_s"))
|
|
753
|
+
# Provisional for direct callers; jobs resets it at worker claim so
|
|
754
|
+
# queued/backoff time does not shorten the actual execution budget.
|
|
755
|
+
run["deadline_at"] = time.time() + run["timeout_budget_s"]
|
|
756
|
+
run["execution_snapshot"] = _execution_snapshot(task)
|
|
592
757
|
try:
|
|
593
758
|
eta = _RUN_ESTIMATOR(
|
|
594
759
|
task_type=task.get("type") or "", mode=task.get("mode") or "auto",
|
|
@@ -816,7 +981,6 @@ def update_run(run_id, expected_status=None, **fields):
|
|
|
816
981
|
s["summary"] = ("步骤未正常收尾(任务总时限已到)"
|
|
817
982
|
if st == "timeout" else
|
|
818
983
|
"步骤未正常收尾(终态自动恢复)")
|
|
819
|
-
_save_json(paths.RUNS_DIR / run_id / "run.json", run)
|
|
820
984
|
# 状态回填:起跑与结束都同步任务状态。只回填终态的话,run 在跑、
|
|
821
985
|
# 任务永远显示「排队中」(2026-09-18 实案:「# 重写·续」run 已 running
|
|
822
986
|
# 写了一小时,任务卡 queued,用户以为卡死连点重试)。
|
|
@@ -826,6 +990,16 @@ def update_run(run_id, expected_status=None, **fields):
|
|
|
826
990
|
if task:
|
|
827
991
|
task["status"] = st
|
|
828
992
|
_save_json(paths.TASKS_DIR / (tid + ".json"), task)
|
|
993
|
+
if st in TERMINAL_STATUSES:
|
|
994
|
+
try:
|
|
995
|
+
from . import acceptance
|
|
996
|
+
run["acceptance"] = acceptance.evaluate_run(
|
|
997
|
+
run, (_TASKS.get(run.get("task_id")) if run.get("task_id") else None) or {})
|
|
998
|
+
except Exception:
|
|
999
|
+
# Acceptance reporting is evidence enrichment; it must not
|
|
1000
|
+
# prevent the primary run from reaching a terminal state.
|
|
1001
|
+
pass
|
|
1002
|
+
_save_json(paths.RUNS_DIR / run_id / "run.json", run)
|
|
829
1003
|
if st in TERMINAL_STATUSES and prev_status not in TERMINAL_STATUSES:
|
|
830
1004
|
# 事后对账:只有「非终态 → 终态」这一次翻转才记账,重复写终态
|
|
831
1005
|
# (UI 补写、迟到写手)不得二次入账。快照在锁内浅拷,锁外调用
|
|
@@ -1038,6 +1212,29 @@ def run_artifacts(run_id, limit=200, cache=False):
|
|
|
1038
1212
|
running=running, cache=cache)
|
|
1039
1213
|
|
|
1040
1214
|
|
|
1215
|
+
def run_asset_refs(run_id, limit=200):
|
|
1216
|
+
"""Return content-addressed references for this run's output files."""
|
|
1217
|
+
wd, files = run_artifacts(run_id, limit=limit, cache=False)
|
|
1218
|
+
if not wd:
|
|
1219
|
+
return []
|
|
1220
|
+
from . import attachments
|
|
1221
|
+
refs = attachments.asset_refs(wd, [{"path": f.get("name") or f.get("path") or ""}
|
|
1222
|
+
for f in files if isinstance(f, dict)])
|
|
1223
|
+
try:
|
|
1224
|
+
from . import assets
|
|
1225
|
+
run = get_run(run_id) or {}
|
|
1226
|
+
for ref in refs:
|
|
1227
|
+
saved = assets.register(wd, ref.get("path"), owner_type="run",
|
|
1228
|
+
owner_id=run_id, name=ref.get("name"),
|
|
1229
|
+
mime=ref.get("mime"))
|
|
1230
|
+
if saved:
|
|
1231
|
+
ref.update({k: saved[k] for k in ("asset_id", "revision_id",
|
|
1232
|
+
"content_sha256")})
|
|
1233
|
+
except Exception:
|
|
1234
|
+
pass
|
|
1235
|
+
return refs
|
|
1236
|
+
|
|
1237
|
+
|
|
1041
1238
|
def task_step_count(task_id):
|
|
1042
1239
|
"""该任务所有 run 的步骤总数(含进行中)。
|
|
1043
1240
|
|
|
@@ -1529,30 +1726,47 @@ def rename_task(task_id, title):
|
|
|
1529
1726
|
|
|
1530
1727
|
|
|
1531
1728
|
def update_task_params(task_id, patch):
|
|
1532
|
-
"""更新任务编排参数(对话条三件套:mode/thinking/direct_provider_id/direct_model
|
|
1533
|
-
|
|
1729
|
+
"""更新任务编排参数(对话条三件套:mode/thinking/direct_provider_id/direct_model,
|
|
1730
|
+
另有 timeout_s 任务总时限)。
|
|
1731
|
+
只在任务未在跑时生效——运行中改参数不会影响当前执行,静默跳过避免误导。
|
|
1732
|
+
|
|
1733
|
+
修订 CAS(借鉴 WorkDSH 契约纪律 expectedRevision):patch 携带整数
|
|
1734
|
+
expected_rev 时与任务当前 rev 比对,不符=别处已先改——拒绝写入并说明,
|
|
1735
|
+
不做过期覆盖;不带 expected_rev 的旧调用保持原行为。写成功 rev+1,
|
|
1736
|
+
调用方(路由)从 get_task 回读新 rev 交前端续用。"""
|
|
1534
1737
|
if not _valid_id(task_id):
|
|
1535
1738
|
return False, "非法的任务 ID"
|
|
1739
|
+
patch = dict(patch) if isinstance(patch, dict) else {}
|
|
1740
|
+
expected_rev = patch.pop("expected_rev", None)
|
|
1536
1741
|
with LOCK:
|
|
1537
1742
|
task = _TASKS.get(task_id)
|
|
1538
1743
|
if not task:
|
|
1539
1744
|
return False, "任务不存在"
|
|
1540
1745
|
if task.get("status") in ("queued", "running"):
|
|
1541
1746
|
return False, "任务运行中,参数不可改"
|
|
1747
|
+
if expected_rev is not None:
|
|
1748
|
+
try:
|
|
1749
|
+
want_rev = int(expected_rev)
|
|
1750
|
+
cur_rev = int(task.get("rev") or 1)
|
|
1751
|
+
except (TypeError, ValueError):
|
|
1752
|
+
return False, "expected_rev 必须是整数"
|
|
1753
|
+
if want_rev != cur_rev:
|
|
1754
|
+
return False, ("任务参数已被其他窗口修改(当前修订 %d),"
|
|
1755
|
+
"已拒绝过期写入,界面稍后自动同步最新值" % cur_rev)
|
|
1542
1756
|
changed = False
|
|
1543
|
-
mode = str(
|
|
1757
|
+
mode = str(patch.get("mode") or "").strip()
|
|
1544
1758
|
if mode in ("auto", "fast", "expert", "manual"):
|
|
1545
1759
|
if task.get("mode") != mode:
|
|
1546
1760
|
task["mode"] = mode
|
|
1547
1761
|
changed = True
|
|
1548
|
-
thinking = str(
|
|
1762
|
+
thinking = str(patch.get("thinking") or "").strip()
|
|
1549
1763
|
if thinking in ("auto", "low", "standard", "high"):
|
|
1550
1764
|
if task.get("thinking") != thinking:
|
|
1551
1765
|
task["thinking"] = thinking
|
|
1552
1766
|
changed = True
|
|
1553
1767
|
if task.get("engine") == "direct":
|
|
1554
|
-
pv = str(
|
|
1555
|
-
mv = str(
|
|
1768
|
+
pv = str(patch.get("direct_provider_id") or "").strip()[:80]
|
|
1769
|
+
mv = str(patch.get("direct_model") or "").strip()[:160]
|
|
1556
1770
|
if mv and not pv:
|
|
1557
1771
|
return False, "指定对话模型时必须同时指定厂商"
|
|
1558
1772
|
if task.get("direct_provider_id") != pv or task.get("direct_model") != mv:
|
|
@@ -1561,30 +1775,49 @@ def update_task_params(task_id, patch):
|
|
|
1561
1775
|
changed = True
|
|
1562
1776
|
# 执行 CLI 可改/可清(清空 = 回内置智能体或模型绑定);与模型绑定
|
|
1563
1777
|
# 允许共存,运行时 CLI 优先
|
|
1564
|
-
av = str(
|
|
1778
|
+
av = str(patch.get("direct_agent") or "").strip()[:80]
|
|
1565
1779
|
if task.get("direct_agent") != av and (av or task.get("direct_agent")):
|
|
1566
1780
|
task["direct_agent"] = av
|
|
1567
1781
|
changed = True
|
|
1782
|
+
# 任务总时限(秒):建任务时按工作量给默认值,此处支持空闲时显式改。
|
|
1783
|
+
# 下次重试/续跑按新值重新给预算;已在跑的 run 保持原截止时间。
|
|
1784
|
+
# 统一钳制 [1s, 7天],脏值回落基础默认。
|
|
1785
|
+
raw_ts = patch.get("timeout_s")
|
|
1786
|
+
if raw_ts not in (None, ""):
|
|
1787
|
+
new_ts = _task_timeout_s(raw_ts)
|
|
1788
|
+
if task.get("timeout_s") != new_ts:
|
|
1789
|
+
task["timeout_s"] = new_ts
|
|
1790
|
+
changed = True
|
|
1568
1791
|
if changed:
|
|
1792
|
+
task["rev"] = int(task.get("rev") or 1) + 1
|
|
1569
1793
|
_save_json(paths.TASKS_DIR / (task_id + ".json"), task)
|
|
1570
1794
|
bump_state()
|
|
1571
1795
|
return True, ""
|
|
1572
1796
|
|
|
1573
1797
|
|
|
1574
|
-
def add_step(run_id, role, agent_id, agent_label, note="", model=""):
|
|
1798
|
+
def add_step(run_id, role, agent_id, agent_label, note="", model="", provider=""):
|
|
1575
1799
|
with LOCK:
|
|
1576
1800
|
run = _RUNS.get(run_id)
|
|
1577
1801
|
if not run:
|
|
1578
1802
|
return None, None
|
|
1579
1803
|
n = len(run["steps"]) + 1
|
|
1580
|
-
step = {
|
|
1804
|
+
step = {
|
|
1581
1805
|
"n": n, "role": role, "agent": agent_id, "agent_label": agent_label,
|
|
1582
1806
|
"note": note,
|
|
1583
1807
|
# 起跑即记解析到的模型:运行中蜂巢卡/步骤列表就能看到「在用哪个模型」,
|
|
1584
1808
|
# 不必等 finish_step 用实际结果覆盖(换将成功后以实际为准)
|
|
1585
1809
|
"model": str(model or "")[:80],
|
|
1586
|
-
|
|
1587
|
-
|
|
1810
|
+
# 绑定速记(借鉴 WorkDSH ResolvedExecutionBinding):本步实际用的
|
|
1811
|
+
# provider 链与上游 host——烧错配额/模型漂移直接对账步骤行。
|
|
1812
|
+
# 只记身份不记密钥;老数据无此键,读取一律 .get
|
|
1813
|
+
"provider": str(provider or "")[:120],
|
|
1814
|
+
"status": "running", "started_at": time.strftime("%H:%M:%S"),
|
|
1815
|
+
"started_at_epoch": time.time(),
|
|
1816
|
+
"trace_id": str(run.get("trace_id") or tracing.trace_id(run_id)),
|
|
1817
|
+
"span_id": tracing.span_id(run_id, n),
|
|
1818
|
+
"parent_span_id": str(run.get("root_span_id") or
|
|
1819
|
+
tracing.root_span_id(run_id)),
|
|
1820
|
+
"ended_at": None, "duration_s": None, "exit_code": None,
|
|
1588
1821
|
"summary": "", "log": None, "cost_usd": 0.0, "tokens": 0,
|
|
1589
1822
|
}
|
|
1590
1823
|
run["steps"].append(step)
|
|
@@ -1654,8 +1887,9 @@ def finish_step(run_id, n, status, summary="", exit_code=None,
|
|
|
1654
1887
|
return
|
|
1655
1888
|
for s in run["steps"]:
|
|
1656
1889
|
if s["n"] == n:
|
|
1657
|
-
s["status"] = status
|
|
1658
|
-
s["ended_at"] = time.strftime("%H:%M:%S")
|
|
1890
|
+
s["status"] = status
|
|
1891
|
+
s["ended_at"] = time.strftime("%H:%M:%S")
|
|
1892
|
+
s["ended_at_epoch"] = time.time()
|
|
1659
1893
|
# 步骤摘要是 CLI 文本的汇聚点(失败时=错误尾巴,成功时=智能体结论):
|
|
1660
1894
|
# 落盘前统一清洗,避免 ANSI/覆写/乱码墙进 UI 与报告
|
|
1661
1895
|
s["summary"] = runner.clean_cli_text(summary)
|
|
@@ -1664,8 +1898,11 @@ def finish_step(run_id, n, status, summary="", exit_code=None,
|
|
|
1664
1898
|
s["tokens"] = tokens
|
|
1665
1899
|
if model:
|
|
1666
1900
|
s["model"] = str(model)[:80]
|
|
1667
|
-
if duration_s is not None:
|
|
1668
|
-
s["duration_s"] = round(duration_s, 1)
|
|
1901
|
+
if duration_s is not None:
|
|
1902
|
+
s["duration_s"] = round(duration_s, 1)
|
|
1903
|
+
elif s.get("started_at_epoch") is not None:
|
|
1904
|
+
s["duration_s"] = round(max(
|
|
1905
|
+
0.0, s["ended_at_epoch"] - s["started_at_epoch"]), 1)
|
|
1669
1906
|
if output is not None:
|
|
1670
1907
|
# output 是智能体正文(对话气泡直读):只剥 ANSI,不做噪声折叠
|
|
1671
1908
|
# ——正文里的装饰性长串是作者写的,不能替它省略
|