package model import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "sync" ) type Repository struct { models []Model mu sync.RWMutex } // NewRepository 載入 models.json 的內建模型目錄。 // // models.json 會列出所有「產品支援」的模型定義,但安裝包不一定會帶上每個 // 對應的 .nef(見 Makefile 的 BUNDLED_NEFS 白名單)。因此載入後會過濾掉 // .nef 檔案實際不存在的 model —— 否則使用者會在 UI 看到選不了的模型,選下去 // 才在 flash 階段拿到 "model file not found" 這種沒頭沒尾的錯誤。 // // 過濾只作用在 models.json 的內建模型。使用者上傳的自訂模型走 Add(), // 路徑是絕對路徑且必定存在,不受影響。 func NewRepository(dataPath string) *Repository { r := &Repository{} data, err := os.ReadFile(dataPath) if err != nil { fmt.Printf("Warning: could not load models from %s: %v\n", dataPath, err) return r } var declared []Model if err := json.Unmarshal(data, &declared); err != nil { fmt.Printf("Warning: could not parse models JSON: %v\n", err) return r } r.models = filterAvailableModels(declared, filepath.Dir(dataPath)) return r } // filterAvailableModels 只保留 .nef 檔案實際存在的 model。 // // dataDir 是 models.json 所在的目錄(即 bundle 內的 data/),models.json 的 // filePath 以它為基準解析。 func filterAvailableModels(models []Model, dataDir string) []Model { available := make([]Model, 0, len(models)) for _, m := range models { path := resolveBuiltInModelPath(m.FilePath, dataDir) // 沒宣告 filePath 的 model 不做檔案檢查(沒有東西可以檢查),保留原行為。 if path == "" { available = append(available, m) continue } if info, err := os.Stat(path); err != nil || info.IsDir() { fmt.Printf("[INFO] Skipping model %q (%s): .nef not bundled at %s\n", m.ID, m.Name, path) continue } available = append(available, m) } return available } // resolveBuiltInModelPath 把 models.json 的 filePath 解析成實際的檔案路徑。 // // 規則與 flash.Service.StartFlash 一致: // - 絕對路徑 → 原樣使用 // - "data/nef/..." → 去掉 "data/" 前綴後接在 dataDir 之下 // (因為 dataDir 本身就是那個 data/ 目錄,不去掉會變成 data/data/nef/...) // - 其他相對路徑 → 直接接在 dataDir 之下 func resolveBuiltInModelPath(filePath, dataDir string) string { if filePath == "" { return "" } if filepath.IsAbs(filePath) { return filePath } if strings.HasPrefix(filePath, "data/") || strings.HasPrefix(filePath, "data\\") { return filepath.Join(dataDir, filePath[len("data/"):]) } return filepath.Join(dataDir, filePath) } func (r *Repository) List(filter ModelFilter) ([]ModelSummary, int) { r.mu.RLock() defer r.mu.RUnlock() var results []ModelSummary for _, m := range r.models { if filter.TaskType != "" && m.TaskType != filter.TaskType { continue } if filter.Hardware != "" { found := false for _, hw := range m.SupportedHardware { if hw == filter.Hardware { found = true break } } if !found { continue } } if filter.Query != "" { q := strings.ToLower(filter.Query) if !strings.Contains(strings.ToLower(m.Name), q) && !strings.Contains(strings.ToLower(m.Description), q) { continue } } results = append(results, m.ToSummary()) } return results, len(results) } func (r *Repository) GetByID(id string) (*Model, error) { r.mu.RLock() defer r.mu.RUnlock() for i := range r.models { if r.models[i].ID == id { return &r.models[i], nil } } return nil, fmt.Errorf("model not found: %s", id) } func (r *Repository) Count() int { r.mu.RLock() defer r.mu.RUnlock() return len(r.models) } func (r *Repository) Add(m Model) { r.mu.Lock() defer r.mu.Unlock() r.models = append(r.models, m) } func (r *Repository) Remove(id string) error { r.mu.Lock() defer r.mu.Unlock() for i := range r.models { if r.models[i].ID == id { if !r.models[i].IsCustom { return fmt.Errorf("cannot delete built-in model: %s", id) } r.models = append(r.models[:i], r.models[i+1:]...) return nil } } return fmt.Errorf("model not found: %s", id) }