Files
wechat-gggithub--wechat-cla…/scripts/daemon.sh
T
2026-07-13 12:33:50 +08:00

423 lines
11 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
# =============================================================================
# wechat-claude-code cross-platform daemon manager
# Supports: macOS (launchd) / Linux (systemd + nohup fallback)
# =============================================================================
DATA_DIR="${HOME}/.wechat-claude-code"
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
SERVICE_NAME="wechat-claude-code"
# Platform detection
OS_TYPE="$(uname -s)"
# =============================================================================
# macOS (launchd) functions
# =============================================================================
macos_plist_label() {
echo "com.wechat-claude-code.bridge"
}
macos_plist_path() {
echo "${HOME}/Library/LaunchAgents/$(macos_plist_label).plist"
}
macos_is_loaded() {
launchctl print "gui/$(id -u)/$(macos_plist_label)" &>/dev/null
}
macos_start() {
local plist_label="$(macos_plist_label)"
local plist_path="$(macos_plist_path)"
local node_bin="$(command -v node || echo '/usr/local/bin/node')"
if macos_is_loaded; then
echo "Already running (or plist loaded)"
exit 0
fi
mkdir -p "$DATA_DIR/logs"
# Collect Anthropic/Claude env vars for plist
local plist_extra_env=""
for var in ANTHROPIC_AUTH_TOKEN ANTHROPIC_API_KEY ANTHROPIC_BASE_URL CLAUDE_API_KEY; do
if [ -n "${!var:-}" ]; then
plist_extra_env="${plist_extra_env} <key>${var}</key>
<string>${!var}</string>
"
fi
done
cat > "$plist_path" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${plist_label}</string>
<key>ProgramArguments</key>
<array>
<string>${node_bin}</string>
<string>${PROJECT_DIR}/dist/main.js</string>
<string>start</string>
</array>
<key>WorkingDirectory</key>
<string>${PROJECT_DIR}</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>${DATA_DIR}/logs/stdout.log</string>
<key>StandardErrorPath</key>
<string>${DATA_DIR}/logs/stderr.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>${HOME}/.local/bin:${node_bin%/*}:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
${plist_extra_env} </dict>
</dict>
</plist>
PLIST
launchctl load "$plist_path"
echo "Started wechat-claude-code daemon (macOS launchd)"
}
macos_stop() {
local plist_label="$(macos_plist_label)"
local plist_path="$(macos_plist_path)"
launchctl bootout "gui/$(id -u)/${plist_label}" 2>/dev/null || true
rm -f "$plist_path"
echo "Stopped wechat-claude-code daemon (macOS launchd)"
}
macos_status() {
if macos_is_loaded; then
local pid=$(pgrep -f "dist/main.js start" 2>/dev/null | head -1)
if [ -n "$pid" ]; then
echo "Running (PID: $pid)"
else
echo "Loaded but not running"
fi
else
echo "Not running"
fi
}
macos_logs() {
local log_dir="${DATA_DIR}/logs"
if [ -d "$log_dir" ]; then
local latest=$(ls -t "${log_dir}"/bridge-*.log 2>/dev/null | head -1)
if [ -n "$latest" ]; then
tail -100 "$latest"
else
echo "No bridge logs found. Checking stdout/stderr:"
for f in "${log_dir}"/stdout.log "${log_dir}"/stderr.log; do
if [ -f "$f" ]; then
echo "=== $(basename "$f") ==="
tail -30 "$f"
fi
done
fi
else
echo "No logs found"
fi
}
# =============================================================================
# Linux (systemd) functions
# =============================================================================
linux_ensure_user_session() {
if [ -z "${XDG_RUNTIME_DIR:-}" ]; then
export XDG_RUNTIME_DIR="/run/user/$(id -u)"
mkdir -p "$XDG_RUNTIME_DIR" 2>/dev/null || true
fi
if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then
export DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus"
fi
}
linux_service_file() {
echo "${HOME}/.config/systemd/user/${SERVICE_NAME}.service"
}
linux_pid_file() {
echo "${DATA_DIR}/${SERVICE_NAME}.pid"
}
linux_node_bin() {
local node_bin="$(command -v node 2>/dev/null || echo '')"
if [ -z "$node_bin" ]; then
local nvm_default="${NVM_DIR:-${HOME}/.nvm}/versions/node"
if [ -d "$nvm_default" ]; then
node_bin="$(find "$nvm_default" -name "node" -type f 2>/dev/null | head -1)"
fi
fi
echo "${node_bin:-/usr/bin/node}"
}
linux_systemd_available() {
linux_ensure_user_session
systemctl --user list-units &>/dev/null
}
linux_create_service_file() {
local service_file="$(linux_service_file)"
local node_bin="$(linux_node_bin)"
mkdir -p "$(dirname "$service_file")"
# Collect Anthropic/Claude env vars to pass through to the service
local extra_env=""
for var in ANTHROPIC_AUTH_TOKEN ANTHROPIC_API_KEY ANTHROPIC_BASE_URL CLAUDE_API_KEY; do
if [ -n "${!var:-}" ]; then
extra_env="${extra_env}Environment=${var}=${!var}
"
fi
done
cat > "$service_file" <<SERVICE
[Unit]
Description=WeChat Claude Code Bridge
Documentation=https://github.com/Wechat-ggGitHub/wechat-claude-code
After=network.target
[Service]
Type=simple
ExecStart=${node_bin} ${PROJECT_DIR}/dist/main.js start
WorkingDirectory=${PROJECT_DIR}
Restart=always
RestartSec=10
Environment=PATH=${HOME}/.local/bin:${node_bin%/*}:/usr/local/bin:/usr/bin:/bin
${extra_env}StandardOutput=append:${DATA_DIR}/logs/stdout.log
StandardError=append:${DATA_DIR}/logs/stderr.log
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=default.target
SERVICE
chmod 644 "$service_file"
}
linux_reload_daemon() {
linux_ensure_user_session
systemctl --user daemon-reload 2>/dev/null || true
}
linux_direct_start() {
local pid_file="$(linux_pid_file)"
local node_bin="$(linux_node_bin)"
if [ -f "$pid_file" ]; then
local old_pid=$(cat "$pid_file" 2>/dev/null)
if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
echo "Already running (PID: $old_pid)"
exit 0
fi
rm -f "$pid_file"
fi
mkdir -p "$DATA_DIR/logs"
echo "Starting wechat-claude-code daemon (direct mode)..."
nohup "$node_bin" "${PROJECT_DIR}/dist/main.js" start \
>> "$DATA_DIR/logs/stdout.log" \
2>> "$DATA_DIR/logs/stderr.log" &
local pid=$!
echo "$pid" > "$pid_file"
echo "Started (PID: $pid)"
echo "Logs: $DATA_DIR/logs/stdout.log"
}
linux_direct_stop() {
local pid_file="$(linux_pid_file)"
if [ ! -f "$pid_file" ]; then
echo "Not running (no PID file)"
exit 0
fi
local pid=$(cat "$pid_file" 2>/dev/null)
if [ -z "$pid" ]; then
rm -f "$pid_file"
echo "Stopped"
exit 0
fi
if kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
local count=0
while kill -0 "$pid" 2>/dev/null && [ $count -lt 10 ]; do
sleep 1
count=$((count + 1))
done
kill -9 "$pid" 2>/dev/null || true
echo "Stopped (PID: $pid)"
else
echo "Process not running (cleaning up PID file)"
fi
rm -f "$pid_file"
}
linux_direct_status() {
local pid_file="$(linux_pid_file)"
if [ ! -f "$pid_file" ]; then
echo "Not running"
exit 0
fi
local pid=$(cat "$pid_file" 2>/dev/null)
if [ -z "$pid" ]; then
echo "Not running (invalid PID file)"
exit 0
fi
if kill -0 "$pid" 2>/dev/null; then
echo "Running (PID: $pid)"
else
echo "Not running (stale PID file)"
fi
}
linux_start() {
if linux_systemd_available; then
local service_file="$(linux_service_file)"
if systemctl --user is-active --quiet "${SERVICE_NAME}" 2>/dev/null; then
echo "Already running"
exit 0
fi
mkdir -p "$DATA_DIR/logs"
linux_create_service_file
linux_reload_daemon
systemctl --user start "${SERVICE_NAME}"
systemctl --user enable "${SERVICE_NAME}" 2>/dev/null || true
echo "Started wechat-claude-code daemon (Linux systemd)"
else
echo "Note: systemd user session not available, using direct mode"
echo "To enable systemd mode, run: 'loginctl enable-linger $(whoami)'"
echo ""
linux_direct_start
fi
}
linux_stop() {
if linux_systemd_available && systemctl --user cat "${SERVICE_NAME}" &>/dev/null; then
systemctl --user stop "${SERVICE_NAME}" 2>/dev/null || true
systemctl --user disable "${SERVICE_NAME}" 2>/dev/null || true
echo "Stopped wechat-claude-code daemon (Linux systemd)"
else
linux_direct_stop
fi
}
linux_restart() {
linux_stop
sleep 1
linux_start
}
linux_status() {
if linux_systemd_available && systemctl --user cat "${SERVICE_NAME}" &>/dev/null; then
if systemctl --user is-active --quiet "${SERVICE_NAME}" 2>/dev/null; then
local pid=$(systemctl --user show-property --value=MainPID "${SERVICE_NAME}" 2>/dev/null)
if [ -n "$pid" ] && [ "$pid" != "0" ]; then
echo "Running (PID: $pid)"
else
echo "Active"
fi
else
echo "Not running"
fi
if systemctl --user cat "${SERVICE_NAME}" &>/dev/null; then
echo ""
systemctl --user status "${SERVICE_NAME}" --no-pager 2>/dev/null || true
fi
else
linux_direct_status
fi
}
linux_logs() {
if command -v journalctl >/dev/null 2>&1; then
if journalctl --user --unit="${SERVICE_NAME}" --quiet &>/dev/null; then
echo "=== systemd journal logs (last 100 lines) ==="
journalctl --user --unit="${SERVICE_NAME}" --no-pager -n 100 2>/dev/null || true
echo ""
echo "=== File logs ==="
fi
fi
local log_dir="${DATA_DIR}/logs"
if [ -d "$log_dir" ]; then
for f in "${log_dir}"/stdout.log "${log_dir}"/stderr.log; do
if [ -f "$f" ]; then
echo "=== $(basename "$f") ==="
tail -50 "$f"
echo ""
fi
done
else
echo "No logs found"
fi
}
# =============================================================================
# Main dispatcher
# =============================================================================
main() {
local command="${1:-}"
case "$OS_TYPE" in
Darwin)
case "$command" in
start) macos_start ;;
stop) macos_stop ;;
restart) macos_stop; sleep 1; macos_start ;;
status) macos_status ;;
logs) macos_logs ;;
*)
echo "Usage: daemon.sh {start|stop|restart|status|logs}"
echo "Platform: macOS (launchd)"
exit 1
;;
esac
;;
Linux)
case "$command" in
start) linux_start ;;
stop) linux_stop ;;
restart) linux_restart ;;
status) linux_status ;;
logs) linux_logs ;;
*)
echo "Usage: daemon.sh {start|stop|restart|status|logs}"
echo "Platform: Linux (systemd)"
exit 1
;;
esac
;;
*)
echo "Error: Unsupported platform '$OS_TYPE'"
echo "Supported platforms: macOS (Darwin), Linux"
exit 1
;;
esac
}
main "$@"