codebee 0.1.13 → 0.1.15

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
@@ -235,6 +235,12 @@ class Handler(BaseHTTPRequestHandler):
235
235
  return self._json(200, skills.view())
236
236
  if path == "/api/settings":
237
237
  return self._json(200, dict(settings.load(), **jobs.workers_info()))
238
+ if path == "/api/settings-v2":
239
+ # schema 化设置全貌(secret 已脱敏;前端调参卡直读)
240
+ from core import settings_schema as ss2
241
+ ss2.register_default_namespaces()
242
+ return self._json(200, {"namespaces": {
243
+ ns: ss2.describe(ns) for ns in ss2.names()}})
238
244
  if path == "/api/selfupdate":
239
245
  from core import selfupdate
240
246
  return self._json(200, selfupdate.check(
@@ -298,6 +304,17 @@ class Handler(BaseHTTPRequestHandler):
298
304
  return self._send(200, "(报告尚未生成)", "text/markdown; charset=utf-8")
299
305
  return self._send(200, p.read_text(encoding="utf-8", errors="replace"),
300
306
  "text/markdown; charset=utf-8")
307
+ m = re.match(r"^/api/runs/([^/]+)/share$", path)
308
+ if m:
309
+ from core import share_page
310
+ run = store.get_run(m.group(1))
311
+ if not run:
312
+ return self._json(404, {"error": "not found"})
313
+ task = store.get_task(run.get("task_id") or "") if run.get("task_id") else None
314
+ rp = paths.RUNS_DIR / m.group(1) / "report.md"
315
+ report = rp.read_text(encoding="utf-8", errors="replace") if rp.is_file() else ""
316
+ html = share_page.render_share_html(run, task, report)
317
+ return self._send(200, html, "text/html; charset=utf-8")
301
318
  m = re.match(r"^/api/runs/([^/]+)/files$", path)
302
319
  if m:
303
320
  run = store.get_run(m.group(1))
@@ -431,6 +448,9 @@ class Handler(BaseHTTPRequestHandler):
431
448
  if path == "/api/automation":
432
449
  return self._json(200, {"tasks": automation.list_tasks(),
433
450
  "templates": automation.templates()})
451
+ if path == "/api/zentao":
452
+ from core import zentao
453
+ return self._json(200, zentao.view())
434
454
  m = re.match(r"^/api/automation/([^/]+)$", path)
435
455
  if m:
436
456
  t = automation.get_task(m.group(1))
@@ -814,6 +834,22 @@ class Handler(BaseHTTPRequestHandler):
814
834
  return self._json(400, {"error": err, "settings": view})
815
835
  n = jobs.configure(view["max_concurrent_jobs"])
816
836
  return self._json(200, {"ok": True, "settings": view, "workers": n})
837
+ m = re.match(r"^/api/settings-v2/([a-z_-]+)$", path)
838
+ if m:
839
+ # schema 化设置写入口:{ops:[{op:"set",path,value}], expected_revision?}
840
+ # 带 expected_revision 做 CAS,冲突 409(前端据此重拉重试)
841
+ from core import settings_schema as ss2
842
+ ns = m.group(1)
843
+ body = self._body() or {}
844
+ try:
845
+ rev = ss2.mutate(ns, body.get("ops") or [],
846
+ expected_revision=body.get("expected_revision"))
847
+ except ss2.SettingsConflictError as e:
848
+ return self._json(409, {"error": str(e), "revision": ss2.revision(ns)})
849
+ except ValueError as e:
850
+ return self._json(400, {"error": str(e)})
851
+ return self._json(200, {"ok": True, "revision": rev,
852
+ "values": ss2.describe(ns)["values"]})
817
853
  if path == "/api/settings/default-workdir":
818
854
  body = self._body()
819
855
  old = settings.default_workdir()
@@ -933,6 +969,25 @@ class Handler(BaseHTTPRequestHandler):
933
969
  return self._json(200, {"ok": True, "task": t, "run_id": run_id or ""})
934
970
  ok = automation.delete(tid)
935
971
  return self._json(200, {"ok": True}) if ok else self._json(404, {"error": "not found"})
972
+ if path == "/api/zentao/config":
973
+ from core import zentao
974
+ try:
975
+ cfg = zentao.save_config(self._body())
976
+ except ValueError as e:
977
+ return self._json(400, {"error": str(e)})
978
+ return self._json(200, {"ok": True, "config": cfg})
979
+ if path == "/api/zentao/test":
980
+ from core import zentao
981
+ body = self._body()
982
+ pw = str(body.get("password") or "").strip()
983
+ ok, msg = zentao.test_connection(
984
+ base_url=body.get("base_url"), account=body.get("account"),
985
+ password=pw or None) # 空 = 用已保存密码
986
+ return self._json(200, {"ok": ok, "message": msg})
987
+ if path == "/api/zentao/scan":
988
+ from core import zentao
989
+ res = zentao.scan_now() # 网络操作同步做(ThreadingHTTPServer 不堵别的请求)
990
+ return self._json(200, dict(res, ok=bool(res.get("ok"))))
936
991
  # 外部目录路由必须在通配的 market/<id>/(install|remove) 之前——
937
992
  # 否则 "remote" 会被当成包名吞掉
938
993
  if path == "/api/market/remote/refresh":
@@ -1927,6 +1982,13 @@ def main():
1927
1982
  def _step(msg):
1928
1983
  print("[CodeBee] %s (%.1fs)" % (msg, time.time() - _t0), flush=True)
1929
1984
 
1985
+ _ver = ""
1986
+ try:
1987
+ _pj = Path(__file__).resolve().parents[1] / "package.json"
1988
+ _ver = str(json.loads(_pj.read_text(encoding="utf-8")).get("version") or "")
1989
+ except Exception:
1990
+ pass
1991
+ print("[CodeBee] v%s" % (_ver or "?"), flush=True)
1930
1992
  _step("正在准备数据目录…")
1931
1993
  paths.ensure_dirs()
1932
1994
  from core import attachments
@@ -2018,6 +2080,13 @@ def main():
2018
2080
  n_auto = automation.start() # 自动化:加载定时任务并拉起调度线程(错过的一次性任务不补跑)
2019
2081
  if n_auto:
2020
2082
  print("[CodeBee] 自动化:%d 个定时任务已加载" % n_auto)
2083
+ try:
2084
+ from core import zentao
2085
+ n_zt = zentao.start() # 禅道集成:加载扫描配置与认领台账(调度挂在 automation tick)
2086
+ if n_zt:
2087
+ print("[CodeBee] 禅道集成:%d 条认领记录已加载" % n_zt)
2088
+ except Exception:
2089
+ pass
2021
2090
  tok = remote.token()
2022
2091
  import atexit
2023
2092
  import os as _os
@@ -2034,7 +2103,8 @@ def main():
2034
2103
  except OSError as e:
2035
2104
  # Windows 的 SO_REUSEADDR 允许两个进程同时 LISTEN 同一端口(请求随机
2036
2105
  # 分发,表现为"时好时坏");加独占锁后双起在这里干净失败并指路。
2037
- import os
2106
+ # (此处不能局部 import os:会让 os 变 main() 的局部名,后面 2127 行
2107
+ # 的 os.name 直接 UnboundLocalError——顶部已有全局导入,直接用)
2038
2108
  hint = ""
2039
2109
  if os.name == "nt":
2040
2110
  hint = ("(Windows 排查:netstat -ano | findstr :%d 找到 PID,"
@@ -2062,14 +2132,17 @@ def main():
2062
2132
  % (remote.PUBLIC_URL, tok))
2063
2133
  elif not args.no_public_tunnel and args.host != "127.0.0.1":
2064
2134
  if remote.has_local_creds():
2135
+ extra = ",或 start-public.bat" if os.name == "nt" else ""
2065
2136
  print("[CodeBee] (检测到已有 Cloudflare 隧道凭据:临时隧道在此类机器上不可用,"
2066
- "请用 --public-url 配固定域名,或 start-public.bat)")
2137
+ "请用 --public-url 配固定域名%s)" % extra)
2067
2138
  elif remote.start_quick_tunnel(args.port, _announce_public):
2068
2139
  print("[CodeBee] 正在建立 Cloudflare 快速隧道(公网地址几秒后打印;"
2069
2140
  "若打不开说明当前网络不支持,可改用固定域名或 Tailscale)…")
2070
2141
  else:
2142
+ cf = ("winget install Cloudflare.cloudflared" if os.name == "nt"
2143
+ else "brew install cloudflared")
2071
2144
  print("[CodeBee] (未检测到 cloudflared,跳过公网隧道;"
2072
- "winget install Cloudflare.cloudflared 后重启即可获得公网地址)")
2145
+ "%s 后重启即可获得公网地址)" % cf)
2073
2146
  atexit.register(remote.stop_quick_tunnel)
2074
2147
  if args.host != "127.0.0.1":
2075
2148
  lan = remote.lan_ip()
@@ -2083,9 +2156,21 @@ def main():
2083
2156
  print("[CodeBee] 远程访问受令牌保护;手机打开一次带 token 的地址后会记住。")
2084
2157
  if not args.no_browser:
2085
2158
  threading.Timer(1.0, lambda: webbrowser.open("http://127.0.0.1:%d" % args.port)).start()
2159
+ if os.name != "nt":
2160
+ # POSIX 的 SIGTERM(kill <pid>)/SIGHUP(关终端窗口)默认动作是立即
2161
+ # 终止进程:atexit 不跑,快速隧道的 cloudflared 子进程会变孤儿。转成
2162
+ # SystemExit 走正常退出路径;Ctrl+C 的 KeyboardInterrupt 本就触发
2163
+ # atexit,无需登记。Windows 语义不同,不碰。
2164
+ import signal as _signal
2165
+
2166
+ def _graceful_exit(signum, frame):
2167
+ raise SystemExit(0)
2168
+
2169
+ _signal.signal(_signal.SIGTERM, _graceful_exit)
2170
+ _signal.signal(_signal.SIGHUP, _graceful_exit)
2086
2171
  try:
2087
2172
  httpd.serve_forever()
2088
- except KeyboardInterrupt:
2173
+ except (KeyboardInterrupt, SystemExit):
2089
2174
  print("\n[CodeBee] 已退出")
2090
2175
 
2091
2176
 
package/app/ui/app.js CHANGED
@@ -26,6 +26,7 @@ function flowIconHtml(f) {
26
26
  }
27
27
 
28
28
  function flowDesc(f) {
29
+ if (f.id === "rank_scan") return t("抓七猫榜 → AI 选题洞察");
29
30
  if (f.engine === "direct") return t("单智能体直达(快)");
30
31
  if (f.engine === "code") return t("实现 → 验证 → 评审");
31
32
  // 连载与单稿件同引擎,描述必须区分:连载强调逐章与断点续跑
@@ -3573,6 +3574,7 @@ async function renderRunDetail() {
3573
3574
  $("btn-cancel").classList.toggle("hidden", !active);
3574
3575
  S.cancelTargetRunId = active ? run.id : null;
3575
3576
  $("btn-delete").classList.toggle("hidden", active);
3577
+ $("btn-share").classList.toggle("hidden", active); // 分享页:结束后可生成自包含 HTML
3576
3578
  $("btn-retry").classList.toggle("hidden", !(run.task_id && (run.status === "failed" || run.status === "cancelled")));
3577
3579
  $("btn-talk").classList.toggle("hidden", !(run.task_id && !chatEngineIsDirect(run)));
3578
3580
  const rcTask = ((S.state || {}).tasks || []).find((x) => x.id === run.task_id);
@@ -7680,6 +7682,136 @@ function autoTemplate(tplId) {
7680
7682
  if (tp) autoForm(null, tp);
7681
7683
  }
7682
7684
 
7685
+ /* ---------------------------------------------------------- 禅道 Bug 自动修复 */
7686
+ /* /api/zentao → {config(脱敏), claims, last_scan, next_scan, last_error}。
7687
+ * 进页拉一次回填表单;保存/测试/扫描各自 POST 后重拉。 */
7688
+
7689
+ async function loadZentao() {
7690
+ let v = null;
7691
+ try { v = await api("/api/zentao"); } catch (e) { v = null; }
7692
+ S.zentao = v;
7693
+ const cfg = (v && v.config) || {};
7694
+ const set = (id, val) => { const el = $(id); if (el) el.value = val == null ? "" : val; };
7695
+ set("zt-base-url", cfg.base_url);
7696
+ set("zt-account", cfg.account);
7697
+ const pw = $("zt-password");
7698
+ if (pw) { pw.value = ""; pw.placeholder = cfg.has_password ? t("已保存(不改就留空)") : ""; }
7699
+ set("zt-products", (cfg.products || []).join(","));
7700
+ set("zt-assigned", cfg.assigned_to);
7701
+ const sev = $("zt-sev"); if (sev) sev.value = String(cfg.severity_cap || 0);
7702
+ set("zt-workdir", cfg.workdir);
7703
+ set("zt-gitrev", cfg.git_rev);
7704
+ set("zt-verify", cfg.verify_command);
7705
+ const tg = (id, on) => { const el = $(id); if (el) el.checked = !!on; };
7706
+ tg("zt-autoresolve", cfg.auto_resolve);
7707
+ tg("zt-automerge", cfg.auto_merge);
7708
+ tg("zt-poll", cfg.poll_enabled);
7709
+ set("zt-interval", cfg.interval_hours || 2);
7710
+ renderZentaoStatus();
7711
+ renderZentaoClaims();
7712
+ }
7713
+
7714
+ function zentaoStateTag(st) {
7715
+ const MAP = {
7716
+ fixing: ["修复中", "zt-tag-fix"],
7717
+ resolved: ["已解决", "zt-tag-ok"],
7718
+ merge_failed: ["合并失败", "zt-tag-warn"],
7719
+ commented: ["已评论留人工", "zt-tag-warn"],
7720
+ resolve_failed: ["回写失败", "zt-tag-warn"],
7721
+ done_manual: ["待人工确认", "zt-tag-warn"],
7722
+ lost: ["记录丢失", "zt-tag-err"],
7723
+ };
7724
+ const m = MAP[st] || [st || "?", ""];
7725
+ return '<span class="tag ' + m[1] + '">' + t(m[0]) + "</span>";
7726
+ }
7727
+
7728
+ function renderZentaoStatus() {
7729
+ const box = $("zentao-status");
7730
+ if (!box) return;
7731
+ const v = S.zentao;
7732
+ if (!v) { box.innerHTML = t("加载失败:服务未连接"); return; }
7733
+ const cfg = v.config || {};
7734
+ const parts = [];
7735
+ parts.push(cfg.poll_enabled
7736
+ ? (t("定时扫描已开启,每 ") + (cfg.interval_hours || 2) + t(" 小时一次")
7737
+ + (v.next_scan ? t(",下次 ") + esc(v.next_scan) : ""))
7738
+ : t("定时扫描未开启(仍可手动「立即扫描」)"));
7739
+ if (v.last_scan) parts.push(t("上次扫描 ") + esc(v.last_scan));
7740
+ box.innerHTML = parts.map(esc).join(" · ") +
7741
+ (v.last_error ? '<span class="zt-err">' + t("最近错误:") + esc(v.last_error) + "</span>" : "");
7742
+ }
7743
+
7744
+ function renderZentaoClaims() {
7745
+ const box = $("zentao-claims");
7746
+ if (!box) return;
7747
+ const claims = (S.zentao && S.zentao.claims) || [];
7748
+ box.innerHTML = claims.map((c) =>
7749
+ '<div class="card"><div class="head"><span class="name">#' + esc(c.bug_id) + " " + esc(c.title || "") + "</span>" +
7750
+ zentaoStateTag(c.state) + "</div>" +
7751
+ '<div class="auto-meta">' +
7752
+ "<span>" + t("修复任务:") + '<a href="#" onclick="zentaoOpenTask(\'' + esc(c.task_id || "") + '\');return false;">' + esc(c.task_id || "?") + "</a></span>" +
7753
+ (c.claimed_at ? "<span>" + t("认领于 ") + esc(c.claimed_at) + "</span>" : "") +
7754
+ "</div>" +
7755
+ (c.note ? '<div class="note">' + esc(c.note) + "</div>" : "") +
7756
+ "</div>"
7757
+ ).join("") || '<div class="empty">' + t("还没有认领过 Bug——配置好连接与产品 ID 后点「立即扫描」。") + "</div>";
7758
+ }
7759
+
7760
+ function zentaoOpenTask(taskId) {
7761
+ const c = ((S.zentao && S.zentao.claims) || []).find((x) => x.task_id === taskId);
7762
+ if (c && c.run_id) { openRun(c.run_id); return; }
7763
+ switchTab("tasks");
7764
+ }
7765
+
7766
+ /* 表单 → 保存配置。password 只在用户输入了才提交(空 = 保持已存密码)。 */
7767
+ async function saveZentao() {
7768
+ const val = (id) => (($(id).value || "").trim());
7769
+ const products = val("zt-products").split(/[,,\s]+/).filter(Boolean).map(Number);
7770
+ const payload = {
7771
+ base_url: val("zt-base-url"),
7772
+ account: val("zt-account"),
7773
+ products,
7774
+ assigned_to: val("zt-assigned"),
7775
+ severity_cap: parseInt($("zt-sev").value, 10) || 0,
7776
+ workdir: val("zt-workdir"),
7777
+ git_rev: val("zt-gitrev"),
7778
+ verify_command: val("zt-verify"),
7779
+ auto_resolve: $("zt-autoresolve").checked,
7780
+ auto_merge: $("zt-automerge").checked,
7781
+ poll_enabled: $("zt-poll").checked,
7782
+ interval_hours: parseInt($("zt-interval").value, 10) || 2,
7783
+ };
7784
+ const pw = ($("zt-password").value || "").trim();
7785
+ if (pw) payload.password = pw;
7786
+ try {
7787
+ await api("/api/zentao/config", { method: "POST", body: JSON.stringify(payload) });
7788
+ } catch (e) { toast(t("保存失败:") + e.message, true); return; }
7789
+ toast(t("禅道配置已保存"));
7790
+ loadZentao();
7791
+ }
7792
+
7793
+ async function testZentao() {
7794
+ const val = (id) => (($(id).value || "").trim());
7795
+ const body = { base_url: val("zt-base-url"), account: val("zt-account") };
7796
+ const pw = ($("zt-password").value || "").trim();
7797
+ if (pw) body.password = pw;
7798
+ let r;
7799
+ try {
7800
+ r = await api("/api/zentao/test", { method: "POST", body: JSON.stringify(body) });
7801
+ } catch (e) { toast(e.message, true); return; }
7802
+ toast(r.message || (r.ok ? t("连接成功") : t("连接失败")), !r.ok);
7803
+ }
7804
+
7805
+ async function scanZentao() {
7806
+ let r;
7807
+ try {
7808
+ r = await api("/api/zentao/scan", { method: "POST", body: "{}" });
7809
+ } catch (e) { toast(e.message, true); return; }
7810
+ if (r.error) toast(r.error, true);
7811
+ else toast(t("扫描完成,新认领 ") + (r.claimed || 0) + t(" 个 Bug"));
7812
+ loadZentao();
7813
+ }
7814
+
7683
7815
  /* ---------------------------------------------------------- 插件市场 */
7684
7816
  /* /api/market → {catalog, categories(闭集)};搜索/分类/状态全在本地过滤。
7685
7817
  * 外部目录(/api/market/remote)只读缓存不联网;联网拉取仅在用户点「拉取更新」。 */
@@ -8007,8 +8139,77 @@ async function loadSettings() {
8007
8139
  queueGitProbe(); // 目录在场即探测代码版本,点亮分支胶囊
8008
8140
  }
8009
8141
  } catch (e) { /* 忽略 */ }
8142
+ loadSettingsV2(); // 引擎调参卡独立拉取(挂了不影响基础设置)
8010
8143
  }
8011
8144
 
8145
+ /* ---------------- settings_v2 引擎调参卡(schema 驱动) ----------------
8146
+ * /api/settings-v2 拉 describe(含字段元数据),按 namespace 渲染控件;
8147
+ * 保存走 /api/settings-v2/<ns> mutate(expected_revision CAS,409=别处已改,
8148
+ * 自动重拉最新值)。schema 变更零前端改动。 */
8149
+ async function loadSettingsV2() {
8150
+ const box = $("set-v2-card");
8151
+ if (!box) return;
8152
+ let v2 = null;
8153
+ try { v2 = await api("/api/settings-v2"); } catch (e) { box.innerHTML = ""; return; }
8154
+ S.settingsV2 = v2;
8155
+ const NS_LABEL = { orchestrator: t("编排引擎"), budget: t("预算"), cascade: t("级联路由") };
8156
+ let html = "<label>" + esc(t("引擎调参(schema 化设置,立即生效)")) + "</label>";
8157
+ for (const ns of Object.keys(v2.namespaces || {})) {
8158
+ const d = v2.namespaces[ns] || {};
8159
+ html += '<div class="set-v2-ns" data-ns="' + esc(ns) + '" data-rev="' + (d.revision || 0) + '">' +
8160
+ '<div class="set-v2-ns-h">' + esc(NS_LABEL[ns] || ns) +
8161
+ '<span class="flex1"></span><span class="set-v2-rev">rev ' + (d.revision || 0) + "</span></div>";
8162
+ for (const f of (d.fields || [])) {
8163
+ const iid = "setv2-" + esc(ns) + "-" + f.path.replace(/\./g, "-");
8164
+ const val = f.path.split(".").reduce((o, k) => (o && o[k] !== undefined) ? o[k] : undefined, d.values || {});
8165
+ const cur = val === undefined ? "" : val;
8166
+ let inp;
8167
+ if (f.type === "bool") {
8168
+ inp = '<input id="' + iid + '" type="checkbox"' + (cur ? " checked" : "") + ">";
8169
+ } else if (f.type === "int" || f.type === "float") {
8170
+ const step = f.type === "float" ? "0.01" : "1";
8171
+ inp = '<input id="' + iid + '" type="number" step="' + step + '" value="' + esc(String(cur)) + '"' +
8172
+ (f.clamp ? ' min="' + f.clamp[0] + '" max="' + f.clamp[1] + '"' : "") + ' style="max-width:140px">';
8173
+ } else {
8174
+ inp = '<input id="' + iid + '" type="text" value="' + esc(String(cur)) + '">';
8175
+ }
8176
+ html += '<div class="set-v2-row">' + inp +
8177
+ '<span class="set-v2-lb" title="' + esc(f.description || f.path) + '">' + esc(f.description || f.path) + "</span></div>";
8178
+ }
8179
+ html += '<div class="input-row" style="margin-top:6px"><button class="ghost small" onclick="saveSettingsV2(\'' + esc(ns) + '\')">' + t("保存") + "</button>" +
8180
+ '<span class="set-v2-msg msg"></span></div></div>';
8181
+ }
8182
+ box.innerHTML = html;
8183
+ }
8184
+
8185
+ window.saveSettingsV2 = async function (ns) {
8186
+ const v2 = S.settingsV2 || {};
8187
+ const d = (v2.namespaces || {})[ns];
8188
+ const box = document.querySelector('.set-v2-ns[data-ns="' + ns + '"]');
8189
+ if (!d || !box) return;
8190
+ const ops = [];
8191
+ for (const f of (d.fields || [])) {
8192
+ const iid = "setv2-" + ns + "-" + f.path.replace(/\./g, "-");
8193
+ const el = document.getElementById(iid);
8194
+ if (!el) continue;
8195
+ if (f.type === "bool") ops.push({ op: "set", path: f.path, value: el.checked });
8196
+ else if (f.type === "int" || f.type === "float") ops.push({ op: "set", path: f.path, value: Number(el.value) });
8197
+ else ops.push({ op: "set", path: f.path, value: el.value });
8198
+ }
8199
+ const msg = box.querySelector(".set-v2-msg");
8200
+ try {
8201
+ const r = await api("/api/settings-v2/" + ns, {
8202
+ method: "POST", body: JSON.stringify({ ops, expected_revision: d.revision || 0 }) });
8203
+ if (msg) { msg.className = "set-v2-msg msg ok"; msg.textContent = t("已保存 rev ") + r.revision; }
8204
+ loadSettingsV2();
8205
+ } catch (e) {
8206
+ if (/期望 rev/.test(String(e.message))) {
8207
+ if (msg) { msg.className = "set-v2-msg msg err"; msg.textContent = t("配置已被别处修改,已刷新,请重试"); }
8208
+ loadSettingsV2();
8209
+ } else if (msg) { msg.className = "set-v2-msg msg err"; msg.textContent = e.message; }
8210
+ }
8211
+ };
8212
+
8012
8213
  /* 保存默认保存路径;可选把旧默认路径下的现有任务目录迁移到新路径 */
8013
8214
  async function saveDefaultWorkdir() {
8014
8215
  const msg = $("settings-msg");
@@ -8353,7 +8554,7 @@ async function suStartupCheck() {
8353
8554
  }
8354
8555
 
8355
8556
  /* ---------------------------------------------------------- 页签 & 初始化 */
8356
- const TAB_TITLES = { tasks: "任务", runs: "运行记录", automation: "自动化", usage: "用量统计", agents: "智能体管理", models: "模型接入", bindings: "CLI 绑定", skills: "经验库", market: "插件市场", orch: "编排设置", appearance: "皮肤", about: "关于与更新" };
8557
+ const TAB_TITLES = { tasks: "任务", runs: "运行记录", automation: "自动化", zentao: "禅道 Bug 自动修复", usage: "用量统计", agents: "智能体管理", models: "模型接入", bindings: "CLI 绑定", skills: "经验库", market: "插件市场", orch: "编排设置", appearance: "皮肤", about: "关于与更新" };
8357
8558
  const SET_TABS = new Set(Object.keys(TAB_TITLES)); // 设置导航里的子页(__phone 是弹框,不算)
8358
8559
 
8359
8560
  function tabTitle(name) {
@@ -9012,6 +9213,7 @@ function switchTab(name) {
9012
9213
  if (name === "skills") loadSkills(); // 进经验库页拉取沉淀
9013
9214
  if (name === "automation") { loadAutomation(); startAutoPoll(); } // 进自动化页:拉取 + 页面可见时每 8s 轮询
9014
9215
  else stopAutoPoll(); // 离开自动化页(或切到别的子页)即停表
9216
+ if (name === "zentao") loadZentao(); // 进禅道页:拉配置与修复记录回填表单
9015
9217
  if (name === "market") loadMarket(); // 进插件市场页拉取目录
9016
9218
  if (name === "usage") { syncUsageRange(); loadUsage(); } // 进用量页:对齐范围选中态并拉取
9017
9219
  if (name === "appearance") renderAppearance(); // 进皮肤页:按当前皮肤/明暗重画卡片
@@ -9324,6 +9526,11 @@ document.addEventListener("DOMContentLoaded", () => {
9324
9526
  if (g) g.focus(); // 焦点直接落到目标框,改完即可提交
9325
9527
  });
9326
9528
  $("btn-delete").addEventListener("click", () => { if (S.detailRunId) deleteRun(S.detailRunId); });
9529
+ // 分享页:新窗口打开自包含 HTML(零依赖离线可看,可直接另存/转发)
9530
+ $("btn-share").addEventListener("click", () => {
9531
+ if (S.detailRunId) window.open("/api/runs/" + encodeURIComponent(S.detailRunId) + "/share",
9532
+ "_blank", "noopener");
9533
+ });
9327
9534
  $("btn-theme").addEventListener("click", toggleTheme);
9328
9535
  $("btn-notify-toggle").addEventListener("click", toggleNotifySound);
9329
9536
  // 命令面板:侧栏搜索行 / Ctrl+K 唤起(函数若尚未落地,跳过而不炸整个初始化)
@@ -9504,6 +9711,10 @@ document.addEventListener("DOMContentLoaded", () => {
9504
9711
  // 自动化页:刷新 / 新建 / 三段筛选(本地过滤)
9505
9712
  $("btn-auto-refresh").addEventListener("click", loadAutomation);
9506
9713
  $("btn-auto-new").addEventListener("click", () => autoForm());
9714
+ // 禅道页:测试连接 / 立即扫描 / 保存配置
9715
+ $("btn-zentao-test").addEventListener("click", testZentao);
9716
+ $("btn-zentao-scan").addEventListener("click", scanZentao);
9717
+ $("btn-zentao-save").addEventListener("click", saveZentao);
9507
9718
  $("auto-filter").addEventListener("click", (e) => {
9508
9719
  const b = e.target.closest("[data-f]");
9509
9720
  if (!b) return;
package/app/ui/i18n.js CHANGED
@@ -900,6 +900,7 @@
900
900
  "启用中": "Enabled",
901
901
  "设为主模型": "Set as primary model",
902
902
  "单智能体直达(快)": "Single-agent direct (fast)",
903
+ "抓七猫榜 → AI 选题洞察": "Scan Qimao rankings → AI topic insights",
903
904
  "新增函数补用例": "Add tests for new functions",
904
905
  "同类加函数任务,只要verify_pass=false即不得因review_pass=true或高分判定通过;必须将verify失败原因作为修复输入。": "For similar add-a-function tasks, verify_pass=false blocks acceptance regardless of review_pass or high scores; the verify failure details are the input to the next fix.",
905
906
  "凡主角获取越权信息或关键证据(系统记录、录音、账目),必须当章或前文落实来源链(人脉、留底、委托调查),并让角色当场追问一句'东西哪来的';无来源的特权查询与来历不明的证据一律禁止上稿。": "Whenever the protagonist obtains privileged information or key evidence (system records, recordings, ledgers), establish the source chain in the same or an earlier chapter and have a character ask \"where did this come from\" on the spot; unsourced privileged queries and evidence of unknown origin are banned from the manuscript.",
@@ -1571,6 +1572,8 @@
1571
1572
  "重新生成": "Regenerate",
1572
1573
  "重新生成会覆盖现有内容": "Regenerating overwrites the existing content",
1573
1574
  "生成失败": "Generation failed",
1575
+ "分享页": "Share page",
1576
+ "生成自包含分享页(单文件 HTML),可直接发给他人查看任务成果": "Generate a self-contained share page (single-file HTML) to send to others",
1574
1577
  "封面图": "Cover image",
1575
1578
  "生成封面": "Generate cover",
1576
1579
  "重试": "Retry",
@@ -1772,6 +1775,57 @@
1772
1775
  "一键反馈 Issue": "Report Issue",
1773
1776
  "已打开 GitHub 反馈页,内容已预填,可直接提交(也可附上诊断包 zip)": "GitHub issue page opened with the report pre-filled — submit it, and feel free to attach the diagnostics zip",
1774
1777
  "反馈摘要生成失败:": "Failed to build issue summary: ",
1778
+
1779
+ // —— 禅道 Bug 自动修复 ——
1780
+ "禅道": "ZenTao",
1781
+ "禅道 Bug 自动修复": "ZenTao Bug Auto-Fix",
1782
+ "禅道连接": "ZenTao Connection",
1783
+ "修复任务": "Fix Tasks",
1784
+ "修复记录": "Fix History",
1785
+ "测试连接": "Test Connection",
1786
+ "立即扫描": "Scan Now",
1787
+ "保存配置": "Save Settings",
1788
+ "用当前表单里的连接信息试连禅道": "Test ZenTao with the connection info in this form",
1789
+ "立刻扫一遍激活 Bug 并对账回写": "Scan active bugs now and write results back",
1790
+ "定时扫描禅道的激活 Bug,自动创建代码修复任务(实现→验证→评审→修复);跑完自动合并代码、resolve Bug 并把报告评论回禅道,群机器人同步通知。": "Periodically scans active ZenTao bugs and creates code fix tasks (implement → verify → review → fix); on success it merges the code, resolves the bug, posts a report comment back to ZenTao, and notifies the group bot.",
1791
+ "禅道地址": "ZenTao URL",
1792
+ "账号": "Account",
1793
+ "密码": "Password",
1794
+ "产品 ID": "Product IDs",
1795
+ "只认领指派给": "Only claim bugs assigned to",
1796
+ "严重度上限": "Severity cap",
1797
+ "不限": "Any",
1798
+ "工作目录": "Working directory",
1799
+ "基线分支": "Base branch",
1800
+ "验证命令": "Verify command",
1801
+ "自动 resolve Bug": "Auto-resolve bugs",
1802
+ "自动合并代码": "Auto-merge code",
1803
+ "定时扫描": "Scheduled scan",
1804
+ "扫描间隔(小时)": "Scan interval (hours)",
1805
+ "已保存(不改就留空)": "Saved (leave blank to keep)",
1806
+ "定时扫描已开启,每 ": "Scheduled scan on, every ",
1807
+ " 小时一次": " h",
1808
+ ",下次 ": ", next ",
1809
+ "定时扫描未开启(仍可手动「立即扫描」)": "Scheduled scan off (manual \"Scan Now\" still works)",
1810
+ "上次扫描 ": "Last scan ",
1811
+ "最近错误:": "Last error: ",
1812
+ "加载失败:服务未连接": "Load failed: service not connected",
1813
+ "修复中": "Fixing",
1814
+ "已解决": "Resolved",
1815
+ "合并失败": "Merge failed",
1816
+ "已评论留人工": "Commented, left to human",
1817
+ "回写失败": "Write-back failed",
1818
+ "待人工确认": "Awaiting human confirm",
1819
+ "记录丢失": "Record lost",
1820
+ "修复任务:": "Fix task: ",
1821
+ "认领于 ": "Claimed at ",
1822
+ "还没有认领过 Bug——配置好连接与产品 ID 后点「立即扫描」。": "No bugs claimed yet — set up the connection and product IDs, then click \"Scan Now\".",
1823
+ "禅道配置已保存": "ZenTao settings saved",
1824
+ "保存失败:": "Save failed: ",
1825
+ "连接成功": "Connected",
1826
+ "连接失败": "Connection failed",
1827
+ "扫描完成,新认领 ": "Scan finished, claimed ",
1828
+ " 个 Bug": " bug(s)",
1775
1829
  };
1776
1830
 
1777
1831
  // ---------- 工具 ----------
package/app/ui/index.html CHANGED
@@ -177,6 +177,7 @@
177
177
  <button class="set-item active" data-sub="tasks" data-i18n="任务"><svg class="ico" aria-hidden="true"><use href="#i-tasks"></use></svg>任务</button>
178
178
  <button class="set-item" data-sub="runs" data-i18n="运行记录"><svg class="ico" aria-hidden="true"><use href="#i-history"></use></svg>运行记录</button>
179
179
  <button class="set-item" data-sub="automation" data-i18n="自动化"><svg class="ico" aria-hidden="true"><use href="#i-calendar-days"></use></svg>自动化</button>
180
+ <button class="set-item" data-sub="zentao" data-i18n="禅道"><svg class="ico" aria-hidden="true"><use href="#i-search"></use></svg>禅道</button>
180
181
  <button class="set-item" data-sub="usage" data-i18n="用量统计"><svg class="ico" aria-hidden="true"><use href="#i-chart"></use></svg>用量统计</button>
181
182
  <div data-i18n="配置" class="set-group">配置</div>
182
183
  <button class="set-item" data-sub="agents" data-i18n="智能体管理"><svg class="ico" aria-hidden="true"><use href="#i-cpu"></use></svg>智能体管理</button>
@@ -383,6 +384,7 @@
383
384
  <button data-i18n="✎ 编辑重试" id="btn-editretry" class="ghost hidden" data-i18n-title="取回原任务的目标/目录/评审设置,修改后提交即重新开跑" title="取回原任务的目标/目录/评审设置,修改后提交即重新开跑">✎ 编辑重试</button>
384
385
  <button data-i18n="继续连载" id="btn-continue" class="ghost hidden">继续连载</button>
385
386
  <button data-i18n="基于此任务新建" id="btn-newfrom" class="ghost hidden">基于此任务新建</button>
387
+ <button data-i18n="分享页" id="btn-share" class="ghost hidden" data-i18n-title="生成自包含分享页(单文件 HTML),可直接发给他人查看任务成果" title="生成自包含分享页(单文件 HTML),可直接发给他人查看任务成果">分享页</button>
386
388
  <button data-i18n="删除记录" id="btn-delete" class="danger hidden">删除记录</button>
387
389
  <button data-i18n="✉ 继续对话" id="btn-talk" class="ghost hidden" data-i18n-title="人工检查有问题?告诉主智能体,由它判断怎么引导执行" title="人工检查有问题?告诉主智能体,由它判断怎么引导执行">✉ 继续对话</button>
388
390
  <button id="rd-more-toggle" class="rd-more-toggle" type="button" aria-label="更多任务信息" data-i18n-aria="更多任务信息" aria-expanded="false" aria-controls="rd-meta-popover" title="更多任务信息" data-i18n-title="更多任务信息">•••</button>
@@ -519,6 +521,53 @@
519
521
  </div>
520
522
  </div>
521
523
 
524
+ <!-- 禅道:Bug 自动扫描修复 -->
525
+ <div id="sub-zentao" class="subpage hidden">
526
+ <div class="panel wide">
527
+ <div class="panel-head">
528
+ <h2 data-i18n="禅道 Bug 自动修复">禅道 Bug 自动修复</h2>
529
+ <div class="actions">
530
+ <button id="btn-zentao-test" class="ghost small" title="用当前表单里的连接信息试连禅道" data-i18n-title="用当前表单里的连接信息试连禅道"><span data-i18n="测试连接">测试连接</span></button>
531
+ <button id="btn-zentao-scan" class="primary small" title="立刻扫一遍激活 Bug 并对账回写" data-i18n-title="立刻扫一遍激活 Bug 并对账回写"><span data-i18n="立即扫描">立即扫描</span></button>
532
+ </div>
533
+ </div>
534
+ <p data-i18n="定时扫描禅道的激活 Bug,自动创建代码修复任务(实现→验证→评审→修复);跑完自动合并代码、resolve Bug 并把报告评论回禅道,群机器人同步通知。" class="hint">定时扫描禅道的激活 Bug,自动创建代码修复任务(实现→验证→评审→修复);跑完自动合并代码、resolve Bug 并把报告评论回禅道,群机器人同步通知。</p>
535
+ <div id="zentao-status" class="hint zt-status"></div>
536
+ <h3 data-i18n="禅道连接" class="sec-title">禅道连接</h3>
537
+ <div class="zt-grid">
538
+ <label data-i18n="禅道地址">禅道地址</label><input id="zt-base-url" type="text" placeholder="https://zentao.example.com(子目录部署带子目录)">
539
+ <label data-i18n="账号">账号</label><input id="zt-account" type="text" placeholder="建议建专用账号(如 codebee)收 Bug">
540
+ <label data-i18n="密码">密码</label><input id="zt-password" type="password" placeholder="" autocomplete="new-password">
541
+ <label data-i18n="产品 ID">产品 ID</label><input id="zt-products" type="text" placeholder="逗号分隔,如 1,2(禅道产品页 URL 里的 product= 数字)">
542
+ <label data-i18n="只认领指派给">只认领指派给</label><input id="zt-assigned" type="text" placeholder="禅道账号名,空 = 不按指派过滤">
543
+ <label data-i18n="严重度上限">严重度上限</label>
544
+ <select id="zt-sev">
545
+ <option value="0" data-i18n="不限">不限</option>
546
+ <option value="1">1(最严重)</option>
547
+ <option value="2">2</option>
548
+ <option value="3">3</option>
549
+ <option value="4">4(最轻)</option>
550
+ </select>
551
+ </div>
552
+ <h3 data-i18n="修复任务" class="sec-title">修复任务</h3>
553
+ <div class="zt-grid">
554
+ <label data-i18n="工作目录">工作目录</label><input id="zt-workdir" type="text" placeholder="空 = 跟随默认保存路径">
555
+ <label data-i18n="基线分支">基线分支</label><input id="zt-gitrev" type="text" placeholder="如 main:每个修复检出任务分支,跑完可自动合并;空 = 直接改工作目录">
556
+ <label data-i18n="验证命令">验证命令</label><input id="zt-verify" type="text" placeholder="如 npm test:实现完成后先跑它把关;空 = 靠评审把关">
557
+ </div>
558
+ <h3 data-i18n="自动化" class="sec-title">自动化</h3>
559
+ <div class="zt-grid zt-grid-toggles">
560
+ <label class="toggle" title="修复任务评审通过后,自动到禅道 resolve(fixed)并回评论"><input id="zt-autoresolve" type="checkbox"><span data-i18n="自动 resolve Bug">自动 resolve Bug</span></label>
561
+ <label class="toggle" title="把任务分支自动合并回基线分支(需配置基线分支;合并失败不 resolve,留人工)"><input id="zt-automerge" type="checkbox"><span data-i18n="自动合并代码">自动合并代码</span></label>
562
+ <label class="toggle" title="按下面的间隔定时扫描;关掉后仍可手动「立即扫描」"><input id="zt-poll" type="checkbox"><span data-i18n="定时扫描">定时扫描</span></label>
563
+ <label data-i18n="扫描间隔(小时)">扫描间隔(小时)</label><input id="zt-interval" type="number" min="1" max="168" step="1">
564
+ </div>
565
+ <div class="ops"><button id="btn-zentao-save" class="primary small"><span data-i18n="保存配置">保存配置</span></button></div>
566
+ <h3 data-i18n="修复记录" class="sec-title">修复记录</h3>
567
+ <div id="zentao-claims"></div>
568
+ </div>
569
+ </div>
570
+
522
571
  <!-- 使用统计 -->
523
572
  <div id="sub-usage" class="subpage hidden">
524
573
  <div class="panel wide">
@@ -691,6 +740,7 @@
691
740
  <label class="toggle" style="margin-top:6px"><input id="set-workdir-migrate" type="checkbox"> <span data-i18n="保存时把「旧默认路径下」的现有任务目录迁移到新路径(运行中的跳过;手动指定的目录不受影响)">保存时把「旧默认路径下」的现有任务目录迁移到新路径(运行中的跳过;手动指定的目录不受影响)</span></label>
692
741
  <p class="hint" id="set-workdir-hint"></p>
693
742
  </div>
743
+ <div id="set-v2-card" class="field"></div>
694
744
  <div id="settings-msg" class="msg"></div>
695
745
  </div>
696
746
  </div>
package/app/ui/style.css CHANGED
@@ -5113,3 +5113,30 @@ body.welcome-open { overflow: hidden; }
5113
5113
  }
5114
5114
  .cl-opt:hover { border-color: var(--accent); color: var(--accent); opacity: 1; }
5115
5115
  .cl-opt.picked { border-color: var(--accent); color: var(--accent); opacity: .75; cursor: default; }
5116
+
5117
+ /* ---------------- settings_v2 引擎调参卡(schema 驱动,追加于文件尾防并行重排) ---------------- */
5118
+ #set-v2-card { margin-top: 14px; }
5119
+ .set-v2-ns { border: 1px solid var(--border); border-radius: 10px; padding: 10px 12px; margin: 8px 0; }
5120
+ .set-v2-ns-h { display: flex; align-items: center; gap: 8px; font-weight: 600; margin-bottom: 6px; }
5121
+ .set-v2-rev { font-size: 11px; color: var(--muted); font-weight: 400; }
5122
+ .set-v2-row { display: flex; align-items: center; gap: 10px; margin: 5px 0; }
5123
+ .set-v2-row input[type="number"], .set-v2-row input[type="text"] { width: 140px; }
5124
+ .set-v2-lb { font-size: 12px; color: var(--muted); }
5125
+ .set-v2-msg { font-size: 12px; }
5126
+
5127
+ /* ---------------- 禅道 Bug 自动修复设置页(追加于文件尾防并行重排) ---------------- */
5128
+ .zt-status { margin: 2px 0 6px; }
5129
+ .zt-err { color: var(--danger, #dc2626); margin-left: 10px; }
5130
+ .zt-grid {
5131
+ display: grid; grid-template-columns: 150px minmax(240px, 560px);
5132
+ gap: 8px 14px; align-items: center; margin: 6px 0 4px;
5133
+ }
5134
+ .zt-grid label { font-size: 12.5px; color: var(--muted); }
5135
+ .zt-grid input[type="text"], .zt-grid input[type="password"], .zt-grid input[type="number"],
5136
+ .zt-grid select { width: 100%; max-width: 560px; }
5137
+ .zt-grid-toggles { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
5138
+ .zt-grid-toggles label { display: flex; align-items: center; gap: 8px; color: inherit; font-size: 13px; }
5139
+ .zt-tag-ok { color: var(--ok); }
5140
+ .zt-tag-warn { color: var(--warn, #b45309); border-color: var(--warn, #b45309); }
5141
+ .zt-tag-err { color: var(--danger, #dc2626); border-color: var(--danger, #dc2626); }
5142
+ .zt-tag-fix { color: var(--accent); border-color: var(--accent); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebee",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "CodeBee · 多智能体编排台 — 统一编排本机 AI 编码 CLI(Codex / Claude Code / QwenCode / OpenCode / Aider…):目标→拆解→路由→执行→验证→跨厂商评审→修复→报告。纯 Python 标准库实现,本地 Web UI。",
5
5
  "license": "MIT",
6
6
  "bin": {