From 45f91eee15cedbc6d0764703b14292b19f8efc53 Mon Sep 17 00:00:00 2001 From: Shangxin Date: Mon, 7 Sep 2026 01:18:28 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9F=9F=E5=90=8D=E9=A1=B5=E5=8A=A0?= =?UTF-8?q?=E3=80=8C=E5=8E=BB=E7=BB=91=E5=AE=9A=E3=80=8D=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E2=80=94=E2=80=94=E4=B8=80=E9=94=AE=E7=9B=B4=E8=BE=BE=E5=8F=8D?= =?UTF-8?q?=E4=BB=A3=20Worker=20=E7=9A=84=20Domains=20&=20Routes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 页面原本只读:列出绑定却不管下一步。现在 configured 响应带上 accountId (非机密,本就在每个 dash URL 里),前端用它拼 dash.cloudflare.com/{account}/workers/services/view/{script}/production/domains 的外链,放在「重新读取」旁;缺 accountId 就不显示,不猜。 token 仍永不进响应,测试钉死两条边界。 Co-Authored-By: Claude --- workers/admin-panel/src/api/domains.ts | 7 +++++ workers/admin-panel/src/domains.test.ts | 11 ++++++++ workers/admin-panel/web/src/lib/api.ts | 2 ++ workers/admin-panel/web/src/lib/messages.ts | 1 + .../web/src/views/domains-view.tsx | 27 ++++++++++++++++++- 5 files changed, 47 insertions(+), 1 deletion(-) diff --git a/workers/admin-panel/src/api/domains.ts b/workers/admin-panel/src/api/domains.ts index ce1433c..89ddb53 100644 --- a/workers/admin-panel/src/api/domains.ts +++ b/workers/admin-panel/src/api/domains.ts @@ -79,6 +79,12 @@ export interface DomainsResponse { readonly reason?: UnconfiguredReason; /** Script name the answer is about, so the UI never has to guess. */ readonly script?: string; + /** + * The Cloudflare account the answer was read from. Not a secret — it appears + * in every dashboard URL — and exposed only so the UI can build the deep + * link to the Worker's Custom Domains screen. Absent when unconfigured. + */ + readonly accountId?: string; readonly hosts?: readonly HostBinding[]; /** Sources that could not be read, named individually. */ readonly failures?: readonly { readonly source: string; readonly message: string }[]; @@ -245,6 +251,7 @@ export const discoverDomains = async (env: Env, db: D1Database): Promise 0 ? { failures: result.failures } : {}), ...(result.skippedZones === undefined ? {} : { skippedZones: result.skippedZones }), diff --git a/workers/admin-panel/src/domains.test.ts b/workers/admin-panel/src/domains.test.ts index bf612e1..7e93e3b 100644 --- a/workers/admin-panel/src/domains.test.ts +++ b/workers/admin-panel/src/domains.test.ts @@ -216,6 +216,17 @@ describe('GET /api/domains', () => { expect(raw).not.toContain('read-only-token'); }); + it('exposes the account id, which is not a secret, only when configured', async () => { + // The account id is what the UI builds the dashboard deep link from; unlike + // the token it appears in every dashboard URL already. + const appEnv = configured({ workersDev: true }); + expect((await domains(appEnv, await adminAuth(appEnv))).accountId).toBe('acct-1'); + + // Unconfigured responses name the reason but not the account. + const emptyEnv = envWith({ CF_ACCOUNT_ID: undefined, CF_API_TOKEN: undefined }); + expect((await domains(emptyEnv, await adminAuth(emptyEnv))).accountId).toBeUndefined(); + }); + it('marks a discovered host with the route ids that claim it', async () => { const appEnv = configured({ customDomains: [{ hostname: 'mirror.example.com', service: SCRIPT }], diff --git a/workers/admin-panel/web/src/lib/api.ts b/workers/admin-panel/web/src/lib/api.ts index 9f03ce5..bedb995 100644 --- a/workers/admin-panel/web/src/lib/api.ts +++ b/workers/admin-panel/web/src/lib/api.ts @@ -506,6 +506,8 @@ export interface DomainsResponse { readonly configured: boolean; readonly reason?: UnconfiguredReason; readonly script?: string; + /** 读到答案的 Cloudflare 账户。非机密(所有 dash URL 都带着),仅供拼深链;未配置时省略。 */ + readonly accountId?: string; readonly hosts?: readonly HostBinding[]; readonly failures?: readonly { readonly source: string; readonly message: string }[]; readonly skippedZones?: readonly string[]; diff --git a/workers/admin-panel/web/src/lib/messages.ts b/workers/admin-panel/web/src/lib/messages.ts index 725f219..1494bf5 100644 --- a/workers/admin-panel/web/src/lib/messages.ts +++ b/workers/admin-panel/web/src/lib/messages.ts @@ -592,6 +592,7 @@ export const t = { description: '从 Cloudflare 账号读出真正能打到反代的 hostname,并和路由表对一遍。', refresh: '重新读取', refreshing: '读取中…', + goBind: '去绑定', scriptNote: (script: string) => `查询的是 Worker「${script}」的绑定。`, readOnlyNote: '只读查询,不写数据库也不进审计日志。', columns: { diff --git a/workers/admin-panel/web/src/views/domains-view.tsx b/workers/admin-panel/web/src/views/domains-view.tsx index e561172..0e598db 100644 --- a/workers/admin-panel/web/src/views/domains-view.tsx +++ b/workers/admin-panel/web/src/views/domains-view.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { GlobeIcon, RefreshCwIcon, TriangleAlertIcon } from 'lucide-react'; +import { ExternalLinkIcon, GlobeIcon, RefreshCwIcon, TriangleAlertIcon } from 'lucide-react'; import { Alert, AlertAction, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; @@ -82,6 +82,13 @@ const errorTitle = (error: LoadError): string => { /** kinds 表按 BindingKind 三键全覆盖,但服务端将来加新枚举时别渲染出 undefined。 */ const kindLabel = (kind: BindingKind): string => t.domains.kinds[kind] ?? kind; +/** + * Worker 在 Cloudflare 控制台的 Domains & Routes 标签页深链。accountId 非机密 + * (本就出现在每个 dash URL 里),响应里专门为拼这个链接而暴露。 + */ +const bindUrl = (accountId: string, script: string): string => + `https://dash.cloudflare.com/${accountId}/workers/services/view/${encodeURIComponent(script)}/production/domains`; + export const DomainsView = () => { const [data, setData] = React.useState(null); const [error, setError] = React.useState(null); @@ -111,6 +118,24 @@ export const DomainsView = () => { {t.domains.title} {t.domains.description} + {/* 去控制台绑域名是这页读数的下一步;缺 accountId(未配置凭据)就不显示, + 不猜账户。外链用 render 透传成 ,Base UI 的按钮样式跟着走。 */} + {data?.configured === true && data.accountId !== undefined && ( + + )}