codebee 0.1.66 → 0.1.67
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 +13 -1
- package/app/core/acceptance.py +100 -0
- package/app/core/assets.py +110 -0
- package/app/core/attachments.py +92 -1
- package/app/core/bookmeta.py +160 -26
- package/app/core/defectretro.py +43 -0
- package/app/core/flows.py +47 -1
- package/app/core/jobs.py +20 -4
- package/app/core/knowledge.py +50 -3
- package/app/core/manager.py +7 -0
- package/app/core/market_remote.py +124 -26
- package/app/core/modelhub.py +29 -0
- package/app/core/operations.py +228 -0
- package/app/core/paihang.py +58 -29
- package/app/core/pipeline.py +205 -11
- package/app/core/planner.py +4 -1
- package/app/core/publish/browser.py +6 -0
- package/app/core/publish/fanqie.py +31 -0
- package/app/core/publish/flow.py +9 -1
- package/app/core/publish/ledger.py +7 -2
- package/app/core/publish/manager.py +91 -22
- package/app/core/revisions.py +51 -0
- package/app/core/runner.py +27 -12
- package/app/core/skills.py +94 -1
- package/app/core/store.py +207 -11
- package/app/core/volumes.py +34 -0
- package/app/core/zentao.py +21 -3
- package/app/main.py +140 -21
- package/app/ui/app.js +407 -17
- package/app/ui/artdiff.js +74 -0
- package/app/ui/i18n.js +51 -1
- package/app/ui/index.html +6 -0
- package/app/ui/style.css +96 -24
- package/package.json +1 -1
package/app/core/volumes.py
CHANGED
|
@@ -353,3 +353,37 @@ def merge_titles(plan, named):
|
|
|
353
353
|
e["arc"] = arc[:400]
|
|
354
354
|
out.append(e)
|
|
355
355
|
return out
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def norm_volumes(raw, per, start, n, plan=None):
|
|
359
|
+
"""收大纲 JSON 里模型给的卷名/卷弧光,只留本批章号真正覆盖到的卷
|
|
360
|
+
(模型可能多写、乱写卷号,一律按卷规划表过滤;卷边界不采信模型)。
|
|
361
|
+
|
|
362
|
+
raw 形如 {"1": {"title","arc"}} 或 {"1": "卷名"}(键为卷号字符串)。
|
|
363
|
+
卷边界优先用调用方现成的 plan(显式卷表时与主链推导完全一致),
|
|
364
|
+
缺省按 per 等长推导。返回 {卷号str: {"title":..., "arc":...}};
|
|
365
|
+
raw 不合规或本批卷号一个都没命中返回 None(调用方用既有卷名兜底)。"""
|
|
366
|
+
if not isinstance(raw, dict):
|
|
367
|
+
return None
|
|
368
|
+
s, cnt = _to_int(start), _to_int(n)
|
|
369
|
+
if s is None or cnt is None or cnt <= 0:
|
|
370
|
+
return None
|
|
371
|
+
if plan is None:
|
|
372
|
+
plan = build_plan(None, per, upto=s + cnt - 1)
|
|
373
|
+
hits = {e["vol"] for e in plan_volumes(plan, s, cnt)}
|
|
374
|
+
if not hits:
|
|
375
|
+
return None
|
|
376
|
+
out = {}
|
|
377
|
+
for key, item in raw.items():
|
|
378
|
+
vol = _to_int(str(key).strip())
|
|
379
|
+
if vol is None or vol not in hits:
|
|
380
|
+
continue
|
|
381
|
+
if isinstance(item, str):
|
|
382
|
+
item = {"title": item}
|
|
383
|
+
if not isinstance(item, dict):
|
|
384
|
+
continue
|
|
385
|
+
title = str(item.get("title") or "").strip()[:MAX_TITLE]
|
|
386
|
+
arc = str(item.get("arc") or "").strip()[:400]
|
|
387
|
+
if title or arc:
|
|
388
|
+
out[str(vol)] = {"title": title, "arc": arc}
|
|
389
|
+
return out or None
|
package/app/core/zentao.py
CHANGED
|
@@ -67,7 +67,7 @@ import urllib.request
|
|
|
67
67
|
from datetime import datetime, timedelta
|
|
68
68
|
from pathlib import Path
|
|
69
69
|
|
|
70
|
-
from . import jobs, paths, settings, store, tlsctx
|
|
70
|
+
from . import jobs, operations, paths, settings, store, tlsctx
|
|
71
71
|
|
|
72
72
|
log = logging.getLogger(__name__)
|
|
73
73
|
|
|
@@ -1710,7 +1710,16 @@ def _ensure_resolved(cfg, bug_id, comment, assign_to):
|
|
|
1710
1710
|
body = {"resolution": "fixed", "resolvedBuild": "trunk", "comment": comment}
|
|
1711
1711
|
if assign_to:
|
|
1712
1712
|
body["assignedTo"] = assign_to
|
|
1713
|
-
|
|
1713
|
+
operation_id = operations.begin("zentao:bug:%s:resolve" % bug_id, body,
|
|
1714
|
+
metadata={"bug_id": bug_id, "action": "resolve"})
|
|
1715
|
+
try:
|
|
1716
|
+
result = _call("POST", "/bugs/%s/resolve" % bug_id, cfg=cfg, body=body)
|
|
1717
|
+
receipt = str((result or {}).get("id") or bug_id)
|
|
1718
|
+
operations.confirm(operation_id, remote_receipt=receipt,
|
|
1719
|
+
metadata={"bug_id": bug_id, "action": "resolve"})
|
|
1720
|
+
except Exception as exc:
|
|
1721
|
+
operations.finish_exception(operation_id, exc)
|
|
1722
|
+
raise
|
|
1714
1723
|
return True
|
|
1715
1724
|
|
|
1716
1725
|
|
|
@@ -1727,7 +1736,16 @@ def _transfer(cfg, bug_id, target, comment):
|
|
|
1727
1736
|
else:
|
|
1728
1737
|
cur = _call("GET", "/bugs/%s" % bug_id, cfg=cfg)
|
|
1729
1738
|
body["assignedTo"] = _acct(cur.get("assignedTo")) or ""
|
|
1730
|
-
|
|
1739
|
+
operation_id = operations.begin("zentao:bug:%s:transfer" % bug_id, body,
|
|
1740
|
+
metadata={"bug_id": bug_id, "action": "transfer"})
|
|
1741
|
+
try:
|
|
1742
|
+
result = _call("PUT", "/bugs/%s" % bug_id, cfg=cfg, body=body)
|
|
1743
|
+
receipt = str((result or {}).get("id") or bug_id)
|
|
1744
|
+
operations.confirm(operation_id, remote_receipt=receipt,
|
|
1745
|
+
metadata={"bug_id": bug_id, "action": "transfer"})
|
|
1746
|
+
except Exception as exc:
|
|
1747
|
+
operations.finish_exception(operation_id, exc)
|
|
1748
|
+
raise
|
|
1731
1749
|
|
|
1732
1750
|
|
|
1733
1751
|
def _notify(text):
|
package/app/main.py
CHANGED
|
@@ -288,6 +288,21 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
288
288
|
return self._json(200, {"ok": True, "catalog": manager.catalog_view()})
|
|
289
289
|
if path == "/api/runs":
|
|
290
290
|
return self._json(200, {"runs": store.list_runs()})
|
|
291
|
+
if path == "/api/operations":
|
|
292
|
+
from core import operations
|
|
293
|
+
q = parse_qs(urlparse(self.path).query)
|
|
294
|
+
try:
|
|
295
|
+
limit = max(1, min(1000, int((q.get("limit") or [100])[0])))
|
|
296
|
+
except (TypeError, ValueError):
|
|
297
|
+
limit = 100
|
|
298
|
+
return self._json(200, {"operations": operations.recent(
|
|
299
|
+
target=(q.get("target") or [""])[0][:160],
|
|
300
|
+
status=(q.get("status") or [""])[0][:20], limit=limit)})
|
|
301
|
+
m = re.match(r"^/api/operations/([^/]+)$", path)
|
|
302
|
+
if m:
|
|
303
|
+
from core import operations
|
|
304
|
+
item = operations.get(m.group(1))
|
|
305
|
+
return self._json(200, {"operation": item}) if item else self._json(404, {"error": "not found"})
|
|
291
306
|
if path == "/api/pet_state":
|
|
292
307
|
return self._api_pet_state()
|
|
293
308
|
if path == "/api/usage":
|
|
@@ -420,6 +435,11 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
420
435
|
files = []
|
|
421
436
|
return self._json(200, {"workdir": wd, "files": files,
|
|
422
437
|
"task_id": run.get("task_id") or ""})
|
|
438
|
+
m = re.match(r"^/api/runs/([^/]+)/assets$", path)
|
|
439
|
+
if m:
|
|
440
|
+
if not store.get_run(m.group(1)):
|
|
441
|
+
return self._json(404, {"error": "not found"})
|
|
442
|
+
return self._json(200, {"assets": store.run_asset_refs(m.group(1))})
|
|
423
443
|
m = re.match(r"^/api/runs/([^/]+)/preview$", path)
|
|
424
444
|
if m:
|
|
425
445
|
# 网页成品预览:入口 HTML + 同目录代码文件(前端渲染「预览」页签)
|
|
@@ -587,7 +607,8 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
587
607
|
offset=(q.get("offset") or ["0"])[0],
|
|
588
608
|
limit=(q.get("limit") or [""])[0],
|
|
589
609
|
source=(q.get("source") or [""])[0],
|
|
590
|
-
q=(q.get("q") or [""])[0]
|
|
610
|
+
q=(q.get("q") or [""])[0],
|
|
611
|
+
installed=(q.get("installed") or [""])[0]))
|
|
591
612
|
return self._json(404, {"error": "unknown api"})
|
|
592
613
|
# 静态文件:单文件或 UI 子目录文件(如 icons/icon-192.png);
|
|
593
614
|
# _static 内的 parents 校验确保解析后仍在 UI_DIR 内,防穿越
|
|
@@ -688,6 +709,20 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
688
709
|
return self._api_dir_save()
|
|
689
710
|
if path == "/api/pick_folder":
|
|
690
711
|
return self._api_pick_folder()
|
|
712
|
+
m = re.match(r"^/api/operations/([^/]+)/reconcile$", path)
|
|
713
|
+
if m:
|
|
714
|
+
from core import operations
|
|
715
|
+
body = self._body() or {}
|
|
716
|
+
status = str(body.get("status") or "").strip().lower()
|
|
717
|
+
if status not in ("confirmed", "failed", "unknown"):
|
|
718
|
+
return self._json(400, {"error": "status 必须是 confirmed、failed 或 unknown"})
|
|
719
|
+
ok = operations.reconcile(m.group(1), status,
|
|
720
|
+
remote_receipt=body.get("remote_receipt") or "",
|
|
721
|
+
error=body.get("error") or "",
|
|
722
|
+
metadata=body.get("metadata"))
|
|
723
|
+
if not ok:
|
|
724
|
+
return self._json(404, {"error": "操作不存在或状态无效"})
|
|
725
|
+
return self._json(200, {"ok": True, "operation": operations.get(m.group(1))})
|
|
691
726
|
m = re.match(r"^/api/tasks/([^/]+)/(archive|delete|retry|rename|continue|params)$", path)
|
|
692
727
|
if m:
|
|
693
728
|
if m.group(2) == "archive":
|
|
@@ -744,8 +779,13 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
744
779
|
elif m.group(2) == "rename":
|
|
745
780
|
ok, err = store.rename_task(m.group(1), self._body().get("title"))
|
|
746
781
|
elif m.group(2) == "params":
|
|
747
|
-
# 对话条三件套(mode/thinking/direct
|
|
782
|
+
# 对话条三件套(mode/thinking/direct 模型)随发随改:只许空闲态改。
|
|
783
|
+
# 成功回传最新修订号(expected_rev CAS 的续用凭据,前端连推三件
|
|
784
|
+
# 时靠它避免后两次推成过期写入)
|
|
748
785
|
ok, err = store.update_task_params(m.group(1), self._body())
|
|
786
|
+
if ok:
|
|
787
|
+
_t = store.get_task(m.group(1)) or {}
|
|
788
|
+
return self._json(200, {"ok": True, "rev": _t.get("rev")})
|
|
749
789
|
else:
|
|
750
790
|
ok, err = store.delete_task(m.group(1))
|
|
751
791
|
return self._json(400, {"error": err}) if not ok else self._json(200, {"ok": True})
|
|
@@ -987,6 +1027,15 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
987
1027
|
from core import skills
|
|
988
1028
|
n = skills.learn_from_run((self._body().get("run_id") or ""))
|
|
989
1029
|
return self._json(200, {"ok": True, "learned": n})
|
|
1030
|
+
if path == "/api/skills/evolve":
|
|
1031
|
+
# 教训晋升草稿(ECC /evolve 借鉴):高可信教训聚合成用户包草稿,
|
|
1032
|
+
# 默认停用待人工审阅;返回收录概况
|
|
1033
|
+
from core import skills
|
|
1034
|
+
fname, n = skills.evolve_lessons()
|
|
1035
|
+
if not fname:
|
|
1036
|
+
return self._json(200, {"ok": True, "file": None, "count": 0,
|
|
1037
|
+
"note": "no eligible lessons"})
|
|
1038
|
+
return self._json(200, {"ok": True, "file": fname, "count": n})
|
|
990
1039
|
if path == "/api/knowledge/op":
|
|
991
1040
|
from core import knowledge
|
|
992
1041
|
body = self._body()
|
|
@@ -1279,9 +1328,15 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1279
1328
|
# 否则 "remote" 会被当成包名吞掉
|
|
1280
1329
|
if path == "/api/market/remote/refresh":
|
|
1281
1330
|
return self._json(200, market_remote.refresh((self._body() or {}).get("source")))
|
|
1331
|
+
if path == "/api/market/remote/preview":
|
|
1332
|
+
# 安装前预览(先看后装):下载+剥离检查但不落盘,带 token 两阶段安装
|
|
1333
|
+
res, err = market_remote.preview_remote((self._body() or {}).get("id") or "")
|
|
1334
|
+
return self._json(400, {"error": err}) if err else self._json(200, res)
|
|
1282
1335
|
m = re.match(r"^/api/market/remote/install$", path)
|
|
1283
1336
|
if m:
|
|
1284
|
-
|
|
1337
|
+
body = self._body() or {}
|
|
1338
|
+
res, err = market_remote.install_remote(body.get("id") or "",
|
|
1339
|
+
body.get("token") or "")
|
|
1285
1340
|
return self._json(400, {"error": err}) if err else self._json(200, res)
|
|
1286
1341
|
m = re.match(r"^/api/market/([^/]+)/(install|remove)$", path)
|
|
1287
1342
|
if m:
|
|
@@ -1333,19 +1388,61 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1333
1388
|
if not task:
|
|
1334
1389
|
return self._json(404, {"error": "任务不存在"})
|
|
1335
1390
|
if op == "git-merge":
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1391
|
+
from core import operations
|
|
1392
|
+
operation_id = operations.begin(
|
|
1393
|
+
"git:%s:merge" % task_id,
|
|
1394
|
+
{"task_id": task_id, "branch": task.get("git_branch") or ""},
|
|
1395
|
+
task_id=task_id, metadata={"action": "merge"})
|
|
1396
|
+
remote_applied = False
|
|
1397
|
+
try:
|
|
1398
|
+
ok, err, info = gitmod.merge_task_branch(task.get("workdir"), task)
|
|
1399
|
+
if not ok:
|
|
1400
|
+
operations.fail(operation_id, err, metadata={"action": "merge"})
|
|
1401
|
+
return self._json(400, {"error": err})
|
|
1402
|
+
remote_applied = True
|
|
1403
|
+
store.set_task_git_state(task_id, "merged")
|
|
1404
|
+
operations.confirm(operation_id, remote_receipt=(info or {}).get("commit") or "",
|
|
1405
|
+
metadata={"action": "merge"})
|
|
1406
|
+
return self._json(200, {"ok": True, "operation_id": operation_id,
|
|
1407
|
+
**(info or {})})
|
|
1408
|
+
except Exception as exc:
|
|
1409
|
+
# The remote merge may have happened before local state
|
|
1410
|
+
# persistence failed; keep the operation auditable and
|
|
1411
|
+
# prevent a blind retry from duplicating the merge.
|
|
1412
|
+
if remote_applied:
|
|
1413
|
+
operations.mark_unknown(operation_id,
|
|
1414
|
+
"Git 合并可能已完成,但本地状态回写失败:%s" % exc,
|
|
1415
|
+
metadata={"action": "merge"})
|
|
1416
|
+
else:
|
|
1417
|
+
operations.finish_exception(operation_id, exc)
|
|
1418
|
+
return self._json(500, {"error": "Git 合并收口失败:%s" % exc,
|
|
1419
|
+
"operation_id": operation_id})
|
|
1341
1420
|
body = self._body()
|
|
1342
1421
|
if not body.get("confirm"):
|
|
1343
1422
|
return self._json(400, {"error": "丢弃任务分支不可恢复,需要 confirm=true 二次确认"})
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1423
|
+
from core import operations
|
|
1424
|
+
operation_id = operations.begin(
|
|
1425
|
+
"git:%s:discard" % task_id, {"task_id": task_id}, task_id=task_id,
|
|
1426
|
+
metadata={"action": "discard"})
|
|
1427
|
+
remote_applied = False
|
|
1428
|
+
try:
|
|
1429
|
+
ok, err = gitmod.discard_task_branch(task.get("workdir"), task)
|
|
1430
|
+
if not ok:
|
|
1431
|
+
operations.fail(operation_id, err, metadata={"action": "discard"})
|
|
1432
|
+
return self._json(400, {"error": err})
|
|
1433
|
+
remote_applied = True
|
|
1434
|
+
store.set_task_git_state(task_id, "discarded")
|
|
1435
|
+
operations.confirm(operation_id, metadata={"action": "discard"})
|
|
1436
|
+
return self._json(200, {"ok": True, "operation_id": operation_id})
|
|
1437
|
+
except Exception as exc:
|
|
1438
|
+
if remote_applied:
|
|
1439
|
+
operations.mark_unknown(operation_id,
|
|
1440
|
+
"Git 丢弃可能已完成,但本地状态回写失败:%s" % exc,
|
|
1441
|
+
metadata={"action": "discard"})
|
|
1442
|
+
else:
|
|
1443
|
+
operations.finish_exception(operation_id, exc)
|
|
1444
|
+
return self._json(500, {"error": "Git 丢弃收口失败:%s" % exc,
|
|
1445
|
+
"operation_id": operation_id})
|
|
1349
1446
|
|
|
1350
1447
|
def _api_book_meta_generate(self, task_id):
|
|
1351
1448
|
"""作品信息一键生成(POST /api/tasks/<id>/book-meta,body: {platform})。
|
|
@@ -1967,21 +2064,36 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1967
2064
|
msgs = r.get("messages") or []
|
|
1968
2065
|
steps = r.get("steps") or []
|
|
1969
2066
|
# 消息没有日期(只有 HH:MM:SS),跨 run 不能按时间混排;用语义顺序:
|
|
1970
|
-
# 已消费消息(驱动了本轮回答)→ 已完成步骤 →
|
|
1971
|
-
#
|
|
1972
|
-
#
|
|
2067
|
+
# 已消费消息(驱动了本轮回答)→ 已完成步骤 → 运行中/排队步骤 →
|
|
2068
|
+
# 未消费消息。运行中的回复卡锚在开跑位置,中途追话(指挥信箱,还没
|
|
2069
|
+
# 轮到消费)是比卡更新的排队件,必须落在卡下——否则 21:31 开跑的卡
|
|
2070
|
+
# 排在 21:42/21:43 的追问后面,被读成「回复串到了别人的问题」,且步骤
|
|
2071
|
+
# 收尾时消息会整组跳位(2026-09-24 用户实测)。
|
|
2072
|
+
# 排队卡(还没开跑、无 started_at)反向兼容:拿 run 创建时刻当锚点,
|
|
2073
|
+
# 早于锚点的未消费消息(触发本轮排队的那条)仍排在卡前,开跑消费后
|
|
2074
|
+
# 位置不动、时间线不跳变;晚于锚点的才是中途追话,沉底等下一轮。
|
|
2075
|
+
pending = [m for m in msgs if not m.get("consumed")]
|
|
1973
2076
|
for m in msgs:
|
|
1974
2077
|
if m.get("consumed"):
|
|
1975
2078
|
_emit_msg(m)
|
|
1976
2079
|
for s in steps:
|
|
1977
2080
|
if (s.get("status") or "") == "done":
|
|
1978
2081
|
_emit_step(s, r.get("id") or run_id)
|
|
1979
|
-
for
|
|
1980
|
-
|
|
2082
|
+
live_steps = [s for s in steps if (s.get("status") or "") != "done"]
|
|
2083
|
+
if not live_steps:
|
|
2084
|
+
for m in pending:
|
|
2085
|
+
_emit_msg(m)
|
|
2086
|
+
continue
|
|
2087
|
+
rc = str(r.get("created_at") or "")
|
|
2088
|
+
anchor = (live_steps[0].get("started_at") or "") or (rc[11:19] if len(rc) >= 19 else "")
|
|
2089
|
+
for m in pending:
|
|
2090
|
+
if str(m.get("created_at") or "") <= anchor:
|
|
2091
|
+
_emit_msg(m)
|
|
2092
|
+
for s in live_steps:
|
|
2093
|
+
_emit_step(s, r.get("id") or run_id)
|
|
2094
|
+
for m in pending:
|
|
2095
|
+
if str(m.get("created_at") or "") > anchor:
|
|
1981
2096
|
_emit_msg(m)
|
|
1982
|
-
for s in steps:
|
|
1983
|
-
if (s.get("status") or "") != "done":
|
|
1984
|
-
_emit_step(s, r.get("id") or run_id)
|
|
1985
2097
|
return self._json(200, {
|
|
1986
2098
|
"run_id": run_id, "status": run.get("status") or "",
|
|
1987
2099
|
"engine": engine,
|
|
@@ -2627,6 +2739,13 @@ def main():
|
|
|
2627
2739
|
n_mg = store.recover_interrupted_mgmt()
|
|
2628
2740
|
if n_mg:
|
|
2629
2741
|
print("[CodeBee] 崩溃恢复:%d 个遗留管理操作标记为 failed(interrupted at startup)" % n_mg)
|
|
2742
|
+
try:
|
|
2743
|
+
from core import operations
|
|
2744
|
+
n_op = operations.recover_pending()
|
|
2745
|
+
if n_op:
|
|
2746
|
+
print("[CodeBee] 外部写入对账:%d 个无回执操作标记为 unknown" % n_op)
|
|
2747
|
+
except Exception:
|
|
2748
|
+
pass
|
|
2630
2749
|
try:
|
|
2631
2750
|
# 孤儿 CLI 清扫走后台线程:PowerShell Get-CimInstance 在部分机器上会慢满
|
|
2632
2751
|
# timeout(真实装机 60s,启动被白拖一分钟且无任何提示——看门狗堆栈抓到)。
|