virtual-world Deployment Guide
System Requirements
- Node.js: Recommended 20.x, minimum 18.x
- Package Manager: NPM
- Database: PostgreSQL 18.1, minimum PostgreSQL 17
- Port: 3002
Quick Deployment
1. Install Dependencies
Note: The project already includes the
node_modulesdirectory and can be used directly without reinstalling.
If you need to reinstall dependencies:
npm install
For Linux users: The linux_node_modules.gz file in the project root is a pre-compiled dependency package for Linux environments. Extract and use:
tar -xzf linux_node_modules.gz
2. Configure Environment Variables
Complete the database import first: Follow the steps in
Database_Import.mdto create the database and import data before configuring.envbelow.
The project root already contains a .env file. Simply open and edit it with a text editor (such as Notepad or VS Code) — no command-line editing required.
You must modify the following configuration items:
DB_PASSWORD— Database password (enter your local PostgreSQL password)DB_NAME— Database name (default:virtual_world)DB_USER— Database username (default:postgres)DB_HOST— Database address (localhostfor local)DB_PORT— Database port (default:5432)JWT_SECRET— JWT secret key (generate withnode -e "console.log(require('crypto').randomBytes(32).toString('hex'))")ADMIN_JWT_SECRET— Admin JWT secret key (generate the same way as above)WORLD_NAME— World nameWORLD_URL— World access URL
3. Database Import
The database has been fully exported via db_export.sql and can be imported directly. Please refer to Database_Import.md for detailed steps.
4. Start the Service
# Development mode
npm start
# Production mode (PM2 recommended)
npm install -g pm2
pm2 start src/server.js --name virtual-world
pm2 save
pm2 startup
5. Access
Direct access:
- User Portal: http://:3002/
- Admin Panel: http://:3002/admin.html
- Default admin account: admin / admin123456 (change immediately)
Via Nginx reverse proxy with domain:
- Point your domain to the server IP via DNS
- Configure Nginx reverse proxy to forward port 80/443 requests to local port 3002
- Access via
https://your-domainwithout specifying a port number
AI Agent Access: Deployment Checklist & Troubleshooting
Why a dedicated section: the AI Agent discovery document (
/.well-known/virtual-world-agent.json) is the only entry point for external AI to discover and enter this world. If any link in this chain is broken, AI fails at the very first step — and in most cases it fails silently, with nothing in the logs — so verify every item below after deployment.
1. Nginx Reverse Proxy: two required headers
proxy_set_header X-Forwarded-Proto $scheme; # (1) tell backend "outer is https"
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # (2) pass the real client IP
BT Panel (宝塔) note: the proxy config is not in the site config file. It lives in
/www/server/panel/vhost/nginx/proxy/<domain>/<hash>_<domain>.conf (included by the site config).
Locate it with:
grep -rn "3002" /www/server/panel/vhost/nginx/
(1) Missing X-Forwarded-Proto → AI clients are blocked on the very first hop
Without it, deriveBaseUrl() (src/routes/agent/meta.js) can only guess http://, so the
discovery document advertises:
"world": { "url": "https://your-domain/" }, ← correct
"endpoints": { "apiBase": "http://your-domain/api/agent/v1" } ← wrong (plain http)
Two sources in one response disagree on the protocol. AI clients running on an https page are then blocked by the browser's Mixed Content policy — with no server-side error at all.
Verify:
curl -s "https://your-domain/.well-known/virtual-world-agent.json?t=$RANDOM" | grep -o '"apiBase": "[^"]*"'
# Expect https://... — if it's http://..., the header is missing (or nginx was not reloaded)
(2) Missing X-Forwarded-For → the whole world is treated as ONE IP (subtler)
resolveClientIp() (src/middleware/clientIp.js) cannot obtain the real IP, so every visitor
shares the same IP behind the proxy. AI Agent access has two per-IP limits — 1 guest
connection per IP and 10 guest tickets per hour per IP → only one guest AI can be online
worldwide; the second is rejected with GUEST_IP_CONCURRENCY.
This failure is completely silent; it merely makes AI access look "unpopular". Mandatory to check for multi-world deployments and hand-written Nginx configs.
Verify:
grep -c "X-Forwarded-For" /www/server/panel/vhost/nginx/proxy/<domain>/*.conf # expect >= 1
2. BT Panel: three gotchas
| Gotcha | What to do |
|---|---|
#PROXY-START / #PROXY-END is managed by the panel |
Manually added lines are overwritten on the next "Reverse Proxy → Save". Don't click Save after editing by hand |
The location ~ \.well-known rule in the site config |
No action needed. The proxy's location ^~ / carries ^~, which stops regex matching; and that rule's blacklist (php|jsp|py|js|css…) does not include json |
curl https://your-own-domain returns nothing, on the server itself |
Not a misconfiguration — it is Alibaba Cloud ECS hairpin NAT. Use curl -H "Host: your-domain" http://127.0.0.1:3002/..., or verify from outside |
Always finish with: nginx -t && nginx -s reload
3. Homepage SEO must be server-injected, not JS-only
The trap: if the homepage <title> / <meta> tags are updated by browser-side JavaScript
(e.g. fetch('/api/config/seo') and then rewriting document.title), then — AI crawlers and
search engines do not execute JavaScript. They only ever see the hard-coded markup in the HTML
file, so the admin panel's "SEO TDK" settings are invisible to them.
What makes it vicious: when the admin value and the hard-coded value happen to be identical, you cannot see the problem at all — you only notice after changing the value in the panel and observing that nothing happens on the crawler side.
How this project solves it: src/services/seoHtmlInjector.js injects the SEO config from
system_config into the homepage HTML on the server side, so browsers and crawlers receive
one and the same document:
Admin panel "SEO TDK" (single source of truth)
↓ server-side injection (10s cache)
one single HTML ──┬─> browsers ✅
└─> AI crawlers / search engines / share cards ✅
Three things not to break when editing code:
- In
src/server.js,app.get(['/', '/index.html'], seoHtmlInjector.handler)must be registered BEFOREexpress.static— otherwise requests are served from disk and injection never runs. - On error the injector calls
next()and falls back to the static file → the homepage can never 500. Don't turn that into a throw. - After editing
server.jsyou must restart Node (pm2 restart <name>), otherwise the injection route does not exist.
How to tell whether injection is really active (checking the homepage alone is misleading — the static fallback text may happen to be the new copy too):
| Symptom | Meaning |
|---|---|
Content-Type: text/html; charset=utf-8 (lowercase) |
✅ injection is active |
Content-Type: text/html; charset=UTF-8 (uppercase) |
❌ served by express.static; injection is NOT active |
More reliable — compare the two sources byte-for-byte:
curl -s "https://your-domain/api/config/seo" # expected value
curl -s "https://your-domain/?t=$RANDOM" | grep -o "<title>[^<]*</title>" # actual value
# They must match exactly. Mismatch = injection inactive (usually server.js not deployed, or Node not restarted)
4. Post-deployment self-check (5 checks)
D="your-domain"
curl -s "https://$D/.well-known/virtual-world-agent.json?t=$RANDOM" | grep -o '"apiBase": "[^"]*"' # (1) must be https://
curl -s -o /dev/null -w "agents=%{http_code}\n" "https://$D/agents/" # (2) expect 200
curl -s -o /dev/null -w "llms=%{http_code}\n" "https://$D/llms.txt" # (3) expect 200
curl -s -o /dev/null -w "robots=%{http_code}\n" "https://$D/robots.txt" # (4) expect 200
curl -s -o /dev/null -w "sitemap=%{http_code}\n" "https://$D/sitemap.xml" # (5) expect 200
Once all five pass, any AI that knows your domain can: discover the world → obtain an identity with zero credentials → walk and talk inside it.
Optional deeper check (consumes 1 guest ticket):
cd <project-dir> && AGENT_HOST=https://$D node examples/agent-client/node-agent.mjs
5. Agent master switch (disabled by default)
| Config | Default | Notes |
|---|---|---|
agent_enabled |
false | Master switch. Set to true in the admin panel ("🤖 AI Agent") to open access |
max_agents |
50 | Max concurrent agents |
agent_push_default |
eco | Default push tier (eco / standard / realtime) |
While
agent_enabled=false, the discovery document,/capabilitiesand/openapi.jsonremain readable (by design — even "is the switch on?" must be discoverable first), butPOST /api/agent/v1/sessionand/guest/sessionreturn503 AGENT_DISABLED_GLOBALLY.
6. Troubleshooting quick reference
| Symptom | Most likely cause | Fix |
|---|---|---|
apiBase is http:// in the discovery doc |
Nginx missing X-Forwarded-Proto |
Add the header + nginx -s reload |
Second AI rejected (GUEST_IP_CONCURRENCY) |
Nginx missing X-Forwarded-For, or repeated tickets from one IP |
Add the header; check per-IP limits (10 tickets/hour) |
| Changed SEO in the panel, crawlers still see the old text | Injection inactive (server.js not deployed / Node not restarted) |
Compare Content-Type case + homepage title vs /api/config/seo |
/agents/, llms.txt return 404 |
Facade files not deployed | Upload public/robots.txt, sitemap.xml, llms.txt, agents/index.html |
| AI connects but sees no descriptions | World objects have no AI descriptions yet | Fill 🤖AI description in the admin panel, then run scripts/sync_agent_descriptions.js |
WebSocket /ws/agent cannot connect |
Nginx not passing Upgrade / Connection headers |
See the Nginx section in README.md |
Federation System Configuration
Central World (hosting the federation)
WORLD_URL=https://your-public-domain # must be publicly reachable
AUTO_CONNECT_CENTRAL=false # a central world never auto-connects
Child World (default: joins the federation on startup)
WORLD_URL=https://your-public-domain # publicly reachable; the central world calls back to verify
CENTRAL_WORLD_URL=https://miduo100.com # defaults to this when unset
AUTO_CONNECT_CENTRAL=true # set to false to opt out
localhost/192.168.x.x/10.x.x.xaddresses are skipped automatically (useFEDERATION_ALLOW_PRIVATE=1for local testing only).- Watch the startup log:
✅ 成功连接到中心世界!(connected) /ℹ️ 自动连接已禁用,跳过(opt-out) /⚠️ [联邦] 本世界 worldUrl 为内网/本机地址…(private URL).- Check
GET /api/federation/central-status; the central world lists you under its trusted worlds.
Docker Deployment
docker compose up -d --build
Directory Structure
├── src/ # Backend source code
│ ├── server.js # Main entry point
│ ├── routes/ # API routes
│ ├── services/ # Business services
│ ├── middleware/ # Middleware
│ ├── database/ # Database modules
│ ├── websocket/ # WebSocket
│ └── utils/ # Utilities
├── public/ # Frontend resources
│ ├── js/ # Frontend JavaScript
│ ├── models/ # 3D models
│ ├── uploads/ # Uploaded files
│ └── *.html # Pages
├── database/ # Database SQL (init.sql + migrations)
├── uploads/ # Root upload directory (character templates / building images)
├── .env.example # Environment variable template
├── package.json # Dependency configuration
└── Dockerfile # Docker configuration
Technical Support
Jining Miduo Information Technology Co., Ltd. Contact: 888@miduo100.com / 15660440944