codebee 0.1.66 → 0.1.68

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 (116) hide show
  1. package/CHANGELOG.md +543 -520
  2. package/LICENSE +21 -21
  3. package/README.md +11 -7
  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/core/acceptance.py +100 -0
  12. package/app/core/aiflavor.py +197 -197
  13. package/app/core/artifacts.py +148 -148
  14. package/app/core/assets.py +110 -0
  15. package/app/core/attachments.py +623 -532
  16. package/app/core/automation.py +758 -758
  17. package/app/core/backup.py +470 -470
  18. package/app/core/beekeeper.py +183 -183
  19. package/app/core/board.py +209 -209
  20. package/app/core/bookmeta.py +930 -796
  21. package/app/core/bookmeta_catalog.py +180 -180
  22. package/app/core/branching.py +137 -137
  23. package/app/core/builtin_agent.py +1234 -1234
  24. package/app/core/capability.py +144 -144
  25. package/app/core/catalog.py +42 -0
  26. package/app/core/chaptersafe.py +32 -32
  27. package/app/core/cleanup.py +303 -303
  28. package/app/core/compaction.py +201 -201
  29. package/app/core/covergen.py +356 -356
  30. package/app/core/defectretro.py +43 -0
  31. package/app/core/diagnostics.py +114 -114
  32. package/app/core/dispatch.py +173 -173
  33. package/app/core/dispatch_log.py +128 -113
  34. package/app/core/env_scrub.py +85 -85
  35. package/app/core/error_codes.py +23 -23
  36. package/app/core/errorlog.py +137 -137
  37. package/app/core/evaluation.py +174 -174
  38. package/app/core/failure_notify.py +290 -290
  39. package/app/core/flows.py +48 -2
  40. package/app/core/goal_service.py +167 -167
  41. package/app/core/health.py +374 -374
  42. package/app/core/history.py +38 -38
  43. package/app/core/hooks.py +162 -162
  44. package/app/core/jobs.py +46 -26
  45. package/app/core/knowledge.py +57 -3
  46. package/app/core/manager.py +2122 -2115
  47. package/app/core/market.py +362 -362
  48. package/app/core/market_remote.py +124 -26
  49. package/app/core/mocks.py +64 -64
  50. package/app/core/modelhub.py +51 -3
  51. package/app/core/notify.py +106 -106
  52. package/app/core/operations.py +228 -0
  53. package/app/core/paihang.py +138 -109
  54. package/app/core/paths.py +2 -2
  55. package/app/core/pipeline.py +221 -12
  56. package/app/core/planner.py +6 -1
  57. package/app/core/portguard.py +111 -111
  58. package/app/core/preview.py +293 -293
  59. package/app/core/publish/__init__.py +7 -7
  60. package/app/core/publish/auto.py +367 -367
  61. package/app/core/publish/browser.py +539 -533
  62. package/app/core/publish/fanqie.py +159 -128
  63. package/app/core/publish/flow.py +440 -432
  64. package/app/core/publish/flows-fanqie-calibrated.json +332 -332
  65. package/app/core/publish/flows-qimao-calibrated.json +7 -3
  66. package/app/core/publish/ledger.py +203 -198
  67. package/app/core/publish/manager.py +692 -623
  68. package/app/core/publish/qimao.py +141 -141
  69. package/app/core/publish/ws.py +139 -139
  70. package/app/core/redact.py +38 -38
  71. package/app/core/repeat_guard.py +129 -129
  72. package/app/core/revisions.py +51 -0
  73. package/app/core/router.py +398 -398
  74. package/app/core/runner.py +2067 -2050
  75. package/app/core/selfupdate.py +384 -384
  76. package/app/core/session_log.py +161 -161
  77. package/app/core/sessions.py +312 -312
  78. package/app/core/settings.py +149 -149
  79. package/app/core/settings_schema.py +262 -262
  80. package/app/core/share_page.py +114 -114
  81. package/app/core/skill_scan.py +122 -114
  82. package/app/core/skillpacks/fanqie-novel.md +80 -80
  83. package/app/core/skillpacks/market/character-bible.md +66 -66
  84. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  85. package/app/core/skillpacks/market/git-workflow.md +57 -57
  86. package/app/core/skillpacks/market/release-notes.md +72 -72
  87. package/app/core/skillpacks/market/weekly-report.md +71 -71
  88. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  89. package/app/core/skills.py +109 -2
  90. package/app/core/step_runner.py +132 -132
  91. package/app/core/store.py +278 -41
  92. package/app/core/task_compile.py +199 -199
  93. package/app/core/telemetry.py +292 -292
  94. package/app/core/tlsctx.py +64 -64
  95. package/app/core/token_meter.py +147 -147
  96. package/app/core/tracing.py +144 -0
  97. package/app/core/upstream.py +41 -41
  98. package/app/core/usage.py +862 -858
  99. package/app/core/volumes.py +389 -355
  100. package/app/core/wxdigest.py +858 -858
  101. package/app/core/zentao.py +2465 -2447
  102. package/app/main.py +2981 -2838
  103. package/app/pick_dialog.py +110 -110
  104. package/app/ui/app.js +14399 -13994
  105. package/app/ui/artdiff.js +74 -0
  106. package/app/ui/board.html +162 -162
  107. package/app/ui/board.js +305 -305
  108. package/app/ui/i18n.js +2770 -2716
  109. package/app/ui/icons/bee.svg +79 -79
  110. package/app/ui/index.html +6 -0
  111. package/app/ui/manifest.json +16 -16
  112. package/app/ui/qrcode.js +2297 -2297
  113. package/app/ui/style.css +96 -24
  114. package/bin/tutti.js +147 -147
  115. package/package.json +46 -46
  116. package/tools/wx_reader.py +261 -261
@@ -1,106 +1,106 @@
1
- # -*- coding: utf-8 -*-
2
- """运行结果群推送(借鉴 agency-orchestrator 的 --notify):任务跑完把结果摘要
3
- 推到钉钉/飞书/企业微信群机器人,配合定时自动化就是「AI 团队每天定点交活」。
4
-
5
- webhook 一个地址全包——按域名自动适配三种机器人格式(同 agency-orchestrator
6
- 思路);也接受任意 https 地址(按钉钉 text 形状发,自建 n8n 等自选)。
7
- 推送在后台线程、失败只记日志——通知永远不影响任务本身。
8
- """
9
- from __future__ import annotations
10
-
11
- import logging
12
-
13
- log = logging.getLogger(__name__)
14
-
15
-
16
- def _payload_for(webhook, text):
17
- """按 webhook 域名适配群机器人消息体。返回 None = 无法识别的地址。"""
18
- from urllib.parse import urlparse
19
- host = (urlparse(webhook).hostname or "").lower()
20
- if "dingtalk" in host:
21
- return {"msgtype": "text", "text": {"content": text}}
22
- if "feishu" in host or "larksuite" in host:
23
- return {"msg_type": "text", "content": {"text": text}}
24
- if "weixin" in host or "wechat" in host or "work.weixin" in host:
25
- return {"msgtype": "text", "text": {"content": text}}
26
- if host:
27
- return {"msgtype": "text", "text": {"content": text}} # 未知域名按钉钉形状
28
- return None
29
-
30
-
31
- def _webhook():
32
- try:
33
- from . import settings
34
- return str(settings.load().get("notify_webhook") or "").strip()
35
- except Exception:
36
- return ""
37
-
38
-
39
- def _post(hook, text):
40
- """curl 子进程 POST(Python 不经手响应体);网络失败只记日志。"""
41
- from . import runner
42
- import json as _json
43
- body = _json.dumps(_payload_for(hook, text), ensure_ascii=False)
44
- r = runner.run_process(
45
- argv=["curl", "-sS", "--max-time", "20",
46
- "-H", "Content-Type: application/json",
47
- "-d", body, hook],
48
- timeout=30)
49
- return bool(r["ok"])
50
-
51
-
52
- def push_text(text):
53
- """推一条文本到群。配置了 webhook 才推;失败返回 False 不抛错。"""
54
- hook = _webhook()
55
- if not hook:
56
- return False
57
- if not hook.startswith("https://"):
58
- log.warning("notify webhook 必须是 https")
59
- return False
60
- try:
61
- return _post(hook, text)
62
- except Exception as e:
63
- log.warning("notify push failed: %s", e)
64
- return False
65
-
66
-
67
- def push_run_async(run_id):
68
- """任务收尾后异步推送结果摘要(jobs 层调用;绝不阻塞/影响任务)。"""
69
- threading.Thread(target=push_run, daemon=True,
70
- name="notify-%s" % run_id, args=(run_id,)).start()
71
-
72
-
73
- def push_run(run_id):
74
- """组装 run 结果摘要并推送(webhook 未配置时静默跳过)。
75
-
76
- 摘要末尾附分享页路径(notify_base_url 设置非空时拼完整链接,否则只给
77
- run_id 供在本机 CodeBee 界面查找)。"""
78
- hook = _webhook()
79
- if not hook:
80
- return False
81
- from . import store
82
- run = store.get_run(run_id) or {}
83
- if not run:
84
- return False
85
- status = str(run.get("status") or "")
86
- mark = {"done": "✅", "failed": "❌", "cancelled": "⚪"}.get(status, "🔔")
87
- lines = ["%s CodeBee 任务%s" % (mark, {"done": "完成", "failed": "失败",
88
- "cancelled": "已取消"}.get(status, status))]
89
- lines.append("任务:%s" % (run.get("title") or run_id))
90
- if run.get("error"):
91
- lines.append("错误:%s" % str(run["error"])[:200])
92
- v = run.get("verdict") or {}
93
- if v.get("overall") is not None:
94
- lines.append("综合评分 %.1f(%s)" % (
95
- float(v["overall"]), "达标" if v.get("publishable") else "未达标"))
96
- try:
97
- from .settings import load as _sload
98
- base = str(_sload().get("notify_base_url") or "").strip().rstrip("/")
99
- if base:
100
- lines.append("📄 详情:%s/api/runs/%s/share" % (base, run_id))
101
- except Exception:
102
- pass
103
- return push_text("\n".join(lines))
104
-
105
-
106
- import threading # noqa: E402 (push_run_async 依赖;置底避免顶部循环导入)
1
+ # -*- coding: utf-8 -*-
2
+ """运行结果群推送(借鉴 agency-orchestrator 的 --notify):任务跑完把结果摘要
3
+ 推到钉钉/飞书/企业微信群机器人,配合定时自动化就是「AI 团队每天定点交活」。
4
+
5
+ webhook 一个地址全包——按域名自动适配三种机器人格式(同 agency-orchestrator
6
+ 思路);也接受任意 https 地址(按钉钉 text 形状发,自建 n8n 等自选)。
7
+ 推送在后台线程、失败只记日志——通知永远不影响任务本身。
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+
13
+ log = logging.getLogger(__name__)
14
+
15
+
16
+ def _payload_for(webhook, text):
17
+ """按 webhook 域名适配群机器人消息体。返回 None = 无法识别的地址。"""
18
+ from urllib.parse import urlparse
19
+ host = (urlparse(webhook).hostname or "").lower()
20
+ if "dingtalk" in host:
21
+ return {"msgtype": "text", "text": {"content": text}}
22
+ if "feishu" in host or "larksuite" in host:
23
+ return {"msg_type": "text", "content": {"text": text}}
24
+ if "weixin" in host or "wechat" in host or "work.weixin" in host:
25
+ return {"msgtype": "text", "text": {"content": text}}
26
+ if host:
27
+ return {"msgtype": "text", "text": {"content": text}} # 未知域名按钉钉形状
28
+ return None
29
+
30
+
31
+ def _webhook():
32
+ try:
33
+ from . import settings
34
+ return str(settings.load().get("notify_webhook") or "").strip()
35
+ except Exception:
36
+ return ""
37
+
38
+
39
+ def _post(hook, text):
40
+ """curl 子进程 POST(Python 不经手响应体);网络失败只记日志。"""
41
+ from . import runner
42
+ import json as _json
43
+ body = _json.dumps(_payload_for(hook, text), ensure_ascii=False)
44
+ r = runner.run_process(
45
+ argv=["curl", "-sS", "--max-time", "20",
46
+ "-H", "Content-Type: application/json",
47
+ "-d", body, hook],
48
+ timeout=30)
49
+ return bool(r["ok"])
50
+
51
+
52
+ def push_text(text):
53
+ """推一条文本到群。配置了 webhook 才推;失败返回 False 不抛错。"""
54
+ hook = _webhook()
55
+ if not hook:
56
+ return False
57
+ if not hook.startswith("https://"):
58
+ log.warning("notify webhook 必须是 https")
59
+ return False
60
+ try:
61
+ return _post(hook, text)
62
+ except Exception as e:
63
+ log.warning("notify push failed: %s", e)
64
+ return False
65
+
66
+
67
+ def push_run_async(run_id):
68
+ """任务收尾后异步推送结果摘要(jobs 层调用;绝不阻塞/影响任务)。"""
69
+ threading.Thread(target=push_run, daemon=True,
70
+ name="notify-%s" % run_id, args=(run_id,)).start()
71
+
72
+
73
+ def push_run(run_id):
74
+ """组装 run 结果摘要并推送(webhook 未配置时静默跳过)。
75
+
76
+ 摘要末尾附分享页路径(notify_base_url 设置非空时拼完整链接,否则只给
77
+ run_id 供在本机 CodeBee 界面查找)。"""
78
+ hook = _webhook()
79
+ if not hook:
80
+ return False
81
+ from . import store
82
+ run = store.get_run(run_id) or {}
83
+ if not run:
84
+ return False
85
+ status = str(run.get("status") or "")
86
+ mark = {"done": "✅", "failed": "❌", "cancelled": "⚪"}.get(status, "🔔")
87
+ lines = ["%s CodeBee 任务%s" % (mark, {"done": "完成", "failed": "失败",
88
+ "cancelled": "已取消"}.get(status, status))]
89
+ lines.append("任务:%s" % (run.get("title") or run_id))
90
+ if run.get("error"):
91
+ lines.append("错误:%s" % str(run["error"])[:200])
92
+ v = run.get("verdict") or {}
93
+ if v.get("overall") is not None:
94
+ lines.append("综合评分 %.1f(%s)" % (
95
+ float(v["overall"]), "达标" if v.get("publishable") else "未达标"))
96
+ try:
97
+ from .settings import load as _sload
98
+ base = str(_sload().get("notify_base_url") or "").strip().rstrip("/")
99
+ if base:
100
+ lines.append("📄 详情:%s/api/runs/%s/share" % (base, run_id))
101
+ except Exception:
102
+ pass
103
+ return push_text("\n".join(lines))
104
+
105
+
106
+ import threading # noqa: E402 (push_run_async 依赖;置底避免顶部循环导入)
@@ -0,0 +1,228 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Append-only external operation ledger.
3
+
4
+ External writes cannot promise exactly-once delivery. Each write therefore
5
+ has a stable operation id and an explicit state: pending, confirmed, failed or
6
+ unknown. Unknown means the caller must reconcile with the remote system before
7
+ retrying; it is never treated as success.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ import secrets
14
+ import threading
15
+ import time
16
+ import urllib.error
17
+ from pathlib import Path
18
+
19
+ from . import paths
20
+
21
+ LOCK = threading.RLock()
22
+ STATUSES = ("pending", "confirmed", "failed", "unknown")
23
+ PENDING_TTL_SECONDS = 15 * 60
24
+
25
+
26
+ def _file(day=None) -> Path:
27
+ day = day or time.strftime("%Y-%m-%d")
28
+ return paths.DATA_DIR / "operations" / ("operations-%s.jsonl" % day[:7].replace("-", ""))
29
+
30
+
31
+ def _read_all():
32
+ rows = []
33
+ root = paths.DATA_DIR / "operations"
34
+ try:
35
+ files = sorted(root.glob("operations-*.jsonl"))
36
+ except OSError:
37
+ files = []
38
+ for fp in files:
39
+ try:
40
+ for line in fp.read_text(encoding="utf-8").splitlines():
41
+ try:
42
+ row = json.loads(line)
43
+ except (TypeError, ValueError):
44
+ continue
45
+ if isinstance(row, dict) and row.get("operation_id"):
46
+ rows.append(row)
47
+ except OSError:
48
+ continue
49
+ return rows
50
+
51
+
52
+ def _write_all(rows):
53
+ """Test/support migration helper; production writes use append-only _append."""
54
+ root = paths.DATA_DIR / "operations"
55
+ root.mkdir(parents=True, exist_ok=True)
56
+ fp = _file()
57
+ fp.write_text("".join(json.dumps(r, ensure_ascii=False) + "\n" for r in rows),
58
+ encoding="utf-8")
59
+
60
+
61
+ def _append(row):
62
+ try:
63
+ fp = _file(row.get("submitted_at") and
64
+ time.strftime("%Y-%m-%d", time.localtime(float(row["submitted_at"]))))
65
+ fp.parent.mkdir(parents=True, exist_ok=True)
66
+ with fp.open("a", encoding="utf-8") as handle:
67
+ handle.write(json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n")
68
+ handle.flush()
69
+ except Exception:
70
+ # Observability must not turn a remote write into a local failure.
71
+ pass
72
+
73
+
74
+ def _request_hash(request) -> str:
75
+ raw = json.dumps(request or {}, ensure_ascii=False, sort_keys=True,
76
+ separators=(",", ":"), default=str)
77
+ return hashlib.sha256(raw.encode("utf-8")).hexdigest()
78
+
79
+
80
+ def _operation_id(target: str) -> str:
81
+ stamp = time.strftime("%Y%m%d-%H%M%S")
82
+ return "op-%s-%s-%s" % (stamp, secrets.token_hex(4),
83
+ hashlib.sha256(str(target).encode()).hexdigest()[:6])
84
+
85
+
86
+ def _safe_meta(metadata):
87
+ if not isinstance(metadata, dict):
88
+ return {}
89
+ out = {}
90
+ for key, value in metadata.items():
91
+ name = str(key)[:40]
92
+ if any(token in name.lower() for token in ("key", "token", "password", "secret")):
93
+ continue
94
+ if isinstance(value, (str, int, float, bool)) or value is None:
95
+ out[name] = str(value)[:300] if isinstance(value, str) else value
96
+ return out
97
+
98
+
99
+ def begin(target, request=None, *, task_id="", run_id="", metadata=None):
100
+ operation_id = _operation_id(target)
101
+ now = time.time()
102
+ row = {
103
+ "operation_id": operation_id,
104
+ "target": str(target or "")[:160],
105
+ "request_hash": _request_hash(request),
106
+ "submitted_at": now,
107
+ "status": "pending",
108
+ "remote_receipt": "",
109
+ "error": "",
110
+ "last_reconciled_at": None,
111
+ "task_id": str(task_id or "")[:80],
112
+ "run_id": str(run_id or "")[:80],
113
+ "metadata": _safe_meta(metadata),
114
+ }
115
+ with LOCK:
116
+ _append(row)
117
+ return operation_id
118
+
119
+
120
+ def update(operation_id, status, *, remote_receipt="", error="",
121
+ metadata=None, reconciled=False):
122
+ operation_id = str(operation_id or "").strip()
123
+ status = str(status or "").strip().lower()
124
+ if not operation_id or status not in STATUSES:
125
+ return False
126
+ with LOCK:
127
+ # Read and transition-check under the same lock as append. Two
128
+ # concurrent callbacks must not race a pending operation into
129
+ # conflicting terminal states.
130
+ current = get(operation_id)
131
+ if current is None:
132
+ return False
133
+ current_status = str(current.get("status") or "pending")
134
+ # External effects are monotonic: a confirmed write must never be
135
+ # downgraded by cleanup/error handling. Unknown/failed can only be
136
+ # promoted by an explicit remote reconciliation.
137
+ if current_status == "confirmed" and status != "confirmed":
138
+ return False
139
+ if current_status in ("unknown", "failed") and status == "confirmed" and not reconciled:
140
+ return False
141
+ if current_status in ("unknown", "failed") and status != current_status and not reconciled:
142
+ return False
143
+ if status == "confirmed" and not remote_receipt:
144
+ remote_receipt = current.get("remote_receipt") or ""
145
+ now = time.time()
146
+ row = {
147
+ "operation_id": operation_id,
148
+ "status": status,
149
+ "remote_receipt": str(remote_receipt or "")[:300],
150
+ "error": str(error or "")[:500],
151
+ "last_reconciled_at": now if reconciled else None,
152
+ "updated_at": now,
153
+ }
154
+ if metadata:
155
+ row["metadata"] = _safe_meta(metadata)
156
+ _append(row)
157
+ return True
158
+
159
+
160
+ def confirm(operation_id, remote_receipt="", *, metadata=None):
161
+ return update(operation_id, "confirmed", remote_receipt=remote_receipt,
162
+ metadata=metadata)
163
+
164
+
165
+ def fail(operation_id, error="", *, metadata=None):
166
+ return update(operation_id, "failed", error=error, metadata=metadata)
167
+
168
+
169
+ def mark_unknown(operation_id, error="", *, metadata=None):
170
+ return update(operation_id, "unknown", error=error, metadata=metadata)
171
+
172
+
173
+ def reconcile(operation_id, status, remote_receipt="", error="", metadata=None):
174
+ """Record a remote lookup result for an unknown operation."""
175
+ return update(operation_id, status, remote_receipt=remote_receipt, error=error,
176
+ metadata=metadata, reconciled=True)
177
+
178
+
179
+ def get(operation_id):
180
+ operation_id = str(operation_id or "")
181
+ latest = None
182
+ with LOCK:
183
+ for row in _read_all():
184
+ if row.get("operation_id") == operation_id:
185
+ latest = dict(latest or {}, **row)
186
+ return latest
187
+
188
+
189
+ def recent(*, target="", status="", limit=100):
190
+ latest = {}
191
+ with LOCK:
192
+ for row in _read_all():
193
+ if target and str(row.get("target") or "") != str(target):
194
+ continue
195
+ latest[row["operation_id"]] = dict(latest.get(row["operation_id"]) or {}, **row)
196
+ rows = list(latest.values())
197
+ if status:
198
+ rows = [r for r in rows if r.get("status") == status]
199
+ rows.sort(key=lambda r: float(r.get("updated_at") or r.get("submitted_at") or 0), reverse=True)
200
+ return rows[:max(1, min(1000, int(limit or 100)))]
201
+
202
+
203
+ def recover_pending(*, now=None, ttl_seconds=PENDING_TTL_SECONDS):
204
+ """Turn stale pending writes into unknown so restart never replays blindly."""
205
+ now = time.time() if now is None else float(now)
206
+ changed = 0
207
+ for row in recent(status="pending", limit=1000):
208
+ submitted = float(row.get("submitted_at") or 0)
209
+ if submitted and now - submitted > max(1, float(ttl_seconds)):
210
+ if mark_unknown(row["operation_id"], "进程重启后未收到远端回执,请先对账"):
211
+ changed += 1
212
+ return changed
213
+
214
+
215
+ def status_for_exception(error) -> str:
216
+ text = str(error or "").lower()
217
+ if isinstance(error, (TimeoutError, ConnectionError, urllib.error.URLError)) or any(x in text for x in
218
+ ("timeout", "timed out", "超时", "connection", "connect", "connection reset",
219
+ "断开", "断连", "连不上", "网络", "network", "unreachable", "disconnect")):
220
+ return "unknown"
221
+ return "failed"
222
+
223
+
224
+ def finish_exception(operation_id, error):
225
+ status = status_for_exception(error)
226
+ if status == "unknown":
227
+ return mark_unknown(operation_id, str(error))
228
+ return fail(operation_id, str(error))