codebee 0.1.95 → 0.1.96
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 +725 -720
- package/LICENSE +21 -21
- package/README.md +515 -509
- package/app/core/acceptance.py +100 -100
- package/app/core/agent_context.py +100 -0
- package/app/core/aiflavor.py +197 -197
- package/app/core/analytics.py +38 -38
- package/app/core/artifacts.py +148 -148
- package/app/core/assets.py +110 -110
- package/app/core/attachments.py +623 -623
- package/app/core/audit.py +30 -30
- package/app/core/backup.py +470 -470
- package/app/core/beekeeper.py +183 -183
- package/app/core/benchstore.py +102 -95
- package/app/core/board.py +209 -209
- package/app/core/bookmeta.py +1036 -1036
- package/app/core/bookmeta_catalog.py +180 -180
- package/app/core/branching.py +137 -137
- package/app/core/builtin_agent.py +1680 -1680
- package/app/core/capability.py +144 -144
- package/app/core/chaptersafe.py +32 -32
- package/app/core/checkpoints.py +143 -0
- package/app/core/cleanup.py +303 -303
- package/app/core/compaction.py +201 -201
- package/app/core/contracts.py +255 -255
- package/app/core/covergen.py +356 -356
- package/app/core/defectretro.py +43 -43
- package/app/core/diagnostics.py +114 -114
- package/app/core/dispatch.py +198 -191
- package/app/core/dispatch_log.py +128 -128
- package/app/core/env_scrub.py +85 -85
- package/app/core/errorlog.py +137 -137
- package/app/core/eval_matrix.py +82 -0
- package/app/core/evalbench.py +184 -91
- package/app/core/evaluation.py +174 -174
- package/app/core/failure_notify.py +290 -290
- package/app/core/flow_graph.py +33 -0
- package/app/core/goal_service.py +167 -167
- package/app/core/health.py +389 -389
- package/app/core/history.py +38 -38
- package/app/core/hooks.py +162 -162
- package/app/core/jobs.py +25 -4
- package/app/core/knowledge_pipeline.py +86 -0
- package/app/core/manager.py +2232 -2232
- package/app/core/mocks.py +64 -64
- package/app/core/notify.py +201 -201
- package/app/core/novel_quality.py +181 -189
- package/app/core/openapi.py +177 -154
- package/app/core/operations.py +228 -228
- package/app/core/paihang.py +145 -145
- package/app/core/pipeline.py +43 -19
- package/app/core/planner.py +17 -11
- package/app/core/policy.py +52 -0
- package/app/core/portguard.py +111 -111
- package/app/core/presence.py +77 -77
- package/app/core/preview.py +293 -293
- package/app/core/prompt_guard.py +78 -78
- package/app/core/publish/__init__.py +7 -7
- package/app/core/publish/auto.py +26 -3
- package/app/core/publish/browser.py +655 -655
- package/app/core/publish/chaprows.py +138 -0
- package/app/core/publish/fanqie.py +210 -165
- package/app/core/publish/flow.py +753 -640
- package/app/core/publish/flows-fanqie-calibrated.json +349 -336
- package/app/core/publish/ledger.py +13 -1
- package/app/core/publish/manager.py +1150 -1118
- package/app/core/publish/qimao.py +176 -176
- package/app/core/publish/volumes.py +108 -0
- package/app/core/publish/ws.py +139 -139
- package/app/core/quality_gate.py +194 -194
- package/app/core/redact.py +38 -38
- package/app/core/repeat_guard.py +129 -129
- package/app/core/retrieval.py +137 -137
- package/app/core/router.py +398 -398
- package/app/core/runner.py +2238 -2238
- 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 +20 -0
- package/app/core/settings_schema.py +270 -270
- package/app/core/share_page.py +114 -114
- package/app/core/skill_scan.py +139 -139
- package/app/core/skillpacks/fanqie-novel.md +8 -6
- 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/skillpacks/qimao-signing.md +14 -11
- package/app/core/skills.py +32 -21
- package/app/core/step_runner.py +132 -132
- package/app/core/store.py +9 -0
- package/app/core/story_tracking.py +275 -275
- 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 +207 -144
- package/app/core/upstream.py +41 -41
- package/app/core/usage.py +1038 -1038
- package/app/core/volumes.py +389 -389
- package/app/core/webhooks.py +89 -0
- package/app/core/wxdigest.py +858 -858
- package/app/core/zentao.py +2692 -2692
- package/app/main.py +104 -9
- package/app/mcp_server.py +244 -244
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +286 -18
- package/app/ui/artdiff.js +74 -74
- package/app/ui/assets/community-qr.png +0 -0
- package/app/ui/assets/donate-alipay.jpg +0 -0
- package/app/ui/assets/donate-qr.jpg +0 -0
- package/app/ui/board.html +162 -162
- package/app/ui/board.js +305 -305
- package/app/ui/i18n.js +33 -0
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +32 -3
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +196 -2
- package/bin/tutti.js +147 -147
- package/package.json +48 -48
- package/tools/wx_reader.py +261 -261
- package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
- package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
- 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 +0 -210
- 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/output/_createbook_body.json +0 -1
- package/app/output/_probe_fq_click.py +0 -50
- package/app/output/_probe_fq_form.py +0 -42
- package/app/output/_probe_fq_modal.py +0 -55
- package/app/output/_probe_fq_points.py +0 -71
- package/app/output/_probe_fq_submit.py +0 -34
- package/app/output/_probe_fq_watch.py +0 -62
package/app/core/goal_service.py
CHANGED
|
@@ -1,168 +1,168 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""Goal 状态外置:跨轮目标(title/描述/phase/轮次)独立于 LLM 调用持久化。
|
|
3
|
-
|
|
4
|
-
设计稿:docs/migration/03-state-externalization.md §2A。
|
|
5
|
-
参考 dsh packages/goal/goal/:状态 100% 来自事件溯源(goal/change 事件),
|
|
6
|
-
唯一当前 goal,revision 走 CAS 拒绝陈旧引用。
|
|
7
|
-
|
|
8
|
-
与 planner.py 的关系:planner 继续负责「LLM 拆解」;GoalService 只管
|
|
9
|
-
「目标状态」——谁都可以读(UI/续行驱动器/流水线),变更必须过 CAS。
|
|
10
|
-
"""
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
import json
|
|
14
|
-
import threading
|
|
15
|
-
import time
|
|
16
|
-
import uuid
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
|
|
19
|
-
_LOCK = threading.RLock()
|
|
20
|
-
# goal_id -> dict(内存缓存;磁盘为唯一事实来源)
|
|
21
|
-
_GOALS = {}
|
|
22
|
-
_FILE = None # 由 init() 注入
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def init(data_dir=None):
|
|
26
|
-
"""注入存储路径并从磁盘加载。测试可传临时目录。"""
|
|
27
|
-
global _FILE, _GOALS
|
|
28
|
-
with _LOCK:
|
|
29
|
-
base = Path(data_dir) if data_dir else Path(_default_dir())
|
|
30
|
-
base.mkdir(parents=True, exist_ok=True)
|
|
31
|
-
_FILE = base / "goals.json"
|
|
32
|
-
_GOALS = {}
|
|
33
|
-
try:
|
|
34
|
-
data = json.loads(_FILE.read_text(encoding="utf-8"))
|
|
35
|
-
for g in data.get("goals") or []:
|
|
36
|
-
if isinstance(g, dict) and g.get("goal_id"):
|
|
37
|
-
_GOALS[g["goal_id"]] = g
|
|
38
|
-
except (OSError, json.JSONDecodeError):
|
|
39
|
-
pass
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _default_dir():
|
|
43
|
-
from . import paths
|
|
44
|
-
return str(paths.DATA_DIR)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _persist():
|
|
48
|
-
"""原子写盘(tmp + replace)。调用方必须已持有 _LOCK。"""
|
|
49
|
-
if _FILE is None:
|
|
50
|
-
return
|
|
51
|
-
tmp = _FILE.with_suffix(".tmp")
|
|
52
|
-
tmp.write_text(json.dumps({"goals": list(_GOALS.values())},
|
|
53
|
-
ensure_ascii=False, indent=2), encoding="utf-8")
|
|
54
|
-
tmp.replace(_FILE)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class StaleRevisionError(Exception):
|
|
58
|
-
"""CAS 失败:调用方持有的是陈旧 revision。"""
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
class GoalExistsError(Exception):
|
|
62
|
-
"""已有未完结的当前 goal,需先 complete/abandon。"""
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
class GoalNotFoundError(Exception):
|
|
66
|
-
pass
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def _valid_phase(p):
|
|
70
|
-
return p in ("active", "paused", "complete", "abandoned")
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def current():
|
|
74
|
-
"""返回唯一「当前」goal(未 complete/abandoned 的最新一个);无则 None。"""
|
|
75
|
-
with _LOCK:
|
|
76
|
-
open_goals = [g for g in _GOALS.values()
|
|
77
|
-
if g.get("phase") in ("active", "paused")]
|
|
78
|
-
if not open_goals:
|
|
79
|
-
return None
|
|
80
|
-
return max(open_goals, key=lambda g: g.get("created_at", 0))
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def get(goal_id: str):
|
|
84
|
-
with _LOCK:
|
|
85
|
-
g = _GOALS.get(goal_id)
|
|
86
|
-
return dict(g) if g else None
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def create(title: str, description: str = "", *, rounds_max: int | None = None,
|
|
90
|
-
metadata: dict | None = None) -> dict:
|
|
91
|
-
"""创建新 goal。已有未完结 goal 时拒绝(单一当前目标,仿 dsh)。
|
|
92
|
-
|
|
93
|
-
rounds_max 缺省读 settings_v2 orchestrator.max_goal_rounds(读不到回落 5)。"""
|
|
94
|
-
if rounds_max is None:
|
|
95
|
-
try:
|
|
96
|
-
from .settings_schema import get as ss_get, register_default_namespaces
|
|
97
|
-
register_default_namespaces()
|
|
98
|
-
rounds_max = int(ss_get("orchestrator", "max_goal_rounds") or 5)
|
|
99
|
-
except Exception:
|
|
100
|
-
rounds_max = 5
|
|
101
|
-
with _LOCK:
|
|
102
|
-
cur = current()
|
|
103
|
-
if cur is not None:
|
|
104
|
-
raise GoalExistsError(
|
|
105
|
-
"已有进行中的 goal %s(%s),请先 complete/abandon"
|
|
106
|
-
% (cur["goal_id"], cur["title"]))
|
|
107
|
-
goal = {
|
|
108
|
-
"goal_id": "g" + uuid.uuid4().hex[:12],
|
|
109
|
-
"title": (title or "").strip(),
|
|
110
|
-
"description": (description or "").strip(),
|
|
111
|
-
"phase": "active",
|
|
112
|
-
"revision": 1,
|
|
113
|
-
"rounds_started": 0,
|
|
114
|
-
"rounds_max": max(1, int(rounds_max)),
|
|
115
|
-
"created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
116
|
-
"updated_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
117
|
-
"metadata": dict(metadata or {}),
|
|
118
|
-
}
|
|
119
|
-
_GOALS[goal["goal_id"]] = goal
|
|
120
|
-
_persist()
|
|
121
|
-
return dict(goal)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def update(goal_id: str, expected_revision: int, **changes) -> dict:
|
|
125
|
-
"""CAS 更新:expected_revision 不匹配即抛 StaleRevisionError。
|
|
126
|
-
|
|
127
|
-
允许变更的字段:title/description/phase/rounds_max/metadata。
|
|
128
|
-
phase 只允许合法值;complete/abandoned 为终态不可再改回。
|
|
129
|
-
"""
|
|
130
|
-
with _LOCK:
|
|
131
|
-
g = _GOALS.get(goal_id)
|
|
132
|
-
if not g:
|
|
133
|
-
raise GoalNotFoundError(goal_id)
|
|
134
|
-
if g["revision"] != expected_revision:
|
|
135
|
-
raise StaleRevisionError(
|
|
136
|
-
"%s 期望 rev %d,实际 %d" % (goal_id, expected_revision, g["revision"]))
|
|
137
|
-
if g["phase"] in ("complete", "abandoned"):
|
|
138
|
-
raise ValueError("goal 已终态(%s),不可再变更" % g["phase"])
|
|
139
|
-
allowed = {"title", "description", "phase", "rounds_max",
|
|
140
|
-
"rounds_started", "metadata"}
|
|
141
|
-
for k, v in changes.items():
|
|
142
|
-
if k not in allowed:
|
|
143
|
-
raise ValueError("不可变更字段:%s" % k)
|
|
144
|
-
if k == "phase" and not _valid_phase(v):
|
|
145
|
-
raise ValueError("非法 phase:%s" % v)
|
|
146
|
-
if k == "rounds_max":
|
|
147
|
-
v = max(1, int(v))
|
|
148
|
-
g[k] = v
|
|
149
|
-
g["revision"] += 1
|
|
150
|
-
g["updated_at"] = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
151
|
-
_persist()
|
|
152
|
-
return dict(g)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def increment_rounds(goal_id: str, expected_revision: int) -> dict:
|
|
156
|
-
"""轮次 +1(Goal Round Driver 专用 CAS 入口)。"""
|
|
157
|
-
with _LOCK:
|
|
158
|
-
g = _GOALS.get(goal_id)
|
|
159
|
-
if not g:
|
|
160
|
-
raise GoalNotFoundError(goal_id)
|
|
161
|
-
return update(goal_id, expected_revision, rounds_started=g["rounds_started"] + 1)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def list_goals(limit: int = 50) -> list:
|
|
165
|
-
with _LOCK:
|
|
166
|
-
goals = sorted(_GOALS.values(),
|
|
167
|
-
key=lambda g: g.get("created_at", ""), reverse=True)
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Goal 状态外置:跨轮目标(title/描述/phase/轮次)独立于 LLM 调用持久化。
|
|
3
|
+
|
|
4
|
+
设计稿:docs/migration/03-state-externalization.md §2A。
|
|
5
|
+
参考 dsh packages/goal/goal/:状态 100% 来自事件溯源(goal/change 事件),
|
|
6
|
+
唯一当前 goal,revision 走 CAS 拒绝陈旧引用。
|
|
7
|
+
|
|
8
|
+
与 planner.py 的关系:planner 继续负责「LLM 拆解」;GoalService 只管
|
|
9
|
+
「目标状态」——谁都可以读(UI/续行驱动器/流水线),变更必须过 CAS。
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
import uuid
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
_LOCK = threading.RLock()
|
|
20
|
+
# goal_id -> dict(内存缓存;磁盘为唯一事实来源)
|
|
21
|
+
_GOALS = {}
|
|
22
|
+
_FILE = None # 由 init() 注入
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def init(data_dir=None):
|
|
26
|
+
"""注入存储路径并从磁盘加载。测试可传临时目录。"""
|
|
27
|
+
global _FILE, _GOALS
|
|
28
|
+
with _LOCK:
|
|
29
|
+
base = Path(data_dir) if data_dir else Path(_default_dir())
|
|
30
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
_FILE = base / "goals.json"
|
|
32
|
+
_GOALS = {}
|
|
33
|
+
try:
|
|
34
|
+
data = json.loads(_FILE.read_text(encoding="utf-8"))
|
|
35
|
+
for g in data.get("goals") or []:
|
|
36
|
+
if isinstance(g, dict) and g.get("goal_id"):
|
|
37
|
+
_GOALS[g["goal_id"]] = g
|
|
38
|
+
except (OSError, json.JSONDecodeError):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _default_dir():
|
|
43
|
+
from . import paths
|
|
44
|
+
return str(paths.DATA_DIR)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _persist():
|
|
48
|
+
"""原子写盘(tmp + replace)。调用方必须已持有 _LOCK。"""
|
|
49
|
+
if _FILE is None:
|
|
50
|
+
return
|
|
51
|
+
tmp = _FILE.with_suffix(".tmp")
|
|
52
|
+
tmp.write_text(json.dumps({"goals": list(_GOALS.values())},
|
|
53
|
+
ensure_ascii=False, indent=2), encoding="utf-8")
|
|
54
|
+
tmp.replace(_FILE)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class StaleRevisionError(Exception):
|
|
58
|
+
"""CAS 失败:调用方持有的是陈旧 revision。"""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class GoalExistsError(Exception):
|
|
62
|
+
"""已有未完结的当前 goal,需先 complete/abandon。"""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class GoalNotFoundError(Exception):
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _valid_phase(p):
|
|
70
|
+
return p in ("active", "paused", "complete", "abandoned")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def current():
|
|
74
|
+
"""返回唯一「当前」goal(未 complete/abandoned 的最新一个);无则 None。"""
|
|
75
|
+
with _LOCK:
|
|
76
|
+
open_goals = [g for g in _GOALS.values()
|
|
77
|
+
if g.get("phase") in ("active", "paused")]
|
|
78
|
+
if not open_goals:
|
|
79
|
+
return None
|
|
80
|
+
return max(open_goals, key=lambda g: g.get("created_at", 0))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get(goal_id: str):
|
|
84
|
+
with _LOCK:
|
|
85
|
+
g = _GOALS.get(goal_id)
|
|
86
|
+
return dict(g) if g else None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def create(title: str, description: str = "", *, rounds_max: int | None = None,
|
|
90
|
+
metadata: dict | None = None) -> dict:
|
|
91
|
+
"""创建新 goal。已有未完结 goal 时拒绝(单一当前目标,仿 dsh)。
|
|
92
|
+
|
|
93
|
+
rounds_max 缺省读 settings_v2 orchestrator.max_goal_rounds(读不到回落 5)。"""
|
|
94
|
+
if rounds_max is None:
|
|
95
|
+
try:
|
|
96
|
+
from .settings_schema import get as ss_get, register_default_namespaces
|
|
97
|
+
register_default_namespaces()
|
|
98
|
+
rounds_max = int(ss_get("orchestrator", "max_goal_rounds") or 5)
|
|
99
|
+
except Exception:
|
|
100
|
+
rounds_max = 5
|
|
101
|
+
with _LOCK:
|
|
102
|
+
cur = current()
|
|
103
|
+
if cur is not None:
|
|
104
|
+
raise GoalExistsError(
|
|
105
|
+
"已有进行中的 goal %s(%s),请先 complete/abandon"
|
|
106
|
+
% (cur["goal_id"], cur["title"]))
|
|
107
|
+
goal = {
|
|
108
|
+
"goal_id": "g" + uuid.uuid4().hex[:12],
|
|
109
|
+
"title": (title or "").strip(),
|
|
110
|
+
"description": (description or "").strip(),
|
|
111
|
+
"phase": "active",
|
|
112
|
+
"revision": 1,
|
|
113
|
+
"rounds_started": 0,
|
|
114
|
+
"rounds_max": max(1, int(rounds_max)),
|
|
115
|
+
"created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
116
|
+
"updated_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
117
|
+
"metadata": dict(metadata or {}),
|
|
118
|
+
}
|
|
119
|
+
_GOALS[goal["goal_id"]] = goal
|
|
120
|
+
_persist()
|
|
121
|
+
return dict(goal)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def update(goal_id: str, expected_revision: int, **changes) -> dict:
|
|
125
|
+
"""CAS 更新:expected_revision 不匹配即抛 StaleRevisionError。
|
|
126
|
+
|
|
127
|
+
允许变更的字段:title/description/phase/rounds_max/metadata。
|
|
128
|
+
phase 只允许合法值;complete/abandoned 为终态不可再改回。
|
|
129
|
+
"""
|
|
130
|
+
with _LOCK:
|
|
131
|
+
g = _GOALS.get(goal_id)
|
|
132
|
+
if not g:
|
|
133
|
+
raise GoalNotFoundError(goal_id)
|
|
134
|
+
if g["revision"] != expected_revision:
|
|
135
|
+
raise StaleRevisionError(
|
|
136
|
+
"%s 期望 rev %d,实际 %d" % (goal_id, expected_revision, g["revision"]))
|
|
137
|
+
if g["phase"] in ("complete", "abandoned"):
|
|
138
|
+
raise ValueError("goal 已终态(%s),不可再变更" % g["phase"])
|
|
139
|
+
allowed = {"title", "description", "phase", "rounds_max",
|
|
140
|
+
"rounds_started", "metadata"}
|
|
141
|
+
for k, v in changes.items():
|
|
142
|
+
if k not in allowed:
|
|
143
|
+
raise ValueError("不可变更字段:%s" % k)
|
|
144
|
+
if k == "phase" and not _valid_phase(v):
|
|
145
|
+
raise ValueError("非法 phase:%s" % v)
|
|
146
|
+
if k == "rounds_max":
|
|
147
|
+
v = max(1, int(v))
|
|
148
|
+
g[k] = v
|
|
149
|
+
g["revision"] += 1
|
|
150
|
+
g["updated_at"] = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
151
|
+
_persist()
|
|
152
|
+
return dict(g)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def increment_rounds(goal_id: str, expected_revision: int) -> dict:
|
|
156
|
+
"""轮次 +1(Goal Round Driver 专用 CAS 入口)。"""
|
|
157
|
+
with _LOCK:
|
|
158
|
+
g = _GOALS.get(goal_id)
|
|
159
|
+
if not g:
|
|
160
|
+
raise GoalNotFoundError(goal_id)
|
|
161
|
+
return update(goal_id, expected_revision, rounds_started=g["rounds_started"] + 1)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def list_goals(limit: int = 50) -> list:
|
|
165
|
+
with _LOCK:
|
|
166
|
+
goals = sorted(_GOALS.values(),
|
|
167
|
+
key=lambda g: g.get("created_at", ""), reverse=True)
|
|
168
168
|
return [dict(g) for g in goals[:limit]]
|