Merge branch 'codex/editor-asset-library' of https://git.genarrative.world/git/GenarrativeAI/Genarrative into codex/editor-asset-library
This commit is contained in:
@@ -16,7 +16,7 @@ RUN apt-get update && \
|
||||
|
||||
COPY --from=rust-builder /tmp/api-server /usr/local/bin/api-server
|
||||
|
||||
RUN mkdir -p /var/lib/genarrative/auth /var/lib/genarrative/tracking-outbox && \
|
||||
RUN mkdir -p /var/lib/genarrative/auth /var/lib/genarrative/tracking-outbox /var/lib/genarrative/wallet-refund-outbox && \
|
||||
chown -R genarrative:genarrative /srv/genarrative /var/lib/genarrative
|
||||
|
||||
USER genarrative
|
||||
|
||||
@@ -25,6 +25,11 @@ GENARRATIVE_TRACKING_OUTBOX_DIR=/var/lib/genarrative/tracking-outbox
|
||||
GENARRATIVE_TRACKING_OUTBOX_BATCH_SIZE=500
|
||||
GENARRATIVE_TRACKING_OUTBOX_FLUSH_INTERVAL_MS=1000
|
||||
GENARRATIVE_TRACKING_OUTBOX_MAX_BYTES=268435456
|
||||
GENARRATIVE_WALLET_REFUND_OUTBOX_ENABLED=true
|
||||
GENARRATIVE_WALLET_REFUND_OUTBOX_DIR=/var/lib/genarrative/wallet-refund-outbox
|
||||
GENARRATIVE_WALLET_REFUND_OUTBOX_BATCH_SIZE=100
|
||||
GENARRATIVE_WALLET_REFUND_OUTBOX_FLUSH_INTERVAL_MS=1000
|
||||
GENARRATIVE_WALLET_REFUND_OUTBOX_MAX_BYTES=67108864
|
||||
|
||||
GENARRATIVE_OTEL_ENABLED=true
|
||||
OTEL_SERVICE_NAME=genarrative-api
|
||||
|
||||
Vendored
+5
@@ -25,6 +25,11 @@ GENARRATIVE_TRACKING_OUTBOX_DIR=/var/lib/genarrative/tracking-outbox
|
||||
GENARRATIVE_TRACKING_OUTBOX_BATCH_SIZE=500
|
||||
GENARRATIVE_TRACKING_OUTBOX_FLUSH_INTERVAL_MS=1000
|
||||
GENARRATIVE_TRACKING_OUTBOX_MAX_BYTES=268435456
|
||||
GENARRATIVE_WALLET_REFUND_OUTBOX_ENABLED=true
|
||||
GENARRATIVE_WALLET_REFUND_OUTBOX_DIR=/var/lib/genarrative/wallet-refund-outbox
|
||||
GENARRATIVE_WALLET_REFUND_OUTBOX_BATCH_SIZE=100
|
||||
GENARRATIVE_WALLET_REFUND_OUTBOX_FLUSH_INTERVAL_MS=1000
|
||||
GENARRATIVE_WALLET_REFUND_OUTBOX_MAX_BYTES=67108864
|
||||
GENARRATIVE_OTEL_ENABLED=true
|
||||
OTEL_SERVICE_NAME=genarrative-api
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318
|
||||
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
# 复制到 /etc/genarrative/health-patrol.env 后按生产环境调整。
|
||||
# 默认不启用 Pingora shadow 巡检;只有同时配置 base URL 与 probe token 才会检查。
|
||||
|
||||
GENARRATIVE_HEALTH_PATROL_API_BASE_URL=http://127.0.0.1:8082
|
||||
GENARRATIVE_HEALTH_PATROL_SPACETIME_BASE_URL=http://127.0.0.1:3101
|
||||
GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL=http://127.0.0.1
|
||||
# 默认公网入口仍按 Nginx 巡检;Pingora 直连切换后改为 pingora-direct。
|
||||
GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=nginx
|
||||
# 本机打 127.0.0.1 时可填正式域名,确保命中正确 vhost / 证书语义。
|
||||
GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST=
|
||||
GENARRATIVE_HEALTH_PATROL_TIMEOUT_MS=5000
|
||||
GENARRATIVE_HEALTH_PATROL_SLOW_MS=3000
|
||||
GENARRATIVE_HEALTH_PATROL_FAIL_ON_WARNING=false
|
||||
GENARRATIVE_HEALTH_PATROL_SKIP_JOURNAL=false
|
||||
GENARRATIVE_HEALTH_PATROL_WEBHOOK_URL=
|
||||
|
||||
# Pingora 影子网关巡检;probe token 应与 /etc/genarrative/pingora-gateway.env 保持一致。
|
||||
GENARRATIVE_HEALTH_PATROL_PINGORA_BASE_URL=
|
||||
GENARRATIVE_HEALTH_PATROL_PINGORA_PROBE_TOKEN=
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# 复制到本机临时 shell 或 CI secret 环境后运行:
|
||||
# npm run check:pingora-canary-live
|
||||
# 该检查只读访问已经人工启用的 Nginx -> Pingora canary,不会修改 Nginx。
|
||||
|
||||
# 例如 http://127.0.0.1 或 https://genarrative.world。
|
||||
GENARRATIVE_PINGORA_CANARY_BASE_URL=http://127.0.0.1
|
||||
|
||||
# 默认前缀必须和 deploy/nginx/snippets/genarrative-pingora-canary.conf 保持一致。
|
||||
GENARRATIVE_PINGORA_CANARY_PREFIX=/__genarrative_pingora_canary
|
||||
|
||||
# 可选:真实路径 canary 使用 deploy/nginx/snippets/genarrative-pingora-realpath-canary.conf。
|
||||
# 启用后运行:
|
||||
# GENARRATIVE_PINGORA_CANARY_MODE=realpath \
|
||||
# GENARRATIVE_PINGORA_CANARY_BASE_URL=${GENARRATIVE_PINGORA_REALPATH_CANARY_BASE_URL} \
|
||||
# npm run check:pingora-canary-live
|
||||
# 或使用 release readiness:
|
||||
# npm run check:pingora-release-readiness -- --require-realpath-live --realpath-live-base-url http://127.0.0.1:18083 --realpath-live-host <域名>
|
||||
GENARRATIVE_PINGORA_CANARY_MODE=prefix
|
||||
GENARRATIVE_PINGORA_REALPATH_CANARY_BASE_URL=http://127.0.0.1:18083
|
||||
GENARRATIVE_PINGORA_REALPATH_CANARY_HOST=
|
||||
|
||||
# 本机直连 127.0.0.1 时必须设置正式域名,避免只命中 Nginx 默认 vhost;release readiness --require-live 必须提供。只填域名或 host:port,不要填 https:// URL。
|
||||
GENARRATIVE_PINGORA_CANARY_HOST=
|
||||
|
||||
GENARRATIVE_PINGORA_CANARY_TIMEOUT_MS=5000
|
||||
|
||||
# live smoke 后必须继续按 request_id 对照 Nginx handoff access log 与 Pingora access log。
|
||||
GENARRATIVE_PINGORA_CANARY_NGINX_ACCESS_LOG=/var/log/nginx/genarrative.access.log
|
||||
GENARRATIVE_PINGORA_CANARY_PINGORA_ACCESS_LOG=/var/log/genarrative/pingora-gateway.access.log
|
||||
GENARRATIVE_PINGORA_CANARY_ACCESS_LOG_SINCE_LINES=2000
|
||||
|
||||
# 真实路径 canary 使用独立 Nginx access log,避免和生产真实用户请求混在一起。
|
||||
GENARRATIVE_PINGORA_REALPATH_CANARY_NGINX_ACCESS_LOG=/var/log/nginx/genarrative-pingora-realpath-canary.access.log
|
||||
GENARRATIVE_PINGORA_REALPATH_CANARY_PINGORA_ACCESS_LOG=/var/log/genarrative/pingora-gateway.access.log
|
||||
GENARRATIVE_PINGORA_REALPATH_CANARY_ACCESS_LOG_SINCE_LINES=2000
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
# 复制到本机临时 shell 或 CI secret 环境后运行:
|
||||
# npm run check:pingora-direct-live
|
||||
# 该检查只读访问已经显式配置 TLS_LISTEN / HTTP_REDIRECT_LISTEN 的 pingora-gateway。
|
||||
# 切换窗口建议先运行:
|
||||
# npm run check:pingora-direct-preflight -- --env-file /etc/genarrative/pingora-gateway.env --require-live-env --systemd-cat --check-cert-readable --check-service-env-file --check-service-user-cert-readable --check-service-binary-executable --check-ports-free
|
||||
# release readiness --require-direct 必须确认 TLS/HTTP redirect 端口已释放,复核 health patrol env 已切到 direct 模式,并提供 Pingora access log 路径用于 request_id 落盘校验。
|
||||
# 直连启用前:
|
||||
# /opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh --no-status
|
||||
# /opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh --apply --preflight-env-file /etc/genarrative/pingora-gateway.env --preflight-check-cert-readable --preflight-check-service-env-file --preflight-check-service-user-cert-readable --preflight-check-service-binary-executable --preflight-check-ports-free --direct-https-base-url https://127.0.0.1 --direct-http-base-url http://127.0.0.1 --direct-host <域名> --direct-redirect-host <域名或host:port> --direct-spacetime-database <库名> --direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log
|
||||
# 启用脚本会先执行 current release 自审,确认 pingora-gateway 可执行且 systemd ExecStart 指向 current release;自审失败时不会安装 direct-entry drop-in。
|
||||
# 自审和 preflight 都通过后,启用脚本才会安装 drop-in、重启 Pingora、执行 direct live smoke,并强制 WSS subscribe 返回 101,同时用生成的 request_id 反查 Pingora access log。
|
||||
# 直连接管后同步更新 /etc/genarrative/health-patrol.env:
|
||||
# GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=pingora-direct
|
||||
# GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL=https://127.0.0.1
|
||||
# GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST=<域名>
|
||||
# 然后用 node -- /opt/genarrative/current/scripts/check-production-health-patrol-env.mjs 复核。
|
||||
# 直连验证失败时:
|
||||
# /opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh --apply --reload-nginx --nginx-smoke-url http://127.0.0.1/healthz --nginx-smoke-host <域名>
|
||||
# 回退脚本会先执行 nginx -t,reload 后确认 nginx.service 仍为 active,并用 smoke URL 证明 Nginx 入口可访问。
|
||||
# 回退后把 GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE 改回 nginx。
|
||||
|
||||
# 例如 https://127.0.0.1 或 https://genarrative.world。
|
||||
GENARRATIVE_PINGORA_DIRECT_HTTPS_BASE_URL=https://127.0.0.1
|
||||
|
||||
# 单独 direct live 可选;release readiness --require-direct 必须提供,用于校验 HTTP 入口 301 到 HTTPS,并确认 ACME challenge 没被重定向吞掉。
|
||||
GENARRATIVE_PINGORA_DIRECT_HTTP_BASE_URL=http://127.0.0.1
|
||||
|
||||
# 本机直连 127.0.0.1 时必须设置正式域名;direct live 会同时用作 Host header 和 TLS SNI。release readiness --require-direct 必须提供。只填域名或 host:port,不要填 https:// URL。
|
||||
GENARRATIVE_PINGORA_DIRECT_HOST=
|
||||
|
||||
# 单独 direct live 可选;release readiness --require-direct 必须提供,用于显式校验 HTTP redirect Location 的 host。只填域名或 host:port。
|
||||
GENARRATIVE_PINGORA_DIRECT_REDIRECT_HOST=
|
||||
|
||||
# 可选:高端口 rehearsal 访问 127.0.0.1:18443、但期望 Location 指向正式域名默认 HTTPS 入口时设置;只能填 https:// base URL,不要带路径、查询或片段。
|
||||
GENARRATIVE_PINGORA_DIRECT_REDIRECT_BASE_URL=
|
||||
|
||||
# 可选:配置后会额外检查 /__genarrative_pingora/healthz 内部探针。
|
||||
GENARRATIVE_PINGORA_DIRECT_PROBE_TOKEN=
|
||||
|
||||
# direct live 默认会对 wss://<HTTPS>/v1/database/<database>/subscribe 做 SpacetimeDB 握手检查;release readiness --require-direct 必须按目标环境显式设置。
|
||||
GENARRATIVE_PINGORA_DIRECT_SPACETIME_DATABASE=genarrative-prod
|
||||
|
||||
# release readiness --require-direct 必须显式提供;direct live 会用每个请求生成的 X-Request-Id 反查 Pingora access log,证明直连流量真实进入 Pingora。
|
||||
GENARRATIVE_PINGORA_DIRECT_PINGORA_ACCESS_LOG=/var/log/genarrative/pingora-gateway.access.log
|
||||
GENARRATIVE_PINGORA_DIRECT_ACCESS_LOG_SINCE_LINES=2000
|
||||
|
||||
# direct live 单独运行时可设 true,要求 WSS subscribe 必须返回 101;release readiness --require-direct 会自动强制该口径。
|
||||
GENARRATIVE_PINGORA_DIRECT_REQUIRE_WSS_UPGRADE=true
|
||||
|
||||
# 只允许单独 direct live 临时排障时跳过 WSS;release readiness --require-direct 会直接拒绝,正式切流必须保持 false。
|
||||
GENARRATIVE_PINGORA_DIRECT_SKIP_WSS=false
|
||||
|
||||
# 仅本机自签证书 smoke 使用;生产必须保持 false。
|
||||
GENARRATIVE_PINGORA_DIRECT_INSECURE_TLS=false
|
||||
|
||||
GENARRATIVE_PINGORA_DIRECT_TIMEOUT_MS=5000
|
||||
|
||||
# release readiness --require-direct 会先跑 direct-entry preflight;提供 env 文件后会强制检查 TLS/redirect/cert/key 配置。
|
||||
GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_ENV_FILE=/etc/genarrative/pingora-gateway.env
|
||||
GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_SYSTEMD_CAT=true
|
||||
GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_CERT_READABLE=true
|
||||
GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_ENV_FILE=true
|
||||
GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_USER_CERT_READABLE=true
|
||||
GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_BINARY_EXECUTABLE=true
|
||||
GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_PORTS_FREE=true
|
||||
|
||||
# release readiness --require-direct 必须复核健康巡检 env,避免切直连后巡检仍按 Nginx 模式误报。
|
||||
GENARRATIVE_PINGORA_DIRECT_HEALTH_PATROL_ENV_FILE=/etc/genarrative/health-patrol.env
|
||||
@@ -0,0 +1,10 @@
|
||||
/var/log/genarrative/pingora-gateway.access.log {
|
||||
daily
|
||||
rotate 14
|
||||
missingok
|
||||
notifempty
|
||||
compress
|
||||
delaycompress
|
||||
copytruncate
|
||||
create 0640 genarrative genarrative
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,64 @@
|
||||
# Pingora canary handoff snippet.
|
||||
# 中文注释:本文件只提供人工 include 后的 Nginx -> Pingora 试跑入口,不默认承接公网流量。
|
||||
# 启用前复制到目标机并替换 __GENARRATIVE_PINGORA_PROBE_TOKEN__,确认只允许可信来源访问。
|
||||
|
||||
location = /__genarrative_pingora_canary/healthz {
|
||||
allow 127.0.0.1;
|
||||
allow ::1;
|
||||
deny all;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
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_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
proxy_set_header X-Genarrative-Pingora-Probe "__GENARRATIVE_PINGORA_PROBE_TOKEN__";
|
||||
add_header X-Genarrative-Nginx-Handoff pingora-canary always;
|
||||
proxy_pass http://127.0.0.1:18081/__genarrative_pingora/healthz;
|
||||
}
|
||||
|
||||
location ~ ^/__genarrative_pingora_canary/v1/database/[^/]+/subscribe$ {
|
||||
allow 127.0.0.1;
|
||||
allow ::1;
|
||||
deny all;
|
||||
|
||||
rewrite ^/__genarrative_pingora_canary(/.*)$ $1 break;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
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_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
proxy_read_timeout 3600s;
|
||||
add_header X-Genarrative-Nginx-Handoff pingora-canary always;
|
||||
proxy_pass http://127.0.0.1:18081;
|
||||
}
|
||||
|
||||
location /__genarrative_pingora_canary/ {
|
||||
allow 127.0.0.1;
|
||||
allow ::1;
|
||||
deny all;
|
||||
|
||||
rewrite ^/__genarrative_pingora_canary(/.*)$ $1 break;
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_set_header Connection "";
|
||||
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_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
proxy_set_header X-Genarrative-Pingora-Canary "nginx-prefixed";
|
||||
add_header X-Accel-Buffering no always;
|
||||
add_header X-Genarrative-Nginx-Handoff pingora-canary always;
|
||||
proxy_pass http://127.0.0.1:18081;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
# Pingora real-path canary handoff server.
|
||||
# 中文注释:本文件只提供人工 include 后的本机真实路径 canary,不默认承接公网流量。
|
||||
# 启用前复制到目标机并替换 __GENARRATIVE_PINGORA_PROBE_TOKEN__,只能在 nginx http 上下文 include。
|
||||
|
||||
server {
|
||||
listen 127.0.0.1:18083;
|
||||
server_name genarrative-pingora-realpath-canary.local;
|
||||
access_log /var/log/nginx/genarrative-pingora-realpath-canary.access.log genarrative_upstream;
|
||||
error_log /var/log/nginx/genarrative-pingora-realpath-canary.error.log warn;
|
||||
|
||||
allow 127.0.0.1;
|
||||
allow ::1;
|
||||
deny all;
|
||||
|
||||
location = /__genarrative_pingora_realpath_canary/healthz {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
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_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
proxy_set_header X-Genarrative-Pingora-Probe "__GENARRATIVE_PINGORA_PROBE_TOKEN__";
|
||||
add_header X-Genarrative-Nginx-Handoff pingora-realpath-canary always;
|
||||
proxy_pass http://127.0.0.1:18081/__genarrative_pingora/healthz;
|
||||
}
|
||||
|
||||
location = /api/creation-entry/config {
|
||||
default_type application/json;
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_set_header Connection "";
|
||||
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_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
proxy_set_header X-Genarrative-Pingora-Canary "nginx-realpath";
|
||||
add_header X-Accel-Buffering no always;
|
||||
add_header X-Genarrative-Nginx-Handoff pingora-realpath-canary always;
|
||||
proxy_pass http://127.0.0.1:18081;
|
||||
}
|
||||
|
||||
location ~ ^/v1/database/[^/]+/subscribe$ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
proxy_read_timeout 3600s;
|
||||
add_header X-Genarrative-Nginx-Handoff pingora-realpath-canary always;
|
||||
proxy_pass http://127.0.0.1:18081;
|
||||
}
|
||||
|
||||
location ^~ /v1/identity {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
add_header X-Genarrative-Nginx-Handoff pingora-realpath-canary always;
|
||||
proxy_pass http://127.0.0.1:18081;
|
||||
}
|
||||
|
||||
location = /assets/app.js {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
add_header X-Genarrative-Nginx-Handoff pingora-realpath-canary always;
|
||||
proxy_pass http://127.0.0.1:18081;
|
||||
}
|
||||
|
||||
location = /generated-pingora-canary-smoke {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
add_header X-Genarrative-Nginx-Handoff pingora-realpath-canary always;
|
||||
proxy_pass http://127.0.0.1:18081;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
{
|
||||
"version": 1,
|
||||
"description": "Pingora shadow gateway routes that must stay aligned with the current Genarrative Nginx production/development templates before any public cutover.",
|
||||
"routes": [
|
||||
{
|
||||
"id": "acme_challenge",
|
||||
"samplePath": "/.well-known/acme-challenge/token",
|
||||
"expect": {
|
||||
"kind": "static",
|
||||
"root": "acme",
|
||||
"mode": "exact"
|
||||
},
|
||||
"nginx": {
|
||||
"production": [
|
||||
"location /.well-known/acme-challenge/",
|
||||
"root /var/www/html;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/.well-known/acme-challenge/*`"]
|
||||
},
|
||||
{
|
||||
"id": "shadow_probe",
|
||||
"samplePath": "/__genarrative_pingora/healthz",
|
||||
"expect": {
|
||||
"kind": "shadow_probe"
|
||||
},
|
||||
"docs": ["`/__genarrative_pingora/healthz`"]
|
||||
},
|
||||
{
|
||||
"id": "admin_redirect",
|
||||
"samplePath": "/admin",
|
||||
"expect": {
|
||||
"kind": "redirect_permanent",
|
||||
"location": "/admin/"
|
||||
},
|
||||
"nginx": {
|
||||
"production": ["location = /admin", "return 301 /admin/;"],
|
||||
"development": ["location = /admin", "return 301 /admin/;"]
|
||||
},
|
||||
"docs": ["`/admin`", "301 到 `/admin/`"]
|
||||
},
|
||||
{
|
||||
"id": "admin_api_proxy",
|
||||
"samplePath": "/admin/api/users",
|
||||
"expect": {
|
||||
"kind": "proxy",
|
||||
"target": "api",
|
||||
"bodyLimit": null,
|
||||
"protectionClass": "admin_api"
|
||||
},
|
||||
"nginx": {
|
||||
"production": [
|
||||
"location ^~ /admin/api/",
|
||||
"proxy_pass http://genarrative_api/admin/api/;",
|
||||
"limit_conn genarrative_api_conn 64;",
|
||||
"limit_req zone=genarrative_admin_rps burst=16 nodelay;"
|
||||
],
|
||||
"development": [
|
||||
"location ^~ /admin/api/",
|
||||
"proxy_pass http://genarrative_api/admin/api/;",
|
||||
"limit_conn genarrative_api_conn 64;",
|
||||
"limit_req zone=genarrative_admin_rps burst=16 nodelay;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/admin/api/*`"]
|
||||
},
|
||||
{
|
||||
"id": "admin_assets",
|
||||
"samplePath": "/admin/assets/index.js",
|
||||
"expect": {
|
||||
"kind": "static",
|
||||
"root": "web",
|
||||
"mode": "exact"
|
||||
},
|
||||
"nginx": {
|
||||
"production": ["location ^~ /admin/assets/", "try_files $uri =404;"],
|
||||
"development": ["location ^~ /admin/assets/", "try_files $uri =404;"]
|
||||
},
|
||||
"docs": ["`/admin/assets/*`"]
|
||||
},
|
||||
{
|
||||
"id": "admin_spa_fallback",
|
||||
"samplePath": "/admin/settings",
|
||||
"expect": {
|
||||
"kind": "static",
|
||||
"root": "web",
|
||||
"mode": "spa_fallback"
|
||||
},
|
||||
"nginx": {
|
||||
"production": [
|
||||
"location ^~ /admin/",
|
||||
"try_files $uri $uri/ /admin/index.html;"
|
||||
],
|
||||
"development": [
|
||||
"location ^~ /admin/",
|
||||
"try_files $uri $uri/ /admin/index.html;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/admin/*`"]
|
||||
},
|
||||
{
|
||||
"id": "web_assets",
|
||||
"samplePath": "/assets/app.js",
|
||||
"expect": {
|
||||
"kind": "static",
|
||||
"root": "web",
|
||||
"mode": "exact"
|
||||
},
|
||||
"nginx": {
|
||||
"production": ["location ^~ /assets/", "try_files $uri =404;"],
|
||||
"development": ["location ^~ /assets/", "try_files $uri =404;"]
|
||||
},
|
||||
"docs": ["`/assets/*`"]
|
||||
},
|
||||
{
|
||||
"id": "puzzle_gallery_list",
|
||||
"samplePath": "/api/runtime/puzzle/gallery",
|
||||
"expect": {
|
||||
"kind": "proxy",
|
||||
"target": "api",
|
||||
"bodyLimit": null,
|
||||
"protectionClass": "gallery_list"
|
||||
},
|
||||
"nginx": {
|
||||
"production": [
|
||||
"location = /api/runtime/puzzle/gallery",
|
||||
"limit_conn genarrative_api_conn 320;",
|
||||
"limit_req zone=genarrative_gallery_rps burst=4096 nodelay;",
|
||||
"add_header X-Accel-Buffering no always;"
|
||||
],
|
||||
"development": [
|
||||
"location = /api/runtime/puzzle/gallery",
|
||||
"limit_conn genarrative_api_conn 320;",
|
||||
"limit_req zone=genarrative_gallery_rps burst=4096 nodelay;",
|
||||
"add_header X-Accel-Buffering no always;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/api/runtime/puzzle/gallery`"]
|
||||
},
|
||||
{
|
||||
"id": "custom_world_gallery_list",
|
||||
"samplePath": "/api/runtime/custom-world-gallery",
|
||||
"expect": {
|
||||
"kind": "proxy",
|
||||
"target": "api",
|
||||
"bodyLimit": null,
|
||||
"protectionClass": "gallery_list"
|
||||
},
|
||||
"nginx": {
|
||||
"production": [
|
||||
"location = /api/runtime/custom-world-gallery",
|
||||
"limit_conn genarrative_api_conn 320;",
|
||||
"limit_req zone=genarrative_gallery_rps burst=4096 nodelay;",
|
||||
"add_header X-Accel-Buffering no always;"
|
||||
],
|
||||
"development": [
|
||||
"location = /api/runtime/custom-world-gallery",
|
||||
"limit_conn genarrative_api_conn 320;",
|
||||
"limit_req zone=genarrative_gallery_rps burst=4096 nodelay;",
|
||||
"add_header X-Accel-Buffering no always;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/api/runtime/custom-world-gallery`"]
|
||||
},
|
||||
{
|
||||
"id": "puzzle_gallery_detail",
|
||||
"samplePath": "/api/runtime/puzzle/gallery/work-a",
|
||||
"expect": {
|
||||
"kind": "proxy",
|
||||
"target": "api",
|
||||
"bodyLimit": null,
|
||||
"protectionClass": "gallery_detail"
|
||||
},
|
||||
"nginx": {
|
||||
"production": [
|
||||
"location ~ ^/api/runtime/puzzle/gallery/[^/]+$",
|
||||
"limit_conn genarrative_api_conn 32;",
|
||||
"limit_req zone=genarrative_api_rps burst=32 nodelay;",
|
||||
"add_header X-Accel-Buffering no always;"
|
||||
],
|
||||
"development": [
|
||||
"location ~ ^/api/runtime/puzzle/gallery/[^/]+$",
|
||||
"limit_conn genarrative_api_conn 32;",
|
||||
"limit_req zone=genarrative_api_rps burst=32 nodelay;",
|
||||
"add_header X-Accel-Buffering no always;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/api/runtime/puzzle/gallery/{id}`"]
|
||||
},
|
||||
{
|
||||
"id": "custom_world_gallery_detail",
|
||||
"samplePath": "/api/runtime/custom-world-gallery/profile-a/owner-b",
|
||||
"expect": {
|
||||
"kind": "proxy",
|
||||
"target": "api",
|
||||
"bodyLimit": null,
|
||||
"protectionClass": "gallery_detail"
|
||||
},
|
||||
"nginx": {
|
||||
"production": [
|
||||
"location ~ ^/api/runtime/custom-world-gallery/[^/]+/[^/]+$",
|
||||
"limit_conn genarrative_api_conn 32;",
|
||||
"limit_req zone=genarrative_api_rps burst=32 nodelay;",
|
||||
"add_header X-Accel-Buffering no always;"
|
||||
],
|
||||
"development": [
|
||||
"location ~ ^/api/runtime/custom-world-gallery/[^/]+/[^/]+$",
|
||||
"limit_conn genarrative_api_conn 32;",
|
||||
"limit_req zone=genarrative_api_rps burst=32 nodelay;",
|
||||
"add_header X-Accel-Buffering no always;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/api/runtime/custom-world-gallery/{profile}/{owner}`"]
|
||||
},
|
||||
{
|
||||
"id": "generic_api_proxy",
|
||||
"samplePath": "/api/creation-entry/config",
|
||||
"expect": {
|
||||
"kind": "proxy",
|
||||
"target": "api",
|
||||
"bodyLimit": "default",
|
||||
"protectionClass": "api"
|
||||
},
|
||||
"nginx": {
|
||||
"production": [
|
||||
"location ~ ^/api(?:/|$)",
|
||||
"client_max_body_size 64m;",
|
||||
"limit_conn genarrative_api_conn 64;",
|
||||
"limit_req zone=genarrative_api_rps burst=64 nodelay;",
|
||||
"add_header X-Accel-Buffering no always;"
|
||||
],
|
||||
"development": [
|
||||
"location ~ ^/api(?:/|$)",
|
||||
"client_max_body_size 64m;",
|
||||
"limit_conn genarrative_api_conn 64;",
|
||||
"limit_req zone=genarrative_api_rps burst=64 nodelay;",
|
||||
"add_header X-Accel-Buffering no always;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/api`、`/api/*`"]
|
||||
},
|
||||
{
|
||||
"id": "spacetime_subscribe",
|
||||
"samplePath": "/v1/database/genarrative/subscribe",
|
||||
"expect": {
|
||||
"kind": "proxy",
|
||||
"target": "spacetime",
|
||||
"bodyLimit": null,
|
||||
"protectionClass": "spacetime"
|
||||
},
|
||||
"nginx": {
|
||||
"production": [
|
||||
"location ~ ^/v1/database/[^/]+/subscribe$",
|
||||
"proxy_pass http://127.0.0.1:3101;",
|
||||
"proxy_set_header Upgrade $http_upgrade;",
|
||||
"proxy_set_header Connection \"Upgrade\";",
|
||||
"proxy_read_timeout 3600s;"
|
||||
],
|
||||
"development": [
|
||||
"location ~ ^/v1/database/[^/]+/subscribe$",
|
||||
"proxy_pass http://127.0.0.1:3101;",
|
||||
"proxy_set_header Upgrade $http_upgrade;",
|
||||
"proxy_set_header Connection \"Upgrade\";",
|
||||
"proxy_read_timeout 3600s;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/v1/database/{db}/subscribe`"]
|
||||
},
|
||||
{
|
||||
"id": "spacetime_identity",
|
||||
"samplePath": "/v1/identity",
|
||||
"expect": {
|
||||
"kind": "proxy",
|
||||
"target": "spacetime",
|
||||
"bodyLimit": null,
|
||||
"protectionClass": "spacetime"
|
||||
},
|
||||
"nginx": {
|
||||
"production": [
|
||||
"location ^~ /v1/identity",
|
||||
"proxy_pass http://127.0.0.1:3101;",
|
||||
"proxy_set_header Upgrade $http_upgrade;",
|
||||
"proxy_set_header Connection \"Upgrade\";"
|
||||
],
|
||||
"development": [
|
||||
"location ^~ /v1/identity",
|
||||
"proxy_pass http://127.0.0.1:3101;",
|
||||
"proxy_set_header Upgrade $http_upgrade;",
|
||||
"proxy_set_header Connection \"Upgrade\";"
|
||||
]
|
||||
},
|
||||
"docs": ["`/v1/identity*`"]
|
||||
},
|
||||
{
|
||||
"id": "v1_forbidden",
|
||||
"samplePath": "/v1/ping",
|
||||
"expect": {
|
||||
"kind": "not_found"
|
||||
},
|
||||
"nginx": {
|
||||
"production": ["location ^~ /v1/", "return 404;"],
|
||||
"development": ["location ^~ /v1/", "return 404;"]
|
||||
},
|
||||
"docs": ["`/v1/*`"]
|
||||
},
|
||||
{
|
||||
"id": "healthz_forbidden",
|
||||
"samplePath": "/healthz",
|
||||
"expect": {
|
||||
"kind": "not_found"
|
||||
},
|
||||
"nginx": {
|
||||
"production": [
|
||||
"location ~ ^/(generated-|healthz|readyz)",
|
||||
"return 404;"
|
||||
],
|
||||
"development": [
|
||||
"location ~ ^/(generated-|healthz|readyz)",
|
||||
"return 404;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/healthz*`"]
|
||||
},
|
||||
{
|
||||
"id": "readyz_forbidden",
|
||||
"samplePath": "/readyz",
|
||||
"expect": {
|
||||
"kind": "not_found"
|
||||
},
|
||||
"nginx": {
|
||||
"production": [
|
||||
"location ~ ^/(generated-|healthz|readyz)",
|
||||
"return 404;"
|
||||
],
|
||||
"development": [
|
||||
"location ~ ^/(generated-|healthz|readyz)",
|
||||
"return 404;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/readyz*`"]
|
||||
},
|
||||
{
|
||||
"id": "generated_assets_forbidden",
|
||||
"samplePath": "/generated-puzzle-assets/foo.webp",
|
||||
"expect": {
|
||||
"kind": "not_found"
|
||||
},
|
||||
"nginx": {
|
||||
"production": [
|
||||
"location ~ ^/(generated-|healthz|readyz)",
|
||||
"return 404;"
|
||||
],
|
||||
"development": [
|
||||
"location ~ ^/(generated-|healthz|readyz)",
|
||||
"return 404;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/generated-*`"]
|
||||
},
|
||||
{
|
||||
"id": "web_spa_fallback",
|
||||
"samplePath": "/some/spa/path",
|
||||
"expect": {
|
||||
"kind": "static",
|
||||
"root": "web",
|
||||
"mode": "spa_fallback"
|
||||
},
|
||||
"nginx": {
|
||||
"production": ["location /", "try_files $uri $uri/ /index.html;"],
|
||||
"development": ["location /", "try_files $uri $uri/ /index.html;"]
|
||||
},
|
||||
"docs": ["其它路径", "失败回退 `/index.html`"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
# Pingora 独立网关试点配置示例。
|
||||
# 当前只用于本机影子验证,不绑定 80/443,也不替代生产 Nginx。
|
||||
# systemd 模板默认读取 /etc/genarrative/pingora-gateway.env。
|
||||
|
||||
GENARRATIVE_PINGORA_GATEWAY_LISTEN=127.0.0.1:18081
|
||||
# 可选:直连 HTTPS 入口。默认关闭;启用时必须同时配置 cert/key,并把 FORWARDED_PROTO 设为 https。
|
||||
# cert/key 必须允许 genarrative 用户读取;不要直接 chmod Let's Encrypt live/archive 原始目录。
|
||||
# 可先用随包 scripts/deploy/pingora-tls-cert-sync.mjs 把证书同步到 /etc/genarrative/pingora-tls/<host>/。
|
||||
# 80/443 必须先从 Nginx 或其它进程释放;高端口 loopback 演练可先使用 127.0.0.1:18443 / 127.0.0.1:18080。
|
||||
# 如果要绑定 443/80,必须另行人工启用 systemd direct-entry drop-in 授予 CAP_NET_BIND_SERVICE。
|
||||
# GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN=0.0.0.0:18443
|
||||
# GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE=/etc/genarrative/pingora-tls/genarrative.world/fullchain.pem
|
||||
# GENARRATIVE_PINGORA_GATEWAY_TLS_KEY_FILE=/etc/genarrative/pingora-tls/genarrative.world/privkey.pem
|
||||
# 可选:直连 HTTP 只做 301 到 HTTPS;启用时必须已经配置 TLS_LISTEN,ACME challenge 仍从 ACME_ROOT 静态读取。
|
||||
# GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN=0.0.0.0:18080
|
||||
# GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_TARGET_SCHEME=https
|
||||
GENARRATIVE_PINGORA_GATEWAY_API_UPSTREAM=127.0.0.1:8082
|
||||
GENARRATIVE_PINGORA_GATEWAY_SPACETIME_UPSTREAM=127.0.0.1:3101
|
||||
# 可选:同一公网 IP 上还承载 Gitea 时,按 Host 整站转发到 Gitea。
|
||||
# 命中这些 Host 的请求不走应用维护页、API body limit 或网关接流保护,避免影响 git clone / push。
|
||||
# 直连 443 如果要同时服务这些域名,当前单 listener TLS 配置必须使用覆盖全部 Host 的同一张证书。
|
||||
# GENARRATIVE_PINGORA_GATEWAY_GITEA_HOSTS=git.genarrative.world
|
||||
# GENARRATIVE_PINGORA_GATEWAY_GITEA_UPSTREAM=127.0.0.1:3000
|
||||
|
||||
GENARRATIVE_PINGORA_GATEWAY_WEB_ROOT=/srv/genarrative/web
|
||||
GENARRATIVE_PINGORA_GATEWAY_ACME_ROOT=/var/www/html
|
||||
GENARRATIVE_PINGORA_GATEWAY_MAINTENANCE_FILE=/var/lib/genarrative/maintenance/enabled
|
||||
|
||||
GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO=http
|
||||
GENARRATIVE_PINGORA_GATEWAY_MAX_API_BODY_BYTES=67108864
|
||||
# gzip 默认开启;等级和最小响应长度对齐 Nginx gzip_comp_level 5 / gzip_min_length 1024。
|
||||
# Pingora 正式化口径固定为 gzip-only;br / zstd 不进入当前网关,Brotli 继续由 Nginx / 前置代理承担。
|
||||
GENARRATIVE_PINGORA_GATEWAY_COMPRESSION_ALGORITHMS=gzip
|
||||
GENARRATIVE_PINGORA_GATEWAY_GZIP_ENABLED=true
|
||||
GENARRATIVE_PINGORA_GATEWAY_GZIP_LEVEL=5
|
||||
GENARRATIVE_PINGORA_GATEWAY_GZIP_MIN_LENGTH_BYTES=1024
|
||||
|
||||
# 静态响应缓存头:HTML / SPA fallback 保持 no-cache,Vite 指纹资源长缓存,其它静态资源保守 no-cache。
|
||||
# 网关会额外写入 ETag / Last-Modified 并支持 304 协商缓存;下面三项只控制 Cache-Control。
|
||||
GENARRATIVE_PINGORA_GATEWAY_HTML_CACHE_CONTROL=no-cache
|
||||
GENARRATIVE_PINGORA_GATEWAY_ASSET_CACHE_CONTROL=public, max-age=31536000, immutable
|
||||
GENARRATIVE_PINGORA_GATEWAY_STATIC_CACHE_CONTROL=no-cache
|
||||
|
||||
# 上游超时默认对齐当前 Nginx 模板:普通无显式长超时路由 60 秒,/api、公开列表/详情和 subscribe 为 3600 秒。
|
||||
GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_CONNECT_TIMEOUT_MS=3000
|
||||
GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_DEFAULT_READ_TIMEOUT_SECONDS=60
|
||||
GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_API_READ_TIMEOUT_SECONDS=3600
|
||||
GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_LONG_READ_TIMEOUT_SECONDS=3600
|
||||
GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_WRITE_TIMEOUT_SECONDS=3600
|
||||
|
||||
# 默认只信任 TCP 对端 IP;只有确认前置 Nginx / LB 会清洗 X-Forwarded-For 后才打开。
|
||||
GENARRATIVE_PINGORA_GATEWAY_TRUST_X_FORWARDED_FOR=false
|
||||
# 打开 TRUST_X_FORWARDED_FOR 前必须显式确认;否则网关会拒绝启动。
|
||||
GENARRATIVE_PINGORA_GATEWAY_TRUSTED_FRONT_PROXY_CONFIRMED=false
|
||||
|
||||
# 单进程内接流保护;默认值对齐当前 Nginx limit_conn / limit_req 口径。
|
||||
# INSTANCE_COUNT 默认 1;若横向运行多个 Pingora 实例且仍启用网关接流保护,必须先落地共享保护层并显式确认。
|
||||
GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT=1
|
||||
GENARRATIVE_PINGORA_GATEWAY_SHARED_PROTECTION_CONFIRMED=false
|
||||
GENARRATIVE_PINGORA_GATEWAY_PROTECTION_ENABLED=true
|
||||
GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_MAX_CONCURRENT=64
|
||||
GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_RATE_PER_SECOND=30
|
||||
GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_BURST=16
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_LIST_MAX_CONCURRENT=320
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_LIST_RATE_PER_SECOND=5000
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_LIST_BURST=4096
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_DETAIL_MAX_CONCURRENT=32
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_DETAIL_RATE_PER_SECOND=300
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_DETAIL_BURST=32
|
||||
GENARRATIVE_PINGORA_GATEWAY_API_MAX_CONCURRENT=64
|
||||
GENARRATIVE_PINGORA_GATEWAY_API_RATE_PER_SECOND=300
|
||||
GENARRATIVE_PINGORA_GATEWAY_API_BURST=64
|
||||
GENARRATIVE_PINGORA_GATEWAY_SPACETIME_MAX_CONCURRENT=256
|
||||
GENARRATIVE_PINGORA_GATEWAY_SPACETIME_RATE_PER_SECOND=1000
|
||||
GENARRATIVE_PINGORA_GATEWAY_SPACETIME_BURST=256
|
||||
# 仅供本机影子探针使用;为空时 /__genarrative_pingora/healthz 一律返回 404。
|
||||
GENARRATIVE_PINGORA_GATEWAY_PROBE_TOKEN=
|
||||
|
||||
GENARRATIVE_PINGORA_GATEWAY_LOG=info,pingora=info,pingora_gateway=info
|
||||
GENARRATIVE_PINGORA_GATEWAY_ACCESS_LOG_FILE=/var/log/genarrative/pingora-gateway.access.log
|
||||
GENARRATIVE_PINGORA_GATEWAY_OTEL_ENABLED=false
|
||||
@@ -9,7 +9,7 @@ User=root
|
||||
Group=root
|
||||
WorkingDirectory=/opt/genarrative/current
|
||||
EnvironmentFile=/etc/genarrative/api-server.env
|
||||
ExecStart=/usr/bin/node /opt/genarrative/current/scripts/database-backup-to-oss.mjs --env-file /etc/genarrative/api-server.env --stop-service spacetimedb.service --restart-service-after genarrative-api.service
|
||||
ExecStart=/usr/bin/node -- /opt/genarrative/current/scripts/database-backup-to-oss.mjs --env-file /etc/genarrative/api-server.env --stop-service spacetimedb.service --restart-service-after genarrative-api.service
|
||||
|
||||
# 备份需要停止 / 启动 spacetimedb.service,并读取 /stdb、写入 /var/lib/genarrative/database-backups。
|
||||
PrivateTmp=true
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Pingora 直连公网入口的 systemd drop-in 模板。
|
||||
# 默认 provision 只把本文件安装到 /etc/genarrative/pingora/ 作为人工启用材料;
|
||||
# 需要低端口 80/443 时,才复制到:
|
||||
# /etc/systemd/system/genarrative-pingora-gateway.service.d/direct-entry.conf
|
||||
#
|
||||
# 启用前必须同时在 /etc/genarrative/pingora-gateway.env 中显式配置:
|
||||
# GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN=0.0.0.0:443
|
||||
# GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN=0.0.0.0:80
|
||||
# GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE=...
|
||||
# GENARRATIVE_PINGORA_GATEWAY_TLS_KEY_FILE=...
|
||||
# 证书链和私钥必须允许 genarrative 用户读取;80/443 必须先从 Nginx 或其它进程释放。
|
||||
|
||||
[Service]
|
||||
# 仅授予非 root genarrative 用户绑定 80/443 所需的最小 capability。
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||
NoNewPrivileges=true
|
||||
@@ -0,0 +1,31 @@
|
||||
[Unit]
|
||||
Description=Genarrative Pingora Shadow Gateway
|
||||
After=network-online.target genarrative-api.service spacetimedb.service
|
||||
Wants=network-online.target
|
||||
Requires=genarrative-api.service spacetimedb.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=genarrative
|
||||
Group=genarrative
|
||||
WorkingDirectory=/opt/genarrative/current
|
||||
EnvironmentFile=/etc/genarrative/pingora-gateway.env
|
||||
ExecStartPre=/usr/bin/test -d /srv/genarrative/web
|
||||
ExecStartPre=/usr/bin/test -d /var/www/html
|
||||
ExecStart=/opt/genarrative/current/pingora-gateway
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
KillSignal=SIGINT
|
||||
TimeoutStopSec=60
|
||||
LimitNOFILE=65535
|
||||
TasksMax=2048
|
||||
|
||||
# 影子网关只监听本机高端口,公网入口继续由 Nginx 承接。
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=full
|
||||
ReadOnlyPaths=/srv/genarrative/web /var/www/html
|
||||
ReadWritePaths=/var/lib/genarrative /var/log/genarrative
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
|
||||
本地通过 SSH alias 管理多台服务器、查看硬件 / systemd / HTTP 健康状态并执行受控服务启停的 egui 桌面工具见 [【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md](./technical/【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md)。
|
||||
|
||||
生产部署切换到 systemd + Nginx + SpacetimeDB 自托管的总方案见 [PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md](./technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md),该文档也是当前生产 Jenkinsfile 的唯一入口。SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md);private 表迁移 JSON 导入导出、HTTP 413 分片导入和旧数据库迁移流水线经验见 [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./technical/SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md) 与 [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md);后台管理独立前端工程技术方案见 [ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md](./technical/ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md)。
|
||||
生产部署切换到 systemd + Nginx + SpacetimeDB 自托管的总方案见 [PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md](./technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md),该文档也是当前生产 Jenkinsfile 的唯一入口。Pingora 只作为独立二进制影子网关试点时,边界、路由口径与替换前验收见 [【开发运维】Pingora独立网关试点-2026-06-11.md](./technical/【开发运维】Pingora独立网关试点-2026-06-11.md)。SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md);private 表迁移 JSON 导入导出、HTTP 413 分片导入和旧数据库迁移流水线经验见 [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./technical/SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md) 与 [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md);后台管理独立前端工程技术方案见 [ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md](./technical/ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md)。
|
||||
|
||||
SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md)。
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -47,6 +47,7 @@
|
||||
- 承载中央画布工作区的视觉树:viewport / world DOM、图层渲染、生成占位框、选中图片浮动工具栏、空白和图片右键菜单、左下 dock、缩放菜单、背景设置面板、小地图和底部 AI 工具栏。
|
||||
- 继续通过 props 调用主视图状态机,不接管拖拽 / 平移 / 缩放、画布坐标换算、历史 undo / redo、上传、登录、生成提交、素材持久化和右键命令实现。
|
||||
- 保持 `canvasViewportRef` 由主视图传入,确保 pointer capture、drop 坐标、滚轮缩放和小地图拖拽仍使用同一套坐标源。
|
||||
- 图片图层渲染必须和音频、视频一样走统一资源读取解析;图层存在 `objectKey` 时优先用它换签,避免重进项目后私有上传 / 生成资源只剩旧路径而无法显示。
|
||||
|
||||
第三阶段以后,主视图仍是画布编排入口。继续拆分前应优先选择能形成稳定边界的深模块,避免把上传链路、DataTransfer、画布坐标和历史快照拆成互相回调的小碎片。
|
||||
|
||||
@@ -84,6 +85,7 @@
|
||||
- 承载图片画布工程持久化协调:项目加载、`projectId` 维护、未就绪资源队列、工程资源创建、资源创建后即时 layout 保存、450ms 自动保存和鉴权失败登录弹窗。
|
||||
- 该 hook 以“项目持久化协调器”整体抽出,避免把加载、保存和资源创建拆成多个小 hook 后打散 `projectIdRef`、`pendingProjectResourceLayersRef`、`isProjectReady` 和 `saveTimerRef` 的时序约束。
|
||||
- 主视图继续负责项目重命名 UI、素材库管理、上传流程和用户动作触发;新增图层仍通过 `appendCanvasLayersWithResources` 先写本地图层快照,再创建 project resource 并保存带真实 `resourceId` 的 layout。
|
||||
- 项目加载 hydrate 时必须从 project resource 回填 `objectKey`、`assetObjectId`、`sourceResourceId` 等资源元数据;layout 快照保持轻量,但重进项目后的画布图层仍要保留换签和后续编辑所需线索。
|
||||
|
||||
## 第九阶段模块
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -429,6 +429,13 @@ npm run check:server-rs-ddd
|
||||
- Rust 结构体:`DatabaseMigrationOperator`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/migration.rs`
|
||||
|
||||
### `external_api_key`
|
||||
|
||||
- Rust 结构体:`ExternalApiKey`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/external_api_key_storage.rs`
|
||||
- 说明:外部 OpenAPI 调用使用的账号级 API Key 凭据表,只保存 key prefix、SHA-256 hash、作用域、撤销状态和使用时间;明文 Key 只在 `/api/profile/api-keys` 创建接口返回一次,不进入 SpacetimeDB。
|
||||
- 索引:`by_external_api_key_owner_user_id` 用于登录态 API Key 列表;`key_hash` 唯一索引用于外部 API 鉴权。
|
||||
|
||||
### `editor_project`
|
||||
|
||||
- Rust 结构体:`EditorProject`
|
||||
@@ -447,7 +454,7 @@ npm run check:server-rs-ddd
|
||||
|
||||
- Rust 结构体:`EditorProjectResource`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
|
||||
- 说明:图片画布工程资源元数据表,保存已经放入某个 project 画布的上传 / 生成图片资源快照、OSS 引用、尺寸、来源类型、prompt、provider、task 和源资源关系;账号级素材删除不级联删除该表,避免历史画布丢图。
|
||||
- 说明:图片画布工程资源元数据表,保存已经放入某个 project 画布的上传 / 生成图片资源快照、OSS 引用、尺寸、来源类型、prompt、provider、task、源资源关系、`asset_kind` 和 `generation_inputs_json`。`asset_kind` 标记角色、图标、UI 设计图、视频、音频等素材类别;`generation_inputs_json` 保存用户可见生成输入快照,供图片信息页刷新后恢复。图片 / 图标 / UI 提取等生成 BFF 在请求携带 `project_id` 时负责创建该表记录并把 resource 快照返回前端;前端只保存布局引用,不能把同一生成结果再次作为正式业务真相写入。账号级素材删除不级联删除该表,避免历史画布丢图。`editor_canvas.layers_json` 只保存图层几何、层级、分组、资源引用和生成器对象;新写入不再把素材生成输入快照作为图层布局真相保存,旧布局字段只作为兼容兜底读取。
|
||||
- 索引:`by_editor_project_resource_project_id`、`by_editor_project_resource_owner_user_id`。
|
||||
|
||||
### `editor_asset_folder`
|
||||
@@ -461,7 +468,7 @@ npm run check:server-rs-ddd
|
||||
|
||||
- Rust 结构体:`EditorAsset`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
|
||||
- 说明:图片画布账号级素材表,保存用户上传 / 生成素材的名称、文件夹、图片读取地址、OSS 引用、尺寸、来源类型和生成元数据;素材在同一账号的所有项目中可见。素材放入画布时复制为 `editor_project_resource` 并由图层引用 resourceId。
|
||||
- 说明:图片画布账号级素材表,保存用户上传 / 生成素材的名称、文件夹、图片读取地址、OSS 引用、尺寸、来源类型、prompt、provider、task、`asset_kind` 和 `generation_inputs_json`。素材在同一账号的所有项目中可见;图片 / 图标 / UI 提取等生成 BFF 在请求携带 `asset_folder_id` 时负责创建账号级生成素材并返回 asset 快照,前端只用该快照更新素材栏。素材放入画布时复制为 `editor_project_resource` 并由图层引用 resourceId,画布从 resource / asset 级元数据恢复素材类别和用户可见生成输入快照。
|
||||
- 索引:`by_editor_asset_owner_user_id`、`by_editor_asset_folder_id`。
|
||||
|
||||
### `inventory_slot`
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# 外部 OpenAPI 与 API Key 接入方案
|
||||
|
||||
## 背景
|
||||
|
||||
外部调用方需要通过稳定 HTTP 契约使用图片画布编辑器内的美术生图能力,并能创建项目、保存画板布局。该能力必须走 `server-rs + Axum + SpacetimeDB` 正式链路,不能把 API Key、画板状态或生成结果放到前端临时状态中。
|
||||
|
||||
## v1 范围
|
||||
|
||||
本期新增外部 API 命名空间:
|
||||
|
||||
```text
|
||||
/api/external/v1
|
||||
```
|
||||
|
||||
v1 只开放以下能力:
|
||||
|
||||
- `POST /api/external/v1/editor/projects`:创建图片画布项目。
|
||||
- `GET /api/external/v1/editor/projects/{projectId}`:读取项目与默认画布。
|
||||
- `PATCH /api/external/v1/editor/projects/{projectId}/canvas`:保存默认画布的 viewport 和 layers。
|
||||
- `POST /api/external/v1/editor/images/generations`:调用编辑器美术生图能力;可选传入 `projectId`,生成后自动写入 `editor_project_resource`,同时写入账号级 `editor_asset` 素材库。
|
||||
- `GET /api/external/v1/openapi.json`:导出本版本 OpenAPI 3.1 JSON。
|
||||
|
||||
管理 API Key 的登录态接口:
|
||||
|
||||
```text
|
||||
GET /api/profile/api-keys
|
||||
POST /api/profile/api-keys
|
||||
DELETE /api/profile/api-keys/{keyId}
|
||||
```
|
||||
|
||||
前端入口位于登录后个人中心的 `我的 → 开发者 API Key`,用于查看当前 Key、创建新 Key、复制一次性明文和撤销已创建 Key。
|
||||
|
||||
## 鉴权
|
||||
|
||||
外部调用使用 Bearer API Key:
|
||||
|
||||
```http
|
||||
Authorization: Bearer tnr_sk_xxx
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- API Key 归属于 `owner_user_id`,外部接口只能访问该账号自己的项目、画布和生成素材。
|
||||
- 明文 Key 只在创建接口返回一次,后端只保存 `key_hash` 与 `key_prefix`。
|
||||
- API Key 被撤销后立即不可再用于外部接口。
|
||||
- 外部 API 鉴权不复用登录态 JWT,不检查 refresh session;它是独立开发者凭据。
|
||||
- OpenAPI JSON 公共可读,不需要鉴权。
|
||||
|
||||
## 数据模型
|
||||
|
||||
新增 SpacetimeDB private 表:
|
||||
|
||||
```text
|
||||
external_api_key
|
||||
```
|
||||
|
||||
字段:
|
||||
|
||||
- `key_id`:主键。
|
||||
- `owner_user_id`:所属账号。
|
||||
- `name`:用户可识别名称。
|
||||
- `key_prefix`:前缀片段,用于列表展示和排障。
|
||||
- `key_hash`:完整 Key 的 SHA-256 十六进制摘要,唯一。
|
||||
- `scopes_json`:作用域 JSON,v1 固定包含 `editor:project`、`editor:canvas`、`editor:image-generate`。
|
||||
- `created_at` / `last_used_at` / `revoked_at` / `updated_at`。
|
||||
|
||||
SpacetimeDB procedure:
|
||||
|
||||
- `create_external_api_key_and_return`
|
||||
- `list_external_api_keys_and_return`
|
||||
- `revoke_external_api_key_and_return`
|
||||
- `authenticate_external_api_key_and_return`
|
||||
|
||||
## 生成图落库
|
||||
|
||||
外部生图接口复用编辑器内 `VectorEngine` / `gpt-image-2` 生成链路,后端拿到图片后:
|
||||
|
||||
1. 通过 OSS / asset object adapter 持久化图片。
|
||||
2. 写入 `editor_asset`,让生成图进入账号级素材库。
|
||||
3. 如果请求带 `projectId`,写入 `editor_project_resource`。
|
||||
4. 返回图片读取地址、素材 ID、资源 ID、尺寸、prompt、model、provider 和 taskId。
|
||||
|
||||
如果请求未带 `projectId`,只生成并写入素材库;调用方可随后创建项目或自行保存画板布局。
|
||||
|
||||
## OpenAPI 导出
|
||||
|
||||
OpenAPI 3.1 JSON 固定落在:
|
||||
|
||||
```text
|
||||
docs/openapi/genarrative-external-v1.openapi.json
|
||||
```
|
||||
|
||||
服务端 `GET /api/external/v1/openapi.json` 使用同一份 JSON,通过 `include_str!` 导出,避免运行时生成结果与仓库文档漂移。
|
||||
|
||||
## 验收
|
||||
|
||||
- API Key 创建只返回一次明文,列表不返回明文。
|
||||
- 撤销后的 API Key 调用外部接口返回 `401`。
|
||||
- 外部生图成功后,生成结果同时出现在画布资源和账号级素材库。
|
||||
- OpenAPI JSON 能被 `serde_json` 解析,且 security scheme 为 Bearer API Key。
|
||||
- 修改 SpacetimeDB schema 后运行 `npm run spacetime:generate` 与 `npm run check:spacetime-schema`。
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user