mirror of
https://github.com/OLmatter/glm-coding-helper.git
synced 2026-10-06 15:13:13 +08:00
feat(backend): pipeline GUI launcher + 精简启动器
新增 backend/gui.py Tk 监控窗口:
- 拉起 backend.server 子进程并接管 stdout
- 顶部状态栏(启动中/运行中、YOLO/OCR worker 数、监听地址)
- 中间识别列表(最近 20 条 prompt/pred_text/confidence/耗时)
- 底部日志框(stdout 实时滚动,高亮 worker ready / 错误)
- 关闭窗口自动 terminate 后端
后端新增 /recent?limit=20 端口(GUI 拉取最近识别结果),
/health 同步返回 n_yolo / n_ocr / port 字段。
精简根目录启动器(7 → 2):
- 删 start-backend.cmd / start-backend-pipeline.cmd /
install-env.cmd / 启动后端.cmd / 首次安装环境.cmd
- 留 one-click-start.cmd(首次装环境)
- 留 start-backend-pipeline-gui.cmd(日常启动 + GUI)
- start-backend-pipeline.ps1 → start-backend-pipeline-gui.ps1
打包脚本 build_portable.ps1 / build_release_zips.ps1 同步
更新入口列表和错误提示。
补 .gitignore: official_models/(81M OCR 模型权重,不入库)
补 scripts/tools/evaluate_pipeline_compare.py(PR #10 评估
脚本,cherry-pick 时漏了)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
9410388eff
commit
66a9ea559e
@@ -19,6 +19,8 @@ __pycache__/
|
||||
.paddlex_cache/
|
||||
.paddlex_cache_cpu/
|
||||
.paddlex_cache_gpu/
|
||||
# Local OCR model weights (downloaded by paddle, kept locally only)
|
||||
official_models/
|
||||
|
||||
# Runtime output
|
||||
logs/
|
||||
|
||||
@@ -101,17 +101,11 @@ Greasy Fork 和仓库根目录的 `glm-coding-helper.user.js` 都是给普通用
|
||||
|
||||
### 4. 启动后端
|
||||
|
||||
如果下载的是自带环境包:
|
||||
|
||||
```text
|
||||
start-backend.cmd
|
||||
start-backend-pipeline-gui.cmd
|
||||
```
|
||||
|
||||
如果下载的是在线安装包:
|
||||
|
||||
```text
|
||||
one-click-start.cmd
|
||||
```
|
||||
首次使用如果环境没装好,会弹 PowerShell 提示,按提示输入 `1` 让它自动 `pip install`,或者先双击 `one-click-start.cmd` 装好环境再启动。
|
||||
|
||||
后端启动后默认监听:
|
||||
|
||||
@@ -136,7 +130,7 @@ https://www.bigmodel.cn/glm-coding
|
||||
> **建议**:默认开 **2 个窗口**,先把流程跑稳。多窗口不等于高成功率,反而可能让账号被 RPM 风控盯上,整轮全废。
|
||||
|
||||
1. 先安装好油猴插件,配置好油猴脚本。使用 Chrome 时要在扩展页面开启开发者模式,然后找到 Tampermonkey 详情,把“允许用户脚本”“在无痕模式下启用”“允许访问文件网址”按需打开。
|
||||
2. 下载并解压 Release 包,双击 `start-backend.cmd` 或 `one-click-start.cmd` 启动本地后端。
|
||||
2. 下载并解压 Release 包,双击 `start-backend-pipeline-gui.cmd` 启动本地后端。
|
||||
3. 打开 GLM Coding 页面测试脚本是否正常,脚本会自动补上内置优惠入口。
|
||||
4. 每天 9 点 30 分前进入抢购页面准备,晚了可能就打不开了。提前准备好手机支付宝付款。
|
||||
5. 多开几个窗口,等快到 10 点的时候点击好验证码但不要确定,等 10 点一到再按确定。**默认推荐 2 个窗口**(脚本弹窗默认值已从 3 改为 2,上限仍为 10,按需选择)。窗口开得越多,请求数量按窗口数放大,撞 RPM 上限的概率越高,近期已有大量高并发脚本因此全轮失败。
|
||||
@@ -155,6 +149,7 @@ https://www.bigmodel.cn/glm-coding
|
||||
- 遇到真正有金额的支付二维码,请自行确认后再扫码支付。
|
||||
- 多窗口并发不是越多越好。**2026-06 起智谱升级了 RPM 风控**,市面上高并发(多窗口批量请求)+ 屯码(预刷腾讯验证码 ticket 缓存复用)的同类脚本近期已**大面积失效**。窗口开得越多、请求越密集,越容易撞 RPM 上限,整轮秒杀全部返回 555/429。本项目走单窗口单发 + 实时 OCR 路线,相对安全。脚本默认 2 个窗口、上限 10 个,由用户按需选择;窗口开得越多,自身账号被风控的概率越高,请知悉。
|
||||
- 抢购是否成功受库存、限流、账号状态、支付速度等因素影响,脚本不能保证一定抢到。
|
||||
- **如果之前抢过且账号被风控盯上,强烈建议试试 Chrome / Edge 的"无痕模式"窗口**(`Ctrl+Shift+N`)。无痕窗口没有历史 Cookie / 缓存 / Service Worker / 本地存储,可能消除隐形的风控标记。Tampermonkey 需在扩展详情页允许在无痕模式中启用(见上文)。注意:无痕窗口关掉就丢失所有数据,配置靠 `GM_setValue` 是同步到 Tampermonkey 内部的,正常保留。
|
||||
|
||||
油猴菜单里可以打开配置面板、一键多开窗口、清除今日套餐状态缓存。
|
||||
|
||||
@@ -202,24 +197,38 @@ https://www.bigmodel.cn/glm-coding
|
||||
|
||||
启动(任选其一):
|
||||
```powershell
|
||||
# 方式 1:双击 start-backend-pipeline.cmd(推荐 Windows 用户)
|
||||
# 方式 2:命令行
|
||||
pwsh start-backend-pipeline.ps1
|
||||
# 方式 3:手动
|
||||
# 方式 1:双击 start-backend-pipeline-gui.cmd(推荐 Windows 用户,弹 GUI 窗口)
|
||||
# 方式 2:命令行手动
|
||||
pwsh start-backend-pipeline-gui.ps1
|
||||
# 方式 3:直接跑后端
|
||||
python backend/server.py
|
||||
```
|
||||
|
||||
双击启动器会自动检测 venv(`venv/` 或 `.venv_paddle/`)、检查依赖(fastapi/uvicorn/psutil)、缺失时自动 pip install;端口被占用时会显示中文提示(含 PID/进程名/命令行),杀进程前需用户确认。
|
||||
|
||||
### 可视化 GUI 启动器
|
||||
|
||||
如果想在窗口里实时看后端状态(worker 就绪进度、最近识别结果、stdout 日志),用 GUI 启动器:
|
||||
|
||||
```text
|
||||
start-backend-pipeline-gui.cmd
|
||||
```
|
||||
|
||||
`backend/gui.py` 会拉起 `backend.server` 子进程并接管其 stdout,弹出 Tk 窗口:
|
||||
|
||||
- **顶部状态栏**:系统状态(启动中 / 运行中)、YOLO/OCR worker 数、监听地址
|
||||
- **中间识别列表**:最近 20 条识别结果(提示字、预测字、置信度、yolo/ocr 耗时)
|
||||
- **底部日志框**:后端 stdout 实时滚动,`worker ready` / `[architect]` / 错误高亮
|
||||
|
||||
关闭窗口时 GUI 会自动 `terminate` 后端子进程,不用手动到任务管理器杀。
|
||||
|
||||
## 常用文件
|
||||
|
||||
| 文件 | 用途 |
|
||||
| --- | --- |
|
||||
| `glm-coding-helper.user.js` | 给 Tampermonkey 安装的主脚本 |
|
||||
| `start-backend.cmd` | 启动已有本地后端环境(旧版单进程) |
|
||||
| `start-backend-pipeline.cmd` | 双击启动 pipeline 后端(v8.20+ 推荐) |
|
||||
| `one-click-start.cmd` | 自动安装环境并启动 |
|
||||
| `install-env.cmd` | 手动安装 CPU 后端环境 |
|
||||
| `one-click-start.cmd` | 首次安装环境(CPU 依赖) |
|
||||
| `start-backend-pipeline-gui.cmd` | 日常启动 pipeline 后端 + 弹 Tk 可视化窗口 |
|
||||
| `scripts/` | 后端和打包脚本 |
|
||||
| `backend/` | Pipeline 后端(FastAPI + 多进程 YOLO→OCR) |
|
||||
| `models/` | 本地识别模型 |
|
||||
|
||||
+312
@@ -0,0 +1,312 @@
|
||||
"""
|
||||
Pipeline Backend GUI - Tk 监控面板
|
||||
|
||||
启动 backend.server 子进程,捕获 stdout 写入日志框;
|
||||
定期拉取 /health 和 /recent,实时显示在状态栏和识别列表里。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import queue
|
||||
import threading
|
||||
import subprocess
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from collections import deque
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
if getattr(sys, "frozen", False):
|
||||
ROOT = Path(sys._MEIPASS)
|
||||
else:
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
# ── 配置 ───────────────────────────────────────────────
|
||||
BACKEND_HOST = "127.0.0.1"
|
||||
BACKEND_PORT = 8888
|
||||
BACKEND_URL = f"http://{BACKEND_HOST}:{BACKEND_PORT}"
|
||||
POLL_HEALTH_MS = 1000
|
||||
POLL_RECENT_MS = 500
|
||||
MAX_LOG_LINES = 500
|
||||
MAX_RECENT_SHOWN = 20
|
||||
|
||||
# 颜色
|
||||
BG = "#f0f2f5"
|
||||
FG_NORMAL = "#262626"
|
||||
FG_SUCCESS = "#52c41a"
|
||||
FG_WARN = "#faad14"
|
||||
FG_ERROR = "#ff4d4f"
|
||||
FG_INFO = "#1890ff"
|
||||
FG_GREY = "#8c8c8c"
|
||||
|
||||
state = {
|
||||
"backend_proc": None,
|
||||
"log_queue": queue.Queue(),
|
||||
"log_lines": deque(maxlen=MAX_LOG_LINES),
|
||||
"recent_results": deque(maxlen=MAX_RECENT_SHOWN),
|
||||
"last_seen_req_id": 0,
|
||||
"health": {"status": "starting", "ready_workers": 0, "alive_workers": 0,
|
||||
"workers": 0, "n_yolo": 0, "n_ocr": 0, "port": BACKEND_PORT},
|
||||
"port": BACKEND_PORT,
|
||||
}
|
||||
|
||||
|
||||
def _read_proc_stdout(proc: subprocess.Popen):
|
||||
"""后台线程:读子进程 stdout/stderr,写入 log_queue"""
|
||||
for stream in (proc.stdout, proc.stderr):
|
||||
if stream is None:
|
||||
continue
|
||||
try:
|
||||
for line in iter(stream.readline, b""):
|
||||
try:
|
||||
text = line.decode("utf-8", errors="replace").rstrip("\r\n")
|
||||
except Exception:
|
||||
text = str(line)
|
||||
if text:
|
||||
state["log_queue"].put(text)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _http_get_json(path: str, timeout: float = 1.0):
|
||||
try:
|
||||
with urllib.request.urlopen(BACKEND_URL + path, timeout=timeout) as r:
|
||||
return json.loads(r.read().decode("utf-8"))
|
||||
except (urllib.error.URLError, ConnectionError, OSError, json.JSONDecodeError):
|
||||
return None
|
||||
|
||||
|
||||
def _format_ts(ts: float) -> str:
|
||||
return datetime.fromtimestamp(ts).strftime("%H:%M:%S")
|
||||
|
||||
|
||||
class App:
|
||||
def __init__(self, root: tk.Tk):
|
||||
self.root = root
|
||||
self.root.title("GLM Coding Captcha - Pipeline Backend")
|
||||
self.root.geometry("720x600")
|
||||
self.root.configure(bg=BG)
|
||||
self.root.minsize(640, 480)
|
||||
|
||||
style = ttk.Style()
|
||||
try:
|
||||
style.theme_use("clam")
|
||||
except tk.TclError:
|
||||
pass
|
||||
style.configure("TLabel", background=BG, font=("Microsoft YaHei UI", 10))
|
||||
style.configure("Status.TLabel", font=("Microsoft YaHei UI", 12, "bold"))
|
||||
style.configure("Big.TLabel", font=("Microsoft YaHei UI", 14, "bold"))
|
||||
style.configure("Ok.TLabel", foreground=FG_SUCCESS, font=("Microsoft YaHei UI", 11, "bold"))
|
||||
style.configure("Warn.TLabel", foreground=FG_WARN, font=("Microsoft YaHei UI", 11, "bold"))
|
||||
style.configure("Err.TLabel", foreground=FG_ERROR, font=("Microsoft YaHei UI", 11, "bold"))
|
||||
style.configure("Info.TLabel", foreground=FG_INFO, font=("Microsoft YaHei UI", 11, "bold"))
|
||||
|
||||
self._build_ui()
|
||||
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
|
||||
self._start_backend()
|
||||
self.root.after(100, self._poll_logs)
|
||||
self.root.after(POLL_HEALTH_MS, self._poll_health)
|
||||
self.root.after(POLL_RECENT_MS, self._poll_recent)
|
||||
|
||||
def _build_ui(self):
|
||||
# 顶部状态栏
|
||||
top = ttk.Frame(self.root, padding="12 10")
|
||||
top.pack(fill=tk.X)
|
||||
|
||||
ttk.Label(top, text="智谱 GLM 验证码后端 (Pipeline)", style="Big.TLabel").grid(
|
||||
row=0, column=0, columnspan=4, sticky=tk.W, pady=(0, 8))
|
||||
|
||||
# 第一行:系统状态
|
||||
ttk.Label(top, text="系统状态:").grid(row=1, column=0, sticky=tk.W, pady=2)
|
||||
self.lbl_status = ttk.Label(top, text="启动中…", style="Warn.TLabel")
|
||||
self.lbl_status.grid(row=1, column=1, sticky=tk.W, pady=2)
|
||||
|
||||
# 第二行:worker 就绪
|
||||
ttk.Label(top, text="Workers:").grid(row=2, column=0, sticky=tk.W, pady=2)
|
||||
self.lbl_workers = ttk.Label(top, text="0/0")
|
||||
self.lbl_workers.grid(row=2, column=1, sticky=tk.W, pady=2)
|
||||
ttk.Label(top, text="YOLO / OCR:").grid(row=2, column=2, sticky=tk.W, padx=(20, 4), pady=2)
|
||||
self.lbl_pipeline = ttk.Label(top, text="-/-")
|
||||
self.lbl_pipeline.grid(row=2, column=3, sticky=tk.W, pady=2)
|
||||
|
||||
# 第三行:端口 / 地址
|
||||
ttk.Label(top, text="监听:").grid(row=3, column=0, sticky=tk.W, pady=2)
|
||||
self.lbl_url = ttk.Label(top, text=f"{BACKEND_URL}", style="Info.TLabel")
|
||||
self.lbl_url.grid(row=3, column=1, columnspan=3, sticky=tk.W, pady=2)
|
||||
|
||||
# 中间:最近识别结果
|
||||
mid = ttk.LabelFrame(self.root, text="最近识别结果(最新在上)", padding="8")
|
||||
mid.pack(fill=tk.BOTH, expand=True, padx=12, pady=(4, 4))
|
||||
|
||||
cols = ("time", "prompt", "pred", "conf", "ms", "yolo", "ocr", "req")
|
||||
self.tree = ttk.Treeview(mid, columns=cols, show="headings", height=8)
|
||||
for col, w, anchor in [
|
||||
("time", 70, tk.W), ("prompt", 90, tk.W), ("pred", 110, tk.W),
|
||||
("conf", 60, tk.E), ("ms", 60, tk.E), ("yolo", 60, tk.E),
|
||||
("ocr", 60, tk.E), ("req", 60, tk.E),
|
||||
]:
|
||||
self.tree.heading(col, text=col.upper())
|
||||
self.tree.column(col, width=w, anchor=anchor)
|
||||
self.tree.tag_configure("ok", foreground=FG_SUCCESS)
|
||||
self.tree.tag_configure("err", foreground=FG_ERROR)
|
||||
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
|
||||
sb = ttk.Scrollbar(mid, orient=tk.VERTICAL, command=self.tree.yview)
|
||||
self.tree.configure(yscrollcommand=sb.set)
|
||||
sb.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
# 底部:实时日志
|
||||
bot = ttk.LabelFrame(self.root, text="后端日志(stdout)", padding="6")
|
||||
bot.pack(fill=tk.BOTH, expand=False, padx=12, pady=(0, 8))
|
||||
self.log_box = tk.Text(bot, height=10, font=("Consolas", 9),
|
||||
bg="#1e1e1e", fg="#d4d4d4", insertbackground="#d4d4d4",
|
||||
relief=tk.FLAT, wrap=tk.NONE)
|
||||
self.log_box.tag_configure("info", foreground="#d4d4d4")
|
||||
self.log_box.tag_configure("ready", foreground=FG_SUCCESS)
|
||||
self.log_box.tag_configure("warn", foreground=FG_WARN)
|
||||
self.log_box.tag_configure("err", foreground=FG_ERROR)
|
||||
self.log_box.tag_configure("ts", foreground=FG_GREY)
|
||||
self.log_box.configure(state=tk.DISABLED)
|
||||
self.log_box.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
|
||||
log_sb = ttk.Scrollbar(bot, orient=tk.VERTICAL, command=self.log_box.yview)
|
||||
self.log_box.configure(yscrollcommand=log_sb.set)
|
||||
log_sb.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
def _append_log(self, line: str):
|
||||
state["log_lines"].append(line)
|
||||
ts = datetime.now().strftime("%H:%M:%S")
|
||||
# 着色:[architect] 蓝;worker ready 绿;含 ERROR/Exception/失败 红;含 WARN/⚠ 黄
|
||||
tag = "info"
|
||||
low = line.lower()
|
||||
if "worker ready" in low or "✓" in line or "就绪" in line or "warmed" in low:
|
||||
tag = "ready"
|
||||
elif "error" in low or "exception" in low or "traceback" in low or "fail" in low or "err:" in low:
|
||||
tag = "err"
|
||||
elif "warn" in low or "⚠" in line or "warning" in low:
|
||||
tag = "warn"
|
||||
elif line.startswith("[architect]"):
|
||||
tag = "info"
|
||||
|
||||
self.log_box.configure(state=tk.NORMAL)
|
||||
self.log_box.insert(tk.END, f"[{ts}] ", "ts")
|
||||
self.log_box.insert(tk.END, line + "\n", tag)
|
||||
# 裁剪到 MAX_LOG_LINES
|
||||
line_count = int(self.log_box.index("end-1c").split(".")[0])
|
||||
if line_count > MAX_LOG_LINES:
|
||||
self.log_box.delete("1.0", f"{line_count - MAX_LOG_LINES}.0")
|
||||
self.log_box.see(tk.END)
|
||||
self.log_box.configure(state=tk.DISABLED)
|
||||
|
||||
def _start_backend(self):
|
||||
"""拉起 backend.server 子进程"""
|
||||
env = os.environ.copy()
|
||||
env["PYTHONIOENCODING"] = "utf-8"
|
||||
env["PYTHONUTF8"] = "1"
|
||||
cmd = [sys.executable, "-m", "backend.server"]
|
||||
self._append_log(f"$ {sys.executable} -m backend.server")
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
cmd, cwd=str(ROOT), env=env,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
bufsize=1,
|
||||
)
|
||||
state["backend_proc"] = proc
|
||||
threading.Thread(target=_read_proc_stdout, args=(proc,),
|
||||
daemon=True).start()
|
||||
except Exception as e:
|
||||
self._append_log(f"FATAL: 启动后端失败: {e}")
|
||||
|
||||
def _poll_logs(self):
|
||||
try:
|
||||
while True:
|
||||
line = state["log_queue"].get_nowait()
|
||||
self._append_log(line)
|
||||
except queue.Empty:
|
||||
pass
|
||||
self.root.after(100, self._poll_logs)
|
||||
|
||||
def _poll_health(self):
|
||||
data = _http_get_json("/health")
|
||||
if data:
|
||||
state["health"] = data
|
||||
self._update_health_display(data)
|
||||
else:
|
||||
# 后端还没起来
|
||||
self.lbl_status.config(text="等待后端…", style="Warn.TLabel")
|
||||
self.root.after(POLL_HEALTH_MS, self._poll_health)
|
||||
|
||||
def _update_health_display(self, h: dict):
|
||||
status = h.get("status", "starting")
|
||||
ready = h.get("ready_workers", 0)
|
||||
total = h.get("workers", 0)
|
||||
alive = h.get("alive_workers", 0)
|
||||
n_yolo = h.get("n_yolo", 0)
|
||||
n_ocr = h.get("n_ocr", 0)
|
||||
|
||||
if status == "ok" and alive == total:
|
||||
self.lbl_status.config(text="● 运行中", style="Ok.TLabel")
|
||||
elif status == "starting":
|
||||
self.lbl_status.config(text="● 启动中", style="Warn.TLabel")
|
||||
else:
|
||||
self.lbl_status.config(text=f"● {status}", style="Warn.TLabel")
|
||||
|
||||
self.lbl_workers.config(text=f"{ready}/{total} (alive={alive})")
|
||||
self.lbl_pipeline.config(text=f"{n_yolo} YOLO / {n_ocr} OCR")
|
||||
|
||||
def _poll_recent(self):
|
||||
data = _http_get_json(f"/recent?limit={MAX_RECENT_SHOWN}")
|
||||
if data and "results" in data:
|
||||
self._update_recent_display(data["results"])
|
||||
self.root.after(POLL_RECENT_MS, self._poll_recent)
|
||||
|
||||
def _update_recent_display(self, results: list):
|
||||
# 清空并重绘(识别结果最多 20 条,开销可接受)
|
||||
for iid in self.tree.get_children():
|
||||
self.tree.delete(iid)
|
||||
for item in results:
|
||||
ts = _format_ts(item.get("ts", 0))
|
||||
if item.get("success") is False:
|
||||
self.tree.insert("", tk.END, values=(
|
||||
ts, "-", f"ERR: {item.get('error','?')}", "-", "-", "-", "-",
|
||||
item.get("req_id", "-")
|
||||
), tags=("err",))
|
||||
continue
|
||||
prompt = "".join(item.get("prompt", []))
|
||||
pred = item.get("pred_text", "")
|
||||
conf = f"{item.get('confidence', 0):.2f}"
|
||||
ms = f"{item.get('elapsed_ms', 0):.0f}"
|
||||
yolo = f"{item.get('yolo_ms', 0):.0f}"
|
||||
ocr = f"{item.get('ocr_ms', 0):.0f}"
|
||||
req = item.get("req_id", "-")
|
||||
tag = "ok" if prompt and pred and prompt == pred else ("err" if pred.startswith("ERR") else "ok")
|
||||
self.tree.insert("", tk.END, values=(
|
||||
ts, prompt, pred, conf, ms, yolo, ocr, req
|
||||
), tags=(tag,))
|
||||
|
||||
def on_close(self):
|
||||
proc = state.get("backend_proc")
|
||||
if proc and proc.poll() is None:
|
||||
try:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
self.root.destroy()
|
||||
|
||||
|
||||
def main():
|
||||
root = tk.Tk()
|
||||
App(root)
|
||||
root.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+44
-1
@@ -89,6 +89,10 @@ ready_count = 0
|
||||
ready_count_lock = threading.Lock()
|
||||
_shutdown = threading.Event()
|
||||
|
||||
# ── 最近识别结果 ring buffer(供 GUI 拉取)──────────────────────
|
||||
from collections import deque
|
||||
_recent_results: "deque[dict]" = deque(maxlen=20)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
@@ -173,6 +177,7 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
|
||||
def result_listener_thread():
|
||||
import time as _t
|
||||
while True:
|
||||
res = res_queue.get()
|
||||
if not res:
|
||||
@@ -180,6 +185,26 @@ def result_listener_thread():
|
||||
req_id = res.get("req_id")
|
||||
with request_lock:
|
||||
future = pending_requests.pop(req_id, None)
|
||||
# 写入最近识别结果(脱敏,只保留 GUI 需要的字段)
|
||||
if res.get("success"):
|
||||
snapshot = {
|
||||
"ts": _t.time(),
|
||||
"prompt": res.get("prompt", []),
|
||||
"pred_text": res.get("pred_text", ""),
|
||||
"confidence": res.get("confidence", 0.0),
|
||||
"elapsed_ms": res.get("elapsed_ms", 0.0),
|
||||
"yolo_ms": res.get("yolo_ms", 0.0),
|
||||
"ocr_ms": res.get("ocr_ms", 0.0),
|
||||
"req_id": req_id,
|
||||
}
|
||||
else:
|
||||
snapshot = {
|
||||
"ts": _t.time(),
|
||||
"success": False,
|
||||
"error": res.get("error", "unknown"),
|
||||
"req_id": req_id,
|
||||
}
|
||||
_recent_results.append(snapshot)
|
||||
if future and not future.done():
|
||||
future.get_loop().call_soon_threadsafe(future.set_result, res)
|
||||
|
||||
@@ -222,7 +247,25 @@ async def health():
|
||||
alive = sum(1 for p in workers_list if p.is_alive()) if workers_list else 0
|
||||
total = N_YOLO + N_OCR
|
||||
status = "ok" if r >= total else "starting"
|
||||
return {"status": status, "workers": total, "ready_workers": r, "alive_workers": alive}
|
||||
return {
|
||||
"status": status,
|
||||
"workers": total,
|
||||
"ready_workers": r,
|
||||
"alive_workers": alive,
|
||||
"n_yolo": N_YOLO,
|
||||
"n_ocr": N_OCR,
|
||||
"port": PORT,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/recent")
|
||||
async def recent_results(limit: int = 20):
|
||||
"""返回最近 N 条识别结果,供 GUI 轮询拉取"""
|
||||
limit = max(1, min(20, limit))
|
||||
items = list(_recent_results)[-limit:]
|
||||
# 反转,最新的在前
|
||||
items.reverse()
|
||||
return {"count": len(items), "results": items}
|
||||
|
||||
|
||||
@app.post("/direct")
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
cd /d "%~dp0"
|
||||
echo Installing GLM Coding Helper backend environment...
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "scripts\setup_backend.ps1" -Target cpu
|
||||
pause
|
||||
@@ -74,7 +74,7 @@ if (-not $Ready) {
|
||||
}
|
||||
if (-not $Ready) {
|
||||
Write-Host "[FAIL] Backend environment repair failed. Required deps still missing." -ForegroundColor Red
|
||||
Write-Host " Try running install-env.cmd manually." -ForegroundColor Red
|
||||
Write-Host " Try running one-click-start.cmd manually." -ForegroundColor Red
|
||||
Read-Host "Press Enter to exit"
|
||||
exit 1
|
||||
}
|
||||
@@ -93,7 +93,7 @@ if ($Target -eq "auto" -and $InstallTarget -eq "gpu" -and -not (Test-PythonImpor
|
||||
# ── 检查 pipeline 后端依赖(非阻塞,仅提示)─────────────────
|
||||
$PipelineDepsOk = Test-PythonImports $SelectedPython "import fastapi, uvicorn, psutil"
|
||||
if (-not $PipelineDepsOk) {
|
||||
Write-Host "[INFO] Pipeline backend deps (fastapi/uvicorn/psutil) not installed. Run install-env.cmd to add them." -ForegroundColor Yellow
|
||||
Write-Host "[INFO] Pipeline backend deps (fastapi/uvicorn/psutil) not installed. Run start-backend-pipeline-gui.cmd to add them." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host "Starting backend in $StartMode mode on port $Port..."
|
||||
|
||||
@@ -12,13 +12,13 @@ $env:PYTHONIOENCODING = "utf-8"
|
||||
|
||||
$VenvPython = Join-Path $Root ".venv_paddle\Scripts\python.exe"
|
||||
if (-not (Test-Path $VenvPython)) {
|
||||
throw "Missing .venv_paddle. Run install-env.cmd first."
|
||||
throw "Missing .venv_paddle. Run one-click-start.cmd first."
|
||||
}
|
||||
|
||||
Write-Host "Checking portable CPU environment..."
|
||||
& $VenvPython -c "import ultralytics, paddleocr, paddlex, cv2, PIL, numpy; print('portable imports ok')"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "The local .venv_paddle is incomplete. Run install-env.cmd again, then rerun this script."
|
||||
throw "The local .venv_paddle is incomplete. Run one-click-start.cmd again, then rerun this script."
|
||||
}
|
||||
|
||||
$Weight = Join-Path $Root "models\weights\yolo-captcha-detector.pt"
|
||||
@@ -39,21 +39,22 @@ New-Item -ItemType Directory -Path $PackageDir | Out-Null
|
||||
$Include = @(
|
||||
"glm-coding-helper.user.js",
|
||||
"scripts\userscripts\glm-coding-captcha-direct.user.js",
|
||||
"start-backend.cmd",
|
||||
"install-env.cmd",
|
||||
"one-click-start.cmd",
|
||||
"start-backend-pipeline-gui.cmd",
|
||||
"start-backend-pipeline-gui.ps1",
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"LICENSE",
|
||||
"requirements-backend-cpu.txt",
|
||||
"scripts",
|
||||
"models",
|
||||
"backend",
|
||||
".venv_paddle",
|
||||
".paddlex_cache_cpu",
|
||||
".paddle_home"
|
||||
)
|
||||
|
||||
$KnownRootCmdItems = @("start-backend.cmd", "install-env.cmd", "one-click-start.cmd")
|
||||
$KnownRootCmdItems = @("one-click-start.cmd", "start-backend-pipeline-gui.cmd")
|
||||
$ExtraRootCmdItems = Get-ChildItem -LiteralPath $Root -Filter "*.cmd" -File |
|
||||
Where-Object { $KnownRootCmdItems -notcontains $_.Name } |
|
||||
ForEach-Object { $_.Name }
|
||||
@@ -83,7 +84,7 @@ $Guide = @"
|
||||
GLM Coding Helper portable CPU package
|
||||
|
||||
1. Install or update Tampermonkey script from glm-coding-helper.user.js.
|
||||
2. Double-click start-backend.cmd, one-click-start.cmd, or the localized start shortcut if present.
|
||||
2. Double-click one-click-start.cmd to install the environment on first run, or start-backend-pipeline-gui.cmd to launch the pipeline backend with GUI.
|
||||
3. Open the GLM Coding page from your normal browser session.
|
||||
|
||||
This package includes the CPU Python environment and local model files.
|
||||
|
||||
@@ -71,19 +71,20 @@ if (Test-Path $OnlineDir) { Remove-Item -LiteralPath $OnlineDir -Recurse -Force
|
||||
$CommonItems = @(
|
||||
"glm-coding-helper.user.js",
|
||||
"scripts\userscripts\glm-coding-captcha-direct.user.js",
|
||||
"start-backend.cmd",
|
||||
"install-env.cmd",
|
||||
"one-click-start.cmd",
|
||||
"start-backend-pipeline-gui.cmd",
|
||||
"start-backend-pipeline-gui.ps1",
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"LICENSE",
|
||||
"requirements-backend-cpu.txt",
|
||||
"requirements-backend-gpu.txt",
|
||||
"scripts",
|
||||
"models"
|
||||
"models",
|
||||
"backend"
|
||||
)
|
||||
|
||||
$KnownRootCmdItems = @("start-backend.cmd", "install-env.cmd", "one-click-start.cmd")
|
||||
$KnownRootCmdItems = @("one-click-start.cmd", "start-backend-pipeline-gui.cmd")
|
||||
$ExtraRootCmdItems = Get-ChildItem -LiteralPath $Root -Filter "*.cmd" -File |
|
||||
Where-Object { $KnownRootCmdItems -notcontains $_.Name } |
|
||||
ForEach-Object { $_.Name }
|
||||
@@ -103,7 +104,8 @@ Recommended:
|
||||
3. It will install CPU/GPU backend dependencies automatically when missing, then start the backend.
|
||||
|
||||
Manual:
|
||||
- install-env.cmd installs CPU backend environment.
|
||||
- one-click-start.cmd installs the CPU backend environment on first run.
|
||||
- start-backend-pipeline-gui.cmd launches the pipeline backend with a Tk GUI window.
|
||||
- start-backend.cmd starts CPU backend after environment exists.
|
||||
"@
|
||||
Set-Content -LiteralPath (Join-Path $OnlineDir "ONLINE_INSTALLER_README.txt") -Value $OnlineGuide -Encoding UTF8
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageFont
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
PROJECT_ROOT = ROOT.parents[1]
|
||||
|
||||
|
||||
def find_image(name: str, dataset_root: Path) -> Path | None:
|
||||
candidates = [
|
||||
dataset_root / "auto_captured" / name,
|
||||
dataset_root / "auto_captured_archive" / "captured_310_20260513_042005" / name,
|
||||
dataset_root / "auto_captured_autolabel" / "images" / name,
|
||||
dataset_root / "char_detector_yolo" / "images" / "train" / name,
|
||||
dataset_root / "char_detector_yolo" / "images" / "val" / name,
|
||||
]
|
||||
for path in candidates:
|
||||
if path.exists():
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def point_for_prompt(prompt: str, box_chars: list[str], boxes: list[list[float]]) -> list[dict]:
|
||||
used: set[int] = set()
|
||||
out: list[dict] = []
|
||||
for ch in prompt:
|
||||
found = None
|
||||
for idx, box_ch in enumerate(box_chars):
|
||||
if idx not in used and box_ch == ch:
|
||||
found = idx
|
||||
break
|
||||
if found is None:
|
||||
raise ValueError(f"cannot map prompt={prompt!r} box_chars={box_chars!r}")
|
||||
used.add(found)
|
||||
x1, y1, x2, y2 = boxes[found]
|
||||
out.append({"x": (x1 + x2) / 2.0, "y": (y1 + y2) / 2.0, "char": ch})
|
||||
return out
|
||||
|
||||
|
||||
def points_ok(pred: list[dict], gt: list[dict], threshold: float) -> tuple[bool, float]:
|
||||
if len(pred) != len(gt):
|
||||
return False, float("inf")
|
||||
max_dist = 0.0
|
||||
for p, g in zip(pred, gt):
|
||||
d = math.hypot(float(p["x"]) - float(g["x"]), float(p["y"]) - float(g["y"]))
|
||||
max_dist = max(max_dist, d)
|
||||
if d > threshold:
|
||||
return False, max_dist
|
||||
return True, max_dist
|
||||
|
||||
|
||||
def scan_windows_fonts() -> list[tuple[str, int]]:
|
||||
font_dir = Path(os.environ.get("WINDIR", r"C:\Windows")) / "Fonts"
|
||||
names = [
|
||||
"simsun.ttc",
|
||||
"simhei.ttf",
|
||||
"simkai.ttf",
|
||||
"simfang.ttf",
|
||||
"msyh.ttc",
|
||||
"msyhbd.ttc",
|
||||
"msyhl.ttc",
|
||||
"Deng.ttf",
|
||||
"Dengb.ttf",
|
||||
"Dengl.ttf",
|
||||
]
|
||||
fonts: list[tuple[str, int]] = []
|
||||
seen: set[tuple[str, int]] = set()
|
||||
for name in names:
|
||||
path = font_dir / name
|
||||
if not path.exists():
|
||||
continue
|
||||
for idx in range(8):
|
||||
try:
|
||||
font = ImageFont.truetype(str(path), 40, index=idx)
|
||||
cn = font.getbbox("测")
|
||||
en = font.getbbox("A")
|
||||
except Exception:
|
||||
break
|
||||
cn_w = cn[2] - cn[0]
|
||||
en_w = en[2] - en[0]
|
||||
if cn_w >= 25 and cn_w >= en_w * 1.2 and (str(path), idx) not in seen:
|
||||
fonts.append((str(path), idx))
|
||||
seen.add((str(path), idx))
|
||||
return fonts
|
||||
|
||||
|
||||
def load_grabber(reference_root: Path):
|
||||
server_path = reference_root / "captcha" / "ddddocr_server.py"
|
||||
spec = importlib.util.spec_from_file_location("grabber_ddddocr_server", server_path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"cannot load {server_path}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
logging.getLogger("ddddocr-server").disabled = True
|
||||
module._all_font_paths[:] = scan_windows_fonts()
|
||||
module._variant_cache.clear()
|
||||
module._font_obj_cache.clear()
|
||||
return module
|
||||
|
||||
|
||||
def start_our_worker(mode: str) -> subprocess.Popen:
|
||||
env = os.environ.copy()
|
||||
env["PYTHONIOENCODING"] = "utf-8"
|
||||
env["PYTHONUTF8"] = "1"
|
||||
env["CNCAPTCHA_OCR_MODE"] = mode
|
||||
env.setdefault("CNCAPTCHA_YOLO_DEVICE", "cpu")
|
||||
py = ROOT / ".venv_paddle" / "Scripts" / "python.exe"
|
||||
if not py.exists():
|
||||
py = Path(sys.executable)
|
||||
return subprocess.Popen(
|
||||
[str(py), "-u", str(ROOT / "scripts" / "tools" / "captcha_worker.py")],
|
||||
cwd=str(ROOT),
|
||||
env=env,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
|
||||
def ask_our_worker(proc: subprocess.Popen, image_path: Path, prompt: str, timeout: float = 45.0) -> dict:
|
||||
assert proc.stdin is not None and proc.stdout is not None
|
||||
payload = json.dumps({"image_path": str(image_path), "chars": list(prompt)}, ensure_ascii=False) + "\n"
|
||||
proc.stdin.write(payload.encode("utf-8"))
|
||||
proc.stdin.flush()
|
||||
start = time.perf_counter()
|
||||
while time.perf_counter() - start < timeout:
|
||||
line = proc.stdout.readline()
|
||||
if not line:
|
||||
raise RuntimeError("our worker exited")
|
||||
text = line.decode("utf-8", errors="replace").strip()
|
||||
if not text.startswith("{"):
|
||||
continue
|
||||
return json.loads(text)
|
||||
raise TimeoutError("our worker timeout")
|
||||
|
||||
|
||||
def run(args: argparse.Namespace) -> int:
|
||||
dataset_root = Path(args.dataset_root).resolve()
|
||||
labels = json.loads((dataset_root / "glm_ocr_labels_all.json").read_text(encoding="utf-8"))
|
||||
items = []
|
||||
for name, row in labels.items():
|
||||
if row.get("has_error"):
|
||||
continue
|
||||
image = find_image(name, dataset_root)
|
||||
if image is None:
|
||||
continue
|
||||
prompt = str(row["prompt"])
|
||||
box_chars = list(row["box_chars"])
|
||||
boxes = list(row["boxes"])
|
||||
if len(prompt) != 3 or len(box_chars) != 3 or len(boxes) != 3:
|
||||
continue
|
||||
try:
|
||||
gt = point_for_prompt(prompt, box_chars, boxes)
|
||||
except Exception:
|
||||
continue
|
||||
items.append({"name": name, "image": image, "prompt": prompt, "gt": gt})
|
||||
if args.limit:
|
||||
items = items[: args.limit]
|
||||
|
||||
out_dir = Path(args.output_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
threshold = float(args.threshold)
|
||||
summary = {"items": len(items), "threshold": threshold, "runs": {}}
|
||||
|
||||
if args.engine in {"grabber", "both"}:
|
||||
grabber = load_grabber(Path(args.reference_root).resolve())
|
||||
rows = []
|
||||
ok_count = 0
|
||||
times = []
|
||||
for idx, item in enumerate(items, 1):
|
||||
t0 = time.perf_counter()
|
||||
try:
|
||||
pred = grabber.solve_click_captcha(item["image"].read_bytes(), item["prompt"])
|
||||
success, max_dist = points_ok(pred, item["gt"], threshold)
|
||||
err = ""
|
||||
except Exception as exc:
|
||||
pred = []
|
||||
success = False
|
||||
max_dist = float("inf")
|
||||
err = str(exc)
|
||||
ms = (time.perf_counter() - t0) * 1000
|
||||
times.append(ms)
|
||||
ok_count += int(success)
|
||||
rows.append({"name": item["name"], "ok": success, "max_dist": max_dist, "ms": round(ms, 1), "error": err})
|
||||
if idx % 25 == 0:
|
||||
print(f"grabber {idx}/{len(items)} ok={ok_count}", flush=True)
|
||||
summary["runs"]["grabber_winfonts"] = {
|
||||
"ok": ok_count,
|
||||
"total": len(items),
|
||||
"acc": ok_count / max(len(items), 1),
|
||||
"avg_ms": sum(times) / max(len(times), 1),
|
||||
"p50_ms": sorted(times)[len(times) // 2] if times else 0,
|
||||
"fonts": len(getattr(grabber, "_all_font_paths", [])),
|
||||
}
|
||||
(out_dir / "grabber_winfonts_rows.json").write_text(json.dumps(rows, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
if args.engine in {"our", "both"}:
|
||||
proc = start_our_worker(args.our_mode)
|
||||
rows = []
|
||||
ok_count = 0
|
||||
times = []
|
||||
try:
|
||||
for idx, item in enumerate(items, 1):
|
||||
t0 = time.perf_counter()
|
||||
try:
|
||||
resp = ask_our_worker(proc, item["image"], item["prompt"])
|
||||
except Exception as exc:
|
||||
resp = {"success": False, "error": str(exc)}
|
||||
if resp.get("success"):
|
||||
img = Image.open(item["image"])
|
||||
pred = [{"x": float(c["nx"]) * img.width, "y": float(c["ny"]) * img.height} for c in resp.get("click_coords", [])]
|
||||
success, max_dist = points_ok(pred, item["gt"], threshold)
|
||||
err = ""
|
||||
ms = float(resp.get("elapsed_ms") or ((time.perf_counter() - t0) * 1000))
|
||||
else:
|
||||
success = False
|
||||
max_dist = float("inf")
|
||||
err = str(resp.get("error", "failed"))
|
||||
ms = (time.perf_counter() - t0) * 1000
|
||||
times.append(ms)
|
||||
ok_count += int(success)
|
||||
rows.append({"name": item["name"], "ok": success, "max_dist": max_dist, "ms": round(ms, 1), "error": err, "resp": resp})
|
||||
if idx % 25 == 0:
|
||||
print(f"our {idx}/{len(items)} ok={ok_count}", flush=True)
|
||||
finally:
|
||||
try:
|
||||
proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
summary["runs"][f"our_{args.our_mode}"] = {
|
||||
"ok": ok_count,
|
||||
"total": len(items),
|
||||
"acc": ok_count / max(len(items), 1),
|
||||
"avg_ms": sum(times) / max(len(times), 1),
|
||||
"p50_ms": sorted(times)[len(times) // 2] if times else 0,
|
||||
}
|
||||
(out_dir / f"our_{args.our_mode}_rows.json").write_text(json.dumps(rows, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
(out_dir / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2), flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--dataset-root", default=str(PROJECT_ROOT / "dataset"))
|
||||
parser.add_argument("--reference-root", default=str(PROJECT_ROOT / "_reference" / "glm-coding-grabber"))
|
||||
parser.add_argument("--output-dir", default=str(PROJECT_ROOT / "dataset" / "extreme_compare"))
|
||||
parser.add_argument("--engine", choices=["grabber", "our", "both"], default="both")
|
||||
parser.add_argument("--our-mode", default="cpu")
|
||||
parser.add_argument("--threshold", type=float, default=35.0)
|
||||
parser.add_argument("--limit", type=int, default=0)
|
||||
return run(parser.parse_args())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,7 +1,7 @@
|
||||
// ==UserScript==
|
||||
// @name 智谱 GLM Coding Plan 抢购助手 + 本地 OCR 自动验证码
|
||||
// @namespace http://tampermonkey.net/
|
||||
// @version 8.20
|
||||
// @version 8.21.1
|
||||
// @description GLM Coding Rush / 智谱 GLM Coding Plan 抢购助手,一键抢购油猴脚本 / Tampermonkey userscript,配合本地 CPU/GPU OCR 自动识别中文点选验证码并点击,支持多窗口并发、限流重试和支付页安全保护
|
||||
// @author mumumi
|
||||
// @include https://*bigmodel.cn/glm-coding*
|
||||
@@ -514,10 +514,19 @@
|
||||
RUSH_TARGET_HOUR : 9,
|
||||
RUSH_TARGET_MIN : 59,
|
||||
RUSH_TARGET_SEC : 58,
|
||||
// v8.21: 高级模式(默认关闭;开启后可调点击/限流重试间隔,加 ±20% 随机抖动防风控)
|
||||
ADVANCED_MODE : false,
|
||||
CAPTCHA_CLICK_DELAY : 220, // 验证码三个字之间的间隔(ms)
|
||||
RL_RETRY_DELAY : 1000, // 限流弹窗关闭后多久再点购买(ms)
|
||||
};
|
||||
function loadCfg() { try { const s = GM_getValue(STORAGE_KEY, null); return s ? { ...DEF, ...JSON.parse(s) } : { ...DEF }; } catch { return { ...DEF }; } }
|
||||
function saveCfg(c) { GM_setValue(STORAGE_KEY, JSON.stringify(c)); }
|
||||
const CFG = loadCfg();
|
||||
// v8.21: 延迟统一加 ±20% 随机抖动(经典/高级模式都有,防 RPM 风控)
|
||||
function jitterDelay(base) {
|
||||
const delta = base * 0.2;
|
||||
return Math.max(1, Math.round(base + (Math.random() * 2 - 1) * delta));
|
||||
}
|
||||
GM_registerMenuCommand('⚙️ 打开配置面板', openConfigPanel);
|
||||
GM_registerMenuCommand('🗑️ 清除今日套餐状态缓存', () => { localStorage.removeItem(_dsKey); alert('今日状态已清除,即将刷新。'); location.reload(); });
|
||||
GM_registerMenuCommand('🚀 一键多开窗口', openMultipleWindows);
|
||||
@@ -875,6 +884,24 @@
|
||||
<span style="font-size:14px;color:#888">:</span>
|
||||
<input type="number" id="glm-rs" value="${CFG.RUSH_TARGET_SEC}" min="0" max="59" style="width:52px;padding:3px 6px;border:1px solid #d9d9d9;border-radius:4px;font-size:13px;text-align:center">
|
||||
</div>
|
||||
<div style="border-top:1px dashed #eee;padding-top:12px;margin-top:4px"></div>
|
||||
<label style="display:flex;align-items:center;cursor:pointer">
|
||||
<input type="checkbox" id="glm-advanced" ${CFG.ADVANCED_MODE ? 'checked' : ''} style="margin-right:8px">
|
||||
<span style="font-size:14px;color:#555">高级模式(自定义点击/重试速度)</span>
|
||||
<span title="经典模式已自带 ±20% 随机抖动防风控。开启高级模式可调验证码点击间隔、限流重试间隔,抖动仍然保留。" style="margin-left:6px;cursor:help;color:#999;font-size:14px;border:1px solid #ccc;border-radius:50%;width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center;line-height:1">?</span>
|
||||
</label>
|
||||
<div id="glm-advanced-panel" style="margin-top:8px;padding:10px 12px 10px 26px;background:#fafafa;border-radius:6px;display:${CFG.ADVANCED_MODE ? 'block' : 'none'}">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
|
||||
<span style="font-size:13px;color:#666;min-width:140px">验证码点击间隔 (ms)</span>
|
||||
<input type="number" id="glm-ccd" value="${CFG.CAPTCHA_CLICK_DELAY}" min="50" max="2000" step="10" style="width:80px;padding:3px 6px;border:1px solid #d9d9d9;border-radius:4px;font-size:13px;text-align:center">
|
||||
<span style="font-size:12px;color:#999">默认 220,±20% 抖动</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<span style="font-size:13px;color:#666;min-width:140px">限流重试间隔 (ms)</span>
|
||||
<input type="number" id="glm-rld" value="${CFG.RL_RETRY_DELAY}" min="100" max="10000" step="50" style="width:80px;padding:3px 6px;border:1px solid #d9d9d9;border-radius:4px;font-size:13px;text-align:center">
|
||||
<span style="font-size:12px;color:#999">默认 1000,±20% 抖动</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;gap:10px">
|
||||
<button id="glm-multi" style="padding:8px 16px;border:1px solid #52c41a;background:#f6ffed;color:#52c41a;border-radius:6px;cursor:pointer;font-weight:600">🚀 一键多开</button>
|
||||
<div style="display:flex;gap:10px">
|
||||
@@ -887,6 +914,9 @@
|
||||
const getPkgs = buildTransferBox(document.getElementById('glm-wp'), PKGS_MAP, CFG.PACKAGES_PRIORITY, '套餐优先级');
|
||||
const getTabs = buildTransferBox(document.getElementById('glm-wt'), TABS_MAP, CFG.TABS_PRIORITY, '订阅周期优先级');
|
||||
panel.querySelector('#glm-cc').onclick = () => ov.remove();
|
||||
panel.querySelector('#glm-advanced').onchange = (e) => {
|
||||
panel.querySelector('#glm-advanced-panel').style.display = e.target.checked ? 'block' : 'none';
|
||||
};
|
||||
panel.querySelector('#glm-multi').onclick = () => { openMultipleWindows(); };
|
||||
panel.querySelector('#glm-cs').onclick = () => {
|
||||
const p = getPkgs(), t = getTabs();
|
||||
@@ -904,6 +934,9 @@
|
||||
RUSH_TARGET_HOUR: parseInt(panel.querySelector('#glm-rh').value, 10),
|
||||
RUSH_TARGET_MIN: parseInt(panel.querySelector('#glm-rm').value, 10),
|
||||
RUSH_TARGET_SEC: parseInt(panel.querySelector('#glm-rs').value, 10),
|
||||
ADVANCED_MODE: panel.querySelector('#glm-advanced').checked,
|
||||
CAPTCHA_CLICK_DELAY: Math.max(50, parseInt(panel.querySelector('#glm-ccd').value, 10) || 220),
|
||||
RL_RETRY_DELAY: Math.max(100, parseInt(panel.querySelector('#glm-rld').value, 10) || 1000),
|
||||
SAFE_DEFAULTS_VERSION,
|
||||
});
|
||||
ov.remove(); alert('已保存,即将刷新。'); location.reload();
|
||||
@@ -920,6 +953,11 @@
|
||||
console.log('[GLM] rush lock cleared');
|
||||
}
|
||||
if (ensureDiscountEntry()) return;
|
||||
// v8.21: 黄金时间内每天首条提示建议试试无痕模式(sessionStorage 去重)
|
||||
if (isGoldenTime() && !sessionStorage.getItem('glm_nudge_incognito_v1')) {
|
||||
sessionStorage.setItem('glm_nudge_incognito_v1', '1');
|
||||
setBar('🕶️ 抢不到?试试无痕窗口(Ctrl+Shift+N)!没有历史 Cookie/缓存/Service Worker,可能消除隐形的风控标记。Tampermonkey 需在扩展详情页允许在无痕中启用。', '#722ed1');
|
||||
}
|
||||
if (state === 'SLEEPING') {
|
||||
const rem = sleepUntil - Date.now();
|
||||
if (rem <= 0) {
|
||||
@@ -1074,7 +1112,12 @@
|
||||
return;
|
||||
}
|
||||
setBar(`⚠️ 限流 ${taskRLCount}/${MAX_RL},自动关闭后重试...`, '#d46b08');
|
||||
taskPhase = 'IDLE'; return;
|
||||
// v8.21: 高级模式可调限流重试间隔(带 ±20% 抖动防风控)
|
||||
// 期间用 WAITING_RL 状态占位,避免下一轮 tick 重复关弹窗
|
||||
const _rlDelay = jitterDelay(CFG.RL_RETRY_DELAY);
|
||||
taskPhase = 'WAITING_RL';
|
||||
setTimeout(() => { taskPhase = 'IDLE'; }, _rlDelay);
|
||||
return;
|
||||
}
|
||||
if (isPayDialog()) {
|
||||
const verdict = checkPayDialog();
|
||||
@@ -1768,7 +1811,8 @@
|
||||
if (!Number.isFinite(ny) && Number.isFinite(Number(c.rel_y))) ny = Number(c.rel_y) / rect.height;
|
||||
if (!Number.isFinite(nx) || !Number.isFinite(ny)) continue;
|
||||
dispatchClickAt(bgEl, nx * rect.width, ny * rect.height, c.char || String(i + 1));
|
||||
await new Promise(function(r) { setTimeout(r, 220); });
|
||||
var _clickDelay = jitterDelay(CFG.CAPTCHA_CLICK_DELAY);
|
||||
await new Promise(function(r) { setTimeout(r, _clickDelay); });
|
||||
}
|
||||
await new Promise(function(r) { setTimeout(r, 350); });
|
||||
rushState = 'idle';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
cd /d "%~dp0"
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0start-backend-pipeline.ps1"
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0start-backend-pipeline-gui.ps1"
|
||||
pause
|
||||
@@ -1,12 +1,13 @@
|
||||
# GLM Coding Helper - Pipeline Backend Launcher
|
||||
# Usage: powershell -File start-backend-pipeline.ps1
|
||||
# or double-click start-backend-pipeline.cmd
|
||||
# GLM Coding Helper - Pipeline Backend GUI Launcher
|
||||
# Usage: powershell -File start-backend-pipeline-gui.ps1
|
||||
# or double-click start-backend-pipeline-gui.cmd
|
||||
# 弹 Tk 窗口实时显示后端状态、最近识别结果、worker 启动日志
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
$Root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
Set-Location $Root
|
||||
|
||||
Write-Host "GLM Coding Helper - Pipeline Backend" -ForegroundColor Cyan
|
||||
Write-Host "GLM Coding Helper - Pipeline Backend (GUI)" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# ── 查找 Python venv ──
|
||||
@@ -24,15 +25,14 @@ if (Test-Path "$Root\venv\Scripts\python.exe") {
|
||||
|
||||
Write-Host "[信息] 使用 Python: $Python" -ForegroundColor Gray
|
||||
|
||||
# ── 依赖检查 ──
|
||||
$depsCheck = & $Python -c "import fastapi, uvicorn, psutil" 2>&1
|
||||
# ── 依赖检查 (pipeline 依赖 + tkinter) ──
|
||||
$depsCheck = & $Python -c "import fastapi, uvicorn, psutil, tkinter" 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host ""
|
||||
Write-Host "[警告] 当前后端环境缺少 pipeline 依赖 (fastapi/uvicorn/psutil)。" -ForegroundColor Yellow
|
||||
Write-Host "[WARN] Missing pipeline backend dependencies. Environment needs repair." -ForegroundColor Yellow
|
||||
Write-Host "[警告] 当前后端环境缺少依赖 (fastapi/uvicorn/psutil/tkinter)。" -ForegroundColor Yellow
|
||||
Write-Host "[WARN] Missing backend dependencies. Environment needs repair." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
# 尝试自动调用 setup_backend.ps1
|
||||
$setupScript = "$Root\scripts\setup_backend.ps1"
|
||||
if (Test-Path $setupScript) {
|
||||
Write-Host "是否自动安装缺失的依赖?"
|
||||
@@ -42,11 +42,9 @@ if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "[信息] 正在安装 pipeline 依赖..." -ForegroundColor Cyan
|
||||
$env:PYTHONUTF8 = "1"
|
||||
$env:PYTHONIOENCODING = "utf-8"
|
||||
# 直接 pip install 到当前 venv
|
||||
& $Python -m pip install fastapi "uvicorn[standard]" psutil --quiet
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "[失败] 自动安装失败,请手动运行: install-env.cmd" -ForegroundColor Red
|
||||
Write-Host "[FAIL] Auto-install failed. Run manually: install-env.cmd" -ForegroundColor Red
|
||||
Read-Host "按 Enter 退出"
|
||||
exit 1
|
||||
}
|
||||
@@ -70,19 +68,16 @@ if ($portLines) {
|
||||
$parts = $line -split '\s+'
|
||||
$portPid = $parts[-1]
|
||||
|
||||
# 获取进程信息
|
||||
$procName = ""
|
||||
$procCmd = ""
|
||||
try {
|
||||
$proc = Get-Process -Id $portPid -ErrorAction SilentlyContinue
|
||||
if ($proc) { $procName = $proc.ProcessName }
|
||||
$procCmd = (Get-CimInstance Win32_Process -Filter "ProcessId=$portPid" -ErrorAction SilentlyContinue).CommandLine
|
||||
if (-not $procCmd) { $procCmd = "" }
|
||||
} catch {}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "[警告] 端口 8888 已被占用,后端可能已经在运行。" -ForegroundColor Yellow
|
||||
Write-Host "[WARN] Port 8888 is already in use. The backend may already be running." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host "占用进程:" -ForegroundColor DarkYellow
|
||||
Write-Host " PID : $portPid"
|
||||
@@ -93,10 +88,6 @@ if ($portLines) {
|
||||
Write-Host " 1 - 关闭该进程并重新启动后端"
|
||||
Write-Host " Enter - 不处理,直接退出"
|
||||
Write-Host ""
|
||||
Write-Host "Choose:"
|
||||
Write-Host " 1 - Stop this process and restart backend"
|
||||
Write-Host " Enter - Exit without changing anything"
|
||||
Write-Host ""
|
||||
|
||||
$choice = Read-Host "输入 (Input)"
|
||||
if ($choice -eq "1") {
|
||||
@@ -109,14 +100,11 @@ if ($portLines) {
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "[信息] 正在启动 pipeline 后端 http://127.0.0.1:8888" -ForegroundColor Green
|
||||
Write-Host "[提示] 首次启动需要加载模型 (~10秒),请等待 worker 就绪。" -ForegroundColor DarkYellow
|
||||
Write-Host "[信息] Ctrl+C 停止" -ForegroundColor Gray
|
||||
Write-Host "[信息] 启动 GUI 窗口,后端会在窗口中自动拉起。" -ForegroundColor Green
|
||||
Write-Host "[提示] 关闭窗口会同时停止后端子进程。" -ForegroundColor DarkYellow
|
||||
Write-Host ""
|
||||
|
||||
$env:PYTHONUTF8 = "1"
|
||||
$env:PYTHONIOENCODING = "utf-8"
|
||||
|
||||
& $Python "$Root\backend\server.py"
|
||||
|
||||
Read-Host "按 Enter 退出"
|
||||
& $Python "$Root\backend\gui.py"
|
||||
@@ -1,6 +0,0 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
cd /d "%~dp0"
|
||||
echo Starting GLM Coding Helper backend...
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "scripts\one_click_start.ps1" -Target cpu -Port 8888
|
||||
pause
|
||||
@@ -1,6 +0,0 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
cd /d "%~dp0"
|
||||
echo Starting GLM Coding Helper backend...
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "scripts\one_click_start.ps1" -Target cpu -Port 8888
|
||||
pause
|
||||
@@ -1,6 +0,0 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
cd /d "%~dp0"
|
||||
echo Installing GLM Coding Helper backend environment...
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "scripts\setup_backend.ps1" -Target cpu
|
||||
pause
|
||||
Reference in New Issue
Block a user