codebee 0.1.34 → 0.1.35

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.
@@ -5,7 +5,9 @@
5
5
  - **端口→进程→项目归属推断**:netstat/ss 拿端口→PID,再从进程 CWD 向上走找
6
6
  .git/package.json 等项目根——知道该进程属于哪个项目/用户
7
7
  - **本地 vs LAN 区分**:127.0.0.1 与 0.0.0.0/LAN 的安全边界
8
- - **温和关闭**:SIGTERM only(Windows taskkill /PID 不带 /F),关闭前重验 PID
8
+ - **温和关闭、强杀兜底**:先 SIGTERM(Windows taskkill /PID 不带 /F)并
9
+ 验证端口真的释放;控制台/服务进程对 WM_CLOSE 无反应,温和无效升级
10
+ 强杀,终裁以端口释放为准(关闭前/强杀前都重验 PID)
9
11
 
10
12
  跨平台(Windows netstat + PowerShell / POSIX ss + /proc)纯标准库。
11
13
  """
@@ -15,7 +17,9 @@ import json
15
17
  import logging
16
18
  import os
17
19
  import re
20
+ import signal
18
21
  import subprocess
22
+ import time
19
23
 
20
24
  log = logging.getLogger(__name__)
21
25
 
@@ -156,8 +160,46 @@ def listening_ports(with_names=True):
156
160
  return _ports_linux()
157
161
 
158
162
 
163
+ def _held_by(port, pid):
164
+ """端口当前是否仍被指定 PID 监听(轻量扫描,不起 PowerShell)。"""
165
+ return any(pp["port"] == int(port) and pp["pid"] == int(pid)
166
+ for pp in listening_ports(with_names=False))
167
+
168
+
169
+ def _wait_release(port, pid, deadline_s):
170
+ """轮询等待该 PID 释放端口;释放即 True,超时 False。"""
171
+ end = time.time() + deadline_s
172
+ while True:
173
+ if not _held_by(port, pid):
174
+ return True
175
+ if time.time() >= end:
176
+ return False
177
+ time.sleep(0.3)
178
+
179
+
180
+ def _win_force_kill(pid):
181
+ """Windows 强杀:TerminateProcess 直杀根进程(os.kill 的 SIGTERM 语义,
182
+ 毫秒级不挂),taskkill /F /T 短等待兜底扫子孙(同 runner._kill_tree 策略;
183
+ taskkill 病态挂死时靠 timeout 放弃,不拖死调用方)。"""
184
+ try:
185
+ os.kill(pid, signal.SIGTERM) # Windows 语义 = TerminateProcess
186
+ except OSError:
187
+ pass # 可能已被温和信号杀掉
188
+ try:
189
+ subprocess.run(["taskkill", "/F", "/T", "/PID", str(pid)],
190
+ timeout=8, capture_output=True)
191
+ except Exception:
192
+ pass # 兜底失败不阻塞终裁验证
193
+
194
+
159
195
  def close_port(port):
160
- """温和关闭端口上的进程。返回 (ok, message)。关闭前重验 PID 绑定。"""
196
+ """关闭端口上的进程。返回 (ok, message)。
197
+
198
+ 温和优先(POSIX SIGTERM / Windows WM_CLOSE)并验证端口真的释放。控制台/
199
+ 服务进程对 WM_CLOSE 无反应,taskkill 不带 /F 对它们必败——2026-09-21
200
+ 用户实测「扫描端口后关不掉」根因,且旧实现不看返回码假报成功。因此:
201
+ 温和无效升级强杀,强杀前重验同一 PID 仍监听(防 PID 复用误杀),终裁
202
+ 以端口释放为准,绝不假报成功。"""
161
203
  for p in listening_ports():
162
204
  if p["port"] != port:
163
205
  continue
@@ -169,20 +211,32 @@ def close_port(port):
169
211
  if p.get("process", "").lower() in _SYSTEM_PROCS:
170
212
  return False, "系统服务,不关闭"
171
213
  # 重验 PID 仍在监听该端口(防 PID 复用竞态);轻量扫描省掉 PowerShell
172
- still = any(pp["port"] == port and pp["pid"] == pid
173
- for pp in listening_ports(with_names=False))
174
- if not still:
214
+ if not _held_by(port, pid):
175
215
  return False, "PID %d 已不在端口 %d 上监听(竞态)" % (pid, port)
176
216
  if os.name == "posix":
177
217
  try:
178
218
  os.kill(pid, 15) # SIGTERM
179
- return True, "已发送 SIGTERM(PID %d)" % pid
180
219
  except OSError as e:
181
220
  return False, str(e)[:160]
182
- try:
183
- subprocess.run(["taskkill", "/PID", str(pid)],
184
- timeout=10, capture_output=True)
185
- return True, "已发送关闭信号(PID %d)" % pid
186
- except Exception as e:
187
- return False, str(e)[:160]
221
+ else:
222
+ try:
223
+ subprocess.run(["taskkill", "/PID", str(pid)],
224
+ timeout=10, capture_output=True)
225
+ except Exception:
226
+ pass # 温和失败不在此定论,交给验证+升级
227
+ if _wait_release(port, pid, 3):
228
+ return True, "已关闭(PID %d),端口 %d 已释放" % (pid, port)
229
+ # 温和无效(无窗口进程拒收 WM_CLOSE / 忽略 SIGTERM)→ 升级强杀
230
+ if not _held_by(port, pid):
231
+ return True, "已关闭(PID %d),端口 %d 已释放" % (pid, port)
232
+ if os.name == "posix":
233
+ try:
234
+ os.kill(pid, 9) # SIGKILL
235
+ except OSError as e:
236
+ return False, str(e)[:160]
237
+ else:
238
+ _win_force_kill(pid)
239
+ if _wait_release(port, pid, 3):
240
+ return True, "温和信号无效,已强制结束(PID %d),端口 %d 已释放" % (pid, port)
241
+ return False, "PID %d 未响应关闭,端口 %d 仍被监听,请手动处理" % (pid, port)
188
242
  return False, "端口 %d 未找到监听进程" % port
@@ -391,17 +391,25 @@ def relevance_top(lessons, task, limit):
391
391
  if not probe:
392
392
  return lessons[:limit]
393
393
 
394
- # 使用反馈闭环(pmb「量化记忆真实帮助」借鉴):被选中次数多的教训排前
394
+ # 使用反馈闭环(pmb「量化记忆真实帮助」+ tradememory「按结局加权召回」):
395
+ # 相关性优先;同分比 outcome 胜负(注入后任务过审 +1 / 未过 -1),再比 hits。
395
396
  def rank(x):
396
397
  grams = _text_bigrams(x.get("title")) | _text_bigrams(x.get("content"))
397
398
  overlap = -len(probe & grams)
399
+ karma = int(x.get("won") or 0) - int(x.get("lost") or 0)
398
400
  lid = x.get("id") or ""
399
- return (overlap, -int(x.get("hits") or 0), lid)
401
+ return (overlap, -karma, -int(x.get("hits") or 0), lid)
400
402
 
401
403
  return sorted(lessons, key=rank)[:limit]
402
404
 
403
405
 
404
- def block_for(task, scope_override=None, *, stable_order=False):
406
+ # 运行级注入登记(outcome 加权用):run_id → 注入的教训 id 列表。
407
+ # 容量有界防泄漏;run 收尾 learn_from_run 时消费清除。
408
+ _INJECTED = {}
409
+ _INJECTED_MAX = 200
410
+
411
+
412
+ def block_for(task, scope_override=None, *, stable_order=False, run_id=None):
405
413
  """生成注入提示词的经验块。命中即计数。返回 (文本, 命中的 id 列表)。
406
414
 
407
415
  3A:内置包 + 用户自建包都参与 scope 匹配;3B:带 persona 的包先注入
@@ -414,6 +422,9 @@ def block_for(task, scope_override=None, *, stable_order=False):
414
422
  39 个全文注入会先把 9000 字全局上限吃光,项目教训排在末尾被整段截掉。
415
423
  两道预算:①wildcard 包单包限额(定向命中的包不受限);②教训保底——
416
424
  包区最多吃到「全局上限 − 教训长度」,教训永远完整注入。
425
+
426
+ run_id(tradememory 借鉴·outcome 加权):登记本次注入的教训,run 收尾
427
+ 按结局(过审 +1 / 未过 -1)回写 won/lost——好教训在排序中胜出。
417
428
  """
418
429
  scope = scope_override or task.get("type") or "*"
419
430
  parts, used, lesson_ids = [], [], []
@@ -464,9 +475,35 @@ def block_for(task, scope_override=None, *, stable_order=False):
464
475
  text = text[:MAX_INJECT_CHARS] + "\n…(已截断)"
465
476
  if lesson_ids:
466
477
  bump_hits(lesson_ids) # 包 id 不参与教训热度,命中数据只保留一份真源
478
+ if run_id:
479
+ with _LOCK:
480
+ if len(_INJECTED) >= _INJECTED_MAX:
481
+ _INJECTED.clear() # 有界兜底:登记超量整体作废(丢信号不丢内存)
482
+ _INJECTED[str(run_id)] = list(lesson_ids)
467
483
  return text, used
468
484
 
469
485
 
486
+ def note_outcome(run_id, passed):
487
+ """run 收尾回写注入教训的胜负(tradememory outcome 加权)。
488
+
489
+ passed=True → won+1(这条教训在场时任务过审);False → lost+1。
490
+ 只清算登记在案的教训;幂等(同一 run 消费后清除登记)。"""
491
+ rid = str(run_id or "")
492
+ if not rid:
493
+ return
494
+ with _LOCK:
495
+ ids = _INJECTED.pop(rid, None)
496
+ if not ids:
497
+ return
498
+ data = _load()
499
+ idset = set(ids)
500
+ for it in (data.get("lessons") or []):
501
+ if it.get("id") in idset:
502
+ key = "won" if passed else "lost"
503
+ it[key] = int(it.get(key) or 0) + 1
504
+ _save(data)
505
+
506
+
470
507
  def bump_hits(ids):
471
508
  ids = [i for i in (ids or []) if i]
472
509
  if not ids:
@@ -569,6 +606,14 @@ def learn_from_run(run_id, use_orchestrator=True):
569
606
  run = store.get_run(run_id)
570
607
  if not run:
571
608
  return 0
609
+ # outcome 加权(tradememory):本 run 注入过的教训按结局记胜负——
610
+ # 失败 run 说明在场教训没防住这个问题(lost+1),过审则 won+1。
611
+ # 放最前:无论后续是否沉淀新教训,胜负都要落账。
612
+ try:
613
+ verdict = run.get("verdict") or {}
614
+ note_outcome(run_id, bool(verdict.get("pass")))
615
+ except Exception:
616
+ pass
572
617
  task = store.get_task(run.get("task_id")) if run.get("task_id") else None
573
618
  if not task:
574
619
  return 0
package/app/ui/app.js CHANGED
@@ -412,6 +412,16 @@ function operationLabel(path, opts) {
412
412
  [/\/market\//, "更新插件"],
413
413
  [/\/models\/binding$/, "更新智能体绑定"],
414
414
  [/\/models\//, "更新模型配置"],
415
+ [/\/zentao\/test$/, "测试禅道连接"],
416
+ [/\/zentao\/products$/, "拉取禅道产品"],
417
+ [/\/zentao\/users$/, "拉取禅道账号"],
418
+ [/\/zentao\/modules$/, "拉取模块清单"],
419
+ [/\/zentao\/scan$/, "禅道扫描 Bug"],
420
+ [/\/zentao\/config$/, "保存禅道配置"],
421
+ [/\/wxdigest\/groups$/, "刷新群列表"],
422
+ [/\/wxdigest\/scan$/, "扫描群摘要"],
423
+ [/\/wxdigest\/config$/, "保存群摘要设置"],
424
+ [/\/ports\/close$/, "关闭端口进程"],
415
425
  ];
416
426
  const hit = rules.find((it) => it[0].test(clean));
417
427
  return t(hit ? hit[1] : "提交操作");
@@ -5733,20 +5743,20 @@ function renderChapterScores(run) {
5733
5743
  cs.map((c) => {
5734
5744
  const means = c.means || {};
5735
5745
  const dims = Object.keys(means).map((d) =>
5736
- '<span class="cs-dim' + (means[d] >= 7 ? "" : " low") + '">' + esc(d) + " " + means[d] + "</span>").join("");
5746
+ '<span class="ch-dim' + (means[d] >= 7 ? "" : " low") + '">' + esc(d) + " " + means[d] + "</span>").join("");
5737
5747
  const ev = (c.event_check || []).map((line) => {
5738
5748
  const ok = /已完成/.test(line), bad = /未完成/.test(line);
5739
- return '<div class="cs-ev' + (ok ? " ok" : bad ? " bad" : "") + '">' + esc(line) + "</div>";
5749
+ return '<div class="ch-ev' + (ok ? " ok" : bad ? " bad" : "") + '">' + esc(line) + "</div>";
5740
5750
  }).join("");
5741
- return '<div class="cs-row' + (c.passed ? "" : " fail") + '">' +
5742
- '<div class="cs-head"><b>' + esc(t("第") + " " + c.chapter + " " + t("章")) + '</b>' +
5743
- '<span class="cs-title">' + esc(c.title || "") + "</span>" +
5751
+ return '<div class="ch-row' + (c.passed ? "" : " fail") + '">' +
5752
+ '<div class="ch-head"><b>' + esc(t("第") + " " + c.chapter + " " + t("章")) + '</b>' +
5753
+ '<span class="ch-title">' + esc(c.title || "") + "</span>" +
5744
5754
  '<span class="tag">' + (c.passed ? t("达标") : t("未达标")) + "</span>" +
5745
5755
  (c.reused ? '<span class="tag">' + t("沿用") + "</span>" : "") +
5746
- '<span class="cs-meta">' + Number(c.words || 0) + t(" 字") +
5756
+ '<span class="ch-meta">' + Number(c.words || 0) + t(" 字") +
5747
5757
  (c.rounds > 1 ? " · " + c.rounds + t(" 轮") : "") + "</span></div>" +
5748
- (dims ? '<div class="cs-dims">' + dims + "</div>" : "") +
5749
- (ev ? '<div class="cs-evs"><span class="hint">' + t("逐项目标核对:") + "</span>" + ev + "</div>" : "") +
5758
+ (dims ? '<div class="ch-dims">' + dims + "</div>" : "") +
5759
+ (ev ? '<div class="ch-evs"><span class="hint">' + t("逐项目标核对:") + "</span>" + ev + "</div>" : "") +
5750
5760
  "</div>";
5751
5761
  }).join("");
5752
5762
  }
@@ -8904,14 +8914,14 @@ async function testZentao() {
8904
8914
  }
8905
8915
  }
8906
8916
  if (r.ok) {
8907
- try {
8908
- // 测试成功即保存当前连接并拉产品清单,用户无需再理解“先保存再拉取”的顺序。
8909
- await api("/api/zentao/config", { method: "POST", operation: false,
8910
- body: JSON.stringify(ztFormPayload()) });
8911
- await loadZentao();
8912
- await ztFetchCatalog(true);
8913
- toast(t("连接成功,已保存;现在可直接从禅道选择产品"));
8914
- } catch (e) { toast(t("连接成功,但保存配置失败:") + e.message, true); }
8917
+ // 测试成功即保存当前连接;产品/账号清单后台拉(真机禅道慢,await 会把
8918
+ // 确认按钮按住几十秒——「保存为啥要这么久」根因),拉到后下拉自动就位。
8919
+ api("/api/zentao/config", { method: "POST", operation: false,
8920
+ body: JSON.stringify(ztFormPayload()) })
8921
+ .then(() => loadZentao())
8922
+ .catch(() => {});
8923
+ toast(t("连接成功,已保存;正在拉取产品清单…"));
8924
+ ztFetchCatalog(true).catch(() => {});
8915
8925
  }
8916
8926
  }
8917
8927
 
@@ -9879,10 +9889,10 @@ async function scanPorts() {
9879
9889
  }
9880
9890
 
9881
9891
  async function closePort(port) {
9882
- if (!confirm(t("向占用端口 %1 的进程发送温和关闭信号?(系统进程会被拒绝)").replace("%1", port))) return;
9892
+ if (!confirm(t("结束占用端口 %1 的进程?先温和关闭,无效会自动强制结束(系统进程会被拒绝)").replace("%1", port))) return;
9883
9893
  try {
9884
9894
  const r = await api("/api/ports/close", { method: "POST", body: JSON.stringify({ port }) });
9885
- toast(r.message || (r.ok ? t("已发送关闭信号") : t("未能关闭")), !r.ok);
9895
+ toast(r.message || (r.ok ? t("已关闭") : t("未能关闭")), !r.ok);
9886
9896
  scanPorts();
9887
9897
  } catch (e) {
9888
9898
  toast(e.message || t("关闭失败"), true);
@@ -11649,16 +11659,24 @@ async function beeRefreshGroups() {
11649
11659
  const old = btn.textContent;
11650
11660
  btn.textContent = t("刷新中…");
11651
11661
  try {
11662
+ // 后端 refresh_groups 读的是已保存配置的 reader_python;用户选完候选
11663
+ // 直接点刷新(不点保存)时服务端还是旧值——先静默落盘再刷新
11664
+ // (「填了路径仍报请先填」根因)
11665
+ if (!(await beeSaveCore())) { return; }
11652
11666
  const r = await api("/api/wxdigest/groups", { method: "POST", body: "{}", timeout: 180000 });
11653
11667
  if (Bee.view) Bee.view.reader_groups_cache = r.groups || [];
11654
11668
  toast(t("群列表已更新:") + (r.groups || []).length);
11655
11669
  } catch (e) { toast(t("刷新失败:") + (e.message || e), true); }
11656
- btn.disabled = false;
11657
- btn.textContent = old;
11658
- beeRenderGroups();
11670
+ finally {
11671
+ btn.disabled = false;
11672
+ btn.textContent = old;
11673
+ beeRenderGroups();
11674
+ }
11659
11675
  }
11660
11676
 
11661
- async function beeSave() {
11677
+ async function beeSaveCore() {
11678
+ /* 把表单当前值落到服务端;成功返回 true,失败已 toast。不碰勾选态
11679
+ (beeSave 的 selGroups 置空依赖随后的 beeRefresh 重建,这里没有)。 */
11662
11680
  try {
11663
11681
  const r = await api("/api/wxdigest/config", { method: "POST", body: JSON.stringify({
11664
11682
  enabled: $("bee-enabled").checked,
@@ -11670,11 +11688,16 @@ async function beeSave() {
11670
11688
  groups: Bee.selGroups || [],
11671
11689
  }) });
11672
11690
  if (Bee.view) Bee.view.config = r.config;
11673
- Bee.selGroups = null; // 下次渲染用保存后的值重建勾选态
11674
- Bee.pyDirty = false; // 保存成功:服务端值与所见一致,恢复轮询回填
11675
- toast(t("群摘要设置已保存"));
11676
- beeRefresh();
11677
- } catch (e) { toast(t("保存失败:") + (e.message || e), true); }
11691
+ Bee.pyDirty = false; // 保存成功:服务端值与所见一致,恢复轮询回填
11692
+ return true;
11693
+ } catch (e) { toast(t("保存失败:") + (e.message || e), true); return false; }
11694
+ }
11695
+
11696
+ async function beeSave() {
11697
+ if (!(await beeSaveCore())) return;
11698
+ Bee.selGroups = null; // 下次渲染用保存后的值重建勾选态
11699
+ toast(t("群摘要设置已保存"));
11700
+ beeRefresh();
11678
11701
  }
11679
11702
 
11680
11703
  async function beeScan() {
@@ -11682,13 +11705,15 @@ async function beeScan() {
11682
11705
  if (!btn || btn.disabled) return;
11683
11706
  btn.disabled = true;
11684
11707
  try {
11708
+ // 微信直连拉取依赖 reader_python,先静默落盘(同 beeRefreshGroups)
11709
+ if (!(await beeSaveCore())) { return; }
11685
11710
  const r = await api("/api/wxdigest/scan", { method: "POST", body: "{}", timeout: 300000 });
11686
11711
  if (r.ok && r.made) toast(t("扫描完成,生成") + " " + r.made + t(" 条新摘要"));
11687
11712
  else if (r.ok) toast(t("扫描完成,暂无新内容"));
11688
11713
  else toast(t("扫描有问题:") + (r.error || ""), true);
11689
11714
  beeRefresh();
11690
11715
  } catch (e) { toast(t("扫描失败:") + (e.message || e), true); }
11691
- btn.disabled = false;
11716
+ finally { btn.disabled = false; }
11692
11717
  }
11693
11718
 
11694
11719
  (function () {
package/app/ui/i18n.js CHANGED
@@ -1950,8 +1950,8 @@
1950
1950
  "关闭": "Close",
1951
1951
  "没有发现监听端口": "No listening ports found",
1952
1952
  "扫描失败:": "Scan failed: ",
1953
- "向占用端口 %1 的进程发送温和关闭信号?(系统进程会被拒绝)": "Send a gentle close signal to the process holding port %1? (system processes are refused)",
1954
- "已发送关闭信号": "Close signal sent",
1953
+ "结束占用端口 %1 的进程?先温和关闭,无效会自动强制结束(系统进程会被拒绝)": "Kill the process holding port %1? Gentle close first, forced kill if ignored (system processes are refused)",
1954
+ "已关闭": "Closed",
1955
1955
  "未能关闭": "Could not close",
1956
1956
  "关闭失败": "Close failed",
1957
1957
 
package/app/ui/index.html CHANGED
@@ -797,7 +797,7 @@
797
797
  <!-- 群摘要(正式设置子页:配置与摘要一体,替代原右下角悬浮蜜蜂坞) -->
798
798
  <div id="sub-__wxdigest" class="subpage hidden">
799
799
  <div class="panel wide">
800
- <div class="panel-head"><h2 data-i18n="群摘要">群摘要</h2><span class="spacer"></span><button id="bee-scan-btn" class="ghost small" data-i18n="立即扫描" data-i18n-title="立即扫描" title="立即扫描"><svg class="ico" aria-hidden="true"><use href="#i-refresh"></use></svg><span data-i18n="立即扫描">立即扫描</span></button></div>
800
+ <div class="panel-head"><h2 data-i18n="群摘要">群摘要</h2><span class="spacer"></span><button id="bee-scan-btn" class="ghost small" data-i18n-title="立即扫描" title="立即扫描"><svg class="ico" aria-hidden="true"><use href="#i-refresh"></use></svg><span data-i18n="立即扫描">立即扫描</span></button></div>
801
801
  <p class="hint" data-i18n="导出 txt 丢进监控文件夹,或开启微信直连由蜜蜂自动拉取;按间隔增量生成每群摘要。记录只存本地;摘要请求会发给当前绑定的模型,桌面小蜜蜂会提醒新摘要。">导出 txt 丢进监控文件夹,或开启微信直连由蜜蜂自动拉取;按间隔增量生成每群摘要。记录只存本地;摘要请求会发给当前绑定的模型,桌面小蜜蜂会提醒新摘要。</p>
802
802
  <div id="bee-cfg" class="bee-cfg">
803
803
  <label class="toggle"><input id="bee-enabled" type="checkbox"> <span data-i18n="启用定时扫描">启用定时扫描</span></label>
package/app/ui/style.css CHANGED
@@ -5597,19 +5597,21 @@ button.request-busy > * { opacity: .18; }
5597
5597
  #sub-__wxdigest .bee-list { max-height: 52vh; overflow: auto; }
5598
5598
  #sub-__wxdigest .bee-groups-list { max-height: 240px; }
5599
5599
 
5600
- /* 连载章节评审卡:均分徽标 + 逐项目标审稿(event_check)行级红绿 */
5600
+ /* 连载章节评审卡:均分徽标 + 逐项目标审稿(event_check)行级红绿。
5601
+ * 类名用 ch- 前缀——曾撞代码设置的 .cs-row(后加载覆盖成纵向卡片,
5602
+ * 设置页「代码设置」整列变居中竖排,2026-09-21 用户反馈)。 */
5601
5603
  .rd-chapters { margin: 8px 0; }
5602
- .cs-row { border: 1px solid var(--border, rgba(128,128,128,.22)); border-radius: 8px;
5604
+ .ch-row { border: 1px solid var(--border, rgba(128,128,128,.22)); border-radius: 8px;
5603
5605
  padding: 8px 10px; margin: 6px 0; display: flex; flex-direction: column; gap: 4px; }
5604
- .cs-row.fail { border-color: color-mix(in srgb, var(--bad) 45%, transparent); }
5605
- .cs-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
5606
- .cs-title { color: var(--muted); font-size: 12.5px; }
5607
- .cs-meta { margin-left: auto; color: var(--muted); font-size: 12px; }
5608
- .cs-dims { display: flex; gap: 6px; flex-wrap: wrap; }
5609
- .cs-dim { font-size: 11.5px; padding: 1px 8px; border-radius: 999px;
5606
+ .ch-row.fail { border-color: color-mix(in srgb, var(--bad) 45%, transparent); }
5607
+ .ch-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
5608
+ .ch-title { color: var(--muted); font-size: 12.5px; }
5609
+ .ch-meta { margin-left: auto; color: var(--muted); font-size: 12px; }
5610
+ .ch-dims { display: flex; gap: 6px; flex-wrap: wrap; }
5611
+ .ch-dim { font-size: 11.5px; padding: 1px 8px; border-radius: 999px;
5610
5612
  background: color-mix(in srgb, var(--ok) 12%, transparent); }
5611
- .cs-dim.low { background: color-mix(in srgb, var(--bad) 14%, transparent); }
5612
- .cs-evs { display: flex; flex-direction: column; gap: 2px; font-size: 12px; }
5613
- .cs-ev { padding-left: 10px; border-left: 2px solid var(--border); color: var(--muted); }
5614
- .cs-ev.ok { border-left-color: var(--ok); color: inherit; }
5615
- .cs-ev.bad { border-left-color: var(--bad); color: inherit; }
5613
+ .ch-dim.low { background: color-mix(in srgb, var(--bad) 14%, transparent); }
5614
+ .ch-evs { display: flex; flex-direction: column; gap: 2px; font-size: 12px; }
5615
+ .ch-ev { padding-left: 10px; border-left: 2px solid var(--border); color: var(--muted); }
5616
+ .ch-ev.ok { border-left-color: var(--ok); color: inherit; }
5617
+ .ch-ev.bad { border-left-color: var(--bad); color: inherit; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebee",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "description": "CodeBee · 多智能体编排台 — 统一编排本机 AI 编码 CLI(Codex / Claude Code / QwenCode / OpenCode / Aider…):目标→拆解→路由→执行→验证→跨厂商评审→修复→报告。纯 Python 标准库实现,本地 Web UI。",
5
5
  "license": "MIT",
6
6
  "bin": {