feat: ship native boss android console

This commit is contained in:
kris
2026-03-26 23:16:56 +08:00
parent 90e904814d
commit 90cb6b7ff1
261 changed files with 40051 additions and 135 deletions

49
scripts/bootstrap-server.sh Executable file
View File

@@ -0,0 +1,49 @@
#!/bin/bash
set -euo pipefail
if [[ "${EUID}" -ne 0 ]]; then
echo "Please run with sudo."
exit 1
fi
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y ca-certificates curl gnupg rsync
if ! command -v node >/dev/null 2>&1; then
mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
| gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" \
>/etc/apt/sources.list.d/nodesource.list
apt-get update
apt-get install -y nodejs
fi
if ! command -v caddy >/dev/null 2>&1; then
apt-get install -y debian-keyring debian-archive-keyring apt-transport-https
curl -fsSL https://dl.cloudsmith.io/public/caddy/stable/gpg.key \
| gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -fsSL https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt \
>/etc/apt/sources.list.d/caddy-stable.list
apt-get update
apt-get install -y caddy
fi
mkdir -p /opt/boss
chown -R ubuntu:ubuntu /opt/boss
if [[ -f /opt/boss/deployment/systemd/boss-web.service ]]; then
cp /opt/boss/deployment/systemd/boss-web.service /etc/systemd/system/boss-web.service
fi
if [[ -f /opt/boss/deployment/Caddyfile ]]; then
cp /opt/boss/deployment/Caddyfile /etc/caddy/Caddyfile
fi
systemctl daemon-reload
systemctl enable boss-web || true
systemctl enable caddy || true
echo "bootstrap-server.sh completed"

29
scripts/build-release-apk.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/bin/zsh
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
ANDROID_DIR="$ROOT_DIR/android"
RELEASE_APK="$ANDROID_DIR/app/build/outputs/apk/release/app-release.apk"
BUILD_GRADLE="$ROOT_DIR/android/app/build.gradle"
VERSION_NAME="$(sed -n 's/.*versionName \"\([^\"]*\)\"/\1/p' "$BUILD_GRADLE" | head -n 1)"
VERSIONED_RELEASE_APK="$ANDROID_DIR/app/build/outputs/apk/release/boss-android-v${VERSION_NAME}-release.apk"
zsh "$ROOT_DIR/scripts/prepare-android-signing.sh"
cd "$ANDROID_DIR"
./gradlew assembleRelease
cd "$ROOT_DIR"
cp "$RELEASE_APK" "$VERSIONED_RELEASE_APK"
zsh "$ROOT_DIR/scripts/publish-apk-to-public.sh" "$RELEASE_APK"
APKSIGNER="$(find "${ANDROID_HOME:-$HOME/Library/Android/sdk}/build-tools" -name apksigner -type f 2>/dev/null | sort | tail -n 1)"
if [[ -n "${APKSIGNER:-}" && -f "$APKSIGNER" ]]; then
"$APKSIGNER" verify --print-certs "$RELEASE_APK"
else
echo "apksigner not found, skipped signature verification output" >&2
fi
echo "Signed release APK ready: $RELEASE_APK"
echo "Versioned release APK ready: $VERSIONED_RELEASE_APK"

53
scripts/deploy-server.sh Executable file
View File

@@ -0,0 +1,53 @@
#!/bin/zsh
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
REMOTE_USER="${BOSS_SERVER_USER:-ubuntu}"
REMOTE_HOSTNAME="${BOSS_SERVER_HOST:-106.53.170.158}"
REMOTE_HOST="${BOSS_REMOTE_HOST:-${REMOTE_USER}@${REMOTE_HOSTNAME}}"
REMOTE_DIR="${BOSS_REMOTE_DIR:-/opt/boss}"
SSH_OPTS="-o StrictHostKeyChecking=no"
KEYCHAIN_SERVICE="${BOSS_KEYCHAIN_SERVICE:-boss-server-debug-ssh}"
resolve_password() {
if [[ -n "${BOSS_SERVER_PASS:-}" ]]; then
printf '%s' "${BOSS_SERVER_PASS}"
return 0
fi
if command -v security >/dev/null 2>&1; then
local keychain_pass=""
keychain_pass="$(security find-generic-password -a "${REMOTE_USER}" -s "${KEYCHAIN_SERVICE}" -w 2>/dev/null || true)"
if [[ -n "${keychain_pass}" ]]; then
printf '%s' "${keychain_pass}"
return 0
fi
fi
return 1
}
PASSWORD="$(resolve_password || true)"
if [[ -n "${PASSWORD}" ]]; then
export SSHPASS="${PASSWORD}"
RSYNC_RSH="sshpass -e ssh ${SSH_OPTS}"
SSH_PREFIX=(sshpass -e ssh ${=SSH_OPTS})
else
RSYNC_RSH="ssh ${SSH_OPTS}"
SSH_PREFIX=(ssh ${=SSH_OPTS})
fi
BOSS_RUNTIME_ROOT="$ROOT_DIR" BOSS_STATE_FILE="$ROOT_DIR/data/boss-state.json" npm run build
"${SSH_PREFIX[@]}" "$REMOTE_HOST" "sudo mkdir -p $REMOTE_DIR && sudo chown -R ${REMOTE_USER}:${REMOTE_USER} $REMOTE_DIR && sudo rm -rf $REMOTE_DIR/.next"
rsync -az --delete \
--exclude ".git" \
--exclude "node_modules" \
--exclude "data/" \
--exclude ".DS_Store" \
-e "$RSYNC_RSH" \
"$ROOT_DIR/" "$REMOTE_HOST:$REMOTE_DIR/"
"${SSH_PREFIX[@]}" "$REMOTE_HOST" "sudo bash $REMOTE_DIR/scripts/bootstrap-server.sh"
"${SSH_PREFIX[@]}" "$REMOTE_HOST" "sudo chown -R ${REMOTE_USER}:${REMOTE_USER} $REMOTE_DIR"
"${SSH_PREFIX[@]}" "$REMOTE_HOST" "cd $REMOTE_DIR && npm install --omit=dev && sudo systemctl restart boss-web && sudo systemctl restart caddy && sleep 2 && curl -fsS http://127.0.0.1:3000/api/health"

View File

@@ -0,0 +1,30 @@
#!/bin/zsh
set -euo pipefail
PLIST_SOURCE="/Users/kris/code/boss/deployment/launchd/com.hyzq.boss.local-agent.plist"
PLIST_TARGET="$HOME/Library/LaunchAgents/com.hyzq.boss.local-agent.plist"
CONFIG_PATH="${1:-/Users/kris/code/boss/local-agent/config.cloud.json}"
if [[ "$CONFIG_PATH" != /* ]]; then
CONFIG_PATH="/Users/kris/code/boss/${CONFIG_PATH}"
fi
if [[ ! -f "$CONFIG_PATH" ]]; then
echo "Config file not found: $CONFIG_PATH" >&2
exit 1
fi
mkdir -p "$HOME/Library/LaunchAgents"
cp "$PLIST_SOURCE" "$PLIST_TARGET"
python3 - <<'PY' "$PLIST_TARGET" "$CONFIG_PATH"
from pathlib import Path
import sys
plist_path = Path(sys.argv[1])
config_path = sys.argv[2]
text = plist_path.read_text()
plist_path.write_text(text.replace("__BOSS_AGENT_CONFIG__", config_path))
PY
launchctl unload "$PLIST_TARGET" >/dev/null 2>&1 || true
launchctl load "$PLIST_TARGET"
echo "Loaded $PLIST_TARGET with $CONFIG_PATH"

14
scripts/install-server-mail.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/zsh
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
SERVER_WRAPPER="${BOSS_SERVER_WRAPPER:-$HOME/.codex/skills/boss-server-debug/scripts/server_ssh.sh}"
REMOTE_DIR="${BOSS_REMOTE_DIR:-/opt/boss}"
if [[ ! -x "${SERVER_WRAPPER}" ]]; then
echo "Boss server wrapper not found: ${SERVER_WRAPPER}" >&2
exit 1
fi
"${ROOT_DIR}/scripts/deploy-server.sh"
"${SERVER_WRAPPER}" exec "sudo bash ${REMOTE_DIR}/deployment/mail/install-postfix-dovecot.sh"

View File

@@ -0,0 +1,56 @@
#!/bin/zsh
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
ANDROID_DIR="$ROOT_DIR/android"
SIGNING_DIR="$ANDROID_DIR/signing"
KEYSTORE_DIR="$ANDROID_DIR/keystores"
PROPS_FILE="$SIGNING_DIR/release-signing.properties"
KEYSTORE_FILE="$KEYSTORE_DIR/boss-release.keystore"
KEY_ALIAS="${BOSS_ANDROID_KEY_ALIAS:-bossrelease}"
KEY_DNAME="${BOSS_ANDROID_KEY_DNAME:-CN=Boss Release, OU=Boss, O=HYZQ, L=Shenzhen, ST=Guangdong, C=CN}"
VALIDITY_DAYS="${BOSS_ANDROID_KEY_VALIDITY_DAYS:-3650}"
mkdir -p "$SIGNING_DIR" "$KEYSTORE_DIR"
generate_secret() {
python3 - <<'PY'
import secrets
print(secrets.token_urlsafe(24))
PY
}
if [[ ! -f "$PROPS_FILE" ]]; then
STORE_PASSWORD="${BOSS_ANDROID_STORE_PASSWORD:-$(generate_secret)}"
KEY_PASSWORD="${BOSS_ANDROID_KEY_PASSWORD:-$STORE_PASSWORD}"
cat > "$PROPS_FILE" <<EOF
storeFile=../keystores/boss-release.keystore
storePassword=$STORE_PASSWORD
keyAlias=$KEY_ALIAS
keyPassword=$KEY_PASSWORD
EOF
chmod 600 "$PROPS_FILE"
else
STORE_PASSWORD="$(sed -n 's/^storePassword=//p' "$PROPS_FILE" | head -n 1)"
KEY_PASSWORD="$(sed -n 's/^keyPassword=//p' "$PROPS_FILE" | head -n 1)"
KEY_ALIAS="$(sed -n 's/^keyAlias=//p' "$PROPS_FILE" | head -n 1)"
fi
if [[ ! -f "$KEYSTORE_FILE" ]]; then
keytool -genkeypair \
-storetype PKCS12 \
-keystore "$KEYSTORE_FILE" \
-storepass "$STORE_PASSWORD" \
-keypass "$KEY_PASSWORD" \
-alias "$KEY_ALIAS" \
-keyalg RSA \
-keysize 4096 \
-validity "$VALIDITY_DAYS" \
-dname "$KEY_DNAME"
chmod 600 "$KEYSTORE_FILE"
fi
echo "Prepared Android release signing:"
echo " properties: $PROPS_FILE"
echo " keystore: $KEYSTORE_FILE"
echo " alias: $KEY_ALIAS"

View File

@@ -0,0 +1,58 @@
#!/bin/zsh
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
SOURCE_APK="${1:-$ROOT_DIR/android/app/build/outputs/apk/debug/app-debug.apk}"
TARGET_DIR="$ROOT_DIR/public/downloads"
TARGET_APK="$TARGET_DIR/boss-android-latest.apk"
TARGET_META="$TARGET_DIR/boss-android-latest.json"
STANDALONE_DIR="$ROOT_DIR/.next/standalone/public/downloads"
BUILD_GRADLE="$ROOT_DIR/android/app/build.gradle"
SOURCE_NAME="$(basename "$SOURCE_APK")"
BUILD_FLAVOR="debug"
if [[ "$SOURCE_NAME" == *release* ]]; then
BUILD_FLAVOR="release"
fi
if [[ ! -f "$SOURCE_APK" ]]; then
echo "APK not found: $SOURCE_APK" >&2
exit 1
fi
mkdir -p "$TARGET_DIR"
cp "$SOURCE_APK" "$TARGET_APK"
SIZE_BYTES="$(stat -f '%z' "$TARGET_APK" 2>/dev/null || stat -c '%s' "$TARGET_APK")"
UPDATED_AT="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
SHA256="$(shasum -a 256 "$TARGET_APK" | awk '{print $1}')"
VERSION_NAME="$(sed -n 's/.*versionName \"\([^\"]*\)\"/\1/p' "$BUILD_GRADLE" | head -n 1)"
VERSION_CODE="$(sed -n 's/.*versionCode \([0-9][0-9]*\).*/\1/p' "$BUILD_GRADLE" | head -n 1)"
VERSIONED_APK_NAME="boss-android-v${VERSION_NAME}-${BUILD_FLAVOR}.apk"
VERSIONED_TARGET_APK="$TARGET_DIR/$VERSIONED_APK_NAME"
cp "$TARGET_APK" "$VERSIONED_TARGET_APK"
cat > "$TARGET_META" <<EOF
{
"fileName": "$VERSIONED_APK_NAME",
"urlPath": "/api/v1/user/ota/package",
"sizeBytes": $SIZE_BYTES,
"updatedAt": "$UPDATED_AT",
"sha256": "$SHA256",
"versionName": "$VERSION_NAME",
"versionCode": $VERSION_CODE,
"buildFlavor": "$BUILD_FLAVOR"
}
EOF
if [[ -d "$ROOT_DIR/.next/standalone/public" ]]; then
mkdir -p "$STANDALONE_DIR"
cp "$TARGET_APK" "$STANDALONE_DIR/boss-android-latest.apk"
cp "$VERSIONED_TARGET_APK" "$STANDALONE_DIR/$VERSIONED_APK_NAME"
cp "$TARGET_META" "$STANDALONE_DIR/boss-android-latest.json"
fi
echo "Published APK to $TARGET_APK"
echo "Published versioned APK to $VERSIONED_TARGET_APK"
echo "Metadata written to $TARGET_META"

8
scripts/run-web.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/zsh
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
PORT_VALUE="${PORT:-3000}"
cd "$ROOT_DIR"
exec node .next/standalone/server.js --hostname 0.0.0.0 --port "$PORT_VALUE"

8
scripts/start-local-agent.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/zsh
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
CONFIG_PATH="${1:-$ROOT_DIR/local-agent/config.example.json}"
cd "$ROOT_DIR"
exec node ./local-agent/server.mjs "$CONFIG_PATH"