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 "$@"
|