Appearance
品牌边缘服务器初始化 —— 可直接粘贴给 AI agent 的部署提示词
读者:品牌产品线负责人 + 新购品牌服务器上的 AI agent(本地终端形态,或经 ssh 别名远程操作该机的开发 agent)。 定位:
brand-self-hosted-deployment-guide.md§3.3「品牌边缘」与接入总纲 §5.1「边缘服务器(机器面初始化)」的机器面可执行展开——购机 / DNS / 安全组等控制面仍归人类,本文不覆盖。 状态基准:2026-09-14(2026-09-16 按首次品牌实战修订:模板内置 ACME 与 verify_depth 两处修复;新增 §4「边缘骨架之外的必做项」四条实战沉淀)。 配套:八步上线总览见品牌自托管接入标准;前端派生与部署见saas-mode-b-integration-guide.md;邮件防伪见email-auth-spf-dkim-dmarc.md。
0. 适用场景与人类侧前提
此时品牌线可以还没有任何业务代码——本初始化只立「边缘骨架」:系统底座 + nginx + ACME 证书 + 同源反代 + 临时占位页。完成后 <品牌域>/ 能打开占位页、/auth/ 能打到基座 Keycloak 的品牌 realm。
粘贴提示词前,先确认四件人类侧前提(任何一条不满足,先补齐再找 agent——这些都不在本机能修):
| # | 前提 | 在哪做 |
|---|---|---|
| 1 | 品牌服务器已购,与基座同区域(尽量同 VPC);本机公网 / 内网 IP 已知 | 云控制台 |
| 2 | DNS @ 与 www 两条 A 记录 → 本机公网 IP | 域名商 DNS 面板 |
| 3 | 基座安全组已放行本机内网 IP → 443(注意:配在基座实例的安全组上,不是本机) | 云控制台(基座侧) |
| 4 | 基座已建好本品牌 realm:基座 owner 在基座节点执行 scripts/new-realm.sh <realm> <品牌域> --client-id <realm>-web(操作细则见基座侧 runbook docs/ops/realm-topology-rollout-guide.md §3.2;域名 / DNS / 边缘不存在之前 realm 建不了) | 基座节点 |
1. 填空表(粘贴前替换提示词中的占位符)
| 占位符 | 含义 | worked example |
|---|---|---|
<BRAND_DOMAIN> | 品牌域名 | vylpa.com |
<REALM> | 品牌 realm 名 | arch |
<BASE_DOMAIN> | 基座域名(到基座反代 TLS 的 SNI / 证书校验名) | bhsl.cc |
<BASE_VPC_IP> | 基座网关的 VPC 内网 IP(基座 owner 提供) | 10.0.0.5 |
2. 部署提示词(整段复制 → 替换占位符 → 粘贴给品牌服务器上的 AI agent)
text
# 任务:AgentHub 品牌边缘服务器初始化(<BRAND_DOMAIN> / realm <REALM>)
## 背景
本机是一台全新的「品牌服务器」,为 AgentHub 平台上的产品线 <BRAND_DOMAIN> 服务。
AgentHub 基座部署在另一台服务器(基座域名 <BASE_DOMAIN>)。本机角色 = 品牌边缘 nginx:
- `/`(及未来产品路径)→ 本机应用(现在还没有代码,先放占位页)
- `/api/`、`/auth/`、`/fs/`、`/blog/` → 同源反代到基座网关(VPC 内网),浏览器零 CORS
- 用户全程只看到 <BRAND_DOMAIN>,地址栏不出现基座域名
假定系统为 Ubuntu/Debian;若不是请先告诉我再动手。
## 已定参数(不要再询问)
- 品牌域名:<BRAND_DOMAIN>(www 只做 301 → apex)
- 基座域名(反代 TLS 的 SNI / 证书校验名):<BASE_DOMAIN>(基座已有 ACME 真证书,
proxy_ssl_verify 必须保持 on)
- 基座网关 VPC 内网地址:<BASE_VPC_IP>:443
- 基座侧已建好 Keycloak realm <REALM>(签发者 https://<BRAND_DOMAIN>/auth/realms/<REALM>)
- 本产品暂无租户 blog 站点,__CONTENT_SITE_SLUG__ 用暂定值 <REALM>(/blog/ 现在 404 属正常)
## 第 0 步:前置确认(任何一条不满足就停下来报告,不要继续)
1. DNS:`dig +short <BRAND_DOMAIN> A` 与 `dig +short www.<BRAND_DOMAIN> A` 都指向本机公网 IP。
2. 基座连通性:`curl -s --resolve <BASE_DOMAIN>:443:<BASE_VPC_IP> https://<BASE_DOMAIN>/health/live`
返回 {"status":"alive"} 才算通(同时验证 VPC 路由 + 基座安全组已放行本机 + 证书可信)。
若失败,报告错误原文——大概率是基座安全组没放行本机内网 IP 的 443,这不是本机能修的。
## 第 1 步:系统底座
- apt 更新;安装 nginx、certbot、jq、python3。
- 防火墙(ufw):只放行 22 / 80 / 443,默认 deny 入站。
- 时区与时间同步(timedatectl 确认 NTP active)。
## 第 2 步:ACME 证书(<BRAND_DOMAIN> + www.<BRAND_DOMAIN>)
1. 先 `certbot certonly --standalone -d <BRAND_DOMAIN> -d www.<BRAND_DOMAIN>`(此时 nginx 未占用 80)。
2. 证书落位(nginx 模板引用 /etc/nginx/certs/ 下的一对文件):
mkdir -p /etc/nginx/certs
ln -s /etc/letsencrypt/live/<BRAND_DOMAIN>/fullchain.pem /etc/nginx/certs/<BRAND_DOMAIN>.crt
ln -s /etc/letsencrypt/live/<BRAND_DOMAIN>/privkey.pem /etc/nginx/certs/<BRAND_DOMAIN>.key
3. 续期通道:模板已内置 80 端口的 ACME webroot location(勿把 301 改回 server 级——会遮蔽它)。
续期走 webroot:`certbot renew --webroot -w /var/www/acme --dry-run` 必须通过,
并放 deploy hook /etc/letsencrypt/renewal-hooks/deploy/nginx-reload.sh(内容:systemctl reload nginx)。
## 第 3 步:占位页(现在还没有应用代码)
创建 /var/www/<BRAND_DOMAIN>/index.html:一张极简「即将上线」页。
占位服务用 systemd 跑一个本地静态服务监听 127.0.0.1:3000
(如 ExecStart=/usr/bin/python3 -m http.server 3000 --directory /var/www/<BRAND_DOMAIN>),开机自启。
将来应用上线后,只需把边缘配置里的上游地址换掉并停掉这个服务。
## 第 4 步:品牌边缘 nginx 配置
将本提示词末尾附的模板原文落为 /etc/nginx/sites-available/<BRAND_DOMAIN>.conf,
软链到 sites-enabled,并移除默认 site。按「已定参数」替换全部占位符:
- __BRAND_DOMAIN__=<BRAND_DOMAIN>
- __BASE_GATEWAY__=<BASE_VPC_IP>:443
- __BASE_DOMAIN__=<BASE_DOMAIN>
- __LOCAL_APP_UPSTREAM__=127.0.0.1:3000
- __CONTENT_SITE_SLUG__=<REALM>
- __LOCAL_RENDER_UPSTREAM__ 保留占位符不动(它在注释块里,暂无重能力服务)
模板已内置两处 2026-09-16 修复,**勿回退**:① 80 端口 ACME webroot location(301 包在
`location /` 内——server 级 return 先于 location 执行,会遮蔽 ACME 块致 webroot 续期必败);
② 各 https 代理 location 的 `proxy_ssl_verify_depth 4`(nginx 默认深度 1 撑不起基座 LE
深链,`/api/`、`/auth/` 必 502)。仍需 `mkdir -p /var/www/acme`(webroot 续期目录)。
`nginx -t` 通过后 systemctl reload nginx。
## 第 5 步:验收断言(逐条执行并记录原始输出)
1. curl -s https://<BRAND_DOMAIN>/auth/realms/<REALM>/.well-known/openid-configuration | jq -r .issuer
→ 必须逐字等于 https://<BRAND_DOMAIN>/auth/realms/<REALM>
2. curl -sI https://<BRAND_DOMAIN>/auth/admin/ | head -1 → 404(管理台不得在品牌域暴露)
3. curl -sI https://<BRAND_DOMAIN>/auth/realms/master/ | head -1 → 404
4. curl -sI http://<BRAND_DOMAIN>/ | head -1 → 301 https
5. curl -sI https://www.<BRAND_DOMAIN>/ | head -1 → 301 https://<BRAND_DOMAIN>/
6. curl -s https://<BRAND_DOMAIN>/ | grep -i -m1 . → 占位页可见
7. openssl s_client -connect 127.0.0.1:443 -servername <BRAND_DOMAIN> </dev/null 2>/dev/null \
| openssl x509 -noout -subject -dates → Let's Encrypt 证书
## 红线(违反任何一条即停手)
- 不得触碰基座服务器;本机只做本机配置。
- /auth/ 的反代不得加 X-Forwarded-Prefix(会变 /auth/auth)。
- /auth/admin、/auth/realms/master 的 404 规则不得删除。
- proxy_ssl_verify on 不得关闭;/api/ 的 proxy_buffering off 与 3600s 读超时不得删(SSE 依赖)。
- 模板除第 4 步明示的替换外不得改动结构(ACME location 与 proxy_ssl_verify_depth 已内置)。
## 交付物
按第 5 步逐条给出命令 + 原始输出 + 通过与否;全部通过则本机初始化完成,
明确列出「将来应用上线时要改的唯一一处」= __LOCAL_APP_UPSTREAM__。
## 附:品牌边缘 nginx 模板原文(以此为准,勿凭记忆重写)
(粘贴 AgentHub 仓库 deploy/brand-edge/nginx.brand-edge.example.conf 全文;
模板是唯一维护源——从仓库取原文附在此处,不要转述或重写。)模板获取:deploy/brand-edge/nginx.brand-edge.example.conf(基座仓库路径)。若 agent 所在环境能直读仓库,提示词第 4 步可改为「按仓库该路径取模板」;否则把模板全文粘贴进「附」处,使提示词完全自包含。
3. 验收断言在验什么
| 断言 | 证明 |
|---|---|
| 1 issuer 逐字等于品牌域 | 全链通:DNS → 本机 TLS → 反代 → 基座安全组 → 基座网关 → KC realm frontendUrl。这条过了,登录链路(OIDC 授权跳转)就有物理基础 |
2/3 /auth/admin、/auth/realms/master 404 | Keycloak 管理面只在基座域开放,品牌域不暴露(安全纪律) |
| 4/5 301 跳转 | 80 → HTTPS、www → apex 的流量归口正确 |
| 6 占位页 | 本机数据面(location /)工作正常 |
| 7 证书 | ACME 签发成功且挂载路径正确 |
4. 完成之后:边缘骨架之外的必做项(2026-09-16 首次品牌实战沉淀)
七探针通过只代表「骨架」通了。以下四项在首个品牌(lestoform)接入中全部实际踩坑, 每一项都会把接入卡死在某一步,属骨架之外的必做项:
4.1 基座网关需为品牌 Host 加 server 块(否则 /auth/ /api/ 全 502)
品牌边缘模板对 /auth/、/api/ 透传品牌 Host(proxy_set_header Host $host),而基座网关 nginx.gateway.production.conf 的 default_server 对未知 Host 一律 444——必须在基座网关 为品牌域加专属 server 块(控制面 location → 内网上游、其余 444、复用基座证书按 Host 匹配, 无需品牌证书)。实战块(首个品牌样例已入库 nginx.gateway.production.conf,新品牌照抄换 server_name 即可):
nginx
# ── <品牌域>:品牌 realm 控制面前门(宿主=品牌自托管边缘)──
server {
listen 443 ssl;
server_name <BRAND_DOMAIN>;
ssl_certificate /etc/letsencrypt/live/<BASE_DOMAIN>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<BASE_DOMAIN>/privkey.pem;
server_tokens off;
client_max_body_size 1m;
resolver 127.0.0.11 valid=30s;
location /auth/ {
proxy_pass http://keycloak:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Port 443;
}
location /api/ {
proxy_pass http://api:8000;
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 https;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Upgrade $http_upgrade;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
}
location /fs/ {
set $seaweedfs_upstream seaweedfs;
rewrite ^/fs/(.*)$ /$1 break;
proxy_pass http://$seaweedfs_upstream:8080;
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 https;
client_max_body_size 200m;
proxy_request_buffering off;
}
location / { return 444; }
}4.2 Builder 与 Starter 是「构建时钉死 realm」的 SPA——品牌侧必须自行构建部署
基座域上的 Builder/Starter 只认平台 realm(import.meta.env.VITE_KEYCLOAK_* 构建期烘焙, 无运行时注入)。品牌运营端与用户端都要另行构建并部署到品牌边缘,否则七步重跑第 1 步 (注册)就没有入口:
bash
# Builder(运营端,路径形态 /builder/)
cd frontend/packages/web-builder
# ⚠️ Windows/Git Bash 构建机:环境变量带 / 前缀会被 MSYS 路径转换污染
#(/builder/ → /Program Files/Git/builder/),必须加 MSYS2_ENV_CONV_EXCL="VITE_BASE_PATH"
#(不要用 MSYS_NO_PATHCONV=1——会连 pnpm shim 一起弄坏)
VITE_BASE_PATH=/builder/ \
VITE_KEYCLOAK_URL=https://<BRAND_DOMAIN>/auth \
VITE_KEYCLOAK_REALM=<REALM> \
VITE_KEYCLOAK_CLIENT_ID=<REALM>-web \
pnpm build
# Starter(用户端,根路径;注意:Starter 路由无 basename,只能部署在 /)
VITE_KEYCLOAK_URL=https://<BRAND_DOMAIN>/auth \
VITE_KEYCLOAK_REALM=<REALM> \
VITE_KEYCLOAK_CLIENT_ID=<REALM>-web \
VITE_PROJECT_ID=<project_id> \
pnpm build部署:dist 分别放 /var/www/<BRAND_DOMAIN>/builder/ 与 /app-root/,由 nginx 直出。python 占位服务没有 SPA history 回退——登录回跳 /builder/callback 必 404,必须用 nginx try_files:
nginx
location = /builder { return 308 /builder/; } # 裸路径不落进根 location 出错壳
location ^~ /builder/ {
alias /var/www/<BRAND_DOMAIN>/builder/;
try_files $uri $uri/ /builder/index.html;
}
location = /builder/index.html { # SPA 壳禁缓存(防旧壳缓存坑)
alias /var/www/<BRAND_DOMAIN>/builder/index.html;
add_header Cache-Control "no-cache";
}
# 根路径 Starter 同理:location / { alias .../app-root/; try_files $uri $uri/ /index.html; }
# + location = /index.html { alias .../app-root/index.html; add_header Cache-Control "no-cache"; }4.3 TTS 必须配公网 DashScope vendor(专属 MaaS 实例无 TTS 服务)
「单 vendor 全模态挂 MaaS compatible-mode」对 chat/embedding/stt 成立,但专属 MaaS 实例 没有 /api/v1/services/audio/tts/SpeechSynthesizer 服务路径(返回 url error 400)。tts 模态必须单独挂一个指向公网 https://dashscope.aliyuncs.com/compatible-mode/v1 的 vendor + 经典按量 key(平台适配器会把 compatible-mode 后缀自动重写为原生 REST 树)。另注意:前端 报「没有可用的模型路由」不代表路由缺失——503「endpoints unavailable」是厂商侧失败,按 厂商 / endpoint / key 排查。
4.4 内置工具(如 generate_image)不会自动出现——需在 Builder 手动创建并绑定
内置工具按 slug 命名约定由运行时识别(tool_invoker 内置处理器),但 tools 表里不会 自动生成行:Builder → 工具 → 新建(type=builtin、slug=generate_image 逐字精确、 config={}、挂项目)→ 在 agent 的工具绑定里勾选 → 发布新版本 → 新会话生效(会话钉死 打开时的版本)。未绑定时模型会把 tool_call JSON 当普通文本吐出。
4.5 其他
- 唯一遗留改动点:正式前端从模板派生部署后(模式 B 指南 §2/§6),把边缘配置的
__LOCAL_APP_UPSTREAM__换成真实应用地址,停掉占位服务,nginx -t后 reload。 - 端到端验收(登录 →
GET /api/v1/me→ 一轮对话扣点)见品牌自托管接入标准 §3.8。 - 要用邮箱验证 / Google 登录的话,接着做:邮件防伪与 realm SMTP(email-auth-spf-dkim-dmarc.md);Google IdP 开闸(品牌自托管接入标准 §3.4)。