This commit is contained in:
@@ -2,6 +2,23 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [v1.6.5] - 2026-09-28
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
- **Token 配置连接入口**:应用配置的 Token 区域新增两个入口:
|
||||||
|
- 「测试连接」:后端用 cloudflared 临时启动一次连接,最多等待 25 秒,确认 token 能注册到 Cloudflare 边缘;成功后显示隧道 ID,失败给出明确原因
|
||||||
|
- 「获取 Token」:直达 Cloudflare Zero Trust 面板
|
||||||
|
- 新增 `POST /api/cloudflare/settings/test` 接口;请求体 token 留空时使用已保存的默认 token
|
||||||
|
|
||||||
|
### 变更
|
||||||
|
|
||||||
|
- 版本号提升到 1.6.5;`AgentVersion` 保持 **0.14.1**
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
|
||||||
|
- `go test ./...`、`go vet ./...`、`npm run typecheck`、`npm test`、`npm run build` 通过
|
||||||
|
|
||||||
## [v1.6.4] - 2026-09-28
|
## [v1.6.4] - 2026-09-28
|
||||||
|
|
||||||
### 变更
|
### 变更
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
# Fonu 文档索引
|
# Fonu 文档索引
|
||||||
|
|
||||||
> 维护约定:新增方案 / 审查文档时在下方登记;方案落地后在「状态」列标注实施结果,并给已被推翻的描述加时效说明。
|
> 维护约定:新增方案 / 审查文档时在下方登记;方案落地后在「状态」列标注实施结果,并给已被推翻的描述加时效说明。
|
||||||
> 最近更新:2026-09-28(v1.6.4)
|
> 最近更新:2026-09-28(v1.6.5)
|
||||||
|
|
||||||
## 方案与设计(本目录)
|
## 方案与设计(本目录)
|
||||||
|
|
||||||
|
|||||||
@@ -238,16 +238,16 @@ FONU_VERSION=<旧版本> docker compose up -d --no-deps --force-recreate fonu
|
|||||||
|
|
||||||
## 10. 在线升级验证
|
## 10. 在线升级验证
|
||||||
|
|
||||||
示例页面用于验证升级结果。v1.6.4 的验证流程:
|
示例页面用于验证升级结果。v1.6.5 的验证流程:
|
||||||
|
|
||||||
1. 确认当前线上实例为 v1.3.0,且 `fonu-updater` sidecar 已运行。
|
1. 确认当前线上实例为 v1.3.0,且 `fonu-updater` sidecar 已运行。
|
||||||
2. 确认远端 `main` 分支的 `VERSION` 为 `1.6.4`。
|
2. 确认远端 `main` 分支的 `VERSION` 为 `1.6.5`。
|
||||||
3. 在侧栏看到「一键升级」后点击并确认。
|
3. 在侧栏看到「一键升级」后点击并确认。
|
||||||
4. 等待升级弹窗显示成功,页面自动刷新。
|
4. 等待升级弹窗显示成功,页面自动刷新。
|
||||||
5. 打开侧栏「示例页面」,确认:
|
5. 打开侧栏「示例页面」,确认:
|
||||||
- 页面路由为 `/example`;
|
- 页面路由为 `/example`;
|
||||||
- 页面显示的版本为 `1.6.4`;
|
- 页面显示的版本为 `1.6.5`;
|
||||||
- 页面显示验证标识 `v1.6.4-example-page`;
|
- 页面显示验证标识 `v1.6.5-example-page`;
|
||||||
- 页面显示独立的「本次升级验证」区块;
|
- 页面显示独立的「本次升级验证」区块;
|
||||||
- 「升级验证」显示为「通过」。
|
- 「升级验证」显示为「通过」。
|
||||||
|
|
||||||
|
|||||||
@@ -189,6 +189,22 @@ func (h *CloudflareHandler) SaveAppSettings(w http.ResponseWriter, r *http.Reque
|
|||||||
writeJSON(w, http.StatusOK, settings)
|
writeJSON(w, http.StatusOK, settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *CloudflareHandler) TestToken(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var request struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&request); err != nil {
|
||||||
|
writeError(r, w, http.StatusBadRequest, "请求格式无效")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message, err := h.svc.TestToken(r.Context(), request.Token)
|
||||||
|
if err != nil {
|
||||||
|
writeError(r, w, http.StatusBadGateway, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "message": message})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *CloudflareHandler) DownloadBinary(w http.ResponseWriter, r *http.Request) {
|
func (h *CloudflareHandler) DownloadBinary(w http.ResponseWriter, r *http.Request) {
|
||||||
var request struct {
|
var request struct {
|
||||||
Mirror string `json:"mirror"`
|
Mirror string `json:"mirror"`
|
||||||
|
|||||||
@@ -208,6 +208,7 @@ func newRouterWithSpecs(deps Deps) (http.Handler, []routeSpec) {
|
|||||||
protect("POST /api/cloudflare/binary/download", cf.DownloadBinary)
|
protect("POST /api/cloudflare/binary/download", cf.DownloadBinary)
|
||||||
protect("GET /api/cloudflare/settings", cf.AppSettings)
|
protect("GET /api/cloudflare/settings", cf.AppSettings)
|
||||||
protect("PUT /api/cloudflare/settings", cf.SaveAppSettings)
|
protect("PUT /api/cloudflare/settings", cf.SaveAppSettings)
|
||||||
|
protect("POST /api/cloudflare/settings/test", cf.TestToken)
|
||||||
protect("GET /api/cloudflare/ingress-candidates", cf.IngressCandidates)
|
protect("GET /api/cloudflare/ingress-candidates", cf.IngressCandidates)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fonu/fonu/internal/config"
|
"github.com/fonu/fonu/internal/config"
|
||||||
"github.com/fonu/fonu/internal/fsutil"
|
"github.com/fonu/fonu/internal/fsutil"
|
||||||
@@ -276,6 +277,60 @@ func (s *Service) resolveToken(ctx context.Context, input string) (string, error
|
|||||||
return plain, nil
|
return plain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestToken 用 cloudflared 临时启动一次连接,验证 token 能否注册到 Cloudflare 边缘。
|
||||||
|
func (s *Service) TestToken(ctx context.Context, input string) (string, error) {
|
||||||
|
token, err := s.resolveToken(ctx, input)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
accountTag, tunnelID, tunnelSecret, err := DecodeToken(token)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
binaryPath := s.binaryPath()
|
||||||
|
if _, err := os.Stat(binaryPath); err != nil {
|
||||||
|
return "", fmt.Errorf("cloudflared 未安装,请先在应用配置中下载")
|
||||||
|
}
|
||||||
|
baseDir := filepath.Join(s.cfg.DataDir, "cloudflared")
|
||||||
|
if err := os.MkdirAll(baseDir, 0o755); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
dir, err := os.MkdirTemp(baseDir, "token-test-")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
credsPath := filepath.Join(dir, "creds.json")
|
||||||
|
configPath := filepath.Join(dir, "config.yml")
|
||||||
|
if err := fsutil.WriteFileAtomic(credsPath, []byte(CredentialsJSON(accountTag, tunnelID, tunnelSecret)), 0o600); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
ingress := BuildIngress(nil, defaultOriginService)
|
||||||
|
if err := fsutil.WriteFileAtomic(configPath, []byte(BuildConfig(tunnelID, credsPath, DefaultNetworkSettings(), ingress)), 0o640); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
temp := Tunnel{
|
||||||
|
ID: -1, Name: "token-test", Mode: ModeTokenLocal,
|
||||||
|
Network: DefaultNetworkSettings(), LogPath: filepath.Join(dir, "cloudflared.log"),
|
||||||
|
}
|
||||||
|
if err := s.manager.Start(temp, binaryPath, configPath, ""); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer func() { _ = s.manager.Stop(-1) }()
|
||||||
|
deadline := time.Now().Add(25 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
status := s.manager.Status(-1)
|
||||||
|
if status.Status == "connected" {
|
||||||
|
return fmt.Sprintf("连接成功,隧道 %s 已注册到 Cloudflare 边缘", tunnelID), nil
|
||||||
|
}
|
||||||
|
if status.Status == "failed" {
|
||||||
|
return "", fmt.Errorf("连接失败: %s", status.LastError)
|
||||||
|
}
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("连接超时:25 秒内未注册到 Cloudflare 边缘,请检查网络或 token")
|
||||||
|
}
|
||||||
|
|
||||||
// attachConfig 从 config.yml 回填域名与回源 service,供编辑弹窗使用。
|
// attachConfig 从 config.yml 回填域名与回源 service,供编辑弹窗使用。
|
||||||
func (s *Service) attachConfig(item *Tunnel) {
|
func (s *Service) attachConfig(item *Tunnel) {
|
||||||
data, err := os.ReadFile(item.ConfigPath)
|
data, err := os.ReadFile(item.ConfigPath)
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "fonu-web",
|
"name": "fonu-web",
|
||||||
"version": "1.6.4",
|
"version": "1.6.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "fonu-web",
|
"name": "fonu-web",
|
||||||
"version": "1.6.4",
|
"version": "1.6.5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/commands": "^6.11.1",
|
"@codemirror/commands": "^6.11.1",
|
||||||
"@codemirror/lang-javascript": "^6.2.5",
|
"@codemirror/lang-javascript": "^6.2.5",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "fonu-web",
|
"name": "fonu-web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.6.4",
|
"version": "1.6.5",
|
||||||
"packageManager": "npm@10.9.4",
|
"packageManager": "npm@10.9.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -512,4 +512,9 @@ export const api = {
|
|||||||
getCfSettings: () => request<CfAppSettings>('/api/cloudflare/settings'),
|
getCfSettings: () => request<CfAppSettings>('/api/cloudflare/settings'),
|
||||||
saveCfSettings: (payload: CfAppSettingsInput) =>
|
saveCfSettings: (payload: CfAppSettingsInput) =>
|
||||||
request<CfAppSettings>('/api/cloudflare/settings', { method: 'PUT', body: JSON.stringify(payload) }),
|
request<CfAppSettings>('/api/cloudflare/settings', { method: 'PUT', body: JSON.stringify(payload) }),
|
||||||
|
testCfToken: (token: string) =>
|
||||||
|
request<{ ok: boolean; message: string }>('/api/cloudflare/settings/test', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ token }),
|
||||||
|
}, 60_000),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,6 +133,13 @@
|
|||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</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>
|
||||||
|
</div>
|
||||||
|
<n-alert v-if="tokenTestResult" :type="tokenTestOk ? 'success' : 'error'" :bordered="false" class="cf-mode-alert">
|
||||||
|
{{ tokenTestResult }}
|
||||||
|
</n-alert>
|
||||||
<p class="cf-hint">保存后,新建隧道时可以留空 token,直接使用这里的默认值。</p>
|
<p class="cf-hint">保存后,新建隧道时可以留空 token,直接使用这里的默认值。</p>
|
||||||
|
|
||||||
<n-divider title-placement="left">默认网络设置</n-divider>
|
<n-divider title-placement="left">默认网络设置</n-divider>
|
||||||
@@ -175,6 +182,9 @@ const binaryLoading = ref(false)
|
|||||||
const settingsVisible = ref(false)
|
const settingsVisible = ref(false)
|
||||||
const settingsLoading = ref(false)
|
const settingsLoading = ref(false)
|
||||||
const settingsSaving = ref(false)
|
const settingsSaving = ref(false)
|
||||||
|
const tokenTesting = ref(false)
|
||||||
|
const tokenTestResult = ref('')
|
||||||
|
const tokenTestOk = ref(false)
|
||||||
const appSettings = ref<CfAppSettings>({
|
const appSettings = ref<CfAppSettings>({
|
||||||
default_token_configured: false,
|
default_token_configured: false,
|
||||||
default_token_masked: '',
|
default_token_masked: '',
|
||||||
@@ -395,6 +405,7 @@ async function openSettings() {
|
|||||||
|
|
||||||
async function loadAppSettings() {
|
async function loadAppSettings() {
|
||||||
settingsLoading.value = true
|
settingsLoading.value = true
|
||||||
|
tokenTestResult.value = ''
|
||||||
try {
|
try {
|
||||||
const result = await api.getCfSettings()
|
const result = await api.getCfSettings()
|
||||||
appSettings.value = result
|
appSettings.value = result
|
||||||
@@ -406,6 +417,21 @@ async function loadAppSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function testToken() {
|
||||||
|
tokenTesting.value = true
|
||||||
|
tokenTestResult.value = ''
|
||||||
|
try {
|
||||||
|
const result = await api.testCfToken(settingsForm.value.token)
|
||||||
|
tokenTestOk.value = true
|
||||||
|
tokenTestResult.value = result.message || '连接成功'
|
||||||
|
} catch (error) {
|
||||||
|
tokenTestOk.value = false
|
||||||
|
tokenTestResult.value = error instanceof Error ? error.message : '连接失败'
|
||||||
|
} finally {
|
||||||
|
tokenTesting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveSettings() {
|
async function saveSettings() {
|
||||||
settingsSaving.value = true
|
settingsSaving.value = true
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<n-alert type="success" :bordered="false" class="example-alert" title="v1.6.4 示例页面验证标识已生效">
|
<n-alert type="success" :bordered="false" class="example-alert" title="v1.6.5 示例页面验证标识已生效">
|
||||||
当前页面由 v{{ version }} 提供,验证标识为 <span class="mono">{{ verificationMarker }}</span>。
|
当前页面由 v{{ version }} 提供,验证标识为 <span class="mono">{{ verificationMarker }}</span>。
|
||||||
如果你是在升级后首次看到这行内容,说明前端构建、镜像重建和服务重启均已成功。
|
如果你是在升级后首次看到这行内容,说明前端构建、镜像重建和服务重启均已成功。
|
||||||
</n-alert>
|
</n-alert>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<strong class="example-proof__value">{{ verificationRound }}</strong>
|
<strong class="example-proof__value">{{ verificationRound }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="example-proof__hint">
|
<div class="example-proof__hint">
|
||||||
看到这个新区块,说明前端资源已经从旧版本切换到 v1.6.4。
|
看到这个新区块,说明前端资源已经从旧版本切换到 v1.6.5。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -33,16 +33,16 @@
|
|||||||
<StatCard label="本次验证标识" :value="verificationMarker" sub="用于区分升级前后的前端资源" tone="blue" value-small>
|
<StatCard label="本次验证标识" :value="verificationMarker" sub="用于区分升级前后的前端资源" tone="blue" value-small>
|
||||||
<template #icon><n-icon :component="FlashOutline" /></template>
|
<template #icon><n-icon :component="FlashOutline" /></template>
|
||||||
</StatCard>
|
</StatCard>
|
||||||
<StatCard label="页面状态" value="已更新" sub="v1.6.4 前端资源已加载" tone="teal">
|
<StatCard label="页面状态" value="已更新" sub="v1.6.5 前端资源已加载" tone="teal">
|
||||||
<template #icon><n-icon :component="CheckmarkCircleOutline" /></template>
|
<template #icon><n-icon :component="CheckmarkCircleOutline" /></template>
|
||||||
</StatCard>
|
</StatCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FonuCard title="本次示例页变化" subtitle="用于确认升级后的页面内容确实发生变化">
|
<FonuCard title="本次示例页变化" subtitle="用于确认升级后的页面内容确实发生变化">
|
||||||
<ul class="example-changes">
|
<ul class="example-changes">
|
||||||
<li>页面版本提示更新为 v1.6.4。</li>
|
<li>页面版本提示更新为 v1.6.5。</li>
|
||||||
<li>「本次升级验证」区块和页面状态同步更新为 v1.6.4。</li>
|
<li>「本次升级验证」区块和页面状态同步更新为 v1.6.5。</li>
|
||||||
<li>验证标识更新为 <span class="mono">v1.6.4-example-page</span>。</li>
|
<li>验证标识更新为 <span class="mono">v1.6.5-example-page</span>。</li>
|
||||||
</ul>
|
</ul>
|
||||||
</FonuCard>
|
</FonuCard>
|
||||||
|
|
||||||
@@ -87,8 +87,8 @@ const message = useMessage()
|
|||||||
const version = ref(__APP_VERSION__)
|
const version = ref(__APP_VERSION__)
|
||||||
const checkedAt = ref('')
|
const checkedAt = ref('')
|
||||||
const refreshing = ref(false)
|
const refreshing = ref(false)
|
||||||
const verificationRound = 'v1.6.4'
|
const verificationRound = 'v1.6.5'
|
||||||
const verificationMarker = 'v1.6.4-example-page'
|
const verificationMarker = 'v1.6.5-example-page'
|
||||||
|
|
||||||
async function refreshVersion() {
|
async function refreshVersion() {
|
||||||
refreshing.value = true
|
refreshing.value = true
|
||||||
|
|||||||
Reference in New Issue
Block a user