mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-15 12:53:31 +08:00
The desktop app already had solid session streaming, permission, and tool-rendering flows, but extension setup still forced users into manual forms or external shell work. This change adds an Install Center in Settings that reuses the session chat pipeline for natural-language installs, adds installer-specific guidance for plugin and skill URLs, and includes an agent-browser E2E script for real UI validation. Constraint: Must reuse the existing session/chat execution path instead of introducing a second install runtime Constraint: Plugin installs need real CLI commands while skill installs may come from published install commands on third-party pages Rejected: Separate terminal-only install surface | duplicates session UX and weakens permission/tool visibility Rejected: Pure form-based installer expansion | too much friction for plugin, MCP, and skill onboarding Confidence: medium Scope-risk: moderate Reversibility: clean Directive: Keep installer prompts aligned with the actual CLI install surfaces; do not let the installer fall back to slash-command syntax inside Bash Tested: cd desktop && bun run lint Tested: Real UI automation via agent-browser for Telegram plugin install flow through Settings > Install, verified Plugins page shows telegram enabled Tested: Real UI automation via agent-browser for ui-ux-pro-max skill install flow through Settings > Install, verified ~/.claude/skills/ui-ux-pro-max and Skills page visibility Not-tested: Full e2e-install-center-agent-browser.sh script as a single uninterrupted green run after the latest stability tweaks
169 lines
5.2 KiB
Bash
Executable File
169 lines
5.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||
DESKTOP_DIR="$ROOT_DIR/desktop"
|
||
|
||
if ! command -v agent-browser >/dev/null 2>&1; then
|
||
echo "agent-browser is required but was not found in PATH" >&2
|
||
exit 1
|
||
fi
|
||
|
||
PORTS="$(bun -e 'import { createServer } from "node:net"; const getPort = () => new Promise((resolve) => { const server = createServer(); server.listen(0, "127.0.0.1", () => { const address = server.address(); const port = typeof address === "object" && address ? address.port : 0; server.close(() => resolve(port)); }); }); const apiPort = await getPort(); const webPort = await getPort(); console.log(`${apiPort} ${webPort}`);')"
|
||
API_PORT="${PORTS%% *}"
|
||
WEB_PORT="${PORTS##* }"
|
||
BASE_URL="http://127.0.0.1:${API_PORT}"
|
||
WEB_URL="http://127.0.0.1:${WEB_PORT}/?serverUrl=${BASE_URL}"
|
||
|
||
RUN_ID="$(date +%s)-$RANDOM"
|
||
SESSION_NAME="cc-haha-install-e2e-${RUN_ID}"
|
||
ARTIFACT_DIR="$(mktemp -d "/tmp/cc-haha-install-e2e-${RUN_ID}-XXXX")"
|
||
SERVER_LOG="${ARTIFACT_DIR}/server.log"
|
||
WEB_LOG="${ARTIFACT_DIR}/web.log"
|
||
|
||
if [[ "${CC_HAHA_E2E_USE_REAL_CONFIG:-1}" == "1" ]]; then
|
||
CONFIG_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
||
else
|
||
CONFIG_DIR="${CLAUDE_CONFIG_DIR:-${ARTIFACT_DIR}/claude-config}"
|
||
mkdir -p "${CONFIG_DIR}"
|
||
fi
|
||
|
||
cleanup() {
|
||
local exit_code=$?
|
||
if [[ -n "${SERVER_PID:-}" ]]; then
|
||
kill "${SERVER_PID}" >/dev/null 2>&1 || true
|
||
wait "${SERVER_PID}" >/dev/null 2>&1 || true
|
||
fi
|
||
if [[ -n "${WEB_PID:-}" ]]; then
|
||
kill "${WEB_PID}" >/dev/null 2>&1 || true
|
||
wait "${WEB_PID}" >/dev/null 2>&1 || true
|
||
fi
|
||
agent-browser --session "${SESSION_NAME}" close >/dev/null 2>&1 || true
|
||
if [[ $exit_code -ne 0 ]]; then
|
||
echo "Artifacts kept at: ${ARTIFACT_DIR}" >&2
|
||
else
|
||
rm -rf "${ARTIFACT_DIR}"
|
||
fi
|
||
}
|
||
trap cleanup EXIT
|
||
|
||
echo "Starting isolated backend on ${BASE_URL}"
|
||
(
|
||
cd "${ROOT_DIR}"
|
||
CLAUDE_CONFIG_DIR="${CONFIG_DIR}" SERVER_PORT="${API_PORT}" bun run src/server/index.ts --host 127.0.0.1 --port "${API_PORT}"
|
||
) >"${SERVER_LOG}" 2>&1 &
|
||
SERVER_PID=$!
|
||
|
||
echo "Starting web UI on http://127.0.0.1:${WEB_PORT}"
|
||
(
|
||
cd "${DESKTOP_DIR}"
|
||
bun run dev -- --host 127.0.0.1 --port "${WEB_PORT}"
|
||
) >"${WEB_LOG}" 2>&1 &
|
||
WEB_PID=$!
|
||
|
||
wait_for_http() {
|
||
local url="$1"
|
||
for _ in $(seq 1 120); do
|
||
if curl -fsS "$url" >/dev/null 2>&1; then
|
||
return 0
|
||
fi
|
||
sleep 1
|
||
done
|
||
echo "Timed out waiting for ${url}" >&2
|
||
return 1
|
||
}
|
||
|
||
browser_body() {
|
||
agent-browser --session "${SESSION_NAME}" get text body
|
||
}
|
||
|
||
wait_for_body_contains() {
|
||
local needle="$1"
|
||
local attempts="${2:-180}"
|
||
for _ in $(seq 1 "${attempts}"); do
|
||
if browser_body | grep -Fq "${needle}"; then
|
||
return 0
|
||
fi
|
||
sleep 2
|
||
done
|
||
echo "Timed out waiting for page text: ${needle}" >&2
|
||
return 1
|
||
}
|
||
|
||
wait_for_body_contains_any() {
|
||
local attempts="$1"
|
||
shift
|
||
|
||
for _ in $(seq 1 "${attempts}"); do
|
||
local body
|
||
body="$(browser_body)"
|
||
for needle in "$@"; do
|
||
if grep -Fq "${needle}" <<<"${body}"; then
|
||
return 0
|
||
fi
|
||
done
|
||
sleep 2
|
||
done
|
||
|
||
echo "Timed out waiting for any expected page text: $*" >&2
|
||
return 1
|
||
}
|
||
|
||
wait_for_path() {
|
||
local path="$1"
|
||
local attempts="${2:-180}"
|
||
for _ in $(seq 1 "${attempts}"); do
|
||
if [[ -e "${path}" ]]; then
|
||
return 0
|
||
fi
|
||
sleep 2
|
||
done
|
||
echo "Timed out waiting for path: ${path}" >&2
|
||
return 1
|
||
}
|
||
|
||
AB="agent-browser --session ${SESSION_NAME}"
|
||
TELEGRAM_PROMPT="请根据 https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/telegram 安装 Telegram 官方插件。如果 URL 对应官方 external_plugins,请直接识别 plugin id 并完成用户级安装,然后告诉我结果。"
|
||
SKILL_PROMPT="请根据 https://www.aitmpl.com/component/skill/creative-design/ui-ux-pro-max 这个 skill 页面安装 ui-ux-pro-max。如果页面里有官方安装命令,请直接识别并执行,然后告诉我结果。"
|
||
PLUGIN_DIR="${CONFIG_DIR}/plugins/cache/claude-plugins-official/telegram"
|
||
SKILL_DIR="${CONFIG_DIR}/skills/ui-ux-pro-max"
|
||
|
||
wait_for_http "${BASE_URL}/health"
|
||
wait_for_http "http://127.0.0.1:${WEB_PORT}"
|
||
|
||
${AB} open "${WEB_URL}"
|
||
${AB} click 'button:has-text("Settings")'
|
||
${AB} click 'button:has-text("Install")'
|
||
${AB} screenshot "${ARTIFACT_DIR}/01-install-center.png" >/dev/null
|
||
|
||
run_install_prompt() {
|
||
local prompt="$1"
|
||
${AB} click 'button:has-text("New install chat")'
|
||
${AB} fill 'textarea' "${prompt}"
|
||
${AB} click 'button:has-text("Send request")'
|
||
}
|
||
|
||
echo "Running plugin install flow"
|
||
run_install_prompt "${TELEGRAM_PROMPT}"
|
||
wait_for_body_contains 'telegram@claude-plugins-official'
|
||
wait_for_path "${PLUGIN_DIR}"
|
||
${AB} click 'button:has-text("Open Plugins")'
|
||
wait_for_body_contains 'Enabled1'
|
||
wait_for_body_contains 'telegram Enabled'
|
||
${AB} screenshot "${ARTIFACT_DIR}/02-plugin-installed.png" >/dev/null
|
||
|
||
echo "Running skill install flow"
|
||
${AB} click 'button:has-text("Install")'
|
||
run_install_prompt "${SKILL_PROMPT}"
|
||
wait_for_body_contains 'claude-code-templates@latest'
|
||
wait_for_path "${SKILL_DIR}"
|
||
${AB} click 'button:has-text("Open Skills")'
|
||
wait_for_body_contains 'Installed Skills'
|
||
wait_for_body_contains 'ui-ux-pro-max'
|
||
${AB} screenshot "${ARTIFACT_DIR}/03-skill-installed.png" >/dev/null
|
||
|
||
echo "Install Center E2E passed"
|
||
echo "Config dir: ${CONFIG_DIR}"
|
||
echo "API port: ${API_PORT}"
|
||
echo "Web port: ${WEB_PORT}"
|