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/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 配固定域名,或 start-public.bat)")
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
- "winget install Cloudflare.cloudflared 后重启即可获得公网地址)")
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