434 lines
10 KiB
Bash
Executable File
434 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
|
|
CLAUDE_HOME="${CLAUDE_HOME:-$HOME/.claude}"
|
|
GSTACK_REPO_URL="${GSTACK_REPO_URL:-https://github.com/garrytan/gstack.git}"
|
|
VERIFY_URL="${VERIFY_URL:-https://example.com}"
|
|
SETUP_DOC="$CODEX_HOME/browser-control-setup.md"
|
|
|
|
TMP_DIR="$(mktemp -d)"
|
|
cleanup() {
|
|
rm -rf "$TMP_DIR"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
log() {
|
|
printf '[install-browser-control] %s\n' "$*" >&2
|
|
}
|
|
|
|
fail() {
|
|
printf '[install-browser-control] ERROR: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
ensure_macos() {
|
|
local os_name
|
|
os_name="$(uname -s)"
|
|
if [[ "$os_name" != "Darwin" ]]; then
|
|
fail "当前脚本仅支持 macOS,检测到: $os_name"
|
|
fi
|
|
}
|
|
|
|
ensure_dir() {
|
|
mkdir -p "$1"
|
|
}
|
|
|
|
detect_codex_install() {
|
|
if command -v codex >/dev/null 2>&1; then
|
|
command -v codex
|
|
else
|
|
printf 'not-found'
|
|
fi
|
|
}
|
|
|
|
detect_claude_install() {
|
|
if command -v claude >/dev/null 2>&1; then
|
|
command -v claude
|
|
else
|
|
printf 'not-found'
|
|
fi
|
|
}
|
|
|
|
have_equivalent_gstack() {
|
|
local target="$1"
|
|
[[ -f "$target/connect-chrome/SKILL.md" ]] \
|
|
&& [[ -f "$target/setup-browser-cookies/SKILL.md" ]] \
|
|
&& [[ -f "$target/browse/src/cli.ts" || -x "$target/browse/dist/browse" ]]
|
|
}
|
|
|
|
clone_seed() {
|
|
local seed_dir="$TMP_DIR/gstack-seed"
|
|
if [[ -d "$seed_dir" ]]; then
|
|
return
|
|
fi
|
|
log "cloning gstack from $GSTACK_REPO_URL"
|
|
git clone --depth 1 "$GSTACK_REPO_URL" "$seed_dir" >/dev/null 2>&1
|
|
}
|
|
|
|
backup_path() {
|
|
local path="$1"
|
|
if [[ -e "$path" ]]; then
|
|
mv "$path" "$path.local.bak.$(date +%Y%m%d%H%M%S)"
|
|
fi
|
|
}
|
|
|
|
ensure_gstack_home() {
|
|
local home_root="$1"
|
|
local target="$home_root/skills/gstack"
|
|
|
|
ensure_dir "$home_root/skills"
|
|
|
|
if have_equivalent_gstack "$target"; then
|
|
printf '%s' "$target"
|
|
return
|
|
fi
|
|
|
|
if [[ -e "$target" ]]; then
|
|
backup_path "$target"
|
|
fi
|
|
|
|
clone_seed
|
|
cp -R "$TMP_DIR/gstack-seed" "$target"
|
|
printf '%s' "$target"
|
|
}
|
|
|
|
ensure_bun() {
|
|
if command -v bun >/dev/null 2>&1; then
|
|
return
|
|
fi
|
|
|
|
if command -v brew >/dev/null 2>&1; then
|
|
log "installing bun with Homebrew"
|
|
brew install bun >/dev/null
|
|
else
|
|
log "installing bun from bun.sh"
|
|
curl -fsSL https://bun.sh/install | bash >/dev/null
|
|
export BUN_INSTALL="${BUN_INSTALL:-$HOME/.bun}"
|
|
export PATH="$BUN_INSTALL/bin:$PATH"
|
|
fi
|
|
|
|
command -v bun >/dev/null 2>&1 || fail "bun 安装失败"
|
|
}
|
|
|
|
ensure_browse_runtime() {
|
|
local gstack_root="$1"
|
|
local browse_bin="$gstack_root/browse/dist/browse"
|
|
|
|
if [[ ! -x "$browse_bin" ]]; then
|
|
ensure_bun
|
|
log "building browse binary in $gstack_root"
|
|
(
|
|
cd "$gstack_root"
|
|
bun install >/dev/null
|
|
bun run build >/dev/null
|
|
)
|
|
fi
|
|
|
|
[[ -x "$browse_bin" ]] || fail "browse binary 不可用: $browse_bin"
|
|
|
|
ensure_bun
|
|
log "ensuring Playwright Chromium runtime"
|
|
(
|
|
cd "$gstack_root"
|
|
bunx playwright install chromium >/dev/null 2>&1 || true
|
|
)
|
|
|
|
printf '%s' "$browse_bin"
|
|
}
|
|
|
|
ensure_skill_entry() {
|
|
local home_root="$1"
|
|
local skill_name="$2"
|
|
local dest="$home_root/skills/$skill_name"
|
|
|
|
if [[ -L "$dest" ]]; then
|
|
printf '%s' "$dest"
|
|
return
|
|
fi
|
|
|
|
if [[ -f "$dest/SKILL.md" ]] && grep -q "^name: $skill_name\$" "$dest/SKILL.md"; then
|
|
printf '%s' "$dest"
|
|
return
|
|
fi
|
|
|
|
if [[ -e "$dest" ]]; then
|
|
backup_path "$dest"
|
|
fi
|
|
|
|
ln -sfn "gstack/$skill_name" "$dest"
|
|
printf '%s' "$dest"
|
|
}
|
|
|
|
detect_browser_apps() {
|
|
local apps=()
|
|
[[ -d "/Applications/Google Chrome.app" ]] && apps+=("Google Chrome")
|
|
[[ -d "/Applications/Chromium.app" ]] && apps+=("Chromium")
|
|
[[ -d "/Applications/Microsoft Edge.app" ]] && apps+=("Microsoft Edge")
|
|
[[ -d "/Applications/Brave Browser.app" ]] && apps+=("Brave Browser")
|
|
[[ -d "/Applications/Arc.app" ]] && apps+=("Arc")
|
|
|
|
if [[ ${#apps[@]} -eq 0 ]]; then
|
|
printf 'none-detected'
|
|
else
|
|
local joined=""
|
|
local app
|
|
for app in "${apps[@]}"; do
|
|
if [[ -n "$joined" ]]; then
|
|
joined+=", "
|
|
fi
|
|
joined+="$app"
|
|
done
|
|
printf '%s' "$joined"
|
|
fi
|
|
}
|
|
|
|
visible_browser_processes() {
|
|
/usr/bin/osascript -e 'tell application "System Events" to get the name of every process whose visible is true' 2>/dev/null \
|
|
| tr ',' '\n' \
|
|
| sed 's/^ *//' \
|
|
| grep -E 'Chrome|Chromium' \
|
|
| paste -sd ', ' - \
|
|
|| true
|
|
}
|
|
|
|
verify_browser_control() {
|
|
local browse_bin="$1"
|
|
local verify_tmp="$TMP_DIR/verify"
|
|
local connect_out
|
|
local newtab_out
|
|
local title_out
|
|
local heading_out
|
|
local network_out
|
|
local console_out
|
|
local status_out
|
|
local cookie_out=""
|
|
local cookie_rc=0
|
|
local visible_out=""
|
|
local cookie_status=""
|
|
|
|
mkdir -p "$verify_tmp"
|
|
"$browse_bin" stop >/dev/null 2>&1 || true
|
|
|
|
"$browse_bin" connect >"$verify_tmp/connect.log" 2>&1
|
|
sleep 2
|
|
"$browse_bin" newtab "$VERIFY_URL" >"$verify_tmp/newtab.log" 2>&1
|
|
"$browse_bin" js 'document.title' >"$verify_tmp/title.log" 2>&1
|
|
"$browse_bin" text h1 >"$verify_tmp/heading.log" 2>&1
|
|
"$browse_bin" reload >/dev/null 2>&1 || true
|
|
"$browse_bin" wait --load >/dev/null 2>&1 || true
|
|
"$browse_bin" network >"$verify_tmp/network.log" 2>&1
|
|
"$browse_bin" console >"$verify_tmp/console.log" 2>&1
|
|
"$browse_bin" status >"$verify_tmp/status.log" 2>&1
|
|
visible_out="$(visible_browser_processes)"
|
|
|
|
"$browse_bin" cookie-import-browser chrome --domain example.com >"$verify_tmp/cookie.log" 2>&1 || cookie_rc=$?
|
|
|
|
connect_out="$(cat "$verify_tmp/connect.log")"
|
|
newtab_out="$(cat "$verify_tmp/newtab.log")"
|
|
title_out="$(cat "$verify_tmp/title.log")"
|
|
heading_out="$(cat "$verify_tmp/heading.log")"
|
|
network_out="$(cat "$verify_tmp/network.log")"
|
|
console_out="$(cat "$verify_tmp/console.log")"
|
|
status_out="$(cat "$verify_tmp/status.log")"
|
|
cookie_out="$(cat "$verify_tmp/cookie.log" 2>/dev/null || true)"
|
|
|
|
if [[ $cookie_rc -eq 0 ]]; then
|
|
cookie_status="callable"
|
|
elif [[ "$cookie_out" == *"Keychain"* ]]; then
|
|
cookie_status="callable-requires-keychain-access"
|
|
elif [[ "$cookie_out" == *"No Chromium browsers found"* ]]; then
|
|
cookie_status="callable-no-cookie-source-browser"
|
|
else
|
|
cookie_status="error"
|
|
fi
|
|
|
|
BROWSE_CONNECT_OUT="$connect_out"
|
|
BROWSE_NEWTAB_OUT="$newtab_out"
|
|
BROWSE_TITLE_OUT="$title_out"
|
|
BROWSE_HEADING_OUT="$heading_out"
|
|
BROWSE_NETWORK_OUT="$network_out"
|
|
BROWSE_CONSOLE_OUT="$console_out"
|
|
BROWSE_STATUS_OUT="$status_out"
|
|
BROWSE_VISIBLE_OUT="$visible_out"
|
|
COOKIE_VERIFY_OUT="$cookie_out"
|
|
COOKIE_VERIFY_STATUS="$cookie_status"
|
|
}
|
|
|
|
write_setup_doc() {
|
|
local codex_connect="$1"
|
|
local codex_browse="$2"
|
|
local codex_cookie="$3"
|
|
local claude_connect="$4"
|
|
local claude_browse="$5"
|
|
local claude_cookie="$6"
|
|
local codex_gstack="$7"
|
|
local claude_gstack="$8"
|
|
local browse_bin="$9"
|
|
local codex_install="${10}"
|
|
local claude_install="${11}"
|
|
local browser_apps="${12}"
|
|
|
|
ensure_dir "$CODEX_HOME"
|
|
|
|
cat >"$SETUP_DOC" <<EOF
|
|
# Browser Control Setup
|
|
|
|
## 实际安装来源
|
|
|
|
- 官方上游:$GSTACK_REPO_URL
|
|
- Codex 当前 gstack 根目录:$codex_gstack
|
|
- Claude 当前 gstack 根目录:$claude_gstack
|
|
- Codex 安装入口:$codex_install
|
|
- Claude 安装入口:$claude_install
|
|
- 本机已检测到的系统浏览器:$browser_apps
|
|
- 运行时浏览器:Playwright Chromium(由 $browse_bin 驱动)
|
|
|
|
## 最终技能路径
|
|
|
|
### Codex
|
|
|
|
- connect-chrome: $codex_connect
|
|
- browse: $codex_browse
|
|
- setup-browser-cookies: $codex_cookie
|
|
|
|
### Claude
|
|
|
|
- connect-chrome: $claude_connect
|
|
- browse: $claude_browse
|
|
- setup-browser-cookies: $claude_cookie
|
|
|
|
## 如何调用
|
|
|
|
### Codex
|
|
|
|
- \`connect-chrome\`
|
|
- \`browse\`
|
|
- \`setup-browser-cookies\`
|
|
- \`install-browser-control\`
|
|
|
|
### Claude
|
|
|
|
- \`/connect-chrome\`
|
|
- \`/browse\`
|
|
- \`/setup-browser-cookies\`
|
|
- \`/install-browser-control\`
|
|
|
|
## 如何验证
|
|
|
|
1. 运行 \`connect-chrome\`,确认出现 headed Chromium 窗口。
|
|
2. 运行 \`browse newtab https://example.com\`。
|
|
3. 运行 \`browse js 'document.title'\` 与 \`browse text h1\`,确认可读取 DOM。
|
|
4. 运行 \`browse network\` 与 \`browse console\`,确认能读取请求和控制台信息。
|
|
5. 运行 \`browse cookie-import-browser chrome --domain example.com\`,确认 cookie 导入入口可被调用;首次读取 Chrome Cookie 时,macOS 可能弹出 Keychain 授权框。
|
|
|
|
## 本机最近一次真实验证
|
|
|
|
### connect
|
|
|
|
\`\`\`
|
|
$BROWSE_CONNECT_OUT
|
|
\`\`\`
|
|
|
|
### newtab + DOM
|
|
|
|
\`\`\`
|
|
$BROWSE_NEWTAB_OUT
|
|
$BROWSE_TITLE_OUT
|
|
$BROWSE_HEADING_OUT
|
|
\`\`\`
|
|
|
|
### network
|
|
|
|
\`\`\`
|
|
$BROWSE_NETWORK_OUT
|
|
\`\`\`
|
|
|
|
### console
|
|
|
|
\`\`\`
|
|
$BROWSE_CONSOLE_OUT
|
|
\`\`\`
|
|
|
|
### status
|
|
|
|
\`\`\`
|
|
$BROWSE_STATUS_OUT
|
|
\`\`\`
|
|
|
|
### visible browser processes
|
|
|
|
\`\`\`
|
|
${BROWSE_VISIBLE_OUT:-not-detected}
|
|
\`\`\`
|
|
|
|
### cookie import verification
|
|
|
|
- status: $COOKIE_VERIFY_STATUS
|
|
|
|
\`\`\`
|
|
$COOKIE_VERIFY_OUT
|
|
\`\`\`
|
|
EOF
|
|
}
|
|
|
|
main() {
|
|
local codex_gstack
|
|
local claude_gstack
|
|
local browse_bin
|
|
local claude_browse_bin
|
|
local codex_connect
|
|
local codex_browse
|
|
local codex_cookie
|
|
local claude_connect
|
|
local claude_browse
|
|
local claude_cookie
|
|
local browser_apps
|
|
local codex_install
|
|
local claude_install
|
|
|
|
ensure_macos
|
|
|
|
codex_install="$(detect_codex_install)"
|
|
claude_install="$(detect_claude_install)"
|
|
|
|
codex_gstack="$(ensure_gstack_home "$CODEX_HOME")"
|
|
claude_gstack="$(ensure_gstack_home "$CLAUDE_HOME")"
|
|
browse_bin="$(ensure_browse_runtime "$codex_gstack")"
|
|
claude_browse_bin="$(ensure_browse_runtime "$claude_gstack")"
|
|
|
|
[[ -x "$claude_browse_bin" ]] || fail "Claude browse binary 不可用: $claude_browse_bin"
|
|
|
|
codex_connect="$(ensure_skill_entry "$CODEX_HOME" "connect-chrome")"
|
|
codex_browse="$(ensure_skill_entry "$CODEX_HOME" "browse")"
|
|
codex_cookie="$(ensure_skill_entry "$CODEX_HOME" "setup-browser-cookies")"
|
|
|
|
claude_connect="$(ensure_skill_entry "$CLAUDE_HOME" "connect-chrome")"
|
|
claude_browse="$(ensure_skill_entry "$CLAUDE_HOME" "browse")"
|
|
claude_cookie="$(ensure_skill_entry "$CLAUDE_HOME" "setup-browser-cookies")"
|
|
|
|
browser_apps="$(detect_browser_apps)"
|
|
|
|
verify_browser_control "$browse_bin"
|
|
|
|
write_setup_doc \
|
|
"$codex_connect" "$codex_browse" "$codex_cookie" \
|
|
"$claude_connect" "$claude_browse" "$claude_cookie" \
|
|
"$codex_gstack" "$claude_gstack" "$browse_bin" \
|
|
"$codex_install" "$claude_install" "$browser_apps"
|
|
|
|
printf 'DONE\n'
|
|
printf 'CODEX_HOME=%s\n' "$CODEX_HOME"
|
|
printf 'CLAUDE_HOME=%s\n' "$CLAUDE_HOME"
|
|
printf 'SETUP_DOC=%s\n' "$SETUP_DOC"
|
|
printf 'CODEX_CONNECT=%s\n' "$codex_connect"
|
|
printf 'CODEX_BROWSE=%s\n' "$codex_browse"
|
|
printf 'CODEX_COOKIE=%s\n' "$codex_cookie"
|
|
printf 'CLAUDE_CONNECT=%s\n' "$claude_connect"
|
|
printf 'CLAUDE_BROWSE=%s\n' "$claude_browse"
|
|
printf 'CLAUDE_COOKIE=%s\n' "$claude_cookie"
|
|
printf 'COOKIE_VERIFY_STATUS=%s\n' "$COOKIE_VERIFY_STATUS"
|
|
}
|
|
|
|
main "$@"
|