codebee 0.1.69 → 0.1.71

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.
Files changed (133) hide show
  1. package/CHANGELOG.md +589 -543
  2. package/LICENSE +21 -21
  3. package/README.md +509 -508
  4. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +88 -0
  5. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +126 -0
  6. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  7. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +210 -0
  8. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  9. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  10. package/app/.playwright-cli/run-detail-after.png +0 -0
  11. package/app/browser-final-1440.png +0 -0
  12. package/app/browser-final-390.png +0 -0
  13. package/app/browser-google-fallback.png +0 -0
  14. package/app/browser-google-test.png +0 -0
  15. package/app/browser-workbench-1440.png +0 -0
  16. package/app/browser-workbench-390.png +0 -0
  17. package/app/browser-workspace-google.png +0 -0
  18. package/app/browser-workspace-smoke.png +0 -0
  19. package/app/browser-workspace.png +0 -0
  20. package/app/core/acceptance.py +100 -100
  21. package/app/core/aiflavor.py +197 -197
  22. package/app/core/artifacts.py +148 -148
  23. package/app/core/assets.py +110 -110
  24. package/app/core/attachments.py +623 -623
  25. package/app/core/audit.py +30 -0
  26. package/app/core/automation.py +107 -12
  27. package/app/core/backup.py +470 -470
  28. package/app/core/beekeeper.py +183 -183
  29. package/app/core/benchstore.py +95 -0
  30. package/app/core/board.py +209 -209
  31. package/app/core/bookmeta.py +935 -930
  32. package/app/core/bookmeta_catalog.py +180 -180
  33. package/app/core/branching.py +137 -137
  34. package/app/core/builtin_agent.py +1673 -1234
  35. package/app/core/capability.py +144 -144
  36. package/app/core/catalog.py +7 -1
  37. package/app/core/chaptersafe.py +32 -32
  38. package/app/core/cleanup.py +303 -303
  39. package/app/core/compaction.py +201 -201
  40. package/app/core/covergen.py +356 -356
  41. package/app/core/defectretro.py +43 -43
  42. package/app/core/diagnostics.py +114 -114
  43. package/app/core/dispatch.py +190 -173
  44. package/app/core/dispatch_log.py +128 -128
  45. package/app/core/env_scrub.py +85 -85
  46. package/app/core/error_codes.py +11 -2
  47. package/app/core/errorlog.py +137 -137
  48. package/app/core/evalbench.py +720 -0
  49. package/app/core/evaluation.py +174 -174
  50. package/app/core/failure_notify.py +290 -290
  51. package/app/core/flows.py +234 -9
  52. package/app/core/goal_service.py +167 -167
  53. package/app/core/health.py +374 -374
  54. package/app/core/history.py +38 -38
  55. package/app/core/hooks.py +162 -162
  56. package/app/core/jobs.py +22 -1
  57. package/app/core/manager.py +2143 -2122
  58. package/app/core/market_remote.py +10 -0
  59. package/app/core/mcp_client.py +287 -0
  60. package/app/core/mocks.py +64 -64
  61. package/app/core/modelhub.py +185 -2
  62. package/app/core/notify.py +201 -106
  63. package/app/core/novel_quality.py +189 -0
  64. package/app/core/operations.py +228 -228
  65. package/app/core/paihang.py +138 -138
  66. package/app/core/paths.py +4 -0
  67. package/app/core/pipeline.py +426 -28
  68. package/app/core/planner.py +243 -22
  69. package/app/core/plugins.py +525 -0
  70. package/app/core/portguard.py +111 -111
  71. package/app/core/preview.py +293 -293
  72. package/app/core/publish/__init__.py +7 -7
  73. package/app/core/publish/auto.py +367 -367
  74. package/app/core/publish/browser.py +655 -539
  75. package/app/core/publish/fanqie.py +165 -159
  76. package/app/core/publish/flow.py +589 -440
  77. package/app/core/publish/flows-fanqie-calibrated.json +336 -332
  78. package/app/core/publish/ledger.py +229 -203
  79. package/app/core/publish/manager.py +824 -692
  80. package/app/core/publish/qimao.py +146 -141
  81. package/app/core/publish/ws.py +139 -139
  82. package/app/core/redact.py +38 -38
  83. package/app/core/remotesync.py +121 -0
  84. package/app/core/repeat_guard.py +129 -129
  85. package/app/core/router.py +398 -398
  86. package/app/core/runner.py +2142 -2067
  87. package/app/core/selfupdate.py +384 -384
  88. package/app/core/session_log.py +161 -161
  89. package/app/core/sessions.py +312 -312
  90. package/app/core/settings.py +42 -4
  91. package/app/core/settings_schema.py +270 -262
  92. package/app/core/share_page.py +114 -114
  93. package/app/core/skill_scan.py +122 -122
  94. package/app/core/skillpacks/fanqie-novel.md +80 -80
  95. package/app/core/skillpacks/market/character-bible.md +66 -66
  96. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  97. package/app/core/skillpacks/market/git-workflow.md +57 -57
  98. package/app/core/skillpacks/market/release-notes.md +72 -72
  99. package/app/core/skillpacks/market/weekly-report.md +71 -71
  100. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  101. package/app/core/step_runner.py +132 -132
  102. package/app/core/store.py +30 -8
  103. package/app/core/task_compile.py +199 -199
  104. package/app/core/telemetry.py +292 -292
  105. package/app/core/tlsctx.py +64 -64
  106. package/app/core/token_meter.py +147 -147
  107. package/app/core/tracing.py +144 -144
  108. package/app/core/upstream.py +41 -41
  109. package/app/core/usage.py +1035 -862
  110. package/app/core/volumes.py +389 -389
  111. package/app/core/wxdigest.py +858 -858
  112. package/app/core/zentao.py +2614 -2465
  113. package/app/current-browser.png +0 -0
  114. package/app/main.py +316 -17
  115. package/app/mcp_server.py +175 -0
  116. package/app/pet.py +83 -22
  117. package/app/pick_dialog.py +110 -110
  118. package/app/plugins/code-review-helper/.codex-plugin/plugin.json +18 -0
  119. package/app/plugins/code-review-helper/skills/review.md +16 -0
  120. package/app/ui/app.js +1391 -77
  121. package/app/ui/artdiff.js +74 -74
  122. package/app/ui/board.html +162 -162
  123. package/app/ui/board.js +305 -305
  124. package/app/ui/i18n.js +249 -7
  125. package/app/ui/icons/bee.svg +79 -79
  126. package/app/ui/index.html +277 -13
  127. package/app/ui/manifest.json +16 -16
  128. package/app/ui/qrcode.js +2297 -2297
  129. package/app/ui/style.css +845 -12
  130. package/app//344/275/234/345/223/201/344/277/241/346/201/257-/344/270/203/347/214/253.md +18 -0
  131. package/bin/tutti.js +147 -147
  132. package/package.json +47 -46
  133. package/tools/wx_reader.py +261 -261
@@ -0,0 +1,30 @@
1
+ # -*- coding: utf-8 -*-
2
+ """轻量审计日志:不可逆操作留痕(当前仅任务删除)。
3
+
4
+ 删除任务连运行记录一起物理清除且无回收站,出争议时无据可查
5
+ (2026-09-28 连载根任务被删案:谁删的、何时删的全无线索)。
6
+ 这里按月追加 jsonl 到 data/audit/,一行一对象、正序追加,
7
+ 与用量台账同款纪律。审计失败绝不拖垮主流程。
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import time
13
+ from pathlib import Path
14
+
15
+ from . import paths
16
+
17
+
18
+ def record(event: str, detail: dict) -> None:
19
+ """追加一条审计记录(event 即台账名,如 task_delete)。"""
20
+ try:
21
+ day = time.strftime("%Y-%m")
22
+ d = Path(paths.DATA_DIR) / "audit"
23
+ d.mkdir(parents=True, exist_ok=True)
24
+ line = json.dumps(
25
+ {"ts": time.strftime("%Y-%m-%d %H:%M:%S"), "event": event, **detail},
26
+ ensure_ascii=False)
27
+ with open(d / ("%s-%s.jsonl" % (event, day)), "a", encoding="utf-8") as f:
28
+ f.write(line + "\n")
29
+ except Exception:
30
+ pass
@@ -3,19 +3,18 @@
3
3
 
4
4
  数据落盘 <data>/automation.json(tmp + os.replace 原子写;data 目录由 paths.py
5
5
  决定,TUTTI_DATA 环境变量感知)。调度是一个 daemon 线程,每 TICK_SECONDS 秒
6
- 扫一遍到期任务:到点即复用与 /api/tasks 完全相同的链路(store.create_task →
7
- store.create_run → jobs.enqueue)拉起一次真实运行,不自己造运行器。单次触发
6
+ 扫一遍到期任务:首次到点按 /api/tasks 的链路创建一个持久任务,后续触发复用
7
+ 该任务并只新增 run 记录,再交给 jobs.enqueue 拉起真实运行。单次触发
8
8
  失败只把 last_status 记为 error 并推进 next_run,调度线程绝不允许因异常退出。
9
9
 
10
10
  任务模型:
11
11
  {id, name, prompt, workdir, kind: daily|interval|weekly|once, time: "HH:MM",
12
12
  interval_hours, weekday(0-6 周一=0), run_at(once 用, ISO), flow(编排流程 id),
13
13
  mode(编排模式), thinking(思考程度), direct_provider_id/direct_model(仅 direct 流程),
14
- enabled, created_at, last_run, next_run, run_count, last_status}
14
+ enabled, created_at, last_run, next_run, run_count, last_status, task_id}
15
15
 
16
16
  运行偏好(mode/thinking/对话模型)与新建任务 Composer 的三颗胶囊同一套取值,
17
- 到点拉起时原样透传给 store.create_task——定时任务不是另一条执行链,用户在这里
18
- 选的编排强度与模型偏好必须与手动建任务完全等价。
17
+ 首次创建时原样透传给 store.create_task;配置身份变化时才建立新的任务上下文。
19
18
 
20
19
  重启语义:错过的 once 不补跑(启动恢复时直接停用);daily/weekly/interval
21
20
  重算 next_run 到下一个未来时刻即可,不追赶停机期间错过的周期。
@@ -43,11 +42,24 @@ from . import jobs, paths, store
43
42
  log = logging.getLogger(__name__)
44
43
 
45
44
  _LOCK = threading.RLock()
45
+ # Scheduler ticks and the manual "run now" action can arrive concurrently.
46
+ # Keep the active-run check and enqueue decision single-flight per process.
47
+ _LAUNCH_LOCK = threading.RLock()
46
48
  _FILE = paths.DATA_DIR / "automation.json"
47
49
  _TASKS = {} # id → task dict(内存真源;落盘为 {"version":1,"tasks":[全部任务]})
48
50
  _LOADED = False
49
51
  _STARTED = False
50
52
 
53
+ # 模块外 tick 钩子(main.py 启动注入,如 evalbench.fire_due 定时回归):
54
+ # automation 不点名任何业务模块——注入方向 main(L3)→钩子,本模块静态图保持干净
55
+ _TICK_HOOKS = []
56
+
57
+
58
+ def register_tick_hook(fn):
59
+ """注册一个每 tick 调用的零参函数(自节流,异常自行吞掉)。"""
60
+ if callable(fn) and fn not in _TICK_HOOKS:
61
+ _TICK_HOOKS.append(fn)
62
+
51
63
  TICK_SECONDS = 25 # 调度扫描间隔:到点触发误差不超过半个周期
52
64
  KINDS = ("daily", "interval", "weekly", "once")
53
65
  INTERVAL_MIN, INTERVAL_MAX = 1, 720 # interval_hours 合法区间(小时)
@@ -65,7 +77,7 @@ _DEFAULTS = {"id": "", "name": "", "prompt": "", "workdir": "", "kind": "", "tim
65
77
  "mode": "auto", "thinking": "standard",
66
78
  "direct_provider_id": "", "direct_model": "",
67
79
  "enabled": False, "created_at": "", "last_run": "", "next_run": "",
68
- "run_count": 0, "last_status": "", "last_run_id": ""}
80
+ "run_count": 0, "last_status": "", "last_run_id": "", "task_id": ""}
69
81
 
70
82
  # 允许通过 update() 修改的字段(id/created_at/run_count 等运行痕迹不可改)
71
83
  _UPDATABLE = ("name", "prompt", "workdir", "kind", "time", "interval_hours",
@@ -277,6 +289,7 @@ def _normalize(d):
277
289
  except (TypeError, ValueError):
278
290
  t["run_count"] = 0
279
291
  t["enabled"] = bool(t["enabled"])
292
+ t["task_id"] = str(t.get("task_id") or "").strip()
280
293
  return t
281
294
 
282
295
 
@@ -333,6 +346,60 @@ def _recover_after_restart():
333
346
 
334
347
  # ---------------------------------------------------------------- 运行拉起
335
348
 
349
+
350
+ class _AutomationBusy(RuntimeError):
351
+ """The persistent task already has a queued or running run."""
352
+
353
+
354
+ def _active_run(task_id):
355
+ """Return an active run for a persistent task, if any."""
356
+ try:
357
+ for run in store.task_runs(task_id):
358
+ if run.get("status") in ("queued", "running"):
359
+ return run
360
+ except Exception:
361
+ # Fail closed: if the run index cannot be read, do not risk launching
362
+ # a duplicate run against the same persistent task.
363
+ log.debug("automation: active-run check failed task=%s", task_id,
364
+ exc_info=True)
365
+ return True
366
+ return None
367
+
368
+
369
+ def _effective_workdir(value):
370
+ """Resolve an automation workdir the same way store.create_task does."""
371
+ requested = str(value or "").strip()
372
+ if not requested:
373
+ from . import settings
374
+ requested = settings.default_workdir()
375
+ return os.path.normcase(os.path.abspath(os.path.expanduser(requested)))
376
+
377
+
378
+ def _persistent_task_compatible(task, payload):
379
+ """Only reuse a backing task while its execution identity is unchanged."""
380
+ if not task or task.get("type") != payload.get("type"):
381
+ return False
382
+ if task.get("goal") != payload.get("goal"):
383
+ return False
384
+ if _effective_workdir(task.get("workdir")) != _effective_workdir(payload.get("workdir")):
385
+ return False
386
+ for key in ("mode", "thinking", "direct_provider_id", "direct_model"):
387
+ if (task.get(key) or "") != (payload.get(key) or ""):
388
+ return False
389
+ return True
390
+
391
+
392
+ def _bind_task_id(automation_id, task_id):
393
+ """Persist the CodeBee task backing one schedule."""
394
+ if not automation_id or not task_id:
395
+ return
396
+ with _LOCK:
397
+ current = _TASKS.get(automation_id)
398
+ if current is None or current.get("task_id") == task_id:
399
+ return
400
+ current["task_id"] = task_id
401
+ _save_locked()
402
+
336
403
  def _launch_run(t):
337
404
  """拉起一次真实编排运行:与 main.py 的 /api/tasks 走同一条链路
338
405
  (store.create_task → store.create_run → jobs 直接启动),返回 run_id。
@@ -353,12 +420,27 @@ def _launch_run(t):
353
420
  task = None
354
421
  run = None
355
422
  try:
356
- task = store.create_task(payload)
357
- run = store.create_run("orchestration", task["title"], task_id=task["id"])
358
- store.update_task_status(task["id"], "queued")
359
- jobs.enqueue({"kind": "orchestration", "run_id": run["id"],
360
- "task_id": task["id"]})
361
- return run["id"]
423
+ with _LAUNCH_LOCK:
424
+ persistent_id = str(t.get("task_id") or "").strip()
425
+ if persistent_id:
426
+ task = store.get_task(persistent_id)
427
+ if task is None or not _persistent_task_compatible(task, payload):
428
+ # The user may have deleted the backing task. Recreate the
429
+ # persistent conversation when its definition changed.
430
+ task = None
431
+ elif _active_run(persistent_id):
432
+ raise _AutomationBusy("上一次定时运行仍在执行,跳过本次重叠触发")
433
+ if task is None:
434
+ task = store.create_task(payload)
435
+ t["task_id"] = task["id"]
436
+ _bind_task_id(t.get("id"), task["id"])
437
+ run = store.create_run("orchestration", task["title"], task_id=task["id"])
438
+ store.update_task_status(task["id"], "queued")
439
+ jobs.enqueue({"kind": "orchestration", "run_id": run["id"],
440
+ "task_id": task["id"]})
441
+ return run["id"]
442
+ except _AutomationBusy:
443
+ raise
362
444
  except Exception:
363
445
  # 启动失败必须收口,不能留下看似仍在启动的任务/run。
364
446
  # Keep the public error generic; the detailed traceback stays in the
@@ -389,6 +471,9 @@ def _fire(snapshot, now):
389
471
  run_id = ""
390
472
  try:
391
473
  run_id = _launch_run(snapshot) or ""
474
+ except _AutomationBusy as e:
475
+ log.info("automation: 跳过重叠触发 %s: %s", snapshot.get("id"), e)
476
+ status = "busy"
392
477
  except Exception as e:
393
478
  log.warning("automation: 定时触发 %s 失败: %r", snapshot.get("id"), e)
394
479
  status = "error"
@@ -515,6 +600,13 @@ def _tick():
515
600
  (",%d 项失败" % len(r.get("errors") or [])) if r.get("errors") else "")
516
601
  except Exception:
517
602
  log.debug("automation: 每日清理跳过", exc_info=True)
603
+ # 模块外 tick 钩子(如评测台定时回归):钩子自节流、自行吞错
604
+ for _hook in list(_TICK_HOOKS):
605
+ try:
606
+ _hook()
607
+ except Exception:
608
+ log.debug("automation: tick 钩子跳过", exc_info=True)
609
+
518
610
 
519
611
 
520
612
  def _loop():
@@ -637,6 +729,9 @@ def run_now(tid):
637
729
  with _LOCK:
638
730
  snapshot = dict(_TASKS[tid])
639
731
  run_id = _launch_run(snapshot) or ""
732
+ except _AutomationBusy as e:
733
+ log.info("automation: 手动触发 %s 跳过重叠运行: %s", tid, e)
734
+ status = "busy"
640
735
  except Exception as e:
641
736
  log.warning("automation: 手动触发 %s 失败: %r", tid, e)
642
737
  status = "error"