Compare commits

..

4 Commits

Author SHA1 Message Date
75a332e7b8 release: docs, browser_service, runtime_env, SKILL v1.0.52
All checks were successful
技能自动化发布 / release (push) Successful in 28s
2026-04-08 14:00:42 +08:00
4605d3d0e4 release: runtime_env marker, SKILL v1.0.51
All checks were successful
技能自动化发布 / release (push) Successful in 25s
2026-04-08 12:05:14 +08:00
beb95c1415 release: v1.0.50 playwright stealth gemini
All checks were successful
技能自动化发布 / release (push) Successful in 34s
2026-04-07 15:18:14 +08:00
e2eafb7407 feat: remove wait-login; pick-web only; docs sync
All checks were successful
技能自动化发布 / release (push) Successful in 33s
2026-04-07 11:58:27 +08:00
13 changed files with 108 additions and 293 deletions

View File

@@ -1,7 +1,7 @@
---
name: 账号管理
description: 多平台多账号管理。管理各平台账号与Chrome Profile的对应关系供publisher类Skill调用获取账号信息。
version: 1.0.5
version: 1.0.52
author: 深圳匠厂科技有限公司
metadata:
openclaw:
@@ -14,6 +14,10 @@ allowed-tools:
# 账号管理
## JIANGCHANG 共享 Python匠厂桌面
依赖由宿主安装到 `{JIANGCHANG_DATA_ROOT}/python-runtime/.venv`(技能市场安装时自动 `uv sync`)。勿在技能目录内创建 `.venv`。Playwright 使用本机 **Chrome / Edge**`channel=chrome|msedge`**不**下载自带 Chromium。
## 使用时机
当用户发送以下内容的时候触发本Skill
@@ -53,14 +57,13 @@ python3 {baseDir}/scripts/main.py list all
python3 {baseDir}/scripts/main.py list 搜狐号
```
### 跨技能:单行 JSONget / list-json / pick-web;需会话有效时用 wait-login
### 跨技能:单行 JSONget / list-json / pick-web
```bash
python3 {baseDir}/scripts/main.py get <id>
python3 {baseDir}/scripts/main.py list-json all --limit 50
python3 {baseDir}/scripts/main.py pick-web 搜狐号
python3 {baseDir}/scripts/main.py wait-login <id>
```
约定与字段说明见 `references/INTEGRATION.md`;错误前缀见 `references/ERRORS.md`
约定与字段说明见 `references/INTEGRATION.md`;错误前缀见 `references/ERRORS.md`是否已登录由调用方网页会话处理,本 skill 不做编排用预检。
## 扩展文档(便于 AI 精确调用)

View File

@@ -81,16 +81,7 @@ python main.py login <id>
- 依赖 Playwright超时与轮询间隔等见 [RUNTIME.md](RUNTIME.md)。
- **不**在库中持久化「是否已登录」;仅刷新 `updated_at``pick-web` 等排序参考。
### `wait-login` — 跨技能:**始终以本次 DOM 检测为准**
```bash
python main.py wait-login <id>
```
- 由**账号管理**打开浏览器并完成 DOM 检测;调用方只需 `subprocess` 等待本进程结束,成功时 stdout 含 `OK:WAIT_LOGIN`
- 会先打印若干行说明(含 `INFO:WAIT_LOGIN_BEGIN`),再进入检测;超时、关窗、启动失败等见 [ERRORS.md](ERRORS.md)。
- 时间窗由 `JIANGCHANG_WAIT_LOGIN_TIMEOUT_SECONDS` 控制,见 [RUNTIME.md](RUNTIME.md)。
- 跨技能编排(如 llm-manager 网页生成)**不再**通过本 skill 单独做「登录预检」;由业务侧在同一浏览器会话内处理未登录态。
### `delete` — 删除库记录并尽量删除 profile 目录

View File

@@ -14,7 +14,6 @@
| `ERROR:CLI_DELETE_BAD_MODE` | `delete` 第二段不是 `id`/`platform` |
| `ERROR:CLI_LIST_JSON_MISSING_PLATFORM` | `list-json` 缺少平台参数 |
| `ERROR:CLI_PICK_WEB_MISSING_ARGS` | `pick-web` 缺少平台 |
| `ERROR:CLI_WAIT_LOGIN_MISSING_ID` | `wait-login` 缺少 id |
| `ERROR:CLI_UNKNOWN_OR_BAD_ARGS` | 无法识别的子命令或参数组合 |
## `ERROR:` — 业务与数据
@@ -36,22 +35,9 @@
| 前缀 | 含义 |
|------|------|
| `ERROR:需要 playwrightpip install playwright && playwright install chromium` | 未安装 Playwright 或浏览器未安装 |
| `ERROR:需要 playwright Python 包:…` | 未安装 `playwright` 包,或本机无 Chrome/Edgechannel 模式,**不**需 `playwright install chromium` |
| `ERROR:PROFILE_DIR_MISSING` | 账号记录中 `profile_dir` 为空 |
## `ERROR:` — `wait-login`(跨技能编排)
| 前缀 | 含义 |
|------|------|
| `ERROR:WAIT_LOGIN_ABORTED` | 用户关闭浏览器,放弃登录 |
| `ERROR:WAIT_LOGIN_TIMEOUT` | 限时内未完成登录(默认由 `JIANGCHANG_WAIT_LOGIN_TIMEOUT_SECONDS` 控制,约 180s |
| `ERROR:WAIT_LOGIN_LAUNCH_FAILED` | 无法启动浏览器或创建持久化上下文(端口/占用/安装问题等) |
| `ERROR:WAIT_LOGIN_PAGE_ERROR` | 打开登录页或轮询 DOM 时异常 |
| `ERROR:WAIT_LOGIN_CONFIG_ERROR` | 子进程配置异常 |
| `ERROR:WAIT_LOGIN_CHILD_CRASH` | 子进程异常退出且未写入有效结果文件 |
编排技能(如 llm-manager另可能打印 `ERROR:WAIT_LOGIN_OUTER_TIMEOUT`:等待本进程整体超过上限(登录窗口 + 子进程缓冲),已中止。
## 非前缀输出
- **成功添加账号**、**删除成功**等可能包含 `✅` 等人类可读行,**不宜**作为稳定协议解析。

View File

@@ -24,12 +24,12 @@
常见键名包括:`id`, `name`, `platform`, `phone`, `profile_dir`, `url`, `created_at`, `updated_at``extra_json` 列中的扁平扩展可能并入同一对象。
**库中不再持久化「是否已登录」**;编排侧需登录时请调用 `wait-login <id>`(始终以本次 Playwright DOM 检测为准)或在业务页内自行判断
**库中不再持久化「是否已登录」**;编排侧在网页自动化中自行处理登录(与 `pick-web` / `get` 返回的 `profile_dir` 同一浏览器会话)
## Publisher / 自动化流水线示例
1. `pick-web 搜狐号`(或目标平台)→ 解析 JSON → 取 `id``profile_dir``url`
2. 需要确保会话有效时:调用 `wait-login <id>`,成功则 stdout 首行含 `OK:WAIT_LOGIN`
2. 使用 `profile_dir` 启动持久化浏览器上下文,在业务页面内完成或等待登录
## 退出码

View File

@@ -46,10 +46,10 @@
| `JIANGCHANG_LOGIN_POLL_INTERVAL_SECONDS` | DOM 轮询间隔,默认 `1.5`,限制在 `0.5..10` |
| `JIANGCHANG_LOGIN_DOM_GRACE_SECONDS` | 打开 URL 后等待再检测,默认 `4`,减轻跳转误判 |
依赖:**本机已安装 Chrome 或 Edge**Python 环境需 **`playwright`** 且已执行 `playwright install chromium`错误信息见 CLI 输出)。
依赖:**本机已安装 Chrome 或 Edge**Python 环境需 **`playwright` 包**(匠厂共享 `python-runtime` 已包含)。运行时使用 **Playwright `channel=chrome|msedge`** 调系统浏览器,**不要**执行 `playwright install chromium`避免下载自带 Chromium)。
## 常见问题(简要)
- **`ERROR:需要 playwright`**:安装 `playwright` 并安装浏览器内核
- **`ERROR:需要 playwright Python 包`**:确认共享 venv 已 `uv sync`;本机安装 Chrome 或 Edge
- **`ERROR:PROFILE_DIR_MISSING`**:库中该账号 `profile_dir` 为空,需检查数据或重新添加账号流程。
- **登录检测一直失败**:关闭占用同一 profile 目录的其它浏览器;可调高超时与环境变量;查看统一日志文件。

View File

@@ -11,7 +11,7 @@ from service.account_service import (
cmd_list_json,
cmd_pick_web,
)
from service.browser_service import cmd_login, cmd_open, cmd_wait_login
from service.browser_service import cmd_login, cmd_open
from util.constants import LOG_LOGGER_NAME, SKILL_SLUG
from util.logging_config import get_skill_logger, setup_skill_logging
from util.platforms import _platform_list_cn_for_help, resolve_platform_key
@@ -28,7 +28,6 @@ def _cli_print_full_usage() -> None:
print(" python main.py get <id>")
print(" python main.py open <id>")
print(" python main.py login <id>")
print(" python main.py wait-login <id> # 跨技能:限时内 DOM 检测登录(默认 180s")
print(" python main.py delete id <id>")
print(" python main.py delete platform <平台>")
print(" python main.py delete platform <平台> <手机号>")
@@ -126,7 +125,7 @@ def main() -> None:
if len(sys.argv) < 3:
print("ERROR:CLI_PICK_WEB_MISSING_ARGS")
print("用法python main.py pick-web <平台中文名或英文键>")
print("说明:按 updated_at 选一条账号;是否已登录由 wait-login / 页面检测决定")
print("说明:按 updated_at 选一条账号;是否已登录由调用方网页会话自行处理")
sys.exit(1)
cmd_pick_web(sys.argv[2])
elif cmd == "open":
@@ -134,14 +133,6 @@ def main() -> None:
_cli_fail_need_account_id("open")
sys.exit(1)
cmd_open(sys.argv[2])
elif cmd == "wait-login":
av = [a for a in sys.argv[2:] if a]
if len(av) < 1:
print("ERROR:CLI_WAIT_LOGIN_MISSING_ID")
print("用法python main.py wait-login <账号 id>")
print("说明:始终以本次 DOM 检测为准;供 llm-manager / 搜狐发布等编排。")
sys.exit(1)
sys.exit(cmd_wait_login(av[0]))
elif cmd == "login":
if len(sys.argv) < 3:
_cli_fail_need_account_id("login")

View File

@@ -1,258 +1,137 @@
"""
JIANGCHANG_* 数据根与用户目录:解析规则 + 可选本地 CLI 默认值。
各技能 scripts/jiangchang_skill_core/ 为发布用副本,与 kit 保持同步。
宿主在 skills.entries 与 Gateway 中注入:
- PATH 前缀:{JIANGCHANG_DATA_ROOT}/python-runtime/.venv/(Scripts|bin)
- VIRTUAL_ENV、JIANGCHANG_PYTHON_EXE
Playwright 使用本机 Chrome/Edgelaunch 时 channel=chrome|msedge不依赖宿主下载的 Chromium 包。
技能勿在仓库内维护独立 .venv依赖以 jiangchang-platform-kit/python-runtime 锁文件为准。
"""
from __future__ import annotations
import os
import sys
# 发版/嵌入宿主前改为 False或仅通过环境变量 JIANGCHANG_CLI_LOCAL_DEV=1 开启
CLI_LOCAL_DEV_ENABLED = True
# 本地开发且未设置 JIANGCHANG_USER_ID 时注入(与宿主约定一致即可修改)
DEFAULT_LOCAL_USER_ID = "10032"
# Windows 下未设置 JIANGCHANG_DATA_ROOT 时的默认盘路径
WIN_DEFAULT_DATA_ROOT = r"D:\jiangchang-data"
# 匠厂桌面宿主应用根(未设置 JIANGCHANG_APP_ROOT 时 Windows 兜底;技能一般在 {APP}/skills/ 下并列)
WIN_DEFAULT_JIANGCHANG_APP_ROOT = r"D:\AI\jiangchang"
def platform_default_data_root() -> str:
if sys.platform == "win32":
return WIN_DEFAULT_DATA_ROOT
return os.path.join(os.path.expanduser("~"), ".jiangchang-data")
def get_data_root() -> str:
env = (os.getenv("JIANGCHANG_DATA_ROOT") or "").strip()
if env:
return env
return platform_default_data_root()
def get_user_id() -> str:
return (os.getenv("JIANGCHANG_USER_ID") or "").strip() or "_anon"
def _looks_like_skills_root(path: str) -> bool:
if not path or not os.path.isdir(path):
return False
for marker in (
"llm-manager",
"content-manager",
"account-manager",
"sohu-publisher",
"toutiao-publisher",
"logistics-tracker",
"api-key-vault",
):
if os.path.isdir(os.path.join(path, marker)):
return True
return False
def get_skills_root() -> str:
"""
并列技能安装目录(其下为「技能 slug」子目录
优先级:
1) JIANGCHANG_SKILLS_ROOT
2) CLAW_SKILLS_ROOT
3) JIANGCHANG_APP_ROOT{APP}/skills 存在且像技能根则用之;否则若 APP 下已有技能子目录则 APP 即根
4) Windows默认 D:\\AI\\jiangchang 同上规则
5) 其他平台:~/.openclaw/skills
"""
for key in ("JIANGCHANG_SKILLS_ROOT", "CLAW_SKILLS_ROOT"):
v = (os.getenv(key) or "").strip()
if v:
return os.path.normpath(v)
app = (os.getenv("JIANGCHANG_APP_ROOT") or "").strip()
if sys.platform == "win32" and not app:
app = WIN_DEFAULT_JIANGCHANG_APP_ROOT
if app:
nested = os.path.join(app, "skills")
if _looks_like_skills_root(nested):
return os.path.normpath(nested)
if _looks_like_skills_root(app):
return os.path.normpath(app)
if sys.platform == "win32":
nested = os.path.join(WIN_DEFAULT_JIANGCHANG_APP_ROOT, "skills")
if _looks_like_skills_root(nested):
return os.path.normpath(nested)
if _looks_like_skills_root(WIN_DEFAULT_JIANGCHANG_APP_ROOT):
return os.path.normpath(WIN_DEFAULT_JIANGCHANG_APP_ROOT)
return os.path.normpath(os.path.join(os.path.expanduser("~"), ".openclaw", "skills"))
def get_sibling_skills_root(skill_scripts_dir: str | None = None) -> str:
"""
编排子进程调用兄弟技能时用:先根据本技能 scripts 目录推断并列根;若目录下能识别出兄弟技能
则用之(开发仓与安装目录均满足「技能根之上一级 = 并列根」),
避免全局 JIANGCHANG_SKILLS_ROOT / CLAW_SKILLS_ROOT 与当前检出不一致时错调兄弟进程。
则用之(开发仓 D:\\OpenClaw 与安装目录 ~/.openclaw/skills 均满足「技能根之上一级 = 并列根」),
避免全局 JIANGCHANG_SKILLS_ROOT / CLAW_SKILLS_ROOT 指向与当前检出不一致时错调兄弟进程。
推断失败时再读上述环境变量,最后回落 get_skills_root()。
"""
if skill_scripts_dir:
scripts = os.path.abspath(skill_scripts_dir)
skill_root = os.path.dirname(scripts)
inferred = os.path.dirname(skill_root)
if _looks_like_skills_root(inferred):
return os.path.normpath(inferred)
for key in ("JIANGCHANG_SKILLS_ROOT", "CLAW_SKILLS_ROOT"):
v = (os.getenv(key) or "").strip()
if v:
return os.path.normpath(v)
return get_skills_root()
def apply_cli_local_defaults() -> None:
"""
在 CLI 最早阶段调用main.py 在 import 业务包之前)。
宿主已设置 JIANGCHANG_* 时不会覆盖。
"""
enabled = CLI_LOCAL_DEV_ENABLED
if not enabled:
v = (os.getenv("JIANGCHANG_CLI_LOCAL_DEV") or "").strip().lower()
enabled = v in ("1", "true", "yes", "on")
if not enabled:
return
if not (os.getenv("JIANGCHANG_DATA_ROOT") or "").strip():
os.environ["JIANGCHANG_DATA_ROOT"] = platform_default_data_root()
if not (os.getenv("JIANGCHANG_USER_ID") or "").strip():
os.environ["JIANGCHANG_USER_ID"] = DEFAULT_LOCAL_USER_ID.strip()

View File

@@ -138,7 +138,7 @@ def cmd_list_json(platform_input: str, limit: int = 200) -> None:
def cmd_pick_web(platform_input: str):
"""
供 llm-manager 等:取该平台用于网页自动化的账号候选。
按 updated_at、created_at 倒序选一条,与库内「登录态」无关;实际是否已登录由 wait-login / 页面 DOM 判定
按 updated_at、created_at 倒序选一条;是否已登录由调用方(如网页自动化)在同一会话内处理
成功stdout 仅一行 JSON与 get 一致);失败:首行 ERROR:。
"""
get_skill_logger().info("pick_web platform_input=%r", platform_input)

View File

@@ -99,14 +99,6 @@ def _login_dom_grace_seconds() -> float:
return 4.0
def _wait_login_timeout_seconds() -> int:
"""供 wait-login / llm 编排:默认 180 秒JIANGCHANG_WAIT_LOGIN_TIMEOUT_SECONDS。"""
try:
return max(30, min(600, int((os.getenv("JIANGCHANG_WAIT_LOGIN_TIMEOUT_SECONDS") or "180").strip())))
except ValueError:
return 180
def _login_out_bundle_for(platform_key: str) -> dict:
"""传给子进程anchor + 未登录可见的选择器列表。"""
spec = PLATFORMS.get(platform_key, {}) if platform_key else {}
@@ -154,7 +146,7 @@ def _run_login_browser_session(
except ImportError:
log.error("login_aborted playwright_missing")
if verbose_ui:
print("ERROR:需要 playwrightpip install playwright && playwright install chromium")
print("ERROR:需要 playwright Python 包:请在匠厂共享环境中已含 playwright本机需安装 Google Chrome 或 Microsoft Edgechannel 模式,无需 playwright install chromium")
return False, "playwright_missing"
profile_dir = (target.get("profile_dir") or "").strip()
@@ -176,6 +168,13 @@ def _run_login_browser_session(
if verbose_ui:
print(f"正在为账号 [{target['name']}] 打开 {browser_name}")
print(f"访问地址:{url}")
if platform == "gemini":
print(
"Gemini / Google 登录:若提示「浏览器不安全」或「无法登录」,请关闭本窗口后,"
"用普通 Chrome 启动参数 --user-data-dir= 指向下面 profile 目录,在普通浏览器里完成"
" Google 登录后完全退出 Chrome再执行本 login详见 llm-manager Gemini 引擎说明。"
)
print(f" (本账号 profile 目录){profile_dir}")
print(
"请在窗口内完成登录。系统根据页面是否仍出现「登录」等未登录控件判断;"
"成功后自动关闭窗口并写入状态(无需手动关浏览器)。"
@@ -346,110 +345,6 @@ def cmd_login(account_id):
print(f" 详细轮询日志见:{log_file}(可将 JIANGCHANG_LOG_LEVEL=DEBUG 打开更细粒度)")
def cmd_wait_login(account_id) -> int:
"""
跨技能编排:始终以本次 Playwright DOM 检测为准;限时内打开浏览器并等待用户完成登录。
返回 0本次检测判定已登录1失败超时 / 用户关浏览器 / 其它)。
stdout 含 OK:WAIT_LOGIN 或 ERROR:WAIT_LOGIN_*。
"""
log = get_skill_logger()
log.info("wait_login_command account_id=%r", account_id)
target = get_account_by_id(account_id)
if not target:
log.warning("wait_login_aborted account_not_found account_id=%r", account_id)
print("ERROR:ACCOUNT_NOT_FOUND")
return 1
if not resolve_chromium_channel():
_print_browser_install_hint()
return 1
try:
import playwright # noqa: F401
except ImportError:
print("ERROR:需要 playwrightpip install playwright && playwright install chromium")
return 1
if not (target.get("profile_dir") or "").strip():
print("ERROR:PROFILE_DIR_MISSING 库中 profile_dir 为空。")
return 1
sec = _wait_login_timeout_seconds()
log.info(
"wait_login_orchestration account_id=%s name=%r timeout_sec=%s",
target.get("id"),
target.get("name"),
sec,
)
print("INFO:WAIT_LOGIN_BEGIN", flush=True)
print(
f"账号管理account-manager正在为本账号执行登录检测将打开浏览器请在 {sec} 秒内在窗口中完成登录。",
flush=True,
)
print(
"说明:调用方/编排技能只需等待本进程结束;打开浏览器与 DOM 检测均由账号管理负责。",
flush=True,
)
print(
"· 超时未登录成功 → 本进程报错退出,请中止后续业务。",
flush=True,
)
print(
"· 提前关闭浏览器 → 视为放弃登录,本进程立即报错退出。",
flush=True,
)
ok, end_reason = _run_login_browser_session(target, sec, verbose_ui=False)
if ok:
print("OK:WAIT_LOGIN")
return 0
if end_reason == "user_closed":
print("ERROR:WAIT_LOGIN_ABORTED 已关闭浏览器,未完成登录。")
return 1
if end_reason == "timeout":
print("ERROR:WAIT_LOGIN_TIMEOUT 限时内未完成登录。")
return 1
if end_reason == "playwright_missing":
print("ERROR:需要 playwrightpip install playwright && playwright install chromium")
return 1
if end_reason == "profile_missing":
print("ERROR:PROFILE_DIR_MISSING 库中 profile_dir 为空。")
return 1
if end_reason == "no_channel":
_print_browser_install_hint()
return 1
if end_reason == "launch_failed":
print(
"ERROR:WAIT_LOGIN_LAUNCH_FAILED 无法启动浏览器或创建用户数据上下文,请确认本机 Chrome/Edge 可用、profile 目录未被占用,详见统一日志。"
)
return 1
if end_reason == "page_error":
print(
"ERROR:WAIT_LOGIN_PAGE_ERROR 打开登录页或轮询过程异常,请查看上方 stderr 与统一日志。"
)
return 1
if end_reason == "config_error":
print("ERROR:WAIT_LOGIN_CONFIG_ERROR 登录子进程配置异常。")
return 1
if end_reason == "child_crash":
print("ERROR:WAIT_LOGIN_CHILD_CRASH 登录检测子进程异常退出且未写入有效结果。")
return 1
if end_reason == "profile_dir_unusable":
print(
"ERROR:WAIT_LOGIN_PROFILE_DIR 用户数据目录不可用或无法访问(权限/路径等),详见统一日志中的 login_child_result_detail / stderr。"
)
return 1
if end_reason == "logging_import_failed":
print(
"ERROR:WAIT_LOGIN_INTERNAL 登录子进程无法加载日志模块,请检查 account-manager 安装是否完整。"
)
return 1
if end_reason == "child_fatal":
print(
"ERROR:WAIT_LOGIN_CHILD_FATAL 登录子进程异常,详见统一日志 login_child_result_detail 或 login_child_stderr。"
)
return 1
print(f"ERROR:WAIT_LOGIN_FAILED end_reason={end_reason}")
return 1
def cmd_open(account_id):
"""打开该账号的持久化浏览器,仅用于肉眼确认是否已登录;不写数据库。"""
get_skill_logger().info("open account_id=%r", account_id)
@@ -467,7 +362,7 @@ def cmd_open(account_id):
try:
import playwright # noqa: F401
except ImportError:
print("ERROR:需要 playwrightpip install playwright && playwright install chromium")
print("ERROR:需要 playwright Python 包:请在匠厂共享环境中已含 playwright本机需安装 Google Chrome 或 Microsoft Edgechannel 模式,无需 playwright install chromium")
return
profile_dir = (target.get("profile_dir") or "").strip()

View File

@@ -16,6 +16,12 @@ _scripts_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _scripts_dir not in sys.path:
sys.path.insert(0, _scripts_dir)
from util.playwright_stealth import (
STEALTH_INIT_SCRIPT,
persistent_context_launch_parts,
stealth_enabled,
)
def _is_context_closed_error(ex: BaseException) -> bool:
s = str(ex).lower()
@@ -224,13 +230,19 @@ def _run_login_child(c: dict) -> None:
with sync_playwright() as p:
try:
ctx = p.chromium.launch_persistent_context(
args, ignore_automation = persistent_context_launch_parts()
lc_kw = dict(
user_data_dir=profile_dir,
headless=False,
channel=c["channel"],
no_viewport=True,
args=["--start-maximized"],
args=args,
)
if ignore_automation is not None:
lc_kw["ignore_default_args"] = ignore_automation
ctx = p.chromium.launch_persistent_context(**lc_kw)
if stealth_enabled():
ctx.add_init_script(STEALTH_INIT_SCRIPT)
except Exception as e:
if _is_context_closed_error(e):
end_reason = "user_closed"

View File

@@ -4,18 +4,30 @@ import sys
from playwright.sync_api import sync_playwright
from util.playwright_stealth import (
STEALTH_INIT_SCRIPT,
persistent_context_launch_parts,
stealth_enabled,
)
def main():
with open(sys.argv[1], encoding="utf-8") as f:
c = json.load(f)
with sync_playwright() as p:
ctx = p.chromium.launch_persistent_context(
args, ignore_automation = persistent_context_launch_parts()
lc_kw = dict(
user_data_dir=c["profile_dir"],
headless=False,
channel=c["channel"],
no_viewport=True,
args=["--start-maximized"],
args=args,
)
if ignore_automation is not None:
lc_kw["ignore_default_args"] = ignore_automation
ctx = p.chromium.launch_persistent_context(**lc_kw)
if stealth_enabled():
ctx.add_init_script(STEALTH_INIT_SCRIPT)
try:
page = ctx.pages[0] if ctx.pages else ctx.new_page()
page.goto(c["url"], wait_until="domcontentloaded", timeout=60000)

View File

@@ -62,6 +62,12 @@ PLATFORMS = {
"prefix": "元宝",
"aliases": ["元宝"],
},
"gemini": {
"url": "https://gemini.google.com",
"label": "Gemini",
"prefix": "Gemini",
"aliases": ["谷歌Gemini", "Google Gemini", "google gemini", "Bard"],
},
}
# 未登录页共性主行动点含「登录」——submit 按钮、Element 主按钮、Semi 文案、标题、通栏主按钮等Playwright :has-text 可覆盖多数站点。

View File

@@ -0,0 +1,40 @@
"""
淡化 Playwright 启动时的自动化指纹,减轻 Google 账号页「此浏览器或应用可能不安全」类拦截。
关闭:环境变量 OPENCLAW_PLAYWRIGHT_STEALTH=0或 false/off/no
"""
from __future__ import annotations
import os
STEALTH_INIT_SCRIPT = """
(() => {
try {
Object.defineProperty(navigator, "webdriver", { get: () => undefined });
} catch (e) {}
})();
"""
def stealth_enabled() -> bool:
v = (os.environ.get("OPENCLAW_PLAYWRIGHT_STEALTH") or "1").strip().lower()
return v not in ("0", "false", "no", "off")
def persistent_context_launch_parts(
*,
extra_args: list[str] | None = None,
) -> tuple[list[str], list[str] | None]:
"""
返回 (args, ignore_default_args)。
ignore_default_args 为 None 时不应传给 launch_persistent_context。
"""
args = ["--start-maximized"]
if extra_args:
args = args + list(extra_args)
if not stealth_enabled():
return args, None
if "--disable-blink-features=AutomationControlled" not in args:
args.append("--disable-blink-features=AutomationControlled")
return args, ["--enable-automation"]