codebee 0.1.68 → 0.1.69

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 (106) hide show
  1. package/CHANGELOG.md +543 -543
  2. package/LICENSE +21 -21
  3. package/README.md +10 -0
  4. package/app/core/acceptance.py +100 -100
  5. package/app/core/aiflavor.py +197 -197
  6. package/app/core/artifacts.py +148 -148
  7. package/app/core/assets.py +110 -110
  8. package/app/core/attachments.py +623 -623
  9. package/app/core/automation.py +758 -758
  10. package/app/core/backup.py +470 -470
  11. package/app/core/beekeeper.py +183 -183
  12. package/app/core/board.py +209 -209
  13. package/app/core/bookmeta.py +930 -930
  14. package/app/core/bookmeta_catalog.py +180 -180
  15. package/app/core/branching.py +137 -137
  16. package/app/core/builtin_agent.py +1234 -1234
  17. package/app/core/capability.py +144 -144
  18. package/app/core/chaptersafe.py +32 -32
  19. package/app/core/cleanup.py +303 -303
  20. package/app/core/compaction.py +201 -201
  21. package/app/core/covergen.py +356 -356
  22. package/app/core/defectretro.py +43 -43
  23. package/app/core/diagnostics.py +114 -114
  24. package/app/core/dispatch.py +173 -173
  25. package/app/core/dispatch_log.py +128 -128
  26. package/app/core/env_scrub.py +85 -85
  27. package/app/core/error_codes.py +23 -23
  28. package/app/core/errorlog.py +137 -137
  29. package/app/core/evaluation.py +174 -174
  30. package/app/core/failure_notify.py +290 -290
  31. package/app/core/goal_service.py +167 -167
  32. package/app/core/health.py +374 -374
  33. package/app/core/history.py +38 -38
  34. package/app/core/hooks.py +162 -162
  35. package/app/core/jobs.py +29 -29
  36. package/app/core/manager.py +2122 -2122
  37. package/app/core/market.py +362 -362
  38. package/app/core/mocks.py +64 -64
  39. package/app/core/modelhub.py +8 -8
  40. package/app/core/notify.py +106 -106
  41. package/app/core/operations.py +228 -228
  42. package/app/core/paihang.py +138 -138
  43. package/app/core/paths.py +2 -2
  44. package/app/core/portguard.py +111 -111
  45. package/app/core/preview.py +293 -293
  46. package/app/core/publish/__init__.py +7 -7
  47. package/app/core/publish/auto.py +367 -367
  48. package/app/core/publish/browser.py +539 -539
  49. package/app/core/publish/fanqie.py +159 -159
  50. package/app/core/publish/flow.py +440 -440
  51. package/app/core/publish/flows-fanqie-calibrated.json +332 -332
  52. package/app/core/publish/ledger.py +203 -203
  53. package/app/core/publish/manager.py +692 -692
  54. package/app/core/publish/qimao.py +141 -141
  55. package/app/core/publish/ws.py +139 -139
  56. package/app/core/redact.py +38 -38
  57. package/app/core/repeat_guard.py +129 -129
  58. package/app/core/router.py +398 -398
  59. package/app/core/runner.py +2067 -2067
  60. package/app/core/selfupdate.py +384 -384
  61. package/app/core/session_log.py +161 -161
  62. package/app/core/sessions.py +312 -312
  63. package/app/core/settings.py +149 -149
  64. package/app/core/settings_schema.py +262 -262
  65. package/app/core/share_page.py +114 -114
  66. package/app/core/skill_scan.py +122 -122
  67. package/app/core/skillpacks/fanqie-novel.md +80 -80
  68. package/app/core/skillpacks/market/character-bible.md +66 -66
  69. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  70. package/app/core/skillpacks/market/git-workflow.md +57 -57
  71. package/app/core/skillpacks/market/release-notes.md +72 -72
  72. package/app/core/skillpacks/market/weekly-report.md +71 -71
  73. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  74. package/app/core/step_runner.py +132 -132
  75. package/app/core/store.py +75 -75
  76. package/app/core/task_compile.py +199 -199
  77. package/app/core/telemetry.py +292 -292
  78. package/app/core/tlsctx.py +64 -64
  79. package/app/core/token_meter.py +147 -147
  80. package/app/core/tracing.py +144 -144
  81. package/app/core/upstream.py +41 -41
  82. package/app/core/usage.py +862 -862
  83. package/app/core/volumes.py +389 -389
  84. package/app/core/wxdigest.py +858 -858
  85. package/app/core/zentao.py +2465 -2465
  86. package/app/main.py +2981 -2981
  87. package/app/pick_dialog.py +110 -110
  88. package/app/ui/app.js +14399 -14399
  89. package/app/ui/artdiff.js +74 -74
  90. package/app/ui/board.html +162 -162
  91. package/app/ui/board.js +305 -305
  92. package/app/ui/i18n.js +2770 -2770
  93. package/app/ui/icons/bee.svg +79 -79
  94. package/app/ui/manifest.json +16 -16
  95. package/app/ui/qrcode.js +2297 -2297
  96. package/app/ui/style.css +16 -16
  97. package/bin/tutti.js +147 -147
  98. package/package.json +46 -46
  99. package/tools/wx_reader.py +261 -261
  100. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  101. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  102. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  103. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  104. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  105. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  106. package/app/.playwright-cli/run-detail-after.png +0 -0
@@ -1,203 +1,203 @@
1
- # -*- coding: utf-8 -*-
2
- """发布台账:每次发布动作(连接/建书/传章节)追加一条 JSONL,append-only。
3
-
4
- 落盘 data/publish/publish-YYYYMM.jsonl(按月分文件,与用量台账同范式)。
5
- 幂等键 (task_id, platform, chapter_no):章节发布成功后重发即跳过——
6
- 断点续发(发布到一半挂了,重启后接着发没发完的)靠这一条。
7
-
8
- 作品登记 books.json:task ↔ 平台作品的绑定(建书成功后写入 book_id/标题),
9
- 原子写(tmp + os.replace)。与台账分工:台账只追加不改(审计/历史),
10
- 可变状态(当前绑定的作品)进 books.json。
11
-
12
- 截图存证:data/publish/shots/<platform>/<task_id>/<时间戳>-<步骤名>.png,
13
- 每步动作后落一张,出错可回看卡在哪一步;旧截图按任务清理不自动做(量小)。
14
- """
15
- from __future__ import annotations
16
-
17
- import json
18
- import re
19
- import threading
20
- import time
21
-
22
- from .. import paths
23
-
24
- LOCK = threading.RLock()
25
-
26
- FIELDS = ("ts", "day", "platform", "action", "task_id", "chapter_no",
27
- "book_id", "title", "ok", "error", "shot", "operation_id",
28
- "operation_status", "remote_receipt")
29
-
30
-
31
- def _month_file(day):
32
- return paths.PUBLISH_DIR / ("publish-%s.jsonl" % day[:7].replace("-", ""))
33
-
34
-
35
- def record(platform, action, task_id="", chapter_no=0, book_id="",
36
- title="", ok=True, error="", shot="", operation_id="",
37
- operation_status="", remote_receipt=""):
38
- """追加一条发布记录。异常全吞——记账失败绝不能影响发布主流程。"""
39
- try:
40
- rec = {
41
- "ts": time.strftime("%Y-%m-%d %H:%M:%S"),
42
- "day": time.strftime("%Y-%m-%d"),
43
- "platform": str(platform)[:16],
44
- "action": str(action)[:24],
45
- "task_id": str(task_id)[:64],
46
- "chapter_no": int(chapter_no or 0),
47
- "book_id": str(book_id)[:80],
48
- "title": str(title)[:120],
49
- "ok": bool(ok),
50
- "error": str(error or "")[:300],
51
- "shot": str(shot)[:200],
52
- "operation_id": str(operation_id or "")[:80],
53
- "operation_status": str(operation_status or "")[:20],
54
- "remote_receipt": str(remote_receipt or "")[:300],
55
- }
56
- with LOCK:
57
- paths.PUBLISH_DIR.mkdir(parents=True, exist_ok=True)
58
- with open(_month_file(rec["day"]), "a", encoding="utf-8") as f:
59
- f.write(json.dumps(rec, ensure_ascii=False) + "\n")
60
- except Exception:
61
- pass
62
-
63
-
64
- def _iter_records(days=90):
65
- """近 N 天的记录(月份文件粒度粗滤,行内再按 day 过滤)。"""
66
- import datetime
67
- cutoff = time.strftime("%Y-%m-%d", time.localtime(time.time() - days * 86400))
68
- for fp in sorted(paths.PUBLISH_DIR.glob("publish-*.jsonl")):
69
- try:
70
- lines = fp.read_text(encoding="utf-8").splitlines()
71
- except OSError:
72
- continue
73
- for ln in lines:
74
- try:
75
- r = json.loads(ln)
76
- except ValueError:
77
- continue
78
- if str(r.get("day") or "") >= cutoff:
79
- yield r
80
-
81
-
82
- def published_chapters(task_id, platform):
83
- """该任务在该平台已成功发布的章节号集合(幂等跳过依据)。"""
84
- out = set()
85
- for r in _iter_records(3650):
86
- if (r.get("action") == "upload_chapter" and r.get("ok")
87
- and r.get("task_id") == task_id and r.get("platform") == platform):
88
- n = r.get("chapter_no") or 0
89
- if n > 0:
90
- out.add(int(n))
91
- return out
92
-
93
-
94
- def recent(task_id=None, platform=None, limit=50):
95
- """最近记录(新在前),详情页发布历史用。"""
96
- out = []
97
- for r in _iter_records(90):
98
- if task_id and r.get("task_id") != task_id:
99
- continue
100
- if platform and r.get("platform") != platform:
101
- continue
102
- out.append(r)
103
- out.reverse()
104
- return out[:limit]
105
-
106
-
107
- # ---------------------------------------------------------------- 作品登记
108
- _BOOKS_FILE = paths.PUBLISH_DIR / "books.json"
109
-
110
-
111
- def load_books():
112
- try:
113
- d = json.loads(_BOOKS_FILE.read_text(encoding="utf-8"))
114
- return d if isinstance(d, dict) else {}
115
- except Exception:
116
- return {}
117
-
118
-
119
- def save_book(task_id, platform, info):
120
- """登记/更新任务在某平台的作品绑定。info: {book_id, title, url?}。"""
121
- with LOCK:
122
- paths.PUBLISH_DIR.mkdir(parents=True, exist_ok=True)
123
- books = load_books()
124
- books.setdefault(str(task_id), {})[str(platform)] = {
125
- "book_id": str(info.get("book_id") or ""),
126
- "title": str(info.get("title") or "")[:120],
127
- "url": str(info.get("url") or "")[:300],
128
- "created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
129
- }
130
- tmp = _BOOKS_FILE.with_suffix(".tmp")
131
- tmp.write_text(json.dumps(books, ensure_ascii=False, indent=1),
132
- encoding="utf-8")
133
- tmp.replace(_BOOKS_FILE)
134
-
135
-
136
- def book_for(task_id, platform):
137
- return ((load_books().get(str(task_id)) or {}).get(str(platform))) or None
138
-
139
-
140
- def shot_path(platform, task_id, step):
141
- """截图存证路径(只算路径不落盘,由 browser.Page.screenshot 写)。"""
142
- ts = time.strftime("%Y%m%d-%H%M%S")
143
- return paths.PUBLISH_DIR / "shots" / str(platform) / str(task_id) / \
144
- ("%s-%s.png" % (ts, step))
145
-
146
-
147
- _CHAPTER_RE = re.compile(r"第\s*([0-90-9一二三四五六七八九十百千零两]+)\s*[章节回]")
148
-
149
-
150
- def parse_chapter_no(name):
151
- """从章节标题/文件名解析章号(第12章/第十二章),失败返回 0。"""
152
- m = _CHAPTER_RE.search(str(name or ""))
153
- if not m:
154
- return 0
155
- s = m.group(1)
156
- if s.isdigit():
157
- return int(s)
158
- cn = {"零": 0, "一": 1, "二": 2, "两": 2, "三": 3, "四": 4, "五": 5,
159
- "六": 6, "七": 7, "八": 8, "九": 9}
160
- # 正序解析(十二=12、二十三=23、一百零五=105):数字暂存 num,
161
- # 遇单位(十/百/千)把它乘上去并清零;万字内够用(章号没有更大的)
162
- section, num = 0, 0
163
- for ch in s:
164
- if ch in cn:
165
- num = cn[ch]
166
- elif ch == "十":
167
- section += (num or 1) * 10
168
- num = 0
169
- elif ch == "百":
170
- section += (num or 1) * 100
171
- num = 0
172
- elif ch == "千":
173
- section += (num or 1) * 1000
174
- num = 0
175
- return section + num
176
-
177
-
178
- # ---------------------------------------------------------------- 护栏统计
179
- def today_count(task_id, platform):
180
- """该任务在该平台今日已成功发布的章数(每日上限护栏的计数口径)。"""
181
- day = time.strftime("%Y-%m-%d")
182
- return sum(1 for r in _iter_records(2)
183
- if r.get("action") == "upload_chapter" and r.get("ok")
184
- and r.get("task_id") == task_id and r.get("platform") == platform
185
- and r.get("day") == day)
186
-
187
-
188
- def consecutive_failures(platform):
189
- """该平台最近连续失败的发布动作数(连败退避护栏,跨任务口径)。
190
-
191
- 记录时间序旧→新,倒着数到第一条成功为止;连败大概率是风控或改版,
192
- 此时继续自动重试只会火上浇油,应转人工检查。
193
- """
194
- n = 0
195
- for r in reversed(list(_iter_records(7))):
196
- if r.get("platform") != platform:
197
- continue
198
- if r.get("action") not in ("upload_chapter", "create_book"):
199
- continue
200
- if r.get("ok"):
201
- break
202
- n += 1
203
- return n
1
+ # -*- coding: utf-8 -*-
2
+ """发布台账:每次发布动作(连接/建书/传章节)追加一条 JSONL,append-only。
3
+
4
+ 落盘 data/publish/publish-YYYYMM.jsonl(按月分文件,与用量台账同范式)。
5
+ 幂等键 (task_id, platform, chapter_no):章节发布成功后重发即跳过——
6
+ 断点续发(发布到一半挂了,重启后接着发没发完的)靠这一条。
7
+
8
+ 作品登记 books.json:task ↔ 平台作品的绑定(建书成功后写入 book_id/标题),
9
+ 原子写(tmp + os.replace)。与台账分工:台账只追加不改(审计/历史),
10
+ 可变状态(当前绑定的作品)进 books.json。
11
+
12
+ 截图存证:data/publish/shots/<platform>/<task_id>/<时间戳>-<步骤名>.png,
13
+ 每步动作后落一张,出错可回看卡在哪一步;旧截图按任务清理不自动做(量小)。
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import re
19
+ import threading
20
+ import time
21
+
22
+ from .. import paths
23
+
24
+ LOCK = threading.RLock()
25
+
26
+ FIELDS = ("ts", "day", "platform", "action", "task_id", "chapter_no",
27
+ "book_id", "title", "ok", "error", "shot", "operation_id",
28
+ "operation_status", "remote_receipt")
29
+
30
+
31
+ def _month_file(day):
32
+ return paths.PUBLISH_DIR / ("publish-%s.jsonl" % day[:7].replace("-", ""))
33
+
34
+
35
+ def record(platform, action, task_id="", chapter_no=0, book_id="",
36
+ title="", ok=True, error="", shot="", operation_id="",
37
+ operation_status="", remote_receipt=""):
38
+ """追加一条发布记录。异常全吞——记账失败绝不能影响发布主流程。"""
39
+ try:
40
+ rec = {
41
+ "ts": time.strftime("%Y-%m-%d %H:%M:%S"),
42
+ "day": time.strftime("%Y-%m-%d"),
43
+ "platform": str(platform)[:16],
44
+ "action": str(action)[:24],
45
+ "task_id": str(task_id)[:64],
46
+ "chapter_no": int(chapter_no or 0),
47
+ "book_id": str(book_id)[:80],
48
+ "title": str(title)[:120],
49
+ "ok": bool(ok),
50
+ "error": str(error or "")[:300],
51
+ "shot": str(shot)[:200],
52
+ "operation_id": str(operation_id or "")[:80],
53
+ "operation_status": str(operation_status or "")[:20],
54
+ "remote_receipt": str(remote_receipt or "")[:300],
55
+ }
56
+ with LOCK:
57
+ paths.PUBLISH_DIR.mkdir(parents=True, exist_ok=True)
58
+ with open(_month_file(rec["day"]), "a", encoding="utf-8") as f:
59
+ f.write(json.dumps(rec, ensure_ascii=False) + "\n")
60
+ except Exception:
61
+ pass
62
+
63
+
64
+ def _iter_records(days=90):
65
+ """近 N 天的记录(月份文件粒度粗滤,行内再按 day 过滤)。"""
66
+ import datetime
67
+ cutoff = time.strftime("%Y-%m-%d", time.localtime(time.time() - days * 86400))
68
+ for fp in sorted(paths.PUBLISH_DIR.glob("publish-*.jsonl")):
69
+ try:
70
+ lines = fp.read_text(encoding="utf-8").splitlines()
71
+ except OSError:
72
+ continue
73
+ for ln in lines:
74
+ try:
75
+ r = json.loads(ln)
76
+ except ValueError:
77
+ continue
78
+ if str(r.get("day") or "") >= cutoff:
79
+ yield r
80
+
81
+
82
+ def published_chapters(task_id, platform):
83
+ """该任务在该平台已成功发布的章节号集合(幂等跳过依据)。"""
84
+ out = set()
85
+ for r in _iter_records(3650):
86
+ if (r.get("action") == "upload_chapter" and r.get("ok")
87
+ and r.get("task_id") == task_id and r.get("platform") == platform):
88
+ n = r.get("chapter_no") or 0
89
+ if n > 0:
90
+ out.add(int(n))
91
+ return out
92
+
93
+
94
+ def recent(task_id=None, platform=None, limit=50):
95
+ """最近记录(新在前),详情页发布历史用。"""
96
+ out = []
97
+ for r in _iter_records(90):
98
+ if task_id and r.get("task_id") != task_id:
99
+ continue
100
+ if platform and r.get("platform") != platform:
101
+ continue
102
+ out.append(r)
103
+ out.reverse()
104
+ return out[:limit]
105
+
106
+
107
+ # ---------------------------------------------------------------- 作品登记
108
+ _BOOKS_FILE = paths.PUBLISH_DIR / "books.json"
109
+
110
+
111
+ def load_books():
112
+ try:
113
+ d = json.loads(_BOOKS_FILE.read_text(encoding="utf-8"))
114
+ return d if isinstance(d, dict) else {}
115
+ except Exception:
116
+ return {}
117
+
118
+
119
+ def save_book(task_id, platform, info):
120
+ """登记/更新任务在某平台的作品绑定。info: {book_id, title, url?}。"""
121
+ with LOCK:
122
+ paths.PUBLISH_DIR.mkdir(parents=True, exist_ok=True)
123
+ books = load_books()
124
+ books.setdefault(str(task_id), {})[str(platform)] = {
125
+ "book_id": str(info.get("book_id") or ""),
126
+ "title": str(info.get("title") or "")[:120],
127
+ "url": str(info.get("url") or "")[:300],
128
+ "created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
129
+ }
130
+ tmp = _BOOKS_FILE.with_suffix(".tmp")
131
+ tmp.write_text(json.dumps(books, ensure_ascii=False, indent=1),
132
+ encoding="utf-8")
133
+ tmp.replace(_BOOKS_FILE)
134
+
135
+
136
+ def book_for(task_id, platform):
137
+ return ((load_books().get(str(task_id)) or {}).get(str(platform))) or None
138
+
139
+
140
+ def shot_path(platform, task_id, step):
141
+ """截图存证路径(只算路径不落盘,由 browser.Page.screenshot 写)。"""
142
+ ts = time.strftime("%Y%m%d-%H%M%S")
143
+ return paths.PUBLISH_DIR / "shots" / str(platform) / str(task_id) / \
144
+ ("%s-%s.png" % (ts, step))
145
+
146
+
147
+ _CHAPTER_RE = re.compile(r"第\s*([0-90-9一二三四五六七八九十百千零两]+)\s*[章节回]")
148
+
149
+
150
+ def parse_chapter_no(name):
151
+ """从章节标题/文件名解析章号(第12章/第十二章),失败返回 0。"""
152
+ m = _CHAPTER_RE.search(str(name or ""))
153
+ if not m:
154
+ return 0
155
+ s = m.group(1)
156
+ if s.isdigit():
157
+ return int(s)
158
+ cn = {"零": 0, "一": 1, "二": 2, "两": 2, "三": 3, "四": 4, "五": 5,
159
+ "六": 6, "七": 7, "八": 8, "九": 9}
160
+ # 正序解析(十二=12、二十三=23、一百零五=105):数字暂存 num,
161
+ # 遇单位(十/百/千)把它乘上去并清零;万字内够用(章号没有更大的)
162
+ section, num = 0, 0
163
+ for ch in s:
164
+ if ch in cn:
165
+ num = cn[ch]
166
+ elif ch == "十":
167
+ section += (num or 1) * 10
168
+ num = 0
169
+ elif ch == "百":
170
+ section += (num or 1) * 100
171
+ num = 0
172
+ elif ch == "千":
173
+ section += (num or 1) * 1000
174
+ num = 0
175
+ return section + num
176
+
177
+
178
+ # ---------------------------------------------------------------- 护栏统计
179
+ def today_count(task_id, platform):
180
+ """该任务在该平台今日已成功发布的章数(每日上限护栏的计数口径)。"""
181
+ day = time.strftime("%Y-%m-%d")
182
+ return sum(1 for r in _iter_records(2)
183
+ if r.get("action") == "upload_chapter" and r.get("ok")
184
+ and r.get("task_id") == task_id and r.get("platform") == platform
185
+ and r.get("day") == day)
186
+
187
+
188
+ def consecutive_failures(platform):
189
+ """该平台最近连续失败的发布动作数(连败退避护栏,跨任务口径)。
190
+
191
+ 记录时间序旧→新,倒着数到第一条成功为止;连败大概率是风控或改版,
192
+ 此时继续自动重试只会火上浇油,应转人工检查。
193
+ """
194
+ n = 0
195
+ for r in reversed(list(_iter_records(7))):
196
+ if r.get("platform") != platform:
197
+ continue
198
+ if r.get("action") not in ("upload_chapter", "create_book"):
199
+ continue
200
+ if r.get("ok"):
201
+ break
202
+ n += 1
203
+ return n