codebee 0.1.10 → 0.1.12
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 +21 -0
- package/README.md +8 -6
- package/app/core/aiflavor.py +51 -0
- package/app/core/automation.py +9 -0
- package/app/core/gitmod.py +4 -0
- package/app/core/jobs.py +115 -13
- package/app/core/manager.py +49 -0
- package/app/core/modelhub.py +10 -1
- package/app/core/paths.py +1 -0
- package/app/core/pipeline.py +316 -2
- package/app/core/publish/__init__.py +7 -0
- package/app/core/publish/auto.py +367 -0
- package/app/core/publish/browser.py +410 -0
- package/app/core/publish/fanqie.py +98 -0
- package/app/core/publish/flow.py +281 -0
- package/app/core/publish/ledger.py +198 -0
- package/app/core/publish/manager.py +427 -0
- package/app/core/publish/qimao.py +97 -0
- package/app/core/publish/ws.py +139 -0
- package/app/core/settings.py +18 -3
- package/app/core/store.py +24 -2
- package/app/main.py +174 -0
- package/app/ui/app.js +325 -26
- package/app/ui/i18n.js +5 -1
- package/app/ui/index.html +5 -3
- package/app/ui/style.css +64 -0
- package/package.json +1 -1
package/app/core/settings.py
CHANGED
|
@@ -14,9 +14,14 @@ _FILE = paths.DATA_DIR / "settings.json"
|
|
|
14
14
|
# default_workdir 为空表示未自定义,用 builtin_workdir() 回落;
|
|
15
15
|
# telemetry_errors:匿名错误回传开关(默认开;关掉后版本 ping/错误上传/诊断包遥测部分全部停发,
|
|
16
16
|
# 「导出诊断包」是用户手动操作不受此开关限制)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
# publish_daily_cap / publish_fail_streak:自动发布护栏——每任务每平台每日
|
|
18
|
+
# 成功发章上限、平台连续失败几次后暂停自动发布(publish/auto.py 读取)
|
|
19
|
+
DEFAULTS = {"max_concurrent_jobs": 6, "default_workdir": "", "hooks_token": "",
|
|
20
|
+
"telemetry_errors": True, "publish_daily_cap": 10,
|
|
21
|
+
"publish_fail_streak": 3}
|
|
22
|
+
# 并发上限 12:worker 只是拉起 CLI 子进程的调度位,跨任务无共享资源;
|
|
23
|
+
# 同任务单飞守卫在 jobs 层。默认 6 对齐「多任务并行不排队」的使用预期。
|
|
24
|
+
MIN_WORKERS, MAX_WORKERS = 1, 12
|
|
20
25
|
|
|
21
26
|
|
|
22
27
|
def builtin_workdir():
|
|
@@ -84,6 +89,16 @@ def save(patch):
|
|
|
84
89
|
cur["hooks_token"] = str(patch.get("hooks_token") or "").strip()[:128]
|
|
85
90
|
if "telemetry_errors" in patch:
|
|
86
91
|
cur["telemetry_errors"] = bool(patch.get("telemetry_errors"))
|
|
92
|
+
if "publish_daily_cap" in patch:
|
|
93
|
+
try:
|
|
94
|
+
cur["publish_daily_cap"] = max(1, min(50, int(patch.get("publish_daily_cap"))))
|
|
95
|
+
except (TypeError, ValueError):
|
|
96
|
+
return cur, "publish_daily_cap 必须是 1-50 的整数"
|
|
97
|
+
if "publish_fail_streak" in patch:
|
|
98
|
+
try:
|
|
99
|
+
cur["publish_fail_streak"] = max(1, min(10, int(patch.get("publish_fail_streak"))))
|
|
100
|
+
except (TypeError, ValueError):
|
|
101
|
+
return cur, "publish_fail_streak 必须是 1-10 的整数"
|
|
87
102
|
_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
88
103
|
tmp = _FILE.with_suffix(".tmp")
|
|
89
104
|
tmp.write_text(json.dumps(cur, ensure_ascii=False, indent=2), encoding="utf-8")
|
package/app/core/store.py
CHANGED
|
@@ -299,6 +299,26 @@ def set_book_meta(task_id, platform, entry):
|
|
|
299
299
|
return True
|
|
300
300
|
|
|
301
301
|
|
|
302
|
+
def set_auto_publish(task_id, ap):
|
|
303
|
+
"""写任务的定时发布配置(auto.py 每日到点读它触发批量发布)。
|
|
304
|
+
|
|
305
|
+
校验在 publish/auto.norm_auto_publish(路由层先归一再落这里);存 None
|
|
306
|
+
表示清除。bump_state 同 set_book_meta:前端卡片即时反映开关态。"""
|
|
307
|
+
if not _valid_id(task_id):
|
|
308
|
+
return False
|
|
309
|
+
with LOCK:
|
|
310
|
+
task = _TASKS.get(task_id)
|
|
311
|
+
if not task:
|
|
312
|
+
return False
|
|
313
|
+
if ap is None:
|
|
314
|
+
task.pop("auto_publish", None)
|
|
315
|
+
else:
|
|
316
|
+
task["auto_publish"] = ap
|
|
317
|
+
_save_json(paths.TASKS_DIR / (task_id + ".json"), task)
|
|
318
|
+
bump_state()
|
|
319
|
+
return True
|
|
320
|
+
|
|
321
|
+
|
|
302
322
|
def get_task(task_id):
|
|
303
323
|
if not _valid_id(task_id):
|
|
304
324
|
return None
|
|
@@ -586,9 +606,11 @@ def update_run(run_id, expected_status=None, **fields):
|
|
|
586
606
|
return None
|
|
587
607
|
run.update(fields)
|
|
588
608
|
_save_json(paths.RUNS_DIR / run_id / "run.json", run)
|
|
589
|
-
#
|
|
609
|
+
# 状态回填:起跑与结束都同步任务状态。只回填终态的话,run 在跑、
|
|
610
|
+
# 任务永远显示「排队中」(2026-09-18 实案:「# 重写·续」run 已 running
|
|
611
|
+
# 写了一小时,任务卡 queued,用户以为卡死连点重试)。
|
|
590
612
|
st = fields.get("status")
|
|
591
|
-
if st in ("done", "failed", "cancelled"):
|
|
613
|
+
if st in ("running", "done", "failed", "cancelled"):
|
|
592
614
|
tid = run.get("task_id")
|
|
593
615
|
task = _TASKS.get(tid) if tid else None
|
|
594
616
|
if task:
|
package/app/main.py
CHANGED
|
@@ -334,6 +334,23 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
334
334
|
if not task:
|
|
335
335
|
return self._json(404, {"error": "not found"})
|
|
336
336
|
return self._json(200, {"book_meta": task.get("book_meta") or {}})
|
|
337
|
+
if path == "/api/publish":
|
|
338
|
+
# 一键发布:平台连接状态 + 最近台账(详情页发布面板)
|
|
339
|
+
from core.publish import manager as pub
|
|
340
|
+
view = pub.view()
|
|
341
|
+
view["history"] = pub.history(limit=30)
|
|
342
|
+
return self._json(200, view)
|
|
343
|
+
m = re.match(r"^/api/publish/task/([^/]+)/history$", path)
|
|
344
|
+
if m:
|
|
345
|
+
from core.publish import manager as pub
|
|
346
|
+
from core.publish import ledger as pub_ledger
|
|
347
|
+
return self._json(200, {"history": pub.history(task_id=m.group(1), limit=50),
|
|
348
|
+
"books": pub_ledger.load_books().get(m.group(1)) or {}})
|
|
349
|
+
m = re.match(r"^/api/publish/task/([^/]+)/pending$", path)
|
|
350
|
+
if m:
|
|
351
|
+
# 自动发布视图:待发清单统计 + 护栏状态 + 批量发布进度
|
|
352
|
+
from core.publish import auto as pub_auto
|
|
353
|
+
return self._json(200, pub_auto.status(m.group(1)))
|
|
337
354
|
m = re.match(r"^/api/tasks/([^/]+)/git$", path)
|
|
338
355
|
if m:
|
|
339
356
|
# GIT 工作台全貌(详情页「版本」页签):仓库状态聚合 + 任务隔离态
|
|
@@ -467,6 +484,8 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
467
484
|
return deny
|
|
468
485
|
if path == "/api/tasks":
|
|
469
486
|
return self._api_create_task()
|
|
487
|
+
if path == "/api/tasks/clarify":
|
|
488
|
+
return self._api_task_clarify()
|
|
470
489
|
if path == "/api/hooks/run":
|
|
471
490
|
return self._api_hook_run()
|
|
472
491
|
if path == "/api/health/op":
|
|
@@ -561,6 +580,15 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
561
580
|
m = re.match(r"^/api/tasks/([^/]+)/book-meta$", path)
|
|
562
581
|
if m:
|
|
563
582
|
return self._api_book_meta_generate(m.group(1))
|
|
583
|
+
m = re.match(r"^/api/publish/(fanqie|qimao)/(connect|disconnect|probe)$", path)
|
|
584
|
+
if m:
|
|
585
|
+
return self._api_publish_platform_op(m.group(1), m.group(2))
|
|
586
|
+
m = re.match(r"^/api/publish/task/([^/]+)/(create-book|chapter|auto-publish)$", path)
|
|
587
|
+
if m:
|
|
588
|
+
return self._api_publish_task_op(m.group(1), m.group(2))
|
|
589
|
+
m = re.match(r"^/api/publish/task/([^/]+)/publish-all$", path)
|
|
590
|
+
if m:
|
|
591
|
+
return self._api_publish_auto(m.group(1))
|
|
564
592
|
m = re.match(r"^/api/tasks/([^/]+)/(git-merge|git-discard)$", path)
|
|
565
593
|
if m:
|
|
566
594
|
return self._api_git_verdict(m.group(1), m.group(2))
|
|
@@ -1003,6 +1031,89 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1003
1031
|
args=(task_id, platform, author)).start()
|
|
1004
1032
|
return self._json(200, {"ok": True, "started": True})
|
|
1005
1033
|
|
|
1034
|
+
def _api_publish_platform_op(self, platform, op):
|
|
1035
|
+
"""平台会话操作:connect 开浏览器等扫码 / disconnect 关 / probe 探测表单。"""
|
|
1036
|
+
from core.publish import manager as pub
|
|
1037
|
+
if op == "connect":
|
|
1038
|
+
ok, err = pub.connect(platform)
|
|
1039
|
+
elif op == "disconnect":
|
|
1040
|
+
ok, err = pub.disconnect(platform), ""
|
|
1041
|
+
else:
|
|
1042
|
+
ok, err = pub.probe_form_async(platform)
|
|
1043
|
+
if not ok:
|
|
1044
|
+
return self._json(400, {"error": err or "操作失败"})
|
|
1045
|
+
return self._json(200, {"ok": True})
|
|
1046
|
+
|
|
1047
|
+
def _api_publish_task_op(self, task_id, op):
|
|
1048
|
+
"""发布动作:create-book(按作品信息建书)/ chapter(发一章)。
|
|
1049
|
+
|
|
1050
|
+
章节文件必须在任务工作目录内(防穿越,同 /api/dir/file 口径);
|
|
1051
|
+
auto_submit=false 时流程填好表单即停,提交权留给用户人工确认。"""
|
|
1052
|
+
from pathlib import Path as _P
|
|
1053
|
+
from core import store
|
|
1054
|
+
from core.publish import manager as pub
|
|
1055
|
+
task = store.get_task(task_id)
|
|
1056
|
+
if not task:
|
|
1057
|
+
return self._json(404, {"error": "任务不存在"})
|
|
1058
|
+
body = self._body() or {}
|
|
1059
|
+
platform = (body.get("platform") or "").strip()
|
|
1060
|
+
if platform not in ("fanqie", "qimao"):
|
|
1061
|
+
return self._json(400, {"error": "platform 必须是 fanqie 或 qimao"})
|
|
1062
|
+
auto_submit = bool(body.get("auto_submit"))
|
|
1063
|
+
if op == "create-book":
|
|
1064
|
+
ok, err = pub.create_book_async(task_id, platform, auto_submit)
|
|
1065
|
+
elif op == "auto-publish":
|
|
1066
|
+
# 定时发布配置(P2.5):enabled=false 也落(保留 time 供再开);
|
|
1067
|
+
# 校验/归一在 auto.norm_auto_publish,语义见 auto.py 头注
|
|
1068
|
+
from core.publish import auto as pub_auto
|
|
1069
|
+
body["platform"] = platform
|
|
1070
|
+
if body.get("enabled"):
|
|
1071
|
+
ap, ap_err = pub_auto.norm_auto_publish(body)
|
|
1072
|
+
if not ap:
|
|
1073
|
+
return self._json(400, {"error": ap_err})
|
|
1074
|
+
else:
|
|
1075
|
+
ap, ap_err = pub_auto.norm_auto_publish(body)
|
|
1076
|
+
if not ap:
|
|
1077
|
+
return self._json(400, {"error": ap_err})
|
|
1078
|
+
ap["enabled"] = False
|
|
1079
|
+
ok, err = store.set_auto_publish(task_id, ap), ""
|
|
1080
|
+
if ok:
|
|
1081
|
+
return self._json(200, {"ok": True, "auto_publish": ap})
|
|
1082
|
+
else:
|
|
1083
|
+
f = str(body.get("file") or "").strip()
|
|
1084
|
+
if not f:
|
|
1085
|
+
return self._json(400, {"error": "file 必填(章节文件路径)"})
|
|
1086
|
+
wd = task.get("workdir") or ""
|
|
1087
|
+
try:
|
|
1088
|
+
fp = _P(f) if _P(f).is_absolute() else _P(wd) / f
|
|
1089
|
+
fp = fp.resolve()
|
|
1090
|
+
fp.relative_to(_P(wd).resolve())
|
|
1091
|
+
except (OSError, ValueError):
|
|
1092
|
+
return self._json(400, {"error": "章节文件必须在任务工作目录内"})
|
|
1093
|
+
ok, err = pub.upload_chapter_async(task_id, platform, str(fp), auto_submit)
|
|
1094
|
+
if not ok:
|
|
1095
|
+
return self._json(400, {"error": err or "操作失败"})
|
|
1096
|
+
return self._json(200, {"ok": True, "started": True})
|
|
1097
|
+
|
|
1098
|
+
def _api_publish_auto(self, task_id):
|
|
1099
|
+
"""批量发布全部待发章节(publish/auto.py)。
|
|
1100
|
+
|
|
1101
|
+
护栏(每日上限/连败退避/幂等/单飞)在 auto 层,每章发起前复查;
|
|
1102
|
+
auto_submit 默认 false——每章表单填好后停,提交权留给用户。"""
|
|
1103
|
+
from core import store
|
|
1104
|
+
from core.publish import auto as pub_auto
|
|
1105
|
+
if not store.get_task(task_id):
|
|
1106
|
+
return self._json(404, {"error": "任务不存在"})
|
|
1107
|
+
body = self._body() or {}
|
|
1108
|
+
platform = (body.get("platform") or "").strip()
|
|
1109
|
+
if platform not in ("fanqie", "qimao"):
|
|
1110
|
+
return self._json(400, {"error": "platform 必须是 fanqie 或 qimao"})
|
|
1111
|
+
ok, err = pub_auto.publish_pending_async(
|
|
1112
|
+
task_id, platform, auto_submit=bool(body.get("auto_submit")))
|
|
1113
|
+
if not ok:
|
|
1114
|
+
return self._json(400, {"error": err or "操作失败"})
|
|
1115
|
+
return self._json(200, {"ok": True, "started": True})
|
|
1116
|
+
|
|
1006
1117
|
def _api_task_side(self, task_id):
|
|
1007
1118
|
"""任务检查器(右缘停靠列)的轻量聚合端点。聚合逻辑在 store.task_side
|
|
1008
1119
|
(可单测、单实例状态);这里只做 404 转换。"""
|
|
@@ -1610,6 +1721,59 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1610
1721
|
status, resp = self._create_and_start(self._body())
|
|
1611
1722
|
return self._json(status, resp)
|
|
1612
1723
|
|
|
1724
|
+
def _api_task_clarify(self):
|
|
1725
|
+
"""需求拷问(借鉴 grill-me-skill):goal 过短/模糊时生成澄清问题。
|
|
1726
|
+
|
|
1727
|
+
POST /api/tasks/clarify,体 {goal, type, context?}。内置智能体单次调用
|
|
1728
|
+
产出 1-3 个问题(每题 2-4 个选项+可自由补充);失败/超时静默返回
|
|
1729
|
+
{questions: []}——采访态是增强不是闸门,绝不挡创建。"""
|
|
1730
|
+
body = self._body()
|
|
1731
|
+
goal = str(body.get("goal") or "").strip()
|
|
1732
|
+
ttype = str(body.get("type") or "direct").strip()[:40]
|
|
1733
|
+
context = str(body.get("context") or "").strip()[:2000]
|
|
1734
|
+
# 短路:goal 够具体(≥12 字符)或带背景就不打扰
|
|
1735
|
+
if len(goal) >= 12 or context:
|
|
1736
|
+
return self._json(200, {"questions": []})
|
|
1737
|
+
try:
|
|
1738
|
+
from core import builtin_agent
|
|
1739
|
+
bi = builtin_agent.resolve()
|
|
1740
|
+
except Exception:
|
|
1741
|
+
return self._json(200, {"questions": []})
|
|
1742
|
+
prompt = ("用户想用「%s」任务让 AI 做这件事:%s\n"
|
|
1743
|
+
"这件事的描述比较模糊。请提出最多 3 个最关键的澄清问题(能自答的不要问),"
|
|
1744
|
+
"每个问题给出 2-4 个最常见的选项。只输出 JSON 数组,不要输出其他内容:\n"
|
|
1745
|
+
'[{"q": "问题", "options": ["选项1", "选项2"]}]' % (ttype, goal[:200]))
|
|
1746
|
+
try:
|
|
1747
|
+
res = builtin_agent.run(bi, prompt, os.getcwd() if hasattr(os, "getcwd") else ".",
|
|
1748
|
+
timeout=60)
|
|
1749
|
+
import json as _json
|
|
1750
|
+
arr = None
|
|
1751
|
+
text = (res.get("text") or "").strip()
|
|
1752
|
+
try:
|
|
1753
|
+
arr = _json.loads(text)
|
|
1754
|
+
except Exception:
|
|
1755
|
+
import re as _re
|
|
1756
|
+
m = _re.search(r"\[[\s\S]*\]", text)
|
|
1757
|
+
if m:
|
|
1758
|
+
try:
|
|
1759
|
+
arr = _json.loads(m.group(0))
|
|
1760
|
+
except Exception:
|
|
1761
|
+
arr = None
|
|
1762
|
+
if not isinstance(arr, list):
|
|
1763
|
+
return self._json(200, {"questions": []})
|
|
1764
|
+
questions = []
|
|
1765
|
+
for it in arr[:3]:
|
|
1766
|
+
if not isinstance(it, dict):
|
|
1767
|
+
continue
|
|
1768
|
+
q = str(it.get("q") or "").strip()[:200]
|
|
1769
|
+
opts = [str(o).strip()[:80] for o in (it.get("options") or [])
|
|
1770
|
+
if str(o).strip()][:4]
|
|
1771
|
+
if q and len(opts) >= 2:
|
|
1772
|
+
questions.append({"q": q, "options": opts})
|
|
1773
|
+
return self._json(200, {"questions": questions})
|
|
1774
|
+
except Exception:
|
|
1775
|
+
return self._json(200, {"questions": []})
|
|
1776
|
+
|
|
1613
1777
|
def _api_hook_run(self):
|
|
1614
1778
|
"""外部触发开任务(webhook,借鉴 emdash/mission-control 的外部集成面)。
|
|
1615
1779
|
|
|
@@ -1707,6 +1871,9 @@ def _state_payload(client_id="", ver=None):
|
|
|
1707
1871
|
"control": remote.control_view(client_id),
|
|
1708
1872
|
# 供应商健康/告警(顶栏横幅数据源;有告警时 bump_state 会推给所有端)
|
|
1709
1873
|
"health": health.snapshot(),
|
|
1874
|
+
# 任务队列观测(worker 池目标/存活 + 队列深度):排队问题排障一眼定位
|
|
1875
|
+
# 是「并发满载在等」还是「job 蒸发没人管」(后者由看门狗 2 分钟自愈)
|
|
1876
|
+
"jobs": jobs.workers_info(),
|
|
1710
1877
|
}
|
|
1711
1878
|
|
|
1712
1879
|
|
|
@@ -1817,6 +1984,13 @@ def main():
|
|
|
1817
1984
|
n_bo = bookmeta.recover_orphans() # 作品信息生成线程同样会被重启杀掉,遗留 running 收尸
|
|
1818
1985
|
if n_bo:
|
|
1819
1986
|
print("[CodeBee] 崩溃恢复:%d 条作品信息生成中断标记为 failed(可点重试)" % n_bo)
|
|
1987
|
+
try:
|
|
1988
|
+
from core.publish import manager as publish_mgr
|
|
1989
|
+
n_pb = publish_mgr.recover_orphans() # 发布线程同款收尸:waiting_login/busy 改判 error
|
|
1990
|
+
if n_pb:
|
|
1991
|
+
print("[CodeBee] 崩溃恢复:%d 条平台发布中断标记为可重试" % n_pb)
|
|
1992
|
+
except Exception:
|
|
1993
|
+
pass
|
|
1820
1994
|
try:
|
|
1821
1995
|
from core import settings_schema
|
|
1822
1996
|
settings_schema.register_default_namespaces() # budget/cascade/compaction 配置就绪(幂等)
|