diff --git a/deploy/storyforge-fnos-web-v4.compose.yaml b/deploy/storyforge-fnos-web-v4.compose.yaml new file mode 100644 index 0000000..4778f55 --- /dev/null +++ b/deploy/storyforge-fnos-web-v4.compose.yaml @@ -0,0 +1,9 @@ +services: + storyforge-web-v4-dev: + image: docker.m.daocloud.io/library/nginx:alpine + container_name: storyforge-web-v4-dev + restart: unless-stopped + ports: + - "${STORYFORGE_WEB_V4_DEV_PORT:-19192}:80" + volumes: + - ../../storyforge/web/storyforge-web-v4:/usr/share/nginx/html:ro diff --git a/scripts/deploy_fnos_storyforge_web.sh b/scripts/deploy_fnos_storyforge_web.sh new file mode 100755 index 0000000..4e68810 --- /dev/null +++ b/scripts/deploy_fnos_storyforge_web.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" + +export CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" +export FNOS_SKILL="${FNOS_SKILL:-$CODEX_HOME/skills/fnos-hyzq-deploy}" +export FNOS_SSH="${FNOS_SSH:-$FNOS_SKILL/scripts/fnos_ssh.sh}" +export FNOS_SCP="${FNOS_SCP:-$FNOS_SKILL/scripts/fnos_scp.sh}" + +FNOS_HOST="${FNOS_HOST:-192.168.31.188}" +FNOS_USER="${FNOS_USER:-krisolo}" +REMOTE_ROOT="${STORYFORGE_FNOS_REMOTE_ROOT:-/vol1/docker/hyzq-stack/current/storyforge}" +REMOTE_WEB_PARENT="$REMOTE_ROOT/web" +REMOTE_WEB_DIR="$REMOTE_WEB_PARENT/storyforge-web-v4" +REMOTE_ASSETS_DIR="$REMOTE_WEB_DIR/assets" +REMOTE_COMPOSE_DIR="${STORYFORGE_FNOS_COMPOSE_DIR:-/vol1/docker/hyzq-stack/current/deploy/fnos}" +REMOTE_COMPOSE_FILE="$REMOTE_COMPOSE_DIR/storyforge-fnos-web-v4.compose.yaml" +BACKEND_URL="${STORYFORGE_FNOS_BACKEND_URL:-https://storyforge.hyzq.net}" +WEB_PORT="${STORYFORGE_WEB_V4_DEV_PORT:-19192}" + +need_cmd() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "missing required command: $1" >&2 + exit 1 + fi +} + +need_cmd python3 + +if [ -z "${FNOS_PASSWORD:-}" ]; then + need_cmd security +fi + +shell_quote() { + python3 - "$1" <<'PY' +import shlex +import sys +print(shlex.quote(sys.argv[1])) +PY +} + +json_quote() { + python3 - "$1" <<'PY' +import json +import sys +print(json.dumps(sys.argv[1], ensure_ascii=False)) +PY +} + +resolve_password() { + if [ -n "${FNOS_PASSWORD:-}" ]; then + printf '%s' "$FNOS_PASSWORD" + return 0 + fi + security find-internet-password -s "$FNOS_HOST" -a "$FNOS_USER" -w +} + +run_remote_sudo() { + local password="$1" + shift + local remote_cmd="$*" + local password_quoted + password_quoted="$(shell_quote "$password")" + "$FNOS_SSH" "$(shell_quote "printf '%s\\n' $password_quoted | sudo -S -p '' sh -lc $(shell_quote "$remote_cmd")")" +} + +PASSWORD="$(resolve_password)" +TMPDIR_DEPLOY="$(mktemp -d)" +RUNTIME_CONFIG_FILE="$TMPDIR_DEPLOY/storyforge-runtime-config.js" +BACKEND_URL_JSON="$(json_quote "$BACKEND_URL")" +trap 'rm -rf "$TMPDIR_DEPLOY"' EXIT + +cat >"$RUNTIME_CONFIG_FILE" </dev/null; then + break + fi + sleep 2 +done +curl -fsS "http://$FNOS_HOST:$WEB_PORT/" >/dev/null +curl -fsS "http://$FNOS_HOST:$WEB_PORT/assets/storyforge-runtime-config.js" | grep -q "$BACKEND_URL" + +echo "fnOS StoryForge Web V4 ready: http://$FNOS_HOST:$WEB_PORT/" diff --git a/web/storyforge-web-v4/README.md b/web/storyforge-web-v4/README.md index 4715ee5..1516b55 100644 --- a/web/storyforge-web-v4/README.md +++ b/web/storyforge-web-v4/README.md @@ -110,7 +110,7 @@ python3 -m http.server 3918 - [http://127.0.0.1:3918/index.html](http://127.0.0.1:3918/index.html) -首次进入需要手动连接后端,默认地址是: +如果本地是 `127.0.0.1 / localhost`,前端默认会连: - `http://127.0.0.1:8081` @@ -122,6 +122,18 @@ python3 -m http.server 3918 - `https://storyforge.hyzq.net` +如果页面部署在 NAS 之类的局域网静态站点上,推荐通过运行时配置显式指定后端: + +- `assets/storyforge-runtime-config.js` +- `window.__STORYFORGE_RUNTIME_CONFIG__.backendUrl = "https://storyforge.hyzq.net"` + +仓库内已经提供一键部署脚本,可直接把前端发到飞牛 NAS 上跑开发测试: + +```bash +cd /Users/kris/code/StoryForge-gitea +./scripts/deploy_fnos_storyforge_web.sh +``` + ## 后续建议 - 继续补多平台各自更深的专属采集与解析能力,而不只是一套统一抽象层 diff --git a/web/storyforge-web-v4/assets/storyforge-api-client.js b/web/storyforge-web-v4/assets/storyforge-api-client.js index 432c61a..e7e89e9 100644 --- a/web/storyforge-web-v4/assets/storyforge-api-client.js +++ b/web/storyforge-web-v4/assets/storyforge-api-client.js @@ -3,6 +3,11 @@ if (typeof window === "undefined") { return "http://127.0.0.1:8081"; } + const runtimeConfig = window.__STORYFORGE_RUNTIME_CONFIG__ || window.__STORYFORGE_CONFIG__ || {}; + const configuredBackendUrl = String(runtimeConfig.backendUrl || runtimeConfig.backendURL || "").trim(); + if (configuredBackendUrl) { + return configuredBackendUrl.replace(/\/$/, ""); + } const { origin, hostname, port, pathname } = window.location; if (/^https?:/i.test(origin) && hostname === "storyforge.hyzq.net") { return origin; diff --git a/web/storyforge-web-v4/assets/storyforge-runtime-config.js b/web/storyforge-web-v4/assets/storyforge-runtime-config.js new file mode 100644 index 0000000..e54393c --- /dev/null +++ b/web/storyforge-web-v4/assets/storyforge-runtime-config.js @@ -0,0 +1,8 @@ +(function () { + window.__STORYFORGE_RUNTIME_CONFIG__ = Object.assign( + { + backendUrl: "" + }, + window.__STORYFORGE_RUNTIME_CONFIG__ || {} + ); +})(); diff --git a/web/storyforge-web-v4/index.html b/web/storyforge-web-v4/index.html index 3b27ce8..6f78058 100644 --- a/web/storyforge-web-v4/index.html +++ b/web/storyforge-web-v4/index.html @@ -1912,6 +1912,7 @@ +