405 lines
16 KiB
Bash
405 lines
16 KiB
Bash
#!/bin/bash
|
||
# ══════════════════════════════════════════════════════════
|
||
# CiBird 词鸟 安装脚本
|
||
# 一键部署你的私人英语单词本
|
||
# 项目地址:https://github.com/zhangyang-games/cibird
|
||
# ══════════════════════════════════════════════════════════
|
||
|
||
set -e
|
||
|
||
# ── 颜色 ──────────────────────────────────────────────────
|
||
GREEN='\033[0;32m'
|
||
BLUE='\033[0;34m'
|
||
RED='\033[0;31m'
|
||
YELLOW='\033[1;33m'
|
||
CYAN='\033[0;36m'
|
||
BOLD='\033[1m'
|
||
DIM='\033[2m'
|
||
NC='\033[0m'
|
||
|
||
REPO_RAW="https://raw.githubusercontent.com/zhangyang-games/cibird/main"
|
||
INSTALL_DIR="$HOME/cibird"
|
||
|
||
# ── Banner ────────────────────────────────────────────────
|
||
print_banner() {
|
||
clear
|
||
echo ""
|
||
echo -e "${BOLD}${CYAN}"
|
||
echo " ██████╗██╗██████╗ ██╗██████╗ ██████╗"
|
||
echo " ██╔════╝██║██╔══██╗██║██╔══██╗██╔══██╗"
|
||
echo " ██║ ██║██████╔╝██║██████╔╝██║ ██║"
|
||
echo " ██║ ██║██╔══██╗██║██╔══██╗██║ ██║"
|
||
echo " ╚██████╗██║██████╔╝██║██║ ██║██████╔╝"
|
||
echo " ╚═════╝╚═╝╚═════╝ ╚═╝╚═╝ ╚═╝╚═════╝"
|
||
echo -e "${NC}"
|
||
echo -e "${BOLD} ┌─────────────────────────────────────────────┐${NC}"
|
||
echo -e "${BOLD} │ 🦜 CiBird 词鸟 · 你的私人英语单词本 │${NC}"
|
||
echo -e "${DIM} │ AI造句 · 发音 · 推特/游戏英语场景 │${NC}"
|
||
echo -e "${BOLD} └─────────────────────────────────────────────┘${NC}"
|
||
echo ""
|
||
}
|
||
|
||
# ── 步骤提示 ──────────────────────────────────────────────
|
||
step() { echo -e "\n${BOLD}${CYAN}[$1]${NC} $2"; }
|
||
ok() { echo -e " ${GREEN}✓${NC} $1"; }
|
||
err() { echo -e " ${RED}✗ 错误:${NC}$1"; exit 1; }
|
||
info() { echo -e " ${DIM}$1${NC}"; }
|
||
ask() { echo -e " ${YELLOW}→${NC} $1"; }
|
||
|
||
# ── 0. 检查基础依赖 ────────────────────────────────────────
|
||
check_deps() {
|
||
step "0/6" "检查系统环境"
|
||
|
||
for cmd in curl python3; do
|
||
if ! command -v $cmd &>/dev/null; then
|
||
info "正在安装 $cmd ..."
|
||
if command -v apt-get &>/dev/null; then
|
||
apt-get update -qq && apt-get install -y $cmd -qq
|
||
elif command -v apk &>/dev/null; then
|
||
apk add --no-cache $cmd
|
||
elif command -v yum &>/dev/null; then
|
||
yum install -y $cmd
|
||
else
|
||
err "请手动安装 $cmd 后重试"
|
||
fi
|
||
fi
|
||
ok "$cmd 已就绪"
|
||
done
|
||
}
|
||
|
||
# ── 1. 安装 Python 依赖 ────────────────────────────────────
|
||
install_python_deps() {
|
||
step "1/6" "安装 Python 依赖包"
|
||
info "需要安装:fastapi uvicorn httpx"
|
||
|
||
# 判断是否需要 --break-system-packages(Ubuntu 22+)
|
||
BREAK_SYS=""
|
||
if python3 -c "import sys; exit(0 if sys.version_info>=(3,11) else 1)" 2>/dev/null; then
|
||
BREAK_SYS="--break-system-packages"
|
||
fi
|
||
# 另一种判断:pip 报 externally-managed
|
||
if pip3 install --help 2>&1 | grep -q 'break-system'; then
|
||
BREAK_SYS="--break-system-packages"
|
||
fi
|
||
|
||
python3 -m pip install --quiet fastapi uvicorn[standard] httpx $BREAK_SYS 2>/dev/null \
|
||
|| pip3 install --quiet fastapi uvicorn[standard] httpx $BREAK_SYS 2>/dev/null \
|
||
|| err "pip 安装失败,请手动执行:pip3 install fastapi uvicorn httpx"
|
||
|
||
ok "Python 依赖安装完成"
|
||
}
|
||
|
||
# ── 2. 下载项目文件 ────────────────────────────────────────
|
||
download_files() {
|
||
step "2/6" "下载 CiBird 项目文件"
|
||
mkdir -p "$INSTALL_DIR"
|
||
|
||
for f in server.py index.html; do
|
||
info "下载 $f ..."
|
||
curl -fsSL "$REPO_RAW/$f" -o "$INSTALL_DIR/$f" \
|
||
|| err "下载 $f 失败,请检查网络或 GitHub 地址"
|
||
ok "$f 已下载"
|
||
done
|
||
}
|
||
|
||
# ── 3. 选择 AI 服务商 ──────────────────────────────────────
|
||
choose_provider() {
|
||
step "3/6" "选择 AI 服务商"
|
||
echo ""
|
||
echo -e " ${BOLD}请选择你要使用的 AI 服务商:${NC}"
|
||
echo ""
|
||
echo -e " ${GREEN}1)${NC} Google Gemini ${DIM}← 免费额度大,推荐新手${NC}"
|
||
echo -e " ${GREEN}2)${NC} DeepSeek ${DIM}← 国产之光,便宜效果好${NC}"
|
||
echo -e " ${GREEN}3)${NC} Groq ${DIM}← 速度极快,免费${NC}"
|
||
echo -e " ${GREEN}4)${NC} OpenRouter ${DIM}← 一个Key用几十种模型${NC}"
|
||
echo -e " ${GREEN}5)${NC} Claude ${DIM}← Anthropic,你有付费Key${NC}"
|
||
echo -e " ${GREEN}6)${NC} OpenAI ${DIM}← ChatGPT 同款${NC}"
|
||
echo ""
|
||
read -p " 👉 输入编号 [1-6]:" PROVIDER_CHOICE
|
||
|
||
case "$PROVIDER_CHOICE" in
|
||
1)
|
||
PROVIDER="gemini"
|
||
MODEL="gemini-2.0-flash"
|
||
info "获取 Key:https://aistudio.google.com/apikey"
|
||
;;
|
||
2)
|
||
PROVIDER="deepseek"
|
||
MODEL="deepseek-chat"
|
||
info "获取 Key:https://platform.deepseek.com"
|
||
;;
|
||
3)
|
||
PROVIDER="groq"
|
||
MODEL="llama-3.1-8b-instant"
|
||
info "获取 Key:https://console.groq.com"
|
||
;;
|
||
4)
|
||
PROVIDER="openrouter"
|
||
MODEL="google/gemini-flash-1.5"
|
||
info "获取 Key:https://openrouter.ai"
|
||
;;
|
||
5)
|
||
PROVIDER="claude"
|
||
MODEL="claude-haiku-4-5-20251001"
|
||
info "获取 Key:https://console.anthropic.com"
|
||
;;
|
||
6)
|
||
PROVIDER="openai"
|
||
MODEL="gpt-4o-mini"
|
||
info "获取 Key:https://platform.openai.com"
|
||
;;
|
||
*)
|
||
err "无效选择,请输入 1-6"
|
||
;;
|
||
esac
|
||
|
||
ok "已选择:$PROVIDER(模型:$MODEL)"
|
||
|
||
echo ""
|
||
read -p " 👉 请粘贴你的 API Key:" API_KEY
|
||
[ -z "$API_KEY" ] && err "API Key 不能为空"
|
||
ok "API Key 已录入"
|
||
|
||
echo ""
|
||
echo -e " ${DIM}💡 如果你想用其他模型,可以直接改默认值,也可以回车使用默认${NC}"
|
||
read -p " 👉 模型名称(默认 $MODEL,直接回车跳过):" MODEL_INPUT
|
||
[ -n "$MODEL_INPUT" ] && MODEL="$MODEL_INPUT"
|
||
ok "模型:$MODEL"
|
||
}
|
||
|
||
# ── 4. 设置访问密码 ────────────────────────────────────────
|
||
setup_auth() {
|
||
step "4/6" "设置登录账号"
|
||
|
||
echo ""
|
||
read -p " 👉 用户名(默认 admin,回车跳过):" USERNAME
|
||
[ -z "$USERNAME" ] && USERNAME="admin"
|
||
ok "用户名:$USERNAME"
|
||
|
||
echo ""
|
||
while true; do
|
||
read -s -p " 👉 设置登录密码(至少6位):" PASSWORD
|
||
echo ""
|
||
[ ${#PASSWORD} -ge 6 ] && break
|
||
echo -e " ${RED}密码太短,请至少6位!${NC}"
|
||
done
|
||
read -s -p " 👉 再次确认密码:" PASSWORD2
|
||
echo ""
|
||
[ "$PASSWORD" != "$PASSWORD2" ] && err "两次密码不一致"
|
||
ok "密码已设置"
|
||
|
||
echo ""
|
||
read -p " 👉 访问端口(默认 8848,回车跳过):" PORT
|
||
[ -z "$PORT" ] && PORT="8848"
|
||
ok "端口:$PORT"
|
||
|
||
# 生成密码 hash
|
||
PASSWORD_HASH=$(python3 -c "import hashlib; print(hashlib.sha256('$PASSWORD'.encode()).hexdigest())")
|
||
}
|
||
|
||
# ── 5. 写入配置 ────────────────────────────────────────────
|
||
write_config() {
|
||
step "5/6" "写入配置文件"
|
||
|
||
cat > "$INSTALL_DIR/config.json" <<EOF
|
||
{
|
||
"username": "$USERNAME",
|
||
"password_hash": "$PASSWORD_HASH",
|
||
"port": $PORT,
|
||
"provider": "$PROVIDER",
|
||
"api_key": "$API_KEY",
|
||
"model": "$MODEL"
|
||
}
|
||
EOF
|
||
chmod 600 "$INSTALL_DIR/config.json"
|
||
ok "config.json 已写入(权限已设为仅自己可读)"
|
||
}
|
||
|
||
# ── 6. 启动服务 ────────────────────────────────────────────
|
||
start_service() {
|
||
step "6/6" "启动 CiBird 服务"
|
||
|
||
# 停掉旧进程
|
||
if [ -f "$INSTALL_DIR/cibird.pid" ]; then
|
||
OLD_PID=$(cat "$INSTALL_DIR/cibird.pid")
|
||
kill "$OLD_PID" 2>/dev/null && info "已停止旧进程 (PID $OLD_PID)" || true
|
||
rm -f "$INSTALL_DIR/cibird.pid"
|
||
fi
|
||
# 也搜一下
|
||
pkill -f "server.py" 2>/dev/null || true
|
||
sleep 1
|
||
|
||
# 初始化数据库
|
||
cd "$INSTALL_DIR"
|
||
python3 -c "
|
||
import sys; sys.path.insert(0,'.')
|
||
from server import init_db
|
||
init_db()
|
||
print(' DB 初始化完成')
|
||
"
|
||
# 后台启动
|
||
nohup python3 "$INSTALL_DIR/server.py" >> "$INSTALL_DIR/cibird.log" 2>&1 &
|
||
echo $! > "$INSTALL_DIR/cibird.pid"
|
||
sleep 2
|
||
|
||
# 验证是否启动成功
|
||
if kill -0 $(cat "$INSTALL_DIR/cibird.pid") 2>/dev/null; then
|
||
ok "服务启动成功!(PID $(cat $INSTALL_DIR/cibird.pid))"
|
||
else
|
||
err "服务启动失败,查看日志:cat $INSTALL_DIR/cibird.log"
|
||
fi
|
||
}
|
||
|
||
# ── 写入快捷命令 ───────────────────────────────────────────
|
||
write_shortcut() {
|
||
SHORTCUT="/usr/local/bin/cibird"
|
||
cat > "$SHORTCUT" <<'SCRIPT'
|
||
#!/bin/bash
|
||
INSTALL_DIR="$HOME/cibird"
|
||
GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'
|
||
CYAN='\033[0;36m'; BOLD='\033[1m'; DIM='\033[2m'; NC='\033[0m'
|
||
|
||
get_pid() { [ -f "$INSTALL_DIR/cibird.pid" ] && cat "$INSTALL_DIR/cibird.pid" || echo ""; }
|
||
is_running() { PID=$(get_pid); [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; }
|
||
get_port() { python3 -c "import json; print(json.load(open('$INSTALL_DIR/config.json'))['port'])" 2>/dev/null || echo "8848"; }
|
||
|
||
case "$1" in
|
||
start)
|
||
if is_running; then
|
||
echo -e " ${YELLOW}词鸟已经在运行中${NC}"
|
||
else
|
||
cd "$INSTALL_DIR"
|
||
nohup python3 server.py >> cibird.log 2>&1 &
|
||
echo $! > cibird.pid
|
||
sleep 1
|
||
echo -e " ${GREEN}✓ 词鸟已启动${NC} 访问 http://$(curl -s ifconfig.me 2>/dev/null || echo '你的IP'):$(get_port)"
|
||
fi
|
||
;;
|
||
stop)
|
||
PID=$(get_pid)
|
||
[ -n "$PID" ] && kill "$PID" 2>/dev/null && echo -e " ${GREEN}✓ 词鸟已停止${NC}" || echo -e " ${DIM}没有运行中的词鸟${NC}"
|
||
rm -f "$INSTALL_DIR/cibird.pid"
|
||
;;
|
||
restart)
|
||
$0 stop; sleep 1; $0 start
|
||
;;
|
||
log)
|
||
tail -f "$INSTALL_DIR/cibird.log"
|
||
;;
|
||
status)
|
||
PORT=$(get_port)
|
||
if is_running; then
|
||
echo -e " ${GREEN}● 词鸟运行中${NC} PID $(get_pid)"
|
||
echo -e " ${DIM}访问地址:http://$(curl -s ifconfig.me 2>/dev/null || echo '你的IP'):$PORT${NC}"
|
||
else
|
||
echo -e " ${RED}○ 词鸟未运行${NC}"
|
||
fi
|
||
;;
|
||
*)
|
||
echo ""
|
||
echo -e " ${BOLD}🦜 CiBird 词鸟 管理命令${NC}"
|
||
echo ""
|
||
PORT=$(get_port)
|
||
if is_running; then
|
||
echo -e " ${GREEN}● 状态:运行中${NC} PID $(get_pid)"
|
||
else
|
||
echo -e " ${RED}○ 状态:未运行${NC}"
|
||
fi
|
||
echo -e " ${DIM}访问地址:http://你的IP:$PORT${NC}"
|
||
echo ""
|
||
echo -e " ${CYAN}cibird start${NC} 启动"
|
||
echo -e " ${CYAN}cibird stop${NC} 停止"
|
||
echo -e " ${CYAN}cibird restart${NC} 重启"
|
||
echo -e " ${CYAN}cibird status${NC} 查看状态"
|
||
echo -e " ${CYAN}cibird log${NC} 查看日志"
|
||
echo ""
|
||
;;
|
||
esac
|
||
SCRIPT
|
||
chmod +x "$SHORTCUT" 2>/dev/null || true
|
||
ok "快捷命令 cibird 已安装(可用 cibird start/stop/status)"
|
||
}
|
||
|
||
# ── 获取公网 IP ────────────────────────────────────────────
|
||
get_public_ip() {
|
||
PUBLIC_IP=$(curl -s --connect-timeout 5 ifconfig.me 2>/dev/null \
|
||
|| curl -s --connect-timeout 5 api.ipify.org 2>/dev/null \
|
||
|| echo "你的VPS公网IP")
|
||
}
|
||
|
||
|
||
# ── 导入单词 ───────────────────────────────────────────────
|
||
import_words() {
|
||
echo ""
|
||
echo -e " ${BOLD}📖 导入多邻国单词本${NC}"
|
||
echo -e " ${DIM}从 GitHub 拉取 words.txt 导入到词库${NC}"
|
||
echo ""
|
||
WORDS_URL="https://raw.githubusercontent.com/zhangyang-games/cibird/main/words.txt"
|
||
curl -sL "$WORDS_URL" | python3 -c "
|
||
import sys,sqlite3,pathlib
|
||
DB=pathlib.Path.home()/'cibird/cibird.db'
|
||
if not DB.exists(): print(' 找不到数据库'); exit(1)
|
||
words=[l.strip().split('|',1) for l in sys.stdin if '|' in l]
|
||
conn=sqlite3.connect(DB)
|
||
existing=set(r[0].lower() for r in conn.execute('SELECT word FROM words'))
|
||
added=skipped=0
|
||
for w in words:
|
||
if len(w)!=2: continue
|
||
word,meaning=w
|
||
if word.lower() in existing: skipped+=1; continue
|
||
conn.execute('INSERT INTO words(word,meaning,phonetic,pos,examples,note) VALUES(?,?,?,?,?,?)',(word,meaning,'','','[]',''))
|
||
existing.add(word.lower()); added+=1
|
||
conn.commit(); conn.close()
|
||
print(f' ✓ 导入完成!新增 {added} 个,跳过 {skipped} 个')
|
||
" || echo -e " ${RED}✗ 导入失败,请检查网络${NC}"
|
||
}
|
||
# ═══════════════════════════════════════════════════════════
|
||
# MAIN
|
||
# ═══════════════════════════════════════════════════════════
|
||
print_banner
|
||
|
||
echo -e " 欢迎!这个脚本会帮你在 VPS 上部署 CiBird 词鸟。"
|
||
echo -e " ${DIM}整个过程大约需要 2-3 分钟,请跟着提示一步步来。${NC}"
|
||
echo ""
|
||
read -p " 按回车键开始安装,Ctrl+C 取消..." _
|
||
|
||
check_deps
|
||
install_python_deps
|
||
download_files
|
||
choose_provider
|
||
setup_auth
|
||
write_config
|
||
start_service
|
||
write_shortcut
|
||
get_public_ip
|
||
|
||
# ── 可选:导入单词 ─────────────────────────────────────────
|
||
echo ""
|
||
read -p " 👉 是否导入你的多邻国单词本?(输入 y 导入,回车跳过):" IMPORT_CHOICE
|
||
if [ "$IMPORT_CHOICE" = "y" ] || [ "$IMPORT_CHOICE" = "Y" ]; then
|
||
import_words
|
||
fi
|
||
|
||
# ── 完成提示 ──────────────────────────────────────────────
|
||
echo ""
|
||
echo -e "${BOLD}${GREEN}"
|
||
echo " ┌──────────────────────────────────────────────┐"
|
||
echo " │ 🎉 CiBird 词鸟安装完成! │"
|
||
echo " └──────────────────────────────────────────────┘"
|
||
echo -e "${NC}"
|
||
echo -e " ${BOLD}🌐 访问地址:${CYAN}http://$PUBLIC_IP:$PORT${NC}"
|
||
echo -e " ${BOLD}👤 用户名:${NC}$USERNAME"
|
||
echo -e " ${BOLD}🔑 密码:${NC}你刚才设置的密码"
|
||
echo ""
|
||
echo -e " ${DIM}💡 管理命令:${NC}"
|
||
echo -e " ${DIM} cibird 查看状态${NC}"
|
||
echo -e " ${DIM} cibird stop 停止服务${NC}"
|
||
echo -e " ${DIM} cibird start 启动服务${NC}"
|
||
echo -e " ${DIM} cibird log 查看日志(排查问题用)${NC}"
|
||
echo ""
|
||
echo -e " ${DIM}⚠️ 如果访问不了,请确认防火墙已开放 $PORT 端口${NC}"
|
||
echo -e " ${DIM} Oracle Cloud 需要在安全列表里手动添加规则${NC}"
|
||
echo ""
|
||
echo -e " ${BOLD}展翅高飞!🦜${NC}"
|
||
echo ""
|