diff --git a/README.md b/README.md
index 5b4ddea..bbdd42d 100644
--- a/README.md
+++ b/README.md
@@ -132,6 +132,7 @@ N8N_BASE_URL=http://127.0.0.1:5670
- `collector-service`:`http://127.0.0.1:8081`
- `n8n`:`http://127.0.0.1:5670`
- `cli-proxy-api`:`http://127.0.0.1:8317`
+- 公网入口:`https://test.hyzq.net/storyforge/`
默认会创建最高权限账号:
@@ -151,6 +152,11 @@ N8N_BASE_URL=http://127.0.0.1:5670
- 触发 `real_cut_pipeline`
- 触发 `ai_video_pipeline`
- 历史旧运行链已完成移除,当前运行时只保留 StoryForge 自身服务与外部执行引擎
+- 当前公网接入采用“云服务器 HTTPS 入口 + 本机桥接隧道 + 本机现网执行链”模式:
+ - `https://test.hyzq.net/storyforge/` 由云服务器 `nginx` 提供 HTTPS 入口
+ - `/storyforge/` 静态页反向代理到本机 `Web V4` 静态服务
+ - `/storyforge/v2/*`、`/storyforge/openapi.json`、`/storyforge/healthz` 反向代理到本机 `collector-service`
+ - `cutvideo / huobao / 本机模型 / NAS 录制` 继续由本机和局域网执行链提供
## 说明
diff --git a/deploy/STORYFORGE_TEST_HYZQ_GATEWAY.md b/deploy/STORYFORGE_TEST_HYZQ_GATEWAY.md
new file mode 100644
index 0000000..cb870f7
--- /dev/null
+++ b/deploy/STORYFORGE_TEST_HYZQ_GATEWAY.md
@@ -0,0 +1,37 @@
+# StoryForge `test.hyzq.net/storyforge` 公网入口
+
+当前公网接入不是把执行链整体迁到云服务器,而是:
+
+1. 云服务器 `nginx` 提供 `https://test.hyzq.net/storyforge/`
+2. 本机 `collector-service` 继续承接业务与局域网执行引擎
+3. 本机 `Web V4` 静态服务继续承接前端
+4. 本机通过 SSH 反向隧道把这两个入口桥接到云服务器
+
+## 端口映射
+
+- 云服务器 `127.0.0.1:18181` -> 本机 `127.0.0.1:8081`
+- 云服务器 `127.0.0.1:18191` -> 本机 `127.0.0.1:3918`
+
+## 本机常驻服务
+
+- `com.storyforge.collector`
+- `com.storyforge.web-v4`
+- `com.storyforge.cloud-bridge`
+
+## 云服务器 `nginx` 路由
+
+- `/storyforge/` -> `127.0.0.1:18191`
+- `/storyforge/v2/*` -> `127.0.0.1:18181`
+- `/storyforge/openapi.json` -> `127.0.0.1:18181/openapi.json`
+- `/storyforge/healthz` -> `127.0.0.1:18181/healthz`
+
+## 当前优点
+
+- 不需要把 `cutvideo / huobao / NAS live-recorder / 本机模型` 全部搬上云
+- 公网入口统一
+- 本机现网能力不需要改造即可对外开放
+
+## 当前限制
+
+- 本机桥接断开时,公网入口不可用
+- 这是公网 staging / 私有运营入口,更接近“公网可访问的现网桥接”,不是最终完全云原生部署
diff --git a/deploy/com.storyforge.cloud-bridge.plist.example b/deploy/com.storyforge.cloud-bridge.plist.example
new file mode 100644
index 0000000..164ae77
--- /dev/null
+++ b/deploy/com.storyforge.cloud-bridge.plist.example
@@ -0,0 +1,40 @@
+
+
+
+
+ Label
+ com.storyforge.cloud-bridge
+ ProgramArguments
+
+ /usr/bin/ssh
+ -N
+ -i
+ /Users/kris/.ssh/id_ed25519_kylin188
+ -o
+ BatchMode=yes
+ -o
+ ExitOnForwardFailure=yes
+ -o
+ ServerAliveInterval=30
+ -o
+ ServerAliveCountMax=3
+ -o
+ StrictHostKeyChecking=no
+ -o
+ UserKnownHostsFile=/Users/kris/.ssh/known_hosts
+ -R
+ 127.0.0.1:18181:127.0.0.1:8081
+ -R
+ 127.0.0.1:18191:127.0.0.1:3918
+ ubuntu@111.231.132.51
+
+ RunAtLoad
+
+ KeepAlive
+
+ StandardOutPath
+ /Users/kris/code/StoryForge/data/collector/cloud-bridge.log
+ StandardErrorPath
+ /Users/kris/code/StoryForge/data/collector/cloud-bridge.log
+
+
diff --git a/deploy/com.storyforge.web-v4.plist.example b/deploy/com.storyforge.web-v4.plist.example
new file mode 100644
index 0000000..35d8416
--- /dev/null
+++ b/deploy/com.storyforge.web-v4.plist.example
@@ -0,0 +1,29 @@
+
+
+
+
+ Label
+ com.storyforge.web-v4
+ ProgramArguments
+
+ /usr/bin/python3
+ -m
+ http.server
+ 3918
+ --bind
+ 127.0.0.1
+ --directory
+ /Users/kris/code/StoryForge/web/storyforge-web-v4
+
+ WorkingDirectory
+ /Users/kris/code/StoryForge/web/storyforge-web-v4
+ RunAtLoad
+
+ KeepAlive
+
+ StandardOutPath
+ /Users/kris/code/StoryForge/data/collector/web-v4.log
+ StandardErrorPath
+ /Users/kris/code/StoryForge/data/collector/web-v4.log
+
+
diff --git a/deploy/storyforge-test-hyzq-nginx.conf.fragment b/deploy/storyforge-test-hyzq-nginx.conf.fragment
new file mode 100644
index 0000000..554601a
--- /dev/null
+++ b/deploy/storyforge-test-hyzq-nginx.conf.fragment
@@ -0,0 +1,101 @@
+location = /storyforge {
+ auth_basic off;
+ return 301 /storyforge/;
+}
+
+location = /storyforge/healthz {
+ auth_basic off;
+ proxy_pass http://127.0.0.1:18181/healthz;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+}
+
+location = /storyforge/openapi.json {
+ auth_basic off;
+ proxy_pass http://127.0.0.1:18181/openapi.json;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+}
+
+location = /storyforge/api/v1/app/update/latest {
+ auth_basic off;
+ proxy_pass http://127.0.0.1:18181/api/v1/app/update/latest;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+}
+
+location ^~ /storyforge/downloads/ {
+ auth_basic off;
+ proxy_pass http://127.0.0.1:18181/downloads/;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_read_timeout 300s;
+ proxy_send_timeout 300s;
+}
+
+location ^~ /storyforge/v2/ {
+ auth_basic off;
+ proxy_pass http://127.0.0.1:18181/v2/;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_read_timeout 300s;
+ proxy_send_timeout 300s;
+ proxy_buffering off;
+}
+
+location ^~ /storyforge/docs {
+ auth_basic off;
+ rewrite ^/storyforge(/docs.*)$ $1 break;
+ proxy_pass http://127.0.0.1:18181;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_read_timeout 300s;
+ proxy_send_timeout 300s;
+ proxy_buffering off;
+}
+
+location ^~ /storyforge/redoc {
+ auth_basic off;
+ rewrite ^/storyforge(/redoc.*)$ $1 break;
+ proxy_pass http://127.0.0.1:18181;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_read_timeout 300s;
+ proxy_send_timeout 300s;
+ proxy_buffering off;
+}
+
+location ^~ /storyforge/ {
+ auth_basic off;
+ rewrite ^/storyforge/?(.*)$ /$1 break;
+ proxy_pass http://127.0.0.1:18191;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_read_timeout 300s;
+ proxy_send_timeout 300s;
+ proxy_buffering off;
+}
diff --git a/web/storyforge-web-v4/README.md b/web/storyforge-web-v4/README.md
index 299a0d0..1fbb2fd 100644
--- a/web/storyforge-web-v4/README.md
+++ b/web/storyforge-web-v4/README.md
@@ -14,6 +14,7 @@
- 目录已经从 `output/ui/` 原型区独立出来,并接上了第一层真实业务接口
- 这里面向国内平台的 Web 承载,当前覆盖 `douyin`、`xiaohongshu`、`bilibili`、`kuaishou`、`wechat_video`
- `YouTube` 目前明确不在本轮范围内
+- 已支持通过 `https://test.hyzq.net/storyforge/` 做公网访问
- 通用的项目、内容源、复盘、集成等流程可以正常使用
- 平台工作台和运行时数据目前只有 `douyin` 做到了完整实现,其余平台统一按 `待接入工作台` 处理
- 当前保留的核心页面结构:
@@ -99,6 +100,14 @@ python3 -m http.server 3918
- `http://127.0.0.1:8081`
+如果页面部署在:
+
+- `https://test.hyzq.net/storyforge/`
+
+前端会自动把默认后端切到同源的:
+
+- `https://test.hyzq.net/storyforge`
+
## 后续建议
- 继续补多平台各自更深的专属采集与解析能力,而不只是一套统一抽象层
diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js
index 64828b0..29d0620 100644
--- a/web/storyforge-web-v4/assets/app.js
+++ b/web/storyforge-web-v4/assets/app.js
@@ -1,5 +1,20 @@
const STORAGE_KEY = "storyforge-web-v4-session";
-const DEFAULT_BACKEND_URL = "http://127.0.0.1:8081";
+
+function detectDefaultBackendUrl() {
+ if (typeof window === "undefined") {
+ return "http://127.0.0.1:8081";
+ }
+ const { origin, hostname, port, pathname } = window.location;
+ if (/^https?:/i.test(origin) && pathname.startsWith("/storyforge")) {
+ return `${origin}/storyforge`;
+ }
+ if ((hostname === "127.0.0.1" || hostname === "localhost") && port && port !== "8081") {
+ return "http://127.0.0.1:8081";
+ }
+ return "http://127.0.0.1:8081";
+}
+
+const DEFAULT_BACKEND_URL = detectDefaultBackendUrl();
const navButtons = document.querySelectorAll("[data-screen-target]");
const screens = Array.from(document.querySelectorAll("[data-screen]"));