434 lines
10 KiB
Bash
Executable File
Raw Permalink 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.

#!/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 "$@"