codebee 0.1.28 → 0.1.30
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 +23 -0
- package/README.md +22 -0
- package/app/core/builtin_agent.py +36 -2
- package/app/core/dispatch.py +3 -1
- package/app/core/flows.py +1 -1
- package/app/core/jobs.py +23 -0
- package/app/core/modelhub.py +6 -0
- package/app/core/pipeline.py +118 -26
- package/app/core/planner.py +54 -17
- package/app/core/router.py +16 -9
- package/app/core/runner.py +64 -14
- package/app/core/selfupdate.py +26 -12
- package/app/core/store.py +61 -13
- package/app/core/task_compile.py +111 -0
- package/app/core/usage.py +55 -14
- package/app/core/wxdigest.py +140 -0
- package/app/main.py +28 -1
- package/app/pet.py +95 -58
- package/app/ui/app.js +209 -11
- package/app/ui/i18n.js +35 -0
- package/app/ui/index.html +28 -7
- package/app/ui/style.css +62 -0
- package/package.json +1 -1
package/app/core/wxdigest.py
CHANGED
|
@@ -20,6 +20,7 @@ import json
|
|
|
20
20
|
import logging
|
|
21
21
|
import os
|
|
22
22
|
import re
|
|
23
|
+
import subprocess
|
|
23
24
|
import threading
|
|
24
25
|
import time
|
|
25
26
|
from datetime import datetime, timedelta
|
|
@@ -708,3 +709,142 @@ def _test_reset():
|
|
|
708
709
|
_STATE["groups_cache"] = []
|
|
709
710
|
_STATE["last_scan"] = _STATE["next_scan"] = _STATE["last_error"] = ""
|
|
710
711
|
globals()["_LOADED"] = True
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
# ---- 64 位 Python 自动扫描 + wechatauto-replica 一键安装(2026-09-21 用户诉求:
|
|
715
|
+
# 别让用户手填解释器路径、别让用户自己开 conda 装依赖)----
|
|
716
|
+
|
|
717
|
+
_PY_SCAN = {"scanning": False, "done": True, "results": []}
|
|
718
|
+
_PY_INSTALL = {"running": False, "ok": None, "python": "", "log": []}
|
|
719
|
+
_REPLICA_PKG = "wechatauto-replica"
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _candidate_pythons():
|
|
723
|
+
"""收集本机候选解释器路径(去重、存在性过滤)。只列不判,位数/依赖交给探测。"""
|
|
724
|
+
import glob
|
|
725
|
+
cands = []
|
|
726
|
+
|
|
727
|
+
def _add(p):
|
|
728
|
+
p = str(p).strip().strip('"')
|
|
729
|
+
if p and os.path.isfile(p) and p.lower().endswith((".exe",)) is not False:
|
|
730
|
+
if p not in cands and "python" in os.path.basename(p).lower():
|
|
731
|
+
cands.append(p)
|
|
732
|
+
|
|
733
|
+
if os.name == "nt":
|
|
734
|
+
home = os.environ.get("USERPROFILE") or ""
|
|
735
|
+
patterns = [
|
|
736
|
+
os.path.join(home, "anaconda3", "envs", "*", "python.exe"),
|
|
737
|
+
os.path.join(home, "miniconda3", "envs", "*", "python.exe"),
|
|
738
|
+
os.path.join(home, ".conda", "envs", "*", "python.exe"),
|
|
739
|
+
os.path.join(os.environ.get("LOCALAPPDATA") or "", "Programs",
|
|
740
|
+
"Python", "*", "python.exe"),
|
|
741
|
+
r"C:\Python*\python.exe",
|
|
742
|
+
os.path.join(home, "AppData", "Local", "anaconda3", "python.exe"),
|
|
743
|
+
os.path.join(home, "anaconda3", "python.exe"),
|
|
744
|
+
os.path.join(home, "miniconda3", "python.exe"),
|
|
745
|
+
]
|
|
746
|
+
for pat in patterns:
|
|
747
|
+
for p in glob.glob(pat):
|
|
748
|
+
_add(p)
|
|
749
|
+
try:
|
|
750
|
+
r = subprocess.run(["cmd", "/c", "where", "python"],
|
|
751
|
+
capture_output=True, timeout=15)
|
|
752
|
+
for ln in r.stdout.decode("utf-8", "replace").splitlines():
|
|
753
|
+
_add(ln)
|
|
754
|
+
except Exception:
|
|
755
|
+
pass
|
|
756
|
+
else:
|
|
757
|
+
import glob as _g
|
|
758
|
+
for pat in ("/usr/bin/python3*", "/usr/local/bin/python3*",
|
|
759
|
+
os.path.expanduser("~/.conda/envs/*/bin/python3"),
|
|
760
|
+
os.path.expanduser("~/miniconda3/envs/*/bin/python3"),
|
|
761
|
+
os.path.expanduser("~/anaconda3/envs/*/bin/python3")):
|
|
762
|
+
for p in _g.glob(pat):
|
|
763
|
+
_add(p)
|
|
764
|
+
return cands[:24]
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _probe_python(py):
|
|
768
|
+
"""探测解释器:返回 {path, version, bits} 或 None(跑不动/不是 python)。"""
|
|
769
|
+
try:
|
|
770
|
+
r = subprocess.run(
|
|
771
|
+
[py, "-c", "import sys,struct;print('%d.%d'%sys.version_info[:2],"
|
|
772
|
+
"struct.calcsize('P')*8)"],
|
|
773
|
+
capture_output=True, timeout=20,
|
|
774
|
+
creationflags=CREATE_NO_WINDOW if os.name == "nt" else 0)
|
|
775
|
+
out = (r.stdout or b"").decode("utf-8", "replace").strip()
|
|
776
|
+
ver, bits = out.split()
|
|
777
|
+
return {"path": py, "version": ver, "bits": int(bits)}
|
|
778
|
+
except Exception:
|
|
779
|
+
return None
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def _has_replica(py):
|
|
783
|
+
"""该环境是否已装 wechatauto-replica(pip show 探测,失败视为未装)。"""
|
|
784
|
+
try:
|
|
785
|
+
r = subprocess.run([py, "-m", "pip", "show", _REPLICA_PKG],
|
|
786
|
+
capture_output=True, timeout=60,
|
|
787
|
+
creationflags=CREATE_NO_WINDOW if os.name == "nt" else 0)
|
|
788
|
+
return r.returncode == 0
|
|
789
|
+
except Exception:
|
|
790
|
+
return False
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def scan_pythons():
|
|
794
|
+
"""启动后台扫描(幂等:扫描中重复调用直接忽略)。返回当前状态。"""
|
|
795
|
+
if _PY_SCAN["scanning"]:
|
|
796
|
+
return dict(_PY_SCAN)
|
|
797
|
+
|
|
798
|
+
def _run():
|
|
799
|
+
_PY_SCAN.update(scanning=True, done=False, results=[])
|
|
800
|
+
found = []
|
|
801
|
+
for py in _candidate_pythons():
|
|
802
|
+
info = _probe_python(py)
|
|
803
|
+
if not info:
|
|
804
|
+
continue
|
|
805
|
+
info["has_replica"] = _has_replica(py)
|
|
806
|
+
found.append(info)
|
|
807
|
+
# 排序:64 位优先 → 已装 replica 优先 → 版本新优先 → 路径短优先
|
|
808
|
+
found.sort(key=lambda x: (x["bits"] < 64, not x["has_replica"],
|
|
809
|
+
x["version"], len(x["path"])), reverse=False)
|
|
810
|
+
found.sort(key=lambda x: (x["bits"] >= 64, x["has_replica"]))
|
|
811
|
+
_PY_SCAN.update(scanning=False, done=True, results=found)
|
|
812
|
+
|
|
813
|
+
threading.Thread(target=_run, name="wx-py-scan", daemon=True).start()
|
|
814
|
+
return dict(_PY_SCAN)
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def pythons_status():
|
|
818
|
+
return dict(_PY_SCAN)
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def install_replica(py):
|
|
822
|
+
"""一键安装 wechatauto-replica 到指定解释器(后台 pip,状态可轮询)。"""
|
|
823
|
+
py = str(py or "").strip()
|
|
824
|
+
if not py or not os.path.isfile(py):
|
|
825
|
+
return {"ok": False, "error": "解释器路径无效"}
|
|
826
|
+
if _PY_INSTALL["running"]:
|
|
827
|
+
return {"ok": False, "error": "已有安装在跑,请等它结束"}
|
|
828
|
+
|
|
829
|
+
def _run():
|
|
830
|
+
_PY_INSTALL.update(running=True, ok=None, python=py, log=[])
|
|
831
|
+
try:
|
|
832
|
+
r = subprocess.run(
|
|
833
|
+
[py, "-m", "pip", "install", _REPLICA_PKG],
|
|
834
|
+
capture_output=True, timeout=900,
|
|
835
|
+
creationflags=CREATE_NO_WINDOW if os.name == "nt" else 0)
|
|
836
|
+
ok = r.returncode == 0
|
|
837
|
+
tail = ((r.stderr or b"").decode("utf-8", "replace")
|
|
838
|
+
or (r.stdout or b"").decode("utf-8", "replace"))[-1500:]
|
|
839
|
+
except Exception as e:
|
|
840
|
+
ok, tail = False, str(e)[:500]
|
|
841
|
+
_PY_INSTALL.update(running=False, ok=ok, log=tail.strip().splitlines()[-25:])
|
|
842
|
+
if ok:
|
|
843
|
+
scan_pythons() # 装完自动重扫,has_replica 标记即时生效
|
|
844
|
+
|
|
845
|
+
threading.Thread(target=_run, name="wx-replica-install", daemon=True).start()
|
|
846
|
+
return {"ok": True}
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def install_status():
|
|
850
|
+
return dict(_PY_INSTALL)
|
package/app/main.py
CHANGED
|
@@ -52,6 +52,7 @@ def _utf8_bytes(data):
|
|
|
52
52
|
return data.decode("utf-8", "replace").encode("utf-8")
|
|
53
53
|
|
|
54
54
|
PORT = 8765 # main() 启动时更新;/api/connect 组装扫码地址用
|
|
55
|
+
_BOOT_TS = time.time() # 服务进程启动时间戳:/api/pet_state 下发,蜜蜂据此识别「服务换人了」并让位
|
|
55
56
|
|
|
56
57
|
# 写接口统一限制 JSON 请求体,避免误传文件或异常客户端把 worker 线程和
|
|
57
58
|
# 内存拖垮。16 MiB 足够覆盖任务上下文、故事圣经和附件清单(附件本体走
|
|
@@ -283,7 +284,12 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
283
284
|
edays = max(1, min(3650, int((q.get("days") or ["90"])[0])))
|
|
284
285
|
except ValueError:
|
|
285
286
|
edays = 90
|
|
286
|
-
|
|
287
|
+
mode = (q.get("mode") or ["auto"])[0][:16]
|
|
288
|
+
thinking = (q.get("thinking") or ["standard"])[0][:16]
|
|
289
|
+
rounds = (q.get("rounds") or [None])[0]
|
|
290
|
+
return self._json(200, usage.estimate(
|
|
291
|
+
task_type=ttype, days=edays, mode=mode,
|
|
292
|
+
thinking=thinking, rounds=rounds))
|
|
287
293
|
if path == "/api/dispatch/replay":
|
|
288
294
|
from core import dispatch_log
|
|
289
295
|
q = parse_qs(urlparse(self.path).query)
|
|
@@ -312,6 +318,13 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
312
318
|
# 偏好记忆:上次创建任务的类型/参数(新建表单预填用)
|
|
313
319
|
from core import prefs
|
|
314
320
|
return self._json(200, {"prefs": prefs.load()})
|
|
321
|
+
if path == "/api/wxdigest/pythons":
|
|
322
|
+
# 64 位 Python 候选(蜜蜂坞微信直连配置):扫描后台跑,此端点轮询
|
|
323
|
+
from core import wxdigest
|
|
324
|
+
return self._json(200, wxdigest.pythons_status())
|
|
325
|
+
if path == "/api/wxdigest/replica/status":
|
|
326
|
+
from core import wxdigest
|
|
327
|
+
return self._json(200, wxdigest.install_status())
|
|
315
328
|
if path == "/api/ports":
|
|
316
329
|
# 端口占用诊断(借鉴 leftopen):谁在听、PID/进程/项目归属、
|
|
317
330
|
# 是否仅本机。?port=N 只看单端口。只读,不碰任何进程。
|
|
@@ -1079,6 +1092,15 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1079
1092
|
from core import wxdigest
|
|
1080
1093
|
res = wxdigest.scan_now() # 含模型调用,同步做(ThreadingHTTPServer 不堵别的请求)
|
|
1081
1094
|
return self._json(200, dict(res, ok=bool(res.get("ok"))))
|
|
1095
|
+
if path == "/api/wxdigest/pythons/scan":
|
|
1096
|
+
# 64 位 Python 自动扫描(后台线程,GET /api/wxdigest/pythons 轮询)
|
|
1097
|
+
from core import wxdigest
|
|
1098
|
+
return self._json(200, wxdigest.scan_pythons())
|
|
1099
|
+
if path == "/api/wxdigest/replica/install":
|
|
1100
|
+
# 一键安装 wechatauto-replica 到指定解释器(后台 pip,状态可轮询)
|
|
1101
|
+
from core import wxdigest
|
|
1102
|
+
res = wxdigest.install_replica(self._body().get("python"))
|
|
1103
|
+
return self._json(200 if res.get("ok") else 400, res)
|
|
1082
1104
|
if path == "/api/wxdigest/seen":
|
|
1083
1105
|
from core import wxdigest
|
|
1084
1106
|
return self._json(200, {"ok": True, "view": wxdigest.seen_clear()})
|
|
@@ -1468,6 +1490,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1468
1490
|
「刚完工/刚出事」。设置随响应回传,蜜蜂轮询到 pet_enabled=false 自行
|
|
1469
1491
|
退出——关闭桌宠不需要专门的信号通道。
|
|
1470
1492
|
"""
|
|
1493
|
+
global _BOOT_TS
|
|
1471
1494
|
st = settings.load()
|
|
1472
1495
|
rows = []
|
|
1473
1496
|
n_run = n_q = 0
|
|
@@ -1500,6 +1523,10 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1500
1523
|
dg = {"unseen": 0, "latest": None}
|
|
1501
1524
|
return self._json(200, {
|
|
1502
1525
|
"ok": True, "ts": int(time.time()), "port": PORT,
|
|
1526
|
+
# boot:本服务进程的启动时间戳。蜜蜂记住首次见到的值,值变了说明
|
|
1527
|
+
# 服务换了个进程(升级重启)——旧蜜蜂自行退场让新服务 spawn 新蜜蜂
|
|
1528
|
+
# (旧形象常驻的根因:同端口轮询永远正常,旧进程从不自离)。
|
|
1529
|
+
"boot": _BOOT_TS,
|
|
1503
1530
|
"settings": {"pet_enabled": bool(st.get("pet_enabled", True)),
|
|
1504
1531
|
"pet_mode": str(st.get("pet_mode") or "always"),
|
|
1505
1532
|
"pet_skin": str(st.get("pet_skin") or "plush")},
|
package/app/pet.py
CHANGED
|
@@ -82,6 +82,8 @@ def parse_snapshot(raw):
|
|
|
82
82
|
"error": str(t.get("error") or ""),
|
|
83
83
|
})
|
|
84
84
|
return {
|
|
85
|
+
# boot:服务进程启动标识(值变化=服务换人,本宠让位给新服务的蜜蜂)
|
|
86
|
+
"boot": raw.get("boot") or None,
|
|
85
87
|
"settings": {
|
|
86
88
|
"pet_enabled": bool(st.get("pet_enabled", True)),
|
|
87
89
|
"pet_mode": str(st.get("pet_mode") or "always"),
|
|
@@ -166,6 +168,7 @@ def _short(text, limit=22):
|
|
|
166
168
|
LANG = {
|
|
167
169
|
"zh": {
|
|
168
170
|
"open": "打开 CodeBee",
|
|
171
|
+
"digest": "群摘要",
|
|
169
172
|
"mode": "显示模式",
|
|
170
173
|
"always": "常驻显示",
|
|
171
174
|
"tasks_only": "仅任务运行时出现",
|
|
@@ -195,6 +198,7 @@ LANG = {
|
|
|
195
198
|
},
|
|
196
199
|
"en": {
|
|
197
200
|
"open": "Open CodeBee",
|
|
201
|
+
"digest": "Group Digests",
|
|
198
202
|
"mode": "Display mode",
|
|
199
203
|
"always": "Always visible",
|
|
200
204
|
"tasks_only": "Only when tasks run",
|
|
@@ -309,7 +313,6 @@ KEY_RGB = (1, 2, 3)
|
|
|
309
313
|
WIN_W, WIN_H = 170, 150 # 手绘回落模式的窗口尺寸
|
|
310
314
|
SPRITE_DIR = Path(__file__).resolve().parent
|
|
311
315
|
SPRITE_DISP_H = 112 # 精灵显示高度(宽等比)
|
|
312
|
-
PROG_H = 16 # 底部进度条预留高度(有进度时才显示)
|
|
313
316
|
|
|
314
317
|
|
|
315
318
|
def _pid_alive(pid):
|
|
@@ -371,6 +374,9 @@ _BROWSER_EXES = {
|
|
|
371
374
|
}
|
|
372
375
|
|
|
373
376
|
|
|
377
|
+
petWindow = None
|
|
378
|
+
|
|
379
|
+
|
|
374
380
|
def _window_process_name(hwnd):
|
|
375
381
|
"""取顶层窗口所属进程的可执行文件名(小写);失败返回空串。"""
|
|
376
382
|
try:
|
|
@@ -400,10 +406,8 @@ def _window_process_name(hwnd):
|
|
|
400
406
|
return ""
|
|
401
407
|
|
|
402
408
|
|
|
403
|
-
def
|
|
404
|
-
"""
|
|
405
|
-
避免每点一次桌宠就新开一个浏览器窗口。返回 True 表示已找到并尝试置前;
|
|
406
|
-
False 表示没有现成窗口,调用方应回退 webbrowser.open 新开。"""
|
|
409
|
+
def _find_codebee_window(port, ignore_hwnd=None):
|
|
410
|
+
"""查找当前端口对应的 CodeBee 浏览器顶层窗口,找不到返回 None。"""
|
|
407
411
|
try:
|
|
408
412
|
import ctypes
|
|
409
413
|
from ctypes import wintypes
|
|
@@ -422,22 +426,55 @@ def _focus_codebee_window(ignore_hwnd=None):
|
|
|
422
426
|
return True
|
|
423
427
|
buf = ctypes.create_unicode_buffer(length + 1)
|
|
424
428
|
user32.GetWindowTextW(hwnd, buf, length + 1)
|
|
425
|
-
|
|
429
|
+
title = buf.value
|
|
430
|
+
# 页面启动时把端口写入 document.title。只匹配当前服务端口,避免
|
|
431
|
+
# 把标题里碰巧含 CodeBee 的文档/仓库页或另一套本地实例抢到前台。
|
|
432
|
+
if "CodeBee" in title and ("[%s]" % int(port)) in title:
|
|
426
433
|
found.append(hwnd)
|
|
427
434
|
return True
|
|
428
435
|
|
|
429
436
|
user32.EnumWindows(EnumWindowsProc(_cb), 0)
|
|
430
437
|
if not found:
|
|
438
|
+
return None
|
|
439
|
+
return found[0]
|
|
440
|
+
except Exception:
|
|
441
|
+
return None
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _show_pet_window(port, ignore_hwnd=None):
|
|
445
|
+
"""复用桌宠拉起的 CodeBee 浏览器窗口;窗口已关闭时返回 False。"""
|
|
446
|
+
global petWindow
|
|
447
|
+
try:
|
|
448
|
+
if os.name != "nt":
|
|
449
|
+
return False
|
|
450
|
+
import ctypes
|
|
451
|
+
user32 = ctypes.windll.user32
|
|
452
|
+
if petWindow and user32.IsWindow(petWindow):
|
|
453
|
+
if user32.IsIconic(petWindow):
|
|
454
|
+
user32.ShowWindow(petWindow, 9) # SW_RESTORE
|
|
455
|
+
user32.SetForegroundWindow(petWindow)
|
|
456
|
+
return True
|
|
457
|
+
petWindow = None
|
|
458
|
+
hwnd = _find_codebee_window(port, ignore_hwnd)
|
|
459
|
+
if not hwnd:
|
|
431
460
|
return False
|
|
432
|
-
|
|
433
|
-
if user32.IsIconic(
|
|
434
|
-
user32.ShowWindow(
|
|
435
|
-
user32.SetForegroundWindow(
|
|
461
|
+
petWindow = hwnd
|
|
462
|
+
if user32.IsIconic(hwnd):
|
|
463
|
+
user32.ShowWindow(hwnd, 9)
|
|
464
|
+
user32.SetForegroundWindow(hwnd)
|
|
436
465
|
return True
|
|
437
466
|
except Exception:
|
|
438
467
|
return False
|
|
439
468
|
|
|
440
469
|
|
|
470
|
+
def openPetWindow(port, ignore_hwnd=None, url=None):
|
|
471
|
+
"""打开 CodeBee 网页:Windows 先置前已打开的窗口(用户拍板「已打开就不重复
|
|
472
|
+
开」),找不到才用默认浏览器新开。url 可带锚点(如 #bee=1 直达蜜蜂坞)。"""
|
|
473
|
+
if os.name == "nt" and _show_pet_window(port, ignore_hwnd):
|
|
474
|
+
return
|
|
475
|
+
webbrowser.open(url or ("http://127.0.0.1:%d" % port))
|
|
476
|
+
|
|
477
|
+
|
|
441
478
|
class PetApp:
|
|
442
479
|
"""蜜蜂窗口:蜜蜂本体(精灵/手绘)+ 状态机动画 + 悬停任务清单 + 右键菜单。"""
|
|
443
480
|
|
|
@@ -485,6 +522,7 @@ class PetApp:
|
|
|
485
522
|
# 运行态
|
|
486
523
|
self.snap = None
|
|
487
524
|
self.miss = 0
|
|
525
|
+
self.boot = None # 首次见到的服务 boot 标识;变化=服务换进程,本宠让位
|
|
488
526
|
self.prev_active = set()
|
|
489
527
|
self.mood_kind, self.mood_until = "", 0.0
|
|
490
528
|
self.last_busy = time.time()
|
|
@@ -592,7 +630,26 @@ class PetApp:
|
|
|
592
630
|
try:
|
|
593
631
|
from PIL import Image, ImageEnhance, ImageTk
|
|
594
632
|
except Exception:
|
|
595
|
-
|
|
633
|
+
# 无 Pillow:用 Tk 原生 PhotoImage 直接贴 PNG(Tk 8.6+ 原生解码,
|
|
634
|
+
# 零依赖)——此前直接回落手绘蜂,普通用户 npm 装完没有 Pillow,
|
|
635
|
+
# 看到的永远是手绘丑蜂(同事「还是老的丑蜜蜂」的真凶)。
|
|
636
|
+
# 限制:无平滑缩放/旋转,动画帧退化为静态贴图 + 位移点缀。
|
|
637
|
+
fname = SKINS.get(skin, SKINS[DEFAULT_SKIN])[0]
|
|
638
|
+
try:
|
|
639
|
+
img = self.tk.PhotoImage(file=str(SPRITE_DIR / fname))
|
|
640
|
+
except Exception:
|
|
641
|
+
return None
|
|
642
|
+
h0 = max(1, img.height())
|
|
643
|
+
disp_h = max(60, int(SPRITE_DISP_H * SIZES.get(self.size_key, 1.0)))
|
|
644
|
+
scale = disp_h / h0
|
|
645
|
+
if scale < 0.9:
|
|
646
|
+
k = max(1, round(1 / scale))
|
|
647
|
+
img = img.subsample(k, k)
|
|
648
|
+
elif scale > 1.1:
|
|
649
|
+
k = max(1, round(scale))
|
|
650
|
+
img = img.zoom(k, k)
|
|
651
|
+
return {"alert": [img], "sleep": [img], "dead": [img],
|
|
652
|
+
"work": [img], "cheer": [img]}
|
|
596
653
|
fname = SKINS.get(skin, SKINS[DEFAULT_SKIN])[0]
|
|
597
654
|
try:
|
|
598
655
|
base0 = Image.open(SPRITE_DIR / fname).convert("RGBA")
|
|
@@ -638,18 +695,19 @@ class PetApp:
|
|
|
638
695
|
def _size_for(self, frames):
|
|
639
696
|
fw = max(f.width() for lst in frames.values() for f in lst)
|
|
640
697
|
fh = max(f.height() for lst in frames.values() for f in lst)
|
|
641
|
-
return int(fw) + 8, int(fh) +
|
|
698
|
+
return int(fw) + 8, int(fh) + 4
|
|
642
699
|
|
|
643
700
|
# ---- 蜜蜂绘制(精灵模式:贴图 + 状态点缀画件)----
|
|
644
|
-
#
|
|
645
|
-
#
|
|
701
|
+
# 用户拍板去掉:右上徽章、左侧速度线、底部影子(真机截图圈删)、
|
|
702
|
+
# 底部任务进度条(2026-09-21 真机截图圈删)——
|
|
703
|
+
# 窗口贴着蜂体开,点缀只留睡觉 Zzz / 庆祝星光。
|
|
646
704
|
def _build_sprite(self):
|
|
647
705
|
cv = self.cv
|
|
648
|
-
cx, cy = self.win_w // 2,
|
|
706
|
+
cx, cy = self.win_w // 2, self.win_h // 2 + 2
|
|
649
707
|
self._spr_c = (cx, cy)
|
|
650
708
|
self.spr = cv.create_image(cx, cy, image=self.frames["alert"][0],
|
|
651
709
|
anchor="center")
|
|
652
|
-
hw, hh = self.win_w / 2,
|
|
710
|
+
hw, hh = self.win_w / 2, self.win_h / 2
|
|
653
711
|
self.zzz = [cv.create_text(cx + hw - 12, cy - hh + 14, text="z",
|
|
654
712
|
fill="#8A93A6",
|
|
655
713
|
font=("Segoe UI", 11, "italic bold")),
|
|
@@ -671,15 +729,6 @@ class PetApp:
|
|
|
671
729
|
for grp in (self.zzz, self.spark):
|
|
672
730
|
for i in grp:
|
|
673
731
|
cv.itemconfigure(i, state="hidden")
|
|
674
|
-
# 底部进度条(工作且有步骤进度才出现)
|
|
675
|
-
self.pbar_bg = cv.create_rectangle(cx - 34, self.win_h - 7,
|
|
676
|
-
cx + 34, self.win_h - 2,
|
|
677
|
-
fill="#202A36", outline="#3A4654")
|
|
678
|
-
self.pbar_fg = cv.create_rectangle(cx - 33, self.win_h - 6,
|
|
679
|
-
cx - 32, self.win_h - 3,
|
|
680
|
-
fill="#7ED07E", outline="")
|
|
681
|
-
cv.itemconfigure(self.pbar_bg, state="hidden")
|
|
682
|
-
cv.itemconfigure(self.pbar_fg, state="hidden")
|
|
683
732
|
self.ox = self.oy = 0.0
|
|
684
733
|
self._t0 = time.time()
|
|
685
734
|
|
|
@@ -867,14 +916,6 @@ class PetApp:
|
|
|
867
916
|
self._palette = {i: cv.itemcget(i, "fill")
|
|
868
917
|
for i in (self.body, self.belly)}
|
|
869
918
|
|
|
870
|
-
# —— 贴地件:进度条(用户拍板去掉影子)——
|
|
871
|
-
self.pbar_bg = cv.create_rectangle(46, 139, 128, 147, fill="#202A36",
|
|
872
|
-
outline="#3A4654")
|
|
873
|
-
self.pbar_fg = cv.create_rectangle(47, 140, 48, 146, fill="#7ED07E",
|
|
874
|
-
outline="")
|
|
875
|
-
cv.itemconfigure(self.pbar_bg, state="hidden")
|
|
876
|
-
cv.itemconfigure(self.pbar_fg, state="hidden")
|
|
877
|
-
|
|
878
919
|
# 动画游标
|
|
879
920
|
self.ox = self.oy = 0.0
|
|
880
921
|
self._t0 = time.time()
|
|
@@ -923,26 +964,9 @@ class PetApp:
|
|
|
923
964
|
cv.itemconfigure(self.body, fill=self._palette[self.body])
|
|
924
965
|
cv.itemconfigure(self.belly, fill=self._palette[self.belly])
|
|
925
966
|
cv.itemconfigure(self.gloss, state="normal")
|
|
926
|
-
# —— 共同:Zzz /
|
|
967
|
+
# —— 共同:Zzz / 星光(徽章、速度线、进度条均已按用户要求移除)——
|
|
927
968
|
self._show(self.zzz, False)
|
|
928
969
|
self._show(self.spark, False)
|
|
929
|
-
prog = []
|
|
930
|
-
if st == "work" and self.snap:
|
|
931
|
-
prog = [t for t in self.snap["tasks"]
|
|
932
|
-
if t["run_status"] == "running" and t["steps_total"]]
|
|
933
|
-
if prog:
|
|
934
|
-
done = sum(t["steps_done"] for t in prog)
|
|
935
|
-
total = sum(t["steps_total"] for t in prog)
|
|
936
|
-
ratio = max(0.0, min(1.0, done / float(total)))
|
|
937
|
-
x0 = (self.win_w // 2) - 34
|
|
938
|
-
cv.itemconfigure(self.pbar_bg, state="normal")
|
|
939
|
-
cv.itemconfigure(self.pbar_fg, state="normal")
|
|
940
|
-
cv.coords(self.pbar_bg, x0, self.win_h - 7, x0 + 68,
|
|
941
|
-
self.win_h - 2)
|
|
942
|
-
cv.coords(self.pbar_fg, x0 + 1, self.win_h - 6,
|
|
943
|
-
x0 + 1 + 66 * ratio, self.win_h - 3)
|
|
944
|
-
else:
|
|
945
|
-
self._show([self.pbar_bg, self.pbar_fg], False)
|
|
946
970
|
|
|
947
971
|
def _post_settings(self, body):
|
|
948
972
|
"""合并并串行发送设置,保证快速连点时最后一次选择最终生效。"""
|
|
@@ -1029,6 +1053,15 @@ class PetApp:
|
|
|
1029
1053
|
else:
|
|
1030
1054
|
self.miss = 0
|
|
1031
1055
|
if snap is not None:
|
|
1056
|
+
# 服务换人检测(升级重启同端口场景):boot 与首次见到的不一致,
|
|
1057
|
+
# 说明响应来自新进程——本进程是旧代码的遗老,立即让位,新服务的
|
|
1058
|
+
# 看护会 spawn 带新形象的新蜜蜂(旧形象常驻的根因修复)。
|
|
1059
|
+
boot = snap.get("boot")
|
|
1060
|
+
if boot:
|
|
1061
|
+
if self.boot is None:
|
|
1062
|
+
self.boot = boot
|
|
1063
|
+
elif boot != self.boot:
|
|
1064
|
+
return self._bye()
|
|
1032
1065
|
self._reconcile_desired_settings(snap, confirmed_at_poll_start)
|
|
1033
1066
|
if not snap["settings"]["pet_enabled"]:
|
|
1034
1067
|
return self._bye()
|
|
@@ -1330,6 +1363,7 @@ class PetApp:
|
|
|
1330
1363
|
self._lang_var.set(self.lang)
|
|
1331
1364
|
m = self.tk.Menu(self.root, tearoff=0)
|
|
1332
1365
|
m.add_command(label=self._L("open"), command=self._open_ui)
|
|
1366
|
+
m.add_command(label=self._L("digest"), command=self._open_digest)
|
|
1333
1367
|
m.add_separator()
|
|
1334
1368
|
m.add_command(label="◎ " + self._L("mode"), state="disabled")
|
|
1335
1369
|
m.add_radiobutton(label=" " + self._L("always"),
|
|
@@ -1373,17 +1407,20 @@ class PetApp:
|
|
|
1373
1407
|
self.lang = lang
|
|
1374
1408
|
self._save_cfg(lang=lang)
|
|
1375
1409
|
|
|
1376
|
-
def _open_ui(self):
|
|
1410
|
+
def _open_ui(self, hash_part=""):
|
|
1377
1411
|
try:
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
webbrowser.open(url)
|
|
1412
|
+
if hash_part:
|
|
1413
|
+
openPetWindow(self.port, self.root.winfo_id(),
|
|
1414
|
+
url="http://127.0.0.1:%d/%s" % (self.port, hash_part))
|
|
1415
|
+
else:
|
|
1416
|
+
openPetWindow(self.port, self.root.winfo_id())
|
|
1384
1417
|
except Exception:
|
|
1385
1418
|
pass
|
|
1386
1419
|
|
|
1420
|
+
def _open_digest(self):
|
|
1421
|
+
"""群摘要:打开网页并自动展开蜜蜂坞面板(样式与网页端统一)。"""
|
|
1422
|
+
self._open_ui("#bee=1")
|
|
1423
|
+
|
|
1387
1424
|
def _bind_input(self):
|
|
1388
1425
|
# 菜单变量须先于 _menu 存在
|
|
1389
1426
|
self._mode_var = self.tk.StringVar(value="always")
|