mirror of
https://github.com/cft0808/edict.git
synced 2026-09-06 17:09:57 +08:00
288 lines
9.9 KiB
Bash
Executable File
288 lines
9.9 KiB
Bash
Executable File
#!/bin/bash
|
||
# 三省六部 · OpenCode 一键启动
|
||
|
||
set -euo pipefail
|
||
|
||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
cd "$REPO_DIR"
|
||
|
||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
|
||
|
||
DASHBOARD_HOST="${EDICT_DASHBOARD_HOST:-127.0.0.1}"
|
||
DASHBOARD_PORT="${EDICT_DASHBOARD_PORT:-7891}"
|
||
OPENCODE_HOST="${OPENCODE_HOST:-127.0.0.1}"
|
||
OPENCODE_PORT="${OPENCODE_PORT:-4096}"
|
||
OPENCODE_SERVER_URL="${OPENCODE_SERVER_URL:-http://${OPENCODE_HOST}:${OPENCODE_PORT}}"
|
||
PIDDIR="$REPO_DIR/.pids"
|
||
LOGDIR="$REPO_DIR/logs"
|
||
SERVER_PIDFILE="$PIDDIR/server.pid"
|
||
LOOP_PIDFILE="$PIDDIR/loop.pid"
|
||
OPENCODE_PIDFILE="$PIDDIR/opencode.pid"
|
||
SERVER_LOG="$LOGDIR/server.log"
|
||
LOOP_LOG="$LOGDIR/loop.log"
|
||
OPENCODE_LOG="$LOGDIR/opencode.log"
|
||
|
||
resolve_python() {
|
||
local candidate version major minor
|
||
for candidate in "${EDICT_PYTHON:-}" python3.14 python3.13 python3.12 python3.11 python3.10 python3; do
|
||
[ -n "$candidate" ] || continue
|
||
command -v "$candidate" &>/dev/null || continue
|
||
version=$("$candidate" -c 'import sys; print(f"{sys.version_info[0]}.{sys.version_info[1]}")' 2>/dev/null) || continue
|
||
major=${version%%.*}
|
||
minor=${version#*.}
|
||
if [ "$major" -gt 3 ] || { [ "$major" -eq 3 ] && [ "$minor" -ge 10 ]; }; then
|
||
command -v "$candidate"
|
||
return 0
|
||
fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
resolve_opencode() {
|
||
if [[ -n "${OPENCODE_BIN:-}" && -x "${OPENCODE_BIN:-}" ]]; then
|
||
echo "$OPENCODE_BIN"
|
||
return 0
|
||
fi
|
||
if command -v opencode &>/dev/null; then
|
||
command -v opencode
|
||
return 0
|
||
fi
|
||
if [[ -x "$HOME/.opencode/bin/opencode" ]]; then
|
||
echo "$HOME/.opencode/bin/opencode"
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
http_ok() {
|
||
"$PYTHON_BIN" - "$1" <<'PY' >/dev/null 2>&1
|
||
import sys
|
||
import urllib.request
|
||
try:
|
||
r = urllib.request.urlopen(sys.argv[1], timeout=3)
|
||
raise SystemExit(0 if 200 <= r.status < 500 else 1)
|
||
except Exception:
|
||
raise SystemExit(1)
|
||
PY
|
||
}
|
||
|
||
opencode_agents_ok() {
|
||
"$PYTHON_BIN" - "$OPENCODE_SERVER_URL" <<'PY' >/dev/null 2>&1
|
||
import json
|
||
import sys
|
||
import urllib.request
|
||
try:
|
||
url = sys.argv[1].rstrip("/") + "/agent"
|
||
data = json.loads(urllib.request.urlopen(url, timeout=5).read().decode("utf-8"))
|
||
names = {item.get("name") for item in data if isinstance(item, dict)}
|
||
raise SystemExit(0 if {"taizi", "zhongshu", "shangshu"}.issubset(names) else 1)
|
||
except Exception:
|
||
raise SystemExit(1)
|
||
PY
|
||
}
|
||
|
||
write_opencode_pid_from_port() {
|
||
command -v lsof &>/dev/null || return 0
|
||
local pid
|
||
pid=$(lsof -tiTCP:"$OPENCODE_PORT" -sTCP:LISTEN 2>/dev/null | head -n 1 || true)
|
||
if [[ "$pid" =~ ^[0-9]+$ ]]; then
|
||
echo "$pid" > "$OPENCODE_PIDFILE"
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
stop_pidfile() {
|
||
local pidfile="$1"
|
||
if [[ -f "$pidfile" ]]; then
|
||
local pid
|
||
pid=$(cat "$pidfile" 2>/dev/null || true)
|
||
if [[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null; then
|
||
kill "$pid" 2>/dev/null || true
|
||
for _ in $(seq 1 10); do
|
||
kill -0 "$pid" 2>/dev/null || break
|
||
sleep 0.3
|
||
done
|
||
fi
|
||
rm -f "$pidfile"
|
||
fi
|
||
}
|
||
|
||
stop_opencode_on_port() {
|
||
command -v lsof &>/dev/null || return 0
|
||
local pids cmd
|
||
pids=$(lsof -tiTCP:"$OPENCODE_PORT" -sTCP:LISTEN 2>/dev/null || true)
|
||
for pid in $pids; do
|
||
cmd=$(ps -p "$pid" -o command= 2>/dev/null || true)
|
||
if [[ "$cmd" == *"opencode"* ]]; then
|
||
kill "$pid" 2>/dev/null || true
|
||
fi
|
||
done
|
||
return 0
|
||
}
|
||
|
||
stop_screen_session() {
|
||
local name="$1"
|
||
if command -v screen &>/dev/null && screen -ls 2>/dev/null | grep -q "[.]${name}[[:space:]]"; then
|
||
screen -S "$name" -X quit >/dev/null 2>&1 || true
|
||
fi
|
||
}
|
||
|
||
start_opencode_server() {
|
||
if command -v screen &>/dev/null; then
|
||
screen -dmS edict-opencode bash -lc "cd \"$REPO_DIR\" && echo \$\$ > \"$OPENCODE_PIDFILE\" && exec \"$OPENCODE_BIN_RESOLVED\" serve --hostname \"$OPENCODE_HOST\" --port \"$OPENCODE_PORT\" >> \"$OPENCODE_LOG\" 2>&1"
|
||
else
|
||
nohup "$OPENCODE_BIN_RESOLVED" serve --hostname "$OPENCODE_HOST" --port "$OPENCODE_PORT" \
|
||
>> "$OPENCODE_LOG" 2>&1 &
|
||
echo $! > "$OPENCODE_PIDFILE"
|
||
fi
|
||
}
|
||
|
||
start_dashboard_server() {
|
||
if command -v screen &>/dev/null; then
|
||
screen -dmS edict-server bash -lc "cd \"$REPO_DIR\" && echo \$\$ > \"$SERVER_PIDFILE\" && exec env EDICT_RUNTIME=opencode EDICT_AGENT_RUNTIME=opencode OPENCODE_BIN=\"$OPENCODE_BIN_RESOLVED\" OPENCODE_SERVER_URL=\"$OPENCODE_SERVER_URL\" \"$PYTHON_BIN\" \"$REPO_DIR/dashboard/server.py\" --host \"$DASHBOARD_HOST\" --port \"$DASHBOARD_PORT\" >> \"$SERVER_LOG\" 2>&1"
|
||
else
|
||
nohup env EDICT_RUNTIME=opencode EDICT_AGENT_RUNTIME=opencode \
|
||
OPENCODE_BIN="$OPENCODE_BIN_RESOLVED" OPENCODE_SERVER_URL="$OPENCODE_SERVER_URL" \
|
||
"$PYTHON_BIN" "$REPO_DIR/dashboard/server.py" --host "$DASHBOARD_HOST" --port "$DASHBOARD_PORT" \
|
||
>> "$SERVER_LOG" 2>&1 &
|
||
echo $! > "$SERVER_PIDFILE"
|
||
fi
|
||
}
|
||
|
||
start_refresh_loop() {
|
||
if command -v screen &>/dev/null; then
|
||
screen -dmS edict-loop bash -lc "cd \"$REPO_DIR\" && echo \$\$ > \"$LOOP_PIDFILE\" && exec env EDICT_RUNTIME=opencode EDICT_AGENT_RUNTIME=opencode EDICT_PYTHON=\"$PYTHON_BIN\" OPENCODE_BIN=\"$OPENCODE_BIN_RESOLVED\" OPENCODE_SERVER_URL=\"$OPENCODE_SERVER_URL\" bash \"$REPO_DIR/scripts/run_loop_opencode.sh\" >> \"$LOOP_LOG\" 2>&1"
|
||
else
|
||
nohup env EDICT_RUNTIME=opencode EDICT_AGENT_RUNTIME=opencode EDICT_PYTHON="$PYTHON_BIN" \
|
||
OPENCODE_BIN="$OPENCODE_BIN_RESOLVED" OPENCODE_SERVER_URL="$OPENCODE_SERVER_URL" \
|
||
bash "$REPO_DIR/scripts/run_loop_opencode.sh" \
|
||
>> "$LOOP_LOG" 2>&1 &
|
||
echo $! > "$LOOP_PIDFILE"
|
||
fi
|
||
}
|
||
|
||
stop_repo_dashboard_on_port() {
|
||
command -v lsof &>/dev/null || return 0
|
||
local pids cmd
|
||
pids=$(lsof -tiTCP:"$DASHBOARD_PORT" -sTCP:LISTEN 2>/dev/null || true)
|
||
for pid in $pids; do
|
||
cmd=$(ps -p "$pid" -o command= 2>/dev/null || true)
|
||
if [[ "$cmd" == *"$REPO_DIR/dashboard/server.py"* || "$cmd" == *"dashboard/server.py"* ]]; then
|
||
kill "$pid" 2>/dev/null || true
|
||
fi
|
||
done
|
||
return 0
|
||
}
|
||
|
||
stop_repo_loops() {
|
||
command -v lsof &>/dev/null || return 0
|
||
local pid cmd cwd
|
||
while read -r pid cmd; do
|
||
[[ -n "${pid:-}" ]] || continue
|
||
if [[ "$cmd" != *"scripts/run_loop.sh"* && "$cmd" != *"scripts/run_loop_opencode.sh"* ]]; then
|
||
continue
|
||
fi
|
||
cwd=$(lsof -a -p "$pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -n 1 || true)
|
||
if [[ "$cwd" == "$REPO_DIR" ]]; then
|
||
kill "$pid" 2>/dev/null || true
|
||
fi
|
||
done < <(ps ax -o pid=,command=)
|
||
return 0
|
||
}
|
||
|
||
PYTHON_BIN=$(resolve_python) || {
|
||
echo -e "${RED}未找到 Python 3.10+${NC}"
|
||
exit 1
|
||
}
|
||
OPENCODE_BIN_RESOLVED=$(resolve_opencode) || {
|
||
echo -e "${RED}未找到 OpenCode CLI。请先安装 opencode,或设置 OPENCODE_BIN。${NC}"
|
||
exit 1
|
||
}
|
||
|
||
export EDICT_PYTHON="$PYTHON_BIN"
|
||
export EDICT_RUNTIME=opencode
|
||
export EDICT_AGENT_RUNTIME=opencode
|
||
export OPENCODE_BIN="$OPENCODE_BIN_RESOLVED"
|
||
export OPENCODE_SERVER_URL
|
||
|
||
mkdir -p "$PIDDIR" "$LOGDIR" "$REPO_DIR/data"
|
||
for f in live_status.json agent_config.json model_change_log.json sync_status.json; do
|
||
[[ -f "$REPO_DIR/data/$f" ]] || echo '{}' > "$REPO_DIR/data/$f"
|
||
done
|
||
[[ -f "$REPO_DIR/data/pending_model_changes.json" ]] || echo '[]' > "$REPO_DIR/data/pending_model_changes.json"
|
||
[[ -f "$REPO_DIR/data/tasks_source.json" ]] || echo '[]' > "$REPO_DIR/data/tasks_source.json"
|
||
[[ -f "$REPO_DIR/data/tasks.json" ]] || echo '[]' > "$REPO_DIR/data/tasks.json"
|
||
[[ -f "$REPO_DIR/data/officials.json" ]] || echo '[]' > "$REPO_DIR/data/officials.json"
|
||
[[ -f "$REPO_DIR/data/officials_stats.json" ]] || echo '{}' > "$REPO_DIR/data/officials_stats.json"
|
||
|
||
echo ""
|
||
echo -e "${BLUE}╔══════════════════════════════════════════╗${NC}"
|
||
echo -e "${BLUE}║ 三省六部 · OpenCode 模式启动中 ║${NC}"
|
||
echo -e "${BLUE}╚══════════════════════════════════════════╝${NC}"
|
||
echo ""
|
||
echo -e "Python: ${GREEN}${PYTHON_BIN}${NC}"
|
||
echo -e "OpenCode: ${GREEN}${OPENCODE_BIN_RESOLVED}${NC}"
|
||
|
||
echo -e "${GREEN}▶ 同步 OpenCode agent 配置...${NC}"
|
||
"$PYTHON_BIN" "$REPO_DIR/scripts/sync_opencode_agents.py"
|
||
|
||
if http_ok "${OPENCODE_SERVER_URL}/doc" && opencode_agents_ok; then
|
||
write_opencode_pid_from_port
|
||
echo -e "${GREEN}▶ 复用已运行的 OpenCode server: ${OPENCODE_SERVER_URL}${NC}"
|
||
else
|
||
if http_ok "${OPENCODE_SERVER_URL}/doc"; then
|
||
echo -e "${YELLOW}⚠️ 当前 ${OPENCODE_SERVER_URL} 不是本项目的 OpenCode server,正在切换...${NC}"
|
||
stop_screen_session edict-opencode
|
||
stop_opencode_on_port
|
||
stop_pidfile "$OPENCODE_PIDFILE"
|
||
sleep 0.5
|
||
fi
|
||
echo -e "${GREEN}▶ 启动 OpenCode server...${NC}"
|
||
stop_screen_session edict-opencode
|
||
start_opencode_server
|
||
for _ in $(seq 1 20); do
|
||
http_ok "${OPENCODE_SERVER_URL}/doc" && break
|
||
sleep 0.5
|
||
done
|
||
if ! http_ok "${OPENCODE_SERVER_URL}/doc" || ! opencode_agents_ok; then
|
||
echo -e "${RED}OpenCode server 启动失败,请查看日志: ${OPENCODE_LOG}${NC}"
|
||
exit 1
|
||
fi
|
||
write_opencode_pid_from_port
|
||
fi
|
||
|
||
echo -e "${GREEN}▶ 切换看板到 OpenCode 运行时...${NC}"
|
||
stop_pidfile "$SERVER_PIDFILE"
|
||
stop_pidfile "$LOOP_PIDFILE"
|
||
stop_screen_session edict-server
|
||
stop_screen_session edict-loop
|
||
stop_repo_dashboard_on_port
|
||
stop_repo_loops
|
||
rm -f /tmp/sansheng_liubu_opencode_refresh.pid
|
||
sleep 0.5
|
||
|
||
start_dashboard_server
|
||
start_refresh_loop
|
||
|
||
for _ in $(seq 1 20); do
|
||
http_ok "http://${DASHBOARD_HOST}:${DASHBOARD_PORT}/healthz" && break
|
||
sleep 0.5
|
||
done
|
||
|
||
if ! http_ok "http://${DASHBOARD_HOST}:${DASHBOARD_PORT}/healthz"; then
|
||
echo -e "${RED}看板启动失败,请查看日志: ${SERVER_LOG}${NC}"
|
||
exit 1
|
||
fi
|
||
|
||
echo ""
|
||
echo -e "${GREEN}✅ OpenCode 模式已启动${NC}"
|
||
echo -e " 看板: ${BLUE}http://${DASHBOARD_HOST}:${DASHBOARD_PORT}${NC}"
|
||
echo -e " OpenCode: ${BLUE}${OPENCODE_SERVER_URL}${NC}"
|
||
echo -e " 日志: ${BLUE}${LOGDIR}${NC}"
|
||
|
||
if command -v open &>/dev/null; then
|
||
open "http://${DASHBOARD_HOST}:${DASHBOARD_PORT}" >/dev/null 2>&1 || true
|
||
elif command -v xdg-open &>/dev/null; then
|
||
xdg-open "http://${DASHBOARD_HOST}:${DASHBOARD_PORT}" >/dev/null 2>&1 || true
|
||
fi
|