Skip to content

Commit ddc893c

Browse files
author
admin
committed
feat: 功能完善 + 开源准备
- 新增 destroy/reset 命令(隧道删除+完全重置) - 新增 version/update 命令(版本管理+自更新) - 新增 selfupdate 模块(GitHub Releases 自动更新) - 新增 goreleaser + release workflow(tag 自动发版) - 新增 Makefile(版本号注入构建) - 新增 Claude Code / OpenClaw Skills - 脱敏: 示例域名改为 example.com - 修正 init 权限引导(DNS 而非 DNS 设置)
1 parent ee94b78 commit ddc893c

12 files changed

Lines changed: 411 additions & 3 deletions

File tree

.claude/skills/cftunnel.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# cftunnel — Cloudflare Tunnel 管理
2+
3+
快速管理 Cloudflare Tunnel 的 CLI 工具,3 条命令搞定内网穿透。
4+
5+
## 安装
6+
7+
```bash
8+
# macOS / Linux
9+
curl -fsSL https://github.com/qingchencloud/cftunnel/releases/latest/download/cftunnel_$(uname -s | tr A-Z a-z)_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') -o /usr/local/bin/cftunnel && chmod +x /usr/local/bin/cftunnel
10+
```
11+
12+
## 快速上手
13+
14+
```bash
15+
cftunnel init # 交互式初始化
16+
cftunnel add myapp 3000 --domain myapp.example.com # 添加路由
17+
cftunnel up # 启动隧道
18+
```
19+
20+
## 命令参考
21+
22+
| 命令 | 说明 |
23+
|------|------|
24+
| `init` | 交互式初始化(支持 `--token`/`--account`/`--name` 非交互模式) |
25+
| `add <名称> <端口> --domain <域名>` | 添加路由(自动创建 CNAME) |
26+
| `remove <名称>` | 删除路由(清理 DNS) |
27+
| `list` | 列出所有路由 |
28+
| `up` | 启动隧道 |
29+
| `down` | 停止隧道 |
30+
| `status` | 查看状态 |
31+
| `destroy [--force]` | 删除隧道 + 所有 DNS 记录 |
32+
| `reset [--force]` | 完全重置(删隧道 + 清本地配置) |
33+
| `install` | 注册系统服务(开机自启) |
34+
| `uninstall` | 卸载系统服务 |
35+
| `logs [-f]` | 查看日志 |
36+
| `version [--check]` | 版本信息 / 检查更新 |
37+
| `update` | 自动更新到最新版 |
38+
39+
## CF API Token 权限
40+
41+
```
42+
帐户 │ Cloudflare Tunnel │ 编辑
43+
区域 │ DNS │ 编辑
44+
区域 │ 区域设置 │ 读取
45+
```

.github/workflows/release.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Release
2+
on:
3+
push:
4+
tags: ["v*"]
5+
6+
permissions:
7+
contents: write
8+
9+
jobs:
10+
release:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
with:
15+
fetch-depth: 0
16+
- uses: actions/setup-go@v5
17+
with:
18+
go-version: stable
19+
- uses: goreleaser/goreleaser-action@v6
20+
with:
21+
args: release --clean
22+
env:
23+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.goreleaser.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
project_name: cftunnel
2+
3+
builds:
4+
- env: [CGO_ENABLED=0]
5+
goos: [darwin, linux]
6+
goarch: [amd64, arm64]
7+
ldflags:
8+
- -s -w -X github.com/qingchencloud/cftunnel/cmd.Version={{.Version}}
9+
10+
archives:
11+
- format: tar.gz
12+
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
13+
14+
checksum:
15+
name_template: checksums.txt
16+
17+
release:
18+
github:
19+
owner: qingchencloud
20+
name: cftunnel

Makefile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
2+
LDFLAGS := -s -w -X github.com/qingchencloud/cftunnel/cmd.Version=$(VERSION)
3+
4+
build:
5+
go build -ldflags "$(LDFLAGS)" -o cftunnel .
6+
7+
clean:
8+
rm -f cftunnel
9+
10+
.PHONY: build clean

cmd/add.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
var addDomain string
1414

1515
func init() {
16-
addCmd.Flags().StringVar(&addDomain, "domain", "", "完整域名 (如 webhook.qrj.ai)")
16+
addCmd.Flags().StringVar(&addDomain, "domain", "", "完整域名 (如 webhook.example.com)")
1717
addCmd.MarkFlagRequired("domain")
1818
rootCmd.AddCommand(addCmd)
1919
}

cmd/destroy.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package cmd
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"fmt"
7+
"os"
8+
"strings"
9+
10+
"github.com/qingchencloud/cftunnel/internal/cfapi"
11+
"github.com/qingchencloud/cftunnel/internal/config"
12+
"github.com/qingchencloud/cftunnel/internal/daemon"
13+
"github.com/spf13/cobra"
14+
)
15+
16+
var destroyForce bool
17+
18+
func init() {
19+
destroyCmd.Flags().BoolVar(&destroyForce, "force", false, "跳过确认")
20+
rootCmd.AddCommand(destroyCmd)
21+
}
22+
23+
var destroyCmd = &cobra.Command{
24+
Use: "destroy",
25+
Short: "删除隧道(清理所有 DNS 记录 + 删除 CF 隧道)",
26+
RunE: func(cmd *cobra.Command, args []string) error {
27+
cfg, err := config.Load()
28+
if err != nil {
29+
return err
30+
}
31+
if cfg.Tunnel.ID == "" {
32+
return fmt.Errorf("未初始化,无隧道可删除")
33+
}
34+
35+
if !destroyForce {
36+
fmt.Printf("即将删除隧道 %s (%s) 及其 %d 条路由,此操作不可恢复!\n", cfg.Tunnel.Name, cfg.Tunnel.ID, len(cfg.Routes))
37+
fmt.Print("确认删除?(y/N): ")
38+
reader := bufio.NewReader(os.Stdin)
39+
input, _ := reader.ReadString('\n')
40+
if strings.TrimSpace(strings.ToLower(input)) != "y" {
41+
fmt.Println("已取消")
42+
return nil
43+
}
44+
}
45+
46+
// 停止运行中的进程
47+
if daemon.Running() {
48+
fmt.Println("正在停止隧道...")
49+
daemon.Stop()
50+
}
51+
52+
client := cfapi.New(cfg.Auth.APIToken, cfg.Auth.AccountID)
53+
ctx := context.Background()
54+
55+
// 删除所有 DNS 记录
56+
for _, r := range cfg.Routes {
57+
if r.DNSRecordID != "" && r.ZoneID != "" {
58+
fmt.Printf("删除 DNS: %s\n", r.Hostname)
59+
if err := client.DeleteDNSRecord(ctx, r.ZoneID, r.DNSRecordID); err != nil {
60+
fmt.Printf(" 警告: %v\n", err)
61+
}
62+
}
63+
}
64+
65+
// 删除隧道
66+
fmt.Println("删除隧道...")
67+
if err := client.DeleteTunnel(ctx, cfg.Tunnel.ID); err != nil {
68+
fmt.Printf("警告: %v\n", err)
69+
}
70+
71+
// 清空配置
72+
cfg.Tunnel = config.TunnelConfig{}
73+
cfg.Routes = nil
74+
if err := cfg.Save(); err != nil {
75+
return err
76+
}
77+
78+
fmt.Println("隧道已删除,配置已清空")
79+
return nil
80+
},
81+
}

cmd/reset.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package cmd
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"os"
7+
"strings"
8+
9+
"github.com/qingchencloud/cftunnel/internal/config"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
var resetForce bool
14+
15+
func init() {
16+
resetCmd.Flags().BoolVar(&resetForce, "force", false, "跳过确认")
17+
rootCmd.AddCommand(resetCmd)
18+
}
19+
20+
var resetCmd = &cobra.Command{
21+
Use: "reset",
22+
Short: "重置全部(删除隧道 + 清除本地配置)",
23+
RunE: func(cmd *cobra.Command, args []string) error {
24+
if !resetForce {
25+
fmt.Println("即将删除隧道并清除所有本地配置,此操作不可恢复!")
26+
fmt.Print("确认重置?(y/N): ")
27+
reader := bufio.NewReader(os.Stdin)
28+
input, _ := reader.ReadString('\n')
29+
if strings.TrimSpace(strings.ToLower(input)) != "y" {
30+
fmt.Println("已取消")
31+
return nil
32+
}
33+
}
34+
35+
// 先执行 destroy 逻辑(如果有隧道)
36+
cfg, _ := config.Load()
37+
if cfg != nil && cfg.Tunnel.ID != "" {
38+
destroyForce = true
39+
if err := destroyCmd.RunE(cmd, nil); err != nil {
40+
fmt.Printf("警告: 删除隧道失败: %v\n", err)
41+
}
42+
}
43+
44+
// 删除整个配置目录
45+
dir := config.Dir()
46+
if err := os.RemoveAll(dir); err != nil {
47+
return fmt.Errorf("清除配置目录失败: %w", err)
48+
}
49+
50+
fmt.Printf("已清除 %s,回到全新状态\n", dir)
51+
fmt.Println("重新开始: cftunnel init")
52+
return nil
53+
},
54+
}

cmd/root.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ import (
66
"github.com/spf13/cobra"
77
)
88

9+
var Version = "dev"
10+
911
var rootCmd = &cobra.Command{
10-
Use: "cftunnel",
11-
Short: "Cloudflare Tunnel 一键管理工具",
12+
Use: "cftunnel",
13+
Short: "Cloudflare Tunnel 一键管理工具",
14+
Version: Version,
1215
}
1316

1417
func Execute() {

cmd/selfupdate.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/qingchencloud/cftunnel/internal/selfupdate"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
func init() {
11+
rootCmd.AddCommand(updateCmd)
12+
}
13+
14+
var updateCmd = &cobra.Command{
15+
Use: "update",
16+
Short: "更新 cftunnel 到最新版本",
17+
RunE: func(cmd *cobra.Command, args []string) error {
18+
fmt.Println("正在检查更新...")
19+
latest, err := selfupdate.LatestVersion()
20+
if err != nil {
21+
return fmt.Errorf("检查更新失败: %w", err)
22+
}
23+
if latest == "v"+Version || latest == Version {
24+
fmt.Println("已是最新版本")
25+
return nil
26+
}
27+
fmt.Printf("发现新版本: %s → %s\n", Version, latest)
28+
fmt.Println("正在下载...")
29+
if err := selfupdate.Update(latest); err != nil {
30+
return fmt.Errorf("更新失败: %w", err)
31+
}
32+
fmt.Printf("已更新到 %s\n", latest)
33+
return nil
34+
},
35+
}

cmd/version.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/qingchencloud/cftunnel/internal/selfupdate"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
var checkUpdate bool
11+
12+
func init() {
13+
versionCmd.Flags().BoolVar(&checkUpdate, "check", false, "检查是否有新版本")
14+
rootCmd.AddCommand(versionCmd)
15+
}
16+
17+
var versionCmd = &cobra.Command{
18+
Use: "version",
19+
Short: "显示版本信息",
20+
RunE: func(cmd *cobra.Command, args []string) error {
21+
fmt.Printf("cftunnel %s\n", Version)
22+
if !checkUpdate {
23+
return nil
24+
}
25+
fmt.Println("正在检查更新...")
26+
latest, err := selfupdate.LatestVersion()
27+
if err != nil {
28+
return fmt.Errorf("检查更新失败: %w", err)
29+
}
30+
if latest == "v"+Version || latest == Version {
31+
fmt.Println("已是最新版本")
32+
} else {
33+
fmt.Printf("发现新版本: %s → %s\n", Version, latest)
34+
fmt.Println("运行 cftunnel update 进行更新")
35+
}
36+
return nil
37+
},
38+
}

0 commit comments

Comments
 (0)