Merge branch 'wavelet/main' into main

This commit is contained in:
ryan
2026-09-03 10:08:50 +08:00
24 changed files with 217 additions and 91 deletions
+1 -1
View File
@@ -108,7 +108,7 @@ cross-build:
(version=$(or $(VERSION),dev))..." (version=$(or $(VERSION),dev))..."
@mkdir -p bin @mkdir -p bin
docker build \ docker build \
--file docker/Dockerfile.cross \ --file manifest/docker/Dockerfile.cross \
--target export \ --target export \
--build-arg VERSION=$(or $(VERSION),dev) \ --build-arg VERSION=$(or $(VERSION),dev) \
--build-arg BUILD_DATE="$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')" \ --build-arg BUILD_DATE="$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')" \
+1 -1
View File
@@ -213,7 +213,7 @@ wavelet/
├── main.go # 程序入口(委托给 internal/cmd) ├── main.go # 程序入口(委托给 internal/cmd)
├── config.example.yaml # 配置模板 ├── config.example.yaml # 配置模板
├── Makefile # 常用命令(swagger、tidy、license、cross-build) ├── Makefile # 常用命令(swagger、tidy、license、cross-build)
├── docker/ # Docker 镜像构建文件(集成/前端/后端) ├── manifest/ # 项目清单与编排:docker 镜像构建/compose、deploy (k8s)、config 配置
├── docs/ # Swagger 自动生成文档 ├── docs/ # Swagger 自动生成文档
├── frontend/ # Next.js 前端应用 ├── frontend/ # Next.js 前端应用
│ ├── app/ # App Router 页面 │ ├── app/ # App Router 页面
+12 -7
View File
@@ -10,13 +10,18 @@ import (
// SystemConfigDTO represents a system configuration key-value entry. // SystemConfigDTO represents a system configuration key-value entry.
type SystemConfigDTO struct { type SystemConfigDTO struct {
Key string `json:"key"` Key string `json:"key" gorm:"primaryKey;column:key;size:255"`
Value string `json:"value"` Value string `json:"value" gorm:"column:value;type:text"`
Type string `json:"type"` Type string `json:"type" gorm:"column:type;size:50"`
Visibility int `json:"visibility"` Visibility int `json:"visibility" gorm:"column:visibility"`
Description string `json:"description"` Description string `json:"description" gorm:"column:description;size:500"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
}
// TableName returns the default table name for SystemConfigDTO.
func (SystemConfigDTO) TableName() string {
return "w_system_configs"
} }
// SystemConfigService defines the unified contract for querying and mutating system configurations. // SystemConfigService defines the unified contract for querying and mutating system configurations.
+5
View File
@@ -71,6 +71,11 @@ type TaskExecutionDTO struct {
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
} }
// TableName returns the default table name for TaskExecutionDTO.
func (TaskExecutionDTO) TableName() string {
return "w_task_executions"
}
// Canonical triggered_by values persisted on task executions and shown in admin UI. // Canonical triggered_by values persisted on task executions and shown in admin UI.
const ( const (
TaskTriggerSystem = "system" TaskTriggerSystem = "system"
+14 -9
View File
@@ -23,20 +23,25 @@ type UploadMetadataDTO struct {
// UploadDTO represents an uploaded file record. // UploadDTO represents an uploaded file record.
type UploadDTO struct { type UploadDTO struct {
ID uint64 `json:"id"` ID uint64 `json:"id" gorm:"primaryKey"`
UserID uint64 `json:"user_id"` UserID uint64 `json:"user_id" gorm:"index"`
FileName string `json:"file_name"` FileName string `json:"file_name" gorm:"size:255"`
FilePath string `json:"file_path"` FilePath string `json:"file_path" gorm:"size:500"`
MimeType string `json:"mime_type"` MimeType string `json:"mime_type" gorm:"size:100"`
Size int64 `json:"size"` Size int64 `json:"size"`
Hash string `json:"hash"` Hash string `json:"hash" gorm:"size:64"`
Status string `json:"status"` Status string `json:"status" gorm:"type:varchar(20)"`
Type string `json:"type"` Type string `json:"type" gorm:"size:50;index"`
Metadata UploadMetadataDTO `json:"metadata"` Metadata UploadMetadataDTO `json:"metadata" gorm:"serializer:json;type:jsonb"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
} }
// TableName returns the default table name for UploadDTO.
func (UploadDTO) TableName() string {
return "w_uploads"
}
// OpenedUploadDTO encapsulates the retrieved object stream and its metadata. // OpenedUploadDTO encapsulates the retrieved object stream and its metadata.
type OpenedUploadDTO struct { type OpenedUploadDTO struct {
Upload UploadDTO Upload UploadDTO
@@ -29,6 +29,7 @@ func (PublicConfigAdapter) PublicConfig(ctx context.Context) (map[string]string,
// SystemConfigServiceImpl implements contracts.SystemConfigService. // SystemConfigServiceImpl implements contracts.SystemConfigService.
type SystemConfigServiceImpl struct{} type SystemConfigServiceImpl struct{}
// GetByKey retrieves a system config by its unique key.
func (SystemConfigServiceImpl) GetByKey(ctx context.Context, key string) (contracts.SystemConfigDTO, error) { func (SystemConfigServiceImpl) GetByKey(ctx context.Context, key string) (contracts.SystemConfigDTO, error) {
cfg, err := repository.GetSystemConfigByKey(ctx, key) cfg, err := repository.GetSystemConfigByKey(ctx, key)
if err != nil { if err != nil {
@@ -37,6 +38,7 @@ func (SystemConfigServiceImpl) GetByKey(ctx context.Context, key string) (contra
return toSystemConfigDTO(cfg), nil return toSystemConfigDTO(cfg), nil
} }
// ListByKeys retrieves multiple system configs by their keys.
func (SystemConfigServiceImpl) ListByKeys(ctx context.Context, keys []string) (map[string]contracts.SystemConfigDTO, error) { func (SystemConfigServiceImpl) ListByKeys(ctx context.Context, keys []string) (map[string]contracts.SystemConfigDTO, error) {
cfgs, err := repository.ListSystemConfigsByKeys(ctx, keys) cfgs, err := repository.ListSystemConfigsByKeys(ctx, keys)
if err != nil { if err != nil {
@@ -49,6 +51,7 @@ func (SystemConfigServiceImpl) ListByKeys(ctx context.Context, keys []string) (m
return res, nil return res, nil
} }
// ListVisible returns all user-visible system configs.
func (SystemConfigServiceImpl) ListVisible(ctx context.Context) ([]contracts.SystemConfigDTO, error) { func (SystemConfigServiceImpl) ListVisible(ctx context.Context) ([]contracts.SystemConfigDTO, error) {
cfgs, err := repository.ListVisibleSystemConfigs(ctx) cfgs, err := repository.ListVisibleSystemConfigs(ctx)
if err != nil { if err != nil {
@@ -61,6 +64,7 @@ func (SystemConfigServiceImpl) ListVisible(ctx context.Context) ([]contracts.Sys
return res, nil return res, nil
} }
// ListByType returns all system configs belonging to a specific configuration type.
func (SystemConfigServiceImpl) ListByType(ctx context.Context, configType string) ([]contracts.SystemConfigDTO, error) { func (SystemConfigServiceImpl) ListByType(ctx context.Context, configType string) ([]contracts.SystemConfigDTO, error) {
cfgs, err := repository.ListAdminSystemConfigs(ctx, configType) cfgs, err := repository.ListAdminSystemConfigs(ctx, configType)
if err != nil { if err != nil {
@@ -73,22 +77,27 @@ func (SystemConfigServiceImpl) ListByType(ctx context.Context, configType string
return res, nil return res, nil
} }
// GetIntByKey retrieves an integer system config value.
func (SystemConfigServiceImpl) GetIntByKey(ctx context.Context, key string) (int, error) { func (SystemConfigServiceImpl) GetIntByKey(ctx context.Context, key string) (int, error) {
return repository.GetIntByKey(ctx, key) return repository.GetIntByKey(ctx, key)
} }
// GetBoolByKey retrieves a boolean system config value.
func (SystemConfigServiceImpl) GetBoolByKey(ctx context.Context, key string) (bool, error) { func (SystemConfigServiceImpl) GetBoolByKey(ctx context.Context, key string) (bool, error) {
return repository.GetBoolByKey(ctx, key) return repository.GetBoolByKey(ctx, key)
} }
// SaveOrUpdate persists or updates a system config key-value pair.
func (SystemConfigServiceImpl) SaveOrUpdate(ctx context.Context, key, value string) error { func (SystemConfigServiceImpl) SaveOrUpdate(ctx context.Context, key, value string) error {
return repository.SaveOrUpdateSystemConfig(ctx, key, value) return repository.SaveOrUpdateSystemConfig(ctx, key, value)
} }
// InvalidateCache evicts the cache entry for the specified key.
func (SystemConfigServiceImpl) InvalidateCache(ctx context.Context, key string) error { func (SystemConfigServiceImpl) InvalidateCache(ctx context.Context, key string) error {
return repository.InvalidateSystemConfigCache(ctx, key) return repository.InvalidateSystemConfigCache(ctx, key)
} }
// InvalidateAllCaches purges all system configuration cache entries.
func (SystemConfigServiceImpl) InvalidateAllCaches(ctx context.Context) error { func (SystemConfigServiceImpl) InvalidateAllCaches(ctx context.Context) error {
return repository.InvalidateAllSystemConfigCaches(ctx) return repository.InvalidateAllSystemConfigCaches(ctx)
} }
+9 -9
View File
@@ -69,15 +69,15 @@ func (s *uploadServiceImpl) RebuildStats(ctx context.Context) error {
func toUploadDTO(u *models.Upload) contracts.UploadDTO { func toUploadDTO(u *models.Upload) contracts.UploadDTO {
return contracts.UploadDTO{ return contracts.UploadDTO{
ID: u.ID, ID: u.ID,
UserID: u.UserID, UserID: u.UserID,
FileName: u.FileName, FileName: u.FileName,
FilePath: u.FilePath, FilePath: u.FilePath,
MimeType: u.MimeType, MimeType: u.MimeType,
Size: u.FileSize, Size: u.FileSize,
Hash: u.Hash, Hash: u.Hash,
Status: string(u.Status), Status: string(u.Status),
Type: u.Type, Type: u.Type,
Metadata: contracts.UploadMetadataDTO{ Metadata: contracts.UploadMetadataDTO{
Width: u.Metadata.Width, Width: u.Metadata.Width,
Height: u.Metadata.Height, Height: u.Metadata.Height,
@@ -112,30 +112,25 @@ func (s *inprocTaskService) ListExecutions(ctx context.Context, taskType, status
return res, total, nil return res, total, nil
} }
func (s *inprocTaskService) GetExecution(ctx context.Context, id uint64) (*contracts.TaskExecutionDTO, error) { func findExecution(ctx context.Context, query any, args ...any) (*contracts.TaskExecutionDTO, error) {
db := getDB(ctx) db := getDB(ctx)
if db == nil { if db == nil {
return nil, errors.New("driver_inproc_worker: db not initialized") return nil, errors.New("driver_inproc_worker: db not initialized")
} }
var exec taskExecution var exec taskExecution
if err := db.Where("id = ?", id).First(&exec).Error; err != nil { if err := db.Where(query, args...).First(&exec).Error; err != nil {
return nil, err return nil, err
} }
dto := toExecutionDTO(&exec) dto := toExecutionDTO(&exec)
return &dto, nil return &dto, nil
} }
func (s *inprocTaskService) GetExecution(ctx context.Context, id uint64) (*contracts.TaskExecutionDTO, error) {
return findExecution(ctx, "id = ?", id)
}
func (s *inprocTaskService) GetExecutionByTaskID(ctx context.Context, taskID string) (*contracts.TaskExecutionDTO, error) { func (s *inprocTaskService) GetExecutionByTaskID(ctx context.Context, taskID string) (*contracts.TaskExecutionDTO, error) {
db := getDB(ctx) return findExecution(ctx, "task_id = ?", taskID)
if db == nil {
return nil, errors.New("driver_inproc_worker: db not initialized")
}
var exec taskExecution
if err := db.Where("task_id = ?", taskID).First(&exec).Error; err != nil {
return nil, err
}
dto := toExecutionDTO(&exec)
return &dto, nil
} }
func toExecutionDTO(exec *taskExecution) contracts.TaskExecutionDTO { func toExecutionDTO(exec *taskExecution) contracts.TaskExecutionDTO {
+25 -10
View File
@@ -19,6 +19,13 @@ import (
// DefaultFileName is the configuration file looked up when CONFIG_PATH is unset. // DefaultFileName is the configuration file looked up when CONFIG_PATH is unset.
const DefaultFileName = "config.yaml" const DefaultFileName = "config.yaml"
// DefaultCandidates defines the prioritized list of configuration file paths looked up
// when CONFIG_PATH is unset.
var DefaultCandidates = []string{
"manifest/config/config.yaml",
DefaultFileName,
}
// EnvOnlyOrigin is reported by Describe when no configuration file was loaded. // EnvOnlyOrigin is reported by Describe when no configuration file was loaded.
const EnvOnlyOrigin = "<env only>" const EnvOnlyOrigin = "<env only>"
@@ -56,7 +63,7 @@ func NewSource(opts ...Option) (*Source, error) {
s.path = os.Getenv("CONFIG_PATH") s.path = os.Getenv("CONFIG_PATH")
} }
if s.path == "" { if s.path == "" {
s.path = findConfigPath(DefaultFileName) s.path = findConfigPath(DefaultCandidates...)
} }
v := viper.New() v := viper.New()
@@ -107,20 +114,28 @@ func (s *Source) Describe() string {
return s.path return s.path
} }
// findConfigPath searches upward from the working directory so tests and binaries run // findConfigPath searches upward from the working directory through candidate paths so
// from backend/ still find the repository-root configuration file. // tests and binaries run from subdirectories still find configuration files in manifest/config/
func findConfigPath(configPath string) string { // or the repository root.
if _, err := os.Stat(configPath); err == nil { func findConfigPath(candidates ...string) string {
return configPath if len(candidates) == 0 {
return DefaultFileName
}
for _, candidate := range candidates {
if _, err := os.Stat(candidate); err == nil {
return candidate
}
} }
dir := "." dir := "."
for range maxSearchDepth { for range maxSearchDepth {
dir += "/.." dir += "/.."
path := dir + "/" + configPath for _, candidate := range candidates {
if _, err := os.Stat(path); err == nil { path := dir + "/" + candidate
return path if _, err := os.Stat(path); err == nil {
return path
}
} }
} }
return configPath return candidates[0]
} }
@@ -91,3 +91,32 @@ func TestSourcePrefersConfigPathEnvironmentVariable(t *testing.T) {
require.True(t, ok) require.True(t, ok)
assert.Equal(t, ":9100", value) assert.Equal(t, ":9100", value)
} }
func TestSourceFindsManifestConfigInParentDirectory(t *testing.T) {
tempRoot := t.TempDir()
manifestConfigDir := filepath.Join(tempRoot, "manifest", "config")
require.NoError(t, os.MkdirAll(manifestConfigDir, 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(manifestConfigDir, "config.yaml"),
[]byte("app:\n addr: \":9200\"\n"),
0o600,
))
subDir := filepath.Join(tempRoot, "backend", "cmd")
require.NoError(t, os.MkdirAll(subDir, 0o755))
origWd, err := os.Getwd()
require.NoError(t, err)
require.NoError(t, os.Chdir(subDir))
t.Cleanup(func() {
_ = os.Chdir(origWd)
})
t.Setenv("CONFIG_PATH", "")
src, err := config.NewSource()
require.NoError(t, err)
value, ok := src.Lookup("app.addr")
require.True(t, ok)
assert.Equal(t, ":9200", value)
}
@@ -1,21 +1,24 @@
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: template-single name: wavelet
labels: labels:
app: template-single app: wavelet
spec: spec:
replicas: 1 replicas: 1
selector: selector:
matchLabels: matchLabels:
app: template-single app: wavelet
template: template:
metadata: metadata:
labels: labels:
app: template-single app: wavelet
spec: spec:
containers: containers:
- name : main - name: wavelet
image: template-single image: wavelet
imagePullPolicy: Always imagePullPolicy: Always
ports:
- containerPort: 8000
name: http
+2 -2
View File
@@ -1,12 +1,12 @@
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: template-single name: wavelet
spec: spec:
ports: ports:
- port: 80 - port: 80
protocol: TCP protocol: TCP
targetPort: 8000 targetPort: 8000
selector: selector:
app: template-single app: wavelet
@@ -1,14 +1,15 @@
apiVersion: v1 apiVersion: v1
kind: ConfigMap kind: ConfigMap
metadata: metadata:
name: template-single-configmap name: wavelet-configmap
data: data:
config.yaml: | config.yaml: |
server: app:
address: ":8000" env: "development"
openapiPath: "/api.json" addr: ":8000"
swaggerPath: "/swagger" node_id: 1
database:
logger: enabled: false
level : "all" sqlite_path: "data/wavelet.db"
stdout: true redis:
enabled: false
@@ -1,10 +1,10 @@
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: template-single name: wavelet
spec: spec:
template: template:
spec: spec:
containers: containers:
- name : main - name: wavelet
image: template-single:develop image: ghcr.io/rain-kl/wavelet:canary
@@ -5,8 +5,8 @@ resources:
- ../../base - ../../base
- configmap.yaml - configmap.yaml
patchesStrategicMerge: patches:
- deployment.yaml - path: deployment.yaml
namespace: default namespace: default
+74 -13
View File
@@ -1,16 +1,77 @@
FROM loads/alpine:3.8 # syntax=docker/dockerfile:1.7
############################################################################### ARG GO_VERSION=1.25
# INSTALLATION ARG NODE_VERSION=22
############################################################################### ARG ALPINE_VERSION=3.23
ENV WORKDIR /app FROM node:${NODE_VERSION}-alpine AS frontend-builder
ADD resource $WORKDIR/
ADD ./temp/linux_amd64/main $WORKDIR/main
RUN chmod +x $WORKDIR/main
############################################################################### ARG VERSION=dev
# START ARG BUILD_DATE=""
###############################################################################
WORKDIR $WORKDIR ENV PNPM_HOME="/pnpm"
CMD ./main ENV PATH="$PNPM_HOME:$PATH"
ENV NODE_ENV=production
ENV NEXT_STANDALONE_EXPORT=true
ENV NEXT_PUBLIC_APP_VERSION=${VERSION}
ENV NEXT_PUBLIC_APP_BUILD_DATE=${BUILD_DATE}
RUN corepack enable && corepack prepare pnpm@10.10.0 --activate
WORKDIR /workspace/frontend
COPY frontend/package.json frontend/pnpm-lock.yaml frontend/pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile
COPY frontend/ ./
# Assert index.html so a silent export failure cannot ship an API-only binary.
RUN pnpm build:embed && test -f out/index.html
FROM golang:${GO_VERSION}-alpine AS backend-builder
ARG VERSION=dev
ARG BUILD_DATE=""
RUN apk add --no-cache ca-certificates git
WORKDIR /workspace
# Cache dependency layer
COPY backend/go.mod backend/go.sum ./backend/
RUN cd backend && go mod download
COPY . .
# Drop any stale bundle carried in the context, then overlay the fresh export so
# `//go:embed all:dist` in driver_http picks up exactly this frontend build.
RUN rm -rf backend/plugins/drivers/driver_http/dist
COPY --from=frontend-builder /workspace/frontend/out backend/plugins/drivers/driver_http/dist
WORKDIR /workspace/backend
RUN test -f plugins/drivers/driver_http/dist/index.html && \
CGO_ENABLED=0 GOOS=linux go build \
-tags embed_frontend \
-trimpath \
-ldflags="-s -w -X Wavelet/pkg/buildinfo.Version=${VERSION} -X Wavelet/pkg/buildinfo.BuildTime=${BUILD_DATE}" \
-o /out/wavelet \
./main.go
FROM alpine:${ALPINE_VERSION}
ARG TZ=Asia/Shanghai
RUN apk add --no-cache ca-certificates tzdata postgresql-client && \
cp /usr/share/zoneinfo/${TZ} /etc/localtime && \
echo "${TZ}" > /etc/timezone
WORKDIR /app
COPY --from=backend-builder /out/wavelet ./wavelet
COPY docs ./docs
EXPOSE 8000
ENTRYPOINT ["./wavelet"]
CMD ["all"]
@@ -8,18 +8,16 @@
# TARGET_ARCH — restrict to one arch: amd64 | arm64 (default: all) # TARGET_ARCH — restrict to one arch: amd64 | arm64 (default: all)
# #
# Usage: # Usage:
# docker build -f docker/Dockerfile.cross --output ./bin . # docker build -f manifest/docker/Dockerfile.cross --output ./bin .
# docker build -f docker/Dockerfile.cross \ # docker build -f manifest/docker/Dockerfile.cross \
# --build-arg VERSION=v1.2.3 --output ./bin . # --build-arg TARGET_OS=linux --build-arg TARGET_ARCH=amd64 --output ./bin .
# docker build -f docker/Dockerfile.cross \ # docker build -f manifest/docker/Dockerfile.cross \
# --build-arg TARGET_OS=linux --build-arg TARGET_ARCH=arm64 \ # --build-arg TARGET_OS=windows --build-arg TARGET_ARCH=amd64 --output ./bin .
# --output ./bin .
# #
# Or via Makefile: # Or via Makefile:
# make cross-build # make cross-build
# make cross-build VERSION=v1.2.3 # make cross-build VERSION=v1.2.3
# make cross-build GOOS=linux GOARCH=arm64 # make cross-build GOOS=linux GOARCH=arm64
# make cross-build GOOS=darwin
ARG GO_VERSION=1.25 ARG GO_VERSION=1.25
ARG NODE_VERSION=22 ARG NODE_VERSION=22
View File