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.
Files changed (138) hide show
  1. package/CHANGELOG.md +725 -720
  2. package/LICENSE +21 -21
  3. package/README.md +515 -509
  4. package/app/core/acceptance.py +100 -100
  5. package/app/core/agent_context.py +100 -0
  6. package/app/core/aiflavor.py +197 -197
  7. package/app/core/analytics.py +38 -38
  8. package/app/core/artifacts.py +148 -148
  9. package/app/core/assets.py +110 -110
  10. package/app/core/attachments.py +623 -623
  11. package/app/core/audit.py +30 -30
  12. package/app/core/backup.py +470 -470
  13. package/app/core/beekeeper.py +183 -183
  14. package/app/core/benchstore.py +102 -95
  15. package/app/core/board.py +209 -209
  16. package/app/core/bookmeta.py +1036 -1036
  17. package/app/core/bookmeta_catalog.py +180 -180
  18. package/app/core/branching.py +137 -137
  19. package/app/core/builtin_agent.py +1680 -1680
  20. package/app/core/capability.py +144 -144
  21. package/app/core/chaptersafe.py +32 -32
  22. package/app/core/checkpoints.py +143 -0
  23. package/app/core/cleanup.py +303 -303
  24. package/app/core/compaction.py +201 -201
  25. package/app/core/contracts.py +255 -255
  26. package/app/core/covergen.py +356 -356
  27. package/app/core/defectretro.py +43 -43
  28. package/app/core/diagnostics.py +114 -114
  29. package/app/core/dispatch.py +198 -191
  30. package/app/core/dispatch_log.py +128 -128
  31. package/app/core/env_scrub.py +85 -85
  32. package/app/core/errorlog.py +137 -137
  33. package/app/core/eval_matrix.py +82 -0
  34. package/app/core/evalbench.py +184 -91
  35. package/app/core/evaluation.py +174 -174
  36. package/app/core/failure_notify.py +290 -290
  37. package/app/core/flow_graph.py +33 -0
  38. package/app/core/goal_service.py +167 -167
  39. package/app/core/health.py +389 -389
  40. package/app/core/history.py +38 -38
  41. package/app/core/hooks.py +162 -162
  42. package/app/core/jobs.py +25 -4
  43. package/app/core/knowledge_pipeline.py +86 -0
  44. package/app/core/manager.py +2232 -2232
  45. package/app/core/mocks.py +64 -64
  46. package/app/core/notify.py +201 -201
  47. package/app/core/novel_quality.py +181 -189
  48. package/app/core/openapi.py +177 -154
  49. package/app/core/operations.py +228 -228
  50. package/app/core/paihang.py +145 -145
  51. package/app/core/pipeline.py +43 -19
  52. package/app/core/planner.py +17 -11
  53. package/app/core/policy.py +52 -0
  54. package/app/core/portguard.py +111 -111
  55. package/app/core/presence.py +77 -77
  56. package/app/core/preview.py +293 -293
  57. package/app/core/prompt_guard.py +78 -78
  58. package/app/core/publish/__init__.py +7 -7
  59. package/app/core/publish/auto.py +26 -3
  60. package/app/core/publish/browser.py +655 -655
  61. package/app/core/publish/chaprows.py +138 -0
  62. package/app/core/publish/fanqie.py +210 -165
  63. package/app/core/publish/flow.py +753 -640
  64. package/app/core/publish/flows-fanqie-calibrated.json +349 -336
  65. package/app/core/publish/ledger.py +13 -1
  66. package/app/core/publish/manager.py +1150 -1118
  67. package/app/core/publish/qimao.py +176 -176
  68. package/app/core/publish/volumes.py +108 -0
  69. package/app/core/publish/ws.py +139 -139
  70. package/app/core/quality_gate.py +194 -194
  71. package/app/core/redact.py +38 -38
  72. package/app/core/repeat_guard.py +129 -129
  73. package/app/core/retrieval.py +137 -137
  74. package/app/core/router.py +398 -398
  75. package/app/core/runner.py +2238 -2238
  76. package/app/core/selfupdate.py +384 -384
  77. package/app/core/session_log.py +161 -161
  78. package/app/core/sessions.py +312 -312
  79. package/app/core/settings.py +20 -0
  80. package/app/core/settings_schema.py +270 -270
  81. package/app/core/share_page.py +114 -114
  82. package/app/core/skill_scan.py +139 -139
  83. package/app/core/skillpacks/fanqie-novel.md +8 -6
  84. package/app/core/skillpacks/market/character-bible.md +66 -66
  85. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  86. package/app/core/skillpacks/market/git-workflow.md +57 -57
  87. package/app/core/skillpacks/market/release-notes.md +72 -72
  88. package/app/core/skillpacks/market/weekly-report.md +71 -71
  89. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  90. package/app/core/skillpacks/qimao-signing.md +14 -11
  91. package/app/core/skills.py +32 -21
  92. package/app/core/step_runner.py +132 -132
  93. package/app/core/store.py +9 -0
  94. package/app/core/story_tracking.py +275 -275
  95. package/app/core/task_compile.py +199 -199
  96. package/app/core/telemetry.py +292 -292
  97. package/app/core/tlsctx.py +64 -64
  98. package/app/core/token_meter.py +147 -147
  99. package/app/core/tracing.py +207 -144
  100. package/app/core/upstream.py +41 -41
  101. package/app/core/usage.py +1038 -1038
  102. package/app/core/volumes.py +389 -389
  103. package/app/core/webhooks.py +89 -0
  104. package/app/core/wxdigest.py +858 -858
  105. package/app/core/zentao.py +2692 -2692
  106. package/app/main.py +104 -9
  107. package/app/mcp_server.py +244 -244
  108. package/app/pick_dialog.py +110 -110
  109. package/app/ui/app.js +286 -18
  110. package/app/ui/artdiff.js +74 -74
  111. package/app/ui/assets/community-qr.png +0 -0
  112. package/app/ui/assets/donate-alipay.jpg +0 -0
  113. package/app/ui/assets/donate-qr.jpg +0 -0
  114. package/app/ui/board.html +162 -162
  115. package/app/ui/board.js +305 -305
  116. package/app/ui/i18n.js +33 -0
  117. package/app/ui/icons/bee.svg +79 -79
  118. package/app/ui/index.html +32 -3
  119. package/app/ui/manifest.json +16 -16
  120. package/app/ui/qrcode.js +2297 -2297
  121. package/app/ui/style.css +196 -2
  122. package/bin/tutti.js +147 -147
  123. package/package.json +48 -48
  124. package/tools/wx_reader.py +261 -261
  125. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  126. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  127. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  128. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  129. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  130. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  131. package/app/.playwright-cli/run-detail-after.png +0 -0
  132. package/app/output/_createbook_body.json +0 -1
  133. package/app/output/_probe_fq_click.py +0 -50
  134. package/app/output/_probe_fq_form.py +0 -42
  135. package/app/output/_probe_fq_modal.py +0 -55
  136. package/app/output/_probe_fq_points.py +0 -71
  137. package/app/output/_probe_fq_submit.py +0 -34
  138. package/app/output/_probe_fq_watch.py +0 -62
package/app/main.py CHANGED
@@ -143,6 +143,7 @@ class Handler(BaseHTTPRequestHandler):
143
143
  return cached
144
144
  raw_len = self.headers.get("Content-Length")
145
145
  if not raw_len:
146
+ self._raw_body = b""
146
147
  body = {}
147
148
  else:
148
149
  try:
@@ -158,6 +159,7 @@ class Handler(BaseHTTPRequestHandler):
158
159
  body = {}
159
160
  else:
160
161
  data = self.rfile.read(n)
162
+ self._raw_body = data
161
163
  try:
162
164
  text = data.decode("utf-8")
163
165
  except UnicodeDecodeError:
@@ -171,6 +173,12 @@ class Handler(BaseHTTPRequestHandler):
171
173
  self._parsed_body = body
172
174
  return body
173
175
 
176
+ def _raw_request_body(self):
177
+ """Return the exact bytes used for webhook signature verification."""
178
+ if not hasattr(self, "_raw_body"):
179
+ self._body()
180
+ return getattr(self, "_raw_body", b"")
181
+
174
182
  # ------------------------------------------------------------ 远程访问
175
183
  def _forwarded_ip(self):
176
184
  """经代理进来的真实客户端 IP:X-Forwarded-For 首跳,缺省 CF-Connecting-IP。"""
@@ -300,6 +308,13 @@ class Handler(BaseHTTPRequestHandler):
300
308
  qs = parse_qs(urlparse(self.path).query)
301
309
  fid = (qs.get("id") or [""])[0]
302
310
  return self._json(200, {"versions": flows.flow_versions(fid)})
311
+ m = re.match(r"^/api/flows/([^/]+)/graph$", path)
312
+ if m:
313
+ from core import flow_graph
314
+ try:
315
+ return self._json(200, {"graph": flow_graph.build(m.group(1))})
316
+ except ValueError as exc:
317
+ return self._json(404, {"error": str(exc)})
303
318
  if path == "/api/skills":
304
319
  from core import skills
305
320
  v = skills.view()
@@ -339,8 +354,18 @@ class Handler(BaseHTTPRequestHandler):
339
354
  return self._json(400, {"error": str(exc)})
340
355
  return self._json(200, {"query": (q.get("q") or [""])[0][:500],
341
356
  "results": results})
357
+ if path == "/api/knowledge/pipeline":
358
+ from core import knowledge_pipeline
359
+ q = parse_qs(urlparse(self.path).query)
360
+ return self._json(200, knowledge_pipeline.quality_report(
361
+ (q.get("ingest_id") or [""])[0]))
362
+ if path == "/api/policy":
363
+ from core import policy
364
+ q = parse_qs(urlparse(self.path).query)
365
+ return self._json(200, policy.normalize_sandbox(
366
+ {}, (q.get("workdir") or [settings.default_workdir()])[0]))
342
367
  if path == "/api/settings":
343
- return self._json(200, dict(settings.load(), **jobs.workers_info()))
368
+ return self._json(200, dict(settings.public_view(), **jobs.workers_info()))
344
369
  if path == "/api/settings-v2":
345
370
  # schema 化设置全貌(secret 已脱敏;前端调参卡直读)
346
371
  from core import settings_schema as ss2
@@ -554,6 +579,20 @@ class Handler(BaseHTTPRequestHandler):
554
579
  from core import dispatch_log, tracing
555
580
  return self._json(200, {"trace": tracing.build_trace(
556
581
  run, dispatch_log.replay(run_id=m.group(1), limit=200))})
582
+ m = re.match(r"^/api/runs/([^/]+)/otel$", path)
583
+ if m:
584
+ run = store.get_run(m.group(1))
585
+ if not run:
586
+ return self._json(404, {"error": "not found"})
587
+ from core import dispatch_log, tracing
588
+ trace = tracing.build_trace(run, dispatch_log.replay(run_id=m.group(1), limit=200))
589
+ return self._json(200, tracing.to_otel(trace))
590
+ m = re.match(r"^/api/runs/([^/]+)/checkpoints$", path)
591
+ if m:
592
+ if not store.get_run(m.group(1)):
593
+ return self._json(404, {"error": "not found"})
594
+ from core import checkpoints
595
+ return self._json(200, {"checkpoint": checkpoints.replay_preview(m.group(1))})
557
596
  m = re.match(r"^/api/runs/([^/]+)/preview$", path)
558
597
  if m:
559
598
  # 网页成品预览:入口 HTML + 同目录代码文件(前端渲染「预览」页签)
@@ -947,7 +986,8 @@ class Handler(BaseHTTPRequestHandler):
947
986
  return self._json(200, dict(ok=True, **info))
948
987
  if path == "/api/evalbench/cancel":
949
988
  from core import evalbench
950
- return self._json(200, {"ok": evalbench.cancel()})
989
+ body = self._body() or {}
990
+ return self._json(200, evalbench.cancel(force=bool(body.get("force"))))
951
991
  if path == "/api/evalbench/sample-op":
952
992
  # 自定义样题增删:{op:add|delete, sample:{...}|sid}
953
993
  from core import evalbench
@@ -972,6 +1012,32 @@ class Handler(BaseHTTPRequestHandler):
972
1012
  if err:
973
1013
  return self._json(400, {"error": err})
974
1014
  return self._json(200, dict(ok=True, **res))
1015
+ if path == "/api/eval-matrix/normalize":
1016
+ from core import eval_matrix
1017
+ try:
1018
+ return self._json(200, {"manifest": eval_matrix.normalize_manifest(self._body())})
1019
+ except ValueError as exc:
1020
+ return self._json(400, {"error": str(exc)})
1021
+ if path == "/api/eval-matrix/evaluate":
1022
+ from core import eval_matrix
1023
+ body = self._body() or {}
1024
+ try:
1025
+ report = eval_matrix.evaluate(body.get("manifest"),
1026
+ body.get("results"), body.get("baseline"))
1027
+ except ValueError as exc:
1028
+ return self._json(400, {"error": str(exc)})
1029
+ return self._json(200, {"report": report})
1030
+ if path == "/api/knowledge/pipeline":
1031
+ from core import knowledge_pipeline
1032
+ body = self._body() or {}
1033
+ try:
1034
+ report = knowledge_pipeline.ingest(
1035
+ body.get("source"), body.get("text"), body.get("metadata"),
1036
+ chunk_size=body.get("chunk_size", 1200),
1037
+ overlap=body.get("overlap", 120))
1038
+ except ValueError as exc:
1039
+ return self._json(400, {"error": str(exc)})
1040
+ return self._json(200, {"ok": True, "report": report})
975
1041
  if path == "/api/hooks/run":
976
1042
  return self._api_hook_run()
977
1043
  if path == "/api/health/op":
@@ -1500,9 +1566,9 @@ class Handler(BaseHTTPRequestHandler):
1500
1566
  if path == "/api/settings":
1501
1567
  view, err = settings.save(self._body())
1502
1568
  if err:
1503
- return self._json(400, {"error": err, "settings": view})
1569
+ return self._json(400, {"error": err, "settings": settings.public_view(view)})
1504
1570
  n = jobs.configure(view["max_concurrent_jobs"])
1505
- return self._json(200, {"ok": True, "settings": view, "workers": n})
1571
+ return self._json(200, {"ok": True, "settings": settings.public_view(view), "workers": n})
1506
1572
  m = re.match(r"^/api/settings-v2/([a-z_-]+)$", path)
1507
1573
  if m:
1508
1574
  # schema 化设置写入口:{ops:[{op:"set",path,value}], expected_revision?}
@@ -2143,11 +2209,14 @@ class Handler(BaseHTTPRequestHandler):
2143
2209
  force = bool(body.get("force"))
2144
2210
  force_confirmed = bool(body.get("force_confirmed"))
2145
2211
  force_reason = str(body.get("force_reason") or "").strip()
2212
+ only = body.get("chapters") # 批量选择发布:章号数组(可省=全部待发)
2213
+ kwargs = {"only": only if isinstance(only, list) else None}
2214
+ if force or force_confirmed or force_reason:
2215
+ kwargs.update(force=force, force_confirmed=force_confirmed,
2216
+ force_reason=force_reason)
2146
2217
  ok, err = pub_auto.publish_pending_async(
2147
2218
  task_id, platform, auto_submit=bool(body.get("auto_submit")),
2148
- **({"force": force, "force_confirmed": force_confirmed,
2149
- "force_reason": force_reason}
2150
- if force or force_confirmed or force_reason else {}))
2219
+ **kwargs)
2151
2220
  if not ok:
2152
2221
  payload = {"error": err or "操作失败"}
2153
2222
  if str(err or "").startswith("质量门禁拦截:"):
@@ -3024,10 +3093,13 @@ class Handler(BaseHTTPRequestHandler):
3024
3093
  POST /api/hooks/run,头 X-CodeBee-Token;体 {goal 必填, type, workdir,
3025
3094
  context, title}。令牌取设置 hooks_token(data/settings.json,UI/文件均可
3026
3095
  配置):已配置则必须精确匹配;未配置仅放行本机回环。任务链与 UI 完全相同。"""
3096
+ from core import webhooks
3027
3097
  try:
3028
- tok = str(settings.load().get("hooks_token") or "")
3098
+ cfg = settings.load()
3099
+ tok = str(cfg.get("hooks_token") or "")
3100
+ signing_secret = str(cfg.get("hooks_signing_secret") or "")
3029
3101
  except Exception:
3030
- tok = ""
3102
+ tok, signing_secret = "", ""
3031
3103
  given = (self.headers.get("X-CodeBee-Token") or "").strip()
3032
3104
  if tok:
3033
3105
  if given != tok:
@@ -3037,6 +3109,18 @@ class Handler(BaseHTTPRequestHandler):
3037
3109
  if host not in ("127.0.0.1", "::1", "::ffff:127.0.0.1"):
3038
3110
  return self._json(403, {
3039
3111
  "error": "未配置 hooks.token,仅允许本机触发;请先在设置中配置令牌"})
3112
+ # Optional HMAC envelope for GitHub/GitLab-style integrations. A
3113
+ # configured secret makes the signature mandatory; token auth remains
3114
+ # useful for old local scripts when no secret is configured.
3115
+ if signing_secret:
3116
+ timestamp = self.headers.get("X-CodeBee-Timestamp") or ""
3117
+ signature = (self.headers.get("X-CodeBee-Signature") or
3118
+ self.headers.get("X-Hub-Signature-256") or "")
3119
+ signed = (webhooks.verify(signing_secret, self._raw_request_body(),
3120
+ signature, timestamp) if timestamp else
3121
+ webhooks.verify_raw(signing_secret, self._raw_request_body(), signature))
3122
+ if not signed:
3123
+ return self._json(401, {"error": "Webhook 签名无效或已过期"})
3040
3124
  raw = self._body()
3041
3125
  payload = {}
3042
3126
  for k, cap in (("goal", 4000), ("context", 4000), ("type", 40),
@@ -3050,7 +3134,18 @@ class Handler(BaseHTTPRequestHandler):
3050
3134
  payload["type"] = "direct"
3051
3135
  if not payload.get("title"):
3052
3136
  payload["title"] = payload["goal"][:30]
3137
+ delivery_id = (self.headers.get("X-CodeBee-Delivery") or
3138
+ self.headers.get("X-Delivery-Id") or "").strip()
3139
+ if delivery_id:
3140
+ claimed, info = webhooks.claim_delivery(delivery_id)
3141
+ if not claimed:
3142
+ return self._json(200, {"ok": True, "deduplicated": True,
3143
+ "delivery_id": info.get("delivery_id")})
3053
3144
  status, resp = self._create_and_start(payload)
3145
+ if delivery_id and status != 200:
3146
+ webhooks.release_delivery(delivery_id)
3147
+ if delivery_id:
3148
+ resp = dict(resp, delivery_id=delivery_id)
3054
3149
  return self._json(status, resp)
3055
3150
 
3056
3151
  def _enqueue_run(self, run_id, task_id, job):