feat(cloudflare): 改用 Account ID + API Token 创建管理隧道并发布 v1.6.8
CI / verify (push) Canceled after 0s

This commit is contained in:
2026-09-28 18:23:38 +08:00
parent 7acf13583b
commit 8f78aad19b
15 changed files with 308 additions and 237 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "fonu-web",
"version": "1.6.7",
"version": "1.6.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "fonu-web",
"version": "1.6.7",
"version": "1.6.8",
"dependencies": {
"@codemirror/commands": "^6.11.1",
"@codemirror/lang-javascript": "^6.2.5",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "fonu-web",
"private": true,
"version": "1.6.7",
"version": "1.6.8",
"packageManager": "npm@10.9.4",
"type": "module",
"scripts": {
+2
View File
@@ -1100,6 +1100,7 @@ export interface CfTunnelPayload {
}
export interface CfAppSettings {
account_id: string
default_token_configured: boolean
default_token_masked: string
api_token_configured: boolean
@@ -1109,6 +1110,7 @@ export interface CfAppSettings {
}
export interface CfAppSettingsInput {
account_id: string
token: string
api_token: string
mirror: string
+24 -43
View File
@@ -39,19 +39,18 @@
<n-modal v-model:show="editorVisible" preset="card" :style="{ width: 'min(720px, 96vw)' }" :title="editing ? '编辑隧道' : '新建隧道'">
<div class="cf-modal__body">
<n-alert :type="form.mode === 'token-remote' ? 'warning' : 'info'" :bordered="false" class="cf-mode-alert">
{{ modeHint }}
<n-alert type="info" :bordered="false" class="cf-mode-alert">
隧道由 Cloudflare API 创建;Fonu 负责本地配置、进程、日志与健康。
</n-alert>
<n-alert v-if="!appSettings.default_token_configured" type="warning" :bordered="false" class="cf-mode-alert">
还没有默认 Token,请先到「应用配置」保存,再创建隧道。
<n-alert v-if="!appSettings.account_id || !appSettings.api_token_configured" type="warning" :bordered="false" class="cf-mode-alert">
请先在「应用配置」中填写 Account ID 与 API Token,再创建隧道。
</n-alert>
<n-divider title-placement="left">基本信息</n-divider>
<n-form label-placement="top">
<n-form-item label="名称"><n-input v-model:value="form.name" placeholder="例如 home-nas" /></n-form-item>
<n-form-item label="模式"><n-select v-model:value="form.mode" :options="modeOptions" /></n-form-item>
</n-form>
<n-divider title-placement="left">暴露规则</n-divider>
<n-form v-if="form.mode === 'token-local'" label-placement="top">
<n-form label-placement="top">
<n-form-item label="暴露域名">
<n-select v-model:value="form.hostnames" multiple filterable tag :options="hostnameOptions" placeholder="从本机反代规则选择,或直接输入域名" />
</n-form-item>
@@ -59,14 +58,11 @@
<n-input v-model:value="form.origin_service" placeholder="http://127.0.0.1:80" />
</n-form-item>
</n-form>
<n-alert v-else type="warning" :bordered="false" class="cf-mode-alert">
云端规则模式下,本地域名与回源配置不生效,请在 Cloudflare 面板管理 ingress。
</n-alert>
<n-divider title-placement="left">运行</n-divider>
<n-form label-placement="top">
<n-form-item label="随 Fonu 自动启动"><n-switch v-model:value="form.auto_start" /></n-form-item>
</n-form>
<p class="cf-hint">Token 与网络调优统一在「应用配置」中管理,保存后会应用到新建隧道。</p>
<p class="cf-hint">网络调优统一在「应用配置」中管理,保存后会应用到新建隧道。</p>
</div>
<template #footer>
<div class="cf-modal__footer">
@@ -122,17 +118,12 @@
</div>
<p class="cf-hint">下载后会校验文件大小与可执行性,并原子替换现有二进制。国内环境推荐 gh-proxy 镜像。</p>
<n-divider title-placement="left">Token 配置</n-divider>
<n-divider title-placement="left">Cloudflare 账号</n-divider>
<n-form label-placement="top">
<n-form-item label="隧道 Token(Tunnel Token)">
<n-input
v-model:value="settingsForm.token"
type="password"
show-password-on="click"
:placeholder="appSettings.default_token_configured ? `已配置 ${appSettings.default_token_masked},留空保留` : '粘贴 Cloudflare 隧道 token(eyJ...)'"
/>
<n-form-item label="Account ID">
<n-input v-model:value="settingsForm.account_id" :placeholder="appSettings.account_id || '例如 b590ee0f16701d36f2819b0e3c938e81'" />
</n-form-item>
<n-form-item label="API Token(可选,用于连接验证)">
<n-form-item label="API Token">
<n-input
v-model:value="settingsForm.api_token"
type="password"
@@ -143,12 +134,12 @@
</n-form>
<div class="cf-code-toolbar">
<n-button size="small" :loading="tokenTesting" @click="testToken">测试连接</n-button>
<n-button size="small" tag="a" href="https://one.dash.cloudflare.com/" target="_blank" rel="noopener">获取 Token</n-button>
<n-button size="small" tag="a" href="https://dash.cloudflare.com/profile/api-tokens" target="_blank" rel="noopener">获取 API Token</n-button>
</div>
<n-alert v-if="tokenTestResult" :type="tokenTestOk ? 'success' : 'error'" :bordered="false" class="cf-mode-alert">
{{ tokenTestResult }}
</n-alert>
<p class="cf-hint">隧道 Token 用于 cloudflared 运行;API Token 用于验证 Cloudflare 账号连接(格式 cfat_...)。测试连接优先使用 API Token。</p>
<p class="cf-hint">Fonu 使用 Account ID + API Token 调用 Cloudflare API 创建和管理隧道,不再需要手工填写 Tunnel Token。</p>
<n-divider title-placement="left">默认网络设置</n-divider>
<n-form label-placement="top">
@@ -194,6 +185,7 @@ const tokenTesting = ref(false)
const tokenTestResult = ref('')
const tokenTestOk = ref(false)
const appSettings = ref<CfAppSettings>({
account_id: '',
default_token_configured: false,
default_token_masked: '',
api_token_configured: false,
@@ -201,7 +193,7 @@ const appSettings = ref<CfAppSettings>({
mirror: 'official',
network: emptyNetwork(),
})
const settingsForm = ref<CfAppSettingsInput>({ token: '', api_token: '', mirror: 'official', network: emptyNetwork() })
const settingsForm = ref<CfAppSettingsInput>({ account_id: '', token: '', api_token: '', mirror: 'official', network: emptyNetwork() })
const editorVisible = ref(false)
const detailVisible = ref(false)
const editing = ref<CfTunnel | null>(null)
@@ -213,10 +205,6 @@ const detailLoading = ref(false)
const form = ref<CfTunnelPayload>(emptyForm())
const modeOptions = [
{ label: 'Token + 本地规则(Fonu 生成 ingress)', value: 'token-local' },
{ label: 'Token + 云端规则(Cloudflare 面板管理)', value: 'token-remote' },
]
const protocolOptions = [
{ label: 'HTTP/2(国内推荐)', value: 'http2' },
{ label: 'QUIC', value: 'quic' },
@@ -236,14 +224,10 @@ const mirrorOptions = computed(() => {
const list = mirrors.value.length ? mirrors.value : [{ id: 'official', name: 'GitHub 官方源', base: '' }]
return list.map((item) => ({ label: item.name, value: item.id }))
})
const modeHint = computed(() => (form.value.mode === 'token-remote'
? '云端规则模式:cloudflared 用 --token 启动,ingress 由 Cloudflare 面板管理,Fonu 只负责进程、日志与健康。'
: '本地规则模式:Fonu 解码 token 并生成本地 config.yml,域名与回源由下方配置决定。'))
function emptyForm(): CfTunnelPayload {
return {
name: '',
mode: 'token-local',
mode: 'account-local',
token: '',
hostnames: [],
origin_service: 'http://127.0.0.1:80',
@@ -310,16 +294,17 @@ async function saveTunnel() {
message.warning('请填写隧道名称')
return
}
if (!editing.value && !appSettings.value.default_token_configured) {
message.warning('请先在「应用配置」中保存默认 Token')
if (!appSettings.value.account_id || !appSettings.value.api_token_configured) {
message.warning('请先在「应用配置」中填写 Account ID 与 API Token')
return
}
if (form.value.mode === 'token-local' && !form.value.hostnames.length) {
message.warning('本地规则模式至少需要一个暴露域名')
if (!form.value.hostnames.length) {
message.warning('请至少选择一个暴露域名')
return
}
const payload: CfTunnelPayload = {
...form.value,
mode: editing.value ? form.value.mode : 'account-local',
token: '',
network: { ...appSettings.value.network },
}
@@ -419,7 +404,7 @@ async function loadAppSettings() {
try {
const result = await api.getCfSettings()
appSettings.value = result
settingsForm.value = { token: '', api_token: '', mirror: result.mirror || 'official', network: { ...result.network } }
settingsForm.value = { account_id: result.account_id || '', token: '', api_token: '', mirror: result.mirror || 'official', network: { ...result.network } }
} catch (error) {
message.error(error instanceof Error ? error.message : '读取应用配置失败')
} finally {
@@ -430,13 +415,8 @@ async function loadAppSettings() {
async function testToken() {
tokenTesting.value = true
tokenTestResult.value = ''
const apiToken = (settingsForm.value.api_token || '').trim()
const useAPI = apiToken !== '' || appSettings.value.api_token_configured
try {
const result = await api.testCfToken(
useAPI ? apiToken : (settingsForm.value.token || ''),
useAPI ? 'api' : 'tunnel',
)
const result = await api.testCfToken((settingsForm.value.api_token || '').trim(), 'api')
tokenTestOk.value = true
tokenTestResult.value = result.message || '连接成功'
} catch (error) {
@@ -452,7 +432,7 @@ async function saveSettings() {
try {
const result = await api.saveCfSettings(settingsForm.value)
appSettings.value = result
settingsForm.value = { token: '', api_token: '', mirror: result.mirror, network: { ...result.network } }
settingsForm.value = { account_id: result.account_id || '', token: '', api_token: '', mirror: result.mirror, network: { ...result.network } }
message.success('应用配置已保存')
} catch (error) {
message.error(error instanceof Error ? error.message : '保存应用配置失败')
@@ -475,6 +455,7 @@ async function downloadBinary() {
}
function modeText(mode: string) {
if (mode === 'account-local') return 'API 管理'
if (mode === 'token-remote') return '云端规则'
if (mode === 'token-local') return '本地规则'
return mode
+8 -8
View File
@@ -8,7 +8,7 @@
</template>
</PageHeader>
<n-alert type="success" :bordered="false" class="example-alert" title="v1.6.7 示例页面验证标识已生效">
<n-alert type="success" :bordered="false" class="example-alert" title="v1.6.8 示例页面验证标识已生效">
当前页面由 v{{ version }} 提供,验证标识为 <span class="mono">{{ verificationMarker }}</span>。
如果你是在升级后首次看到这行内容,说明前端构建、镜像重建和服务重启均已成功。
</n-alert>
@@ -19,7 +19,7 @@
<strong class="example-proof__value">{{ verificationRound }}</strong>
</div>
<div class="example-proof__hint">
看到这个新区块,说明前端资源已经从旧版本切换到 v1.6.7。
看到这个新区块,说明前端资源已经从旧版本切换到 v1.6.8。
</div>
</div>
@@ -33,16 +33,16 @@
<StatCard label="本次验证标识" :value="verificationMarker" sub="用于区分升级前后的前端资源" tone="blue" value-small>
<template #icon><n-icon :component="FlashOutline" /></template>
</StatCard>
<StatCard label="页面状态" value="已更新" sub="v1.6.7 前端资源已加载" tone="teal">
<StatCard label="页面状态" value="已更新" sub="v1.6.8 前端资源已加载" tone="teal">
<template #icon><n-icon :component="CheckmarkCircleOutline" /></template>
</StatCard>
</div>
<FonuCard title="本次示例页变化" subtitle="用于确认升级后的页面内容确实发生变化">
<ul class="example-changes">
<li>页面版本提示更新为 v1.6.7。</li>
<li>「本次升级验证」区块和页面状态同步更新为 v1.6.7。</li>
<li>验证标识更新为 <span class="mono">v1.6.7-example-page</span>。</li>
<li>页面版本提示更新为 v1.6.8。</li>
<li>「本次升级验证」区块和页面状态同步更新为 v1.6.8。</li>
<li>验证标识更新为 <span class="mono">v1.6.8-example-page</span>。</li>
</ul>
</FonuCard>
@@ -87,8 +87,8 @@ const message = useMessage()
const version = ref(__APP_VERSION__)
const checkedAt = ref('')
const refreshing = ref(false)
const verificationRound = 'v1.6.7'
const verificationMarker = 'v1.6.7-example-page'
const verificationRound = 'v1.6.8'
const verificationMarker = 'v1.6.8-example-page'
async function refreshVersion() {
refreshing.value = true