codebee 0.1.14 → 0.1.16
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 +22 -8
- package/app/core/automation.py +7 -0
- package/app/core/bookmeta.py +10 -3
- package/app/core/bookmeta_catalog.py +16 -0
- package/app/core/catalog.py +9 -1
- package/app/core/covergen.py +176 -56
- package/app/core/env_scrub.py +5 -3
- package/app/core/jobs.py +33 -11
- package/app/core/modelhub.py +27 -7
- package/app/core/notify.py +11 -1
- package/app/core/paihang.py +66 -22
- package/app/core/pipeline.py +77 -4
- package/app/core/publish/browser.py +62 -5
- package/app/core/publish/fanqie.py +32 -2
- package/app/core/publish/flow.py +48 -2
- package/app/core/publish/flows-fanqie-calibrated.json +324 -0
- package/app/core/publish/manager.py +25 -9
- package/app/core/publish/qimao.py +18 -3
- package/app/core/runner.py +19 -4
- package/app/core/settings.py +3 -1
- package/app/core/share_page.py +111 -0
- package/app/core/telemetry.py +3 -2
- package/app/core/tlsctx.py +16 -0
- package/app/core/zentao.py +1302 -0
- package/app/main.py +87 -4
- package/app/ui/app.js +525 -25
- package/app/ui/i18n.js +124 -1
- package/app/ui/index.html +53 -26
- package/app/ui/style.css +133 -24
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -304,6 +304,28 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
304
304
|
return self._send(200, "(报告尚未生成)", "text/markdown; charset=utf-8")
|
|
305
305
|
return self._send(200, p.read_text(encoding="utf-8", errors="replace"),
|
|
306
306
|
"text/markdown; charset=utf-8")
|
|
307
|
+
m = re.match(r"^/api/runs/([^/]+)/cover$", path)
|
|
308
|
+
if m:
|
|
309
|
+
# 封面图在运行目录而非任务工作目录,「成果」扫描够不着它,
|
|
310
|
+
# 前端封面卡内嵌缩略图走这条专用通道(文件名固定无穿越面)
|
|
311
|
+
run = store.get_run(m.group(1))
|
|
312
|
+
if not run:
|
|
313
|
+
return self._json(404, {"error": "not found"})
|
|
314
|
+
p = paths.RUNS_DIR / m.group(1) / "cover.png"
|
|
315
|
+
if not p.is_file():
|
|
316
|
+
return self._json(404, {"error": "封面尚未生成"})
|
|
317
|
+
return self._send(200, p.read_bytes(), "image/png")
|
|
318
|
+
m = re.match(r"^/api/runs/([^/]+)/share$", path)
|
|
319
|
+
if m:
|
|
320
|
+
from core import share_page
|
|
321
|
+
run = store.get_run(m.group(1))
|
|
322
|
+
if not run:
|
|
323
|
+
return self._json(404, {"error": "not found"})
|
|
324
|
+
task = store.get_task(run.get("task_id") or "") if run.get("task_id") else None
|
|
325
|
+
rp = paths.RUNS_DIR / m.group(1) / "report.md"
|
|
326
|
+
report = rp.read_text(encoding="utf-8", errors="replace") if rp.is_file() else ""
|
|
327
|
+
html = share_page.render_share_html(run, task, report)
|
|
328
|
+
return self._send(200, html, "text/html; charset=utf-8")
|
|
307
329
|
m = re.match(r"^/api/runs/([^/]+)/files$", path)
|
|
308
330
|
if m:
|
|
309
331
|
run = store.get_run(m.group(1))
|
|
@@ -437,6 +459,9 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
437
459
|
if path == "/api/automation":
|
|
438
460
|
return self._json(200, {"tasks": automation.list_tasks(),
|
|
439
461
|
"templates": automation.templates()})
|
|
462
|
+
if path == "/api/zentao":
|
|
463
|
+
from core import zentao
|
|
464
|
+
return self._json(200, zentao.view())
|
|
440
465
|
m = re.match(r"^/api/automation/([^/]+)$", path)
|
|
441
466
|
if m:
|
|
442
467
|
t = automation.get_task(m.group(1))
|
|
@@ -724,6 +749,10 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
724
749
|
if path == "/api/models/refresh":
|
|
725
750
|
from core import modelhub
|
|
726
751
|
n, err = modelhub.refresh_models(self._body().get("id") or "")
|
|
752
|
+
if modelhub.is_no_list_note(err):
|
|
753
|
+
# 网关无 /models 列表接口(zcode-plan 等):预期情况按成功回报,
|
|
754
|
+
# 前端给中性提示而非红色「获取失败」
|
|
755
|
+
return self._json(200, {"ok": True, "count": 0, "note": err})
|
|
727
756
|
return self._json(200, {"ok": bool(n), "count": n, "message": err})
|
|
728
757
|
if path == "/api/models/add":
|
|
729
758
|
# 手工添加模型:厂商列表接口调不通时直接填模型名进列表
|
|
@@ -955,6 +984,30 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
955
984
|
return self._json(200, {"ok": True, "task": t, "run_id": run_id or ""})
|
|
956
985
|
ok = automation.delete(tid)
|
|
957
986
|
return self._json(200, {"ok": True}) if ok else self._json(404, {"error": "not found"})
|
|
987
|
+
if path == "/api/zentao/config":
|
|
988
|
+
from core import zentao
|
|
989
|
+
try:
|
|
990
|
+
cfg = zentao.save_config(self._body())
|
|
991
|
+
except ValueError as e:
|
|
992
|
+
return self._json(400, {"error": str(e)})
|
|
993
|
+
return self._json(200, {"ok": True, "config": cfg})
|
|
994
|
+
if path == "/api/zentao/test":
|
|
995
|
+
from core import zentao
|
|
996
|
+
body = self._body()
|
|
997
|
+
pw = str(body.get("password") or "").strip()
|
|
998
|
+
ok, msg = zentao.test_connection(
|
|
999
|
+
base_url=body.get("base_url"), account=body.get("account"),
|
|
1000
|
+
password=pw or None) # 空 = 用已保存密码
|
|
1001
|
+
return self._json(200, {"ok": ok, "message": msg})
|
|
1002
|
+
if path == "/api/zentao/scan":
|
|
1003
|
+
from core import zentao
|
|
1004
|
+
res = zentao.scan_now() # 网络操作同步做(ThreadingHTTPServer 不堵别的请求)
|
|
1005
|
+
return self._json(200, dict(res, ok=bool(res.get("ok"))))
|
|
1006
|
+
if path == "/api/zentao/modules":
|
|
1007
|
+
from core import zentao
|
|
1008
|
+
pid = (self._body() or {}).get("product")
|
|
1009
|
+
res = zentao.fetch_modules(pid)
|
|
1010
|
+
return self._json(200, dict(res, ok=bool(res.get("ok"))))
|
|
958
1011
|
# 外部目录路由必须在通配的 market/<id>/(install|remove) 之前——
|
|
959
1012
|
# 否则 "remote" 会被当成包名吞掉
|
|
960
1013
|
if path == "/api/market/remote/refresh":
|
|
@@ -1949,6 +2002,13 @@ def main():
|
|
|
1949
2002
|
def _step(msg):
|
|
1950
2003
|
print("[CodeBee] %s (%.1fs)" % (msg, time.time() - _t0), flush=True)
|
|
1951
2004
|
|
|
2005
|
+
_ver = ""
|
|
2006
|
+
try:
|
|
2007
|
+
_pj = Path(__file__).resolve().parents[1] / "package.json"
|
|
2008
|
+
_ver = str(json.loads(_pj.read_text(encoding="utf-8")).get("version") or "")
|
|
2009
|
+
except Exception:
|
|
2010
|
+
pass
|
|
2011
|
+
print("[CodeBee] v%s" % (_ver or "?"), flush=True)
|
|
1952
2012
|
_step("正在准备数据目录…")
|
|
1953
2013
|
paths.ensure_dirs()
|
|
1954
2014
|
from core import attachments
|
|
@@ -2040,6 +2100,13 @@ def main():
|
|
|
2040
2100
|
n_auto = automation.start() # 自动化:加载定时任务并拉起调度线程(错过的一次性任务不补跑)
|
|
2041
2101
|
if n_auto:
|
|
2042
2102
|
print("[CodeBee] 自动化:%d 个定时任务已加载" % n_auto)
|
|
2103
|
+
try:
|
|
2104
|
+
from core import zentao
|
|
2105
|
+
n_zt = zentao.start() # 禅道集成:加载扫描配置与认领台账(调度挂在 automation tick)
|
|
2106
|
+
if n_zt:
|
|
2107
|
+
print("[CodeBee] 禅道集成:%d 条认领记录已加载" % n_zt)
|
|
2108
|
+
except Exception:
|
|
2109
|
+
pass
|
|
2043
2110
|
tok = remote.token()
|
|
2044
2111
|
import atexit
|
|
2045
2112
|
import os as _os
|
|
@@ -2056,7 +2123,8 @@ def main():
|
|
|
2056
2123
|
except OSError as e:
|
|
2057
2124
|
# Windows 的 SO_REUSEADDR 允许两个进程同时 LISTEN 同一端口(请求随机
|
|
2058
2125
|
# 分发,表现为"时好时坏");加独占锁后双起在这里干净失败并指路。
|
|
2059
|
-
import os
|
|
2126
|
+
# (此处不能局部 import os:会让 os 变 main() 的局部名,后面 2127 行
|
|
2127
|
+
# 的 os.name 直接 UnboundLocalError——顶部已有全局导入,直接用)
|
|
2060
2128
|
hint = ""
|
|
2061
2129
|
if os.name == "nt":
|
|
2062
2130
|
hint = ("(Windows 排查:netstat -ano | findstr :%d 找到 PID,"
|
|
@@ -2084,14 +2152,17 @@ def main():
|
|
|
2084
2152
|
% (remote.PUBLIC_URL, tok))
|
|
2085
2153
|
elif not args.no_public_tunnel and args.host != "127.0.0.1":
|
|
2086
2154
|
if remote.has_local_creds():
|
|
2155
|
+
extra = ",或 start-public.bat" if os.name == "nt" else ""
|
|
2087
2156
|
print("[CodeBee] (检测到已有 Cloudflare 隧道凭据:临时隧道在此类机器上不可用,"
|
|
2088
|
-
"请用 --public-url
|
|
2157
|
+
"请用 --public-url 配固定域名%s)" % extra)
|
|
2089
2158
|
elif remote.start_quick_tunnel(args.port, _announce_public):
|
|
2090
2159
|
print("[CodeBee] 正在建立 Cloudflare 快速隧道(公网地址几秒后打印;"
|
|
2091
2160
|
"若打不开说明当前网络不支持,可改用固定域名或 Tailscale)…")
|
|
2092
2161
|
else:
|
|
2162
|
+
cf = ("winget install Cloudflare.cloudflared" if os.name == "nt"
|
|
2163
|
+
else "brew install cloudflared")
|
|
2093
2164
|
print("[CodeBee] (未检测到 cloudflared,跳过公网隧道;"
|
|
2094
|
-
"
|
|
2165
|
+
"%s 后重启即可获得公网地址)" % cf)
|
|
2095
2166
|
atexit.register(remote.stop_quick_tunnel)
|
|
2096
2167
|
if args.host != "127.0.0.1":
|
|
2097
2168
|
lan = remote.lan_ip()
|
|
@@ -2105,9 +2176,21 @@ def main():
|
|
|
2105
2176
|
print("[CodeBee] 远程访问受令牌保护;手机打开一次带 token 的地址后会记住。")
|
|
2106
2177
|
if not args.no_browser:
|
|
2107
2178
|
threading.Timer(1.0, lambda: webbrowser.open("http://127.0.0.1:%d" % args.port)).start()
|
|
2179
|
+
if os.name != "nt":
|
|
2180
|
+
# POSIX 的 SIGTERM(kill <pid>)/SIGHUP(关终端窗口)默认动作是立即
|
|
2181
|
+
# 终止进程:atexit 不跑,快速隧道的 cloudflared 子进程会变孤儿。转成
|
|
2182
|
+
# SystemExit 走正常退出路径;Ctrl+C 的 KeyboardInterrupt 本就触发
|
|
2183
|
+
# atexit,无需登记。Windows 语义不同,不碰。
|
|
2184
|
+
import signal as _signal
|
|
2185
|
+
|
|
2186
|
+
def _graceful_exit(signum, frame):
|
|
2187
|
+
raise SystemExit(0)
|
|
2188
|
+
|
|
2189
|
+
_signal.signal(_signal.SIGTERM, _graceful_exit)
|
|
2190
|
+
_signal.signal(_signal.SIGHUP, _graceful_exit)
|
|
2108
2191
|
try:
|
|
2109
2192
|
httpd.serve_forever()
|
|
2110
|
-
except KeyboardInterrupt:
|
|
2193
|
+
except (KeyboardInterrupt, SystemExit):
|
|
2111
2194
|
print("\n[CodeBee] 已退出")
|
|
2112
2195
|
|
|
2113
2196
|
|