21 Commits

Author SHA1 Message Date
1f527dce98 refactor(middleware): 调整中间件执行顺序和代码格式化
All checks were successful
ci/woodpecker/tag/woodpecker Pipeline was successful
- 调整中间件执行顺序:将 TimeoutOver 移至 SoundStart 之前,确保超时检查在音频播放前生效
- 简化 BGM 停止逻辑:移除 select 语句中的多余花括号
- 修正导入顺序:将 sync 标准库导入置于第三方库之前

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 16:39:36 +08:00
cbccb07398 feat(audio): 添加音频播放进度监控和停滞检测
为诊断 TTS 音频播放卡死问题,在 PlayWav 函数中添加实时播放进度监控:

- 每秒打印当前播放位置、进度百分比和播放时间
- 检测播放停滞(位置不变时打印警告)
- 改进日志输出,显示总样本数和预计时长
- 移除 select case 中的多余花括号

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 16:38:35 +08:00
b0f07624b0 refactor(cli): 将 cobra 替换为 urfave/cli v3 并添加版本号注入
All checks were successful
ci/woodpecker/tag/woodpecker Pipeline was successful
- 移除 cobra 依赖,使用更轻量的 urfave/cli v3
- 删除 cmd/root.go,将 CLI 逻辑整合到 main.go
- 添加编译时版本号注入(Version 和 Commit)
- 适配 .woodpecker.yml 以支持新的版本号路径
- 代码从 147 行减少到 135 行(净减少 12 行)

版本号现在通过 ldflags 在编译时注入,不再硬编码。
CI 构建时会自动从 Git tag 和 commit SHA 注入版本信息。
2026-04-08 14:44:58 +08:00
e31fca22c8 fix: 修复待机控制器的 context 使用和忙循环问题
- interval: 添加 Sleep 避免默认分支的忙循环(CPU 100%)
- cron: 使用 context.Background() 确保定时任务完整执行,不受外部取消影响
- wait_card: 使用 context.Background() 确保读卡器监听完整执行

这些修复确保了关键操作能够完整运行,同时避免 CPU 资源浪费。
2026-04-08 14:25:56 +08:00
bee3b98798 refactor(tts): 删除 errorsx 包,使用标准库错误处理
## 变更内容

### 1. 删除 errorsx 包
- 删除 pkg/errorsx/handler.go(未使用)
- 删除 pkg/errorsx/error.go(无意义的常量)

### 2. 使用标准库错误处理
- 移除 "game-driver/pkg/errorsx" 导入
- 用 fmt.Errorf 替换所有 errorsx.XxxErr
- 错误信息更清晰,保留完整上下文

### 3. 改进前后对比
```go
// 之前
return errorsx.ThirdPartyErr  //  上下文丢失

// 现在
return fmt.Errorf("获取Token失败: %s", resultMessage.ErrMsg)  //  完整上下文
```

## 优势
 错误信息包含完整上下文
 减少一个自定义包
 使用标准库,更优雅
 代码行数减少:166 → 161 行

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 14:10:41 +08:00
e4c34f0eec refactor(tts): TTS 极简重构与代码质量提升
## 核心改进

### TTS 模块重构
- 统一 API,仅保留 Sound(ctx, text) 方法
- 优化日志,添加 [TTS] 前缀和结构化字段
- 实现互斥等待:同时只播放一个,新请求等待旧播放完成
- 响应 context 取消:超时或断开时立即停止播放
- 移除全局 context 存储,改为参数传递
- 简化实例化:New(config) 无需传入 context

### 代码质量提升
- 修复 PlayWav/PlayMP3 的死循环 bug(context 取消时缺少 return)
- 修复 standby_ctrl/pause.go 的忙循环(添加 Sleep 避免CPU 100%)
- 添加关键路径错误传播(only_video.go 不再忽略播放错误)
- 新增 pkg/errorsx/handler.go 统一错误处理工具

## 代码优化
- TTS 代码从 234 行精简到 166 行(减少 29%)
- 移除冗余状态管理(playing 标志、等待循环)
- 利用互斥锁的阻塞特性实现优雅等待
- 保持简洁易读的代码风格

## 行为说明
 同时只能播放一个 TTS(互斥)
 新请求等待当前播放完成(不打断)
 响应 context 取消(超时停止)
 日志完善,便于排查问题

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 14:05:16 +08:00
2331d0c73f fix(tts): 修复 TTS 播放卡死问题并增强日志
All checks were successful
ci/woodpecker/tag/woodpecker Pipeline was successful
- 添加 SoundWithContext 方法,使用请求 context 而非全局 context
- 修复 TTS 使用服务器全局 context 导致无法取消的问题
- 添加详细的诊断日志(解码、播放、TTS 合成各阶段)
- 检测并记录 TTS 合成数据为空的情况

修复前 TTS 播放使用全局 context,当播放卡住时无法通过超时
或取消机制中断,导致后续任务永远无法执行。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 12:07:39 +08:00
5ee8e15965 fix(audio): 修复音频播放死循环并增强错误日志
All checks were successful
ci/woodpecker/tag/woodpecker Pipeline was successful
- 修复 PlayWav 和 PlayMP3 在 context 取消时的死循环 bug
- 添加 WAV/MP3 解码失败的错误日志
- 添加 TTS 播放开始/完成的日志,便于排查问题

修复前 context 取消会导致无限循环,阻塞后续任务执行。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 11:34:27 +08:00
ae5f998d88 ci: 优化构建配置
All checks were successful
ci/woodpecker/tag/woodpecker Pipeline was successful
- amd64 原生构建移除冗余的 GOOS/GOARCH/CGO/CC 设置
- 删除不需要的 gcc-x86-64-linux-gnu 安装
- 添加构建产物列表便于调试
- 修复 release 步骤的 YAML 缩进

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-07 18:35:55 +08:00
6cf28217a4 ci: 增强多架构构建支持
Some checks failed
ci/woodpecker/tag/woodpecker Pipeline was canceled
- 启用 arm64 多架构和安装 libasound2/libvlc 开发库
- 设置 PKG_CONFIG_PATH 确保交叉编译时依赖正确链接
- 分别配置 amd64 和 arm64 的 pkg-config 路径

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-07 18:22:07 +08:00
e93e99480b ci: 修复 amd64 交叉编译器变量名
Some checks failed
ci/woodpecker/tag/woodpecker Pipeline was canceled
CC 应从 x86-64-linux-gnu-gcc 改为 x86_64-linux-gnu-gcc

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-07 18:13:24 +08:00
619e919fa0 ci: 优化构建速度,添加 Go 代理和 APT 镜像
Some checks failed
ci/woodpecker/tag/woodpecker Pipeline was canceled
- 设置 GOPROXY 为 goproxy.cn 加速依赖下载
- 替换 Debian 源为阿里云镜像加速 apt 安装

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-07 18:07:29 +08:00
809f123854 ci: 升级构建环境至 Go 1.24
Some checks failed
ci/woodpecker/tag/woodpecker Pipeline was canceled
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-07 17:54:23 +08:00
46961040b3 ci: 添加 Woodpecker CI 配置并更新 .gitignore
Some checks failed
ci/woodpecker/tag/woodpecker Pipeline was canceled
- 新增 tag 触发的 amd64/arm64 多架构构建与发布流水线
- .gitignore 忽略 .qwen 目录
- 修复 .gitignore 文件末尾换行符

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-07 17:48:38 +08:00
4349413887 refactor(utils): 禁用屏幕开关并优化 xset 查找逻辑
- 注释掉所有 BlankOpen/BlankClose 调用,启动不再关屏
- 将 xset 路径查找改为 init + sync.Once 缓存,避免重复执行
- 清理未使用的 utils 导入

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-07 16:52:33 +08:00
994023553d chore(deps): 清理未使用的 go 依赖
移除旧版本的 aliyun-sdk、fetchup、x/exp、x/net、x/sys、x/tools 等

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-07 16:52:20 +08:00
dd0c7b8feb chore(config): 更新 wushan-5 点位配置
- 点位从 3 调整为 5
- 更新 MQTT 服务器地址和客户端 ID
- 更新游戏服务器 host 和端口
- 修复文件末尾换行符

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-07 16:52:03 +08:00
2d5d3919e2 降级日志 2025-09-26 16:48:11 +08:00
ac0a338b76 脚本优化 2025-09-25 18:07:59 +08:00
24b2b6c199 增加 fontconfig 包 2025-09-25 11:48:51 +08:00
a314a1a0d8 增加初始化脚本 2025-07-11 17:41:36 +08:00
27 changed files with 998 additions and 266 deletions

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
/logs /logs
/.idea /.idea
/.vscode /.vscode
/.qwen
*.mp3 *.mp3
game-driver* game-driver*

43
.woodpecker.yml Normal file
View File

@@ -0,0 +1,43 @@
when:
- event: tag
clone:
git:
image: docker.m.daocloud.io/woodpeckerci/plugin-git
settings:
depth: 1
steps:
# 构建多架构二进制文件
build:
image: docker.m.daocloud.io/golang:1.24-trixie
environment:
GOPROXY: https://goproxy.cn
commands:
- sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources
# 启用多架构支持并安装交叉编译工具
- dpkg --add-architecture arm64
- apt-get update
- apt-get install -y gcc-aarch64-linux-gnu pkg-config
- apt-get install -y libasound2-dev libvlc-dev
- apt-get install -y libasound2-dev:arm64 libvlc-dev:arm64
- mkdir -p release
# 构建 amd64 (native)
- PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig go build -ldflags="-w -s -X main.Version=${CI_COMMIT_TAG} -X main.Commit=${CI_COMMIT_SHA}" -o release/game-driver-linux-amd64 .
# 构建 arm64 (cross-compile)
- PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc go build -ldflags="-w -s -X main.Version=${CI_COMMIT_TAG} -X main.Commit=${CI_COMMIT_SHA}" -o release/game-driver-linux-arm64 .
- ls -lh release/
# 发布构建产物(可选)
release:
image: docker.m.daocloud.io/woodpeckerci/plugin-release
settings:
base-url: https://gitea.tides.top
title: ${CI_COMMIT_TAG}
api-key:
from_secret: gitea_token
files:
- release/game-driver-linux-amd64
- release/game-driver-linux-arm64
depends_on:
- build

View File

@@ -1,90 +0,0 @@
/*
Copyright © 2024 慕枫Go <mapleafgo@163.com>
*/
package cmd
import (
"errors"
"game-driver/config"
"game-driver/config/game"
"game-driver/config/wait"
"game-driver/internal"
"io/fs"
"log"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "game-driver",
Version: "1.0.1",
Short: "A brief description of your application",
Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
// Uncomment the following line if your bare application
// has an action associated with it:
Run: func(cmd *cobra.Command, args []string) {
internal.Run()
},
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "config.yml", "默认当前目录下的 config.yml")
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
viper.SetConfigFile(cfgFile)
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
log.Printf("Using config file: %s", viper.ConfigFileUsed())
} else if errors.Is(err, fs.ErrNotExist) {
log.Printf("无法找到主配置文件: %s", viper.ConfigFileUsed())
os.Exit(1)
} else {
log.Panicln("read config file error: ", err)
}
err := viper.Unmarshal(&config.C)
if err != nil {
log.Panicln("unmarshal config failed: ", err)
}
// 初始化游戏节点配置
game.C = game.NewConfig(config.C.Point)
if game.C != nil { // 如果需要游戏配置
err = viper.UnmarshalKey("game", &game.C)
if err != nil {
log.Panicln("unmarshal game config failed: ", err)
}
}
// 初始化游戏节点待机时配置
wait.C = wait.NewConfig(config.C.Point)
if wait.C != nil { // 如果需要游戏配置
err = viper.UnmarshalKey("wait", &wait.C)
if err != nil {
log.Panicln("unmarshal wait config failed: ", err)
}
}
}

View File

@@ -1,5 +1,5 @@
location: wushan # 项目名称 location: wushan # 项目名称
point: 3 # 点位 point: 5 # 点位
relay: relay:
maxTimeout: 60 # 单位 s必须大于 0 maxTimeout: 60 # 单位 s必须大于 0
standbyCache: # 待机缓存文件路径 standbyCache: # 待机缓存文件路径
@@ -17,9 +17,9 @@ log:
maxAge: 30 maxAge: 30
compress: true compress: true
mqtt: mqtt:
url: mqtt://wushan-mqtt.chaoshengshuzi.com:1883 url: mqtt://mqtt.wxsxlj.com:1883
clientID: wushan-3 clientID: wushan-5
password: wushan@1013 password:
aliyun: aliyun:
accessKeyID: accessKeyID:
accessKeySecret: accessKeySecret:
@@ -31,8 +31,8 @@ aliyun:
# 激光秀点位 osc 参数 # 激光秀点位 osc 参数
game: game:
host: 192.168.0.167 host: 192.168.1.191
port: 3033 port: 8000
# 待机投影仪控制参数 # 待机投影仪控制参数
#wait: #wait:

31
go.mod
View File

@@ -11,9 +11,9 @@ require (
github.com/gopxl/beep/v2 v2.1.1 github.com/gopxl/beep/v2 v2.1.1
github.com/grid-x/modbus v0.0.0-20250219144522-2b18d136199f github.com/grid-x/modbus v0.0.0-20250219144522-2b18d136199f
github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5 github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5
github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.21.0
github.com/spf13/viper v1.19.0
github.com/tencentcloud/tencentcloud-cls-sdk-go v1.0.11 github.com/tencentcloud/tencentcloud-cls-sdk-go v1.0.11
github.com/urfave/cli/v3 v3.8.0
github.com/warthog618/go-gpiocdev v0.9.1 github.com/warthog618/go-gpiocdev v0.9.1
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
@@ -21,33 +21,31 @@ require (
require ( require (
github.com/aliyun/alibaba-cloud-sdk-go v1.63.93 // indirect github.com/aliyun/alibaba-cloud-sdk-go v1.63.93 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/ebitengine/oto/v3 v3.3.2 // indirect github.com/ebitengine/oto/v3 v3.3.2 // indirect
github.com/ebitengine/purego v0.8.2 // indirect github.com/ebitengine/purego v0.8.2 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/gorilla/websocket v1.5.3 // indirect
github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa // indirect github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa // indirect
github.com/hajimehoshi/go-mp3 v0.3.4 // indirect github.com/hajimehoshi/go-mp3 v0.3.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/magiconair/properties v1.8.9 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/satori/go.uuid v1.2.0 // indirect github.com/satori/go.uuid v1.2.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.12.0 // indirect github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.7.1 // indirect github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/ysmood/fetchup v0.3.0 // indirect github.com/ysmood/fetchup v0.3.0 // indirect
github.com/ysmood/goob v0.4.0 // indirect github.com/ysmood/goob v0.4.0 // indirect
@@ -56,11 +54,10 @@ require (
github.com/ysmood/leakless v0.9.0 // indirect github.com/ysmood/leakless v0.9.0 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.37.0 // indirect golang.org/x/net v0.37.0 // indirect
golang.org/x/sys v0.31.0 // indirect golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect golang.org/x/text v0.28.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

74
go.sum
View File

@@ -5,13 +5,10 @@ github.com/adrg/libvlc-go/v3 v3.1.6 h1:Cm22w6xNMDdzYCW8koHgAvjonYm4xbPP5TrlVTtMd
github.com/adrg/libvlc-go/v3 v3.1.6/go.mod h1:xJK0YD8cyMDejnrTFQinStE6RYCV1nlfS8KmqTpszSc= github.com/adrg/libvlc-go/v3 v3.1.6/go.mod h1:xJK0YD8cyMDejnrTFQinStE6RYCV1nlfS8KmqTpszSc=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1376/go.mod h1:9CMdKNL3ynIGPpfTcdwTvIm8SGuAZYYC4jFVSSvE1YQ= github.com/aliyun/alibaba-cloud-sdk-go v1.61.1376/go.mod h1:9CMdKNL3ynIGPpfTcdwTvIm8SGuAZYYC4jFVSSvE1YQ=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.92 h1:qespx4b6EexlXkvQUow9x0v1GnWUJYGU5FWYw3a4Wlg=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.92/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.93 h1:yHRWq/QmBJ3lC15zy1A1+TkvcAN+6dr1bgHsFghKvmk= github.com/aliyun/alibaba-cloud-sdk-go v1.63.93 h1:yHRWq/QmBJ3lC15zy1A1+TkvcAN+6dr1bgHsFghKvmk=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.93/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= github.com/aliyun/alibaba-cloud-sdk-go v1.63.93/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/aliyun/alibabacloud-nls-go-sdk v1.1.1 h1:LjItoNZuu5xHlsByFo+kr3nGa4LRIESCGWhfurayxBg= github.com/aliyun/alibabacloud-nls-go-sdk v1.1.1 h1:LjItoNZuu5xHlsByFo+kr3nGa4LRIESCGWhfurayxBg=
github.com/aliyun/alibabacloud-nls-go-sdk v1.1.1/go.mod h1:4BDMUKpEaP/Ct79w0ozR0nbnEj49g1k3mrgX/IKG5I4= github.com/aliyun/alibabacloud-nls-go-sdk v1.1.1/go.mod h1:4BDMUKpEaP/Ct79w0ozR0nbnEj49g1k3mrgX/IKG5I4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -27,13 +24,15 @@ github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/
github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-pkgz/cronrange v0.2.0 h1:FaJ/TB7Ng3xTCfRgblfLecL07RccXVVB6+/hdFaGbBE= github.com/go-pkgz/cronrange v0.2.0 h1:FaJ/TB7Ng3xTCfRgblfLecL07RccXVVB6+/hdFaGbBE=
github.com/go-pkgz/cronrange v0.2.0/go.mod h1:2dPQzEVkSwXsRdcGFXIE6xllAnwELWUusad2MyVskLs= github.com/go-pkgz/cronrange v0.2.0/go.mod h1:2dPQzEVkSwXsRdcGFXIE6xllAnwELWUusad2MyVskLs=
github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
@@ -58,12 +57,8 @@ github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa/go.mod h1:kdOd86/VGF
github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68= github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68=
github.com/hajimehoshi/go-mp3 v0.3.4/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo= github.com/hajimehoshi/go-mp3 v0.3.4/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo=
github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo= github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5 h1:fqwINudmUrvGCuw+e3tedZ2UJ0hklSw6t8UPomctKyQ= github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5 h1:fqwINudmUrvGCuw+e3tedZ2UJ0hklSw6t8UPomctKyQ=
github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5/go.mod h1:lqMjoCs0y0GoRRujSPZRBaGb4c5ER6TfkFKSClxkMbY= github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5/go.mod h1:lqMjoCs0y0GoRRujSPZRBaGb4c5ER6TfkFKSClxkMbY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
@@ -85,10 +80,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -101,8 +92,8 @@ github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:Ff
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw= github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw=
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0= github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -113,32 +104,27 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tencentcloud/tencentcloud-cls-sdk-go v1.0.11 h1:LJshkcQ14A/7XCgqalheBHv8qLwwOXr/xqttQbjWdHM= github.com/tencentcloud/tencentcloud-cls-sdk-go v1.0.11 h1:LJshkcQ14A/7XCgqalheBHv8qLwwOXr/xqttQbjWdHM=
@@ -147,12 +133,12 @@ github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaO
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/warthog618/go-gpiocdev v0.9.1 h1:pwHPaqjJfhCipIQl78V+O3l9OKHivdRDdmgXYbmhuCI= github.com/warthog618/go-gpiocdev v0.9.1 h1:pwHPaqjJfhCipIQl78V+O3l9OKHivdRDdmgXYbmhuCI=
github.com/warthog618/go-gpiocdev v0.9.1/go.mod h1:dN3e3t/S2aSNC+hgigGE/dBW8jE1ONk9bDSEYfoPyl8= github.com/warthog618/go-gpiocdev v0.9.1/go.mod h1:dN3e3t/S2aSNC+hgigGE/dBW8jE1ONk9bDSEYfoPyl8=
github.com/warthog618/go-gpiosim v0.1.1 h1:MRAEv+T+itmw+3GeIGpQJBfanUVyg0l3JCTwHtwdre4= github.com/warthog618/go-gpiosim v0.1.1 h1:MRAEv+T+itmw+3GeIGpQJBfanUVyg0l3JCTwHtwdre4=
github.com/warthog618/go-gpiosim v0.1.1/go.mod h1:YXsnB+I9jdCMY4YAlMSRrlts25ltjmuIsrnoUrBLdqU= github.com/warthog618/go-gpiosim v0.1.1/go.mod h1:YXsnB+I9jdCMY4YAlMSRrlts25ltjmuIsrnoUrBLdqU=
github.com/ysmood/fetchup v0.2.4 h1:2kfWr/UrdiHg4KYRrxL2Jcrqx4DZYD+OtWu7WPBZl5o=
github.com/ysmood/fetchup v0.2.4/go.mod h1:hbysoq65PXL0NQeNzUczNYIKpwpkwFL4LXMDEvIQq9A=
github.com/ysmood/fetchup v0.3.0 h1:UhYz9xnLEVn2ukSuK3KCgcznWpHMdrmbsPpllcylyu8= github.com/ysmood/fetchup v0.3.0 h1:UhYz9xnLEVn2ukSuK3KCgcznWpHMdrmbsPpllcylyu8=
github.com/ysmood/fetchup v0.3.0/go.mod h1:hbysoq65PXL0NQeNzUczNYIKpwpkwFL4LXMDEvIQq9A= github.com/ysmood/fetchup v0.3.0/go.mod h1:hbysoq65PXL0NQeNzUczNYIKpwpkwFL4LXMDEvIQq9A=
github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=
@@ -176,6 +162,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -183,10 +171,6 @@ golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 h1:aWwlzYV971S4BXRS9AmqwDLAD85ouC6X+pocatKY58c=
golang.org/x/exp v0.0.0-20250228200357-dead58393ab7/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -195,8 +179,6 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -204,15 +186,11 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=

602
init-device.py Executable file
View File

@@ -0,0 +1,602 @@
#!/usr/bin/env python3
"""
设备初始化自动化脚本
根据 init_device.md 文档自动执行设备初始化步骤
"""
import subprocess
import sys
import os
import argparse
import tempfile
import re
from pathlib import Path
from typing import Optional, List
from dataclasses import dataclass
from contextlib import contextmanager
@dataclass
class InitConfig:
"""初始化配置"""
install_chromium: bool = False
username: Optional[str] = None
dry_run: bool = False
class Logger:
"""优雅的日志输出"""
@staticmethod
def section(title: str):
"""输出章节标题"""
print(f"\n{'='*60}")
print(f" {title}")
print(f"{'='*60}")
@staticmethod
def step(description: str):
"""输出步骤说明"""
print(f"\n🔧 {description}")
@staticmethod
def success(message: str):
"""输出成功信息"""
print(f"{message}")
@staticmethod
def warning(message: str):
"""输出警告信息"""
print(f"⚠️ {message}")
@staticmethod
def error(message: str):
"""输出错误信息"""
print(f"{message}")
@staticmethod
def info(message: str):
"""输出信息"""
print(f" {message}")
class CommandRunner:
"""命令执行器"""
def __init__(self, dry_run: bool = False):
self.dry_run = dry_run
def _execute_command(self, command: str) -> subprocess.CompletedProcess:
"""执行命令的核心逻辑"""
with subprocess.Popen(
command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1
) as process:
output_lines = []
# 实时读取并显示输出
for line in iter(process.stdout.readline, ''):
print(line.rstrip())
output_lines.append(line)
# 等待进程完成
process.wait()
return subprocess.CompletedProcess(
command,
process.returncode,
''.join(output_lines),
""
)
def run(self, command: str, description: str = "", check: bool = True) -> subprocess.CompletedProcess:
"""执行命令"""
Logger.step(description or command)
if self.dry_run:
Logger.info(f"[预览模式] 命令: {command}")
return subprocess.CompletedProcess(command, 0, "", "")
try:
result = self._execute_command(command)
if check and result.returncode != 0:
raise subprocess.CalledProcessError(result.returncode, command, result.stdout)
Logger.success("执行成功")
return result
except subprocess.CalledProcessError as e:
Logger.error(f"命令执行失败 (退出码: {e.returncode})")
if e.stderr:
print(f"错误详情: {e.stderr.strip()}")
if check:
sys.exit(1)
except Exception as e:
Logger.error(f"执行异常: {e}")
if check:
sys.exit(1)
@contextmanager
def temp_file_with_content(content: str, suffix: str = ""):
"""创建临时文件上下文管理器"""
with tempfile.NamedTemporaryFile(mode='w', suffix=suffix, delete=False) as f:
f.write(content)
temp_path = f.name
try:
yield temp_path
finally:
Path(temp_path).unlink(missing_ok=True)
class DeviceInitializer:
"""设备初始化器"""
# 常量定义
SECTION_OPTIMIZE_BOOT = "优化系统启动时间"
SECTION_UPDATE_SYSTEM = "更新系统包列表"
SECTION_INSTALL_BASIC = "安装基础依赖"
SECTION_ADD_REPOS = "添加第三方软件源"
SECTION_INSTALL_PACKAGES = "安装主要软件包"
SECTION_CONFIGURE_SYSTEM = "配置系统设置"
SECTION_AUTO_STARTX = "配置图形界面自动启动"
SECTION_AUTO_LOGIN = "配置自动登录"
SECTION_CONFIGURE_I3 = "配置 i3 窗口管理器"
SECTION_MANUAL_STEPS = "手动配置步骤"
SECTION_COMPLETE = "初始化完成"
def __init__(self, config: InitConfig):
self.config = config
# 获取实际用户名sudo 下运行时从 SUDO_USER 获取)
self.username = config.username or os.getenv('SUDO_USER') or os.getenv('USER')
self.user_home = Path(f"/home/{self.username}")
self.runner = CommandRunner(config.dry_run)
# 软件包配置
self.base_packages = [
"fontconfig",
"fonts-noto-cjk",
"fonts-noto-color-emoji",
"unclutter",
"xorg",
"i3-wm",
"libvlc-dev",
"vlc-plugin-base",
"vlc-plugin-video-output",
"libasound2-dev",
"alsa-utils",
"trzsz",
"wireguard",
"wireguard-tools"
]
# 基础软件源(始终需要)
self.repositories = [
"ppa:trzsz/ppa"
]
# Chromium 仅在需要时添加的源
self.chromium_repo = "ppa:xtradeb/apps"
def _edit_systemd_service(self, service_name: str, override_content: str, description: str):
"""兼容地编辑 systemd 服务配置"""
# 直接创建 override 目录和文件,兼容所有 systemctl 版本
override_dir = f"/etc/systemd/system/{service_name}.d"
override_file = f"{override_dir}/override.conf"
# 创建目录
self.runner.run(f"sudo mkdir -p {override_dir}", f"创建服务 override 目录: {override_dir}")
# 写入配置文件
with temp_file_with_content(override_content) as temp_file:
self.runner.run(f"sudo cp {temp_file} {override_file}", f"创建服务配置文件: {override_file}")
# 重新加载 systemd 配置
self.runner.run("sudo systemctl daemon-reload", "重新加载 systemd 配置")
Logger.success(f"{description} - 已完成")
def optimize_boot_time(self):
"""优化 Ubuntu 24 开机时间"""
Logger.section(self.SECTION_OPTIMIZE_BOOT)
override_content = "[Service]\nTimeoutStartSec=2sec\n"
self._edit_systemd_service(
"systemd-networkd-wait-online.service",
override_content,
"配置网络等待服务超时时间为2秒"
)
def update_system(self):
"""更新系统包"""
Logger.section(self.SECTION_UPDATE_SYSTEM)
self.runner.run("sudo apt-get update", "刷新软件包索引")
def install_basic_packages(self):
"""安装基础包"""
Logger.section(self.SECTION_INSTALL_BASIC)
basic_packages = ["curl", "gpg"]
packages_str = " ".join(basic_packages)
self.runner.run(f"sudo apt-get install -y {packages_str}", f"安装基础包: {', '.join(basic_packages)}")
def add_repositories(self):
"""添加软件源"""
Logger.section(self.SECTION_ADD_REPOS)
# 基础源始终添加Chromium 源仅在需要安装 Chromium 时添加
repos = list(self.repositories)
if self.config.install_chromium:
repos.append(self.chromium_repo)
Logger.info("已选择安装 Chromium将添加其 PPA 源")
else:
Logger.info("未选择安装 Chromium跳过添加 xtradeb/apps PPA")
for repo in repos:
self.runner.run(f"sudo add-apt-repository -y {repo}", f"添加软件源: {repo}")
self.runner.run("sudo apt-get update", "更新软件包索引")
def install_packages(self):
"""安装主要软件包"""
Logger.section(self.SECTION_INSTALL_PACKAGES)
packages = self.base_packages.copy()
if self.config.install_chromium:
packages.insert(0, "ungoogled-chromium")
Logger.info("已包含 ungoogled-chromium")
packages_str = " ".join(packages)
package_list = ", ".join(packages)
# 安装软件包,实时显示进度
self.runner.run(f"sudo apt-get install -y {packages_str}", f"安装软件包: {package_list}")
def configure_system(self):
"""配置系统设置"""
Logger.section(self.SECTION_CONFIGURE_SYSTEM)
# 设置时区
self.runner.run("sudo timedatectl set-timezone Asia/Shanghai", "设置时区为上海")
# 添加用户到相关组
groups = "audio,video,dialout"
self.runner.run(
f"sudo usermod -aG {groups} {self.username}",
f"将用户 {self.username} 添加到 {groups}"
)
def setup_auto_startx(self):
"""设置自动启动 Xorg"""
Logger.section(self.SECTION_AUTO_STARTX)
bashrc_path = self.user_home / ".bashrc"
startx_config = """
# 自动启动 Xorg 和窗口管理器
if [ -z "$DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then
startx
fi
"""
if self.config.dry_run:
Logger.step("配置自动启动 Xorg")
Logger.info(f"[预览模式] 将添加配置到 {bashrc_path}")
return
# 检查是否已存在配置
if bashrc_path.exists():
try:
content = bashrc_path.read_text(encoding='utf-8')
if 'startx' in content and 'tty1' in content:
Logger.warning("自动启动 Xorg 配置已存在,跳过")
return
except Exception as e:
Logger.warning(f"读取 .bashrc 文件失败: {e}")
# 添加配置
try:
with open(bashrc_path, 'a', encoding='utf-8') as f:
f.write(startx_config)
# 设置文件所有权为实际用户
self.runner.run(f"sudo chown {self.username}:{self.username} {bashrc_path}",
f"设置 .bashrc 所有权为 {self.username}")
Logger.success(f"已配置自动启动 Xorg: {bashrc_path}")
except Exception as e:
Logger.error(f"配置自动启动 Xorg 失败: {e}")
def setup_auto_login(self):
"""设置自动登录"""
Logger.section(self.SECTION_AUTO_LOGIN)
override_content = f"""[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin {self.username} --noclear %I $TERM
"""
self._edit_systemd_service(
"getty@tty1.service",
override_content,
f"配置用户 {self.username} 自动登录"
)
def configure_i3(self):
"""配置 i3 窗口管理器"""
Logger.section(self.SECTION_CONFIGURE_I3)
i3_config_dir = self.user_home / ".config" / "i3"
i3_config_path = i3_config_dir / "config"
if self.config.dry_run:
Logger.step("配置 i3 窗口管理器")
Logger.info(f"[预览模式] 将配置 i3 配置文件: {i3_config_path}")
return
# 创建 i3 配置目录
i3_config_dir.mkdir(parents=True, exist_ok=True)
# 检查是否已有 i3 配置文件
if not i3_config_path.exists():
Logger.info("i3 配置文件不存在,从系统默认配置复制")
self._copy_system_i3_config(i3_config_path)
else:
Logger.info("i3 配置文件已存在,直接更新配置")
# 无论是新生成还是已存在,都更新配置
self._update_i3_config(i3_config_path)
# 设置配置文件所有权为实际用户
self.runner.run(f"sudo chown -R {self.username}:{self.username} {i3_config_dir}", f"设置 i3 配置目录所有权为 {self.username}")
Logger.success(f"已配置 i3 窗口管理器: {i3_config_path}")
def _copy_system_i3_config(self, config_path: Path):
"""从系统默认配置复制 i3 配置文件"""
Logger.step("从系统默认配置复制 i3 配置文件")
system_config = "/etc/i3/config"
self.runner.run(f"cp {system_config} {config_path}", f"复制系统默认 i3 配置到 {config_path}")
# 移除 i3-config-wizard 指令
try:
content = config_path.read_text(encoding='utf-8')
if 'i3-config-wizard' in content:
# 移除包含 i3-config-wizard 的行
lines = content.splitlines()
filtered_lines = [line for line in lines if 'i3-config-wizard' not in line]
with open(config_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(filtered_lines) + '\n')
Logger.info("已移除系统配置中的 i3-config-wizard 指令")
except Exception as e:
Logger.warning(f"移除 i3-config-wizard 指令失败: {e}")
Logger.success("已从系统默认配置复制 i3 配置文件")
def _update_i3_config(self, config_path: Path):
"""更新现有 i3 配置文件"""
Logger.step("更新现有 i3 配置文件")
try:
content = config_path.read_text(encoding='utf-8')
except Exception as e:
Logger.error(f"读取 i3 配置文件失败: {e}")
return
# 添加启动配置项
startup_configs = [
"exec --no-startup-id unclutter -root # 隐藏鼠标",
"exec --no-startup-id xset dpms 0 0 0 # 关闭屏幕自动关闭",
"exec --no-startup-id xset s off # 关闭屏幕保护"
]
config_modified = False
for config_line in startup_configs:
if config_line not in content:
content += f"\n{config_line}\n"
Logger.info(f"添加配置: {config_line}")
config_modified = True
# 移除 i3bar 配置块
if "bar {" in content:
# 使用正则表达式直接移除整个 bar 配置块
bar_pattern = r'bar\s*\{[^}]*\}'
new_content = re.sub(bar_pattern, '', content, flags=re.DOTALL)
if new_content != content:
content = new_content
Logger.info("已移除 i3bar 状态栏配置")
config_modified = True
# 只在有修改时才写回文件
if config_modified:
try:
with open(config_path, 'w', encoding='utf-8') as f:
f.write(content)
Logger.success("i3 配置文件更新完成")
except Exception as e:
Logger.error(f"写入 i3 配置文件失败: {e}")
else:
Logger.info("i3 配置无需修改")
def show_manual_steps(self):
"""显示需要手动完成的步骤"""
Logger.section(self.SECTION_MANUAL_STEPS)
Logger.info("请手动完成以下配置步骤:")
print("\n📋 WireGuard 配置:")
print(" 1. 从服务器获取 WireGuard 配置文件")
print(" 2. 保存到 /etc/wireguard/wg0.conf")
print(" 3. 启用并启动 WireGuard:")
print(" sudo systemctl enable wg-quick@wg0")
print(" sudo systemctl start wg-quick@wg0")
print("\n📋 系统音量配置:")
print(" 1. 运行 alsamixer 进入音量控制界面")
print(" 2. 调节 master 音量到合适级别")
print(" 3. 按 ESC 退出")
print(" 4. 运行 sudo alsactl store 保存音量设置")
print("\n📋 i3 窗口管理器:")
print(" 1. 重启系统后会自动进入 i3")
print(" 2. 脚本已自动配置必要的设置(隐藏鼠标、禁用屏保等)")
print(" 3. 可以根据需要进一步自定义 ~/.config/i3/config")
def run_initialization(self):
"""运行完整的初始化流程"""
Logger.section("设备初始化开始")
Logger.info(f"目标用户: {self.username}")
Logger.info(f"用户主目录: {self.user_home}")
Logger.info(f"安装 Chromium: {'' if self.config.install_chromium else ''}")
Logger.info(f"预览模式: {'' if self.config.dry_run else ''}")
# 初始化步骤配置
initialization_steps = [
("优化系统启动", self.optimize_boot_time),
("更新系统包", self.update_system),
("安装基础依赖", self.install_basic_packages),
("添加软件源", self.add_repositories),
("安装主要软件", self.install_packages),
("配置系统设置", self.configure_system),
("配置自动启动X", self.setup_auto_startx),
("配置自动登录", self.setup_auto_login),
("配置i3窗口管理器", self.configure_i3),
]
try:
for step_name, step_func in initialization_steps:
Logger.info(f"正在执行: {step_name}")
step_func()
self.show_manual_steps()
Logger.section(self.SECTION_COMPLETE)
Logger.success("🎉 所有自动化步骤已完成")
Logger.warning("⚠️ 请重启系统以使所有更改生效")
Logger.info("💡 重启后请完成 WireGuard 和音量的手动配置")
except KeyboardInterrupt:
Logger.error("❌ 用户中断了初始化过程")
sys.exit(1)
except Exception as e:
Logger.error(f"❌ 初始化过程中发生错误: {e}")
sys.exit(1)
class SystemValidator:
"""系统环境验证器"""
@staticmethod
def validate_environment():
"""验证运行环境"""
Logger.section("验证系统环境")
# 检查是否通过 sudo 运行
if os.geteuid() != 0:
Logger.error("此脚本需要使用 sudo 运行")
Logger.info("请使用: sudo python3 init-device.py")
sys.exit(1)
# 检查是否有原始用户信息
if not os.getenv('SUDO_USER'):
Logger.error("请不要直接以 root 用户运行此脚本")
Logger.info("请使用普通用户通过 sudo 运行: sudo python3 init-device.py")
sys.exit(1)
Logger.info(f"检测到通过 sudo 运行脚本,原用户: {os.getenv('SUDO_USER')}")
Logger.success("系统环境验证通过")
def create_argument_parser() -> argparse.ArgumentParser:
"""创建命令行参数解析器"""
parser = argparse.ArgumentParser(
description="设备初始化自动化脚本",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
使用示例:
%(prog)s # 基础安装
%(prog)s --chromium # 包含 Chromium 的完整安装
%(prog)s --dry-run # 预览模式,不实际执行
%(prog)s --username user1 # 指定用户名
"""
)
parser.add_argument(
"--chromium",
action="store_true",
help="安装 ungoogled-chromium 浏览器"
)
parser.add_argument(
"--username",
type=str,
help="指定用户名(默认使用当前用户)"
)
parser.add_argument(
"--dry-run",
action="store_true",
help="预览模式:仅显示将要执行的操作,不实际执行"
)
parser.add_argument(
"--version",
action="version",
version="%(prog)s 1.0.0"
)
return parser
def show_preview(config: InitConfig):
"""显示预览信息"""
Logger.section("预览模式")
steps = [
DeviceInitializer.SECTION_OPTIMIZE_BOOT,
DeviceInitializer.SECTION_UPDATE_SYSTEM,
f"{DeviceInitializer.SECTION_INSTALL_BASIC} (curl, gpg)",
DeviceInitializer.SECTION_ADD_REPOS,
f"{DeviceInitializer.SECTION_INSTALL_PACKAGES}" + (" (包含 ungoogled-chromium)" if config.install_chromium else ""),
f"{DeviceInitializer.SECTION_CONFIGURE_SYSTEM} (时区、用户组)",
DeviceInitializer.SECTION_AUTO_STARTX,
DeviceInitializer.SECTION_AUTO_LOGIN,
DeviceInitializer.SECTION_CONFIGURE_I3,
"显示手动配置步骤"
]
Logger.info("将要执行的操作步骤:")
for i, step in enumerate(steps, 1):
print(f" {i:2d}. {step}")
Logger.warning("这是预览模式,不会实际执行任何操作")
def main():
"""主函数"""
parser = create_argument_parser()
args = parser.parse_args()
# 创建配置
config = InitConfig(
install_chromium=args.chromium,
username=args.username,
dry_run=args.dry_run
)
# 预览模式
if config.dry_run:
show_preview(config)
return
# 验证环境
SystemValidator.validate_environment()
# 运行初始化
initializer = DeviceInitializer(config)
initializer.run_initialization()
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
Logger.error("用户中断操作")
sys.exit(1)
except Exception as e:
Logger.error(f"程序异常退出: {e}")
sys.exit(1)

View File

@@ -2,7 +2,10 @@
```bash ```bash
# 在 systemd-networkd-wait-online.service Service 加入 TimeoutStartSec=2sec # 在 systemd-networkd-wait-online.service Service 加入 TimeoutStartSec=2sec
sudo vim /etc/systemd/system/network-online.target.wants/systemd-networkd-wait-online.service sudo EDITOR=vim systemctl edit systemd-networkd-wait-online.service
# 在打开的编辑器中添加:
# [Service]
# TimeoutStartSec=2sec
``` ```
### 初始化设备 ### 初始化设备
@@ -10,9 +13,9 @@ sudo vim /etc/systemd/system/network-online.target.wants/systemd-networkd-wait-o
```bash ```bash
sudo apt update sudo apt update
sudo apt install curl gpg sudo apt install curl gpg
sudo add-apt-repository ppa:xtradeb/apps sudo add-apt-repository ppa:xtradeb/apps # 不安装 ungoogled-chromium 时,不要添加。可能与系统源的库冲突
sudo add-apt-repository ppa:trzsz/ppa sudo add-apt-repository ppa:trzsz/ppa
sudo apt install -y ungoogled-chromium fonts-noto-cjk fonts-noto-color-emoji unclutter xorg i3-wm libvlc-dev libasound2-dev alsa-utils trzsz wireguard wireguard-tools sudo apt install -y ungoogled-chromium fontconfig fonts-noto-cjk fonts-noto-color-emoji unclutter xorg i3-wm libvlc-dev vlc-plugin-base vlc-plugin-video-output libasound2-dev alsa-utils trzsz wireguard wireguard-tools
sudo timedatectl set-timezone Asia/Shanghai sudo timedatectl set-timezone Asia/Shanghai
sudo usermod -aG audio,video,dialout $USER sudo usermod -aG audio,video,dialout $USER
``` ```
@@ -48,10 +51,14 @@ fi
### 自动登录 ### 自动登录
编辑 `/etc/systemd/system/getty.target.wants/getty@tty1.service` 文件,将 `ExecStart` 行修改为 使用 systemctl edit 修改 getty@tty1 服务
```bash ```bash
ExecStart=-/sbin/agetty --autologin <your_username> --noclear %I $TERM sudo EDITOR=vim systemctl edit getty@tty1.service
# 在打开的编辑器中添加:
# [Service]
# ExecStart=
# ExecStart=-/sbin/agetty --autologin <your_username> --noclear %I $TERM
``` ```
其中: 其中:

View File

@@ -5,9 +5,10 @@ import (
"game-driver/leaf" "game-driver/leaf"
"game-driver/pkg/audio" "game-driver/pkg/audio"
"game-driver/pkg/utils" "game-driver/pkg/utils"
"sync"
"github.com/gopxl/beep/v2/speaker" "github.com/gopxl/beep/v2/speaker"
"go.uber.org/zap" "go.uber.org/zap"
"sync"
) )
// PlayBgm 播放背景音乐 // PlayBgm 播放背景音乐
@@ -44,15 +45,11 @@ func PlayBgm() leaf.HandlerFunc {
return return
} }
select { <-a
case <-a:
{ speaker.Lock()
speaker.Lock() ctrl.Streamer = nil
ctrl.Streamer = nil speaker.Unlock()
speaker.Unlock()
return
}
}
}() }()
} else { } else {
zap.S().Infoln("未解析到背景音乐") zap.S().Infoln("未解析到背景音乐")

View File

@@ -10,16 +10,24 @@ import (
func SoundStart() leaf.HandlerFunc { func SoundStart() leaf.HandlerFunc {
return func(c *leaf.Context) { return func(c *leaf.Context) {
pm := leaf.Value[*schema.PlayModal](c, PayloadJSONKey) pm := leaf.Value[*schema.PlayModal](c, PayloadJSONKey)
tts.DefaultTTS.Sound(pm.TTS.Start)
// 使用请求的 context支持取消和超时
if pm.TTS.Start != "" {
tts.DefaultTTS.Sound(c, pm.TTS.Start)
}
defer func() { defer func() {
var text string
switch leaf.Value[leaf.EndType](c, leaf.EndKey) { switch leaf.Value[leaf.EndType](c, leaf.EndKey) {
case leaf.End: case leaf.End:
tts.DefaultTTS.Sound(pm.TTS.End) text = pm.TTS.End
case leaf.EndTimeout: case leaf.EndTimeout:
tts.DefaultTTS.Sound(pm.TTS.Timeout) text = pm.TTS.Timeout
case leaf.EndStop: case leaf.EndStop:
tts.DefaultTTS.Sound(pm.TTS.Stop) text = pm.TTS.Stop
}
if text != "" {
tts.DefaultTTS.Sound(c, text)
} }
}() }()

View File

@@ -1,6 +1,7 @@
package middleware package middleware
import ( import (
"context"
"game-driver/internal/schema" "game-driver/internal/schema"
"game-driver/leaf" "game-driver/leaf"
"game-driver/pkg/tts" "game-driver/pkg/tts"
@@ -34,7 +35,7 @@ func TickerAction() leaf.HandlerFunc {
defer close(a) defer close(a)
wait.Add(1) wait.Add(1)
go func() { go func(ctx context.Context) {
start := time.Now() start := time.Now()
defer wait.Done() defer wait.Done()
// 定时器 // 定时器
@@ -48,6 +49,9 @@ func TickerAction() leaf.HandlerFunc {
select { select {
case <-a: case <-a:
over = true over = true
case <-ctx.Done():
zap.S().Infoln("Ticker 计时被取消")
over = true
case m := <-ticker.C: case m := <-ticker.C:
{ {
s := int(m.Sub(start).Seconds()) s := int(m.Sub(start).Seconds())
@@ -55,12 +59,12 @@ func TickerAction() leaf.HandlerFunc {
//TODO: 屏幕打印 //TODO: 屏幕打印
} }
if to, ok := ttsMap[s]; ok { if to, ok := ttsMap[s]; ok {
tts.DefaultTTS.Sound(to.Value) tts.DefaultTTS.Sound(ctx, to.Value)
} }
} }
} }
} }
}() }(c)
c.Next() c.Next()
} }
} }

View File

@@ -6,14 +6,15 @@ import (
"game-driver/leaf" "game-driver/leaf"
"game-driver/pkg/utils" "game-driver/pkg/utils"
"game-driver/pkg/video" "game-driver/pkg/video"
"go.uber.org/zap" "go.uber.org/zap"
) )
func OnlyVideo(c *leaf.Context) { func OnlyVideo(c *leaf.Context) {
payload := leaf.Value[*schema.PlayModal](c, middleware.PayloadJSONKey) payload := leaf.Value[*schema.PlayModal](c, middleware.PayloadJSONKey)
utils.BlankOpen() // utils.BlankOpen()
defer utils.BlankClose() // defer utils.BlankClose()
if url, ok := payload.Game["video"]; ok { if url, ok := payload.Game["video"]; ok {
path, local, err := utils.LinkVideo(url.(string)) path, local, err := utils.LinkVideo(url.(string))
@@ -21,6 +22,8 @@ func OnlyVideo(c *leaf.Context) {
zap.S().Errorln("视频文件获取异常: ", err) zap.S().Errorln("视频文件获取异常: ", err)
return return
} }
_ = video.Play(c, path, local) if err := video.Play(c, path, local); err != nil {
zap.S().Errorln("视频播放异常: ", err)
}
} }
} }

View File

@@ -10,11 +10,12 @@ import (
"game-driver/pkg/card_reader" "game-driver/pkg/card_reader"
"game-driver/pkg/channel" "game-driver/pkg/channel"
"game-driver/pkg/tts" "game-driver/pkg/tts"
"go.uber.org/zap"
"io/fs" "io/fs"
"os" "os"
"sync" "sync"
"time" "time"
"go.uber.org/zap"
) )
func WaitCard(ctx context.Context) leaf.HandlerFunc { func WaitCard(ctx context.Context) leaf.HandlerFunc {
@@ -79,13 +80,14 @@ func WaitCard(ctx context.Context) leaf.HandlerFunc {
defer cardInfo.Close() defer cardInfo.Close()
// 结束信号通道 // 结束信号通道
cc, cancel := context.WithCancel(context.TODO()) // 使用独立 context 确保读卡器监听完整执行,不受外部取消影响
c2, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
wait.Add(1) wait.Add(1)
go func() { go func() {
defer wait.Done() defer wait.Done()
reader.OnCardInfo(cc, func(info *card_reader.CardInfo) { reader.OnCardInfo(c2, func(info *card_reader.CardInfo) {
cardInfo.Send(info.ID) cardInfo.Send(info.ID)
}) })
}() }()
@@ -103,12 +105,12 @@ func WaitCard(ctx context.Context) leaf.HandlerFunc {
if cardId != id { if cardId != id {
zap.S().Infof("读取到卡片数据%q与预期卡片数据%q不一致", id, cardId) zap.S().Infof("读取到卡片数据%q与预期卡片数据%q不一致", id, cardId)
// 播报错误提示 // 播报错误提示
tts.DefaultTTS.Sound(cardError) tts.DefaultTTS.Sound(c, cardError)
isNeed = true isNeed = true
break break
} }
// 播报恭喜语音 // 播报恭喜语音
tts.DefaultTTS.Sound(cardOk) tts.DefaultTTS.Sound(c, cardOk)
//TODO: 打开炫酷光效,屏幕跳转恭喜页面 //TODO: 打开炫酷光效,屏幕跳转恭喜页面
zap.S().Infof("读取到卡片数据%q开始打开炫酷光效", id) zap.S().Infof("读取到卡片数据%q开始打开炫酷光效", id)
Default(c) Default(c)

View File

@@ -11,7 +11,7 @@ import (
func TTS(item schema.WaitItemModel) func(c context.Context) error { func TTS(item schema.WaitItemModel) func(c context.Context) error {
return func(c context.Context) error { return func(c context.Context) error {
reader, err := tts.DefaultTTS.Get(item.Data) reader, err := tts.DefaultTTS.Get(c, item.Data)
if err != nil { if err != nil {
return fmt.Errorf("语音合成异常: %w", err) return fmt.Errorf("语音合成异常: %w", err)
} }

View File

@@ -6,6 +6,7 @@ import (
"game-driver/internal/schema" "game-driver/internal/schema"
"game-driver/pkg/utils" "game-driver/pkg/utils"
"game-driver/pkg/video" "game-driver/pkg/video"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -19,8 +20,8 @@ func Video(item schema.WaitItemModel) func(c context.Context) error {
zap.S().Infoln("播放待机视频") zap.S().Infoln("播放待机视频")
defer zap.S().Infoln("结束待机视频") defer zap.S().Infoln("结束待机视频")
utils.BlankOpen() // utils.BlankOpen()
defer utils.BlankClose() // defer utils.BlankClose()
err = video.Play(c, path, local) err = video.Play(c, path, local)
if err != nil { if err != nil {

View File

@@ -4,7 +4,7 @@ import (
"context" "context"
"game-driver/internal/schema" "game-driver/internal/schema"
"game-driver/pkg/browser" "game-driver/pkg/browser"
"game-driver/pkg/utils"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -13,8 +13,8 @@ func Web(item schema.WaitItemModel) func(c context.Context) error {
zap.S().Infoln("打开待机网页") zap.S().Infoln("打开待机网页")
// 控制背光 // 控制背光
utils.BlankOpen() // utils.BlankOpen()
defer utils.BlankClose() // defer utils.BlankClose()
browser.OpenApp(c, item.Data) browser.OpenApp(c, item.Data)
return nil return nil

View File

@@ -65,7 +65,8 @@ func Cron(rootRules []cronrange.Rule, cron string, play func(c context.Context)
case r := <-a: case r := <-a:
if r { if r {
if ok := m.TryLock(); ok { if ok := m.TryLock(); ok {
ctx, cc := context.WithCancel(context.TODO()) // 使用独立 context 确保任务完整执行,不受外部取消影响
ctx, cc := context.WithCancel(context.Background())
cancel = cc cancel = cc
waitGroup.Add(1) waitGroup.Add(1)
go func() { go func() {

View File

@@ -28,6 +28,8 @@ func Interval(interval int64, play func(c context.Context) error) func(c context
case <-c.Done(): case <-c.Done():
return nil return nil
default: default:
// 避免忙循环,短暂休眠
time.Sleep(10 * time.Millisecond)
} }
} }
} }

View File

@@ -5,6 +5,7 @@ import (
"game-driver/internal/common" "game-driver/internal/common"
"go.uber.org/zap" "go.uber.org/zap"
"sync" "sync"
"time"
) )
// Pause 暂停控制器 // Pause 暂停控制器
@@ -58,6 +59,8 @@ func Pause(ps *common.PauseSub, isPause bool, play func(c context.Context) error
zap.S().Infoln("执行后续操作异常: ", err) zap.S().Infoln("执行后续操作异常: ", err)
} }
} }
// 避免忙循环,短暂休眠
time.Sleep(10 * time.Millisecond)
} }
} }
} }

View File

@@ -83,13 +83,13 @@ func Run() {
cls, err := logger.NewTenCls(fmt.Sprintf("game-driver-%s-%v", config.C.Location, config.C.Point)) cls, err := logger.NewTenCls(fmt.Sprintf("game-driver-%s-%v", config.C.Location, config.C.Point))
if err != nil { if err != nil {
log.Println("初始化腾讯云日志服务异常: ", err) log.Println("初始化腾讯云日志服务异常: ", err)
logger.InitDevLogger()
} else {
cls.Start()
defer cls.Close()
logger.InitProLogger(cls)
} }
cls.Start()
defer cls.Close()
logger.InitProLogger(cls)
//logger.InitDevLogger()
// 应用退出时刷新所有缓冲日志 // 应用退出时刷新所有缓冲日志
defer logger.Sync() defer logger.Sync()
@@ -101,7 +101,7 @@ func Run() {
zap.S().Infoln("当前IP: ", addrs) zap.S().Infoln("当前IP: ", addrs)
// 启动时关闭屏幕 // 启动时关闭屏幕
utils.BlankClose() // utils.BlankClose()
topicPrefix := fmt.Sprintf("server/%s/%v/", config.C.Location, config.C.Point) topicPrefix := fmt.Sprintf("server/%s/%v/", config.C.Location, config.C.Point)
publishTopic := fmt.Sprintf("device/%s/%v/status", config.C.Location, config.C.Point) publishTopic := fmt.Sprintf("device/%s/%v/status", config.C.Location, config.C.Point)
@@ -119,7 +119,7 @@ func Run() {
}) })
// 构建语音合成对象 // 构建语音合成对象
tts.DefaultTTS = tts.New(ctx, config.C.Aliyun) tts.DefaultTTS = tts.New(config.C.Aliyun)
// 构建继电器对象 // 构建继电器对象
var r relay.Relay var r relay.Relay
@@ -143,9 +143,9 @@ func Run() {
middleware.DeviceLock(device), middleware.DeviceLock(device),
middleware.PauseWait(common.PassCtrl), middleware.PauseWait(common.PassCtrl),
middleware.EmergencyStop(common.GlobalStopper), middleware.EmergencyStop(common.GlobalStopper),
middleware.TimeoutOver(config.C.MaxTimeout),
middleware.SoundStart(), middleware.SoundStart(),
middleware.RelayMaster(r), middleware.RelayMaster(r),
middleware.TimeoutOver(config.C.MaxTimeout),
middleware.TickerAction(), middleware.TickerAction(),
middleware.PlayBgm(), middleware.PlayBgm(),
routes.PlayRouter(ctx, config.C.Location, config.C.Point), routes.PlayRouter(ctx, config.C.Location, config.C.Point),

95
main.go
View File

@@ -3,8 +3,99 @@ Copyright © 2024 慕枫Go <mapleafgo@163.com>
*/ */
package main package main
import "game-driver/cmd" import (
"context"
"errors"
"fmt"
"game-driver/config"
"game-driver/config/game"
"game-driver/config/wait"
"game-driver/internal"
"io/fs"
"log"
"os"
"github.com/spf13/viper"
"github.com/urfave/cli/v3"
)
// 版本信息,编译时通过 ldflags 注入
var (
Version = "dev"
Commit = "unknown"
)
// formatVersion 格式化版本信息
func formatVersion() string {
if Commit == "unknown" {
return Version
}
if len(Commit) > 7 {
return Version + " (" + Commit[:7] + ")"
}
return Version + " (" + Commit + ")"
}
// initConfig 读取配置文件和环境变量
func initConfig(cfgFile string) error {
viper.SetConfigFile(cfgFile)
viper.AutomaticEnv()
// 读取配置文件
if err := viper.ReadInConfig(); err == nil {
log.Printf("使用配置文件: %s", viper.ConfigFileUsed())
} else if errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("配置文件不存在: %s", cfgFile)
} else {
return fmt.Errorf("读取配置文件错误: %w", err)
}
// 解析主配置
if err := viper.Unmarshal(&config.C); err != nil {
return fmt.Errorf("解析主配置失败: %w", err)
}
// 解析游戏配置
if game.C = game.NewConfig(config.C.Point); game.C != nil {
if err := viper.UnmarshalKey("game", &game.C); err != nil {
return fmt.Errorf("解析游戏配置失败: %w", err)
}
}
// 解析待机配置
if wait.C = wait.NewConfig(config.C.Point); wait.C != nil {
if err := viper.UnmarshalKey("wait", &wait.C); err != nil {
return fmt.Errorf("解析待机配置失败: %w", err)
}
}
return nil
}
func main() { func main() {
cmd.Execute() app := &cli.Command{
Name: "game-driver",
Usage: "游戏驱动程序",
Version: formatVersion(),
Flags: []cli.Flag{
&cli.StringFlag{
Name: "config",
Aliases: []string{"c"},
Value: "config.yml",
Usage: "配置文件路径",
Sources: cli.EnvVars("CONFIG_FILE"),
},
},
Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
return ctx, initConfig(cmd.String("config"))
},
Action: func(ctx context.Context, cmd *cli.Command) error {
internal.Run()
return nil
},
}
if err := app.Run(context.Background(), os.Args); err != nil {
os.Exit(1)
}
} }

View File

@@ -2,13 +2,14 @@ package audio
import ( import (
"context" "context"
"io"
"time"
"github.com/gopxl/beep/v2" "github.com/gopxl/beep/v2"
"github.com/gopxl/beep/v2/mp3" "github.com/gopxl/beep/v2/mp3"
"github.com/gopxl/beep/v2/speaker" "github.com/gopxl/beep/v2/speaker"
"github.com/gopxl/beep/v2/wav" "github.com/gopxl/beep/v2/wav"
"go.uber.org/zap" "go.uber.org/zap"
"io"
"time"
) )
var DefaultSampleRate = beep.SampleRate(44100) var DefaultSampleRate = beep.SampleRate(44100)
@@ -22,29 +23,55 @@ func init() {
} }
func PlayWav(c context.Context, r io.Reader) { func PlayWav(c context.Context, r io.Reader) {
zap.S().Debugln("开始 WAV 解码")
streamer, format, err := wav.Decode(r) streamer, format, err := wav.Decode(r)
if err != nil { if err != nil {
zap.S().Errorln("WAV解码失败: ", err)
return return
} }
defer streamer.Close() defer streamer.Close()
// 获取音频长度信息
totalSamples := streamer.Len()
zap.S().Debugf("WAV解码成功采样率: %d, 总样本数: %d, 预计时长: %.2f秒",
format.SampleRate, totalSamples, float64(totalSamples)/float64(format.SampleRate))
s := beep.Resample(4, format.SampleRate, DefaultSampleRate, streamer) s := beep.Resample(4, format.SampleRate, DefaultSampleRate, streamer)
ctrl := &beep.Ctrl{Streamer: s} ctrl := &beep.Ctrl{Streamer: s}
done := make(chan struct{}) done := make(chan struct{})
speaker.Play(beep.Seq(ctrl, beep.Callback(func() { speaker.Play(beep.Seq(ctrl, beep.Callback(func() {
zap.S().Debugln("音频播放完成")
close(done) close(done)
}))) })))
zap.S().Debugln("等待音频播放完成...")
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
lastPos := 0
for { for {
select { select {
case <-done: case <-done:
zap.S().Infoln("音频播放正常结束")
return return
case <-c.Done(): case <-c.Done():
{ zap.S().Debugf("音频播放被 context 取消: %v", c.Err())
speaker.Lock() speaker.Lock()
ctrl.Streamer = nil ctrl.Streamer = nil
speaker.Unlock() speaker.Unlock()
return
case <-ticker.C:
// 获取当前播放位置
pos := streamer.Position()
if pos != lastPos {
progress := float64(pos) / float64(totalSamples) * 100
currentTime := float64(pos) / float64(format.SampleRate)
zap.S().Debugf("播放进度: %d/%d (%.1f%%), %.2f秒", pos, totalSamples, progress, currentTime)
lastPos = pos
} else {
zap.S().Debugf("播放停滞在位置: %d/%d, Streamer状态: %v",
pos, totalSamples, ctrl.Streamer != nil)
} }
} }
} }
@@ -53,6 +80,7 @@ func PlayWav(c context.Context, r io.Reader) {
func PlayMP3(c context.Context, r io.ReadCloser) { func PlayMP3(c context.Context, r io.ReadCloser) {
streamer, format, err := mp3.Decode(r) streamer, format, err := mp3.Decode(r)
if err != nil { if err != nil {
zap.S().Errorln("MP3解码失败: ", err)
return return
} }
defer streamer.Close() defer streamer.Close()
@@ -70,11 +98,10 @@ func PlayMP3(c context.Context, r io.ReadCloser) {
case <-done: case <-done:
return return
case <-c.Done(): case <-c.Done():
{ speaker.Lock()
speaker.Lock() ctrl.Streamer = nil
ctrl.Streamer = nil speaker.Unlock()
speaker.Unlock() return
}
} }
} }
} }

View File

@@ -1,9 +0,0 @@
package errorsx
import "errors"
var DriverTimeoutErr = errors.New("处理超时")
var DriverCancelErr = errors.New("系统取消")
var ThirdPartyErr = errors.New("第三方请求异常")

View File

@@ -14,51 +14,61 @@ func New(host string, port int) *Client {
} }
} }
// StartCue 播放节目
func (c *Client) StartCue(data string) error { func (c *Client) StartCue(data string) error {
msg := osc.NewMessage("/beyond/general/StartCue", data) msg := osc.NewMessage("/beyond/general/StartCue", data)
return c.o.Send(msg) return c.o.Send(msg)
} }
// EnableLaserOutput 打开激光
func (c *Client) EnableLaserOutput() error { func (c *Client) EnableLaserOutput() error {
msg := osc.NewMessage("/beyond/general/EnableLaserOutput") msg := osc.NewMessage("/beyond/general/EnableLaserOutput")
return c.o.Send(msg) return c.o.Send(msg)
} }
// DisableLaserOutput 关闭激光
func (c *Client) DisableLaserOutput() error { func (c *Client) DisableLaserOutput() error {
msg := osc.NewMessage("/beyond/general/DisableLaserOutput") msg := osc.NewMessage("/beyond/general/DisableLaserOutput")
return c.o.Send(msg) return c.o.Send(msg)
} }
// SetLaserOutput 设置激光输出
func (c *Client) SetLaserOutput(data string) error { func (c *Client) SetLaserOutput(data string) error {
msg := osc.NewMessage("/beyond/general/SetLaserOutput", data) msg := osc.NewMessage("/beyond/general/SetLaserOutput", data)
return c.o.Send(msg) return c.o.Send(msg)
} }
// SetLaserOutputColor 设置激光颜色
func (c *Client) SetLaserOutputColor(data string) error { func (c *Client) SetLaserOutputColor(data string) error {
msg := osc.NewMessage("/beyond/general/SetLaserOutputColor", data) msg := osc.NewMessage("/beyond/general/SetLaserOutputColor", data)
return c.o.Send(msg) return c.o.Send(msg)
} }
// SetLaserOutputIntensity 设置激光强度
func (c *Client) SetLaserOutputIntensity(data string) error { func (c *Client) SetLaserOutputIntensity(data string) error {
msg := osc.NewMessage("/beyond/general/SetLaserOutputIntensity", data) msg := osc.NewMessage("/beyond/general/SetLaserOutputIntensity", data)
return c.o.Send(msg) return c.o.Send(msg)
} }
// SetLaserOutputPosition 设置激光位置
func (c *Client) SetLaserOutputPosition(data string) error { func (c *Client) SetLaserOutputPosition(data string) error {
msg := osc.NewMessage("/beyond/general/SetLaserOutputPosition", data) msg := osc.NewMessage("/beyond/general/SetLaserOutputPosition", data)
return c.o.Send(msg) return c.o.Send(msg)
} }
// SetLaserOutputSize 设置激光尺寸
func (c *Client) SetLaserOutputSize(data string) error { func (c *Client) SetLaserOutputSize(data string) error {
msg := osc.NewMessage("/beyond/general/SetLaserOutputSize", data) msg := osc.NewMessage("/beyond/general/SetLaserOutputSize", data)
return c.o.Send(msg) return c.o.Send(msg)
} }
// SetLaserOutputSpeed 设置激光速度
func (c *Client) SetLaserOutputSpeed(data string) error { func (c *Client) SetLaserOutputSpeed(data string) error {
msg := osc.NewMessage("/beyond/general/SetLaserOutputSpeed", data) msg := osc.NewMessage("/beyond/general/SetLaserOutputSpeed", data)
return c.o.Send(msg) return c.o.Send(msg)
} }
// Status 获取状态
func (c *Client) Status() error { func (c *Client) Status() error {
msg := osc.NewMessage("/beyond/general/Status") msg := osc.NewMessage("/beyond/general/Status")
return c.o.Send(msg) return c.o.Send(msg)

View File

@@ -7,19 +7,21 @@ import (
"game-driver/config" "game-driver/config"
"game-driver/leaf" "game-driver/leaf"
"game-driver/pkg/audio" "game-driver/pkg/audio"
"game-driver/pkg/errorsx"
"go.uber.org/zap" "go.uber.org/zap"
"io" "io"
"log" "log"
"sync"
"time" "time"
nls "github.com/aliyun/alibabacloud-nls-go-sdk" nls "github.com/aliyun/alibabacloud-nls-go-sdk"
) )
// AliTTS 阿里云语音合成
// 同一时间只能播放一个 TTS
type AliTTS struct { type AliTTS struct {
config.AliyunConfig config.AliyunConfig
ctx context.Context
tokenResult nls.TokenResult tokenResult nls.TokenResult
mu sync.Mutex // 互斥锁,确保同时只播放一个
} }
type result struct { type result struct {
@@ -29,59 +31,85 @@ type result struct {
var DefaultTTS = &AliTTS{} var DefaultTTS = &AliTTS{}
// onTaskFailed 识别过程中的错误处理回调参数 // onTaskFailed TTS 合成失败回调
func (tts *AliTTS) onTaskFailed(text string, param interface{}) { func (tts *AliTTS) onTaskFailed(text string, param interface{}) {
p, _ := param.(*result) p, _ := param.(*result)
p.Error = fmt.Errorf("语音合成异常: %v", text) p.Error = fmt.Errorf("语音合成异常: %v", text)
} }
// onSynthesisResult 语音合成数据回调参数 // onSynthesisResult TTS 合成数据回调
func (tts *AliTTS) onSynthesisResult(data []byte, param interface{}) { func (tts *AliTTS) onSynthesisResult(data []byte, param interface{}) {
p, _ := param.(*result) p, _ := param.(*result)
p.Data.Write(data) p.Data.Write(data)
} }
func (tts *AliTTS) Sound(text string) { // Sound 播放 TTS
// 如果已有 TTS 在播放,会等待当前播放完成后再播放新的
func (tts *AliTTS) Sound(ctx context.Context, text string) {
if text == "" { if text == "" {
return return
} }
buf, err := tts.Get(text)
if err == nil && buf != nil { zap.S().Infof("[TTS] 开始播放: %s", text)
audio.PlayWav(tts.ctx, buf)
} else { buf, err := tts.Get(ctx, text)
zap.S().Errorln("AliTTS 请求异常: ", err) if err != nil {
zap.S().Errorw("[TTS] 合成失败", "text", text, "error", err)
return
} }
size := buf.(*bytes.Buffer).Len()
zap.S().Debugf("[TTS] 合成成功: %s (%d字节)", text, size)
// 获取锁,阻塞等待直到可以播放
zap.S().Debugf("[TTS] 等待播放锁: %s", text)
tts.mu.Lock()
defer tts.mu.Unlock()
audio.PlayWav(ctx, buf)
// 检查是否被取消
if ctx.Err() != nil {
zap.S().Debugf("[TTS] 播放被取消: %s", text)
return
}
zap.S().Infof("[TTS] 播放完成: %s", text)
} }
// getToken 获取阿里云 TTS Token
func (tts *AliTTS) getToken() error { func (tts *AliTTS) getToken() error {
// Token 未过期则复用
if tts.tokenResult.ExpireTime != 0 && time.Unix(tts.tokenResult.ExpireTime, 0).After(time.Now()) { if tts.tokenResult.ExpireTime != 0 && time.Unix(tts.tokenResult.ExpireTime, 0).After(time.Now()) {
return nil return nil
} }
tts.tokenResult = nls.TokenResult{} tts.tokenResult = nls.TokenResult{}
resultMessage, err := nls.GetToken("cn-shanghai", "nls-meta.cn-shanghai.aliyuncs.com", tts.AccessKeyID, tts.AccessKeySecret, "2019-02-28") resultMessage, err := nls.GetToken("cn-shanghai", "nls-meta.cn-shanghai.aliyuncs.com", tts.AccessKeyID, tts.AccessKeySecret, "2019-02-28")
if err != nil { if err != nil {
return err return err
} else if resultMessage.ErrMsg != "" {
zap.S().Errorf("获取Token失败: %s", resultMessage.ErrMsg)
return errorsx.ThirdPartyErr
} }
if resultMessage.ErrMsg != "" {
return fmt.Errorf("获取Token失败: %s", resultMessage.ErrMsg)
}
tts.tokenResult = resultMessage.TokenResult tts.tokenResult = resultMessage.TokenResult
return nil return nil
} }
func (tts *AliTTS) Get(text string) (io.Reader, error) { // Get 合成语音文本(内部方法)
func (tts *AliTTS) Get(ctx context.Context, text string) (io.Reader, error) {
param := nls.DefaultSpeechSynthesisParam() param := nls.DefaultSpeechSynthesisParam()
param.Volume = tts.Volume param.Volume = tts.Volume
param.Voice = tts.Voice param.Voice = tts.Voice
param.SpeechRate = tts.SpeechRate param.SpeechRate = tts.SpeechRate
err := tts.getToken() if err := tts.getToken(); err != nil {
if err != nil {
return nil, err return nil, err
} }
connectConfig := nls.NewConnectionConfigWithToken(nls.DEFAULT_URL, tts.AppKey, tts.tokenResult.Id) connectConfig := nls.NewConnectionConfigWithToken(nls.DEFAULT_URL, tts.AppKey, tts.tokenResult.Id)
logger := nls.NewNlsLogger(leaf.DefaultWriter, "", log.LstdFlags|log.Ltime) logger := nls.NewNlsLogger(leaf.DefaultWriter, "", log.LstdFlags|log.Ltime)
logger.SetLogSil(false) logger.SetLogSil(false)
logger.SetDebug(true) logger.SetDebug(true)
@@ -105,25 +133,29 @@ func (tts *AliTTS) Get(text string) (io.Reader, error) {
return ttsData.Data, err return ttsData.Data, err
} }
// 等待语音合成结束 // 等待合成完成
select { select {
case done := <-ch: case done := <-ch:
{ if !done {
if !done { return ttsData.Data, fmt.Errorf("TTS合成失败")
return ttsData.Data, errorsx.ThirdPartyErr
}
return ttsData.Data, nil
} }
size := ttsData.Data.(*bytes.Buffer).Len()
if size == 0 {
return ttsData.Data, fmt.Errorf("TTS合成数据为空")
}
return ttsData.Data, nil
case <-time.After(time.Duration(tts.Timeout) * time.Second): case <-time.After(time.Duration(tts.Timeout) * time.Second):
return ttsData.Data, errorsx.DriverTimeoutErr return ttsData.Data, fmt.Errorf("TTS合成超时")
case <-tts.ctx.Done():
return ttsData.Data, errorsx.DriverCancelErr case <-ctx.Done():
return ttsData.Data, fmt.Errorf("请求被取消")
} }
} }
func New(ctx context.Context, config config.AliyunConfig) *AliTTS { // New 创建 TTS 实例
func New(config config.AliyunConfig) *AliTTS {
return &AliTTS{ return &AliTTS{
ctx: ctx,
AliyunConfig: config, AliyunConfig: config,
} }
} }

View File

@@ -3,12 +3,26 @@ package utils
import ( import (
"os" "os"
"os/exec" "os/exec"
"sync"
) )
var (
xsetBin string
once sync.Once
)
func init() {
once.Do(func() {
if found, err := exec.LookPath("xset"); err == nil {
xsetBin = found
}
})
}
// BlankOpen 打开屏幕 // BlankOpen 打开屏幕
func BlankOpen() { func BlankOpen() {
if found, err := exec.LookPath("xset"); err == nil { if xsetBin != "" {
exec.Command(found, "dpms", "force", "on").Run() exec.Command(xsetBin, "dpms", "force", "on").Run()
return return
} }
os.WriteFile("/sys/class/graphics/fb0/blank", []byte("0"), 0644) os.WriteFile("/sys/class/graphics/fb0/blank", []byte("0"), 0644)
@@ -16,8 +30,8 @@ func BlankOpen() {
// BlankClose 关闭屏幕 // BlankClose 关闭屏幕
func BlankClose() { func BlankClose() {
if found, err := exec.LookPath("xset"); err == nil { if xsetBin != "" {
exec.Command(found, "dpms", "force", "off").Run() exec.Command(xsetBin, "dpms", "force", "off").Run()
return return
} }
os.WriteFile("/sys/class/graphics/fb0/blank", []byte("1"), 0644) os.WriteFile("/sys/class/graphics/fb0/blank", []byte("1"), 0644)

14
todo.md
View File

@@ -4,10 +4,14 @@
```bash ```bash
# 在 systemd-networkd-wait-online.service Service 加入 TimeoutStartSec=2sec # 在 systemd-networkd-wait-online.service Service 加入 TimeoutStartSec=2sec
sudo vim /etc/systemd/system/network-online.target.wants/systemd-networkd-wait-online.service sudo EDITOR=vim systemctl edit systemd-networkd-wait-online.service
# 在打开的编辑器中添加:
# [Service]
# TimeoutStartSec=2sec
``` ```
### 配置时区 ### 配置时区
```bash ```bash
sudo timedatectl set-timezone Asia/Shanghai sudo timedatectl set-timezone Asia/Shanghai
``` ```
@@ -107,10 +111,14 @@ fi
### 自动登录 ### 自动登录
编辑 `/etc/systemd/system/getty.target.wants/getty@tty1.service` 文件,将 `ExecStart` 行修改为 使用 systemctl edit 修改 getty@tty1 服务
```bash ```bash
ExecStart=-/sbin/agetty --autologin <your_username> --noclear %I $TERM sudo EDITOR=vim systemctl edit getty@tty1.service
# 在打开的编辑器中添加:
# [Service]
# ExecStart=
# ExecStart=-/sbin/agetty --autologin <your_username> --noclear %I $TERM
``` ```
其中: 其中: