codebee 0.1.96 → 0.1.97
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 +8 -0
- package/README.md +3 -2
- package/app/core/agent_context.py +21 -3
- package/app/core/board.py +39 -2
- package/app/core/builtin_agent.py +409 -34
- package/app/core/checkpoints.py +505 -0
- package/app/core/mcp_client.py +200 -29
- package/app/core/pipeline.py +276 -40
- package/app/core/policy.py +36 -4
- package/app/core/project_memory.py +139 -0
- package/app/core/publish/auto.py +80 -19
- package/app/core/publish/browser.py +39 -0
- package/app/core/publish/fanqie.py +2 -0
- package/app/core/publish/flow.py +96 -17
- package/app/core/publish/flows-fanqie-calibrated.json +23 -11
- package/app/core/publish/ledger.py +38 -1
- package/app/core/runner.py +8 -7
- package/app/core/settings.py +13 -5
- package/app/core/settings_schema.py +14 -3
- package/app/core/store.py +21 -6
- package/app/core/tool_outputs.py +107 -0
- package/app/core/wxdigest.py +8 -1
- package/app/main.py +76 -2
- package/app/ui/app.js +356 -82
- package/app/ui/assets/workbench-reference.png +0 -0
- package/app/ui/board.css +451 -666
- package/app/ui/board.html +97 -94
- package/app/ui/board.js +316 -112
- package/app/ui/hive3d.js +242 -0
- package/app/ui/i18n.js +93 -10
- package/app/ui/index.html +31 -3
- package/app/ui/style.css +97 -111
- package/package.json +3 -2
package/app/core/runner.py
CHANGED
|
@@ -1370,17 +1370,18 @@ def _attempt_protocol(att):
|
|
|
1370
1370
|
|
|
1371
1371
|
|
|
1372
1372
|
def _codex_sandbox(readonly):
|
|
1373
|
-
"""
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1373
|
+
"""Return the narrowest supported Codex sandbox mode.
|
|
1374
|
+
|
|
1375
|
+
A full-access override defeats task-level filesystem policy and checkpoints;
|
|
1376
|
+
ignore the former ``danger-full-access`` value even if it remains in the
|
|
1377
|
+
user's environment. Read-only review steps must not be able to mutate files.
|
|
1378
|
+
"""
|
|
1378
1379
|
if readonly:
|
|
1379
1380
|
return "read-only"
|
|
1380
1381
|
env = os.environ.get("TUTTI_CODEX_SANDBOX", "").strip().lower()
|
|
1381
|
-
if env in ("workspace-write", "read-only"
|
|
1382
|
+
if env in ("workspace-write", "read-only"):
|
|
1382
1383
|
return env
|
|
1383
|
-
return "
|
|
1384
|
+
return "workspace-write"
|
|
1384
1385
|
|
|
1385
1386
|
|
|
1386
1387
|
def _prompt_to_file(prompt, workdir):
|
package/app/core/settings.py
CHANGED
|
@@ -14,8 +14,9 @@ _FILE = paths.DATA_DIR / "settings.json"
|
|
|
14
14
|
# default_workdir 为空表示未自定义,用 builtin_workdir() 回落;
|
|
15
15
|
# telemetry_errors:匿名错误回传开关(默认开;关掉后版本 ping/错误上传/诊断包遥测部分全部停发,
|
|
16
16
|
# 「导出诊断包」是用户手动操作不受此开关限制)
|
|
17
|
-
# publish_daily_cap / publish_fail_streak
|
|
18
|
-
#
|
|
17
|
+
# publish_daily_cap / publish_fail_streak / publish_fail_window_h:自动发布护栏——
|
|
18
|
+
# 每任务每平台每日成功发章上限、平台连续失败几次后暂停自动发布、连败只统计
|
|
19
|
+
# 最近多少小时内的失败(跨天旧失败不进「连续」口径)(publish/auto.py 读取)
|
|
19
20
|
# pet_enabled / pet_mode:桌面蜜蜂(app/pet.py)开关与显示模式(默认关,设置页
|
|
20
21
|
# 可开)。关闭后看护线程不再拉起、在岗蜜蜂轮询到 false 自行退出;mode=always
|
|
21
22
|
# 不再拉起、在岗蜜蜂轮询到 false 自行退出;mode=always 常驻,tasks_only 仅任务
|
|
@@ -28,7 +29,8 @@ _FILE = paths.DATA_DIR / "settings.json"
|
|
|
28
29
|
DEFAULTS = {"max_concurrent_jobs": 12, "default_workdir": "", "hooks_token": "",
|
|
29
30
|
"hooks_signing_secret": "",
|
|
30
31
|
"telemetry_errors": True, "publish_daily_cap": 10,
|
|
31
|
-
"publish_fail_streak": 3, "
|
|
32
|
+
"publish_fail_streak": 3, "publish_fail_window_h": 12,
|
|
33
|
+
"notify_webhook": "", "notify_base_url": "",
|
|
32
34
|
"notify_bark_server": "", "notify_bark_key": "",
|
|
33
35
|
"notify_ntfy_topic": "", "notify_serverchan_key": "",
|
|
34
36
|
"notify_telegram_token": "", "notify_telegram_chat_id": "",
|
|
@@ -142,14 +144,20 @@ def save(patch):
|
|
|
142
144
|
cur["telemetry_errors"] = bool(patch.get("telemetry_errors"))
|
|
143
145
|
if "publish_daily_cap" in patch:
|
|
144
146
|
try:
|
|
145
|
-
|
|
147
|
+
v = int(patch.get("publish_daily_cap"))
|
|
146
148
|
except (TypeError, ValueError):
|
|
147
|
-
return cur, "publish_daily_cap 必须是
|
|
149
|
+
return cur, "publish_daily_cap 必须是 0-50 的整数(0=不限制)"
|
|
150
|
+
cur["publish_daily_cap"] = max(0, min(50, v))
|
|
148
151
|
if "publish_fail_streak" in patch:
|
|
149
152
|
try:
|
|
150
153
|
cur["publish_fail_streak"] = max(1, min(10, int(patch.get("publish_fail_streak"))))
|
|
151
154
|
except (TypeError, ValueError):
|
|
152
155
|
return cur, "publish_fail_streak 必须是 1-10 的整数"
|
|
156
|
+
if "publish_fail_window_h" in patch:
|
|
157
|
+
try:
|
|
158
|
+
cur["publish_fail_window_h"] = max(1, min(168, float(patch.get("publish_fail_window_h"))))
|
|
159
|
+
except (TypeError, ValueError):
|
|
160
|
+
return cur, "publish_fail_window_h 必须是 1-168 的数字"
|
|
153
161
|
if "notify_webhook" in patch:
|
|
154
162
|
cur["notify_webhook"] = str(patch.get("notify_webhook") or "").strip()[:300]
|
|
155
163
|
if "notify_base_url" in patch:
|
|
@@ -223,8 +223,8 @@ def register_default_namespaces():
|
|
|
223
223
|
register_namespace(
|
|
224
224
|
"orchestrator",
|
|
225
225
|
fields=[
|
|
226
|
-
FieldDef("compaction.enabled", "bool",
|
|
227
|
-
"
|
|
226
|
+
FieldDef("compaction.enabled", "bool", True,
|
|
227
|
+
"上下文压缩总开关(默认开启;可在设置页或用 TUTTI_COMPACTION=0 关闭)"),
|
|
228
228
|
FieldDef("compaction.pressure_threshold", "float", 0.8,
|
|
229
229
|
"触发压缩的压力比阈值",
|
|
230
230
|
clamp=(0.1, 0.99)),
|
|
@@ -259,6 +259,17 @@ def register_default_namespaces():
|
|
|
259
259
|
],
|
|
260
260
|
validate=lambda v: None,
|
|
261
261
|
)
|
|
262
|
+
|
|
263
|
+
# Revision 1 is the pre-release schema default (disabled). Values written
|
|
264
|
+
# by mutate() advance the namespace to revision 2+, so preserve an explicit
|
|
265
|
+
# user opt-out while migrating untouched installations to the new default.
|
|
266
|
+
with _LOCK:
|
|
267
|
+
orchestrator = _VALUES.get("orchestrator", {})
|
|
268
|
+
if (_REVISIONS.get("orchestrator") == 1 and
|
|
269
|
+
_get_path(orchestrator, "compaction.enabled") is False):
|
|
270
|
+
_set_path(orchestrator, "compaction.enabled", True)
|
|
271
|
+
_REVISIONS["orchestrator"] = 2
|
|
272
|
+
_persist()
|
|
262
273
|
if "cascade" not in _NAMESPACES:
|
|
263
274
|
register_namespace(
|
|
264
275
|
"cascade",
|
|
@@ -268,4 +279,4 @@ def register_default_namespaces():
|
|
|
268
279
|
"FrugalGPT 式级联:easy 任务按 tier 升序重排模型链(需供应商声明 tier)"),
|
|
269
280
|
],
|
|
270
281
|
validate=lambda v: None,
|
|
271
|
-
)
|
|
282
|
+
)
|
package/app/core/store.py
CHANGED
|
@@ -180,15 +180,13 @@ def create_task(payload):
|
|
|
180
180
|
"created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
181
181
|
"status": "created",
|
|
182
182
|
}
|
|
183
|
-
#
|
|
184
|
-
#
|
|
183
|
+
# Every new task must persist one verified execution policy. If policy
|
|
184
|
+
# normalization fails, do not silently replace it with a broader default.
|
|
185
185
|
try:
|
|
186
186
|
from . import policy
|
|
187
187
|
task["sandbox"] = policy.normalize_sandbox(payload.get("sandbox"), wd)
|
|
188
|
-
except Exception:
|
|
189
|
-
|
|
190
|
-
"network": True, "timeout_s": 3600,
|
|
191
|
-
"max_output_bytes": 10 * 1024 * 1024}
|
|
188
|
+
except Exception as exc:
|
|
189
|
+
raise ValueError("沙箱策略无法验证,任务未创建") from exc
|
|
192
190
|
thinking = str(payload.get("thinking") or "standard").strip().lower()
|
|
193
191
|
task["thinking"] = thinking if thinking in ("auto", "low", "standard", "high") else "auto"
|
|
194
192
|
# 流程修订出处(借鉴 WorkDSH ADR-0010):创建时钉住当时流程定义的指纹。
|
|
@@ -1747,6 +1745,23 @@ def retry_task(task_id):
|
|
|
1747
1745
|
"chapter_scores": scores,
|
|
1748
1746
|
}
|
|
1749
1747
|
break
|
|
1748
|
+
else:
|
|
1749
|
+
# 断点续跑(代码等非连载任务,A+B):上一遍失败/取消/超时 run 的
|
|
1750
|
+
# 实现会话(inherit_session,跨 run 续 CLI 会话)与断点简报
|
|
1751
|
+
# (handoff,走到哪/改了什么/还剩什么)继承给新 run——重试从断点
|
|
1752
|
+
# 继续而不是从零烧 token。done 的 run 不继承:主动重跑语义就是重来。
|
|
1753
|
+
prev_runs = sorted((r for r in _RUNS.values()
|
|
1754
|
+
if r.get("task_id") == task_id and r["id"] != run["id"]),
|
|
1755
|
+
key=lambda r: r["id"], reverse=True)
|
|
1756
|
+
for prev in prev_runs:
|
|
1757
|
+
if prev.get("status") not in ("failed", "cancelled", "timeout"):
|
|
1758
|
+
continue
|
|
1759
|
+
if prev.get("impl_session") and "inherit_session" not in run:
|
|
1760
|
+
run["inherit_session"] = dict(prev["impl_session"])
|
|
1761
|
+
if prev.get("handoff") and "handoff" not in run:
|
|
1762
|
+
run["handoff"] = dict(prev["handoff"])
|
|
1763
|
+
if "inherit_session" in run or "handoff" in run:
|
|
1764
|
+
break
|
|
1750
1765
|
task["status"] = "queued"
|
|
1751
1766
|
_save_json(paths.TASKS_DIR / (task_id + ".json"), task)
|
|
1752
1767
|
_save_json(paths.RUNS_DIR / run["id"] / "run.json", run)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Bounded, run-scoped storage for large tool results omitted from model context."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import stat
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from . import paths
|
|
12
|
+
|
|
13
|
+
_DIR = paths.DATA_DIR / "tool_outputs"
|
|
14
|
+
_REF_RE = re.compile(r"^[a-f0-9]{32}$")
|
|
15
|
+
MAX_RESULT_BYTES = 4 * 1024 * 1024
|
|
16
|
+
MAX_RUN_BYTES = 32 * 1024 * 1024
|
|
17
|
+
MAX_READ_CHARS = 20000
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _run_dir(run_id):
|
|
21
|
+
return _DIR / hashlib.sha256(str(run_id or "").encode("utf-8")).hexdigest()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def save(run_id, call_id, content):
|
|
25
|
+
"""Store one output and return an opaque reference, or empty string on limits/errors."""
|
|
26
|
+
if not str(run_id or "").strip():
|
|
27
|
+
return ""
|
|
28
|
+
raw = str(content or "").encode("utf-8", errors="replace")
|
|
29
|
+
if len(raw) > MAX_RESULT_BYTES:
|
|
30
|
+
return ""
|
|
31
|
+
run_dir = _run_dir(run_id)
|
|
32
|
+
ref = hashlib.sha256((str(call_id or "") + "\0").encode("utf-8") + raw).hexdigest()[:32]
|
|
33
|
+
try:
|
|
34
|
+
_DIR.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
run_dir.mkdir(mode=0o700, exist_ok=True)
|
|
36
|
+
if run_dir.is_symlink() or (hasattr(run_dir, "is_junction") and run_dir.is_junction()):
|
|
37
|
+
return ""
|
|
38
|
+
target = run_dir / (ref + ".txt")
|
|
39
|
+
if target.exists():
|
|
40
|
+
return ref if target.is_file() and not target.is_symlink() else ""
|
|
41
|
+
total = sum(p.stat().st_size for p in run_dir.glob("*.txt") if p.is_file())
|
|
42
|
+
if total + len(raw) > MAX_RUN_BYTES:
|
|
43
|
+
return ""
|
|
44
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
|
45
|
+
flags |= getattr(os, "O_NOFOLLOW", 0)
|
|
46
|
+
fd = os.open(str(target), flags, 0o600)
|
|
47
|
+
try:
|
|
48
|
+
if not stat.S_ISREG(os.fstat(fd).st_mode):
|
|
49
|
+
return ""
|
|
50
|
+
with os.fdopen(fd, "wb", closefd=False) as handle:
|
|
51
|
+
handle.write(raw)
|
|
52
|
+
handle.flush()
|
|
53
|
+
finally:
|
|
54
|
+
os.close(fd)
|
|
55
|
+
return ref
|
|
56
|
+
except (OSError, ValueError):
|
|
57
|
+
return ""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def read(run_id, ref, *, offset=0, limit=MAX_READ_CHARS):
|
|
61
|
+
"""Read a bounded slice, scoped to a single run and an opaque content reference."""
|
|
62
|
+
if not str(run_id or "").strip() or not _REF_RE.fullmatch(str(ref or "")):
|
|
63
|
+
return {"ok": False, "error": "工具结果引用无效"}
|
|
64
|
+
run_dir = _run_dir(run_id)
|
|
65
|
+
if run_dir.is_symlink() or (hasattr(run_dir, "is_junction") and run_dir.is_junction()):
|
|
66
|
+
return {"ok": False, "error": "工具结果存储路径不安全"}
|
|
67
|
+
target = run_dir / (str(ref) + ".txt")
|
|
68
|
+
if target.is_symlink() or (hasattr(target, "is_junction") and target.is_junction()):
|
|
69
|
+
return {"ok": False, "error": "工具结果存储路径不安全"}
|
|
70
|
+
try:
|
|
71
|
+
offset = max(0, int(offset or 0))
|
|
72
|
+
limit = max(1, min(MAX_READ_CHARS, int(limit or MAX_READ_CHARS)))
|
|
73
|
+
with target.open("rb") as handle:
|
|
74
|
+
handle.seek(offset)
|
|
75
|
+
raw = handle.read(limit * 4 + 4)
|
|
76
|
+
text = raw.decode("utf-8", errors="replace")[:limit]
|
|
77
|
+
end = handle.tell()
|
|
78
|
+
handle.seek(0, os.SEEK_END)
|
|
79
|
+
size = handle.tell()
|
|
80
|
+
return {"ok": True, "text": text, "offset": offset,
|
|
81
|
+
"next_offset": end if end < size else None, "size_bytes": size}
|
|
82
|
+
except (OSError, ValueError, TypeError):
|
|
83
|
+
return {"ok": False, "error": "找不到此运行中的工具结果引用"}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def matches_compacted_output(run_id, call_id, ref, head, tail):
|
|
87
|
+
"""Verify a compacted preview belongs to this call and stored full output."""
|
|
88
|
+
if not str(run_id or "").strip() or not _REF_RE.fullmatch(str(ref or "")):
|
|
89
|
+
return False
|
|
90
|
+
run_dir = _run_dir(run_id)
|
|
91
|
+
if run_dir.is_symlink() or (hasattr(run_dir, "is_junction") and run_dir.is_junction()):
|
|
92
|
+
return False
|
|
93
|
+
target = run_dir / (str(ref) + ".txt")
|
|
94
|
+
if target.is_symlink() or (hasattr(target, "is_junction") and target.is_junction()):
|
|
95
|
+
return False
|
|
96
|
+
try:
|
|
97
|
+
if target.stat().st_size > MAX_RESULT_BYTES:
|
|
98
|
+
return False
|
|
99
|
+
raw = target.read_bytes()
|
|
100
|
+
expected_ref = hashlib.sha256(
|
|
101
|
+
(str(call_id or "") + "\0").encode("utf-8") + raw).hexdigest()[:32]
|
|
102
|
+
if expected_ref != ref:
|
|
103
|
+
return False
|
|
104
|
+
return (raw.startswith(str(head or "").encode("utf-8", errors="replace"))
|
|
105
|
+
and raw.endswith(str(tail or "").encode("utf-8", errors="replace")))
|
|
106
|
+
except (OSError, ValueError):
|
|
107
|
+
return False
|
package/app/core/wxdigest.py
CHANGED
|
@@ -358,7 +358,14 @@ def _summarize(cfg, group, window, watch):
|
|
|
358
358
|
m["ts"], m["sender"] or "?", m["text"].replace("\n", " ")) for m in window)
|
|
359
359
|
prompt = _PROMPT % (group, len(window), body)
|
|
360
360
|
# 工作目录用专用空目录而非监控文件夹:见 _scratch_dir 的说明
|
|
361
|
-
|
|
361
|
+
scratch = str(_scratch_dir())
|
|
362
|
+
from . import builtin_agent as _builtin_agent
|
|
363
|
+
disabled_tools = [str(tool.get("name") or "") for tool in _builtin_agent.TOOLS_SPEC]
|
|
364
|
+
disabled_tools.extend(["create_task", "read_tool_output"])
|
|
365
|
+
r = builtin_agent.run(
|
|
366
|
+
bi, prompt, workdir=scratch, timeout=240,
|
|
367
|
+
sandbox={"allowed_roots": [scratch], "network": False,
|
|
368
|
+
"disabled_tools": disabled_tools})
|
|
362
369
|
if not r.get("ok"):
|
|
363
370
|
raise RuntimeError(r.get("error") or "模型调用失败")
|
|
364
371
|
text = (r.get("text") or "").strip()
|
package/app/main.py
CHANGED
|
@@ -286,6 +286,13 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
286
286
|
if path == "/api/board":
|
|
287
287
|
# 任务驾驶舱大屏的轻量聚合(KB 级;别让它去拉 /api/state 全量)
|
|
288
288
|
return self._json(200, board.payload())
|
|
289
|
+
if path == "/api/board/run":
|
|
290
|
+
# 大屏下钻抽屉的步骤链:点开才拉,不进 /api/board 的 8 秒缓存
|
|
291
|
+
rid = (parse_qs(urlparse(self.path).query).get("run_id") or [""])[0]
|
|
292
|
+
detail = board.run_detail(rid)
|
|
293
|
+
if not detail:
|
|
294
|
+
return self._json(404, {"error": "not found"})
|
|
295
|
+
return self._json(200, detail)
|
|
289
296
|
if path == "/api/analytics":
|
|
290
297
|
from core import analytics
|
|
291
298
|
return self._json(200, analytics.summary(store))
|
|
@@ -341,6 +348,16 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
341
348
|
if path == "/api/knowledge":
|
|
342
349
|
from core import knowledge
|
|
343
350
|
return self._json(200, knowledge.view())
|
|
351
|
+
m = re.match(r"^/api/tasks/([^/]+)/memory$", path)
|
|
352
|
+
if m:
|
|
353
|
+
task = store.get_task(m.group(1))
|
|
354
|
+
if not task:
|
|
355
|
+
return self._json(404, {"error": "not found"})
|
|
356
|
+
from core import project_memory
|
|
357
|
+
try:
|
|
358
|
+
return self._json(200, {"entries": project_memory.list_entries(task.get("workdir"))})
|
|
359
|
+
except (OSError, ValueError) as exc:
|
|
360
|
+
return self._json(409, {"error": str(exc)})
|
|
344
361
|
if path == "/api/agents/presence":
|
|
345
362
|
from core import presence
|
|
346
363
|
return self._json(200, {"agents": presence.list_agents()})
|
|
@@ -592,7 +609,11 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
592
609
|
if not store.get_run(m.group(1)):
|
|
593
610
|
return self._json(404, {"error": "not found"})
|
|
594
611
|
from core import checkpoints
|
|
595
|
-
|
|
612
|
+
run = store.get_run(m.group(1)) or {}
|
|
613
|
+
task = store.get_task(run.get("task_id")) or {}
|
|
614
|
+
return self._json(200, {
|
|
615
|
+
"checkpoint": checkpoints.replay_preview(m.group(1)),
|
|
616
|
+
"files": checkpoints.snapshot_preview(m.group(1), task.get("workdir") or "")})
|
|
596
617
|
m = re.match(r"^/api/runs/([^/]+)/preview$", path)
|
|
597
618
|
if m:
|
|
598
619
|
# 网页成品预览:入口 HTML + 同目录代码文件(前端渲染「预览」页签)
|
|
@@ -886,6 +907,22 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
886
907
|
return self._json(200, {"ok": ok, "output": out})
|
|
887
908
|
if path == "/api/tasks/clarify":
|
|
888
909
|
return self._api_task_clarify()
|
|
910
|
+
m = re.match(r"^/api/tasks/([^/]+)/memory$", path)
|
|
911
|
+
if m:
|
|
912
|
+
task = store.get_task(m.group(1))
|
|
913
|
+
if not task:
|
|
914
|
+
return self._json(404, {"error": "not found"})
|
|
915
|
+
body = self._body() or {}
|
|
916
|
+
from core import project_memory
|
|
917
|
+
try:
|
|
918
|
+
row = project_memory.decide(task.get("workdir"), body.get("id"),
|
|
919
|
+
body.get("action"),
|
|
920
|
+
expected_version=body.get("expected_version"))
|
|
921
|
+
return self._json(200, {"entry": row})
|
|
922
|
+
except KeyError as exc:
|
|
923
|
+
return self._json(404, {"error": str(exc)})
|
|
924
|
+
except (OSError, TypeError, ValueError) as exc:
|
|
925
|
+
return self._json(400, {"error": str(exc)})
|
|
889
926
|
if path == "/api/agents/presence":
|
|
890
927
|
from core import presence
|
|
891
928
|
body = self._body() or {}
|
|
@@ -1247,6 +1284,35 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1247
1284
|
m = re.match(r"^/api/(tasks|runs)/([^/]+)/reveal$", path)
|
|
1248
1285
|
if m:
|
|
1249
1286
|
return self._api_reveal(m.group(1), m.group(2))
|
|
1287
|
+
m = re.match(r"^/api/runs/([^/]+)/checkpoints/restore$", path)
|
|
1288
|
+
if m:
|
|
1289
|
+
if not bool((self._body() or {}).get("confirm")):
|
|
1290
|
+
return self._json(400, {"error": "文件恢复会覆盖检查点记录的文件,请确认 confirm=true 后重试"})
|
|
1291
|
+
run = store.get_run(m.group(1))
|
|
1292
|
+
if not run:
|
|
1293
|
+
return self._json(404, {"error": "not found"})
|
|
1294
|
+
if run.get("status") not in ("done", "failed", "cancelled", "timeout"):
|
|
1295
|
+
return self._json(409, {"error": "任务仍在运行或排队;请先停止并等待终态后再恢复文件"})
|
|
1296
|
+
task = store.get_task(run.get("task_id")) or {}
|
|
1297
|
+
if not task.get("workdir"):
|
|
1298
|
+
return self._json(409, {"error": "任务工作目录不可用,拒绝恢复"})
|
|
1299
|
+
from core import checkpoints
|
|
1300
|
+
# Serialize the final safety check and file restore with run creation
|
|
1301
|
+
# and status transitions. Otherwise a new run can start on this task
|
|
1302
|
+
# after the terminal check and race with restoration in the same tree.
|
|
1303
|
+
with store.LOCK:
|
|
1304
|
+
current = store.get_run(m.group(1)) or {}
|
|
1305
|
+
task = store.get_task(current.get("task_id")) or {}
|
|
1306
|
+
latest = store.latest_run_by_task().get(current.get("task_id")) or {}
|
|
1307
|
+
terminal = ("done", "failed", "cancelled", "timeout")
|
|
1308
|
+
if current.get("status") not in terminal:
|
|
1309
|
+
return self._json(409, {"error": "任务状态已变化;请等待终态后再恢复文件"})
|
|
1310
|
+
task_active = task.get("status") in ("queued", "running")
|
|
1311
|
+
newer_run_active = latest.get("status") in ("queued", "running")
|
|
1312
|
+
if task_active or newer_run_active:
|
|
1313
|
+
return self._json(409, {"error": "该任务已有运行中的新任务;请等待其结束后再恢复文件"})
|
|
1314
|
+
return self._json(200, checkpoints.restore_files(
|
|
1315
|
+
m.group(1), task.get("workdir") or ""))
|
|
1250
1316
|
m = re.match(r"^/api/runs/([^/]+)/messages$", path)
|
|
1251
1317
|
if m:
|
|
1252
1318
|
return self._api_add_message(m.group(1))
|
|
@@ -3057,8 +3123,16 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
3057
3123
|
'[{"q": "问题", "options": ["选项1", "选项2"]}]' % (ttype, goal[:200]))
|
|
3058
3124
|
try:
|
|
3059
3125
|
# 澄清是可选增强,不能占住创建请求;主流程有自己的执行超时。
|
|
3126
|
+
# 此调用只负责生成问题,不得继承完整执行工具权限或访问网络。
|
|
3127
|
+
from core import policy
|
|
3128
|
+
clarify_sandbox = policy.normalize_sandbox({
|
|
3129
|
+
"network": False,
|
|
3130
|
+
"disabled_tools": ["run_command", "write_file", "edit_file",
|
|
3131
|
+
"append_file", "fs_manage", "create_task",
|
|
3132
|
+
"read_tool_output"],
|
|
3133
|
+
}, os.getcwd() if hasattr(os, "getcwd") else ".")
|
|
3060
3134
|
res = builtin_agent.run(bi, prompt, os.getcwd() if hasattr(os, "getcwd") else ".",
|
|
3061
|
-
timeout=8)
|
|
3135
|
+
timeout=8, sandbox=clarify_sandbox)
|
|
3062
3136
|
import json as _json
|
|
3063
3137
|
arr = None
|
|
3064
3138
|
text = (res.get("text") or "").strip()
|