234 lines
8.8 KiB
Bash
Executable File
234 lines
8.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
FNOS_HOST="${FNOS_HOST:-192.168.31.188}"
|
|
WEB_PORT="${STORYFORGE_WEB_V4_DEV_PORT:-19192}"
|
|
COLLECTOR_PORT="${STORYFORGE_COLLECTOR_DEV_PORT:-19193}"
|
|
CUTVIDEO_FORWARD_PORT="${CUTVIDEO_FORWARD_PORT:-19186}"
|
|
STORYFORGE_COMPAT_FORWARD_PORT="${STORYFORGE_COMPAT_FORWARD_PORT:-19181}"
|
|
WEB_URL="${STORYFORGE_FNOS_WEB_URL:-http://$FNOS_HOST:$WEB_PORT}"
|
|
BACKEND_URL="${STORYFORGE_FNOS_COLLECTOR_URL:-http://$FNOS_HOST:$COLLECTOR_PORT}"
|
|
CUTVIDEO_URL="${CUTVIDEO_BASE_URL:-http://$FNOS_HOST:$CUTVIDEO_FORWARD_PORT}"
|
|
COMPAT_URL="${STORYFORGE_COMPAT_BASE_URL:-http://$FNOS_HOST:$STORYFORGE_COMPAT_FORWARD_PORT}"
|
|
LIVE_RECORDER_URL="${LIVE_RECORDER_BASE_URL:-http://$FNOS_HOST:19106}"
|
|
CURL_MAX_TIME="${STORYFORGE_FNOS_CURL_MAX_TIME:-60}"
|
|
|
|
need_cmd() {
|
|
if ! command -v "$1" >/dev/null 2>&1; then
|
|
echo "missing required command: $1" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
need_cmd curl
|
|
need_cmd python3
|
|
need_cmd rg
|
|
|
|
curl_fetch() {
|
|
curl -fsS --max-time "$CURL_MAX_TIME" "$@"
|
|
}
|
|
|
|
tmp_dir="$(mktemp -d)"
|
|
trap 'rm -rf "$tmp_dir"' EXIT
|
|
|
|
index_file="$tmp_dir/index.html"
|
|
runtime_file="$tmp_dir/runtime.js"
|
|
health_file="$tmp_dir/health.json"
|
|
session_file="$tmp_dir/session.json"
|
|
projects_file="$tmp_dir/projects.json"
|
|
action_registry_file="$tmp_dir/action-registry.json"
|
|
integrations_file="$tmp_dir/integrations.json"
|
|
bootstrap_file="$tmp_dir/bootstrap.json"
|
|
compat_file="$tmp_dir/compat.html"
|
|
live_recorder_health_file="$tmp_dir/live-recorder-health.json"
|
|
token_file="$tmp_dir/token.txt"
|
|
project_id_file="$tmp_dir/project-id.txt"
|
|
asr_url_file="$tmp_dir/asr-url.txt"
|
|
asr_wav="$tmp_dir/asr.wav"
|
|
asr_result="$tmp_dir/asr.json"
|
|
|
|
echo "[1/8] check fnOS web"
|
|
curl_fetch "$WEB_URL/" >"$index_file"
|
|
rg -Fq "StoryForge" "$index_file"
|
|
echo "web ok"
|
|
|
|
echo "[2/8] check runtime config"
|
|
curl_fetch "$WEB_URL/assets/storyforge-runtime-config.js" >"$runtime_file"
|
|
rg -Fq "$BACKEND_URL" "$runtime_file"
|
|
echo "runtime config ok"
|
|
|
|
echo "[3/8] check collector healthz"
|
|
curl_fetch "$BACKEND_URL/healthz" >"$health_file"
|
|
python3 -c '
|
|
import json, pathlib, sys
|
|
payload = json.loads(pathlib.Path(sys.argv[1]).read_text())
|
|
expected_cutvideo = sys.argv[2]
|
|
if str(payload.get("status") or "").lower() != "ok":
|
|
raise SystemExit(f"unexpected health status: {payload.get('status')!r}")
|
|
lan_routing = payload.get("lanRouting") or {}
|
|
if not isinstance(lan_routing, dict):
|
|
raise SystemExit("lanRouting missing")
|
|
if lan_routing.get("cutvideoBaseUrl") != expected_cutvideo:
|
|
raise SystemExit(f"unexpected cutvideoBaseUrl: {lan_routing.get('cutvideoBaseUrl')!r}")
|
|
if lan_routing.get("cutvideoRouteMode") != "fnos_tunnel":
|
|
raise SystemExit(f"unexpected cutvideoRouteMode: {lan_routing.get('cutvideoRouteMode')!r}")
|
|
asr_url = str(payload.get("asrHttpBaseUrl") or "").strip()
|
|
pathlib.Path(sys.argv[3]).write_text(asr_url, encoding="utf-8")
|
|
print("healthz ok")
|
|
' "$health_file" "$CUTVIDEO_URL" "$asr_url_file"
|
|
|
|
echo "[4/8] check ASR transcribe"
|
|
python3 - "$asr_wav" <<'PY'
|
|
import math
|
|
import struct
|
|
import sys
|
|
import wave
|
|
|
|
path = sys.argv[1]
|
|
sample_rate = 16000
|
|
duration = 0.25
|
|
freq = 440.0
|
|
samples = int(sample_rate * duration)
|
|
with wave.open(path, "wb") as handle:
|
|
handle.setnchannels(1)
|
|
handle.setsampwidth(2)
|
|
handle.setframerate(sample_rate)
|
|
for i in range(samples):
|
|
value = int(16000 * math.sin(2 * math.pi * freq * (i / sample_rate)))
|
|
handle.writeframes(struct.pack("<h", value))
|
|
PY
|
|
asr_url="$(cat "$asr_url_file")"
|
|
curl_fetch -X POST -F "wav=@$asr_wav" "$asr_url/transcribe" >"$asr_result"
|
|
python3 - "$asr_result" <<'PY'
|
|
import json
|
|
import pathlib
|
|
import sys
|
|
|
|
payload = json.loads(pathlib.Path(sys.argv[1]).read_text())
|
|
if "duration_ms" not in payload:
|
|
raise SystemExit("missing duration_ms")
|
|
if "success" not in payload:
|
|
raise SystemExit("missing success flag")
|
|
print("asr transcribe ok")
|
|
PY
|
|
|
|
echo "[5/8] check auto-session"
|
|
curl_fetch -X POST "$BACKEND_URL/v2/auth/auto-session" \
|
|
-H 'content-type: application/json' \
|
|
-d '{}' >"$session_file"
|
|
python3 -c '
|
|
import json, pathlib, sys
|
|
payload = json.loads(pathlib.Path(sys.argv[1]).read_text())
|
|
token = str(payload.get("token") or "")
|
|
mode = str(payload.get("mode") or "")
|
|
username = str((payload.get("account") or {}).get("username") or "")
|
|
if not token:
|
|
raise SystemExit("auto-session did not return token")
|
|
if mode != "auto":
|
|
raise SystemExit(f"unexpected mode: {mode!r}")
|
|
if not username:
|
|
raise SystemExit("auto-session returned empty username")
|
|
pathlib.Path(sys.argv[2]).write_text(token, encoding="utf-8")
|
|
print(f"auto-session ok: {username}")
|
|
' "$session_file" "$token_file"
|
|
token="$(cat "$token_file")"
|
|
|
|
echo "[6/8] check live project context and action registry"
|
|
curl_fetch "$BACKEND_URL/v2/projects" \
|
|
-H "Authorization: Bearer $token" >"$projects_file"
|
|
python3 -c '
|
|
import json, pathlib, sys
|
|
projects = json.loads(pathlib.Path(sys.argv[1]).read_text())
|
|
if not isinstance(projects, list) or not projects:
|
|
raise SystemExit("projects endpoint returned no project")
|
|
project_id = str(projects[0].get("id") or "")
|
|
if not project_id:
|
|
raise SystemExit("project id missing")
|
|
pathlib.Path(sys.argv[2]).write_text(project_id, encoding="utf-8")
|
|
print(f"project ok: {project_id}")
|
|
' "$projects_file" "$project_id_file"
|
|
project_id="$(cat "$project_id_file")"
|
|
curl_fetch "$BACKEND_URL/v2/oneliner/action-registry?project_id=$project_id" \
|
|
-H "Authorization: Bearer $token" >"$action_registry_file"
|
|
python3 -c '
|
|
import json, pathlib, sys
|
|
payload = json.loads(pathlib.Path(sys.argv[1]).read_text())
|
|
items = payload.get("items") or []
|
|
keys = {str(item.get("action_key") or "") for item in items}
|
|
required = {"import-homepage", "search-similar-accounts", "save-benchmark-link", "refresh-tracking", "mark-tracking-read"}
|
|
required = required | {"track-account", "import-video-link", "import-text", "generate-copy", "create-assistant"}
|
|
missing = sorted(required - keys)
|
|
if missing:
|
|
raise SystemExit(f"action registry missing: {missing}")
|
|
print("action registry ok")
|
|
' "$action_registry_file"
|
|
|
|
echo "[7/8] check integrations health"
|
|
curl_fetch "$BACKEND_URL/v2/integrations/health" \
|
|
-H "Authorization: Bearer $token" >"$integrations_file"
|
|
python3 -c '
|
|
import json, pathlib, sys
|
|
payload = json.loads(pathlib.Path(sys.argv[1]).read_text())
|
|
expected_cutvideo = sys.argv[2]
|
|
cutvideo = payload.get("cutvideo") or {}
|
|
if cutvideo.get("base_url") != expected_cutvideo:
|
|
raise SystemExit(f"unexpected cutvideo base_url: {cutvideo.get('base_url')!r}")
|
|
if not cutvideo.get("reachable"):
|
|
raise SystemExit("cutvideo is not reachable")
|
|
if not cutvideo.get("supports_uploads"):
|
|
raise SystemExit("cutvideo uploads are not available")
|
|
if cutvideo.get("deployment_label") != "NAS 隧道":
|
|
raise SystemExit(f"unexpected cutvideo deployment label: {cutvideo.get('deployment_label')!r}")
|
|
n8n = payload.get("n8n") or {}
|
|
if not n8n.get("reachable") or n8n.get("deployment_label") != "服务器":
|
|
raise SystemExit(f"unexpected n8n status: {n8n!r}")
|
|
huobao = payload.get("huobao") or {}
|
|
if not huobao.get("reachable") or huobao.get("deployment_label") != "服务器":
|
|
raise SystemExit(f"unexpected huobao status: {huobao!r}")
|
|
asr = payload.get("asr") or {}
|
|
if not asr.get("reachable") or asr.get("deployment_label") != "Windows":
|
|
raise SystemExit(f"unexpected asr status: {asr!r}")
|
|
if not str(asr.get("active_device") or "").strip():
|
|
raise SystemExit("asr active_device missing")
|
|
live_recorder = payload.get("live_recorder") or {}
|
|
if not live_recorder.get("reachable") or live_recorder.get("deployment_label") != "NAS":
|
|
raise SystemExit(f"unexpected live recorder status: {live_recorder!r}")
|
|
local_model = payload.get("local_model") or {}
|
|
if str(local_model.get("error") or "") != "not_configured":
|
|
raise SystemExit(f"local_model should be not_configured, got {local_model.get('error')!r}")
|
|
print("integrations ok")
|
|
' "$integrations_file" "$CUTVIDEO_URL"
|
|
|
|
echo "[8/8] check fnOS tunnel endpoints"
|
|
curl_fetch "$CUTVIDEO_URL/api/bootstrap" >"$bootstrap_file"
|
|
curl_fetch "$COMPAT_URL/" >"$compat_file"
|
|
python3 -c '
|
|
import json, pathlib, sys
|
|
payload = json.loads(pathlib.Path(sys.argv[1]).read_text())
|
|
if not payload:
|
|
raise SystemExit("empty cutvideo bootstrap payload")
|
|
print("cutvideo bootstrap ok")
|
|
' "$bootstrap_file"
|
|
if ! rg -Fq "数字人网页业务台" "$compat_file" && ! rg -Fq "BUSINESS CONSOLE" "$compat_file"; then
|
|
echo "compat page does not look like the expected business console" >&2
|
|
exit 1
|
|
fi
|
|
echo "compat ok"
|
|
|
|
echo "[8/8] check live recorder health"
|
|
curl_fetch "$LIVE_RECORDER_URL/api/healthz" >"$live_recorder_health_file"
|
|
python3 -c '
|
|
import json, pathlib, sys
|
|
payload = json.loads(pathlib.Path(sys.argv[1]).read_text())
|
|
if payload.get("ok") is not True:
|
|
raise SystemExit(f"unexpected live recorder health payload: {payload!r}")
|
|
print("live recorder ok")
|
|
' "$live_recorder_health_file"
|
|
|
|
echo "fnOS lan smoke passed:"
|
|
echo " web: $WEB_URL/"
|
|
echo " collector: $BACKEND_URL"
|
|
echo " cutvideo: $CUTVIDEO_URL"
|
|
echo " compat: $COMPAT_URL"
|
|
echo " live_recorder: $LIVE_RECORDER_URL"
|