feat: 初始化 Wails + React + Go 桌面应用模板
- Wails v2.16.0 + React 19 + TypeScript + Vite 7 - 插件系统(build tag 控制编译,plugin.json 运行时配置) - 支付插件(授权、账号、订单、退款、账单) - 高并发基础设施(任务池、批量处理器、限流器、环形缓冲、管道) - Windows 系统托盘(Win32 API) - CodeGraph 索引 + .memory 记忆包 - 默认图标 assets/icon.png
This commit is contained in:
26
internal/plugins/ai_impl.go
Normal file
26
internal/plugins/ai_impl.go
Normal file
@@ -0,0 +1,26 @@
|
||||
//go:build ai
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type aiPlugin struct {
|
||||
enabled bool
|
||||
}
|
||||
|
||||
func (p *aiPlugin) Name() string { return "ai" }
|
||||
func (p *aiPlugin) Enabled() bool { return p.enabled }
|
||||
func (p *aiPlugin) Init(ctx context.Context) error {
|
||||
fmt.Println("[ai] initialized")
|
||||
return nil
|
||||
}
|
||||
func (p *aiPlugin) Register(app *App) error { return nil }
|
||||
func (p *aiPlugin) Shutdown() error { return nil }
|
||||
|
||||
func init() {
|
||||
cfg, _ := LoadOrDefault()
|
||||
Register(&aiPlugin{enabled: cfg.Plugins["ai"].Enabled})
|
||||
}
|
||||
26
internal/plugins/concurrent_impl.go
Normal file
26
internal/plugins/concurrent_impl.go
Normal file
@@ -0,0 +1,26 @@
|
||||
//go:build concurrent
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type concurrentPlugin struct {
|
||||
enabled bool
|
||||
}
|
||||
|
||||
func (p *concurrentPlugin) Name() string { return "concurrent" }
|
||||
func (p *concurrentPlugin) Enabled() bool { return p.enabled }
|
||||
func (p *concurrentPlugin) Init(ctx context.Context) error {
|
||||
fmt.Println("[concurrent] initialized")
|
||||
return nil
|
||||
}
|
||||
func (p *concurrentPlugin) Register(app *App) error { return nil }
|
||||
func (p *concurrentPlugin) Shutdown() error { return nil }
|
||||
|
||||
func init() {
|
||||
cfg, _ := LoadOrDefault()
|
||||
Register(&concurrentPlugin{enabled: cfg.Plugins["concurrent"].Enabled})
|
||||
}
|
||||
26
internal/plugins/iconfont_impl.go
Normal file
26
internal/plugins/iconfont_impl.go
Normal file
@@ -0,0 +1,26 @@
|
||||
//go:build iconfont
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type iconfontPlugin struct {
|
||||
enabled bool
|
||||
}
|
||||
|
||||
func (p *iconfontPlugin) Name() string { return "iconfont" }
|
||||
func (p *iconfontPlugin) Enabled() bool { return p.enabled }
|
||||
func (p *iconfontPlugin) Init(ctx context.Context) error {
|
||||
fmt.Println("[iconfont] initialized")
|
||||
return nil
|
||||
}
|
||||
func (p *iconfontPlugin) Register(app *App) error { return nil }
|
||||
func (p *iconfontPlugin) Shutdown() error { return nil }
|
||||
|
||||
func init() {
|
||||
cfg, _ := LoadOrDefault()
|
||||
Register(&iconfontPlugin{enabled: cfg.Plugins["iconfont"].Enabled})
|
||||
}
|
||||
153
internal/plugins/loader.go
Normal file
153
internal/plugins/loader.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Config 插件配置文件结构
|
||||
type Config struct {
|
||||
Version string `json:"version"`
|
||||
Plugins map[string]PluginOption `json:"plugins"`
|
||||
}
|
||||
|
||||
// PluginOption 单个插件配置项
|
||||
type PluginOption struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Description string `json:"description"`
|
||||
// Params 插件专用参数,由插件自己解析
|
||||
Params map[string]interface{} `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
// Loader 插件配置加载器
|
||||
type Loader struct {
|
||||
mu sync.RWMutex
|
||||
cfg *Config
|
||||
path string
|
||||
}
|
||||
|
||||
// NewLoader 创建配置加载器
|
||||
func NewLoader(path string) *Loader {
|
||||
return &Loader{path: path}
|
||||
}
|
||||
|
||||
// Load 从文件加载配置
|
||||
func (l *Loader) Load() (*Config, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
data, err := os.ReadFile(l.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 环境变量覆盖优先级最高
|
||||
if v := os.Getenv("PLUGIN_ICONFONT"); v == "false" || v == "0" {
|
||||
tmp := cfg.Plugins["iconfont"]; tmp.Enabled = false; cfg.Plugins["iconfont"] = tmp
|
||||
}
|
||||
if v := os.Getenv("PLUGIN_AI"); v == "true" || v == "1" {
|
||||
tmp := cfg.Plugins["ai"]; tmp.Enabled = true; cfg.Plugins["ai"] = tmp
|
||||
}
|
||||
if v := os.Getenv("PLUGIN_CONCURRENT"); v == "false" || v == "0" {
|
||||
tmp := cfg.Plugins["concurrent"]; tmp.Enabled = false; cfg.Plugins["concurrent"] = tmp
|
||||
}
|
||||
|
||||
l.cfg = &cfg
|
||||
return l.cfg, nil
|
||||
}
|
||||
|
||||
// IsEnabled 查询插件启用状态
|
||||
func (l *Loader) IsEnabled(name string) bool {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
if l.cfg == nil {
|
||||
return false
|
||||
}
|
||||
pc, ok := l.cfg.Plugins[name]
|
||||
return ok && pc.Enabled
|
||||
}
|
||||
|
||||
// GetEnabledNames 返回所有已启用插件名
|
||||
func (l *Loader) GetEnabledNames() []string {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
if l.cfg == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]string, 0)
|
||||
for name, pc := range l.cfg.Plugins {
|
||||
if pc.Enabled {
|
||||
result = append(result, name)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Save 保存配置
|
||||
func (l *Loader) Save(cfg *Config) error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir := filepath.Dir(l.path)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(l.path, data, 0644)
|
||||
}
|
||||
|
||||
// LoadOrDefault 加载配置,找不到则返回默认值
|
||||
func LoadOrDefault() (*Config, error) {
|
||||
paths := []string{"config/plugin.json", "./plugin.json"}
|
||||
exe, _ := os.Executable()
|
||||
if exe != "" {
|
||||
dir := filepath.Dir(exe)
|
||||
paths = append([]string{
|
||||
filepath.Join(dir, "config", "plugin.json"),
|
||||
filepath.Join(dir, "plugin.json"),
|
||||
}, paths...)
|
||||
}
|
||||
for _, p := range paths {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
loader := NewLoader(p)
|
||||
if cfg, err := loader.Load(); err == nil {
|
||||
return cfg, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultConfig(), nil
|
||||
}
|
||||
|
||||
// EnsureConfig 确保配置文件存在
|
||||
func EnsureConfig() error {
|
||||
_, err := os.Stat("config/plugin.json")
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
cfg, _ := LoadOrDefault()
|
||||
loader := NewLoader("config/plugin.json")
|
||||
if mkErr := os.MkdirAll("config", 0755); mkErr != nil {
|
||||
return mkErr
|
||||
}
|
||||
return loader.Save(cfg)
|
||||
}
|
||||
|
||||
func defaultConfig() *Config {
|
||||
return &Config{
|
||||
Version: "1.0.0",
|
||||
Plugins: map[string]PluginOption{
|
||||
"iconfont": {Enabled: false, Description: "阿里IconFont图标库,支持字体图标"},
|
||||
"ai": {Enabled: false, Description: "AI大模型集成,支持对话、分析等功能"},
|
||||
"concurrent": {Enabled: true, Description: "高并发工具集,任务池、限流器等"},
|
||||
},
|
||||
}
|
||||
}
|
||||
54
internal/plugins/payment_impl.go
Normal file
54
internal/plugins/payment_impl.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// Package plugins — payment 插件实现(带 build tag,未启用时不编译)
|
||||
//go:build payment
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"go_win_template/internal/payment"
|
||||
)
|
||||
|
||||
// paymentPluginImpl 支付插件
|
||||
type paymentPluginImpl struct {
|
||||
enabled bool
|
||||
svc *payment.Service
|
||||
}
|
||||
|
||||
func (p *paymentPluginImpl) Name() string { return "payment" }
|
||||
func (p *paymentPluginImpl) Enabled() bool { return p.enabled }
|
||||
func (p *paymentPluginImpl) Init(ctx context.Context) error {
|
||||
cfg, _ := LoadOrDefault()
|
||||
pcfg := payment.Config{
|
||||
AppID: getStringParam(cfg, "payment", "app_id"),
|
||||
MchID: getStringParam(cfg, "payment", "mch_id"),
|
||||
APIKey: getStringParam(cfg, "payment", "api_key"),
|
||||
BaseURL: getStringParam(cfg, "payment", "base_url"),
|
||||
AutoRefreshToken: true,
|
||||
}
|
||||
p.svc = payment.NewService(pcfg)
|
||||
fmt.Printf("[payment] initialized, base_url=%s\n", pcfg.BaseURL)
|
||||
return nil
|
||||
}
|
||||
func (p *paymentPluginImpl) Register(app *App) error { return nil }
|
||||
func (p *paymentPluginImpl) Shutdown() error { return nil }
|
||||
|
||||
// GetPaymentService 返回支付服务(any 类型,供 App 层类型断言)
|
||||
func (p *paymentPluginImpl) GetPaymentService() any {
|
||||
return p.svc
|
||||
}
|
||||
|
||||
func init() {
|
||||
cfg, _ := LoadOrDefault()
|
||||
Register(&paymentPluginImpl{enabled: cfg.Plugins["payment"].Enabled})
|
||||
}
|
||||
|
||||
func getStringParam(cfg *Config, plugin, key string) string {
|
||||
if opts, ok := cfg.Plugins[plugin]; ok {
|
||||
if v, ok := opts.Params[key].(string); ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
150
internal/plugins/plugins.go
Normal file
150
internal/plugins/plugins.go
Normal file
@@ -0,0 +1,150 @@
|
||||
// Package plugins 插件系统核心接口与注册中心
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Plugin 插件接口,所有插件必须实现
|
||||
type Plugin interface {
|
||||
Name() string
|
||||
Init(ctx context.Context) error
|
||||
Register(app *App) error
|
||||
Shutdown() error
|
||||
Enabled() bool
|
||||
}
|
||||
|
||||
// App 插件可见的应用上下文
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
config map[string]interface{}
|
||||
}
|
||||
|
||||
// NewApp 创建应用上下文
|
||||
func NewApp(ctx context.Context, config map[string]interface{}) *App {
|
||||
return &App{ctx: ctx, config: config}
|
||||
}
|
||||
|
||||
// Registry 插件注册中心(线程安全)
|
||||
type Registry struct {
|
||||
mu sync.Mutex
|
||||
plugins map[string]Plugin
|
||||
}
|
||||
|
||||
var globalRegistry *Registry
|
||||
var once sync.Once
|
||||
|
||||
func getGlobalRegistry() *Registry {
|
||||
once.Do(func() {
|
||||
globalRegistry = &Registry{plugins: make(map[string]Plugin)}
|
||||
})
|
||||
return globalRegistry
|
||||
}
|
||||
|
||||
// Register 全局注册插件(由各插件 build.go 的 init() 调用)
|
||||
func Register(p Plugin) {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.plugins[p.Name()] = p
|
||||
log.Printf("[plugins] registered: %s (enabled=%v)", p.Name(), p.Enabled())
|
||||
}
|
||||
|
||||
// Get 获取已注册插件
|
||||
func Get(name string) Plugin {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.plugins[name]
|
||||
}
|
||||
|
||||
// List 列出所有已注册插件名
|
||||
func List() []string {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
names := make([]string, 0, len(r.plugins))
|
||||
for name := range r.plugins {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// Count 返回已注册插件数
|
||||
func Count() int {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return len(r.plugins)
|
||||
}
|
||||
|
||||
// Start 启动所有已启用的插件
|
||||
func Start(ctx context.Context) error {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
for name, p := range r.plugins {
|
||||
if !p.Enabled() {
|
||||
log.Printf("[plugins] skip disabled: %s", name)
|
||||
continue
|
||||
}
|
||||
if err := p.Init(ctx); err != nil {
|
||||
log.Printf("[plugins] init %s failed: %v", name, err)
|
||||
continue
|
||||
}
|
||||
if err := p.Register(NewApp(ctx, nil)); err != nil {
|
||||
log.Printf("[plugins] register %s failed: %v", name, err)
|
||||
continue
|
||||
}
|
||||
log.Printf("[plugins] started: %s", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown 关闭所有插件
|
||||
func Shutdown() error {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
for name, p := range r.plugins {
|
||||
if err := p.Shutdown(); err != nil {
|
||||
log.Printf("[plugins] shutdown %s error: %v", name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnabledPlugins 返回所有已启用插件列表
|
||||
func EnabledPlugins() []Plugin {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
result := make([]Plugin, 0)
|
||||
for _, p := range r.plugins {
|
||||
if p.Enabled() {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// PrintStatus 打印当前插件状态(调试用)
|
||||
func PrintStatus() {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
fmt.Println("[plugins] current status:")
|
||||
for name, p := range r.plugins {
|
||||
status := "disabled"
|
||||
if p.Enabled() {
|
||||
status = "enabled"
|
||||
}
|
||||
fmt.Printf(" - %s: %s\n", name, status)
|
||||
}
|
||||
if len(r.plugins) == 0 {
|
||||
fmt.Println(" (no plugins registered — check build tags)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user