codebee 0.1.21 → 0.1.22

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.
@@ -390,52 +390,14 @@ def relevance_top(lessons, task, limit):
390
390
  if not probe:
391
391
  return lessons[:limit]
392
392
 
393
- def rank(x):
394
- grams = _text_bigrams(x.get("title")) | _text_bigrams(x.get("content"))
395
- return (-len(probe & grams), x.get("id") or "")
396
-
397
- # 使用反馈闭环(pmb「量化记忆真实帮助」借鉴):被选中次数多的教训排前
398
- hits = _load_hits()
399
- def rank(x):
400
- grams = _text_bigrams(x.get("title")) | _text_bigrams(x.get("content"))
401
- overlap = -len(probe & grams)
402
- lid = x.get("id") or ""
403
- return (overlap, -hits.get(lid, 0), lid)
404
-
405
- return sorted(lessons, key=rank)[:limit]
406
-
407
-
408
- def _hits_path():
409
- return paths.DATA_DIR / "skill_hits.json"
410
-
411
-
412
- def _load_hits():
413
- """读取教训使用计数({lesson_id: 次数})。文件缺失/损坏返回空 dict。"""
414
- try:
415
- p = _hits_path()
416
- if p.is_file():
417
- import json
418
- return json.loads(p.read_text(encoding="utf-8"))
419
- except Exception:
420
- pass
421
- return {}
422
-
423
-
424
- def _bump_hits(ids):
425
- """注入后递增使用计数并持久化(fire-and-forget,失败静默)。"""
426
- try:
427
- import json
428
- p = _hits_path()
429
- hits = _load_hits()
430
- for lid in ids:
431
- if lid:
432
- hits[lid] = hits.get(lid, 0) + 1
433
- p.parent.mkdir(parents=True, exist_ok=True)
434
- tmp = p.with_suffix(".tmp")
435
- tmp.write_text(json.dumps(hits, ensure_ascii=False), encoding="utf-8")
436
- tmp.replace(p)
437
- except Exception:
438
- pass
393
+ # 使用反馈闭环(pmb「量化记忆真实帮助」借鉴):被选中次数多的教训排前
394
+ def rank(x):
395
+ grams = _text_bigrams(x.get("title")) | _text_bigrams(x.get("content"))
396
+ overlap = -len(probe & grams)
397
+ lid = x.get("id") or ""
398
+ return (overlap, -int(x.get("hits") or 0), lid)
399
+
400
+ return sorted(lessons, key=rank)[:limit]
439
401
 
440
402
 
441
403
  def block_for(task, scope_override=None, *, stable_order=False):
@@ -448,7 +410,7 @@ def block_for(task, scope_override=None, *, stable_order=False):
448
410
  前缀缓存(同一任务 8 章应看到完全相同的技能块)。内容不变,只稳排序。
449
411
  """
450
412
  scope = scope_override or task.get("type") or "*"
451
- parts, used = [], []
413
+ parts, used, lesson_ids = [], [], []
452
414
 
453
415
  for p in all_packs():
454
416
  if scope not in p["scopes"] and "*" not in p["scopes"]:
@@ -471,9 +433,10 @@ def block_for(task, scope_override=None, *, stable_order=False):
471
433
  if stable_order:
472
434
  lessons.sort(key=lambda x: x.get("id") or "")
473
435
  lines = []
474
- for x in lessons:
475
- lines.append("- **%s**:%s" % (x["title"], x["content"]))
476
- used.append(x["id"])
436
+ for x in lessons:
437
+ lines.append("- **%s**:%s" % (x["title"], x["content"]))
438
+ used.append(x["id"])
439
+ lesson_ids.append(x["id"])
477
440
  parts.append("### 【本项目已沉淀的教训(历史评审反复出现,务必规避)】\n" + "\n".join(lines))
478
441
 
479
442
  if not parts:
@@ -481,10 +444,8 @@ def block_for(task, scope_override=None, *, stable_order=False):
481
444
  text = "## 经验库(写作/工程规范 + 历史教训,必须遵守)\n\n" + "\n\n".join(parts)
482
445
  if len(text) > MAX_INJECT_CHARS:
483
446
  text = text[:MAX_INJECT_CHARS] + "\n…(已截断)"
484
- if used:
485
- _bump_hits(used) # 使用反馈闭环:被选中的教训递增计数,下次排序升权
486
- if used:
487
- bump_hits(used)
447
+ if lesson_ids:
448
+ bump_hits(lesson_ids) # 包 id 不参与教训热度,命中数据只保留一份真源
488
449
  return text, used
489
450
 
490
451
 
@@ -0,0 +1,83 @@
1
+ # -*- coding: utf-8 -*-
2
+ """任务编译器:把用户任务和预置流程编译成统一、可审计的运行规格。"""
3
+ from __future__ import annotations
4
+
5
+ from . import dispatch, flows
6
+
7
+ SCHEMA_VERSION = 1
8
+ DIFFICULTIES = ("easy", "default", "hard")
9
+
10
+
11
+ def _difficulty(task, flow):
12
+ explicit = str((task or {}).get("difficulty") or "").strip().lower()
13
+ if explicit in DIFFICULTIES:
14
+ return explicit, "用户指定"
15
+ try:
16
+ threshold = float((task or {}).get("threshold")
17
+ if (task or {}).get("threshold") is not None
18
+ else (flow or {}).get("threshold") or 7.0)
19
+ except (TypeError, ValueError):
20
+ threshold = 7.0
21
+ if threshold >= 8.5:
22
+ return "hard", "高质量门槛"
23
+ if threshold <= 6.0:
24
+ return "easy", "快速交付门槛"
25
+ if str((task or {}).get("type") or "").lower() in (
26
+ "research", "tech_proposal", "rank_scan"):
27
+ return "hard", "研究/方案类默认需要深度"
28
+ return "default", "标准难度"
29
+
30
+
31
+ def _capabilities(task, dimension, engine):
32
+ caps = [dimension]
33
+ if engine == "code" or (task or {}).get("verify_command"):
34
+ caps.extend(["filesystem", "verification"])
35
+ if (task or {}).get("serial"):
36
+ caps.extend(["long_context", "continuity"])
37
+ if (task or {}).get("attachments"):
38
+ caps.append("attachments")
39
+ return list(dict.fromkeys(caps))
40
+
41
+
42
+ def compile_task(task):
43
+ """返回统一任务规格;输入缺失或字段异常时保持可编排的安全兜底。"""
44
+ raw = dict(task or {})
45
+ ttype = str(raw.get("type") or "direct").strip().lower()
46
+ flow = flows.get_flow(ttype) or {}
47
+ engine = str(raw.get("engine") or flow.get("engine") or
48
+ ("code" if ttype == "code" else "review")).strip()
49
+ if engine not in flows.ENGINES:
50
+ engine = flow.get("engine") or ("code" if ttype == "code" else "review")
51
+ difficulty, difficulty_reason = _difficulty(raw, flow)
52
+ dimension = dispatch.task_dimension(ttype, raw.get("role") or "")
53
+ rubric = raw.get("rubric") or flow.get("rubric") or []
54
+ if isinstance(rubric, str):
55
+ rubric = [x.strip() for x in rubric.replace(",", ",").split(",") if x.strip()]
56
+ rubric = [str(x).strip() for x in rubric if str(x).strip()][:8]
57
+ deliverable = str(raw.get("manuscript") or flow.get("manuscript") or "").strip()
58
+ serial = raw.get("serial") if isinstance(raw.get("serial"), dict) else None
59
+ return {
60
+ "schema_version": SCHEMA_VERSION,
61
+ "type": ttype,
62
+ "title": str(raw.get("title") or "")[:120],
63
+ "goal": str(raw.get("goal") or "")[:2000],
64
+ "engine": engine,
65
+ "dimension": dimension,
66
+ "difficulty": difficulty,
67
+ "difficulty_reason": difficulty_reason,
68
+ "capabilities": _capabilities(raw, dimension, engine),
69
+ "deliverable": deliverable,
70
+ "quality_dimensions": rubric,
71
+ "serial": serial,
72
+ "explicit_agent": str(raw.get("implementer") or "").strip(),
73
+ "has_verification": bool(raw.get("verify_command")),
74
+ }
75
+
76
+
77
+ def summary(spec):
78
+ """给运行日志/UI 的短说明,不包含用户正文。"""
79
+ spec = spec or {}
80
+ return "%s/%s/%s(能力:%s)" % (
81
+ spec.get("type") or "direct", spec.get("engine") or "direct",
82
+ spec.get("difficulty") or "default",
83
+ "、".join(spec.get("capabilities") or []) or "通用")
package/app/pet.py CHANGED
@@ -30,8 +30,10 @@ import http.client
30
30
  import json
31
31
  import math
32
32
  import os
33
+ import queue
33
34
  import random
34
35
  import sys
36
+ import threading
35
37
  import time
36
38
  import webbrowser
37
39
  from pathlib import Path
@@ -39,7 +41,8 @@ from pathlib import Path
39
41
  # --------------------------------------------------------------- 纯逻辑(可测试)
40
42
 
41
43
  ACTIVE_STATUSES = ("queued", "running")
42
- BAD_STATUSES = ("failed", "cancelled")
44
+ GOOD_STATUSES = ("done",)
45
+ BAD_STATUSES = ("failed", "cancelled", "timeout")
43
46
  POLL_MS = 4000 # 轮询间隔
44
47
  POLL_TIMEOUT = 2.5 # 单次拉取超时(秒)
45
48
  MAX_MISS = 15 # 连续拉不到服务 N 次后自离(约 1 分钟,防孤儿常驻)
@@ -49,20 +52,32 @@ HOVER_MS = 250 # 指针轮询周期
49
52
  HOVER_DELAY_MS = 350 # 悬停多久后才弹任务清单(扫过不弹)
50
53
 
51
54
 
55
+ def _safe_int(value, default=0, minimum=0):
56
+ """外部状态里的计数可能是空值或脏字符串;统一兜底并限制为非负数。"""
57
+ try:
58
+ number = int(value)
59
+ except (TypeError, ValueError, OverflowError):
60
+ number = int(default)
61
+ return max(minimum, number)
62
+
63
+
52
64
  def parse_snapshot(raw):
53
65
  """把 /api/pet_state 响应裁成蜜蜂关心的最小集。字段缺失一律兜底,不抛。"""
54
66
  raw = raw if isinstance(raw, dict) else {}
55
- st = raw.get("settings") or {}
67
+ st = raw.get("settings") if isinstance(raw.get("settings"), dict) else {}
68
+ workers = raw.get("workers") if isinstance(raw.get("workers"), dict) else {}
69
+ digest = raw.get("digest") if isinstance(raw.get("digest"), dict) else {}
70
+ task_rows = raw.get("tasks") if isinstance(raw.get("tasks"), list) else []
56
71
  tasks = []
57
- for t in (raw.get("tasks") or [])[:300]:
72
+ for t in task_rows[:300]:
58
73
  if not isinstance(t, dict):
59
74
  continue
60
75
  tasks.append({
61
76
  "id": str(t.get("id") or ""),
62
77
  "title": str(t.get("title") or t.get("id") or ""),
63
78
  "run_status": str(t.get("run_status") or ""),
64
- "steps_done": int(t.get("steps_done") or 0),
65
- "steps_total": int(t.get("steps_total") or 0),
79
+ "steps_done": _safe_int(t.get("steps_done")),
80
+ "steps_total": _safe_int(t.get("steps_total")),
66
81
  "step_current": str(t.get("step_current") or ""),
67
82
  "error": str(t.get("error") or ""),
68
83
  })
@@ -73,15 +88,14 @@ def parse_snapshot(raw):
73
88
  "pet_skin": str(st.get("pet_skin") or DEFAULT_SKIN),
74
89
  },
75
90
  "workers": {
76
- "running": int((raw.get("workers") or {}).get("running") or 0),
77
- "queued": int((raw.get("workers") or {}).get("queued") or 0),
91
+ "running": _safe_int(workers.get("running")),
92
+ "queued": _safe_int(workers.get("queued")),
78
93
  },
79
94
  "tasks": tasks,
80
95
  "digest": {
81
- "unseen": int((raw.get("digest") or {}).get("unseen") or 0),
82
- "latest": (raw.get("digest") or {}).get("latest")
83
- if isinstance((raw.get("digest") or {}).get("latest"), dict)
84
- else None,
96
+ "unseen": _safe_int(digest.get("unseen")),
97
+ "latest": digest.get("latest")
98
+ if isinstance(digest.get("latest"), dict) else None,
85
99
  },
86
100
  }
87
101
 
@@ -105,10 +119,12 @@ def derive_state(prev_active_ids, tasks):
105
119
  by_id = {t["id"]: t for t in tasks}
106
120
  for tid in prev_active_ids:
107
121
  t = by_id.get(tid)
108
- if t is None or t["run_status"] not in BAD_STATUSES:
109
- info["good"].append(t or {"id": tid, "title": tid})
110
- else:
122
+ if t is None:
123
+ continue
124
+ if t["run_status"] in BAD_STATUSES:
111
125
  info["bad"].append(t)
126
+ elif t["run_status"] in GOOD_STATUSES:
127
+ info["good"].append(t)
112
128
  if info["bad"]:
113
129
  return "alert", info
114
130
  if info["good"]:
@@ -276,7 +292,10 @@ def _http_json(port, path, body=None):
276
292
  method = "POST"
277
293
  conn.request(method, path, body=payload, headers=headers)
278
294
  resp = conn.getresponse()
279
- return json.loads(resp.read().decode("utf-8"))
295
+ data = resp.read()
296
+ if not 200 <= resp.status < 300:
297
+ raise RuntimeError("CodeBee API 返回 HTTP %d" % resp.status)
298
+ return json.loads(data.decode("utf-8"))
280
299
  finally:
281
300
  conn.close()
282
301
 
@@ -405,6 +424,14 @@ class PetApp:
405
424
  self.hop_until = 0.0 # 冒气泡时的蹦跶动作截止
406
425
  self.next_chatter = time.time() + random.uniform(90, 240)
407
426
  self.seen_digests = 0 # 本轮已通知过的摘要未读数(涨了才报,不重复轰炸)
427
+ self._polling = False
428
+ self._poll_results = queue.Queue(maxsize=1)
429
+ self._settings_lock = threading.Lock()
430
+ self._settings_pending = {}
431
+ self._settings_desired = {}
432
+ self._settings_confirmed = {}
433
+ self._settings_seq = 0
434
+ self._settings_worker_running = False
408
435
 
409
436
  if self.frames:
410
437
  self._build_sprite()
@@ -583,10 +610,7 @@ class PetApp:
583
610
  return
584
611
  self.skin = skin
585
612
  self._save_cfg(skin=skin)
586
- try:
587
- _http_json(self.port, "/api/settings", body={"pet_skin": skin})
588
- except Exception:
589
- pass
613
+ self._post_settings({"pet_skin": skin})
590
614
  self._rebuild_sprite()
591
615
 
592
616
  def _rebuild_sprite(self):
@@ -841,16 +865,92 @@ class PetApp:
841
865
  else:
842
866
  self._show([self.pbar_bg, self.pbar_fg], False)
843
867
 
844
- # ---- 数据轮询(4s 一拍)----
868
+ def _post_settings(self, body):
869
+ """合并并串行发送设置,保证快速连点时最后一次选择最终生效。"""
870
+ patch = dict(body or {})
871
+ if not patch:
872
+ return
873
+ with self._settings_lock:
874
+ self._settings_seq += 1
875
+ seq = self._settings_seq
876
+ for key, value in patch.items():
877
+ self._settings_pending[key] = (seq, value)
878
+ self._settings_desired[key] = (seq, value)
879
+ if self._settings_worker_running:
880
+ return
881
+ self._settings_worker_running = True
882
+
883
+ def send():
884
+ while True:
885
+ with self._settings_lock:
886
+ if not self._settings_pending:
887
+ self._settings_worker_running = False
888
+ return
889
+ current = dict(self._settings_pending)
890
+ self._settings_pending.clear()
891
+ try:
892
+ _http_json(self.port, "/api/settings",
893
+ body={key: item[1] for key, item in current.items()})
894
+ with self._settings_lock:
895
+ for key, item in current.items():
896
+ self._settings_confirmed[key] = max(
897
+ item[0], self._settings_confirmed.get(key, 0))
898
+ except Exception:
899
+ # 新值优先;失败的旧值只补回尚未被新点击覆盖的键。
900
+ with self._settings_lock:
901
+ for key, item in current.items():
902
+ self._settings_pending.setdefault(key, item)
903
+ time.sleep(1.0)
904
+ threading.Thread(target=send, name="pet-settings", daemon=True).start()
905
+
906
+ def _reconcile_desired_settings(self, snap, confirmed_at_poll_start):
907
+ """只覆盖写入确认前已发出的旧轮询;确认后的快照以服务端为准。"""
908
+ with self._settings_lock:
909
+ for key, item in list(self._settings_desired.items()):
910
+ seq, value = item
911
+ if confirmed_at_poll_start.get(key, 0) >= seq:
912
+ del self._settings_desired[key]
913
+ else:
914
+ snap["settings"][key] = value
915
+
916
+ # ---- 数据轮询(4s 一拍;网络请求不占 Tk 主线程)----
845
917
  def _tick(self):
846
918
  self._touch_lock()
847
- snap = None
919
+ if self._polling:
920
+ return
921
+ self._polling = True
922
+ with self._settings_lock:
923
+ confirmed_at_poll_start = dict(self._settings_confirmed)
924
+
925
+ def fetch():
926
+ snap = None
927
+ error = None
928
+ try:
929
+ snap = parse_snapshot(_http_json(self.port, "/api/pet_state"))
930
+ except Exception as exc:
931
+ error = exc
932
+ try:
933
+ self._poll_results.put_nowait(
934
+ (snap, error, confirmed_at_poll_start))
935
+ except queue.Full:
936
+ pass
937
+
938
+ threading.Thread(target=fetch, name="pet-poll", daemon=True).start()
939
+ self.root.after(50, self._collect_poll)
940
+
941
+ def _collect_poll(self):
848
942
  try:
849
- snap = parse_snapshot(_http_json(self.port, "/api/pet_state"))
850
- self.miss = 0
851
- except Exception:
943
+ snap, error, confirmed_at_poll_start = self._poll_results.get_nowait()
944
+ except queue.Empty:
945
+ self.root.after(50, self._collect_poll)
946
+ return
947
+ self._polling = False
948
+ if error is not None:
852
949
  self.miss += 1
950
+ else:
951
+ self.miss = 0
853
952
  if snap is not None:
953
+ self._reconcile_desired_settings(snap, confirmed_at_poll_start)
854
954
  if not snap["settings"]["pet_enabled"]:
855
955
  return self._bye()
856
956
  self.snap = snap
@@ -1187,10 +1287,7 @@ class PetApp:
1187
1287
  return m
1188
1288
 
1189
1289
  def _set_mode(self, mode):
1190
- try:
1191
- _http_json(self.port, "/api/settings", body={"pet_mode": mode})
1192
- except Exception:
1193
- pass
1290
+ self._post_settings({"pet_mode": mode})
1194
1291
 
1195
1292
  def _set_lang(self, lang):
1196
1293
  self.lang = lang
package/app/ui/app.js CHANGED
@@ -297,6 +297,93 @@ function urlAuth(u) {
297
297
  }
298
298
 
299
299
  /* ---------------------------------------------------------- 工具 */
300
+ let _requestBusyCount = 0;
301
+ let _lastActionButton = null;
302
+ let _lastActionAt = 0;
303
+ const _requestBusyButtons = new WeakMap();
304
+
305
+ /* 记录发起请求的按钮。写请求统一在 api() 里挂忙碌态,避免每个业务入口各写一套;
306
+ * 确认框的按钮不覆盖原始操作按钮,但会续期,使“确认后执行”仍反馈在原按钮上。 */
307
+ document.addEventListener("click", (e) => {
308
+ const btn = e.target && e.target.closest ? e.target.closest("button") : null;
309
+ if (!btn) return;
310
+ if (btn.classList.contains("request-busy")) {
311
+ e.preventDefault();
312
+ e.stopImmediatePropagation();
313
+ return;
314
+ }
315
+ _lastActionAt = performance.now();
316
+ if (!btn.closest("#ask")) _lastActionButton = btn;
317
+ }, true);
318
+
319
+ function requestBusyStart(opts) {
320
+ const method = String((opts && opts.method) || "GET").toUpperCase();
321
+ if (opts && opts.busy === false) return null;
322
+ if ((method === "GET" || method === "HEAD") && !(opts && opts.busy)) return null;
323
+
324
+ _requestBusyCount++;
325
+ let bar = $("request-progress");
326
+ if (!bar && document.body) {
327
+ bar = document.createElement("div");
328
+ bar.id = "request-progress";
329
+ bar.className = "request-progress";
330
+ bar.setAttribute("role", "status");
331
+ bar.setAttribute("aria-label", t("操作处理中"));
332
+ bar.setAttribute("aria-hidden", "true");
333
+ document.body.appendChild(bar);
334
+ }
335
+ if (bar) {
336
+ bar.classList.add("active");
337
+ bar.setAttribute("aria-hidden", "false");
338
+ }
339
+
340
+ let btn = opts && opts.busyElement;
341
+ if (typeof btn === "string") btn = document.querySelector(btn);
342
+ if (!btn && performance.now() - _lastActionAt < 2000) btn = _lastActionButton;
343
+ if (!btn || !document.documentElement.contains(btn)) btn = null;
344
+ if (btn) {
345
+ let state = _requestBusyButtons.get(btn);
346
+ if (!state) {
347
+ state = {
348
+ count: 0,
349
+ ariaBusy: btn.getAttribute("aria-busy"),
350
+ ariaDisabled: btn.getAttribute("aria-disabled"),
351
+ };
352
+ _requestBusyButtons.set(btn, state);
353
+ }
354
+ state.count++;
355
+ btn.classList.add("request-busy");
356
+ btn.setAttribute("aria-busy", "true");
357
+ btn.setAttribute("aria-disabled", "true");
358
+ }
359
+ return { button: btn };
360
+ }
361
+
362
+ function requestBusyEnd(token) {
363
+ if (!token) return;
364
+ _requestBusyCount = Math.max(0, _requestBusyCount - 1);
365
+ const btn = token.button;
366
+ const state = btn && _requestBusyButtons.get(btn);
367
+ if (state) {
368
+ state.count--;
369
+ if (state.count <= 0) {
370
+ btn.classList.remove("request-busy");
371
+ if (state.ariaBusy === null) btn.removeAttribute("aria-busy");
372
+ else btn.setAttribute("aria-busy", state.ariaBusy);
373
+ if (state.ariaDisabled === null) btn.removeAttribute("aria-disabled");
374
+ else btn.setAttribute("aria-disabled", state.ariaDisabled);
375
+ _requestBusyButtons.delete(btn);
376
+ }
377
+ }
378
+ if (_requestBusyCount === 0) {
379
+ const bar = $("request-progress");
380
+ if (bar) {
381
+ bar.classList.remove("active");
382
+ bar.setAttribute("aria-hidden", "true");
383
+ }
384
+ }
385
+ }
386
+
300
387
  async function api(path, opts) {
301
388
  opts = opts || {};
302
389
  // 可选超时:长请求(如外部插件下载)传 opts.timeout,网络卡死时也能
@@ -306,21 +393,30 @@ async function api(path, opts) {
306
393
  ctrl = new AbortController();
307
394
  var timer = setTimeout(() => ctrl.abort(), opts.timeout);
308
395
  }
309
- let res;
396
+ const busyToken = requestBusyStart(opts);
397
+ const fetchOpts = Object.assign({}, opts);
398
+ delete fetchOpts.timeout;
399
+ delete fetchOpts.busy;
400
+ delete fetchOpts.busyElement;
310
401
  try {
311
- res = await fetch(path, Object.assign({ headers: authHeaders() }, opts, ctrl ? { signal: ctrl.signal } : {}));
312
- } catch (e) {
313
- if (ctrl && e.name === "AbortError") throw new Error(t("请求超时,请重试或检查网络"));
314
- throw e;
402
+ let res;
403
+ try {
404
+ res = await fetch(path, Object.assign({ headers: authHeaders() }, fetchOpts,
405
+ ctrl ? { signal: ctrl.signal } : {}));
406
+ } catch (e) {
407
+ if (ctrl && e.name === "AbortError") throw new Error(t("请求超时,请重试或检查网络"));
408
+ throw e;
409
+ }
410
+ if (res.status === 401) { showTokenGate(t("令牌不正确或已更换,请重新输入")); throw new Error(t("需要访问令牌")); }
411
+ let data = null;
412
+ try { data = await res.json(); } catch (e) { /* ignore */ }
413
+ if (res.status === 423 && data && data.control) setControl(data.control);
414
+ if (!res.ok) throw new Error((data && data.error) || ("HTTP " + res.status));
415
+ return data;
315
416
  } finally {
316
417
  if (timer) clearTimeout(timer);
418
+ requestBusyEnd(busyToken);
317
419
  }
318
- if (res.status === 401) { showTokenGate(t("令牌不正确或已更换,请重新输入")); throw new Error(t("需要访问令牌")); }
319
- let data = null;
320
- try { data = await res.json(); } catch (e) { /* ignore */ }
321
- if (res.status === 423 && data && data.control) setControl(data.control);
322
- if (!res.ok) throw new Error((data && data.error) || ("HTTP " + res.status));
323
- return data;
324
420
  }
325
421
 
326
422
  /* 轻提示:3.5s 自动消失 */
@@ -783,7 +879,7 @@ async function ctrlClick() {
783
879
  function startCtrlHeartbeat() {
784
880
  setInterval(() => {
785
881
  if (S.control && S.control.mine) {
786
- api("/api/control/heartbeat", { method: "POST", body: "{}" })
882
+ api("/api/control/heartbeat", { method: "POST", body: "{}", busy: false })
787
883
  .then((d) => setControl(d.control)).catch(() => {});
788
884
  }
789
885
  }, 15000);
@@ -1687,7 +1783,7 @@ async function openImportDialog() {
1687
1783
  '<button class="ghost" onclick="closeModal()">' + t("取消") + '</button>');
1688
1784
  let data;
1689
1785
  try {
1690
- data = await api("/api/models/sources");
1786
+ data = await api("/api/models/sources", { busy: true });
1691
1787
  } catch (e) {
1692
1788
  $("modal-body").innerHTML = '<div class="msg bad">' + t("扫描失败:") + esc(e.message) + "</div>";
1693
1789
  return;
@@ -2586,7 +2682,7 @@ async function retryTask(id) {
2586
2682
  async function continueSerial(id) {
2587
2683
  let info;
2588
2684
  try {
2589
- info = await api("/api/tasks/" + encodeURIComponent(id) + "/continue-info");
2685
+ info = await api("/api/tasks/" + encodeURIComponent(id) + "/continue-info", { busy: true });
2590
2686
  } catch (e) { toast(t("无法续写:") + e.message, true); return; }
2591
2687
  if (!info || !info.can) {
2592
2688
  toast(t("无法续写:") + ((info && info.reason) || t("当前状态不支持")), true);
@@ -6378,6 +6474,7 @@ function bindVoiceInput() {
6378
6474
  if (e.results[i].isFinal) text += e.results[i][0].transcript;
6379
6475
  }
6380
6476
  if (text) {
6477
+ text = voiceFixup(text);
6381
6478
  ta.value = (ta.value ? ta.value.replace(/\s+$/, "") + " " : "") + text;
6382
6479
  ta.dispatchEvent(new Event("input", { bubbles: true }));
6383
6480
  }
@@ -6389,6 +6486,24 @@ function bindVoiceInput() {
6389
6486
  });
6390
6487
  }
6391
6488
 
6489
+ /* 语音识别结果纠偏(借鉴 lexicon 个人词典):设置里维护的专有名词表
6490
+ * (localStorage orch.voice_terms,一行一个「错误→正确」或直接「术语」),
6491
+ * 识别结果做逐词替换。人名/书名/项目名是识别高频错位,词表是最小可用解。 */
6492
+ function voiceFixup(text) {
6493
+ let terms = [];
6494
+ try {
6495
+ const raw = localStorage.getItem("orch.voice_terms") || "";
6496
+ terms = raw.split("\n").map((l) => l.trim()).filter(Boolean);
6497
+ } catch (e) {}
6498
+ for (const t of terms) {
6499
+ const m = t.split("→");
6500
+ if (m.length === 2 && m[0].trim() && m[1].trim()) {
6501
+ text = text.split(m[0].trim()).join(m[1].trim());
6502
+ }
6503
+ }
6504
+ return text;
6505
+ }
6506
+
6392
6507
  /* ---------------------------------------------------------- 外观:皮肤 + 明暗(换肤) */
6393
6508
  /* 调色板全部在 style.css(html[data-skin="X"],每套含夜间/日间两版变量);这里只放顺序
6394
6509
  * 与文案。卡片预览色块用 skinPalette 从 CSS 变量实时取值,不在 JS 里重复写色值——
@@ -6796,7 +6911,7 @@ function updateChip(c) {
6796
6911
  async function autoCheckUpdates() {
6797
6912
  if (Date.now() - (S.updateCheckAt || 0) < 5 * 60 * 1000) return;
6798
6913
  S.updateCheckAt = Date.now();
6799
- try { await api("/api/catalog/check-updates", { method: "POST" }); } catch (e) { /* 忽略 */ }
6914
+ try { await api("/api/catalog/check-updates", { method: "POST", busy: false }); } catch (e) { /* 忽略 */ }
6800
6915
  poll();
6801
6916
  }
6802
6917
 
@@ -6954,7 +7069,7 @@ async function autoRebindSoon() {
6954
7069
  if (!act) continue;
6955
7070
  const b = (S.bindings || {})[c.id] || {};
6956
7071
  try {
6957
- await api("/api/models/binding", { method: "POST", body: JSON.stringify({
7072
+ await api("/api/models/binding", { method: "POST", busy: false, body: JSON.stringify({
6958
7073
  agent_id: c.id, provider_id: act[0].p,
6959
7074
  chain: act.map((x) => ({ provider_id: x.p, model: x.m })),
6960
7075
  difficulty_routing: !!b.difficulty_routing }) });
@@ -8882,7 +8997,7 @@ function suLastUpgradeRun() {
8882
8997
 
8883
8998
  async function loadSelfupdate(force) {
8884
8999
  try {
8885
- SU = await api("/api/selfupdate" + (force ? "?force=1" : ""));
9000
+ SU = await api("/api/selfupdate" + (force ? "?force=1" : ""), { busy: !!force });
8886
9001
  } catch (e) { SU = null; }
8887
9002
  renderSu();
8888
9003
  maybeWhatsnew();
@@ -9198,6 +9313,7 @@ async function savePetSkin(skin) {
9198
9313
  async function exportDiagBundle() {
9199
9314
  const b = $("diag-export");
9200
9315
  if (b) b.disabled = true;
9316
+ const busyToken = requestBusyStart({ method: "GET", busy: true, busyElement: b });
9201
9317
  try {
9202
9318
  const r = await fetch("/api/diagnostics/bundle", { headers: authHeaders() });
9203
9319
  if (!r.ok) throw new Error("HTTP " + r.status);
@@ -9213,6 +9329,7 @@ async function exportDiagBundle() {
9213
9329
  } catch (e) {
9214
9330
  toast(t("诊断包导出失败:") + e.message, true);
9215
9331
  } finally {
9332
+ requestBusyEnd(busyToken);
9216
9333
  if (b) b.disabled = false;
9217
9334
  }
9218
9335
  }
@@ -9220,7 +9337,7 @@ async function exportDiagBundle() {
9220
9337
  /* 一键反馈 Issue:拉脱敏摘要 → 预填 GitHub Issue 新建页(用户亲手提交,不自动回传) */
9221
9338
  async function reportIssue() {
9222
9339
  try {
9223
- const s = await api("/api/diagnostics/issue-summary");
9340
+ const s = await api("/api/diagnostics/issue-summary", { busy: true });
9224
9341
  const url = "https://github.com/Vercel-By-WXP/CodeBee/issues/new" +
9225
9342
  "?title=" + encodeURIComponent(s.title || "") +
9226
9343
  "&body=" + encodeURIComponent(s.body || "");
package/app/ui/i18n.js CHANGED
@@ -1273,6 +1273,7 @@
1273
1273
  "安装中…": "Installing…",
1274
1274
  "正在下载安装,插件包较大时需要约一分钟…": "Downloading and installing — large plugin packages can take about a minute…",
1275
1275
  "请求超时,请重试或检查网络": "Request timed out — try again or check your network",
1276
+ "操作处理中": "Working on it",
1276
1277
  "该插件含脚本/钩子/MCP 组件,仅支持纯技能类插件": "Contains scripts/hooks/MCP components — only pure skill plugins are supported",
1277
1278
  " 个供应商(google 等协议)仅登记,不支持注入 CLI,未出现在上面的下拉中。": " provider(s) (google etc.) are registry-only, can't inject into CLIs, and are hidden from the dropdown above.",
1278
1279
  " 个降级备选)。": " fallback(s)).",