codebee 0.1.95 → 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.
Files changed (144) hide show
  1. package/CHANGELOG.md +733 -720
  2. package/LICENSE +21 -21
  3. package/README.md +516 -509
  4. package/app/core/acceptance.py +100 -100
  5. package/app/core/agent_context.py +118 -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 +246 -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 +2055 -1680
  20. package/app/core/capability.py +144 -144
  21. package/app/core/chaptersafe.py +32 -32
  22. package/app/core/checkpoints.py +648 -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/mcp_client.py +200 -29
  46. package/app/core/mocks.py +64 -64
  47. package/app/core/notify.py +201 -201
  48. package/app/core/novel_quality.py +181 -189
  49. package/app/core/openapi.py +177 -154
  50. package/app/core/operations.py +228 -228
  51. package/app/core/paihang.py +145 -145
  52. package/app/core/pipeline.py +319 -59
  53. package/app/core/planner.py +17 -11
  54. package/app/core/policy.py +84 -0
  55. package/app/core/portguard.py +111 -111
  56. package/app/core/presence.py +77 -77
  57. package/app/core/preview.py +293 -293
  58. package/app/core/project_memory.py +139 -0
  59. package/app/core/prompt_guard.py +78 -78
  60. package/app/core/publish/__init__.py +7 -7
  61. package/app/core/publish/auto.py +106 -22
  62. package/app/core/publish/browser.py +694 -655
  63. package/app/core/publish/chaprows.py +138 -0
  64. package/app/core/publish/fanqie.py +212 -165
  65. package/app/core/publish/flow.py +832 -640
  66. package/app/core/publish/flows-fanqie-calibrated.json +361 -336
  67. package/app/core/publish/ledger.py +51 -2
  68. package/app/core/publish/manager.py +1150 -1118
  69. package/app/core/publish/qimao.py +176 -176
  70. package/app/core/publish/volumes.py +108 -0
  71. package/app/core/publish/ws.py +139 -139
  72. package/app/core/quality_gate.py +194 -194
  73. package/app/core/redact.py +38 -38
  74. package/app/core/repeat_guard.py +129 -129
  75. package/app/core/retrieval.py +137 -137
  76. package/app/core/router.py +398 -398
  77. package/app/core/runner.py +2239 -2238
  78. package/app/core/selfupdate.py +384 -384
  79. package/app/core/session_log.py +161 -161
  80. package/app/core/sessions.py +312 -312
  81. package/app/core/settings.py +33 -5
  82. package/app/core/settings_schema.py +282 -271
  83. package/app/core/share_page.py +114 -114
  84. package/app/core/skill_scan.py +139 -139
  85. package/app/core/skillpacks/fanqie-novel.md +8 -6
  86. package/app/core/skillpacks/market/character-bible.md +66 -66
  87. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  88. package/app/core/skillpacks/market/git-workflow.md +57 -57
  89. package/app/core/skillpacks/market/release-notes.md +72 -72
  90. package/app/core/skillpacks/market/weekly-report.md +71 -71
  91. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  92. package/app/core/skillpacks/qimao-signing.md +14 -11
  93. package/app/core/skills.py +32 -21
  94. package/app/core/step_runner.py +132 -132
  95. package/app/core/store.py +24 -0
  96. package/app/core/story_tracking.py +275 -275
  97. package/app/core/task_compile.py +199 -199
  98. package/app/core/telemetry.py +292 -292
  99. package/app/core/tlsctx.py +64 -64
  100. package/app/core/token_meter.py +147 -147
  101. package/app/core/tool_outputs.py +107 -0
  102. package/app/core/tracing.py +207 -144
  103. package/app/core/upstream.py +41 -41
  104. package/app/core/usage.py +1038 -1038
  105. package/app/core/volumes.py +389 -389
  106. package/app/core/webhooks.py +89 -0
  107. package/app/core/wxdigest.py +865 -858
  108. package/app/core/zentao.py +2692 -2692
  109. package/app/main.py +179 -10
  110. package/app/mcp_server.py +244 -244
  111. package/app/pick_dialog.py +110 -110
  112. package/app/ui/app.js +563 -21
  113. package/app/ui/artdiff.js +74 -74
  114. package/app/ui/assets/community-qr.png +0 -0
  115. package/app/ui/assets/donate-alipay.jpg +0 -0
  116. package/app/ui/assets/donate-qr.jpg +0 -0
  117. package/app/ui/assets/workbench-reference.png +0 -0
  118. package/app/ui/board.css +451 -666
  119. package/app/ui/board.html +165 -162
  120. package/app/ui/board.js +509 -305
  121. package/app/ui/hive3d.js +242 -0
  122. package/app/ui/i18n.js +125 -9
  123. package/app/ui/icons/bee.svg +79 -79
  124. package/app/ui/index.html +60 -3
  125. package/app/ui/manifest.json +16 -16
  126. package/app/ui/qrcode.js +2297 -2297
  127. package/app/ui/style.css +189 -9
  128. package/bin/tutti.js +147 -147
  129. package/package.json +49 -48
  130. package/tools/wx_reader.py +261 -261
  131. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  132. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  133. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  134. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  135. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  136. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  137. package/app/.playwright-cli/run-detail-after.png +0 -0
  138. package/app/output/_createbook_body.json +0 -1
  139. package/app/output/_probe_fq_click.py +0 -50
  140. package/app/output/_probe_fq_form.py +0 -42
  141. package/app/output/_probe_fq_modal.py +0 -55
  142. package/app/output/_probe_fq_points.py +0 -71
  143. package/app/output/_probe_fq_submit.py +0 -34
  144. 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。"""
@@ -278,6 +286,13 @@ class Handler(BaseHTTPRequestHandler):
278
286
  if path == "/api/board":
279
287
  # 任务驾驶舱大屏的轻量聚合(KB 级;别让它去拉 /api/state 全量)
280
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)
281
296
  if path == "/api/analytics":
282
297
  from core import analytics
283
298
  return self._json(200, analytics.summary(store))
@@ -300,6 +315,13 @@ class Handler(BaseHTTPRequestHandler):
300
315
  qs = parse_qs(urlparse(self.path).query)
301
316
  fid = (qs.get("id") or [""])[0]
302
317
  return self._json(200, {"versions": flows.flow_versions(fid)})
318
+ m = re.match(r"^/api/flows/([^/]+)/graph$", path)
319
+ if m:
320
+ from core import flow_graph
321
+ try:
322
+ return self._json(200, {"graph": flow_graph.build(m.group(1))})
323
+ except ValueError as exc:
324
+ return self._json(404, {"error": str(exc)})
303
325
  if path == "/api/skills":
304
326
  from core import skills
305
327
  v = skills.view()
@@ -326,6 +348,16 @@ class Handler(BaseHTTPRequestHandler):
326
348
  if path == "/api/knowledge":
327
349
  from core import knowledge
328
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)})
329
361
  if path == "/api/agents/presence":
330
362
  from core import presence
331
363
  return self._json(200, {"agents": presence.list_agents()})
@@ -339,8 +371,18 @@ class Handler(BaseHTTPRequestHandler):
339
371
  return self._json(400, {"error": str(exc)})
340
372
  return self._json(200, {"query": (q.get("q") or [""])[0][:500],
341
373
  "results": results})
374
+ if path == "/api/knowledge/pipeline":
375
+ from core import knowledge_pipeline
376
+ q = parse_qs(urlparse(self.path).query)
377
+ return self._json(200, knowledge_pipeline.quality_report(
378
+ (q.get("ingest_id") or [""])[0]))
379
+ if path == "/api/policy":
380
+ from core import policy
381
+ q = parse_qs(urlparse(self.path).query)
382
+ return self._json(200, policy.normalize_sandbox(
383
+ {}, (q.get("workdir") or [settings.default_workdir()])[0]))
342
384
  if path == "/api/settings":
343
- return self._json(200, dict(settings.load(), **jobs.workers_info()))
385
+ return self._json(200, dict(settings.public_view(), **jobs.workers_info()))
344
386
  if path == "/api/settings-v2":
345
387
  # schema 化设置全貌(secret 已脱敏;前端调参卡直读)
346
388
  from core import settings_schema as ss2
@@ -554,6 +596,24 @@ class Handler(BaseHTTPRequestHandler):
554
596
  from core import dispatch_log, tracing
555
597
  return self._json(200, {"trace": tracing.build_trace(
556
598
  run, dispatch_log.replay(run_id=m.group(1), limit=200))})
599
+ m = re.match(r"^/api/runs/([^/]+)/otel$", path)
600
+ if m:
601
+ run = store.get_run(m.group(1))
602
+ if not run:
603
+ return self._json(404, {"error": "not found"})
604
+ from core import dispatch_log, tracing
605
+ trace = tracing.build_trace(run, dispatch_log.replay(run_id=m.group(1), limit=200))
606
+ return self._json(200, tracing.to_otel(trace))
607
+ m = re.match(r"^/api/runs/([^/]+)/checkpoints$", path)
608
+ if m:
609
+ if not store.get_run(m.group(1)):
610
+ return self._json(404, {"error": "not found"})
611
+ from core import checkpoints
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 "")})
557
617
  m = re.match(r"^/api/runs/([^/]+)/preview$", path)
558
618
  if m:
559
619
  # 网页成品预览:入口 HTML + 同目录代码文件(前端渲染「预览」页签)
@@ -847,6 +907,22 @@ class Handler(BaseHTTPRequestHandler):
847
907
  return self._json(200, {"ok": ok, "output": out})
848
908
  if path == "/api/tasks/clarify":
849
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)})
850
926
  if path == "/api/agents/presence":
851
927
  from core import presence
852
928
  body = self._body() or {}
@@ -947,7 +1023,8 @@ class Handler(BaseHTTPRequestHandler):
947
1023
  return self._json(200, dict(ok=True, **info))
948
1024
  if path == "/api/evalbench/cancel":
949
1025
  from core import evalbench
950
- return self._json(200, {"ok": evalbench.cancel()})
1026
+ body = self._body() or {}
1027
+ return self._json(200, evalbench.cancel(force=bool(body.get("force"))))
951
1028
  if path == "/api/evalbench/sample-op":
952
1029
  # 自定义样题增删:{op:add|delete, sample:{...}|sid}
953
1030
  from core import evalbench
@@ -972,6 +1049,32 @@ class Handler(BaseHTTPRequestHandler):
972
1049
  if err:
973
1050
  return self._json(400, {"error": err})
974
1051
  return self._json(200, dict(ok=True, **res))
1052
+ if path == "/api/eval-matrix/normalize":
1053
+ from core import eval_matrix
1054
+ try:
1055
+ return self._json(200, {"manifest": eval_matrix.normalize_manifest(self._body())})
1056
+ except ValueError as exc:
1057
+ return self._json(400, {"error": str(exc)})
1058
+ if path == "/api/eval-matrix/evaluate":
1059
+ from core import eval_matrix
1060
+ body = self._body() or {}
1061
+ try:
1062
+ report = eval_matrix.evaluate(body.get("manifest"),
1063
+ body.get("results"), body.get("baseline"))
1064
+ except ValueError as exc:
1065
+ return self._json(400, {"error": str(exc)})
1066
+ return self._json(200, {"report": report})
1067
+ if path == "/api/knowledge/pipeline":
1068
+ from core import knowledge_pipeline
1069
+ body = self._body() or {}
1070
+ try:
1071
+ report = knowledge_pipeline.ingest(
1072
+ body.get("source"), body.get("text"), body.get("metadata"),
1073
+ chunk_size=body.get("chunk_size", 1200),
1074
+ overlap=body.get("overlap", 120))
1075
+ except ValueError as exc:
1076
+ return self._json(400, {"error": str(exc)})
1077
+ return self._json(200, {"ok": True, "report": report})
975
1078
  if path == "/api/hooks/run":
976
1079
  return self._api_hook_run()
977
1080
  if path == "/api/health/op":
@@ -1181,6 +1284,35 @@ class Handler(BaseHTTPRequestHandler):
1181
1284
  m = re.match(r"^/api/(tasks|runs)/([^/]+)/reveal$", path)
1182
1285
  if m:
1183
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 ""))
1184
1316
  m = re.match(r"^/api/runs/([^/]+)/messages$", path)
1185
1317
  if m:
1186
1318
  return self._api_add_message(m.group(1))
@@ -1500,9 +1632,9 @@ class Handler(BaseHTTPRequestHandler):
1500
1632
  if path == "/api/settings":
1501
1633
  view, err = settings.save(self._body())
1502
1634
  if err:
1503
- return self._json(400, {"error": err, "settings": view})
1635
+ return self._json(400, {"error": err, "settings": settings.public_view(view)})
1504
1636
  n = jobs.configure(view["max_concurrent_jobs"])
1505
- return self._json(200, {"ok": True, "settings": view, "workers": n})
1637
+ return self._json(200, {"ok": True, "settings": settings.public_view(view), "workers": n})
1506
1638
  m = re.match(r"^/api/settings-v2/([a-z_-]+)$", path)
1507
1639
  if m:
1508
1640
  # schema 化设置写入口:{ops:[{op:"set",path,value}], expected_revision?}
@@ -2143,11 +2275,14 @@ class Handler(BaseHTTPRequestHandler):
2143
2275
  force = bool(body.get("force"))
2144
2276
  force_confirmed = bool(body.get("force_confirmed"))
2145
2277
  force_reason = str(body.get("force_reason") or "").strip()
2278
+ only = body.get("chapters") # 批量选择发布:章号数组(可省=全部待发)
2279
+ kwargs = {"only": only if isinstance(only, list) else None}
2280
+ if force or force_confirmed or force_reason:
2281
+ kwargs.update(force=force, force_confirmed=force_confirmed,
2282
+ force_reason=force_reason)
2146
2283
  ok, err = pub_auto.publish_pending_async(
2147
2284
  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 {}))
2285
+ **kwargs)
2151
2286
  if not ok:
2152
2287
  payload = {"error": err or "操作失败"}
2153
2288
  if str(err or "").startswith("质量门禁拦截:"):
@@ -2988,8 +3123,16 @@ class Handler(BaseHTTPRequestHandler):
2988
3123
  '[{"q": "问题", "options": ["选项1", "选项2"]}]' % (ttype, goal[:200]))
2989
3124
  try:
2990
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 ".")
2991
3134
  res = builtin_agent.run(bi, prompt, os.getcwd() if hasattr(os, "getcwd") else ".",
2992
- timeout=8)
3135
+ timeout=8, sandbox=clarify_sandbox)
2993
3136
  import json as _json
2994
3137
  arr = None
2995
3138
  text = (res.get("text") or "").strip()
@@ -3024,10 +3167,13 @@ class Handler(BaseHTTPRequestHandler):
3024
3167
  POST /api/hooks/run,头 X-CodeBee-Token;体 {goal 必填, type, workdir,
3025
3168
  context, title}。令牌取设置 hooks_token(data/settings.json,UI/文件均可
3026
3169
  配置):已配置则必须精确匹配;未配置仅放行本机回环。任务链与 UI 完全相同。"""
3170
+ from core import webhooks
3027
3171
  try:
3028
- tok = str(settings.load().get("hooks_token") or "")
3172
+ cfg = settings.load()
3173
+ tok = str(cfg.get("hooks_token") or "")
3174
+ signing_secret = str(cfg.get("hooks_signing_secret") or "")
3029
3175
  except Exception:
3030
- tok = ""
3176
+ tok, signing_secret = "", ""
3031
3177
  given = (self.headers.get("X-CodeBee-Token") or "").strip()
3032
3178
  if tok:
3033
3179
  if given != tok:
@@ -3037,6 +3183,18 @@ class Handler(BaseHTTPRequestHandler):
3037
3183
  if host not in ("127.0.0.1", "::1", "::ffff:127.0.0.1"):
3038
3184
  return self._json(403, {
3039
3185
  "error": "未配置 hooks.token,仅允许本机触发;请先在设置中配置令牌"})
3186
+ # Optional HMAC envelope for GitHub/GitLab-style integrations. A
3187
+ # configured secret makes the signature mandatory; token auth remains
3188
+ # useful for old local scripts when no secret is configured.
3189
+ if signing_secret:
3190
+ timestamp = self.headers.get("X-CodeBee-Timestamp") or ""
3191
+ signature = (self.headers.get("X-CodeBee-Signature") or
3192
+ self.headers.get("X-Hub-Signature-256") or "")
3193
+ signed = (webhooks.verify(signing_secret, self._raw_request_body(),
3194
+ signature, timestamp) if timestamp else
3195
+ webhooks.verify_raw(signing_secret, self._raw_request_body(), signature))
3196
+ if not signed:
3197
+ return self._json(401, {"error": "Webhook 签名无效或已过期"})
3040
3198
  raw = self._body()
3041
3199
  payload = {}
3042
3200
  for k, cap in (("goal", 4000), ("context", 4000), ("type", 40),
@@ -3050,7 +3208,18 @@ class Handler(BaseHTTPRequestHandler):
3050
3208
  payload["type"] = "direct"
3051
3209
  if not payload.get("title"):
3052
3210
  payload["title"] = payload["goal"][:30]
3211
+ delivery_id = (self.headers.get("X-CodeBee-Delivery") or
3212
+ self.headers.get("X-Delivery-Id") or "").strip()
3213
+ if delivery_id:
3214
+ claimed, info = webhooks.claim_delivery(delivery_id)
3215
+ if not claimed:
3216
+ return self._json(200, {"ok": True, "deduplicated": True,
3217
+ "delivery_id": info.get("delivery_id")})
3053
3218
  status, resp = self._create_and_start(payload)
3219
+ if delivery_id and status != 200:
3220
+ webhooks.release_delivery(delivery_id)
3221
+ if delivery_id:
3222
+ resp = dict(resp, delivery_id=delivery_id)
3054
3223
  return self._json(status, resp)
3055
3224
 
3056
3225
  def _enqueue_run(self, run_id, task_id, job):