codebee 0.1.51 → 0.1.52

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 CHANGED
@@ -23,6 +23,12 @@ README 元数据带回,供老版本在「发现新版本」时展示新版更
23
23
 
24
24
  - **首选 KEY 超限自动切备用了**:同厂商多密钥此前只认欠费类文案——首选 KEY 撞上 429 限流(智谱「并发数超过限制」「每分钟Token数已超过上限」、claude 的 rate_limit_error、codex 的 "429 Too Many Requests" 等)只记一笔错误、不进冷却,每个新步骤仍从超限的首选 KEY 重新烧起。现在限流文案与欠费同口径:进冷却 30 分钟、密钥镜像自动切下一把、链展开跳过冷却中的 KEY(到期或「恢复」按钮一键回首选)。顺带补一处漏账:无显式链时「换模型回退」的链条目不带 KEY 标记,这类重试失败此前完全不记 KEY 账。
25
25
 
26
+ ## v0.1.52(2026-09-22)
27
+
28
+ - 手动指定执行者优先级全面收口:用户点名的 CLI 压过一切自动选择——路由不换将、旁观不抢占、绑定不覆盖;「手动指定执行者」作为一等偏好随任务走
29
+ - 管理页执行者下拉可指定为对话执行者的真实 CLI(与新建任务两级菜单复用同一段逻辑)
30
+ - runner 静默挂死看门狗对「等结束才一次性输出」的 CLI 保留原语义,防误杀
31
+
26
32
  ## v0.1.51(2026-09-22)
27
33
 
28
34
  - 智能路由:绑定链可用性细化为「有备胎」维度(解析出的调用链有第二条及以上的任务加分),静态能力基线不再把配额烧干的供应商顶在前面
package/README.md CHANGED
@@ -21,11 +21,10 @@ Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness……),提供
21
21
  不会把你的任务内容交给任何第三方。
22
22
 
23
23
  <!-- relnotes:start -->
24
- ### 最新版更新内容(v0.1.51)
24
+ ### 最新版更新内容(v0.1.52)
25
25
 
26
- - 智能路由识别「有备胎」的绑定链并加分;配额烧干的供应商不再霸占前排
27
- - 内置智能体尊重用户指定的实现者;推理思考 token 计入输出预算
28
- - 错误识别降噪:提示词回显不再被误判为执行错误
26
+ - 手动指定执行者为一等偏好:用户点名的 CLI 压过自动路由,全程不换将、不被抢占
27
+ - 管理页执行者下拉与新建任务菜单统一;静默挂死看门狗防误杀保留
29
28
  <!-- relnotes:end -->
30
29
 
31
30
  ---
@@ -592,7 +592,13 @@ def run_now(tid):
592
592
  t = _TASKS.get(tid)
593
593
  if t is None:
594
594
  return None, None
595
- t["last_run"] = _fmt_dt(now)
595
+ # interval 任务的 last_run 是 next_run 的计算锚点(compute_next_run 按
596
+ # last_run + N 小时向前滚),手动触发写进来会让重启重算把档期整体后移
597
+ # ——2026-09-23 实案:22:28 手动跑一轮 + 23:43 服务重启,本该 23:53
598
+ # 触发的档期被抹成 00:28。interval 只让调度触发推进 last_run;其余
599
+ # kind 的 next_run 由 time/weekday 决定,last_run 仅作展示,照旧写。
600
+ if t.get("kind") != "interval":
601
+ t["last_run"] = _fmt_dt(now)
596
602
  t["run_count"] = int(t.get("run_count") or 0) + 1
597
603
  t["last_status"] = status
598
604
  _save_locked()
@@ -64,7 +64,10 @@ DEFAULT_CATALOG = [
64
64
  "note": "通义千问编码 CLI(gemini-cli 系);无头走 stdin;绑定凭据经打开注入"
65
65
  "写 settings.json 的 env 段(OPENAI_*,qwen 存在即优先 openai 兼容通道)",
66
66
  "detect": {"cli": "qwen"},
67
- "orch": {"kind": "qwen", "command": "qwen"},
67
+ # stall_timeout_s:静默 15 分钟即杀(2026-09-22 全书总评实案:qwen 吐完
68
+ # 评审内容后卡在 MCP 收尾死锁,输出归零但进程活着,stall=0 时只能干等
69
+ # 2400s 总超时;900s 取「评审模型调用常见上限 5 分钟」的 3 倍,防误杀)
70
+ "orch": {"kind": "qwen", "command": "qwen", "stall_timeout_s": 900},
68
71
  # settings.json 顶层 "model" 已是 legacy(qwen 忽略并告警,新格式是
69
72
  # model.name)——模型落盘由打开注入器写 OPENAI_MODEL env 负责,
70
73
  # 这里不再走 write_model
@@ -154,7 +157,7 @@ DEFAULT_CATALOG = [
154
157
  "note": "DeepSeek 官方 agent harness(dsh,profile 插件架构);无头是「一次性任务」——"
155
158
  "答完即退、无交互后续、不支持会话恢复;任务只走位置参数(超长提示词受 Windows "
156
159
  "命令行上限约 32k 约束);模型写进 ~/.dsh/settings.yaml 的 agent-default-model.model;"
157
- "密钥由运行时自动调度或「模型调度(可选)」注入 DEEPSEEK_API_KEY(优先级最高);端点注入 DEEPSEEK_BASE_URL,"
160
+ "密钥由运行时自动调度或「模型调度(可选)」注入 DEEPSEEK_API_KEY(优先级最高);端点注入 DEEPSEEK_BASE_URL,"
158
161
  "但若 settings.yaml 已固定 llm-deepseek.baseURL,则以它为准(settings 高于 env)",
159
162
  "detect": {"cli": "dsh"},
160
163
  "orch": {"kind": "generic", "command": "dsh",
@@ -316,6 +319,21 @@ def _apply_config_patch(entries):
316
319
  e["config"] = dict(patch)
317
320
 
318
321
 
322
+ def _apply_stall_patch(entries):
323
+ """存量数据幂等补齐:qwencode 补 stall_timeout_s=900(2026-09-22 实案)。
324
+
325
+ data/catalog.json 生成后不会再随版本重写,老清单里的 qwencode 没有
326
+ 停滞看门狗配置(当时按「结束才一次性输出」留 0);实际验证 qwen 评审
327
+ 会先流式吐内容再卡死在 MCP 收尾,静默看门狗能兜住。用户手写过该字段的
328
+ 不覆盖(包括显式写 0 关闭的)。"""
329
+ for e in entries:
330
+ if e.get("id") != "qwencode":
331
+ continue
332
+ orch = e.get("orch")
333
+ if isinstance(orch, dict) and "stall_timeout_s" not in orch:
334
+ orch["stall_timeout_s"] = 900
335
+
336
+
319
337
  def _apply_install_patch(entries):
320
338
  # Aider 渠道整体迁移到 uv tool(跨平台、含存量数据幂等修正):anaconda 的
321
339
  # py -3.13 装 aider 必挂在 numpy 源码构建(老 setuptools 引用 py3.12 已删除
@@ -371,6 +389,7 @@ def load(force=False):
371
389
  _apply_launch_patch(entries)
372
390
  _apply_config_patch(entries)
373
391
  _apply_install_patch(entries)
392
+ _apply_stall_patch(entries)
374
393
  _CACHE["entries"] = entries
375
394
  return entries
376
395
 
@@ -234,7 +234,10 @@ def block_for(task):
234
234
  grams = (skills._text_bigrams(x.get("title"))
235
235
  | skills._text_bigrams(x.get("body"))
236
236
  | set(x.get("tags") or []))
237
- return (-len(probe & grams), x.get("id") or "")
237
+ # 过期降权(inkos 检索保留来源/位置的启发):同等相关性下,
238
+ # 可能过期的事实排在新鲜事实之后——旧知识不该压过新知识
239
+ return (-len(probe & grams), 1 if _is_stale(x.get("as_of")) else 0,
240
+ x.get("id") or "")
238
241
  entries = sorted(entries, key=rank)
239
242
  else:
240
243
  entries.sort(key=lambda x: x.get("id") or "")
@@ -1481,11 +1481,31 @@ def _run_direct(run, task, agents, ev, stats, mode):
1481
1481
  route = {}
1482
1482
  resume_ctx = _valid_resume(task, agents)
1483
1483
  bi = None
1484
+ impl = None
1484
1485
  direct_provider = (task.get("direct_provider_id") or "").strip()
1485
1486
  direct_model = (task.get("direct_model") or "").strip()
1487
+ direct_agent = (task.get("direct_agent") or "").strip()
1486
1488
  direct_difficulty = ("hard" if task.get("thinking") == "high" else
1487
1489
  "easy" if task.get("thinking") == "low" else "default")
1488
- if resume_ctx is None and direct_provider:
1490
+ if resume_ctx is None and direct_agent:
1491
+ # 手动指定执行 CLI(2026-09-23):用户点名压过一切自动(模型绑定/
1492
+ # 自动推荐/路由)。目标未启用编排时按续会话语义注入(installed_agent
1493
+ # 无视启停开关)——显式指定不该被「参与编排」开关否决。
1494
+ impl = _pick(agents, direct_agent)
1495
+ if impl is None:
1496
+ extra = registry.installed_agent(direct_agent, catalog.load(),
1497
+ manager.detect_all())
1498
+ if extra:
1499
+ agents.append(extra)
1500
+ impl = extra
1501
+ if impl is None:
1502
+ store.update_run(run_id, expected_status="running", status="failed",
1503
+ error="指定的执行 CLI「%s」不可用(未安装或未检测到),"
1504
+ "请在对话条改回自动推荐,或到目录页先安装" % direct_agent,
1505
+ ended_at=_now())
1506
+ return
1507
+ route["implementer"] = "%s(手动指定 CLI)" % (impl.get("label") or impl.get("id"))
1508
+ elif resume_ctx is None and direct_provider:
1489
1509
  bi = builtin_agent.resolve(direct_provider, direct_model, direct_difficulty)
1490
1510
  if bi is None:
1491
1511
  store.update_run(run_id, expected_status="running", status="failed",
@@ -1507,6 +1527,8 @@ def _run_direct(run, task, agents, ev, stats, mode):
1507
1527
  elif resume_ctx is not None:
1508
1528
  impl = resume_ctx["agent"]
1509
1529
  route["implementer"] = resume_ctx["note"]
1530
+ elif impl is not None:
1531
+ pass # 手动指定 CLI 已就位(route 亦已写)
1510
1532
  elif mode == "manual":
1511
1533
  impl, _ = _pick_implementer(agents, task.get("implementer"))
1512
1534
  else:
package/app/core/prefs.py CHANGED
@@ -14,15 +14,20 @@ from . import paths
14
14
 
15
15
  _LOCK = threading.Lock()
16
16
 
17
- SAVE_KEYS = ("type", "rounds", "threshold", "best_of", "chapters",
18
- "words_per_chapter", "variants", "mode", "thinking")
19
- _LIMITS = {"rounds": (1, 5), "threshold": (1.0, 10.0), "best_of": (1, 3),
20
- "chapters": (1, 2000), "words_per_chapter": (200, 20000),
21
- "variants": (1, 3)}
22
- _CHOICES = {
23
- "mode": ("auto", "fast", "expert", "manual"),
24
- "thinking": ("auto", "low", "standard", "high"),
25
- }
17
+ SAVE_KEYS = ("type", "rounds", "threshold", "best_of", "chapters",
18
+ "words_per_chapter", "variants", "mode", "thinking",
19
+ # direct 类型的对话模型偏好(2026-09-23 补全偏好记忆覆盖面):
20
+ # 厂商 id/模型名/推理档——下次新建 direct 任务沿用上次手选
21
+ "direct_provider", "direct_model", "direct_thinking")
22
+ _LIMITS = {"rounds": (1, 5), "threshold": (1.0, 10.0), "best_of": (1, 3),
23
+ "chapters": (1, 2000), "words_per_chapter": (200, 20000),
24
+ "variants": (1, 3)}
25
+ _CHOICES = {
26
+ "mode": ("auto", "fast", "expert", "manual"),
27
+ "thinking": ("auto", "low", "standard", "high"),
28
+ "direct_thinking": ("auto", "low", "standard", "high"),
29
+ }
30
+ _STR_KEYS = ("type", "direct_provider", "direct_model") # 自由字符串截断白名单
26
31
 
27
32
 
28
33
  def _file():
@@ -51,17 +56,17 @@ def record(payload):
51
56
  v = payload.get(key)
52
57
  if v is None:
53
58
  continue
54
- if key == "type":
55
- v = str(v)[:32]
56
- if v:
57
- cur["type"] = v
58
- continue
59
- if key in _CHOICES:
60
- v = str(v).strip().lower()
61
- if v in _CHOICES[key]:
62
- cur[key] = v
63
- continue
64
- c = _clamp(key, v)
59
+ if key in _STR_KEYS:
60
+ v = str(v).strip()[:64]
61
+ if v:
62
+ cur[key] = v
63
+ continue
64
+ if key in _CHOICES:
65
+ v = str(v).strip().lower()
66
+ if v in _CHOICES[key]:
67
+ cur[key] = v
68
+ continue
69
+ c = _clamp(key, v)
65
70
  if c is not None:
66
71
  cur[key] = c
67
72
  tmp = _file().with_suffix(".tmp")
@@ -1276,9 +1276,9 @@ def run_agent(agent, prompt, workdir=None, readonly=True,
1276
1276
  elif kind == "claude":
1277
1277
  stall_t = _stall_timeout("TUTTI_CLAUDE_STALL_TIMEOUT", 600)
1278
1278
  else:
1279
- # 数据驱动:catalog orch.stall_timeout_s——给 kimi 这类边写边吐进度行的
1280
- # CLI 配置后,静默挂死 10 分钟即杀(不必耗满总超时);qwen/mimo 这类
1281
- # 结束才一次性输出的留 0(开了必误杀),靠总超时兜底
1279
+ # 数据驱动:catalog orch.stall_timeout_s——给 kimi/qwen 这类评审用
1280
+ # CLI 配置后,静默挂死即杀(qwen 900s:2026-09-22 MCP 收尾死锁案);
1281
+ # mimo 等结束才一次性输出的仍留 0(开了必误杀),靠总超时兜底
1282
1282
  try:
1283
1283
  stall_t = max(0, int((agent.get("orch") or {}).get("stall_timeout_s") or 0))
1284
1284
  except Exception:
@@ -295,7 +295,11 @@ def list_lessons(scope=None, only_enabled=False, category=None):
295
295
  if (x.get("category") or LESSON_UNCATEGORIZED) == category]
296
296
  if only_enabled:
297
297
  items = [x for x in items if x.get("enabled", True)]
298
- items.sort(key=lambda x: (-int(x.get("hits") or 0), -int(x.get("seen") or 1),
298
+ # karma 感知排序(UI 列表用;注入排序在 relevance_top 不走这里):
299
+ # 有效教训(won-lost 高)排最前——经验库页首屏即「真实帮上忙的」,
300
+ # 其次按注入热度;失守多的自然沉底(不隐藏——降权不删除)。
301
+ items.sort(key=lambda x: (-(int(x.get("won") or 0) - int(x.get("lost") or 0)),
302
+ -int(x.get("hits") or 0), -int(x.get("seen") or 1),
299
303
  x.get("created_at") or ""))
300
304
  return items
301
305
 
package/app/core/store.py CHANGED
@@ -135,6 +135,11 @@ def create_task(payload):
135
135
  task["direct_model"] = _text(payload.get("direct_model"), "direct_model")[:160]
136
136
  if task["direct_model"] and not task["direct_provider_id"]:
137
137
  raise ValueError("指定对话模型时必须同时指定厂商")
138
+ # 手动指定执行 CLI(2026-09-23):优先级高于模型绑定——运行时走该 CLI
139
+ # 的会话链而非内置智能体。存 id(如 codex-cli),可用性在运行时校验
140
+ da = _text(payload.get("direct_agent"), "direct_agent")[:80]
141
+ if da:
142
+ task["direct_agent"] = da
138
143
  # 代码版本:仅当引用合法才固化(流水线执行前据此检出任务分支)
139
144
  from . import gitmod
140
145
  git_rev = _text(payload.get("git_rev"), "git_rev")
@@ -1511,6 +1516,12 @@ def update_task_params(task_id, patch):
1511
1516
  task["direct_provider_id"] = pv
1512
1517
  task["direct_model"] = mv
1513
1518
  changed = True
1519
+ # 执行 CLI 可改/可清(清空 = 回内置智能体或模型绑定);与模型绑定
1520
+ # 允许共存,运行时 CLI 优先
1521
+ av = str((patch or {}).get("direct_agent") or "").strip()[:80]
1522
+ if task.get("direct_agent") != av and (av or task.get("direct_agent")):
1523
+ task["direct_agent"] = av
1524
+ changed = True
1514
1525
  if changed:
1515
1526
  _save_json(paths.TASKS_DIR / (task_id + ".json"), task)
1516
1527
  bump_state()
package/app/ui/app.js CHANGED
@@ -105,28 +105,59 @@ window.syncCmpSelFace = syncCmpSelFace; // applyI18n 重译 option 后同步
105
105
  * 菜单只改它们的值——既有提交流 / onTypeChange 显隐 / 测试全兼容。 */
106
106
  let cmpModelProv = ""; // ""=厂商级;否则=已钻入的厂商 id(模型级)
107
107
 
108
+ /* 可指定为对话执行者的真实 CLI(手动指定 CLI,2026-09-23):
109
+ * state.agents 即 effective_agents(已装+启用编排),mode=real 排掉 mock */
110
+ function realCliAgents() {
111
+ return ((S.state && S.state.agents) || []).filter((a) => a && a.mode === "real");
112
+ }
113
+
114
+ function cmpDirectAgentVal() {
115
+ const el = $("f-direct-agent");
116
+ return el ? String(el.value || "").trim() : "";
117
+ }
118
+
108
119
  function cmpDirectBtnSync() {
109
120
  const btn = $("f-direct-btn");
110
121
  if (!btn) return;
122
+ const av = cmpDirectAgentVal();
123
+ if (av) {
124
+ const a = realCliAgents().find((x) => x.id === av);
125
+ btn.textContent = (a ? (a.label || a.id) : av) + " · CLI";
126
+ return;
127
+ }
111
128
  const pv = $("f-direct-provider").value, mv = $("f-direct-model-name").value;
112
129
  if (!pv) { btn.textContent = t("自动推荐"); return; }
113
130
  const p = directProviders().find((x) => x.id === pv);
114
131
  btn.textContent = (p ? (p.name || p.id) : pv) + (mv ? "/" + mv : "/" + t("推荐"));
115
132
  }
116
133
 
134
+ /* 菜单里「指定 CLI」分组(两级菜单复用同一段,差别只在真源变量):
135
+ * entriesHtml(选中id, 空文案) 返回分组 HTML;空列表返回 "" */
136
+ function cliMenuSectionHtml(selectedId) {
137
+ const clis = realCliAgents();
138
+ if (!clis.length) return "";
139
+ const chk = '<svg class="ico ti-check" aria-hidden="true"><use href="#i-check"></use></svg>';
140
+ return '<div class="type-menu-cap">' + t("指定 CLI") + "</div>" +
141
+ clis.map((a) =>
142
+ '<button type="button" class="type-item" data-cli="' + esc(a.id) + '"><span class="ti-body"><span class="ti-name">' +
143
+ esc(a.label || a.id) + "</span></span>" + (selectedId === a.id ? chk : "") + "</button>").join("");
144
+ }
145
+
117
146
  function cmpModelMenuRender() {
118
147
  const menu = $("cmp-model-menu");
119
148
  if (!menu) return;
120
149
  const pv = $("f-direct-provider").value, mv = $("f-direct-model-name").value;
150
+ const av = cmpDirectAgentVal();
121
151
  const chk = '<svg class="ico ti-check" aria-hidden="true"><use href="#i-check"></use></svg>';
122
152
  const arr = '<svg class="ico ti-check" aria-hidden="true"><use href="#i-chevron-r"></use></svg>';
123
153
  if (!cmpModelProv) {
124
154
  menu.innerHTML =
125
155
  '<button type="button" class="type-item" data-p="" data-m=""><span class="ti-body"><span class="ti-name">' +
126
- t("自动推荐") + "</span></span>" + (!pv ? chk : "") + "</button>" +
156
+ t("自动推荐") + "</span></span>" + (!pv && !av ? chk : "") + "</button>" +
127
157
  directProviders().map((p) =>
128
158
  '<button type="button" class="type-item" data-p="' + esc(p.id) + '"><span class="ti-body"><span class="ti-name">' +
129
- esc(p.name || p.id) + "</span></span>" + (pv === p.id ? chk : arr) + "</button>").join("") +
159
+ esc(p.name || p.id) + "</span></span>" + (pv === p.id && !av ? chk : arr) + "</button>").join("") +
160
+ cliMenuSectionHtml(av) +
130
161
  '<div class="type-menu-foot"><button type="button" class="type-item" data-manage="1"><span class="ti-body"><span class="ti-name">' +
131
162
  t("管理模型…") + "</span></span></button></div>";
132
163
  } else {
@@ -162,10 +193,17 @@ window.toggleModelMenu = toggleModelMenu;
162
193
  // innerHTML 重写后 e.target 是已脱离 DOM 的旧节点,wrap.contains 会误判
163
194
  // false 而把刚钻取的菜单收掉(真机「点厂商没反应」根因)
164
195
  e.stopPropagation();
165
- const b = e.target.closest("[data-p],[data-back],[data-manage]");
196
+ const b = e.target.closest("[data-p],[data-back],[data-manage],[data-cli]");
166
197
  if (!b) return;
167
198
  if (b.dataset.manage) { toggleModelMenu(false); switchTab("models"); return; }
168
199
  if (b.dataset.back) { cmpModelProv = ""; cmpModelMenuRender(); return; }
200
+ if (b.dataset.cli !== undefined) {
201
+ // 手动指定执行 CLI:写真源即可,模型绑定保留在任务上(运行时 CLI 优先)
202
+ $("f-direct-agent").value = b.dataset.cli;
203
+ cmpDirectBtnSync();
204
+ toggleModelMenu(false);
205
+ return;
206
+ }
169
207
  if (b.dataset.m === undefined) {
170
208
  // 点厂商 = 只钻取到模型级(用户拍板 2026-09-21:模型必须亲手点,
171
209
  // 不要自动带推荐)。真源不动——用户点了具体模型才写 provider/model。
@@ -175,7 +213,9 @@ window.toggleModelMenu = toggleModelMenu;
175
213
  }
176
214
  // 选定:写回隐藏 select 真源(提交流/回归测试全走原路径)。顺序关键:
177
215
  // 先让 provider 的 option 就位再赋值,否则 select.value 静默失效(真机
178
- // 「没法选模型」根因:options 由 poll 重建,可能仍是残缺态)
216
+ // 「没法选模型」根因:options 由 poll 重建,可能仍是残缺态)。
217
+ // 选定模型/自动推荐同时清除 CLI 指定(三者互斥,模型生效=走内置智能体)
218
+ $("f-direct-agent").value = "";
179
219
  renderDirectModelPicker();
180
220
  $("f-direct-provider").value = b.dataset.p;
181
221
  renderDirectModelPicker();
@@ -2466,9 +2506,11 @@ async function createTask() {
2466
2506
  if (_eng === "code") {
2467
2507
  payload.verify_command = $("f-verify").value.trim();
2468
2508
  } else if (_eng === "direct") {
2469
- // 直连:目标+附件即全部输入,不带验证/评审参数
2509
+ // 直连:目标+附件即全部输入,不带验证/评审参数;direct_agent 为空时走
2510
+ // 内置智能体/模型绑定(后端按空串处理)
2470
2511
  payload.direct_provider_id = $("f-direct-provider").value;
2471
2512
  payload.direct_model = $("f-direct-model-name").value;
2513
+ payload.direct_agent = cmpDirectAgentVal();
2472
2514
  } else {
2473
2515
  payload.manuscript = $("f-manuscript").value.trim() || "manuscript.md";
2474
2516
  payload.rounds = parseInt($("f-rounds").value, 10) || 2;
@@ -3153,6 +3195,8 @@ function newFromTask(id) {
3153
3195
  $("f-direct-provider").value = tk.direct_provider_id || "";
3154
3196
  renderDirectModelPicker();
3155
3197
  $("f-direct-model-name").value = tk.direct_model || "";
3198
+ $("f-direct-agent").value = tk.direct_agent || "";
3199
+ cmpDirectBtnSync();
3156
3200
  } else {
3157
3201
  $("f-manuscript").value = tk.manuscript || "manuscript.md";
3158
3202
  $("f-rounds").value = tk.rounds || 2;
@@ -7318,6 +7362,7 @@ function renderDetailOverview(run, runs, task) {
7318
7362
  * 回填自当前任务;改动即写任务(空闲态),发送 /chat 前再兜底同步一次。 */
7319
7363
  let rdPrefsTaskId = "";
7320
7364
  let rdModelPv = "", rdModelMv = ""; // 对话条当前模型选择(厂商 id + 模型名)
7365
+ let rdModelAgent = ""; // 手动指定的执行 CLI(空 = 不指定)
7321
7366
 
7322
7367
  function rdPrefsFill(task) {
7323
7368
  if (!task) return;
@@ -7328,6 +7373,7 @@ function rdPrefsFill(task) {
7328
7373
  rdPrefsTaskId = task.id;
7329
7374
  rdModelPv = task.direct_provider_id || "";
7330
7375
  rdModelMv = task.direct_model || "";
7376
+ rdModelAgent = task.direct_agent || "";
7331
7377
  }
7332
7378
  ["rd-mode", "rd-thinking"].forEach((id) => syncCmpSelFace(id));
7333
7379
  rdModelFaceSync();
@@ -7336,6 +7382,11 @@ function rdPrefsFill(task) {
7336
7382
  function rdModelFaceSync() {
7337
7383
  const face = $("rd-model-btn");
7338
7384
  if (!face) return;
7385
+ if (rdModelAgent) {
7386
+ const a = realCliAgents().find((x) => x.id === rdModelAgent);
7387
+ face.textContent = (a ? (a.label || a.id) : rdModelAgent) + " · CLI";
7388
+ return;
7389
+ }
7339
7390
  if (!rdModelPv) { face.textContent = t("自动推荐"); return; }
7340
7391
  const p = directProviders().find((x) => x.id === rdModelPv);
7341
7392
  face.textContent = (p ? (p.name || p.id) : rdModelPv) +
@@ -7353,10 +7404,11 @@ function rdModelMenuRender() {
7353
7404
  if (!rdMenuProv) {
7354
7405
  menu.innerHTML =
7355
7406
  '<button type="button" class="type-item" data-p="" data-m=""><span class="ti-body"><span class="ti-name">' +
7356
- t("自动推荐") + "</span></span>" + (!rdModelPv ? chk : "") + "</button>" +
7407
+ t("自动推荐") + "</span></span>" + (!rdModelPv && !rdModelAgent ? chk : "") + "</button>" +
7357
7408
  directProviders().map((p) =>
7358
7409
  '<button type="button" class="type-item" data-p="' + esc(p.id) + '"><span class="ti-body"><span class="ti-name">' +
7359
- esc(p.name || p.id) + "</span></span>" + (rdModelPv === p.id ? chk : arr) + "</button>").join("") +
7410
+ esc(p.name || p.id) + "</span></span>" + (rdModelPv === p.id && !rdModelAgent ? chk : arr) + "</button>").join("") +
7411
+ cliMenuSectionHtml(rdModelAgent) +
7360
7412
  '<div class="type-menu-foot"><button type="button" class="type-item" data-manage="1"><span class="ti-body"><span class="ti-name">' +
7361
7413
  t("管理模型…") + "</span></span></button></div>";
7362
7414
  } else {
@@ -7389,13 +7441,21 @@ window.toggleRdModelMenu = toggleRdModelMenu;
7389
7441
  menu.dataset.rdBound = "1";
7390
7442
  menu.addEventListener("click", (e) => {
7391
7443
  e.stopPropagation(); // 同 cmp-model-menu:防「点外面收起」误判(DOM 重写竞态)
7392
- const b = e.target.closest("[data-p],[data-back],[data-manage]");
7444
+ const b = e.target.closest("[data-p],[data-back],[data-manage],[data-cli]");
7393
7445
  if (!b) return;
7394
7446
  if (b.dataset.manage) { toggleRdModelMenu(false); switchTab("models"); return; }
7395
7447
  if (b.dataset.back) { rdMenuProv = ""; rdModelMenuRender(); return; }
7448
+ if (b.dataset.cli !== undefined) {
7449
+ rdModelAgent = b.dataset.cli;
7450
+ rdModelFaceSync();
7451
+ toggleRdModelMenu(false);
7452
+ rdPrefsPush("model");
7453
+ return;
7454
+ }
7396
7455
  if (b.dataset.m === undefined) { rdMenuProv = b.dataset.p; rdModelMenuRender(); return; }
7397
7456
  rdModelPv = b.dataset.p;
7398
7457
  rdModelMv = b.dataset.m;
7458
+ rdModelAgent = ""; // 选定模型/自动推荐:CLI 指定随之清除(互斥)
7399
7459
  rdModelFaceSync();
7400
7460
  toggleRdModelMenu(false);
7401
7461
  rdPrefsPush("model");
@@ -7420,6 +7480,7 @@ async function rdPrefsPush(what) {
7420
7480
  if (what === "model") {
7421
7481
  patch.direct_provider_id = rdModelPv;
7422
7482
  patch.direct_model = rdModelMv;
7483
+ patch.direct_agent = rdModelAgent;
7423
7484
  }
7424
7485
  try {
7425
7486
  await api("/api/tasks/" + encodeURIComponent(task.id) + "/params", {
@@ -8884,6 +8945,8 @@ function renderSkills() {
8884
8945
  '<span class="tag">' + esc(x.scope === "*" ? t("通用") : x.scope) + "</span>" +
8885
8946
  (x.seen > 1 ? '<span class="tag">' + t("出现 ") + x.seen + t(" 次") + "</span>" : "") +
8886
8947
  (x.hits ? '<span class="tag">' + t("已注入 ") + x.hits + t(" 次") + "</span>" : "") +
8948
+ (x.won > 0 ? '<span class="tag ok" title="' + t("注入后任务过审的次数(真实帮上忙)") + '">' + t("有效 ") + x.won + "</span>" : "") +
8949
+ (x.lost > 0 ? '<span class="tag" style="color:var(--bad)" title="' + t("注入后任务仍失败的次数(没防住)") + '">' + t("失守 ") + x.lost + "</span>" : "") +
8887
8950
  (x.enabled === false ? '<span class="tag">' + t("已停用") + "</span>" : "") + "</div>" +
8888
8951
  '<div class="note">' + esc(t(x.content)) + "</div>" +
8889
8952
  '<div class="ops">' +
@@ -9115,6 +9178,11 @@ function autoPrefTags(tsk) {
9115
9178
  if (tsk.thinking && tsk.thinking !== "standard" && TH[tsk.thinking]) {
9116
9179
  out.push(t("思考程度") + t(":") + t(TH[tsk.thinking]));
9117
9180
  }
9181
+ if (tsk.direct_agent) {
9182
+ const a = realCliAgents().find((x) => x.id === tsk.direct_agent) ||
9183
+ ((S.state && S.state.agents) || []).find((x) => x.id === tsk.direct_agent);
9184
+ out.push(t("执行 CLI") + t(":") + (a ? (a.label || a.id) : tsk.direct_agent));
9185
+ }
9118
9186
  if (tsk.direct_provider_id) {
9119
9187
  const p = directProviders().find((x) => x.id === tsk.direct_provider_id);
9120
9188
  out.push(t("对话模型") + t(":") + (p ? (p.name || p.id) : tsk.direct_provider_id) +
package/app/ui/i18n.js CHANGED
@@ -42,6 +42,9 @@
42
42
  "对话厂商": "Chat provider",
43
43
  "对话模型": "Chat model",
44
44
  "随厂商推荐": "Provider default",
45
+ "指定 CLI": "Run via CLI",
46
+ "执行 CLI": "Agent CLI",
47
+ "执行 CLI:": "Agent CLI: ",
45
48
  "只对本次对话生效,不会修改全局绑定;指定渠道不可用时会明确报错。": "Applies only to this chat and does not change global bindings. An unavailable selected route returns an explicit error.",
46
49
  "推荐任务类型": "Recommended task type",
47
50
  "切换类型": "Switch type",
@@ -869,6 +872,10 @@
869
872
  "自定义 CLI": "Custom CLI",
870
873
  "已归档": "Archived",
871
874
  "已停用": "Disabled",
875
+ "有效 ": "Helped ",
876
+ "失守 ": "Missed ",
877
+ "注入后任务过审的次数(真实帮上忙)": "Times tasks passed review with this lesson injected (actually helped)",
878
+ "注入后任务仍失败的次数(没防住)": "Times tasks still failed with this lesson injected (didn't prevent it)",
872
879
  "正在刷新模型列表…": "Refreshing model list…",
873
880
  "无任务": "No tasks yet",
874
881
  "暂无任务": "No tasks yet",
package/app/ui/index.html CHANGED
@@ -424,7 +424,7 @@
424
424
  <div id="f-direct-model" class="field hidden"><label data-i18n="对话模型(可选)">对话模型(可选)</label><div class="grid-2">
425
425
  <select id="f-direct-provider" aria-label="对话厂商" data-i18n-aria="对话厂商"><option data-i18n="自动推荐">自动推荐</option></select>
426
426
  <select id="f-direct-model-name" aria-label="对话模型" data-i18n-aria="对话模型"><option data-i18n="随厂商推荐">随厂商推荐</option></select>
427
- </div><p class="hint" data-i18n="只对本次对话生效,不会修改全局绑定;指定渠道不可用时会明确报错。">只对本次对话生效,不会修改全局绑定;指定渠道不可用时会明确报错。</p></div>
427
+ </div><input type="hidden" id="f-direct-agent" value=""><p class="hint" data-i18n="只对本次对话生效,不会修改全局绑定;指定渠道不可用时会明确报错。">只对本次对话生效,不会修改全局绑定;指定渠道不可用时会明确报错。</p></div>
428
428
  <div class="grid-2">
429
429
  <div class="field">
430
430
  <label data-i18n="继续会话">继续会话</label>
package/app/ui/style.css CHANGED
@@ -6076,3 +6076,13 @@ button.request-busy > * { opacity: .18; }
6076
6076
  display: block; width: 100%; height: min(72vh, 660px);
6077
6077
  border: none; border-radius: 10px; background: #fff;
6078
6078
  }
6079
+
6080
+ /* 对话模型菜单里的 CLI 分组标题(2026-09-23 手动指定 CLI):与底部
6081
+ * 「管理模型…」同款弱化字重,但不可点,仅作分组界碑。 */
6082
+ .type-menu-cap {
6083
+ padding: 7px 14px 3px;
6084
+ font-size: 11px; font-weight: 600; letter-spacing: .04em;
6085
+ color: var(--muted);
6086
+ border-top: 1px solid var(--line, rgba(127,127,127,.18));
6087
+ margin-top: 4px;
6088
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebee",
3
- "version": "0.1.51",
3
+ "version": "0.1.52",
4
4
  "description": "CodeBee · 多智能体编排台 — 统一编排本机 AI 编码 CLI(Codex / Claude Code / QwenCode / OpenCode / Aider…):目标→拆解→路由→执行→验证→跨厂商评审→修复→报告。纯 Python 标准库实现,本地 Web UI。",
5
5
  "license": "MIT",
6
6
  "bin": {