codebee 0.1.18 → 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 +15 -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 +26 -4
- 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 +50 -8
- 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 +263 -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 -10389
- package/app/ui/i18n.js +169 -9
- package/app/ui/index.html +132 -2
- package/app/ui/style.css +156 -2
- package/package.json +1 -1
package/app/core/settings.py
CHANGED
|
@@ -16,9 +16,19 @@ _FILE = paths.DATA_DIR / "settings.json"
|
|
|
16
16
|
# 「导出诊断包」是用户手动操作不受此开关限制)
|
|
17
17
|
# publish_daily_cap / publish_fail_streak:自动发布护栏——每任务每平台每日
|
|
18
18
|
# 成功发章上限、平台连续失败几次后暂停自动发布(publish/auto.py 读取)
|
|
19
|
+
# pet_enabled / pet_mode:桌面蜜蜂(app/pet.py)开关与显示模式。关闭后看护线程
|
|
20
|
+
# 不再拉起、在岗蜜蜂轮询到 false 自行退出;mode=always 常驻,tasks_only 仅任务
|
|
21
|
+
# 运行时出现(空闲 90s 隐身)。
|
|
22
|
+
# cleanup_enabled / cleanup_retention_days:每日垃圾清理(core/cleanup.py)——
|
|
23
|
+
# 运行过程日志/发布截图/bak 残留等超期自动清理;retention 为保留天数。
|
|
19
24
|
DEFAULTS = {"max_concurrent_jobs": 6, "default_workdir": "", "hooks_token": "",
|
|
20
25
|
"telemetry_errors": True, "publish_daily_cap": 10,
|
|
21
|
-
"publish_fail_streak": 3, "notify_webhook": "", "notify_base_url": ""
|
|
26
|
+
"publish_fail_streak": 3, "notify_webhook": "", "notify_base_url": "",
|
|
27
|
+
"pet_enabled": True, "pet_mode": "always", "pet_skin": "plush",
|
|
28
|
+
"cleanup_enabled": True, "cleanup_retention_days": 14}
|
|
29
|
+
# 桌宠形象白名单(与 app/pet.py 的 SKINS 对齐;这里不 import pet 模块,避免
|
|
30
|
+
# core 反向依赖 app 根目录脚本)
|
|
31
|
+
PET_SKINS = ("plush", "robot")
|
|
22
32
|
# 并发上限 12:worker 只是拉起 CLI 子进程的调度位,跨任务无共享资源;
|
|
23
33
|
# 同任务单飞守卫在 jobs 层。默认 6 对齐「多任务并行不排队」的使用预期。
|
|
24
34
|
MIN_WORKERS, MAX_WORKERS = 1, 12
|
|
@@ -103,6 +113,27 @@ def save(patch):
|
|
|
103
113
|
cur["notify_webhook"] = str(patch.get("notify_webhook") or "").strip()[:300]
|
|
104
114
|
if "notify_base_url" in patch:
|
|
105
115
|
cur["notify_base_url"] = str(patch.get("notify_base_url") or "").strip()[:200]
|
|
116
|
+
if "pet_enabled" in patch:
|
|
117
|
+
cur["pet_enabled"] = bool(patch.get("pet_enabled"))
|
|
118
|
+
if "pet_mode" in patch:
|
|
119
|
+
pm = str(patch.get("pet_mode") or "").strip()
|
|
120
|
+
if pm not in ("always", "tasks_only"):
|
|
121
|
+
return cur, "pet_mode 只能是 always 或 tasks_only"
|
|
122
|
+
cur["pet_mode"] = pm
|
|
123
|
+
if "pet_skin" in patch:
|
|
124
|
+
ps = str(patch.get("pet_skin") or "").strip()
|
|
125
|
+
if ps not in PET_SKINS:
|
|
126
|
+
return cur, "pet_skin 只能是 %s 之一" % "/".join(PET_SKINS)
|
|
127
|
+
cur["pet_skin"] = ps
|
|
128
|
+
if "cleanup_enabled" in patch:
|
|
129
|
+
cur["cleanup_enabled"] = bool(patch.get("cleanup_enabled"))
|
|
130
|
+
if "cleanup_retention_days" in patch:
|
|
131
|
+
try:
|
|
132
|
+
cur["cleanup_retention_days"] = int(patch.get("cleanup_retention_days"))
|
|
133
|
+
except (TypeError, ValueError):
|
|
134
|
+
return cur, "cleanup_retention_days 必须是整数"
|
|
135
|
+
if not 1 <= cur["cleanup_retention_days"] <= 365:
|
|
136
|
+
return cur, "cleanup_retention_days 取值 1-365"
|
|
106
137
|
_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
107
138
|
tmp = _FILE.with_suffix(".tmp")
|
|
108
139
|
tmp.write_text(json.dumps(cur, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""skill 装前危险模式扫描(借鉴 NVIDIA SkillSpector 的静态扫描思路)。
|
|
3
|
+
|
|
4
|
+
研究数据(SkillSpector 对 31,132 个 skill 的分析):26.1% 含漏洞、5.2% 疑似
|
|
5
|
+
恶意。我们在「白名单闸门 + SSRF 防护 + 剥离式检查」之外补一道**内容静态扫描**:
|
|
6
|
+
装前扫 skill 正文里的危险模式,给风险提示——提示不是拦阻(用户仍可装),
|
|
7
|
+
但危险必须被看见。
|
|
8
|
+
|
|
9
|
+
纯内存函数:输入是文本,输出是发现列表,无网络无落盘。
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
# 危险模式(静态正则;命中即在装前提示行展示)
|
|
16
|
+
# —— 每条:(模式, 类别, 中文说明)
|
|
17
|
+
_PATTERNS = [
|
|
18
|
+
# 代码执行
|
|
19
|
+
(r"\beval\s*\(", "代码执行", "动态 eval 执行任意代码"),
|
|
20
|
+
(r"\bexec\s*\(", "代码执行", "exec 执行任意代码"),
|
|
21
|
+
(r"subprocess|os\.system|Popen", "代码执行", "起子进程执行命令"),
|
|
22
|
+
# 数据外发
|
|
23
|
+
(r"https?://(?!api\.|docs\.|github\.com|raw\.githubusercontent)[a-z0-9.-]+/(upload|collect|track|ingest|webhook|callback)",
|
|
24
|
+
"数据外发", "向非常规端点上传/回调数据"),
|
|
25
|
+
(r"requests\.post|urllib\.request|fetch\s*\(", "网络请求", "发起网络请求(确认目标可信)"),
|
|
26
|
+
# 敏感信息读取
|
|
27
|
+
(r"os\.environ|process\.env|getenv", "环境读取", "读取环境变量(可能带走密钥)"),
|
|
28
|
+
(r"\.ssh/|\.aws/|\.npmrc|\.gitconfig|credentials|\.env\b", "敏感文件", "触碰凭据/密钥文件路径"),
|
|
29
|
+
(r"keychain|credential manager|dpapi", "敏感文件", "访问系统凭据库"),
|
|
30
|
+
# 提示注入特征
|
|
31
|
+
(r"(ignore|disregard|forget).{0,30}(previous|above|prior|all).{0,20}(instruction|prompt|rule)",
|
|
32
|
+
"提示注入", "指令覆盖话术(试图无视既有规则)"),
|
|
33
|
+
(r"system prompt|开发者指令|隐藏指令", "提示注入", "提及系统提示词/隐藏指令"),
|
|
34
|
+
(r"do not (tell|reveal|mention).{0,20}(user|human|player)", "提示注入", "要求对用户隐瞒行为"),
|
|
35
|
+
# 反拒答/越权
|
|
36
|
+
(r"(you (are|must) (now|act as)|从此你是|你现在必须)", "越权人格", "试图重设助手人格"),
|
|
37
|
+
(r"exfiltrat|渗透|后门|backdoor|keylog", "可疑意图", "包含可疑渗透/后门词汇"),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
_COMPILED = [(re.compile(p, re.I), cat, desc) for p, cat, desc in _PATTERNS]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def scan_text(text, max_findings=12):
|
|
44
|
+
"""扫描一段 skill 文本。返回 [{category, detail, line}];空列表=干净。
|
|
45
|
+
|
|
46
|
+
line 是 1 起的行号(供装前提示定位)。同一类别只报首个命中
|
|
47
|
+
(提示行是给人看的,重复刷屏没有信息量)。"""
|
|
48
|
+
findings, seen_cats = [], set()
|
|
49
|
+
if not text:
|
|
50
|
+
return []
|
|
51
|
+
lines = text.splitlines()
|
|
52
|
+
for rx, cat, desc in _COMPILED:
|
|
53
|
+
if cat in seen_cats:
|
|
54
|
+
continue
|
|
55
|
+
for i, ln in enumerate(lines, 1):
|
|
56
|
+
if rx.search(ln):
|
|
57
|
+
findings.append({"category": cat, "detail": desc, "line": i})
|
|
58
|
+
seen_cats.add(cat)
|
|
59
|
+
break
|
|
60
|
+
if len(findings) >= max_findings:
|
|
61
|
+
break
|
|
62
|
+
return findings
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def risk_label(findings):
|
|
66
|
+
"""发现列表 → 风险标签(装前提示行用)。
|
|
67
|
+
|
|
68
|
+
高危(代码执行/敏感文件/可疑意图)任一命中 = 高风险;否则有发现 = 注意;
|
|
69
|
+
空 = 干净。"""
|
|
70
|
+
if not findings:
|
|
71
|
+
return ""
|
|
72
|
+
cats = {f["category"] for f in findings}
|
|
73
|
+
if cats & {"代码执行", "敏感文件", "可疑意图", "数据外发"}:
|
|
74
|
+
return "⚠ 高风险"
|
|
75
|
+
return "△ 注意"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def scan_summary(text):
|
|
79
|
+
"""一步到位:扫描+汇总成一行提示文案(空=干净返回空串)。"""
|
|
80
|
+
fs = scan_text(text)
|
|
81
|
+
if not fs:
|
|
82
|
+
return ""
|
|
83
|
+
label = risk_label(fs)
|
|
84
|
+
top = "、".join("%s(行%d)" % (f["category"], f["line"]) for f in fs[:4])
|
|
85
|
+
more = " 等 %d 项" % len(fs) if len(fs) > 4 else ""
|
|
86
|
+
return "%s:%s%s" % (label, top, more)
|
package/app/core/store.py
CHANGED
|
@@ -410,6 +410,28 @@ def migrate_task_workdirs(old_root, new_root):
|
|
|
410
410
|
return moved, skipped
|
|
411
411
|
|
|
412
412
|
|
|
413
|
+
def _sanitize_run_text(r):
|
|
414
|
+
"""就地清洗 run 与各步骤里的 CLI 文本字段(错误串/摘要/输出)。
|
|
415
|
+
|
|
416
|
+
写入侧与读盘侧共用:runner 已经把 ANSI/覆写/乱码墙洗过一遍,这里兜住
|
|
417
|
+
「错误串是我们自己拼的」「历史数据是旧版本写的」两条漏网路径。只动字符串
|
|
418
|
+
值,不碰结构——清洗是幂等的,重复调用无副作用。"""
|
|
419
|
+
for k in ("error", "summary"):
|
|
420
|
+
v = r.get(k)
|
|
421
|
+
if isinstance(v, str) and v:
|
|
422
|
+
r[k] = runner.clean_cli_text(v)
|
|
423
|
+
for s in r.get("steps") or []:
|
|
424
|
+
if not isinstance(s, dict):
|
|
425
|
+
continue
|
|
426
|
+
for k in ("summary", "error"):
|
|
427
|
+
v = s.get(k)
|
|
428
|
+
if isinstance(v, str) and v:
|
|
429
|
+
s[k] = runner.clean_cli_text(v)
|
|
430
|
+
# output 是智能体正文:只剥 ANSI(同 finish_step 的口径)
|
|
431
|
+
if isinstance(s.get("output"), str) and s["output"]:
|
|
432
|
+
s["output"] = runner.strip_ansi(s["output"])
|
|
433
|
+
|
|
434
|
+
|
|
413
435
|
def load_all():
|
|
414
436
|
with LOCK:
|
|
415
437
|
for p in paths.TASKS_DIR.glob("*.json"):
|
|
@@ -422,6 +444,10 @@ def load_all():
|
|
|
422
444
|
try:
|
|
423
445
|
r = json.loads(p.read_text(encoding="utf-8"))
|
|
424
446
|
r.pop("cancel_event", None)
|
|
447
|
+
# 存量清洗:早期版本把 CLI 的 ANSI 转义/控制字符原样写进了摘要与
|
|
448
|
+
# 错误串(`[91m[1mError:`、U+FFFD 乱码墙),读盘时统一洗一遍——
|
|
449
|
+
# 老运行不必等重跑才干净(2026-09-20「咋还有乱码」实测)
|
|
450
|
+
_sanitize_run_text(r)
|
|
425
451
|
# 队列不跨进程持久化:磁盘上仍是 queued/running 的运行必是上次进程中断的残骸
|
|
426
452
|
if r.get("status") in ("queued", "running"):
|
|
427
453
|
r["status"] = "failed"
|
|
@@ -620,6 +646,11 @@ def update_run(run_id, expected_status=None, **fields):
|
|
|
620
646
|
return None
|
|
621
647
|
if expected_status is not None and run.get("status") != expected_status:
|
|
622
648
|
return None
|
|
649
|
+
for k in ("error", "summary"):
|
|
650
|
+
if isinstance(fields.get(k), str):
|
|
651
|
+
# 错误串多是我们自己拼的 CLI 尾巴(含 ANSI/覆写/乱码墙):
|
|
652
|
+
# 落内存前洗一遍,UI/台账读到的就是干净文本
|
|
653
|
+
fields[k] = runner.clean_cli_text(fields[k])
|
|
623
654
|
run.update(fields)
|
|
624
655
|
# 终态收尸:run 已结束却还挂 queued/running 的步骤统一落 cancelled,
|
|
625
656
|
# 语义与 recover_orphaned_runs 的启动清扫对齐(那套清跨进程遗留,
|
|
@@ -1105,7 +1136,26 @@ def retry_task(task_id):
|
|
|
1105
1136
|
if s.get("status") == "done"
|
|
1106
1137
|
and (s.get("role") or "").startswith("draft-c")
|
|
1107
1138
|
and str(s.get("role")).split("c")[-1].isdigit()})
|
|
1108
|
-
|
|
1139
|
+
v_scores = (prev.get("verdict") or {}).get("chapter_scores") or []
|
|
1140
|
+
# 重写未达标章:上一遍完整跑完但质量未过线(verdict.publishable
|
|
1141
|
+
# =False)时,未过线章的成稿与分数都不进继承——流水线对缺继承
|
|
1142
|
+
# 的章走正常起草+评审,等于只重写这几章;已过线章照常复用不烧
|
|
1143
|
+
# token。大纲始终继承(全章未过线时 done 清空也继承),保住全书
|
|
1144
|
+
# 结构——这正是「重写未达标章」按钮(前端 done+未达标态放行
|
|
1145
|
+
# retry)区别于断点续跑的语义。
|
|
1146
|
+
redo = set()
|
|
1147
|
+
if (prev.get("verdict") or {}).get("publishable") is False:
|
|
1148
|
+
redo = {int(c["chapter"]) for c in v_scores
|
|
1149
|
+
if not c.get("passed") and c.get("chapter") is not None}
|
|
1150
|
+
if redo:
|
|
1151
|
+
done = [n for n in done if n not in redo]
|
|
1152
|
+
v_scores = [c for c in v_scores if c.get("passed")]
|
|
1153
|
+
if done or redo:
|
|
1154
|
+
if redo:
|
|
1155
|
+
scores = v_scores # 未达标重写:只带已过线章的分数
|
|
1156
|
+
else:
|
|
1157
|
+
scores = ((prev.get("verdict") or {}).get("chapter_scores")
|
|
1158
|
+
or prev.get("chapter_scores") or [])
|
|
1109
1159
|
run["inherit"] = {
|
|
1110
1160
|
"outline": outline,
|
|
1111
1161
|
"done_chapters": done,
|
|
@@ -1113,8 +1163,7 @@ def retry_task(task_id):
|
|
|
1113
1163
|
# failed/cancelled 的 run 没有 verdict,退回每章实时
|
|
1114
1164
|
# 落账的 chapter_scores——否则多轮失败恢复会把全部
|
|
1115
1165
|
# 已过线章节重新评审(实测一晚白烧数百万 token)
|
|
1116
|
-
"chapter_scores":
|
|
1117
|
-
or prev.get("chapter_scores") or []),
|
|
1166
|
+
"chapter_scores": scores,
|
|
1118
1167
|
}
|
|
1119
1168
|
break
|
|
1120
1169
|
task["status"] = "queued"
|
|
@@ -1333,7 +1382,9 @@ def finish_step(run_id, n, status, summary="", exit_code=None,
|
|
|
1333
1382
|
if s["n"] == n:
|
|
1334
1383
|
s["status"] = status
|
|
1335
1384
|
s["ended_at"] = time.strftime("%H:%M:%S")
|
|
1336
|
-
|
|
1385
|
+
# 步骤摘要是 CLI 文本的汇聚点(失败时=错误尾巴,成功时=智能体结论):
|
|
1386
|
+
# 落盘前统一清洗,避免 ANSI/覆写/乱码墙进 UI 与报告
|
|
1387
|
+
s["summary"] = runner.clean_cli_text(summary)
|
|
1337
1388
|
s["exit_code"] = exit_code
|
|
1338
1389
|
s["cost_usd"] = round(cost_usd, 4)
|
|
1339
1390
|
s["tokens"] = tokens
|
|
@@ -1342,7 +1393,9 @@ def finish_step(run_id, n, status, summary="", exit_code=None,
|
|
|
1342
1393
|
if duration_s is not None:
|
|
1343
1394
|
s["duration_s"] = round(duration_s, 1)
|
|
1344
1395
|
if output is not None:
|
|
1345
|
-
|
|
1396
|
+
# output 是智能体正文(对话气泡直读):只剥 ANSI,不做噪声折叠
|
|
1397
|
+
# ——正文里的装饰性长串是作者写的,不能替它省略
|
|
1398
|
+
s["output"] = runner.strip_ansi(str(output))[:6000]
|
|
1346
1399
|
if followups:
|
|
1347
1400
|
s["followups"] = list(followups)[:3]
|
|
1348
1401
|
break
|
|
@@ -1497,9 +1550,10 @@ def read_step_log(run_id, rel_path, tail=paths.LOG_TAIL_CHARS, pretty=False):
|
|
|
1497
1550
|
text = "...(已截断)...\n" + runner.tail_decoded(data, tail)
|
|
1498
1551
|
else:
|
|
1499
1552
|
text = runner.decode_output(data)
|
|
1500
|
-
#
|
|
1501
|
-
#
|
|
1502
|
-
|
|
1553
|
+
# 先洗终端噪声(ANSI/覆写/乱码墙),再折叠遥测刷屏(时间戳不同的重复
|
|
1554
|
+
# WARN);pretty 最后把 codex JSONL 事件流翻译成【消息】【命令】等可读行
|
|
1555
|
+
# ——顺序不能反:乱码墙会挤掉折叠分组,翻译也会把 ANSI 当正文
|
|
1556
|
+
text = runner.collapse_dup_lines(runner.clean_cli_text(text))
|
|
1503
1557
|
if pretty:
|
|
1504
1558
|
text = runner.pretty_cli_log(text)
|
|
1505
1559
|
return text
|