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:
OLmatter
2026-06-15 22:53:36 +08:00
co-authored by Claude Opus 4.7
parent 9410388eff
commit 66a9ea559e
15 changed files with 729 additions and 83 deletions
+2
View File
@@ -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/
+26 -17
View File
@@ -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
View File
@@ -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
View File
@@ -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")
-6
View File
@@ -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
+2 -2
View File
@@ -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..."
+7 -6
View File
@@ -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.
+7 -5
View File
@@ -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
+269
View File
@@ -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())
+47 -3
View File
@@ -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"
-6
View File
@@ -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
-6
View File
@@ -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
-6
View File
@@ -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