codebee 0.1.17 → 0.1.19
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.
- package/CHANGELOG.md +22 -0
- package/README.md +4 -3
- package/app/core/automation.py +19 -0
- package/app/core/backup.py +470 -0
- package/app/core/bookmeta.py +4 -1
- package/app/core/builtin_agent.py +33 -1
- package/app/core/cleanup.py +303 -0
- package/app/core/flows.py +1 -1
- package/app/core/jobs.py +22 -4
- package/app/core/knowledge.py +393 -0
- package/app/core/manager.py +25 -1
- package/app/core/market.py +14 -2
- package/app/core/modelhub.py +16 -0
- package/app/core/pipeline.py +62 -9
- package/app/core/planner.py +22 -4
- package/app/core/publish/manager.py +80 -3
- package/app/core/runner.py +127 -7
- package/app/core/selfupdate.py +80 -38
- package/app/core/settings.py +32 -1
- package/app/core/skill_scan.py +86 -0
- package/app/core/store.py +62 -8
- package/app/core/wxdigest.py +710 -0
- package/app/core/zentao.py +516 -50
- package/app/main.py +276 -1
- package/app/pet.py +1323 -0
- package/app/pet_bee.png +0 -0
- package/app/pet_bee_robot.png +0 -0
- package/app/pick_dialog.py +34 -2
- package/app/ui/app.js +11063 -10267
- package/app/ui/i18n.js +194 -4
- package/app/ui/index.html +148 -1
- package/app/ui/style.css +160 -2
- package/package.json +1 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""每日垃圾清理:只清 CodeBee 自己产生的可再生数据,绝不碰用户成果。
|
|
3
|
+
|
|
4
|
+
垃圾来源(实测 data/ 923MB 构成):
|
|
5
|
+
- runs/<id>/steps/*.log 运行过程日志,单文件可达 22MB(保留 run.json
|
|
6
|
+
与 report.md,运行记录/结果页签不受影响)
|
|
7
|
+
- publish/shots/ 发布过程截图证据
|
|
8
|
+
- data 根 *.bak* 配置改写工具留下的备份残留
|
|
9
|
+
- pending/ 运行中指挥信箱的过期附件
|
|
10
|
+
- publish/profiles/ 浏览器 profile 迁移残留(新版已搬家目录,
|
|
11
|
+
旧位置 688MB 实测;仅当家目录同款平台目录
|
|
12
|
+
存在——即迁移已完成——才按废弃清理)
|
|
13
|
+
- exports/ imports/ 备份模块自己的历史产物
|
|
14
|
+
- data 根 *.log 服务日志按体量截尾,不是按天删
|
|
15
|
+
|
|
16
|
+
调度挂在 automation._tick(fire_due 自节流:启用 + 当天没清过才真跑)。
|
|
17
|
+
配置在 data/settings.json(cleanup_enabled / cleanup_retention_days),
|
|
18
|
+
状态在 data/cleanup.json(last_run/last_freed/history 供设置页展示)。
|
|
19
|
+
|
|
20
|
+
发布浏览器 profile(家目录 ~/.codebee/publish_profiles,约 1GB)含平台
|
|
21
|
+
登录态,清了要重新扫码——绝不进每日自动清理,只留手动入口
|
|
22
|
+
(run_cleanup(include_profiles=True))。
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import time
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
from . import paths, settings as settings_mod
|
|
32
|
+
|
|
33
|
+
DEFAULT_RETENTION_DAYS = 14
|
|
34
|
+
LOG_MAX_BYTES = 5 * 1024 * 1024 # 服务日志超过这个体量才截
|
|
35
|
+
LOG_KEEP_BYTES = 1 * 1024 * 1024 # 截尾保留的末段
|
|
36
|
+
STATE_NAME = "cleanup.json"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------- 状态
|
|
40
|
+
|
|
41
|
+
def _state_path():
|
|
42
|
+
return Path(paths.DATA_DIR) / STATE_NAME
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _load_state():
|
|
46
|
+
try:
|
|
47
|
+
d = json.loads(_state_path().read_text(encoding="utf-8"))
|
|
48
|
+
return d if isinstance(d, dict) else {}
|
|
49
|
+
except Exception:
|
|
50
|
+
return {}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _save_state(st):
|
|
54
|
+
p = _state_path()
|
|
55
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
tmp = p.with_suffix(".tmp")
|
|
57
|
+
tmp.write_text(json.dumps(st, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
58
|
+
os.replace(str(tmp), str(p))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _profiles_base(home=None):
|
|
62
|
+
"""家目录浏览器 profile 根(与 publish/manager 同款落点)。home 供测试注入。"""
|
|
63
|
+
if home:
|
|
64
|
+
return Path(home) / ".codebee" / "publish_profiles"
|
|
65
|
+
return Path.home() / ".codebee" / "publish_profiles"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _parse_local_dt(s):
|
|
69
|
+
try:
|
|
70
|
+
return time.mktime(time.strptime(str(s)[:19], "%Y-%m-%d %H:%M:%S"))
|
|
71
|
+
except Exception:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------- 扫描
|
|
76
|
+
|
|
77
|
+
def _run_ended(run_dir, cutoff_ts):
|
|
78
|
+
"""run 目录整体是否过期:看 run.json 的 ended_at;没有就退回目录 mtime。"""
|
|
79
|
+
rj = run_dir / "run.json"
|
|
80
|
+
if rj.is_file():
|
|
81
|
+
ts = None
|
|
82
|
+
try:
|
|
83
|
+
ts = _parse_local_dt(json.loads(rj.read_text(encoding="utf-8"))
|
|
84
|
+
.get("ended_at"))
|
|
85
|
+
except Exception:
|
|
86
|
+
ts = None
|
|
87
|
+
if ts:
|
|
88
|
+
return ts < cutoff_ts
|
|
89
|
+
try:
|
|
90
|
+
return run_dir.stat().st_mtime < cutoff_ts
|
|
91
|
+
except OSError:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _plan_items(retention_days=None, include_profiles=False, home=None, now=None):
|
|
96
|
+
"""扫描各类垃圾,返回条目列表。每条:key/label/note/bytes/count/files,
|
|
97
|
+
files 元素为 {path, op},op ∈ delete | rmtree | truncate。"""
|
|
98
|
+
now = now or time.time()
|
|
99
|
+
try:
|
|
100
|
+
days = int(retention_days if retention_days is not None
|
|
101
|
+
else settings_mod.load().get("cleanup_retention_days"))
|
|
102
|
+
except (TypeError, ValueError):
|
|
103
|
+
days = DEFAULT_RETENTION_DAYS
|
|
104
|
+
days = max(1, min(365, days))
|
|
105
|
+
cutoff = now - days * 86400
|
|
106
|
+
data_dir = Path(paths.DATA_DIR)
|
|
107
|
+
items = []
|
|
108
|
+
|
|
109
|
+
def add(key, label, note, files):
|
|
110
|
+
size = 0
|
|
111
|
+
good = []
|
|
112
|
+
for f in files:
|
|
113
|
+
try:
|
|
114
|
+
p = Path(f["path"])
|
|
115
|
+
if f["op"] == "truncate":
|
|
116
|
+
sz = p.stat().st_size - LOG_KEEP_BYTES
|
|
117
|
+
elif p.is_dir():
|
|
118
|
+
sz = sum(x.stat().st_size for x in p.rglob("*") if x.is_file())
|
|
119
|
+
else:
|
|
120
|
+
sz = p.stat().st_size
|
|
121
|
+
except OSError:
|
|
122
|
+
continue
|
|
123
|
+
# truncate 只在有得砍时才留;delete/rmtree 即使 0 字节(空目录)也要清
|
|
124
|
+
if sz > 0 or f["op"] != "truncate":
|
|
125
|
+
good.append(f)
|
|
126
|
+
size += max(0, sz)
|
|
127
|
+
if good:
|
|
128
|
+
items.append({"key": key, "label": label, "note": note,
|
|
129
|
+
"bytes": size, "count": len(good), "files": good})
|
|
130
|
+
|
|
131
|
+
# 1) 过期运行的过程日志(保留 run.json / report.md / 封面)
|
|
132
|
+
runs_dir = data_dir / "runs"
|
|
133
|
+
files = []
|
|
134
|
+
if runs_dir.is_dir():
|
|
135
|
+
for rd in runs_dir.iterdir():
|
|
136
|
+
if not rd.is_dir() or not _run_ended(rd, cutoff):
|
|
137
|
+
continue
|
|
138
|
+
steps = rd / "steps"
|
|
139
|
+
if steps.is_dir():
|
|
140
|
+
files.append({"path": str(steps), "op": "rmtree"})
|
|
141
|
+
add("runs_old_logs", "运行过程日志",
|
|
142
|
+
"保留运行记录与结果报告,只删过程中的原始日志", files)
|
|
143
|
+
|
|
144
|
+
# 2) 发布截图
|
|
145
|
+
shots = data_dir / "publish" / "shots"
|
|
146
|
+
add("publish_shots", "发布截图", "发布过程留档截图,超期即清",
|
|
147
|
+
[{"path": str(p), "op": "delete"}
|
|
148
|
+
for p in (shots.iterdir() if shots.is_dir() else [])
|
|
149
|
+
if p.is_file() and p.stat().st_mtime < cutoff])
|
|
150
|
+
|
|
151
|
+
# 3) 配置备份残留 *.bak*
|
|
152
|
+
add("bak_files", "配置备份残留", "改配置时自动留的 .bak 副本,超期即清",
|
|
153
|
+
[{"path": str(p), "op": "delete"}
|
|
154
|
+
for p in data_dir.glob("*.bak*")
|
|
155
|
+
if p.is_file() and p.stat().st_mtime < cutoff])
|
|
156
|
+
|
|
157
|
+
# 4) 过期指挥信箱(运行早已结束,附件永远不会被消费)
|
|
158
|
+
pend = data_dir / "pending"
|
|
159
|
+
files = []
|
|
160
|
+
for p in (pend.iterdir() if pend.is_dir() else []):
|
|
161
|
+
try:
|
|
162
|
+
if p.stat().st_mtime >= cutoff:
|
|
163
|
+
continue
|
|
164
|
+
except OSError:
|
|
165
|
+
continue
|
|
166
|
+
files.append({"path": str(p), "op": "rmtree" if p.is_dir() else "delete"})
|
|
167
|
+
add("pending_stale", "过期指挥信箱", "已结束任务的运行中留言附件",
|
|
168
|
+
files)
|
|
169
|
+
|
|
170
|
+
# 5) 浏览器 profile 迁移残留:家目录同款平台目录存在(迁移已完成)才清
|
|
171
|
+
legacy = data_dir / "publish" / "profiles"
|
|
172
|
+
files = []
|
|
173
|
+
if legacy.is_dir():
|
|
174
|
+
for p in legacy.iterdir():
|
|
175
|
+
if not p.is_dir():
|
|
176
|
+
continue
|
|
177
|
+
try:
|
|
178
|
+
if (_profiles_base(home) / p.name).exists():
|
|
179
|
+
files.append({"path": str(p), "op": "rmtree"})
|
|
180
|
+
except OSError:
|
|
181
|
+
continue
|
|
182
|
+
add("legacy_profiles", "旧版浏览器缓存残留",
|
|
183
|
+
"发布浏览器 profile 已迁往用户主目录,旧位置属废弃残留", files)
|
|
184
|
+
|
|
185
|
+
# 6) 历史备份包(导出 zip / 导入前自动备份,超期即清)
|
|
186
|
+
files = []
|
|
187
|
+
for d in ("exports", "imports"):
|
|
188
|
+
base = data_dir / d
|
|
189
|
+
for p in (base.iterdir() if base.is_dir() else []):
|
|
190
|
+
try:
|
|
191
|
+
if p.stat().st_mtime < cutoff:
|
|
192
|
+
files.append({"path": str(p),
|
|
193
|
+
"op": "rmtree" if p.is_dir() else "delete"})
|
|
194
|
+
except OSError:
|
|
195
|
+
continue
|
|
196
|
+
add("backups_old", "历史备份包", "导出与导入前自动备份的旧 zip", files)
|
|
197
|
+
|
|
198
|
+
# 7) 服务日志截尾(不删文件,砍掉前段)
|
|
199
|
+
files = [{"path": str(p), "op": "truncate"}
|
|
200
|
+
for p in data_dir.glob("*.log")
|
|
201
|
+
if p.is_file() and p.stat().st_size > LOG_MAX_BYTES]
|
|
202
|
+
add("logs_truncate", "服务日志瘦身", "只保留每个日志的末尾 1MB", files)
|
|
203
|
+
|
|
204
|
+
# 8) 手动:发布浏览器缓存(含登录态,清了要重新扫码;绝不自动清)
|
|
205
|
+
if include_profiles:
|
|
206
|
+
base = _profiles_base(home)
|
|
207
|
+
files = [{"path": str(p), "op": "rmtree"}
|
|
208
|
+
for p in (base.iterdir() if base.is_dir() else []) if p.is_dir()]
|
|
209
|
+
add("publish_profiles", "发布浏览器缓存",
|
|
210
|
+
"含平台登录态,清理后发布时需重新扫码登录", files)
|
|
211
|
+
return {"retention_days": days, "items": items,
|
|
212
|
+
"total_bytes": sum(i["bytes"] for i in items)}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def plan(retention_days=None, include_profiles=False, home=None):
|
|
216
|
+
return _plan_items(retention_days=retention_days,
|
|
217
|
+
include_profiles=include_profiles, home=home)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def status():
|
|
221
|
+
"""设置页一次拉全:配置 + 上次清理状态 + 当前可清理预估。"""
|
|
222
|
+
cfg = settings_mod.load()
|
|
223
|
+
return {"config": {"enabled": bool(cfg.get("cleanup_enabled")),
|
|
224
|
+
"retention_days": cfg.get("cleanup_retention_days")},
|
|
225
|
+
"state": _load_state(), "plan": plan()}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ---------------------------------------------------------------- 执行
|
|
229
|
+
|
|
230
|
+
def _delete_path(p: Path, op):
|
|
231
|
+
if op == "rmtree":
|
|
232
|
+
import shutil
|
|
233
|
+
shutil.rmtree(str(p), ignore_errors=False)
|
|
234
|
+
elif op == "truncate":
|
|
235
|
+
with open(str(p), "rb") as f:
|
|
236
|
+
f.seek(-LOG_KEEP_BYTES, 2)
|
|
237
|
+
tail = f.read()
|
|
238
|
+
tmp = p.with_suffix(".tmp-cl")
|
|
239
|
+
tmp.write_bytes(tail)
|
|
240
|
+
os.replace(str(tmp), str(p))
|
|
241
|
+
else:
|
|
242
|
+
p.unlink()
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def run_cleanup(retention_days=None, include_profiles=False):
|
|
246
|
+
"""按当前扫描结果执行清理。返回汇总并落状态(供设置页展示)。"""
|
|
247
|
+
scan = _plan_items(retention_days=retention_days,
|
|
248
|
+
include_profiles=include_profiles)
|
|
249
|
+
freed = 0
|
|
250
|
+
done = []
|
|
251
|
+
errors = []
|
|
252
|
+
for item in scan["items"]:
|
|
253
|
+
got = 0
|
|
254
|
+
n = 0
|
|
255
|
+
for f in item["files"]:
|
|
256
|
+
try:
|
|
257
|
+
p = Path(f["path"])
|
|
258
|
+
if f["op"] == "truncate":
|
|
259
|
+
sz = p.stat().st_size - LOG_KEEP_BYTES
|
|
260
|
+
elif p.is_dir():
|
|
261
|
+
sz = sum(x.stat().st_size for x in p.rglob("*") if x.is_file())
|
|
262
|
+
else:
|
|
263
|
+
sz = p.stat().st_size
|
|
264
|
+
_delete_path(p, f["op"])
|
|
265
|
+
got += max(0, sz)
|
|
266
|
+
n += 1
|
|
267
|
+
except OSError as e:
|
|
268
|
+
errors.append("%s:%s" % (f["path"], e.strerror or e))
|
|
269
|
+
if n:
|
|
270
|
+
done.append({"key": item["key"], "label": item["label"],
|
|
271
|
+
"bytes": got, "count": n})
|
|
272
|
+
freed += got
|
|
273
|
+
result = {"ts": time.strftime("%Y-%m-%d %H:%M:%S"), "freed": freed,
|
|
274
|
+
"items": done, "errors": errors[:10],
|
|
275
|
+
"include_profiles": bool(include_profiles)}
|
|
276
|
+
st = _load_state()
|
|
277
|
+
st["last_run"] = result["ts"]
|
|
278
|
+
st["last_freed"] = freed
|
|
279
|
+
st["last_result"] = result
|
|
280
|
+
hist = st.get("history") or []
|
|
281
|
+
hist.insert(0, {"ts": result["ts"], "freed": freed})
|
|
282
|
+
st["history"] = hist[:30]
|
|
283
|
+
_save_state(st)
|
|
284
|
+
try:
|
|
285
|
+
from . import store
|
|
286
|
+
store.bump_state()
|
|
287
|
+
except Exception:
|
|
288
|
+
pass
|
|
289
|
+
return result
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def fire_due():
|
|
293
|
+
"""automation._tick 每拍调用:启用且当天没清过才真跑,否则零开销返回。"""
|
|
294
|
+
try:
|
|
295
|
+
cfg = settings_mod.load()
|
|
296
|
+
except Exception:
|
|
297
|
+
return None
|
|
298
|
+
if not cfg.get("cleanup_enabled"):
|
|
299
|
+
return None
|
|
300
|
+
today = time.strftime("%Y-%m-%d")
|
|
301
|
+
if str(_load_state().get("last_run") or "").startswith(today):
|
|
302
|
+
return None
|
|
303
|
+
return run_cleanup()
|
package/app/core/flows.py
CHANGED
|
@@ -76,7 +76,7 @@ BUILTIN_FLOWS = [
|
|
|
76
76
|
"note": "起草 → 多维评审 → 修订循环 → 发布门禁"},
|
|
77
77
|
{"id": "rank_scan", "name": "扫榜选材", "icon": "i-chart", "engine": "direct", "builtin": True,
|
|
78
78
|
"goal_hint": "想写哪个方向(一句话,可留空默认分析总榜热门题材)",
|
|
79
|
-
"note": "
|
|
79
|
+
"note": "抓取七猫+番茄排行榜公开数据 → AI 提炼跨平台热门题材/人设/差异化切入点(快档直出报告)"},
|
|
80
80
|
{"id": "research", "name": "调研报告", "icon": "i-file-search", "engine": "review", "builtin": True,
|
|
81
81
|
"manuscript": "report.md",
|
|
82
82
|
"rubric": ["全面性", "深度", "论据可靠", "可读性", "结论质量"],
|
package/app/core/jobs.py
CHANGED
|
@@ -156,6 +156,18 @@ def cancel(run_id):
|
|
|
156
156
|
ev = CANCELS.get(run_id)
|
|
157
157
|
if ev:
|
|
158
158
|
ev.set()
|
|
159
|
+
# 强制终止语义:置位事件后立刻把 running 落成 cancelled 终态,
|
|
160
|
+
# UI 即刻翻牌,不等流水线走到下一个检查点(CLI 子进程树由取消
|
|
161
|
+
# 事件在 run_process 的 0.4s 轮询里秒杀;内置智能体生成中的 HTTP
|
|
162
|
+
# 由取消等待方放弃)。expected_status 守卫保证已正常结束的运行
|
|
163
|
+
# 不被误改;流水线随后照常收尾,终态幂等。
|
|
164
|
+
try:
|
|
165
|
+
from . import store
|
|
166
|
+
store.update_run(run_id, expected_status="running",
|
|
167
|
+
status="cancelled", ended_at=_now(),
|
|
168
|
+
error="用户主动取消")
|
|
169
|
+
except Exception:
|
|
170
|
+
pass
|
|
159
171
|
return True
|
|
160
172
|
# 事件不存在=任务还在队列里没被 worker 拿起:直接落终态(取消事件在
|
|
161
173
|
# worker 起跑时才创建,排队任务点取消会在这里漏掉——起跑后再杀一遍)。
|
|
@@ -576,12 +588,13 @@ def _do_mgmt(job, ev):
|
|
|
576
588
|
cost_usd=res.get("cost_usd", 0.0), tokens=res.get("tokens", 0))
|
|
577
589
|
else:
|
|
578
590
|
store.finish_step(run_id, step["n"], "failed", summary="未知操作 %s" % op)
|
|
579
|
-
# 以步骤状态汇总 run
|
|
591
|
+
# 以步骤状态汇总 run 状态;expected_status 守卫:用户已强制终止的运行
|
|
592
|
+
# 保持 cancelled,不被步骤汇总改写成 failed(被杀的步骤记的就是非 done)
|
|
580
593
|
run = store.get_run(run_id)
|
|
581
594
|
statuses = [s["status"] for s in (run.get("steps") if run else [])] or ["failed"]
|
|
582
595
|
final = "done" if all(s == "done" for s in statuses) else "failed"
|
|
583
596
|
suffix = "(AI 修复成功)" if ok and len((run.get("steps") if run else [])) > 1 else ""
|
|
584
|
-
store.update_run(run_id, status=final, ended_at=_now(),
|
|
597
|
+
store.update_run(run_id, expected_status="running", status=final, ended_at=_now(),
|
|
585
598
|
summary=("%s %s %s%s" % (entry.get("name"), op,
|
|
586
599
|
"完成" if final == "done" else "失败", suffix)))
|
|
587
600
|
|
|
@@ -600,7 +613,9 @@ def _do_selfupgrade(job):
|
|
|
600
613
|
summary="升级完成,点「重启」生效" if res["ok"]
|
|
601
614
|
else ("升级失败: " + res["error"][:300]),
|
|
602
615
|
exit_code=res.get("exit_code"))
|
|
603
|
-
|
|
616
|
+
# expected_status 守卫:用户已强制终止的运行保持 cancelled,不被改写
|
|
617
|
+
store.update_run(run_id, expected_status="running",
|
|
618
|
+
status="done" if res["ok"] else "failed", ended_at=_now(),
|
|
604
619
|
summary="CodeBee selfupgrade %s" % ("完成" if res["ok"] else "失败"))
|
|
605
620
|
|
|
606
621
|
|
|
@@ -619,7 +634,10 @@ def _ai_repair(run_id, entry, ev, failed_cmd, orig_log):
|
|
|
619
634
|
if agent is None or agent.get("mode") != "real":
|
|
620
635
|
return False # 无真实智能体可用,维持原失败
|
|
621
636
|
try:
|
|
622
|
-
|
|
637
|
+
# 日志尾巴喂进 AI 修复提示词:先洗终端噪声,否则 ANSI 转义/乱码墙会被
|
|
638
|
+
# 模型当成"日志原文"照抄进修复推理里
|
|
639
|
+
log_tail = runner.clean_cli_text(
|
|
640
|
+
runner.tail_decoded(orig_log.read_bytes(), 2000)) if orig_log.exists() else "(无输出)"
|
|
623
641
|
except Exception:
|
|
624
642
|
log_tail = "(日志不可读)"
|
|
625
643
|
import shutil
|