Files
workbuddy_skills/browser-harness/agent-workspace/bh_chrome_keepalive.py
T
admin fe37c94096 browser-harness:把「Windows 起浏览器」的可抄三步写进正文,启动器随包
其他会话反馈(用户转述):「技能里写了,但说得不完整——这就是我反复踩坑的原因。
正文只写了『起实例 = 丢进常驻后台任务』,没给能直接抄的命令,也没说 Windows 上该用哪个启动器;
真正能跑的入口在技能外的另一个文件里;我照正文起,没做永不退出的循环 ⇒ 任务一结束 Chrome 被连坐。」

真因(读到代码才看清,两节自相矛盾):
- §1 说「要丢进常驻后台任务、且一直不退出」,§2 却直接给了一条 **同步** 的 `chrome.exe` 命令。
  照 §2 抄 ⇒ 父进程一退 Chrome 立刻死(实测 `WinError 10061`)。
- 起法真正落在**工作区根**的 `bh_chrome_keepalive.py`(`while True` 守端口那段),而那支脚本**不在技能包里**。

改动
- 新增 `agent-workspace/bh_chrome_keepalive.py`(随包的可复制启动器,行为沿用已实测那支):
  幂等(端口已在听 ⇒ 立刻退出)· `while True` 守子进程、退出就重起 · 僵死态**只记日志不自动重启**(保住现场)·
  起 Chrome 前清代理 env · `DETACHED|NO_WINDOW|NEW_PROCESS_GROUP` ·
  参数化 `--port/--chrome/--profile/--log/--proxy`(默认值与线上一致;`--proxy` 对应文档里那条 socks5)。
- `SKILL.md` §1 改写成**可直接抄的三步**:复制启动器 → `pythonw.exe` 起 → 计划任务兜(动作也用 pythonw、
  5 分钟一次、MultipleInstances=IgnoreNew;`schtasks.exe` 在本机黑名单 ⇒ 走 PowerShell ScheduledTasks);
  另给校验两条(netstat / curl --noproxy)。
- `SKILL.md` §2 加前置说明:那条 `chrome.exe` 是**启动器脚本内部的那一步**,用途是看懂与排障;
  并补 `--proxy` 的用法。
- 顶部那颗「起实例」子弹由「细节见 §1,本处不复述」改成「三步见 §1」——**从"叮嘱"改成"给命令"**。
- 表述按 2026-10-09 的「一律用肯定表述」写:先写要什么(永不退出的循环 / 保住现场),
  把踩过的坑放括号里当依据;僵死态那两条由「别把 A 当成 B」改成正向判据「两条都过才叫可用」。

验证
- `--help` 正常;`--chrome Z:/不存在` ⇒ rc=2 且不起浏览器(守卫)。
- 真机幂等:9223 正在听 ⇒ 跑它 rc=0、日志 `port 9223 already listening ⇒ exit (idempotent)`、端口数不变(没新起浏览器)。
- ⚠️ 已知不一致(本次不改,见次日报告):contentm_agent 工作区根的部署副本与 `tmp/bh_keep_chrome.py`
  仍是旧的非参数化版本;本次只把**源**落进技能包。
2026-10-10 16:26:18 +08:00

122 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""常驻守端口:让本机有一个可用的独立 Chrome 实例(默认 9223,固定 profile ⇒ 登录态保留)。
## 它解决什么
本机沙箱**在父进程退出时连带杀子进程** ⇒ 会话/工具调用里起的浏览器,调用一返回就死
(症状:`netstat` 查不到端口、`curl` 报 `WinError 10061`)。
要的是:浏览器活在一个**永不退出的循环**里 —— 起完 Chrome 就守在端口上。
## 怎么用(三步,Windows 实测 2026-10-10)
1、复制到**工作区根**(⛔ 别放 `tmp/`:`tmp/` 会被清理,脚本一没、计划任务就静默失效)
2、用 **`pythonw.exe`** 起(⛔ 别用 `python.exe`:控制台程序会闪黑窗)
3、让**计划任务**兜住它(进程挂了自动拉起)
## 做法约定
- **幂等**:端口已在听 ⇒ 立刻退出。(避免叠加第二个实例、避免两个实例抢同一个 profile)
- **守**:`while True` 里看着子进程;它退出了就重新起。
- **僵死态只记日志**:端口不应答而进程仍在时,脚本**不自动重启** —— 那会把正在被排查的现场清掉,
留给人/会话按 SKILL.md 的判据处置。
- **清代理 env**:`HTTP_PROXY` / `HTTPS_PROXY` 会劫持本地 CDP 连接(报 `502 Bad Gateway`)。
- **只用独立实例**:⛔ 不附着用户日常 Chrome(那是 9222,会弹「允许远程调试?」动到用户浏览器)。
用法:
pythonw bh_chrome_keepalive.py --log "<工作区根>/tmp/_bh_chrome.log"
可选:`--port` `--chrome` `--profile`(都有默认值,见下)
"""
from __future__ import annotations
import argparse
import io
import os
import socket
import subprocess
import sys
import time
# 清掉会劫持本地 CDP 的代理变量(要在起 Chrome 之前做)
for _k in ("HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"):
os.environ.pop(_k, None)
os.environ.setdefault("NO_PROXY", "127.0.0.1,localhost")
DEFAULT_CHROME = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
DEFAULT_PROFILE = r"E:/ProgramData/.workbuddy/tmp/bu-chatgpt-profile"
DEFAULT_PORT = 9223
def _log(path, msg):
if not path:
return
try:
os.makedirs(os.path.dirname(path), exist_ok=True)
with io.open(path, "a", encoding="utf-8") as f:
f.write("[%s] %s\n" % (time.strftime("%m-%d %H:%M:%S"), msg))
except Exception:
pass
def _listening(port: int) -> bool:
try:
s = socket.create_connection(("127.0.0.1", port), 1)
s.close()
return True
except Exception:
return False
def _launch(chrome: str, profile: str, port: int, proxy: str = ""):
cmd = [chrome, "--remote-debugging-port=%d" % port,
"--user-data-dir=%s" % profile,
"--no-first-run", "--no-default-browser-check"]
if proxy:
# 目标站点要走代理时才加(例:--proxy socks5://127.0.0.1:10800)
cmd.append("--proxy-server=%s" % proxy)
cmd.append("about:blank")
return subprocess.Popen(
cmd,
# DETACHED | CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP
creationflags=0x00000008 | 0x08000000 | 0x00000200,
)
def main() -> int:
ap = argparse.ArgumentParser(add_help=True)
ap.add_argument("--port", type=int, default=DEFAULT_PORT)
ap.add_argument("--chrome", default=os.environ.get("BH_CHROME") or DEFAULT_CHROME)
ap.add_argument("--profile", default=os.environ.get("BH_PROFILE") or DEFAULT_PROFILE)
ap.add_argument("--log", default=os.environ.get("BH_LOG") or "")
ap.add_argument("--proxy", default=os.environ.get("BH_PROXY") or "",
help='目标站点要走代理时才给,例 --proxy socks5://127.0.0.1:10800')
a = ap.parse_args()
if not os.path.isfile(a.chrome):
_log(a.log, "chrome 可执行体不存在:%s ⇒ 停(⛔ 不静默)" % a.chrome)
print("✗ 找不到 chrome:%s" % a.chrome, file=sys.stderr)
return 2
# ── 幂等前置:端口已在听 ⇒ 什么都不做 ──────────────────────────
if _listening(a.port):
_log(a.log, "port %d already listening ⇒ exit (idempotent)" % a.port)
return 0
p = _launch(a.chrome, a.profile, a.port, a.proxy)
_log(a.log, "launched pid=%s" % p.pid)
# ── 守:循环不结束 ⇒ Chrome 不被连坐 ───────────────────────────
while True:
time.sleep(5)
if p.poll() is not None:
_log(a.log, "chrome exited rc=%s ⇒ relaunch" % p.returncode)
p = _launch(a.chrome, a.profile, a.port, a.proxy)
_log(a.log, "relaunched pid=%s" % p.pid)
elif not _listening(a.port):
# ⚠️ 僵死态(端口不应答、进程还在):只记日志,⛔ 不自动重启 ——
# 自动重启会把"正在被排查的现场"清掉。判据与处置见 SKILL.md §1。
_log(a.log, "port %d not answering (pid alive=True)" % a.port)
if __name__ == "__main__":
sys.exit(main())