Sfoglia il codice sorgente

feat:EMS参数字典改为部署时生成,进程启动加载到Redis,缺失时自动重新加载

- 部署脚本调用 deploy_emspara.sh:由 vid_macro.h / pid_macro.h 生成 emspara.db 并拷贝到部署目录
- 主服务与 fwh-agent-mcp 启动时加载字典到 Redis;先写临时 key 再 RENAME,替换原子
- 查询时 Redis 中没有字典则自动从 emspara.db 重新加载(每分钟最多一次)
- 生成命令每次全新生成、批量写入;兼容头文件中的 UNIT32 笔误和宏名后带空格,补回 VID 62 条、PID 14 条
- 仓库中不再提交 emspara.db

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jerry Liang 5 giorni fa
parent
commit
a6c4481d4b

+ 5 - 0
.gitignore

@@ -32,3 +32,8 @@ coverage
 
 test-results/
 playwright-report/
+
+# EMS 参数字典:部署时由 service/cmd/deploy_emspara.sh 生成
+service/emspara.db
+service/emspara.db.tmp
+service/utils/emspara/emspara.db

+ 1 - 0
deploy/agent-assistant/SETUP.md

@@ -19,6 +19,7 @@
 | `config.env` | 主服务 `fwh-toolbox-server` | 第 6 节的 `AGENT_*` 加在这里。**不要新建 `.env`**:程序优先读取 `./.env`,存在时 `config.env` 整个不再生效 |
 | `config.agent-mcp.env` | `fwh-agent-mcp` | 独立配置(第 1.1 节),不读 `config.env` |
 | `config.mcp.env` | 旧 Kiro MCP | 与新助手无关 |
+| `emspara.db` | 主服务、`fwh-agent-mcp` | EMS 参数(PID / VID)字典。部署脚本由 EMS 头文件生成并拷贝到这里(`service/cmd/deploy_emspara.sh`,`deploy_toolbox.sh` 与 `deploy_agent_mcp.sh` 都会调用);进程启动时加载到 Redis,Redis 中没有字典时自动重新加载。缺失时 `ems_param_lookup` 查不到任何编号 |
 
 ## 1. 部署 fwh-agent-mcp
 

+ 3 - 0
service/agentmcp/bootstrap.go

@@ -18,6 +18,7 @@ import (
 	"fwh-toolbox-server/repositories"
 	task_core "fwh-toolbox-server/task/core"
 	"fwh-toolbox-server/task/queue/fwhQueue"
+	"fwh-toolbox-server/utils/emspara"
 )
 
 // DefaultConfigFile fwh-agent-mcp 的独立配置文件(相对工作目录),与主服务的 config.env、旧 MCP 的 config.mcp.env 互不共用。
@@ -104,6 +105,8 @@ func InitInfra(configFile string) error {
 		return fmt.Errorf("数据库初始化失败: %w", err)
 	}
 	cache.Init(cfg.App.RedisAddr)
+	// ems_param_lookup 依赖的 EMS 参数字典(部署目录的 emspara.db -> Redis),与主服务相同
+	emspara.Init(emspara.DefaultDBPath)
 	if err := repositories.Init(context.Background()); err != nil {
 		return fmt.Errorf("Repository 初始化失败: %w", err)
 	}

+ 3 - 0
service/cmd/deploy_agent_mcp.sh

@@ -22,6 +22,9 @@ echo "📦 Copying binary to $MCP_PATH..."
 sudo mv $MCP_NAME $MCP_PATH
 sudo chmod +x $MCP_PATH
 
+# ===== 生成并拷贝 EMS 参数字典(ems_param_lookup 使用,启动时加载到 Redis) =====
+bash "$(dirname "$0")/deploy_emspara.sh"
+
 # ===== 安装 / 更新 systemd 单元 =====
 if [ -f "$UNIT_SRC" ]; then
   if ! sudo cmp -s "$UNIT_SRC" "$UNIT_DST"; then

+ 28 - 0
service/cmd/deploy_emspara.sh

@@ -0,0 +1,28 @@
+#!/bin/bash
+set -e
+
+# 生成 EMS 参数字典(PID / VID)SQLite 并拷贝到部署目录。
+# 主服务与 fwh-agent-mcp 启动时把它加载到 Redis(utils/emspara),Redis 中没有字典时也会从这里重新加载。
+# 由 deploy_toolbox.sh、deploy_agent_mcp.sh 调用,也可以在 service/ 目录下单独执行:./cmd/deploy_emspara.sh
+# 头文件更新:替换 utils/emspara/updateCmd/vid_macro.h、pid_macro.h 后重新部署(或执行本脚本后重启服务)。
+
+# ===== 配置部分 =====
+APP_DIR="/var/www/fwh-toolbox/server"
+DB_NAME="emspara.db"
+DB_PATH="$APP_DIR/$DB_NAME"
+
+SERVICE_DIR="$(cd "$(dirname "$0")/.." && pwd)"
+cd "$SERVICE_DIR"
+
+# ===== 生成字典 =====
+echo "📚 Generating $DB_NAME..."
+go run ./utils/emspara/updateCmd -out "./$DB_NAME"
+
+# ===== 拷贝到部署目录(先拷临时文件再改名,运行中的进程不会读到写了一半的文件) =====
+echo "📦 Copying $DB_NAME to $DB_PATH..."
+sudo cp "./$DB_NAME" "$DB_PATH.tmp"
+sudo chmod 644 "$DB_PATH.tmp"
+sudo mv "$DB_PATH.tmp" "$DB_PATH"
+rm -f "./$DB_NAME"
+
+echo "✅ $DB_NAME deployed to $DB_PATH"

+ 3 - 0
service/cmd/deploy_toolbox.sh

@@ -16,6 +16,9 @@ echo "📦 Copying binary to $APP_PATH..."
 sudo mv $APP_NAME $APP_PATH
 sudo chmod +x $APP_PATH
 
+# ===== 生成并拷贝 EMS 参数字典(启动时加载到 Redis) =====
+bash "$(dirname "$0")/deploy_emspara.sh"
+
 # ===== 安装 systemd 服务(如果不存在) =====
 if [ ! -f "$SERVICE_FILE" ]; then
     echo "📝 Creating systemd service: $SERVICE_FILE"

+ 4 - 0
service/main.go

@@ -13,6 +13,7 @@ import (
 	task_core "fwh-toolbox-server/task/core"
 	"fwh-toolbox-server/task/queue/faultRecordQueue"
 	"fwh-toolbox-server/utils/casdoor"
+	"fwh-toolbox-server/utils/emspara"
 	"fwh-toolbox-server/utils/minio"
 	"log"
 	"net"
@@ -58,6 +59,9 @@ func main() {
 	// 初始化缓存
 	cache.Init(cfg.App.RedisAddr)
 
+	// 加载 EMS 参数字典(部署目录的 emspara.db -> Redis)
+	emspara.Init(emspara.DefaultDBPath)
+
 	// 初始化Minio
 	minio.InitClient(&cfg.App)
 

+ 99 - 40
service/repositories/redis/emsparaRepo.go

@@ -3,12 +3,17 @@ package redis
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"fwh-toolbox-server/cache"
 	"fwh-toolbox-server/domain"
+	"os"
+	"strconv"
+	"sync"
 	"time"
 
 	rds "github.com/redis/go-redis/v9"
+	"go.uber.org/zap"
 )
 
 var (
@@ -18,48 +23,73 @@ var (
 	emsparaPidKey    = emsparaKeyPrefix + "PID"
 )
 
+// emsparaReloadInterval 字典缺失时两次自动重新加载的最小间隔,避免加载失败时每次查询都去读 SQLite
+const emsparaReloadInterval = time.Minute
+
+var (
+	emsparaLoader     func() error // 由 utils/emspara.Init 注册:SQLite -> Redis
+	emsparaReloadMu   sync.Mutex
+	emsparaLastReload time.Time
+)
+
+// SetEmsparaLoader 注册字典加载函数。查询时发现 Redis 中没有字典(整个 key 不存在)会调用它重新加载。
+// 由 utils/emspara 注册,避免 repositories 反向依赖 SQLite。
+func SetEmsparaLoader(fn func() error) {
+	emsparaReloadMu.Lock()
+	defer emsparaReloadMu.Unlock()
+	emsparaLoader = fn
+}
+
+// UpdateEmspara 用 SQLite 中的字典整体替换 Redis 中的字典。
+// 先写入本进程专用的临时 key,再 RENAME 为正式 key:替换是原子的,读取方不会看到空字典或半份字典,
+// 多个进程(主服务、fwh-agent-mcp)同时加载也不会互相覆盖写到一半的数据。
 func UpdateEmspara(vidMacros []domain.ParamsVidMacroModel, pidMacros []domain.ParamsPidMacroModel) error {
-	ctx, cancel := context.WithTimeout(emsparaCtx, 5*time.Second)
-	defer cancel()
+	if len(vidMacros) == 0 || len(pidMacros) == 0 {
+		return fmt.Errorf("EMS 参数字典为空(VID %d 条,PID %d 条),不替换 Redis 中的数据", len(vidMacros), len(pidMacros))
+	}
 
-	_, err := cache.Pipeline(ctx, func(p rds.Pipeliner) error {
-		p.Del(ctx, emsparaVidKey) // 清空VID原数据
-		p.Del(ctx, emsparaPidKey) // 清空PID原数据
-
-		// 插入VID数据
-		for _, m := range vidMacros {
-			val, err := json.Marshal(map[string]string{
-				"name": m.MacroName,
-				"desc": m.Description,
-			})
-			if err != nil {
-				return err
-			}
-			p.HSet(ctx, emsparaVidKey, m.ID, val)
+	vid := make(map[string]any, len(vidMacros))
+	for _, m := range vidMacros {
+		val, err := json.Marshal(map[string]string{"name": m.MacroName, "desc": m.Description})
+		if err != nil {
+			return err
 		}
-
-		// 插入PID数据
-		for _, m := range pidMacros {
-			val, err := json.Marshal(map[string]string{
-				"name": m.MacroName,
-				"desc": m.Description,
-			})
-			if err != nil {
-				return err
-			}
-			p.HSet(ctx, emsparaPidKey, m.ID, val)
+		vid[strconv.FormatUint(uint64(m.ID), 10)] = val
+	}
+	pid := make(map[string]any, len(pidMacros))
+	for _, m := range pidMacros {
+		val, err := json.Marshal(map[string]string{"name": m.MacroName, "desc": m.Description})
+		if err != nil {
+			return err
 		}
+		pid[strconv.FormatUint(uint64(m.ID), 10)] = val
+	}
 
+	ctx, cancel := context.WithTimeout(emsparaCtx, 30*time.Second)
+	defer cancel()
+
+	suffix := fmt.Sprintf(":loading:%d:%d", os.Getpid(), time.Now().UnixNano())
+	_, err := cache.Pipeline(ctx, func(p rds.Pipeliner) error {
+		for key, data := range map[string]map[string]any{emsparaVidKey: vid, emsparaPidKey: pid} {
+			tmp := key + suffix
+			p.HSet(ctx, tmp, data)
+			// 写到一半失败时临时 key 自动过期;RENAME 会带上 TTL,替换后再 PERSIST
+			p.Expire(ctx, tmp, 10*time.Minute)
+			p.Rename(ctx, tmp, key)
+			p.Persist(ctx, key)
+		}
 		return nil
 	})
 	return err
 }
 
-func GetEmsparaVidInfo(id uint) (string, string, error) {
-	ctx, cancel := context.WithTimeout(emsparaCtx, 5*time.Second)
-	defer cancel()
-
-	v, err := cache.HGet(ctx, emsparaVidKey, fmt.Sprintf("%d", id))
+// emsparaInfo 按编号查询字典;字典整个不存在时触发一次重新加载后重试
+func emsparaInfo(key string, id uint) (string, string, error) {
+	field := strconv.FormatUint(uint64(id), 10)
+	v, err := emsparaGet(key, field)
+	if errors.Is(err, rds.Nil) && emsparaReloadIfMissing(key) {
+		v, err = emsparaGet(key, field)
+	}
 	if err != nil {
 		return "", "", err
 	}
@@ -71,18 +101,47 @@ func GetEmsparaVidInfo(id uint) (string, string, error) {
 	return m["name"], m["desc"], nil
 }
 
-func GetEmsparaPidInfo(id uint) (string, string, error) {
+func emsparaGet(key, field string) (string, error) {
 	ctx, cancel := context.WithTimeout(emsparaCtx, 5*time.Second)
 	defer cancel()
+	return cache.HGet(ctx, key, field)
+}
 
-	v, err := cache.HGet(ctx, emsparaPidKey, fmt.Sprintf("%d", id))
-	if err != nil {
-		return "", "", err
+// emsparaReloadIfMissing 字典 key 不存在时重新加载;返回是否已重新加载成功。
+// 只是单个编号不在字典中(key 存在)时不加载。
+func emsparaReloadIfMissing(key string) bool {
+	if exists, err := emsparaExists(key); err != nil || exists {
+		return false
 	}
 
-	var m map[string]string
-	if err := json.Unmarshal([]byte(v), &m); err != nil {
-		return "", "", err
+	emsparaReloadMu.Lock()
+	defer emsparaReloadMu.Unlock()
+	// 等锁期间可能已被其他查询重新加载
+	if exists, err := emsparaExists(key); err == nil && exists {
+		return true
 	}
-	return m["name"], m["desc"], nil
+	if emsparaLoader == nil || time.Since(emsparaLastReload) < emsparaReloadInterval {
+		return false
+	}
+	emsparaLastReload = time.Now()
+	if err := emsparaLoader(); err != nil {
+		zap.L().Error("[emspara] Redis 中没有 EMS 参数字典,重新加载失败", zap.String("key", key), zap.Error(err))
+		return false
+	}
+	zap.L().Info("[emspara] Redis 中没有 EMS 参数字典,已重新加载", zap.String("key", key))
+	return true
+}
+
+func emsparaExists(key string) (bool, error) {
+	ctx, cancel := context.WithTimeout(emsparaCtx, 5*time.Second)
+	defer cancel()
+	return cache.Exists(ctx, key)
+}
+
+func GetEmsparaVidInfo(id uint) (string, string, error) {
+	return emsparaInfo(emsparaVidKey, id)
+}
+
+func GetEmsparaPidInfo(id uint) (string, string, error) {
+	return emsparaInfo(emsparaPidKey, id)
 }

+ 1 - 0
service/repositories/repositories.go

@@ -475,6 +475,7 @@ var UpdateBatchDownloadCallFileResult = redis.UpdateCallFileResult
 
 // EMS Para
 var UpdateEmspara = redis.UpdateEmspara
+var SetEmsparaLoader = redis.SetEmsparaLoader
 var GetEmsparaVidInfo = redis.GetEmsparaVidInfo
 var GetEmsparaPidInfo = redis.GetEmsparaPidInfo
 

BIN
service/utils/emspara/emspara.db


+ 34 - 12
service/utils/emspara/emspara.go

@@ -1,25 +1,51 @@
+// Package emspara EMS 参数(PID / VID)字典:SQLite -> Redis。
+//
+// 字典的生成与使用:
+//  1. 部署时由 updateCmd 从 EMS 头文件 vid_macro.h / pid_macro.h 生成 SQLite,拷贝到部署目录(cmd/deploy_emspara.sh);
+//  2. 进程启动时调用 Init,把 SQLite 整体加载到 Redis(主服务与 fwh-agent-mcp 都会加载,替换是原子的);
+//  3. 查询走 Redis(repositories.GetEmsparaVidInfo / GetEmsparaPidInfo);发现 Redis 中没有字典时自动重新加载。
 package emspara
 
 import (
 	"fmt"
 	"fwh-toolbox-server/domain"
 	"fwh-toolbox-server/repositories"
+	"os"
 
+	"go.uber.org/zap"
 	"gorm.io/driver/sqlite"
 	"gorm.io/gorm"
+	"gorm.io/gorm/logger"
 )
 
+// DefaultDBPath 字典 SQLite 的默认位置(相对工作目录,即部署目录 /var/www/fwh-toolbox/server)
+const DefaultDBPath = "./emspara.db"
+
+// Init 启动时加载字典到 Redis,并注册“Redis 中没有字典时重新加载”的回调。
+// 加载失败只记录日志,不影响进程启动:查询时会再次尝试。
+func Init(dbPath string) {
+	repositories.SetEmsparaLoader(func() error { return SyncRedis(dbPath) })
+	if err := SyncRedis(dbPath); err != nil {
+		zap.L().Error("[emspara] 加载 EMS 参数字典到 Redis 失败", zap.String("db", dbPath), zap.Error(err))
+		return
+	}
+	zap.L().Info("[emspara] EMS 参数字典已加载到 Redis", zap.String("db", dbPath))
+}
+
 // SyncRedis 全量同步 SQLite -> Redis
-func SyncRedis() error {
-	// 打开 SQLite
-	db, err := gorm.Open(sqlite.Open("./utils/emspara/emspara.db"), &gorm.Config{})
+func SyncRedis(dbPath string) error {
+	// 文件不存在时 SQLite 会新建空库,先检查,给出明确的错误
+	if _, err := os.Stat(dbPath); err != nil {
+		return fmt.Errorf("字典文件不可用(部署时由 cmd/deploy_emspara.sh 生成): %w", err)
+	}
+
+	// 只读打开,不建表、不写入
+	db, err := gorm.Open(sqlite.Open("file:"+dbPath+"?mode=ro"), &gorm.Config{Logger: logger.Discard})
 	if err != nil {
 		return fmt.Errorf("failed to connect database: %w", err)
 	}
-
-	// 自动建表
-	if err := db.AutoMigrate(&domain.ParamsVidMacroModel{}, &domain.ParamsPidMacroModel{}); err != nil {
-		return err
+	if sqlDB, err := db.DB(); err == nil {
+		defer sqlDB.Close()
 	}
 
 	var vidMacros []domain.ParamsVidMacroModel
@@ -32,9 +58,5 @@ func SyncRedis() error {
 		return err
 	}
 
-	if err := repositories.UpdateEmspara(vidMacros, pidMacros); err != nil {
-		return err
-	}
-
-	return nil
+	return repositories.UpdateEmspara(vidMacros, pidMacros)
 }

+ 82 - 55
service/utils/emspara/updateCmd/updateEmspara.go

@@ -2,6 +2,7 @@ package main
 
 import (
 	"bufio"
+	"flag"
 	"fmt"
 	"fwh-toolbox-server/domain"
 	"os"
@@ -12,6 +13,7 @@ import (
 	"gorm.io/driver/sqlite"
 	"gorm.io/gorm"
 	"gorm.io/gorm/clause"
+	"gorm.io/gorm/logger"
 )
 
 type MacroItem struct {
@@ -22,62 +24,92 @@ type MacroItem struct {
 	Description string // 描述
 }
 
-// 更新vid_macro.h和pid_macro.h的数据到sqlite
+// 由 vid_macro.h 和 pid_macro.h 生成 EMS 参数字典 SQLite(每次全新生成,不保留旧数据)。
+// 在 service/ 目录下执行:go run ./utils/emspara/updateCmd -out ./emspara.db
+// 部署时由 cmd/deploy_emspara.sh 调用,生成后拷贝到部署目录,进程启动时加载到 Redis(utils/emspara)。
 func main() {
-	// 打开 GBK 编码的文件
-	vidFile, err := os.Open("./utils/emspara/updateCmd/vid_macro.h")
+	vidPath := flag.String("vid", "./utils/emspara/updateCmd/vid_macro.h", "VID 头文件")
+	pidPath := flag.String("pid", "./utils/emspara/updateCmd/pid_macro.h", "PID 头文件")
+	outPath := flag.String("out", "./emspara.db", "输出的 SQLite 文件")
+	flag.Parse()
+
+	vidCount, pidCount, err := generate(*vidPath, *pidPath, *outPath)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "生成 EMS 参数字典失败: %v\n", err)
+		os.Exit(1)
+	}
+	fmt.Printf("已生成 %s:VID %d 条,PID %d 条\n", *outPath, vidCount, pidCount)
+}
+
+// generate 先写入同目录的临时文件,全部成功后再改名替换 outPath,失败时不影响已有文件
+func generate(vidPath, pidPath, outPath string) (int64, int64, error) {
+	vidFile, err := os.Open(vidPath)
 	if err != nil {
-		panic(err)
+		return 0, 0, err
 	}
 	defer vidFile.Close()
 
-	pidFile, err := os.Open("./utils/emspara/updateCmd/pid_macro.h")
+	pidFile, err := os.Open(pidPath)
 	if err != nil {
-		panic(err)
+		return 0, 0, err
 	}
 	defer pidFile.Close()
 
-	// 打开 SQLite
-	db, err := gorm.Open(sqlite.Open("./utils/emspara/emspara.db"), &gorm.Config{})
+	tmpPath := outPath + ".tmp"
+	_ = os.Remove(tmpPath)
+	defer os.Remove(tmpPath)
+
+	db, err := gorm.Open(sqlite.Open(tmpPath), &gorm.Config{Logger: logger.Discard})
 	if err != nil {
-		panic(err)
+		return 0, 0, err
+	}
+	if err := fillMacros(db, vidFile, pidFile); err != nil {
+		return 0, 0, err
 	}
 
-	// 自动建表
-	if err := db.AutoMigrate(&domain.ParamsVidMacroModel{}, &domain.ParamsPidMacroModel{}); err != nil {
-		panic(err)
+	var vidCount, pidCount int64
+	db.Model(&domain.ParamsVidMacroModel{}).Count(&vidCount)
+	db.Model(&domain.ParamsPidMacroModel{}).Count(&pidCount)
+	if sqlDB, err := db.DB(); err == nil {
+		sqlDB.Close()
 	}
+	if vidCount == 0 || pidCount == 0 {
+		return 0, 0, fmt.Errorf("没有解析到参数(VID %d 条,PID %d 条),请检查头文件格式", vidCount, pidCount)
+	}
+	return vidCount, pidCount, os.Rename(tmpPath, outPath)
+}
 
-	// 处理vid文件
-	err = parserMacroFile(vidFile, func(line string) error {
-		item, err := parserMacroLine(line)
-		if err != nil {
-			return err
-		}
+func fillMacros(db *gorm.DB, vidFile, pidFile *os.File) error {
+	if err := db.AutoMigrate(&domain.ParamsVidMacroModel{}, &domain.ParamsPidMacroModel{}); err != nil {
+		return err
+	}
 
-		// 非 macro 数据行
-		if item == nil {
-			return nil
-		}
+	// 先解析为列表(同一编号以最后出现的为准),再分表批量写入
+	var vids []domain.ParamsVidMacroModel
+	if err := parserMacroFile(vidFile, macroHandler(func(it MacroItem) {
+		vids = append(vids, domain.ParamsVidMacroModel{ID: it.TotalNo, MacroName: it.MacroName, Description: it.Description})
+	})); err != nil {
+		return fmt.Errorf("解析 VID 头文件: %w", err)
+	}
+	var pids []domain.ParamsPidMacroModel
+	if err := parserMacroFile(pidFile, macroHandler(func(it MacroItem) {
+		pids = append(pids, domain.ParamsPidMacroModel{ID: it.TotalNo, MacroName: it.MacroName, Description: it.Description})
+	})); err != nil {
+		return fmt.Errorf("解析 PID 头文件: %w", err)
+	}
 
-		// 检查多组数据,如PE、MPPT、APbox2.0等
-		itemList := checkMacroDataGroup(*item)
-		for _, it := range itemList {
-			if it.TotalNo == 0 {
-				// 0无法插入,抛弃
-				continue
-			}
-			// 插入VID数据
-			db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&domain.ParamsVidMacroModel{ID: it.TotalNo, MacroName: it.MacroName, Description: it.Description})
+	return db.Transaction(func(tx *gorm.DB) error {
+		// 每次写入都从 tx 新建链式调用:复用同一个带 Clauses 的 *gorm.DB 会共用 Statement,后一张表的数据会写进前一张表
+		if err := tx.Clauses(clause.OnConflict{UpdateAll: true}).CreateInBatches(vids, 500).Error; err != nil {
+			return err
 		}
-		return nil
+		return tx.Clauses(clause.OnConflict{UpdateAll: true}).CreateInBatches(pids, 500).Error
 	})
-	if err != nil {
-		panic(err)
-	}
+}
 
-	// 处理pid文件
-	err = parserMacroFile(pidFile, func(line string) error {
+// macroHandler 解析一行宏定义,按分组(PE、MPPT、APbox2.0 等)展开后逐个交给 add
+func macroHandler(add func(it MacroItem)) func(line string) error {
+	return func(line string) error {
 		item, err := parserMacroLine(line)
 		if err != nil {
 			return err
@@ -88,20 +120,14 @@ func main() {
 			return nil
 		}
 
-		// 检查多组数据,如PE、MPPT、APbox2.0等
-		itemList := checkMacroDataGroup(*item)
-		for _, it := range itemList {
+		for _, it := range checkMacroDataGroup(*item) {
 			if it.TotalNo == 0 {
 				// 0无法插入,抛弃
 				continue
 			}
-			// 插入VID数据
-			db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&domain.ParamsPidMacroModel{ID: it.TotalNo, MacroName: it.MacroName, Description: it.Description})
+			add(it)
 		}
 		return nil
-	})
-	if err != nil {
-		panic(err)
 	}
 }
 
@@ -113,7 +139,6 @@ func parserMacroFile(file *os.File, fn func(line string) error) error {
 	for scanner.Scan() {
 		line := scanner.Text()         // 已经是 UTF-8 的字符串
 		line = strings.TrimSpace(line) // 去掉前后空格、制表符和换行符
-		fmt.Println("原始行:", line)
 
 		if err := fn(line); err != nil {
 			fmt.Printf("[Warn]\nline: %s\nerror: %s\n", line, err.Error())
@@ -123,6 +148,8 @@ func parserMacroFile(file *os.File, fn func(line string) error) error {
 	return scanner.Err()
 }
 
+var macroLineRe = regexp.MustCompile(`^(\w+)\s*,\s*///<\s*\((\d+)/(\d+)\)\s*(UINT32|UNIT32|INT32|FLOAT|CHAR\*|None)\s+(.*)$`)
+
 func parserMacroLine(line string) (*MacroItem, error) {
 	if strings.HasPrefix(line, "VID_") || strings.HasPrefix(line, "PID_") {
 		// macro 数据
@@ -132,10 +159,10 @@ func parserMacroLine(line string) (*MacroItem, error) {
 		// 1. ^(\w+),           						-> 宏名 + 逗号
 		// 2. \s*///<\s*        						-> 注释标记,空格可有可无
 		// 3. \((\d+)/(\d+)\)   						-> 括号中的两个数字
-		// 4. (UINT32|INT32|FLOAT|CHAR\*)   -> 类型
+		// 4. (UINT32|...|None)              -> 类型;头文件中有 UNIT32 笔误,按 UINT32 处理;None 为 *_MAX_NUM 等占位行
 		// 5. (.*)$             						-> 剩余描述
-		re := regexp.MustCompile(`^(\w+),\s*///<\s*\((\d+)/(\d+)\)\s*(UINT32|INT32|FLOAT|CHAR\*)\s+(.*)$`)
-		matches := re.FindStringSubmatch(line)
+		// 宏名与逗号之间允许有空格(头文件中存在 "PID_AWS_4G_SWITCH  ," 这样的写法)
+		matches := macroLineRe.FindStringSubmatch(line)
 
 		if len(matches) == 6 {
 			macroName := matches[1]
@@ -143,12 +170,12 @@ func parserMacroLine(line string) (*MacroItem, error) {
 			totalNoStr := matches[3]
 			typ := matches[4]
 			desc := matches[5]
-
-			fmt.Println("宏名:", macroName)
-			fmt.Println("子序号:", subNoStr)
-			fmt.Println("总序号:", totalNoStr)
-			fmt.Println("类型:", typ)
-			fmt.Println("描述:", desc)
+			if typ == "None" {
+				return nil, nil
+			}
+			if typ == "UNIT32" {
+				typ = "UINT32"
+			}
 
 			subNo, err := strconv.ParseUint(subNoStr, 10, 64)
 			if err != nil {