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.
- package/CHANGELOG.md +171 -171
- package/README.md +31 -6
- package/app/core/knowledge.py +19 -7
- package/app/core/pipeline.py +13 -1
- package/app/core/router.py +39 -5
- package/app/core/skills.py +15 -54
- package/app/core/task_compile.py +83 -0
- package/app/pet.py +125 -28
- package/app/ui/app.js +135 -18
- package/app/ui/i18n.js +1 -0
- package/app/ui/style.css +30 -0
- package/package.json +1 -1
package/app/core/skills.py
CHANGED
|
@@ -390,52 +390,14 @@ def relevance_top(lessons, task, limit):
|
|
|
390
390
|
if not probe:
|
|
391
391
|
return lessons[:limit]
|
|
392
392
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
|
485
|
-
|
|
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
|
-
|
|
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")
|
|
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
|
|
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":
|
|
65
|
-
"steps_total":
|
|
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":
|
|
77
|
-
"queued":
|
|
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":
|
|
82
|
-
"latest":
|
|
83
|
-
if isinstance(
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
850
|
-
|
|
851
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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)).",
|