Files
Cyrene/ethend.sh
T
AskaEth 6ef9e082a6 feat: 语音流式输入管线 + VAD前端集成 + 插件-工具合并清理
- 前端: VAD语音检测(@ricky0123/vad-web) + useVoiceInput双模式(流式WS/REST)
- Gateway: VoiceStreamManager代理WS流式STT到voice-service
- Voice-service: DashScope REST → Realtime WS → Whisper三级引擎 + ffmpeg转码
- 共享模块: pkg/audio(音频转换) + pkg/dashscope(ASR REST客户端)
- 清理: 移除旧plugin-manager和pkg/plugins,完成插件→工具合并
- 文档: 完善gateway-api.md和voice-service.md语音API文档
- 工具: scripts/voice/ 语音转换脚本集

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 11:50:40 +08:00

471 lines
13 KiB
Bash

#!/bin/bash
# ========================================
# Cyrene ethend 启动脚本
# 管理开发环境: 数据库 / 服务编译 / ethend 控制台
# ========================================
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ETHEND_DIR="$SCRIPT_DIR/ethend"
ROOT="$SCRIPT_DIR"
PORT="${ETHEND_PORT:-9090}"
LOG_DIR="$ETHEND_DIR/logs"
LOG_FILE="$LOG_DIR/sh.log"
# 颜色
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
# ========== 平台检测 ==========
IS_WIN=false
case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*) IS_WIN=true ;;
esac
# ========== 帮助 ==========
show_help() {
echo -e "${CYAN}Cyrene ethend${NC} — 开发环境管理工具"
echo ""
echo -e "${BOLD}用法:${NC} ./ethend.sh [命令] [选项]"
echo ""
echo -e "${BOLD}命令:${NC}"
echo " (无参数) 启动 ethend 控制台 (默认)"
echo " start 启动 ethend 控制台"
echo " start --fresh 强制重启全部后端服务后启动"
echo " start --build 编译全部服务后启动"
echo " stop 停止 ethend 控制台"
echo " status 查看所有服务状态"
echo " logs [服务ID] 查看服务日志 (默认显示最近 20 行)"
echo " build [服务ID] 编译服务 (不指定则编译全部)"
echo " db:start 启动数据库容器 (Docker Compose)"
echo " db:stop 停止数据库容器"
echo " db:status 检查数据库连接状态"
echo " help 显示此帮助"
echo ""
echo -e "${BOLD}选项:${NC}"
echo " --port, -p <端口> 指定 ethend 端口 (默认: 9090)"
echo " --fresh 启动前强制重启全部后端服务"
echo " --build 启动前编译全部服务"
echo ""
echo -e "${BOLD}示例:${NC}"
echo " ./ethend.sh # 快速启动"
echo " ./ethend.sh start --build # 编译后启动"
echo " ./ethend.sh start --fresh # 全新重启"
echo " ./ethend.sh logs gateway # 查看 Gateway 日志"
echo " ./ethend.sh build ai-core # 仅编译 AI-Core"
echo " ./ethend.sh db:status # 检查数据库"
echo ""
echo -e "${BOLD}Web 控制台:${NC} http://localhost:$PORT"
}
# ========== 依赖检查 ==========
check_deps() {
local missing=0
# Node.js
if ! command -v node &>/dev/null; then
if [ -x /usr/local/node/bin/node ]; then
export PATH="/usr/local/node/bin:$PATH"
else
echo -e "${RED}✗ Node.js 未安装${NC}"
missing=1
fi
fi
# Docker (用于数据库)
if ! command -v docker &>/dev/null; then
echo -e "${YELLOW}⚠ Docker 未安装 (数据库容器功能不可用)${NC}"
fi
# Go (用于编译后端服务)
if ! command -v go &>/dev/null; then
if $IS_WIN && [ -f "/c/Program Files/Go/bin/go.exe" ]; then
export PATH="$PATH:/c/Program Files/Go/bin"
else
echo -e "${YELLOW}⚠ Go 未安装 (服务编译功能不可用)${NC}"
fi
fi
return $missing
}
# ========== 加载 .env ==========
load_env() {
local env_file="$ROOT/.env"
if [ -f "$env_file" ]; then
echo -e "${GREEN}✓ 加载环境变量: .env${NC}"
set -a
source "$env_file"
set +a
else
echo -e "${YELLOW}⚠ 未找到 .env,使用默认值${NC}"
fi
}
# ========== 端口工具 (跨平台) ==========
port_in_use() {
local port=$1
if $IS_WIN; then
netstat -ano 2>/dev/null | grep -q ":$port " | grep -q "LISTENING"
else
ss -tlnp 2>/dev/null | grep -q ":$port " || netstat -tlnp 2>/dev/null | grep -q ":$port "
fi
}
kill_port() {
local port=$1
if $IS_WIN; then
local pid=$(netstat -ano 2>/dev/null | grep ":$port " | grep "LISTENING" | awk '{print $NF}' | head -1)
if [ -n "$pid" ] && [ "$pid" != "0" ]; then
powershell -Command "Stop-Process -Id $pid -Force" 2>/dev/null || true
fi
else
fuser -k "$port/tcp" 2>/dev/null || true
fi
sleep 1
}
# ========== 健康检查 ==========
health_check() {
local url=$1
local max_wait=${2:-30}
local waited=0
while [ $waited -lt $max_wait ]; do
if curl -s -o /dev/null -w "%{http_code}" "$url" 2>/dev/null | grep -q "200"; then
return 0
fi
sleep 1
waited=$((waited + 1))
done
return 1
}
# ========== 数据库管理 ==========
DB_COMPOSE_FILE="$ROOT/docker-compose.dev.db.yml"
db_status() {
if port_in_use 5432; then
echo -e "${GREEN}✓ PostgreSQL 在线 (端口 5432)${NC}"
return 0
else
echo -e "${RED}✗ PostgreSQL 离线${NC}"
return 1
fi
}
db_start() {
echo -e "${CYAN}启动数据库容器...${NC}"
if [ ! -f "$DB_COMPOSE_FILE" ]; then
echo -e "${RED}✗ 未找到 $DB_COMPOSE_FILE${NC}"
return 1
fi
docker compose -f "$DB_COMPOSE_FILE" up -d
echo -e "${YELLOW}等待数据库就绪...${NC}"
if health_check "http://localhost:5432" 30; then
echo -e "${GREEN}✓ 数据库已就绪${NC}"
else
echo -e "${YELLOW}⚠ 数据库可能仍在启动中,请稍后检查${NC}"
fi
}
db_stop() {
echo -e "${CYAN}停止数据库容器...${NC}"
if [ ! -f "$DB_COMPOSE_FILE" ]; then
echo -e "${RED}✗ 未找到 $DB_COMPOSE_FILE${NC}"
return 1
fi
docker compose -f "$DB_COMPOSE_FILE" down
echo -e "${GREEN}✓ 数据库已停止${NC}"
}
# ========== 服务编译 ==========
SERVICES=(
"memory-service:backend/memory-service"
"iot-debug-service:backend/iot-debug-service"
"voice-service:backend/voice-service"
"ai-core:backend/ai-core"
"plugin-manager:backend/cyrene-plugins"
"platform-bridge:backend/platform-bridge"
"gateway:backend/gateway"
)
build_service() {
local id=$1
local dir=$2
local label=${3:-$id}
echo -e "${CYAN}[编译] $label...${NC}"
local binary="main"
$IS_WIN && binary="main.exe"
cd "$ROOT/$dir"
local build_target="./cmd/main.go"
if [ "$id" = "plugin-manager" ]; then
build_target="./cmd/plugin-manager/"
fi
if GOWORK=off go build -o "$binary" "$build_target" 2>&1; then
echo -e "${GREEN}$label 编译完成${NC}"
return 0
else
echo -e "${RED}$label 编译失败${NC}"
return 1
fi
}
build_all() {
echo -e "${BOLD}编译全部后端服务...${NC}"
local failed=0
for entry in "${SERVICES[@]}"; do
IFS=':' read -r id dir <<< "$entry"
# 检查目录是否存在
if [ -d "$ROOT/$dir" ]; then
build_service "$id" "$dir" "$id" || failed=$((failed + 1))
fi
done
if [ $failed -eq 0 ]; then
echo -e "${GREEN}✓ 全部编译完成${NC}"
else
echo -e "${YELLOW}$failed 个服务编译失败${NC}"
fi
cd "$ROOT"
return $failed
}
# ========== 查看日志 ==========
show_logs() {
local service_id=$1
local lines=${2:-20}
local log_path="$LOG_DIR/${service_id}.log"
if [ ! -f "$log_path" ]; then
echo -e "${YELLOW}日志文件不存在: $log_path${NC}"
return 1
fi
echo -e "${BOLD}=== $service_id 日志 (最近 $lines 行) ===${NC}"
tail -n "$lines" "$log_path"
}
# ========== 启动 ethend ==========
start_ethend() {
local do_build=false
local do_fresh=false
# 解析参数
for arg in "$@"; do
case $arg in
--build) do_build=true ;;
--fresh) do_fresh=true ;;
--port=*) PORT="${arg#*=}" ;;
-p) shift; PORT="$1" ;;
esac
done
echo ""
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${CYAN} Cyrene ethend${NC}"
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
# 依赖检查
check_deps || exit 1
echo -e "${YELLOW}Node.js:${NC} $(node --version)"
command -v go &>/dev/null && echo -e "${YELLOW}Go:${NC} $(go version | cut -d' ' -f3)"
command -v docker &>/dev/null && echo -e "${YELLOW}Docker:${NC} $(docker --version | cut -d' ' -f3 | tr -d ',')"
# 加载环境变量
load_env
# 编译 (如果指定)
if $do_build; then
build_all
fi
# 全新重启 (如果指定)
if $do_fresh; then
echo -e "${YELLOW}强制重启全部后端服务...${NC}"
curl -s -X POST "http://localhost:$PORT/api/services/start-all-fresh" 2>/dev/null || true
fi
# 检查并释放端口
if port_in_use "$PORT"; then
# 检查是否是已有 ethend 实例
if curl -s -o /dev/null "http://localhost:$PORT/api/health" 2>/dev/null; then
echo -e "${GREEN}✓ ethend 已在运行: http://localhost:$PORT${NC}"
echo -e "${CYAN} API: http://localhost:$PORT/api/health${NC}"
return 0
fi
echo -e "${YELLOW}⚠ 端口 $PORT 被占用,正在释放...${NC}"
kill_port "$PORT"
fi
# 切换到 ethend 目录
cd "$ETHEND_DIR"
# 安装依赖
if [ ! -d "node_modules" ] || [ ! -f "node_modules/.package-lock.json" ]; then
echo -e "${YELLOW}安装依赖...${NC}"
npm install --silent
fi
# 确保日志目录存在
mkdir -p "$LOG_DIR"
echo ""
echo -e "${GREEN}启动 ethend 服务器 (端口: $PORT)...${NC}"
echo -e "${CYAN} Web 控制台: http://localhost:$PORT${NC}"
echo -e "${CYAN} API: http://localhost:$PORT/api/health${NC}"
echo -e "${CYAN} WebSocket: ws://localhost:$PORT/ws${NC}"
echo ""
echo -e "${YELLOW}正在后台启动...${NC}"
# 后台启动 ethend
nohup node src/index.js > "$LOG_FILE" 2>&1 &
local pid=$!
cd "$ROOT"
# 等待健康检查
echo -e "${YELLOW}等待服务就绪...${NC}"
if health_check "http://localhost:$PORT/api/health" 30; then
echo ""
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN} ethend 已启动!${NC}"
echo -e "${GREEN} PID: ${pid}${NC}"
echo -e "${GREEN} 控制台: http://localhost:$PORT${NC}"
echo -e "${GREEN} 日志: ${LOG_FILE}${NC}"
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
# 检查数据库状态
db_status
return 0
fi
# 超时
if kill -0 "$pid" 2>/dev/null; then
echo ""
echo -e "${YELLOW}⚠ 服务可能仍在启动中 (已等待 30 秒)${NC}"
echo -e "${CYAN} 请稍后检查 http://localhost:$PORT/api/health${NC}"
else
echo ""
echo -e "${RED}✗ 服务启动失败,请检查日志: ${LOG_FILE}${NC}"
return 1
fi
}
# ========== 停止 ethend ==========
stop_ethend() {
if port_in_use "$PORT"; then
echo -e "${CYAN}停止 ethend...${NC}"
if $IS_WIN; then
kill_port "$PORT"
else
fuser -k "$PORT/tcp" 2>/dev/null || true
fi
echo -e "${GREEN}✓ ethend 已停止${NC}"
else
echo -e "${YELLOW}ethend 未在运行${NC}"
fi
}
# ========== 查看状态 ==========
show_status() {
if curl -s -o /dev/null "http://localhost:$PORT/api/health" 2>/dev/null; then
echo -e "${GREEN}✓ ethend 在线${NC}"
# 获取服务状态
local status_json=$(curl -s "http://localhost:$PORT/api/services" 2>/dev/null)
if [ -n "$status_json" ]; then
echo ""
echo -e "${BOLD}服务状态:${NC}"
local tmpfile="$(mktemp)"
echo "$status_json" > "$tmpfile"
node -e "
const fs = require('fs');
const data = JSON.parse(fs.readFileSync(process.argv[1], 'utf-8'));
for (const [id, svc] of Object.entries(data)) {
const icon = svc.status === 'running' ? '✓' : svc.status === 'stopped' ? '✗' : '○';
const color = svc.status === 'running' ? '\x1b[32m' : '\x1b[31m';
const pid = svc.pid ? ' (PID: ' + svc.pid + ')' : '';
const uptime = svc.uptime ? ' | uptime: ' + Math.round(svc.uptime / 1000) + 's' : '';
console.log(' ' + color + icon + '\x1b[0m ' + svc.name.padEnd(16) + ' [' + svc.status + ']' + pid + uptime);
}
" "$tmpfile"
rm -f "$tmpfile"
fi
# 数据库状态
echo ""
db_status
else
echo -e "${RED}✗ ethend 离线${NC}"
fi
}
# ========== 主入口 ==========
CMD="${1:-start}"
shift 2>/dev/null || true
case "$CMD" in
help|--help|-h)
show_help
;;
start|"")
start_ethend "$@"
;;
stop)
stop_ethend
;;
status)
show_status
;;
logs)
if [ -z "$1" ]; then
echo -e "${YELLOW}用法: ./ethend.sh logs <服务ID>${NC}"
echo "可用服务: gateway, ai-core, memory-service, tool-engine, voice-service, iot-debug-service, plugin-manager, platform-bridge, frontend"
exit 1
fi
show_logs "$1" "${2:-20}"
;;
build)
check_deps || exit 1
load_env
if [ -n "$1" ]; then
# 查找服务目录
for entry in "${SERVICES[@]}"; do
IFS=':' read -r id dir <<< "$entry"
if [ "$id" = "$1" ]; then
build_service "$id" "$dir" "$id"
exit $?
fi
done
echo -e "${RED}未知服务: $1${NC}"
echo "可用服务: ${SERVICES[*]}"
exit 1
else
build_all
fi
;;
db:start)
db_start
;;
db:stop)
db_stop
;;
db:status)
db_status
;;
*)
echo -e "${RED}未知命令: $CMD${NC}"
echo ""
show_help
exit 1
;;
esac