优化发布构建资源加载

This commit is contained in:
dushixiang committed 2026-05-07 21:45:22 +08:00
1 parent e3dcdb73cc
commit d3690832f9
16 files changed
+270 -161

No files matched your search

+1 -2
View File
@@ -14,8 +14,7 @@
*~
# 构建产物
# bin/agents/ 不能忽略,Agent Docker 镜像构建需要使用
web/dist/
# bin/agents/ 和 web/dist/ 不能忽略,运行时从本地文件加载
web/node_modules/
# 数据目录
+6 -5
View File
@@ -51,11 +51,12 @@ jobs:
# 验证 index.html 模板代码
- name: 验证 index.html 模板代码
run: |
if grep -q "{{.CustomJS}}" web/dist/index.html && grep -q "{{.CustomCSS}}" web/dist/index.html; then
echo "✓ 自定义模板代码已成功添加到 index.html"
if grep -Fq "__PIKA_CUSTOM_JS__" web/dist/index.html && grep -Fq "__PIKA_CUSTOM_CSS__" web/dist/index.html && grep -Fq "[[.SystemNameZh]]" web/dist/index.html; then
cp web/dist/index.html web/dist/index.html.tmpl
echo "✓ index.html 模板已准备完成"
else
echo "✗ 自定义模板代码未找到,重新执行 patch-html"
go run cmd/patch-html/main.go
echo "✗ index.html 模板代码未找到"
exit 1
fi
# 列出编译产物
@@ -116,4 +117,4 @@ jobs:
echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**平台:** linux/amd64" >> $GITHUB_STEP_SUMMARY
echo "**平台:** linux/amd64" >> $GITHUB_STEP_SUMMARY
+5 -4
View File
@@ -57,11 +57,12 @@ jobs:
# 验证 index.html 模板代码
- name: 验证 index.html 模板代码
run: |
if grep -q "{{.CustomJS}}" web/dist/index.html && grep -q "{{.CustomCSS}}" web/dist/index.html; then
echo "✓ 自定义模板代码已成功添加到 index.html"
if grep -Fq "__PIKA_CUSTOM_JS__" web/dist/index.html && grep -Fq "__PIKA_CUSTOM_CSS__" web/dist/index.html && grep -Fq "[[.SystemNameZh]]" web/dist/index.html; then
cp web/dist/index.html web/dist/index.html.tmpl
echo "✓ index.html 模板已准备完成"
else
echo "✗ 自定义模板代码未找到,重新执行 patch-html"
go run cmd/patch-html/main.go
echo "✗ index.html 模板代码未找到"
exit 1
fi
# 列出编译产物
+3
View File
@@ -15,6 +15,9 @@ ARG TARGETARCH
# 从外部编译的产物复制文件
COPY ./bin/pika-linux-${TARGETARCH} ./pika
COPY ./bin/agents ./bin/agents
COPY ./web/dist ./web/dist
COPY ./web/public/logo.png ./web/public/logo.png
# 暴露端口
EXPOSE 8080
+5 -7
View File
@@ -9,9 +9,8 @@ GIT_REVISION=$(shell git rev-parse HEAD)
GO_VERSION=$(shell go version)
BUILD_TIME=$(shell date +%Y-%m-%d_%H:%M:%S)
# 是否使用最佳压缩(release 时设置为 true)
RELEASE ?= false
UPX_FLAGS=$(if $(filter true,$(RELEASE)),--best,)
# UPX 默认使用快速压缩。可通过 make UPX_FLAGS=... 覆盖。
UPX_FLAGS ?=
# Go 构建参数
LDFLAGS=-s -w -X 'github.com/dushixiang/pika/pkg/version.Version=$(VERSION)' -X 'github.com/dushixiang/pika/pkg/version.AgentVersion=$(AGENT_VERSION)'
@@ -21,7 +20,6 @@ GOFLAGS=CGO_ENABLED=0
# 构建前端
build-web:
cd web && yarn && yarn build
@go run cmd/patch-html/main.go
# 构建服务端(开发)
build-server:
@@ -64,11 +62,11 @@ build-agents:
@echo "All agents compressed successfully!"
@ls -lh bin/agents/
# 构建所有(release版本,使用最佳压缩)
# 构建所有(release版本)
build-release:
make build-web
make build-agents RELEASE=true
make build-servers RELEASE=true
make build-agents
make build-servers
# 清理编译产物
clean:
-14
View File
@@ -1,14 +0,0 @@
package pika
import (
"embed"
"io/fs"
)
//go:embed bin/agents/*
var agentsFS embed.FS
func AgentFS() fs.FS {
sub, _ := fs.Sub(agentsFS, "bin/agents")
return sub
}
-46
View File
@@ -1,46 +0,0 @@
package main
import (
"fmt"
"os"
"strings"
)
func main() {
// 读取 web/dist/index.html
htmlPath := "web/dist/index.html"
content, err := os.ReadFile(htmlPath)
if err != nil {
fmt.Fprintf(os.Stderr, "读取文件失败: %v\n", err)
os.Exit(1)
}
html := string(content)
// 检查是否已经包含模板代码(避免重复插入)
if strings.Contains(html, "{{.CustomJS}}") {
fmt.Println("模板代码已存在,跳过插入")
return
}
// 在 </head> 前插入模板代码
customTemplate := ` <script type="application/javascript">
{{.CustomJS}}
</script>
<style type="text/css">
{{.CustomCSS}}
</style>
`
// 替换 </head>
newHTML := strings.Replace(html, "</head>", customTemplate+"</head>", 1)
// 写回文件
err = os.WriteFile(htmlPath, []byte(newHTML), 0644)
if err != nil {
fmt.Fprintf(os.Stderr, "写入文件失败: %v\n", err)
os.Exit(1)
}
fmt.Println("✓ 成功添加自定义模板到 index.html")
}
+16 -32
View File
@@ -1,26 +1,22 @@
package internal
import (
"bytes"
"context"
"fmt"
"log"
"net/http"
"strings"
"text/template"
"time"
"github.com/dushixiang/pika/internal/assets"
"github.com/dushixiang/pika/internal/config"
"github.com/dushixiang/pika/internal/handler"
"github.com/dushixiang/pika/internal/migrate"
"github.com/dushixiang/pika/internal/models"
"github.com/dushixiang/pika/internal/scheduler"
"github.com/dushixiang/pika/internal/service"
"github.com/dushixiang/pika/pkg/replace"
"github.com/dushixiang/pika/pkg/version"
"github.com/dushixiang/pika/web"
"github.com/google/uuid"
"github.com/spf13/afero/mem"
"github.com/go-errors/errors"
"github.com/go-orz/orz"
@@ -111,12 +107,14 @@ func setup(app *orz.App) error {
go components.PublicIPService.Run(ctx)
// 设置API
setupApi(app, components)
if err := setupApi(app, components); err != nil {
return err
}
return nil
}
func setupApi(app *orz.App, components *AppComponents) {
func setupApi(app *orz.App, components *AppComponents) error {
logger := app.Logger()
e := app.GetEcho()
@@ -130,10 +128,11 @@ func setupApi(app *orz.App, components *AppComponents) {
},
}))
indexTemplate, err := template.New("index").Parse(web.IndexHtml())
if err != nil {
logger.Fatal("failed to parse index.html", zap.Error(err))
webDir := assets.WebDir()
if err := assets.RenderUIFilesInDir(webDir, components.PropertyService); err != nil {
return fmt.Errorf("render ui files: %w", err)
}
// 静态文件服务
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Skipper: func(c echo.Context) bool {
@@ -145,34 +144,17 @@ func setupApi(app *orz.App, components *AppComponents) {
if strings.HasPrefix(c.Request().RequestURI, "/ws") {
return true
}
// 不暴露模板文件
if strings.HasSuffix(c.Request().RequestURI, ".tmpl") {
return true
}
return false
},
Index: "index.html",
HTML5: true,
Browse: false,
IgnoreBase: false,
Filesystem: replace.FS(http.FS(web.Assets()), func(name string, file http.File) (http.File, error) {
if name == "index.html" {
fileData := mem.CreateFile(name)
fileHandle := mem.NewFileHandle(fileData)
systemConfig, err := components.PropertyService.GetSystemConfig(context.Background())
if err != nil {
return file, nil
}
var buf bytes.Buffer
err = indexTemplate.Execute(&buf, systemConfig)
if err != nil {
return file, err
}
if _, err := fileHandle.Write(buf.Bytes()); err != nil {
return nil, err
}
return fileHandle, nil
}
return file, nil
}),
Filesystem: http.Dir(webDir),
}))
customValidator := CustomValidator{Validator: validator.New()}
@@ -321,6 +303,8 @@ func setupApi(app *orz.App, components *AppComponents) {
// GitHub 认证路由(如果启用)
publicApi.POST("/auth/github/callback", components.AccountHandler.GitHubLogin)
return nil
}
func autoMigrate(database *gorm.DB) error {
+48
View File
@@ -0,0 +1,48 @@
package assets
import (
"encoding/base64"
"os"
"path/filepath"
)
const (
defaultWebDir = "web/dist"
defaultAgentDir = "bin/agents"
defaultLogoPath = "web/public/logo.png"
)
func WebDir() string {
if dir := os.Getenv("PIKA_WEB_DIR"); dir != "" {
return dir
}
return defaultWebDir
}
func AgentDir() string {
if dir := os.Getenv("PIKA_AGENT_DIR"); dir != "" {
return dir
}
return defaultAgentDir
}
func AgentPath(filename string) string {
return filepath.Join(AgentDir(), filename)
}
func DefaultLogoBase64() string {
for _, path := range []string{
os.Getenv("PIKA_DEFAULT_LOGO_PATH"),
defaultLogoPath,
filepath.Join(WebDir(), "logo.png"),
} {
if path == "" {
continue
}
logo, err := os.ReadFile(path)
if err == nil && len(logo) > 0 {
return "data:image/png;base64," + base64.StdEncoding.EncodeToString(logo)
}
}
return ""
}
+81
View File
@@ -0,0 +1,81 @@
package assets
import (
"context"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/dushixiang/pika/internal/models"
)
type SystemConfigProvider interface {
GetSystemConfig(context.Context) (*models.SystemConfig, error)
}
func RenderUIFiles(provider SystemConfigProvider) error {
return RenderUIFilesInDir(WebDir(), provider)
}
func RenderUIFilesInDir(dir string, provider SystemConfigProvider) error {
if err := ensureIndexTemplate(dir); err != nil {
return err
}
systemConfig, err := provider.GetSystemConfig(context.Background())
if err != nil {
return err
}
return renderIndexHTML(dir, systemConfig)
}
func ensureIndexTemplate(dir string) error {
tmplPath := filepath.Join(dir, "index.html.tmpl")
if _, err := os.Stat(tmplPath); err == nil {
return nil
} else if !os.IsNotExist(err) {
return err
}
indexPath := filepath.Join(dir, "index.html")
src, err := os.ReadFile(indexPath)
if err != nil {
return err
}
return os.WriteFile(tmplPath, src, 0644)
}
func renderIndexHTML(dir string, systemConfig *models.SystemConfig) error {
tmplPath := filepath.Join(dir, "index.html.tmpl")
tmpl, err := template.New(filepath.Base(tmplPath)).Delims("[[", "]]").ParseFiles(tmplPath)
if err != nil {
return err
}
data := struct {
*models.SystemConfig
}{
SystemConfig: systemConfig,
}
outPath := filepath.Join(dir, "index.html")
var rendered strings.Builder
if err := tmpl.Execute(&rendered, data); err != nil {
return err
}
html := strings.ReplaceAll(rendered.String(), "/*__PIKA_CUSTOM_JS__*/", systemConfig.CustomJS)
html = strings.ReplaceAll(html, "/*__PIKA_CUSTOM_CSS__*/", systemConfig.CustomCSS)
out, err := os.Create(outPath)
if err != nil {
return err
}
defer out.Close()
_, err = out.WriteString(html)
return err
}
+75
View File
@@ -0,0 +1,75 @@
package assets
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"github.com/dushixiang/pika/internal/models"
)
type testSystemConfigProvider struct {
config *models.SystemConfig
}
func (p testSystemConfigProvider) GetSystemConfig(context.Context) (*models.SystemConfig, error) {
return p.config, nil
}
func TestRenderUIFilesInDir(t *testing.T) {
dir := t.TempDir()
indexPath := filepath.Join(dir, "index.html")
src := `<!doctype html>
<title>[[.SystemNameZh]][[if and .SystemNameZh .SystemNameEn]] | [[end]][[.SystemNameEn]]</title>
<script>
window.SystemConfig = {
SystemNameZh: "[[.SystemNameZh]]",
SystemNameEn: "[[.SystemNameEn]]",
ICPCode: "[[.ICPCode]]",
DefaultView: "[[.DefaultView]]",
Version: "[[.Version]]",
};
</script>
<script>/*__PIKA_CUSTOM_JS__*/</script>
<style>/*__PIKA_CUSTOM_CSS__*/</style>`
if err := os.WriteFile(indexPath, []byte(src), 0644); err != nil {
t.Fatal(err)
}
provider := testSystemConfigProvider{
config: &models.SystemConfig{
SystemNameZh: "皮卡监控",
SystemNameEn: "Pika Monitor",
ICPCode: "ICP-1",
DefaultView: "grid",
CustomJS: `console.log("pika");`,
CustomCSS: `body { color: red; }`,
Version: "v1.2.3",
},
}
if err := RenderUIFilesInDir(dir, provider); err != nil {
t.Fatal(err)
}
rendered, err := os.ReadFile(indexPath)
if err != nil {
t.Fatal(err)
}
html := string(rendered)
for _, want := range []string{
"<title>皮卡监控 | Pika Monitor</title>",
`SystemNameZh: "皮卡监控"`,
`Version: "v1.2.3"`,
`console.log("pika");`,
`body { color: red; }`,
} {
if !strings.Contains(html, want) {
t.Fatalf("rendered index.html missing %q:\n%s", want, html)
}
}
if _, err := os.Stat(filepath.Join(dir, "index.html.tmpl")); err != nil {
t.Fatal(err)
}
}
+8 -3
View File
@@ -3,9 +3,10 @@ package handler
import (
"fmt"
"net/http"
"os"
"strings"
"github.com/dushixiang/pika"
"github.com/dushixiang/pika/internal/assets"
"github.com/dushixiang/pika/pkg/version"
"github.com/go-orz/orz"
"github.com/labstack/echo/v4"
@@ -22,6 +23,9 @@ func (h *AgentHandler) GetAgentVersion(c echo.Context) error {
// DownloadAgent 下载 Agent 二进制文件
func (h *AgentHandler) DownloadAgent(c echo.Context) error {
filename := c.Param("filename")
if filename == "" || strings.ContainsAny(filename, `/\`) {
return orz.NewError(400, "无效的 Agent 文件名")
}
// 校验 API Key
apiKey := c.QueryParam("key")
@@ -43,12 +47,13 @@ func (h *AgentHandler) DownloadAgent(c echo.Context) error {
h.logger.Info("download agent allowed by ip whitelist", zap.String("ip", clientIP), zap.String("filename", filename))
}
// 从嵌入的文件系统读取
agentFile, err := pika.AgentFS().Open(fmt.Sprintf("pika-%s", filename))
agentFilename := fmt.Sprintf("pika-%s", filename)
agentFile, err := os.Open(assets.AgentPath(agentFilename))
if err != nil {
h.logger.Error("agent binary not found", zap.String("filename", filename), zap.Error(err))
return orz.NewError(404, "未找到对应平台的 Agent 二进制文件")
}
defer agentFile.Close()
// 设置响应头
c.Response().Header().Set("Content-Type", "application/octet-stream")
+7
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"net/http"
"github.com/dushixiang/pika/internal/assets"
"github.com/dushixiang/pika/internal/models"
"github.com/dushixiang/pika/internal/service"
"github.com/go-orz/orz"
@@ -88,6 +89,12 @@ func (h *PropertyHandler) SetProperty(c echo.Context) error {
h.logger.Error("设置属性失败", zap.String("id", id), zap.Error(err))
return orz.NewError(500, "设置属性失败")
}
if id == service.PropertyIDSystemConfig {
if err := assets.RenderUIFiles(h.service); err != nil {
h.logger.Error("渲染前端模板失败", zap.String("id", id), zap.Error(err))
return orz.NewError(500, "渲染前端模板失败")
}
}
return orz.Ok(c, orz.Map{})
}
+2 -2
View File
@@ -6,10 +6,10 @@ import (
"fmt"
"time"
"github.com/dushixiang/pika/internal/assets"
"github.com/dushixiang/pika/internal/models"
"github.com/dushixiang/pika/internal/repo"
"github.com/dushixiang/pika/pkg/version"
"github.com/dushixiang/pika/web"
"github.com/go-orz/cache"
"go.uber.org/zap"
"gorm.io/gorm"
@@ -347,7 +347,7 @@ func (s *PropertyService) InitializeDefaultConfigs(ctx context.Context) error {
Value: models.SystemConfig{
SystemNameZh: "皮卡监控",
SystemNameEn: "Pika Monitor",
LogoBase64: web.DefaultLogoBase64(),
LogoBase64: assets.DefaultLogoBase64(),
ICPCode: "",
DefaultView: "grid",
},
-39
View File
@@ -1,39 +0,0 @@
package web
import (
"embed"
"encoding/base64"
"io/fs"
)
//go:embed dist/*
var distFS embed.FS
func Assets() fs.FS {
sub, _ := fs.Sub(distFS, "dist")
return sub
}
//go:embed dist/index.html
var indexHtml string
func IndexHtml() string {
return indexHtml
}
//go:embed public/logo.png
var defaultLogo []byte
func DefaultLogo() []byte {
return defaultLogo
}
// DefaultLogoBase64 返回默认 Logo 的 base64 编码(data URI 格式)
func DefaultLogoBase64() string {
if len(defaultLogo) == 0 {
return ""
}
// 编码为 base64 并添加 data URI 前缀
encoded := base64.StdEncoding.EncodeToString(defaultLogo)
return "data:image/png;base64," + encoded
}
+13 -7
View File
@@ -5,16 +5,22 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/api/logo" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{.SystemNameZh}}{{if and .SystemNameZh .SystemNameEn}} | {{end}}{{.SystemNameEn}}</title>
<title>[[.SystemNameZh]][[if and .SystemNameZh .SystemNameEn]] | [[end]][[.SystemNameEn]]</title>
<script type="application/javascript">
window.SystemConfig = {
SystemNameZh: "{{.SystemNameZh}}",
SystemNameEn: "{{.SystemNameEn}}",
ICPCode: "{{.ICPCode}}",
DefaultView: "{{.DefaultView}}",
Version: "{{.Version}}",
SystemNameZh: "[[.SystemNameZh]]",
SystemNameEn: "[[.SystemNameEn]]",
ICPCode: "[[.ICPCode]]",
DefaultView: "[[.DefaultView]]",
Version: "[[.Version]]",
};
</script>
<script type="application/javascript">
/*__PIKA_CUSTOM_JS__*/
</script>
<style>
/*__PIKA_CUSTOM_CSS__*/
</style>
</head>
<body>
@@ -22,4 +28,4 @@
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
</html>