From 2d48372a57943414df5079a2f56f3fcd8a940d16 Mon Sep 17 00:00:00 2001 From: twosugar Date: Mon, 12 Oct 2026 18:25:35 +0800 Subject: [PATCH] feat(cloudflare): serve open.longbridge.com from Cloudflare Pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prepare the docs site to replace the open.longbridge.com nginx server with Cloudflare Pages. Merging changes nothing for users until DNS moves; nginx keeps serving .com (before cutover), .cn and open.longbridge.xyz from OSS. Cloudflare track - functions/[[path]].ts: Pages Function ports every open.longbridge.com nginx rule in declaration order (login pages, locale llms, terminal proxy, trailing slash, /en, SPA forwarding, install 301/proxy, markdown negotiation, X-Frame-Options). Upstreams are picked per host from the SITES table. - public/_routes.json, public/_redirects, 404 page, canary-only robots. - release.yml deploys Cloudflare in its own job (ASSETS_DIR=_docs). - canary-cloudflare.yml publishes open-canary.longbridge.xyz from the canary branch. canary-aliyun.yml and oss-probe.yml are manual-only. - pack-skills.yml re-dispatches the Cloudflare builds after uploading zips. nginx (OSS) track stays byte-compatible - ASSETS_DIR picks the hashed asset dir: OSS builds keep /assets, Cloudflare builds use /_docs. - scripts/copy-routes.ts runs only in the OSS jobs, giving nginx the foo/index.html shape it reads. - OSS jobs assert both before uploading. Also: SEO metadata, locale sitemaps, .md links in llms.txt, skill zip packing, canary hostname rewrite. Full record and verification in docs/superpowers/plans/2026-09-16-openapi-nginx-decommission.md (§15). Co-Authored-By: Claude Opus 5.5 --- .github/workflows/canary-aliyun.yml | 135 ++ .github/workflows/canary-cloudflare.yml | 167 ++ .github/workflows/canary.yml | 13 +- .github/workflows/oss-probe.yml | 126 ++ .github/workflows/pack-skills.yml | 25 + .github/workflows/release.yml | 65 +- .gitignore | 11 +- astro.config.ts | 52 +- docs/en/docs/api.mdx | 2 + docs/en/docs/changelog.mdx | 1 + docs/en/docs/getting-started.mdx | 2 + docs/en/docs/index.mdx | 2 + docs/en/docs/llm.mdx | 1 + docs/en/docs/mcp.mdx | 2 + docs/en/docs/quote/objects.mdx | 1 + docs/en/docs/socket/control-command.mdx | 2 +- docs/en/docs/socket/diff-ws-tcp.mdx | 2 +- docs/en/docs/socket/socket-otp-api.mdx | 2 +- docs/en/docs/trade/definition.mdx | 1 + docs/en/docs/trade/order/close_position.mdx | 2 +- docs/en/docs/trade/overview.mdx | 1 + docs/en/index.mdx | 1 + docs/en/pricing/index.mdx | 1 + docs/public/assets/ai.svg | 121 -- docs/public/assets/bar.svg | 15 - docs/public/assets/code.svg | 112 -- docs/public/assets/icon-cloud.svg | 10 - docs/public/assets/icon-code.svg | 27 - docs/public/assets/icon-lanuch.svg | 10 - docs/public/assets/quote.svg | 52 - docs/public/assets/sdk.svg | 97 -- docs/public/assets/trade.svg | 56 - .../2026-09-16-openapi-nginx-decommission.md | 1406 +++++++++++++++++ docs/zh-CN/docs/api.mdx | 2 + docs/zh-CN/docs/changelog.mdx | 1 + docs/zh-CN/docs/getting-started.mdx | 2 + docs/zh-CN/docs/index.mdx | 2 + docs/zh-CN/docs/llm.mdx | 1 + docs/zh-CN/docs/mcp.mdx | 2 + docs/zh-CN/docs/quote/objects.mdx | 1 + docs/zh-CN/docs/trade/definition.mdx | 1 + docs/zh-CN/docs/trade/overview.mdx | 1 + docs/zh-CN/index.mdx | 1 + docs/zh-CN/pricing/index.mdx | 1 + docs/zh-CN/skill/index.mdx | 2 +- docs/zh-HK/docs/api.mdx | 2 + docs/zh-HK/docs/changelog.mdx | 1 + docs/zh-HK/docs/getting-started.mdx | 2 + docs/zh-HK/docs/index.mdx | 2 + docs/zh-HK/docs/llm.mdx | 1 + docs/zh-HK/docs/mcp.mdx | 2 + docs/zh-HK/docs/quote/objects.mdx | 1 + docs/zh-HK/docs/trade/definition.mdx | 1 + docs/zh-HK/docs/trade/overview.mdx | 1 + docs/zh-HK/index.mdx | 1 + docs/zh-HK/pricing/index.mdx | 1 + docs/zh-HK/skill/index.mdx | 2 +- functions/[[path]].ts | 283 ++++ package.json | 7 +- public/_redirects | 22 + public/_routes.json | 5 + .../longbridge}/longbridge-terminal/install | 0 .../longbridge-terminal/install.ps1 | 0 scripts/copy-routes.ts | 4 + src/components/shell/SearchButton.tsx | 9 +- src/content.config.ts | 3 + src/integrations/prebuild-skills.ts | 103 +- src/integrations/region-hostname-rewrite.ts | 24 +- src/integrations/sitemap-locale-files.ts | 46 + src/layouts/BaseLayout.astro | 29 +- src/layouts/DocsLayout.astro | 5 +- src/lib/page-meta.test.ts | 105 ++ src/lib/page-meta.ts | 124 ++ src/lib/published-urls.ts | 16 + src/pages/404.astro | 21 + src/pages/[...slug].astro | 14 +- src/pages/llms-full.txt.ts | 2 +- src/pages/llms.txt.ts | 4 +- src/pages/robots.txt.ts | 20 +- src/pages/zh-CN/[...slug].astro | 14 +- src/pages/zh-HK/[...slug].astro | 14 +- 81 files changed, 2797 insertions(+), 634 deletions(-) create mode 100644 .github/workflows/canary-aliyun.yml create mode 100644 .github/workflows/canary-cloudflare.yml create mode 100644 .github/workflows/oss-probe.yml delete mode 100644 docs/public/assets/ai.svg delete mode 100644 docs/public/assets/bar.svg delete mode 100644 docs/public/assets/code.svg delete mode 100644 docs/public/assets/icon-cloud.svg delete mode 100644 docs/public/assets/icon-code.svg delete mode 100644 docs/public/assets/icon-lanuch.svg delete mode 100644 docs/public/assets/quote.svg delete mode 100644 docs/public/assets/sdk.svg delete mode 100644 docs/public/assets/trade.svg create mode 100644 docs/superpowers/plans/2026-09-16-openapi-nginx-decommission.md create mode 100644 functions/[[path]].ts create mode 100644 public/_redirects create mode 100644 public/_routes.json rename {docs/public => public/longbridge}/longbridge-terminal/install (100%) rename {docs/public => public/longbridge}/longbridge-terminal/install.ps1 (100%) create mode 100644 src/integrations/sitemap-locale-files.ts create mode 100644 src/lib/page-meta.test.ts create mode 100644 src/lib/page-meta.ts create mode 100644 src/lib/published-urls.ts create mode 100644 src/pages/404.astro diff --git a/.github/workflows/canary-aliyun.yml b/.github/workflows/canary-aliyun.yml new file mode 100644 index 000000000..8dd8b57c5 --- /dev/null +++ b/.github/workflows/canary-aliyun.yml @@ -0,0 +1,135 @@ +# Canary 验证 —— 阿里云 CDN + OSS 轨(将来承载 open.longbridge.cn)。 +# 把本分支产物发到专用桶 lb-openapi-canary,由 https://open-canary.longbridge.xyz 承载。 +# 该域名已 CNAME 到阿里云 CDN(…w.cdngslb.com),私有 Bucket 回源也已验证通过 +# (响应头 x-oss-cdn-auth: success)——不是早先以为的「纯 OSS 无 CDN」。 +# +# 产物形态:显式设 BUILD_FORMAT=directory。本轨靠 OSS 静态网站托管的 +# 「子目录首页 + 文件404规则=Index」让 /foo 直接 200;而 astro.config.ts 的默认值是 +# 'file',那是 Cloudflare Pages 轨需要的形态(用 directory 会把规范 URL 变成带尾斜杠, +# 与 canonical 自相矛盾,见方案 §15.9)。 +# +# 刻意不复用 canary.yml:那条的上传目标是 lb-assets/github/canary/... 共享前缀, +# 旧 nginx 取扁平 key 且全链路无 --delete,目录索引产物传进去不会报错, +# 而是让现有站点静默冻结在上一次构建。 +# +# ⚠️ 当前阻塞:lb-openapi-canary 的写权限。本流水线用的 AK 属于 RAM 用户 +# lb-assets-github(STS GetCallerIdentity 实测),对该桶仍是 AccessDenied—— +# 运维已加的授权没有落到这个用户上。 +name: Canary · Aliyun CDN + OSS + +on: + # 只手动触发:上面的写权限阻塞未解,挂在 push 上每次都会失败。阻塞解除后再决定挂哪个分支 + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + OSS_BUCKET: oss://lb-openapi-canary + OSS_ENDPOINT: oss-cn-hongkong.aliyuncs.com + SITE_HOSTNAME: https://open-canary.longbridge.xyz + +jobs: + deploy_canary_oss: + name: build and deploy to lb-openapi-canary + timeout-minutes: 30 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Install ossutil + run: | + wget -q https://github.com/aliyun/ossutil/releases/download/v1.7.17/ossutil-v1.7.17-linux-amd64.zip + unzip -q ossutil-v1.7.17-linux-amd64.zip + cp ./ossutil-v1.7.17-linux-amd64/ossutil ${{github.workspace}}/ossutil + chmod +x ${{github.workspace}}/ossutil + ${{github.workspace}}/ossutil --version + + - uses: oven-sh/setup-bun@v2 + + - name: Cache bun dependencies + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} + restore-keys: bun-${{ runner.os }}- + + - name: Install dependency + run: bun install --frozen-lockfile + + - name: Build canary + run: bun run build:canary + env: + # 覆盖 build:canary 的默认站点域名,让 canonical / sitemap / llms.txt + # 都指向验证域名,验证时点链接不会跳出环境 + VITE_SITE_HOSTNAME: ${{ env.SITE_HOSTNAME }} + VITE_PORTAL_GATEWAY_BASE_URL: 'https://mr.longbridge.xyz' + NODE_OPTIONS: --max-old-space-size=14336 --expose-gc + # 本流水线验证的是 OSS 静态网站托管形态,必须显式要目录索引; + # astro.config.ts 的默认值是 'file'(供 Cloudflare Pages 轨用) + BUILD_FORMAT: directory + ASSETS_DIR: _docs # 资源目录分轨,见 astro.config.ts + + - name: Sanity check dist shape + run: | + set -e + echo "--- 目录索引形态抽查 ---" + test -f dist/index.html || { echo "缺 dist/index.html"; exit 1; } + test -f dist/docs/getting-started/index.html || { echo "缺 docs/getting-started/index.html(format:directory 未生效?)"; exit 1; } + test -f dist/longbridge/longbridge-terminal/install || { echo "缺 longbridge/longbridge-terminal/install"; exit 1; } + test -d dist/_docs || { echo "缺 _docs/(assets 命名空间未生效?)"; exit 1; } + echo "--- .md 端点是否仍为扁平文件 ---" + ls dist/docs/getting-started.md + echo "--- 不应再有扁平 HTML ---" + test ! -f dist/docs/getting-started.html || { echo "仍存在扁平 docs/getting-started.html"; exit 1; } + echo "✓ 产物形态符合预期" + + - name: Upload to Aliyun OSS (lb-openapi-canary) + env: + # 优先用按桶授权的专用 AK;没配就回退到现有的 FE_LB_ASSET_*。 + # 后者是配 lb-assets(杭州)用的,对香港的 lb-openapi-canary 有没有写权限取决于 + # 其 RAM 策略是按桶还是账号级——跑一次就知道,失败会是 AccessDenied。 + AK: ${{ secrets.FE_LB_OPENAPI_ACCESS_KEY_ID || secrets.FE_LB_ASSET_ACCESS_KEY_ID }} + SK: ${{ secrets.FE_LB_OPENAPI_ACCESS_KEY_SECRET || secrets.FE_LB_ASSET_ACCESS_KEY_SECRET }} + # 只暴露"用了哪一把",不暴露值;失败时据此判断是凭证没填还是策略没给 + HAS_OPENAPI_AK: ${{ secrets.FE_LB_OPENAPI_ACCESS_KEY_ID != '' && 'yes' || 'no' }} + CRED_SOURCE: ${{ secrets.FE_LB_OPENAPI_ACCESS_KEY_ID != '' && 'FE_LB_OPENAPI_*' || 'FE_LB_ASSET_*(回退)' }} + run: | + set -e + OSS="${{github.workspace}}/ossutil" + BASE="-u -r -j 10 -e $OSS_ENDPOINT -i $AK -k $SK" + echo "--- 凭证来源 ---" + echo " FE_LB_OPENAPI_ACCESS_KEY_ID 已配置: $HAS_OPENAPI_AK" + echo " 本次实际使用: $CRED_SOURCE" + echo " AccessKeyId 长度: ${#AK}" + + echo "--- 写权限预检 ---" + echo ok > /tmp/_probe.txt + if ! $OSS cp /tmp/_probe.txt $OSS_BUCKET/.ci-probe -f -e $OSS_ENDPOINT -i $AK -k $SK; then + echo "::error::$CRED_SOURCE 这把 AK 无法写入 $OSS_BUCKET(region: $OSS_ENDPOINT)。" + if [ "$HAS_OPENAPI_AK" = "no" ]; then + echo "::error::当前走的是回退凭证。若运维是给另一把 AK(如写 whale-assets 那把)放的权," + echo "::error::需要把那把的值填进 Secret FE_LB_OPENAPI_ACCESS_KEY_ID / FE_LB_OPENAPI_ACCESS_KEY_SECRET。" + else + echo "::error::已在用专用 AK 仍被拒,说明该 AK 的 RAM 策略里没有本桶,或本桶不属于该 AK 所在账号。" + fi + exit 1 + fi + $OSS rm $OSS_BUCKET/.ci-probe -f -e $OSS_ENDPOINT -i $AK -k $SK || true + echo "✓ 写权限正常" + # 桶根即站点根,不带任何前缀 + $OSS cp ./dist/ $OSS_BUCKET/ $BASE --exclude "*.html" --exclude "*.md" --exclude "*.txt" --exclude "*.zip" --meta=Cache-Control:max-age=31536000 + $OSS cp ./dist/ $OSS_BUCKET/ $BASE --include "*.zip" --meta "Cache-Control:no-cache#Content-type:application/zip;charset=utf-8" + $OSS cp ./dist/ $OSS_BUCKET/ $BASE --include "*.md" --meta "Cache-Control:no-cache#Content-type:text/markdown;charset=utf-8" + $OSS cp ./dist/ $OSS_BUCKET/ $BASE --include "*.txt" --meta "Cache-Control:no-cache#Content-type:text/plain;charset=utf-8" + $OSS cp ./dist/ $OSS_BUCKET/ $BASE --include "*.html" --meta "Cache-Control:no-cache#Content-type:text/html;charset=utf-8" + + - name: Summary + run: | + echo "### Canary OSS 部署完成" >> $GITHUB_STEP_SUMMARY + echo "- 桶: \`lb-openapi-canary\` (hongkong)" >> $GITHUB_STEP_SUMMARY + echo "- 验证地址: $SITE_HOSTNAME" >> $GITHUB_STEP_SUMMARY + echo "- commit: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/canary-cloudflare.yml b/.github/workflows/canary-cloudflare.yml new file mode 100644 index 000000000..9b1fa1138 --- /dev/null +++ b/.github/workflows/canary-cloudflare.yml @@ -0,0 +1,167 @@ +# Canary 验证 —— Cloudflare Pages 轨。测试域名 open-canary.longbridge.xyz, +# 文档与 SPA 同域(SPA 由 Function 转发,见 functions/[[path]].ts 的 SITES)。 +# +# 发到 canary 专用 project longbridge-developers-canary,并以它的 **production** +# 对外,而不是做 longbridge-developers 的预览分支:测试域名的 DNS 在 Route 53, +# Cloudflare 对外部 DNS 的自定义域名只路由到 production 分支,挂不到预览分支 +# (官方文档 pages/how-to/custom-branch-aliases 的明文限制)。 +# +# 由 canary 分支触发,与 canary.yml(nginx 版 open.longbridge.xyz)同一分支。git 分支、 +# 部署标签与该 project 的 production 分支三处都叫 canary。 +name: Canary · Cloudflare Pages + +on: + push: + branches: + - canary # 改分支时同步 pack-skills.yml 的 CANARY_REF + paths-ignore: + - '.github/workflows/oss-probe.yml' + - 'docs/superpowers/**' + workflow_dispatch: + inputs: + cf_branch: + description: 'Cloudflare 部署标签。canary = 测试域名那份;其它值发成临时预览' + required: false + default: canary + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CF_ACCOUNT_ID: 335162bab35b74f065a0351212011d5a + CF_PROJECT: longbridge-developers-canary + +jobs: + preview: + name: build & deploy preview + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - uses: oven-sh/setup-bun@v2 + + - name: Cache bun dependencies + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} + restore-keys: bun-${{ runner.os }}- + + - name: Install dependency + run: bun install --frozen-lockfile + + # wrangler 的 --branch 是部署标签。canary 即该 project 的 production(建 project + # 时声明),由测试域名承载;其它标签发成