Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
"labeled",
"ldflags",
"lname",
"lumgr",
"lurl",
"marimo",
"Math",
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
build:
go build -o bin/feishu-github-tracker ./cmd/feishu-github-tracker

# Run the application locally
# Run the application locally (with -reload so panel edits apply live)
run:
go run ./cmd/feishu-github-tracker
go run ./cmd/feishu-github-tracker -reload

# Run tests
test:
Expand Down
31 changes: 28 additions & 3 deletions QUICKSTART.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,32 @@
- [./configs/templates.jsonc](configs/templates.jsonc):默认消息模板(可选:创建/使用 `templates.<自定义名称,如「cn」>.jsonc` 自定义模板)
- 修改后保存,程序会在下一次收到 GitHub Webhook 请求时自动热重载最新配置。

3. 多模板配置(可选)
3. Web 管理面板(可选)

除了手改 YAML,本项目还内置一个 Web 管理面板,可在浏览器里增删改:仓库规则、飞书机器人、服务设置、事件配置、消息模板。

- 面板地址:

```bash
http://localhost:4594/
```

(与 webhook 服务同端口;`/webhook`、`/health` 仍照常工作。)

- 设置管理员账号:
- 默认账号:用户名 `admin` / 密码 `admin`。默认配置文件已带 `panel.password: "admin"`;从老版本升级且未配置面板账号时,也会自动使用 `admin` / `admin`。
- 用户名:默认 `admin`,可在 [./configs/server.yaml](configs/server.yaml) 的 `panel.username` 或环境变量 `PANEL_USERNAME` 中自定义;也可在面板「服务设置」页直接修改。
- 密码(优先级从高到低):
- 环境变量(推荐):`PANEL_PASSWORD=你的密码`
- [./configs/server.yaml](configs/server.yaml) 的 `panel.password`(明文)。**若存在这一项则优先使用它**:启动 / reload 时会自动转为 `password_hash`(覆盖原 hash)、删除该明文行,并补回一行 `# password: "admin"` 注释。
- [./configs/server.yaml](configs/server.yaml) 的 `panel.password_hash`(bcrypt,可用 `htpasswd -bnBC 10 "" 你的密码 | tr -d ':\n' | sed 's/^\$2y/\$2a/'` 生成)
- 在面板「服务设置」页修改用户名或密码:修改密码需先填写「当前密码」校验通过后才生效;新密码会生成 `password_hash`(同时补回 `# password: "admin"` 注释)。保存后下次登录即用新账号,无需重启。

- 面板内的修改保存后会自动 reload 生效(无需等待下一次 webhook,也无需重启);手动编辑 `./configs/` 下的文件则需以 `--reload` 启动或重启进程。端口 / 密钥的改动仍需重启进程。

- 注意:在「消息模板」页保存 `templates.*.jsonc` 会移除文件中的 `//` 注释并按字母重排键(功能不变)。

4. 多模板配置(可选)

如果需要为不同的飞书 bot 配置不同的消息模板(如中英文双语),可以在 `./configs/feishu-bots.yaml` 中指定模板:

Expand All @@ -82,7 +107,7 @@

也可以根据现有的修改并创建新的模版文件 `templates.<自定义名称>.jsonc`,然后在 `feishu-bots.yaml` 中引用。

4. 添加 GitHub Webhook
5. 添加 GitHub Webhook

- 进入你想监听的 GitHub 仓库,点击 `Settings` -> `Webhooks` -> `Add webhook`
- 在 `Payload URL` 中填入你的服务器地址,例如 `http://your-domain-or-ip:4594/webhook`
Expand All @@ -92,7 +117,7 @@
- 点击 `Add webhook` 保存
- **✅ 成功提示**:如果前面的步骤没有错误,几秒钟后你会在飞书群组中收到一条 "GitHub Webhook 添加成功" 的通知(这是 GitHub 发送的 ping 事件)。这表示 Webhook 已正确配置并能正常工作!

5. 简要调试
6. 简要调试

- 若没有收到通知,请检查:
- GitHub Webhook 配置(Payload URL、Secret、事件类型)
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

一个用于接收 GitHub Webhook 并转发到飞书机器人的中间件服务。支持灵活的配置、事件过滤和自定义消息模板。

![logo](./assets/images/logo.png)
![logo](./assets/images/banner.png)

## 写在前面

Expand All @@ -17,14 +17,15 @@
所以,我还是决定直接搓一个给大伙用了。我这边的主要目标是:

- 简单易用:配置简单,Docker Compose 开箱即用,基于 GitHub 的 Webhook 实现
- 可视化管理:内置 Web 管理面板,浏览器里即可增删改仓库规则、飞书机器人、事件、模板等,无需手改 YAML
- 灵活可定制:支持多种事件过滤和自定义消息模板,只要替换现有的 `configs/templates.jsonc` 就可以满足大部分的模版定制需求。
- 高效稳定:使用 Go 语言编写,性能优越
- 安全可靠:支持签名验证,防止伪造请求
- 开源免费:MIT 许可证,欢迎自开分支或者贡献回来(plz)

### TODO

- [ ] 计划添加基于 `html` 模板的 `web` 管理页面(类似 [lumgr](https://github.com/hnrobert/lumgr)), 方便查看日志 / 修改配置 / 监控状态等(目前只能通过修改配置文件和查看日志来维护)
- [x] 已添加基于 `html/template` 的 `web` 管理面板(白色 + `#4EACF8`/`#071C37` 主题,中英双语),可在浏览器中查看日志 / 修改仓库规则、飞书机器人、服务设置、事件与消息模板。详见 [QUICKSTART.md](QUICKSTART.md#3-web-管理面板)。
- [ ] 计划添加更多的事件模板(目前已经包含了大部分常用事件的模板,后续会根据反馈继续完善)

## 支持的 GitHub 事件
Expand Down
File renamed without changes
62 changes: 62 additions & 0 deletions cmd/feishu-github-tracker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"encoding/base64"
"flag"
"fmt"
"io"
Expand All @@ -17,6 +18,7 @@ import (
"github.com/hnrobert/feishu-github-tracker/internal/handler"
"github.com/hnrobert/feishu-github-tracker/internal/logger"
"github.com/hnrobert/feishu-github-tracker/internal/notifier"
"github.com/hnrobert/feishu-github-tracker/internal/panel"
)

func main() {
Expand Down Expand Up @@ -81,6 +83,22 @@ func main() {
h.EnableHotReload(configDir)
}

// Normalize the panel password once at startup: if server.yaml has a
// plaintext panel.password, convert it to password_hash and drop the
// plaintext line. Also run on each hot-reload so manual edits are converted.
if changed, err := panel.NormalizePanelPassword(configDir); err != nil {
logger.Warn("Panel password normalization failed: %v", err)
} else if changed {
logger.Info("Converted panel plaintext password to password_hash")
}
h.OnReload = func(dir string) {
if changed, err := panel.NormalizePanelPassword(dir); err != nil {
logger.Warn("Panel password normalization failed: %v", err)
} else if changed {
logger.Info("Converted panel plaintext password to password_hash")
}
}

// Setup HTTP server
mux := http.NewServeMux()
mux.Handle("/webhook", h)
Expand All @@ -89,6 +107,22 @@ func main() {
w.Write([]byte("OK"))
})

// Mount the web management panel at "/" (ServeMux longest-prefix matching
// keeps /webhook and /health routed to their handlers above). The panel
// resolves admin username/password from server.yaml + env on each login.
panelApp, err := panel.New(panel.Options{
ConfigDir: configDir,
LogDir: logDir,
JWTSecret: resolvePanelSecret(cfg),
OnSave: h.Reload, // reload running config after any panel edit
})
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to initialize panel: %v\n", err)
os.Exit(1)
}
logger.Info("Management panel mounted at / (default login: admin / admin)")
mux.Handle("/", panelApp)

srv := NewServer(cfg, mux)

// Start server in a goroutine
Expand Down Expand Up @@ -209,3 +243,31 @@ func NewServer(cfg *config.Config, handler http.Handler) *http.Server {
IdleTimeout: 60 * time.Second,
}
}

// resolvePanelSecret derives the panel JWT signing secret. Precedence:
// PANEL_JWT_SECRET env > server.yaml panel.secret > nil (the panel then uses
// an ephemeral random secret, which logs everyone out on restart).
//
// Admin username/password are resolved by the panel itself on each login from
// the same sources, so they don't need to be passed at startup.
func resolvePanelSecret(cfg *config.Config) []byte {
secretText := os.Getenv("PANEL_JWT_SECRET")
if secretText == "" {
secretText = cfg.Server.Panel.Secret
}
if secretText == "" {
return nil
}
var secret []byte
if decoded, err := base64.RawURLEncoding.DecodeString(secretText); err == nil {
secret = decoded
} else {
secret = []byte(secretText)
}
if len(secret) < 16 {
pad := make([]byte, 16)
copy(pad, secret)
secret = pad
}
return secret
}
30 changes: 30 additions & 0 deletions configs/server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,33 @@ allowed_sources:
- "github.com"
- "api.github.com"
- "your-github-enterprise-domain.com"

# =========================================
# 管理面板 / Management Panel
# -----------------------------------------
# 可选。配置管理员账号密码后,可在 http://<host>:<port>/ 访问 Web 管理面板。
# Optional. Once an admin account is configured, the web panel is available at http://<host>:<port>/.
#
# 默认账号 / Default account: 用户名 / username = admin,密码 / password = admin
# · 老版本升级时若未配置面板账号,也会自动使用 admin / admin。
# · Upgraders without a panel account also get admin / admin by default.
#
# 设置管理员密码的方式(任选其一)/ Set the admin password (any one):
# 1) 环境变量 / env PANEL_PASSWORD(明文,登录时自动 bcrypt 哈希)—— 推荐 / recommended
# 2) password:明文。若出现这一项,则【优先】使用它。/ plaintext; if present, this TAKES PRIORITY.
# 程序在启动 / reload 时会自动将其转换为 password_hash(覆盖原有 hash),然后删除该明文行。
# On startup / reload it is auto-converted to password_hash (overwriting any existing hash),
# then the plaintext line is removed (and a `# password: "admin"` hint comment is added).
# 3) password_hash:bcrypt 哈希。可用 htpasswd 生成 / generate with htpasswd:
# htpasswd -bnBC 10 "" YOUR_PASSWORD | tr -d ':\n' | sed 's/^\$2y/\$2a/'
#
# 用户名 / Username: 默认 admin;可用 username 自定义,或环境变量 PANEL_USERNAME 覆盖。
# defaults to "admin"; override via username or the PANEL_USERNAME env var.
panel:
enabled: true
username: "admin" # 登录用户名 / login username (default: admin)
password: "admin" # 默认明文密码;启动/reload 时会自动转为 password_hash 并删除此行 / default plaintext; auto-converted to password_hash on load
secret: "change-me-panel-jwt-secret" # JWT 签名密钥;留空则每次启动随机生成(会登出所有人)/ JWT secret; blank => ephemeral per restart



12 changes: 3 additions & 9 deletions configs/templates.cn.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -3528,7 +3528,7 @@
"tag": "div",
"text": {
"tag": "lark_md",
"content": "**作者:** {{sender_link_md | default(sender.login)}}\n**Issue:** #{{issue.number}} {{issue.title}}\n**评论:** {{comment.body}}\n"
"content": "**作者:** {{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**评论:** {{comment.body}}\n"
}
},
{
Expand Down Expand Up @@ -8582,9 +8582,6 @@
"content": "{{pr_body}}"
}
},
{
"tag": "hr"
},
{
"tag": "action",
"actions": [
Expand Down Expand Up @@ -8694,9 +8691,6 @@
"content": "{{pr_body}}"
}
},
{
"tag": "hr"
},
{
"tag": "action",
"actions": [
Expand Down Expand Up @@ -8774,7 +8768,7 @@
"header": {
"title": {
"tag": "plain_text",
"content": " Pull Request 已更新"
"content": "🔄 Pull Request 已更新"
},
"template": "blue"
},
Expand Down Expand Up @@ -13980,4 +13974,4 @@
]
}
}
}
}
12 changes: 3 additions & 9 deletions configs/templates.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -3528,7 +3528,7 @@
"tag": "div",
"text": {
"tag": "lark_md",
"content": "**Author:** {{sender_link_md | default(sender.login)}}\n**Issue:** #{{issue.number}} {{issue.title}}\n**Comment:** {{comment.body}}\n"
"content": "**Author:** {{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**Comment:** {{comment.body}}\n"
}
},
{
Expand Down Expand Up @@ -8625,9 +8625,6 @@
"content": "{{pr_body}}"
}
},
{
"tag": "hr"
},
{
"tag": "action",
"actions": [
Expand Down Expand Up @@ -8737,9 +8734,6 @@
"content": "{{pr_body}}"
}
},
{
"tag": "hr"
},
{
"tag": "action",
"actions": [
Expand Down Expand Up @@ -8817,7 +8811,7 @@
"header": {
"title": {
"tag": "plain_text",
"content": " Pull Request Updated"
"content": "🔄 Pull Request Updated"
},
"template": "blue"
},
Expand Down Expand Up @@ -14023,4 +14017,4 @@
]
}
}
}
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ go 1.21

require (
github.com/gobwas/glob v0.2.3
github.com/golang-jwt/jwt/v5 v5.2.2
golang.org/x/crypto v0.31.0
gopkg.in/yaml.v3 v3.0.1
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
Loading
Loading